From 9eed43bc8d1d5a4e5ae87f7f1a024b90fb7ae150 Mon Sep 17 00:00:00 2001 From: yuanyuanxiang <962914132@qq.com> Date: Thu, 1 Oct 2026 16:52:12 +0200 Subject: [PATCH] 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 --- docs/Mcp_Manage_Design.md | 219 +++++++++ server/2015Remote/2015Remote.rc | Bin 172836 -> 172902 bytes server/2015Remote/2015RemoteDlg.cpp | 20 +- server/2015Remote/McpServer.cpp | 611 ++++++++++++++++++++++++- server/2015Remote/McpServer.h | 33 ++ server/2015Remote/McpSettingsDlg.cpp | 15 +- server/2015Remote/McpSettingsDlg.h | 2 + server/2015Remote/Server.h | 14 + server/2015Remote/context.h | 10 + server/2015Remote/lang/de_DE.ini | 6 +- server/2015Remote/lang/en_US.ini | 6 +- server/2015Remote/lang/zh_TW.ini | 6 +- server/2015Remote/res/ToolBar_Main.bmp | Bin 103734 -> 110646 bytes 13 files changed, 932 insertions(+), 10 deletions(-) create mode 100644 docs/Mcp_Manage_Design.md diff --git a/docs/Mcp_Manage_Design.md b/docs/Mcp_Manage_Design.md new file mode 100644 index 0000000..4b3208d --- /dev/null +++ b/docs/Mcp_Manage_Design.md @@ -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)`。 diff --git a/server/2015Remote/2015Remote.rc b/server/2015Remote/2015Remote.rc index f87dacbbb3c374e1e390658de4b6c404d2e0bcb5..f424ce54000bf457471c7ab5d1dc4bd66dd5c7fc 100644 GIT binary patch delta 52 zcmV-40L%ZR#tP=f3V?(Gv;x9VmsU*zB$trL0R#g>08p2q$pIIW)@Blyu*LxdmvB@9 K4!7n|0zOhGD-+lN delta 33 pcmaEMjBCj;u7(!IElh_3CSOYvn!e!}Bg^!E>P$l0bpn~Jf&c;T4SfIr diff --git a/server/2015Remote/2015RemoteDlg.cpp b/server/2015Remote/2015RemoteDlg.cpp index 0304dd2..022785d 100644 --- a/server/2015Remote/2015RemoteDlg.cpp +++ b/server/2015Remote/2015RemoteDlg.cpp @@ -1415,6 +1415,7 @@ VOID CMy2015RemoteDlg::CreateToolBar() m_ToolBar.SetButtonText(12, _TR("搜索")); m_ToolBar.SetButtonText(13, _TR("帮助")); m_ToolBar.SetButtonText(14, _T("Lang/语言")); + m_ToolBar.SetButtonText(15, _T("MCP")); // ============================================================ // UIBranding: 根据编译时配置或运行时标志隐藏工具栏按钮 @@ -1465,7 +1466,7 @@ VOID CMy2015RemoteDlg::CreateToolBar() } // 如果所有按钮都被隐藏,则隐藏整个工具栏 - if (hiddenCount >= 15) { + if (hiddenCount >= 16) { m_ToolBar.ShowWindow(SW_HIDE); } @@ -2209,6 +2210,8 @@ BOOL CMy2015RemoteDlg::OnInitDialog() McpServer().SetRemoteControlEnabled(THIS_CFG.GetInt("settings", "McpRemoteControl", 0) != 0); // 文件传输开关:默认关;仅 download_file 门控(不要求只读关)。 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)) { Mprintf("McpServer start failed on %s:%d\n", mcpBind.c_str(), mcpPort); } else { @@ -5202,6 +5205,12 @@ BOOL CALLBACK CMy2015RemoteDlg::OfflineProc(CONTEXT_OBJECT* ContextObject) McpServer().OnScreenControlClosed(ContextObject); } + // MCP P7 管理写操作(kill/窗口/服务)接管的子连接断开:清挂起,避免超时路径对 + // 已回收(连接池复用)的 context 误 CancelIO(与终端/屏幕子连接的清理同理)。 + if (McpServer().IsRunning()) { + McpServer().OnManageSubClosed(ContextObject); + } + SOCKET nSocket = ContextObject->sClientSocket; CDialogBase* p = (CDialogBase*)ContextObject->hDlg; @@ -6541,8 +6550,13 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject) case TOKEN_PSLIST: { // 进程管理【x】 // P2b:MCP 挂起时接管。进程/窗口列表无请求 id,按 device_id 匹配并同步拷贝 // (此时 IO 线程阻塞在 NotifyProc,缓冲尚未被覆盖,拷贝安全)。 + // P7:写工具(kill_process/operate_window)的「就绪包」与「确认包」是同一 token, + // OnSystemReady 命中就绪包时接管子连接并下发写命令(返回 true、不 CancelIO), + // 命中确认包或只读工具时返回 false,走 TakeMainResponse + CancelIO 收尾。 uint64_t devId = ContextObject->GetClientID(); if (McpServer().IsPending(devId)) { + if (McpServer().OnSystemReady(devId, ContextObject)) + break; McpServer().TakeMainResponse(devId, ContextObject->GetBuffer(0), ContextObject->GetBufferLength()); ContextObject->CancelIO(); // 用完即关,避免子链接泄漏(仿 WebService 终端关闭) break; @@ -6585,8 +6599,12 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject) case TOKEN_SERVERLIST: { // 服务管理【x】 // P3:MCP 挂起时接管。服务列表为一次性子链接回传(CServicesManager 构造即发), // 取走即 CancelIO 关子链接(无 MFC 对话框续用该子链接)。 + // P7:control_service 的就绪/确认是同一 token,OnServiceReady 命中就绪包时接管并下发 + // COMMAND_SERVICECONFIG(返回 true、不 CancelIO),确认包走 TakeMainResponse 收尾。 uint64_t devId = ContextObject->GetClientID(); if (McpServer().IsPending(devId)) { + if (McpServer().OnServiceReady(devId, ContextObject)) + break; McpServer().TakeMainResponse(devId, szBuffer, len); ContextObject->CancelIO(); break; diff --git a/server/2015Remote/McpServer.cpp b/server/2015Remote/McpServer.cpp index e6797bd..7f37952 100644 --- a/server/2015Remote/McpServer.cpp +++ b/server/2015Remote/McpServer.cpp @@ -840,6 +840,129 @@ Json::Value BuildServiceListOutputSchema() { 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 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 缺省/空 = 列根键) Json::Value BuildListRegistryInputSchema() { 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 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 std::string BuildToolsListResult(const Json::Value& id) { Json::Value result(Json::objectValue); @@ -1433,6 +1570,43 @@ std::string BuildToolsListResult(const Json::Value& id) { 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; 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); } +// ========== 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 pidList; + std::vector 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 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 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 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 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 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 分派 std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) { 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 == "download_file") return BuildDownloadFile(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, "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) { std::lock_guard lk(m_PendingMutex); 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.done = true; m_PendingCv.notify_one(); @@ -4902,6 +5407,110 @@ bool CMcpServer::WaitPendingRegistry(uint64_t device_id, std::vector& subk return true; } +// ===== P7:设备管理控制(写工具接管)===== + +bool CMcpServer::BeginPendingFollowUp(uint64_t device_id, const std::string& tool, + const std::vector& followUp) { + if (IsFileTransferPending(device_id)) return false; // 该 host 有文件传输会话在飞(互斥,见 F2) + std::lock_guard 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 followUp; + { + std::lock_guard 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 followUp; + { + std::lock_guard 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& out, int timeoutMs) { + context* subCtx = nullptr; + std::unique_lock 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 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)。其声明需在 前 // 定义 _CRT_RAND_S;为避免依赖 PCH 的包含顺序,这里手动声明其导出原型(errno_t == int)。 extern "C" int __cdecl rand_s(unsigned int* randomValue); diff --git a/server/2015Remote/McpServer.h b/server/2015Remote/McpServer.h index 8488dcd..3beaa28 100644 --- a/server/2015Remote/McpServer.h +++ b/server/2015Remote/McpServer.h @@ -242,6 +242,34 @@ public: // 收尾:擦会话 + 锁外 CancelIO 文件管理器/流式两条子链接(幂等)。 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& 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& out, int timeoutMs); + + // OfflineProc 调用:接管的子连接断开时清挂起(置 done 并清 subCtx,唤醒等待线程)。 + // 不擦表(由 WaitPendingWrite 持有迭代器时自行擦除,防迭代器失效);清 subCtx 使 + // 超时路径不会对已回收(连接池复用)的 context 误 CancelIO。 + void OnManageSubClosed(context* subCtx); + // 安全配置(启动时由 CMy2015RemoteDlg 读 THIS_CFG 后设置)。 void SetReadonly(bool readonly) { m_readonly = readonly; } void SetCmdWhitelist(const std::string& whitelist) { m_cmdWhitelist = whitelist; } @@ -274,6 +302,10 @@ private: bool pathDone = false; // list_registry:已收到 TOKEN_REG_PATH bool keyDone = false; // list_registry:已收到 TOKEN_REG_KEY bool done = false; + // ===== P7:写工具(kill_process / operate_window / control_service)接管进度 ===== + std::vector followUp; // 接管后下发的写命令 payload(首字节 = 写命令) + bool followUpSent = false; // false=等子连接就绪(首包);true=已发写命令、等确认包 + context* subCtx = nullptr; // 接管的子连接(超时/失败 CancelIO 用,防孤儿子连接) }; std::mutex m_PendingMutex; std::condition_variable m_PendingCv; @@ -348,6 +380,7 @@ private: bool m_terminalEnabled = false; // 持久终端开关(默认关;要求 m_readonly=false) bool m_remoteControlEnabled = false; // 远程控制开关(默认关;要求 m_readonly=false) bool m_fileTransferEnabled = false; // 文件传输开关(默认关;download_file) + bool m_manageEnabled = false; // P7 管理控制开关(默认关;要求 m_readonly=false) }; // 全局访问器(仿 WebService(),见 WebService.h 末尾) diff --git a/server/2015Remote/McpSettingsDlg.cpp b/server/2015Remote/McpSettingsDlg.cpp index 92f65e1..575f88b 100644 --- a/server/2015Remote/McpSettingsDlg.cpp +++ b/server/2015Remote/McpSettingsDlg.cpp @@ -145,6 +145,9 @@ BOOL CMcpSettingsDlg::OnInitDialog() m_btnFileTransfer.Create(_TR("启用文件传输(download_file)"), WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX, 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_editWhitelist.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | 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_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_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)); @@ -184,6 +188,7 @@ BOOL CMcpSettingsDlg::OnInitDialog() int terminal = THIS_CFG.GetInt("settings", "McpTerminal", 0); int remoteControl = THIS_CFG.GetInt("settings", "McpRemoteControl", 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", ""); m_btnEnable.SetCheck(enabled ? BST_CHECKED : BST_UNCHECKED); @@ -194,6 +199,7 @@ BOOL CMcpSettingsDlg::OnInitDialog() m_btnTerminal.SetCheck(terminal ? BST_CHECKED : BST_UNCHECKED); 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_editWhitelist.SetWindowText(CString(WhitelistForDisplay(whitelist).c_str())); @@ -213,6 +219,7 @@ void CMcpSettingsDlg::OnOK() bool terminal = (m_btnTerminal.GetCheck() == BST_CHECKED); bool remoteControl = (m_btnRemoteControl.GetCheck() == BST_CHECKED); bool fileTransfer = (m_btnFileTransfer.GetCheck() == BST_CHECKED); + bool manage = (m_btnManage.GetCheck() == BST_CHECKED); // 端口校验:1-65535 int port = atoi(CT2A(sPort)); @@ -239,14 +246,15 @@ void CMcpSettingsDlg::OnOK() THIS_CFG.SetInt("settings", "McpTerminal", terminal ? 1 : 0); THIS_CFG.SetInt("settings", "McpRemoteControl", remoteControl ? 1 : 0); THIS_CFG.SetInt("settings", "McpFileTransfer", fileTransfer ? 1 : 0); + THIS_CFG.SetInt("settings", "McpManage", manage ? 1 : 0); std::string whitelist = CT2A(sWhitelist); whitelist = NormalizeWhitelist(whitelist); THIS_CFG.SetStr("settings", "McpCmdWhitelist", whitelist); // 拆成两段可翻译的单行键,中间用 \r\n 连接(多行键无法在 INI 中表示) MessageBox(_TR("MCP 设置已保存。") + _T("\r\n") + - _TR("启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输的改动需重启程序生效。") + _T("\r\n") + - _TR("持久终端与远程控制仅在只读模式关闭时生效。"), + _TR("启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。") + _T("\r\n") + + _TR("持久终端、远程控制与管理操作仅在只读模式关闭时生效。"), _TR("提示"), MB_ICONINFORMATION); CDialog::OnOK(); @@ -289,6 +297,9 @@ void CMcpSettingsDlg::LayoutControls(int cx, int cy) m_btnFileTransfer.MoveWindow(margin, y, cx - margin * 2, 22); y += 30; + m_btnManage.MoveWindow(margin, y, cx - margin * 2, 22); + y += 30; + const int whitelistH = 90; m_lblWhitelist.MoveWindow(margin, y, labelW, rowH); m_editWhitelist.MoveWindow(margin + labelW, y - 2, cx - margin * 2 - labelW, whitelistH); diff --git a/server/2015Remote/McpSettingsDlg.h b/server/2015Remote/McpSettingsDlg.h index 3ace154..43cbbde 100644 --- a/server/2015Remote/McpSettingsDlg.h +++ b/server/2015Remote/McpSettingsDlg.h @@ -31,6 +31,7 @@ private: IDC_MCP_TERMINAL = 1007, // 「启用持久终端」复选框(全命令,无白名单,要求只读关) IDC_MCP_REMOTECONTROL = 1008, // 「启用远程控制」复选框(AI 操控桌面,要求只读关) IDC_MCP_FILETRANSFER = 1009, // 「启用文件传输」复选框(download_file,不要求只读关) + IDC_MCP_MANAGE = 1010, // 「启用管理操作」复选框(kill/窗口/服务/电源,要求只读关) }; CButton m_btnEnable; @@ -40,6 +41,7 @@ private: CButton m_btnTerminal; CButton m_btnRemoteControl; CButton m_btnFileTransfer; + CButton m_btnManage; CStatic m_lblWhitelist; CEdit m_editWhitelist; CButton m_btnOK, m_btnCancel; diff --git a/server/2015Remote/Server.h b/server/2015Remote/Server.h index 47fb7c7..f4da931 100644 --- a/server/2015Remote/Server.h +++ b/server/2015Remote/Server.h @@ -573,6 +573,20 @@ public: } 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& a = {}) { for (int i = 0; i < ONLINELIST_MAX; i++) { diff --git a/server/2015Remote/context.h b/server/2015Remote/context.h index 2897cd5..a3d9056 100644 --- a/server/2015Remote/context.h +++ b/server/2015Remote/context.h @@ -55,6 +55,16 @@ public: } virtual void SetGroupName(const std::string& group) {} + // 等待本连接待发送缓冲排空(用于「发写命令 → 补发刷新命令」的背靠背发送: + // 两次 Send2Client 会对同一发送缓冲并发 WSASend,造成竞态/丢包)。非 IOCP + // 服务端无异步发送缓冲,默认立即返回 true;CONTEXT_OBJECT 覆写为轮询发送缓冲 + // 归零直到排空或超时。 + virtual bool WaitSendIdle(int timeoutMs) + { + (void)timeoutMs; + return true; + } + // 检查客户端是否支持 V2 文件传输 bool SupportsFileV2() const { CString caps = GetClientData(ONLINELIST_CAPABILITIES); diff --git a/server/2015Remote/lang/de_DE.ini b/server/2015Remote/lang/de_DE.ini index b8a72d3..7ed13b1 100644 --- a/server/2015Remote/lang/de_DE.ini +++ b/server/2015Remote/lang/de_DE.ini @@ -1989,18 +1989,20 @@ 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 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) ÃüÁî°×Ãûµ¥=Befehls-Whitelist MCPÃüÁîִÐÐ=MCP-Befehlsausfuehrung ÆôÓó־ÃÖնˣ¨ȫÃüÁÎް×Ãûµ¥£©=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-Fernsteuerung MCPÎļþ´«Êä=MCP-Dateiuebertragung +MCP¹ÜÀí²Ù×÷=MCP-Verwaltungsoperation Token=Token ÆôÓÃԶ³̿ØÖƣ¨AI ²ٿØ×ÀÃ棩=Fernsteuerung aktivieren (KI steuert den Desktop) ÆôÓÃÎļþ´«Ê䣨download_file£©=Dateiuebertragung aktivieren (download_file) +ÆôÓùÜÀí²Ù×÷£¨½ø³Ì/´°¿Ú/·þÎñ/µçԴ£©=Verwaltungsoperationen aktivieren (Prozess/Fenster/Dienst/Energie) µÚ %d ¸öǮ°üµØַ²»ºϷ¨!=Wallet-Adresse #%d ist ungueltig! %d ÐÂÁ¬½Ó\r\n=%d Neue Verbindung\r\n %d ±¾µØÁ¬½ӶϿª\r\n=%d Lokale Verbindung geschlossen\r\n diff --git a/server/2015Remote/lang/en_US.ini b/server/2015Remote/lang/en_US.ini index b0f6f62..12c688a 100644 --- a/server/2015Remote/lang/en_US.ini +++ b/server/2015Remote/lang/en_US.ini @@ -1990,18 +1990,20 @@ 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 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) ÃüÁî°×Ãûµ¥=Command whitelist MCPÃüÁîִÐÐ=MCP command execution ÆôÓó־ÃÖնˣ¨ȫÃüÁÎް×Ãûµ¥£©=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 remote control MCPÎļþ´«Êä=MCP file transfer +MCP¹ÜÀí²Ù×÷=MCP management operation Token=Token ÆôÓÃԶ³̿ØÖƣ¨AI ²ٿØ×ÀÃ棩=Enable remote control (AI controls the desktop) ÆôÓÃÎļþ´«Ê䣨download_file£©=Enable file transfer (download_file) +ÆôÓùÜÀí²Ù×÷£¨½ø³Ì/´°¿Ú/·þÎñ/µçԴ£©=Enable management operations (process/window/service/power) µÚ %d ¸öǮ°üµØַ²»ºϷ¨!=Wallet address #%d is invalid! %d ÐÂÁ¬½Ó\r\n=%d New connection\r\n %d ±¾µØÁ¬½ӶϿª\r\n=%d Local connection closed\r\n diff --git a/server/2015Remote/lang/zh_TW.ini b/server/2015Remote/lang/zh_TW.ini index 19576ee..5530a65 100644 --- a/server/2015Remote/lang/zh_TW.ini +++ b/server/2015Remote/lang/zh_TW.ini @@ -1981,18 +1981,20 @@ MCP ¶˿ÚÐèΪ 1-65535 µÄÊý×Ö=ßB½ӲºÐèžé 1-65535 µĔµ×Ö Token ²»ÄÜΪ¿Õ=Token ²»Äܞé¿Õ MCP ÉèÖÃÒѱ£´档=MCP ÔO¶¨Òу¦´档 -ÆôÓÃ/¶˿Ú/°󶨵Øַ/Token/ֻ¶Á/°×Ãûµ¥/³־ÃÖնË/Զ³̿ØÖÆ/Îļþ´«Êäµĸ͝ÐèÖØÆô³ÌÐòÉúЧ¡£=†¢ÓÃ/ßB½Ӳº/½‰¶¨λַ/Token/Ψ×x/°×Ãû†Î/³־ýK¶Ë/ßh¶˿ØÖÆ/™n°¸‚÷ݔµÄ׃¸üÐèÖØÐ†¢„ӳÌʽááÉúЧ¡£ +ÆôÓÃ/¶˿Ú/°󶨵Øַ/Token/ֻ¶Á/°×Ãûµ¥/³־ÃÖնË/Զ³̿ØÖÆ/Îļþ´«Êä/¹ÜÀí²Ù×÷µĸ͝ÐèÖØÆô³ÌÐòÉúЧ¡£=†¢ÓÃ/ßB½Ӳº/½‰¶¨λַ/Token/Ψ×x/°×Ãû†Î/³־ýK¶Ë/ßh¶˿ØÖÆ/™n°¸‚÷ݔ/¹ÜÀí²Ù×÷µÄ׃¸üÐèÖØÐ†¢„ӳÌʽááÉúЧ¡£ ֻ¶Áģʽ£¨½ûÃüÁîִÐУ©=Ψ×xģʽ£¨½ûÃüÁîˆÌÐУ© ÃüÁî°×Ãûµ¥=ÃüÁî°×Ãû†Î MCPÃüÁîִÐÐ=MCPÃüÁîˆÌÐÐ ÆôÓó־ÃÖնˣ¨ȫÃüÁÎް×Ãûµ¥£©=†¢Óó־ýK¶ˣ¨ÍêÕûÃüÁŸo°×Ãû†Σ© -³־ÃÖնËÓëԶ³̿ØÖƽöÔÚֻ¶Áģʽ¹رÕʱÉúЧ¡£=³־ýK¶ËÅcßh¶˿ØÖƃHÔÚΨ×xģʽêPé]•rÉúЧ¡£ +³־ÃÖնˡ¢Զ³̿ØÖÆÓë¹ÜÀí²Ù×÷½öÔÚֻ¶Áģʽ¹رÕʱÉúЧ¡£=³־ýK¶ˡ¢ßh¶˿ØÖÆÅc¹ÜÀí²Ù×÷ƒHÔÚΨ×xģʽêPé]•rÉúЧ¡£ MCP³־ÃÖնË=MCP³־ýK¶Ë MCPԶ³̿ØÖÆ=MCPßh¶˿ØÖÆ MCPÎļþ´«Êä=MCP™n°¸‚÷ݔ +MCP¹ÜÀí²Ù×÷=MCP¹ÜÀí²Ù×÷ Token=Token ÆôÓÃԶ³̿ØÖƣ¨AI ²ٿØ×ÀÃ棩=†¢ÓÃßh¶˿ØÖƣ¨AI ²ٿØ×ÀÃ棩 ÆôÓÃÎļþ´«Ê䣨download_file£©=†¢ÓÙn°¸‚÷ݔ£¨download_file£© +ÆôÓùÜÀí²Ù×÷£¨½ø³Ì/´°¿Ú/·þÎñ/µçԴ£©=†¢ÓùÜÀí²Ù×÷£¨³ÌÐò/ҕ´°/·þ„Õ/ëŠԴ£© µÚ %d ¸öǮ°üµØַ²»ºϷ¨!=µÚ %d ‚€åX°üλַ²»ºϷ¨! %d ÐÂÁ¬½Ó\r\n=%d ÐÂßB¾€\r\n %d ±¾µØÁ¬½ӶϿª\r\n=%d ±¾™CßB¾€”àé_\r\n diff --git a/server/2015Remote/res/ToolBar_Main.bmp b/server/2015Remote/res/ToolBar_Main.bmp index 5435fd10bee99fc42265ee357ba33a8b281a7f8a..08c638802c81c7e45670ddb8ae777ac2be85463a 100644 GIT binary patch delta 2619 zcmcguO=uHA6y7l!vl<#3x;7AKEd)}6u_=Mj)>^WWLW)1#NTE_}joSL(Hc-?{F}?NR zp&bT{#Y5{wMK6s59lR(=FZI}-M9>}-FIIE#B#6%2ZH6VACTXlLJG*apzVCZ)cV^z3 zrDyh~b^GDismLOd;E3?n42DgTLi~m#4?IZ{?_aKwcq6UD9a*iVKCA6RG+lpe7uIG; zy1ra{PQNsC+5o%apT?S8_?LB;TJtO^-R9`ymOh*9=e;LbZLn>0Ok(vWS!i=p zXZWncnakyPk8xtvYxG~{l6G=HmFdLMAqUp%vI*mnxrT9KHA(LDxaj(E9pw_Jyq;pl z4$`iZqmD=c$oj}b5{Jpv02w>uq^xh42PB6IXo4UN!OVx+Pd*ID)Heuxm-&Mfmj&BL zj@*zb9s#Vo3^qX?PB8U;KLxNaTvpi%z-l`k&jQ`sZSAe%4OT<+ z<4X`!5>?PkQ?MGKy>CF=d8ssVM|!c^L|?y6InsR6?}*@bMK%{3j@Qzbwg39r^Y8!R z5V}5r_zq6E^j8q9dI5a@F~RA9N>=7(0t(WLzrbanv+R(HgeeS>TKRNlt9uX#mB~el zIY$OqtOn^3m+~&$2}Afb3{pE3X0HySE8xDklAsEsK*M;%svmMRYi-oU0`a0h#n31! zu+>XmP%~XYB(q0;be)Ik|I09gP@-5}$OzL~mOR!98-l9HrrUIi-%9bCM<&9)MfH*K zl>xRxS!byp1cl0HF5*p4;WBCx#mH3Do>}Nc4)!^LMmVm)jJuY}okwnVZU9|?E@NLY zo|#Ib*$OXnBp3IP#i)DfDhl&0lgH4wm3f!q8kJq{la06+G)T0~PR0`ftc4k;u2r~Q VGhz_jZobnygtFTD-c4h7-5*sZIf(!O delta 675 zcmbV~F-Sr|7{}i!J?=n^Tzt~vSdWHKa7e=tg_bApfoL?CqELu}5*0M)&>(G3k9;^O zjY)%CcvCbaH5Rlq)>`1w(9+O%YO~V8|M&g5@BZ9(aDz8_a3!xrw7>BMH61CpY$v;bn5p`&zrfSYmn;rWS z_GZY%ih&&rhvjCBuv|Ee>9@AXGTUj);zkM6YRbV=W&KhkY!i^v*OxlU4Xv54rrGew|qzJ-`cxDg5q?32@+yOLOC&Sj