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
192 lines
6.9 KiB
C++
192 lines
6.9 KiB
C++
#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);
|
||
}
|