Feature: Add MCP (Model Context Protocol) server integration
Add an optional MCP server (JSON-RPC 2.0 over Streamable HTTP) exposing an online-host listing tool, protected by a Bearer token. Disabled by default; configured via a new "Extensions > MCP Settings" dialog. - McpServer: httplib + JSON-RPC 2.0 dispatch (initialize/ping/tools/list/tools/call) - McpSettingsDlg: runtime-created dialog for enable/port/bind/token - HostJson: extract single-host JSON serialization shared with WebService - FRP: expose MCP port (union with listening/Web ports) when bound to 0.0.0.0 - i18n: en_US / zh_TW translations Co-Authored-By: deepseek-v4-pro
This commit is contained in:
@@ -43,6 +43,8 @@
|
||||
#include <sstream>
|
||||
#include "common/skCrypter.h"
|
||||
#include "common/commands.h"
|
||||
#include "McpServer.h"
|
||||
#include "McpSettingsDlg.h"
|
||||
#include "common/md5.h"
|
||||
#include <algorithm>
|
||||
#include <set>
|
||||
@@ -976,6 +978,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
|
||||
ON_COMMAND(ID_SHELLCODE_TEST_AES_BIN, &CMy2015RemoteDlg::OnShellcodeTestAesBin)
|
||||
ON_COMMAND(ID_TOOL_RELOAD_PLUGINS, &CMy2015RemoteDlg::OnToolReloadPlugins)
|
||||
ON_COMMAND(ID_TOOL_PLUGIN_SETTINGS, &CMy2015RemoteDlg::OnToolPluginSettings)
|
||||
ON_COMMAND(ID_MCP_SETTINGS, &CMy2015RemoteDlg::OnMcpSettings)
|
||||
ON_COMMAND(ID_TRIGGER_SETTINGS, &CMy2015RemoteDlg::OnTriggerSettings)
|
||||
ON_COMMAND(ID_SHELLCODE_AES_C_ARRAY, &CMy2015RemoteDlg::OnShellcodeAesCArray)
|
||||
ON_COMMAND(ID_PARAM_KBLOGGER, &CMy2015RemoteDlg::OnParamKblogger)
|
||||
@@ -1217,13 +1220,63 @@ VOID CMy2015RemoteDlg::CreateSolidMenu()
|
||||
pExtMenu->DeleteMenu(ID_BACKUP_DATA, MF_BYCOMMAND);
|
||||
if (SHOULD_HIDE_MENU(HIDE_MENU_IMPORT_DATA, MF_IMPORT_DATA))
|
||||
pExtMenu->DeleteMenu(ID_IMPORT_DATA, MF_BYCOMMAND);
|
||||
if (SHOULD_HIDE_MENU(HIDE_MENU_RELOAD_PLUGINS, MF_RELOAD_PLUGINS))
|
||||
pExtMenu->DeleteMenu(ID_TOOL_RELOAD_PLUGINS, MF_BYCOMMAND);
|
||||
if (SHOULD_HIDE_MENU(HIDE_MENU_PLUGIN_REQUEST, MF_PLUGIN_REQUEST))
|
||||
pExtMenu->DeleteMenu(ID_PLUGIN_REQUEST, MF_BYCOMMAND);
|
||||
if (SHOULD_HIDE_MENU(HIDE_MENU_FRPS_FOR_SUB, MF_FRPS_FOR_SUB))
|
||||
pExtMenu->DeleteMenu(ID_FRPS_FOR_SUB, MF_BYCOMMAND);
|
||||
|
||||
// 动态插入「MCP设置」菜单项(2015Remote.rc 为 UTF-16 不便编辑,改运行时插入,
|
||||
// 位于「地理信息」之后、「插件设置」之前,见 docs/Mcp_Design.md §3.8.1)
|
||||
if (!SHOULD_HIDE_MENU(HIDE_MENU_MCP_SETTINGS, MF_MCP_SETTINGS)) {
|
||||
UINT mcpFlags = MF_BYCOMMAND;
|
||||
if (THIS_CFG.GetInt("settings", "McpEnabled", 0)) {
|
||||
mcpFlags |= MF_CHECKED; // 已启用 MCP 则菜单打勾
|
||||
}
|
||||
pExtMenu->InsertMenu(ID_TOOL_PLUGIN_SETTINGS, mcpFlags,
|
||||
ID_MCP_SETTINGS, _TR("MCP设置(&M)..."));
|
||||
}
|
||||
|
||||
// 将 3 个插件相关菜单项收拢到「插件」二级菜单(位于「MCP设置」之下、原「插件设置」之前,
|
||||
// 顺序不变:插件设置 / 刷新插件 / 获取插件)。
|
||||
// 注意:先删除顶层项、再按位置插入子菜单,避免同一命令 ID 同时存在于子菜单与顶层时,
|
||||
// MF_BYCOMMAND 的 DeleteMenu/InsertMenu 因重复 ID 而误删/误定位子菜单项。
|
||||
{
|
||||
// 记录「插件设置」的索引,作为「插件」子菜单的插入点
|
||||
int pluginPos = -1;
|
||||
for (UINT i = 0; i < pExtMenu->GetMenuItemCount(); i++) {
|
||||
if (pExtMenu->GetMenuItemID(i) == ID_TOOL_PLUGIN_SETTINGS) {
|
||||
pluginPos = (int)i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (pluginPos >= 0) {
|
||||
// 先删除 3 个顶层项
|
||||
pExtMenu->DeleteMenu(ID_TOOL_PLUGIN_SETTINGS, MF_BYCOMMAND);
|
||||
pExtMenu->DeleteMenu(ID_TOOL_RELOAD_PLUGINS, MF_BYCOMMAND);
|
||||
pExtMenu->DeleteMenu(ID_PLUGIN_REQUEST, MF_BYCOMMAND);
|
||||
|
||||
// 再创建「插件」子菜单
|
||||
CMenu pluginMenu;
|
||||
pluginMenu.CreatePopupMenu();
|
||||
pluginMenu.AppendMenu(MF_STRING, ID_TOOL_PLUGIN_SETTINGS, _TR("插件设置(&S)"));
|
||||
if (!SHOULD_HIDE_MENU(HIDE_MENU_RELOAD_PLUGINS, MF_RELOAD_PLUGINS))
|
||||
pluginMenu.AppendMenu(MF_STRING, ID_TOOL_RELOAD_PLUGINS, _TR("刷新插件(&P)"));
|
||||
if (!SHOULD_HIDE_MENU(HIDE_MENU_PLUGIN_REQUEST, MF_PLUGIN_REQUEST))
|
||||
pluginMenu.AppendMenu(MF_STRING, ID_PLUGIN_REQUEST, _TR("获取插件(&P)"));
|
||||
|
||||
// 继承原 3 项的菜单图标(见上方 SetMenuItemBitmaps 区)
|
||||
pluginMenu.SetMenuItemBitmaps(ID_TOOL_PLUGIN_SETTINGS, MF_BYCOMMAND, &m_bmOnline[53], &m_bmOnline[53]);
|
||||
if (!SHOULD_HIDE_MENU(HIDE_MENU_RELOAD_PLUGINS, MF_RELOAD_PLUGINS))
|
||||
pluginMenu.SetMenuItemBitmaps(ID_TOOL_RELOAD_PLUGINS, MF_BYCOMMAND, &m_bmOnline[43], &m_bmOnline[43]);
|
||||
if (!SHOULD_HIDE_MENU(HIDE_MENU_PLUGIN_REQUEST, MF_PLUGIN_REQUEST))
|
||||
pluginMenu.SetMenuItemBitmaps(ID_PLUGIN_REQUEST, MF_BYCOMMAND, &m_bmOnline[44], &m_bmOnline[44]);
|
||||
|
||||
// 插入到原「插件设置」位置(MF_BYPOSITION)
|
||||
pExtMenu->InsertMenu((UINT)pluginPos, MF_BYPOSITION | MF_POPUP,
|
||||
(UINT_PTR)pluginMenu.GetSafeHmenu(), _TR("插件(&P)"));
|
||||
pluginMenu.Detach(); // HMENU 所有权转交 pExtMenu,避免局部析构销毁子菜单
|
||||
}
|
||||
}
|
||||
|
||||
// --- 扩展菜单 - 语言设置子菜单 ---
|
||||
CMenu* pLangMenu = FindSubMenuByCommand(pExtMenu, ID_CHANGE_LANG);
|
||||
if (pLangMenu) {
|
||||
@@ -2080,6 +2133,34 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
|
||||
}
|
||||
}
|
||||
|
||||
// Start MCP service(默认禁用;经「扩展 → MCP设置」开启后监听,见 docs/Mcp_Design.md §3.7)
|
||||
if (THIS_CFG.GetInt("settings", "McpEnabled", 0)) {
|
||||
auto mcpPort = THIS_CFG.GetInt("settings", "McpPort", 6544);
|
||||
auto mcpBind = THIS_CFG.GetStr("settings", "McpBind", "127.0.0.1");
|
||||
|
||||
// token 优先顺序:env BRAND_MCP_ENV_VAR → THIS_CFG McpToken → 皆空则随机(仅本次进程有效)
|
||||
const char* envTok = getenv(BRAND_MCP_ENV_VAR);
|
||||
std::string mcpToken;
|
||||
if (envTok && *envTok) {
|
||||
mcpToken = envTok;
|
||||
} else {
|
||||
mcpToken = THIS_CFG.GetStr("settings", "McpToken", "");
|
||||
}
|
||||
if (mcpToken.empty()) {
|
||||
mcpToken = GenerateRandomToken();
|
||||
Mprintf("[McpServer] %s / McpToken 均未设置,本次进程随机 token:%s\n",
|
||||
BRAND_MCP_ENV_VAR, mcpToken.c_str());
|
||||
}
|
||||
|
||||
McpServer().SetParentDlg(this);
|
||||
McpServer().SetToken(mcpToken);
|
||||
if (!McpServer().Start(mcpBind, mcpPort)) {
|
||||
Mprintf("McpServer start failed on %s:%d\n", mcpBind.c_str(), mcpPort);
|
||||
} else {
|
||||
Mprintf("McpServer started on %s:%d\n", mcpBind.c_str(), mcpPort);
|
||||
}
|
||||
}
|
||||
|
||||
UPDATE_SPLASH(18, "正在初始化文件上传模块...");
|
||||
int ret = InitFileUpload({}, {}, GetHMAC(), 64, 50, Logf, GetUpperHash());
|
||||
g_hKeyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, AfxGetInstanceHandle(), 0);
|
||||
@@ -3185,6 +3266,12 @@ void CMy2015RemoteDlg::ApplyFrpSettings()
|
||||
auto arr = StringToVector(ports, ';');
|
||||
int fileServerPort = THIS_CFG.GetInt("settings", "WebSvrPort", 8080);
|
||||
|
||||
// MCP 端口:仅当「启用 MCP 且绑定 0.0.0.0」时纳入 FRP 暴露(与监听/Web 取并集,不覆盖)。
|
||||
int mcpEnabled = THIS_CFG.GetInt("settings", "McpEnabled", 0);
|
||||
std::string mcpBind = THIS_CFG.GetStr("settings", "McpBind", "127.0.0.1");
|
||||
int mcpPort = THIS_CFG.GetInt("settings", "McpPort", 6544);
|
||||
bool exposeMcp = (mcpEnabled != 0) && (mcpBind == "0.0.0.0") && (mcpPort > 0);
|
||||
|
||||
// 为每个服务端生成独立配置文件 (index=0 用 frpc.ini 保持兼容)
|
||||
for (size_t idx = 0; idx < servers.size(); ++idx) {
|
||||
std::string path = (idx == 0) ? basePath : baseNoExt + "." + std::to_string(idx) + ".ini";
|
||||
@@ -3214,6 +3301,14 @@ void CMy2015RemoteDlg::ApplyFrpSettings()
|
||||
cfg.SetInt(name, "remote_port", fileServerPort);
|
||||
cfg.SetStr(name, "proxy_protocol_version", "v2"); // 传递真实客户端 IP
|
||||
}
|
||||
if (exposeMcp) {
|
||||
std::string name = BRAND_NET_PREFIX "-MCP-" + std::to_string(mcpPort);
|
||||
cfg.SetStr(name, "type", "tcp");
|
||||
cfg.SetInt(name, "local_port", mcpPort);
|
||||
cfg.SetInt(name, "remote_port", mcpPort);
|
||||
// 不设置 proxy_protocol_version:MCP 由 httplib 提供、不解析 Proxy Protocol,
|
||||
// 且 MCP 以 Bearer token 鉴权、无需透传真实 IP(监听/Web 端口才需要 PP2)。
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3805,6 +3900,12 @@ void CMy2015RemoteDlg::Release()
|
||||
WebService().Stop();
|
||||
}
|
||||
|
||||
// Stop MCP service
|
||||
if (McpServer().IsRunning()) {
|
||||
Mprintf("Stopping McpServer...\n");
|
||||
McpServer().Stop();
|
||||
}
|
||||
|
||||
UninitFileUpload();
|
||||
DeletePopupWindow(TRUE);
|
||||
CloseAllLoopTips(); // 关闭所有"播放快照"循环窗口
|
||||
@@ -10756,6 +10857,16 @@ void CMy2015RemoteDlg::OnToolPluginSettings()
|
||||
dlg.DoModal();
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnMcpSettings()
|
||||
{
|
||||
CMcpSettingsDlg dlg(this);
|
||||
if (dlg.DoModal() != IDOK) return;
|
||||
|
||||
// 保存后重生成 frpc.ini:若已启用 MCP 且绑定 0.0.0.0,则把 MCP 端口纳入 FRP 暴露列表
|
||||
// (与监听/Web 端口取并集,互不覆盖)。MCP 生效仍需重启,此处仅为下次启动准备配置。
|
||||
ApplyFrpSettings();
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnTriggerSettings()
|
||||
{
|
||||
if (m_DllList.empty()) {
|
||||
|
||||
@@ -638,6 +638,7 @@ public:
|
||||
afx_msg void OnMasterTrail();
|
||||
afx_msg void OnCancelShare();
|
||||
afx_msg void OnWebRemoteControl();
|
||||
afx_msg void OnMcpSettings();
|
||||
afx_msg void OnProxyPortAutorun();
|
||||
afx_msg void OnScreenpreviewLoop();
|
||||
afx_msg void OnMenuCompress();
|
||||
|
||||
@@ -317,6 +317,7 @@
|
||||
<ClInclude Include="file\CFileTransferModeDlg.h" />
|
||||
<ClInclude Include="HideScreenSpyDlg.h" />
|
||||
<ClInclude Include="HostInfo.h" />
|
||||
<ClInclude Include="HostJson.h" />
|
||||
<ClInclude Include="InputDlg.h" />
|
||||
<ClInclude Include="ZstaPickerDlg.h" />
|
||||
<ClInclude Include="IOCPKCPServer.h" />
|
||||
@@ -325,6 +326,8 @@
|
||||
<ClInclude Include="IPHistoryDlg.h" />
|
||||
<ClInclude Include="KeyBoardDlg.h" />
|
||||
<ClInclude Include="LangManager.h" />
|
||||
<ClInclude Include="McpServer.h" />
|
||||
<ClInclude Include="McpSettingsDlg.h" />
|
||||
<ClInclude Include="NetworkDlg.h" />
|
||||
<ClInclude Include="NotifyConfig.h" />
|
||||
<ClInclude Include="NotifyManager.h" />
|
||||
@@ -387,6 +390,9 @@
|
||||
<ClCompile Include="PreviewTipWnd.cpp" />
|
||||
<ClCompile Include="TriggerSettingsDlg.cpp" />
|
||||
<ClCompile Include="WebService.cpp" />
|
||||
<ClCompile Include="HostJson.cpp" />
|
||||
<ClCompile Include="McpServer.cpp" />
|
||||
<ClCompile Include="McpSettingsDlg.cpp" />
|
||||
<ClCompile Include="..\..\client\MemoryModule.c">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
|
||||
@@ -84,6 +84,9 @@
|
||||
<ClCompile Include="PluginSettingsDlg.cpp" />
|
||||
<ClCompile Include="PreviewTipWnd.cpp" />
|
||||
<ClCompile Include="TriggerSettingsDlg.cpp" />
|
||||
<ClCompile Include="HostJson.cpp" />
|
||||
<ClCompile Include="McpServer.cpp" />
|
||||
<ClCompile Include="McpSettingsDlg.cpp" />
|
||||
<ClCompile Include="CClientLog.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
@@ -164,6 +167,7 @@
|
||||
<ClInclude Include="CDlgFileSend.h" />
|
||||
<ClInclude Include="..\..\client\reg_startup.h" />
|
||||
<ClInclude Include="HostInfo.h" />
|
||||
<ClInclude Include="HostJson.h" />
|
||||
<ClInclude Include="CClientListDlg.h" />
|
||||
<ClInclude Include="context.h" />
|
||||
<ClInclude Include="CUpdateDlg.h" />
|
||||
@@ -189,6 +193,8 @@
|
||||
<ClInclude Include="PluginSettingsDlg.h" />
|
||||
<ClInclude Include="PreviewTipWnd.h" />
|
||||
<ClInclude Include="TriggerSettingsDlg.h" />
|
||||
<ClInclude Include="McpServer.h" />
|
||||
<ClInclude Include="McpSettingsDlg.h" />
|
||||
<ClInclude Include="CClientLog.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -88,7 +88,9 @@ typedef struct FeatureFlags {
|
||||
#define MF_MASTER_TRAIL (1ULL << 41) // HIDE_MENU_MASTER_TRAIL
|
||||
#define MF_REQUEST_AUTH (1ULL << 42) // HIDE_MENU_REQUEST_AUTH
|
||||
|
||||
// [43-63] Reserved
|
||||
#define MF_MCP_SETTINGS (1ULL << 43) // HIDE_MENU_MCP_SETTINGS
|
||||
|
||||
// [44-63] Reserved
|
||||
|
||||
|
||||
// ============================================================
|
||||
|
||||
117
server/2015Remote/HostJson.cpp
Normal file
117
server/2015Remote/HostJson.cpp
Normal file
@@ -0,0 +1,117 @@
|
||||
#include "stdafx.h"
|
||||
#include "HostJson.h"
|
||||
#include "HostInfo.h" // _ClientList / MAP_NOTE
|
||||
#include "context.h" // context 接口 + ONLINELIST_* 枚举
|
||||
#include "2015RemoteDlg.h" // GetClientEncoding 辅助函数(UTF-8 能力位判断)
|
||||
|
||||
#ifndef _WIN64
|
||||
#ifdef _DEBUG
|
||||
#pragma comment(lib, "jsoncpp/jsoncppd.lib")
|
||||
#else
|
||||
#pragma comment(lib, "jsoncpp/jsoncpp.lib")
|
||||
#endif
|
||||
#else
|
||||
#ifdef _DEBUG
|
||||
#pragma comment(lib, "jsoncpp/jsoncpp_x64d.lib")
|
||||
#else
|
||||
#pragma comment(lib, "jsoncpp/jsoncpp_x64.lib")
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Helper: Convert ANSI (GBK) string to UTF-8
|
||||
// (自 WebService.cpp 抽出,供 BuildDeviceListJson 与 BuildHostJson 共用)
|
||||
static std::string AnsiToUtf8(const CString& str) {
|
||||
if (str.IsEmpty()) return "";
|
||||
#ifdef _UNICODE
|
||||
// Unicode build: CString is already UTF-16, convert to UTF-8
|
||||
int len = WideCharToMultiByte(CP_UTF8, 0, str, -1, NULL, 0, NULL, NULL);
|
||||
if (len <= 0) return "";
|
||||
std::string result(len - 1, '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, str, -1, &result[0], len, NULL, NULL);
|
||||
return result;
|
||||
#else
|
||||
// MBCS build: CString is ANSI (GBK), need to convert to UTF-16 first, then to UTF-8
|
||||
int wlen = MultiByteToWideChar(CP_ACP, 0, str, -1, NULL, 0);
|
||||
if (wlen <= 0) return "";
|
||||
std::wstring wstr(wlen - 1, L'\0');
|
||||
MultiByteToWideChar(CP_ACP, 0, str, -1, &wstr[0], wlen);
|
||||
|
||||
int u8len = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, NULL, 0, NULL, NULL);
|
||||
if (u8len <= 0) return "";
|
||||
std::string result(u8len - 1, '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, &result[0], u8len, NULL, NULL);
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
Json::Value BuildHostJson(context* ctx, _ClientList* clientMap) {
|
||||
Json::Value device;
|
||||
|
||||
if (!ctx) return device; // 空 context → 返回 null Json::Value
|
||||
|
||||
// Use string for ID to avoid JavaScript number precision loss
|
||||
device["id"] = std::to_string(ctx->GetClientID());
|
||||
|
||||
CString name = ctx->GetClientData(ONLINELIST_COMPUTER_NAME);
|
||||
device["name"] = AnsiToUtf8(name);
|
||||
|
||||
// 用户在 MFC 端给这台主机起的备注(菜单"修改备注"写入 MAP_NOTE)
|
||||
if (clientMap) {
|
||||
CString remark = clientMap->GetClientMapData(ctx->GetClientID(), MAP_NOTE);
|
||||
if (!remark.IsEmpty()) {
|
||||
device["remark"] = AnsiToUtf8(remark);
|
||||
}
|
||||
}
|
||||
|
||||
CString ip = ctx->GetClientData(ONLINELIST_IP);
|
||||
device["ip"] = AnsiToUtf8(ip);
|
||||
|
||||
CString os = ctx->GetClientData(ONLINELIST_OS);
|
||||
device["os"] = AnsiToUtf8(os);
|
||||
|
||||
CString location = ctx->GetClientData(ONLINELIST_LOCATION);
|
||||
device["location"] = AnsiToUtf8(location);
|
||||
|
||||
CString rtt = ctx->GetClientData(ONLINELIST_PING);
|
||||
device["rtt"] = AnsiToUtf8(rtt);
|
||||
|
||||
CString version = ctx->GetClientData(ONLINELIST_VERSION);
|
||||
device["version"] = AnsiToUtf8(version);
|
||||
|
||||
// 活动窗口编码由客户端能力位决定:新客户端是 UTF-8,老客户端是 CP_ACP(默认 936)。
|
||||
// 不能像其它字段那样无脑 AnsiToUtf8——会把新客户端的 UTF-8 字节再当 GBK 双重编码。
|
||||
CString activeWindow = ctx->GetClientData(ONLINELIST_LOGINTIME);
|
||||
std::string activeWindowU8;
|
||||
if (!activeWindow.IsEmpty()) {
|
||||
UINT cp = GetClientEncoding(ctx);
|
||||
int wlen = MultiByteToWideChar(cp, 0, activeWindow, -1, NULL, 0);
|
||||
if (wlen > 1) {
|
||||
std::wstring w(wlen - 1, L'\0');
|
||||
MultiByteToWideChar(cp, 0, activeWindow, -1, &w[0], wlen);
|
||||
int u8len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, NULL, 0, NULL, NULL);
|
||||
if (u8len > 1) {
|
||||
activeWindowU8.resize(u8len - 1);
|
||||
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, &activeWindowU8[0], u8len, NULL, NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
device["activeWindow"] = activeWindowU8;
|
||||
device["online"] = true;
|
||||
|
||||
// Add device group to response
|
||||
std::string deviceGroup = ctx->GetGroupName();
|
||||
if (deviceGroup.empty()) deviceGroup = "default";
|
||||
device["group"] = deviceGroup;
|
||||
|
||||
// Get screen info from client's reported resolution
|
||||
// Format: "n:MxN" where n=monitor count, M=width, N=height
|
||||
CString resolution = ctx->GetAdditionalData(RES_RESOLUTION);
|
||||
if (!resolution.IsEmpty()) {
|
||||
device["screen"] = AnsiToUtf8(resolution); // e.g. "2:3840x1080"
|
||||
}
|
||||
|
||||
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
||||
device["clientType"] = AnsiToUtf8(clientType); // e.g. "MAC", "LNX", "EXE"
|
||||
|
||||
return device;
|
||||
}
|
||||
15
server/2015Remote/HostJson.h
Normal file
15
server/2015Remote/HostJson.h
Normal file
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "jsoncpp/json.h"
|
||||
|
||||
class context;
|
||||
class _ClientList;
|
||||
|
||||
// 公共函数:将单台在线主机 context 序列化为 JSON(UTF-8,含 GBK/UTF-8 编码处理)。
|
||||
// 供 Web UI(CWebService::BuildDeviceListJson)与 MCP 工具(list_online_hosts)复用,
|
||||
// 避免两处重复易错的编码转换(见 docs/Mcp_Design.md §3.4 方案 C)。
|
||||
//
|
||||
// 职责边界:本函数只负责「单台主机 → Json::Value」的字段序列化与编码转换。
|
||||
// 分组/用户过滤、m_cs 加锁、外层结构(Web 的 {"cmd","devices"} vs MCP 的 {"hosts"})
|
||||
// 属于调用方,不进本函数。调用方须在 m_cs 锁内调用。
|
||||
Json::Value BuildHostJson(context* ctx, _ClientList* clientMap);
|
||||
291
server/2015Remote/McpServer.cpp
Normal file
291
server/2015Remote/McpServer.cpp
Normal file
@@ -0,0 +1,291 @@
|
||||
#include "stdafx.h"
|
||||
#include "McpServer.h"
|
||||
#include "jsoncpp/json.h"
|
||||
#include "HostJson.h" // BuildHostJson(单台主机序列化公共函数)
|
||||
#include "context.h" // context 接口
|
||||
#include "2015RemoteDlg.h" // CMy2015RemoteDlg 成员(m_HostList/m_cs/m_ClientMap)+ VERSION_STR
|
||||
|
||||
#include <sstream>
|
||||
|
||||
#ifndef _WIN64
|
||||
#ifdef _DEBUG
|
||||
#pragma comment(lib, "jsoncpp/jsoncppd.lib")
|
||||
#else
|
||||
#pragma comment(lib, "jsoncpp/jsoncpp.lib")
|
||||
#endif
|
||||
#else
|
||||
#ifdef _DEBUG
|
||||
#pragma comment(lib, "jsoncpp/jsoncpp_x64d.lib")
|
||||
#else
|
||||
#pragma comment(lib, "jsoncpp/jsoncpp_x64.lib")
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
// Json::Value → 紧凑 JSON 字符串
|
||||
std::string JsonToString(const Json::Value& v) {
|
||||
Json::StreamWriterBuilder b;
|
||||
b["indentation"] = "";
|
||||
return Json::writeString(b, v);
|
||||
}
|
||||
|
||||
// JSON-RPC 2.0 成功响应
|
||||
std::string BuildResult(const Json::Value& id, const Json::Value& result) {
|
||||
Json::Value resp(Json::objectValue);
|
||||
resp["jsonrpc"] = "2.0";
|
||||
resp["id"] = id;
|
||||
resp["result"] = result;
|
||||
return JsonToString(resp);
|
||||
}
|
||||
|
||||
// JSON-RPC 2.0 错误响应
|
||||
std::string BuildError(const Json::Value& id, int code, const std::string& msg) {
|
||||
Json::Value resp(Json::objectValue);
|
||||
resp["jsonrpc"] = "2.0";
|
||||
resp["id"] = id;
|
||||
Json::Value err(Json::objectValue);
|
||||
err["code"] = code;
|
||||
err["message"] = msg;
|
||||
resp["error"] = err;
|
||||
return JsonToString(resp);
|
||||
}
|
||||
|
||||
// initialize 握手(MCP 规范:protocolVersion + capabilities + serverInfo)
|
||||
std::string BuildInitializeResult(const Json::Value& id) {
|
||||
Json::Value result(Json::objectValue);
|
||||
result["protocolVersion"] = "2025-06-18";
|
||||
Json::Value caps(Json::objectValue);
|
||||
caps["tools"] = Json::Value(Json::objectValue);
|
||||
result["capabilities"] = caps;
|
||||
Json::Value serverInfo(Json::objectValue);
|
||||
serverInfo["name"] = "yama";
|
||||
serverInfo["version"] = VERSION_STR;
|
||||
result["serverInfo"] = serverInfo;
|
||||
return BuildResult(id, result);
|
||||
}
|
||||
|
||||
// ping 健康检查:返回空 result
|
||||
std::string BuildPingResult(const Json::Value& id) {
|
||||
return BuildResult(id, Json::Value(Json::objectValue));
|
||||
}
|
||||
|
||||
// list_online_hosts 的 outputSchema(见 docs/Mcp_Design.md §3.3.2)
|
||||
Json::Value BuildHostOutputSchema() {
|
||||
Json::Value props(Json::objectValue);
|
||||
|
||||
Json::Value hostsProp(Json::objectValue);
|
||||
hostsProp["type"] = "array";
|
||||
Json::Value items(Json::objectValue);
|
||||
items["type"] = "object";
|
||||
Json::Value itemProps(Json::objectValue);
|
||||
const char* strFields[] = {
|
||||
"id", "name", "remark", "ip", "os", "location", "rtt",
|
||||
"version", "activeWindow", "group", "screen", "clientType"
|
||||
};
|
||||
for (const char* f : strFields) {
|
||||
Json::Value p(Json::objectValue);
|
||||
p["type"] = "string";
|
||||
itemProps[f] = p;
|
||||
}
|
||||
Json::Value onlineProp(Json::objectValue);
|
||||
onlineProp["type"] = "boolean";
|
||||
itemProps["online"] = onlineProp;
|
||||
items["properties"] = itemProps;
|
||||
hostsProp["items"] = items;
|
||||
props["hosts"] = hostsProp;
|
||||
|
||||
Json::Value schema(Json::objectValue);
|
||||
schema["type"] = "object";
|
||||
schema["properties"] = props;
|
||||
Json::Value required(Json::arrayValue);
|
||||
required.append("hosts");
|
||||
schema["required"] = required;
|
||||
return schema;
|
||||
}
|
||||
|
||||
// tools/list
|
||||
std::string BuildToolsListResult(const Json::Value& id) {
|
||||
Json::Value result(Json::objectValue);
|
||||
Json::Value tools(Json::arrayValue);
|
||||
|
||||
Json::Value tool(Json::objectValue);
|
||||
tool["name"] = "list_online_hosts";
|
||||
// 说明文字为 UTF-8:项目 /execution-charset:.936 会把普通窄字面量编译成 GBK,
|
||||
// 故用 u8 前缀确保输出到 JSON 的字节是 UTF-8。
|
||||
tool["description"] = u8"获取当前所有在线主机的列表,包含计算机名、IP、操作系统、版本、备注、分组、活动窗口、延迟等实时信息。";
|
||||
|
||||
Json::Value inputSchema(Json::objectValue);
|
||||
inputSchema["type"] = "object";
|
||||
inputSchema["properties"] = Json::Value(Json::objectValue);
|
||||
inputSchema["required"] = Json::Value(Json::arrayValue);
|
||||
tool["inputSchema"] = inputSchema;
|
||||
|
||||
tool["outputSchema"] = BuildHostOutputSchema();
|
||||
|
||||
tools.append(tool);
|
||||
result["tools"] = tools;
|
||||
return BuildResult(id, result);
|
||||
}
|
||||
|
||||
// tools/call(list_online_hosts)
|
||||
std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
|
||||
const Json::Value& id = root["id"];
|
||||
const Json::Value& params = root.isMember("params") ? root["params"]
|
||||
: Json::Value(Json::objectValue);
|
||||
|
||||
std::string toolName;
|
||||
if (params.isObject() && params.isMember("name") && params["name"].isString()) {
|
||||
toolName = params["name"].asString();
|
||||
}
|
||||
if (toolName != "list_online_hosts") {
|
||||
return BuildError(id, -32602,
|
||||
"Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName));
|
||||
}
|
||||
|
||||
Json::Value hosts(Json::arrayValue);
|
||||
int count = 0;
|
||||
if (parent) {
|
||||
// 与 WebService 侧一致的锁内遍历,复用 BuildHostJson 序列化(方案 C)。
|
||||
EnterCriticalSection(&parent->m_cs);
|
||||
for (context* ctx : parent->m_HostList) {
|
||||
if (!ctx || !ctx->IsLogin()) continue;
|
||||
hosts.append(BuildHostJson(ctx, parent->m_ClientMap));
|
||||
++count;
|
||||
}
|
||||
LeaveCriticalSection(&parent->m_cs);
|
||||
}
|
||||
|
||||
Json::Value result(Json::objectValue);
|
||||
Json::Value structuredContent(Json::objectValue);
|
||||
structuredContent["hosts"] = hosts;
|
||||
result["structuredContent"] = structuredContent;
|
||||
|
||||
Json::Value content(Json::arrayValue);
|
||||
Json::Value item(Json::objectValue);
|
||||
item["type"] = "text";
|
||||
item["text"] = std::string(u8"共 ") + std::to_string(count) + std::string(u8" 台主机在线。");
|
||||
content.append(item);
|
||||
result["content"] = content;
|
||||
result["isError"] = false;
|
||||
|
||||
return BuildResult(id, result);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// CMcpServer Implementation
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
CMcpServer& CMcpServer::Instance() {
|
||||
static CMcpServer instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
CMcpServer::CMcpServer() {
|
||||
m_server.Post("/mcp", [this](const httplib::Request& req, httplib::Response& res) {
|
||||
HandleMcp(req, res);
|
||||
});
|
||||
}
|
||||
|
||||
CMcpServer::~CMcpServer() {
|
||||
Stop(); // 兜底:确保监听线程 join,避免 std::thread 析构触发 terminate
|
||||
}
|
||||
|
||||
bool CMcpServer::Start(const std::string& bind, int port) {
|
||||
if (m_running.load()) return true; // 已在运行
|
||||
|
||||
m_thread = std::thread([this, bind, port]() {
|
||||
m_server.listen(bind, port);
|
||||
});
|
||||
|
||||
// 给 listen 一点时间绑定端口;httplib::Server::is_running() 在 listen 内部置位。
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
m_running.store(m_server.is_running());
|
||||
return m_running.load();
|
||||
}
|
||||
|
||||
void CMcpServer::Stop() {
|
||||
m_server.stop();
|
||||
if (m_thread.joinable()) {
|
||||
m_thread.join();
|
||||
}
|
||||
m_running.store(false);
|
||||
}
|
||||
|
||||
void CMcpServer::HandleMcp(const httplib::Request& req, httplib::Response& res) {
|
||||
res.set_header("Content-Type", "application/json");
|
||||
|
||||
// 静态 token 校验:Authorization: Bearer <token>(Start 前经 SetToken 保证非空)
|
||||
if (req.get_header_value("Authorization") != ("Bearer " + m_token)) {
|
||||
res.status = 401;
|
||||
res.set_content(BuildError(Json::nullValue, -32000, "Unauthorized"), "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析 JSON-RPC 请求体
|
||||
Json::Value root;
|
||||
Json::CharReaderBuilder rbuilder;
|
||||
std::string errs;
|
||||
std::istringstream iss(req.body);
|
||||
if (!Json::parseFromStream(rbuilder, iss, &root, &errs) || !root.isObject()) {
|
||||
res.set_content(BuildError(Json::nullValue, -32700, "Parse error"), "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
// 通知(无 id)→ 不返回 JSON-RPC 响应(如 notifications/initialized)
|
||||
if (!root.isMember("id")) {
|
||||
res.status = 202;
|
||||
res.set_content("", "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
// 结构校验:缺 method
|
||||
if (!root.isMember("method") || !root["method"].isString()) {
|
||||
res.set_content(BuildError(root["id"], -32600, "Invalid Request"), "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string method = root["method"].asString();
|
||||
|
||||
if (method == "initialize") {
|
||||
res.set_content(BuildInitializeResult(root["id"]), "application/json");
|
||||
return;
|
||||
}
|
||||
if (method == "ping") {
|
||||
res.set_content(BuildPingResult(root["id"]), "application/json");
|
||||
return;
|
||||
}
|
||||
if (method == "tools/list") {
|
||||
res.set_content(BuildToolsListResult(root["id"]), "application/json");
|
||||
return;
|
||||
}
|
||||
if (method == "tools/call") {
|
||||
res.set_content(BuildToolsCall(root, m_parent), "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
// 未实现的方法
|
||||
res.set_content(BuildError(root["id"], -32601, "Method not found"), "application/json");
|
||||
}
|
||||
|
||||
// rand_s:Windows CRT 加密安全随机源(基于系统 CSPRNG)。其声明需在 <stdlib.h> 前
|
||||
// 定义 _CRT_RAND_S;为避免依赖 PCH 的包含顺序,这里手动声明其导出原型(errno_t == int)。
|
||||
extern "C" int __cdecl rand_s(unsigned int* randomValue);
|
||||
|
||||
std::string GenerateRandomToken() {
|
||||
static const char hex[] = "0123456789abcdef";
|
||||
std::string out;
|
||||
out.reserve(32);
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
unsigned int v = 0;
|
||||
if (rand_s(&v) != 0) {
|
||||
// rand_s 失败(罕见):退化为时间 + 地址熵,保证仍返回非空 token。
|
||||
v = (unsigned int)(GetTickCount() ^ (ULONG_PTR)&out);
|
||||
}
|
||||
out.push_back(hex[(v >> 4) & 0xF]);
|
||||
out.push_back(hex[v & 0xF]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
58
server/2015Remote/McpServer.h
Normal file
58
server/2015Remote/McpServer.h
Normal file
@@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include <Windows.h>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
|
||||
// httplib 与 Windows 头部的 min/max 宏冲突,按 file_server.h 的既有约定处理。
|
||||
#undef min
|
||||
#undef max
|
||||
#include "httplib.h"
|
||||
#ifndef max
|
||||
#define max(a,b) (((a) > (b)) ? (a) : (b))
|
||||
#endif
|
||||
#ifndef min
|
||||
#define min(a,b) (((a) < (b)) ? (a) : (b))
|
||||
#endif
|
||||
|
||||
#pragma comment(lib, "ws2_32.lib")
|
||||
|
||||
class CMy2015RemoteDlg;
|
||||
|
||||
// MCP (Model Context Protocol) 服务端:httplib 封装 + JSON-RPC 2.0 分发 + 静态 token 校验。
|
||||
// 与 CWebService 平级、互不依赖;默认禁用,经「扩展 → MCP设置」开启后监听
|
||||
// (默认 127.0.0.1:6544,仅本机回环)。见 docs/Mcp_Design.md。
|
||||
class CMcpServer {
|
||||
public:
|
||||
static CMcpServer& Instance();
|
||||
|
||||
void SetParentDlg(CMy2015RemoteDlg* pDlg) { m_parent = pDlg; }
|
||||
void SetToken(const std::string& token) { m_token = token; }
|
||||
|
||||
bool Start(const std::string& bind, int port);
|
||||
void Stop();
|
||||
bool IsRunning() const { return m_running.load(); }
|
||||
|
||||
private:
|
||||
CMcpServer();
|
||||
~CMcpServer();
|
||||
CMcpServer(const CMcpServer&) = delete;
|
||||
CMcpServer& operator=(const CMcpServer&) = delete;
|
||||
|
||||
// POST /mcp 处理器:token 校验 + JSON-RPC 分发。
|
||||
void HandleMcp(const httplib::Request& req, httplib::Response& res);
|
||||
|
||||
httplib::Server m_server;
|
||||
std::thread m_thread;
|
||||
std::atomic<bool> m_running{false};
|
||||
std::string m_token;
|
||||
CMy2015RemoteDlg* m_parent = nullptr;
|
||||
};
|
||||
|
||||
// 全局访问器(仿 WebService(),见 WebService.h 末尾)
|
||||
inline CMcpServer& McpServer() { return CMcpServer::Instance(); }
|
||||
|
||||
// 生成 32 hex(128-bit)随机 token;供运行时兜底与「MCP设置」对话框预填复用。
|
||||
std::string GenerateRandomToken();
|
||||
191
server/2015Remote/McpSettingsDlg.cpp
Normal file
191
server/2015Remote/McpSettingsDlg.cpp
Normal file
@@ -0,0 +1,191 @@
|
||||
#include "stdafx.h"
|
||||
#include "McpSettingsDlg.h"
|
||||
#include "LangManager.h" // _TR / MessageBoxL
|
||||
#include "McpServer.h" // GenerateRandomToken
|
||||
#include "2015Remote.h" // THIS_CFG (GetThisCfg)
|
||||
#include <atlconv.h>
|
||||
|
||||
#ifdef _DEBUG
|
||||
#define new DEBUG_NEW
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
// 构造一个无控件的 DLGTEMPLATE:cdit=0,控件由 OnInitDialog 动态创建。
|
||||
// (与 ZstaPickerDlg.cpp 内同名函数一致)
|
||||
void BuildDialogTemplate(std::vector<BYTE>& out, LPCWSTR caption, short cx, short cy)
|
||||
{
|
||||
out.clear();
|
||||
auto append = [&](const void* p, size_t n) {
|
||||
const BYTE* b = (const BYTE*)p;
|
||||
out.insert(out.end(), b, b + n);
|
||||
};
|
||||
auto appendW = [&](WORD v) {
|
||||
out.push_back((BYTE)(v & 0xFF));
|
||||
out.push_back((BYTE)((v >> 8) & 0xFF));
|
||||
};
|
||||
auto appendWStr = [&](LPCWSTR s) {
|
||||
size_t n = wcslen(s);
|
||||
append(s, (n + 1) * sizeof(WCHAR));
|
||||
};
|
||||
|
||||
DLGTEMPLATE dt = { 0 };
|
||||
dt.style = DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER |
|
||||
WS_POPUP | WS_CAPTION | WS_SYSMENU;
|
||||
dt.dwExtendedStyle = 0;
|
||||
dt.cdit = 0;
|
||||
dt.x = 0; dt.y = 0;
|
||||
dt.cx = cx; dt.cy = cy;
|
||||
append(&dt, sizeof(dt));
|
||||
appendW(0); // no menu
|
||||
appendW(0); // default dialog class
|
||||
appendWStr(caption); // caption
|
||||
appendW(8); // font point size
|
||||
appendWStr(L"MS Shell Dlg"); // typeface
|
||||
|
||||
while (out.size() % 4) out.push_back(0); // DWORD align
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
BEGIN_MESSAGE_MAP(CMcpSettingsDlg, CDialog)
|
||||
END_MESSAGE_MAP()
|
||||
|
||||
CMcpSettingsDlg::CMcpSettingsDlg(CWnd* parent)
|
||||
: CDialog((LPCTSTR)NULL, parent)
|
||||
{
|
||||
}
|
||||
|
||||
INT_PTR CMcpSettingsDlg::DoModal()
|
||||
{
|
||||
USES_CONVERSION;
|
||||
CString title = _TR("MCP设置");
|
||||
BuildDialogTemplate(m_Template, T2CW(title), 320, 150);
|
||||
InitModalIndirect((LPCDLGTEMPLATE)m_Template.data());
|
||||
return CDialog::DoModal();
|
||||
}
|
||||
|
||||
BOOL CMcpSettingsDlg::OnInitDialog()
|
||||
{
|
||||
CDialog::OnInitDialog();
|
||||
|
||||
CRect cli;
|
||||
GetClientRect(&cli);
|
||||
|
||||
// 占位 rect,真正布局在 LayoutControls 里
|
||||
CRect r0(0, 0, 10, 10);
|
||||
|
||||
m_btnEnable.Create(_TR("启用 MCP"),
|
||||
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
|
||||
r0, this, IDC_MCP_ENABLE);
|
||||
m_lblPort.Create(_TR("端口"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1);
|
||||
m_editPort.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | ES_AUTOHSCROLL,
|
||||
r0, this, IDC_MCP_PORT);
|
||||
m_lblBind.Create(_TR("绑定地址"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1);
|
||||
m_editBind.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | ES_AUTOHSCROLL,
|
||||
r0, this, IDC_MCP_BIND);
|
||||
m_lblToken.Create(_TR("Token"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1);
|
||||
m_editToken.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | ES_AUTOHSCROLL,
|
||||
r0, this, IDC_MCP_TOKEN);
|
||||
m_btnOK.Create(_TR("确定"), WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON,
|
||||
r0, this, IDOK);
|
||||
m_btnCancel.Create(_TR("取消"), WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON,
|
||||
r0, this, IDCANCEL);
|
||||
|
||||
// 子控件继承对话框字体 (DS_SETFONT)
|
||||
HFONT hFont = (HFONT)::SendMessage(GetSafeHwnd(), WM_GETFONT, 0, 0);
|
||||
if (hFont) {
|
||||
m_btnEnable.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_lblPort.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_editPort.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_lblBind.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_editBind.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_lblToken.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_editToken.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_btnOK.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_btnCancel.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
}
|
||||
|
||||
// 回填配置(首次打开且 token 为空时预填随机值,随保存持久化)
|
||||
int enabled = THIS_CFG.GetInt("settings", "McpEnabled", 0);
|
||||
int port = THIS_CFG.GetInt("settings", "McpPort", 6544);
|
||||
std::string bind = THIS_CFG.GetStr("settings", "McpBind", "127.0.0.1");
|
||||
std::string tok = THIS_CFG.GetStr("settings", "McpToken", "");
|
||||
if (tok.empty()) tok = GenerateRandomToken();
|
||||
|
||||
m_btnEnable.SetCheck(enabled ? BST_CHECKED : BST_UNCHECKED);
|
||||
m_editPort.SetWindowText(CString(std::to_string(port).c_str()));
|
||||
m_editBind.SetWindowText(CString(bind.c_str()));
|
||||
m_editToken.SetWindowText(CString(tok.c_str()));
|
||||
|
||||
LayoutControls(cli.Width(), cli.Height());
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void CMcpSettingsDlg::OnOK()
|
||||
{
|
||||
CString sPort, sBind, sToken;
|
||||
m_editPort.GetWindowText(sPort);
|
||||
m_editBind.GetWindowText(sBind);
|
||||
m_editToken.GetWindowText(sToken);
|
||||
bool enabled = (m_btnEnable.GetCheck() == BST_CHECKED);
|
||||
|
||||
// 端口校验:1-65535
|
||||
int port = atoi(CT2A(sPort));
|
||||
if (port < 1 || port > 65535) {
|
||||
MessageBoxL(_TR("端口需为 1-65535 的数字"), _TR("提示"), MB_ICONWARNING);
|
||||
return;
|
||||
}
|
||||
|
||||
std::string bind = CT2A(sBind);
|
||||
if (bind.empty()) bind = "127.0.0.1";
|
||||
|
||||
std::string token = CT2A(sToken);
|
||||
if (enabled && token.empty()) {
|
||||
MessageBoxL(_TR("Token 不能为空"), _TR("提示"), MB_ICONWARNING);
|
||||
return;
|
||||
}
|
||||
|
||||
// 落盘
|
||||
THIS_CFG.SetInt("settings", "McpEnabled", enabled ? 1 : 0);
|
||||
THIS_CFG.SetInt("settings", "McpPort", port);
|
||||
THIS_CFG.SetStr("settings", "McpBind", bind);
|
||||
THIS_CFG.SetStr("settings", "McpToken", token);
|
||||
|
||||
// 拆成两段可翻译的单行键,中间用 \r\n 连接(多行键无法在 INI 中表示)
|
||||
MessageBox(_TR("MCP 设置已保存。") + _T("\r\n") +
|
||||
_TR("启用/端口/绑定地址/Token 的改动需重启程序生效。"),
|
||||
_TR("提示"), MB_ICONINFORMATION);
|
||||
|
||||
CDialog::OnOK();
|
||||
}
|
||||
|
||||
void CMcpSettingsDlg::LayoutControls(int cx, int cy)
|
||||
{
|
||||
const int margin = 14;
|
||||
const int labelW = 72;
|
||||
const int rowH = 24;
|
||||
const int gap = 10;
|
||||
const int btnW = 88;
|
||||
const int btnH = 26;
|
||||
|
||||
int y = margin;
|
||||
m_btnEnable.MoveWindow(margin, y, cx - margin * 2, 22);
|
||||
y += 30;
|
||||
|
||||
m_lblPort.MoveWindow(margin, y, labelW, rowH);
|
||||
m_editPort.MoveWindow(margin + labelW, y - 2, 100, rowH);
|
||||
y += rowH + gap;
|
||||
|
||||
m_lblBind.MoveWindow(margin, y, labelW, rowH);
|
||||
m_editBind.MoveWindow(margin + labelW, y - 2, cx - margin * 2 - labelW, rowH);
|
||||
y += rowH + gap;
|
||||
|
||||
m_lblToken.MoveWindow(margin, y, labelW, rowH);
|
||||
m_editToken.MoveWindow(margin + labelW, y - 2, cx - margin * 2 - labelW, rowH);
|
||||
y += rowH + gap;
|
||||
|
||||
int bottomY = cy - margin - btnH;
|
||||
m_btnCancel.MoveWindow(cx - margin - btnW, bottomY, btnW, btnH);
|
||||
m_btnOK.MoveWindow(cx - margin - btnW * 2 - gap, bottomY, btnW, btnH);
|
||||
}
|
||||
39
server/2015Remote/McpSettingsDlg.h
Normal file
39
server/2015Remote/McpSettingsDlg.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <afxwin.h>
|
||||
#include <afxcmn.h>
|
||||
#include <vector>
|
||||
|
||||
// 「MCP设置」配置对话框:启用 MCP / 端口 / 绑定地址 / Token。
|
||||
// 因 2015Remote.rc 为 UTF-16 编码、不宜手工编辑,参照 ZstaPickerDlg 用
|
||||
// InitModalIndirect + 内存 DLGTEMPLATE 程序化创建(见 docs/Mcp_Design.md §3.8)。
|
||||
class CMcpSettingsDlg : public CDialog
|
||||
{
|
||||
public:
|
||||
explicit CMcpSettingsDlg(CWnd* parent = nullptr);
|
||||
|
||||
virtual INT_PTR DoModal();
|
||||
|
||||
protected:
|
||||
virtual BOOL OnInitDialog();
|
||||
virtual void OnOK();
|
||||
|
||||
DECLARE_MESSAGE_MAP()
|
||||
|
||||
private:
|
||||
enum CtrlId {
|
||||
IDC_MCP_ENABLE = 1001, // 「启用 MCP」复选框
|
||||
IDC_MCP_PORT = 1002, // 端口编辑框
|
||||
IDC_MCP_BIND = 1003, // 绑定地址编辑框
|
||||
IDC_MCP_TOKEN = 1004, // Token 编辑框
|
||||
};
|
||||
|
||||
CButton m_btnEnable;
|
||||
CStatic m_lblPort, m_lblBind, m_lblToken;
|
||||
CEdit m_editPort, m_editBind, m_editToken;
|
||||
CButton m_btnOK, m_btnCancel;
|
||||
|
||||
std::vector<BYTE> m_Template; // 内存 DLGTEMPLATE 字节
|
||||
|
||||
void LayoutControls(int cx, int cy);
|
||||
};
|
||||
@@ -171,6 +171,7 @@
|
||||
#define HIDE_MENU_RELOAD_PLUGINS 0 // 重新加载插件
|
||||
#define HIDE_MENU_PLUGIN_REQUEST 0 // 插件请求
|
||||
#define HIDE_MENU_FRPS_FOR_SUB 0 // 下级FRP
|
||||
#define HIDE_MENU_MCP_SETTINGS 0 // MCP设置
|
||||
|
||||
// --- 扩展菜单 - 语言子菜单 ---
|
||||
#define HIDE_MENU_CHANGE_LANG 0 // 更改语言
|
||||
@@ -298,6 +299,8 @@
|
||||
// 用的 master password 解耦——后者一旦泄漏影响面更大,应避免在公网登录
|
||||
// 时复用。与 Go 服务端的 YAMA_WEB_ADMIN_PASS 语义一致。
|
||||
#define BRAND_WEB_ENV_VAR "YAMA_WEB_ADMIN_PASS"
|
||||
// MCP 服务端静态 token 环境变量(优先级高于 settings/McpToken)
|
||||
#define BRAND_MCP_ENV_VAR "YAMA_MCP_TOKEN"
|
||||
|
||||
// --- 宽字符版本(自动生成)---
|
||||
#define BRAND_APP_NAME_W _T(BRAND_APP_NAME)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "2015RemoteDlg.h"
|
||||
#include "Server.h" // For CONTEXT_OBJECT
|
||||
#include "jsoncpp/json.h"
|
||||
#include "HostJson.h" // BuildHostJson(单台主机序列化公共函数)
|
||||
#include "WebPage.h"
|
||||
#include "SimpleWebSocket.h"
|
||||
#include "common/commands.h"
|
||||
@@ -88,31 +89,6 @@ static void ClearNonce(void* ws_ptr) {
|
||||
s_ClientNonces.erase(ws_ptr);
|
||||
}
|
||||
|
||||
// Helper: Convert ANSI (GBK) string to UTF-8
|
||||
static std::string AnsiToUtf8(const CString& str) {
|
||||
if (str.IsEmpty()) return "";
|
||||
#ifdef _UNICODE
|
||||
// Unicode build: CString is already UTF-16, convert to UTF-8
|
||||
int len = WideCharToMultiByte(CP_UTF8, 0, str, -1, NULL, 0, NULL, NULL);
|
||||
if (len <= 0) return "";
|
||||
std::string result(len - 1, '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, str, -1, &result[0], len, NULL, NULL);
|
||||
return result;
|
||||
#else
|
||||
// MBCS build: CString is ANSI (GBK), need to convert to UTF-16 first, then to UTF-8
|
||||
int wlen = MultiByteToWideChar(CP_ACP, 0, str, -1, NULL, 0);
|
||||
if (wlen <= 0) return "";
|
||||
std::wstring wstr(wlen - 1, L'\0');
|
||||
MultiByteToWideChar(CP_ACP, 0, str, -1, &wstr[0], wlen);
|
||||
|
||||
int u8len = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, NULL, 0, NULL, NULL);
|
||||
if (u8len <= 0) return "";
|
||||
std::string result(u8len - 1, '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, &result[0], u8len, NULL, NULL);
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// CWebService Implementation
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
@@ -1544,71 +1520,9 @@ std::string CWebService::BuildDeviceListJson(const std::string& username) {
|
||||
if (!allowed) continue; // Skip device not in allowed groups
|
||||
}
|
||||
|
||||
Json::Value device;
|
||||
// Use string for ID to avoid JavaScript number precision loss
|
||||
device["id"] = std::to_string(ctx->GetClientID());
|
||||
|
||||
CString name = ctx->GetClientData(ONLINELIST_COMPUTER_NAME);
|
||||
device["name"] = AnsiToUtf8(name);
|
||||
|
||||
// 用户在 MFC 端给这台主机起的备注(菜单"修改备注"写入 MAP_NOTE)
|
||||
// 例如 hostname="A6" + remark="我的Windows" → web 显示"A6 (我的Windows)"
|
||||
if (m_pParentDlg->m_ClientMap) {
|
||||
CString remark = m_pParentDlg->m_ClientMap->GetClientMapData(
|
||||
ctx->GetClientID(), MAP_NOTE);
|
||||
if (!remark.IsEmpty()) {
|
||||
device["remark"] = AnsiToUtf8(remark);
|
||||
}
|
||||
}
|
||||
|
||||
CString ip = ctx->GetClientData(ONLINELIST_IP);
|
||||
device["ip"] = AnsiToUtf8(ip);
|
||||
|
||||
CString os = ctx->GetClientData(ONLINELIST_OS);
|
||||
device["os"] = AnsiToUtf8(os);
|
||||
|
||||
CString location = ctx->GetClientData(ONLINELIST_LOCATION);
|
||||
device["location"] = AnsiToUtf8(location);
|
||||
|
||||
CString rtt = ctx->GetClientData(ONLINELIST_PING);
|
||||
device["rtt"] = AnsiToUtf8(rtt);
|
||||
|
||||
CString version = ctx->GetClientData(ONLINELIST_VERSION);
|
||||
device["version"] = AnsiToUtf8(version);
|
||||
|
||||
// 活动窗口编码由客户端能力位决定:新客户端是 UTF-8,老客户端是 CP_ACP(默认 936)。
|
||||
// 不能像其它字段那样无脑 AnsiToUtf8——会把新客户端的 UTF-8 字节再当 GBK 双重编码。
|
||||
CString activeWindow = ctx->GetClientData(ONLINELIST_LOGINTIME);
|
||||
std::string activeWindowU8;
|
||||
if (!activeWindow.IsEmpty()) {
|
||||
UINT cp = GetClientEncoding(ctx);
|
||||
int wlen = MultiByteToWideChar(cp, 0, activeWindow, -1, NULL, 0);
|
||||
if (wlen > 1) {
|
||||
std::wstring w(wlen - 1, L'\0');
|
||||
MultiByteToWideChar(cp, 0, activeWindow, -1, &w[0], wlen);
|
||||
int u8len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, NULL, 0, NULL, NULL);
|
||||
if (u8len > 1) {
|
||||
activeWindowU8.resize(u8len - 1);
|
||||
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, &activeWindowU8[0], u8len, NULL, NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
device["activeWindow"] = activeWindowU8;
|
||||
device["online"] = true;
|
||||
|
||||
// Add device group to response
|
||||
device["group"] = deviceGroup;
|
||||
|
||||
// Get screen info from client's reported resolution
|
||||
// Format: "n:MxN" where n=monitor count, M=width, N=height
|
||||
CString resolution = ctx->GetAdditionalData(RES_RESOLUTION);
|
||||
if (!resolution.IsEmpty()) {
|
||||
device["screen"] = AnsiToUtf8(resolution); // e.g. "2:3840x1080"
|
||||
}
|
||||
|
||||
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
||||
device["clientType"] = AnsiToUtf8(clientType); // e.g. "MAC", "LNX", "EXE"
|
||||
|
||||
// 单台主机序列化委托给公共函数 BuildHostJson(与 MCP 侧复用,
|
||||
// 字段与编码转换逻辑保持一致,见 docs/Mcp_Design.md §3.4 方案 C)。
|
||||
Json::Value device = BuildHostJson(ctx, m_pParentDlg->m_ClientMap);
|
||||
res["devices"].append(device);
|
||||
}
|
||||
LeaveCriticalSection(&m_pParentDlg->m_cs);
|
||||
|
||||
@@ -427,6 +427,7 @@ Rar
|
||||
生成+AES -> bin 文件=Generate+AES -> bin File
|
||||
测试 AES bin文件=Test AES bin File
|
||||
替换图标(&R)=&Replace Icon
|
||||
插件(&P)=&Plugins
|
||||
刷新插件(&P)=Refresh &Plugins
|
||||
口令生成(&A)=&Auth Generate
|
||||
主控生成(&M)=&Master Generate
|
||||
@@ -1978,3 +1979,12 @@ FRPC Զ
|
||||
已将 %d 个 IP 从黑名单移除=Removed %d IP(s) from blacklist
|
||||
已恢复 %d 个客户端的授权状态=Restored authorization for %d client(s)
|
||||
未找到需要解除封禁的 IP=No IPs found that need unbanning
|
||||
; ===== MCP Settings =====
|
||||
MCP设置=MCP Settings
|
||||
MCP设置(&M)...=MCP Settings(&M)...
|
||||
启用 MCP=Enable MCP
|
||||
绑定地址=Bind Address
|
||||
端口需为 1-65535 的数字=Port must be a number between 1 and 65535
|
||||
Token 不能为空=Token cannot be empty
|
||||
MCP 设置已保存。=MCP settings saved.
|
||||
启用/端口/绑定地址/Token 的改动需重启程序生效。=Changes to enable/port/bind address/token take effect after restart.
|
||||
|
||||
@@ -427,6 +427,7 @@ Rar
|
||||
生成+AES -> bin 文件=產生+AES -> bin 檔案
|
||||
测试 AES bin文件=測試 AES bin 檔案
|
||||
替换图标(&R)=替換圖示(&R)
|
||||
插件(&P)=外掛程式(&P)
|
||||
刷新插件(&P)=重新整理外掛程式(&P)
|
||||
口令生成(&A)=密碼產生(&A)
|
||||
主控生成(&M)=主控產生(&M)
|
||||
@@ -1969,3 +1970,12 @@ FRPC Զ
|
||||
已将 %d 个 IP 从黑名单移除=已將 %d 個 IP 從黑名單移除
|
||||
已恢复 %d 个客户端的授权状态=已恢復 %d 個客戶端的授權狀態
|
||||
未找到需要解除封禁的 IP=未找到需要解除封禁的 IP
|
||||
; ===== MCP Settings =====
|
||||
MCP设置=MCP設定
|
||||
MCP设置(&M)...=MCP設定(&M)...
|
||||
启用 MCP=啟用 MCP
|
||||
绑定地址=綁定位址
|
||||
端口需为 1-65535 的数字=連接埠需為 1-65535 的數字
|
||||
Token 不能为空=Token 不能為空
|
||||
MCP 设置已保存。=MCP 設定已儲存。
|
||||
启用/端口/绑定地址/Token 的改动需重启程序生效。=啟用/連接埠/綁定位址/Token 的變更需重新啟動程式後生效。
|
||||
|
||||
@@ -999,6 +999,7 @@
|
||||
#define ID_MSGLOG_UNBAN_IP 33081
|
||||
#define ID_WEB_REMOTE_CONTROL 33044
|
||||
#define ID_TOOL_PLUGIN_SETTINGS 33045
|
||||
#define ID_MCP_SETTINGS 33077
|
||||
#define ID_33046 33046
|
||||
#define ID_PROXY_PORT_AUTORUN 33047
|
||||
#define ID_TRIGGER_SETTINGS 33048
|
||||
|
||||
Reference in New Issue
Block a user