Feature: Add list_processes, list_windows and get_activity_history MCP tools
Add three read-only P2b MCP tools over the existing protocol. list_processes and list_windows trigger the client via COMMAND_SYSTEM / COMMAND_WSLIST on the main connection and receive TOKEN_PSLIST / TOKEN_WSLIST on a one-shot sub-link; get_activity_history uses the main-connection RPC COMMAND_QUERY_ACTIVITY -> TOKEN_REPORT_ACTIVITY. A per-host single-flight pending registry (m_Pending) with a 20s timeout correlates each response to its request and rejects a concurrent request for the same host with -32003. Parsers stop on the first empty record to ignore the client's LocalSize trailing zero padding, and window titles are decoded per the client UTF-8 capability bit. MessageHandle only adds if-guarded branches, so the MFC dialogs are untouched. Sync Mcp_Phase2_Design.md with the mode A'/A architecture, the verification notes, and the registry-backed config location. Co-Authored-By: deepseek-v4-pro
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# YAMA MCP 功能开发技术书(Phase 2 及后续)
|
# YAMA MCP 功能开发技术书(Phase 2 及后续)
|
||||||
|
|
||||||
> **状态**:设计定稿(已按两轮评审修订);P2a 已实现,待 build 验证。
|
> **状态**:设计定稿(已按两轮评审修订);P2a、P2b 已实现并经真实主机验证(`search_hosts` / `get_host_detail` / `list_processes` / `list_windows` / `get_activity_history`)。
|
||||||
> **读者**:MCP 后续功能的研发/评审人员。
|
> **读者**:MCP 后续功能的研发/评审人员。
|
||||||
> **关联文档**:[Mcp_Design.md](./Mcp_Design.md)(Phase 1 的协议、架构、配置与菜单设计)。
|
> **关联文档**:[Mcp_Design.md](./Mcp_Design.md)(Phase 1 的协议、架构、配置与菜单设计)。
|
||||||
> **核心目标**:让 MCP 从「单个只读工具」平滑演进为「分阶段、可回滚、影响面可控」的工具集,**不一次性大改现有功能**。
|
> **核心目标**:让 MCP 从「单个只读工具」平滑演进为「分阶段、可回滚、影响面可控」的工具集,**不一次性大改现有功能**。
|
||||||
@@ -54,38 +54,66 @@ Phase 1 已跑通 MCP 最小闭环:`httplib` + JSON-RPC 2.0,单工具 `list_
|
|||||||
|
|
||||||
`common/commands.h` 定义了完整协议,分两层:
|
`common/commands.h` 定义了完整协议,分两层:
|
||||||
|
|
||||||
- **主连接**:客户端上线后与主控保持的长连接。承载心跳(`TOKEN_HEARTBEAT`)、命令下发,以及**轻量 request/response**——进程/窗口列表、屏幕预览缩略图等。响应以 `TOKEN_*` 回到主连接(捕获点见 §4.2)。
|
- **主连接**:客户端上线后与主控保持的长连接。承载心跳(`TOKEN_HEARTBEAT`)、命令下发,以及**轻量 request/response**——屏幕预览缩略图(`TOKEN_SCREEN_PREVIEW_RSP`)、历史活动(`TOKEN_REPORT_ACTIVITY`)等。响应以 `TOKEN_*` 回到主连接(捕获点见 §4.2)。
|
||||||
- **子链接**:屏幕、终端、文件传输等**大流量/持续流**功能,服务端下 `COMMAND_*` 后客户端**新建一条 TCP 子链接**回连,数据以 `TOKEN_*` 开头,作为一个**新的 `CONTEXT_OBJECT`** 进入分派。
|
- **子链接**:屏幕、终端、文件传输、**进程/窗口列表**等功能,服务端下 `COMMAND_*` 后客户端**新建一条 TCP 子链接**回连,数据以 `TOKEN_*` 开头,作为一个**新的 `CONTEXT_OBJECT`** 进入分派。其中进程/窗口列表是**一次性子链接**(回一条 `TOKEN_PSLIST`/`TOKEN_WSLIST` 即关,见 §4.2 模式 A′),终端/屏幕/文件传输则是**持续流子链接**。
|
||||||
|
|
||||||
两者都由同一个 `MessageHandle(CONTEXT_OBJECT*)` 分派,靠 `ContextObject->GetClientID()` 关联到同一台主机。**判断一个 `TOKEN_*` 属于哪层,看它的数据落在哪个 context 上**:
|
两者都由同一个 `MessageHandle(CONTEXT_OBJECT*)` 分派,靠 `ContextObject->GetClientID()` 关联到同一台主机。**判断一个 `TOKEN_*` 属于哪层,看它的数据落在哪个 context 上**:
|
||||||
|
|
||||||
| 证据 | 结论 |
|
| 证据 | 结论 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `SystemDlg.cpp:471-499`:`GetProcessList()` 用 `m_ContextObject->Send2Client(COMMAND_PSLIST)`,`OnReceiveComplete()` 读 `m_ContextObject->InDeCompressedBuffer` | 进程/窗口列表 = **主连接 RPC** |
|
| 客户端 `KernelManager.cpp:1156` `COMMAND_SYSTEM` → `new IOCPClient` 子链接 + `LoopProcessManager`;`SystemManager.cpp:75` `GetProcessList()` 里 `szBuffer[0]=TOKEN_PSLIST`,经子链接 `Send2Server` 回传 | 进程/窗口列表 = **一次性子链接**(主连接下 `COMMAND_SYSTEM`/`COMMAND_WSLIST`,子连接回 `TOKEN_PSLIST`/`TOKEN_WSLIST`) |
|
||||||
| `2015RemoteDlg.cpp:8527-8536`:`SendScreenPreviewRequest` 用 `ctx->Send2Client(...)`;响应 `TOKEN_SCREEN_PREVIEW_RSP` 在 `MessageHandle:6173` | 屏幕预览 = **主连接 RPC** |
|
| `2015RemoteDlg.cpp:8527-8536`:`SendScreenPreviewRequest` 用 `ctx->Send2Client(...)`;响应 `TOKEN_SCREEN_PREVIEW_RSP` 在 `MessageHandle:6173` | 屏幕预览 = **主连接 RPC** |
|
||||||
| `WebService.cpp:1836-1900` + `2015RemoteDlg.cpp:6312-6351`:`COMMAND_SHELL` → 客户端建 shell 子上下文 → `TOKEN_SHELL_START`/`TOKEN_TERMINAL_START` | 终端 = **子链接** |
|
| `WebService.cpp:1836-1900` + `2015RemoteDlg.cpp:6312-6351`:`COMMAND_SHELL` → 客户端建 shell 子上下文 → `TOKEN_SHELL_START`/`TOKEN_TERMINAL_START` | 终端 = **子链接** |
|
||||||
|
|
||||||
### 4.2 模式 A:主连接 RPC(进程 / 窗口 / 截图)
|
### 4.2 模式 A:主连接 RPC(截图 / 历史活动)
|
||||||
|
|
||||||
适用于:`list_processes`、`list_windows`、`get_screenshot`(屏幕预览)。
|
适用于:`get_screenshot`(屏幕预览)、`get_activity_history`(历史活动)。
|
||||||
|
|
||||||
```
|
```
|
||||||
MCP 工具 tools/call
|
MCP 工具 tools/call
|
||||||
└─► 主 context ctx = FindHost(device_id)
|
└─► 主 context ctx = FindHost(device_id)
|
||||||
└─► ctx->Send2Client(COMMAND_PSLIST) // 主连接发 1 字节命令
|
└─► ctx->Send2Client(COMMAND_*) // 主连接发 1 字节命令
|
||||||
客户端处理后在主连接回 TOKEN_PSLIST
|
客户端处理后在主连接回 TOKEN_*
|
||||||
└─► MessageHandle 收到 TOKEN_PSLIST(数据已在 ctx->InDeCompressedBuffer)
|
└─► MessageHandle 收到 TOKEN_*(数据已在 ctx->InDeCompressedBuffer)
|
||||||
├─ if (McpServer().IsRunning() && McpServer().IsPending(devId))
|
├─ if (McpServer().IsPending(devId))
|
||||||
│ McpServer().TakeMainResponse(devId, ctx->InDeCompressedBuffer); // IO 线程同步拷贝
|
│ McpServer().TakeMainResponse(devId, ctx->GetBuffer(0), ctx->GetBufferLength()); // IO 线程同步拷贝
|
||||||
│ break; // 跳过 WM_OPENSYSTEMDIALOG
|
│ break; // 跳过 MFC 弹框(不 CancelIO,主连接不可关)
|
||||||
└─ 否则 g_2015RemoteDlg->SendMessage(WM_OPENSYSTEMDIALOG, ...) // MFC 路径
|
└─ 否则走原 MFC 路径(如打开历史活动对话框)
|
||||||
```
|
```
|
||||||
|
|
||||||
要点:
|
要点:
|
||||||
|
|
||||||
- **无子链接、无 subCtx**:数据就在主 context 缓冲里,MCP 只需在 `MessageHandle` 的对应 `TOKEN_*` 分支里**同步拷贝**到结果缓冲,再唤醒等待者。
|
- **无子链接、无 subCtx**:数据就在主 context 缓冲里,MCP 只需在 `MessageHandle` 的对应 `TOKEN_*` 分支里**同步拷贝**到结果缓冲,再唤醒等待者。
|
||||||
- **捕获点因工具而异(必须在 IO 线程同步拷贝)**:进程/窗口列表的响应在 `InDeCompressedBuffer`(`SystemDlg::OnReceiveComplete` 读它);屏幕预览的 `TOKEN_SCREEN_PREVIEW_RSP` 则**直接读本消息的 `szBuffer`/`len`**(`2015RemoteDlg.cpp:6173-6185` 已在 case 内把 `szBuffer` 拷贝到堆消息再转主线程)。两者都是每 context 单例/单消息缓冲,主连接 recv 循环处理完本条消息后即被覆盖,故拷贝必须发生在 `MessageHandle` 内部(IO 线程),不能异步等 UI 线程。
|
- **不 CancelIO**:响应落在主连接上,关闭会断开整个主机会话;故拦截后仅 `TakeMainResponse` + `break`,主连接保持。
|
||||||
- **请求关联**:`get_screenshot` 有 `reqId`(`ScreenPreviewReq.reqId`)可丢弃过期响应;进程/窗口列表**无请求 id**,靠「每 host 单飞行 + 与 MFC 对话框互斥」保证正确性(见 §4.4)。
|
- **捕获点因工具而异(必须在 IO 线程同步拷贝)**:屏幕预览的 `TOKEN_SCREEN_PREVIEW_RSP` **直接读本消息的 `szBuffer`/`len`**(`2015RemoteDlg.cpp:6173-6185` 已在 case 内把 `szBuffer` 拷贝到堆消息再转主线程);历史活动的 `TOKEN_REPORT_ACTIVITY` 读 `GetBuffer(0)`(= `InDeCompressedBuffer`)。两者都是每 context 单消息缓冲,主连接 recv 循环处理完本条消息后即被覆盖,故拷贝必须发生在 `MessageHandle` 内部(IO 线程),不能异步等 UI 线程。
|
||||||
|
- **请求关联**:`get_screenshot` 有 `reqId`(`ScreenPreviewReq.reqId`)可丢弃过期响应;`get_activity_history` **无请求 id**,靠「每 host 单飞行」(见 §4.4)保证正确性。
|
||||||
|
|
||||||
|
### 4.2′ 模式 A′:一次性子链接(进程 / 窗口列表)
|
||||||
|
|
||||||
|
适用于:`list_processes`、`list_windows`。**这是本技术书相对原设计的重要纠正**:进程/窗口列表**不是**主连接 RPC,而是一次性子链接——`COMMAND_PSLIST` 在主连接上是空操作(客户端 `KernelManager::OnReceive` 无此 case),正确的触发命令是 `COMMAND_SYSTEM`(进程)/`COMMAND_WSLIST`(窗口)。
|
||||||
|
|
||||||
|
```
|
||||||
|
MCP 工具 tools/call
|
||||||
|
└─► 主 context ctx = FindHost(device_id)
|
||||||
|
└─► ctx->Send2Client(COMMAND_SYSTEM / COMMAND_WSLIST) // 主连接发 1 字节命令
|
||||||
|
客户端 KernelManager::OnReceive 收到后新建一条子链接(new IOCPClient,EnableSubConnAuth)
|
||||||
|
└─► 子链接 ConnectServer 后先发 TOKEN_CONN_AUTH(服务端 SetID 把 clientID 钉在子 context 上)
|
||||||
|
└─► CSystemManager 构造函数立即 Send2Server(TOKEN_PSLIST / TOKEN_WSLIST)
|
||||||
|
└─► MessageHandle 在【子 context】上收到 TOKEN_PSLIST / TOKEN_WSLIST(GetClientID()==主 host id)
|
||||||
|
├─ if (McpServer().IsPending(devId))
|
||||||
|
│ McpServer().TakeMainResponse(devId, GetBuffer(0), GetBufferLength());
|
||||||
|
│ ContextObject->CancelIO(); // 用完即关一次性子链接(不关会泄漏)
|
||||||
|
│ break; // 跳过 WM_OPENSYSTEMDIALOG
|
||||||
|
└─ 否则 g_2015RemoteDlg->SendMessage(WM_OPENSYSTEMDIALOG, ...) // MFC 路径
|
||||||
|
```
|
||||||
|
|
||||||
|
要点:
|
||||||
|
|
||||||
|
- **有子链接、有 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` 构造,会随断开退出);不关会泄漏子链接。
|
||||||
|
- **与 MFC 对话框可并存**:MCP 与 MFC 对话框**各自使用独立的子链接**(客户端每次 `COMMAND_SYSTEM`/`COMMAND_WSLIST` 都新建一条),因此两者互不阻塞——MCP 的一次性调用不会导致 MFC 无法查看进程/窗口,反之亦然。无请求 id 的串扰靠「每 host 单飞行」(§4.4)规避,而非「与 MFC 对话框互斥」。
|
||||||
|
- **编码**:进程名/路径为客户端 ANSI(CP_ACP);窗口标题为客户端 UTF-8(按能力位 `CLIENT_CAP_UTF8` 判别,老客户端回落 CP_ACP)。
|
||||||
|
- **请求关联**:无请求 id → 每 host 单飞行(§4.4)。
|
||||||
|
|
||||||
> **模式 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 的退化情形,实现最简单。
|
||||||
|
|
||||||
@@ -111,41 +139,41 @@ MCP 工具 tools/call
|
|||||||
|
|
||||||
### 4.4 统一挂起请求注册表
|
### 4.4 统一挂起请求注册表
|
||||||
|
|
||||||
两种模式共用一个按 `device_id`(= `context::GetClientID()`)索引的注册表,`mode` 区分行为:
|
模式 A(主连接 RPC)与模式 A′(一次性子链接)共用一个按 `device_id`(= `context::GetClientID()`)索引的注册表。**当前实现(P2b)只覆盖 A/A′ 两种「一次性响应」**,故结构里只有 `data` + `done`,无需 `mode`/`subCtx`/`timedOut`;未来 P3a 的 `exec_command`(模式 B 流式)再扩展 `subCtx` 字段。
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
struct McpPendingRequest {
|
struct PendingRequest {
|
||||||
std::string tool;
|
std::string tool;
|
||||||
enum Mode { A_MainRpc, B_SubLink } mode;
|
std::vector<BYTE> data; // 结果缓冲(MessageHandle 内同步拷贝的字节)
|
||||||
std::mutex mtx;
|
bool done = false;
|
||||||
std::condition_variable cv; // 等待数据到达
|
|
||||||
CONTEXT_OBJECT* subCtx = nullptr; // 仅模式 B:子上下文
|
|
||||||
std::string result; // 结果缓冲(模式 A 拷贝的 buffer / 模式 B 收集的 stdout)
|
|
||||||
bool done = false;
|
|
||||||
bool timedOut = false;
|
|
||||||
};
|
};
|
||||||
std::map<uint64_t, McpPendingRequest> m_Pending; // 受 m_PendingMutex 保护
|
std::mutex m_PendingMutex;
|
||||||
|
std::condition_variable m_PendingCv;
|
||||||
|
std::map<uint64_t, PendingRequest> m_Pending; // 受 m_PendingMutex 保护
|
||||||
```
|
```
|
||||||
|
|
||||||
暴露给 `MessageHandle` 的钩子:
|
暴露给 `MessageHandle` 的钩子:
|
||||||
|
|
||||||
- `bool IsPending(uint64_t device_id)` —— 该 `TOKEN_*` 是否为 MCP 触发。
|
- `bool IsPending(uint64_t device_id)` —— 该 `TOKEN_*` 是否为 MCP 触发。
|
||||||
- `void TakeMainResponse(uint64_t device_id, const Buffer& buf)` —— 模式 A:拷贝缓冲、`done=true`、唤醒 `cv`。
|
- `void TakeMainResponse(uint64_t device_id, const BYTE* data, ULONG len)` —— 模式 A/A′:拷贝缓冲、`done=true`、唤醒 `cv`(在 IO 线程阻塞期间同步调用,拷贝安全)。
|
||||||
- `void TakeSubContext(uint64_t device_id, CONTEXT_OBJECT* ctx)` —— 模式 B:记录 `subCtx`、唤醒 `cv`。
|
|
||||||
- `void OnShellData(CONTEXT_OBJECT* ctx, const BYTE* data, ULONG len)` —— 模式 B:收集 stdout。
|
|
||||||
|
|
||||||
**每 host 单飞行**:同一 `device_id` 同一时刻只允许一个挂起请求。这是进程/窗口列表「无请求 id」的正确性前提,也是避免与 MFC 对话框争抢同一 `TOKEN_PSLIST` 响应的手段:
|
工具线程侧的方法:
|
||||||
|
|
||||||
- MCP 请求某 host 的 `list_processes` 前,若该 host 已有挂起请求或**正在被 MFC 对话框占用**(进程管理/屏幕预览浮窗开着),返回「设备忙/占用」错误。
|
- `bool BeginPending(uint64_t device_id, const std::string& tool)` —— 登记挂起(`false` = 该 host 已有挂起,返回「设备忙」)。
|
||||||
- 反过来,MFC 侧触发对话框时若发现 MCP 正挂起该 host,也应拒绝或排队(本阶段先按「互斥拒绝」实现,最简单)。
|
- `bool WaitPending(uint64_t device_id, std::vector<BYTE>& out, int timeoutMs)` —— 等待响应(超时/失败自动清理并返回 `false`)。
|
||||||
|
- `void ClearPending(uint64_t device_id)` —— 提前退出路径的清理。
|
||||||
|
|
||||||
|
**每 host 单飞行**:同一 `device_id` 同一时刻只允许一个挂起请求。这是「无请求 id」工具的正确性前提——进程/窗口/历史活动响应都不带请求 id,若同 host 并发两个请求,无法区分响应归属,故并发第二个请求返回「设备忙」(`-32003`)而非串扰。
|
||||||
|
|
||||||
|
> 说明:单飞行只约束「MCP 侧同 host 并发」,**不**约束「MCP 与 MFC 对话框并存」——二者各用独立子链接(模式 A′)或各自触发(模式 A),互不占位。详见 §4.2′。
|
||||||
|
|
||||||
### 4.5 超时与清理(MCP 与 Web 的关键差异)
|
### 4.5 超时与清理(MCP 与 Web 的关键差异)
|
||||||
|
|
||||||
Web 是长连接,子链接可长驻;MCP 是**一次性 request/response**。因此:
|
Web 是长连接,子链接可长驻;MCP 是**一次性 request/response**。因此:
|
||||||
|
|
||||||
- **超时**:`wait_for(timeout)`(建议 15–30s,可配置 `McpToolTimeoutMs`)等数据;超时返回 JSON-RPC 错误(如 `-32001`),并清理注册表。
|
- **超时**:`wait_for(timeout)` 等数据;当前 P2b 用编译期常量 `kMcpToolTimeoutMs = 20000`(暂未接配置,见 §9)。超时返回 JSON-RPC 错误(`-32001`),并清理注册表。
|
||||||
- **迟到回收**:数据在超时后才到达(`IsPending` 已清)→ 模式 A 直接丢弃该缓冲;模式 B 直接关闭迟到子上下文,不留孤儿会话。
|
- **迟到回收(已知边界)**:数据在超时后才到达(`IsPending` 已清)时,拦截分支不成立,响应会**回落到原 MFC 路径**(进程/窗口弹 `WM_OPENSYSTEMDIALOG`,历史活动弹对话框)——属「客户端在 20s 内未响应」这一病态场景的罕见副作用,不会串扰或崩溃。正常路径(<1s 内响应)不受影响。若未来要彻底消除,可给注册表加「超时墓碑 + 宽限期丢弃」。
|
||||||
- **用完即关**:模式 B 工具收集到结果后主动关闭子上下文(与对话框/Web 的长生命周期不同)。
|
- **用完即关**:模式 A′(进程/窗口)拦截后 `CancelIO()` 关一次性子链接;模式 A(主连接 RPC)不关主连接。未来模式 B(终端)收集完 stdout 后主动关子上下文。
|
||||||
- **阻塞 httplib worker**:`wait_for` 会阻塞 `HandleMcp` 所在的 httplib worker 线程。httplib 默认线程池约 8 线程,阻塞一个 20–30s 会占住一个 worker。MCP 属低频串行调用(AI 逐条调用),**本阶段可接受**;但需在 §12 显式声明,若未来需并发改用「提交任务 + 轮询结果」的异步模型。
|
- **阻塞 httplib worker**:`wait_for` 会阻塞 `HandleMcp` 所在的 httplib worker 线程。httplib 默认线程池约 8 线程,阻塞一个 20–30s 会占住一个 worker。MCP 属低频串行调用(AI 逐条调用),**本阶段可接受**;但需在 §12 显式声明,若未来需并发改用「提交任务 + 轮询结果」的异步模型。
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -154,16 +182,16 @@ Web 是长连接,子链接可长驻;MCP 是**一次性 request/response**。
|
|||||||
|
|
||||||
### 5.1 工具总表
|
### 5.1 工具总表
|
||||||
|
|
||||||
> 「模式」标注实现路径(A=主连接 RPC,A0=主连接单向命令[无响应],B=子链接流式,—=纯内存)。
|
> 「模式」标注实现路径(A=主连接 RPC,A′=一次性子链接,A0=主连接单向命令[无响应],B=子链接流式,—=纯内存)。
|
||||||
|
|
||||||
| 阶段 | 工具 | 功能 | 复用协议/函数 | 模式 | 风险 |
|
| 阶段 | 工具 | 功能 | 复用协议/函数 | 模式 | 风险 |
|
||||||
|------|------|------|---------------|------|------|
|
|------|------|------|---------------|------|------|
|
||||||
| P1(已交付) | `list_online_hosts` | 在线主机列表 | `BuildHostJson` | — | 只读 |
|
| P1(已交付) | `list_online_hosts` | 在线主机列表 | `BuildHostJson` | — | 只读 |
|
||||||
| P2a(已实现) | `search_hosts` | 按名称/IP/分组/OS/在线态过滤 | `list_online_hosts` 内存过滤 | — | 只读 |
|
| P2a(已实现并验证) | `search_hosts` | 按名称/IP/分组/OS/在线态过滤 | `list_online_hosts` 内存过滤 | — | 只读 |
|
||||||
| P2a(已实现) | `get_host_detail` | 单机详情(不含活动历史) | 现成 device 字段(`BuildHostJson`) | — | 只读 |
|
| P2a(已实现并验证) | `get_host_detail` | 单机详情(不含活动历史) | 现成 device 字段(`BuildHostJson`) | — | 只读 |
|
||||||
| P2b | `list_processes` | 主机进程列表 | `COMMAND_PSLIST`→`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`/`COMMAND_LIST_FILES`→`TOKEN_DRIVE_LIST`/`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** | **写** |
|
||||||
@@ -189,7 +217,7 @@ Web 是长连接,子链接可长驻;MCP 是**一次性 request/response**。
|
|||||||
|
|
||||||
> 每期都给出:改动文件、接口、测试点、回滚方式。原则是**每期可独立合入、独立验收、独立回滚**。
|
> 每期都给出:改动文件、接口、测试点、回滚方式。原则是**每期可独立合入、独立验收、独立回滚**。
|
||||||
|
|
||||||
### 6.1 P2a:`search_hosts` + `get_host_detail`(纯内存,零子链接)✅ 已实现
|
### 6.1 P2a:`search_hosts` + `get_host_detail`(纯内存,零子链接)✅ 已实现并验证
|
||||||
|
|
||||||
**改动**:仅 `McpServer.cpp`。
|
**改动**:仅 `McpServer.cpp`。
|
||||||
- `BuildToolsListResult` 增两个工具描述 + schema。
|
- `BuildToolsListResult` 增两个工具描述 + schema。
|
||||||
@@ -202,20 +230,36 @@ Web 是长连接,子链接可长驻;MCP 是**一次性 request/response**。
|
|||||||
|
|
||||||
> **范围修正**:原计划 `get_host_detail` 附带「活动历史」,但活动历史是 `COMMAND_QUERY_ACTIVITY`(159)→`TOKEN_REPORT_ACTIVITY`(160) 的**主连接往返**(客户端上报、服务端不落库),非纯内存字段,违反 P2a「纯内存」约束。故 P2a 的 `get_host_detail` 只返回主机实时字段,活动历史单列为 P2b 的 `get_activity_history`(模式 A)。
|
> **范围修正**:原计划 `get_host_detail` 附带「活动历史」,但活动历史是 `COMMAND_QUERY_ACTIVITY`(159)→`TOKEN_REPORT_ACTIVITY`(160) 的**主连接往返**(客户端上报、服务端不落库),非纯内存字段,违反 P2a「纯内存」约束。故 P2a 的 `get_host_detail` 只返回主机实时字段,活动历史单列为 P2b 的 `get_activity_history`(模式 A)。
|
||||||
|
|
||||||
### 6.2 P2b:`list_processes` + `list_windows`(模式 A,首次无头接管)
|
### 6.2 P2b:`list_processes` + `list_windows` + `get_activity_history`(模式 A′ / 模式 A,首次无头接管)✅ 已实现并验证
|
||||||
|
|
||||||
|
> 本节已按实际实现修订:进程/窗口是**一次性子链接**(模式 A′),历史活动是**主连接 RPC**(模式 A),见 §4.2/§4.2′。
|
||||||
|
|
||||||
**改动**:
|
**改动**:
|
||||||
- `McpServer.h/.cpp`:加 §4.4 的 `m_Pending` 注册表、`IsPending`/`TakeMainResponse`、发起/等待/超时清理、每 host 单飞行检查。
|
|
||||||
- `2015RemoteDlg.cpp` 的 `MessageHandle`:`TOKEN_PSLIST`/`TOKEN_WSLIST` 分支加 `if (McpServer().IsPending(devId)) { TakeMainResponse(...); break; }`(**在 IO 线程同步拷贝 `InDeCompressedBuffer`**)。
|
|
||||||
- 工具分支:`FindHost(id)` → 登记挂起 → `ctx->Send2Client(COMMAND_PSLIST)` → 等 `cv` → 取结果 → 解析为 JSON 返回。
|
|
||||||
|
|
||||||
**影响面**:`MessageHandle` 只新增 `if` 分支,不影响现有 MFC 弹框路径(非 MCP 触发时行为不变)。
|
- `McpServer.h/.cpp`:
|
||||||
|
- 加 §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` 对齐引入的尾部零字节。
|
||||||
|
- 加三个工具:`list_processes`(发 `COMMAND_SYSTEM`)、`list_windows`(发 `COMMAND_WSLIST`)、`get_activity_history`(发 `COMMAND_QUERY_ACTIVITY`)。
|
||||||
|
- `2015RemoteDlg.cpp` 的 `MessageHandle`:
|
||||||
|
- `TOKEN_PSLIST`/`TOKEN_WSLIST` 分支(子 context):`if (McpServer().IsPending(devId)) { TakeMainResponse(devId, GetBuffer(0), GetBufferLength()); ContextObject->CancelIO(); break; }`(**在 UI 线程、IO 线程阻塞期间同步拷贝**;用完即关子链接)。
|
||||||
|
- `TOKEN_REPORT_ACTIVITY` 分支(主 context):`if (McpServer().IsPending(devId)) { TakeMainResponse(...); break; }`(**不 CancelIO**,主连接不可关)。
|
||||||
|
|
||||||
**验收**:MFC 双击进程管理弹框仍正常(回归点);MCP `list_processes` 返回 PID/名称/CPU/内存;`list_windows` 返回窗口列表;超时(如对离线主机)返回明确错误而非挂死;同 host 并发第二次调用返回「设备忙」。
|
**影响面**:`MessageHandle` 只新增 `if` 分支,不影响现有 MFC 弹框路径(非 MCP 触发时 `IsPending` 为假,行为不变)。
|
||||||
|
|
||||||
**回滚**:revert `McpServer` + `MessageHandle` 两处改动。
|
**验收**:
|
||||||
|
- MFC 双击进程/窗口管理弹框仍正常(回归点);MFC 历史活动对话框仍正常(回归点)。
|
||||||
|
- MCP `list_processes` 返回 `[{pid,name,arch,path}]`;`list_windows` 返回 `[{hwnd,title,status,pid}]`;`get_activity_history` 返回 `{records:[...], activityHistory}`(UTF-8 文本,逐行拆记录)。
|
||||||
|
- 超时(如对离线主机)返回 `-32001` 而非挂死;同 host 并发第二次调用返回 `-32003`「设备忙」;发送失败返回 `-32004`。
|
||||||
|
- 中文进程名/窗口标题无乱码(进程名 CP_ACP、窗口标题按能力位 UTF-8)。
|
||||||
|
|
||||||
> 同属 P2b 的 `get_activity_history`(`COMMAND_QUERY_ACTIVITY`→`TOKEN_REPORT_ACTIVITY`,主连接 RPC)机制一致,不再单列改动。
|
**验证记录**(2026-08-19,真实在线主机实测):
|
||||||
|
- `list_processes`(Windows 11 主机):`pid`/`name`/`arch`/`path` 完整;`pid==0` 仅 1 条(合法 `[System Process]`),无尾部零填充空记录——padding 防护生效。
|
||||||
|
- `list_windows`:`hwnd`/`title`/`status`/`pid` 完整,无空标题记录;中文标题(「搭建 MCP 架构…」「微信」)UTF-8 正确。
|
||||||
|
- `get_activity_history`:返回 `[时间] [标题] 时长` 记录,UTF-8 正确。
|
||||||
|
- 错误路径实测:缺 `id` → `-32602`;非数字 `id` → `-32602`;未知/离线主机 → `-32002`;错误 token → HTTP 401。
|
||||||
|
- **客户端版本门槛**:`get_activity_history` 依赖客户端 `2026-08-15`(提交 `29929e4`)加入的 `COMMAND_QUERY_ACTIVITY` 处理;更早的客户端(如 `Jul 14 2026`)不识别该命令、无响应,服务端 20s 超时返回 `-32001`——属预期行为,非 bug。测试须选 `Aug 15 2026` 及之后的客户端。
|
||||||
|
|
||||||
|
**回滚**:revert `McpServer.h` + `McpServer.cpp` + `MessageHandle` 两处 `if` 分支。
|
||||||
|
|
||||||
### 6.3 P2c:`get_screenshot` + `list_files`(模式 A)
|
### 6.3 P2c:`get_screenshot` + `list_files`(模式 A)
|
||||||
|
|
||||||
@@ -261,7 +305,8 @@ Web 是长连接,子链接可长驻;MCP 是**一次性 request/response**。
|
|||||||
4. **实现数据通路**:
|
4. **实现数据通路**:
|
||||||
- 纯内存/现成字段 → 直接实现,不进 `MessageHandle`。
|
- 纯内存/现成字段 → 直接实现,不进 `MessageHandle`。
|
||||||
- 模式 A0(单向命令)→ 直接 `Send2Client` 后返回,无需挂起/超时。
|
- 模式 A0(单向命令)→ 直接 `Send2Client` 后返回,无需挂起/超时。
|
||||||
- 模式 A → 加 `m_Pending` 挂起 + `MessageHandle` 对应 `TOKEN_*` 加 `if (IsPending) TakeMainResponse`(IO 线程同步拷贝)。
|
- 模式 A → 加 `m_Pending` 挂起 + `MessageHandle` 对应 `TOKEN_*` 加 `if (IsPending) TakeMainResponse`(IO 线程同步拷贝,**不 CancelIO**)。
|
||||||
|
- 模式 A′ → 同模式 A,但拦截后额外 `ContextObject->CancelIO()` 关一次性子链接。
|
||||||
- 模式 B → 加 `m_Pending` 挂起 + `MessageHandle` 对应 `TOKEN_*` 加 `if (IsPending) TakeSubContext` + `OnShellData` 泵数据。
|
- 模式 B → 加 `m_Pending` 挂起 + `MessageHandle` 对应 `TOKEN_*` 加 `if (IsPending) TakeSubContext` + `OnShellData` 泵数据。
|
||||||
5. **超时与清理**:带超时、迟到回收、用完即关(模式 B);每 host 单飞行(§4.5)。
|
5. **超时与清理**:带超时、迟到回收、用完即关(模式 B);每 host 单飞行(§4.5)。
|
||||||
6. **验收与回滚**:写 §11 的 DoD 清单,确认回归点(对应 MFC 弹框路径不变)。
|
6. **验收与回滚**:写 §11 的 DoD 清单,确认回归点(对应 MFC 弹框路径不变)。
|
||||||
@@ -290,6 +335,8 @@ Web 是长连接,子链接可长驻;MCP 是**一次性 request/response**。
|
|||||||
|
|
||||||
延续 Phase 1 的配置风格(`settings` 段 + env 覆盖 + `UIBranding` 开关):
|
延续 Phase 1 的配置风格(`settings` 段 + env 覆盖 + `UIBranding` 开关):
|
||||||
|
|
||||||
|
> **配置存储位置(重要)**:`THIS_CFG` 在 **Release 版恒走注册表** `HKCU\Software\YAMA\settings`(`2015Remote.cpp:238` 的 `#else` 分支 `new iniFile`);`settings.ini` 仅在 **Debug 版**(`#ifdef _DEBUG` 且 `GetPwdHash()==masterHash`)才读,否则同样走注册表。故 Release 版改 MCP 配置应走「扩展 → MCP设置」对话框(写注册表)或直接改注册表,手改 `settings.ini` 对 Release 无效。
|
||||||
|
|
||||||
| 配置键 | 默认 | 说明 |
|
| 配置键 | 默认 | 说明 |
|
||||||
|--------|------|------|
|
|--------|------|------|
|
||||||
| `McpEnabled` | 0 | 总开关(已有) |
|
| `McpEnabled` | 0 | 总开关(已有) |
|
||||||
@@ -298,7 +345,7 @@ Web 是长连接,子链接可长驻;MCP 是**一次性 request/response**。
|
|||||||
| `McpToken` | 空 | 静态 token(已有) |
|
| `McpToken` | 空 | 静态 token(已有) |
|
||||||
| `McpReadonly` | 1 | **P3 新增**:1=仅只读工具,0=开放写工具(配合完整 token) |
|
| `McpReadonly` | 1 | **P3 新增**:1=仅只读工具,0=开放写工具(配合完整 token) |
|
||||||
| `McpCmdWhitelist` | 内置只读命令集 | **P3 新增**:`exec_command` 白名单(分号分隔) |
|
| `McpCmdWhitelist` | 内置只读命令集 | **P3 新增**:`exec_command` 白名单(分号分隔) |
|
||||||
| `McpToolTimeoutMs` | 20000 | **P2b 新增**:数据等待超时 |
|
| `McpToolTimeoutMs` | 20000 | **P2b 新增**:数据等待超时(当前为 `McpServer.cpp` 内编译期常量 `kMcpToolTimeoutMs`,未接配置) |
|
||||||
|
|
||||||
新增写能力时,`UIBranding.h`/`FeatureFlags.h` 加对应隐藏/特性位,保持「品牌定制可裁剪」的既有约定。
|
新增写能力时,`UIBranding.h`/`FeatureFlags.h` 加对应隐藏/特性位,保持「品牌定制可裁剪」的既有约定。
|
||||||
|
|
||||||
@@ -337,7 +384,7 @@ Web 是长连接,子链接可长驻;MCP 是**一次性 request/response**。
|
|||||||
|------|------|
|
|------|------|
|
||||||
| 无头接管串扰 MFC/Web 路径 | 挂起标记按 `device_id` 唯一;`if (IsPending)` 仅在 MCP 触发时成立,其余路径原样 |
|
| 无头接管串扰 MFC/Web 路径 | 挂起标记按 `device_id` 唯一;`if (IsPending)` 仅在 MCP 触发时成立,其余路径原样 |
|
||||||
| 共享 `InDeCompressedBuffer` 被覆盖 | 模式 A 在 IO 线程 `MessageHandle` 内同步拷贝,不等异步 |
|
| 共享 `InDeCompressedBuffer` 被覆盖 | 模式 A 在 IO 线程 `MessageHandle` 内同步拷贝,不等异步 |
|
||||||
| 无请求 id 的工具响应串扰 | 每 host 单飞行 + 与 MFC 对话框互斥(§4.4) |
|
| 无请求 id 的工具响应串扰 | 每 host 单飞行(§4.4);MCP 与 MFC 各用独立子链接,互不占位(§4.2′) |
|
||||||
| 阻塞 httplib worker 饿死后续请求 | 低频串行可接受;文档显式声明,未来并发改「任务+轮询」(§4.5) |
|
| 阻塞 httplib worker 饿死后续请求 | 低频串行可接受;文档显式声明,未来并发改「任务+轮询」(§4.5) |
|
||||||
| 超时/挂死 | 所有工具强制 `wait_for(timeout)` + 迟到回收 |
|
| 超时/挂死 | 所有工具强制 `wait_for(timeout)` + 迟到回收 |
|
||||||
| 写操作越权 | P3 前三道评审边界(白名单/审计/分组授权)+ `McpReadonly` 默认 1 |
|
| 写操作越权 | P3 前三道评审边界(白名单/审计/分组授权)+ `McpReadonly` 默认 1 |
|
||||||
@@ -351,7 +398,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`)——首次引入**模式 A 主连接 RPC 无头接管**,把 §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`)——补上「看」的能力,仍是只读、模式 A。
|
||||||
4. **P3a**(`exec_command`)——首次引入**模式 B 子链接流式**,在 P2 的挂起机制成熟后,走完安全评审再落地写操作。
|
4. **P3a**(`exec_command`)——首次引入**模式 B 子链接流式**,在 P2 的挂起机制成熟后,走完安全评审再落地写操作。
|
||||||
|
|
||||||
|
|||||||
@@ -5473,6 +5473,13 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TOKEN_REPORT_ACTIVITY: {
|
case TOKEN_REPORT_ACTIVITY: {
|
||||||
|
// P2b:MCP 挂起时接管。历史活动为「主连接 RPC」(无子链接),响应回主 context,
|
||||||
|
// 不能 CancelIO(会断开主连接),仅同步拷贝后 break。
|
||||||
|
uint64_t devId = ContextObject->GetClientID();
|
||||||
|
if (McpServer().IsPending(devId)) {
|
||||||
|
McpServer().TakeMainResponse(devId, ContextObject->GetBuffer(0), ContextObject->GetBufferLength());
|
||||||
|
break;
|
||||||
|
}
|
||||||
// 一次性快照:直接打开“历史活动”对话框展示,不占用 hDlg(避免与运行日志对话框冲突)
|
// 一次性快照:直接打开“历史活动”对话框展示,不占用 hDlg(避免与运行日志对话框冲突)
|
||||||
std::string text((char*)(szBuffer + 1), len > 1 ? len - 1 : 0);
|
std::string text((char*)(szBuffer + 1), len > 1 ? len - 1 : 0);
|
||||||
CActivityDialog* dlg = new CActivityDialog(this, ContextObject->GetServer(), ContextObject);
|
CActivityDialog* dlg = new CActivityDialog(this, ContextObject->GetServer(), ContextObject);
|
||||||
@@ -6351,6 +6358,14 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
|||||||
}
|
}
|
||||||
case TOKEN_WSLIST: // 窗口管理【x】
|
case TOKEN_WSLIST: // 窗口管理【x】
|
||||||
case TOKEN_PSLIST: { // 进程管理【x】
|
case TOKEN_PSLIST: { // 进程管理【x】
|
||||||
|
// P2b:MCP 挂起时接管。进程/窗口列表无请求 id,按 device_id 匹配并同步拷贝
|
||||||
|
// (此时 IO 线程阻塞在 NotifyProc,缓冲尚未被覆盖,拷贝安全)。
|
||||||
|
uint64_t devId = ContextObject->GetClientID();
|
||||||
|
if (McpServer().IsPending(devId)) {
|
||||||
|
McpServer().TakeMainResponse(devId, ContextObject->GetBuffer(0), ContextObject->GetBufferLength());
|
||||||
|
ContextObject->CancelIO(); // 用完即关,避免子链接泄漏(仿 WebService 终端关闭)
|
||||||
|
break;
|
||||||
|
}
|
||||||
g_2015RemoteDlg->SendMessage(WM_OPENSYSTEMDIALOG, 0, (LPARAM)ContextObject);
|
g_2015RemoteDlg->SendMessage(WM_OPENSYSTEMDIALOG, 0, (LPARAM)ContextObject);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,9 @@
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
// P2b 工具等待响应的超时(ms)。MCP 一次性请求:等待子连接回传进程/窗口列表。
|
||||||
|
static const int kMcpToolTimeoutMs = 20000;
|
||||||
|
|
||||||
// Json::Value → 紧凑 JSON 字符串
|
// Json::Value → 紧凑 JSON 字符串
|
||||||
std::string JsonToString(const Json::Value& v) {
|
std::string JsonToString(const Json::Value& v) {
|
||||||
Json::StreamWriterBuilder b;
|
Json::StreamWriterBuilder b;
|
||||||
@@ -111,6 +114,155 @@ std::string GetStringArg(const Json::Value& args, const char* key) {
|
|||||||
return JsonStrField(args, key);
|
return JsonStrField(args, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== P2b 辅助 ==========
|
||||||
|
|
||||||
|
// 解析 id 入参(必填、纯数字)为 uint64;非法返回 false。
|
||||||
|
bool ParseHostIdArg(const Json::Value& args, uint64_t& out, std::string& err) {
|
||||||
|
std::string sid = GetStringArg(args, "id");
|
||||||
|
if (sid.empty()) {
|
||||||
|
err = "Missing required parameter: id";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!IsDigits(sid)) {
|
||||||
|
err = "Invalid id: expected a decimal host id string";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out = strtoull(sid.c_str(), nullptr, 10);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在 m_HostList 中按 clientID 找在线主 context(复刻 CollectOnlineHosts 的锁内遍历)。
|
||||||
|
context* FindMainContext(CMy2015RemoteDlg* parent, uint64_t id) {
|
||||||
|
if (!parent) return nullptr;
|
||||||
|
context* found = nullptr;
|
||||||
|
EnterCriticalSection(&parent->m_cs);
|
||||||
|
for (context* ctx : parent->m_HostList) {
|
||||||
|
if (ctx && ctx->GetClientID() == id && ctx->IsLogin()) {
|
||||||
|
found = ctx;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LeaveCriticalSection(&parent->m_cs);
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 有界 strlen:p 最多可读 avail 字节,返回 n < avail 表示遇到 '\0'。
|
||||||
|
size_t BoundedStrlen(const char* p, size_t avail) {
|
||||||
|
size_t n = 0;
|
||||||
|
while (n < avail && p[n]) ++n;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从源编码 cp 转为 UTF-8(输出到 JSON)。空/失败返回 ""。
|
||||||
|
std::string ToUtf8(const char* s, UINT cp) {
|
||||||
|
if (!s || !*s) return "";
|
||||||
|
int wlen = MultiByteToWideChar(cp, 0, s, -1, NULL, 0);
|
||||||
|
if (wlen <= 0) return "";
|
||||||
|
std::wstring w(wlen - 1, L'\0');
|
||||||
|
MultiByteToWideChar(cp, 0, s, -1, &w[0], wlen);
|
||||||
|
int u8len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, NULL, 0, NULL, NULL);
|
||||||
|
if (u8len <= 0) return "";
|
||||||
|
std::string out(u8len - 1, '\0');
|
||||||
|
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, &out[0], u8len, NULL, NULL);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 "title|status|pid|r1|r2" 解析(自末尾 4 个 '|' 反推),兼容只有标题的老客户端。
|
||||||
|
// 逻辑复刻 SystemDlg.cpp::ParseWindowAttrs。
|
||||||
|
void ParseWindowAttrsStr(const char* s, std::string& title, std::string& status, uint64_t& pid) {
|
||||||
|
title.clear();
|
||||||
|
status = "normal";
|
||||||
|
pid = 0;
|
||||||
|
if (!s || !*s) return;
|
||||||
|
std::string t(s);
|
||||||
|
int len = (int)t.size();
|
||||||
|
int pipePos[4] = { -1, -1, -1, -1 };
|
||||||
|
int cnt = 0;
|
||||||
|
for (int i = len - 1; i >= 0 && cnt < 4; --i) {
|
||||||
|
if (t[i] == '|') pipePos[cnt++] = i;
|
||||||
|
}
|
||||||
|
if (cnt < 4) { // 老格式:只有标题
|
||||||
|
title = t;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pid = strtoull(t.c_str() + pipePos[2] + 1, nullptr, 10);
|
||||||
|
status = t.substr(pipePos[3] + 1, pipePos[2] - pipePos[3] - 1);
|
||||||
|
title = t.substr(0, pipePos[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 TOKEN_PSLIST 缓冲(data[0]=token,其后为 [pid:4][name:arch\0][path\0] 记录)。
|
||||||
|
// 进程名/路径来自客户端 ANSI(CP_ACP),转 UTF-8 输出。
|
||||||
|
Json::Value ParseProcessList(const std::vector<BYTE>& data) {
|
||||||
|
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 + sizeof(DWORD) <= len) {
|
||||||
|
DWORD pid = *(const DWORD*)(p + off);
|
||||||
|
off += sizeof(DWORD);
|
||||||
|
|
||||||
|
const char* exeFile = p + off;
|
||||||
|
size_t exeLen = BoundedStrlen(exeFile, len - off);
|
||||||
|
if (exeLen >= len - off) break; // 未以 '\0' 结尾,异常数据
|
||||||
|
if (exeLen == 0) break; // 空进程名 = 尾部零填充(LocalSize 对齐),停止解析
|
||||||
|
off += exeLen + 1;
|
||||||
|
|
||||||
|
const char* fullPath = p + off;
|
||||||
|
size_t pathLen = BoundedStrlen(fullPath, len - off);
|
||||||
|
if (pathLen >= len - off) break;
|
||||||
|
off += pathLen + 1;
|
||||||
|
|
||||||
|
std::string name(exeFile, exeLen);
|
||||||
|
std::string arch;
|
||||||
|
size_t colon = name.find(':');
|
||||||
|
if (colon != std::string::npos) {
|
||||||
|
arch = name.substr(colon + 1);
|
||||||
|
name = name.substr(0, colon);
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value item(Json::objectValue);
|
||||||
|
item["pid"] = (Json::UInt64)pid;
|
||||||
|
item["name"] = ToUtf8(name.c_str(), CP_ACP);
|
||||||
|
item["arch"] = arch.empty() ? "N/A" : ToUtf8(arch.c_str(), CP_ACP);
|
||||||
|
item["path"] = ToUtf8(fullPath, CP_ACP);
|
||||||
|
arr.append(item);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 TOKEN_WSLIST 缓冲(data[0]=token,其后为 [hwnd:4][title|status|pid|r1|r2\0] 记录)。
|
||||||
|
// 窗口标题为客户端 UTF-8(老客户端为 CP_ACP),用 cp 解码后转 UTF-8 输出。
|
||||||
|
Json::Value ParseWindowList(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;
|
||||||
|
while (off + sizeof(DWORD) <= len) {
|
||||||
|
DWORD hwnd = *(const DWORD*)(p + off);
|
||||||
|
off += sizeof(DWORD);
|
||||||
|
|
||||||
|
const char* titleWithAttrs = p + off;
|
||||||
|
size_t tlen = BoundedStrlen(titleWithAttrs, len - off);
|
||||||
|
if (tlen >= len - off) break;
|
||||||
|
if (tlen == 0) break; // 空记录 = 尾部零填充(LocalSize 对齐),停止解析
|
||||||
|
off += tlen + 1;
|
||||||
|
|
||||||
|
std::string title, status;
|
||||||
|
uint64_t pid = 0;
|
||||||
|
ParseWindowAttrsStr(titleWithAttrs, title, status, pid);
|
||||||
|
|
||||||
|
Json::Value item(Json::objectValue);
|
||||||
|
item["hwnd"] = (Json::UInt64)hwnd;
|
||||||
|
item["title"] = ToUtf8(title.c_str(), cp);
|
||||||
|
item["status"] = status;
|
||||||
|
item["pid"] = (Json::UInt64)pid;
|
||||||
|
arr.append(item);
|
||||||
|
}
|
||||||
|
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;
|
||||||
@@ -218,6 +370,92 @@ Json::Value BuildGetHostDetailInputSchema() {
|
|||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// list_processes 的 outputSchema(processes 数组)
|
||||||
|
Json::Value BuildProcessListOutputSchema() {
|
||||||
|
Json::Value props(Json::objectValue);
|
||||||
|
|
||||||
|
Json::Value procsProp(Json::objectValue);
|
||||||
|
procsProp["type"] = "array";
|
||||||
|
Json::Value items(Json::objectValue);
|
||||||
|
items["type"] = "object";
|
||||||
|
Json::Value itemProps(Json::objectValue);
|
||||||
|
Json::Value pidProp(Json::objectValue);
|
||||||
|
pidProp["type"] = "integer";
|
||||||
|
itemProps["pid"] = pidProp;
|
||||||
|
const char* strFields[] = { "name", "arch", "path" };
|
||||||
|
for (const char* f : strFields) {
|
||||||
|
Json::Value s(Json::objectValue);
|
||||||
|
s["type"] = "string";
|
||||||
|
itemProps[f] = s;
|
||||||
|
}
|
||||||
|
items["properties"] = itemProps;
|
||||||
|
procsProp["items"] = items;
|
||||||
|
props["processes"] = procsProp;
|
||||||
|
|
||||||
|
Json::Value schema(Json::objectValue);
|
||||||
|
schema["type"] = "object";
|
||||||
|
schema["properties"] = props;
|
||||||
|
Json::Value required(Json::arrayValue);
|
||||||
|
required.append("processes");
|
||||||
|
schema["required"] = required;
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
// list_windows 的 outputSchema(windows 数组)
|
||||||
|
Json::Value BuildWindowListOutputSchema() {
|
||||||
|
Json::Value props(Json::objectValue);
|
||||||
|
|
||||||
|
Json::Value winsProp(Json::objectValue);
|
||||||
|
winsProp["type"] = "array";
|
||||||
|
Json::Value items(Json::objectValue);
|
||||||
|
items["type"] = "object";
|
||||||
|
Json::Value itemProps(Json::objectValue);
|
||||||
|
Json::Value hwndProp(Json::objectValue);
|
||||||
|
hwndProp["type"] = "integer";
|
||||||
|
itemProps["hwnd"] = hwndProp;
|
||||||
|
Json::Value pidProp(Json::objectValue);
|
||||||
|
pidProp["type"] = "integer";
|
||||||
|
itemProps["pid"] = pidProp;
|
||||||
|
const char* strFields[] = { "title", "status" };
|
||||||
|
for (const char* f : strFields) {
|
||||||
|
Json::Value s(Json::objectValue);
|
||||||
|
s["type"] = "string";
|
||||||
|
itemProps[f] = s;
|
||||||
|
}
|
||||||
|
items["properties"] = itemProps;
|
||||||
|
winsProp["items"] = items;
|
||||||
|
props["windows"] = winsProp;
|
||||||
|
|
||||||
|
Json::Value schema(Json::objectValue);
|
||||||
|
schema["type"] = "object";
|
||||||
|
schema["properties"] = props;
|
||||||
|
Json::Value required(Json::arrayValue);
|
||||||
|
required.append("windows");
|
||||||
|
schema["required"] = required;
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get_activity_history 的 outputSchema(records 数组 + 原始文本)
|
||||||
|
Json::Value BuildActivityHistoryOutputSchema() {
|
||||||
|
Json::Value props(Json::objectValue);
|
||||||
|
|
||||||
|
Json::Value recordsProp(Json::objectValue);
|
||||||
|
recordsProp["type"] = "array";
|
||||||
|
Json::Value items(Json::objectValue);
|
||||||
|
items["type"] = "string";
|
||||||
|
recordsProp["items"] = items;
|
||||||
|
props["records"] = recordsProp;
|
||||||
|
|
||||||
|
Json::Value rawProp(Json::objectValue);
|
||||||
|
rawProp["type"] = "string";
|
||||||
|
props["activityHistory"] = rawProp;
|
||||||
|
|
||||||
|
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);
|
||||||
@@ -266,6 +504,42 @@ std::string BuildToolsListResult(const Json::Value& id) {
|
|||||||
tools.append(tool);
|
tools.append(tool);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4) list_processes(P2b:主连接 RPC,进程列表)
|
||||||
|
{
|
||||||
|
Json::Value tool(Json::objectValue);
|
||||||
|
tool["name"] = "list_processes";
|
||||||
|
tool["description"] = u8"获取指定在线主机的进程列表(PID、映像名称、架构、程序完整路径)。通过主连接下发命令、子连接回传,一次性返回。";
|
||||||
|
|
||||||
|
tool["inputSchema"] = BuildGetHostDetailInputSchema(); // 复用 { id } 必填 schema
|
||||||
|
tool["outputSchema"] = BuildProcessListOutputSchema();
|
||||||
|
|
||||||
|
tools.append(tool);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) list_windows(P2b:主连接 RPC,窗口列表)
|
||||||
|
{
|
||||||
|
Json::Value tool(Json::objectValue);
|
||||||
|
tool["name"] = "list_windows";
|
||||||
|
tool["description"] = u8"获取指定在线主机的顶层窗口列表(句柄、窗口标题、窗口状态、所属进程 PID)。一次性返回。";
|
||||||
|
|
||||||
|
tool["inputSchema"] = BuildGetHostDetailInputSchema(); // 复用 { id } 必填 schema
|
||||||
|
tool["outputSchema"] = BuildWindowListOutputSchema();
|
||||||
|
|
||||||
|
tools.append(tool);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6) get_activity_history(P2b:主连接 RPC,历史活动记录)
|
||||||
|
{
|
||||||
|
Json::Value tool(Json::objectValue);
|
||||||
|
tool["name"] = "get_activity_history";
|
||||||
|
tool["description"] = u8"获取指定在线主机的历史活动记录(前台窗口驻留时长,每行一条「[时间] [标题] 时长」)。主连接 RPC、一次性返回。";
|
||||||
|
|
||||||
|
tool["inputSchema"] = BuildGetHostDetailInputSchema(); // 复用 { id } 必填 schema
|
||||||
|
tool["outputSchema"] = BuildActivityHistoryOutputSchema();
|
||||||
|
|
||||||
|
tools.append(tool);
|
||||||
|
}
|
||||||
|
|
||||||
result["tools"] = tools;
|
result["tools"] = tools;
|
||||||
return BuildResult(id, result);
|
return BuildResult(id, result);
|
||||||
}
|
}
|
||||||
@@ -375,6 +649,156 @@ std::string BuildGetHostDetail(const Json::Value& id, const Json::Value& args, C
|
|||||||
return BuildError(id, -32002, "Host not found or offline: " + sid);
|
return BuildError(id, -32002, "Host not found or offline: " + sid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tools/call:list_processes(主连接 RPC,子连接一次性回传)
|
||||||
|
std::string BuildListProcesses(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));
|
||||||
|
|
||||||
|
CMcpServer& mcp = CMcpServer::Instance();
|
||||||
|
if (!mcp.BeginPending(devId, "list_processes"))
|
||||||
|
return BuildError(id, -32003, "Device busy: another request is pending for this host");
|
||||||
|
|
||||||
|
BYTE cmd = COMMAND_SYSTEM;
|
||||||
|
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 process list");
|
||||||
|
|
||||||
|
Json::Value procs = ParseProcessList(data);
|
||||||
|
int count = (int)procs.size();
|
||||||
|
|
||||||
|
Json::Value result(Json::objectValue);
|
||||||
|
Json::Value structuredContent(Json::objectValue);
|
||||||
|
structuredContent["processes"] = procs;
|
||||||
|
result["structuredContent"] = structuredContent;
|
||||||
|
|
||||||
|
Json::Value content(Json::arrayValue);
|
||||||
|
Json::Value item(Json::objectValue);
|
||||||
|
item["type"] = "text";
|
||||||
|
item["text"] = 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:list_windows(主连接 RPC,子连接一次性回传)
|
||||||
|
std::string BuildListWindows(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));
|
||||||
|
|
||||||
|
CMcpServer& mcp = CMcpServer::Instance();
|
||||||
|
if (!mcp.BeginPending(devId, "list_windows"))
|
||||||
|
return BuildError(id, -32003, "Device busy: another request is pending for this host");
|
||||||
|
|
||||||
|
BYTE cmd = COMMAND_WSLIST;
|
||||||
|
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 window list");
|
||||||
|
|
||||||
|
// 窗口标题编码由客户端能力位决定(新客户端 UTF-8,老客户端 CP_ACP)。
|
||||||
|
UINT cp = GetClientEncoding(ctx);
|
||||||
|
Json::Value wins = ParseWindowList(data, cp);
|
||||||
|
int count = (int)wins.size();
|
||||||
|
|
||||||
|
Json::Value result(Json::objectValue);
|
||||||
|
Json::Value structuredContent(Json::objectValue);
|
||||||
|
structuredContent["windows"] = wins;
|
||||||
|
result["structuredContent"] = structuredContent;
|
||||||
|
|
||||||
|
Json::Value content(Json::arrayValue);
|
||||||
|
Json::Value item(Json::objectValue);
|
||||||
|
item["type"] = "text";
|
||||||
|
item["text"] = 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:get_activity_history(主连接 RPC,无子链接、不弹框、不 CancelIO)
|
||||||
|
std::string BuildGetActivityHistory(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));
|
||||||
|
|
||||||
|
CMcpServer& mcp = CMcpServer::Instance();
|
||||||
|
if (!mcp.BeginPending(devId, "get_activity_history"))
|
||||||
|
return BuildError(id, -32003, "Device busy: another request is pending for this host");
|
||||||
|
|
||||||
|
BYTE cmd = COMMAND_QUERY_ACTIVITY;
|
||||||
|
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 activity history");
|
||||||
|
|
||||||
|
// data[0]=token,其后为客户端 ActivityHistory::Dump() 的 UTF-8 纯文本
|
||||||
|
// (标题在客户端已由 GetActiveWindowTitle 转 UTF-8,服务端无需再转码)。
|
||||||
|
std::string text;
|
||||||
|
if (data.size() > 1)
|
||||||
|
text.assign((const char*)data.data() + 1, data.size() - 1);
|
||||||
|
|
||||||
|
// 拆行:每行一条记录,过滤空行(含 Dump 末尾的换行)。
|
||||||
|
Json::Value records(Json::arrayValue);
|
||||||
|
size_t start = 0;
|
||||||
|
while (start <= text.size()) {
|
||||||
|
size_t nl = text.find('\n', start);
|
||||||
|
std::string line = text.substr(start, (nl == std::string::npos ? text.size() : nl) - start);
|
||||||
|
if (!line.empty()) records.append(line);
|
||||||
|
if (nl == std::string::npos) break;
|
||||||
|
start = nl + 1;
|
||||||
|
}
|
||||||
|
int count = (int)records.size();
|
||||||
|
|
||||||
|
Json::Value result(Json::objectValue);
|
||||||
|
Json::Value structuredContent(Json::objectValue);
|
||||||
|
structuredContent["records"] = records;
|
||||||
|
structuredContent["activityHistory"] = text;
|
||||||
|
result["structuredContent"] = structuredContent;
|
||||||
|
|
||||||
|
Json::Value content(Json::arrayValue);
|
||||||
|
Json::Value item(Json::objectValue);
|
||||||
|
item["type"] = "text";
|
||||||
|
item["text"] = 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"];
|
||||||
@@ -390,6 +814,9 @@ std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
|
|||||||
if (toolName == "list_online_hosts") return BuildListOnlineHosts(id, parent);
|
if (toolName == "list_online_hosts") return BuildListOnlineHosts(id, parent);
|
||||||
if (toolName == "search_hosts") return BuildSearchHosts(id, args, parent);
|
if (toolName == "search_hosts") return BuildSearchHosts(id, args, parent);
|
||||||
if (toolName == "get_host_detail") return BuildGetHostDetail(id, args, parent);
|
if (toolName == "get_host_detail") return BuildGetHostDetail(id, args, parent);
|
||||||
|
if (toolName == "list_processes") return BuildListProcesses(id, args, parent);
|
||||||
|
if (toolName == "list_windows") return BuildListWindows(id, args, parent);
|
||||||
|
if (toolName == "get_activity_history") return BuildGetActivityHistory(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));
|
||||||
@@ -493,6 +920,52 @@ void CMcpServer::HandleMcp(const httplib::Request& req, httplib::Response& res)
|
|||||||
res.set_content(BuildError(root["id"], -32601, "Method not found"), "application/json");
|
res.set_content(BuildError(root["id"], -32601, "Method not found"), "application/json");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== P2b 挂起请求注册表实现 =====
|
||||||
|
|
||||||
|
bool CMcpServer::IsPending(uint64_t device_id) {
|
||||||
|
std::lock_guard<std::mutex> lk(m_PendingMutex);
|
||||||
|
return m_Pending.find(device_id) != m_Pending.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMcpServer::TakeMainResponse(uint64_t device_id, 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; // 已超时清理 → 迟到数据,丢弃
|
||||||
|
it->second.data.assign(data, data + len);
|
||||||
|
it->second.done = true;
|
||||||
|
m_PendingCv.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CMcpServer::BeginPending(uint64_t device_id, const std::string& tool) {
|
||||||
|
std::lock_guard<std::mutex> lk(m_PendingMutex);
|
||||||
|
if (m_Pending.find(device_id) != m_Pending.end()) return false; // 设备忙
|
||||||
|
PendingRequest r;
|
||||||
|
r.tool = tool;
|
||||||
|
m_Pending[device_id] = std::move(r);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CMcpServer::WaitPending(uint64_t device_id, std::vector<BYTE>& out, int timeoutMs) {
|
||||||
|
std::unique_lock<std::mutex> lk(m_PendingMutex);
|
||||||
|
auto it = m_Pending.find(device_id);
|
||||||
|
if (it == m_Pending.end()) return false;
|
||||||
|
|
||||||
|
bool signaled = m_PendingCv.wait_for(lk, std::chrono::milliseconds(timeoutMs),
|
||||||
|
[&] { return it->second.done; });
|
||||||
|
if (!signaled || it->second.data.empty()) {
|
||||||
|
m_Pending.erase(it); // 超时/空数据 → 清理
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out = std::move(it->second.data);
|
||||||
|
m_Pending.erase(it);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMcpServer::ClearPending(uint64_t device_id) {
|
||||||
|
std::lock_guard<std::mutex> lk(m_PendingMutex);
|
||||||
|
m_Pending.erase(device_id);
|
||||||
|
}
|
||||||
|
|
||||||
// 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);
|
||||||
|
|||||||
@@ -5,6 +5,11 @@
|
|||||||
#include <thread>
|
#include <thread>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <mutex>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <map>
|
||||||
|
#include <vector>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
// httplib 与 Windows 头部的 min/max 宏冲突,按 file_server.h 的既有约定处理。
|
// httplib 与 Windows 头部的 min/max 宏冲突,按 file_server.h 的既有约定处理。
|
||||||
#undef min
|
#undef min
|
||||||
@@ -35,6 +40,28 @@ public:
|
|||||||
void Stop();
|
void Stop();
|
||||||
bool IsRunning() const { return m_running.load(); }
|
bool IsRunning() const { return m_running.load(); }
|
||||||
|
|
||||||
|
// ===== P2b:无请求 id 的一次性响应 → 每 host 单飞行 =====
|
||||||
|
// 进程/窗口列表(TOKEN_PSLIST/TOKEN_WSLIST,一次性子链接)与历史活动
|
||||||
|
// (TOKEN_REPORT_ACTIVITY,主连接 RPC)都不带请求关联,同一 host 同一时刻
|
||||||
|
// 只允许一个挂起请求,避免同 host 并发请求响应归属歧义。
|
||||||
|
// 注意:单飞行只约束 MCP 侧同 host 并发,不约束与 MFC 对话框并存(各用独立子链接)。
|
||||||
|
|
||||||
|
// 该 host 是否已有挂起请求(MessageHandle 在 TOKEN_PSLIST/TOKEN_WSLIST 分支调用)。
|
||||||
|
bool IsPending(uint64_t device_id);
|
||||||
|
|
||||||
|
// 拷贝响应缓冲到挂起结果并唤醒等待者(在 MessageHandle 内同步调用,数据此时仍在
|
||||||
|
// context 缓冲中、IO 线程被阻塞,拷贝是安全的)。
|
||||||
|
void TakeMainResponse(uint64_t device_id, const BYTE* data, ULONG len);
|
||||||
|
|
||||||
|
// 工具线程:登记挂起(false = 该 host 已有挂起请求,设备忙)。
|
||||||
|
bool BeginPending(uint64_t device_id, const std::string& tool);
|
||||||
|
|
||||||
|
// 工具线程:等待响应;成功返回 true 并把缓冲写入 out,超时/失败返回 false(并清理)。
|
||||||
|
bool WaitPending(uint64_t device_id, std::vector<BYTE>& out, int timeoutMs);
|
||||||
|
|
||||||
|
// 工具线程:清理挂起状态(发送失败等提前退出路径)。
|
||||||
|
void ClearPending(uint64_t device_id);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
CMcpServer();
|
CMcpServer();
|
||||||
~CMcpServer();
|
~CMcpServer();
|
||||||
@@ -49,6 +76,16 @@ private:
|
|||||||
std::atomic<bool> m_running{false};
|
std::atomic<bool> m_running{false};
|
||||||
std::string m_token;
|
std::string m_token;
|
||||||
CMy2015RemoteDlg* m_parent = nullptr;
|
CMy2015RemoteDlg* m_parent = nullptr;
|
||||||
|
|
||||||
|
// 挂起请求注册表(受 m_PendingMutex 保护,键 = device_id)。
|
||||||
|
struct PendingRequest {
|
||||||
|
std::string tool;
|
||||||
|
std::vector<BYTE> data;
|
||||||
|
bool done = false;
|
||||||
|
};
|
||||||
|
std::mutex m_PendingMutex;
|
||||||
|
std::condition_variable m_PendingCv;
|
||||||
|
std::map<uint64_t, PendingRequest> m_Pending;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 全局访问器(仿 WebService(),见 WebService.h 末尾)
|
// 全局访问器(仿 WebService(),见 WebService.h 末尾)
|
||||||
|
|||||||
Reference in New Issue
Block a user