f21fa5b5e8
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)。 本次为公开发布初始提交:完整开发历史(含部署细节)留存于私有归档,公开仓库自此干净起步。
404 lines
42 KiB
Markdown
404 lines
42 KiB
Markdown
# cdrop 桌面客户端实施计划
|
||
|
||
> Wails v2 · macOS 优先 + Windows 已落地
|
||
> 状态:**macOS 与 Windows 双端均已实现并 RDP/真机实测通过**——A2 前端复用 + 托盘/菜单栏 + 剪贴板双向同步 + 桌面设置 + session 持久化 + 开机自启(含自启静默驻留)+ 设备类型登记。macOS 见 §10,Windows 落地(含「custom-scheme 不支持流式」核心坑与 127.0.0.1 本地代理解法)见 §11。计划已据原生平台研究 sweep(`desktop/RESEARCH.md`)校正。
|
||
> 关联:根 `README.md`、`desktop/`(脚手架)、`desktop/RESEARCH.md`(调研依据),记忆 `cdrop-desktop-client` / `cdrop-deploy-pointers` / `cdrop-state`。
|
||
|
||
---
|
||
|
||
## 实现进度(截至 2026-06-13)
|
||
|
||
| 部分 | 状态 | commit |
|
||
|---|---|---|
|
||
| 后端 OIDC audience 多值(R1) | ✅ 已提交(现非必需、保留无害) | `36abec1` |
|
||
| 后端 `/api/auth/config` 发布 `token_url` | ✅ 已提交 + **已部署 prod** | `a393b5d` |
|
||
| **D1 OAuth loopback 登录**(复用 web client + PKCE + Refresh) | ✅ 已实现,**登录真机 e2e 通过** | `c876413` |
|
||
| D4 剪贴板双向策略层 + 认证 API 客户端 | ✅ 已实现(23 单测全绿,尚未接入 app) | `516d3b2` |
|
||
| **桌面 app 主体(§10)**:A2 复用 + 菜单栏/dock + 剪贴板接入 + 设置 + 本地配置 | ✅ 已实现(本地全绿,待真机勾验菜单栏/剪贴板/自启) | 本轮待提交 |
|
||
| **登录 session 本地持久化(§10.7.1)**:Go 文件 + 启动注入 + 同步水合 | ✅ 已实现,**重启免登真机跑通** | 本轮待提交 |
|
||
| **凭据安全一致化(§10.8)**:refresh_token 全平台不进 JS | ✅ 已实现(绑定层 grep 零命中) | 本轮待提交 |
|
||
|
||
**复用 web client(暂行决策)**:桌面端不建独立 Casdoor client,复用 web 的 confidential + PKCE client(与 web 同一 client_id)做 loopback PKCE(不送 secret),靠长寿命 refresh token 维持。token 的 aud = web client_id = 后端 `OIDCAudience`,故 R1 多值非必需。
|
||
|
||
**待实现 / 待验证**:① 真机勾验(本环境无 GUI 不可达)——菜单栏三项、dock 动态隐藏、剪贴板跨设备上下行、开机自启、refresh,清单见 §10.6;② D3 系统通知(未做);文件落盘已落地(桌面收文件写入可配置目录,默认 `~/Downloads`,见 §D3 落盘);③ D5 全局快捷键 + Quick Send 浮窗(受 v2/v3 决策闸,未做);④ token 持久化升级 Keychain(推迟到 Developer-ID 签名后,§10.8);⑤ Windows/Linux 原生 Source / 托盘 / 自启目前为 stub。
|
||
|
||
---
|
||
|
||
## 0. 范围、非范围与本轮关键决策
|
||
|
||
**范围**:常驻桌面客户端,补浏览器做不到的——后台常驻、剪贴板自动监听上传、文件落盘、系统通知、全局快捷键。平台 **macOS(Apple Silicon + Intel)优先**,**Windows(x64)保持兼容**(同源、同分支,平台差异用 Go build tag 隔离)。
|
||
|
||
**非范围(推后)**:Linux 打包、iOS / Android 原生、自更新(仅版本检查提示)、产品化 / 多租户、Admin、Symmetric NAT 的 mDNS 直连、本地内容缓存 / 离线。
|
||
|
||
**定位**:桌面端不是“另一份 UI”,而是“带原生外壳的同一份 UI”——UI 复用 `web/src` 的 React 应用,价值全在 Go 外壳提供的原生能力。
|
||
|
||
### 决策 C:先做框架无关里程碑,推迟 v2/v3 取舍(2026-06-13 定)
|
||
|
||
sweep 发现 Wails v2 缺两样桌面核心能力:**无内建系统托盘**、**单原生窗口**(见 §4 D2 / D5、§9)。是否升级 Wails v3-alpha 是悬而未决的架构岔路。**本轮决定:先在 v2 上推进与窗口框架无关的 D1(OAuth)+ D3(通知)+ D4(剪贴板),把 v2/v3 取舍推迟到真正动 D2(托盘)/ D5(多窗)之前。** 理由:这三个里程碑在 v2 / v3 上实现差异不大,能先产出价值,同时让 v3 再成熟一截。
|
||
|
||
### 签名决策:暂不申请 Developer 证书(2026-06-13 定)
|
||
|
||
暂不申请 Apple Developer 证书。macOS 产物仅 **未签名 / ad-hoc 签名的 `.app`**,**不公证、不封装 DMG**。下游影响(须正视):
|
||
|
||
- 分发侧 macOS Gatekeeper 会拦“未知开发者”,自用需右键打开或 `xattr -dr com.apple.quarantine`。
|
||
- **D3 macOS 系统通知(`UNUserNotificationCenter`)要求 app 有 `CFBundleIdentifier` 且“已签名”**——未签名不工作。无 Developer 证书时须用 **ad-hoc 签名**(`codesign -s - --force --deep YourApp.app`)补一个本地有效签名 + bundle id;ad-hoc 是否足以让本机通知工作**须真机实测**(§4 D3)。
|
||
- `desktop/RESEARCH.md` 的“打包·签名·公证手册”留作**未来若申请证书**时的参考;当前路径以本文 D6 为准。
|
||
|
||
---
|
||
|
||
## 1. 技术栈与总体架构
|
||
|
||
### 1.1 选型
|
||
|
||
| 维度 | 决定 | 理由 |
|
||
|---|---|---|
|
||
| 框架 | **Wails v2.12.0**(Go + 系统 WebView) | 系统 WebView 省内存;Go 单语言写原生;单 codebase |
|
||
| 待评估 | **Wails v3-alpha**(见决策 C / §9) | 原生覆盖 systray + 多窗口 + ActivationPolicy;D2/D5 前定夺 |
|
||
| 淘汰 | Electron / Tauri / 原生双栈 / PWA | Electron 200+ MB;Tauri 多一门 Rust;原生双 codebase;PWA 无后台常驻 |
|
||
| 资源预算 | macOS idle 80–120 MB;Windows idle 100–150 MB;包体 10–20 MB | WebView 复用系统组件 |
|
||
|
||
### 1.2 瘦客户端架构
|
||
|
||
- **全部业务走 `drop.commilitia.net`**:桌面端沿用 Web 的 API 客户端,本地只存 token + 少量设置。
|
||
- **Go 外壳只做四类原生事**:①把 OS 事件源(剪贴板变更、热键、OAuth 回调)翻译成事件推给 WebView;②把 WebView 的请求(开系统浏览器、写文件、发通知)翻译成 OS 调用;③后台常驻;④打包。
|
||
|
||
### 1.3 与后端的契约(桌面端用到的端点)
|
||
|
||
沿用 Web 现有 API(`web/src/net/api.ts` 注入 `Authorization: Bearer`,401 懒刷新):
|
||
|
||
| 端点 | 用途 | 里程碑 |
|
||
|---|---|---|
|
||
| `GET /api/auth/config`、`POST /api/auth/exchange`、`POST /api/auth/refresh` | OIDC 配置 / 换 token / 刷新 | D1 |
|
||
| `GET/PUT /api/clipboard` | 云剪贴板读写 | D4 |
|
||
| `GET /api/hub/events`(SSE) | 服务端推送(新剪贴板 / 消息 / presence) | D3 / D4 |
|
||
| `POST /api/message` | 文本消息 | D5 Quick Send |
|
||
| `POST /api/transfer/initiate`(+ `/api/hub/signal`、`/api/calls/credentials`) | 文件传输 / WebRTC | 后续(非 MVP 主线) |
|
||
| `POST /api/me/disconnect` | 登出即时下线 | D2 退出 |
|
||
|
||
> D1 例外:桌面端**不复用** `/api/auth/exchange`(它是 confidential 代理、附 web client_secret),改为直连 Casdoor token 端点做 public-client PKCE。详见 §3。
|
||
|
||
---
|
||
|
||
## 2. 前端复用 + 平台抽象层
|
||
|
||
### 2.1 前端复用(方案 A2)
|
||
|
||
D0 脚手架的 `desktop/frontend` 是 Wails 默认 vanilla-ts 模板,**尚未指向 `web/src`**。D1 第一步改为复用现有 React 应用:
|
||
|
||
- **A2(采用)**:`desktop/frontend` 作薄入口,import `web` 为本地包(npm workspace / `file:` 依赖)。`web` 暴露可复用入口 `mountApp(root, platform)`,desktop 入口传 `platform: "desktop"` 适配器。隔离清晰、桌面专属代码有家、web 不被污染。
|
||
- 否决:A1(`frontend` 直指 `../web`,桌面专属代码无处落脚)、A3(复制 web,分叉地狱)。
|
||
|
||
### 2.2 平台抽象层(Platform 接口)
|
||
|
||
Web 与桌面在四处行为不同,UI 只依赖 `Platform` 接口,web / desktop 各给一份实现:
|
||
|
||
| 能力 | Web 实现 | Desktop 实现(经 Wails bind 调 Go) |
|
||
|---|---|---|
|
||
| **登录** | `window.location.href` 整页跳 authorize | 系统浏览器 + loopback 回调 + 注入(§3,D1) |
|
||
| **token 存储** | zustand store | Go 侧 Keychain / DPAPI,启动注入 |
|
||
| **通知** | Web Notification(前台限定) | 系统通知中心(后台也行,D3) |
|
||
| **文件落盘** | 浏览器下载 | Go 写可配置目录(默认 `~/Downloads`)+ 揭示 Finder(✅ 已落地) |
|
||
|
||
> 铁律:**WebView 内绝不 `window.location.href` 跳外部 authorize**(会把 WebView 自身导航走、应用白屏,且违反 RFC 8252 §8.12 嵌入式 user-agent 禁令)。登录走系统浏览器 + 原生回调桥。
|
||
|
||
---
|
||
|
||
## 3. D1:OAuth 登录闭环(已据 sweep 定型)
|
||
|
||
sweep 把 D1 从“最高风险未知”变成“路径已明、含一处后端改动”。完整客户端机制 + 代码骨架见 `desktop/RESEARCH.md` 的“OAuth 原生回调通道——Wails 客户端机制”。要点:
|
||
|
||
### 3.1 回调通道:loopback(已定,弃 scheme)
|
||
|
||
采用 **RFC 8252 loopback**:Go 起临时 `127.0.0.1:0`(必须 IPv4 字面量,不用 `localhost`)HTTP server 捕获 `code`。相对自定义 scheme `cdrop://` 的优势:免单实例锁、免 `Info.plist` / 注册表登记、免深链解析、跨平台代码一致。scheme 留作未来“浏览器点 cdrop 链接唤起桌面端”的另一用例。
|
||
|
||
### 3.2 token 交换放 Go 侧(已定)
|
||
|
||
PKCE `verifier` 与换得的 token 全程不进 WebView / JS 上下文(防注入脚本窃取),并由 Go 直接持久化进 Keychain / DPAPI。前端只收“登录成功 / 失败”事件(`runtime.EventsEmit` → 前端 `EventsOn`)。
|
||
|
||
### 3.3 R1:后端 / Casdoor 协调(最小一处改动,已解)
|
||
|
||
sweep 源码级确认:Casdoor 支持 public client(PKCE 时 `client_secret` 可空)、对 `http://127.0.0.1` 任意端口自动放行(`IsValidOrigin` 短路,需 Casdoor ≥ ~v3.3)。桌面直连 Casdoor 端点:authorize=`/login/oauth/authorize`,token=`/api/login/oauth/access_token`(**非**标准 `/oauth/token`)。
|
||
|
||
**唯一后端改动(方案 A,推荐)**:`CDROP_OIDC_AUDIENCE` 从单值扩成逗号分隔多值(web + desktop 两个 client_id),`internal/jwtauth/middleware.go:150` 展开进 `AnyAudience`(go-jose 语义“命中其一即可”)。issuer / JWKS / 签名校验与 web 完全一致,不动。
|
||
|
||
**前置核实**:prod 自建 Casdoor 版本须 ≥ ~v3.3.0;低于则升近版。
|
||
|
||
### 3.4 D1 任务清单
|
||
|
||
- [ ] 前端复用落地(§2.1 A2):`web` 暴露 `mountApp(root, platform)`;`desktop/frontend` 薄入口。
|
||
- [ ] `Platform` 接口 + web / desktop 双实现(先只做 login 一处)。
|
||
- [ ] Casdoor 新建 public application `cdrop-desktop`(JWT、无 secret、不勾 shared、登记 loopback redirect 兜底)。
|
||
- [ ] 后端:`CDROP_OIDC_AUDIENCE` 改多值(方案 A)。
|
||
- [ ] Go:loopback server + `runtime.BrowserOpenURL` + state 校验 + token 交换 + `EventsEmit`;token 持久化(Keychain / DPAPI)。
|
||
- **验收**:冷启动 → 点登录 → 系统浏览器授权 → 自动回应用已登录;杀进程重启仍登录;refresh 跑通。dev 模式(`VITE_CDROP_DEV_TOKEN`)可先绕 Casdoor 打通壳。
|
||
|
||
---
|
||
|
||
## 4. D2–D6 详解(含 sweep 校正)
|
||
|
||
### D2 · 菜单栏 + 窗口生命周期 ⚠️ 受 v2/v3 决策影响
|
||
- **sweep 校正(refuted)**:**Wails v2 无任何可用内建托盘 API**(`options.App` 无 `Tray`、runtime 无托盘函数、`pkg/menu/tray.go` 是 onhold 死代码、Issue #4990 官方 closed not planned)。原计划“Wails 原生托盘”不成立。
|
||
- **窗口生命周期(v2 原生可做)**:`StartHidden:true` + `HideWindowOnClose:true` 实现“启动驻留、关闭隐藏不退出”(与 `OnBeforeClose` 钩子二者互斥,不可同设)。
|
||
- **托盘图标 + 菜单(必须第三方 / 自写)**:macOS 先验证 `github.com/ra1phdd/systray-on-wails`(非阻塞 `Register`、规避 NSApplication 冲突),不达标则自写 ObjC `NSStatusBar + NSMenu`;`getlantern/systray` 在 macOS+Wails 有 objc 链接冲突,排除。Windows 用 `fyne.io/systray`。
|
||
- **隐藏 dock 图标**:`mac.ActivationPolicy` 在 v2.12 不可用;改在 `desktop/build/darwin/Info.plist` 手加 `<key>LSUIElement</key><true/>`(启动瞬间 dock 闪现约 10ms 是系统通病、无法消除)。
|
||
- **决策闸**:D2 是 v2/v3 取舍的触发点(见 §9)。若决定上 v3,D2 用 v3 原生 systray 重做。
|
||
|
||
### D3 · 系统通知 + 文件落盘 + SSE 接入
|
||
- **SSE 由 Go 持有**(非 WebView):后台常驻、窗口隐藏仍收推送、免 WebView 休眠断流。事件分流:要弹通知的走 Go 通知 API,要更新 UI 的经 `EventsEmit` 推 WebView。
|
||
- **macOS 通知**:`UNUserNotificationCenter`(cgo + ObjC)。三按钮 = `UNNotificationCategory` 含 3 个 `UNNotificationAction`,点击经 delegate `didReceiveResponse` 区分。**硬门槛(与签名决策冲突,须正视)**:要求已签名 bundle + `CFBundleIdentifier`,且须从 `.app` 运行——无 Developer 证书时须 ad-hoc 签名补救,**ad-hoc 能否让本机通知工作须真机实测**;不行则 macOS 通知降级(仅前台 toast)或推迟到将来获证书。
|
||
- **Windows 通知**:`go-toast/v2`(已在 go.mod)。`Notification.Actions` 加 3 按钮、`SetActivationCallback` 按 `Arguments` 区分。坑:须先 `SetAppData` 注册 AUMID + stub CLSID,否则 COM 激活失效、回调收不到。
|
||
- **落地节奏**:先做“单击整条通知拉主窗 + 打开文件”基础版,三按钮作增量。
|
||
- **落盘** ✅ 已落地(2026-06-14,Go 绑定写盘):单文件 `platform/download.go`(非按平台分),`SaveDownload` 写入可配置目录(`DesktopConfig.DownloadDir` 空=系统 `~/Downloads`),`sanitizeFileName` 剥目录成分防路径穿越、`uniquePath` 重名加 ` (n)`、`revealInFileManager`(`open -R` / `explorer /select`)。桌面端 web 的 `downloadBlob` 改走绑定 `SaveDownload`(blob→base64 过桥),失败回退浏览器下载;设置页可改目录(`ChooseDownloadDir` 原生选择框)。
|
||
|
||
### D4 · 剪贴板自动同步(核心价值)
|
||
- **统一底层 `golang.design/x/clipboard` v0.8.0**(macOS 已 purego/objc **无 cgo**;Windows / Linux 纯 syscall)。**sweep 校正 PLAN 旧假设**:① macOS 不是 3s,是该库写死 1s 轮询 `changeCount`;② **Windows 也是轮询**(`GetClipboardSequenceNumber` 1s),**不是** `AddClipboardFormatListener` 事件——两端机制其实一致。若坚持 Windows 真事件,可自写 syscall message-only window,但建议先轮询上线(剪贴板同步对 1s 不敏感)、按真机反馈再定。
|
||
- **自写一层 `ClipboardSource` 接口**做库不负责的三件事:
|
||
1. **敏感内容过滤**(库不做,直接用会同步密码):macOS 读 `-types` 比对 `org.nspasteboard.ConcealedType` / `TransientType` / `AutoGeneratedType` 及私有 UTI;Windows 读后 `EnumClipboardFormats` 查 `ExcludeClipboardContentFromMonitorProcessing` / `CanUploadToCloudClipboard`,命中跳过上传。
|
||
2. **回环防护**(R4):自写剪贴板会自增计数触发自己的 Watch 风暴;写入前记 hash + 自写序号,Watch 收变更先比对跳过。
|
||
3. **去重 + debounce**:复用已在 go.mod 的 `github.com/bep/debounce` + 内容 hash。
|
||
- **写回本机剪贴板**优先用库的 `Write`(规避 Wails #4132:macOS LANG 为空时编码 bug)。
|
||
- **go 版本**:该库 go.mod 要求 go 1.24,`desktop/go.mod` 声明 1.23——引入需把模块声明升 1.24+(本机 toolchain 已满足)。
|
||
- **三按钮通知**:新剪贴板到达 →“复制到本机 / 忽略 / 打开”。
|
||
- **⚠️ D4 最大不确定项(须真机实测)**:**macOS 26(Tahoe)剪贴板隐私授权**——非用户 paste UI 触发的程序化读取会弹授权 Alert,正中“后台自动监听上传”要害。Apple 新增 `detectPatterns/detectValues`(只探测类型不读内容、不弹窗)但该库未适配。须在 macOS 26 真机验证:纯 `changeCount` 轮询 vs 读内容 各自的弹窗行为。
|
||
|
||
### D5 · 全局快捷键 + Quick Send 浮窗 ⚠️ 受 v2/v3 决策影响
|
||
- **热键(已定)**:`golang.design/x/hotkey` **pin v0.4.1**(Carbon `RegisterEventHotKey`,对 `Cmd+Shift+V` 零授权弹窗)。**勿用最新 v0.6.x**——它改用 CGEventTap,每个热键都强制 Accessibility 授权,开机弹窗劣化体验;且 v0.5.0+ 要 go 1.24,v0.4.1 仅 go 1.17。**不调 `mainthread.Init`**(Wails 已占主线程 run loop)。Windows 走 `RegisterHotKey`,零 cgo、无权限。
|
||
- **Quick Send 浮窗(sweep 校正)**:**Wails v2 单原生窗口**,开不了第二个原生窗。v2 内只能“同窗 HTML 浮层 / 路由切换”近似;“失焦自隐”v2 无现成钩子、要自写 ObjC `NSWindowDelegate windowDidResignKey`(v2 路线最脏一块)。**这是上 v3 的最强理由**(v3 多窗口每窗一等对象)。
|
||
- 产品化时建议热键可重绑(规避撞车)。
|
||
|
||
### D6 · 打包(无证书:仅 `.app`)
|
||
- **macOS(当前路径)**:`wails build -platform darwin/universal` 产出 `build/bin/<App>.app`(`lipo` 合并 arm64 + amd64)。**不公证、不 DMG、无 Developer ID**。为满足 D3 通知与稳定身份,做 **ad-hoc 签名** `codesign -s - --force --deep <App>.app` + `Info.plist` 带 `CFBundleIdentifier`。分发靠手动(右键打开 / 去 quarantine)。
|
||
- **entitlements(若 ad-hoc 也带)**:非沙箱只需 `com.apple.security.network.client`;purego dlopen 系统 framework **不需** `disable-library-validation`;Carbon 热键不需 entitlement。(不要列通知 / 剪贴板 / 热键 entitlement——过度声明。)
|
||
- **Windows**:`wails build -platform windows/amd64 [-nsis]` 产出 `.exe` / NSIS 安装器;当前**不签名**(SmartScreen 会提示),获代码签名证书后再 `signtool`。
|
||
- **版本检查**:启动 / 定时拉版本端点,过期提示去下载,**不自更新**。
|
||
- **将来获证书**:完整 codesign(Developer ID)+ notarytool + DMG + CI 流程见 `desktop/RESEARCH.md` D6 手册,届时直接套用。
|
||
- **CI**:D6 后 GitHub Actions 双 runner(macos-latest / windows-latest)跑构建 + 防回归(无证书阶段仅构建 + ad-hoc,不跑公证)。
|
||
|
||
---
|
||
|
||
## 5. 跨平台代码组织(防分叉硬约束)
|
||
|
||
```
|
||
desktop/
|
||
main.go # Wails 入口(窗口 / bind / 托盘装配)
|
||
app.go # App struct + 暴露给 WebView 的方法(bind:StartLogin 等)
|
||
platform/
|
||
oauth.go # loopback server + PKCE(平台无关)
|
||
clipboard.go # ClipboardSource 接口 + 公共逻辑(过滤 / 去重 / 回环防护)
|
||
clipboard_darwin.go //go:build darwin
|
||
clipboard_windows.go //go:build windows
|
||
tray_darwin.go / tray_windows.go
|
||
hotkey_darwin.go / hotkey_windows.go
|
||
notify_darwin.go / notify_windows.go
|
||
download_darwin.go / download_windows.go
|
||
secrets_darwin.go # Keychain
|
||
secrets_windows.go # DPAPI / Credential Manager
|
||
frontend/ # 薄入口 + Platform desktop 实现,UI import 自 web
|
||
```
|
||
|
||
**铁律**:①平台代码全用 build tag,同 monorepo 同分支,**绝不开 windows-branch**;②Windows(Parallels Win11 ARM VM)只 `git pull`、**从不 commit**,Windows-only 操作封进 `just` target;③ARM Win11 跑 x64 emulator 性能 -20–40%,**不能跑资源 / 性能基准**,D6 借真 x64 PC 锁数字;④D6 后 CI 防退化。
|
||
|
||
---
|
||
|
||
## 6. 安全审查
|
||
|
||
| 面 | 风险 | 缓解 |
|
||
|---|---|---|
|
||
| **token 存储** | 明文落盘即被盗即接管 | macOS Keychain / Windows DPAPI;不写明文文件 / localStorage |
|
||
| **OAuth 回调** | scheme 劫持 / CSRF | 用 loopback(无 scheme 抢注面);强制 PKCE S256 + state 校验;token 交换走 HTTPS 直连 Casdoor |
|
||
| **剪贴板隐私** | 自动上传密码 / 验证码 | 自写 ClipboardSource 做敏感类型过滤(§4 D4);默认策略待定;提供暂停 + 排除 |
|
||
| **更新** | 无自更新 = 不引入更新通道劫持面 | 仅版本检查提示 |
|
||
| **无签名分发** | 用户绕过 Gatekeeper 习惯被培养、易被仿冒 | 自用 / 小范围可接受;公开分发前应申请 Developer 证书签名公证(见 RESEARCH.md) |
|
||
| **WebView ↔ Go 桥** | 暴露的 Go 方法被 XSS 滥用 | bind 方法最小化 + 参数校验;只加载本地 embed 资源 + 固定 API origin |
|
||
|
||
---
|
||
|
||
## 7. 验证与测试策略
|
||
|
||
- **每里程碑**:`just desktop-build-mac` 冷 / 增量、`desktop-build-win` cross-compile 双绿,再真机手测(各 D 的“验收”)。
|
||
- **真机实测清单(sweep 标注,资料无法定论)**:① macOS 26 剪贴板隐私授权对轮询的行为(D4);② ad-hoc 签名能否让 `UNUserNotificationCenter` 本机通知工作(D3);③ hotkey v0.4.1 在 Wails 主线程下稳定收事件(D5);④ Casdoor loopback 任意端口端到端不报 `redirect_uri_mismatch`(D1)。
|
||
- **资源基准**:idle 内存在真机测(mac 本机、Windows 借真 x64 PC,ARM VM 不算数),D6 锁定。
|
||
|
||
---
|
||
|
||
## 8. 风险登记表
|
||
|
||
| # | 风险 | 影响 | 缓解 / 状态 |
|
||
|---|---|---|---|
|
||
| R1 | OAuth 后端协调 | D1 | **已降级**:sweep 给出最小路径(多值 audience 一处改动 + Casdoor ≥ v3.3),不再是未知阻塞 |
|
||
| R2 | 前端复用污染 web 构建 | 维护 / 分叉 | §2.1 A2 隔离 + Platform 接口 + CI 校验 |
|
||
| R3 | **Wails v2 缺托盘 + 单窗口** | D2 / D5 | 决策 C:推迟 v2/v3 至 D2/D5 前;v2 走第三方 systray + 自写 NSWindowDelegate,或升 v3 |
|
||
| R4 | 剪贴板回环(自写触发自传) | 上传风暴 | ClipboardSource 层来源标记 + hash 去重 + debounce |
|
||
| R5 | **macOS 26 剪贴板隐私授权** | D4 自动同步可能被弹窗打断 | 真机实测 changeCount 轮询是否触发;必要时改用 detect API |
|
||
| R6 | **无证书 → D3 通知 / Gatekeeper** | D3 通知可能不工作、分发受阻 | ad-hoc 签名 + 真机实测;不行则通知降级 / 推迟;公开分发前补证书 |
|
||
| R7 | ARM VM 测不了性能 / 真签名 | 数字不可信 | D6 借真 x64 PC |
|
||
|
||
---
|
||
|
||
## 9. 执行顺序与 v2/v3 决策闸
|
||
|
||
**顺序(决策 C)**:
|
||
|
||
1. **R1 后端协调**(非代码,可并行起草)+ Casdoor 建桌面 client。
|
||
2. **D1**(前端复用 + loopback OAuth)——框架无关,最高价值。
|
||
3. **D3**(SSE 常驻 + 通知 + 落盘)+ **D4**(剪贴板核心价值)——框架无关。
|
||
4. **⟵ v2/v3 决策闸**:动 D2 / D5 前,正式评估三选一:
|
||
- **A) 留 v2**:稳定、文档全;D2 托盘 + D5 多窗靠第三方 / 自写 ObjC,技术债重。
|
||
- **B) 升 v3-alpha**:原生 systray + 多窗口 + ActivationPolicy;但 alpha 有风险、文档薄、需重做 D0 脚手架与 D1–D4 的少量框架接缝。
|
||
- **C) 混合**:v2 上把 D2/D5 做到“可用即可”(不追求完美失焦自隐等),待 v3 稳定再迁移。
|
||
5. **D2**(托盘 + 生命周期)→ **D5**(热键 + Quick Send)。
|
||
6. **D6**(打包:当前无证书仅 `.app`;CI)。
|
||
|
||
**判据**:若 menubar 常驻 + Quick Send 浮窗被确认为产品核心体验、且 v3-alpha 在评估时已足够稳,倾向 B;否则 A/C 先交付。
|
||
|
||
---
|
||
|
||
## 10. 当前执行计划:桌面 app(A2 复用 + 菜单栏 + 剪贴板 + 设置,2026-06-13 定)
|
||
|
||
> 用户要求:实现 web 全部功能、复用大部分界面与设计逻辑(A2)+ 剪贴板同步;**无活动窗口时从 dock 隐藏 + 有 menu bar item**;**可能需独立的桌面设置界面**。
|
||
|
||
### 10.0 架构决策:留 v2(单窗菜单栏)
|
||
|
||
本需求是单窗口形态(一个主窗 + 菜单栏,无 Quick Send 多窗),v2 可达:菜单栏 `fyne.io/systray`、dock 动态隐藏 purego objc `setActivationPolicy`、关闭即隐藏 v2 `OnBeforeClose`。复用 D1 全部成果,不冒 v3-alpha 迁移;Quick Send 多窗若将来要,再议 v3。
|
||
|
||
### 10.1 A2 前端复用(地基)
|
||
|
||
Wails 构建并嵌入 web/ 的 React 应用,加薄平台 seam(web 默认行为不变):
|
||
|
||
- **API base**:`net/api.ts` 用 `API_BASE = import.meta.env.VITE_CDROP_API_BASE ?? ""` 前缀 fetch 的 URL;web 留空(相对、不变),desktop 构建设 `https://drop.commilitia.net`(WebView 内相对 `/api` 到不了后端)。
|
||
- **登录桥**:login 路由检测 desktop(Wails 注入 `window.runtime`)→ 调 Go `StartLogin` → Go 直连 Casdoor 换 token + 解 access_token JWT 取 sub/name → `EventsEmit {access_token, refresh_token, user}` → JS `setAuth`。即 **token 进 JS store**(A2 必需、本机可接受,放弃「token 只在 Go」)。
|
||
- **refresh**:desktop 的 `refreshTokens` 走 Go `Flow.Refresh`(绕开 `/api/auth/refresh` 后端代理)→ emit 新 token。
|
||
- **设备名**:desktop 取主机名(`os.Hostname`)填 `selfDeviceName`。
|
||
- **构建接线**:desktop/frontend 引用 web(npm workspace / `file:` 或 vite 指 `web/src`),保留 wailsjs 绑定;以能 `wails build` 为准、方式待落地时定。
|
||
|
||
### 10.2 剪贴板同步(D4)
|
||
|
||
- **上行**:darwin native Source(NSPasteboard changeCount 轮询,purego,clipprobe 已证可跑)→ Monitor(去重 / 回环防护;`Sensitive` 过滤 best-effort、非主防线)→ `api.PutClipboard`。
|
||
- **下行**:Go 持 SSE `/api/hub/events` → `clipboard:update` → `ApplyRemote` → `Source.Write`(loop-guard 防自传)。
|
||
- **安全**:服务端 5 min TTL 已上线兜底(密码即使上传,5 分钟即逝)。
|
||
- **开关**:受桌面设置「自动同步」控制(建议默认开 + 可关)。
|
||
|
||
### 10.3 菜单栏 + dock 隐藏(D2,单窗)
|
||
|
||
- **菜单栏**:`fyne.io/systray`,菜单项:显示主窗 / 设置 / 退出。
|
||
- **dock 动态**:purego objc `NSApp setActivationPolicy:`——窗口显示 = regular(有 dock);隐藏 = accessory(无 dock、仅菜单栏)。
|
||
- **生命周期**:v2 `OnBeforeClose` → `WindowHide` + accessory;菜单栏「显示主窗」→ `WindowShow` + regular;「退出」才真正退出(触发 `/api/me/disconnect`)。
|
||
|
||
### 10.4 桌面独立设置界面
|
||
|
||
- **桌面专属设置**(web 无):剪贴板自动同步开关、开机自启、设备名、菜单栏 / dock 行为、(TTL 只读展示)。
|
||
- **形态**:desktop-only 路由 / 页,复用 web 设计系统(Mantine + tokens + Field / Switch / Panel 组件)保持视觉一致;由菜单栏「设置」打开。
|
||
- **存储**:Go 侧本地配置文件(`~/Library/Application Support/cdrop/config.json`)+ 绑定方法读写;前端经 Wails binding 读写。
|
||
|
||
### 10.5 执行顺序
|
||
|
||
A2 复用(地基,先让真 web UI 在桌面跑起来 + 登录走 Go 桥)→ 菜单栏 + dock 隐藏 → 剪贴板同步 → 桌面设置。每步 `wails build` 可验;登录 / 同步 / 菜单栏需真机测。
|
||
|
||
### 10.6 实现状态(2026-06-13 实现,本地全绿)
|
||
|
||
`go build ./... / go test -race ./... / web tsc / wails build` 全部通过,产出 `build/bin/cdrop-desktop.app`(内嵌真 web 应用,8 个绑定就绪)。
|
||
|
||
落地与对 §10.1–10.4 的关键决策:
|
||
|
||
1. **同源反向代理替代 API_BASE 跨域**(§10.1 偏差):`platform.NewAPIProxy` 在 Wails `AssetServer.Handler` 反代 `/api/*` → backend,前端相对 URL 原样不变。理由:后端无 CORS,跨域方案要加 CORS + 重新部署(不可达);代理同源、零 CORS、零后端改动。SSE 流式已核实可行并单测(darwin `URLSchemeTaskDidReceiveData` 逐 write 推流 + `FlushInterval=-1`;`proxy_test.go` TestProxyStreamsSSE)。
|
||
2. **登录桥 token 入 JS store**:`StartLogin` 解码 JWT(`UserFromToken`,镜像后端 `extractUser`:id_token 优先、preferred_username→name→email→sub、avatar→picture)发 `oauth:success{access_token,refresh_token,user}`;`Refresh` 走 public-client(不带 secret);`DeviceName`=主机名跳过 /setup。web `net/desktop.ts` 全程 `isDesktop()` 门控,浏览器零影响。
|
||
3. **菜单栏 + dock:自写 CGO/ObjC `NSStatusBar` 桥,不碰 app delegate**(§10.3 落地):核实 energye/systray 与 systray-on-wails 的 external-loop 路径仍会 `setDelegate:owner` 抢占 Wails 的 NSApplication delegate → 破坏窗口/运行时,**全部排除**。`statusbar_darwin.m` 在 main queue 建 `NSStatusItem` + 菜单(显示主窗 / 设置 / 退出),dock 经 `setActivationPolicy` 动态切(regular↔accessory);`OnBeforeClose` 隐藏不退、菜单「退出」置 `quitting` 真退。CGO/ObjC 而非 purego:clang 编译期校验 selector,盲写更稳。
|
||
4. **剪贴板 JS-centric**(§10.2 落地):Go 只做原生读写(`clipboard_darwin.m` NSPasteboard 轮询 changeCount + 读写)+ 发 `clipboard:native` 事件 / `ApplyRemoteClipboard` 绑定;上行复用 web `uploadClipboard`、下行复用 `applyRemoteClipboard`,**单 SSE**(JS 的)。回环防护用既有 `Monitor`(已加 mutex 并发安全,-race 通过)。窗口隐藏后 WebView 仍存活 → menubar 模式下行有效。Sensitive(ConcealedType 等)best-effort 过滤 + 服务端 5min TTL 兜底。
|
||
5. **设置 + 本地配置**(§10.4 落地):`appsettings.go` 读写 `~/Library/Application Support/cdrop/config.json`;`launchagent_darwin.go` 用 LaunchAgent plist 实现开机自启(纯文件 I/O,已单测)。web `features/desktop/DesktopSettings.tsx`(复用 Panel + Mantine Switch)挂在 /settings、`isDesktop()` 门控;菜单栏「设置」→ `menu:settings` 事件 → 路由跳转。
|
||
|
||
新增/改动(desktop/platform):proxy.go、identity.go、statusbar{.go,_darwin.go,_darwin.m,_other.go}、clipboard_darwin.{go,m}、clipboard_source_other.go、appsettings.go、launchagent_{darwin,other}.go(+ proxy/identity/appsettings/launchagent 4 测试);clipboard.go 加 mutex;oauth.go 加 IDToken 字段。desktop:app.go(登录桥 / 生命周期 / 剪贴板 / 设置绑定)、main.go(proxy + OnBeforeClose)、wails.json(frontend 指向 web 构建)。web:net/desktop.ts + DesktopSettings.tsx 新增;login.tsx / auth.ts / clipboard.ts / __root.tsx / settings.tsx / main.tsx 加 guarded 桌面分支。
|
||
|
||
**待真机 + 鉴权验证(本环境无 GUI、无法登录,以下不可本地达成):**
|
||
|
||
- [ ] 登录 e2e:loopback PKCE → token 入 store → 进首页(含设备名自动取主机名)
|
||
- [ ] refresh:API 401 → Go public-client 刷新成功
|
||
- [ ] 菜单栏 item 出现 + 三项菜单可点
|
||
- [ ] dock 动态隐藏:关窗 → 仅菜单栏;菜单「显示主窗口」→ 窗口与 dock 复现
|
||
- [ ] 剪贴板跨设备:上行(复制即传)/ 下行(远端更新写回本机)/ 回环不抖
|
||
- [ ] 开机自启:勾选后写入 LaunchAgent、重登录系统自动起
|
||
|
||
**已知后续:** 桌面设置 UI 已接入 `t()`(zh-CN / zh-TW / en-US 三语);菜单栏标签仍 Go 侧硬编码简体(需启动期把 locale 传入 Go,待后续);`Greet`/`LoggedIn` 为脚手架残留绑定可清理;Windows/Linux 的原生 Source / 托盘 / 自启目前为 stub。
|
||
|
||
### 10.7 用户首轮真机反馈修复(2026-06-13,本地全绿)
|
||
|
||
真机实测「功能基本正常」,三处调整已修:
|
||
|
||
1. **登录 session 本地持久化(Go 文件 + 启动注入;已真机跑通)**:
|
||
|
||
**两个独立根因,第二个掩盖了第一个、害得多轮误判:**
|
||
|
||
- **根因 A(真正的持久化障碍)**:Wails 从 `wails://` 自定义 scheme 提供页面,**WKWebView 对 custom-scheme origin 不持久化 localStorage / sessionStorage**,故任何「前端存储」跨重启即丢。→ 持久化必须交给 Go。
|
||
- **根因 B(构建管线,长期误导)**:`wails build` 的 `frontend:build` 在本仓库结构下**实际不重建 `web/`**(无 vite 输出、dist 不更新),导致 `.app` 嵌入的是**数小时前的旧 JS**。所以连续多轮「仍无效」其实是在测**根本不含本轮代码的 stale bundle**——save/load/注入逻辑一直是对的,只是 JS 从未真正进过包。已用源码 marker 定位证实(marker 进不了 dist)。**修法:先手动 `npm run build` 再 `wails build -s`(跳过 wails 自带前端步),Justfile `desktop-build-mac` 已改成两步。**
|
||
|
||
**终方案(最简、已真机验证落首页)**:① Go 持久化——`platform/session.go` 把 `LoginResult` 写 `~/Library/Application Support/cdrop/session.json`(0600),`StartLogin`/`Refresh` 即存、登出 `ClearSession` 删;② **启动注入**——`platform/sessioninject.go` 的 `AssetServer.Middleware` 在 index.html `</head>` 前注入 `<script>window.__CDROP_BOOT__={session,device_name}</script>`(启动读文件、`json.Marshal` 默认 HTML 转义防 `</script>` 逃逸、只改文档请求、assets 与 /api SSE 直通、加 `no-store`+删条件头防 304/缓存);③ 内联脚本在 deferred module bundle 前同步执行,**`store/helpers.ts` 初始化即同步读 `window.__CDROP_BOOT__` 水合 session + 设备名**——无 IPC、无异步 boot、不依赖 window.go。设备名同样不能靠 WebView 存储:`DesktopConfig` 加 `DeviceName` + `ResolveDeviceName()`(config 优先否则 hostname),重命名经 `SetDeviceName` 绑定持久化,devices slice 初始化 `readInjectedDeviceName() ?? readSelfDevice()`。
|
||
|
||
真机诊断证据:`[js] render: injected=true user=Commilitia device=…` + 随后加载首页组件(DeviceStrip / clipboard / SSE)。曾试过的 localStorage(根因 A 挡)、IPC 绑定 / 异步 boot(为绕「以为是缓存」加的冗余)均已移除——它们「无效」只因根因 B 让它们从没真正跑过。Keychain 加固仍留后续。
|
||
2. **常用快捷键**:`app.go buildMenu()` 装应用菜单——`menu.EditMenu()` 给 ⌘C/⌘V/⌘X/⌘A/⌘Z,自写「窗口」菜单 ⌘W=隐藏 / ⌘M=最小化。**关键坑**:Wails 把窗口关闭按钮与 ⌘Q **都**经同一 `OnBeforeClose("Q")` 通道(`windowShouldClose` 与 `applicationShouldTerminate` 都 `processMessage("Q")`),不可区分 → 用 role `AppMenu()` 的 ⌘Q 会被 hide 逻辑吞掉永不退出。故自写应用菜单 + 自定义 ⌘Q 退出项(先置 `quitting` 再退),实现「关闭按钮 / ⌘W 隐藏,⌘Q / 菜单退出 真退」。
|
||
3. **菜单栏图标 + 设置崩溃**:状态项改用 SF Symbol `doc.on.clipboard`(template image,随明暗着色,旧系统回退文字)。崩溃根因:原生菜单动作回调在 AppKit 主线程同步调 Wails runtime(`WindowShow` / `EventsEmit`)→ 主 run loop 重入崩溃;修为状态项三个回调 `go` 异步派发(Wails 再安全调度回主线程)。
|
||
|
||
### 10.8 凭据安全策略(2026-06-13 确立,一致化)
|
||
|
||
**原则:`refresh_token`(长寿命密钥)是受保护对象,绝不暴露给 WebView/JS;`access_token`(短寿命)是工作凭据,是 WebView 唯一持有的凭据。** 此前不一致——Web 把 refresh_token 仅留内存(XSS 最小化),桌面却把整套 token(含 refresh)既写明文文件又灌进 JS(注入 + store)。统一为:
|
||
|
||
- **refresh_token 全平台都不进 WebView/JS**:Web 仅内存(页面生命周期)、不落盘,刷新走后端代理(持 client secret);桌面只在 **Go 进程内存 + 受限文件**,刷新走 **Go 内部**——`Refresh()` **无参**,Go 用自己的副本换新 access_token,绝不注入、不返回 JS。已在绑定边界核实:生成的 TS `SessionView` 无 `refresh_token` 字段、整个 wailsjs 生成层 `grep refresh_token` 零命中。
|
||
- **access_token 在 JS store**(API 调用必需),短寿命、由 refresh 续期;桌面随 boot 注入 + 落盘(offline-tolerant),401 时 Go 刷新。
|
||
- **静态存储(桌面)**:`session.json` 文件 0600、目录 0700(仅当前用户)。威胁模型:防其他本地用户 + 随手访问;**不防**同用户身份运行的恶意程序。**加固路线=macOS Keychain,推迟到 Developer-ID 正式签名后**——ad-hoc 签名每次构建都改签名,会导致每次重建都弹 Keychain 授权(且当前无法干净验证)。
|
||
- 实现:`platform.SessionView`(`LoginResult.View()` 投影去掉 refresh)用于登录 emit / 刷新返回 / 注入;`StartLogin` 存全量到文件、emit View;`Refresh()` 无参用 `a.token.RefreshToken`(startup 时从文件载入 Go 内存,且容忍 provider 不轮换 refresh);登出 `ClearSession` 删文件 + 清 Go 内存。
|
||
|
||
### 10.9 品牌资产接入 + 桌面打磨(2026-06-13,web 已部署 prod)
|
||
|
||
吉祥物品牌资产(源见记忆 `cdrop-brand-assets`)接入。web 与 desktop 共享同一前端,故多数改动两端通吃。
|
||
|
||
**Web(已部署 drop.commilitia.net):**
|
||
|
||
- **站点图标**:`web/public/` favicon-16/32/48/96(源 avatar-head)+ apple-touch-180 + PWA icon-192/512(源 app-icon 金底)+ `site.webmanifest`;`index.html` 替换 emoji 占位。Google 搜索要求 favicon 为 **48px 整数倍**,故必挂 48/96。
|
||
- **品牌字标** `ui/brand/Wordmark`:吉祥物 logo(`logo/logo-icon.png`→1024² 870KB)+ Fredoka 600 混排双色(浅 Ink/Amber、深 白/`#F8C637`,主题三态)。Fredoka 经 `gfonts.commilitia.net` 镜像、`--font-display`。用于 AuthShell 顶栏 / `__root` header。规范:**页面禁止纯文字呈现品牌**(须 logo 图或图+文字)。
|
||
- **登录两坑**:① 跳转帧「两个 logo」(顶栏 logo 与 header logo 重叠)→ 过渡屏 `oauth.callback`/`setup` 传 `hideBrand` 隐顶栏 logo;② 图片重载闪烁(embed.FS 无 modtime、`http.FileServer` 不发缓存头)→ `internal/webui/embed.go` 补 `Cache-Control`(`/assets/*` immutable、index `no-cache`、logo/favicon `max-age=604800`)+ index.html `preload`。
|
||
|
||
**Desktop:**
|
||
|
||
- **App 图标**:`build/appicon.png`←`app-icon/icon-1024.png`,构建期转 `iconfile.icns`。**bundle ID `com.wails.{{.Name}}` 占位致 LaunchServices 缓存陈旧(一直显默认图)→ 改 `net.commilitia.cdrop`**(与 launchAgentLabel 一致)+ `lsregister -f` 重注册。
|
||
- **菜单栏图标**:SF Symbol → **彩色品牌头像**(`platform/menubar-icon.png` avatar-head 256px,`go:embed`→NSImage,`template=NO`,尺寸取 NSStatusBar `thickness`)。
|
||
- **菜单栏交互(未来 Windows 托盘须对齐)**:**左键单击→打开主窗口;右键/⌃单击→菜单**。不挂 `item.menu`(否则 mousedown 即弹菜单),改 `item.button` action + `sendActionOn:LeftMouseUp|RightMouseUp` 手动路由。
|
||
|
||
**待办**:Keychain 加固仍待签名后;菜单栏标签仍 Go 侧硬编码简体(locale 注入待办)。下一步:Windows 落地,见 §11。
|
||
|
||
## 11. Windows 端落地(2026-06-14,RDP 实测通过)
|
||
|
||
macOS 优先做完后接 Windows。业务逻辑全跨平台纯 Go,平台差异用 build tag 隔离(`_windows.go` / `_darwin.go` / `_other.go`)。
|
||
|
||
### 11.1 交叉编译:macOS 原生即可,无需 mingw
|
||
|
||
Windows 侧无 cgo——darwin 的 NSStatusBar/NSPasteboard CGO 走 build tag 排除,托盘/剪贴板/自启均纯 syscall,WebView2 是纯 Go(`go-webview2`)。`just desktop-build-win` = 手动构建前端 + `wails build -s -platform windows/amd64`,从 macOS 直接出 `.exe`。`os.UserConfigDir()` 跨平台 → session/config 落 `%AppData%\cdrop\`。
|
||
|
||
### 11.2 核心坑:Wails 的 Windows custom-scheme AssetServer 不支持流式
|
||
|
||
**根因**:Wails 的 Windows AssetServer 把 `http.Handler` 的响应**攒到 handler 返回才交给 WebView2**。长连 SSE(`/api/hub/events`,handler 永不返回)→ WebView2 永远拿不到响应 → fetch 一直 pending(连 `resp.ok` 都不触发)→ 设备列表加载不出、状态卡「连接中」。短请求(剪贴板 fetch 等 handler 返回即送达)正常,故迷惑性强。两个同名进程互相 kick → 连接频繁**结束** → 每次结束才冲刷,是「开两个就好」的假象。
|
||
|
||
**排错历程**:先加 2KB SSE padding(标准抗缓冲技术)→ 无效,**反证是 buffer-until-return 而非流式缓冲**(padding 已撤)。
|
||
|
||
**修法**:`platform/localproxy.go` 仅 Windows 在 `127.0.0.1:0` 起**真 HTTP 代理**处理 `/api`——WebView2 的正常网络栈对 loopback 源真流式。前端经注入的 `api_base` 前缀 `/api`(`net/api.ts`)。CORS 回显 `Access-Control-Request-Headers`(覆盖 Authorization / If-None-Match / X-Device-Type,否则剪贴板刷新的 `If-None-Match` 预检失败=「Failed to fetch」)+ Private-Network 应答头。macOS 维持 custom scheme(WKWebView 流式正常,零改动)。
|
||
|
||
### 11.3 原生能力(`_windows.go`)
|
||
|
||
- **托盘** `statusbar_windows.go`:`fyne.io/systray`(goroutine + LockOSThread 跑自有消息循环),品牌 `tray-icon.ico`,`SetOnTapped` 左键开窗 / 右键默认菜单(显示主窗·设置·退出)。关窗→`WindowHide`(任务栏按钮随之消失),托盘常驻。
|
||
- **剪贴板** `clipboard_windows.go`:`golang.design/x/clipboard`(`GetClipboardSequenceNumber` 轮询,纯 syscall),上下行经既有 `Monitor`/`Run`。敏感内容过滤暂缺(`Sensitive` 恒 false,靠服务端 5min TTL 兜底;`EnumClipboardFormats` 检查待后续)。
|
||
- **开机自启** `launchagent_windows.go`:注册表 `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`(per-user 免提权),值为带引号的 exe 路径(尾带 `--hidden`,见 §11.5)。
|
||
|
||
### 11.4 跨端打磨
|
||
|
||
- **设备类型**:客户端发 `X-Device-Type`(桌面按 `runtime.GOOS` 注入 macos/windows),后端中间件白名单登记(替代写死 `browser`),前端 i18n 本地化展示(浏览器 / macOS 客户端 / Windows 客户端…)。
|
||
- **设备名持久化**修复:`SaveSettings` 合并保留 `DeviceName`(设置页 `DesktopConfig` 无 device_name 字段,原先存设置会用空值覆盖)。
|
||
- **统一软件名** `Commilitia Drop Desktop`(`.app` / `.exe`);品牌 `.icns`(macOS)/ `.ico`(Windows,无 ImageMagick 时用一次性 Go 工具封 PNG-in-ICO);bundle id `net.commilitia.cdrop`。
|
||
- **Cmd+,(Ctrl+,)→ 设置**:`main.tsx` 全局 keydown,桌面与浏览器通用。
|
||
|
||
### 11.5 自启静默驻留(autostart → 不弹窗,直接驻留菜单栏 / 托盘)
|
||
|
||
自启拉起的进程不该抢焦点弹窗——用户没主动开 app。机制:自启项在启动命令尾部追加 `--hidden`,进程 `launchedHidden()`(扫 `os.Args`)命中即 `options.StartHidden`(无窗口起);macOS 再 `SetDockVisible(false)` 降 accessory(隐 dock,仅留菜单栏项)。前端照常加载 → 剪贴板同步后台照跑。手动启动(Finder / 任务栏 / 终端)不带标志 → 正常显示窗口。
|
||
|
||
- macOS:LaunchAgent `open <bundle> --args --hidden`(裸 dev 二进制 `exe --hidden`)。
|
||
- Windows:注册表值 `"exe" --hidden`。
|
||
- **自启项自愈**:`startup` 时若自启已开,重写一次条目——旧版本(无 `--hidden`)或迁移过位置的 bundle 自动刷新到当前路径+标志,无需手动重勾。
|
||
|
||
**仍待办**:Windows 剪贴板敏感过滤;菜单栏/托盘标签 i18n(仍硬编码简体);Keychain(待 Developer-ID 签名);Windows 代码签名 / SmartScreen。
|