Files
SimpleRemoter/server/2015Remote/McpSettingsDlg.cpp
yuanyuanxiang 70dbb6b255 Feature: Add persistent remote terminal MCP tools
Add terminal_open / terminal_exec / terminal_close so an AI can hold one
shell session per Windows host and run a sequence of commands with cwd and
environment preserved, instead of the one-shot exec_command.

The persistent terminal is a separate full-command write capability gated by
McpTerminal (default off) plus McpReadonly=0, with no whitelist and full
audit. One device maps to one terminal session via the shared m_TermSessions
map; idle sessions are swept after 300s. terminal_exec rejects commands that
contain & or | (they corrupt the sentinel control-operator chain) as well as
control characters.

Also harden exec_command and terminal_exec against newline/CR injection, fix a
dangling subCtx after an abrupt shell disconnect, and refresh lastActiveAt on
command completion. Add en/zh-TW translations for the new UI strings and a
design document covering both exec_command and the persistent terminal.

Co-Authored-By: deepseek-v4-pro
2026-08-24 19:11:16 +02:00

279 lines
11 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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 {
// 构造一个无控件的 DLGTEMPLATEcdit=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
}
// 把白名单文本规范化为「逗号分隔、逐项去首尾空白、去空项」的存储格式。
// 换行视作分隔符(与逗号等价),便于多行输入。
std::string NormalizeWhitelist(const std::string& raw)
{
std::string norm;
norm.reserve(raw.size());
for (char ch : raw)
norm += (ch == '\r' || ch == '\n') ? ',' : ch;
std::vector<std::string> items;
size_t start = 0;
while (start < norm.size()) {
size_t comma = norm.find(',', start);
std::string item = norm.substr(start, comma == std::string::npos ? std::string::npos : comma - start);
size_t b = item.find_first_not_of(" \t\r\n");
if (b != std::string::npos) {
size_t e = item.find_last_not_of(" \t\r\n");
item = item.substr(b, e - b + 1);
} else {
item.clear();
}
if (!item.empty()) items.push_back(item);
if (comma == std::string::npos) break;
start = comma + 1;
}
std::string out;
for (size_t i = 0; i < items.size(); ++i) {
if (i) out += ",";
out += items[i];
}
return out;
}
// 存储格式(逗号分隔)→ 多行展示(每行一条)。
std::string WhitelistForDisplay(const std::string& stored)
{
std::string disp;
disp.reserve(stored.size());
for (char ch : stored) {
if (ch == ',') disp += "\r\n";
else disp += ch;
}
return disp;
}
} // 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, 360);
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_btnReadonly.Create(_TR("只读模式(禁命令执行)"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
r0, this, IDC_MCP_READONLY);
m_btnTerminal.Create(_TR("启用持久终端(全命令,无白名单)"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
r0, this, IDC_MCP_TERMINAL);
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,
r0, this, IDC_MCP_WHITELIST);
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_btnReadonly.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnTerminal.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));
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();
int readonly = THIS_CFG.GetInt("settings", "McpReadonly", 1);
int terminal = THIS_CFG.GetInt("settings", "McpTerminal", 0);
std::string whitelist = THIS_CFG.GetStr("settings", "McpCmdWhitelist", "");
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()));
m_btnReadonly.SetCheck(readonly ? BST_CHECKED : BST_UNCHECKED);
m_btnTerminal.SetCheck(terminal ? BST_CHECKED : BST_UNCHECKED);
// 白名单存储为逗号分隔,展示为每行一条。
m_editWhitelist.SetWindowText(CString(WhitelistForDisplay(whitelist).c_str()));
LayoutControls(cli.Width(), cli.Height());
return TRUE;
}
void CMcpSettingsDlg::OnOK()
{
CString sPort, sBind, sToken, sWhitelist;
m_editPort.GetWindowText(sPort);
m_editBind.GetWindowText(sBind);
m_editToken.GetWindowText(sToken);
m_editWhitelist.GetWindowText(sWhitelist);
bool enabled = (m_btnEnable.GetCheck() == BST_CHECKED);
bool readonly = (m_btnReadonly.GetCheck() == BST_CHECKED);
bool terminal = (m_btnTerminal.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);
THIS_CFG.SetInt("settings", "McpReadonly", readonly ? 1 : 0);
THIS_CFG.SetInt("settings", "McpTerminal", terminal ? 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("提示"), 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;
m_btnReadonly.MoveWindow(margin, y, cx - margin * 2, 22);
y += 30;
m_btnTerminal.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);
y += whitelistH + 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);
}