Feature: Add list_services and get_client_log MCP tools
Add the two P2d read-only MCP tools, both one-shot sub-links (mode A') on the existing single-flight pending registry. list_services sends COMMAND_SERVICES; the client's CServicesManager emits TOKEN_SERVERLIST on sub-link creation, parsed as 5 null-terminated fields per record (display_name/service_name/binary_path/status/start_type) with zero-padding termination. Services are Windows-only, so LNX/MAC hosts get -32005 up front instead of a 20s timeout. get_client_log sends COMMAND_QUERY_LOG; the client's CClientLogManager dumps its full in-memory Logger ring buffer once, then pushes deltas every 3s. The MCP path takes the first (full) TOKEN_REPORT_LOG and cancels the sub-link so later deltas stop, while the MFC log dialog keeps receiving deltas on the non-pending branch. Log text is client ANSI on Windows, decoded by clientType like process/file names. MessageHandle intercepts both with IsPending -> TakeMainResponse + CancelIO. Sync the design doc (P2d section + verification notes; Linux get_client_log timeout is a client-version gap). Co-Authored-By: deepseek-v4-pro
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# YAMA MCP 功能开发技术书(Phase 2 及后续)
|
# YAMA MCP 功能开发技术书(Phase 2 及后续)
|
||||||
|
|
||||||
> **状态**:设计定稿(已按两轮评审修订);P2a、P2b 已实现并经真实主机验证(`search_hosts` / `get_host_detail` / `list_processes` / `list_windows` / `get_activity_history`);P2c 已实现、待实机验证(`get_screenshot` / `list_files`)。
|
> **状态**:设计定稿(已按两轮评审修订);P2a、P2b 已实现并经真实主机验证(`search_hosts` / `get_host_detail` / `list_processes` / `list_windows` / `get_activity_history`);P2c 已实现(`get_screenshot` / `list_files`);P2d 已实现并验证(`list_services` / `get_client_log`)。
|
||||||
> **读者**:MCP 后续功能的研发/评审人员。
|
> **读者**:MCP 后续功能的研发/评审人员。
|
||||||
> **关联文档**:[Mcp_Design.md](./Mcp_Design.md)(Phase 1 的协议、架构、配置与菜单设计)。
|
> **关联文档**:[Mcp_Design.md](./Mcp_Design.md)(Phase 1 的协议、架构、配置与菜单设计)。
|
||||||
> **核心目标**:让 MCP 从「单个只读工具」平滑演进为「分阶段、可回滚、影响面可控」的工具集,**不一次性大改现有功能**。
|
> **核心目标**:让 MCP 从「单个只读工具」平滑演进为「分阶段、可回滚、影响面可控」的工具集,**不一次性大改现有功能**。
|
||||||
@@ -196,6 +196,8 @@ Web 是长连接,子链接可长驻;MCP 是**一次性 request/response**。
|
|||||||
| P2b(已实现并验证) | `get_activity_history` | 单机历史活动(文本快照) | `COMMAND_QUERY_ACTIVITY`(159)→`TOKEN_REPORT_ACTIVITY`(160) | **A** | 只读 |
|
| P2b(已实现并验证) | `get_activity_history` | 单机历史活动(文本快照) | `COMMAND_QUERY_ACTIVITY`(159)→`TOKEN_REPORT_ACTIVITY`(160) | **A** | 只读 |
|
||||||
| P2c(已实现) | `get_screenshot` | 单帧屏幕截图(JPEG) | `COMMAND_SCREEN_PREVIEW_REQ`→`TOKEN_SCREEN_PREVIEW_RSP` | **A** | 只读 |
|
| P2c(已实现) | `get_screenshot` | 单帧屏幕截图(JPEG) | `COMMAND_SCREEN_PREVIEW_REQ`→`TOKEN_SCREEN_PREVIEW_RSP` | **A** | 只读 |
|
||||||
| P2c(已实现) | `list_files` | 目录列举 | `COMMAND_LIST_DRIVE`→`TOKEN_DRIVE_LIST`(列盘)/ 同子链接再 `COMMAND_LIST_FILES`→`TOKEN_FILE_LIST`(列目录) | **A′** | 只读 |
|
| P2c(已实现) | `list_files` | 目录列举 | `COMMAND_LIST_DRIVE`→`TOKEN_DRIVE_LIST`(列盘)/ 同子链接再 `COMMAND_LIST_FILES`→`TOKEN_FILE_LIST`(列目录) | **A′** | 只读 |
|
||||||
|
| P2d(已实现) | `list_services` | Windows 服务列表 | `COMMAND_SERVICES`(218)→(子链接)`TOKEN_SERVERLIST` | **A′** | 只读 |
|
||||||
|
| P2d(已实现) | `get_client_log` | 客户端运行日志快照 | `COMMAND_QUERY_LOG`(155)→(子链接)`TOKEN_REPORT_LOG`(156) | **A′** | 只读 |
|
||||||
| P3 | `exec_command` | 执行命令、返回 stdout | `COMMAND_SHELL`→`TOKEN_SHELL_START`/`TOKEN_SHELL_DATA` | **B** | **写** |
|
| P3 | `exec_command` | 执行命令、返回 stdout | `COMMAND_SHELL`→`TOKEN_SHELL_START`/`TOKEN_SHELL_DATA` | **B** | **写** |
|
||||||
| P3 | `send_message` | 给被控端弹消息 | `COMMAND_TALK`(220) | **A0** | 写(低危) |
|
| P3 | `send_message` | 给被控端弹消息 | `COMMAND_TALK`(220) | **A0** | 写(低危) |
|
||||||
| P3 | `kill_process` | 结束进程 | `COMMAND_KILLPROCESS` | **A0** | **写** |
|
| P3 | `kill_process` | 结束进程 | `COMMAND_KILLPROCESS` | **A0** | **写** |
|
||||||
@@ -203,7 +205,7 @@ Web 是长连接,子链接可长驻;MCP 是**一次性 request/response**。
|
|||||||
|
|
||||||
### 5.2 Phase 2:只读观测(低风险,无写操作)
|
### 5.2 Phase 2:只读观测(低风险,无写操作)
|
||||||
|
|
||||||
全部只读,不改变被控端任何状态,token 模型不变(仍单一静态 token)。P2a 纯内存、零改动;P2b/P2c 引入模式 A/A′ 无头接管,但只读、可回滚。
|
全部只读,不改变被控端任何状态,token 模型不变(仍单一静态 token)。P2a 纯内存、零改动;P2b/P2c/P2d 引入模式 A/A′ 无头接管,但只读、可回滚。
|
||||||
|
|
||||||
### 5.3 Phase 3:安全写操作(需单独评审)
|
### 5.3 Phase 3:安全写操作(需单独评审)
|
||||||
|
|
||||||
@@ -307,7 +309,52 @@ Web 是长连接,子链接可长驻;MCP 是**一次性 request/response**。
|
|||||||
|
|
||||||
**回滚**:revert `McpServer.h` + `McpServer.cpp` + `MessageHandle` 三处拦截。
|
**回滚**:revert `McpServer.h` + `McpServer.cpp` + `MessageHandle` 三处拦截。
|
||||||
|
|
||||||
### 6.4 P3a:`exec_command`(模式 B,写操作,评审后实施)
|
### 6.4 P2d:`list_services` + `get_client_log`(模式 A′,只读感知扩展)✅ 已实现并验证
|
||||||
|
|
||||||
|
> 本期新增两个**只读**感知工具,均走**一次性子链接**(模式 A′),复用 §4.4 挂起注册表,无新增机制、无写操作。二者与 P2b 的进程/窗口、P2c 的列盘同构:主连接下发命令 → 客户端建子链接回传 → 取走即 `CancelIO`。
|
||||||
|
|
||||||
|
**`list_services`(服务管理链路,一次性子链接,仅 Windows)**:
|
||||||
|
|
||||||
|
- 复用经典服务链路 `COMMAND_SERVICES`(218)→`TOKEN_SERVERLIST`。客户端 `KernelManager::OnReceive` 收到 `COMMAND_SERVICES` 建子链接 `LoopServicesManager`,`CServicesManager` **构造函数即** `SendServicesList()`(无需再下 `COMMAND_SERVICELIST` 刷新命令),在子链接上一次性回传 `TOKEN_SERVERLIST`。
|
||||||
|
- **记录格式**(`ServicesManager.cpp::GetServicesList`):`[TOKEN_SERVERLIST:1]` 后接 N 条 `[displayName\0][serviceName\0][binaryPath\0][runWay\0][autoRun\0]`,尾部 `LocalAlloc(LPTR)` 零填充终止。**字段顺序以代码为准**(`displayName` 在前、`serviceName` 在后;源码注释「真实名称/显示名称」标注相反,勿据注释判断)。`runWay`/`autoRun` 为英文枚举串(`Stopped/Running/Paused/…`、`Boot-Start/Auto-Start/Demand-Start/Disabled/…`)。
|
||||||
|
- **编码**:字段全部来自 Windows A 接口(`EnumServicesStatus`/`QueryServiceConfig` 返回 ANSI),按 clientType 判定 `cp`(与进程/文件一致);仅 Windows 客户端实现(`EnumServicesStatus` 为 Windows-only),LNX/MAC 主机提前返回 `-32005`。
|
||||||
|
- 返回结构:`structuredContent.services[] = {display_name, service_name, binary_path, status, start_type}`。
|
||||||
|
|
||||||
|
**`get_client_log`(客户端运行日志,一次性子链接取首条全量)**:
|
||||||
|
|
||||||
|
- 复用经典日志链路 `COMMAND_QUERY_LOG`(155)→`TOKEN_REPORT_LOG`(156)。客户端 `KernelManager::OnReceive` 收到 `COMMAND_QUERY_LOG` 建子链接 `LoopClientLogManager`,`CClientLogManager` 构造函数 `SendLogDump()`(`m_sentIdx=0` 返回**全量**),并启动 push 线程**每 3s 推增量**。
|
||||||
|
- **关键语义**:`TOKEN_REPORT_LOG` 会**多次**到达(全量 + 每 3s 增量)。MCP 只取**首条全量**:`MessageHandle` 在 `IsPending` 命中时 `TakeMainResponse` 后立即 `CancelIO` 关子链接,后续增量不再到达(与 MFC 日志对话框「持续接收增量、不关链」的路径区分)。
|
||||||
|
- **日志内容**:YAMA 客户端**自身运行时日志**(`Logger` 内存 ring buffer,最近最多 1000 条,含 `[pid][时间戳] [文件:行号] 消息`),**非** Windows 系统日志。
|
||||||
|
- **编码**:日志文本为客户端 ANSI(Windows 走 `vsnprintf` A 版),按 clientType 判定 `cp`;`data[0]=token` 后为无 `'\0'` 终止的原始字节,需先 `std::string` 包装再 `ToUtf8`(`ToUtf8` 内部以 `-1` 读 null 终止串)。
|
||||||
|
- 返回结构:`structuredContent.log = "<原始日志文本>"`。
|
||||||
|
|
||||||
|
**改动**:
|
||||||
|
|
||||||
|
- `McpServer.h/.cpp`:
|
||||||
|
- 解析器 `ParseServiceList`(5 字段记录,有界读 + 「displayName 与 serviceName 皆空 = 尾部零填充」终止)。
|
||||||
|
- schema `BuildServiceListOutputSchema` / `BuildClientLogOutputSchema`(inputSchema 复用 `BuildGetHostDetailInputSchema` 的 `{ id }`)。
|
||||||
|
- 工具 `BuildListServices`(发 `COMMAND_SERVICES`,LNX/MAC 提前 `-32005`)、`BuildGetClientLog`(发 `COMMAND_QUERY_LOG`)。
|
||||||
|
- `2015RemoteDlg.cpp` 的 `MessageHandle`(两处拦截):
|
||||||
|
- `TOKEN_SERVERLIST`(子 context):`if (IsPending) { TakeMainResponse; CancelIO(); break; }`(一次性,取走即关)。
|
||||||
|
- `TOKEN_REPORT_LOG`(子 context):`if (IsPending) { TakeMainResponse; CancelIO(); break; }`(取首条全量即关,停止 3s 增量)。
|
||||||
|
|
||||||
|
**影响面**:`MessageHandle` 仅新增两个 `if` 分支;非 MCP 触发时 `IsPending` 为假,MFC 服务管理弹框与日志对话框路径不变(回归点)。
|
||||||
|
|
||||||
|
**验收 / 实机验证点**:
|
||||||
|
- MFC 双击「服务管理」弹框仍正常(回归点);MFC 打开「客户端运行日志」对话框仍正常、增量持续追加(回归点)。
|
||||||
|
- MCP `list_services` 对 Windows 主机返回 `services[]`,中文显示名/路径无乱码,`status`/`start_type` 为英文枚举;对 LNX/MAC 主机返回 `-32005`。
|
||||||
|
- MCP `get_client_log` 返回 `log` 文本快照(UTF-8),中文日志无乱码;空日志返回空 `log`。
|
||||||
|
- 错误路径:缺 `id`/非数字 `id` → `-32602`;未知/离线主机 → `-32002`;同 host 并发 → `-32003`;发送失败 → `-32004`;超时 → `-32001`。
|
||||||
|
|
||||||
|
**验证记录**(2026-08-19,真实在线主机实测):
|
||||||
|
- `list_services`(Windows 11 EXE 主机「Essential / VCU」):返回 255 个服务,字段序(显示名→服务名→路径→状态→启动类型)正确,`status`/`start_type` 英文枚举、路径完整。
|
||||||
|
- `get_client_log`(同上主机):返回 510 行 / 40309 字节全量快照,`[pid][时间戳][文件:行号]` 格式正确,中文日志(「启动运行」「任务已存在」「打开注册表失败,错误码:2」等)GBK→UTF-8 转码全部正确。
|
||||||
|
- 错误路径实测:LNX 主机 `list_services` → `-32005`;缺/非法 `id` → `-32602`;离线 → `-32002`。
|
||||||
|
- **客户端版本门槛**:`get_client_log` 对 Linux 客户端 20s 超时返回 `-32001`——当前 Linux 客户端运行版本未实现 `COMMAND_QUERY_LOG`→`CClientLogManager` 日志回传链路(服务端下发命令无响应),属版本/实现门槛,非 MCP 缺陷(与 §6.2 `get_activity_history` 对老客户端超时同类)。`list_services` 因服务为 Windows-only(`EnumServicesStatus`),已对 LNX/MAC 提前 `-32005`,不受此门槛影响。
|
||||||
|
|
||||||
|
**回滚**:revert `McpServer.h` + `McpServer.cpp` + `MessageHandle` 两处拦截。
|
||||||
|
|
||||||
|
### 6.5 P3a:`exec_command`(模式 B,写操作,评审后实施)
|
||||||
|
|
||||||
**改动**:复刻 Web 终端链路(§4.3),把 `m_TermPending` 换成 MCP 自己的挂起标记:
|
**改动**:复刻 Web 终端链路(§4.3),把 `m_TermPending` 换成 MCP 自己的挂起标记:
|
||||||
- 下 `COMMAND_SHELL` → `TOKEN_SHELL_START`/`TOKEN_SHELL_DATA` 接管 → 发送命令文本 → 收集 stdout → 关子链接 → 返回。
|
- 下 `COMMAND_SHELL` → `TOKEN_SHELL_START`/`TOKEN_SHELL_DATA` 接管 → 发送命令文本 → 收集 stdout → 关子链接 → 返回。
|
||||||
@@ -433,6 +480,7 @@ Web 是长连接,子链接可长驻;MCP 是**一次性 request/response**。
|
|||||||
1. **P2a**(`search_hosts` + `get_host_detail`)✅ ——纯内存、零风险,已实现,验证「多工具 dispatch」模式。
|
1. **P2a**(`search_hosts` + `get_host_detail`)✅ ——纯内存、零风险,已实现,验证「多工具 dispatch」模式。
|
||||||
2. **P2b**(`list_processes` + `list_windows` + `get_activity_history`)✅ ——首次引入**模式 A′ 一次性子链接**(进程/窗口)与**模式 A 主连接 RPC**(历史活动)无头接管,把 §4.2/§4.2′ 的机制跑通并沉淀成 §7 配方。
|
2. **P2b**(`list_processes` + `list_windows` + `get_activity_history`)✅ ——首次引入**模式 A′ 一次性子链接**(进程/窗口)与**模式 A 主连接 RPC**(历史活动)无头接管,把 §4.2/§4.2′ 的机制跑通并沉淀成 §7 配方。
|
||||||
3. **P2c**(`get_screenshot` 走屏幕预览链路 + `list_files`)✅ ——补上「看」的能力,仍是只读(`get_screenshot` 模式 A、`list_files` 模式 A′)。
|
3. **P2c**(`get_screenshot` 走屏幕预览链路 + `list_files`)✅ ——补上「看」的能力,仍是只读(`get_screenshot` 模式 A、`list_files` 模式 A′)。
|
||||||
4. **P3a**(`exec_command`)——首次引入**模式 B 子链接流式**,在 P2 的挂起机制成熟后,走完安全评审再落地写操作。
|
4. **P2d**(`list_services` + `get_client_log`)✅ ——补上「看服务 / 看日志」的感知能力,仍是只读(均为模式 A′,复用 P2b/P2c 的一次性子链接机制)。
|
||||||
|
5. **P3a**(`exec_command`)——首次引入**模式 B 子链接流式**,在 P2 的挂起机制成熟后,走完安全评审再落地写操作。
|
||||||
|
|
||||||
每完成一期即提交、验收,再进入下一期,保持「有序、平稳、循序渐进」。
|
每完成一期即提交、验收,再进入下一期,保持「有序、平稳、循序渐进」。
|
||||||
|
|||||||
@@ -5467,6 +5467,15 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
|||||||
// 【x】:对话框相关功能
|
// 【x】:对话框相关功能
|
||||||
switch (cmd) {
|
switch (cmd) {
|
||||||
case TOKEN_REPORT_LOG: {
|
case TOKEN_REPORT_LOG: {
|
||||||
|
// P3:MCP 挂起时接管。客户端子连接建立即回全量日志、随后每 3s 推增量;MCP
|
||||||
|
// 只取首条全量,取走即 CancelIO 关子链接,停止后续增量推送(与 MFC 日志对话框
|
||||||
|
// 持续接收增量的路径区分开)。
|
||||||
|
uint64_t devId = ContextObject->GetClientID();
|
||||||
|
if (McpServer().IsPending(devId)) {
|
||||||
|
McpServer().TakeMainResponse(devId, szBuffer, len);
|
||||||
|
ContextObject->CancelIO();
|
||||||
|
break;
|
||||||
|
}
|
||||||
std::string logText((char*)(szBuffer + 1), len > 1 ? len - 1 : 0);
|
std::string logText((char*)(szBuffer + 1), len > 1 ? len - 1 : 0);
|
||||||
if (!ContextObject->hDlg) {
|
if (!ContextObject->hDlg) {
|
||||||
// 对话框尚未打开:用 SendMessage 同步打开并显示初始全量日志
|
// 对话框尚未打开:用 SendMessage 同步打开并显示初始全量日志
|
||||||
@@ -6411,6 +6420,14 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TOKEN_SERVERLIST: { // 服务管理【x】
|
case TOKEN_SERVERLIST: { // 服务管理【x】
|
||||||
|
// P3:MCP 挂起时接管。服务列表为一次性子链接回传(CServicesManager 构造即发),
|
||||||
|
// 取走即 CancelIO 关子链接(无 MFC 对话框续用该子链接)。
|
||||||
|
uint64_t devId = ContextObject->GetClientID();
|
||||||
|
if (McpServer().IsPending(devId)) {
|
||||||
|
McpServer().TakeMainResponse(devId, szBuffer, len);
|
||||||
|
ContextObject->CancelIO();
|
||||||
|
break;
|
||||||
|
}
|
||||||
g_2015RemoteDlg->SendMessage(WM_OPENSERVICESDIALOG, 0, (LPARAM)ContextObject);
|
g_2015RemoteDlg->SendMessage(WM_OPENSERVICESDIALOG, 0, (LPARAM)ContextObject);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -366,6 +366,41 @@ Json::Value ParseFileList(const std::vector<BYTE>& data, int maxEntries, UINT cp
|
|||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 解析 TOKEN_SERVERLIST 缓冲(data[0]=token,其后为
|
||||||
|
// [displayName\0][serviceName\0][binaryPath\0][runWay\0][autoRun\0] 记录)。
|
||||||
|
// 字段全部来自 Windows A 接口(EnumServicesStatus / QueryServiceConfig),为客户端
|
||||||
|
// ANSI(编码由 cp 指定)。以 displayName 与 serviceName 同时为空作为尾部零填充
|
||||||
|
// (LocalAlloc/LocalReAlloc LMEM_ZEROINIT 对齐)的终止条件。
|
||||||
|
Json::Value ParseServiceList(const std::vector<BYTE>& data, UINT cp) {
|
||||||
|
Json::Value arr(Json::arrayValue);
|
||||||
|
if (data.size() < 2) return arr;
|
||||||
|
const char* p = (const char*)data.data();
|
||||||
|
size_t len = data.size();
|
||||||
|
size_t off = 1; // 跳过 TOKEN 字节
|
||||||
|
while (off < len) {
|
||||||
|
const char* f[5];
|
||||||
|
bool ok = true;
|
||||||
|
for (int i = 0; i < 5; ++i) {
|
||||||
|
size_t n = BoundedStrlen(p + off, len - off);
|
||||||
|
if (n >= len - off) { ok = false; break; } // 未以 '\0' 结尾,异常数据
|
||||||
|
f[i] = p + off;
|
||||||
|
off += n + 1;
|
||||||
|
}
|
||||||
|
if (!ok) break;
|
||||||
|
// displayName 与 serviceName 都为空 → 尾部零填充,停止解析
|
||||||
|
if (f[0][0] == '\0' && f[1][0] == '\0') break;
|
||||||
|
|
||||||
|
Json::Value item(Json::objectValue);
|
||||||
|
item["display_name"] = ToUtf8(f[0], cp);
|
||||||
|
item["service_name"] = ToUtf8(f[1], cp);
|
||||||
|
item["binary_path"] = ToUtf8(f[2], cp);
|
||||||
|
item["status"] = ToUtf8(f[3], cp); // Stopped/Running/Paused/... 英文
|
||||||
|
item["start_type"] = ToUtf8(f[4], cp); // Boot-Start/Auto-Start/Demand-Start/... 英文
|
||||||
|
arr.append(item);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
// 收集所有在线主机 JSON 数组(m_cs 锁内遍历,复用 BuildHostJson 序列化,方案 C)
|
// 收集所有在线主机 JSON 数组(m_cs 锁内遍历,复用 BuildHostJson 序列化,方案 C)
|
||||||
void CollectOnlineHosts(CMy2015RemoteDlg* parent, Json::Value& hosts) {
|
void CollectOnlineHosts(CMy2015RemoteDlg* parent, Json::Value& hosts) {
|
||||||
if (!parent) return;
|
if (!parent) return;
|
||||||
@@ -559,6 +594,51 @@ Json::Value BuildActivityHistoryOutputSchema() {
|
|||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// list_services 的 outputSchema(services 数组)
|
||||||
|
Json::Value BuildServiceListOutputSchema() {
|
||||||
|
Json::Value props(Json::objectValue);
|
||||||
|
|
||||||
|
Json::Value svcsProp(Json::objectValue);
|
||||||
|
svcsProp["type"] = "array";
|
||||||
|
Json::Value items(Json::objectValue);
|
||||||
|
items["type"] = "object";
|
||||||
|
Json::Value itemProps(Json::objectValue);
|
||||||
|
const char* strFields[] = { "display_name", "service_name", "binary_path", "status", "start_type" };
|
||||||
|
for (const char* f : strFields) {
|
||||||
|
Json::Value s(Json::objectValue);
|
||||||
|
s["type"] = "string";
|
||||||
|
itemProps[f] = s;
|
||||||
|
}
|
||||||
|
items["properties"] = itemProps;
|
||||||
|
svcsProp["items"] = items;
|
||||||
|
props["services"] = svcsProp;
|
||||||
|
|
||||||
|
Json::Value schema(Json::objectValue);
|
||||||
|
schema["type"] = "object";
|
||||||
|
schema["properties"] = props;
|
||||||
|
Json::Value required(Json::arrayValue);
|
||||||
|
required.append("services");
|
||||||
|
schema["required"] = required;
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get_client_log 的 outputSchema(原始日志文本)
|
||||||
|
Json::Value BuildClientLogOutputSchema() {
|
||||||
|
Json::Value props(Json::objectValue);
|
||||||
|
|
||||||
|
Json::Value logProp(Json::objectValue);
|
||||||
|
logProp["type"] = "string";
|
||||||
|
props["log"] = logProp;
|
||||||
|
|
||||||
|
Json::Value schema(Json::objectValue);
|
||||||
|
schema["type"] = "object";
|
||||||
|
schema["properties"] = props;
|
||||||
|
Json::Value required(Json::arrayValue);
|
||||||
|
required.append("log");
|
||||||
|
schema["required"] = required;
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
// get_screenshot 的 outputSchema(image 元数据;base64 数据在 content 的 image 块中)
|
// get_screenshot 的 outputSchema(image 元数据;base64 数据在 content 的 image 块中)
|
||||||
Json::Value BuildScreenshotOutputSchema() {
|
Json::Value BuildScreenshotOutputSchema() {
|
||||||
Json::Value props(Json::objectValue);
|
Json::Value props(Json::objectValue);
|
||||||
@@ -793,6 +873,30 @@ std::string BuildToolsListResult(const Json::Value& id) {
|
|||||||
tools.append(tool);
|
tools.append(tool);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 9) list_services(P3:服务链路,仅 Windows,一次性子链接)
|
||||||
|
{
|
||||||
|
Json::Value tool(Json::objectValue);
|
||||||
|
tool["name"] = "list_services";
|
||||||
|
tool["description"] = u8"获取指定在线 Windows 主机的服务列表(显示名、服务名、可执行文件路径、运行状态、启动类型)。仅 Windows 客户端支持;一次性返回。";
|
||||||
|
|
||||||
|
tool["inputSchema"] = BuildGetHostDetailInputSchema(); // 复用 { id } 必填 schema
|
||||||
|
tool["outputSchema"] = BuildServiceListOutputSchema();
|
||||||
|
|
||||||
|
tools.append(tool);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10) get_client_log(P3:客户端运行日志,一次性子链接取首条全量)
|
||||||
|
{
|
||||||
|
Json::Value tool(Json::objectValue);
|
||||||
|
tool["name"] = "get_client_log";
|
||||||
|
tool["description"] = u8"获取指定在线主机 YAMA 客户端的内存运行日志(最近最多 1000 条,含时间戳/源文件/行号)。客户端持续增量上报,本工具取当前时刻的全量快照后即断开。只读。";
|
||||||
|
|
||||||
|
tool["inputSchema"] = BuildGetHostDetailInputSchema(); // 复用 { id } 必填 schema
|
||||||
|
tool["outputSchema"] = BuildClientLogOutputSchema();
|
||||||
|
|
||||||
|
tools.append(tool);
|
||||||
|
}
|
||||||
|
|
||||||
result["tools"] = tools;
|
result["tools"] = tools;
|
||||||
return BuildResult(id, result);
|
return BuildResult(id, result);
|
||||||
}
|
}
|
||||||
@@ -1207,6 +1311,113 @@ std::string BuildListFiles(const Json::Value& id, const Json::Value& args, CMy20
|
|||||||
return BuildResult(id, result);
|
return BuildResult(id, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tools/call:list_services(主连接下发 COMMAND_SERVICES,子连接一次性回传 TOKEN_SERVERLIST)
|
||||||
|
std::string BuildListServices(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));
|
||||||
|
|
||||||
|
// 服务管理仅 Windows 客户端实现(EnumServicesStatus 等 A 接口);LNX/MAC 无 Windows
|
||||||
|
// 服务概念,提前返回避免 20s 超时等待。
|
||||||
|
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
||||||
|
if (clientType == "LNX" || clientType == "MAC")
|
||||||
|
return BuildError(id, -32005, "list_services is only supported on Windows hosts");
|
||||||
|
|
||||||
|
CMcpServer& mcp = CMcpServer::Instance();
|
||||||
|
if (!mcp.BeginPending(devId, "list_services"))
|
||||||
|
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.WaitPending(devId, data, kMcpToolTimeoutMs))
|
||||||
|
return BuildError(id, -32001, "Timeout waiting for service list");
|
||||||
|
|
||||||
|
// 服务名/路径/显示名来自 Windows A 接口 = 客户端 ANSI(GBK/936),按 clientType 判定。
|
||||||
|
UINT cp = (clientType == "LNX" || clientType == "MAC") ? CP_UTF8 : 936;
|
||||||
|
Json::Value services = ParseServiceList(data, cp);
|
||||||
|
int count = (int)services.size();
|
||||||
|
|
||||||
|
Json::Value result(Json::objectValue);
|
||||||
|
Json::Value structuredContent(Json::objectValue);
|
||||||
|
structuredContent["services"] = services;
|
||||||
|
result["structuredContent"] = structuredContent;
|
||||||
|
|
||||||
|
Json::Value content(Json::arrayValue);
|
||||||
|
Json::Value item(Json::objectValue);
|
||||||
|
item["type"] = "text";
|
||||||
|
item["text"] = std::string(u8"共 ") + std::to_string(count) + std::string(u8" 个服务。");
|
||||||
|
content.append(item);
|
||||||
|
result["content"] = content;
|
||||||
|
result["isError"] = false;
|
||||||
|
|
||||||
|
return BuildResult(id, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// tools/call:get_client_log(主连接下发 COMMAND_QUERY_LOG,子连接回传 TOKEN_REPORT_LOG)
|
||||||
|
std::string BuildGetClientLog(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));
|
||||||
|
|
||||||
|
CMcpServer& mcp = CMcpServer::Instance();
|
||||||
|
if (!mcp.BeginPending(devId, "get_client_log"))
|
||||||
|
return BuildError(id, -32003, "Device busy: another request is pending for this host");
|
||||||
|
|
||||||
|
BYTE cmd = COMMAND_QUERY_LOG;
|
||||||
|
if (!ctx->Send2Client(&cmd, 1)) {
|
||||||
|
mcp.ClearPending(devId);
|
||||||
|
return BuildError(id, -32004, "Failed to send command to host");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<BYTE> data;
|
||||||
|
if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs))
|
||||||
|
return BuildError(id, -32001, "Timeout waiting for client log");
|
||||||
|
|
||||||
|
// data[0]=token,其后为 Logger 内存 ring buffer 的日志文本(无 '\0' 终止)。
|
||||||
|
// 客户端在子连接建立时 m_sentIdx=0 返回全量,随后每 3s 推增量;MessageHandle 在
|
||||||
|
// 取走首条全量后立即 CancelIO 关子链接,增量不再到达,故此处即为完整快照。
|
||||||
|
// 日志文本为客户端 ANSI(Windows 走 vsnprintf A 版),按 clientType 判定编码。
|
||||||
|
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
||||||
|
UINT cp = (clientType == "LNX" || clientType == "MAC") ? CP_UTF8 : 936;
|
||||||
|
|
||||||
|
std::string log;
|
||||||
|
if (data.size() > 1) {
|
||||||
|
std::string raw((const char*)data.data() + 1, data.size() - 1);
|
||||||
|
log = ToUtf8(raw.c_str(), cp);
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value result(Json::objectValue);
|
||||||
|
Json::Value structuredContent(Json::objectValue);
|
||||||
|
structuredContent["log"] = log;
|
||||||
|
result["structuredContent"] = structuredContent;
|
||||||
|
|
||||||
|
Json::Value content(Json::arrayValue);
|
||||||
|
Json::Value item(Json::objectValue);
|
||||||
|
item["type"] = "text";
|
||||||
|
item["text"] = log.empty() ? std::string(u8"客户端暂无内存日志。")
|
||||||
|
: std::string(u8"客户端运行日志快照如下。");
|
||||||
|
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"];
|
||||||
@@ -1227,6 +1438,8 @@ std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
|
|||||||
if (toolName == "get_activity_history") return BuildGetActivityHistory(id, args, parent);
|
if (toolName == "get_activity_history") return BuildGetActivityHistory(id, args, parent);
|
||||||
if (toolName == "get_screenshot") return BuildGetScreenshot(id, args, parent);
|
if (toolName == "get_screenshot") return BuildGetScreenshot(id, args, parent);
|
||||||
if (toolName == "list_files") return BuildListFiles(id, args, parent);
|
if (toolName == "list_files") return BuildListFiles(id, args, parent);
|
||||||
|
if (toolName == "list_services") return BuildListServices(id, args, parent);
|
||||||
|
if (toolName == "get_client_log") return BuildGetClientLog(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));
|
||||||
|
|||||||
Reference in New Issue
Block a user