小舞 · DSH 桌宠(dsh-plugin-xiaowu)
一个常驻 DSH Web 界面右下角的桌面宠物插件。她会跟着当前会话里 agent 的工作状态换动作,
任务做完的时候跳起来撒花,并且用一段运行时合成的甜美声音说一句「做完啦……」。
声音不是任何录音或采样:全部由 WebAudio 振荡器实时合成(详见下面「声音设计」)。
角色形象是按「斗罗大陆里那位兔耳少女」的气质原创绘制的 SVG 小人(粉发、兔耳、长辫、
白色兔耳发饰、粉色裙装),不是任何动画素材或截图的搬运。
原创说明
这是本人(jxboop)的原创作品:
- 形象:纯手写 SVG 绘制的 chibi 小人。粉发、兔耳、长辫、白色兔耳发饰、粉白连衣裙,
都是一条条
<path> / <circle> 画出来的,没有使用任何动画截图、贴图或第三方素材。
气质上致敬「斗罗大陆」里的兔耳少女小舞,但不是该作品的官方形象,也没有搬运其素材。
- 动作:七个心情各自的 keyframes(呼吸、眨眼、甩辫、抡锤、蹦跳、垂耳、Zzz)为自己编写。
- 状态机:
PetFollow 如何把 DSH 会话事件折叠成心情、以及「先补读事件日志再判定
成功还是报错」的时序,为自己设计。
- 声音:全部由 WebAudio 振荡器实时合成,没有任何录音或采样文件。四音节旋律动机、
FM 共振峰、气音瞬态、立体声摆位都是自己调参的。
- 代码:Host 半边、浏览器半边与测试套件为本项目原创,采用 MIT 许可。
二次创作或商用请遵守 MIT 许可并保留作者署名。
目录
她长什么样
纯手写 SVG 画的 chibi 小人,约 112px 高,贴在窗口右下角:粉发 + 前额刘海、两条细长兔耳、
一条垂到脚边的长辫(末端系蝴蝶结)、白色兔耳发饰、粉白连衣裙配缎带、脚上小圆靴。
她有一缕很淡的灵光在背后呼吸;眼睛会跟着鼠标转。
preview.html / preview-zoom.html 是真实浏览器渲染出来的七个心情状态预览页,
用浏览器直接打开就能看到(preview-zoom.html 是放大 4.2 倍细看版)。
她会做什么
心情由当前会话(ctx.sessions.list.current)的真实状态推导,不是随机播放:
| 心情 | 触发条件 | 动作 | 声音 |
|---|
idle 待机 | 会话空闲 | 呼吸、眨眼、耳朵轻抖、偶尔甩辫子 | 偶尔一声轻跳 |
think 思考 | 正在跑,但还没发工具 | 左右轻晃、头顶冒「?」 | — |
work 干活 | 最新事件是 tool/call | 抡小锤子、火星四溅、汗滴 | 每次新工具一声轻脆 tick |
wait 等确认 | approval/asked 尚未 decided | 举手、头顶「!」、光圈脉动 | 柔和上行双音 |
done 完成 | 一轮 turn/end 成功结束(running 由真变假) | 原地跳起、爱心+星星+音符 | 「做完啦……」 + 收尾闪音 |
oops 出错 | lastAgentError 或 turn/end reason=error | 耳朵垂下、冒汗、掉泪 | 下行小三度 |
sleep 睡觉 | 90 秒没动静,或还没打开任何会话 | 闭眼、冒 Zzz | 四声下行摇篮曲 |
交互:
- 点她一下:蹦一下 + 一声「啵」,泡泡里冒出「呼哇~好舒服!」。
- 点右上角小喇叭:开关声音(选择记在
localStorage,键名 dsh-plugin-xiaowu:audio)。
- 点状态胶囊 / 悬停:展开小面板,显示当前心情、正在用的工具名、会话标题。
- 浏览器禁止自动播放音频,所以第一次点击/按键时才会解锁 AudioContext;解锁之前她依然会动,只是不出声。
- 尊重
prefers-reduced-motion:动画会被压到最短。
声音设计
四段「咿呀」的旋律动机对应语音的音节节奏,全部由 lib/client.js 里的 voice 模块现场合成:
| 时间 | 音高 | 对应 | 音色处理 |
|---|
| 0.00s–0.24s | G5 (784Hz) | 「做」 | 三角波主音 + FM 调制 + 气音瞬态 |
| 0.20s–0.50s | A5 (880Hz) | 「完」 | 同上,略强 |
| 0.47s–0.77s | G5 (784Hz) | 「啦」 | 同上,稍弱 |
| 0.75s–1.41s | D5 (587Hz) | 「……」 | 长音下滑,收尾 |
| 1.32s–1.82s | G5→C6→E6 | 闪音 | 铃铛式正弦 + 二次谐波 |
每个音节叠了三层:
- 主音:
triangle 振荡器,带 5.4Hz 轻微颤音(vibrato),起音有 20–45ms 的滑入(ramp)。
- FM 调制:
sine 调制器以约 1.7 倍基频调制主音频率,做出类似人声共振峰的「唔」感。
- 气音瞬态:40ms 白噪声经带通滤波(中心频率约 1.7 倍基频),模拟辅音起音,让音节听起来像「说话」而不是纯音阶。
- 低八度衬底:每个音节再加一层低八度正弦,音量约 30%,让声音有胸腔共鸣的暖度,不会太尖。
再加一层立体声:四个音节左右轻摆(-0.22 / 0.06 / 0.24 / 0),闪音尾随其后,听感更「甜甜的」。
整段约 1.9 秒,主输出增益 0.9、单音峰值 0.13–0.20,保证不会突然吓人。
其他提示音(hop / tick / chime / oops / pop / lullaby)用同一套 bell() 原语拼出来。
想换音高或换成整句语音朗读,改 phrase() 里的 notes 数组即可。
安装与卸载
环境要求
| 项目 | 要求 |
|---|
| DSH | 已在跑 dsh web(有 web profile) |
| Node.js | ≥ 18 |
| git | 安装时需要 |
| 浏览器 | 支持 Web Audio(Chrome / Edge / Firefox / Safari 都行) |
安装
git clone https://github.com/jxboop/dsh-plugin-xiaowu.git
node dsh-plugin-xiaowu/install.mjs
PowerShell(5.1 不支持 &&,用分号):
git clone https://github.com/jxboop/dsh-plugin-xiaowu.git; node .\dsh-plugin-xiaowu\install.mjs
脚本幂等,做四件事:
- 把插件放进
<DSH 主目录>/plugin-src/dsh-plugin-xiaowu
- 在
<profile>/node_modules 建链接(Windows 用 junction,无需管理员权限)
- 把包名写进
<profile>/package.json 的 dsh.profile.bundles(只改这一个字段)
- 打印重启指引
然后:
Ctrl+C 停掉 dsh web,重新执行 dsh web
- 浏览器
Ctrl+Shift+R 硬刷新
为什么必须重启:bundle 列表与插件树只在进程启动时组合,热加载覆盖不到新装的插件。
常用参数:
node install.mjs --profile web # 指定 profile(默认 web)
node install.mjs --dsh-home D:\dsh-home # 自定义 DSH 主目录
node install.mjs --source ./本地目录 # 从本地目录安装(改代码时用)
node install.mjs --help
DSH 主目录默认按 $DSH_HOME → ~/.dsh 的顺序推断。
只想先看看她长什么样
不用装插件:用浏览器直接打开仓库里的 preview.html(preview-zoom.html 是放大 4.2 倍细看版)。
卸载
node dsh-plugin-xiaowu/uninstall.mjs # 移除链接与 bundles 注册,保留插件目录
node dsh-plugin-xiaowu/uninstall.mjs --purge # 连插件目录一起删
重启 dsh web 生效。
改完源码怎么生效
重开 dsh web(或重启当前那个进程),再刷新浏览器页面。
profile 的 patchReload 是 live,所以首页刷新会重新拿配置;但已经在跑的进程不会重新组合插件树。
更新流程(不用重启)
如果你的 DSH 装了 dsh-super-injector(本机开发常用的可选工具),改完源码可以热重载,不必重启:
- 改:动
lib/client.js(浏览器半边)或 lib/index.js(node 半边)。
- 自测:跑下面「开发与自测」里的测试。
- 推送:让注入器热重载这个包 ——
dev_reload_package packageName: dsh-plugin-xiaowu
它清掉该包的模块缓存、重建 fiber,并把新的客户端 bundle 以新 revision 发布出去(失败会自动回滚保留旧代)。
之后刷新浏览器页面即可,不用重启 dsh web。
没有注入器也没关系:改完重启 dsh web 即可。
怎么确认刷新后跑的确实是新版本
bundle 里有一个版本戳常量 BUILD(lib/client.js 顶部,语义化版本,改完记得 +1)。
载入时它会把 window.__DSH_XIAOWU__ 挂到页面上并打一行日志:
[xiaowu] 桌宠已载入 build 0.1.2 — window.__DSH_XIAOWU__ 可核对
刷新后的页面在 DevTools 控制台里敲 window.__DSH_XIAOWU__.build,跟文件里的 BUILD 对得上,
就说明这一页跑的是磁盘上这一版,而不是缓存的旧 bundle。
开发与自测
node --check lib/client.js # 语法
node test/harness.cjs # 无浏览器:挂载 + 状态机(各个心情逐个断言)
需要浏览器级渲染的测试要装开发依赖(只有改代码的人需要):
npm i # jsdom / react / react-dom
node test/dom.cjs # jsdom + 真 React 18:真渲染、真 DOM、真音频调度、版本戳
node test/preview.cjs # 生成 preview.html / preview-zoom.html
实现说明
插件是双面的(DSH 客户端插件的标准形态):
- node 半边(
lib/index.js):空的 apply()。它的唯一作用是让 Cordis 树里出现这一行,
这样 @deepseek-ai/dsh-client-modules 扫描 dsh.client 声明时能发现本包。
- 浏览器半边(
lib/client.js):手写的懒 CJS bundle,通过 window.__ModuleLoader__.load
注册工厂;不需要任何构建步骤(没有 JSX,用 React.createElement 的 h() 小助手)。
浏览器半边做了三件事:
- 占位:
ctx.slots.inject("shell.overlay", …) —— shell.overlay 是 ui-layout 声明的
「整框浮动层」,本身是点击穿透的,所以 CSS 里只有 .dsh-xw-stage 把自己重新打开
pointer-events:auto,不会挡住下面的界面。
- 读状态:
PetFollow 跟着 ctx.sessions 走。它订阅会话列表(当前会话变化)、
会话快照(running、lastAgentError、awaitingFirstTurn)和事件窗口
(turn/start、turn/end、tool/call、tool/result、approval/asked、approval/decided),
把它们折叠成上面那张表里的七个心情。顺序很讲究:running 由真变假时先补读一遍事件日志,
再判断是「做完」还是「出错」,所以一轮报错的 turn 不会被误当成成功来庆祝。
- 画与响:React 组件读 store,把心情写到
data-mood 上,CSS 里每个心情一套 keyframes;
useMoodSounds 在心情边沿触发对应音效(done 边沿就是那句「做完啦……」)。
依赖清单写在自己的 cordis.patch.yml 里(一行 insert),package.json 的 dsh.client.inject
声明了必须先在线的客户端插件,剩下的 react / react-dom 走 DSH 客户端的静态模块表,不需要额外依赖。
许可
MIT © 2026 jxboop
形象、动作、状态机与全部音频合成算法均为原创;二次创作或商用请遵守 MIT 许可并保留署名。