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
This commit is contained in:
yuanyuanxiang
2026-08-31 22:43:16 +02:00
parent 5c77f5ce14
commit 6bcd593b6d
4 changed files with 531 additions and 12 deletions

View File

@@ -1,6 +1,6 @@
# 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_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. 设计原则
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 进度框。
3. **单设备单传输会话**:与终端/远程控制一致,避免同 host 并发传输的归属歧义;并发返回 `-32003 Device busy`
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`
- **比 download 更简单**:服务端是发送方,工具线程直接驱动 `FileBatchTransferWorkerV2`,无需等待外来流;只需等客户端回 `COMMAND_FILE_COMPLETE_V2`(复用同一文件会话注册表)。
- **Schema**§3.2 已列):`id` + `local_path` + `remote_dir` + `overwrite` + `timeout_ms`
- **安全**:写远程盘,独立评审;`remote_dir` 路径规范化,可选系统目录黑名单。
### 7.1 工具 Schema
```
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_fileP1 | upload_fileP2 |
|---|---|---|
| 数据方向 | 远程客户端 → 主控 | 主控 → 远程客户端 |
| 发送方 | 客户端(`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延后 P3worker 回传哈希,或服务端引入 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`§8upload 写远程盘,复用「允许写」主开关。
- `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/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` 配置项 |
| 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. 分阶段实施与回滚
- **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`)、大文件进度流式上报。
- **回滚**:改动集中在 `McpServer.*` + `MessageHandle` 三个 `if` 分支revert 当期 commit 即可,不影响既有 GUI 文件管理器。
@@ -239,6 +331,10 @@ MCP工具线程 服务端 MessageHandle
- 大文件(>2GB超时与断线并发下载同主机返回 `-32003`
- 编码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 零影响 |
**判定**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 对应小节,无阻塞项,**定稿**。