Files
admin 19f8f7588e docs: 架构文档与变更记录
cdrop 的架构 / 协议 / 鉴权设计(PROJECT_BRIEF)、前端设计(FRONTEND_DESIGN)、变更记录(CHANGELOG)。

公开发布初始提交:完整文档历史留存于私有归档,公开分支自此干净起步。
2026-06-15 21:52:15 +08:00

245 lines
8.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Commilitia Drop — 跨 OS 剪贴板与文件传输服务
> 本文是项目最初的 MVP 规格(设计基线),保留作架构参考;**当前实现状态以 [`CHANGELOG`](CHANGELOG.html) 为准**——其后已落地剪贴板/消息通道、Wails 桌面端、安全加固等,部分「远期」项已实现。
项目代号 / 仓库名 / 二进制名:`cdrop`
部署域名:`drop.commilitia.net`
节点:自托管(单机 Docker + 反代)
---
## 1. 目标
跨 OSWin / 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 存内存或 sessionStoragerefresh_token 存内存(不写 localStorage 防 XSS)。
- **Native client**desktop / mobile,远期):标准 Casdoor OAuth + PKCE + refresh_token rotation。系统浏览器或 loopback redirect 完成首次登录;token 存 OS keychainmacOS 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 自签 JWTHS256),长 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、信令、传输状态):SSEserver→client+ POSTclient→server
- 不用浏览器原生 EventSource(不支持自定义 header),用 fetch + ReadableStream 自实现 SSE 解析
- **Relay 通道**P2P 失败时):
- Receiver:流式 GETchunked transfer-encoding
- Sender:多次分块 POSTSafari / Firefox 不支持 streaming POST
- Server 端 in-memory ring buffer 桥接
### 文件传输
- **优先 P2P**WebRTC DataChannelDTLS 即 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 二次确认;不限文件大小,但中继有内存与并发上限
### 存储
- SQLitemodernc.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 用 gofmtTS / JSX / JSON 按 Allman、数组内空格、运算符前置。
---
## 4. SQLite SchemaMVP
`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 扩大到剪贴板或客户端