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
35 KiB
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)已实现:
ParseHostIdArg→FindMainContext→ 能力门ctx->SupportsScreenPreview()(CLIENT_CAP_SCREEN_PREVIEW,不支持 →-32005)。BeginPending(devId,"get_screenshot")设备级互斥(忙 →-32003)。reqId = NextPreviewReqId();SetPendingReqId(devId, reqId)(stale-drop 机制)。parent->ChooseScreenPreviewParams(...)按 RTT/FRP 自适应缩图档位 +max_width覆盖(clamp 64..1920)。SendScreenPreviewRequest→COMMAND_SCREEN_PREVIEW_REQ。WaitPending收[ScreenPreviewRspHeader][JPEG],校验status==OK && format==JPEG。- base64 JPEG 返回:
structuredContent.image = {mimeType:"image/jpeg", width, height, bytes},content=[{type:"image", data:<base64>, 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.hCOMMAND_SCREEN_CONTROL=20,MSG641581–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 子连接前置条件(关键约束)
注入必须经屏幕子连接:
- 主连接发
COMMAND_SCREEN_SPY(16)(载荷{cmd, USING_DXGI|2, ALGORITHM_*, MultiScreen},见2015RemoteDlg.cpp:8443-8470)→ 客户端开子连接(ClientDll.cpp:89-94)。 - 客户端回
TOKEN_BITMAPINFO(含物理BITMAPINFOHEADER:biWidth/biHeight)——这是归一化坐标映射所需的物理分辨率来源。 - 服务器发
COMMAND_NEXT(30) 开始流/允许控制。 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 阶段二(后续)
- 多显示器虚拟桌面捕获:
get_screenshot增加虚拟桌面抓屏,或新增remote_screenshot从子连接 DIB 服务端缩图(复用BmpToJpeg/CacheThumbnailGDI+ 路径),使 Observe/Act 同源。 - 窗口定向:
remote_focus_window(复用COMMAND_SCREEN_WINDOW+ 客户端SetForegroundWindow)。 - 光标读取 / 剪贴板。
- 服务端缩图:从
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<context*,uint64_t> m_ScreenCtrlContextToDevice; std::map<uint64_t,ScreenCtrlSession> 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构造(或抽取成共享 helperBuildMouseMsg64/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。
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":"<HOSTID>"}}}'
# 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":"<HOSTID>","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":"<HOSTID>","session_id":"<SID>","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":"<HOSTID>","session_id":"<SID>","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":"<HOSTID>","session_id":"<SID>","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":"<HOSTID>","session_id":"<SID>"}}}'
# 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 闭环
list_windows找到聊天窗口(hwnd/title)。remote_focus_window(或remote_mouse点击输入框)聚焦聊天窗。- 抓聊天窗口高分辨率截图 → 视觉模型读出朋友最新消息。
- 模型生成回复。
remote_clipboard把回复写入远程剪贴板(UTF-8)。remote_keyboard注入Ctrl+V(粘贴)+Enter(发送)。- 再截图确认已发出,等待下一条。
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 实施顺序(按依赖)
remote_open/remote_close(屏幕子连接 +TOKEN_BITMAPINFO取分辨率)。remote_keyboard(先只做key_press/type+ 修饰键,够发Ctrl+V/Enter)。remote_mouse(click/move,用于点输入框;聚焦可用点击替代)。remote_clipboard(先 GBK 直发跑通,再上 UTF-8 变体)。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