diff --git a/server/2015Remote/McpServer.cpp b/server/2015Remote/McpServer.cpp index 461c45a..a234ea2 100644 --- a/server/2015Remote/McpServer.cpp +++ b/server/2015Remote/McpServer.cpp @@ -1132,8 +1132,11 @@ Json::Value BuildRemoteOpenInputSchema(); Json::Value BuildRemoteOpenOutputSchema(); Json::Value BuildRemoteCloseInputSchema(); Json::Value BuildRemoteCloseOutputSchema(); +Json::Value BuildRemoteKeyboardInputSchema(); +Json::Value BuildRemoteKeyboardOutputSchema(); 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); // tools/list std::string BuildToolsListResult(const Json::Value& id) { @@ -1357,6 +1360,14 @@ std::string BuildToolsListResult(const Json::Value& id) { tool["outputSchema"] = BuildRemoteCloseOutputSchema(); tools.append(tool); } + { + Json::Value tool(Json::objectValue); + tool["name"] = "remote_keyboard"; + tool["description"] = u8"向已建立的远程控制会话注入键盘事件。action 可选 key_down / key_up / key_press / type:前三种需 key(Windows 虚拟键名,如 ENTER/TAB/F5/LEFT/CTRL/ALT/SHIFT/WIN),type 需 text(仅 ASCII 文本;非 ASCII 请用 remote_clipboard + Ctrl+V)。modifiers 可选(CTRL/ALT/SHIFT/WIN)。"; + tool["inputSchema"] = BuildRemoteKeyboardInputSchema(); + tool["outputSchema"] = BuildRemoteKeyboardOutputSchema(); + tools.append(tool); + } } result["tools"] = tools; @@ -2845,6 +2856,263 @@ std::string BuildRemoteClose(const Json::Value& id, const Json::Value& args, CMy return BuildResult(id, result); } +// ===== P5:remote_keyboard(键盘注入,key_down / key_up / key_press / type)===== + +// 键名 → 虚拟键码(对齐 Windows VK_* 去掉前缀,见设计 §4.3)。 +// 支持:单字符 A-Z/0-9、F1-F24、命名键、修饰键。未知返回 0(VK 0 非真实键)。 +static int MapKeyNameToVk(const std::string& nameIn) { + std::string n; + for (char c : nameIn) + n += (c >= 'a' && c <= 'z') ? (char)(c - 'a' + 'A') : c; + + if (n.size() == 1) { + unsigned char c = (unsigned char)n[0]; + if (c >= 'A' && c <= 'Z') return c; // 'A'..'Z' == VK_A..VK_Z + if (c >= '0' && c <= '9') return c; // '0'..'9' == VK_0..VK_9 + return 0; // 标点等请走 type + } + + // F1..F24 + if (n.size() >= 2 && n.size() <= 3 && n[0] == 'F') { + int fnum = atoi(n.c_str() + 1); + if (fnum >= 1 && fnum <= 24) return VK_F1 + (fnum - 1); + return 0; + } + + struct Named { const char* name; int vk; }; + static const Named kNames[] = { + {"ENTER", VK_RETURN}, {"RETURN", VK_RETURN}, {"TAB", VK_TAB}, {"SPACE", VK_SPACE}, + {"ESC", VK_ESCAPE}, {"ESCAPE", VK_ESCAPE}, {"BACKSPACE", VK_BACK}, + {"DELETE", VK_DELETE}, {"DEL", VK_DELETE}, {"INSERT", VK_INSERT}, {"INS", VK_INSERT}, + {"HOME", VK_HOME}, {"END", VK_END}, {"PAGEUP", VK_PRIOR}, {"PRIOR", VK_PRIOR}, + {"PAGEDOWN", VK_NEXT}, {"NEXT", VK_NEXT}, + {"LEFT", VK_LEFT}, {"RIGHT", VK_RIGHT}, {"UP", VK_UP}, {"DOWN", VK_DOWN}, + {"CAPSLOCK", VK_CAPITAL}, {"CAPITAL", VK_CAPITAL}, {"NUMLOCK", VK_NUMLOCK}, + {"SCROLLLOCK", VK_SCROLL}, {"PRINTSCREEN", VK_SNAPSHOT}, {"SNAPSHOT", VK_SNAPSHOT}, + {"PAUSE", VK_PAUSE}, {"APPS", VK_APPS}, {"CONTEXTMENU", VK_APPS}, + {"CTRL", VK_CONTROL}, {"CONTROL", VK_CONTROL}, {"ALT", VK_MENU}, {"MENU", VK_MENU}, + {"SHIFT", VK_SHIFT}, {"WIN", VK_LWIN}, {"WINDOWS", VK_LWIN}, {"CMD", VK_LWIN}, {"SUPER", VK_LWIN}, + }; + for (const Named& e : kNames) + if (n == e.name) return e.vk; + return 0; +} + +// 是否修饰键(modifiers 白名单:CTRL/ALT/SHIFT/WIN) +static bool IsModifierVk(int vk) { + return vk == VK_CONTROL || vk == VK_MENU || vk == VK_SHIFT || vk == VK_LWIN || vk == VK_RWIN; +} + +// 解析可选 modifiers 数组(JSON 字符串数组)→ 修饰键 VK 序列。非法返回 false 并填 err。 +static bool ParseModifiers(const Json::Value& args, std::vector& out, std::string& err) { + out.clear(); + if (!args.isMember("modifiers")) return true; + const Json::Value& mods = args["modifiers"]; + if (mods.isNull()) return true; + if (!mods.isArray()) { err = "modifiers must be an array of strings"; return false; } + for (const auto& m : mods) { + if (out.size() >= 8) { err = "too many modifiers (max 8)"; return false; } + int vk = MapKeyNameToVk(m.asString()); + if (vk == 0 || !IsModifierVk(vk)) { + err = "invalid modifier: " + (m.isString() ? m.asString() : std::string("(non-string)")); + return false; + } + out.push_back(vk); + } + return true; +} + +// 是否纯 ASCII(type 走物理键事件,只能可靠注入 ASCII/ANSI;见设计 §6.4) +static bool IsAscii(const std::string& s) { + for (unsigned char c : s) if (c >= 0x80) return false; + return true; +} + +// type:逐字符 VkKeyScanA 映射为「(Shift↓)字符↓ 字符↑(Shift↑)」,回车/制表符映射为按键。 +// 注:VkKeyScanA 在服务端按服务端线程键盘布局解析字符→虚拟键;仅限 ASCII(0..127)时 +// 各布局的字母/数字/常用标点虚拟键一致,跨布局差异可忽略(非 ASCII 一律走剪贴板)。 +static void BuildTypeBatch(const std::string& text, std::vector& batch) { + for (unsigned char ch : text) { + if (ch == '\r' || ch == '\n') { + batch.push_back(BuildKeyMsg64(VK_RETURN, true, false)); + batch.push_back(BuildKeyMsg64(VK_RETURN, false, false)); + continue; + } + if (ch == '\t') { + batch.push_back(BuildKeyMsg64(VK_TAB, true, false)); + batch.push_back(BuildKeyMsg64(VK_TAB, false, false)); + continue; + } + SHORT scan = VkKeyScanA((char)ch); + if (scan == -1) continue; // 无法映射(控制字符等)→ 跳过 + int vk = scan & 0xFF; + bool needShift = (scan >> 8) & 1; + if (needShift) batch.push_back(BuildKeyMsg64(VK_SHIFT, true, false)); + batch.push_back(BuildKeyMsg64(vk, true, false)); + batch.push_back(BuildKeyMsg64(vk, false, false)); + if (needShift) batch.push_back(BuildKeyMsg64(VK_SHIFT, false, false)); + } +} + +Json::Value BuildRemoteKeyboardInputSchema() { + 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"动作:key_down / key_up / key_press / type"; + props["action"] = act; + + Json::Value key(Json::objectValue); + key["type"] = "string"; + key["description"] = u8"按键(key_down/key_up/key_press 用):Windows 虚拟键名(如 ENTER/TAB/F5/LEFT/CTRL/ALT/SHIFT/WIN)或单个字母/数字"; + props["key"] = key; + + Json::Value text(Json::objectValue); + text["type"] = "string"; + text["description"] = u8"文本(type 用,仅 ASCII;非 ASCII 请走 remote_clipboard + Ctrl+V)"; + props["text"] = text; + + Json::Value mods(Json::objectValue); + mods["type"] = "array"; + Json::Value modItem(Json::objectValue); + modItem["type"] = "string"; + mods["items"] = modItem; + mods["description"] = u8"修饰键(可选):CTRL/ALT/SHIFT/WIN"; + props["modifiers"] = mods; + + 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"); + schema["required"] = required; + return schema; +} + +Json::Value BuildRemoteKeyboardOutputSchema() { + Json::Value props(Json::objectValue); + Json::Value schema(Json::objectValue); + schema["type"] = "object"; + schema["properties"] = props; + return schema; +} + +// tools/call:remote_keyboard(键盘注入;成功返回空对象) +std::string BuildRemoteKeyboard(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"); + + std::string keyName = GetStringArg(args, "key"); + std::string text = GetStringArg(args, "text"); + std::vector mods; + { + std::string mErr; + if (!ParseModifiers(args, mods, mErr)) + return BuildError(id, -32602, mErr); + } + + // 组装注入批次(按发送顺序) + std::vector batch; + if (action == "type") { + if (text.empty()) + return BuildError(id, -32602, "Missing required parameter: text"); + if (!IsAscii(text)) + return BuildError(id, -32602, "text must be ASCII; non-ASCII text requires remote_clipboard + Ctrl+V"); + if (text.size() > 1024) + return BuildError(id, -32602, "text too long (max 1024 bytes); use remote_clipboard + Ctrl+V for long text"); + BuildTypeBatch(text, batch); + } else if (action == "key_down" || action == "key_up" || action == "key_press") { + if (keyName.empty()) + return BuildError(id, -32602, "Missing required parameter: key"); + int keyVk = MapKeyNameToVk(keyName); + if (keyVk == 0) + return BuildError(id, -32602, "Unknown key: " + keyName); + bool doDown = (action != "key_up"); + bool doUp = (action != "key_down"); + if (doDown) { + for (int m : mods) batch.push_back(BuildKeyMsg64(m, true, false)); + batch.push_back(BuildKeyMsg64(keyVk, true, false)); + } + if (doUp) { + batch.push_back(BuildKeyMsg64(keyVk, false, false)); + for (auto it = mods.rbegin(); it != mods.rend(); ++it) + batch.push_back(BuildKeyMsg64(*it, false, false)); + } + } else { + return BuildError(id, -32602, "Unknown action: " + action); + } + + if (batch.empty()) + return BuildError(id, -32602, "No key events generated"); + + // 校验 session_id/busy 并置 busy(注入期间 close/sweep/断线不擦会话,镜像终端 busy 模式)。 + 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); + + // 组包 [COMMAND_SCREEN_CONTROL][MSG64*N],经屏幕子连接发送(主连接无效,设计 P2)。 + const int len = (int)(1 + batch.size() * sizeof(MSG64)); + std::vector 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 keyboard injection"); + + // 审计(不可关闭) + if (parent) { + std::string audit = "host " + std::to_string(devId) + " remote-keyboard: session " + sessionId + + " action=" + action; + if (action == "type") audit += " text=" + text; + else audit += " key=" + keyName; + 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"]; @@ -2875,6 +3143,7 @@ std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* 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); + if (toolName == "remote_keyboard") return BuildRemoteKeyboard(id, args, parent); return BuildError(id, -32602, "Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName)); diff --git a/server/2015Remote/WebService.cpp b/server/2015Remote/WebService.cpp index 55c94d0..b969159 100644 --- a/server/2015Remote/WebService.cpp +++ b/server/2015Remote/WebService.cpp @@ -907,41 +907,8 @@ void CWebService::HandleKey(void* ws_ptr, const std::string& msg) { return; } - // Build MSG64 structure - MSG64 msg64; - memset(&msg64, 0, sizeof(MSG64)); - - // Use WM_SYSKEYDOWN/UP for Alt combinations (same as MFC version) - if (altKey) { - msg64.message = isDown ? WM_SYSKEYDOWN : WM_SYSKEYUP; - } else { - msg64.message = isDown ? WM_KEYDOWN : WM_KEYUP; - } - msg64.wParam = keyCode; - msg64.time = GetTickCount(); - - // Build lParam for keyboard message: - // bits 0-15: repeat count (1) - // bits 16-23: scan code - // bit 24: extended key flag - // bit 29: context code (1 if Alt is pressed) - // bit 30: previous key state (1 for keyup) - // bit 31: transition state (1 for keyup) - UINT scanCode = MapVirtualKey(keyCode, MAPVK_VK_TO_VSC); - - // Extended keys: arrows, insert, delete, home, end, page up/down, numpad enter, etc. - bool isExtended = (keyCode >= VK_PRIOR && keyCode <= VK_DOWN) || // Page Up/Down, End, Home, Arrows - keyCode == VK_INSERT || keyCode == VK_DELETE || - keyCode == VK_NUMLOCK || keyCode == VK_RCONTROL || keyCode == VK_RMENU || - keyCode == VK_APPS; - - LPARAM lParam = 1; // repeat count = 1 - lParam |= (scanCode & 0xFF) << 16; - if (isExtended) lParam |= (1 << 24); - if (altKey) lParam |= (1 << 29); // context code for Alt - if (!isDown) lParam |= (3UL << 30); // bit 30 and 31 set for key up - - msg64.lParam = lParam; + // Build MSG64 structure(共享 helper,见 WebService.h;与 MCP remote_keyboard 复用) + MSG64 msg64 = BuildKeyMsg64(keyCode, isDown, altKey); // Send command to device const int length = sizeof(MSG64) + 1; diff --git a/server/2015Remote/WebService.h b/server/2015Remote/WebService.h index 9cc3f22..2fdeeed 100644 --- a/server/2015Remote/WebService.h +++ b/server/2015Remote/WebService.h @@ -13,11 +13,41 @@ #include #include +#include "common/commands.h" // MSG64 / COMMAND_SCREEN_CONTROL:共享键盘注入 helper(BuildKeyMsg64) + // Forward declarations class context; class CMy2015RemoteDlg; class CONTEXT_OBJECT; +// 构造单个键盘注入 MSG64(复用 WebService::HandleKey 的 lParam 逻辑,抽取为共享 helper, +// 供 Web 与 MCP 远程控制两处使用,避免漂移,见 docs/Mcp_RemoteControl_Design.md §6.2)。 +// keyCode:虚拟键码;isDown:按下/抬起;isSysKey:true 用 WM_SYSKEYDOWN/UP 并置 Alt 上下文位。 +inline MSG64 BuildKeyMsg64(int keyCode, bool isDown, bool isSysKey) { + MSG64 msg64; + memset(&msg64, 0, sizeof(MSG64)); + + if (isSysKey) msg64.message = isDown ? WM_SYSKEYDOWN : WM_SYSKEYUP; + else msg64.message = isDown ? WM_KEYDOWN : WM_KEYUP; + msg64.wParam = (uint64_t)keyCode; + msg64.time = GetTickCount(); + + UINT scanCode = MapVirtualKey(keyCode, MAPVK_VK_TO_VSC); + bool isExtended = (keyCode >= VK_PRIOR && keyCode <= VK_DOWN) || + keyCode == VK_INSERT || keyCode == VK_DELETE || + keyCode == VK_NUMLOCK || keyCode == VK_RCONTROL || keyCode == VK_RMENU || + keyCode == VK_APPS; + + LPARAM lParam = 1; // repeat count = 1 + lParam |= (scanCode & 0xFF) << 16; + if (isExtended) lParam |= (1 << 24); + if (isSysKey) lParam |= (1 << 29); // context code for Alt + if (!isDown) lParam |= (3UL << 30); // bit 30/31 for key up + + msg64.lParam = (uint64_t)lParam; + return msg64; +} + // Web client state struct WebClient { std::string token;