194 lines
10 KiB
Markdown
194 lines
10 KiB
Markdown
# 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.0:CJK 综合排版——中西间隙 / 长英文断词 / 标点挤压 / 禁则 / justify,跨 Claude / Nvim / cdrop 共享)。首次 clone 须带 `--recurse-submodules`,否则 `vite build` 会因找不到 import 而失败:
|
||
|
||
```sh
|
||
git clone --recurse-submodules <URL> cdrop
|
||
# 已 clone 但未拉 submodule:
|
||
git submodule update --init --recursive
|
||
```
|
||
|
||
## 当前阶段
|
||
|
||
MVP(M0–M13)已上线 prod(OIDC PKCE 接入自建 Casdoor,Cloudflare Realtime TURN over TLS)。其后进入阶段二,已落地:
|
||
|
||
- **剪贴板同步**:单条覆写式云剪贴板(短 TTL 限暴露面)+ 文本消息一次性通道
|
||
- **桌面客户端**:Wails v2(macOS Apple Silicon `.app` + Windows x64 `.exe`)——
|
||
瘦客户端复用 web,业务全走后端 API;refresh_token 存系统密钥库(信封加密)
|
||
- **原生数据面**:P2P 数据通道从系统 WebView 的 JS 下沉到原生——桌面走 Go/pion、iOS(原生 SwiftUI 客户端)走 libwebrtc——逃出 WebKit / WebView2 写死的 256KB SCTP 接收窗与渲染器节流;信令 / 中继 / 编排仍由共享 JS 引擎统一,三端共享同一线协议(详见 docs 分支 `CHANGELOG`)
|
||
- **浏览器免重登 + PWA**:可安装为独立 PWA;浏览器侧 refresh_token 不再进 JS,改由服务端 HttpOnly cookie 会话持有(AES-256-GCM 落盘),关闭重开自动免重登(7 天滑动失活),设备名随会话持久化(抗 PWA 存储清除)
|
||
- **推送通知**:页面 / PWA 关闭时,收到文件、消息、传输完成或失败以系统通知提醒;页面打开时仍走应用内提示。网页走 Web Push(VAPID,覆盖浏览器 / Android / iOS 16.4+ 已安装 PWA),桌面客户端走原生系统通知(仅窗口非前台才弹)。文案按设备语言在服务端渲染(中简 / 中繁 / 英)
|
||
- **扫码登录**:临时借用陌生设备时,陌生设备显二维码、已登录手机扫码批准即可登入(微信 / WhatsApp 网页版模型)。批准方选“信任此设备(7 天)”或“仅此次(1 小时)”,授予的是受限访客会话(能收发文件,不能改账号 / 不能再批准别的设备 / 不能签发长效 token)。三密钥分离:二维码只含批准信息,会话只投递给持私有轮询密钥的原设备,偷拍二维码者拿不到会话。身份仍源自 OAuth provider,cdrop 自维护薄账户层 + 自签会话令牌(详见 `AUTH.md`)
|
||
- **iOS Shortcut**:HS256 scoped token 手动签发路径(网页签发 UI 暂停)
|
||
- **安全加固**:中继会话防耗尽、HS256 强制 jti、prod 强制 audience、TURN 短 TTL、OAuth 端点限流(详见 CHANGELOG)
|
||
|
||
### 正式客户端入口
|
||
|
||
同一平台只保留一份正式客户端;开发中的迁移 target 不进入下载或分发入口。
|
||
|
||
| 平台 | 当前正式客户端 | 说明 |
|
||
|---|---|---|
|
||
| Web | `web/` React SPA / PWA | 浏览器入口 |
|
||
| Windows | Wails | 产物名 `Commilitia Drop.exe` |
|
||
| macOS | Wails | 产物名 `Commilitia Drop.app`;当前仍是正式版本 |
|
||
| iOS | SwiftUI | 主 target `CommilitiaDrop` |
|
||
|
||
`ios/CDrop` 中的原生 macOS target 仍是迁移候选。它在完成架构覆盖、功能对等、三语界面、
|
||
签名扩展与真机验收前,不替换 Wails,也不得作为第二个 macOS 客户端分发。
|
||
|
||
面向系统与跨端诊断可见的协议标识也使用完整品牌命名空间:URL Scheme 为
|
||
`commilitia-drop://`,WebRTC DataChannel 与 iOS 文件流 Scheme 为
|
||
`commilitia-drop-file`,Bonjour 服务为 `_commilitia-drop._tcp`。Bundle ID、App Group、
|
||
Keychain service、LaunchAgent label、环境变量与存储路径等不直接决定显示名的内部标识保持稳定。
|
||
|
||
Auth Broker 的 application key 会出现在 OAuth `app` 参数和 JWT scope 中,因此随本次更新把
|
||
既有 `cdrop` application 原位改为 `commilitia-drop`,并将回调改为
|
||
`commilitia-drop://auth-callback`;旧会话可直接失效并由各客户端重新登录。
|
||
|
||
全客户端正式构建、签名、公证、安装、服务端迁移与验证流程见
|
||
[`docs/client-build-install.md`](docs/client-build-install.md)。
|
||
|
||
MVP 里程碑(历史记录):
|
||
|
||
| 里程碑 | 内容 | 状态 |
|
||
|---|---|---|
|
||
| M0–M5 | 后端:chi / sqlite WAL / dev+prod auth 中间件 / SSE Hub / signaling + 状态机 / Relay ring buffer | 完成 |
|
||
| M6–M10 | 前端: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 + callback;Casdoor confidential client + client_secret | 完成 |
|
||
| M13 | 部署:cdrop-base 缓存层 + 应用镜像(BUILDPLATFORM 跨编译,6 MB distroless),Cloudflare Realtime TURN App,反代 reverse_proxy + flush_interval -1(SSE 友好),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
|
||
# 监听 :8080,json 日志到 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 + 拖文件 → Send;tab-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_SESSION_SECRET` 必须非空**(任意长度高熵串)—— 浏览器免重登会话的 refresh_token 落盘加密密钥(内部 SHA-256 派生 AES-256 密钥);后端 prod 启动期强制此项,缺则拒启动
|
||
5. 设 `CDROP_HS256_SECRET`(iOS Shortcut scoped token 用,不用可留空)
|
||
6. 可选 `CDROP_CF_TURN_KEY_ID` + `CDROP_CF_TURN_API_TOKEN` 启用 Cloudflare Realtime TURN over TLS(不配则前端 STUN-only 兜底)
|
||
7. 可选 `CDROP_VAPID_PUBLIC_KEY` + `CDROP_VAPID_PRIVATE_KEY` 启用 Web Push 推送通知(用 `just vapid-keygen` 生成一对;两者须同时设置,半对则拒启动;都留空则推送惰性关闭,不阻塞启动)。`CDROP_VAPID_SUBJECT` 可选(联系标识,留空回退站点 URL)
|