Files
SimpleRemoter/docs/Mcp_RemoteControl_Design.md
T
yuanyuanxianganddeepseek-v4-pro 84e3565de1 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
2026-08-24 21:51:12 +02:00

35 KiB
Raw Blame History

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:<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.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<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 构造(或抽取成共享 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。

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 闭环

  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)。

参考(行业最佳实践)