feat: 统一 Commilitia Drop 全客户端命名与分发
This commit is contained in:
+12
-2
@@ -15,8 +15,8 @@ CDROP_DEV_TOKEN=replace-with-32-byte-random-base64
|
|||||||
# broker 公开源(浏览器全局 SSO 登录跳转 / 原生客户端设备授权流的目标)。留空则
|
# broker 公开源(浏览器全局 SSO 登录跳转 / 原生客户端设备授权流的目标)。留空则
|
||||||
# /api/auth/login 与原生登录不可用。
|
# /api/auth/login 与原生登录不可用。
|
||||||
# CDROP_BROKER_PUBLIC_URL=https://sso.your-domain.example
|
# CDROP_BROKER_PUBLIC_URL=https://sso.your-domain.example
|
||||||
# 本应用在 broker apps 注册表里的 key(默认 cdrop)。
|
# 本应用在 broker apps 注册表里的 key(默认 commilitia-drop)。
|
||||||
# CDROP_BROKER_APP=cdrop
|
# CDROP_BROKER_APP=commilitia-drop
|
||||||
# 部署公开源(拼扫码 QR 链接 / CSRF Origin 校验 / Web Push 默认联系标识)。
|
# 部署公开源(拼扫码 QR 链接 / CSRF Origin 校验 / Web Push 默认联系标识)。
|
||||||
# CDROP_PUBLIC_URL=https://drop.your-domain.example
|
# CDROP_PUBLIC_URL=https://drop.your-domain.example
|
||||||
# CDROP_TURN_URL=stun:your-stun.example:3478
|
# 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。
|
# 申请:dash.cloudflare.com → Realtime → TURN → "Create TURN App",拿到 Key ID 与 API Token。
|
||||||
# CDROP_CF_TURN_KEY_ID=
|
# CDROP_CF_TURN_KEY_ID=
|
||||||
# CDROP_CF_TURN_API_TOKEN=
|
# 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)
|
||||||
|
|||||||
@@ -45,23 +45,47 @@ wails := `go env GOPATH` + "/bin/wails"
|
|||||||
desktop-dev:
|
desktop-dev:
|
||||||
cd desktop && {{wails}} dev
|
cd desktop && {{wails}} dev
|
||||||
|
|
||||||
# 构建 macOS universal .app(含 darwin/arm64 + darwin/amd64)。
|
# 构建 macOS Apple Silicon .app(darwin/arm64;不支持 Intel)。
|
||||||
# 不加 -clean,让 mac/win 产物在 build/bin/ 共存;清理走 desktop-clean。
|
# 每次清理 build/bin:带空格的 macOS .app 若残留在 Go 模块内,会被后续跨平台
|
||||||
|
# 绑定生成误判为包路径;同一输出槽也只保留本次平台的一份客户端。
|
||||||
#
|
#
|
||||||
# ⚠️ 先手动构建前端再 `wails build -s`(跳过 wails 自带前端步骤):wails build 的
|
# ⚠️ 先手动构建前端再 `wails build -s`(跳过 wails 自带前端步骤):wails build 的
|
||||||
# frontend:build 在本仓库结构下不可靠(不实际重建 web/ → 嵌入旧 JS)。显式构建
|
# frontend:build 在本仓库结构下不可靠(不实际重建 web/ → 嵌入旧 JS)。显式构建
|
||||||
# web → desktop/frontend/dist,再用 -s 让 go:embed 嵌入这份新 dist。
|
# web → desktop/frontend/dist,再用 -s 让 go:embed 嵌入这份新 dist。
|
||||||
desktop-build-mac:
|
desktop-build-mac:
|
||||||
cd web && npm run build -- --outDir ../desktop/frontend/dist --emptyOutDir
|
cd web && npm run build -- --outDir ../desktop/frontend/dist --emptyOutDir
|
||||||
cd desktop && {{wails}} build -s -platform darwin/universal
|
cd desktop && {{wails}} build -clean -s -platform darwin/arm64
|
||||||
|
|
||||||
|
# 正式 macOS 分发:复用钥匙串中的 Developer ID Application 证书,对 ARM64 Wails app
|
||||||
|
# 启用 hardened runtime + 时间戳签名,先公证并装订 app,再生成、签名、公证和装订 DMG。
|
||||||
|
# 公证使用与 iOS provisioning 共用的 ASC API Key;所有身份和密钥路径从 .env 读取。
|
||||||
|
desktop-dist-mac: desktop-build-mac
|
||||||
|
[ -n "$CDROP_DEVID_IDENTITY" ] && [ -n "$CDROP_ASC_KEY_PATH" ] && [ -n "$CDROP_ASC_KEY_ID" ] && [ -n "$CDROP_ASC_ISSUER_ID" ] || { echo "缺 .env(CDROP_DEVID_IDENTITY / CDROP_ASC_KEY_PATH / CDROP_ASC_KEY_ID / CDROP_ASC_ISSUER_ID)"; exit 1; }
|
||||||
|
[ -f "$CDROP_ASC_KEY_PATH" ]
|
||||||
|
security find-identity -v -p codesigning | rg -F "$CDROP_DEVID_IDENTITY"
|
||||||
|
codesign --force --deep --options runtime --timestamp --sign "$CDROP_DEVID_IDENTITY" 'desktop/build/bin/Commilitia Drop.app'
|
||||||
|
codesign --verify --deep --strict --verbose=2 'desktop/build/bin/Commilitia Drop.app'
|
||||||
|
rm -f 'desktop/build/bin/Commilitia Drop.app.zip' 'desktop/build/bin/Commilitia Drop.dmg'
|
||||||
|
ditto -c -k --keepParent 'desktop/build/bin/Commilitia Drop.app' 'desktop/build/bin/Commilitia Drop.app.zip'
|
||||||
|
xcrun notarytool submit 'desktop/build/bin/Commilitia Drop.app.zip' --key "$CDROP_ASC_KEY_PATH" --key-id "$CDROP_ASC_KEY_ID" --issuer "$CDROP_ASC_ISSUER_ID" --wait
|
||||||
|
xcrun stapler staple 'desktop/build/bin/Commilitia Drop.app'
|
||||||
|
xcrun stapler validate 'desktop/build/bin/Commilitia Drop.app'
|
||||||
|
bash desktop/scripts/make-dmg.sh 'desktop/build/bin/Commilitia Drop.app' 'desktop/build/bin/Commilitia Drop.dmg'
|
||||||
|
codesign --force --timestamp --sign "$CDROP_DEVID_IDENTITY" 'desktop/build/bin/Commilitia Drop.dmg'
|
||||||
|
xcrun notarytool submit 'desktop/build/bin/Commilitia Drop.dmg' --key "$CDROP_ASC_KEY_PATH" --key-id "$CDROP_ASC_KEY_ID" --issuer "$CDROP_ASC_ISSUER_ID" --wait
|
||||||
|
xcrun stapler staple 'desktop/build/bin/Commilitia Drop.dmg'
|
||||||
|
xcrun stapler validate 'desktop/build/bin/Commilitia Drop.dmg'
|
||||||
|
spctl --assess --type execute --verbose=2 'desktop/build/bin/Commilitia Drop.app'
|
||||||
|
spctl --assess --type open --context context:primary-signature --verbose=2 'desktop/build/bin/Commilitia Drop.dmg'
|
||||||
|
rm -f 'desktop/build/bin/Commilitia Drop.app.zip'
|
||||||
|
|
||||||
# 交叉编译 Windows amd64 .exe。macOS 原生即可——Windows 侧无 cgo(darwin 的
|
# 交叉编译 Windows amd64 .exe。macOS 原生即可——Windows 侧无 cgo(darwin 的
|
||||||
# NSStatusBar/NSPasteboard CGO 走 build tag 排除,WebView2 是纯 Go),不需 mingw。
|
# NSStatusBar/NSPasteboard CGO 走 build tag 排除,WebView2 是纯 Go),不需 mingw。
|
||||||
# 与 mac 同:先手动构建前端再 -s(wails 自带 frontend:build 在本仓库不可靠)。
|
# 与 mac 同:先手动构建前端再 -s(wails 自带 frontend:build 在本仓库不可靠)。
|
||||||
# 产物 build/bin/desktop.exe;真测试经 RDP 到 Windows 机器跑。
|
# 产物 build/bin/Commilitia Drop.exe;真测试经 RDP 到 Windows 机器跑。
|
||||||
desktop-build-win:
|
desktop-build-win:
|
||||||
cd web && npm run build -- --outDir ../desktop/frontend/dist --emptyOutDir
|
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/ 产物。
|
# 清空 build/bin/ 产物。
|
||||||
desktop-clean:
|
desktop-clean:
|
||||||
@@ -106,9 +130,10 @@ mac-prep:
|
|||||||
cd ios/CDrop && xcodebuild -project CommilitiaDrop.xcodeproj -scheme CommilitiaDropMac -sdk macosx -derivedDataPath build/DD-mac -resolvePackageDependencies
|
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
|
bash ios/CDrop/scripts/patch-macos-webrtc-headers.sh ios/CDrop/build/DD-mac
|
||||||
|
|
||||||
# macOS 原生客户端本地编译(ad-hoc 签名,无需 Apple 账号)。当前为脚手架(MacApp/ 导航骨架 +
|
# macOS 原生迁移候选的本地编译(ad-hoc 签名,无需 Apple 账号),仅供开发验证,不是正式
|
||||||
# 菜单栏占位);引擎 / 复用视图 / 桌面功能随后续阶段并入。产物 build/DD-mac/Build/Products/Debug/
|
# 客户端或分发入口。正式 macOS 客户端仍由 desktop-build-mac 构建;只有完成功能对等、三语、
|
||||||
# CommilitiaDropMac.app(本地打开自测)。
|
# 签名扩展与真机验收后才允许切换。候选产物位于 build/DD-mac/Build/Products/Debug/
|
||||||
|
# CommilitiaDropMac.app。
|
||||||
mac-build: mac-prep
|
mac-build: mac-prep
|
||||||
cd ios/CDrop && xcodebuild -project CommilitiaDrop.xcodeproj -scheme CommilitiaDropMac -sdk macosx -destination 'platform=macOS' -derivedDataPath build/DD-mac -disableAutomaticPackageResolution build
|
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
|
mac-test: mac-prep
|
||||||
cd ios/CDrop && xcodebuild test -project CommilitiaDrop.xcodeproj -scheme CommilitiaDropMac -sdk macosx -destination 'platform=macOS' -derivedDataPath build/DD-mac -disableAutomaticPackageResolution
|
cd ios/CDrop && xcodebuild test -project CommilitiaDrop.xcodeproj -scheme CommilitiaDropMac -sdk macosx -destination 'platform=macOS' -derivedDataPath build/DD-mac -disableAutomaticPackageResolution
|
||||||
|
|
||||||
# 真机构建 + 装机(自动 provisioning,经 ASC API Key)。前置:付费 ADP;App ID 的 App Groups /
|
# 真机构建 + 装机(自动 provisioning,经 ASC API Key)。必须把
|
||||||
# Push 能力(门户建好或自动建);根目录 gitignore 的 .env 配齐:CDROP_TEAM_ID / CDROP_ASC_KEY_PATH
|
# CDROP_APPLE_DEVELOPMENT_IDENTITY 设为钥匙串内既有 Apple Development 证书的 SHA-1;构建前
|
||||||
# (.p8 路径)/ CDROP_ASC_KEY_ID / CDROP_ASC_ISSUER_ID。-allowProvisioningUpdates 自动登记连接的
|
# 先验证该身份存在,再将其固定传给 xcodebuild,不允许以“自动签名”为由切换或新建证书。
|
||||||
# 设备 + 建 profile(绕过手动 profile 的设备选择坑)。模拟器构建不需这些(base ad-hoc)。原生数据面
|
# -allowProvisioningUpdates 只用于登记设备、更新能力与 profile。模拟器构建不需这些。
|
||||||
# 经 SPM 拉 libwebrtc,无 gomobile 预构建步骤(早先 ios-engine recipe 已随 pion-on-iOS 废弃移除)。
|
# 用法:just ios-device <设备UDID>(UDID 见 just ios-devices)。全程不使用 Xcode GUI。
|
||||||
# 用法:just ios-device <设备UDID>(UDID 见 just ios-devices)。
|
|
||||||
ios-device udid:
|
ios-device udid:
|
||||||
[ -n "$CDROP_TEAM_ID" ] && [ -n "$CDROP_ASC_KEY_PATH" ] && [ -n "$CDROP_ASC_KEY_ID" ] && [ -n "$CDROP_ASC_ISSUER_ID" ] || { echo "缺 .env(CDROP_TEAM_ID / CDROP_ASC_KEY_PATH / CDROP_ASC_KEY_ID / CDROP_ASC_ISSUER_ID)——见 ios/CDrop/REALDEVICE.md"; exit 1; }
|
[ -n "$CDROP_TEAM_ID" ] && [ -n "$CDROP_ASC_KEY_PATH" ] && [ -n "$CDROP_ASC_KEY_ID" ] && [ -n "$CDROP_ASC_ISSUER_ID" ] && [ -n "$CDROP_APPLE_DEVELOPMENT_IDENTITY" ] || { echo "缺 .env(CDROP_TEAM_ID / CDROP_ASC_KEY_PATH / CDROP_ASC_KEY_ID / CDROP_ASC_ISSUER_ID / CDROP_APPLE_DEVELOPMENT_IDENTITY)——见 ios/CDrop/REALDEVICE.md"; exit 1; }
|
||||||
|
security find-identity -v -p codesigning | rg -F "$CDROP_APPLE_DEVELOPMENT_IDENTITY"
|
||||||
just ios-fonts
|
just ios-fonts
|
||||||
cd ios/CDrop && xcodegen generate
|
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
|
xcrun devicectl device install app --device {{udid}} ios/CDrop/build/DD-device/Build/Products/Debug-iphoneos/CommilitiaDrop.app
|
||||||
|
|
||||||
# ---- deploy plumbing ----
|
# ---- deploy plumbing ----
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cdrop — Commilitia Drop
|
# Commilitia Drop
|
||||||
|
|
||||||
跨 OS 剪贴板与文件传输服务。架构文档(`PROJECT_BRIEF.md` / `FRONTEND_DESIGN.md`)与 Changelog 位于 **`docs` 分支**:
|
跨 OS 剪贴板与文件传输服务。架构文档(`PROJECT_BRIEF.md` / `FRONTEND_DESIGN.md`)与 Changelog 位于 **`docs` 分支**:
|
||||||
|
|
||||||
@@ -23,7 +23,8 @@ git submodule update --init --recursive
|
|||||||
MVP(M0–M13)已上线 prod(OIDC PKCE 接入自建 Casdoor,Cloudflare Realtime TURN over TLS)。其后进入阶段二,已落地:
|
MVP(M0–M13)已上线 prod(OIDC PKCE 接入自建 Casdoor,Cloudflare Realtime TURN over TLS)。其后进入阶段二,已落地:
|
||||||
|
|
||||||
- **剪贴板同步**:单条覆写式云剪贴板(短 TTL 限暴露面)+ 文本消息一次性通道
|
- **剪贴板同步**:单条覆写式云剪贴板(短 TTL 限暴露面)+ 文本消息一次性通道
|
||||||
- **桌面客户端**:Wails v2(macOS universal `.app` + Windows `.exe`)—— 瘦客户端复用 web,业务全走后端 API;refresh_token 存系统密钥库(信封加密)
|
- **桌面客户端**:Wails v2(macOS Apple Silicon `.app` + Windows x64 `.exe`)——
|
||||||
|
瘦客户端复用 web,业务全走后端 API;refresh_token 存系统密钥库(信封加密)
|
||||||
- **原生数据面**:P2P 数据通道从系统 WebView 的 JS 下沉到原生——桌面走 Go/pion、iOS(原生 SwiftUI 客户端)走 libwebrtc——逃出 WebKit / WebView2 写死的 256KB SCTP 接收窗与渲染器节流;信令 / 中继 / 编排仍由共享 JS 引擎统一,三端共享同一线协议(详见 docs 分支 `CHANGELOG`)
|
- **原生数据面**: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**:可安装为独立 PWA;浏览器侧 refresh_token 不再进 JS,改由服务端 HttpOnly cookie 会话持有(AES-256-GCM 落盘),关闭重开自动免重登(7 天滑动失活),设备名随会话持久化(抗 PWA 存储清除)
|
||||||
- **推送通知**:页面 / PWA 关闭时,收到文件、消息、传输完成或失败以系统通知提醒;页面打开时仍走应用内提示。网页走 Web Push(VAPID,覆盖浏览器 / Android / iOS 16.4+ 已安装 PWA),桌面客户端走原生系统通知(仅窗口非前台才弹)。文案按设备语言在服务端渲染(中简 / 中繁 / 英)
|
- **推送通知**:页面 / PWA 关闭时,收到文件、消息、传输完成或失败以系统通知提醒;页面打开时仍走应用内提示。网页走 Web Push(VAPID,覆盖浏览器 / Android / iOS 16.4+ 已安装 PWA),桌面客户端走原生系统通知(仅窗口非前台才弹)。文案按设备语言在服务端渲染(中简 / 中繁 / 英)
|
||||||
@@ -31,6 +32,32 @@ MVP(M0–M13)已上线 prod(OIDC PKCE 接入自建 Casdoor,Cloudflare Re
|
|||||||
- **iOS Shortcut**:HS256 scoped token 手动签发路径(网页签发 UI 暂停)
|
- **iOS Shortcut**:HS256 scoped token 手动签发路径(网页签发 UI 暂停)
|
||||||
- **安全加固**:中继会话防耗尽、HS256 强制 jti、prod 强制 audience、TURN 短 TTL、OAuth 端点限流(详见 CHANGELOG)
|
- **安全加固**:中继会话防耗尽、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 里程碑(历史记录):
|
MVP 里程碑(历史记录):
|
||||||
|
|
||||||
| 里程碑 | 内容 | 状态 |
|
| 里程碑 | 内容 | 状态 |
|
||||||
@@ -164,4 +191,3 @@ env 见 [`.env.example`](.env.example) / `compose.snippet.yaml`。要点:
|
|||||||
5. 设 `CDROP_HS256_SECRET`(iOS Shortcut scoped token 用,不用可留空)
|
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 兜底)
|
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)
|
7. 可选 `CDROP_VAPID_PUBLIC_KEY` + `CDROP_VAPID_PRIVATE_KEY` 启用 Web Push 推送通知(用 `just vapid-keygen` 生成一对;两者须同时设置,半对则拒启动;都留空则推送惰性关闭,不阻塞启动)。`CDROP_VAPID_SUBJECT` 可选(联系标识,留空回退站点 URL)
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -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=""`→照旧匹配轮换,零影响。
|
- **R2 幂等键** `(user, app, meta)` → `(user, app, meta, sub)`:查既有过滤加 `&& ex.Sub==req.Sub`,`lockMintIdent` 键改 `user|app|meta|sub`。老客户端 `sub=""`、既有会话 `Sub=""`→照旧匹配轮换,零影响。
|
||||||
|
|
||||||
### 三、scope 缩减:复用现有 `tier`,broker 无新增
|
### 三、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` 最省,**推荐复用**。
|
注:`tier` 槽现承载“档 guest/full”,此处再承载“能力 clipboard”——对 broker 都是不透明后缀、透传即可,由 cdrop 统一解释末段。若你要把“档”与“能力”分两维,须另开 scope 字段;单一消费者下复用 `tier` 最省,**推荐复用**。
|
||||||
|
|
||||||
### 四、R1 归并:取 (b) broker 暴露 `sub`+cdrop 归并(非你倾向的 (a) broker 隐藏)
|
### 四、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`=设备内会话判别。默认 `""`=主会话。
|
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 消费方再按需加(同你“按需延后”原则)。
|
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)` 防级联中途复活——同意。
|
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 接边缘那次一并)。
|
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)` 上限。
|
6. **`sub` 取值集:现仅 `""`(主)/ `"widget"`(控制中心 + 主屏小组件,tier=clipboard)。** 命名空间预留给未来扩展进程(如 `"share"` 分享扩展、`"siri"`)。**暂不要 sub 数护栏**——cdrop 自控铸造、正常恒 2 条;若日后扩展进程增多再加每 `(user,app,meta)` 上限。
|
||||||
|
|||||||
+2
-2
@@ -32,7 +32,7 @@ func main() {
|
|||||||
slog.Error("config load failed", "err", err)
|
slog.Error("config load failed", "err", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
slog.Info("cdropd booting",
|
slog.Info("Commilitia Drop server booting",
|
||||||
"auth_mode", cfg.AuthMode,
|
"auth_mode", cfg.AuthMode,
|
||||||
"listen", cfg.Listen,
|
"listen", cfg.Listen,
|
||||||
"db_path", cfg.DBPath,
|
"db_path", cfg.DBPath,
|
||||||
@@ -120,5 +120,5 @@ func main() {
|
|||||||
if err := srv.Shutdown(shutCtx); err != nil {
|
if err := srv.Shutdown(shutCtx); err != nil {
|
||||||
slog.Error("http shutdown error", "err", err)
|
slog.Error("http shutdown error", "err", err)
|
||||||
}
|
}
|
||||||
slog.Info("cdropd stopped")
|
slog.Info("Commilitia Drop server stopped")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cdrop 桌面原生数据面(Option A)设计
|
# Commilitia Drop 桌面原生数据面(Option A)设计
|
||||||
|
|
||||||
> 目标:把桌面 **P2P 数据面**(WebRTC DataChannel 收发)从 WebView 的 JS 移到 Go 进程(`pion/webrtc`),**留在 Wails、不上 Tauri**。
|
> 目标:把桌面 **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)。
|
> 关联:`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):
|
Sender(sessionId, peerName, filePath, iceServers):
|
||||||
pc ← pion.NewPeerConnection(SettingEngine{ rwnd 大; host 候选真 IP; 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:
|
onOpen:
|
||||||
send JSON {type:meta, name, size, sha256?} // 与 p2p.ts 同帧
|
send JSON {type:meta, name, size, sha256?} // 与 p2p.ts 同帧
|
||||||
for chunk in readFileByPath(filePath, CHUNK): // Go 直接磁盘读,无桥、无 base64
|
for chunk in readFileByPath(filePath, CHUNK): // Go 直接磁盘读,无桥、无 base64
|
||||||
@@ -117,7 +117,7 @@ ICE creds:Go 拉 /api/calls/credentials(带 a.token),与 JS 同源
|
|||||||
> 双实现的唯一契约是**线协议**。任何一项漂移都会让桌面↔web/iOS 互通断裂。建议抽出共享测试向量(meta/chunk/done/ack 的字节级样例)双侧回归。
|
> 双实现的唯一契约是**线协议**。任何一项漂移都会让桌面↔web/iOS 互通断裂。建议抽出共享测试向量(meta/chunk/done/ack 的字节级样例)双侧回归。
|
||||||
|
|
||||||
- **信令**:`POST /api/hub/signal {to, payload:{type:"offer"|"answer"|"ice", sdp?, candidate?}}`;入站经 SSE。
|
- **信令**:`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 回传累计已收字节,单调取大)。
|
- **帧**:`meta` JSON `{type,name,size,sha256?}` / 二进制 chunk / `done` JSON / `ack` JSON `{type,bytes}`(接收端节流 200ms 回传累计已收字节,单调取大)。
|
||||||
- **状态机**:`/api/transfer/{initiate,p2p,done,fail,cancel,fallback}`;**完成由接收端 POST `/done`**(权威),发送端不重复。
|
- **状态机**:`/api/transfer/{initiate,p2p,done,fail,cancel,fallback}`;**完成由接收端 POST `/done`**(权威),发送端不重复。
|
||||||
- **中继回退语义**:JS 看门狗 30s 未 `connected` → `POST /fallback` 并对 Go `Cancel(session)`;Go 拆 pion,relay 由 JS 接管(数据面回到 JS relay,符合「relay 留 JS」)。
|
- **中继回退语义**:JS 看门狗 30s 未 `connected` → `POST /fallback` 并对 Go `Cancel(session)`;Go 拆 pion,relay 由 JS 接管(数据面回到 JS relay,符合「relay 留 JS」)。
|
||||||
@@ -130,7 +130,7 @@ ICE creds:Go 拉 /api/calls/credentials(带 a.token),与 JS 同源
|
|||||||
>
|
>
|
||||||
> 实测证据:iOS 经 gomobile/pion 时 Go 的 raw BSD socket 不与 iOS Network framework 集成(WebKit/libwebrtc 才集成)——Mac→iOS 常连不上(~30s ICE 超时)、iOS→Mac 偶连上 host↔host 但 ~4.5MB 中途断流(`read/write on closed pipe`)、TURN CreatePermission 刷屏、时好时坏。接口 / 链路本地 / IPv4-only 过滤只去噪、治不了架构不兼容(且那些 iOS 定向过滤漏进桌面共享引擎致 IPv6 host 对被裁退中继、重大回归)。即下方 §137 标注的“LOW-MEDIUM 成熟度、无 pion+gomobile 生产先例”风险兑现。
|
> 实测证据: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 门控)。
|
> **验证(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
@@ -1,4 +1,4 @@
|
|||||||
# cdrop 桌面客户端实施计划
|
# Commilitia Drop 桌面客户端实施计划
|
||||||
|
|
||||||
> Wails v2 · macOS 优先 + Windows 已落地
|
> Wails v2 · macOS 优先 + Windows 已落地
|
||||||
> 状态:**macOS 与 Windows 双端均已实现并 RDP/真机实测通过**——A2 前端复用 + 托盘/菜单栏 + 剪贴板双向同步 + 桌面设置 + session 持久化 + 开机自启(含自启静默驻留)+ 设备类型登记。macOS 见 §10,Windows 落地(含「custom-scheme 不支持流式」核心坑与 127.0.0.1 本地代理解法)见 §11。计划已据原生平台研究 sweep(`desktop/RESEARCH.md`)校正。
|
> 状态:**macOS 与 Windows 双端均已实现并 RDP/真机实测通过**——A2 前端复用 + 托盘/菜单栏 + 剪贴板双向同步 + 桌面设置 + session 持久化 + 开机自启(含自启静默驻留)+ 设备类型登记。macOS 见 §10,Windows 落地(含「custom-scheme 不支持流式」核心坑与 127.0.0.1 本地代理解法)见 §11。计划已据原生平台研究 sweep(`desktop/RESEARCH.md`)校正。
|
||||||
@@ -26,7 +26,9 @@
|
|||||||
|
|
||||||
## 0. 范围、非范围与本轮关键决策
|
## 0. 范围、非范围与本轮关键决策
|
||||||
|
|
||||||
**范围**:常驻桌面客户端,补浏览器做不到的——后台常驻、剪贴板自动监听上传、文件落盘、系统通知、全局快捷键。平台 **macOS(Apple Silicon + Intel)优先**,**Windows(x64)保持兼容**(同源、同分支,平台差异用 Go build tag 隔离)。
|
**范围**:常驻桌面客户端,补浏览器做不到的——后台常驻、剪贴板自动监听上传、文件落盘、
|
||||||
|
系统通知、全局快捷键。平台 **macOS(仅 Apple Silicon)优先**,**Windows(x64)保持兼容**
|
||||||
|
(同源、同分支,平台差异用 Go build tag 隔离)。
|
||||||
|
|
||||||
**非范围(推后)**:Linux 打包、iOS / Android 原生、自更新(仅版本检查提示)、产品化 / 多租户、Admin、Symmetric NAT 的 mDNS 直连、本地内容缓存 / 离线。
|
**非范围(推后)**:Linux 打包、iOS / Android 原生、自更新(仅版本检查提示)、产品化 / 多租户、Admin、Symmetric NAT 的 mDNS 直连、本地内容缓存 / 离线。
|
||||||
|
|
||||||
@@ -109,7 +111,7 @@ sweep 把 D1 从“最高风险未知”变成“路径已明、含一处后端
|
|||||||
|
|
||||||
### 3.1 回调通道:loopback(已定,弃 scheme)
|
### 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 侧(已定)
|
### 3.2 token 交换放 Go 侧(已定)
|
||||||
|
|
||||||
@@ -166,13 +168,20 @@ sweep 源码级确认:Casdoor 支持 public client(PKCE 时 `client_secret`
|
|||||||
- **Quick Send 浮窗(sweep 校正)**:**Wails v2 单原生窗口**,开不了第二个原生窗。v2 内只能“同窗 HTML 浮层 / 路由切换”近似;“失焦自隐”v2 无现成钩子、要自写 ObjC `NSWindowDelegate windowDidResignKey`(v2 路线最脏一块)。**这是上 v3 的最强理由**(v3 多窗口每窗一等对象)。
|
- **Quick Send 浮窗(sweep 校正)**:**Wails v2 单原生窗口**,开不了第二个原生窗。v2 内只能“同窗 HTML 浮层 / 路由切换”近似;“失焦自隐”v2 无现成钩子、要自写 ObjC `NSWindowDelegate windowDidResignKey`(v2 路线最脏一块)。**这是上 v3 的最强理由**(v3 多窗口每窗一等对象)。
|
||||||
- 产品化时建议热键可重绑(规避撞车)。
|
- 产品化时建议热键可重绑(规避撞车)。
|
||||||
|
|
||||||
### D6 · 打包(无证书:仅 `.app`)
|
### D6 · 打包与分发(2026-07-31 已验证)
|
||||||
- **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)。
|
- **macOS(当前路径)**:`just desktop-dist-mac` 构建 Apple Silicon ARM64 Wails app,复用
|
||||||
- **entitlements(若 ad-hoc 也带)**:非沙箱只需 `com.apple.security.network.client`;purego dlopen 系统 framework **不需** `disable-library-validation`;Carbon 热键不需 entitlement。(不要列通知 / 剪贴板 / 热键 entitlement——过度声明。)
|
既有 Developer ID Application 证书,以 hardened runtime + secure timestamp 签名;先提交
|
||||||
|
app 公证并 staple,再生成含 `/Applications` 链接的 DMG,对 DMG 签名、单独公证并 staple。
|
||||||
|
app 与 DMG 均已获 Apple `Accepted`,Gatekeeper 评估为 `Notarized Developer ID`。
|
||||||
|
- **entitlements**:当前非沙箱瘦客户端不额外声明通知、剪贴板或热键 entitlement;purego
|
||||||
|
`dlopen` Apple 系统 framework 不需 `disable-library-validation`,Carbon 热键也不需要
|
||||||
|
entitlement。后续引入第三方 dylib 或沙箱时重新评估。
|
||||||
- **Windows**:`wails build -platform windows/amd64 [-nsis]` 产出 `.exe` / NSIS 安装器;当前**不签名**(SmartScreen 会提示),获代码签名证书后再 `signtool`。
|
- **Windows**:`wails build -platform windows/amd64 [-nsis]` 产出 `.exe` / NSIS 安装器;当前**不签名**(SmartScreen 会提示),获代码签名证书后再 `signtool`。
|
||||||
- **版本检查**:启动 / 定时拉版本端点,过期提示去下载,**不自更新**。
|
- **版本检查**:启动 / 定时拉版本端点,过期提示去下载,**不自更新**。
|
||||||
- **将来获证书**:完整 codesign(Developer ID)+ notarytool + DMG + CI 流程见 `desktop/RESEARCH.md` D6 手册,届时直接套用。
|
- **操作入口**:完整构建、签名、公证、安装和验收流程见
|
||||||
- **CI**:D6 后 GitHub Actions 双 runner(macos-latest / windows-latest)跑构建 + 防回归(无证书阶段仅构建 + ad-hoc,不跑公证)。
|
`docs/client-build-install.md`;底层命令与设计依据见 `desktop/RESEARCH.md` D6 手册。
|
||||||
|
- **CI**:后续 GitHub Actions 双 runner(macos-latest / windows-latest)跑构建 + 防回归;
|
||||||
|
macOS runner 导入 Developer ID 后跑公证,Windows 在获得 Authenticode 证书后加入签名。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -296,7 +305,8 @@ A2 复用(地基,先让真 web UI 在桌面跑起来 + 登录走 Go 桥)
|
|||||||
|
|
||||||
### 10.6 实现状态(2026-06-13 实现,本地全绿)
|
### 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 的关键决策:
|
落地与对 §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 客户端…)。
|
- **设备类型**:客户端发 `X-Device-Type`(桌面按 `runtime.GOOS` 注入 macos/windows),后端中间件白名单登记(替代写死 `browser`),前端 i18n 本地化展示(浏览器 / macOS 客户端 / Windows 客户端…)。
|
||||||
- **设备名持久化**修复:`SaveSettings` 合并保留 `DeviceName`(设置页 `DesktopConfig` 无 device_name 字段,原先存设置会用空值覆盖)。
|
- **设备名持久化**修复:`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,桌面与浏览器通用。
|
- **Cmd+,(Ctrl+,)→ 设置**:`main.tsx` 全局 keydown,桌面与浏览器通用。
|
||||||
|
|
||||||
### 11.5 自启静默驻留(autostart → 不弹窗,直接驻留菜单栏 / 托盘)
|
### 11.5 自启静默驻留(autostart → 不弹窗,直接驻留菜单栏 / 托盘)
|
||||||
|
|||||||
+22
-12
@@ -1,19 +1,29 @@
|
|||||||
# README
|
# Commilitia Drop(Wails)
|
||||||
|
|
||||||
## About
|
这是 Windows 与 macOS 当前唯一的正式桌面客户端。两个平台都复用仓库根目录 `web/`
|
||||||
|
的 React 前端;`frontend/src` 只保留为早期 Wails 桥接示例,不是可发布的第二套界面。
|
||||||
|
|
||||||
This is the official Wails Vanilla-TS template.
|
正式构建必须从仓库根目录运行:
|
||||||
|
|
||||||
You can configure the project by editing `wails.json`. More information about the project settings can be found
|
```sh
|
||||||
here: https://wails.io/docs/reference/project-config
|
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
|
```sh
|
||||||
server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser
|
just desktop-dist-mac
|
||||||
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.
|
|
||||||
|
|
||||||
## 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
@@ -1,6 +1,8 @@
|
|||||||
# cdrop 桌面端 D1 前决策就绪简报
|
# Commilitia Drop 桌面端 D1 前决策就绪简报
|
||||||
|
|
||||||
> 框架基线:Wails v2.12.0(Go 1.23 模块声明,本机 toolchain 1.26.3)+ 系统 WebView,瘦客户端,业务全走 `drop.commilitia.net`。平台优先级 macOS(Apple Silicon + Intel universal),Windows x64 兼容。
|
> 框架基线:Wails v2.12.0(Go 1.23 模块声明,本机 toolchain 1.26.3)+ 系统 WebView,
|
||||||
|
> 瘦客户端,业务全走 `drop.commilitia.net`。平台优先级 macOS(仅 Apple Silicon),
|
||||||
|
> Windows x64 兼容。
|
||||||
> 已对四项关键论断做对抗式核验:托盘能力 **refuted**、热键库可用性 **supported(附 pin 条件)**、Casdoor/R1 **mixed(需一处后端改动 + 版本前提)**、签名/公证 entitlement **mixed(核心子句成立,捆绑表述需修正)**。
|
> 已对四项关键论断做对抗式核验:托盘能力 **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`。
|
本节只覆盖 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 下实现成本差异明显。
|
两条路线都符合 RFC 8252 对原生应用的要求(§7.1 私有 scheme、§7.3 loopback),但在 Wails 下实现成本差异明显。
|
||||||
|
|
||||||
@@ -132,10 +134,15 @@
|
|||||||
- 跨平台零差异:macOS / Windows / Linux 代码一致,只依赖 `net/http`。
|
- 跨平台零差异: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 允许请求时指定任意端口”。
|
- 必须用 IP 字面量 `127.0.0.1`(或 `[::1]`),不要用 `localhost`——RFC 8252 §8.3:用 `localhost` 可能因 DNS / hosts 解析意外监听到非回环接口。端口用 OS 分配的临时端口(`:0`),符合 RFC 8252 §7.3“服务器 MUST 允许请求时指定任意端口”。
|
||||||
|
|
||||||
**自定义 scheme(`cdrop://callback`):**
|
**自定义 scheme(`commilitia-drop://callback`):**
|
||||||
|
|
||||||
- 需要 OS 级登记:macOS 在 `Info.plist` 写 `CFBundleURLTypes` → `CFBundleURLSchemes`(值 `cdrop`);Windows 要在注册表 `HKEY_CLASSES_ROOT\cdrop`(或 per-user `HKCU\Software\Classes\cdrop`)写 `URL Protocol` 键 + `shell\open\command` 指向 exe;Linux 走 `.desktop` 的 `x-scheme-handler/cdrop`。
|
- 需要 OS 级登记:macOS 在 `Info.plist` 写 `CFBundleURLTypes` → `CFBundleURLSchemes`
|
||||||
- 必须配单实例锁。点 `cdrop://` 时 OS 会重新拉起 app 的“第二个实例”,深链作为命令行参数传入。需要 `options.App.SingleInstanceLock` 把这个 deep link 转发给首实例,否则 token 落在一个马上要退出的临时进程里,拿不到。
|
(值 `commilitia-drop`);Windows 要在注册表
|
||||||
|
`HKEY_CLASSES_ROOT\commilitia-drop`(或 per-user
|
||||||
|
`HKCU\Software\Classes\commilitia-drop`)写 `URL Protocol` 键 +
|
||||||
|
`shell\open\command` 指向 exe;Linux 走
|
||||||
|
`.desktop` 的 `x-scheme-handler/commilitia-drop`。
|
||||||
|
- 必须配单实例锁。点 `commilitia-drop://` 时 OS 会重新拉起 app 的“第二个实例”,深链作为命令行参数传入。需要 `options.App.SingleInstanceLock` 把这个 deep link 转发给首实例,否则 token 落在一个马上要退出的临时进程里,拿不到。
|
||||||
- macOS 上单实例锁与深链协作有已知坑(见 wails issue #5089:v3 的 single instance lock 与 `OpenedWithURL` 不兼容;v2 也需自己从 `Args` 解析 URL 并 `WindowUnminimise` + `Show`)。
|
- macOS 上单实例锁与深链协作有已知坑(见 wails issue #5089:v3 的 single instance lock 与 `OpenedWithURL` 不兼容;v2 也需自己从 `Args` 解析 URL 并 `WindowUnminimise` + `Show`)。
|
||||||
|
|
||||||
scheme 路线的 `SingleInstanceLock` 形态(作为对比,**本项目不采用**):
|
scheme 路线的 `SingleInstanceLock` 形态(作为对比,**本项目不采用**):
|
||||||
@@ -145,9 +152,9 @@ scheme 路线的 `SingleInstanceLock` 形态(作为对比,**本项目不采
|
|||||||
SingleInstanceLock: &options.SingleInstanceLock{
|
SingleInstanceLock: &options.SingleInstanceLock{
|
||||||
UniqueId: "net.commilitia.cdrop",
|
UniqueId: "net.commilitia.cdrop",
|
||||||
OnSecondInstanceLaunch: func(d options.SecondInstanceData) {
|
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 {
|
for _, arg := range d.Args {
|
||||||
if strings.HasPrefix(arg, "cdrop://") {
|
if strings.HasPrefix(arg, "commilitia-drop://") {
|
||||||
runtime.WindowUnminimise(appCtx) // 回调不会自动聚焦窗口
|
runtime.WindowUnminimise(appCtx) // 回调不会自动聚焦窗口
|
||||||
runtime.Show(appCtx)
|
runtime.Show(appCtx)
|
||||||
runtime.EventsEmit(appCtx, "oauth:callback", arg)
|
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` 用法与注意
|
### 2. `runtime.BrowserOpenURL` 用法与注意
|
||||||
|
|
||||||
@@ -397,13 +404,13 @@ export async function onLoginClick(): Promise<void>
|
|||||||
func writeClosePage(w http.ResponseWriter, ok bool)
|
func writeClosePage(w http.ResponseWriter, ok bool)
|
||||||
{
|
{
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
msg := "已登录,可关闭本页返回 cdrop。"
|
msg := "已登录,可关闭本页返回 Commilitia Drop。"
|
||||||
if !ok
|
if !ok
|
||||||
{
|
{
|
||||||
msg = "登录未完成,可关闭本页返回 cdrop 重试。"
|
msg = "登录未完成,可关闭本页返回 Commilitia Drop 重试。"
|
||||||
}
|
}
|
||||||
fmt.Fprintf(w, `<!doctype html><html lang="zh-Hans"><head><meta charset="utf-8">`+
|
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>`+
|
`<p>%s</p><script>setTimeout(function(){window.close();},800);</script>`+
|
||||||
`</body></html>`, msg)
|
`</body></html>`, msg)
|
||||||
}
|
}
|
||||||
@@ -443,7 +450,8 @@ func writeClosePage(w http.ResponseWriter, ok bool)
|
|||||||
|
|
||||||
## 打包·签名·公证操作手册(D6 补充)
|
## 打包·签名·公证操作手册(D6 补充)
|
||||||
|
|
||||||
本节只补**具体命令与 CI 配置**;entitlement/公证“会不会被拒”的判断已在前文完成。环境基准:Wails v2.12.0(Go),macOS universal 优先 + Windows x64,瘦客户端。
|
本节只补**具体命令与 CI 配置**;entitlement/公证“会不会被拒”的判断已在前文完成。
|
||||||
|
环境基准:Wails v2.12.0(Go),macOS arm64 + Windows x64,瘦客户端。
|
||||||
|
|
||||||
> 重要前提:Wails 官方 signing 指南至今仍推荐 `gon`,但该工具已停止维护、且依赖已废弃的 `altool` 语义。本手册一律改用 Apple 现行的 `codesign` + `notarytool` + `stapler` 链路,这是已确定的正确路径。
|
> 重要前提:Wails 官方 signing 指南至今仍推荐 `gon`,但该工具已停止维护、且依赖已废弃的 `altool` 语义。本手册一律改用 Apple 现行的 `codesign` + `notarytool` + `stapler` 链路,这是已确定的正确路径。
|
||||||
|
|
||||||
@@ -452,13 +460,15 @@ func writeClosePage(w http.ResponseWriter, ok bool)
|
|||||||
#### 1.1 构建产物与已知坑(已确定)
|
#### 1.1 构建产物与已知坑(已确定)
|
||||||
|
|
||||||
```bash
|
```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` 等都改这里。
|
- 项目脚手架在 `build/darwin/` 下生成 `Info.plist`(模板 `Info.plist`/`Info.dev.plist`),这是 `.app` 内 `Contents/Info.plist` 的来源。改 bundle ID、版本号、`LSMinimumSystemVersion` 等都改这里。
|
||||||
- 已知坑:
|
- 已知坑:
|
||||||
- universal 构建要求**本机同时具备 arm64 与 amd64 的 CGO 工具链**。在 Apple Silicon 的 `macos-latest`(macos-14/15)runner 上原生满足;不要尝试从 Linux 交叉编译 darwin(CGO + macOS SDK 缺失,社区反复确认不可行)。
|
- 当前只构建 arm64;不要尝试从 Linux 交叉编译 darwin(CGO + macOS SDK 缺失,
|
||||||
|
社区反复确认不可行)。
|
||||||
- `wails build` 本身**不签名、不公证 macOS 产物**(与 Windows 的 `-nsis` 不同,没有内建签名参数)。签名/公证完全是构建后的独立步骤。
|
- `wails build` 本身**不签名、不公证 macOS 产物**(与 Windows 的 `-nsis` 不同,没有内建签名参数)。签名/公证完全是构建后的独立步骤。
|
||||||
- Wails 没有内建 DMG 封装,需自己做(见 1.4)。
|
- Wails 没有内建 DMG 封装,需自己做(见 1.4)。
|
||||||
|
|
||||||
@@ -563,7 +573,22 @@ xcrun stapler staple "YourApp.dmg"
|
|||||||
|
|
||||||
#### 1.6 Wails 对 darwin 签名的内建支持(已确定)
|
#### 1.6 Wails 对 darwin 签名的内建支持(已确定)
|
||||||
|
|
||||||
`wails build -platform darwin/universal` **无签名参数**,不做 codesign/notarize;签名、公证、DMG 全是构建后的外部步骤。`build/darwin/` 下有 `Info.plist`/`Info.dev.plist`;`entitlements.plist` 非自动生成,需手动放进该路径。universal 由 Wails 内部 `lipo` 合并,无需手动 `lipo`。
|
`wails build -platform darwin/arm64` **无签名参数**,不做 codesign/notarize;签名、
|
||||||
|
公证、DMG 全是构建后的外部步骤。`build/darwin/` 下有 `Info.plist`/`Info.dev.plist`;
|
||||||
|
`entitlements.plist` 非自动生成,需手动放进该路径。
|
||||||
|
|
||||||
|
#### 1.7 本项目已验证的正式 recipe(2026-07-31)
|
||||||
|
|
||||||
|
仓库根目录的 `just desktop-dist-mac` 已把上述外部步骤固化:ARM64 构建 → 复用既有
|
||||||
|
Developer ID Application 身份签名 → `ditto` ZIP 公证 → app staple → 生成 DMG → DMG 签名与
|
||||||
|
单独公证 → DMG staple → Gatekeeper 评估。DMG 由 `desktop/scripts/make-dmg.sh` 使用纯系统工具
|
||||||
|
创建,内容只有 `Commilitia Drop.app` 和 `/Applications` 链接,不依赖 `create-dmg`。
|
||||||
|
|
||||||
|
本次实测 app submission `0e2c80e1-e40d-4108-951e-64c810bd118e`、DMG submission
|
||||||
|
`7a6aea7a-6c40-4bdb-96e3-46e6f8fbc813` 均为 `Accepted`;两者 staple/validate 成功,
|
||||||
|
`spctl` 返回 `Notarized Developer ID`。这证明 Wails 应用本身满足 Apple 公证要求;后续发布应
|
||||||
|
复用 recipe,不再使用 ad-hoc 签名或未公证 `.app` 作为正式分发物。面向操作者的完整流程见
|
||||||
|
`docs/client-build-install.md`。
|
||||||
|
|
||||||
### 2. Windows
|
### 2. Windows
|
||||||
|
|
||||||
@@ -596,7 +621,7 @@ signtool sign /fd sha256 /tr http://ts.ssl.com /td sha256 /f certificate.pfx /p
|
|||||||
```yaml
|
```yaml
|
||||||
jobs:
|
jobs:
|
||||||
macos:
|
macos:
|
||||||
runs-on: macos-latest # Apple Silicon,原生支持 universal CGO
|
runs-on: macos-latest # Apple Silicon arm64
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with: { submodules: recursive } # cjk-autospace 等 submodule
|
with: { submodules: recursive } # cjk-autospace 等 submodule
|
||||||
@@ -605,7 +630,7 @@ jobs:
|
|||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with: { node-version: '20' }
|
with: { node-version: '20' }
|
||||||
- run: go install github.com/wailsapp/wails/v2/cmd/wails@v2.12.0
|
- 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
|
- uses: apple-actions/import-codesign-certs@v7
|
||||||
with:
|
with:
|
||||||
p12-file-base64: ${{ secrets.APPLE_CERT_P12_BASE64 }}
|
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 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 installer 指南(build/windows/installer、-nsis):https://wails.io/docs/guides/windows-installer/
|
||||||
- Wails NSIS 不签主 exe 的 issue #3716:https://github.com/wailsapp/wails/issues/3716
|
- Wails NSIS 不签主 exe 的 issue #3716:https://github.com/wailsapp/wails/issues/3716
|
||||||
- Wails Crossplatform build(GitHub Actions、darwin/universal 矩阵):https://wails.io/docs/guides/crossplatform-build/
|
- Wails Crossplatform build(GitHub Actions):https://wails.io/docs/guides/crossplatform-build/
|
||||||
- notarytool man page:https://keith.github.io/xcode-man-pages/notarytool.1.html
|
- notarytool man page:https://keith.github.io/xcode-man-pages/notarytool.1.html
|
||||||
- Apple TN3147 迁移到新公证工具:https://developer.apple.com/documentation/technotes/tn3147-migrating-to-the-latest-notarization-tool
|
- Apple TN3147 迁移到新公证工具:https://developer.apple.com/documentation/technotes/tn3147-migrating-to-the-latest-notarization-tool
|
||||||
- Apple Disable Library Validation entitlement:https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_cs_disable-library-validation
|
- Apple Disable Library Validation entitlement:https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_cs_disable-library-validation
|
||||||
|
|||||||
+8
-5
@@ -62,6 +62,9 @@ func (a *App) startup(ctx context.Context) {
|
|||||||
if platform.IsLaunchAtLoginEnabled() {
|
if platform.IsLaunchAtLoginEnabled() {
|
||||||
_ = platform.SetLaunchAtLogin(true)
|
_ = 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 同步到引擎)。
|
// 原生传输引擎:落地目录取当前配置(设置页改目录时经 SaveSettings 同步到引擎)。
|
||||||
a.transfer = engine.New(engine.Config{DownloadDir: platform.ResolveDownloadDir()}, &transferEvents{app: a})
|
a.transfer = engine.New(engine.Config{DownloadDir: platform.ResolveDownloadDir()}, &transferEvents{app: a})
|
||||||
a.startClipboardSync(ctx)
|
a.startClipboardSync(ctx)
|
||||||
@@ -70,10 +73,10 @@ func (a *App) startup(ctx context.Context) {
|
|||||||
platform.TriggerLocalNetwork()
|
platform.TriggerLocalNetwork()
|
||||||
platform.InstallStatusBar(
|
platform.InstallStatusBar(
|
||||||
platform.StatusBarMenu{
|
platform.StatusBarMenu{
|
||||||
Title: "cdrop",
|
Title: "Commilitia Drop",
|
||||||
Show: "显示主窗口",
|
Show: "显示主窗口",
|
||||||
Settings: "设置…",
|
Settings: "设置…",
|
||||||
Quit: "退出 cdrop",
|
Quit: "退出 Commilitia Drop",
|
||||||
},
|
},
|
||||||
// The native menu-action callbacks fire on the AppKit main thread; calling
|
// The native menu-action callbacks fire on the AppKit main thread; calling
|
||||||
// Wails runtime methods synchronously there can re-enter the main run loop
|
// 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()
|
m := menu.NewMenu()
|
||||||
|
|
||||||
appSub := 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()
|
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
|
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 {
|
func oauthConfigFromEnv() platform.OAuthConfig {
|
||||||
return platform.OAuthConfig{
|
return platform.OAuthConfig{
|
||||||
BrokerURL: os.Getenv("CDROP_BROKER_URL"),
|
BrokerURL: os.Getenv("CDROP_BROKER_URL"),
|
||||||
App: envOr("CDROP_BROKER_APP", "cdrop"),
|
App: envOr("CDROP_BROKER_APP", "commilitia-drop"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<string>Commilitia Drop 使用本地网络发现同内网设备并建立点对点直连传输。</string>
|
<string>Commilitia Drop 使用本地网络发现同内网设备并建立点对点直连传输。</string>
|
||||||
<key>NSBonjourServices</key>
|
<key>NSBonjourServices</key>
|
||||||
<array>
|
<array>
|
||||||
<string>_cdrop._tcp</string>
|
<string>_commilitia-drop._tcp</string>
|
||||||
</array>
|
</array>
|
||||||
{{if .Info.FileAssociations}}
|
{{if .Info.FileAssociations}}
|
||||||
<key>CFBundleDocumentTypes</key>
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<string>Commilitia Drop 使用本地网络发现同内网设备并建立点对点直连传输。</string>
|
<string>Commilitia Drop 使用本地网络发现同内网设备并建立点对点直连传输。</string>
|
||||||
<key>NSBonjourServices</key>
|
<key>NSBonjourServices</key>
|
||||||
<array>
|
<array>
|
||||||
<string>_cdrop._tcp</string>
|
<string>_commilitia-drop._tcp</string>
|
||||||
</array>
|
</array>
|
||||||
{{if .Info.FileAssociations}}
|
{{if .Info.FileAssociations}}
|
||||||
<key>CFBundleDocumentTypes</key>
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
// 留给宿主(桌面 JS / iOS Swift),引擎只经回调收发不透明信令串——纯数据面。
|
// 留给宿主(桌面 JS / iOS Swift),引擎只经回调收发不透明信令串——纯数据面。
|
||||||
//
|
//
|
||||||
// 线协议与 web 引擎 web/src/features/transfer/p2p.ts 逐字节一致,故 Go 端可与浏览器 /
|
// 线协议与 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?}。
|
// 文件分片走二进制帧;信令 payload 形如 {type, sdp?, candidate?}。
|
||||||
package engine
|
package engine
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
channelName = "cdrop-file"
|
channelName = "commilitia-drop-file"
|
||||||
chunkSize = 64 * 1024
|
chunkSize = 64 * 1024
|
||||||
highWatermark = 16 * 1024 * 1024
|
highWatermark = 16 * 1024 * 1024
|
||||||
lowWatermark = 4 * 1024 * 1024
|
lowWatermark = 4 * 1024 * 1024
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
<title>cdrop-desktop</title>
|
<title>Commilitia Drop</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import { EventsOn } from "../wailsjs/runtime/runtime";
|
|||||||
// Go 换 token → oauth:success / oauth:error 事件回到这里。
|
// Go 换 token → oauth:success / oauth:error 事件回到这里。
|
||||||
document.querySelector("#app")!.innerHTML = `
|
document.querySelector("#app")!.innerHTML = `
|
||||||
<main class="login-demo">
|
<main class="login-demo">
|
||||||
<h1>cdrop 桌面端</h1>
|
<h1>Commilitia Drop</h1>
|
||||||
<p class="status" id="status">检查登录状态……</p>
|
<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>
|
<p class="hint">登录会在系统浏览器中打开授权页,完成后自动返回。</p>
|
||||||
</main>
|
</main>
|
||||||
`;
|
`;
|
||||||
|
|||||||
+1
-1
@@ -67,7 +67,7 @@ func main() {
|
|||||||
|
|
||||||
// Create application with options
|
// Create application with options
|
||||||
err = wails.Run(&options.App{
|
err = wails.Run(&options.App{
|
||||||
Title: "cdrop",
|
Title: "Commilitia Drop",
|
||||||
Width: 1024,
|
Width: 1024,
|
||||||
Height: 768,
|
Height: 768,
|
||||||
StartHidden: hidden,
|
StartHidden: hidden,
|
||||||
|
|||||||
@@ -68,7 +68,14 @@ func ResolveDeviceName() string {
|
|||||||
if h, err := os.Hostname(); err == nil && h != "" {
|
if h, err := os.Hostname(); err == nil && h != "" {
|
||||||
return 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.
|
// DefaultConfig is what a fresh install gets: clipboard sync on, no autostart.
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func FetchOAuthConfig(ctx context.Context, apiBase string) (OAuthConfig, error)
|
|||||||
|
|
||||||
app := c.App
|
app := c.App
|
||||||
if app == "" {
|
if app == "" {
|
||||||
app = "cdrop"
|
app = "commilitia-drop"
|
||||||
}
|
}
|
||||||
cfg := OAuthConfig{BrokerURL: c.BrokerURL, App: app}
|
cfg := OAuthConfig{BrokerURL: c.BrokerURL, App: app}
|
||||||
if cfg.BrokerURL == "" {
|
if cfg.BrokerURL == "" {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ func TestFetchOAuthConfig_Success(t *testing.T) {
|
|||||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
"auth_mode": "prod",
|
"auth_mode": "prod",
|
||||||
"broker_url": "https://sso.example.net",
|
"broker_url": "https://sso.example.net",
|
||||||
"broker_app": "cdrop",
|
"broker_app": "commilitia-drop",
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
@@ -30,13 +30,13 @@ func TestFetchOAuthConfig_Success(t *testing.T) {
|
|||||||
if cfg.BrokerURL != "https://sso.example.net" {
|
if cfg.BrokerURL != "https://sso.example.net" {
|
||||||
t.Errorf("broker_url = %q", cfg.BrokerURL)
|
t.Errorf("broker_url = %q", cfg.BrokerURL)
|
||||||
}
|
}
|
||||||
if cfg.App != "cdrop" {
|
if cfg.App != "commilitia-drop" {
|
||||||
t.Errorf("app = %q, want cdrop", cfg.App)
|
t.Errorf("app = %q, want commilitia-drop", cfg.App)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFetchOAuthConfig_DefaultsApp(t *testing.T) {
|
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) {
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{"broker_url": "https://sso.example.net"})
|
_ = 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 {
|
if err != nil {
|
||||||
t.Fatalf("FetchOAuthConfig: %v", err)
|
t.Fatalf("FetchOAuthConfig: %v", err)
|
||||||
}
|
}
|
||||||
if cfg.App != "cdrop" {
|
if cfg.App != "commilitia-drop" {
|
||||||
t.Errorf("app = %q, want default cdrop", cfg.App)
|
t.Errorf("app = %q, want default commilitia-drop", cfg.App)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 代铸 (proxy-mint) on the desktop. The device-authorization flow yields a bootstrap
|
// 代铸 (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
|
// 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
|
// 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.
|
// managed exactly like a browser — the same model, not a separate native-only track.
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ func writeInto(dir, name string, data []byte) (string, error) {
|
|||||||
}
|
}
|
||||||
safe := sanitizeFileName(name)
|
safe := sanitizeFileName(name)
|
||||||
if safe == "" {
|
if safe == "" {
|
||||||
safe = "cdrop-download"
|
safe = "Commilitia Drop Download"
|
||||||
}
|
}
|
||||||
target := uniquePath(dir, safe)
|
target := uniquePath(dir, safe)
|
||||||
if err := os.WriteFile(target, data, 0o644); err != nil {
|
if err := os.WriteFile(target, data, 0o644); err != nil {
|
||||||
@@ -248,7 +248,7 @@ func FinalizeStreamingDownload(sessionId, name string) (string, error) {
|
|||||||
}
|
}
|
||||||
safe := sanitizeFileName(name)
|
safe := sanitizeFileName(name)
|
||||||
if safe == "" {
|
if safe == "" {
|
||||||
safe = "cdrop-download"
|
safe = "Commilitia Drop Download"
|
||||||
}
|
}
|
||||||
target := uniquePath(d.dir, safe)
|
target := uniquePath(d.dir, safe)
|
||||||
if err := os.Rename(d.tmp, target); err != nil {
|
if err := os.Rename(d.tmp, target); err != nil {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ func HealIdentity(s *LoginResult, apiBase string) *LoginResult {
|
|||||||
}
|
}
|
||||||
if changed {
|
if changed {
|
||||||
if err := SaveSession(*s); err != nil {
|
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
|
return s
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// launchAgentLabel is the reverse-DNS label + plist filename for the per-user
|
// launchAgentLabel is a stable internal identifier. The product name shown to
|
||||||
// LaunchAgent that starts cdrop at login.
|
// users comes from the application metadata, not this LaunchAgent label.
|
||||||
const launchAgentLabel = "net.commilitia.cdrop"
|
const launchAgentLabel = "net.commilitia.cdrop"
|
||||||
|
|
||||||
func launchAgentPath() (string, error) {
|
func launchAgentPath() (string, error) {
|
||||||
@@ -71,7 +71,40 @@ func SetLaunchAtLogin(enabled bool) error {
|
|||||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||||||
return err
|
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.
|
// IsLaunchAtLoginEnabled reports whether the LaunchAgent plist is present.
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ func TestSetLaunchAtLoginInstallsAndRemoves(t *testing.T) {
|
|||||||
|
|
||||||
func TestAppBundlePath(t *testing.T) {
|
func TestAppBundlePath(t *testing.T) {
|
||||||
cases := map[string]string{
|
cases := map[string]string{
|
||||||
"/Apps/cdrop.app/Contents/MacOS/desktop": "/Apps/cdrop.app",
|
"/Apps/Commilitia Drop.app/Contents/MacOS/Commilitia Drop": "/Apps/Commilitia Drop.app",
|
||||||
"/usr/local/bin/desktop": "",
|
"/usr/local/bin/Commilitia Drop": "",
|
||||||
}
|
}
|
||||||
for in, want := range cases {
|
for in, want := range cases {
|
||||||
if got := appBundlePath(in); got != want {
|
if got := appBundlePath(in); got != want {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#import <Network/Network.h>
|
#import <Network/Network.h>
|
||||||
|
|
||||||
// cdropTriggerLocalNetwork:起一个对 _cdrop._tcp 的 Bonjour 浏览,触发 macOS 本地网络权限弹窗。
|
// cdropTriggerLocalNetwork:起一个对 _commilitia-drop._tcp 的 Bonjour 浏览,触发 macOS 本地网络权限弹窗。
|
||||||
// WKWebView 自身不会触发该权限请求,须由宿主 App 主动发起一次本地网络访问;授权后本进程内的
|
// WKWebView 自身不会触发该权限请求,须由宿主 App 主动发起一次本地网络访问;授权后本进程内的
|
||||||
// WebRTC 才能收集 host / mDNS 候选实现同内网直连(见 localnetwork_darwin.go 注释)。浏览结果本身
|
// WebRTC 才能收集 host / mDNS 候选实现同内网直连(见 localnetwork_darwin.go 注释)。浏览结果本身
|
||||||
// 不关心——「发起访问」这一动作即触发授权。保活单个 browser(静态全局,ARC 下持有),幂等。
|
// 不关心——「发起访问」这一动作即触发授权。保活单个 browser(静态全局,ARC 下持有),幂等。
|
||||||
@@ -11,7 +11,7 @@ void cdropTriggerLocalNetwork(void) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
nw_browse_descriptor_t descriptor =
|
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_t parameters = nw_parameters_create();
|
||||||
nw_parameters_set_include_peer_to_peer(parameters, true);
|
nw_parameters_set_include_peer_to_peer(parameters, true);
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ import "C"
|
|||||||
|
|
||||||
import "unsafe"
|
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.
|
// Notify shows a native system notification.
|
||||||
//
|
//
|
||||||
// macOS uses UNUserNotificationCenter, which REQUIRES the app bundle to be
|
// macOS uses UNUserNotificationCenter, which REQUIRES the app bundle to be
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
package platform
|
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.
|
// 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
|
// The desktop client targets macOS and Windows; this keeps the package building
|
||||||
// on other GOOS (e.g. a Linux `go vet` / CI pass).
|
// on other GOOS (e.g. a Linux `go vet` / CI pass).
|
||||||
|
|||||||
@@ -4,14 +4,18 @@ package platform
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
toast "git.sr.ht/~jackmordaunt/go-toast/v2"
|
toast "git.sr.ht/~jackmordaunt/go-toast/v2"
|
||||||
|
"golang.org/x/sys/windows/registry"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// toastAppID 是 Windows Action Center 里显示的应用标识,与 macOS bundle id 对齐。
|
// toastAppID 是稳定的内部 AppUserModelID;Windows 通知设置中的用户可见名称由
|
||||||
|
// toastDisplayName 单独写入 DisplayName,避免为了改显示名破坏系统身份。
|
||||||
toastAppID = "net.commilitia.cdrop"
|
toastAppID = "net.commilitia.cdrop"
|
||||||
|
toastDisplayName = "Commilitia Drop"
|
||||||
// toastGUID 固定不变——它把通知归属到注册表里的本应用条目;更换会让既有通知
|
// toastGUID 固定不变——它把通知归属到注册表里的本应用条目;更换会让既有通知
|
||||||
// 失去归属。
|
// 失去归属。
|
||||||
toastGUID = "{c4d8e2a1-6b3f-4e7a-9c2d-1f5b8a0e3d6c}"
|
toastGUID = "{c4d8e2a1-6b3f-4e7a-9c2d-1f5b8a0e3d6c}"
|
||||||
@@ -19,20 +23,41 @@ const (
|
|||||||
|
|
||||||
var toastInit sync.Once
|
var toastInit sync.Once
|
||||||
|
|
||||||
// Notify shows a native Windows toast. go-toast renders via the WinRT/COM path
|
// InitializeNotifications registers the stable Windows notification identity
|
||||||
// (PowerShell fallback when the AppID isn't registered), so it works without code
|
// and corrects its user-visible name. It is safe to call repeatedly.
|
||||||
// signing — SmartScreen only gates the installer, not notifications. Best-effort:
|
func InitializeNotifications() {
|
||||||
// errors are swallowed. SetAppData registers the app identity once so the toast
|
|
||||||
// shows "cdrop" rather than the PowerShell host.
|
|
||||||
func Notify(title, body string) {
|
|
||||||
toastInit.Do(func() {
|
toastInit.Do(func() {
|
||||||
data := toast.AppData{AppID: toastAppID, GUID: toastGUID}
|
data := toast.AppData{AppID: toastAppID, GUID: toastGUID}
|
||||||
if exe, err := os.Executable(); err == nil {
|
if exe, err := os.Executable(); err == nil {
|
||||||
data.ActivationExe = exe
|
data.ActivationExe = exe
|
||||||
}
|
}
|
||||||
_ = toast.SetAppData(data)
|
_ = 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{
|
n := toast.Notification{
|
||||||
AppID: toastAppID,
|
AppID: toastAppID,
|
||||||
Title: title,
|
Title: title,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import (
|
|||||||
|
|
||||||
// OAuthConfig carries the Auth Broker coordinates. BrokerURL is the broker's PUBLIC
|
// 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
|
// 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.
|
// apps.json (redirect_uris) for the device flow to accept it.
|
||||||
type OAuthConfig struct {
|
type OAuthConfig struct {
|
||||||
BrokerURL string
|
BrokerURL string
|
||||||
@@ -138,7 +138,7 @@ func (f *Flow) Login(ctx context.Context) (*TokenResult, error) {
|
|||||||
"state": {state},
|
"state": {state},
|
||||||
"code_challenge": {challenge},
|
"code_challenge": {challenge},
|
||||||
"code_challenge_method": {"S256"},
|
"code_challenge_method": {"S256"},
|
||||||
"description": {"cdrop 桌面客户端"},
|
"description": {"Commilitia Drop"},
|
||||||
}.Encode()
|
}.Encode()
|
||||||
f.openURL(authURL)
|
f.openURL(authURL)
|
||||||
|
|
||||||
@@ -255,11 +255,11 @@ func randString(n int) (string, error) {
|
|||||||
// the real affordance; the close attempt is best-effort.
|
// the real affordance; the close attempt is best-effort.
|
||||||
func writeClosePage(w http.ResponseWriter, ok bool) {
|
func writeClosePage(w http.ResponseWriter, ok bool) {
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
msg := "已登录,可关闭本页返回 cdrop。"
|
msg := "已登录,可关闭本页返回 Commilitia Drop。"
|
||||||
if !ok {
|
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">`+
|
`<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)
|
`<p>%s</p><script>setTimeout(function(){window.close();},800);</script></body></html>`, msg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ func TestLogin_Success(t *testing.T) {
|
|||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
"id": "sid-1",
|
"id": "sid-1",
|
||||||
"app": "cdrop",
|
"app": "commilitia-drop",
|
||||||
"access": "at-123",
|
"access": "at-123",
|
||||||
"refresh": "rtk-456",
|
"refresh": "rtk-456",
|
||||||
"access_expires": time.Now().Add(15 * time.Minute).Unix(),
|
"access_expires": time.Now().Add(15 * time.Minute).Unix(),
|
||||||
@@ -63,7 +63,7 @@ func TestLogin_Success(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
defer brokerSrv.Close()
|
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,
|
// Fake browser: parse the /device/authorize URL, assert it carries app + PKCE,
|
||||||
// then GET the loopback redirect with a code + the same state (approved).
|
// 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" {
|
if q.Get("code_challenge") == "" || q.Get("code_challenge_method") != "S256" {
|
||||||
t.Errorf("authorize request missing PKCE challenge: %v", q)
|
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"))
|
t.Errorf("authorize app = %q", q.Get("app"))
|
||||||
}
|
}
|
||||||
cb := q.Get("redirect_uri") + "?code=auth-code-xyz&state=" + url.QueryEscape(q.Get("state"))
|
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) {
|
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) {
|
openURL := func(authURL string) {
|
||||||
u, _ := url.Parse(authURL)
|
u, _ := url.Parse(authURL)
|
||||||
redirect := u.Query().Get("redirect_uri")
|
redirect := u.Query().Get("redirect_uri")
|
||||||
@@ -157,7 +157,7 @@ func TestRefresh_Success(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
defer brokerSrv.Close()
|
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")
|
tok, err := NewFlow(cfg, func(string) {}).Refresh(context.Background(), "old-rtk")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Refresh: %v", err)
|
t.Fatalf("Refresh: %v", err)
|
||||||
@@ -174,7 +174,7 @@ func TestRefresh_Success(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRefresh_EmptyToken(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 {
|
if _, err := NewFlow(cfg, func(string) {}).Refresh(context.Background(), ""); err == nil {
|
||||||
t.Fatal("want error for empty refresh token")
|
t.Fatal("want error for empty refresh token")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,7 +177,11 @@ func SaveSession(res LoginResult) error {
|
|||||||
if enc, err := encryptToken(res.RefreshToken); err == nil {
|
if enc, err := encryptToken(res.RefreshToken); err == nil {
|
||||||
rec.RefreshTokenEnc = enc
|
rec.RefreshTokenEnc = enc
|
||||||
} else {
|
} 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
|
rec.RefreshToken = res.RefreshToken
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,7 +230,7 @@ func LoadSession() (*LoginResult, error) {
|
|||||||
// still unavailable, leaving the file as-is).
|
// still unavailable, leaving the file as-is).
|
||||||
res.RefreshToken = rec.RefreshToken
|
res.RefreshToken = rec.RefreshToken
|
||||||
if err := SaveSession(*res); err != nil {
|
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 != "":
|
case rec.RefreshTokenEnc != "":
|
||||||
if rt, err := decryptToken(rec.RefreshTokenEnc); err == nil {
|
if rt, err := decryptToken(rec.RefreshTokenEnc); err == nil {
|
||||||
@@ -234,7 +238,7 @@ func LoadSession() (*LoginResult, error) {
|
|||||||
} else {
|
} else {
|
||||||
// Key gone / ciphertext corrupt: drop to an access-token-only session;
|
// Key gone / ciphertext corrupt: drop to an access-token-only session;
|
||||||
// the app will require a fresh login once the access_token lapses.
|
// 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
|
return res, nil
|
||||||
@@ -252,7 +256,7 @@ func ClearSession() error {
|
|||||||
}
|
}
|
||||||
if err := keyring.Delete(keyringService, keyringKeyAccount); err != nil &&
|
if err := keyring.Delete(keyringService, keyringKeyAccount); err != nil &&
|
||||||
!errors.Is(err, keyring.ErrNotFound) {
|
!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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ package platform
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
#cgo darwin CFLAGS: -x objective-c -fobjc-arc
|
#cgo darwin CFLAGS: -x objective-c -fobjc-arc
|
||||||
#cgo darwin LDFLAGS: -framework Cocoa
|
#cgo darwin LDFLAGS: -framework Cocoa -framework UniformTypeIdentifiers
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
|
||||||
void cdropStatusBarInstall(const void *iconPNG, int iconLen, const char *title,
|
void cdropStatusBarInstall(const void *iconPNG, int iconLen, const char *title,
|
||||||
|
|||||||
Executable
+33
@@ -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
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://wails.io/schemas/config.v2.json",
|
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||||
"name": "Commilitia Drop Desktop",
|
"name": "Commilitia Drop",
|
||||||
"outputfilename": "Commilitia Drop Desktop",
|
"outputfilename": "Commilitia Drop",
|
||||||
"frontend:install": "cd ../../web && npm install",
|
"frontend:install": "cd ../../web && npm install",
|
||||||
"frontend:build": "cd ../../web && npm run build -- --outDir ../desktop/frontend/dist --emptyOutDir",
|
"frontend:build": "cd ../../web && npm run build -- --outDir ../desktop/frontend/dist --emptyOutDir",
|
||||||
"frontend:dev:watcher": "cd ../../web && npm run dev",
|
"frontend:dev:watcher": "cd ../../web && npm run dev",
|
||||||
"frontend:dev:serverUrl": "auto",
|
"frontend:dev:serverUrl": "auto",
|
||||||
"info": {
|
"info": {
|
||||||
"productName": "Commilitia Drop Desktop"
|
"productName": "Commilitia Drop"
|
||||||
},
|
},
|
||||||
"author": {
|
"author": {
|
||||||
"name": "commilitia",
|
"name": "commilitia",
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ NN-cdrop:
|
|||||||
# CDROP_BROKER_INTERNAL_KEY: REPLACE_WITH_BROKER_INTERNAL_KEY
|
# CDROP_BROKER_INTERNAL_KEY: REPLACE_WITH_BROKER_INTERNAL_KEY
|
||||||
# broker 公开源(浏览器全局 SSO 登录跳转 / 原生设备授权流目标)。
|
# broker 公开源(浏览器全局 SSO 登录跳转 / 原生设备授权流目标)。
|
||||||
# CDROP_BROKER_PUBLIC_URL: https://sso.your-domain.example
|
# CDROP_BROKER_PUBLIC_URL: https://sso.your-domain.example
|
||||||
# 本应用在 broker apps 注册表里的 key(默认 cdrop)。
|
# 本应用在 broker apps 注册表里的 key(默认 commilitia-drop)。
|
||||||
# CDROP_BROKER_APP: cdrop
|
# CDROP_BROKER_APP: commilitia-drop
|
||||||
# 部署公开源(扫码 QR 链接 / CSRF Origin / Web Push 默认联系标识)。
|
# 部署公开源(扫码 QR 链接 / CSRF Origin / Web Push 默认联系标识)。
|
||||||
# CDROP_PUBLIC_URL: https://drop.your-domain.example
|
# CDROP_PUBLIC_URL: https://drop.your-domain.example
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,348 @@
|
|||||||
|
# Commilitia Drop 全客户端构建与安装手册
|
||||||
|
|
||||||
|
本文记录 Web、服务端、Windows、macOS、iPhone 与 iPad 的正式构建、签名、安装和验证流程。
|
||||||
|
命令默认从仓库根目录执行。项目内部目录名和稳定系统身份可以继续使用 `cdrop`;所有用户可见名称、
|
||||||
|
协议和诊断界面统一使用 “Commilitia Drop” 或 `commilitia-drop` 命名空间。
|
||||||
|
|
||||||
|
## 1. 正式客户端矩阵
|
||||||
|
|
||||||
|
| 平台 | 唯一正式实现 | 架构/设备 | 正式产物 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Web | `web/` React SPA / PWA | 现代浏览器 | `web/dist/`,嵌入服务端 |
|
||||||
|
| Windows | `desktop/` Wails | x86-64 | `Commilitia Drop.exe` |
|
||||||
|
| macOS | `desktop/` Wails | Apple Silicon ARM64 | `Commilitia Drop.app`/`.dmg` |
|
||||||
|
| iPhone | `ios/CDrop` SwiftUI | 已登记真机 | `CommilitiaDrop.app` |
|
||||||
|
| iPad | `ios/CDrop` SwiftUI | 已登记真机 | 与 iPhone 共用 target |
|
||||||
|
|
||||||
|
`ios/CDrop` 中的原生 macOS target 只是迁移候选。它在功能对等、三语界面、签名扩展和真机验收
|
||||||
|
全部完成前,不得替代 Wails,也不得作为第二个 macOS 客户端发布。macOS 只考虑 ARM64。
|
||||||
|
|
||||||
|
## 2. 命名与兼容边界
|
||||||
|
|
||||||
|
- 用户可见产品名:`Commilitia Drop`,桌面应用名中不再出现 `Desktop`。
|
||||||
|
- iOS/iPadOS 登录回调:`commilitia-drop://auth-callback`。
|
||||||
|
- WebRTC DataChannel 与 iOS 文件流 Scheme:`commilitia-drop-file`。
|
||||||
|
- Bonjour 服务类型:`_commilitia-drop._tcp`。
|
||||||
|
- Auth Broker application key:`commilitia-drop`。它会出现在 OAuth 参数和 JWT scope 中,属于
|
||||||
|
用户可见/可诊断协议文本。
|
||||||
|
- Bundle ID、App Group、Keychain service、LaunchAgent label、环境变量、数据库和配置目录不直接
|
||||||
|
决定显示名,保持稳定以保护系统权限、钥匙串数据和既有配置。
|
||||||
|
- 每个平台只保留上表的一份正式客户端;迁移验证成功后删除同平台旧名称副本。
|
||||||
|
|
||||||
|
命名复扫时应区分用户可见文本和内部身份。建议至少检查:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
rg -n -i 'commilitia drop desktop|cdrop desktop|cdrop://|_cdrop\._tcp|cdrop-file' \
|
||||||
|
README.md docs auth desktop ios web
|
||||||
|
```
|
||||||
|
|
||||||
|
允许保留的典型内部值包括 `CDROP_*` 环境变量、`net.commilitia.cdrop` LaunchAgent/Bundle ID、
|
||||||
|
源码 package 和存储目录。若某个内部值直接、不可更改地成为系统显示名,则必须单独迁移。
|
||||||
|
|
||||||
|
## 3. 通用前置与密钥规则
|
||||||
|
|
||||||
|
本地需要 Go、Node.js、Wails v2、Just、Xcode Command Line Tools、XcodeGen,以及 iOS 依赖所需的
|
||||||
|
Swift Package Manager。环境配置放在仓库根目录、已被 Git 忽略的 `.env`;变量说明见
|
||||||
|
`.env.example`。
|
||||||
|
|
||||||
|
签名和密钥遵循以下规则:
|
||||||
|
|
||||||
|
- 不在仓库记录 `.p8`、私钥、Broker internal key、VAPID 私钥或证书导出文件。
|
||||||
|
- iPhone/iPad 必须复用钥匙串中既有 Apple Development 证书。把其 SHA-1 写入
|
||||||
|
`CDROP_APPLE_DEVELOPMENT_IDENTITY`;recipe 会在构建前检查身份并把该值固定传给
|
||||||
|
`xcodebuild`。不得用 Xcode GUI,不得新建证书或修改私钥 ACL。
|
||||||
|
- macOS 正式分发必须复用既有 Developer ID Application 证书;完整身份名写入
|
||||||
|
`CDROP_DEVID_IDENTITY`。
|
||||||
|
- App Store Connect API Key 只通过 `CDROP_ASC_KEY_PATH`、`CDROP_ASC_KEY_ID` 和
|
||||||
|
`CDROP_ASC_ISSUER_ID` 引用。
|
||||||
|
- 登录钥匙串必须已经解锁。`errSecInternalComponent` 通常表示签名进程不能使用私钥;先在用户的
|
||||||
|
普通 Terminal 解锁登录钥匙串,再重试同一证书,不要创建替代证书。
|
||||||
|
|
||||||
|
构建前可做基础检查:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git status --short
|
||||||
|
go version
|
||||||
|
node --version
|
||||||
|
just desktop-doctor
|
||||||
|
security find-identity -v -p codesigning
|
||||||
|
xcrun devicectl list devices
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Web 与服务端
|
||||||
|
|
||||||
|
### 4.1 构建和验证 Web
|
||||||
|
|
||||||
|
```sh
|
||||||
|
just typecheck-front
|
||||||
|
just build-front
|
||||||
|
```
|
||||||
|
|
||||||
|
正式 Web 构建输出在 `web/dist/`。服务端通过嵌入静态文件提供 SPA、PWA manifest、service worker
|
||||||
|
和 iOS 原生引擎页,因此发布 Web 改动时必须重新构建服务端镜像,不能只替换单个 HTML 文件。
|
||||||
|
|
||||||
|
```sh
|
||||||
|
just test
|
||||||
|
just docker-image
|
||||||
|
```
|
||||||
|
|
||||||
|
部署新镜像后至少验证:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/healthz
|
||||||
|
/api/auth/config
|
||||||
|
/site.webmanifest
|
||||||
|
/engine.html
|
||||||
|
```
|
||||||
|
|
||||||
|
`/api/auth/config` 应返回 `broker_app=commilitia-drop`;manifest 的 `name` 与 `short_name` 均应为
|
||||||
|
“Commilitia Drop”。登录、扫码批准、设备续期和传输必须各做一次实际冒烟。
|
||||||
|
|
||||||
|
### 4.2 Auth Broker 原位迁移
|
||||||
|
|
||||||
|
Broker application 不创建并行副本,直接把既有 `cdrop` 项改为:
|
||||||
|
|
||||||
|
```text
|
||||||
|
key: commilitia-drop
|
||||||
|
callback: commilitia-drop://auth-callback
|
||||||
|
```
|
||||||
|
|
||||||
|
只需在修改前备份一次 Broker 配置;客户端和服务端源码由 Git 历史恢复,不另做文件备份。迁移后旧
|
||||||
|
session 可以全部失效,客户端重新登录。Broker、Caddy 和服务端的 application key 必须同时一致;
|
||||||
|
否则批准可以完成,但客户端会在兑换或 scope 校验阶段显示“登录失败”。
|
||||||
|
|
||||||
|
若 Caddyfile 以单文件 bind mount 进入容器,宿主机上的 `sed -i`/原子替换会生成新 inode,而运行
|
||||||
|
容器仍可能读旧 inode。修改后必须热加载,并从 Caddy admin API 或容器内实际挂载内容验证生效;
|
||||||
|
不能只查看宿主机路径。
|
||||||
|
|
||||||
|
## 5. macOS 正式客户端(Wails ARM64)
|
||||||
|
|
||||||
|
### 5.1 开发构建
|
||||||
|
|
||||||
|
```sh
|
||||||
|
just desktop-build-mac
|
||||||
|
```
|
||||||
|
|
||||||
|
任务先构建最新 Web 前端,再执行 `wails build -clean -s -platform darwin/arm64`。输出为:
|
||||||
|
|
||||||
|
```text
|
||||||
|
desktop/build/bin/Commilitia Drop.app
|
||||||
|
```
|
||||||
|
|
||||||
|
`-clean` 是必要的:macOS 与 Windows 共用 `desktop/build/bin/`,另一平台的残留产物可能被 Wails/Go
|
||||||
|
误判为构建输入。不要把原生 macOS 候选 target 的产物放进正式分发目录。
|
||||||
|
|
||||||
|
### 5.2 Developer ID 签名、公证与 DMG
|
||||||
|
|
||||||
|
在 `.env` 配齐以下变量:
|
||||||
|
|
||||||
|
```text
|
||||||
|
CDROP_DEVID_IDENTITY
|
||||||
|
CDROP_ASC_KEY_PATH
|
||||||
|
CDROP_ASC_KEY_ID
|
||||||
|
CDROP_ASC_ISSUER_ID
|
||||||
|
```
|
||||||
|
|
||||||
|
然后运行:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
just desktop-dist-mac
|
||||||
|
```
|
||||||
|
|
||||||
|
该 recipe 会依次完成:
|
||||||
|
|
||||||
|
1. ARM64 Wails 构建;
|
||||||
|
2. 使用既有 Developer ID Application 身份、hardened runtime 和 secure timestamp 签名 `.app`;
|
||||||
|
3. 将 `.app` 以 `ditto` 打包提交 Apple 公证,等待 `Accepted`,再 staple/validate;
|
||||||
|
4. 生成含 `.app` 和 `/Applications` 链接的压缩 DMG;
|
||||||
|
5. 对 DMG 签名、单独提交公证并 staple/validate;
|
||||||
|
6. 用 Gatekeeper `spctl` 分别评估 app 和 DMG。
|
||||||
|
|
||||||
|
最终文件为 `desktop/build/bin/Commilitia Drop.dmg`。发布前再次检查:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
codesign --verify --deep --strict --verbose=2 \
|
||||||
|
'desktop/build/bin/Commilitia Drop.app'
|
||||||
|
xcrun stapler validate 'desktop/build/bin/Commilitia Drop.app'
|
||||||
|
xcrun stapler validate 'desktop/build/bin/Commilitia Drop.dmg'
|
||||||
|
spctl --assess --type execute --verbose=2 \
|
||||||
|
'desktop/build/bin/Commilitia Drop.app'
|
||||||
|
spctl --assess --type open --context context:primary-signature --verbose=2 \
|
||||||
|
'desktop/build/bin/Commilitia Drop.dmg'
|
||||||
|
```
|
||||||
|
|
||||||
|
2026-07-31 已用既有 Developer ID 证书验证完整链路:app 与 DMG 均获 Apple `Accepted`,并完成
|
||||||
|
staple 和 Gatekeeper 的 `Notarized Developer ID` 评估。
|
||||||
|
|
||||||
|
### 5.3 本机安装
|
||||||
|
|
||||||
|
先验证新 app,再替换 `/Applications` 中的正式客户端;不保留第二个 `Desktop` 副本:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ditto 'desktop/build/bin/Commilitia Drop.app' \
|
||||||
|
'/Applications/Commilitia Drop.app'
|
||||||
|
```
|
||||||
|
|
||||||
|
LaunchAgent label `net.commilitia.cdrop` 是稳定内部身份,可以保留;但其 `ProgramArguments` 必须指向:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/Applications/Commilitia Drop.app/Contents/MacOS/Commilitia Drop
|
||||||
|
```
|
||||||
|
|
||||||
|
更新磁盘 plist 后需要重新 bootstrap,不能只看文件,因为 launchd 可能仍缓存旧路径。最终检查已安装
|
||||||
|
app 的签名、公证、架构、进程路径,以及“登录后启动”开关。
|
||||||
|
|
||||||
|
## 6. Windows 正式客户端(Wails x64)
|
||||||
|
|
||||||
|
### 6.1 构建
|
||||||
|
|
||||||
|
```sh
|
||||||
|
just desktop-build-win
|
||||||
|
```
|
||||||
|
|
||||||
|
输出为 `desktop/build/bin/Commilitia Drop.exe`。在 macOS 上交叉构建无需 MinGW;Windows 特有通知
|
||||||
|
和 WebView2 绑定不依赖 Darwin CGO。当前 EXE 尚无 Authenticode 签名,Windows 会报告
|
||||||
|
`NotSigned`,并可能显示 SmartScreen 提示;获得 Windows 代码签名证书后应把签名步骤加入 recipe。
|
||||||
|
|
||||||
|
### 6.2 通过 mDNS 无代理直连安装
|
||||||
|
|
||||||
|
目标主机使用 `Desktop-C.local`、SSH 端口 `223`、用户 `commilitia`。访问该主机不得使用任何代理。
|
||||||
|
先确认 shell、系统和 SSH 配置没有代理:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
env | rg -i '^(http|https|all|ftp|no)_proxy='
|
||||||
|
scutil --proxy
|
||||||
|
ssh -G -p 223 commilitia@Desktop-C.local | \
|
||||||
|
rg -i '^(hostname|user|port|proxycommand|proxyjump) '
|
||||||
|
```
|
||||||
|
|
||||||
|
mDNS 可能先返回不可达 IPv6,使 SSH 看似超时;继续保留 mDNS 主机名并用 `-4` 强制 IPv4。显式清除
|
||||||
|
代理变量、禁用 SSH 代理和跳板,并建立任务专用、10 分钟 TTL 的复用连接:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
env -u HTTP_PROXY -u HTTPS_PROXY -u ALL_PROXY \
|
||||||
|
-u http_proxy -u https_proxy -u all_proxy \
|
||||||
|
ssh -4 -M -S /tmp/commilitia-drop-windows-%C \
|
||||||
|
-o ControlPersist=600 -o ProxyCommand=none -o ProxyJump=none \
|
||||||
|
-p 223 commilitia@Desktop-C.local exit
|
||||||
|
```
|
||||||
|
|
||||||
|
复用前用 `stat` 检查 socket 创建时间仍在 TTL 内,并执行 `ssh -O check`。安装流程为:
|
||||||
|
|
||||||
|
1. 在 Windows 上只读侦查 `D:\Tools`、匹配进程和
|
||||||
|
`HKCU\Software\Microsoft\Windows\CurrentVersion\Run`;
|
||||||
|
2. 本地记录 EXE 的 SHA-256;
|
||||||
|
3. 用同一 `-4`、`ProxyCommand=none`、`ProxyJump=none` 连接上传到用户目录暂存;
|
||||||
|
4. 远端再次校验 SHA-256;
|
||||||
|
5. 停止旧 `D:\Tools\Commilitia Drop Desktop.exe` 进程;
|
||||||
|
6. 先复制为 `Commilitia Drop.installing.exe` 并再次校验,再原子移到
|
||||||
|
`D:\Tools\Commilitia Drop.exe`;
|
||||||
|
7. 把既有 “Commilitia Drop” Run 项改为
|
||||||
|
`"D:\Tools\Commilitia Drop.exe" --hidden`;
|
||||||
|
8. 只有新文件哈希正确后才删除旧 `Desktop` 文件与上传暂存文件;
|
||||||
|
9. 从 SSH 服务会话直接 `Start-Process` 可能因没有交互桌面而立即退出。使用当前用户完整 Windows
|
||||||
|
身份注册 `LogonType=Interactive` 的一次性计划任务,启动后立即删除任务;
|
||||||
|
10. 独立复核新进程在交互 Session、旧文件不存在、Run 项正确且应用日志无新错误。
|
||||||
|
|
||||||
|
2026-07-31 在 `Desktop-C.local` 已完成上述流程,正式路径为
|
||||||
|
`D:\Tools\Commilitia Drop.exe`,旧 `Desktop` 文件已删除,登录启动项和运行进程均指向新路径。
|
||||||
|
|
||||||
|
## 7. iPhone 与 iPad
|
||||||
|
|
||||||
|
两种设备共用 `CommilitiaDrop` scheme 和一套签名约束。全程使用 CLI,不打开 Xcode GUI。
|
||||||
|
|
||||||
|
### 7.1 模拟器验证
|
||||||
|
|
||||||
|
```sh
|
||||||
|
just ios-sim-build
|
||||||
|
```
|
||||||
|
|
||||||
|
该任务会拉取/生成品牌字体、运行 XcodeGen、解析 SPM 的 libwebrtc,并针对通用 iOS Simulator
|
||||||
|
目标编译。模拟器不需要 Apple 签名,但不能替代真机的 URL Scheme、本地网络、APNs、Share
|
||||||
|
Extension 和后台行为验收。
|
||||||
|
|
||||||
|
### 7.2 复用既有证书真机构建
|
||||||
|
|
||||||
|
在 `.env` 配齐:
|
||||||
|
|
||||||
|
```text
|
||||||
|
CDROP_TEAM_ID
|
||||||
|
CDROP_ASC_KEY_PATH
|
||||||
|
CDROP_ASC_KEY_ID
|
||||||
|
CDROP_ASC_ISSUER_ID
|
||||||
|
CDROP_APPLE_DEVELOPMENT_IDENTITY=<既有 Apple Development 证书 SHA-1>
|
||||||
|
```
|
||||||
|
|
||||||
|
确认设备与身份:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
just ios-devices
|
||||||
|
security find-identity -v -p codesigning | \
|
||||||
|
rg -F "$CDROP_APPLE_DEVELOPMENT_IDENTITY"
|
||||||
|
```
|
||||||
|
|
||||||
|
分别覆盖安装 iPhone 与 iPad:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
just ios-device <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 app/DMG | ARM64、Developer ID、hardened runtime、Accepted、staple、Gatekeeper |
|
||||||
|
| Windows EXE | PE32+ x86-64、SHA-256、唯一文件、Run 项、交互 Session 进程 |
|
||||||
|
| iPhone/iPad | 既有证书 SHA-1、签名验证、显示名、URL Scheme、Broker 登录、真机传输 |
|
||||||
|
|
||||||
|
提交时不加入 `.env`、`.p8`、构建产物、临时 PowerShell/安装脚本或 Broker 备份。客户端源码和构建
|
||||||
|
流程由 Git 历史恢复;Auth Broker 配置备份由服务端运维位置单独保留。
|
||||||
@@ -96,7 +96,7 @@ token 用 `CDROP_HS256_SECRET` 派生的 32 字节 HMAC 密钥签发(SHA-256
|
|||||||
- 标头:`Authorization` = 文本 `Bearer ` 后插变量 `TOKEN`;`X-Device-Name` = 变量 `NAME`。
|
- 标头:`Authorization` = 文本 `Bearer ` 后插变量 `TOKEN`;`X-Device-Name` = 变量 `NAME`。
|
||||||
6. 「获取词典值」:键 `content`,输入为上一步结果。
|
6. 「获取词典值」:键 `content`,输入为上一步结果。
|
||||||
7. 「如果」上一步「有任何值」:
|
7. 「如果」上一步「有任何值」:
|
||||||
- 「拷贝到剪贴板」= 上一步值;(可选)「显示通知」`已从 cdrop 拉取`。
|
- 「拷贝到剪贴板」= 上一步值;(可选)「显示通知」`已从 Commilitia Drop 拉取`。
|
||||||
- 否则:(可选)「显示通知」`云端剪贴板为空`。
|
- 否则:(可选)「显示通知」`云端剪贴板为空`。
|
||||||
|
|
||||||
### 「上传到云端」分支
|
### 「上传到云端」分支
|
||||||
@@ -108,14 +108,14 @@ token 用 `CDROP_HS256_SECRET` 派生的 32 字节 HMAC 密钥签发(SHA-256
|
|||||||
- 方法:`PUT`。
|
- 方法:`PUT`。
|
||||||
- 请求体:`JSON`,两字段——`content`(文本)= 变量「剪贴板」;`content_type`(文本)= `text/plain`。
|
- 请求体:`JSON`,两字段——`content`(文本)= 变量「剪贴板」;`content_type`(文本)= `text/plain`。
|
||||||
- 标头:`Authorization` = `Bearer ` + 变量 `TOKEN`;`X-Device-Name` = 变量 `NAME`。(请求体选 JSON 时通常自动带 `Content-Type: application/json`,没有就手动加。)
|
- 标头:`Authorization` = `Bearer ` + 变量 `TOKEN`;`X-Device-Name` = 变量 `NAME`。(请求体选 JSON 时通常自动带 `Content-Type: application/json`,没有就手动加。)
|
||||||
- (可选)「显示通知」`已上传到 cdrop`。
|
- (可选)「显示通知」`已上传到 Commilitia Drop`。
|
||||||
- 否则:(可选)「显示通知」`本机剪贴板为空`。
|
- 否则:(可选)「显示通知」`本机剪贴板为空`。
|
||||||
|
|
||||||
### 配置 Import Questions(分享前)
|
### 配置 Import Questions(分享前)
|
||||||
|
|
||||||
打开指令详情(设置)→「导入问题」区,添加三个,对应顶部三条「文本」:
|
打开指令详情(设置)→「导入问题」区,添加三个,对应顶部三条「文本」:
|
||||||
- 服务器地址 → 问题「服务器地址」,默认答案 `https://drop.commilitia.net`。
|
- 服务器地址 → 问题「服务器地址」,默认答案 `https://drop.commilitia.net`。
|
||||||
- 令牌 → 问题「粘贴 cdrop 令牌」,无默认。
|
- 令牌 → 问题「粘贴 Commilitia Drop 令牌」,无默认。
|
||||||
- 设备名 → 问题「设备名(仅 ASCII)」,默认 `iPhone`。
|
- 设备名 → 问题「设备名(仅 ASCII)」,默认 `iPhone`。
|
||||||
|
|
||||||
### 分发
|
### 分发
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ type Config struct {
|
|||||||
// 内网直连(绝不经公网反代,以保 /internal/* 与 /refresh 可达)。
|
// 内网直连(绝不经公网反代,以保 /internal/* 与 /refresh 可达)。
|
||||||
// BrokerInternalKey —— broker /internal/* 端点的共享密钥(作 X-Internal-Key 发送),
|
// BrokerInternalKey —— broker /internal/* 端点的共享密钥(作 X-Internal-Key 发送),
|
||||||
// prod 必填。
|
// prod 必填。
|
||||||
// BrokerApp —— 本应用在 broker apps 注册表里的 key(默认 "cdrop")。
|
// BrokerApp —— 本应用在 broker apps 注册表里的 key(默认 "commilitia-drop")。
|
||||||
BrokerBaseURL string `koanf:"broker_base_url"`
|
BrokerBaseURL string `koanf:"broker_base_url"`
|
||||||
BrokerInternalKey string `koanf:"broker_internal_key"`
|
BrokerInternalKey string `koanf:"broker_internal_key"`
|
||||||
BrokerApp string `koanf:"broker_app"`
|
BrokerApp string `koanf:"broker_app"`
|
||||||
@@ -96,7 +96,7 @@ type Config struct {
|
|||||||
// APNSKeyPath — filesystem path to the .p8 (PEM PKCS8 ES256) key Apple issues.
|
// APNSKeyPath — filesystem path to the .p8 (PEM PKCS8 ES256) key Apple issues.
|
||||||
// APNSKeyID — the 10-character Key ID from the Apple Developer portal.
|
// APNSKeyID — the 10-character Key ID from the Apple Developer portal.
|
||||||
// APNSTeamID — the 10-character Team 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).
|
// APNSEnv — "prod" (default) or "sandbox" (dev/TestFlight builds).
|
||||||
// When unset, APNs is disabled and the register endpoint returns 503.
|
// When unset, APNs is disabled and the register endpoint returns 503.
|
||||||
APNSKeyPath string `koanf:"apns_key_path"`
|
APNSKeyPath string `koanf:"apns_key_path"`
|
||||||
@@ -123,12 +123,12 @@ func (c *Config) QRLoginOn() bool {
|
|||||||
return c.QRLoginEnabled && c.BrokerBaseURL != ""
|
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 {
|
func (c *Config) BrokerAppOrDefault() string {
|
||||||
if c.BrokerApp != "" {
|
if c.BrokerApp != "" {
|
||||||
return c.BrokerApp
|
return c.BrokerApp
|
||||||
}
|
}
|
||||||
return "cdrop"
|
return "commilitia-drop"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load reads config from optional ./config.yaml then overrides with CDROP_* env.
|
// 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("clipboard_debounce_sec", 3)
|
||||||
k.Set("qr_login_enabled", true)
|
k.Set("qr_login_enabled", true)
|
||||||
k.Set("qr_request_ttl_seconds", 120)
|
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_access_ttl_seconds", 900)
|
||||||
k.Set("full_refresh_ttl_seconds", 604800)
|
k.Set("full_refresh_ttl_seconds", 604800)
|
||||||
k.Set("guest_access_ttl_seconds", 900)
|
k.Set("guest_access_ttl_seconds", 900)
|
||||||
|
|||||||
+11
-11
@@ -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}
|
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{
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
"id": sid, "app": "cdrop",
|
"id": sid, "app": "commilitia-drop",
|
||||||
"access": "acc-" + sid, "refresh": "rtk-" + sid,
|
"access": "acc-" + sid, "refresh": "rtk-" + sid,
|
||||||
"access_expires": time.Now().Add(15 * time.Minute).Unix(),
|
"access_expires": time.Now().Add(15 * time.Minute).Unix(),
|
||||||
"refresh_expires": time.Now().Add(24 * time.Hour).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)
|
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
|
// 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)
|
handler.ServeHTTP(w, r)
|
||||||
return w.Code
|
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)
|
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)
|
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"] {
|
if !st.revoked["sid-1"] {
|
||||||
t.Error("broker session sid-1 was not revoked")
|
t.Error("broker session sid-1 was not revoked")
|
||||||
}
|
}
|
||||||
if st.lastRevokeApp != "cdrop" {
|
if st.lastRevokeApp != "commilitia-drop" {
|
||||||
t.Errorf("revoke X-Broker-App: got %q, want cdrop", st.lastRevokeApp)
|
t.Errorf("revoke X-Broker-App: got %q, want commilitia-drop", st.lastRevokeApp)
|
||||||
}
|
}
|
||||||
if _, err := s.queries.GetDevice(context.Background(), got.DeviceID); err == nil {
|
if _, err := s.queries.GetDevice(context.Background(), got.DeviceID); err == nil {
|
||||||
t.Error("device row should be gone after delete")
|
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))
|
got := decodeStatus(t, qrStatus(s, start.RequestID, start.PollSecret))
|
||||||
|
|
||||||
r := httptest.NewRequest(http.MethodGet, "/api/auth/sessions", nil)
|
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()
|
w := httptest.NewRecorder()
|
||||||
s.handleSessionsList(w, r)
|
s.handleSessionsList(w, r)
|
||||||
if w.Code != http.StatusOK {
|
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" {
|
if st.lastMint["tier"] != "full" || st.lastMint["meta"] != "dev_browser01" || st.lastMint["label"] != "Laptop" {
|
||||||
t.Errorf("mint params: %+v", st.lastMint)
|
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 {
|
if len(list) != 1 || list[0].DeviceID != "dev_browser01" || list[0].Kind != "browser" || !list[0].Current {
|
||||||
t.Errorf("unified list wrong: %+v", list)
|
t.Errorf("unified list wrong: %+v", list)
|
||||||
}
|
}
|
||||||
@@ -474,7 +474,7 @@ func TestDeviceSession_Idempotent(t *testing.T) {
|
|||||||
s, _ := newQRTestServer(t)
|
s, _ := newQRTestServer(t)
|
||||||
_ = deviceSession(t, s, "owner", "full", "dev_same01", "Laptop", "browser", "")
|
_ = deviceSession(t, s, "owner", "full", "dev_same01", "Laptop", "browser", "")
|
||||||
_ = deviceSession(t, s, "owner", "full", "dev_same01", "Laptop Renamed", "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 {
|
if len(list) != 1 {
|
||||||
t.Fatalf("idempotent re-mint: got %d sessions, want 1", len(list))
|
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)
|
t.Fatalf("bootstrap mint: %v", err)
|
||||||
}
|
}
|
||||||
_ = deviceSession(t, s, "owner", "full", "dev_real01", "Laptop", "browser", "")
|
_ = 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" {
|
if len(list) != 1 || list[0].DeviceID != "dev_real01" {
|
||||||
t.Fatalf("metaless bootstrap not filtered: %+v", list)
|
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.
|
// A restricted guest minting its device session stays guest — no escalation to full.
|
||||||
func TestDeviceSession_GuestTierNotEscalated(t *testing.T) {
|
func TestDeviceSession_GuestTierNotEscalated(t *testing.T) {
|
||||||
s, st := newQRTestServer(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" {
|
if st.lastMint["tier"] != "guest" {
|
||||||
t.Errorf("guest caller minted tier %v, want guest", st.lastMint["tier"])
|
t.Errorf("guest caller minted tier %v, want guest", st.lastMint["tier"])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ type Claims struct {
|
|||||||
Avatar string // X-Auth-Avatar (profile picture URL from the broker account); may be empty
|
Avatar string // X-Auth-Avatar (profile picture URL from the broker account); may be empty
|
||||||
Groups []string // X-Auth-Roles, comma-split
|
Groups []string // X-Auth-Roles, comma-split
|
||||||
// Scope is the raw X-Auth-Scope: a global SSO user is "full"; a cdrop delegated
|
// 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
|
Scope string
|
||||||
// DeviceID is X-Auth-Meta: the cdrop device_id this session was minted for — the
|
// 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
|
// 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
|
// 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.
|
// Shared by Claims.Tier() and the session-list overlay so the two never diverge.
|
||||||
func ScopeTier(scope string) string {
|
func ScopeTier(scope string) string {
|
||||||
if i := strings.LastIndex(scope, ":"); i >= 0 {
|
if i := strings.LastIndex(scope, ":"); i >= 0 {
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ func bearerToken(r *http.Request) (string, bool) {
|
|||||||
|
|
||||||
func unauthorized(w http.ResponseWriter, reason string) {
|
func unauthorized(w http.ResponseWriter, reason string) {
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
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)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
_ = json.NewEncoder(w).Encode(map[string]string{
|
_ = json.NewEncoder(w).Encode(map[string]string{
|
||||||
"error": "unauthorized",
|
"error": "unauthorized",
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func TestMiddleware_ProdReadsAuthHeaders(t *testing.T) {
|
|||||||
a := New(&config.Config{AuthMode: "prod"}, &fakeDeviceStore{})
|
a := New(&config.Config{AuthMode: "prod"}, &fakeDeviceStore{})
|
||||||
r := httptest.NewRequest(http.MethodGet, "/api/me", nil)
|
r := httptest.NewRequest(http.MethodGet, "/api/me", nil)
|
||||||
r.Header.Set("X-Auth-Subject", "user-1")
|
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-Meta", "dev_abc")
|
||||||
r.Header.Set("X-Auth-Name", "Alice")
|
r.Header.Set("X-Auth-Name", "Alice")
|
||||||
r.Header.Set("X-Auth-Roles", "admin, user")
|
r.Header.Set("X-Auth-Roles", "admin, user")
|
||||||
@@ -54,7 +54,7 @@ func TestMiddleware_ProdReadsAuthHeaders(t *testing.T) {
|
|||||||
t.Errorf("claims wrong: %+v", c)
|
t.Errorf("claims wrong: %+v", c)
|
||||||
}
|
}
|
||||||
if !c.Guest() {
|
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" {
|
if len(c.Groups) != 2 || c.Groups[0] != "admin" || c.Groups[1] != "user" {
|
||||||
t.Errorf("groups: got %v", c.Groups)
|
t.Errorf("groups: got %v", c.Groups)
|
||||||
@@ -78,7 +78,7 @@ func TestMiddleware_TouchesManagedDevice(t *testing.T) {
|
|||||||
a := New(&config.Config{AuthMode: "prod"}, fs)
|
a := New(&config.Config{AuthMode: "prod"}, fs)
|
||||||
r := httptest.NewRequest(http.MethodGet, "/api/me", nil)
|
r := httptest.NewRequest(http.MethodGet, "/api/me", nil)
|
||||||
r.Header.Set("X-Auth-Subject", "user-1")
|
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")
|
r.Header.Set("X-Auth-Meta", "dev_x")
|
||||||
runMiddleware(a, r)
|
runMiddleware(a, r)
|
||||||
if len(fs.touched) != 1 {
|
if len(fs.touched) != 1 {
|
||||||
@@ -133,10 +133,10 @@ func TestClaimsTier(t *testing.T) {
|
|||||||
tier string
|
tier string
|
||||||
guest bool
|
guest bool
|
||||||
}{
|
}{
|
||||||
{"app:cdrop:guest", "guest", true},
|
{"app:commilitia-drop:guest", "guest", true},
|
||||||
{"app:cdrop:full", "full", false},
|
{"app:commilitia-drop:full", "full", false},
|
||||||
{"full", "full", false},
|
{"full", "full", false},
|
||||||
{"app:cdrop", "cdrop", false},
|
{"app:commilitia-drop", "commilitia-drop", false},
|
||||||
{"", "", false},
|
{"", "", false},
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
// 原生 macOS 登录:以 Commilitia 账户(Auth Broker)登录为主,免相机——ASWebAuthenticationSession
|
// 原生 macOS 登录:以 Commilitia 账户(Auth Broker)登录为主,免相机——ASWebAuthenticationSession
|
||||||
// 弹系统授权窗,回调经 cdrop:// 回到 app(见 BrokerLogin / AuthManager.startBrokerLogin)。不复用
|
// 弹系统授权窗,回调经 commilitia-drop:// 回到 app(见 BrokerLogin /
|
||||||
|
// AuthManager.startBrokerLogin)。不复用
|
||||||
// iOS 的 LoginView(其二维码 / UIImage 路径为 iOS 专属,且没人扫 Mac 屏幕来登录 Mac 自己)。
|
// iOS 的 LoginView(其二维码 / UIImage 路径为 iOS 专属,且没人扫 Mac 屏幕来登录 Mac 自己)。
|
||||||
struct MacLoginView: View
|
struct MacLoginView: View
|
||||||
{
|
{
|
||||||
|
|||||||
+31
-9
@@ -1,6 +1,9 @@
|
|||||||
# cdrop iOS 真机分发手册
|
# Commilitia Drop iPhone/iPad 真机分发手册
|
||||||
|
|
||||||
把 cdrop 装到你自己的 iPhone(开发签名)。代码侧全部就绪并经模拟器验证;真机只差**账号 + 签名**这一层。本手册走 **ASC API Key 自动 provisioning + 全 CLI** 路线——一把 App Store Connect API Key,`-allowProvisioningUpdates` 自动登记连接的设备 + 创建/更新 App ID / App Group / Push / profile / 开发证书,构建装机一条命令,**全程不碰 Xcode GUI、不必手动建 profile**(绕过手动 profile「设备列表只剩 Mac」的坑)。
|
把 Commilitia Drop 覆盖安装到已登记的 iPhone 与 iPad(开发签名)。本手册走
|
||||||
|
**既有 Apple Development 证书 + ASC API Key provisioning + 全 CLI** 路线:签名身份固定为
|
||||||
|
钥匙串内既有证书的 SHA-1;ASC Key 只用于登记设备、更新 App ID/App Group/Push 能力和 profile。
|
||||||
|
构建装机一条命令,**全程不碰 Xcode GUI、不新建开发证书、不修改私钥 ACL**。
|
||||||
|
|
||||||
## 包名与标识
|
## 包名与标识
|
||||||
|
|
||||||
@@ -15,13 +18,24 @@
|
|||||||
## 前置
|
## 前置
|
||||||
- 付费 Apple Developer Program($99/年)。
|
- 付费 Apple Developer Program($99/年)。
|
||||||
- 一台 iPhone(iOS 26)+ Mac(Xcode 26 命令行工具)。
|
- 一台 iPhone(iOS 26)+ Mac(Xcode 26 命令行工具)。
|
||||||
- 一个已登录的 cdrop(网页 / 桌面)——用来扫码批准本机登录。
|
- 一个已登录的 Commilitia Drop(网页 / 桌面)——用来扫码批准本机登录。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## A. ASC API Key + .env(核心,一次性)
|
## A. 既有签名身份 + ASC API Key + .env
|
||||||
|
|
||||||
`-allowProvisioningUpdates` 凭一把 App Store Connect API Key 替你在门户自动登记设备、建/改 App ID(含 App Groups + Push 能力)、建 App Group、建 profile、建/下开发证书——**不必手动建 profile**。
|
先用 CLI 列出当前钥匙串中可用的代码签名身份:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
security find-identity -v -p codesigning
|
||||||
|
```
|
||||||
|
|
||||||
|
找到必须复用的 `Apple Development` 证书,记录行首 40 位 SHA-1。若身份不存在或私钥不可用,停止
|
||||||
|
构建并解锁当前登录钥匙串;不要让自动签名生成替代证书。
|
||||||
|
|
||||||
|
`-allowProvisioningUpdates` 凭 App Store Connect API Key 自动登记设备、建/改 App ID(含 App
|
||||||
|
Groups + Push 能力)、更新 App Group 与 profile,免去手动 profile 的设备选择问题。签名证书由
|
||||||
|
`CDROP_APPLE_DEVELOPMENT_IDENTITY` 固定,不由 provisioning 自行选择。
|
||||||
|
|
||||||
**生成 API Key**:appstoreconnect.apple.com → **用户和访问** → **集成** → **App Store Connect API** → **团队密钥** → 「+」→ 角色 **Admin**(要能管设备 / profile / 标识符)→ 命名 → 生成 → 下载 `AuthKey_XXXXX.p8`(**仅一次**)。记下三样:
|
**生成 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_PATH=/绝对路径/AuthKey_XXXXX.p8
|
||||||
CDROP_ASC_KEY_ID=XXXXXXXXXX
|
CDROP_ASC_KEY_ID=XXXXXXXXXX
|
||||||
CDROP_ASC_ISSUER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
CDROP_ASC_ISSUER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||||
|
CDROP_APPLE_DEVELOPMENT_IDENTITY=<既有证书的40位SHA-1>
|
||||||
```
|
```
|
||||||
|
|
||||||
`.p8` 放仓库外即可,`CDROP_ASC_KEY_PATH` 指它绝对路径(落本目录则被 `*.p8` 忽略)。
|
`.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)。
|
**顺手: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 <你的设备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 开发证书,无需手动「信任开发者」)。
|
- 首次装机后,iPhone 上首启该开发者 app 即可直接跑(付费 ADP 开发证书,无需手动「信任开发者」)。
|
||||||
|
|
||||||
|
若出现 `errSecInternalComponent`,优先在当前用户的普通 Terminal 解锁登录钥匙串,然后重试原证书。
|
||||||
|
不要创建新证书、修改钥匙串 ACL,或改用 Xcode GUI。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## C. 服务端前置
|
## 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`)。缺配置则推送惰性关闭,其余功能照常。
|
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` 指向可达引擎。
|
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 候选、走真直连;拒绝则回退中继仍可传。
|
- [ ] **本地网络权限 + 同内网直连**(R-iOS-6):首次同内网传输弹 `NSLocalNetworkUsageDescription`;授予后 ICE 收 host 候选、走真直连;拒绝则回退中继仍可传。
|
||||||
- [ ] 发送 / 接收:选文件 → 选设备 → 对端收到;对端发来落 Files(Documents)。
|
- [ ] 发送 / 接收:选文件 → 选设备 → 对端收到;对端发来落 Files(Documents)。
|
||||||
- [ ] **大文件流式发送**(R-iOS-4 已实现):发大文件,WebView 内存应有界(按 Range 块拉取,不整文件入内存)。
|
- [ ] **大文件流式发送**(R-iOS-4 已实现):发大文件,WebView 内存应有界(按 Range 块拉取,不整文件入内存)。
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>$(PRODUCT_NAME)</string>
|
<string>Commilitia Drop</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>XPC!</string>
|
<string>XPC!</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ final class ShareViewController: UIViewController
|
|||||||
// (立即 completeRequest 会拆掉扩展进程、可能取消尚未真正开始的唤起)。文件留在收件箱供主 app 接手。
|
// (立即 completeRequest 会拆掉扩展进程、可能取消尚未真正开始的唤起)。文件留在收件箱供主 app 接手。
|
||||||
private func openAndFinish()
|
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)
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35)
|
||||||
{ [weak self] in
|
{ [weak self] in
|
||||||
self?.extensionContext?.completeRequest(returningItems: nil)
|
self?.extensionContext?.completeRequest(returningItems: nil)
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ final class AuthManager
|
|||||||
let verifier = PKCE.randomToken(64)
|
let verifier = PKCE.randomToken(64)
|
||||||
let challenge = PKCE.challenge(for: verifier)
|
let challenge = PKCE.challenge(for: verifier)
|
||||||
let state = PKCE.randomToken(24)
|
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")!
|
var comp = URLComponents(string: cfg.brokerURL.trimmingTrailingSlash() + "/device/authorize")!
|
||||||
comp.queryItems = [
|
comp.queryItems = [
|
||||||
@@ -144,13 +144,13 @@ final class AuthManager
|
|||||||
URLQueryItem(name: "state", value: state),
|
URLQueryItem(name: "state", value: state),
|
||||||
URLQueryItem(name: "code_challenge", value: challenge),
|
URLQueryItem(name: "code_challenge", value: challenge),
|
||||||
URLQueryItem(name: "code_challenge_method", value: "S256"),
|
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 }
|
guard let authURL = comp.url else { throw BrokerLoginError.incompleteConfig }
|
||||||
|
|
||||||
let flow = BrokerAuthFlow()
|
let flow = BrokerAuthFlow()
|
||||||
brokerFlow = flow
|
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
|
brokerFlow = nil
|
||||||
if gen != loginGeneration { return }
|
if gen != loginGeneration { return }
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,15 @@ import AppKit
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// 应用内 Broker 登录(device-authorization + PKCE,对齐桌面 desktop/platform/oauth.go)。iOS 无法
|
// 应用内 Broker 登录(device-authorization + PKCE,对齐桌面 desktop/platform/oauth.go)。iOS 无法
|
||||||
// 跑 loopback 监听,故用 ASWebAuthenticationSession + 自定义 scheme 回调(cdrop://auth-callback):
|
// 跑 loopback 监听,故用 ASWebAuthenticationSession + 自定义 scheme 回调
|
||||||
// 1. GET {broker}/device/authorize?app=cdrop&redirect_uri=cdrop://auth-callback&state&code_challenge&S256
|
// (commilitia-drop://auth-callback):
|
||||||
// 2. 用户在 broker SSO 授权 → 回调 cdrop://auth-callback?code=..&state=..
|
// 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 机器令牌
|
// 3. POST {broker}/device/token {code, code_verifier, redirect_uri} → bootstrap 机器令牌
|
||||||
// 4. POST {api}/api/auth/device-session(Bearer bootstrap)→ cdrop 设备会话(带真名 / device_id)
|
// 4. POST {api}/api/auth/device-session(Bearer bootstrap)→ cdrop 设备会话(带真名 / device_id)
|
||||||
// 这样手机上直接输账号登录,无需另一台设备扫码批准。
|
// 这样手机上直接输账号登录,无需另一台设备扫码批准。
|
||||||
// 前置:broker 须为 cdrop 应用注册 redirect_uri cdrop://auth-callback(同桌面 loopback 须注册)。
|
// 前置:broker 须为 Commilitia Drop 应用注册 redirect_uri
|
||||||
|
// commilitia-drop://auth-callback(同桌面 loopback 须注册)。
|
||||||
|
|
||||||
enum BrokerLoginError: Error
|
enum BrokerLoginError: Error
|
||||||
{
|
{
|
||||||
@@ -35,7 +37,8 @@ final class BrokerAuthFlow: NSObject, ASWebAuthenticationPresentationContextProv
|
|||||||
{
|
{
|
||||||
private var session: ASWebAuthenticationSession?
|
private var session: ASWebAuthenticationSession?
|
||||||
|
|
||||||
// 打开授权页,等回调 URL(cdrop://auth-callback?code=..&state=..)。用户取消 / 失败抛错。
|
// 打开授权页,等回调 URL(commilitia-drop://auth-callback?code=..&state=..)。
|
||||||
|
// 用户取消 / 失败抛错。
|
||||||
func run(url: URL, callbackScheme: String) async throws -> URL
|
func run(url: URL, callbackScheme: String) async throws -> URL
|
||||||
{
|
{
|
||||||
try await withCheckedThrowingContinuation
|
try await withCheckedThrowingContinuation
|
||||||
|
|||||||
@@ -72,11 +72,11 @@ struct AppRoot: View
|
|||||||
{ _, uid in
|
{ _, uid in
|
||||||
if let uid { engine.activateRecords(userId: uid) }
|
if let uid { engine.activateRecords(userId: uid) }
|
||||||
}
|
}
|
||||||
// Share Extension 深链:cdrop://share 唤起 → 导入收件箱待发文件(RootView 据此弹选设备)。
|
// Share Extension 深链:commilitia-drop://share 唤起 → 导入收件箱待发文件。
|
||||||
// 放在 AppRoot 以便登录 / 未登录任一界面都接得到;未登录时文件留收件箱,登录后 onAppear 补扫。
|
// 放在 AppRoot 以便登录 / 未登录任一界面都接得到;未登录时文件留收件箱,登录后 onAppear 补扫。
|
||||||
.onOpenURL
|
.onOpenURL
|
||||||
{ url in
|
{ url in
|
||||||
guard url.scheme == "cdrop", url.host == "share" else { return }
|
guard url.scheme == "commilitia-drop", url.host == "share" else { return }
|
||||||
engine.loadPendingShares()
|
engine.loadPendingShares()
|
||||||
}
|
}
|
||||||
// 被其他设备登出的推送到达(AppDelegate 发):前台即时清会话 + 复位引擎 → 回登录页。
|
// 被其他设备登出的推送到达(AppDelegate 发):前台即时清会话 + 复位引擎 → 回登录页。
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ final class EngineController: NSObject
|
|||||||
private var webView: WKWebView?
|
private var webView: WKWebView?
|
||||||
private let downloads = DownloadManager()
|
private let downloads = DownloadManager()
|
||||||
|
|
||||||
// 待发文件暂存:id → 安全作用域 URL(文件选择器选中的文件)。引擎经 cdrop-file://<id>
|
// 待发文件暂存:id → 安全作用域 URL。引擎经 commilitia-drop-file://<id>
|
||||||
// 回取字节(下方 WKURLSchemeHandler 供给)。
|
// 回取字节(下方 WKURLSchemeHandler 供给)。
|
||||||
private var outgoing: [String: URL] = [:]
|
private var outgoing: [String: URL] = [:]
|
||||||
|
|
||||||
@@ -271,7 +271,7 @@ final class EngineController: NSObject
|
|||||||
}
|
}
|
||||||
|
|
||||||
// makeWebView:构建离屏 WebView——注入 __CDROP_BOOT__(device_type:"ios")、注册消息
|
// makeWebView:构建离屏 WebView——注入 __CDROP_BOOT__(device_type:"ios")、注册消息
|
||||||
// 处理器、注册 cdrop-file 自定义 scheme(发送侧给 JS 喂文件字节)。幂等。
|
// 处理器、注册 commilitia-drop-file 自定义 scheme(发送侧给 JS 喂文件字节)。幂等。
|
||||||
func makeWebView() -> WKWebView
|
func makeWebView() -> WKWebView
|
||||||
{
|
{
|
||||||
if let existing = webView { return existing }
|
if let existing = webView { return existing }
|
||||||
@@ -297,7 +297,7 @@ final class EngineController: NSObject
|
|||||||
|
|
||||||
let cfg = WKWebViewConfiguration()
|
let cfg = WKWebViewConfiguration()
|
||||||
cfg.userContentController = ucc
|
cfg.userContentController = ucc
|
||||||
cfg.setURLSchemeHandler(self, forURLScheme: "cdrop-file")
|
cfg.setURLSchemeHandler(self, forURLScheme: "commilitia-drop-file")
|
||||||
|
|
||||||
// 迁移到 Auth Broker 后引擎自刷走注入的 refresh_token(POST /api/auth/refresh 代理
|
// 迁移到 Auth Broker 后引擎自刷走注入的 refresh_token(POST /api/auth/refresh 代理
|
||||||
// broker),不再依赖 cdrop_session cookie,故无需把 URLSession 的 cookie 同步进 WebView。
|
// broker),不再依赖 cdrop_session cookie,故无需把 URLSession 的 cookie 同步进 WebView。
|
||||||
@@ -672,7 +672,7 @@ final class EngineController: NSObject
|
|||||||
webView?.evaluateJavaScript("window.__cdropEngineEvent(\(jsString(name)), \(json));")
|
webView?.evaluateJavaScript("window.__cdropEngineEvent(\(jsString(name)), \(json));")
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendFile:暂存选中文件、把 cdrop-file://<id> 引用 + 目标设备交给引擎发起传输。
|
// sendFile:暂存选中文件、把 commilitia-drop-file://<id> 引用 + 目标设备交给引擎发起传输。
|
||||||
func sendFile(to target: String, fileURL: URL)
|
func sendFile(to target: String, fileURL: URL)
|
||||||
{
|
{
|
||||||
let ref = stageOutgoingFile(fileURL)
|
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] = []
|
var pendingShareFiles: [URL] = []
|
||||||
|
|
||||||
@@ -730,7 +730,7 @@ final class EngineController: NSObject
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 图库选择(#14):PhotosPicker 给的是 PhotosPickerItem,须先 loadTransferable 取字节,再落
|
// 图库选择(#14):PhotosPicker 给的是 PhotosPickerItem,须先 loadTransferable 取字节,再落
|
||||||
// 一个临时文件,才能复用既有 cdrop-file scheme 发送链路(按 Range 读盘、整文件不入内存)。
|
// 一个临时文件,才能复用既有 commilitia-drop-file scheme 发送链路。
|
||||||
// 返回临时文件 URL(在 temporaryDirectory,系统回收);写盘失败回 nil。
|
// 返回临时文件 URL(在 temporaryDirectory,系统回收);写盘失败回 nil。
|
||||||
func stagePhotoData(_ data: Data, suggestedName: String) -> URL?
|
func stagePhotoData(_ data: Data, suggestedName: String) -> URL?
|
||||||
{
|
{
|
||||||
@@ -743,18 +743,18 @@ final class EngineController: NSObject
|
|||||||
catch { return nil }
|
catch { return nil }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 暂存待发文件,返回引擎用的 cdrop-file://<id> 引用。文件选择器给的 URL 是安全作用域
|
// 暂存待发文件,返回引擎用的 commilitia-drop-file://<id> 引用。
|
||||||
// 资源,需 start...Access 才能读。
|
// 资源,需 start...Access 才能读。
|
||||||
private func stageOutgoingFile(_ url: URL) -> String
|
private func stageOutgoingFile(_ url: URL) -> String
|
||||||
{
|
{
|
||||||
let id = UUID().uuidString
|
let id = UUID().uuidString
|
||||||
_ = url.startAccessingSecurityScopedResource()
|
_ = url.startAccessingSecurityScopedResource()
|
||||||
outgoing[id] = url
|
outgoing[id] = url
|
||||||
return "cdrop-file://\(id)"
|
return "commilitia-drop-file://\(id)"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送侧按 [start,end) seek 读暂存的待发文件、回 base64(供 bridgeFileSource 经桥取片)。
|
// 发送侧按 [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
|
fileprivate func readOutgoingSlice(url: String, start: Int, end: Int) throws -> String
|
||||||
{
|
{
|
||||||
guard let id = URL(string: url)?.host, let fileURL = outgoing[id]
|
guard let id = URL(string: url)?.host, let fileURL = outgoing[id]
|
||||||
@@ -787,7 +787,7 @@ final class EngineController: NSObject
|
|||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
// 把 cdrop-file://<id> 引用解析回暂存文件的沙盒绝对路径(Go 据此直接读盘发送)。安全作用域
|
// 把 commilitia-drop-file://<id> 引用解析回暂存文件的沙盒绝对路径。
|
||||||
// 访问已在 stageOutgoingFile 开启、持续到 reset,故同进程的 Go 可直接 os.Open 该路径。无对应
|
// 访问已在 stageOutgoingFile 开启、持续到 reset,故同进程的 Go 可直接 os.Open 该路径。无对应
|
||||||
// 暂存项返回空串。
|
// 暂存项返回空串。
|
||||||
private func resolveOutgoingPath(_ ref: String) -> String
|
private func resolveOutgoingPath(_ ref: String) -> String
|
||||||
@@ -871,7 +871,7 @@ extension EngineController: WKScriptMessageHandler
|
|||||||
end: Self.intOf(payload["end"]))
|
end: Self.intOf(payload["end"]))
|
||||||
resolve(id: id, ok: true, value: b64)
|
resolve(id: id, ok: true, value: b64)
|
||||||
case "p2pStartOutgoing":
|
case "p2pStartOutgoing":
|
||||||
// 发起原生发送:filePath 为 cdrop-file://<id>,解析回沙盒绝对路径交引擎直接读盘。引擎
|
// 发起原生发送:filePath 为 commilitia-drop-file://<id>,解析回沙盒绝对路径。
|
||||||
// 调用立即返回(libwebrtc 异步),completion 在 offer 已铸+发出后回,经 resolveOnMain 回交。
|
// 调用立即返回(libwebrtc 异步),completion 在 offer 已铸+发出后回,经 resolveOnMain 回交。
|
||||||
let engine = nativeTransferEngine()
|
let engine = nativeTransferEngine()
|
||||||
let path = resolveOutgoingPath(payload["filePath"] as? String ?? "")
|
let path = resolveOutgoingPath(payload["filePath"] as? String ?? "")
|
||||||
@@ -1133,11 +1133,11 @@ extension EngineController: WKScriptMessageHandler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 发送侧自定义 scheme(cdrop-file)
|
// MARK: - 发送侧自定义 scheme(commilitia-drop-file)
|
||||||
|
|
||||||
extension EngineController: WKURLSchemeHandler
|
extension EngineController: WKURLSchemeHandler
|
||||||
{
|
{
|
||||||
// 发送侧:原生把暂存的待发文件经 cdrop-file://<id> 喂给引擎(JS fetch 它得到字节,
|
// 发送侧:原生把暂存文件经 commilitia-drop-file://<id> 喂给引擎。
|
||||||
// 见 PLAN §2 / R-iOS-4)。引擎(rangeSource)按块带 `Range` 头拉取——这里 seek 文件
|
// 见 PLAN §2 / R-iOS-4)。引擎(rangeSource)按块带 `Range` 头拉取——这里 seek 文件
|
||||||
// 仅读该区间、回 206 Partial Content,整文件永不入内存,避大文件 jetsam。无 Range
|
// 仅读该区间、回 206 Partial Content,整文件永不入内存,避大文件 jetsam。无 Range
|
||||||
// 头时回退整文件(防御路径;引擎正常总带 Range)。
|
// 头时回退整文件(防御路径;引擎正常总带 Range)。
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import WebRTC
|
|||||||
// 集成(WebKit/libwebrtc 才集成),真机确证架构性不可靠(见 desktop/NATIVE-TRANSFER.md §7)。
|
// 集成(WebKit/libwebrtc 才集成),真机确证架构性不可靠(见 desktop/NATIVE-TRANSFER.md §7)。
|
||||||
//
|
//
|
||||||
// 线协议与 desktop/engine(pion)及 web/src/features/transfer/p2p.ts 逐字节一致,故可与桌面 pion
|
// 线协议与 desktop/engine(pion)及 web/src/features/transfer/p2p.ts 逐字节一致,故可与桌面 pion
|
||||||
// 引擎、浏览器 / 旧 iOS 的 JS 引擎互通:DataChannel "cdrop-file"(ordered),控制帧 meta/done/ack 走
|
// 引擎、浏览器 / 旧 iOS 的 JS 引擎互通:DataChannel "commilitia-drop-file"(ordered),控制帧
|
||||||
// 文本帧、文件分片走二进制帧(64KB),信令 payload 形如 {type, sdp?, candidate?}。本类是 session.go +
|
// 文本帧、文件分片走二进制帧(64KB),信令 payload 形如 {type, sdp?, candidate?}。本类是 session.go +
|
||||||
// engine.go 的忠实 Swift 端口——逻辑、水位、ack 语义、收尾顺序一一对应。
|
// engine.go 的忠实 Swift 端口——逻辑、水位、ack 语义、收尾顺序一一对应。
|
||||||
//
|
//
|
||||||
@@ -39,7 +39,7 @@ protocol P2PEngineDelegate: AnyObject
|
|||||||
|
|
||||||
private enum Wire
|
private enum Wire
|
||||||
{
|
{
|
||||||
static let channelName = "cdrop-file"
|
static let channelName = "commilitia-drop-file"
|
||||||
static let chunkSize = 64 * 1024
|
static let chunkSize = 64 * 1024
|
||||||
static let highWatermark: UInt64 = 16 * 1024 * 1024
|
static let highWatermark: UInt64 = 16 * 1024 * 1024
|
||||||
static let lowWatermark: UInt64 = 4 * 1024 * 1024
|
static let lowWatermark: UInt64 = 4 * 1024 * 1024
|
||||||
@@ -94,7 +94,7 @@ final class LibWebRtcEngine
|
|||||||
}
|
}
|
||||||
|
|
||||||
// StartOutgoing:建 PeerConnection + DataChannel,发 offer,通道就绪后流式发出 filePath。
|
// StartOutgoing:建 PeerConnection + DataChannel,发 offer,通道就绪后流式发出 filePath。
|
||||||
// filePath 为本机绝对路径(原生从 cdrop-file://<id> 解析得到)。completion 在 offer 已铸并
|
// filePath 为本机绝对路径(原生从 commilitia-drop-file://<id> 解析得到)。completion 在 offer 已铸并
|
||||||
// setLocalDescription 完成后回(或出错时回错误),对齐 JS 侧信令链 seed 语义(见 p2pIos.ts chain)。
|
// setLocalDescription 完成后回(或出错时回错误),对齐 JS 侧信令链 seed 语义(见 p2pIos.ts chain)。
|
||||||
func startOutgoing(sessionId: String, peerName: String, filePath: String,
|
func startOutgoing(sessionId: String, peerName: String, filePath: String,
|
||||||
iceServersJSON: String, completion: @escaping (Error?) -> Void)
|
iceServersJSON: String, completion: @escaping (Error?) -> Void)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<string>en</string>
|
<string>en</string>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>$(PRODUCT_NAME)</string>
|
<string>Commilitia Drop</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
<string>net.commilitia.Commilitia-Drop</string>
|
<string>net.commilitia.Commilitia-Drop</string>
|
||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>cdrop</string>
|
<string>commilitia-drop</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
</dict>
|
</dict>
|
||||||
<key>NSBonjourServices</key>
|
<key>NSBonjourServices</key>
|
||||||
<array>
|
<array>
|
||||||
<string>_cdrop._tcp</string>
|
<string>_commilitia-drop._tcp</string>
|
||||||
</array>
|
</array>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>Commilitia Drop 使用相机扫描二维码登录新设备。</string>
|
<string>Commilitia Drop 使用相机扫描二维码登录新设备。</string>
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import Network
|
|||||||
|
|
||||||
// 触发本地网络权限弹窗(#5)。iOS 14+ 默认拒绝 App 访问本地网络;未授权时 WKWebView 内的
|
// 触发本地网络权限弹窗(#5)。iOS 14+ 默认拒绝 App 访问本地网络;未授权时 WKWebView 内的
|
||||||
// WebRTC 收集不到 host 候选 → 同内网也退回 srflx / relay(慢、非直连)。WKWebView 自身不会
|
// WebRTC 收集不到 host 候选 → 同内网也退回 srflx / relay(慢、非直连)。WKWebView 自身不会
|
||||||
// 触发该权限请求,须由宿主 App 主动发起一次本地网络访问。这里启动一个对 _cdrop._tcp 的
|
// 触发该权限请求,须由宿主 App 主动发起一次本地网络访问。这里启动一个对
|
||||||
|
// _commilitia-drop._tcp 的
|
||||||
// NWBrowser 浏览即触发系统弹窗;用户允许后,后续 RTCPeerConnection 才能收集本地 host / mDNS
|
// NWBrowser 浏览即触发系统弹窗;用户允许后,后续 RTCPeerConnection 才能收集本地 host / mDNS
|
||||||
// 候选实现直连。仅需触发一次;保持 browser 存活以维持授权态。project.yml 已声明
|
// 候选实现直连。仅需触发一次;保持 browser 存活以维持授权态。project.yml 已声明
|
||||||
// NSLocalNetworkUsageDescription + NSBonjourServices(_cdrop._tcp)。
|
// NSLocalNetworkUsageDescription + NSBonjourServices(_commilitia-drop._tcp)。
|
||||||
enum LocalNetworkPermission
|
enum LocalNetworkPermission
|
||||||
{
|
{
|
||||||
private static var browser: NWBrowser?
|
private static var browser: NWBrowser?
|
||||||
@@ -18,7 +19,7 @@ enum LocalNetworkPermission
|
|||||||
guard browser == nil else { return }
|
guard browser == nil else { return }
|
||||||
let params = NWParameters()
|
let params = NWParameters()
|
||||||
params.includePeerToPeer = true
|
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 }
|
b.browseResultsChangedHandler = { _, _ in }
|
||||||
browser = b
|
browser = b
|
||||||
b.start(queue: .main)
|
b.start(queue: .main)
|
||||||
|
|||||||
@@ -97,7 +97,7 @@
|
|||||||
"qr.show.nameHint": "Shown to your other devices. ASCII characters only.",
|
"qr.show.nameHint": "Shown to your other devices. ASCII characters only.",
|
||||||
"qr.show.generate": "Generate code",
|
"qr.show.generate": "Generate code",
|
||||||
"qr.show.scanTitle": "Scan to approve",
|
"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.howto2": "Scan the code above and approve this device.",
|
||||||
"qr.show.waiting": "Waiting for approval…",
|
"qr.show.waiting": "Waiting for approval…",
|
||||||
"qr.show.approved": "Approved — signing you in…",
|
"qr.show.approved": "Approved — signing you in…",
|
||||||
@@ -189,11 +189,11 @@
|
|||||||
"notify.transfer.failedTitle": "Transfer failed",
|
"notify.transfer.failedTitle": "Transfer failed",
|
||||||
"settings.account.title": "Account",
|
"settings.account.title": "Account",
|
||||||
"settings.account.signOut": "Sign out",
|
"settings.account.signOut": "Sign out",
|
||||||
"settings.desktop.title": "Desktop",
|
"settings.desktop.title": "App settings",
|
||||||
"settings.desktop.clipboardSync": "Auto-sync clipboard",
|
"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.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.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.downloadDir": "Download folder",
|
||||||
"settings.desktop.downloadDirHint": "Received files are saved here. Leave unset to use the system Downloads folder.",
|
"settings.desktop.downloadDirHint": "Received files are saved here. Leave unset to use the system Downloads folder.",
|
||||||
"settings.desktop.downloadDirChoose": "Change…",
|
"settings.desktop.downloadDirChoose": "Change…",
|
||||||
@@ -292,7 +292,7 @@
|
|||||||
"errors.messageEmpty": "Message is empty",
|
"errors.messageEmpty": "Message is empty",
|
||||||
"errors.messageOverflow": "Message exceeds 4 KB; send a file instead",
|
"errors.messageOverflow": "Message exceeds 4 KB; send a file instead",
|
||||||
"errors.noReceiver": "No receiver selected",
|
"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.noAccessToken": "No access token; user must log in",
|
||||||
"errors.clipboardUnavailable": "Browser clipboard API unavailable (requires HTTPS + permission)",
|
"errors.clipboardUnavailable": "Browser clipboard API unavailable (requires HTTPS + permission)",
|
||||||
"errors.clipboardOverflow": "Content too large; exceeds {{max}} byte limit",
|
"errors.clipboardOverflow": "Content too large; exceeds {{max}} byte limit",
|
||||||
|
|||||||
@@ -97,7 +97,7 @@
|
|||||||
"qr.show.nameHint": "此名称会展示给你的其他设备,仅可使用 ASCII 字符。",
|
"qr.show.nameHint": "此名称会展示给你的其他设备,仅可使用 ASCII 字符。",
|
||||||
"qr.show.generate": "生成二维码",
|
"qr.show.generate": "生成二维码",
|
||||||
"qr.show.scanTitle": "用手机扫码批准",
|
"qr.show.scanTitle": "用手机扫码批准",
|
||||||
"qr.show.howto1": "在另一台已登录的设备上打开相机或 cdrop。",
|
"qr.show.howto1": "在另一台已登录的设备上打开相机或 Commilitia Drop。",
|
||||||
"qr.show.howto2": "扫描上方二维码,按提示批准此设备。",
|
"qr.show.howto2": "扫描上方二维码,按提示批准此设备。",
|
||||||
"qr.show.waiting": "等待批准…",
|
"qr.show.waiting": "等待批准…",
|
||||||
"qr.show.approved": "已批准,正在进入…",
|
"qr.show.approved": "已批准,正在进入…",
|
||||||
@@ -189,11 +189,11 @@
|
|||||||
"notify.transfer.failedTitle": "传输失败",
|
"notify.transfer.failedTitle": "传输失败",
|
||||||
"settings.account.title": "账号",
|
"settings.account.title": "账号",
|
||||||
"settings.account.signOut": "退出登录",
|
"settings.account.signOut": "退出登录",
|
||||||
"settings.desktop.title": "桌面",
|
"settings.desktop.title": "应用设置",
|
||||||
"settings.desktop.clipboardSync": "剪贴板自动同步",
|
"settings.desktop.clipboardSync": "剪贴板自动同步",
|
||||||
"settings.desktop.clipboardSyncHint": "复制即上传到云端,并接收其他设备的剪贴板更新。",
|
"settings.desktop.clipboardSyncHint": "复制即上传到云端,并接收其他设备的剪贴板更新。",
|
||||||
"settings.desktop.launchAtLogin": "开机自启",
|
"settings.desktop.launchAtLogin": "开机自启",
|
||||||
"settings.desktop.launchAtLoginHint": "登录系统后自动在后台启动 cdrop(菜单栏常驻)。",
|
"settings.desktop.launchAtLoginHint": "登录系统后自动在后台启动 Commilitia Drop(常驻菜单栏或系统托盘)。",
|
||||||
"settings.desktop.downloadDir": "下载目录",
|
"settings.desktop.downloadDir": "下载目录",
|
||||||
"settings.desktop.downloadDirHint": "接收到的文件保存到此目录;留空则使用系统下载目录。",
|
"settings.desktop.downloadDirHint": "接收到的文件保存到此目录;留空则使用系统下载目录。",
|
||||||
"settings.desktop.downloadDirChoose": "更改…",
|
"settings.desktop.downloadDirChoose": "更改…",
|
||||||
@@ -292,7 +292,7 @@
|
|||||||
"errors.messageEmpty": "消息为空",
|
"errors.messageEmpty": "消息为空",
|
||||||
"errors.messageOverflow": "消息超过4 KB;请改用文件传输",
|
"errors.messageOverflow": "消息超过4 KB;请改用文件传输",
|
||||||
"errors.noReceiver": "未选择接收方",
|
"errors.noReceiver": "未选择接收方",
|
||||||
"errors.devTokenMissing": "Dev模式:未在.env.local设置VITE_CDROP_DEV_TOKEN",
|
"errors.devTokenMissing": "Dev 模式:未在 .env.local 配置开发令牌",
|
||||||
"errors.noAccessToken": "无访问令牌:用户必须先登录",
|
"errors.noAccessToken": "无访问令牌:用户必须先登录",
|
||||||
"errors.clipboardUnavailable": "浏览器不支持剪贴板API(需HTTPS与权限)",
|
"errors.clipboardUnavailable": "浏览器不支持剪贴板API(需HTTPS与权限)",
|
||||||
"errors.clipboardOverflow": "内容过大,超过 {{max}} 字节上限",
|
"errors.clipboardOverflow": "内容过大,超过 {{max}} 字节上限",
|
||||||
|
|||||||
@@ -97,7 +97,7 @@
|
|||||||
"qr.show.nameHint": "此名稱會顯示給你的其他裝置,僅可使用 ASCII 字元。",
|
"qr.show.nameHint": "此名稱會顯示給你的其他裝置,僅可使用 ASCII 字元。",
|
||||||
"qr.show.generate": "產生 QR 碼",
|
"qr.show.generate": "產生 QR 碼",
|
||||||
"qr.show.scanTitle": "用手機掃碼批准",
|
"qr.show.scanTitle": "用手機掃碼批准",
|
||||||
"qr.show.howto1": "在另一部已登入的裝置上開啟相機或 cdrop。",
|
"qr.show.howto1": "在另一部已登入的裝置上開啟相機或 Commilitia Drop。",
|
||||||
"qr.show.howto2": "掃描上方 QR 碼,依提示批准此裝置。",
|
"qr.show.howto2": "掃描上方 QR 碼,依提示批准此裝置。",
|
||||||
"qr.show.waiting": "等待批准…",
|
"qr.show.waiting": "等待批准…",
|
||||||
"qr.show.approved": "已批准,正在進入…",
|
"qr.show.approved": "已批准,正在進入…",
|
||||||
@@ -189,11 +189,11 @@
|
|||||||
"notify.transfer.failedTitle": "傳輸失敗",
|
"notify.transfer.failedTitle": "傳輸失敗",
|
||||||
"settings.account.title": "帳號",
|
"settings.account.title": "帳號",
|
||||||
"settings.account.signOut": "登出",
|
"settings.account.signOut": "登出",
|
||||||
"settings.desktop.title": "桌面",
|
"settings.desktop.title": "應用程式設定",
|
||||||
"settings.desktop.clipboardSync": "剪貼簿自動同步",
|
"settings.desktop.clipboardSync": "剪貼簿自動同步",
|
||||||
"settings.desktop.clipboardSyncHint": "複製後即上傳雲端,並接收其他裝置的剪貼簿更新。",
|
"settings.desktop.clipboardSyncHint": "複製後即上傳雲端,並接收其他裝置的剪貼簿更新。",
|
||||||
"settings.desktop.launchAtLogin": "開機自動啟動",
|
"settings.desktop.launchAtLogin": "開機自動啟動",
|
||||||
"settings.desktop.launchAtLoginHint": "登入系統後自動在背景啟動 cdrop(常駐選單列)。",
|
"settings.desktop.launchAtLoginHint": "登入系統後自動在背景啟動 Commilitia Drop(常駐選單列或系統匣)。",
|
||||||
"settings.desktop.downloadDir": "下載資料夾",
|
"settings.desktop.downloadDir": "下載資料夾",
|
||||||
"settings.desktop.downloadDirHint": "接收到的檔案會儲存至此資料夾;留空則使用系統下載資料夾。",
|
"settings.desktop.downloadDirHint": "接收到的檔案會儲存至此資料夾;留空則使用系統下載資料夾。",
|
||||||
"settings.desktop.downloadDirChoose": "變更…",
|
"settings.desktop.downloadDirChoose": "變更…",
|
||||||
@@ -292,7 +292,7 @@
|
|||||||
"errors.messageEmpty": "訊息為空",
|
"errors.messageEmpty": "訊息為空",
|
||||||
"errors.messageOverflow": "訊息超過4 KB;請改用檔案傳輸",
|
"errors.messageOverflow": "訊息超過4 KB;請改用檔案傳輸",
|
||||||
"errors.noReceiver": "未選擇接收方",
|
"errors.noReceiver": "未選擇接收方",
|
||||||
"errors.devTokenMissing": "Dev模式:未在.env.local設定VITE_CDROP_DEV_TOKEN",
|
"errors.devTokenMissing": "Dev 模式:未在 .env.local 設定開發權杖",
|
||||||
"errors.noAccessToken": "無存取權杖:使用者必須先登入",
|
"errors.noAccessToken": "無存取權杖:使用者必須先登入",
|
||||||
"errors.clipboardUnavailable": "瀏覽器不支援剪貼簿API(需HTTPS與權限)",
|
"errors.clipboardUnavailable": "瀏覽器不支援剪貼簿API(需HTTPS與權限)",
|
||||||
"errors.clipboardOverflow": "內容過大,超過 {{max}} 位元組上限",
|
"errors.clipboardOverflow": "內容過大,超過 {{max}} 位元組上限",
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ struct RootView: View
|
|||||||
.opacity(0)
|
.opacity(0)
|
||||||
.allowsHitTesting(false)
|
.allowsHitTesting(false)
|
||||||
}
|
}
|
||||||
// Share Extension 交接:cdrop://share 唤起 / 前台扫到收件箱待发文件 → 直入发送预览(与手动
|
// Share Extension 交接:commilitia-drop://share 唤起 / 前台扫到收件箱待发文件。
|
||||||
// 选文件同一表单 SendComposeSheet),预填分享的文件,选目标设备发送。onSend 走 sendShares
|
// 选文件同一表单 SendComposeSheet),预填分享的文件,选目标设备发送。onSend 走 sendShares
|
||||||
// (把收件箱文件 move 到私有暂存再发),完成后 pendingShareFiles 清空 → 表单自动收起。
|
// (把收件箱文件 move 到私有暂存再发),完成后 pendingShareFiles 清空 → 表单自动收起。
|
||||||
.onAppear { engine.loadPendingShares() }
|
.onAppear { engine.loadPendingShares() }
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
<string>net.commilitia.Commilitia-Drop</string>
|
<string>net.commilitia.Commilitia-Drop</string>
|
||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>cdrop</string>
|
<string>commilitia-drop</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<string>26.0</string>
|
<string>26.0</string>
|
||||||
<key>NSBonjourServices</key>
|
<key>NSBonjourServices</key>
|
||||||
<array>
|
<array>
|
||||||
<string>_cdrop._tcp</string>
|
<string>_commilitia-drop._tcp</string>
|
||||||
</array>
|
</array>
|
||||||
<key>NSLocalNetworkUsageDescription</key>
|
<key>NSLocalNetworkUsageDescription</key>
|
||||||
<string>Commilitia Drop 需要访问本地网络以发现同内网设备并建立直连传输。</string>
|
<string>Commilitia Drop 需要访问本地网络以发现同内网设备并建立直连传输。</string>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>$(PRODUCT_NAME)</string>
|
<string>Commilitia Drop</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>XPC!</string>
|
<string>XPC!</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
|
|||||||
@@ -45,14 +45,15 @@ targets:
|
|||||||
path: Sources/Info.plist
|
path: Sources/Info.plist
|
||||||
properties:
|
properties:
|
||||||
CFBundleDisplayName: Commilitia Drop
|
CFBundleDisplayName: Commilitia Drop
|
||||||
|
CFBundleName: Commilitia Drop
|
||||||
# 品牌字标字体 Fredoka SemiBold(Shared/Fonts/,随 bundle 打包),用于 Wordmark(与 web 一致)。
|
# 品牌字标字体 Fredoka SemiBold(Shared/Fonts/,随 bundle 打包),用于 Wordmark(与 web 一致)。
|
||||||
UIAppFonts:
|
UIAppFonts:
|
||||||
- Fredoka-SemiBold.ttf
|
- Fredoka-SemiBold.ttf
|
||||||
# cdrop:// URL scheme:Share Extension 交接后经 cdrop://share 深链唤起主 app 选设备发送。
|
# commilitia-drop:// URL scheme:Share Extension 交接后唤起主 app 选设备发送。
|
||||||
CFBundleURLTypes:
|
CFBundleURLTypes:
|
||||||
- CFBundleURLName: net.commilitia.Commilitia-Drop
|
- CFBundleURLName: net.commilitia.Commilitia-Drop
|
||||||
CFBundleURLSchemes:
|
CFBundleURLSchemes:
|
||||||
- cdrop
|
- commilitia-drop
|
||||||
# 声明支持的语言,使系统控件(EditButton 的「编辑」、滑动删除的「删除」等系统字符
|
# 声明支持的语言,使系统控件(EditButton 的「编辑」、滑动删除的「删除」等系统字符
|
||||||
# 串)跟随设备语言本地化,而非永远英文。开发区域设简体。
|
# 串)跟随设备语言本地化,而非永远英文。开发区域设简体。
|
||||||
CFBundleDevelopmentRegion: zh-Hans
|
CFBundleDevelopmentRegion: zh-Hans
|
||||||
@@ -88,7 +89,7 @@ targets:
|
|||||||
NSAllowsLocalNetworking: true
|
NSAllowsLocalNetworking: true
|
||||||
NSLocalNetworkUsageDescription: "Commilitia Drop 需要访问本地网络以发现同内网设备并建立直连传输。"
|
NSLocalNetworkUsageDescription: "Commilitia Drop 需要访问本地网络以发现同内网设备并建立直连传输。"
|
||||||
NSBonjourServices:
|
NSBonjourServices:
|
||||||
- "_cdrop._tcp"
|
- "_commilitia-drop._tcp"
|
||||||
NSCameraUsageDescription: "Commilitia Drop 使用相机扫描二维码登录新设备。"
|
NSCameraUsageDescription: "Commilitia Drop 使用相机扫描二维码登录新设备。"
|
||||||
# 图库选择:从相册选取图片 / 视频发送(PhotosPicker,见 RootView #14)。
|
# 图库选择:从相册选取图片 / 视频发送(PhotosPicker,见 RootView #14)。
|
||||||
NSPhotoLibraryUsageDescription: "Commilitia Drop 需要访问照片库,以便从相册选取图片或视频发送给其他设备。"
|
NSPhotoLibraryUsageDescription: "Commilitia Drop 需要访问照片库,以便从相册选取图片或视频发送给其他设备。"
|
||||||
@@ -115,6 +116,7 @@ targets:
|
|||||||
path: Share/Info.plist
|
path: Share/Info.plist
|
||||||
properties:
|
properties:
|
||||||
CFBundleDisplayName: Commilitia Drop
|
CFBundleDisplayName: Commilitia Drop
|
||||||
|
CFBundleName: Commilitia Drop
|
||||||
# 品牌字标字体(分享引导界面的 Wordmark 用),随扩展 bundle 打包 + 注册。
|
# 品牌字标字体(分享引导界面的 Wordmark 用),随扩展 bundle 打包 + 注册。
|
||||||
UIAppFonts:
|
UIAppFonts:
|
||||||
- Fredoka-SemiBold.ttf
|
- Fredoka-SemiBold.ttf
|
||||||
@@ -145,6 +147,7 @@ targets:
|
|||||||
path: Widgets/Info.plist
|
path: Widgets/Info.plist
|
||||||
properties:
|
properties:
|
||||||
CFBundleDisplayName: Commilitia Drop
|
CFBundleDisplayName: Commilitia Drop
|
||||||
|
CFBundleName: Commilitia Drop
|
||||||
NSExtension:
|
NSExtension:
|
||||||
NSExtensionPointIdentifier: com.apple.widgetkit-extension
|
NSExtensionPointIdentifier: com.apple.widgetkit-extension
|
||||||
settings:
|
settings:
|
||||||
@@ -212,15 +215,15 @@ targets:
|
|||||||
- zh-Hans
|
- zh-Hans
|
||||||
- zh-Hant
|
- zh-Hant
|
||||||
- en
|
- en
|
||||||
# ASWebAuthenticationSession 的 OAuth 回调经 cdrop:// scheme 回到 app(同 iOS)。
|
# ASWebAuthenticationSession 的 OAuth 回调经 commilitia-drop:// scheme 回到 app。
|
||||||
CFBundleURLTypes:
|
CFBundleURLTypes:
|
||||||
- CFBundleURLName: net.commilitia.Commilitia-Drop
|
- CFBundleURLName: net.commilitia.Commilitia-Drop
|
||||||
CFBundleURLSchemes:
|
CFBundleURLSchemes:
|
||||||
- cdrop
|
- commilitia-drop
|
||||||
# 本地网络:libwebrtc 收集 host 候选、同内网直连须本地网络权限(macOS 15+ 亦弹窗)。
|
# 本地网络:libwebrtc 收集 host 候选、同内网直连须本地网络权限(macOS 15+ 亦弹窗)。
|
||||||
NSLocalNetworkUsageDescription: "Commilitia Drop 需要访问本地网络以发现同内网设备并建立直连传输。"
|
NSLocalNetworkUsageDescription: "Commilitia Drop 需要访问本地网络以发现同内网设备并建立直连传输。"
|
||||||
NSBonjourServices:
|
NSBonjourServices:
|
||||||
- "_cdrop._tcp"
|
- "_commilitia-drop._tcp"
|
||||||
settings:
|
settings:
|
||||||
base:
|
base:
|
||||||
PRODUCT_BUNDLE_IDENTIFIER: net.commilitia.Commilitia-Drop
|
PRODUCT_BUNDLE_IDENTIFIER: net.commilitia.Commilitia-Drop
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
# cdrop web ↔ iOS 同步检查单
|
# Commilitia Drop Web ↔ iOS 同步检查单
|
||||||
|
|
||||||
> 机制,不是清单快照。web(React)与 iOS(SwiftUI)视图层**有意分叉**(液态玻璃决定不可能像素一致)。一致性**优先靠强约束同步**(单一真源、机械消费、漂移不可能,见 `ios/PLAN.md` §5);本文件**只装强约束管不住、需人判定的残余**。
|
> 机制,不是清单快照。web(React)与 iOS(SwiftUI)视图层**有意分叉**(液态玻璃决定不可能像素一致)。一致性**优先靠强约束同步**(单一真源、机械消费、漂移不可能,见 `ios/PLAN.md` §5);本文件**只装强约束管不住、需人判定的残余**。
|
||||||
> 关联:`ios/PLAN.md` 决策 D / §5。
|
> 关联:`ios/PLAN.md` 决策 D / §5。
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
| 2026-06-24 | 建档(iOS 未开工) | — | 初始:架构 A 定、引擎 / i18n / 品牌 / API 四强约束确立,检查单建立 |
|
| 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 | 引擎入口 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/ 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 | i18n locales + scripts/emit-i18n.mjs(未提交) | ios/CDrop i18n(未提交) | **强约束②落地**:web locales 加 `ios.*` 键 → `emit-i18n.mjs` 产出 JSON → iOS `t()` 读(loader I18n.swift)。用户可见**去代号 cdrop**(品牌用 `app.brand`=Commilitia Drop),显示名改 Commilitia Drop。**改 locale 后须重跑 `node web/scripts/emit-i18n.mjs`**(已记入脚本头) |
|
||||||
| 2026-06-24 | engine/main.ts 加 presence 推送 + 初始快照(未提交) | RootView / EngineController / AuthManager 真数据绑定(未提交) | **A 任务**:引擎订阅 `store.devices` 推 `presence` 事件 + boot 后补发 presence/transfers 初始快照;原生 `EngineController` 解码进 `@Observable` devices/transfers/history;`RootView` 三屏全替 demo→真数据 + 新增 `TransferDetailView`(TODO #5)。i18n 加 `ios.detail.*` / `ios.settings.account|user|logout` / `ios.transfer.empty|incoming|outgoing` / `ios.send.noDevices` / `ios.devices.empty`,传输态文案复用共享 `transfer.state|phase|mode|bytesRate`。iOS `t()` 加 `{{name}}` 插值变体对齐 web。Keychain 会话持久化 + 设置页登出(闭合 TODO #4)。web tsc/build + xcodebuild iphonesimulator26.5 双过,模拟器三屏空态截图验。**已部署 prod**:`engine.html` → `engine-BB8iav7K.js`(含 presence 推送),真会话下即生效 |
|
| 2026-06-24 | engine/main.ts 加 presence 推送 + 初始快照(未提交) | RootView / EngineController / AuthManager 真数据绑定(未提交) | **A 任务**:引擎订阅 `store.devices` 推 `presence` 事件 + boot 后补发 presence/transfers 初始快照;原生 `EngineController` 解码进 `@Observable` devices/transfers/history;`RootView` 三屏全替 demo→真数据 + 新增 `TransferDetailView`(TODO #5)。i18n 加 `ios.detail.*` / `ios.settings.account|user|logout` / `ios.transfer.empty|incoming|outgoing` / `ios.send.noDevices` / `ios.devices.empty`,传输态文案复用共享 `transfer.state|phase|mode|bytesRate`。iOS `t()` 加 `{{name}}` 插值变体对齐 web。Keychain 会话持久化 + 设置页登出(闭合 TODO #4)。web tsc/build + xcodebuild iphonesimulator26.5 双过,模拟器三屏空态截图验。**已部署 prod**:`engine.html` → `engine-BB8iav7K.js`(含 presence 推送),真会话下即生效 |
|
||||||
| 2026-06-24 | engine/main.ts:transfers 推送节流 ~3Hz + toWire 加 ice 摘要(未提交,**待部署**) | EngineController 手动容忍解析 + ICE 字段;RootView 详情页 ICE 行 + 收到文件视图 + 设备名编辑 + 设备计数;DownloadManager listFiles/delete;新 DeviceNameStore / QuickLookPreview;project.yml 加文件共享键 + 品牌化权限串(未提交) | **首轮真机测试反馈修复**:① 速度数字 ~10Hz 闪 → 节流活跃传输推送到 3Hz(iOS-only,亦降 P2P 接收主线程抖动);② 设备列表空 → 原生改手动容忍解析(严格 JSONDecoder 遇 bool/int 失配整组失败的隐患),加「已知设备」计数诊断;③ 收到文件无处开 / Files 不可见 / 阻塞发送 → `UIFileSharingEnabled`+`LSSupportsOpeningDocumentsInPlace`(Documents 已是落地目录)+ 应用内「收到的文件」视图(QuickLook 预览 / 系统分享 / 转发在线设备 / 删除);④ 设备名不可改 → `DeviceNameStore`(UserDefaults)+ 设置页编辑框(后端令牌烤名、改名下次登录生效);⑤ P2P <100KB/s(relay 1MB/s)→ 详情页暴露选中 ICE 候选对诊断是否 TURN 中继(不盲改共享 p2p.ts 水位)。i18n 加 `ios.files.*`/`ios.settings.deviceName|deviceNameNote|deviceCount`,ICE 行复用 `transfer.debug.*`。web 双过 + xcodebuild 编过 + 模拟器设置 / 传输屏截图验。**已部署 prod**:`engine.html` → `engine-BC6mZ9zP.js`(含节流 + ice 摘要) |
|
| 2026-06-24 | engine/main.ts:transfers 推送节流 ~3Hz + toWire 加 ice 摘要(未提交,**待部署**) | EngineController 手动容忍解析 + ICE 字段;RootView 详情页 ICE 行 + 收到文件视图 + 设备名编辑 + 设备计数;DownloadManager listFiles/delete;新 DeviceNameStore / QuickLookPreview;project.yml 加文件共享键 + 品牌化权限串(未提交) | **首轮真机测试反馈修复**:① 速度数字 ~10Hz 闪 → 节流活跃传输推送到 3Hz(iOS-only,亦降 P2P 接收主线程抖动);② 设备列表空 → 原生改手动容忍解析(严格 JSONDecoder 遇 bool/int 失配整组失败的隐患),加「已知设备」计数诊断;③ 收到文件无处开 / Files 不可见 / 阻塞发送 → `UIFileSharingEnabled`+`LSSupportsOpeningDocumentsInPlace`(Documents 已是落地目录)+ 应用内「收到的文件」视图(QuickLook 预览 / 系统分享 / 转发在线设备 / 删除);④ 设备名不可改 → `DeviceNameStore`(UserDefaults)+ 设置页编辑框(后端令牌烤名、改名下次登录生效);⑤ P2P <100KB/s(relay 1MB/s)→ 详情页暴露选中 ICE 候选对诊断是否 TURN 中继(不盲改共享 p2p.ts 水位)。i18n 加 `ios.files.*`/`ios.settings.deviceName|deviceNameNote|deviceCount`,ICE 行复用 `transfer.debug.*`。web 双过 + xcodebuild 编过 + 模拟器设置 / 传输屏截图验。**已部署 prod**:`engine.html` → `engine-BC6mZ9zP.js`(含节流 + ice 摘要) |
|
||||||
|
|||||||
+4
-4
@@ -1,4 +1,4 @@
|
|||||||
# cdrop iOS 客户端实施计划
|
# Commilitia Drop iOS 客户端实施计划
|
||||||
|
|
||||||
> 原生 SwiftUI 界面(液态玻璃)· 离屏无头 WebView 复用传输引擎 · 控制中心剪贴板 + Share Extension + APNs
|
> 原生 SwiftUI 界面(液态玻璃)· 离屏无头 WebView 复用传输引擎 · 控制中心剪贴板 + Share Extension + APNs
|
||||||
> 状态:**架构已定(2026-06-24 修订)**,账号无关部分即刻可开工;真机 / 推送 / 扩展实测硬等 Apple Developer 账号。
|
> 状态:**架构已定(2026-06-24 修订)**,账号无关部分即刻可开工;真机 / 推送 / 扩展实测硬等 Apple Developer 账号。
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
- **无头 WebView 此后只跑编排**:信令转发 / 会话 / presence / 消息 / 状态机 / relay(`hub.ts` + `transfer.ts`),**不再承载 P2P 数据面**。决策 A 的“原生壳 + 无头引擎”仍成立,只是引擎的职责收窄到编排层。
|
- **无头 WebView 此后只跑编排**:信令转发 / 会话 / presence / 消息 / 状态机 / relay(`hub.ts` + `transfer.ts`),**不再承载 P2P 数据面**。决策 A 的“原生壳 + 无头引擎”仍成立,只是引擎的职责收窄到编排层。
|
||||||
- **缘由(U1→U2 翻案)**:先试 gomobile+pion(与桌面同引擎,U1),真机 POC **证伪**——Go raw BSD socket 不与 iOS Network framework 集成(连接时好时坏 + 中途断流)。按闸退到 **libwebrtc.framework**(U2,stasel/WebRTC M149 经 SPM、pin `exactVersion 149.0.0`)。完整决策依据与实测证据见 `desktop/NATIVE-TRANSFER.md` §7/§8。
|
- **缘由(U1→U2 翻案)**:先试 gomobile+pion(与桌面同引擎,U1),真机 POC **证伪**——Go raw BSD socket 不与 iOS Network framework 集成(连接时好时坏 + 中途断流)。按闸退到 **libwebrtc.framework**(U2,stasel/WebRTC M149 经 SPM、pin `exactVersion 149.0.0`)。完整决策依据与实测证据见 `desktop/NATIVE-TRANSFER.md` §7/§8。
|
||||||
- **收益**:libwebrtc 原生集成 iOS 网络栈 + DcSCTP 默认 ~5MB rwnd——摆脱 WebKit 写死的 256KB 接收窗(R-iOS-3 的吞吐残差)对数据面的钳制,连通性与吞吐一并根治。
|
- **收益**: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` 共享包**。
|
- **桥与 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 门控)。
|
- **验证**:环回端到端(两个引擎同进程交叉连、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 纯原生)。
|
- **引擎 → 原生**:① 单向通知(`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)。
|
- **复用边界**:接收侧字节桥沿用桌面已验证的 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` 多入口。
|
- **已落地(web 侧,账号无关、`tsc`/`vite build` 双过、对现网零影响)**:`net/ios.ts`(桥 + `isIOSShell` + 接收封装 + `notifyNative`)、`engine/main.ts`(无头入口)、`incomingSink.ts`(通用混合槽路由 iOS)、`engine.html` + `vite.config.ts` 多入口。
|
||||||
- **加载源(关键)**:无头引擎**必须从安全上下文加载**——`RTCPeerConnection` 在非安全 origin(如自定义 `cdrop://` scheme,WebKit 视作 non-secure context)下可能被禁。故引擎页走 `https://`:线上 `drop.commilitia.net` 或应用内本地 https 服务的静态 bundle;**自定义 scheme 仅用于 `WKURLSchemeHandler` 给 JS 流式喂文件字节**,不可用它直载引擎页。见 R-iOS-3。
|
- **加载源(关键)**:无头引擎**必须从安全上下文加载**——`RTCPeerConnection` 在非安全 origin(如自定义 `commilitia-drop://` scheme,WebKit 视作 non-secure context)下可能被禁。故引擎页走 `https://`:线上 `drop.commilitia.net` 或应用内本地 https 服务的静态 bundle;**自定义 scheme 仅用于 `WKURLSchemeHandler` 给 JS 流式喂文件字节**,不可用它直载引擎页。见 R-iOS-3。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ Share Extension 落法(据上):**抓文件 → 写 App Group 容器 →
|
|||||||
| **I6** APNs | 服务端通道(§3)+ 原生注册;不含剪贴板 | `.p8` + 真机硬等账号 |
|
| **I6** APNs | 服务端通道(§3)+ 原生注册;不含剪贴板 | `.p8` + 真机硬等账号 |
|
||||||
| **I7** 打磨与分发 | 后台窗口;图标 / 启动屏(复用品牌资产);旁加载分发 | 硬等账号 |
|
| **I7** 打磨与分发 | 后台窗口;图标 / 启动屏(复用品牌资产);旁加载分发 | 硬等账号 |
|
||||||
|
|
||||||
> **实现状态(2026-06-27)**:I1/I2/I3/I4/I5/I6 的**代码**全部落地——发送端流式(R-iOS-4,FileSource + 原生 Range,整文件不进 WebView 内存);后台续传(BGContinuedProcessingTask);APNs(后端 `internal/apns` ES256 + 原生注册经引擎桥);Share Extension(App Group 收件箱 + `cdrop://share` 深链);控制中心两控件(**专用 broker 设备会话**,纯原生 REST,不与引擎抢 refresh 轮换)。模拟器验 + ultracode 多 agent 审查修讫(0 HIGH,修 2 MED + 7 LOW)。**I0 账号 / 证书 + 真机签名 / 真发推送 / 旁加载分发**仍账号门控——手册 `ios/CDrop/REALDEVICE.md`(ASC API Key 自动 provisioning + `just ios-device` 全 CLI 装机,`-allowProvisioningUpdates` 自动登记设备 / 建 profile)。包名 `net.commilitia.Commilitia-Drop`,App Group `group.net.commilitia.Commilitia-Drop`。
|
> **实现状态(2026-06-27)**:I1/I2/I3/I4/I5/I6 的**代码**全部落地——发送端流式(R-iOS-4,FileSource + 原生 Range,整文件不进 WebView 内存);后台续传(BGContinuedProcessingTask);APNs(后端 `internal/apns` ES256 + 原生注册经引擎桥);Share Extension(App Group 收件箱 + `commilitia-drop://share` 深链);控制中心两控件(**专用 broker 设备会话**,纯原生 REST,不与引擎抢 refresh 轮换)。模拟器验 + ultracode 多 agent 审查修讫(0 HIGH,修 2 MED + 7 LOW)。**I0 账号 / 证书 + 真机签名 / 真发推送 / 旁加载分发**仍账号门控——手册 `ios/CDrop/REALDEVICE.md`(ASC API Key 自动 provisioning + `just ios-device` 全 CLI 装机,`-allowProvisioningUpdates` 自动登记设备 / 建 profile)。包名 `net.commilitia.Commilitia-Drop`,App Group `group.net.commilitia.Commilitia-Drop`。
|
||||||
|
|
||||||
> **更新(2026-06-28)**:① **P2P 数据面改原生 libwebrtc**(决策 F;完整依据 `desktop/NATIVE-TRANSFER.md` §7/§8)——已真机装机、用户反馈“传输总体良好”,真网吞吐互通为剩余闸;② **UX 改造**(发送预览直入 / 消息按设备会话 / 删除粒度,§10);③ **品牌与字体系统**(logo + Orbit Gothic / Maple Mono / Fredoka,§11)。
|
> **更新(2026-06-28)**:① **P2P 数据面改原生 libwebrtc**(决策 F;完整依据 `desktop/NATIVE-TRANSFER.md` §7/§8)——已真机装机、用户反馈“传输总体良好”,真网吞吐互通为剩余闸;② **UX 改造**(发送预览直入 / 消息按设备会话 / 删除粒度,§10);③ **品牌与字体系统**(logo + Orbit Gothic / Maple Mono / Fredoka,§11)。
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
传输引擎。仅由原生 iOS 壳的离屏 WKWebView 从安全源(https)加载;原生须在本
|
传输引擎。仅由原生 iOS 壳的离屏 WKWebView 从安全源(https)加载;原生须在本
|
||||||
模块执行前经 WKUserScript 注入 window.__CDROP_BOOT__(session / device_name /
|
模块执行前经 WKUserScript 注入 window.__CDROP_BOOT__(session / device_name /
|
||||||
api_base / device_type:"ios"),见 src/engine/main.ts。 -->
|
api_base / device_type:"ios"),见 src/engine/main.ts。 -->
|
||||||
<title>cdrop engine</title>
|
<title>Commilitia Drop</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script type="module" src="/src/engine/main.ts"></script>
|
<script type="module" src="/src/engine/main.ts"></script>
|
||||||
|
|||||||
+2
-2
@@ -19,8 +19,8 @@
|
|||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="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-status-bar-style" content="default" />
|
||||||
<meta name="apple-mobile-web-app-title" content="cdrop" />
|
<meta name="apple-mobile-web-app-title" content="Commilitia Drop" />
|
||||||
<meta name="application-name" content="cdrop" />
|
<meta name="application-name" content="Commilitia Drop" />
|
||||||
<!-- 预取品牌 logo:登录跳转回来后首页 header 立即用到;早取 + 后端一周缓存,
|
<!-- 预取品牌 logo:登录跳转回来后首页 header 立即用到;早取 + 后端一周缓存,
|
||||||
避免那一刻才发起请求导致的闪烁重载。 -->
|
避免那一刻才发起请求导致的闪烁重载。 -->
|
||||||
<link rel="preload" as="image" href="/logo-mark.png" />
|
<link rel="preload" as="image" href="/logo-mark.png" />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "/",
|
"id": "/",
|
||||||
"name": "Commilitia Drop",
|
"name": "Commilitia Drop",
|
||||||
"short_name": "cdrop",
|
"short_name": "Commilitia Drop",
|
||||||
"description": "跨设备剪贴板同步与点对点文件传输",
|
"description": "跨设备剪贴板同步与点对点文件传输",
|
||||||
"lang": "zh-CN",
|
"lang": "zh-CN",
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
|
|||||||
+1
-1
@@ -72,7 +72,7 @@ self.addEventListener("push", (event) =>
|
|||||||
try { data = event.data ? event.data.json() : {}; }
|
try { data = event.data ? event.data.json() : {}; }
|
||||||
catch (_e) { data = {}; }
|
catch (_e) { data = {}; }
|
||||||
|
|
||||||
const title = data.title || "cdrop";
|
const title = data.title || "Commilitia Drop";
|
||||||
const options = {
|
const options = {
|
||||||
body: data.body || "",
|
body: data.body || "",
|
||||||
tag: data.tag || undefined, // same tag replaces a stale notification in place
|
tag: data.tag || undefined, // same tag replaces a stale notification in place
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ function subscribeStore(): void
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleSendFile:原生把待发文件经 payload.url(cdrop-file://<id>)暴露。R-iOS-4:用
|
// handleSendFile:原生把待发文件经 payload.url(commilitia-drop-file://<id>)暴露。R-iOS-4:用
|
||||||
// bridgeFileSource 经桥按块取片(原生据 [start,end) seek 文件、回 base64),整文件永不整体进
|
// bridgeFileSource 经桥按块取片(原生据 [start,end) seek 文件、回 base64),整文件永不整体进
|
||||||
// WebView 内存——大文件不再有 jetsam 风险,传输层 p2p / relay 透明复用。改走桥而非跨 origin
|
// WebView 内存——大文件不再有 jetsam 风险,传输层 p2p / relay 透明复用。改走桥而非跨 origin
|
||||||
// fetch,绕开自定义 scheme 的 CORS 拦截(否则发送在读文件即失败)。
|
// fetch,绕开自定义 scheme 的 CORS 拦截(否则发送在读文件即失败)。
|
||||||
@@ -520,7 +520,7 @@ function boot(): void
|
|||||||
{
|
{
|
||||||
// 非 iOS 壳(误加载 engine.html)——保持惰性,不触发任何网络 / 引擎行为。
|
// 非 iOS 壳(误加载 engine.html)——保持惰性,不触发任何网络 / 引擎行为。
|
||||||
// eslint-disable-next-line no-console
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import type { FileSource } from "./source";
|
|||||||
// - iceServers: pulled from lib/iceServers (Cloudflare Realtime TURN over
|
// - iceServers: pulled from lib/iceServers (Cloudflare Realtime TURN over
|
||||||
// TLS when CDROP_CF_TURN_* is configured; STUN-only fallback otherwise).
|
// TLS when CDROP_CF_TURN_* is configured; STUN-only fallback otherwise).
|
||||||
// Refreshed on login by RootLayout.
|
// 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
|
// - chunk size 64 KiB, backpressure thresholds 16 MiB / 4 MiB
|
||||||
// - 接收端流式写 OPFS(lib/incomingSink),避免 iOS Safari 内存压力
|
// - 接收端流式写 OPFS(lib/incomingSink),避免 iOS Safari 内存压力
|
||||||
// - control frames as JSON strings (head meta + tail done), payload bytes binary
|
// - 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 预热候选;
|
// Same-LAN Restricted Cone NAT 排查依据:iceCandidatePoolSize=4 预热候选;
|
||||||
// 收集本/远端 host/mdns/srflx/prflx/relay 计数 + 选中候选对类型推到 store,
|
// 收集本/远端 host/mdns/srflx/prflx/relay 计数 + 选中候选对类型推到 store,
|
||||||
// 让 UI 暴露"实际通过哪条路径连上"(host:host = 真直连,relay:* = 走了 TURN)。
|
// 让 UI 暴露"实际通过哪条路径连上"(host:host = 真直连,relay:* = 走了 TURN)。
|
||||||
const CHANNEL_NAME = "cdrop-file";
|
const CHANNEL_NAME = "commilitia-drop-file";
|
||||||
const CHUNK_SIZE = 64 * 1024;
|
const CHUNK_SIZE = 64 * 1024;
|
||||||
const HIGH_WATERMARK = 16 * 1024 * 1024;
|
const HIGH_WATERMARK = 16 * 1024 * 1024;
|
||||||
const LOW_WATERMARK = 4 * 1024 * 1024;
|
const LOW_WATERMARK = 4 * 1024 * 1024;
|
||||||
@@ -971,7 +971,7 @@ export function p2pStartOutgoing(
|
|||||||
{
|
{
|
||||||
return nativeStartOutgoing(sessionId, receiverName, src.path, src.size);
|
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 Session(WebKit WebRTC)。
|
// 绝对路径直接读盘)。IOS_NATIVE 关闭时回退下方 JS Session(WebKit WebRTC)。
|
||||||
if (IOS_NATIVE && isIOSShell() && src.path)
|
if (IOS_NATIVE && isIOSShell() && src.path)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ function register(s: NativeSess): void
|
|||||||
idByPeer.set(s.peerName, s.sessionId);
|
idByPeer.set(s.peerName, s.sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// nativeIosStartOutgoing 经 Go 引擎发送 filePath(cdrop-file://<id> 引用,原生解析回沙盒绝对
|
// nativeIosStartOutgoing 经 Go 引擎发送 filePath(commilitia-drop-file://<id> 引用,原生解析回沙盒绝对
|
||||||
// 路径)到 peerName。total 用于进度事件的分母(store 的 bytesTransferred 仍是 UI 真值源)。
|
// 路径)到 peerName。total 用于进度事件的分母(store 的 bytesTransferred 仍是 UI 真值源)。
|
||||||
export function nativeIosStartOutgoing(
|
export function nativeIosStartOutgoing(
|
||||||
sessionId: string, peerName: string, filePath: string, total: number,
|
sessionId: string, peerName: string, filePath: string, total: number,
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function fileSource(file: File): FileSource
|
|||||||
|
|
||||||
// rangeSource:经 HTTP Range 向原生 WKURLSchemeHandler 惰性拉取文件片(iOS 大文件流式发送)。
|
// rangeSource:经 HTTP Range 向原生 WKURLSchemeHandler 惰性拉取文件片(iOS 大文件流式发送)。
|
||||||
// 每次 slice 只取 [start, end) 一段,WebView 常驻内存上限即单块大小(p2p 的
|
// 每次 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 仅含该区间字节。
|
// 回 206 Partial Content 仅含该区间字节。
|
||||||
export function rangeSource(
|
export function rangeSource(
|
||||||
url: string,
|
url: string,
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export const enUS: Partial<TranslationDict> = {
|
|||||||
"qr.show.nameHint": "Shown to your other devices. ASCII characters only.",
|
"qr.show.nameHint": "Shown to your other devices. ASCII characters only.",
|
||||||
"qr.show.generate": "Generate code",
|
"qr.show.generate": "Generate code",
|
||||||
"qr.show.scanTitle": "Scan to approve",
|
"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.howto2": "Scan the code above and approve this device.",
|
||||||
"qr.show.waiting": "Waiting for approval…",
|
"qr.show.waiting": "Waiting for approval…",
|
||||||
"qr.show.approved": "Approved — signing you in…",
|
"qr.show.approved": "Approved — signing you in…",
|
||||||
@@ -225,11 +225,11 @@ export const enUS: Partial<TranslationDict> = {
|
|||||||
"notify.transfer.failedTitle": "Transfer failed",
|
"notify.transfer.failedTitle": "Transfer failed",
|
||||||
"settings.account.title": "Account",
|
"settings.account.title": "Account",
|
||||||
"settings.account.signOut": "Sign out",
|
"settings.account.signOut": "Sign out",
|
||||||
"settings.desktop.title": "Desktop",
|
"settings.desktop.title": "App settings",
|
||||||
"settings.desktop.clipboardSync": "Auto-sync clipboard",
|
"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.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.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.downloadDir": "Download folder",
|
||||||
"settings.desktop.downloadDirHint": "Received files are saved here. Leave unset to use the system Downloads folder.",
|
"settings.desktop.downloadDirHint": "Received files are saved here. Leave unset to use the system Downloads folder.",
|
||||||
"settings.desktop.downloadDirChoose": "Change…",
|
"settings.desktop.downloadDirChoose": "Change…",
|
||||||
@@ -344,7 +344,7 @@ export const enUS: Partial<TranslationDict> = {
|
|||||||
"errors.messageOverflow": "Message exceeds 4 KB; send a file instead",
|
"errors.messageOverflow": "Message exceeds 4 KB; send a file instead",
|
||||||
"errors.noReceiver": "No receiver selected",
|
"errors.noReceiver": "No receiver selected",
|
||||||
"errors.devTokenMissing":
|
"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.noAccessToken": "No access token; user must log in",
|
||||||
"errors.clipboardUnavailable": "Browser clipboard API unavailable (requires HTTPS + permission)",
|
"errors.clipboardUnavailable": "Browser clipboard API unavailable (requires HTTPS + permission)",
|
||||||
"errors.clipboardOverflow": "Content too large; exceeds {{max}} byte limit",
|
"errors.clipboardOverflow": "Content too large; exceeds {{max}} byte limit",
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export const zhCN = {
|
|||||||
"qr.show.nameHint": "此名称会展示给你的其他设备,仅可使用 ASCII 字符。",
|
"qr.show.nameHint": "此名称会展示给你的其他设备,仅可使用 ASCII 字符。",
|
||||||
"qr.show.generate": "生成二维码",
|
"qr.show.generate": "生成二维码",
|
||||||
"qr.show.scanTitle": "用手机扫码批准",
|
"qr.show.scanTitle": "用手机扫码批准",
|
||||||
"qr.show.howto1": "在另一台已登录的设备上打开相机或 cdrop。",
|
"qr.show.howto1": "在另一台已登录的设备上打开相机或 Commilitia Drop。",
|
||||||
"qr.show.howto2": "扫描上方二维码,按提示批准此设备。",
|
"qr.show.howto2": "扫描上方二维码,按提示批准此设备。",
|
||||||
"qr.show.waiting": "等待批准…",
|
"qr.show.waiting": "等待批准…",
|
||||||
"qr.show.approved": "已批准,正在进入…",
|
"qr.show.approved": "已批准,正在进入…",
|
||||||
@@ -221,11 +221,11 @@ export const zhCN = {
|
|||||||
"notify.transfer.failedTitle": "传输失败",
|
"notify.transfer.failedTitle": "传输失败",
|
||||||
"settings.account.title": "账号",
|
"settings.account.title": "账号",
|
||||||
"settings.account.signOut": "退出登录",
|
"settings.account.signOut": "退出登录",
|
||||||
"settings.desktop.title": "桌面",
|
"settings.desktop.title": "应用设置",
|
||||||
"settings.desktop.clipboardSync": "剪贴板自动同步",
|
"settings.desktop.clipboardSync": "剪贴板自动同步",
|
||||||
"settings.desktop.clipboardSyncHint": "复制即上传到云端,并接收其他设备的剪贴板更新。",
|
"settings.desktop.clipboardSyncHint": "复制即上传到云端,并接收其他设备的剪贴板更新。",
|
||||||
"settings.desktop.launchAtLogin": "开机自启",
|
"settings.desktop.launchAtLogin": "开机自启",
|
||||||
"settings.desktop.launchAtLoginHint": "登录系统后自动在后台启动 cdrop(菜单栏常驻)。",
|
"settings.desktop.launchAtLoginHint": "登录系统后自动在后台启动 Commilitia Drop(常驻菜单栏或系统托盘)。",
|
||||||
"settings.desktop.downloadDir": "下载目录",
|
"settings.desktop.downloadDir": "下载目录",
|
||||||
"settings.desktop.downloadDirHint": "接收到的文件保存到此目录;留空则使用系统下载目录。",
|
"settings.desktop.downloadDirHint": "接收到的文件保存到此目录;留空则使用系统下载目录。",
|
||||||
"settings.desktop.downloadDirChoose": "更改…",
|
"settings.desktop.downloadDirChoose": "更改…",
|
||||||
@@ -340,7 +340,7 @@ export const zhCN = {
|
|||||||
"errors.messageOverflow": "消息超过4 KB;请改用文件传输",
|
"errors.messageOverflow": "消息超过4 KB;请改用文件传输",
|
||||||
"errors.noReceiver": "未选择接收方",
|
"errors.noReceiver": "未选择接收方",
|
||||||
"errors.devTokenMissing":
|
"errors.devTokenMissing":
|
||||||
"Dev模式:未在.env.local设置VITE_CDROP_DEV_TOKEN",
|
"Dev 模式:未在 .env.local 配置开发令牌",
|
||||||
"errors.noAccessToken": "无访问令牌:用户必须先登录",
|
"errors.noAccessToken": "无访问令牌:用户必须先登录",
|
||||||
"errors.clipboardUnavailable": "浏览器不支持剪贴板API(需HTTPS与权限)",
|
"errors.clipboardUnavailable": "浏览器不支持剪贴板API(需HTTPS与权限)",
|
||||||
"errors.clipboardOverflow": "内容过大,超过 {{max}} 字节上限",
|
"errors.clipboardOverflow": "内容过大,超过 {{max}} 字节上限",
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export const zhTW: Partial<TranslationDict> = {
|
|||||||
"qr.show.nameHint": "此名稱會顯示給你的其他裝置,僅可使用 ASCII 字元。",
|
"qr.show.nameHint": "此名稱會顯示給你的其他裝置,僅可使用 ASCII 字元。",
|
||||||
"qr.show.generate": "產生 QR 碼",
|
"qr.show.generate": "產生 QR 碼",
|
||||||
"qr.show.scanTitle": "用手機掃碼批准",
|
"qr.show.scanTitle": "用手機掃碼批准",
|
||||||
"qr.show.howto1": "在另一部已登入的裝置上開啟相機或 cdrop。",
|
"qr.show.howto1": "在另一部已登入的裝置上開啟相機或 Commilitia Drop。",
|
||||||
"qr.show.howto2": "掃描上方 QR 碼,依提示批准此裝置。",
|
"qr.show.howto2": "掃描上方 QR 碼,依提示批准此裝置。",
|
||||||
"qr.show.waiting": "等待批准…",
|
"qr.show.waiting": "等待批准…",
|
||||||
"qr.show.approved": "已批准,正在進入…",
|
"qr.show.approved": "已批准,正在進入…",
|
||||||
@@ -225,11 +225,11 @@ export const zhTW: Partial<TranslationDict> = {
|
|||||||
"notify.transfer.failedTitle": "傳輸失敗",
|
"notify.transfer.failedTitle": "傳輸失敗",
|
||||||
"settings.account.title": "帳號",
|
"settings.account.title": "帳號",
|
||||||
"settings.account.signOut": "登出",
|
"settings.account.signOut": "登出",
|
||||||
"settings.desktop.title": "桌面",
|
"settings.desktop.title": "應用程式設定",
|
||||||
"settings.desktop.clipboardSync": "剪貼簿自動同步",
|
"settings.desktop.clipboardSync": "剪貼簿自動同步",
|
||||||
"settings.desktop.clipboardSyncHint": "複製後即上傳雲端,並接收其他裝置的剪貼簿更新。",
|
"settings.desktop.clipboardSyncHint": "複製後即上傳雲端,並接收其他裝置的剪貼簿更新。",
|
||||||
"settings.desktop.launchAtLogin": "開機自動啟動",
|
"settings.desktop.launchAtLogin": "開機自動啟動",
|
||||||
"settings.desktop.launchAtLoginHint": "登入系統後自動在背景啟動 cdrop(常駐選單列)。",
|
"settings.desktop.launchAtLoginHint": "登入系統後自動在背景啟動 Commilitia Drop(常駐選單列或系統匣)。",
|
||||||
"settings.desktop.downloadDir": "下載資料夾",
|
"settings.desktop.downloadDir": "下載資料夾",
|
||||||
"settings.desktop.downloadDirHint": "接收到的檔案會儲存至此資料夾;留空則使用系統下載資料夾。",
|
"settings.desktop.downloadDirHint": "接收到的檔案會儲存至此資料夾;留空則使用系統下載資料夾。",
|
||||||
"settings.desktop.downloadDirChoose": "變更…",
|
"settings.desktop.downloadDirChoose": "變更…",
|
||||||
@@ -344,7 +344,7 @@ export const zhTW: Partial<TranslationDict> = {
|
|||||||
"errors.messageOverflow": "訊息超過4 KB;請改用檔案傳輸",
|
"errors.messageOverflow": "訊息超過4 KB;請改用檔案傳輸",
|
||||||
"errors.noReceiver": "未選擇接收方",
|
"errors.noReceiver": "未選擇接收方",
|
||||||
"errors.devTokenMissing":
|
"errors.devTokenMissing":
|
||||||
"Dev模式:未在.env.local設定VITE_CDROP_DEV_TOKEN",
|
"Dev 模式:未在 .env.local 設定開發權杖",
|
||||||
"errors.noAccessToken": "無存取權杖:使用者必須先登入",
|
"errors.noAccessToken": "無存取權杖:使用者必須先登入",
|
||||||
"errors.clipboardUnavailable": "瀏覽器不支援剪貼簿API(需HTTPS與權限)",
|
"errors.clipboardUnavailable": "瀏覽器不支援剪貼簿API(需HTTPS與權限)",
|
||||||
"errors.clipboardOverflow": "內容過大,超過 {{max}} 位元組上限",
|
"errors.clipboardOverflow": "內容過大,超過 {{max}} 位元組上限",
|
||||||
|
|||||||
+3
-3
@@ -196,7 +196,7 @@ export async function abortIncomingDownloadIOS(sessionId: string): Promise<void>
|
|||||||
const BRIDGE_SLICE_MAX = 512 * 1024;
|
const BRIDGE_SLICE_MAX = 512 * 1024;
|
||||||
|
|
||||||
// readFileSliceIOS:经桥让原生按 [start, end) seek 读暂存的待发文件、回 base64,再解回字节。
|
// 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 拦死(Range 头触发预检、scheme handler 无 CORS 头)→ 发送在读文件即失败。改走桥与
|
||||||
// 接收落盘同形,绕开 CORS;整文件仍永不整体进 WebView 内存(按需取片)。
|
// 接收落盘同形,绕开 CORS;整文件仍永不整体进 WebView 内存(按需取片)。
|
||||||
async function readFileSliceIOS(url: string, start: number, end: number): Promise<Uint8Array<ArrayBuffer>>
|
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,
|
name,
|
||||||
size,
|
size,
|
||||||
type: type || "application/octet-stream",
|
type: type || "application/octet-stream",
|
||||||
// path 带 cdrop-file://<id> 引用:iOS 原生数据面(gomobile/pion)据此让原生侧解析回沙盒
|
// path 带 commilitia-drop-file://<id> 引用:iOS 原生数据面据此让原生侧解析回沙盒
|
||||||
// 绝对路径、由 Go 直接读盘发送(见 p2pIos.ts / EngineController.p2pStartOutgoing)。JS 收发
|
// 绝对路径、由 Go 直接读盘发送(见 p2pIos.ts / EngineController.p2pStartOutgoing)。JS 收发
|
||||||
// 路径不读 path,故对回退到 JS 引擎的传输无影响(与桌面 nativeFileSource 同范式)。
|
// 路径不读 path,故对回退到 JS 引擎的传输无影响(与桌面 nativeFileSource 同范式)。
|
||||||
path: url,
|
path: url,
|
||||||
@@ -246,7 +246,7 @@ export function bridgeFileSource(url: string, name: string, size: number, type:
|
|||||||
// 四个入口走 callNative(RPC,原生回 resolve);进度 / 状态 / 出站信令 / 落盘 / 候选对经
|
// 四个入口走 callNative(RPC,原生回 resolve);进度 / 状态 / 出站信令 / 落盘 / 候选对经
|
||||||
// onNativeEvent 反向回来。原生侧契约见 ios/CDrop/Sources/Engine/EngineController.swift。
|
// onNativeEvent 反向回来。原生侧契约见 ios/CDrop/Sources/Engine/EngineController.swift。
|
||||||
|
|
||||||
// nativeP2PStartOutgoing:发起原生发送。filePath 为 cdrop-file://<id> 引用,原生解析回沙盒
|
// nativeP2PStartOutgoing:发起原生发送。filePath 为 commilitia-drop-file://<id> 引用,原生解析回沙盒
|
||||||
// 绝对路径后交 Go 直接读盘(不经 base64 桥 / 不整文件进内存)。
|
// 绝对路径后交 Go 直接读盘(不经 base64 桥 / 不整文件进内存)。
|
||||||
export async function nativeP2PStartOutgoing(
|
export async function nativeP2PStartOutgoing(
|
||||||
sessionId: string, peerName: string, filePath: string, iceServersJSON: string,
|
sessionId: string, peerName: string, filePath: string, iceServersJSON: string,
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ function suggestDeviceName(): string
|
|||||||
else if (ua.includes("Chrome/")) { browser = "Chrome"; }
|
else if (ua.includes("Chrome/")) { browser = "Chrome"; }
|
||||||
else if (ua.includes("Safari/")) { browser = "Safari"; }
|
else if (ua.includes("Safari/")) { browser = "Safari"; }
|
||||||
|
|
||||||
let os = "Desktop";
|
let os = "Computer";
|
||||||
if (ua.includes("Mac")) { os = "macOS"; }
|
if (ua.includes("Mac")) { os = "macOS"; }
|
||||||
else if (ua.includes("Windows")) { os = "Windows"; }
|
else if (ua.includes("Windows")) { os = "Windows"; }
|
||||||
else if (ua.includes("Linux")) { os = "Linux"; }
|
else if (ua.includes("Linux")) { os = "Linux"; }
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ function suggestDefault(): string
|
|||||||
else if (ua.includes("Chrome/")) { browser = "Chrome"; }
|
else if (ua.includes("Chrome/")) { browser = "Chrome"; }
|
||||||
else if (ua.includes("Safari/")) { browser = "Safari"; }
|
else if (ua.includes("Safari/")) { browser = "Safari"; }
|
||||||
|
|
||||||
let os = "Desktop";
|
let os = "Computer";
|
||||||
if (ua.includes("Mac")) { os = "macOS"; }
|
if (ua.includes("Mac")) { os = "macOS"; }
|
||||||
else if (ua.includes("Windows")) { os = "Windows"; }
|
else if (ua.includes("Windows")) { os = "Windows"; }
|
||||||
else if (ua.includes("Linux")) { os = "Linux"; }
|
else if (ua.includes("Linux")) { os = "Linux"; }
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ function StyleGuide()
|
|||||||
<Container size="lg" py="lg">
|
<Container size="lg" py="lg">
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<Group justify="space-between" align="baseline">
|
<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">
|
<Group gap="xs">
|
||||||
<Text size="xs" c="dimmed">当前主题:</Text>
|
<Text size="xs" c="dimmed">当前主题:</Text>
|
||||||
<Tabs<ThemeMode>
|
<Tabs<ThemeMode>
|
||||||
|
|||||||
Reference in New Issue
Block a user