diff --git a/.env.example b/.env.example index 3f651a7..5da1ba5 100644 --- a/.env.example +++ b/.env.example @@ -15,8 +15,8 @@ CDROP_DEV_TOKEN=replace-with-32-byte-random-base64 # broker 公开源(浏览器全局 SSO 登录跳转 / 原生客户端设备授权流的目标)。留空则 # /api/auth/login 与原生登录不可用。 # CDROP_BROKER_PUBLIC_URL=https://sso.your-domain.example -# 本应用在 broker apps 注册表里的 key(默认 cdrop)。 -# CDROP_BROKER_APP=cdrop +# 本应用在 broker apps 注册表里的 key(默认 commilitia-drop)。 +# CDROP_BROKER_APP=commilitia-drop # 部署公开源(拼扫码 QR 链接 / CSRF Origin 校验 / Web Push 默认联系标识)。 # CDROP_PUBLIC_URL=https://drop.your-domain.example # CDROP_TURN_URL=stun:your-stun.example:3478 @@ -48,3 +48,13 @@ CDROP_DEVICE_TTL_HOURS=196 # 申请:dash.cloudflare.com → Realtime → TURN → "Create TURN App",拿到 Key ID 与 API Token。 # CDROP_CF_TURN_KEY_ID= # CDROP_CF_TURN_API_TOKEN= + +# Apple 客户端签名与公证(仅本机构建;实际值放 gitignore 的 .env)。iPhone / iPad 必须把 +# CDROP_APPLE_DEVELOPMENT_IDENTITY 固定为既有 Apple Development 证书的 SHA-1,禁止自动 +# 新建或切换证书。macOS 正式分发复用钥匙串中的 Developer ID Application 身份。 +# CDROP_TEAM_ID= +# CDROP_ASC_KEY_PATH=/absolute/path/AuthKey_XXXXXXXXXX.p8 +# CDROP_ASC_KEY_ID= +# CDROP_ASC_ISSUER_ID= +# CDROP_APPLE_DEVELOPMENT_IDENTITY= +# CDROP_DEVID_IDENTITY=Developer ID Application: Your Name (TEAMID1234) diff --git a/Justfile b/Justfile index 5d56798..8221c8a 100644 --- a/Justfile +++ b/Justfile @@ -45,23 +45,47 @@ wails := `go env GOPATH` + "/bin/wails" desktop-dev: cd desktop && {{wails}} dev -# 构建 macOS universal .app(含 darwin/arm64 + darwin/amd64)。 -# 不加 -clean,让 mac/win 产物在 build/bin/ 共存;清理走 desktop-clean。 +# 构建 macOS Apple Silicon .app(darwin/arm64;不支持 Intel)。 +# 每次清理 build/bin:带空格的 macOS .app 若残留在 Go 模块内,会被后续跨平台 +# 绑定生成误判为包路径;同一输出槽也只保留本次平台的一份客户端。 # # ⚠️ 先手动构建前端再 `wails build -s`(跳过 wails 自带前端步骤):wails build 的 # frontend:build 在本仓库结构下不可靠(不实际重建 web/ → 嵌入旧 JS)。显式构建 # web → desktop/frontend/dist,再用 -s 让 go:embed 嵌入这份新 dist。 desktop-build-mac: cd web && npm run build -- --outDir ../desktop/frontend/dist --emptyOutDir - cd desktop && {{wails}} build -s -platform darwin/universal + cd desktop && {{wails}} build -clean -s -platform darwin/arm64 + +# 正式 macOS 分发:复用钥匙串中的 Developer ID Application 证书,对 ARM64 Wails app +# 启用 hardened runtime + 时间戳签名,先公证并装订 app,再生成、签名、公证和装订 DMG。 +# 公证使用与 iOS provisioning 共用的 ASC API Key;所有身份和密钥路径从 .env 读取。 +desktop-dist-mac: desktop-build-mac + [ -n "$CDROP_DEVID_IDENTITY" ] && [ -n "$CDROP_ASC_KEY_PATH" ] && [ -n "$CDROP_ASC_KEY_ID" ] && [ -n "$CDROP_ASC_ISSUER_ID" ] || { echo "缺 .env(CDROP_DEVID_IDENTITY / CDROP_ASC_KEY_PATH / CDROP_ASC_KEY_ID / CDROP_ASC_ISSUER_ID)"; exit 1; } + [ -f "$CDROP_ASC_KEY_PATH" ] + security find-identity -v -p codesigning | rg -F "$CDROP_DEVID_IDENTITY" + codesign --force --deep --options runtime --timestamp --sign "$CDROP_DEVID_IDENTITY" 'desktop/build/bin/Commilitia Drop.app' + codesign --verify --deep --strict --verbose=2 'desktop/build/bin/Commilitia Drop.app' + rm -f 'desktop/build/bin/Commilitia Drop.app.zip' 'desktop/build/bin/Commilitia Drop.dmg' + ditto -c -k --keepParent 'desktop/build/bin/Commilitia Drop.app' 'desktop/build/bin/Commilitia Drop.app.zip' + xcrun notarytool submit 'desktop/build/bin/Commilitia Drop.app.zip' --key "$CDROP_ASC_KEY_PATH" --key-id "$CDROP_ASC_KEY_ID" --issuer "$CDROP_ASC_ISSUER_ID" --wait + xcrun stapler staple 'desktop/build/bin/Commilitia Drop.app' + xcrun stapler validate 'desktop/build/bin/Commilitia Drop.app' + bash desktop/scripts/make-dmg.sh 'desktop/build/bin/Commilitia Drop.app' 'desktop/build/bin/Commilitia Drop.dmg' + codesign --force --timestamp --sign "$CDROP_DEVID_IDENTITY" 'desktop/build/bin/Commilitia Drop.dmg' + xcrun notarytool submit 'desktop/build/bin/Commilitia Drop.dmg' --key "$CDROP_ASC_KEY_PATH" --key-id "$CDROP_ASC_KEY_ID" --issuer "$CDROP_ASC_ISSUER_ID" --wait + xcrun stapler staple 'desktop/build/bin/Commilitia Drop.dmg' + xcrun stapler validate 'desktop/build/bin/Commilitia Drop.dmg' + spctl --assess --type execute --verbose=2 'desktop/build/bin/Commilitia Drop.app' + spctl --assess --type open --context context:primary-signature --verbose=2 'desktop/build/bin/Commilitia Drop.dmg' + rm -f 'desktop/build/bin/Commilitia Drop.app.zip' # 交叉编译 Windows amd64 .exe。macOS 原生即可——Windows 侧无 cgo(darwin 的 # NSStatusBar/NSPasteboard CGO 走 build tag 排除,WebView2 是纯 Go),不需 mingw。 # 与 mac 同:先手动构建前端再 -s(wails 自带 frontend:build 在本仓库不可靠)。 -# 产物 build/bin/desktop.exe;真测试经 RDP 到 Windows 机器跑。 +# 产物 build/bin/Commilitia Drop.exe;真测试经 RDP 到 Windows 机器跑。 desktop-build-win: cd web && npm run build -- --outDir ../desktop/frontend/dist --emptyOutDir - cd desktop && {{wails}} build -s -platform windows/amd64 + cd desktop && {{wails}} build -clean -s -platform windows/amd64 # 清空 build/bin/ 产物。 desktop-clean: @@ -106,9 +130,10 @@ mac-prep: cd ios/CDrop && xcodebuild -project CommilitiaDrop.xcodeproj -scheme CommilitiaDropMac -sdk macosx -derivedDataPath build/DD-mac -resolvePackageDependencies bash ios/CDrop/scripts/patch-macos-webrtc-headers.sh ios/CDrop/build/DD-mac -# macOS 原生客户端本地编译(ad-hoc 签名,无需 Apple 账号)。当前为脚手架(MacApp/ 导航骨架 + -# 菜单栏占位);引擎 / 复用视图 / 桌面功能随后续阶段并入。产物 build/DD-mac/Build/Products/Debug/ -# CommilitiaDropMac.app(本地打开自测)。 +# macOS 原生迁移候选的本地编译(ad-hoc 签名,无需 Apple 账号),仅供开发验证,不是正式 +# 客户端或分发入口。正式 macOS 客户端仍由 desktop-build-mac 构建;只有完成功能对等、三语、 +# 签名扩展与真机验收后才允许切换。候选产物位于 build/DD-mac/Build/Products/Debug/ +# CommilitiaDropMac.app。 mac-build: mac-prep cd ios/CDrop && xcodebuild -project CommilitiaDrop.xcodeproj -scheme CommilitiaDropMac -sdk macosx -destination 'platform=macOS' -derivedDataPath build/DD-mac -disableAutomaticPackageResolution build @@ -117,17 +142,17 @@ mac-build: mac-prep mac-test: mac-prep cd ios/CDrop && xcodebuild test -project CommilitiaDrop.xcodeproj -scheme CommilitiaDropMac -sdk macosx -destination 'platform=macOS' -derivedDataPath build/DD-mac -disableAutomaticPackageResolution -# 真机构建 + 装机(自动 provisioning,经 ASC API Key)。前置:付费 ADP;App ID 的 App Groups / -# Push 能力(门户建好或自动建);根目录 gitignore 的 .env 配齐:CDROP_TEAM_ID / CDROP_ASC_KEY_PATH -# (.p8 路径)/ CDROP_ASC_KEY_ID / CDROP_ASC_ISSUER_ID。-allowProvisioningUpdates 自动登记连接的 -# 设备 + 建 profile(绕过手动 profile 的设备选择坑)。模拟器构建不需这些(base ad-hoc)。原生数据面 -# 经 SPM 拉 libwebrtc,无 gomobile 预构建步骤(早先 ios-engine recipe 已随 pion-on-iOS 废弃移除)。 -# 用法:just ios-device <设备UDID>(UDID 见 just ios-devices)。 +# 真机构建 + 装机(自动 provisioning,经 ASC API Key)。必须把 +# CDROP_APPLE_DEVELOPMENT_IDENTITY 设为钥匙串内既有 Apple Development 证书的 SHA-1;构建前 +# 先验证该身份存在,再将其固定传给 xcodebuild,不允许以“自动签名”为由切换或新建证书。 +# -allowProvisioningUpdates 只用于登记设备、更新能力与 profile。模拟器构建不需这些。 +# 用法:just ios-device <设备UDID>(UDID 见 just ios-devices)。全程不使用 Xcode GUI。 ios-device udid: - [ -n "$CDROP_TEAM_ID" ] && [ -n "$CDROP_ASC_KEY_PATH" ] && [ -n "$CDROP_ASC_KEY_ID" ] && [ -n "$CDROP_ASC_ISSUER_ID" ] || { echo "缺 .env(CDROP_TEAM_ID / CDROP_ASC_KEY_PATH / CDROP_ASC_KEY_ID / CDROP_ASC_ISSUER_ID)——见 ios/CDrop/REALDEVICE.md"; exit 1; } + [ -n "$CDROP_TEAM_ID" ] && [ -n "$CDROP_ASC_KEY_PATH" ] && [ -n "$CDROP_ASC_KEY_ID" ] && [ -n "$CDROP_ASC_ISSUER_ID" ] && [ -n "$CDROP_APPLE_DEVELOPMENT_IDENTITY" ] || { echo "缺 .env(CDROP_TEAM_ID / CDROP_ASC_KEY_PATH / CDROP_ASC_KEY_ID / CDROP_ASC_ISSUER_ID / CDROP_APPLE_DEVELOPMENT_IDENTITY)——见 ios/CDrop/REALDEVICE.md"; exit 1; } + security find-identity -v -p codesigning | rg -F "$CDROP_APPLE_DEVELOPMENT_IDENTITY" just ios-fonts cd ios/CDrop && xcodegen generate - cd ios/CDrop && xcodebuild -project CommilitiaDrop.xcodeproj -scheme CommilitiaDrop -configuration Debug -destination "platform=iOS,id={{udid}}" -derivedDataPath build/DD-device -allowProvisioningUpdates -authenticationKeyPath "$CDROP_ASC_KEY_PATH" -authenticationKeyID "$CDROP_ASC_KEY_ID" -authenticationKeyIssuerID "$CDROP_ASC_ISSUER_ID" DEVELOPMENT_TEAM="$CDROP_TEAM_ID" CODE_SIGN_STYLE=Automatic CODE_SIGN_IDENTITY="Apple Development" build + cd ios/CDrop && xcodebuild -project CommilitiaDrop.xcodeproj -scheme CommilitiaDrop -configuration Debug -destination "platform=iOS,id={{udid}}" -derivedDataPath build/DD-device -allowProvisioningUpdates -authenticationKeyPath "$CDROP_ASC_KEY_PATH" -authenticationKeyID "$CDROP_ASC_KEY_ID" -authenticationKeyIssuerID "$CDROP_ASC_ISSUER_ID" DEVELOPMENT_TEAM="$CDROP_TEAM_ID" CODE_SIGN_STYLE=Automatic CODE_SIGN_IDENTITY="$CDROP_APPLE_DEVELOPMENT_IDENTITY" build xcrun devicectl device install app --device {{udid}} ios/CDrop/build/DD-device/Build/Products/Debug-iphoneos/CommilitiaDrop.app # ---- deploy plumbing ---- diff --git a/README.md b/README.md index a16eca2..2e3af11 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# cdrop — Commilitia Drop +# Commilitia Drop 跨 OS 剪贴板与文件传输服务。架构文档(`PROJECT_BRIEF.md` / `FRONTEND_DESIGN.md`)与 Changelog 位于 **`docs` 分支**: @@ -23,7 +23,8 @@ git submodule update --init --recursive MVP(M0–M13)已上线 prod(OIDC PKCE 接入自建 Casdoor,Cloudflare Realtime TURN over TLS)。其后进入阶段二,已落地: - **剪贴板同步**:单条覆写式云剪贴板(短 TTL 限暴露面)+ 文本消息一次性通道 -- **桌面客户端**:Wails v2(macOS universal `.app` + Windows `.exe`)—— 瘦客户端复用 web,业务全走后端 API;refresh_token 存系统密钥库(信封加密) +- **桌面客户端**: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),桌面客户端走原生系统通知(仅窗口非前台才弹)。文案按设备语言在服务端渲染(中简 / 中繁 / 英) @@ -31,6 +32,32 @@ MVP(M0–M13)已上线 prod(OIDC PKCE 接入自建 Casdoor,Cloudflare Re - **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 里程碑(历史记录): | 里程碑 | 内容 | 状态 | @@ -164,4 +191,3 @@ env 见 [`.env.example`](.env.example) / `compose.snippet.yaml`。要点: 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) - diff --git a/auth/docs/子会话方案.md b/auth/docs/子会话方案.md index 680381d..ee82f86 100644 --- a/auth/docs/子会话方案.md +++ b/auth/docs/子会话方案.md @@ -128,7 +128,7 @@ broker 侧 `role` 已被授权语义占用(Casdoor role、apps.json `required_ - **R2 幂等键** `(user, app, meta)` → `(user, app, meta, sub)`:查既有过滤加 `&& ex.Sub==req.Sub`,`lockMintIdent` 键改 `user|app|meta|sub`。老客户端 `sub=""`、既有会话 `Sub=""`→照旧匹配轮换,零影响。 ### 三、scope 缩减:复用现有 `tier`,broker 无新增 -你要的“子会话带缩减 scope(clipboard)”现机制已能给:`tier` 即 scope 后缀(token scope=`app:cdrop:`,cdrop 读末段)。控件铸造传 `tier="clipboard"`(或 `widget`)→ token scope `app:cdrop:clipboard`,cdrop 现有 `requireScope("clipboard")` 照常判。**无需新增 `scope` 参数**。 +你要的“子会话带缩减 scope(clipboard)”现机制已能给:`tier` 即 scope 后缀(token scope=`app:commilitia-drop:`,Commilitia Drop 读末段)。控件铸造传 `tier="clipboard"`(或 `widget`)→ token scope `app:commilitia-drop:clipboard`,Commilitia Drop 现有 `requireScope("clipboard")` 照常判。**无需新增 `scope` 参数**。 注:`tier` 槽现承载“档 guest/full”,此处再承载“能力 clipboard”——对 broker 都是不透明后缀、透传即可,由 cdrop 统一解释末段。若你要把“档”与“能力”分两维,须另开 scope 字段;单一消费者下复用 `tier` 最省,**推荐复用**。 ### 四、R1 归并:取 (b) broker 暴露 `sub`+cdrop 归并(非你倾向的 (a) broker 隐藏) @@ -195,7 +195,7 @@ DELETE /internal/sessions?user_id=&app=&meta= (X-Internal-Key, X-Broker-App) 1. **命名 `sub`(替 `role`)——确认。** 与 broker 的 `role`(Casdoor / `required_role` / verify 角色)解耦正确:`meta`=设备身份、`sub`=设备内会话判别。默认 `""`=主会话。 2. **R1 取 (b)(broker 暴露 `sub` + cdrop 按 meta 归并隐藏 `sub!=""`)——确认。** 你的“仅子存活则设备不该消失”反例是决定性的:cdrop 按 `meta` 归并能让“任一会话存活即在列”,而 (a) 朴素隐藏会误删只剩控件会话的设备。R1 当前唯一消费方是 cdrop,(b) 即满足“处处一台”。**不要 `?group=meta`**——待出现第二个 R1 消费方再按需加(同你“按需延后”原则)。 -3. **scope 缩减复用 `tier`(控件 `tier="clipboard"`)——确认。** token scope `app:cdrop:clipboard`,cdrop 现有 clipboard scope 门照判(clipboard 路由放行、其余路由 reject)。不开独立 scope 维——单一消费者下复用 `tier` 最省,“档 / 能力”都由 cdrop 统一解释 scope 末段。cdrop 侧据此校验:clipboard 档会话仅 `/api/clipboard` 放行。 +3. **scope 缩减复用 `tier`(控件 `tier="clipboard"`)——确认。** token scope `app:commilitia-drop:clipboard`,Commilitia Drop 现有 clipboard scope 门照判(clipboard 路由放行、其余路由 reject)。不开独立 scope 维——单一消费者下复用 `tier` 最省,“档 / 能力”都由 Commilitia Drop 统一解释 scope 末段。服务端据此校验:clipboard 档会话仅 `/api/clipboard` 放行。 4. **级联吊销 `DELETE …?user_id=&app=&meta=` → `{revoked:N}`——确认。** 与既有 `DELETE /internal/sessions/{sid}` 同风格,优于 `POST /revoke`。`user_id` 必填我方满足(移除设备时 cdrop 持 user + device_id=meta)。每条各自 `lockRefresh(sid)` 防级联中途复活——同意。 5. **`X-Auth-Sub` 注头——暂不需要,不必为此重建 caddy-custom。** cdrop 判能力只靠 scope 末段(clipboard)即够;设备列表归并用的 `sub` 来自 R1 内部响应、非边缘头。控件只打 `/api/clipboard`、不入 presence / 传输,故请求期无需区分 main / widget。**待将来确有需要再加**(与 X-Auth-Sub 接边缘那次一并)。 6. **`sub` 取值集:现仅 `""`(主)/ `"widget"`(控制中心 + 主屏小组件,tier=clipboard)。** 命名空间预留给未来扩展进程(如 `"share"` 分享扩展、`"siri"`)。**暂不要 sub 数护栏**——cdrop 自控铸造、正常恒 2 条;若日后扩展进程增多再加每 `(user,app,meta)` 上限。 diff --git a/cmd/cdropd/main.go b/cmd/cdropd/main.go index 31bcc73..7f5cd0c 100644 --- a/cmd/cdropd/main.go +++ b/cmd/cdropd/main.go @@ -32,7 +32,7 @@ func main() { slog.Error("config load failed", "err", err) os.Exit(1) } - slog.Info("cdropd booting", + slog.Info("Commilitia Drop server booting", "auth_mode", cfg.AuthMode, "listen", cfg.Listen, "db_path", cfg.DBPath, @@ -120,5 +120,5 @@ func main() { if err := srv.Shutdown(shutCtx); err != nil { slog.Error("http shutdown error", "err", err) } - slog.Info("cdropd stopped") + slog.Info("Commilitia Drop server stopped") } diff --git a/desktop/NATIVE-TRANSFER.md b/desktop/NATIVE-TRANSFER.md index ee59787..b67fe3c 100644 --- a/desktop/NATIVE-TRANSFER.md +++ b/desktop/NATIVE-TRANSFER.md @@ -1,4 +1,4 @@ -# cdrop 桌面原生数据面(Option A)设计 +# Commilitia Drop 桌面原生数据面(Option A)设计 > 目标:把桌面 **P2P 数据面**(WebRTC DataChannel 收发)从 WebView 的 JS 移到 Go 进程(`pion/webrtc`),**留在 Wails、不上 Tauri**。 > 关联:`desktop/PLAN.md`(桌面总计划)、`desktop/RESEARCH.md`(调研)、`web/src/features/transfer/{transfer,p2p,relay,incomingSink,source}.ts`、吞吐专项诊断(task #38)。 @@ -62,7 +62,7 @@ selectBackend() → isDesktop() ? goBridgeBackend : jsWebrtcBackend // web / i ``` Sender(sessionId, peerName, filePath, iceServers): pc ← pion.NewPeerConnection(SettingEngine{ rwnd 大; host 候选真 IP; iceServers }) - dc ← pc.CreateDataChannel("cdrop-file", ordered=true) + dc ← pc.CreateDataChannel("commilitia-drop-file", ordered=true) onOpen: send JSON {type:meta, name, size, sha256?} // 与 p2p.ts 同帧 for chunk in readFileByPath(filePath, CHUNK): // Go 直接磁盘读,无桥、无 base64 @@ -117,7 +117,7 @@ ICE creds:Go 拉 /api/calls/credentials(带 a.token),与 JS 同源 > 双实现的唯一契约是**线协议**。任何一项漂移都会让桌面↔web/iOS 互通断裂。建议抽出共享测试向量(meta/chunk/done/ack 的字节级样例)双侧回归。 - **信令**:`POST /api/hub/signal {to, payload:{type:"offer"|"answer"|"ice", sdp?, candidate?}}`;入站经 SSE。 -- **DataChannel**:名 `"cdrop-file"`,`ordered:true`。 +- **DataChannel**:名 `"commilitia-drop-file"`,`ordered:true`。 - **帧**:`meta` JSON `{type,name,size,sha256?}` / 二进制 chunk / `done` JSON / `ack` JSON `{type,bytes}`(接收端节流 200ms 回传累计已收字节,单调取大)。 - **状态机**:`/api/transfer/{initiate,p2p,done,fail,cancel,fallback}`;**完成由接收端 POST `/done`**(权威),发送端不重复。 - **中继回退语义**:JS 看门狗 30s 未 `connected` → `POST /fallback` 并对 Go `Cancel(session)`;Go 拆 pion,relay 由 JS 接管(数据面回到 JS relay,符合「relay 留 JS」)。 @@ -130,7 +130,7 @@ ICE creds:Go 拉 /api/calls/credentials(带 a.token),与 JS 同源 > > 实测证据:iOS 经 gomobile/pion 时 Go 的 raw BSD socket 不与 iOS Network framework 集成(WebKit/libwebrtc 才集成)——Mac→iOS 常连不上(~30s ICE 超时)、iOS→Mac 偶连上 host↔host 但 ~4.5MB 中途断流(`read/write on closed pipe`)、TURN CreatePermission 刷屏、时好时坏。接口 / 链路本地 / IPv4-only 过滤只去噪、治不了架构不兼容(且那些 iOS 定向过滤漏进桌面共享引擎致 IPv6 host 对被裁退中继、重大回归)。即下方 §137 标注的“LOW-MEDIUM 成熟度、无 pion+gomobile 生产先例”风险兑现。 > -> **U2 落地(现行)**:JS 路由 / 桥协议 / 线协议 / `IOS_NATIVE` 开关全不变,只把 Swift 侧原生引擎从 gomobile `EngineEngine` 换成 `ios/CDrop/Sources/Engine/LibWebRtcEngine.swift`(libwebrtc `RTCPeerConnection`,stasel/WebRTC M149 经 SPM、pin 精确版本 149.0.0)。该引擎是 `engine.go`/`session.go` 线协议的忠实 Swift 端口——DataChannel `cdrop-file`(ordered)、meta/分片(64KB)/done/ack 文本+二进制帧、16MB/4MB 水位背压、ack 追平完成、不冲突落盘,逐字节对齐桌面 pion 与 web JS 引擎,故 iOS↔桌面、iOS↔浏览器互通。桌面数据面仍走 pion(isDesktop 分支,不受影响)。libwebrtc 与 iOS 网络栈原生集成 + DcSCTP 默认 ~5MB rwnd(不受 WebKit 256KB 限),是吞吐与连通性的根治。 +> **U2 落地(现行)**:JS 路由 / 桥协议 / 线协议 / `IOS_NATIVE` 开关全不变,只把 Swift 侧原生引擎从 gomobile `EngineEngine` 换成 `ios/CDrop/Sources/Engine/LibWebRtcEngine.swift`(libwebrtc `RTCPeerConnection`,stasel/WebRTC M149 经 SPM、pin 精确版本 149.0.0)。该引擎是 `engine.go`/`session.go` 线协议的忠实 Swift 端口——DataChannel `commilitia-drop-file`(ordered)、meta/分片(64KB)/done/ack 文本+二进制帧、16MB/4MB 水位背压、ack 追平完成、不冲突落盘,逐字节对齐桌面 pion 与 web JS 引擎,故 iOS↔桌面、iOS↔浏览器互通。桌面数据面仍走 pion(isDesktop 分支,不受影响)。libwebrtc 与 iOS 网络栈原生集成 + DcSCTP 默认 ~5MB rwnd(不受 WebKit 256KB 限),是吞吐与连通性的根治。 > > **验证(2026-06-28)**:模拟器全 app(含 Share/控件扩展)编译过 + web typecheck/build 过;**环回端到端测试**(两个 LibWebRtcEngine 同进程交叉连、20MB 文件经完整线协议传输后逐字节完整性比对)✅ 过(`ios/CDrop/Tests/LibWebRtcEngineTests.swift`,`just ios-sim-build` 同款免签名构建)。真机 / 真网吞吐为剩余闸(须 deploy prod 让设备拿到 `IOS_NATIVE=true` 的引擎 + 真机装机,二者 Touch ID 门控)。 > diff --git a/desktop/PLAN.md b/desktop/PLAN.md index 97982fa..a409c3b 100644 --- a/desktop/PLAN.md +++ b/desktop/PLAN.md @@ -1,4 +1,4 @@ -# cdrop 桌面客户端实施计划 +# Commilitia Drop 桌面客户端实施计划 > Wails v2 · macOS 优先 + Windows 已落地 > 状态:**macOS 与 Windows 双端均已实现并 RDP/真机实测通过**——A2 前端复用 + 托盘/菜单栏 + 剪贴板双向同步 + 桌面设置 + session 持久化 + 开机自启(含自启静默驻留)+ 设备类型登记。macOS 见 §10,Windows 落地(含「custom-scheme 不支持流式」核心坑与 127.0.0.1 本地代理解法)见 §11。计划已据原生平台研究 sweep(`desktop/RESEARCH.md`)校正。 @@ -26,7 +26,9 @@ ## 0. 范围、非范围与本轮关键决策 -**范围**:常驻桌面客户端,补浏览器做不到的——后台常驻、剪贴板自动监听上传、文件落盘、系统通知、全局快捷键。平台 **macOS(Apple Silicon + Intel)优先**,**Windows(x64)保持兼容**(同源、同分支,平台差异用 Go build tag 隔离)。 +**范围**:常驻桌面客户端,补浏览器做不到的——后台常驻、剪贴板自动监听上传、文件落盘、 +系统通知、全局快捷键。平台 **macOS(仅 Apple Silicon)优先**,**Windows(x64)保持兼容** +(同源、同分支,平台差异用 Go build tag 隔离)。 **非范围(推后)**:Linux 打包、iOS / Android 原生、自更新(仅版本检查提示)、产品化 / 多租户、Admin、Symmetric NAT 的 mDNS 直连、本地内容缓存 / 离线。 @@ -109,7 +111,7 @@ sweep 把 D1 从“最高风险未知”变成“路径已明、含一处后端 ### 3.1 回调通道:loopback(已定,弃 scheme) -采用 **RFC 8252 loopback**:Go 起临时 `127.0.0.1:0`(必须 IPv4 字面量,不用 `localhost`)HTTP server 捕获 `code`。相对自定义 scheme `cdrop://` 的优势:免单实例锁、免 `Info.plist` / 注册表登记、免深链解析、跨平台代码一致。scheme 留作未来“浏览器点 cdrop 链接唤起桌面端”的另一用例。 +采用 **RFC 8252 loopback**:Go 起临时 `127.0.0.1:0`(必须 IPv4 字面量,不用 `localhost`)HTTP server 捕获 `code`。相对自定义 scheme `commilitia-drop://` 的优势:免单实例锁、免 `Info.plist` / 注册表登记、免深链解析、跨平台代码一致。scheme 留作未来“浏览器点 Commilitia Drop 链接唤起桌面端”的另一用例。 ### 3.2 token 交换放 Go 侧(已定) @@ -166,13 +168,20 @@ sweep 源码级确认:Casdoor 支持 public client(PKCE 时 `client_secret` - **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`(`lipo` 合并 arm64 + amd64)。**不公证、不 DMG、无 Developer ID**。为满足 D3 通知与稳定身份,做 **ad-hoc 签名** `codesign -s - --force --deep .app` + `Info.plist` 带 `CFBundleIdentifier`。分发靠手动(右键打开 / 去 quarantine)。 -- **entitlements(若 ad-hoc 也带)**:非沙箱只需 `com.apple.security.network.client`;purego dlopen 系统 framework **不需** `disable-library-validation`;Carbon 热键不需 entitlement。(不要列通知 / 剪贴板 / 热键 entitlement——过度声明。) +### D6 · 打包与分发(2026-07-31 已验证) +- **macOS(当前路径)**:`just desktop-dist-mac` 构建 Apple Silicon ARM64 Wails app,复用 + 既有 Developer ID Application 证书,以 hardened runtime + secure timestamp 签名;先提交 + app 公证并 staple,再生成含 `/Applications` 链接的 DMG,对 DMG 签名、单独公证并 staple。 + app 与 DMG 均已获 Apple `Accepted`,Gatekeeper 评估为 `Notarized Developer ID`。 +- **entitlements**:当前非沙箱瘦客户端不额外声明通知、剪贴板或热键 entitlement;purego + `dlopen` Apple 系统 framework 不需 `disable-library-validation`,Carbon 热键也不需要 + entitlement。后续引入第三方 dylib 或沙箱时重新评估。 - **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,不跑公证)。 +- **操作入口**:完整构建、签名、公证、安装和验收流程见 + `docs/client-build-install.md`;底层命令与设计依据见 `desktop/RESEARCH.md` D6 手册。 +- **CI**:后续 GitHub Actions 双 runner(macos-latest / windows-latest)跑构建 + 防回归; + macOS runner 导入 Developer ID 后跑公证,Windows 在获得 Authenticode 证书后加入签名。 --- @@ -296,7 +305,8 @@ A2 复用(地基,先让真 web UI 在桌面跑起来 + 登录走 Go 桥) ### 10.6 实现状态(2026-06-13 实现,本地全绿) -`go build ./... / go test -race ./... / web tsc / wails build` 全部通过,产出 `build/bin/cdrop-desktop.app`(内嵌真 web 应用,8 个绑定就绪)。 +`go build ./... / go test -race ./... / web tsc / wails build` 全部通过,产出 +`build/bin/Commilitia Drop.app`(内嵌真 web 应用,8 个绑定就绪)。 落地与对 §10.1–10.4 的关键决策: @@ -389,7 +399,9 @@ Windows 侧无 cgo——darwin 的 NSStatusBar/NSPasteboard CGO 走 build tag - **设备类型**:客户端发 `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`。 +- **统一软件名** `Commilitia Drop`(`.app` / `.exe`);品牌 `.icns`(macOS)/ `.ico` + (Windows,无 ImageMagick 时用一次性 Go 工具封 PNG-in-ICO);稳定内部 bundle id + `net.commilitia.cdrop`。 - **Cmd+,(Ctrl+,)→ 设置**:`main.tsx` 全局 keydown,桌面与浏览器通用。 ### 11.5 自启静默驻留(autostart → 不弹窗,直接驻留菜单栏 / 托盘) diff --git a/desktop/README.md b/desktop/README.md index 4d7bcd3..889c238 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -1,19 +1,29 @@ -# README +# Commilitia Drop(Wails) -## About +这是 Windows 与 macOS 当前唯一的正式桌面客户端。两个平台都复用仓库根目录 `web/` +的 React 前端;`frontend/src` 只保留为早期 Wails 桥接示例,不是可发布的第二套界面。 -This is the official Wails Vanilla-TS template. +正式构建必须从仓库根目录运行: -You can configure the project by editing `wails.json`. More information about the project settings can be found -here: https://wails.io/docs/reference/project-config +```sh +just desktop-build-win +just desktop-build-mac +``` -## Live Development +macOS 正式分发使用既有 Developer ID Application 证书和 ASC API Key: -To run in live development mode, run `wails dev` in the project directory. This will run a Vite development -server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser -and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect -to this in your browser, and you can call your Go code from devtools. +```sh +just desktop-dist-mac +``` -## Building +该任务会依次签名、公证并装订 `.app`,再生成、签名、公证和装订 +`desktop/build/bin/Commilitia Drop.dmg`。Windows 当前输出未做 Authenticode 签名。 -To build a redistributable, production mode package, use `wails build`. +macOS 构建仅支持 Apple Silicon(ARM64)。构建任务会先生成最新 Web 产物,再以 +`wails build -clean` 打包,避免将陈旧前端或另一平台的旧产物混入发布目录。 + +`ios/CDrop` 中的原生 macOS target 仍是迁移候选,只供本地验证;在功能对等、三语界面、 +签名扩展和真机验收全部完成前,不得代替或与本 Wails 客户端同时分发。 + +完整的全客户端构建、安装和验证步骤见 +[`../docs/client-build-install.md`](../docs/client-build-install.md)。 diff --git a/desktop/RESEARCH.md b/desktop/RESEARCH.md index 4e360e8..9383783 100644 --- a/desktop/RESEARCH.md +++ b/desktop/RESEARCH.md @@ -1,6 +1,8 @@ -# cdrop 桌面端 D1 前决策就绪简报 +# Commilitia Drop 桌面端 D1 前决策就绪简报 -> 框架基线:Wails v2.12.0(Go 1.23 模块声明,本机 toolchain 1.26.3)+ 系统 WebView,瘦客户端,业务全走 `drop.commilitia.net`。平台优先级 macOS(Apple Silicon + Intel universal),Windows x64 兼容。 +> 框架基线:Wails v2.12.0(Go 1.23 模块声明,本机 toolchain 1.26.3)+ 系统 WebView, +> 瘦客户端,业务全走 `drop.commilitia.net`。平台优先级 macOS(仅 Apple Silicon), +> Windows x64 兼容。 > 已对四项关键论断做对抗式核验:托盘能力 **refuted**、热键库可用性 **supported(附 pin 条件)**、Casdoor/R1 **mixed(需一处后端改动 + 版本前提)**、签名/公证 entitlement **mixed(核心子句成立,捆绑表述需修正)**。 --- @@ -121,7 +123,7 @@ 本节只覆盖 Wails/Go 客户端侧的实现机制。前提已确定:public client + PKCE,回调用 `http://127.0.0.1:<临时端口>/callback`,Casdoor 对 loopback 任意端口放行,后端 audience 改多值校验。Casdoor 端点:authorize=`/login/oauth/authorize`,token=`/api/login/oauth/access_token`。 -### 1. loopback vs 自定义 scheme `cdrop://` 的取舍(Wails 语境) +### 1. loopback vs 自定义 scheme `commilitia-drop://` 的取舍(Wails 语境) 两条路线都符合 RFC 8252 对原生应用的要求(§7.1 私有 scheme、§7.3 loopback),但在 Wails 下实现成本差异明显。 @@ -132,10 +134,15 @@ - 跨平台零差异:macOS / Windows / Linux 代码一致,只依赖 `net/http`。 - 必须用 IP 字面量 `127.0.0.1`(或 `[::1]`),不要用 `localhost`——RFC 8252 §8.3:用 `localhost` 可能因 DNS / hosts 解析意外监听到非回环接口。端口用 OS 分配的临时端口(`:0`),符合 RFC 8252 §7.3“服务器 MUST 允许请求时指定任意端口”。 -**自定义 scheme(`cdrop://callback`):** +**自定义 scheme(`commilitia-drop://callback`):** -- 需要 OS 级登记:macOS 在 `Info.plist` 写 `CFBundleURLTypes` → `CFBundleURLSchemes`(值 `cdrop`);Windows 要在注册表 `HKEY_CLASSES_ROOT\cdrop`(或 per-user `HKCU\Software\Classes\cdrop`)写 `URL Protocol` 键 + `shell\open\command` 指向 exe;Linux 走 `.desktop` 的 `x-scheme-handler/cdrop`。 -- 必须配单实例锁。点 `cdrop://` 时 OS 会重新拉起 app 的“第二个实例”,深链作为命令行参数传入。需要 `options.App.SingleInstanceLock` 把这个 deep link 转发给首实例,否则 token 落在一个马上要退出的临时进程里,拿不到。 +- 需要 OS 级登记:macOS 在 `Info.plist` 写 `CFBundleURLTypes` → `CFBundleURLSchemes` + (值 `commilitia-drop`);Windows 要在注册表 + `HKEY_CLASSES_ROOT\commilitia-drop`(或 per-user + `HKCU\Software\Classes\commilitia-drop`)写 `URL Protocol` 键 + + `shell\open\command` 指向 exe;Linux 走 + `.desktop` 的 `x-scheme-handler/commilitia-drop`。 +- 必须配单实例锁。点 `commilitia-drop://` 时 OS 会重新拉起 app 的“第二个实例”,深链作为命令行参数传入。需要 `options.App.SingleInstanceLock` 把这个 deep link 转发给首实例,否则 token 落在一个马上要退出的临时进程里,拿不到。 - macOS 上单实例锁与深链协作有已知坑(见 wails issue #5089:v3 的 single instance lock 与 `OpenedWithURL` 不兼容;v2 也需自己从 `Args` 解析 URL 并 `WindowUnminimise` + `Show`)。 scheme 路线的 `SingleInstanceLock` 形态(作为对比,**本项目不采用**): @@ -145,9 +152,9 @@ scheme 路线的 `SingleInstanceLock` 形态(作为对比,**本项目不采 SingleInstanceLock: &options.SingleInstanceLock{ UniqueId: "net.commilitia.cdrop", OnSecondInstanceLaunch: func(d options.SecondInstanceData) { - // d.Args 里含被 OS 透传的 cdrop://callback?code=...&state=... + // d.Args 里含被 OS 透传的 commilitia-drop://callback?code=...&state=... for _, arg := range d.Args { - if strings.HasPrefix(arg, "cdrop://") { + if strings.HasPrefix(arg, "commilitia-drop://") { runtime.WindowUnminimise(appCtx) // 回调不会自动聚焦窗口 runtime.Show(appCtx) runtime.EventsEmit(appCtx, "oauth:callback", arg) @@ -157,7 +164,7 @@ SingleInstanceLock: &options.SingleInstanceLock{ }, ``` -**推荐:loopback。** 在 Wails 下它省掉了单实例锁、`Info.plist`/注册表登记、深链解析与跨平台分叉这一整套,授权流在单进程内自洽,代码量和真机调试面都小得多。`cdrop://` 的唯一优势是不占端口、回调 URL 更“原生”,但对瘦客户端不值这些成本。scheme 留作未来若需“浏览器里点 cdrop 链接唤起桌面端”的备选——那是另一个用例(深链唤起),与本次登录回调无关。 +**推荐:loopback。** 在 Wails 下它省掉了单实例锁、`Info.plist`/注册表登记、深链解析与跨平台分叉这一整套,授权流在单进程内自洽,代码量和真机调试面都小得多。`commilitia-drop://` 的唯一优势是不占端口、回调 URL 更“原生”,但对瘦客户端不值这些成本。scheme 留作未来若需“浏览器里点 Commilitia Drop 链接唤起桌面端”的备选——那是另一个用例(深链唤起),与本次登录回调无关。 ### 2. `runtime.BrowserOpenURL` 用法与注意 @@ -397,13 +404,13 @@ export async function onLoginClick(): Promise func writeClosePage(w http.ResponseWriter, ok bool) { w.Header().Set("Content-Type", "text/html; charset=utf-8") - msg := "已登录,可关闭本页返回 cdrop。" + msg := "已登录,可关闭本页返回 Commilitia Drop。" if !ok { - msg = "登录未完成,可关闭本页返回 cdrop 重试。" + msg = "登录未完成,可关闭本页返回 Commilitia Drop 重试。" } fmt.Fprintf(w, ``+ - `cdrop`+ + `Commilitia Drop`+ `

%s

`+ ``, msg) } @@ -443,7 +450,8 @@ func writeClosePage(w http.ResponseWriter, ok bool) ## 打包·签名·公证操作手册(D6 补充) -本节只补**具体命令与 CI 配置**;entitlement/公证“会不会被拒”的判断已在前文完成。环境基准:Wails v2.12.0(Go),macOS universal 优先 + Windows x64,瘦客户端。 +本节只补**具体命令与 CI 配置**;entitlement/公证“会不会被拒”的判断已在前文完成。 +环境基准:Wails v2.12.0(Go),macOS arm64 + Windows x64,瘦客户端。 > 重要前提:Wails 官方 signing 指南至今仍推荐 `gon`,但该工具已停止维护、且依赖已废弃的 `altool` 语义。本手册一律改用 Apple 现行的 `codesign` + `notarytool` + `stapler` 链路,这是已确定的正确路径。 @@ -452,13 +460,15 @@ func writeClosePage(w http.ResponseWriter, ok bool) #### 1.1 构建产物与已知坑(已确定) ```bash -wails build -platform darwin/universal -clean +wails build -platform darwin/arm64 -clean ``` -- 产物:`build/bin/.app`(universal `.app` bundle,`lipo` 已合并 arm64 + amd64 两份 Go 二进制)。`` 来自 `wails.json` 的 `outputfilename`/项目名。 +- 产物:`build/bin/.app`(Apple Silicon arm64)。`` 来自 + `wails.json` 的 `outputfilename`/项目名。 - 项目脚手架在 `build/darwin/` 下生成 `Info.plist`(模板 `Info.plist`/`Info.dev.plist`),这是 `.app` 内 `Contents/Info.plist` 的来源。改 bundle ID、版本号、`LSMinimumSystemVersion` 等都改这里。 - 已知坑: - - universal 构建要求**本机同时具备 arm64 与 amd64 的 CGO 工具链**。在 Apple Silicon 的 `macos-latest`(macos-14/15)runner 上原生满足;不要尝试从 Linux 交叉编译 darwin(CGO + macOS SDK 缺失,社区反复确认不可行)。 + - 当前只构建 arm64;不要尝试从 Linux 交叉编译 darwin(CGO + macOS SDK 缺失, + 社区反复确认不可行)。 - `wails build` 本身**不签名、不公证 macOS 产物**(与 Windows 的 `-nsis` 不同,没有内建签名参数)。签名/公证完全是构建后的独立步骤。 - Wails 没有内建 DMG 封装,需自己做(见 1.4)。 @@ -563,7 +573,22 @@ xcrun stapler staple "YourApp.dmg" #### 1.6 Wails 对 darwin 签名的内建支持(已确定) -`wails build -platform darwin/universal` **无签名参数**,不做 codesign/notarize;签名、公证、DMG 全是构建后的外部步骤。`build/darwin/` 下有 `Info.plist`/`Info.dev.plist`;`entitlements.plist` 非自动生成,需手动放进该路径。universal 由 Wails 内部 `lipo` 合并,无需手动 `lipo`。 +`wails build -platform darwin/arm64` **无签名参数**,不做 codesign/notarize;签名、 +公证、DMG 全是构建后的外部步骤。`build/darwin/` 下有 `Info.plist`/`Info.dev.plist`; +`entitlements.plist` 非自动生成,需手动放进该路径。 + +#### 1.7 本项目已验证的正式 recipe(2026-07-31) + +仓库根目录的 `just desktop-dist-mac` 已把上述外部步骤固化:ARM64 构建 → 复用既有 +Developer ID Application 身份签名 → `ditto` ZIP 公证 → app staple → 生成 DMG → DMG 签名与 +单独公证 → DMG staple → Gatekeeper 评估。DMG 由 `desktop/scripts/make-dmg.sh` 使用纯系统工具 +创建,内容只有 `Commilitia Drop.app` 和 `/Applications` 链接,不依赖 `create-dmg`。 + +本次实测 app submission `0e2c80e1-e40d-4108-951e-64c810bd118e`、DMG submission +`7a6aea7a-6c40-4bdb-96e3-46e6f8fbc813` 均为 `Accepted`;两者 staple/validate 成功, +`spctl` 返回 `Notarized Developer ID`。这证明 Wails 应用本身满足 Apple 公证要求;后续发布应 +复用 recipe,不再使用 ad-hoc 签名或未公证 `.app` 作为正式分发物。面向操作者的完整流程见 +`docs/client-build-install.md`。 ### 2. Windows @@ -596,7 +621,7 @@ signtool sign /fd sha256 /tr http://ts.ssl.com /td sha256 /f certificate.pfx /p ```yaml jobs: macos: - runs-on: macos-latest # Apple Silicon,原生支持 universal CGO + runs-on: macos-latest # Apple Silicon arm64 steps: - uses: actions/checkout@v4 with: { submodules: recursive } # cjk-autospace 等 submodule @@ -605,7 +630,7 @@ jobs: - uses: actions/setup-node@v4 with: { node-version: '20' } - run: go install github.com/wailsapp/wails/v2/cmd/wails@v2.12.0 - - run: wails build -platform darwin/universal -clean + - run: wails build -platform darwin/arm64 -clean - uses: apple-actions/import-codesign-certs@v7 with: p12-file-base64: ${{ secrets.APPLE_CERT_P12_BASE64 }} @@ -685,7 +710,7 @@ jobs: - Wails Mac App Store 指南(entitlements/codesign --options=runtime、build/darwin 路径):https://wails.io/docs/guides/mac-appstore/ - Wails NSIS installer 指南(build/windows/installer、-nsis):https://wails.io/docs/guides/windows-installer/ - Wails NSIS 不签主 exe 的 issue #3716:https://github.com/wailsapp/wails/issues/3716 -- Wails Crossplatform build(GitHub Actions、darwin/universal 矩阵):https://wails.io/docs/guides/crossplatform-build/ +- Wails Crossplatform build(GitHub Actions):https://wails.io/docs/guides/crossplatform-build/ - notarytool man page:https://keith.github.io/xcode-man-pages/notarytool.1.html - Apple TN3147 迁移到新公证工具:https://developer.apple.com/documentation/technotes/tn3147-migrating-to-the-latest-notarization-tool - Apple Disable Library Validation entitlement:https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_cs_disable-library-validation diff --git a/desktop/app.go b/desktop/app.go index 7103879..04d76b3 100644 --- a/desktop/app.go +++ b/desktop/app.go @@ -62,6 +62,9 @@ func (a *App) startup(ctx context.Context) { if platform.IsLaunchAtLoginEnabled() { _ = platform.SetLaunchAtLogin(true) } + // Migrate native notification metadata at startup so an old internal-name + // entry does not remain visible until the first incoming notification. + platform.InitializeNotifications() // 原生传输引擎:落地目录取当前配置(设置页改目录时经 SaveSettings 同步到引擎)。 a.transfer = engine.New(engine.Config{DownloadDir: platform.ResolveDownloadDir()}, &transferEvents{app: a}) a.startClipboardSync(ctx) @@ -70,10 +73,10 @@ func (a *App) startup(ctx context.Context) { platform.TriggerLocalNetwork() platform.InstallStatusBar( platform.StatusBarMenu{ - Title: "cdrop", + Title: "Commilitia Drop", Show: "显示主窗口", Settings: "设置…", - Quit: "退出 cdrop", + Quit: "退出 Commilitia Drop", }, // The native menu-action callbacks fire on the AppKit main thread; calling // Wails runtime methods synchronously there can re-enter the main run loop @@ -107,10 +110,10 @@ func (a *App) buildMenu() *menu.Menu { m := menu.NewMenu() appSub := menu.NewMenu() - appSub.Append(menu.Text("退出 cdrop", keys.CmdOrCtrl("q"), func(*menu.CallbackData) { + appSub.Append(menu.Text("退出 Commilitia Drop", keys.CmdOrCtrl("q"), func(*menu.CallbackData) { a.requestQuit() })) - m.Append(menu.SubMenu("cdrop", appSub)) // macOS renders the first menu as the app menu + m.Append(menu.SubMenu("Commilitia Drop", appSub)) // macOS renders the first menu as the app menu m.Append(menu.EditMenu()) // Undo/Redo/Cut/Copy/Paste/SelectAll @@ -415,7 +418,7 @@ func (a *App) resolveOAuthConfig() (platform.OAuthConfig, error) { func oauthConfigFromEnv() platform.OAuthConfig { return platform.OAuthConfig{ BrokerURL: os.Getenv("CDROP_BROKER_URL"), - App: envOr("CDROP_BROKER_APP", "cdrop"), + App: envOr("CDROP_BROKER_APP", "commilitia-drop"), } } diff --git a/desktop/build/darwin/Info.dev.plist b/desktop/build/darwin/Info.dev.plist index b8cfbec..6be46cc 100644 --- a/desktop/build/darwin/Info.dev.plist +++ b/desktop/build/darwin/Info.dev.plist @@ -29,7 +29,7 @@ Commilitia Drop 使用本地网络发现同内网设备并建立点对点直连传输。 NSBonjourServices - _cdrop._tcp + _commilitia-drop._tcp {{if .Info.FileAssociations}} CFBundleDocumentTypes diff --git a/desktop/build/darwin/Info.plist b/desktop/build/darwin/Info.plist index bd6c26c..478b734 100644 --- a/desktop/build/darwin/Info.plist +++ b/desktop/build/darwin/Info.plist @@ -29,7 +29,7 @@ Commilitia Drop 使用本地网络发现同内网设备并建立点对点直连传输。 NSBonjourServices - _cdrop._tcp + _commilitia-drop._tcp {{if .Info.FileAssociations}} CFBundleDocumentTypes diff --git a/desktop/engine/wire.go b/desktop/engine/wire.go index 8c950ac..a7a9510 100644 --- a/desktop/engine/wire.go +++ b/desktop/engine/wire.go @@ -7,7 +7,7 @@ // 留给宿主(桌面 JS / iOS Swift),引擎只经回调收发不透明信令串——纯数据面。 // // 线协议与 web 引擎 web/src/features/transfer/p2p.ts 逐字节一致,故 Go 端可与浏览器 / -// iOS 的 JS 引擎互通:DataChannel "cdrop-file"(ordered),控制帧 meta/done/ack 走文本帧、 +// iOS 的 JS 引擎互通:DataChannel "commilitia-drop-file"(ordered),控制帧 meta/done/ack 走文本帧、 // 文件分片走二进制帧;信令 payload 形如 {type, sdp?, candidate?}。 package engine @@ -22,7 +22,7 @@ import ( ) const ( - channelName = "cdrop-file" + channelName = "commilitia-drop-file" chunkSize = 64 * 1024 highWatermark = 16 * 1024 * 1024 lowWatermark = 4 * 1024 * 1024 diff --git a/desktop/frontend/index.html b/desktop/frontend/index.html index e6fdeed..1878434 100644 --- a/desktop/frontend/index.html +++ b/desktop/frontend/index.html @@ -3,7 +3,7 @@ - cdrop-desktop + Commilitia Drop
diff --git a/desktop/frontend/src/main.ts b/desktop/frontend/src/main.ts index b89cf63..99f4a56 100644 --- a/desktop/frontend/src/main.ts +++ b/desktop/frontend/src/main.ts @@ -8,9 +8,9 @@ import { EventsOn } from "../wailsjs/runtime/runtime"; // Go 换 token → oauth:success / oauth:error 事件回到这里。 document.querySelector("#app")!.innerHTML = `
-

cdrop 桌面端

+

Commilitia Drop

检查登录状态……

- +

登录会在系统浏览器中打开授权页,完成后自动返回。

`; diff --git a/desktop/main.go b/desktop/main.go index 57b5a40..e249174 100644 --- a/desktop/main.go +++ b/desktop/main.go @@ -67,7 +67,7 @@ func main() { // Create application with options err = wails.Run(&options.App{ - Title: "cdrop", + Title: "Commilitia Drop", Width: 1024, Height: 768, StartHidden: hidden, diff --git a/desktop/platform/appsettings.go b/desktop/platform/appsettings.go index be7f8ff..92b03e4 100644 --- a/desktop/platform/appsettings.go +++ b/desktop/platform/appsettings.go @@ -68,7 +68,14 @@ func ResolveDeviceName() string { if h, err := os.Hostname(); err == nil && h != "" { return h } - return "cdrop-desktop" + switch runtime.GOOS { + case "darwin": + return "Commilitia Drop (macOS)" + case "windows": + return "Commilitia Drop (Windows)" + default: + return "Commilitia Drop" + } } // DefaultConfig is what a fresh install gets: clipboard sync on, no autostart. diff --git a/desktop/platform/config.go b/desktop/platform/config.go index 97db96a..c9c41f5 100644 --- a/desktop/platform/config.go +++ b/desktop/platform/config.go @@ -43,7 +43,7 @@ func FetchOAuthConfig(ctx context.Context, apiBase string) (OAuthConfig, error) app := c.App if app == "" { - app = "cdrop" + app = "commilitia-drop" } cfg := OAuthConfig{BrokerURL: c.BrokerURL, App: app} if cfg.BrokerURL == "" { diff --git a/desktop/platform/config_test.go b/desktop/platform/config_test.go index 28ec169..c538fae 100644 --- a/desktop/platform/config_test.go +++ b/desktop/platform/config_test.go @@ -17,7 +17,7 @@ func TestFetchOAuthConfig_Success(t *testing.T) { _ = json.NewEncoder(w).Encode(map[string]any{ "auth_mode": "prod", "broker_url": "https://sso.example.net", - "broker_app": "cdrop", + "broker_app": "commilitia-drop", }) })) defer srv.Close() @@ -30,13 +30,13 @@ func TestFetchOAuthConfig_Success(t *testing.T) { if cfg.BrokerURL != "https://sso.example.net" { t.Errorf("broker_url = %q", cfg.BrokerURL) } - if cfg.App != "cdrop" { - t.Errorf("app = %q, want cdrop", cfg.App) + if cfg.App != "commilitia-drop" { + t.Errorf("app = %q, want commilitia-drop", cfg.App) } } func TestFetchOAuthConfig_DefaultsApp(t *testing.T) { - // broker_app omitted → defaults to "cdrop". + // broker_app omitted → defaults to "commilitia-drop". srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]any{"broker_url": "https://sso.example.net"}) @@ -46,8 +46,8 @@ func TestFetchOAuthConfig_DefaultsApp(t *testing.T) { if err != nil { t.Fatalf("FetchOAuthConfig: %v", err) } - if cfg.App != "cdrop" { - t.Errorf("app = %q, want default cdrop", cfg.App) + if cfg.App != "commilitia-drop" { + t.Errorf("app = %q, want default commilitia-drop", cfg.App) } } diff --git a/desktop/platform/devicesession.go b/desktop/platform/devicesession.go index fbd53a3..207a67e 100644 --- a/desktop/platform/devicesession.go +++ b/desktop/platform/devicesession.go @@ -12,7 +12,7 @@ import ( ) // 代铸 (proxy-mint) on the desktop. The device-authorization flow yields a bootstrap -// machine token (scope app:cdrop, no meta) that proves the user's identity but is not a +// machine token (scope app:commilitia-drop, no meta) that proves the user's identity but is not a // cdrop-managed device. This call exchanges it for a managed device session bound to this // device's stable device_id, so the desktop joins cdrop's unified device list and is // managed exactly like a browser — the same model, not a separate native-only track. diff --git a/desktop/platform/download.go b/desktop/platform/download.go index 9167602..806352d 100644 --- a/desktop/platform/download.go +++ b/desktop/platform/download.go @@ -46,7 +46,7 @@ func writeInto(dir, name string, data []byte) (string, error) { } safe := sanitizeFileName(name) if safe == "" { - safe = "cdrop-download" + safe = "Commilitia Drop Download" } target := uniquePath(dir, safe) if err := os.WriteFile(target, data, 0o644); err != nil { @@ -248,7 +248,7 @@ func FinalizeStreamingDownload(sessionId, name string) (string, error) { } safe := sanitizeFileName(name) if safe == "" { - safe = "cdrop-download" + safe = "Commilitia Drop Download" } target := uniquePath(d.dir, safe) if err := os.Rename(d.tmp, target); err != nil { diff --git a/desktop/platform/heal.go b/desktop/platform/heal.go index 318e631..09cfd9e 100644 --- a/desktop/platform/heal.go +++ b/desktop/platform/heal.go @@ -69,7 +69,7 @@ func HealIdentity(s *LoginResult, apiBase string) *LoginResult { } if changed { if err := SaveSession(*s); err != nil { - slog.Warn("cdrop: heal identity save failed", "err", err) + slog.Warn("Commilitia Drop: heal identity save failed", "err", err) } } return s diff --git a/desktop/platform/launchagent_darwin.go b/desktop/platform/launchagent_darwin.go index 3f33c96..627a4dc 100644 --- a/desktop/platform/launchagent_darwin.go +++ b/desktop/platform/launchagent_darwin.go @@ -10,8 +10,8 @@ import ( "strings" ) -// launchAgentLabel is the reverse-DNS label + plist filename for the per-user -// LaunchAgent that starts cdrop at login. +// launchAgentLabel is a stable internal identifier. The product name shown to +// users comes from the application metadata, not this LaunchAgent label. const launchAgentLabel = "net.commilitia.cdrop" func launchAgentPath() (string, error) { @@ -71,7 +71,40 @@ func SetLaunchAtLogin(enabled bool) error { if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { return err } - return os.WriteFile(p, []byte(buildLaunchAgentPlist(launchAgentLabel, args)), 0o644) + if err := writeLaunchAgentAtomically( + p, + []byte(buildLaunchAgentPlist(launchAgentLabel, args)), + ); err != nil { + return err + } + return nil +} + +func writeLaunchAgentAtomically(path string, contents []byte) error { + dir := filepath.Dir(path) + f, err := os.CreateTemp(dir, "."+filepath.Base(path)+".tmp-*") + if err != nil { + return err + } + tempPath := f.Name() + defer os.Remove(tempPath) + + if err := f.Chmod(0o644); err != nil { + f.Close() + return err + } + if _, err := f.Write(contents); err != nil { + f.Close() + return err + } + if err := f.Sync(); err != nil { + f.Close() + return err + } + if err := f.Close(); err != nil { + return err + } + return os.Rename(tempPath, path) } // IsLaunchAtLoginEnabled reports whether the LaunchAgent plist is present. diff --git a/desktop/platform/launchagent_darwin_test.go b/desktop/platform/launchagent_darwin_test.go index 1fcf1dd..388c602 100644 --- a/desktop/platform/launchagent_darwin_test.go +++ b/desktop/platform/launchagent_darwin_test.go @@ -48,8 +48,8 @@ func TestSetLaunchAtLoginInstallsAndRemoves(t *testing.T) { func TestAppBundlePath(t *testing.T) { cases := map[string]string{ - "/Apps/cdrop.app/Contents/MacOS/desktop": "/Apps/cdrop.app", - "/usr/local/bin/desktop": "", + "/Apps/Commilitia Drop.app/Contents/MacOS/Commilitia Drop": "/Apps/Commilitia Drop.app", + "/usr/local/bin/Commilitia Drop": "", } for in, want := range cases { if got := appBundlePath(in); got != want { diff --git a/desktop/platform/localnetwork_darwin.m b/desktop/platform/localnetwork_darwin.m index 3a07159..be724b1 100644 --- a/desktop/platform/localnetwork_darwin.m +++ b/desktop/platform/localnetwork_darwin.m @@ -1,6 +1,6 @@ #import -// cdropTriggerLocalNetwork:起一个对 _cdrop._tcp 的 Bonjour 浏览,触发 macOS 本地网络权限弹窗。 +// cdropTriggerLocalNetwork:起一个对 _commilitia-drop._tcp 的 Bonjour 浏览,触发 macOS 本地网络权限弹窗。 // WKWebView 自身不会触发该权限请求,须由宿主 App 主动发起一次本地网络访问;授权后本进程内的 // WebRTC 才能收集 host / mDNS 候选实现同内网直连(见 localnetwork_darwin.go 注释)。浏览结果本身 // 不关心——「发起访问」这一动作即触发授权。保活单个 browser(静态全局,ARC 下持有),幂等。 @@ -11,7 +11,7 @@ void cdropTriggerLocalNetwork(void) { return; } nw_browse_descriptor_t descriptor = - nw_browse_descriptor_create_bonjour_service("_cdrop._tcp", NULL); + nw_browse_descriptor_create_bonjour_service("_commilitia-drop._tcp", NULL); nw_parameters_t parameters = nw_parameters_create(); nw_parameters_set_include_peer_to_peer(parameters, true); diff --git a/desktop/platform/notify_darwin.go b/desktop/platform/notify_darwin.go index 7beae65..e697807 100644 --- a/desktop/platform/notify_darwin.go +++ b/desktop/platform/notify_darwin.go @@ -13,6 +13,10 @@ import "C" import "unsafe" +// InitializeNotifications is a no-op on macOS; the notification center is +// initialized lazily by the native framework. +func InitializeNotifications() {} + // Notify shows a native system notification. // // macOS uses UNUserNotificationCenter, which REQUIRES the app bundle to be diff --git a/desktop/platform/notify_other.go b/desktop/platform/notify_other.go index d528939..e376768 100644 --- a/desktop/platform/notify_other.go +++ b/desktop/platform/notify_other.go @@ -2,6 +2,9 @@ package platform +// InitializeNotifications is a no-op on unsupported desktop platforms. +func InitializeNotifications() {} + // Notify is a no-op on platforms without a native notification backend wired up. // The desktop client targets macOS and Windows; this keeps the package building // on other GOOS (e.g. a Linux `go vet` / CI pass). diff --git a/desktop/platform/notify_windows.go b/desktop/platform/notify_windows.go index e18bb11..036a347 100644 --- a/desktop/platform/notify_windows.go +++ b/desktop/platform/notify_windows.go @@ -4,14 +4,18 @@ package platform import ( "os" + "path/filepath" "sync" toast "git.sr.ht/~jackmordaunt/go-toast/v2" + "golang.org/x/sys/windows/registry" ) const ( - // toastAppID 是 Windows Action Center 里显示的应用标识,与 macOS bundle id 对齐。 - toastAppID = "net.commilitia.cdrop" + // toastAppID 是稳定的内部 AppUserModelID;Windows 通知设置中的用户可见名称由 + // toastDisplayName 单独写入 DisplayName,避免为了改显示名破坏系统身份。 + toastAppID = "net.commilitia.cdrop" + toastDisplayName = "Commilitia Drop" // toastGUID 固定不变——它把通知归属到注册表里的本应用条目;更换会让既有通知 // 失去归属。 toastGUID = "{c4d8e2a1-6b3f-4e7a-9c2d-1f5b8a0e3d6c}" @@ -19,20 +23,41 @@ const ( var toastInit sync.Once -// Notify shows a native Windows toast. go-toast renders via the WinRT/COM path -// (PowerShell fallback when the AppID isn't registered), so it works without code -// signing — SmartScreen only gates the installer, not notifications. Best-effort: -// errors are swallowed. SetAppData registers the app identity once so the toast -// shows "cdrop" rather than the PowerShell host. -func Notify(title, body string) { +// InitializeNotifications registers the stable Windows notification identity +// and corrects its user-visible name. It is safe to call repeatedly. +func InitializeNotifications() { toastInit.Do(func() { data := toast.AppData{AppID: toastAppID, GUID: toastGUID} if exe, err := os.Executable(); err == nil { data.ActivationExe = exe } _ = toast.SetAppData(data) - }) + // go-toast v2.0.3 没有独立 DisplayName 字段,会把 AppID 写进 + // DisplayName,且已有值时不更新;在同一稳定键上显式覆盖显示名。 + appKey := filepath.Join( + "SOFTWARE", + "Classes", + "AppUserModelId", + toastAppID, + ) + if key, err := registry.OpenKey( + registry.CURRENT_USER, + appKey, + registry.SET_VALUE, + ); err == nil { + _ = key.SetStringValue("DisplayName", toastDisplayName) + _ = key.Close() + } + }) +} + +// Notify shows a native Windows toast. go-toast renders via the WinRT/COM path +// (PowerShell fallback when the AppID isn't registered), so it works without code +// signing — SmartScreen only gates the installer, not notifications. Best-effort: +// errors are swallowed. +func Notify(title, body string) { + InitializeNotifications() n := toast.Notification{ AppID: toastAppID, Title: title, diff --git a/desktop/platform/oauth.go b/desktop/platform/oauth.go index 21dd1eb..6fdce60 100644 --- a/desktop/platform/oauth.go +++ b/desktop/platform/oauth.go @@ -26,7 +26,7 @@ import ( // OAuthConfig carries the Auth Broker coordinates. BrokerURL is the broker's PUBLIC // origin (e.g. https://sso.commilitia.net); App is this app's key in the broker apps -// registry ("cdrop"). The app's loopback redirect must be registered in the broker's +// registry ("commilitia-drop"). The app's loopback redirect must be registered in the broker's // apps.json (redirect_uris) for the device flow to accept it. type OAuthConfig struct { BrokerURL string @@ -138,7 +138,7 @@ func (f *Flow) Login(ctx context.Context) (*TokenResult, error) { "state": {state}, "code_challenge": {challenge}, "code_challenge_method": {"S256"}, - "description": {"cdrop 桌面客户端"}, + "description": {"Commilitia Drop"}, }.Encode() f.openURL(authURL) @@ -255,11 +255,11 @@ func randString(n int) (string, error) { // the real affordance; the close attempt is best-effort. func writeClosePage(w http.ResponseWriter, ok bool) { w.Header().Set("Content-Type", "text/html; charset=utf-8") - msg := "已登录,可关闭本页返回 cdrop。" + msg := "已登录,可关闭本页返回 Commilitia Drop。" if !ok { - msg = "登录未完成,可关闭本页返回 cdrop 重试。" + msg = "登录未完成,可关闭本页返回 Commilitia Drop 重试。" } - fmt.Fprintf(w, `cdrop`+ + fmt.Fprintf(w, `Commilitia Drop`+ ``+ `

%s

`, msg) } diff --git a/desktop/platform/oauth_test.go b/desktop/platform/oauth_test.go index 888663f..dc3dac2 100644 --- a/desktop/platform/oauth_test.go +++ b/desktop/platform/oauth_test.go @@ -55,7 +55,7 @@ func TestLogin_Success(t *testing.T) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]any{ "id": "sid-1", - "app": "cdrop", + "app": "commilitia-drop", "access": "at-123", "refresh": "rtk-456", "access_expires": time.Now().Add(15 * time.Minute).Unix(), @@ -63,7 +63,7 @@ func TestLogin_Success(t *testing.T) { })) defer brokerSrv.Close() - cfg := OAuthConfig{BrokerURL: brokerSrv.URL, App: "cdrop"} + cfg := OAuthConfig{BrokerURL: brokerSrv.URL, App: "commilitia-drop"} // Fake browser: parse the /device/authorize URL, assert it carries app + PKCE, // then GET the loopback redirect with a code + the same state (approved). @@ -80,7 +80,7 @@ func TestLogin_Success(t *testing.T) { if q.Get("code_challenge") == "" || q.Get("code_challenge_method") != "S256" { t.Errorf("authorize request missing PKCE challenge: %v", q) } - if q.Get("app") != "cdrop" { + if q.Get("app") != "commilitia-drop" { t.Errorf("authorize app = %q", q.Get("app")) } cb := q.Get("redirect_uri") + "?code=auth-code-xyz&state=" + url.QueryEscape(q.Get("state")) @@ -119,7 +119,7 @@ func TestLogin_Success(t *testing.T) { } func TestLogin_StateMismatch(t *testing.T) { - cfg := OAuthConfig{BrokerURL: "https://sso.example.net", App: "cdrop"} + cfg := OAuthConfig{BrokerURL: "https://sso.example.net", App: "commilitia-drop"} openURL := func(authURL string) { u, _ := url.Parse(authURL) redirect := u.Query().Get("redirect_uri") @@ -157,7 +157,7 @@ func TestRefresh_Success(t *testing.T) { })) defer brokerSrv.Close() - cfg := OAuthConfig{BrokerURL: brokerSrv.URL, App: "cdrop"} + cfg := OAuthConfig{BrokerURL: brokerSrv.URL, App: "commilitia-drop"} tok, err := NewFlow(cfg, func(string) {}).Refresh(context.Background(), "old-rtk") if err != nil { t.Fatalf("Refresh: %v", err) @@ -174,7 +174,7 @@ func TestRefresh_Success(t *testing.T) { } func TestRefresh_EmptyToken(t *testing.T) { - cfg := OAuthConfig{BrokerURL: "https://sso.example.net", App: "cdrop"} + cfg := OAuthConfig{BrokerURL: "https://sso.example.net", App: "commilitia-drop"} if _, err := NewFlow(cfg, func(string) {}).Refresh(context.Background(), ""); err == nil { t.Fatal("want error for empty refresh token") } diff --git a/desktop/platform/session.go b/desktop/platform/session.go index 94b71c4..1275c64 100644 --- a/desktop/platform/session.go +++ b/desktop/platform/session.go @@ -177,7 +177,11 @@ func SaveSession(res LoginResult) error { if enc, err := encryptToken(res.RefreshToken); err == nil { rec.RefreshTokenEnc = enc } else { - slog.Warn("cdrop: refresh_token kept in session file; OS secret store unavailable", "err", err) + slog.Warn( + "Commilitia Drop: refresh_token kept in session file; OS secret store unavailable", + "err", + err, + ) rec.RefreshToken = res.RefreshToken } } @@ -226,7 +230,7 @@ func LoadSession() (*LoginResult, error) { // still unavailable, leaving the file as-is). res.RefreshToken = rec.RefreshToken if err := SaveSession(*res); err != nil { - slog.Warn("cdrop: refresh_token migration to secret store failed", "err", err) + slog.Warn("Commilitia Drop: refresh_token migration to secret store failed", "err", err) } case rec.RefreshTokenEnc != "": if rt, err := decryptToken(rec.RefreshTokenEnc); err == nil { @@ -234,7 +238,7 @@ func LoadSession() (*LoginResult, error) { } else { // Key gone / ciphertext corrupt: drop to an access-token-only session; // the app will require a fresh login once the access_token lapses. - slog.Warn("cdrop: decrypt refresh_token failed; re-login will be required", "err", err) + slog.Warn("Commilitia Drop: decrypt refresh_token failed; re-login will be required", "err", err) } } return res, nil @@ -252,7 +256,7 @@ func ClearSession() error { } if err := keyring.Delete(keyringService, keyringKeyAccount); err != nil && !errors.Is(err, keyring.ErrNotFound) { - slog.Warn("cdrop: clear session key from secret store failed", "err", err) + slog.Warn("Commilitia Drop: clear session key from secret store failed", "err", err) } return nil } diff --git a/desktop/platform/statusbar_darwin.go b/desktop/platform/statusbar_darwin.go index 4d23c4b..3b578af 100644 --- a/desktop/platform/statusbar_darwin.go +++ b/desktop/platform/statusbar_darwin.go @@ -4,7 +4,7 @@ package platform /* #cgo darwin CFLAGS: -x objective-c -fobjc-arc -#cgo darwin LDFLAGS: -framework Cocoa +#cgo darwin LDFLAGS: -framework Cocoa -framework UniformTypeIdentifiers #include void cdropStatusBarInstall(const void *iconPNG, int iconLen, const char *title, diff --git a/desktop/scripts/make-dmg.sh b/desktop/scripts/make-dmg.sh new file mode 100755 index 0000000..4472a10 --- /dev/null +++ b/desktop/scripts/make-dmg.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ $# -ne 2 ]] +then + echo "用法:$0 " >&2 + exit 2 +fi + +appPath="$1" +outputPath="$2" + +if [[ ! -d "$appPath" ]] +then + echo "找不到应用:$appPath" >&2 + exit 1 +fi + +stagingDirectory="$(mktemp -d /tmp/commilitia-drop-dmg.XXXXXX)" +trap 'rm -rf "$stagingDirectory"' EXIT + +ditto "$appPath" "$stagingDirectory/Commilitia Drop.app" +ln -s /Applications "$stagingDirectory/Applications" +mkdir -p "$(dirname "$outputPath")" +rm -f "$outputPath" + +hdiutil create \ + -volname "Commilitia Drop" \ + -srcfolder "$stagingDirectory" \ + -ov \ + -format UDZO \ + "$outputPath" diff --git a/desktop/wails.json b/desktop/wails.json index 8b68d96..c35713c 100644 --- a/desktop/wails.json +++ b/desktop/wails.json @@ -1,13 +1,13 @@ { "$schema": "https://wails.io/schemas/config.v2.json", - "name": "Commilitia Drop Desktop", - "outputfilename": "Commilitia Drop Desktop", + "name": "Commilitia Drop", + "outputfilename": "Commilitia Drop", "frontend:install": "cd ../../web && npm install", "frontend:build": "cd ../../web && npm run build -- --outDir ../desktop/frontend/dist --emptyOutDir", "frontend:dev:watcher": "cd ../../web && npm run dev", "frontend:dev:serverUrl": "auto", "info": { - "productName": "Commilitia Drop Desktop" + "productName": "Commilitia Drop" }, "author": { "name": "commilitia", diff --git a/docker/compose.snippet.yaml b/docker/compose.snippet.yaml index 71bfcea..7667582 100644 --- a/docker/compose.snippet.yaml +++ b/docker/compose.snippet.yaml @@ -28,8 +28,8 @@ NN-cdrop: # CDROP_BROKER_INTERNAL_KEY: REPLACE_WITH_BROKER_INTERNAL_KEY # broker 公开源(浏览器全局 SSO 登录跳转 / 原生设备授权流目标)。 # CDROP_BROKER_PUBLIC_URL: https://sso.your-domain.example - # 本应用在 broker apps 注册表里的 key(默认 cdrop)。 - # CDROP_BROKER_APP: cdrop + # 本应用在 broker apps 注册表里的 key(默认 commilitia-drop)。 + # CDROP_BROKER_APP: commilitia-drop # 部署公开源(扫码 QR 链接 / CSRF Origin / Web Push 默认联系标识)。 # CDROP_PUBLIC_URL: https://drop.your-domain.example diff --git a/docs/client-build-install.md b/docs/client-build-install.md new file mode 100644 index 0000000..22835dd --- /dev/null +++ b/docs/client-build-install.md @@ -0,0 +1,348 @@ +# Commilitia Drop 全客户端构建与安装手册 + +本文记录 Web、服务端、Windows、macOS、iPhone 与 iPad 的正式构建、签名、安装和验证流程。 +命令默认从仓库根目录执行。项目内部目录名和稳定系统身份可以继续使用 `cdrop`;所有用户可见名称、 +协议和诊断界面统一使用 “Commilitia Drop” 或 `commilitia-drop` 命名空间。 + +## 1. 正式客户端矩阵 + +| 平台 | 唯一正式实现 | 架构/设备 | 正式产物 | +|---|---|---|---| +| Web | `web/` React SPA / PWA | 现代浏览器 | `web/dist/`,嵌入服务端 | +| Windows | `desktop/` Wails | x86-64 | `Commilitia Drop.exe` | +| macOS | `desktop/` Wails | Apple Silicon ARM64 | `Commilitia Drop.app`/`.dmg` | +| iPhone | `ios/CDrop` SwiftUI | 已登记真机 | `CommilitiaDrop.app` | +| iPad | `ios/CDrop` SwiftUI | 已登记真机 | 与 iPhone 共用 target | + +`ios/CDrop` 中的原生 macOS target 只是迁移候选。它在功能对等、三语界面、签名扩展和真机验收 +全部完成前,不得替代 Wails,也不得作为第二个 macOS 客户端发布。macOS 只考虑 ARM64。 + +## 2. 命名与兼容边界 + +- 用户可见产品名:`Commilitia Drop`,桌面应用名中不再出现 `Desktop`。 +- iOS/iPadOS 登录回调:`commilitia-drop://auth-callback`。 +- WebRTC DataChannel 与 iOS 文件流 Scheme:`commilitia-drop-file`。 +- Bonjour 服务类型:`_commilitia-drop._tcp`。 +- Auth Broker application key:`commilitia-drop`。它会出现在 OAuth 参数和 JWT scope 中,属于 + 用户可见/可诊断协议文本。 +- Bundle ID、App Group、Keychain service、LaunchAgent label、环境变量、数据库和配置目录不直接 + 决定显示名,保持稳定以保护系统权限、钥匙串数据和既有配置。 +- 每个平台只保留上表的一份正式客户端;迁移验证成功后删除同平台旧名称副本。 + +命名复扫时应区分用户可见文本和内部身份。建议至少检查: + +```sh +rg -n -i 'commilitia drop desktop|cdrop desktop|cdrop://|_cdrop\._tcp|cdrop-file' \ + README.md docs auth desktop ios web +``` + +允许保留的典型内部值包括 `CDROP_*` 环境变量、`net.commilitia.cdrop` LaunchAgent/Bundle ID、 +源码 package 和存储目录。若某个内部值直接、不可更改地成为系统显示名,则必须单独迁移。 + +## 3. 通用前置与密钥规则 + +本地需要 Go、Node.js、Wails v2、Just、Xcode Command Line Tools、XcodeGen,以及 iOS 依赖所需的 +Swift Package Manager。环境配置放在仓库根目录、已被 Git 忽略的 `.env`;变量说明见 +`.env.example`。 + +签名和密钥遵循以下规则: + +- 不在仓库记录 `.p8`、私钥、Broker internal key、VAPID 私钥或证书导出文件。 +- iPhone/iPad 必须复用钥匙串中既有 Apple Development 证书。把其 SHA-1 写入 + `CDROP_APPLE_DEVELOPMENT_IDENTITY`;recipe 会在构建前检查身份并把该值固定传给 + `xcodebuild`。不得用 Xcode GUI,不得新建证书或修改私钥 ACL。 +- macOS 正式分发必须复用既有 Developer ID Application 证书;完整身份名写入 + `CDROP_DEVID_IDENTITY`。 +- App Store Connect API Key 只通过 `CDROP_ASC_KEY_PATH`、`CDROP_ASC_KEY_ID` 和 + `CDROP_ASC_ISSUER_ID` 引用。 +- 登录钥匙串必须已经解锁。`errSecInternalComponent` 通常表示签名进程不能使用私钥;先在用户的 + 普通 Terminal 解锁登录钥匙串,再重试同一证书,不要创建替代证书。 + +构建前可做基础检查: + +```sh +git status --short +go version +node --version +just desktop-doctor +security find-identity -v -p codesigning +xcrun devicectl list devices +``` + +## 4. Web 与服务端 + +### 4.1 构建和验证 Web + +```sh +just typecheck-front +just build-front +``` + +正式 Web 构建输出在 `web/dist/`。服务端通过嵌入静态文件提供 SPA、PWA manifest、service worker +和 iOS 原生引擎页,因此发布 Web 改动时必须重新构建服务端镜像,不能只替换单个 HTML 文件。 + +```sh +just test +just docker-image +``` + +部署新镜像后至少验证: + +```text +/healthz +/api/auth/config +/site.webmanifest +/engine.html +``` + +`/api/auth/config` 应返回 `broker_app=commilitia-drop`;manifest 的 `name` 与 `short_name` 均应为 +“Commilitia Drop”。登录、扫码批准、设备续期和传输必须各做一次实际冒烟。 + +### 4.2 Auth Broker 原位迁移 + +Broker application 不创建并行副本,直接把既有 `cdrop` 项改为: + +```text +key: commilitia-drop +callback: commilitia-drop://auth-callback +``` + +只需在修改前备份一次 Broker 配置;客户端和服务端源码由 Git 历史恢复,不另做文件备份。迁移后旧 +session 可以全部失效,客户端重新登录。Broker、Caddy 和服务端的 application key 必须同时一致; +否则批准可以完成,但客户端会在兑换或 scope 校验阶段显示“登录失败”。 + +若 Caddyfile 以单文件 bind mount 进入容器,宿主机上的 `sed -i`/原子替换会生成新 inode,而运行 +容器仍可能读旧 inode。修改后必须热加载,并从 Caddy admin API 或容器内实际挂载内容验证生效; +不能只查看宿主机路径。 + +## 5. macOS 正式客户端(Wails ARM64) + +### 5.1 开发构建 + +```sh +just desktop-build-mac +``` + +任务先构建最新 Web 前端,再执行 `wails build -clean -s -platform darwin/arm64`。输出为: + +```text +desktop/build/bin/Commilitia Drop.app +``` + +`-clean` 是必要的:macOS 与 Windows 共用 `desktop/build/bin/`,另一平台的残留产物可能被 Wails/Go +误判为构建输入。不要把原生 macOS 候选 target 的产物放进正式分发目录。 + +### 5.2 Developer ID 签名、公证与 DMG + +在 `.env` 配齐以下变量: + +```text +CDROP_DEVID_IDENTITY +CDROP_ASC_KEY_PATH +CDROP_ASC_KEY_ID +CDROP_ASC_ISSUER_ID +``` + +然后运行: + +```sh +just desktop-dist-mac +``` + +该 recipe 会依次完成: + +1. ARM64 Wails 构建; +2. 使用既有 Developer ID Application 身份、hardened runtime 和 secure timestamp 签名 `.app`; +3. 将 `.app` 以 `ditto` 打包提交 Apple 公证,等待 `Accepted`,再 staple/validate; +4. 生成含 `.app` 和 `/Applications` 链接的压缩 DMG; +5. 对 DMG 签名、单独提交公证并 staple/validate; +6. 用 Gatekeeper `spctl` 分别评估 app 和 DMG。 + +最终文件为 `desktop/build/bin/Commilitia Drop.dmg`。发布前再次检查: + +```sh +codesign --verify --deep --strict --verbose=2 \ + 'desktop/build/bin/Commilitia Drop.app' +xcrun stapler validate 'desktop/build/bin/Commilitia Drop.app' +xcrun stapler validate 'desktop/build/bin/Commilitia Drop.dmg' +spctl --assess --type execute --verbose=2 \ + 'desktop/build/bin/Commilitia Drop.app' +spctl --assess --type open --context context:primary-signature --verbose=2 \ + 'desktop/build/bin/Commilitia Drop.dmg' +``` + +2026-07-31 已用既有 Developer ID 证书验证完整链路:app 与 DMG 均获 Apple `Accepted`,并完成 +staple 和 Gatekeeper 的 `Notarized Developer ID` 评估。 + +### 5.3 本机安装 + +先验证新 app,再替换 `/Applications` 中的正式客户端;不保留第二个 `Desktop` 副本: + +```sh +ditto 'desktop/build/bin/Commilitia Drop.app' \ + '/Applications/Commilitia Drop.app' +``` + +LaunchAgent label `net.commilitia.cdrop` 是稳定内部身份,可以保留;但其 `ProgramArguments` 必须指向: + +```text +/Applications/Commilitia Drop.app/Contents/MacOS/Commilitia Drop +``` + +更新磁盘 plist 后需要重新 bootstrap,不能只看文件,因为 launchd 可能仍缓存旧路径。最终检查已安装 +app 的签名、公证、架构、进程路径,以及“登录后启动”开关。 + +## 6. Windows 正式客户端(Wails x64) + +### 6.1 构建 + +```sh +just desktop-build-win +``` + +输出为 `desktop/build/bin/Commilitia Drop.exe`。在 macOS 上交叉构建无需 MinGW;Windows 特有通知 +和 WebView2 绑定不依赖 Darwin CGO。当前 EXE 尚无 Authenticode 签名,Windows 会报告 +`NotSigned`,并可能显示 SmartScreen 提示;获得 Windows 代码签名证书后应把签名步骤加入 recipe。 + +### 6.2 通过 mDNS 无代理直连安装 + +目标主机使用 `Desktop-C.local`、SSH 端口 `223`、用户 `commilitia`。访问该主机不得使用任何代理。 +先确认 shell、系统和 SSH 配置没有代理: + +```sh +env | rg -i '^(http|https|all|ftp|no)_proxy=' +scutil --proxy +ssh -G -p 223 commilitia@Desktop-C.local | \ + rg -i '^(hostname|user|port|proxycommand|proxyjump) ' +``` + +mDNS 可能先返回不可达 IPv6,使 SSH 看似超时;继续保留 mDNS 主机名并用 `-4` 强制 IPv4。显式清除 +代理变量、禁用 SSH 代理和跳板,并建立任务专用、10 分钟 TTL 的复用连接: + +```sh +env -u HTTP_PROXY -u HTTPS_PROXY -u ALL_PROXY \ + -u http_proxy -u https_proxy -u all_proxy \ + ssh -4 -M -S /tmp/commilitia-drop-windows-%C \ + -o ControlPersist=600 -o ProxyCommand=none -o ProxyJump=none \ + -p 223 commilitia@Desktop-C.local exit +``` + +复用前用 `stat` 检查 socket 创建时间仍在 TTL 内,并执行 `ssh -O check`。安装流程为: + +1. 在 Windows 上只读侦查 `D:\Tools`、匹配进程和 + `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`; +2. 本地记录 EXE 的 SHA-256; +3. 用同一 `-4`、`ProxyCommand=none`、`ProxyJump=none` 连接上传到用户目录暂存; +4. 远端再次校验 SHA-256; +5. 停止旧 `D:\Tools\Commilitia Drop Desktop.exe` 进程; +6. 先复制为 `Commilitia Drop.installing.exe` 并再次校验,再原子移到 + `D:\Tools\Commilitia Drop.exe`; +7. 把既有 “Commilitia Drop” Run 项改为 + `"D:\Tools\Commilitia Drop.exe" --hidden`; +8. 只有新文件哈希正确后才删除旧 `Desktop` 文件与上传暂存文件; +9. 从 SSH 服务会话直接 `Start-Process` 可能因没有交互桌面而立即退出。使用当前用户完整 Windows + 身份注册 `LogonType=Interactive` 的一次性计划任务,启动后立即删除任务; +10. 独立复核新进程在交互 Session、旧文件不存在、Run 项正确且应用日志无新错误。 + +2026-07-31 在 `Desktop-C.local` 已完成上述流程,正式路径为 +`D:\Tools\Commilitia Drop.exe`,旧 `Desktop` 文件已删除,登录启动项和运行进程均指向新路径。 + +## 7. iPhone 与 iPad + +两种设备共用 `CommilitiaDrop` scheme 和一套签名约束。全程使用 CLI,不打开 Xcode GUI。 + +### 7.1 模拟器验证 + +```sh +just ios-sim-build +``` + +该任务会拉取/生成品牌字体、运行 XcodeGen、解析 SPM 的 libwebrtc,并针对通用 iOS Simulator +目标编译。模拟器不需要 Apple 签名,但不能替代真机的 URL Scheme、本地网络、APNs、Share +Extension 和后台行为验收。 + +### 7.2 复用既有证书真机构建 + +在 `.env` 配齐: + +```text +CDROP_TEAM_ID +CDROP_ASC_KEY_PATH +CDROP_ASC_KEY_ID +CDROP_ASC_ISSUER_ID +CDROP_APPLE_DEVELOPMENT_IDENTITY=<既有 Apple Development 证书 SHA-1> +``` + +确认设备与身份: + +```sh +just ios-devices +security find-identity -v -p codesigning | \ + rg -F "$CDROP_APPLE_DEVELOPMENT_IDENTITY" +``` + +分别覆盖安装 iPhone 与 iPad: + +```sh +just ios-device +just ios-device +``` + +`ios-device` 会把 SHA-1 作为 `CODE_SIGN_IDENTITY` 固定传入 `xcodebuild`,ASC API Key 仅用于自动 +provisioning 的设备、能力和 profile 更新。若签名测试返回 `errSecInternalComponent`,应解锁当前 +登录钥匙串后重试,不得生成新证书或修改私钥 ACL。 + +安装后验证: + +- 设备端显示名为“Commilitia Drop”; +- Bundle ID 保持 `net.commilitia.Commilitia-Drop`; +- `commilitia-drop://auth-callback` 已登记; +- 旧 session 失效时能进入登录页,经 Broker 批准后回到 app 并成功建立新 session; +- iPhone 与 iPad 各自完成一次发送、接收、本地网络权限和后台/扩展冒烟。 + +更完整的真机能力清单见 `ios/CDrop/REALDEVICE.md`。 + +## 8. 发布顺序与分发目录 + +Wails 的 macOS 和 Windows recipe 都用 `-clean` 且共享 `desktop/build/bin/`,因此不能假设两个产物 +会同时保留。推荐固定顺序: + +1. `just desktop-dist-mac`; +2. 复制 `Commilitia Drop.dmg` 到分发目录; +3. `just desktop-build-win`; +4. 复制 `Commilitia Drop.exe` 到分发目录; +5. 检查分发目录只保留这两个正式文件,不保留 `Commilitia Drop Desktop.*`; +6. 记录两者 SHA-256。 + +当前分发目录约定为: + +```text +~/Library/CloudStorage/Dropbox/软件客户端/Commilitia Drop/ +``` + +不要把 `.app` bundle 当成 Dropbox 的正式发布物;macOS 使用已签名、公证、装订的 DMG。 + +## 9. 提交前验证矩阵 + +```sh +git diff --check +go test ./... +(cd desktop && go test ./...) +(cd web && npm run typecheck) +(cd web && npm run build) +just ios-sim-build +bash -n desktop/scripts/make-dmg.sh +just --dry-run desktop-dist-mac +``` + +平台产物还需验证: + +| 对象 | 必验项 | +|---|---| +| Web/服务端 | health、auth config、manifest、登录、传输 | +| Mac app/DMG | ARM64、Developer ID、hardened runtime、Accepted、staple、Gatekeeper | +| Windows EXE | PE32+ x86-64、SHA-256、唯一文件、Run 项、交互 Session 进程 | +| iPhone/iPad | 既有证书 SHA-1、签名验证、显示名、URL Scheme、Broker 登录、真机传输 | + +提交时不加入 `.env`、`.p8`、构建产物、临时 PowerShell/安装脚本或 Broker 备份。客户端源码和构建 +流程由 Git 历史恢复;Auth Broker 配置备份由服务端运维位置单独保留。 diff --git a/docs/ios-shortcut.md b/docs/ios-shortcut.md index af04639..26997d3 100644 --- a/docs/ios-shortcut.md +++ b/docs/ios-shortcut.md @@ -96,7 +96,7 @@ token 用 `CDROP_HS256_SECRET` 派生的 32 字节 HMAC 密钥签发(SHA-256 - 标头:`Authorization` = 文本 `Bearer ` 后插变量 `TOKEN`;`X-Device-Name` = 变量 `NAME`。 6. 「获取词典值」:键 `content`,输入为上一步结果。 7. 「如果」上一步「有任何值」: - - 「拷贝到剪贴板」= 上一步值;(可选)「显示通知」`已从 cdrop 拉取`。 + - 「拷贝到剪贴板」= 上一步值;(可选)「显示通知」`已从 Commilitia Drop 拉取`。 - 否则:(可选)「显示通知」`云端剪贴板为空`。 ### 「上传到云端」分支 @@ -108,14 +108,14 @@ token 用 `CDROP_HS256_SECRET` 派生的 32 字节 HMAC 密钥签发(SHA-256 - 方法:`PUT`。 - 请求体:`JSON`,两字段——`content`(文本)= 变量「剪贴板」;`content_type`(文本)= `text/plain`。 - 标头:`Authorization` = `Bearer ` + 变量 `TOKEN`;`X-Device-Name` = 变量 `NAME`。(请求体选 JSON 时通常自动带 `Content-Type: application/json`,没有就手动加。) - - (可选)「显示通知」`已上传到 cdrop`。 + - (可选)「显示通知」`已上传到 Commilitia Drop`。 - 否则:(可选)「显示通知」`本机剪贴板为空`。 ### 配置 Import Questions(分享前) 打开指令详情(设置)→「导入问题」区,添加三个,对应顶部三条「文本」: - 服务器地址 → 问题「服务器地址」,默认答案 `https://drop.commilitia.net`。 -- 令牌 → 问题「粘贴 cdrop 令牌」,无默认。 +- 令牌 → 问题「粘贴 Commilitia Drop 令牌」,无默认。 - 设备名 → 问题「设备名(仅 ASCII)」,默认 `iPhone`。 ### 分发 diff --git a/internal/config/config.go b/internal/config/config.go index fca7c53..7466335 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -44,7 +44,7 @@ type Config struct { // 内网直连(绝不经公网反代,以保 /internal/* 与 /refresh 可达)。 // BrokerInternalKey —— broker /internal/* 端点的共享密钥(作 X-Internal-Key 发送), // prod 必填。 - // BrokerApp —— 本应用在 broker apps 注册表里的 key(默认 "cdrop")。 + // BrokerApp —— 本应用在 broker apps 注册表里的 key(默认 "commilitia-drop")。 BrokerBaseURL string `koanf:"broker_base_url"` BrokerInternalKey string `koanf:"broker_internal_key"` BrokerApp string `koanf:"broker_app"` @@ -96,7 +96,7 @@ type Config struct { // APNSKeyPath — filesystem path to the .p8 (PEM PKCS8 ES256) key Apple issues. // APNSKeyID — the 10-character Key ID from the Apple Developer portal. // APNSTeamID — the 10-character Team ID from the Apple Developer portal. - // APNSTopic — the app bundle ID (e.g. "net.commilitia.cdrop"). + // APNSTopic — the app bundle ID (e.g. "net.commilitia.Commilitia-Drop"). // APNSEnv — "prod" (default) or "sandbox" (dev/TestFlight builds). // When unset, APNs is disabled and the register endpoint returns 503. APNSKeyPath string `koanf:"apns_key_path"` @@ -123,12 +123,12 @@ func (c *Config) QRLoginOn() bool { return c.QRLoginEnabled && c.BrokerBaseURL != "" } -// BrokerAppOrDefault returns the configured broker app key, defaulting to "cdrop". +// BrokerAppOrDefault returns the configured broker app key, defaulting to "commilitia-drop". func (c *Config) BrokerAppOrDefault() string { if c.BrokerApp != "" { return c.BrokerApp } - return "cdrop" + return "commilitia-drop" } // Load reads config from optional ./config.yaml then overrides with CDROP_* env. @@ -144,7 +144,7 @@ func Load() (*Config, error) { k.Set("clipboard_debounce_sec", 3) k.Set("qr_login_enabled", true) k.Set("qr_request_ttl_seconds", 120) - k.Set("broker_app", "cdrop") + k.Set("broker_app", "commilitia-drop") k.Set("full_access_ttl_seconds", 900) k.Set("full_refresh_ttl_seconds", 604800) k.Set("guest_access_ttl_seconds", 900) diff --git a/internal/httpapi/qr_test.go b/internal/httpapi/qr_test.go index a28a8e9..d201c73 100644 --- a/internal/httpapi/qr_test.go +++ b/internal/httpapi/qr_test.go @@ -83,7 +83,7 @@ func newMockBroker(t *testing.T) (*brokerclient.Client, *mockBrokerState) { } st.sessions[sid] = &mockSession{sid: sid, userID: userID, app: app, meta: meta, sub: str(body, "sub"), label: label, scope: scope, createdAt: created, lastUsedAt: now} _ = json.NewEncoder(w).Encode(map[string]any{ - "id": sid, "app": "cdrop", + "id": sid, "app": "commilitia-drop", "access": "acc-" + sid, "refresh": "rtk-" + sid, "access_expires": time.Now().Add(15 * time.Minute).Unix(), "refresh_expires": time.Now().Add(24 * time.Hour).Unix(), @@ -128,7 +128,7 @@ func newMockBroker(t *testing.T) (*brokerclient.Client, *mockBrokerState) { } })) t.Cleanup(srv.Close) - return brokerclient.New(srv.URL, "test-key", "cdrop"), st + return brokerclient.New(srv.URL, "test-key", "commilitia-drop"), st } // newQRTestServer builds a Server with only the fields the scan-login + device @@ -332,10 +332,10 @@ func TestRequireFullSession_GuestBlockedFullPasses(t *testing.T) { handler.ServeHTTP(w, r) return w.Code } - if c := check("app:cdrop:guest"); c != http.StatusForbidden { + if c := check("app:commilitia-drop:guest"); c != http.StatusForbidden { t.Errorf("guest on full-only route: got %d, want 403", c) } - if c := check("app:cdrop:full"); c != http.StatusOK { + if c := check("app:commilitia-drop:full"); c != http.StatusOK { t.Errorf("full on full-only route: got %d, want 200", c) } } @@ -365,8 +365,8 @@ func TestDeleteDevice_RevokesBrokerSession(t *testing.T) { if !st.revoked["sid-1"] { t.Error("broker session sid-1 was not revoked") } - if st.lastRevokeApp != "cdrop" { - t.Errorf("revoke X-Broker-App: got %q, want cdrop", st.lastRevokeApp) + if st.lastRevokeApp != "commilitia-drop" { + t.Errorf("revoke X-Broker-App: got %q, want commilitia-drop", st.lastRevokeApp) } if _, err := s.queries.GetDevice(context.Background(), got.DeviceID); err == nil { t.Error("device row should be gone after delete") @@ -381,7 +381,7 @@ func TestSessionsList_ShowsDevices(t *testing.T) { got := decodeStatus(t, qrStatus(s, start.RequestID, start.PollSecret)) r := httptest.NewRequest(http.MethodGet, "/api/auth/sessions", nil) - r = r.WithContext(jwtauth.ContextWithClaims(r.Context(), &jwtauth.Claims{UserID: "owner", Scope: "app:cdrop:guest", DeviceID: got.DeviceID})) + r = r.WithContext(jwtauth.ContextWithClaims(r.Context(), &jwtauth.Claims{UserID: "owner", Scope: "app:commilitia-drop:guest", DeviceID: got.DeviceID})) w := httptest.NewRecorder() s.handleSessionsList(w, r) if w.Code != http.StatusOK { @@ -462,7 +462,7 @@ func TestDeviceSession_MintsManagedDevice(t *testing.T) { if st.lastMint["tier"] != "full" || st.lastMint["meta"] != "dev_browser01" || st.lastMint["label"] != "Laptop" { t.Errorf("mint params: %+v", st.lastMint) } - list := listSessions(t, s, "owner", "app:cdrop:full", "dev_browser01") + list := listSessions(t, s, "owner", "app:commilitia-drop:full", "dev_browser01") if len(list) != 1 || list[0].DeviceID != "dev_browser01" || list[0].Kind != "browser" || !list[0].Current { t.Errorf("unified list wrong: %+v", list) } @@ -474,7 +474,7 @@ func TestDeviceSession_Idempotent(t *testing.T) { s, _ := newQRTestServer(t) _ = deviceSession(t, s, "owner", "full", "dev_same01", "Laptop", "browser", "") _ = deviceSession(t, s, "owner", "full", "dev_same01", "Laptop Renamed", "browser", "") - list := listSessions(t, s, "owner", "app:cdrop:full", "dev_same01") + list := listSessions(t, s, "owner", "app:commilitia-drop:full", "dev_same01") if len(list) != 1 { t.Fatalf("idempotent re-mint: got %d sessions, want 1", len(list)) } @@ -493,7 +493,7 @@ func TestSessionsList_FiltersMetalessBootstrap(t *testing.T) { t.Fatalf("bootstrap mint: %v", err) } _ = deviceSession(t, s, "owner", "full", "dev_real01", "Laptop", "browser", "") - list := listSessions(t, s, "owner", "app:cdrop:full", "dev_real01") + list := listSessions(t, s, "owner", "app:commilitia-drop:full", "dev_real01") if len(list) != 1 || list[0].DeviceID != "dev_real01" { t.Fatalf("metaless bootstrap not filtered: %+v", list) } @@ -517,7 +517,7 @@ func TestDeviceSession_RejectsCrossOrigin(t *testing.T) { // A restricted guest minting its device session stays guest — no escalation to full. func TestDeviceSession_GuestTierNotEscalated(t *testing.T) { s, st := newQRTestServer(t) - _ = deviceSession(t, s, "owner", "app:cdrop:guest", "dev_guest01", "Borrowed", "browser", "") + _ = deviceSession(t, s, "owner", "app:commilitia-drop:guest", "dev_guest01", "Borrowed", "browser", "") if st.lastMint["tier"] != "guest" { t.Errorf("guest caller minted tier %v, want guest", st.lastMint["tier"]) } diff --git a/internal/jwtauth/claims.go b/internal/jwtauth/claims.go index fc3ac51..8fca65f 100644 --- a/internal/jwtauth/claims.go +++ b/internal/jwtauth/claims.go @@ -15,7 +15,7 @@ type Claims struct { Avatar string // X-Auth-Avatar (profile picture URL from the broker account); may be empty Groups []string // X-Auth-Roles, comma-split // Scope is the raw X-Auth-Scope: a global SSO user is "full"; a cdrop delegated - // session is "app:cdrop:". Tier() reads the capability grade off the end. + // session is "app:commilitia-drop:". Tier() reads the capability grade off the end. Scope string // DeviceID is X-Auth-Meta: the cdrop device_id this session was minted for — the // join key to the devices row. Empty for an unmanaged caller (e.g. a global SSO @@ -24,7 +24,7 @@ type Claims struct { } // ScopeTier returns the capability grade — the last colon-separated segment of a broker -// scope ("app:cdrop:guest" → "guest", "full" → "full"). A tierless scope is its own tier. +// scope ("app:commilitia-drop:guest" → "guest", "full" → "full"). A tierless scope is its own tier. // Shared by Claims.Tier() and the session-list overlay so the two never diverge. func ScopeTier(scope string) string { if i := strings.LastIndex(scope, ":"); i >= 0 { diff --git a/internal/jwtauth/middleware.go b/internal/jwtauth/middleware.go index 7c6347b..0b84475 100644 --- a/internal/jwtauth/middleware.go +++ b/internal/jwtauth/middleware.go @@ -150,7 +150,7 @@ func bearerToken(r *http.Request) (string, bool) { func unauthorized(w http.ResponseWriter, reason string) { w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Header().Set("WWW-Authenticate", `Bearer realm="cdrop"`) + w.Header().Set("WWW-Authenticate", `Bearer realm="Commilitia Drop"`) w.WriteHeader(http.StatusUnauthorized) _ = json.NewEncoder(w).Encode(map[string]string{ "error": "unauthorized", diff --git a/internal/jwtauth/middleware_test.go b/internal/jwtauth/middleware_test.go index edb9c3b..eea8aa0 100644 --- a/internal/jwtauth/middleware_test.go +++ b/internal/jwtauth/middleware_test.go @@ -38,7 +38,7 @@ func TestMiddleware_ProdReadsAuthHeaders(t *testing.T) { a := New(&config.Config{AuthMode: "prod"}, &fakeDeviceStore{}) r := httptest.NewRequest(http.MethodGet, "/api/me", nil) r.Header.Set("X-Auth-Subject", "user-1") - r.Header.Set("X-Auth-Scope", "app:cdrop:guest") + r.Header.Set("X-Auth-Scope", "app:commilitia-drop:guest") r.Header.Set("X-Auth-Meta", "dev_abc") r.Header.Set("X-Auth-Name", "Alice") r.Header.Set("X-Auth-Roles", "admin, user") @@ -54,7 +54,7 @@ func TestMiddleware_ProdReadsAuthHeaders(t *testing.T) { t.Errorf("claims wrong: %+v", c) } if !c.Guest() { - t.Error("app:cdrop:guest scope should mark Guest()") + t.Error("app:commilitia-drop:guest scope should mark Guest()") } if len(c.Groups) != 2 || c.Groups[0] != "admin" || c.Groups[1] != "user" { t.Errorf("groups: got %v", c.Groups) @@ -78,7 +78,7 @@ func TestMiddleware_TouchesManagedDevice(t *testing.T) { a := New(&config.Config{AuthMode: "prod"}, fs) r := httptest.NewRequest(http.MethodGet, "/api/me", nil) r.Header.Set("X-Auth-Subject", "user-1") - r.Header.Set("X-Auth-Scope", "app:cdrop:full") + r.Header.Set("X-Auth-Scope", "app:commilitia-drop:full") r.Header.Set("X-Auth-Meta", "dev_x") runMiddleware(a, r) if len(fs.touched) != 1 { @@ -133,10 +133,10 @@ func TestClaimsTier(t *testing.T) { tier string guest bool }{ - {"app:cdrop:guest", "guest", true}, - {"app:cdrop:full", "full", false}, + {"app:commilitia-drop:guest", "guest", true}, + {"app:commilitia-drop:full", "full", false}, {"full", "full", false}, - {"app:cdrop", "cdrop", false}, + {"app:commilitia-drop", "commilitia-drop", false}, {"", "", false}, } for _, tc := range cases { diff --git a/ios/CDrop/MacApp/MacLoginView.swift b/ios/CDrop/MacApp/MacLoginView.swift index dfbf654..8bf777b 100644 --- a/ios/CDrop/MacApp/MacLoginView.swift +++ b/ios/CDrop/MacApp/MacLoginView.swift @@ -2,7 +2,8 @@ import SwiftUI // 原生 macOS 登录:以 Commilitia 账户(Auth Broker)登录为主,免相机——ASWebAuthenticationSession -// 弹系统授权窗,回调经 cdrop:// 回到 app(见 BrokerLogin / AuthManager.startBrokerLogin)。不复用 +// 弹系统授权窗,回调经 commilitia-drop:// 回到 app(见 BrokerLogin / +// AuthManager.startBrokerLogin)。不复用 // iOS 的 LoginView(其二维码 / UIImage 路径为 iOS 专属,且没人扫 Mac 屏幕来登录 Mac 自己)。 struct MacLoginView: View { diff --git a/ios/CDrop/REALDEVICE.md b/ios/CDrop/REALDEVICE.md index 90d9bca..d4c37ea 100644 --- a/ios/CDrop/REALDEVICE.md +++ b/ios/CDrop/REALDEVICE.md @@ -1,6 +1,9 @@ -# cdrop iOS 真机分发手册 +# Commilitia Drop iPhone/iPad 真机分发手册 -把 cdrop 装到你自己的 iPhone(开发签名)。代码侧全部就绪并经模拟器验证;真机只差**账号 + 签名**这一层。本手册走 **ASC API Key 自动 provisioning + 全 CLI** 路线——一把 App Store Connect API Key,`-allowProvisioningUpdates` 自动登记连接的设备 + 创建/更新 App ID / App Group / Push / profile / 开发证书,构建装机一条命令,**全程不碰 Xcode GUI、不必手动建 profile**(绕过手动 profile「设备列表只剩 Mac」的坑)。 +把 Commilitia Drop 覆盖安装到已登记的 iPhone 与 iPad(开发签名)。本手册走 +**既有 Apple Development 证书 + ASC API Key provisioning + 全 CLI** 路线:签名身份固定为 +钥匙串内既有证书的 SHA-1;ASC Key 只用于登记设备、更新 App ID/App Group/Push 能力和 profile。 +构建装机一条命令,**全程不碰 Xcode GUI、不新建开发证书、不修改私钥 ACL**。 ## 包名与标识 @@ -15,13 +18,24 @@ ## 前置 - 付费 Apple Developer Program($99/年)。 - 一台 iPhone(iOS 26)+ Mac(Xcode 26 命令行工具)。 -- 一个已登录的 cdrop(网页 / 桌面)——用来扫码批准本机登录。 +- 一个已登录的 Commilitia Drop(网页 / 桌面)——用来扫码批准本机登录。 --- -## A. ASC API Key + .env(核心,一次性) +## A. 既有签名身份 + ASC API Key + .env -`-allowProvisioningUpdates` 凭一把 App Store Connect API Key 替你在门户自动登记设备、建/改 App ID(含 App Groups + Push 能力)、建 App Group、建 profile、建/下开发证书——**不必手动建 profile**。 +先用 CLI 列出当前钥匙串中可用的代码签名身份: + +```sh +security find-identity -v -p codesigning +``` + +找到必须复用的 `Apple Development` 证书,记录行首 40 位 SHA-1。若身份不存在或私钥不可用,停止 +构建并解锁当前登录钥匙串;不要让自动签名生成替代证书。 + +`-allowProvisioningUpdates` 凭 App Store Connect API Key 自动登记设备、建/改 App ID(含 App +Groups + Push 能力)、更新 App Group 与 profile,免去手动 profile 的设备选择问题。签名证书由 +`CDROP_APPLE_DEVELOPMENT_IDENTITY` 固定,不由 provisioning 自行选择。 **生成 API Key**:appstoreconnect.apple.com → **用户和访问** → **集成** → **App Store Connect API** → **团队密钥** → 「+」→ 角色 **Admin**(要能管设备 / profile / 标识符)→ 命名 → 生成 → 下载 `AuthKey_XXXXX.p8`(**仅一次**)。记下三样: @@ -36,11 +50,14 @@ CDROP_TEAM_ID=ABCDE12345 CDROP_ASC_KEY_PATH=/绝对路径/AuthKey_XXXXX.p8 CDROP_ASC_KEY_ID=XXXXXXXXXX CDROP_ASC_ISSUER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +CDROP_APPLE_DEVELOPMENT_IDENTITY=<既有证书的40位SHA-1> ``` `.p8` 放仓库外即可,`CDROP_ASC_KEY_PATH` 指它绝对路径(落本目录则被 `*.p8` 忽略)。 -> 你若已在门户手动建过 App ID / App Group / 证书,自动 provisioning 会复用、不冲突。**「手动建 profile 时设备列表只剩 Mac」**=那台 iPhone 在 **Devices** 里被登记成了 macOS 平台(登记表单 Platform 默认 macOS);自动 provisioning 直接按连接的真机重登记,绕过此坑。 +> 你若已在门户手动建过 App ID / App Group,自动 provisioning 会复用或更新。**「手动建 +> profile 时设备列表只剩 Mac」**=那台 iPhone 在 **Devices** 里被登记成了 macOS 平台(登记 +> 表单 Platform 默认 macOS);自动 provisioning 按连接的真机登记,绕过此坑。 **顺手:APNs Auth Key**(发推送用,与上面 ASC Key 是两码事):门户 **Keys** → + → 勾 **Apple Push Notifications service** → 下载 `AuthKey_YYYYY.p8`(仅一次),记 Key ID + Issuer ID(服务端配,见 C)。 @@ -53,14 +70,19 @@ just ios-devices # 手机插 USB,读 UDID just ios-device <你的设备UDID> # 自动 provisioning 构建 + devicectl 装机 ``` -- `just ios-device` = `xcodegen generate` → `xcodebuild -allowProvisioningUpdates`(凭 .env 的 ASC Key 自动签名 + 登记设备 + 建 profile)→ `xcrun devicectl device install app`。无 Xcode GUI。 +- `just ios-device` 先确认 `.env` 指定的既有证书仍在钥匙串,再执行 `xcodegen generate` → + `xcodebuild -allowProvisioningUpdates`(固定该 SHA-1 签名,ASC Key 更新设备与 profile)→ + `xcrun devicectl device install app`。无 Xcode GUI。 - 首次装机后,iPhone 上首启该开发者 app 即可直接跑(付费 ADP 开发证书,无需手动「信任开发者」)。 +若出现 `errSecInternalComponent`,优先在当前用户的普通 Terminal 解锁登录钥匙串,然后重试原证书。 +不要创建新证书、修改钥匙串 ACL,或改用 Xcode GUI。 + --- ## C. 服务端前置 -1. **引擎可达**:真机上引擎 WebView 加载 `https://drop.commilitia.net/engine.html`——该文件随 cdrop 二进制部署到 prod(`//go:embed` 进 binary,`just docker-image` 含最新 `dist`)。手机要用,prod 须是含本轮改动的最新部署。 +1. **引擎可达**:真机上引擎 WebView 加载 `https://drop.commilitia.net/engine.html`——该文件随 Commilitia Drop 服务端二进制部署到 prod(`//go:embed` 进 binary,`just docker-image` 含最新 `dist`)。手机要用,prod 须是含本轮改动的最新部署。 2. **APNs 真发**:把 A 里那把 **APNs Auth Key** `.p8` 放到服务器,给后端容器配 `CDROP_APNS_KEY_PATH` / `CDROP_APNS_KEY_ID` / `CDROP_APNS_TEAM_ID` / `CDROP_APNS_TOPIC=net.commilitia.Commilitia-Drop` / `CDROP_APNS_ENV`(Xcode 开发构建的 device token 属 **sandbox**,故联调填 `sandbox`)。缺配置则推送惰性关闭,其余功能照常。 3. 本机联调可选:环境变量 `CDROP_ENGINE_URL` 指向可达引擎。 @@ -71,7 +93,7 @@ just ios-device <你的设备UDID> # 自动 provisioning 构建 + devicectl 代码已实现,下列是真机才能验的点: - [ ] 液态玻璃真机渲染(陀螺仪高光 / 动效)。 -- [ ] 扫码登录:本机显码 → 已登录 cdrop 扫码批准 → 进主界面(强制 full/persist)。 +- [ ] 扫码登录:本机显码 → 已登录 Commilitia Drop 扫码批准 → 进主界面(强制 full/persist)。 - [ ] **本地网络权限 + 同内网直连**(R-iOS-6):首次同内网传输弹 `NSLocalNetworkUsageDescription`;授予后 ICE 收 host 候选、走真直连;拒绝则回退中继仍可传。 - [ ] 发送 / 接收:选文件 → 选设备 → 对端收到;对端发来落 Files(Documents)。 - [ ] **大文件流式发送**(R-iOS-4 已实现):发大文件,WebView 内存应有界(按 Range 块拉取,不整文件入内存)。 diff --git a/ios/CDrop/Share/Info.plist b/ios/CDrop/Share/Info.plist index 6e73c41..beff366 100644 --- a/ios/CDrop/Share/Info.plist +++ b/ios/CDrop/Share/Info.plist @@ -13,7 +13,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - $(PRODUCT_NAME) + Commilitia Drop CFBundlePackageType XPC! CFBundleShortVersionString diff --git a/ios/CDrop/Share/ShareViewController.swift b/ios/CDrop/Share/ShareViewController.swift index 66fdd08..f96ce8a 100644 --- a/ios/CDrop/Share/ShareViewController.swift +++ b/ios/CDrop/Share/ShareViewController.swift @@ -59,7 +59,7 @@ final class ShareViewController: UIViewController // (立即 completeRequest 会拆掉扩展进程、可能取消尚未真正开始的唤起)。文件留在收件箱供主 app 接手。 private func openAndFinish() { - if let url = URL(string: "cdrop://share") { openViaResponder(url) } + if let url = URL(string: "commilitia-drop://share") { openViaResponder(url) } DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { [weak self] in self?.extensionContext?.completeRequest(returningItems: nil) diff --git a/ios/CDrop/Sources/Auth/AuthManager.swift b/ios/CDrop/Sources/Auth/AuthManager.swift index 5bbe802..f20679f 100644 --- a/ios/CDrop/Sources/Auth/AuthManager.swift +++ b/ios/CDrop/Sources/Auth/AuthManager.swift @@ -135,7 +135,7 @@ final class AuthManager let verifier = PKCE.randomToken(64) let challenge = PKCE.challenge(for: verifier) let state = PKCE.randomToken(24) - let redirectURI = "cdrop://auth-callback" + let redirectURI = "commilitia-drop://auth-callback" var comp = URLComponents(string: cfg.brokerURL.trimmingTrailingSlash() + "/device/authorize")! comp.queryItems = [ @@ -144,13 +144,13 @@ final class AuthManager URLQueryItem(name: "state", value: state), URLQueryItem(name: "code_challenge", value: challenge), URLQueryItem(name: "code_challenge_method", value: "S256"), - URLQueryItem(name: "description", value: "Commilitia Drop iOS"), + URLQueryItem(name: "description", value: "Commilitia Drop"), ] guard let authURL = comp.url else { throw BrokerLoginError.incompleteConfig } let flow = BrokerAuthFlow() brokerFlow = flow - let callback = try await flow.run(url: authURL, callbackScheme: "cdrop") + let callback = try await flow.run(url: authURL, callbackScheme: "commilitia-drop") brokerFlow = nil if gen != loginGeneration { return } diff --git a/ios/CDrop/Sources/Auth/BrokerLogin.swift b/ios/CDrop/Sources/Auth/BrokerLogin.swift index cebce47..e7e8458 100644 --- a/ios/CDrop/Sources/Auth/BrokerLogin.swift +++ b/ios/CDrop/Sources/Auth/BrokerLogin.swift @@ -8,13 +8,15 @@ import AppKit #endif // 应用内 Broker 登录(device-authorization + PKCE,对齐桌面 desktop/platform/oauth.go)。iOS 无法 -// 跑 loopback 监听,故用 ASWebAuthenticationSession + 自定义 scheme 回调(cdrop://auth-callback): -// 1. GET {broker}/device/authorize?app=cdrop&redirect_uri=cdrop://auth-callback&state&code_challenge&S256 -// 2. 用户在 broker SSO 授权 → 回调 cdrop://auth-callback?code=..&state=.. +// 跑 loopback 监听,故用 ASWebAuthenticationSession + 自定义 scheme 回调 +// (commilitia-drop://auth-callback): +// 1. GET {broker}/device/authorize?app=commilitia-drop&redirect_uri=commilitia-drop://auth-callback&... +// 2. 用户在 broker SSO 授权 → 回调 commilitia-drop://auth-callback?code=..&state=.. // 3. POST {broker}/device/token {code, code_verifier, redirect_uri} → bootstrap 机器令牌 // 4. POST {api}/api/auth/device-session(Bearer bootstrap)→ cdrop 设备会话(带真名 / device_id) // 这样手机上直接输账号登录,无需另一台设备扫码批准。 -// 前置:broker 须为 cdrop 应用注册 redirect_uri cdrop://auth-callback(同桌面 loopback 须注册)。 +// 前置:broker 须为 Commilitia Drop 应用注册 redirect_uri +// commilitia-drop://auth-callback(同桌面 loopback 须注册)。 enum BrokerLoginError: Error { @@ -35,7 +37,8 @@ final class BrokerAuthFlow: NSObject, ASWebAuthenticationPresentationContextProv { private var session: ASWebAuthenticationSession? - // 打开授权页,等回调 URL(cdrop://auth-callback?code=..&state=..)。用户取消 / 失败抛错。 + // 打开授权页,等回调 URL(commilitia-drop://auth-callback?code=..&state=..)。 + // 用户取消 / 失败抛错。 func run(url: URL, callbackScheme: String) async throws -> URL { try await withCheckedThrowingContinuation diff --git a/ios/CDrop/Sources/CDropApp.swift b/ios/CDrop/Sources/CDropApp.swift index 941b5e0..5bf9132 100644 --- a/ios/CDrop/Sources/CDropApp.swift +++ b/ios/CDrop/Sources/CDropApp.swift @@ -72,11 +72,11 @@ struct AppRoot: View { _, uid in if let uid { engine.activateRecords(userId: uid) } } - // Share Extension 深链:cdrop://share 唤起 → 导入收件箱待发文件(RootView 据此弹选设备)。 + // Share Extension 深链:commilitia-drop://share 唤起 → 导入收件箱待发文件。 // 放在 AppRoot 以便登录 / 未登录任一界面都接得到;未登录时文件留收件箱,登录后 onAppear 补扫。 .onOpenURL { url in - guard url.scheme == "cdrop", url.host == "share" else { return } + guard url.scheme == "commilitia-drop", url.host == "share" else { return } engine.loadPendingShares() } // 被其他设备登出的推送到达(AppDelegate 发):前台即时清会话 + 复位引擎 → 回登录页。 diff --git a/ios/CDrop/Sources/Engine/EngineController.swift b/ios/CDrop/Sources/Engine/EngineController.swift index fdab65d..69ce4e3 100644 --- a/ios/CDrop/Sources/Engine/EngineController.swift +++ b/ios/CDrop/Sources/Engine/EngineController.swift @@ -193,7 +193,7 @@ final class EngineController: NSObject private var webView: WKWebView? private let downloads = DownloadManager() - // 待发文件暂存:id → 安全作用域 URL(文件选择器选中的文件)。引擎经 cdrop-file:// + // 待发文件暂存:id → 安全作用域 URL。引擎经 commilitia-drop-file:// // 回取字节(下方 WKURLSchemeHandler 供给)。 private var outgoing: [String: URL] = [:] @@ -271,7 +271,7 @@ final class EngineController: NSObject } // makeWebView:构建离屏 WebView——注入 __CDROP_BOOT__(device_type:"ios")、注册消息 - // 处理器、注册 cdrop-file 自定义 scheme(发送侧给 JS 喂文件字节)。幂等。 + // 处理器、注册 commilitia-drop-file 自定义 scheme(发送侧给 JS 喂文件字节)。幂等。 func makeWebView() -> WKWebView { if let existing = webView { return existing } @@ -297,7 +297,7 @@ final class EngineController: NSObject let cfg = WKWebViewConfiguration() cfg.userContentController = ucc - cfg.setURLSchemeHandler(self, forURLScheme: "cdrop-file") + cfg.setURLSchemeHandler(self, forURLScheme: "commilitia-drop-file") // 迁移到 Auth Broker 后引擎自刷走注入的 refresh_token(POST /api/auth/refresh 代理 // broker),不再依赖 cdrop_session cookie,故无需把 URLSession 的 cookie 同步进 WebView。 @@ -672,7 +672,7 @@ final class EngineController: NSObject webView?.evaluateJavaScript("window.__cdropEngineEvent(\(jsString(name)), \(json));") } - // sendFile:暂存选中文件、把 cdrop-file:// 引用 + 目标设备交给引擎发起传输。 + // sendFile:暂存选中文件、把 commilitia-drop-file:// 引用 + 目标设备交给引擎发起传输。 func sendFile(to target: String, fileURL: URL) { let ref = stageOutgoingFile(fileURL) @@ -687,7 +687,7 @@ final class EngineController: NSObject ]) } - // Share Extension 经 App Group 收件箱交接来的待发文件(经 cdrop://share 唤起后导入)。 + // Share Extension 经 App Group 收件箱交接来的待发文件(经 commilitia-drop://share 唤起后导入)。 // 非空即触发主界面的「选设备发送」弹窗。 var pendingShareFiles: [URL] = [] @@ -730,7 +730,7 @@ final class EngineController: NSObject } // 图库选择(#14):PhotosPicker 给的是 PhotosPickerItem,须先 loadTransferable 取字节,再落 - // 一个临时文件,才能复用既有 cdrop-file scheme 发送链路(按 Range 读盘、整文件不入内存)。 + // 一个临时文件,才能复用既有 commilitia-drop-file scheme 发送链路。 // 返回临时文件 URL(在 temporaryDirectory,系统回收);写盘失败回 nil。 func stagePhotoData(_ data: Data, suggestedName: String) -> URL? { @@ -743,18 +743,18 @@ final class EngineController: NSObject catch { return nil } } - // 暂存待发文件,返回引擎用的 cdrop-file:// 引用。文件选择器给的 URL 是安全作用域 + // 暂存待发文件,返回引擎用的 commilitia-drop-file:// 引用。 // 资源,需 start...Access 才能读。 private func stageOutgoingFile(_ url: URL) -> String { let id = UUID().uuidString _ = url.startAccessingSecurityScopedResource() outgoing[id] = url - return "cdrop-file://\(id)" + return "commilitia-drop-file://\(id)" } // 发送侧按 [start,end) seek 读暂存的待发文件、回 base64(供 bridgeFileSource 经桥取片)。 - // url 形如 cdrop-file://,id 即 stageOutgoingFile 的暂存键。无该文件 / 读失败抛错。 + // url 形如 commilitia-drop-file://,id 即 stageOutgoingFile 的暂存键。 fileprivate func readOutgoingSlice(url: String, start: Int, end: Int) throws -> String { guard let id = URL(string: url)?.host, let fileURL = outgoing[id] @@ -787,7 +787,7 @@ final class EngineController: NSObject return e } - // 把 cdrop-file:// 引用解析回暂存文件的沙盒绝对路径(Go 据此直接读盘发送)。安全作用域 + // 把 commilitia-drop-file:// 引用解析回暂存文件的沙盒绝对路径。 // 访问已在 stageOutgoingFile 开启、持续到 reset,故同进程的 Go 可直接 os.Open 该路径。无对应 // 暂存项返回空串。 private func resolveOutgoingPath(_ ref: String) -> String @@ -871,7 +871,7 @@ extension EngineController: WKScriptMessageHandler end: Self.intOf(payload["end"])) resolve(id: id, ok: true, value: b64) case "p2pStartOutgoing": - // 发起原生发送:filePath 为 cdrop-file://,解析回沙盒绝对路径交引擎直接读盘。引擎 + // 发起原生发送:filePath 为 commilitia-drop-file://,解析回沙盒绝对路径。 // 调用立即返回(libwebrtc 异步),completion 在 offer 已铸+发出后回,经 resolveOnMain 回交。 let engine = nativeTransferEngine() let path = resolveOutgoingPath(payload["filePath"] as? String ?? "") @@ -1133,11 +1133,11 @@ extension EngineController: WKScriptMessageHandler } } -// MARK: - 发送侧自定义 scheme(cdrop-file) +// MARK: - 发送侧自定义 scheme(commilitia-drop-file) extension EngineController: WKURLSchemeHandler { - // 发送侧:原生把暂存的待发文件经 cdrop-file:// 喂给引擎(JS fetch 它得到字节, + // 发送侧:原生把暂存文件经 commilitia-drop-file:// 喂给引擎。 // 见 PLAN §2 / R-iOS-4)。引擎(rangeSource)按块带 `Range` 头拉取——这里 seek 文件 // 仅读该区间、回 206 Partial Content,整文件永不入内存,避大文件 jetsam。无 Range // 头时回退整文件(防御路径;引擎正常总带 Range)。 diff --git a/ios/CDrop/Sources/Engine/LibWebRtcEngine.swift b/ios/CDrop/Sources/Engine/LibWebRtcEngine.swift index 2cf8598..2e6ae6b 100644 --- a/ios/CDrop/Sources/Engine/LibWebRtcEngine.swift +++ b/ios/CDrop/Sources/Engine/LibWebRtcEngine.swift @@ -6,7 +6,7 @@ import WebRTC // 集成(WebKit/libwebrtc 才集成),真机确证架构性不可靠(见 desktop/NATIVE-TRANSFER.md §7)。 // // 线协议与 desktop/engine(pion)及 web/src/features/transfer/p2p.ts 逐字节一致,故可与桌面 pion -// 引擎、浏览器 / 旧 iOS 的 JS 引擎互通:DataChannel "cdrop-file"(ordered),控制帧 meta/done/ack 走 +// 引擎、浏览器 / 旧 iOS 的 JS 引擎互通:DataChannel "commilitia-drop-file"(ordered),控制帧 // 文本帧、文件分片走二进制帧(64KB),信令 payload 形如 {type, sdp?, candidate?}。本类是 session.go + // engine.go 的忠实 Swift 端口——逻辑、水位、ack 语义、收尾顺序一一对应。 // @@ -39,7 +39,7 @@ protocol P2PEngineDelegate: AnyObject private enum Wire { - static let channelName = "cdrop-file" + static let channelName = "commilitia-drop-file" static let chunkSize = 64 * 1024 static let highWatermark: UInt64 = 16 * 1024 * 1024 static let lowWatermark: UInt64 = 4 * 1024 * 1024 @@ -94,7 +94,7 @@ final class LibWebRtcEngine } // StartOutgoing:建 PeerConnection + DataChannel,发 offer,通道就绪后流式发出 filePath。 - // filePath 为本机绝对路径(原生从 cdrop-file:// 解析得到)。completion 在 offer 已铸并 + // filePath 为本机绝对路径(原生从 commilitia-drop-file:// 解析得到)。completion 在 offer 已铸并 // setLocalDescription 完成后回(或出错时回错误),对齐 JS 侧信令链 seed 语义(见 p2pIos.ts chain)。 func startOutgoing(sessionId: String, peerName: String, filePath: String, iceServersJSON: String, completion: @escaping (Error?) -> Void) diff --git a/ios/CDrop/Sources/Info.plist b/ios/CDrop/Sources/Info.plist index 431a915..f2c03c2 100644 --- a/ios/CDrop/Sources/Info.plist +++ b/ios/CDrop/Sources/Info.plist @@ -23,7 +23,7 @@ en CFBundleName - $(PRODUCT_NAME) + Commilitia Drop CFBundlePackageType APPL CFBundleShortVersionString @@ -35,7 +35,7 @@ net.commilitia.Commilitia-Drop CFBundleURLSchemes - cdrop + commilitia-drop @@ -50,7 +50,7 @@ NSBonjourServices - _cdrop._tcp + _commilitia-drop._tcp NSCameraUsageDescription Commilitia Drop 使用相机扫描二维码登录新设备。 diff --git a/ios/CDrop/Sources/LocalNetworkPermission.swift b/ios/CDrop/Sources/LocalNetworkPermission.swift index a73a43b..f6dc482 100644 --- a/ios/CDrop/Sources/LocalNetworkPermission.swift +++ b/ios/CDrop/Sources/LocalNetworkPermission.swift @@ -3,10 +3,11 @@ import Network // 触发本地网络权限弹窗(#5)。iOS 14+ 默认拒绝 App 访问本地网络;未授权时 WKWebView 内的 // WebRTC 收集不到 host 候选 → 同内网也退回 srflx / relay(慢、非直连)。WKWebView 自身不会 -// 触发该权限请求,须由宿主 App 主动发起一次本地网络访问。这里启动一个对 _cdrop._tcp 的 +// 触发该权限请求,须由宿主 App 主动发起一次本地网络访问。这里启动一个对 +// _commilitia-drop._tcp 的 // NWBrowser 浏览即触发系统弹窗;用户允许后,后续 RTCPeerConnection 才能收集本地 host / mDNS // 候选实现直连。仅需触发一次;保持 browser 存活以维持授权态。project.yml 已声明 -// NSLocalNetworkUsageDescription + NSBonjourServices(_cdrop._tcp)。 +// NSLocalNetworkUsageDescription + NSBonjourServices(_commilitia-drop._tcp)。 enum LocalNetworkPermission { private static var browser: NWBrowser? @@ -18,7 +19,7 @@ enum LocalNetworkPermission guard browser == nil else { return } let params = NWParameters() params.includePeerToPeer = true - let b = NWBrowser(for: .bonjour(type: "_cdrop._tcp", domain: nil), using: params) + let b = NWBrowser(for: .bonjour(type: "_commilitia-drop._tcp", domain: nil), using: params) b.browseResultsChangedHandler = { _, _ in } browser = b b.start(queue: .main) diff --git a/ios/CDrop/Sources/Resources/i18n/en-US.json b/ios/CDrop/Sources/Resources/i18n/en-US.json index 4e09af4..5e2f161 100644 --- a/ios/CDrop/Sources/Resources/i18n/en-US.json +++ b/ios/CDrop/Sources/Resources/i18n/en-US.json @@ -97,7 +97,7 @@ "qr.show.nameHint": "Shown to your other devices. ASCII characters only.", "qr.show.generate": "Generate code", "qr.show.scanTitle": "Scan to approve", - "qr.show.howto1": "Open the camera or cdrop on another signed-in device.", + "qr.show.howto1": "Open the camera or Commilitia Drop on another signed-in device.", "qr.show.howto2": "Scan the code above and approve this device.", "qr.show.waiting": "Waiting for approval…", "qr.show.approved": "Approved — signing you in…", @@ -189,11 +189,11 @@ "notify.transfer.failedTitle": "Transfer failed", "settings.account.title": "Account", "settings.account.signOut": "Sign out", - "settings.desktop.title": "Desktop", + "settings.desktop.title": "App settings", "settings.desktop.clipboardSync": "Auto-sync clipboard", "settings.desktop.clipboardSyncHint": "Copies are uploaded to the cloud, and clipboard updates from your other devices are applied here.", "settings.desktop.launchAtLogin": "Launch at login", - "settings.desktop.launchAtLoginHint": "Start cdrop in the background after you sign in (lives in the menu bar).", + "settings.desktop.launchAtLoginHint": "Start Commilitia Drop in the background after you sign in (lives in the menu bar or system tray).", "settings.desktop.downloadDir": "Download folder", "settings.desktop.downloadDirHint": "Received files are saved here. Leave unset to use the system Downloads folder.", "settings.desktop.downloadDirChoose": "Change…", @@ -292,7 +292,7 @@ "errors.messageEmpty": "Message is empty", "errors.messageOverflow": "Message exceeds 4 KB; send a file instead", "errors.noReceiver": "No receiver selected", - "errors.devTokenMissing": "Dev mode: VITE_CDROP_DEV_TOKEN is not set in .env.local", + "errors.devTokenMissing": "Dev mode: the development token is not configured in .env.local", "errors.noAccessToken": "No access token; user must log in", "errors.clipboardUnavailable": "Browser clipboard API unavailable (requires HTTPS + permission)", "errors.clipboardOverflow": "Content too large; exceeds {{max}} byte limit", diff --git a/ios/CDrop/Sources/Resources/i18n/zh-CN.json b/ios/CDrop/Sources/Resources/i18n/zh-CN.json index a28c048..ee68f4d 100644 --- a/ios/CDrop/Sources/Resources/i18n/zh-CN.json +++ b/ios/CDrop/Sources/Resources/i18n/zh-CN.json @@ -97,7 +97,7 @@ "qr.show.nameHint": "此名称会展示给你的其他设备,仅可使用 ASCII 字符。", "qr.show.generate": "生成二维码", "qr.show.scanTitle": "用手机扫码批准", - "qr.show.howto1": "在另一台已登录的设备上打开相机或 cdrop。", + "qr.show.howto1": "在另一台已登录的设备上打开相机或 Commilitia Drop。", "qr.show.howto2": "扫描上方二维码,按提示批准此设备。", "qr.show.waiting": "等待批准…", "qr.show.approved": "已批准,正在进入…", @@ -189,11 +189,11 @@ "notify.transfer.failedTitle": "传输失败", "settings.account.title": "账号", "settings.account.signOut": "退出登录", - "settings.desktop.title": "桌面", + "settings.desktop.title": "应用设置", "settings.desktop.clipboardSync": "剪贴板自动同步", "settings.desktop.clipboardSyncHint": "复制即上传到云端,并接收其他设备的剪贴板更新。", "settings.desktop.launchAtLogin": "开机自启", - "settings.desktop.launchAtLoginHint": "登录系统后自动在后台启动 cdrop(菜单栏常驻)。", + "settings.desktop.launchAtLoginHint": "登录系统后自动在后台启动 Commilitia Drop(常驻菜单栏或系统托盘)。", "settings.desktop.downloadDir": "下载目录", "settings.desktop.downloadDirHint": "接收到的文件保存到此目录;留空则使用系统下载目录。", "settings.desktop.downloadDirChoose": "更改…", @@ -292,7 +292,7 @@ "errors.messageEmpty": "消息为空", "errors.messageOverflow": "消息超过4 KB;请改用文件传输", "errors.noReceiver": "未选择接收方", - "errors.devTokenMissing": "Dev模式:未在.env.local设置VITE_CDROP_DEV_TOKEN", + "errors.devTokenMissing": "Dev 模式:未在 .env.local 配置开发令牌", "errors.noAccessToken": "无访问令牌:用户必须先登录", "errors.clipboardUnavailable": "浏览器不支持剪贴板API(需HTTPS与权限)", "errors.clipboardOverflow": "内容过大,超过 {{max}} 字节上限", diff --git a/ios/CDrop/Sources/Resources/i18n/zh-TW.json b/ios/CDrop/Sources/Resources/i18n/zh-TW.json index 9a82128..643c734 100644 --- a/ios/CDrop/Sources/Resources/i18n/zh-TW.json +++ b/ios/CDrop/Sources/Resources/i18n/zh-TW.json @@ -97,7 +97,7 @@ "qr.show.nameHint": "此名稱會顯示給你的其他裝置,僅可使用 ASCII 字元。", "qr.show.generate": "產生 QR 碼", "qr.show.scanTitle": "用手機掃碼批准", - "qr.show.howto1": "在另一部已登入的裝置上開啟相機或 cdrop。", + "qr.show.howto1": "在另一部已登入的裝置上開啟相機或 Commilitia Drop。", "qr.show.howto2": "掃描上方 QR 碼,依提示批准此裝置。", "qr.show.waiting": "等待批准…", "qr.show.approved": "已批准,正在進入…", @@ -189,11 +189,11 @@ "notify.transfer.failedTitle": "傳輸失敗", "settings.account.title": "帳號", "settings.account.signOut": "登出", - "settings.desktop.title": "桌面", + "settings.desktop.title": "應用程式設定", "settings.desktop.clipboardSync": "剪貼簿自動同步", "settings.desktop.clipboardSyncHint": "複製後即上傳雲端,並接收其他裝置的剪貼簿更新。", "settings.desktop.launchAtLogin": "開機自動啟動", - "settings.desktop.launchAtLoginHint": "登入系統後自動在背景啟動 cdrop(常駐選單列)。", + "settings.desktop.launchAtLoginHint": "登入系統後自動在背景啟動 Commilitia Drop(常駐選單列或系統匣)。", "settings.desktop.downloadDir": "下載資料夾", "settings.desktop.downloadDirHint": "接收到的檔案會儲存至此資料夾;留空則使用系統下載資料夾。", "settings.desktop.downloadDirChoose": "變更…", @@ -292,7 +292,7 @@ "errors.messageEmpty": "訊息為空", "errors.messageOverflow": "訊息超過4 KB;請改用檔案傳輸", "errors.noReceiver": "未選擇接收方", - "errors.devTokenMissing": "Dev模式:未在.env.local設定VITE_CDROP_DEV_TOKEN", + "errors.devTokenMissing": "Dev 模式:未在 .env.local 設定開發權杖", "errors.noAccessToken": "無存取權杖:使用者必須先登入", "errors.clipboardUnavailable": "瀏覽器不支援剪貼簿API(需HTTPS與權限)", "errors.clipboardOverflow": "內容過大,超過 {{max}} 位元組上限", diff --git a/ios/CDrop/Sources/RootView.swift b/ios/CDrop/Sources/RootView.swift index 37f641a..66613dd 100644 --- a/ios/CDrop/Sources/RootView.swift +++ b/ios/CDrop/Sources/RootView.swift @@ -80,7 +80,7 @@ struct RootView: View .opacity(0) .allowsHitTesting(false) } - // Share Extension 交接:cdrop://share 唤起 / 前台扫到收件箱待发文件 → 直入发送预览(与手动 + // Share Extension 交接:commilitia-drop://share 唤起 / 前台扫到收件箱待发文件。 // 选文件同一表单 SendComposeSheet),预填分享的文件,选目标设备发送。onSend 走 sendShares // (把收件箱文件 move 到私有暂存再发),完成后 pendingShareFiles 清空 → 表单自动收起。 .onAppear { engine.loadPendingShares() } diff --git a/ios/CDrop/Support/macOS/App-Info.plist b/ios/CDrop/Support/macOS/App-Info.plist index ff6c112..b3b6e24 100644 --- a/ios/CDrop/Support/macOS/App-Info.plist +++ b/ios/CDrop/Support/macOS/App-Info.plist @@ -31,7 +31,7 @@ net.commilitia.Commilitia-Drop CFBundleURLSchemes - cdrop + commilitia-drop @@ -41,7 +41,7 @@ 26.0 NSBonjourServices - _cdrop._tcp + _commilitia-drop._tcp NSLocalNetworkUsageDescription Commilitia Drop 需要访问本地网络以发现同内网设备并建立直连传输。 diff --git a/ios/CDrop/Widgets/Info.plist b/ios/CDrop/Widgets/Info.plist index 29cf963..73163c8 100644 --- a/ios/CDrop/Widgets/Info.plist +++ b/ios/CDrop/Widgets/Info.plist @@ -13,7 +13,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - $(PRODUCT_NAME) + Commilitia Drop CFBundlePackageType XPC! CFBundleShortVersionString diff --git a/ios/CDrop/project.yml b/ios/CDrop/project.yml index d00d310..a0a6581 100644 --- a/ios/CDrop/project.yml +++ b/ios/CDrop/project.yml @@ -45,14 +45,15 @@ targets: path: Sources/Info.plist properties: CFBundleDisplayName: Commilitia Drop + CFBundleName: Commilitia Drop # 品牌字标字体 Fredoka SemiBold(Shared/Fonts/,随 bundle 打包),用于 Wordmark(与 web 一致)。 UIAppFonts: - Fredoka-SemiBold.ttf - # cdrop:// URL scheme:Share Extension 交接后经 cdrop://share 深链唤起主 app 选设备发送。 + # commilitia-drop:// URL scheme:Share Extension 交接后唤起主 app 选设备发送。 CFBundleURLTypes: - CFBundleURLName: net.commilitia.Commilitia-Drop CFBundleURLSchemes: - - cdrop + - commilitia-drop # 声明支持的语言,使系统控件(EditButton 的「编辑」、滑动删除的「删除」等系统字符 # 串)跟随设备语言本地化,而非永远英文。开发区域设简体。 CFBundleDevelopmentRegion: zh-Hans @@ -88,7 +89,7 @@ targets: NSAllowsLocalNetworking: true NSLocalNetworkUsageDescription: "Commilitia Drop 需要访问本地网络以发现同内网设备并建立直连传输。" NSBonjourServices: - - "_cdrop._tcp" + - "_commilitia-drop._tcp" NSCameraUsageDescription: "Commilitia Drop 使用相机扫描二维码登录新设备。" # 图库选择:从相册选取图片 / 视频发送(PhotosPicker,见 RootView #14)。 NSPhotoLibraryUsageDescription: "Commilitia Drop 需要访问照片库,以便从相册选取图片或视频发送给其他设备。" @@ -115,6 +116,7 @@ targets: path: Share/Info.plist properties: CFBundleDisplayName: Commilitia Drop + CFBundleName: Commilitia Drop # 品牌字标字体(分享引导界面的 Wordmark 用),随扩展 bundle 打包 + 注册。 UIAppFonts: - Fredoka-SemiBold.ttf @@ -145,6 +147,7 @@ targets: path: Widgets/Info.plist properties: CFBundleDisplayName: Commilitia Drop + CFBundleName: Commilitia Drop NSExtension: NSExtensionPointIdentifier: com.apple.widgetkit-extension settings: @@ -212,15 +215,15 @@ targets: - zh-Hans - zh-Hant - en - # ASWebAuthenticationSession 的 OAuth 回调经 cdrop:// scheme 回到 app(同 iOS)。 + # ASWebAuthenticationSession 的 OAuth 回调经 commilitia-drop:// scheme 回到 app。 CFBundleURLTypes: - CFBundleURLName: net.commilitia.Commilitia-Drop CFBundleURLSchemes: - - cdrop + - commilitia-drop # 本地网络:libwebrtc 收集 host 候选、同内网直连须本地网络权限(macOS 15+ 亦弹窗)。 NSLocalNetworkUsageDescription: "Commilitia Drop 需要访问本地网络以发现同内网设备并建立直连传输。" NSBonjourServices: - - "_cdrop._tcp" + - "_commilitia-drop._tcp" settings: base: PRODUCT_BUNDLE_IDENTIFIER: net.commilitia.Commilitia-Drop diff --git a/ios/PARITY.md b/ios/PARITY.md index 3f96e9d..25d2c41 100644 --- a/ios/PARITY.md +++ b/ios/PARITY.md @@ -1,4 +1,4 @@ -# cdrop web ↔ iOS 同步检查单 +# Commilitia Drop Web ↔ iOS 同步检查单 > 机制,不是清单快照。web(React)与 iOS(SwiftUI)视图层**有意分叉**(液态玻璃决定不可能像素一致)。一致性**优先靠强约束同步**(单一真源、机械消费、漂移不可能,见 `ios/PLAN.md` §5);本文件**只装强约束管不住、需人判定的残余**。 > 关联:`ios/PLAN.md` 决策 D / §5。 @@ -56,7 +56,7 @@ | 2026-06-24 | 建档(iOS 未开工) | — | 初始:架构 A 定、引擎 / i18n / 品牌 / API 四强约束确立,检查单建立 | | 2026-06-24 | 引擎入口 net/ios.ts + engine/main.ts(未提交) | — | web 侧无头引擎入口 + iOS 桥 + 接收 sink 接入 + Vite 多入口;tsc / build 双过 | | 2026-06-24 | — | ios/CDrop/ scaffold(未提交) | iOS 原生壳:液态玻璃 TabView + 离屏引擎 WebView 宿主 + 桥 + 落盘管理器;编过 iphonesimulator26.5 | -| 2026-06-24 | — | ios/CDrop/ 登录+发送(未提交) | 扫码登录新设备侧(AuthManager + LoginView,对 prod qr/start 验通显码)+ 发送路径(cdrop-file scheme 供文件 + 文件选择器)+ 会话注入引擎 boot | +| 2026-06-24 | — | ios/CDrop/ 登录+发送(未提交) | 扫码登录新设备侧(AuthManager + LoginView,对 prod qr/start 验通显码)+ 发送路径(commilitia-drop-file scheme 供文件 + 文件选择器)+ 会话注入引擎 boot | | 2026-06-24 | i18n locales + scripts/emit-i18n.mjs(未提交) | ios/CDrop i18n(未提交) | **强约束②落地**:web locales 加 `ios.*` 键 → `emit-i18n.mjs` 产出 JSON → iOS `t()` 读(loader I18n.swift)。用户可见**去代号 cdrop**(品牌用 `app.brand`=Commilitia Drop),显示名改 Commilitia Drop。**改 locale 后须重跑 `node web/scripts/emit-i18n.mjs`**(已记入脚本头) | | 2026-06-24 | engine/main.ts 加 presence 推送 + 初始快照(未提交) | RootView / EngineController / AuthManager 真数据绑定(未提交) | **A 任务**:引擎订阅 `store.devices` 推 `presence` 事件 + boot 后补发 presence/transfers 初始快照;原生 `EngineController` 解码进 `@Observable` devices/transfers/history;`RootView` 三屏全替 demo→真数据 + 新增 `TransferDetailView`(TODO #5)。i18n 加 `ios.detail.*` / `ios.settings.account|user|logout` / `ios.transfer.empty|incoming|outgoing` / `ios.send.noDevices` / `ios.devices.empty`,传输态文案复用共享 `transfer.state|phase|mode|bytesRate`。iOS `t()` 加 `{{name}}` 插值变体对齐 web。Keychain 会话持久化 + 设置页登出(闭合 TODO #4)。web tsc/build + xcodebuild iphonesimulator26.5 双过,模拟器三屏空态截图验。**已部署 prod**:`engine.html` → `engine-BB8iav7K.js`(含 presence 推送),真会话下即生效 | | 2026-06-24 | engine/main.ts:transfers 推送节流 ~3Hz + toWire 加 ice 摘要(未提交,**待部署**) | EngineController 手动容忍解析 + ICE 字段;RootView 详情页 ICE 行 + 收到文件视图 + 设备名编辑 + 设备计数;DownloadManager listFiles/delete;新 DeviceNameStore / QuickLookPreview;project.yml 加文件共享键 + 品牌化权限串(未提交) | **首轮真机测试反馈修复**:① 速度数字 ~10Hz 闪 → 节流活跃传输推送到 3Hz(iOS-only,亦降 P2P 接收主线程抖动);② 设备列表空 → 原生改手动容忍解析(严格 JSONDecoder 遇 bool/int 失配整组失败的隐患),加「已知设备」计数诊断;③ 收到文件无处开 / Files 不可见 / 阻塞发送 → `UIFileSharingEnabled`+`LSSupportsOpeningDocumentsInPlace`(Documents 已是落地目录)+ 应用内「收到的文件」视图(QuickLook 预览 / 系统分享 / 转发在线设备 / 删除);④ 设备名不可改 → `DeviceNameStore`(UserDefaults)+ 设置页编辑框(后端令牌烤名、改名下次登录生效);⑤ P2P <100KB/s(relay 1MB/s)→ 详情页暴露选中 ICE 候选对诊断是否 TURN 中继(不盲改共享 p2p.ts 水位)。i18n 加 `ios.files.*`/`ios.settings.deviceName|deviceNameNote|deviceCount`,ICE 行复用 `transfer.debug.*`。web 双过 + xcodebuild 编过 + 模拟器设置 / 传输屏截图验。**已部署 prod**:`engine.html` → `engine-BC6mZ9zP.js`(含节流 + ice 摘要) | diff --git a/ios/PLAN.md b/ios/PLAN.md index 89921ce..c8193da 100644 --- a/ios/PLAN.md +++ b/ios/PLAN.md @@ -1,4 +1,4 @@ -# cdrop iOS 客户端实施计划 +# Commilitia Drop iOS 客户端实施计划 > 原生 SwiftUI 界面(液态玻璃)· 离屏无头 WebView 复用传输引擎 · 控制中心剪贴板 + Share Extension + APNs > 状态:**架构已定(2026-06-24 修订)**,账号无关部分即刻可开工;真机 / 推送 / 扩展实测硬等 Apple Developer 账号。 @@ -60,7 +60,7 @@ - **无头 WebView 此后只跑编排**:信令转发 / 会话 / presence / 消息 / 状态机 / relay(`hub.ts` + `transfer.ts`),**不再承载 P2P 数据面**。决策 A 的“原生壳 + 无头引擎”仍成立,只是引擎的职责收窄到编排层。 - **缘由(U1→U2 翻案)**:先试 gomobile+pion(与桌面同引擎,U1),真机 POC **证伪**——Go raw BSD socket 不与 iOS Network framework 集成(连接时好时坏 + 中途断流)。按闸退到 **libwebrtc.framework**(U2,stasel/WebRTC M149 经 SPM、pin `exactVersion 149.0.0`)。完整决策依据与实测证据见 `desktop/NATIVE-TRANSFER.md` §7/§8。 - **收益**:libwebrtc 原生集成 iOS 网络栈 + DcSCTP 默认 ~5MB rwnd——摆脱 WebKit 写死的 256KB 接收窗(R-iOS-3 的吞吐残差)对数据面的钳制,连通性与吞吐一并根治。 -- **互通靠线协议逐字节对齐**:`LibWebRtcEngine` 是 `desktop/engine` 的 `engine.go`/`session.go` 线协议的忠实 Swift 端口——DataChannel `cdrop-file`(ordered)、`meta`/二进制分片(64KB)/`done`/`ack` 帧、16MB/4MB 水位背压、ack 追平完成、不冲突落盘。故 iOS↔桌面(pion)、iOS↔浏览器(JS)三端互通。 +- **互通靠线协议逐字节对齐**:`LibWebRtcEngine` 是 `desktop/engine` 的 `engine.go`/`session.go` 线协议的忠实 Swift 端口——DataChannel `commilitia-drop-file`(ordered)、`meta`/二进制分片(64KB)/`done`/`ack` 帧、16MB/4MB 水位背压、ack 追平完成、不冲突落盘。故 iOS↔桌面(pion)、iOS↔浏览器(JS)三端互通。 - **桥与 JS 路由零改动**:`IOS_NATIVE` 开关、`p2pIos.ts`/`net/ios.ts` 桥协议原样;只在 Swift 侧新增一条原生传输 RPC(`startOutgoing`/`startIncoming`/`handleSignal`/`cancel`,completion-based,经 `resolveOnMain` 跳主线程 resolve)。**iOS 定向的引擎配置绝不回流 `desktop/engine` 共享包**。 - **验证**:环回端到端(两个引擎同进程交叉连、20MB 经完整线协议传输后逐字节比对)✅;模拟器全 app 编译 ✅;web typecheck/build ✅。真机 / 真网吞吐互通为剩余闸(须 deploy prod 让设备拿到 `IOS_NATIVE=true` 引擎 + 真机装机,二者 Touch ID 门控)。 @@ -93,7 +93,7 @@ Share Extension 落法(据上):**抓文件 → 写 App Group 容器 → - **引擎 → 原生**:① 单向通知(`postMessage({ notify, payload })`):`ready` / `transfers` / `transferDone` / `sendStarted` / `error`;② 请求 / 响应 RPC(`postMessage({ id, method, payload })`,原生回调 `window.__cdropEngineResolve(id, ok, value)`):接收落盘 `saveDownload` / `beginDownload` / `appendDownload` / `finalizeDownload` / `abortDownload`(data 为 base64,复用桌面同形)。剪贴板不走此桥(决策 C 纯原生)。 - **复用边界**:接收侧字节桥沿用桌面已验证的 base64 分批、有界内存(iOS 上限更小,64MB/16MB 避 jetsam);**发送侧经 `url` 取回文件为唯一新实现点**——当前整文件 fetch 进内存(小 / 中文件可行),大文件按 slice 向原生拉取是真机优化点(R-iOS-4)。 - **已落地(web 侧,账号无关、`tsc`/`vite build` 双过、对现网零影响)**:`net/ios.ts`(桥 + `isIOSShell` + 接收封装 + `notifyNative`)、`engine/main.ts`(无头入口)、`incomingSink.ts`(通用混合槽路由 iOS)、`engine.html` + `vite.config.ts` 多入口。 -- **加载源(关键)**:无头引擎**必须从安全上下文加载**——`RTCPeerConnection` 在非安全 origin(如自定义 `cdrop://` scheme,WebKit 视作 non-secure context)下可能被禁。故引擎页走 `https://`:线上 `drop.commilitia.net` 或应用内本地 https 服务的静态 bundle;**自定义 scheme 仅用于 `WKURLSchemeHandler` 给 JS 流式喂文件字节**,不可用它直载引擎页。见 R-iOS-3。 +- **加载源(关键)**:无头引擎**必须从安全上下文加载**——`RTCPeerConnection` 在非安全 origin(如自定义 `commilitia-drop://` scheme,WebKit 视作 non-secure context)下可能被禁。故引擎页走 `https://`:线上 `drop.commilitia.net` 或应用内本地 https 服务的静态 bundle;**自定义 scheme 仅用于 `WKURLSchemeHandler` 给 JS 流式喂文件字节**,不可用它直载引擎页。见 R-iOS-3。 --- @@ -124,7 +124,7 @@ Share Extension 落法(据上):**抓文件 → 写 App Group 容器 → | **I6** APNs | 服务端通道(§3)+ 原生注册;不含剪贴板 | `.p8` + 真机硬等账号 | | **I7** 打磨与分发 | 后台窗口;图标 / 启动屏(复用品牌资产);旁加载分发 | 硬等账号 | -> **实现状态(2026-06-27)**:I1/I2/I3/I4/I5/I6 的**代码**全部落地——发送端流式(R-iOS-4,FileSource + 原生 Range,整文件不进 WebView 内存);后台续传(BGContinuedProcessingTask);APNs(后端 `internal/apns` ES256 + 原生注册经引擎桥);Share Extension(App Group 收件箱 + `cdrop://share` 深链);控制中心两控件(**专用 broker 设备会话**,纯原生 REST,不与引擎抢 refresh 轮换)。模拟器验 + ultracode 多 agent 审查修讫(0 HIGH,修 2 MED + 7 LOW)。**I0 账号 / 证书 + 真机签名 / 真发推送 / 旁加载分发**仍账号门控——手册 `ios/CDrop/REALDEVICE.md`(ASC API Key 自动 provisioning + `just ios-device` 全 CLI 装机,`-allowProvisioningUpdates` 自动登记设备 / 建 profile)。包名 `net.commilitia.Commilitia-Drop`,App Group `group.net.commilitia.Commilitia-Drop`。 +> **实现状态(2026-06-27)**:I1/I2/I3/I4/I5/I6 的**代码**全部落地——发送端流式(R-iOS-4,FileSource + 原生 Range,整文件不进 WebView 内存);后台续传(BGContinuedProcessingTask);APNs(后端 `internal/apns` ES256 + 原生注册经引擎桥);Share Extension(App Group 收件箱 + `commilitia-drop://share` 深链);控制中心两控件(**专用 broker 设备会话**,纯原生 REST,不与引擎抢 refresh 轮换)。模拟器验 + ultracode 多 agent 审查修讫(0 HIGH,修 2 MED + 7 LOW)。**I0 账号 / 证书 + 真机签名 / 真发推送 / 旁加载分发**仍账号门控——手册 `ios/CDrop/REALDEVICE.md`(ASC API Key 自动 provisioning + `just ios-device` 全 CLI 装机,`-allowProvisioningUpdates` 自动登记设备 / 建 profile)。包名 `net.commilitia.Commilitia-Drop`,App Group `group.net.commilitia.Commilitia-Drop`。 > **更新(2026-06-28)**:① **P2P 数据面改原生 libwebrtc**(决策 F;完整依据 `desktop/NATIVE-TRANSFER.md` §7/§8)——已真机装机、用户反馈“传输总体良好”,真网吞吐互通为剩余闸;② **UX 改造**(发送预览直入 / 消息按设备会话 / 删除粒度,§10);③ **品牌与字体系统**(logo + Orbit Gothic / Maple Mono / Fredoka,§11)。 diff --git a/web/engine.html b/web/engine.html index b3acbc0..2009a86 100644 --- a/web/engine.html +++ b/web/engine.html @@ -6,7 +6,7 @@ 传输引擎。仅由原生 iOS 壳的离屏 WKWebView 从安全源(https)加载;原生须在本 模块执行前经 WKUserScript 注入 window.__CDROP_BOOT__(session / device_name / api_base / device_type:"ios"),见 src/engine/main.ts。 --> - cdrop engine + Commilitia Drop diff --git a/web/index.html b/web/index.html index b1e2b41..513e233 100644 --- a/web/index.html +++ b/web/index.html @@ -19,8 +19,8 @@ - - + + diff --git a/web/public/site.webmanifest b/web/public/site.webmanifest index 2c8e5c2..191212d 100644 --- a/web/public/site.webmanifest +++ b/web/public/site.webmanifest @@ -1,7 +1,7 @@ { "id": "/", "name": "Commilitia Drop", - "short_name": "cdrop", + "short_name": "Commilitia Drop", "description": "跨设备剪贴板同步与点对点文件传输", "lang": "zh-CN", "start_url": "/", diff --git a/web/public/sw.js b/web/public/sw.js index 151f5fc..7ce08ec 100644 --- a/web/public/sw.js +++ b/web/public/sw.js @@ -72,7 +72,7 @@ self.addEventListener("push", (event) => try { data = event.data ? event.data.json() : {}; } catch (_e) { data = {}; } - const title = data.title || "cdrop"; + const title = data.title || "Commilitia Drop"; const options = { body: data.body || "", tag: data.tag || undefined, // same tag replaces a stale notification in place diff --git a/web/src/engine/main.ts b/web/src/engine/main.ts index ee7a1c7..b1b6ffd 100644 --- a/web/src/engine/main.ts +++ b/web/src/engine/main.ts @@ -179,7 +179,7 @@ function subscribeStore(): void }); } -// handleSendFile:原生把待发文件经 payload.url(cdrop-file://)暴露。R-iOS-4:用 +// handleSendFile:原生把待发文件经 payload.url(commilitia-drop-file://)暴露。R-iOS-4:用 // bridgeFileSource 经桥按块取片(原生据 [start,end) seek 文件、回 base64),整文件永不整体进 // WebView 内存——大文件不再有 jetsam 风险,传输层 p2p / relay 透明复用。改走桥而非跨 origin // fetch,绕开自定义 scheme 的 CORS 拦截(否则发送在读文件即失败)。 @@ -520,7 +520,7 @@ function boot(): void { // 非 iOS 壳(误加载 engine.html)——保持惰性,不触发任何网络 / 引擎行为。 // eslint-disable-next-line no-console - console.warn("cdrop engine: not running inside the iOS shell, idle"); + console.warn("Commilitia Drop engine: not running inside the iOS shell, idle"); return; } diff --git a/web/src/features/transfer/p2p.ts b/web/src/features/transfer/p2p.ts index d4366a8..3b2758b 100644 --- a/web/src/features/transfer/p2p.ts +++ b/web/src/features/transfer/p2p.ts @@ -24,7 +24,7 @@ import type { FileSource } from "./source"; // - iceServers: pulled from lib/iceServers (Cloudflare Realtime TURN over // TLS when CDROP_CF_TURN_* is configured; STUN-only fallback otherwise). // Refreshed on login by RootLayout. -// - DataChannel "cdrop-file", ordered:true +// - DataChannel "commilitia-drop-file", ordered:true // - chunk size 64 KiB, backpressure thresholds 16 MiB / 4 MiB // - 接收端流式写 OPFS(lib/incomingSink),避免 iOS Safari 内存压力 // - control frames as JSON strings (head meta + tail done), payload bytes binary @@ -36,7 +36,7 @@ import type { FileSource } from "./source"; // Same-LAN Restricted Cone NAT 排查依据:iceCandidatePoolSize=4 预热候选; // 收集本/远端 host/mdns/srflx/prflx/relay 计数 + 选中候选对类型推到 store, // 让 UI 暴露"实际通过哪条路径连上"(host:host = 真直连,relay:* = 走了 TURN)。 -const CHANNEL_NAME = "cdrop-file"; +const CHANNEL_NAME = "commilitia-drop-file"; const CHUNK_SIZE = 64 * 1024; const HIGH_WATERMARK = 16 * 1024 * 1024; const LOW_WATERMARK = 4 * 1024 * 1024; @@ -971,7 +971,7 @@ export function p2pStartOutgoing( { return nativeStartOutgoing(sessionId, receiverName, src.path, src.size); } - // iOS 无头壳:源带 cdrop-file:// 引用(path)即走 libwebrtc 原生数据面(Swift 侧解析回沙盒 + // iOS 无头壳:源带 commilitia-drop-file:// 引用(path)即走 libwebrtc 原生数据面(Swift 侧解析回沙盒 // 绝对路径直接读盘)。IOS_NATIVE 关闭时回退下方 JS Session(WebKit WebRTC)。 if (IOS_NATIVE && isIOSShell() && src.path) { diff --git a/web/src/features/transfer/p2pIos.ts b/web/src/features/transfer/p2pIos.ts index 59a4437..cd7eb06 100644 --- a/web/src/features/transfer/p2pIos.ts +++ b/web/src/features/transfer/p2pIos.ts @@ -235,7 +235,7 @@ function register(s: NativeSess): void idByPeer.set(s.peerName, s.sessionId); } -// nativeIosStartOutgoing 经 Go 引擎发送 filePath(cdrop-file:// 引用,原生解析回沙盒绝对 +// nativeIosStartOutgoing 经 Go 引擎发送 filePath(commilitia-drop-file:// 引用,原生解析回沙盒绝对 // 路径)到 peerName。total 用于进度事件的分母(store 的 bytesTransferred 仍是 UI 真值源)。 export function nativeIosStartOutgoing( sessionId: string, peerName: string, filePath: string, total: number, diff --git a/web/src/features/transfer/source.ts b/web/src/features/transfer/source.ts index 0471c71..c193bd6 100644 --- a/web/src/features/transfer/source.ts +++ b/web/src/features/transfer/source.ts @@ -35,7 +35,7 @@ export function fileSource(file: File): FileSource // rangeSource:经 HTTP Range 向原生 WKURLSchemeHandler 惰性拉取文件片(iOS 大文件流式发送)。 // 每次 slice 只取 [start, end) 一段,WebView 常驻内存上限即单块大小(p2p 的 -// READ_BLOCK_SIZE = 4 MiB)。url 形如 `cdrop-file://`;原生据 Range 头 seek 文件、 +// READ_BLOCK_SIZE = 4 MiB)。url 形如 `commilitia-drop-file://`;原生据 Range 头 seek 文件、 // 回 206 Partial Content 仅含该区间字节。 export function rangeSource( url: string, diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index 1790a61..c4268c8 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -123,7 +123,7 @@ export const enUS: Partial = { "qr.show.nameHint": "Shown to your other devices. ASCII characters only.", "qr.show.generate": "Generate code", "qr.show.scanTitle": "Scan to approve", - "qr.show.howto1": "Open the camera or cdrop on another signed-in device.", + "qr.show.howto1": "Open the camera or Commilitia Drop on another signed-in device.", "qr.show.howto2": "Scan the code above and approve this device.", "qr.show.waiting": "Waiting for approval…", "qr.show.approved": "Approved — signing you in…", @@ -225,11 +225,11 @@ export const enUS: Partial = { "notify.transfer.failedTitle": "Transfer failed", "settings.account.title": "Account", "settings.account.signOut": "Sign out", - "settings.desktop.title": "Desktop", + "settings.desktop.title": "App settings", "settings.desktop.clipboardSync": "Auto-sync clipboard", "settings.desktop.clipboardSyncHint": "Copies are uploaded to the cloud, and clipboard updates from your other devices are applied here.", "settings.desktop.launchAtLogin": "Launch at login", - "settings.desktop.launchAtLoginHint": "Start cdrop in the background after you sign in (lives in the menu bar).", + "settings.desktop.launchAtLoginHint": "Start Commilitia Drop in the background after you sign in (lives in the menu bar or system tray).", "settings.desktop.downloadDir": "Download folder", "settings.desktop.downloadDirHint": "Received files are saved here. Leave unset to use the system Downloads folder.", "settings.desktop.downloadDirChoose": "Change…", @@ -344,7 +344,7 @@ export const enUS: Partial = { "errors.messageOverflow": "Message exceeds 4 KB; send a file instead", "errors.noReceiver": "No receiver selected", "errors.devTokenMissing": - "Dev mode: VITE_CDROP_DEV_TOKEN is not set in .env.local", + "Dev mode: the development token is not configured in .env.local", "errors.noAccessToken": "No access token; user must log in", "errors.clipboardUnavailable": "Browser clipboard API unavailable (requires HTTPS + permission)", "errors.clipboardOverflow": "Content too large; exceeds {{max}} byte limit", diff --git a/web/src/i18n/locales/zh-CN.ts b/web/src/i18n/locales/zh-CN.ts index 8540905..543e864 100644 --- a/web/src/i18n/locales/zh-CN.ts +++ b/web/src/i18n/locales/zh-CN.ts @@ -120,7 +120,7 @@ export const zhCN = { "qr.show.nameHint": "此名称会展示给你的其他设备,仅可使用 ASCII 字符。", "qr.show.generate": "生成二维码", "qr.show.scanTitle": "用手机扫码批准", - "qr.show.howto1": "在另一台已登录的设备上打开相机或 cdrop。", + "qr.show.howto1": "在另一台已登录的设备上打开相机或 Commilitia Drop。", "qr.show.howto2": "扫描上方二维码,按提示批准此设备。", "qr.show.waiting": "等待批准…", "qr.show.approved": "已批准,正在进入…", @@ -221,11 +221,11 @@ export const zhCN = { "notify.transfer.failedTitle": "传输失败", "settings.account.title": "账号", "settings.account.signOut": "退出登录", - "settings.desktop.title": "桌面", + "settings.desktop.title": "应用设置", "settings.desktop.clipboardSync": "剪贴板自动同步", "settings.desktop.clipboardSyncHint": "复制即上传到云端,并接收其他设备的剪贴板更新。", "settings.desktop.launchAtLogin": "开机自启", - "settings.desktop.launchAtLoginHint": "登录系统后自动在后台启动 cdrop(菜单栏常驻)。", + "settings.desktop.launchAtLoginHint": "登录系统后自动在后台启动 Commilitia Drop(常驻菜单栏或系统托盘)。", "settings.desktop.downloadDir": "下载目录", "settings.desktop.downloadDirHint": "接收到的文件保存到此目录;留空则使用系统下载目录。", "settings.desktop.downloadDirChoose": "更改…", @@ -340,7 +340,7 @@ export const zhCN = { "errors.messageOverflow": "消息超过4 KB;请改用文件传输", "errors.noReceiver": "未选择接收方", "errors.devTokenMissing": - "Dev模式:未在.env.local设置VITE_CDROP_DEV_TOKEN", + "Dev 模式:未在 .env.local 配置开发令牌", "errors.noAccessToken": "无访问令牌:用户必须先登录", "errors.clipboardUnavailable": "浏览器不支持剪贴板API(需HTTPS与权限)", "errors.clipboardOverflow": "内容过大,超过 {{max}} 字节上限", diff --git a/web/src/i18n/locales/zh-TW.ts b/web/src/i18n/locales/zh-TW.ts index 089f5dd..52857ec 100644 --- a/web/src/i18n/locales/zh-TW.ts +++ b/web/src/i18n/locales/zh-TW.ts @@ -124,7 +124,7 @@ export const zhTW: Partial = { "qr.show.nameHint": "此名稱會顯示給你的其他裝置,僅可使用 ASCII 字元。", "qr.show.generate": "產生 QR 碼", "qr.show.scanTitle": "用手機掃碼批准", - "qr.show.howto1": "在另一部已登入的裝置上開啟相機或 cdrop。", + "qr.show.howto1": "在另一部已登入的裝置上開啟相機或 Commilitia Drop。", "qr.show.howto2": "掃描上方 QR 碼,依提示批准此裝置。", "qr.show.waiting": "等待批准…", "qr.show.approved": "已批准,正在進入…", @@ -225,11 +225,11 @@ export const zhTW: Partial = { "notify.transfer.failedTitle": "傳輸失敗", "settings.account.title": "帳號", "settings.account.signOut": "登出", - "settings.desktop.title": "桌面", + "settings.desktop.title": "應用程式設定", "settings.desktop.clipboardSync": "剪貼簿自動同步", "settings.desktop.clipboardSyncHint": "複製後即上傳雲端,並接收其他裝置的剪貼簿更新。", "settings.desktop.launchAtLogin": "開機自動啟動", - "settings.desktop.launchAtLoginHint": "登入系統後自動在背景啟動 cdrop(常駐選單列)。", + "settings.desktop.launchAtLoginHint": "登入系統後自動在背景啟動 Commilitia Drop(常駐選單列或系統匣)。", "settings.desktop.downloadDir": "下載資料夾", "settings.desktop.downloadDirHint": "接收到的檔案會儲存至此資料夾;留空則使用系統下載資料夾。", "settings.desktop.downloadDirChoose": "變更…", @@ -344,7 +344,7 @@ export const zhTW: Partial = { "errors.messageOverflow": "訊息超過4 KB;請改用檔案傳輸", "errors.noReceiver": "未選擇接收方", "errors.devTokenMissing": - "Dev模式:未在.env.local設定VITE_CDROP_DEV_TOKEN", + "Dev 模式:未在 .env.local 設定開發權杖", "errors.noAccessToken": "無存取權杖:使用者必須先登入", "errors.clipboardUnavailable": "瀏覽器不支援剪貼簿API(需HTTPS與權限)", "errors.clipboardOverflow": "內容過大,超過 {{max}} 位元組上限", diff --git a/web/src/net/ios.ts b/web/src/net/ios.ts index 9d2d951..99718e2 100644 --- a/web/src/net/ios.ts +++ b/web/src/net/ios.ts @@ -196,7 +196,7 @@ export async function abortIncomingDownloadIOS(sessionId: string): Promise const BRIDGE_SLICE_MAX = 512 * 1024; // readFileSliceIOS:经桥让原生按 [start, end) seek 读暂存的待发文件、回 base64,再解回字节。 -// 取代原先「fetch(cdrop-file://) + Range」——引擎在 https origin 下跨 origin fetch 自定义 scheme +// 取代原先「fetch(commilitia-drop-file://) + Range」——引擎在 https origin 下跨 origin fetch 自定义 scheme // 被 CORS 拦死(Range 头触发预检、scheme handler 无 CORS 头)→ 发送在读文件即失败。改走桥与 // 接收落盘同形,绕开 CORS;整文件仍永不整体进 WebView 内存(按需取片)。 async function readFileSliceIOS(url: string, start: number, end: number): Promise> @@ -214,7 +214,7 @@ export function bridgeFileSource(url: string, name: string, size: number, type: name, size, type: type || "application/octet-stream", - // path 带 cdrop-file:// 引用:iOS 原生数据面(gomobile/pion)据此让原生侧解析回沙盒 + // path 带 commilitia-drop-file:// 引用:iOS 原生数据面据此让原生侧解析回沙盒 // 绝对路径、由 Go 直接读盘发送(见 p2pIos.ts / EngineController.p2pStartOutgoing)。JS 收发 // 路径不读 path,故对回退到 JS 引擎的传输无影响(与桌面 nativeFileSource 同范式)。 path: url, @@ -246,7 +246,7 @@ export function bridgeFileSource(url: string, name: string, size: number, type: // 四个入口走 callNative(RPC,原生回 resolve);进度 / 状态 / 出站信令 / 落盘 / 候选对经 // onNativeEvent 反向回来。原生侧契约见 ios/CDrop/Sources/Engine/EngineController.swift。 -// nativeP2PStartOutgoing:发起原生发送。filePath 为 cdrop-file:// 引用,原生解析回沙盒 +// nativeP2PStartOutgoing:发起原生发送。filePath 为 commilitia-drop-file:// 引用,原生解析回沙盒 // 绝对路径后交 Go 直接读盘(不经 base64 桥 / 不整文件进内存)。 export async function nativeP2PStartOutgoing( sessionId: string, peerName: string, filePath: string, iceServersJSON: string, diff --git a/web/src/routes/link_.new.tsx b/web/src/routes/link_.new.tsx index 618ad66..3fd20b9 100644 --- a/web/src/routes/link_.new.tsx +++ b/web/src/routes/link_.new.tsx @@ -262,7 +262,7 @@ function suggestDeviceName(): string else if (ua.includes("Chrome/")) { browser = "Chrome"; } else if (ua.includes("Safari/")) { browser = "Safari"; } - let os = "Desktop"; + let os = "Computer"; if (ua.includes("Mac")) { os = "macOS"; } else if (ua.includes("Windows")) { os = "Windows"; } else if (ua.includes("Linux")) { os = "Linux"; } diff --git a/web/src/routes/setup.tsx b/web/src/routes/setup.tsx index 6d51ff4..a4dd8be 100644 --- a/web/src/routes/setup.tsx +++ b/web/src/routes/setup.tsx @@ -104,7 +104,7 @@ function suggestDefault(): string else if (ua.includes("Chrome/")) { browser = "Chrome"; } else if (ua.includes("Safari/")) { browser = "Safari"; } - let os = "Desktop"; + let os = "Computer"; if (ua.includes("Mac")) { os = "macOS"; } else if (ua.includes("Windows")) { os = "Windows"; } else if (ua.includes("Linux")) { os = "Linux"; } diff --git a/web/src/routes/styleguide.tsx b/web/src/routes/styleguide.tsx index 017f1c4..6de98f1 100644 --- a/web/src/routes/styleguide.tsx +++ b/web/src/routes/styleguide.tsx @@ -31,7 +31,7 @@ function StyleGuide() - cdrop · UI 原子组件总览 + Commilitia Drop · UI 原子组件总览 当前主题: