Files
HomeAgent/DESKTOP-PET-NOTES.md
JianFeeeee 57bf942474 feat(gui): 注入改 SendInput(默认)+ 用户操作时暂缓;拆解 Coopanion 桌宠动作系统
## 拆解 Coopanion(用户指出的参考项目)
新增 DESKTOP-PET-NOTES.md,记录 Pal-AI-Lab/Coopanion 的桌宠动作系统
(packages/cortico-world-desktop-pet):

  · 4 个工具:pet_say(气泡说话,可夹带动作标记) / pet_ask(提问,异步回答) /
    pet_walk_to(走到 0-1 比例或 left/center/right/cursor,含走到鼠标位置) /
    pet_act(只做动作; sit/sleep 是持续态)
  · 动作词表 VOCAB:10 个表情 + 13 个动作,每个词多个中文别名,
    英文 id 与中文别名进同一张 Map;未知词丢弃而非报错
  · 脚本标记:【词,词】= 先做动作再换新气泡;<词> = 打字到那里时做、不换气泡。
    让文字与动作在时间轴上对齐 —— 实现成本低但表现力强
  · 设计要点:身体是 agent 唯一可见输出通道(用户看不见消息正文);
    互动事件不单独唤醒模型,跟着下批事件到

可借鉴(已写入文档待办):姿态表达状态、动作与文字对齐、持续态、
动作解析容错、待机自主行为。不建议照搬:桌宠承载全部对话输出、ASR、戳摸甩。

## 注入层改 SendInput(参考它的实现)
原 PostMessage 路径只能操作原生 Win32 程序 —— Chromium/Electron 自绘界面
与游戏会忽略合成消息。这不是「平台硬限制」,是所选 API 的限制。
Coopanion 用 SendInput 注入系统输入队列,可靠性高得多。

新增三档模式 gui-prefs.deviceBridge.agentCursor:
  sendinput(默认) SendInput,可靠;会动真光标,故先等用户停手
  overlay           PostMessage,不动用户鼠标;只能操作原生程序
  real              保留旧代码路径
另有 deferMs:用户活动时最多等多久(默认 3000,上限 10000)

★ SendInput 绝对坐标是 0–65535 归一化值,不是像素。按虚拟桌面归一化
  并带 MOUSEEVENTV_VIRTUALDESK,多屏下也正确(Coopanion 只按主屏归一化)。
★ 文本用 KEYEVENTF.UNICODE 逐字符发,不受键盘布局与输入法影响(比 WM_CHAR 可靠)。

## 修「用户活动感知」把自己也算进去的 bug
GetLastInputInfo 是**全局**的 —— agent 自己的 SendInput 也被算作「用户输入」。
实测:deferMs 设 0 仍被暂缓,回执写「开始时空闲仅 0ms」,即上一条命令的
注入刚把 idleMs 清零。若不排除自身注入,会永远处于「用户正在操作」状态。

按 Coopanion 的三重判定修正:
  1. ownTick   自己最后一次注入时刻,之后 40ms 内的输入忽略
  2. ownCursor 自己放下的光标位置,偏离 >2px 说明是用户动的
  3. 无符号差值  GetTickCount 每 2^32ms 回绕,必须按无符号比较

## 修同一条命令被执行两遍
sendinput 分支把逻辑包进 runAction 箭头函数,里面的 return 只退出它自己,
于是执行完继续 fall through 到旧 real 路径。实测 8 条命令回了 16 个
cmd_result,且真光标被额外抢一次。补上外层 return。

## 判据
computeruse.test.mjs 47 条(npm test 共 131 条全绿),新增 4 条钉住本轮
踩过的坑,均做变异验证:
  · 三档模式存在(overlay/real 只在条件里,不能要求赋值形式)
  · 用户活动排除自身注入;且盯 send() 里的**调用点** —— 只查函数名时
    删掉调用仍会误报绿(已实测并改正)
  · SendInput 坐标归一化 + VIRTUALDESK
  · sendinput 分支必须 return(防双执行)

## 待办(已写入 GUI-PLAN.md)
  · loadGuiPrefs 不透传 agentCursor,导致 deferMs 配置读取不生效(实测)
  · 显式调 SetProcessDpiAwarenessContext(-4),不依赖 Electron 默认值
  · mascot 姿态:思考/等待/出错
  · 真实点击效果仍未验证(当前环境无可注入的可见窗口)
2026-10-02 15:27:33 +08:00

9.7 KiB
Raw Blame History

桌宠动作系统: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 待机自主行为:闲置时轻微眨眼/呼吸