Skip to content

dsh-coopanion-pet

Verified

dsh-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,是干净的可移植单元。

已做的适配

移植时改动集中在这几处,其余照搬:

  1. 指针改为坐标命中判定。 Coopanion 里桌宠跑在 Electron 透明窗口里,靠窗口级 setInteractive 切换鼠标穿透。浏览器没有这个概念。本插件让覆盖层与其中所有元素始终 pointer-events:none,把指针处理挂在 window 捕获阶段,用渲染内核每帧回报的命中圆 按坐标判定:命中她本体、或命中她自己弹出的按钮条与气泡时才拦事件,否则原样放行。

    这样处理还避开了一个陷阱:pointer-events 从 none 切到 auto 时,浏览器不会把已在 该位置的指针重定向过去,因此「悬停时才激活热区」的写法在真实点击下并不生效。

  2. 不调用 set({ theme })。 渲染内核的该方法会写 document.documentElement.dataset.theme, 与 DSH 自己的主题机制(body[data-ds-dark-theme])冲突。配色改由内核 CSS 的 prefers-color-scheme 与气泡 CSS 的 DSH 令牌各自判断。

  3. 帧循环加了 dt 上限。 标签页切回或系统休眠后 dt 会非常大,不限制的话她会一帧冲过整个屏幕。

  4. 音效去掉音频文件支路。 上游会加载形象包自带的音频,本插件只用合成音, 因此 sfx.js 去掉了 usePack 与解码逻辑,play 直接进合成器。

  5. 气泡按需创建、随桌宠卸载。 上游的气泡是页面固定元素,这里由插件创建并 在卸载时移除,不留残余。

  6. 状态来自宿主而非页面。 见上文「状态为什么要绕宿主一圈」。

  7. 装扮面板重写为插件内面板。 上游是独立页面(dress.html + dress.js), 本插件把它做成跟随桌宠的浮层,只保留配色与服装两条轴。

  8. 状态上报不用 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。