chore: update docs and tests

This commit is contained in:
eeee0717 2025-12-17 21:50:45 +08:00
parent c2c416ea93
commit 8bab2c8ebc
4 changed files with 121 additions and 212 deletions

View File

@ -1,6 +1,6 @@
# Cherry Studio 局域网传输协议规范 # Cherry Studio 局域网传输协议规范
> 版本: 3.0 > 版本: 1.0
> 最后更新: 2025-12 > 最后更新: 2025-12
本文档定义了 Cherry Studio 桌面客户端Electron与移动端Expo之间的局域网文件传输协议。 本文档定义了 Cherry Studio 桌面客户端Electron与移动端Expo之间的局域网文件传输协议。
@ -31,7 +31,7 @@
| **Client客户端** | Electron 桌面端 | 扫描服务、发起连接、发送文件 | | **Client客户端** | Electron 桌面端 | 扫描服务、发起连接、发送文件 |
| **Server服务端** | Expo 移动端 | 发布服务、接受连接、接收文件 | | **Server服务端** | Expo 移动端 | 发布服务、接受连接、接收文件 |
### 1.2 协议栈v3 ### 1.2 协议栈v1
``` ```
┌─────────────────────────────────────┐ ┌─────────────────────────────────────┐
@ -39,7 +39,6 @@
├─────────────────────────────────────┤ ├─────────────────────────────────────┤
│ 消息层(控制: JSON \n │ 消息层(控制: JSON \n
│ (数据: 二进制帧) │ │ (数据: 二进制帧) │
│ v3: 流式传输,无逐块确认 │
├─────────────────────────────────────┤ ├─────────────────────────────────────┤
│ 传输层TCP │ 传输层TCP
├─────────────────────────────────────┤ ├─────────────────────────────────────┤
@ -51,15 +50,11 @@
``` ```
1. 服务发现 → 移动端发布 mDNS 服务,桌面端扫描发现 1. 服务发现 → 移动端发布 mDNS 服务,桌面端扫描发现
2. TCP 握手 → 建立连接,交换设备信息(`version=3` 2. TCP 握手 → 建立连接,交换设备信息(`version=1`
3. 文件传输 → 控制消息使用 JSON`file_chunk` 使用二进制帧连续发送(无需等待确认) 3. 文件传输 → 控制消息使用 JSON`file_chunk` 使用二进制帧分块传输
4. 连接保活 → ping/pong 心跳 4. 连接保活 → ping/pong 心跳
``` ```
### 1.4 安全说明
该协议默认运行在**可信局域网**环境:协议本身不提供传输加密、身份鉴权或防中间人攻击能力。若需要在不可信网络使用,建议在握手阶段增加一次性配对码/Token并在传输层使用 TLS 或对文件数据进行端到端加密。
--- ---
## 2. 服务发现Bonjour/mDNS ## 2. 服务发现Bonjour/mDNS
@ -84,7 +79,7 @@
protocol: "tcp", // 协议 protocol: "tcp", // 协议
port: 53317, // TCP 监听端口 port: 53317, // TCP 监听端口
txt: { // TXT 记录(可选) txt: { // TXT 记录(可选)
version: "3", version: "1",
platform: "ios" // 或 "android" platform: "ios" // 或 "android"
} }
} }
@ -129,7 +124,7 @@ const preferredAddress = addresses.find((addr) => isIPv4(addr)) || addresses[0];
2. 连接成功后立即发送握手消息 2. 连接成功后立即发送握手消息
3. 等待服务端响应握手确认 3. 等待服务端响应握手确认
### 3.2 握手消息(协议版本 v3 ### 3.2 握手消息(协议版本 v1
#### Client → Server: `handshake` #### Client → Server: `handshake`
@ -137,7 +132,7 @@ const preferredAddress = addresses.find((addr) => isIPv4(addr)) || addresses[0];
type LanTransferHandshakeMessage = { type LanTransferHandshakeMessage = {
type: "handshake"; type: "handshake";
deviceName: string; // 设备名称 deviceName: string; // 设备名称
version: string; // 协议版本,当前为 "3" version: string; // 协议版本,当前为 "1"
platform?: string; // 平台:'darwin' | 'win32' | 'linux' platform?: string; // 平台:'darwin' | 'win32' | 'linux'
appVersion?: string; // 应用版本 appVersion?: string; // 应用版本
}; };
@ -149,7 +144,7 @@ type LanTransferHandshakeMessage = {
{ {
"type": "handshake", "type": "handshake",
"deviceName": "Cherry Studio 1.7.2", "deviceName": "Cherry Studio 1.7.2",
"version": "3", "version": "1",
"platform": "darwin", "platform": "darwin",
"appVersion": "1.7.2" "appVersion": "1.7.2"
} }
@ -157,10 +152,10 @@ type LanTransferHandshakeMessage = {
### 4. 消息格式规范(混合协议) ### 4. 消息格式规范(混合协议)
v3 采用"控制 JSON + 二进制数据帧"的混合协议,支持流式传输 v1 使用"控制 JSON + 二进制数据帧"的混合协议(流式传输模式,无 per-chunk ACK
- **控制消息**握手、心跳、file_start/ack、file_end、file_complete、file_cancelUTF-8 JSON`\n` 分隔 - **控制消息**握手、心跳、file_start/ack、file_end、file_completeUTF-8 JSON`\n` 分隔
- **数据消息**`file_chunk`):二进制帧,使用 Magic + 总长度做分帧,连续发送无需等待确认 - **数据消息**`file_chunk`):二进制帧,使用 Magic + 总长度做分帧,不经 Base64
### 4.1 控制消息编码JSON + `\n` ### 4.1 控制消息编码JSON + `\n`
@ -209,26 +204,25 @@ function sendControlMessage(socket: Socket, message: object): void {
3. 否则若首字节为 `{` → 按 JSON + `\n` 解析控制消息 3. 否则若首字节为 `{` → 按 JSON + `\n` 解析控制消息
4. 其它数据丢弃 1 字节并继续循环,避免阻塞。 4. 其它数据丢弃 1 字节并继续循环,避免阻塞。
### 4.4 消息类型汇总v3 ### 4.4 消息类型汇总v1
| 类型 | 方向 | 编码 | 用途 | | 类型 | 方向 | 编码 | 用途 |
| ---------------- | --------------- | -------- | ------------------------------ | | ---------------- | --------------- | -------- | ----------------------- |
| `handshake` | Client → Server | JSON+\n | 握手请求version=3 | | `handshake` | Client → Server | JSON+\n | 握手请求version=1 |
| `handshake_ack` | Server → Client | JSON+\n | 握手响应 | | `handshake_ack` | Server → Client | JSON+\n | 握手响应 |
| `ping` | Client → Server | JSON+\n | 心跳请求 | | `ping` | Client → Server | JSON+\n | 心跳请求 |
| `pong` | Server → Client | JSON+\n | 心跳响应 | | `pong` | Server → Client | JSON+\n | 心跳响应 |
| `file_start` | Client → Server | JSON+\n | 开始文件传输 | | `file_start` | Client → Server | JSON+\n | 开始文件传输 |
| `file_start_ack` | Server → Client | JSON+\n | 文件传输确认 | | `file_start_ack` | Server → Client | JSON+\n | 文件传输确认 |
| `file_chunk` | Client → Server | 二进制帧 | 文件数据块(连续发送,无确认) | | `file_chunk` | Client → Server | 二进制帧 | 文件数据块(无 Base64流式无 per-chunk ACK |
| `file_end` | Client → Server | JSON+\n | 文件传输结束 | | `file_end` | Client → Server | JSON+\n | 文件传输结束 |
| `file_complete` | Server → Client | JSON+\n | 传输完成结果 | | `file_complete` | Server → Client | JSON+\n | 传输完成结果 |
| `file_cancel` | Client → Server | JSON+\n | 取消传输 |
``` ```
{"type":"message_type",...其他字段...}\n {"type":"message_type",...其他字段...}\n
``` ```
### 4.5 消息发送 ### 4.3 消息发送
```typescript ```typescript
function sendMessage(socket: Socket, message: object): void { function sendMessage(socket: Socket, message: object): void {
@ -237,63 +231,48 @@ function sendMessage(socket: Socket, message: object): void {
} }
``` ```
### 4.6 消息接收与解析v3 混合协议) ### 4.4 消息接收与解析
```typescript ```typescript
const MAGIC = Buffer.from([0x43, 0x53]); // "CS" let buffer = "";
let buffer = Buffer.alloc(0);
socket.on("data", (chunk: Buffer) => { socket.on("data", (chunk: Buffer) => {
buffer = Buffer.concat([buffer, chunk]); buffer += chunk.toString("utf8");
while (buffer.length > 0) { let newlineIndex = buffer.indexOf("\n");
// 检查是否为二进制帧Magic: 0x43 0x53 while (newlineIndex !== -1) {
if (buffer.length >= 2 && buffer[0] === 0x43 && buffer[1] === 0x53) { const line = buffer.slice(0, newlineIndex).trim();
// 需要至少 6 字节头Magic + TotalLen buffer = buffer.slice(newlineIndex + 1);
if (buffer.length < 6) break;
const totalLen = buffer.readUInt32BE(2); if (line.length > 0) {
const frameLen = 6 + totalLen; // Magic(2) + TotalLen(4) + payload const message = JSON.parse(line);
handleMessage(message);
if (buffer.length < frameLen) break; // 等待更多数据
// 解析二进制帧
const type = buffer[6];
const transferIdLen = buffer.readUInt16BE(7);
const transferId = buffer.slice(9, 9 + transferIdLen).toString("utf8");
const chunkIndex = buffer.readUInt32BE(9 + transferIdLen);
const data = buffer.slice(13 + transferIdLen, frameLen);
handleBinaryChunk(transferId, chunkIndex, data);
buffer = buffer.slice(frameLen);
} }
// 检查是否为 JSON 控制消息(以 '{' 开头)
else if (buffer[0] === 0x7b) {
// '{' = 0x7b
const newlineIndex = buffer.indexOf(0x0a); // '\n' = 0x0a
if (newlineIndex === -1) break; // 等待完整的 JSON 行
const line = buffer.slice(0, newlineIndex).toString("utf8").trim(); newlineIndex = buffer.indexOf("\n");
buffer = buffer.slice(newlineIndex + 1);
if (line.length > 0) {
const message = JSON.parse(line);
handleMessage(message);
}
}
// 其他数据丢弃 1 字节,继续解析
else {
buffer = buffer.slice(1);
}
} }
}); });
``` ```
### 4.5 消息类型汇总
| 类型 | 方向 | 用途 |
| ---------------- | --------------- | ------------ |
| `handshake` | Client → Server | 握手请求 |
| `handshake_ack` | Server → Client | 握手响应 |
| `ping` | Client → Server | 心跳请求 |
| `pong` | Server → Client | 心跳响应 |
| `file_start` | Client → Server | 开始文件传输 |
| `file_start_ack` | Server → Client | 文件传输确认 |
| `file_chunk` | Client → Server | 文件数据块(流式,无 per-chunk ACK |
| `file_end` | Client → Server | 文件传输结束 |
| `file_complete` | Server → Client | 传输完成结果 |
--- ---
## 5. 文件传输协议 ## 5. 文件传输协议
### 5.1 传输流程v3 流式传输) ### 5.1 传输流程
``` ```
Client (Sender) Server (Receiver) Client (Sender) Server (Receiver)
@ -304,25 +283,23 @@ Client (Sender) Server (Receiver)
|<─── 2. file_start_ack ─────────────| |<─── 2. file_start_ack ─────────────|
| (接受/拒绝) | | (接受/拒绝) |
| | | |
|══════ 连续发送数据块(无确认)═══════| |══════ 循环发送数据块(流式,无 ACK ═════|
| | | |
|──── 3. file_chunk [0] ────────────>| |──── 3. file_chunk [0] ────────────>|
| |
|──── 3. file_chunk [1] ────────────>| |──── 3. file_chunk [1] ────────────>|
|──── 3. file_chunk [2] ────────────>| | |
| ... 连续发送所有块 ... | | ... 重复直到所有块发送完成 ... |
|──── 3. file_chunk [N-1] ──────────>|
| | | |
|══════════════════════════════════════ |══════════════════════════════════════
| | | |
|──── 4. file_end ──────────────────>| |──── 5. file_end ──────────────────>|
| (所有块已发送) | | (所有块已发送) |
| | | |
|<─── 5. file_complete ──────────────| |<─── 6. file_complete ──────────────|
| (校验和验证结果) | | (最终结果) |
``` ```
> **v3 特性**: 数据块连续发送,无需等待每块确认。接收端在收到 `file_end` 后验证完整性,通过 `file_complete` 返回最终结果。
### 5.2 消息定义 ### 5.2 消息定义
#### 5.2.1 `file_start` - 开始传输 #### 5.2.1 `file_start` - 开始传输
@ -352,8 +329,8 @@ type LanTransferFileStartMessage = {
"fileSize": 524288000, "fileSize": 524288000,
"mimeType": "application/zip", "mimeType": "application/zip",
"checksum": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456", "checksum": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456",
"totalChunks": 1000, "totalChunks": 8192,
"chunkSize": 524288 "chunkSize": 65536
} }
``` ```
@ -400,7 +377,11 @@ type LanTransferFileStartAckMessage = {
- `Type` 固定 `0x01``Data` 为原始文件二进制数据 - `Type` 固定 `0x01``Data` 为原始文件二进制数据
- 传输完整性依赖 `file_start.checksum`(全文件 SHA-256分块校验和可选不在帧中发送 - 传输完整性依赖 `file_start.checksum`(全文件 SHA-256分块校验和可选不在帧中发送
#### 5.2.4 `file_end` - 传输结束 #### 5.2.4 `file_chunk_ack` - 数据块确认v1 流式不使用)
v1 采用流式传输,不发送 per-chunk ACK。本节类型仅保留作为向后兼容参考实际不会发送。
#### 5.2.5 `file_end` - 传输结束
**方向:** Client → Server **方向:** Client → Server
@ -420,7 +401,7 @@ type LanTransferFileEndMessage = {
} }
``` ```
#### 5.2.5 `file_complete` - 传输完成 #### 5.2.6 `file_complete` - 传输完成
**方向:** Server → Client **方向:** Server → Client
@ -431,26 +412,9 @@ type LanTransferFileCompleteMessage = {
success: boolean; // 是否成功 success: boolean; // 是否成功
filePath?: string; // 保存路径(成功时) filePath?: string; // 保存路径(成功时)
error?: string; // 错误信息(失败时) error?: string; // 错误信息(失败时)
// v3 新增字段
errorCode?:
| "CHECKSUM_MISMATCH"
| "INCOMPLETE_TRANSFER"
| "DISK_ERROR"
| "CANCELLED";
receivedChunks?: number; // 实际接收的数据块数量
receivedBytes?: number; // 实际接收的字节数
}; };
``` ```
**错误码说明:**
| 错误码 | 说明 |
| --------------------- | -------------------------- |
| `CHECKSUM_MISMATCH` | 校验和不匹配 |
| `INCOMPLETE_TRANSFER` | 传输不完整,缺少数据块 |
| `DISK_ERROR` | 磁盘写入错误或存储空间不足 |
| `CANCELLED` | 传输被取消 |
**成功示例:** **成功示例:**
```json ```json
@ -458,9 +422,7 @@ type LanTransferFileCompleteMessage = {
"type": "file_complete", "type": "file_complete",
"transferId": "550e8400-e29b-41d4-a716-446655440000", "transferId": "550e8400-e29b-41d4-a716-446655440000",
"success": true, "success": true,
"filePath": "/storage/emulated/0/Documents/backup.zip", "filePath": "/storage/emulated/0/Documents/backup.zip"
"receivedChunks": 1000,
"receivedBytes": 524288000
} }
``` ```
@ -471,32 +433,7 @@ type LanTransferFileCompleteMessage = {
"type": "file_complete", "type": "file_complete",
"transferId": "550e8400-e29b-41d4-a716-446655440000", "transferId": "550e8400-e29b-41d4-a716-446655440000",
"success": false, "success": false,
"error": "File checksum verification failed", "error": "File checksum verification failed"
"errorCode": "CHECKSUM_MISMATCH",
"receivedChunks": 1000,
"receivedBytes": 524288000
}
```
#### 5.2.6 `file_cancel` - 取消传输
**方向:** Client → Server
```typescript
type LanTransferFileCancelMessage = {
type: "file_cancel";
transferId: string; // 传输 ID
reason?: string; // 取消原因
};
```
**示例:**
```json
{
"type": "file_cancel",
"transferId": "550e8400-e29b-41d4-a716-446655440000",
"reason": "Cancelled by user"
} }
``` ```
@ -519,7 +456,7 @@ async function calculateFileChecksum(filePath: string): Promise<string> {
#### 数据块校验和 #### 数据块校验和
v3 默认 **不传输分块校验和**,依赖最终文件 checksum。若需要可在应用层自定义非协议字段 v1 默认 **不传输分块校验和**,依赖最终文件 checksum。若需要可在应用层自定义非协议字段
### 5.4 校验流程 ### 5.4 校验流程
@ -602,25 +539,22 @@ type LanTransferPongMessage = {
### 7.1 超时配置 ### 7.1 超时配置
| 操作 | 超时时间 | 说明 | | 操作 | 超时时间 | 说明 |
| -------- | -------- | -------------------- | | ---------- | -------- | --------------------- |
| TCP 连接 | 10 秒 | 连接建立超时 | | TCP 连接 | 10 秒 | 连接建立超时 |
| 握手等待 | 10 秒 | 等待 `handshake_ack` | | 握手等待 | 10 秒 | 等待 `handshake_ack` |
| 传输完成 | 60 秒 | 等待 `file_complete` | | 传输完成 | 60 秒 | 等待 `file_complete` |
| 全局超时 | 10 分钟 | 整个文件传输超时 |
### 7.2 错误场景处理 ### 7.2 错误场景处理
| 场景 | Client 处理 | Server 处理 | | 场景 | Client 处理 | Server 处理 |
| ------------ | ------------------------ | ------------------------------- | | --------------- | ------------------ | ---------------------- |
| TCP 连接失败 | 通知 UI允许重试 | - | | TCP 连接失败 | 通知 UI允许重试 | - |
| 握手超时 | 断开连接,通知 UI | 关闭 socket | | 握手超时 | 断开连接,通知 UI | 关闭 socket |
| 握手被拒绝 | 显示拒绝原因 | - | | 握手被拒绝 | 显示拒绝原因 | - |
| 用户取消 | 发送 `file_cancel`,清理 | 清理临时文件 | | 数据块处理失败 | 中止传输,清理状态 | 清理临时文件 |
| 连接意外断开 | 清理状态,通知 UI | 清理临时文件 | | 连接意外断开 | 清理状态,通知 UI | 清理临时文件 |
| 存储空间不足 | - | 发送 `accepted: false` | | 存储空间不足 | - | 发送 `accepted: false` |
| 校验和失败 | 显示错误信息 | 发送 `file_complete` 带错误码 |
| 数据块缺失 | 显示错误信息 | 发送 `INCOMPLETE_TRANSFER` 错误 |
### 7.3 资源清理 ### 7.3 资源清理
@ -663,8 +597,8 @@ function cleanup(): void {
### 8.1 协议常量 ### 8.1 协议常量
```typescript ```typescript
// 协议版本v3 = 控制 JSON + 二进制 chunk 流式传输) // 协议版本v1 = 控制 JSON + 二进制 chunk + 流式传输)
export const LAN_TRANSFER_PROTOCOL_VERSION = "3"; export const LAN_TRANSFER_PROTOCOL_VERSION = "1";
// 服务发现 // 服务发现
export const LAN_TRANSFER_SERVICE_TYPE = "cherrystudio"; export const LAN_TRANSFER_SERVICE_TYPE = "cherrystudio";
@ -675,14 +609,11 @@ export const LAN_TRANSFER_TCP_PORT = 53317;
// 文件传输(与二进制帧一致) // 文件传输(与二进制帧一致)
export const LAN_TRANSFER_CHUNK_SIZE = 512 * 1024; // 512KB export const LAN_TRANSFER_CHUNK_SIZE = 512 * 1024; // 512KB
export const LAN_TRANSFER_MAX_FILE_SIZE = 500 * 1024 * 1024; // 500MB
export const LAN_TRANSFER_GLOBAL_TIMEOUT_MS = 10 * 60 * 1000; // 10 分钟 export const LAN_TRANSFER_GLOBAL_TIMEOUT_MS = 10 * 60 * 1000; // 10 分钟
// 注意:接收端必须支持至少 512KB 的分片大小,否则会拒收并返回类似
// "Chunk size 524288 exceeds limit 65536" 的错误。
// 超时设置 // 超时设置
export const LAN_TRANSFER_HANDSHAKE_TIMEOUT_MS = 10_000; // 10秒 export const LAN_TRANSFER_HANDSHAKE_TIMEOUT_MS = 10_000; // 10秒
export const LAN_TRANSFER_CHUNK_TIMEOUT_MS = 30_000; // 30秒
export const LAN_TRANSFER_COMPLETE_TIMEOUT_MS = 60_000; // 60秒 export const LAN_TRANSFER_COMPLETE_TIMEOUT_MS = 60_000; // 60秒
``` ```
@ -702,7 +633,7 @@ export const LAN_TRANSFER_ALLOWED_MIME_TYPES = [
## 9. 完整时序图 ## 9. 完整时序图
### 9.1 完整传输流程v3,流式传输) ### 9.1 完整传输流程v1,流式传输)
``` ```
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
@ -727,7 +658,7 @@ export const LAN_TRANSFER_ALLOWED_MIME_TYPES = [
│────────────────────────────────────>│ │ │────────────────────────────────────>│ │
│ │──────── TCP Connect ───────────────>│ │ │──────── TCP Connect ───────────────>│
│ │ │ │ │ │
│ │──────── handshake (v=3) ───────────>│ │ │──────── handshake ─────────────────>│
│ │ │ │ │ │
│ │<─────── handshake_ack ──────────────│ │ │<─────── handshake_ack ──────────────│
│ │ │ │ │ │
@ -744,20 +675,21 @@ export const LAN_TRANSFER_ALLOWED_MIME_TYPES = [
│ │ │ │ │ │
│ │<─────── file_start_ack ─────────────│ │ │<─────── file_start_ack ─────────────│
│ │ │ │ │ │
│ │══════ 连续发送数据块(无确认)═══════│ │ │ │
│ │══════ 循环发送数据块 ═══════════════│
│ │ │ │ │ │
│ │──────── file_chunk[0] (binary) ────>│ │ │──────── file_chunk[0] (binary) ────>│
<────── progress event ──────────────│ │
│ │ │
│ │──────── file_chunk[1] (binary) ────>│ │ │──────── file_chunk[1] (binary) ────>│
│ │──────── file_chunk[2] (binary) ────>│ <────── progress event ──────────────│
<────── progress event ──────────────│ ... 连续发送 ...
│ │──────── file_chunk[N-1] (binary) ──> │ │ ... 重复 ...
│ │ │ │ │ │
│ │══════════════════════════════════════│ │ │══════════════════════════════════════│
│ │ │ │ │ │
│ │──────── file_end ──────────────────>│ │ │──────── file_end ──────────────────>│
│ │ │ │ │ │
│ │ (接收端验证 checksum) │
│ │ │
│ │<─────── file_complete ──────────────│ │ │<─────── file_complete ──────────────│
│ │ │ │ │ │
<────── complete event ──────────────│ │ <────── complete event ──────────────│ │
@ -765,11 +697,9 @@ export const LAN_TRANSFER_ALLOWED_MIME_TYPES = [
│ │ │ │ │ │
``` ```
> **v3 特性**: 数据块连续发送,不等待单个确认,大幅提高传输速度。接收端在 `file_end` 后统一验证完整性。
--- ---
## 10. 移动端实现指南v3 要点) ## 10. 移动端实现指南v1 要点)
### 10.1 必须实现的功能 ### 10.1 必须实现的功能
@ -791,21 +721,16 @@ export const LAN_TRANSFER_ALLOWED_MIME_TYPES = [
4. **握手处理** 4. **握手处理**
- 验证 `handshake` 消息version=3 - 验证 `handshake` 消息
- 发送 `handshake_ack` 响应 - 发送 `handshake_ack` 响应
- 响应 `ping` 消息 - 响应 `ping` 消息
5. **文件接收v3 流式模式)** 5. **文件接收(流式模式)**
- 解析 `file_start`,准备接收 - 解析 `file_start`,准备接收
- 接收 `file_chunk` 二进制帧,直接写入文件并增量计算哈希 - 接收 `file_chunk` 二进制帧,直接写入文件/缓冲并增量哈希
- **无需发送 `file_chunk_ack`**v3 移除了逐块确认 - v1 不发送 per-chunk ACK流式传输
- 处理 `file_end`,完成增量哈希并校验 checksum - 处理 `file_end`,完成增量哈希并校验 checksum
- 发送 `file_complete` 结果(包含 errorCode、receivedChunks、receivedBytes - 发送 `file_complete` 结果
6. **取消处理**
- 监听 `file_cancel` 消息
- 清理临时文件和状态
### 10.2 推荐的库 ### 10.2 推荐的库
@ -827,7 +752,7 @@ class FileReceiver {
totalChunks: number; totalChunks: number;
receivedChunks: number; receivedChunks: number;
tempPath: string; tempPath: string;
// v3: 边收边写文件,避免大文件 OOM // v1: 边收边写文件,避免大文件 OOM
// stream: FileSystem writable stream (平台相关封装) // stream: FileSystem writable stream (平台相关封装)
}; };
@ -842,13 +767,10 @@ class FileReceiver {
case "file_start": case "file_start":
this.handleFileStart(message); this.handleFileStart(message);
break; break;
// v3: file_chunk 为二进制帧,不走 JSON 分支 // v1: file_chunk 为二进制帧,不再走 JSON 分支
case "file_end": case "file_end":
this.handleFileEnd(message); this.handleFileEnd(message);
break; break;
case "file_cancel":
this.handleFileCancel(message);
break;
} }
} }
@ -859,12 +781,12 @@ class FileReceiver {
// 4. 发送 file_start_ack // 4. 发送 file_start_ack
} }
// v3: 二进制帧处理在 socket data 流中解析,随后调用 handleBinaryFileChunk // v1: 二进制帧处理在 socket data 流中解析,随后调用 handleBinaryFileChunk
handleBinaryFileChunk(transferId: string, chunkIndex: number, data: Buffer) { handleBinaryFileChunk(transferId: string, chunkIndex: number, data: Buffer) {
// 直接使用二进制数据,按 chunkSize/lastChunk 计算长度 // 直接使用二进制数据,按 chunkSize/lastChunk 计算长度
// 写入文件流并更新增量 SHA-256 // 写入文件流并更新增量 SHA-256
this.transfer.receivedChunks++; this.transfer.receivedChunks++;
// v3: 无需发送 file_chunk_ack连续接收数据块即可 // v1: 流式传输,不发送 per-chunk ACK
} }
handleFileEnd(msg: LanTransferFileEndMessage) { handleFileEnd(msg: LanTransferFileEndMessage) {
@ -922,13 +844,11 @@ export interface LanTransferFileStartMessage {
chunkSize: number; chunkSize: number;
} }
// v3: file_chunk 以二进制帧传输,不是 JSON 消息 export interface LanTransferFileChunkMessage {
// 帧格式: Magic(2B) + TotalLen(4B) + Type(1B, 0x01) + TransferIdLen(2B) + TransferId(nB) + ChunkIndex(4B) + Data type: "file_chunk";
export interface LanTransferFileChunkBinaryFrame {
type: 0x01; // 二进制帧类型标识 (file_chunk)
transferId: string; transferId: string;
chunkIndex: number; chunkIndex: number;
data: Buffer; // 原始二进制数据 data: string; // Base64 encoded (v1: 二进制帧模式下不使用)
} }
export interface LanTransferFileEndMessage { export interface LanTransferFileEndMessage {
@ -936,12 +856,6 @@ export interface LanTransferFileEndMessage {
transferId: string; transferId: string;
} }
export interface LanTransferFileCancelMessage {
type: "file_cancel";
transferId: string;
reason?: string;
}
// 文件传输响应消息 (Server -> Client) // 文件传输响应消息 (Server -> Client)
export interface LanTransferFileStartAckMessage { export interface LanTransferFileStartAckMessage {
type: "file_start_ack"; type: "file_start_ack";
@ -950,7 +864,14 @@ export interface LanTransferFileStartAckMessage {
message?: string; message?: string;
} }
// v3: 移除了 LanTransferFileChunkAckMessage流式传输无需逐块确认 // v1 流式不发送 per-chunk ACK以下类型仅用于向后兼容参考
export interface LanTransferFileChunkAckMessage {
type: "file_chunk_ack";
transferId: string;
chunkIndex: number;
received: boolean;
error?: string;
}
export interface LanTransferFileCompleteMessage { export interface LanTransferFileCompleteMessage {
type: "file_complete"; type: "file_complete";
@ -958,30 +879,18 @@ export interface LanTransferFileCompleteMessage {
success: boolean; success: boolean;
filePath?: string; filePath?: string;
error?: string; error?: string;
// v3 新增字段
errorCode?:
| "CHECKSUM_MISMATCH"
| "INCOMPLETE_TRANSFER"
| "DISK_ERROR"
| "CANCELLED";
receivedChunks?: number;
receivedBytes?: number;
} }
// 常量 // 常量
export const LAN_TRANSFER_PROTOCOL_VERSION = "3";
export const LAN_TRANSFER_TCP_PORT = 53317; export const LAN_TRANSFER_TCP_PORT = 53317;
export const LAN_TRANSFER_CHUNK_SIZE = 512 * 1024; export const LAN_TRANSFER_CHUNK_SIZE = 512 * 1024;
export const LAN_TRANSFER_MAX_FILE_SIZE = 500 * 1024 * 1024; export const LAN_TRANSFER_CHUNK_TIMEOUT_MS = 30_000;
// v3: 移除了 CHUNK_TIMEOUT_MS流式传输无需逐块等待超时
``` ```
--- ---
## 附录 B版本历史 ## 附录 B版本历史
| 版本 | 日期 | 变更 | | 版本 | 日期 | 变更 |
| ---- | ------- | ------------------------------------------------------------------ | | ---- | ------- | ---------------------------------------- |
| 1.0 | 2025-12 | 初始版本,与移动端实现对齐 | | 1.0 | 2025-12 | 初始发布版本,支持二进制帧格式与流式传输 |
| 2.0 | 2025-12 | 引入二进制帧格式传输数据块,仍使用逐块 ACK 确认 |
| 3.0 | 2025-12 | 流式传输模式,移除 `file_chunk_ack`,增强 `file_complete` 错误诊断 |

View File

@ -162,7 +162,7 @@ export const LAN_TRANSFER_COMPLETE_TIMEOUT_MS = 60_000 // 60s - wait for file_co
export const LAN_TRANSFER_GLOBAL_TIMEOUT_MS = 10 * 60 * 1000 // 10 minutes - global transfer timeout export const LAN_TRANSFER_GLOBAL_TIMEOUT_MS = 10 * 60 * 1000 // 10 minutes - global transfer timeout
// Binary protocol constants (v3) // Binary protocol constants (v3)
export const LAN_TRANSFER_PROTOCOL_VERSION = '3' export const LAN_TRANSFER_PROTOCOL_VERSION = '1'
export const LAN_BINARY_FRAME_MAGIC = 0x4353 // "CS" as uint16 export const LAN_BINARY_FRAME_MAGIC = 0x4353 // "CS" as uint16
export const LAN_BINARY_TYPE_FILE_CHUNK = 0x01 export const LAN_BINARY_TYPE_FILE_CHUNK = 0x01

View File

@ -29,7 +29,7 @@ describe('connection handlers', () => {
}) })
it('should use protocol version 3', () => { it('should use protocol version 3', () => {
expect(HANDSHAKE_PROTOCOL_VERSION).toBe('3') expect(HANDSHAKE_PROTOCOL_VERSION).toBe('1')
}) })
}) })

View File

@ -7,7 +7,7 @@ import { app } from 'electron'
import type { ConnectionContext } from '../types' import type { ConnectionContext } from '../types'
export const HANDSHAKE_PROTOCOL_VERSION = '3' export const HANDSHAKE_PROTOCOL_VERSION = '1'
const logger = loggerService.withContext('LanTransferConnection') const logger = loggerService.withContext('LanTransferConnection')