Two independent bugs made MCP terminal tools time out despite the command finishing. First, timeout_ms is declared integer in the tool schemas but was parsed through GetStringArg, which only reads JSON strings. A numeric value fell through to the 20s default, so callers asking for a longer wait were cut off at 20s. exec_command, terminal_open, terminal_exec and remote_open now parse timeout_ms through GetIntArg, which accepts both JSON numbers and digit strings, keeping the 1..600000 range guard. Second, FindSentinel treated the __MCP_DONE_<nonce>__ marker as a line start only when preceded by \n. Commands that produce no output (ping > nul, Start-Sleep, tar -czf) echo their command line ending in \r, so the marker never matched and the wait ran to timeout even though the command had completed. The check now also accepts \r. Co-Authored-By: deepseek-v4-pro
4261 lines
173 KiB
C++
4261 lines
173 KiB
C++
#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 "WebService.h" // 远程控制复用屏幕子连接(StartRemoteDesktop / GetScreenContext / GetScreenSize)
|
||
#include "Server.h" // CONTEXT_OBJECT 定义(GetScreenContext 返回 CONTEXT_OBJECT* → context* 上转型)
|
||
#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 通用辅助 ==========
|
||
|
||
// 小写化(仅 ASCII,UTF-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);
|
||
}
|
||
|
||
// 读取可选整数入参(JSON number 或数字字符串);缺失/非法返回 false(不写 out)。
|
||
// 定义见文件后半段(随 P3 终端会话辅助一起),此处前置声明供早于定义的 exec_command /
|
||
// terminal_exec 等工具复用。
|
||
static bool GetIntArg(const Json::Value& args, const char* key, int& out);
|
||
|
||
// ========== 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;
|
||
}
|
||
|
||
// 有界 strlen:p 最多可读 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;
|
||
// 修改时间(FILETIME,100ns 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.cpp):MREG_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 ==========
|
||
|
||
// 单台主机字段 schema(hosts 数组元素 / 单机详情共用的形状)
|
||
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 的 outputSchema(hosts 数组)
|
||
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 的 inputSchema(id 必填)
|
||
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 的 outputSchema(processes 数组)
|
||
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 的 outputSchema(windows 数组)
|
||
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 的 outputSchema(records 数组 + 原始文本)
|
||
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 的 outputSchema(services 数组)
|
||
Json::Value BuildServiceListOutputSchema() {
|
||
Json::Value props(Json::objectValue);
|
||
|
||
Json::Value svcsProp(Json::objectValue);
|
||
svcsProp["type"] = "array";
|
||
Json::Value items(Json::objectValue);
|
||
items["type"] = "object";
|
||
Json::Value itemProps(Json::objectValue);
|
||
const char* strFields[] = { "display_name", "service_name", "binary_path", "status", "start_type" };
|
||
for (const char* f : strFields) {
|
||
Json::Value s(Json::objectValue);
|
||
s["type"] = "string";
|
||
itemProps[f] = s;
|
||
}
|
||
items["properties"] = itemProps;
|
||
svcsProp["items"] = items;
|
||
props["services"] = svcsProp;
|
||
|
||
Json::Value schema(Json::objectValue);
|
||
schema["type"] = "object";
|
||
schema["properties"] = props;
|
||
Json::Value required(Json::arrayValue);
|
||
required.append("services");
|
||
schema["required"] = required;
|
||
return schema;
|
||
}
|
||
|
||
// list_registry 的 inputSchema(id 必填,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 的 outputSchema(keys 字符串数组 / 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 的 inputSchema(id 必填、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 的 outputSchema(stdout 文本 / 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 的 outputSchema(image 元数据;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 的 inputSchema(id 必填,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 的 inputSchema(id 必填,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 的 outputSchema(drives 数组 / 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);
|
||
|
||
// ===== P5 前置声明(定义见下方「tools/call 分派」前)=====
|
||
Json::Value BuildRemoteOpenInputSchema();
|
||
Json::Value BuildRemoteOpenOutputSchema();
|
||
Json::Value BuildRemoteCloseInputSchema();
|
||
Json::Value BuildRemoteCloseOutputSchema();
|
||
Json::Value BuildRemoteKeyboardInputSchema();
|
||
Json::Value BuildRemoteKeyboardOutputSchema();
|
||
Json::Value BuildRemoteMouseInputSchema();
|
||
Json::Value BuildRemoteMouseOutputSchema();
|
||
Json::Value BuildRemoteClipboardInputSchema();
|
||
Json::Value BuildRemoteClipboardOutputSchema();
|
||
std::string BuildRemoteOpen(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
|
||
std::string BuildRemoteClose(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
|
||
std::string BuildRemoteKeyboard(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
|
||
std::string BuildRemoteMouse(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
|
||
std::string BuildRemoteClipboard(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_hosts(P2a:纯内存过滤,无子链接)
|
||
{
|
||
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_detail(P2a:单机详情,纯内存)
|
||
{
|
||
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_processes(P2b:主连接 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_windows(P2b:主连接 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_history(P2b:主连接 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_screenshot(P2c:屏幕预览链路,主连接 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_files(P2c:文件链路,列盘/列目录)
|
||
{
|
||
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_services(P3:服务链路,仅 Windows,一次性子链接)
|
||
{
|
||
Json::Value tool(Json::objectValue);
|
||
tool["name"] = "list_services";
|
||
tool["description"] = u8"获取指定在线 Windows 主机的服务列表(显示名、服务名、可执行文件路径、运行状态、启动类型)。仅 Windows 客户端支持;一次性返回。";
|
||
|
||
tool["inputSchema"] = BuildGetHostDetailInputSchema(); // 复用 { id } 必填 schema
|
||
tool["outputSchema"] = BuildServiceListOutputSchema();
|
||
|
||
tools.append(tool);
|
||
}
|
||
|
||
// 10) get_client_log(P3:客户端运行日志,一次性子链接取首条全量)
|
||
{
|
||
Json::Value tool(Json::objectValue);
|
||
tool["name"] = "get_client_log";
|
||
tool["description"] = u8"获取指定在线主机 YAMA 客户端的内存运行日志(最近最多 1000 条,含时间戳/源文件/行号)。客户端持续增量上报,本工具取当前时刻的全量快照后即断开。只读。";
|
||
|
||
tool["inputSchema"] = BuildGetHostDetailInputSchema(); // 复用 { id } 必填 schema
|
||
tool["outputSchema"] = BuildClientLogOutputSchema();
|
||
|
||
tools.append(tool);
|
||
}
|
||
|
||
// 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_registry(P3:注册表查询,列根键/列子键+值,仅 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_command(P3:一次性远程命令,仅 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_close(P4:持久远程终端,仅 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);
|
||
}
|
||
}
|
||
|
||
// 15) remote_open / remote_close(P5:MCP 远程控制,仅 Windows,
|
||
// 安全门:McpRemoteControl=1 且 McpReadonly=0;全程审计 + idle 回收)
|
||
if (CMcpServer::Instance().IsRemoteControlEnabled() && !CMcpServer::Instance().IsReadonly()) {
|
||
{
|
||
Json::Value tool(Json::objectValue);
|
||
tool["name"] = "remote_open";
|
||
tool["description"] = u8"在指定在线 Windows 主机上建立远程控制会话(隐藏屏幕子连接)并返回 session_id 与物理屏幕分辨率(screen_w/screen_h)。后续 remote_mouse/remote_keyboard 用归一化坐标(0..1)注入输入,用毕须 remote_close。";
|
||
tool["inputSchema"] = BuildRemoteOpenInputSchema();
|
||
tool["outputSchema"] = BuildRemoteOpenOutputSchema();
|
||
tools.append(tool);
|
||
}
|
||
{
|
||
Json::Value tool(Json::objectValue);
|
||
tool["name"] = "remote_close";
|
||
tool["description"] = u8"关闭并释放指定远程控制会话(幂等:会话已不存在也返回成功)。";
|
||
tool["inputSchema"] = BuildRemoteCloseInputSchema();
|
||
tool["outputSchema"] = BuildRemoteCloseOutputSchema();
|
||
tools.append(tool);
|
||
}
|
||
{
|
||
Json::Value tool(Json::objectValue);
|
||
tool["name"] = "remote_keyboard";
|
||
tool["description"] = u8"向已建立的远程控制会话注入键盘事件。action 可选 key_down / key_up / key_press / type:前三种需 key(Windows 虚拟键名,如 ENTER/TAB/F5/LEFT/CTRL/ALT/SHIFT/WIN),type 需 text(仅 ASCII 文本;非 ASCII 请用 remote_clipboard + Ctrl+V)。modifiers 可选(CTRL/ALT/SHIFT/WIN)。";
|
||
tool["inputSchema"] = BuildRemoteKeyboardInputSchema();
|
||
tool["outputSchema"] = BuildRemoteKeyboardOutputSchema();
|
||
tools.append(tool);
|
||
}
|
||
{
|
||
Json::Value tool(Json::objectValue);
|
||
tool["name"] = "remote_mouse";
|
||
tool["description"] = u8"向已建立的远程控制会话注入鼠标事件。action 可选 move / down / up / click / right_click / middle_click / drag / scroll;坐标 x/y(及 drag 的 x2/y2)为归一化 0..1 浮点。button 可选 left/middle/right(默认 left);click 的 clicks 可选 1/2/3(默认 1);scroll 的 delta 正=向下滚、负=向上滚。";
|
||
tool["inputSchema"] = BuildRemoteMouseInputSchema();
|
||
tool["outputSchema"] = BuildRemoteMouseOutputSchema();
|
||
tools.append(tool);
|
||
}
|
||
{
|
||
Json::Value tool(Json::objectValue);
|
||
tool["name"] = "remote_clipboard";
|
||
tool["description"] = u8"把文本写入远程主机的剪贴板(UTF-8 → GBK/ANSI,非 GBK 字符如 emoji 会丢失)。仅设置剪贴板,粘贴需随后用 remote_keyboard 注入 Ctrl+V。";
|
||
tool["inputSchema"] = BuildRemoteClipboardInputSchema();
|
||
tool["outputSchema"] = BuildRemoteClipboardOutputSchema();
|
||
tools.append(tool);
|
||
}
|
||
}
|
||
|
||
result["tools"] = tools;
|
||
return BuildResult(id, result);
|
||
}
|
||
|
||
// tools/call:list_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/call:search_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/call:get_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/call:list_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 等)
|
||
// =客户端 ANSI(GBK/936),不随 CLIENT_CAP_UTF8 转 UTF-8;LNX/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/call:list_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/call:get_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/call:get_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/call:list_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-8;LNX/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/call:list_services(主连接下发 COMMAND_SERVICES,子连接一次性回传 TOKEN_SERVERLIST)
|
||
std::string BuildListServices(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||
uint64_t devId = 0;
|
||
std::string err;
|
||
if (!ParseHostIdArg(args, devId, err))
|
||
return BuildError(id, -32602, err);
|
||
|
||
context* ctx = FindMainContext(parent, devId);
|
||
if (!ctx)
|
||
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
|
||
|
||
// 服务管理仅 Windows 客户端实现(EnumServicesStatus 等 A 接口);LNX/MAC 无 Windows
|
||
// 服务概念,提前返回避免 20s 超时等待。
|
||
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
||
if (clientType == "LNX" || clientType == "MAC")
|
||
return BuildError(id, -32005, "list_services is only supported on Windows hosts");
|
||
|
||
CMcpServer& mcp = CMcpServer::Instance();
|
||
if (!mcp.BeginPending(devId, "list_services"))
|
||
return BuildError(id, -32003, "Device busy: another request is pending for this host");
|
||
|
||
BYTE cmd = COMMAND_SERVICES;
|
||
if (!ctx->Send2Client(&cmd, 1)) {
|
||
mcp.ClearPending(devId);
|
||
return BuildError(id, -32004, "Failed to send command to host");
|
||
}
|
||
|
||
std::vector<BYTE> data;
|
||
if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs))
|
||
return BuildError(id, -32001, "Timeout waiting for service list");
|
||
|
||
// 服务名/路径/显示名来自 Windows A 接口 = 客户端 ANSI(GBK/936),按 clientType 判定。
|
||
UINT cp = (clientType == "LNX" || clientType == "MAC") ? CP_UTF8 : 936;
|
||
Json::Value services = ParseServiceList(data, cp);
|
||
int count = (int)services.size();
|
||
|
||
Json::Value result(Json::objectValue);
|
||
Json::Value structuredContent(Json::objectValue);
|
||
structuredContent["services"] = services;
|
||
result["structuredContent"] = structuredContent;
|
||
|
||
Json::Value content(Json::arrayValue);
|
||
Json::Value item(Json::objectValue);
|
||
item["type"] = "text";
|
||
item["text"] = std::string(u8"共 ") + std::to_string(count) + std::string(u8" 个服务。");
|
||
content.append(item);
|
||
result["content"] = content;
|
||
result["isError"] = false;
|
||
|
||
return BuildResult(id, result);
|
||
}
|
||
|
||
// tools/call:list_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)");
|
||
|
||
// 注册表键路径为客户端 ANSI(GBK/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);
|
||
}
|
||
|
||
// ========== P3:exec_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/call:exec_command(Windows 一次性远程命令:主连接 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;
|
||
int v = 0;
|
||
if (GetIntArg(args, "timeout_ms", v) && 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/call:get_client_log(主连接下发 COMMAND_QUERY_LOG,子连接回传 TOKEN_REPORT_LOG)
|
||
std::string BuildGetClientLog(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||
uint64_t devId = 0;
|
||
std::string err;
|
||
if (!ParseHostIdArg(args, devId, err))
|
||
return BuildError(id, -32602, err);
|
||
|
||
context* ctx = FindMainContext(parent, devId);
|
||
if (!ctx)
|
||
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
|
||
|
||
CMcpServer& mcp = CMcpServer::Instance();
|
||
if (!mcp.BeginPending(devId, "get_client_log"))
|
||
return BuildError(id, -32003, "Device busy: another request is pending for this host");
|
||
|
||
BYTE cmd = COMMAND_QUERY_LOG;
|
||
if (!ctx->Send2Client(&cmd, 1)) {
|
||
mcp.ClearPending(devId);
|
||
return BuildError(id, -32004, "Failed to send command to host");
|
||
}
|
||
|
||
std::vector<BYTE> data;
|
||
if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs))
|
||
return BuildError(id, -32001, "Timeout waiting for client log");
|
||
|
||
// data[0]=token,其后为 Logger 内存 ring buffer 的日志文本(无 '\0' 终止)。
|
||
// 客户端在子连接建立时 m_sentIdx=0 返回全量,随后每 3s 推增量;MessageHandle 在
|
||
// 取走首条全量后立即 CancelIO 关子链接,增量不再到达,故此处即为完整快照。
|
||
// 日志文本为客户端 ANSI(Windows 走 vsnprintf A 版),按 clientType 判定编码。
|
||
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
||
UINT cp = (clientType == "LNX" || clientType == "MAC") ? CP_UTF8 : 936;
|
||
|
||
std::string log;
|
||
if (data.size() > 1) {
|
||
std::string raw((const char*)data.data() + 1, data.size() - 1);
|
||
log = ToUtf8(raw.c_str(), cp);
|
||
}
|
||
|
||
Json::Value result(Json::objectValue);
|
||
Json::Value structuredContent(Json::objectValue);
|
||
structuredContent["log"] = log;
|
||
result["structuredContent"] = structuredContent;
|
||
|
||
Json::Value content(Json::arrayValue);
|
||
Json::Value item(Json::objectValue);
|
||
item["type"] = "text";
|
||
item["text"] = log.empty() ? std::string(u8"客户端暂无内存日志。")
|
||
: std::string(u8"客户端运行日志快照如下。");
|
||
content.append(item);
|
||
result["content"] = content;
|
||
result["isError"] = false;
|
||
|
||
return BuildResult(id, result);
|
||
}
|
||
|
||
// tools/call:get_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 的 inputSchema(id 必填、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/call:terminal_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;
|
||
int v = 0;
|
||
if (GetIntArg(args, "timeout_ms", v) && 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/call:terminal_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;
|
||
int v = 0;
|
||
if (GetIntArg(args, "timeout_ms", v) && 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/call:terminal_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);
|
||
}
|
||
|
||
// ===== P5:MCP 远程控制(remote_open / remote_close)=====
|
||
|
||
static const int kScreenCtrlIdleTimeoutSec = 300; // 远程控制 idle 回收超时(秒)
|
||
|
||
// 共享前置:校验 McpRemoteControl && !readonly → 解析 id → FindMainContext → 拒绝 LNX/MAC。
|
||
// 成功返回 true 并输出 devId/ctx;失败时 errJson 已写入对应 JSON 错误串。
|
||
static bool ResolveScreenCtrlSessionHost(const Json::Value& id, const Json::Value& args,
|
||
CMy2015RemoteDlg* parent,
|
||
uint64_t& devId, context*& ctx, std::string& errJson) {
|
||
CMcpServer& mcp = CMcpServer::Instance();
|
||
if (!mcp.IsRemoteControlEnabled() || mcp.IsReadonly()) {
|
||
errJson = BuildError(id, -32006,
|
||
"Remote control is disabled: requires McpRemoteControl=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, "Remote control is only supported on Windows hosts");
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// remote_open 的 inputSchema(id 必填、timeout_ms 可选)
|
||
Json::Value BuildRemoteOpenInputSchema() {
|
||
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"等待屏幕子连接建立 + 分辨率到达的超时毫秒数(可选,默认 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 BuildRemoteOpenOutputSchema() {
|
||
Json::Value props(Json::objectValue);
|
||
Json::Value sid(Json::objectValue);
|
||
sid["type"] = "string";
|
||
sid["description"] = u8"控制会话 token,后续 remote_mouse / remote_keyboard / remote_close 用";
|
||
props["session_id"] = sid;
|
||
Json::Value sw(Json::objectValue);
|
||
sw["type"] = "integer";
|
||
sw["description"] = u8"物理屏幕宽度(虚拟桌面像素),用于把归一化坐标 0..1 映射为像素";
|
||
props["screen_w"] = sw;
|
||
Json::Value sh(Json::objectValue);
|
||
sh["type"] = "integer";
|
||
sh["description"] = u8"物理屏幕高度(虚拟桌面像素)";
|
||
props["screen_h"] = sh;
|
||
Json::Value schema(Json::objectValue);
|
||
schema["type"] = "object";
|
||
schema["properties"] = props;
|
||
Json::Value required(Json::arrayValue);
|
||
required.append("session_id");
|
||
required.append("screen_w");
|
||
required.append("screen_h");
|
||
schema["required"] = required;
|
||
return schema;
|
||
}
|
||
|
||
Json::Value BuildRemoteCloseInputSchema() {
|
||
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"remote_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 BuildRemoteCloseOutputSchema() {
|
||
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/call:remote_open(建立隐藏屏幕子连接 + 取物理分辨率)
|
||
std::string BuildRemoteOpen(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||
CMcpServer& mcp = CMcpServer::Instance();
|
||
mcp.SweepIdleScreenCtrl(kScreenCtrlIdleTimeoutSec);
|
||
|
||
uint64_t devId = 0;
|
||
context* ctx = nullptr;
|
||
std::string errJson;
|
||
if (!ResolveScreenCtrlSessionHost(id, args, parent, devId, ctx, errJson))
|
||
return errJson;
|
||
|
||
// 多显示器门(§7.2 / §12.1):Observe 抓主屏,Act 注入在虚拟桌面空间,两者仅单显示器时
|
||
// 重合。客户端心跳 RES_RESOLUTION = "N:W*H"(N=显示器数);N>1 时归一化坐标会系统性
|
||
// 偏移,阶段一直接拒绝(-32008)。无冒号(老客户端格式)则无法判定,放行不拦。
|
||
{
|
||
CString res = ctx->GetAdditionalData(RES_RESOLUTION);
|
||
int colon = res.Find(':');
|
||
if (colon > 0) {
|
||
CString countStr = res.Left(colon);
|
||
int monitorCount = atoi(countStr);
|
||
if (monitorCount > 1)
|
||
return BuildError(id, -32008,
|
||
"Multi-monitor hosts are not supported yet (phase 1): monitor count " +
|
||
std::to_string(monitorCount));
|
||
}
|
||
}
|
||
|
||
// 远程控制复用 Web 的隐藏屏幕子连接(COMMAND_SCREEN_SPY → CScreenSpyDlg → RegisterScreenContext)。
|
||
// 该路径依赖 Web 远程服务已启动(WebSvrPort>0,默认开启);未启动时子连接无法建立/注册。
|
||
if (!WebService().IsRunning())
|
||
return BuildError(id, -32005,
|
||
"Remote control requires the Web remote service (WebSvrPort) to be enabled");
|
||
|
||
// 与人类远程桌面观看互斥:已有屏幕子连接(web 观看)→ 拒绝,避免向人类正在看的画面注入。
|
||
if (WebService().HasActiveSession(devId))
|
||
return BuildError(id, -32003, "Device busy: a remote desktop session is already active for this host");
|
||
|
||
int timeoutMs = kMcpToolTimeoutMs;
|
||
int v = 0;
|
||
if (GetIntArg(args, "timeout_ms", v) && v > 0 && v <= 600000) timeoutMs = v;
|
||
|
||
std::string sessionId = GenerateRandomToken();
|
||
if (!mcp.BeginScreenCtrlOpen(devId, sessionId))
|
||
return BuildError(id, -32003, "Device busy: a remote control session is already active for this host");
|
||
|
||
// 建立隐藏屏幕子连接(发 COMMAND_SCREEN_SPY、标记 web-triggered)。
|
||
if (!WebService().StartRemoteDesktop(devId)) {
|
||
mcp.CloseScreenCtrlSession(devId, sessionId);
|
||
// StartRemoteDesktop 先置 web-triggered 再 Send2Client,发送失败需一并清理,
|
||
// 否则该设备残留 web-triggered 标记、后续人类 web 观看会误走隐藏会话路径。
|
||
WebService().ClearWebTriggered(devId);
|
||
return BuildError(id, -32004, "Failed to start remote desktop session");
|
||
}
|
||
|
||
// 轮询等待子连接就绪(HasActiveSession)+ 分辨率到达(TOKEN_BITMAPINFO → NotifyResolutionChange)。
|
||
// 屏幕子连接的注册/分辨率由 CScreenSpyDlg 完成,无 McpServer 回调,故用轮询(open 低频)。
|
||
int screenW = 0, screenH = 0;
|
||
context* subCtx = nullptr;
|
||
auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs);
|
||
bool ready = false;
|
||
while (std::chrono::steady_clock::now() < deadline) {
|
||
if (WebService().HasActiveSession(devId) && WebService().GetScreenSize(devId, screenW, screenH)) {
|
||
subCtx = WebService().GetScreenContext(devId);
|
||
ready = (subCtx != nullptr);
|
||
break;
|
||
}
|
||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||
}
|
||
|
||
if (!ready) {
|
||
mcp.CloseScreenCtrlSession(devId, sessionId);
|
||
if (parent) parent->CloseWebRemoteDesktopByClientID(devId);
|
||
WebService().ClearWebTriggered(devId);
|
||
return BuildError(id, -32001, "Timeout waiting for screen sub-connection / resolution");
|
||
}
|
||
|
||
if (!mcp.MarkScreenCtrlReady(devId, sessionId, subCtx, screenW, screenH)) {
|
||
// 并发 remote_close 已关掉会话:放弃本次 open(子连接已由 close 关闭)。
|
||
return BuildError(id, -32002, "Remote control session was closed concurrently: " + sessionId);
|
||
}
|
||
|
||
// 审计:打开会话(不可关闭)。
|
||
if (parent) {
|
||
std::string text = "host " + std::to_string(devId) + " remote-open: session " + sessionId
|
||
+ " (" + std::to_string(screenW) + "x" + std::to_string(screenH) + ")";
|
||
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["screen_w"] = screenW;
|
||
structuredContent["screen_h"] = screenH;
|
||
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/call:remote_close(关闭控制会话;幂等,session_id 不匹配报错)
|
||
std::string BuildRemoteClose(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||
CMcpServer& mcp = CMcpServer::Instance();
|
||
mcp.SweepIdleScreenCtrl(kScreenCtrlIdleTimeoutSec);
|
||
|
||
if (!mcp.IsRemoteControlEnabled() || mcp.IsReadonly())
|
||
return BuildError(id, -32006, "Remote control is disabled: requires McpRemoteControl=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.CloseScreenCtrlSession(devId, sessionId);
|
||
if (r == 2)
|
||
return BuildError(id, -32002, "Remote control session_id mismatch: " + sessionId);
|
||
|
||
// 仅当确实关闭了 MCP 会话(r==0)时才关闭隐藏屏幕子连接;r==1(幂等,会话已不存在,
|
||
// 如已被 idle 回收)时不得误关人类正在观看的 web 会话(CloseWebRemoteDesktopByClientID
|
||
// 会关掉该设备任一 IsWebSession 的对话框)。
|
||
if (r == 0) {
|
||
// 关闭隐藏屏幕子连接(幂等):WM_CLOSE → CScreenSpyDlg 析构 → UnregisterScreenContext。
|
||
if (parent) parent->CloseWebRemoteDesktopByClientID(devId);
|
||
WebService().ClearWebTriggered(devId);
|
||
}
|
||
|
||
// 审计:关闭会话(不可关闭)。
|
||
if (parent) {
|
||
std::string text = "host " + std::to_string(devId) + " remote-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);
|
||
}
|
||
|
||
// ===== P5:remote_keyboard(键盘注入,key_down / key_up / key_press / type)=====
|
||
|
||
// 键名 → 虚拟键码(对齐 Windows VK_* 去掉前缀,见设计 §4.3)。
|
||
// 支持:单字符 A-Z/0-9、F1-F24、命名键、修饰键。未知返回 0(VK 0 非真实键)。
|
||
static int MapKeyNameToVk(const std::string& nameIn) {
|
||
std::string n;
|
||
for (char c : nameIn)
|
||
n += (c >= 'a' && c <= 'z') ? (char)(c - 'a' + 'A') : c;
|
||
|
||
if (n.size() == 1) {
|
||
unsigned char c = (unsigned char)n[0];
|
||
if (c >= 'A' && c <= 'Z') return c; // 'A'..'Z' == VK_A..VK_Z
|
||
if (c >= '0' && c <= '9') return c; // '0'..'9' == VK_0..VK_9
|
||
return 0; // 标点等请走 type
|
||
}
|
||
|
||
// F1..F24
|
||
if (n.size() >= 2 && n.size() <= 3 && n[0] == 'F') {
|
||
int fnum = atoi(n.c_str() + 1);
|
||
if (fnum >= 1 && fnum <= 24) return VK_F1 + (fnum - 1);
|
||
return 0;
|
||
}
|
||
|
||
struct Named { const char* name; int vk; };
|
||
static const Named kNames[] = {
|
||
{"ENTER", VK_RETURN}, {"RETURN", VK_RETURN}, {"TAB", VK_TAB}, {"SPACE", VK_SPACE},
|
||
{"ESC", VK_ESCAPE}, {"ESCAPE", VK_ESCAPE}, {"BACKSPACE", VK_BACK},
|
||
{"DELETE", VK_DELETE}, {"DEL", VK_DELETE}, {"INSERT", VK_INSERT}, {"INS", VK_INSERT},
|
||
{"HOME", VK_HOME}, {"END", VK_END}, {"PAGEUP", VK_PRIOR}, {"PRIOR", VK_PRIOR},
|
||
{"PAGEDOWN", VK_NEXT}, {"NEXT", VK_NEXT},
|
||
{"LEFT", VK_LEFT}, {"RIGHT", VK_RIGHT}, {"UP", VK_UP}, {"DOWN", VK_DOWN},
|
||
{"CAPSLOCK", VK_CAPITAL}, {"CAPITAL", VK_CAPITAL}, {"NUMLOCK", VK_NUMLOCK},
|
||
{"SCROLLLOCK", VK_SCROLL}, {"PRINTSCREEN", VK_SNAPSHOT}, {"SNAPSHOT", VK_SNAPSHOT},
|
||
{"PAUSE", VK_PAUSE}, {"APPS", VK_APPS}, {"CONTEXTMENU", VK_APPS},
|
||
{"CTRL", VK_CONTROL}, {"CONTROL", VK_CONTROL}, {"ALT", VK_MENU}, {"MENU", VK_MENU},
|
||
{"SHIFT", VK_SHIFT}, {"WIN", VK_LWIN}, {"WINDOWS", VK_LWIN}, {"CMD", VK_LWIN}, {"SUPER", VK_LWIN},
|
||
};
|
||
for (const Named& e : kNames)
|
||
if (n == e.name) return e.vk;
|
||
return 0;
|
||
}
|
||
|
||
// 是否修饰键(modifiers 白名单:CTRL/ALT/SHIFT/WIN)
|
||
static bool IsModifierVk(int vk) {
|
||
return vk == VK_CONTROL || vk == VK_MENU || vk == VK_SHIFT || vk == VK_LWIN || vk == VK_RWIN;
|
||
}
|
||
|
||
// 解析可选 modifiers 数组(JSON 字符串数组)→ 修饰键 VK 序列。非法返回 false 并填 err。
|
||
static bool ParseModifiers(const Json::Value& args, std::vector<int>& out, std::string& err) {
|
||
out.clear();
|
||
if (!args.isMember("modifiers")) return true;
|
||
const Json::Value& mods = args["modifiers"];
|
||
if (mods.isNull()) return true;
|
||
if (!mods.isArray()) { err = "modifiers must be an array of strings"; return false; }
|
||
for (const auto& m : mods) {
|
||
if (out.size() >= 8) { err = "too many modifiers (max 8)"; return false; }
|
||
int vk = MapKeyNameToVk(m.asString());
|
||
if (vk == 0 || !IsModifierVk(vk)) {
|
||
err = "invalid modifier: " + (m.isString() ? m.asString() : std::string("(non-string)"));
|
||
return false;
|
||
}
|
||
out.push_back(vk);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// 是否纯 ASCII(type 走物理键事件,只能可靠注入 ASCII/ANSI;见设计 §6.4)
|
||
static bool IsAscii(const std::string& s) {
|
||
for (unsigned char c : s) if (c >= 0x80) return false;
|
||
return true;
|
||
}
|
||
|
||
// type:逐字符 VkKeyScanA 映射为「(Shift↓)字符↓ 字符↑(Shift↑)」,回车/制表符映射为按键。
|
||
// 注:VkKeyScanA 在服务端按服务端线程键盘布局解析字符→虚拟键;仅限 ASCII(0..127)时
|
||
// 各布局的字母/数字/常用标点虚拟键一致,跨布局差异可忽略(非 ASCII 一律走剪贴板)。
|
||
static void BuildTypeBatch(const std::string& text, std::vector<MSG64>& batch) {
|
||
for (unsigned char ch : text) {
|
||
if (ch == '\r' || ch == '\n') {
|
||
batch.push_back(BuildKeyMsg64(VK_RETURN, true, false));
|
||
batch.push_back(BuildKeyMsg64(VK_RETURN, false, false));
|
||
continue;
|
||
}
|
||
if (ch == '\t') {
|
||
batch.push_back(BuildKeyMsg64(VK_TAB, true, false));
|
||
batch.push_back(BuildKeyMsg64(VK_TAB, false, false));
|
||
continue;
|
||
}
|
||
SHORT scan = VkKeyScanA((char)ch);
|
||
if (scan == -1) continue; // 无法映射(控制字符等)→ 跳过
|
||
int vk = scan & 0xFF;
|
||
bool needShift = (scan >> 8) & 1;
|
||
if (needShift) batch.push_back(BuildKeyMsg64(VK_SHIFT, true, false));
|
||
batch.push_back(BuildKeyMsg64(vk, true, false));
|
||
batch.push_back(BuildKeyMsg64(vk, false, false));
|
||
if (needShift) batch.push_back(BuildKeyMsg64(VK_SHIFT, false, false));
|
||
}
|
||
}
|
||
|
||
Json::Value BuildRemoteKeyboardInputSchema() {
|
||
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"remote_open 返回的 session_id";
|
||
props["session_id"] = sid;
|
||
|
||
Json::Value act(Json::objectValue);
|
||
act["type"] = "string";
|
||
act["description"] = u8"动作:key_down / key_up / key_press / type";
|
||
props["action"] = act;
|
||
|
||
Json::Value key(Json::objectValue);
|
||
key["type"] = "string";
|
||
key["description"] = u8"按键(key_down/key_up/key_press 用):Windows 虚拟键名(如 ENTER/TAB/F5/LEFT/CTRL/ALT/SHIFT/WIN)或单个字母/数字";
|
||
props["key"] = key;
|
||
|
||
Json::Value text(Json::objectValue);
|
||
text["type"] = "string";
|
||
text["description"] = u8"文本(type 用,仅 ASCII;非 ASCII 请走 remote_clipboard + Ctrl+V)";
|
||
props["text"] = text;
|
||
|
||
Json::Value mods(Json::objectValue);
|
||
mods["type"] = "array";
|
||
Json::Value modItem(Json::objectValue);
|
||
modItem["type"] = "string";
|
||
mods["items"] = modItem;
|
||
mods["description"] = u8"修饰键(可选):CTRL/ALT/SHIFT/WIN";
|
||
props["modifiers"] = mods;
|
||
|
||
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("action");
|
||
schema["required"] = required;
|
||
return schema;
|
||
}
|
||
|
||
Json::Value BuildRemoteKeyboardOutputSchema() {
|
||
Json::Value props(Json::objectValue);
|
||
Json::Value schema(Json::objectValue);
|
||
schema["type"] = "object";
|
||
schema["properties"] = props;
|
||
return schema;
|
||
}
|
||
|
||
// tools/call:remote_keyboard(键盘注入;成功返回空对象)
|
||
std::string BuildRemoteKeyboard(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||
CMcpServer& mcp = CMcpServer::Instance();
|
||
mcp.SweepIdleScreenCtrl(kScreenCtrlIdleTimeoutSec);
|
||
|
||
uint64_t devId = 0;
|
||
context* ctx = nullptr;
|
||
std::string errJson;
|
||
if (!ResolveScreenCtrlSessionHost(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 action = GetStringArg(args, "action");
|
||
if (action.empty())
|
||
return BuildError(id, -32602, "Missing required parameter: action");
|
||
|
||
std::string keyName = GetStringArg(args, "key");
|
||
std::string text = GetStringArg(args, "text");
|
||
std::vector<int> mods;
|
||
{
|
||
std::string mErr;
|
||
if (!ParseModifiers(args, mods, mErr))
|
||
return BuildError(id, -32602, mErr);
|
||
}
|
||
|
||
// 组装注入批次(按发送顺序)
|
||
std::vector<MSG64> batch;
|
||
if (action == "type") {
|
||
if (text.empty())
|
||
return BuildError(id, -32602, "Missing required parameter: text");
|
||
if (!IsAscii(text))
|
||
return BuildError(id, -32602, "text must be ASCII; non-ASCII text requires remote_clipboard + Ctrl+V");
|
||
if (text.size() > 1024)
|
||
return BuildError(id, -32602, "text too long (max 1024 bytes); use remote_clipboard + Ctrl+V for long text");
|
||
BuildTypeBatch(text, batch);
|
||
} else if (action == "key_down" || action == "key_up" || action == "key_press") {
|
||
if (keyName.empty())
|
||
return BuildError(id, -32602, "Missing required parameter: key");
|
||
int keyVk = MapKeyNameToVk(keyName);
|
||
if (keyVk == 0)
|
||
return BuildError(id, -32602, "Unknown key: " + keyName);
|
||
bool doDown = (action != "key_up");
|
||
bool doUp = (action != "key_down");
|
||
if (doDown) {
|
||
for (int m : mods) batch.push_back(BuildKeyMsg64(m, true, false));
|
||
batch.push_back(BuildKeyMsg64(keyVk, true, false));
|
||
}
|
||
if (doUp) {
|
||
batch.push_back(BuildKeyMsg64(keyVk, false, false));
|
||
for (auto it = mods.rbegin(); it != mods.rend(); ++it)
|
||
batch.push_back(BuildKeyMsg64(*it, false, false));
|
||
}
|
||
} else {
|
||
return BuildError(id, -32602, "Unknown action: " + action);
|
||
}
|
||
|
||
if (batch.empty())
|
||
return BuildError(id, -32602, "No key events generated");
|
||
|
||
// 校验 session_id/busy 并置 busy(注入期间 close/sweep/断线不擦会话,镜像终端 busy 模式)。
|
||
context* subCtx = nullptr;
|
||
int screenW = 0, screenH = 0;
|
||
int r = mcp.BeginScreenCtrlAction(devId, sessionId, subCtx, screenW, screenH);
|
||
if (r == 2) return BuildError(id, -32003, "Device busy: another injection is in flight for this session");
|
||
if (r == 1) return BuildError(id, -32002, "Remote control session not found, not ready, or closed: " + sessionId);
|
||
|
||
// 组包 [COMMAND_SCREEN_CONTROL][MSG64*N],经屏幕子连接发送(主连接无效,设计 P2)。
|
||
const int len = (int)(1 + batch.size() * sizeof(MSG64));
|
||
std::vector<BYTE> packet((size_t)len);
|
||
packet[0] = COMMAND_SCREEN_CONTROL;
|
||
memcpy(packet.data() + 1, batch.data(), batch.size() * sizeof(MSG64));
|
||
bool ok = subCtx->Send2Client(packet.data(), (ULONG)len) != FALSE;
|
||
|
||
mcp.EndScreenCtrlAction(devId, sessionId);
|
||
|
||
if (!ok)
|
||
return BuildError(id, -32004, "Failed to send keyboard injection");
|
||
|
||
// 审计(不可关闭)
|
||
if (parent) {
|
||
std::string audit = "host " + std::to_string(devId) + " remote-keyboard: session " + sessionId
|
||
+ " action=" + action;
|
||
if (action == "type") audit += " text=" + text;
|
||
else audit += " key=" + keyName;
|
||
parent->PostMessageA(WM_SHOWERRORMSG,
|
||
(WPARAM)new CString(ToAnsi(audit, 936).c_str()),
|
||
(LPARAM)new CString(_TR("MCP远程控制")));
|
||
}
|
||
|
||
Json::Value result(Json::objectValue);
|
||
result["structuredContent"] = Json::Value(Json::objectValue);
|
||
Json::Value content(Json::arrayValue);
|
||
Json::Value item(Json::objectValue);
|
||
item["type"] = "text";
|
||
item["text"] = std::string(u8"ok");
|
||
content.append(item);
|
||
result["content"] = content;
|
||
result["isError"] = false;
|
||
|
||
return BuildResult(id, result);
|
||
}
|
||
|
||
// ===== P5:remote_mouse(鼠标注入,move/down/up/click/right_click/middle_click/drag/scroll)=====
|
||
|
||
// 读取归一化坐标(0..1 浮点,接受 JSON number 或数字字符串),越界钳到 [0,1]。
|
||
static bool GetNormCoord(const Json::Value& args, const char* key, double& out, std::string& err) {
|
||
if (!args.isMember(key)) { err = std::string("Missing required parameter: ") + key; return false; }
|
||
const Json::Value& v = args[key];
|
||
if (v.isNumeric()) {
|
||
out = v.asDouble();
|
||
} else if (v.isString()) {
|
||
const std::string s = v.asString();
|
||
if (s.empty()) { err = std::string("Invalid ") + key + ": expected a number in 0..1"; return false; }
|
||
char* end = nullptr;
|
||
double d = strtod(s.c_str(), &end);
|
||
if (end == s.c_str() || *end != '\0') { err = std::string("Invalid ") + key + ": expected a number in 0..1"; return false; }
|
||
out = d;
|
||
} else {
|
||
err = std::string("Invalid ") + key + ": expected a number in 0..1";
|
||
return false;
|
||
}
|
||
if (out != out) { // NaN(如字符串 "nan")会绕过钳制并令 (int)(n*screen) 未定义行为
|
||
err = std::string("Invalid ") + key + ": not a finite number";
|
||
return false;
|
||
}
|
||
if (out < 0.0) out = 0.0;
|
||
if (out > 1.0) out = 1.0;
|
||
return true;
|
||
}
|
||
|
||
// 读取可选整数参数(JSON number 或数字字符串);缺失/非法返回 false(不写 out)。
|
||
static bool GetIntArg(const Json::Value& args, const char* key, int& out) {
|
||
if (!args.isMember(key)) return false;
|
||
const Json::Value& v = args[key];
|
||
if (v.isNumeric()) { out = v.asInt(); return true; }
|
||
if (v.isString()) {
|
||
const std::string s = v.asString();
|
||
if (IsDigits(s) || (!s.empty() && s[0] == '-' && IsDigits(s.substr(1)))) {
|
||
out = atoi(s.c_str());
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// button 字符串 → 0=左/1=中/2=右;空或 left 默认左。
|
||
static bool ParseButton(const std::string& s, int& out) {
|
||
if (s.empty() || s == "left") { out = 0; return true; }
|
||
if (s == "middle") { out = 1; return true; }
|
||
if (s == "right") { out = 2; return true; }
|
||
return false;
|
||
}
|
||
|
||
// button → 按下/抬起消息 + 按下 wParam(对齐 WebService::HandleMouse)。
|
||
static void ButtonMessages(int button, UINT& downMsg, UINT& upMsg, uint64_t& downWParam) {
|
||
if (button == 1) { downMsg = WM_MBUTTONDOWN; upMsg = WM_MBUTTONUP; downWParam = MK_MBUTTON; }
|
||
else if (button == 2) { downMsg = WM_RBUTTONDOWN; upMsg = WM_RBUTTONUP; downWParam = MK_RBUTTON; }
|
||
else { downMsg = WM_LBUTTONDOWN; upMsg = WM_LBUTTONUP; downWParam = MK_LBUTTON; }
|
||
}
|
||
|
||
Json::Value BuildRemoteMouseInputSchema() {
|
||
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"remote_open 返回的 session_id";
|
||
props["session_id"] = sid;
|
||
|
||
Json::Value act(Json::objectValue);
|
||
act["type"] = "string";
|
||
act["description"] = u8"动作:move / down / up / click / right_click / middle_click / drag / scroll";
|
||
Json::Value actEnum(Json::arrayValue);
|
||
actEnum.append("move"); actEnum.append("down"); actEnum.append("up"); actEnum.append("click");
|
||
actEnum.append("right_click"); actEnum.append("middle_click"); actEnum.append("drag"); actEnum.append("scroll");
|
||
act["enum"] = actEnum;
|
||
props["action"] = act;
|
||
|
||
Json::Value xProp(Json::objectValue);
|
||
xProp["type"] = "number";
|
||
xProp["description"] = u8"归一化 X 坐标(0=屏幕最左,1=最右)";
|
||
props["x"] = xProp;
|
||
|
||
Json::Value yProp(Json::objectValue);
|
||
yProp["type"] = "number";
|
||
yProp["description"] = u8"归一化 Y 坐标(0=屏幕最上,1=最下)";
|
||
props["y"] = yProp;
|
||
|
||
Json::Value x2Prop(Json::objectValue);
|
||
x2Prop["type"] = "number";
|
||
x2Prop["description"] = u8"拖拽终点 X(仅 drag)";
|
||
props["x2"] = x2Prop;
|
||
|
||
Json::Value y2Prop(Json::objectValue);
|
||
y2Prop["type"] = "number";
|
||
y2Prop["description"] = u8"拖拽终点 Y(仅 drag)";
|
||
props["y2"] = y2Prop;
|
||
|
||
Json::Value btn(Json::objectValue);
|
||
btn["type"] = "string";
|
||
btn["description"] = u8"按键:left / middle / right(默认 left)";
|
||
Json::Value btnEnum(Json::arrayValue);
|
||
btnEnum.append("left"); btnEnum.append("middle"); btnEnum.append("right");
|
||
btn["enum"] = btnEnum;
|
||
props["button"] = btn;
|
||
|
||
Json::Value clk(Json::objectValue);
|
||
clk["type"] = "integer";
|
||
clk["description"] = u8"点击次数 1/2/3(仅 click,默认 1)";
|
||
props["clicks"] = clk;
|
||
|
||
Json::Value del(Json::objectValue);
|
||
del["type"] = "integer";
|
||
del["description"] = u8"滚动量(仅 scroll;正=向下滚,负=向上滚,与 Web 控制台一致;仅垂直滚轮,忽略 button)";
|
||
props["delta"] = del;
|
||
|
||
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("action");
|
||
required.append("x");
|
||
required.append("y");
|
||
schema["required"] = required;
|
||
return schema;
|
||
}
|
||
|
||
Json::Value BuildRemoteMouseOutputSchema() {
|
||
Json::Value props(Json::objectValue);
|
||
Json::Value schema(Json::objectValue);
|
||
schema["type"] = "object";
|
||
schema["properties"] = props;
|
||
return schema;
|
||
}
|
||
|
||
// tools/call:remote_mouse(鼠标注入;归一化坐标 → 物理像素)
|
||
std::string BuildRemoteMouse(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||
CMcpServer& mcp = CMcpServer::Instance();
|
||
mcp.SweepIdleScreenCtrl(kScreenCtrlIdleTimeoutSec);
|
||
|
||
uint64_t devId = 0;
|
||
context* ctx = nullptr;
|
||
std::string errJson;
|
||
if (!ResolveScreenCtrlSessionHost(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 action = GetStringArg(args, "action");
|
||
if (action.empty())
|
||
return BuildError(id, -32602, "Missing required parameter: action");
|
||
|
||
// ---- 纯参数解析(不依赖会话分辨率;BeginScreenCtrlAction 之后再无提前返回)----
|
||
bool knownAction =
|
||
action == "move" || action == "down" || action == "up" || action == "click" ||
|
||
action == "right_click" || action == "middle_click" || action == "drag" || action == "scroll";
|
||
if (!knownAction)
|
||
return BuildError(id, -32602, "Unknown action: " + action);
|
||
|
||
int button = 0;
|
||
std::string buttonStr = GetStringArg(args, "button");
|
||
if (!ParseButton(buttonStr, button))
|
||
return BuildError(id, -32602, "Invalid button: " + buttonStr + " (expected left/middle/right)");
|
||
|
||
int clicks = 1;
|
||
if (args.isMember("clicks")) {
|
||
if (!GetIntArg(args, "clicks", clicks))
|
||
return BuildError(id, -32602, "clicks must be an integer 1..3");
|
||
if (clicks < 1 || clicks > 3)
|
||
return BuildError(id, -32602, "clicks must be 1, 2 or 3");
|
||
}
|
||
|
||
int delta = 0;
|
||
if (args.isMember("delta") && !GetIntArg(args, "delta", delta))
|
||
return BuildError(id, -32602, "delta must be an integer");
|
||
|
||
int notches = 0; // scroll 实际注入的滚动档数(= clamp(delta, -10, 10)),审计用
|
||
|
||
double nx = 0, ny = 0, nx2 = 0, ny2 = 0;
|
||
std::string coordErr;
|
||
if (!GetNormCoord(args, "x", nx, coordErr)) return BuildError(id, -32602, coordErr);
|
||
if (!GetNormCoord(args, "y", ny, coordErr)) return BuildError(id, -32602, coordErr);
|
||
if (action == "drag") {
|
||
if (!GetNormCoord(args, "x2", nx2, coordErr)) return BuildError(id, -32602, coordErr);
|
||
if (!GetNormCoord(args, "y2", ny2, coordErr)) return BuildError(id, -32602, coordErr);
|
||
}
|
||
|
||
// 校验 session_id/busy 并置 busy,同时取物理分辨率(screenW/H>0 由会话就绪保证)。
|
||
context* subCtx = nullptr;
|
||
int screenW = 0, screenH = 0;
|
||
int r = mcp.BeginScreenCtrlAction(devId, sessionId, subCtx, screenW, screenH);
|
||
if (r == 2) return BuildError(id, -32003, "Device busy: another injection is in flight for this session");
|
||
if (r == 1) return BuildError(id, -32002, "Remote control session not found, not ready, or closed: " + sessionId);
|
||
|
||
// 归一化坐标 → 物理像素(round + clamp 到 [0, screen-1])
|
||
auto toPxX = [&](double n) -> int { int p = (int)(n * screenW + 0.5); return p < 0 ? 0 : (p > screenW - 1 ? screenW - 1 : p); };
|
||
auto toPxY = [&](double n) -> int { int p = (int)(n * screenH + 0.5); return p < 0 ? 0 : (p > screenH - 1 ? screenH - 1 : p); };
|
||
int px = toPxX(nx), py = toPxY(ny);
|
||
int px2 = 0, py2 = 0;
|
||
|
||
UINT downMsg, upMsg; uint64_t downWParam;
|
||
ButtonMessages(button, downMsg, upMsg, downWParam);
|
||
|
||
// 组装注入批次(按发送顺序;每个 MSG64 由客户端 ProcessCommand 分发到 SendInput)
|
||
std::vector<MSG64> batch;
|
||
if (action == "move") {
|
||
batch.push_back(BuildMouseMsg64(px, py, WM_MOUSEMOVE, 0));
|
||
} else if (action == "down") {
|
||
batch.push_back(BuildMouseMsg64(px, py, downMsg, downWParam));
|
||
} else if (action == "up") {
|
||
batch.push_back(BuildMouseMsg64(px, py, upMsg, 0));
|
||
} else if (action == "click" || action == "right_click" || action == "middle_click") {
|
||
int b = (action == "right_click") ? 2 : (action == "middle_click" ? 1 : button);
|
||
UINT dMsg, uMsg; uint64_t dW;
|
||
ButtonMessages(b, dMsg, uMsg, dW);
|
||
for (int i = 0; i < clicks; ++i) {
|
||
batch.push_back(BuildMouseMsg64(px, py, dMsg, dW));
|
||
batch.push_back(BuildMouseMsg64(px, py, uMsg, 0));
|
||
}
|
||
} else if (action == "drag") {
|
||
px2 = toPxX(nx2); py2 = toPxY(ny2);
|
||
batch.push_back(BuildMouseMsg64(px, py, downMsg, downWParam));
|
||
batch.push_back(BuildMouseMsg64(px2, py2, WM_MOUSEMOVE, 0));
|
||
batch.push_back(BuildMouseMsg64(px2, py2, upMsg, 0));
|
||
} else if (action == "scroll") {
|
||
// delta 正=向下滚(与 Web 控制台一致);WM_MOUSEWHEEL 负值=向下滚,故取反。
|
||
// 客户端对 WM_MOUSEWHEEL 会先 SetCursorPos(x,y) 再 MOUSEEVENTF_WHEEL,单条消息即可定位+滚动。
|
||
notches = delta;
|
||
if (notches > 10) notches = 10;
|
||
if (notches < -10) notches = -10;
|
||
short wheelDelta = (short)(-notches * 120);
|
||
batch.push_back(BuildMouseMsg64(px, py, WM_MOUSEWHEEL, MAKEWPARAM(0, wheelDelta)));
|
||
}
|
||
|
||
// 组包 [COMMAND_SCREEN_CONTROL][MSG64*N],经屏幕子连接发送(主连接无效,设计 P2)。
|
||
const int len = (int)(1 + batch.size() * sizeof(MSG64));
|
||
std::vector<BYTE> packet((size_t)len);
|
||
packet[0] = COMMAND_SCREEN_CONTROL;
|
||
memcpy(packet.data() + 1, batch.data(), batch.size() * sizeof(MSG64));
|
||
bool ok = subCtx->Send2Client(packet.data(), (ULONG)len) != FALSE;
|
||
|
||
mcp.EndScreenCtrlAction(devId, sessionId);
|
||
|
||
if (!ok)
|
||
return BuildError(id, -32004, "Failed to send mouse injection");
|
||
|
||
// 审计(不可关闭;含归一化坐标与换算后的物理像素)
|
||
if (parent) {
|
||
std::string audit = "host " + std::to_string(devId) + " remote-mouse: session " + sessionId
|
||
+ " action=" + action;
|
||
char cbuf[96];
|
||
sprintf(cbuf, " norm=(%.3f,%.3f) px=(%d,%d)", nx, ny, px, py);
|
||
audit += cbuf;
|
||
if (action == "drag") {
|
||
sprintf(cbuf, " -> norm2=(%.3f,%.3f) px2=(%d,%d)", nx2, ny2, px2, py2);
|
||
audit += cbuf;
|
||
}
|
||
if (action == "scroll")
|
||
audit += " notches=" + std::to_string(notches);
|
||
if (action == "click" || action == "right_click" || action == "middle_click")
|
||
audit += " clicks=" + std::to_string(clicks);
|
||
parent->PostMessageA(WM_SHOWERRORMSG,
|
||
(WPARAM)new CString(ToAnsi(audit, 936).c_str()),
|
||
(LPARAM)new CString(_TR("MCP远程控制")));
|
||
}
|
||
|
||
Json::Value result(Json::objectValue);
|
||
result["structuredContent"] = Json::Value(Json::objectValue);
|
||
Json::Value content(Json::arrayValue);
|
||
Json::Value item(Json::objectValue);
|
||
item["type"] = "text";
|
||
item["text"] = std::string(u8"ok");
|
||
content.append(item);
|
||
result["content"] = content;
|
||
result["isError"] = false;
|
||
|
||
return BuildResult(id, result);
|
||
}
|
||
|
||
// ===== P5:remote_clipboard(写远程剪贴板,UTF-8 → GBK 直发,见设计 §6.4)=====
|
||
|
||
Json::Value BuildRemoteClipboardInputSchema() {
|
||
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"remote_open 返回的 session_id";
|
||
props["session_id"] = sid;
|
||
|
||
Json::Value text(Json::objectValue);
|
||
text["type"] = "string";
|
||
text["description"] = u8"要写入剪贴板的文本(UTF-8;非 GBK 字符如 emoji 会丢失)";
|
||
props["text"] = text;
|
||
|
||
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("text");
|
||
schema["required"] = required;
|
||
return schema;
|
||
}
|
||
|
||
Json::Value BuildRemoteClipboardOutputSchema() {
|
||
Json::Value props(Json::objectValue);
|
||
Json::Value schema(Json::objectValue);
|
||
schema["type"] = "object";
|
||
schema["properties"] = props;
|
||
return schema;
|
||
}
|
||
|
||
// tools/call:remote_clipboard(写远程剪贴板;成功返回空对象)
|
||
std::string BuildRemoteClipboard(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||
CMcpServer& mcp = CMcpServer::Instance();
|
||
mcp.SweepIdleScreenCtrl(kScreenCtrlIdleTimeoutSec);
|
||
|
||
uint64_t devId = 0;
|
||
context* ctx = nullptr;
|
||
std::string errJson;
|
||
if (!ResolveScreenCtrlSessionHost(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 text = GetStringArg(args, "text");
|
||
if (text.empty())
|
||
return BuildError(id, -32602, "Missing required parameter: text");
|
||
if (text.size() > 262144) // 256 KB 上限(CF_TEXT 单包,防内存/传输失控)
|
||
return BuildError(id, -32602, "text too long (max 262144 bytes)");
|
||
|
||
// UTF-8 → GBK:客户端 UpdateClientClipboard 以 CF_TEXT(ANSI/GBK)落剪贴板(设计 §6.4)。
|
||
// 非 GBK 字符(emoji 等)在此被替换为 '?',MVP 已知限制。
|
||
std::string ansi = ToAnsi(text, 936);
|
||
|
||
// 校验 session_id/busy 并置 busy,取屏幕子连接(注入与剪贴板共用同一子连接串行化)。
|
||
context* subCtx = nullptr;
|
||
int screenW = 0, screenH = 0;
|
||
int r = mcp.BeginScreenCtrlAction(devId, sessionId, subCtx, screenW, screenH);
|
||
if (r == 2) return BuildError(id, -32003, "Device busy: another injection is in flight for this session");
|
||
if (r == 1) return BuildError(id, -32002, "Remote control session not found, not ready, or closed: " + sessionId);
|
||
|
||
// 组包 [COMMAND_SCREEN_SET_CLIPBOARD][GBK text],经屏幕子连接发送(客户端自行补 '\0')。
|
||
const int len = (int)(1 + ansi.size());
|
||
std::vector<BYTE> packet((size_t)len);
|
||
packet[0] = COMMAND_SCREEN_SET_CLIPBOARD;
|
||
memcpy(packet.data() + 1, ansi.data(), ansi.size());
|
||
bool ok = subCtx->Send2Client(packet.data(), (ULONG)len) != FALSE;
|
||
|
||
mcp.EndScreenCtrlAction(devId, sessionId);
|
||
|
||
if (!ok)
|
||
return BuildError(id, -32004, "Failed to send clipboard text");
|
||
|
||
// 审计(不可关闭;记字节数,不落全文以免审计日志膨胀)
|
||
if (parent) {
|
||
std::string audit = "host " + std::to_string(devId) + " remote-clipboard: session " + sessionId
|
||
+ " input_bytes=" + std::to_string(text.size());
|
||
parent->PostMessageA(WM_SHOWERRORMSG,
|
||
(WPARAM)new CString(ToAnsi(audit, 936).c_str()),
|
||
(LPARAM)new CString(_TR("MCP远程控制")));
|
||
}
|
||
|
||
Json::Value result(Json::objectValue);
|
||
result["structuredContent"] = Json::Value(Json::objectValue);
|
||
Json::Value content(Json::arrayValue);
|
||
Json::Value item(Json::objectValue);
|
||
item["type"] = "text";
|
||
item["text"] = std::string(u8"ok");
|
||
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);
|
||
if (toolName == "remote_open") return BuildRemoteOpen(id, args, parent);
|
||
if (toolName == "remote_close") return BuildRemoteClose(id, args, parent);
|
||
if (toolName == "remote_keyboard") return BuildRemoteKeyboard(id, args, parent);
|
||
if (toolName == "remote_mouse") return BuildRemoteMouse(id, args, parent);
|
||
if (toolName == "remote_clipboard") return BuildRemoteClipboard(id, args, parent);
|
||
|
||
return BuildError(id, -32602,
|
||
"Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName));
|
||
}
|
||
|
||
} // namespace
|
||
|
||
// ===== P3:exec_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') || (s[p - 1] == '\r');
|
||
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();
|
||
}
|
||
|
||
// ===== P5:MCP 远程控制(状态机方法)=====
|
||
|
||
bool CMcpServer::IsScreenCtrlContext(context* subCtx) {
|
||
std::lock_guard<std::mutex> lk(m_ScreenCtrlMutex);
|
||
return m_ScreenCtrlContextToDevice.find(subCtx) != m_ScreenCtrlContextToDevice.end();
|
||
}
|
||
|
||
bool CMcpServer::BeginScreenCtrlOpen(uint64_t device_id, const std::string& sessionId) {
|
||
std::lock_guard<std::mutex> lk(m_ScreenCtrlMutex);
|
||
if (m_ScreenCtrlSessions.find(device_id) != m_ScreenCtrlSessions.end()) return false; // 单设备单会话
|
||
ScreenCtrlSession s;
|
||
s.sessionId = sessionId;
|
||
s.lastActiveAt = time(nullptr);
|
||
m_ScreenCtrlSessions[device_id] = std::move(s);
|
||
// 双向互斥(方向二):标记该设备屏幕子连接归 MCP 会话独占,人类 Web 观看期间被拒绝
|
||
// (HandleConnect 查 IsMcpTriggered)。会话擦除点(CloseScreenCtrlSession /
|
||
// SweepIdleScreenCtrl / OnScreenControlClosed / EndScreenCtrlAction)同步 ClearMcpTriggered。
|
||
WebService().SetMcpTriggered(device_id);
|
||
return true;
|
||
}
|
||
|
||
bool CMcpServer::MarkScreenCtrlReady(uint64_t device_id, const std::string& sessionId,
|
||
context* subCtx, int screenW, int screenH) {
|
||
std::lock_guard<std::mutex> lk(m_ScreenCtrlMutex);
|
||
auto it = m_ScreenCtrlSessions.find(device_id);
|
||
if (it == m_ScreenCtrlSessions.end() || it->second.sessionId != sessionId) return false;
|
||
ScreenCtrlSession& s = it->second;
|
||
s.subCtx = subCtx;
|
||
s.started = true;
|
||
s.screenW = screenW;
|
||
s.screenH = screenH;
|
||
s.lastActiveAt = time(nullptr);
|
||
m_ScreenCtrlContextToDevice[subCtx] = device_id;
|
||
return true;
|
||
}
|
||
|
||
int CMcpServer::CloseScreenCtrlSession(uint64_t device_id, const std::string& sessionId) {
|
||
std::lock_guard<std::mutex> lk(m_ScreenCtrlMutex);
|
||
auto it = m_ScreenCtrlSessions.find(device_id);
|
||
if (it == m_ScreenCtrlSessions.end()) return 1; // 不存在(幂等)
|
||
if (it->second.sessionId != sessionId) return 2; // token 不匹配
|
||
if (it->second.busy) {
|
||
// 注入在飞:置 closed 交注入线程收尾,避免与注入并发擦会话(镜像终端 busy 模式)。
|
||
it->second.closed = true;
|
||
return 0;
|
||
}
|
||
if (it->second.subCtx) m_ScreenCtrlContextToDevice.erase(it->second.subCtx);
|
||
m_ScreenCtrlSessions.erase(it);
|
||
WebService().ClearMcpTriggered(device_id);
|
||
return 0;
|
||
}
|
||
|
||
int CMcpServer::SweepIdleScreenCtrl(time_t idleTimeoutSec) {
|
||
time_t now = time(nullptr);
|
||
std::vector<uint64_t> toClose;
|
||
{
|
||
std::lock_guard<std::mutex> lk(m_ScreenCtrlMutex);
|
||
for (auto it = m_ScreenCtrlSessions.begin(); it != m_ScreenCtrlSessions.end(); ) {
|
||
ScreenCtrlSession& s = it->second;
|
||
if (!s.busy && difftime(now, s.lastActiveAt) > (double)idleTimeoutSec) {
|
||
if (s.subCtx) m_ScreenCtrlContextToDevice.erase(s.subCtx);
|
||
uint64_t devId = it->first;
|
||
toClose.push_back(devId);
|
||
it = m_ScreenCtrlSessions.erase(it);
|
||
WebService().ClearMcpTriggered(devId); // 锁内清除,避免与并发 remote_open 竞态
|
||
} else {
|
||
++it;
|
||
}
|
||
}
|
||
}
|
||
// 锁外关闭隐藏对话框(WM_CLOSE → CScreenSpyDlg 析构 → UnregisterScreenContext)。
|
||
for (uint64_t devId : toClose) {
|
||
if (m_parent) m_parent->CloseWebRemoteDesktopByClientID(devId);
|
||
WebService().ClearWebTriggered(devId);
|
||
}
|
||
return (int)toClose.size();
|
||
}
|
||
|
||
void CMcpServer::OnScreenControlClosed(context* subCtx) {
|
||
std::lock_guard<std::mutex> lk(m_ScreenCtrlMutex);
|
||
auto it = m_ScreenCtrlContextToDevice.find(subCtx);
|
||
if (it == m_ScreenCtrlContextToDevice.end()) return;
|
||
auto sit = m_ScreenCtrlSessions.find(it->second);
|
||
if (sit == m_ScreenCtrlSessions.end()) { // 路由在但会话已擦(防御)
|
||
m_ScreenCtrlContextToDevice.erase(it);
|
||
return;
|
||
}
|
||
if (sit->second.busy) {
|
||
// 注入在飞:不擦会话(注入线程仍持 subCtx),仅置 closed,由 EndScreenCtrlAction 收尾。
|
||
sit->second.closed = true;
|
||
} else {
|
||
uint64_t devId = it->second;
|
||
m_ScreenCtrlSessions.erase(sit);
|
||
m_ScreenCtrlContextToDevice.erase(it);
|
||
WebService().ClearMcpTriggered(devId);
|
||
}
|
||
}
|
||
|
||
int CMcpServer::BeginScreenCtrlAction(uint64_t device_id, const std::string& sessionId,
|
||
context*& subCtx, int& screenW, int& screenH) {
|
||
std::lock_guard<std::mutex> lk(m_ScreenCtrlMutex);
|
||
auto it = m_ScreenCtrlSessions.find(device_id);
|
||
if (it == m_ScreenCtrlSessions.end()) return 1; // 会话不存在
|
||
ScreenCtrlSession& s = it->second;
|
||
if (s.sessionId != sessionId) return 1; // token 不匹配
|
||
if (!s.started) return 1; // 未就绪
|
||
if (s.busy) return 2; // 已有注入在飞
|
||
if (s.closed) return 1; // 子连接已断
|
||
s.busy = true;
|
||
s.lastActiveAt = time(nullptr);
|
||
subCtx = s.subCtx;
|
||
screenW = s.screenW;
|
||
screenH = s.screenH;
|
||
return 0;
|
||
}
|
||
|
||
void CMcpServer::EndScreenCtrlAction(uint64_t device_id, const std::string& sessionId) {
|
||
std::lock_guard<std::mutex> lk(m_ScreenCtrlMutex);
|
||
auto it = m_ScreenCtrlSessions.find(device_id);
|
||
if (it == m_ScreenCtrlSessions.end()) return;
|
||
if (it->second.sessionId != sessionId) return;
|
||
it->second.busy = false;
|
||
it->second.lastActiveAt = time(nullptr);
|
||
if (it->second.closed) { // 注入期间子连接已断:擦会话+路由
|
||
if (it->second.subCtx) m_ScreenCtrlContextToDevice.erase(it->second.subCtx);
|
||
m_ScreenCtrlSessions.erase(it);
|
||
WebService().ClearMcpTriggered(device_id);
|
||
}
|
||
}
|
||
|
||
//////////////////////////////////////////////////////////////////////////
|
||
// 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);
|
||
}
|
||
|
||
// ===== P2c:list_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
|
||
}
|
||
|
||
// ===== P3:list_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_s:Windows 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;
|
||
}
|