9.9 KiB
Commilitia Drop — 跨 OS 剪贴板与文件传输服务
本文是项目最初的 MVP 规格(设计基线),保留作架构参考;当前实现状态以
CHANGELOG为准。全客户端正式构建、签名、安装和分发流程见CLIENT_BUILD_INSTALL.md。
产品名:Commilitia Drop
内部项目代号 / 仓库名:cdrop
部署域名:drop.commilitia.net
节点:自托管(单机 Docker + 反代)
当前实现叠加层(2026-07-31)
以下状态覆盖本文后续仍保留的历史 MVP 假设:
- 正式客户端为 Web、Windows Wails x86-64、macOS Wails ARM64,以及共用 SwiftUI target 的 iPhone / iPad。macOS 原生 target 尚未完成迁移验收,不得替代 Wails,也不得并行发布。
- 用户可见应用名统一为“Commilitia Drop”,桌面端不再带 “Desktop” 后缀;正式桌面产物为
Commilitia Drop.exe、Commilitia Drop.app和Commilitia Drop.dmg。 - 用户可见或可诊断的协议命名统一为
commilitia-drop://auth-callback、commilitia-drop-file、_commilitia-drop._tcp和 Broker application keycommilitia-drop。 - Bundle ID、App Group、Keychain service、LaunchAgent label、环境变量和数据目录等内部身份, 只要不直接、不可更改地决定系统显示名,就保持稳定以保护既有权限、钥匙串和配置。
- 鉴权已委托 Auth Broker,Broker、Caddy 与服务端使用同一个 application key;历史 Casdoor 直连、自签会话和 shortcut token 描述仅用于理解演进,不代表当前部署。
- P2P 数据面已从 WebView JavaScript 下沉:桌面使用 Go / pion,iOS / iPadOS 使用 libwebrtc;
Web 仍使用浏览器
RTCPeerConnection。三端共享信令、relay 编排与线协议。 - macOS 正式分发复用既有 Developer ID Application 证书并经 Apple 公证;iPhone / iPad 复用既有 Apple Development 证书,使用 CLI 构建安装,不使用 Xcode GUI。
1. 目标
跨 OS(Win / macOS / Linux / iOS / Android)服务,提供:
- 剪贴板同步(双向)
- ad-hoc 文件传输(双设备均在线)
复用现有 Casdoor SSO,前置 Caddy。
2. 锁定的架构决策(不要重新讨论)
身份与鉴权
Casdoor 已为 cdrop 创建独立 application。Token 配置:
ExpireInHours = 1(access_token 1 小时)RefreshExpireInHours = 196(refresh_token 约 8.16 天,sliding 行为已基于源码分析确认)- 启用 grant types:
authorization_code、refresh_token - 启用 PKCE
三条客户端路径:
- 浏览器:标准 Casdoor OAuth + PKCE。access_token 存内存或 sessionStorage;refresh_token 存内存(不写 localStorage 防 XSS)。
- Native client(desktop / mobile,远期):标准 Casdoor OAuth + PKCE + refresh_token rotation。系统浏览器或 loopback redirect 完成首次登录;token 存 OS keychain(macOS Keychain / Windows DPAPI / Linux Secret Service)。access_token 即将过期时(< 5 min)调
/api/login/oauth/refresh_token换新 pair。Casdoor 每次 refresh 返回新 refresh_token,新 token 的exp重置(sliding 7-8 天窗口)。 - iOS Shortcut(远期):cdrop 自签 JWT(HS256),长 TTL(1 年),不刷新。
shortcut_tokens表只存元数据(jti / scope / expires_at / revoked),不存 token 本身。
后端鉴权中间件统一处理:
- 优先尝试 cdrop 自签 HS256 验签(命中 → Shortcut 路径)
- 失败回退 Casdoor JWKS RS256 验签(命中 → 浏览器或 native client 路径)
不引入 caddy-security。
设备身份
X-Device-Name header 自报,UPSERT 到 devices 表。不做注册分配 UUID。
设备类型
| type | clipboard | file send | file recv |
|---|---|---|---|
| desktop | yes | yes | yes |
| mobile | yes | yes | yes |
| browser | no | yes | yes |
| ios_shortcut | yes | no | no |
同一物理设备的浏览器和 Shortcut 视作两个独立 device 记录。
传输层(h2/h3 偏好,禁用 WebSocket)
WebSocket 在 Caddy + Go 当前栈下会被强制降级到 h1.1(RFC 8441 未实现),违背 h2/h3 偏好。改用:
- Hub 通道(presence、信令、传输状态):SSE(server→client)+ POST(client→server)
- 不用浏览器原生 EventSource(不支持自定义 header),用 fetch + ReadableStream 自实现 SSE 解析
- Relay 通道(P2P 失败时):
- Receiver:流式 GET(chunked transfer-encoding)
- Sender:多次分块 POST(Safari / Firefox 不支持 streaming POST)
- Server 端 in-memory ring buffer 桥接
文件传输
- 优先 P2P:WebRTC DataChannel,DTLS 即 E2E
- 不用 simple-peer,自己包 RTCPeerConnection
- (协议不变,数据面后续从浏览器 JS 下沉原生:桌面 Go/pion、iOS libwebrtc,逃 WebKit 256KB rwnd 与渲染器节流;见
CHANGELOG2026-06-28) - ICE 仅自建 STUN(
stun:your-stun.example:3478),不配 TURN(自建 coturn 跑 STUN-only;现已改用 Cloudflare Realtime TURN)
- P2P 失败 → Relay(不做 Pipe 持久化邮箱)
- 双方均在线;传输中掉线重连最多 3 次
- 同 user 自动接受;> 100 MB 二次确认;不限文件大小,但中继有内存与并发上限
存储
- SQLite(modernc.org/sqlite,纯 Go 无 cgo)
- 启用 WAL:
PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; - 中继不落盘,仅内存
权限
cdrop 服务侧不调 Casdoor enforce API。仅读 JWT 的 groups 数组自判。MVP 阶段所有合法登录用户视作普通用户,不分等级。
3. 技术栈
后端:Go 1.22+ / chi / go-jose v4 / modernc.org/sqlite / koanf / sqlc 前端:Vite + React + TypeScript / Tailwind / Zustand / 原生 RTCPeerConnection
代码风格:严格遵守 ~/.claude/rules/code-style.md。Go 用 gofmt;TS / JSX / JSON 按 Allman、数组内空格、运算符前置。
4. SQLite Schema(MVP)
devices、shortcut_tokens、transfer_sessions 三表写入;clipboard_state 建表留空壳。
native client 不需要 cdrop 自管 token 表(直接验 Casdoor JWKS)。
CREATE TABLE devices (
user_id TEXT NOT NULL,
name TEXT NOT NULL,
type TEXT NOT NULL,
last_seen INTEGER NOT NULL,
PRIMARY KEY (user_id, name)
);
CREATE TABLE shortcut_tokens (
jti TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
label TEXT NOT NULL,
scopes TEXT NOT NULL,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
last_used_at INTEGER,
revoked INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE transfer_sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
sender_name TEXT NOT NULL,
receiver_name TEXT NOT NULL,
state TEXT NOT NULL,
mode TEXT,
file_name TEXT NOT NULL,
file_size INTEGER NOT NULL,
file_sha256 TEXT,
bytes_transferred INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
finished_at INTEGER,
fail_reason TEXT
);
CREATE TABLE clipboard_state (
user_id TEXT PRIMARY KEY,
content_type TEXT NOT NULL,
content TEXT,
source_device TEXT,
updated_at INTEGER NOT NULL
);
State machine: PENDING → ACCEPTED → (P2P_ACTIVE | RELAY_ACTIVE) → DONE | FAILED | CANCELLED
5. SSE 消息协议
server → client (event types):
presence{ devices: [ { name, type, online } ] }signal{ from, payload }transfer:incoming{ session, auto_accept }transfer:state{ session_id, state }transfer:relay_ready{ session_id, sender_url, receiver_url }ping(keepalive)
client → server (POST body):
{ type: "signal", to, payload }{ type: "transfer:fallback", session_id }
6. 前置条件(缺失就问我)
- OIDC application 详细参数(client_id、JWKS endpoint、issuer、audience、token format)
- 反代(Caddy 等)现状(版本、h3 是否启用、站点 block 是否已配)
- STUN / TURN 是否已就绪
- DNS 是否已指向部署节点
7. MVP 范围
只做 Web 文件传输端到端。
不做:客户端(任何形态)、剪贴板逻辑、用户间共享、Pipe 模型、历史 UI、audit log 持久化、i18n。
验收标准:
- 两台桌面浏览器 Casdoor 登录
- Home 页看到对方在线
- 拖拽文件 → P2P 直传成功
- 模拟 NAT 阻塞 → 自动 fallback Relay 成功
- 传输中关闭一方 → 重连续传;3 次失败终止
MVP 阶段只用浏览器路径。Native client 和 Shortcut 路径在 §2 已锁定但属于后续阶段。
8. MVP 后开发计划
阶段二:剪贴板同步
clipboard_state CRUD + Hub 推送;桌面客户端(Tauri 候选,前端复用,Casdoor OAuth 走系统浏览器 + loopback redirect);iOS Shortcut 模板生成(启用 cdrop 自签 token 路径);Android 客户端原型。
阶段三:UX 完善
Inbox 历史、设备管理、多文件 / 文件夹、PWA 化、通知、i18n、暗色模式。
阶段四:生产化
audit log 持久化、限流、Prometheus 指标、健康检查、SQLite → PostgreSQL 迁移路径、配额、备份、安全审计。
阶段五:扩展能力(远期)
用户间共享、命名 pipe、原生客户端、WebTransport 切换、E2E Relay。
9. Plan 模式输出要求
至少包含:
- §6 前置条件的获取方案(哪些自己探测、哪些问我)
- MVP 实施计划(粒度自定)
- 最高风险点 + 缓解思路
- 本 brief 中你认为不清楚或需要决策的地方
10. 协作方式
你直接对我负责。可自然决策的部分不必复述。
允许向我提出任何合理建议——包括质疑本 brief 中的决策、提出更好的实现方式、要求补充信息、推迟某个里程碑等。我接受被反驳。
红线:
- 不跳过 Plan 直接编码
- 不重新讨论 §2 已锁定的架构(除非有强证据)
- 增加未列出依赖前先确认
- 不把 MVP 扩大到剪贴板或客户端