2 Commits
Author SHA1 Message Date
yuanyuanxiang b41b27bfe4 Release v1.3.9 2026-09-02 19:13:40 +02:00
yuanyuanxianganddeepseek-v4-pro 6bcd593b6d Feature: Add upload_file MCP tool (V2 protocol, main-connection upload)
Implement the P2 upload_file tool to push a local file or directory from the
master to an online Windows host over the existing V2 file-transfer protocol.
The server drives FileBatchTransferWorkerV2 synchronously on the main
connection through a headless callback, reuses the list_files chain for the
overwrite pre-check, and is gated by McpFileTransfer=1 plus McpReadonly=0.

The client main connection never initialized the file-transfer module, so
g_status stayed 0 and RecvFileChunkV2 silently dropped every chunk, truncating
uploads to zero bytes. Add a once-per-process lazy InitFileUpload in the
COMMAND_SEND_FILE_V2 handler that mirrors the FileManager init; the destructor
deliberately does not Uninit so g_status remains 1 across reconnects.

Update the design doc to record the client-side change and correct the
upload_file description to state that sha256 is not returned (V2 has no
receiver-to-sender ACK; integrity is checked client-side and logged only).

Co-Authored-By: deepseek-v4-pro
2026-08-31 22:43:16 +02:00
16 changed files with 656 additions and 22 deletions

No files matched your search

+28
View File
@@ -378,6 +378,34 @@ nohup ./server_linux_amd64 --port 6543 --http-port 9001 > yama.log 2>&1 &
## 更新日志 ## 更新日志
### v1.3.9 (2026.9.2)
**MCP 服务端集成(AI 远程管理)& 远程控制与文件传输闭环 & 主控增强**
**新功能:**
- **MCP 服务端集成**:本地 `127.0.0.1:6544` 暴露 JSON-RPC 2.0 接口,Bearer Token 鉴权,「扩展 > MCP 设置」对话框配置启用 / 端口 / 绑定 / Token;23 个工具覆盖「观测 → 执行 → 控制 → 传输」全链路,默认关闭、分级开关(只读 / 白名单 / 终端 / 远程控制 / 文件传输)全面审计
- **MCP 观测类工具(只读)**:search_hosts、get_host_detail、list_processes、list_windows、get_activity_history、list_files、get_screenshot(可选 max_width)、list_services、get_client_log、list_registry、get_audit_log;单飞请求注册表 20s 超时 + 编码修正(clientType / clientID 定位)
- **MCP 远程终端工具**:exec_command 一次性执行 + terminal_open / terminal_exec / terminal_close 持久终端(cwd / 环境保留,空闲 300s 清扫),哨兵判定与 timeout_ms 解析修复
- **MCP 远程控制工具**:remote_open / remote_close 会话 + remote_keyboard(key_down / up / press / type)、remote_mouse(move / click / drag / scroll)、remote_clipboard 注入;与人工 Web 查看双向互斥,会话状态机 busy / closed 加固
- **MCP 文件传输工具**:download_file(V2 协议 + SHA-256 校验)、upload_file(主连接上传),客户端侧 `COMMAND_SEND_FILE_V2` 懒初始化 `InitFileUpload` 修复上传截断
- **客户端活动历史**:记录活跃窗口历史(上限 500 条、跳过 <5s),主机「客户端管理」子菜单新增「活动历史」对话框,Unicode 编辑控件正确渲染 UTF-8 标题
- **日志右键菜单三连**:消息日志右键新增「在线通知」「拉黑 IP」「解封 IP」,IPv4 提取即时生效
- **列表排序记忆**:主机列表、进程 / 窗口列表排序偏好持久化(`list\*Sort` 注册表键),下次启动默认沿用
**改进:**
- 重复连接 standby 处理:重复登录暂存 standby 待旧连接超时 / 断线后晋升,避免循环重连与误报离线
- 远程桌面 / 虚拟屏幕对话框标题显示主机备注而非 IP(无备注回退 IP)
- 主机列表三态排序(升序 → 降序 → 恢复默认,默认序按在线时间重建)
- 在线主机右键三个菜单项新增图标(位图数组 63→66)
**Bug 修复:**
- 客户端屏幕控制批解析错误:记录数是 7 的倍数时误判 MSG32 致注入错乱(`%48==0` 优先)
- 心跳延迟更新 use-after-free:跨 PostMessage 持有 IoRefCount + memcpy 显式截断
- ONLINELIST_PRIVILEGE 枚举错位损坏历史数据库(权限列移至 16)
- 远程桌面光标跨对话框污染(改 `OnSetCursor` 逐窗口路由)
- 客户端日志对话框 EDIT 上限停止刷新(`SetLimitText(0)`)
- 窗口管理器视图检查读取陈旧状态、进程列表架构列排序无效、FRP / NAT 下窗口标题乱码
### v1.3.8 (2026.8.3) ### v1.3.8 (2026.8.3)
**SYSTEM 权限客户端 & 主控可用性增强 & Bug 修复** **SYSTEM 权限客户端 & 主控可用性增强 & Bug 修复**
+28
View File
@@ -378,6 +378,34 @@ Valid : 2026-02-01 to 2028-02-01
## Changelog ## Changelog
### v1.3.9 (2026.9.2)
**MCP Server Integration (AI Remote Management) & Remote Control / File Transfer Loop & Master Enhancements**
**New features:**
- **MCP server integration**: local JSON-RPC 2.0 endpoint on `127.0.0.1:6544` with Bearer token auth, configured via "Extensions > MCP Settings"; 23 tools spanning observe → execute → control → transfer, disabled by default with per-tier gates (read-only / whitelist / terminal / remote control / file transfer) and full audit logging
- **MCP read-only observation tools**: search_hosts, get_host_detail, list_processes, list_windows, get_activity_history, list_files, get_screenshot (optional max_width), list_services, get_client_log, list_registry, get_audit_log; single-flight pending registry with 20s timeout, plus encoding fixes (clientType / clientID resolution)
- **MCP remote terminal tools**: one-shot exec_command plus persistent terminal_open / terminal_exec / terminal_close (cwd and environment preserved, 300s idle sweep); sentinel detection and timeout_ms parsing fixes
- **MCP remote control tools**: remote_open / remote_close sessions plus remote_keyboard (key_down / up / press / type), remote_mouse (move / click / drag / scroll), and remote_clipboard injection; two-way mutual exclusion with human Web viewing and a hardened busy/closed session state machine
- **MCP file transfer tools**: download_file (V2 protocol + SHA-256 verification) and upload_file (main-connection upload); client-side lazy `InitFileUpload` in `COMMAND_SEND_FILE_V2` fixes truncated uploads
- **Client activity history**: records active-window history (capped at 500 entries, skips <5s); new "Activity History" item under the host's Client Management menu with a Unicode edit control for correct UTF-8 title rendering
- **Log context-menu trio**: right-click "Online Notify", "Ban IP", and "Unban IP" on the message log, with IPv4 extraction applied immediately
- **Sort preference memory**: host list and process/window list sort preferences persisted (`list\*Sort` registry keys), restored on next launch
**Improvements:**
- Duplicate connections parked as standby and promoted after the old connection times out / disconnects, avoiding reconnect loops and false offline notifications
- Remote desktop and virtual screen dialog titles show the host remark instead of the IP (falling back to IP)
- Three-state host list sorting (ascending → descending → restore default, default rebuilt by online time)
- Icons added to three online-host context menu items (bitmap array 63→66)
**Bug fixes:**
- Client screen-control batch misparse: record count that is a multiple of 7 was misclassified as MSG32, garbling injected input (`%48==0` checked first)
- Heartbeat deferred-update use-after-free: IoRefCount held across PostMessage plus explicit memcpy truncation
- ONLINELIST_PRIVILEGE enum shift corrupted the history host database (column moved to 16)
- Remote desktop cursor cross-contamination across dialogs (routed via `OnSetCursor` per window)
- Client log dialog stopped refreshing past the EDIT control limit (`SetLimitText(0)`)
- Window manager view check reading stale state, process-list architecture column sort no-op, and FRP/NAT window-title mojibake
### v1.3.8 (2026.8.3) ### v1.3.8 (2026.8.3)
**SYSTEM-level Client & Master Usability Enhancements & Bug Fixes** **SYSTEM-level Client & Master Usability Enhancements & Bug Fixes**
+28
View File
@@ -378,6 +378,34 @@ nohup ./server_linux_amd64 --port 6543 --http-port 9001 > yama.log 2>&1 &
## 更新日誌 ## 更新日誌
### v1.3.9 (2026.9.2)
**MCP 伺服端整合(AI 遠端管理)& 遠端控制與檔案傳輸閉環 & 主控增強**
**新功能:**
- **MCP 伺服端整合**:本地 `127.0.0.1:6544` 暴露 JSON-RPC 2.0 介面,Bearer Token 鑑權,「擴充 > MCP 設定」對話框設定啟用 / 連接埠 / 綁定 / Token;23 個工具覆蓋「觀測 → 執行 → 控制 → 傳輸」全鏈路,預設關閉、分級開關(唯讀 / 白名單 / 終端 / 遠端控制 / 檔案傳輸)全面稽核
- **MCP 觀測類工具(唯讀)**:search_hosts、get_host_detail、list_processes、list_windows、get_activity_history、list_files、get_screenshot(可選 max_width)、list_services、get_client_log、list_registry、get_audit_log;單飛請求登錄表 20s 逾時 + 編碼修正(clientType / clientID 定位)
- **MCP 遠端終端工具**:exec_command 一次性執行 + terminal_open / terminal_exec / terminal_close 持久終端(cwd / 環境保留,閒置 300s 清掃),哨兵判定與 timeout_ms 解析修復
- **MCP 遠端控制工具**:remote_open / remote_close 工作階段 + remote_keyboard(key_down / up / press / type)、remote_mouse(move / click / drag / scroll)、remote_clipboard 注入;與人工 Web 查看雙向互斥,工作階段狀態機 busy / closed 加固
- **MCP 檔案傳輸工具**:download_file(V2 協定 + SHA-256 校驗)、upload_file(主連線上傳),用戶端側 `COMMAND_SEND_FILE_V2` 懶初始化 `InitFileUpload` 修復上傳截斷
- **用戶端活動歷史**:記錄活躍視窗歷史(上限 500 條、跳過 <5s),主機「用戶端管理」子選單新增「活動歷史」對話框,Unicode 編輯控件正確渲染 UTF-8 標題
- **訊息右鍵選單三連**:訊息記錄右鍵新增「線上通知」「封鎖 IP」「解封 IP」,IPv4 提取即時生效
- **列表排序記憶**:主機列表、處理程序 / 視窗列表排序偏好持久化(`list\*Sort` 登錄檔鍵),下次啟動預設沿用
**改進:**
- 重複連線 standby 處理:重複登入暫存 standby 待舊連線逾時 / 斷線後晉升,避免循環重連與誤報離線
- 遠端桌面 / 虛擬螢幕對話框標題顯示主機備註而非 IP(無備註回退 IP)
- 主機列表三態排序(升冪 → 降冪 → 恢復預設,預設序按上線時間重建)
- 線上主機右鍵三個選單項目新增圖示(位圖陣列 63→66)
**Bug 修復:**
- 用戶端螢幕控制批次解析錯誤:記錄數是 7 的倍數時誤判 MSG32 致注入錯亂(`%48==0` 優先)
- 心跳延遲更新 use-after-free:跨 PostMessage 持有 IoRefCount + memcpy 顯式截斷
- ONLINELIST_PRIVILEGE 列舉錯位損壞歷史資料庫(權限列移至 16)
- 遠端桌面游標跨對話框污染(改 `OnSetCursor` 逐視窗路由)
- 用戶端記錄對話框 EDIT 上限停止刷新(`SetLimitText(0)`)
- 視窗管理員檢視檢查讀取陳舊狀態、處理程序列表架構欄排序無效、FRP / NAT 下視窗標題亂碼
### v1.3.8 (2026.8.3) ### v1.3.8 (2026.8.3)
**SYSTEM 權限用戶端 & 主控可用性增強 & Bug 修復** **SYSTEM 權限用戶端 & 主控可用性增強 & Bug 修復**
+7
View File
@@ -1379,6 +1379,13 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
} }
case COMMAND_SEND_FILE_V2: { case COMMAND_SEND_FILE_V2: {
// 主连接接收 V2 上传:确保文件传输模块已初始化(RecvFileChunkV2 依赖 g_status==1)。
// static 标志保证进程内只初始化一次,不随主连接重连反复 Init/Uninit。
static bool s_v2RecvInited = false;
if (!s_v2RecvInited) {
InitFileUpload({}, m_LoginMsg, m_LoginSignature, 64, 50, Logf);
s_v2RecvInited = true;
}
// C2C/V2 文件接收(RecvFileChunkV2 内部会打印进度) // C2C/V2 文件接收(RecvFileChunkV2 内部会打印进度)
int n = RecvFileChunkV2((char*)szBuffer, ulLength, m_conn, int n = RecvFileChunkV2((char*)szBuffer, ulLength, m_conn,
nullptr, m_hash, m_hmac, m_MyClientID); nullptr, m_hash, m_hmac, m_MyClientID);
+2 -2
View File
@@ -88,7 +88,7 @@ IDR_WAVE WAVE "Res\\msg.wav"
// //
VS_VERSION_INFO VERSIONINFO VS_VERSION_INFO VERSIONINFO
FILEVERSION 1,0,3,8 FILEVERSION 1,0,3,9
PRODUCTVERSION 1,0,0,1 PRODUCTVERSION 1,0,0,1
FILEFLAGSMASK 0x3fL FILEFLAGSMASK 0x3fL
#ifdef _DEBUG #ifdef _DEBUG
@@ -106,7 +106,7 @@ BEGIN
BEGIN BEGIN
VALUE "CompanyName", "FUCK THE UNIVERSE" VALUE "CompanyName", "FUCK THE UNIVERSE"
VALUE "FileDescription", "A GHOST" VALUE "FileDescription", "A GHOST"
VALUE "FileVersion", "1.0.3.8" VALUE "FileVersion", "1.0.3.9"
VALUE "InternalName", "ServerDll.dll" VALUE "InternalName", "ServerDll.dll"
VALUE "LegalCopyright", "Copyright (C) 2019-2026" VALUE "LegalCopyright", "Copyright (C) 2019-2026"
VALUE "OriginalFilename", "ServerDll.dll" VALUE "OriginalFilename", "ServerDll.dll"
BIN
View File
Binary file not shown.
+122 -10
View File
@@ -1,6 +1,6 @@
# YAMA MCP 文件传输功能设计(download_file / upload_file) # YAMA MCP 文件传输功能设计(download_file / upload_file)
> **状态**:设计定稿(经专家审查修订,结论见 §13)。`download_file` 采用 **V2 文件传输协议**(`CMD_DOWN_FILES_V2` + 流式子连接 + SHA-256 校验),`upload_file` 复用服务端既有 `FileBatchTransferWorkerV2`。两者共享同一套「流式文件会话」注册表。 > **状态**:设计定稿(经专家审查修订,结论见 §13)。`download_file`(P1)已实施并验收;`upload_file`(P2)已实施(详见 §7,评审修订见 §13.1)。`download_file` 采用 **V2 文件传输协议**(`CMD_DOWN_FILES_V2` + 流式子连接 + SHA-256 校验),`upload_file` 复用服务端既有 `FileBatchTransferWorkerV2`(主连接同步驱动)。两者共享同一套「文件会话」注册表。
> **读者**:MCP 后续功能研发/评审人员。 > **读者**:MCP 后续功能研发/评审人员。
> **关联文档**:[Mcp_Phase2_Design.md](./Mcp_Phase2_Design.md)(双模式无头驱动机制)、[FILE_TRANSFER_V2.md](./FILE_TRANSFER_V2.md)(V2 协议清单)、[Mcp_Design.md](./Mcp_Design.md)(Phase 1 协议/架构/配置)。 > **关联文档**:[Mcp_Phase2_Design.md](./Mcp_Phase2_Design.md)(双模式无头驱动机制)、[FILE_TRANSFER_V2.md](./FILE_TRANSFER_V2.md)(V2 协议清单)、[Mcp_Design.md](./Mcp_Design.md)(Phase 1 协议/架构/配置)。
@@ -54,7 +54,7 @@ MCP 目前已暴露 `list_files`(只读列目录,`McpServer.cpp:1741`),
## 4. 设计原则 ## 4. 设计原则
1. **复用现有协议,不新造命令**:`CMD_DOWN_FILES_V2`、`COMMAND_SEND_FILE_V2`、`COMMAND_FILE_COMPLETE_V2`、`COMMAND_LIST_DRIVE` 全部为 `common/commands.h` 既有;客户端零改动。 1. **复用现有协议,不新造命令**:`CMD_DOWN_FILES_V2`、`COMMAND_SEND_FILE_V2`、`COMMAND_FILE_COMPLETE_V2`、`COMMAND_LIST_DRIVE` 全部为 `common/commands.h` 既有;客户端零改动(upload 方向例外:`COMMAND_SEND_FILE_V2` 需懒初始化文件模块,见 §7.8)。
2. **无头接管复用会话模式**:镜像 `McpServer.h` 的 `TermSession` / `ScreenCtrlSession`,新增 `FileTransferSession`;`MessageHandle` 用 `IsFileTransferContext(context*)` 判定是否路由到 MCP 无头落盘,否则回落 GUI 进度框。 2. **无头接管复用会话模式**:镜像 `McpServer.h` 的 `TermSession` / `ScreenCtrlSession`,新增 `FileTransferSession`;`MessageHandle` 用 `IsFileTransferContext(context*)` 判定是否路由到 MCP 无头落盘,否则回落 GUI 进度框。
3. **单设备单传输会话**:与终端/远程控制一致,避免同 host 并发传输的归属歧义;并发返回 `-32003 Device busy`。 3. **单设备单传输会话**:与终端/远程控制一致,避免同 host 并发传输的归属歧义;并发返回 `-32003 Device busy`。
4. **超时与清理是硬约束**:大文件传输是长任务,超时需独立于 `kMcpToolTimeoutMs`(20s);断线/超时必须清会话 + 关子链接 + 删半成品文件。 4. **超时与清理是硬约束**:大文件传输是长任务,超时需独立于 `kMcpToolTimeoutMs`(20s);断线/超时必须清会话 + 关子链接 + 删半成品文件。
@@ -186,12 +186,102 @@ MCP工具线程 服务端 MessageHandle
--- ---
## 7. `upload_file` 设计(P2,未来) ## 7. `upload_file` 设计(P2)
- **复用服务端 sender**:`2015RemoteDlg.cpp:7572` 已有 `FileBatchTransferWorkerV2(files, targetDir, ..., SendFileChunkToClientV2, ...)`,服务端读本机文件分块 `COMMAND_SEND_FILE_V2` 推给客户端;客户端 `RecvFileChunkV2` 落盘并回 `COMMAND_FILE_COMPLETE_V2`。 ### 7.1 工具 Schema
- **比 download 更简单**:服务端是发送方,工具线程直接驱动 `FileBatchTransferWorkerV2`,无需等待外来流;只需等客户端回 `COMMAND_FILE_COMPLETE_V2`(复用同一文件会话注册表)。
- **Schema**(§3.2 已列):`id` + `local_path` + `remote_dir` + `overwrite` + `timeout_ms`。 ```
- **安全**:写远程盘,独立评审;`remote_dir` 路径规范化,可选系统目录黑名单。 input: {
id: string (必填, 目标主机 id)
local_path: string (必填, 主控本机文件或目录绝对路径; 目录递归上传)
remote_dir: string (必填, 远程主机保存目录; 不存在会自动创建)
overwrite: boolean (可选, 默认 false; true 覆盖同名文件, false 跳过)
timeout_ms: integer (可选, 默认 600000, 上限 3600000)
}
output: {
files: [{ path: string, size: integer, sha256: string }] // 已发送文件(远程完整路径);P2 sha256 恒为空串(见 §7.5)
total_bytes: integer
skipped: integer
}
```
### 7.2 方向与连接(与 download 的关键差异)
| 维度 | download_file(P1) | upload_file(P2) |
|---|---|---|
| 数据方向 | 远程客户端 → 主控 | 主控 → 远程客户端 |
| 发送方 | 客户端(`UploadToRemoteV2` → `FileBatchTransferWorkerV2`) | 主控(`FileBatchTransferWorkerV2`) |
| 连接 | 客户端**新开一条鉴权流式子连接** | 复用**主连接**(`ctx->Send2Client`),无子连接 |
| 完成信号 | 客户端发 `COMMAND_FILE_COMPLETE_V2`,服务端 `OnFileCompleteV2` 校验计数 | 服务端**作为发送方**自己发 `COMMAND_FILE_COMPLETE_V2`(SHA-256),客户端 `HandleFileCompleteV2` 校验 |
| MessageHandle 改动 | 2 处守卫分支 + `OnDriveList` 扩展 | **零改动**(客户端 `KernelManager.cpp:1381` 接收,但需懒初始化文件模块,见 §7.8) |
> **修正旧稿(§13.1 评审)**:旧稿写「只需等客户端回 `COMMAND_FILE_COMPLETE_V2`」是错的。V2 协议里 COMPLETE 包恒由**发送方**发、接收方校验,**无接收方→发送方 ACK**。upload 的服务端是发送方,故它**发** COMPLETE 而非「等」。这也意味着 upload 比 download 更简单——无需路由外来流式子连接、无需 `OnFileCompleteV2` 计数。
### 7.3 时序
```
MCP工具线程 主连接 (ctx→Send2Client) 客户端 KernelManager
1. 校验 McpFileTransfer=1 && McpReadonly=0(§8)
2. CollectLocalFiles({local_path}) 收集本机文件+目录项(目录项在前、子项随后)
3. (overwrite=false) list_files 预检 remote_dir 一层 → 得已存在顶层名 → skip 列表
4. BeginFileUpload(id) 登记会话(单设备单传输互斥标记,§6.5)
5. 同步驱动(工具线程内):FileBatchTransferWorkerV2(files, remote_dir, cb, f, hash, hmac, opts)
6. ── COMMAND_SEND_FILE_V2 分块流 ───► RecvFileChunkV2 落盘
7. ── COMMAND_FILE_COMPLETE_V2(SHA256)► HandleFileCompleteV2 校验
8. worker 同步返回 → ClearFileTransfer → 返回 files/total_bytes/skipped
```
### 7.4 服务端改动
1. **`McpServer.h`**:新增 `BeginFileUpload(id)`,登记 `FileTransferSession{tool="upload_file", startAt}` 作为**单设备单传输互斥标记**(与 `m_Pending`/`m_FileXferSessions` 互斥,见 §6.5)。upload 走主连接、工具线程**同步**驱动 `FileBatchTransferWorkerV2`,会话**无** `fmSubCtx`/`streamSubCtx`/`fileEntries`/`done` 等字段(`ClearFileTransfer` 对空指针安全)。
2. **`McpServer.cpp`**:
- `BuildUploadFileInputSchema/OutputSchema` + `BuildUploadFile(...)`(`tools/call` 分派);
- 无头回调 `UploadSendChunkHeadless(user, chunk, data, size)`:经 `UploadCallbackData{parent,clientID,deadline}` 里 `parent->FindHost(clientID)` 定位 ctx → `ctx->Send2Client(data,size)`;客户端离线或整体超时返回 false 中止(镜像 GUI `SendFileChunkToClientV2`,去掉 `dlg` 进度,见 `2015RemoteDlg.cpp:7424-7459`);
- 收集:本地 `CollectLocalFiles` 递归(`common/file_upload.cpp:38` 的 `ExpandDirectories` 未在 `file_upload.h` 导出,故在 McpServer.cpp 本地同构实现,目录项在前、子项随后);
- overwrite 预检:复用 `list_files` 的 `COMMAND_LIST_DRIVE→TOKEN_DRIVE_LIST→COMMAND_LIST_FILES→TOKEN_FILE_LIST` 机制列 `remote_dir` **一层**(顶层名,不递归),过滤同名 → `skipped`;
- 驱动:`FileBatchTransferWorkerV2(files, remote_dir, &cb, headlessCallback, nullptr, GetPwdHash(), GetHMAC(100), opts)`;`opts.transferID=GenerateTransferID()`、`srcClientID=0`、`dstClientID=clientID`、`enableResume=false`;
- 完成判定:`result==0 && FindHost(clientID)!=nullptr`(镜像 GUI `SendFilesToClientV2Internal` 末尾)。
3. **`2015RemoteDlg.cpp`(MessageHandle)**:**零改动**。
4. **`McpSettingsDlg.cpp/.h`**:零改动(`McpFileTransfer` 已存在,§8 复用)。
### 7.5 完整性边界(诚实声明)
- V2 无接收方→发送方 ACK:客户端 `HandleFileCompleteV2` 校验失败只打日志(`RecvFileChunkV2` 返回 8,`KernelManager.cpp:1381` 不回传主控)。
- **P2 的 `output.files[].sha256` 恒为空串**:`FileBatchTransferWorkerV2` 内部会为每个文件自算 SHA-256 并写入 `COMMAND_FILE_COMPLETE_V2` 包发给客户端,但该值**不回传调用方**;服务端侧又无导出的 SHA-256 工具函数可自行复算。故 P2 输出不填 sha256(延后 P3:worker 回传哈希,或服务端引入 SHA-256 工具)。传输完整性仍依赖客户端本地 `HandleFileCompleteV2` 校验(与 GUI upload / C2C 同权,属 V2 协议既有边界,非 MCP 引入)。
- 若未来需要「接收方验真回执」,需新增反向 ACK 包(协议扩展,进 P3)。
### 7.6 overwrite 语义
- `overwrite=false`(默认):发送前用 `list_files` 预检 `remote_dir` **一层**(顶层名,不递归),本地顶层项名已存在者整体跳过(单文件精确、目录整体跳过),文件条目计入 `skipped`(目录项不计)。编码 UTF-8→ANSI(936),与 `list_files`/`download_file` 一致。
- `overwrite=true`:全量发送(客户端 `RecvFileChunkV2` 覆盖写)。
- 代价:一次预检往返;对称于 download 的「本地同名跳过」语义。
### 7.7 安全
- 门槛 `McpFileTransfer=1 && McpReadonly=0`(§8):upload 写远程盘,复用「允许写」主开关。
- `remote_dir` 路径规范化(防 `..` 穿越到预期目录之外)。`local_path` 是主控本机路径(信任本地文件系统),主要风险是 AI 误推敏感文件/覆盖远程关键文件——由 `McpReadonly=0` + 审计兜底(与 `terminal_*`/`remote_*` 同款「开关+审计」,不加目录黑名单,见 §12.1)。
### 7.8 客户端改动(实施期修正,评审见 §13.1 U7)
`RecvFileChunkV2` 依赖全局 `g_status==1`(`SimplePlugins/file_upload.cpp:2812` `if (!g_status) return -1;`),但客户端**主连接此前从不初始化文件传输模块**——`InitFileUpload` 只在 `FileManager`/`ScreenManager` 构造里成对调用(下载/屏幕方向),上传走主连接时 `g_status==0`,每个 chunk 都被 `RecvFileChunkV2` 直接丢弃(表现为落盘 0 字节 / 截断)。
修法:在 `client/KernelManager.cpp` 的 `COMMAND_SEND_FILE_V2` 分支加**懒初始化**(进程内仅一次):
```cpp
static bool s_v2RecvInited = false;
if (!s_v2RecvInited) {
InitFileUpload({}, m_LoginMsg, m_LoginSignature, 64, 50, Logf);
s_v2RecvInited = true;
}
int n = RecvFileChunkV2((char*)szBuffer, ulLength, m_conn, nullptr, m_hash, m_hmac, m_MyClientID);
```
要点(独立评审结论,均为安全):
- 与 `FileManager.cpp:40` 的 Init 参数**逐字节一致**(`{}`, `m_LoginMsg`, `m_LoginSignature`, 64, 50, `Logf`),不新增路径。
- `static` 保证进程内只初始化一次,主连接重连(`ClientDll.cpp:660/665` 反复 `SAFE_DELETE`+`new CKernelManager`)不反复 Init/Uninit;`~CKernelManager` 保持原样**不** `UninitFileUpload`,故 `g_threadCount` 从 1 起永不归 0,`g_status` 恒为 1。
- `InitFileUpload` 幂等(`g_fileStatesMtx` + `g_threadCount` 引用计数,二次调用 `g_threadCount>1` 早退)、非阻塞(仅置标志 + 派生 detach 线程)、license 校验在启动期 `licenseInit()` 后必过、`verifyMessage` 对空签名 `return false`(`license.cpp:244`)不会越界。
- 影响面:主连接 `OnReceive` 无 V1 `COMMAND_SEND_FILE` 分支,`g_status=1` 只作用于 `RecvFileChunkV2`;`FileManager`/`ScreenManager` 的成对 Init/Uninit 只是在 1↔2 间震荡,语义不变。
--- ---
@@ -216,16 +306,18 @@ MCP工具线程 服务端 MessageHandle
| `server/2015Remote/McpServer.cpp` | `download_file` schema/实现 + 会话状态机 + 路由分支 | | `server/2015Remote/McpServer.cpp` | `download_file` schema/实现 + 会话状态机 + 路由分支 |
| `server/2015Remote/2015RemoteDlg.cpp` | `MessageHandle`:`COMMAND_SEND_FILE_V2`(85) / `COMMAND_FILE_COMPLETE_V2`(91) 两处守卫分支 + `TOKEN_DRIVE_LIST` 处 `OnDriveList` 扩展;`TOKEN_CONN_AUTH` 不动 | | `server/2015Remote/2015RemoteDlg.cpp` | `MessageHandle`:`COMMAND_SEND_FILE_V2`(85) / `COMMAND_FILE_COMPLETE_V2`(91) 两处守卫分支 + `TOKEN_DRIVE_LIST` 处 `OnDriveList` 扩展;`TOKEN_CONN_AUTH` 不动 |
| `server/2015Remote/McpSettingsDlg.cpp/.h` | `McpFileTransfer` 配置项 | | `server/2015Remote/McpSettingsDlg.cpp/.h` | `McpFileTransfer` 配置项 |
| (P2)`McpServer.cpp` | `upload_file` 实现(复用 `FileBatchTransferWorkerV2`) | | (P2)`server/2015Remote/McpServer.h` | `FileTransferSession.tool` 增 `"upload_file"`;新增 `BeginFileUpload(id)`(单设备单传输互斥标记) |
| (P2)`server/2015Remote/McpServer.cpp` | `upload_file` schema/实现:无头回调 `UploadSendChunkHeadless` + `CollectLocalFiles` 收集 + overwrite 预检(复用 `list_files` 链路)+ 驱动 `FileBatchTransferWorkerV2`(复用主连接,无子连接路由) |
| (P2)`client/KernelManager.cpp` | `COMMAND_SEND_FILE_V2` 分支加懒初始化 `InitFileUpload`(`g_status` 由 0 置 1,见 §7.8) |
**客户端零改动**(`UploadToRemoteV2` / `RecvFileChunkV2` / `FileBatchTransferWorkerV2` 均已存在)。 **客户端**:`UploadToRemoteV2` / `RecvFileChunkV2` / `FileBatchTransferWorkerV2` 均已存在、未改动;仅 `COMMAND_SEND_FILE_V2` 分支新增懒初始化(见 §7.8)。
--- ---
## 10. 分阶段实施与回滚 ## 10. 分阶段实施与回滚
- **P1**:`download_file` + `McpFileTransfer` 开关 + 路由分支。可独立合入、独立验收(真实主机拖回一个目录,SHA-256 与 `certutil -hashfile` 比对一致)。 - **P1**:`download_file` + `McpFileTransfer` 开关 + 路由分支。可独立合入、独立验收(真实主机拖回一个目录,SHA-256 与 `certutil -hashfile` 比对一致)。
- **P2**:`upload_file`。 - **P2**:`upload_file`(§7,主连接复用 + 发送方驱动,`MessageHandle` 零改动)。可独立合入、独立验收(推一个目录到真实主机,SHA-256 与源文件 `certutil -hashfile` 比对一致)。
- **P3**(可选):断点续传(需先验证服务端续传状态落盘;文件管理器侧现 `enableResume=false`,`client/FileManager.cpp:1164`)、大文件进度流式上报。 - **P3**(可选):断点续传(需先验证服务端续传状态落盘;文件管理器侧现 `enableResume=false`,`client/FileManager.cpp:1164`)、大文件进度流式上报。
- **回滚**:改动集中在 `McpServer.*` + `MessageHandle` 三个 `if` 分支,revert 当期 commit 即可,不影响既有 GUI 文件管理器。 - **回滚**:改动集中在 `McpServer.*` + `MessageHandle` 三个 `if` 分支,revert 当期 commit 即可,不影响既有 GUI 文件管理器。
@@ -239,6 +331,10 @@ MCP工具线程 服务端 MessageHandle
- 大文件(>2GB)超时与断线;并发下载同主机返回 `-32003`。 - 大文件(>2GB)超时与断线;并发下载同主机返回 `-32003`。
- 编码:GBK 中文文件名往返无乱码(与 `list_files` 同规则)。 - 编码:GBK 中文文件名往返无乱码(与 `list_files` 同规则)。
- 断线收尾:传输中客户端掉线 → 会话擦除 + 半成品删除,无句柄/内存泄漏。 - 断线收尾:传输中客户端掉线 → 会话擦除 + 半成品删除,无句柄/内存泄漏。
- **上传(P2)**:单文件 / 目录(含中文名、深层嵌套)上传;`remote_dir` 不存在自动创建;SHA-256 与源文件 `certutil -hashfile` 比对一致。
- 上传 `overwrite=false` 同名跳过(`skipped` 计数);`overwrite=true` 覆盖。
- 上传 `remote_dir` 含 `..` 逃逸被拒;大文件(>2GB)超时与断线;并发上传/上传-下载同主机返回 `-32003`。
- 上传编码:GBK 中文文件名往返无乱码。
--- ---
@@ -282,3 +378,19 @@ MCP工具线程 服务端 MessageHandle
| F7 | 三处改动均为「加 if 守卫 + 现有逻辑作 else」,C2C 分支不动 | 结构性满足原则 #1,对既有 GUI/C2C 零影响 | | F7 | 三处改动均为「加 if 守卫 + 现有逻辑作 else」,C2C 分支不动 | 结构性满足原则 #1,对既有 GUI/C2C 零影响 |
**判定**:7 项问题均已在正文对应章节修正,无阻塞项,**定稿**。 **判定**:7 项问题均已在正文对应章节修正,无阻塞项,**定稿**。
### 13.1 upload_file 评审记录(P2,实施前)
核对了 `2015RemoteDlg.cpp` 的 `SendFilesToClientV2Internal`/`SendFileChunkToClientV2`(GUI upload 发送方)、`SimplePlugins/file_upload.cpp` 的 `FileBatchTransferWorkerV2`/`RecvFileChunkV2`、`client/KernelManager.cpp:1381` 的接收分支,结论:
| # | 审查发现 | 结论 |
|---|---|---|
| U1 | 旧稿「服务端等客户端回 `COMMAND_FILE_COMPLETE_V2`」方向写反 | COMPLETE 恒由**发送方**发、接收方 `HandleFileCompleteV2` 校验,无 ACK 回传。upload 服务端是发送方 → **发** COMPLETE,不是「等」 |
| U2 | upload 走**主连接**(`ctx->Send2Client`),不像 download 要客户端新开鉴权流式子连接 | `MessageHandle` 零改动;无需 `IsFileTransferPending` 路由守卫、无需 `OnFileCompleteV2` 收包计数 |
| U3 | 复用 `FileBatchTransferWorkerV2` 时回调需无头版 | 镜像 GUI `SendFileChunkToClientV2` 去掉 `dlg` 进度,`m_parent->FindHost(clientID)` 定位 ctx;离线返回 false 中止 |
| U4 | 完成判定无接收方回执 | `result==0 && FindHost(clientID)!=nullptr`(与 GUI `SendFilesToClientV2Internal` 末尾一致);`output.sha256` 恒为空串,非接收方验真(见 §7.5) |
| U5 | `overwrite=false` 需预知远程同名文件 | 复用 `list_files` 的 `COMMAND_LIST_DRIVE→TOKEN_DRIVE_LIST` 预检 `remote_dir`,过滤同名进 `skipped` |
| U6 | 本地文件收集 | `common/file_upload.cpp:38` `ExpandDirectories` 未在 `file_upload.h` 导出,改为 McpServer.cpp 本地 `CollectLocalFiles` 同构实现(目录项在前、子项随后) |
| U7 | 实施验证发现:客户端主连接 `g_status==0`,`RecvFileChunkV2` 逐 chunk `return -1`(上传落盘 0 字节/截断) | 客户端 `COMMAND_SEND_FILE_V2` 分支加懒初始化(§7.8):`static` 一次 + 永久引用计数、析构不 `Uninit`;与 `FileManager.cpp:40` 参数一致,独立评审确认无稳定性风险 |
**判定**:7 项均已写入 §7 对应小节,无阻塞项,**定稿**。
Binary file not shown.
@@ -518,6 +518,7 @@
<Image Include="res\Bitmap\AuthGen.bmp" /> <Image Include="res\Bitmap\AuthGen.bmp" />
<Image Include="res\Bitmap\authorize.bmp" /> <Image Include="res\Bitmap\authorize.bmp" />
<Image Include="res\Bitmap\Backup.bmp" /> <Image Include="res\Bitmap\Backup.bmp" />
<Image Include="res\Bitmap\BrowseWnd.bmp" />
<Image Include="res\Bitmap\CancelShare.bmp" /> <Image Include="res\Bitmap\CancelShare.bmp" />
<Image Include="res\Bitmap\ClientLog.bmp" /> <Image Include="res\Bitmap\ClientLog.bmp" />
<Image Include="res\Bitmap\Forbidden.bmp" /> <Image Include="res\Bitmap\Forbidden.bmp" />
@@ -534,6 +535,7 @@
<Image Include="res\Bitmap\GrayDesktop.bmp" /> <Image Include="res\Bitmap\GrayDesktop.bmp" />
<Image Include="res\Bitmap\Help.bmp" /> <Image Include="res\Bitmap\Help.bmp" />
<Image Include="res\Bitmap\History.bmp" /> <Image Include="res\Bitmap\History.bmp" />
<Image Include="res\Bitmap\HistoryActivity.bmp" />
<Image Include="res\Bitmap\HostProxy.bmp" /> <Image Include="res\Bitmap\HostProxy.bmp" />
<Image Include="res\Bitmap\Import.bmp" /> <Image Include="res\Bitmap\Import.bmp" />
<Image Include="res\Bitmap\ImportLicense.bmp" /> <Image Include="res\Bitmap\ImportLicense.bmp" />
@@ -552,6 +554,7 @@
<Image Include="res\Bitmap\PEEdit.bmp" /> <Image Include="res\Bitmap\PEEdit.bmp" />
<Image Include="res\Bitmap\Plugin.bmp" /> <Image Include="res\Bitmap\Plugin.bmp" />
<Image Include="res\Bitmap\PluginConfig.bmp" /> <Image Include="res\Bitmap\PluginConfig.bmp" />
<Image Include="res\Bitmap\PortProxyAuto.bmp" />
<Image Include="res\Bitmap\PortProxyStd.bmp" /> <Image Include="res\Bitmap\PortProxyStd.bmp" />
<Image Include="res\Bitmap\PrivateScreen.bmp" /> <Image Include="res\Bitmap\PrivateScreen.bmp" />
<Image Include="res\Bitmap\proxy.bmp" /> <Image Include="res\Bitmap\proxy.bmp" />
@@ -88,6 +88,8 @@
<ClCompile Include="McpServer.cpp" /> <ClCompile Include="McpServer.cpp" />
<ClCompile Include="McpSettingsDlg.cpp" /> <ClCompile Include="McpSettingsDlg.cpp" />
<ClCompile Include="CClientLog.cpp" /> <ClCompile Include="CClientLog.cpp" />
<ClCompile Include="CActivityDialog.cpp" />
<ClCompile Include="LogSearchBar.cpp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="..\..\client\Audio.h" /> <ClInclude Include="..\..\client\Audio.h" />
@@ -196,6 +198,8 @@
<ClInclude Include="McpServer.h" /> <ClInclude Include="McpServer.h" />
<ClInclude Include="McpSettingsDlg.h" /> <ClInclude Include="McpSettingsDlg.h" />
<ClInclude Include="CClientLog.h" /> <ClInclude Include="CClientLog.h" />
<ClInclude Include="CActivityDialog.h" />
<ClInclude Include="LogSearchBar.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ResourceCompile Include="2015Remote.rc" /> <ResourceCompile Include="2015Remote.rc" />
@@ -338,5 +342,9 @@
<Image Include="res\Bitmap\Debug.bmp" /> <Image Include="res\Bitmap\Debug.bmp" />
<Image Include="res\ClientLog.ico" /> <Image Include="res\ClientLog.ico" />
<Image Include="res\Bitmap\ClientLog.bmp" /> <Image Include="res\Bitmap\ClientLog.bmp" />
<Image Include="res\Bitmap\Forbidden.bmp" />
<Image Include="res\Bitmap\HistoryActivity.bmp" />
<Image Include="res\Bitmap\BrowseWnd.bmp" />
<Image Include="res\Bitmap\PortProxyAuto.bmp" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+395
View File
@@ -1158,6 +1158,9 @@ std::string BuildRemoteMouse(const Json::Value& id, const Json::Value& args, CMy
Json::Value BuildDownloadFileInputSchema(); Json::Value BuildDownloadFileInputSchema();
Json::Value BuildDownloadFileOutputSchema(); Json::Value BuildDownloadFileOutputSchema();
std::string BuildDownloadFile(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent); std::string BuildDownloadFile(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
Json::Value BuildUploadFileInputSchema();
Json::Value BuildUploadFileOutputSchema();
std::string BuildUploadFile(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
std::string BuildRemoteClipboard(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent); std::string BuildRemoteClipboard(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
// tools/list // tools/list
@@ -1419,6 +1422,17 @@ std::string BuildToolsListResult(const Json::Value& id) {
tools.append(tool); tools.append(tool);
} }
// 17) upload_file(P2:MCP 文件上传,仅 Windows,安全门:McpFileTransfer=1 且 McpReadonly=0)
// 写远程盘,故额外要求关闭只读(与 terminal_* / remote_* 一致)。
if (CMcpServer::Instance().IsFileTransferEnabled() && !CMcpServer::Instance().IsReadonly()) {
Json::Value tool(Json::objectValue);
tool["name"] = "upload_file";
tool["description"] = u8"把主控本机文件或目录上传到指定在线 Windows 主机的目录(V2 协议,主连接发送)。local_path 为主控本机绝对路径(文件或目录,目录递归上传);remote_dir 为远程保存目录(不存在自动创建);overwrite=false 时跳过 remote_dir 下已存在的同名顶层项。返回 files[{path,size,sha256}] 与 total_bytes/skipped;sha256 恒为空(V2 无接收方回执,完整性由客户端本地校验、失败仅记日志)。";
tool["inputSchema"] = BuildUploadFileInputSchema();
tool["outputSchema"] = BuildUploadFileOutputSchema();
tools.append(tool);
}
result["tools"] = tools; result["tools"] = tools;
return BuildResult(id, result); return BuildResult(id, result);
} }
@@ -3796,6 +3810,371 @@ std::string BuildDownloadFile(const Json::Value& id, const Json::Value& args, CM
return BuildError(id, -32001, msg); return BuildError(id, -32001, msg);
} }
// ===== P2:upload_file 辅助 =====
// ASCII 小写(GBK 双字节首字节 0x81..0xFE 不含 A-Z,安全);Windows 路径名不区分大小写。
static std::string AsciiLower(std::string s) {
for (char& c : s) if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
return s;
}
// 词法检查路径是否含 ".." 段(防上传目标逃逸到预期目录之外)。
static bool HasDotDotSegment(const std::string& path) {
size_t i = 0;
while (i < path.size()) {
size_t j = path.find_first_of("\\/", i);
if (j == std::string::npos) j = path.size();
size_t n = j - i;
if (n == 2 && path[i] == '.' && path[i + 1] == '.') return true;
i = j + 1;
}
return false;
}
// 递归收集本机目录下的文件与目录项(目录在前、子项随后,供 FileBatchTransferWorkerV2 发送)。
// 与 common/file_upload.cpp 的 ExpandDirectory 同构(该函数未在 file_upload.h 导出,故本地实现)。
static void CollectLocalFiles(const std::string& dir, std::vector<std::string>& out) {
std::string searchPath = dir + "\\*";
WIN32_FIND_DATAA fd;
HANDLE hFind = FindFirstFileA(searchPath.c_str(), &fd);
if (hFind == INVALID_HANDLE_VALUE) return;
do {
if (strcmp(fd.cFileName, ".") == 0 || strcmp(fd.cFileName, "..") == 0) continue;
std::string fullPath = dir + "\\" + fd.cFileName;
out.push_back(fullPath); // 文件与目录都加入
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
CollectLocalFiles(fullPath, out); // 递归(目录项先于其子文件)
} while (FindNextFileA(hFind, &fd));
FindClose(hFind);
}
// upload_file 无头发送回调数据
struct UploadCallbackData {
CMy2015RemoteDlg* parent;
uint64_t clientID;
DWORD deadline = 0; // 整体传输截止(GetTickCount 毫秒;0=不限)
bool timedOut = false;
bool failed = false; // 任一 Send2Client 失败或客户端离线
};
// upload_file 无头发送回调(镜像 GUI SendFileChunkToClientV2,去掉 dlg 进度)。
static bool UploadSendChunkHeadless(void* user, FileChunkPacketV2* chunk, unsigned char* data, int size) {
UploadCallbackData* cb = (UploadCallbackData*)user;
if (!cb || !cb->parent) return false;
if (cb->deadline && (int)(GetTickCount() - cb->deadline) >= 0) { // 整体超时(DWORD 回绕安全)
cb->timedOut = true;
cb->failed = true;
return false;
}
context* ctx = cb->parent->FindHost(cb->clientID);
if (!ctx) { cb->failed = true; return false; }
BOOL sent = ctx->Send2Client(data, size);
if (!sent) cb->failed = true;
return sent != FALSE;
}
// 列远程目录一层,返回顶层名集合(小写原始 ANSI;客户端 ANSI=936 与主控本地名同编码)。
// 复用 list_files 的 COMMAND_LIST_DRIVE→TOKEN_DRIVE_LIST→COMMAND_LIST_FILES→TOKEN_FILE_LIST 链路。
static bool ListRemoteTopLevelNames(CMy2015RemoteDlg* parent, uint64_t devId,
const std::string& remoteDirAnsi,
std::set<std::string>& names) {
CMcpServer& mcp = CMcpServer::Instance();
if (!mcp.BeginPending(devId, "list_files", remoteDirAnsi)) return false;
context* ctx = FindMainContext(parent, devId);
if (!ctx) { mcp.ClearPending(devId); return false; }
BYTE cmd = COMMAND_LIST_DRIVE;
if (!ctx->Send2Client(&cmd, 1)) { mcp.ClearPending(devId); return false; }
std::vector<BYTE> data;
if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs)) return false;
// TOKEN_FILE_LIST 布局:[token:1][attr:1][name\0][sizeHigh:4][sizeLow:4][ft:8]...
if (data.size() < 2) return true; // 空目录 → 无顶层名
const char* p = (const char*)data.data();
size_t len = data.size();
size_t off = 1; // 跳过 token 字节
while (off + 1 <= len) {
off += 1; // attr
const char* name = p + off;
size_t nlen = BoundedStrlen(name, len - off);
if (nlen == 0 || nlen >= len - off) break; // 空记录/越界 = 尾部零填充
off += nlen + 1;
if (off + 16 > len) break;
off += 16; // sizeHigh/sizeLow/ft(8)
names.insert(AsciiLower(std::string(name, nlen)));
}
return true;
}
Json::Value BuildUploadFileInputSchema() {
Json::Value props(Json::objectValue);
Json::Value idProp(Json::objectValue);
idProp["type"] = "string";
idProp["description"] = u8"主机 id,取 list_online_hosts / search_hosts 返回的 id 字段";
props["id"] = idProp;
Json::Value lp(Json::objectValue);
lp["type"] = "string";
lp["description"] = u8"主控本机文件或目录的绝对路径(目录递归上传),如 C:\\Users\\foo\\Pictures";
props["local_path"] = lp;
Json::Value rd(Json::objectValue);
rd["type"] = "string";
rd["description"] = u8"远程主机的保存目录(不存在则自动创建),如 C:\\uploads";
props["remote_dir"] = rd;
Json::Value ow(Json::objectValue);
ow["type"] = "boolean";
ow["description"] = u8"是否覆盖 remote_dir 下已存在的同名顶层文件/目录;默认 false(跳过并计入 skipped)";
props["overwrite"] = ow;
Json::Value to(Json::objectValue);
to["type"] = "integer";
to["description"] = u8"超时毫秒(默认 600000=10 分钟,上限 3600000=1 小时)";
props["timeout_ms"] = to;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("id");
required.append("local_path");
required.append("remote_dir");
schema["required"] = required;
return schema;
}
Json::Value BuildUploadFileOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value filesProp(Json::objectValue);
filesProp["type"] = "array";
Json::Value items(Json::objectValue);
items["type"] = "object";
Json::Value itemProps(Json::objectValue);
Json::Value pathProp(Json::objectValue); pathProp["type"] = "string"; itemProps["path"] = pathProp;
Json::Value sizeProp(Json::objectValue); sizeProp["type"] = "integer"; itemProps["size"] = sizeProp;
Json::Value shaProp(Json::objectValue); shaProp["type"] = "string"; itemProps["sha256"] = shaProp;
items["properties"] = itemProps;
filesProp["items"] = items;
props["files"] = filesProp;
Json::Value tb(Json::objectValue); tb["type"] = "integer"; props["total_bytes"] = tb;
Json::Value sk(Json::objectValue); sk["type"] = "integer"; props["skipped"] = sk;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
return schema;
}
// tools/call:upload_file(上传主控本机文件/目录到远程主机,V2 协议,主连接发送)
std::string BuildUploadFile(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
// 分派门控(McpFileTransfer=1 且 McpReadonly=0):即便绕过 tools/list 直调也拒绝。
if (!CMcpServer::Instance().IsFileTransferEnabled())
return BuildError(id, -32006, "File transfer is disabled: requires McpFileTransfer=1");
if (CMcpServer::Instance().IsReadonly())
return BuildError(id, -32006, "upload_file is disabled in read-only mode: requires McpReadonly=0");
uint64_t devId = 0;
std::string err;
if (!ParseHostIdArg(args, devId, err))
return BuildError(id, -32602, err);
context* ctx = FindMainContext(parent, devId);
if (!ctx)
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
// 仅 Windows 客户端实现 V2 主连接接收链路(与 download_file 一致)。
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
if (clientType == "LNX" || clientType == "MAC")
return BuildError(id, -32005, "upload_file is only supported on Windows hosts");
if (!ctx->SupportsFileV2())
return BuildError(id, -32006, "Host does not support V2 file transfer");
std::string localPathUtf8 = GetStringArg(args, "local_path");
if (localPathUtf8.empty())
return BuildError(id, -32602, "Missing required parameter: local_path");
std::string remoteDirUtf8 = GetStringArg(args, "remote_dir");
if (remoteDirUtf8.empty())
return BuildError(id, -32602, "Missing required parameter: remote_dir");
bool overwrite = false;
if (args.isMember("overwrite") && args["overwrite"].isBool())
overwrite = args["overwrite"].asBool();
int timeoutMs = 600000;
if (args.isMember("timeout_ms")) {
int v = 0;
if (!GetIntArg(args, "timeout_ms", v) || v <= 0 || v > 3600000)
return BuildError(id, -32602, "timeout_ms must be in range 1..3600000");
timeoutMs = v;
}
// 主控本机路径:UTF-8 → 主控 ANSI,去尾斜杠。
std::string localPathAnsi = ToAnsi(localPathUtf8, CP_ACP);
if (localPathAnsi.empty())
return BuildError(id, -32602, "Invalid local_path encoding");
while (!localPathAnsi.empty() && (localPathAnsi.back() == '\\' || localPathAnsi.back() == '/'))
localPathAnsi.pop_back();
DWORD attr = GetFileAttributesA(localPathAnsi.c_str());
if (attr == INVALID_FILE_ATTRIBUTES)
return BuildError(id, -32602, "local_path not found: " + localPathUtf8);
// 收集本机文件 + 目录项(目录在前、子项随后),镜像 ExpandDirectories 的顺序语义。
std::vector<std::string> files;
files.push_back(localPathAnsi);
if (attr & FILE_ATTRIBUTE_DIRECTORY)
CollectLocalFiles(localPathAnsi, files);
// 远程保存目录:UTF-8 → 客户端 ANSI(936),去尾斜杠 + 结尾 '\'(作 targetDir 前缀)。
std::string remoteDirAnsi = ToAnsi(remoteDirUtf8, 936);
if (remoteDirAnsi.empty())
return BuildError(id, -32602, "Invalid remote_dir encoding");
while (!remoteDirAnsi.empty() && (remoteDirAnsi.back() == '\\' || remoteDirAnsi.back() == '/'))
remoteDirAnsi.pop_back();
if (remoteDirAnsi.empty())
return BuildError(id, -32602, "Invalid remote_dir: " + remoteDirUtf8);
if (HasDotDotSegment(remoteDirAnsi))
return BuildError(id, -32602, "remote_dir must not contain '..' path segments");
std::string targetDir = remoteDirAnsi + "\\";
// overwrite=false:预检 remote_dir 一层,跳过已存在的同名顶层项(单文件精确、目录整体跳过)。
int skipped = 0;
std::vector<std::string> sendFiles;
if (!overwrite) {
std::set<std::string> existing;
if (!ListRemoteTopLevelNames(parent, devId, remoteDirAnsi, existing))
return BuildError(id, -32001, "Failed to list remote_dir for overwrite check");
std::string rootDir = GetCommonRoot(files);
for (const std::string& f : files) {
DWORD a = GetFileAttributesA(f.c_str());
bool isDir = (a != INVALID_FILE_ATTRIBUTES && (a & FILE_ATTRIBUTE_DIRECTORY));
std::string rel = GetRelativePath(rootDir, f);
size_t sep = rel.find_first_of("\\/");
std::string top = (sep == std::string::npos) ? rel : rel.substr(0, sep);
if (existing.count(AsciiLower(top))) {
if (!isDir) skipped++; // 目录项不计入 skipped(与 download 一致)
continue;
}
sendFiles.push_back(f);
}
} else {
sendFiles = files;
}
// 全部被跳过:直接返回成功(不发起传输)。
if (sendFiles.empty()) {
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["files"] = Json::Value(Json::arrayValue);
structuredContent["total_bytes"] = (Json::UInt64)0;
structuredContent["skipped"] = skipped;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"上传完成:所有文件均已存在,跳过 ") + std::to_string(skipped) + u8" 个。";
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
CMcpServer& mcp = CMcpServer::Instance();
if (!mcp.BeginFileUpload(devId))
return BuildError(id, -32003, "Device busy: another request is pending for this host");
TransferOptionsV2 opts;
opts.transferID = GenerateTransferID();
opts.srcClientID = 0; // 主控端
opts.dstClientID = devId;
opts.enableResume = false;
std::string hash = GetPwdHash();
std::string hmac = GetHMAC(100);
UploadCallbackData cbData;
cbData.parent = parent;
cbData.clientID = devId;
cbData.deadline = GetTickCount() + (DWORD)timeoutMs; // DWORD 回绕由回调的 (int) 差值判断吸收
// 同步驱动发送(走主连接);无外来流,无需 WaitFileTransferDone / 收包计数。
int result = FileBatchTransferWorkerV2(sendFiles, targetDir, &cbData,
UploadSendChunkHeadless, nullptr, hash, hmac, opts);
bool clientOnline = (FindMainContext(parent, devId) != nullptr);
mcp.ClearFileTransfer(devId);
bool ok = (result == 0) && !cbData.failed && clientOnline;
// 计算已发送文件(远程完整路径 = targetDir + relPath)与字节数;目录项不计入 files。
std::string outRoot = GetCommonRoot(sendFiles);
std::vector<FileTransferEntry> outFiles;
uint64_t totalBytes = 0;
for (const std::string& f : sendFiles) {
DWORD a = GetFileAttributesA(f.c_str());
if (a == INVALID_FILE_ATTRIBUTES || (a & FILE_ATTRIBUTE_DIRECTORY)) continue;
std::string rel = GetRelativePath(outRoot, f);
FileTransferEntry e;
e.path = targetDir + rel;
WIN32_FILE_ATTRIBUTE_DATA fad;
if (GetFileAttributesExA(f.c_str(), GetFileExInfoStandard, &fad))
e.size = ((uint64_t)fad.nFileSizeHigh << 32) | fad.nFileSizeLow;
// §7.5:V2 无接收方→发送方 ACK,发送方自算 SHA-256 未由 worker 回传;P2 留空(延后 P3)。
e.sha256 = "";
totalBytes += e.size;
outFiles.push_back(std::move(e));
}
if (ok) {
if (parent) {
std::string audit = "host " + std::to_string(devId) + " upload_file: "
+ localPathUtf8 + " -> " + remoteDirUtf8
+ " (" + std::to_string(outFiles.size()) + " files, "
+ std::to_string(totalBytes) + " bytes, skipped " + std::to_string(skipped) + ")";
parent->PostMessageA(WM_SHOWERRORMSG,
(WPARAM)new CString(ToAnsi(audit, 936).c_str()),
(LPARAM)new CString(_TR("MCP文件传输")));
}
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
Json::Value filesArr(Json::arrayValue);
for (auto& f : outFiles) {
Json::Value fo(Json::objectValue);
fo["path"] = ToUtf8(f.path.c_str(), 936);
fo["size"] = (Json::UInt64)f.size;
fo["sha256"] = f.sha256;
filesArr.append(fo);
}
structuredContent["files"] = filesArr;
structuredContent["total_bytes"] = (Json::UInt64)totalBytes;
structuredContent["skipped"] = skipped;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"上传完成:") + std::to_string(outFiles.size()) + u8" 个文件("
+ std::to_string(totalBytes) + u8" 字节),跳过 " + std::to_string(skipped) + u8" 个。";
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
if (parent) {
std::string audit = "host " + std::to_string(devId) + " upload_file FAILED: "
+ localPathUtf8 + " -> " + remoteDirUtf8;
parent->PostMessageA(WM_SHOWERRORMSG,
(WPARAM)new CString(ToAnsi(audit, 936).c_str()),
(LPARAM)new CString(_TR("MCP文件传输")));
}
const char* msg = cbData.timedOut ? "Timeout waiting for upload to complete"
: "Upload failed (target offline or send error)";
return BuildError(id, -32001, msg);
}
// tools/call 分派 // tools/call 分派
std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) { std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
const Json::Value& id = root["id"]; const Json::Value& id = root["id"];
@@ -3830,6 +4209,7 @@ std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
if (toolName == "remote_mouse") return BuildRemoteMouse(id, args, parent); if (toolName == "remote_mouse") return BuildRemoteMouse(id, args, parent);
if (toolName == "remote_clipboard") return BuildRemoteClipboard(id, args, parent); if (toolName == "remote_clipboard") return BuildRemoteClipboard(id, args, parent);
if (toolName == "download_file") return BuildDownloadFile(id, args, parent); if (toolName == "download_file") return BuildDownloadFile(id, args, parent);
if (toolName == "upload_file") return BuildUploadFile(id, args, parent);
return BuildError(id, -32602, return BuildError(id, -32602,
"Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName)); "Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName));
@@ -4569,6 +4949,21 @@ bool CMcpServer::BeginFileTransferPending(uint64_t device_id, const std::string&
return true; return true;
} }
bool CMcpServer::BeginFileUpload(uint64_t device_id) {
// 与一次性挂起请求互斥(F2):先查对方注册表再查自己的,避免跨锁嵌套死锁。
{
std::lock_guard<std::mutex> lk(m_PendingMutex);
if (m_Pending.find(device_id) != m_Pending.end()) return false;
}
std::lock_guard<std::mutex> lk(m_FileXferMutex);
if (m_FileXferSessions.find(device_id) != m_FileXferSessions.end()) return false; // 单设备单传输
FileTransferSession s;
s.tool = "upload_file";
s.startAt = time(nullptr);
m_FileXferSessions[device_id] = std::move(s);
return true;
}
bool CMcpServer::OnDownloadDriveList(uint64_t device_id, context* fmSubCtx) { bool CMcpServer::OnDownloadDriveList(uint64_t device_id, context* fmSubCtx) {
std::string localDir, remotePath; std::string localDir, remotePath;
{ {
+7 -2
View File
@@ -218,6 +218,11 @@ public:
const std::string& localDir, const std::string& remotePath, const std::string& localDir, const std::string& remotePath,
bool overwrite); bool overwrite);
// 登记上传会话(false = 该 host 已有文件会话或一次性挂起请求)。upload 走主连接、
// 工具线程同步驱动 FileBatchTransferWorkerV2,会话仅为单设备单传输互斥标记
// (无 fmSubCtx/streamSubCtx,ClearFileTransfer 对空指针安全)。
bool BeginFileUpload(uint64_t device_id);
// TOKEN_DRIVE_LIST:识别 download_file,存文件管理器子链接并下发 CMD_DOWN_FILES_V2。 // TOKEN_DRIVE_LIST:识别 download_file,存文件管理器子链接并下发 CMD_DOWN_FILES_V2。
// 返回 true=已接管(保持子链接,收尾由 ClearFileTransfer);false=会话已清理(调用方 CancelIO)。 // 返回 true=已接管(保持子链接,收尾由 ClearFileTransfer);false=会话已清理(调用方 CancelIO)。
bool OnDownloadDriveList(uint64_t device_id, context* fmSubCtx); bool OnDownloadDriveList(uint64_t device_id, context* fmSubCtx);
@@ -317,8 +322,8 @@ private:
// ===== P6:文件传输会话(受 m_FileXferMutex 保护;单设备单传输)===== // ===== P6:文件传输会话(受 m_FileXferMutex 保护;单设备单传输)=====
struct FileTransferSession { struct FileTransferSession {
std::string tool; // 恒为 "download_file" std::string tool; // "download_file" / "upload_file"
std::string localDir; // 本机保存目录(ANSI,结尾 '\') std::string localDir; // download:本机保存目录(ANSI,结尾 '\')
std::string remotePath; // 客户端远程路径(ANSI,发 CMD_DOWN_FILES_V2 用) std::string remotePath; // 客户端远程路径(ANSI,发 CMD_DOWN_FILES_V2 用)
bool overwrite = false; // 是否覆盖已存在文件 bool overwrite = false; // 是否覆盖已存在文件
context* fmSubCtx = nullptr; // 文件管理器子链接(下发 CMD_DOWN_FILES_V2) context* fmSubCtx = nullptr; // 文件管理器子链接(下发 CMD_DOWN_FILES_V2)
+13 -3
View File
@@ -294,7 +294,17 @@ void CMcpSettingsDlg::LayoutControls(int cx, int cy)
m_editWhitelist.MoveWindow(margin + labelW, y - 2, cx - margin * 2 - labelW, whitelistH); m_editWhitelist.MoveWindow(margin + labelW, y - 2, cx - margin * 2 - labelW, whitelistH);
y += whitelistH + gap; y += whitelistH + gap;
int bottomY = cy - margin - btnH; // 确定 / 取消按钮紧跟白名单下方,不再锚定到底部,避免白名单与按钮之间出现大段空白。
m_btnCancel.MoveWindow(cx - margin - btnW, bottomY, btnW, btnH); m_btnCancel.MoveWindow(cx - margin - btnW, y, btnW, btnH);
m_btnOK.MoveWindow(cx - margin - btnW * 2 - gap, bottomY, 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();
}
} }
+1 -1
View File
@@ -46,7 +46,7 @@
// 程序版本号 [建议格式: X.Y.Z] // 程序版本号 [建议格式: X.Y.Z]
// 影响:关于对话框、标题栏 // 影响:关于对话框、标题栏
#define BRAND_VERSION "1.3.8" #define BRAND_VERSION "1.3.9"
// 启动画面名称 [建议大写,更有 Logo 感] // 启动画面名称 [建议大写,更有 Logo 感]
// 影响:启动画面 Logo 文字(大号艺术字体渲染) // 影响:启动画面 Logo 文字(大号艺术字体渲染)
+7 -2
View File
@@ -1987,10 +1987,15 @@ MCP
端口需为 1-65535 的数字=Port must be a number between 1 and 65535 端口需为 1-65535 的数字=Port must be a number between 1 and 65535
Token 不能为空=Token cannot be empty Token 不能为空=Token cannot be empty
MCP 设置已保存。=MCP settings saved. MCP 设置已保存。=MCP settings saved.
启用/端口/绑定地址/Token/只读/白名单/持久终端的改动需重启程序生效。=Changes to enable/port/bind/token/read-only/whitelist/persistent terminal take effect after restart. 启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输的改动需重启程序生效。=Changes to enable/port/bind address/token/read-only/whitelist/persistent terminal/remote control/file transfer take effect after restart.
只读模式(禁命令执行)=Read-only mode (disable command execution) 只读模式(禁命令执行)=Read-only mode (disable command execution)
命令白名单=Command whitelist 命令白名单=Command whitelist
MCP命令执行=MCP command execution MCP命令执行=MCP command execution
启用持久终端(全命令,无白名单)=Enable persistent terminal (full commands, no whitelist) 启用持久终端(全命令,无白名单)=Enable persistent terminal (full commands, no whitelist)
持久终端仅在只读模式关闭时生效。=Persistent terminal takes effect only when read-only mode is off. 持久终端与远程控制仅在只读模式关闭时生效。=Persistent terminal and remote control take effect only when read-only mode is off.
MCP持久终端=MCP persistent terminal MCP持久终端=MCP persistent terminal
MCP远程控制=MCP remote control
MCP文件传输=MCP file transfer
Token=Token
启用远程控制(AI 操控桌面)=Enable remote control (AI controls the desktop)
启用文件传输(download_file)=Enable file transfer (download_file)
+7 -2
View File
@@ -1978,10 +1978,15 @@ MCP
端口需为 1-65535 的数字=連接埠需為 1-65535 的數字 端口需为 1-65535 的数字=連接埠需為 1-65535 的數字
Token 不能为空=Token 不能為空 Token 不能为空=Token 不能為空
MCP 设置已保存。=MCP 設定已儲存。 MCP 设置已保存。=MCP 設定已儲存。
启用/端口/绑定地址/Token/只读/白名单/持久终端的改动需重启程序生效。=啟用/連接埠/綁定位址/Token/唯讀/白名單/持久終端的變更需重新啟動程式後生效。 启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输的改动需重启程序生效。=啟用/連接埠/綁定位址/Token/唯讀/白名單/持久終端/遠端控制/檔案傳輸的變更需重新啟動程式後生效。
只读模式(禁命令执行)=唯讀模式(禁命令執行) 只读模式(禁命令执行)=唯讀模式(禁命令執行)
命令白名单=命令白名單 命令白名单=命令白名單
MCP命令执行=MCP命令執行 MCP命令执行=MCP命令執行
启用持久终端(全命令,无白名单)=啟用持久終端(完整命令,無白名單) 启用持久终端(全命令,无白名单)=啟用持久終端(完整命令,無白名單)
持久终端仅在只读模式关闭时生效。=持久終端僅在唯讀模式關閉時生效。 持久终端与远程控制仅在只读模式关闭时生效。=持久終端與遠端控制僅在唯讀模式關閉時生效。
MCP持久终端=MCP持久終端 MCP持久终端=MCP持久終端
MCP远程控制=MCP遠端控制
MCP文件传输=MCP檔案傳輸
Token=Token
启用远程控制(AI 操控桌面)=啟用遠端控制(AI 操控桌面)
启用文件传输(download_file)=啟用檔案傳輸(download_file)