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

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

  1. Protocol Overview
  2. Service Discovery (Bonjour/mDNS)
  3. TCP Connection and Handshake
  4. Message Format Specification
  5. File Transfer Protocol
  6. Heartbeat and Keep-alive
  7. Error Handling
  8. Constants and Configuration
  9. Complete Sequence Diagram
  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:

{
  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

  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

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

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 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

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

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