DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

wei125775-lab /

wei125775-lab/whalegirl-deskpet

Verified

会跟着 Claude Code 干活的鲸鱼娘桌宠(PetPet / dsh 双版本,Windows x64)。干活时她端碗吃饭,收工收碗,按 Esc 打断会甩你一个屑表情。A desktop pet that reacts to your Claude Code session.

★ 6 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@0f2d7dd2

鲸鱼娘桌宠

鲸鱼娘桌宠

一只会跟着你干活的鲸鱼娘桌宠,同一个角色的两个平台版本:Claude Code(PetPet 桌宠) 和 dsh(DshDesktop)。

她不是那种只会原地晃两下的桌宠 —— 你发消息她捧着碗一口一口吃,这一轮干完她收碗,偶尔双手合十给你祝福,你按 Esc 打断她,她会停下手里的事甩你一个屑表情,点她会挥手/比心。

English: a desktop pet that reacts to your AI coding session. Same character, two builds: Claude Code (PetPet) and dsh. She eats from a bowl while you work, puts it away when the turn ends, and gives you a smug look if you hit Esc to interrupt. On Windows you can grab the green build, unzip it and double-click 启动.cmd.

她在 Claude Code 上干活的样子

实拍:左边是 Claude Code 正在跑任务,右边就是她——干活期间一直端着碗吃,气泡是她自己弹的。

她会做什么

上图是她的十个动作,全部实拍自宠物素材。

她动起来的样子

挥手 → 起子代理时脚边游出一只小海豚,绕着她转一圈再沉下去。上面这段是 6 秒循环(436×640 原尺寸,15 秒完整版还含比心、吃饭、收碗和屑表情)。

她什么时候做什么

你做什么 Claude Code 版 dsh 版
你发消息 / 模型开始干活 捧着碗吃(循环);10% 的轮次换成端着中碗、拿筷子正经吃一顿 捧着碗吃(循环,全程一条轨道,见坑五)
这一轮结束 收碗 → 55% 托腮发呆 / 10% 双手合十祝福 / 35% 待机 native 同左(靠 transitions.json);legacy 直接回待机(粘性 phase 的限制,见坑一)
按 Esc 打断她 立刻停下手里的动作,甩你一个屑表情 工具失败/本轮出错时同样甩屑表情
点她 挥手(8% 换成屑表情) 57% 挥手 / 20% 托腮 / 15% 比心 / 8% 屑表情
起子代理 收碗 → 脚边游出一只小海豚转圈 → 跑完沉下去 同左(靠打补丁,见「看鲸鱼」一节)
什么都不做 老实待机——长发和尾巴轻轻晃,偶尔眨眼 同左

待机时她就长这样

上面这段就是宠物里真实的待机循环(24 帧 @12fps):呼吸、飘发、眨眼都是程序化烘焙的,不是抽帧——AI 画不出这种幅度的小动作。其余动作由 AI 视频转序列帧、再逐帧抠像配准,工具链在 whalegirl-pet-kit。

装

最省事:下 绿色版 —— Windows x64,解压后双击 启动.cmd 就有她。不用装 Node、不用 clone 源码、不用编译。代价是包大(约 270MB,整个 Electron 运行时都在里面)。

⚠️ 双击时可能弹「Windows 已保护你的电脑」——点「更多信息」→「仍要运行」即可,不是文件坏了。浏览器下载的 zip 会被打上「来自 Internet」标记,解压时传给里面的文件,Windows 对没签名的程序都会拦这一次。交给 AI 装的话它可以帮你把这行去掉(见下面的「给 AI 的一句话」)。

绿色版就是下面「Claude Code 版」预先构建好的成品;想自己构建,或者要用 dsh 版,走这两条:

版本 给谁用 怎么装
Claude Code 版 PetPet 桌宠,跟 Claude Code 联动的那只 node claude/install.mjs → 按它打印的收尾。详见 claude/README.md
dsh 版 dsh(桌面外壳 DshDesktop)里那只 双击根目录的 install.cmd(或 node install.mjs)→ 重启 dsh。渲染器没装的话脚本会自己检测并装上
WorkBuddy 版 WorkBuddy 里那只 下 release 里的 whalegirl-workbuddy-win-x64.zip(自带插件,不用再装别的)→ 先双击 启动.cmd,再双击 装WorkBuddy钩子.cmd。详见 workbuddy/README.md

dsh 版也有打好的 zip:dsh-v1.9.2(73MB)——解压后跟 clone 下来一样跑 install.cmd,不用 clone、不用装依赖。注意 releases/latest 指的是绿色版那条线(里面有两个包:Claude Code 版和 WorkBuddy 版,按名字挑),dsh 版得从 tag 进。

说明:dsh 版的渲染器是另一个插件(@linxin666/dsh-pet),本包只管把素材放好、把她画出来的是它。安装脚本会先看它有没有、没有就调 dsh plugin add 装上(顺带把 bundle 登记好、并给它的相位白名单打上看鲸鱼要用的补丁)——不想让它动你的插件环境就加 --no-renderer。

自动装失败也不会坏:素材已经就位,脚本会在收尾给出可直接复制的命令,装完重启即可、不用重跑本脚本。装渲染器之前界面上什么都不会出现,这是分工决定的,不是坏了。

两个安装脚本都是幂等的:会自己找/下依赖,改配置前先备份,重复跑不会出问题;都能加 --dry-run 先看它会改什么。

给 AI 的一句话(把仓库链接和下面这段一起发给你用的 AI):

「帮我装鲸鱼娘桌宠(<仓库链接>)。

要 Claude Code 那只:走绿色版 —— 从 releases/latest 下 whalegirl-petpet-win-x64.zip,解压到一个固定目录(别在压缩包里直接点)。解压完先跑这行,去掉「来自 Internet」标记,不然双击会弹 Windows 拦截框: powershell -NoProfile -Command "Get-ChildItem '<解压目录>' -Recurse -File | Unblock-File" 然后跑 <解压目录>\claude\install.mjs --hooks-only --pet-exe "<解压目录>\PetPet.exe"(先加 --dry-run 看一眼)——挂上钩子她才会跟着我干活。脚本在解压出来的包里带着,不用联网。

要 dsh 那只:跑根目录的 install.mjs。

都一样:先 --dry-run 看一眼,按脚本最后打印的收尾,做完告诉我一声重启。」

仓库里都有什么

pet/            dsh 版素材 + manifest(frames2d 格式)
lib/ · install.mjs · install.cmd      dsh 版插件:把 pet/ 释放给 dsh-pet
claude/         Claude Code 版:宠物包(petpack)+ 一键安装 + viewer 补丁 + 钩子脚本
workbuddy/      WorkBuddy 版:hook 插件(宠物本体仍是绿色版那只,这个只负责让她跟着你干活)
docs/preview/   README 用的预览图

两个版本用的是同一批动作素材,只是打包格式不同:Claude 版是精灵表 + pet.json v3(PetPet 框架),dsh 版是逐帧 PNG + manifest v2(@linxin666/dsh-pet 插件)。

⚠️ 素材全部由 AI 生成 —— 立绘经 see-through 分层,动作由豆包图生视频抽帧。拿去做别的事情之前,先确认所用平台的服务条款:不同平台对生成内容的商用和再分发规定不一样。代码部分是 MIT(见 LICENSE)。


WorkBuddy 版

WorkBuddy(腾讯那个桌面 AI 工作台)里也能用同一只桌宠。做法和 dsh 完全不同:

她不画在 WorkBuddy 窗口里 —— 她就是你桌面上的那只;WorkBuddy 这边只有一个 hook 插件,负责告诉她"我在干活"。

最省事的是下 release 里的 whalegirl-workbuddy-win-x64.zip —— 宠物本体和插件都在里面:

  1. 解压,双击 启动.cmd —— 桌面上就有她了
  2. 双击 装WorkBuddy钩子.cmd
  3. 完全退出 WorkBuddy 再打开

已经有绿色版、不想重下的话,也能单独装插件:node workbuddy/install.mjs。

装完的效果:你发消息她端碗吃饭、一轮回答结束收碗、派子代理时脚边游出一只小海豚。

WorkBuddy 的 hook 机制和 Claude Code 是同一套 —— 事件名(UserPromptSubmit / Stop / SessionStart / SubagentStart …)、payload 字段、配置结构全都对得上,所以这一版复用 claude/ 下那三个脚本,没有第二份实现。唯一要留意的是两边共用 ~/.petpet/state.json:同时开着、一个在干活一个待机时她会闪一下,只用其中一边就没事。

装、卸、排查都写在 workbuddy/README.md 里。


dsh 版细节

装完在宠物选择里叫鲸鱼娘(id whalegirl-hd)。

装 / 卸

双击 install.cmd(或 node install.mjs),它做三件事:

  1. 把包复制到 <DSH_HOME>/profiles/<profile>/node_modules/@wei125775-lab/whalegirl-deskpet/
  2. 在 profile 的 package.json 里加 dependencies 和 dsh.profile.bundles 条目
  3. 备份改之前的 manifest 到 package.json.bak-whalegirl

然后重启 dsh(自建的 DshDesktop 或官方桌面版)。插件本身不注册任何东西,只在启动时把包内的 pet/ 释放到 <DSH_HOME>/pets/whalegirl-hd/ —— dsh-pet 的 frames2d 宠物只能从那个目录扫出来(它内联 manifest 的通道只认 v1 sprite2d 精灵表,喂 frames2d 进去会静默降级,而 sprite2d 只有固定 9 个 Codex 动画名,装不下这些自定义动作)。

释放是幂等的:目标目录的内容和包内一致就跳过。比的是内容不是版本号——改了 pet/ 里的任何东西(素材、pet.json、voice.json)都不用手动删目录,下次启动发现对不上就会重建;部署目录被拷坏(缺帧、拷到一半)也是同样处理,会自己修好。想强制覆盖就删掉 <DSH_HOME>/pets/whalegirl-hd/ 再重启。宠物没出现就再重启一次——插件释放和 dsh-pet 扫目录都发生在启动期间,安装脚本已经把 bundle 排在 @linxin666/dsh-pet 前面,但万一顺序还是反了,第二次必然正确。

卸:删掉上面那三处(node_modules 里的包、profile package.json 的条目、<DSH_HOME>/pets/whalegirl-hd/)。

给对方装

整个文件夹拷过去(zip / 网盘 / clone 都行),对方双击 install.cmd 或跑 node install.mjs,重启 dsh。对方那边需要:

  • dsh ≥ 0.1.5-rc.1(插件走 dsh.bundle.patch 机制)
  • @linxin666/dsh-pet —— 可选,两条路只走一条。装了就走 legacy(挂在它上面渲染,有气泡和台词包);没装就走自研引擎(lib/engine/,素材直接从包目录读、相位自己投影),她照样会显示。入口按实际探测结果定,不猜
  • node(dsh 本身就依赖它)

安装脚本自己会找地方:--dsh-home=<目录> > DSH_HOME 环境变量 > ~/.dsh。profile 优先挑装了 dsh-pet 的那个,找不到退回 web;有多个装了 dsh-pet 的 profile 时它不会猜,会报出来让你用 --profile=<名字> 指定(挑错的代价是改了另一个 profile 的 bundles)。

渲染器版本必须跟着目标 profile 的 dsh 走,所以 --renderer-spec= 是刚需:

目标 dsh 渲染器 spec
自建 DshDesktop / dsh web 0.1.5-rc.x @linxin666/dsh-pet@0.3.23
官方桌面版 0.1.7-rc.x @linxin666/dsh-pet@0.4.2

dsh-pet 0.4.2 的 peerDeps 是 dsh >=0.1.7-rc.1,装上 0.1.5 的 profile 会直接坏掉。所以脚本不猜:web profile 有钉好的默认值(0.3.23),其它 profile 查不到默认值时它会停下来,让你显式传 --renderer-spec=(或加 --no-renderer 走自研引擎)—— 宁可停下,也不装一个可能把环境装坏的版本。

官方桌面版(Electron 那版)

桌面版有自己的数据目录(DSH_HOME)和自己的 profile,叫 desktop。它走自研引擎:这个 profile 里不装 @linxin666/dsh-pet,lib/engine/ 直接从包目录读素材、自己投影相位、自己画。所以安装只要一句:

node install.mjs --profile=desktop --dsh-home=D:\dsh-desktop-home --no-renderer

--no-renderer 就是这个意思 —— 这个 profile 不需要 dsh-pet,脚本也不会再去提示你装它(装了两只同时画在屏幕上)。

启动:

set "DSH_HOME=D:\dsh-desktop-home"
start "" "D:\DeepSeekHarness\DeepSeek Harness.exe"

注意安装器给桌面版建的快捷方式指向 exe 本体、不带 DSH_HOME,双击它会用默认的 ~/.dsh —— 指向启动脚本才走隔离目录。

两条路径:入口自己判定,不用配

legacy native
什么时候走 profile 里有 dsh-pet profile 里没有 dsh-pet
素材 「释放」到 $DSH_HOME/pets/ 直接从包目录读
相位 改 dsh-pet 的白名单 自己定义,不碰第三方
看鲸鱼 包一层它的 applyActivity 自己的管线,天然不被打断
需要 dsh-pet 是 否

启动日志会打一行 引擎:legacy 或 引擎:native,不静默切换。想强制可以给插件行加 config: { engine: 'native' },但装了 dsh-pet 又强走 native 会被拒绝并回退(两个引擎会同时画两只宠物)。

排查「宠物没出现」

引擎自带一个诊断接口,先看它再猜:

curl http://127.0.0.1:19387/api/whalegirl/diagnostics
# {"hits":{"beacon":1,"pet":1,"state":182,"frame":24,"touch":0,"config":0},...}
  • beacon: 0 → 浏览器半端压根没进页面(dsh.client 没被扫到,或 exports['./client'] 解析不出来)
  • beacon 有、pet: 0 → 进来了但挂载失败
  • 三个都在涨、frame 也涨 → 引擎是好的,问题在显示层(被别的窗口盖住、或位置在视口外)

这个接口是上一次排查"她怎么不出现"留下的:当时从外面看只有"右下角空的"一种症状,而它背后可能是完全不同的三种毛病。

干完活收碗(putaway)与动作链(transitions.json)

一轮跑完(phase 变 done)她会把手里的碗收掉再回待机。这条只在 native 引擎(官方桌面版)上有,声明写在 pet/transitions.json 的保留键 $phases 里,engine 载入时并进相位表:

{
  "$phases": { "done": "putaway" },
  "putaway": { "touchface": 55, "bless": 10, "idle": 35 },
  "bless": { "idle": 100 }
}

$phases 是"相位 → 轨名"的补丁,用来放在 pet.json 里写不得的映射。为什么 done → putaway 写不得:done 是粘性相位,dsh-pet 客户端在点击动作释放时会按当前相位重新解析一次,映射到一次性动作就变成每点她一次重播一遍收碗(坑一那条规则)。pet.json 是两条路共用的,所以它保持 done: "idle"。

收碗之后接什么,是 transitions.json 的另外那些键,按权重掷。键是"播完之后要挑下一段"的那条轨,值是 {目标轨: 权重}(权重,不是概率,不要求和为 1)。没写这条轨、或掷到的目标不存在,就走它自己的 fallback(缺省待机)。链上走过的轨不会重复走(A→B→A 走到重复就落 fallback),所以不会打转。

为什么这些都要放同级文件:dsh-pet 对 track 的字段是白名单 + fail-closed(只认 frames / frameMs / loop / fallback),多一个字段就是 diag.error、整只宠物被丢掉。而 pet.json 还要给 legacy(dsh-pet)那条路用,所以扩展只能放它不认的文件里 —— 跟 voice.json 同一个套路。键名带 $ 前缀的是保留指令(轨道名走 /^[a-z0-9][a-z0-9-]*$/,撞不上)。

两条实现上的注意(改这块之前先看):

  1. 同一次里,同一条一次性轨只认一次。 收碗 875ms 播完,客户端按 fallback 自己落回待机,而服务端在这个相位里还会继续说"该播 putaway" —— 400ms 一次的轮询会把收碗一遍遍重播。 判据是 /state 里的 seq(状态机每收一个新相位就 +1),不是相位名:done/failed 允许重复提交(重新计时),第二轮结束时相位名还是 done,只看名字会把第二次收碗静默吞掉 —— 触发条件是客户端错过了中间那些相位,而窗口最小化时它本来就不轮询。
  2. 动作链只在相位驱动时掷,点击动作不掷:点击有 override 和释放定时器,插进来两边会打架。

她的台词(pet/voice.json)

✅ 两条路都接了台词(2026-09-27)。native 引擎(官方桌面版)在 lib/engine/voice.js 里复刻了同一套 voice-pack 语义 —— 11 个状态场景、17 个工具类别、并行工具计数、碎碎念,气泡视觉照 dsh-pet 的规格做,行为也一致(干活期间常驻、干完/失败满 2.4 秒才收)。所以下面这套文案两条路共用同一个 voice.json,改一处两边都变。

dsh-pet 把会话事件投影成 phase,同时也会投影出一句台词冒在气泡里。台词默认来自官方内置文案("准备开始""正在思考""爬取中"…)—— 那不是鲸鱼娘在说话。所以她在 dsh 里的话由 pet/voice.json 提供。

voice 不能写进 pet.json —— manifest v2 的顶层白名单里没有这个字段。它是一个跟 pet.json 平级的独立文件,放在宠物目录里就会被读走(scanPetDir 里 loadVoicePackFile(join(entryDir, "voice.json")))。宠物被释放到 ~/.dsh/pets/<id>/ 时会一起带过去。native 引擎不经过释放目录,直接从包内的 pet/ 读(loadVoice(definition.petDir))—— 改完台词文件记得同步 profile 里那份副本。

能配四块:

字段 覆盖什么 允许的键
status 各 phase 的状态台词 prepare waiting thinking review toolResult done failed toolFailed maxTokens interrupted blocked
tools 按工具类别出词 read write edit shell grep find ls webSearch webFetch mcp memory subagent todo browser git ask generic
toolRemaining 还有几个工具在跑 可用占位符 {n}
whispers 干活期间的碎碎念(分类 9 秒、结果 5 秒节流) categories:thinking writing reading editing running searching git delegating browsing generic;results:pass fail done

另外 panel(面板标签)、remarks、ranks 也能覆盖,本宠物没用。

占位符按块白名单校验:tools 只认 {tool} 和 {hint},toolRemaining 只认 {n},status 一个都不认(写了会被丢掉并记一条 warning)。单行 ≤160 字,单池 ≤64 条。写错不会崩 —— 整包解析失败只是被忽略,降级回官方文案,所以 voice.json 是低风险改动。

想确认改动生效、又不想起 dsh:loadPetRegistry 的 warnings 应当为空,且 reg.byId('<id>').voice.overrides 里能看到你的池子。链路是 voicePools() = mergeVoicePacks(globalVoice, entry.voice).overrides → emptyProjectionRuntime(pools) → new StatusVoice(pools) / new WhisperEngine(pools)。

看鲸鱼(子代理来了,脚边游出一只小海豚)

起子代理的时候,她会收碗、脚边游出一只小海豚转圈,等子代理跑完,海豚沉下去,她回去吃饭。

这一块是三层非官方机制搭出来的,dsh-pet 一升级就可能失效,所以单独写清楚。

它靠什么跑起来

  1. 给 dsh-pet 的相位白名单打补丁。 frames2d.phases 是 fail-closed 校验的 —— 写进去不认识的名字(unknown activity phase)会让整只宠物被静默丢弃,界面上直接没有她。所以要往 dsh-pet 的 PET_ACTIVITY_PHASES 里补 whale-in / whale-loop / whale-out,补丁落在两个文件:lib/index.js(服务端那份,实际校验走它)和 lib/types/manifest-v2.js。 脚本是 patch-dshpet.mjs(install.mjs 会自动调它),幂等,--dry-run 可预览。
  2. 相位是"偷渡"过去的。 pet 服务上的 applyActivity() 内部直接 machine.onActivityStatus(input),不校验相位;相位原样流到客户端,而客户端的 trackForPhase 只是查 config.phases[phase] 这张表。所以只要表里挂得上键就能播。
  3. 演出期间吞掉子代理的事件。 dsh-pet 对子代理会话没有任何过滤 —— 子代理自己的 tool/call 之类会 applyActivity 到自己身上并抢走 displaySession,不处理的话鲸鱼每被子代理的一次工具调用打断就从头重播一遍。所以插件把 applyActivity 包了一层,演出期间只吞来源是子代理会话的调用,主会话照常放行。

dsh-pet 升级之后怎么办

补丁会被冲掉。宠物不会消失 —— 插件里的 syncWhalePhases() 会在 dsh-pet 读 manifest 之前,先探一下它认不认那几个相位(读主文件里有没有 "whale-in" 字面量):不认就把相位键从部署的 manifest 里摘掉,降级成"没有看鲸鱼"的版本,并在日志里喊一声。

它是双向的 —— 这一点是有意设计的:releasePet() 只在包内版本比已装的新时才覆盖,所以"没装 dsh-pet 时先装了本包(相位被摘)→ 之后才装 dsh-pet 并打上补丁"这条路上,被摘掉的相位没人会加回来。syncWhalePhases() 检测到补丁在了就把相位补回去,所以先装哪个都行,不用重装。

恢复:node patch-dshpet.mjs,然后重启 DshDesktop。

不要了怎么关

把 pet/pet.json 里 frames2d.phases 的 whale-in / whale-loop / whale-out 三个键删掉就行,补丁留着无害(白名单只是放行,多几个名字不影响别的宠物)。插件会因为你没声明那几个相位而不启动演出。

phase 映射

dsh-pet 把会话事件投影成 7 个 phase,宠物按 phase 播动作:

phase 什么时候 播什么
idle 没有会话活动 / 一轮被中止 待机(循环)
waiting 你发出消息、每步开始 吃饭
thinking 模型在推理 吃饭
tool 工具调用中 吃饭
review 模型在输出回复 吃饭
done 一轮完成 待机
failed 工具失败 / 本轮出错 屑表情(播一遍回待机)

改这些就是改 pet/pet.json 里的 frames2d.phases 和 gameplay.touch.zones,两个都是 dsh-pet 的 manifest v2 字段,改了要重启 dsh(宠物是启动时扫的)。

四个踩过的坑,改 manifest 前先看

一、done 和 failed 是粘性 phase,映射到它们的 track 必须是循环动作或者 idle。

phase 只在收到新事件时才变 —— dsh-pet 里只有 sprite2d 走的 animation 字段带 2.4 秒回落窗口,frames2d 直接吃 phases[phase],没有回落。而 track 默认 loop: true。所以映射循环动作会一直播到你下次发消息(一开始配的 done → 比心 就是这样,比心循环个没完);映射一次性动作则每次点击后都会补播一次(点击是"占住 override + 定时释放",释放时重新按当前 phase 解析,那会儿 phase 多半还粘在 done 上)。

所以 done 只能映射 idle。想在 dsh 里表达"干完活"这类一次性动作,只能挂到 gameplay.touch 上。

native 引擎(官方桌面版)现在能让 done 播收碗,靠的是 pet/transitions.json 里那个保留键 $phases(native 载入时并进相位表)—— 不是把 pet.json 的 done 改掉。因为 pet.json 是两条路共用的,一旦在那里写成 done: putaway,legacy 那边每次点她都会重播一遍收碗(这条规则上面刚写过)。同一个文件要伺候两个宿主时,能写进共用文件的只有两边都成立的映射。

failed → 屑表情 是这条规则的有意例外:屑表情是一次性 track(loop: false + fallback: idle),那 30 帧是"抬手/眨眼 → 保持得意表情 → 收回"的完整表情动作,循环播会变成反复眨眼。代价是出错后那个窗口里点她一下会补播一次屑表情——补的正是当前 phase 该有的动作,看着还算连贯,就接受了。(之前这里挂的是托腮,换成屑表情是因为表情类素材在宠物尺寸下读得出来,比肢体动作更贴"被打断了"的语境;托腮现在只做点击反应。)

二、一次性动作的 stateMs 必须等于素材总时长(帧数 × 帧长)。

点击分支的 stateMs 是"占住 override 多久",不写时默认 3000ms(gameplay-hud.tsx 里 result.stateMs ?? 3000),不是素材时长。它和素材对不上就会切在动作中段:比素材长 → 循环动作多播小半遍再被切走(托腮原来配 3200ms、素材只有 2490ms,于是"做完之后手又抬了一次",就是被这个问题坑的);比素材短 → 一次性动作被砍掉尾巴。

所以四个点击分支的 stateMs 都按各自素材的帧数 × 83ms 对齐、再多留一帧(剪完死帧后:挥手 23 帧 → 1992、托腮 27 → 2324、比心 27 → 2324、屑表情 27 → 2324)。剪了素材的帧就必须回来改这里,否则就退回"多播一截"。多留的那一帧是防定时器抖动:stateMs 比素材短会把一次性动作砍掉尾巴。

三、frames2d 没有 pingpong。

petpet 那边靠 pingpong 往返播放解决"素材首尾接不上",dsh 这边没有。所以吃饭那 7 帧在 manifest 里是手工展开成 12 格往返序列的(f01..f07, f06..f02)。同一个文件名可以重复引用,客户端按 URL 缓存,不会重复解码。

这个数组就是"剪死帧"的存放处,两条路都认(native 引擎 2026-09-26 之前只列目录、把它整份丢掉了,见坑七)。所以 frames 里写了什么、屏幕上就播什么 —— 改这里的帧数必须同步改点击分支的 stateMs(坑二),两处差一帧就会两头切错位置。

四、轨道名不许有下划线,单轨最多 64 帧。

  • 轨道名走 /^[a-z0-9][a-z0-9-]*$/,下划线不合法(叫 eat_mid 会让整条轨道被拒);frames 里的必须是相对该轨道自己目录的纯文件名(写成 eat/f01.png 这种带路径的会被当缺帧逐个丢掉)
  • 单轨 frames 最多 64 条。超限是整只宠物被静默丢弃、界面上直接没有她,所以"端碗吃 N 轮之后换成中口吃"这种长循环写不出来。想改只能动 dsh-pet 的 FRAMES2D_MAX_FRAMES(src/manifest-v2.ts 与编译产物 lib/ 各一处,改完会被下次升级覆盖)

五、同一段工作里别给不同 phase 映射不同轨道。

每次 phase 变化,只要映射到的轨道名和当前的不一样,就从第 0 帧重播(renderers/frames2d.ts:订阅里 if (target !== track) play(target),而 play() 第一件事就是 frameIndex = 0)。

而一轮里 tool 和 thinking 是交替出现的 —— tool/call → tool,tool/result → thinking,reasoning-delta → thinking。所以这两者映射到不同轨道时,每调用一次工具就会重播两次。原来 thinking → 中口吃 就是这么坏的:一轮十几个工具调用,等于二十几次"把碗放下又端起来",看起来极乱。

修法是把 waiting / thinking / tool / review 全部映射到同一条 eat,一轮里轨道只在开头和结尾各切一次。(中口吃素材还在 pet/frames/eat-mid/,轨道也保留着,只是现在没人引用。)

六、phase 映射没有概率,而 idleDirector 在这版里是死的。

带随机的只有四处:点击分支 branches[].probability、待机导演 idleDirector.acts[].weight、打工 work.successProbability、商店 shop.lottery。想要"某个 phase 按概率换动作",manifest 表达不了。

idleDirector 本来是最像"闲着时小概率做个动作"的机制,但它的守卫是 phaseRef.current !== 'idle',而 phase 是粘性的(见坑一)——正常跑完一轮后永久停在 done,只有"一轮被中止"或"会话被 dispose"才会回到 idle。官方自带的四只宠物也没有一只用它(grep idleDirector assets/*/pet.json 全空)。所以别指望它。

七、素材首尾自带"动作之外"的帧,要剪。

build_video.py 是 np.linspace(0, total-1, n) 均匀抽帧,抽出来的窗口是按"整条视频"切的,不是按动作边界切的。所以每条轨道的开头/结尾都挂着动作之外的帧:开头是她还没起手的站桩(点了之后先愣一下),结尾是动作做完后的站桩、有时候还混进下一段动作的开头几帧。

实测(eat 那条最严重):

  • eat:源视频第 63~84 帧(约 0.9 秒)她本身就是定住的,抽中的 5 帧全是这段;而窗口取到第 91 帧,把下一口的起手也切了进来。循环里 38% 是死气。
  • wave:30 帧里开头 3 帧 + 末尾 4 帧是站桩,占 30%。
  • touchface / smug / putaway / heart:各有 2~3 帧。

剪法:直接改 frames 数组(文件不用删,留着方便回退)。eat 因为是手工展开的往返序列,剪完要重新补上回程。putaway 有 frameMs 数组,剪 frames 必须同步剪 frameMs,长度不等会让整只宠物被静默丢弃。

native 引擎(官方桌面版)也认这个数组 —— 2026-09-26 之前它只列目录、把剪好的序列整份丢掉(eat 的往返没了、38% 死气回来了;wave 从还没起手的 f01 开始播,而点击的 stateMs 是按剪后帧数配的,两头都切错)。现在帧序列这一块两条路语义一致:声明了就用声明,省略才列目录。("相位怎么映射"那部分两边仍有意不同,见坑一和 transitions.json 那节。)

判断"哪几帧是多余的"用逐帧相邻差:取整条轨道的相邻差中位数,低于它 35% 的连续首/尾段就是死帧。注意区分"死帧"和"有意的保持"——smug(保持得意表情)、touchface(托住)、heart(红心成型后停一拍)中间那些小差异是动作本身,别剪。

八、所有动作都在以约 2 倍速播放。

build_video.py 从 121 帧 @24fps(5.04 秒)的视频里均匀抽 30 帧 = 每 4.03 帧抽一张(等效 6fps),而播放是 12fps → 整个动作 2.02 倍速。每一条 30 帧的动作都如此。

这是设计时就这样的(12fps 是精灵动画的常规速率),一直没人抱怨;但如果哪天觉得"动作太急",先把这条对上。想改就加轨道级的 frameMs 数组(KNOWN_FRAMES2D_TRACK 允许 frames/frameMs/loop/fallback),把 83 调成约 166。eat 因为往返播放,前向速度本来就和源视频接近,改速要连 eat-mid 一起改,否则两种吃法会一快一慢。


素材与授权

pet/ 和 claude/whalegirl.petpack 里的素材全部由 AI 生成:立绘经 see-through 分层,动作由豆包图生视频再抽帧。要用在自己的项目里,请先确认对应平台的服务条款——不同平台对生成内容的商用和再分发规定不一样。

脚本部分 MIT。

—/ 5

No ratings yet

Verified DSH bundle

Commit 0f2d7dd2ade7

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout