# YAMA MCP 文件传输功能设计(download_file / upload_file) > **状态**:设计定稿(经专家审查修订,结论见 §13)。`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 协议/架构/配置)。 --- ## 1. 背景与目标 MCP 目前已暴露 `list_files`(只读列目录,`McpServer.cpp:1741`),但**没有文件传输工具**:把远程主机的文件/目录拉回主控本机(下载),或把本机文件推到远程(上传),只能通过 GUI 文件管理器手工操作。 本技术书解决: 1. **`download_file`**(本期 P1)——远程文件/目录 → 主控本机指定目录,复用 V2 推送协议,带 SHA-256 完整性校验、支持大文件。 2. **`upload_file`**(后续 P2)——主控本机文件/目录 → 远程指定目录,复用服务端 V2 发送器。 3. **一套共享的「流式文件会话」基础设施**——一次投入,下载/上传两处受益,镜像现有终端(`TermSession`)与远程控制(`ScreenCtrlSession`)会话模式。 --- ## 2. 现状回顾 | 能力 | 现有协议/命令 | 服务端现有实现 | 是否暴露给 MCP | |---|---|---|---| | 远程列目录 | `COMMAND_LIST_DRIVE`→`TOKEN_DRIVE_LIST`;`COMMAND_LIST_FILES`→`TOKEN_FILE_LIST` | `McpServer.cpp:1741`(`list_files`) | ✅ | | 远程→本地下载(V1) | `COMMAND_DOWN_FILES`(`commands.h:166`) + `TOKEN_FILE_SIZE/DATA/FINISH` | `FileManagerDlg.cpp:2164` / `file/CFileManagerDlg.cpp:1136` | ❌ 仅 GUI | | 远程→本地下载(V2) | `CMD_DOWN_FILES_V2`(94, `commands.h:260`) + `COMMAND_SEND_FILE_V2`(85) + `COMMAND_FILE_COMPLETE_V2`(91) | `FileManagerDlg.cpp:3337-3384`(下发)、`2756`(`RecvFileChunkV2` 落盘) | ❌ 仅 GUI | | 本地→远程上传(V2) | `FileBatchTransferWorkerV2`(服务端作 sender) | `2015RemoteDlg.cpp:7572` | ❌ 仅 GUI | ### 2.1 关键事实 1. **V2 下载的客户端会另开一条新子连接**:`client/FileManager.cpp:1101-1190` `UploadToRemoteV2()` 解析完文件列表后,`new IOCPClient(...)` → `EnableSubConnAuth()` → `ConnectServer(...)`(`FileManager.cpp:1170-1172`),在**独立鉴权子连接**上推 `COMMAND_SEND_FILE_V2` 分块 + `COMMAND_FILE_COMPLETE_V2` 校验包。即下载 ≠ 复用文件管理器那条子链接,而是一条**持续流子链接**——正属 `Mcp_Phase2_Design.md §4.3` 当初判定「本期不做」的流式能力。 2. **接收端 `RecvFileChunkV2` 无 UI 可调**:`CDlgFileSend.cpp:128` 已 `RecvFileChunkV2(buf, len, nullptr, nullptr, hash, hmac, 0)` 无头落盘;落盘状态在 `SimplePlugins/file_upload.cpp` 内部按 `transferID` 自维护,天然适合 MCP 无头复用。 3. **V2 落盘路径由客户端回填**:`CMD_DOWN_FILES_V2` 的 `targetDir` 是**主控本机**保存目录(`FileManagerDlg.cpp:3370` `m_Local_Path`);客户端把它拼进每个 chunk 的 `filename` 发回,服务端 `RecvFileChunkV2` 直接按该完整路径写盘。因此「下载保存到哪」由服务端在命令里指定、客户端原样回填。 --- ## 3. 协议选型(已定:V2) | 维度 | **V2 推送(选定)** | V1 拉取(回退) | |---|---|---| | 命令 | `CMD_DOWN_FILES_V2` + `COMMAND_SEND_FILE_V2` + `COMMAND_FILE_COMPLETE_V2` | `COMMAND_DOWN_FILES` + `TOKEN_FILE_SIZE/DATA/FINISH` | | 连接 | 客户端新开一条鉴权流式子连接 | 复用文件管理器子链接 | | 完整性 | ✅ SHA-256(`COMMAND_FILE_COMPLETE_V2`) | ❌ 无 | | 大文件 | ✅ 64 位偏移,>4GB | 64 位偏移但无校验 | | 客户端代码 | ✅ 已存在,是当前 GUI 正规路径 | ⚠️ 遗留路径 | | MCP 侧复杂度 | 需路由一条**新流式子连接**(新增会话注册表) | 需在 `MessageHandle` 维护「发 CONTINUE/收 DATA」状态机,逐块 ACK 慢 | **结论:采用 V2。** 代价是服务端要新增「流式文件会话」注册与路由;但这是 `upload_file` 同样要复用的基础设施,一次投入两处受益。V1 仅作「不想引入新子连接路由」时的降级备案,不在本期实现。 --- ## 4. 设计原则 1. **复用现有协议,不新造命令**:`CMD_DOWN_FILES_V2`、`COMMAND_SEND_FILE_V2`、`COMMAND_FILE_COMPLETE_V2`、`COMMAND_LIST_DRIVE` 全部为 `common/commands.h` 既有;客户端零改动。 2. **无头接管复用会话模式**:镜像 `McpServer.h` 的 `TermSession` / `ScreenCtrlSession`,新增 `FileTransferSession`;`MessageHandle` 用 `IsFileTransferContext(context*)` 判定是否路由到 MCP 无头落盘,否则回落 GUI 进度框。 3. **单设备单传输会话**:与终端/远程控制一致,避免同 host 并发传输的归属歧义;并发返回 `-32003 Device busy`。 4. **超时与清理是硬约束**:大文件传输是长任务,超时需独立于 `kMcpToolTimeoutMs`(20s);断线/超时必须清会话 + 关子链接 + 删半成品文件。 5. **输出 schema 先行**:两个工具先在 `tools/list` 声明完整 `inputSchema`/`outputSchema`。 --- ## 5. 核心机制:流式文件会话 ### 5.1 会话注册表(`McpServer.h/.cpp`) ```cpp struct FileTransferSession { std::string tool; // "download_file" / "upload_file" std::string localDir; // download: 本机落盘目录(规范化后绝对路径,用于穿越校验) std::string remotePath; // 审计用原始远程路径(UTF-8) uint64_t transferID = 0; // 绑定后从首包填充(download);upload 由本端 GenerateTransferID() uint32_t totalFiles = 0; // 完成判定:首个 COMMAND_SEND_FILE_V2 chunk 的 totalFiles(§13 F4) uint32_t filesDone = 0; // 完成判定:累计收到的 COMMAND_FILE_COMPLETE_V2 数 bool done = false; int error = 0; // FEV2_* / 自定义 std::vector files; // {path, size} 已落盘/已发送文件 std::vector skipped; // overwrite=false 时跳过的同名文件 time_t startAt = 0; }; std::mutex m_FileXferMutex; std::map m_FileXferSessions; // device_id → 会话 ``` > **取消 `m_FileXferContextToDevice` 映射与 `IsFileTransferContext(context*)`**:流式子连接经 `TOKEN_CONN_AUTH` 已把 `clientID` 钉在 `ContextObject` 上(`2015RemoteDlg.cpp:6411 SetID`),分派时直接用 `ContextObject->GetClientID()` 定位 `device_id`,无需 context 路由表(§13 F1)。 配套接口(与 `BeginTermPending` / `OnTerminalData` / `WaitTerminalDone` 同构): ```cpp bool BeginFileTransferPending(uint64_t device_id, const std::string& tool, const std::string& localDir, const std::string& remotePath); bool IsFileTransferPending(uint64_t device_id); // 分支顶部守卫 void OnFileChunkV2(uint64_t device_id, const BYTE* buf, ULONG len); // 路径校验 + 无头 RecvFileChunkV2 void OnFileCompleteV2(uint64_t device_id, const BYTE* buf, ULONG len);// SHA-256 + filesDone 计数 bool WaitFileTransferDone(uint64_t device_id, int timeoutMs, std::vector& out); void ClearFileTransfer(uint64_t device_id); // 失败/超时/断线收尾(删半成品) ``` ### 5.2 流式子连接的路由(device_id 键,镜像 `TOKEN_DRIVE_LIST` 守卫) V2 下载的数据包 `COMMAND_SEND_FILE_V2`(85) / `COMMAND_FILE_COMPLETE_V2`(91) 本就由 `MessageHandle` 按 `ContextObject->GetClientID()` 分派(`2015RemoteDlg.cpp:5842` / `6101`)。MCP 只需在这两个 case 的 **`dstClientID==0`(M2C)分支顶部**加守卫: ```cpp case COMMAND_SEND_FILE_V2: { FileChunkPacketV2* pkt = (FileChunkPacketV2*)szBuffer; if (pkt->dstClientID == 0) { uint64_t devId = ContextObject->GetClientID(); if (McpServer().IsFileTransferPending(devId)) { // ← 新增守卫:无头接管 McpServer().OnFileChunkV2(devId, szBuffer, len); break; // 不建 CDlgFileSend、不设 hDlg } // ... 原 GUI 逻辑(建 CDlgFileSend + OnReceiveComplete)不变 } // ... C2C 分支(dstClientID != 0)完全不动 } ``` - `TOKEN_CONN_AUTH`(`6378`) **完全不动**——它只负责把 `clientID` 钉在 ctx 上,下载复用既有行为。 - `TOKEN_DRIVE_LIST`(`6430`) 既有 `IsPending(devId)` 守卫**扩展复用**:`OnDriveList` 识别当前挂起工具为 `download_file` 时,改发 `CMD_DOWN_FILES_V2` 而非 `COMMAND_LIST_FILES`,发完即 `CancelIO` 文件管理器子链接(数据走新流式子连接,§13 F3)。 - 守卫为假时全部回落原 GUI/C2C 路径——对既有功能零影响(原则 #1,§13 F7)。 --- ## 6. `download_file` 设计(P1) ### 6.1 工具 Schema ``` input: { id: string (必填, 主机 id) remote_path: string (必填, 远程文件或目录绝对路径, 如 C:\Users\shaun\Pictures) local_dir: string (必填, 主控本机保存目录; 不存在会自动创建) overwrite: boolean (可选, 默认 false; true 覆盖同名文件, false 则跳过) timeout_ms: integer (可选, 默认 600000, 上限 3600000) } output: { files: [{ path: string, size: integer, sha256: string }] // 实际落盘文件 total_bytes: integer skipped: integer } ``` ### 6.2 时序 ``` MCP工具线程 服务端 MessageHandle 客户端 │ 1. BeginFileTransferPending(id,"download_file",localDir,remotePath) │ │ 2. 主连接发 COMMAND_LIST_DRIVE ────────────────────────────────────────► 开文件管理器子链接 │ 3. ◄── TOKEN_DRIVE_LIST ─────────────────────┤ │ OnDriveList → 锁外下发 CMD_DOWN_FILES_V2[targetDir\0][remote_path\0]\0 │ │ 4. ────────────────────────────────────────► UploadToRemoteV2() │ (客户端另开一条鉴权流式子连接 ConnectServer) │ 5. 新子连接 TOKEN_CONN_AUTH 钉 clientID → 后续分块按 device_id 守卫接管 │ │ 6. ◄── COMMAND_SEND_FILE_V2 分块流 ──────────┤ │ → OnFileChunkV2 → RecvFileChunkV2() 无头落盘 │ 7. ◄── COMMAND_FILE_COMPLETE_V2(SHA-256) ────┤ │ 8. 校验通过 → done → 唤醒工具线程 → CancelIO 两条子连接 → 返回 │ ``` ### 6.3 服务端改动 1. **`McpServer.h`**:新增 `FileTransferSession` 结构 + 注册表 + 接口声明(§5.1)。 2. **`McpServer.cpp`**: - `BuildDownloadFileInputSchema/OutputSchema` + `BuildDownloadFile(...)`(`tools/call` 分派); - `OnFileChunkV2`:**先路径校验**(chunk `filename` 规范化后仍在 `local_dir` 内,防 `..` 穿越,§13 F5)→ 调 `RecvFileChunkV2(buf,len,nullptr,nullptr,hash,hmac,0)` 无头落盘,记 `totalFiles`/进度; - `OnFileCompleteV2`:`HandleFileCompleteV2` 校验 SHA-256 → `filesDone++`;`filesDone==totalFiles` 时置 `done` + `notify`(§13 F4); - `hash/hmac` 镜像 `FileManagerDlg.cpp:2755` 的 `GetPwdHash()/GetHMAC(100)`,兼容客户端 M2C `hmac` 为空(`client/FileManager.cpp:1167`,§13 F6); - `ClearFileTransfer`(发送失败/超时/断线):`CancelIO` 子链接 + 删除半成品。 3. **`2015RemoteDlg.cpp`(`MessageHandle`)**:`COMMAND_SEND_FILE_V2`(85)、`COMMAND_FILE_COMPLETE_V2`(91) 的 `dstClientID==0` 分支顶部加 `IsFileTransferPending(devId)` 守卫;`TOKEN_DRIVE_LIST` 处扩展 `OnDriveList` 识别 `download_file` 改发 `CMD_DOWN_FILES_V2`。`TOKEN_CONN_AUTH` 与 C2C 分支**完全不动**。 4. **`McpSettingsDlg.cpp/.h`**:新增 `McpFileTransfer` 配置项(默认 0)。 ### 6.4 落盘与路径安全 - 发 `CMD_DOWN_FILES_V2` 前把 `local_dir` 规范化为绝对路径并 `CreateDirectory`;`targetDir` 用 ANSI(MBCS 构建本机路径)。 - 每收到一个 chunk,校验其 `filename` 规范化后**前缀必须在 `local_dir` 内**(防 `..` 穿越,呼应 `FILE_TRANSFER_V2.md §8.3` 已识别的风险);逃逸则置 `error` 并 `CancelIO`。 - 编码:`remote_path` 走 UTF-8→ANSI(936)(Windows 客户端,复用 `McpServer.cpp` 既有 `ToAnsi`,与 `list_files` 一致)。 ### 6.5 超时与并发 - **P1 范围:同步 + 单文件/已打包目录**。一次 `tools/call` 阻塞到底,不引入 job/task 异步模型(决策见 §12)。核心场景「先 `terminal_exec` 压缩成单 zip,再 `download_file` 拉回」天然是单文件,同步足够。 - 默认 `timeout_ms=600000`、上限 3600000,因大文件远超 `kMcpToolTimeoutMs`(20s)。 - **单飞互斥(§13 F2)**:`BeginFileTransferPending` 与既有一次性 `m_Pending` **双向互斥**——下载登记时占用 `m_Pending`,反之 `BeginPending` 也检查 `m_FileXferSessions`,保证同 host 任意时刻只一个 MCP 工具在飞;并发返回 `-32003 Device busy`。 - 断线(`OfflineProc` 擦会话)+ 超时(删半成品)双收尾。 --- ## 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` 路径规范化,可选系统目录黑名单。 --- ## 8. 安全门 | 工具 | 定性 | 门槛 | |---|---|---| | `download_file` | 不改远程状态,但**数据外带** | `McpFileTransfer=1`(仅此一开关;**不依赖** `McpReadonly`,与 `list_files`/`get_screenshot` 同权) | | `upload_file` | **写远程盘** | `McpFileTransfer=1` **且** `McpReadonly=0`(复用既有「允许写」主开关) | - **只新增一个开关 `McpFileTransfer`(默认 0)**:download 只看它;upload 额外要求 `McpReadonly=0`。决策依据见 §12。 - `tools/list` 据此隐藏两个工具(与 `exec_command`/`terminal_*`/`remote_*` 同款开关判断)。 - 审计:`host_id + tool + remote_path + local_dir + 结果`,写 `Mprintf` + 审计日志。 --- ## 9. 改动文件清单 | 文件 | 改动 | |---|---| | `server/2015Remote/McpServer.h` | `FileTransferSession` + 注册表 + 8 个接口声明 | | `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`) | **客户端零改动**(`UploadToRemoteV2` / `RecvFileChunkV2` / `FileBatchTransferWorkerV2` 均已存在)。 --- ## 10. 分阶段实施与回滚 - **P1**:`download_file` + `McpFileTransfer` 开关 + 路由分支。可独立合入、独立验收(真实主机拖回一个目录,SHA-256 与 `certutil -hashfile` 比对一致)。 - **P2**:`upload_file`。 - **P3**(可选):断点续传(需先验证服务端续传状态落盘;文件管理器侧现 `enableResume=false`,`client/FileManager.cpp:1164`)、大文件进度流式上报。 - **回滚**:改动集中在 `McpServer.*` + `MessageHandle` 三个 `if` 分支,revert 当期 commit 即可,不影响既有 GUI 文件管理器。 --- ## 11. 测试点 - 单文件 / 目录(含中文名、深层嵌套)下载;SHA-256 与 `certutil -hashfile` 比对一致。 - `overwrite=false` 同名跳过;`local_dir` 不存在自动创建。 - 路径穿越:`remote_path` 含 `..` 或 chunk 文件名逃逸 `local_dir` 时被拒。 - 大文件(>2GB)超时与断线;并发下载同主机返回 `-32003`。 - 编码:GBK 中文文件名往返无乱码(与 `list_files` 同规则)。 - 断线收尾:传输中客户端掉线 → 会话擦除 + 半成品删除,无句柄/内存泄漏。 --- ## 12. 决策记录(以「简单易用」为原则) 原则:最少开关、最少认知负担、复用既有机制、不为低频场景预留复杂度;只有威胁模型确实需要时才加复杂度。 ### 12.1 download 数据外带 → **不加目录白名单,信任 `McpFileTransfer` + 审计** - **威胁模型**:MCP 默认 `127.0.0.1` + token;「数据外带」的实际顾虑是 AI 误操作或提示注入。但一旦开启 `terminal_exec`(全 shell、无白名单),外带通道远大于 download,目录白名单对已开终端者是冗余防御。 - **一致性**:`terminal_*` / `remote_*` 均只有开关 + 审计、无路径白名单;给 download 单独加白名单是特例,增加解释负担。 - **简单**:工具契约保持 `id + remote_path + local_dir`,不引入「允许根目录」概念。 - **逃生舱**:若未来确有需要,复用 `McpCmdWhitelist` 那种**单字符串**配置作可选收紧项,进 P3,不进 P1。 ### 12.2 大文件模型 → **P1 同步 + 大超时,收窄到单文件/已打包目录** - MCP 是 request/response;为一次性传输引入「提交任务 + 轮询进度 + 任务清理」违背「简单 + 每阶段独立可交付」。 - 核心场景(先压缩成单 zip 再拉回)天然是单文件,同步足够;`get_screenshot` 已先例式地内联返回字节。 - 异步任务模型进 P3,且**只在确有超大流式需求时**才做,不预建。 ### 12.3 upload 开关 → **单一 `McpFileTransfer`,upload 复用 `McpReadonly=0`** - `download_file` = `McpFileTransfer=1`(不依赖 `McpReadonly`):它只读远程,与 `list_files`/`get_screenshot` 同权,readonly 语义本就只拦「改远程」。 - `upload_file` = `McpFileTransfer=1 && McpReadonly=0`:upload 改远程,复用既有「允许写」主开关,与 `terminal_*`/`remote_*` 的 `X && !Readonly` 模式一致。 - 只新增 **1 个**开关;代价是「不能只开 upload 不开 download」——这是可接受的简化(无人有此诉求)。 --- ## 13. 专家审查记录(实施前) 原则优先级:① 对既有功能影响最小 → ② 简单易用 → ③ 优先 V2。逐条核对了 `2015RemoteDlg.cpp` 的 `MessageHandle` 分派、`client/FileManager.cpp` 与 `SimplePlugins/file_upload.cpp` 的实现,结论如下: | # | 审查发现 | 结论 | |---|---|---| | F1 | 路由机制比草案更简单:`COMMAND_SEND_FILE_V2`(`5842`)/`COMMAND_FILE_COMPLETE_V2`(`6101`) 本就按 `ContextObject->GetClientID()` 分派,流式子连接经 `TOKEN_CONN_AUTH`(`6411 SetID`) 钉住 clientID | **无需 context 路由表**。改为 `device_id` 键 + 两个 case 顶部的 `IsFileTransferPending(devId)` 守卫,`TOKEN_CONN_AUTH` 完全不动 | | F2 | 一次性 `m_Pending` 与新增文件会话可能并发冲突(同 host 双工具) | `BeginFileTransferPending` 与 `m_Pending` **双向互斥**,同 host 全 MCP 工具单飞 | | F3 | 文件管理器子链接发完 `CMD_DOWN_FILES_V2` 后即无用(数据走新流式子连接) | `OnDriveList` 发完命令即 `CancelIO` 该子链接;客户端 `UploadToRemoteV2` 用独立 `IOCPClient`,不依赖它 | | F4 | 完成信号是**每文件一个** `COMMAND_FILE_COMPLETE_V2`(`FileCompletePacketV2.fileIndex`) | 会话记 `totalFiles`(首 chunk)/`filesDone`(complete 计数),`filesDone==totalFiles` 才算 done | | F5 | `RecvFileChunkV2` 内部直接按 chunk `filename` 落盘 | MCP 在调用**前**校验 `filename` 规范化仍在 `local_dir` 内,逃逸即取消传输 | | F6 | `RecvFileChunkV2` 需正确 `hash/hmac` | 镜像 `FileManagerDlg.cpp:2755` 的 `GetPwdHash()/GetHMAC(100)`;客户端 M2C `hmac` 为空(`FileManager.cpp:1167`) | | F7 | 三处改动均为「加 if 守卫 + 现有逻辑作 else」,C2C 分支不动 | 结构性满足原则 #1,对既有 GUI/C2C 零影响 | **判定**:7 项问题均已在正文对应章节修正,无阻塞项,**定稿**。