Feature: MCP 远程控制(remote_open/close/keyboard/mouse/clipboard) #4

Merged
yuanyuanxiang merged 8 commits from feature/mcp-remote-control into main 2026-08-26 12:05:24 +00:00
7 changed files with 469 additions and 4 deletions
Showing only changes of commit 6045baadb8 - Show all commits

View File

@@ -2195,6 +2195,8 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
McpServer().SetCmdWhitelist(THIS_CFG.GetStr("settings", "McpCmdWhitelist", ""));
// 持久终端开关默认关要求只读关McpReadonly=0才生效工具列表/分派双重门控)。
McpServer().SetTerminalEnabled(THIS_CFG.GetInt("settings", "McpTerminal", 0) != 0);
// 远程控制开关默认关要求只读关McpReadonly=0才生效工具列表/分派双重门控)。
McpServer().SetRemoteControlEnabled(THIS_CFG.GetInt("settings", "McpRemoteControl", 0) != 0);
if (!McpServer().Start(mcpBind, mcpPort)) {
Mprintf("McpServer start failed on %s:%d\n", mcpBind.c_str(), mcpPort);
} else {
@@ -5166,6 +5168,11 @@ BOOL CALLBACK CMy2015RemoteDlg::OfflineProc(CONTEXT_OBJECT* ContextObject)
McpServer().OnTerminalClosed(ContextObject);
}
// MCP 远程控制的屏幕子连接断开:同步清理会话,避免悬空 subCtx 被连接池复用误路由。
if (McpServer().IsRunning() && McpServer().IsScreenCtrlContext(ContextObject)) {
McpServer().OnScreenControlClosed(ContextObject);
}
SOCKET nSocket = ContextObject->sClientSocket;
CDialogBase* p = (CDialogBase*)ContextObject->hDlg;

View File

@@ -4,6 +4,8 @@
#include "HostJson.h" // BuildHostJson单台主机序列化公共函数
#include "context.h" // context 接口
#include "2015RemoteDlg.h" // CMy2015RemoteDlg 成员m_HostList/m_cs/m_ClientMap+ VERSION_STR
#include "WebService.h" // 远程控制复用屏幕子连接StartRemoteDesktop / GetScreenContext / GetScreenSize
#include "Server.h" // CONTEXT_OBJECT 定义GetScreenContext 返回 CONTEXT_OBJECT* → context* 上转型)
#include "LangManager.h" // _TR审计日志标题语言映射
#include <sstream>
@@ -1125,6 +1127,14 @@ std::string BuildTerminalOpen(const Json::Value& id, const Json::Value& args, CM
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();
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);
// tools/list
std::string BuildToolsListResult(const Json::Value& id) {
Json::Value result(Json::objectValue);
@@ -1328,6 +1338,27 @@ std::string BuildToolsListResult(const Json::Value& id) {
}
}
// 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);
}
}
result["tools"] = tools;
return BuildResult(id, result);
}
@@ -2533,6 +2564,287 @@ std::string BuildTerminalClose(const Json::Value& id, const Json::Value& args, C
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;
std::string t = GetStringArg(args, "timeout_ms");
if (!t.empty() && IsDigits(t)) {
int v = atoi(t.c_str());
if (v > 0 && v <= 600000) timeoutMs = v;
}
std::string sessionId = GenerateRandomToken();
if (!mcp.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);
}
// tools/call 分派
std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
const Json::Value& id = root["id"];
@@ -2561,6 +2873,8 @@ std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* 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);
return BuildError(id, -32602,
"Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName));
@@ -2842,6 +3156,80 @@ int CMcpServer::SweepIdleTerminals(time_t idleTimeoutSec) {
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);
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.subCtx) m_ScreenCtrlContextToDevice.erase(it->second.subCtx);
m_ScreenCtrlSessions.erase(it);
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 (difftime(now, s.lastActiveAt) > (double)idleTimeoutSec) {
if (s.subCtx) m_ScreenCtrlContextToDevice.erase(s.subCtx);
toClose.push_back(it->first);
it = m_ScreenCtrlSessions.erase(it);
} 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;
m_ScreenCtrlSessions.erase(it->second);
m_ScreenCtrlContextToDevice.erase(it);
}
//////////////////////////////////////////////////////////////////////////
// CMcpServer Implementation
//////////////////////////////////////////////////////////////////////////

View File

@@ -157,6 +157,34 @@ public:
// idle 回收:关闭 lastActiveAt 超时且非 busy 的持久会话。返回回收数量。
int SweepIdleTerminals(time_t idleTimeoutSec);
// ===== P5MCP 远程控制remote_open / remote_close / remote_mouse / remote_keyboard=====
// 屏幕子连接由 WebService::StartRemoteDesktop 建立的隐藏 CScreenSpyDlg 持有(复用 Web
// 路径,见 docs/Mcp_RemoteControl_Design.md §6.3);会话仅记录逻辑状态 + 子连接指针。
// 注入走该子连接COMMAND_SCREEN_CONTROL与终端一样单设备单会话。
void SetRemoteControlEnabled(bool enabled) { m_remoteControlEnabled = enabled; }
bool IsRemoteControlEnabled() const { return m_remoteControlEnabled; }
// 该 host 的屏幕子连接是否被 MCP 远程控制会话持有OfflineProc 反查用)。
bool IsScreenCtrlContext(context* subCtx);
// 登记控制会话单设备单会话sessionId 由调用方生成。false = 该设备已有会话。
bool BeginScreenCtrlOpen(uint64_t device_id, const std::string& sessionId);
// 子连接就绪HasActiveSession && 分辨率到达)后调用:填 subCtx/分辨率并置 started。
// false = 会话已被并发关闭(调用方应放弃本次 open
bool MarkScreenCtrlReady(uint64_t device_id, const std::string& sessionId,
context* subCtx, int screenW, int screenH);
// 关闭控制会话(校验 sessionId擦路由 + 会话)。屏幕子连接的关闭由调用方锁外执行
// CloseWebRemoteDesktopByClientID。返回 0=已关1=不存在幂等2=sessionId 不匹配。
int CloseScreenCtrlSession(uint64_t device_id, const std::string& sessionId);
// idle 回收:关闭 lastActiveAt 超时的控制会话,并锁外关闭其隐藏对话框。返回回收数量。
int SweepIdleScreenCtrl(time_t idleTimeoutSec);
// 屏幕子连接断开OfflineProc 调用):擦除该子连接对应的会话 + 路由(幂等)。
void OnScreenControlClosed(context* subCtx);
// 安全配置(启动时由 CMy2015RemoteDlg 读 THIS_CFG 后设置)。
void SetReadonly(bool readonly) { m_readonly = readonly; }
void SetCmdWhitelist(const std::string& whitelist) { m_cmdWhitelist = whitelist; }
@@ -220,9 +248,23 @@ private:
std::map<uint64_t, TermSession> m_TermSessions; // device_id → 会话
std::map<context*, uint64_t> m_TermContextToDevice; // subCtx → device_id顶部路由
// ===== P5远程控制会话受 m_ScreenCtrlMutex 保护;单设备单会话)=====
struct ScreenCtrlSession {
std::string sessionId; // 会话 token每次 open 独立随机)
context* subCtx = nullptr; // 屏幕子连接上下文(就绪后填;用于注入)
bool started = false; // false=子连接建立中true=已就绪
time_t lastActiveAt = 0; // idle 回收用(秒)
int screenW = 0; // 物理捕获分辨率(来自 TOKEN_BITMAPINFO
int screenH = 0;
};
std::mutex m_ScreenCtrlMutex;
std::map<uint64_t, ScreenCtrlSession> m_ScreenCtrlSessions; // device_id → 会话
std::map<context*, uint64_t> m_ScreenCtrlContextToDevice; // subCtx → device_idOfflineProc 反查)
bool m_readonly = true;
std::string m_cmdWhitelist;
bool m_terminalEnabled = false; // 持久终端开关(默认关;要求 m_readonly=false
bool m_remoteControlEnabled = false; // 远程控制开关(默认关;要求 m_readonly=false
};
// 全局访问器(仿 WebService(),见 WebService.h 末尾)

View File

@@ -106,7 +106,7 @@ INT_PTR CMcpSettingsDlg::DoModal()
{
USES_CONVERSION;
CString title = _TR("MCP设置");
BuildDialogTemplate(m_Template, T2CW(title), 320, 360);
BuildDialogTemplate(m_Template, T2CW(title), 320, 390);
InitModalIndirect((LPCDLGTEMPLATE)m_Template.data());
return CDialog::DoModal();
}
@@ -139,6 +139,9 @@ BOOL CMcpSettingsDlg::OnInitDialog()
m_btnTerminal.Create(_TR("启用持久终端(全命令,无白名单)"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
r0, this, IDC_MCP_TERMINAL);
m_btnRemoteControl.Create(_TR("启用远程控制AI 操控桌面)"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
r0, this, IDC_MCP_REMOTECONTROL);
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,
@@ -160,6 +163,7 @@ BOOL CMcpSettingsDlg::OnInitDialog()
m_editToken.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnReadonly.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnTerminal.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnRemoteControl.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));
@@ -174,6 +178,7 @@ BOOL CMcpSettingsDlg::OnInitDialog()
if (tok.empty()) tok = GenerateRandomToken();
int readonly = THIS_CFG.GetInt("settings", "McpReadonly", 1);
int terminal = THIS_CFG.GetInt("settings", "McpTerminal", 0);
int remoteControl = THIS_CFG.GetInt("settings", "McpRemoteControl", 0);
std::string whitelist = THIS_CFG.GetStr("settings", "McpCmdWhitelist", "");
m_btnEnable.SetCheck(enabled ? BST_CHECKED : BST_UNCHECKED);
@@ -182,6 +187,7 @@ BOOL CMcpSettingsDlg::OnInitDialog()
m_editToken.SetWindowText(CString(tok.c_str()));
m_btnReadonly.SetCheck(readonly ? BST_CHECKED : BST_UNCHECKED);
m_btnTerminal.SetCheck(terminal ? BST_CHECKED : BST_UNCHECKED);
m_btnRemoteControl.SetCheck(remoteControl ? BST_CHECKED : BST_UNCHECKED);
// 白名单存储为逗号分隔,展示为每行一条。
m_editWhitelist.SetWindowText(CString(WhitelistForDisplay(whitelist).c_str()));
@@ -199,6 +205,7 @@ void CMcpSettingsDlg::OnOK()
bool enabled = (m_btnEnable.GetCheck() == BST_CHECKED);
bool readonly = (m_btnReadonly.GetCheck() == BST_CHECKED);
bool terminal = (m_btnTerminal.GetCheck() == BST_CHECKED);
bool remoteControl = (m_btnRemoteControl.GetCheck() == BST_CHECKED);
// 端口校验1-65535
int port = atoi(CT2A(sPort));
@@ -223,14 +230,15 @@ void CMcpSettingsDlg::OnOK()
THIS_CFG.SetStr("settings", "McpToken", token);
THIS_CFG.SetInt("settings", "McpReadonly", readonly ? 1 : 0);
THIS_CFG.SetInt("settings", "McpTerminal", terminal ? 1 : 0);
THIS_CFG.SetInt("settings", "McpRemoteControl", remoteControl ? 1 : 0);
std::string whitelist = CT2A(sWhitelist);
whitelist = NormalizeWhitelist(whitelist);
THIS_CFG.SetStr("settings", "McpCmdWhitelist", whitelist);
// 拆成两段可翻译的单行键,中间用 \r\n 连接(多行键无法在 INI 中表示)
MessageBox(_TR("MCP 设置已保存。") + _T("\r\n") +
_TR("启用/端口/绑定地址/Token/只读/白名单/持久终端的改动需重启程序生效。") + _T("\r\n") +
_TR("持久终端仅在只读模式关闭时生效。"),
_TR("启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制的改动需重启程序生效。") + _T("\r\n") +
_TR("持久终端与远程控制仅在只读模式关闭时生效。"),
_TR("提示"), MB_ICONINFORMATION);
CDialog::OnOK();
@@ -267,6 +275,9 @@ void CMcpSettingsDlg::LayoutControls(int cx, int cy)
m_btnTerminal.MoveWindow(margin, y, cx - margin * 2, 22);
y += 30;
m_btnRemoteControl.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);

View File

@@ -29,6 +29,7 @@ private:
IDC_MCP_READONLY = 1005, // 「只读模式」复选框(默认勾选,禁 exec_command
IDC_MCP_WHITELIST = 1006, // 命令白名单编辑框(多行,逗号/换行分隔,空 = 内置只读前缀)
IDC_MCP_TERMINAL = 1007, // 「启用持久终端」复选框(全命令,无白名单,要求只读关)
IDC_MCP_REMOTECONTROL = 1008, // 「启用远程控制」复选框AI 操控桌面,要求只读关)
};
CButton m_btnEnable;
@@ -36,6 +37,7 @@ private:
CEdit m_editPort, m_editBind, m_editToken;
CButton m_btnReadonly;
CButton m_btnTerminal;
CButton m_btnRemoteControl;
CStatic m_lblWhitelist;
CEdit m_editWhitelist;
CButton m_btnOK, m_btnCancel;

View File

@@ -1670,6 +1670,17 @@ void CWebService::NotifyResolutionChange(uint64_t device_id, int width, int heig
}
}
bool CWebService::GetScreenSize(uint64_t device_id, int& width, int& height) {
width = 0;
height = 0;
std::lock_guard<std::mutex> lock(m_DeviceCacheMutex);
auto it = m_DeviceCache.find(device_id);
if (it == m_DeviceCache.end()) return false;
width = it->second->screen_width;
height = it->second->screen_height;
return width > 0 && height > 0;
}
void CWebService::NotifyAudioState(uint64_t device_id, bool enabled) {
if (m_bStopping) return;

View File

@@ -104,6 +104,10 @@ public:
// Resolution change notification
void NotifyResolutionChange(uint64_t device_id, int width, int height, bool top_down = false, const std::string& client_type = "");
// 读回 NotifyResolutionChange 缓存的物理捕获分辨率(来自 TOKEN_BITMAPINFO
// 尚未收到分辨率时返回 false 并置 w/h 为 0。MCP 远程控制remote_open用它映射归一化坐标。
bool GetScreenSize(uint64_t device_id, int& width, int& height);
// Audio enable/disable notification — pushes current state to all web
// clients watching this device and caches it for newcomers.
void NotifyAudioState(uint64_t device_id, bool enabled);