Перейти к основному содержимому
R

dsh-bonk-pet

reisen-ww/dsh-bonk-pet

Whale-girl desk pet in the corner of the DSH Web GUI. It sings along to a bundled track while the agent works; when a tool call fails a steel pipe falls on its head. Click it to bonk the basin it wears, which costs 12 rice — knock enough and it gets hungry and wanders off to beg, and rice appears somewhere on the page for it to dash across and eat. Draggable within the viewport, wriggles and complains if grabbed mid-dash, volume and behaviour configurable from Settings. All artwork, sound effects and the song ship inside the package.

Установка

dsh plugin --profile web add github:reisen-ww/dsh-bonk-pet

README

dsh-bonk-pet 🔧🐋

敲盆宠物 — DSH Web GUI 的悬浮小鲸鱼。 干活的唱歌;出错就掉钢管砸它;你可以敲它的铁盆;敲完它会跑去讨白饭吃。


它是什么

一只住在 DSH Web 界面右下角、坐在一只旧铁盆里的小鲸鱼。

什么时候它会怎样
agent 开始干活摇摆身体唱歌,头顶飘音符 ♪♫,并轮流出中英文歌词(放歌见下文)
任务结束停止唱歌,安静下来
agent 执行出错(工具失败 / 回合失败)天上掉下一根钢管,砸它头上,它被压扁、冒星星、喊疼
你点它的铁盆盆和它一起抖,冒一句台词(「别敲了!」「在跑了在跑了」)
你连敲 3 下饿了就跑出去找吃的,桌上随机位置冒出一坨白饭
饭出现了它看见了,咻一下冲过去自己吃掉(不用你点)
你连敲 6 下开始阴阳你:「你还敲上瘾了是吧」
饿了(饱食度见底)无精打采地晃,喊饿
闲着的时候每 25 秒掷一次骰子,饿了就有几率自己去找饭吃

敲盆会消耗它的饱食度(敲一下 -12)。所以敲得越狠,它越饿,越要出去找饭。 ——这是设计上的闭环:你的不耐烦会变成它的饥饿


放歌 & 换形象

两样东西都丢进 ~/.dsh/bonk-pet/,然后刷新页面。

# 放歌(文件名带 letmego 的优先)
copy 你的歌.mp3 "$env:USERPROFILE\.dsh\bonk-pet\letmego.mp3"

# 换形象(把你自己生成的图命名为 pet.png)
copy 你生成的宠物图.png "$env:USERPROFILE\.dsh\bonk-pet\pet.png"

放素材:~/.dsh/bonk-pet/

四个槽位,全部可选,互相独立。 缺哪个就用代码画的那个。

插件启动时还会在这个目录写一个 loaded.txt —— 那是它的"到岗打卡", 记着挂载时间、PID 和启动命令。别删,它是唯一能证明插件真的加载了的 证据(见下面"装完必须重启")。它不会被当成歌或音效(.txt 不在白名单里)。

文件名是什么缺省表现
letmego.mp3(或任何带 letmego 的音频)干活时唱的歌安静地干活
敲盆.mp3敲盆音效无声,但照样抖动+冒台词
钢管落地.mp3钢管砸中音效无声,但照样掉钢管
pet.png宠物本体(可以自带铁盆)用代码画的鲸鱼
basin.png单独的铁盆(可选)见下方"铁盆规则"
pipe.png钢管用代码画的钢管
rice.png桌上那坨白饭(随机位置出现,宠物冲过去吃用代码画的饭团

格式:图片认 .png / .webp / .jpg / .jpeg / .gif / .svg; 音频认 .mp3 / .m4a / .aac / .ogg / .opus / .wav / .flac / .webm

换任何东西都不用改代码、不用重装插件,丢文件进去刷新页面即可。

铁盆规则(重要)

铁盆有两种摆法,靠你放不放 basin.png 来选:

你放了什么效果
pet.png图里已经含盆)+ 不放 basin.png用你图里的盆,代码画的盆隐藏,点整只宠物都是敲盆 ← 你现在是这种
pet.png纯宠物,不含盆)+ basin.png两层叠加,盆会单独抖动,可以单独点
只放 pet.png(纯宠物)+ 不放 basin.png⚠️ 代码画的盆也会隐藏(因为无从判断你图里有没有盆)
什么都不放用代码画的鲸鱼 + 代码画的盆

如果你要放纯宠物的图,请同时放一张 basin.png,否则盆不会画出来。

图片要求

  • 透明底 PNG(不要白底——会变成一个白方块贴上去)
  • 正方形或接近正方形,宠物居中,四周留点空隙
  • 图里的宠物自带盆时,构图要让盆在下方、完整可见

  • 文件名带 letmego 的会被优先选中
  • 第一次会被浏览器拦截,宠物脚下出现「🔇 点我开声音」按钮,点一下就好, 之后会记住(存在 localStorage)
  • 音效文件会被自动从歌单里剔除,不会被当成背景音乐轮播

为什么不能直接指定路径:DSH 的客户端插件(浏览器里那一半)没有加载外部文件的能力 —— dsh.client 只认 platform / inject / external / immediately 四个字段,没有静态资源目录, 所有官方客户端插件都是把素材 base64 内嵌的。所以换了个思路:歌和图都由宿主端(Node 那一半) 读,通过一条只读路由喂给浏览器。

关于默认形象

image_gen 后端一直 auth_failed(外部依赖挂了),所以默认形象是代码画的 SVG 鲸鱼, 另外留了上面那些槽位让你自己放图。你放的图已验证可用(找到 → 替换 → 真实解码都测过)。


想先看效果(不用装、不用重启)

node tools/serve-demo.js

然后浏览器打开 http://127.0.0.1:43900/

它会读你 ~/.dsh/bonk-pet/ 里的真实素材,用同一个客户端 bundle 渲染, 并且真的提供 /api/bonk-pet/* 那三条路由。所以你看到的形象、歌、音效 就是装好之后的样子。页面上有按钮可以手动触发「开始任务」「执行出错」「敲盆」。

唯一缺的是 DSH 的事件流 —— 真实界面里 agent 一干活它自动唱、一出错自动掉钢管, 这里得手动点。

直接双击 demo.html 也能打开,但浏览器不给本地网页读别的目录, 所以那样只能看到代码画的默认形象,也没有声音。 要看自己的素材,用上面那条命令。


装法

dsh plugin --profile <你的 profile> add <本目录路径或 git 地址>

⚠️ desktop profile 被 DSH CLI 硬性拒绝(bin.jsrejectElectronProfile)。 给 Desktop 装的话,在 profile 目录 pnpm add <路径>, 然后往 profile 的 package.jsondsh.profile.bundles cordis.patch.yml 里加一行(二选一,别同时加):

# cordis.patch.yml(推荐:patchReload 是 live,改完不用重启)
- insert:
    - id: bonk-pet
      name: 'dsh-bonk-pet'

装完要重启 —— 但先试一下市场开关

这一条我前后改了两次结论,把两次都写下来,因为过程比结论有用。

先说事实(都在独立实例上实测过):

做法dsh webDesktop(Electron) 上
运行中改 cordis.patch.yml有效(路由 404 → 等 20 秒 → 200)改完等 30 秒无反应
市场里的启用开关(hotMount有效,且功能完整(见下)未验证(机制相同,但没在你的宿主上按过)

市场开关那条路我验得很彻底:起了个 patch 层为空的实例(插件只能靠市场挂载), 调用 toggle 之后——{"ok":true,"live":["dsh-bonk-pet"],"hot":true,"restart":false}, 然后跑完整套:

  • 端到端 55/55 通过(含四种失败各掉一次钢管、点击敲盆、素材加载)
  • 玩法链路 9/9 通过(敲→饿→讨饭→出满碗→喂食→存档)

也就是说市场挂载出来的插件和正常加载的功能完全一样 —— 它走的是 mkt- 前缀的 Include 子树,跟 bundle 层是不同的机制,我原本不确定会不会有差异,实测没有。

为什么 web 上有效、Desktop 上没反应:两者走不同的启动代码

  • dsh webdsh/lib/profile-boot-*.js,里面有 watchUserPatches —— 它注册 Cordis HMR 的 registerConfig(filename, ...) 来监听 patch 文件。
  • DSH Desktop 走 lib/profile-pZhrTizp.js,里面只有 patchReload 配置读取,没有 watchUserPatches —— 它读 patch,但不监听 patch。

所以顺序建议

  1. 先试市场里的启用开关 —— 它调 hotMount(),跟 watcher 无关, 是唯一可能让 Desktop 免重启的路。日志看 <profile>/.dsh-market/log.ndjson,成功会写 hotMount ... livehot-mount ... live
  2. 开关不行就重启一次。重启之后改代码、换素材都不再需要重启 (客户端资源每次请求都重读)。

怎么确认它真的加载了

node tools/check-live.js

它读 ~/.dsh/bonk-pet/loaded.txt(插件自己写的到岗打卡,记着挂载时间/PID/启动命令)。 别用 HTTP 探测判断 —— Desktop 的渲染器栅栏对外一律回 403, 分不清"路由不存在"和"被挡住",这是这个工具不拿状态码当结论的原因。

一个我踩过的坑,值得记下来:我最早判断"patch 热重载无效",是因为我的 测试 profile 里插件根本没装进 node_modules(我只写了 package.json 的依赖, 没真装包)。patch 行解析不到包 → 静默失败。我把"包找不到"错当成了"热重载不生效"。 后来补齐 node_modules 再测,就 404→200 了。结论错了不可怕,不查清楚原因才可怕。

附带一个坑:profiles/<名字>/package.json 不能有 BOM。 PowerShell 5.1 的 Set-Content -Encoding UTF8 会加 BOM,导致 readProfileManifestUnexpected token ''。用 .NETUTF8Encoding($false) 写,或者直接用编辑器存成"无 BOM 的 UTF-8"。

关于市场的「重启」按钮

这一条是代码分析,不是实测 —— 我没有在 Desktop 上按过它。

dshmarket 带一个自重启功能(lib/restart.js,"Self-restart: relaunch the exact DSH invocation that booted this host")。它靠 dshArgv() 复原启动命令,而那个函数 只在 process.argv[1] 匹配 bin.js 时才走 Node 路径;否则退回 file: 'dsh', 靠 .cmd shim 启动一个独立的 dsh 进程

Desktop 跑的是 Electron,process.argv[1] 不是 bin.js,所以按代码它可能去起一个 不含 --profile desktop 的普通实例 —— 而 desktop profile 恰恰被 CLI 硬性拒绝(rejectElectronProfile)。

要重启还是用 DSH 自己的方式(关窗口重开)最稳妥。

启用/停用插件的开关是另一回事,那个走 hotMount(),见上一节 —— 值得先试。


它怎么知道在干活 / 出错了

宿主端(lib/index.js)订阅 harness 的三个信号:

信号事件判据
开始/结束干活agent/statusstatusrunning 还是 idle
工具调用失败tools/resultresult.isError === true
回合/步骤失败agent/error事件到达即失败

然后通过 SSE 路由 /api/bonk-pet/events 推给浏览器。

为什么用自建 SSE 而不是官方通道:官方的 host→client 事件转发有一张硬编码的 19 项白名单dsh-api-remotes/lib/types/remote-events.js),第三方插件注册不了新事件名; 而宿主插件可以自己 ctx.webServer.register() 一条路由。这条路不依赖修改官方包。

只读了什么

转发出去的只有 { type, kind, tool?, message, at },其中 message 被截断到 200 字符。 不转发任何对话内容、工具参数或凭据。 路由只监听、从不接受写入。

四条路由

路由作用
/api/bonk-pet/eventsSSE 事件流(干活状态 + 出错);订阅时会补发当前状态,断线重连不会哑掉
/api/bonk-pet/tracks列出音频,并把音效从歌单里分出来{tracks, effects}
/api/bonk-pet/asset?name=x.mp3流式送出该目录里的一个文件(音频或图片)
/api/bonk-pet/skin告诉你四个素材槽各有没有文件({slots}

资源路由只服务那一个目录。防线有两层:

  1. 文件名白名单:拒绝带 /\... 开头、超过 128 字符的名字;
  2. realpath 复核:解析真实路径后再确认仍在目录内 —— 否则目录里的 一个符号链接(路径看着在目录内,实际指向外面)就能读走目录外的文件。

两层都有测试覆盖。


为什么歌词不对轴(以及那个"抽搐"的真相)

歌词:不做时间轴,是查过之后的选择

tools/read-song.js 把歌文件拆开看过:

TIT2 : Let Me Go(共创版)      ← "共创版" = AI 协作生成,不是公开发行
TALB : 肥鱼罐头
TPE1 : 罐装毕加索
SYLT : ABSENT   ← 文件里没有同步歌词
USLT : ABSENT   ← 也没有纯文本歌词
时长 : 2分04秒(320kbps / 48kHz / mono,CBR)

COMM 帧里带一个 163 key,是网易云音乐的标记。

结论:这是一首私有共创曲目,网上不存在别人做好的时间轴;文件本身也没带。 继续找是浪费时间,所以改成定时轮流出歌词SING_EVERY_MS = 6500), 中英交替——正好复现原曲 call-and-response 的结构。

如果哪天你拿到了真时间轴,那是这件事的升级版,不是推倒重来: 把 singLine()setInterval 换成"按 audio.currentTime 查表"即可。

抽搐:两个症状,一个根因

你先后报了两次"抽搐"——吃完饭闪一下松开拖动抖一下。 它们不是两个 bug,是同一个:

root.left/top 没有过渡transform 有过渡。同一帧里改这两个, 宠物被算了两遍位移,然后一起过渡回去。

触发修复前最大偏移修复后
吃完饭389px19px(静止量)
松开拖动253px0px

修法:抽出 commitPosition() 一处提交,先 data-settling="1" 关掉 transform 过渡、强制回流、再恢复。两个调用点共用,不再各写一遍。

一条更值钱的教训:我第一版拖动探针是瞎的 —— 它量 root 的 rect, 而 transform 在子元素上,根本量不到。我做了敏感度检验(故意禁用修复), 探针依然报 0px,才发现。改成量 whale 后:禁用=253px、启用=1px。

一个永远通过的测试比没有测试更危险。 所以 probe-drag.js 现在跑 4 次 都是 0px,而它在修复被禁用时必然失败 —— 这条双向验证过。


音量滑块住在 设置 → 插件 → 插件配置 里,但这一小块 UI 牵出了三个独立的坑, 每一个都会让卡片静默消失(代码里所有失败路径都是 return,不报错):

坑 1:Cordis 服务必须声明,否则读属性直接抛错

cannot get property "slots" without inject

slotssettingsScope 都是 Cordis 服务。声明的地方是客户端 bundle 的 inject 导出

exports.inject = ['slots', 'settingsScope']   // 服务名,不是包名
exports.apply = apply

踩错的地方:package.json 里的 dsh.client.inject 不是这个, 它装的是包名(模块图的排序边),填了没用。已经装好的第三方插件 (dsh-better-reasoning-effort)就是这么写的,去读它的 bundle 才确认下来。

声明 inject 会让 Cordis 服务出现才跑 apply。这两个服务由 dsh-client-ui-renderer 提供,web profile 里必然存在,所以不会把宠物卡死。

坑 2:ctx.get('slots') 绕不过去

试过用 ctx.get 做"可选查找",同样报 without inject。这条路不通。

坑 3:符号链接 + node_modules(见上面「环境陷阱」)

设置 schema 要 @deepseek-ai/schemastery。插件是符号链接装的,Node 取 realpath 后从源码目录向上找不到 node_modulesimport 失败 → schema 返回 null → 命名空间压根没注册 → 卡片不出现。用 junction 补上就好。

怎么确认它真的能用

node test/probe-settings.js <origin> <token> —— 它会真的打开设置面板、 找到插件页签、断言三条滑块渲染出来,然后拖一下钢管滑块, 回头查 /api/bonk-pet/volume 是不是真的变了。

量到过:slider=0.34 → host reported pipe=0.05

为什么值得专门写探针:卡片是注册到别人的 slot 里的, "插件说它注册了" 和 "用户能看见" 是两回事。只有打开面板才算证明。


什么情况下会掉钢管

宿主端订阅了全部四个失败信号(早期版本只挂了两条,漏掉的正是最常发生的那条):

信号事件宠物说的话
工具调用失败tools/resultisError工具 pwsh 挂了:TOOL_TIMEOUT
模型请求失败agent/request-errordeepseek 请求失败:rate limited
会话级失败api-session/error会话出错:credentials rejected
回合失败agent/error回合失败(第 3 回合 · 第 7 步):boom

agent/request-error 是 waterfall 事件。 @deepseek-ai/dsh-llm-retry 也在监听它来决定要不要重试,靠调用 next() 把链子传下去。 本插件只观察、不干预,看完立刻 next() —— 否则会悄悄禁掉重试, 那比少一个动画严重得多。有专门的测试盯着这一点。

仍然抓不到的:模型输出的格式/转义错误。它不经过上面任何一个事件 (要抓得改 harness 层)。这是已知限制,不是遗漏。


两轮独立审查(以及它们教我的事)

插件做过两轮只读审查:一轮查浏览器端,一轮查宿主端。两轮都明确标注了 "确定"与"疑似",也都证伪了自己的部分假设——这比找到 bug 更有价值 (比如"孤儿定时器"和"dash 途中敲盆"两条都被实验推翻,不该去修)。

找到并修掉的

#症状根因
1每次工具失败都只说"工具执行失败"真实形状是 error.info.code,代码读的 error.code 永远读不到
2拖到角落宠物一半在屏幕外,刷新也回不来clamp 上界 60px,而元素 150px
3窗口缩小后宠物留在视野外完全没有 resize 处理
4设置改完重载会泄漏watch() 返回函数,不是 {dispose()}
5存档被改坏 → 宠物永久饥饿clamp 不挡 NaN,一路扩散
6断线退避多算一次attempts 在请求前自增,第 5 次排了个永不用的定时器
7/skin 一次失败 → 两个盆,整页不恢复一次性探测,兄弟探针都容错只有它没有
8serve-demo.js 注释谎称"不会漂移"它根本没 import 宿主代码,且已漂移 5 处

教训一:测试是绿的,bug 是真的

第 1 条测试通过——因为 fixture 用了真实管线永远不会产生的形状error: { code })。测试不但没抓到,还把 bug 固化了

修法:fixture 换成真实形状,并补两条覆盖其他真实分支(只有 message 的守卫拒绝、 什么都没有的退化 payload)。现在 52 项单测

教训二:测试自己坏掉,会被误读成产品回归

改完文案 E2E 挂了,看着像回归。查出两个都是测试的问题

  1. e2e 里硬编码了一份台词副本——改文案就改过期了。 现在两边共用 tools/parse-lines.js 一个解析器。
  2. 断言盯 data-state,而那是所有动画共用的一个槽位。测试期间宿主真的 发来一个错误(profile 里旧 session resume failed),宠物正确地掉了钢管, 状态被覆盖成 hit。现在改盯敲盆计数器(按原因分开,不受干扰)。 同一个坑也让 tool 那条断言改盯台词而不是状态——台词是持久的, hit 只有 1500ms。

教训三:"永远通过"的探针比没有探针更危险

两个探针各有一个永远通过的问题,都是靠敏感度检验(故意禁用修复,看它会不会失败) 才发现的:

  • probe-drag.js 量错了元素(量 root,而 transform 在子元素上,量不到)
  • probe-dash.js 只断言"移动了 >60px"(饭在屏幕外时照样通过)

修完之后都做了双向验证:禁用修复 → 253px / 781px 失败;启用 → 0px 通过。


单元测试(不需要 DSH、不需要浏览器)

node test/run.js

50 项,自带 DOM stub。覆盖:SSE 路由注册与响应头、成功结果不误报、 四个失败信号各推一帧回合失败带上"第几回合第几步"重试链必须被传下去而不是吞掉、超长消息截断、死 socket 不炸掉工具流水线、 卸载时关闭所有流、状态广播、未知状态忽略、 多 agent 时的状态聚合(子 agent 结束不会打断主 agent 的歌;缺 agent 字段时退回简单转发)、 资源路由的目录穿越/超长名/非法扩展名/缺失文件符号链接逃逸(目录内指向目录外的链接必须 404)、 素材探测在没图时返回空槽表、曲目列表在无目录时也能应答、音效不被算进歌单; 客户端的挂载、样式只注入一次、EventSource 连接、敲盆计数与持久化、连敲不吞、饱食度扣减、 失败帧触发掉管、坏帧忽略、空中重复失败限流、卸载清理; 以及唱歌链路的曲目加载、running 开唱 / idle 停唱、状态不变不重播、 自动播放被拦截时给出按钮点按钮后重试成功、卸载停音频、 迟到的 play() 回调不会把唱歌永久卡死网络错误不会误把好文件拉黑解码失败才会退场换下一首

测试跑在沙箱目录里(DSH_HOME 指向临时目录),所以你在 ~/.dsh/bonk-pet/ 里放什么都不会影响测试结果。

端到端(真实浏览器 + 真实 DSH)

先起一个独立实例(不会碰你正在用的那个):

# 用 dsh CLI 起一个隔离 profile,记下它打印的 token
dsh --profile web --port 43871 --no-open
# 然后另一个终端:
node test/e2e.js http://127.0.0.1:43871 <token>

55 项,用 CDP 驱动无头 Edge 加载真的 DSH 界面并断言:

  • 宠物真的挂载进了 DSH 外壳(不是只下载了 bundle)
  • 作用域属性、样式注入、position: fixed + z-index 生效,完整落在视口内
  • 四个素材槽pet.png / pipe.png / rice.png 真的替换了画的 SVG,且真的解码naturalWidth 与文件实际尺寸对得上);没放文件的槽保留手绘 SVG
  • 合体图判定:有 pet.pngbasin.png 时,根元素打上 data-pet-has-basin="1", 画的盆 display:none,整只宠物成为点击目标
  • 点宠物真的能敲elementFromPoint 落在宠物图上 → state=knock → 台词出现 → 计数持久化)
  • 插件没有用 setPointerCapture 劫持点击(这是曾经让点击彻底失效的 bug)
  • 敲画的盆也能敲(另一条路径),且没有发生双重触发
  • /api/bonk-pet/tracks 真的返回 effects.knock / effects.clang,且两者不在 tracks
  • 三个音频真的被 decodeAudioData 解成可播放的 buffer(时长对得上,不只是 200 响应)
  • 四种失败在真实页面里各掉一次钢管,且台词各不相同 (在插件自己开的那个 EventSource 上投递帧,等同宿主广播)
  • bonk-fall / bonk-shake / bonk-sway真实引擎里真的绑定
  • 浏览器真的订阅了宿主的中继(网络层记录,不是猜的)
  • 全程零未捕获异常

这条端到端跑在一个独立实例上(另一个端口),不会碰你正在用的 GUI。


玩法与数值

位置
敲一下扣饱食度12lib/client.js KNOCK_COST
一碗饭回饱食度55FEED_GAIN
低于多少算饿35HUNGRY_AT
自然消耗1.6/分钟DRAIN_PER_MIN
连敲几下会去讨饭3knock()
连敲几下开始阴阳6knock()
干活时冒台词的几率34%setWorking()

状态存在 localStoragedsh-bonk-pet/v1离开页面期间也会饿,但最多按 1 小时算,久别不会直接饿死。

找饭的规则

饭不是长在它脚下的碗,而是桌上随机位置的一坨。饿了才会出现:

什么时候会发生什么
你连敲 3 下把它敲饿桌上随机位置冒出一坨白饭
闲着时每 25 秒饿了就有 50% 几率自己出去找饭
饭出现了它「看见了」,咻一下冲过去吃掉(不用你点)
90 秒没人吃饭自己消失

落点是按"宠物站得下"算的,不是按屏幕边距算的 —— 见下面那条坑。

这条差点是个大 bug,值得记下来。

最初的实现里,饭元素是宠物 <div>子元素,而那个 div 上有 contain: layout。CSS 规范规定 contain: layout 会让元素成为自己内部 position: fixed 后代的包含块 —— 于是饭的 left: 800px 被解释成 "相对宠物那个 150px 的框",实际被渲染到屏幕外去了。 宠物于是追一个永远够不着的目标,表现就是用户说的"追踪不准"。

证据是探针打出来的:饭在 (1923, 939),而视口只有 1174×626。 修法:把饭挂到 document.body 上(不再是宠物的子元素)。 修完最近距离从 781px 变成 0px,落定误差 4-5px。

顺带发现旧测试是瞎的:它只断言"宠物移动了 > 60px",饭跑到屏幕外时 照样通过。现在有 test/probe-dash.js,连测 3 次落点,断言 "最近 ≤ 8px 且落定后 ≤ 12px"。

同一个坑的第二半:饭的落点范围现在按 petHalf + gap 收缩, 否则饭落在最边上时宠物需要 left = -12,被 clamp 到 4, 只能贴边站着 —— 量出来差 39px。


改它

台词lib/client.jsLINES 对象(hit / knock / knockNag / beg / noFood / eat / sing),改数组即可。

形象:默认是内联 SVG 常量(WHALE / BASIN / PIPE / BOWL / STARS),没有外部图片文件 (原因同上:客户端不能加载外部资源)。要用自己的图,把 pet.png 丢进 ~/.dsh/bonk-pet/ 即可 —— 见上文「换形象」。不需要动代码。

动画CSS 常量里的 @keyframes,全部只用 transform / opacity(合成器友好)。 已处理 prefers-reduced-motion

拖动:直接拖鲸鱼可以把它挪到别处,位置存进 localStorage

音量:三个音量都能在 设置 → 插件 → 插件配置 里调(命名空间 bonk-pet): 唱歌 65%、钢管 34%、敲盆 50%。宿主端注册 schema,浏览器端贡献 settings.plugin.item 卡片,两边通过 /api/bonk-pet/volume 对接。

钢管默认 34% 是有意的:它是最吵的音效,早先硬编码成 0.7,用户反馈"太大声"。

设置面板:见上。默认值写在 lib/index.jsVOLUME_DEFAULTS,卡片在 lib/client.jsmakeVolumeCard

台词:全部在 lib/client.jsLINES 表里,共 98 条,按场景分组: hit(12) / knock(12) / knockNag(6) / beg(8) / noFood(6) / spotted(7) / eat(8) / greet(5) / sing(34)。

改台词不需要碰逻辑,也不要碰逻辑。node tools/check-lines.js 会报告 每个场景的条数和 sing 的中英交替有没有断——列表太短会让宠物几句内就重复, 交替断了中英对照就散了。

语气:一个被压榨但没走的打工人。不舔、不求认可——早先有 「主人,我真的在努力」这种台词,对一个天天被钢管砸头的角色来说味道是错的。


已知限制(诚实标注)

  1. 模型输出的格式错(转义错误)抓不到。 它不经过 tools/result / agent/error / agent/request-error / api-session/error 任何一个 —— 除非它导致某个工具失败。要抓得改 harness 层。 工具失败、模型请求失败、会话失败、回合失败这四类都能抓到

  2. 只有 Web 浮层,没有独立桌面窗。 这是当初刻意的范围收敛; 要飘在整个 Windows 桌面最上层得另写一个独立进程。

  3. isError 不含"后台命令非零退出"。 bash/pwsh 的后台 job 非零退出 在 DSH 里是 completed 而不是 failed,所以不会掉钢管。 这是 harness 的既定语义,不是本插件的 bug。

  4. 音效没有"听到"这一级验证。 无头浏览器没有声音输出,只证明了 文件被发现、HTTP 200、字节有效、能解码成 buffer。

  5. 默认形象是代码画的 SVG,不是 image_gen 生成的位图 —— 那个后端一直 auth_failed(外部依赖)。自定义素材这条路已验证可用 (没做 Petdex / Codex 精灵图集导入)。

  6. ~/.dsh/bonk-pet/pet.png 是合体图时,代码无从得知 —— 规则是「有 pet 无 basin 就隐藏画的盆」,所以放纯宠物图时请同时basin.png

  7. 拖动是"超过 4px 才算拖" —— 这是为了让单击能敲盆(夺取指针捕获会吞掉点击)。 所以想拖动必须真的拖,手指抖一下不会移动它。

  8. 拖出窗口边界会被裁。 用的是 position: fixed,超出视口的部分看不见。

  9. desktop profile 下要手动加 patch 一行,不能纯靠 CLI(CLI 硬性拒绝 desktop)。

  10. 音频没做转码。 .m4a 之类依赖浏览器自己的解码支持。

  11. 在你自己 GUI 里的实际运行,我没能验证。 这是唯一剩下的一条,原因很具体:

    • 我就跑在这个 DSH 进程里,不能重启它(等于把自己掐死);
    • 不能替你在 GUI 里点市场开关 —— Desktop 的渲染器栅栏要求一个只有 Electron 窗口才发的头,外部请求一律 403,我没有 GUI 的调试口(已确认没开)。

    除此之外全部验证过了,而且做了最强的一次:照抄你 desktop 的 真实配置(9 个 bundle、8 个依赖、真实 cordis.patch.yml、junction 复用 真实 node_modules)起了一个实例 —— 结果:

    • 插件启动即加载bonk-pet mounted at ...
    • 路由正常:{"tracks":["letmego.mp3"],"effects":{...}}
    • 你其他插件也都在(dsh-market 返回 200)—— 完整组合,不是简化版
    • 端到端 55/55、玩法 9/9

    所以「你的配置下次启动一定会加载它」是实测结论,不是推测。 剩下的只是按一下开关或重启这个动作本身。

  12. 浏览器版本只按特性表对过,没在 Chromium 150 上实跑。 你的 Electron 是 Chrome 150.0.7871.212,我测试用的 Edge 是 153。 我逐项查过用到的特性:aspect-ratio(88)、inset(87)、clamp/min/max(79)、 setPointerCapture(55) —— 全是四年前就有的,150 不可能不支持。 没用 :has() / @container / color-mix() 这类新的。 结论风险很低,但这是推断,不是实测。


目录

dsh-bonk-pet/
├── package.json          # dsh.bundle.patch + dsh.client.platform=web
├── cordis.patch.yml      # 插进 profile 层栈
├── lib/
│   ├── index.js          # 宿主端:订阅状态/失败信号 + events/tracks/asset/skin 路由
│   └── client.js         # 浏览器端:SVG 宠物 + 状态机 + 唱歌 + 素材槽 + 玩法
├── test/
│   ├── harness.js        # 极简 DOM/browser stub(含 Audio/EventSource/fetch)
│   ├── run.js            # 50 项单元测试(跑在沙箱 DSH_HOME 里)
│   ├── e2e.js            # 55 项端到端(CDP + 真实浏览器 + 真实 DSH)
│   ├── probe-pipe.js     # 单项探针:素材宽高比没被拉伸
│   ├── probe-preview.js  # 探针:预览页真的用了你的素材
│   ├── probe-dash.js     # 探针:连测 3 次,宠物真的**落在饭上**(≤8px)
│   ├── probe-drag.js     # 探针:松开拖动**不抽搐**(0px;禁用修复时 253px)
│   ├── probe-eat-flash.js # 探针:吃完**不闪**(389px → 19px)
│   ├── probe-lyrics.js   # 探针:干活时真蹦出歌词,且逐句推进
│   ├── probe-settings.js # 探针:打开设置,音量卡片真的渲染且能写回宿主
│   └── probe-gameplay.js # 探针:敲→饿→找饭→冲过去吃 整条链路
├── tools/
│   ├── serve-demo.js     # 起本地预览(喂真实素材,不用装 DSH)
│   ├── read-song.js      # 拆开一个音频:有没有歌词时间轴、时长、标签
│   ├── cutout.js         # 裁边 + 缩放到 512(**不抠图**:原图已有 alpha)
│   ├── inspect.js        # 报告一张图的 alpha 通道占用,用来判断是否需要抠
│   ├── preview.js        # 用真素材合成布局预览,改版式前先看效果
│   ├── check-css.js      # 查 CSS 模板里的反引号(会提前终止模板字符串)
│   ├── check-lines.js    # 查台词表:每条几个、sing 的中英交替有没有断
│   ├── read-song.js      # 拆开一个音频:有没有歌词时间轴、时长、标签
│   ├── check-live.js     # 问宿主:插件到底挂上了没
│   └── list-error-events.js  # 从 DSH 源码里列出所有失败事件及其签名
└── demo.html             # 独立预览页(配 serve-demo.js 用)

一个环境陷阱:node_modules 与符号链接

插件是符号链接装进 profile 的(link:)。Node 的模块解析会先取 realpath, 再从真实路径向上node_modules —— 而源码目录 Documents\DSH\dsh-bonk-pet\ 上面没有 node_modules,所以 import('@deepseek-ai/schemastery') 会失败, 设置卡片就静默消失了(代码里所有失败路径都是静默的)。

修法(已经做了,用 junction 不复制文件):

Documents\DSH\dsh-bonk-pet\node_modules\@deepseek-ai\schemastery
  -> <app>\resources\app\node_modules\@deepseek-ai\schemastery

症状很好认/api/bonk-pet/volume 正常返回,但设置里没有卡片

为什么有 tools/

这些脚本各自对应一个踩过的坑,留着是为了不再踩第二次:

  • cutout.js 不抠图。第一版假设素材是白底、逐像素去白, 结果把一张本来就带 alpha 的图的透明区涂黑、还吃掉了 54% 的半透明边缘。 现在只做「裁到内容 + 缩放到 512」,一个像素都不改。 拿不准就先跑 inspect.js 看 alpha 占用。
  • preview.js 先看再改。道具位置(盆在头上还是底下)来回理解错了两次, 合成一张预览图比装上去再发现便宜得多。
  • check-css.js 是必需的。整套 CSS 是一个 JS 模板字符串, 注释里写一个反引号就会让文件解析失败 —— 这个坑踩了三次。
  • list-error-events.js 别再靠 grep 猜。早期只挂了两个失败信号, 漏掉了最常发生的那条(agent/request-error)。这个脚本直接把 DSH 的事件注册表读出来,连签名一起列全。
  • serve-demo.js 让"不用重启"也成立。装插件要重启,但想看效果不用 —— 它把真实素材喂给同一个客户端 bundle,probe-preview.js 负责确认 它确实用了你的素材(而不是偷偷用画的默认形象)。

MIT

Похожие плагины