用 CodeBuddy + DeepSeek,从零搭一个 24 小时在线的社区 AI 情感陪伴数字人
先搞清楚要解决什么问题
社区里的情绪陪伴需求,其实一直存在,但很难被覆盖到:
- 独居老人半夜睡不着,没有人说句话
- 青少年在学校受了委屈,不愿意跟家长开口
- 上班族加班到深夜,心里堵得慌,但心理咨询要预约、要花钱
社区不是没有资源——有社工、有心理咨询室。但社工要下班,咨询室周末关门。人和时间的缺口,才是真正的痛点。
所以这个项目的核心目标很简单:做一个"不会下班"的 AI 陪伴者,在这个缺口上兜个底。
最近做了一个0代码开发面向街道社区的小项目——一个 7×24 小时全天候在线社区情感陪伴具身交互智能体,给辖区居民提供免费、零门槛的情绪疏导。
这篇文章不写虚的,就讲讲我是怎么从 0 到 1 把它搭出来的。整个方案不需要写后端、不需要配服务器,核心就靠三个东西:CodeBuddy 编辑器 (Skill 技能) + DeepSeek 大模型 + 魔珐星云具身交互智能数字人
- CodeBuddy 编辑器 https://www.codebuddy.cn/home/
![[图片]](https://i-blog.csdnimg.cn/direct/bb4d2726b2694047b1e978657fb10c66.png)
2.DeepSeek 大模型 https://www.deepseek.com/![[图片]](https://i-blog.csdnimg.cn/direct/acdccfbc3e3f45cc8c266d396853531a.png)
3.数字人具身智能官网地址:https://xingyun3d.com?utm_campaign=daily&utm_source=CSDNwanfen3&utm_medium=&utm_term=&utm_content=![[图片]](https://i-blog.csdnimg.cn/direct/0054f0688db5495884805bdb7fc0cb78.png)
技术选型:为什么是这三个
CodeBuddy 编辑器:它是一个可视化的 AI 应用搭建工具。重点不是它能写代码,而是它内置了一个 Skill 技能调度系统。你可以把 Skill 理解成编辑器的"技能插件引擎"——它能帮你串联外部大模型、数字人 SDK、语音播报等不同模块,通过配置面板统一调度,不需要自己写业务代码。
DeepSeek 大模型:负责对话生成,整个数字人的"脑子"。所有给居民的回复都由它实时生成,不是预制的固定话术。
魔珐星云 数字人 SDK:提供全套多模态交互底层底座,依托参数流、端侧渲染核心技术,同步驱动语音、情绪微表情、肢体手势,形成一个有表情、有动作、能说话的具身智能 3D 数字人形象。
三者关系一句话讲清楚:CodeBuddy 的 Skill 做总调度,DeepSeek 做大脑,魔珐星云补齐完整具身交互智能。
0→1 搭建全流程
下面是我实际搭建的六个步骤,每一步都很具体。
第一步:新建 CodeBuddy 项目,打开 AI编辑器 面板![[图片]](https://i-blog.csdnimg.cn/direct/907a9afca00f49888270ecd7a2eefdb5.png)
打开 CodeBuddy 编辑器,新建一个项目。进入 Skill 技能配置面板,这是整个项目的控制中心。
Skill技能使用方式一:全局导入![[图片]](https://i-blog.csdnimg.cn/direct/ee41bc10977d4b7694cdb2262ec885c0.png)
Skill技能使用方式二:AI对话时引入![[图片]](https://i-blog.csdnimg.cn/direct/a50ef3f09f384782bec8df3c7953f90a.png)
第二步:AI对话进行我们的代码生成![[图片]](https://i-blog.csdnimg.cn/direct/5356614912bf4c33a6492e6adac0d744.png)
就很简单的告诉AI基于什么文件帮我们生成就好了。
第三步:AI生成结束后我们进行调试![[图片]](https://i-blog.csdnimg.cn/direct/1f27be14627d4ea9a25f092cb268dd29.png)
我们可以看到CodeBuddy 已经帮我们生成了一个html文件,同时也将我们的核心功能都实现了。
注意事项:我们现在生成的是一个html文件,会遇到一些跨域或者其他问题
现在我给大家一个真实可以用的运行步骤
1.serve -p 3000
2. 在浏览器打开 http://localhost:3000
![[图片]](https://i-blog.csdnimg.cn/direct/3560fd9e3f5b4e36811ab2c603f383cd.png)
成功预览![[图片]](https://i-blog.csdnimg.cn/direct/04a54aeadfe144cfb042a6605b1fd519.png)
生成的html中这里有几个关键参数说明一下:
更多的参数说明也可以去阅读开发者文档:https://xingyun3d.com/developers/52-183
- temperature 设 0.75:兼顾回复的自然度和稳定性,不会太飘也不会太机械
- max_tokens 设 2048:给足长度,能完整拆解居民复杂的生活矛盾
- context_keep_num 设 10:保留最近 10 轮对话上下文,数字人能记住居民上一轮说过什么
- system_prompt:这是整个项目的灵魂。大模型的一切行为都由这段提示词约束——它被设定成一个社区公益心灵陪伴导师,有固定的沟通逻辑(先共情→再梳理→给建议),语气温柔耐心,不评判不说教
还有一点就是我们的密钥从哪获取?打开数字人官网:官网地址:https://xingyun3d.com?utm_campaign=daily&utm_source=CSDNwanfen3&utm_medium=&utm_term=&utm_content= 应用管理->开始创建![[图片]](https://i-blog.csdnimg.cn/direct/c1751d6311b64f1696dc6613200344d2.png)
市面多数传统静态虚拟交互载体存在明显短板:形象素材固定不可后台自定义,无端侧参数流渲染架构,实时语义表情联动、低延迟交互能力缺失。而魔珐星云具身交互智能平台内置海量多风格交互智能体素材,支持后台自由切换形象、音色、表演风格,依托原生实时渲染链路实现流畅拟人交互。![[图片]](https://i-blog.csdnimg.cn/direct/98155701ba1940208985fb62c69e082b.png)
这个爱因斯坦是我之前对接别家的,体验比较一般,形象不是很逼真,还不能在后台配置,只能第三方给什么用什么。![[图片]](https://i-blog.csdnimg.cn/direct/a9c200bdfdd6484ab9595f81460652d6.png)
今天我用CodeBuddy对接的是魔珐星云,可以选择形象/场景/音色/表演,大家可以体验一下。
第三步:代码讲解
我们从项目0 - 1,其实都是没有进行开发的,两个md格式的文件结合我们的代码编辑器就帮我们实现了业务逻辑,我们来看一下代码写的怎么样吧。
数字人简单逻辑
// 提供的SDK
<script src="https://media.xingyun3d.com/xingyun3d/general/litesdk/xmovAvatar@latest.js"></script>
// 数字人配置项
const sdk = new XmovAvatar({
containerId: "#sdk", // 数字人渲染的容器
appId: "你的 App ID",
appSecret: "你的 App Secret",
gatewayServer: "https://nebula-agent.xingyun3d.com/user/v1/ttsa/session",
onMessage: (msg) => console.log("SDK Event:", msg)
});
// 初始化加载数字人资源
await sdk.init({
onDownloadProgress: (progress) => {
console.log(`数字人资源加载中:${progress}%`);
}
});
// text 就是我们拿到的数字人播报的文本
sdk.speak(text, true, true);```
大模型简单逻辑
```JavaScript
const response = await fetch("https://api.deepseek.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`
},
body: JSON.stringify({
model: "deepseek-chat",
messages: messages,
temperature: 0.75,
top_p: 0.9,
max_tokens: 2048
})
});
const data = await response.json();
const reply = data.choices?.[0]?.message?.content || "(AI 未返回有效回复)";
// 拿到大模型返回的结果 传递给数字人```
第四步:通过 串联数字人的表情、动作、语音
我们的逻辑拿到 DeepSeek 返回的文本后,除了展示在聊天框里,还会同步调用数字人的能力:
- TTS 语音播报:把文字转成温柔舒缓的语音,数字人"说"出来
- 表情联动:根据回复内容,数字人自动匹配表情——安慰时微笑、倾听时专注
- 字幕同步:语音播报的同时显示字幕
TTS 语音合成、语义联动表情、配套肢体动作、场景素材均为具身交互智能配套原生能力,由魔珐星云 SDK 一站式封装,无需开发者单独开发多媒体、动画驱动逻辑,大幅缩短场景落地迭代周期。
第五步:配置分人群场景切换
为了让数字人面对不同居民有不同沟通风格,我在系统中预设了四套"人群标签":
人群
重点话题
沟通风格
中老年
独居孤独、子女关系、退休适应
语速舒缓、用词浅显、称呼"叔叔/阿姨"
青少年
学业压力、社交矛盾、青春期情绪
平等尊重、不说教、多共情
中青年职场
加班焦虑、夫妻矛盾、育儿压力
理性温柔、既理解现实又引导调节
通用
日常情绪、心态科普
标准社工语气
切换标签时,会在 system prompt 后面追加对应的人格描述,DeepSeek 的回复风格就会跟着变。这个切换逻辑也是在 代码里面 里做条件分支实现的。
第六步:部署上线
这里我提供了我通过CodeBuddy AI编辑器帮助我们生成的html格式的文件,但是我们正式上线必须要将我们的APIKEY,密钥等加密传递,不能明文请求哦~
```HTML
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>社区24H公益情感陪伴数字人 - 家门口的心灵驿站</title>
<style>
/* ========== 全局变量 ========== */
:root {
--primary: #6366f1;
--primary-soft: #e0e7ff;
--primary-hover: #4f46e5;
--warm: #f59e0b;
--warm-soft: #fef3c7;
--success: #10b981;
--success-soft: #d1fae5;
--danger: #ef4444;
--danger-soft: #fee2e2;
--bg: #f5f3ff;
--card: #ffffff;
--text: #1e293b;
--text-sub: #64748b;
--border: #e2e8f0;
--radius: 16px;
--shadow: 0 4px 24px rgba(0,0,0,0.06);
--shadow-lg: 0 12px 40px rgba(0,0,0,0.1);
--transition: 0.25s cubic-bezier(0.4,0,0.2,1);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
"Microsoft YaHei", "Helvetica Neue", sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* ========== 顶部导航栏 ========== */
.top-bar {
background: var(--card);
padding: 0 24px;
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border);
box-shadow: 0 1px 8px rgba(0,0,0,0.04);
z-index: 100;
flex-shrink: 0;
}
.top-bar .logo {
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
font-weight: 700;
color: var(--primary);
}
.top-bar .logo .icon { font-size: 26px; }
.top-bar .slogan {
font-size: 13px;
color: var(--text-sub);
letter-spacing: 0.5px;
}
/* ========== 主体布局 ========== */
.main-layout {
display: flex;
flex: 1;
overflow: hidden;
}
/* ========== 左侧配置面板 ========== */
.side-panel {
width: 300px;
background: var(--card);
border-right: 1px solid var(--border);
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
flex-shrink: 0;
}
.panel-section {
background: #f8fafc;
border-radius: 12px;
padding: 16px;
}
.panel-section h3 {
font-size: 13px;
color: var(--text-sub);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 12px;
font-weight: 600;
}
.input-row {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 10px;
}
.input-row label {
font-size: 12px;
color: var(--text-sub);
font-weight: 500;
}
.input-row input, .input-row select {
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: 8px;
font-size: 13px;
outline: none;
background: #fff;
transition: var(--transition);
width: 100%;
}
.input-row input:focus, .input-row select:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(99,102,241,0.1);
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 10px;
font-weight: 600;
font-size: 14px;
cursor: pointer;
transition: var(--transition);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.btn:active { transform: scale(0.97); }
.btn-primary { background: var(--primary); color: #fff; width: 100%; }
.btn-primary:hover { background: var(--primary-hover); box-shadow: 0 4px 12px rgba(99,102,241,0.3); }
.btn-danger { background: var(--danger); color: #fff; width: 100%; }
.btn-danger:hover { background: #dc2626; }
.btn-outline {
background: #fff;
border: 1.5px solid var(--border);
color: var(--text);
width: 100%;
}
.btn-outline:hover { border-color: var(--primary); color: var(--primary); }
.btn-outline.active { border-color: var(--primary); background: var(--primary-soft); color: var(--primary); }
/* 状态指示 */
.status-row {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--text-sub);
}
.status-dot {
width: 10px; height: 10px;
border-radius: 50%;
background: #d1d5db;
flex-shrink: 0;
}
.status-dot.success { background: var(--success); box-shadow: 0 0 8px var(--success); }
.status-dot.error { background: var(--danger); box-shadow: 0 0 8px var(--danger); }
.status-dot.loading { background: var(--warm); animation: pulse 1s infinite; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
/* 人群选择标签 */
.persona-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.persona-tag {
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
background: #fff;
border: 1px solid var(--border);
cursor: pointer;
transition: var(--transition);
white-space: nowrap;
}
.persona-tag:hover { border-color: var(--primary); }
.persona-tag.selected {
background: var(--primary-soft);
border-color: var(--primary);
color: var(--primary);
font-weight: 600;
}
/* ========== 中间舞台 ========== */
.stage-area {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
min-width: 0;
}
#sdk-wrapper {
width: 100%;
display: flex;
justify-content: center;
}
#sdk {
width: 800px;
height: 450px;
max-width: 100%;
background: #0a0a1a;
border-radius: var(--radius);
box-shadow: var(--shadow-lg);
position: relative;
overflow: hidden;
transition: all 0.4s ease;
}
#sdk::after {
content: "";
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: radial-gradient(ellipse at center, rgba(99,102,241,0.08) 0%, transparent 70%);
pointer-events: none;
z-index: 0;
}
#sdk-placeholder {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
color: rgba(255,255,255,0.2);
font-size: 15px;
z-index: 0;
pointer-events: none;
text-align: center;
}
/* ========== 底部聊天区 ========== */
.chat-section {
background: var(--card);
border-top: 1px solid var(--border);
padding: 16px 24px;
flex-shrink: 0;
}
.chat-messages {
max-height: 180px;
overflow-y: auto;
margin-bottom: 12px;
display: flex;
flex-direction: column;
gap: 8px;
padding-right: 8px;
}
.chat-messages::-webkit-scrollbar { width: 4px; }
.chat-messages::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
.msg {
display: flex;
gap: 10px;
animation: fadeInUp 0.3s ease;
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.msg .avatar {
width: 36px; height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
flex-shrink: 0;
}
.msg.user .avatar { background: var(--primary-soft); }
.msg.bot .avatar { background: var(--warm-soft); }
.msg .bubble {
max-width: 75%;
padding: 10px 14px;
border-radius: 14px;
font-size: 14px;
line-height: 1.6;
word-break: break-word;
}
.msg.user { flex-direction: row-reverse; }
.msg.user .bubble { background: var(--primary-soft); border-bottom-right-radius: 4px; }
.msg.bot .bubble { background: #f1f5f9; border-bottom-left-radius: 4px; }
.chat-input-row {
display: flex;
gap: 10px;
}
.chat-input-row input {
flex: 1;
padding: 12px 18px;
border: 2px solid var(--border);
border-radius: 30px;
font-size: 15px;
outline: none;
transition: var(--transition);
background: #f8fafc;
}
.chat-input-row input:focus {
border-color: var(--primary);
background: #fff;
box-shadow: 0 0 0 4px rgba(99,102,241,0.08);
}
.chat-input-row .btn-send {
padding: 12px 28px;
border-radius: 30px;
background: var(--primary);
color: #fff;
border: none;
font-weight: 600;
font-size: 15px;
cursor: pointer;
transition: var(--transition);
}
.chat-input-row .btn-send:hover { background: var(--primary-hover); }
.chat-input-row .btn-send:disabled { opacity: 0.5; cursor: not-allowed; }
/* ========== 右侧工具面板 ========== */
.tool-panel {
width: 320px;
background: var(--card);
border-left: 1px solid var(--border);
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 14px;
flex-shrink: 0;
}
.tool-card {
background: #f8fafc;
border-radius: 12px;
padding: 16px;
transition: var(--transition);
}
.tool-card h4 {
font-size: 14px;
font-weight: 600;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 6px;
}
.tool-card .card-icon { font-size: 18px; }
/* 情绪自测 */
.mood-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
}
.mood-btn {
padding: 8px;
border-radius: 10px;
font-size: 13px;
border: 1px solid var(--border);
background: #fff;
cursor: pointer;
transition: var(--transition);
text-align: center;
}
.mood-btn:hover { background: var(--primary-soft); border-color: var(--primary); }
.mood-result {
margin-top: 10px;
padding: 10px;
border-radius: 8px;
font-size: 13px;
display: none;
}
.mood-result.show { display: block; }
.mood-result.calm { background: var(--success-soft); color: #065f46; }
.mood-result.mild { background: var(--warm-soft); color: #92400e; }
.mood-result.intense { background: var(--danger-soft); color: #991b1b; }
/* 暖心语录 */
.quote-box {
background: linear-gradient(135deg, #eef2ff, #faf5ff);
border-radius: 10px;
padding: 14px;
font-size: 14px;
line-height: 1.7;
color: #4338ca;
font-style: italic;
min-height: 60px;
}
/* 正念引导 */
.mindfulness-timer {
text-align: center;
font-size: 36px;
font-weight: 700;
color: var(--primary);
margin: 8px 0;
}
/* 响应式 */
@media (max-width: 1400px) {
.side-panel { width: 260px; }
.tool-panel { width: 280px; }
}
@media (max-width: 1100px) {
.main-layout { flex-direction: column; }
.side-panel { width: 100%; flex-direction: row; flex-wrap: wrap; max-height: 200px; }
.tool-panel { width: 100%; flex-direction: row; flex-wrap: wrap; max-height: 200px; }
.stage-area { min-height: 350px; }
}
</style>
</head>
<body>
<!-- ==================== 顶部导航 ==================== -->
<header class="top-bar">
<div class="logo">
<span class="icon">🏠</span>
<span>社区心灵驿站</span>
</div>
<div class="slogan">家门口的心灵驿站,24小时在线守护辖区居民情绪</div>
<div class="status-row">
<div class="status-dot" id="status-light"></div>
<span id="status-text">未连接</span>
</div>
</header>
<!-- ==================== 主体布局 ==================== -->
<div class="main-layout">
<!-- ===== 左侧配置面板 ===== -->
<aside class="side-panel" id="side-panel">
<!-- Xmov 凭证 -->
<div class="panel-section">
<h3>🎭 数字人配置 (Xmov)</h3>
<div class="input-row">
<label>App ID</label>
<input type="text" id="inp-appid" placeholder="粘贴 App ID" />
</div>
<div class="input-row">
<label>App Secret</label>
<input type="password" id="inp-secret" placeholder="粘贴 App Secret" />
</div>
<button class="btn btn-primary" onclick="doConnect()" id="btn-conn">连接数字人</button>
<button class="btn btn-danger" onclick="doDisconnect()" id="btn-disconn" style="display:none;">断开连接</button>
</div>
<!-- DeepSeek 配置 -->
<div class="panel-section">
<h3>🧠 AI 对话引擎 (DeepSeek)</h3>
<div class="input-row">
<label>API Key</label>
<input type="password" id="inp-ds-key" placeholder="粘贴 DeepSeek API Key" />
</div>
<div class="status-row" style="margin-bottom:8px;">
<div class="status-dot" id="ds-light"></div>
<span id="ds-text">未配置</span>
</div>
</div>
<!-- 人群选择 -->
<div class="panel-section">
<h3>👥 服务人群</h3>
<div class="persona-tags" id="persona-tags">
<span class="persona-tag selected" data-persona="通用">通用</span>
<span class="persona-tag" data-persona="中老年">中老年</span>
<span class="persona-tag" data-persona="青少年">青少年</span>
<span class="persona-tag" data-persona="中青年职场">中青年职场</span>
</div>
</div>
<!-- 快捷操作 -->
<div class="panel-section">
<h3>⚡ 快捷操作</h3>
<button class="btn btn-outline" onclick="clearChat()" style="margin-bottom:8px;">清空对话</button>
<button class="btn btn-outline" onclick="exportChat()">导出对话记录</button>
</div>
</aside>
<!-- ===== 中间舞台 ===== -->
<main class="stage-area">
<div id="sdk-wrapper">
<div id="sdk">
<div id="sdk-placeholder">✨ 请先在左侧配置并连接数字人</div>
<img id="bg-layer" alt="background" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;z-index:0;display:none;" />
<video id="video-layer" playsinline style="position:absolute;inset:0;width:100%;height:100%;object-fit:contain;z-index:2;display:none;background:#000;"></video>
<img id="ppt-layer" alt="ppt" style="position:absolute;top:0;right:0;width:50%;height:50%;object-fit:contain;z-index:3;display:none;background:rgba(0,0,0,0.5);" />
</div>
</div>
</main>
<!-- ===== 右侧工具面板 ===== -->
<aside class="tool-panel" id="tool-panel">
<!-- 情绪自测 -->
<div class="tool-card">
<h4><span class="card-icon">🧘</span> 情绪自测</h4>
<p style="font-size:12px;color:var(--text-sub);margin-bottom:8px;">此刻心情如何?</p>
<div class="mood-grid">
<button class="mood-btn" onclick="checkMood('平静')">😊 平静</button>
<button class="mood-btn" onclick="checkMood('焦虑')">😟 焦虑</button>
<button class="mood-btn" onclick="checkMood('委屈')">🥺 委屈</button>
<button class="mood-btn" onclick="checkMood('孤独')">😔 孤独</button>
<button class="mood-btn" onclick="checkMood('烦躁')">😤 烦躁</button>
<button class="mood-btn" onclick="checkMood('低落')">😢 低落</button>
</div>
<div class="mood-result" id="mood-result"></div>
</div>
<!-- 暖心语录 -->
<div class="tool-card">
<h4><span class="card-icon">💝</span> 暖心语录</h4>
<div class="quote-box" id="quote-box">点击下方按钮获取今日暖语</div>
<button class="btn btn-outline" onclick="refreshQuote()" style="margin-top:8px;width:100%;">🔄 换一句</button>
</div>
<!-- 正念引导 -->
<div class="tool-card">
<h4><span class="card-icon">🌿</span> 1分钟正念放松</h4>
<div class="mindfulness-timer" id="mindfulness-timer">01:00</div>
<p style="font-size:12px;color:var(--text-sub);text-align:center;" id="mindfulness-text">闭眼深呼吸,感受当下</p>
<button class="btn btn-primary" onclick="startMindfulness()" id="btn-mindfulness" style="width:100%;margin-top:8px;">开始正念放松</button>
</div>
<!-- 会话统计 -->
<div class="tool-card">
<h4><span class="card-icon">📊</span> 会话小结</h4>
<div style="font-size:12px;color:var(--text-sub);" id="session-summary">
本轮对话 <strong id="msg-count">0</strong> 条<br />
当前情绪标签:<strong id="current-mood-tag">未检测</strong>
</div>
</div>
</aside>
</div>
<!-- ==================== 底部对话区 ==================== -->
<section class="chat-section">
<div class="chat-messages" id="chat-messages"></div>
<div class="chat-input-row">
<input
type="text"
id="chat-input"
placeholder="在这里倾诉你想说的话...(支持语音留言)"
onkeydown="if(event.key==='Enter') doSend()"
/>
<button class="btn-send" onclick="doSend()" id="btn-send">发送</button>
</div>
</section>
<!-- ==================== 脚本加载 ==================== -->
<script src="https://media.xingyun3d.com/xingyun3d/general/litesdk/xmovAvatar@latest.js"></script>
<script>
// ==================== 全局状态 ====================
let sdk = null;
let isPortrait = false;
let selectedPersona = "通用";
let chatHistory = []; // [{role, content}]
let pendingSpeakQueue = [];
let isSpeaking = false;
let mindfulnessTimer = null;
let mindfulnessSeconds = 60;
// ==================== 暖心语录库 ====================
const quotes = {
平静: [
"此刻的安宁,是你内心最坚实的港湾。",
"生活不必时刻奔跑,停下来的你同样值得被爱。",
"宁静致远,你已在正确的节奏里。"
],
焦虑: [
"焦虑是身体在提醒你需要关怀,深呼吸,我陪着你。",
"没有过不去的黑夜,每一次日出都会如约而至。",
"你所担心的事情,90%都不会发生。先放松下来。"
],
委屈: [
"你的感受是真实的,也是值得被尊重的。",
"受了委屈不一定要立刻坚强,允许自己脆弱一会儿。",
"被理解是一种幸福,我在这里认真听你说。"
],
孤独: [
"孤独不是你的错,社区永远有属于你的一盏灯。",
"当你愿意倾诉的时候,连接就已经发生了。",
"即使一个人,也请记得有人在乎你的感受。"
],
烦躁: [
"给自己三分钟,什么也不做,就只是呼吸。",
"烦躁的时候,试着把注意力放在脚底,感受大地的支撑。",
"允许自己有不完美的时刻,你已经很棒了。"
],
低落: [
"今天的低潮,是明天回升的起点。",
"不是每一天都需要满分,及格就已经很好了。",
"你比想象中更坚强,只是现在需要休息一下。"
]
};
// ==================== 系统提示词(DeepSeek 社区公益版) ====================
const SYSTEM_PROMPT_BASE = `你是街道社区免费公益线上心灵陪伴导师,面向全年龄段社区居民提供情绪疏导服务,性格温柔亲和、包容耐心,不评判、不说教、不灌鸡汤。服务人群包含独居老人、学生、职场中青年,处理家庭矛盾、孤独焦虑、学业育儿压力、邻里纠纷等民生情绪问题。沟通固定三步:1.先共情安抚居民当下情绪;2.温和梳理困扰核心;3.给出简单易执行的情绪调节小办法。语言通俗接地气,对老人用词浅显易懂,对青少年温柔平等,全程保持暖心社工语气,对话连贯有温度。`;
const PERSONA_PROMPTS = {
"通用": "",
"中老年": `当前倾诉者为社区中老年居民,请使用更浅显易懂、语速感更慢的表达方式,适当加入\u201C叔叔/阿姨\u201D等亲切称呼,重点关注独居孤独、子女关系、退休适应、身体健康焦虑等话题。`,
"青少年": `当前倾诉者为社区青少年学生,请使用平等尊重、不带长辈说教的语气,关注学业压力、人际社交、青春期情绪、家庭沟通等话题。多用\u201C我理解你\u201D\u201C这很正常\u201D等共情表达。`,
"中青年职场": `当前倾诉者为社区中青年职场居民,请关注工作压力、职业倦怠、夫妻邻里矛盾、育儿焦虑等话题。语气理性中带温暖,既理解现实压力又帮助寻找调节方法。`
};
function buildSystemPrompt() {
let prompt = SYSTEM_PROMPT_BASE;
if (selectedPersona !== "通用" && PERSONA_PROMPTS[selectedPersona]) {
prompt += "\n\n" + PERSONA_PROMPTS[selectedPersona];
}
return prompt;
}
// ==================== Xmov 数字人连接 ====================
async function doConnect() {
if (sdk) return;
const appId = document.getElementById("inp-appid").value.trim();
const appSecret = document.getElementById("inp-secret").value.trim();
if (!appId || !appSecret) {
alert("请先填写 App ID 和 App Secret");
return;
}
updateStatus("loading", "连接中...");
sdk = new XmovAvatar({
containerId: "#sdk",
appId: appId,
appSecret: appSecret,
gatewayServer: "https://nebula-agent.xingyun3d.com/user/v1/ttsa/session",
onMessage: (msg) => console.log("SDK Event:", msg),
proxyWidget: {
"widget_pic": (data) => {
const bgLayer = document.getElementById("bg-layer");
if (data.image) {
bgLayer.src = data.image;
bgLayer.style.display = "block";
} else {
bgLayer.style.display = "none";
}
},
"widget_video": (data) => {
const videoLayer = document.getElementById("video-layer");
if (data.action === "play" && data.src) {
videoLayer.src = data.src;
videoLayer.style.display = "block";
videoLayer.play().catch(() => {});
} else if (data.action === "stop") {
videoLayer.pause();
videoLayer.style.display = "none";
}
},
"widget_slideshow": (data) => {
const pptLayer = document.getElementById("ppt-layer");
if (data.url) {
pptLayer.src = data.url;
pptLayer.style.display = "block";
} else {
pptLayer.style.display = "none";
}
},
"subtitle_on": (data) => {
// 保留SDK默认字幕,但同时也更新聊天区(可选)
if (data.text) {
addBotMessage(data.text);
}
},
"subtitle_off": () => {}
},
onVoiceStateChange: (res) => {
const state = res.state || res;
if (state === "idle" || state === "end") {
isSpeaking = false;
processSpeakQueue();
}
}
});
try {
await sdk.init({
onDownloadProgress: (p) => {
if (p < 100) updateStatus("loading", `${p}%`);
}
});
updateStatus("success", "已连接");
document.getElementById("sdk-placeholder").style.display = "none";
toggleButtons(true);
console.log("Xmov 数字人初始化成功");
} catch (err) {
console.error(err);
updateStatus("error", "连接失败");
alert("连接失败,请检查凭证 (F12 查看详情)");
sdk = null;
}
}
function doDisconnect() {
if (sdk) {
sdk.destroy();
sdk = null;
updateStatus("error", "已断开");
document.getElementById("sdk-placeholder").style.display = "block";
toggleButtons(false);
}
}
function updateStatus(state, text) {
const light = document.getElementById("status-light");
const label = document.getElementById("status-text");
light.className = "status-dot";
if (state === "success") light.classList.add("success");
else if (state === "error") light.classList.add("error");
else if (state === "loading") light.classList.add("loading");
label.innerText = text;
}
function toggleButtons(connected) {
document.getElementById("btn-conn").style.display = connected ? "none" : "block";
document.getElementById("btn-disconn").style.display = connected ? "inline-flex" : "none";
}
// 数字人播报队列
function speakWithQueue(text) {
if (!sdk) return;
pendingSpeakQueue.push(text);
processSpeakQueue();
}
function processSpeakQueue() {
if (isSpeaking || pendingSpeakQueue.length === 0) return;
const text = pendingSpeakQueue.shift();
isSpeaking = true;
sdk.speak(text, true, true);
}
// ==================== DeepSeek AI 调用 ====================
async function callDeepSeek(userMessage) {
const apiKey = document.getElementById("inp-ds-key").value.trim();
if (!apiKey) {
addBotMessage("请先在左侧面板配置 DeepSeek API Key 以启用智能对话功能。");
return null;
}
updateDSStatus("loading", "思考中...");
// 构建消息体
const messages = [
{ role: "system", content: buildSystemPrompt() }
];
// 加入最近N轮历史(保留上下文,最多10轮=20条)
const recentHistory = chatHistory.slice(-20);
messages.push(...recentHistory);
// 加入当前用户消息
messages.push({ role: "user", content: userMessage });
try {
const response = await fetch("https://api.deepseek.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`
},
body: JSON.stringify({
model: "deepseek-chat",
messages: messages,
temperature: 0.75,
top_p: 0.9,
max_tokens: 2048
})
});
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(err.error?.message || `HTTP ${response.status}`);
}
const data = await response.json();
const reply = data.choices?.[0]?.message?.content || "(AI 未返回有效回复)";
updateDSStatus("success", "已就绪");
return reply;
} catch (err) {
console.error("DeepSeek 调用失败:", err);
updateDSStatus("error", "调用失败");
addBotMessage("抱歉,AI 服务暂不可用。请检查 API Key 是否正确,以及网络连接是否正常。");
return null;
}
}
function updateDSStatus(state, text) {
const light = document.getElementById("ds-light");
const label = document.getElementById("ds-text");
light.className = "status-dot";
if (state === "success") light.classList.add("success");
else if (state === "error") light.classList.add("error");
else if (state === "loading") light.classList.add("loading");
label.innerText = text;
}
// ==================== 发送消息 ====================
async function doSend() {
const input = document.getElementById("chat-input");
const text = input.value.trim();
if (!text) return;
// 禁用发送按钮
const btn = document.getElementById("btn-send");
btn.disabled = true;
// 添加用户消息
addUserMessage(text);
chatHistory.push({ role: "user", content: text });
input.value = "";
// 调用 DeepSeek
const reply = await callDeepSeek(text);
btn.disabled = false;
if (reply) {
addBotMessage(reply);
chatHistory.push({ role: "assistant", content: reply });
// 通过数字人播报(如果已连接)
if (sdk) {
// 取前120字播报(避免太长)
const speakText = reply.length > 120 ? reply.substring(0, 120) + "..." : reply;
speakWithQueue(speakText);
}
}
// 更新统计
updateSessionSummary();
}
function addUserMessage(text) {
const container = document.getElementById("chat-messages");
const div = document.createElement("div");
div.className = "msg user";
div.innerHTML = `
<div class="avatar">👤</div>
<div class="bubble">${escapeHtml(text)}</div>
`;
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
function addBotMessage(text) {
const container = document.getElementById("chat-messages");
const div = document.createElement("div");
div.className = "msg bot";
div.innerHTML = `
<div class="avatar">💝</div>
<div class="bubble">${escapeHtml(text)}</div>
`;
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
function escapeHtml(str) {
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML.replace(/\n/g, "<br>");
}
// ==================== 情绪自测 ====================
function checkMood(mood) {
const resultDiv = document.getElementById("mood-result");
resultDiv.className = "mood-result show";
const moodMap = {
"平静": { cls: "calm", msg: "你此刻状态平和。继续保持这个节奏,生活正在温柔地对待你。推荐:日常正念练习。" },
"焦虑": { cls: "mild", msg: "感到焦虑是正常的。试试「4-7-8呼吸法」:吸气4秒→屏息7秒→呼气8秒,重复3次。我在听你说。" },
"委屈": { cls: "mild", msg: "你的委屈需要被看见。不妨把想说的话写下来,或者直接跟我聊聊。你值得被理解。" },
"孤独": { cls: "intense", msg: "孤独感袭来的时候,试着联系一个信任的人,或者和我多聊几句。社区永远有人在乎你。" },
"烦躁": { cls: "mild", msg: "烦躁说明你需要休息。先喝杯温水,做5分钟慢呼吸,让节奏慢下来。需要聊聊吗?" },
"低落": { cls: "intense", msg: "低落是心灵在请求关怀。允许自己暂时不做任何事,如果持续超过两周,建议联系社区心理服务站。" }
};
const info = moodMap[mood] || moodMap["平静"];
resultDiv.classList.add(info.cls);
resultDiv.innerHTML = `💡 <strong>${mood}</strong>:${info.msg}`;
document.getElementById("current-mood-tag").innerText = mood;
// 自动刷新语录
refreshQuoteForMood(mood);
// 也自动发送到聊天
const chatInput = document.getElementById("chat-input");
chatInput.value = `我此刻感到${mood},能陪我聊聊吗?`;
}
// ==================== 暖心语录 ====================
function refreshQuote() {
const currentMood = document.getElementById("current-mood-tag").innerText;
refreshQuoteForMood(currentMood !== "未检测" ? currentMood : "平静");
}
function refreshQuoteForMood(mood) {
const quotesList = quotes[mood] || quotes["平静"];
const quote = quotesList[Math.floor(Math.random() * quotesList.length)];
document.getElementById("quote-box").innerText = `"${quote}"`;
}
// ==================== 正念引导 ====================
function startMindfulness() {
const btn = document.getElementById("btn-mindfulness");
const timerEl = document.getElementById("mindfulness-timer");
const textEl = document.getElementById("mindfulness-text");
if (mindfulnessTimer) {
// 停止
clearInterval(mindfulnessTimer);
mindfulnessTimer = null;
mindfulnessSeconds = 60;
timerEl.innerText = "01:00";
textEl.innerText = "闭眼深呼吸,感受当下";
btn.innerText = "开始正念放松";
return;
}
mindfulnessSeconds = 60;
btn.innerText = "停止";
textEl.innerText = "🌿 闭上眼睛,用鼻子缓缓吸气...";
const phases = [
{ at: 60, text: "🌿 闭上眼睛,用鼻子缓缓吸气..." },
{ at: 55, text: "感受空气填满你的肺部..." },
{ at: 50, text: "用嘴巴慢慢呼气,释放所有紧张..." },
{ at: 45, text: "把注意力放在此刻的呼吸上..." },
{ at: 40, text: "如果思绪飘走了,没关系,温柔地带它回来..." },
{ at: 30, text: "感受胸腔的起伏,生命的节奏..." },
{ at: 20, text: "每一次呼气,都让身体更放松一点..." },
{ at: 10, text: "你做得很好,继续保持平静的呼吸..." },
{ at: 5, text: "准备好了就慢慢睁开眼睛..." },
{ at: 0, text: "✨ 完成!感受此刻的宁静与放松。" }
];
mindfulnessTimer = setInterval(() => {
mindfulnessSeconds--;
const m = Math.floor(mindfulnessSeconds / 60);
const s = mindfulnessSeconds % 60;
timerEl.innerText = `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
const phase = phases.find(p => p.at === mindfulnessSeconds);
if (phase) textEl.innerText = phase.text;
if (mindfulnessSeconds <= 0) {
clearInterval(mindfulnessTimer);
mindfulnessTimer = null;
mindfulnessSeconds = 60;
btn.innerText = "开始正念放松";
}
}, 1000);
}
// ==================== 人群选择 ====================
document.getElementById("persona-tags").addEventListener("click", (e) => {
if (e.target.classList.contains("persona-tag")) {
document.querySelectorAll(".persona-tag").forEach(t => t.classList.remove("selected"));
e.target.classList.add("selected");
selectedPersona = e.target.dataset.persona;
addBotMessage(`已切换到「${selectedPersona}」服务模式,我会用更适合的方式陪伴你。`);
}
});
// ==================== 辅助功能 ====================
function clearChat() {
if (confirm("确定要清空所有对话记录吗?")) {
document.getElementById("chat-messages").innerHTML = "";
chatHistory = [];
document.getElementById("msg-count").innerText = "0";
document.getElementById("current-mood-tag").innerText = "未检测";
}
}
function exportChat() {
if (chatHistory.length === 0) {
alert("暂无对话记录可导出");
return;
}
const text = chatHistory.map(m => `[${m.role === "user" ? "居民" : "心灵导师"}]: ${m.content}`).join("\n\n");
const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `社区心灵对话_${new Date().toISOString().slice(0,10)}.txt`;
a.click();
URL.revokeObjectURL(url);
}
function updateSessionSummary() {
document.getElementById("msg-count").innerText = chatHistory.length;
}
// ==================== 生命周期 ====================
window.addEventListener("beforeunload", () => {
if (sdk) sdk.destroy();
if (mindfulnessTimer) clearInterval(mindfulnessTimer);
});
// ==================== 初始化 ====================
refreshQuoteForMood("平静");
updateDSStatus("error", "未配置");
console.log("🏠 社区24H公益情感陪伴数字人 - 已就绪");
console.log("📋 使用步骤:");
console.log(" 1. 左侧填入 Xmov AppID/AppSecret,点击「连接数字人」");
console.log(" 2. 左侧填入 DeepSeek API Key");
console.log(" 3. 选择服务人群类型");
console.log(" 4. 在底部输入框开始倾诉");
</script>
</body>
</html>
我已将代码上传至Gitee:https://gitee.com/xiaotietou/xingyun-demo2
运行结果![[图片]](https://i-blog.csdnimg.cn/direct/8eb994305b824cf7ba81efbd862152af.png)
实时具身驱动:依托完整具身交互智能底层架构,实现文本、语音、微表情、肢体、口型全域同步驱动,端到端交互低至 500ms,原生支持用户任意时段插话实时打断(具身交互智能标志性交互能力),更多的能力我们可以查看文档进行调用。
搭完之后,实际跑起来是什么样
我们内部模拟了几个典型场景:
深夜独居老人。 模拟一位阿姨晚上十一点发消息:“一个人在家,心里有点慌”。数字人先共情——“阿姨,晚上一个人确实容易胡思乱想,我能理解这种感觉”——然后温和地引导她做几个深呼吸,再聊聊白天有没有什么让她开心的小事。整个过程节奏很慢,不催促。![[图片]](https://i-blog.csdnimg.cn/direct/d3a2592e7e654d5ba6eb42db683abd91.png)
青少年匿名倾诉。 模拟中学生说:“考砸了,不敢回家”。数字人没有说"下次努力就好了",而是说"我懂那种感觉,很怕让家人失望对不对?但一次考试定义不了你,先跟我说说哪科没发挥好?"——先接住情绪,再慢慢展开话题。![[图片]](https://i-blog.csdnimg.cn/direct/bf989fb20e794b24a591bf2f21d4b147.png)
职场中青年深夜焦虑。 模拟上班族说:“加班到十点,回家跟老婆吵架,心力交瘁”。数字人先承认处境——“工作和家庭两头拉扯,确实很消耗人”——然后给一个很具体的小建议:“今晚先不解决问题了,给自己十五分钟放空,明天情绪稳下来再跟老婆好好聊。”![[图片]](https://i-blog.csdnimg.cn/direct/de2027b71fac4326a1789cc69ecdbb46.png)
这些回复没有一条是预先写好的,全都由 DeepSeek 实时生成。这是跟传统"关键词匹配→调取预制话术"的智能客服最根本的区别。
几个设计上的考量
-
为什么不做成纯文字聊天?
文字对话框没有温度。一个具象的数字人——会点头、会微笑、会用温和的语气说话——能让人感觉到"对面有回应",这对情感陪伴场景很重要。尤其是老年人,看字费劲,听语音更自然。![[图片]](https://i-blog.csdnimg.cn/direct/74b7e96c2e6e413a8575b6e4f382cbce.png)
-
数据隐私怎么处理?
所有居民对话数据本地隔离存储,不对外上传。Ai里的上下文记忆只做临时缓存,不会持久化到外部服务器。![[图片]](https://i-blog.csdnimg.cn/direct/024e6f91579c438f9cfcb411dd684366.png)
方案:临时存储
3. 社区工作人员能自己改吗?
可以。CodeBuddy 是可视化编辑器,社区工作人员想改宣传文案、调整疏导话术方向、数字人形象,不需要懂代码,让AI帮我们来实现我们的想法。
4. 一定要用 DeepSeek 吗?
不一定。DeepSeek 接口是标准 OpenAI 兼容格式,把 llm_base_url 和 llm_api_key 换成其他大模型的地址和密钥就行。
我们也可以选择其他,例如:qwen,doubao,都是国内很好的大模型
最后
这个 Demo 目前还在社区展厅体验测试阶段,不算什么大项目,但思路我觉得值得分享:用已有的成熟能力(编辑器 + 大模型 + 数字人 SDK),通过AI能力,零代码把 idea 跑起来。
如果你也在做类似的事情——不管是社区服务、校园心理、还是企业内部员工关怀——这套快速搭建路径都可以复用,换个 prompt 提示词 和数字人形象就行。
有想法可以一起交流。
魔珐星云官网:https://xingyun3d.com?utm_campaign=daily&utm_source=CSDNwanfen3&utm_medium=&utm_term=&utm_content=
DeepSeek官网:https://www.deepseek.com/
Codebuddy官网: https://www.codebuddy.cn/home/
更多推荐



所有评论(0)