Feature: Add list_files and get_screenshot MCP tools
Add the two P2c MCP tools on top of the P2b protocol. list_files lists drives (COMMAND_LIST_DRIVE -> TOKEN_DRIVE_LIST) or a directory (follow-up COMMAND_LIST_FILES -> TOKEN_FILE_LIST on the same one-shot sub-link); get_screenshot reuses the screen-preview RPC with a per-host reqId correlation so stale or MFC-preview responses fall through to the MFC path untouched. Both share the P2b single-flight pending registry. Fix file/process name encoding: the client reads process names, paths and file names via the ANSI (A) APIs, so they are GBK on Windows regardless of the CLIENT_CAP_UTF8 capability bit (which governs window titles only). Decode by clientType (LNX/MAC = UTF-8, Windows = 936) rather than GetClientEncoding, and convert the list_files path to the client ANSI code page before sending. Sync Mcp_Phase2_Design.md with the P2c design and the encoding correction. Co-Authored-By: deepseek-v4-pro
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# YAMA MCP 功能开发技术书(Phase 2 及后续)
|
# YAMA MCP 功能开发技术书(Phase 2 及后续)
|
||||||
|
|
||||||
> **状态**:设计定稿(已按两轮评审修订);P2a、P2b 已实现并经真实主机验证(`search_hosts` / `get_host_detail` / `list_processes` / `list_windows` / `get_activity_history`)。
|
> **状态**:设计定稿(已按两轮评审修订);P2a、P2b 已实现并经真实主机验证(`search_hosts` / `get_host_detail` / `list_processes` / `list_windows` / `get_activity_history`);P2c 已实现、待实机验证(`get_screenshot` / `list_files`)。
|
||||||
> **读者**:MCP 后续功能的研发/评审人员。
|
> **读者**:MCP 后续功能的研发/评审人员。
|
||||||
> **关联文档**:[Mcp_Design.md](./Mcp_Design.md)(Phase 1 的协议、架构、配置与菜单设计)。
|
> **关联文档**:[Mcp_Design.md](./Mcp_Design.md)(Phase 1 的协议、架构、配置与菜单设计)。
|
||||||
> **核心目标**:让 MCP 从「单个只读工具」平滑演进为「分阶段、可回滚、影响面可控」的工具集,**不一次性大改现有功能**。
|
> **核心目标**:让 MCP 从「单个只读工具」平滑演进为「分阶段、可回滚、影响面可控」的工具集,**不一次性大改现有功能**。
|
||||||
@@ -112,9 +112,11 @@ MCP 工具 tools/call
|
|||||||
- **有子链接、有 subCtx**:数据落在子 context 上,非主 context。子 context 的 `GetClientID()` 已由 `TOKEN_CONN_AUTH`(`2015RemoteDlg.cpp:6291` 的 `SetID`)钉成主连接 clientID,故 `IsPending(devId)` 能与主 context 的 `device_id` 对上。
|
- **有子链接、有 subCtx**:数据落在子 context 上,非主 context。子 context 的 `GetClientID()` 已由 `TOKEN_CONN_AUTH`(`2015RemoteDlg.cpp:6291` 的 `SetID`)钉成主连接 clientID,故 `IsPending(devId)` 能与主 context 的 `device_id` 对上。
|
||||||
- **用完即关**:MCP 拦截后 `CancelIO()` 关闭子链接(客户端 IOCPClient 以 `exit_while_disconnect=true` 构造,会随断开退出);不关会泄漏子链接。
|
- **用完即关**:MCP 拦截后 `CancelIO()` 关闭子链接(客户端 IOCPClient 以 `exit_while_disconnect=true` 构造,会随断开退出);不关会泄漏子链接。
|
||||||
- **与 MFC 对话框可并存**:MCP 与 MFC 对话框**各自使用独立的子链接**(客户端每次 `COMMAND_SYSTEM`/`COMMAND_WSLIST` 都新建一条),因此两者互不阻塞——MCP 的一次性调用不会导致 MFC 无法查看进程/窗口,反之亦然。无请求 id 的串扰靠「每 host 单飞行」(§4.4)规避,而非「与 MFC 对话框互斥」。
|
- **与 MFC 对话框可并存**:MCP 与 MFC 对话框**各自使用独立的子链接**(客户端每次 `COMMAND_SYSTEM`/`COMMAND_WSLIST` 都新建一条),因此两者互不阻塞——MCP 的一次性调用不会导致 MFC 无法查看进程/窗口,反之亦然。无请求 id 的串扰靠「每 host 单飞行」(§4.4)规避,而非「与 MFC 对话框互斥」。
|
||||||
- **编码**:进程名/路径为客户端 ANSI(CP_ACP);窗口标题为客户端 UTF-8(按能力位 `CLIENT_CAP_UTF8` 判别,老客户端回落 CP_ACP)。
|
- **编码**:进程名/路径按 clientType 判定(Windows=GBK/936、LNX/MAC=UTF-8,进程枚举走 A 接口不随 `CLIENT_CAP_UTF8` 转 UTF-8);窗口标题为客户端 UTF-8(按能力位 `CLIENT_CAP_UTF8` 判别,老客户端回落 CP936)。
|
||||||
- **请求关联**:无请求 id → 每 host 单飞行(§4.4)。
|
- **请求关联**:无请求 id → 每 host 单飞行(§4.4)。
|
||||||
|
|
||||||
|
- **文件列举也走 A′(`list_files`)**:列盘与进程/窗口同构(`COMMAND_LIST_DRIVE`→一次性子链接→`TOKEN_DRIVE_LIST` 即关);列目录则在此一次性子链接上**多一轮** `COMMAND_LIST_FILES`→`TOKEN_FILE_LIST` 后再关。故 `list_files` 归入模式 A′,而非原设计的模式 A(详见 §6.3)。
|
||||||
|
|
||||||
> **模式 A0(主连接单向命令,无响应)**:`kill_process`(`COMMAND_KILLPROCESS`)、`send_message`(`COMMAND_TALK`)这类「发完即走」的命令没有 MCP 需要的响应数据,**不需要** §4.4 的挂起注册表/超时/单飞行——`Send2Client` 后直接返回成功。它是模式 A 的退化情形,实现最简单。
|
> **模式 A0(主连接单向命令,无响应)**:`kill_process`(`COMMAND_KILLPROCESS`)、`send_message`(`COMMAND_TALK`)这类「发完即走」的命令没有 MCP 需要的响应数据,**不需要** §4.4 的挂起注册表/超时/单飞行——`Send2Client` 后直接返回成功。它是模式 A 的退化情形,实现最简单。
|
||||||
|
|
||||||
### 4.3 模式 B:子链接流式(终端 / 完整屏幕 / 完整文件)
|
### 4.3 模式 B:子链接流式(终端 / 完整屏幕 / 完整文件)
|
||||||
@@ -192,8 +194,8 @@ Web 是长连接,子链接可长驻;MCP 是**一次性 request/response**。
|
|||||||
| P2b(已实现并验证) | `list_processes` | 主机进程列表 | `COMMAND_SYSTEM`→(子链接)`TOKEN_PSLIST` | **A′** | 只读 |
|
| P2b(已实现并验证) | `list_processes` | 主机进程列表 | `COMMAND_SYSTEM`→(子链接)`TOKEN_PSLIST` | **A′** | 只读 |
|
||||||
| P2b(已实现并验证) | `list_windows` | 主机窗口列表 | `COMMAND_WSLIST`→(子链接)`TOKEN_WSLIST` | **A′** | 只读 |
|
| P2b(已实现并验证) | `list_windows` | 主机窗口列表 | `COMMAND_WSLIST`→(子链接)`TOKEN_WSLIST` | **A′** | 只读 |
|
||||||
| P2b(已实现并验证) | `get_activity_history` | 单机历史活动(文本快照) | `COMMAND_QUERY_ACTIVITY`(159)→`TOKEN_REPORT_ACTIVITY`(160) | **A** | 只读 |
|
| P2b(已实现并验证) | `get_activity_history` | 单机历史活动(文本快照) | `COMMAND_QUERY_ACTIVITY`(159)→`TOKEN_REPORT_ACTIVITY`(160) | **A** | 只读 |
|
||||||
| P2c | `get_screenshot` | 单帧屏幕截图(JPEG) | `COMMAND_SCREEN_PREVIEW_REQ`→`TOKEN_SCREEN_PREVIEW_RSP` | **A** | 只读 |
|
| P2c(已实现) | `get_screenshot` | 单帧屏幕截图(JPEG) | `COMMAND_SCREEN_PREVIEW_REQ`→`TOKEN_SCREEN_PREVIEW_RSP` | **A** | 只读 |
|
||||||
| P2c | `list_files` | 目录列举 | `COMMAND_LIST_DRIVE`/`COMMAND_LIST_FILES`→`TOKEN_DRIVE_LIST`/`TOKEN_FILE_LIST` | **A** | 只读 |
|
| P2c(已实现) | `list_files` | 目录列举 | `COMMAND_LIST_DRIVE`→`TOKEN_DRIVE_LIST`(列盘)/ 同子链接再 `COMMAND_LIST_FILES`→`TOKEN_FILE_LIST`(列目录) | **A′** | 只读 |
|
||||||
| P3 | `exec_command` | 执行命令、返回 stdout | `COMMAND_SHELL`→`TOKEN_SHELL_START`/`TOKEN_SHELL_DATA` | **B** | **写** |
|
| P3 | `exec_command` | 执行命令、返回 stdout | `COMMAND_SHELL`→`TOKEN_SHELL_START`/`TOKEN_SHELL_DATA` | **B** | **写** |
|
||||||
| P3 | `send_message` | 给被控端弹消息 | `COMMAND_TALK`(220) | **A0** | 写(低危) |
|
| P3 | `send_message` | 给被控端弹消息 | `COMMAND_TALK`(220) | **A0** | 写(低危) |
|
||||||
| P3 | `kill_process` | 结束进程 | `COMMAND_KILLPROCESS` | **A0** | **写** |
|
| P3 | `kill_process` | 结束进程 | `COMMAND_KILLPROCESS` | **A0** | **写** |
|
||||||
@@ -201,7 +203,7 @@ Web 是长连接,子链接可长驻;MCP 是**一次性 request/response**。
|
|||||||
|
|
||||||
### 5.2 Phase 2:只读观测(低风险,无写操作)
|
### 5.2 Phase 2:只读观测(低风险,无写操作)
|
||||||
|
|
||||||
全部只读,不改变被控端任何状态,token 模型不变(仍单一静态 token)。P2a 纯内存、零改动;P2b/P2c 引入模式 A 无头接管,但只读、可回滚。
|
全部只读,不改变被控端任何状态,token 模型不变(仍单一静态 token)。P2a 纯内存、零改动;P2b/P2c 引入模式 A/A′ 无头接管,但只读、可回滚。
|
||||||
|
|
||||||
### 5.3 Phase 3:安全写操作(需单独评审)
|
### 5.3 Phase 3:安全写操作(需单独评审)
|
||||||
|
|
||||||
@@ -238,7 +240,7 @@ Web 是长连接,子链接可长驻;MCP 是**一次性 request/response**。
|
|||||||
|
|
||||||
- `McpServer.h/.cpp`:
|
- `McpServer.h/.cpp`:
|
||||||
- 加 §4.4 的 `m_Pending` 注册表 + `IsPending`/`TakeMainResponse`/`BeginPending`/`WaitPending`/`ClearPending`,每 host 单飞行检查。
|
- 加 §4.4 的 `m_Pending` 注册表 + `IsPending`/`TakeMainResponse`/`BeginPending`/`WaitPending`/`ClearPending`,每 host 单飞行检查。
|
||||||
- 加解析器 `ParseProcessList`(PID/name/arch/path,CP_ACP 转 UTF-8)、`ParseWindowList`(hwnd/title/status/pid,标题按能力位解码);两者都用 `BoundedStrlen` 有界读 + 「空记录 = 尾部零填充」终止,规避客户端 `LocalSize` 对齐引入的尾部零字节。
|
- 加解析器 `ParseProcessList`(PID/name/arch/path,按 clientType 编码转 UTF-8)、`ParseWindowList`(hwnd/title/status/pid,标题按能力位解码);两者都用 `BoundedStrlen` 有界读 + 「空记录 = 尾部零填充」终止,规避客户端 `LocalSize` 对齐引入的尾部零字节。
|
||||||
- 加三个工具:`list_processes`(发 `COMMAND_SYSTEM`)、`list_windows`(发 `COMMAND_WSLIST`)、`get_activity_history`(发 `COMMAND_QUERY_ACTIVITY`)。
|
- 加三个工具:`list_processes`(发 `COMMAND_SYSTEM`)、`list_windows`(发 `COMMAND_WSLIST`)、`get_activity_history`(发 `COMMAND_QUERY_ACTIVITY`)。
|
||||||
- `2015RemoteDlg.cpp` 的 `MessageHandle`:
|
- `2015RemoteDlg.cpp` 的 `MessageHandle`:
|
||||||
- `TOKEN_PSLIST`/`TOKEN_WSLIST` 分支(子 context):`if (McpServer().IsPending(devId)) { TakeMainResponse(devId, GetBuffer(0), GetBufferLength()); ContextObject->CancelIO(); break; }`(**在 UI 线程、IO 线程阻塞期间同步拷贝**;用完即关子链接)。
|
- `TOKEN_PSLIST`/`TOKEN_WSLIST` 分支(子 context):`if (McpServer().IsPending(devId)) { TakeMainResponse(devId, GetBuffer(0), GetBufferLength()); ContextObject->CancelIO(); break; }`(**在 UI 线程、IO 线程阻塞期间同步拷贝**;用完即关子链接)。
|
||||||
@@ -250,7 +252,7 @@ Web 是长连接,子链接可长驻;MCP 是**一次性 request/response**。
|
|||||||
- MFC 双击进程/窗口管理弹框仍正常(回归点);MFC 历史活动对话框仍正常(回归点)。
|
- MFC 双击进程/窗口管理弹框仍正常(回归点);MFC 历史活动对话框仍正常(回归点)。
|
||||||
- MCP `list_processes` 返回 `[{pid,name,arch,path}]`;`list_windows` 返回 `[{hwnd,title,status,pid}]`;`get_activity_history` 返回 `{records:[...], activityHistory}`(UTF-8 文本,逐行拆记录)。
|
- MCP `list_processes` 返回 `[{pid,name,arch,path}]`;`list_windows` 返回 `[{hwnd,title,status,pid}]`;`get_activity_history` 返回 `{records:[...], activityHistory}`(UTF-8 文本,逐行拆记录)。
|
||||||
- 超时(如对离线主机)返回 `-32001` 而非挂死;同 host 并发第二次调用返回 `-32003`「设备忙」;发送失败返回 `-32004`。
|
- 超时(如对离线主机)返回 `-32001` 而非挂死;同 host 并发第二次调用返回 `-32003`「设备忙」;发送失败返回 `-32004`。
|
||||||
- 中文进程名/窗口标题无乱码(进程名 CP_ACP、窗口标题按能力位 UTF-8)。
|
- 中文进程名/窗口标题无乱码(进程名按 clientType 解码、窗口标题按能力位 UTF-8)。
|
||||||
|
|
||||||
**验证记录**(2026-08-19,真实在线主机实测):
|
**验证记录**(2026-08-19,真实在线主机实测):
|
||||||
- `list_processes`(Windows 11 主机):`pid`/`name`/`arch`/`path` 完整;`pid==0` 仅 1 条(合法 `[System Process]`),无尾部零填充空记录——padding 防护生效。
|
- `list_processes`(Windows 11 主机):`pid`/`name`/`arch`/`path` 完整;`pid==0` 仅 1 条(合法 `[System Process]`),无尾部零填充空记录——padding 防护生效。
|
||||||
@@ -261,18 +263,48 @@ Web 是长连接,子链接可长驻;MCP 是**一次性 request/response**。
|
|||||||
|
|
||||||
**回滚**:revert `McpServer.h` + `McpServer.cpp` + `MessageHandle` 两处 `if` 分支。
|
**回滚**:revert `McpServer.h` + `McpServer.cpp` + `MessageHandle` 两处 `if` 分支。
|
||||||
|
|
||||||
### 6.3 P2c:`get_screenshot` + `list_files`(模式 A)
|
### 6.3 P2c:`get_screenshot` + `list_files`(模式 A / A′)✅ 已实现(待实机验证)
|
||||||
|
|
||||||
**`get_screenshot`(屏幕预览链路,主连接 RPC)**:
|
> 本节按实际实现修订:`get_screenshot` 是**主连接 RPC**(模式 A,有 reqId 关联);`list_files` 是**一次性子链接**(模式 A′)——列盘 `COMMAND_LIST_DRIVE`→`TOKEN_DRIVE_LIST` 即关,列目录则在 `TOKEN_DRIVE_LIST` 到达时就地同子链接再发 `COMMAND_LIST_FILES`→`TOKEN_FILE_LIST` 后关链。原设计把 `list_files` 误标为模式 A,已纠正(与 §4.2′ 对进程/窗口的同类纠正一致)。
|
||||||
|
|
||||||
- 复用**现有双击预览机制** `COMMAND_SCREEN_PREVIEW_REQ`(247)→`TOKEN_SCREEN_PREVIEW_RSP`(248)(`common/commands.h:416-455`,单帧 JPEG,`ScreenPreviewReq`/`ScreenPreviewRspHeader`),`ctx->Send2Client` 发请求、主连接回响应,**不建子链接、不弹框**。
|
**`get_screenshot`(屏幕预览链路,主连接 RPC,模式 A)**:
|
||||||
- `ScreenPreviewReq.reqId` 提供请求关联,可丢弃过期响应。
|
|
||||||
- 客户端能力位 `CLIENT_CAP_SCREEN_PREVIEW`(0x0004,仅 Windows 客户端声明):非 Windows 主机返回「不支持」错误。
|
|
||||||
- 若后续需要更高清/控制,再升级为完整屏幕子链接(模式 B,需 `RegisterScreenContext` 接管)。
|
|
||||||
|
|
||||||
**`list_files`**:复用经典文件链路 `COMMAND_LIST_DRIVE`→`TOKEN_DRIVE_LIST` 与 `COMMAND_LIST_FILES`→`TOKEN_FILE_LIST`(模式 A)。**明确范围**:本阶段只做经典链路;项目另有 `COMMAND_GET_FOLDER`(66)、插件版 `TOKEN_DRIVE_LIST_PLUGIN`(150)、V2 文件传输,均列为非目标。文件列表可能较大,需**分页/上限**(首期限 `path` 一层、条目上限 500),避免 JSON 响应过大。
|
- 复用现有双击预览机制 `COMMAND_SCREEN_PREVIEW_REQ`(247)→`TOKEN_SCREEN_PREVIEW_RSP`(248)(`common/commands.h:416-455`,单帧 JPEG,`ScreenPreviewReq`/`ScreenPreviewRspHeader`),`ctx->Send2Client` 发请求、主连接回响应,**不建子链接、不弹框**。
|
||||||
|
- 复用 MFC 预览的 RTT/FRP 自适应参数挑选 `ChooseScreenPreviewParams` + `SendScreenPreviewRequest`,不重复实现 `GetTargetQualityLevel`。
|
||||||
|
- **请求关联**:`ScreenPreviewReq.reqId` 提供关联;MCP 用独立 16 位 reqId 发生器(`NextPreviewReqId`,跳过 0)+ `SetPendingReqId` 登记期望值,`TakePreviewResponse` 只接受「挂起命中 + reqId 一致」的响应,过期/他途(MFC 预览并发)响应原样回落到 MFC 路径。
|
||||||
|
- 客户端能力位 `CLIENT_CAP_SCREEN_PREVIEW`(0x0004,仅 Windows 客户端声明):非 Windows 主机返回 `-32005`「不支持」。
|
||||||
|
- 返回结构:`content[0] = {type:"image", data: base64(JPEG), mimeType:"image/jpeg"}` + `structuredContent.image = {mimeType,width,height,bytes}`(元数据,不含 base64,避免二次膨胀)。
|
||||||
|
|
||||||
**验收**:`get_screenshot` 返回 JPEG(base64 或经 MCP 图片内容类型);Windows 主机正常、非 Windows 返回明确「不支持」;`list_files` 正确列目录且有大列表保护。
|
**`list_files`(文件链路,一次性子链接,模式 A′)**:
|
||||||
|
|
||||||
|
- 复用经典文件链路 `COMMAND_LIST_DRIVE`→`TOKEN_DRIVE_LIST` 与 `COMMAND_LIST_FILES`→`TOKEN_FILE_LIST`。**明确范围**:本阶段只做经典链路;项目另有 `COMMAND_GET_FOLDER`(66)、插件版 `TOKEN_DRIVE_LIST_PLUGIN`(150)、V2 文件传输,均列为非目标。
|
||||||
|
- **两种调用形态**:
|
||||||
|
- `path` 缺省/空(或 `.`/`/`/`\`)→ 只列盘:`COMMAND_LIST_DRIVE` 开子链接,`TOKEN_DRIVE_LIST` 到达即取走盘列表并关链(与进程/窗口同构的 A′)。
|
||||||
|
- `path` 指定目录 → 列目录:`TOKEN_DRIVE_LIST` 到达时 `OnDriveList` 就地同子链接下发 `COMMAND_LIST_FILES`+`path`(`BuildListFiles` 已按客户端 ANSI 转好,`OnDriveList` 不再转),等 `TOKEN_FILE_LIST` 取走并关链。
|
||||||
|
- **编码**:盘符类型名/文件系统、文件名、`path` 均按 **clientType** 判定(与进程名/路径一致):Windows 走 A 接口(`FindFirstFileA`/`GetLogicalDriveStringsA`/`SHGetFileInfoA` 等)= 客户端 ANSI(GBK/936);LNX/MAC 文件系统天然 UTF-8。**不能用 `GetClientEncoding`**——它按能力位返回 CP_UTF8,会误解声明 `CLIENT_CAP_UTF8` 的 Windows 客户端(其文件名实为 GBK)。解析用 `ToUtf8(buf, cp)`、下发用 `ToAnsi(path, cp)`,`cp = (clientType=="LNX"||"MAC") ? CP_UTF8 : 936`。
|
||||||
|
- **大列表保护**:`ParseFileList(data, 500, cp)` 条目上限 500,避免 JSON 响应过大。
|
||||||
|
- 返回结构:列盘 → `structuredContent.drives[]`;列目录 → `structuredContent.files[]`(`{name,isDir,size,mtime}`,mtime 为 Unix 秒)。
|
||||||
|
|
||||||
|
**改动**:
|
||||||
|
|
||||||
|
- `McpServer.h/.cpp`:
|
||||||
|
- 挂起注册表扩展:`PendingRequest` 增 `path`、`expectedReqId` 字段;`BeginPending(devId, tool, path)` 重载,新增 `NextPreviewReqId`/`SetPendingReqId`/`TakePreviewResponse`/`OnDriveList` 四方法。
|
||||||
|
- 解析器:`ParseDriveList`(`[letter][type][totalMB:4][freeMB:4][typeName\0][fileSystem\0]`,`letter=='\0'` 终止)、`ParseFileList`(`[attr][filename\0][sizeHigh:4][sizeLow:4][ft:8]`,空记录/上限 500 终止)。
|
||||||
|
- 工具:`get_screenshot`(模式 A)、`list_files`(模式 A′),注册 schema(`get_screenshot` 无 inputSchema、`list_files` 带可选 `path`)。
|
||||||
|
- `2015RemoteDlg.cpp` 的 `MessageHandle`(三处拦截):
|
||||||
|
- `TOKEN_SCREEN_PREVIEW_RSP`(主 context):`TakePreviewResponse(devId, reqId, szBuffer, len)` 命中则 break(**不 CancelIO**,主连接不可关)。
|
||||||
|
- `TOKEN_DRIVE_LIST`(子 context):`if (IsPending) { OnDriveList(...) ? CancelIO() : /*继续等*/ break; }`(列盘关链、列目录不关)。
|
||||||
|
- `TOKEN_FILE_LIST`(子 context,**新增 case**):`if (IsPending) TakeMainResponse(...); CancelIO();`(无论命中都关孤儿子链接,防泄漏)。
|
||||||
|
|
||||||
|
**影响面**:`MessageHandle` 仅新增 `if` 分支与一个 `TOKEN_FILE_LIST` case;非 MCP 触发时 `IsPending` 为假、`TOKEN_FILE_LIST` case 在 MFC 路径下因 `hDlg` 接管根本不进 `MessageHandle`,现有 MFC 文件管理/屏幕预览弹框路径不变。
|
||||||
|
|
||||||
|
**验收 / 实机验证点(待构建后执行)**:
|
||||||
|
- MFC 双击文件管理弹框仍正常(回归点);MFC 双击预览缩略图仍正常(回归点)。
|
||||||
|
- MCP `get_screenshot` 返回 JPEG(base64 + 元数据),Windows 主机正常;非 Windows 主机(或未声明 `CLIENT_CAP_SCREEN_PREVIEW`)返回 `-32005`。
|
||||||
|
- MCP `list_files`(无 `path`)返回 `drives[]`;`list_files(path="C:\\")` 返回 `files[]`(含目录/文件/大小/mtime),中文文件名无乱码;条目 >500 时截断为 500。
|
||||||
|
- 错误路径:缺 `id`/非数字 `id` → `-32602`;未知/离线主机 → `-32002`;同 host 并发 → `-32003`;发送失败 → `-32004`;超时 → `-32001`。
|
||||||
|
|
||||||
|
**回滚**:revert `McpServer.h` + `McpServer.cpp` + `MessageHandle` 三处拦截。
|
||||||
|
|
||||||
### 6.4 P3a:`exec_command`(模式 B,写操作,评审后实施)
|
### 6.4 P3a:`exec_command`(模式 B,写操作,评审后实施)
|
||||||
|
|
||||||
@@ -399,7 +431,7 @@ Web 是长连接,子链接可长驻;MCP 是**一次性 request/response**。
|
|||||||
|
|
||||||
1. **P2a**(`search_hosts` + `get_host_detail`)✅ ——纯内存、零风险,已实现,验证「多工具 dispatch」模式。
|
1. **P2a**(`search_hosts` + `get_host_detail`)✅ ——纯内存、零风险,已实现,验证「多工具 dispatch」模式。
|
||||||
2. **P2b**(`list_processes` + `list_windows` + `get_activity_history`)✅ ——首次引入**模式 A′ 一次性子链接**(进程/窗口)与**模式 A 主连接 RPC**(历史活动)无头接管,把 §4.2/§4.2′ 的机制跑通并沉淀成 §7 配方。
|
2. **P2b**(`list_processes` + `list_windows` + `get_activity_history`)✅ ——首次引入**模式 A′ 一次性子链接**(进程/窗口)与**模式 A 主连接 RPC**(历史活动)无头接管,把 §4.2/§4.2′ 的机制跑通并沉淀成 §7 配方。
|
||||||
3. **P2c**(`get_screenshot` 走屏幕预览链路 + `list_files`)——补上「看」的能力,仍是只读、模式 A。
|
3. **P2c**(`get_screenshot` 走屏幕预览链路 + `list_files`)✅ ——补上「看」的能力,仍是只读(`get_screenshot` 模式 A、`list_files` 模式 A′)。
|
||||||
4. **P3a**(`exec_command`)——首次引入**模式 B 子链接流式**,在 P2 的挂起机制成熟后,走完安全评审再落地写操作。
|
4. **P3a**(`exec_command`)——首次引入**模式 B 子链接流式**,在 P2 的挂起机制成熟后,走完安全评审再落地写操作。
|
||||||
|
|
||||||
每完成一期即提交、验收,再进入下一期,保持「有序、平稳、循序渐进」。
|
每完成一期即提交、验收,再进入下一期,保持「有序、平稳、循序渐进」。
|
||||||
|
|||||||
@@ -6190,6 +6190,12 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
|||||||
// (IO 线程不接触 UI;clientId 用于循环快照路由到对应窗口)。
|
// (IO 线程不接触 UI;clientId 用于循环快照路由到对应窗口)。
|
||||||
// 不用 WPARAM 携带 ID:32 位 Windows 上 WPARAM 是 32 位,会截 64 位 clientID。
|
// 不用 WPARAM 携带 ID:32 位 Windows 上 WPARAM 是 32 位,会截 64 位 clientID。
|
||||||
if (len < sizeof(ScreenPreviewRspHeader)) break;
|
if (len < sizeof(ScreenPreviewRspHeader)) break;
|
||||||
|
// P2c:MCP 挂起时接管(屏幕预览 = 主连接 RPC,不能 CancelIO)。
|
||||||
|
// reqId 关联:命中 MCP 的 expectedReqId 才取走,避免误吞 MFC 预览在途响应。
|
||||||
|
uint64_t previewDevId = ContextObject->GetClientID();
|
||||||
|
const ScreenPreviewRspHeader* previewHdr = reinterpret_cast<const ScreenPreviewRspHeader*>(szBuffer);
|
||||||
|
if (McpServer().TakePreviewResponse(previewDevId, previewHdr->reqId, szBuffer, len))
|
||||||
|
break;
|
||||||
auto* msg = new CMy2015RemoteDlg::PreviewRspMsg{
|
auto* msg = new CMy2015RemoteDlg::PreviewRspMsg{
|
||||||
ContextObject->GetClientID(),
|
ContextObject->GetClientID(),
|
||||||
std::vector<BYTE>(szBuffer, szBuffer + len)
|
std::vector<BYTE>(szBuffer, szBuffer + len)
|
||||||
@@ -6316,10 +6322,29 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TOKEN_DRIVE_LIST: { // 文件管理【x】
|
case TOKEN_DRIVE_LIST: { // 文件管理【x】
|
||||||
|
// P2c:MCP 挂起时接管。列盘 = 取走数据后用完即关;列目录 = 转向下发
|
||||||
|
// COMMAND_LIST_FILES、等 TOKEN_FILE_LIST(此时不关子链接,交给 TOKEN_FILE_LIST 分支收尾)。
|
||||||
|
uint64_t devId = ContextObject->GetClientID();
|
||||||
|
if (McpServer().IsPending(devId)) {
|
||||||
|
if (McpServer().OnDriveList(devId, ContextObject, szBuffer, len))
|
||||||
|
ContextObject->CancelIO(); // 只列盘 → 用完即关一次性子链接
|
||||||
|
break;
|
||||||
|
}
|
||||||
ContextObject->EnableZstdContext(6);
|
ContextObject->EnableZstdContext(6);
|
||||||
g_2015RemoteDlg->SendMessage(WM_OPENFILEMANAGERDIALOG, 0, (LPARAM)ContextObject);
|
g_2015RemoteDlg->SendMessage(WM_OPENFILEMANAGERDIALOG, 0, (LPARAM)ContextObject);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case TOKEN_FILE_LIST: { // 文件列表【P2c:仅 MCP 无头路径会到达】
|
||||||
|
// 正常文件管理由 FileManagerDlg 的 hDlg 接管,TOKEN_FILE_LIST 不进此分派;
|
||||||
|
// 到达此处只有 MCP 无头列目录这一种情形。挂起命中则取数据,否则(超时迟到)
|
||||||
|
// 直接关掉孤儿子链接,避免泄漏。
|
||||||
|
uint64_t devId = ContextObject->GetClientID();
|
||||||
|
if (McpServer().IsPending(devId)) {
|
||||||
|
McpServer().TakeMainResponse(devId, ContextObject->GetBuffer(0), ContextObject->GetBufferLength());
|
||||||
|
}
|
||||||
|
ContextObject->CancelIO(); // 无论是否命中,用完即关(无 MFC 对话框接管此子链接)
|
||||||
|
break;
|
||||||
|
}
|
||||||
case TOKEN_TALK_START: { // 发送消息【x】
|
case TOKEN_TALK_START: { // 发送消息【x】
|
||||||
g_2015RemoteDlg->SendMessage(WM_OPENTALKDIALOG, 0, (LPARAM)ContextObject);
|
g_2015RemoteDlg->SendMessage(WM_OPENTALKDIALOG, 0, (LPARAM)ContextObject);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -167,6 +167,22 @@ std::string ToUtf8(const char* s, UINT cp) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从 UTF-8 转为目标编码 cp(文件链路的目录路径在 Windows 走 ANSI,与 FileManagerDlg 的
|
||||||
|
// CString 一致;cp 按 clientType 判定:Windows=936、LNX/MAC=CP_UTF8,见 BuildListFiles)。
|
||||||
|
// 非 UTF-8(如纯 ASCII)失败时原样返回。
|
||||||
|
std::string ToAnsi(const std::string& utf8, UINT cp) {
|
||||||
|
if (utf8.empty()) return "";
|
||||||
|
int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), (int)utf8.size(), NULL, 0);
|
||||||
|
if (wlen <= 0) return utf8;
|
||||||
|
std::wstring w(wlen, L'\0');
|
||||||
|
MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), (int)utf8.size(), &w[0], wlen);
|
||||||
|
int alen = WideCharToMultiByte(cp, 0, w.c_str(), wlen, NULL, 0, NULL, NULL);
|
||||||
|
if (alen <= 0) return utf8;
|
||||||
|
std::string out(alen, '\0');
|
||||||
|
WideCharToMultiByte(cp, 0, w.c_str(), wlen, &out[0], alen, NULL, NULL);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
// 从 "title|status|pid|r1|r2" 解析(自末尾 4 个 '|' 反推),兼容只有标题的老客户端。
|
// 从 "title|status|pid|r1|r2" 解析(自末尾 4 个 '|' 反推),兼容只有标题的老客户端。
|
||||||
// 逻辑复刻 SystemDlg.cpp::ParseWindowAttrs。
|
// 逻辑复刻 SystemDlg.cpp::ParseWindowAttrs。
|
||||||
void ParseWindowAttrsStr(const char* s, std::string& title, std::string& status, uint64_t& pid) {
|
void ParseWindowAttrsStr(const char* s, std::string& title, std::string& status, uint64_t& pid) {
|
||||||
@@ -191,8 +207,8 @@ void ParseWindowAttrsStr(const char* s, std::string& title, std::string& status,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 解析 TOKEN_PSLIST 缓冲(data[0]=token,其后为 [pid:4][name:arch\0][path\0] 记录)。
|
// 解析 TOKEN_PSLIST 缓冲(data[0]=token,其后为 [pid:4][name:arch\0][path\0] 记录)。
|
||||||
// 进程名/路径来自客户端 ANSI(CP_ACP),转 UTF-8 输出。
|
// 进程名/路径为客户端 ANSI(编码由 cp 指定),转 UTF-8 输出。
|
||||||
Json::Value ParseProcessList(const std::vector<BYTE>& data) {
|
Json::Value ParseProcessList(const std::vector<BYTE>& data, UINT cp) {
|
||||||
Json::Value arr(Json::arrayValue);
|
Json::Value arr(Json::arrayValue);
|
||||||
if (data.size() < 2) return arr;
|
if (data.size() < 2) return arr;
|
||||||
const char* p = (const char*)data.data();
|
const char* p = (const char*)data.data();
|
||||||
@@ -223,9 +239,9 @@ Json::Value ParseProcessList(const std::vector<BYTE>& data) {
|
|||||||
|
|
||||||
Json::Value item(Json::objectValue);
|
Json::Value item(Json::objectValue);
|
||||||
item["pid"] = (Json::UInt64)pid;
|
item["pid"] = (Json::UInt64)pid;
|
||||||
item["name"] = ToUtf8(name.c_str(), CP_ACP);
|
item["name"] = ToUtf8(name.c_str(), cp);
|
||||||
item["arch"] = arch.empty() ? "N/A" : ToUtf8(arch.c_str(), CP_ACP);
|
item["arch"] = arch.empty() ? "N/A" : ToUtf8(arch.c_str(), cp);
|
||||||
item["path"] = ToUtf8(fullPath, CP_ACP);
|
item["path"] = ToUtf8(fullPath, cp);
|
||||||
arr.append(item);
|
arr.append(item);
|
||||||
}
|
}
|
||||||
return arr;
|
return arr;
|
||||||
@@ -263,6 +279,93 @@ Json::Value ParseWindowList(const std::vector<BYTE>& data, UINT cp) {
|
|||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 解析 TOKEN_DRIVE_LIST 缓冲(data[0]=token,其后为
|
||||||
|
// [letter:1][GetDriveType:1][totalMB:4][freeMB:4][typeName\0][fileSystem\0] 记录,
|
||||||
|
// 以 letter=='\0' 终止)。typeName/fileSystem 为客户端 ANSI(编码由 cp 指定)。
|
||||||
|
Json::Value ParseDriveList(const std::vector<BYTE>& data, UINT cp) {
|
||||||
|
Json::Value arr(Json::arrayValue);
|
||||||
|
if (data.size() < 2) return arr;
|
||||||
|
const char* p = (const char*)data.data();
|
||||||
|
size_t len = data.size();
|
||||||
|
size_t off = 1; // 跳过 TOKEN 字节
|
||||||
|
while (off + 10 <= len && p[off] != '\0') {
|
||||||
|
char letter = p[off];
|
||||||
|
unsigned int type = (unsigned int)(unsigned char)p[off + 1];
|
||||||
|
DWORD totalMB = *(const DWORD*)(p + off + 2);
|
||||||
|
DWORD freeMB = *(const DWORD*)(p + off + 6);
|
||||||
|
off += 10;
|
||||||
|
|
||||||
|
const char* typeName = p + off;
|
||||||
|
size_t tlen = BoundedStrlen(typeName, len - off);
|
||||||
|
if (tlen >= len - off) break;
|
||||||
|
off += tlen + 1;
|
||||||
|
|
||||||
|
const char* fileSystem = p + off;
|
||||||
|
size_t flen = BoundedStrlen(fileSystem, len - off);
|
||||||
|
if (flen >= len - off) break;
|
||||||
|
off += flen + 1;
|
||||||
|
|
||||||
|
std::string drive;
|
||||||
|
drive += letter;
|
||||||
|
drive += ":\\";
|
||||||
|
|
||||||
|
Json::Value item(Json::objectValue);
|
||||||
|
item["drive"] = drive;
|
||||||
|
item["type"] = (Json::UInt64)type;
|
||||||
|
item["typeName"] = ToUtf8(typeName, cp);
|
||||||
|
item["fileSystem"] = ToUtf8(fileSystem, cp);
|
||||||
|
item["totalMB"] = (Json::UInt64)totalMB;
|
||||||
|
item["freeMB"] = (Json::UInt64)freeMB;
|
||||||
|
arr.append(item);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 TOKEN_FILE_LIST 缓冲(data[0]=token,其后为
|
||||||
|
// [attr:1][filename\0][sizeHigh:4][sizeLow:4][ftLastWriteTime:8] 记录)。
|
||||||
|
// 文件名为客户端 ANSI(编码由 cp 指定);attr 非 0 表示目录(FILE_ATTRIBUTE_DIRECTORY)。
|
||||||
|
Json::Value ParseFileList(const std::vector<BYTE>& data, int maxEntries, UINT cp) {
|
||||||
|
Json::Value arr(Json::arrayValue);
|
||||||
|
if (data.size() < 2) return arr;
|
||||||
|
const char* p = (const char*)data.data();
|
||||||
|
size_t len = data.size();
|
||||||
|
size_t off = 1;
|
||||||
|
int count = 0;
|
||||||
|
while (off + 1 <= len && count < maxEntries) {
|
||||||
|
bool isDir = (p[off] & FILE_ATTRIBUTE_DIRECTORY) != 0;
|
||||||
|
off += 1;
|
||||||
|
|
||||||
|
const char* name = p + off;
|
||||||
|
size_t nlen = BoundedStrlen(name, len - off);
|
||||||
|
if (nlen >= len - off) break;
|
||||||
|
if (nlen == 0) break; // 空记录 = 尾部零填充(LocalAlloc 对齐),停止解析
|
||||||
|
off += nlen + 1;
|
||||||
|
|
||||||
|
if (off + 16 > len) break;
|
||||||
|
DWORD sizeHigh = *(const DWORD*)(p + off);
|
||||||
|
DWORD sizeLow = *(const DWORD*)(p + off + 4);
|
||||||
|
ULONGLONG size = ((ULONGLONG)sizeHigh << 32) | sizeLow;
|
||||||
|
// 修改时间(FILETIME,100ns since 1601-01-01)→ Unix 秒
|
||||||
|
// FILETIME 在线上是 dwLowDateTime(低 4 字节) 在前、dwHighDateTime(高 4 字节) 在后
|
||||||
|
// (客户端 memcpy(&ftLastWriteTime, sizeof(FILETIME)),见 FileManager.cpp::SendFilesList)。
|
||||||
|
DWORD ftLow = *(const DWORD*)(p + off + 8);
|
||||||
|
DWORD ftHigh = *(const DWORD*)(p + off + 12);
|
||||||
|
off += 16;
|
||||||
|
|
||||||
|
ULONGLONG ft = ((ULONGLONG)ftHigh << 32) | ftLow;
|
||||||
|
Json::Int64 mtime = (Json::Int64)(ft / 10000000ULL) - 11644473600LL;
|
||||||
|
|
||||||
|
Json::Value item(Json::objectValue);
|
||||||
|
item["name"] = ToUtf8(name, cp);
|
||||||
|
item["isDir"] = isDir;
|
||||||
|
item["size"] = (Json::UInt64)size;
|
||||||
|
item["mtime"] = mtime;
|
||||||
|
arr.append(item);
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
// 收集所有在线主机 JSON 数组(m_cs 锁内遍历,复用 BuildHostJson 序列化,方案 C)
|
// 收集所有在线主机 JSON 数组(m_cs 锁内遍历,复用 BuildHostJson 序列化,方案 C)
|
||||||
void CollectOnlineHosts(CMy2015RemoteDlg* parent, Json::Value& hosts) {
|
void CollectOnlineHosts(CMy2015RemoteDlg* parent, Json::Value& hosts) {
|
||||||
if (!parent) return;
|
if (!parent) return;
|
||||||
@@ -456,6 +559,109 @@ Json::Value BuildActivityHistoryOutputSchema() {
|
|||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get_screenshot 的 outputSchema(image 元数据;base64 数据在 content 的 image 块中)
|
||||||
|
Json::Value BuildScreenshotOutputSchema() {
|
||||||
|
Json::Value props(Json::objectValue);
|
||||||
|
|
||||||
|
Json::Value imageProp(Json::objectValue);
|
||||||
|
imageProp["type"] = "object";
|
||||||
|
Json::Value imageProps(Json::objectValue);
|
||||||
|
Json::Value mimeProp(Json::objectValue);
|
||||||
|
mimeProp["type"] = "string";
|
||||||
|
imageProps["mimeType"] = mimeProp;
|
||||||
|
const char* intFields[] = { "width", "height", "bytes" };
|
||||||
|
for (const char* f : intFields) {
|
||||||
|
Json::Value s(Json::objectValue);
|
||||||
|
s["type"] = "integer";
|
||||||
|
imageProps[f] = s;
|
||||||
|
}
|
||||||
|
imageProp["properties"] = imageProps;
|
||||||
|
props["image"] = imageProp;
|
||||||
|
|
||||||
|
Json::Value schema(Json::objectValue);
|
||||||
|
schema["type"] = "object";
|
||||||
|
schema["properties"] = props;
|
||||||
|
Json::Value required(Json::arrayValue);
|
||||||
|
required.append("image");
|
||||||
|
schema["required"] = required;
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
// list_files 的 inputSchema(id 必填,path 可选;path 缺省/空 = 列盘)
|
||||||
|
Json::Value BuildListFilesInputSchema() {
|
||||||
|
Json::Value props(Json::objectValue);
|
||||||
|
|
||||||
|
Json::Value idProp(Json::objectValue);
|
||||||
|
idProp["type"] = "string";
|
||||||
|
idProp["description"] = u8"主机 id,取 list_online_hosts / search_hosts 返回的 id 字段";
|
||||||
|
props["id"] = idProp;
|
||||||
|
|
||||||
|
Json::Value pathProp(Json::objectValue);
|
||||||
|
pathProp["type"] = "string";
|
||||||
|
pathProp["description"] = u8"要列举的目录路径(如 C:\\Windows);省略或传空则返回驱动器列表";
|
||||||
|
props["path"] = pathProp;
|
||||||
|
|
||||||
|
Json::Value schema(Json::objectValue);
|
||||||
|
schema["type"] = "object";
|
||||||
|
schema["properties"] = props;
|
||||||
|
Json::Value required(Json::arrayValue);
|
||||||
|
required.append("id");
|
||||||
|
schema["required"] = required;
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
// list_files 的 outputSchema(drives 数组 / files 数组,二者其一非空)
|
||||||
|
Json::Value BuildListFilesOutputSchema() {
|
||||||
|
Json::Value props(Json::objectValue);
|
||||||
|
|
||||||
|
Json::Value drivesProp(Json::objectValue);
|
||||||
|
drivesProp["type"] = "array";
|
||||||
|
Json::Value driveItems(Json::objectValue);
|
||||||
|
driveItems["type"] = "object";
|
||||||
|
Json::Value driveProps(Json::objectValue);
|
||||||
|
const char* driveStr[] = { "drive", "typeName", "fileSystem" };
|
||||||
|
for (const char* f : driveStr) {
|
||||||
|
Json::Value s(Json::objectValue);
|
||||||
|
s["type"] = "string";
|
||||||
|
driveProps[f] = s;
|
||||||
|
}
|
||||||
|
const char* driveInt[] = { "type", "totalMB", "freeMB" };
|
||||||
|
for (const char* f : driveInt) {
|
||||||
|
Json::Value s(Json::objectValue);
|
||||||
|
s["type"] = "integer";
|
||||||
|
driveProps[f] = s;
|
||||||
|
}
|
||||||
|
driveItems["properties"] = driveProps;
|
||||||
|
drivesProp["items"] = driveItems;
|
||||||
|
props["drives"] = drivesProp;
|
||||||
|
|
||||||
|
Json::Value filesProp(Json::objectValue);
|
||||||
|
filesProp["type"] = "array";
|
||||||
|
Json::Value fileItems(Json::objectValue);
|
||||||
|
fileItems["type"] = "object";
|
||||||
|
Json::Value fileProps(Json::objectValue);
|
||||||
|
Json::Value nameProp(Json::objectValue);
|
||||||
|
nameProp["type"] = "string";
|
||||||
|
fileProps["name"] = nameProp;
|
||||||
|
Json::Value isDirProp(Json::objectValue);
|
||||||
|
isDirProp["type"] = "boolean";
|
||||||
|
fileProps["isDir"] = isDirProp;
|
||||||
|
const char* fileInt[] = { "size", "mtime" };
|
||||||
|
for (const char* f : fileInt) {
|
||||||
|
Json::Value s(Json::objectValue);
|
||||||
|
s["type"] = "integer";
|
||||||
|
fileProps[f] = s;
|
||||||
|
}
|
||||||
|
fileItems["properties"] = fileProps;
|
||||||
|
filesProp["items"] = fileItems;
|
||||||
|
props["files"] = filesProp;
|
||||||
|
|
||||||
|
Json::Value schema(Json::objectValue);
|
||||||
|
schema["type"] = "object";
|
||||||
|
schema["properties"] = props;
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
// tools/list
|
// tools/list
|
||||||
std::string BuildToolsListResult(const Json::Value& id) {
|
std::string BuildToolsListResult(const Json::Value& id) {
|
||||||
Json::Value result(Json::objectValue);
|
Json::Value result(Json::objectValue);
|
||||||
@@ -540,6 +746,30 @@ std::string BuildToolsListResult(const Json::Value& id) {
|
|||||||
tools.append(tool);
|
tools.append(tool);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 7) get_screenshot(P2c:屏幕预览链路,主连接 RPC)
|
||||||
|
{
|
||||||
|
Json::Value tool(Json::objectValue);
|
||||||
|
tool["name"] = "get_screenshot";
|
||||||
|
tool["description"] = u8"截取指定在线主机的一帧屏幕,返回 base64 编码的 JPEG 图片(image 对象含 mimeType/data/width/height)。仅 Windows 客户端支持(能力位 CLIENT_CAP_SCREEN_PREVIEW),主连接 RPC、一次性返回。";
|
||||||
|
|
||||||
|
tool["inputSchema"] = BuildGetHostDetailInputSchema(); // 复用 { id } 必填 schema
|
||||||
|
tool["outputSchema"] = BuildScreenshotOutputSchema();
|
||||||
|
|
||||||
|
tools.append(tool);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8) list_files(P2c:文件链路,列盘/列目录)
|
||||||
|
{
|
||||||
|
Json::Value tool(Json::objectValue);
|
||||||
|
tool["name"] = "list_files";
|
||||||
|
tool["description"] = u8"列举指定在线主机的目录。省略或空 path 返回驱动器列表(drives);给定 path 返回该目录一层内的文件/子目录(files,最多 500 条,含 name/isDir/size/mtime)。只读。";
|
||||||
|
|
||||||
|
tool["inputSchema"] = BuildListFilesInputSchema();
|
||||||
|
tool["outputSchema"] = BuildListFilesOutputSchema();
|
||||||
|
|
||||||
|
tools.append(tool);
|
||||||
|
}
|
||||||
|
|
||||||
result["tools"] = tools;
|
result["tools"] = tools;
|
||||||
return BuildResult(id, result);
|
return BuildResult(id, result);
|
||||||
}
|
}
|
||||||
@@ -674,7 +904,13 @@ std::string BuildListProcesses(const Json::Value& id, const Json::Value& args, C
|
|||||||
if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs))
|
if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs))
|
||||||
return BuildError(id, -32001, "Timeout waiting for process list");
|
return BuildError(id, -32001, "Timeout waiting for process list");
|
||||||
|
|
||||||
Json::Value procs = ParseProcessList(data);
|
// 进程名/路径编码按 clientType 判定:Windows 走 A 接口(QueryFullProcessImageNameA 等)
|
||||||
|
// =客户端 ANSI(GBK/936),不随 CLIENT_CAP_UTF8 转 UTF-8;LNX/MAC 的 /proc 天然 UTF-8。
|
||||||
|
// 不能用 GetClientEncoding——它按能力位返回 CP_UTF8,会误解 Windows 客户端的 GBK 进程名。
|
||||||
|
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
||||||
|
UINT cp = (clientType == "LNX" || clientType == "MAC") ? CP_UTF8 : 936;
|
||||||
|
|
||||||
|
Json::Value procs = ParseProcessList(data, cp);
|
||||||
int count = (int)procs.size();
|
int count = (int)procs.size();
|
||||||
|
|
||||||
Json::Value result(Json::objectValue);
|
Json::Value result(Json::objectValue);
|
||||||
@@ -799,6 +1035,143 @@ std::string BuildGetActivityHistory(const Json::Value& id, const Json::Value& ar
|
|||||||
return BuildResult(id, result);
|
return BuildResult(id, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tools/call:get_screenshot(屏幕预览链路,主连接 RPC,不建子链接、不弹框)
|
||||||
|
std::string BuildGetScreenshot(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||||||
|
uint64_t devId = 0;
|
||||||
|
std::string err;
|
||||||
|
if (!ParseHostIdArg(args, devId, err))
|
||||||
|
return BuildError(id, -32602, err);
|
||||||
|
|
||||||
|
context* ctx = FindMainContext(parent, devId);
|
||||||
|
if (!ctx)
|
||||||
|
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
|
||||||
|
|
||||||
|
// 能力位门槛:仅 Windows 客户端声明 CLIENT_CAP_SCREEN_PREVIEW(非 Windows 返回明确错误)
|
||||||
|
if (!ctx->SupportsScreenPreview())
|
||||||
|
return BuildError(id, -32005, "Host does not support screen preview");
|
||||||
|
|
||||||
|
CMcpServer& mcp = CMcpServer::Instance();
|
||||||
|
if (!mcp.BeginPending(devId, "get_screenshot"))
|
||||||
|
return BuildError(id, -32003, "Device busy: another request is pending for this host");
|
||||||
|
|
||||||
|
uint16_t reqId = mcp.NextPreviewReqId();
|
||||||
|
mcp.SetPendingReqId(devId, reqId);
|
||||||
|
|
||||||
|
// 复用 MFC 预览的 RTT/FRP 自适应参数挑选,避免重复实现 GetTargetQualityLevel 逻辑
|
||||||
|
WORD maxWidth = 0;
|
||||||
|
BYTE quality = 0;
|
||||||
|
parent->ChooseScreenPreviewParams(ctx, maxWidth, quality);
|
||||||
|
parent->SendScreenPreviewRequest(ctx, reqId, maxWidth, quality);
|
||||||
|
|
||||||
|
std::vector<BYTE> data;
|
||||||
|
if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs))
|
||||||
|
return BuildError(id, -32001, "Timeout waiting for screenshot");
|
||||||
|
|
||||||
|
// data = [ScreenPreviewRspHeader][JPEG]
|
||||||
|
if (data.size() < sizeof(ScreenPreviewRspHeader))
|
||||||
|
return BuildError(id, -32000, "Invalid screenshot response");
|
||||||
|
|
||||||
|
const ScreenPreviewRspHeader* hdr = reinterpret_cast<const ScreenPreviewRspHeader*>(data.data());
|
||||||
|
if (hdr->status != SCREEN_PREVIEW_OK || hdr->format != SCREEN_PREVIEW_FMT_JPEG ||
|
||||||
|
hdr->bytes == 0 || data.size() < sizeof(ScreenPreviewRspHeader) + hdr->bytes) {
|
||||||
|
return BuildError(id, -32000,
|
||||||
|
"Screenshot capture failed (status " + std::to_string((int)hdr->status) + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string b64 = httplib::detail::base64_encode(
|
||||||
|
std::string((const char*)data.data() + sizeof(ScreenPreviewRspHeader), hdr->bytes));
|
||||||
|
|
||||||
|
Json::Value image(Json::objectValue);
|
||||||
|
image["mimeType"] = "image/jpeg";
|
||||||
|
image["width"] = (Json::UInt64)hdr->width;
|
||||||
|
image["height"] = (Json::UInt64)hdr->height;
|
||||||
|
image["bytes"] = (Json::UInt64)hdr->bytes;
|
||||||
|
|
||||||
|
Json::Value result(Json::objectValue);
|
||||||
|
Json::Value structuredContent(Json::objectValue);
|
||||||
|
structuredContent["image"] = image; // 元数据(不含 base64,避免在 content 之外重复大 payload)
|
||||||
|
result["structuredContent"] = structuredContent;
|
||||||
|
|
||||||
|
Json::Value content(Json::arrayValue);
|
||||||
|
Json::Value item(Json::objectValue);
|
||||||
|
item["type"] = "image";
|
||||||
|
item["data"] = b64;
|
||||||
|
item["mimeType"] = "image/jpeg";
|
||||||
|
content.append(item);
|
||||||
|
result["content"] = content;
|
||||||
|
result["isError"] = false;
|
||||||
|
|
||||||
|
return BuildResult(id, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// tools/call:list_files(列盘走 COMMAND_LIST_DRIVE→TOKEN_DRIVE_LIST;
|
||||||
|
// 列目录走 COMMAND_LIST_DRIVE 开子链接后,再下发 COMMAND_LIST_FILES→TOKEN_FILE_LIST)
|
||||||
|
std::string BuildListFiles(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||||||
|
uint64_t devId = 0;
|
||||||
|
std::string err;
|
||||||
|
if (!ParseHostIdArg(args, devId, err))
|
||||||
|
return BuildError(id, -32602, err);
|
||||||
|
|
||||||
|
context* ctx = FindMainContext(parent, devId);
|
||||||
|
if (!ctx)
|
||||||
|
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
|
||||||
|
|
||||||
|
std::string path = GetStringArg(args, "path");
|
||||||
|
bool listDrives = path.empty() || path == "." || path == "/" || path == "\\";
|
||||||
|
|
||||||
|
// 目录路径/文件名编码按 clientType 判定:Windows 走 A 接口(FindFirstFileA 等)= 客户端
|
||||||
|
// ANSI(GBK/936),不随 CLIENT_CAP_UTF8 转 UTF-8;LNX/MAC 文件系统天然 UTF-8。不能用
|
||||||
|
// GetClientEncoding——它按能力位返回 CP_UTF8,会误解 Windows 客户端的 GBK 文件名。
|
||||||
|
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
||||||
|
UINT cp = (clientType == "LNX" || clientType == "MAC") ? CP_UTF8 : 936;
|
||||||
|
|
||||||
|
// 下发前把 UTF-8 path 转成客户端 ANSI(空/./\/\\ 为 ASCII,转换后不变),OnDriveList 直接用。
|
||||||
|
std::string ansiPath = ToAnsi(path, cp);
|
||||||
|
|
||||||
|
CMcpServer& mcp = CMcpServer::Instance();
|
||||||
|
if (!mcp.BeginPending(devId, "list_files", ansiPath))
|
||||||
|
return BuildError(id, -32003, "Device busy: another request is pending for this host");
|
||||||
|
|
||||||
|
BYTE cmd = COMMAND_LIST_DRIVE;
|
||||||
|
if (!ctx->Send2Client(&cmd, 1)) {
|
||||||
|
mcp.ClearPending(devId);
|
||||||
|
return BuildError(id, -32004, "Failed to send command to host");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<BYTE> data;
|
||||||
|
if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs))
|
||||||
|
return BuildError(id, -32001, "Timeout waiting for file list");
|
||||||
|
|
||||||
|
Json::Value drives(Json::arrayValue);
|
||||||
|
Json::Value files(Json::arrayValue);
|
||||||
|
int count = 0;
|
||||||
|
if (listDrives) {
|
||||||
|
drives = ParseDriveList(data, cp);
|
||||||
|
count = (int)drives.size();
|
||||||
|
} else {
|
||||||
|
files = ParseFileList(data, 500, cp); // 上限 500,避免 JSON 响应过大
|
||||||
|
count = (int)files.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value result(Json::objectValue);
|
||||||
|
Json::Value structuredContent(Json::objectValue);
|
||||||
|
if (listDrives) structuredContent["drives"] = drives;
|
||||||
|
else structuredContent["files"] = files;
|
||||||
|
result["structuredContent"] = structuredContent;
|
||||||
|
|
||||||
|
Json::Value content(Json::arrayValue);
|
||||||
|
Json::Value item(Json::objectValue);
|
||||||
|
item["type"] = "text";
|
||||||
|
item["text"] = listDrives
|
||||||
|
? (std::string(u8"共 ") + std::to_string(count) + std::string(u8" 个驱动器。"))
|
||||||
|
: (std::string(u8"共 ") + std::to_string(count) + std::string(u8" 个条目。"));
|
||||||
|
content.append(item);
|
||||||
|
result["content"] = content;
|
||||||
|
result["isError"] = false;
|
||||||
|
|
||||||
|
return BuildResult(id, result);
|
||||||
|
}
|
||||||
|
|
||||||
// tools/call 分派
|
// tools/call 分派
|
||||||
std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
|
std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
|
||||||
const Json::Value& id = root["id"];
|
const Json::Value& id = root["id"];
|
||||||
@@ -817,6 +1190,8 @@ std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
|
|||||||
if (toolName == "list_processes") return BuildListProcesses(id, args, parent);
|
if (toolName == "list_processes") return BuildListProcesses(id, args, parent);
|
||||||
if (toolName == "list_windows") return BuildListWindows(id, args, parent);
|
if (toolName == "list_windows") return BuildListWindows(id, args, parent);
|
||||||
if (toolName == "get_activity_history") return BuildGetActivityHistory(id, args, parent);
|
if (toolName == "get_activity_history") return BuildGetActivityHistory(id, args, parent);
|
||||||
|
if (toolName == "get_screenshot") return BuildGetScreenshot(id, args, parent);
|
||||||
|
if (toolName == "list_files") return BuildListFiles(id, args, parent);
|
||||||
|
|
||||||
return BuildError(id, -32602,
|
return BuildError(id, -32602,
|
||||||
"Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName));
|
"Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName));
|
||||||
@@ -966,6 +1341,71 @@ void CMcpServer::ClearPending(uint64_t device_id) {
|
|||||||
m_Pending.erase(device_id);
|
m_Pending.erase(device_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== P2c:list_files / get_screenshot 扩展 =====
|
||||||
|
|
||||||
|
bool CMcpServer::BeginPending(uint64_t device_id, const std::string& tool, const std::string& path) {
|
||||||
|
std::lock_guard<std::mutex> lk(m_PendingMutex);
|
||||||
|
if (m_Pending.find(device_id) != m_Pending.end()) return false; // 设备忙
|
||||||
|
PendingRequest r;
|
||||||
|
r.tool = tool;
|
||||||
|
r.path = path;
|
||||||
|
m_Pending[device_id] = std::move(r);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t CMcpServer::NextPreviewReqId() {
|
||||||
|
uint16_t v = m_PreviewReqId.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
if (v == 0) v = m_PreviewReqId.fetch_add(1, std::memory_order_relaxed); // 跳过 0
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMcpServer::SetPendingReqId(uint64_t device_id, uint16_t reqId) {
|
||||||
|
std::lock_guard<std::mutex> lk(m_PendingMutex);
|
||||||
|
auto it = m_Pending.find(device_id);
|
||||||
|
if (it != m_Pending.end()) it->second.expectedReqId = reqId;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CMcpServer::TakePreviewResponse(uint64_t device_id, uint16_t reqId, const BYTE* data, ULONG len) {
|
||||||
|
std::lock_guard<std::mutex> lk(m_PendingMutex);
|
||||||
|
auto it = m_Pending.find(device_id);
|
||||||
|
if (it == m_Pending.end()) return false; // 已超时清理 → 迟到数据,回落 MFC
|
||||||
|
if (it->second.tool != "get_screenshot") return false;
|
||||||
|
if (it->second.expectedReqId == 0 || it->second.expectedReqId != reqId) return false; // 过期/他途响应
|
||||||
|
it->second.data.assign(data, data + len);
|
||||||
|
it->second.done = true;
|
||||||
|
m_PendingCv.notify_one();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CMcpServer::OnDriveList(uint64_t device_id, context* subCtx, const BYTE* buf, ULONG len) {
|
||||||
|
std::string path;
|
||||||
|
bool listDrives = true;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(m_PendingMutex);
|
||||||
|
auto it = m_Pending.find(device_id);
|
||||||
|
if (it == m_Pending.end()) return true; // 已超时清理 → 调用方 CancelIO 收尾
|
||||||
|
path = it->second.path;
|
||||||
|
listDrives = path.empty() || path == "." || path == "/" || path == "\\";
|
||||||
|
if (listDrives) {
|
||||||
|
it->second.data.assign(buf, buf + len);
|
||||||
|
it->second.done = true;
|
||||||
|
m_PendingCv.notify_one();
|
||||||
|
return true; // 只列盘 → 调用方用完即关
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列目录:锁外下发 COMMAND_LIST_FILES + path,子链接保持,等 TOKEN_FILE_LIST。
|
||||||
|
// path 已在 BuildListFiles 按客户端 ANSI 转好(OnDriveList 无需再转),结尾 '\0'
|
||||||
|
// 与 FileManagerDlg 的 PacketSize=len+2 一致。
|
||||||
|
std::vector<BYTE> pkt;
|
||||||
|
pkt.reserve(1 + path.size() + 1);
|
||||||
|
pkt.push_back((BYTE)COMMAND_LIST_FILES);
|
||||||
|
pkt.insert(pkt.end(), path.begin(), path.end());
|
||||||
|
pkt.push_back(0);
|
||||||
|
subCtx->Send2Client(pkt.data(), (ULONG)pkt.size());
|
||||||
|
return false; // 继续等 TOKEN_FILE_LIST,调用方不 CancelIO
|
||||||
|
}
|
||||||
|
|
||||||
// rand_s:Windows CRT 加密安全随机源(基于系统 CSPRNG)。其声明需在 <stdlib.h> 前
|
// rand_s:Windows CRT 加密安全随机源(基于系统 CSPRNG)。其声明需在 <stdlib.h> 前
|
||||||
// 定义 _CRT_RAND_S;为避免依赖 PCH 的包含顺序,这里手动声明其导出原型(errno_t == int)。
|
// 定义 _CRT_RAND_S;为避免依赖 PCH 的包含顺序,这里手动声明其导出原型(errno_t == int)。
|
||||||
extern "C" int __cdecl rand_s(unsigned int* randomValue);
|
extern "C" int __cdecl rand_s(unsigned int* randomValue);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
#pragma comment(lib, "ws2_32.lib")
|
#pragma comment(lib, "ws2_32.lib")
|
||||||
|
|
||||||
class CMy2015RemoteDlg;
|
class CMy2015RemoteDlg;
|
||||||
|
class context;
|
||||||
|
|
||||||
// MCP (Model Context Protocol) 服务端:httplib 封装 + JSON-RPC 2.0 分发 + 静态 token 校验。
|
// MCP (Model Context Protocol) 服务端:httplib 封装 + JSON-RPC 2.0 分发 + 静态 token 校验。
|
||||||
// 与 CWebService 平级、互不依赖;默认禁用,经「扩展 → MCP设置」开启后监听
|
// 与 CWebService 平级、互不依赖;默认禁用,经「扩展 → MCP设置」开启后监听
|
||||||
@@ -62,6 +63,26 @@ public:
|
|||||||
// 工具线程:清理挂起状态(发送失败等提前退出路径)。
|
// 工具线程:清理挂起状态(发送失败等提前退出路径)。
|
||||||
void ClearPending(uint64_t device_id);
|
void ClearPending(uint64_t device_id);
|
||||||
|
|
||||||
|
// ===== P2c:list_files / get_screenshot 扩展 =====
|
||||||
|
|
||||||
|
// 登记挂起(带目录路径;list_files 专用,path 为空表示只列盘)。
|
||||||
|
bool BeginPending(uint64_t device_id, const std::string& tool, const std::string& path);
|
||||||
|
|
||||||
|
// get_screenshot:工具线程生成 16 位 reqId(跳过 0,0 表示未设置),用于丢弃过期响应。
|
||||||
|
uint16_t NextPreviewReqId();
|
||||||
|
|
||||||
|
// get_screenshot:登记期望的 reqId(必须在发送 COMMAND_SCREEN_PREVIEW_REQ 前调用)。
|
||||||
|
void SetPendingReqId(uint64_t device_id, uint16_t reqId);
|
||||||
|
|
||||||
|
// get_screenshot:MessageHandle(TOKEN_SCREEN_PREVIEW_RSP) 在 IO 线程同步调用;
|
||||||
|
// 挂起命中且 reqId 一致则拷贝响应并返回 true,否则 false(回落到 MFC 预览路径)。
|
||||||
|
bool TakePreviewResponse(uint64_t device_id, uint16_t reqId, const BYTE* data, ULONG len);
|
||||||
|
|
||||||
|
// list_files:MessageHandle(TOKEN_DRIVE_LIST) 调用。
|
||||||
|
// 返回 true = 已取走盘列表(调用方 CancelIO);false = 已转向下发 COMMAND_LIST_FILES、
|
||||||
|
// 等 TOKEN_FILE_LIST(调用方不关子链接)。
|
||||||
|
bool OnDriveList(uint64_t device_id, context* subCtx, const BYTE* buf, ULONG len);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
CMcpServer();
|
CMcpServer();
|
||||||
~CMcpServer();
|
~CMcpServer();
|
||||||
@@ -80,12 +101,17 @@ private:
|
|||||||
// 挂起请求注册表(受 m_PendingMutex 保护,键 = device_id)。
|
// 挂起请求注册表(受 m_PendingMutex 保护,键 = device_id)。
|
||||||
struct PendingRequest {
|
struct PendingRequest {
|
||||||
std::string tool;
|
std::string tool;
|
||||||
|
std::string path; // list_files:目录路径;空 = 只列盘
|
||||||
|
uint16_t expectedReqId = 0; // get_screenshot:期望的预览 reqId(0 = 未设置)
|
||||||
std::vector<BYTE> data;
|
std::vector<BYTE> data;
|
||||||
bool done = false;
|
bool done = false;
|
||||||
};
|
};
|
||||||
std::mutex m_PendingMutex;
|
std::mutex m_PendingMutex;
|
||||||
std::condition_variable m_PendingCv;
|
std::condition_variable m_PendingCv;
|
||||||
std::map<uint64_t, PendingRequest> m_Pending;
|
std::map<uint64_t, PendingRequest> m_Pending;
|
||||||
|
|
||||||
|
// get_screenshot 的 reqId 发生器(16 位,跳过 0)。与 MFC 预览的 m_PreviewReqId 各自独立计数。
|
||||||
|
std::atomic<uint16_t> m_PreviewReqId{1};
|
||||||
};
|
};
|
||||||
|
|
||||||
// 全局访问器(仿 WebService(),见 WebService.h 末尾)
|
// 全局访问器(仿 WebService(),见 WebService.h 末尾)
|
||||||
|
|||||||
Reference in New Issue
Block a user