diff --git a/server/2015Remote/2015RemoteDlg.cpp b/server/2015Remote/2015RemoteDlg.cpp index 2ce018c..c85cc56 100644 --- a/server/2015Remote/2015RemoteDlg.cpp +++ b/server/2015Remote/2015RemoteDlg.cpp @@ -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; diff --git a/server/2015Remote/McpServer.cpp b/server/2015Remote/McpServer.cpp index 601eef7..95d1f9c 100644 --- a/server/2015Remote/McpServer.cpp +++ b/server/2015Remote/McpServer.cpp @@ -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 @@ -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_close(P5:MCP 远程控制,仅 Windows, + // 安全门:McpRemoteControl=1 且 McpReadonly=0;全程审计 + idle 回收) + if (CMcpServer::Instance().IsRemoteControlEnabled() && !CMcpServer::Instance().IsReadonly()) { + { + Json::Value tool(Json::objectValue); + tool["name"] = "remote_open"; + tool["description"] = u8"在指定在线 Windows 主机上建立远程控制会话(隐藏屏幕子连接)并返回 session_id 与物理屏幕分辨率(screen_w/screen_h)。后续 remote_mouse/remote_keyboard 用归一化坐标(0..1)注入输入,用毕须 remote_close。"; + tool["inputSchema"] = BuildRemoteOpenInputSchema(); + tool["outputSchema"] = BuildRemoteOpenOutputSchema(); + tools.append(tool); + } + { + Json::Value tool(Json::objectValue); + tool["name"] = "remote_close"; + tool["description"] = u8"关闭并释放指定远程控制会话(幂等:会话已不存在也返回成功)。"; + tool["inputSchema"] = BuildRemoteCloseInputSchema(); + tool["outputSchema"] = BuildRemoteCloseOutputSchema(); + tools.append(tool); + } + } + 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); } +// ===== P5:MCP 远程控制(remote_open / remote_close)===== + +static const int kScreenCtrlIdleTimeoutSec = 300; // 远程控制 idle 回收超时(秒) + +// 共享前置:校验 McpRemoteControl && !readonly → 解析 id → FindMainContext → 拒绝 LNX/MAC。 +// 成功返回 true 并输出 devId/ctx;失败时 errJson 已写入对应 JSON 错误串。 +static bool ResolveScreenCtrlSessionHost(const Json::Value& id, const Json::Value& args, + CMy2015RemoteDlg* parent, + uint64_t& devId, context*& ctx, std::string& errJson) { + CMcpServer& mcp = CMcpServer::Instance(); + if (!mcp.IsRemoteControlEnabled() || mcp.IsReadonly()) { + errJson = BuildError(id, -32006, + "Remote control is disabled: requires McpRemoteControl=1 and McpReadonly=0"); + return false; + } + + std::string err; + if (!ParseHostIdArg(args, devId, err)) { + errJson = BuildError(id, -32602, err); + return false; + } + + ctx = FindMainContext(parent, devId); + if (!ctx) { + errJson = BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId)); + return false; + } + + CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE); + if (clientType == "LNX" || clientType == "MAC") { + errJson = BuildError(id, -32005, "Remote control is only supported on Windows hosts"); + return false; + } + return true; +} + +// remote_open 的 inputSchema(id 必填、timeout_ms 可选) +Json::Value BuildRemoteOpenInputSchema() { + Json::Value props(Json::objectValue); + Json::Value idProp(Json::objectValue); + idProp["type"] = "string"; + idProp["description"] = u8"主机 id,取 list_online_hosts / search_hosts 返回的 id 字段"; + props["id"] = idProp; + Json::Value timeoutProp(Json::objectValue); + timeoutProp["type"] = "integer"; + timeoutProp["description"] = u8"等待屏幕子连接建立 + 分辨率到达的超时毫秒数(可选,默认 20000,上限 600000)"; + props["timeout_ms"] = timeoutProp; + Json::Value schema(Json::objectValue); + schema["type"] = "object"; + schema["properties"] = props; + Json::Value required(Json::arrayValue); + required.append("id"); + schema["required"] = required; + return schema; +} + +Json::Value BuildRemoteOpenOutputSchema() { + Json::Value props(Json::objectValue); + Json::Value sid(Json::objectValue); + sid["type"] = "string"; + sid["description"] = u8"控制会话 token,后续 remote_mouse / remote_keyboard / remote_close 用"; + props["session_id"] = sid; + Json::Value sw(Json::objectValue); + sw["type"] = "integer"; + sw["description"] = u8"物理屏幕宽度(虚拟桌面像素),用于把归一化坐标 0..1 映射为像素"; + props["screen_w"] = sw; + Json::Value sh(Json::objectValue); + sh["type"] = "integer"; + sh["description"] = u8"物理屏幕高度(虚拟桌面像素)"; + props["screen_h"] = sh; + Json::Value schema(Json::objectValue); + schema["type"] = "object"; + schema["properties"] = props; + Json::Value required(Json::arrayValue); + required.append("session_id"); + required.append("screen_w"); + required.append("screen_h"); + schema["required"] = required; + return schema; +} + +Json::Value BuildRemoteCloseInputSchema() { + Json::Value props(Json::objectValue); + Json::Value idProp(Json::objectValue); + idProp["type"] = "string"; + idProp["description"] = u8"主机 id,取 list_online_hosts / search_hosts 返回的 id 字段"; + props["id"] = idProp; + Json::Value sid(Json::objectValue); + sid["type"] = "string"; + sid["description"] = u8"remote_open 返回的 session_id"; + props["session_id"] = sid; + Json::Value schema(Json::objectValue); + schema["type"] = "object"; + schema["properties"] = props; + Json::Value required(Json::arrayValue); + required.append("id"); + required.append("session_id"); + schema["required"] = required; + return schema; +} + +Json::Value BuildRemoteCloseOutputSchema() { + Json::Value props(Json::objectValue); + Json::Value closed(Json::objectValue); + closed["type"] = "boolean"; + closed["description"] = u8"恒为 true(幂等)"; + props["closed"] = closed; + Json::Value schema(Json::objectValue); + schema["type"] = "object"; + schema["properties"] = props; + Json::Value required(Json::arrayValue); + required.append("closed"); + schema["required"] = required; + return schema; +} + +// tools/call:remote_open(建立隐藏屏幕子连接 + 取物理分辨率) +std::string BuildRemoteOpen(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) { + CMcpServer& mcp = CMcpServer::Instance(); + mcp.SweepIdleScreenCtrl(kScreenCtrlIdleTimeoutSec); + + uint64_t devId = 0; + context* ctx = nullptr; + std::string errJson; + if (!ResolveScreenCtrlSessionHost(id, args, parent, devId, ctx, errJson)) + return errJson; + + // 多显示器门(§7.2 / §12.1):Observe 抓主屏,Act 注入在虚拟桌面空间,两者仅单显示器时 + // 重合。客户端心跳 RES_RESOLUTION = "N:W*H"(N=显示器数);N>1 时归一化坐标会系统性 + // 偏移,阶段一直接拒绝(-32008)。无冒号(老客户端格式)则无法判定,放行不拦。 + { + CString res = ctx->GetAdditionalData(RES_RESOLUTION); + int colon = res.Find(':'); + if (colon > 0) { + CString countStr = res.Left(colon); + int monitorCount = atoi(countStr); + if (monitorCount > 1) + return BuildError(id, -32008, + "Multi-monitor hosts are not supported yet (phase 1): monitor count " + + std::to_string(monitorCount)); + } + } + + // 远程控制复用 Web 的隐藏屏幕子连接(COMMAND_SCREEN_SPY → CScreenSpyDlg → RegisterScreenContext)。 + // 该路径依赖 Web 远程服务已启动(WebSvrPort>0,默认开启);未启动时子连接无法建立/注册。 + if (!WebService().IsRunning()) + return BuildError(id, -32005, + "Remote control requires the Web remote service (WebSvrPort) to be enabled"); + + // 与人类远程桌面观看互斥:已有屏幕子连接(web 观看)→ 拒绝,避免向人类正在看的画面注入。 + if (WebService().HasActiveSession(devId)) + return BuildError(id, -32003, "Device busy: a remote desktop session is already active for this host"); + + int timeoutMs = kMcpToolTimeoutMs; + 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/call:remote_close(关闭控制会话;幂等,session_id 不匹配报错) +std::string BuildRemoteClose(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) { + CMcpServer& mcp = CMcpServer::Instance(); + mcp.SweepIdleScreenCtrl(kScreenCtrlIdleTimeoutSec); + + if (!mcp.IsRemoteControlEnabled() || mcp.IsReadonly()) + return BuildError(id, -32006, "Remote control is disabled: requires McpRemoteControl=1 and McpReadonly=0"); + + uint64_t devId = 0; + std::string err; + if (!ParseHostIdArg(args, devId, err)) + return BuildError(id, -32602, err); + + std::string sessionId = GetStringArg(args, "session_id"); + if (sessionId.empty()) + return BuildError(id, -32602, "Missing required parameter: session_id"); + + int r = mcp.CloseScreenCtrlSession(devId, sessionId); + if (r == 2) + return BuildError(id, -32002, "Remote control session_id mismatch: " + sessionId); + + // 仅当确实关闭了 MCP 会话(r==0)时才关闭隐藏屏幕子连接;r==1(幂等,会话已不存在, + // 如已被 idle 回收)时不得误关人类正在观看的 web 会话(CloseWebRemoteDesktopByClientID + // 会关掉该设备任一 IsWebSession 的对话框)。 + if (r == 0) { + // 关闭隐藏屏幕子连接(幂等):WM_CLOSE → CScreenSpyDlg 析构 → UnregisterScreenContext。 + if (parent) parent->CloseWebRemoteDesktopByClientID(devId); + WebService().ClearWebTriggered(devId); + } + + // 审计:关闭会话(不可关闭)。 + if (parent) { + std::string text = "host " + std::to_string(devId) + " remote-close: session " + sessionId; + parent->PostMessageA(WM_SHOWERRORMSG, + (WPARAM)new CString(ToAnsi(text, 936).c_str()), + (LPARAM)new CString(_TR("MCP远程控制"))); + } + + Json::Value result(Json::objectValue); + Json::Value structuredContent(Json::objectValue); + structuredContent["closed"] = true; + result["structuredContent"] = structuredContent; + + Json::Value content(Json::arrayValue); + Json::Value item(Json::objectValue); + item["type"] = "text"; + item["text"] = std::string(u8"closed"); + content.append(item); + result["content"] = content; + result["isError"] = false; + + return BuildResult(id, result); +} + // 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(); } +// ===== P5:MCP 远程控制(状态机方法)===== + +bool CMcpServer::IsScreenCtrlContext(context* subCtx) { + std::lock_guard 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 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 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 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 toClose; + { + std::lock_guard 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 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 ////////////////////////////////////////////////////////////////////////// diff --git a/server/2015Remote/McpServer.h b/server/2015Remote/McpServer.h index cb15131..928f19c 100644 --- a/server/2015Remote/McpServer.h +++ b/server/2015Remote/McpServer.h @@ -157,6 +157,34 @@ public: // idle 回收:关闭 lastActiveAt 超时且非 busy 的持久会话。返回回收数量。 int SweepIdleTerminals(time_t idleTimeoutSec); + // ===== P5:MCP 远程控制(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 m_TermSessions; // device_id → 会话 std::map 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 m_ScreenCtrlSessions; // device_id → 会话 + std::map m_ScreenCtrlContextToDevice; // subCtx → device_id(OfflineProc 反查) + bool m_readonly = true; std::string m_cmdWhitelist; - bool m_terminalEnabled = false; // 持久终端开关(默认关;要求 m_readonly=false) + bool m_terminalEnabled = false; // 持久终端开关(默认关;要求 m_readonly=false) + bool m_remoteControlEnabled = false; // 远程控制开关(默认关;要求 m_readonly=false) }; // 全局访问器(仿 WebService(),见 WebService.h 末尾) diff --git a/server/2015Remote/McpSettingsDlg.cpp b/server/2015Remote/McpSettingsDlg.cpp index ffe2427..34bd92b 100644 --- a/server/2015Remote/McpSettingsDlg.cpp +++ b/server/2015Remote/McpSettingsDlg.cpp @@ -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); diff --git a/server/2015Remote/McpSettingsDlg.h b/server/2015Remote/McpSettingsDlg.h index c11e8aa..54b5623 100644 --- a/server/2015Remote/McpSettingsDlg.h +++ b/server/2015Remote/McpSettingsDlg.h @@ -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; diff --git a/server/2015Remote/WebService.cpp b/server/2015Remote/WebService.cpp index 8b34f09..55c94d0 100644 --- a/server/2015Remote/WebService.cpp +++ b/server/2015Remote/WebService.cpp @@ -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 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; diff --git a/server/2015Remote/WebService.h b/server/2015Remote/WebService.h index 41b80be..9cc3f22 100644 --- a/server/2015Remote/WebService.h +++ b/server/2015Remote/WebService.h @@ -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);