feat: 统一 Commilitia Drop 全客户端命名与分发

This commit is contained in:
2026-07-31 22:56:10 +08:00
parent f7b0f04c9c
commit c480f0d1c2
77 changed files with 861 additions and 263 deletions
+12 -2
View File
@@ -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)
+41 -16
View File
@@ -45,23 +45,47 @@ wails := `go env GOPATH` + "/bin/wails"
desktop-dev:
cd desktop && {{wails}} dev
# 构建 macOS universal .appdarwin/arm64 + darwin/amd64)。
# 不加 -clean,让 mac/win 产物在 build/bin/ 共存;清理走 desktop-clean。
# 构建 macOS Apple Silicon .appdarwin/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 "缺 .envCDROP_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 侧无 cgodarwin 的
# NSStatusBar/NSPasteboard CGO 走 build tag 排除,WebView2 是纯 Go),不需 mingw。
# 与 mac 同:先手动构建前端再 -swails 自带 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)。前置:付费 ADPApp 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 "缺 .envCDROP_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 "缺 .envCDROP_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 ----
+29 -3
View File
@@ -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
MVPM0M13)已上线 prodOIDC PKCE 接入自建 CasdoorCloudflare Realtime TURN over TLS)。其后进入阶段二,已落地:
- **剪贴板同步**:单条覆写式云剪贴板(短 TTL 限暴露面)+ 文本消息一次性通道
- **桌面客户端**Wails v2macOS universal `.app` + Windows `.exe`)—— 瘦客户端复用 web,业务全走后端 APIrefresh_token 存系统密钥库(信封加密)
- **桌面客户端**Wails v2macOS Apple Silicon `.app` + Windows x64 `.exe`)——
瘦客户端复用 web,业务全走后端 APIrefresh_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 @@ MVPM0M13)已上线 prodOIDC PKCE 接入自建 CasdoorCloudflare 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)
+2 -2
View File
@@ -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:<tier>`cdrop 读末段)。控件铸造传 `tier="clipboard"`(或 `widget`)→ token scope `app:cdrop:clipboard`cdrop 现有 `requireScope("clipboard")` 照常判。**无需新增 `scope` 参数**。
你要的“子会话带缩减 scope(clipboard)”现机制已能给:`tier` 即 scope 后缀(token scope`app:commilitia-drop:<tier>`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)` 上限。
+2 -2
View File
@@ -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")
}
+4 -4
View File
@@ -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 credsGo 拉 /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 拆 pionrelay 由 JS 接管(数据面回到 JS relay,符合「relay 留 JS」)。
@@ -130,7 +130,7 @@ ICE credsGo 拉 /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 门控)。
>
+22 -10
View File
@@ -1,4 +1,4 @@
# cdrop 桌面客户端实施计划
# Commilitia Drop 桌面客户端实施计划
> Wails v2 · macOS 优先 + Windows 已落地
> 状态:**macOS 与 Windows 双端均已实现并 RDP/真机实测通过**——A2 前端复用 + 托盘/菜单栏 + 剪贴板双向同步 + 桌面设置 + session 持久化 + 开机自启(含自启静默驻留)+ 设备类型登记。macOS 见 §10Windows 落地(含「custom-scheme 不支持流式」核心坑与 127.0.0.1 本地代理解法)见 §11。计划已据原生平台研究 sweep`desktop/RESEARCH.md`)校正。
@@ -26,7 +26,9 @@
## 0. 范围、非范围与本轮关键决策
**范围**:常驻桌面客户端,补浏览器做不到的——后台常驻、剪贴板自动监听上传、文件落盘、系统通知、全局快捷键。平台 **macOSApple Silicon + Intel)优先****Windows(x64)保持兼容**(同源、同分支,平台差异用 Go build tag 隔离)。
**范围**:常驻桌面客户端,补浏览器做不到的——后台常驻、剪贴板自动监听上传、文件落盘、
系统通知、全局快捷键。平台 **macOS(仅 Apple Silicon)优先****Windowsx64)保持兼容**
(同源、同分支,平台差异用 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 clientPKCE 时 `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>.app``lipo` 合并 arm64 + amd64)。**不公证、不 DMG、无 Developer ID**。为满足 D3 通知与稳定身份,做 **ad-hoc 签名** `codesign -s - --force --deep <App>.app` + `Info.plist``CFBundleIdentifier`。分发靠手动(右键打开 / 去 quarantine)。
- **entitlements(若 ad-hoc 也带)**:非沙箱只需 `com.apple.security.network.client`purego dlopen 系统 framework **不需** `disable-library-validation`Carbon 热键不需 entitlement。(不要列通知 / 剪贴板 / 热键 entitlement——过度声明。)
### 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**:当前非沙箱瘦客户端不额外声明通知、剪贴板或热键 entitlementpurego
`dlopen` Apple 系统 framework 不需 `disable-library-validation`Carbon 热键也不需要
entitlement。后续引入第三方 dylib 或沙箱时重新评估。
- **Windows**`wails build -platform windows/amd64 [-nsis]` 产出 `.exe` / NSIS 安装器;当前**不签名**SmartScreen 会提示),获代码签名证书后再 `signtool`
- **版本检查**:启动 / 定时拉版本端点,过期提示去下载,**不自更新**。
- **将来获证书**:完整 codesignDeveloper ID+ notarytool + DMG + CI 流程见 `desktop/RESEARCH.md` D6 手册,届时直接套用。
- **CI**D6 后 GitHub Actions 双 runnermacos-latest / windows-latest)跑构建 + 防回归(无证书阶段仅构建 + ad-hoc,不跑公证)
- **操作入口**:完整构建、签名、公证、安装和验收流程见
`docs/client-build-install.md`;底层命令与设计依据见 `desktop/RESEARCH.md` D6 手册
- **CI**:后续 GitHub Actions 双 runnermacos-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 → 不弹窗,直接驻留菜单栏 / 托盘)
+22 -12
View File
@@ -1,19 +1,29 @@
# README
# Commilitia DropWails
## 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)。
+45 -20
View File
@@ -1,6 +1,8 @@
# cdrop 桌面端 D1 前决策就绪简报
# Commilitia Drop 桌面端 D1 前决策就绪简报
> 框架基线:Wails v2.12.0Go 1.23 模块声明,本机 toolchain 1.26.3+ 系统 WebView瘦客户端,业务全走 `drop.commilitia.net`。平台优先级 macOSApple Silicon + Intel universal),Windows x64 兼容。
> 框架基线:Wails v2.12.0Go 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` 指向 exeLinux 走 `.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` 指向 exeLinux 走
`.desktop``x-scheme-handler/commilitia-drop`
- 必须配单实例锁。点 `commilitia-drop://` 时 OS 会重新拉起 app 的“第二个实例”,深链作为命令行参数传入。需要 `options.App.SingleInstanceLock` 把这个 deep link 转发给首实例,否则 token 落在一个马上要退出的临时进程里,拿不到。
- macOS 上单实例锁与深链协作有已知坑(见 wails issue #5089v3 的 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<void>
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, `<!doctype html><html lang="zh-Hans"><head><meta charset="utf-8">`+
`<title>cdrop</title></head><body style="font-family:sans-serif;text-align:center;margin-top:20vh">`+
`<title>Commilitia Drop</title></head><body style="font-family:sans-serif;text-align:center;margin-top:20vh">`+
`<p>%s</p><script>setTimeout(function(){window.close();},800);</script>`+
`</body></html>`, msg)
}
@@ -443,7 +450,8 @@ func writeClosePage(w http.ResponseWriter, ok bool)
## 打包·签名·公证操作手册(D6 补充)
本节只补**具体命令与 CI 配置**;entitlement/公证“会不会被拒”的判断已在前文完成。环境基准:Wails v2.12.0Go),macOS universal 优先 + Windows x64,瘦客户端。
本节只补**具体命令与 CI 配置**;entitlement/公证“会不会被拒”的判断已在前文完成。
环境基准:Wails v2.12.0Go),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/<AppName>.app`universal `.app` bundle`lipo` 已合并 arm64 + amd64 两份 Go 二进制)。`<AppName>` 来自 `wails.json``outputfilename`/项目名。
- 产物:`build/bin/<AppName>.app`Apple Silicon arm64)。`<AppName>` 来自
`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/15runner 上原生满足;不要尝试从 Linux 交叉编译 darwinCGO + macOS SDK 缺失,社区反复确认不可行)。
- 当前只构建 arm64;不要尝试从 Linux 交叉编译 darwinCGO + 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` **无签名参数**,不做 codesignnotarize;签名、公证、DMG 全是构建后的外部步骤。`build/darwin/` 下有 `Info.plist``Info.dev.plist``entitlements.plist` 非自动生成,需手动放进该路径。universal 由 Wails 内部 `lipo` 合并,无需手动 `lipo`
`wails build -platform darwin/arm64` **无签名参数**,不做 codesignnotarize;签名、
公证、DMG 全是构建后的外部步骤。`build/darwin/` 下有 `Info.plist``Info.dev.plist`
`entitlements.plist` 非自动生成,需手动放进该路径。
#### 1.7 本项目已验证的正式 recipe2026-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`;两者 staplevalidate 成功,
`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 #3716https://github.com/wailsapp/wails/issues/3716
- Wails Crossplatform buildGitHub Actions、darwin/universal 矩阵):https://wails.io/docs/guides/crossplatform-build/
- Wails Crossplatform buildGitHub Actions):https://wails.io/docs/guides/crossplatform-build/
- notarytool man pagehttps://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 entitlementhttps://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_cs_disable-library-validation
+8 -5
View File
@@ -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"),
}
}
+1 -1
View File
@@ -29,7 +29,7 @@
<string>Commilitia Drop 使用本地网络发现同内网设备并建立点对点直连传输。</string>
<key>NSBonjourServices</key>
<array>
<string>_cdrop._tcp</string>
<string>_commilitia-drop._tcp</string>
</array>
{{if .Info.FileAssociations}}
<key>CFBundleDocumentTypes</key>
+1 -1
View File
@@ -29,7 +29,7 @@
<string>Commilitia Drop 使用本地网络发现同内网设备并建立点对点直连传输。</string>
<key>NSBonjourServices</key>
<array>
<string>_cdrop._tcp</string>
<string>_commilitia-drop._tcp</string>
</array>
{{if .Info.FileAssociations}}
<key>CFBundleDocumentTypes</key>
+2 -2
View File
@@ -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
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>cdrop-desktop</title>
<title>Commilitia Drop</title>
</head>
<body>
<div id="app"></div>
+2 -2
View File
@@ -8,9 +8,9 @@ import { EventsOn } from "../wailsjs/runtime/runtime";
// Go 换 token → oauth:success / oauth:error 事件回到这里。
document.querySelector("#app")!.innerHTML = `
<main class="login-demo">
<h1>cdrop 桌面端</h1>
<h1>Commilitia Drop</h1>
<p class="status" id="status">检查登录状态……</p>
<button class="btn" id="loginBtn" disabled>登录 cdrop</button>
<button class="btn" id="loginBtn" disabled>登录 Commilitia Drop</button>
<p class="hint">登录会在系统浏览器中打开授权页,完成后自动返回。</p>
</main>
`;
+1 -1
View File
@@ -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,
+8 -1
View File
@@ -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.
+1 -1
View File
@@ -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 == "" {
+6 -6
View File
@@ -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)
}
}
+1 -1
View File
@@ -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.
+2 -2
View File
@@ -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 {
+1 -1
View File
@@ -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
+36 -3
View File
@@ -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.
+2 -2
View File
@@ -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 {
+2 -2
View File
@@ -1,6 +1,6 @@
#import <Network/Network.h>
// 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);
+4
View File
@@ -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
+3
View File
@@ -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).
+33 -8
View File
@@ -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 是稳定的内部 AppUserModelIDWindows 通知设置中的用户可见名称由
// 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,
+5 -5
View File
@@ -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, `<!doctype html><html lang="zh-Hans"><head><meta charset="utf-8"><title>cdrop</title></head>`+
fmt.Fprintf(w, `<!doctype html><html lang="zh-Hans"><head><meta charset="utf-8"><title>Commilitia Drop</title></head>`+
`<body style="font-family:system-ui,sans-serif;text-align:center;margin-top:20vh">`+
`<p>%s</p><script>setTimeout(function(){window.close();},800);</script></body></html>`, msg)
}
+6 -6
View File
@@ -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")
}
+8 -4
View File
@@ -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
}
+1 -1
View File
@@ -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 <stdlib.h>
void cdropStatusBarInstall(const void *iconPNG, int iconLen, const char *title,
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 2 ]]
then
echo "用法:$0 <Commilitia Drop.app> <Commilitia Drop.dmg>" >&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"
+3 -3
View File
@@ -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",
+2 -2
View File
@@ -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
+348
View File
@@ -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`
- iOSiPadOS 登录回调:`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` LaunchAgentBundle 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 私钥或证书导出文件。
- iPhoneiPad 必须复用钥匙串中既有 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`,再 staplevalidate
4. 生成含 `.app``/Applications` 链接的压缩 DMG
5. 对 DMG 签名、单独提交公证并 staplevalidate
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 上交叉构建无需 MinGWWindows 特有通知
和 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 <iPhone-UDID>
just ios-device <iPad-UDID>
```
`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 appDMG | ARM64、Developer ID、hardened runtime、Accepted、staple、Gatekeeper |
| Windows EXE | PE32+ x86-64、SHA-256、唯一文件、Run 项、交互 Session 进程 |
| iPhoneiPad | 既有证书 SHA-1、签名验证、显示名、URL Scheme、Broker 登录、真机传输 |
提交时不加入 `.env``.p8`、构建产物、临时 PowerShell/安装脚本或 Broker 备份。客户端源码和构建
流程由 Git 历史恢复;Auth Broker 配置备份由服务端运维位置单独保留。
+3 -3
View File
@@ -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`
### 分发
+5 -5
View File
@@ -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)
+11 -11
View File
@@ -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"])
}
+2 -2
View File
@@ -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>". Tier() reads the capability grade off the end.
// session is "app:commilitia-drop:<tier>". 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 {
+1 -1
View File
@@ -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",
+6 -6
View File
@@ -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 {
+2 -1
View File
@@ -2,7 +2,8 @@
import SwiftUI
// macOS Commilitia Auth BrokerASWebAuthenticationSession
// cdrop:// app BrokerLogin / AuthManager.startBrokerLogin
// commilitia-drop:// app BrokerLogin /
// AuthManager.startBrokerLogin
// iOS LoginView / UIImage iOS Mac Mac
struct MacLoginView: View
{
+31 -9
View File
@@ -1,6 +1,9 @@
# cdrop iOS 真机分发手册
# Commilitia Drop iPhoneiPad 真机分发手册
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 IDApp GroupPush 能力和 profile。
构建装机一条命令,**全程不碰 Xcode GUI、不新建开发证书、不修改私钥 ACL**。
## 包名与标识
@@ -15,13 +18,24 @@
## 前置
- 付费 Apple Developer Program$99/年)。
- 一台 iPhoneiOS 26+ MacXcode 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 块拉取,不整文件入内存)。
+1 -1
View File
@@ -13,7 +13,7 @@
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<string>Commilitia Drop</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
+1 -1
View File
@@ -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)
+3 -3
View File
@@ -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 }
+8 -5
View File
@@ -8,13 +8,15 @@ import AppKit
#endif
// Broker device-authorization + PKCE desktop/platform/oauth.goiOS
// 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-sessionBearer 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?
// URLcdrop://auth-callback?code=..&state=.. /
// URLcommilitia-drop://auth-callback?code=..&state=..
// /
func run(url: URL, callbackScheme: String) async throws -> URL
{
try await withCheckedThrowingContinuation
+2 -2
View File
@@ -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 +
+13 -13
View File
@@ -193,7 +193,7 @@ final class EngineController: NSObject
private var webView: WKWebView?
private let downloads = DownloadManager()
// id URL cdrop-file://<id>
// id URL commilitia-drop-file://<id>
// 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_tokenPOST /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://<id> +
// sendFile commilitia-drop-file://<id> +
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
}
// #14PhotosPicker 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://<id> URL
// commilitia-drop-file://<id>
// 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>id stageOutgoingFile /
// url commilitia-drop-file://<id>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://<id> Go
// commilitia-drop-file://<id>
// 访 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://<id>
// filePath commilitia-drop-file://<id>
// libwebrtc completion offer + resolveOnMain
let engine = nativeTransferEngine()
let path = resolveOutgoingPath(payload["filePath"] as? String ?? "")
@@ -1133,11 +1133,11 @@ extension EngineController: WKScriptMessageHandler
}
}
// MARK: - schemecdrop-file
// MARK: - schemecommilitia-drop-file
extension EngineController: WKURLSchemeHandler
{
// cdrop-file://<id> JS fetch
// commilitia-drop-file://<id>
// PLAN §2 / R-iOS-4rangeSource `Range` seek
// 206 Partial Content jetsam Range
// 退 Range
@@ -6,7 +6,7 @@ import WebRTC
// WebKit/libwebrtc desktop/NATIVE-TRANSFER.md §7
//
// 线 desktop/enginepion 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://<id> completion offer
// filePath commilitia-drop-file://<id> completion offer
// setLocalDescription JS seed p2pIos.ts chain
func startOutgoing(sessionId: String, peerName: String, filePath: String,
iceServersJSON: String, completion: @escaping (Error?) -> Void)
+3 -3
View File
@@ -23,7 +23,7 @@
<string>en</string>
</array>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<string>Commilitia Drop</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
@@ -35,7 +35,7 @@
<string>net.commilitia.Commilitia-Drop</string>
<key>CFBundleURLSchemes</key>
<array>
<string>cdrop</string>
<string>commilitia-drop</string>
</array>
</dict>
</array>
@@ -50,7 +50,7 @@
</dict>
<key>NSBonjourServices</key>
<array>
<string>_cdrop._tcp</string>
<string>_commilitia-drop._tcp</string>
</array>
<key>NSCameraUsageDescription</key>
<string>Commilitia Drop 使用相机扫描二维码登录新设备。</string>
@@ -3,10 +3,11 @@ import Network
// #5iOS 14+ App 访 WKWebView
// WebRTC host 退 srflx / relayWKWebView
// 宿 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)
+4 -4
View File
@@ -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",
+4 -4
View File
@@ -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}} 字节上限",
+4 -4
View File
@@ -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}} 位元組上限",
+1 -1
View File
@@ -80,7 +80,7 @@ struct RootView: View
.opacity(0)
.allowsHitTesting(false)
}
// Share Extension cdrop://share /
// Share Extension commilitia-drop://share /
// SendComposeSheetonSend sendShares
// move pendingShareFiles
.onAppear { engine.loadPendingShares() }
+2 -2
View File
@@ -31,7 +31,7 @@
<string>net.commilitia.Commilitia-Drop</string>
<key>CFBundleURLSchemes</key>
<array>
<string>cdrop</string>
<string>commilitia-drop</string>
</array>
</dict>
</array>
@@ -41,7 +41,7 @@
<string>26.0</string>
<key>NSBonjourServices</key>
<array>
<string>_cdrop._tcp</string>
<string>_commilitia-drop._tcp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>Commilitia Drop 需要访问本地网络以发现同内网设备并建立直连传输。</string>
+1 -1
View File
@@ -13,7 +13,7 @@
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<string>Commilitia Drop</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
+9 -6
View File
@@ -45,14 +45,15 @@ targets:
path: Sources/Info.plist
properties:
CFBundleDisplayName: Commilitia Drop
CFBundleName: Commilitia Drop
# 品牌字标字体 Fredoka SemiBoldShared/Fonts/,随 bundle 打包),用于 Wordmark(与 web 一致)。
UIAppFonts:
- Fredoka-SemiBold.ttf
# cdrop:// URL schemeShare Extension 交接后经 cdrop://share 深链唤起主 app 选设备发送。
# commilitia-drop:// URL schemeShare 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
+2 -2
View File
@@ -1,4 +1,4 @@
# cdrop web ↔ iOS 同步检查单
# Commilitia Drop Web ↔ iOS 同步检查单
> 机制,不是清单快照。webReact)与 iOSSwiftUI)视图层**有意分叉**(液态玻璃决定不可能像素一致)。一致性**优先靠强约束同步**(单一真源、机械消费、漂移不可能,见 `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.tstransfers 推送节流 ~3Hz + toWire 加 ice 摘要(未提交,**待部署** | EngineController 手动容忍解析 + ICE 字段;RootView 详情页 ICE 行 + 收到文件视图 + 设备名编辑 + 设备计数;DownloadManager listFiles/delete;新 DeviceNameStore / QuickLookPreviewproject.yml 加文件共享键 + 品牌化权限串(未提交) | **首轮真机测试反馈修复**:① 速度数字 ~10Hz 闪 → 节流活跃传输推送到 3Hz(iOS-only,亦降 P2P 接收主线程抖动);② 设备列表空 → 原生改手动容忍解析(严格 JSONDecoder 遇 bool/int 失配整组失败的隐患),加「已知设备」计数诊断;③ 收到文件无处开 / Files 不可见 / 阻塞发送 → `UIFileSharingEnabled`+`LSSupportsOpeningDocumentsInPlace`Documents 已是落地目录)+ 应用内「收到的文件」视图(QuickLook 预览 / 系统分享 / 转发在线设备 / 删除);④ 设备名不可改 → `DeviceNameStore`UserDefaults)+ 设置页编辑框(后端令牌烤名、改名下次登录生效);⑤ P2P <100KB/srelay 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 摘要) |
+4 -4
View File
@@ -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**U2stasel/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://` schemeWebKit 视作 non-secure context)下可能被禁。故引擎页走 `https://`:线上 `drop.commilitia.net` 或应用内本地 https 服务的静态 bundle**自定义 scheme 仅用于 `WKURLSchemeHandler` 给 JS 流式喂文件字节**,不可用它直载引擎页。见 R-iOS-3。
- **加载源(关键)**:无头引擎**必须从安全上下文加载**——`RTCPeerConnection` 在非安全 origin(如自定义 `commilitia-drop://` schemeWebKit 视作 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-4FileSource + 原生 Range,整文件不进 WebView 内存);后台续传(BGContinuedProcessingTask);APNs(后端 `internal/apns` ES256 + 原生注册经引擎桥);Share ExtensionApp 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-4FileSource + 原生 Range,整文件不进 WebView 内存);后台续传(BGContinuedProcessingTask);APNs(后端 `internal/apns` ES256 + 原生注册经引擎桥);Share ExtensionApp 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)。
+1 -1
View File
@@ -6,7 +6,7 @@
传输引擎。仅由原生 iOS 壳的离屏 WKWebView 从安全源(https)加载;原生须在本
模块执行前经 WKUserScript 注入 window.__CDROP_BOOT__session / device_name /
api_base / device_type:"ios"),见 src/engine/main.ts。 -->
<title>cdrop engine</title>
<title>Commilitia Drop</title>
</head>
<body>
<script type="module" src="/src/engine/main.ts"></script>
+2 -2
View File
@@ -19,8 +19,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="cdrop" />
<meta name="application-name" content="cdrop" />
<meta name="apple-mobile-web-app-title" content="Commilitia Drop" />
<meta name="application-name" content="Commilitia Drop" />
<!-- 预取品牌 logo:登录跳转回来后首页 header 立即用到;早取 + 后端一周缓存,
避免那一刻才发起请求导致的闪烁重载。 -->
<link rel="preload" as="image" href="/logo-mark.png" />
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "/",
"name": "Commilitia Drop",
"short_name": "cdrop",
"short_name": "Commilitia Drop",
"description": "跨设备剪贴板同步与点对点文件传输",
"lang": "zh-CN",
"start_url": "/",
+1 -1
View File
@@ -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
+2 -2
View File
@@ -179,7 +179,7 @@ function subscribeStore(): void
});
}
// handleSendFile:原生把待发文件经 payload.urlcdrop-file://<id>)暴露。R-iOS-4:用
// handleSendFile:原生把待发文件经 payload.urlcommilitia-drop-file://<id>)暴露。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;
}
+3 -3
View File
@@ -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
// - 接收端流式写 OPFSlib/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://<id> 引用(path)即走 libwebrtc 原生数据面(Swift 侧解析回沙盒
// iOS 无头壳:源带 commilitia-drop-file://<id> 引用(path)即走 libwebrtc 原生数据面(Swift 侧解析回沙盒
// 绝对路径直接读盘)。IOS_NATIVE 关闭时回退下方 JS SessionWebKit WebRTC)。
if (IOS_NATIVE && isIOSShell() && src.path)
{
+1 -1
View File
@@ -235,7 +235,7 @@ function register(s: NativeSess): void
idByPeer.set(s.peerName, s.sessionId);
}
// nativeIosStartOutgoing 经 Go 引擎发送 filePathcdrop-file://<id> 引用,原生解析回沙盒绝对
// nativeIosStartOutgoing 经 Go 引擎发送 filePathcommilitia-drop-file://<id> 引用,原生解析回沙盒绝对
// 路径)到 peerName。total 用于进度事件的分母(store 的 bytesTransferred 仍是 UI 真值源)。
export function nativeIosStartOutgoing(
sessionId: string, peerName: string, filePath: string, total: number,
+1 -1
View File
@@ -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://<id>`;原生据 Range 头 seek 文件、
// READ_BLOCK_SIZE = 4 MiB)。url 形如 `commilitia-drop-file://<id>`;原生据 Range 头 seek 文件、
// 回 206 Partial Content 仅含该区间字节。
export function rangeSource(
url: string,
+4 -4
View File
@@ -123,7 +123,7 @@ export const enUS: Partial<TranslationDict> = {
"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<TranslationDict> = {
"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<TranslationDict> = {
"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",
+4 -4
View File
@@ -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}} 字节上限",
+4 -4
View File
@@ -124,7 +124,7 @@ export const zhTW: Partial<TranslationDict> = {
"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<TranslationDict> = {
"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<TranslationDict> = {
"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}} 位元組上限",
+3 -3
View File
@@ -196,7 +196,7 @@ export async function abortIncomingDownloadIOS(sessionId: string): Promise<void>
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<Uint8Array<ArrayBuffer>>
@@ -214,7 +214,7 @@ export function bridgeFileSource(url: string, name: string, size: number, type:
name,
size,
type: type || "application/octet-stream",
// path 带 cdrop-file://<id> 引用:iOS 原生数据面gomobile/pion据此让原生侧解析回沙盒
// path 带 commilitia-drop-file://<id> 引用: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:
// 四个入口走 callNativeRPC,原生回 resolve);进度 / 状态 / 出站信令 / 落盘 / 候选对经
// onNativeEvent 反向回来。原生侧契约见 ios/CDrop/Sources/Engine/EngineController.swift。
// nativeP2PStartOutgoing:发起原生发送。filePath 为 cdrop-file://<id> 引用,原生解析回沙盒
// nativeP2PStartOutgoing:发起原生发送。filePath 为 commilitia-drop-file://<id> 引用,原生解析回沙盒
// 绝对路径后交 Go 直接读盘(不经 base64 桥 / 不整文件进内存)。
export async function nativeP2PStartOutgoing(
sessionId: string, peerName: string, filePath: string, iceServersJSON: string,
+1 -1
View File
@@ -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"; }
+1 -1
View File
@@ -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"; }
+1 -1
View File
@@ -31,7 +31,7 @@ function StyleGuide()
<Container size="lg" py="lg">
<Stack gap="lg">
<Group justify="space-between" align="baseline">
<Title order={3} fw={600}>cdrop · UI </Title>
<Title order={3} fw={600}>Commilitia Drop · UI </Title>
<Group gap="xs">
<Text size="xs" c="dimmed"></Text>
<Tabs<ThemeMode>