Feature: Add host management and connection management MCP tools
Expose the online-list TLS column through list_online_hosts, search_hosts and get_host_detail. The value comes from the flag the client reports at login, so old clients always read false. Add list_network_connections and kill_network_connection on top of the connection manager sub-connection protocol. The kill path matches the original 110-byte record instead of rebuilding addresses, supports a dry run, refuses UDP/IPv6 targets, and verifies that the quadruple really disappeared instead of assuming success. Add a host_* family backed by the machine manager protocol: ten read tools covering every tab of the host management dialog, and four recoverable write tools for process freeze/thaw, window restore/hide/ maximize/minimize, service start/stop/pause/continue/restart, and scheduled task run/stop. Every write re-reads the snapshot before sending and re-checks afterwards, reporting -32009 for stale targets rather than acting blindly. The family is gated by McpMachine, the risky subset by McpMachineDangerous, and writes additionally require read-only mode to be off. Route managed sub-connections from MessageHandle and OfflineProc, keep the single-in-flight rule by sharing the pending table with a request kind, and move the shared JSON/lock/encoding helpers into McpInternal so both tool families use one implementation. Settings dialog, design docs and en_US/zh_TW/de_DE translations are updated accordingly. Co-Authored-By: deepseek-flash
This commit is contained in:
1 parent
6c8336b497
commit
38cd207f5d
17 files changed
+3615
-184
No files matched your search
+30
-176
@@ -8,6 +8,8 @@
|
||||
#include "Server.h" // CONTEXT_OBJECT 定义(GetScreenContext 返回 CONTEXT_OBJECT* → context* 上转型)
|
||||
#include "LangManager.h" // _TR(审计日志标题语言映射)
|
||||
#include "common/file_upload.h" // V2 文件传输(RecvFileChunkV2 / HandleFileCompleteV2 / 包结构)
|
||||
#include "McpInternal.h" // 共享助手(McpMachine.cpp 也使用)
|
||||
#include "McpMachine.h" // 主机管理 / 连接管理工具族
|
||||
|
||||
#include <sstream>
|
||||
|
||||
@@ -30,39 +32,15 @@
|
||||
std::string GetPwdHash();
|
||||
std::string GetHMAC(int offset);
|
||||
|
||||
// 共享助手(JsonToString/BuildResult/ParseHostIdArg/FindMainContext/ToUtf8/ToAnsi/...)
|
||||
// 已抽到 McpInternal.cpp;此处引入后调用点保持原样。
|
||||
using namespace McpInternal;
|
||||
|
||||
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);
|
||||
@@ -84,33 +62,6 @@ std::string BuildPingResult(const Json::Value& id) {
|
||||
|
||||
// ========== P2a 通用辅助 ==========
|
||||
|
||||
// 小写化(仅 ASCII,UTF-8 多字节原样保留):用于不区分大小写的子串匹配
|
||||
std::string ToLowerAscii(const std::string& s) {
|
||||
std::string r = s;
|
||||
for (char& c : r) if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
|
||||
return r;
|
||||
}
|
||||
|
||||
// 不区分大小写的子串匹配
|
||||
bool ContainsCI(const std::string& haystack, const std::string& needle) {
|
||||
if (needle.empty()) return true;
|
||||
return ToLowerAscii(haystack).find(ToLowerAscii(needle)) != std::string::npos;
|
||||
}
|
||||
|
||||
// 取对象的字符串字段,缺失/非字符串返回 ""
|
||||
std::string JsonStrField(const Json::Value& v, const char* key) {
|
||||
if (v.isObject() && v.isMember(key) && v[key].isString())
|
||||
return v[key].asString();
|
||||
return "";
|
||||
}
|
||||
|
||||
// 是否纯数字(host id 为 uint64 十进制字符串)
|
||||
bool IsDigits(const std::string& s) {
|
||||
if (s.empty()) return false;
|
||||
for (char c : s) if (c < '0' || c > '9') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 读取 tools/call 的入参(MCP 规范:params.arguments 为工具入参对象)
|
||||
Json::Value GetCallArguments(const Json::Value& params) {
|
||||
if (params.isObject() && params.isMember("arguments") && params["arguments"].isObject())
|
||||
@@ -118,85 +69,8 @@ Json::Value GetCallArguments(const Json::Value& params) {
|
||||
return Json::Value(Json::objectValue);
|
||||
}
|
||||
|
||||
// 读取可选字符串入参,缺失返回 ""
|
||||
std::string GetStringArg(const Json::Value& args, const char* key) {
|
||||
return JsonStrField(args, key);
|
||||
}
|
||||
|
||||
// 读取可选整数入参(JSON number 或数字字符串);缺失/非法返回 false(不写 out)。
|
||||
// 定义见文件后半段(随 P3 终端会话辅助一起),此处前置声明供早于定义的 exec_command /
|
||||
// terminal_exec 等工具复用。
|
||||
static bool GetIntArg(const Json::Value& args, const char* key, int& out);
|
||||
|
||||
// ========== P2b 辅助 ==========
|
||||
|
||||
// 解析 id 入参(必填、纯数字)为 uint64;非法返回 false。
|
||||
bool ParseHostIdArg(const Json::Value& args, uint64_t& out, std::string& err) {
|
||||
std::string sid = GetStringArg(args, "id");
|
||||
if (sid.empty()) {
|
||||
err = "Missing required parameter: id";
|
||||
return false;
|
||||
}
|
||||
if (!IsDigits(sid)) {
|
||||
err = "Invalid id: expected a decimal host id string";
|
||||
return false;
|
||||
}
|
||||
out = strtoull(sid.c_str(), nullptr, 10);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 在 m_HostList 中按 clientID 找在线主 context(复刻 CollectOnlineHosts 的锁内遍历)。
|
||||
context* FindMainContext(CMy2015RemoteDlg* parent, uint64_t id) {
|
||||
if (!parent) return nullptr;
|
||||
context* found = nullptr;
|
||||
EnterCriticalSection(&parent->m_cs);
|
||||
for (context* ctx : parent->m_HostList) {
|
||||
if (ctx && ctx->GetClientID() == id && ctx->IsLogin()) {
|
||||
found = ctx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
LeaveCriticalSection(&parent->m_cs);
|
||||
return found;
|
||||
}
|
||||
|
||||
// 有界 strlen:p 最多可读 avail 字节,返回 n < avail 表示遇到 '\0'。
|
||||
size_t BoundedStrlen(const char* p, size_t avail) {
|
||||
size_t n = 0;
|
||||
while (n < avail && p[n]) ++n;
|
||||
return n;
|
||||
}
|
||||
|
||||
// 从源编码 cp 转为 UTF-8(输出到 JSON)。空/失败返回 ""。
|
||||
std::string ToUtf8(const char* s, UINT cp) {
|
||||
if (!s || !*s) return "";
|
||||
int wlen = MultiByteToWideChar(cp, 0, s, -1, NULL, 0);
|
||||
if (wlen <= 0) return "";
|
||||
std::wstring w(wlen - 1, L'\0');
|
||||
MultiByteToWideChar(cp, 0, s, -1, &w[0], wlen);
|
||||
int u8len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, NULL, 0, NULL, NULL);
|
||||
if (u8len <= 0) return "";
|
||||
std::string out(u8len - 1, '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, &out[0], u8len, NULL, NULL);
|
||||
return out;
|
||||
}
|
||||
|
||||
// 从 UTF-8 转为目标编码 cp(文件链路的目录路径在 Windows 走 ANSI,与 FileManagerDlg 的
|
||||
// CString 一致;cp 按 clientType 判定:Windows=936、LNX/MAC=CP_UTF8,见 BuildListFiles)。
|
||||
// 非 UTF-8(如纯 ASCII)失败时原样返回。
|
||||
std::string ToAnsi(const std::string& utf8, UINT cp) {
|
||||
if (utf8.empty()) return "";
|
||||
int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), (int)utf8.size(), NULL, 0);
|
||||
if (wlen <= 0) return utf8;
|
||||
std::wstring w(wlen, L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), (int)utf8.size(), &w[0], wlen);
|
||||
int alen = WideCharToMultiByte(cp, 0, w.c_str(), wlen, NULL, 0, NULL, NULL);
|
||||
if (alen <= 0) return utf8;
|
||||
std::string out(alen, '\0');
|
||||
WideCharToMultiByte(cp, 0, w.c_str(), wlen, &out[0], alen, NULL, NULL);
|
||||
return out;
|
||||
}
|
||||
|
||||
// 从 "title|status|pid|r1|r2" 解析(自末尾 4 个 '|' 反推),兼容只有标题的老客户端。
|
||||
// 逻辑复刻 SystemDlg.cpp::ParseWindowAttrs。
|
||||
void ParseWindowAttrsStr(const char* s, std::string& title, std::string& status, uint64_t& pid) {
|
||||
@@ -647,6 +521,10 @@ Json::Value BuildHostItemSchema() {
|
||||
Json::Value onlineProp(Json::objectValue);
|
||||
onlineProp["type"] = "boolean";
|
||||
itemProps["online"] = onlineProp;
|
||||
// 客户端登录时上报的 TLS 传输标志(老客户端恒为 false)
|
||||
Json::Value tlsProp(Json::objectValue);
|
||||
tlsProp["type"] = "boolean";
|
||||
itemProps["tls"] = tlsProp;
|
||||
return itemProps;
|
||||
}
|
||||
|
||||
@@ -1311,7 +1189,7 @@ std::string BuildToolsListResult(const Json::Value& id) {
|
||||
tool["name"] = "list_online_hosts";
|
||||
// 说明文字为 UTF-8:项目 /execution-charset:.936 会把普通窄字面量编译成 GBK,
|
||||
// 故用 u8 前缀确保输出到 JSON 的字节是 UTF-8。
|
||||
tool["description"] = u8"获取当前所有在线主机的列表,包含计算机名、IP、操作系统、版本、备注、分组、活动窗口、延迟等实时信息。";
|
||||
tool["description"] = u8"获取当前所有在线主机的列表,包含计算机名、IP、操作系统、版本、备注、分组、活动窗口、延迟等实时信息。tls=true 表示该客户端登录时使用了 TLS 加密传输(登录时上报,非实时探测;老客户端恒为 false)。";
|
||||
|
||||
Json::Value inputSchema(Json::objectValue);
|
||||
inputSchema["type"] = "object";
|
||||
@@ -1328,7 +1206,7 @@ std::string BuildToolsListResult(const Json::Value& id) {
|
||||
{
|
||||
Json::Value tool(Json::objectValue);
|
||||
tool["name"] = "search_hosts";
|
||||
tool["description"] = u8"按计算机名/备注、IP、分组、操作系统过滤在线主机。所有条件均可选、按 AND 组合;子串匹配(ASCII 不区分大小写)。只返回在线主机。";
|
||||
tool["description"] = u8"按计算机名/备注、IP、分组、操作系统过滤在线主机。所有条件均可选、按 AND 组合;子串匹配(ASCII 不区分大小写)。只返回在线主机。tls=true 表示该客户端登录时使用了 TLS 加密传输(登录时上报,老客户端恒为 false)。";
|
||||
|
||||
tool["inputSchema"] = BuildSearchHostsInputSchema();
|
||||
tool["outputSchema"] = BuildHostOutputSchema();
|
||||
@@ -1340,7 +1218,7 @@ std::string BuildToolsListResult(const Json::Value& id) {
|
||||
{
|
||||
Json::Value tool(Json::objectValue);
|
||||
tool["name"] = "get_host_detail";
|
||||
tool["description"] = u8"获取单台在线主机的详细信息(id、计算机名、IP、操作系统、备注、分组、活动窗口、屏幕分辨率、客户端类型等)。";
|
||||
tool["description"] = u8"获取单台在线主机的详细信息(id、计算机名、IP、操作系统、备注、分组、活动窗口、屏幕分辨率、客户端类型等)。tls=true 表示该客户端登录时使用了 TLS 加密传输(登录时上报,老客户端恒为 false)。";
|
||||
|
||||
tool["inputSchema"] = BuildGetHostDetailInputSchema();
|
||||
tool["outputSchema"] = BuildHostDetailOutputSchema();
|
||||
@@ -1607,6 +1485,11 @@ std::string BuildToolsListResult(const Json::Value& id) {
|
||||
}
|
||||
}
|
||||
|
||||
// 19) 连接管理 / 主机管理(P8:list_network_connections / kill_network_connection /
|
||||
// host_* 共 16 个工具)。开关在 McpMachine.cpp 内部按 McpMachine /
|
||||
// McpMachineDangerous / McpReadonly 逐项判断,关闭时不追加任何工具。
|
||||
McpMachine::AppendTools(tools);
|
||||
|
||||
result["tools"] = tools;
|
||||
return BuildResult(id, result);
|
||||
}
|
||||
@@ -3366,21 +3249,6 @@ static bool GetNormCoord(const Json::Value& args, const char* key, double& out,
|
||||
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; }
|
||||
@@ -4351,31 +4219,6 @@ std::string BuildUploadFile(const Json::Value& id, const Json::Value& args, CMy2
|
||||
|
||||
// ========== P7:设备管理控制 ==========
|
||||
|
||||
// 读取 JSON number 或纯数字字符串为 uint64。
|
||||
static bool ReadUInt64(const Json::Value& v, uint64_t& out) {
|
||||
if (v.isNumeric()) { out = v.asUInt64(); return true; }
|
||||
if (v.isString()) {
|
||||
std::string s = v.asString();
|
||||
if (IsDigits(s)) { out = strtoull(s.c_str(), nullptr, 10); return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool ReadUInt64Arg(const Json::Value& args, const char* key, uint64_t& out) {
|
||||
if (!args.isMember(key)) return false;
|
||||
return ReadUInt64(args[key], out);
|
||||
}
|
||||
|
||||
// 写审计(杀进程/窗口/服务/电源共用):PostMessageA 落到服务端消息/审计日志(不可关闭)。
|
||||
static void AuditManageAction(CMy2015RemoteDlg* parent, uint64_t devId,
|
||||
const std::string& tool, const std::string& detail) {
|
||||
if (!parent) return;
|
||||
std::string text = "host " + std::to_string(devId) + " " + tool + ": " + detail;
|
||||
parent->PostMessageA(WM_SHOWERRORMSG,
|
||||
(WPARAM)new CString(ToAnsi(text, 936).c_str()),
|
||||
(LPARAM)new CString(_TR("MCP管理操作")));
|
||||
}
|
||||
|
||||
// kill_process:主连接 COMMAND_SYSTEM → 系统子连接 TOKEN_PSLIST(就绪)→ COMMAND_KILLPROCESS
|
||||
// → 补发 COMMAND_PSLIST → 第二个 TOKEN_PSLIST(确认)。payload 与 SystemDlg::OnPlistKill 一致。
|
||||
std::string BuildKillProcess(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||||
@@ -4687,6 +4530,9 @@ std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
|
||||
|
||||
const Json::Value args = GetCallArguments(params);
|
||||
|
||||
// P8:连接管理 / 主机管理工具族(独立分发;门控在 McpMachine::Call 内复核)。
|
||||
if (McpMachine::Handles(toolName)) return McpMachine::Call(id, toolName, args, parent);
|
||||
|
||||
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);
|
||||
@@ -5225,13 +5071,18 @@ void CMcpServer::HandleMcp(const httplib::Request& req, httplib::Response& res)
|
||||
|
||||
bool CMcpServer::IsPending(uint64_t device_id) {
|
||||
std::lock_guard<std::mutex> lk(m_PendingMutex);
|
||||
return m_Pending.find(device_id) != m_Pending.end();
|
||||
auto it = m_Pending.find(device_id);
|
||||
if (it == m_Pending.end()) return false;
|
||||
// P8:主机管理/连接管理会话的响应包走 OnManagedSubPacket,不归 MFC 的
|
||||
// TOKEN_PSLIST/TOKEN_WSLIST/SERVERLIST/DRIVE_LIST 分支消费(否则会抢走 UI 包)。
|
||||
return it->second.kind == PendingKind::Generic || it->second.kind == PendingKind::FollowUp;
|
||||
}
|
||||
|
||||
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()) { Mprintf("[MCP-P7] TakeMainResponse dev=%llu len=%d: no pending (late/dropped)\n", device_id, (int)len); return; } // 已超时清理 → 迟到数据,丢弃
|
||||
if (it->second.kind != PendingKind::Generic && it->second.kind != PendingKind::FollowUp) return; // P8 会话:不接管
|
||||
Mprintf("[MCP-P7] TakeMainResponse dev=%llu len=%d done\n", device_id, (int)len);
|
||||
it->second.data.assign(data, data + len);
|
||||
it->second.done = true;
|
||||
@@ -5416,6 +5267,7 @@ bool CMcpServer::BeginPendingFollowUp(uint64_t device_id, const std::string& too
|
||||
if (m_Pending.find(device_id) != m_Pending.end()) return false; // 设备忙
|
||||
PendingRequest r;
|
||||
r.tool = tool;
|
||||
r.kind = PendingKind::FollowUp;
|
||||
r.followUp = followUp;
|
||||
m_Pending[device_id] = std::move(r);
|
||||
return true;
|
||||
@@ -5501,6 +5353,8 @@ void CMcpServer::OnManageSubClosed(context* subCtx) {
|
||||
// 扫描而非按 device_id 查找,避免依赖断线后 GetClientID 是否仍有效。
|
||||
for (auto& kv : m_Pending) {
|
||||
if (kv.second.subCtx == subCtx) {
|
||||
// P8 会话由 OnManagedContextClosed 处理(这里若清 subCtx 会让其无法按指针反查设备)。
|
||||
if (kv.second.kind == PendingKind::Machine || kv.second.kind == PendingKind::ConnMgr) continue;
|
||||
Mprintf("[MCP-P7] OnManageSubClosed subCtx=%p tool=%s: sub-conn closed before confirm\n",
|
||||
(void*)subCtx, kv.second.tool.c_str());
|
||||
kv.second.subCtx = nullptr; // 断线后子连接已死/将回收,超时路径不得再 CancelIO
|
||||
|
||||
Reference in new issue
Block a user