Files
SimpleRemoter/server/2015Remote/McpServer.cpp
yuanyuanxiang 5611ba621c 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
2026-08-19 14:28:10 +02:00

1676 lines
64 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "stdafx.h"
#include "McpServer.h"
#include "jsoncpp/json.h"
#include "HostJson.h" // BuildHostJson单台主机序列化公共函数
#include "context.h" // context 接口
#include "2015RemoteDlg.h" // CMy2015RemoteDlg 成员m_HostList/m_cs/m_ClientMap+ VERSION_STR
#include <sstream>
#ifndef _WIN64
#ifdef _DEBUG
#pragma comment(lib, "jsoncpp/jsoncppd.lib")
#else
#pragma comment(lib, "jsoncpp/jsoncpp.lib")
#endif
#else
#ifdef _DEBUG
#pragma comment(lib, "jsoncpp/jsoncpp_x64d.lib")
#else
#pragma comment(lib, "jsoncpp/jsoncpp_x64.lib")
#endif
#endif
namespace {
// P2b 工具等待响应的超时ms。MCP 一次性请求:等待子连接回传进程/窗口列表。
static const int kMcpToolTimeoutMs = 20000;
// Json::Value → 紧凑 JSON 字符串
std::string JsonToString(const Json::Value& v) {
Json::StreamWriterBuilder b;
b["indentation"] = "";
return Json::writeString(b, v);
}
// JSON-RPC 2.0 成功响应
std::string BuildResult(const Json::Value& id, const Json::Value& result) {
Json::Value resp(Json::objectValue);
resp["jsonrpc"] = "2.0";
resp["id"] = id;
resp["result"] = result;
return JsonToString(resp);
}
// JSON-RPC 2.0 错误响应
std::string BuildError(const Json::Value& id, int code, const std::string& msg) {
Json::Value resp(Json::objectValue);
resp["jsonrpc"] = "2.0";
resp["id"] = id;
Json::Value err(Json::objectValue);
err["code"] = code;
err["message"] = msg;
resp["error"] = err;
return JsonToString(resp);
}
// initialize 握手MCP 规范protocolVersion + capabilities + serverInfo
std::string BuildInitializeResult(const Json::Value& id) {
Json::Value result(Json::objectValue);
result["protocolVersion"] = "2025-06-18";
Json::Value caps(Json::objectValue);
caps["tools"] = Json::Value(Json::objectValue);
result["capabilities"] = caps;
Json::Value serverInfo(Json::objectValue);
serverInfo["name"] = "yama";
serverInfo["version"] = VERSION_STR;
result["serverInfo"] = serverInfo;
return BuildResult(id, result);
}
// ping 健康检查:返回空 result
std::string BuildPingResult(const Json::Value& id) {
return BuildResult(id, Json::Value(Json::objectValue));
}
// ========== P2a 通用辅助 ==========
// 小写化(仅 ASCIIUTF-8 多字节原样保留):用于不区分大小写的子串匹配
std::string ToLowerAscii(const std::string& s) {
std::string r = s;
for (char& c : r) if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
return r;
}
// 不区分大小写的子串匹配
bool ContainsCI(const std::string& haystack, const std::string& needle) {
if (needle.empty()) return true;
return ToLowerAscii(haystack).find(ToLowerAscii(needle)) != std::string::npos;
}
// 取对象的字符串字段,缺失/非字符串返回 ""
std::string JsonStrField(const Json::Value& v, const char* key) {
if (v.isObject() && v.isMember(key) && v[key].isString())
return v[key].asString();
return "";
}
// 是否纯数字host id 为 uint64 十进制字符串)
bool IsDigits(const std::string& s) {
if (s.empty()) return false;
for (char c : s) if (c < '0' || c > '9') return false;
return true;
}
// 读取 tools/call 的入参MCP 规范params.arguments 为工具入参对象)
Json::Value GetCallArguments(const Json::Value& params) {
if (params.isObject() && params.isMember("arguments") && params["arguments"].isObject())
return params["arguments"];
return Json::Value(Json::objectValue);
}
// 读取可选字符串入参,缺失返回 ""
std::string GetStringArg(const Json::Value& args, const char* key) {
return JsonStrField(args, key);
}
// ========== P2b 辅助 ==========
// 解析 id 入参(必填、纯数字)为 uint64非法返回 false。
bool ParseHostIdArg(const Json::Value& args, uint64_t& out, std::string& err) {
std::string sid = GetStringArg(args, "id");
if (sid.empty()) {
err = "Missing required parameter: id";
return false;
}
if (!IsDigits(sid)) {
err = "Invalid id: expected a decimal host id string";
return false;
}
out = strtoull(sid.c_str(), nullptr, 10);
return true;
}
// 在 m_HostList 中按 clientID 找在线主 context复刻 CollectOnlineHosts 的锁内遍历)。
context* FindMainContext(CMy2015RemoteDlg* parent, uint64_t id) {
if (!parent) return nullptr;
context* found = nullptr;
EnterCriticalSection(&parent->m_cs);
for (context* ctx : parent->m_HostList) {
if (ctx && ctx->GetClientID() == id && ctx->IsLogin()) {
found = ctx;
break;
}
}
LeaveCriticalSection(&parent->m_cs);
return found;
}
// 有界 strlenp 最多可读 avail 字节,返回 n < avail 表示遇到 '\0'。
size_t BoundedStrlen(const char* p, size_t avail) {
size_t n = 0;
while (n < avail && p[n]) ++n;
return n;
}
// 从源编码 cp 转为 UTF-8输出到 JSON。空/失败返回 ""。
std::string ToUtf8(const char* s, UINT cp) {
if (!s || !*s) return "";
int wlen = MultiByteToWideChar(cp, 0, s, -1, NULL, 0);
if (wlen <= 0) return "";
std::wstring w(wlen - 1, L'\0');
MultiByteToWideChar(cp, 0, s, -1, &w[0], wlen);
int u8len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, NULL, 0, NULL, NULL);
if (u8len <= 0) return "";
std::string out(u8len - 1, '\0');
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, &out[0], u8len, NULL, NULL);
return out;
}
// 从 UTF-8 转为目标编码 cp文件链路的目录路径在 Windows 走 ANSI与 FileManagerDlg 的
// CString 一致cp 按 clientType 判定Windows=936、LNX/MAC=CP_UTF8见 BuildListFiles
// 非 UTF-8如纯 ASCII失败时原样返回。
std::string ToAnsi(const std::string& utf8, UINT cp) {
if (utf8.empty()) return "";
int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), (int)utf8.size(), NULL, 0);
if (wlen <= 0) return utf8;
std::wstring w(wlen, L'\0');
MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), (int)utf8.size(), &w[0], wlen);
int alen = WideCharToMultiByte(cp, 0, w.c_str(), wlen, NULL, 0, NULL, NULL);
if (alen <= 0) return utf8;
std::string out(alen, '\0');
WideCharToMultiByte(cp, 0, w.c_str(), wlen, &out[0], alen, NULL, NULL);
return out;
}
// 从 "title|status|pid|r1|r2" 解析(自末尾 4 个 '|' 反推),兼容只有标题的老客户端。
// 逻辑复刻 SystemDlg.cpp::ParseWindowAttrs。
void ParseWindowAttrsStr(const char* s, std::string& title, std::string& status, uint64_t& pid) {
title.clear();
status = "normal";
pid = 0;
if (!s || !*s) return;
std::string t(s);
int len = (int)t.size();
int pipePos[4] = { -1, -1, -1, -1 };
int cnt = 0;
for (int i = len - 1; i >= 0 && cnt < 4; --i) {
if (t[i] == '|') pipePos[cnt++] = i;
}
if (cnt < 4) { // 老格式:只有标题
title = t;
return;
}
pid = strtoull(t.c_str() + pipePos[2] + 1, nullptr, 10);
status = t.substr(pipePos[3] + 1, pipePos[2] - pipePos[3] - 1);
title = t.substr(0, pipePos[3]);
}
// 解析 TOKEN_PSLIST 缓冲data[0]=token其后为 [pid:4][name:arch\0][path\0] 记录)。
// 进程名/路径为客户端 ANSI编码由 cp 指定),转 UTF-8 输出。
Json::Value ParseProcessList(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 + sizeof(DWORD) <= len) {
DWORD pid = *(const DWORD*)(p + off);
off += sizeof(DWORD);
const char* exeFile = p + off;
size_t exeLen = BoundedStrlen(exeFile, len - off);
if (exeLen >= len - off) break; // 未以 '\0' 结尾,异常数据
if (exeLen == 0) break; // 空进程名 = 尾部零填充LocalSize 对齐),停止解析
off += exeLen + 1;
const char* fullPath = p + off;
size_t pathLen = BoundedStrlen(fullPath, len - off);
if (pathLen >= len - off) break;
off += pathLen + 1;
std::string name(exeFile, exeLen);
std::string arch;
size_t colon = name.find(':');
if (colon != std::string::npos) {
arch = name.substr(colon + 1);
name = name.substr(0, colon);
}
Json::Value item(Json::objectValue);
item["pid"] = (Json::UInt64)pid;
item["name"] = ToUtf8(name.c_str(), cp);
item["arch"] = arch.empty() ? "N/A" : ToUtf8(arch.c_str(), cp);
item["path"] = ToUtf8(fullPath, cp);
arr.append(item);
}
return arr;
}
// 解析 TOKEN_WSLIST 缓冲data[0]=token其后为 [hwnd:4][title|status|pid|r1|r2\0] 记录)。
// 窗口标题为客户端 UTF-8老客户端为 CP_ACP用 cp 解码后转 UTF-8 输出。
Json::Value ParseWindowList(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;
while (off + sizeof(DWORD) <= len) {
DWORD hwnd = *(const DWORD*)(p + off);
off += sizeof(DWORD);
const char* titleWithAttrs = p + off;
size_t tlen = BoundedStrlen(titleWithAttrs, len - off);
if (tlen >= len - off) break;
if (tlen == 0) break; // 空记录 = 尾部零填充LocalSize 对齐),停止解析
off += tlen + 1;
std::string title, status;
uint64_t pid = 0;
ParseWindowAttrsStr(titleWithAttrs, title, status, pid);
Json::Value item(Json::objectValue);
item["hwnd"] = (Json::UInt64)hwnd;
item["title"] = ToUtf8(title.c_str(), cp);
item["status"] = status;
item["pid"] = (Json::UInt64)pid;
arr.append(item);
}
return arr;
}
// 解析 TOKEN_DRIVE_LIST 缓冲data[0]=token其后为
// [letter:1][GetDriveType:1][totalMB:4][freeMB:4][typeName\0][fileSystem\0] 记录,
// 以 letter=='\0' 终止。typeName/fileSystem 为客户端 ANSI编码由 cp 指定)。
Json::Value ParseDriveList(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 + 10 <= len && p[off] != '\0') {
char letter = p[off];
unsigned int type = (unsigned int)(unsigned char)p[off + 1];
DWORD totalMB = *(const DWORD*)(p + off + 2);
DWORD freeMB = *(const DWORD*)(p + off + 6);
off += 10;
const char* typeName = p + off;
size_t tlen = BoundedStrlen(typeName, len - off);
if (tlen >= len - off) break;
off += tlen + 1;
const char* fileSystem = p + off;
size_t flen = BoundedStrlen(fileSystem, len - off);
if (flen >= len - off) break;
off += flen + 1;
std::string drive;
drive += letter;
drive += ":\\";
Json::Value item(Json::objectValue);
item["drive"] = drive;
item["type"] = (Json::UInt64)type;
item["typeName"] = ToUtf8(typeName, cp);
item["fileSystem"] = ToUtf8(fileSystem, cp);
item["totalMB"] = (Json::UInt64)totalMB;
item["freeMB"] = (Json::UInt64)freeMB;
arr.append(item);
}
return arr;
}
// 解析 TOKEN_FILE_LIST 缓冲data[0]=token其后为
// [attr:1][filename\0][sizeHigh:4][sizeLow:4][ftLastWriteTime:8] 记录)。
// 文件名为客户端 ANSI编码由 cp 指定attr 非 0 表示目录FILE_ATTRIBUTE_DIRECTORY
Json::Value ParseFileList(const std::vector<BYTE>& data, int maxEntries, 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;
int count = 0;
while (off + 1 <= len && count < maxEntries) {
bool isDir = (p[off] & FILE_ATTRIBUTE_DIRECTORY) != 0;
off += 1;
const char* name = p + off;
size_t nlen = BoundedStrlen(name, len - off);
if (nlen >= len - off) break;
if (nlen == 0) break; // 空记录 = 尾部零填充LocalAlloc 对齐),停止解析
off += nlen + 1;
if (off + 16 > len) break;
DWORD sizeHigh = *(const DWORD*)(p + off);
DWORD sizeLow = *(const DWORD*)(p + off + 4);
ULONGLONG size = ((ULONGLONG)sizeHigh << 32) | sizeLow;
// 修改时间FILETIME100ns since 1601-01-01→ Unix 秒
// FILETIME 在线上是 dwLowDateTime(低 4 字节) 在前、dwHighDateTime(高 4 字节) 在后
// (客户端 memcpy(&ftLastWriteTime, sizeof(FILETIME)),见 FileManager.cpp::SendFilesList
DWORD ftLow = *(const DWORD*)(p + off + 8);
DWORD ftHigh = *(const DWORD*)(p + off + 12);
off += 16;
ULONGLONG ft = ((ULONGLONG)ftHigh << 32) | ftLow;
Json::Int64 mtime = (Json::Int64)(ft / 10000000ULL) - 11644473600LL;
Json::Value item(Json::objectValue);
item["name"] = ToUtf8(name, cp);
item["isDir"] = isDir;
item["size"] = (Json::UInt64)size;
item["mtime"] = mtime;
arr.append(item);
++count;
}
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
void CollectOnlineHosts(CMy2015RemoteDlg* parent, Json::Value& hosts) {
if (!parent) return;
EnterCriticalSection(&parent->m_cs);
for (context* ctx : parent->m_HostList) {
if (!ctx || !ctx->IsLogin()) continue;
hosts.append(BuildHostJson(ctx, parent->m_ClientMap));
}
LeaveCriticalSection(&parent->m_cs);
}
// ========== 工具 schema ==========
// 单台主机字段 schemahosts 数组元素 / 单机详情共用的形状)
Json::Value BuildHostItemSchema() {
Json::Value itemProps(Json::objectValue);
const char* strFields[] = {
"id", "name", "remark", "ip", "os", "location", "rtt",
"version", "activeWindow", "group", "screen", "clientType"
};
for (const char* f : strFields) {
Json::Value p(Json::objectValue);
p["type"] = "string";
itemProps[f] = p;
}
Json::Value onlineProp(Json::objectValue);
onlineProp["type"] = "boolean";
itemProps["online"] = onlineProp;
return itemProps;
}
// list_online_hosts / search_hosts 的 outputSchemahosts 数组)
Json::Value BuildHostOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value hostsProp(Json::objectValue);
hostsProp["type"] = "array";
Json::Value items(Json::objectValue);
items["type"] = "object";
items["properties"] = BuildHostItemSchema();
hostsProp["items"] = items;
props["hosts"] = hostsProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("hosts");
schema["required"] = required;
return schema;
}
// get_host_detail 的 outputSchema单台主机
Json::Value BuildHostDetailOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value hostProp(Json::objectValue);
hostProp["type"] = "object";
hostProp["properties"] = BuildHostItemSchema();
props["host"] = hostProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("host");
schema["required"] = required;
return schema;
}
// search_hosts 的 inputSchema全部可选
Json::Value BuildSearchHostsInputSchema() {
Json::Value props(Json::objectValue);
const char* strParams[] = { "name", "ip", "group", "os" };
for (const char* p : strParams) {
Json::Value s(Json::objectValue);
s["type"] = "string";
props[p] = s;
}
Json::Value onlineProp(Json::objectValue);
onlineProp["type"] = "boolean";
props["online"] = onlineProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
return schema;
}
// get_host_detail 的 inputSchemaid 必填)
Json::Value BuildGetHostDetailInputSchema() {
Json::Value props(Json::objectValue);
Json::Value idProp(Json::objectValue);
idProp["type"] = "string";
idProp["description"] = u8"主机 id取 list_online_hosts / search_hosts 返回的 id 字段";
props["id"] = idProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("id");
schema["required"] = required;
return schema;
}
// list_processes 的 outputSchemaprocesses 数组)
Json::Value BuildProcessListOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value procsProp(Json::objectValue);
procsProp["type"] = "array";
Json::Value items(Json::objectValue);
items["type"] = "object";
Json::Value itemProps(Json::objectValue);
Json::Value pidProp(Json::objectValue);
pidProp["type"] = "integer";
itemProps["pid"] = pidProp;
const char* strFields[] = { "name", "arch", "path" };
for (const char* f : strFields) {
Json::Value s(Json::objectValue);
s["type"] = "string";
itemProps[f] = s;
}
items["properties"] = itemProps;
procsProp["items"] = items;
props["processes"] = procsProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("processes");
schema["required"] = required;
return schema;
}
// list_windows 的 outputSchemawindows 数组)
Json::Value BuildWindowListOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value winsProp(Json::objectValue);
winsProp["type"] = "array";
Json::Value items(Json::objectValue);
items["type"] = "object";
Json::Value itemProps(Json::objectValue);
Json::Value hwndProp(Json::objectValue);
hwndProp["type"] = "integer";
itemProps["hwnd"] = hwndProp;
Json::Value pidProp(Json::objectValue);
pidProp["type"] = "integer";
itemProps["pid"] = pidProp;
const char* strFields[] = { "title", "status" };
for (const char* f : strFields) {
Json::Value s(Json::objectValue);
s["type"] = "string";
itemProps[f] = s;
}
items["properties"] = itemProps;
winsProp["items"] = items;
props["windows"] = winsProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("windows");
schema["required"] = required;
return schema;
}
// get_activity_history 的 outputSchemarecords 数组 + 原始文本)
Json::Value BuildActivityHistoryOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value recordsProp(Json::objectValue);
recordsProp["type"] = "array";
Json::Value items(Json::objectValue);
items["type"] = "string";
recordsProp["items"] = items;
props["records"] = recordsProp;
Json::Value rawProp(Json::objectValue);
rawProp["type"] = "string";
props["activityHistory"] = rawProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
return schema;
}
// list_services 的 outputSchemaservices 数组)
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 的 outputSchemaimage 元数据base64 数据在 content 的 image 块中)
Json::Value BuildScreenshotOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value imageProp(Json::objectValue);
imageProp["type"] = "object";
Json::Value imageProps(Json::objectValue);
Json::Value mimeProp(Json::objectValue);
mimeProp["type"] = "string";
imageProps["mimeType"] = mimeProp;
const char* intFields[] = { "width", "height", "bytes" };
for (const char* f : intFields) {
Json::Value s(Json::objectValue);
s["type"] = "integer";
imageProps[f] = s;
}
imageProp["properties"] = imageProps;
props["image"] = imageProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("image");
schema["required"] = required;
return schema;
}
// get_screenshot 的 inputSchemaid 必填max_width 可选)
Json::Value BuildGetScreenshotInputSchema() {
Json::Value props(Json::objectValue);
Json::Value idProp(Json::objectValue);
idProp["type"] = "string";
idProp["description"] = u8"主机 id取 list_online_hosts / search_hosts 返回的 id 字段";
props["id"] = idProp;
Json::Value mwProp(Json::objectValue);
mwProp["type"] = "integer";
mwProp["description"] = u8"期望图片最大宽度(像素,钳制到 64~1920。省略或传 0 时沿用 RTT 自适应缩略图档位(最大 1024传 1920 可拿到接近原分辨率1080p 源屏即原分辨率4K 源屏最多 1920供 AI 视觉/OCR 场景提升清晰度。";
props["max_width"] = mwProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("id");
schema["required"] = required;
return schema;
}
// list_files 的 inputSchemaid 必填path 可选path 缺省/空 = 列盘)
Json::Value BuildListFilesInputSchema() {
Json::Value props(Json::objectValue);
Json::Value idProp(Json::objectValue);
idProp["type"] = "string";
idProp["description"] = u8"主机 id取 list_online_hosts / search_hosts 返回的 id 字段";
props["id"] = idProp;
Json::Value pathProp(Json::objectValue);
pathProp["type"] = "string";
pathProp["description"] = u8"要列举的目录路径(如 C:\\Windows省略或传空则返回驱动器列表";
props["path"] = pathProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("id");
schema["required"] = required;
return schema;
}
// list_files 的 outputSchemadrives 数组 / files 数组,二者其一非空)
Json::Value BuildListFilesOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value drivesProp(Json::objectValue);
drivesProp["type"] = "array";
Json::Value driveItems(Json::objectValue);
driveItems["type"] = "object";
Json::Value driveProps(Json::objectValue);
const char* driveStr[] = { "drive", "typeName", "fileSystem" };
for (const char* f : driveStr) {
Json::Value s(Json::objectValue);
s["type"] = "string";
driveProps[f] = s;
}
const char* driveInt[] = { "type", "totalMB", "freeMB" };
for (const char* f : driveInt) {
Json::Value s(Json::objectValue);
s["type"] = "integer";
driveProps[f] = s;
}
driveItems["properties"] = driveProps;
drivesProp["items"] = driveItems;
props["drives"] = drivesProp;
Json::Value filesProp(Json::objectValue);
filesProp["type"] = "array";
Json::Value fileItems(Json::objectValue);
fileItems["type"] = "object";
Json::Value fileProps(Json::objectValue);
Json::Value nameProp(Json::objectValue);
nameProp["type"] = "string";
fileProps["name"] = nameProp;
Json::Value isDirProp(Json::objectValue);
isDirProp["type"] = "boolean";
fileProps["isDir"] = isDirProp;
const char* fileInt[] = { "size", "mtime" };
for (const char* f : fileInt) {
Json::Value s(Json::objectValue);
s["type"] = "integer";
fileProps[f] = s;
}
fileItems["properties"] = fileProps;
filesProp["items"] = fileItems;
props["files"] = filesProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
return schema;
}
// tools/list
std::string BuildToolsListResult(const Json::Value& id) {
Json::Value result(Json::objectValue);
Json::Value tools(Json::arrayValue);
// 1) list_online_hosts
{
Json::Value tool(Json::objectValue);
tool["name"] = "list_online_hosts";
// 说明文字为 UTF-8项目 /execution-charset:.936 会把普通窄字面量编译成 GBK
// 故用 u8 前缀确保输出到 JSON 的字节是 UTF-8。
tool["description"] = u8"获取当前所有在线主机的列表包含计算机名、IP、操作系统、版本、备注、分组、活动窗口、延迟等实时信息。";
Json::Value inputSchema(Json::objectValue);
inputSchema["type"] = "object";
inputSchema["properties"] = Json::Value(Json::objectValue);
inputSchema["required"] = Json::Value(Json::arrayValue);
tool["inputSchema"] = inputSchema;
tool["outputSchema"] = BuildHostOutputSchema();
tools.append(tool);
}
// 2) search_hostsP2a纯内存过滤无子链接
{
Json::Value tool(Json::objectValue);
tool["name"] = "search_hosts";
tool["description"] = u8"按计算机名/备注、IP、分组、操作系统过滤在线主机。所有条件均可选、按 AND 组合子串匹配ASCII 不区分大小写)。只返回在线主机。";
tool["inputSchema"] = BuildSearchHostsInputSchema();
tool["outputSchema"] = BuildHostOutputSchema();
tools.append(tool);
}
// 3) get_host_detailP2a单机详情纯内存
{
Json::Value tool(Json::objectValue);
tool["name"] = "get_host_detail";
tool["description"] = u8"获取单台在线主机的详细信息id、计算机名、IP、操作系统、备注、分组、活动窗口、屏幕分辨率、客户端类型等";
tool["inputSchema"] = BuildGetHostDetailInputSchema();
tool["outputSchema"] = BuildHostDetailOutputSchema();
tools.append(tool);
}
// 4) list_processesP2b主连接 RPC进程列表
{
Json::Value tool(Json::objectValue);
tool["name"] = "list_processes";
tool["description"] = u8"获取指定在线主机的进程列表PID、映像名称、架构、程序完整路径。通过主连接下发命令、子连接回传一次性返回。";
tool["inputSchema"] = BuildGetHostDetailInputSchema(); // 复用 { id } 必填 schema
tool["outputSchema"] = BuildProcessListOutputSchema();
tools.append(tool);
}
// 5) list_windowsP2b主连接 RPC窗口列表
{
Json::Value tool(Json::objectValue);
tool["name"] = "list_windows";
tool["description"] = u8"获取指定在线主机的顶层窗口列表(句柄、窗口标题、窗口状态、所属进程 PID。一次性返回。";
tool["inputSchema"] = BuildGetHostDetailInputSchema(); // 复用 { id } 必填 schema
tool["outputSchema"] = BuildWindowListOutputSchema();
tools.append(tool);
}
// 6) get_activity_historyP2b主连接 RPC历史活动记录
{
Json::Value tool(Json::objectValue);
tool["name"] = "get_activity_history";
tool["description"] = u8"获取指定在线主机的历史活动记录(前台窗口驻留时长,每行一条「[时间] [标题] 时长」)。主连接 RPC、一次性返回。";
tool["inputSchema"] = BuildGetHostDetailInputSchema(); // 复用 { id } 必填 schema
tool["outputSchema"] = BuildActivityHistoryOutputSchema();
tools.append(tool);
}
// 7) get_screenshotP2c屏幕预览链路主连接 RPC
{
Json::Value tool(Json::objectValue);
tool["name"] = "get_screenshot";
tool["description"] = u8"截取指定在线主机的一帧屏幕,返回 base64 编码的 JPEG 图片image 对象含 mimeType/data/width/height。仅 Windows 客户端支持(能力位 CLIENT_CAP_SCREEN_PREVIEW主连接 RPC、一次性返回。";
tool["inputSchema"] = BuildGetScreenshotInputSchema(); // { id } 必填 + max_width 可选
tool["outputSchema"] = BuildScreenshotOutputSchema();
tools.append(tool);
}
// 8) list_filesP2c文件链路列盘/列目录)
{
Json::Value tool(Json::objectValue);
tool["name"] = "list_files";
tool["description"] = u8"列举指定在线主机的目录。省略或空 path 返回驱动器列表drives给定 path 返回该目录一层内的文件/子目录files最多 500 条,含 name/isDir/size/mtime。只读。";
tool["inputSchema"] = BuildListFilesInputSchema();
tool["outputSchema"] = BuildListFilesOutputSchema();
tools.append(tool);
}
// 9) list_servicesP3服务链路仅 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_logP3客户端运行日志一次性子链接取首条全量
{
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;
return BuildResult(id, result);
}
// tools/calllist_online_hosts
std::string BuildListOnlineHosts(const Json::Value& id, CMy2015RemoteDlg* parent) {
Json::Value hosts(Json::arrayValue);
CollectOnlineHosts(parent, hosts);
int count = (int)hosts.size();
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["hosts"] = hosts;
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/callsearch_hosts
std::string BuildSearchHosts(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
Json::Value all(Json::arrayValue);
CollectOnlineHosts(parent, all);
std::string fName = GetStringArg(args, "name");
std::string fIp = GetStringArg(args, "ip");
std::string fGroup = GetStringArg(args, "group");
std::string fOs = GetStringArg(args, "os");
bool hasOnline = args.isObject() && args.isMember("online") && args["online"].isBool();
bool wantOnline = hasOnline ? args["online"].asBool() : true;
Json::Value hosts(Json::arrayValue);
// 列表只含在线主机:显式 online=false 时直接空结果
if (!hasOnline || wantOnline) {
for (unsigned int i = 0; i < all.size(); ++i) {
const Json::Value& h = all[i];
if (!fName.empty()) {
std::string name = JsonStrField(h, "name");
std::string remark = JsonStrField(h, "remark");
if (!ContainsCI(name, fName) && !ContainsCI(remark, fName)) continue;
}
if (!fIp.empty() && !ContainsCI(JsonStrField(h, "ip"), fIp)) continue;
if (!fGroup.empty() && !ContainsCI(JsonStrField(h, "group"), fGroup)) continue;
if (!fOs.empty() && !ContainsCI(JsonStrField(h, "os"), fOs)) continue;
hosts.append(h);
}
}
int count = (int)hosts.size();
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["hosts"] = hosts;
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/callget_host_detail
std::string BuildGetHostDetail(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
std::string sid = GetStringArg(args, "id");
if (sid.empty()) {
return BuildError(id, -32602, "Missing required parameter: id");
}
if (!IsDigits(sid)) {
return BuildError(id, -32602, "Invalid id: expected a decimal host id string");
}
Json::Value all(Json::arrayValue);
CollectOnlineHosts(parent, all);
for (unsigned int i = 0; i < all.size(); ++i) {
const Json::Value& h = all[i];
if (JsonStrField(h, "id") == sid) {
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["host"] = h;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
std::string name = JsonStrField(h, "name");
std::string ip = JsonStrField(h, "ip");
item["text"] = std::string(u8"主机 ") + name + " (" + ip + ")" + u8" 的详情。";
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
}
return BuildError(id, -32002, "Host not found or offline: " + sid);
}
// tools/calllist_processes主连接 RPC子连接一次性回传
std::string BuildListProcesses(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, "list_processes"))
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.WaitPending(devId, data, kMcpToolTimeoutMs))
return BuildError(id, -32001, "Timeout waiting for process list");
// 进程名/路径编码按 clientType 判定Windows 走 A 接口QueryFullProcessImageNameA 等)
// =客户端 ANSIGBK/936不随 CLIENT_CAP_UTF8 转 UTF-8LNX/MAC 的 /proc 天然 UTF-8。
// 不能用 GetClientEncoding——它按能力位返回 CP_UTF8会误解 Windows 客户端的 GBK 进程名。
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
UINT cp = (clientType == "LNX" || clientType == "MAC") ? CP_UTF8 : 936;
Json::Value procs = ParseProcessList(data, cp);
int count = (int)procs.size();
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["processes"] = procs;
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/calllist_windows主连接 RPC子连接一次性回传
std::string BuildListWindows(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, "list_windows"))
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.WaitPending(devId, data, kMcpToolTimeoutMs))
return BuildError(id, -32001, "Timeout waiting for window list");
// 窗口标题编码由客户端能力位决定(新客户端 UTF-8老客户端 CP_ACP
UINT cp = GetClientEncoding(ctx);
Json::Value wins = ParseWindowList(data, cp);
int count = (int)wins.size();
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["windows"] = wins;
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/callget_activity_history主连接 RPC无子链接、不弹框、不 CancelIO
std::string BuildGetActivityHistory(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_activity_history"))
return BuildError(id, -32003, "Device busy: another request is pending for this host");
BYTE cmd = COMMAND_QUERY_ACTIVITY;
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 activity history");
// data[0]=token其后为客户端 ActivityHistory::Dump() 的 UTF-8 纯文本
// (标题在客户端已由 GetActiveWindowTitle 转 UTF-8服务端无需再转码
std::string text;
if (data.size() > 1)
text.assign((const char*)data.data() + 1, data.size() - 1);
// 拆行:每行一条记录,过滤空行(含 Dump 末尾的换行)。
Json::Value records(Json::arrayValue);
size_t start = 0;
while (start <= text.size()) {
size_t nl = text.find('\n', start);
std::string line = text.substr(start, (nl == std::string::npos ? text.size() : nl) - start);
if (!line.empty()) records.append(line);
if (nl == std::string::npos) break;
start = nl + 1;
}
int count = (int)records.size();
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["records"] = records;
structuredContent["activityHistory"] = text;
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/callget_screenshot屏幕预览链路主连接 RPC不建子链接、不弹框
std::string BuildGetScreenshot(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 客户端声明 CLIENT_CAP_SCREEN_PREVIEW非 Windows 返回明确错误)
if (!ctx->SupportsScreenPreview())
return BuildError(id, -32005, "Host does not support screen preview");
CMcpServer& mcp = CMcpServer::Instance();
if (!mcp.BeginPending(devId, "get_screenshot"))
return BuildError(id, -32003, "Device busy: another request is pending for this host");
uint16_t reqId = mcp.NextPreviewReqId();
mcp.SetPendingReqId(devId, reqId);
// 复用 MFC 预览的 RTT/FRP 自适应参数挑选,避免重复实现 GetTargetQualityLevel 逻辑
WORD maxWidth = 0;
BYTE quality = 0;
parent->ChooseScreenPreviewParams(ctx, maxWidth, quality);
// 可选 max_width缺省/0 沿用缩略图档位;>0 覆盖宽度(钳制到客户端上限 [64,1920]
// 供 AI 视觉/OCR 场景请求接近原分辨率的大图。jpegQuality 仍沿用档位自适应值。
if (args.isMember("max_width") && args["max_width"].isInt()) {
int mw = args["max_width"].asInt();
if (mw > 0) {
if (mw < 64) mw = 64;
if (mw > 1920) mw = 1920;
maxWidth = (WORD)mw;
}
}
parent->SendScreenPreviewRequest(ctx, reqId, maxWidth, quality);
std::vector<BYTE> data;
if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs))
return BuildError(id, -32001, "Timeout waiting for screenshot");
// data = [ScreenPreviewRspHeader][JPEG]
if (data.size() < sizeof(ScreenPreviewRspHeader))
return BuildError(id, -32000, "Invalid screenshot response");
const ScreenPreviewRspHeader* hdr = reinterpret_cast<const ScreenPreviewRspHeader*>(data.data());
if (hdr->status != SCREEN_PREVIEW_OK || hdr->format != SCREEN_PREVIEW_FMT_JPEG ||
hdr->bytes == 0 || data.size() < sizeof(ScreenPreviewRspHeader) + hdr->bytes) {
return BuildError(id, -32000,
"Screenshot capture failed (status " + std::to_string((int)hdr->status) + ")");
}
std::string b64 = httplib::detail::base64_encode(
std::string((const char*)data.data() + sizeof(ScreenPreviewRspHeader), hdr->bytes));
Json::Value image(Json::objectValue);
image["mimeType"] = "image/jpeg";
image["width"] = (Json::UInt64)hdr->width;
image["height"] = (Json::UInt64)hdr->height;
image["bytes"] = (Json::UInt64)hdr->bytes;
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["image"] = image; // 元数据(不含 base64避免在 content 之外重复大 payload
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "image";
item["data"] = b64;
item["mimeType"] = "image/jpeg";
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// tools/calllist_files列盘走 COMMAND_LIST_DRIVE→TOKEN_DRIVE_LIST
// 列目录走 COMMAND_LIST_DRIVE 开子链接后,再下发 COMMAND_LIST_FILES→TOKEN_FILE_LIST
std::string BuildListFiles(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));
std::string path = GetStringArg(args, "path");
bool listDrives = path.empty() || path == "." || path == "/" || path == "\\";
// 目录路径/文件名编码按 clientType 判定Windows 走 A 接口FindFirstFileA 等)= 客户端
// ANSI(GBK/936),不随 CLIENT_CAP_UTF8 转 UTF-8LNX/MAC 文件系统天然 UTF-8。不能用
// GetClientEncoding——它按能力位返回 CP_UTF8会误解 Windows 客户端的 GBK 文件名。
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
UINT cp = (clientType == "LNX" || clientType == "MAC") ? CP_UTF8 : 936;
// 下发前把 UTF-8 path 转成客户端 ANSI空/./\/\\ 为 ASCII转换后不变OnDriveList 直接用。
std::string ansiPath = ToAnsi(path, cp);
CMcpServer& mcp = CMcpServer::Instance();
if (!mcp.BeginPending(devId, "list_files", ansiPath))
return BuildError(id, -32003, "Device busy: another request is pending for this host");
BYTE cmd = COMMAND_LIST_DRIVE;
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 file list");
Json::Value drives(Json::arrayValue);
Json::Value files(Json::arrayValue);
int count = 0;
if (listDrives) {
drives = ParseDriveList(data, cp);
count = (int)drives.size();
} else {
files = ParseFileList(data, 500, cp); // 上限 500避免 JSON 响应过大
count = (int)files.size();
}
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
if (listDrives) structuredContent["drives"] = drives;
else structuredContent["files"] = files;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = listDrives
? (std::string(u8"") + std::to_string(count) + std::string(u8" 个驱动器。"))
: (std::string(u8"") + std::to_string(count) + std::string(u8" 个条目。"));
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// tools/calllist_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 接口 = 客户端 ANSIGBK/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/callget_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 关子链接,增量不再到达,故此处即为完整快照。
// 日志文本为客户端 ANSIWindows 走 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 分派
std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
const Json::Value& id = root["id"];
Json::Value params = root.isMember("params") ? root["params"] : Json::Value(Json::objectValue);
std::string toolName;
if (params.isObject() && params.isMember("name") && params["name"].isString()) {
toolName = params["name"].asString();
}
const Json::Value args = GetCallArguments(params);
if (toolName == "list_online_hosts") return BuildListOnlineHosts(id, parent);
if (toolName == "search_hosts") return BuildSearchHosts(id, args, parent);
if (toolName == "get_host_detail") return BuildGetHostDetail(id, args, parent);
if (toolName == "list_processes") return BuildListProcesses(id, args, parent);
if (toolName == "list_windows") return BuildListWindows(id, args, parent);
if (toolName == "get_activity_history") return BuildGetActivityHistory(id, args, parent);
if (toolName == "get_screenshot") return BuildGetScreenshot(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,
"Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName));
}
} // namespace
//////////////////////////////////////////////////////////////////////////
// CMcpServer Implementation
//////////////////////////////////////////////////////////////////////////
CMcpServer& CMcpServer::Instance() {
static CMcpServer instance;
return instance;
}
CMcpServer::CMcpServer() {
m_server.Post("/mcp", [this](const httplib::Request& req, httplib::Response& res) {
HandleMcp(req, res);
});
}
CMcpServer::~CMcpServer() {
Stop(); // 兜底:确保监听线程 join避免 std::thread 析构触发 terminate
}
bool CMcpServer::Start(const std::string& bind, int port) {
if (m_running.load()) return true; // 已在运行
m_thread = std::thread([this, bind, port]() {
m_server.listen(bind, port);
});
// 给 listen 一点时间绑定端口httplib::Server::is_running() 在 listen 内部置位。
std::this_thread::sleep_for(std::chrono::milliseconds(100));
m_running.store(m_server.is_running());
return m_running.load();
}
void CMcpServer::Stop() {
m_server.stop();
if (m_thread.joinable()) {
m_thread.join();
}
m_running.store(false);
}
void CMcpServer::HandleMcp(const httplib::Request& req, httplib::Response& res) {
res.set_header("Content-Type", "application/json");
// 静态 token 校验Authorization: Bearer <token>Start 前经 SetToken 保证非空)
if (req.get_header_value("Authorization") != ("Bearer " + m_token)) {
res.status = 401;
res.set_content(BuildError(Json::nullValue, -32000, "Unauthorized"), "application/json");
return;
}
// 解析 JSON-RPC 请求体
Json::Value root;
Json::CharReaderBuilder rbuilder;
std::string errs;
std::istringstream iss(req.body);
if (!Json::parseFromStream(rbuilder, iss, &root, &errs) || !root.isObject()) {
res.set_content(BuildError(Json::nullValue, -32700, "Parse error"), "application/json");
return;
}
// 通知(无 id→ 不返回 JSON-RPC 响应(如 notifications/initialized
if (!root.isMember("id")) {
res.status = 202;
res.set_content("", "application/json");
return;
}
// 结构校验:缺 method
if (!root.isMember("method") || !root["method"].isString()) {
res.set_content(BuildError(root["id"], -32600, "Invalid Request"), "application/json");
return;
}
std::string method = root["method"].asString();
if (method == "initialize") {
res.set_content(BuildInitializeResult(root["id"]), "application/json");
return;
}
if (method == "ping") {
res.set_content(BuildPingResult(root["id"]), "application/json");
return;
}
if (method == "tools/list") {
res.set_content(BuildToolsListResult(root["id"]), "application/json");
return;
}
if (method == "tools/call") {
res.set_content(BuildToolsCall(root, m_parent), "application/json");
return;
}
// 未实现的方法
res.set_content(BuildError(root["id"], -32601, "Method not found"), "application/json");
}
// ===== P2b 挂起请求注册表实现 =====
bool CMcpServer::IsPending(uint64_t device_id) {
std::lock_guard<std::mutex> lk(m_PendingMutex);
return m_Pending.find(device_id) != m_Pending.end();
}
void CMcpServer::TakeMainResponse(uint64_t device_id, const BYTE* data, ULONG len) {
std::lock_guard<std::mutex> lk(m_PendingMutex);
auto it = m_Pending.find(device_id);
if (it == m_Pending.end()) return; // 已超时清理 → 迟到数据,丢弃
it->second.data.assign(data, data + len);
it->second.done = true;
m_PendingCv.notify_one();
}
bool CMcpServer::BeginPending(uint64_t device_id, const std::string& tool) {
std::lock_guard<std::mutex> lk(m_PendingMutex);
if (m_Pending.find(device_id) != m_Pending.end()) return false; // 设备忙
PendingRequest r;
r.tool = tool;
m_Pending[device_id] = std::move(r);
return true;
}
bool CMcpServer::WaitPending(uint64_t device_id, std::vector<BYTE>& out, int timeoutMs) {
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()) {
m_Pending.erase(it); // 超时/空数据 → 清理
return false;
}
out = std::move(it->second.data);
m_Pending.erase(it);
return true;
}
void CMcpServer::ClearPending(uint64_t device_id) {
std::lock_guard<std::mutex> lk(m_PendingMutex);
m_Pending.erase(device_id);
}
// ===== P2clist_files / get_screenshot 扩展 =====
bool CMcpServer::BeginPending(uint64_t device_id, const std::string& tool, const std::string& path) {
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.path = path;
m_Pending[device_id] = std::move(r);
return true;
}
uint16_t CMcpServer::NextPreviewReqId() {
uint16_t v = m_PreviewReqId.fetch_add(1, std::memory_order_relaxed);
if (v == 0) v = m_PreviewReqId.fetch_add(1, std::memory_order_relaxed); // 跳过 0
return v;
}
void CMcpServer::SetPendingReqId(uint64_t device_id, uint16_t reqId) {
std::lock_guard<std::mutex> lk(m_PendingMutex);
auto it = m_Pending.find(device_id);
if (it != m_Pending.end()) it->second.expectedReqId = reqId;
}
bool CMcpServer::TakePreviewResponse(uint64_t device_id, uint16_t reqId, const BYTE* data, ULONG len) {
std::lock_guard<std::mutex> lk(m_PendingMutex);
auto it = m_Pending.find(device_id);
if (it == m_Pending.end()) return false; // 已超时清理 → 迟到数据,回落 MFC
if (it->second.tool != "get_screenshot") return false;
if (it->second.expectedReqId == 0 || it->second.expectedReqId != reqId) return false; // 过期/他途响应
it->second.data.assign(data, data + len);
it->second.done = true;
m_PendingCv.notify_one();
return true;
}
bool CMcpServer::OnDriveList(uint64_t device_id, context* subCtx, const BYTE* buf, ULONG len) {
std::string path;
bool listDrives = true;
{
std::lock_guard<std::mutex> lk(m_PendingMutex);
auto it = m_Pending.find(device_id);
if (it == m_Pending.end()) return true; // 已超时清理 → 调用方 CancelIO 收尾
path = it->second.path;
listDrives = path.empty() || path == "." || path == "/" || path == "\\";
if (listDrives) {
it->second.data.assign(buf, buf + len);
it->second.done = true;
m_PendingCv.notify_one();
return true; // 只列盘 → 调用方用完即关
}
}
// 列目录:锁外下发 COMMAND_LIST_FILES + path子链接保持等 TOKEN_FILE_LIST。
// path 已在 BuildListFiles 按客户端 ANSI 转好OnDriveList 无需再转),结尾 '\0'
// 与 FileManagerDlg 的 PacketSize=len+2 一致。
std::vector<BYTE> pkt;
pkt.reserve(1 + path.size() + 1);
pkt.push_back((BYTE)COMMAND_LIST_FILES);
pkt.insert(pkt.end(), path.begin(), path.end());
pkt.push_back(0);
subCtx->Send2Client(pkt.data(), (ULONG)pkt.size());
return false; // 继续等 TOKEN_FILE_LIST调用方不 CancelIO
}
// rand_sWindows CRT 加密安全随机源(基于系统 CSPRNG。其声明需在 <stdlib.h> 前
// 定义 _CRT_RAND_S为避免依赖 PCH 的包含顺序这里手动声明其导出原型errno_t == int
extern "C" int __cdecl rand_s(unsigned int* randomValue);
std::string GenerateRandomToken() {
static const char hex[] = "0123456789abcdef";
std::string out;
out.reserve(32);
for (int i = 0; i < 16; ++i) {
unsigned int v = 0;
if (rand_s(&v) != 0) {
// rand_s 失败(罕见):退化为时间 + 地址熵,保证仍返回非空 token。
v = (unsigned int)(GetTickCount() ^ (ULONG_PTR)&out);
}
out.push_back(hex[(v >> 4) & 0xF]);
out.push_back(hex[v & 0xF]);
}
return out;
}