mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-05 21:50:46 +08:00
Dissolve the by-kind @shared/config junk drawer per shared-layer governance: route each member by shape and actual consumer process — cross-process slices into types//utils//ai/, single-process code back into main/renderer (Invariant 1.1). Confirm each item's real consumer process rather than trusting the directional plan (API_SERVER_DEFAULTS is renderer-only, MIN_WINDOW_* is cross-process, providers.ts is renderer-only), and drop dead consts (ZOOM_LEVELS/ZOOM_OPTIONS, bookExts, thirdPartyApplicationExts). Purge runtime logic from types/ so the bucket holds only declarations: move serializeError + AI-SDK error guards to utils/error.ts, the FileHandle factories/guards to utils/file/handle.ts, isSerializable + SerializableSchema to utils/serializable.ts, and the tab-instance guard/normalizer to utils/tabInstanceMetadata.ts. Tests follow the logic to utils/__tests__; the type-level ipc contract test is retired (its invariants kept as a breadcrumb for the future IpcApi Zod schema). types/ is now logic-free and test-free. Also: remove the dead @shared mock from the packages/ui code-editor test so packages/ui no longer references production code; fix the data-classify preference generator prompts import and the update-languages output path to the relocated modules. Update shared-layer-architecture (3.1 type/util test rule, 5/6 config dissolution) and renderer-architecture cross-references.
422 lines
16 KiB
Markdown
422 lines
16 KiB
Markdown
# Cherry Studio LAN Transfer Protocol Specification
|
|
|
|
> Version: 1.0
|
|
> Last Updated: 2025-12
|
|
|
|
This document defines the LAN file transfer protocol between the Cherry Studio desktop client (Electron) and mobile client (Expo).
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Protocol Overview](#1-protocol-overview)
|
|
2. [Service Discovery (Bonjour/mDNS)](#2-service-discovery-bonjourmdns)
|
|
3. [TCP Connection and Handshake](#3-tcp-connection-and-handshake)
|
|
4. [Message Format Specification](#4-message-format-specification)
|
|
5. [File Transfer Protocol](#5-file-transfer-protocol)
|
|
6. [Heartbeat and Keep-alive](#6-heartbeat-and-keep-alive)
|
|
7. [Error Handling](#7-error-handling)
|
|
8. [Constants and Configuration](#8-constants-and-configuration)
|
|
9. [Complete Sequence Diagram](#9-complete-sequence-diagram)
|
|
10. [Mobile Implementation Guide](#10-mobile-implementation-guide)
|
|
|
|
---
|
|
|
|
## 1. Protocol Overview
|
|
|
|
### 1.1 Architecture Roles
|
|
|
|
| Role | Platform | Responsibility |
|
|
|------|----------|---------------|
|
|
| **Client** | Electron Desktop | Scan services, initiate connections, send files |
|
|
| **Server** | Expo Mobile | Publish services, accept connections, receive files |
|
|
|
|
### 1.2 Protocol Stack (v1)
|
|
|
|
```
|
|
┌─────────────────────────────────────┐
|
|
│ Application Layer (File Transfer)│
|
|
├─────────────────────────────────────┤
|
|
│ Message Layer (Control: JSON \n) │
|
|
│ (Data: Binary Frame)│
|
|
├─────────────────────────────────────┤
|
|
│ Transport Layer (TCP) │
|
|
├─────────────────────────────────────┤
|
|
│ Discovery Layer (Bonjour/mDNS) │
|
|
└─────────────────────────────────────┘
|
|
```
|
|
|
|
### 1.3 Communication Flow Overview
|
|
|
|
```
|
|
1. Service Discovery → Mobile publishes mDNS service, Desktop scans and discovers
|
|
2. TCP Handshake → Establish connection, exchange device info (version=1)
|
|
3. File Transfer → Control messages use JSON, file_chunk uses binary frame chunked transfer
|
|
4. Keep-alive → ping/pong heartbeat
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Service Discovery (Bonjour/mDNS)
|
|
|
|
### 2.1 Service Type
|
|
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| Service Type | `cherrystudio` |
|
|
| Protocol | `tcp` |
|
|
| Full Service ID | `_cherrystudio._tcp` |
|
|
|
|
### 2.2 Service Publishing (Mobile)
|
|
|
|
Mobile must publish the service via mDNS/Bonjour:
|
|
|
|
```typescript
|
|
{
|
|
name: "Cherry Studio Mobile",
|
|
type: "cherrystudio",
|
|
protocol: "tcp",
|
|
port: 53317,
|
|
txt: {
|
|
version: "1",
|
|
platform: "ios" // or "android"
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2.3 Service Discovery (Desktop)
|
|
|
|
Desktop scans and resolves service information:
|
|
|
|
```typescript
|
|
type LanTransferPeer = {
|
|
id: string;
|
|
name: string;
|
|
host?: string;
|
|
fqdn?: string;
|
|
port?: number;
|
|
type?: string;
|
|
protocol?: 'tcp' | 'udp';
|
|
addresses: string[];
|
|
txt?: Record<string, string>;
|
|
updatedAt: number;
|
|
}
|
|
```
|
|
|
|
### 2.4 IP Address Selection Strategy
|
|
|
|
When a service has multiple IP addresses, prefer IPv4:
|
|
|
|
```typescript
|
|
const preferredAddress = addresses.find((addr) => isIPv4(addr)) || addresses[0]
|
|
```
|
|
|
|
---
|
|
|
|
## 3. TCP Connection and Handshake
|
|
|
|
### 3.1 Connection Establishment
|
|
|
|
1. Client establishes TCP connection using the discovered `host:port`
|
|
2. Immediately sends a handshake message upon connection
|
|
3. Waits for server handshake acknowledgment
|
|
|
|
### 3.2 Handshake Messages (Protocol Version v1)
|
|
|
|
#### Client → Server: `handshake`
|
|
|
|
```typescript
|
|
type LanTransferHandshakeMessage = {
|
|
type: 'handshake';
|
|
deviceName: string;
|
|
version: string; // Protocol version, currently "1"
|
|
platform?: string; // 'darwin' | 'win32' | 'linux'
|
|
appVersion?: string;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Message Format Specification (Mixed Protocol)
|
|
|
|
v1 uses a "control JSON + binary data frame" mixed protocol (streaming mode, no per-chunk ACK):
|
|
|
|
- **Control messages** (handshake, heartbeat, file_start/ack, file_end, file_complete): UTF-8 JSON, `\n` delimited
|
|
- **Data messages** (`file_chunk`): Binary frames using Magic + total length for framing, no Base64
|
|
|
|
### 4.1 Control Message Encoding (JSON + `\n`)
|
|
|
|
| Property | Specification |
|
|
|----------|--------------|
|
|
| Encoding | UTF-8 |
|
|
| Serialization | JSON |
|
|
| Message Delimiter | `\n` (0x0A) |
|
|
|
|
### 4.2 `file_chunk` Binary Frame Format
|
|
|
|
To solve TCP packet splitting/merging and eliminate Base64 overhead, `file_chunk` uses binary frames with total length:
|
|
|
|
```
|
|
┌──────────┬──────────┬────────┬───────────────┬──────────────┬────────────┬───────────┐
|
|
│ Magic │ TotalLen │ Type │ TransferId Len│ TransferId │ ChunkIdx │ Data │
|
|
│ 0x43 0x53│ (4B BE) │ 0x01 │ (2B BE) │ (UTF-8) │ (4B BE) │ (raw) │
|
|
└──────────┴──────────┴────────┴───────────────┴──────────────┴────────────┴───────────┘
|
|
```
|
|
|
|
| Field | Size | Description |
|
|
|-------|------|-------------|
|
|
| Magic | 2B | Constant `0x43 0x53` ("CS"), distinguishes from JSON messages |
|
|
| TotalLen | 4B | Big-endian, total frame length (excluding Magic/TotalLen) |
|
|
| Type | 1B | `0x01` for `file_chunk` |
|
|
| TransferId Len | 2B | Big-endian, transferId string length |
|
|
| TransferId | nB | UTF-8 transferId (length from previous field) |
|
|
| ChunkIdx | 4B | Big-endian, chunk index starting from 0 |
|
|
| Data | mB | Raw file binary data (unencoded) |
|
|
|
|
> Total frame length calculation: `TotalLen = 1 + 2 + transferIdLen + 4 + dataLen`
|
|
|
|
### 4.3 Message Parsing Strategy
|
|
|
|
1. Read socket data into buffer
|
|
2. If first two bytes are `0x43 0x53` → parse as binary frame
|
|
3. Else if first byte is `{` → parse as JSON + `\n` control message
|
|
4. Otherwise discard 1 byte and continue loop
|
|
|
|
### 4.4 Message Type Summary (v1)
|
|
|
|
| Type | Direction | Encoding | Purpose |
|
|
|------|-----------|----------|---------|
|
|
| `handshake` | Client → Server | JSON+\n | Handshake request (version=1) |
|
|
| `handshake_ack` | Server → Client | JSON+\n | Handshake response |
|
|
| `ping` | Client → Server | JSON+\n | Heartbeat request |
|
|
| `pong` | Server → Client | JSON+\n | Heartbeat response |
|
|
| `file_start` | Client → Server | JSON+\n | Start file transfer |
|
|
| `file_start_ack` | Server → Client | JSON+\n | File transfer acknowledgment |
|
|
| `file_chunk` | Client → Server | Binary | File data chunk (no Base64, streaming, no per-chunk ACK) |
|
|
| `file_end` | Client → Server | JSON+\n | File transfer end |
|
|
| `file_complete` | Server → Client | JSON+\n | Transfer completion result |
|
|
|
|
---
|
|
|
|
## 5. File Transfer Protocol
|
|
|
|
### 5.1 Transfer Flow
|
|
|
|
```
|
|
Client (Sender) Server (Receiver)
|
|
| |
|
|
|──── 1. file_start ────────────────>|
|
|
| |
|
|
|<─── 2. file_start_ack ─────────────|
|
|
| |
|
|
|══════ Loop: send data chunks ══════|
|
|
| |
|
|
|──── 3. file_chunk [0] ────────────>|
|
|
|──── 3. file_chunk [1] ────────────>|
|
|
| ... repeat until all sent ... |
|
|
| |
|
|
|──── 5. file_end ──────────────────>|
|
|
| |
|
|
|<─── 6. file_complete ──────────────|
|
|
```
|
|
|
|
### 5.2 Message Definitions
|
|
|
|
#### 5.2.1 `file_start`
|
|
|
|
```typescript
|
|
type LanTransferFileStartMessage = {
|
|
type: 'file_start';
|
|
transferId: string; // UUID, unique transfer identifier
|
|
fileName: string;
|
|
fileSize: number;
|
|
mimeType: string;
|
|
checksum: string; // SHA-256 hash of entire file (hex)
|
|
totalChunks: number;
|
|
chunkSize: number;
|
|
}
|
|
```
|
|
|
|
#### 5.2.2 `file_start_ack`
|
|
|
|
```typescript
|
|
type LanTransferFileStartAckMessage = {
|
|
type: 'file_start_ack';
|
|
transferId: string;
|
|
accepted: boolean;
|
|
message?: string; // Rejection reason
|
|
}
|
|
```
|
|
|
|
#### 5.2.3 `file_chunk` — Binary Frame
|
|
|
|
See section 4.2 for frame format. `Data` is raw file binary data. Integrity relies on `file_start.checksum` (full file SHA-256).
|
|
|
|
#### 5.2.4 `file_end`
|
|
|
|
```typescript
|
|
type LanTransferFileEndMessage = {
|
|
type: 'file_end';
|
|
transferId: string;
|
|
}
|
|
```
|
|
|
|
#### 5.2.5 `file_complete`
|
|
|
|
```typescript
|
|
type LanTransferFileCompleteMessage = {
|
|
type: 'file_complete';
|
|
transferId: string;
|
|
success: boolean;
|
|
filePath?: string; // Save path (on success)
|
|
error?: string; // Error message (on failure)
|
|
}
|
|
```
|
|
|
|
### 5.3 Checksum
|
|
|
|
```typescript
|
|
async function calculateFileChecksum(filePath: string): Promise<string> {
|
|
const hash = crypto.createHash('sha256')
|
|
const stream = fs.createReadStream(filePath)
|
|
for await (const chunk of stream) {
|
|
hash.update(chunk)
|
|
}
|
|
return hash.digest('hex')
|
|
}
|
|
```
|
|
|
|
### 5.4 Chunk Size
|
|
|
|
```typescript
|
|
const CHUNK_SIZE = 512 * 1024 // 512KB
|
|
const totalChunks = Math.ceil(fileSize / CHUNK_SIZE)
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Heartbeat and Keep-alive
|
|
|
|
### 6.1 Messages
|
|
|
|
- **`ping`** (Client → Server): `{ type: 'ping', payload?: string }`
|
|
- **`pong`** (Server → Client): `{ type: 'pong', received: boolean, payload?: string }`
|
|
|
|
### 6.2 Strategy
|
|
|
|
- Send `ping` immediately after successful handshake to verify connection
|
|
- Optional: periodically send heartbeats to keep the connection alive
|
|
|
|
---
|
|
|
|
## 7. Error Handling
|
|
|
|
### 7.1 Timeout Configuration
|
|
|
|
| Operation | Timeout | Description |
|
|
|-----------|---------|-------------|
|
|
| TCP Connection | 10s | Connection establishment timeout |
|
|
| Handshake | 10s | Waiting for `handshake_ack` |
|
|
| Transfer Complete | 60s | Waiting for `file_complete` |
|
|
|
|
### 7.2 Error Scenarios
|
|
|
|
| Scenario | Client Handling | Server Handling |
|
|
|----------|----------------|-----------------|
|
|
| TCP connection failure | Notify UI, allow retry | - |
|
|
| Handshake timeout | Disconnect, notify UI | Close socket |
|
|
| Handshake rejected | Show rejection reason | - |
|
|
| Chunk processing failure | Abort transfer, cleanup | Clean up temp files |
|
|
| Unexpected disconnect | Cleanup state, notify UI | Clean up temp files |
|
|
| Insufficient storage | - | Send `accepted: false` |
|
|
|
|
---
|
|
|
|
## 8. Constants and Configuration
|
|
|
|
```typescript
|
|
export const LAN_TRANSFER_PROTOCOL_VERSION = '1'
|
|
export const LAN_TRANSFER_SERVICE_TYPE = 'cherrystudio'
|
|
export const LAN_TRANSFER_SERVICE_FULL_NAME = '_cherrystudio._tcp'
|
|
export const LAN_TRANSFER_TCP_PORT = 53317
|
|
export const LAN_TRANSFER_CHUNK_SIZE = 512 * 1024 // 512KB
|
|
export const LAN_TRANSFER_GLOBAL_TIMEOUT_MS = 10 * 60 * 1000 // 10 minutes
|
|
export const LAN_TRANSFER_HANDSHAKE_TIMEOUT_MS = 10_000
|
|
export const LAN_TRANSFER_CHUNK_TIMEOUT_MS = 30_000
|
|
export const LAN_TRANSFER_COMPLETE_TIMEOUT_MS = 60_000
|
|
|
|
export const LAN_TRANSFER_ALLOWED_EXTENSIONS = ['.zip']
|
|
export const LAN_TRANSFER_ALLOWED_MIME_TYPES = ['application/zip', 'application/x-zip-compressed']
|
|
```
|
|
|
|
---
|
|
|
|
## 9. Complete Sequence Diagram
|
|
|
|
```
|
|
┌─────────┐ ┌─────────┐ ┌─────────┐
|
|
│ Renderer│ │ Main │ │ Mobile │
|
|
│ (UI) │ │ Process │ │ Server │
|
|
└────┬────┘ └────┬────┘ └────┬────┘
|
|
│ │ │
|
|
│ ═══════ Service Discovery ═════════ │
|
|
│ startScan() │ │
|
|
│────────────────────────────────────>│ mDNS browse │
|
|
│ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─>│
|
|
│ │<─ ─ ─ service discovered ─ ─ ─ ─ ─ ─│
|
|
│<────── onServicesUpdated ───────────│ │
|
|
│ │ │
|
|
│ ═══════ Handshake ════════════════ │
|
|
│ connect(peer) │ │
|
|
│────────────────────────────────────>│──────── TCP Connect ───────────────>│
|
|
│ │──────── handshake ─────────────────>│
|
|
│ │<─────── handshake_ack ──────────────│
|
|
│ │──────── ping ──────────────────────>│
|
|
│ │<─────── pong ───────────────────────│
|
|
│<────── connect result ──────────────│ │
|
|
│ │ │
|
|
│ ═══════ File Transfer ════════════ │
|
|
│ sendFile(path) │ │
|
|
│────────────────────────────────────>│──────── file_start ────────────────>│
|
|
│ │<─────── file_start_ack ─────────────│
|
|
│ │──────── file_chunk[0] (binary) ────>│
|
|
│<────── progress event ──────────────│ │
|
|
│ │──────── file_chunk[1] (binary) ────>│
|
|
│<────── progress event ──────────────│ ... repeat ... │
|
|
│ │──────── file_end ──────────────────>│
|
|
│ │<─────── file_complete ──────────────│
|
|
│<────── complete event ──────────────│ │
|
|
```
|
|
|
|
---
|
|
|
|
## 10. Mobile Implementation Guide (v1)
|
|
|
|
### 10.1 Required Features
|
|
|
|
1. **mDNS Service Publishing**: Publish `_cherrystudio._tcp` service on TCP port `53317`
|
|
2. **TCP Server**: Listen on the specified port
|
|
3. **Message Parsing**: Control messages via UTF-8 + `\n` JSON; data messages via binary frames (Magic+TotalLen framing)
|
|
4. **Handshake Handling**: Validate `handshake`, send `handshake_ack`, respond to `ping`
|
|
5. **File Receiving (Streaming)**: Parse `file_start`, receive `file_chunk` binary frames (write to file + incremental hash), process `file_end`, send `file_complete`
|
|
|
|
### 10.2 Recommended Libraries
|
|
|
|
**React Native / Expo:**
|
|
|
|
- mDNS: `react-native-zeroconf` or `@homielab/react-native-bonjour`
|
|
- TCP: `react-native-tcp-socket`
|
|
- Crypto: `expo-crypto` or `react-native-quick-crypto`
|
|
|
|
---
|
|
|
|
## Appendix A: TypeScript Type Definitions
|
|
|
|
Complete type definitions are located in `src/shared/types/lanTransfer.ts`. See the source code for the full interface definitions.
|
|
|
|
## Appendix B: Version History
|
|
|
|
| Version | Date | Changes |
|
|
|---------|------|---------|
|
|
| 1.0 | 2025-12 | Initial release with binary frame format and streaming transfer |
|