Files
Commilitia-Drop/README.md
T
admin 92a03aeafd 推送通知:Web Push(网页 / PWA)+ 桌面原生通知
页面 / PWA 关闭、或桌面窗口非前台时,收到文件、消息、传输完成 / 失败以系统通知提醒。
判定统一为「页面打开走应用内提示,否则系统通知」,三端各自落地;网页端的判定直接复用
Hub.SendTo 的投递结果,零额外状态。

- Web Push(VAPID,账号无关):服务端 internal/push.Sender 在事件未经 SSE 投递到目标
  设备时(即该设备页面已关)发推送,文案按订阅 locale 在服务端渲染(中简 / 中繁 / 英)、
  404 / 410 自动清理死订阅;新增 push_subscriptions 表(含 locale,主键为 endpoint 的
  SHA-256 以幂等 upsert)+ /api/push/{vapid-key, subscribe POST/DELETE} 端点
- 事件挂接:transfer:incoming / message(离线返 202,文本随推送送达)/ transfer:state
  DONE|FAILED,均未投递才推
- 配置 CDROP_VAPID_PUBLIC_KEY / CDROP_VAPID_PRIVATE_KEY(半对即拒启动,都空则推送惰性
  关闭)+ just vapid-keygen 生成工具(cmd/vapidgen)
- 前端:service worker 加 push / notificationclick(仍不拦截 /api 与导航);net/push.ts
  订阅管理 + push store slice + 设置页「推送通知」面板(仅浏览器 / PWA);登录后 resyncPush
  把既有订阅重新登记到当前用户,换用户 / endpoint 轮换 / 切换语言皆自愈
- 桌面原生通知:platform/notify_{darwin,windows,other}.go + notify_darwin.m,仅窗口非
  前台时弹原生通知(macOS UNUserNotificationCenter,未签名时安全空操作、待签名后显示;
  Windows go-toast 即用);app.go ShowNotification 绑定,前端 notifyNativeIfBackground
  复用 hub 事件分发 + 客户端本地化
- 文档:README / .env.example / compose 模板补 CDROP_VAPID_* 配置

注:sqlc v1.31.1 对 query 文件里的多字节字符会错位偏移、生成损坏 SQL,故 query 注释保持纯
ASCII(文件内留有警示)。
2026-06-18 16:56:47 +08:00

166 lines
8.1 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 存系统密钥库(信封加密)
- **浏览器免重登 + PWA**:可安装为独立 PWA;浏览器侧 refresh_token 不再进 JS,改由服务端 HttpOnly cookie 会话持有(AES-256-GCM 落盘),关闭重开自动免重登(7 天滑动失活),设备名随会话持久化(抗 PWA 存储清除)
- **推送通知**:页面 / PWA 关闭时,收到文件、消息、传输完成或失败以系统通知提醒;页面打开时仍走应用内提示。网页走 Web Push(VAPID,覆盖浏览器 / Android / iOS 16.4+ 已安装 PWA),桌面客户端走原生系统通知(仅窗口非前台才弹)。文案按设备语言在服务端渲染(中简 / 中繁 / 英)
- **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_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)