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
397 lines
32 KiB
Markdown
397 lines
32 KiB
Markdown
# YAMA MCP 文件传输功能设计(download_file / upload_file)
|
||
|
||
> **状态**:设计定稿(经专家审查修订,结论见 §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 协议/架构/配置)。
|
||
|
||
---
|
||
|
||
## 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` 既有;客户端零改动(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);断线/超时必须清会话 + 关子链接 + 删半成品文件。
|
||
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<FileEntry> files; // {path, size} 已落盘/已发送文件
|
||
std::vector<FileEntry> skipped; // overwrite=false 时跳过的同名文件
|
||
time_t startAt = 0;
|
||
};
|
||
std::mutex m_FileXferMutex;
|
||
std::map<uint64_t, FileTransferSession> 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<FileEntry>& 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)
|
||
|
||
### 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_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 间震荡,语义不变。
|
||
|
||
---
|
||
|
||
## 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)`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` 均已存在、未改动;仅 `COMMAND_SEND_FILE_V2` 分支新增懒初始化(见 §7.8)。
|
||
|
||
---
|
||
|
||
## 10. 分阶段实施与回滚
|
||
|
||
- **P1**:`download_file` + `McpFileTransfer` 开关 + 路由分支。可独立合入、独立验收(真实主机拖回一个目录,SHA-256 与 `certutil -hashfile` 比对一致)。
|
||
- **P2**:`upload_file`(§7,主连接复用 + 发送方驱动,`MessageHandle` 零改动)。可独立合入、独立验收(推一个目录到真实主机,SHA-256 与源文件 `certutil -hashfile` 比对一致)。
|
||
- **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` 同规则)。
|
||
- 断线收尾:传输中客户端掉线 → 会话擦除 + 半成品删除,无句柄/内存泄漏。
|
||
- **上传(P2)**:单文件 / 目录(含中文名、深层嵌套)上传;`remote_dir` 不存在自动创建;SHA-256 与源文件 `certutil -hashfile` 比对一致。
|
||
- 上传 `overwrite=false` 同名跳过(`skipped` 计数);`overwrite=true` 覆盖。
|
||
- 上传 `remote_dir` 含 `..` 逃逸被拒;大文件(>2GB)超时与断线;并发上传/上传-下载同主机返回 `-32003`。
|
||
- 上传编码:GBK 中文文件名往返无乱码。
|
||
|
||
---
|
||
|
||
## 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 项问题均已在正文对应章节修正,无阻塞项,**定稿**。
|
||
|
||
### 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 对应小节,无阻塞项,**定稿**。
|