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:
@@ -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);
|
||||||
|
|||||||
@@ -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 对应小节,无阻塞项,**定稿**。
|
||||||
|
|||||||
@@ -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;
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user