# Commilitia Drop — 跨 OS 剪贴板与文件传输服务 > 本文是项目最初的 MVP 规格(设计基线),保留作架构参考;**当前实现状态以 [`CHANGELOG`](CHANGELOG.html) 为准**——其后已落地剪贴板/消息通道、Wails 桌面端、安全加固等,部分「远期」项已实现。 项目代号 / 仓库名 / 二进制名:`cdrop` 部署域名:`drop.commilitia.net` 节点:自托管(单机 Docker + 反代) --- ## 1. 目标 跨 OS(Win / macOS / Linux / iOS / Android)服务,提供: 1. 剪贴板同步(双向) 2. 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 本身。 后端鉴权中间件统一处理: 1. 优先尝试 cdrop 自签 HS256 验签(命中 → Shortcut 路径) 2. 失败回退 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 - 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)。 ```sql 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. 前置条件(缺失就问我) 1. OIDC application 详细参数(client_id、JWKS endpoint、issuer、audience、token format) 2. 反代(Caddy 等)现状(版本、h3 是否启用、站点 block 是否已配) 3. STUN / TURN 是否已就绪 4. DNS 是否已指向部署节点 --- ## 7. MVP 范围 **只做 Web 文件传输端到端**。 不做:客户端(任何形态)、剪贴板逻辑、用户间共享、Pipe 模型、历史 UI、audit log 持久化、i18n。 验收标准: 1. 两台桌面浏览器 Casdoor 登录 2. Home 页看到对方在线 3. 拖拽文件 → P2P 直传成功 4. 模拟 NAT 阻塞 → 自动 fallback Relay 成功 5. 传输中关闭一方 → 重连续传;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 模式输出要求 至少包含: 1. §6 前置条件的获取方案(哪些自己探测、哪些问我) 2. MVP 实施计划(粒度自定) 3. 最高风险点 + 缓解思路 4. 本 brief 中你认为不清楚或需要决策的地方 --- ## 10. 协作方式 你直接对我负责。可自然决策的部分不必复述。 允许向我提出**任何**合理建议——包括质疑本 brief 中的决策、提出更好的实现方式、要求补充信息、推迟某个里程碑等。我接受被反驳。 红线: - 不跳过 Plan 直接编码 - 不重新讨论 §2 已锁定的架构(除非有强证据) - 增加未列出依赖前先确认 - 不把 MVP 扩大到剪贴板或客户端