先搞清楚要解决什么问题
社区里的情绪陪伴需求,其实一直存在,但很难被覆盖到:

  • 独居老人半夜睡不着,没有人说句话
  • 青少年在学校受了委屈,不愿意跟家长开口
  • 上班族加班到深夜,心里堵得慌,但心理咨询要预约、要花钱
    社区不是没有资源——有社工、有心理咨询室。但社工要下班,咨询室周末关门。人和时间的缺口,才是真正的痛点。
    所以这个项目的核心目标很简单:做一个"不会下班"的 AI 陪伴者,在这个缺口上兜个底。
    在这里插入图片描述

最近做了一个0代码开发面向街道社区的小项目——一个 7×24 小时全天候在线社区情感陪伴具身交互智能体,给辖区居民提供免费、零门槛的情绪疏导。
在这里插入图片描述

这篇文章不写虚的,就讲讲我是怎么从 0 到 1 把它搭出来的。整个方案不需要写后端、不需要配服务器,核心就靠三个东西:CodeBuddy 编辑器 (Skill 技能) + DeepSeek 大模型 + 魔珐星云具身交互智能数字人

  1. CodeBuddy 编辑器 https://www.codebuddy.cn/home/
    [图片]

2.DeepSeek 大模型 https://www.deepseek.com/
[图片]

3.数字人具身智能官网地址:https://xingyun3d.com?utm_campaign=daily&utm_source=CSDNwanfen3&utm_medium=&utm_term=&utm_content=
[图片]


技术选型:为什么是这三个
CodeBuddy 编辑器:它是一个可视化的 AI 应用搭建工具。重点不是它能写代码,而是它内置了一个 Skill 技能调度系统。你可以把 Skill 理解成编辑器的"技能插件引擎"——它能帮你串联外部大模型、数字人 SDK、语音播报等不同模块,通过配置面板统一调度,不需要自己写业务代码。
DeepSeek 大模型:负责对话生成,整个数字人的"脑子"。所有给居民的回复都由它实时生成,不是预制的固定话术。
魔珐星云 数字人 SDK:提供全套多模态交互底层底座,依托参数流、端侧渲染核心技术,同步驱动语音、情绪微表情、肢体手势,形成一个有表情、有动作、能说话的具身智能 3D 数字人形象。
三者关系一句话讲清楚:CodeBuddy 的 Skill 做总调度,DeepSeek 做大脑,魔珐星云补齐完整具身交互智能。


0→1 搭建全流程
下面是我实际搭建的六个步骤,每一步都很具体。
第一步:新建 CodeBuddy 项目,打开 AI编辑器 面板
[图片]

打开 CodeBuddy 编辑器,新建一个项目。进入 Skill 技能配置面板,这是整个项目的控制中心。
Skill技能使用方式一:全局导入
[图片]

Skill技能使用方式二:AI对话时引入
[图片]

第二步:AI对话进行我们的代码生成
[图片]

就很简单的告诉AI基于什么文件帮我们生成就好了。
第三步:AI生成结束后我们进行调试
[图片]

我们可以看到CodeBuddy 已经帮我们生成了一个html文件,同时也将我们的核心功能都实现了。

注意事项:我们现在生成的是一个html文件,会遇到一些跨域或者其他问题
现在我给大家一个真实可以用的运行步骤
1.serve -p 3000
2. 在浏览器打开  http://localhost:3000

[图片]

成功预览
[图片]

生成的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= 应用管理->开始创建
    [图片]

市面多数传统静态虚拟交互载体存在明显短板:形象素材固定不可后台自定义,无端侧参数流渲染架构,实时语义表情联动、低延迟交互能力缺失。而魔珐星云具身交互智能平台内置海量多风格交互智能体素材,支持后台自由切换形象、音色、表演风格,依托原生实时渲染链路实现流畅拟人交互。
[图片]

这个爱因斯坦是我之前对接别家的,体验比较一般,形象不是很逼真,还不能在后台配置,只能第三方给什么用什么。
[图片]

今天我用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


运行结果
[图片]

实时具身驱动:依托完整具身交互智能底层架构,实现文本、语音、微表情、肢体、口型全域同步驱动,端到端交互低至 500ms,原生支持用户任意时段插话实时打断(具身交互智能标志性交互能力),更多的能力我们可以查看文档进行调用。

搭完之后,实际跑起来是什么样

我们内部模拟了几个典型场景:
深夜独居老人。 模拟一位阿姨晚上十一点发消息:“一个人在家,心里有点慌”。数字人先共情——“阿姨,晚上一个人确实容易胡思乱想,我能理解这种感觉”——然后温和地引导她做几个深呼吸,再聊聊白天有没有什么让她开心的小事。整个过程节奏很慢,不催促。
[图片]

青少年匿名倾诉。 模拟中学生说:“考砸了,不敢回家”。数字人没有说"下次努力就好了",而是说"我懂那种感觉,很怕让家人失望对不对?但一次考试定义不了你,先跟我说说哪科没发挥好?"——先接住情绪,再慢慢展开话题。
[图片]

职场中青年深夜焦虑。 模拟上班族说:“加班到十点,回家跟老婆吵架,心力交瘁”。数字人先承认处境——“工作和家庭两头拉扯,确实很消耗人”——然后给一个很具体的小建议:“今晚先不解决问题了,给自己十五分钟放空,明天情绪稳下来再跟老婆好好聊。”
[图片]

这些回复没有一条是预先写好的,全都由 DeepSeek 实时生成。这是跟传统"关键词匹配→调取预制话术"的智能客服最根本的区别。


几个设计上的考量

  1. 为什么不做成纯文字聊天?
    文字对话框没有温度。一个具象的数字人——会点头、会微笑、会用温和的语气说话——能让人感觉到"对面有回应",这对情感陪伴场景很重要。尤其是老年人,看字费劲,听语音更自然。
    [图片]

  2. 数据隐私怎么处理?
    所有居民对话数据本地隔离存储,不对外上传。Ai里的上下文记忆只做临时缓存,不会持久化到外部服务器。
    [图片]

方案:临时存储
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/

更多推荐