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:
yuanyuanxianganddeepseek-flash committed 2026-10-21 20:01:41 +02:00
1 parent 6c8336b497
commit 38cd207f5d
17 files changed
+3615 -184

No files matched your search

+3 -1
View File
@@ -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` 一致。
+273
View File
@@ -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 工具,服务端无残留子连接。
+19
View File
@@ -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" />
+4
View File
@@ -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;
}
+172
View File
@@ -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
+59
View File
@@ -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
+31
View File
@@ -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
View File
@@ -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
+78
View File
@@ -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 末尾)
+35 -4
View File
@@ -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);
+4
View File
@@ -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;
+5 -1
View File
@@ -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
+5 -1
View File
@@ -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
+5 -1
View File
@@ -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