Add chat_open, chat_send, chat_wait and chat_close to the MCP tool set, behind a new McpChat switch that defaults to off and is independent of the read-only switch. The tools reuse the existing client chat link: the main connection sends COMMAND_CHAT, the client opens a sub-connection with TOKEN_CONN_AUTH and TOKEN_CHAT_START, the server answers with COMMAND_NEXT_CHAT, and both directions then carry raw CP936 text. Sessions are per host and held in shared pointers under their own mutex, so a waiting chat_wait survives a close or a dropped sub-connection, and chat_send is serialised per session. Text is converted strictly between UTF-8 and CP936: invalid UTF-8, characters GBK cannot represent (emoji, accented letters) and messages over the client's 4000-byte buffer are rejected with -32602 instead of being mangled. A one-second settle delay after the sub-connection binds keeps the first message from reaching the client before its chat window exists. The routing hooks run before the host-management hooks in MessageHandle and clear the route in OfflineProc before the context leaves the list. A five-second tombstone absorbs a late TOKEN_CHAT_START after a timeout, so a failed chat_open does not pop the MFC chat dialog. Idle sessions are swept on every tool call, and audit entries record the session id and byte counts only, never message bodies. The settings dialog gains the chat checkbox and the translated labels for all three languages, and the strict UTF-8 to CP936 helpers move from McpMachine.cpp to McpInternal so both tool families can share them. Co-Authored-By: deepseek-flash
365 lines
16 KiB
C++
365 lines
16 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, 510);
|
||
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_btnRemoteControl.Create(_TR("启用远程控制(AI 操控桌面)"),
|
||
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
|
||
r0, this, IDC_MCP_REMOTECONTROL);
|
||
m_btnFileTransfer.Create(_TR("启用文件传输(download_file)"),
|
||
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
|
||
r0, this, IDC_MCP_FILETRANSFER);
|
||
m_btnManage.Create(_TR("启用管理操作(进程/窗口/服务/电源)"),
|
||
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
|
||
r0, this, IDC_MCP_MANAGE);
|
||
m_btnMachine.Create(_TR("启用主机管理(进程/窗口/服务/任务/网络状态)"),
|
||
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
|
||
r0, this, IDC_MCP_MACHINE);
|
||
m_btnMachineDangerous.Create(_TR("启用危险主机操作(冻结/解冻进程、停止/重启服务)"),
|
||
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
|
||
r0, this, IDC_MCP_MACHINE_DANGEROUS);
|
||
m_btnChat.Create(_TR("启用远程交谈(AI 与用户对话)"),
|
||
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
|
||
r0, this, IDC_MCP_CHAT);
|
||
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_btnRemoteControl.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||
m_btnFileTransfer.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||
m_btnManage.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||
m_btnMachine.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||
m_btnMachineDangerous.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||
m_btnChat.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);
|
||
int remoteControl = THIS_CFG.GetInt("settings", "McpRemoteControl", 0);
|
||
int fileTransfer = THIS_CFG.GetInt("settings", "McpFileTransfer", 0);
|
||
int manage = THIS_CFG.GetInt("settings", "McpManage", 0);
|
||
int machine = THIS_CFG.GetInt("settings", "McpMachine", 0);
|
||
int machineDangerous = THIS_CFG.GetInt("settings", "McpMachineDangerous", 0);
|
||
int chat = THIS_CFG.GetInt("settings", "McpChat", 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_btnRemoteControl.SetCheck(remoteControl ? BST_CHECKED : BST_UNCHECKED);
|
||
m_btnFileTransfer.SetCheck(fileTransfer ? BST_CHECKED : BST_UNCHECKED);
|
||
m_btnManage.SetCheck(manage ? BST_CHECKED : BST_UNCHECKED);
|
||
m_btnMachine.SetCheck(machine ? BST_CHECKED : BST_UNCHECKED);
|
||
m_btnMachineDangerous.SetCheck(machineDangerous ? BST_CHECKED : BST_UNCHECKED);
|
||
m_btnChat.SetCheck(chat ? 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);
|
||
bool remoteControl = (m_btnRemoteControl.GetCheck() == BST_CHECKED);
|
||
bool fileTransfer = (m_btnFileTransfer.GetCheck() == BST_CHECKED);
|
||
bool manage = (m_btnManage.GetCheck() == BST_CHECKED);
|
||
bool machine = (m_btnMachine.GetCheck() == BST_CHECKED);
|
||
bool machineDangerous = (m_btnMachineDangerous.GetCheck() == BST_CHECKED);
|
||
bool chat = (m_btnChat.GetCheck() == BST_CHECKED);
|
||
|
||
// 危险子集依附于主机管理总开关:没开主机管理就没有工具可放开,直接清除并提示。
|
||
if (machineDangerous && !machine) {
|
||
machineDangerous = false;
|
||
m_btnMachineDangerous.SetCheck(BST_UNCHECKED);
|
||
MessageBoxL(_TR("危险主机操作依附于主机管理:请先启用主机管理。"), _TR("提示"),
|
||
MB_ICONWARNING);
|
||
}
|
||
|
||
// 端口校验: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);
|
||
THIS_CFG.SetInt("settings", "McpRemoteControl", remoteControl ? 1 : 0);
|
||
THIS_CFG.SetInt("settings", "McpFileTransfer", fileTransfer ? 1 : 0);
|
||
THIS_CFG.SetInt("settings", "McpManage", manage ? 1 : 0);
|
||
THIS_CFG.SetInt("settings", "McpMachine", machine ? 1 : 0);
|
||
THIS_CFG.SetInt("settings", "McpMachineDangerous", machineDangerous ? 1 : 0);
|
||
THIS_CFG.SetInt("settings", "McpChat", chat ? 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("持久终端、远程控制与管理操作仅在只读模式关闭时生效。") + _T("\r\n") +
|
||
_TR("主机管理的写操作需只读模式关闭;冻结/解冻进程、停止/重启服务还需启用危险主机操作。") + _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;
|
||
|
||
m_btnRemoteControl.MoveWindow(margin, y, cx - margin * 2, 22);
|
||
y += 30;
|
||
|
||
m_btnFileTransfer.MoveWindow(margin, y, cx - margin * 2, 22);
|
||
y += 30;
|
||
|
||
m_btnManage.MoveWindow(margin, y, cx - margin * 2, 22);
|
||
y += 30;
|
||
|
||
m_btnMachine.MoveWindow(margin, y, cx - margin * 2, 22);
|
||
y += 30;
|
||
|
||
m_btnMachineDangerous.MoveWindow(margin, y, cx - margin * 2, 22);
|
||
y += 30;
|
||
|
||
m_btnChat.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;
|
||
|
||
// 确定 / 取消按钮紧跟白名单下方,不再锚定到底部,避免白名单与按钮之间出现大段空白。
|
||
m_btnCancel.MoveWindow(cx - margin - btnW, y, btnW, btnH);
|
||
m_btnOK.MoveWindow(cx - margin - btnW * 2 - gap, y, btnW, btnH);
|
||
y += btnH + margin;
|
||
|
||
// 收缩对话框到内容实际高度并重新居中,消除底部空白。
|
||
if (y < cy) {
|
||
CRect wnd;
|
||
GetWindowRect(&wnd);
|
||
SetWindowPos(NULL, 0, 0, wnd.Width(), y + (wnd.Height() - cy),
|
||
SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
|
||
CenterWindow();
|
||
}
|
||
}
|