Feature: MCP 远程控制(remote_open/close/keyboard/mouse/clipboard) #4
@@ -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;
|
||||
|
||||
@@ -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_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<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
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -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<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_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 末尾)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user