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
3 changed files with 321 additions and 31 deletions
Showing only changes of commit 18259b8526 - Show all commits

View File

@@ -1134,9 +1134,12 @@ Json::Value BuildRemoteCloseInputSchema();
Json::Value BuildRemoteCloseOutputSchema();
Json::Value BuildRemoteKeyboardInputSchema();
Json::Value BuildRemoteKeyboardOutputSchema();
Json::Value BuildRemoteMouseInputSchema();
Json::Value BuildRemoteMouseOutputSchema();
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);
std::string BuildRemoteKeyboard(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
std::string BuildRemoteMouse(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
// tools/list
std::string BuildToolsListResult(const Json::Value& id) {
@@ -1368,6 +1371,14 @@ std::string BuildToolsListResult(const Json::Value& id) {
tool["outputSchema"] = BuildRemoteKeyboardOutputSchema();
tools.append(tool);
}
{
Json::Value tool(Json::objectValue);
tool["name"] = "remote_mouse";
tool["description"] = u8"向已建立的远程控制会话注入鼠标事件。action 可选 move / down / up / click / right_click / middle_click / drag / scroll坐标 x/y及 drag 的 x2/y2为归一化 0..1 浮点。button 可选 left/middle/right默认 leftclick 的 clicks 可选 1/2/3默认 1scroll 的 delta 正=向下滚、负=向上滚。";
tool["inputSchema"] = BuildRemoteMouseInputSchema();
tool["outputSchema"] = BuildRemoteMouseOutputSchema();
tools.append(tool);
}
}
result["tools"] = tools;
@@ -3113,6 +3124,290 @@ std::string BuildRemoteKeyboard(const Json::Value& id, const Json::Value& args,
return BuildResult(id, result);
}
// ===== P5remote_mouse鼠标注入move/down/up/click/right_click/middle_click/drag/scroll=====
// 读取归一化坐标0..1 浮点,接受 JSON number 或数字字符串),越界钳到 [0,1]。
static bool GetNormCoord(const Json::Value& args, const char* key, double& out, std::string& err) {
if (!args.isMember(key)) { err = std::string("Missing required parameter: ") + key; return false; }
const Json::Value& v = args[key];
if (v.isNumeric()) {
out = v.asDouble();
} else if (v.isString()) {
const std::string s = v.asString();
if (s.empty()) { err = std::string("Invalid ") + key + ": expected a number in 0..1"; return false; }
char* end = nullptr;
double d = strtod(s.c_str(), &end);
if (end == s.c_str() || *end != '\0') { err = std::string("Invalid ") + key + ": expected a number in 0..1"; return false; }
out = d;
} else {
err = std::string("Invalid ") + key + ": expected a number in 0..1";
return false;
}
if (out != out) { // NaN如字符串 "nan")会绕过钳制并令 (int)(n*screen) 未定义行为
err = std::string("Invalid ") + key + ": not a finite number";
return false;
}
if (out < 0.0) out = 0.0;
if (out > 1.0) out = 1.0;
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; }
if (s == "middle") { out = 1; return true; }
if (s == "right") { out = 2; return true; }
return false;
}
// button → 按下/抬起消息 + 按下 wParam对齐 WebService::HandleMouse
static void ButtonMessages(int button, UINT& downMsg, UINT& upMsg, uint64_t& downWParam) {
if (button == 1) { downMsg = WM_MBUTTONDOWN; upMsg = WM_MBUTTONUP; downWParam = MK_MBUTTON; }
else if (button == 2) { downMsg = WM_RBUTTONDOWN; upMsg = WM_RBUTTONUP; downWParam = MK_RBUTTON; }
else { downMsg = WM_LBUTTONDOWN; upMsg = WM_LBUTTONUP; downWParam = MK_LBUTTON; }
}
Json::Value BuildRemoteMouseInputSchema() {
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 act(Json::objectValue);
act["type"] = "string";
act["description"] = u8"动作move / down / up / click / right_click / middle_click / drag / scroll";
Json::Value actEnum(Json::arrayValue);
actEnum.append("move"); actEnum.append("down"); actEnum.append("up"); actEnum.append("click");
actEnum.append("right_click"); actEnum.append("middle_click"); actEnum.append("drag"); actEnum.append("scroll");
act["enum"] = actEnum;
props["action"] = act;
Json::Value xProp(Json::objectValue);
xProp["type"] = "number";
xProp["description"] = u8"归一化 X 坐标0=屏幕最左1=最右)";
props["x"] = xProp;
Json::Value yProp(Json::objectValue);
yProp["type"] = "number";
yProp["description"] = u8"归一化 Y 坐标0=屏幕最上1=最下)";
props["y"] = yProp;
Json::Value x2Prop(Json::objectValue);
x2Prop["type"] = "number";
x2Prop["description"] = u8"拖拽终点 X仅 drag";
props["x2"] = x2Prop;
Json::Value y2Prop(Json::objectValue);
y2Prop["type"] = "number";
y2Prop["description"] = u8"拖拽终点 Y仅 drag";
props["y2"] = y2Prop;
Json::Value btn(Json::objectValue);
btn["type"] = "string";
btn["description"] = u8"按键left / middle / right默认 left";
Json::Value btnEnum(Json::arrayValue);
btnEnum.append("left"); btnEnum.append("middle"); btnEnum.append("right");
btn["enum"] = btnEnum;
props["button"] = btn;
Json::Value clk(Json::objectValue);
clk["type"] = "integer";
clk["description"] = u8"点击次数 1/2/3仅 click默认 1";
props["clicks"] = clk;
Json::Value del(Json::objectValue);
del["type"] = "integer";
del["description"] = u8"滚动量(仅 scroll正=向下滚,负=向上滚,与 Web 控制台一致;仅垂直滚轮,忽略 button";
props["delta"] = del;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("id");
required.append("session_id");
required.append("action");
required.append("x");
required.append("y");
schema["required"] = required;
return schema;
}
Json::Value BuildRemoteMouseOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
return schema;
}
// tools/callremote_mouse鼠标注入归一化坐标 → 物理像素)
std::string BuildRemoteMouse(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;
std::string sessionId = GetStringArg(args, "session_id");
if (sessionId.empty())
return BuildError(id, -32602, "Missing required parameter: session_id");
std::string action = GetStringArg(args, "action");
if (action.empty())
return BuildError(id, -32602, "Missing required parameter: action");
// ---- 纯参数解析不依赖会话分辨率BeginScreenCtrlAction 之后再无提前返回)----
bool knownAction =
action == "move" || action == "down" || action == "up" || action == "click" ||
action == "right_click" || action == "middle_click" || action == "drag" || action == "scroll";
if (!knownAction)
return BuildError(id, -32602, "Unknown action: " + action);
int button = 0;
std::string buttonStr = GetStringArg(args, "button");
if (!ParseButton(buttonStr, button))
return BuildError(id, -32602, "Invalid button: " + buttonStr + " (expected left/middle/right)");
int clicks = 1;
if (args.isMember("clicks")) {
if (!GetIntArg(args, "clicks", clicks))
return BuildError(id, -32602, "clicks must be an integer 1..3");
if (clicks < 1 || clicks > 3)
return BuildError(id, -32602, "clicks must be 1, 2 or 3");
}
int delta = 0;
if (args.isMember("delta") && !GetIntArg(args, "delta", delta))
return BuildError(id, -32602, "delta must be an integer");
int notches = 0; // scroll 实际注入的滚动档数(= clamp(delta, -10, 10)),审计用
double nx = 0, ny = 0, nx2 = 0, ny2 = 0;
std::string coordErr;
if (!GetNormCoord(args, "x", nx, coordErr)) return BuildError(id, -32602, coordErr);
if (!GetNormCoord(args, "y", ny, coordErr)) return BuildError(id, -32602, coordErr);
if (action == "drag") {
if (!GetNormCoord(args, "x2", nx2, coordErr)) return BuildError(id, -32602, coordErr);
if (!GetNormCoord(args, "y2", ny2, coordErr)) return BuildError(id, -32602, coordErr);
}
// 校验 session_id/busy 并置 busy同时取物理分辨率screenW/H>0 由会话就绪保证)。
context* subCtx = nullptr;
int screenW = 0, screenH = 0;
int r = mcp.BeginScreenCtrlAction(devId, sessionId, subCtx, screenW, screenH);
if (r == 2) return BuildError(id, -32003, "Device busy: another injection is in flight for this session");
if (r == 1) return BuildError(id, -32002, "Remote control session not found, not ready, or closed: " + sessionId);
// 归一化坐标 → 物理像素round + clamp 到 [0, screen-1]
auto toPxX = [&](double n) -> int { int p = (int)(n * screenW + 0.5); return p < 0 ? 0 : (p > screenW - 1 ? screenW - 1 : p); };
auto toPxY = [&](double n) -> int { int p = (int)(n * screenH + 0.5); return p < 0 ? 0 : (p > screenH - 1 ? screenH - 1 : p); };
int px = toPxX(nx), py = toPxY(ny);
int px2 = 0, py2 = 0;
UINT downMsg, upMsg; uint64_t downWParam;
ButtonMessages(button, downMsg, upMsg, downWParam);
// 组装注入批次(按发送顺序;每个 MSG64 由客户端 ProcessCommand 分发到 SendInput
std::vector<MSG64> batch;
if (action == "move") {
batch.push_back(BuildMouseMsg64(px, py, WM_MOUSEMOVE, 0));
} else if (action == "down") {
batch.push_back(BuildMouseMsg64(px, py, downMsg, downWParam));
} else if (action == "up") {
batch.push_back(BuildMouseMsg64(px, py, upMsg, 0));
} else if (action == "click" || action == "right_click" || action == "middle_click") {
int b = (action == "right_click") ? 2 : (action == "middle_click" ? 1 : button);
UINT dMsg, uMsg; uint64_t dW;
ButtonMessages(b, dMsg, uMsg, dW);
for (int i = 0; i < clicks; ++i) {
batch.push_back(BuildMouseMsg64(px, py, dMsg, dW));
batch.push_back(BuildMouseMsg64(px, py, uMsg, 0));
}
} else if (action == "drag") {
px2 = toPxX(nx2); py2 = toPxY(ny2);
batch.push_back(BuildMouseMsg64(px, py, downMsg, downWParam));
batch.push_back(BuildMouseMsg64(px2, py2, WM_MOUSEMOVE, 0));
batch.push_back(BuildMouseMsg64(px2, py2, upMsg, 0));
} else if (action == "scroll") {
// delta 正=向下滚(与 Web 控制台一致WM_MOUSEWHEEL 负值=向下滚,故取反。
// 客户端对 WM_MOUSEWHEEL 会先 SetCursorPos(x,y) 再 MOUSEEVENTF_WHEEL单条消息即可定位+滚动。
notches = delta;
if (notches > 10) notches = 10;
if (notches < -10) notches = -10;
short wheelDelta = (short)(-notches * 120);
batch.push_back(BuildMouseMsg64(px, py, WM_MOUSEWHEEL, MAKEWPARAM(0, wheelDelta)));
}
// 组包 [COMMAND_SCREEN_CONTROL][MSG64*N],经屏幕子连接发送(主连接无效,设计 P2
const int len = (int)(1 + batch.size() * sizeof(MSG64));
std::vector<BYTE> packet((size_t)len);
packet[0] = COMMAND_SCREEN_CONTROL;
memcpy(packet.data() + 1, batch.data(), batch.size() * sizeof(MSG64));
bool ok = subCtx->Send2Client(packet.data(), (ULONG)len) != FALSE;
mcp.EndScreenCtrlAction(devId, sessionId);
if (!ok)
return BuildError(id, -32004, "Failed to send mouse injection");
// 审计(不可关闭;含归一化坐标与换算后的物理像素)
if (parent) {
std::string audit = "host " + std::to_string(devId) + " remote-mouse: session " + sessionId
+ " action=" + action;
char cbuf[96];
sprintf(cbuf, " norm=(%.3f,%.3f) px=(%d,%d)", nx, ny, px, py);
audit += cbuf;
if (action == "drag") {
sprintf(cbuf, " -> norm2=(%.3f,%.3f) px2=(%d,%d)", nx2, ny2, px2, py2);
audit += cbuf;
}
if (action == "scroll")
audit += " notches=" + std::to_string(notches);
if (action == "click" || action == "right_click" || action == "middle_click")
audit += " clicks=" + std::to_string(clicks);
parent->PostMessageA(WM_SHOWERRORMSG,
(WPARAM)new CString(ToAnsi(audit, 936).c_str()),
(LPARAM)new CString(_TR("MCP远程控制")));
}
Json::Value result(Json::objectValue);
result["structuredContent"] = Json::Value(Json::objectValue);
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"ok");
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"];
@@ -3144,6 +3439,7 @@ std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
if (toolName == "remote_open") return BuildRemoteOpen(id, args, parent);
if (toolName == "remote_close") return BuildRemoteClose(id, args, parent);
if (toolName == "remote_keyboard") return BuildRemoteKeyboard(id, args, parent);
if (toolName == "remote_mouse") return BuildRemoteMouse(id, args, parent);
return BuildError(id, -32602,
"Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName));

View File

@@ -799,34 +799,18 @@ void CWebService::HandleMouse(void* ws_ptr, const std::string& msg) {
return;
}
// Build MSG64 structure
MSG64 msg64;
memset(&msg64, 0, sizeof(MSG64));
msg64.pt.x = x;
msg64.pt.y = y;
msg64.lParam = MAKELPARAM(x, y);
msg64.time = GetTickCount();
// Build MSG64 structure(共享 helper见 WebService.h与 MCP remote_mouse 复用)
MSG64 msg64 = BuildMouseMsg64(x, y, 0, 0); // 默认 WM_NULL未知 button 时仍发送(保持原行为)
// Map type and button to Windows message
if (type == "down") {
if (button == 0) {
msg64.message = WM_LBUTTONDOWN;
msg64.wParam = MK_LBUTTON;
} else if (button == 1) {
msg64.message = WM_MBUTTONDOWN;
msg64.wParam = MK_MBUTTON;
} else if (button == 2) {
msg64.message = WM_RBUTTONDOWN;
msg64.wParam = MK_RBUTTON;
}
if (button == 0) { msg64.message = WM_LBUTTONDOWN; msg64.wParam = MK_LBUTTON; }
else if (button == 1) { msg64.message = WM_MBUTTONDOWN; msg64.wParam = MK_MBUTTON; }
else if (button == 2) { msg64.message = WM_RBUTTONDOWN; msg64.wParam = MK_RBUTTON; }
} else if (type == "up") {
if (button == 0) {
msg64.message = WM_LBUTTONUP;
} else if (button == 1) {
msg64.message = WM_MBUTTONUP;
} else if (button == 2) {
msg64.message = WM_RBUTTONUP;
}
if (button == 0) { msg64.message = WM_LBUTTONUP; }
else if (button == 1) { msg64.message = WM_MBUTTONUP; }
else if (button == 2) { msg64.message = WM_RBUTTONUP; }
} else if (type == "move") {
msg64.message = WM_MOUSEMOVE;
} else if (type == "wheel") {
@@ -845,13 +829,8 @@ void CWebService::HandleMouse(void* ws_ptr, const std::string& msg) {
if (clientType != GetClientType(CLIENT_TYPE_MACOS) && clientType != "macOS") {
return; // Skip dblclick for non-macOS clients
}
if (button == 0) {
msg64.message = WM_LBUTTONDBLCLK;
msg64.wParam = MK_LBUTTON;
} else if (button == 2) {
msg64.message = WM_RBUTTONDBLCLK;
msg64.wParam = MK_RBUTTON;
}
if (button == 0) { msg64.message = WM_LBUTTONDBLCLK; msg64.wParam = MK_LBUTTON; }
else if (button == 2) { msg64.message = WM_RBUTTONDBLCLK; msg64.wParam = MK_RBUTTON; }
} else {
return; // Unknown type
}

View File

@@ -48,6 +48,21 @@ inline MSG64 BuildKeyMsg64(int keyCode, bool isDown, bool isSysKey) {
return msg64;
}
// 构造单个鼠标注入 MSG64复用 WebService::HandleMouse 的基座字段逻辑,抽取为共享 helper
// 供 Web 与 MCP 远程控制两处使用,见 docs/Mcp_RemoteControl_Design.md §6.2)。
// x/y 为物理像素坐标已由调用方从归一化坐标映射message/wParam 由调用方按动作设定。
inline MSG64 BuildMouseMsg64(int x, int y, UINT message, uint64_t wParam) {
MSG64 msg64;
memset(&msg64, 0, sizeof(MSG64));
msg64.pt.x = x;
msg64.pt.y = y;
msg64.lParam = MAKELPARAM(x, y);
msg64.time = GetTickCount();
msg64.message = message;
msg64.wParam = wParam;
return msg64;
}
// Web client state
struct WebClient {
std::string token;