Commit Graph

4 Commits

Author SHA1 Message Date
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
f0a7ce23b1 feat(gui): 模式 A agent 独立光标 + 后台注入;修坐标换算;固化计划与交接文档
## 模式 A:agent 独立光标(不抢用户鼠标)

现有 computeruse 走 SetCursorPos + mouse_event,agent 一干活就抢走用户
真实鼠标,用户无法同时使用电脑。现改为:GUI 自绘 agent 专属光标,实际
输入经 PostMessage 直接投递到目标窗口句柄,用户鼠标全程不动。

新增:
  cmd/gui/agent-cursor.js   透明置顶层窗口 + mascot 指针 + 移动/点击脉冲
                            动画;记录上次命中的 hwnd 供键盘动作使用
  cmd/gui/agent-inject.js   Windows 后台注入:WindowFromPoint 定位、
                            客户区坐标换算、PostMessage 投递鼠标/键盘/
                            滚轮/文本;拖拽分步移动

配置 gui-prefs.deviceBridge.agentCursor.mode = "overlay"(默认) | "real"。

明确的设计边界(不是实现缺陷,是平台限制):
  · 只对「接受窗口消息的程序」可靠。原生 Win32 程序可靠;Chromium/
    Electron 自绘界面、游戏、DirectX 会忽略合成消息。
  · 不做真实鼠标降级。用户既选了不抢鼠标,就不该偷偷改成操作真鼠标,
    失败即 status=error 并说明原因。
  · 键盘类动作依赖先前的鼠标操作(后台注入不动真光标,
    GetCursorPos 拿到的是用户自己的光标位置)。

端到端 8/8 正确响应;「真实点击是否落到目标窗口」因当前环境无可注入的
可见窗口而未验证,已在 GUI-PLAN.md 列为待办。

## 修坐标换算(我自己上一轮引入的 bug)

上一轮我加了 rawX / scaleFactor 的换算,理由是「SetCursorPos 期望 DIP」。
**那个前提是错的** —— Electron 主进程是 per-monitor DPI-aware,Win32 坐标
不做虚拟化。实测:

  Electron screen API (DIP) : 1260 x 840   scaleFactor=2
  GetSystemMetrics          : 2520 x 1680
  SetCursorPos(200,200) -> GetCursorPos 读回 (200,200)   恒等,不缩放

加 /scale 会把原本正确的点击改坏:200% 屏上点 (600,400) 被送到
(400,267),越靠右下偏得越远。已改回 physical ? rawX : rawX*scale,
drag 终点同步修正(原先两处换算不一致)。

易误判之处:在普通 node 进程里测同一函数读到的是 1260x840(虚拟化值),
会以为 Win32 用 DIP —— 必须在 Electron 内测。

## 判据
computeruse.test.mjs 43 条(npm test 共 127 条全绿),新增 5 条钉住
模式 A 的核心承诺:
  · agent-inject / agent-cursor 均无 SetCursorPos 调用(查前先剥注释,
    因为注释里会引用该名字解释「为什么不用它」)
  · koffi 指针参数用 Buffer(普通对象不写回,会拿错 hwnd)
  · overlay 默认开启且可切回 real
  · 自绘光标层 setIgnoreMouseEvents 穿透
核心不变量已做变异验证:注入 SetCursorPos 后判据立即转红。
另修正坐标判据方向(原判据断言 /scale 是对的,与实测相反)。

## 文档
GUI-PLAN.md          改造计划与进度的单一事实来源:已完成/待办、坐标
                     实测结论与踩坑、koffi 3.x 使用约定、端到端测试方法、
                     环境注意事项(含本机无 Go 工具链、waiter 改动未编译验证)
SERVER-HANDOFF.md    服务端需同步的部分:放开 action 白名单(schema enum
                     + switch default 双重限制,不改则 11 个新动作下发不到)、
                     透传 tox/toy/steps/dx/ms、统一 scroll 方向语义、
                     写入坐标约定防重蹈
2026-10-02 14:12:32 +08:00
d6dd84e1c8 fix(gui): 启动期误判服务端未启动;keybd_event 类型名致组合键全废
## 修 GUI 反复误判「服务端未启动」
isServerRunning() 硬编码 http://localhost:8080/。实测该部署下服务端在
192.168.2.60:8080(homed 跑在 WSL 里):
  localhost:8080      -> 000 连不上
  192.168.2.60:8080   -> 302 正常

于是每次启动都:探测失败 → 误判服务没起 → 去 startHomed() 拉起 GUI 旁
并不存在的 homed.exe(findHomed 返回 null 就 skip)→ 再 waitForServer()
干等 8s 超时 → 打印 "homed failed to start within timeout"。
gui.log 里这条 ERR 累计出现 53 次,是日志里唯一的错误项。

改法:
  · serverProbeTargets() 按「connections.json 里已配置的连接(当前连接
    优先)→ localhost」顺序探测,命中任一即视为在线
  · 服务可达时直接 skip homed 自动拉起
  · 无本地 homed 可执行文件时不再空等超时,直接说明情况
  · probeOne 加超时保护,避免启动被挂住

实测:修复后启动日志为 "server reachable, skip homed autostart",
该 ERR 不再出现。

## 修 keybd_event 类型名(组合键整体失效)
koffi 不认 Win32 的 `byte`:
  "void keybd_event(byte bVk, ...)"
    -> Error: Unknown or invalid type name 'byte'
  "void keybd_event(uint8 bVk, ...)"  -> 正常

原代码用的正是 `byte`,且整段包在空 catch 里,于是 keybd_event 恒为 null,
所有组合键(keypress/hotkey)都返回 "keybd_event unavailable"。
这个 bug 很难被发现:单键 click/move/scroll 走 mouse_event,不受影响。

端到端实测确认:修复前 hotkey ctrl+shift+t 报
  computeruse hotkey: keybd_event unavailable
另外签名解析失败现在会打日志,不再静默。

## 判据
computeruse.test.mjs 扩到 38 条(进 npm test,全量 121 条全绿),新增:
  · keybd_event 签名类型(且已做变异验证:改回 byte 判据立即转红)
  · 服务探测不只探 localhost / localhost 仅兜底 / 仅当有 homed 才拉起
  · 服务可达时 skip autostart / 探测有超时保护
2026-10-02 13:03:17 +08:00
40119e8116 feat(gui+waiter): computeruse 补高级操作、修 HiDPI 坐标;记录 PiDeck 借鉴
## computeruse 高级操作
此前两端(GUI 本机桥 / waiter 设备侧)都只有 9 个基础 action
(click/move/scroll/keypress/type…),与业界 computer-use 的基本盘差距明显。
现补到 18 个,两端对齐:

  drag          拖拽(分步移动,见下)
  hover         只悬停不点击(触发 tooltip / 悬浮菜单)
  mousedown/up  按下与释放分离,可配对做「按住」
  tripleclick   三击选中整行
  middleclick   中键
  hotkey/combo  组合键(ctrl+c 这类)
  wait/sleep    显式等待,等 UI/动画完成再下一步
  display       显示器几何(bounds + scaleFactor + primary),多屏定位必需

★ 拖拽**分步移动**(默认 12 步,上限 60)而不是一步到位:canvas 拖拽、
列表排序、滑动条这类应用依赖中间 mousemove 事件,一步到位会失效。

## 修 HiDPI 坐标错位(GUI 侧)
截图给模型的是**物理像素**,而 SetCursorPos / xdotool / cliclick 期望
**逻辑点(DIP)**。旧代码只加了 display bounds 的原点偏移,完全没换算
scaleFactor ⇒ 150% 缩放屏上点击系统性偏移(越靠右下偏得越多)。

现在按目标屏 scaleFactor 换算,并提供 `physical:false` 让调用方声明
「坐标已是逻辑点」以免二次换算。

## 组合键归一化(waiter 侧)
旧实现把模型给的 key 字符串原样丢给 `xdotool key`,于是 "Ctrl+C"、
"ctrl + c"、"CTRL-C" 全部失效。新增 normKeySpec:统一 + 与空格分隔、
统一小写、去重复修饰键。

顺带修 waiter 的 `type`:加 `--clearmodifiers`,避免残留的 Ctrl/Alt
把后续输入变成快捷键(旧实现一次输入就可能污染后续所有按键)。

## scroll 修正(两端)
旧实现固定滚 `click 4/5`(上/下),忽略方向语义也不支持横向。
现按 dy 正负给方向、按量级分步、支持 dx 横向滚动。

## 显式声明借鉴 PiDeck
用户要求声明 GUI 借鉴了 PiDeck 的设计与实现。做法:

1. 新增 `cmd/gui/DESIGN-NOTES.md` —— 逐项列出借鉴自 PiDeck 的设计
   (三点渐次明暗 / 运行态呼吸光晕 / 工具调用分组卡)、PiDeck 的原始
   实现(@keyframes 名与 CSS 片段)、HomeAgent 的落地位置、以及**动机**,
   并明确区分「本仓自行决策」的部分(如「只有 1 个工具时不分组」是本地决定,
   不是 PiDeck 做法)。
2. app.js / style.css 的相关代码处补出处注释。
3. 判据 computeruse.test.mjs 钉住这几条声明,防止后续丢失。

参考方式为阅读 PiDeck 打包产物 app.asar 中的 CSS 实现后**独立重写**,
未复制其代码或资源文件。

## 判据
新增 cmd/gui/computeruse.test.mjs(进 npm test),30 条断言:
  · 两端各 9 项高级操作
  · HiDPI 换算与 physical 开关
  · 组合键归一化、type 的 --clearmodifiers
  · 拖拽分步移动(两端)
  · PiDeck 借鉴声明四处(含 DESIGN-NOTES.md 存在)

判据:cmd/gui npm test 全绿(114 条)。
2026-10-02 11:10:53 +08:00