From 84e3565de1f1a32723a50e5a4979aeaebf4f4d03 Mon Sep 17 00:00:00 2001 From: yuanyuanxiang <962914132@qq.com> Date: Mon, 24 Aug 2026 21:51:12 +0200 Subject: [PATCH] doc: Add remote control MCP design doc Add docs/Mcp_RemoteControl_Design.md, the reference design for an screenshot-driven AI remote-control feature: the existing get_screenshot for observation plus remote_open/remote_close/remote_mouse/remote_keyboard for input injection, reusing COMMAND_SCREEN_PREVIEW_REQ and COMMAND_SCREEN_CONTROL + MSG64. Coordinates are normalized (0..1) and mapped server-side to physical pixels; injection runs over the screen sub-connection opened by WebService::StartRemoteDesktop's hidden CScreenSpyDlg. Includes a chat-on-behalf experiment case with clipboard-encoding and window-capture notes. Also fold in the get_audit_log encoding correction for docs/Mcp_Terminal_Design.md, so the two doc changes ship as one commit. Co-Authored-By: deepseek-v4-pro --- docs/Mcp_RemoteControl_Design.md | 484 +++++++++++++++++++++++++++++++ docs/Mcp_Terminal_Design.md | 2 +- 2 files changed, 485 insertions(+), 1 deletion(-) create mode 100644 docs/Mcp_RemoteControl_Design.md diff --git a/docs/Mcp_RemoteControl_Design.md b/docs/Mcp_RemoteControl_Design.md new file mode 100644 index 0000000..39dd1c0 --- /dev/null +++ b/docs/Mcp_RemoteControl_Design.md @@ -0,0 +1,484 @@ +# MCP 远程控制(Remote Control)设计文档 + +> 版本:v1(设计基线) 状态:待开发 关联:`docs/Mcp_Design.md`、`docs/Mcp_Terminal_Design.md` +> 定位:本文件是「AI 远程控制一台 Windows 客户端(像人一样点鼠标、敲键盘、看画面完成任务)」功能的**唯一参照设计**,后续开发一律以本文为准,改需求先改本文。 + +--- + +## 1. 背景与目标 + +### 1.1 问题 + +现有 MCP 工具只能做「文本/命令行」层面的操作(`exec_command` 一次性命令、`terminal_*` 持久终端、`list_files`/`get_screenshot` 等只读)。很多真实任务只能在 **GUI 上完成**:点按钮、填表单、拖拽窗口、操作只有图形界面的软件。 + +AI 无法「看懂」视频流。把 25fps 的远程画面直接丢给视觉模型是行不通的——每秒 25 张、每张数十万像素,海量且绝大部分帧对决策无用。 + +### 1.2 目标 + +提供一组 MCP 工具,让 AI 以**「截图 → 观察 → 决策 → 注入单个输入动作 → 等待 → 重复」**的闭环驱动远程桌面: + +- **Observe(看)**:按需取一帧解码后的位图(JPEG),按视觉模型能力缩放到合适尺寸。 +- **Act(动)**:注入鼠标/键盘事件,复用现有远程桌面的输入注入链路。 +- **Decide(想)**:由 AI/宿主(Claude Code / 任何 MCP 客户端)在工具返回结果之上编排循环——**YAMA 只提供原语,不提供黑盒 `perform_task`**(与终端设计同一哲学)。 + +### 1.3 关键设计决策(已确认) + +| # | 决策 | 理由 | +|---|---|---| +| D1 | **截图驱动,非视频流** | 每次决策一张图,3–8s/步;视觉模型天然面向静态图 | +| D2 | **原语 + AI 编排,非黑盒任务** | 可控、可审、可中断;AI 在循环外可插判断/确认 | +| D3 | **归一化坐标(0..1)** | 彻底规避「截图缩放 vs 注入物理像素」的比例错位(最佳实践第一大坑) | +| D4 | **复用现有注入链路** | `COMMAND_SCREEN_CONTROL` + `MSG64` + 客户端 `SendInput` 已成熟 | +| D5 | **Observe 复用 `get_screenshot`** | 主连接按需抓 JPEG,无需常驻子连接,最省 | +| D6 | **独立开关 + 全程审计** | 与终端同级:`McpRemoteControl=1 && McpReadonly=0`,默认关 | + +--- + +## 2. 行业最佳实践借鉴 + +本节是「科学设计」的依据,逐条落到下文对应章节。主要参考 Anthropic 官方 Computer Use / Browser Use 最佳实践与参考实现(见文末「参考」)。 + +| 最佳实践 | 本设计落地 | +|---|---| +| 视觉模型是「规划者+眼睛」,宿主是「手」;工具调用=动作,返回=截图 | §3 循环归属:YAMA=手,AI=规划者 | +| **客户端先自行缩图**,绝不让模型 API 静默缩放(否则坐标系统性偏移) | §5:客户端按 `max_width` 缩图后 JPEG 回传,模型见到的就是声明尺寸 | +| 声明 `display_width/height` 必须 == 实际发送的图尺寸 | §7:归一化坐标,服务器侧映射物理像素,不依赖声明尺寸 | +| 动作原语集合(move/click/drag/scroll/type/key/…) | §4:`remote_mouse`/`remote_keyboard` 的动作枚举对齐 | +| 文本指令放在截图**之前**(内容顺序) | §8/宿主侧建议:由 MCP 客户端保证,本文仅记录 | +| 缩图上限:长边 ~1568px、总像素 ~1.15MP(Claude 4.6 系);推荐 1280×720 | §5.3:`max_width` 默认建议 1280,可配 | +| 输入注入 vs 高危动作:不可逆操作应人工确认(human-in-the-loop) | §9.4:审计 + 宿主确认,YAMA 不做每动作确认(防延迟) | +| 提示注入是首要风险(模型处理不可信 UI 文本) | §9.5:全审计 + 只读关才可用 + 建议宿主隔离 | +| 长会话上下文管理(滚动缓冲/压缩) | §3:宿主职责,本文提示 | + +--- + +## 3. 总体架构 + +``` +┌──────────────────── 宿主(Claude Code / MCP 客户端)────────────────────┐ +│ 系统提示(含远程控制约定) + 任务文本 │ +│ ↓ │ +│ ┌──────────────────────── 智能体循环 ─────────────────────────┐ │ +│ │ get_screenshot(max_width) ──→ 视觉模型看图 ──→ 决策 ──→ │ │ +│ │ remote_mouse/remote_keyboard(归一化坐标/文本) ──→ 等待0.5–2s │ │ +│ │ 循环,直到任务完成或模型判定需用户介入 │ │ +│ └────────────────────────────────────────────────────────────┘ │ +└───────────────────────────────┬───────────────────────────────────────┘ + │ JSON-RPC over HTTP POST /mcp (Bearer) +┌───────────────────────────────▼───────────────────────────────────────┐ +│ YAMA 服务器(McpServer.cpp) │ +│ · get_screenshot:COMMAND_SCREEN_PREVIEW_REQ → 客户端缩图 JPEG │ +│ · remote_open:COMMAND_SCREEN_SPY → 建立屏幕子连接(控制会话) │ +│ · remote_mouse/remote_keyboard:归一化→物理像素 → MSG64 → │ +│ COMMAND_SCREEN_CONTROL(经屏幕子连接) │ +└───────────────────────────────┬───────────────────────────────────────┘ + │ 主连接(控制)+ 屏幕子连接(注入) +┌───────────────────────────────▼───────────────────────────────────────┐ +│ 客户端(ClientDll / ScreenManager / ScreenPreview) │ +│ · CaptureAndEncodePreview:抓主屏 + StretchBlt 缩图 + JPEG │ +│ · ScreenManager.ProcessCommand:解码 MSG64 → SendInput │ +└───────────────────────────────────────────────────────────────────────┘ +``` + +**核心事实(已核实,见 §13 源码索引)**: + +- **Observe** 走**主连接**、无子连接、无需控制模式:`COMMAND_SCREEN_PREVIEW_REQ`(247) → 客户端抓主屏缩图 JPEG → `TOKEN_SCREEN_PREVIEW_RSP`(248)。现有 `get_screenshot` 已完整实现。 +- **Act(注入)必须走屏幕子连接**:`COMMAND_SCREEN_CONTROL`(20) 只能经 `COMMAND_SCREEN_SPY`(16) 建立的子连接发送(`hub.go` 注释:input events MUST go through the sub-connection)。故需要一个 `remote_open` 建立/持有该子连接。 +- 服务器端同时维护一张**原始 BGRA 32bpp DIB**(`m_BitmapData_Full`),但 MCP 的 `get_screenshot` **不使用**它——它拿的是客户端已编码的 JPEG。两者坐标空间不同,见 §7。 + +--- + +## 4. 工具集设计 + +### 4.1 工具清单 + +| 工具 | 作用 | 入参(required 标注) | 出参 | +|---|---|---|---| +| `get_screenshot`(**已有,复用**) | Observe | `id`;可选 `max_width`(64..1920) | JPEG 图片(见 §5) | +| `remote_open` | 建立控制会话(屏幕子连接) | `id`;可选 `timeout_ms` | `session_id`(32hex)、`screen_w`/`screen_h`(物理虚拟桌面像素) | +| `remote_close` | 关闭会话 | `id`、`session_id` | `closed`(bool) | +| `remote_mouse` | 注入鼠标 | `id`、`session_id`、`action`、`x`、`y`(+ 按 action 需 `x2`/`y2`/`button`/`delta`/`clicks`) | `{}`(成功即空对象) | +| `remote_keyboard` | 注入键盘 | `id`、`session_id`、`action`(+ 按 action 需 `text`/`key`/`modifiers`) | `{}` | + +> 命名对齐现有 `terminal_*`(open/exec/close),Observe 沿用 `get_screenshot` 避免重复造轮子。鼠标/键盘各一个工具、用 `action` 枚举区分具体动作——既对齐 Anthropic 的 `computer` 工具动作集,又贴合现有 `WebService::HandleMouse/HandleKey` 的「鼠标/键盘分家」实现。 + +### 4.2 `remote_mouse` 动作枚举 + +| action | 参数 | 语义 | +|---|---|---| +| `move` | `x`,`y` | 移动光标(不按键) | +| `down` | `x`,`y`,`button`(left/middle/right) | 按下 | +| `up` | `x`,`y`,`button` | 抬起 | +| `click` | `x`,`y`,`button`(默认 left)、`clicks`(1/2/3,默认 1) | 单击/双击/三击 | +| `right_click` | `x`,`y` | 右键(`click` 的便捷别名) | +| `middle_click` | `x`,`y` | 中键 | +| `drag` | `x`,`y`,`x2`,`y2`,`button`(默认 left) | 从 (x,y) 拖到 (x2,y2) | +| `scroll` | `x`,`y`,`delta`(+/- 滚动量)、`button`(可选,垂直默认) | 滚轮 | + +### 4.3 `remote_keyboard` 动作枚举 + +| action | 参数 | 语义 | +|---|---|---| +| `type` | `text` | 输入一串文本(字符级映射,非粘贴) | +| `key_down` | `key`、`modifiers`(可选) | 按下组合键(如 `Ctrl`+`C`) | +| `key_up` | `key`、`modifiers`(可选) | 抬起 | +| `key_press` | `key`、`modifiers`(可选) | 按下并抬起(最常用) | + +`key` 取值对齐 Windows 虚拟键名(`VK_*` 去掉前缀,如 `ENTER`/`TAB`/`F5`/`LEFT`)与常见修饰键 `CTRL`/`ALT`/`SHIFT`/`WIN`。`type` 文本中的字符经客户端 `VkKeyScan`/Unicode 注入映射为键盘事件(非剪贴板粘贴,保证焦点内输入)。 + +### 4.4 可选/扩展工具(`remote_clipboard`、`remote_focus_window` 为 §17 代聊实验所需,建议随阶段一落地) + +- `remote_clipboard`:写远程剪贴板(UTF-8→Unicode),非 ASCII 文本输入的关键路径;复用 `COMMAND_C2C_TEXT`(90) 的 UTF-8 逻辑或新增 UTF-8 设剪贴板命令(§6.4)。 +- `remote_focus_window`:`COMMAND_SCREEN_WINDOW`(153) / 客户端 `SetForegroundWindow`,把焦点给到指定窗口(配合 `list_windows` 的 `hwnd`);代聊第一步。 +- `remote_cursor_position`:读当前光标(`TOKEN_NEXTSCREEN` 帧头已带 `POINT cursor`)。 +- `remote_screenshot`(会话内/窗口抓屏):从子连接 DIB 服务端缩图,或按窗口抓取(`COMMAND_SCREEN_WINDOW`),供高分辨率读聊天文字与多显示器场景(§12 阶段二、§17)。 + +--- + +## 5. 屏幕捕获与编码(Observe) + +### 5.1 现有链路(复用) + +`get_screenshot`(`McpServer.cpp` `BuildGetScreenshot`,~1597)已实现: + +1. `ParseHostIdArg` → `FindMainContext` → 能力门 `ctx->SupportsScreenPreview()`(`CLIENT_CAP_SCREEN_PREVIEW`,不支持 → `-32005`)。 +2. `BeginPending(devId,"get_screenshot")` 设备级互斥(忙 → `-32003`)。 +3. `reqId = NextPreviewReqId()`;`SetPendingReqId(devId, reqId)`(stale-drop 机制)。 +4. `parent->ChooseScreenPreviewParams(...)` 按 RTT/FRP 自适应缩图档位 + `max_width` 覆盖(clamp 64..1920)。 +5. `SendScreenPreviewRequest` → `COMMAND_SCREEN_PREVIEW_REQ`。 +6. `WaitPending` 收 `[ScreenPreviewRspHeader][JPEG]`,校验 `status==OK && format==JPEG`。 +7. base64 JPEG 返回:`structuredContent.image = {mimeType:"image/jpeg", width, height, bytes}`,`content=[{type:"image", data:, mimeType:"image/jpeg"}]`。 + +客户端 `CaptureAndEncodePreview`(`client/ScreenPreview.cpp:107`)抓**主屏** + `StretchBlt`(HALFTONE) 缩图 + GDI+ `Bitmap::Save` JPEG——**位图永不离开客户端,只回传 JPEG**。 + +### 5.2 缩放策略(对齐最佳实践 D5) + +- 缩图在**客户端**完成(`max_width` 长边约束),服务器/MCP 宿主**不再二次缩放**,模型看到的就是声明尺寸——避免「模型 API 静默缩放导致坐标漂移」这个头号坑。 +- `max_width` 由宿主按视觉模型能力显式传入;YAMA 提供**默认建议 1280**(长边),上限维持 1920。 + +### 5.3 推荐参数 + +| 项 | 建议值 | 说明 | +|---|---|---| +| `max_width`(长边) | 1280(默认)/ 1568(上限) | 1280×720 ≈ 80% 像素预算、训练常见分辨率 | +| 编码 | JPEG,quality ~70–85 | 客户端 `jpegQuality` 已有,按 `ChooseScreenPreviewParams` 自适应 | +| 返回 | base64 `image/jpeg` | 已实现 | + +--- + +## 6. 输入注入(Act) + +### 6.1 协议(复用,已核实) + +- **包格式**:`[cmd:1][MSG64:48]`,可批量重复(`commands.h` `COMMAND_SCREEN_CONTROL=20`,`MSG64` 1581–1623)。 +- **MSG64 布局**:`{uint64 hwnd, message, wParam, lParam, time; POINT pt;}` = 48 字节(`_WIN64` 下 `MYMSG` 即 `MSG`;客户端亦接受 28 字节 `MSG32`,由 `ulLength % 28/48` 判定)。 +- **客户端解码**:`ScreenManager.cpp:1175` → `ProcessCommand(1727)` → `SendInput`: + - 鼠标:坐标取 `LOWORD/HIWORD(lParam)`,`WM_MOUSEMOVE`→`MOUSEEVENTF_MOVE`,`WM_LBUTTONDOWN/UP`→`LEFTDOWN/LEFTUP`,`WM_LBUTTONDBLCLK`→额外 `LEFTDOWN`,`WM_MBUTTONDOWN/UP`,`WM_MOUSEWHEEL`→`MOUSEEVENTF_WHEEL`(`mouseData=GET_WHEEL_DELTA_WPARAM`)。 + - 键盘:`wVk=(WORD)wParam`,`wScan=(lParam>>16)&0xFF`,`KEYEVENTF_EXTENDEDKEY` 由 `(lParam>>24)&1`。 + +### 6.2 服务端可复用实现 + +`WebService::HandleMouse`(`WebService.cpp:773-865`)与 `HandleKey`(`867-952`)已从 JSON(`type/x/y/button/delta`;`keyCode/down/altKey`)构造 `MSG64` 并 `[COMMAND_SCREEN_CONTROL][MSG64]` 发送。其中 `HandleKey` 已正确处理 `lParam`:repeat=1、`MapVirtualKey` 扫瞄码、扩展键表、Alt 上下文位 29、keyup 位 30–31。 + +**结论**:`remote_mouse`/`remote_keyboard` 是这两个函数的 MCP 薄封装——先归一化→物理像素,再套用其 `MSG64` 构造逻辑。**不要新写注入协议**。 + +### 6.3 子连接前置条件(关键约束) + +注入**必须**经屏幕子连接: + +1. 主连接发 `COMMAND_SCREEN_SPY`(16)(载荷 `{cmd, USING_DXGI|2, ALGORITHM_*, MultiScreen}`,见 `2015RemoteDlg.cpp:8443-8470`)→ 客户端开子连接(`ClientDll.cpp:89-94`)。 +2. 客户端回 `TOKEN_BITMAPINFO`(含物理 `BITMAPINFOHEADER`:`biWidth/biHeight`)——**这是归一化坐标映射所需的物理分辨率来源**。 +3. 服务器发 `COMMAND_NEXT`(30) 开始流/允许控制。 +4. `GetScreenContext(device_id)`(`WebService.cpp:2084`)从 `m_ScreenContexts` 取该子连接上下文。 + +> 实现提示:上述 1–4 的建立/持有逻辑**耦合在 `CScreenSpyDlg`(隐藏对话框)里**——子连接、帧 DIB、`TOKEN_BITMAPINFO` 处理都由它完成。MCP 的 `remote_open` 应复用 `WebService::StartRemoteDesktop`(1725) 的「隐藏对话框」路径,而非另造无对话框持有者(§13.2)。 + +`remote_open` 即上述 1–4 的封装;`screen_w/screen_h` 取自 `TOKEN_BITMAPINFO`。 + +### 6.4 文本输入:ASCII 直注 vs 非 ASCII 走剪贴板 + +`remote_keyboard` 的 `type` 走物理键事件(`wVk`/`wScan` → `SendInput`),只能可靠注入 ASCII/ANSI 字符——**中文等非 ASCII 文本无法靠模拟键盘键入**(需 IME 组合,键事件层面做不到)。 + +非 ASCII 文本的可靠路径是**剪贴板 + 粘贴**: + +- 服务器设远程剪贴板命令 `COMMAND_SCREEN_SET_CLIPBOARD`(25),包 `[cmd:1][text:N]`,经屏幕子连接发送;客户端 `UpdateClientClipboard`(`ScreenManager.cpp:1223/1599`)落到剪贴板。 +- 随后 `remote_keyboard` 注入 `Ctrl+V` 粘贴、`Enter` 发送。 +- **编码注意(已核实)**:`UpdateClientClipboard` 用 `SetClipboardData(CF_TEXT,...)`——ANSI/GBK 格式(`CF_TEXT`,非 `CF_UNICODETEXT`)。MVP 下中文可经 `ToAnsi(utf8, 936)` 转 GBK 后直发(与终端命令同模式);但 emoji/非 GBK 字符会丢、部分新式聊天软件粘贴偏好 Unicode。更稳做法是复用 `COMMAND_C2C_TEXT`(90) 的 UTF-8→Unicode→`CF_UNICODETEXT` 逻辑(`KernelManager.cpp:1573-1587`,且已内置「设完剪贴板后自动模拟 Ctrl+V」),或新增一个 UTF-8 设剪贴板变体。见 §17 实验案例。 + +--- + +## 7. 坐标约定与映射 + +### 7.1 归一化坐标(D3) + +- AI 在**所有鼠标动作**里输出 **0..1 浮点**:`x = 目标横向比例,y = 目标纵向比例`,相对**它看到的那张截图**。 +- 服务器侧映射:`phys_x = round(norm_x * screen_w)`、`phys_y = round(norm_y * screen_h)`,其中 `screen_w/h` 是 `remote_open` 时从 `TOKEN_BITMAPINFO` 得到的**物理捕获分辨率**。 +- 好处:无论 `max_width` 怎么缩、显示器多大,AI 永远只说「在图的 40%/60% 处点一下」,比例错位风险为零。 + +### 7.2 坐标空间一致性(最重要正确性点) + +Observe(`get_screenshot` 抓**主屏**)与 Act(`SendInput` 绝对坐标在**虚拟桌面**空间)默认不同源。**阶段一只支持单显示器**:主屏 == 虚拟桌面,两者重合,映射严格正确。 + +多显示器(`MultiScreen`)在**阶段二**处理:届时 Observe 必须改抓**整个虚拟桌面**(而非主屏),`screen_w/h` 用虚拟桌面尺寸,归一化坐标才继续成立。**在阶段二落地前,`remote_open` 对多显示器配置应返回显式错误或降级为主屏**(见 §12)。 + +### 7.3 边界与钳制 + +- 归一化值越界(`<0` 或 `>1`)→ 钳制到 `[0,1]` 或返回 `-32602`(建议钳制,模型偶发 `1.0001`)。 +- `phys_x/y` 不得越过 `screen_w/h`,注入前 `clamp(0, screen_w-1)`。 + +--- + +## 8. 控制会话状态机 + +### 8.1 生命周期 + +``` +remote_open ──► 建立子连接 ──► [remote_mouse/remote_keyboard × N + get_screenshot × M] + │ │ + └──(busy 互斥 / idle 回收 / 断线清理)──────────┴──► remote_close(幂等) +``` + +### 8.2 状态与不变量(镜像 `docs/Mcp_Terminal_Design.md` §并发) + +- **单设备单控制会话**:`remote_open` 对已存在会话 → `-32003`(忙)。 +- **互斥**:同一设备上,MCP 远程控制会话与**人类远程桌面观看**(`m_ScreenContexts` 已被占用)互斥——`remote_open` 发现已有屏幕子连接 → `-32003`,避免 AI 向人类正在观看/控制的画面注入。`remote_mouse/keyboard` 要求 `session_id` 匹配且会话未关闭。 +- **busy 标志**:一条注入在飞时 `busy=true`,保证等待线程是唯一擦除者(沿用终端的 `m_TermMutex`+`busy` 模式;远程控制建议独立 `m_ScreenCtrlMutex`/`m_ScreenCtrlCv`)。 +- **idle 回收**:会话闲置超时(默认 300s)自动关闭并 `CancelIO`(镜像 `SweepIdleTerminals`)。 +- **断线清理**:`OfflineProc`/`OnTerminalClosed` 同位置加 `McpServer().OnScreenControlClosed(subCtx)`,防悬空 `subCtx` 被连接池复用(复刻终端设计里发现的第 2 个修复点)。 +- **锁外 `CancelIO`**:所有取消动作在锁外执行(终端的 P2 教训)。 + +### 8.3 并发(P5 教训) + +`httplib` 多线程并发处理请求;`remote_mouse` 会高频调用。会话状态一律 `m_ScreenCtrlMutex` 串行化,**不要假定请求串行**。高频注入可加**节流**(沿用 `SendScaledMouseMessage` 的鼠标移动节流思路,`ScreenSpyDlg.cpp:2728-2743`)。 + +--- + +## 9. 安全模型 + +延续既有「可控、可关、可审、可隔离」原则(与终端同级): + +### 9.1 开关与门控 + +- 新增 `McpRemoteControl`(默认 **0=关**)。 +- `remote_*` 生效条件:`McpRemoteControl=1 && McpReadonly=0`,否则 `-32006`。 +- `get_screenshot` 是只读工具,不受此门控,仍仅受现有能力门(供纯观察场景)。 + +### 9.2 全程审计 + +每条 `remote_open/close/mouse/keyboard` 调用 → `PostMessageA(WM_SHOWERRORMSG, ...)` → `m_MessageLog` → `get_audit_log` 可查(复用 `McpServer.cpp:2050` 的审计模式,标题 `MCP远程控制`,标记「不可关闭」)。审计内容含:设备 id、动作、归一化坐标与换算后的物理坐标、session_id、时间戳。 + +### 9.3 会话级鉴权 + +- `session_id` 由 `GenerateRandomToken()`(32hex),每次 `remote_open` 独立随机,防串用(镜像终端 P6)。 +- 注入工具校验 `session_id` 归属与设备匹配,不匹配 → `-32002`。 + +### 9.4 不可逆操作与人工确认 + +- YAMA **不做逐动作确认**(会引入每步 0.5s+ 延迟,破坏闭环节奏)。 +- 高危动作(提交表单、删除、付款、改系统设置)的确认由**宿主/AI 编排层**负责(Claude Code 的权限系统 / 系统提示约定「不可逆操作前暂停询问用户」)。本文记录此分工;YAMA 提供审计留痕兜底。 + +### 9.5 提示注入(Prompt Injection) + +视觉模型会读到客户端屏幕上的**不可信 UI 文本**,存在注入风险。缓解:远程控制本身即高危能力(默认关 + 只读关才开 + 全审计);建议宿主将远程控制会话隔离在独立权限域,并对「屏幕文本里的指令」保持怀疑。YAMA 侧不做内容分类(无意义且加延迟),记录即可。 + +--- + +## 10. 错误码 + +沿用现有约定(`BuildError(id, code, msg)`): + +| code | 含义 | 触发 | +|---|---|---| +| -32602 | 参数非法 | action 未知、必填参数缺失、`max_width` 越界、归一化值格式错 | +| -32000 | 通用失败 | 未分类错误 | +| -32001 | 超时 | `remote_open` 等子连接/`get_screenshot` 等帧超时 | +| -32002 | 设备/会话不存在 | `id` 无在线主机、`session_id` 不存在或不匹配 | +| -32003 | 设备/会话忙 | 已有控制会话、人类远程桌面占用、`get_screenshot` 在飞 | +| -32004 | 发送失败 | 注入命令发送失败 | +| -32005 | 能力不支持 | 客户端无 `CLIENT_CAP_SCREEN_PREVIEW`(Observe)/ 不支持屏幕控制(Act) | +| -32006 | 被开关禁用 | `McpRemoteControl=0` 或 `McpReadonly=1` | +| -32008 | 非法/被拒参数 | 多显示器未支持时的 `remote_open` 降级拒绝(阶段一) | + +--- + +## 11. 配置项 + +新增 1 项(`THIS_CFG` `settings` 节,键名沿用蛇形): + +| 键 | 默认 | 说明 | +|---|---|---| +| `McpRemoteControl` | `0` | 远程控制开关;要求 `McpReadonly=0` 才生效 | + +其余沿用:`McpEnabled`/`McpPort`/`McpBind`/`McpToken`/`McpReadonly`/`McpTerminal`/`McpCmdWhitelist`。改动需重启生效(与终端一致)。 + +--- + +## 12. 已知限制与后续扩展 + +### 12.1 阶段一(本期) + +- **单显示器**:Observe 抓主屏,坐标空间与虚拟桌面重合;多显示器 `remote_open` 显式报错(`-32008`)。 +- 注入基于**绝对坐标** `SendInput`,要求客户端处于可注入的桌面会话(已登录、非锁屏/非 UAC 安全桌面);锁屏/UAC 提示场景注入无效(客户端 `SendInput` 限制),设计文档记录为已知限制。 +- **代聊实验使能项**:窗口抓屏(`COMMAND_SCREEN_WINDOW`)、UTF-8 剪贴板、窗口聚焦随阶段一落地(§17)。 + +### 12.2 阶段二(后续) + +1. **多显示器虚拟桌面捕获**:`get_screenshot` 增加虚拟桌面抓屏,或新增 `remote_screenshot` 从子连接 DIB 服务端缩图(复用 `BmpToJpeg`/`CacheThumbnail` GDI+ 路径),使 Observe/Act 同源。 +2. **窗口定向**:`remote_focus_window`(复用 `COMMAND_SCREEN_WINDOW` + 客户端 `SetForegroundWindow`)。 +3. **光标读取 / 剪贴板**。 +4. **服务端缩图**:从 `m_BitmapData_Full`(BGRA DIB)直接按 API 上限缩图编码,进一步省客户端往返。 + +--- + +## 13. 改动清单(供实现参照) + +> 行号以当前树为准(实现前请 grep 复核)。新增/改动尽量「复用」而非「重写」。 + +### 13.1 `server/2015Remote/McpServer.h` + +- 新增 `struct ScreenCtrlSession`(仿 `TermSession`):`sessionId`、`deviceId`、`busy`、`started`、`closed`、`lastActiveAt`、`screenW/screenH`(来自 `TOKEN_BITMAPINFO`)、`subCtx` 引用。 +- 新增方法:`SetRemoteControlEnabled/IsRemoteControlEnabled`、`BeginScreenCtrlOpen/…/CloseScreenCtrlSession/SweepIdleScreenCtrl`、`OnScreenControlClosed(context*)`、`ResolveScreenCtrlSessionHost`。 +- 新增成员:`bool m_remoteControlEnabled=false;` 及 `std::mutex m_ScreenCtrlMutex; std::condition_variable m_ScreenCtrlCv; std::map m_ScreenCtrlContextToDevice; std::map m_ScreenCtrlSessions;`(或复用 WebService 的 `m_ScreenContexts` 生命周期,见下)。 + +### 13.2 `server/2015Remote/McpServer.cpp` + +- 新增 4 个 schema 构建器 + 3 个 handler:`BuildRemoteOpen/Close/Mouse/Keyboard`(镜像 `BuildTerminalOpen` 等,`BuildToolsListResult` ~1278 后加条目,`BuildToolsCall` ~2124 加分派)。 +- `remote_open`:`SweepIdleScreenCtrl` → `ResolveScreenCtrlSessionHost` → 已有会话 `-32003` → `GenerateRandomToken` → **复用 Web 远程桌面的会话建立路径 `WebService::StartRemoteDesktop`(1725)**(发 `COMMAND_SCREEN_SPY` → 以 `SW_HIDE` 打开隐藏 `CScreenSpyDlg` 持有子连接 → 客户端子连接到达 → `RegisterScreenContext` 记入 `m_ScreenContexts`)→ 等 `TOKEN_BITMAPINFO` 取 `screenW/H` → `COMMAND_NEXT` → 审计 → 返回 `{session_id, screen_w, screen_h}`。**注意:屏幕子连接/帧 DIB/`TOKEN_BITMAPINFO` 处理都与 `CScreenSpyDlg` 耦合,不要另造无对话框持有者——沿 Web 的隐藏对话框路径是低风险正解。** +- `remote_mouse/keyboard`:`ResolveScreenCtrlSessionHost` → 校验 `session_id`/`busy` → 归一化→物理像素(`round(norm * screenW/H)` + clamp)→ **复用 `WebService::HandleMouse/HandleKey` 的 `MSG64` 构造**(或抽取成共享 helper `BuildMouseMsg64/BuildKeyMsg64`,避免 MCP 与 Web 两处重复)→ `subCtx->Send2Client([COMMAND_SCREEN_CONTROL][MSG64])` → 审计。 +- `remote_close`:幂等 `CloseScreenCtrlSession`(不存在 → `{closed:true}`;sid 不匹配 → `-32002`)→ 锁外 `CancelIO` → 审计。 +- `OnScreenControlClosed`:镜像终端修复点——空闲会话直接擦路由+会话,busy 会话唤醒等待线程由其清理。 +- `SweepIdleScreenCtrl`:镜像 `SweepIdleTerminals`(`difftime` 防时钟回拨)。 + +### 13.3 `server/2015Remote/McpSettingsDlg.h/.cpp` + +- 新增复选框 `IDC_MCP_REMOTECONTROL = 1008`,文案 `_TR("启用远程控制(AI 操控桌面)")`;回填/落盘 `McpRemoteControl`;对话框高度 +30。 + +### 13.4 `server/2015Remote/2015RemoteDlg.cpp` + +- 启动读配置处加 `McpServer().SetRemoteControlEnabled(THIS_CFG.GetInt("settings","McpRemoteControl",0)!=0);`。 +- `OfflineProc` 加 `if (McpServer().IsRunning() && McpServer().IsScreenCtrlContext(ContextObject)) McpServer().OnScreenControlClosed(ContextObject);`(复刻终端修复点)。 + +### 13.5 语言文件 + +- 沿用终端约定:MCP 相关文案本就在 `lang/*.ini` 无条目(`_TR` 对未命中键原样返回中文)。若日后翻译,用 **GBK 工具**改 `lang/{en_US,zh_TW}.ini`(ANSI/GBK,**不能用 Write/Edit 直接写**)。 + +--- + +## 14. 关键点(实现时务必遵守) + +- **P1(坐标一致性,最重要)**:归一化坐标映射所用 `screenW/H` 必须与**当前截图来源的分辨率**一致。阶段一单屏下 `TOKEN_BITMAPINFO` 的主屏尺寸 == 截图主屏物理尺寸,才成立;任何「用虚拟桌面尺寸映射主屏截图」都会造成系统性偏移(最佳实践第一大坑)。 +- **P2(注入走子连接)**:`COMMAND_SCREEN_CONTROL` **必须**经 `GetScreenContext` 取得的子连接发送,主连接无效。 +- **P3(锁外 CancelIO)**:所有 `CancelIO` 在 `m_ScreenCtrlMutex` 之外(终端 P2 教训)。 +- **P4(复用勿重写)**:`MSG64` 构造逻辑复用 `WebService::HandleMouse/HandleKey`;理想做法抽共享 helper,否则未来两处漂移。 +- **P5(并发串行化)**:`remote_mouse` 高频并发,状态全部走锁;勿假定请求串行(终端 P5 教训)。 +- **P6(session_id 独立随机 + 互斥)**:会话 token 用 `GenerateRandomToken`;单设备单会话;与人类远程桌面观看互斥(`-32003`)。 +- **P7(非黑盒)**:不实现 `perform_task`;循环归宿主/AI,YAMA 只出原语(D2)。 + +--- + +## 15. 验证(端到端,127.0.0.1:6544,Bearer `a2176213eddcf0acfdb285940ec4eb2a`) + +前置:设置开 `McpEnabled=1`、`McpReadonly=0`、`McpRemoteControl=1`;目标 Windows 客户端在线、已登录桌面。`id` 取 `list_online_hosts` 的十进制主机 id。 + +```bash +B='Authorization: Bearer a2176213eddcf0acfdb285940ec4eb2a' +U=http://127.0.0.1:6544/mcp + +# 1) tools/list 应出现 remote_open/close/mouse/keyboard 四个工具 +curl -s $U -H "$B" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' + +# 2) 打开会话 → session_id + screen_w/h +curl -s $U -H "$B" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"remote_open","arguments":{"id":""}}}' + +# 3) Observe:截图(复用) +curl -s $U -H "$B" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_screenshot","arguments":{"id":"","max_width":1280}}}' + +# 4) Act:移动 + 单击(归一化坐标,0.5,0.5 = 屏幕中心) +curl -s $U -H "$B" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"remote_mouse","arguments":{"id":"","session_id":"","action":"move","x":0.5,"y":0.5}}}' +curl -s $U -H "$B" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"remote_mouse","arguments":{"id":"","session_id":"","action":"click","x":0.5,"y":0.5,"button":"left"}}}' + +# 5) 键盘:按下并抬起 Ctrl+Esc 或输入文本 +curl -s $U -H "$B" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"remote_keyboard","arguments":{"id":"","session_id":"","action":"key_press","key":"WIN"}}}' + +# 6) 负例:未开会话/错误 sid → -32002;已开会话再 open → -32003;只读/开关关 → -32006 +# 7) 关闭 + 幂等关闭 +curl -s $U -H "$B" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":9,"method":"tools/call","params":{"name":"remote_close","arguments":{"id":"","session_id":""}}}' + +# 8) 审计链 +curl -s $U -H "$B" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":12,"method":"tools/call","params":{"name":"get_audit_log","arguments":{}}}' +``` + +人工核对:注入后目标机光标/焦点确实变化;审计列表出现标题「MCP远程控制」的 open/close/mouse/keyboard 条目;闲置 >300s 后旧 `session_id` 调用 → `-32002`。编译由你以 VS2019(MSBuild v143)完成;我侧仅代码审查(工具集 v142,无法在此构建)。 + +--- + +## 16. 涉及文件 + +- `server/2015Remote/McpServer.h` / `.cpp`(会话状态机 + schema + handler + 分派) +- `server/2015Remote/McpSettingsDlg.h` / `.cpp`(新开关) +- `server/2015Remote/2015RemoteDlg.cpp`(启动读配置 + `OfflineProc` 清理钩子) +- `common/commands.h`(**只读参照**:`COMMAND_SCREEN_SPY`/`COMMAND_SCREEN_CONTROL`/`MSG64`/`COMMAND_SCREEN_PREVIEW_REQ` 等,无需改) +- `server/2015Remote/WebService.cpp`(**复用参照**:`HandleMouse`/`HandleKey`/`GetScreenContext`) +- `lang/{en_US,zh_TW}.ini`(可选,GBK 工具改) + +--- + +## 17. 实验案例:AI 代聊(chat-on-behalf) + +> 目标:以「AI 通过远程桌面,在聊天软件里代表你与朋友聊天」为第一个实验用例,验证整条「看 → 想 → 动 → 确认」链路。低风险、目标清晰、信号明确(有新消息 / 已发出)。 + +### 17.1 闭环 + +1. `list_windows` 找到聊天窗口(`hwnd`/`title`)。 +2. `remote_focus_window`(或 `remote_mouse` 点击输入框)聚焦聊天窗。 +3. 抓**聊天窗口**高分辨率截图 → 视觉模型读出朋友最新消息。 +4. 模型生成回复。 +5. `remote_clipboard` 把回复写入远程剪贴板(UTF-8)。 +6. `remote_keyboard` 注入 `Ctrl+V`(粘贴)+ `Enter`(发送)。 +7. 再截图确认已发出,等待下一条。 + +### 17.2 两个已验证的关键依赖 + +- **中文输入(走剪贴板,已解决)**:见 §6.4。`COMMAND_SCREEN_SET_CLIPBOARD`(25) 为 ANSI/GBK(`CF_TEXT`),中文可经 `ToAnsi(utf8, 936)` 直发跑通 MVP;要支持 emoji/完整 Unicode,复用 `COMMAND_C2C_TEXT`(90) 的 UTF-8→`CF_UNICODETEXT` 逻辑或新增 UTF-8 变体。客户端 `KernelManager.cpp:1590` 已内置「设剪贴板后自动模拟 Ctrl+V」。 +- **读消息(需窗口抓屏)**:`get_screenshot` 抓整屏,缩到 1280 后聊天文字过小、OCR 易错。用 `COMMAND_SCREEN_WINDOW`(153) 按窗口抓取聊天窗(更高有效分辨率),或对该区域用更高 `max_width`。 + +### 17.3 使能清单(相对现有代码的增量) + +| 能力 | 状态 | 复用点 | +|---|---|---| +| `get_screenshot` / `list_windows` | ✅ 已有 | — | +| 剪贴板写入 | ⚠️ 客户端已有(GBK 直发可跑通),缺 MCP 封装 | `COMMAND_SCREEN_SET_CLIPBOARD`(25) / `COMMAND_C2C_TEXT`(90) | +| `remote_open` / `remote_close`(屏幕子连接) | ❌ 待建 | `COMMAND_SCREEN_SPY`(16) 建立逻辑 `2015RemoteDlg.cpp:8443` | +| `remote_mouse` / `remote_keyboard` | ❌ 待建 | `WebService::HandleMouse/HandleKey` | +| `remote_focus_window` | ❌ 待建 | `COMMAND_SCREEN_WINDOW`(153) | +| `remote_clipboard` | ❌ 待建 | 见 §6.4 | + +### 17.4 最小 MVP 实施顺序(按依赖) + +1. `remote_open` / `remote_close`(屏幕子连接 + `TOKEN_BITMAPINFO` 取分辨率)。 +2. `remote_keyboard`(先只做 `key_press`/`type` + 修饰键,够发 `Ctrl+V`/`Enter`)。 +3. `remote_mouse`(`click`/`move`,用于点输入框;聚焦可用点击替代)。 +4. `remote_clipboard`(先 GBK 直发跑通,再上 UTF-8 变体)。 +5. `remote_focus_window`(或用步骤 3 的点击替代,二选一即可跑通)。 + +> 1→2→3 已足以发出一条消息(点输入框 + 粘贴 + 回车);4 是中文关键路径;5 是体验优化,可后置。 + +### 17.5 实验注意事项 + +- 目标机必须**已登录且未锁屏**(锁屏/UAC 安全桌面下抓屏与注入均失效,§12.1)。 +- 「代聊」会让朋友误以为是真人在聊——实验前建议告知对方;避免让 AI 在承诺/隐私等话术上自由发挥(§9.4 的人工确认原则同样适用)。 +- 新消息检测先用「轮询截图」即可(MVP 接受),后续用未读角标/窗口通知优化(§17.6)。 + +### 17.6 后续优化(非 MVP) + +- 新消息触发:从「定时截图」改为「未读角标/窗口通知驱动」,大幅省 token。 +- 窗口定向抓屏(`COMMAND_SCREEN_WINDOW`)并入 `remote_screenshot`。 +- 会话历史:AI 保留最近几轮聊天上下文,避免回复脱节(宿主侧滚动缓冲,§3)。 + +--- + +## 参考(行业最佳实践) + +- Anthropic《Best practices for computer and browser use with Claude》— https://claude.com/fr/blog/best-practices-for-computer-and-browser-use-with-claude +- Anthropic 参考实现 `computer.py`(动作枚举、缩放、坐标换算)— https://github.com/anthropics/claude-quickstarts/blob/main/computer-use-demo/computer_use_demo/tools/computer.py +- 《How Claude Computer Use Works: Architecture Internals》— https://callsphere.ai/blog/how-claude-computer-use-works-architecture-internals diff --git a/docs/Mcp_Terminal_Design.md b/docs/Mcp_Terminal_Design.md index e73c744..c6be08e 100644 --- a/docs/Mcp_Terminal_Design.md +++ b/docs/Mcp_Terminal_Design.md @@ -451,7 +451,7 @@ McpServer().SetTerminalEnabled(THIS_CFG.GetInt("settings", "McpTerminal", 0) != ### 10.6 中文/乱码 - 输出乱码:检查 `isPty` 与 `cp` 是否匹配(PTY=UTF-8,老管道=GBK)。 -- **`get_audit_log` 中文乱码**:已知问题(GBK 写入 UTF-8 JSON),影响**所有**审计条目,非终端特有,暂未修复。 +- **`get_audit_log` 中文正确**:`BuildGetAuditLog` 用 `ToUtf8(type/time/msg, 936)` 把 GBK 转 UTF-8,jsoncpp 输出 `\uXXXX` 转义,客户端解析后中文正常(已实测 `MCP持久终端`/`MCP命令执行`/`操作成功`/`主机上线` 等均无乱码)。 ### 10.7 会话泄漏 / 子链接不关