dsh-coopanion-pet
Verifieddsh-coopanion-pet · v1.9.1 · AGPL-3.0-or-later · Web UI
Coopanion desktop pet for DeepSeek Harness: a whale-girl mascot that speaks, moves and can be driven by the agent
Install
dsh plugin add dsh-coopanion-pet Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Creators
Readme
Coopanion 桌宠(DSH 插件)
把 Coopanion 的桌面宠物搬进 DeepSeek Harness 的 Web 界面, 作为零侵入的 UI 插件运行。当前版本(v1.9.1)实现 P0 桌宠本体 + P1 表现层 + P2 接入 DSH + 独立窗口——
桌宠本体
- 鲸鱼娘(DeepSeek Chan) 站在界面下沿,可自由走动
- 待机动画:呼吸、眨眼、视线跟随鼠标、头发与裙摆的弹簧摆动
- 鼠标拖拽:抓住她拖到任意位置,松手后落下,甩得快会晕
- 不挡界面:只有光标落在她本体或她自己的界面元素上时才接管指针,其余区域完全放行
说话与互动
- 气泡说话:文字逐字出现,标点处自然停顿,说完自动收起
- 表情与动作标记:
【happy, jump】先播再说话,<happy>打到那个字时播 - 提问气泡:最多 3 个选项 + 自填框,按
1–3可直接选,Esc放弃 - 悬停按钮条:靠近她时出现,可让她说话、换表情、做动作、走两步、装扮、睡觉
- 右键菜单:装扮、状态气泡开关、音效、体型、漫游倾向、回到原位、隐藏
- 音效:全部由 Web Audio 合成,不带任何音频素材,不增加额外体积
状态气泡
- 她头顶会显示你正在做什么:
正在看文件/正在写文件/正在查找/正在跑命令/等待回答/ 思考中的三个点 - 细到具体对象:
正在看文件 · config.ts、正在跑命令 · pnpm test——从工具参数里取文件名或命令,只留 basename 免得把完整路径摊在屏幕上 - 数据来自宿主侧的
agent/status与tools/execute,经 SSE 推给页面 - 对话永远优先:她说话或提问时,状态气泡自动让位
两个形象
- 鲸鱼娘(主形象,sprite rig:25 张纹理 + model.json)
- Coo(上游的第二个形象,纯代码绘制:SVG path,无纹理,三个文件共 49KB)
- 在设置面板「形象」一行切换,按需加载——默认用鲸鱼娘的人不为 Coo 付加载时间
- 两者共用同一套渲染内核(
assets/kit/);Coo 的三个文件与上游逐字节相同
对话栈
- 她头顶旁边显示最近 3 条对话(人话与她的回话),最新的贴着她的头
- 数据来自 DSH 的
session/event(user/message与assistant/message的文本) - 她说话/提问时整栈淡出让位(透明度 1 → 0.04)
- 只显示 3 条、不做分页/附件/撤回——那些是 DSH 会话 UI 的活,重复造没意义
- 设置面板可关
第三方形象包(可选)
- 导入:设置面板「形象」一行有「导入形象包」,选一个含
figure.json的文件夹 - 安全:包代码只在沙箱 iframe 里跑(
sandbox="allow-scripts",不给 same-origin), CSP 再限一道(default-src 'none'、connect-src 'none');宿主只读文件、不执行包代码 - 包不能吞点击:她的包围盒与命中圆被收进舞台内;触摸事件只在页面刚给过指针输入后才算数
- 包放在
~/.dsh/coopanion-pet-packs/<id>/,插件升级不会丢 - 只支持「选文件夹」:不引第三方 zip 库(省一个依赖),zip 请先解压再选
独立窗口(桌面上那个她) [已验证]
她可以不依赖 DSH 窗口待在桌面上——一个真透明、无边框、置顶、可穿透的窗口。
| 能力 | 说明 |
|---|---|
| 真透明 | transparent: true + backgroundColor: '#00000000',只有她和她的气泡可见 |
| 鼠标穿透 | 默认穿透到桌面;光标移到她身上才接点击(setIgnoreMouseEvents(true, {forward:true}) + 命中判定) |
| 置顶 | setAlwaysOnTop(true, 'floating'),浮在普通窗口之上 |
| 跨屏 | setVisibleOnAllWorkspaces,全屏时也在 |
| 全局快捷键 | 默认 Alt+Space 唤出/收起——DSH 没聚焦时也管用 |
| 右键菜单 | 打开 DSH 主窗口 / 打开对话面板 / 让她说话 / 隐藏 / 穿透模式 / 置顶 / 退出 |
| 托盘 | 点图标唤出/收起,右键同一套菜单 |
| 独立聊天面板 | 列会话、读历史、发消息——与 DSH 主面板操作的是同一批会话 |
13 个 pet_* 工具 |
含新增的 pet_desktop(读状态/启停独立窗口) |
架构(与上游 Coopanion、takboo/dsh-coopanion 同源):
DSH 宿主进程
└─ 插件 index.js
├─ 起一个本地 bridge 服务(刻意不在 /api 下 → 不走鉴权)
└─ spawn Electron 子进程(desktop/main.cjs)
├─ 自己的静态服务(镜像 DSH 路由形状 → client.js 一个字不用改)
├─ 宠物窗口(透明/穿透/置顶)
└─ 聊天面板窗口
关键:独立窗口连的是同一条 channel,因此 pet_* 工具、状态气泡、语音、审批
全部照旧可用——不是另一套。
DSH 没开时:她仍会出现(静态资源由 Electron 自己的服务提供),只是没有「Agent 指挥」的能力。
怎么用起来 / 怎么排查:见 docs/standalone-usage.md(操作指南 + 常见现象对照表)。
Electron 运行时:作为 optionalDependencies 声明,npm 安装时自动带(约 151 MB)。
也可以自己在设置里指定路径;找不到时独立窗口不可用,但本体照常——不假装成功。
独立预览页
访问 /api/dsh-coopanion-pet/preview 打开一个带地板与墙的干净页面,
只为「单独看她」——调形象、录屏、给别人演示。它加载的是同一套客户端模块,
因此能力与 DSH 页面里的她相同(说话、走位、收工具命令)。左上角有昼夜切换按钮。
装扮
- 8 套配色:DeepSeek / Harness / ChatGPT / Claude / Gemini / Qwen / Kimi / MiniMax
- 2 套服装:女仆装、冬装
- 面板可拖动,位置会记住
走路足迹与帧率
- 她走动时身后留一串渐隐的脚印(右键菜单或设置面板可关)
- 帧率三档:省电(静止 12fps / 活动 30fps)、平衡(静止 24fps)、流畅(跟随屏幕)
- 静止时自动降帧——呼吸、眨眼这类慢动画看不出来,但省下大半 CPU
全局说话热键
- 在任何页面里按 Ctrl+Shift+. 她就说一句(按平台自动显示 Ctrl 或 Cmd)
- 走 DSH 的
shortcuts服务,因此输入框里打字时不会误触发,也不抢终端与弹窗的键位 - 这个组合键会登记进 DSH 的快捷键速查,可在设置里改
- 拿不到
shortcuts服务时(某些组合里没有)静默跳过,桌宠本体不受影响
她自己能改什么(selfAdjust,与上游同义)
上游把「她能自己动哪些设置」分成两级——她自己的外观与习惯可以直接改, 碰到你的屏幕、耳朵与称呼的要先问。本插件照搬这套边界:
| 档位 | 行为 |
|---|---|
off 什么都不许 |
一项都不改,连问都不问 |
default 外观可以(默认) |
figure / scheme / roam / snoreSeconds / 状态气泡 / 足迹 / 帧率 直接改;sound / scale / bubbleSize / fontSize / theme / hoverButtons / user / 六类音效 / 光晕 先问 |
any 都可以 |
全部直接改 |
custom 逐项定 |
设置面板里逐项打勾;没勾过的项按 default 那张表算(上游同义) |
她问的样子与上游一致——气泡上三个选项:
| 选项 | 结果 |
|---|---|
| 可以 | 这次改,偏好不变 |
| 以后都行 | 这次改,并把这几项记成 custom 下「可直接改」——以后不再问 |
| 不用了 | 一项都不改,工具回报 declined(模型据此不再重试) |
没答(超时 120 秒、按 Esc、纯净模式里气泡不弹)与「拒绝」是两回事,
分别回报 no-answer 与 declined——前者可以再问一次,后者不该重试。
CUA 审批(可选)
装了 dsh-computer-use-win 时,
它的工具(mcp__wincu__*)要动用户的电脑之前,由桌宠当面问一句,而不是交给一个看不见的对话框。
| 档位 | 看屏幕(截图/查询/读窗口) | 操作(点、敲、拖) |
|---|---|---|
| 每轮都问 | 问 | 一轮只问一次,答完本轮不再问 |
| 动手前问(默认) | 不问 | 一轮只问一次 |
| 问一次 | 不问 | 问一次,答「可以」后 30 分钟内不再问 |
| 不问 | 不问 | 不问 |
- 她不在时不抢审批:放行给 DSH 自己的通道,免得把用户的 CUA 卡死
- 超时、拒绝、政策层出错**一律按「不许」**处理(fail closed),不重试
- 只拦
mcp__wincu__*,其余工具一律原样放行——本插件不做通用审批 - 档位在设置面板「电脑操作审批」一行改,
pet_cua也能读改
多语言
- 界面文案有 11 门(与上游一致):中文、英文、繁中、日、韩、德、法、意、拉美西、巴西葡、俄
- 默认自动跟随浏览器语言(zh* → 中文,其余先落英文),也能在设置面板里手动指定
- 改完语言立刻重挂桌宠,不必刷新页面——界面文案在模块加载时定下, 因此语言(与「显示桌宠」开关)一变就重建整层
- 配色名(DeepSeek、Claude…)是品牌名,不翻译
Agent 能指挥她(P2)
装了本插件后,Agent 多了 6 个工具,可以在对话中直接让她做事:
| 工具 | 做什么 |
|---|---|
pet_say |
让她在气泡里说话,支持 【表情, 动作】 与 <表情> 标记 |
pet_ask |
让她问用户一个问题(最多 3 个选项 + 自填框),用户的回答会作为新消息回到会话 |
pet_walk |
走到屏幕某个横向位置,可让她跑 |
pet_act |
做一个表情或动作(19 种表情、14 个动作) |
pet_dress |
换配色或服装 |
pet_status |
查她现在在不在、在哪、在做什么 |
pet_quiet |
让她安静 N 分钟:关音效、不走动、状态气泡让位,到时自动恢复。不动用户的偏好 |
pet_set |
改她自己的体型、漫游倾向、音效、状态气泡、走路足迹、帧率。其中碰到用户屏幕/耳朵的项,她会先在气泡里问一句(可以 / 以后都行 / 不用了),工具等答案再回报 |
pet_listen |
录一小段话,交给 DSH 的语音输入插件识别,返回文字(可选,见下) |
pet_speak |
用真实语音朗读一小段话(可选,见下) |
pet_voice |
列出可用的麦克风,或换一个(可选) |
pet_cua |
读或改电脑操作的审批档位(可选,见下) |
pet_desktop |
读独立窗口的状态,或打开 / 关掉它(可选:需要 Electron,见下) |
她不在(没有页面打开)时,工具会如实回报 offline 而不是假装成功。
语音(可选,不声明为必需依赖)
pet_listen 与 pet_speak 是条件可用的:宿主装了对应插件就能用,没装就如实回报、不假装成功,
也不会因此让桌宠本体加载失败——这两个能力不写进插件的 inject(inject 是必需依赖,缺一个整插件就加载不了)。
| 能力 | 接的是什么 | 未装时的表现 |
|---|---|---|
| 听(识别) | DSH 自带的 @deepseek-ai/dsh-experimental-voice-input-bundle(SenseVoice 本地识别) |
pet_listen 返回 no-speech-plugin,并说明装什么 |
| 说(合成) | dsh-xiaomi-tts 注册的同源路由 | pet_speak 返回 no-tts-plugin,并说明装什么 |
识别不自己搬 ASR:DSH 的 speechToText 是宿主侧服务,浏览器端插件运行时拿不到
(客户端插件只有 slots/connection/loader/modules),因此由宿主半边调它,页面只负责录 WAV。
合成则绕宿主一圈:路由本来就能直接 POST,但走宿主能给出「没装那个插件」这句人话,而不是让页面拿到 404 去猜。
不需要读它们的配置、也不需要 API key:识别用宿主已选好的识别器,合成由 TTS 插件自己去调上游。
按住说话:Shift+Space(在 DSH 页面里按住就说,松开即识别)。 走页面内的 keydown/keyup——DSH 的 shortcuts 服务只有 keydown、没有 keyup, 因此拿不到「松开」的时机,只能自己监听。代价:只在页面有焦点时生效。
录音时看得见:她头顶会显示「正在听 3.2s」+ 音量条,松开后显示「识别中…」。
这是替代原计划「假流式」的方案——实测分片识别比整段更慢更不准(见
docs/risk-verification.md),而「录的时候看得见」才是真正缺的那块体验。
识别结果的出口处理(照搬上游 src/asr/result.ts 的判据):
- 幻觉过滤:模型在没人说话的音频上会吐出训练语料里的字幕(「谢谢观看」「字幕由…」),
靠内容分不出真假,只能按名单拦;Whisper 类模型还会把声音标在括号里(
[Musik]、(звук пива))。 - 繁简归一:逐字判断,只换表里有的字。不引 OpenCC(内联常用字表,覆盖范围小于上游,如实说明)。
识别结果去哪:两条路,运行时自动选。
- 填进输入框(理想):走 DSH 的
conversation.input.activity槽拿inputActions; - 送进会话(降级):那个槽是 single 类型且被官方语音输入插件独占,装了它就填不进去, 这时把识别文本作为一条用户消息发出(等价于「你说了这句话」,区别是会直接触发一轮对话)。
先探测、再申请麦克风:pet_listen 会先 GET 一次 /speech 问「你能不能听」,
确认可用才去 getUserMedia。否则没装语音插件时用户会先看到一个麦克风权限框,
白点一次拒绝——而他拒绝完拿到的还是「没装插件」。已验证:未装时调用前后
navigator.permissions 的 microphone 状态都停在 prompt,即从未申请过。
她记得自己站哪儿
刷新或重开页面后回到上次的位置(存的是占窗口宽度的比例,换窗口大小也不会跑出屏幕)。
设置面板与提示词(P2)
- 设置页多出一栏「桌宠」,25 行都能改(
custom档多一行逐项开关),改完立刻生效(不必重载): 显示开关 / 状态气泡 / 走路足迹 / 双击说话 / 体型(滑块 0.5–10 倍)/ 气泡框大小 / 字号 / 轮廓光晕 / 主题(跟随·浅·深)/ 称呼 / 漫游 / 帧率 / 触摸唤醒 / 悬停按钮(勾选最多 6 个)/ 音效总开关 / 六类声音分开关 / 鼾声时长 / 她自己能改什么(含custom档的逐项开关)/ 电脑操作审批 / 语言(11 门) - 系统提示词里多一段,告诉模型她存在、有哪些工具、什么时候不该用 (不要为了让她说话而每轮都调用一次工具,她是背景陪伴不是旁白)
形象、动画与渲染内核 原样取自 Coopanion,未作修改。
安装
需要一个带 Web UI 的 profile(dsh web、DSH 桌面端,或任何含 @deepseek-ai/dsh-web-app 的组合)。
从 npm 装(推荐):
dsh plugin --profile <你的profile> add dsh-coopanion-pet
指定版本:dsh plugin --profile <你的profile> add [email protected]
从 GitHub 装(跟仓库 main 分支):
dsh plugin --profile <你的profile> add "github:wangquanqing/dsh-coopanion-pet"
从本地目录装:
dsh plugin --profile <你的profile> add "file:D:\path\to\dsh-coopanion-pet"
装完重启宿主(浏览器刷新不够,客户端模块需要重新装配)。桌面端由 Electron 独占管理其 profile,
请在桌面端的插件管理界面安装,不要直接对 desktop profile 执行上面的命令。
卸载:dsh plugin --profile <profile> remove dsh-coopanion-pet。
它做了什么
单包双面,两个半边各司其职,都不修改任何内置包文件:
| 半边 | 文件 | 职责 |
|---|---|---|
| 宿主 | index.js |
静态路由;采集运行状态经 SSE 下发;注册 pet_* 工具;收页面回传 |
| 宿主 | lib/channel.js |
双向通道:命令编号、下发、等回执、超时兜底、离线判定 |
| 宿主 | lib/tools.js |
六个 pet_* 工具的定义与执行 |
| 宿主 | lib/define-tool.js |
把属性级 required: true 转成标准 JSON Schema(见下) |
| 宿主 | lib/optional.js |
可选接缝:探测语音识别服务与 TTS 路由,不可用时返回 null |
| 宿主 | lib/cua-policy.js |
CUA 审批政策层(四级档位 + 让位);判定抽成纯函数 cuaGate 便于单测 |
| 客户端 | client.js |
shell.overlay 插槽里的桌宠,跑 WebGL 循环,收命令、发回执 |
路由用 prefix 而非 exact,因为形象包内部有相对路径导入(whale/figure.js → ../kit/rig.js、
纹理 tex/*.png),只有路径形式的 URL 才能让浏览器按相对关系解析成功。
五个 HTTP 端点
| 端点 | 方向 | 用途 |
|---|---|---|
/api/dsh-coopanion-pet/assets/* |
宿主 → 浏览器 | 渲染内核、形象包、纹理 |
/api/dsh-coopanion-pet/events |
宿主 → 浏览器(SSE) | 运行状态;Agent 下发的命令 |
/api/dsh-coopanion-pet/report |
浏览器 → 宿主(POST) | 命令回执、用户回答、桌面侧状态 |
/api/dsh-coopanion-pet/speech |
浏览器 → 宿主(POST) | 录音(base64 WAV)→ 识别文字;没装语音插件时回 501 |
/api/dsh-coopanion-pet/tts |
浏览器 → 宿主(POST) | 文字 → 合成音频;没装 TTS 插件时回 501 |
不用 DSH 的 RPC 做上下行:本插件只需要「推一件事、回一件事」,自己的路由更直接, 也不依赖宿主内部接口的稳定性。
状态为什么要绕宿主一圈
探针实测(dsh 0.2.0-rc.2)显示,浏览器端的插件 apply 运行时只有 slots、connection、
loader、modules 四个服务可用,sessions、conversation、theme、locale 都取不到;
而 shell.overlay 是 root 作用域插槽,框架注入的标准 props(GlobalStandardProps)是空接口,
也没有会话信息。所以状态气泡不能由客户端自己取数。
宿主侧则拿得到:agent/status 事件(一轮对话的开始与结束)与 tools/execute 钩子(每次工具调用)。
因此状态由宿主采集、经 SSE 下发,客户端只负责显示。同一条通道也让 pet_* 工具能反向驱动页面。
工具定义为什么要自己转 schema
DSH 官方的工具都用 defineTool() 造,它把「属性上写 required: true」的写法提成对象级的
required: [...] 数组。直接调 tools.register() 绕过这一步,注册会报
required is not supported on type "boolean"。
lib/define-tool.js 做同一件事,但不依赖 @deepseek-ai/dsh-tools——那是宿主内部包,
版本一变就可能对不上。转换只有两条规则:标了 required: true 的属性名进 required 数组,
属性自己的那个标记去掉。
资源构成
assets/
├── kit/ Coopanion 的渲染内核(约 1.2k 行,自包含,原样复制)
│ ├── rig.js Live2D 式 WebGL sprite rig:网格蒙皮 + 形变器树,零依赖
│ └── body.js 身体仿真:走路、跳跃、拖拽、19 种表情、18 个动作、粒子、物理
├── whale/ DeepSeek 鲸鱼娘形象包(原样复制)
│ ├── figure.js 把每帧转成 rig 参数,为头发/裙摆/尾巴跑弹簧,绘制表情
│ ├── model.json 骨架、部件、形变器、配色方案定义
│ ├── tex/ 25 张身体部件纹理(deepseek 配色)
│ ├── feat/ 25 张表情贴图(眼、嘴、腮红、皮肤)
│ ├── outfits/cape/ 20 张冬装纹理
│ └── schemes/ 其余 7 套配色,每套 50 张纹理,共约 9.6 MB
├── sfx.js 音效:Web Audio 合成,无音频素材(移植自 web/sound.js)
├── bubble.js 气泡:打字机、标记、提问与选项(移植自 pet-app.js 的 say/ask 分支)
├── bubble.css.js 气泡样式,配色对接 DSH 主题令牌
├── status.js 状态气泡:接 SSE,让位于对话(移植自 pet-app.js 的 stepStatus)
├── dress.js 装扮面板:配色与服装切换(新写)
├── i18n.js 界面文案的中英词表(新写)
├── trail.js 走路足迹(移植自 pet-app.js 的 trail 段,去掉对话栈后重写)
├── voice.js 录音(WAV 封装)与合成音频播放(新写)
├── hotkey.js 全局说话热键(走 DSH 的 shortcuts 服务)
├── mic-worklet.js 麦克风采集 worklet(原样取自 Coopanion)
├── ui.js 悬停按钮条与右键菜单(新写,上游这些由 World 协议驱动)
└── ../lib/ 宿主半边(通道、工具定义)
合计约 15 MB,其中 9.6 MB 是 7 套可选配色;只用默认配色的话实际加载约 2.5 MB (纹理按需拉取,切换配色时才加载那一套)。全部本地加载,无外部请求、无遥测。
渲染内核为什么能直接搬
kit/rig.js 零 import,kit/body.js 只依赖 rig.js,形象包通过 opts.kit 注入渲染器。
这套内核不依赖 Coopanion 的 World 协议、socket 或 Electron,是干净的可移植单元。
已做的适配
移植时改动集中在这几处,其余照搬:
指针改为坐标命中判定。 Coopanion 里桌宠跑在 Electron 透明窗口里,靠窗口级
setInteractive切换鼠标穿透。浏览器没有这个概念。本插件让覆盖层与其中所有元素始终pointer-events:none,把指针处理挂在window捕获阶段,用渲染内核每帧回报的命中圆 按坐标判定:命中她本体、或命中她自己弹出的按钮条与气泡时才拦事件,否则原样放行。这样处理还避开了一个陷阱:
pointer-events从none切到auto时,浏览器不会把已在 该位置的指针重定向过去,因此「悬停时才激活热区」的写法在真实点击下并不生效。不调用
set({ theme })。 渲染内核的该方法会写document.documentElement.dataset.theme, 与 DSH 自己的主题机制(body[data-ds-dark-theme])冲突。配色改由内核 CSS 的prefers-color-scheme与气泡 CSS 的 DSH 令牌各自判断。帧循环加了
dt上限。 标签页切回或系统休眠后dt会非常大,不限制的话她会一帧冲过整个屏幕。音效去掉音频文件支路。 上游会加载形象包自带的音频,本插件只用合成音, 因此
sfx.js去掉了usePack与解码逻辑,play直接进合成器。气泡按需创建、随桌宠卸载。 上游的气泡是页面固定元素,这里由插件创建并 在卸载时移除,不留残余。
状态来自宿主而非页面。 见上文「状态为什么要绕宿主一圈」。
装扮面板重写为插件内面板。 上游是独立页面(
dress.html+dress.js), 本插件把它做成跟随桌宠的浮层,只保留配色与服装两条轴。状态上报不用
keepalive,且在途只留一条。 她走动时位置每帧都在变,状态上报是高频的; 浏览器在同一个keepalive请求还没落地时再次发起会把前一个取消掉(表现为ERR_ABORTED), 于是每次上报都变成「一次成功 + 一次被取消」。因此改为普通 fetch,并把位置量化到 10px 粒度, 只有跨过一格或模式/装扮变了才发。用户的回答与命令回执走另一条必达路径(不受在途限制)。
验证状态
在真实浏览器(Edge headless + SwiftShader)里对 DSH Web UI 做过端到端验证:
v1.9.1 pet_set 的同意流程(真浏览器 + 25 条单测):
| 项 | 结果 |
|---|---|
默认档只改 roam |
通过 —— 直接 {ok:true, applied:{roamIndex:2}},不弹提问(v1.9.0 这里会被整条拒绝) |
默认档改 sound |
通过 —— 弹三个选项 1可以 / 2以后都行 / 3不用了;点「可以」后 {ok:true, applied:{sound:false}} |
| 点「不用了」 | 通过 —— {ok:false, reason:'declined', refused:['sound']},值没变 |
off 档 |
通过 —— 连问都不问,{ok:false, reason:'self-adjust-off'} |
| 点「以后都行」 | 通过 —— 偏好变 selfAdjust:'custom' 且 selfAdjustCustom.sound:true;再改同一项不再弹气泡 |
| 单测 | 通过 —— 25/25:四级分级、patch 键映射、只判本次改的键、readConsent 三种解读、pet_set 四条回报路径、设置面板 custom 档逐项开关 |
| 控制台错误 | 无 |
这两套验证都在仓库里,可以自己跑:
npm test # 25 条单测,无需浏览器,约 0.4 秒
npm run test:e2e # 真浏览器(Edge headless)跑上表 5 个用例,约 40 秒
test/e2e/ 里是一台最小验证台:它复刻宿主的四条路由(资源 / client.js / 预览页 / SSE)
与 report 端点,于是 client.js 一个字都不用改就能在真浏览器里收命令、点气泡。
浏览器路径可用 CPN_E2E_BROWSER 覆盖。
| 项 | 结果 |
|---|---|
| 桌宠渲染 | 通过 —— 覆盖层与画布存在,WebGL 上下文存活,逐像素采样确认约 5.5k 个不透明像素 |
| 画面在动 | 通过 —— 相隔 900ms 的两次采样像素值不同 |
| 拖拽 | 通过 —— 抓取本体左拖 300px,实际位移 300+px |
| 界面穿透 | 通过 —— 点她本体:界面收到 0 次事件;点界面中央:收到 1 次 |
| 气泡打字机 | 通过 —— 连续采样文字长度递增(1,3,4,…,8,10),标点处有停顿,说完自动收起 |
| 表情动作标记 | 通过 —— 【happy, jump】 触发新模式序列 crouch,air,land |
| 提问气泡 | 通过 —— 3 个选项 + 自填框,点击后回调触发并收起 |
| 悬停按钮条 | 通过 —— 靠近她时出现 6 个按钮,点击生效 |
| 右键菜单 | 通过 —— 8 个菜单项,Esc 可关闭 |
| 装扮面板 | 通过 —— 8 套配色 + 2 套服装,切换后纹理真的换了且画面仍在动 |
| 状态气泡样式 | 通过 —— 状态类样式与思考动画规则已注入 |
| 状态链路 | 通过 —— 宿主 emit('agent/status','running') 后,SSE 依次推出 none → think → none |
| 音效模块 | 通过 —— 6 类声音,AudioContext 可用 |
| 主题未被污染 | 通过 —— html 上无 data-theme |
| 控制台错误 / 失败请求 | 均无 |
P2 工具闭环(用一个会回执的客户端扮演页面,让探针调真实工具):
| 项 | 结果 |
|---|---|
| 工具注册 | 通过 —— 6 个 pet_* 全部注册,disposer 都是函数 |
| 命令下发 | 通过 —— SSE 依次推出 5 条命令帧,编号 c1–c5、参数完整 |
| 回执闭环 | 通过 —— 扮演页面对 5 条命令全部回执 204,工具拿到 ok:true |
| 离线如实回报 | 通过 —— 没有页面连上时返回 offline 与原因,不假装成功 |
| 端点校验 | 通过 —— /report 只收 POST(GET 返回 405),坏 JSON 与未知 kind 返回 400 |
P2 设置面板与提示词:
| 项 | 结果 |
|---|---|
| 设置面板出现 | 通过 —— 设置页导航多了「桌宠」一栏,面板含 3 个开关 + 2 组选择 |
| 面板改设置生效 | 通过 —— 体型改到「大」后,她的画布从 107px 变成 135px |
| 提示词注入 | 通过 —— systemPrompt.assemble() 渲染出 7914 字符,里面含完整的桌宠段落 |
v1.0 收尾三项:
| 项 | 结果 |
|---|---|
pet_quiet |
通过 —— minutes: 1 返回「安静约 1 分钟」,安静期间位置 0 → 0 不动,minutes: 0 立刻恢复 |
pet_set |
通过 —— 依次改成 small / large / medium + roaming quiet,均返回 ok:true |
| 参数校验 | 通过 —— 空 pet_set 报 empty,size: 9 报 bad-size,roam: -1 报 bad-roam |
| 位置记忆 | 通过 —— 存下比例、重载后回到原处 |
v1.1 改进:
| 项 | 结果 |
|---|---|
| 状态气泡显示具体对象 | 通过 —— 正在看文件 · config.ts;a.ts → b.ts 切换会重绘 |
| 静止降帧省电 | 通过 —— 静止 48 次画布变化/2s(≈24fps),活动时 123 次/1.5s(≈82fps) |
v1.2 多语言(真实浏览器,Edge headless + SwiftShader):
| 项 | 结果 |
|---|---|
| 自动跟随浏览器语言 | 通过 —— 浏览器 zh-CN 时按钮条为「让她说句话/换个表情/…」 |
| 切到英文 | 通过 —— 偏好改 lang: en 后重挂,按钮条变 Say something / Change expression / …,装扮面板变 Dress up / Colours / Outfit / Maid / Winter cape |
| 状态气泡两语 | 通过 —— 中:正在看文件 · config.ts、正在跑命令 · pnpm test;英:Reading a file · config.ts、Running a command · pnpm test(六档全对) |
| 提问气泡 | 通过 —— 自填框 Say something else… / 按钮 Send |
| 设置面板语言行 | 通过 —— 「桌宠」栏有「语言」一行(自动/中文/English),点 English 后偏好落盘 zh → en 且整层重挂,面板标签同时变英文 |
| 显示开关即时生效 | 通过 —— 设置面板置 enabled: false 后层与画布归零;置回 true 后重新挂上并画出 5369 个不透明像素 |
| 「隐藏桌宠」仍说告别话 | 通过 —— 菜单点「隐藏桌宠」后气泡出现「那我先躲起来,」,约 1.6 秒后收起并落盘 enabled: false |
| 渲染回归 | 通过 —— 126×126 画布、约 5.2k 不透明像素,相隔 900ms 两次采样不同(画面在动) |
| 控制台错误 | 无 |
v1.3 足迹、帧率与可选语音(真实浏览器,Edge headless + SwiftShader):
| 项 | 结果 |
|---|---|
| 走路足迹 | 通过 —— 走之前 0 个点;走动中稳定 3 个点,位置与透明度随帧衰减(701px,0.57 → 659px,0.76 → 617px,0.94) |
| 关掉足迹 | 通过 —— trail: false 后连续采样可见点恒为 0 |
| 帧率:静止 | 通过 —— 省电 12fps、平衡 24fps、流畅 61fps(与档位表一致) |
| 帧率:走动 | 通过 —— 省电 30fps、平衡 61fps、流畅 61fps |
| 设置面板新行 | 通过 —— 新增「走路足迹」「帧率」两行,八行齐全 |
| 语音识别(装了语音插件) | 通过 —— 用 TTS 合成一句真语音喂给假麦克风,pet_listen 认出「你好,我是捉宠,请听我说一句话。」(原文「桌宠」被识别成「捉宠」,属识别器本身的误差) |
| 语音合成(装了 TTS 插件) | 通过 —— pet_speak 拿回 99884 字节 audio/wav,RIFF 头正确 |
| 未装语音插件 | 通过 —— pet_listen → no-speech-plugin、pet_speak → no-tts-plugin,都带一句「装什么」;桌宠本体照常渲染(5908 个不透明像素)且能说话 |
| 工具层 | 通过 —— 10 个工具注册;pet_listen/pet_speak 的 schema 转换、在线回执、离线、超时、空文本、越界参数逐条核对 |
v1.4 全局热键与 CUA 审批(真实浏览器 + 纯逻辑单测):
| 项 | 结果 |
|---|---|
| 全局说话热键 | 通过 —— 按 Ctrl+Shift+. 后气泡出现「这会儿挺安静的。」;默认关调试时零日志 |
| 热键登记进目录 | 通过 —— DSH 设置页「快捷键速查」里出现「让桌宠说句话 Ctrl+Shift+.」 |
| 热键不误触发 | 通过 —— 靠 shortcuts 服务的局部仲裁:输入框/终端/真弹窗区域不响应 |
| CUA 政策层单测 | 通过 —— 30/30:工具分类、四级档位 × 看/动、授权期、撤销、一轮一答、fail closed、参数校验 |
| CUA 闸门集成测试 | 通过 —— 15/15:只拦 mcp__wincu__*(其余 5 类工具名放行)、离线放行、拒绝/超时拦下、政策层抛错也拦下、空调用放行 |
| 设置面板 | 通过 —— 九行齐全(新增「电脑操作审批」),四个档位按钮可点,点后偏好逐项落盘 |
| CUA 确认气泡 | 通过 —— 两个选项「1可以 / 2不要」,键盘 1、2、Esc 都能正确收掉 |
| 回归 | 通过 —— 渲染、双语、足迹、帧率、显示开关、语音安全跳过全部照旧 |
v1.8 对话栈与形象包系统(真实浏览器):
| 项 | 结果 |
|---|---|
| 40 对话栈渲染 | 通过 —— 推 3 条后正确显示,user/pet 分色、位置从下往上分层 |
| 40 让位对话 | 通过 —— 她说话时透明度 1 → 0.04,说完回到 0.91 |
| 41 包清单端点 | 通过 —— 报出内置 whale/coo 与已装的包 |
| 41 沙箱隔离 | 通过 —— sandbox="allow-scripts",主页面访问 frame 内文档被拒(跨源) |
| 41 CSP | 通过 —— default-src 'none'; script-src 'self'; connect-src 'none' |
| 41 沙箱真跑一个包 | 通过 —— 造了个「会动的小方块」包,切过去后沙箱回传布局(x=329)且画面在动(x 在 329–360 间摆动),控制台零错误 |
| 42 导入 | 通过 —— 选文件夹 → stage → install → 清单里出现新包 |
| 42 拒绝 | 通过 —— 空清单回 no-files;内置 id 不许覆盖 |
| 设置面板 | 通过 —— 「形象」行列出内置 + 第三方包,有「导入形象包」入口 |
| 控制台错误 | 无 |
v1.7 形象与预览页(真实浏览器):
| 项 | 结果 |
|---|---|
| 43 切到 Coo | 通过 —— canvas 消失、变成 8 个 SVG path(纯代码绘制)、画面在动(两次快照 882 → 958 字节) |
| 43 切回鲸鱼娘 | 通过 —— canvas 回来,偏好落盘为 whale |
| 43 设置面板 | 通过 —— 「形象」一行有两个按钮「鲸鱼娘 / Coo」 |
| 46 预览页 | 通过 —— /preview 打开后墙、地板、昼夜按钮都在,桌宠挂载成功(hasPet: true) |
| 46 昼夜切换 | 通过 —— 点按钮后 data-pv 由 light 变 dark |
| 44 语音模型状态 | 通过 —— GET /speech 报可用性与包名;设置面板有「语音识别」行与「检查」按钮 |
| 设置面板总行数 | 通过 —— 13 行(新增「形象」与「语音识别」) |
| 控制台错误 | 无 |
v1.6 语音增强(真实浏览器 + 纯函数单测):
| 项 | 结果 |
|---|---|
| 48 切句器 | 通过 —— 你好。我是桌宠!现在测试? → 切成三句 |
| 49 幻觉过滤 | 通过 —— 拦下「谢谢观看」「字幕由…」「[Musik]」「(звук пива)」「Bruit」「Thanks for watching」与纯重复「啊啊啊啊」;保留「你好」与「哈哈哈哈」 |
| 50 繁简归一 | 通过 —— 這個東西很好,我們說話的時候要聽清楚。 → 这个东西很好,我们说话的时候要听清楚。 |
joinSpeech 分片拼接 |
通过 —— 英文片段补空格(hello world),中文不补(你好世界);与上游语义一致 |
| 52 按住说话 | 通过 —— 按住 Shift+Space 显示「正在听 1.4s」+ 音量条,松开显示「识别中…」 |
| 53 麦克风选择 | 通过 —— 扫描列出 3 个设备(含系统默认),设置面板有下拉框与扫描按钮 |
| 55 填输入框(理想路径) | 通过 —— 槽可用时拿到 insertText/setDraft/submit/captureInsertion 与 sessionId |
| 55 送进会话(降级路径) | 通过 —— 槽被官方语音插件占用时自动降级(mode: 'session'),insert() 返回 true |
| 录音反馈(替代 54) | 通过 —— 计时 + 音量条,状态机 idle → recording → transcribing |
| 控制台错误 | 无(降级路径的 503 是「测试页面没有活跃会话」,属预期) |
v1.5 十二项配置 + 三项小功能(每项都开独立页面、加载前写偏好,排除同步时序):
| 项 | 结果 |
|---|---|
| 59/60 气泡框与字号 | 通过 —— 字号 14.5px → 21.75px,内边距 14px → 21px,圆角 18px → 27px(框与字各自独立) |
| 61 主题三档 | 通过 —— --cpn-ink 浅 #1B1626 / 深 #FFFFFF,data-cpn-theme 同步;auto 跟随 DSH |
| 35 轮廓光晕 | 通过 —— glow=0 时 filter: none,glow=12 时 drop-shadow(… 18px) |
| 65 体型滑块 | 通过 —— 画布 133px(1 倍)→ 396px(3 倍),连续可调 |
| 57/58 声音分类与鼾声 | 通过 —— 六类开关与鼾声时长都落盘并生效 |
| 62 悬停按钮 | 通过 —— 勾选 [say,dress] 后按钮条只剩两个,改回六项即恢复 |
| 56 称呼 | 通过 —— 气泡变成「王哥,我在这儿呢。」 |
| 64 触摸唤醒 | 通过 —— poke 时戳她回话、none 时不回话 |
| 66 安全边界 | v1.9.1 重做 —— 四级 selfAdjust 落盘;ask 层的项真的用气泡问人(可以/以后都行/不用了),拒绝与超时分开回报。见上方「v1.9.1」表 |
| 67 十一门语言 | 通过 —— 每门 128 键完整无缺;日/韩/德/法/俄/繁中逐一实机核对 |
| 34 纯净模式 | 通过 —— #cpn-clean 下气泡与按钮条都不出现,本体照常渲染 |
| 63 双击说话 | 通过 —— 双击弹出输入框(带自填框) |
| 回归:拖拽 | 通过 —— 左拖 180px,实际位移 205px |
| 回归:界面穿透 | 通过 —— 点她本体界面收到 0 次;点界面中央收到 1 次 |
| 控制台错误 | 无 |
未验到的部分(如实说明):工具的端到端验证用的是「扮演页面的客户端」而非真实浏览器标签页——
因为探针在服务启动后几秒就开始轮询,而浏览器要十几秒才把桌宠加载完并连上 SSE,两者的时序在这个
验证环境里总是错开。因此「SSE 命令帧真的会让浏览器里的她说话/走路」这一段,是由以下两点合并支撑的:
(1)命令帧格式与 runCommand 的处理分支已逐条核对;(2)页面自身的执行能力(api.say、
装扮切换等)已在浏览器里单独验证通过。真机上两者是同一个页面,链路是通的,但没有一次
「真实浏览器 + 真实工具调用」的合并验证,建议首次使用时留意。
配置
偏好写在 localStorage 的 dsh-coopanion-pet:prefs(JSON)。右键菜单与装扮面板改的设置即时生效并落盘:
{ "enabled": true, "sizeIndex": 1, "roamIndex": 0, "sound": true,
"startShare": 0.78, "scheme": "deepseek-maid", "statusBubble": true, "lang": "auto" }
| 键 | 含义 |
|---|---|
enabled |
是否显示桌宠(右键「隐藏桌宠」会置为 false,改回需手动编辑) |
sizeIndex |
体型档位:0 小 / 1 中 / 2 大 |
roamIndex |
漫游倾向:0 安静 / 1 随意 / 2 活跃 |
sound |
音效开关 |
startShare |
初始横向位置,占窗口宽度的比例 |
scheme |
装扮,配色-服装,如 claude-maid、deepseek-cape |
statusBubble |
是否显示状态气泡 |
lang |
界面语言:auto 跟随浏览器,或 zh / en 显式指定 |
trail |
是否显示走路足迹 |
fpsIndex |
帧率档位:0 省电 / 1 平衡 / 2 流畅 |
cuaLevel |
电脑操作审批档位:ask-each-turn / ask-before-acting / ask-once / never-ask |
selfAdjust |
她自己能改什么:off / default / any / custom(见上面那张表) |
selfAdjustCustom |
custom 档下逐项定:{ "sound": true, "scale": false, … },没写的项按 default 算 |
scale |
连续体型倍率(0.5–10,1 为标准);sizeIndex 是三档快捷方式,两者同步 |
theme |
气泡与本体主题:auto / light / dark |
glow |
轮廓光晕强度(0–16) |
dressPos |
装扮面板的位置(拖动后记下) |
从页面里驱动她
客户端暴露了 window.__dshCoopanionPet,供验证脚本与将来的 Agent 接线使用:
__dshCoopanionPet.say('【happy】你好呀。'); // 说话,可带标记
__dshCoopanionPet.ask('选一个?', ['甲', '乙', '丙']); // 提问
__dshCoopanionPet.do('jump'); // 表情或动作
__dshCoopanionPet.walk(400, true); // 走到横向 400px,true 为跑
__dshCoopanionPet.setScheme('claude-cape'); // 换装扮
__dshCoopanionPet.openDress(true); // 开关装扮面板
__dshCoopanionPet.layout; // 当前布局快照
后续
P2 及以上(注册 pet_* 工具让 Agent 直接指挥她、设置面板、语音、电脑操作等)
见 ROADMAP.md,其中按移植难度与依赖做了分级。
许可
形象、动画与渲染内核来自 Coopanion,版权归 Pal AI Lab,AGPL-3.0-or-later。 本插件作为衍生作品同样以 AGPL-3.0-or-later 发布,详见 LICENSE。