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
@@ -2418,6 +2418,11 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
|
||||
McpServer().SetFileTransferEnabled(THIS_CFG.GetInt("settings", "McpFileTransfer", 0) != 0);
|
||||
// P7 管理控制开关:默认关;要求只读关(McpReadonly=0)才生效(工具列表/分派双重门控)。
|
||||
McpServer().SetManageEnabled(THIS_CFG.GetInt("settings", "McpManage", 0) != 0);
|
||||
// P8 主机管理 / 连接管理工具族:总开关默认关(关闭时 16 个工具都不可见)。
|
||||
// 危险子集(进程冻结/解冻、服务停止/重启)在总开关之上再叠一道,默认关。
|
||||
McpServer().SetMachineEnabled(THIS_CFG.GetInt("settings", "McpMachine", 0) != 0);
|
||||
McpServer().SetMachineDangerousEnabled(
|
||||
THIS_CFG.GetInt("settings", "McpMachineDangerous", 0) != 0);
|
||||
if (!McpServer().Start(mcpBind, mcpPort)) {
|
||||
Mprintf("McpServer start failed on %s:%d\n", mcpBind.c_str(), mcpPort);
|
||||
} else {
|
||||
@@ -5516,6 +5521,12 @@ BOOL CALLBACK CMy2015RemoteDlg::OfflineProc(CONTEXT_OBJECT* ContextObject)
|
||||
McpServer().OnManageSubClosed(ContextObject);
|
||||
}
|
||||
|
||||
// MCP P8 主机管理 / 连接管理会话的子连接断开:清路由表、唤醒等待线程。必须在
|
||||
// RemoveFromHostList 之前,否则连接池复用同一 context 指针后新连接的包会被误吞。
|
||||
if (McpServer().IsRunning()) {
|
||||
McpServer().OnManagedContextClosed(ContextObject);
|
||||
}
|
||||
|
||||
SOCKET nSocket = ContextObject->sClientSocket;
|
||||
|
||||
CDialogBase* p = (CDialogBase*)ContextObject->hDlg;
|
||||
@@ -5878,6 +5889,14 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
||||
return;
|
||||
}
|
||||
|
||||
// ===== MCP 主机管理 / 连接管理:子连接路由(P8)=====
|
||||
// 已绑定的子连接上的所有包由 MCP 消费;未绑定的子连接只有就绪包
|
||||
// TOKEN_SYSINFOLIST(61) / TOKEN_CONNECTION_LIST(255) 会被尝试绑定,其余原样交回
|
||||
// 下方 MFC 分发(用户手动打开对话框的行为不变)。无会话在飞时走无锁快速路径。
|
||||
if (McpServer().OnManagedSubPacket(ContextObject, szBuffer, len)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 【L】:主机上下线和授权
|
||||
// 【x】:对话框相关功能
|
||||
switch (cmd) {
|
||||
|
||||
@@ -328,6 +328,8 @@
|
||||
<ClInclude Include="KeyBoardDlg.h" />
|
||||
<ClInclude Include="LangManager.h" />
|
||||
<ClInclude Include="McpServer.h" />
|
||||
<ClInclude Include="McpInternal.h" />
|
||||
<ClInclude Include="McpMachine.h" />
|
||||
<ClInclude Include="McpSettingsDlg.h" />
|
||||
<ClInclude Include="TlsCertificateDlg.h" />
|
||||
<ClInclude Include="NetworkDlg.h" />
|
||||
@@ -397,6 +399,8 @@
|
||||
<ClCompile Include="WebService.cpp" />
|
||||
<ClCompile Include="HostJson.cpp" />
|
||||
<ClCompile Include="McpServer.cpp" />
|
||||
<ClCompile Include="McpInternal.cpp" />
|
||||
<ClCompile Include="McpMachine.cpp" />
|
||||
<ClCompile Include="McpSettingsDlg.cpp" />
|
||||
<ClCompile Include="TlsCertificateDlg.cpp" />
|
||||
<ClCompile Include="..\..\client\MemoryModule.c">
|
||||
|
||||
@@ -89,6 +89,8 @@
|
||||
<ClCompile Include="TriggerSettingsDlg.cpp" />
|
||||
<ClCompile Include="HostJson.cpp" />
|
||||
<ClCompile Include="McpServer.cpp" />
|
||||
<ClCompile Include="McpInternal.cpp" />
|
||||
<ClCompile Include="McpMachine.cpp" />
|
||||
<ClCompile Include="McpSettingsDlg.cpp" />
|
||||
<ClCompile Include="TlsCertificateDlg.cpp" />
|
||||
<ClCompile Include="CClientLog.cpp" />
|
||||
@@ -203,6 +205,8 @@
|
||||
<ClInclude Include="PreviewTipWnd.h" />
|
||||
<ClInclude Include="TriggerSettingsDlg.h" />
|
||||
<ClInclude Include="McpServer.h" />
|
||||
<ClInclude Include="McpInternal.h" />
|
||||
<ClInclude Include="McpMachine.h" />
|
||||
<ClInclude Include="McpSettingsDlg.h" />
|
||||
<ClInclude Include="TlsCertificateDlg.h" />
|
||||
<ClInclude Include="CClientLog.h" />
|
||||
|
||||
@@ -114,5 +114,9 @@ Json::Value BuildHostJson(context* ctx, _ClientList* clientMap) {
|
||||
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
||||
device["clientType"] = AnsiToUtf8(clientType); // e.g. "MAC", "LNX", "EXE"
|
||||
|
||||
// 登录时上报的 TLS 传输标志(RES_TLS=="1" → "Y")。老客户端不上报,取值为 "N"。
|
||||
// 注意这是登录凭据而不是实时探测:连接建立后若配置发生变化,需要客户端重连才会更新。
|
||||
device["tls"] = (ctx->GetClientData(ONLINELIST_TLS) == "Y");
|
||||
|
||||
return device;
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
#include "stdafx.h"
|
||||
#include "McpInternal.h"
|
||||
#include "context.h" // context 接口(GetClientID/IsLogin)
|
||||
#include "2015RemoteDlg.h" // CMy2015RemoteDlg 成员(m_HostList/m_cs)+ WM_SHOWERRORMSG
|
||||
#include "LangManager.h" // _TR
|
||||
|
||||
#ifndef _WIN64
|
||||
#ifdef _DEBUG
|
||||
#pragma comment(lib, "jsoncpp/jsoncppd.lib")
|
||||
#else
|
||||
#pragma comment(lib, "jsoncpp/jsoncpp.lib")
|
||||
#endif
|
||||
#else
|
||||
#ifdef _DEBUG
|
||||
#pragma comment(lib, "jsoncpp/jsoncpp_x64d.lib")
|
||||
#else
|
||||
#pragma comment(lib, "jsoncpp/jsoncpp_x64.lib")
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace McpInternal {
|
||||
|
||||
std::string JsonToString(const Json::Value& v) {
|
||||
Json::StreamWriterBuilder b;
|
||||
b["indentation"] = "";
|
||||
return Json::writeString(b, v);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 "";
|
||||
}
|
||||
|
||||
bool IsDigits(const std::string& s) {
|
||||
if (s.empty()) return false;
|
||||
for (char c : s) if (c < '0' || c > '9') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string GetStringArg(const Json::Value& args, const char* key) {
|
||||
return JsonStrField(args, key);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
bool ReadUInt64Arg(const Json::Value& args, const char* key, uint64_t& out) {
|
||||
if (!args.isMember(key)) return false;
|
||||
return ReadUInt64(args[key], out);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
size_t BoundedStrlen(const char* p, size_t avail) {
|
||||
size_t n = 0;
|
||||
while (n < avail && p[n]) ++n;
|
||||
return n;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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管理操作")));
|
||||
}
|
||||
|
||||
} // namespace McpInternal
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
// MCP 工具实现共享的内部辅助函数。
|
||||
//
|
||||
// 这些函数原先都定义在 McpServer.cpp 的匿名命名空间里;主机管理 / 连接管理
|
||||
// 工具族(McpMachine.cpp)需要复用同一套 JSON、host id 解析与编码转换逻辑,
|
||||
// 故抽出到独立编译单元,由 McpServer.cpp 与 McpMachine.cpp 共同引用。
|
||||
//
|
||||
// 约定:函数语义与实现与抽取前完全一致(McpServer.cpp 通过 using-directive
|
||||
// 引入,调用点不变),不做行为改动。
|
||||
|
||||
#include <Windows.h>
|
||||
#include "jsoncpp/json.h"
|
||||
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
|
||||
class CMy2015RemoteDlg;
|
||||
class context;
|
||||
|
||||
namespace McpInternal {
|
||||
|
||||
// Json::Value → 紧凑 JSON 字符串
|
||||
std::string JsonToString(const Json::Value& v);
|
||||
|
||||
// JSON-RPC 2.0 成功 / 错误响应
|
||||
std::string BuildResult(const Json::Value& id, const Json::Value& result);
|
||||
std::string BuildError(const Json::Value& id, int code, const std::string& msg);
|
||||
|
||||
// 仅 ASCII 小写化;不区分大小写的子串匹配
|
||||
std::string ToLowerAscii(const std::string& s);
|
||||
bool ContainsCI(const std::string& haystack, const std::string& needle);
|
||||
|
||||
// 入参读取
|
||||
std::string JsonStrField(const Json::Value& v, const char* key);
|
||||
bool IsDigits(const std::string& s);
|
||||
std::string GetStringArg(const Json::Value& args, const char* key);
|
||||
bool GetIntArg(const Json::Value& args, const char* key, int& out);
|
||||
bool ReadUInt64(const Json::Value& v, uint64_t& out);
|
||||
bool ReadUInt64Arg(const Json::Value& args, const char* key, uint64_t& out);
|
||||
|
||||
// 解析 id 入参(必填、纯数字)为 uint64;非法返回 false 并写 err。
|
||||
bool ParseHostIdArg(const Json::Value& args, uint64_t& out, std::string& err);
|
||||
|
||||
// 在 m_HostList 中按 clientID 找在线主 context(锁内遍历 m_cs)。
|
||||
context* FindMainContext(CMy2015RemoteDlg* parent, uint64_t id);
|
||||
|
||||
// 有界 strlen:p 最多可读 avail 字节,返回 n < avail 表示遇到 '\0'。
|
||||
size_t BoundedStrlen(const char* p, size_t avail);
|
||||
|
||||
// 编码转换:任意代码页 → UTF-8(输出 JSON 用);UTF-8 → 目标代码页(下发客户端用)。
|
||||
std::string ToUtf8(const char* s, UINT cp);
|
||||
std::string ToAnsi(const std::string& utf8, UINT cp);
|
||||
|
||||
// 写操作审计:PostMessageA(WM_SHOWERRORMSG) 落到服务端消息/审计日志(不可关闭)。
|
||||
void AuditManageAction(CMy2015RemoteDlg* parent, uint64_t devId,
|
||||
const std::string& tool, const std::string& detail);
|
||||
|
||||
} // namespace McpInternal
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
// 主机管理(10 个标签页)与连接管理(连接列表 / 强制断开)MCP 工具族。
|
||||
//
|
||||
// 协议(详见 docs/Mcp_Machine_Design.md):
|
||||
// 主机管理:主连接 COMMAND_MACHINE_MANAGE(162) → 客户端 MachineManager 子连接回
|
||||
// TOKEN_SYSINFOLIST(61) → 发 1 字节 tab(0..9) → 回 TOKEN_MACHINE_*(31..42)。
|
||||
// 连接管理:主连接 COMMAND_CONNECTION(253) → CConnectionsManager 子连接回
|
||||
// TOKEN_CONNECTION_LIST(255) → 发 254+原始 110 字节记录强制断开。
|
||||
//
|
||||
// 会话状态由 CMcpServer 持有(与 m_Pending 共用单飞行表),本模块只负责工具 schema、
|
||||
// 参数校验、组包与解析。
|
||||
|
||||
#include "jsoncpp/json.h"
|
||||
#include <string>
|
||||
|
||||
class CMy2015RemoteDlg;
|
||||
|
||||
namespace McpMachine {
|
||||
|
||||
// 是否由本模块处理(AppendTools 与 Call 必须保持一致)。
|
||||
bool Handles(const std::string& toolName);
|
||||
|
||||
// 按当前开关(McpMachine / McpMachineDangerous / McpReadonly)追加可见工具。
|
||||
void AppendTools(Json::Value& tools);
|
||||
|
||||
// 执行工具;返回完整 JSON-RPC 响应字符串(成功 = BuildResult,失败 = BuildError)。
|
||||
std::string Call(const Json::Value& id, const std::string& toolName,
|
||||
const Json::Value& args, CMy2015RemoteDlg* parent);
|
||||
|
||||
} // namespace McpMachine
|
||||
+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
|
||||
|
||||
@@ -270,6 +270,41 @@ public:
|
||||
// 超时路径不会对已回收(连接池复用)的 context 误 CancelIO。
|
||||
void OnManageSubClosed(context* subCtx);
|
||||
|
||||
// ===== P8:主机管理 / 连接管理(子连接会话)=====
|
||||
// 协议:主连接下发 COMMAND_MACHINE_MANAGE(162) / COMMAND_CONNECTION(253) → 客户端新建
|
||||
// 子连接 → 子连接回就绪包(TOKEN_SYSINFOLIST=61 / TOKEN_CONNECTION_LIST=255)→ 在同一
|
||||
// 子连接上多步收发。会话复用 m_Pending 表(统一「单设备单飞行」),kind 区分工具族。
|
||||
// 见 docs/Mcp_Machine_Design.md。
|
||||
void SetMachineEnabled(bool enabled) { m_machineEnabled = enabled; }
|
||||
bool IsMachineEnabled() const { return m_machineEnabled; }
|
||||
void SetMachineDangerousEnabled(bool enabled) { m_machineDangerousEnabled = enabled; }
|
||||
bool IsMachineDangerousEnabled() const { return m_machineDangerousEnabled; }
|
||||
|
||||
// MessageHandle 顶部钩子(终端拦截之后、switch 之前):true = 本包已由 MCP 消费。
|
||||
// 已绑定的子连接:只保存期望 token 的包与 TOKEN_MACHINE_MSG(42),其余丢弃。
|
||||
// 未绑定的子连接:仅 61/255 可作为就绪包尝试绑定;否则返回 false(MFC 行为不变)。
|
||||
bool OnManagedSubPacket(context* subCtx, const BYTE* data, ULONG len);
|
||||
|
||||
// OfflineProc 钩子:接管的子连接断开 → 置 closed 唤醒等待者(幂等)。
|
||||
void OnManagedContextClosed(context* subCtx);
|
||||
|
||||
// 工具线程:登记会话(false = 该设备已有挂起请求在飞 → 设备忙)。
|
||||
bool BeginMachineSession(uint64_t device_id, const std::string& tool, context* mainCtx);
|
||||
bool BeginConnMgrSession(uint64_t device_id, const std::string& tool, context* mainCtx);
|
||||
|
||||
// 工具线程:发命令前清空上一次响应并登记期望 token。
|
||||
void ArmManagedResponse(uint64_t device_id, BYTE expectedToken);
|
||||
|
||||
// 工具线程:等待子连接就绪包 / 指定 token 的响应(超时由调用方按步给)。
|
||||
enum class ManagedWaitResult { Ok, Timeout, Closed, ClientError };
|
||||
ManagedWaitResult WaitManagedPacket(uint64_t device_id, BYTE token,
|
||||
std::vector<BYTE>& out, context*& subCtxOut,
|
||||
int timeoutMs, std::string& clientError);
|
||||
|
||||
// 工具线程:结束会话;cancelIo = 关闭子连接;markOrphan = 打 5s 墓碑
|
||||
// (吸收迟到的 61/255,避免超时后弹出 MFC 对话框)。
|
||||
void EndManagedSession(uint64_t device_id, bool cancelIo, bool markOrphan);
|
||||
|
||||
// 安全配置(启动时由 CMy2015RemoteDlg 读 THIS_CFG 后设置)。
|
||||
void SetReadonly(bool readonly) { m_readonly = readonly; }
|
||||
void SetCmdWhitelist(const std::string& whitelist) { m_cmdWhitelist = whitelist; }
|
||||
@@ -285,6 +320,15 @@ private:
|
||||
// POST /mcp 处理器:token 校验 + JSON-RPC 分发。
|
||||
void HandleMcp(const httplib::Request& req, httplib::Response& res);
|
||||
|
||||
// 挂起请求类型:主机管理/连接管理会话与既有一次性请求共用 m_Pending 保证单设备单飞行,
|
||||
// 但 MessageHandle 的 P2b/P3/P7 拦截分支(IsPending/TakeMainResponse)必须把这两类排除,
|
||||
// 否则用户手动打开对话框时 UI 包会被 MCP 抢走。
|
||||
enum class PendingKind { Generic, FollowUp, Machine, ConnMgr };
|
||||
static bool KindAcceptsToken(PendingKind kind, BYTE token);
|
||||
|
||||
bool BeginManagedSession(uint64_t device_id, const std::string& tool,
|
||||
PendingKind kind, context* mainCtx);
|
||||
|
||||
httplib::Server m_server;
|
||||
std::thread m_thread;
|
||||
std::atomic<bool> m_running{false};
|
||||
@@ -306,11 +350,43 @@ private:
|
||||
std::vector<BYTE> followUp; // 接管后下发的写命令 payload(首字节 = 写命令)
|
||||
bool followUpSent = false; // false=等子连接就绪(首包);true=已发写命令、等确认包
|
||||
context* subCtx = nullptr; // 接管的子连接(超时/失败 CancelIO 用,防孤儿子连接)
|
||||
// ===== P8:主机管理 / 连接管理会话(kind = Machine / ConnMgr 时有效)=====
|
||||
PendingKind kind = PendingKind::Generic;
|
||||
context* mainCtx = nullptr; // Begin 时抓取的主连接(只作身份校验,不从其收发)
|
||||
std::string peer; // mainCtx->GetPeerName()(老客户端子连接 ID=0 时兜底)
|
||||
BYTE expectedToken = 0; // 期望的响应 token(0 = 未设置)
|
||||
bool hasResponse = false;
|
||||
std::vector<BYTE> response; // 含首字节 token
|
||||
std::string clientError; // TOKEN_MACHINE_MSG(42) 文本(已转 UTF-8)
|
||||
bool closed = false; // 子连接在等待期间断开
|
||||
std::chrono::steady_clock::time_point deadline; // 整个会话的截止时间
|
||||
};
|
||||
std::mutex m_PendingMutex;
|
||||
std::condition_variable m_PendingCv;
|
||||
std::map<uint64_t, PendingRequest> m_Pending;
|
||||
|
||||
// ===== P8:子连接路由(受 m_PendingMutex 保护)=====
|
||||
// 已绑定子连接 → device(该子连接的一切包都由 MCP 消费)。
|
||||
std::map<context*, uint64_t> m_SubCtxToDevice;
|
||||
// CancelIO 之后仍可能到达的包:静默丢弃(避免子连接回收前的尾巴包弹对话框)。
|
||||
std::set<context*> m_AbsorbedSubCtx;
|
||||
// 会话异常结束后 5s 内的迟到就绪包墓碑(device / peer 两个维度)。
|
||||
struct OrphanMark {
|
||||
std::chrono::steady_clock::time_point expiry;
|
||||
PendingKind kind = PendingKind::Generic;
|
||||
};
|
||||
std::map<uint64_t, OrphanMark> m_OrphanByDevice;
|
||||
std::map<std::string, OrphanMark> m_OrphanByPeer;
|
||||
|
||||
// 清理过期墓碑(持 m_PendingMutex 调用)。
|
||||
void PruneOrphanMarksLocked(std::chrono::steady_clock::time_point now);
|
||||
|
||||
// 路由表大小(已绑定 + 已吸收的子连接数)。MessageHandle 每个包都会调用
|
||||
// OnManagedSubPacket,若无会话在飞则用它无锁快速返回,避免音频/屏幕等高频
|
||||
// 子连接逐包争用 m_PendingMutex。只在持锁时更新(store)。
|
||||
std::atomic<int> m_ManagedRoutes{0};
|
||||
void RefreshManagedRoutesLocked();
|
||||
|
||||
// get_screenshot 的 reqId 发生器(16 位,跳过 0)。与 MFC 预览的 m_PreviewReqId 各自独立计数。
|
||||
std::atomic<uint16_t> m_PreviewReqId{1};
|
||||
|
||||
@@ -381,6 +457,8 @@ private:
|
||||
bool m_remoteControlEnabled = false; // 远程控制开关(默认关;要求 m_readonly=false)
|
||||
bool m_fileTransferEnabled = false; // 文件传输开关(默认关;download_file)
|
||||
bool m_manageEnabled = false; // P7 管理控制开关(默认关;要求 m_readonly=false)
|
||||
bool m_machineEnabled = false; // P8 主机管理族总开关(默认关;16 个 host_* 工具)
|
||||
bool m_machineDangerousEnabled = false; // P8 高风险子集(冻结/解冻进程、停止/重启服务)
|
||||
};
|
||||
|
||||
// 全局访问器(仿 WebService(),见 WebService.h 末尾)
|
||||
|
||||
@@ -106,7 +106,7 @@ INT_PTR CMcpSettingsDlg::DoModal()
|
||||
{
|
||||
USES_CONVERSION;
|
||||
CString title = _TR("MCP设置");
|
||||
BuildDialogTemplate(m_Template, T2CW(title), 320, 420);
|
||||
BuildDialogTemplate(m_Template, T2CW(title), 320, 480);
|
||||
InitModalIndirect((LPCDLGTEMPLATE)m_Template.data());
|
||||
return CDialog::DoModal();
|
||||
}
|
||||
@@ -148,6 +148,12 @@ BOOL CMcpSettingsDlg::OnInitDialog()
|
||||
m_btnManage.Create(_TR("启用管理操作(进程/窗口/服务/电源)"),
|
||||
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
|
||||
r0, this, IDC_MCP_MANAGE);
|
||||
m_btnMachine.Create(_TR("启用主机管理(进程/窗口/服务/任务/网络状态)"),
|
||||
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
|
||||
r0, this, IDC_MCP_MACHINE);
|
||||
m_btnMachineDangerous.Create(_TR("启用危险主机操作(冻结/解冻进程、停止/重启服务)"),
|
||||
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
|
||||
r0, this, IDC_MCP_MACHINE_DANGEROUS);
|
||||
m_lblWhitelist.Create(_TR("命令白名单"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1);
|
||||
m_editWhitelist.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP |
|
||||
ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN | WS_VSCROLL,
|
||||
@@ -172,6 +178,8 @@ BOOL CMcpSettingsDlg::OnInitDialog()
|
||||
m_btnRemoteControl.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_btnFileTransfer.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_btnManage.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_btnMachine.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_btnMachineDangerous.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_lblWhitelist.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_editWhitelist.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_btnOK.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
@@ -189,6 +197,8 @@ BOOL CMcpSettingsDlg::OnInitDialog()
|
||||
int remoteControl = THIS_CFG.GetInt("settings", "McpRemoteControl", 0);
|
||||
int fileTransfer = THIS_CFG.GetInt("settings", "McpFileTransfer", 0);
|
||||
int manage = THIS_CFG.GetInt("settings", "McpManage", 0);
|
||||
int machine = THIS_CFG.GetInt("settings", "McpMachine", 0);
|
||||
int machineDangerous = THIS_CFG.GetInt("settings", "McpMachineDangerous", 0);
|
||||
std::string whitelist = THIS_CFG.GetStr("settings", "McpCmdWhitelist", "");
|
||||
|
||||
m_btnEnable.SetCheck(enabled ? BST_CHECKED : BST_UNCHECKED);
|
||||
@@ -200,6 +210,8 @@ BOOL CMcpSettingsDlg::OnInitDialog()
|
||||
m_btnRemoteControl.SetCheck(remoteControl ? BST_CHECKED : BST_UNCHECKED);
|
||||
m_btnFileTransfer.SetCheck(fileTransfer ? BST_CHECKED : BST_UNCHECKED);
|
||||
m_btnManage.SetCheck(manage ? BST_CHECKED : BST_UNCHECKED);
|
||||
m_btnMachine.SetCheck(machine ? BST_CHECKED : BST_UNCHECKED);
|
||||
m_btnMachineDangerous.SetCheck(machineDangerous ? BST_CHECKED : BST_UNCHECKED);
|
||||
// 白名单存储为逗号分隔,展示为每行一条。
|
||||
m_editWhitelist.SetWindowText(CString(WhitelistForDisplay(whitelist).c_str()));
|
||||
|
||||
@@ -220,6 +232,16 @@ void CMcpSettingsDlg::OnOK()
|
||||
bool remoteControl = (m_btnRemoteControl.GetCheck() == BST_CHECKED);
|
||||
bool fileTransfer = (m_btnFileTransfer.GetCheck() == BST_CHECKED);
|
||||
bool manage = (m_btnManage.GetCheck() == BST_CHECKED);
|
||||
bool machine = (m_btnMachine.GetCheck() == BST_CHECKED);
|
||||
bool machineDangerous = (m_btnMachineDangerous.GetCheck() == BST_CHECKED);
|
||||
|
||||
// 危险子集依附于主机管理总开关:没开主机管理就没有工具可放开,直接清除并提示。
|
||||
if (machineDangerous && !machine) {
|
||||
machineDangerous = false;
|
||||
m_btnMachineDangerous.SetCheck(BST_UNCHECKED);
|
||||
MessageBoxL(_TR("危险主机操作依附于主机管理:请先启用主机管理。"), _TR("提示"),
|
||||
MB_ICONWARNING);
|
||||
}
|
||||
|
||||
// 端口校验:1-65535
|
||||
int port = atoi(CT2A(sPort));
|
||||
@@ -247,14 +269,17 @@ void CMcpSettingsDlg::OnOK()
|
||||
THIS_CFG.SetInt("settings", "McpRemoteControl", remoteControl ? 1 : 0);
|
||||
THIS_CFG.SetInt("settings", "McpFileTransfer", fileTransfer ? 1 : 0);
|
||||
THIS_CFG.SetInt("settings", "McpManage", manage ? 1 : 0);
|
||||
THIS_CFG.SetInt("settings", "McpMachine", machine ? 1 : 0);
|
||||
THIS_CFG.SetInt("settings", "McpMachineDangerous", machineDangerous ? 1 : 0);
|
||||
std::string whitelist = CT2A(sWhitelist);
|
||||
whitelist = NormalizeWhitelist(whitelist);
|
||||
THIS_CFG.SetStr("settings", "McpCmdWhitelist", whitelist);
|
||||
|
||||
// 拆成两段可翻译的单行键,中间用 \r\n 连接(多行键无法在 INI 中表示)
|
||||
// 拆成多段可翻译的单行键,中间用 \r\n 连接(多行键无法在 INI 中表示)
|
||||
MessageBox(_TR("MCP 设置已保存。") + _T("\r\n") +
|
||||
_TR("启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。") + _T("\r\n") +
|
||||
_TR("持久终端、远程控制与管理操作仅在只读模式关闭时生效。"),
|
||||
_TR("启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作/主机管理/危险主机操作的改动需重启程序生效。") + _T("\r\n") +
|
||||
_TR("持久终端、远程控制与管理操作仅在只读模式关闭时生效。") + _T("\r\n") +
|
||||
_TR("主机管理的写操作需只读模式关闭;冻结/解冻进程、停止/重启服务还需启用危险主机操作。"),
|
||||
_TR("提示"), MB_ICONINFORMATION);
|
||||
|
||||
CDialog::OnOK();
|
||||
@@ -300,6 +325,12 @@ void CMcpSettingsDlg::LayoutControls(int cx, int cy)
|
||||
m_btnManage.MoveWindow(margin, y, cx - margin * 2, 22);
|
||||
y += 30;
|
||||
|
||||
m_btnMachine.MoveWindow(margin, y, cx - margin * 2, 22);
|
||||
y += 30;
|
||||
|
||||
m_btnMachineDangerous.MoveWindow(margin, y, cx - margin * 2, 22);
|
||||
y += 30;
|
||||
|
||||
const int whitelistH = 90;
|
||||
m_lblWhitelist.MoveWindow(margin, y, labelW, rowH);
|
||||
m_editWhitelist.MoveWindow(margin + labelW, y - 2, cx - margin * 2 - labelW, whitelistH);
|
||||
|
||||
@@ -32,6 +32,8 @@ private:
|
||||
IDC_MCP_REMOTECONTROL = 1008, // 「启用远程控制」复选框(AI 操控桌面,要求只读关)
|
||||
IDC_MCP_FILETRANSFER = 1009, // 「启用文件传输」复选框(download_file,不要求只读关)
|
||||
IDC_MCP_MANAGE = 1010, // 「启用管理操作」复选框(kill/窗口/服务/电源,要求只读关)
|
||||
IDC_MCP_MACHINE = 1011, // 「启用主机管理」复选框(host_* 工具族)
|
||||
IDC_MCP_MACHINE_DANGEROUS = 1012, // 「启用危险主机操作」复选框(冻结/停止/重启)
|
||||
};
|
||||
|
||||
CButton m_btnEnable;
|
||||
@@ -42,6 +44,8 @@ private:
|
||||
CButton m_btnRemoteControl;
|
||||
CButton m_btnFileTransfer;
|
||||
CButton m_btnManage;
|
||||
CButton m_btnMachine;
|
||||
CButton m_btnMachineDangerous;
|
||||
CStatic m_lblWhitelist;
|
||||
CEdit m_editWhitelist;
|
||||
CButton m_btnOK, m_btnCancel;
|
||||
|
||||
@@ -1992,7 +1992,7 @@ MCP
|
||||
端口需为 1-65535 的数字=Port muss eine Zahl zwischen 1 und 65535 sein
|
||||
Token 不能为空=Token darf nicht leer sein
|
||||
MCP 设置已保存。=MCP-Einstellungen gespeichert.
|
||||
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。=Aenderungen an Aktivierung/Port/Bindeadresse/Token/Nur-Lesen/Whitelist/persistentem Terminal/Fernsteuerung/Dateiuebertragung/Verwaltungsoperationen werden nach Neustart wirksam.
|
||||
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作/主机管理/危险主机操作的改动需重启程序生效。=Aenderungen an Aktivierung/Port/Bindeadresse/Token/Nur-Lesen/Whitelist/persistentem Terminal/Fernsteuerung/Dateiuebertragung/Verwaltungsoperationen/Maschinenverwaltung/gefaehrlichen Maschinenoperationen werden nach Neustart wirksam.
|
||||
只读模式(禁命令执行)=Nur-Lesen-Modus (Befehlsausfuehrung deaktiviert)
|
||||
命令白名单=Befehls-Whitelist
|
||||
MCP命令执行=MCP-Befehlsausfuehrung
|
||||
@@ -2006,6 +2006,10 @@ Token=Token
|
||||
启用远程控制(AI 操控桌面)=Fernsteuerung aktivieren (KI steuert den Desktop)
|
||||
启用文件传输(download_file)=Dateiuebertragung aktivieren (download_file)
|
||||
启用管理操作(进程/窗口/服务/电源)=Verwaltungsoperationen aktivieren (Prozess/Fenster/Dienst/Energie)
|
||||
启用主机管理(进程/窗口/服务/任务/网络状态)=Maschinenverwaltung aktivieren (Prozess/Fenster/Dienst/Aufgabe/Netzwerkstatus)
|
||||
启用危险主机操作(冻结/解冻进程、停止/重启服务)=Gefaehrliche Maschinenoperationen aktivieren (Prozess einfrieren/auftauen, Dienst stoppen/neu starten)
|
||||
危险主机操作依附于主机管理:请先启用主机管理。=Gefaehrliche Maschinenoperationen setzen die Maschinenverwaltung voraus. Aktivieren Sie zuerst die Maschinenverwaltung.
|
||||
主机管理的写操作需只读模式关闭;冻结/解冻进程、停止/重启服务还需启用危险主机操作。=Schreiboperationen der Maschinenverwaltung erfordern deaktivierten Nur-Lesen-Modus; Einfrieren/Auftauen von Prozessen und Stoppen/Neustarten von Diensten erfordern zusaetzlich gefaehrliche Maschinenoperationen.
|
||||
第 %d 个钱包地址不合法!=Wallet-Adresse #%d ist ungueltig!
|
||||
%d 新连接\r\n=%d Neue Verbindung\r\n
|
||||
%d 本地连接断开\r\n=%d Lokale Verbindung geschlossen\r\n
|
||||
|
||||
@@ -1997,7 +1997,7 @@ MCP
|
||||
端口需为 1-65535 的数字=Port must be a number between 1 and 65535
|
||||
Token 不能为空=Token cannot be empty
|
||||
MCP 设置已保存。=MCP settings saved.
|
||||
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。=Changes to enable/port/bind address/token/read-only/whitelist/persistent terminal/remote control/file transfer/management operations take effect after restart.
|
||||
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作/主机管理/危险主机操作的改动需重启程序生效。=Changes to enable/port/bind address/token/read-only/whitelist/persistent terminal/remote control/file transfer/management operations/host management/dangerous host operations take effect after restart.
|
||||
只读模式(禁命令执行)=Read-only mode (disable command execution)
|
||||
命令白名单=Command whitelist
|
||||
MCP命令执行=MCP command execution
|
||||
@@ -2011,6 +2011,10 @@ Token=Token
|
||||
启用远程控制(AI 操控桌面)=Enable remote control (AI controls the desktop)
|
||||
启用文件传输(download_file)=Enable file transfer (download_file)
|
||||
启用管理操作(进程/窗口/服务/电源)=Enable management operations (process/window/service/power)
|
||||
启用主机管理(进程/窗口/服务/任务/网络状态)=Enable host management (processes/windows/services/tasks/network state)
|
||||
启用危险主机操作(冻结/解冻进程、停止/重启服务)=Enable dangerous host operations (freeze/thaw process, stop/restart service)
|
||||
危险主机操作依附于主机管理:请先启用主机管理。=Dangerous host operations depend on host management. Enable host management first.
|
||||
主机管理的写操作需只读模式关闭;冻结/解冻进程、停止/重启服务还需启用危险主机操作。=Host management write operations require read-only mode to be off; freezing/thawing processes and stopping/restarting services also require dangerous host operations.
|
||||
第 %d 个钱包地址不合法!=Wallet address #%d is invalid!
|
||||
%d 新连接\r\n=%d New connection\r\n
|
||||
%d 本地连接断开\r\n=%d Local connection closed\r\n
|
||||
|
||||
@@ -1988,7 +1988,7 @@ MCP
|
||||
端口需为 1-65535 的数字=連接埠需為 1-65535 的數字
|
||||
Token 不能为空=Token 不能為空
|
||||
MCP 设置已保存。=MCP 設定已儲存。
|
||||
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。=啟用/連接埠/綁定位址/Token/唯讀/白名單/持久終端/遠端控制/檔案傳輸/管理操作的變更需重新啟動程式後生效。
|
||||
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作/主机管理/危险主机操作的改动需重启程序生效。=啟用/連接埠/綁定位址/Token/唯讀/白名單/持久終端/遠端控制/檔案傳輸/管理操作/主機管理/危險主機操作的變更需重新啟動程式後生效。
|
||||
只读模式(禁命令执行)=唯讀模式(禁命令執行)
|
||||
命令白名单=命令白名單
|
||||
MCP命令执行=MCP命令執行
|
||||
@@ -2002,6 +2002,10 @@ Token=Token
|
||||
启用远程控制(AI 操控桌面)=啟用遠端控制(AI 操控桌面)
|
||||
启用文件传输(download_file)=啟用檔案傳輸(download_file)
|
||||
启用管理操作(进程/窗口/服务/电源)=啟用管理操作(程序/視窗/服務/電源)
|
||||
启用主机管理(进程/窗口/服务/任务/网络状态)=啟用主機管理(處理程序/視窗/服務/排程工作/網路狀態)
|
||||
启用危险主机操作(冻结/解冻进程、停止/重启服务)=啟用危險主機操作(凍結/解凍處理程序、停止/重新啟動服務)
|
||||
危险主机操作依附于主机管理:请先启用主机管理。=危險主機操作依附於主機管理:請先啟用主機管理。
|
||||
主机管理的写操作需只读模式关闭;冻结/解冻进程、停止/重启服务还需启用危险主机操作。=主機管理的寫入操作需關閉唯讀模式;凍結/解凍處理程序、停止/重新啟動服務還需啟用危險主機操作。
|
||||
第 %d 个钱包地址不合法!=第 %d 個錢包位址不合法!
|
||||
%d 新连接\r\n=%d 新連線\r\n
|
||||
%d 本地连接断开\r\n=%d 本機連線斷開\r\n
|
||||
|
||||
Reference in new issue
Block a user