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
279 lines
11 KiB
C++
279 lines
11 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
|
||
}
|
||
|
||
// 把白名单文本规范化为「逗号分隔、逐项去首尾空白、去空项」的存储格式。
|
||
// 换行视作分隔符(与逗号等价),便于多行输入。
|
||
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);
|
||
}
|