diff --git a/docs/Mcp_Design.md b/docs/Mcp_Design.md index df41e17..73f9ddc 100644 --- a/docs/Mcp_Design.md +++ b/docs/Mcp_Design.md @@ -147,7 +147,8 @@ MCP 是 JSON-RPC 2.0。MVP 只需实现以下方法: "online":{"type":"boolean"}, "group":{"type":"string"}, "screen":{"type":"string"}, - "clientType":{"type":"string"} + "clientType":{"type":"string"}, + "tls":{"type":"boolean"} } } } @@ -231,6 +232,7 @@ Json::Value BuildHostJson(context* ctx, _ClientList* clientMap); - 基础标识:`id` / `name` / `remark` / `ip` / `os` / `location` / `group` / `version` / `clientType` / `screen` - 实时信息:`rtt`(延迟)、`activeWindow`(当前活动窗口,即被控端正在做什么) +- 连接凭据:`tls`(布尔,对应在线主机列表的 TLS 列;`true` 表示该客户端本轮以 TLS 模式登录,是**登录时的凭据**而非实时探测;老客户端/空值一律 `false`) 输出结构 `{"hosts": [...]}`,每台主机一个对象,字段与 §3.3.2 的 `outputSchema` 一致。 diff --git a/docs/Mcp_Machine_Design.md b/docs/Mcp_Machine_Design.md new file mode 100644 index 0000000..c0a7527 --- /dev/null +++ b/docs/Mcp_Machine_Design.md @@ -0,0 +1,273 @@ +# MCP P8 设计:在线主机 TLS 字段 / 连接管理 / 主机管理 + +> 延续 `Mcp_Design.md` → `Mcp_Phase2_Design.md`(P2)→ P3~P6 → `Mcp_Manage_Design.md`(P7)。 +> P7 复用的是「系统/窗口/服务」对话框使用的 `COMMAND_SYSTEM`、`COMMAND_WSLIST`、 +> `COMMAND_SERVICECONFIG` 链路;本轮要覆盖的「连接管理」与「主机管理」走的是**另一套** +> 命令(`COMMAND_CONNECTION` / `COMMAND_MACHINE_MANAGE`),必须在同一子连接上收发多步, +> 因此新增一层会话抽象,并为工具族增加独立开关。 +> +> 安全门结论(已确认):新增独立 `host_*` 工具族;写入范围限定「只读 + 可恢复操作」; +> 开关分两层——`McpMachine`(工具族总开关)与 `McpMachineDangerous`(高风险子集), +> 均默认关闭,并与 `McpReadonly` 叠加。现有 27 个工具(含 P7)一律不改行为。 + +## 1. 范围 + +| 交付 | 内容 | +|---|---| +| TLS 字段 | `list_online_hosts` / `search_hosts` / `get_host_detail` 增加 `tls` | +| 连接管理 | `list_network_connections`、`kill_network_connection` | +| 主机管理读 | 10 个标签页各一个:`host_list_processes` … `host_get_hosts_file` | +| 主机管理写 | `host_control_process` / `host_control_window` / `host_control_service` / `host_control_task` | +| 配置 | 设置对话框两个复选框 + 启动接线(`McpMachine` / `McpMachineDangerous`) | + +不做的(明确排除):kill 进程、关闭窗口、删除服务/任务、卸载软件、写 hosts、 +创建任务、强制删除文件、代码注入。 + +## 2. TLS 字段 + +- `HostJson.cpp` 的 `BuildHostJson()` 末尾追加 + `device["tls"] = (ctx->GetClientData(ONLINELIST_TLS) == "Y");`。 + 数据源是登录时写入的 `ONLINELIST_TLS`(`2015RemoteDlg.cpp` 收到 `RES_TLS=="1"` 时置 `"Y"`)。 +- 语义:**登录凭据**,不是实时探测。老客户端不上报该值 → `false`。 +- `BuildHostJson` 同时被 WebService 的设备列表复用,Web 端也会多出该字段(向后兼容)。 +- `get_host_detail` 复用 `BuildHostItemSchema()`,自动带上。 + +## 3. 会话架构 + +两条新链路都需要「主连接发一条命令 → 客户端新建子连接 → 子连接回就绪包 → +在同一子连接上多次收发」,现有 `m_Pending` 只能等主连接上的单包。做法是给 +`PendingRequest` 增加 `kind`,**复用同一张表** `m_Pending`(从而天然保证「单设备单飞行」), +而不是另开一套状态。 + +```cpp +enum class PendingKind { Generic, FollowUp, Machine, ConnMgr }; +``` + +| 成员 | 用途 | +|---|---| +| `m_SubCtxToDevice` | 已绑定的子连接 → device;绑定的子连接上所有包都由 MCP 消费 | +| `m_AbsorbedSubCtx` | `CancelIO` 后仍可能到达的包:静默丢弃 | +| `m_OrphanByDevice` / `m_OrphanByPeer` | 5 秒墓碑,防止迟到的就绪包弹出 MFC 对话框 | + +### 3.1 时序(工具线程) + +``` +BeginMachineSession(dev, tool, mainCtx) // 登记单飞行;忙 → -32003 + → 主连接 Send2Client(COMMAND_MACHINE_MANAGE) + → 客户端开 CMachineManager 子连接,回 TOKEN_SYSINFOLIST(61) + → OnManagedSubPacket 绑定子连接、保存首包、notify_all +WaitManagedPacket(dev, 61, …, 5000) // 就绪 + +每次读: + ArmManagedResponse(dev, 期望 token) + → 子连接 Send2Client([tab 0..9]) / WaitSendIdle + → WaitManagedPacket(dev, 期望 token, …, 12000) + +EndManagedSession(dev, cancelIo=true) // 关闭子连接,不留长连接 +``` + +整体 60 秒会话截止;每一步取 `min(step, deadline - now)`。所有超时返回 `-32001`。 + +### 3.2 子连接路由(`OnManagedSubPacket`) + +在 `3.1` 的模型下,UI 线程收到的每个包先经过钩子 +(`MessageHandle` 中终端拦截块之后、`switch (cmd)` 之前): + +1. 已绑定 → 只保留 `expectedToken` 匹配的包;`TOKEN_MACHINE_MSG(42)` 存为 + `clientError`(CP936→UTF-8);其余丢弃。已绑定的子连接上的一切包都归 MCP。 +2. `m_AbsorbedSubCtx` 命中 → 丢弃。 +3. 未绑定且 token 不是 `61` / `255` → 交回主 switch(MFC 行为不变)。 +4. 未绑定且是 `61` / `255`: + - 该 context 就是主连接(`FindMainContext(parent, cid)`)→ 交回主 switch; + - 否则在同 device 的未绑定会话中按 `clientID` 精确绑定; + - 老客户端子连接 `clientID` 可能为 0 → 按 `peer` 兜底,**仅当同 peer 唯一**时才绑定; + - 都不命中:墓碑未过期 → 吸收 + `CancelIO` + 消费;否则交回主 switch + (用户手动打开对话框)。 + +### 3.3 必须同步调整的现有语义 + +- `IsPending()` 排除 `Machine` / `ConnMgr`:否则会话在飞时用户点「进程管理」, + `case TOKEN_PSLIST` 会把界面包抢走。 +- `TakeMainResponse()` 顶部对 managed kind 直接返回。 +- `OfflineProc` 在 `OnManageSubClosed` 之后、`RemoveFromHostList` **之前**调用 + `OnManagedContextClosed`(否则连接池复用同一 context 指针后,新连接的包会被误吞)。 +- 锁序:`parent->m_cs` → `m_PendingMutex`;持锁期间禁止 `Send2Client` / `CancelIO` / + `WaitSendIdle`。 + +### 3.4 健壮性 + +- UI 线程只做「有界拷贝 + 保存 + 唤醒」:单包上限 8 MiB,超限置 `clientError` 并 `CancelIO`。 +- 解析全部在 MCP 工作线程上做;字符串一律用 `BoundedStrlen` / 显式偏移, + 未对齐的多字节整数用 `memcpy`,禁止指针强转。 +- 客户端部分列表用 `Send(buf, LocalSize(buf))` 发送,长度可能带出少量尾随 0 填充: + 记录解析循环遇到「剩余全 0」即结束,定长记录列表忽略不足一条记录的尾部。 + +## 4. 连接管理 + +### 4.1 协议 + +- 主连接:`COMMAND_CONNECTION(253)`;客户端建 `CConnectionsManager` 子连接。 +- 子连接回 `[255][u16 LE count][ConnectionInfo × count]`。 +- `ConnectionInfo`(`common/commands.h`,`#pragma pack(1)`,110 字节,已 `static_assert`): + `proto(1)` / `af(1)` / `pid(4)` / `localAddr[16]` / `localPort(2,主机序)` / + `remoteAddr[16]` / `remotePort(2,主机序)` / `state(4)` / `procName[64](UTF-8)`。 + 地址为网络序;IPv4 取前 4 字节,`af == AF_INET(2)`,IPv6 为 `AF_INET6(23)`。 +- 强制断开:子连接发 `[254][原始 110 字节记录]`;客户端 `SetTcpEntry(DELETE_TCB)` + 后自动重发一次 255。 + +### 4.2 工具 + +| 工具 | 门控 | 说明 | +|---|---|---| +| `list_network_connections` | `McpMachine` | 过滤 `protocol/family/pid/process/local_port/remote_port/remote_address/state` + 分页;输出 `killable = (proto==1 && af==AF_INET)` | +| `kill_network_connection` | `McpMachine && McpManage && !McpReadonly` | 必填四元组;可选 `pid` 消歧、`dry_run` | + +`kill_network_connection` 的流程刻意避免「重新拼地址」: + +1. 取最新列表,**按原始 110 字节记录精确匹配**四元组(可选同时校验 PID); +2. 匹配 0 条 → `-32009`(调用方应刷新);匹配多条 → `-32602`(要求给 `pid`); +3. 目标不是 IPv4 TCP → `-32005`,不发包; +4. `dry_run` → 直接返回目标; +5. `Arm(255)` → 发 `[254][原记录]` → 等刷新列表 → 复核该四元组是否消失。 + +客户端 `SetTcpEntry` 的返回值被忽略且需要管理员权限,所以「断开后仍在」返回 +`structuredContent.ok=false, reason="still_present"`,**绝不谎报成功**。 +每次调用写审计:`tcp a:p->b:q pid=N result=…`。 + +## 5. 主机管理 + +### 5.1 协议 + +- 主连接:`COMMAND_MACHINE_MANAGE(162)` → 客户端 `CMachineManager` 子连接回 + `TOKEN_SYSINFOLIST(61)`。 +- 之后在同一子连接上发 **1 字节 tab**(0–9),响应 token 31–42。 +- 动作 10–30;多数动作客户端会自动回一份刷新后的列表(服务 = 40,任务 = 41), + 进程冻结/解冻(16/17)与窗口操作(11)**不回包**。 + +`MachineManager` 枚举(`common/commands.h`):请求 0 PROCESS / 1 WINDOWS / 2 NETSTATE / +3 SOFTWARE / 4 HTML / 5 FAVORITES / 6 WIN32SERVICE / 7 DRIVERSERVICE / 8 TASK / 9 HOSTS; +响应 31 PROCESS / 32 WINDOWS / 33 NETSTATE / 34 SOFTWARE / 35 HTML / 36 FAVORITES / +37 WIN32SERVICE / 38 DRIVERSERVICE / 39 HOSTS / 40 SERVICE_LIST / 41 TASKLIST / 42 MACHINE_MSG。 + +### 5.2 读工具 + +| 工具 | tab | 响应 | 主要过滤 | +|---|---|---|---| +| `host_list_processes` | 0 | 31 | `pid` / `name` / `arch` | +| `host_list_windows` | 1 | 32 | `pid` / `title` | +| `host_list_network_state` | 2 | 33 | `pid` / `process` / `protocol` / `local_port` / `remote_port` / `state` | +| `host_list_software` | 3 | 34 | `name` / `publisher` | +| `host_list_browser_history` | 4 | 35 | `title` / `url` | +| `host_list_favorites` | 5 | 36 | `name` / `url` | +| `host_list_win32_services` | 6 | 40 | `name` / `status` / `start_type` | +| `host_list_driver_services` | 7 | 40 | 同上 | +| `host_list_scheduled_tasks` | 8 | 41 | `name` / `folder` / `status` | +| `host_get_hosts_file` | 9 | 39 | `include_content` / `max_bytes` | + +公共分页 `offset`(默认 0)/ `limit`(默认 100,上限 1000);输出 +`total`(过滤后)/ `totalAll` / `offset` / `limit` / `hasMore`。 + +### 5.3 记录格式(与 `CMachineDlg::Show*` 逐字段对齐) + +| 列表 | 格式 | +|---|---| +| 进程 31 | `DWORD pid`、`bool is64`、`title[260]`、`exe`+pad、`path`+pad、`DWORD priClass`、`DWORD threads`、`user`+pad、`DWORD workingSetKb`、`DWORD fileSizeKb`;CP936 | +| 窗口 32 | `WINDOWSINFO` 定长 1044 字节;**只含可见窗口**;空列表 = 1 字节 | +| 网络状态 33 | 进程名、`DWORD pid`、`[TCP]/[UDP]`、`ip:port`、远端、状态,各串 +2;UDP 远端 `*.*.*.*:*`、状态为空;按最后一个 `:` 拆地址 | +| 软件 34 | 5 个串各 +2:显示名、发布者、版本、安装日期、卸载命令 | +| 浏览记录 35 | 定长 2148:`time[100]` + `title[1024]` + `url[1024]` | +| 收藏夹 36 | 2 个串各 +2 | +| 服务 40 | 8 个串各 +1 NUL:显示名、描述、状态、启动类型、登录身份、交互式、**服务名**、可执行路径 | +| 计划任务 41 | `name/path/exe` 为 UTF-8、`status` 为 GBK,各 +1 NUL;两个 `DATE`;两个 0 字节结束;`folder` 由 `path` 去掉末尾 `"\"` 派生 | +| hosts 39 | `[39][文件原文][0]`;有 BOM → UTF-8,否则合法 UTF-8 → UTF-8,否则 CP936 | + +### 5.4 写工具 + +统一模式:**先取列表校验目标存在 → 发动作 → 再取列表复核**。目标不在最新快照里 +返回 `-32009`,绝不盲发。 + +| 工具 | action | 额外门控 | 动作包 | 复核 | +|---|---|---|---|---| +| `host_control_process` | `freeze` / `thaw` | `McpMachineDangerous` | `[16/17][DWORD pid]`(无回包) | 重新取 tab 0:PID 仍在 | +| `host_control_window` | `restore/hide/maximize/minimize` | — | `[11][DWORD hwnd][DWORD SW_*]` + 零填充到 20 字节(客户端只读前 9 字节) | 重新取 tab 1:hide 后 hwnd 应消失,其余应仍在 | +| `host_control_service` | `start`/`pause`/`continue` | — | `[22/24/25][服务名 CP936][NUL]` | 客户端自动回 40 | +| `host_control_service` | `stop`/`restart` | `McpMachineDangerous` | stop = `[23]`;restart = stop → 等 40 → start | 等 40(12 s),状态在期望集合内 | +| `host_control_task` | `run` / `stop` | — | `[29/28][folder CP936][NUL][name CP936][NUL]` | 客户端自动回 41:任务仍在 + 当前状态 | + +补充规则: + +- 服务动作前先取一次服务列表(prime),既校验目标,也把客户端 `m_dwServiceType` + 钉到正确类型(Win32 / 驱动)。 +- 已是目标状态时不下发动作,返回 `ok=true, changed=false, reason=already_*`。 +- 冻结/解冻、窗口动作、任务动作**没有回执**:`verified` 只表示「已用动作后的快照复核」, + 不表示「动作一定生效」。冻结不改变进程存在性,因此只能确认目标仍在。 +- 写工具输入串(服务名、任务 folder/name)必须能转成 CP936,否则 `-32602`,不发包。 +- 审计格式:`host : action=… target=… result=…`。 + +## 6. 门控矩阵 + +| 配置 | 可见工具 | 直接调用被隐藏的工具 | +|---|---|---| +| `McpMachine=0`(默认) | 0 个 | `-32006` | +| `McpMachine=1, McpReadonly=1` | 11 个读工具(`list_network_connections` + 10 个 `host_list_*` / `host_get_hosts_file`) | 5 个写工具 → `-32006` | +| `McpMachine=1, McpReadonly=0, McpManage=1` | 16 个(含 5 个写) | — | +| `McpMachine=1, McpReadonly=0, McpManage=0` | 15 个(无 `kill_network_connection`) | `kill_network_connection` → `-32006` | +| `McpMachineDangerous=0` | 工具仍可见 | `host_control_process`、`host_control_service{stop,restart}` → `-32006` | + +设置对话框勾选「危险主机操作」但未勾「主机管理」时,保存会清除危险开关并提示。 + +## 7. 错误码 + +沿用 P1–P7 的约定,新增 `-32009`: + +| 码 | 含义 | +|---|---| +| `-32602` | 参数非法(含:目标多条匹配、字符无法用 CP936 表示) | +| `-32000` | 解析失败 / 客户端返回错误文本(`TOKEN_MACHINE_MSG`) | +| `-32001` | 超时(就绪 5 s / 列表 12 s / 会话 60 s) | +| `-32002` | 主机离线 | +| `-32003` | 设备忙(已有 MCP 会话在飞:单设备单飞行) | +| `-32004` | 发送失败 | +| `-32005` | 不支持(非 Windows 客户端;UDP/IPv6 断连) | +| `-32006` | 开关未开启 | +| `-32009` | **目标在最新快照中不存在**(调用方应刷新后重试) | + +## 8. 已知限制 + +1. **token 数值碰撞**:机器响应 31–42 与 `COMMAND_KEYBOARD/AUDIO/SYSTEM/SHELL/…` 同值。 + 靠「先按子连接身份路由」解决——未绑定的子连接不会按 token 猜测。 +2. **UI 争抢就绪包**:用户恰好在 MCP 请求期间点开同一对话框时,首包可能被 MCP 绑定, + 界面对话框这次不弹(与 P7 的取舍一致),重试即可。 +3. **5 s 墓碑**:MCP 异常结束后 5 s 内手动打开同一台主机的对话框可能被吞,重试即可。 +4. **老客户端子连接 `clientID=0`**:靠 peer 兜底,仅在同 peer 唯一会话时绑定。 +5. **权限**:`SetTcpEntry`、冻结/解冻、服务控制需要客户端管理员;失败按「未生效」如实返回。 +6. **编码**:机器协议以 CP936 为主;任务 `name/path/exe` 为 UTF-8、`status` 为 GBK; + 写回输入统一转 CP936。GBK 不可表示的字符明确报 `-32602`。 +7. **`host_list_network_state` 只有 IPv4**:数据来自客户端自身的 `GetTcpTable` 视图, + 与「连接管理」(`ConnectionInfo`,含 IPv6)不是同一份数据。 +8. **`McpMachineDangerous` 只覆盖本轮高风险动作**;后续加入 kill/删除/卸载/注入时, + 必须按 `McpMachine && McpMachineDangerous && !McpReadonly` 三层校验。 +9. **`host_control_window {action:"hide"}` 是单向的**:客户端只枚举可见窗口,隐藏后该 hwnd + 从快照中消失,再发 `restore` 会在发包前被 `-32009` 拦下(MFC 对话框同样无法再操作它)。 + 实测确认:`hide` 复核为 `state:"hidden"`,随后 `restore` 返回 `-32009`。 +10. **`host_control_service {action:"pause"}` 依赖服务自身支持 PAUSE**:实测虚拟机上的 + `Spooler` 下发 PAUSE 后状态仍为 `Running`,工具如实返回 `ok:false, reason:"unexpected_state"` + (不谎报成功)。`start/stop/continue/restart` 在同一台机器上均通过。 +11. **开关只在进程启动时读取**(Release 版配置来自注册表 `HKCU\Software\YAMA\settings`, + 不是 `Bin\settings.ini`);在「MCP设置」里改动 `McpMachine` / `McpMachineDangerous` / + `McpReadonly` 等必须重启服务端才生效。未重启时 `tools/list` 里看不到新工具, + 直接调用返回 `-32006`。 + +## 9. 验证要点 + +- TLS:`RES_TLS=1` 的主机三个工具都返回 `tls:true`;老客户端 `false`。 +- 路由:MCP 关闭时手动打开「主机管理」「连接管理」照常工作; + `host_list_favorites`(token 36,与键盘等命令同值)在飞时手动打开「进程管理」, + 两边都不串包。 +- 迟到包:强制就绪超时后,61/255 到达应被吸收且不弹对话框。 +- 单飞行:同主机并发两个 host/connection 工具,第二个 `-32003`;两台主机并发都成功。 +- 连接管理:与对话框逐条对比;断开后复核消失;UDP/IPv6 → `-32005`;过期四元组 → `-32009`。 +- 10 个读工具:与 MFC 标签页对比行数、首末行、中文编码;空 tab 返回 `total:0`。 +- 4 个写工具:freeze→thaw、窗口四动作、服务五动作、任务 run/stop;过期目标发包前 `-32009`。 +- 回归:现有 27 个工具、终端、远程控制、文件传输、10 个 MFC 标签页行为不变。 +- 资源:连续调用 20 次 host 工具,服务端无残留子连接。 diff --git a/server/2015Remote/2015RemoteDlg.cpp b/server/2015Remote/2015RemoteDlg.cpp index 2968775..a5da873 100644 --- a/server/2015Remote/2015RemoteDlg.cpp +++ b/server/2015Remote/2015RemoteDlg.cpp @@ -2418,6 +2418,11 @@ BOOL CMy2015RemoteDlg::OnInitDialog() McpServer().SetFileTransferEnabled(THIS_CFG.GetInt("settings", "McpFileTransfer", 0) != 0); // P7 管理控制开关:默认关;要求只读关(McpReadonly=0)才生效(工具列表/分派双重门控)。 McpServer().SetManageEnabled(THIS_CFG.GetInt("settings", "McpManage", 0) != 0); + // P8 主机管理 / 连接管理工具族:总开关默认关(关闭时 16 个工具都不可见)。 + // 危险子集(进程冻结/解冻、服务停止/重启)在总开关之上再叠一道,默认关。 + McpServer().SetMachineEnabled(THIS_CFG.GetInt("settings", "McpMachine", 0) != 0); + McpServer().SetMachineDangerousEnabled( + THIS_CFG.GetInt("settings", "McpMachineDangerous", 0) != 0); if (!McpServer().Start(mcpBind, mcpPort)) { Mprintf("McpServer start failed on %s:%d\n", mcpBind.c_str(), mcpPort); } else { @@ -5516,6 +5521,12 @@ BOOL CALLBACK CMy2015RemoteDlg::OfflineProc(CONTEXT_OBJECT* ContextObject) McpServer().OnManageSubClosed(ContextObject); } + // MCP P8 主机管理 / 连接管理会话的子连接断开:清路由表、唤醒等待线程。必须在 + // RemoveFromHostList 之前,否则连接池复用同一 context 指针后新连接的包会被误吞。 + if (McpServer().IsRunning()) { + McpServer().OnManagedContextClosed(ContextObject); + } + SOCKET nSocket = ContextObject->sClientSocket; CDialogBase* p = (CDialogBase*)ContextObject->hDlg; @@ -5878,6 +5889,14 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject) return; } + // ===== MCP 主机管理 / 连接管理:子连接路由(P8)===== + // 已绑定的子连接上的所有包由 MCP 消费;未绑定的子连接只有就绪包 + // TOKEN_SYSINFOLIST(61) / TOKEN_CONNECTION_LIST(255) 会被尝试绑定,其余原样交回 + // 下方 MFC 分发(用户手动打开对话框的行为不变)。无会话在飞时走无锁快速路径。 + if (McpServer().OnManagedSubPacket(ContextObject, szBuffer, len)) { + return; + } + // 【L】:主机上下线和授权 // 【x】:对话框相关功能 switch (cmd) { diff --git a/server/2015Remote/2015Remote_vs2015.vcxproj b/server/2015Remote/2015Remote_vs2015.vcxproj index 7c91e4b..d2079a1 100644 --- a/server/2015Remote/2015Remote_vs2015.vcxproj +++ b/server/2015Remote/2015Remote_vs2015.vcxproj @@ -328,6 +328,8 @@ + + @@ -397,6 +399,8 @@ + + diff --git a/server/2015Remote/2015Remote_vs2015.vcxproj.filters b/server/2015Remote/2015Remote_vs2015.vcxproj.filters index 07dcd44..ef0252b 100644 --- a/server/2015Remote/2015Remote_vs2015.vcxproj.filters +++ b/server/2015Remote/2015Remote_vs2015.vcxproj.filters @@ -89,6 +89,8 @@ + + @@ -203,6 +205,8 @@ + + diff --git a/server/2015Remote/HostJson.cpp b/server/2015Remote/HostJson.cpp index 11438d2..62d1fda 100644 --- a/server/2015Remote/HostJson.cpp +++ b/server/2015Remote/HostJson.cpp @@ -114,5 +114,9 @@ Json::Value BuildHostJson(context* ctx, _ClientList* clientMap) { CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE); device["clientType"] = AnsiToUtf8(clientType); // e.g. "MAC", "LNX", "EXE" + // 登录时上报的 TLS 传输标志(RES_TLS=="1" → "Y")。老客户端不上报,取值为 "N"。 + // 注意这是登录凭据而不是实时探测:连接建立后若配置发生变化,需要客户端重连才会更新。 + device["tls"] = (ctx->GetClientData(ONLINELIST_TLS) == "Y"); + return device; } diff --git a/server/2015Remote/McpInternal.cpp b/server/2015Remote/McpInternal.cpp new file mode 100644 index 0000000..eb9230b --- /dev/null +++ b/server/2015Remote/McpInternal.cpp @@ -0,0 +1,172 @@ +#include "stdafx.h" +#include "McpInternal.h" +#include "context.h" // context 接口(GetClientID/IsLogin) +#include "2015RemoteDlg.h" // CMy2015RemoteDlg 成员(m_HostList/m_cs)+ WM_SHOWERRORMSG +#include "LangManager.h" // _TR + +#ifndef _WIN64 +#ifdef _DEBUG +#pragma comment(lib, "jsoncpp/jsoncppd.lib") +#else +#pragma comment(lib, "jsoncpp/jsoncpp.lib") +#endif +#else +#ifdef _DEBUG +#pragma comment(lib, "jsoncpp/jsoncpp_x64d.lib") +#else +#pragma comment(lib, "jsoncpp/jsoncpp_x64.lib") +#endif +#endif + +namespace McpInternal { + +std::string JsonToString(const Json::Value& v) { + Json::StreamWriterBuilder b; + b["indentation"] = ""; + return Json::writeString(b, v); +} + +std::string BuildResult(const Json::Value& id, const Json::Value& result) { + Json::Value resp(Json::objectValue); + resp["jsonrpc"] = "2.0"; + resp["id"] = id; + resp["result"] = result; + return JsonToString(resp); +} + +std::string BuildError(const Json::Value& id, int code, const std::string& msg) { + Json::Value resp(Json::objectValue); + resp["jsonrpc"] = "2.0"; + resp["id"] = id; + Json::Value err(Json::objectValue); + err["code"] = code; + err["message"] = msg; + resp["error"] = err; + return JsonToString(resp); +} + +std::string ToLowerAscii(const std::string& s) { + std::string r = s; + for (char& c : r) if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a'); + return r; +} + +bool ContainsCI(const std::string& haystack, const std::string& needle) { + if (needle.empty()) return true; + return ToLowerAscii(haystack).find(ToLowerAscii(needle)) != std::string::npos; +} + +std::string JsonStrField(const Json::Value& v, const char* key) { + if (v.isObject() && v.isMember(key) && v[key].isString()) + return v[key].asString(); + return ""; +} + +bool IsDigits(const std::string& s) { + if (s.empty()) return false; + for (char c : s) if (c < '0' || c > '9') return false; + return true; +} + +std::string GetStringArg(const Json::Value& args, const char* key) { + return JsonStrField(args, key); +} + +bool GetIntArg(const Json::Value& args, const char* key, int& out) { + if (!args.isMember(key)) return false; + const Json::Value& v = args[key]; + if (v.isNumeric()) { out = v.asInt(); return true; } + if (v.isString()) { + const std::string s = v.asString(); + if (IsDigits(s) || (!s.empty() && s[0] == '-' && IsDigits(s.substr(1)))) { + out = atoi(s.c_str()); + return true; + } + } + return false; +} + +bool ReadUInt64(const Json::Value& v, uint64_t& out) { + if (v.isNumeric()) { out = v.asUInt64(); return true; } + if (v.isString()) { + std::string s = v.asString(); + if (IsDigits(s)) { out = strtoull(s.c_str(), nullptr, 10); return true; } + } + return false; +} + +bool ReadUInt64Arg(const Json::Value& args, const char* key, uint64_t& out) { + if (!args.isMember(key)) return false; + return ReadUInt64(args[key], out); +} + +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; +} + +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; +} + +size_t BoundedStrlen(const char* p, size_t avail) { + size_t n = 0; + while (n < avail && p[n]) ++n; + return n; +} + +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; +} + +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; +} + +void AuditManageAction(CMy2015RemoteDlg* parent, uint64_t devId, + const std::string& tool, const std::string& detail) { + if (!parent) return; + std::string text = "host " + std::to_string(devId) + " " + tool + ": " + detail; + parent->PostMessageA(WM_SHOWERRORMSG, + (WPARAM)new CString(ToAnsi(text, 936).c_str()), + (LPARAM)new CString(_TR("MCP管理操作"))); +} + +} // namespace McpInternal diff --git a/server/2015Remote/McpInternal.h b/server/2015Remote/McpInternal.h new file mode 100644 index 0000000..cd338dd --- /dev/null +++ b/server/2015Remote/McpInternal.h @@ -0,0 +1,59 @@ +#pragma once + +// MCP 工具实现共享的内部辅助函数。 +// +// 这些函数原先都定义在 McpServer.cpp 的匿名命名空间里;主机管理 / 连接管理 +// 工具族(McpMachine.cpp)需要复用同一套 JSON、host id 解析与编码转换逻辑, +// 故抽出到独立编译单元,由 McpServer.cpp 与 McpMachine.cpp 共同引用。 +// +// 约定:函数语义与实现与抽取前完全一致(McpServer.cpp 通过 using-directive +// 引入,调用点不变),不做行为改动。 + +#include +#include "jsoncpp/json.h" + +#include +#include + +class CMy2015RemoteDlg; +class context; + +namespace McpInternal { + +// Json::Value → 紧凑 JSON 字符串 +std::string JsonToString(const Json::Value& v); + +// JSON-RPC 2.0 成功 / 错误响应 +std::string BuildResult(const Json::Value& id, const Json::Value& result); +std::string BuildError(const Json::Value& id, int code, const std::string& msg); + +// 仅 ASCII 小写化;不区分大小写的子串匹配 +std::string ToLowerAscii(const std::string& s); +bool ContainsCI(const std::string& haystack, const std::string& needle); + +// 入参读取 +std::string JsonStrField(const Json::Value& v, const char* key); +bool IsDigits(const std::string& s); +std::string GetStringArg(const Json::Value& args, const char* key); +bool GetIntArg(const Json::Value& args, const char* key, int& out); +bool ReadUInt64(const Json::Value& v, uint64_t& out); +bool ReadUInt64Arg(const Json::Value& args, const char* key, uint64_t& out); + +// 解析 id 入参(必填、纯数字)为 uint64;非法返回 false 并写 err。 +bool ParseHostIdArg(const Json::Value& args, uint64_t& out, std::string& err); + +// 在 m_HostList 中按 clientID 找在线主 context(锁内遍历 m_cs)。 +context* FindMainContext(CMy2015RemoteDlg* parent, uint64_t id); + +// 有界 strlen:p 最多可读 avail 字节,返回 n < avail 表示遇到 '\0'。 +size_t BoundedStrlen(const char* p, size_t avail); + +// 编码转换:任意代码页 → UTF-8(输出 JSON 用);UTF-8 → 目标代码页(下发客户端用)。 +std::string ToUtf8(const char* s, UINT cp); +std::string ToAnsi(const std::string& utf8, UINT cp); + +// 写操作审计:PostMessageA(WM_SHOWERRORMSG) 落到服务端消息/审计日志(不可关闭)。 +void AuditManageAction(CMy2015RemoteDlg* parent, uint64_t devId, + const std::string& tool, const std::string& detail); + +} // namespace McpInternal diff --git a/server/2015Remote/McpMachine.cpp b/server/2015Remote/McpMachine.cpp new file mode 100644 index 0000000..6d71832 --- /dev/null +++ b/server/2015Remote/McpMachine.cpp @@ -0,0 +1,2884 @@ +// McpMachine.cpp : MCP「连接管理」与「主机管理」工具族 +// +// 连接管理:主连接 COMMAND_CONNECTION(253) → 客户端 CConnectionsManager 子连接回 +// TOKEN_CONNECTION_LIST(255)。包格式 [255][u16 LE count][ConnectionInfo × count], +// ConnectionInfo 为 110 字节 packed 结构。强制断开 = 回发 [254][原始 110 字节]。 +// 主机管理:主连接 COMMAND_MACHINE_MANAGE(162) → 客户端 CMachineManager 子连接回 +// TOKEN_SYSINFOLIST(61) → 之后在同一子连接上发 1 字节 tab(0..9),回 +// TOKEN_MACHINE_*(31..42)。动作 10..30 由客户端处理,多数动作会自动回一份 +// 刷新后的列表(服务 = 40,任务 = 41),进程冻结/解冻与窗口操作不回包。 +// +// 会话状态(m_Pending 中的 Machine / ConnMgr 条目)全部由 CMcpServer 持有; +// 本文件只负责工具 schema、参数校验、组包,以及响应解析。协议与设计见 +// docs/Mcp_Machine_Design.md。 + +#include "stdafx.h" +#include "McpMachine.h" +#include "McpServer.h" +#include "McpInternal.h" +#include "context.h" +#include "2015RemoteDlg.h" +#include "LangManager.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace McpInternal; + +namespace { + +// ========== 错误码(与 McpServer.cpp 保持一致,另加 -32009) ========== +const int kErrGeneric = -32000; +const int kErrTimeout = -32001; +const int kErrOffline = -32002; +const int kErrBusy = -32003; +const int kErrSend = -32004; +const int kErrUnsupported = -32005; +const int kErrDisabled = -32006; +const int kErrNotFound = -32009; // 目标在最新快照中不存在(PID/HWND/服务/任务/连接) + +// ========== 超时(ms) ========== +const int kReadyTimeoutMs = 5000; // 主连接发命令 → 子连接回就绪包 +const int kListTimeoutMs = 12000; // tab 请求 → 列表响应(服务/软件枚举较慢) +const int kActionTimeoutMs = 12000; // 动作 → 客户端自动回的刷新列表 + +// 整个会话(含多步动作)的截止时间。restart 需要 prime + stop + start 三步, +// 每步最多 12s,故留 60s 余量;单步等待再按剩余时间收敛。 +const int kSessionDeadlineMs = 60000; + +// 单个包的上限:正常列表远小于此;超过即视为异常,置 clientError 并断开子连接。 +const size_t kMaxPacketBytes = 8u * 1024u * 1024u; + +const size_t kConnRecordSize = sizeof(ConnectionInfo); +static_assert(sizeof(ConnectionInfo) == 110, "ConnectionInfo layout changed; update the parser"); +static_assert(sizeof(WINDOWSINFO) == 1044, "WINDOWSINFO layout changed; update the parser"); + +// ========== 小工具 ========== + +std::string Trim(const std::string& s) { + size_t b = 0, e = s.size(); + while (b < e && (s[b] == ' ' || s[b] == '\t')) ++b; + while (e > b && (s[e - 1] == ' ' || s[e - 1] == '\t')) --e; + return s.substr(b, e - b); +} + +bool MatchCI(const std::string& value, const std::string& needle) { + return needle.empty() || ContainsCI(value, needle); +} + +bool EqualsCI(const std::string& a, const std::string& b) { + return a.size() == b.size() && _stricmp(a.c_str(), b.c_str()) == 0; +} + +bool GetBoolArg(const Json::Value& args, const char* key, bool def) { + if (!args.isMember(key)) return def; + const Json::Value& v = args[key]; + if (v.isBool()) return v.asBool(); + if (v.isNumeric()) return v.asInt() != 0; + if (v.isString()) { + std::string s = ToLowerAscii(v.asString()); + return s == "1" || s == "true" || s == "yes"; + } + return def; +} + +// 可选整数:缺省不算错;类型/范围错写 errJson。 +bool GetOptIntArg(const Json::Value& id, const Json::Value& args, const char* key, + int minV, int maxV, int& out, std::string& errJson) { + if (!args.isMember(key)) return true; + int v = 0; + if (!GetIntArg(args, key, v)) { + errJson = BuildError(id, -32602, std::string("Invalid parameter '") + key + "': expected an integer"); + return false; + } + if (v < minV || v > maxV) { + errJson = BuildError(id, -32602, std::string("Parameter '") + key + "' out of range [" + + std::to_string(minV) + ", " + std::to_string(maxV) + "]"); + return false; + } + out = v; + return true; +} + +std::string OptStr(const Json::Value& args, const char* key) { + return Trim(GetStringArg(args, key)); +} + +// 严格 UTF-8 → 目标代码页:任一字符无法表示即失败(不用 '?' 顶替)。 +bool Utf8ToCpExact(const std::string& utf8, UINT cp, std::string& out) { + out.clear(); + if (utf8.empty()) return true; + int wlen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8.c_str(), (int)utf8.size(), NULL, 0); + if (wlen <= 0) return false; + std::wstring w(wlen, L'\0'); + if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8.c_str(), (int)utf8.size(), + &w[0], wlen) != wlen) + return false; + + BOOL usedDefault = FALSE; + int alen = WideCharToMultiByte(cp, WC_NO_BEST_FIT_CHARS, w.c_str(), wlen, NULL, 0, NULL, &usedDefault); + if (alen <= 0 || usedDefault) return false; + out.assign(alen, '\0'); + usedDefault = FALSE; + if (WideCharToMultiByte(cp, WC_NO_BEST_FIT_CHARS, w.c_str(), wlen, &out[0], alen, + NULL, &usedDefault) != alen || usedDefault) { + out.clear(); + return false; + } + return true; +} + +bool IsValidUtf8(const char* s, size_t n) { + if (!s || n == 0) return true; + int wlen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, s, (int)n, NULL, 0); + return wlen > 0; +} + +bool IsWindowsClient(context* ctx) { + if (!ctx) return false; + CString t = ctx->GetAdditionalData(RES_CLIENT_TYPE); + return !(t == "LNX" || t == "MAC" || t == "APK"); +} + +std::string EndpointAddr(const std::string& ep) { + size_t c = ep.rfind(':'); + return c == std::string::npos ? ep : ep.substr(0, c); +} + +int EndpointPort(const std::string& ep) { + size_t c = ep.rfind(':'); + if (c == std::string::npos) return 0; + std::string p = ep.substr(c + 1); + return IsDigits(p) ? atoi(p.c_str()) : 0; +} + +const char* TcpStateName(DWORD state) { + switch (state) { + case 1: return "CLOSED"; + case 2: return "LISTEN"; + case 3: return "SYN_SENT"; + case 4: return "SYN_RCVD"; + case 5: return "ESTABLISHED"; + case 6: return "FIN_WAIT1"; + case 7: return "FIN_WAIT2"; + case 8: return "CLOSE_WAIT"; + case 9: return "CLOSING"; + case 10: return "LAST_ACK"; + case 11: return "TIME_WAIT"; + case 12: return "DELETE_TCB"; + default: return ""; + } +} + +const char* PriorityName(DWORD pri) { + switch (pri) { + case 0x00000040: return "Idle"; + case 0x00004000: return "BelowNormal"; + case 0x00000020: return "Normal"; + case 0x00008000: return "AboveNormal"; + case 0x00000080: return "High"; + case 0x00000100: return "Realtime"; + default: return ""; + } +} + +// OLE Automation DATE → "YYYY-MM-DD hh:mm:ss"(与 sys/MachineDlg.cpp:oleTime2Str 完全一致) +std::string OleDateToStr(double t) { + if (t == 0.0) return ""; + time_t tt = (time_t)(t * 24.0 * 3600.0 - 2209190400.0); + struct tm tm1; + if (localtime_s(&tm1, &tt) != 0) return ""; + char buf[32] = { 0 }; + sprintf_s(buf, "%04d-%02d-%02d %02d:%02d:%02d", + tm1.tm_year + 1900, tm1.tm_mon + 1, tm1.tm_mday, + tm1.tm_hour, tm1.tm_min, tm1.tm_sec); + return buf; +} + +// ========== schema 辅助 ========== + +Json::Value PropStr(const char* desc = nullptr) { + Json::Value p(Json::objectValue); + p["type"] = "string"; + if (desc) p["description"] = desc; + return p; +} + +Json::Value PropInt(const char* desc = nullptr) { + Json::Value p(Json::objectValue); + p["type"] = "integer"; + if (desc) p["description"] = desc; + return p; +} + +Json::Value PropBool(const char* desc = nullptr) { + Json::Value p(Json::objectValue); + p["type"] = "boolean"; + if (desc) p["description"] = desc; + return p; +} + +Json::Value PropStrEnum(const char* desc, std::initializer_list values) { + Json::Value p = PropStr(desc); + Json::Value enums(Json::arrayValue); + for (const char* v : values) enums.append(v); + p["enum"] = enums; + return p; +} + +Json::Value MakeSchema(const Json::Value& props, std::initializer_list required) { + Json::Value schema(Json::objectValue); + schema["type"] = "object"; + schema["properties"] = props; + if (required.size() > 0) { + Json::Value req(Json::arrayValue); + for (const char* r : required) req.append(r); + schema["required"] = req; + } + return schema; +} + +// 列表类 outputSchema:{ : [...], total, totalAll, offset, limit, hasMore } +Json::Value MakeListOutputSchema(const char* arrayKey, const Json::Value& itemProps) { + Json::Value props(Json::objectValue); + + Json::Value arr(Json::objectValue); + arr["type"] = "array"; + Json::Value items(Json::objectValue); + items["type"] = "object"; + items["properties"] = itemProps; + arr["items"] = items; + props[arrayKey] = arr; + + props["total"] = PropInt(u8"过滤后的条目数"); + props["totalAll"] = PropInt(u8"未过滤的条目总数"); + props["offset"] = PropInt(); + props["limit"] = PropInt(); + props["hasMore"] = PropBool(); + + Json::Value schema(Json::objectValue); + schema["type"] = "object"; + schema["properties"] = props; + Json::Value req(Json::arrayValue); + req.append(arrayKey); + schema["required"] = req; + return schema; +} + +Json::Value HostIdProp() { + return PropStr(u8"主机 id,取 list_online_hosts / search_hosts 返回的 id 字段"); +} + +// 所有读工具的公共分页参数 +void AddPagingProps(Json::Value& props) { + props["offset"] = PropInt(u8"分页起点,默认 0"); + props["limit"] = PropInt(u8"本次最多返回条数,默认 100,上限 1000"); +} + +Json::Value ActionOutputSchema() { + Json::Value props(Json::objectValue); + props["ok"] = PropBool(u8"动作是否达到预期(false 表示未生效/无法确认,不会伪报成功)"); + props["action"] = PropStr(); + props["target"] = PropStr(); + props["verified"] = PropBool(u8"是否已用动作后的最新快照复核"); + props["changed"] = PropBool(u8"是否真的改变了状态(如服务本来就是目标状态时为 false)"); + props["state"] = PropStr(u8"复核时观测到的状态"); + props["reason"] = PropStr(u8"ok=false 时的原因"); + props["message"] = PropStr(u8"人类可读的说明"); + Json::Value schema(Json::objectValue); + schema["type"] = "object"; + schema["properties"] = props; + Json::Value req(Json::arrayValue); + req.append("ok"); + schema["required"] = req; + return schema; +} + +std::string BuildStructuredResult(const Json::Value& id, const Json::Value& sc, + const std::string& text) { + Json::Value result(Json::objectValue); + result["structuredContent"] = sc; + + Json::Value content(Json::arrayValue); + Json::Value item(Json::objectValue); + item["type"] = "text"; + item["text"] = text; + content.append(item); + result["content"] = content; + result["isError"] = false; + + return BuildResult(id, result); +} + +std::string BuildListResult(const Json::Value& id, const char* arrayKey, + const Json::Value& page, int total, int totalAll, + int offset, int limit, bool hasMore, + const std::string& text) { + Json::Value sc(Json::objectValue); + sc[arrayKey] = page; + sc["total"] = total; + sc["totalAll"] = totalAll; + sc["offset"] = offset; + sc["limit"] = limit; + sc["hasMore"] = hasMore; + return BuildStructuredResult(id, sc, text); +} + +std::string BuildActionResult(const Json::Value& id, bool ok, bool verified, bool changed, + const std::string& action, const std::string& target, + const std::string& state, const std::string& reason, + const std::string& message) { + Json::Value sc(Json::objectValue); + sc["ok"] = ok; + sc["action"] = action; + sc["target"] = target; + sc["verified"] = verified; + sc["changed"] = changed; + sc["state"] = state; + if (!reason.empty()) sc["reason"] = reason; + sc["message"] = message; + + std::string text = std::string(ok ? "OK: " : "NOT OK: ") + action; + if (!target.empty()) text += " " + target; + if (!state.empty()) text += " (state=" + state + ")"; + if (!reason.empty()) text += " reason=" + reason; + return BuildStructuredResult(id, sc, text); +} + +// ========== 分页与过滤 ========== + +struct Paging { + int offset = 0; + int limit = 100; +}; + +bool ParsePaging(const Json::Value& id, const Json::Value& args, Paging& p, std::string& errJson) { + if (!GetOptIntArg(id, args, "offset", 0, 100000000, p.offset, errJson)) return false; + if (!GetOptIntArg(id, args, "limit", 1, 1000, p.limit, errJson)) return false; + return true; +} + +template +Json::Value FilterAndPage(const Json::Value& all, Pred pred, const Paging& p, + int& totalFiltered, bool& hasMore) { + std::vector keep; + keep.reserve(all.size()); + for (Json::ArrayIndex i = 0; i < all.size(); ++i) { + if (pred(all[i])) keep.push_back(all[i]); + } + totalFiltered = (int)keep.size(); + + int begin = p.offset < totalFiltered ? p.offset : totalFiltered; + int end = begin + p.limit; + if (end > totalFiltered) end = totalFiltered; + + Json::Value page(Json::arrayValue); + for (int i = begin; i < end; ++i) page.append(keep[i]); + hasMore = end < totalFiltered; + return page; +} + +// ========== 报文读取器(显式偏移 + 有界,禁止未对齐指针强转) ========== + +class Reader { +public: + explicit Reader(const std::vector& v) : m_p(v.data()), m_len(v.size()) {} + + size_t Offset() const { return m_off; } + size_t Remaining() const { return m_off <= m_len ? m_len - m_off : 0; } + bool Need(size_t n) const { return Remaining() >= n; } + + bool U8(BYTE& v) { + if (!Need(1)) return false; + v = m_p[m_off++]; + return true; + } + + bool U32(DWORD& v) { + if (!Need(4)) return false; + memcpy(&v, m_p + m_off, 4); + m_off += 4; + return true; + } + + bool Dates(double& a, double& b) { + if (!Need(16)) return false; + memcpy(&a, m_p + m_off, 8); + memcpy(&b, m_p + m_off + 8, 8); + m_off += 16; + return true; + } + + bool Skip(size_t n) { + if (!Need(n)) return false; + m_off += n; + return true; + } + + // 剩余字节是否全为 0。客户端用 LocalSize() 决定发送长度,分配粒度可能带出少量 + // 尾随 0 填充;记录解析循环据此提前结束,而不是把填充误判成截断。 + bool AllZero() const { + for (size_t i = m_off; i < m_len; ++i) { + if (m_p[i] != 0) return false; + } + return true; + } + + // 读取 NUL 结尾字符串(最多 maxBytes),推进 n + 1 + skipExtra 字节。 + // 找不到 NUL 视为截断。 + bool Str(UINT cp, size_t maxBytes, size_t skipExtra, std::string& out) { + size_t avail = Remaining(); + if (avail > maxBytes) avail = maxBytes; + if (avail == 0) return false; + size_t n = BoundedStrlen((const char*)m_p + m_off, avail); + if (n == avail) return false; // 没有 NUL → 截断 + if (!Need(n + 1 + skipExtra)) return false; // 尾随的 pad 字节缺失 + out = ToUtf8((const char*)m_p + m_off, cp); + m_off += n + 1 + skipExtra; + return true; + } + + // 定长字符串字段(如 char title[260]),字段内以 NUL 结尾或被 pad 填满。 + bool FixedStr(UINT cp, size_t fixed, std::string& out) { + if (!Need(fixed)) return false; + char buf[1025]; + size_t n = fixed < sizeof(buf) - 1 ? fixed : sizeof(buf) - 1; + memcpy(buf, m_p + m_off, n); + buf[n] = 0; + out = ToUtf8(buf, cp); + m_off += fixed; + return true; + } + +private: + const BYTE* m_p; + size_t m_len; + size_t m_off = 0; +}; + +// ========== 解析:主机管理各 tab ========== + +bool ParseProcesses(const std::vector& data, UINT cp, Json::Value& out, std::string& err) { + out = Json::Value(Json::arrayValue); + if (data.size() <= 1) return true; // 只有 token = 空列表 + + Reader r(data); + if (!r.Skip(1)) { err = "empty process packet"; return false; } + + while (r.Remaining() > 0) { + if (r.AllZero()) break; // 末尾 0 填充 + size_t start = r.Offset(); + DWORD pid = 0; + BYTE is64 = 0; + std::string title, exe, path, user; + DWORD pri = 0, threads = 0, workingSetKb = 0, fileSizeKb = 0; + + if (!r.U32(pid) || !r.U8(is64) || + !r.FixedStr(cp, MAX_PATH, title) || + !r.Str(cp, r.Remaining(), 1, exe) || + !r.Str(cp, r.Remaining(), 1, path) || + !r.U32(pri) || !r.U32(threads) || + !r.Str(cp, r.Remaining(), 1, user) || + !r.U32(workingSetKb) || !r.U32(fileSizeKb)) { + err = "truncated process record at offset " + std::to_string(start); + return false; + } + + Json::Value item(Json::objectValue); + item["pid"] = (Json::UInt)pid; + item["arch"] = is64 ? "x64" : "x86"; + item["title"] = title; + item["name"] = exe; + item["path"] = path; + item["priorityClass"] = (Json::UInt)pri; + item["priority"] = PriorityName(pri); + item["threads"] = (Json::UInt)threads; + item["user"] = user; + item["workingSetKb"] = (Json::UInt)workingSetKb; + item["fileSizeKb"] = (Json::UInt)fileSizeKb; + out.append(item); + } + return true; +} + +bool ParseWindows(const std::vector& data, UINT cp, Json::Value& out, std::string& err) { + out = Json::Value(Json::arrayValue); + if (data.size() <= 1) return true; + + size_t payload = data.size() - 1; + // 记录是定长的;若末尾有不足一条记录的多余字节(分配粒度带来的 0 填充),忽略即可。 + size_t count = payload / sizeof(WINDOWSINFO); + for (size_t i = 0; i < count; ++i) { + WINDOWSINFO info; + memcpy(&info, data.data() + 1 + i * sizeof(WINDOWSINFO), sizeof(info)); + + char title[sizeof(info.strTitle) + 1] = { 0 }; + memcpy(title, info.strTitle, sizeof(info.strTitle)); + + Json::Value item(Json::objectValue); + item["hwnd"] = (Json::UInt)info.m_hwnd; + item["title"] = ToUtf8(title, cp); + item["pid"] = (Json::UInt)info.m_poceessid; + item["width"] = info.w; + item["height"] = info.h; + item["canSee"] = info.canlook ? true : false; + out.append(item); + } + return true; +} + +bool ParseNetState(const std::vector& data, UINT cp, Json::Value& out, std::string& err) { + out = Json::Value(Json::arrayValue); + if (data.size() <= 1) return true; + + Reader r(data); + if (!r.Skip(1)) { err = "empty netstate packet"; return false; } + + while (r.Remaining() > 0) { + if (r.AllZero()) break; // 末尾 0 填充 + size_t start = r.Offset(); + std::string proc, proto, local, remote, state; + DWORD pid = 0; + + if (!r.Str(cp, r.Remaining(), 1, proc) || + !r.U32(pid) || !r.Skip(2) || + !r.Str(cp, r.Remaining(), 1, proto) || + !r.Str(cp, r.Remaining(), 1, local) || + !r.Str(cp, r.Remaining(), 1, remote) || + !r.Str(cp, r.Remaining(), 1, state)) { + err = "truncated netstate record at offset " + std::to_string(start); + return false; + } + + std::string protocol = (proto == "[UDP]") ? "udp" : "tcp"; + Json::Value item(Json::objectValue); + item["process"] = proc; + item["pid"] = (Json::UInt)pid; + item["protocol"] = protocol; + item["localEndpoint"] = local; + item["localAddress"] = EndpointAddr(local); + item["localPort"] = EndpointPort(local); + item["remoteEndpoint"] = remote; + item["remoteAddress"] = EndpointAddr(remote); + item["remotePort"] = EndpointPort(remote); + item["state"] = Trim(state); + out.append(item); + } + return true; +} + +bool ParseSoftware(const std::vector& data, UINT cp, Json::Value& out, std::string& err) { + out = Json::Value(Json::arrayValue); + if (data.size() <= 1) return true; + + Reader r(data); + if (!r.Skip(1)) { err = "empty software packet"; return false; } + + while (r.Remaining() > 0) { + if (r.AllZero()) break; // 末尾 0 填充 + size_t start = r.Offset(); + std::string name, publisher, version, installDate, uninstall; + + if (!r.Str(cp, r.Remaining(), 1, name) || + !r.Str(cp, r.Remaining(), 1, publisher) || + !r.Str(cp, r.Remaining(), 1, version) || + !r.Str(cp, r.Remaining(), 1, installDate) || + !r.Str(cp, r.Remaining(), 1, uninstall)) { + err = "truncated software record at offset " + std::to_string(start); + return false; + } + + Json::Value item(Json::objectValue); + item["name"] = name; + item["publisher"] = publisher; + item["version"] = version; + item["installDate"] = installDate; + item["uninstallString"] = uninstall; + out.append(item); + } + return true; +} + +bool ParseBrowserHistory(const std::vector& data, UINT cp, Json::Value& out, std::string& err) { + out = Json::Value(Json::arrayValue); + if (data.size() <= 1) return true; + + size_t payload = data.size() - 1; + const size_t kRec = 100 + 1024 + 1024; // Browsinghistory + size_t count = payload / kRec; // 末尾不足一条记录的字节忽略 + for (size_t i = 0; i < count; ++i) { + const BYTE* p = data.data() + 1 + i * kRec; + char t[101] = { 0 }, title[1025] = { 0 }, url[1025] = { 0 }; + memcpy(t, p, 100); + memcpy(title, p + 100, 1024); + memcpy(url, p + 100 + 1024, 1024); + + Json::Value item(Json::objectValue); + item["time"] = t; // 客户端已格式化为 ASCII 时间 + item["title"] = ToUtf8(title, cp); + item["url"] = ToUtf8(url, cp); + out.append(item); + } + return true; +} + +bool ParseFavorites(const std::vector& data, UINT cp, Json::Value& out, std::string& err) { + out = Json::Value(Json::arrayValue); + if (data.size() <= 1) return true; + + Reader r(data); + if (!r.Skip(1)) { err = "empty favorites packet"; return false; } + + while (r.Remaining() > 0) { + if (r.AllZero()) break; // 末尾 0 填充 + size_t start = r.Offset(); + std::string name, url; + if (!r.Str(cp, r.Remaining(), 1, name) || + !r.Str(cp, r.Remaining(), 1, url)) { + err = "truncated favorites record at offset " + std::to_string(start); + return false; + } + Json::Value item(Json::objectValue); + item["name"] = name; + item["url"] = url; + out.append(item); + } + return true; +} + +bool ParseServices(const std::vector& data, UINT cp, Json::Value& out, std::string& err) { + out = Json::Value(Json::arrayValue); + if (data.size() <= 1) return true; + + Reader r(data); + if (!r.Skip(1)) { err = "empty service packet"; return false; } + + while (r.Remaining() > 0) { + if (r.AllZero()) break; // 末尾 0 填充 + size_t start = r.Offset(); + std::string displayName, description, state, startType, login, interactive, name, binaryPath; + + if (!r.Str(cp, r.Remaining(), 0, displayName) || + !r.Str(cp, r.Remaining(), 0, description) || + !r.Str(cp, r.Remaining(), 0, state) || + !r.Str(cp, r.Remaining(), 0, startType) || + !r.Str(cp, r.Remaining(), 0, login) || + !r.Str(cp, r.Remaining(), 0, interactive) || + !r.Str(cp, r.Remaining(), 0, name) || + !r.Str(cp, r.Remaining(), 0, binaryPath)) { + err = "truncated service record at offset " + std::to_string(start); + return false; + } + + Json::Value item(Json::objectValue); + item["displayName"] = displayName; + item["description"] = description; + item["state"] = state; + item["startType"] = startType; + item["login"] = login; + item["interactive"] = (interactive == u8"是"); + item["name"] = name; + item["binaryPath"] = binaryPath; + out.append(item); + } + return true; +} + +// 任务记录:name/path/exe 为 UTF-8,status 为 ANSI(GBK),各以 NUL 结尾; +// 之后两个 DATE(double)。列表以两个 0 字节结尾。 +bool ParseTasks(const std::vector& data, Json::Value& out, std::string& err) { + out = Json::Value(Json::arrayValue); + if (data.size() <= 2) return true; // 只有 token(可能带 1 个结束 0) + + Reader r(data); + if (!r.Skip(1)) { err = "empty task packet"; return false; } + + while (r.Remaining() > 2) { + if (data[r.Offset()] == 0) break; // 结束标记(两个 0 字节) + size_t start = r.Offset(); + std::string name, path, exe, status; + double last = 0, next = 0; + + if (!r.Str(CP_UTF8, r.Remaining(), 0, name) || + !r.Str(CP_UTF8, r.Remaining(), 0, path) || + !r.Str(CP_UTF8, r.Remaining(), 0, exe) || + !r.Str(936, r.Remaining(), 0, status) || + !r.Dates(last, next)) { + err = "truncated task record at offset " + std::to_string(start); + return false; + } + if (name.empty()) break; // 防御:空名视为结束 + + // folder = path 去掉末尾 "\";不匹配时退回最后一个反斜杠之前。 + std::string folder; + std::string suffix = "\\" + name; + if (path.size() >= suffix.size() && + _stricmp(path.c_str() + path.size() - suffix.size(), suffix.c_str()) == 0) { + folder = path.substr(0, path.size() - suffix.size()); + } else { + size_t pos = path.rfind('\\'); + folder = (pos == std::string::npos) ? "\\" : path.substr(0, pos); + } + if (folder.empty()) folder = "\\"; + + Json::Value item(Json::objectValue); + item["name"] = name; + item["path"] = path; + item["folder"] = folder; + item["exe"] = exe; + item["status"] = status; + item["lastRun"] = OleDateToStr(last); + item["nextRun"] = OleDateToStr(next); + out.append(item); + } + return true; +} + +// ========== 解析:连接管理 ========== + +std::string FormatAddr(uint8_t af, const uint8_t* addr) { + char buf[INET6_ADDRSTRLEN] = { 0 }; + if (af == AF_INET) { + inet_ntop(AF_INET, addr, buf, INET_ADDRSTRLEN); + } else if (af == AF_INET6) { + inet_ntop(AF_INET6, addr, buf, INET6_ADDRSTRLEN); + } + return buf; +} + +struct ConnRecord { + const BYTE* raw = nullptr; // 指向 data 中的原始 110 字节(强制断开原样回传) + int proto = 0; + int af = 0; + uint32_t pid = 0; + uint8_t localAddr[16] = { 0 }; + uint16_t localPort = 0; + uint8_t remoteAddr[16] = { 0 }; + uint16_t remotePort = 0; + uint32_t state = 0; + std::string procName; +}; + +// 解析连接列表。data 首字节必须是 TOKEN_CONNECTION_LIST。 +bool ParseConnections(const std::vector& data, + std::vector& records, std::string& err) { + records.clear(); + if (data.empty()) { err = "empty connection packet"; return false; } + if (data[0] != TOKEN_CONNECTION_LIST) { + err = "unexpected token " + std::to_string((int)data[0]) + " for connection list"; + return false; + } + if (data.size() < 3) { err = "connection packet too short"; return false; } + + uint16_t count = 0; + memcpy(&count, data.data() + 1, 2); + size_t need = 3 + (size_t)count * kConnRecordSize; + if (data.size() < need) { + err = "connection packet truncated: declared " + std::to_string(count) + + " records but only " + std::to_string(data.size() - 3) + " bytes"; + return false; + } + + for (uint16_t i = 0; i < count; ++i) { + const BYTE* p = data.data() + 3 + (size_t)i * kConnRecordSize; + ConnRecord rec; + rec.raw = p; + rec.proto = p[0]; + rec.af = p[1]; + memcpy(&rec.pid, p + 2, 4); + memcpy(rec.localAddr, p + 6, 16); + memcpy(&rec.localPort, p + 22, 2); + memcpy(rec.remoteAddr, p + 24, 16); + memcpy(&rec.remotePort, p + 40, 2); + memcpy(&rec.state, p + 42, 4); + char name[65] = { 0 }; + memcpy(name, p + 46, 64); + rec.procName = name; // 客户端写的是 UTF-8 + records.push_back(rec); + } + return true; +} + +Json::Value ConnRecordToJson(const ConnRecord& rec) { + Json::Value item(Json::objectValue); + item["protocol"] = (rec.proto == 1) ? "tcp" : (rec.proto == 2 ? "udp" : "unknown"); + item["family"] = (rec.af == AF_INET) ? "ipv4" : (rec.af == AF_INET6 ? "ipv6" : "unknown"); + item["pid"] = (Json::UInt)rec.pid; + item["process"] = rec.procName; + item["localAddress"] = FormatAddr((uint8_t)rec.af, rec.localAddr); + item["localPort"] = (Json::UInt)rec.localPort; + item["remoteAddress"] = FormatAddr((uint8_t)rec.af, rec.remoteAddr); + item["remotePort"] = (Json::UInt)rec.remotePort; + item["state"] = (Json::UInt)rec.state; + item["stateName"] = (rec.proto == 1) ? TcpStateName(rec.state) : ""; + item["killable"] = (rec.proto == 1 && rec.af == AF_INET); + return item; +} + +// ========== 主机解析 / 会话封装 ========== + +struct HostTool { + uint64_t dev = 0; + context* mainCtx = nullptr; +}; + +// requireWindows_ = true 时对 LNX/MAC/APK 直接返回 -32005,避免白等超时。 +bool ResolveHost(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent, + HostTool& ht, bool requireWindows_, std::string& errJson) { + uint64_t dev = 0; + std::string err; + if (!ParseHostIdArg(args, dev, err)) { + errJson = BuildError(id, -32602, err); + return false; + } + context* ctx = FindMainContext(parent, dev); + if (!ctx) { + errJson = BuildError(id, kErrOffline, "Host not found or offline: " + std::to_string(dev)); + return false; + } + if (requireWindows_ && !IsWindowsClient(ctx)) { + errJson = BuildError(id, kErrUnsupported, "This tool is only supported on Windows hosts"); + return false; + } + ht.dev = dev; + ht.mainCtx = ctx; + return true; +} + +std::string WaitFailJson(const Json::Value& id, CMcpServer::ManagedWaitResult r, + const std::string& clientErr, const char* what, bool readyPhase) { + switch (r) { + case CMcpServer::ManagedWaitResult::ClientError: + return BuildError(id, kErrGeneric, std::string("Client reported an error: ") + + (clientErr.empty() ? std::string("(no detail)") : clientErr)); + case CMcpServer::ManagedWaitResult::Closed: + return BuildError(id, kErrTimeout, std::string("Sub-connection closed while waiting for ") + what); + case CMcpServer::ManagedWaitResult::Timeout: + default: { + std::string msg = std::string("Timeout waiting for ") + what; + if (readyPhase) { + msg += " (host may be offline, the feature may be disabled on the client, " + "or the client is too old to support it)"; + } + return BuildError(id, kErrTimeout, msg); + } + } +} + +// 一次主机管理 / 连接管理会话。析构时若未正常收尾,按失败路径打墓碑并关子连接。 +class ManagedSession { +public: + ManagedSession() {} + ~ManagedSession() { Abort(); } + ManagedSession(const ManagedSession&) = delete; + ManagedSession& operator=(const ManagedSession&) = delete; + + bool Open(const Json::Value& id, const std::string& tool, const HostTool& ht, + bool connMgr, std::vector* firstPacket, std::string& errJson) { + m_dev = ht.dev; + CMcpServer& mcp = McpServer(); + bool began = connMgr ? mcp.BeginConnMgrSession(m_dev, tool, ht.mainCtx) + : mcp.BeginMachineSession(m_dev, tool, ht.mainCtx); + if (!began) { + errJson = BuildError(id, kErrBusy, + "Device busy: another MCP request is pending for this host"); + return false; + } + m_open = true; + + BYTE cmd = connMgr ? (BYTE)COMMAND_CONNECTION : (BYTE)COMMAND_MACHINE_MANAGE; + if (!ht.mainCtx->Send2Client(&cmd, 1)) { + errJson = BuildError(id, kErrSend, "Failed to send command to host"); + return false; // 析构 → Abort(打墓碑) + } + + BYTE readyToken = connMgr ? (BYTE)TOKEN_CONNECTION_LIST : (BYTE)TOKEN_SYSINFOLIST; + std::vector ready; + std::string clientErr; + CMcpServer::ManagedWaitResult r = + mcp.WaitManagedPacket(m_dev, readyToken, ready, m_sub, kReadyTimeoutMs, clientErr); + if (r != CMcpServer::ManagedWaitResult::Ok) { + errJson = WaitFailJson(id, r, clientErr, + connMgr ? "connection list" : "machine manager readiness", + true); + return false; + } + if (firstPacket) *firstPacket = ready; + return true; + } + + // 发 1 字节 tab 请求并等待指定响应 token。 + bool Request(const Json::Value& id, BYTE req, BYTE respToken, std::vector& out, + std::string& errJson) { + CMcpServer& mcp = McpServer(); + mcp.ArmManagedResponse(m_dev, respToken); + if (!m_sub || !m_sub->Send2Client(&req, 1)) { + errJson = BuildError(id, kErrSend, "Failed to send request to host"); + return false; + } + std::string clientErr; + CMcpServer::ManagedWaitResult r = + mcp.WaitManagedPacket(m_dev, respToken, out, m_sub, kListTimeoutMs, clientErr); + if (r != CMcpServer::ManagedWaitResult::Ok) { + errJson = WaitFailJson(id, r, clientErr, "host management response", false); + return false; + } + return true; + } + + // 发一个动作包并等待指定的刷新响应(客户端自动回)。 + bool Action(const Json::Value& id, std::vector& pkt, BYTE respToken, + std::vector& out, std::string& errJson) { + CMcpServer& mcp = McpServer(); + mcp.ArmManagedResponse(m_dev, respToken); + if (!m_sub || !m_sub->Send2Client(pkt.data(), (ULONG)pkt.size())) { + errJson = BuildError(id, kErrSend, "Failed to send action to host"); + return false; + } + m_sub->WaitSendIdle(2000); + std::string clientErr; + CMcpServer::ManagedWaitResult r = + mcp.WaitManagedPacket(m_dev, respToken, out, m_sub, kActionTimeoutMs, clientErr); + if (r != CMcpServer::ManagedWaitResult::Ok) { + errJson = WaitFailJson(id, r, clientErr, "action confirmation", false); + return false; + } + return true; + } + + // 发一个无响应的动作包(进程冻结/解冻、窗口操作)。 + bool SendNoReply(const Json::Value& id, std::vector& pkt, std::string& errJson) { + if (!m_sub || !m_sub->Send2Client(pkt.data(), (ULONG)pkt.size())) { + errJson = BuildError(id, kErrSend, "Failed to send action to host"); + return false; + } + m_sub->WaitSendIdle(2000); + return true; + } + + void Finish() { + if (!m_open) return; + McpServer().EndManagedSession(m_dev, true, false); + m_open = false; + } + + void Abort() { + if (!m_open) return; + McpServer().EndManagedSession(m_dev, true, true); + m_open = false; + } + + uint64_t Dev() const { return m_dev; } + context* Sub() const { return m_sub; } + +private: + bool m_open = false; + uint64_t m_dev = 0; + context* m_sub = nullptr; +}; + +// 目标过期:-32009,提示调用方刷新快照后重试。 +std::string NotFoundJson(const Json::Value& id, const std::string& what) { + return BuildError(id, kErrNotFound, + what + " does not exist in the latest snapshot; refresh the list and retry"); +} + +} // namespace + +// ============================================================================ +// CMcpServer:P8 会话状态机(在 UI 线程 / 工具线程之间共享) +// ============================================================================ + +bool CMcpServer::KindAcceptsToken(PendingKind kind, BYTE token) { + if (kind == PendingKind::Machine) return token == (BYTE)TOKEN_SYSINFOLIST; + if (kind == PendingKind::ConnMgr) return token == (BYTE)TOKEN_CONNECTION_LIST; + return false; +} + +void CMcpServer::RefreshManagedRoutesLocked() { + m_ManagedRoutes.store((int)(m_SubCtxToDevice.size() + m_AbsorbedSubCtx.size()), + std::memory_order_relaxed); +} + +void CMcpServer::PruneOrphanMarksLocked(std::chrono::steady_clock::time_point now) { + for (auto it = m_OrphanByDevice.begin(); it != m_OrphanByDevice.end();) { + if (it->second.expiry <= now) it = m_OrphanByDevice.erase(it); + else ++it; + } + for (auto it = m_OrphanByPeer.begin(); it != m_OrphanByPeer.end();) { + if (it->second.expiry <= now) it = m_OrphanByPeer.erase(it); + else ++it; + } +} + +bool CMcpServer::BeginManagedSession(uint64_t device_id, const std::string& tool, + PendingKind kind, context* mainCtx) { + if (IsFileTransferPending(device_id)) return false; // 文件传输在飞:单设备单飞行 + + std::lock_guard lk(m_PendingMutex); + if (m_Pending.find(device_id) != m_Pending.end()) return false; // 设备忙 + + PendingRequest r; + r.tool = tool; + r.kind = kind; + r.mainCtx = mainCtx; + r.peer = mainCtx ? mainCtx->GetPeerName() : ""; + r.deadline = std::chrono::steady_clock::now() + + std::chrono::milliseconds(kSessionDeadlineMs); + m_Pending[device_id] = std::move(r); + return true; +} + +bool CMcpServer::BeginMachineSession(uint64_t device_id, const std::string& tool, context* mainCtx) { + return BeginManagedSession(device_id, tool, PendingKind::Machine, mainCtx); +} + +bool CMcpServer::BeginConnMgrSession(uint64_t device_id, const std::string& tool, context* mainCtx) { + return BeginManagedSession(device_id, tool, PendingKind::ConnMgr, mainCtx); +} + +void CMcpServer::ArmManagedResponse(uint64_t device_id, BYTE expectedToken) { + std::lock_guard lk(m_PendingMutex); + auto it = m_Pending.find(device_id); + if (it == m_Pending.end()) return; + if (it->second.kind != PendingKind::Machine && it->second.kind != PendingKind::ConnMgr) return; + it->second.expectedToken = expectedToken; + it->second.hasResponse = false; + it->second.response.clear(); + it->second.clientError.clear(); +} + +CMcpServer::ManagedWaitResult CMcpServer::WaitManagedPacket(uint64_t device_id, BYTE token, + std::vector& out, + context*& subCtxOut, + int timeoutMs, std::string& clientError) { + std::unique_lock lk(m_PendingMutex); + + auto waitUntil = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs); + + // 循环等待:迟到的/无关的包被丢弃后继续等,直到本次步骤超时或会话截止时间。 + for (;;) { + auto it = m_Pending.find(device_id); + if (it == m_Pending.end()) return ManagedWaitResult::Closed; + + PendingRequest& r = it->second; + if (r.kind != PendingKind::Machine && r.kind != PendingKind::ConnMgr) + return ManagedWaitResult::Closed; + + subCtxOut = r.subCtx; // 绑定可能发生在等待期间 + + if (!r.clientError.empty()) { + clientError = r.clientError; + r.clientError.clear(); + return ManagedWaitResult::ClientError; + } + if (r.hasResponse) { + bool match = !r.response.empty() && r.response[0] == token; + if (match) { + out = std::move(r.response); + r.hasResponse = false; + return ManagedWaitResult::Ok; + } + // 期望 token 之外的响应(例如客户端主动发来的提示):丢弃后继续等。 + r.hasResponse = false; + r.response.clear(); + } + if (r.closed) return ManagedWaitResult::Closed; + + if (waitUntil > r.deadline) waitUntil = r.deadline; + auto now = std::chrono::steady_clock::now(); + if (now >= waitUntil) return ManagedWaitResult::Timeout; + m_PendingCv.wait_until(lk, waitUntil); + } +} + +void CMcpServer::EndManagedSession(uint64_t device_id, bool cancelIo, bool markOrphan) { + context* sub = nullptr; + PendingKind kind = PendingKind::Generic; + std::string peer; + { + std::lock_guard lk(m_PendingMutex); + auto now = std::chrono::steady_clock::now(); + auto it = m_Pending.find(device_id); + if (it == m_Pending.end()) { + PruneOrphanMarksLocked(now); + return; + } + PendingRequest& r = it->second; + if (r.kind != PendingKind::Machine && r.kind != PendingKind::ConnMgr) return; + + sub = r.subCtx; + kind = r.kind; + peer = r.peer; + if (sub) { + m_SubCtxToDevice.erase(sub); + // MCP 关掉的子连接上仍可能有在途包:登记为吸收,直到 OfflineProc 回收。 + if (cancelIo) m_AbsorbedSubCtx.insert(sub); + } + m_Pending.erase(it); + + if (markOrphan) { + OrphanMark mk; + mk.expiry = now + std::chrono::seconds(5); + mk.kind = kind; + m_OrphanByDevice[device_id] = mk; + if (!peer.empty()) m_OrphanByPeer[peer] = mk; + } + PruneOrphanMarksLocked(now); + RefreshManagedRoutesLocked(); + } + Mprintf("[MCP-P8] EndManagedSession dev=%llu kind=%d sub=%p cancelIo=%d orphan=%d\n", + device_id, (int)kind, (void*)sub, cancelIo ? 1 : 0, markOrphan ? 1 : 0); + if (sub && cancelIo) sub->CancelIO(); // 锁外关子连接 +} + +bool CMcpServer::OnManagedSubPacket(context* subCtx, const BYTE* data, ULONG len) { + if (!subCtx || !data || len == 0) return false; + + BYTE token = data[0]; + bool readyToken = (token == (BYTE)TOKEN_SYSINFOLIST) || + (token == (BYTE)TOKEN_CONNECTION_LIST); + // 快速路径:没有已绑定/已吸收的子连接,且不是就绪包 → 与 P8 无关。 + // MessageHandle 每个包都会调用本函数(含音频/屏幕等高频子连接),不能逐包抢锁。 + if (!readyToken && m_ManagedRoutes.load(std::memory_order_relaxed) == 0) return false; + + // 主连接自身不会发就绪包;若 clientID 指向的“主连接”就是本 ctx,回落 MFC。 + // FindMainContext 要取 parent->m_cs,必须在持 m_PendingMutex 之前完成, + // 维持锁序 m_cs → m_PendingMutex(与 P2/P7 的调用顺序一致),避免反向等待。 + uint64_t cid = subCtx->GetClientID(); + bool mainIsSelf = readyToken && cid != 0 && FindMainContext(m_parent, cid) == subCtx; + + std::unique_lock lk(m_PendingMutex); + + // ---- 情形 1:已绑定的子连接 → 该连接上的包全部由 MCP 消费 ---- + auto bindIt = m_SubCtxToDevice.find(subCtx); + if (bindIt != m_SubCtxToDevice.end()) { + uint64_t dev = bindIt->second; + auto it = m_Pending.find(dev); + if (it == m_Pending.end() || + (it->second.kind != PendingKind::Machine && it->second.kind != PendingKind::ConnMgr)) { + // 会话已结束但包还在路上:吸收并断开 + m_SubCtxToDevice.erase(bindIt); + m_AbsorbedSubCtx.insert(subCtx); + RefreshManagedRoutesLocked(); + lk.unlock(); + Mprintf("[MCP-P8] bound sub=%p without session: absorb+close\n", (void*)subCtx); + subCtx->CancelIO(); + return true; + } + + PendingRequest& r = it->second; + if (len > kMaxPacketBytes) { + r.clientError = "response packet too large (" + std::to_string(len) + " bytes)"; + m_PendingCv.notify_all(); + lk.unlock(); + subCtx->CancelIO(); + return true; + } + if (token == (BYTE)TOKEN_MACHINE_MSG) { + // 客户端主动报错(CP936 文本) + r.clientError = ToUtf8((const char*)data + 1, 936); + m_PendingCv.notify_all(); + return true; + } + if (r.expectedToken != 0 && token == r.expectedToken) { + r.response.assign(data, data + len); + r.hasResponse = true; + m_PendingCv.notify_all(); + } + // 其它包(迟到/无关)丢弃 + return true; + } + + // ---- 情形 2:已吸收(CancelIO 后仍到达的尾包)→ 丢弃 ---- + if (m_AbsorbedSubCtx.find(subCtx) != m_AbsorbedSubCtx.end()) { + lk.unlock(); + subCtx->CancelIO(); + return true; + } + + // ---- 情形 3:未绑定且不是就绪包 → 交回 MFC ---- + if (!readyToken) return false; + if (mainIsSelf) return false; + + PendingKind wantKind = (token == (BYTE)TOKEN_SYSINFOLIST) ? PendingKind::Machine + : PendingKind::ConnMgr; + + // 3a. 按 clientID 精确匹配(新客户端经 TOKEN_CONN_AUTH 已钉 ID) + uint64_t boundDev = 0; + if (cid != 0) { + auto it = m_Pending.find(cid); + if (it != m_Pending.end() && it->second.kind == wantKind && it->second.subCtx == nullptr) + boundDev = cid; + } + + std::string peer = subCtx->GetPeerName(); + + // 3b. 老客户端子连接 clientID 可能为 0:按 peer 兜底,仅在同 peer 唯一候选时绑定 + if (boundDev == 0 && cid == 0 && !peer.empty()) { + uint64_t candidate = 0; + int matches = 0; + for (auto& kv : m_Pending) { + if (kv.second.kind != wantKind) continue; + if (kv.second.subCtx != nullptr) continue; + if (kv.second.peer == peer) { + candidate = kv.first; + ++matches; + } + } + if (matches == 1) boundDev = candidate; + } + + if (boundDev == 0) { + // 3c. 没有等待中的会话:若刚有会话异常结束(5s 墓碑),吸收这个迟到的就绪包, + // 避免弹出 MFC 对话框。 + auto now = std::chrono::steady_clock::now(); + PruneOrphanMarksLocked(now); + bool absorb = false; + if (cid != 0) { + auto o = m_OrphanByDevice.find(cid); + if (o != m_OrphanByDevice.end() && o->second.expiry > now && + KindAcceptsToken(o->second.kind, token)) + absorb = true; + } else if (!peer.empty()) { + auto o = m_OrphanByPeer.find(peer); + if (o != m_OrphanByPeer.end() && o->second.expiry > now && + KindAcceptsToken(o->second.kind, token)) + absorb = true; + } + if (absorb) { + m_AbsorbedSubCtx.insert(subCtx); + RefreshManagedRoutesLocked(); + lk.unlock(); + Mprintf("[MCP-P8] late ready token=%d peer=%s cid=%llu: absorb+close\n", + (int)token, peer.c_str(), (unsigned long long)cid); + subCtx->CancelIO(); + return true; + } + return false; // 用户手动打开对话框 + } + + // ---- 绑定 ---- + PendingRequest& r = m_Pending[boundDev]; + r.subCtx = subCtx; + m_SubCtxToDevice[subCtx] = boundDev; + RefreshManagedRoutesLocked(); + + if (len > kMaxPacketBytes) { + r.clientError = "readiness packet too large (" + std::to_string(len) + " bytes)"; + m_PendingCv.notify_all(); + lk.unlock(); + subCtx->CancelIO(); + return true; + } + + r.response.assign(data, data + len); + r.hasResponse = true; + m_PendingCv.notify_all(); + Mprintf("[MCP-P8] bind dev=%llu tool=%s sub=%p token=%d len=%d\n", + boundDev, r.tool.c_str(), (void*)subCtx, (int)token, (int)len); + return true; +} + +void CMcpServer::OnManagedContextClosed(context* subCtx) { + if (!subCtx) return; + std::lock_guard lk(m_PendingMutex); + + m_AbsorbedSubCtx.erase(subCtx); + m_SubCtxToDevice.erase(subCtx); + + for (auto& kv : m_Pending) { + PendingRequest& r = kv.second; + if (r.kind != PendingKind::Machine && r.kind != PendingKind::ConnMgr) continue; + if (r.subCtx == subCtx || r.mainCtx == subCtx) { + Mprintf("[MCP-P8] context closed dev=%llu tool=%s: wake waiter\n", + kv.first, r.tool.c_str()); + if (r.subCtx == subCtx) { + // 子连接已死:不要再让超时路径对它 CancelIO + r.subCtx = nullptr; + } + r.closed = true; + m_PendingCv.notify_all(); + } + } + RefreshManagedRoutesLocked(); +} + +// ============================================================================ +// 连接管理 +// ============================================================================ + +namespace { + +bool ListConnections(const Json::Value& id, const HostTool& ht, const std::string& toolName, + std::vector& records, std::string& errJson) { + ManagedSession session; + std::vector first; + if (!session.Open(id, toolName, ht, /*connMgr=*/true, &first, errJson)) + return false; + if (!ParseConnections(first, records, errJson)) { + errJson = BuildError(id, kErrGeneric, "Malformed connection list: " + errJson); + return false; + } + session.Finish(); + return true; +} + +std::string BuildListNetworkConnections(const Json::Value& id, const Json::Value& args, + CMy2015RemoteDlg* parent) { + HostTool ht; + std::string errJson; + if (!ResolveHost(id, args, parent, ht, /*requireWindows=*/true, errJson)) return errJson; + + Paging paging; + if (!ParsePaging(id, args, paging, errJson)) return errJson; + + std::string protocol = ToLowerAscii(OptStr(args, "protocol")); + if (protocol.empty() || protocol == "all") protocol = "all"; + else if (protocol != "tcp" && protocol != "udp") { + return BuildError(id, -32602, "Invalid protocol: expected tcp, udp or all"); + } + + std::string family = ToLowerAscii(OptStr(args, "family")); + if (family.empty() || family == "all") family = "all"; + else if (family != "ipv4" && family != "ipv6") { + return BuildError(id, -32602, "Invalid family: expected ipv4, ipv6 or all"); + } + + int pid = -1; + if (!GetOptIntArg(id, args, "pid", 0, 0x7FFFFFFF, pid, errJson)) return errJson; + int localPort = -1, remotePort = -1; + if (!GetOptIntArg(id, args, "local_port", 0, 65535, localPort, errJson)) return errJson; + if (!GetOptIntArg(id, args, "remote_port", 0, 65535, remotePort, errJson)) return errJson; + + std::string process = OptStr(args, "process"); + std::string remoteAddr = OptStr(args, "remote_address"); + std::string state = OptStr(args, "state"); + + std::vector records; + if (!ListConnections(id, ht, "list_network_connections", records, errJson)) return errJson; + + Json::Value all(Json::arrayValue); + for (const ConnRecord& rec : records) all.append(ConnRecordToJson(rec)); + + auto pred = [&](const Json::Value& item) { + if (protocol != "all" && item["protocol"].asString() != protocol) return false; + if (family != "all" && item["family"].asString() != family) return false; + if (pid >= 0 && item["pid"].asUInt() != (Json::UInt)pid) return false; + if (localPort >= 0 && item["localPort"].asUInt() != (Json::UInt)localPort) return false; + if (remotePort >= 0 && item["remotePort"].asUInt() != (Json::UInt)remotePort) return false; + if (!MatchCI(item["process"].asString(), process)) return false; + if (!remoteAddr.empty() && !ContainsCI(item["remoteAddress"].asString(), remoteAddr)) return false; + if (!state.empty()) { + if (IsDigits(state)) { + if (item["state"].asUInt() != (Json::UInt)atoi(state.c_str())) return false; + } else if (!ContainsCI(item["stateName"].asString(), state)) { + return false; + } + } + return true; + }; + + int totalAll = (int)all.size(); + int totalFiltered = 0; + bool hasMore = false; + Json::Value page = FilterAndPage(all, pred, paging, totalFiltered, hasMore); + + std::string text = std::string(u8"共 ") + std::to_string(totalFiltered) + u8" 条连接(库中 " + + std::to_string(totalAll) + u8" 条,本次返回 " + std::to_string(page.size()) + u8" 条)"; + return BuildListResult(id, "connections", page, totalFiltered, totalAll, + paging.offset, paging.limit, hasMore, text); +} + +// 解析 IPv4 点分十进制 → 4 字节;仅接受严格 IPv4。 +bool ParseIPv4(const std::string& s, uint8_t out[4]) { + return inet_pton(AF_INET, s.c_str(), out) == 1; +} + +std::string BuildKillNetworkConnection(const Json::Value& id, const Json::Value& args, + CMy2015RemoteDlg* parent) { + CMcpServer& mcp = McpServer(); + if (!mcp.IsManageEnabled() || mcp.IsReadonly()) { + return BuildError(id, kErrDisabled, + "kill_network_connection is disabled (requires McpManage=1 and McpReadonly=0)"); + } + + HostTool ht; + std::string errJson; + if (!ResolveHost(id, args, parent, ht, /*requireWindows=*/true, errJson)) return errJson; + + std::string protocol = ToLowerAscii(OptStr(args, "protocol")); + if (protocol.empty()) protocol = "tcp"; + if (protocol != "tcp" && protocol != "udp") + return BuildError(id, -32602, "Invalid protocol: expected tcp or udp"); + + std::string localAddrStr = OptStr(args, "local_address"); + std::string remoteAddrStr = OptStr(args, "remote_address"); + if (localAddrStr.empty() || remoteAddrStr.empty()) + return BuildError(id, -32602, "local_address and remote_address are required"); + + int localPort = 0, remotePort = 0; + if (!GetOptIntArg(id, args, "local_port", 0, 65535, localPort, errJson)) return errJson; + if (!GetOptIntArg(id, args, "remote_port", 0, 65535, remotePort, errJson)) return errJson; + if (!args.isMember("local_port") || !args.isMember("remote_port")) + return BuildError(id, -32602, "local_port and remote_port are required"); + + int pidFilter = -1; + if (!GetOptIntArg(id, args, "pid", 0, 0x7FFFFFFF, pidFilter, errJson)) return errJson; + bool dryRun = GetBoolArg(args, "dry_run", false); + + uint8_t localBytes[4] = { 0 }, remoteBytes[4] = { 0 }; + if (!ParseIPv4(localAddrStr, localBytes) || !ParseIPv4(remoteAddrStr, remoteBytes)) + return BuildError(id, kErrUnsupported, + "Only IPv4 addresses are supported for forced disconnect " + "(the client can only drop IPv4 TCP connections)"); + + std::string toolName = "kill_network_connection"; + std::vector records; + if (!ListConnections(id, ht, toolName, records, errJson)) return errJson; + + // 精确匹配:协议 + 地址字节 + 端口(+ 可选 pid)。用客户端上报的原始记录,不重拼地址。 + std::string target = protocol + " " + localAddrStr + ":" + std::to_string(localPort) + + " -> " + remoteAddrStr + ":" + std::to_string(remotePort); + const ConnRecord* found = nullptr; + int matches = 0; + for (const ConnRecord& rec : records) { + int wantProto = (protocol == "tcp") ? 1 : 2; + if (rec.proto != wantProto) continue; + if (rec.af != AF_INET) continue; + if (memcmp(rec.localAddr, localBytes, 4) != 0) continue; + if (rec.localPort != (uint16_t)localPort) continue; + if (memcmp(rec.remoteAddr, remoteBytes, 4) != 0) continue; + if (rec.remotePort != (uint16_t)remotePort) continue; + if (pidFilter >= 0 && rec.pid != (uint32_t)pidFilter) continue; + found = &rec; + ++matches; + } + + if (matches == 0) { + AuditManageAction(parent, ht.dev, toolName, target + " result=not_found"); + return NotFoundJson(id, "connection " + target); + } + if (matches > 1) { + return BuildError(id, -32602, + "Ambiguous target: " + std::to_string(matches) + + " connections match; specify pid to disambiguate"); + } + + Json::Value targetJson = ConnRecordToJson(*found); + if (found->proto != 1 || found->af != AF_INET) { + return BuildError(id, kErrUnsupported, + "Only IPv4 TCP connections can be forcibly disconnected"); + } + if (pidFilter < 0) pidFilter = (int)found->pid; + + if (dryRun) { + Json::Value sc(Json::objectValue); + sc["ok"] = true; + sc["dryRun"] = true; + sc["target"] = target; + sc["connection"] = targetJson; + AuditManageAction(parent, ht.dev, toolName, target + " pid=" + std::to_string(pidFilter) + + " result=dry_run"); + return BuildStructuredResult(id, sc, "Dry run: target found, nothing was sent"); + } + + // 会话保持:取列表用的是第一条 255,断开后客户端会自动再回一条 255。 + ManagedSession session; + std::vector first; + if (!session.Open(id, toolName, ht, /*connMgr=*/true, &first, errJson)) return errJson; + std::vector pre; + if (!ParseConnections(first, pre, errJson)) { + errJson = BuildError(id, kErrGeneric, "Malformed connection list: " + errJson); + return errJson; + } + // 重新在最新列表里定位(模板绑定期间列表可能变化;原始记录也必须来自最新包) + const ConnRecord* latest = nullptr; + int latestMatches = 0; + for (const ConnRecord& rec : pre) { + if (rec.proto != 1 || rec.af != AF_INET) continue; + if (memcmp(rec.localAddr, localBytes, 4) != 0) continue; + if (rec.localPort != (uint16_t)localPort) continue; + if (memcmp(rec.remoteAddr, remoteBytes, 4) != 0) continue; + if (rec.remotePort != (uint16_t)remotePort) continue; + if (pidFilter >= 0 && rec.pid != (uint32_t)pidFilter) continue; + latest = &rec; + ++latestMatches; + } + if (latestMatches != 1) { + AuditManageAction(parent, ht.dev, toolName, + target + " result=not_found(stale)"); + return latestMatches == 0 ? NotFoundJson(id, "connection " + target) + : BuildError(id, -32602, + "Ambiguous target in the latest snapshot; specify pid"); + } + + std::vector pkt; + pkt.reserve(1 + kConnRecordSize); + pkt.push_back((BYTE)COMMAND_KILL_CONNECTION); + pkt.insert(pkt.end(), latest->raw, latest->raw + kConnRecordSize); + + std::vector after; + if (!session.Action(id, pkt, (BYTE)TOKEN_CONNECTION_LIST, after, errJson)) { + AuditManageAction(parent, ht.dev, toolName, + target + " pid=" + std::to_string(pidFilter) + " result=send_or_timeout"); + return errJson; + } + + std::vector post; + std::string perr; + if (!ParseConnections(after, post, perr)) { + AuditManageAction(parent, ht.dev, toolName, + target + " pid=" + std::to_string(pidFilter) + " result=bad_refresh"); + return BuildError(id, kErrGeneric, + "The action was sent but the refreshed list could not be parsed: " + perr); + } + + bool stillPresent = false; + for (const ConnRecord& rec : post) { + if (rec.proto != 1 || rec.af != AF_INET) continue; + if (memcmp(rec.localAddr, localBytes, 4) != 0) continue; + if (rec.localPort != (uint16_t)localPort) continue; + if (memcmp(rec.remoteAddr, remoteBytes, 4) != 0) continue; + if (rec.remotePort != (uint16_t)remotePort) continue; + if (pidFilter >= 0 && rec.pid != (uint32_t)pidFilter) continue; + stillPresent = true; + break; + } + session.Finish(); + + AuditManageAction(parent, ht.dev, toolName, + target + " pid=" + std::to_string(pidFilter) + + (stillPresent ? " result=still_present" : " result=disconnected")); + + if (stillPresent) { + return BuildActionResult(id, false, true, false, "kill", target, "present", + "still_present", + u8"连接仍在:客户端可能没有管理员权限(SetTcpEntry 失败)," + u8"或该连接已被系统重建。"); + } + return BuildActionResult(id, true, true, true, "kill", target, "removed", "", + u8"连接已断开(刷新后的连接列表中四元组已消失)。"); +} + +} // namespace + +// ============================================================================ +// 主机管理:读工具 +// ============================================================================ + +namespace { + +// 打开主机管理会话 → 发 1 字节 tab → 取响应 → 结束会话。 +bool FetchMachineTab(const Json::Value& id, const HostTool& ht, const std::string& toolName, + BYTE tab, BYTE respToken, std::vector& out, std::string& errJson) { + ManagedSession session; + if (!session.Open(id, toolName, ht, /*connMgr=*/false, nullptr, errJson)) return false; + if (!session.Request(id, tab, respToken, out, errJson)) return false; + session.Finish(); + return true; +} + +std::string ParseErrorToJson(const Json::Value& id, const char* what, const std::string& detail) { + return BuildError(id, kErrGeneric, + std::string("Malformed ") + what + " response: " + detail); +} + +} // namespace + +// ---- host_list_processes ---- +static std::string BuildHostListProcesses(const Json::Value& id, const Json::Value& args, + CMy2015RemoteDlg* parent) { + HostTool ht; + std::string errJson; + if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson; + + Paging paging; + if (!ParsePaging(id, args, paging, errJson)) return errJson; + + int pidFilter = -1; + if (!GetOptIntArg(id, args, "pid", 0, 0x7FFFFFFF, pidFilter, errJson)) return errJson; + std::string name = OptStr(args, "name"); + std::string arch = ToLowerAscii(OptStr(args, "arch")); + if (!arch.empty() && arch != "x64" && arch != "x86") + return BuildError(id, -32602, "Invalid arch: expected x64 or x86"); + + std::vector data; + if (!FetchMachineTab(id, ht, "host_list_processes", (BYTE)COMMAND_MACHINE_PROCESS, + (BYTE)TOKEN_MACHINE_PROCESS, data, errJson)) + return errJson; + + Json::Value all; + std::string perr; + if (!ParseProcesses(data, 936, all, perr)) return ParseErrorToJson(id, "process list", perr); + + auto pred = [&](const Json::Value& item) { + if (pidFilter >= 0 && item["pid"].asUInt() != (Json::UInt)pidFilter) return false; + if (!MatchCI(item["name"].asString(), name)) return false; + if (!arch.empty() && item["arch"].asString() != arch) return false; + return true; + }; + + int totalAll = (int)all.size(), total = 0; + bool hasMore = false; + Json::Value page = FilterAndPage(all, pred, paging, total, hasMore); + std::string text = std::string(u8"共 ") + std::to_string(total) + u8" 个进程(全部 " + + std::to_string(totalAll) + u8" 个)"; + return BuildListResult(id, "processes", page, total, totalAll, + paging.offset, paging.limit, hasMore, text); +} + +// ---- host_list_windows ---- +static std::string BuildHostListWindows(const Json::Value& id, const Json::Value& args, + CMy2015RemoteDlg* parent) { + HostTool ht; + std::string errJson; + if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson; + + Paging paging; + if (!ParsePaging(id, args, paging, errJson)) return errJson; + + int pidFilter = -1; + if (!GetOptIntArg(id, args, "pid", 0, 0x7FFFFFFF, pidFilter, errJson)) return errJson; + std::string title = OptStr(args, "title"); + + std::vector data; + if (!FetchMachineTab(id, ht, "host_list_windows", (BYTE)COMMAND_MACHINE_WINDOWS, + (BYTE)TOKEN_MACHINE_WINDOWS, data, errJson)) + return errJson; + + Json::Value all; + std::string perr; + if (!ParseWindows(data, 936, all, perr)) return ParseErrorToJson(id, "window list", perr); + + auto pred = [&](const Json::Value& item) { + if (pidFilter >= 0 && item["pid"].asUInt() != (Json::UInt)pidFilter) return false; + if (!MatchCI(item["title"].asString(), title)) return false; + return true; + }; + + int totalAll = (int)all.size(), total = 0; + bool hasMore = false; + Json::Value page = FilterAndPage(all, pred, paging, total, hasMore); + std::string text = std::string(u8"共 ") + std::to_string(total) + u8" 个可见窗口(全部 " + + std::to_string(totalAll) + u8" 个)"; + return BuildListResult(id, "windows", page, total, totalAll, + paging.offset, paging.limit, hasMore, text); +} + +// ---- host_list_network_state ---- +static std::string BuildHostListNetworkState(const Json::Value& id, const Json::Value& args, + CMy2015RemoteDlg* parent) { + HostTool ht; + std::string errJson; + if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson; + + Paging paging; + if (!ParsePaging(id, args, paging, errJson)) return errJson; + + int pidFilter = -1, localPort = -1, remotePort = -1; + if (!GetOptIntArg(id, args, "pid", 0, 0x7FFFFFFF, pidFilter, errJson)) return errJson; + if (!GetOptIntArg(id, args, "local_port", 0, 65535, localPort, errJson)) return errJson; + if (!GetOptIntArg(id, args, "remote_port", 0, 65535, remotePort, errJson)) return errJson; + std::string process = OptStr(args, "process"); + std::string protocol = ToLowerAscii(OptStr(args, "protocol")); + std::string state = OptStr(args, "state"); + + std::vector data; + if (!FetchMachineTab(id, ht, "host_list_network_state", (BYTE)COMMAND_MACHINE_NETSTATE, + (BYTE)TOKEN_MACHINE_NETSTATE, data, errJson)) + return errJson; + + Json::Value all; + std::string perr; + if (!ParseNetState(data, 936, all, perr)) return ParseErrorToJson(id, "network state", perr); + + auto pred = [&](const Json::Value& item) { + if (pidFilter >= 0 && item["pid"].asUInt() != (Json::UInt)pidFilter) return false; + if (localPort >= 0 && item["localPort"].asInt() != localPort) return false; + if (remotePort >= 0 && item["remotePort"].asInt() != remotePort) return false; + if (!process.empty() && !MatchCI(item["process"].asString(), process)) return false; + if (!protocol.empty() && protocol != "all" && item["protocol"].asString() != protocol) + return false; + if (!state.empty() && !MatchCI(item["state"].asString(), state)) return false; + return true; + }; + + int totalAll = (int)all.size(), total = 0; + bool hasMore = false; + Json::Value page = FilterAndPage(all, pred, paging, total, hasMore); + std::string text = std::string(u8"共 ") + std::to_string(total) + u8" 条网络连接(全部 " + + std::to_string(totalAll) + u8" 条)"; + return BuildListResult(id, "connections", page, total, totalAll, + paging.offset, paging.limit, hasMore, text); +} + +// ---- host_list_software ---- +static std::string BuildHostListSoftware(const Json::Value& id, const Json::Value& args, + CMy2015RemoteDlg* parent) { + HostTool ht; + std::string errJson; + if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson; + + Paging paging; + if (!ParsePaging(id, args, paging, errJson)) return errJson; + std::string name = OptStr(args, "name"); + std::string publisher = OptStr(args, "publisher"); + + std::vector data; + if (!FetchMachineTab(id, ht, "host_list_software", (BYTE)COMMAND_MACHINE_SOFTWARE, + (BYTE)TOKEN_MACHINE_SOFTWARE, data, errJson)) + return errJson; + + Json::Value all; + std::string perr; + if (!ParseSoftware(data, 936, all, perr)) return ParseErrorToJson(id, "software list", perr); + + auto pred = [&](const Json::Value& item) { + if (!MatchCI(item["name"].asString(), name)) return false; + if (!MatchCI(item["publisher"].asString(), publisher)) return false; + return true; + }; + + int totalAll = (int)all.size(), total = 0; + bool hasMore = false; + Json::Value page = FilterAndPage(all, pred, paging, total, hasMore); + std::string text = std::string(u8"共 ") + std::to_string(total) + u8" 个已安装软件(全部 " + + std::to_string(totalAll) + u8" 个)"; + return BuildListResult(id, "software", page, total, totalAll, + paging.offset, paging.limit, hasMore, text); +} + +// ---- host_list_browser_history ---- +static std::string BuildHostListBrowserHistory(const Json::Value& id, const Json::Value& args, + CMy2015RemoteDlg* parent) { + HostTool ht; + std::string errJson; + if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson; + + Paging paging; + if (!ParsePaging(id, args, paging, errJson)) return errJson; + std::string title = OptStr(args, "title"); + std::string url = OptStr(args, "url"); + + std::vector data; + if (!FetchMachineTab(id, ht, "host_list_browser_history", (BYTE)COMMAND_MACHINE_HTML, + (BYTE)TOKEN_MACHINE_HTML, data, errJson)) + return errJson; + + Json::Value all; + std::string perr; + if (!ParseBrowserHistory(data, 936, all, perr)) + return ParseErrorToJson(id, "browser history", perr); + + auto pred = [&](const Json::Value& item) { + if (!MatchCI(item["title"].asString(), title)) return false; + if (!MatchCI(item["url"].asString(), url)) return false; + return true; + }; + + int totalAll = (int)all.size(), total = 0; + bool hasMore = false; + Json::Value page = FilterAndPage(all, pred, paging, total, hasMore); + std::string text = std::string(u8"共 ") + std::to_string(total) + u8" 条浏览记录(全部 " + + std::to_string(totalAll) + u8" 条)"; + return BuildListResult(id, "history", page, total, totalAll, + paging.offset, paging.limit, hasMore, text); +} + +// ---- host_list_favorites ---- +static std::string BuildHostListFavorites(const Json::Value& id, const Json::Value& args, + CMy2015RemoteDlg* parent) { + HostTool ht; + std::string errJson; + if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson; + + Paging paging; + if (!ParsePaging(id, args, paging, errJson)) return errJson; + std::string name = OptStr(args, "name"); + std::string url = OptStr(args, "url"); + + std::vector data; + if (!FetchMachineTab(id, ht, "host_list_favorites", (BYTE)COMMAND_MACHINE_FAVORITES, + (BYTE)TOKEN_MACHINE_FAVORITES, data, errJson)) + return errJson; + + Json::Value all; + std::string perr; + if (!ParseFavorites(data, 936, all, perr)) return ParseErrorToJson(id, "favorites", perr); + + auto pred = [&](const Json::Value& item) { + if (!MatchCI(item["name"].asString(), name)) return false; + if (!MatchCI(item["url"].asString(), url)) return false; + return true; + }; + + int totalAll = (int)all.size(), total = 0; + bool hasMore = false; + Json::Value page = FilterAndPage(all, pred, paging, total, hasMore); + std::string text = std::string(u8"共 ") + std::to_string(total) + u8" 个收藏(全部 " + + std::to_string(totalAll) + u8" 个)"; + return BuildListResult(id, "favorites", page, total, totalAll, + paging.offset, paging.limit, hasMore, text); +} + +// ---- host_list_win32_services / host_list_driver_services ---- +static std::string BuildHostListServices(const Json::Value& id, const Json::Value& args, + CMy2015RemoteDlg* parent, bool drivers) { + HostTool ht; + std::string errJson; + const char* toolName = drivers ? "host_list_driver_services" : "host_list_win32_services"; + BYTE tab = drivers ? (BYTE)COMMAND_MACHINE_DRIVERSERVICE : (BYTE)COMMAND_MACHINE_WIN32SERVICE; + if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson; + + Paging paging; + if (!ParsePaging(id, args, paging, errJson)) return errJson; + std::string name = OptStr(args, "name"); + std::string status = OptStr(args, "status"); + std::string startType = OptStr(args, "start_type"); + + std::vector data; + if (!FetchMachineTab(id, ht, toolName, tab, (BYTE)TOKEN_MACHINE_SERVICE_LIST, data, errJson)) + return errJson; + + Json::Value all; + std::string perr; + if (!ParseServices(data, 936, all, perr)) return ParseErrorToJson(id, "service list", perr); + + auto pred = [&](const Json::Value& item) { + if (!name.empty() && + !(MatchCI(item["name"].asString(), name) || MatchCI(item["displayName"].asString(), name))) + return false; + if (!MatchCI(item["state"].asString(), status)) return false; + if (!MatchCI(item["startType"].asString(), startType)) return false; + return true; + }; + + int totalAll = (int)all.size(), total = 0; + bool hasMore = false; + Json::Value page = FilterAndPage(all, pred, paging, total, hasMore); + std::string text = std::string(u8"共 ") + std::to_string(total) + u8" 个服务(全部 " + + std::to_string(totalAll) + u8" 个)"; + return BuildListResult(id, "services", page, total, totalAll, + paging.offset, paging.limit, hasMore, text); +} + +// ---- host_list_scheduled_tasks ---- +static std::string BuildHostListScheduledTasks(const Json::Value& id, const Json::Value& args, + CMy2015RemoteDlg* parent) { + HostTool ht; + std::string errJson; + if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson; + + Paging paging; + if (!ParsePaging(id, args, paging, errJson)) return errJson; + std::string name = OptStr(args, "name"); + std::string folder = OptStr(args, "folder"); + std::string status = OptStr(args, "status"); + + std::vector data; + if (!FetchMachineTab(id, ht, "host_list_scheduled_tasks", (BYTE)COMMAND_MACHINE_TASK, + (BYTE)TOKEN_MACHINE_TASKLIST, data, errJson)) + return errJson; + + Json::Value all; + std::string perr; + if (!ParseTasks(data, all, perr)) return ParseErrorToJson(id, "scheduled task list", perr); + + auto pred = [&](const Json::Value& item) { + if (!MatchCI(item["name"].asString(), name)) return false; + if (!folder.empty() && !MatchCI(item["folder"].asString(), folder)) return false; + if (!MatchCI(item["status"].asString(), status)) return false; + return true; + }; + + int totalAll = (int)all.size(), total = 0; + bool hasMore = false; + Json::Value page = FilterAndPage(all, pred, paging, total, hasMore); + std::string text = std::string(u8"共 ") + std::to_string(total) + u8" 个计划任务(全部 " + + std::to_string(totalAll) + u8" 个)"; + return BuildListResult(id, "tasks", page, total, totalAll, + paging.offset, paging.limit, hasMore, text); +} + +// ---- host_get_hosts_file ---- +static std::string BuildHostGetHostsFile(const Json::Value& id, const Json::Value& args, + CMy2015RemoteDlg* parent) { + HostTool ht; + std::string errJson; + if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson; + + int maxBytes = 256 * 1024; + if (!GetOptIntArg(id, args, "max_bytes", 1, 1024 * 1024, maxBytes, errJson)) return errJson; + bool includeContent = GetBoolArg(args, "include_content", true); + + std::vector data; + if (!FetchMachineTab(id, ht, "host_get_hosts_file", (BYTE)COMMAND_MACHINE_HOSTS, + (BYTE)TOKEN_MACHINE_HOSTS, data, errJson)) + return errJson; + + // [39][文件原文][2 个 0 字节]:取到第一个 NUL 为止。 + if (data.size() < 1) return BuildError(id, kErrGeneric, "Empty hosts response"); + size_t n = data.size() - 1; + size_t strLen = BoundedStrlen((const char*)data.data() + 1, n); + + const BYTE* raw = data.data() + 1; + UINT cp = 936; + size_t skip = 0; + if (strLen >= 3 && raw[0] == 0xEF && raw[1] == 0xBB && raw[2] == 0xBF) { + cp = CP_UTF8; + skip = 3; + } else if (IsValidUtf8((const char*)raw, strLen)) { + cp = CP_UTF8; + } + + size_t contentLen = strLen - skip; + bool truncated = contentLen > (size_t)maxBytes; + size_t take = truncated ? (size_t)maxBytes : contentLen; + + std::string text = ToUtf8(std::string((const char*)raw + skip, take).c_str(), cp); + + Json::Value sc(Json::objectValue); + sc["path"] = "%SystemRoot%\\System32\\drivers\\etc\\hosts"; + sc["encoding"] = (cp == CP_UTF8) ? "utf-8" : "gbk"; + sc["bytes"] = (Json::UInt)contentLen; + sc["returnedBytes"] = (Json::UInt)take; + sc["truncated"] = truncated; + + if (includeContent) { + // 按行切分(去掉 \r,跳过末尾空行);截断时丢弃最后一行不完整的内容。 + Json::Value lines(Json::arrayValue); + size_t pos = 0; + bool droppedPartial = false; + while (pos < text.size()) { + size_t nl = text.find('\n', pos); + bool last = (nl == std::string::npos); + std::string line = last ? text.substr(pos) : text.substr(pos, nl - pos); + if (!line.empty() && line[line.size() - 1] == '\r') line.erase(line.size() - 1); + if (truncated && last) { droppedPartial = true; break; } + lines.append(line); + if (last) break; + pos = nl + 1; + } + if (droppedPartial) sc["truncatedAtLineBoundary"] = true; + sc["lines"] = lines; + sc["lineCount"] = (Json::UInt)lines.size(); + if (!truncated) sc["content"] = text; + } + + std::string summary = std::string(u8"hosts 文件 ") + std::to_string(contentLen) + u8" 字节" + + (truncated ? u8"(已截断)" : ""); + return BuildStructuredResult(id, sc, summary); +} + +// ============================================================================ +// 主机管理:写工具(只读 + 可恢复操作) +// ============================================================================ + +namespace { + +bool RequireWritable(const Json::Value& id, std::string& errJson) { + if (McpServer().IsReadonly()) { + errJson = BuildError(id, kErrDisabled, + "Host management write operations are disabled: MCP is in read-only mode " + "(set McpReadonly=0 to enable)"); + return false; + } + return true; +} + +bool RequireDangerous(const Json::Value& id, std::string& errJson) { + if (!McpServer().IsMachineDangerousEnabled()) { + errJson = BuildError(id, kErrDisabled, + "This action is disabled: set McpMachineDangerous=1 to enable it"); + return false; + } + return true; +} + +// 从 JSON 读 hwnd:接受整数、十进制字符串或 0x 前缀十六进制字符串。 +bool ReadHwndArg(const Json::Value& id, const Json::Value& args, uint32_t& out, std::string& errJson) { + if (!args.isMember("hwnd")) { + errJson = BuildError(id, -32602, "Missing required parameter: hwnd"); + return false; + } + const Json::Value& v = args["hwnd"]; + if (v.isNumeric()) { + out = (uint32_t)v.asUInt64(); + return true; + } + if (v.isString()) { + std::string s = Trim(v.asString()); + if (s.size() > 2 && (s[0] == '0') && (s[1] == 'x' || s[1] == 'X')) { + out = (uint32_t)strtoul(s.c_str() + 2, nullptr, 16); + return true; + } + if (IsDigits(s)) { + out = (uint32_t)strtoul(s.c_str(), nullptr, 10); + return true; + } + } + errJson = BuildError(id, -32602, "Invalid hwnd: expected an integer, decimal string or 0x-hex string"); + return false; +} + +const Json::Value* FindProcess(const Json::Value& list, DWORD pid) { + for (Json::ArrayIndex i = 0; i < list.size(); ++i) { + if (list[i]["pid"].asUInt() == (Json::UInt)pid) return &list[i]; + } + return nullptr; +} + +const Json::Value* FindWindow(const Json::Value& list, DWORD hwnd) { + for (Json::ArrayIndex i = 0; i < list.size(); ++i) { + if (list[i]["hwnd"].asUInt() == (Json::UInt)hwnd) return &list[i]; + } + return nullptr; +} + +const Json::Value* FindService(const Json::Value& list, const std::string& name) { + for (Json::ArrayIndex i = 0; i < list.size(); ++i) { + if (EqualsCI(list[i]["name"].asString(), name)) return &list[i]; + } + return nullptr; +} + +bool ServiceStateIn(const std::string& state, std::initializer_list accepted) { + for (const char* s : accepted) { + if (EqualsCI(state, s)) return true; + } + return false; +} + +// 服务 tab 先取列表(同时把客户端 m_dwServiceType 钉到正确类型)。 +bool FetchServicesList(const Json::Value& id, ManagedSession& session, BYTE tab, + Json::Value& all, std::string& errJson) { + std::vector data; + if (!session.Request(id, tab, (BYTE)TOKEN_MACHINE_SERVICE_LIST, data, errJson)) return false; + std::string perr; + if (!ParseServices(data, 936, all, perr)) { + errJson = ParseErrorToJson(id, "service list", perr); + return false; + } + return true; +} + +} // namespace + +// ---- host_control_process(freeze / thaw) ---- +static std::string BuildHostControlProcess(const Json::Value& id, const Json::Value& args, + CMy2015RemoteDlg* parent) { + std::string errJson; + if (!RequireWritable(id, errJson)) return errJson; + + std::string action = ToLowerAscii(OptStr(args, "action")); + if (action != "freeze" && action != "thaw") + return BuildError(id, -32602, "Invalid action: expected freeze or thaw"); + if (!RequireDangerous(id, errJson)) return errJson; + + int pidFilter = -1; + if (!GetOptIntArg(id, args, "pid", 0, 0x7FFFFFFF, pidFilter, errJson)) return errJson; + if (pidFilter < 0) return BuildError(id, -32602, "Missing required parameter: pid"); + std::string expectedName = OptStr(args, "name"); + + HostTool ht; + if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson; + + ManagedSession session; + if (!session.Open(id, "host_control_process", ht, false, nullptr, errJson)) return errJson; + + std::vector data; + if (!session.Request(id, (BYTE)COMMAND_MACHINE_PROCESS, (BYTE)TOKEN_MACHINE_PROCESS, + data, errJson)) + return errJson; + Json::Value pre; + std::string perr; + if (!ParseProcesses(data, 936, pre, perr)) return ParseErrorToJson(id, "process list", perr); + + const Json::Value* proc = FindProcess(pre, (DWORD)pidFilter); + if (!proc) { + AuditManageAction(parent, ht.dev, "host_control_process", + action + " pid=" + std::to_string(pidFilter) + " result=not_found"); + return NotFoundJson(id, "process pid=" + std::to_string(pidFilter)); + } + if (!expectedName.empty() && !EqualsCI(proc->get("name", Json::Value("")).asString(), expectedName)) { + // PID 可能已被系统复用:名字对不上时拒绝,避免误冻别的进程 + return NotFoundJson(id, "process pid=" + std::to_string(pidFilter) + + " named '" + expectedName + "' (current name: " + + proc->get("name", Json::Value("")).asString() + ")"); + } + + std::vector pkt(5); + pkt[0] = (action == "freeze") ? (BYTE)COMMAND_PROCESS_FREEZING + : (BYTE)COMMAND_PROCESS_THAW; + DWORD pid32 = (DWORD)pidFilter; + memcpy(&pkt[1], &pid32, 4); + if (!session.SendNoReply(id, pkt, errJson)) { + AuditManageAction(parent, ht.dev, "host_control_process", + action + " pid=" + std::to_string(pidFilter) + " result=send_failed"); + return errJson; + } + + // 动作不回包:再取一次进程列表复核目标仍在。 + std::vector after; + if (!session.Request(id, (BYTE)COMMAND_MACHINE_PROCESS, (BYTE)TOKEN_MACHINE_PROCESS, + after, errJson)) + return errJson; + Json::Value post; + if (!ParseProcesses(after, 936, post, perr)) return ParseErrorToJson(id, "process list", perr); + session.Finish(); + + const Json::Value* now = FindProcess(post, (DWORD)pidFilter); + bool present = now != nullptr; + AuditManageAction(parent, ht.dev, "host_control_process", + action + " pid=" + std::to_string(pidFilter) + + " result=" + (present ? "present" : "missing")); + + if (!present) { + return BuildActionResult(id, false, true, false, action, + "pid=" + std::to_string(pidFilter), "missing", "process_gone", + u8"动作已下发,但复核时进程已不存在(可能已自行退出)。"); + } + // 冻结不改变「进程是否存在」,客户端也不回报结果:只能确认动作已发送且目标仍在。 + return BuildActionResult(id, true, true, true, action, + "pid=" + std::to_string(pidFilter), "present", "", + u8"动作已下发,复核时进程仍在。注意:冻结/解冻需要客户端管理员权限," + u8"若未生效只能通过目标进程的行为确认。"); +} + +// ---- host_control_window(restore / hide / maximize / minimize) ---- +static std::string BuildHostControlWindow(const Json::Value& id, const Json::Value& args, + CMy2015RemoteDlg* parent) { + std::string errJson; + if (!RequireWritable(id, errJson)) return errJson; + + std::string action = ToLowerAscii(OptStr(args, "action")); + DWORD sw = 0; + if (action == "restore") sw = SW_RESTORE; + else if (action == "hide") sw = SW_HIDE; + else if (action == "maximize") sw = SW_SHOWMAXIMIZED; + else if (action == "minimize") sw = SW_SHOWMINIMIZED; + else return BuildError(id, -32602, "Invalid action: expected restore, hide, maximize or minimize"); + + uint32_t hwnd = 0; + if (!ReadHwndArg(id, args, hwnd, errJson)) return errJson; + + HostTool ht; + if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson; + + ManagedSession session; + if (!session.Open(id, "host_control_window", ht, false, nullptr, errJson)) return errJson; + + std::vector data; + if (!session.Request(id, (BYTE)COMMAND_MACHINE_WINDOWS, (BYTE)TOKEN_MACHINE_WINDOWS, + data, errJson)) + return errJson; + Json::Value pre; + std::string perr; + if (!ParseWindows(data, 936, pre, perr)) return ParseErrorToJson(id, "window list", perr); + if (!FindWindow(pre, hwnd)) { + AuditManageAction(parent, ht.dev, "host_control_window", + action + " hwnd=" + std::to_string(hwnd) + " result=not_found"); + return NotFoundJson(id, "window hwnd=" + std::to_string(hwnd)); + } + + // [11][hwnd][SW_*] + 零填充到 20 字节(与 MFC 下发格式一致;客户端只读前 9 字节) + std::vector pkt(20, 0); + pkt[0] = (BYTE)COMMAND_WINDOW_OPERATE; + DWORD hwnd32 = (DWORD)hwnd; + memcpy(&pkt[1], &hwnd32, 4); + memcpy(&pkt[5], &sw, 4); + if (!session.SendNoReply(id, pkt, errJson)) { + AuditManageAction(parent, ht.dev, "host_control_window", + action + " hwnd=" + std::to_string(hwnd) + " result=send_failed"); + return errJson; + } + + std::vector after; + if (!session.Request(id, (BYTE)COMMAND_MACHINE_WINDOWS, (BYTE)TOKEN_MACHINE_WINDOWS, + after, errJson)) + return errJson; + Json::Value post; + if (!ParseWindows(after, 936, post, perr)) return ParseErrorToJson(id, "window list", perr); + session.Finish(); + + bool present = FindWindow(post, hwnd) != nullptr; + // 列表只含可见窗口:hide 后应消失;其余动作应仍然可见(最小化不改变可见性)。 + bool expectedPresent = (action != "hide"); + std::string state = present ? "visible" : "hidden"; + + AuditManageAction(parent, ht.dev, "host_control_window", + action + " hwnd=" + std::to_string(hwnd) + " result=" + state); + + if (present != expectedPresent) { + return BuildActionResult(id, false, true, false, action, + "hwnd=" + std::to_string(hwnd), state, "unexpected_state", + u8"动作已下发,但复核结果与预期不符(窗口可能已关闭," + u8"或该操作需要更高权限)。"); + } + return BuildActionResult(id, true, true, true, action, + "hwnd=" + std::to_string(hwnd), state, "", + action == "hide" ? u8"窗口已隐藏(可见窗口列表中已消失)。" + : u8"动作已生效,窗口仍在可见窗口列表中。"); +} + +// ---- host_control_service(start / pause / continue / stop / restart) ---- +static std::string BuildHostControlService(const Json::Value& id, const Json::Value& args, + CMy2015RemoteDlg* parent) { + std::string errJson; + if (!RequireWritable(id, errJson)) return errJson; + + std::string action = ToLowerAscii(OptStr(args, "action")); + bool dangerous = (action == "stop" || action == "restart"); + if (action != "start" && action != "stop" && action != "pause" && + action != "continue" && action != "restart") + return BuildError(id, -32602, + "Invalid action: expected start, stop, pause, continue or restart"); + if (dangerous && !RequireDangerous(id, errJson)) return errJson; + + std::string serviceName = Trim(GetStringArg(args, "name")); + if (serviceName.empty()) + return BuildError(id, -32602, "Missing required parameter: name (service name)"); + + HostTool ht; + if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson; + + std::string ansiName; + if (!Utf8ToCpExact(serviceName, 936, ansiName)) { + return BuildError(id, -32602, + "Service name contains characters that cannot be represented in the " + "client's ANSI code page (CP936)"); + } + + ManagedSession session; + if (!session.Open(id, "host_control_service", ht, false, nullptr, errJson)) return errJson; + + // prime:优先 Win32 服务,找不到再查驱动服务(同时钉住客户端 m_dwServiceType)。 + Json::Value list; + BYTE serviceTab = (BYTE)COMMAND_MACHINE_WIN32SERVICE; + if (!FetchServicesList(id, session, serviceTab, list, errJson)) return errJson; + const Json::Value* svc = FindService(list, serviceName); + if (!svc) { + serviceTab = (BYTE)COMMAND_MACHINE_DRIVERSERVICE; + if (!FetchServicesList(id, session, serviceTab, list, errJson)) return errJson; + svc = FindService(list, serviceName); + } + if (!svc) { + AuditManageAction(parent, ht.dev, "host_control_service", + action + " " + serviceName + " result=not_found"); + return NotFoundJson(id, "service '" + serviceName + "'"); + } + + std::string initial = svc->get("state", Json::Value("")).asString(); + + // 客户端只在状态满足时才真正动作:先按同样的规则判断,避免把 no-op 报成成功。 + auto stateIs = [](const std::string& s, const char* want) { return EqualsCI(s, want); }; + if (action == "start" && stateIs(initial, "Running")) { + session.Finish(); + return BuildActionResult(id, true, true, false, action, serviceName, initial, "already_running", + u8"服务已是 Running,未下发动作。"); + } + if (action == "stop" && stateIs(initial, "Stopped")) { + session.Finish(); + return BuildActionResult(id, true, true, false, action, serviceName, initial, "already_stopped", + u8"服务已是 Stopped,未下发动作。"); + } + if (action == "pause" && stateIs(initial, "Paused")) { + session.Finish(); + return BuildActionResult(id, true, true, false, action, serviceName, initial, "already_paused", + u8"服务已是 Paused,未下发动作。"); + } + + auto sendServiceAction = [&](BYTE cmd, const char* label, + std::vector& resp, std::string& err) -> bool { + std::vector pkt; + pkt.reserve(1 + ansiName.size() + 1); + pkt.push_back(cmd); + pkt.insert(pkt.end(), ansiName.begin(), ansiName.end()); + pkt.push_back(0); + return session.Action(id, pkt, (BYTE)TOKEN_MACHINE_SERVICE_LIST, resp, err); + }; + + auto findState = [&](const std::vector& resp, std::string& state, + std::string& err) -> bool { + Json::Value after; + if (!ParseServices(resp, 936, after, err)) return false; + const Json::Value* now = FindService(after, serviceName); + if (!now) return false; + state = now->get("state", Json::Value("")).asString(); + return true; + }; + + std::vector resp; + std::string state; + + if (action != "restart") { + BYTE cmd = (action == "start") ? (BYTE)COMMAND_STARTSERVERICE + : (action == "stop") ? (BYTE)COMMAND_STOPSERVERICE + : (action == "pause") ? (BYTE)COMMAND_PAUSESERVERICE + : (BYTE)COMMAND_CONTINUESERVERICE; + if (!sendServiceAction(cmd, action.c_str(), resp, errJson)) { + AuditManageAction(parent, ht.dev, "host_control_service", + action + " " + serviceName + " result=send_or_timeout"); + return errJson; + } + } else { + // restart = stop → 等刷新 → start → 等刷新 + if (!sendServiceAction((BYTE)COMMAND_STOPSERVERICE, "stop", resp, errJson)) { + AuditManageAction(parent, ht.dev, "host_control_service", + "restart " + serviceName + " result=stop_send_or_timeout"); + return errJson; + } + std::string sErr; + if (!findState(resp, state, sErr)) { + return BuildError(id, kErrGeneric, + "Service list after stop could not be parsed: " + sErr); + } + if (!sendServiceAction((BYTE)COMMAND_STARTSERVERICE, "start", resp, errJson)) { + AuditManageAction(parent, ht.dev, "host_control_service", + "restart " + serviceName + " (stop state=" + state + + ") result=start_send_or_timeout"); + return errJson; + } + } + + std::string sErr; + bool listed = findState(resp, state, sErr); + session.Finish(); + + if (!listed) { + AuditManageAction(parent, ht.dev, "host_control_service", + action + " " + serviceName + " result=missing_or_bad_list"); + return BuildActionResult(id, false, true, true, action, serviceName, "missing", + "service_missing_or_bad_refresh", + u8"动作已下发,但刷新后的服务列表里找不到该服务或无法解析。"); + } + + bool ok = false; + if (action == "start" || action == "continue" || action == "restart") + ok = ServiceStateIn(state, { "Running", "Starting", "Continue" }); + else if (action == "stop") + ok = ServiceStateIn(state, { "Stopped", "Stopping" }); + else if (action == "pause") + ok = ServiceStateIn(state, { "Paused", "Pausing" }); + + AuditManageAction(parent, ht.dev, "host_control_service", + action + " " + serviceName + " initial=" + initial + + " result=" + state + (ok ? " (ok)" : " (unexpected)")); + + if (!ok) { + return BuildActionResult(id, false, true, true, action, serviceName, state, + "unexpected_state", + u8"动作已下发,但复核时服务状态不在预期集合内" + u8"(可能需要管理员权限,或服务拒绝该控制请求)。"); + } + return BuildActionResult(id, true, true, true, action, serviceName, state, "", + action == "restart" ? u8"已先停止再启动,复核状态符合预期。" + : u8"复核时服务状态符合预期。"); +} + +// ---- host_control_task(run / stop) ---- +static std::string BuildHostControlTask(const Json::Value& id, const Json::Value& args, + CMy2015RemoteDlg* parent) { + std::string errJson; + if (!RequireWritable(id, errJson)) return errJson; + + std::string action = ToLowerAscii(OptStr(args, "action")); + if (action != "run" && action != "stop") + return BuildError(id, -32602, "Invalid action: expected run or stop"); + + std::string taskName = Trim(GetStringArg(args, "name")); + if (taskName.empty()) return BuildError(id, -32602, "Missing required parameter: name"); + std::string wantedFolder = Trim(GetStringArg(args, "folder")); + + HostTool ht; + if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson; + + ManagedSession session; + if (!session.Open(id, "host_control_task", ht, false, nullptr, errJson)) return errJson; + + std::vector data; + if (!session.Request(id, (BYTE)COMMAND_MACHINE_TASK, (BYTE)TOKEN_MACHINE_TASKLIST, + data, errJson)) + return errJson; + Json::Value pre; + std::string perr; + if (!ParseTasks(data, pre, perr)) return ParseErrorToJson(id, "scheduled task list", perr); + + std::vector hits; + for (Json::ArrayIndex i = 0; i < pre.size(); ++i) { + if (!EqualsCI(pre[i]["name"].asString(), taskName)) continue; + if (!wantedFolder.empty() && !EqualsCI(pre[i]["folder"].asString(), wantedFolder)) continue; + hits.push_back(&pre[i]); + } + if (hits.empty()) { + AuditManageAction(parent, ht.dev, "host_control_task", + action + " " + taskName + " result=not_found"); + return NotFoundJson(id, "scheduled task '" + taskName + "'"); + } + if (hits.size() > 1) { + return BuildError(id, -32602, + "Ambiguous task name: " + + std::to_string(hits.size()) + + " tasks share this name; specify folder"); + } + + std::string folder = hits[0]->get("folder", Json::Value("\\")).asString(); + if (folder.empty()) folder = "\\"; + + std::string ansiFolder, ansiName; + if (!Utf8ToCpExact(folder, 936, ansiFolder) || !Utf8ToCpExact(taskName, 936, ansiName)) { + return BuildError(id, -32602, + "Task folder/name contains characters that cannot be represented in the " + "client's ANSI code page (CP936)"); + } + + std::vector pkt; + pkt.push_back(action == "run" ? (BYTE)COMMAND_TASKSTART : (BYTE)COMMAND_TASKSTOP); + pkt.insert(pkt.end(), ansiFolder.begin(), ansiFolder.end()); + pkt.push_back(0); + pkt.insert(pkt.end(), ansiName.begin(), ansiName.end()); + pkt.push_back(0); + + std::vector resp; + if (!session.Action(id, pkt, (BYTE)TOKEN_MACHINE_TASKLIST, resp, errJson)) { + AuditManageAction(parent, ht.dev, "host_control_task", + action + " " + taskName + " result=send_or_timeout"); + return errJson; + } + + Json::Value post; + if (!ParseTasks(resp, post, perr)) { + return BuildError(id, kErrGeneric, "Task list after action could not be parsed: " + perr); + } + session.Finish(); + + const Json::Value* after = nullptr; + for (Json::ArrayIndex i = 0; i < post.size(); ++i) { + if (EqualsCI(post[i]["name"].asString(), taskName) && + EqualsCI(post[i]["folder"].asString(), folder)) { + after = &post[i]; + break; + } + } + + if (!after) { + AuditManageAction(parent, ht.dev, "host_control_task", + action + " " + taskName + " result=missing"); + return BuildActionResult(id, false, true, true, action, taskName, "missing", "task_missing", + u8"动作已下发,但复核时任务已不在列表中。"); + } + + std::string state = after->get("status", Json::Value("")).asString(); + AuditManageAction(parent, ht.dev, "host_control_task", + action + " " + taskName + " result=" + state); + + // 任务动作没有回执:只能确认任务仍存在并报告当前状态,不推断是否真的运行成功。 + return BuildActionResult(id, true, true, true, action, taskName, state, "", + action == "run" + ? u8"动作已下发;任务仍存在,当前状态见 state。" + u8"(客户端无回执,若任务被禁用或凭据无效,Run 会失败且状态仍为原值。)" + : u8"动作已下发;任务仍存在,当前状态见 state。"); +} + +// ============================================================================ +// 工具注册与分发 +// ============================================================================ + +namespace McpMachine { + +namespace { + +void AddTool(Json::Value& tools, const char* name, const char* desc, + const Json::Value& inputSchema, const Json::Value& outputSchema) { + Json::Value tool(Json::objectValue); + tool["name"] = name; + tool["description"] = desc; // 调用方传 u8 字面量,保证 JSON 里是 UTF-8 + tool["inputSchema"] = inputSchema; + tool["outputSchema"] = outputSchema; + tools.append(tool); +} + +// 主机管理各 tab 的条目 schema +Json::Value ProcessItemSchema() { + Json::Value p(Json::objectValue); + p["pid"] = PropInt(); + p["arch"] = PropStr(u8"x86 / x64"); + p["title"] = PropStr(u8"进程主窗口标题"); + p["name"] = PropStr(u8"可执行文件名"); + p["path"] = PropStr(); + p["priorityClass"] = PropInt(u8"Windows 优先级类原始值"); + p["priority"] = PropStr(u8"优先级名称"); + p["threads"] = PropInt(); + p["user"] = PropStr(); + p["workingSetKb"] = PropInt(); + p["fileSizeKb"] = PropInt(); + return p; +} + +Json::Value WindowItemSchema() { + Json::Value p(Json::objectValue); + p["hwnd"] = PropInt(u8"窗口句柄(十进制)"); + p["title"] = PropStr(); + p["pid"] = PropInt(); + p["width"] = PropInt(); + p["height"] = PropInt(); + p["canSee"] = PropBool(u8"客户端是否可抓取该窗口画面"); + return p; +} + +Json::Value NetStateItemSchema() { + Json::Value p(Json::objectValue); + p["process"] = PropStr(); + p["pid"] = PropInt(); + p["protocol"] = PropStr(u8"tcp / udp"); + p["localEndpoint"] = PropStr(u8"地址:端口"); + p["localAddress"] = PropStr(); + p["localPort"] = PropInt(); + p["remoteEndpoint"] = PropStr(u8"地址:端口;UDP 无对端时为 *.*.*.*:*"); + p["remoteAddress"] = PropStr(); + p["remotePort"] = PropInt(); + p["state"] = PropStr(u8"TCP 状态;UDP 为空"); + return p; +} + +Json::Value ConnectionItemSchema() { + Json::Value p(Json::objectValue); + p["protocol"] = PropStr(u8"tcp / udp"); + p["family"] = PropStr(u8"ipv4 / ipv6"); + p["pid"] = PropInt(); + p["process"] = PropStr(); + p["localAddress"] = PropStr(); + p["localPort"] = PropInt(); + p["remoteAddress"] = PropStr(); + p["remotePort"] = PropInt(); + p["state"] = PropInt(u8"MIB_TCP_STATE_* 原始值"); + p["stateName"] = PropStr(u8"TCP 状态名;UDP 为空"); + p["killable"] = PropBool(u8"是否可被 kill_network_connection 强制断开(仅 IPv4 TCP)"); + return p; +} + +Json::Value SoftwareItemSchema() { + Json::Value p(Json::objectValue); + p["name"] = PropStr(); + p["publisher"] = PropStr(); + p["version"] = PropStr(); + p["installDate"] = PropStr(); + p["uninstallString"] = PropStr(u8"卸载命令原文(本工具不执行)"); + return p; +} + +Json::Value HistoryItemSchema() { + Json::Value p(Json::objectValue); + p["time"] = PropStr(); + p["title"] = PropStr(); + p["url"] = PropStr(); + return p; +} + +Json::Value FavoriteItemSchema() { + Json::Value p(Json::objectValue); + p["name"] = PropStr(); + p["url"] = PropStr(); + return p; +} + +Json::Value ServiceItemSchema() { + Json::Value p(Json::objectValue); + p["displayName"] = PropStr(); + p["description"] = PropStr(); + p["state"] = PropStr(u8"Running / Stopped / Paused / ..."); + p["startType"] = PropStr(); + p["login"] = PropStr(); + p["interactive"] = PropBool(); + p["name"] = PropStr(u8"服务名(host_control_service 的 name 参数)"); + p["binaryPath"] = PropStr(); + return p; +} + +Json::Value TaskItemSchema() { + Json::Value p(Json::objectValue); + p["name"] = PropStr(); + p["path"] = PropStr(); + p["folder"] = PropStr(u8"任务所在文件夹(host_control_task 的 folder 参数)"); + p["exe"] = PropStr(); + p["status"] = PropStr(u8"Ready / Running / Disabled / ..."); + p["lastRun"] = PropStr(u8"上次运行时间,未知为空"); + p["nextRun"] = PropStr(u8"下次运行时间,未知为空"); + return p; +} + +// 读工具公共 input:id + 分页 +Json::Value ReadInputSchema(Json::Value props) { + props["id"] = HostIdProp(); + AddPagingProps(props); + return MakeSchema(props, { "id" }); +} + +} // namespace + +bool Handles(const std::string& toolName) { + static const char* kTools[] = { + "list_network_connections", "kill_network_connection", + "host_list_processes", "host_list_windows", "host_list_network_state", + "host_list_software", "host_list_browser_history", "host_list_favorites", + "host_list_win32_services", "host_list_driver_services", + "host_list_scheduled_tasks", "host_get_hosts_file", + "host_control_process", "host_control_window", + "host_control_service", "host_control_task", + }; + for (const char* t : kTools) { + if (toolName == t) return true; + } + return false; +} + +void AppendTools(Json::Value& tools) { + CMcpServer& mcp = McpServer(); + // 总开关关闭:本族工具全部不可见(直接调用返回 -32006)。 + if (!mcp.IsMachineEnabled()) return; + const bool writable = !mcp.IsReadonly(); + + // ---- 连接管理 ---- + { + Json::Value props(Json::objectValue); + props["protocol"] = PropStrEnum(u8"协议过滤,默认 all", { "tcp", "udp", "all" }); + props["family"] = PropStrEnum(u8"地址族过滤,默认 all", { "ipv4", "ipv6", "all" }); + props["pid"] = PropInt(u8"按进程 PID 过滤"); + props["process"] = PropStr(u8"按进程名子串过滤(不区分大小写)"); + props["local_port"] = PropInt(); + props["remote_port"] = PropInt(); + props["remote_address"] = PropStr(u8"按远端地址子串过滤"); + props["state"] = PropStr(u8"按状态过滤:数字(MIB_TCP_STATE_*)或状态名子串,如 ESTABLISHED"); + AddTool(tools, "list_network_connections", + u8"列出指定在线 Windows 主机的 TCP/UDP 连接(相当于「连接管理」对话框)。返回四元组、所属进程与 TCP 状态;killable=true 表示该连接可被 kill_network_connection 断开。", + ReadInputSchema(props), MakeListOutputSchema("connections", ConnectionItemSchema())); + } + if (writable && mcp.IsManageEnabled()) { + Json::Value props(Json::objectValue); + props["id"] = HostIdProp(); + props["protocol"] = PropStrEnum(u8"协议,默认 tcp(仅 IPv4 TCP 可断开)", { "tcp", "udp" }); + props["local_address"] = PropStr(u8"本地 IPv4 地址,如 192.168.0.55"); + props["local_port"] = PropInt(u8"本地端口(主机序)"); + props["remote_address"] = PropStr(u8"远端 IPv4 地址"); + props["remote_port"] = PropInt(u8"远端端口(主机序)"); + props["pid"] = PropInt(u8"可选:同时校验所属进程 PID(多行匹配时用于消歧)"); + props["dry_run"] = PropBool(u8"true 时只校验目标存在,不发断开命令,默认 false"); + AddTool(tools, "kill_network_connection", + u8"强制断开指定在线 Windows 主机上的一条 IPv4 TCP 连接(客户端调 SetTcpEntry DELETE_TCB)。必须提供完整四元组;目标不存在返回 -32009(请先刷新 list_network_connections)。断开后会用客户端自动刷新回来的列表复核,若连接仍在则 ok=false、reason=still_present(常见原因:客户端无管理员权限)。", + MakeSchema(props, { "id", "local_address", "local_port", + "remote_address", "remote_port" }), + ActionOutputSchema()); + } + + // ---- 主机管理:10 个只读标签页 ---- + { + Json::Value props(Json::objectValue); + props["pid"] = PropInt(u8"按 PID 过滤"); + props["name"] = PropStr(u8"按进程名子串过滤"); + props["arch"] = PropStrEnum(u8"按架构过滤", { "x86", "x64" }); + AddTool(tools, "host_list_processes", + u8"列出在线 Windows 主机的进程(主机管理「进程」标签页)。返回 PID、架构、窗口标题、路径、优先级、线程数、用户、工作集与文件大小。", + ReadInputSchema(props), MakeListOutputSchema("processes", ProcessItemSchema())); + } + { + Json::Value props(Json::objectValue); + props["pid"] = PropInt(u8"按所属进程 PID 过滤"); + props["title"] = PropStr(u8"按窗口标题子串过滤"); + AddTool(tools, "host_list_windows", + u8"列出在线 Windows 主机上当前可见的顶层窗口(主机管理「窗口」标签页)。只包含可见窗口,故隐藏后窗口会从结果中消失。hwnd 可用于 host_control_window。", + ReadInputSchema(props), MakeListOutputSchema("windows", WindowItemSchema())); + } + { + Json::Value props(Json::objectValue); + props["pid"] = PropInt(); + props["process"] = PropStr(u8"按进程名子串过滤"); + props["protocol"] = PropStrEnum(u8"协议过滤", { "tcp", "udp" }); + props["local_port"] = PropInt(); + props["remote_port"] = PropInt(); + props["state"] = PropStr(u8"按状态子串过滤"); + AddTool(tools, "host_list_network_state", + u8"列出在线 Windows 主机的网络连接及其所属进程(主机管理「网络状态」标签页)。数据来源与「连接管理」不同(这里是进程视角、含 UDP 的无对端标记);仅支持 IPv4 展示。", + ReadInputSchema(props), MakeListOutputSchema("connections", NetStateItemSchema())); + } + { + Json::Value props(Json::objectValue); + props["name"] = PropStr(u8"按软件名子串过滤"); + props["publisher"] = PropStr(u8"按发布者子串过滤"); + AddTool(tools, "host_list_software", + u8"列出在线 Windows 主机已安装的软件(主机管理「软件」标签页,来自注册表 Uninstall 项)。只读;不提供卸载能力。", + ReadInputSchema(props), MakeListOutputSchema("software", SoftwareItemSchema())); + } + { + Json::Value props(Json::objectValue); + props["title"] = PropStr(u8"按标题子串过滤"); + props["url"] = PropStr(u8"按 URL 子串过滤"); + AddTool(tools, "host_list_browser_history", + u8"列出在线 Windows 主机的 IE 浏览历史(主机管理「浏览记录」标签页)。隐私数据,仅在显式开启主机管理时可见。", + ReadInputSchema(props), MakeListOutputSchema("history", HistoryItemSchema())); + } + { + Json::Value props(Json::objectValue); + props["name"] = PropStr(u8"按收藏名子串过滤"); + props["url"] = PropStr(u8"按 URL 子串过滤"); + AddTool(tools, "host_list_favorites", + u8"列出在线 Windows 主机的 IE 收藏夹(主机管理「收藏夹」标签页)。隐私数据,仅在显式开启主机管理时可见。", + ReadInputSchema(props), MakeListOutputSchema("favorites", FavoriteItemSchema())); + } + { + Json::Value props(Json::objectValue); + props["name"] = PropStr(u8"按服务名或显示名子串过滤"); + props["status"] = PropStr(u8"按状态子串过滤,如 Running"); + props["start_type"] = PropStr(u8"按启动类型子串过滤,如 Auto"); + AddTool(tools, "host_list_win32_services", + u8"列出在线 Windows 主机的 Win32 服务(主机管理「服务」标签页之一)。返回显示名、描述、状态、启动类型、登录身份、服务名与可执行路径;服务名可用于 host_control_service。", + ReadInputSchema(props), MakeListOutputSchema("services", ServiceItemSchema())); + } + { + Json::Value props(Json::objectValue); + props["name"] = PropStr(u8"按服务名或显示名子串过滤"); + props["status"] = PropStr(u8"按状态子串过滤"); + props["start_type"] = PropStr(u8"按启动类型子串过滤"); + AddTool(tools, "host_list_driver_services", + u8"列出在线 Windows 主机的驱动服务(主机管理「服务」标签页之一)。字段同 host_list_win32_services。", + ReadInputSchema(props), MakeListOutputSchema("services", ServiceItemSchema())); + } + { + Json::Value props(Json::objectValue); + props["name"] = PropStr(u8"按任务名子串过滤"); + props["folder"] = PropStr(u8"按任务文件夹子串过滤,如 \\Microsoft"); + props["status"] = PropStr(u8"按状态子串过滤,如 Ready / Disabled"); + AddTool(tools, "host_list_scheduled_tasks", + u8"列出在线 Windows 主机的计划任务(主机管理「计划任务」标签页)。返回任务名、完整路径、所在文件夹、可执行文件、状态与上次/下次运行时间。任务名 + folder 可用于 host_control_task。", + ReadInputSchema(props), MakeListOutputSchema("tasks", TaskItemSchema())); + } + { + Json::Value props(Json::objectValue); + props["id"] = HostIdProp(); + props["include_content"] = PropBool(u8"是否返回按行切分的内容,默认 true"); + props["max_bytes"] = PropInt(u8"最多返回的原始字节数,默认 262144,上限 1048576"); + + Json::Value outProps(Json::objectValue); + outProps["path"] = PropStr(); + outProps["encoding"] = PropStr(u8"utf-8 / gbk(自动探测)"); + outProps["bytes"] = PropInt(u8"文件原始字节数"); + outProps["returnedBytes"] = PropInt(); + outProps["truncated"] = PropBool(); + outProps["lineCount"] = PropInt(); + Json::Value lines(Json::objectValue); + lines["type"] = "array"; + lines["items"] = PropStr(); + outProps["lines"] = lines; + outProps["content"] = PropStr(u8"未截断时的完整文本"); + + Json::Value out(Json::objectValue); + out["type"] = "object"; + out["properties"] = outProps; + + AddTool(tools, "host_get_hosts_file", + u8"读取在线 Windows 主机的 hosts 文件(主机管理「HOSTS」标签页),按行返回并自动识别 UTF-8/GBK。只读;不提供写入能力。", + MakeSchema(props, { "id" }), out); + } + + // ---- 主机管理:4 个写工具(只读 + 可恢复操作) ---- + if (writable) { + { + Json::Value props(Json::objectValue); + props["id"] = HostIdProp(); + props["action"] = PropStrEnum(u8"freeze=挂起进程,thaw=恢复进程", { "freeze", "thaw" }); + props["pid"] = PropInt(u8"目标进程 PID"); + props["name"] = PropStr(u8"可选:校验进程名,防止 PID 被系统复用后误操作"); + AddTool(tools, "host_control_process", + u8"挂起或恢复在线 Windows 主机上的一个进程(对应主机管理「进程」右键的冻结/解冻)。可能导致目标进程超时或崩溃,故归入危险操作:需要同时开启 McpMachineDangerous。动作没有回执,客户端也不回报结果;工具会在动作后重新拉取进程列表确认目标仍存在。", + MakeSchema(props, { "id", "action", "pid" }), + ActionOutputSchema()); + } + { + Json::Value props(Json::objectValue); + props["id"] = HostIdProp(); + props["action"] = PropStrEnum(u8"restore/hide/maximize/minimize", + { "restore", "hide", "maximize", "minimize" }); + Json::Value hwnd(Json::objectValue); + Json::Value types(Json::arrayValue); + types.append("integer"); + types.append("string"); + hwnd["type"] = types; + hwnd["description"] = u8"窗口句柄,取 host_list_windows 的 hwnd;整数或 0x 前缀字符串"; + props["hwnd"] = hwnd; + AddTool(tools, "host_control_window", + u8"对在线 Windows 主机的窗口执行可恢复操作:restore(还原)/hide(隐藏)/maximize(最大化)/minimize(最小化)。动作后重新枚举可见窗口复核:hide 后 hwnd 应消失,其余动作后仍应存在。不提供关闭窗口。", + MakeSchema(props, { "id", "action", "hwnd" }), + ActionOutputSchema()); + } + { + Json::Value props(Json::objectValue); + props["id"] = HostIdProp(); + props["action"] = PropStrEnum(u8"start/stop/pause/continue/restart", + { "start", "stop", "pause", "continue", "restart" }); + props["name"] = PropStr(u8"服务名(ServiceName,不是显示名),取 host_list_*_services 的 name"); + AddTool(tools, "host_control_service", + u8"控制在线 Windows 主机的服务:start/stop/pause/continue/restart。客户端对每次控制都会自动回一份刷新后的服务列表,工具据此复核状态并如实返回 ok;stop 与 restart 归入危险操作,需要同时开启 McpMachineDangerous。restart 先 stop 再 start。不提供删除服务。", + MakeSchema(props, { "id", "action", "name" }), + ActionOutputSchema()); + } + { + Json::Value props(Json::objectValue); + props["id"] = HostIdProp(); + props["action"] = PropStrEnum(u8"run=立即运行,stop=停止正在运行的任务", { "run", "stop" }); + props["name"] = PropStr(u8"任务名,取 host_list_scheduled_tasks 的 name"); + props["folder"] = PropStr(u8"可选:任务所在文件夹;同名任务存在多个时必填"); + AddTool(tools, "host_control_task", + u8"运行或停止在线 Windows 主机的计划任务(主机管理「计划任务」右键)。客户端无回执,工具只复核任务仍存在并报告当前状态,不声称任务一定执行成功。不提供创建/删除任务。", + MakeSchema(props, { "id", "action", "name" }), + ActionOutputSchema()); + } + } +} + +std::string Call(const Json::Value& id, const std::string& toolName, + const Json::Value& args, CMy2015RemoteDlg* parent) { + CMcpServer& mcp = McpServer(); + if (!mcp.IsMachineEnabled()) { + return BuildError(id, kErrDisabled, + "Host management tools are disabled (set McpMachine=1 to enable)"); + } + if (!args.isObject()) { + return BuildError(id, -32602, "Invalid params: arguments must be an object"); + } + + // ---- 连接管理 ---- + if (toolName == "list_network_connections") + return BuildListNetworkConnections(id, args, parent); + if (toolName == "kill_network_connection") + return BuildKillNetworkConnection(id, args, parent); + + // ---- 主机管理:只读 ---- + if (toolName == "host_list_processes") return BuildHostListProcesses(id, args, parent); + if (toolName == "host_list_windows") return BuildHostListWindows(id, args, parent); + if (toolName == "host_list_network_state") return BuildHostListNetworkState(id, args, parent); + if (toolName == "host_list_software") return BuildHostListSoftware(id, args, parent); + if (toolName == "host_list_browser_history") return BuildHostListBrowserHistory(id, args, parent); + if (toolName == "host_list_favorites") return BuildHostListFavorites(id, args, parent); + if (toolName == "host_list_win32_services") + return BuildHostListServices(id, args, parent, /*drivers=*/false); + if (toolName == "host_list_driver_services") + return BuildHostListServices(id, args, parent, /*drivers=*/true); + if (toolName == "host_list_scheduled_tasks") return BuildHostListScheduledTasks(id, args, parent); + if (toolName == "host_get_hosts_file") return BuildHostGetHostsFile(id, args, parent); + + // ---- 主机管理:写(每个 builder 内部再校验只读/危险开关) ---- + if (toolName == "host_control_process") return BuildHostControlProcess(id, args, parent); + if (toolName == "host_control_window") return BuildHostControlWindow(id, args, parent); + if (toolName == "host_control_service") return BuildHostControlService(id, args, parent); + if (toolName == "host_control_task") return BuildHostControlTask(id, args, parent); + + return BuildError(id, -32601, "Unknown tool: " + toolName); +} + +} // namespace McpMachine diff --git a/server/2015Remote/McpMachine.h b/server/2015Remote/McpMachine.h new file mode 100644 index 0000000..a128f7b --- /dev/null +++ b/server/2015Remote/McpMachine.h @@ -0,0 +1,31 @@ +#pragma once + +// 主机管理(10 个标签页)与连接管理(连接列表 / 强制断开)MCP 工具族。 +// +// 协议(详见 docs/Mcp_Machine_Design.md): +// 主机管理:主连接 COMMAND_MACHINE_MANAGE(162) → 客户端 MachineManager 子连接回 +// TOKEN_SYSINFOLIST(61) → 发 1 字节 tab(0..9) → 回 TOKEN_MACHINE_*(31..42)。 +// 连接管理:主连接 COMMAND_CONNECTION(253) → CConnectionsManager 子连接回 +// TOKEN_CONNECTION_LIST(255) → 发 254+原始 110 字节记录强制断开。 +// +// 会话状态由 CMcpServer 持有(与 m_Pending 共用单飞行表),本模块只负责工具 schema、 +// 参数校验、组包与解析。 + +#include "jsoncpp/json.h" +#include + +class CMy2015RemoteDlg; + +namespace McpMachine { + +// 是否由本模块处理(AppendTools 与 Call 必须保持一致)。 +bool Handles(const std::string& toolName); + +// 按当前开关(McpMachine / McpMachineDangerous / McpReadonly)追加可见工具。 +void AppendTools(Json::Value& tools); + +// 执行工具;返回完整 JSON-RPC 响应字符串(成功 = BuildResult,失败 = BuildError)。 +std::string Call(const Json::Value& id, const std::string& toolName, + const Json::Value& args, CMy2015RemoteDlg* parent); + +} // namespace McpMachine diff --git a/server/2015Remote/McpServer.cpp b/server/2015Remote/McpServer.cpp index 7f37952..d170f6b 100644 --- a/server/2015Remote/McpServer.cpp +++ b/server/2015Remote/McpServer.cpp @@ -8,6 +8,8 @@ #include "Server.h" // CONTEXT_OBJECT 定义(GetScreenContext 返回 CONTEXT_OBJECT* → context* 上转型) #include "LangManager.h" // _TR(审计日志标题语言映射) #include "common/file_upload.h" // V2 文件传输(RecvFileChunkV2 / HandleFileCompleteV2 / 包结构) +#include "McpInternal.h" // 共享助手(McpMachine.cpp 也使用) +#include "McpMachine.h" // 主机管理 / 连接管理工具族 #include @@ -30,39 +32,15 @@ std::string GetPwdHash(); std::string GetHMAC(int offset); +// 共享助手(JsonToString/BuildResult/ParseHostIdArg/FindMainContext/ToUtf8/ToAnsi/...) +// 已抽到 McpInternal.cpp;此处引入后调用点保持原样。 +using namespace McpInternal; + namespace { // P2b 工具等待响应的超时(ms)。MCP 一次性请求:等待子连接回传进程/窗口列表。 static const int kMcpToolTimeoutMs = 20000; -// Json::Value → 紧凑 JSON 字符串 -std::string JsonToString(const Json::Value& v) { - Json::StreamWriterBuilder b; - b["indentation"] = ""; - return Json::writeString(b, v); -} - -// JSON-RPC 2.0 成功响应 -std::string BuildResult(const Json::Value& id, const Json::Value& result) { - Json::Value resp(Json::objectValue); - resp["jsonrpc"] = "2.0"; - resp["id"] = id; - resp["result"] = result; - return JsonToString(resp); -} - -// JSON-RPC 2.0 错误响应 -std::string BuildError(const Json::Value& id, int code, const std::string& msg) { - Json::Value resp(Json::objectValue); - resp["jsonrpc"] = "2.0"; - resp["id"] = id; - Json::Value err(Json::objectValue); - err["code"] = code; - err["message"] = msg; - resp["error"] = err; - return JsonToString(resp); -} - // initialize 握手(MCP 规范:protocolVersion + capabilities + serverInfo) std::string BuildInitializeResult(const Json::Value& id) { Json::Value result(Json::objectValue); @@ -84,33 +62,6 @@ std::string BuildPingResult(const Json::Value& id) { // ========== P2a 通用辅助 ========== -// 小写化(仅 ASCII,UTF-8 多字节原样保留):用于不区分大小写的子串匹配 -std::string ToLowerAscii(const std::string& s) { - std::string r = s; - for (char& c : r) if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a'); - return r; -} - -// 不区分大小写的子串匹配 -bool ContainsCI(const std::string& haystack, const std::string& needle) { - if (needle.empty()) return true; - return ToLowerAscii(haystack).find(ToLowerAscii(needle)) != std::string::npos; -} - -// 取对象的字符串字段,缺失/非字符串返回 "" -std::string JsonStrField(const Json::Value& v, const char* key) { - if (v.isObject() && v.isMember(key) && v[key].isString()) - return v[key].asString(); - return ""; -} - -// 是否纯数字(host id 为 uint64 十进制字符串) -bool IsDigits(const std::string& s) { - if (s.empty()) return false; - for (char c : s) if (c < '0' || c > '9') return false; - return true; -} - // 读取 tools/call 的入参(MCP 规范:params.arguments 为工具入参对象) Json::Value GetCallArguments(const Json::Value& params) { if (params.isObject() && params.isMember("arguments") && params["arguments"].isObject()) @@ -118,85 +69,8 @@ Json::Value GetCallArguments(const Json::Value& params) { return Json::Value(Json::objectValue); } -// 读取可选字符串入参,缺失返回 "" -std::string GetStringArg(const Json::Value& args, const char* key) { - return JsonStrField(args, key); -} - -// 读取可选整数入参(JSON number 或数字字符串);缺失/非法返回 false(不写 out)。 -// 定义见文件后半段(随 P3 终端会话辅助一起),此处前置声明供早于定义的 exec_command / -// terminal_exec 等工具复用。 -static bool GetIntArg(const Json::Value& args, const char* key, int& out); - // ========== 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; -} - -// 从 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 个 '|' 反推),兼容只有标题的老客户端。 // 逻辑复刻 SystemDlg.cpp::ParseWindowAttrs。 void ParseWindowAttrsStr(const char* s, std::string& title, std::string& status, uint64_t& pid) { @@ -647,6 +521,10 @@ Json::Value BuildHostItemSchema() { Json::Value onlineProp(Json::objectValue); onlineProp["type"] = "boolean"; itemProps["online"] = onlineProp; + // 客户端登录时上报的 TLS 传输标志(老客户端恒为 false) + Json::Value tlsProp(Json::objectValue); + tlsProp["type"] = "boolean"; + itemProps["tls"] = tlsProp; return itemProps; } @@ -1311,7 +1189,7 @@ std::string BuildToolsListResult(const Json::Value& id) { tool["name"] = "list_online_hosts"; // 说明文字为 UTF-8:项目 /execution-charset:.936 会把普通窄字面量编译成 GBK, // 故用 u8 前缀确保输出到 JSON 的字节是 UTF-8。 - tool["description"] = u8"获取当前所有在线主机的列表,包含计算机名、IP、操作系统、版本、备注、分组、活动窗口、延迟等实时信息。"; + tool["description"] = u8"获取当前所有在线主机的列表,包含计算机名、IP、操作系统、版本、备注、分组、活动窗口、延迟等实时信息。tls=true 表示该客户端登录时使用了 TLS 加密传输(登录时上报,非实时探测;老客户端恒为 false)。"; Json::Value inputSchema(Json::objectValue); inputSchema["type"] = "object"; @@ -1328,7 +1206,7 @@ std::string BuildToolsListResult(const Json::Value& id) { { Json::Value tool(Json::objectValue); tool["name"] = "search_hosts"; - tool["description"] = u8"按计算机名/备注、IP、分组、操作系统过滤在线主机。所有条件均可选、按 AND 组合;子串匹配(ASCII 不区分大小写)。只返回在线主机。"; + tool["description"] = u8"按计算机名/备注、IP、分组、操作系统过滤在线主机。所有条件均可选、按 AND 组合;子串匹配(ASCII 不区分大小写)。只返回在线主机。tls=true 表示该客户端登录时使用了 TLS 加密传输(登录时上报,老客户端恒为 false)。"; tool["inputSchema"] = BuildSearchHostsInputSchema(); tool["outputSchema"] = BuildHostOutputSchema(); @@ -1340,7 +1218,7 @@ std::string BuildToolsListResult(const Json::Value& id) { { Json::Value tool(Json::objectValue); tool["name"] = "get_host_detail"; - tool["description"] = u8"获取单台在线主机的详细信息(id、计算机名、IP、操作系统、备注、分组、活动窗口、屏幕分辨率、客户端类型等)。"; + tool["description"] = u8"获取单台在线主机的详细信息(id、计算机名、IP、操作系统、备注、分组、活动窗口、屏幕分辨率、客户端类型等)。tls=true 表示该客户端登录时使用了 TLS 加密传输(登录时上报,老客户端恒为 false)。"; tool["inputSchema"] = BuildGetHostDetailInputSchema(); tool["outputSchema"] = BuildHostDetailOutputSchema(); @@ -1607,6 +1485,11 @@ std::string BuildToolsListResult(const Json::Value& id) { } } + // 19) 连接管理 / 主机管理(P8:list_network_connections / kill_network_connection / + // host_* 共 16 个工具)。开关在 McpMachine.cpp 内部按 McpMachine / + // McpMachineDangerous / McpReadonly 逐项判断,关闭时不追加任何工具。 + McpMachine::AppendTools(tools); + result["tools"] = tools; return BuildResult(id, result); } @@ -3366,21 +3249,6 @@ static bool GetNormCoord(const Json::Value& args, const char* key, double& out, return true; } -// 读取可选整数参数(JSON number 或数字字符串);缺失/非法返回 false(不写 out)。 -static bool GetIntArg(const Json::Value& args, const char* key, int& out) { - if (!args.isMember(key)) return false; - const Json::Value& v = args[key]; - if (v.isNumeric()) { out = v.asInt(); return true; } - if (v.isString()) { - const std::string s = v.asString(); - if (IsDigits(s) || (!s.empty() && s[0] == '-' && IsDigits(s.substr(1)))) { - out = atoi(s.c_str()); - return true; - } - } - return false; -} - // button 字符串 → 0=左/1=中/2=右;空或 left 默认左。 static bool ParseButton(const std::string& s, int& out) { if (s.empty() || s == "left") { out = 0; return true; } @@ -4351,31 +4219,6 @@ std::string BuildUploadFile(const Json::Value& id, const Json::Value& args, CMy2 // ========== P7:设备管理控制 ========== -// 读取 JSON number 或纯数字字符串为 uint64。 -static bool ReadUInt64(const Json::Value& v, uint64_t& out) { - if (v.isNumeric()) { out = v.asUInt64(); return true; } - if (v.isString()) { - std::string s = v.asString(); - if (IsDigits(s)) { out = strtoull(s.c_str(), nullptr, 10); return true; } - } - return false; -} - -static bool ReadUInt64Arg(const Json::Value& args, const char* key, uint64_t& out) { - if (!args.isMember(key)) return false; - return ReadUInt64(args[key], out); -} - -// 写审计(杀进程/窗口/服务/电源共用):PostMessageA 落到服务端消息/审计日志(不可关闭)。 -static void AuditManageAction(CMy2015RemoteDlg* parent, uint64_t devId, - const std::string& tool, const std::string& detail) { - if (!parent) return; - std::string text = "host " + std::to_string(devId) + " " + tool + ": " + detail; - parent->PostMessageA(WM_SHOWERRORMSG, - (WPARAM)new CString(ToAnsi(text, 936).c_str()), - (LPARAM)new CString(_TR("MCP管理操作"))); -} - // kill_process:主连接 COMMAND_SYSTEM → 系统子连接 TOKEN_PSLIST(就绪)→ COMMAND_KILLPROCESS // → 补发 COMMAND_PSLIST → 第二个 TOKEN_PSLIST(确认)。payload 与 SystemDlg::OnPlistKill 一致。 std::string BuildKillProcess(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) { @@ -4687,6 +4530,9 @@ std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) { const Json::Value args = GetCallArguments(params); + // P8:连接管理 / 主机管理工具族(独立分发;门控在 McpMachine::Call 内复核)。 + if (McpMachine::Handles(toolName)) return McpMachine::Call(id, toolName, args, parent); + if (toolName == "list_online_hosts") return BuildListOnlineHosts(id, parent); if (toolName == "search_hosts") return BuildSearchHosts(id, args, parent); if (toolName == "get_host_detail") return BuildGetHostDetail(id, args, parent); @@ -5225,13 +5071,18 @@ void CMcpServer::HandleMcp(const httplib::Request& req, httplib::Response& res) bool CMcpServer::IsPending(uint64_t device_id) { std::lock_guard lk(m_PendingMutex); - return m_Pending.find(device_id) != m_Pending.end(); + auto it = m_Pending.find(device_id); + if (it == m_Pending.end()) return false; + // P8:主机管理/连接管理会话的响应包走 OnManagedSubPacket,不归 MFC 的 + // TOKEN_PSLIST/TOKEN_WSLIST/SERVERLIST/DRIVE_LIST 分支消费(否则会抢走 UI 包)。 + return it->second.kind == PendingKind::Generic || it->second.kind == PendingKind::FollowUp; } void CMcpServer::TakeMainResponse(uint64_t device_id, const BYTE* data, ULONG len) { std::lock_guard lk(m_PendingMutex); auto it = m_Pending.find(device_id); if (it == m_Pending.end()) { Mprintf("[MCP-P7] TakeMainResponse dev=%llu len=%d: no pending (late/dropped)\n", device_id, (int)len); return; } // 已超时清理 → 迟到数据,丢弃 + if (it->second.kind != PendingKind::Generic && it->second.kind != PendingKind::FollowUp) return; // P8 会话:不接管 Mprintf("[MCP-P7] TakeMainResponse dev=%llu len=%d done\n", device_id, (int)len); it->second.data.assign(data, data + len); it->second.done = true; @@ -5416,6 +5267,7 @@ bool CMcpServer::BeginPendingFollowUp(uint64_t device_id, const std::string& too if (m_Pending.find(device_id) != m_Pending.end()) return false; // 设备忙 PendingRequest r; r.tool = tool; + r.kind = PendingKind::FollowUp; r.followUp = followUp; m_Pending[device_id] = std::move(r); return true; @@ -5501,6 +5353,8 @@ void CMcpServer::OnManageSubClosed(context* subCtx) { // 扫描而非按 device_id 查找,避免依赖断线后 GetClientID 是否仍有效。 for (auto& kv : m_Pending) { if (kv.second.subCtx == subCtx) { + // P8 会话由 OnManagedContextClosed 处理(这里若清 subCtx 会让其无法按指针反查设备)。 + if (kv.second.kind == PendingKind::Machine || kv.second.kind == PendingKind::ConnMgr) continue; Mprintf("[MCP-P7] OnManageSubClosed subCtx=%p tool=%s: sub-conn closed before confirm\n", (void*)subCtx, kv.second.tool.c_str()); kv.second.subCtx = nullptr; // 断线后子连接已死/将回收,超时路径不得再 CancelIO diff --git a/server/2015Remote/McpServer.h b/server/2015Remote/McpServer.h index 3beaa28..775a990 100644 --- a/server/2015Remote/McpServer.h +++ b/server/2015Remote/McpServer.h @@ -270,6 +270,41 @@ public: // 超时路径不会对已回收(连接池复用)的 context 误 CancelIO。 void OnManageSubClosed(context* subCtx); + // ===== P8:主机管理 / 连接管理(子连接会话)===== + // 协议:主连接下发 COMMAND_MACHINE_MANAGE(162) / COMMAND_CONNECTION(253) → 客户端新建 + // 子连接 → 子连接回就绪包(TOKEN_SYSINFOLIST=61 / TOKEN_CONNECTION_LIST=255)→ 在同一 + // 子连接上多步收发。会话复用 m_Pending 表(统一「单设备单飞行」),kind 区分工具族。 + // 见 docs/Mcp_Machine_Design.md。 + void SetMachineEnabled(bool enabled) { m_machineEnabled = enabled; } + bool IsMachineEnabled() const { return m_machineEnabled; } + void SetMachineDangerousEnabled(bool enabled) { m_machineDangerousEnabled = enabled; } + bool IsMachineDangerousEnabled() const { return m_machineDangerousEnabled; } + + // MessageHandle 顶部钩子(终端拦截之后、switch 之前):true = 本包已由 MCP 消费。 + // 已绑定的子连接:只保存期望 token 的包与 TOKEN_MACHINE_MSG(42),其余丢弃。 + // 未绑定的子连接:仅 61/255 可作为就绪包尝试绑定;否则返回 false(MFC 行为不变)。 + bool OnManagedSubPacket(context* subCtx, const BYTE* data, ULONG len); + + // OfflineProc 钩子:接管的子连接断开 → 置 closed 唤醒等待者(幂等)。 + void OnManagedContextClosed(context* subCtx); + + // 工具线程:登记会话(false = 该设备已有挂起请求在飞 → 设备忙)。 + bool BeginMachineSession(uint64_t device_id, const std::string& tool, context* mainCtx); + bool BeginConnMgrSession(uint64_t device_id, const std::string& tool, context* mainCtx); + + // 工具线程:发命令前清空上一次响应并登记期望 token。 + void ArmManagedResponse(uint64_t device_id, BYTE expectedToken); + + // 工具线程:等待子连接就绪包 / 指定 token 的响应(超时由调用方按步给)。 + enum class ManagedWaitResult { Ok, Timeout, Closed, ClientError }; + ManagedWaitResult WaitManagedPacket(uint64_t device_id, BYTE token, + std::vector& out, context*& subCtxOut, + int timeoutMs, std::string& clientError); + + // 工具线程:结束会话;cancelIo = 关闭子连接;markOrphan = 打 5s 墓碑 + // (吸收迟到的 61/255,避免超时后弹出 MFC 对话框)。 + void EndManagedSession(uint64_t device_id, bool cancelIo, bool markOrphan); + // 安全配置(启动时由 CMy2015RemoteDlg 读 THIS_CFG 后设置)。 void SetReadonly(bool readonly) { m_readonly = readonly; } void SetCmdWhitelist(const std::string& whitelist) { m_cmdWhitelist = whitelist; } @@ -285,6 +320,15 @@ private: // POST /mcp 处理器:token 校验 + JSON-RPC 分发。 void HandleMcp(const httplib::Request& req, httplib::Response& res); + // 挂起请求类型:主机管理/连接管理会话与既有一次性请求共用 m_Pending 保证单设备单飞行, + // 但 MessageHandle 的 P2b/P3/P7 拦截分支(IsPending/TakeMainResponse)必须把这两类排除, + // 否则用户手动打开对话框时 UI 包会被 MCP 抢走。 + enum class PendingKind { Generic, FollowUp, Machine, ConnMgr }; + static bool KindAcceptsToken(PendingKind kind, BYTE token); + + bool BeginManagedSession(uint64_t device_id, const std::string& tool, + PendingKind kind, context* mainCtx); + httplib::Server m_server; std::thread m_thread; std::atomic m_running{false}; @@ -306,11 +350,43 @@ private: std::vector followUp; // 接管后下发的写命令 payload(首字节 = 写命令) bool followUpSent = false; // false=等子连接就绪(首包);true=已发写命令、等确认包 context* subCtx = nullptr; // 接管的子连接(超时/失败 CancelIO 用,防孤儿子连接) + // ===== P8:主机管理 / 连接管理会话(kind = Machine / ConnMgr 时有效)===== + PendingKind kind = PendingKind::Generic; + context* mainCtx = nullptr; // Begin 时抓取的主连接(只作身份校验,不从其收发) + std::string peer; // mainCtx->GetPeerName()(老客户端子连接 ID=0 时兜底) + BYTE expectedToken = 0; // 期望的响应 token(0 = 未设置) + bool hasResponse = false; + std::vector response; // 含首字节 token + std::string clientError; // TOKEN_MACHINE_MSG(42) 文本(已转 UTF-8) + bool closed = false; // 子连接在等待期间断开 + std::chrono::steady_clock::time_point deadline; // 整个会话的截止时间 }; std::mutex m_PendingMutex; std::condition_variable m_PendingCv; std::map m_Pending; + // ===== P8:子连接路由(受 m_PendingMutex 保护)===== + // 已绑定子连接 → device(该子连接的一切包都由 MCP 消费)。 + std::map m_SubCtxToDevice; + // CancelIO 之后仍可能到达的包:静默丢弃(避免子连接回收前的尾巴包弹对话框)。 + std::set m_AbsorbedSubCtx; + // 会话异常结束后 5s 内的迟到就绪包墓碑(device / peer 两个维度)。 + struct OrphanMark { + std::chrono::steady_clock::time_point expiry; + PendingKind kind = PendingKind::Generic; + }; + std::map m_OrphanByDevice; + std::map m_OrphanByPeer; + + // 清理过期墓碑(持 m_PendingMutex 调用)。 + void PruneOrphanMarksLocked(std::chrono::steady_clock::time_point now); + + // 路由表大小(已绑定 + 已吸收的子连接数)。MessageHandle 每个包都会调用 + // OnManagedSubPacket,若无会话在飞则用它无锁快速返回,避免音频/屏幕等高频 + // 子连接逐包争用 m_PendingMutex。只在持锁时更新(store)。 + std::atomic m_ManagedRoutes{0}; + void RefreshManagedRoutesLocked(); + // get_screenshot 的 reqId 发生器(16 位,跳过 0)。与 MFC 预览的 m_PreviewReqId 各自独立计数。 std::atomic m_PreviewReqId{1}; @@ -381,6 +457,8 @@ private: bool m_remoteControlEnabled = false; // 远程控制开关(默认关;要求 m_readonly=false) bool m_fileTransferEnabled = false; // 文件传输开关(默认关;download_file) bool m_manageEnabled = false; // P7 管理控制开关(默认关;要求 m_readonly=false) + bool m_machineEnabled = false; // P8 主机管理族总开关(默认关;16 个 host_* 工具) + bool m_machineDangerousEnabled = false; // P8 高风险子集(冻结/解冻进程、停止/重启服务) }; // 全局访问器(仿 WebService(),见 WebService.h 末尾) diff --git a/server/2015Remote/McpSettingsDlg.cpp b/server/2015Remote/McpSettingsDlg.cpp index 575f88b..d76e959 100644 --- a/server/2015Remote/McpSettingsDlg.cpp +++ b/server/2015Remote/McpSettingsDlg.cpp @@ -106,7 +106,7 @@ INT_PTR CMcpSettingsDlg::DoModal() { USES_CONVERSION; CString title = _TR("MCP设置"); - BuildDialogTemplate(m_Template, T2CW(title), 320, 420); + BuildDialogTemplate(m_Template, T2CW(title), 320, 480); InitModalIndirect((LPCDLGTEMPLATE)m_Template.data()); return CDialog::DoModal(); } @@ -148,6 +148,12 @@ BOOL CMcpSettingsDlg::OnInitDialog() m_btnManage.Create(_TR("启用管理操作(进程/窗口/服务/电源)"), WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX, r0, this, IDC_MCP_MANAGE); + m_btnMachine.Create(_TR("启用主机管理(进程/窗口/服务/任务/网络状态)"), + WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX, + r0, this, IDC_MCP_MACHINE); + m_btnMachineDangerous.Create(_TR("启用危险主机操作(冻结/解冻进程、停止/重启服务)"), + WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX, + r0, this, IDC_MCP_MACHINE_DANGEROUS); m_lblWhitelist.Create(_TR("命令白名单"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1); m_editWhitelist.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN | WS_VSCROLL, @@ -172,6 +178,8 @@ BOOL CMcpSettingsDlg::OnInitDialog() m_btnRemoteControl.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0)); m_btnFileTransfer.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0)); m_btnManage.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0)); + m_btnMachine.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0)); + m_btnMachineDangerous.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0)); m_lblWhitelist.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0)); m_editWhitelist.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0)); m_btnOK.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0)); @@ -189,6 +197,8 @@ BOOL CMcpSettingsDlg::OnInitDialog() int remoteControl = THIS_CFG.GetInt("settings", "McpRemoteControl", 0); int fileTransfer = THIS_CFG.GetInt("settings", "McpFileTransfer", 0); int manage = THIS_CFG.GetInt("settings", "McpManage", 0); + int machine = THIS_CFG.GetInt("settings", "McpMachine", 0); + int machineDangerous = THIS_CFG.GetInt("settings", "McpMachineDangerous", 0); std::string whitelist = THIS_CFG.GetStr("settings", "McpCmdWhitelist", ""); m_btnEnable.SetCheck(enabled ? BST_CHECKED : BST_UNCHECKED); @@ -200,6 +210,8 @@ BOOL CMcpSettingsDlg::OnInitDialog() m_btnRemoteControl.SetCheck(remoteControl ? BST_CHECKED : BST_UNCHECKED); m_btnFileTransfer.SetCheck(fileTransfer ? BST_CHECKED : BST_UNCHECKED); m_btnManage.SetCheck(manage ? BST_CHECKED : BST_UNCHECKED); + m_btnMachine.SetCheck(machine ? BST_CHECKED : BST_UNCHECKED); + m_btnMachineDangerous.SetCheck(machineDangerous ? BST_CHECKED : BST_UNCHECKED); // 白名单存储为逗号分隔,展示为每行一条。 m_editWhitelist.SetWindowText(CString(WhitelistForDisplay(whitelist).c_str())); @@ -220,6 +232,16 @@ void CMcpSettingsDlg::OnOK() bool remoteControl = (m_btnRemoteControl.GetCheck() == BST_CHECKED); bool fileTransfer = (m_btnFileTransfer.GetCheck() == BST_CHECKED); bool manage = (m_btnManage.GetCheck() == BST_CHECKED); + bool machine = (m_btnMachine.GetCheck() == BST_CHECKED); + bool machineDangerous = (m_btnMachineDangerous.GetCheck() == BST_CHECKED); + + // 危险子集依附于主机管理总开关:没开主机管理就没有工具可放开,直接清除并提示。 + if (machineDangerous && !machine) { + machineDangerous = false; + m_btnMachineDangerous.SetCheck(BST_UNCHECKED); + MessageBoxL(_TR("危险主机操作依附于主机管理:请先启用主机管理。"), _TR("提示"), + MB_ICONWARNING); + } // 端口校验:1-65535 int port = atoi(CT2A(sPort)); @@ -247,14 +269,17 @@ void CMcpSettingsDlg::OnOK() THIS_CFG.SetInt("settings", "McpRemoteControl", remoteControl ? 1 : 0); THIS_CFG.SetInt("settings", "McpFileTransfer", fileTransfer ? 1 : 0); THIS_CFG.SetInt("settings", "McpManage", manage ? 1 : 0); + THIS_CFG.SetInt("settings", "McpMachine", machine ? 1 : 0); + THIS_CFG.SetInt("settings", "McpMachineDangerous", machineDangerous ? 1 : 0); std::string whitelist = CT2A(sWhitelist); whitelist = NormalizeWhitelist(whitelist); THIS_CFG.SetStr("settings", "McpCmdWhitelist", whitelist); - // 拆成两段可翻译的单行键,中间用 \r\n 连接(多行键无法在 INI 中表示) + // 拆成多段可翻译的单行键,中间用 \r\n 连接(多行键无法在 INI 中表示) MessageBox(_TR("MCP 设置已保存。") + _T("\r\n") + - _TR("启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。") + _T("\r\n") + - _TR("持久终端、远程控制与管理操作仅在只读模式关闭时生效。"), + _TR("启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作/主机管理/危险主机操作的改动需重启程序生效。") + _T("\r\n") + + _TR("持久终端、远程控制与管理操作仅在只读模式关闭时生效。") + _T("\r\n") + + _TR("主机管理的写操作需只读模式关闭;冻结/解冻进程、停止/重启服务还需启用危险主机操作。"), _TR("提示"), MB_ICONINFORMATION); CDialog::OnOK(); @@ -300,6 +325,12 @@ void CMcpSettingsDlg::LayoutControls(int cx, int cy) m_btnManage.MoveWindow(margin, y, cx - margin * 2, 22); y += 30; + m_btnMachine.MoveWindow(margin, y, cx - margin * 2, 22); + y += 30; + + m_btnMachineDangerous.MoveWindow(margin, y, cx - margin * 2, 22); + y += 30; + const int whitelistH = 90; m_lblWhitelist.MoveWindow(margin, y, labelW, rowH); m_editWhitelist.MoveWindow(margin + labelW, y - 2, cx - margin * 2 - labelW, whitelistH); diff --git a/server/2015Remote/McpSettingsDlg.h b/server/2015Remote/McpSettingsDlg.h index 43cbbde..a398b40 100644 --- a/server/2015Remote/McpSettingsDlg.h +++ b/server/2015Remote/McpSettingsDlg.h @@ -32,6 +32,8 @@ private: IDC_MCP_REMOTECONTROL = 1008, // 「启用远程控制」复选框(AI 操控桌面,要求只读关) IDC_MCP_FILETRANSFER = 1009, // 「启用文件传输」复选框(download_file,不要求只读关) IDC_MCP_MANAGE = 1010, // 「启用管理操作」复选框(kill/窗口/服务/电源,要求只读关) + IDC_MCP_MACHINE = 1011, // 「启用主机管理」复选框(host_* 工具族) + IDC_MCP_MACHINE_DANGEROUS = 1012, // 「启用危险主机操作」复选框(冻结/停止/重启) }; CButton m_btnEnable; @@ -42,6 +44,8 @@ private: CButton m_btnRemoteControl; CButton m_btnFileTransfer; CButton m_btnManage; + CButton m_btnMachine; + CButton m_btnMachineDangerous; CStatic m_lblWhitelist; CEdit m_editWhitelist; CButton m_btnOK, m_btnCancel; diff --git a/server/2015Remote/lang/de_DE.ini b/server/2015Remote/lang/de_DE.ini index 5507761..80d342d 100644 --- a/server/2015Remote/lang/de_DE.ini +++ b/server/2015Remote/lang/de_DE.ini @@ -1992,7 +1992,7 @@ MCP ¶˿ÚÐèΪ 1-65535 µÄÊý×Ö=Port muss eine Zahl zwischen 1 und 65535 sein Token ²»ÄÜΪ¿Õ=Token darf nicht leer sein MCP ÉèÖÃÒѱ£´档=MCP-Einstellungen gespeichert. -ÆôÓÃ/¶˿Ú/°󶨵Øַ/Token/ֻ¶Á/°×Ãûµ¥/³־ÃÖնË/Զ³̿ØÖÆ/Îļþ´«Êä/¹ÜÀí²Ù×÷µĸ͝ÐèÖØÆô³ÌÐòÉúЧ¡£=Aenderungen an Aktivierung/Port/Bindeadresse/Token/Nur-Lesen/Whitelist/persistentem Terminal/Fernsteuerung/Dateiuebertragung/Verwaltungsoperationen werden nach Neustart wirksam. +ÆôÓÃ/¶˿Ú/°󶨵Øַ/Token/ֻ¶Á/°×Ãûµ¥/³־ÃÖնË/Զ³̿ØÖÆ/Îļþ´«Êä/¹ÜÀí²Ù×÷/Ö÷»ú¹ÜÀí/ΣÏÕÖ÷»ú²Ù×÷µĸ͝ÐèÖØÆô³ÌÐòÉúЧ¡£=Aenderungen an Aktivierung/Port/Bindeadresse/Token/Nur-Lesen/Whitelist/persistentem Terminal/Fernsteuerung/Dateiuebertragung/Verwaltungsoperationen/Maschinenverwaltung/gefaehrlichen Maschinenoperationen werden nach Neustart wirksam. ֻ¶Áģʽ£¨½ûÃüÁîִÐУ©=Nur-Lesen-Modus (Befehlsausfuehrung deaktiviert) ÃüÁî°×Ãûµ¥=Befehls-Whitelist MCPÃüÁîִÐÐ=MCP-Befehlsausfuehrung @@ -2006,6 +2006,10 @@ Token=Token ÆôÓÃԶ³̿ØÖƣ¨AI ²ٿØ×ÀÃ棩=Fernsteuerung aktivieren (KI steuert den Desktop) ÆôÓÃÎļþ´«Ê䣨download_file£©=Dateiuebertragung aktivieren (download_file) ÆôÓùÜÀí²Ù×÷£¨½ø³Ì/´°¿Ú/·þÎñ/µçԴ£©=Verwaltungsoperationen aktivieren (Prozess/Fenster/Dienst/Energie) +ÆôÓÃÖ÷»ú¹ÜÀí£¨½ø³Ì/´°¿Ú/·þÎñ/ÈÎÎñ/ÍøÂç״̬£©=Maschinenverwaltung aktivieren (Prozess/Fenster/Dienst/Aufgabe/Netzwerkstatus) +ÆôÓÃΣÏÕÖ÷»ú²Ù×÷£¨¶³½á/½ⶳ½ø³̡¢ֹͣ/ÖØÆô·þÎñ£©=Gefaehrliche Maschinenoperationen aktivieren (Prozess einfrieren/auftauen, Dienst stoppen/neu starten) +ΣÏÕÖ÷»ú²Ù×÷ÒÀ¸½ÓÚÖ÷»ú¹ÜÀí£ºÇëÏÈÆôÓÃÖ÷»ú¹ÜÀí¡£=Gefaehrliche Maschinenoperationen setzen die Maschinenverwaltung voraus. Aktivieren Sie zuerst die Maschinenverwaltung. +Ö÷»ú¹ÜÀíµÄд²Ù×÷Ðèֻ¶Áģʽ¹رգ»¶³½á/½ⶳ½ø³̡¢ֹͣ/ÖØÆô·þÎñ»¹ÐèÆôÓÃΣÏÕÖ÷»ú²Ù×÷¡£=Schreiboperationen der Maschinenverwaltung erfordern deaktivierten Nur-Lesen-Modus; Einfrieren/Auftauen von Prozessen und Stoppen/Neustarten von Diensten erfordern zusaetzlich gefaehrliche Maschinenoperationen. µÚ %d ¸öǮ°üµØַ²»ºϷ¨!=Wallet-Adresse #%d ist ungueltig! %d ÐÂÁ¬½Ó\r\n=%d Neue Verbindung\r\n %d ±¾µØÁ¬½ӶϿª\r\n=%d Lokale Verbindung geschlossen\r\n diff --git a/server/2015Remote/lang/en_US.ini b/server/2015Remote/lang/en_US.ini index 1935629..1ebcacf 100644 --- a/server/2015Remote/lang/en_US.ini +++ b/server/2015Remote/lang/en_US.ini @@ -1997,7 +1997,7 @@ MCP ¶˿ÚÐèΪ 1-65535 µÄÊý×Ö=Port must be a number between 1 and 65535 Token ²»ÄÜΪ¿Õ=Token cannot be empty MCP ÉèÖÃÒѱ£´档=MCP settings saved. -ÆôÓÃ/¶˿Ú/°󶨵Øַ/Token/ֻ¶Á/°×Ãûµ¥/³־ÃÖնË/Զ³̿ØÖÆ/Îļþ´«Êä/¹ÜÀí²Ù×÷µĸ͝ÐèÖØÆô³ÌÐòÉúЧ¡£=Changes to enable/port/bind address/token/read-only/whitelist/persistent terminal/remote control/file transfer/management operations take effect after restart. +ÆôÓÃ/¶˿Ú/°󶨵Øַ/Token/ֻ¶Á/°×Ãûµ¥/³־ÃÖնË/Զ³̿ØÖÆ/Îļþ´«Êä/¹ÜÀí²Ù×÷/Ö÷»ú¹ÜÀí/ΣÏÕÖ÷»ú²Ù×÷µĸ͝ÐèÖØÆô³ÌÐòÉúЧ¡£=Changes to enable/port/bind address/token/read-only/whitelist/persistent terminal/remote control/file transfer/management operations/host management/dangerous host operations take effect after restart. ֻ¶Áģʽ£¨½ûÃüÁîִÐУ©=Read-only mode (disable command execution) ÃüÁî°×Ãûµ¥=Command whitelist MCPÃüÁîִÐÐ=MCP command execution @@ -2011,6 +2011,10 @@ Token=Token ÆôÓÃԶ³̿ØÖƣ¨AI ²ٿØ×ÀÃ棩=Enable remote control (AI controls the desktop) ÆôÓÃÎļþ´«Ê䣨download_file£©=Enable file transfer (download_file) ÆôÓùÜÀí²Ù×÷£¨½ø³Ì/´°¿Ú/·þÎñ/µçԴ£©=Enable management operations (process/window/service/power) +ÆôÓÃÖ÷»ú¹ÜÀí£¨½ø³Ì/´°¿Ú/·þÎñ/ÈÎÎñ/ÍøÂç״̬£©=Enable host management (processes/windows/services/tasks/network state) +ÆôÓÃΣÏÕÖ÷»ú²Ù×÷£¨¶³½á/½ⶳ½ø³̡¢ֹͣ/ÖØÆô·þÎñ£©=Enable dangerous host operations (freeze/thaw process, stop/restart service) +ΣÏÕÖ÷»ú²Ù×÷ÒÀ¸½ÓÚÖ÷»ú¹ÜÀí£ºÇëÏÈÆôÓÃÖ÷»ú¹ÜÀí¡£=Dangerous host operations depend on host management. Enable host management first. +Ö÷»ú¹ÜÀíµÄд²Ù×÷Ðèֻ¶Áģʽ¹رգ»¶³½á/½ⶳ½ø³̡¢ֹͣ/ÖØÆô·þÎñ»¹ÐèÆôÓÃΣÏÕÖ÷»ú²Ù×÷¡£=Host management write operations require read-only mode to be off; freezing/thawing processes and stopping/restarting services also require dangerous host operations. µÚ %d ¸öǮ°üµØַ²»ºϷ¨!=Wallet address #%d is invalid! %d ÐÂÁ¬½Ó\r\n=%d New connection\r\n %d ±¾µØÁ¬½ӶϿª\r\n=%d Local connection closed\r\n diff --git a/server/2015Remote/lang/zh_TW.ini b/server/2015Remote/lang/zh_TW.ini index 9843852..4b32300 100644 --- a/server/2015Remote/lang/zh_TW.ini +++ b/server/2015Remote/lang/zh_TW.ini @@ -1988,7 +1988,7 @@ MCP ¶˿ÚÐèΪ 1-65535 µÄÊý×Ö=ßB½ӲºÐèžé 1-65535 µĔµ×Ö Token ²»ÄÜΪ¿Õ=Token ²»Äܞé¿Õ MCP ÉèÖÃÒѱ£´档=MCP ÔO¶¨Òу¦´档 -ÆôÓÃ/¶˿Ú/°󶨵Øַ/Token/ֻ¶Á/°×Ãûµ¥/³־ÃÖնË/Զ³̿ØÖÆ/Îļþ´«Êä/¹ÜÀí²Ù×÷µĸ͝ÐèÖØÆô³ÌÐòÉúЧ¡£=†¢ÓÃ/ßB½Ӳº/½‰¶¨λַ/Token/Ψ×x/°×Ãû†Î/³־ýK¶Ë/ßh¶˿ØÖÆ/™n°¸‚÷ݔ/¹ÜÀí²Ù×÷µÄ׃¸üÐèÖØÐ†¢„ӳÌʽááÉúЧ¡£ +ÆôÓÃ/¶˿Ú/°󶨵Øַ/Token/ֻ¶Á/°×Ãûµ¥/³־ÃÖնË/Զ³̿ØÖÆ/Îļþ´«Êä/¹ÜÀí²Ù×÷/Ö÷»ú¹ÜÀí/ΣÏÕÖ÷»ú²Ù×÷µĸ͝ÐèÖØÆô³ÌÐòÉúЧ¡£=†¢ÓÃ/ßB½Ӳº/½‰¶¨λַ/Token/Ψ×x/°×Ãû†Î/³־ýK¶Ë/ßh¶˿ØÖÆ/™n°¸‚÷ݔ/¹ÜÀí²Ù×÷/Ö÷™C¹ÜÀí/ΣëUÖ÷™C²Ù×÷µÄ׃¸üÐèÖØÐ†¢„ӳÌʽááÉúЧ¡£ ֻ¶Áģʽ£¨½ûÃüÁîִÐУ©=Ψ×xģʽ£¨½ûÃüÁîˆÌÐУ© ÃüÁî°×Ãûµ¥=ÃüÁî°×Ãû†Î MCPÃüÁîִÐÐ=MCPÃüÁîˆÌÐÐ @@ -2002,6 +2002,10 @@ Token=Token ÆôÓÃԶ³̿ØÖƣ¨AI ²ٿØ×ÀÃ棩=†¢ÓÃßh¶˿ØÖƣ¨AI ²ٿØ×ÀÃ棩 ÆôÓÃÎļþ´«Ê䣨download_file£©=†¢ÓÙn°¸‚÷ݔ£¨download_file£© ÆôÓùÜÀí²Ù×÷£¨½ø³Ì/´°¿Ú/·þÎñ/µçԴ£©=†¢ÓùÜÀí²Ù×÷£¨³ÌÐò/ҕ´°/·þ„Õ/ëŠԴ£© +ÆôÓÃÖ÷»ú¹ÜÀí£¨½ø³Ì/´°¿Ú/·þÎñ/ÈÎÎñ/ÍøÂç״̬£©=†¢ÓÃÖ÷™C¹ÜÀí£¨̎Àí³ÌÐò/ҕ´°/·þ„Õ/Åų̹¤×÷/¾W· î‘B£© +ÆôÓÃΣÏÕÖ÷»ú²Ù×÷£¨¶³½á/½ⶳ½ø³̡¢ֹͣ/ÖØÆô·þÎñ£©=†¢ÓÃΣëUÖ÷™C²Ù×÷£¨ƒö½Y/½âƒö̎Àí³ÌÐò¡¢ֹͣ/ÖØÐ†¢„ӷþ„գ© +ΣÏÕÖ÷»ú²Ù×÷ÒÀ¸½ÓÚÖ÷»ú¹ÜÀí£ºÇëÏÈÆôÓÃÖ÷»ú¹ÜÀí¡£=ΣëUÖ÷™C²Ù×÷ÒÀ¸½ì¶Ö÷™C¹ÜÀí£ºՈÏȆ¢ÓÃÖ÷™C¹ÜÀí¡£ +Ö÷»ú¹ÜÀíµÄд²Ù×÷Ðèֻ¶Áģʽ¹رգ»¶³½á/½ⶳ½ø³̡¢ֹͣ/ÖØÆô·þÎñ»¹ÐèÆôÓÃΣÏÕÖ÷»ú²Ù×÷¡£=Ö÷™C¹ÜÀíµČ‘Èë²Ù×÷ÐèêPé]Ψ×xģʽ£»ƒö½Y/½âƒö̎Àí³ÌÐò¡¢ֹͣ/ÖØÐ†¢„ӷþ„Õ߀Ð膢ÓÃΣëUÖ÷™C²Ù×÷¡£ µÚ %d ¸öǮ°üµØַ²»ºϷ¨!=µÚ %d ‚€åX°üλַ²»ºϷ¨! %d ÐÂÁ¬½Ó\r\n=%d ÐÂßB¾€\r\n %d ±¾µØÁ¬½ӶϿª\r\n=%d ±¾™CßB¾€”àé_\r\n