# 桌宠动作系统:Coopanion 拆解与可借鉴点 > 来源:`Pal-AI-Lab/Coopanion`(本机 clone 于 `/tmp/coop`) > 相关包:`packages/cortico-world-desktop-pet/` > > 本文只记录**桌宠的动作/表情/互动系统**,以及本仓(HomeAgent GUI)可借鉴什么。 > 输入注入层(SendInput)的对比见 `GUI-PLAN.md` 与下文第七节。 --- ## 一、它是什么 一个 C 形小生物,站在屏幕底边(任务栏上沿)。**用户看得见它的身体,看不见 agent 的消息正文** —— 想让用户看到内容,只能通过它冒气泡。 这句话是整个设计的关键: > 桌宠身体是 agent 的**唯一可见输出通道**,而不是装饰。 这和我们 GUI 的定位不同:我们的 mascot 只是「agent 光标」,不承载信息。这是一个可以借的点。 --- ## 二、核心工具(4 个) | 工具 | 作用 | 关键设计 | |---|---|---| | `pet_say` | 头顶冒气泡说话 | 脚本里可夹带**动作标记**,边说边做 | | `pet_ask` | 冒提问气泡 + 最多 3 个选项 | **立即返回**,回答通过 `[回答]` 事件异步送达 | | `pet_walk_to` | 沿屏幕底边走到某处 | 目标是 0–1 比例或 `left/center/right/cursor` | | `pet_act` | 不说话,只做一串动作 | `sit`/`sleep` 会**持续**到下个动作 | ### 值得注意的三个设计 1. **`pet_ask` 是异步的**。agent 提完问不阻塞,用户答了再通过事件唤醒。 避免 agent 干等,也允许用户先不答。 实用细节:新问题会**替换**还没回答的旧问题;关掉不答也会回一个事件, agent 不会永远等下去。 2. **`pet_walk_to` 支持 `cursor`** —— 走到鼠标当前横向位置。 这是个很自然的"到我这儿来"表达,不需要坐标。 3. **`sit`/`sleep` 是持续态**,其他动作是一次性。 这让 agent 能表达"待机"与"我在休息",而不只是瞬间动画。 --- ## 三、动作词表(`src/script.ts` 的 `VOCAB`) 它把动作分成两类:**表情**(持续几秒后回到平常脸)与**动作**。 ### 表情(expression,10 个) | id | 中文 | 表现 | |---|---|---| | `neutral` | 平静 | 默认的脸 | | `happy` | 开心 / 高兴 | 眼睛弯成 ^ ^ | | `wink` | 眨眼 | 一只眼 ^ | | `love` | 喜欢 / 爱心 | 眼睛变心形,冒小心心 | | `shy` | 害羞 | 脸红,眼神躲开 | | `surprised` | 惊讶 / 吃惊 | 眼睛放大,头顶感叹号 | | `angry` | 生气 | 皱眉,头顶怒气符号,身体发抖 | | `sad` | 难过 / 伤心 | 八字眉,掉眼泪 | | `sleepy` | 犯困 / 困 | 眯眼打哈欠 | | `thinking` | 思考 / 想想 | 眼睛往上看,头顶冒圈 | ### 动作(motion,13 个) | id | 中文 | 表现 | |---|---|---| | `stand` | 站起 / 站 | 站起来(坐着、睡着时) | | `jump` | 跳 / 跳起来 | 原地起跳 | | `hop` | 小跳 / 蹦 | 小小蹦一下 | | `look` | 张望 / 看看 | 左右张望 | | `turn` | 转身 | 转向另一边 | | `nod` | 点头 | 点两下头 | | `shake` | 摇头 | 摇头 | | `spin` | 转圈 | 原地转一圈 | | `sit` | 坐下 / 坐 | **坐下,持续到下个动作** | | `sleep` | 睡觉 / 睡 | **躺下睡觉,持续到下个动作** | | `dizzy` | 晕 / 转晕 | 头晕眼花几秒 | | `walk` | 走走 / 散步 | 随便走一段 | | `run` | 跑 / 跑起来 | 跑到屏幕另一头 | ### 词表设计的可借鉴处 - **每个词有多个中文别名**(`开心`/`高兴` 同一个 id)。LLM 输出自然语言时容错率更高。 - **英文 id 与中文别名都进同一张 Map**(`BY_WORD`),不区分来源。 - **未知词被丢弃而非报错**(`dropped` 数组收集但不中断)。 对 LLM 输出的容错策略,值得学。 - 词表通过 `pet.vocab` 变量**注入到 prompt**,让模型知道能做什么 —— 而不是把 30 个动作硬编码进 tool schema 的 enum。 --- ## 四、脚本标记(说明与动作混排) `pet_say` 的 `script` 参数里可以夹带两种标记: | 标记 | 作用 | |---|---| | `【词, 词】` | 先做这些动作,然后**换一个新气泡**继续说 | | `<词>` | 打字打到这里时做,**不换气泡** | 示例(源码里的原例): ``` 【开心】今天也辛苦啦!<眨眼>要不要歇一会儿? ``` 这个设计解决了一个真实问题:**让文字与动作在时间轴上对齐**。 `<眨眼>` 落在句子中间,让"眨眼"发生在念到那个字的时候,而不是整句之前。 比"先做动作再说话"生动得多,实现成本却很低(解析时切分即可)。 相关实现:`parseScript` / `parseActions` / `estimateSeconds`(`src/script.ts`), `estimateSeconds` 用于让回执报告"大约显示多久"。 --- ## 五、自主行为与互动 ### 待机(没有指令时) > 没收到指令时身体会自己眨眼、张望、散步、坐下打盹;你的指令会先执行。 注意最后半句:**agent 指令优先于自主行为**。自主行为是填充,不是竞争。 ### 用户互动(事件形式回给 agent) | 事件 | 含义 | |---|---| | `[语音] 用户:…` | 麦克风听到并转写的话(可能同音错字) | | `[打字] 用户:…` | 在桌宠上双击或点悬停按钮打的字 | | `[回答] …` | 对 `pet_ask` 的回答 | | `[互动] …` | 戳、摸、拎起来甩出去,以及落地摔晕 | 细节值得注意: - 互动事件**不单独唤醒 agent**,"跟着下一批事件到,可能已经过去一会儿" —— 避免用户随手一戳就把模型叫起来烧钱。 - `asleep` 状态下被摸,文案不同(`摸了摸睡着的你`)。 状态影响事件语义,而不只是影响动画。 --- ## 六、对本仓(HomeAgent GUI)的可借鉴点 ### 6.1 可以借鉴(成本低、收益明确) | 点 | 现状 | 建议 | |---|---|---| | **动作与文字时间轴对齐**(`【】`/`<>`) | 我们的 mascot 只有移动/点击两种状态 | 若要表达"思考/等待/出错",可引入轻量标记 | | **姿态表达状态** | 「忙碌呼吸」一种 | 可区分:思考中(转圈)、等待用户(张望)、出错(晕) | | **持续态 vs 一次性**(`sit`/`sleep`) | 无持续态 | agent 空闲/被禁用时用持续态表达,而非静止 | | **LLM 输出容错**(未知词丢弃不报错) | 未处理 | 动作解析应容错,不能因一个错词整条失败 | | **待机自主行为** | 无 | 光标闲置时可轻微眨眼/呼吸,让"活着"可感知 | ### 6.2 不建议照搬 | 点 | 原因 | |---|---| | **桌宠承载全部对话输出** | 我们是桌面客户端,有完整聊天界面,不该退化到只能冒气泡 | | **语音输入(ASR)** | 与本期目标无关,且引入麦克风权限问题 | | **戳/摸/甩出去** | 需要模型 + 物理引擎,投入产出比低 | | **`pet.vocab` 注入 prompt** | 我们没把 mascot 交给 LLM 控,动作由 computeruse 隐式驱动 | ### 6.3 一个更重要的判断 **Coopanion 的桌宠和我们的"独立光标"解决的是不同问题:** - 它的桌宠 = **输出通道**(让用户看见 agent 想说什么) - 我们的 mascot 光标 = **过程指示**(让用户看见 agent 在操作哪里) 两者不冲突,甚至可以合并 —— 让我们的 mascot 在 agent 思考时表现"思考"姿态, 在执行 computeruse 时表现"移动/点击"。这正是 6.1 表格里那几条的价值所在。 --- ## 七、接口层对比(顺带记录,详见 GUI-PLAN.md) 它和我们在**输入注入**上的关键分歧: | | Coopanion | 本仓(HomeAgent GUI) | |---|---|---| | 注入 API | `SendInput` | `SendInput`(默认)/ `PostMessage`(可选) | | 能否操作 Chromium/游戏 | ✅ 能 | ✅ 能(sendinput 模式) | | 是否移动真实光标 | ✅ 会移动 | ✅ 会移动(sendinput)/ ❌ 不动(overlay) | | 用户操作时 | 等待空闲再注入 | 等待空闲再注入(**借鉴自它**) | | 自绘光标 | ❌ 没有 | ✅ 有(mascot + 点击脉冲) | ### 从它学到的关键一条 `GetLastInputInfo` 是**全局**的 —— agent 自己的 `SendInput` 也会被算作"用户输入"。 若不排除自己的注入,`idleMs()` 每次注入后归零,于是**永远处于"用户正在操作"状态**, 所有命令都被暂缓。 它的做法(我们已照此实现)是三重判定: 1. `ownTick` —— 自己最后一次注入的时刻,之后 40ms 内的输入忽略 2. `ownCursor` —— 自己放下的光标位置,偏离 >2px 说明是用户动的 3. 无符号差值 —— `GetTickCount` 每 2^32 ms 回绕,必须按无符号比较 这个坑我们实测踩到了(`deferMs:0` 仍被暂缓),教训已写进 `agent-inject.js` 注释。 --- ## 八、其他独立印证 Coopanion 独立印证了我们上一轮的两个判断: 1. **坐标就是物理像素,不要换算** —— 它注释明写: > All coordinates are physical pixels of the primary display. The process declares > itself per-monitor DPI aware (v2) before anything else, so no API here returns scaled values. 我们曾错误地加了 `/scaleFactor`,已改回并在判据中钉住。 2. **必须显式声明 DPI 感知** —— 它显式调用 `SetProcessDpiAwarenessContext(-4)`, 并注明 "before anything else"。我们是**依赖 Electron 默认值**,未显式设。 虽然 Electron 默认已是 per-monitor aware,但显式声明更稳。**建议补上**(见待办)。 --- ## 九、待办(由本文档派生) - [ ] `main.js` 显式调用 `SetProcessDpiAwarenessContext(-4)`,不依赖 Electron 默认值 - [ ] 修 `deferMs` 配置读取不生效的问题(实测 `deferMs:0` 仍走 3000ms 默认) - [ ] mascot 增加姿态:思考 / 等待 / 出错(参考第三节词表) - [ ] 动作解析容错:未知动作丢弃而非整条失败 - [ ] (可选)mascot 待机自主行为:闲置时轻微眨眼/呼吸