Files
Commilitia-Drop/README.md
T
admin f21fa5b5e8 cdrop — 跨 OS 剪贴板与文件传输服务
Commilitia Drop:自托管的跨设备剪贴板同步与点对点文件传输。

- 后端 Go(chi / SQLite WAL / SSE Hub / WebRTC signaling + 状态机 / Relay ring buffer),编译进单个 distroless 镜像(前端 go:embed)。
- 前端 React + TanStack Router + Zustand,自实现 SSE + WebRTC P2P,NAT 受阻时回退服务端中继;聚珍(Juzhen)CJK 综合排版。
- 桌面端 Wails v2(macOS / Windows),瘦客户端复用 web。
- 鉴权 OIDC PKCE(自建 Casdoor 等),refresh_token 信封加密存系统密钥库;iOS Shortcut 用 HS256 scoped token。

架构文档与变更记录见 docs 分支(PROJECT_BRIEF / FRONTEND_DESIGN / CHANGELOG)。

本次为公开发布初始提交:完整开发历史(含部署细节)留存于私有归档,公开仓库自此干净起步。
2026-06-15 21:38:28 +08:00

162 lines
6.9 KiB
Markdown
Raw 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.
# cdrop — Commilitia Drop
跨 OS 剪贴板与文件传输服务。架构文档(`PROJECT_BRIEF.md` / `FRONTEND_DESIGN.md`)与 Changelog 位于 **`docs` 分支**
```sh
git worktree add ../cdrop-docs docs # 并列检出,main / docs 互不扰动
open ../cdrop-docs/PROJECT_BRIEF.md
open ../cdrop-docs/CHANGELOG.html
```
## 克隆
前端 `web/src/lib/cjk-autospace/` 为 submodule(聚珍 / Juzhen 2.0CJK 综合排版——中西间隙 / 长英文断词 / 标点挤压 / 禁则 / justify,跨 Claude / Nvim / cdrop 共享)。首次 clone 须带 `--recurse-submodules`,否则 `vite build` 会因找不到 import 而失败:
```sh
git clone --recurse-submodules <URL> cdrop
# 已 clone 但未拉 submodule
git submodule update --init --recursive
```
## 当前阶段
MVPM0M13)已上线 prodOIDC PKCE 接入自建 CasdoorCloudflare Realtime TURN over TLS)。其后进入阶段二,已落地:
- **剪贴板同步**:单条覆写式云剪贴板(短 TTL 限暴露面)+ 文本消息一次性通道
- **桌面客户端**Wails v2macOS universal `.app` + Windows `.exe`)—— 瘦客户端复用 web,业务全走后端 APIrefresh_token 存系统密钥库(信封加密)
- **iOS Shortcut**HS256 scoped token 手动签发路径(网页签发 UI 暂停)
- **安全加固**:中继会话防耗尽、HS256 强制 jti、prod 强制 audience、TURN 短 TTL、OAuth 端点限流(详见 CHANGELOG
MVP 里程碑(历史记录):
| 里程碑 | 内容 | 状态 |
|---|---|---|
| M0M5 | 后端:chi / sqlite WAL / dev+prod auth 中间件 / SSE Hub / signaling + 状态机 / Relay ring buffer | 完成 |
| M6M10 | 前端:Mantine + TanStack Router + Zustand(其后叠加自定义设计 token 与 Theme B 重塑)/ 自实现 SSE / WebRTC P2P / Relay 回退 / SSE 重连 + chunk 重试 | 完成 |
| M11 | 本机双 tab 端到端验收(dev mock auth+ 多轮 bug 修复(race / 状态机 / DOM nesting / 状态着色) | 完成 |
| M12 | OIDC PKCE:后端 /api/auth/{config,exchange,refresh} 代理 token endpoint,前端 PKCE redirect + callbackCasdoor confidential client + client_secret | 完成 |
| M13 | 部署:cdrop-base 缓存层 + 应用镜像(BUILDPLATFORM 跨编译,6 MB distroless),Cloudflare Realtime TURN App,反代 reverse_proxy + flush_interval -1SSE 友好),h3 已启 | 完成 |
## 本机开发:两 tab 验收流程(M11)
### 1. 准备 dev token
把同一个随机 base64 串同时写到两份 env 文件:
```bash
TOKEN=$(openssl rand -hex 32)
cp .env.example .env
sed -i '' "s|replace-with-32-byte-random-base64|$TOKEN|" .env
cp web/.env.example web/.env.local
sed -i '' "s|replace-with-32-byte-random-base64|$TOKEN|" web/.env.local
```
`.env` 会被根目录的 Justfile 自动加载(`set dotenv-load := true`)。
### 2. 起后端(dev 模式)
```bash
just dev-back
# 监听 :8080json 日志到 stdout
```
后端要求 `CDROP_AUTH_MODE=dev``CDROP_DEV_TOKEN` 同时设置;缺一会 panic 拒启动(防止误带到生产)。`.env` 文件由后端自动加载(koanf)。
### 3. 起前端(vite dev
另开一个终端:
```bash
just dev-front
# 默认 http://localhost:5173/api 自动代理到 :8080
```
### 4. 打开两个浏览器 tab
| tab | URL | 期望 |
|---|---|---|
| tab-1 | `http://localhost:5173/login?dev_user=alice` | 输入 `Continue` → Setup 输入设备名 `tab-1` → Home |
| tab-2 | `http://localhost:5173/login?dev_user=alice` | 同上但设备名填 `tab-2` |
两 tab 的 Home 页应在 1–2 秒内互相看到对方在线,顶部 `● online` 亮绿。
### 5. 验收 brief §7 五条
| # | 步骤 | 通过标准 |
|---|---|---|
| 1 | 双 tab Casdoor 登录 | M11 阶段使用 dev mock 等价:两 tab 都进 Home |
| 2 | Home 页看到对方在线 | DeviceStrip 互相显示对方卡片,绿点 |
| 3 | 拖拽文件 → P2P 直传 | tab-1 选 tab-2 + 拖文件 → Sendtab-2 浏览器自动下载,sha256 与原文件一致 |
| 4 | 模拟 NAT 阻塞 → fallback Relay | DevTools console 注入 `RTCPeerConnection.prototype._origCreateOffer = RTCPeerConnection.prototype.createOffer; RTCPeerConnection.prototype.createOffer = function() { return new Promise(()=>{}) }`(永远 pending),30s 后看 transfer 状态切到 `RELAY_ACTIVE`,依然下载成功 |
| 5 | 传输中关闭一方 → 重连续传 | M11 范围内可手动 kill backend 几秒再起;或断网卡 30s |
`> 100 MB` 文件不会自动接受(brief §2 阈值);当前 MVP 还没接 UI 二次确认,可暂停在大文件场景验证。
### 6. 单元测试
```bash
just test # go 测试
just typecheck # 后端 go vet 由 just vet 触发
cd web && npm run typecheck && npm run build
```
## 部署
后端编译进单个 distroless 镜像(前端经 Dockerfile 构建后 `go:embed` 进二进制)。镜像约 6 MB。
### 构建镜像
```bash
# 依赖预热(仅 go.mod/go.sum 或 web/package*.json 变动时需重跑):把 Go modules
# 与 node_modules 烤进 cdrop-base:latest,加速后续源码层构建。
just docker-base
# 应用镜像:从 cdrop-base 起步编前端 + 跨编译 Go,落 linux/amd64 distroless。
just docker-image # → cdrop:latest
```
宿主原生 arch 即可(`BUILDPLATFORM` 让 Go 跨编译走原生,无需 qemu 执行 amd64)。
### 上线
镜像送到目标主机(二选一):
```bash
# A. 本地构建 → 传镜像(无需在服务器装 Node/Go)
docker save cdrop:latest | gzip | ssh <user>@<host> 'docker load'
# B. 或把源码同步到服务器,在服务器上 docker build
```
服务条目模板见 [`docker/compose.snippet.yaml`](docker/compose.snippet.yaml):填进你的 `docker-compose.yaml`,挂一个目录到 `/data`(存 SQLite 库),不开放端口、由反代访问 `<service>:8080`
```bash
docker compose up -d <service>
curl -s https://<your-domain>/healthz # {"status":"ok","auth_mode":"prod","db_ok":true}
```
### 反代(SSE 要点)
cdrop 用 SSE 推事件,反代**必须关闭响应缓冲**否则事件不实时。Caddy 示例:
```caddyfile
your-domain.example {
reverse_proxy <service>:8080 {
flush_interval -1 # 关闭缓冲,SSE 即时下发
}
}
```
### prod 配置
env 见 [`.env.example`](.env.example) / `compose.snippet.yaml`。要点:
1. `CDROP_AUTH_MODE=prod`,删掉 `CDROP_DEV_TOKEN`
2.`CDROP_OIDC_*`(你的 OIDC provider,如自建 Casdoor / Keycloak);在 IdP 建 confidential + PKCE client,回调白名单加 `https://<your-domain>/oauth/callback`
3. **`CDROP_OIDC_AUDIENCE` 必须非空**(你的 client_id,多值逗号分隔)—— 留空会跳过 audience 校验,同一 JWKS 下其他应用的 token 也能验过;后端 prod 启动期强制此项,缺则拒启动
4.`CDROP_HS256_SECRET`iOS Shortcut scoped token 用,不用可留空)
5. 可选 `CDROP_CF_TURN_KEY_ID` + `CDROP_CF_TURN_API_TOKEN` 启用 Cloudflare Realtime TURN over TLS(不配则前端 STUN-only 兜底)