Files
SimpleRemoter/server/2015Remote/McpServer.cpp
yuanyuanxiang 70dbb6b255 Feature: Add persistent remote terminal MCP tools
Add terminal_open / terminal_exec / terminal_close so an AI can hold one
shell session per Windows host and run a sequence of commands with cwd and
environment preserved, instead of the one-shot exec_command.

The persistent terminal is a separate full-command write capability gated by
McpTerminal (default off) plus McpReadonly=0, with no whitelist and full
audit. One device maps to one terminal session via the shared m_TermSessions
map; idle sessions are swept after 300s. terminal_exec rejects commands that
contain & or | (they corrupt the sentinel control-operator chain) as well as
control characters.

Also harden exec_command and terminal_exec against newline/CR injection, fix a
dangling subCtx after an abrupt shell disconnect, and refresh lastActiveAt on
command completion. Add en/zh-TW translations for the new UI strings and a
design document covering both exec_command and the persistent terminal.

Co-Authored-By: deepseek-v4-pro
2026-08-24 19:11:16 +02:00

3143 lines
123 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 "LangManager.h" // _TR审计日志标题语言映射
#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;
}
// ========== list_registry注册表解析 ==========
// 注册表 REGMSG 头client RegisterOperation.cpp / server RegisterDlg.cpp 各自本地定义,此处同构)。
struct RegMsgHeader {
int count; // 名字个数
DWORD size; // 名字大小(定宽)
DWORD valsize; // 值大小(定宽)
};
// client KEYVALUE 枚举RegisterOperation.cppMREG_SZ=0 … MREG_NONE=6。
enum RegValueType {
REG_T_SZ = 0,
REG_T_DWORD = 1,
REG_T_BINARY = 2,
REG_T_EXPAND_SZ = 3,
REG_T_MULTI_SZ = 4,
REG_T_QWORD = 5,
REG_T_NONE = 6,
};
// client MYKEY 枚举:根键 token。
enum RegRootToken {
REG_ROOT_CLASSES_ROOT = 0,
REG_ROOT_CURRENT_USER = 1,
REG_ROOT_LOCAL_MACHINE = 2,
REG_ROOT_USERS = 3,
REG_ROOT_CURRENT_CONFIG = 4,
};
// ASCII 不区分大小写前缀匹配(注册表根键名大小写不敏感)。
bool StrPrefixCI(const std::string& s, const char* prefix, size_t len) {
if (s.size() < len) return false;
for (size_t i = 0; i < len; ++i) {
char a = s[i], b = prefix[i];
if (a >= 'A' && a <= 'Z') a += 32;
if (b >= 'A' && b <= 'Z') b += 32;
if (a != b) return false;
}
return true;
}
// 值类型名(可读字符串)
const char* RegTypeName(BYTE type) {
switch (type) {
case REG_T_SZ: return "REG_SZ";
case REG_T_DWORD: return "REG_DWORD";
case REG_T_BINARY: return "REG_BINARY";
case REG_T_EXPAND_SZ: return "REG_EXPAND_SZ";
case REG_T_MULTI_SZ: return "REG_MULTI_SZ";
case REG_T_QWORD: return "REG_QWORD";
case REG_T_NONE: return "REG_NONE";
default: return "REG_UNKNOWN";
}
}
// 将注册表值数据格式化为字符串。valSize 为定宽client 按 MaxDataLen+1 填充),
// 字符串类取到 '\0' 为止、数值类取定长前缀REG_BINARY 长度无法从协议还原,按 valSize 输出 hex。
std::string FormatRegData(BYTE type, const BYTE* val, size_t valSize, UINT cp) {
switch (type) {
case REG_T_SZ:
case REG_T_EXPAND_SZ:
return ToUtf8((const char*)val, cp);
case REG_T_MULTI_SZ: { // 多个 '\0' 结尾串、双 '\0' 结束
std::string out;
size_t i = 0;
while (i < valSize && val[i] != '\0') {
const char* s = (const char*)(val + i);
size_t n = BoundedStrlen(s, valSize - i);
if (!out.empty()) out += "\n";
out += ToUtf8(s, cp);
i += n + 1;
}
return out;
}
case REG_T_DWORD: { // 4 字节小端
if (valSize < 4) return "";
DWORD v = 0;
memcpy(&v, val, 4);
char buf[64];
sprintf(buf, "0x%08lX (%lu)", (unsigned long)v, (unsigned long)v);
return buf;
}
case REG_T_QWORD: { // 8 字节小端
if (valSize < 8) return "";
uint64_t v = 0;
memcpy(&v, val, 8);
char buf[96];
sprintf(buf, "0x%016I64X (%I64u)", (unsigned __int64)v, (unsigned __int64)v);
return buf;
}
case REG_T_BINARY: { // hex尾部可能含 0 填充
std::string out;
char buf[4];
for (size_t i = 0; i < valSize; ++i) {
sprintf(buf, "%02X", val[i]);
out += buf;
}
return out;
}
case REG_T_NONE:
default:
return "";
}
}
// 解析 TOKEN_REG_PATH 缓冲:[token:1][RegMsgHeader:12][count * size 定宽子键名]。cp 为客户端 ANSI。
Json::Value ParseRegPath(const std::vector<BYTE>& data, UINT cp) {
Json::Value arr(Json::arrayValue);
if (data.size() < 1 + sizeof(RegMsgHeader)) return arr;
const char* p = (const char*)data.data();
size_t off = 1; // 跳过 TOKEN 字节
RegMsgHeader hdr;
memcpy(&hdr, p + off, sizeof(hdr));
off += sizeof(hdr);
int count = hdr.count;
size_t nameSize = hdr.size;
for (int i = 0; i < count; ++i) {
if (off + nameSize > data.size()) break;
const char* name = p + off;
off += nameSize;
if (name[0] == '\0') break; // 空名 = 尾部零填充,停止
arr.append(ToUtf8(name, cp));
}
return arr;
}
// 解析 TOKEN_REG_KEY 缓冲:[token:1][RegMsgHeader:12][count * {type:1,name:size,data:valsize}]。
Json::Value ParseRegKey(const std::vector<BYTE>& data, UINT cp) {
Json::Value arr(Json::arrayValue);
if (data.size() < 1 + sizeof(RegMsgHeader)) return arr;
const char* p = (const char*)data.data();
size_t off = 1; // 跳过 TOKEN 字节
RegMsgHeader hdr;
memcpy(&hdr, p + off, sizeof(hdr));
off += sizeof(hdr);
int count = hdr.count;
size_t nameSize = hdr.size;
size_t valSize = hdr.valsize;
for (int i = 0; i < count; ++i) {
if (off + 1 + nameSize + valSize > data.size()) break;
BYTE type = (BYTE)p[off];
off += 1;
const char* name = p + off; // 定宽 nameSize 内以 '\0' 结尾
off += nameSize;
const BYTE* val = (const BYTE*)(p + off);
off += valSize;
Json::Value item(Json::objectValue);
item["name"] = ToUtf8(name, cp);
item["type"] = RegTypeName(type);
item["data"] = FormatRegData(type, val, valSize, cp);
arr.append(item);
}
return arr;
}
// 解析 "HKEY_LOCAL_MACHINE\Software\..." → rootToken + 相对子键路径(去掉根键名与首个 '\')。
// 返回 false 表示 path 为空(调用方列根键)或未匹配任何根键(调用方报错)。
bool ParseRegistryPath(const std::string& path, BYTE& rootToken, std::string& relPath) {
struct RootMap { const char* name; BYTE token; };
static const RootMap roots[] = {
{ "HKEY_CLASSES_ROOT", REG_ROOT_CLASSES_ROOT },
{ "HKEY_CURRENT_USER", REG_ROOT_CURRENT_USER },
{ "HKEY_LOCAL_MACHINE", REG_ROOT_LOCAL_MACHINE },
{ "HKEY_USERS", REG_ROOT_USERS },
{ "HKEY_CURRENT_CONFIG", REG_ROOT_CURRENT_CONFIG },
};
if (path.empty()) return false;
for (const RootMap& r : roots) {
size_t n = strlen(r.name);
if (StrPrefixCI(path, r.name, n)) {
rootToken = r.token;
relPath = path.substr(n);
if (!relPath.empty() && relPath[0] == '\\') relPath.erase(0, 1);
return true;
}
}
return false;
}
// 列根键path 为空):返回 5 个固定根键,无需查询客户端。
std::string BuildRegistryRoots(const Json::Value& id) {
Json::Value keys(Json::arrayValue);
const char* roots[] = {
"HKEY_CLASSES_ROOT", "HKEY_CURRENT_USER", "HKEY_LOCAL_MACHINE",
"HKEY_USERS", "HKEY_CURRENT_CONFIG"
};
for (const char* r : roots) keys.append(r);
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["keys"] = keys;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"") + std::to_string((int)keys.size()) + std::string(u8" 个根键。");
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// 收集所有在线主机 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;
}
// list_registry 的 inputSchemaid 必填path 可选path 缺省/空 = 列根键)
Json::Value BuildListRegistryInputSchema() {
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"注册表键路径(如 HKEY_LOCAL_MACHINE\\Software省略或传空则返回 5 个根键";
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_registry 的 outputSchemakeys 字符串数组 / values 对象数组)
Json::Value BuildListRegistryOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value pathProp(Json::objectValue);
pathProp["type"] = "string";
props["path"] = pathProp;
Json::Value keysProp(Json::objectValue);
keysProp["type"] = "array";
Json::Value keyItems(Json::objectValue);
keyItems["type"] = "string";
keysProp["items"] = keyItems;
props["keys"] = keysProp;
Json::Value valsProp(Json::objectValue);
valsProp["type"] = "array";
Json::Value valItems(Json::objectValue);
valItems["type"] = "object";
Json::Value valProps(Json::objectValue);
const char* strFields[] = { "name", "type", "data" };
for (const char* f : strFields) {
Json::Value s(Json::objectValue);
s["type"] = "string";
valProps[f] = s;
}
valItems["properties"] = valProps;
valsProp["items"] = valItems;
props["values"] = valsProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("keys");
required.append("values");
schema["required"] = required;
return schema;
}
// exec_command 的 inputSchemaid 必填、command 必填、timeout_ms 可选)
Json::Value BuildExecCommandInputSchema() {
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 cmdProp(Json::objectValue);
cmdProp["type"] = "string";
cmdProp["description"] = u8"要执行的命令(受只读模式与命令白名单约束,仅允许只读命令前缀)";
props["command"] = cmdProp;
Json::Value timeoutProp(Json::objectValue);
timeoutProp["type"] = "integer";
timeoutProp["description"] = u8"等待输出完成的超时毫秒数(可选,默认 20000上限 600000";
props["timeout_ms"] = timeoutProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("id");
required.append("command");
schema["required"] = required;
return schema;
}
// exec_command 的 outputSchemastdout 文本 / exit_code
Json::Value BuildExecCommandOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value stdoutProp(Json::objectValue);
stdoutProp["type"] = "string";
stdoutProp["description"] = u8"命令输出(已剥哨兵与 ANSI 转义)";
props["stdout"] = stdoutProp;
Json::Value exitProp(Json::objectValue);
exitProp["type"] = "integer";
exitProp["description"] = u8"退出码0=成功、1=非零退出、-1=未知(进程异常退出)";
props["exit_code"] = exitProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("stdout");
required.append("exit_code");
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_audit_log 的 outputSchema服务端消息/审计日志条目数组)
Json::Value BuildAuditLogOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value entriesProp(Json::objectValue);
entriesProp["type"] = "array";
Json::Value items(Json::objectValue);
items["type"] = "object";
Json::Value itemProps(Json::objectValue);
const char* strFields[] = { "type", "time", "msg" };
for (const char* f : strFields) {
Json::Value s(Json::objectValue);
s["type"] = "string";
itemProps[f] = s;
}
items["properties"] = itemProps;
entriesProp["items"] = items;
props["entries"] = entriesProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("entries");
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;
}
// ===== P4 前置声明定义见下方「tools/call 分派」前)=====
Json::Value BuildTerminalOpenInputSchema();
Json::Value BuildTerminalOpenOutputSchema();
Json::Value BuildTerminalExecInputSchema();
Json::Value BuildTerminalExecOutputSchema();
Json::Value BuildTerminalCloseInputSchema();
Json::Value BuildTerminalCloseOutputSchema();
std::string BuildTerminalOpen(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
std::string BuildTerminalExec(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
std::string BuildTerminalClose(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
// 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);
}
// 11) get_audit_log服务端本地主界面消息/审计日志,纯内存,无子链接)
{
Json::Value tool(Json::objectValue);
tool["name"] = "get_audit_log";
tool["description"] = u8"获取 YAMA 服务端主界面的消息/审计日志(主机上线/下线、操作结果、告警等)。返回列表内当前全部条目(最多 1000 条,新在前)。只读、纯内存。";
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"] = BuildAuditLogOutputSchema();
tools.append(tool);
}
// 12) list_registryP3注册表查询列根键/列子键+值,仅 Windows一次性子链接
{
Json::Value tool(Json::objectValue);
tool["name"] = "list_registry";
tool["description"] = u8"查询指定在线 Windows 主机的注册表。省略或空 path 返回 5 个根键;给定 path如 HKEY_LOCAL_MACHINE\\Software返回该键一层内的子键keys与值values含 name/type/data。只读。";
tool["inputSchema"] = BuildListRegistryInputSchema();
tool["outputSchema"] = BuildListRegistryOutputSchema();
tools.append(tool);
}
// 13) exec_commandP3一次性远程命令仅 Windows安全门只读默认 + 白名单 + 审计)
// 只读模式下 tools/list 直接隐藏该工具即便被绕过BuildExecCommand 也会再次校验只读。
if (!CMcpServer::Instance().IsReadonly()) {
Json::Value tool(Json::objectValue);
tool["name"] = "exec_command";
tool["description"] = u8"在指定在线 Windows 主机上执行一条命令并返回 stdout 与退出码(受只读模式与命令白名单约束,默认仅允许只读命令前缀,见 McpReadonly / McpCmdWhitelist";
tool["inputSchema"] = BuildExecCommandInputSchema();
tool["outputSchema"] = BuildExecCommandOutputSchema();
tools.append(tool);
}
// 14) terminal_open / terminal_exec / terminal_closeP4持久远程终端仅 Windows
// 安全门McpTerminal=1 且 McpReadonly=0无白名单全命令审计 + idle 回收)
if (CMcpServer::Instance().IsTerminalEnabled() && !CMcpServer::Instance().IsReadonly()) {
{
Json::Value tool(Json::objectValue);
tool["name"] = "terminal_open";
tool["description"] = u8"在指定在线 Windows 主机上打开一个持久 shell 会话并返回 session_id 与终端模式。后续 terminal_exec 复用该会话cwd/环境变量跨命令保持),用毕须 terminal_close。";
tool["inputSchema"] = BuildTerminalOpenInputSchema();
tool["outputSchema"] = BuildTerminalOpenOutputSchema();
tools.append(tool);
}
{
Json::Value tool(Json::objectValue);
tool["name"] = "terminal_exec";
tool["description"] = u8"在已打开的持久会话中执行一条命令并返回 stdout 与退出码。命令不受白名单约束,但不能含 & 或 |(会破坏输出哨兵捕获),请拆成多条调用;重定向 > < 与转义 ^ 允许。";
tool["inputSchema"] = BuildTerminalExecInputSchema();
tool["outputSchema"] = BuildTerminalExecOutputSchema();
tools.append(tool);
}
{
Json::Value tool(Json::objectValue);
tool["name"] = "terminal_close";
tool["description"] = u8"关闭并释放指定持久会话(幂等:会话已不存在也返回成功)。";
tool["inputSchema"] = BuildTerminalCloseInputSchema();
tool["outputSchema"] = BuildTerminalCloseOutputSchema();
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/calllist_registry主连接下发 COMMAND_REGEDIT → 子连接 TOKEN_REGEDIT → 下发
// COMMAND_REG_FIND → TOKEN_REG_PATH + TOKEN_REG_KEY 两包path 空 = 列根键,无需查询客户端)
std::string BuildListRegistry(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 客户端实现RegOpenKeyEx 等 A 接口LNX/MAC 无注册表。
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
if (clientType == "LNX" || clientType == "MAC")
return BuildError(id, -32005, "list_registry is only supported on Windows hosts");
std::string path = GetStringArg(args, "path");
BYTE rootToken = 0;
std::string relPath;
if (!ParseRegistryPath(path, rootToken, relPath)) {
if (path.empty())
return BuildRegistryRoots(id); // 空 path → 列根键
return BuildError(id, -32602,
"Invalid path: expected a root key like HKEY_LOCAL_MACHINE\\Software");
}
// 客户端 RegisterOperation 以 char KeyPath[MAX_PATH] 承载相对子键路径SetPath→strcpy 无界),
// 超长会栈溢出。按 UTF-8 字节数做保守上限GBK/936 字节数 ≤ UTF-8故只查 UTF-8 即可)。
if (relPath.size() >= MAX_PATH)
return BuildError(id, -32602, "Registry path too long (exceeds MAX_PATH)");
// 注册表键路径为客户端 ANSIGBK/936下发前转好rootToken 编码进 path[0] 供 OnRegeditReady 拆分。
UINT cp = (clientType == "LNX" || clientType == "MAC") ? CP_UTF8 : 936;
std::string rootAndPath = std::string(1, (char)rootToken) + ToAnsi(relPath, cp);
CMcpServer& mcp = CMcpServer::Instance();
if (!mcp.BeginPending(devId, "list_registry", rootAndPath))
return BuildError(id, -32003, "Device busy: another request is pending for this host");
BYTE cmd = COMMAND_REGEDIT;
if (!ctx->Send2Client(&cmd, 1)) {
mcp.ClearPending(devId);
return BuildError(id, -32004, "Failed to send command to host");
}
std::vector<BYTE> subkeys, values;
if (!mcp.WaitPendingRegistry(devId, subkeys, values, kMcpToolTimeoutMs))
return BuildError(id, -32001, "Timeout waiting for registry data");
Json::Value keysArr = ParseRegPath(subkeys, cp);
Json::Value valsArr = ParseRegKey(values, cp);
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["path"] = path;
structuredContent["keys"] = keysArr;
structuredContent["values"] = valsArr;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"") + std::to_string((int)keysArr.size()) + std::string(u8" 个子键、")
+ std::to_string((int)valsArr.size()) + std::string(u8" 个值。");
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// ========== P3exec_command ==========
// exec_command 默认白名单:只读命令前缀(逗号分隔,前缀匹配,大小写不敏感)。
// 只放查询/只读类命令写命令del/copy/move/reg add/sc stop/...)一律不在列。
static const char* kDefaultCmdWhitelist =
"dir,type,cd,chdir,ver,hostname,whoami,where,tasklist,systeminfo,ipconfig,netstat,"
"path,set,find,findstr,reg query,sc query,"
"ping,tracert,nslookup,getmac,driverquery,query,gpresult,"
"arp -a,route print,schtasks /query";
// 去首尾空白(空格/制表符/回车/换行)。
static std::string Trim(const std::string& s) {
size_t b = s.find_first_not_of(" \t\r\n");
if (b == std::string::npos) return "";
return s.substr(b, s.find_last_not_of(" \t\r\n") - b + 1);
}
// 去尾部空白(含尾部 CR/LF
static std::string TrimRight(const std::string& s) {
size_t e = s.find_last_not_of(" \t\r\n");
return (e == std::string::npos) ? "" : s.substr(0, e + 1);
}
// 命令白名单校验命令trim + ASCII 小写)以某白名单项为前缀,且其后字符为空/空格/制表符,
// 防止 "dirx" 前缀误配 "dir"。多词前缀sc query / reg query也按完整前缀匹配。
static bool IsCommandAllowed(const std::string& command, const std::string& whitelist) {
std::string c = ToLowerAscii(Trim(command));
if (c.empty()) return false;
size_t start = 0;
while (start < whitelist.size()) {
size_t comma = whitelist.find(',', start);
std::string item = Trim(whitelist.substr(
start, comma == std::string::npos ? std::string::npos : comma - start));
if (!item.empty()) {
std::string p = ToLowerAscii(item);
if (c.size() >= p.size() && c.compare(0, p.size(), p) == 0 &&
(c.size() == p.size() || c[p.size()] == ' ' || c[p.size()] == '\t')) {
return true;
}
}
if (comma == std::string::npos) break;
start = comma + 1;
}
return false;
}
// 剥 ANSI 转义序列CSI/OSC/单字节 ESC避免终端彩色码污染返回文本。
static std::string StripAnsi(const std::string& s) {
std::string out;
out.reserve(s.size());
for (size_t i = 0; i < s.size(); ++i) {
if (s[i] != '\x1b') { out += s[i]; continue; }
++i; // 吞 ESC
if (i < s.size() && s[i] == '[') { // CSI ... 结尾 @~
while (++i < s.size() && !(s[i] >= '@' && s[i] <= '~')) {}
} else if (i < s.size() && s[i] == ']') { // OSC ... 结尾 BEL/ESC
while (++i < s.size() && s[i] != '\x07' && s[i] != '\x1b') {}
}
// 其它单字节 ESC 序列吞掉当前字节即可(循环 ++i 会跳过它)
}
return out;
}
// 去掉 ConPTY 把整行输入回显造成的噪声echoedLine 是发送的哨兵命令行(含唯一 nonce
// 输出里若出现该行(连同其后换行)则整行剔除。非 PTY 老 ShellManager 已自行跳回显,
// 找不到该行即无操作。echoedLine 含随机 nonce命令真实输出不可能恰好相同。
static std::string StripEchoedCommand(const std::string& s, const std::string& echoedLine) {
if (echoedLine.empty() || s.empty()) return s;
size_t p = s.find(echoedLine);
if (p == std::string::npos) return s;
size_t q = p + echoedLine.size();
if (q < s.size() && s[q] == '\n') ++q; // CRLF 已归一为 \n
return s.substr(0, p) + s.substr(q);
}
// 命令是否含控制字符(<0x20含 \r\n。\r\n 会把命令拆成多行:一次性 exec 里会绕过
// 白名单执行任意后续行(安全);持久终端里只有末行被哨兵包装、退出码失真(正确性)。二者都拒绝。
static bool ContainsControlChar(const std::string& s) {
for (char ch : s)
if ((unsigned char)ch < 0x20) return true;
return false;
}
// tools/callexec_commandWindows 一次性远程命令:主连接 COMMAND_SHELL → 子连接终端 →
// 哨兵命令行 → 收集 stdout + exit_code → 关子链接)。安全门:只读默认 + 白名单 + 审计。
std::string BuildExecCommand(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 客户端实现ConPTY / 老 cmd 管道LNX/MAC 远期再考虑。
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
if (clientType == "LNX" || clientType == "MAC")
return BuildError(id, -32005, "exec_command is only supported on Windows hosts");
std::string command = Trim(GetStringArg(args, "command"));
if (command.empty())
return BuildError(id, -32602, "Missing required parameter: command");
CMcpServer& mcp = CMcpServer::Instance();
// 安全门 1只读模式直接拒绝默认只读exec_command 需显式关只读才可用)。
if (mcp.IsReadonly())
return BuildError(id, -32006, "exec_command is disabled: MCP is in read-only mode (set McpReadonly=0 to enable)");
// 安全门 2拒绝 shell 元字符与控制字符(\r\n 会把命令拆成多行绕过白名单),防注入。
if (command.find_first_of("&|<>^") != std::string::npos || ContainsControlChar(command))
return BuildError(id, -32008, "Command contains forbidden shell characters: " + command);
// 安全门 3命令白名单前缀校验。
std::string whitelist = mcp.GetCmdWhitelist();
if (whitelist.empty()) whitelist = kDefaultCmdWhitelist;
if (!IsCommandAllowed(command, whitelist))
return BuildError(id, -32007, "Command not allowed by whitelist: " + command);
int timeoutMs = kMcpToolTimeoutMs;
std::string t = GetStringArg(args, "timeout_ms");
if (!t.empty() && IsDigits(t)) {
int v = atoi(t.c_str());
if (v > 0 && v <= 600000) timeoutMs = v;
}
std::string nonce = GenerateRandomToken().substr(0, 8);
if (!mcp.BeginTermPending(devId, command, nonce))
return BuildError(id, -32003, "Device busy: a terminal session is already active for this host");
// 主连接下发 COMMAND_SHELL客户端建立 shell 子连接并回 TOKEN_*_START。
BYTE cmd = COMMAND_SHELL;
if (!ctx->Send2Client(&cmd, 1)) {
mcp.ClearTermPending(devId);
return BuildError(id, -32004, "Failed to send command to host");
}
context* subCtx = nullptr;
bool isPty = false;
if (!mcp.WaitTerminalReady(devId, subCtx, isPty, timeoutMs))
return BuildError(id, -32001, "Timeout waiting for shell to start");
// 编码ConPTY(UTF-8) / 老 cmd 管道(GBK)。哨兵为纯 ASCII两种编码下字节一致。
UINT cp = isPty ? CP_UTF8 : 936;
// 哨兵命令行:@echo off 仅 ConPTY 需要(抑制回显噪声);老 ShellManager 已自行跳过回显,
// 加 @echo off 反而破坏其回显跳过逻辑。&&/|| 取命令真实退出码(%errorlevel% 在复合句中
// 是解析期展开、已过期,故用控制操作符)。哨兵为随机串,命令输出不可能恰好包含。
std::string line;
if (isPty) line += "@echo off & ";
line += command;
line += " 2>&1 && echo __MCP_DONE_" + nonce + "__0 || echo __MCP_DONE_" + nonce + "__1";
std::string wireLine = ToAnsi(line, cp) + "\r\n";
subCtx->Send2Client((BYTE*)wireLine.data(), (ULONG)wireLine.size());
std::vector<BYTE> raw;
int exitCode = -1;
bool closed = false;
if (!mcp.WaitTerminalDone(devId, raw, exitCode, closed, timeoutMs)) {
subCtx->CancelIO();
return BuildError(id, -32001, "Timeout waiting for command output");
}
subCtx->CancelIO(); // 关子链接,结束 shell 进程
// 清洗raw → UTF-8 → 剥 ANSI → CRLF 归一 → 去 ConPTY 回显命令行 → TrimRight。
// raw 已截断到哨兵前。
std::string stdoutStr;
if (!raw.empty()) {
std::string rawStr((const char*)raw.data(), raw.size());
stdoutStr = StripAnsi(ToUtf8(rawStr.c_str(), cp));
size_t p = 0;
while ((p = stdoutStr.find("\r\n", p)) != std::string::npos)
stdoutStr.erase(p, 1);
stdoutStr = StripEchoedCommand(stdoutStr, line);
stdoutStr = TrimRight(stdoutStr);
}
// 审计:命令执行落服务端消息/审计日志(不可关闭)。
if (parent) {
std::string text = "host " + std::to_string(devId) + " exec: " + command;
parent->PostMessageA(WM_SHOWERRORMSG,
(WPARAM)new CString(ToAnsi(text, 936).c_str()),
(LPARAM)new CString(_TR("MCP命令执行"))); // 标题走语言映射GBK随语言切换
}
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["stdout"] = stdoutStr;
structuredContent["exit_code"] = exitCode;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = stdoutStr.empty() ? std::string(u8"(无输出)") : stdoutStr;
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/callget_audit_log服务端本地消息日志读取 UI 日志列表的线程安全镜像)
std::string BuildGetAuditLog(const Json::Value& id, CMy2015RemoteDlg* parent) {
Json::Value entries(Json::arrayValue);
if (parent) {
EnterCriticalSection(&parent->m_cs);
// m_MessageLog 新在前、旧在后(与界面一致),直接顺序输出。
for (const auto& e : parent->m_MessageLog) {
Json::Value item(Json::objectValue);
item["type"] = ToUtf8(e.type.c_str(), 936);
item["time"] = ToUtf8(e.time.c_str(), 936);
item["msg"] = ToUtf8(e.msg.c_str(), 936);
entries.append(item);
}
LeaveCriticalSection(&parent->m_cs);
}
int count = (int)entries.size();
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["entries"] = entries;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = count > 0 ? std::string(u8"") + std::to_string(count) + std::string(u8" 条日志。")
: std::string(u8"当前无日志。");
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// ===== P4持久远程终端terminal_open / terminal_exec / terminal_close=====
static const int kTermIdleTimeoutSec = 300; // 持久终端 idle 回收超时(秒)
// 共享前置:校验 McpTerminal && !readonly → 解析 id → FindMainContext → 拒绝 LNX/MAC。
// 成功返回 true 并输出 devId/ctx失败时 errJson 已写入对应 JSON 错误串。
static bool ResolveTerminalSessionHost(const Json::Value& id, const Json::Value& args,
CMy2015RemoteDlg* parent,
uint64_t& devId, context*& ctx, std::string& errJson) {
CMcpServer& mcp = CMcpServer::Instance();
if (!mcp.IsTerminalEnabled() || mcp.IsReadonly()) {
errJson = BuildError(id, -32006,
"Persistent terminal is disabled: requires McpTerminal=1 and McpReadonly=0");
return false;
}
std::string err;
if (!ParseHostIdArg(args, devId, err)) {
errJson = BuildError(id, -32602, err);
return false;
}
ctx = FindMainContext(parent, devId);
if (!ctx) {
errJson = BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
return false;
}
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
if (clientType == "LNX" || clientType == "MAC") {
errJson = BuildError(id, -32005, "Persistent terminal is only supported on Windows hosts");
return false;
}
return true;
}
// 发送哨兵命令行(复用 exec_command 的编码/哨兵逻辑);返回所发送的完整命令行,
// 供调用方剔除 ConPTY 把输入整行回显造成的噪声。cp 由 isPty 决定。
static std::string SendTerminalCommandLine(context* subCtx, bool isPty, const std::string& command,
const std::string& nonce) {
UINT cp = isPty ? CP_UTF8 : 936;
std::string line;
if (isPty) line += "@echo off & ";
line += command;
line += " 2>&1 && echo __MCP_DONE_" + nonce + "__0 || echo __MCP_DONE_" + nonce + "__1";
std::string wireLine = ToAnsi(line, cp) + "\r\n";
subCtx->Send2Client((BYTE*)wireLine.data(), (ULONG)wireLine.size());
return line;
}
// 输出清洗(复用 exec_command 尾部逻辑raw → UTF-8 → 剥 ANSI → CRLF 归一 →
// 去 ConPTY 回显命令行 → TrimRight。
static std::string CleanTerminalOutput(const std::vector<BYTE>& raw, UINT cp,
const std::string& echoedLine) {
std::string stdoutStr;
if (!raw.empty()) {
std::string rawStr((const char*)raw.data(), raw.size());
stdoutStr = StripAnsi(ToUtf8(rawStr.c_str(), cp));
size_t p = 0;
while ((p = stdoutStr.find("\r\n", p)) != std::string::npos)
stdoutStr.erase(p, 1);
stdoutStr = StripEchoedCommand(stdoutStr, echoedLine);
stdoutStr = TrimRight(stdoutStr);
}
return stdoutStr;
}
// terminal_open 的 inputSchemaid 必填、timeout_ms 可选)
Json::Value BuildTerminalOpenInputSchema() {
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 timeoutProp(Json::objectValue);
timeoutProp["type"] = "integer";
timeoutProp["description"] = u8"等待 shell 启动的超时毫秒数(可选,默认 20000上限 600000";
props["timeout_ms"] = timeoutProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("id");
schema["required"] = required;
return schema;
}
Json::Value BuildTerminalOpenOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value sid(Json::objectValue);
sid["type"] = "string";
sid["description"] = u8"持久会话 token后续 terminal_exec / terminal_close 用";
props["session_id"] = sid;
Json::Value pty(Json::objectValue);
pty["type"] = "boolean";
pty["description"] = u8"true=ConPTY(UTF-8)false=老 cmd 管道(GBK)";
props["is_pty"] = pty;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("session_id");
required.append("is_pty");
schema["required"] = required;
return schema;
}
Json::Value BuildTerminalExecInputSchema() {
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 sid(Json::objectValue);
sid["type"] = "string";
sid["description"] = u8"terminal_open 返回的 session_id";
props["session_id"] = sid;
Json::Value cmdProp(Json::objectValue);
cmdProp["type"] = "string";
cmdProp["description"] = u8"要执行的命令(完整 shell不受白名单约束但不能含 & 或 |,请拆成多条调用)";
props["command"] = cmdProp;
Json::Value timeoutProp(Json::objectValue);
timeoutProp["type"] = "integer";
timeoutProp["description"] = u8"等待输出完成的超时毫秒数(可选,默认 20000上限 600000";
props["timeout_ms"] = timeoutProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("id");
required.append("session_id");
required.append("command");
schema["required"] = required;
return schema;
}
Json::Value BuildTerminalExecOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value stdoutProp(Json::objectValue);
stdoutProp["type"] = "string";
stdoutProp["description"] = u8"命令输出(已剥哨兵与 ANSI 转义)";
props["stdout"] = stdoutProp;
Json::Value exitProp(Json::objectValue);
exitProp["type"] = "integer";
exitProp["description"] = u8"退出码0=成功、1=非零退出、-1=未知(进程异常退出)";
props["exit_code"] = exitProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("stdout");
required.append("exit_code");
schema["required"] = required;
return schema;
}
Json::Value BuildTerminalCloseInputSchema() {
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 sid(Json::objectValue);
sid["type"] = "string";
sid["description"] = u8"terminal_open 返回的 session_id";
props["session_id"] = sid;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("id");
required.append("session_id");
schema["required"] = required;
return schema;
}
Json::Value BuildTerminalCloseOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value closed(Json::objectValue);
closed["type"] = "boolean";
closed["description"] = u8"恒为 true关闭幂等";
props["closed"] = closed;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("closed");
schema["required"] = required;
return schema;
}
// tools/callterminal_open打开持久 shell 会话,返回 session_id用毕须 terminal_close
std::string BuildTerminalOpen(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
CMcpServer& mcp = CMcpServer::Instance();
mcp.SweepIdleTerminals(kTermIdleTimeoutSec);
uint64_t devId = 0;
context* ctx = nullptr;
std::string errJson;
if (!ResolveTerminalSessionHost(id, args, parent, devId, ctx, errJson))
return errJson;
int timeoutMs = kMcpToolTimeoutMs;
std::string t = GetStringArg(args, "timeout_ms");
if (!t.empty() && IsDigits(t)) {
int v = atoi(t.c_str());
if (v > 0 && v <= 600000) timeoutMs = v;
}
std::string sessionId = GenerateRandomToken();
if (!mcp.BeginTermOpen(devId, sessionId))
return BuildError(id, -32003, "Device busy: a terminal session is already active for this host");
BYTE cmd = COMMAND_SHELL;
if (!ctx->Send2Client(&cmd, 1)) {
mcp.ClearTermPending(devId);
return BuildError(id, -32004, "Failed to send command to host");
}
context* subCtx = nullptr;
bool isPty = false;
if (!mcp.WaitTerminalReady(devId, subCtx, isPty, timeoutMs))
return BuildError(id, -32001, "Timeout waiting for shell to start");
// 审计:打开会话(不可关闭)。
if (parent) {
std::string text = "host " + std::to_string(devId) + " term-open: session " + sessionId;
parent->PostMessageA(WM_SHOWERRORMSG,
(WPARAM)new CString(ToAnsi(text, 936).c_str()),
(LPARAM)new CString(_TR("MCP持久终端")));
}
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["session_id"] = sessionId;
structuredContent["is_pty"] = isPty;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = sessionId;
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// tools/callterminal_exec在已打开的持久会话中执行一条命令无白名单但禁 & |
std::string BuildTerminalExec(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
CMcpServer& mcp = CMcpServer::Instance();
mcp.SweepIdleTerminals(kTermIdleTimeoutSec);
uint64_t devId = 0;
context* ctx = nullptr;
std::string errJson;
if (!ResolveTerminalSessionHost(id, args, parent, devId, ctx, errJson))
return errJson;
std::string sessionId = GetStringArg(args, "session_id");
if (sessionId.empty())
return BuildError(id, -32602, "Missing required parameter: session_id");
std::string command = Trim(GetStringArg(args, "command"));
if (command.empty())
return BuildError(id, -32602, "Missing required parameter: command");
// 正确性门(非安全门):& 与 | 会破坏哨兵控制操作符链、导致退出码失真;拆成多条调用即可。
// \r\n 会把命令拆成多行、只有末行被哨兵包装,同样破坏退出码捕获,一并拒绝。
if (command.find_first_of("&|") != std::string::npos || ContainsControlChar(command))
return BuildError(id, -32008,
"Command contains & or | or a newline (breaks output capture); run chained/piped commands as separate terminal_exec calls");
int timeoutMs = kMcpToolTimeoutMs;
std::string t = GetStringArg(args, "timeout_ms");
if (!t.empty() && IsDigits(t)) {
int v = atoi(t.c_str());
if (v > 0 && v <= 600000) timeoutMs = v;
}
std::string nonce = GenerateRandomToken().substr(0, 8);
context* subCtx = nullptr;
bool isPty = false;
if (!mcp.BeginTermCommand(devId, sessionId, command, nonce, subCtx, isPty))
return BuildError(id, -32002, "Terminal session not found, not ready, or busy: " + sessionId);
std::string sentLine = SendTerminalCommandLine(subCtx, isPty, command, nonce);
std::vector<BYTE> raw;
int exitCode = -1;
bool closed = false;
if (!mcp.WaitTermCommand(devId, raw, exitCode, closed, timeoutMs)) {
subCtx->CancelIO();
return BuildError(id, -32001, "Timeout waiting for command output");
}
if (closed) {
// shell 进程退出:会话已被 WaitTermCommand 清理子链接已死CancelIO 仅对称(无害)。
subCtx->CancelIO();
}
std::string stdoutStr = CleanTerminalOutput(raw, isPty ? CP_UTF8 : 936, sentLine);
// 审计:命令执行(不可关闭)。
if (parent) {
std::string text = "host " + std::to_string(devId) + " term-exec [" + sessionId + "]: " + command;
parent->PostMessageA(WM_SHOWERRORMSG,
(WPARAM)new CString(ToAnsi(text, 936).c_str()),
(LPARAM)new CString(_TR("MCP持久终端")));
}
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["stdout"] = stdoutStr;
structuredContent["exit_code"] = exitCode;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = stdoutStr.empty() ? std::string(u8"(无输出)") : stdoutStr;
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// tools/callterminal_close关闭持久会话幂等session_id 不匹配报错)
std::string BuildTerminalClose(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
CMcpServer& mcp = CMcpServer::Instance();
mcp.SweepIdleTerminals(kTermIdleTimeoutSec);
if (!mcp.IsTerminalEnabled() || mcp.IsReadonly())
return BuildError(id, -32006, "Persistent terminal is disabled: requires McpTerminal=1 and McpReadonly=0");
uint64_t devId = 0;
std::string err;
if (!ParseHostIdArg(args, devId, err))
return BuildError(id, -32602, err);
std::string sessionId = GetStringArg(args, "session_id");
if (sessionId.empty())
return BuildError(id, -32602, "Missing required parameter: session_id");
int r = mcp.CloseTermSession(devId, sessionId);
if (r == 2)
return BuildError(id, -32002, "Terminal session_id mismatch: " + sessionId);
// 审计:关闭会话(不可关闭)。
if (parent) {
std::string text = "host " + std::to_string(devId) + " term-close: session " + sessionId;
parent->PostMessageA(WM_SHOWERRORMSG,
(WPARAM)new CString(ToAnsi(text, 936).c_str()),
(LPARAM)new CString(_TR("MCP持久终端")));
}
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["closed"] = true;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"closed");
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);
if (toolName == "get_audit_log") return BuildGetAuditLog(id, parent);
if (toolName == "list_registry") return BuildListRegistry(id, args, parent);
if (toolName == "exec_command") return BuildExecCommand(id, args, parent);
if (toolName == "terminal_open") return BuildTerminalOpen(id, args, parent);
if (toolName == "terminal_exec") return BuildTerminalExec(id, args, parent);
if (toolName == "terminal_close") return BuildTerminalClose(id, args, parent);
return BuildError(id, -32602,
"Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName));
}
} // namespace
// ===== P3exec_command 哨兵检测与终端会话 =====
// 在原始 shell 输出字节流中定位哨兵(纯 ASCII编码无关。真实哨兵由 echo 单独输出、独占
// 一行(前面是 \n 或缓冲区开头);而 ConPTY 会把整条命令行回显进输出流,哨兵字样嵌在
// "&& echo ... || echo ..." 里、前面是空格。据此从后往前找「行首命中」,排除回显行误判:
// 否则回显包先于命令输出单独到达时rfind 会命中回显里的 __1 提前结束、截断真实输出。
// 老 ShellManager 已自行跳过回显,输出里只有真实哨兵(仍为行首)。命中 __0 → exit 0__1 → exit 1。
static bool FindSentinel(const std::vector<BYTE>& buf, const std::string& nonce,
size_t& pos, int& exitCode) {
std::string marker = "__MCP_DONE_" + nonce + "__";
if (marker.size() + 1 > buf.size()) return false;
std::string s((const char*)buf.data(), buf.size());
size_t from = std::string::npos;
while (true) {
size_t p = s.rfind(marker, from);
if (p == std::string::npos) return false;
bool lineStart = (p == 0) || (s[p - 1] == '\n');
size_t digitPos = p + marker.size();
if (lineStart && digitPos < s.size() &&
(s[digitPos] == '0' || s[digitPos] == '1')) {
exitCode = (s[digitPos] == '0') ? 0 : 1;
pos = p;
return true;
}
if (p == 0) return false;
from = p - 1;
}
}
bool CMcpServer::IsTermPending(uint64_t device_id) {
std::lock_guard<std::mutex> lk(m_TermMutex);
auto it = m_TermSessions.find(device_id);
return it != m_TermSessions.end() && !it->second.started;
}
void CMcpServer::RegisterTerminalContext(uint64_t device_id, context* subCtx, bool isPty) {
{
std::lock_guard<std::mutex> lk(m_TermMutex);
auto it = m_TermSessions.find(device_id);
if (it == m_TermSessions.end() || it->second.started) return; // 没有等它的会话
TermSession& s = it->second;
s.started = true;
s.subCtx = subCtx;
s.isPty = isPty;
m_TermContextToDevice[subCtx] = device_id;
}
// 关键步骤:告知客户端「启动 shell 输出回流」。客户端读线程靠 COMMAND_NEXT 才启动,
// 漏发会导致 shell 在跑但输出永不送回。PTY 还要先告知初始 80x24否则 TUI 尺寸错乱。
if (isPty) {
BYTE resizeBuf[5];
resizeBuf[0] = CMD_TERMINAL_RESIZE;
*(short*)(resizeBuf + 1) = (short)80;
*(short*)(resizeBuf + 3) = (short)24;
subCtx->Send2Client(resizeBuf, 5);
}
BYTE startCmd = COMMAND_NEXT;
subCtx->Send2Client(&startCmd, 1);
m_TermCv.notify_all();
}
bool CMcpServer::IsTerminalContext(context* subCtx) {
std::lock_guard<std::mutex> lk(m_TermMutex);
return m_TermContextToDevice.find(subCtx) != m_TermContextToDevice.end();
}
void CMcpServer::OnTerminalData(context* subCtx, const BYTE* data, ULONG len) {
std::lock_guard<std::mutex> lk(m_TermMutex);
auto it = m_TermContextToDevice.find(subCtx);
if (it == m_TermContextToDevice.end()) return;
auto sit = m_TermSessions.find(it->second);
if (sit == m_TermSessions.end()) return;
TermSession& s = sit->second;
if (s.done || s.closed) return; // 已结束,忽略迟到数据
s.data.insert(s.data.end(), data, data + len);
if (FindSentinel(s.data, s.nonce, s.sentPos, s.exitCode)) {
s.done = true;
m_TermCv.notify_all();
}
}
void CMcpServer::OnTerminalClosed(context* subCtx) {
std::lock_guard<std::mutex> lk(m_TermMutex);
auto it = m_TermContextToDevice.find(subCtx);
if (it == m_TermContextToDevice.end()) return;
auto sit = m_TermSessions.find(it->second);
if (sit == m_TermSessions.end()) return;
TermSession& s = sit->second;
s.closed = true;
if (s.persistent && !s.busy && s.started) {
// 空闲持久会话shell 已退、无等待线程会清理,直接擦路由+会话(无需 CancelIO已死
m_TermContextToDevice.erase(subCtx);
m_TermSessions.erase(sit);
}
m_TermCv.notify_all(); // busy 场景唤醒等待线程由其清理;一次性 exec 路径不变
}
bool CMcpServer::BeginTermPending(uint64_t device_id, const std::string& command, const std::string& nonce) {
std::lock_guard<std::mutex> lk(m_TermMutex);
if (m_TermSessions.find(device_id) != m_TermSessions.end()) return false; // 已有会话
TermSession s;
s.command = command;
s.nonce = nonce;
m_TermSessions[device_id] = std::move(s);
return true;
}
bool CMcpServer::WaitTerminalReady(uint64_t device_id, context*& subCtx, bool& isPty, int timeoutMs) {
std::unique_lock<std::mutex> lk(m_TermMutex);
auto it = m_TermSessions.find(device_id);
if (it == m_TermSessions.end()) return false;
bool signaled = m_TermCv.wait_for(lk, std::chrono::milliseconds(timeoutMs),
[&] { return it->second.started; });
if (!signaled) {
m_TermSessions.erase(it); // 超时 → 清理
return false;
}
subCtx = it->second.subCtx;
isPty = it->second.isPty;
return true;
}
bool CMcpServer::WaitTerminalDone(uint64_t device_id, std::vector<BYTE>& out,
int& exitCode, bool& closed, int timeoutMs) {
std::unique_lock<std::mutex> lk(m_TermMutex);
auto it = m_TermSessions.find(device_id);
if (it == m_TermSessions.end()) return false;
bool signaled = m_TermCv.wait_for(lk, std::chrono::milliseconds(timeoutMs),
[&] { return it->second.done || it->second.closed; });
if (!signaled) { // 超时 → 清理(含路由表)
m_TermContextToDevice.erase(it->second.subCtx);
m_TermSessions.erase(it);
return false;
}
TermSession s = it->second; // 拷贝出,避免擦除后悬空
m_TermContextToDevice.erase(s.subCtx);
m_TermSessions.erase(it);
closed = s.closed;
exitCode = s.exitCode;
if (s.done && s.sentPos <= s.data.size())
out.assign(s.data.begin(), s.data.begin() + s.sentPos); // 截断到哨兵前
else
out = s.data; // 进程退出无哨兵:返回已收集的原始输出
return true;
}
void CMcpServer::ClearTermPending(uint64_t device_id) {
std::lock_guard<std::mutex> lk(m_TermMutex);
auto it = m_TermSessions.find(device_id);
if (it != m_TermSessions.end()) m_TermSessions.erase(it);
}
// ===== P4持久远程终端状态机方法=====
bool CMcpServer::BeginTermOpen(uint64_t device_id, const std::string& sessionId) {
std::lock_guard<std::mutex> lk(m_TermMutex);
if (m_TermSessions.find(device_id) != m_TermSessions.end()) return false; // 单设备单终端
TermSession s;
s.sessionId = sessionId;
s.persistent = true;
s.lastActiveAt = time(nullptr);
m_TermSessions[device_id] = std::move(s);
return true;
}
bool CMcpServer::BeginTermCommand(uint64_t device_id, const std::string& sessionId,
const std::string& command, const std::string& nonce,
context*& subCtx, bool& isPty) {
std::lock_guard<std::mutex> lk(m_TermMutex);
auto it = m_TermSessions.find(device_id);
if (it == m_TermSessions.end()) return false;
TermSession& s = it->second;
if (!s.persistent || s.sessionId != sessionId) return false; // 非持久或 token 不匹配
if (!s.started) return false; // 未就绪
if (s.busy) return false; // 已有命令在飞
if (s.closed) return false; // 已关闭
s.busy = true;
s.command = command;
s.nonce = nonce;
s.data.clear();
s.sentPos = 0;
s.exitCode = -1;
s.done = false;
s.closed = false;
s.lastActiveAt = time(nullptr);
subCtx = s.subCtx;
isPty = s.isPty;
return true;
}
bool CMcpServer::WaitTermCommand(uint64_t device_id, std::vector<BYTE>& out,
int& exitCode, bool& closed, int timeoutMs) {
std::unique_lock<std::mutex> lk(m_TermMutex);
auto it = m_TermSessions.find(device_id);
if (it == m_TermSessions.end()) return false;
bool signaled = m_TermCv.wait_for(lk, std::chrono::milliseconds(timeoutMs),
[&] { return it->second.done || it->second.closed; });
if (!signaled) { // 超时 → 清理会话+路由(调用方负责 CancelIO
context* subCtx = it->second.subCtx;
m_TermContextToDevice.erase(subCtx);
m_TermSessions.erase(it);
return false;
}
if (it->second.closed) { // 进程退出 → 清理,返回已收集输出(无哨兵)
context* subCtx = it->second.subCtx;
out = it->second.data;
exitCode = -1;
closed = true;
m_TermContextToDevice.erase(subCtx);
m_TermSessions.erase(it);
return true;
}
// 哨兵命中 → 保持会话,复位 busy刷新 lastActiveAt避免「长命令刚完成即被 idle 回收」
// idle 语义应为「距上次活动」,而非「距上次命令开始」)。
it->second.busy = false;
it->second.lastActiveAt = time(nullptr);
exitCode = it->second.exitCode;
closed = false;
if (it->second.sentPos <= it->second.data.size())
out.assign(it->second.data.begin(), it->second.data.begin() + it->second.sentPos);
return true;
}
int CMcpServer::CloseTermSession(uint64_t device_id, const std::string& sessionId) {
context* subCtx = nullptr;
{
std::lock_guard<std::mutex> lk(m_TermMutex);
auto it = m_TermSessions.find(device_id);
if (it == m_TermSessions.end()) return 1; // 不存在(幂等)
TermSession& s = it->second;
if (!s.persistent || s.sessionId != sessionId) return 2; // token 不匹配
if (s.busy) {
// 有命令在飞:置 closed 唤醒等待线程,由其清理(避免双重擦除)。
s.closed = true;
m_TermCv.notify_all();
return 0;
}
subCtx = s.subCtx;
m_TermContextToDevice.erase(s.subCtx);
m_TermSessions.erase(it);
}
if (subCtx) subCtx->CancelIO(); // 锁外取消 IO触发客户端 shell 退出
return 0;
}
int CMcpServer::SweepIdleTerminals(time_t idleTimeoutSec) {
time_t now = time(nullptr);
std::vector<context*> toCancel;
{
std::lock_guard<std::mutex> lk(m_TermMutex);
for (auto it = m_TermSessions.begin(); it != m_TermSessions.end(); ) {
TermSession& s = it->second;
if (s.persistent && !s.busy && difftime(now, s.lastActiveAt) > (double)idleTimeoutSec) {
if (s.subCtx) { m_TermContextToDevice.erase(s.subCtx); toCancel.push_back(s.subCtx); }
it = m_TermSessions.erase(it);
} else {
++it;
}
}
}
for (context* c : toCancel) if (c) c->CancelIO(); // 锁外 CancelIO
return (int)toCancel.size();
}
//////////////////////////////////////////////////////////////////////////
// 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
}
// ===== P3list_registry 扩展 =====
bool CMcpServer::OnRegeditReady(uint64_t device_id, context* subCtx) {
std::string rootAndPath;
{
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 != "list_registry") return false;
rootAndPath = it->second.path; // [rootToken:1][相对子键路径...]
}
if (rootAndPath.empty()) return false; // 不应发生list_registry 必有 rootToken
BYTE rootToken = (BYTE)rootAndPath[0];
std::string relPath = rootAndPath.substr(1);
// 下发 COMMAND_REG_FIND布局与 RegisterDlg::OnTvnSelchangedTree 一致
// [COMMAND_REG_FIND][rootToken][relPath...]['\0']。子链接保持,等 TOKEN_REG_PATH + KEY。
std::vector<BYTE> pkt;
pkt.reserve(2 + relPath.size() + 1);
pkt.push_back((BYTE)COMMAND_REG_FIND);
pkt.push_back(rootToken);
pkt.insert(pkt.end(), relPath.begin(), relPath.end());
pkt.push_back(0);
subCtx->Send2Client(pkt.data(), (ULONG)pkt.size());
return true; // 接管子链接,不打开 MFC 对话框
}
void CMcpServer::TakeRegPath(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; // 已超时清理 → 迟到数据丢弃
if (it->second.tool != "list_registry") return;
it->second.regPath.assign(data, data + len);
it->second.pathDone = true;
if (it->second.keyDone) {
it->second.done = true;
m_PendingCv.notify_one();
}
}
void CMcpServer::TakeRegKey(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;
if (it->second.tool != "list_registry") return;
it->second.regKey.assign(data, data + len);
it->second.keyDone = true;
if (it->second.pathDone) {
it->second.done = true;
m_PendingCv.notify_one();
}
}
bool CMcpServer::WaitPendingRegistry(uint64_t device_id, std::vector<BYTE>& subkeys,
std::vector<BYTE>& values, 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) {
m_Pending.erase(it); // 超时 → 清理
return false;
}
subkeys = std::move(it->second.regPath);
values = std::move(it->second.regKey);
m_Pending.erase(it);
return true;
}
// 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;
}