# Claude 版(PetPet 桌宠) ![鲸鱼娘桌宠](../docs/preview/hero.png) 同一个鲸鱼娘的 [PetPet](https://github.com/stshourenxy-dev/petpet-playbook) 版本——挂在 Claude Code 桌面上的那只。她跟着你干活:你发消息她捧着碗吃(10% 的轮次换成端中碗拿筷子吃),**你一派子代理她就收碗、低头看脚边的小鲸鱼绕着她转圈**(跑完了鲸鱼沉入水面),这一轮干完收碗(10% 双手合十给你祝福),你按 Esc 打断她,她停下手里的事甩你一个屑表情。 > **不想手动折腾?让 AI 装**:把仓库链接和这句话发给你的 AI —— > 「照 `claude/README.md` 装鲸鱼娘桌宠。我用的是绿色版(解压出来的 `whalegirl-petpet` 目录,里面就带着 `claude\install.mjs`)。先跑一行去掉「来自 Internet」标记、免得双击弹 Windows 拦截框:`powershell -NoProfile -Command "Get-ChildItem '<绿色版目录>' -Recurse -File | Unblock-File"`,再跑 `node "<绿色版目录>\claude\install.mjs" --hooks-only --pet-exe "<绿色版目录>\PetPet.exe"`(先加 `--dry-run` 看一眼),然后按它最后打印的那一步收尾。」 > > **`--pet-exe` 别省**:不给的话,脚本会去桌面快捷方式和几个常见位置猜 PetPet.exe,猜不到就**跳过"开 Claude 自动把她拉起来"那一步**——它不报错,你也不会知道少了什么。**`--hooks-only` 是绿色版专用**:绿色版自带的 viewer 已经打好补丁了,加上它可以省掉一次注定失败的 GitHub 连接(脚本就在解压出来的包里,整条路完全不碰网络)。 > > 脚本是幂等的:重复跑不会出问题;宠物素材没换过就不动它,换过才覆盖(覆盖前备份 `pet.json`)。绿色版跑 `--hooks-only` 时**不会碰素材**——包里没有 `.petpack`,素材归「启动.cmd」管。 跟仓库根目录那套 dsh 版的区别:这是**精灵表 + pet.json v3** 的格式,走 PetPet 框架;dsh 那套是 **frames2d + manifest v2**,走 `@linxin666/dsh-pet` 插件。素材是同一批,动作逻辑也一样,只是打包格式不同。 --- ## 装 > ### 墙内(连不上 GitHub),或者你下的是绿色版 → 走这条 > > ```bash > node install.mjs --hooks-only --pet-exe "<解压目录>\PetPet.exe" > ``` > > `--hooks-only` 跳过「找源码 → 打补丁 → npm build」那三步。绿色版里的 viewer **已经打好补丁了**,那三步不但白做,第 1 步还要 clone `github.com`——墙内**必然失败**,脚本会白等一轮超时才报错,然后你多半会以为"这脚本坏了"。 > > `--pet-exe` 指到绿色版解压目录下的 `PetPet.exe`,写进 SessionStart 钩子用来自动拉起她。**这条路完全不碰 GitHub**,也不需要本机有 petpet-playbook 源码。 > > 不用绿色版、要自己从源码 build 的,看下面。 一条命令(Windows 上也可以直接双击 `install.cmd`): ```bash node install.mjs # 装 node install.mjs --dry-run # 先看它会改什么,什么都不动 ``` 它依次做完这四件事,**幂等**——补丁已打过、hook 已挂过都会跳过,重复跑没有副作用(宠物素材换过了才会覆盖更新,且覆盖前自动备份旧的 `pet.json`): 1. 找到(本机找不到就 clone)petpet-playbook 源码 2. 把 `viewer.patch` 打上去(内置 diff 应用器,不强依赖 git,能容忍行尾 CRLF/LF 差异) 3. 在 viewer 里 `npm install && npm run build` 4. 四个 hook 放进 `~/.claude/hooks/`、**合并**进 `~/.claude/settings.json`(先备份、只加不改、不覆盖你原有配置);再把 `whalegirl.petpack`、`naijing.petpack` 分别解到 `~/.petpet/pets/` 下各自的目录 常用参数:`--hooks-only`(只装 hook + 宠物,跳过源码那三步——**绿色版用户加这个**)、`--viewer <源码路径>`、`--pet-exe `(写进 SessionStart 钩子,开 Claude 自动拉起她)、`--no-build`、`--force`。 跑完还剩两步(脚本结尾会打印): - **让它跑起来**:`cd <源码>/viewer && npm run dev`(图省事),或者把 `viewer/dist/` 覆盖进你那份 PetPet 应用的 `resources/app/dist/`(先删旧目录,文件名带 hash) - **重启 Claude Code**(让 SessionStart 钩子生效) ### 它到底改了什么 / 不打补丁会怎样 - **补丁**是对 petpet-playbook **v1.3.0** 生成的(5 个文件:`viewer/main.js` / `src/main.ts` / `src/state-priority.ts` / `preload.cjs` / `pet-import.cjs`)。补丁开头有一份完整的改动清单(编号 ①~㉑),摘要:干活时动作可按权重换(`variants`)、收碗动作可分开(`variants[].putaway`)、外部状态多一个 `interrupted`、动作可标 `hold`(何时离开由外部信号决定,不参与自动转移链)、`loop: false` 的动作一律按一次性处理**且播完即走**、窗口几何 IPC 加 NaN 守卫、窗口默认 `focusable: false`(点她不再把焦点从终端抢走)、`petpet://` 协议补 `corsEnabled`、气泡文案分场景、**加单实例锁**(否则启动两份就并排出现两只宠物)、外部状态的"文件里是什么"和"上次发过什么"拆成两个变量、**精灵表支持折行**(帧数一多,单行排的表宽 = 帧数×帧宽,会超过 GPU 的最大纹理边长 16384;超限时纹理创建是**静默失败**的——不报错,只是那个动作永远加载不出来,点上去毫无反应。配套改掉了 `main.js` 和 `pet-import.cjs` 里按"帧宽×帧数"预估表宽的两处老校验,它们同样假设所有帧排成一行,会把折行表算出根本没发生过的宽度、把合法宠物拦在门外)、**连点摔倒**(2.5 秒内点够 8 次她绊一跤坐地上,坐满 3 秒自己爬起来,这三段播放期间点击一律不响应)、**导入宠物包用系统自带的 tar**(裸名 `tar` 在装了 Git 的机器上命中的是 GNU tar,它不认 zip;顺带一个更隐蔽的后果是解压前的"总量 / 单文件 / 压缩比"三道预算校验靠 `tar -tvf` 的字段位置取 size,GNU tar 的列排布不同会解析出空数组,**三条上限全部静默失效**)、**隐藏窗口时的系统通知从来没发出过**(Electron 主进程没有全局 `Notification`,那句 `new Notification(...)` 一直在抛 `ReferenceError`、被调用处的 try/catch 吞掉)、**干活期间重启桌宠她会整轮待机**(首帧状态在渲染层注册监听之前就推走了,而去重标记已经记下、之后不再重发)、**摔倒动作表不全时会锁死左键**(`canTrip()` 原来只查 `fall` / `sit`,`recover` 缺失就卡在一个没有出口的 hold 里;现在三段齐备才允许摔,真卡住也会自己回待机),外加几处上游 viewer 自己的 bug(提醒重启后不恢复、窗口位置根本不保存、日记窗口会出界等)。**⑳** 修 ⑲ 带出来的回归(`'idle'` 被挡在保留上一版状态的白名单外)、**㉑** 去掉双击回默认大小(双击必然先触发两次 click,她会连做两个点击动作再缩回去,逗她的时候特别容易误触)。上游比 v1.3.0 新的话可能打不上,脚本会明确报出来——按补丁里每处的注释手工合并即可。 - **装过旧版补丁的注意**:脚本判断"补丁打没打过"看的是几个**只有最新补丁才有的**特征串(比如 `main.js` 里有没有 `requestSingleInstanceLock`)。旧补丁缺它,所以重跑 `install.mjs` 会**尝试重打**——但旧改动还在、上下文对不上,多半会报"打不上",这时要么照 `claude/viewer.patch` 手工合并那几处,要么把 `viewer/` 还原到 v1.3.0 再装一遍。(更早的版本是"命中任一关键词就跳过",那样会静默漏掉后加的修复,已经改掉。) - **`.petpack` 就是个 zip**:顶层 `<宠物 id>/` 目录,里面 `pet.json` + 每个动作一张精灵表。有两只:`whalegirl.petpack`(17 张表 / 18 个动作——`putaway` 和 `subagent_putaway` 共用同一张)、`naijing.petpack`(奶鲸,3 张表)。**两只都要装**:whalegirl 的 `pet.json` 里写着 `invasion.to = naijing`,右键菜单那条入侵动画播完会切到奶鲸,少装它就切到一只不存在的宠物。也能用 PetPet 托盘菜单「导入宠物包」手动装。**这两个文件不在仓库里**(`whalegirl.petpack` 打出来 115MB,超过 GitHub 单文件 100MB 上限)——用绿色版的话素材已经在它的 `pets/` 里、由 `启动.cmd` 装好,根本用不着 petpack;真想要就 `python claude/pack_petpack.py` 从 `~/.petpet/pets/` 现打一份。 - **不打补丁也能跑**:那几个字段会被忽略——永远吃普通的小口饭、点她只会挥手、被打断只是停下(没有屑表情),不会报错、不会卡住。**但前面那个几何 IPC 守卫属于稳定性修复**,不打的话拖拽/缩放时一旦算出坏坐标,主进程会直接弹框退出(是上游 v1.3.0 自带的隐患,不是这个宠物包引入的)。 ### 手动装(不想跑脚本,或脚本卡住了) 1. 装好 PetPet([petpet-playbook](https://github.com/stshourenxy-dev/petpet-playbook))。它的 [Releases](https://github.com/stshourenxy-dev/petpet-playbook/releases) 有预编译版(`PetPet.<版本>.exe` 便携版 / `PetPet.Setup.<版本>.exe` 安装版 / macOS `.dmg`)——**但预编译版把应用代码打进了 `app.asar`,外部改不动,所以想要完整效果必须走源码**。 2. ```bash git clone --branch v1.3.0 https://github.com/stshourenxy-dev/petpet-playbook.git cd petpet-playbook git apply /path/to/claude/viewer.patch # 或 patch -p1 < claude/viewer.patch cd viewer && npm install && npm run build ``` 3. 绿色版再把 `viewer/dist/` 整个覆盖进 `resources/app/dist/`(先删旧的)。 4. 托盘菜单 → **导入宠物包** → 选一个 `.petpack`(仓库里没有,见上一节末怎么得到)。 5. 四个 hook 按下节的手动配置来挂。 --- ## 想让它跟着 Claude Code 干活 桌宠本身就能跑(待机、点它挥手/比心)。要它"你干活时捧碗吃饭、干完收碗、被打断甩你一眼"还得挂四个 hook——桌宠不知道 Claude 在干什么,得有人告诉它。 把 `petpet-state.mjs`、`interrupt-watch.mjs`、`petpet-launch.mjs`、`petpet-subagent.mjs` 四个文件放进 `~/.claude/hooks/`(只有 `petpet-launch.mjs` 需要改路径——它顶部 `EXE` 那行要指向你自己的 PetPet),然后改 `~/.claude/settings.json`: ```json { "hooks": { "SessionStart": [ { "hooks": [{ "type": "command", "command": "node C:/Users/you/.claude/hooks/petpet-launch.mjs" }] } ], "UserPromptSubmit": [ { "hooks": [{ "type": "command", "command": "node C:/Users/you/.claude/hooks/petpet-state.mjs working" }] } ], "Stop": [ { "hooks": [{ "type": "command", "command": "node C:/Users/you/.claude/hooks/petpet-state.mjs idle" }] } ], "SubagentStart": [ { "hooks": [{ "type": "command", "command": "node C:/Users/you/.claude/hooks/petpet-subagent.mjs" }] } ], "SubagentStop": [ { "hooks": [{ "type": "command", "command": "node C:/Users/you/.claude/hooks/petpet-subagent.mjs" }] } ] } } ``` 四个脚本挂在五个事件上,各管一摊: | hook | 干什么 | |---|---| | `SessionStart` | 开 Claude 时把 PetPet 和打断检测器拉起来(没在跑才拉) | | `UserPromptSubmit` | 你按下回车 → 写 `working` → 她开始吃 | | `Stop` | 一轮结束 → 写 `idle` → 她收碗 | | `SubagentStart` / `SubagentStop` | 派子代理 → 她收碗、低头看脚边的鲸鱼转圈;全跑完 → 鲸鱼沉入,她接着干活 | 后两个共用 `petpet-subagent.mjs`:它按事件的 `agent_id` 在 `~/.petpet/subagents/` 里建/删一个记号文件,桌宠那边数文件个数。**不用计数器**是因为 hook 是短命且并发的进程,加减一定错——按文件天然免疫。 **如果你不是用绿色版 exe 跑的**(比如 `npm run dev`):把 `SessionStart` 那行去掉,改成开机后手动跑一次 `node ~/.claude/hooks/interrupt-watch.mjs` 就行——打断检测和吃饭这两件事都不依赖它。 状态写进 `~/.petpet/state.json`,PetPet 每秒轮询那个文件。**顺带**:状态文件超过 30 分钟没更新会被当成 idle,防止 Claude 被强杀之后桌宠卡在"一直吃"。 ### 打断(Esc)得单独检测 上面两个 hook 有个缺口:**官方没有任何 hook 在"用户按 Esc 打断"时触发**——hooks 文档对 `Stop` 写得明白,"Does not run if the stoppage occurred due to a user interrupt",也没有 `Interrupt`/`TurnCancelled` 这类事件。所以只挂这两个 hook 的话,**你一按 Esc 她就会一直端着碗吃下去**,直到你下次发消息。 打断这个事实只会落进会话 transcript: ```json {"type":"user","message":{"content":[{"type":"text","text":"[Request interrupted by user]"}]}, "interruptedMessageId":"<被打断那条 assistant 消息的 uuid>"} ``` 所以仓库里另有一个 `interrupt-watch.mjs`(放 `~/.claude/hooks/`,由 `SessionStart` 钩子顺带拉起、自带 pid 文件保证单实例):它每秒瞄一眼最近动过的 transcript,读到新的 `interruptedMessageId` 就往 `state.json` 写 `{"state":"interrupted"}`。桌宠这边收到这个状态就停下吃饭、按 `pet.json` 的 `reactions.interrupted` 播个反应动作(这里是屑表情),然后回待机。 **注意**:这段依赖 transcript 的内部格式(没有官方背书),字段名变了它只会静默失效——变成"打断后还是停不下来"的老样子,不会报错。日志在 `~/.petpet/interrupt-watch.log`。 --- ## 她的行为 | 你做什么 | 她做什么 | |---|---| | 发出消息、Claude 干活期间 | 捧着碗一口一口吃(循环),并弹一句"吃饭"的气泡。**其中 10% 的轮次会换成端出中碗、拿筷子正经吃一顿**(同样循环,直到这轮结束) | ![干活时她就这样吃](../docs/preview/eat.gif) | **派子代理的时候** | 先把手里的碗收掉,低头看脚边的小鲸鱼绕着她转圈(一直转,直到子代理跑完);子代理全跑完了鲸鱼沉入水面,她接着回去干活 | ![派子代理时她就这样](../docs/preview/subagent.gif) | 这一轮结束 | 收碗,然后 55% 双手托腮发呆 / 10% 双手合十给你祝福一下 / 35% 回待机 | | **你按 Esc 打断她** | 立刻停下手里的动作,甩你一个屑表情(眨眼 + 半眯眼笑),然后回待机 | | 什么都不做 | 老实待机——长发和尾巴轻轻晃,偶尔眨眼 | | 单击她 | 挥手打招呼(**8% 的几率改成甩你一个屑表情**) | | 连续点 4 次以上 | 每次有 45% 概率改比心 | | **2.5 秒内点够 8 次** | 绊一跤坐地上("别戳啦,我摔给你看了"),坐满 3 秒自己爬起来拍裙子;摔倒这三段里点她一律没反应 | | 隔了十分钟以上没理她,再点她 | 不说"要摸摸头吗",换成一句"你回来啦"那种 | 连点计数(`RAPID_WINDOW` / `RAPID_AFTER`)和比心那个 `clickStreak` **是两套**:比心窗口只有 600ms,而且一旦触发就自己清零,共用的话摔倒永远排不上队。她端着碗吃、或者正低头看小鲸鱼转圈的时候不摔——这两个状态由外部信号驱动,摔完没人把她送回原来那一步。 除待机外所有动作 `weight: 0`,所以不会自己随机乱播(这是刻意的)。 概率都写在 `pet.json` 里,就两处: - **`actions.eat.variants`** —— 干活时吃哪碗饭。`[{ "action": "eat_mid", "weight": 10, "putaway": "eat_mid_putaway" }]` 的意思是"10% 换成中口吃",剩下的 90% 仍是普通的吃;`putaway` 指明这一碗收工后走哪条收碗动作(中碗要放下的是中碗,用小口吃那条收碗会硬跳)。想让中口吃更常见就调大 `weight`,不想要就删掉这个字段(删了就永远只吃小口饭)。**每次从待机进入干活时掷一次骰子**,一轮对话里不会反复重掷。 中口吃用的**不是**素材的完整 30 帧:那 30 帧是一整套"空手站 → 端起碗 → 吃 → 放下碗 → 空手站",loop 起来会变成每 2.5 秒放一次碗。这里只取 f11~f22 那 12 帧"碗举在脸前、筷子扒饭",配 pingpong 往返(共 22 格),和小口吃是同一套做法;**收碗也从同一条素材里单独切了 f22~f30 那 9 帧**(放下中碗 + 手收回),生成脚本在 `whalegirl-pet-kit` 的 `tools/build_eat_mid_core.py`。 - **`putaway.transitions`**:收碗之后按权重挑下一个动作。普通收碗是 `{touchface: 55, bless: 10, idle: 35}`;中碗收工走的是 `eat_mid_putaway`,它的是 `{touchface: 60, idle: 40}`,不掷祝福。 - **`bubbleScenes`** —— 她说话分场景,一个场景一套词,从里面随机挑:`click`(点她)、`working`(干活时吃那碗饭)、`interrupted`(你按 Esc 打断,配着屑表情说)、`return`(隔了十分钟以上没理她、再来点她)。**没配的场景回落到 `bubbles` 那个通用池**,所以老宠物包和官方包行为不变。想换她的语气就改这里;想加新场景就在这个对象里多写一个键,代码里相应位置传上场景名即可。十分钟这个阈值在 `viewer/src/main.ts` 顶部的 `RETURN_GAP_MS`。 PetPet 只在启动时读 `pet.json`,改完要重启它。 **`variants` / `reactions` / `bubbleScenes` 是本仓库对 viewer 的扩展**,不是 petpet-playbook 官方字段(见上面「装」第 1 步的 `claude/viewer.patch`):`viewer/src/main.ts` 里加了 `pickVariant()`,按 `actions.<动作>.variants` 的权重决定播哪个动作;`reactions` 把外部状态映射到反应动作;`bubbleScenes` 按场景挑气泡文案。用**官方未改版**的 viewer 打开这个宠物包,这些字段会被忽略——不会报错,只是没有中口吃/屑表情这些花样,气泡也只会用通用的那套。改动本身也不影响别的宠物(没配这些字段时行为与原来完全一致)。 同一份补丁还修了几个上游 viewer 自己的问题(与这只宠物无关):**提醒重启后不恢复**(而且存档会被空地图覆盖掉,"每天"提醒的语义形同虚设)、每日/每周提醒休眠醒来会连发好几次、日记窗口在宠物位于屏幕下半部时会出界、**窗口位置根本不会保存**(拖完重启她就回到老地方)、宠物目录名不合法时列表里会出现一个永远加载不了的项。 补丁还顺手修了三个会让宠物**点不动**的 bug,都出在点击/穿透这条链上。 一是穿透状态卡死:窗口尺寸或位置一变(换动作、缩放、拖拽之后),角色相对光标的位置就跟着变,而"鼠标在不在她身上"这个判断只在鼠标移动时才重算——鼠标没动时旧结论一直留着,窗口就卡在穿透状态。实测卡住以后连 mousemove 都收不到,怎么移鼠标都点不到她,只能重启。现在主进程每秒推一次光标位置过去重算,不再依赖 Electron 的 forward 转发。 二是切换滞后,也是日常最容易撞上的那个:穿透状态下的窗口收不到 pointermove,什么时候能点全看主进程喂坐标的节奏。原来一秒喂一次,"光标落到她身上"到"窗口真的能收点击"之间要空出半秒(实测 405~512ms)——而人回来找她必然是"甩过去就按",按下那一下窗口还是穿透的,这次点击就被系统丢给了下面的窗口,症状正是"画面照常动、右键照常用、左键怎么点都没反应"。现在改成 60ms 喂一次,实测降到 30~50ms。顺带给点击判定补了条兜底:一次点击只收到"松开"、没收到"按下"(就是按下那一下被丢掉了),也按点击处理,不再被误判成拖拽吞掉。 三是右键引起的:右键点她会弹出原生菜单,菜单一出来鼠标就被它独占,"松开"送不回页面,拖拽状态于是一直挂着、把穿透状态冻住,这几秒里她照样点不动。现在拖拽只认左键。 待机那套呼吸、飘发、眨眼不是抽帧来的——是从分层 PSD 程序化烘焙的,24 帧,AI 画不出这种幅度的小动作。其余动作由 AI 视频抽帧配准而来。工具链在 [whalegirl-pet-kit](https://github.com/wei125775-lab/whalegirl-pet-kit)。 --- ## 素材 `whalegirl.petpack` 里的立绘和动作帧**全部由 AI 生成**(立绘经 see-through 分层,动作由豆包图生视频抽帧)。拿去做别的事情之前,先确认所用平台的服务条款——不同平台对生成内容的商用和再分发规定不一样。 脚本(包括 `petpet-state.mjs`)是 MIT。