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.
16 KiB
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
- Protocol Overview
- Service Discovery (Bonjour/mDNS)
- TCP Connection and Handshake
- Message Format Specification
- File Transfer Protocol
- Heartbeat and Keep-alive
- Error Handling
- Constants and Configuration
- Complete Sequence Diagram
- 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:
{
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:
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:
const preferredAddress = addresses.find((addr) => isIPv4(addr)) || addresses[0]
3. TCP Connection and Handshake
3.1 Connection Establishment
- Client establishes TCP connection using the discovered
host:port - Immediately sends a handshake message upon connection
- Waits for server handshake acknowledgment
3.2 Handshake Messages (Protocol Version v1)
Client → Server: handshake
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,
\ndelimited - 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
- Read socket data into buffer
- If first two bytes are
0x43 0x53→ parse as binary frame - Else if first byte is
{→ parse as JSON +\ncontrol message - 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
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
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
type LanTransferFileEndMessage = {
type: 'file_end';
transferId: string;
}
5.2.5 file_complete
type LanTransferFileCompleteMessage = {
type: 'file_complete';
transferId: string;
success: boolean;
filePath?: string; // Save path (on success)
error?: string; // Error message (on failure)
}
5.3 Checksum
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
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
pingimmediately 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
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
- mDNS Service Publishing: Publish
_cherrystudio._tcpservice on TCP port53317 - TCP Server: Listen on the specified port
- Message Parsing: Control messages via UTF-8 +
\nJSON; data messages via binary frames (Magic+TotalLen framing) - Handshake Handling: Validate
handshake, sendhandshake_ack, respond toping - File Receiving (Streaming): Parse
file_start, receivefile_chunkbinary frames (write to file + incremental hash), processfile_end, sendfile_complete
10.2 Recommended Libraries
React Native / Expo:
- mDNS:
react-native-zeroconfor@homielab/react-native-bonjour - TCP:
react-native-tcp-socket - Crypto:
expo-cryptoorreact-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 |