From 19f8f7588ea95dd50be403c644ad3b1af60fd08a Mon Sep 17 00:00:00 2001 From: commilitia Date: Mon, 15 Jun 2026 21:49:23 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=9E=B6=E6=9E=84=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E4=B8=8E=E5=8F=98=E6=9B=B4=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cdrop 的架构 / 协议 / 鉴权设计(PROJECT_BRIEF)、前端设计(FRONTEND_DESIGN)、变更记录(CHANGELOG)。 公开发布初始提交:完整文档历史留存于私有归档,公开分支自此干净起步。 --- CHANGELOG.html | 6607 ++++++++++++++++++++++++++++++++++++++++++++ CHANGELOG.src.html | 153 + FRONTEND_DESIGN.md | 317 +++ PROJECT_BRIEF.md | 244 ++ 4 files changed, 7321 insertions(+) create mode 100644 CHANGELOG.html create mode 100644 CHANGELOG.src.html create mode 100644 FRONTEND_DESIGN.md create mode 100644 PROJECT_BRIEF.md diff --git a/CHANGELOG.html b/CHANGELOG.html new file mode 100644 index 0000000..e830249 --- /dev/null +++ b/CHANGELOG.html @@ -0,0 +1,6607 @@ + + + + + + +cdrop — Changelog + + + + + + +
+
+

cdrop — Changelog

+

时间倒序。细粒度迭代见 git 历史;本档只记录结构性 / 行为性变更与重要 fix。

+

· docs branch

+
+
+ + + + + +
+
+ +
+ +

本 Changelog 自 2026-05-22 起以 HTML 格式维护。此前之变更可从 main 分支 git log 取得;下表为近期较显著之 commit。

+ +

2026-06-14 — 安全加固:中继会话防耗尽 + 令牌入系统密钥库 + 鉴权 / 限流多项收紧

+ +

多 agent 安全审计后的一轮加固,按信任模型(信任账号所有者 + 服务端;不信任跨用户 / 网络 hop peer / 本机其他软件)逐项收紧。

+ + + +

2026-06-14 — 桌面文件落盘可配置 + 传输列表行内操作 + macOS 下载兜底修正

+ + + +

2026-06-12 — 接入聚珍(Juzhen)2.0 综合 CJK 排版:全角式 + 功能分级 + 本地优先字体

+ + + +

2026-05-22 — README 补 clone 注意事项

+ + + +

2026-05-22 — cjk-autospace 抽出为共用 submodule

+ + + +

历史变更(移转前未以本档记录)

+ + + + + + + + + + + +
commit说明
aa80793CJK 排版子系统:Pangu.js v7.2.1 端口 + cjk.css + Button label 包裹
4e8c541前端视觉重塑后续迭代 + i18n 本地化清理
81dfbe1前端重设计:tokens 变量化 + features-first + 原子组件 + 首页重画 + Toast + Dracula Pro 主题
6db6444新增繁体中文(臺灣)+ locale 检测按 Hans / Hant + 区域子标签细分
+ +
+ + + + + + + + + + + diff --git a/CHANGELOG.src.html b/CHANGELOG.src.html new file mode 100644 index 0000000..7d4865f --- /dev/null +++ b/CHANGELOG.src.html @@ -0,0 +1,153 @@ + + + + + +cdrop — Changelog + + + + + + +
+
+

cdrop — Changelog

+

时间倒序。细粒度迭代见 git 历史;本档只记录结构性 / 行为性变更与重要 fix。

+

· docs branch

+
+
+ +
+
+ +
+ +

本 Changelog 自 2026-05-22 起以 HTML 格式维护。此前之变更可从 main 分支 git log 取得;下表为近期较显著之 commit。

+ +

2026-06-14 — 安全加固:中继会话防耗尽 + 令牌入系统密钥库 + 鉴权 / 限流多项收紧

+ +

多 agent 安全审计后的一轮加固,按信任模型(信任账号所有者 + 服务端;不信任跨用户 / 网络 hop peer / 本机其他软件)逐项收紧。

+ + + +

2026-06-14 — 桌面文件落盘可配置 + 传输列表行内操作 + macOS 下载兜底修正

+ + + +

2026-06-12 — 接入聚珍(Juzhen)2.0 综合 CJK 排版:全角式 + 功能分级 + 本地优先字体

+ + + +

2026-05-22 — README 补 clone 注意事项

+ + + +

2026-05-22 — cjk-autospace 抽出为共用 submodule

+ + + +

历史变更(移转前未以本档记录)

+ + + + + + + + + + + +
commit说明
aa80793CJK 排版子系统:Pangu.js v7.2.1 端口 + cjk.css + Button label 包裹
4e8c541前端视觉重塑后续迭代 + i18n 本地化清理
81dfbe1前端重设计:tokens 变量化 + features-first + 原子组件 + 首页重画 + Toast + Dracula Pro 主题
6db6444新增繁体中文(臺灣)+ locale 检测按 Hans / Hant + 区域子标签细分
+ +
+ + + + + + + + + + + diff --git a/FRONTEND_DESIGN.md b/FRONTEND_DESIGN.md new file mode 100644 index 0000000..eadfad3 --- /dev/null +++ b/FRONTEND_DESIGN.md @@ -0,0 +1,317 @@ +# Commilitia Drop — 前端设计 + +补充 PROJECT_BRIEF.md,专注 web 前端实现。后端、协议、鉴权见主 brief。 + +> 本文记录 MVP 初版前端设计(Mantine + 浅色)。其后前端经「Theme B」重塑:自定义设计 token、品牌 web font、Dracula 深色、聚珍(Juzhen)CJK 综合排版——视觉部分以 [`CHANGELOG`](CHANGELOG.html) 与实际代码为准。 + +--- + +## 1. 技术栈 + +``` +Vite 构建 +React 18 + TypeScript 框架 +TanStack Router 路由(类型安全 > React Router) +Zustand state 管理 +Mantine v7 UI 组件库(CSS modules,零运行时开销) +@mantine/dropzone 文件拖拽 +@mantine/notifications toast +Tailwind CSS utility 微调(不主导样式) +lucide-react 图标 +原生 RTCPeerConnection P2P +``` + +**不引入** starter 模板。从 `npm create vite@latest -- --template react-ts` 起步,按需装包。 + +**不引入**:MUI / Ant Design(bundle 过大)、shadcn/ui(手搓成本与 MVP 速度冲突)、TanStack Query(MVP 阶段 SSE + Zustand 已够,需要时再加)、Storybook / i18next / Husky 等企业级配套。 + +--- + +## 2. 资源占用 + +前端是纯静态资源,部署在 Caddy 静态目录。服务器开销可忽略: + +- gzipped bundle 估计 400-600 KB +- 服务器内存 / CPU 接近零(无 SSR) +- 真正吃资源的是后端 Go 进程的 ring buffer 和 SSE 长连接 + +构建在本地完成,部署只 push 静态文件。 + +--- + +## 3. 设计语言 + +中性、克制、工具感。参考点: + +- **Linear**:留白、灰阶、克制的强调色 +- **Cloudflare Dashboard**:信息密度合理的卡片式布局 +- **Tailscale Admin**:设备列表、状态指示 + +避开: + +- AirDrop 圆圈布局(不打算抄 Apple 的视觉语言,且圆圈在设备数 > 8 时退化严重) +- Snapdrop / PairDrop 极简主义(看着是玩具,不是工具) +- Material You(不适合自托管极简风) + +**配色**:浅色优先,且 MVP 阶段**仅实现浅色**。深色模式属阶段三 UX 完善的范围,不在 MVP 内。Mantine 配置 `defaultColorScheme="light"`,不读取 `prefers-color-scheme`,不提供切换控件。 + +强调色 cdrop 蓝(`#3b82f6` 量级,待最终选定)。文本主色深灰(`#1f2937`),次级灰阶按 Mantine `gray.6 / gray.7` 走。背景纯白或近白(`#fafafa`)以减少屏幕反光强度。 + +### 字体与 CJK 排版 + +> 原 MVP 计划用系统字体栈、不引入 web font;前端重塑(Theme B)后改为品牌 web font + CJK 综合排版。 + +**字体栈**(`web/src/styles/fonts.css`,逐 region 由 `:lang()` 切 SC/TC/JP/KR,`:root` 默认 SC,亦兼英文 / 无 lang 回退): + +- **Sans**(正文):`Orbit Gothic CJK ` + 系统回退 +- **Serif**:`Noto Serif CJK `(本地合一、全字覆盖)→ `Noto Serif `(Google Fonts Web 子集,跨区并集补字)→ 系统 +- **Mono**:`Maple Mono` + `Orbit Gothic CJK ` + 系统 + +web font 由三个 CDN 的 `@font-face` 提供(`index.html` 三条 ``),均 `font-display: swap`。因这些 `@font-face` 不含 `local()`、family 名又与本机安装同名 → 浏览器被 `@font-face` 捕获、永远走网络(同名本地副本不被用)。故 `web/src/styles/fontface.css` 加**本地优先覆盖层**:在 CDN `` 之后、对项目用到的字重重声明 `@font-face`,`src: local(...), url()` —— 本机命中即零网络瞬时渲染,未装才回退 CDN(swap)。 + +**CJK 综合排版(聚珍 / Juzhen)**:submodule `web/src/lib/cjk-autospace`(上游 `git.commilitia.net/admin/cjk-autospace`;引擎机制见其 `README.md` / `ARCHITECTURE.md`)。`web/src/utils/cjkAutospace.ts` 为 cdrop 侧薄壳: + +- `createJuzhen({ lang:{style:"quanjiao"}, scope:{include:".juzhen"}, level:{text:".juzhen"}, pillSelector:"code, kbd, .cjk-pill" })`,`render` / `revert` + MutationObserver 增量(render 幂等)。 +- **全角式**(`quanjiao`):句内点号不挤、占全角,仅承载 justify 单份间距与行端收缩。 +- **作用域** `.juzhen`(`#root` 加该类):处理与 normalize 收敛于此子树,`body` 上 Mantine portal 不受扰。 +- **功能分级**:文本级(中西间隙 / 标点挤压,任意文本片段适用)全树默认跑;段落级(禁则避头尾 / 垂悬字避免 / 长英文断词,需行段布局)仅带 `data-jz-level="paragraph"` 的流动正文(消息 / 剪贴板 / Callout / Toast / 登录说明等)跑。cdrop 绝大多数是 UI chrome(短标签),故默认文本级、正文 opt-in,失败模式良性。 +- **copy-clean**:注入的间隙 / 结构元素不入剪贴板,复制得紧凑原文。 + +--- + +## 4. 核心交互:选择目标设备 + +放弃圆圈布局,采用**单行可横向滚动的设备条**(接近 AirDrop iOS 分享面板)。要求同时支持两种顺序: + +- **先选文件,再选设备**(拖入 → 高亮可用设备 → 点击设备发送) +- **先选设备,再选文件**(点击设备打钩 → 拖入或点击"选择文件" → 自动发送) + +### 布局(Home 页) + +``` +┌──────────────────────────────────────────────────────────────┐ +│ Commilitia Drop [Settings] [User] │ +├──────────────────────────────────────────────────────────────┤ +│ │ +│ Devices (3 online) │ +│ ┌──────────┬──────────┬──────────┬──────────┬───── │ +│ │ ● │ ● │ ○ │ ● │ … │ +│ │ MBA M4 │ Win-Desk │ iPhone │ Mac mini │ │ +│ │ desktop │ desktop │ mobile │ desktop │ │ +│ │ this dev │ ✓ │ │ │ │ +│ └──────────┴──────────┴──────────┴──────────┴───── │ +│ │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ │ │ +│ │ Drop files here, or click to browse │ │ +│ │ │ │ +│ │ Selected: report.pdf (4.2 MB) [×] │ │ +│ │ │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ │ +│ [ Send to Win-Desk ] ← primary CTA, 状态联动 │ +│ │ +├──────────────────────────────────────────────────────────────┤ +│ Recent transfers │ +│ • report.pdf → Win-Desk ✓ 2 min ago │ +│ • photo.zip ← iPhone ✓ 5 min ago │ +│ • video.mp4 → Mac mini × failed (timeout) │ +└──────────────────────────────────────────────────────────────┘ +``` + +### 设备卡片状态 + +每张卡片宽度约 120-140 px,固定不随屏幕拉伸。横向 `overflow-x: auto`,触控板 / 鼠标滚轮可推。 + +| 状态 | 视觉 | +|---|---| +| this device | 灰色边框 + "this device" 标签,不可选 | +| online idle | 白底 + 浅灰边框 + 绿点,可选 | +| online selected | 蓝色描边 + ✓ 角标 + 浅蓝底 | +| online incompatible(文件已选但目标设备 capability 不允许) | 灰底 + 50% 透明 + 不可选 | +| online incompatible(被选后用户拖入了不兼容文件) | 同上,自动取消选中 + Notification 提示 | +| online busy(已在收发) | 黄色环 + 进度数字 | +| offline | 灰底 + 50% 透明 + 不可选(默认隐藏,"显示离线"开关展开) | + +每张卡片显示:状态点 + 设备名 + 类型小字。不显示头像 / 图标(避免维护设备图标资源)。设备名超长截断为省略号,hover 显示 tooltip 全名。 + +### 双向选择顺序的状态机 + +文件和目标设备分别独立可选,CTA 状态由两者组合: + +``` +state = (file: File | null, target: Device | null) + +(null, null) → CTA: disabled "Select file and device" +(file, null) → CTA: disabled "Select a device" +(null, device) → CTA: disabled "Select a file" +(file, device, ok) → CTA: enabled "Send to {device.name}" +(file, device, !compat) → CTA: disabled "Cannot send {file.type} to {device.type}" +``` + +CTA 按钮永远可见在拖拽区下方,文案随状态变化。点击 CTA 即触发 `POST /api/transfer/initiate`。 + +### 拖拽优化 + +- **Tab 键支持**:设备条可用方向键导航,Space 切换选中。无障碍是免费的,Mantine 默认就有 +- **拖入页面任意位置**触发全屏 dropzone overlay,提示松手位置(默认行为继承 dropzone) +- 文件拖到具体设备卡片上 → 该设备自动选中并立即发送("快捷路径",跳过 CTA)。这是 AirDrop 没有的额外便捷 +- 选了不止一个文件 → MVP 拒绝(提示"目前每次只支持一个文件")。多文件留到阶段三 + +### 为什么不用圆圈 + +- 圆圈布局在设备数 > 8 时退化为环形排列,视觉混乱 +- 圆圈区分度低,必须靠头像区分设备,但头像维护成本高 +- 单行卡片在桌面浏览器(cdrop 主要场景)上信息密度更合适 +- 一行卡片可以承载更多信息(设备名 + 类型 + 状态 + 选中标),圆圈只能放头像和小字 +- AirDrop 单行布局是 Apple 在分享面板里给出的样式,本身就是为"多目标快速选择"优化过的 + +--- + +## 5. 路由 + +``` +/ Home(设备 + 拖拽 + 历史) +/login Casdoor PKCE 跳转中转 +/oauth/callback Casdoor 回调 +/setup 首次填设备名(设备名未设时强制跳转此页) +/settings 设备名修改、退出登录 + 后续:shortcut token 管理、外观(深色切换) +``` + +TanStack Router file-based 路由模式,类型安全 + 自动代码分割。 + +--- + +## 6. 状态管理 + +Zustand 一个 store,分 slice: + +``` +useStore = { + auth: { tokens, user, login, logout, refresh } + device: { selfName, selfType, set, hydrate } + hub: { connected, devices, signal, sse-handlers } + transfer: { current, history, initiate, accept, cancel } + ui: { selectedDevice, selectedFile, notifications } +} +``` + +不引入 Redux 或 Jotai。Zustand 的 selector + `useShallow` 处理性能问题足够。 + +--- + +## 7. 组件清单 + +业务组件(自己写): + +``` +DeviceCard 单设备卡片(状态联动) +DeviceStrip 横向滚动设备条 +FileDropzone 拖拽区(@mantine/dropzone 包一层) +SendCTA 主按钮(状态机文案) +TransferRow 历史记录单行 +TransferProgress 进度条 / RingProgress +DeviceTypeBadge desktop / mobile / browser 标签 +``` + +页面级: + +``` +HomePage +LoginPage +SetupPage +SettingsPage +OAuthCallbackPage +``` + +通用 lib(与组件无关): + +``` +lib/auth.ts PKCE flow, refresh, fetch wrapper +lib/api.ts 带 Authorization + X-Device-Name 的 fetch 封装 +lib/hub.ts 自实现 SSE 客户端(fetch + ReadableStream) +lib/p2p.ts P2PSession(裸 RTCPeerConnection) +lib/relay.ts RelaySession(流式 GET + 分块 POST) +lib/transfer.ts Orchestrator(决定 P2P / Relay / fallback) +``` + +--- + +## 8. SSE 客户端要点 + +浏览器原生 `EventSource` 不支持自定义 header,所以不能直接带 `Authorization`。自实现: + +```ts +const res = await fetch('/api/hub/events', { + headers: { Authorization: `Bearer ${token}`, 'X-Device-Name': name }, + signal: abortController.signal +}); +const reader = res.body!.getReader(); +const decoder = new TextDecoder(); +let buf = ''; +while (true) { + const { value, done } = await reader.read(); + if (done) break; + buf += decoder.decode(value, { stream: true }); + // 按 '\n\n' 切分 SSE 事件,按 'event:' / 'data:' 字段解析 +} +``` + +重连:指数退避(1s → 2s → 4s → 8s → 16s 上限),断开时 UI 显示 "Reconnecting..."。 + +token 接近过期时(< 5min)先 refresh,再重建 SSE 连接,避免连接中途 401。 + +--- + +## 9. 性能 / Bundle 优化 + +- TanStack Router 自动代码分割:每个路由独立 chunk +- Mantine 按需 import,不要 `import { Button } from '@mantine/core/dist/...'` +- 使用 `vite-plugin-imp` 或依赖 Mantine 自身的 ESM tree-shake(v7 默认就好) +- recharts / d3 不需要(cdrop 不画图) +- `react-helmet` 不需要(应用很简单,document.title 直接改) + +构建产物期望: + +``` +index.html ~ 2 KB +assets/index.[hash].js ~ 250-350 KB (gzipped) +assets/index.[hash].css ~ 30-50 KB (gzipped) +其他 chunk 按路由懒加载 +``` + +--- + +## 10. 浏览器兼容 + +目标: + +- Chrome / Edge ≥ 110(涵盖 99% Chromium 用户) +- Firefox ≥ 110 +- Safari ≥ 16.4(macOS 13+ / iOS 16.4+,WebRTC 完整支持) + +不支持 IE / 旧 Edge / iOS 15 及以下。原因:WebRTC DataChannel 行为差异和 fetch streaming 兼容问题不值得投入精力。 + +--- + +## 11. 与主 brief 的对齐 + +§3 技术栈一行更新为: + +``` +**前端**:Vite + React 18 + TypeScript / Mantine v7 + Tailwind / TanStack Router / Zustand / 原生 RTCPeerConnection / lucide-react +``` + +§7 MVP 验收标准的 "Home 页看到对方在线" 默认指本文件 §4 描述的设备条。 + +§8 阶段三 UX 完善中"暗色模式"对应本文件 §3 提到的 MVP 不实现深色——阶段三再做。 + +--- + +## 12. 你(Claude Code)需要决策的事 + +1. 设备卡片的具体尺寸、间距、状态色值,等 §3 强调色锁定后再敲定 +2. CTA 按钮的具体文案(中文 vs 英文 vs 双语)。MVP 用英文,i18n 走阶段三 +3. 历史记录显示几条 / 是否分页 / 是否支持过滤。MVP 默认显示最近 20 条 +4. 有更好的设计参考时,可以反驳 §3 的方向选择 diff --git a/PROJECT_BRIEF.md b/PROJECT_BRIEF.md new file mode 100644 index 0000000..d0b1d35 --- /dev/null +++ b/PROJECT_BRIEF.md @@ -0,0 +1,244 @@ +# Commilitia Drop — 跨 OS 剪贴板与文件传输服务 + +> 本文是项目最初的 MVP 规格(设计基线),保留作架构参考;**当前实现状态以 [`CHANGELOG`](CHANGELOG.html) 为准**——其后已落地剪贴板/消息通道、Wails 桌面端、安全加固等,部分「远期」项已实现。 + +项目代号 / 仓库名 / 二进制名:`cdrop` +部署域名:`drop.commilitia.net` +节点:自托管(单机 Docker + 反代) + +--- + +## 1. 目标 + +跨 OS(Win / 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 存内存或 sessionStorage;refresh_token 存内存(不写 localStorage 防 XSS)。 +- **Native client**(desktop / mobile,远期):标准 Casdoor OAuth + PKCE + refresh_token rotation。系统浏览器或 loopback redirect 完成首次登录;token 存 OS keychain(macOS 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 自签 JWT(HS256),长 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、信令、传输状态):SSE(server→client)+ POST(client→server) + - 不用浏览器原生 EventSource(不支持自定义 header),用 fetch + ReadableStream 自实现 SSE 解析 +- **Relay 通道**(P2P 失败时): + - Receiver:流式 GET(chunked transfer-encoding) + - Sender:多次分块 POST(Safari / Firefox 不支持 streaming POST) + - Server 端 in-memory ring buffer 桥接 + +### 文件传输 + +- **优先 P2P**:WebRTC DataChannel,DTLS 即 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 二次确认;不限文件大小,但中继有内存与并发上限 + +### 存储 + +- SQLite(modernc.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 用 gofmt;TS / JSX / JSON 按 Allman、数组内空格、运算符前置。 + +--- + +## 4. SQLite Schema(MVP) + +`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 扩大到剪贴板或客户端