Files
SimpleRemoter/server/2015Remote/McpServer.cpp
yuanyuanxiang 6bcd593b6d Feature: Add upload_file MCP tool (V2 protocol, main-connection upload)
Implement the P2 upload_file tool to push a local file or directory from the
master to an online Windows host over the existing V2 file-transfer protocol.
The server drives FileBatchTransferWorkerV2 synchronously on the main
connection through a headless callback, reuses the list_files chain for the
overwrite pre-check, and is gated by McpFileTransfer=1 plus McpReadonly=0.

The client main connection never initialized the file-transfer module, so
g_status stayed 0 and RecvFileChunkV2 silently dropped every chunk, truncating
uploads to zero bytes. Add a once-per-process lazy InitFileUpload in the
COMMAND_SEND_FILE_V2 handler that mirrors the FileManager init; the destructor
deliberately does not Uninit so g_status remains 1 across reconnects.

Update the design doc to record the client-side change and correct the
upload_file description to state that sha256 is not returned (V2 has no
receiver-to-sender ACK; integrity is checked client-side and logged only).

Co-Authored-By: deepseek-v4-pro
2026-08-31 22:43:16 +02:00

5141 lines
213 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "stdafx.h"
#include "McpServer.h"
#include "jsoncpp/json.h"
#include "HostJson.h" // BuildHostJson单台主机序列化公共函数
#include "context.h" // context 接口
#include "2015RemoteDlg.h" // CMy2015RemoteDlg 成员m_HostList/m_cs/m_ClientMap+ VERSION_STR
#include "WebService.h" // 远程控制复用屏幕子连接StartRemoteDesktop / GetScreenContext / GetScreenSize
#include "Server.h" // CONTEXT_OBJECT 定义GetScreenContext 返回 CONTEXT_OBJECT* → context* 上转型)
#include "LangManager.h" // _TR审计日志标题语言映射
#include "common/file_upload.h" // V2 文件传输RecvFileChunkV2 / HandleFileCompleteV2 / 包结构)
#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
// P6 download_file 复用 V2 落盘接口,需 FileManagerDlg 同款密码哈希 / HMAC 参数
// (声明见 CPasswordDlg.h避免在此引入重量级 MFC 头)。
std::string GetPwdHash();
std::string GetHMAC(int offset);
namespace {
// P2b 工具等待响应的超时ms。MCP 一次性请求:等待子连接回传进程/窗口列表。
static const int kMcpToolTimeoutMs = 20000;
// Json::Value → 紧凑 JSON 字符串
std::string JsonToString(const Json::Value& v) {
Json::StreamWriterBuilder b;
b["indentation"] = "";
return Json::writeString(b, v);
}
// JSON-RPC 2.0 成功响应
std::string BuildResult(const Json::Value& id, const Json::Value& result) {
Json::Value resp(Json::objectValue);
resp["jsonrpc"] = "2.0";
resp["id"] = id;
resp["result"] = result;
return JsonToString(resp);
}
// JSON-RPC 2.0 错误响应
std::string BuildError(const Json::Value& id, int code, const std::string& msg) {
Json::Value resp(Json::objectValue);
resp["jsonrpc"] = "2.0";
resp["id"] = id;
Json::Value err(Json::objectValue);
err["code"] = code;
err["message"] = msg;
resp["error"] = err;
return JsonToString(resp);
}
// initialize 握手MCP 规范protocolVersion + capabilities + serverInfo
std::string BuildInitializeResult(const Json::Value& id) {
Json::Value result(Json::objectValue);
result["protocolVersion"] = "2025-06-18";
Json::Value caps(Json::objectValue);
caps["tools"] = Json::Value(Json::objectValue);
result["capabilities"] = caps;
Json::Value serverInfo(Json::objectValue);
serverInfo["name"] = "yama";
serverInfo["version"] = VERSION_STR;
result["serverInfo"] = serverInfo;
return BuildResult(id, result);
}
// ping 健康检查:返回空 result
std::string BuildPingResult(const Json::Value& id) {
return BuildResult(id, Json::Value(Json::objectValue));
}
// ========== P2a 通用辅助 ==========
// 小写化(仅 ASCIIUTF-8 多字节原样保留):用于不区分大小写的子串匹配
std::string ToLowerAscii(const std::string& s) {
std::string r = s;
for (char& c : r) if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
return r;
}
// 不区分大小写的子串匹配
bool ContainsCI(const std::string& haystack, const std::string& needle) {
if (needle.empty()) return true;
return ToLowerAscii(haystack).find(ToLowerAscii(needle)) != std::string::npos;
}
// 取对象的字符串字段,缺失/非字符串返回 ""
std::string JsonStrField(const Json::Value& v, const char* key) {
if (v.isObject() && v.isMember(key) && v[key].isString())
return v[key].asString();
return "";
}
// 是否纯数字host id 为 uint64 十进制字符串)
bool IsDigits(const std::string& s) {
if (s.empty()) return false;
for (char c : s) if (c < '0' || c > '9') return false;
return true;
}
// 读取 tools/call 的入参MCP 规范params.arguments 为工具入参对象)
Json::Value GetCallArguments(const Json::Value& params) {
if (params.isObject() && params.isMember("arguments") && params["arguments"].isObject())
return params["arguments"];
return Json::Value(Json::objectValue);
}
// 读取可选字符串入参,缺失返回 ""
std::string GetStringArg(const Json::Value& args, const char* key) {
return JsonStrField(args, key);
}
// 读取可选整数入参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;
}
// 有界 strlenp 最多可读 avail 字节,返回 n < avail 表示遇到 '\0'。
size_t BoundedStrlen(const char* p, size_t avail) {
size_t n = 0;
while (n < avail && p[n]) ++n;
return n;
}
// 从源编码 cp 转为 UTF-8输出到 JSON。空/失败返回 ""。
std::string ToUtf8(const char* s, UINT cp) {
if (!s || !*s) return "";
int wlen = MultiByteToWideChar(cp, 0, s, -1, NULL, 0);
if (wlen <= 0) return "";
std::wstring w(wlen - 1, L'\0');
MultiByteToWideChar(cp, 0, s, -1, &w[0], wlen);
int u8len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, NULL, 0, NULL, NULL);
if (u8len <= 0) return "";
std::string out(u8len - 1, '\0');
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, &out[0], u8len, NULL, NULL);
return out;
}
// 从 UTF-8 转为目标编码 cp文件链路的目录路径在 Windows 走 ANSI与 FileManagerDlg 的
// CString 一致cp 按 clientType 判定Windows=936、LNX/MAC=CP_UTF8见 BuildListFiles
// 非 UTF-8如纯 ASCII失败时原样返回。
std::string ToAnsi(const std::string& utf8, UINT cp) {
if (utf8.empty()) return "";
int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), (int)utf8.size(), NULL, 0);
if (wlen <= 0) return utf8;
std::wstring w(wlen, L'\0');
MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), (int)utf8.size(), &w[0], wlen);
int alen = WideCharToMultiByte(cp, 0, w.c_str(), wlen, NULL, 0, NULL, NULL);
if (alen <= 0) return utf8;
std::string out(alen, '\0');
WideCharToMultiByte(cp, 0, w.c_str(), wlen, &out[0], alen, NULL, NULL);
return out;
}
// 从 "title|status|pid|r1|r2" 解析(自末尾 4 个 '|' 反推),兼容只有标题的老客户端。
// 逻辑复刻 SystemDlg.cpp::ParseWindowAttrs。
void ParseWindowAttrsStr(const char* s, std::string& title, std::string& status, uint64_t& pid) {
title.clear();
status = "normal";
pid = 0;
if (!s || !*s) return;
std::string t(s);
int len = (int)t.size();
int pipePos[4] = { -1, -1, -1, -1 };
int cnt = 0;
for (int i = len - 1; i >= 0 && cnt < 4; --i) {
if (t[i] == '|') pipePos[cnt++] = i;
}
if (cnt < 4) { // 老格式:只有标题
title = t;
return;
}
pid = strtoull(t.c_str() + pipePos[2] + 1, nullptr, 10);
status = t.substr(pipePos[3] + 1, pipePos[2] - pipePos[3] - 1);
title = t.substr(0, pipePos[3]);
}
// 解析 TOKEN_PSLIST 缓冲data[0]=token其后为 [pid:4][name:arch\0][path\0] 记录)。
// 进程名/路径为客户端 ANSI编码由 cp 指定),转 UTF-8 输出。
Json::Value ParseProcessList(const std::vector<BYTE>& data, UINT cp) {
Json::Value arr(Json::arrayValue);
if (data.size() < 2) return arr;
const char* p = (const char*)data.data();
size_t len = data.size();
size_t off = 1; // 跳过 TOKEN 字节
while (off + sizeof(DWORD) <= len) {
DWORD pid = *(const DWORD*)(p + off);
off += sizeof(DWORD);
const char* exeFile = p + off;
size_t exeLen = BoundedStrlen(exeFile, len - off);
if (exeLen >= len - off) break; // 未以 '\0' 结尾,异常数据
if (exeLen == 0) break; // 空进程名 = 尾部零填充LocalSize 对齐),停止解析
off += exeLen + 1;
const char* fullPath = p + off;
size_t pathLen = BoundedStrlen(fullPath, len - off);
if (pathLen >= len - off) break;
off += pathLen + 1;
std::string name(exeFile, exeLen);
std::string arch;
size_t colon = name.find(':');
if (colon != std::string::npos) {
arch = name.substr(colon + 1);
name = name.substr(0, colon);
}
Json::Value item(Json::objectValue);
item["pid"] = (Json::UInt64)pid;
item["name"] = ToUtf8(name.c_str(), cp);
item["arch"] = arch.empty() ? "N/A" : ToUtf8(arch.c_str(), cp);
item["path"] = ToUtf8(fullPath, cp);
arr.append(item);
}
return arr;
}
// 解析 TOKEN_WSLIST 缓冲data[0]=token其后为 [hwnd:4][title|status|pid|r1|r2\0] 记录)。
// 窗口标题为客户端 UTF-8老客户端为 CP_ACP用 cp 解码后转 UTF-8 输出。
Json::Value ParseWindowList(const std::vector<BYTE>& data, UINT cp) {
Json::Value arr(Json::arrayValue);
if (data.size() < 2) return arr;
const char* p = (const char*)data.data();
size_t len = data.size();
size_t off = 1;
while (off + sizeof(DWORD) <= len) {
DWORD hwnd = *(const DWORD*)(p + off);
off += sizeof(DWORD);
const char* titleWithAttrs = p + off;
size_t tlen = BoundedStrlen(titleWithAttrs, len - off);
if (tlen >= len - off) break;
if (tlen == 0) break; // 空记录 = 尾部零填充LocalSize 对齐),停止解析
off += tlen + 1;
std::string title, status;
uint64_t pid = 0;
ParseWindowAttrsStr(titleWithAttrs, title, status, pid);
Json::Value item(Json::objectValue);
item["hwnd"] = (Json::UInt64)hwnd;
item["title"] = ToUtf8(title.c_str(), cp);
item["status"] = status;
item["pid"] = (Json::UInt64)pid;
arr.append(item);
}
return arr;
}
// 解析 TOKEN_DRIVE_LIST 缓冲data[0]=token其后为
// [letter:1][GetDriveType:1][totalMB:4][freeMB:4][typeName\0][fileSystem\0] 记录,
// 以 letter=='\0' 终止。typeName/fileSystem 为客户端 ANSI编码由 cp 指定)。
Json::Value ParseDriveList(const std::vector<BYTE>& data, UINT cp) {
Json::Value arr(Json::arrayValue);
if (data.size() < 2) return arr;
const char* p = (const char*)data.data();
size_t len = data.size();
size_t off = 1; // 跳过 TOKEN 字节
while (off + 10 <= len && p[off] != '\0') {
char letter = p[off];
unsigned int type = (unsigned int)(unsigned char)p[off + 1];
DWORD totalMB = *(const DWORD*)(p + off + 2);
DWORD freeMB = *(const DWORD*)(p + off + 6);
off += 10;
const char* typeName = p + off;
size_t tlen = BoundedStrlen(typeName, len - off);
if (tlen >= len - off) break;
off += tlen + 1;
const char* fileSystem = p + off;
size_t flen = BoundedStrlen(fileSystem, len - off);
if (flen >= len - off) break;
off += flen + 1;
std::string drive;
drive += letter;
drive += ":\\";
Json::Value item(Json::objectValue);
item["drive"] = drive;
item["type"] = (Json::UInt64)type;
item["typeName"] = ToUtf8(typeName, cp);
item["fileSystem"] = ToUtf8(fileSystem, cp);
item["totalMB"] = (Json::UInt64)totalMB;
item["freeMB"] = (Json::UInt64)freeMB;
arr.append(item);
}
return arr;
}
// 解析 TOKEN_FILE_LIST 缓冲data[0]=token其后为
// [attr:1][filename\0][sizeHigh:4][sizeLow:4][ftLastWriteTime:8] 记录)。
// 文件名为客户端 ANSI编码由 cp 指定attr 非 0 表示目录FILE_ATTRIBUTE_DIRECTORY
Json::Value ParseFileList(const std::vector<BYTE>& data, int maxEntries, UINT cp) {
Json::Value arr(Json::arrayValue);
if (data.size() < 2) return arr;
const char* p = (const char*)data.data();
size_t len = data.size();
size_t off = 1;
int count = 0;
while (off + 1 <= len && count < maxEntries) {
bool isDir = (p[off] & FILE_ATTRIBUTE_DIRECTORY) != 0;
off += 1;
const char* name = p + off;
size_t nlen = BoundedStrlen(name, len - off);
if (nlen >= len - off) break;
if (nlen == 0) break; // 空记录 = 尾部零填充LocalAlloc 对齐),停止解析
off += nlen + 1;
if (off + 16 > len) break;
DWORD sizeHigh = *(const DWORD*)(p + off);
DWORD sizeLow = *(const DWORD*)(p + off + 4);
ULONGLONG size = ((ULONGLONG)sizeHigh << 32) | sizeLow;
// 修改时间FILETIME100ns since 1601-01-01→ Unix 秒
// FILETIME 在线上是 dwLowDateTime(低 4 字节) 在前、dwHighDateTime(高 4 字节) 在后
// (客户端 memcpy(&ftLastWriteTime, sizeof(FILETIME)),见 FileManager.cpp::SendFilesList
DWORD ftLow = *(const DWORD*)(p + off + 8);
DWORD ftHigh = *(const DWORD*)(p + off + 12);
off += 16;
ULONGLONG ft = ((ULONGLONG)ftHigh << 32) | ftLow;
Json::Int64 mtime = (Json::Int64)(ft / 10000000ULL) - 11644473600LL;
Json::Value item(Json::objectValue);
item["name"] = ToUtf8(name, cp);
item["isDir"] = isDir;
item["size"] = (Json::UInt64)size;
item["mtime"] = mtime;
arr.append(item);
++count;
}
return arr;
}
// 解析 TOKEN_SERVERLIST 缓冲data[0]=token其后为
// [displayName\0][serviceName\0][binaryPath\0][runWay\0][autoRun\0] 记录)。
// 字段全部来自 Windows A 接口EnumServicesStatus / QueryServiceConfig为客户端
// ANSI编码由 cp 指定)。以 displayName 与 serviceName 同时为空作为尾部零填充
// LocalAlloc/LocalReAlloc LMEM_ZEROINIT 对齐)的终止条件。
Json::Value ParseServiceList(const std::vector<BYTE>& data, UINT cp) {
Json::Value arr(Json::arrayValue);
if (data.size() < 2) return arr;
const char* p = (const char*)data.data();
size_t len = data.size();
size_t off = 1; // 跳过 TOKEN 字节
while (off < len) {
const char* f[5];
bool ok = true;
for (int i = 0; i < 5; ++i) {
size_t n = BoundedStrlen(p + off, len - off);
if (n >= len - off) { ok = false; break; } // 未以 '\0' 结尾,异常数据
f[i] = p + off;
off += n + 1;
}
if (!ok) break;
// displayName 与 serviceName 都为空 → 尾部零填充,停止解析
if (f[0][0] == '\0' && f[1][0] == '\0') break;
Json::Value item(Json::objectValue);
item["display_name"] = ToUtf8(f[0], cp);
item["service_name"] = ToUtf8(f[1], cp);
item["binary_path"] = ToUtf8(f[2], cp);
item["status"] = ToUtf8(f[3], cp); // Stopped/Running/Paused/... 英文
item["start_type"] = ToUtf8(f[4], cp); // Boot-Start/Auto-Start/Demand-Start/... 英文
arr.append(item);
}
return arr;
}
// ========== list_registry注册表解析 ==========
// 注册表 REGMSG 头client RegisterOperation.cpp / server RegisterDlg.cpp 各自本地定义,此处同构)。
struct RegMsgHeader {
int count; // 名字个数
DWORD size; // 名字大小(定宽)
DWORD valsize; // 值大小(定宽)
};
// client KEYVALUE 枚举RegisterOperation.cppMREG_SZ=0 … MREG_NONE=6。
enum RegValueType {
REG_T_SZ = 0,
REG_T_DWORD = 1,
REG_T_BINARY = 2,
REG_T_EXPAND_SZ = 3,
REG_T_MULTI_SZ = 4,
REG_T_QWORD = 5,
REG_T_NONE = 6,
};
// client MYKEY 枚举:根键 token。
enum RegRootToken {
REG_ROOT_CLASSES_ROOT = 0,
REG_ROOT_CURRENT_USER = 1,
REG_ROOT_LOCAL_MACHINE = 2,
REG_ROOT_USERS = 3,
REG_ROOT_CURRENT_CONFIG = 4,
};
// ASCII 不区分大小写前缀匹配(注册表根键名大小写不敏感)。
bool StrPrefixCI(const std::string& s, const char* prefix, size_t len) {
if (s.size() < len) return false;
for (size_t i = 0; i < len; ++i) {
char a = s[i], b = prefix[i];
if (a >= 'A' && a <= 'Z') a += 32;
if (b >= 'A' && b <= 'Z') b += 32;
if (a != b) return false;
}
return true;
}
// 值类型名(可读字符串)
const char* RegTypeName(BYTE type) {
switch (type) {
case REG_T_SZ: return "REG_SZ";
case REG_T_DWORD: return "REG_DWORD";
case REG_T_BINARY: return "REG_BINARY";
case REG_T_EXPAND_SZ: return "REG_EXPAND_SZ";
case REG_T_MULTI_SZ: return "REG_MULTI_SZ";
case REG_T_QWORD: return "REG_QWORD";
case REG_T_NONE: return "REG_NONE";
default: return "REG_UNKNOWN";
}
}
// 将注册表值数据格式化为字符串。valSize 为定宽client 按 MaxDataLen+1 填充),
// 字符串类取到 '\0' 为止、数值类取定长前缀REG_BINARY 长度无法从协议还原,按 valSize 输出 hex。
std::string FormatRegData(BYTE type, const BYTE* val, size_t valSize, UINT cp) {
switch (type) {
case REG_T_SZ:
case REG_T_EXPAND_SZ:
return ToUtf8((const char*)val, cp);
case REG_T_MULTI_SZ: { // 多个 '\0' 结尾串、双 '\0' 结束
std::string out;
size_t i = 0;
while (i < valSize && val[i] != '\0') {
const char* s = (const char*)(val + i);
size_t n = BoundedStrlen(s, valSize - i);
if (!out.empty()) out += "\n";
out += ToUtf8(s, cp);
i += n + 1;
}
return out;
}
case REG_T_DWORD: { // 4 字节小端
if (valSize < 4) return "";
DWORD v = 0;
memcpy(&v, val, 4);
char buf[64];
sprintf(buf, "0x%08lX (%lu)", (unsigned long)v, (unsigned long)v);
return buf;
}
case REG_T_QWORD: { // 8 字节小端
if (valSize < 8) return "";
uint64_t v = 0;
memcpy(&v, val, 8);
char buf[96];
sprintf(buf, "0x%016I64X (%I64u)", (unsigned __int64)v, (unsigned __int64)v);
return buf;
}
case REG_T_BINARY: { // hex尾部可能含 0 填充
std::string out;
char buf[4];
for (size_t i = 0; i < valSize; ++i) {
sprintf(buf, "%02X", val[i]);
out += buf;
}
return out;
}
case REG_T_NONE:
default:
return "";
}
}
// 解析 TOKEN_REG_PATH 缓冲:[token:1][RegMsgHeader:12][count * size 定宽子键名]。cp 为客户端 ANSI。
Json::Value ParseRegPath(const std::vector<BYTE>& data, UINT cp) {
Json::Value arr(Json::arrayValue);
if (data.size() < 1 + sizeof(RegMsgHeader)) return arr;
const char* p = (const char*)data.data();
size_t off = 1; // 跳过 TOKEN 字节
RegMsgHeader hdr;
memcpy(&hdr, p + off, sizeof(hdr));
off += sizeof(hdr);
int count = hdr.count;
size_t nameSize = hdr.size;
for (int i = 0; i < count; ++i) {
if (off + nameSize > data.size()) break;
const char* name = p + off;
off += nameSize;
if (name[0] == '\0') break; // 空名 = 尾部零填充,停止
arr.append(ToUtf8(name, cp));
}
return arr;
}
// 解析 TOKEN_REG_KEY 缓冲:[token:1][RegMsgHeader:12][count * {type:1,name:size,data:valsize}]。
Json::Value ParseRegKey(const std::vector<BYTE>& data, UINT cp) {
Json::Value arr(Json::arrayValue);
if (data.size() < 1 + sizeof(RegMsgHeader)) return arr;
const char* p = (const char*)data.data();
size_t off = 1; // 跳过 TOKEN 字节
RegMsgHeader hdr;
memcpy(&hdr, p + off, sizeof(hdr));
off += sizeof(hdr);
int count = hdr.count;
size_t nameSize = hdr.size;
size_t valSize = hdr.valsize;
for (int i = 0; i < count; ++i) {
if (off + 1 + nameSize + valSize > data.size()) break;
BYTE type = (BYTE)p[off];
off += 1;
const char* name = p + off; // 定宽 nameSize 内以 '\0' 结尾
off += nameSize;
const BYTE* val = (const BYTE*)(p + off);
off += valSize;
Json::Value item(Json::objectValue);
item["name"] = ToUtf8(name, cp);
item["type"] = RegTypeName(type);
item["data"] = FormatRegData(type, val, valSize, cp);
arr.append(item);
}
return arr;
}
// 解析 "HKEY_LOCAL_MACHINE\Software\..." → rootToken + 相对子键路径(去掉根键名与首个 '\')。
// 返回 false 表示 path 为空(调用方列根键)或未匹配任何根键(调用方报错)。
bool ParseRegistryPath(const std::string& path, BYTE& rootToken, std::string& relPath) {
struct RootMap { const char* name; BYTE token; };
static const RootMap roots[] = {
{ "HKEY_CLASSES_ROOT", REG_ROOT_CLASSES_ROOT },
{ "HKEY_CURRENT_USER", REG_ROOT_CURRENT_USER },
{ "HKEY_LOCAL_MACHINE", REG_ROOT_LOCAL_MACHINE },
{ "HKEY_USERS", REG_ROOT_USERS },
{ "HKEY_CURRENT_CONFIG", REG_ROOT_CURRENT_CONFIG },
};
if (path.empty()) return false;
for (const RootMap& r : roots) {
size_t n = strlen(r.name);
if (StrPrefixCI(path, r.name, n)) {
rootToken = r.token;
relPath = path.substr(n);
if (!relPath.empty() && relPath[0] == '\\') relPath.erase(0, 1);
return true;
}
}
return false;
}
// 列根键path 为空):返回 5 个固定根键,无需查询客户端。
std::string BuildRegistryRoots(const Json::Value& id) {
Json::Value keys(Json::arrayValue);
const char* roots[] = {
"HKEY_CLASSES_ROOT", "HKEY_CURRENT_USER", "HKEY_LOCAL_MACHINE",
"HKEY_USERS", "HKEY_CURRENT_CONFIG"
};
for (const char* r : roots) keys.append(r);
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["keys"] = keys;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"") + std::to_string((int)keys.size()) + std::string(u8" 个根键。");
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// 收集所有在线主机 JSON 数组m_cs 锁内遍历,复用 BuildHostJson 序列化,方案 C
void CollectOnlineHosts(CMy2015RemoteDlg* parent, Json::Value& hosts) {
if (!parent) return;
EnterCriticalSection(&parent->m_cs);
for (context* ctx : parent->m_HostList) {
if (!ctx || !ctx->IsLogin()) continue;
hosts.append(BuildHostJson(ctx, parent->m_ClientMap));
}
LeaveCriticalSection(&parent->m_cs);
}
// ========== 工具 schema ==========
// 单台主机字段 schemahosts 数组元素 / 单机详情共用的形状)
Json::Value BuildHostItemSchema() {
Json::Value itemProps(Json::objectValue);
const char* strFields[] = {
"id", "name", "remark", "ip", "os", "location", "rtt",
"version", "activeWindow", "group", "screen", "clientType"
};
for (const char* f : strFields) {
Json::Value p(Json::objectValue);
p["type"] = "string";
itemProps[f] = p;
}
Json::Value onlineProp(Json::objectValue);
onlineProp["type"] = "boolean";
itemProps["online"] = onlineProp;
return itemProps;
}
// list_online_hosts / search_hosts 的 outputSchemahosts 数组)
Json::Value BuildHostOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value hostsProp(Json::objectValue);
hostsProp["type"] = "array";
Json::Value items(Json::objectValue);
items["type"] = "object";
items["properties"] = BuildHostItemSchema();
hostsProp["items"] = items;
props["hosts"] = hostsProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("hosts");
schema["required"] = required;
return schema;
}
// get_host_detail 的 outputSchema单台主机
Json::Value BuildHostDetailOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value hostProp(Json::objectValue);
hostProp["type"] = "object";
hostProp["properties"] = BuildHostItemSchema();
props["host"] = hostProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("host");
schema["required"] = required;
return schema;
}
// search_hosts 的 inputSchema全部可选
Json::Value BuildSearchHostsInputSchema() {
Json::Value props(Json::objectValue);
const char* strParams[] = { "name", "ip", "group", "os" };
for (const char* p : strParams) {
Json::Value s(Json::objectValue);
s["type"] = "string";
props[p] = s;
}
Json::Value onlineProp(Json::objectValue);
onlineProp["type"] = "boolean";
props["online"] = onlineProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
return schema;
}
// get_host_detail 的 inputSchemaid 必填)
Json::Value BuildGetHostDetailInputSchema() {
Json::Value props(Json::objectValue);
Json::Value idProp(Json::objectValue);
idProp["type"] = "string";
idProp["description"] = u8"主机 id取 list_online_hosts / search_hosts 返回的 id 字段";
props["id"] = idProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("id");
schema["required"] = required;
return schema;
}
// list_processes 的 outputSchemaprocesses 数组)
Json::Value BuildProcessListOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value procsProp(Json::objectValue);
procsProp["type"] = "array";
Json::Value items(Json::objectValue);
items["type"] = "object";
Json::Value itemProps(Json::objectValue);
Json::Value pidProp(Json::objectValue);
pidProp["type"] = "integer";
itemProps["pid"] = pidProp;
const char* strFields[] = { "name", "arch", "path" };
for (const char* f : strFields) {
Json::Value s(Json::objectValue);
s["type"] = "string";
itemProps[f] = s;
}
items["properties"] = itemProps;
procsProp["items"] = items;
props["processes"] = procsProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("processes");
schema["required"] = required;
return schema;
}
// list_windows 的 outputSchemawindows 数组)
Json::Value BuildWindowListOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value winsProp(Json::objectValue);
winsProp["type"] = "array";
Json::Value items(Json::objectValue);
items["type"] = "object";
Json::Value itemProps(Json::objectValue);
Json::Value hwndProp(Json::objectValue);
hwndProp["type"] = "integer";
itemProps["hwnd"] = hwndProp;
Json::Value pidProp(Json::objectValue);
pidProp["type"] = "integer";
itemProps["pid"] = pidProp;
const char* strFields[] = { "title", "status" };
for (const char* f : strFields) {
Json::Value s(Json::objectValue);
s["type"] = "string";
itemProps[f] = s;
}
items["properties"] = itemProps;
winsProp["items"] = items;
props["windows"] = winsProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("windows");
schema["required"] = required;
return schema;
}
// get_activity_history 的 outputSchemarecords 数组 + 原始文本)
Json::Value BuildActivityHistoryOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value recordsProp(Json::objectValue);
recordsProp["type"] = "array";
Json::Value items(Json::objectValue);
items["type"] = "string";
recordsProp["items"] = items;
props["records"] = recordsProp;
Json::Value rawProp(Json::objectValue);
rawProp["type"] = "string";
props["activityHistory"] = rawProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
return schema;
}
// list_services 的 outputSchemaservices 数组)
Json::Value BuildServiceListOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value svcsProp(Json::objectValue);
svcsProp["type"] = "array";
Json::Value items(Json::objectValue);
items["type"] = "object";
Json::Value itemProps(Json::objectValue);
const char* strFields[] = { "display_name", "service_name", "binary_path", "status", "start_type" };
for (const char* f : strFields) {
Json::Value s(Json::objectValue);
s["type"] = "string";
itemProps[f] = s;
}
items["properties"] = itemProps;
svcsProp["items"] = items;
props["services"] = svcsProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("services");
schema["required"] = required;
return schema;
}
// list_registry 的 inputSchemaid 必填path 可选path 缺省/空 = 列根键)
Json::Value BuildListRegistryInputSchema() {
Json::Value props(Json::objectValue);
Json::Value idProp(Json::objectValue);
idProp["type"] = "string";
idProp["description"] = u8"主机 id取 list_online_hosts / search_hosts 返回的 id 字段";
props["id"] = idProp;
Json::Value pathProp(Json::objectValue);
pathProp["type"] = "string";
pathProp["description"] = u8"注册表键路径(如 HKEY_LOCAL_MACHINE\\Software省略或传空则返回 5 个根键";
props["path"] = pathProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("id");
schema["required"] = required;
return schema;
}
// list_registry 的 outputSchemakeys 字符串数组 / values 对象数组)
Json::Value BuildListRegistryOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value pathProp(Json::objectValue);
pathProp["type"] = "string";
props["path"] = pathProp;
Json::Value keysProp(Json::objectValue);
keysProp["type"] = "array";
Json::Value keyItems(Json::objectValue);
keyItems["type"] = "string";
keysProp["items"] = keyItems;
props["keys"] = keysProp;
Json::Value valsProp(Json::objectValue);
valsProp["type"] = "array";
Json::Value valItems(Json::objectValue);
valItems["type"] = "object";
Json::Value valProps(Json::objectValue);
const char* strFields[] = { "name", "type", "data" };
for (const char* f : strFields) {
Json::Value s(Json::objectValue);
s["type"] = "string";
valProps[f] = s;
}
valItems["properties"] = valProps;
valsProp["items"] = valItems;
props["values"] = valsProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("keys");
required.append("values");
schema["required"] = required;
return schema;
}
// exec_command 的 inputSchemaid 必填、command 必填、timeout_ms 可选)
Json::Value BuildExecCommandInputSchema() {
Json::Value props(Json::objectValue);
Json::Value idProp(Json::objectValue);
idProp["type"] = "string";
idProp["description"] = u8"主机 id取 list_online_hosts / search_hosts 返回的 id 字段";
props["id"] = idProp;
Json::Value cmdProp(Json::objectValue);
cmdProp["type"] = "string";
cmdProp["description"] = u8"要执行的命令(受只读模式与命令白名单约束,仅允许只读命令前缀)";
props["command"] = cmdProp;
Json::Value timeoutProp(Json::objectValue);
timeoutProp["type"] = "integer";
timeoutProp["description"] = u8"等待输出完成的超时毫秒数(可选,默认 20000上限 600000";
props["timeout_ms"] = timeoutProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("id");
required.append("command");
schema["required"] = required;
return schema;
}
// exec_command 的 outputSchemastdout 文本 / exit_code
Json::Value BuildExecCommandOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value stdoutProp(Json::objectValue);
stdoutProp["type"] = "string";
stdoutProp["description"] = u8"命令输出(已剥哨兵与 ANSI 转义)";
props["stdout"] = stdoutProp;
Json::Value exitProp(Json::objectValue);
exitProp["type"] = "integer";
exitProp["description"] = u8"退出码0=成功、1=非零退出、-1=未知(进程异常退出)";
props["exit_code"] = exitProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("stdout");
required.append("exit_code");
schema["required"] = required;
return schema;
}
// get_client_log 的 outputSchema原始日志文本
Json::Value BuildClientLogOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value logProp(Json::objectValue);
logProp["type"] = "string";
props["log"] = logProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("log");
schema["required"] = required;
return schema;
}
// get_audit_log 的 outputSchema服务端消息/审计日志条目数组)
Json::Value BuildAuditLogOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value entriesProp(Json::objectValue);
entriesProp["type"] = "array";
Json::Value items(Json::objectValue);
items["type"] = "object";
Json::Value itemProps(Json::objectValue);
const char* strFields[] = { "type", "time", "msg" };
for (const char* f : strFields) {
Json::Value s(Json::objectValue);
s["type"] = "string";
itemProps[f] = s;
}
items["properties"] = itemProps;
entriesProp["items"] = items;
props["entries"] = entriesProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("entries");
schema["required"] = required;
return schema;
}
// get_screenshot 的 outputSchemaimage 元数据base64 数据在 content 的 image 块中)
Json::Value BuildScreenshotOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value imageProp(Json::objectValue);
imageProp["type"] = "object";
Json::Value imageProps(Json::objectValue);
Json::Value mimeProp(Json::objectValue);
mimeProp["type"] = "string";
imageProps["mimeType"] = mimeProp;
const char* intFields[] = { "width", "height", "bytes" };
for (const char* f : intFields) {
Json::Value s(Json::objectValue);
s["type"] = "integer";
imageProps[f] = s;
}
imageProp["properties"] = imageProps;
props["image"] = imageProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("image");
schema["required"] = required;
return schema;
}
// get_screenshot 的 inputSchemaid 必填max_width 可选)
Json::Value BuildGetScreenshotInputSchema() {
Json::Value props(Json::objectValue);
Json::Value idProp(Json::objectValue);
idProp["type"] = "string";
idProp["description"] = u8"主机 id取 list_online_hosts / search_hosts 返回的 id 字段";
props["id"] = idProp;
Json::Value mwProp(Json::objectValue);
mwProp["type"] = "integer";
mwProp["description"] = u8"期望图片最大宽度(像素,钳制到 64~1920。省略或传 0 时沿用 RTT 自适应缩略图档位(最大 1024传 1920 可拿到接近原分辨率1080p 源屏即原分辨率4K 源屏最多 1920供 AI 视觉/OCR 场景提升清晰度。";
props["max_width"] = mwProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("id");
schema["required"] = required;
return schema;
}
// list_files 的 inputSchemaid 必填path 可选path 缺省/空 = 列盘)
Json::Value BuildListFilesInputSchema() {
Json::Value props(Json::objectValue);
Json::Value idProp(Json::objectValue);
idProp["type"] = "string";
idProp["description"] = u8"主机 id取 list_online_hosts / search_hosts 返回的 id 字段";
props["id"] = idProp;
Json::Value pathProp(Json::objectValue);
pathProp["type"] = "string";
pathProp["description"] = u8"要列举的目录路径(如 C:\\Windows省略或传空则返回驱动器列表";
props["path"] = pathProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("id");
schema["required"] = required;
return schema;
}
// list_files 的 outputSchemadrives 数组 / files 数组,二者其一非空)
Json::Value BuildListFilesOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value drivesProp(Json::objectValue);
drivesProp["type"] = "array";
Json::Value driveItems(Json::objectValue);
driveItems["type"] = "object";
Json::Value driveProps(Json::objectValue);
const char* driveStr[] = { "drive", "typeName", "fileSystem" };
for (const char* f : driveStr) {
Json::Value s(Json::objectValue);
s["type"] = "string";
driveProps[f] = s;
}
const char* driveInt[] = { "type", "totalMB", "freeMB" };
for (const char* f : driveInt) {
Json::Value s(Json::objectValue);
s["type"] = "integer";
driveProps[f] = s;
}
driveItems["properties"] = driveProps;
drivesProp["items"] = driveItems;
props["drives"] = drivesProp;
Json::Value filesProp(Json::objectValue);
filesProp["type"] = "array";
Json::Value fileItems(Json::objectValue);
fileItems["type"] = "object";
Json::Value fileProps(Json::objectValue);
Json::Value nameProp(Json::objectValue);
nameProp["type"] = "string";
fileProps["name"] = nameProp;
Json::Value isDirProp(Json::objectValue);
isDirProp["type"] = "boolean";
fileProps["isDir"] = isDirProp;
const char* fileInt[] = { "size", "mtime" };
for (const char* f : fileInt) {
Json::Value s(Json::objectValue);
s["type"] = "integer";
fileProps[f] = s;
}
fileItems["properties"] = fileProps;
filesProp["items"] = fileItems;
props["files"] = filesProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
return schema;
}
// ===== P4 前置声明定义见下方「tools/call 分派」前)=====
Json::Value BuildTerminalOpenInputSchema();
Json::Value BuildTerminalOpenOutputSchema();
Json::Value BuildTerminalExecInputSchema();
Json::Value BuildTerminalExecOutputSchema();
Json::Value BuildTerminalCloseInputSchema();
Json::Value BuildTerminalCloseOutputSchema();
std::string BuildTerminalOpen(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
std::string BuildTerminalExec(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
std::string BuildTerminalClose(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
// ===== 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);
// ===== P6 前置声明定义见下方「tools/call 分派」前)=====
Json::Value BuildDownloadFileInputSchema();
Json::Value BuildDownloadFileOutputSchema();
std::string BuildDownloadFile(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
Json::Value BuildUploadFileInputSchema();
Json::Value BuildUploadFileOutputSchema();
std::string BuildUploadFile(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
std::string BuildRemoteClipboard(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
// tools/list
std::string BuildToolsListResult(const Json::Value& id) {
Json::Value result(Json::objectValue);
Json::Value tools(Json::arrayValue);
// 1) list_online_hosts
{
Json::Value tool(Json::objectValue);
tool["name"] = "list_online_hosts";
// 说明文字为 UTF-8项目 /execution-charset:.936 会把普通窄字面量编译成 GBK
// 故用 u8 前缀确保输出到 JSON 的字节是 UTF-8。
tool["description"] = u8"获取当前所有在线主机的列表包含计算机名、IP、操作系统、版本、备注、分组、活动窗口、延迟等实时信息。";
Json::Value inputSchema(Json::objectValue);
inputSchema["type"] = "object";
inputSchema["properties"] = Json::Value(Json::objectValue);
inputSchema["required"] = Json::Value(Json::arrayValue);
tool["inputSchema"] = inputSchema;
tool["outputSchema"] = BuildHostOutputSchema();
tools.append(tool);
}
// 2) search_hostsP2a纯内存过滤无子链接
{
Json::Value tool(Json::objectValue);
tool["name"] = "search_hosts";
tool["description"] = u8"按计算机名/备注、IP、分组、操作系统过滤在线主机。所有条件均可选、按 AND 组合子串匹配ASCII 不区分大小写)。只返回在线主机。";
tool["inputSchema"] = BuildSearchHostsInputSchema();
tool["outputSchema"] = BuildHostOutputSchema();
tools.append(tool);
}
// 3) get_host_detailP2a单机详情纯内存
{
Json::Value tool(Json::objectValue);
tool["name"] = "get_host_detail";
tool["description"] = u8"获取单台在线主机的详细信息id、计算机名、IP、操作系统、备注、分组、活动窗口、屏幕分辨率、客户端类型等";
tool["inputSchema"] = BuildGetHostDetailInputSchema();
tool["outputSchema"] = BuildHostDetailOutputSchema();
tools.append(tool);
}
// 4) list_processesP2b主连接 RPC进程列表
{
Json::Value tool(Json::objectValue);
tool["name"] = "list_processes";
tool["description"] = u8"获取指定在线主机的进程列表PID、映像名称、架构、程序完整路径。通过主连接下发命令、子连接回传一次性返回。";
tool["inputSchema"] = BuildGetHostDetailInputSchema(); // 复用 { id } 必填 schema
tool["outputSchema"] = BuildProcessListOutputSchema();
tools.append(tool);
}
// 5) list_windowsP2b主连接 RPC窗口列表
{
Json::Value tool(Json::objectValue);
tool["name"] = "list_windows";
tool["description"] = u8"获取指定在线主机的顶层窗口列表(句柄、窗口标题、窗口状态、所属进程 PID。一次性返回。";
tool["inputSchema"] = BuildGetHostDetailInputSchema(); // 复用 { id } 必填 schema
tool["outputSchema"] = BuildWindowListOutputSchema();
tools.append(tool);
}
// 6) get_activity_historyP2b主连接 RPC历史活动记录
{
Json::Value tool(Json::objectValue);
tool["name"] = "get_activity_history";
tool["description"] = u8"获取指定在线主机的历史活动记录(前台窗口驻留时长,每行一条「[时间] [标题] 时长」)。主连接 RPC、一次性返回。";
tool["inputSchema"] = BuildGetHostDetailInputSchema(); // 复用 { id } 必填 schema
tool["outputSchema"] = BuildActivityHistoryOutputSchema();
tools.append(tool);
}
// 7) get_screenshotP2c屏幕预览链路主连接 RPC
{
Json::Value tool(Json::objectValue);
tool["name"] = "get_screenshot";
tool["description"] = u8"截取指定在线主机的一帧屏幕,返回 base64 编码的 JPEG 图片image 对象含 mimeType/data/width/height。仅 Windows 客户端支持(能力位 CLIENT_CAP_SCREEN_PREVIEW主连接 RPC、一次性返回。";
tool["inputSchema"] = BuildGetScreenshotInputSchema(); // { id } 必填 + max_width 可选
tool["outputSchema"] = BuildScreenshotOutputSchema();
tools.append(tool);
}
// 8) list_filesP2c文件链路列盘/列目录)
{
Json::Value tool(Json::objectValue);
tool["name"] = "list_files";
tool["description"] = u8"列举指定在线主机的目录。省略或空 path 返回驱动器列表drives给定 path 返回该目录一层内的文件/子目录files最多 500 条,含 name/isDir/size/mtime。只读。";
tool["inputSchema"] = BuildListFilesInputSchema();
tool["outputSchema"] = BuildListFilesOutputSchema();
tools.append(tool);
}
// 9) list_servicesP3服务链路仅 Windows一次性子链接
{
Json::Value tool(Json::objectValue);
tool["name"] = "list_services";
tool["description"] = u8"获取指定在线 Windows 主机的服务列表(显示名、服务名、可执行文件路径、运行状态、启动类型)。仅 Windows 客户端支持;一次性返回。";
tool["inputSchema"] = BuildGetHostDetailInputSchema(); // 复用 { id } 必填 schema
tool["outputSchema"] = BuildServiceListOutputSchema();
tools.append(tool);
}
// 10) get_client_logP3客户端运行日志一次性子链接取首条全量
{
Json::Value tool(Json::objectValue);
tool["name"] = "get_client_log";
tool["description"] = u8"获取指定在线主机 YAMA 客户端的内存运行日志(最近最多 1000 条,含时间戳/源文件/行号)。客户端持续增量上报,本工具取当前时刻的全量快照后即断开。只读。";
tool["inputSchema"] = BuildGetHostDetailInputSchema(); // 复用 { id } 必填 schema
tool["outputSchema"] = BuildClientLogOutputSchema();
tools.append(tool);
}
// 11) get_audit_log服务端本地主界面消息/审计日志,纯内存,无子链接)
{
Json::Value tool(Json::objectValue);
tool["name"] = "get_audit_log";
tool["description"] = u8"获取 YAMA 服务端主界面的消息/审计日志(主机上线/下线、操作结果、告警等)。返回列表内当前全部条目(最多 1000 条,新在前)。只读、纯内存。";
Json::Value inputSchema(Json::objectValue);
inputSchema["type"] = "object";
inputSchema["properties"] = Json::Value(Json::objectValue);
inputSchema["required"] = Json::Value(Json::arrayValue);
tool["inputSchema"] = inputSchema;
tool["outputSchema"] = BuildAuditLogOutputSchema();
tools.append(tool);
}
// 12) list_registryP3注册表查询列根键/列子键+值,仅 Windows一次性子链接
{
Json::Value tool(Json::objectValue);
tool["name"] = "list_registry";
tool["description"] = u8"查询指定在线 Windows 主机的注册表。省略或空 path 返回 5 个根键;给定 path如 HKEY_LOCAL_MACHINE\\Software返回该键一层内的子键keys与值values含 name/type/data。只读。";
tool["inputSchema"] = BuildListRegistryInputSchema();
tool["outputSchema"] = BuildListRegistryOutputSchema();
tools.append(tool);
}
// 13) exec_commandP3一次性远程命令仅 Windows安全门只读默认 + 白名单 + 审计)
// 只读模式下 tools/list 直接隐藏该工具即便被绕过BuildExecCommand 也会再次校验只读。
if (!CMcpServer::Instance().IsReadonly()) {
Json::Value tool(Json::objectValue);
tool["name"] = "exec_command";
tool["description"] = u8"在指定在线 Windows 主机上执行一条命令并返回 stdout 与退出码(受只读模式与命令白名单约束,默认仅允许只读命令前缀,见 McpReadonly / McpCmdWhitelist";
tool["inputSchema"] = BuildExecCommandInputSchema();
tool["outputSchema"] = BuildExecCommandOutputSchema();
tools.append(tool);
}
// 14) terminal_open / terminal_exec / terminal_closeP4持久远程终端仅 Windows
// 安全门McpTerminal=1 且 McpReadonly=0无白名单全命令审计 + idle 回收)
if (CMcpServer::Instance().IsTerminalEnabled() && !CMcpServer::Instance().IsReadonly()) {
{
Json::Value tool(Json::objectValue);
tool["name"] = "terminal_open";
tool["description"] = u8"在指定在线 Windows 主机上打开一个持久 shell 会话并返回 session_id 与终端模式。后续 terminal_exec 复用该会话cwd/环境变量跨命令保持),用毕须 terminal_close。";
tool["inputSchema"] = BuildTerminalOpenInputSchema();
tool["outputSchema"] = BuildTerminalOpenOutputSchema();
tools.append(tool);
}
{
Json::Value tool(Json::objectValue);
tool["name"] = "terminal_exec";
tool["description"] = u8"在已打开的持久会话中执行一条命令并返回 stdout 与退出码。命令不受白名单约束,但不能含 & 或 |(会破坏输出哨兵捕获),请拆成多条调用;重定向 > < 与转义 ^ 允许。";
tool["inputSchema"] = BuildTerminalExecInputSchema();
tool["outputSchema"] = BuildTerminalExecOutputSchema();
tools.append(tool);
}
{
Json::Value tool(Json::objectValue);
tool["name"] = "terminal_close";
tool["description"] = u8"关闭并释放指定持久会话(幂等:会话已不存在也返回成功)。";
tool["inputSchema"] = BuildTerminalCloseInputSchema();
tool["outputSchema"] = BuildTerminalCloseOutputSchema();
tools.append(tool);
}
}
// 15) remote_open / remote_closeP5MCP 远程控制,仅 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前三种需 keyWindows 虚拟键名,如 ENTER/TAB/F5/LEFT/CTRL/ALT/SHIFT/WINtype 需 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默认 leftclick 的 clicks 可选 1/2/3默认 1scroll 的 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);
}
}
// 16) download_fileP6MCP 文件传输,仅 Windows安全门McpFileTransfer=1
// 独立开关(不要求 McpReadonly=0下载是只读方向主控落盘风险可控。
if (CMcpServer::Instance().IsFileTransferEnabled()) {
Json::Value tool(Json::objectValue);
tool["name"] = "download_file";
tool["description"] = u8"从指定在线 Windows 主机下载文件或目录到主控本机V2 协议,逐文件 SHA-256 校验。remote_path 为远程绝对路径文件或目录目录递归下载local_dir 为本机保存目录overwrite=false 时跳过已存在文件。返回 files[{path,size,sha256}] 与 total_bytes/skipped。";
tool["inputSchema"] = BuildDownloadFileInputSchema();
tool["outputSchema"] = BuildDownloadFileOutputSchema();
tools.append(tool);
}
// 17) upload_fileP2MCP 文件上传,仅 Windows安全门McpFileTransfer=1 且 McpReadonly=0
// 写远程盘,故额外要求关闭只读(与 terminal_* / remote_* 一致)。
if (CMcpServer::Instance().IsFileTransferEnabled() && !CMcpServer::Instance().IsReadonly()) {
Json::Value tool(Json::objectValue);
tool["name"] = "upload_file";
tool["description"] = u8"把主控本机文件或目录上传到指定在线 Windows 主机的目录V2 协议主连接发送。local_path 为主控本机绝对路径文件或目录目录递归上传remote_dir 为远程保存目录不存在自动创建overwrite=false 时跳过 remote_dir 下已存在的同名顶层项。返回 files[{path,size,sha256}] 与 total_bytes/skippedsha256 恒为空V2 无接收方回执,完整性由客户端本地校验、失败仅记日志)。";
tool["inputSchema"] = BuildUploadFileInputSchema();
tool["outputSchema"] = BuildUploadFileOutputSchema();
tools.append(tool);
}
result["tools"] = tools;
return BuildResult(id, result);
}
// tools/calllist_online_hosts
std::string BuildListOnlineHosts(const Json::Value& id, CMy2015RemoteDlg* parent) {
Json::Value hosts(Json::arrayValue);
CollectOnlineHosts(parent, hosts);
int count = (int)hosts.size();
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["hosts"] = hosts;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"") + std::to_string(count) + std::string(u8" 台主机在线。");
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// tools/callsearch_hosts
std::string BuildSearchHosts(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
Json::Value all(Json::arrayValue);
CollectOnlineHosts(parent, all);
std::string fName = GetStringArg(args, "name");
std::string fIp = GetStringArg(args, "ip");
std::string fGroup = GetStringArg(args, "group");
std::string fOs = GetStringArg(args, "os");
bool hasOnline = args.isObject() && args.isMember("online") && args["online"].isBool();
bool wantOnline = hasOnline ? args["online"].asBool() : true;
Json::Value hosts(Json::arrayValue);
// 列表只含在线主机:显式 online=false 时直接空结果
if (!hasOnline || wantOnline) {
for (unsigned int i = 0; i < all.size(); ++i) {
const Json::Value& h = all[i];
if (!fName.empty()) {
std::string name = JsonStrField(h, "name");
std::string remark = JsonStrField(h, "remark");
if (!ContainsCI(name, fName) && !ContainsCI(remark, fName)) continue;
}
if (!fIp.empty() && !ContainsCI(JsonStrField(h, "ip"), fIp)) continue;
if (!fGroup.empty() && !ContainsCI(JsonStrField(h, "group"), fGroup)) continue;
if (!fOs.empty() && !ContainsCI(JsonStrField(h, "os"), fOs)) continue;
hosts.append(h);
}
}
int count = (int)hosts.size();
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["hosts"] = hosts;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"") + std::to_string(count) + std::string(u8" 台主机匹配。");
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// tools/callget_host_detail
std::string BuildGetHostDetail(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
std::string sid = GetStringArg(args, "id");
if (sid.empty()) {
return BuildError(id, -32602, "Missing required parameter: id");
}
if (!IsDigits(sid)) {
return BuildError(id, -32602, "Invalid id: expected a decimal host id string");
}
Json::Value all(Json::arrayValue);
CollectOnlineHosts(parent, all);
for (unsigned int i = 0; i < all.size(); ++i) {
const Json::Value& h = all[i];
if (JsonStrField(h, "id") == sid) {
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["host"] = h;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
std::string name = JsonStrField(h, "name");
std::string ip = JsonStrField(h, "ip");
item["text"] = std::string(u8"主机 ") + name + " (" + ip + ")" + u8" 的详情。";
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
}
return BuildError(id, -32002, "Host not found or offline: " + sid);
}
// tools/calllist_processes主连接 RPC子连接一次性回传
std::string BuildListProcesses(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
uint64_t devId = 0;
std::string err;
if (!ParseHostIdArg(args, devId, err))
return BuildError(id, -32602, err);
context* ctx = FindMainContext(parent, devId);
if (!ctx)
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
CMcpServer& mcp = CMcpServer::Instance();
if (!mcp.BeginPending(devId, "list_processes"))
return BuildError(id, -32003, "Device busy: another request is pending for this host");
BYTE cmd = COMMAND_SYSTEM;
if (!ctx->Send2Client(&cmd, 1)) {
mcp.ClearPending(devId);
return BuildError(id, -32004, "Failed to send command to host");
}
std::vector<BYTE> data;
if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs))
return BuildError(id, -32001, "Timeout waiting for process list");
// 进程名/路径编码按 clientType 判定Windows 走 A 接口QueryFullProcessImageNameA 等)
// =客户端 ANSIGBK/936不随 CLIENT_CAP_UTF8 转 UTF-8LNX/MAC 的 /proc 天然 UTF-8。
// 不能用 GetClientEncoding——它按能力位返回 CP_UTF8会误解 Windows 客户端的 GBK 进程名。
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
UINT cp = (clientType == "LNX" || clientType == "MAC") ? CP_UTF8 : 936;
Json::Value procs = ParseProcessList(data, cp);
int count = (int)procs.size();
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["processes"] = procs;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"") + std::to_string(count) + std::string(u8" 个进程。");
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// tools/calllist_windows主连接 RPC子连接一次性回传
std::string BuildListWindows(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
uint64_t devId = 0;
std::string err;
if (!ParseHostIdArg(args, devId, err))
return BuildError(id, -32602, err);
context* ctx = FindMainContext(parent, devId);
if (!ctx)
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
CMcpServer& mcp = CMcpServer::Instance();
if (!mcp.BeginPending(devId, "list_windows"))
return BuildError(id, -32003, "Device busy: another request is pending for this host");
BYTE cmd = COMMAND_WSLIST;
if (!ctx->Send2Client(&cmd, 1)) {
mcp.ClearPending(devId);
return BuildError(id, -32004, "Failed to send command to host");
}
std::vector<BYTE> data;
if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs))
return BuildError(id, -32001, "Timeout waiting for window list");
// 窗口标题编码由客户端能力位决定(新客户端 UTF-8老客户端 CP_ACP
UINT cp = GetClientEncoding(ctx);
Json::Value wins = ParseWindowList(data, cp);
int count = (int)wins.size();
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["windows"] = wins;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"") + std::to_string(count) + std::string(u8" 个窗口。");
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// tools/callget_activity_history主连接 RPC无子链接、不弹框、不 CancelIO
std::string BuildGetActivityHistory(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
uint64_t devId = 0;
std::string err;
if (!ParseHostIdArg(args, devId, err))
return BuildError(id, -32602, err);
context* ctx = FindMainContext(parent, devId);
if (!ctx)
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
CMcpServer& mcp = CMcpServer::Instance();
if (!mcp.BeginPending(devId, "get_activity_history"))
return BuildError(id, -32003, "Device busy: another request is pending for this host");
BYTE cmd = COMMAND_QUERY_ACTIVITY;
if (!ctx->Send2Client(&cmd, 1)) {
mcp.ClearPending(devId);
return BuildError(id, -32004, "Failed to send command to host");
}
std::vector<BYTE> data;
if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs))
return BuildError(id, -32001, "Timeout waiting for activity history");
// data[0]=token其后为客户端 ActivityHistory::Dump() 的 UTF-8 纯文本
// (标题在客户端已由 GetActiveWindowTitle 转 UTF-8服务端无需再转码
std::string text;
if (data.size() > 1)
text.assign((const char*)data.data() + 1, data.size() - 1);
// 拆行:每行一条记录,过滤空行(含 Dump 末尾的换行)。
Json::Value records(Json::arrayValue);
size_t start = 0;
while (start <= text.size()) {
size_t nl = text.find('\n', start);
std::string line = text.substr(start, (nl == std::string::npos ? text.size() : nl) - start);
if (!line.empty()) records.append(line);
if (nl == std::string::npos) break;
start = nl + 1;
}
int count = (int)records.size();
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["records"] = records;
structuredContent["activityHistory"] = text;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"") + std::to_string(count) + std::string(u8" 条历史活动记录。");
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// tools/callget_screenshot屏幕预览链路主连接 RPC不建子链接、不弹框
std::string BuildGetScreenshot(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
uint64_t devId = 0;
std::string err;
if (!ParseHostIdArg(args, devId, err))
return BuildError(id, -32602, err);
context* ctx = FindMainContext(parent, devId);
if (!ctx)
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
// 能力位门槛:仅 Windows 客户端声明 CLIENT_CAP_SCREEN_PREVIEW非 Windows 返回明确错误)
if (!ctx->SupportsScreenPreview())
return BuildError(id, -32005, "Host does not support screen preview");
CMcpServer& mcp = CMcpServer::Instance();
if (!mcp.BeginPending(devId, "get_screenshot"))
return BuildError(id, -32003, "Device busy: another request is pending for this host");
uint16_t reqId = mcp.NextPreviewReqId();
mcp.SetPendingReqId(devId, reqId);
// 复用 MFC 预览的 RTT/FRP 自适应参数挑选,避免重复实现 GetTargetQualityLevel 逻辑
WORD maxWidth = 0;
BYTE quality = 0;
parent->ChooseScreenPreviewParams(ctx, maxWidth, quality);
// 可选 max_width缺省/0 沿用缩略图档位;>0 覆盖宽度(钳制到客户端上限 [64,1920]
// 供 AI 视觉/OCR 场景请求接近原分辨率的大图。jpegQuality 仍沿用档位自适应值。
if (args.isMember("max_width") && args["max_width"].isInt()) {
int mw = args["max_width"].asInt();
if (mw > 0) {
if (mw < 64) mw = 64;
if (mw > 1920) mw = 1920;
maxWidth = (WORD)mw;
}
}
parent->SendScreenPreviewRequest(ctx, reqId, maxWidth, quality);
std::vector<BYTE> data;
if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs))
return BuildError(id, -32001, "Timeout waiting for screenshot");
// data = [ScreenPreviewRspHeader][JPEG]
if (data.size() < sizeof(ScreenPreviewRspHeader))
return BuildError(id, -32000, "Invalid screenshot response");
const ScreenPreviewRspHeader* hdr = reinterpret_cast<const ScreenPreviewRspHeader*>(data.data());
if (hdr->status != SCREEN_PREVIEW_OK || hdr->format != SCREEN_PREVIEW_FMT_JPEG ||
hdr->bytes == 0 || data.size() < sizeof(ScreenPreviewRspHeader) + hdr->bytes) {
return BuildError(id, -32000,
"Screenshot capture failed (status " + std::to_string((int)hdr->status) + ")");
}
std::string b64 = httplib::detail::base64_encode(
std::string((const char*)data.data() + sizeof(ScreenPreviewRspHeader), hdr->bytes));
Json::Value image(Json::objectValue);
image["mimeType"] = "image/jpeg";
image["width"] = (Json::UInt64)hdr->width;
image["height"] = (Json::UInt64)hdr->height;
image["bytes"] = (Json::UInt64)hdr->bytes;
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["image"] = image; // 元数据(不含 base64避免在 content 之外重复大 payload
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "image";
item["data"] = b64;
item["mimeType"] = "image/jpeg";
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// tools/calllist_files列盘走 COMMAND_LIST_DRIVE→TOKEN_DRIVE_LIST
// 列目录走 COMMAND_LIST_DRIVE 开子链接后,再下发 COMMAND_LIST_FILES→TOKEN_FILE_LIST
std::string BuildListFiles(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
uint64_t devId = 0;
std::string err;
if (!ParseHostIdArg(args, devId, err))
return BuildError(id, -32602, err);
context* ctx = FindMainContext(parent, devId);
if (!ctx)
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
std::string path = GetStringArg(args, "path");
bool listDrives = path.empty() || path == "." || path == "/" || path == "\\";
// 目录路径/文件名编码按 clientType 判定Windows 走 A 接口FindFirstFileA 等)= 客户端
// ANSI(GBK/936),不随 CLIENT_CAP_UTF8 转 UTF-8LNX/MAC 文件系统天然 UTF-8。不能用
// GetClientEncoding——它按能力位返回 CP_UTF8会误解 Windows 客户端的 GBK 文件名。
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
UINT cp = (clientType == "LNX" || clientType == "MAC") ? CP_UTF8 : 936;
// 下发前把 UTF-8 path 转成客户端 ANSI空/./\/\\ 为 ASCII转换后不变OnDriveList 直接用。
std::string ansiPath = ToAnsi(path, cp);
CMcpServer& mcp = CMcpServer::Instance();
if (!mcp.BeginPending(devId, "list_files", ansiPath))
return BuildError(id, -32003, "Device busy: another request is pending for this host");
BYTE cmd = COMMAND_LIST_DRIVE;
if (!ctx->Send2Client(&cmd, 1)) {
mcp.ClearPending(devId);
return BuildError(id, -32004, "Failed to send command to host");
}
std::vector<BYTE> data;
if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs))
return BuildError(id, -32001, "Timeout waiting for file list");
Json::Value drives(Json::arrayValue);
Json::Value files(Json::arrayValue);
int count = 0;
if (listDrives) {
drives = ParseDriveList(data, cp);
count = (int)drives.size();
} else {
files = ParseFileList(data, 500, cp); // 上限 500避免 JSON 响应过大
count = (int)files.size();
}
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
if (listDrives) structuredContent["drives"] = drives;
else structuredContent["files"] = files;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = listDrives
? (std::string(u8"") + std::to_string(count) + std::string(u8" 个驱动器。"))
: (std::string(u8"") + std::to_string(count) + std::string(u8" 个条目。"));
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// tools/calllist_services主连接下发 COMMAND_SERVICES子连接一次性回传 TOKEN_SERVERLIST
std::string BuildListServices(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
uint64_t devId = 0;
std::string err;
if (!ParseHostIdArg(args, devId, err))
return BuildError(id, -32602, err);
context* ctx = FindMainContext(parent, devId);
if (!ctx)
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
// 服务管理仅 Windows 客户端实现EnumServicesStatus 等 A 接口LNX/MAC 无 Windows
// 服务概念,提前返回避免 20s 超时等待。
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
if (clientType == "LNX" || clientType == "MAC")
return BuildError(id, -32005, "list_services is only supported on Windows hosts");
CMcpServer& mcp = CMcpServer::Instance();
if (!mcp.BeginPending(devId, "list_services"))
return BuildError(id, -32003, "Device busy: another request is pending for this host");
BYTE cmd = COMMAND_SERVICES;
if (!ctx->Send2Client(&cmd, 1)) {
mcp.ClearPending(devId);
return BuildError(id, -32004, "Failed to send command to host");
}
std::vector<BYTE> data;
if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs))
return BuildError(id, -32001, "Timeout waiting for service list");
// 服务名/路径/显示名来自 Windows A 接口 = 客户端 ANSIGBK/936按 clientType 判定。
UINT cp = (clientType == "LNX" || clientType == "MAC") ? CP_UTF8 : 936;
Json::Value services = ParseServiceList(data, cp);
int count = (int)services.size();
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["services"] = services;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"") + std::to_string(count) + std::string(u8" 个服务。");
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// tools/calllist_registry主连接下发 COMMAND_REGEDIT → 子连接 TOKEN_REGEDIT → 下发
// COMMAND_REG_FIND → TOKEN_REG_PATH + TOKEN_REG_KEY 两包path 空 = 列根键,无需查询客户端)
std::string BuildListRegistry(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
uint64_t devId = 0;
std::string err;
if (!ParseHostIdArg(args, devId, err))
return BuildError(id, -32602, err);
context* ctx = FindMainContext(parent, devId);
if (!ctx)
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
// 注册表仅 Windows 客户端实现RegOpenKeyEx 等 A 接口LNX/MAC 无注册表。
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
if (clientType == "LNX" || clientType == "MAC")
return BuildError(id, -32005, "list_registry is only supported on Windows hosts");
std::string path = GetStringArg(args, "path");
BYTE rootToken = 0;
std::string relPath;
if (!ParseRegistryPath(path, rootToken, relPath)) {
if (path.empty())
return BuildRegistryRoots(id); // 空 path → 列根键
return BuildError(id, -32602,
"Invalid path: expected a root key like HKEY_LOCAL_MACHINE\\Software");
}
// 客户端 RegisterOperation 以 char KeyPath[MAX_PATH] 承载相对子键路径SetPath→strcpy 无界),
// 超长会栈溢出。按 UTF-8 字节数做保守上限GBK/936 字节数 ≤ UTF-8故只查 UTF-8 即可)。
if (relPath.size() >= MAX_PATH)
return BuildError(id, -32602, "Registry path too long (exceeds MAX_PATH)");
// 注册表键路径为客户端 ANSIGBK/936下发前转好rootToken 编码进 path[0] 供 OnRegeditReady 拆分。
UINT cp = (clientType == "LNX" || clientType == "MAC") ? CP_UTF8 : 936;
std::string rootAndPath = std::string(1, (char)rootToken) + ToAnsi(relPath, cp);
CMcpServer& mcp = CMcpServer::Instance();
if (!mcp.BeginPending(devId, "list_registry", rootAndPath))
return BuildError(id, -32003, "Device busy: another request is pending for this host");
BYTE cmd = COMMAND_REGEDIT;
if (!ctx->Send2Client(&cmd, 1)) {
mcp.ClearPending(devId);
return BuildError(id, -32004, "Failed to send command to host");
}
std::vector<BYTE> subkeys, values;
if (!mcp.WaitPendingRegistry(devId, subkeys, values, kMcpToolTimeoutMs))
return BuildError(id, -32001, "Timeout waiting for registry data");
Json::Value keysArr = ParseRegPath(subkeys, cp);
Json::Value valsArr = ParseRegKey(values, cp);
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["path"] = path;
structuredContent["keys"] = keysArr;
structuredContent["values"] = valsArr;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"") + std::to_string((int)keysArr.size()) + std::string(u8" 个子键、")
+ std::to_string((int)valsArr.size()) + std::string(u8" 个值。");
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// ========== P3exec_command ==========
// exec_command 默认白名单:只读命令前缀(逗号分隔,前缀匹配,大小写不敏感)。
// 只放查询/只读类命令写命令del/copy/move/reg add/sc stop/...)一律不在列。
static const char* kDefaultCmdWhitelist =
"dir,type,cd,chdir,ver,hostname,whoami,where,tasklist,systeminfo,ipconfig,netstat,"
"path,set,find,findstr,reg query,sc query,"
"ping,tracert,nslookup,getmac,driverquery,query,gpresult,"
"arp -a,route print,schtasks /query";
// 去首尾空白(空格/制表符/回车/换行)。
static std::string Trim(const std::string& s) {
size_t b = s.find_first_not_of(" \t\r\n");
if (b == std::string::npos) return "";
return s.substr(b, s.find_last_not_of(" \t\r\n") - b + 1);
}
// 去尾部空白(含尾部 CR/LF
static std::string TrimRight(const std::string& s) {
size_t e = s.find_last_not_of(" \t\r\n");
return (e == std::string::npos) ? "" : s.substr(0, e + 1);
}
// 命令白名单校验命令trim + ASCII 小写)以某白名单项为前缀,且其后字符为空/空格/制表符,
// 防止 "dirx" 前缀误配 "dir"。多词前缀sc query / reg query也按完整前缀匹配。
static bool IsCommandAllowed(const std::string& command, const std::string& whitelist) {
std::string c = ToLowerAscii(Trim(command));
if (c.empty()) return false;
size_t start = 0;
while (start < whitelist.size()) {
size_t comma = whitelist.find(',', start);
std::string item = Trim(whitelist.substr(
start, comma == std::string::npos ? std::string::npos : comma - start));
if (!item.empty()) {
std::string p = ToLowerAscii(item);
if (c.size() >= p.size() && c.compare(0, p.size(), p) == 0 &&
(c.size() == p.size() || c[p.size()] == ' ' || c[p.size()] == '\t')) {
return true;
}
}
if (comma == std::string::npos) break;
start = comma + 1;
}
return false;
}
// 剥 ANSI 转义序列CSI/OSC/单字节 ESC避免终端彩色码污染返回文本。
static std::string StripAnsi(const std::string& s) {
std::string out;
out.reserve(s.size());
for (size_t i = 0; i < s.size(); ++i) {
if (s[i] != '\x1b') { out += s[i]; continue; }
++i; // 吞 ESC
if (i < s.size() && s[i] == '[') { // CSI ... 结尾 @~
while (++i < s.size() && !(s[i] >= '@' && s[i] <= '~')) {}
} else if (i < s.size() && s[i] == ']') { // OSC ... 结尾 BEL/ESC
while (++i < s.size() && s[i] != '\x07' && s[i] != '\x1b') {}
}
// 其它单字节 ESC 序列吞掉当前字节即可(循环 ++i 会跳过它)
}
return out;
}
// 去掉 ConPTY 把整行输入回显造成的噪声echoedLine 是发送的哨兵命令行(含唯一 nonce
// 输出里若出现该行(连同其后换行)则整行剔除。非 PTY 老 ShellManager 已自行跳回显,
// 找不到该行即无操作。echoedLine 含随机 nonce命令真实输出不可能恰好相同。
static std::string StripEchoedCommand(const std::string& s, const std::string& echoedLine) {
if (echoedLine.empty() || s.empty()) return s;
size_t p = s.find(echoedLine);
if (p == std::string::npos) return s;
size_t q = p + echoedLine.size();
if (q < s.size() && s[q] == '\n') ++q; // CRLF 已归一为 \n
return s.substr(0, p) + s.substr(q);
}
// 命令是否含控制字符(<0x20含 \r\n。\r\n 会把命令拆成多行:一次性 exec 里会绕过
// 白名单执行任意后续行(安全);持久终端里只有末行被哨兵包装、退出码失真(正确性)。二者都拒绝。
static bool ContainsControlChar(const std::string& s) {
for (char ch : s)
if ((unsigned char)ch < 0x20) return true;
return false;
}
// tools/callexec_commandWindows 一次性远程命令:主连接 COMMAND_SHELL → 子连接终端 →
// 哨兵命令行 → 收集 stdout + exit_code → 关子链接)。安全门:只读默认 + 白名单 + 审计。
std::string BuildExecCommand(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
uint64_t devId = 0;
std::string err;
if (!ParseHostIdArg(args, devId, err))
return BuildError(id, -32602, err);
context* ctx = FindMainContext(parent, devId);
if (!ctx)
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
// 终端仅 Windows 客户端实现ConPTY / 老 cmd 管道LNX/MAC 远期再考虑。
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
if (clientType == "LNX" || clientType == "MAC")
return BuildError(id, -32005, "exec_command is only supported on Windows hosts");
std::string command = Trim(GetStringArg(args, "command"));
if (command.empty())
return BuildError(id, -32602, "Missing required parameter: command");
CMcpServer& mcp = CMcpServer::Instance();
// 安全门 1只读模式直接拒绝默认只读exec_command 需显式关只读才可用)。
if (mcp.IsReadonly())
return BuildError(id, -32006, "exec_command is disabled: MCP is in read-only mode (set McpReadonly=0 to enable)");
// 安全门 2拒绝 shell 元字符与控制字符(\r\n 会把命令拆成多行绕过白名单),防注入。
if (command.find_first_of("&|<>^") != std::string::npos || ContainsControlChar(command))
return BuildError(id, -32008, "Command contains forbidden shell characters: " + command);
// 安全门 3命令白名单前缀校验。
std::string whitelist = mcp.GetCmdWhitelist();
if (whitelist.empty()) whitelist = kDefaultCmdWhitelist;
if (!IsCommandAllowed(command, whitelist))
return BuildError(id, -32007, "Command not allowed by whitelist: " + command);
int timeoutMs = kMcpToolTimeoutMs;
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/callget_client_log主连接下发 COMMAND_QUERY_LOG子连接回传 TOKEN_REPORT_LOG
std::string BuildGetClientLog(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
uint64_t devId = 0;
std::string err;
if (!ParseHostIdArg(args, devId, err))
return BuildError(id, -32602, err);
context* ctx = FindMainContext(parent, devId);
if (!ctx)
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
CMcpServer& mcp = CMcpServer::Instance();
if (!mcp.BeginPending(devId, "get_client_log"))
return BuildError(id, -32003, "Device busy: another request is pending for this host");
BYTE cmd = COMMAND_QUERY_LOG;
if (!ctx->Send2Client(&cmd, 1)) {
mcp.ClearPending(devId);
return BuildError(id, -32004, "Failed to send command to host");
}
std::vector<BYTE> data;
if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs))
return BuildError(id, -32001, "Timeout waiting for client log");
// data[0]=token其后为 Logger 内存 ring buffer 的日志文本(无 '\0' 终止)。
// 客户端在子连接建立时 m_sentIdx=0 返回全量,随后每 3s 推增量MessageHandle 在
// 取走首条全量后立即 CancelIO 关子链接,增量不再到达,故此处即为完整快照。
// 日志文本为客户端 ANSIWindows 走 vsnprintf A 版),按 clientType 判定编码。
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
UINT cp = (clientType == "LNX" || clientType == "MAC") ? CP_UTF8 : 936;
std::string log;
if (data.size() > 1) {
std::string raw((const char*)data.data() + 1, data.size() - 1);
log = ToUtf8(raw.c_str(), cp);
}
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["log"] = log;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = log.empty() ? std::string(u8"客户端暂无内存日志。")
: std::string(u8"客户端运行日志快照如下。");
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// tools/callget_audit_log服务端本地消息日志读取 UI 日志列表的线程安全镜像)
std::string BuildGetAuditLog(const Json::Value& id, CMy2015RemoteDlg* parent) {
Json::Value entries(Json::arrayValue);
if (parent) {
EnterCriticalSection(&parent->m_cs);
// m_MessageLog 新在前、旧在后(与界面一致),直接顺序输出。
for (const auto& e : parent->m_MessageLog) {
Json::Value item(Json::objectValue);
item["type"] = ToUtf8(e.type.c_str(), 936);
item["time"] = ToUtf8(e.time.c_str(), 936);
item["msg"] = ToUtf8(e.msg.c_str(), 936);
entries.append(item);
}
LeaveCriticalSection(&parent->m_cs);
}
int count = (int)entries.size();
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["entries"] = entries;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = count > 0 ? std::string(u8"") + std::to_string(count) + std::string(u8" 条日志。")
: std::string(u8"当前无日志。");
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// ===== P4持久远程终端terminal_open / terminal_exec / terminal_close=====
static const int kTermIdleTimeoutSec = 300; // 持久终端 idle 回收超时(秒)
// 共享前置:校验 McpTerminal && !readonly → 解析 id → FindMainContext → 拒绝 LNX/MAC。
// 成功返回 true 并输出 devId/ctx失败时 errJson 已写入对应 JSON 错误串。
static bool ResolveTerminalSessionHost(const Json::Value& id, const Json::Value& args,
CMy2015RemoteDlg* parent,
uint64_t& devId, context*& ctx, std::string& errJson) {
CMcpServer& mcp = CMcpServer::Instance();
if (!mcp.IsTerminalEnabled() || mcp.IsReadonly()) {
errJson = BuildError(id, -32006,
"Persistent terminal is disabled: requires McpTerminal=1 and McpReadonly=0");
return false;
}
std::string err;
if (!ParseHostIdArg(args, devId, err)) {
errJson = BuildError(id, -32602, err);
return false;
}
ctx = FindMainContext(parent, devId);
if (!ctx) {
errJson = BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
return false;
}
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
if (clientType == "LNX" || clientType == "MAC") {
errJson = BuildError(id, -32005, "Persistent terminal is only supported on Windows hosts");
return false;
}
return true;
}
// 发送哨兵命令行(复用 exec_command 的编码/哨兵逻辑);返回所发送的完整命令行,
// 供调用方剔除 ConPTY 把输入整行回显造成的噪声。cp 由 isPty 决定。
static std::string SendTerminalCommandLine(context* subCtx, bool isPty, const std::string& command,
const std::string& nonce) {
UINT cp = isPty ? CP_UTF8 : 936;
std::string line;
if (isPty) line += "@echo off & ";
line += command;
line += " 2>&1 && echo __MCP_DONE_" + nonce + "__0 || echo __MCP_DONE_" + nonce + "__1";
std::string wireLine = ToAnsi(line, cp) + "\r\n";
subCtx->Send2Client((BYTE*)wireLine.data(), (ULONG)wireLine.size());
return line;
}
// 输出清洗(复用 exec_command 尾部逻辑raw → UTF-8 → 剥 ANSI → CRLF 归一 →
// 去 ConPTY 回显命令行 → TrimRight。
static std::string CleanTerminalOutput(const std::vector<BYTE>& raw, UINT cp,
const std::string& echoedLine) {
std::string stdoutStr;
if (!raw.empty()) {
std::string rawStr((const char*)raw.data(), raw.size());
stdoutStr = StripAnsi(ToUtf8(rawStr.c_str(), cp));
size_t p = 0;
while ((p = stdoutStr.find("\r\n", p)) != std::string::npos)
stdoutStr.erase(p, 1);
stdoutStr = StripEchoedCommand(stdoutStr, echoedLine);
stdoutStr = TrimRight(stdoutStr);
}
return stdoutStr;
}
// terminal_open 的 inputSchemaid 必填、timeout_ms 可选)
Json::Value BuildTerminalOpenInputSchema() {
Json::Value props(Json::objectValue);
Json::Value idProp(Json::objectValue);
idProp["type"] = "string";
idProp["description"] = u8"主机 id取 list_online_hosts / search_hosts 返回的 id 字段";
props["id"] = idProp;
Json::Value timeoutProp(Json::objectValue);
timeoutProp["type"] = "integer";
timeoutProp["description"] = u8"等待 shell 启动的超时毫秒数(可选,默认 20000上限 600000";
props["timeout_ms"] = timeoutProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("id");
schema["required"] = required;
return schema;
}
Json::Value BuildTerminalOpenOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value sid(Json::objectValue);
sid["type"] = "string";
sid["description"] = u8"持久会话 token后续 terminal_exec / terminal_close 用";
props["session_id"] = sid;
Json::Value pty(Json::objectValue);
pty["type"] = "boolean";
pty["description"] = u8"true=ConPTY(UTF-8)false=老 cmd 管道(GBK)";
props["is_pty"] = pty;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("session_id");
required.append("is_pty");
schema["required"] = required;
return schema;
}
Json::Value BuildTerminalExecInputSchema() {
Json::Value props(Json::objectValue);
Json::Value idProp(Json::objectValue);
idProp["type"] = "string";
idProp["description"] = u8"主机 id取 list_online_hosts / search_hosts 返回的 id 字段";
props["id"] = idProp;
Json::Value sid(Json::objectValue);
sid["type"] = "string";
sid["description"] = u8"terminal_open 返回的 session_id";
props["session_id"] = sid;
Json::Value cmdProp(Json::objectValue);
cmdProp["type"] = "string";
cmdProp["description"] = u8"要执行的命令(完整 shell不受白名单约束但不能含 & 或 |,请拆成多条调用)";
props["command"] = cmdProp;
Json::Value timeoutProp(Json::objectValue);
timeoutProp["type"] = "integer";
timeoutProp["description"] = u8"等待输出完成的超时毫秒数(可选,默认 20000上限 600000";
props["timeout_ms"] = timeoutProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("id");
required.append("session_id");
required.append("command");
schema["required"] = required;
return schema;
}
Json::Value BuildTerminalExecOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value stdoutProp(Json::objectValue);
stdoutProp["type"] = "string";
stdoutProp["description"] = u8"命令输出(已剥哨兵与 ANSI 转义)";
props["stdout"] = stdoutProp;
Json::Value exitProp(Json::objectValue);
exitProp["type"] = "integer";
exitProp["description"] = u8"退出码0=成功、1=非零退出、-1=未知(进程异常退出)";
props["exit_code"] = exitProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("stdout");
required.append("exit_code");
schema["required"] = required;
return schema;
}
Json::Value BuildTerminalCloseInputSchema() {
Json::Value props(Json::objectValue);
Json::Value idProp(Json::objectValue);
idProp["type"] = "string";
idProp["description"] = u8"主机 id取 list_online_hosts / search_hosts 返回的 id 字段";
props["id"] = idProp;
Json::Value sid(Json::objectValue);
sid["type"] = "string";
sid["description"] = u8"terminal_open 返回的 session_id";
props["session_id"] = sid;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("id");
required.append("session_id");
schema["required"] = required;
return schema;
}
Json::Value BuildTerminalCloseOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value closed(Json::objectValue);
closed["type"] = "boolean";
closed["description"] = u8"恒为 true关闭幂等";
props["closed"] = closed;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("closed");
schema["required"] = required;
return schema;
}
// tools/callterminal_open打开持久 shell 会话,返回 session_id用毕须 terminal_close
std::string BuildTerminalOpen(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
CMcpServer& mcp = CMcpServer::Instance();
mcp.SweepIdleTerminals(kTermIdleTimeoutSec);
uint64_t devId = 0;
context* ctx = nullptr;
std::string errJson;
if (!ResolveTerminalSessionHost(id, args, parent, devId, ctx, errJson))
return errJson;
int timeoutMs = kMcpToolTimeoutMs;
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/callterminal_exec在已打开的持久会话中执行一条命令无白名单但禁 & |
std::string BuildTerminalExec(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
CMcpServer& mcp = CMcpServer::Instance();
mcp.SweepIdleTerminals(kTermIdleTimeoutSec);
uint64_t devId = 0;
context* ctx = nullptr;
std::string errJson;
if (!ResolveTerminalSessionHost(id, args, parent, devId, ctx, errJson))
return errJson;
std::string sessionId = GetStringArg(args, "session_id");
if (sessionId.empty())
return BuildError(id, -32602, "Missing required parameter: session_id");
std::string command = Trim(GetStringArg(args, "command"));
if (command.empty())
return BuildError(id, -32602, "Missing required parameter: command");
// 正确性门(非安全门):& 与 | 会破坏哨兵控制操作符链、导致退出码失真;拆成多条调用即可。
// \r\n 会把命令拆成多行、只有末行被哨兵包装,同样破坏退出码捕获,一并拒绝。
if (command.find_first_of("&|") != std::string::npos || ContainsControlChar(command))
return BuildError(id, -32008,
"Command contains & or | or a newline (breaks output capture); run chained/piped commands as separate terminal_exec calls");
int timeoutMs = kMcpToolTimeoutMs;
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/callterminal_close关闭持久会话幂等session_id 不匹配报错)
std::string BuildTerminalClose(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
CMcpServer& mcp = CMcpServer::Instance();
mcp.SweepIdleTerminals(kTermIdleTimeoutSec);
if (!mcp.IsTerminalEnabled() || mcp.IsReadonly())
return BuildError(id, -32006, "Persistent terminal is disabled: requires McpTerminal=1 and McpReadonly=0");
uint64_t devId = 0;
std::string err;
if (!ParseHostIdArg(args, devId, err))
return BuildError(id, -32602, err);
std::string sessionId = GetStringArg(args, "session_id");
if (sessionId.empty())
return BuildError(id, -32602, "Missing required parameter: session_id");
int r = mcp.CloseTermSession(devId, sessionId);
if (r == 2)
return BuildError(id, -32002, "Terminal session_id mismatch: " + sessionId);
// 审计:关闭会话(不可关闭)。
if (parent) {
std::string text = "host " + std::to_string(devId) + " term-close: session " + sessionId;
parent->PostMessageA(WM_SHOWERRORMSG,
(WPARAM)new CString(ToAnsi(text, 936).c_str()),
(LPARAM)new CString(_TR("MCP持久终端")));
}
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["closed"] = true;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"closed");
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// ===== P5MCP 远程控制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 的 inputSchemaid 必填、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/callremote_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.1Observe 抓主屏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/callremote_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);
}
// ===== P5remote_keyboard键盘注入key_down / key_up / key_press / type=====
// 键名 → 虚拟键码(对齐 Windows VK_* 去掉前缀,见设计 §4.3)。
// 支持:单字符 A-Z/0-9、F1-F24、命名键、修饰键。未知返回 0VK 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;
}
// 是否纯 ASCIItype 走物理键事件,只能可靠注入 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 在服务端按服务端线程键盘布局解析字符→虚拟键;仅限 ASCII0..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/callremote_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);
}
// ===== P5remote_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/callremote_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);
}
// ===== P5remote_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/callremote_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_TEXTANSI/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);
}
// ===== P6download_file 辅助 =====
// SHA-256 摘要转小写 hex32 字节 → 64 字符)
std::string Sha256Hex(const uint8_t* b, size_t n) {
static const char hex[] = "0123456789abcdef";
std::string out;
out.reserve(n * 2);
for (size_t i = 0; i < n; ++i) {
out += hex[b[i] >> 4];
out += hex[b[i] & 0xF];
}
return out;
}
// 逐层创建目录Windows 盘符路径;失败返回 false
bool EnsureDirA(const std::string& path) {
std::string current;
for (size_t i = 0; i < path.size(); ++i) {
current += path[i];
if (path[i] == '\\' || path[i] == '/' || i + 1 == path.size()) {
if (current.size() == 2 && current[1] == ':') continue; // 跳过盘符 "C:"
if (GetFileAttributesA(current.c_str()) == INVALID_FILE_ATTRIBUTES) {
if (!CreateDirectoryA(current.c_str(), nullptr) &&
GetLastError() != ERROR_ALREADY_EXISTS) {
return false;
}
}
}
}
return GetFileAttributesA(path.c_str()) != INVALID_FILE_ATTRIBUTES;
}
// 把 UTF-8 local_dir 转为 ANSI 绝对路径(结尾 '\'),并创建目录。失败返回 false 并写 err。
bool NormalizeLocalDir(const std::string& utf8, std::string& ansiOut, std::string& err) {
if (utf8.empty()) { err = "Missing required parameter: local_dir"; return false; }
std::string ansi = ToAnsi(utf8, CP_ACP);
if (ansi.empty()) { err = "Invalid local_dir encoding"; return false; }
char full[MAX_PATH];
DWORD n = GetFullPathNameA(ansi.c_str(), MAX_PATH, full, nullptr);
if (n == 0 || n >= MAX_PATH) { err = "Invalid local_dir: " + utf8; return false; }
std::string dir = full;
while (!dir.empty() && (dir.back() == '\\' || dir.back() == '/')) dir.pop_back();
if (dir.size() == 2 && dir[1] == ':') { err = "Invalid local_dir: " + utf8; return false; }
if (!EnsureDirA(dir)) { err = "Failed to create local_dir: " + utf8; return false; }
ansiOut = dir + "\\";
return true;
}
// 防路径穿越fullPath 规范化后必须位于 baseDir 内(含 baseDir 本身)。
bool PathWithinDirA(const std::string& baseDir, const std::string& fullPath) {
char baseFull[MAX_PATH], pathFull[MAX_PATH];
if (!GetFullPathNameA(baseDir.c_str(), MAX_PATH, baseFull, nullptr)) return false;
if (!GetFullPathNameA(fullPath.c_str(), MAX_PATH, pathFull, nullptr)) return false;
for (char* p = baseFull; *p; ++p) { if (*p == '/') *p = '\\'; else if (*p >= 'A' && *p <= 'Z') *p = (char)(*p - 'A' + 'a'); }
for (char* p = pathFull; *p; ++p) { if (*p == '/') *p = '\\'; else if (*p >= 'A' && *p <= 'Z') *p = (char)(*p - 'A' + 'a'); }
size_t bl = strlen(baseFull);
while (bl > 0 && baseFull[bl - 1] == '\\') --bl;
if (bl == 0) return true; // 根目录
if (strncmp(pathFull, baseFull, bl) != 0) return false;
return pathFull[bl] == '\\' || pathFull[bl] == '\0';
}
Json::Value BuildDownloadFileInputSchema() {
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 rp(Json::objectValue);
rp["type"] = "string";
rp["description"] = u8"远程绝对路径(文件或目录;目录递归下载),如 C:\\Users\\foo\\Pictures";
props["remote_path"] = rp;
Json::Value ld(Json::objectValue);
ld["type"] = "string";
ld["description"] = u8"主控本机保存目录(不存在则自动创建),如 C:\\Downloads";
props["local_dir"] = ld;
Json::Value ow(Json::objectValue);
ow["type"] = "boolean";
ow["description"] = u8"是否覆盖已存在的同名文件;默认 false跳过并计入 skipped";
props["overwrite"] = ow;
Json::Value to(Json::objectValue);
to["type"] = "integer";
to["description"] = u8"超时毫秒(默认 600000=10 分钟,上限 3600000=1 小时)";
props["timeout_ms"] = to;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("id");
required.append("remote_path");
required.append("local_dir");
schema["required"] = required;
return schema;
}
Json::Value BuildDownloadFileOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value filesProp(Json::objectValue);
filesProp["type"] = "array";
Json::Value items(Json::objectValue);
items["type"] = "object";
Json::Value itemProps(Json::objectValue);
Json::Value pathProp(Json::objectValue); pathProp["type"] = "string"; itemProps["path"] = pathProp;
Json::Value sizeProp(Json::objectValue); sizeProp["type"] = "integer"; itemProps["size"] = sizeProp;
Json::Value shaProp(Json::objectValue); shaProp["type"] = "string"; itemProps["sha256"] = shaProp;
items["properties"] = itemProps;
filesProp["items"] = items;
props["files"] = filesProp;
Json::Value tb(Json::objectValue); tb["type"] = "integer"; props["total_bytes"] = tb;
Json::Value sk(Json::objectValue); sk["type"] = "integer"; props["skipped"] = sk;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
return schema;
}
// tools/calldownload_file下载远程文件/目录到本机V2 协议)
std::string BuildDownloadFile(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
// 分派门控(独立开关 McpFileTransfer不要求 McpReadonly=0即便绕过 tools/list 直调也拒绝。
if (!CMcpServer::Instance().IsFileTransferEnabled())
return BuildError(id, -32006, "File transfer is disabled: requires McpFileTransfer=1");
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 客户端实现 CMD_DOWN_FILES_V2client/FileManager.cppLNX/MAC 提前拒绝。
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
if (clientType == "LNX" || clientType == "MAC")
return BuildError(id, -32005, "download_file is only supported on Windows hosts");
// 客户端须支持 V2 文件传输(否则 CMD_DOWN_FILES_V2 会被静默忽略 → 超时)。
if (!ctx->SupportsFileV2())
return BuildError(id, -32006, "Host does not support V2 file transfer");
std::string remotePathUtf8 = GetStringArg(args, "remote_path");
if (remotePathUtf8.empty())
return BuildError(id, -32602, "Missing required parameter: remote_path");
std::string localDirUtf8 = GetStringArg(args, "local_dir");
if (localDirUtf8.empty())
return BuildError(id, -32602, "Missing required parameter: local_dir");
bool overwrite = false;
if (args.isMember("overwrite") && args["overwrite"].isBool())
overwrite = args["overwrite"].asBool();
int timeoutMs = 600000;
if (args.isMember("timeout_ms")) {
int v = 0;
if (!GetIntArg(args, "timeout_ms", v) || v <= 0 || v > 3600000)
return BuildError(id, -32602, "timeout_ms must be in range 1..3600000");
timeoutMs = v;
}
// 本机保存目录UTF-8 → ANSI 绝对路径(结尾 '\'),并创建。
std::string localDirAnsi;
if (!NormalizeLocalDir(localDirUtf8, localDirAnsi, err))
return BuildError(id, -32602, err);
// 远程路径UTF-8 → 客户端 ANSIWindows=936与 list_files 一致)。
std::string remotePathAnsi = ToAnsi(remotePathUtf8, 936);
if (remotePathAnsi.empty())
return BuildError(id, -32602, "Invalid remote_path encoding");
CMcpServer& mcp = CMcpServer::Instance();
if (!mcp.BeginFileTransferPending(devId, "download_file", localDirAnsi, remotePathAnsi, overwrite))
return BuildError(id, -32003, "Device busy: another request is pending for this host");
BYTE cmd = COMMAND_LIST_DRIVE;
if (!ctx->Send2Client(&cmd, 1)) {
mcp.ClearFileTransfer(devId);
return BuildError(id, -32004, "Failed to send command to host");
}
std::vector<FileTransferEntry> files;
int skipped = 0, error = 0;
bool ok = mcp.WaitFileTransferDone(devId, timeoutMs, files, skipped, error);
mcp.ClearFileTransfer(devId); // 擦会话 + CancelIO 子链接
if (ok) {
uint64_t totalBytes = 0;
for (auto& f : files) totalBytes += f.size;
// 审计(不可关闭;只记路径与字节数,不落文件内容)
if (parent) {
std::string audit = "host " + std::to_string(devId) + " download_file: "
+ remotePathUtf8 + " -> " + localDirUtf8
+ " (" + std::to_string(files.size()) + " files, "
+ std::to_string(totalBytes) + " bytes, skipped " + std::to_string(skipped) + ")";
parent->PostMessageA(WM_SHOWERRORMSG,
(WPARAM)new CString(ToAnsi(audit, 936).c_str()),
(LPARAM)new CString(_TR("MCP文件传输")));
}
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
Json::Value filesArr(Json::arrayValue);
for (auto& f : files) {
Json::Value fo(Json::objectValue);
fo["path"] = ToUtf8(f.path.c_str(), CP_ACP);
fo["size"] = (Json::UInt64)f.size;
fo["sha256"] = f.sha256;
filesArr.append(fo);
}
structuredContent["files"] = filesArr;
structuredContent["total_bytes"] = (Json::UInt64)totalBytes;
structuredContent["skipped"] = skipped;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"下载完成:") + std::to_string(files.size()) + u8" 个文件("
+ std::to_string(totalBytes) + u8" 字节),跳过 " + std::to_string(skipped) + u8" 个。";
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// 失败:删除已落盘的半成品文件(跳过的文件是既有的,不删)。
for (auto& f : files) {
if (!f.path.empty()) DeleteFileA(f.path.c_str());
}
if (parent) {
std::string audit = "host " + std::to_string(devId) + " download_file FAILED (error " + std::to_string(error)
+ "): " + remotePathUtf8 + " -> " + localDirUtf8;
parent->PostMessageA(WM_SHOWERRORMSG,
(WPARAM)new CString(ToAnsi(audit, 936).c_str()),
(LPARAM)new CString(_TR("MCP文件传输")));
}
const char* msg = (error == 1002) ? "Timeout waiting for file transfer"
: (error == 1001) ? "Path traversal detected in received path"
: (error == FEV2_HASH_MISMATCH) ? "File SHA-256 verification failed"
: "File transfer failed";
return BuildError(id, -32001, msg);
}
// ===== P2upload_file 辅助 =====
// ASCII 小写GBK 双字节首字节 0x81..0xFE 不含 A-Z安全Windows 路径名不区分大小写。
static std::string AsciiLower(std::string s) {
for (char& c : s) if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
return s;
}
// 词法检查路径是否含 ".." 段(防上传目标逃逸到预期目录之外)。
static bool HasDotDotSegment(const std::string& path) {
size_t i = 0;
while (i < path.size()) {
size_t j = path.find_first_of("\\/", i);
if (j == std::string::npos) j = path.size();
size_t n = j - i;
if (n == 2 && path[i] == '.' && path[i + 1] == '.') return true;
i = j + 1;
}
return false;
}
// 递归收集本机目录下的文件与目录项(目录在前、子项随后,供 FileBatchTransferWorkerV2 发送)。
// 与 common/file_upload.cpp 的 ExpandDirectory 同构(该函数未在 file_upload.h 导出,故本地实现)。
static void CollectLocalFiles(const std::string& dir, std::vector<std::string>& out) {
std::string searchPath = dir + "\\*";
WIN32_FIND_DATAA fd;
HANDLE hFind = FindFirstFileA(searchPath.c_str(), &fd);
if (hFind == INVALID_HANDLE_VALUE) return;
do {
if (strcmp(fd.cFileName, ".") == 0 || strcmp(fd.cFileName, "..") == 0) continue;
std::string fullPath = dir + "\\" + fd.cFileName;
out.push_back(fullPath); // 文件与目录都加入
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
CollectLocalFiles(fullPath, out); // 递归(目录项先于其子文件)
} while (FindNextFileA(hFind, &fd));
FindClose(hFind);
}
// upload_file 无头发送回调数据
struct UploadCallbackData {
CMy2015RemoteDlg* parent;
uint64_t clientID;
DWORD deadline = 0; // 整体传输截止GetTickCount 毫秒0=不限)
bool timedOut = false;
bool failed = false; // 任一 Send2Client 失败或客户端离线
};
// upload_file 无头发送回调(镜像 GUI SendFileChunkToClientV2去掉 dlg 进度)。
static bool UploadSendChunkHeadless(void* user, FileChunkPacketV2* chunk, unsigned char* data, int size) {
UploadCallbackData* cb = (UploadCallbackData*)user;
if (!cb || !cb->parent) return false;
if (cb->deadline && (int)(GetTickCount() - cb->deadline) >= 0) { // 整体超时DWORD 回绕安全)
cb->timedOut = true;
cb->failed = true;
return false;
}
context* ctx = cb->parent->FindHost(cb->clientID);
if (!ctx) { cb->failed = true; return false; }
BOOL sent = ctx->Send2Client(data, size);
if (!sent) cb->failed = true;
return sent != FALSE;
}
// 列远程目录一层,返回顶层名集合(小写原始 ANSI客户端 ANSI=936 与主控本地名同编码)。
// 复用 list_files 的 COMMAND_LIST_DRIVE→TOKEN_DRIVE_LIST→COMMAND_LIST_FILES→TOKEN_FILE_LIST 链路。
static bool ListRemoteTopLevelNames(CMy2015RemoteDlg* parent, uint64_t devId,
const std::string& remoteDirAnsi,
std::set<std::string>& names) {
CMcpServer& mcp = CMcpServer::Instance();
if (!mcp.BeginPending(devId, "list_files", remoteDirAnsi)) return false;
context* ctx = FindMainContext(parent, devId);
if (!ctx) { mcp.ClearPending(devId); return false; }
BYTE cmd = COMMAND_LIST_DRIVE;
if (!ctx->Send2Client(&cmd, 1)) { mcp.ClearPending(devId); return false; }
std::vector<BYTE> data;
if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs)) return false;
// TOKEN_FILE_LIST 布局:[token:1][attr:1][name\0][sizeHigh:4][sizeLow:4][ft:8]...
if (data.size() < 2) return true; // 空目录 → 无顶层名
const char* p = (const char*)data.data();
size_t len = data.size();
size_t off = 1; // 跳过 token 字节
while (off + 1 <= len) {
off += 1; // attr
const char* name = p + off;
size_t nlen = BoundedStrlen(name, len - off);
if (nlen == 0 || nlen >= len - off) break; // 空记录/越界 = 尾部零填充
off += nlen + 1;
if (off + 16 > len) break;
off += 16; // sizeHigh/sizeLow/ft(8)
names.insert(AsciiLower(std::string(name, nlen)));
}
return true;
}
Json::Value BuildUploadFileInputSchema() {
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 lp(Json::objectValue);
lp["type"] = "string";
lp["description"] = u8"主控本机文件或目录的绝对路径(目录递归上传),如 C:\\Users\\foo\\Pictures";
props["local_path"] = lp;
Json::Value rd(Json::objectValue);
rd["type"] = "string";
rd["description"] = u8"远程主机的保存目录(不存在则自动创建),如 C:\\uploads";
props["remote_dir"] = rd;
Json::Value ow(Json::objectValue);
ow["type"] = "boolean";
ow["description"] = u8"是否覆盖 remote_dir 下已存在的同名顶层文件/目录;默认 false跳过并计入 skipped";
props["overwrite"] = ow;
Json::Value to(Json::objectValue);
to["type"] = "integer";
to["description"] = u8"超时毫秒(默认 600000=10 分钟,上限 3600000=1 小时)";
props["timeout_ms"] = to;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("id");
required.append("local_path");
required.append("remote_dir");
schema["required"] = required;
return schema;
}
Json::Value BuildUploadFileOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value filesProp(Json::objectValue);
filesProp["type"] = "array";
Json::Value items(Json::objectValue);
items["type"] = "object";
Json::Value itemProps(Json::objectValue);
Json::Value pathProp(Json::objectValue); pathProp["type"] = "string"; itemProps["path"] = pathProp;
Json::Value sizeProp(Json::objectValue); sizeProp["type"] = "integer"; itemProps["size"] = sizeProp;
Json::Value shaProp(Json::objectValue); shaProp["type"] = "string"; itemProps["sha256"] = shaProp;
items["properties"] = itemProps;
filesProp["items"] = items;
props["files"] = filesProp;
Json::Value tb(Json::objectValue); tb["type"] = "integer"; props["total_bytes"] = tb;
Json::Value sk(Json::objectValue); sk["type"] = "integer"; props["skipped"] = sk;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
return schema;
}
// tools/callupload_file上传主控本机文件/目录到远程主机V2 协议,主连接发送)
std::string BuildUploadFile(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
// 分派门控McpFileTransfer=1 且 McpReadonly=0即便绕过 tools/list 直调也拒绝。
if (!CMcpServer::Instance().IsFileTransferEnabled())
return BuildError(id, -32006, "File transfer is disabled: requires McpFileTransfer=1");
if (CMcpServer::Instance().IsReadonly())
return BuildError(id, -32006, "upload_file is disabled in read-only mode: requires McpReadonly=0");
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 客户端实现 V2 主连接接收链路(与 download_file 一致)。
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
if (clientType == "LNX" || clientType == "MAC")
return BuildError(id, -32005, "upload_file is only supported on Windows hosts");
if (!ctx->SupportsFileV2())
return BuildError(id, -32006, "Host does not support V2 file transfer");
std::string localPathUtf8 = GetStringArg(args, "local_path");
if (localPathUtf8.empty())
return BuildError(id, -32602, "Missing required parameter: local_path");
std::string remoteDirUtf8 = GetStringArg(args, "remote_dir");
if (remoteDirUtf8.empty())
return BuildError(id, -32602, "Missing required parameter: remote_dir");
bool overwrite = false;
if (args.isMember("overwrite") && args["overwrite"].isBool())
overwrite = args["overwrite"].asBool();
int timeoutMs = 600000;
if (args.isMember("timeout_ms")) {
int v = 0;
if (!GetIntArg(args, "timeout_ms", v) || v <= 0 || v > 3600000)
return BuildError(id, -32602, "timeout_ms must be in range 1..3600000");
timeoutMs = v;
}
// 主控本机路径UTF-8 → 主控 ANSI去尾斜杠。
std::string localPathAnsi = ToAnsi(localPathUtf8, CP_ACP);
if (localPathAnsi.empty())
return BuildError(id, -32602, "Invalid local_path encoding");
while (!localPathAnsi.empty() && (localPathAnsi.back() == '\\' || localPathAnsi.back() == '/'))
localPathAnsi.pop_back();
DWORD attr = GetFileAttributesA(localPathAnsi.c_str());
if (attr == INVALID_FILE_ATTRIBUTES)
return BuildError(id, -32602, "local_path not found: " + localPathUtf8);
// 收集本机文件 + 目录项(目录在前、子项随后),镜像 ExpandDirectories 的顺序语义。
std::vector<std::string> files;
files.push_back(localPathAnsi);
if (attr & FILE_ATTRIBUTE_DIRECTORY)
CollectLocalFiles(localPathAnsi, files);
// 远程保存目录UTF-8 → 客户端 ANSI(936),去尾斜杠 + 结尾 '\'(作 targetDir 前缀)。
std::string remoteDirAnsi = ToAnsi(remoteDirUtf8, 936);
if (remoteDirAnsi.empty())
return BuildError(id, -32602, "Invalid remote_dir encoding");
while (!remoteDirAnsi.empty() && (remoteDirAnsi.back() == '\\' || remoteDirAnsi.back() == '/'))
remoteDirAnsi.pop_back();
if (remoteDirAnsi.empty())
return BuildError(id, -32602, "Invalid remote_dir: " + remoteDirUtf8);
if (HasDotDotSegment(remoteDirAnsi))
return BuildError(id, -32602, "remote_dir must not contain '..' path segments");
std::string targetDir = remoteDirAnsi + "\\";
// overwrite=false预检 remote_dir 一层,跳过已存在的同名顶层项(单文件精确、目录整体跳过)。
int skipped = 0;
std::vector<std::string> sendFiles;
if (!overwrite) {
std::set<std::string> existing;
if (!ListRemoteTopLevelNames(parent, devId, remoteDirAnsi, existing))
return BuildError(id, -32001, "Failed to list remote_dir for overwrite check");
std::string rootDir = GetCommonRoot(files);
for (const std::string& f : files) {
DWORD a = GetFileAttributesA(f.c_str());
bool isDir = (a != INVALID_FILE_ATTRIBUTES && (a & FILE_ATTRIBUTE_DIRECTORY));
std::string rel = GetRelativePath(rootDir, f);
size_t sep = rel.find_first_of("\\/");
std::string top = (sep == std::string::npos) ? rel : rel.substr(0, sep);
if (existing.count(AsciiLower(top))) {
if (!isDir) skipped++; // 目录项不计入 skipped与 download 一致)
continue;
}
sendFiles.push_back(f);
}
} else {
sendFiles = files;
}
// 全部被跳过:直接返回成功(不发起传输)。
if (sendFiles.empty()) {
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["files"] = Json::Value(Json::arrayValue);
structuredContent["total_bytes"] = (Json::UInt64)0;
structuredContent["skipped"] = skipped;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"上传完成:所有文件均已存在,跳过 ") + std::to_string(skipped) + u8" 个。";
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
CMcpServer& mcp = CMcpServer::Instance();
if (!mcp.BeginFileUpload(devId))
return BuildError(id, -32003, "Device busy: another request is pending for this host");
TransferOptionsV2 opts;
opts.transferID = GenerateTransferID();
opts.srcClientID = 0; // 主控端
opts.dstClientID = devId;
opts.enableResume = false;
std::string hash = GetPwdHash();
std::string hmac = GetHMAC(100);
UploadCallbackData cbData;
cbData.parent = parent;
cbData.clientID = devId;
cbData.deadline = GetTickCount() + (DWORD)timeoutMs; // DWORD 回绕由回调的 (int) 差值判断吸收
// 同步驱动发送(走主连接);无外来流,无需 WaitFileTransferDone / 收包计数。
int result = FileBatchTransferWorkerV2(sendFiles, targetDir, &cbData,
UploadSendChunkHeadless, nullptr, hash, hmac, opts);
bool clientOnline = (FindMainContext(parent, devId) != nullptr);
mcp.ClearFileTransfer(devId);
bool ok = (result == 0) && !cbData.failed && clientOnline;
// 计算已发送文件(远程完整路径 = targetDir + relPath与字节数目录项不计入 files。
std::string outRoot = GetCommonRoot(sendFiles);
std::vector<FileTransferEntry> outFiles;
uint64_t totalBytes = 0;
for (const std::string& f : sendFiles) {
DWORD a = GetFileAttributesA(f.c_str());
if (a == INVALID_FILE_ATTRIBUTES || (a & FILE_ATTRIBUTE_DIRECTORY)) continue;
std::string rel = GetRelativePath(outRoot, f);
FileTransferEntry e;
e.path = targetDir + rel;
WIN32_FILE_ATTRIBUTE_DATA fad;
if (GetFileAttributesExA(f.c_str(), GetFileExInfoStandard, &fad))
e.size = ((uint64_t)fad.nFileSizeHigh << 32) | fad.nFileSizeLow;
// §7.5V2 无接收方→发送方 ACK发送方自算 SHA-256 未由 worker 回传P2 留空(延后 P3
e.sha256 = "";
totalBytes += e.size;
outFiles.push_back(std::move(e));
}
if (ok) {
if (parent) {
std::string audit = "host " + std::to_string(devId) + " upload_file: "
+ localPathUtf8 + " -> " + remoteDirUtf8
+ " (" + std::to_string(outFiles.size()) + " files, "
+ std::to_string(totalBytes) + " bytes, skipped " + std::to_string(skipped) + ")";
parent->PostMessageA(WM_SHOWERRORMSG,
(WPARAM)new CString(ToAnsi(audit, 936).c_str()),
(LPARAM)new CString(_TR("MCP文件传输")));
}
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
Json::Value filesArr(Json::arrayValue);
for (auto& f : outFiles) {
Json::Value fo(Json::objectValue);
fo["path"] = ToUtf8(f.path.c_str(), 936);
fo["size"] = (Json::UInt64)f.size;
fo["sha256"] = f.sha256;
filesArr.append(fo);
}
structuredContent["files"] = filesArr;
structuredContent["total_bytes"] = (Json::UInt64)totalBytes;
structuredContent["skipped"] = skipped;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"上传完成:") + std::to_string(outFiles.size()) + u8" 个文件("
+ std::to_string(totalBytes) + u8" 字节),跳过 " + std::to_string(skipped) + u8" 个。";
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
if (parent) {
std::string audit = "host " + std::to_string(devId) + " upload_file FAILED: "
+ localPathUtf8 + " -> " + remoteDirUtf8;
parent->PostMessageA(WM_SHOWERRORMSG,
(WPARAM)new CString(ToAnsi(audit, 936).c_str()),
(LPARAM)new CString(_TR("MCP文件传输")));
}
const char* msg = cbData.timedOut ? "Timeout waiting for upload to complete"
: "Upload failed (target offline or send error)";
return BuildError(id, -32001, msg);
}
// 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);
if (toolName == "download_file") return BuildDownloadFile(id, args, parent);
if (toolName == "upload_file") return BuildUploadFile(id, args, parent);
return BuildError(id, -32602,
"Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName));
}
} // namespace
// ===== P3exec_command 哨兵检测与终端会话 =====
// 在原始 shell 输出字节流中定位哨兵(纯 ASCII编码无关。真实哨兵由 echo 单独输出、独占
// 一行(前面是 \n 或缓冲区开头);而 ConPTY 会把整条命令行回显进输出流,哨兵字样嵌在
// "&& echo ... || echo ..." 里、前面是空格。据此从后往前找「行首命中」,排除回显行误判:
// 否则回显包先于命令输出单独到达时rfind 会命中回显里的 __1 提前结束、截断真实输出。
// 老 ShellManager 已自行跳过回显,输出里只有真实哨兵(仍为行首)。命中 __0 → exit 0__1 → exit 1。
static bool FindSentinel(const std::vector<BYTE>& buf, const std::string& nonce,
size_t& pos, int& exitCode) {
std::string marker = "__MCP_DONE_" + nonce + "__";
if (marker.size() + 1 > buf.size()) return false;
std::string s((const char*)buf.data(), buf.size());
size_t from = std::string::npos;
while (true) {
size_t p = s.rfind(marker, from);
if (p == std::string::npos) return false;
bool lineStart = (p == 0) || (s[p - 1] == '\n') || (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();
}
// ===== P5MCP 远程控制(状态机方法)=====
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) {
if (IsFileTransferPending(device_id)) return false; // 该 host 有文件传输会话在飞(互斥,见 F2
std::lock_guard<std::mutex> lk(m_PendingMutex);
if (m_Pending.find(device_id) != m_Pending.end()) return false; // 设备忙
PendingRequest r;
r.tool = tool;
m_Pending[device_id] = std::move(r);
return true;
}
bool CMcpServer::WaitPending(uint64_t device_id, std::vector<BYTE>& out, int timeoutMs) {
std::unique_lock<std::mutex> lk(m_PendingMutex);
auto it = m_Pending.find(device_id);
if (it == m_Pending.end()) return false;
bool signaled = m_PendingCv.wait_for(lk, std::chrono::milliseconds(timeoutMs),
[&] { return it->second.done; });
if (!signaled || it->second.data.empty()) {
m_Pending.erase(it); // 超时/空数据 → 清理
return false;
}
out = std::move(it->second.data);
m_Pending.erase(it);
return true;
}
void CMcpServer::ClearPending(uint64_t device_id) {
std::lock_guard<std::mutex> lk(m_PendingMutex);
m_Pending.erase(device_id);
}
// ===== P2clist_files / get_screenshot 扩展 =====
bool CMcpServer::BeginPending(uint64_t device_id, const std::string& tool, const std::string& path) {
if (IsFileTransferPending(device_id)) return false; // 该 host 有文件传输会话在飞(互斥,见 F2
std::lock_guard<std::mutex> lk(m_PendingMutex);
if (m_Pending.find(device_id) != m_Pending.end()) return false; // 设备忙
PendingRequest r;
r.tool = tool;
r.path = path;
m_Pending[device_id] = std::move(r);
return true;
}
uint16_t CMcpServer::NextPreviewReqId() {
uint16_t v = m_PreviewReqId.fetch_add(1, std::memory_order_relaxed);
if (v == 0) v = m_PreviewReqId.fetch_add(1, std::memory_order_relaxed); // 跳过 0
return v;
}
void CMcpServer::SetPendingReqId(uint64_t device_id, uint16_t reqId) {
std::lock_guard<std::mutex> lk(m_PendingMutex);
auto it = m_Pending.find(device_id);
if (it != m_Pending.end()) it->second.expectedReqId = reqId;
}
bool CMcpServer::TakePreviewResponse(uint64_t device_id, uint16_t reqId, const BYTE* data, ULONG len) {
std::lock_guard<std::mutex> lk(m_PendingMutex);
auto it = m_Pending.find(device_id);
if (it == m_Pending.end()) return false; // 已超时清理 → 迟到数据,回落 MFC
if (it->second.tool != "get_screenshot") return false;
if (it->second.expectedReqId == 0 || it->second.expectedReqId != reqId) return false; // 过期/他途响应
it->second.data.assign(data, data + len);
it->second.done = true;
m_PendingCv.notify_one();
return true;
}
bool CMcpServer::OnDriveList(uint64_t device_id, context* subCtx, const BYTE* buf, ULONG len) {
std::string path;
bool listDrives = true;
{
std::lock_guard<std::mutex> lk(m_PendingMutex);
auto it = m_Pending.find(device_id);
if (it == m_Pending.end()) return true; // 已超时清理 → 调用方 CancelIO 收尾
path = it->second.path;
listDrives = path.empty() || path == "." || path == "/" || path == "\\";
if (listDrives) {
it->second.data.assign(buf, buf + len);
it->second.done = true;
m_PendingCv.notify_one();
return true; // 只列盘 → 调用方用完即关
}
}
// 列目录:锁外下发 COMMAND_LIST_FILES + path子链接保持等 TOKEN_FILE_LIST。
// path 已在 BuildListFiles 按客户端 ANSI 转好OnDriveList 无需再转),结尾 '\0'
// 与 FileManagerDlg 的 PacketSize=len+2 一致。
std::vector<BYTE> pkt;
pkt.reserve(1 + path.size() + 1);
pkt.push_back((BYTE)COMMAND_LIST_FILES);
pkt.insert(pkt.end(), path.begin(), path.end());
pkt.push_back(0);
subCtx->Send2Client(pkt.data(), (ULONG)pkt.size());
return false; // 继续等 TOKEN_FILE_LIST调用方不 CancelIO
}
// ===== P3list_registry 扩展 =====
bool CMcpServer::OnRegeditReady(uint64_t device_id, context* subCtx) {
std::string rootAndPath;
{
std::lock_guard<std::mutex> lk(m_PendingMutex);
auto it = m_Pending.find(device_id);
if (it == m_Pending.end()) return false; // 无挂起 → 回落 MFC
if (it->second.tool != "list_registry") return false;
rootAndPath = it->second.path; // [rootToken:1][相对子键路径...]
}
if (rootAndPath.empty()) return false; // 不应发生list_registry 必有 rootToken
BYTE rootToken = (BYTE)rootAndPath[0];
std::string relPath = rootAndPath.substr(1);
// 下发 COMMAND_REG_FIND布局与 RegisterDlg::OnTvnSelchangedTree 一致
// [COMMAND_REG_FIND][rootToken][relPath...]['\0']。子链接保持,等 TOKEN_REG_PATH + KEY。
std::vector<BYTE> pkt;
pkt.reserve(2 + relPath.size() + 1);
pkt.push_back((BYTE)COMMAND_REG_FIND);
pkt.push_back(rootToken);
pkt.insert(pkt.end(), relPath.begin(), relPath.end());
pkt.push_back(0);
subCtx->Send2Client(pkt.data(), (ULONG)pkt.size());
return true; // 接管子链接,不打开 MFC 对话框
}
void CMcpServer::TakeRegPath(uint64_t device_id, const BYTE* data, ULONG len) {
std::lock_guard<std::mutex> lk(m_PendingMutex);
auto it = m_Pending.find(device_id);
if (it == m_Pending.end()) return; // 已超时清理 → 迟到数据丢弃
if (it->second.tool != "list_registry") return;
it->second.regPath.assign(data, data + len);
it->second.pathDone = true;
if (it->second.keyDone) {
it->second.done = true;
m_PendingCv.notify_one();
}
}
void CMcpServer::TakeRegKey(uint64_t device_id, const BYTE* data, ULONG len) {
std::lock_guard<std::mutex> lk(m_PendingMutex);
auto it = m_Pending.find(device_id);
if (it == m_Pending.end()) return;
if (it->second.tool != "list_registry") return;
it->second.regKey.assign(data, data + len);
it->second.keyDone = true;
if (it->second.pathDone) {
it->second.done = true;
m_PendingCv.notify_one();
}
}
bool CMcpServer::WaitPendingRegistry(uint64_t device_id, std::vector<BYTE>& subkeys,
std::vector<BYTE>& values, int timeoutMs) {
std::unique_lock<std::mutex> lk(m_PendingMutex);
auto it = m_Pending.find(device_id);
if (it == m_Pending.end()) return false;
bool signaled = m_PendingCv.wait_for(lk, std::chrono::milliseconds(timeoutMs),
[&] { return it->second.done; });
if (!signaled) {
m_Pending.erase(it); // 超时 → 清理
return false;
}
subkeys = std::move(it->second.regPath);
values = std::move(it->second.regKey);
m_Pending.erase(it);
return true;
}
// rand_sWindows CRT 加密安全随机源(基于系统 CSPRNG。其声明需在 <stdlib.h> 前
// 定义 _CRT_RAND_S为避免依赖 PCH 的包含顺序这里手动声明其导出原型errno_t == int
extern "C" int __cdecl rand_s(unsigned int* randomValue);
std::string GenerateRandomToken() {
static const char hex[] = "0123456789abcdef";
std::string out;
out.reserve(32);
for (int i = 0; i < 16; ++i) {
unsigned int v = 0;
if (rand_s(&v) != 0) {
// rand_s 失败(罕见):退化为时间 + 地址熵,保证仍返回非空 token。
v = (unsigned int)(GetTickCount() ^ (ULONG_PTR)&out);
}
out.push_back(hex[(v >> 4) & 0xF]);
out.push_back(hex[v & 0xF]);
}
return out;
}
// ===== P6download_file 文件传输会话(状态机方法)=====
bool CMcpServer::IsFileTransferPending(uint64_t device_id) {
std::lock_guard<std::mutex> lk(m_FileXferMutex);
return m_FileXferSessions.find(device_id) != m_FileXferSessions.end();
}
bool CMcpServer::BeginFileTransferPending(uint64_t device_id, const std::string& tool,
const std::string& localDir, const std::string& remotePath,
bool overwrite) {
// 与一次性挂起请求互斥F2先查对方注册表再查自己的避免跨锁嵌套死锁。
{
std::lock_guard<std::mutex> lk(m_PendingMutex);
if (m_Pending.find(device_id) != m_Pending.end()) return false;
}
std::lock_guard<std::mutex> lk(m_FileXferMutex);
if (m_FileXferSessions.find(device_id) != m_FileXferSessions.end()) return false; // 单设备单传输
FileTransferSession s;
s.tool = tool;
s.localDir = localDir;
s.remotePath = remotePath;
s.overwrite = overwrite;
s.startAt = time(nullptr);
m_FileXferSessions[device_id] = std::move(s);
return true;
}
bool CMcpServer::BeginFileUpload(uint64_t device_id) {
// 与一次性挂起请求互斥F2先查对方注册表再查自己的避免跨锁嵌套死锁。
{
std::lock_guard<std::mutex> lk(m_PendingMutex);
if (m_Pending.find(device_id) != m_Pending.end()) return false;
}
std::lock_guard<std::mutex> lk(m_FileXferMutex);
if (m_FileXferSessions.find(device_id) != m_FileXferSessions.end()) return false; // 单设备单传输
FileTransferSession s;
s.tool = "upload_file";
s.startAt = time(nullptr);
m_FileXferSessions[device_id] = std::move(s);
return true;
}
bool CMcpServer::OnDownloadDriveList(uint64_t device_id, context* fmSubCtx) {
std::string localDir, remotePath;
{
std::lock_guard<std::mutex> lk(m_FileXferMutex);
auto it = m_FileXferSessions.find(device_id);
if (it == m_FileXferSessions.end() || it->second.tool != "download_file")
return false; // 会话已清理 → 调用方 CancelIO 收尾
FileTransferSession& s = it->second;
if (s.fmSubCtx != nullptr) return true; // 已下发过,防重复
s.fmSubCtx = fmSubCtx;
localDir = s.localDir;
remotePath = s.remotePath;
}
// 锁外组包下发Send2Client 异步投递,子链接保持到 ClearFileTransfer 收尾,见 §13 F3
// 布局与 FileManagerDlg::OnTransferV2ToLocal 一致:[cmd][targetDir\0][remotePath\0][\0]
std::vector<BYTE> pkt;
pkt.reserve(1 + localDir.size() + 1 + remotePath.size() + 2);
pkt.push_back((BYTE)CMD_DOWN_FILES_V2);
pkt.insert(pkt.end(), localDir.begin(), localDir.end());
pkt.push_back(0);
pkt.insert(pkt.end(), remotePath.begin(), remotePath.end());
pkt.push_back(0);
pkt.push_back(0);
fmSubCtx->Send2Client(pkt.data(), (ULONG)pkt.size());
Mprintf("[MCP] download_file: 下发 CMD_DOWN_FILES_V2 remote=%s\n", remotePath.c_str());
return true;
}
void CMcpServer::OnFileChunkV2(uint64_t device_id, context* streamSubCtx, const BYTE* buf, ULONG len) {
if (len < (ULONG)sizeof(FileChunkPacketV2)) return;
const FileChunkPacketV2* pkt = (const FileChunkPacketV2*)buf;
if (pkt->nameLength > (uint64_t)(len - sizeof(FileChunkPacketV2))) return; // 畸形包:名字越界
// 与 FileManagerDlg.cpp 落盘同款参数(镜像其 GetPwdHash()/GetHMAC(100))。
std::string hash = GetPwdHash(), hmac = GetHMAC(100);
std::lock_guard<std::mutex> lk(m_FileXferMutex);
auto it = m_FileXferSessions.find(device_id);
if (it == m_FileXferSessions.end()) return;
FileTransferSession& s = it->second;
if (s.tool != "download_file") return;
if (s.done) return; // 已出错/完成,丢弃迟到包
if (s.streamSubCtx == nullptr) s.streamSubCtx = streamSubCtx;
if (s.transferID == 0) {
s.transferID = pkt->transferID;
s.totalFiles = pkt->totalFiles;
}
uint32_t fileIndex = pkt->fileIndex;
std::string fileName((char*)(pkt + 1), pkt->nameLength); // ANSI = localDir + relPath
// 目录项只建目录RecvFileChunkV2 会 mkdir不参与 files 输出(见 §13 F4
// 与文件项一样先做路径穿越校验F5目录项的 filename 同样可能越界(如远程目录名含 ..)。
if (pkt->flags & FFV2_DIRECTORY) {
if (!PathWithinDirA(s.localDir, fileName)) {
s.error = 1001;
s.done = true;
m_FileXferCv.notify_one();
return;
}
s.directoryCount++;
RecvFileChunkV2((char*)buf, len, nullptr, nullptr, hash, hmac, 0);
return;
}
if (pkt->offset == 0) {
// 路径穿越校验F5规范化后必须在 localDir 内。
if (!PathWithinDirA(s.localDir, fileName)) {
s.error = 1001;
s.done = true;
m_FileXferCv.notify_one();
return;
}
// overwrite=false 且同名已存在 → 跳过该文件(丢弃其后续分块,不落盘)。
if (!s.overwrite && GetFileAttributesA(fileName.c_str()) != INVALID_FILE_ATTRIBUTES) {
s.skipIndexes.insert(fileIndex);
FileTransferEntry e;
e.path = fileName;
e.size = pkt->fileSize;
s.skipped.push_back(std::move(e));
return;
}
// 记录文件条目size 已知sha256 待 COMPLETE 包回填)。
FileTransferEntry e;
e.path = fileName;
e.size = pkt->fileSize;
s.fileEntries[fileIndex] = std::move(e);
} else {
if (s.skipIndexes.count(fileIndex)) return; // 已跳过的文件,丢弃后续分块
}
int n = RecvFileChunkV2((char*)buf, len, nullptr, nullptr, hash, hmac, 0);
if (n) {
s.error = n;
s.done = true;
m_FileXferCv.notify_one();
}
}
void CMcpServer::OnFileCompleteV2(uint64_t device_id, const BYTE* buf, ULONG len) {
if (len < (ULONG)sizeof(FileCompletePacketV2)) return;
const FileCompletePacketV2* pkt = (const FileCompletePacketV2*)buf;
std::lock_guard<std::mutex> lk(m_FileXferMutex);
auto it = m_FileXferSessions.find(device_id);
if (it == m_FileXferSessions.end()) return;
FileTransferSession& s = it->second;
if (s.tool != "download_file") return;
if (s.done) return;
uint32_t fileIndex = pkt->fileIndex;
if (s.skipIndexes.count(fileIndex)) {
s.filesDone++; // 跳过的文件:不校验,直接计数
} else {
bool verifyOk = HandleFileCompleteV2((const char*)buf, len, 0);
auto fe = s.fileEntries.find(fileIndex);
if (fe != s.fileEntries.end()) fe->second.sha256 = Sha256Hex(pkt->sha256, 32);
if (!verifyOk) {
s.error = FEV2_HASH_MISMATCH;
s.done = true;
m_FileXferCv.notify_one();
return;
}
s.filesDone++;
}
// 完成判定:目录不发 COMPLETE 包,故 filesDone 达 totalFiles - directoryCount 即完成F4
uint32_t expectedFiles = (s.totalFiles > s.directoryCount) ? (s.totalFiles - s.directoryCount) : 0;
if (s.filesDone >= expectedFiles) {
s.done = true;
m_FileXferCv.notify_one();
}
}
bool CMcpServer::WaitFileTransferDone(uint64_t device_id, int timeoutMs,
std::vector<FileTransferEntry>& files, int& skipped, int& error) {
std::unique_lock<std::mutex> lk(m_FileXferMutex);
auto it = m_FileXferSessions.find(device_id);
if (it == m_FileXferSessions.end()) { error = 1002; return false; }
bool signaled = m_FileXferCv.wait_for(lk, std::chrono::milliseconds(timeoutMs),
[&] { return it->second.done; });
FileTransferSession& s = it->second;
if (!signaled) s.error = 1002; // 超时
error = s.error;
// 复制已见文件(成功=完整文件;失败=供调用方删半成品)。跳过的既有文件不在此列。
for (auto& kv : s.fileEntries) files.push_back(kv.second);
skipped = (int)s.skipped.size();
return signaled && s.error == 0;
}
void CMcpServer::ClearFileTransfer(uint64_t device_id) {
FileTransferSession s;
bool found = false;
{
std::lock_guard<std::mutex> lk(m_FileXferMutex);
auto it = m_FileXferSessions.find(device_id);
if (it != m_FileXferSessions.end()) {
s = std::move(it->second);
m_FileXferSessions.erase(it);
found = true;
}
}
if (!found) return;
// 锁外关闭两条子链接CancelIO 触发客户端断开,停掉仍在飞的流式数据)。
if (s.fmSubCtx) s.fmSubCtx->CancelIO();
if (s.streamSubCtx) s.streamSubCtx->CancelIO();
}