Files
SimpleRemoter/docs/Mcp_FileTransfer_Design.md
yuanyuanxiang 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

32 KiB
Raw Permalink Blame History

YAMA MCP 文件传输功能设计download_file / upload_file

状态:设计定稿(经专家审查修订,结论见 §13download_fileP1已实施并验收upload_fileP2已实施详见 §7评审修订见 §13.1)。download_file 采用 V2 文件传输协议CMD_DOWN_FILES_V2 + 流式子连接 + SHA-256 校验),upload_file 复用服务端既有 FileBatchTransferWorkerV2(主连接同步驱动)。两者共享同一套「文件会话」注册表。 读者MCP 后续功能研发/评审人员。 关联文档Mcp_Phase2_Design.md(双模式无头驱动机制)、FILE_TRANSFER_V2.mdV2 协议清单)、Mcp_Design.mdPhase 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_DRIVETOKEN_DRIVE_LISTCOMMAND_LIST_FILESTOKEN_FILE_LIST McpServer.cpp:1741list_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(下发)、2756RecvFileChunkV2 落盘) 仅 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:128RecvFileChunkV2(buf, len, nullptr, nullptr, hash, hmac, 0) 无头落盘;落盘状态在 SimplePlugins/file_upload.cpp 内部按 transferID 自维护,天然适合 MCP 无头复用。

  3. V2 落盘路径由客户端回填CMD_DOWN_FILES_V2targetDir主控本机保存目录(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-256COMMAND_FILE_COMPLETE_V2
大文件 64 位偏移,>4GB 64 位偏移但无校验
客户端代码 已存在,是当前 GUI 正规路径 ⚠️ 遗留路径
MCP 侧复杂度 需路由一条新流式子连接(新增会话注册表) 需在 MessageHandle 维护「发 CONTINUE/收 DATA」状态机逐块 ACK 慢

结论:采用 V2。 代价是服务端要新增「流式文件会话」注册与路由;但这是 upload_file 同样要复用的基础设施一次投入两处受益。V1 仅作「不想引入新子连接路由」时的降级备案,不在本期实现。


4. 设计原则

  1. 复用现有协议,不新造命令CMD_DOWN_FILES_V2COMMAND_SEND_FILE_V2COMMAND_FILE_COMPLETE_V2COMMAND_LIST_DRIVE 全部为 common/commands.h 既有客户端零改动upload 方向例外:COMMAND_SEND_FILE_V2 需懒初始化文件模块,见 §7.8)。
  2. 无头接管复用会话模式:镜像 McpServer.hTermSession / ScreenCtrlSession,新增 FileTransferSessionMessageHandleIsFileTransferContext(context*) 判定是否路由到 MCP 无头落盘,否则回落 GUI 进度框。
  3. 单设备单传输会话:与终端/远程控制一致,避免同 host 并发传输的归属歧义;并发返回 -32003 Device busy
  4. 超时与清理是硬约束:大文件传输是长任务,超时需独立于 kMcpToolTimeoutMs(20s);断线/超时必须清会话 + 关子链接 + 删半成品文件。
  5. 输出 schema 先行:两个工具先在 tools/list 声明完整 inputSchema/outputSchema

5. 核心机制:流式文件会话

5.1 会话注册表(McpServer.h/.cpp

struct FileTransferSession {
    std::string  tool;           // "download_file" / "upload_file"
    std::string  localDir;       // download: 本机落盘目录(规范化后绝对路径,用于穿越校验)
    std::string  remotePath;     // 审计用原始远程路径UTF-8
    uint64_t     transferID = 0; // 绑定后从首包填充downloadupload 由本端 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 同构):

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) 本就由 MessageHandleContextObject->GetClientID() 分派(2015RemoteDlg.cpp:5842 / 6101。MCP 只需在这两个 case 的 dstClientID==0M2C分支顶部加守卫:

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/进度;
    • OnFileCompleteV2HandleFileCompleteV2 校验 SHA-256 → filesDone++filesDone==totalFiles 时置 done + notify§13 F4
    • hash/hmac 镜像 FileManagerDlg.cpp:2755GetPwdHash()/GetHMAC(100),兼容客户端 M2C hmac 为空(client/FileManager.cpp:1167§13 F6
    • ClearFileTransfer(发送失败/超时/断线):CancelIO 子链接 + 删除半成品。
  3. 2015RemoteDlg.cppMessageHandleCOMMAND_SEND_FILE_V2(85)、COMMAND_FILE_COMPLETE_V2(91) 的 dstClientID==0 分支顶部加 IsFileTransferPending(devId) 守卫;TOKEN_DRIVE_LIST 处扩展 OnDriveList 识别 download_file 改发 CMD_DOWN_FILES_V2TOKEN_CONN_AUTH 与 C2C 分支完全不动
  4. McpSettingsDlg.cpp/.h:新增 McpFileTransfer 配置项(默认 0

6.4 落盘与路径安全

  • CMD_DOWN_FILES_V2 前把 local_dir 规范化为绝对路径并 CreateDirectorytargetDir 用 ANSIMBCS 构建本机路径)。
  • 每收到一个 chunk校验其 filename 规范化后前缀必须在 local_dir(防 .. 穿越,呼应 FILE_TRANSFER_V2.md §8.3 已识别的风险);逃逸则置 errorCancelIO
  • 编码:remote_path 走 UTF-8→ANSI(936)Windows 客户端,复用 McpServer.cpp 既有 ToAnsi,与 list_files 一致)。

6.5 超时与并发

  • P1 范围:同步 + 单文件/已打包目录。一次 tools/call 阻塞到底,不引入 job/task 异步模型(决策见 §12。核心场景「先 terminal_exec 压缩成单 zipdownload_file 拉回」天然是单文件,同步足够。
  • 默认 timeout_ms=600000、上限 3600000因大文件远超 kMcpToolTimeoutMs(20s)。
  • 单飞互斥§13 F2BeginFileTransferPending 与既有一次性 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_fileP1 upload_fileP2
数据方向 远程客户端 → 主控 主控 → 远程客户端
发送方 客户端(UploadToRemoteV2FileBatchTransferWorkerV2 主控(FileBatchTransferWorkerV2
连接 客户端新开一条鉴权流式子连接 复用主连接ctx->Send2Client),无子连接
完成信号 客户端发 COMMAND_FILE_COMPLETE_V2,服务端 OnFileCompleteV2 校验计数 服务端作为发送方自己发 COMMAND_FILE_COMPLETE_V2SHA-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:38ExpandDirectories 未在 file_upload.h 导出,故在 McpServer.cpp 本地同构实现,目录项在前、子项随后);
    • overwrite 预检:复用 list_filesCOMMAND_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=0dstClientID=clientIDenableResume=false
    • 完成判定:result==0 && FindHost(clientID)!=nullptr(镜像 GUI SendFilesToClientV2Internal 末尾)。
  3. 2015RemoteDlg.cppMessageHandle零改动
  4. McpSettingsDlg.cpp/.h:零改动(McpFileTransfer 已存在§8 复用)。

7.5 完整性边界(诚实声明)

  • V2 无接收方→发送方 ACK客户端 HandleFileCompleteV2 校验失败只打日志(RecvFileChunkV2 返回 8KernelManager.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==1SimplePlugins/file_upload.cpp:2812 if (!g_status) return -1;),但客户端主连接此前从不初始化文件传输模块——InitFileUpload 只在 FileManager/ScreenManager 构造里成对调用(下载/屏幕方向),上传走主连接时 g_status==0,每个 chunk 都被 RecvFileChunkV2 直接丢弃(表现为落盘 0 字节 / 截断)。

修法:在 client/KernelManager.cppCOMMAND_SEND_FILE_V2 分支加懒初始化(进程内仅一次):

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 起永不归 0g_status 恒为 1。
  • InitFileUpload 幂等(g_fileStatesMtx + g_threadCount 引用计数,二次调用 g_threadCount>1 早退)、非阻塞(仅置标志 + 派生 detach 线程、license 校验在启动期 licenseInit() 后必过、verifyMessage 对空签名 return falselicense.cpp:244)不会越界。
  • 影响面:主连接 OnReceive 无 V1 COMMAND_SEND_FILE 分支,g_status=1 只作用于 RecvFileChunkV2FileManager/ScreenManager 的成对 Init/Uninit 只是在 1↔2 间震荡,语义不变。

8. 安全门

工具 定性 门槛
download_file 不改远程状态,但数据外带 McpFileTransfer=1(仅此一开关;不依赖 McpReadonly,与 list_files/get_screenshot 同权)
upload_file 写远程盘 McpFileTransfer=1 McpReadonly=0(复用既有「允许写」主开关)
  • 只新增一个开关 McpFileTransfer(默认 0download 只看它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 MessageHandleCOMMAND_SEND_FILE_V2(85) / COMMAND_FILE_COMPLETE_V2(91) 两处守卫分支 + TOKEN_DRIVE_LISTOnDriveList 扩展;TOKEN_CONN_AUTH 不动
server/2015Remote/McpSettingsDlg.cpp/.h McpFileTransfer 配置项
P2server/2015Remote/McpServer.h FileTransferSession.tool"upload_file";新增 BeginFileUpload(id)(单设备单传输互斥标记)
P2server/2015Remote/McpServer.cpp upload_file schema/实现:无头回调 UploadSendChunkHeadless + CollectLocalFiles 收集 + overwrite 预检(复用 list_files 链路)+ 驱动 FileBatchTransferWorkerV2(复用主连接,无子连接路由)
P2client/KernelManager.cpp COMMAND_SEND_FILE_V2 分支加懒初始化 InitFileUploadg_status 由 0 置 1见 §7.8

客户端UploadToRemoteV2 / RecvFileChunkV2 / FileBatchTransferWorkerV2 均已存在、未改动;仅 COMMAND_SEND_FILE_V2 分支新增懒初始化(见 §7.8)。


10. 分阶段实施与回滚

  • P1download_file + McpFileTransfer 开关 + 路由分支。可独立合入、独立验收真实主机拖回一个目录SHA-256 与 certutil -hashfile 比对一致)。
  • P2upload_file§7主连接复用 + 发送方驱动,MessageHandle 零改动。可独立合入、独立验收推一个目录到真实主机SHA-256 与源文件 certutil -hashfile 比对一致)。
  • P3(可选):断点续传(需先验证服务端续传状态落盘;文件管理器侧现 enableResume=falseclient/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 开关 → 单一 McpFileTransferupload 复用 McpReadonly=0

  • download_file = McpFileTransfer=1(不依赖 McpReadonly):它只读远程,与 list_files/get_screenshot 同权readonly 语义本就只拦「改远程」。
  • upload_file = McpFileTransfer=1 && McpReadonly=0upload 改远程,复用既有「允许写」主开关,与 terminal_*/remote_*X && !Readonly 模式一致。
  • 只新增 1 个开关;代价是「不能只开 upload 不开 download」——这是可接受的简化无人有此诉求

13. 专家审查记录(实施前)

原则优先级:① 对既有功能影响最小 → ② 简单易用 → ③ 优先 V2。逐条核对了 2015RemoteDlg.cppMessageHandle 分派、client/FileManager.cppSimplePlugins/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 双工具) BeginFileTransferPendingm_Pending 双向互斥,同 host 全 MCP 工具单飞
F3 文件管理器子链接发完 CMD_DOWN_FILES_V2 后即无用(数据走新流式子连接) OnDriveList 发完命令即 CancelIO 该子链接;客户端 UploadToRemoteV2 用独立 IOCPClient,不依赖它
F4 完成信号是每文件一个 COMMAND_FILE_COMPLETE_V2FileCompletePacketV2.fileIndex 会话记 totalFiles(首 chunk/filesDonecomplete 计数),filesDone==totalFiles 才算 done
F5 RecvFileChunkV2 内部直接按 chunk filename 落盘 MCP 在调用校验 filename 规范化仍在 local_dir 内,逃逸即取消传输
F6 RecvFileChunkV2 需正确 hash/hmac 镜像 FileManagerDlg.cpp:2755GetPwdHash()/GetHMAC(100);客户端 M2C hmac 为空(FileManager.cpp:1167
F7 三处改动均为「加 if 守卫 + 现有逻辑作 else」C2C 分支不动 结构性满足原则 #1对既有 GUI/C2C 零影响

判定7 项问题均已在正文对应章节修正,无阻塞项,定稿

13.1 upload_file 评审记录P2实施前

核对了 2015RemoteDlg.cppSendFilesToClientV2Internal/SendFileChunkToClientV2GUI upload 发送方)、SimplePlugins/file_upload.cppFileBatchTransferWorkerV2/RecvFileChunkV2client/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_filesCOMMAND_LIST_DRIVE→TOKEN_DRIVE_LIST 预检 remote_dir,过滤同名进 skipped
U6 本地文件收集 common/file_upload.cpp:38 ExpandDirectories 未在 file_upload.h 导出,改为 McpServer.cpp 本地 CollectLocalFiles 同构实现(目录项在前、子项随后)
U7 实施验证发现:客户端主连接 g_status==0RecvFileChunkV2 逐 chunk return -1(上传落盘 0 字节/截断) 客户端 COMMAND_SEND_FILE_V2 分支加懒初始化§7.8static 一次 + 永久引用计数、析构不 Uninit;与 FileManager.cpp:40 参数一致,独立评审确认无稳定性风险

判定7 项均已写入 §7 对应小节,无阻塞项,定稿