2 Commits
Author SHA1 Message Date
yuanyuanxianganddeepseek-v4-pro 9eed43bc8d Feature: Add MCP device management-control tools (kill/window/service/power)
Add P7 management-control tools behind a single McpManage flag (plus
McpReadonly=0): kill_process, operate_window, control_service and
power_action. The write tools run a ready->confirm lifecycle on their
sub-connection, using a new followUp/followUpSent field to tell the two
arrivals of the same TOKEN_*LIST apart; the write command and the follow-up
refresh command go out as two separate Send2Client calls, with a new
WaitSendIdle() (context.h / Server.h) draining the outbound buffer in between
so the same buffer is never WSASend'd concurrently.

McpSettingsDlg gains a "management operations" checkbox (IDC_MCP_MANAGE), the
main toolbar gains an MCP entry button, and en/de/zh-TW strings are synced.
power_action is fire-and-forget over the main connection (no confirmation).

Verified end-to-end against a real Windows 10/VMware client: kill_process
returns remaining:[] and operate_window/control_service return ok:true with
their confirm packet arriving after an HTTP split. Verification surfaced a
prerequisite parser bug, fixed separately in a72e6eb, where HeaderParser
latched m_bShouldUnmask FALSE on HTTP continuation chunks and dropped the
confirm packet.

Co-Authored-By: deepseek-v4-pro
2026-10-01 16:56:30 +02:00
yuanyuanxianganddeepseek-v4-pro a72e6eb391 Fix: keep m_bShouldUnmask latched TRUE once a connection is HTTP
HeaderParser::Parse reset m_bShouldUnmask to FALSE on any non-HTTP
maskType, including the continuation chunk of an HTTP connection whose
read offset is already past the "POST " header. When a large packet
(e.g. a >1460-byte process list) was split by MSS, the second chunk
latched the flag to FALSE, so the next new HTTP-masked packet was no
longer unmasked: CheckHead then read the HTTP header as the flag,
returned FLAG_UNKNOWN/PARSER_FAILED, and the packet was dropped. This
explains "first packet succeeds, subsequent packets vanish", and why
the MFC process dialog went blank after killing a process (the confirm
packet never reached NotifyProc).

Only clear the flag when the connection was not already determined to
be HTTP (initial -1, or already confirmed non-HTTP).

Co-Authored-By: deepseek-v4-pro
2026-10-01 16:18:26 +02:00
13 changed files with 938 additions and 11 deletions

No files matched your search

+219
View File
@@ -0,0 +1,219 @@
# MCP P7 设计:设备管理控制(kill / window / service / power)
> 延续 `Mcp_Design.md` → `Mcp_Phase2_Design.md`(P2)→ P3/P4/P5/P6 的阶段演进。
> 本文定义 P7:把 YAMA 服务端高频「控制类」操作 MCP 化,补上从「观测」到「管控」的短板,
> 服务「自然语言即可操作服务端、对受管设备全方位管控」的最终目标。
>
> 安全门结论(已确认):新增**单一 `McpManage` 开关**统一门控本阶段全部控制类工具,
> 并要求 `McpReadonly=0`;不再为每个工具单设开关。
## 1. 目标与范围
P7 交付 4 个控制类工具,覆盖服务端日常最常用的写操作:
| 工具 | 动作 | 命令 | 连接 | 确认方式 |
|---|---|---|---|---|
| `kill_process` | 结束进程(多 pid) | `COMMAND_KILLPROCESS` | 系统子连接 | 补发 `COMMAND_PSLIST` 收新列表 |
| `operate_window` | 关闭/显示/隐藏/最小化/最大化/还原 | `CMD_WINDOW_CLOSE` / `CMD_WINDOW_TEST` | 窗口子连接 | close 自动回 `TOKEN_WSLIST`;test 补发 `COMMAND_WSLIST` |
| `control_service` | 启动/停止/设为自动/设为手动 | `COMMAND_SERVICECONFIG` | 服务子连接 | 客户端每项 Sleep(500) 后自动 `SendServicesList` |
| `power_action` | 注销/关机/重启 | `TOKEN_MACHINE_MANAGE` | 主连接 | 发完即走(无响应) |
仅 Windows 客户端支持(`clientType` 为 LNX/MAC/**APK** 提前返回 `-32005`;注意 `list_services`
只排除了 LNX/MAC、漏掉 APK,P7 需把 APK 一并排除)。
## 2. 复用现状
P7 **不改客户端**。客户端已全部实现下列处理(MFC 界面在跑),MCP 侧只需补齐
「发命令 → 接管子连接 → 发写命令 → 收确认 → 关闭」这条链:
- 系统子连接:`COMMAND_SYSTEM`(进程)/ `COMMAND_WSLIST`(窗口)→ 客户端开 `CSystemManager`
子连接(`client/KernelManager.cpp` 1169/1182,`client/SystemManager.cpp`)。
- `COMMAND_KILLPROCESS`:`CSystemManager::KillProcess`(`TerminateProcess`,无回包)。
- `CMD_WINDOW_CLOSE`:`PostMessage(hWnd, WM_CLOSE)` 后自动 `SendWindowsList`。
- `CMD_WINDOW_TEST`:`ShowWindow(hwnd, dHow)`(不自动回包)。
- `COMMAND_SERVICECONFIG`:`CServicesManager::ServicesConfig`(`client/ServicesManager.cpp` 213),
bCmd=1 启动 / 2 停止 / 3 自动 / 4 手动,每项后 `Sleep(500)` + `SendServicesList`。
- `TOKEN_MACHINE_MANAGE`:`KernelManager.cpp` 872,`ExitWindowsEx`(注销/关机/重启)。
复用现有 MCP 基建(`server/2015Remote/McpServer.{h,cpp}`):
- `ParseHostIdArg` / `FindMainContext`:入参解析 + 主连接定位。
- `BeginPending` / `WaitPending` / `ClearPending` / `TakeMainResponse`:单飞行挂起 + 响应回收。
- `OnRegeditReady`:已示范「接管子连接 → 发后续命令」的范式(P3 list_registry 三阶段),P7 直接泛化。
## 3. 子连接生命周期(P7 核心新增)
写工具与只读工具的关键差异:只读工具「子连接回一包即完成」,写工具需要
「**接管子连接 → 发写命令 → 收确认 → 关闭**」。在现有 `PendingRequest` 上增加
`followUp`(接管后要发的写命令 payload,二进制)+ `followUpSent`(是否已发写命令)两个字段:
```
followUpSent == false → 等子连接就绪(首包 TOKEN_*LIST 到达 = 就绪)
followUpSent == true → 已发写命令,等确认包
```
> 关键事实(已核实):三个工具各自的「就绪 token」与「确认 token」**是同一个 token**——
> 客户端子连接构造即回一包(就绪),写操作后再回一包(确认)。因此必须用 `followUpSent`
> 区分这两次到达,而不能靠 token 区分。这也是它与 `list_registry`(PATH/KEY 两个不同 token)
> 的差别。
流程(以 `kill_process` 为例):
1. 工具线程 `BeginPending(devId, "kill_process")` 登记单飞行(false = 设备忙),并预填
`followUp`(`COMMAND_KILLPROCESS` 多 pid 包)。
2. 主连接发 `COMMAND_SYSTEM`。
3. 客户端开系统子连接,首包 `TOKEN_PSLIST` 到达 → `MessageHandle` 调新增的
`OnSystemReady(devId, subCtx)`(类比 `OnRegeditReady`):
- 命中挂起且 `followUpSent==false` → 保存 `subCtx`、置 `followUpSent=true`、向子连接发
`followUp`,随后补发 `COMMAND_PSLIST` 索取新列表。
4. 客户端杀进程后回第二个 `TOKEN_PSLIST` → `TakeMainResponse` 取走(`followUpSent==true`
命中)→ 关闭子连接(`CancelIO`)→ 唤醒工具线程。
5. 工具线程解析新进程列表返回;超时/失败走 `ClearPending`(**含 `subCtx->CancelIO()`**,防孤儿子连接)。
> 注意:`followUp`(写命令)与「补发的 `COMMAND_PSLIST`/`COMMAND_WSLIST`」必须是**两次独立的
> `Send2Client` 调用**,不能拼成一个 buffer——客户端 `OnReceive` 按 `szBuffer[0]` 单命令分发,
> 拼包会把后续字节当成 pid/hwnd 解析。
接管函数(进程/窗口共用同一个 `OnSystemReady`——两者都在同一个 `CSystemManager` 子连接上,
客户端 `OnReceive` 统一处理 `COMMAND_PSLIST`/`COMMAND_KILLPROCESS`/`COMMAND_WSLIST`/
`CMD_WINDOW_CLOSE`/`CMD_WINDOW_TEST`):
| 工具 | 开子连接命令 | 就绪包(首包) | 接管后写命令 | 确认包(第二包) |
|---|---|---|---|---|
| `kill_process` | `COMMAND_SYSTEM` | `TOKEN_PSLIST` | `COMMAND_KILLPROCESS` + 补 `COMMAND_PSLIST` | `TOKEN_PSLIST` |
| `operate_window` | `COMMAND_WSLIST` | `TOKEN_WSLIST` | `CMD_WINDOW_CLOSE`(close 自动回);`CMD_WINDOW_TEST`(test 补 `COMMAND_WSLIST`) | `TOKEN_WSLIST` |
| `control_service` | `COMMAND_SERVICES` | `TOKEN_SERVERLIST` | `COMMAND_SERVICECONFIG`(客户端写后自动 `SendServicesList`) | `TOKEN_SERVERLIST` |
`MessageHandle` 现有分派点(均已核实):
- `TOKEN_PSLIST`/`TOKEN_WSLIST` 分支:`2015RemoteDlg.cpp:6540`(当前 `TakeMainResponse`+`CancelIO`)
- `TOKEN_SERVERLIST` 分支:`2015RemoteDlg.cpp:6585`(同上)
接入方式:在这两个分支的 `IsPending(devId)` 命中后,**先按 `pending.tool` 判断是否为
写工具(kill/operate/control_service)**;是则调 `OnSystemReady`/`OnServiceReady`(返回 true
= 已接管、不 `CancelIO`),否则沿用现有 `TakeMainResponse`+`CancelIO`。与 `TOKEN_REGEDIT`
分支调用 `OnRegeditReady`(`2015RemoteDlg.cpp:6561`)的写法一致,避免与人工界面抢子连接。
## 4. 工具 schema
### 4.1 `kill_process`
```jsonc
// inputSchema
{ "id": "string(必填,主机 id)",
"pids": ["integer 数组(必填,至少 1 个进程 PID)"] }
// outputSchema
{ "remaining": ["integer 数组(杀完后仍在运行的 PID)"] }
```
- payload:`[COMMAND_KILLPROCESS:1][pid:4]*N`(DWORD 小端,与 `SystemDlg::OnPlistKill` 一致)。
- 成功判定:返回 `remaining`(发 `COMMAND_PSLIST` 后进程列表里已不含目标 pid 即视为成功)。
- 描述需写明:`TerminateProcess` 强制结束、不保存进程状态,可能造成数据丢失。
### 4.2 `operate_window`
```jsonc
// inputSchema
{ "id": "string(必填)",
"hwnd": "integer(必填,窗口句柄,取 list_windows 返回的 hwnd)",
"action": "string(必填)close|show|hide|minimize|maximize|restore" }
// outputSchema
{ "ok": "boolean" }
```
- payload(与 `SystemDlg::OnWlistClose/OnWlistHide/OnWlistRecover/OnWlistMax/OnWlistMin` 一致,
**固定 20 字节零填充 `BYTE lpMsgBuf[20]`,hwnd 一律按 `DWORD`(4 字节)小端写入 offset 1**):
- `close` → `[CMD_WINDOW_CLOSE:1][hwnd:4]`(客户端 `PostMessage WM_CLOSE`)。
- 其余 → `[CMD_WINDOW_TEST:1][hwnd:4][dHow:4]`,dHow 映射(MFC 实测值):
`hide=SW_HIDE(0)` / `show=SW_NORMAL(1)` / `restore=SW_NORMAL(1)` /
`maximize=SW_MAXIMIZE(3)` / `minimize=SW_MINIMIZE(6)`。
- 缓冲器必须补零到 20 字节:客户端 `close` 分支读 `*((HWND*)(szBuffer+1))`,在 x64 是 8 字节,
而服务端只写了 4 字节 hwnd——正是 20 字节零填充补齐了高 4 字节,读出的 HWND 才是正确值;
若 MCP 发最小 5 字节包,会越界读。`test` 分支(`TestWindow`)读 `[hwnd:4][dHow:4]` 共 9 字节,
20 字节缓冲也足够。MFC 固定发 `lpMsgBuf[20]` 正是为此,MCP 照搬。
### 4.3 `control_service`
```jsonc
// inputSchema
{ "id": "string(必填)",
"service_name": "string(必填,服务名,取 list_services 返回的 service_name)",
"action": "string(必填)start|stop|set_auto|set_manual" }
// outputSchema
{ "ok": "boolean" }
```
- payload:`COMMAND_SERVICECONFIG [cmd:1][bCmd:1][serviceName\0]`,
bCmd:`start=1` / `stop=2` / `set_auto=3` / `set_manual=4`。
- 边界:客户端 `ServicesManager` 仅实现这 4 项;**无** pause / continue / delete
(它们属于 `MachineDlg` 机器管理的另一条链路,留待 P8 评估)。
### 4.4 `power_action`
```jsonc
// inputSchema
{ "id": "string(必填)",
"action": "string(必填)logout|shutdown|reboot" }
// outputSchema
{ "ok": "boolean" }
```
- payload:`TOKEN_MACHINE_MANAGE [token:1][type:1]`,type:`logout=0` / `shutdown=1` / `reboot=2`
(`enum MachineCommand` 顺序)。MFC 发的是 32 字节缓冲(`2015RemoteDlg::MachineManage`),
但客户端只校验 `ulLength>1` 并读 `szBuffer[1]`,故 2 字节即可。
- 走**主连接**、发完即走、无响应;主机将立即离线。描述需明确「不可撤销、主机下线」。
- 需客户端管理员权限(`EnableShutdownPrivilege`),无权限时客户端静默忽略(同 MFC 现状)。
- **仅 Windows 客户端**:Linux 侧文档写的是 `COMMAND_SESSION`(`linux/README.md`),但代码中
未发现对应 handler,即 Linux 实际未实现电源操作——`power_action` 对 LNX/MAC/APK 一律返回
`-32005`,不尝试发送。
## 5. 安全门(单一 `McpManage` 开关)
- 新增配置键 `settings/McpManage`(默认 `0`,关闭)。
- 生效条件:`McpManage=1` 且 `McpReadonly=0`;二者任一不满足即隐藏 4 个工具。
- 双保险:`tools/list` 隐藏 + 每个 `BuildXxx` 入口二次校验(与 `exec_command` 的
`IsReadonly()` 双保险一致)。
- `McpSettingsDlg` 增加「管理操作(进程/窗口/服务/电源)」复选框,写入 `THIS_CFG`。
- 审计:每次写操作写入服务端审计日志(`get_audit_log` 可见),记录 tool + host id + 参数摘要 + 结果。
## 6. 单飞行与并发约束
- 沿用现有约束:同 host 同一时刻只允许一个 MCP 挂起请求(`BeginPending` 保证)。
- 写工具与只读工具共用 `m_Pending`,天然互斥;与 MFC 对话框并存(各用独立子连接,互不干扰)。
- 超时沿用 `kMcpToolTimeoutMs = 20000`;`power_action` 因无响应、发完即返回成功。
## 7. 边界与风险
1. **杀进程/关机/重启不可逆**:描述与审计都需显著标注;不提供「确认对话框」,
确认责任在调用方(MCP 客户端/LLM agent)。
2. **kill 无客户端回执**:成功与否靠「新进程列表不含目标 pid」推断,`TerminateProcess`
失败(权限不足等)时进程仍在 `remaining`,由调用方判断。
3. **服务操作仅 4 项**:pause/continue/delete 缺省,如需需评估 `MachineDlg` 链路。
4. **窗口句柄时效**:hwnd 可能已失效(窗口已关闭),客户端 `PostMessage/ShowWindow`
对失效句柄静默无操作,`operate_window` 的 `ok=true` 只表示「已下发」。
5. **子连接竞态**:写工具接管子连接后若客户端断线,`OfflineProc` 调 `OnManageSubClosed`
清理挂起(置 done 并清 `subCtx`、唤醒等待线程;**不擦表**,由 `WaitPendingWrite` 自行擦除,
防迭代器失效)。清 `subCtx` 是关键——context 对象被连接池回收复用,若超时路径仍对旧指针
`CancelIO` 会误伤新连接(与 `OnTerminalClosed`/`OnScreenControlClosed` 同理)。
## 8. P8 / P9 路线图(后续)
- **P8 文件与信息面**:`delete_file/delete_directory`、`create_folder`、`rename_file`、
`open_file`;`list_network_connections`、`list_software`、`get/set_hosts`、
`list_scheduled_tasks`(+增删启停)、`get_keylog`。
- **P9 进阶与 NL 增强**:`set/delete_registry`、`screen_blank`/`lock_input`/`get_clipboard`、
`open_url`/`send_message`;**批量操作**(`ids[]`/`group` 维度);离线主机枚举
`list_all_hosts`。
## 9. 实现与验证记录(2026-10-01)
已实现并端到端验证(真实客户端 DESKTOP-A55C0OI / Windows 10 / VMware,id=3441387693643366974):
- `kill_process` → `remaining:[]`;`operate_window`(restore) → `ok:true`;
`control_service`(set_manual AJRouter) → `ok:true`;`power_action`(reboot) → `ok:true`(发完即走)。
三个写工具的确认包(第二个 `TOKEN_*LIST`)均正常到达,列表均 >1460 字节触发 HTTP 分片。
- 落地:`McpManage` 开关 + `McpReadonly=0` 双保险;`McpSettingsDlg` 增「管理操作」复选框;
主工具栏增 `MCP` 入口按钮(`ID_MCP_SETTINGS`);en/de/zh-TW 三语文案同步。
- 前置解析 bug(验证中暴露,已单独提交 `a72e6eb`):`HeaderParser::Parse` 的
`m_bShouldUnmask` 在 HTTP 分片续包上被误置 `FALSE`,导致写工具的确认包被
`PARSER_FAILED` 丢弃(「首包能到、后续包到不了」);修复为 `else if (m_bShouldUnmask != TRUE)`。
Binary file not shown.
+19 -1
View File
@@ -1415,6 +1415,7 @@ VOID CMy2015RemoteDlg::CreateToolBar()
m_ToolBar.SetButtonText(12, _TR("搜索")); m_ToolBar.SetButtonText(12, _TR("搜索"));
m_ToolBar.SetButtonText(13, _TR("帮助")); m_ToolBar.SetButtonText(13, _TR("帮助"));
m_ToolBar.SetButtonText(14, _T("Lang/语言")); m_ToolBar.SetButtonText(14, _T("Lang/语言"));
m_ToolBar.SetButtonText(15, _T("MCP"));
// ============================================================ // ============================================================
// UIBranding: 根据编译时配置或运行时标志隐藏工具栏按钮 // UIBranding: 根据编译时配置或运行时标志隐藏工具栏按钮
@@ -1465,7 +1466,7 @@ VOID CMy2015RemoteDlg::CreateToolBar()
} }
// 如果所有按钮都被隐藏,则隐藏整个工具栏 // 如果所有按钮都被隐藏,则隐藏整个工具栏
if (hiddenCount >= 15) { if (hiddenCount >= 16) {
m_ToolBar.ShowWindow(SW_HIDE); m_ToolBar.ShowWindow(SW_HIDE);
} }
@@ -2209,6 +2210,8 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
McpServer().SetRemoteControlEnabled(THIS_CFG.GetInt("settings", "McpRemoteControl", 0) != 0); McpServer().SetRemoteControlEnabled(THIS_CFG.GetInt("settings", "McpRemoteControl", 0) != 0);
// 文件传输开关:默认关;仅 download_file 门控(不要求只读关)。 // 文件传输开关:默认关;仅 download_file 门控(不要求只读关)。
McpServer().SetFileTransferEnabled(THIS_CFG.GetInt("settings", "McpFileTransfer", 0) != 0); McpServer().SetFileTransferEnabled(THIS_CFG.GetInt("settings", "McpFileTransfer", 0) != 0);
// P7 管理控制开关:默认关;要求只读关(McpReadonly=0)才生效(工具列表/分派双重门控)。
McpServer().SetManageEnabled(THIS_CFG.GetInt("settings", "McpManage", 0) != 0);
if (!McpServer().Start(mcpBind, mcpPort)) { if (!McpServer().Start(mcpBind, mcpPort)) {
Mprintf("McpServer start failed on %s:%d\n", mcpBind.c_str(), mcpPort); Mprintf("McpServer start failed on %s:%d\n", mcpBind.c_str(), mcpPort);
} else { } else {
@@ -5202,6 +5205,12 @@ BOOL CALLBACK CMy2015RemoteDlg::OfflineProc(CONTEXT_OBJECT* ContextObject)
McpServer().OnScreenControlClosed(ContextObject); McpServer().OnScreenControlClosed(ContextObject);
} }
// MCP P7 管理写操作(kill/窗口/服务)接管的子连接断开:清挂起,避免超时路径对
// 已回收(连接池复用)的 context 误 CancelIO(与终端/屏幕子连接的清理同理)。
if (McpServer().IsRunning()) {
McpServer().OnManageSubClosed(ContextObject);
}
SOCKET nSocket = ContextObject->sClientSocket; SOCKET nSocket = ContextObject->sClientSocket;
CDialogBase* p = (CDialogBase*)ContextObject->hDlg; CDialogBase* p = (CDialogBase*)ContextObject->hDlg;
@@ -6541,8 +6550,13 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
case TOKEN_PSLIST: { // 进程管理【x】 case TOKEN_PSLIST: { // 进程管理【x】
// P2b:MCP 挂起时接管。进程/窗口列表无请求 id,按 device_id 匹配并同步拷贝 // P2b:MCP 挂起时接管。进程/窗口列表无请求 id,按 device_id 匹配并同步拷贝
// (此时 IO 线程阻塞在 NotifyProc,缓冲尚未被覆盖,拷贝安全)。 // (此时 IO 线程阻塞在 NotifyProc,缓冲尚未被覆盖,拷贝安全)。
// P7:写工具(kill_process/operate_window)的「就绪包」与「确认包」是同一 token,
// OnSystemReady 命中就绪包时接管子连接并下发写命令(返回 true、不 CancelIO),
// 命中确认包或只读工具时返回 false,走 TakeMainResponse + CancelIO 收尾。
uint64_t devId = ContextObject->GetClientID(); uint64_t devId = ContextObject->GetClientID();
if (McpServer().IsPending(devId)) { if (McpServer().IsPending(devId)) {
if (McpServer().OnSystemReady(devId, ContextObject))
break;
McpServer().TakeMainResponse(devId, ContextObject->GetBuffer(0), ContextObject->GetBufferLength()); McpServer().TakeMainResponse(devId, ContextObject->GetBuffer(0), ContextObject->GetBufferLength());
ContextObject->CancelIO(); // 用完即关,避免子链接泄漏(仿 WebService 终端关闭) ContextObject->CancelIO(); // 用完即关,避免子链接泄漏(仿 WebService 终端关闭)
break; break;
@@ -6585,8 +6599,12 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
case TOKEN_SERVERLIST: { // 服务管理【x】 case TOKEN_SERVERLIST: { // 服务管理【x】
// P3:MCP 挂起时接管。服务列表为一次性子链接回传(CServicesManager 构造即发), // P3:MCP 挂起时接管。服务列表为一次性子链接回传(CServicesManager 构造即发),
// 取走即 CancelIO 关子链接(无 MFC 对话框续用该子链接)。 // 取走即 CancelIO 关子链接(无 MFC 对话框续用该子链接)。
// P7:control_service 的就绪/确认是同一 token,OnServiceReady 命中就绪包时接管并下发
// COMMAND_SERVICECONFIG(返回 true、不 CancelIO),确认包走 TakeMainResponse 收尾。
uint64_t devId = ContextObject->GetClientID(); uint64_t devId = ContextObject->GetClientID();
if (McpServer().IsPending(devId)) { if (McpServer().IsPending(devId)) {
if (McpServer().OnServiceReady(devId, ContextObject))
break;
McpServer().TakeMainResponse(devId, szBuffer, len); McpServer().TakeMainResponse(devId, szBuffer, len);
ContextObject->CancelIO(); ContextObject->CancelIO();
break; break;
+610 -1
View File
@@ -840,6 +840,129 @@ Json::Value BuildServiceListOutputSchema() {
return schema; return schema;
} }
// ========== P7 输入/输出 schema ==========
// 公共:为 props 追加必填的 id 字符串属性。
static void AddIdRequiredProp(Json::Value& props) {
Json::Value idProp(Json::objectValue);
idProp["type"] = "string";
idProp["description"] = u8"主机 id,取 list_online_hosts / search_hosts 返回的 id 字段";
props["id"] = idProp;
}
// 公共:组装 {id, ...额外必填} 的 object schema。
static Json::Value FinalizeIdSchema(Json::Value props, std::initializer_list<const char*> extraRequired) {
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("id");
for (const char* r : extraRequired) required.append(r);
schema["required"] = required;
return schema;
}
// 公共:{ ok: boolean } 输出 schema(operate_window / control_service / power_action 共用)。
static Json::Value BuildOkOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value ok(Json::objectValue);
ok["type"] = "boolean";
props["ok"] = ok;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
return schema;
}
Json::Value BuildKillProcessInputSchema() {
Json::Value props(Json::objectValue);
AddIdRequiredProp(props);
Json::Value pidsProp(Json::objectValue);
pidsProp["type"] = "array";
Json::Value pidItems(Json::objectValue);
pidItems["type"] = "integer";
pidsProp["items"] = pidItems;
pidsProp["description"] = u8"要终止的进程 PID 列表(至少 1 个)";
props["pids"] = pidsProp;
return FinalizeIdSchema(props, { "pids" });
}
Json::Value BuildKillProcessOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value remProp(Json::objectValue);
remProp["type"] = "array";
Json::Value remItems(Json::objectValue);
remItems["type"] = "integer";
remProp["items"] = remItems;
remProp["description"] = u8"杀完后仍在运行的 PID(含权限不足未能终止的)";
props["remaining"] = remProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
return schema;
}
Json::Value BuildOperateWindowInputSchema() {
Json::Value props(Json::objectValue);
AddIdRequiredProp(props);
Json::Value hwndProp(Json::objectValue);
hwndProp["type"] = "integer";
hwndProp["description"] = u8"窗口句柄,取 list_windows 返回的 hwnd 字段";
props["hwnd"] = hwndProp;
Json::Value actionProp(Json::objectValue);
actionProp["type"] = "string";
actionProp["description"] = u8"操作:close(关闭) | show(显示) | hide(隐藏) | minimize(最小化) | maximize(最大化) | restore(还原)";
props["action"] = actionProp;
return FinalizeIdSchema(props, { "hwnd", "action" });
}
Json::Value BuildOperateWindowOutputSchema() {
return BuildOkOutputSchema();
}
Json::Value BuildControlServiceInputSchema() {
Json::Value props(Json::objectValue);
AddIdRequiredProp(props);
Json::Value nameProp(Json::objectValue);
nameProp["type"] = "string";
nameProp["description"] = u8"服务名,取 list_services 返回的 service_name 字段";
props["service_name"] = nameProp;
Json::Value actionProp(Json::objectValue);
actionProp["type"] = "string";
actionProp["description"] = u8"操作:start(启动) | stop(停止) | set_auto(设为自动) | set_manual(设为手动)";
props["action"] = actionProp;
return FinalizeIdSchema(props, { "service_name", "action" });
}
Json::Value BuildControlServiceOutputSchema() {
return BuildOkOutputSchema();
}
Json::Value BuildPowerActionInputSchema() {
Json::Value props(Json::objectValue);
AddIdRequiredProp(props);
Json::Value actionProp(Json::objectValue);
actionProp["type"] = "string";
actionProp["description"] = u8"操作:logout(注销) | shutdown(关机) | reboot(重启)";
props["action"] = actionProp;
return FinalizeIdSchema(props, { "action" });
}
Json::Value BuildPowerActionOutputSchema() {
return BuildOkOutputSchema();
}
// list_registry 的 inputSchema(id 必填,path 可选;path 缺省/空 = 列根键) // list_registry 的 inputSchema(id 必填,path 可选;path 缺省/空 = 列根键)
Json::Value BuildListRegistryInputSchema() { Json::Value BuildListRegistryInputSchema() {
Json::Value props(Json::objectValue); Json::Value props(Json::objectValue);
@@ -1163,6 +1286,20 @@ Json::Value BuildUploadFileOutputSchema();
std::string BuildUploadFile(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent); std::string BuildUploadFile(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
std::string BuildRemoteClipboard(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent); std::string BuildRemoteClipboard(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
// ===== P7 前置声明(定义见下方「tools/call 分派」前)=====
Json::Value BuildKillProcessInputSchema();
Json::Value BuildKillProcessOutputSchema();
Json::Value BuildOperateWindowInputSchema();
Json::Value BuildOperateWindowOutputSchema();
Json::Value BuildControlServiceInputSchema();
Json::Value BuildControlServiceOutputSchema();
Json::Value BuildPowerActionInputSchema();
Json::Value BuildPowerActionOutputSchema();
std::string BuildKillProcess(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
std::string BuildOperateWindow(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
std::string BuildControlService(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
std::string BuildPowerAction(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
// tools/list // tools/list
std::string BuildToolsListResult(const Json::Value& id) { std::string BuildToolsListResult(const Json::Value& id) {
Json::Value result(Json::objectValue); Json::Value result(Json::objectValue);
@@ -1433,6 +1570,43 @@ std::string BuildToolsListResult(const Json::Value& id) {
tools.append(tool); tools.append(tool);
} }
// 18) kill_process / operate_window / control_service / power_action(P7:设备管理控制,
// 仅 Windows,安全门:McpManage=1 且 McpReadonly=0)
if (CMcpServer::Instance().IsManageEnabled() && !CMcpServer::Instance().IsReadonly()) {
{
Json::Value tool(Json::objectValue);
tool["name"] = "kill_process";
tool["description"] = u8"终止指定在线 Windows 主机上的一个或多个进程(TerminateProcess 强制结束,不保存进程状态,可能造成数据丢失)。返回 remaining(杀完后仍在运行的 PID)。不可撤销,请谨慎确认 PID。";
tool["inputSchema"] = BuildKillProcessInputSchema();
tool["outputSchema"] = BuildKillProcessOutputSchema();
tools.append(tool);
}
{
Json::Value tool(Json::objectValue);
tool["name"] = "operate_window";
tool["description"] = u8"对指定在线 Windows 主机的窗口执行操作:close(关闭)/show(显示)/hide(隐藏)/minimize(最小化)/maximize(最大化)/restore(还原)。hwnd 取 list_windows 返回的 hwnd。ok 仅表示已下发(窗口句柄可能已失效)。";
tool["inputSchema"] = BuildOperateWindowInputSchema();
tool["outputSchema"] = BuildOperateWindowOutputSchema();
tools.append(tool);
}
{
Json::Value tool(Json::objectValue);
tool["name"] = "control_service";
tool["description"] = u8"控制指定在线 Windows 主机的服务:start(启动)/stop(停止)/set_auto(设为自动)/set_manual(设为手动)。service_name 取 list_services 返回的 service_name。ok 仅表示已下发(成功与否需再查 list_services 核对状态)。";
tool["inputSchema"] = BuildControlServiceInputSchema();
tool["outputSchema"] = BuildControlServiceOutputSchema();
tools.append(tool);
}
{
Json::Value tool(Json::objectValue);
tool["name"] = "power_action";
tool["description"] = u8"对指定在线 Windows 主机执行电源操作:logout(注销)/shutdown(关机)/reboot(重启)。不可撤销,主机将立即下线;需客户端管理员权限。";
tool["inputSchema"] = BuildPowerActionInputSchema();
tool["outputSchema"] = BuildPowerActionOutputSchema();
tools.append(tool);
}
}
result["tools"] = tools; result["tools"] = tools;
return BuildResult(id, result); return BuildResult(id, result);
} }
@@ -4175,6 +4349,332 @@ std::string BuildUploadFile(const Json::Value& id, const Json::Value& args, CMy2
return BuildError(id, -32001, msg); return BuildError(id, -32001, msg);
} }
// ========== 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) {
uint64_t devId = 0;
std::string err;
if (!ParseHostIdArg(args, devId, err))
return BuildError(id, -32602, err);
context* ctx = FindMainContext(parent, devId);
if (!ctx)
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
if (clientType == "LNX" || clientType == "MAC" || clientType == "APK")
return BuildError(id, -32005, "kill_process is only supported on Windows hosts");
CMcpServer& mcp = CMcpServer::Instance();
if (mcp.IsReadonly())
return BuildError(id, -32006, "kill_process is disabled: MCP is in read-only mode (set McpReadonly=0 to enable)");
if (!mcp.IsManageEnabled())
return BuildError(id, -32006, "kill_process is disabled: management operations are off (set McpManage=1 to enable)");
const Json::Value& pids = args["pids"];
if (!pids.isArray() || pids.size() == 0)
return BuildError(id, -32602, "Missing or invalid parameter: pids (non-empty integer array)");
std::vector<uint64_t> pidList;
std::vector<BYTE> followUp;
followUp.push_back((BYTE)COMMAND_KILLPROCESS);
for (unsigned int i = 0; i < pids.size(); ++i) {
uint64_t pid = 0;
if (!ReadUInt64(pids[i], pid) || pid == 0)
return BuildError(id, -32602, "pids must be non-zero integers");
pidList.push_back(pid);
DWORD d = (DWORD)pid;
followUp.push_back((BYTE)(d & 0xFF));
followUp.push_back((BYTE)((d >> 8) & 0xFF));
followUp.push_back((BYTE)((d >> 16) & 0xFF));
followUp.push_back((BYTE)((d >> 24) & 0xFF));
}
if (!mcp.BeginPendingFollowUp(devId, "kill_process", followUp))
return BuildError(id, -32003, "Device busy: another request is pending for this host");
BYTE cmd = COMMAND_SYSTEM;
if (!ctx->Send2Client(&cmd, 1)) {
mcp.ClearPending(devId);
return BuildError(id, -32004, "Failed to send command to host");
}
std::vector<BYTE> data;
if (!mcp.WaitPendingWrite(devId, data, kMcpToolTimeoutMs))
return BuildError(id, -32001, "Timeout waiting for process list after kill");
UINT cp = (clientType == "LNX" || clientType == "MAC") ? CP_UTF8 : 936;
Json::Value procs = ParseProcessList(data, cp);
Json::Value remaining(Json::arrayValue);
std::string detail = "pids=";
for (size_t i = 0; i < pidList.size(); ++i) {
if (i) detail += ",";
detail += std::to_string(pidList[i]);
bool stillAlive = false;
for (unsigned int j = 0; j < procs.size(); ++j) {
if (procs[j]["pid"].asUInt64() == pidList[i]) { stillAlive = true; break; }
}
if (stillAlive) remaining.append((Json::UInt64)pidList[i]);
}
AuditManageAction(parent, devId, "kill_process", detail);
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["remaining"] = remaining;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = remaining.empty()
? std::string(u8"目标进程已全部终止。")
: std::string(u8"仍有 ") + std::to_string((int)remaining.size()) + std::string(u8" 个进程存活(见 remaining)。");
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// operate_window:主连接 COMMAND_WSLIST → 窗口子连接 TOKEN_WSLIST(就绪)→ CMD_WINDOW_CLOSE
// (close 客户端自动回 TOKEN_WSLIST)或 CMD_WINDOW_TEST(test 补发 COMMAND_WSLIST 索取)。
// 20 字节零填充 buffer 复刻 SystemDlg::OnWlistClose/OnWlistHide/Recover/Max/Min(hwnd 为 DWORD)。
std::string BuildOperateWindow(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
uint64_t devId = 0;
std::string err;
if (!ParseHostIdArg(args, devId, err))
return BuildError(id, -32602, err);
context* ctx = FindMainContext(parent, devId);
if (!ctx)
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
if (clientType == "LNX" || clientType == "MAC" || clientType == "APK")
return BuildError(id, -32005, "operate_window is only supported on Windows hosts");
CMcpServer& mcp = CMcpServer::Instance();
if (mcp.IsReadonly())
return BuildError(id, -32006, "operate_window is disabled: MCP is in read-only mode (set McpReadonly=0 to enable)");
if (!mcp.IsManageEnabled())
return BuildError(id, -32006, "operate_window is disabled: management operations are off (set McpManage=1 to enable)");
uint64_t hwnd = 0;
if (!ReadUInt64Arg(args, "hwnd", hwnd) || hwnd == 0)
return BuildError(id, -32602, "Missing or invalid parameter: hwnd (non-zero window handle)");
std::string action = Trim(GetStringArg(args, "action"));
BYTE lpMsgBuf[20] = { 0 };
DWORD h = (DWORD)hwnd;
if (action == "close") {
lpMsgBuf[0] = CMD_WINDOW_CLOSE;
memcpy(lpMsgBuf + 1, &h, sizeof(DWORD));
} else {
DWORD dHow;
if (action == "hide") dHow = SW_HIDE; // 0
else if (action == "show") dHow = SW_NORMAL; // 1
else if (action == "restore") dHow = SW_NORMAL; // 1
else if (action == "maximize") dHow = SW_MAXIMIZE; // 3
else if (action == "minimize") dHow = SW_MINIMIZE; // 6
else return BuildError(id, -32602,
"Invalid action: " + action + " (expect close|show|hide|minimize|maximize|restore)");
lpMsgBuf[0] = CMD_WINDOW_TEST;
memcpy(lpMsgBuf + 1, &h, sizeof(DWORD));
memcpy(lpMsgBuf + 1 + sizeof(DWORD), &dHow, sizeof(DWORD));
}
std::vector<BYTE> followUp(lpMsgBuf, lpMsgBuf + sizeof(lpMsgBuf));
if (!mcp.BeginPendingFollowUp(devId, "operate_window", followUp))
return BuildError(id, -32003, "Device busy: another request is pending for this host");
BYTE cmd = COMMAND_WSLIST;
if (!ctx->Send2Client(&cmd, 1)) {
mcp.ClearPending(devId);
return BuildError(id, -32004, "Failed to send command to host");
}
std::vector<BYTE> data;
if (!mcp.WaitPendingWrite(devId, data, kMcpToolTimeoutMs))
return BuildError(id, -32001, "Timeout waiting for window operation");
AuditManageAction(parent, devId, "operate_window",
"action=" + action + " hwnd=" + std::to_string(hwnd));
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["ok"] = true;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"已下发窗口操作:") + action;
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// control_service:主连接 COMMAND_SERVICES → 服务子连接 TOKEN_SERVERLIST(就绪)→
// COMMAND_SERVICECONFIG → 客户端写后自动 SendServicesList(确认,无需补发)。
std::string BuildControlService(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
uint64_t devId = 0;
std::string err;
if (!ParseHostIdArg(args, devId, err))
return BuildError(id, -32602, err);
context* ctx = FindMainContext(parent, devId);
if (!ctx)
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
if (clientType == "LNX" || clientType == "MAC" || clientType == "APK")
return BuildError(id, -32005, "control_service is only supported on Windows hosts");
CMcpServer& mcp = CMcpServer::Instance();
if (mcp.IsReadonly())
return BuildError(id, -32006, "control_service is disabled: MCP is in read-only mode (set McpReadonly=0 to enable)");
if (!mcp.IsManageEnabled())
return BuildError(id, -32006, "control_service is disabled: management operations are off (set McpManage=1 to enable)");
std::string serviceName = Trim(GetStringArg(args, "service_name"));
if (serviceName.empty())
return BuildError(id, -32602, "Missing required parameter: service_name");
std::string action = Trim(GetStringArg(args, "action"));
BYTE bCmd;
if (action == "start") bCmd = 1;
else if (action == "stop") bCmd = 2;
else if (action == "set_auto") bCmd = 3;
else if (action == "set_manual") bCmd = 4;
else return BuildError(id, -32602,
"Invalid action: " + action + " (expect start|stop|set_auto|set_manual)");
// 服务名来自 Windows A 接口(GBK/936),下发前转 ANSI。payload 与 MachineDlg 一致:
// [COMMAND_SERVICECONFIG:1][bCmd:1][serviceName\0]。
std::string ansiName = ToAnsi(serviceName, 936);
std::vector<BYTE> followUp;
followUp.reserve(2 + ansiName.size() + 1);
followUp.push_back((BYTE)COMMAND_SERVICECONFIG);
followUp.push_back(bCmd);
followUp.insert(followUp.end(), ansiName.begin(), ansiName.end());
followUp.push_back(0);
if (!mcp.BeginPendingFollowUp(devId, "control_service", followUp))
return BuildError(id, -32003, "Device busy: another request is pending for this host");
BYTE cmd = COMMAND_SERVICES;
if (!ctx->Send2Client(&cmd, 1)) {
mcp.ClearPending(devId);
return BuildError(id, -32004, "Failed to send command to host");
}
std::vector<BYTE> data;
if (!mcp.WaitPendingWrite(devId, data, kMcpToolTimeoutMs))
return BuildError(id, -32001, "Timeout waiting for service operation");
AuditManageAction(parent, devId, "control_service",
"service=" + serviceName + " action=" + action);
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["ok"] = true;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"已下发服务操作:") + action + std::string(u8" ") + serviceName;
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// power_action:主连接 TOKEN_MACHINE_MANAGE,发完即走(无响应);主机立即离线。
std::string BuildPowerAction(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
uint64_t devId = 0;
std::string err;
if (!ParseHostIdArg(args, devId, err))
return BuildError(id, -32602, err);
context* ctx = FindMainContext(parent, devId);
if (!ctx)
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
if (clientType == "LNX" || clientType == "MAC" || clientType == "APK")
return BuildError(id, -32005, "power_action is only supported on Windows hosts");
CMcpServer& mcp = CMcpServer::Instance();
if (mcp.IsReadonly())
return BuildError(id, -32006, "power_action is disabled: MCP is in read-only mode (set McpReadonly=0 to enable)");
if (!mcp.IsManageEnabled())
return BuildError(id, -32006, "power_action is disabled: management operations are off (set McpManage=1 to enable)");
std::string action = Trim(GetStringArg(args, "action"));
BYTE type;
if (action == "logout") type = MACHINE_LOGOUT;
else if (action == "shutdown") type = MACHINE_SHUTDOWN;
else if (action == "reboot") type = MACHINE_REBOOT;
else return BuildError(id, -32602,
"Invalid action: " + action + " (expect logout|shutdown|reboot)");
// 主连接、发完即走、无响应(客户端 KernelManager 只校验 ulLength>1 并读 szBuffer[1])。
BYTE token[2] = { (BYTE)TOKEN_MACHINE_MANAGE, type };
if (!ctx->Send2Client(token, 2))
return BuildError(id, -32004, "Failed to send command to host");
AuditManageAction(parent, devId, "power_action", "action=" + action);
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["ok"] = true;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"已下发电源操作:") + action;
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// tools/call 分派 // tools/call 分派
std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) { std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
const Json::Value& id = root["id"]; const Json::Value& id = root["id"];
@@ -4210,6 +4710,10 @@ std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
if (toolName == "remote_clipboard") return BuildRemoteClipboard(id, args, parent); if (toolName == "remote_clipboard") return BuildRemoteClipboard(id, args, parent);
if (toolName == "download_file") return BuildDownloadFile(id, args, parent); if (toolName == "download_file") return BuildDownloadFile(id, args, parent);
if (toolName == "upload_file") return BuildUploadFile(id, args, parent); if (toolName == "upload_file") return BuildUploadFile(id, args, parent);
if (toolName == "kill_process") return BuildKillProcess(id, args, parent);
if (toolName == "operate_window") return BuildOperateWindow(id, args, parent);
if (toolName == "control_service") return BuildControlService(id, args, parent);
if (toolName == "power_action") return BuildPowerAction(id, args, parent);
return BuildError(id, -32602, return BuildError(id, -32602,
"Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName)); "Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName));
@@ -4727,7 +5231,8 @@ bool CMcpServer::IsPending(uint64_t device_id) {
void CMcpServer::TakeMainResponse(uint64_t device_id, const BYTE* data, ULONG len) { void CMcpServer::TakeMainResponse(uint64_t device_id, const BYTE* data, ULONG len) {
std::lock_guard<std::mutex> lk(m_PendingMutex); std::lock_guard<std::mutex> lk(m_PendingMutex);
auto it = m_Pending.find(device_id); auto it = m_Pending.find(device_id);
if (it == m_Pending.end()) return; // 已超时清理 → 迟到数据,丢弃 if (it == m_Pending.end()) { Mprintf("[MCP-P7] TakeMainResponse dev=%llu len=%d: no pending (late/dropped)\n", device_id, (int)len); return; } // 已超时清理 → 迟到数据,丢弃
Mprintf("[MCP-P7] TakeMainResponse dev=%llu len=%d done\n", device_id, (int)len);
it->second.data.assign(data, data + len); it->second.data.assign(data, data + len);
it->second.done = true; it->second.done = true;
m_PendingCv.notify_one(); m_PendingCv.notify_one();
@@ -4902,6 +5407,110 @@ bool CMcpServer::WaitPendingRegistry(uint64_t device_id, std::vector<BYTE>& subk
return true; return true;
} }
// ===== P7:设备管理控制(写工具接管)=====
bool CMcpServer::BeginPendingFollowUp(uint64_t device_id, const std::string& tool,
const std::vector<BYTE>& followUp) {
if (IsFileTransferPending(device_id)) return false; // 该 host 有文件传输会话在飞(互斥,见 F2)
std::lock_guard<std::mutex> lk(m_PendingMutex);
if (m_Pending.find(device_id) != m_Pending.end()) return false; // 设备忙
PendingRequest r;
r.tool = tool;
r.followUp = followUp;
m_Pending[device_id] = std::move(r);
return true;
}
bool CMcpServer::OnSystemReady(uint64_t device_id, context* subCtx) {
std::string tool;
std::vector<BYTE> followUp;
{
std::lock_guard<std::mutex> lk(m_PendingMutex);
auto it = m_Pending.find(device_id);
if (it == m_Pending.end()) { Mprintf("[MCP-P7] OnSystemReady dev=%llu: no pending\n", device_id); return false; } // 无挂起 → 回落 MFC
tool = it->second.tool;
if (tool != "kill_process" && tool != "operate_window") { Mprintf("[MCP-P7] OnSystemReady dev=%llu tool=%s: not write tool\n", device_id, tool.c_str()); return false; }
if (it->second.followUpSent) { Mprintf("[MCP-P7] OnSystemReady dev=%llu tool=%s: CONFIRM token -> TakeMainResponse\n", device_id, tool.c_str()); return false; } // 确认包 → TakeMainResponse
followUp = it->second.followUp;
it->second.subCtx = subCtx;
it->second.followUpSent = true;
}
// 锁外下发写命令;kill/test 需补发刷新命令索取新列表(close/control_service 客户端自动回)。
BOOL b1 = subCtx->Send2Client(followUp.data(), (ULONG)followUp.size());
BYTE refresh = 0;
if (tool == "kill_process") {
refresh = (BYTE)COMMAND_PSLIST;
} else if (!followUp.empty() && followUp[0] == CMD_WINDOW_TEST) {
refresh = (BYTE)COMMAND_WSLIST;
}
if (refresh) {
// followUp 与 refresh 是两次独立 Send2Client,各自 PostQueuedCompletionStatus(IOWrite),
// worker 会对同一 OutCompressedBuffer 并发发起 WSASend,造成竞态/丢包。用 WaitSendIdle
// 确定性等首个包真正发完再发刷新包(比 MFC 的 Sleep(100) 盲等更稳)。
subCtx->WaitSendIdle(2000);
BOOL b2 = subCtx->Send2Client(&refresh, 1);
Mprintf("[MCP-P7] OnSystemReady dev=%llu tool=%s READY: followUpSend=%d refreshSend=%d refresh=%d\n",
device_id, tool.c_str(), b1 ? 1 : 0, b2 ? 1 : 0, (int)refresh);
}
return true; // 已接管,不 CancelIO
}
bool CMcpServer::OnServiceReady(uint64_t device_id, context* subCtx) {
std::vector<BYTE> followUp;
{
std::lock_guard<std::mutex> lk(m_PendingMutex);
auto it = m_Pending.find(device_id);
if (it == m_Pending.end()) return false;
if (it->second.tool != "control_service") return false;
if (it->second.followUpSent) return false; // 确认包 → 调用方 TakeMainResponse
followUp = it->second.followUp;
it->second.subCtx = subCtx;
it->second.followUpSent = true;
}
subCtx->Send2Client(followUp.data(), (ULONG)followUp.size());
return true; // 客户端写后自动回,无需补发
}
bool CMcpServer::WaitPendingWrite(uint64_t device_id, std::vector<BYTE>& out, int timeoutMs) {
context* subCtx = nullptr;
std::unique_lock<std::mutex> lk(m_PendingMutex);
auto it = m_Pending.find(device_id);
if (it == m_Pending.end()) return false;
bool signaled = m_PendingCv.wait_for(lk, std::chrono::milliseconds(timeoutMs),
[&] { return it->second.done; });
if (!signaled || it->second.data.empty()) {
subCtx = it->second.subCtx;
Mprintf("[MCP-P7] WaitPendingWrite dev=%llu TIMEOUT: signaled=%d dataEmpty=%d subCtx=%p\n",
device_id, signaled ? 1 : 0, it->second.data.empty() ? 1 : 0, (void*)subCtx);
m_Pending.erase(it); // 超时/空数据 → 清理
lk.unlock();
if (subCtx) subCtx->CancelIO(); // 锁外关子连接,防孤儿子连接
return false;
}
out = std::move(it->second.data);
Mprintf("[MCP-P7] WaitPendingWrite dev=%llu OK len=%d\n", device_id, (int)out.size());
m_Pending.erase(it);
return true;
}
void CMcpServer::OnManageSubClosed(context* subCtx) {
std::lock_guard<std::mutex> lk(m_PendingMutex);
// 只匹配「已接管、等确认包」的写工具子连接(read 工具从不存 subCtx,天然不命中)。
// 扫描而非按 device_id 查找,避免依赖断线后 GetClientID 是否仍有效。
for (auto& kv : m_Pending) {
if (kv.second.subCtx == subCtx) {
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
kv.second.done = true; // 唤醒等待线程(WaitPendingWrite 见 data 空 → 返回失败)
m_PendingCv.notify_all();
break;
}
}
}
// rand_s:Windows CRT 加密安全随机源(基于系统 CSPRNG)。其声明需在 <stdlib.h> 前 // rand_s:Windows CRT 加密安全随机源(基于系统 CSPRNG)。其声明需在 <stdlib.h> 前
// 定义 _CRT_RAND_S;为避免依赖 PCH 的包含顺序,这里手动声明其导出原型(errno_t == int)。 // 定义 _CRT_RAND_S;为避免依赖 PCH 的包含顺序,这里手动声明其导出原型(errno_t == int)。
extern "C" int __cdecl rand_s(unsigned int* randomValue); extern "C" int __cdecl rand_s(unsigned int* randomValue);
+33
View File
@@ -242,6 +242,34 @@ public:
// 收尾:擦会话 + 锁外 CancelIO 文件管理器/流式两条子链接(幂等)。 // 收尾:擦会话 + 锁外 CancelIO 文件管理器/流式两条子链接(幂等)。
void ClearFileTransfer(uint64_t device_id); void ClearFileTransfer(uint64_t device_id);
// ===== P7:设备管理控制(kill_process / operate_window / control_service / power_action)=====
// 写工具复用 m_Pending 单飞行;followUp/followUpSent/subCtx 标记接管进度。
// 「就绪 token」与「确认 token」是同一 token(客户端子连接构造即回一包,写后再回一包),
// 故必须用 followUpSent 区分两次到达,不能靠 token 区分(与 list_registry 的 PATH/KEY 双 token 不同)。
void SetManageEnabled(bool enabled) { m_manageEnabled = enabled; }
bool IsManageEnabled() const { return m_manageEnabled; }
// 登记写挂起:与 BeginPending 同单飞行,另预填 followUp(写命令 payload)。
bool BeginPendingFollowUp(uint64_t device_id, const std::string& tool,
const std::vector<BYTE>& followUp);
// MessageHandle(TOKEN_PSLIST/TOKEN_WSLIST) 调用:命中且为写工具(kill_process/operate_window)
// 则接管子连接、下发 followUp(返回 true=已接管,调用方不 CancelIO);否则 false(回落 MFC
// 或按确认包走 TakeMainResponse)。
bool OnSystemReady(uint64_t device_id, context* subCtx);
// MessageHandle(TOKEN_SERVERLIST) 调用:命中且为 control_service 则接管、下发
// COMMAND_SERVICECONFIG(返回 true=已接管);否则 false。
bool OnServiceReady(uint64_t device_id, context* subCtx);
// 工具线程:等待写确认包(第二个同 token 包);超时/空数据清挂起并 CancelIO 子连接。
bool WaitPendingWrite(uint64_t device_id, std::vector<BYTE>& out, int timeoutMs);
// OfflineProc 调用:接管的子连接断开时清挂起(置 done 并清 subCtx,唤醒等待线程)。
// 不擦表(由 WaitPendingWrite 持有迭代器时自行擦除,防迭代器失效);清 subCtx 使
// 超时路径不会对已回收(连接池复用)的 context 误 CancelIO。
void OnManageSubClosed(context* subCtx);
// 安全配置(启动时由 CMy2015RemoteDlg 读 THIS_CFG 后设置)。 // 安全配置(启动时由 CMy2015RemoteDlg 读 THIS_CFG 后设置)。
void SetReadonly(bool readonly) { m_readonly = readonly; } void SetReadonly(bool readonly) { m_readonly = readonly; }
void SetCmdWhitelist(const std::string& whitelist) { m_cmdWhitelist = whitelist; } void SetCmdWhitelist(const std::string& whitelist) { m_cmdWhitelist = whitelist; }
@@ -274,6 +302,10 @@ private:
bool pathDone = false; // list_registry:已收到 TOKEN_REG_PATH bool pathDone = false; // list_registry:已收到 TOKEN_REG_PATH
bool keyDone = false; // list_registry:已收到 TOKEN_REG_KEY bool keyDone = false; // list_registry:已收到 TOKEN_REG_KEY
bool done = false; bool done = false;
// ===== P7:写工具(kill_process / operate_window / control_service)接管进度 =====
std::vector<BYTE> followUp; // 接管后下发的写命令 payload(首字节 = 写命令)
bool followUpSent = false; // false=等子连接就绪(首包);true=已发写命令、等确认包
context* subCtx = nullptr; // 接管的子连接(超时/失败 CancelIO 用,防孤儿子连接)
}; };
std::mutex m_PendingMutex; std::mutex m_PendingMutex;
std::condition_variable m_PendingCv; std::condition_variable m_PendingCv;
@@ -348,6 +380,7 @@ private:
bool m_terminalEnabled = false; // 持久终端开关(默认关;要求 m_readonly=false) bool m_terminalEnabled = false; // 持久终端开关(默认关;要求 m_readonly=false)
bool m_remoteControlEnabled = false; // 远程控制开关(默认关;要求 m_readonly=false) bool m_remoteControlEnabled = false; // 远程控制开关(默认关;要求 m_readonly=false)
bool m_fileTransferEnabled = false; // 文件传输开关(默认关;download_file) bool m_fileTransferEnabled = false; // 文件传输开关(默认关;download_file)
bool m_manageEnabled = false; // P7 管理控制开关(默认关;要求 m_readonly=false)
}; };
// 全局访问器(仿 WebService(),见 WebService.h 末尾) // 全局访问器(仿 WebService(),见 WebService.h 末尾)
+13 -2
View File
@@ -145,6 +145,9 @@ BOOL CMcpSettingsDlg::OnInitDialog()
m_btnFileTransfer.Create(_TR("启用文件传输(download_file)"), m_btnFileTransfer.Create(_TR("启用文件传输(download_file)"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX, WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
r0, this, IDC_MCP_FILETRANSFER); r0, this, IDC_MCP_FILETRANSFER);
m_btnManage.Create(_TR("启用管理操作(进程/窗口/服务/电源)"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
r0, this, IDC_MCP_MANAGE);
m_lblWhitelist.Create(_TR("命令白名单"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1); m_lblWhitelist.Create(_TR("命令白名单"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1);
m_editWhitelist.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | m_editWhitelist.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP |
ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN | WS_VSCROLL, ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN | WS_VSCROLL,
@@ -168,6 +171,7 @@ BOOL CMcpSettingsDlg::OnInitDialog()
m_btnTerminal.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0)); m_btnTerminal.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnRemoteControl.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0)); m_btnRemoteControl.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnFileTransfer.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_lblWhitelist.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_editWhitelist.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnOK.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0)); m_btnOK.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
@@ -184,6 +188,7 @@ BOOL CMcpSettingsDlg::OnInitDialog()
int terminal = THIS_CFG.GetInt("settings", "McpTerminal", 0); int terminal = THIS_CFG.GetInt("settings", "McpTerminal", 0);
int remoteControl = THIS_CFG.GetInt("settings", "McpRemoteControl", 0); int remoteControl = THIS_CFG.GetInt("settings", "McpRemoteControl", 0);
int fileTransfer = THIS_CFG.GetInt("settings", "McpFileTransfer", 0); int fileTransfer = THIS_CFG.GetInt("settings", "McpFileTransfer", 0);
int manage = THIS_CFG.GetInt("settings", "McpManage", 0);
std::string whitelist = THIS_CFG.GetStr("settings", "McpCmdWhitelist", ""); std::string whitelist = THIS_CFG.GetStr("settings", "McpCmdWhitelist", "");
m_btnEnable.SetCheck(enabled ? BST_CHECKED : BST_UNCHECKED); m_btnEnable.SetCheck(enabled ? BST_CHECKED : BST_UNCHECKED);
@@ -194,6 +199,7 @@ BOOL CMcpSettingsDlg::OnInitDialog()
m_btnTerminal.SetCheck(terminal ? BST_CHECKED : BST_UNCHECKED); m_btnTerminal.SetCheck(terminal ? BST_CHECKED : BST_UNCHECKED);
m_btnRemoteControl.SetCheck(remoteControl ? BST_CHECKED : BST_UNCHECKED); m_btnRemoteControl.SetCheck(remoteControl ? BST_CHECKED : BST_UNCHECKED);
m_btnFileTransfer.SetCheck(fileTransfer ? BST_CHECKED : BST_UNCHECKED); m_btnFileTransfer.SetCheck(fileTransfer ? BST_CHECKED : BST_UNCHECKED);
m_btnManage.SetCheck(manage ? BST_CHECKED : BST_UNCHECKED);
// 白名单存储为逗号分隔,展示为每行一条。 // 白名单存储为逗号分隔,展示为每行一条。
m_editWhitelist.SetWindowText(CString(WhitelistForDisplay(whitelist).c_str())); m_editWhitelist.SetWindowText(CString(WhitelistForDisplay(whitelist).c_str()));
@@ -213,6 +219,7 @@ void CMcpSettingsDlg::OnOK()
bool terminal = (m_btnTerminal.GetCheck() == BST_CHECKED); bool terminal = (m_btnTerminal.GetCheck() == BST_CHECKED);
bool remoteControl = (m_btnRemoteControl.GetCheck() == BST_CHECKED); bool remoteControl = (m_btnRemoteControl.GetCheck() == BST_CHECKED);
bool fileTransfer = (m_btnFileTransfer.GetCheck() == BST_CHECKED); bool fileTransfer = (m_btnFileTransfer.GetCheck() == BST_CHECKED);
bool manage = (m_btnManage.GetCheck() == BST_CHECKED);
// 端口校验:1-65535 // 端口校验:1-65535
int port = atoi(CT2A(sPort)); int port = atoi(CT2A(sPort));
@@ -239,14 +246,15 @@ void CMcpSettingsDlg::OnOK()
THIS_CFG.SetInt("settings", "McpTerminal", terminal ? 1 : 0); THIS_CFG.SetInt("settings", "McpTerminal", terminal ? 1 : 0);
THIS_CFG.SetInt("settings", "McpRemoteControl", remoteControl ? 1 : 0); THIS_CFG.SetInt("settings", "McpRemoteControl", remoteControl ? 1 : 0);
THIS_CFG.SetInt("settings", "McpFileTransfer", fileTransfer ? 1 : 0); THIS_CFG.SetInt("settings", "McpFileTransfer", fileTransfer ? 1 : 0);
THIS_CFG.SetInt("settings", "McpManage", manage ? 1 : 0);
std::string whitelist = CT2A(sWhitelist); std::string whitelist = CT2A(sWhitelist);
whitelist = NormalizeWhitelist(whitelist); whitelist = NormalizeWhitelist(whitelist);
THIS_CFG.SetStr("settings", "McpCmdWhitelist", whitelist); THIS_CFG.SetStr("settings", "McpCmdWhitelist", whitelist);
// 拆成两段可翻译的单行键,中间用 \r\n 连接(多行键无法在 INI 中表示) // 拆成两段可翻译的单行键,中间用 \r\n 连接(多行键无法在 INI 中表示)
MessageBox(_TR("MCP 设置已保存。") + _T("\r\n") + MessageBox(_TR("MCP 设置已保存。") + _T("\r\n") +
_TR("启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输的改动需重启程序生效。") + _T("\r\n") + _TR("启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。") + _T("\r\n") +
_TR("持久终端与远程控制仅在只读模式关闭时生效。"), _TR("持久终端、远程控制与管理操作仅在只读模式关闭时生效。"),
_TR("提示"), MB_ICONINFORMATION); _TR("提示"), MB_ICONINFORMATION);
CDialog::OnOK(); CDialog::OnOK();
@@ -289,6 +297,9 @@ void CMcpSettingsDlg::LayoutControls(int cx, int cy)
m_btnFileTransfer.MoveWindow(margin, y, cx - margin * 2, 22); m_btnFileTransfer.MoveWindow(margin, y, cx - margin * 2, 22);
y += 30; y += 30;
m_btnManage.MoveWindow(margin, y, cx - margin * 2, 22);
y += 30;
const int whitelistH = 90; const int whitelistH = 90;
m_lblWhitelist.MoveWindow(margin, y, labelW, rowH); m_lblWhitelist.MoveWindow(margin, y, labelW, rowH);
m_editWhitelist.MoveWindow(margin + labelW, y - 2, cx - margin * 2 - labelW, whitelistH); m_editWhitelist.MoveWindow(margin + labelW, y - 2, cx - margin * 2 - labelW, whitelistH);
+2
View File
@@ -31,6 +31,7 @@ private:
IDC_MCP_TERMINAL = 1007, // 「启用持久终端」复选框(全命令,无白名单,要求只读关) IDC_MCP_TERMINAL = 1007, // 「启用持久终端」复选框(全命令,无白名单,要求只读关)
IDC_MCP_REMOTECONTROL = 1008, // 「启用远程控制」复选框(AI 操控桌面,要求只读关) IDC_MCP_REMOTECONTROL = 1008, // 「启用远程控制」复选框(AI 操控桌面,要求只读关)
IDC_MCP_FILETRANSFER = 1009, // 「启用文件传输」复选框(download_file,不要求只读关) IDC_MCP_FILETRANSFER = 1009, // 「启用文件传输」复选框(download_file,不要求只读关)
IDC_MCP_MANAGE = 1010, // 「启用管理操作」复选框(kill/窗口/服务/电源,要求只读关)
}; };
CButton m_btnEnable; CButton m_btnEnable;
@@ -40,6 +41,7 @@ private:
CButton m_btnTerminal; CButton m_btnTerminal;
CButton m_btnRemoteControl; CButton m_btnRemoteControl;
CButton m_btnFileTransfer; CButton m_btnFileTransfer;
CButton m_btnManage;
CStatic m_lblWhitelist; CStatic m_lblWhitelist;
CEdit m_editWhitelist; CEdit m_editWhitelist;
CButton m_btnOK, m_btnCancel; CButton m_btnOK, m_btnCancel;
+20 -1
View File
@@ -112,7 +112,12 @@ protected:
m_bShouldUnmask = TRUE; m_bShouldUnmask = TRUE;
std::string clientIP = getXForwardedFor(str); std::string clientIP = getXForwardedFor(str);
if (!clientIP.empty()) peer = clientIP; if (!clientIP.empty()) peer = clientIP;
} else { } else if (m_bShouldUnmask != TRUE) {
// 仅当连接尚未被判定为 HTTP(初始 -1,或已确认非 HTTP)时才置为 FALSE。
// 否则会误伤 HTTP 连接的分片续包:续包到达时读位置已越过掩码头,
// TryUnMask 找不到 "POST "(maskType==None),若在这里把 m_bShouldUnmask
// 从 TRUE 改回 FALSE,下一个新包(仍是 HTTP 掩码)就不再解掩码,从而被 Parse
// 判为 PARSER_FAILED。这正是「kill 后进程列表确认包到不了 NotifyProc」的根因。
m_bShouldUnmask = FALSE; m_bShouldUnmask = FALSE;
} }
if (nullptr == m_Masker) { if (nullptr == m_Masker) {
@@ -568,6 +573,20 @@ public:
} }
return FALSE; return FALSE;
} }
// 等待 OutCompressedBuffer 排空。OnClientPostSending 在最后一个 WSASend 完成后
// ClearBuffer 使有效长度归零,此处轮询该信号,避免背靠背 Send2Client 对同一缓冲
// 并发 WSASend(MCP OnSystemReady 发写命令后补发刷新命令时调用)。
bool WaitSendIdle(int timeoutMs) override
{
ULONGLONG deadline = (timeoutMs > 0) ? (GetTickCount64() + (ULONGLONG)timeoutMs) : 0;
while (OutCompressedBuffer.GetBufferLength() != 0) {
if (timeoutMs > 0 && GetTickCount64() >= deadline) {
return false;
}
Sleep(1);
}
return true;
}
VOID SetClientInfo(const CString(&s)[ONLINELIST_MAX], const std::vector<std::string>& a = {}) VOID SetClientInfo(const CString(&s)[ONLINELIST_MAX], const std::vector<std::string>& a = {})
{ {
for (int i = 0; i < ONLINELIST_MAX; i++) { for (int i = 0; i < ONLINELIST_MAX; i++) {
+10
View File
@@ -55,6 +55,16 @@ public:
} }
virtual void SetGroupName(const std::string& group) {} virtual void SetGroupName(const std::string& group) {}
// 等待本连接待发送缓冲排空(用于「发写命令 → 补发刷新命令」的背靠背发送:
// 两次 Send2Client 会对同一发送缓冲并发 WSASend,造成竞态/丢包)。非 IOCP
// 服务端无异步发送缓冲,默认立即返回 true;CONTEXT_OBJECT 覆写为轮询发送缓冲
// 归零直到排空或超时。
virtual bool WaitSendIdle(int timeoutMs)
{
(void)timeoutMs;
return true;
}
// 检查客户端是否支持 V2 文件传输 // 检查客户端是否支持 V2 文件传输
bool SupportsFileV2() const { bool SupportsFileV2() const {
CString caps = GetClientData(ONLINELIST_CAPABILITIES); CString caps = GetClientData(ONLINELIST_CAPABILITIES);
+4 -2
View File
@@ -1989,18 +1989,20 @@ MCP
端口需为 1-65535 的数字=Port muss eine Zahl zwischen 1 und 65535 sein 端口需为 1-65535 的数字=Port muss eine Zahl zwischen 1 und 65535 sein
Token 不能为空=Token darf nicht leer sein Token 不能为空=Token darf nicht leer sein
MCP 设置已保存。=MCP-Einstellungen gespeichert. MCP 设置已保存。=MCP-Einstellungen gespeichert.
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输的改动需重启程序生效。=Aenderungen an Aktivierung/Port/Bindeadresse/Token/Nur-Lesen/Whitelist/persistentem Terminal/Fernsteuerung/Dateiuebertragung werden nach Neustart wirksam. 启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。=Aenderungen an Aktivierung/Port/Bindeadresse/Token/Nur-Lesen/Whitelist/persistentem Terminal/Fernsteuerung/Dateiuebertragung/Verwaltungsoperationen werden nach Neustart wirksam.
只读模式(禁命令执行)=Nur-Lesen-Modus (Befehlsausfuehrung deaktiviert) 只读模式(禁命令执行)=Nur-Lesen-Modus (Befehlsausfuehrung deaktiviert)
命令白名单=Befehls-Whitelist 命令白名单=Befehls-Whitelist
MCP命令执行=MCP-Befehlsausfuehrung MCP命令执行=MCP-Befehlsausfuehrung
启用持久终端(全命令,无白名单)=Persistentes Terminal aktivieren (volle Befehle, keine Whitelist) 启用持久终端(全命令,无白名单)=Persistentes Terminal aktivieren (volle Befehle, keine Whitelist)
持久终端与远程控制仅在只读模式关闭时生效。=Persistentes Terminal und Fernsteuerung werden nur wirksam, wenn der Nur-Lesen-Modus aus ist. 持久终端、远程控制与管理操作仅在只读模式关闭时生效。=Persistentes Terminal, Fernsteuerung und Verwaltungsoperationen werden nur wirksam, wenn der Nur-Lesen-Modus aus ist.
MCP持久终端=MCP-persistentes Terminal MCP持久终端=MCP-persistentes Terminal
MCP远程控制=MCP-Fernsteuerung MCP远程控制=MCP-Fernsteuerung
MCP文件传输=MCP-Dateiuebertragung MCP文件传输=MCP-Dateiuebertragung
MCP管理操作=MCP-Verwaltungsoperation
Token=Token Token=Token
启用远程控制(AI 操控桌面)=Fernsteuerung aktivieren (KI steuert den Desktop) 启用远程控制(AI 操控桌面)=Fernsteuerung aktivieren (KI steuert den Desktop)
启用文件传输(download_file)=Dateiuebertragung aktivieren (download_file) 启用文件传输(download_file)=Dateiuebertragung aktivieren (download_file)
启用管理操作(进程/窗口/服务/电源)=Verwaltungsoperationen aktivieren (Prozess/Fenster/Dienst/Energie)
第 %d 个钱包地址不合法!=Wallet-Adresse #%d ist ungueltig! 第 %d 个钱包地址不合法!=Wallet-Adresse #%d ist ungueltig!
%d 新连接\r\n=%d Neue Verbindung\r\n %d 新连接\r\n=%d Neue Verbindung\r\n
%d 本地连接断开\r\n=%d Lokale Verbindung geschlossen\r\n %d 本地连接断开\r\n=%d Lokale Verbindung geschlossen\r\n
+4 -2
View File
@@ -1990,18 +1990,20 @@ MCP
端口需为 1-65535 的数字=Port must be a number between 1 and 65535 端口需为 1-65535 的数字=Port must be a number between 1 and 65535
Token 不能为空=Token cannot be empty Token 不能为空=Token cannot be empty
MCP 设置已保存。=MCP settings saved. MCP 设置已保存。=MCP settings saved.
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输的改动需重启程序生效。=Changes to enable/port/bind address/token/read-only/whitelist/persistent terminal/remote control/file transfer take effect after restart. 启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。=Changes to enable/port/bind address/token/read-only/whitelist/persistent terminal/remote control/file transfer/management operations take effect after restart.
只读模式(禁命令执行)=Read-only mode (disable command execution) 只读模式(禁命令执行)=Read-only mode (disable command execution)
命令白名单=Command whitelist 命令白名单=Command whitelist
MCP命令执行=MCP command execution MCP命令执行=MCP command execution
启用持久终端(全命令,无白名单)=Enable persistent terminal (full commands, no whitelist) 启用持久终端(全命令,无白名单)=Enable persistent terminal (full commands, no whitelist)
持久终端与远程控制仅在只读模式关闭时生效。=Persistent terminal and remote control take effect only when read-only mode is off. 持久终端、远程控制与管理操作仅在只读模式关闭时生效。=Persistent terminal, remote control and management operations take effect only when read-only mode is off.
MCP持久终端=MCP persistent terminal MCP持久终端=MCP persistent terminal
MCP远程控制=MCP remote control MCP远程控制=MCP remote control
MCP文件传输=MCP file transfer MCP文件传输=MCP file transfer
MCP管理操作=MCP management operation
Token=Token Token=Token
启用远程控制(AI 操控桌面)=Enable remote control (AI controls the desktop) 启用远程控制(AI 操控桌面)=Enable remote control (AI controls the desktop)
启用文件传输(download_file)=Enable file transfer (download_file) 启用文件传输(download_file)=Enable file transfer (download_file)
启用管理操作(进程/窗口/服务/电源)=Enable management operations (process/window/service/power)
第 %d 个钱包地址不合法!=Wallet address #%d is invalid! 第 %d 个钱包地址不合法!=Wallet address #%d is invalid!
%d 新连接\r\n=%d New connection\r\n %d 新连接\r\n=%d New connection\r\n
%d 本地连接断开\r\n=%d Local connection closed\r\n %d 本地连接断开\r\n=%d Local connection closed\r\n
+4 -2
View File
@@ -1981,18 +1981,20 @@ MCP
端口需为 1-65535 的数字=連接埠需為 1-65535 的數字 端口需为 1-65535 的数字=連接埠需為 1-65535 的數字
Token 不能为空=Token 不能為空 Token 不能为空=Token 不能為空
MCP 设置已保存。=MCP 設定已儲存。 MCP 设置已保存。=MCP 設定已儲存。
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输的改动需重启程序生效。=啟用/連接埠/綁定位址/Token/唯讀/白名單/持久終端/遠端控制/檔案傳輸的變更需重新啟動程式後生效。 启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。=啟用/連接埠/綁定位址/Token/唯讀/白名單/持久終端/遠端控制/檔案傳輸/管理操作的變更需重新啟動程式後生效。
只读模式(禁命令执行)=唯讀模式(禁命令執行) 只读模式(禁命令执行)=唯讀模式(禁命令執行)
命令白名单=命令白名單 命令白名单=命令白名單
MCP命令执行=MCP命令執行 MCP命令执行=MCP命令執行
启用持久终端(全命令,无白名单)=啟用持久終端(完整命令,無白名單) 启用持久终端(全命令,无白名单)=啟用持久終端(完整命令,無白名單)
持久终端与远程控制仅在只读模式关闭时生效。=持久終端與遠端控制僅在唯讀模式關閉時生效。 持久终端、远程控制与管理操作仅在只读模式关闭时生效。=持久終端、遠端控制與管理操作僅在唯讀模式關閉時生效。
MCP持久终端=MCP持久終端 MCP持久终端=MCP持久終端
MCP远程控制=MCP遠端控制 MCP远程控制=MCP遠端控制
MCP文件传输=MCP檔案傳輸 MCP文件传输=MCP檔案傳輸
MCP管理操作=MCP管理操作
Token=Token Token=Token
启用远程控制(AI 操控桌面)=啟用遠端控制(AI 操控桌面) 启用远程控制(AI 操控桌面)=啟用遠端控制(AI 操控桌面)
启用文件传输(download_file)=啟用檔案傳輸(download_file) 启用文件传输(download_file)=啟用檔案傳輸(download_file)
启用管理操作(进程/窗口/服务/电源)=啟用管理操作(程序/視窗/服務/電源)
第 %d 个钱包地址不合法!=第 %d 個錢包位址不合法! 第 %d 个钱包地址不合法!=第 %d 個錢包位址不合法!
%d 新连接\r\n=%d 新連線\r\n %d 新连接\r\n=%d 新連線\r\n
%d 本地连接断开\r\n=%d 本機連線斷開\r\n %d 本地连接断开\r\n=%d 本機連線斷開\r\n
Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 108 KiB