Feature: Add host management and connection management MCP tools
Expose the online-list TLS column through list_online_hosts, search_hosts and get_host_detail. The value comes from the flag the client reports at login, so old clients always read false. Add list_network_connections and kill_network_connection on top of the connection manager sub-connection protocol. The kill path matches the original 110-byte record instead of rebuilding addresses, supports a dry run, refuses UDP/IPv6 targets, and verifies that the quadruple really disappeared instead of assuming success. Add a host_* family backed by the machine manager protocol: ten read tools covering every tab of the host management dialog, and four recoverable write tools for process freeze/thaw, window restore/hide/ maximize/minimize, service start/stop/pause/continue/restart, and scheduled task run/stop. Every write re-reads the snapshot before sending and re-checks afterwards, reporting -32009 for stale targets rather than acting blindly. The family is gated by McpMachine, the risky subset by McpMachineDangerous, and writes additionally require read-only mode to be off. Route managed sub-connections from MessageHandle and OfflineProc, keep the single-in-flight rule by sharing the pending table with a request kind, and move the shared JSON/lock/encoding helpers into McpInternal so both tool families use one implementation. Settings dialog, design docs and en_US/zh_TW/de_DE translations are updated accordingly. Co-Authored-By: deepseek-flash
This commit is contained in:
1 parent
6c8336b497
commit
38cd207f5d
17 files changed
+3615
-184
No files matched your search
+3
-1
@@ -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` 一致。
|
||||
|
||||
|
||||
@@ -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` 去掉末尾 `"\<name>"` 派生 |
|
||||
| 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 <id> <tool>: 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 工具,服务端无残留子连接。
|
||||
@@ -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) {
|
||||
|
||||
@@ -328,6 +328,8 @@
|
||||
<ClInclude Include="KeyBoardDlg.h" />
|
||||
<ClInclude Include="LangManager.h" />
|
||||
<ClInclude Include="McpServer.h" />
|
||||
<ClInclude Include="McpInternal.h" />
|
||||
<ClInclude Include="McpMachine.h" />
|
||||
<ClInclude Include="McpSettingsDlg.h" />
|
||||
<ClInclude Include="TlsCertificateDlg.h" />
|
||||
<ClInclude Include="NetworkDlg.h" />
|
||||
@@ -397,6 +399,8 @@
|
||||
<ClCompile Include="WebService.cpp" />
|
||||
<ClCompile Include="HostJson.cpp" />
|
||||
<ClCompile Include="McpServer.cpp" />
|
||||
<ClCompile Include="McpInternal.cpp" />
|
||||
<ClCompile Include="McpMachine.cpp" />
|
||||
<ClCompile Include="McpSettingsDlg.cpp" />
|
||||
<ClCompile Include="TlsCertificateDlg.cpp" />
|
||||
<ClCompile Include="..\..\client\MemoryModule.c">
|
||||
|
||||
@@ -89,6 +89,8 @@
|
||||
<ClCompile Include="TriggerSettingsDlg.cpp" />
|
||||
<ClCompile Include="HostJson.cpp" />
|
||||
<ClCompile Include="McpServer.cpp" />
|
||||
<ClCompile Include="McpInternal.cpp" />
|
||||
<ClCompile Include="McpMachine.cpp" />
|
||||
<ClCompile Include="McpSettingsDlg.cpp" />
|
||||
<ClCompile Include="TlsCertificateDlg.cpp" />
|
||||
<ClCompile Include="CClientLog.cpp" />
|
||||
@@ -203,6 +205,8 @@
|
||||
<ClInclude Include="PreviewTipWnd.h" />
|
||||
<ClInclude Include="TriggerSettingsDlg.h" />
|
||||
<ClInclude Include="McpServer.h" />
|
||||
<ClInclude Include="McpInternal.h" />
|
||||
<ClInclude Include="McpMachine.h" />
|
||||
<ClInclude Include="McpSettingsDlg.h" />
|
||||
<ClInclude Include="TlsCertificateDlg.h" />
|
||||
<ClInclude Include="CClientLog.h" />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
// MCP 工具实现共享的内部辅助函数。
|
||||
//
|
||||
// 这些函数原先都定义在 McpServer.cpp 的匿名命名空间里;主机管理 / 连接管理
|
||||
// 工具族(McpMachine.cpp)需要复用同一套 JSON、host id 解析与编码转换逻辑,
|
||||
// 故抽出到独立编译单元,由 McpServer.cpp 与 McpMachine.cpp 共同引用。
|
||||
//
|
||||
// 约定:函数语义与实现与抽取前完全一致(McpServer.cpp 通过 using-directive
|
||||
// 引入,调用点不变),不做行为改动。
|
||||
|
||||
#include <Windows.h>
|
||||
#include "jsoncpp/json.h"
|
||||
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
|
||||
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
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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 <string>
|
||||
|
||||
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
|
||||
+30
-176
@@ -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 <sstream>
|
||||
|
||||
@@ -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<std::mutex> 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<std::mutex> 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
|
||||
|
||||
@@ -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<BYTE>& 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<bool> m_running{false};
|
||||
@@ -306,11 +350,43 @@ private:
|
||||
std::vector<BYTE> 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<BYTE> 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<uint64_t, PendingRequest> m_Pending;
|
||||
|
||||
// ===== P8:子连接路由(受 m_PendingMutex 保护)=====
|
||||
// 已绑定子连接 → device(该子连接的一切包都由 MCP 消费)。
|
||||
std::map<context*, uint64_t> m_SubCtxToDevice;
|
||||
// CancelIO 之后仍可能到达的包:静默丢弃(避免子连接回收前的尾巴包弹对话框)。
|
||||
std::set<context*> m_AbsorbedSubCtx;
|
||||
// 会话异常结束后 5s 内的迟到就绪包墓碑(device / peer 两个维度)。
|
||||
struct OrphanMark {
|
||||
std::chrono::steady_clock::time_point expiry;
|
||||
PendingKind kind = PendingKind::Generic;
|
||||
};
|
||||
std::map<uint64_t, OrphanMark> m_OrphanByDevice;
|
||||
std::map<std::string, OrphanMark> m_OrphanByPeer;
|
||||
|
||||
// 清理过期墓碑(持 m_PendingMutex 调用)。
|
||||
void PruneOrphanMarksLocked(std::chrono::steady_clock::time_point now);
|
||||
|
||||
// 路由表大小(已绑定 + 已吸收的子连接数)。MessageHandle 每个包都会调用
|
||||
// OnManagedSubPacket,若无会话在飞则用它无锁快速返回,避免音频/屏幕等高频
|
||||
// 子连接逐包争用 m_PendingMutex。只在持锁时更新(store)。
|
||||
std::atomic<int> m_ManagedRoutes{0};
|
||||
void RefreshManagedRoutesLocked();
|
||||
|
||||
// get_screenshot 的 reqId 发生器(16 位,跳过 0)。与 MFC 预览的 m_PreviewReqId 各自独立计数。
|
||||
std::atomic<uint16_t> 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 末尾)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1988,7 +1988,7 @@ MCP
|
||||
端口需为 1-65535 的数字=連接埠需為 1-65535 的數字
|
||||
Token 不能为空=Token 不能為空
|
||||
MCP 设置已保存。=MCP 設定已儲存。
|
||||
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。=啟用/連接埠/綁定位址/Token/唯讀/白名單/持久終端/遠端控制/檔案傳輸/管理操作的變更需重新啟動程式後生效。
|
||||
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作/主机管理/危险主机操作的改动需重启程序生效。=啟用/連接埠/綁定位址/Token/唯讀/白名單/持久終端/遠端控制/檔案傳輸/管理操作/主機管理/危險主機操作的變更需重新啟動程式後生效。
|
||||
只读模式(禁命令执行)=唯讀模式(禁命令執行)
|
||||
命令白名单=命令白名單
|
||||
MCP命令执行=MCP命令執行
|
||||
@@ -2002,6 +2002,10 @@ Token=Token
|
||||
启用远程控制(AI 操控桌面)=啟用遠端控制(AI 操控桌面)
|
||||
启用文件传输(download_file)=啟用檔案傳輸(download_file)
|
||||
启用管理操作(进程/窗口/服务/电源)=啟用管理操作(程序/視窗/服務/電源)
|
||||
启用主机管理(进程/窗口/服务/任务/网络状态)=啟用主機管理(處理程序/視窗/服務/排程工作/網路狀態)
|
||||
启用危险主机操作(冻结/解冻进程、停止/重启服务)=啟用危險主機操作(凍結/解凍處理程序、停止/重新啟動服務)
|
||||
危险主机操作依附于主机管理:请先启用主机管理。=危險主機操作依附於主機管理:請先啟用主機管理。
|
||||
主机管理的写操作需只读模式关闭;冻结/解冻进程、停止/重启服务还需启用危险主机操作。=主機管理的寫入操作需關閉唯讀模式;凍結/解凍處理程序、停止/重新啟動服務還需啟用危險主機操作。
|
||||
第 %d 个钱包地址不合法!=第 %d 個錢包位址不合法!
|
||||
%d 新连接\r\n=%d 新連線\r\n
|
||||
%d 本地连接断开\r\n=%d 本機連線斷開\r\n
|
||||
|
||||
Reference in new issue
Block a user