Feature: Add MCP tools for remote chat with the client user
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
This commit is contained in:
1 parent
fff7c67af6
commit
c99a1c375e
17 files changed
+1519
-31
No files matched your search
@@ -0,0 +1,245 @@
|
||||
# MCP 远程交谈(P9):AI ↔ 客户端使用者双向对话
|
||||
|
||||
## 1. 目标与边界
|
||||
|
||||
让接入服务端 MCP 的大模型与坐在 Windows 客户端电脑前的人进行多轮文字交谈。
|
||||
|
||||
- **仅 Windows 客户端**:`LNX` / `MAC` / `APK` 直接返回 `-32005`,不发命令。
|
||||
- **复用既有功能,协议零改动**:沿用「远程交谈」(`client/ChatManager.cpp` + `IDD_CHAT`),
|
||||
报文与握手不变。客户端侧只有两处改动:聊天窗口改为 Unicode 对话框(见 §5.8,
|
||||
解决非 936 ACP 系统上的中文乱码),以及加壳部署下对话框改用内嵌模板兜底(见 §5.7)。
|
||||
- **独立开关 `McpChat`**(默认 `0`):与 `McpReadonly` 无关;关闭时四个工具不出现在
|
||||
`tools/list`,直接调用返回 `-32006`。
|
||||
- **直接弹出客户端窗口**:没有确认框;会话建立即代表开始交谈。
|
||||
- 验证方式:在虚拟机里运行交互式 Windows 客户端,由人扮演使用者与服务端 AI 对话。
|
||||
|
||||
## 2. 复用协议
|
||||
|
||||
| 方向 | 报文 |
|
||||
|---|---|
|
||||
| 服务端主连接 → 客户端 | `[COMMAND_CHAT=161]`(1 字节) |
|
||||
| 客户端新建子连接 | 先 `[TOKEN_CONN_AUTH=246]`(`EnableSubConnAuth`),再 `[TOKEN_CHAT_START=62]` |
|
||||
| 服务端子连接 → 客户端 | `[COMMAND_NEXT_CHAT=0]`(1 字节,通知客户端弹窗) |
|
||||
| 双向文本 | `[CP936/GBK 字节][0x00]`,无 token 前缀 |
|
||||
| 服务端子连接 → 客户端 | `[COMMAND_CHAT_CLOSE=1]` 结束 |
|
||||
|
||||
时序:
|
||||
|
||||
```
|
||||
chat_open:
|
||||
FindMainContext(id) → 校验 Windows → 校验 McpChat
|
||||
mainCtx->Send2Client([161])
|
||||
客户端建子连接 → [246] 认证 → [62] 就绪
|
||||
OnChatSubPacket 绑定 62 → 回 [0] → readyAt = now + 1000ms
|
||||
chat_open 等到 started && now >= readyAt 才返回
|
||||
chat_send:
|
||||
subCtx->Send2Client(GBK + NUL) → WaitSendIdle(2000)
|
||||
客户端使用者输入:
|
||||
subCtx → [GBK + NUL] → OnChatSubPacket → inbox → 唤醒 chat_wait
|
||||
chat_close:
|
||||
subCtx->Send2Client([1]) → WaitSendIdle(1000) → CancelIO
|
||||
```
|
||||
|
||||
未绑定子连接上的 `TOKEN_CONN_AUTH(246)` 不被聊天钩子消费,仍由 `MessageHandle`
|
||||
的鉴权分支处理并回 `ConnAuthAck`,认证后子连接的 `clientID` 才可靠(新客户端路径)。
|
||||
已绑定子连接上首字节为 `0xF6` 的 GBK 文本不会被误判为鉴权包(绑定连接上所有包都由
|
||||
聊天钩子消费)。
|
||||
|
||||
## 3. 工具
|
||||
|
||||
| 工具 | 输入 | 输出 |
|
||||
|---|---|---|
|
||||
| `chat_open` | `id`;`timeout_ms`(默认 15000,2000–60000) | `{session_id, status:"open", message}` |
|
||||
| `chat_send` | `id`, `session_id`, `message` | `{session_id, sent:true, bytes}` |
|
||||
| `chat_wait` | `id`, `session_id`;`timeout_ms`(默认 30000,0–55000)、`max_messages`(默认 20,1–50) | `{session_id, messages:[{id,at,text}], timed_out, closed, dropped}` |
|
||||
| `chat_close` | `id`, `session_id` | `{session_id, closed:true}`;主机在线时会话已不存在按幂等处理为成功;主机离线返回 `-32002` |
|
||||
|
||||
典型对话循环:
|
||||
|
||||
```
|
||||
chat_open(id) → session_id
|
||||
chat_send(id, sid, "你好,我是助手")
|
||||
chat_wait(id, sid, 30000) → 对方的回复
|
||||
chat_send(id, sid, "……")
|
||||
chat_wait(id, sid, 30000)
|
||||
chat_close(id, sid)
|
||||
```
|
||||
|
||||
错误码沿用 MCP 约定:
|
||||
|
||||
| 码 | 含义 |
|
||||
|---|---|
|
||||
| `-32001` | `chat_open` 等不到客户端建窗(离线、老客户端、非交互桌面) |
|
||||
| `-32002` | 主机离线 / 会话不存在或已关闭 / `session_id` 不匹配 |
|
||||
| `-32003` | 同设备已有活跃会话;或同一会话已有 `chat_send` / `chat_wait` 在飞 |
|
||||
| `-32004` | 发送失败(`Send2Client` 返回 FALSE) |
|
||||
| `-32005` | 非 Windows 客户端 |
|
||||
| `-32006` | `McpChat` 关闭 |
|
||||
| `-32602` | 参数、UTF-8、GBK 可表示性或长度错误 |
|
||||
|
||||
## 4. 编码与长度
|
||||
|
||||
- **出站**:UTF-8 → CP936 **严格转换**(`MB_ERR_INVALID_CHARS` +
|
||||
`WC_NO_BEST_FIT_CHARS` + `lpUsedDefaultChar`)。emoji、`ö` 等 GBK 不可表示字符
|
||||
直接返回 `-32602`,绝不静默变成 `?`。
|
||||
- **入站**:CP936 → UTF-8,按首个 NUL 截断。
|
||||
- 出站上限 4000 GBK 字节(客户端输入缓冲 `char str[4096]`,硬上限 4095)。
|
||||
- 入站单条上限 8 KiB;inbox 100 条 / 256 KiB,溢出丢最旧并在 `chat_wait` 的
|
||||
`dropped` 中报告。
|
||||
- 拒绝空消息、纯空白、`\r\n`、NUL 及所有 C0 控制字符。
|
||||
|
||||
客户端聊天窗口自 P9 起是 Unicode 对话框(见 §5.8):入站 GBK 字节先转 UTF-16 再
|
||||
显示,出站 UTF-16 严格转 GBK 后发送,因此在德语等非 936 系统上中文不再乱码。协议
|
||||
本身仍是 GBK,`ö`/`ß`/emoji 等 GBK 外字符两端都无法传输(服务端 `-32602`;客户端
|
||||
在发送框下方给出提示并不发送)。
|
||||
|
||||
## 5. 客户端怪癖与规避(协议不变)
|
||||
|
||||
1. **首字节 101 ('e') 被丢弃**:`client/ChatManager.cpp` 在任何长度判断之前执行
|
||||
`if (szBuffer[0] == TOKEN_HEARTBEAT) return;`。服务端在转换后的 GBK 首字节为
|
||||
101 时前置一个空格(显示上无感)。若客户端日后修复此行,可移除该规避。
|
||||
2. **窗口创建前的文本被丢弃**:客户端收到 `[0]` 后 `WaitForDialogOpen` 只
|
||||
`Sleep(150)`,随后才在线程里 `CreateDialogParamW`;`OnReceive` 仅在 `m_hWnd`
|
||||
非空时显示文本。服务端把 `readyAt` 设为绑定后 1000ms,`chat_open` 等满再返回,
|
||||
`chat_send` 也拒绝早于 `readyAt` 的发送。
|
||||
3. **X / 结束交谈只隐藏不关闭**:客户端 `ShowWindow(SW_HIDE)`,会话仍活;下一条
|
||||
服务端文本会 `SW_RESTORE` 重新弹出。因此隐藏不是关闭信号,结束时必须显式
|
||||
`chat_close`。
|
||||
4. **一字节控制包**:客户端把 `len==1` 的包当命令,故文本必须带尾部 NUL(至少
|
||||
2 字节),空消息在工具层拒绝。
|
||||
5. **手动「远程交谈」竞争**:MCP 会话在飞时用户又点菜单会产生第二个 `[62]`;
|
||||
先到者绑定,后到者被吸收并 `CancelIO`。极窄竞争下用户这次看不到 MFC 窗口,
|
||||
重试即可。
|
||||
6. **Session 0 / SYSTEM**:`COMMAND_CHAT` 不像抓屏那样重定向到交互会话;以服务
|
||||
方式运行的客户端没有交互桌面,`chat_open` 会超时。VM 验证必须在交互桌面运行。
|
||||
7. **加壳部署的对话框兜底**:EXE + SC_AES/sRDI 打包时,
|
||||
`g_hInstance = GetModuleHandle(NULL)` 是外壳进程映像,资源里只有清单、没有
|
||||
`IDD_CHAT`;载荷以裸 shellcode 在壳进程内运行,未注册进加载器,Windows 的
|
||||
`FindResource`/`LoadResource`/`CreateDialogParamW` 取不到对话框
|
||||
(实测 err=1812 / 59),聊天线程建窗后立即退出。客户端因此保留
|
||||
`CreateDialogParamW` 原路径,失败时回退到 `CreateDialogIndirectParamW` +
|
||||
内嵌模板(`client/ChatDialogTemplate.h`,取自载荷 `RT_DIALOG 106`,240 字节),
|
||||
图标回退为「直接遍历本模块映像的资源目录取 `RT_GROUP_ICON 105` →
|
||||
`RT_ICON` → `CreateIconFromResourceEx`」(`client/ImageResource.h`),
|
||||
仍失败才用 `LoadIcon(NULL, IDI_APPLICATION)`。**旧客户端无此兜底,加壳部署下
|
||||
不会弹窗**:MDLL 由服务端自动下发新 DLL 即生效,EXE 需重新分发客户端。
|
||||
8. **Unicode 对话框(非 936 系统乱码修复)**:客户端是 MBCS 编译,但对话框可以建
|
||||
成 Unicode 窗口:`CreateDialogParamW` / `CreateDialogIndirectParamW` 创建的窗口
|
||||
`IsWindowUnicode` 为真,而 `DLGTEMPLATEEX` 的字符串本来就按 UTF-16 存储。因此
|
||||
聊天窗口全部改走 W 版 API——`GetWindowTextW` / `SetDlgItemTextW` / 日志用
|
||||
UTF-16 拼接,消息循环用 `GetMessageW` / `IsDialogMessageW` / `DispatchMessageW`
|
||||
(否则 WM_CHAR / WM_IME_* 会先按 ACP 转换)。入站
|
||||
`MultiByteToWideChar(936, …)`,出站 `WideCharToMultiByte(936,
|
||||
WC_NO_BEST_FIT_CHARS, …, &usedDefault)`;`usedDefault` 置位即 GBK 不可表示
|
||||
字符,提示后不发送。`IDD_CHAT` 字体由点阵 "System" 改为 "MS Shell Dlg"
|
||||
(可缩放,并经 SystemLink 字体链接到中文字体),否则 Unicode 窗口里同样没有
|
||||
中文字形;前提是系统装有至少一种 CJK 字体(简体中文补充字体或 Microsoft YaHei
|
||||
等),否则只能显示方框。静态文案按 `GetUserDefaultUILanguage()` 在中文/英文两套
|
||||
间切换,`Script.rc` 因此直接写英文(德语系统看到的是英文而非乱码)。协议与服务端
|
||||
逻辑不变。
|
||||
|
||||
## 6. 服务端状态机
|
||||
|
||||
数据结构(`McpServer.h`,受 `m_ChatMutex` 保护):
|
||||
|
||||
```cpp
|
||||
struct ChatSession {
|
||||
std::string sessionId;
|
||||
context* mainCtx; // 主连接(仅身份判断;主连接断开后置空)
|
||||
uint64_t mainClientId;
|
||||
context* subCtx; // 已绑定的子连接;关闭后置空
|
||||
std::string peer; // GetPeerName()(老客户端 clientID=0 兜底)
|
||||
bool started, closed;
|
||||
bool sendBusy, waitBusy; // 同一会话串行发送 / 只允许一个 chat_wait
|
||||
steady_clock::time_point readyAt;
|
||||
time_t lastActiveAt;
|
||||
std::deque<ChatMessage> inbox; // UTF-8,未投递
|
||||
size_t inboxBytes;
|
||||
uint64_t nextMsgId;
|
||||
int dropped;
|
||||
};
|
||||
using ChatSessionPtr = std::shared_ptr<ChatSession>;
|
||||
```
|
||||
|
||||
- 会话用 `shared_ptr` 持有:`chat_wait` / `chat_send` 在工作线程复制一份后,即使
|
||||
map 条目被 `chat_close` 或断线删除也安全。
|
||||
- `m_ChatSessions[device]`、`m_ChatContextToDevice[subCtx]`、`m_ChatAbsorbedSubCtx`
|
||||
与 `m_ChatRoutes`(原子计数)供 `MessageHandle` 快速路径判断;无会话在飞时
|
||||
`OnChatSubPacket` 无锁返回,避免音频/屏幕等高频子连接逐包抢锁。
|
||||
- 锁序:`parent->m_cs`(`FindMainContext`)→ `m_ChatMutex`。持锁期间不发送、
|
||||
不 `CancelIO`、不 `WaitSendIdle`。
|
||||
- 超时/关闭后到达的 `[62]` 由 5s 墓碑(按 device 与 peer 两个维度)吸收并
|
||||
`CancelIO`,避免弹出 MFC 聊天对话框。
|
||||
- 空闲回收 600s(任一无 `busy` 且 `lastActiveAt` 超时的会话),每个 chat 工具
|
||||
入口惰性调用。
|
||||
|
||||
## 7. 钩子与生命周期
|
||||
|
||||
`2015RemoteDlg.cpp`:
|
||||
|
||||
1. `MessageHandle`:MCP 终端块之后、**P8 `OnManagedSubPacket` 之前**调用
|
||||
`OnChatSubPacket`。聊天文本是裸 GBK,首字节可能是 61/255,若先交给 P8 会被
|
||||
误绑定。
|
||||
2. `OfflineProc`:P8 清理之后、`RemoveFromHostList` **之前**调用
|
||||
`OnChatContextClosed`(连接池会复用 `context*`,晚清理会误路由新连接)。
|
||||
子连接断开 → 会话 `closed` 并唤醒 `chat_wait`;主连接断开 → 关闭其所有会话并
|
||||
锁外 `CancelIO` 子连接。
|
||||
3. 启动:`SetChatEnabled(THIS_CFG.GetInt("settings", "McpChat", 0) != 0)`。
|
||||
|
||||
审计:`AuditChatAction` 与 `AuditManageAction` 同落地方式(`WM_SHOWERRORMSG`),
|
||||
标题 `_TR("MCP远程交谈")`;只记录 `session=...` 与 `bytes=N`,**绝不记录消息正文**。
|
||||
`chat_wait` 不审计,避免刷屏。
|
||||
|
||||
## 8. 配置
|
||||
|
||||
| INI key | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `McpChat` | `0` | 远程交谈工具族开关;改动需重启服务端 |
|
||||
|
||||
「扩展 → MCP设置」新增复选框「启用远程交谈(AI 与用户对话)」。
|
||||
|
||||
## 9. 验证矩阵
|
||||
|
||||
准备:`McpEnabled=1`、`McpChat=1` 后重启服务端;MCP 客户端重连使 `tools/list`
|
||||
刷新;VM 中交互运行 Windows 客户端。
|
||||
|
||||
| # | 操作 | 期望 |
|
||||
|---|---|---|
|
||||
| 1 | `tools/list`(开关开 / 关) | 四个 chat 工具出现 / 消失;关闭时直调 `-32006` |
|
||||
| 2 | `chat_open(id)` | VM 约 1s 内弹出「远程交谈」;返回 32 位 hex `session_id` |
|
||||
| 3 | `chat_send` 中文;用户回中文;`chat_wait` | VM 显示「对方: …」;返回 UTF-8 原文 |
|
||||
| 4 | 连续多轮、用户连发两条 | 下一轮批量返回,顺序正确,`dropped=0` |
|
||||
| 5 | 发送 `"echo test"`(首字节 'e') | 正常显示(前置空格规避生效) |
|
||||
| 6 | open 后立刻 send,重复 10 次 | 第一条不丢(1000ms 建窗等待) |
|
||||
| 7 | 4000 / 4001 字节、emoji | 边界内成功;超长与 emoji 返回 `-32602` |
|
||||
| 8 | 无回复时 `chat_wait` | 到点 `timed_out:true`,会话仍活 |
|
||||
| 9 | 用户点 X 隐藏后再 send | 窗口恢复,会话仍活 |
|
||||
| 10 | `chat_close` | 窗口关闭;旧 id 发送 `-32002`;立即重新 open 成功 |
|
||||
| 11 | `chat_wait` 阻塞时杀掉客户端 | 返回 `closed:true`;服务端稳定;重连后可重新 open |
|
||||
| 12 | Linux / macOS / Android 主机 | `-32005`,不发命令 |
|
||||
| 13 | 同设备第二次 `chat_open` | `-32003` |
|
||||
| 14 | 无 MCP 会话时手动「远程交谈」 | MFC 聊天窗口照常打开(回归) |
|
||||
| 15 | 服务端审计日志 | 有 open/send/close 的 session id 与字节数,无消息正文 |
|
||||
| 16 | 现有 MCP 工具 + 手动主机/连接/终端对话框 | 行为不变 |
|
||||
| 17 | `chat_wait timeout_ms=55000` | 在常见 60s 工具超时前返回 |
|
||||
| 18 | 加壳(SC_AES/sRDI)EXE 客户端 `chat_open` | 仍弹出「远程交谈」且标题栏图标正常(内嵌模板 + 自模块映像取图标);客户端日志有 `CreateDialogParamW(1812) 失败,改用内嵌对话框模板` |
|
||||
| 19 | 非 936 系统(如德语 Windows)`chat_open` + 中文 `chat_send` | 标题、按钮为英文(系统 UI 非中文),中文消息正常显示而非乱码;中文系统为中文按钮 |
|
||||
| 20 | 德语系统客户端发送 `Grüße` / emoji | 客户端不发送并在日志区提示 GBK 不支持;服务端若直接 `chat_send` 同类字符返回 `-32602` |
|
||||
| 21 | 未安装任何 CJK 字体的系统 | 中文显示为方框(字体缺失,非编码问题);安装中文补充字体后恢复 |
|
||||
|
||||
## 10. 已知限制
|
||||
|
||||
- **无投递回执**:`sent:true` 只表示已写入子连接并 `WaitSendIdle` 成功,不保证
|
||||
使用者已看见。
|
||||
- **HTTP 工作线程**:`chat_wait` 最长 55s 会占用一个 httplib 工作线程;不要在
|
||||
同一 MCP 连接上并行大量长等待。
|
||||
- **连接池复用窄竞争**:发送时捕获的 `subCtx` 在极端情况下可能已断开并被池复用;
|
||||
与现有终端/屏幕实现同一取舍,靠 `closed` 检查与 `WaitSendIdle` 缩短窗口。
|
||||
- **极慢 VM**:若首条消息仍丢,可把 `kChatOpenSettleMs` 调到 1500–2000ms,代价是
|
||||
`chat_open` 变慢。
|
||||
- **仍是 GBK 通道**:Unicode 对话框解决了「显示」,但线路上仍是 CP936,德语变音
|
||||
字母(`ö`/`ä`/`ß`)、emoji 等无法传输。要根治需引入会话级编码协商(如
|
||||
`TOKEN_CHAT_START` 后追加能力位,双方改用 UTF-8),属未来工作;服务端与客户端
|
||||
必须同时升级,且旧客户端不能误解新报文。
|
||||
- **字体依赖**:中文显示依赖系统已安装的 CJK 字体(字体链接到 "MS Shell Dlg"
|
||||
的后备字体);仅此一点无法由程序保证,缺少字体时显示为方框。
|
||||
@@ -428,6 +428,7 @@ void CMcpSettingsDlg::OnBnClickedBtnSave()
|
||||
- 写操作工具的安全边界评审(操作对象、权限、审计)
|
||||
- 认证升级:复用 `WebServiceAuth` 的签名 token / 过期机制
|
||||
- 分组过滤:`list_online_hosts` 加 `group` 参数
|
||||
- P9 远程交谈(已实现):`chat_open` / `chat_send` / `chat_wait` / `chat_close`,独立开关 `McpChat`(默认 `0`,与只读开关无关);协议不变,客户端聊天窗口改为 Unicode 对话框以支持非中文系统(见 `docs/Mcp_Chat_Design.md`)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -2423,6 +2423,8 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
|
||||
McpServer().SetMachineEnabled(THIS_CFG.GetInt("settings", "McpMachine", 0) != 0);
|
||||
McpServer().SetMachineDangerousEnabled(
|
||||
THIS_CFG.GetInt("settings", "McpMachineDangerous", 0) != 0);
|
||||
// P9 远程交谈(chat_*):独立开关默认关,与只读开关无关。
|
||||
McpServer().SetChatEnabled(THIS_CFG.GetInt("settings", "McpChat", 0) != 0);
|
||||
if (!McpServer().Start(mcpBind, mcpPort)) {
|
||||
Mprintf("McpServer start failed on %s:%d\n", mcpBind.c_str(), mcpPort);
|
||||
} else {
|
||||
@@ -5527,6 +5529,12 @@ BOOL CALLBACK CMy2015RemoteDlg::OfflineProc(CONTEXT_OBJECT* ContextObject)
|
||||
McpServer().OnManagedContextClosed(ContextObject);
|
||||
}
|
||||
|
||||
// MCP P9 远程交谈:子连接(或主连接)断开时清路由/标记会话 closed 并唤醒 chat_wait。
|
||||
// 同样必须在 RemoveFromHostList 之前,避免连接池复用同一 context 指针后误路由。
|
||||
if (McpServer().IsRunning()) {
|
||||
McpServer().OnChatContextClosed(ContextObject);
|
||||
}
|
||||
|
||||
SOCKET nSocket = ContextObject->sClientSocket;
|
||||
|
||||
CDialogBase* p = (CDialogBase*)ContextObject->hDlg;
|
||||
@@ -5889,6 +5897,14 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
||||
return;
|
||||
}
|
||||
|
||||
// ===== MCP 远程交谈:子连接路由(P9)=====
|
||||
// 必须在 P8 之前:聊天文本是裸 GBK,首字节可能是 61(SYSINFOLIST)/255(CONNECTION_LIST),
|
||||
// 若先交给 P8 会被误当作就绪包绑定。已绑定的子连接由 MCP 消费;未绑定的只有
|
||||
// TOKEN_CHAT_START(62) 会被尝试绑定,其余(含 TOKEN_CONN_AUTH=246)原样交回下方分发。
|
||||
if (McpServer().IsRunning() && McpServer().OnChatSubPacket(ContextObject, szBuffer, len)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ===== MCP 主机管理 / 连接管理:子连接路由(P8)=====
|
||||
// 已绑定的子连接上的所有包由 MCP 消费;未绑定的子连接只有就绪包
|
||||
// TOKEN_SYSINFOLIST(61) / TOKEN_CONNECTION_LIST(255) 会被尝试绑定,其余原样交回
|
||||
|
||||
@@ -328,6 +328,7 @@
|
||||
<ClInclude Include="KeyBoardDlg.h" />
|
||||
<ClInclude Include="LangManager.h" />
|
||||
<ClInclude Include="McpServer.h" />
|
||||
<ClInclude Include="McpChat.h" />
|
||||
<ClInclude Include="McpInternal.h" />
|
||||
<ClInclude Include="McpMachine.h" />
|
||||
<ClInclude Include="McpSettingsDlg.h" />
|
||||
@@ -399,6 +400,7 @@
|
||||
<ClCompile Include="WebService.cpp" />
|
||||
<ClCompile Include="HostJson.cpp" />
|
||||
<ClCompile Include="McpServer.cpp" />
|
||||
<ClCompile Include="McpChat.cpp" />
|
||||
<ClCompile Include="McpInternal.cpp" />
|
||||
<ClCompile Include="McpMachine.cpp" />
|
||||
<ClCompile Include="McpSettingsDlg.cpp" />
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
<ClCompile Include="TriggerSettingsDlg.cpp" />
|
||||
<ClCompile Include="HostJson.cpp" />
|
||||
<ClCompile Include="McpServer.cpp" />
|
||||
<ClCompile Include="McpChat.cpp" />
|
||||
<ClCompile Include="McpInternal.cpp" />
|
||||
<ClCompile Include="McpMachine.cpp" />
|
||||
<ClCompile Include="McpSettingsDlg.cpp" />
|
||||
@@ -205,6 +206,7 @@
|
||||
<ClInclude Include="PreviewTipWnd.h" />
|
||||
<ClInclude Include="TriggerSettingsDlg.h" />
|
||||
<ClInclude Include="McpServer.h" />
|
||||
<ClInclude Include="McpChat.h" />
|
||||
<ClInclude Include="McpInternal.h" />
|
||||
<ClInclude Include="McpMachine.h" />
|
||||
<ClInclude Include="McpSettingsDlg.h" />
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
// MCP「远程交谈」工具族:chat_open / chat_send / chat_wait / chat_close。
|
||||
//
|
||||
// 复用 Windows 客户端既有的「远程交谈」链路(协议不变):
|
||||
// 主连接 [COMMAND_CHAT=161]
|
||||
// → 客户端子连接先 [TOKEN_CONN_AUTH=246],再 [TOKEN_CHAT_START=62]
|
||||
// → 服务端在子连接回 [COMMAND_NEXT_CHAT=0](客户端据此弹 IDD_CHAT)
|
||||
// → 双向文本 [CP936 字节][NUL];服务端 [COMMAND_CHAT_CLOSE=1] 结束。
|
||||
//
|
||||
// 会话状态由 CMcpServer 持有(McpServer.h),本文件只负责工具 schema、参数校验、
|
||||
// 编码转换与响应构造。协议、客户端怪癖与验证矩阵见 docs/Mcp_Chat_Design.md。
|
||||
|
||||
#include "jsoncpp/json.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
class CMy2015RemoteDlg;
|
||||
|
||||
namespace McpChat {
|
||||
|
||||
bool Handles(const std::string& toolName);
|
||||
|
||||
// 开关 close 时不追加任何工具(tools/list 与调用侧一致)。
|
||||
void AppendTools(Json::Value& tools);
|
||||
|
||||
std::string Call(const Json::Value& id, const std::string& toolName,
|
||||
const Json::Value& args, CMy2015RemoteDlg* parent);
|
||||
|
||||
} // namespace McpChat
|
||||
@@ -160,6 +160,35 @@ std::string ToAnsi(const std::string& utf8, UINT cp) {
|
||||
return out;
|
||||
}
|
||||
|
||||
bool Utf8ToCpExact(const std::string& utf8, UINT cp, std::string& out) {
|
||||
out.clear();
|
||||
if (utf8.empty()) return true;
|
||||
int wlen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8.c_str(), (int)utf8.size(), NULL, 0);
|
||||
if (wlen <= 0) return false;
|
||||
std::wstring w(wlen, L'\0');
|
||||
if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8.c_str(), (int)utf8.size(),
|
||||
&w[0], wlen) != wlen)
|
||||
return false;
|
||||
|
||||
BOOL usedDefault = FALSE;
|
||||
int alen = WideCharToMultiByte(cp, WC_NO_BEST_FIT_CHARS, w.c_str(), wlen, NULL, 0, NULL, &usedDefault);
|
||||
if (alen <= 0 || usedDefault) return false;
|
||||
out.assign(alen, '\0');
|
||||
usedDefault = FALSE;
|
||||
if (WideCharToMultiByte(cp, WC_NO_BEST_FIT_CHARS, w.c_str(), wlen, &out[0], alen,
|
||||
NULL, &usedDefault) != alen || usedDefault) {
|
||||
out.clear();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsValidUtf8(const char* s, size_t n) {
|
||||
if (!s || n == 0) return true;
|
||||
int wlen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, s, (int)n, NULL, 0);
|
||||
return wlen > 0;
|
||||
}
|
||||
|
||||
void AuditManageAction(CMy2015RemoteDlg* parent, uint64_t devId,
|
||||
const std::string& tool, const std::string& detail) {
|
||||
if (!parent) return;
|
||||
@@ -169,4 +198,13 @@ void AuditManageAction(CMy2015RemoteDlg* parent, uint64_t devId,
|
||||
(LPARAM)new CString(_TR("MCP管理操作")));
|
||||
}
|
||||
|
||||
void AuditChatAction(CMy2015RemoteDlg* parent, uint64_t devId,
|
||||
const std::string& tool, const std::string& detail) {
|
||||
if (!parent) return;
|
||||
std::string text = "host " + std::to_string(devId) + " " + tool + ": " + detail;
|
||||
parent->PostMessageA(WM_SHOWERRORMSG,
|
||||
(WPARAM)new CString(ToAnsi(text, 936).c_str()),
|
||||
(LPARAM)new CString(_TR("MCP远程交谈")));
|
||||
}
|
||||
|
||||
} // namespace McpInternal
|
||||
@@ -52,8 +52,20 @@ size_t BoundedStrlen(const char* p, size_t avail);
|
||||
std::string ToUtf8(const char* s, UINT cp);
|
||||
std::string ToAnsi(const std::string& utf8, UINT cp);
|
||||
|
||||
// 严格 UTF-8 → 目标代码页:任一字符无法表示即失败(不用 '?' 顶替)。
|
||||
// UTF-8 本身非法也失败。聊天消息用它拒绝 GBK 不可表示的字符(emoji 等)。
|
||||
bool Utf8ToCpExact(const std::string& utf8, UINT cp, std::string& out);
|
||||
|
||||
// 校验字节串是否为合法 UTF-8(n=0 视为合法)。
|
||||
bool IsValidUtf8(const char* s, size_t n);
|
||||
|
||||
// 写操作审计:PostMessageA(WM_SHOWERRORMSG) 落到服务端消息/审计日志(不可关闭)。
|
||||
void AuditManageAction(CMy2015RemoteDlg* parent, uint64_t devId,
|
||||
const std::string& tool, const std::string& detail);
|
||||
|
||||
// 远程交谈审计:与 AuditManageAction 同落地方式,标题区分(MCP远程交谈)。
|
||||
// 只记录会话 id / 字节数,绝不记录消息正文。
|
||||
void AuditChatAction(CMy2015RemoteDlg* parent, uint64_t devId,
|
||||
const std::string& tool, const std::string& detail);
|
||||
|
||||
} // namespace McpInternal
|
||||
@@ -110,35 +110,7 @@ std::string OptStr(const Json::Value& args, const char* key) {
|
||||
return Trim(GetStringArg(args, key));
|
||||
}
|
||||
|
||||
// 严格 UTF-8 → 目标代码页:任一字符无法表示即失败(不用 '?' 顶替)。
|
||||
bool Utf8ToCpExact(const std::string& utf8, UINT cp, std::string& out) {
|
||||
out.clear();
|
||||
if (utf8.empty()) return true;
|
||||
int wlen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8.c_str(), (int)utf8.size(), NULL, 0);
|
||||
if (wlen <= 0) return false;
|
||||
std::wstring w(wlen, L'\0');
|
||||
if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8.c_str(), (int)utf8.size(),
|
||||
&w[0], wlen) != wlen)
|
||||
return false;
|
||||
|
||||
BOOL usedDefault = FALSE;
|
||||
int alen = WideCharToMultiByte(cp, WC_NO_BEST_FIT_CHARS, w.c_str(), wlen, NULL, 0, NULL, &usedDefault);
|
||||
if (alen <= 0 || usedDefault) return false;
|
||||
out.assign(alen, '\0');
|
||||
usedDefault = FALSE;
|
||||
if (WideCharToMultiByte(cp, WC_NO_BEST_FIT_CHARS, w.c_str(), wlen, &out[0], alen,
|
||||
NULL, &usedDefault) != alen || usedDefault) {
|
||||
out.clear();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsValidUtf8(const char* s, size_t n) {
|
||||
if (!s || n == 0) return true;
|
||||
int wlen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, s, (int)n, NULL, 0);
|
||||
return wlen > 0;
|
||||
}
|
||||
// Utf8ToCpExact / IsValidUtf8 由 McpInternal 提供(McpChat.cpp 共用)。
|
||||
|
||||
bool IsWindowsClient(context* ctx) {
|
||||
if (!ctx) return false;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "common/file_upload.h" // V2 文件传输(RecvFileChunkV2 / HandleFileCompleteV2 / 包结构)
|
||||
#include "McpInternal.h" // 共享助手(McpMachine.cpp 也使用)
|
||||
#include "McpMachine.h" // 主机管理 / 连接管理工具族
|
||||
#include "McpChat.h" // 远程交谈工具族
|
||||
|
||||
#include <sstream>
|
||||
|
||||
@@ -1490,6 +1491,10 @@ std::string BuildToolsListResult(const Json::Value& id) {
|
||||
// McpMachineDangerous / McpReadonly 逐项判断,关闭时不追加任何工具。
|
||||
McpMachine::AppendTools(tools);
|
||||
|
||||
// 20) 远程交谈(P9:chat_open / chat_send / chat_wait / chat_close)。
|
||||
// 独立开关 McpChat(默认关),与 McpReadonly 无关;关闭时不追加任何工具。
|
||||
McpChat::AppendTools(tools);
|
||||
|
||||
result["tools"] = tools;
|
||||
return BuildResult(id, result);
|
||||
}
|
||||
@@ -4533,6 +4538,9 @@ std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
|
||||
// P8:连接管理 / 主机管理工具族(独立分发;门控在 McpMachine::Call 内复核)。
|
||||
if (McpMachine::Handles(toolName)) return McpMachine::Call(id, toolName, args, parent);
|
||||
|
||||
// P9:远程交谈工具族(独立开关 McpChat;门控在 McpChat::Call 内复核)。
|
||||
if (McpChat::Handles(toolName)) return McpChat::Call(id, toolName, args, parent);
|
||||
|
||||
if (toolName == "list_online_hosts") return BuildListOnlineHosts(id, parent);
|
||||
if (toolName == "search_hosts") return BuildSearchHosts(id, args, parent);
|
||||
if (toolName == "get_host_detail") return BuildGetHostDetail(id, args, parent);
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
|
||||
@@ -37,6 +39,14 @@ struct FileTransferEntry {
|
||||
std::string sha256;
|
||||
};
|
||||
|
||||
// 远程交谈消息(chat_wait 输出 messages[] 的一项)。
|
||||
// text 为 UTF-8;at 为收到消息的 Unix 秒;id 为会话内单调递增序号。
|
||||
struct ChatMessage {
|
||||
uint64_t id = 0;
|
||||
int64_t at = 0;
|
||||
std::string text;
|
||||
};
|
||||
|
||||
// MCP (Model Context Protocol) 服务端:httplib 封装 + JSON-RPC 2.0 分发 + 静态 token 校验。
|
||||
// 与 CWebService 平级、互不依赖;默认禁用,经「扩展 → MCP设置」开启后监听
|
||||
// (默认 127.0.0.1:6544,仅本机回环)。见 docs/Mcp_Design.md。
|
||||
@@ -305,6 +315,57 @@ public:
|
||||
// (吸收迟到的 61/255,避免超时后弹出 MFC 对话框)。
|
||||
void EndManagedSession(uint64_t device_id, bool cancelIo, bool markOrphan);
|
||||
|
||||
// ===== P9:远程交谈(chat_open / chat_send / chat_wait / chat_close)=====
|
||||
// 复用 Windows 客户端既有「远程交谈」:主连接下发 COMMAND_CHAT(161) → 客户端新建子连接
|
||||
// (先 TOKEN_CONN_AUTH(246) 再 TOKEN_CHAT_START(62))→ 服务端在该子连接回
|
||||
// COMMAND_NEXT_CHAT(0) → 客户端弹 IDD_CHAT。之后双向文本为 raw CP936 + NUL。
|
||||
// 协议不变(客户端聊天窗口已改为 Unicode 对话框,仅影响显示);独立 m_ChatMutex,
|
||||
// 不与 P8 的 m_Pending 共享(不受单设备单飞行约束)。
|
||||
// 见 docs/Mcp_Chat_Design.md。
|
||||
void SetChatEnabled(bool enabled) { m_chatEnabled = enabled; }
|
||||
bool IsChatEnabled() const { return m_chatEnabled; }
|
||||
|
||||
// MessageHandle 顶部钩子(MCP 终端之后、P8 之前:聊天文本首字节可能是 61/255/246)。
|
||||
// true = 本包已由 MCP 消费。未绑定子连接上的 246(TOKEN_CONN_AUTH) 走快速路径返回
|
||||
// false,交给认证分支;已绑定子连接上的 0xF6 前导字节文本则由本函数消费。
|
||||
bool OnChatSubPacket(context* subCtx, const BYTE* data, ULONG len);
|
||||
|
||||
// OfflineProc 钩子:接管的子连接断开 → 会话 closed + 清路由 + 唤醒等待者(幂等)。
|
||||
void OnChatContextClosed(context* subCtx);
|
||||
|
||||
// 工具线程:登记会话并发 COMMAND_CHAT 之后调用。false = 该设备已有未关闭会话(-32003)。
|
||||
bool BeginChatOpen(uint64_t device_id, context* mainCtx, const std::string& sessionId);
|
||||
|
||||
// 工具线程:等待客户端子连接就绪且建窗等待期满(readyAt)。
|
||||
enum class ChatReadyResult { Ok, Timeout, Closed, NotFound };
|
||||
ChatReadyResult WaitChatReady(uint64_t device_id, const std::string& sessionId, int timeoutMs);
|
||||
|
||||
// 工具线程:发送前登记(校验 sessionId/started/readyAt/sendBusy)。输出 subCtx。
|
||||
// 返回:0=ok;1=不存在/不匹配/未就绪/已关;2=忙。
|
||||
int BeginChatSend(uint64_t device_id, const std::string& sessionId, context*& subCtx);
|
||||
|
||||
// 工具线程:发送结束(清 sendBusy)。closed 期间发送失败时调用方仍须调用。
|
||||
void FinishChatSend(uint64_t device_id, const std::string& sessionId);
|
||||
|
||||
// 工具线程:取走最多 maxMessages 条;inbox 为空则等到 deadline / closed。
|
||||
enum class ChatWaitResult { Ok, Timeout, Closed, NotFound, Mismatch, Busy };
|
||||
ChatWaitResult WaitChatMessages(uint64_t device_id, const std::string& sessionId,
|
||||
int timeoutMs, size_t maxMessages,
|
||||
std::vector<ChatMessage>& out, bool& timedOut,
|
||||
bool& closed, int& dropped);
|
||||
|
||||
// 关闭会话(校验 sessionId;标记 closed + 清路由 + 唤醒)。返回 0=已关;1=不存在(幂等);
|
||||
// 2=sessionId 不匹配。子连接 [1] 与 CancelIO 由调用方在锁外执行。
|
||||
int CloseChatSession(uint64_t device_id, const std::string& sessionId,
|
||||
context*& subCtxOut, bool& hadSession);
|
||||
|
||||
// chat_open 超时/失败收尾:标记 closed + 清路由;已绑定子连接写入 subCtxOut(调用方 CancelIO),
|
||||
// 未绑定则打 device/peer 5s 墓碑,吸收迟到的 62(避免超时后弹出 MFC 对话框)。
|
||||
void AbortChatOpen(uint64_t device_id, context*& subCtxOut, bool& hadSession);
|
||||
|
||||
// idle 回收:关闭 lastActiveAt 超时且无等待/无发送的会话。返回回收数量。
|
||||
int SweepIdleChats(time_t idleTimeoutSec);
|
||||
|
||||
// 安全配置(启动时由 CMy2015RemoteDlg 读 THIS_CFG 后设置)。
|
||||
void SetReadonly(bool readonly) { m_readonly = readonly; }
|
||||
void SetCmdWhitelist(const std::string& whitelist) { m_cmdWhitelist = whitelist; }
|
||||
@@ -451,6 +512,44 @@ private:
|
||||
std::condition_variable m_FileXferCv;
|
||||
std::map<uint64_t, FileTransferSession> m_FileXferSessions; // device_id → 会话
|
||||
|
||||
// ===== P9:远程交谈(受 m_ChatMutex 保护;单设备单会话)=====
|
||||
// 用 shared_ptr 持有会话:chat_wait / chat_send 在工作线程里复制一份后,即使 map 条目
|
||||
// 被 chat_close 或断线删除也安全,不需要额外的「迭代器有效」规则。
|
||||
struct ChatSession {
|
||||
std::string sessionId;
|
||||
context* mainCtx = nullptr; // 主连接(仅身份校验)
|
||||
uint64_t mainClientId = 0;
|
||||
context* subCtx = nullptr; // 已绑定的子连接;关闭后置空
|
||||
std::string peer; // GetPeerName()(老客户端 clientID=0 兜底)
|
||||
bool started = false; // 已收到 TOKEN_CHAT_START 并回 [0]
|
||||
bool closed = false;
|
||||
bool sendBusy = false; // 同一会话串行发送
|
||||
bool waitBusy = false; // 同一会话只允许一个 chat_wait
|
||||
std::chrono::steady_clock::time_point readyAt; // 建窗等待期满(绑定 + 1000ms)
|
||||
time_t lastActiveAt = 0;
|
||||
std::deque<ChatMessage> inbox; // 未投递消息(UTF-8)
|
||||
size_t inboxBytes = 0;
|
||||
uint64_t nextMsgId = 1;
|
||||
int dropped = 0; // inbox 溢出丢弃的条数(chat_wait 报告)
|
||||
};
|
||||
using ChatSessionPtr = std::shared_ptr<ChatSession>;
|
||||
|
||||
std::mutex m_ChatMutex;
|
||||
std::condition_variable m_ChatCv;
|
||||
std::map<uint64_t, ChatSessionPtr> m_ChatSessions; // device_id → 会话
|
||||
std::map<context*, uint64_t> m_ChatContextToDevice; // subCtx → device_id(顶部路由)
|
||||
std::set<context*> m_ChatAbsorbedSubCtx; // CancelIO 后尾巴包静默丢弃
|
||||
struct ChatOrphanMark { std::chrono::steady_clock::time_point expiry; };
|
||||
std::map<uint64_t, ChatOrphanMark> m_ChatOrphanByDevice; // 迟到 62 墓碑(5s)
|
||||
std::map<std::string, ChatOrphanMark> m_ChatOrphanByPeer;
|
||||
std::atomic<int> m_ChatRoutes{0}; // 路由集合大小(快速路径)
|
||||
bool m_chatEnabled = false; // P9 开关(默认关)
|
||||
|
||||
// 持 m_ChatMutex 调用:只更新 m_ChatRoutes(store,relaxed 读取在锁外)。
|
||||
void RefreshChatRoutesLocked();
|
||||
// 持 m_ChatMutex 调用:清理过期墓碑。
|
||||
void PruneChatOrphansLocked(std::chrono::steady_clock::time_point now);
|
||||
|
||||
bool m_readonly = true;
|
||||
std::string m_cmdWhitelist;
|
||||
bool m_terminalEnabled = false; // 持久终端开关(默认关;要求 m_readonly=false)
|
||||
|
||||
@@ -106,7 +106,7 @@ INT_PTR CMcpSettingsDlg::DoModal()
|
||||
{
|
||||
USES_CONVERSION;
|
||||
CString title = _TR("MCP设置");
|
||||
BuildDialogTemplate(m_Template, T2CW(title), 320, 480);
|
||||
BuildDialogTemplate(m_Template, T2CW(title), 320, 510);
|
||||
InitModalIndirect((LPCDLGTEMPLATE)m_Template.data());
|
||||
return CDialog::DoModal();
|
||||
}
|
||||
@@ -154,6 +154,9 @@ BOOL CMcpSettingsDlg::OnInitDialog()
|
||||
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,
|
||||
@@ -180,6 +183,7 @@ BOOL CMcpSettingsDlg::OnInitDialog()
|
||||
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));
|
||||
@@ -199,6 +203,7 @@ BOOL CMcpSettingsDlg::OnInitDialog()
|
||||
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);
|
||||
@@ -212,6 +217,7 @@ BOOL CMcpSettingsDlg::OnInitDialog()
|
||||
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()));
|
||||
|
||||
@@ -234,6 +240,7 @@ void CMcpSettingsDlg::OnOK()
|
||||
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) {
|
||||
@@ -271,6 +278,7 @@ void CMcpSettingsDlg::OnOK()
|
||||
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);
|
||||
@@ -279,7 +287,8 @@ void CMcpSettingsDlg::OnOK()
|
||||
MessageBox(_TR("MCP 设置已保存。") + _T("\r\n") +
|
||||
_TR("启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作/主机管理/危险主机操作的改动需重启程序生效。") + _T("\r\n") +
|
||||
_TR("持久终端、远程控制与管理操作仅在只读模式关闭时生效。") + _T("\r\n") +
|
||||
_TR("主机管理的写操作需只读模式关闭;冻结/解冻进程、停止/重启服务还需启用危险主机操作。"),
|
||||
_TR("主机管理的写操作需只读模式关闭;冻结/解冻进程、停止/重启服务还需启用危险主机操作。") + _T("\r\n") +
|
||||
_TR("远程交谈开关的改动需重启程序生效。"),
|
||||
_TR("提示"), MB_ICONINFORMATION);
|
||||
|
||||
CDialog::OnOK();
|
||||
@@ -331,6 +340,9 @@ void CMcpSettingsDlg::LayoutControls(int cx, int cy)
|
||||
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);
|
||||
|
||||
@@ -34,6 +34,7 @@ private:
|
||||
IDC_MCP_MANAGE = 1010, // 「启用管理操作」复选框(kill/窗口/服务/电源,要求只读关)
|
||||
IDC_MCP_MACHINE = 1011, // 「启用主机管理」复选框(host_* 工具族)
|
||||
IDC_MCP_MACHINE_DANGEROUS = 1012, // 「启用危险主机操作」复选框(冻结/停止/重启)
|
||||
IDC_MCP_CHAT = 1013, // 「启用远程交谈」复选框(chat_* 工具族,不要求只读关)
|
||||
};
|
||||
|
||||
CButton m_btnEnable;
|
||||
@@ -46,6 +47,7 @@ private:
|
||||
CButton m_btnManage;
|
||||
CButton m_btnMachine;
|
||||
CButton m_btnMachineDangerous;
|
||||
CButton m_btnChat;
|
||||
CStatic m_lblWhitelist;
|
||||
CEdit m_editWhitelist;
|
||||
CButton m_btnOK, m_btnCancel;
|
||||
|
||||
@@ -2009,6 +2009,9 @@ Token=Token
|
||||
启用主机管理(进程/窗口/服务/任务/网络状态)=Maschinenverwaltung aktivieren (Prozess/Fenster/Dienst/Aufgabe/Netzwerkstatus)
|
||||
启用危险主机操作(冻结/解冻进程、停止/重启服务)=Gefaehrliche Maschinenoperationen aktivieren (Prozess einfrieren/auftauen, Dienst stoppen/neu starten)
|
||||
危险主机操作依附于主机管理:请先启用主机管理。=Gefaehrliche Maschinenoperationen setzen die Maschinenverwaltung voraus. Aktivieren Sie zuerst die Maschinenverwaltung.
|
||||
启用远程交谈(AI 与用户对话)=Fernchat aktivieren (KI spricht mit dem Benutzer)
|
||||
MCP远程交谈=MCP-Fernchat
|
||||
远程交谈开关的改动需重启程序生效。=Aenderungen am Fernchat-Schalter werden nach einem Neustart wirksam.
|
||||
主机管理的写操作需只读模式关闭;冻结/解冻进程、停止/重启服务还需启用危险主机操作。=Schreiboperationen der Maschinenverwaltung erfordern deaktivierten Nur-Lesen-Modus; Einfrieren/Auftauen von Prozessen und Stoppen/Neustarten von Diensten erfordern zusaetzlich gefaehrliche Maschinenoperationen.
|
||||
第 %d 个钱包地址不合法!=Wallet-Adresse #%d ist ungueltig!
|
||||
%d 新连接\r\n=%d Neue Verbindung\r\n
|
||||
|
||||
@@ -2014,6 +2014,9 @@ Token=Token
|
||||
启用主机管理(进程/窗口/服务/任务/网络状态)=Enable host management (processes/windows/services/tasks/network state)
|
||||
启用危险主机操作(冻结/解冻进程、停止/重启服务)=Enable dangerous host operations (freeze/thaw process, stop/restart service)
|
||||
危险主机操作依附于主机管理:请先启用主机管理。=Dangerous host operations depend on host management. Enable host management first.
|
||||
启用远程交谈(AI 与用户对话)=Enable remote chat (AI talks to the user)
|
||||
MCP远程交谈=MCP remote chat
|
||||
远程交谈开关的改动需重启程序生效。=Changes to the remote chat switch take effect after restart.
|
||||
主机管理的写操作需只读模式关闭;冻结/解冻进程、停止/重启服务还需启用危险主机操作。=Host management write operations require read-only mode to be off; freezing/thawing processes and stopping/restarting services also require dangerous host operations.
|
||||
第 %d 个钱包地址不合法!=Wallet address #%d is invalid!
|
||||
%d 新连接\r\n=%d New connection\r\n
|
||||
|
||||
@@ -2005,6 +2005,9 @@ Token=Token
|
||||
启用主机管理(进程/窗口/服务/任务/网络状态)=啟用主機管理(處理程序/視窗/服務/排程工作/網路狀態)
|
||||
启用危险主机操作(冻结/解冻进程、停止/重启服务)=啟用危險主機操作(凍結/解凍處理程序、停止/重新啟動服務)
|
||||
危险主机操作依附于主机管理:请先启用主机管理。=危險主機操作依附於主機管理:請先啟用主機管理。
|
||||
启用远程交谈(AI 与用户对话)=啟用遠端交談(AI 與使用者對話)
|
||||
MCP远程交谈=MCP遠端交談
|
||||
远程交谈开关的改动需重启程序生效。=遠端交談開關的變更需重新啟動程式後生效。
|
||||
主机管理的写操作需只读模式关闭;冻结/解冻进程、停止/重启服务还需启用危险主机操作。=主機管理的寫入操作需關閉唯讀模式;凍結/解凍處理程序、停止/重新啟動服務還需啟用危險主機操作。
|
||||
第 %d 个钱包地址不合法!=第 %d 個錢包位址不合法!
|
||||
%d 新连接\r\n=%d 新連線\r\n
|
||||
|
||||
Reference in new issue
Block a user