Files
CherryHQ-cherry-studio/docs/references/lan-transfer-protocol.md
fullex 1c6ff30a18 refactor(shared): dissolve config/ and move logic out of types/ into utils/
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.
2026-06-19 20:41:18 -07:00

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 |