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

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

8.2 KiB
Raw Permalink Blame History

Commilitia Drop — 跨 OS 剪贴板与文件传输服务

本文是项目最初的 MVP 规格(设计基线),保留作架构参考;当前实现状态以 CHANGELOG 为准——其后已落地剪贴板/消息通道、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 = 1access_token 1 小时)
  • RefreshExpireInHours = 196refresh_token 约 8.16 天,sliding 行为已基于源码分析确认)
  • 启用 grant typesauthorization_coderefresh_token
  • 启用 PKCE

三条客户端路径:

  • 浏览器:标准 Casdoor OAuth + PKCE。access_token 存内存或 sessionStoragerefresh_token 存内存(不写 localStorage 防 XSS)。
  • Native clientdesktop / 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 桥接

文件传输

  • 优先 P2PWebRTC DataChannelDTLS 即 E2E
    • 不用 simple-peer,自己包 RTCPeerConnection
    • ICE 仅自建 STUNstun:your-stun.example:3478),不配 TURN(自建 coturn 跑 STUN-only;现已改用 Cloudflare Realtime TURN
  • P2P 失败 → Relay做 Pipe 持久化邮箱)
  • 双方均在线;传输中掉线重连最多 3 次
  • 同 user 自动接受;> 100 MB 二次确认;不限文件大小,但中继有内存与并发上限

存储

  • SQLitemodernc.org/sqlite,纯 Go 无 cgo
  • 启用 WALPRAGMA 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

devicesshortcut_tokenstransfer_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. 前置条件(缺失就问我)

  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 扩大到剪贴板或客户端