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

This commit is contained in:
2026-07-31 22:56:10 +08:00
parent f7b0f04c9c
commit c480f0d1c2
77 changed files with 861 additions and 263 deletions
+2 -1
View File
@@ -2,7 +2,8 @@
import SwiftUI
// macOS Commilitia Auth BrokerASWebAuthenticationSession
// cdrop:// app BrokerLogin / AuthManager.startBrokerLogin
// commilitia-drop:// app BrokerLogin /
// AuthManager.startBrokerLogin
// iOS LoginView / UIImage iOS Mac Mac
struct MacLoginView: View
{
+31 -9
View File
@@ -1,6 +1,9 @@
# cdrop iOS 真机分发手册
# Commilitia Drop iPhoneiPad 真机分发手册
cdrop 装到你自己的 iPhone(开发签名)。代码侧全部就绪并经模拟器验证;真机只差**账号 + 签名**这一层。本手册走 **ASC API Key 自动 provisioning + 全 CLI** 路线——一把 App Store Connect API Key`-allowProvisioningUpdates` 自动登记连接的设备 + 创建/更新 App ID / App Group / Push / profile / 开发证书,构建装机一条命令,**全程不碰 Xcode GUI、不必手动建 profile**(绕过手动 profile「设备列表只剩 Mac」的坑)。
Commilitia Drop 覆盖安装到已登记的 iPhone 与 iPad(开发签名)。本手册走
**既有 Apple Development 证书 + ASC API Key provisioning + 全 CLI** 路线:签名身份固定为
钥匙串内既有证书的 SHA-1;ASC Key 只用于登记设备、更新 App IDApp GroupPush 能力和 profile。
构建装机一条命令,**全程不碰 Xcode GUI、不新建开发证书、不修改私钥 ACL**。
## 包名与标识
@@ -15,13 +18,24 @@
## 前置
- 付费 Apple Developer Program$99/年)。
- 一台 iPhoneiOS 26+ MacXcode 26 命令行工具)。
- 一个已登录的 cdrop(网页 / 桌面)——用来扫码批准本机登录。
- 一个已登录的 Commilitia Drop(网页 / 桌面)——用来扫码批准本机登录。
---
## A. ASC API Key + .env(核心,一次性)
## A. 既有签名身份 + ASC API Key + .env
`-allowProvisioningUpdates` 凭一把 App Store Connect API Key 替你在门户自动登记设备、建/改 App ID(含 App Groups + Push 能力)、建 App Group、建 profile、建/下开发证书——**不必手动建 profile**。
先用 CLI 列出当前钥匙串中可用的代码签名身份:
```sh
security find-identity -v -p codesigning
```
找到必须复用的 `Apple Development` 证书,记录行首 40 位 SHA-1。若身份不存在或私钥不可用,停止
构建并解锁当前登录钥匙串;不要让自动签名生成替代证书。
`-allowProvisioningUpdates` 凭 App Store Connect API Key 自动登记设备、建/改 App ID(含 App
Groups + Push 能力)、更新 App Group 与 profile,免去手动 profile 的设备选择问题。签名证书由
`CDROP_APPLE_DEVELOPMENT_IDENTITY` 固定,不由 provisioning 自行选择。
**生成 API Key**appstoreconnect.apple.com → **用户和访问****集成****App Store Connect API****团队密钥** → 「+」→ 角色 **Admin**(要能管设备 / profile / 标识符)→ 命名 → 生成 → 下载 `AuthKey_XXXXX.p8`**仅一次**)。记下三样:
@@ -36,11 +50,14 @@ CDROP_TEAM_ID=ABCDE12345
CDROP_ASC_KEY_PATH=/绝对路径/AuthKey_XXXXX.p8
CDROP_ASC_KEY_ID=XXXXXXXXXX
CDROP_ASC_ISSUER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
CDROP_APPLE_DEVELOPMENT_IDENTITY=<既有证书的40位SHA-1>
```
`.p8` 放仓库外即可,`CDROP_ASC_KEY_PATH` 指它绝对路径(落本目录则被 `*.p8` 忽略)。
> 你若已在门户手动建过 App ID / App Group / 证书,自动 provisioning 会复用、不冲突。**「手动建 profile 时设备列表只剩 Mac」**=那台 iPhone 在 **Devices** 里被登记成了 macOS 平台(登记表单 Platform 默认 macOS);自动 provisioning 直接按连接的真机重登记,绕过此坑。
> 你若已在门户手动建过 App ID / App Group,自动 provisioning 会复用或更新。**「手动建
> profile 时设备列表只剩 Mac」**=那台 iPhone 在 **Devices** 里被登记成了 macOS 平台(登记
> 表单 Platform 默认 macOS);自动 provisioning 按连接的真机登记,绕过此坑。
**顺手:APNs Auth Key**(发推送用,与上面 ASC Key 是两码事):门户 **Keys** → 勾 **Apple Push Notifications service** → 下载 `AuthKey_YYYYY.p8`(仅一次),记 Key ID + Issuer ID(服务端配,见 C)。
@@ -53,14 +70,19 @@ just ios-devices # 手机插 USB,读 UDID
just ios-device <你的设备UDID> # 自动 provisioning 构建 + devicectl 装机
```
- `just ios-device` `xcodegen generate``xcodebuild -allowProvisioningUpdates`(凭 .env 的 ASC Key 自动签名 + 登记设备 + 建 profile)→ `xcrun devicectl device install app`。无 Xcode GUI。
- `just ios-device` 先确认 `.env` 指定的既有证书仍在钥匙串,再执行 `xcodegen generate`
`xcodebuild -allowProvisioningUpdates`(固定该 SHA-1 签名,ASC Key 更新设备与 profile)→
`xcrun devicectl device install app`。无 Xcode GUI。
- 首次装机后,iPhone 上首启该开发者 app 即可直接跑(付费 ADP 开发证书,无需手动「信任开发者」)。
若出现 `errSecInternalComponent`,优先在当前用户的普通 Terminal 解锁登录钥匙串,然后重试原证书。
不要创建新证书、修改钥匙串 ACL,或改用 Xcode GUI。
---
## C. 服务端前置
1. **引擎可达**:真机上引擎 WebView 加载 `https://drop.commilitia.net/engine.html`——该文件随 cdrop 二进制部署到 prod`//go:embed` 进 binary`just docker-image` 含最新 `dist`)。手机要用,prod 须是含本轮改动的最新部署。
1. **引擎可达**:真机上引擎 WebView 加载 `https://drop.commilitia.net/engine.html`——该文件随 Commilitia Drop 服务端二进制部署到 prod`//go:embed` 进 binary`just docker-image` 含最新 `dist`)。手机要用,prod 须是含本轮改动的最新部署。
2. **APNs 真发**:把 A 里那把 **APNs Auth Key** `.p8` 放到服务器,给后端容器配 `CDROP_APNS_KEY_PATH` / `CDROP_APNS_KEY_ID` / `CDROP_APNS_TEAM_ID` / `CDROP_APNS_TOPIC=net.commilitia.Commilitia-Drop` / `CDROP_APNS_ENV`Xcode 开发构建的 device token 属 **sandbox**,故联调填 `sandbox`)。缺配置则推送惰性关闭,其余功能照常。
3. 本机联调可选:环境变量 `CDROP_ENGINE_URL` 指向可达引擎。
@@ -71,7 +93,7 @@ just ios-device <你的设备UDID> # 自动 provisioning 构建 + devicectl
代码已实现,下列是真机才能验的点:
- [ ] 液态玻璃真机渲染(陀螺仪高光 / 动效)。
- [ ] 扫码登录:本机显码 → 已登录 cdrop 扫码批准 → 进主界面(强制 full/persist)。
- [ ] 扫码登录:本机显码 → 已登录 Commilitia Drop 扫码批准 → 进主界面(强制 full/persist)。
- [ ] **本地网络权限 + 同内网直连**R-iOS-6):首次同内网传输弹 `NSLocalNetworkUsageDescription`;授予后 ICE 收 host 候选、走真直连;拒绝则回退中继仍可传。
- [ ] 发送 / 接收:选文件 → 选设备 → 对端收到;对端发来落 Files(Documents)。
- [ ] **大文件流式发送**(R-iOS-4 已实现):发大文件,WebView 内存应有界(按 Range 块拉取,不整文件入内存)。
+1 -1
View File
@@ -13,7 +13,7 @@
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<string>Commilitia Drop</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
+1 -1
View File
@@ -59,7 +59,7 @@ final class ShareViewController: UIViewController
// completeRequest app
private func openAndFinish()
{
if let url = URL(string: "cdrop://share") { openViaResponder(url) }
if let url = URL(string: "commilitia-drop://share") { openViaResponder(url) }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35)
{ [weak self] in
self?.extensionContext?.completeRequest(returningItems: nil)
+3 -3
View File
@@ -135,7 +135,7 @@ final class AuthManager
let verifier = PKCE.randomToken(64)
let challenge = PKCE.challenge(for: verifier)
let state = PKCE.randomToken(24)
let redirectURI = "cdrop://auth-callback"
let redirectURI = "commilitia-drop://auth-callback"
var comp = URLComponents(string: cfg.brokerURL.trimmingTrailingSlash() + "/device/authorize")!
comp.queryItems = [
@@ -144,13 +144,13 @@ final class AuthManager
URLQueryItem(name: "state", value: state),
URLQueryItem(name: "code_challenge", value: challenge),
URLQueryItem(name: "code_challenge_method", value: "S256"),
URLQueryItem(name: "description", value: "Commilitia Drop iOS"),
URLQueryItem(name: "description", value: "Commilitia Drop"),
]
guard let authURL = comp.url else { throw BrokerLoginError.incompleteConfig }
let flow = BrokerAuthFlow()
brokerFlow = flow
let callback = try await flow.run(url: authURL, callbackScheme: "cdrop")
let callback = try await flow.run(url: authURL, callbackScheme: "commilitia-drop")
brokerFlow = nil
if gen != loginGeneration { return }
+8 -5
View File
@@ -8,13 +8,15 @@ import AppKit
#endif
// Broker device-authorization + PKCE desktop/platform/oauth.goiOS
// loopback ASWebAuthenticationSession + scheme cdrop://auth-callback
// 1. GET {broker}/device/authorize?app=cdrop&redirect_uri=cdrop://auth-callback&state&code_challenge&S256
// 2. broker SSO cdrop://auth-callback?code=..&state=..
// loopback ASWebAuthenticationSession + scheme
// commilitia-drop://auth-callback
// 1. GET {broker}/device/authorize?app=commilitia-drop&redirect_uri=commilitia-drop://auth-callback&...
// 2. broker SSO commilitia-drop://auth-callback?code=..&state=..
// 3. POST {broker}/device/token {code, code_verifier, redirect_uri} bootstrap
// 4. POST {api}/api/auth/device-sessionBearer bootstrap cdrop / device_id
//
// broker cdrop redirect_uri cdrop://auth-callback loopback
// broker Commilitia Drop redirect_uri
// commilitia-drop://auth-callback loopback
enum BrokerLoginError: Error
{
@@ -35,7 +37,8 @@ final class BrokerAuthFlow: NSObject, ASWebAuthenticationPresentationContextProv
{
private var session: ASWebAuthenticationSession?
// URLcdrop://auth-callback?code=..&state=.. /
// URLcommilitia-drop://auth-callback?code=..&state=..
// /
func run(url: URL, callbackScheme: String) async throws -> URL
{
try await withCheckedThrowingContinuation
+2 -2
View File
@@ -72,11 +72,11 @@ struct AppRoot: View
{ _, uid in
if let uid { engine.activateRecords(userId: uid) }
}
// Share Extension cdrop://share RootView
// Share Extension commilitia-drop://share
// AppRoot 便 / onAppear
.onOpenURL
{ url in
guard url.scheme == "cdrop", url.host == "share" else { return }
guard url.scheme == "commilitia-drop", url.host == "share" else { return }
engine.loadPendingShares()
}
// AppDelegate +
+13 -13
View File
@@ -193,7 +193,7 @@ final class EngineController: NSObject
private var webView: WKWebView?
private let downloads = DownloadManager()
// id URL cdrop-file://<id>
// id URL commilitia-drop-file://<id>
// WKURLSchemeHandler
private var outgoing: [String: URL] = [:]
@@ -271,7 +271,7 @@ final class EngineController: NSObject
}
// makeWebView WebView __CDROP_BOOT__device_type:"ios"
// cdrop-file scheme JS
// commilitia-drop-file scheme JS
func makeWebView() -> WKWebView
{
if let existing = webView { return existing }
@@ -297,7 +297,7 @@ final class EngineController: NSObject
let cfg = WKWebViewConfiguration()
cfg.userContentController = ucc
cfg.setURLSchemeHandler(self, forURLScheme: "cdrop-file")
cfg.setURLSchemeHandler(self, forURLScheme: "commilitia-drop-file")
// Auth Broker refresh_tokenPOST /api/auth/refresh
// broker cdrop_session cookie URLSession cookie WebView
@@ -672,7 +672,7 @@ final class EngineController: NSObject
webView?.evaluateJavaScript("window.__cdropEngineEvent(\(jsString(name)), \(json));")
}
// sendFile cdrop-file://<id> +
// sendFile commilitia-drop-file://<id> +
func sendFile(to target: String, fileURL: URL)
{
let ref = stageOutgoingFile(fileURL)
@@ -687,7 +687,7 @@ final class EngineController: NSObject
])
}
// Share Extension App Group cdrop://share
// Share Extension App Group commilitia-drop://share
//
var pendingShareFiles: [URL] = []
@@ -730,7 +730,7 @@ final class EngineController: NSObject
}
// #14PhotosPicker PhotosPickerItem loadTransferable
// cdrop-file scheme Range
// commilitia-drop-file scheme
// URL temporaryDirectory nil
func stagePhotoData(_ data: Data, suggestedName: String) -> URL?
{
@@ -743,18 +743,18 @@ final class EngineController: NSObject
catch { return nil }
}
// cdrop-file://<id> URL
// commilitia-drop-file://<id>
// start...Access
private func stageOutgoingFile(_ url: URL) -> String
{
let id = UUID().uuidString
_ = url.startAccessingSecurityScopedResource()
outgoing[id] = url
return "cdrop-file://\(id)"
return "commilitia-drop-file://\(id)"
}
// [start,end) seek base64 bridgeFileSource
// url cdrop-file://<id>id stageOutgoingFile /
// url commilitia-drop-file://<id>id stageOutgoingFile
fileprivate func readOutgoingSlice(url: String, start: Int, end: Int) throws -> String
{
guard let id = URL(string: url)?.host, let fileURL = outgoing[id]
@@ -787,7 +787,7 @@ final class EngineController: NSObject
return e
}
// cdrop-file://<id> Go
// commilitia-drop-file://<id>
// 访 stageOutgoingFile reset Go os.Open
//
private func resolveOutgoingPath(_ ref: String) -> String
@@ -871,7 +871,7 @@ extension EngineController: WKScriptMessageHandler
end: Self.intOf(payload["end"]))
resolve(id: id, ok: true, value: b64)
case "p2pStartOutgoing":
// filePath cdrop-file://<id>
// filePath commilitia-drop-file://<id>
// libwebrtc completion offer + resolveOnMain
let engine = nativeTransferEngine()
let path = resolveOutgoingPath(payload["filePath"] as? String ?? "")
@@ -1133,11 +1133,11 @@ extension EngineController: WKScriptMessageHandler
}
}
// MARK: - schemecdrop-file
// MARK: - schemecommilitia-drop-file
extension EngineController: WKURLSchemeHandler
{
// cdrop-file://<id> JS fetch
// commilitia-drop-file://<id>
// PLAN §2 / R-iOS-4rangeSource `Range` seek
// 206 Partial Content jetsam Range
// 退 Range
@@ -6,7 +6,7 @@ import WebRTC
// WebKit/libwebrtc desktop/NATIVE-TRANSFER.md §7
//
// 线 desktop/enginepion web/src/features/transfer/p2p.ts pion
// / iOS JS DataChannel "cdrop-file"ordered meta/done/ack
// / iOS JS DataChannel "commilitia-drop-file"ordered
// 64KB payload {type, sdp?, candidate?} session.go +
// engine.go Swift ack
//
@@ -39,7 +39,7 @@ protocol P2PEngineDelegate: AnyObject
private enum Wire
{
static let channelName = "cdrop-file"
static let channelName = "commilitia-drop-file"
static let chunkSize = 64 * 1024
static let highWatermark: UInt64 = 16 * 1024 * 1024
static let lowWatermark: UInt64 = 4 * 1024 * 1024
@@ -94,7 +94,7 @@ final class LibWebRtcEngine
}
// StartOutgoing PeerConnection + DataChannel offer filePath
// filePath cdrop-file://<id> completion offer
// filePath commilitia-drop-file://<id> completion offer
// setLocalDescription JS seed p2pIos.ts chain
func startOutgoing(sessionId: String, peerName: String, filePath: String,
iceServersJSON: String, completion: @escaping (Error?) -> Void)
+3 -3
View File
@@ -23,7 +23,7 @@
<string>en</string>
</array>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<string>Commilitia Drop</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
@@ -35,7 +35,7 @@
<string>net.commilitia.Commilitia-Drop</string>
<key>CFBundleURLSchemes</key>
<array>
<string>cdrop</string>
<string>commilitia-drop</string>
</array>
</dict>
</array>
@@ -50,7 +50,7 @@
</dict>
<key>NSBonjourServices</key>
<array>
<string>_cdrop._tcp</string>
<string>_commilitia-drop._tcp</string>
</array>
<key>NSCameraUsageDescription</key>
<string>Commilitia Drop 使用相机扫描二维码登录新设备。</string>
@@ -3,10 +3,11 @@ import Network
// #5iOS 14+ App 访 WKWebView
// WebRTC host 退 srflx / relayWKWebView
// 宿 App 访 _cdrop._tcp
// 宿 App 访
// _commilitia-drop._tcp
// NWBrowser RTCPeerConnection host / mDNS
// browser project.yml
// NSLocalNetworkUsageDescription + NSBonjourServices(_cdrop._tcp)
// NSLocalNetworkUsageDescription + NSBonjourServices(_commilitia-drop._tcp)
enum LocalNetworkPermission
{
private static var browser: NWBrowser?
@@ -18,7 +19,7 @@ enum LocalNetworkPermission
guard browser == nil else { return }
let params = NWParameters()
params.includePeerToPeer = true
let b = NWBrowser(for: .bonjour(type: "_cdrop._tcp", domain: nil), using: params)
let b = NWBrowser(for: .bonjour(type: "_commilitia-drop._tcp", domain: nil), using: params)
b.browseResultsChangedHandler = { _, _ in }
browser = b
b.start(queue: .main)
+4 -4
View File
@@ -97,7 +97,7 @@
"qr.show.nameHint": "Shown to your other devices. ASCII characters only.",
"qr.show.generate": "Generate code",
"qr.show.scanTitle": "Scan to approve",
"qr.show.howto1": "Open the camera or cdrop on another signed-in device.",
"qr.show.howto1": "Open the camera or Commilitia Drop on another signed-in device.",
"qr.show.howto2": "Scan the code above and approve this device.",
"qr.show.waiting": "Waiting for approval…",
"qr.show.approved": "Approved — signing you in…",
@@ -189,11 +189,11 @@
"notify.transfer.failedTitle": "Transfer failed",
"settings.account.title": "Account",
"settings.account.signOut": "Sign out",
"settings.desktop.title": "Desktop",
"settings.desktop.title": "App settings",
"settings.desktop.clipboardSync": "Auto-sync clipboard",
"settings.desktop.clipboardSyncHint": "Copies are uploaded to the cloud, and clipboard updates from your other devices are applied here.",
"settings.desktop.launchAtLogin": "Launch at login",
"settings.desktop.launchAtLoginHint": "Start cdrop in the background after you sign in (lives in the menu bar).",
"settings.desktop.launchAtLoginHint": "Start Commilitia Drop in the background after you sign in (lives in the menu bar or system tray).",
"settings.desktop.downloadDir": "Download folder",
"settings.desktop.downloadDirHint": "Received files are saved here. Leave unset to use the system Downloads folder.",
"settings.desktop.downloadDirChoose": "Change…",
@@ -292,7 +292,7 @@
"errors.messageEmpty": "Message is empty",
"errors.messageOverflow": "Message exceeds 4 KB; send a file instead",
"errors.noReceiver": "No receiver selected",
"errors.devTokenMissing": "Dev mode: VITE_CDROP_DEV_TOKEN is not set in .env.local",
"errors.devTokenMissing": "Dev mode: the development token is not configured in .env.local",
"errors.noAccessToken": "No access token; user must log in",
"errors.clipboardUnavailable": "Browser clipboard API unavailable (requires HTTPS + permission)",
"errors.clipboardOverflow": "Content too large; exceeds {{max}} byte limit",
+4 -4
View File
@@ -97,7 +97,7 @@
"qr.show.nameHint": "此名称会展示给你的其他设备,仅可使用 ASCII 字符。",
"qr.show.generate": "生成二维码",
"qr.show.scanTitle": "用手机扫码批准",
"qr.show.howto1": "在另一台已登录的设备上打开相机或 cdrop。",
"qr.show.howto1": "在另一台已登录的设备上打开相机或 Commilitia Drop。",
"qr.show.howto2": "扫描上方二维码,按提示批准此设备。",
"qr.show.waiting": "等待批准…",
"qr.show.approved": "已批准,正在进入…",
@@ -189,11 +189,11 @@
"notify.transfer.failedTitle": "传输失败",
"settings.account.title": "账号",
"settings.account.signOut": "退出登录",
"settings.desktop.title": "桌面",
"settings.desktop.title": "应用设置",
"settings.desktop.clipboardSync": "剪贴板自动同步",
"settings.desktop.clipboardSyncHint": "复制即上传到云端,并接收其他设备的剪贴板更新。",
"settings.desktop.launchAtLogin": "开机自启",
"settings.desktop.launchAtLoginHint": "登录系统后自动在后台启动 cdrop(菜单栏常驻)。",
"settings.desktop.launchAtLoginHint": "登录系统后自动在后台启动 Commilitia Drop常驻菜单栏或系统托盘)。",
"settings.desktop.downloadDir": "下载目录",
"settings.desktop.downloadDirHint": "接收到的文件保存到此目录;留空则使用系统下载目录。",
"settings.desktop.downloadDirChoose": "更改…",
@@ -292,7 +292,7 @@
"errors.messageEmpty": "消息为空",
"errors.messageOverflow": "消息超过4 KB;请改用文件传输",
"errors.noReceiver": "未选择接收方",
"errors.devTokenMissing": "Dev模式:未在.env.local设置VITE_CDROP_DEV_TOKEN",
"errors.devTokenMissing": "Dev 模式:未在 .env.local 配置开发令牌",
"errors.noAccessToken": "无访问令牌:用户必须先登录",
"errors.clipboardUnavailable": "浏览器不支持剪贴板API(需HTTPS与权限)",
"errors.clipboardOverflow": "内容过大,超过 {{max}} 字节上限",
+4 -4
View File
@@ -97,7 +97,7 @@
"qr.show.nameHint": "此名稱會顯示給你的其他裝置,僅可使用 ASCII 字元。",
"qr.show.generate": "產生 QR 碼",
"qr.show.scanTitle": "用手機掃碼批准",
"qr.show.howto1": "在另一部已登入的裝置上開啟相機或 cdrop。",
"qr.show.howto1": "在另一部已登入的裝置上開啟相機或 Commilitia Drop。",
"qr.show.howto2": "掃描上方 QR 碼,依提示批准此裝置。",
"qr.show.waiting": "等待批准…",
"qr.show.approved": "已批准,正在進入…",
@@ -189,11 +189,11 @@
"notify.transfer.failedTitle": "傳輸失敗",
"settings.account.title": "帳號",
"settings.account.signOut": "登出",
"settings.desktop.title": "桌面",
"settings.desktop.title": "應用程式設定",
"settings.desktop.clipboardSync": "剪貼簿自動同步",
"settings.desktop.clipboardSyncHint": "複製後即上傳雲端,並接收其他裝置的剪貼簿更新。",
"settings.desktop.launchAtLogin": "開機自動啟動",
"settings.desktop.launchAtLoginHint": "登入系統後自動在背景啟動 cdrop(常駐選單列)。",
"settings.desktop.launchAtLoginHint": "登入系統後自動在背景啟動 Commilitia Drop(常駐選單列或系統匣)。",
"settings.desktop.downloadDir": "下載資料夾",
"settings.desktop.downloadDirHint": "接收到的檔案會儲存至此資料夾;留空則使用系統下載資料夾。",
"settings.desktop.downloadDirChoose": "變更…",
@@ -292,7 +292,7 @@
"errors.messageEmpty": "訊息為空",
"errors.messageOverflow": "訊息超過4 KB;請改用檔案傳輸",
"errors.noReceiver": "未選擇接收方",
"errors.devTokenMissing": "Dev模式:未在.env.local設定VITE_CDROP_DEV_TOKEN",
"errors.devTokenMissing": "Dev 模式:未在 .env.local 設定開發權杖",
"errors.noAccessToken": "無存取權杖:使用者必須先登入",
"errors.clipboardUnavailable": "瀏覽器不支援剪貼簿API(需HTTPS與權限)",
"errors.clipboardOverflow": "內容過大,超過 {{max}} 位元組上限",
+1 -1
View File
@@ -80,7 +80,7 @@ struct RootView: View
.opacity(0)
.allowsHitTesting(false)
}
// Share Extension cdrop://share /
// Share Extension commilitia-drop://share /
// SendComposeSheetonSend sendShares
// move pendingShareFiles
.onAppear { engine.loadPendingShares() }
+2 -2
View File
@@ -31,7 +31,7 @@
<string>net.commilitia.Commilitia-Drop</string>
<key>CFBundleURLSchemes</key>
<array>
<string>cdrop</string>
<string>commilitia-drop</string>
</array>
</dict>
</array>
@@ -41,7 +41,7 @@
<string>26.0</string>
<key>NSBonjourServices</key>
<array>
<string>_cdrop._tcp</string>
<string>_commilitia-drop._tcp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>Commilitia Drop 需要访问本地网络以发现同内网设备并建立直连传输。</string>
+1 -1
View File
@@ -13,7 +13,7 @@
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<string>Commilitia Drop</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
+9 -6
View File
@@ -45,14 +45,15 @@ targets:
path: Sources/Info.plist
properties:
CFBundleDisplayName: Commilitia Drop
CFBundleName: Commilitia Drop
# 品牌字标字体 Fredoka SemiBoldShared/Fonts/,随 bundle 打包),用于 Wordmark(与 web 一致)。
UIAppFonts:
- Fredoka-SemiBold.ttf
# cdrop:// URL schemeShare Extension 交接后经 cdrop://share 深链唤起主 app 选设备发送。
# commilitia-drop:// URL schemeShare Extension 交接后唤起主 app 选设备发送。
CFBundleURLTypes:
- CFBundleURLName: net.commilitia.Commilitia-Drop
CFBundleURLSchemes:
- cdrop
- commilitia-drop
# 声明支持的语言,使系统控件(EditButton 的「编辑」、滑动删除的「删除」等系统字符
# 串)跟随设备语言本地化,而非永远英文。开发区域设简体。
CFBundleDevelopmentRegion: zh-Hans
@@ -88,7 +89,7 @@ targets:
NSAllowsLocalNetworking: true
NSLocalNetworkUsageDescription: "Commilitia Drop 需要访问本地网络以发现同内网设备并建立直连传输。"
NSBonjourServices:
- "_cdrop._tcp"
- "_commilitia-drop._tcp"
NSCameraUsageDescription: "Commilitia Drop 使用相机扫描二维码登录新设备。"
# 图库选择:从相册选取图片 / 视频发送(PhotosPicker,见 RootView #14)。
NSPhotoLibraryUsageDescription: "Commilitia Drop 需要访问照片库,以便从相册选取图片或视频发送给其他设备。"
@@ -115,6 +116,7 @@ targets:
path: Share/Info.plist
properties:
CFBundleDisplayName: Commilitia Drop
CFBundleName: Commilitia Drop
# 品牌字标字体(分享引导界面的 Wordmark 用),随扩展 bundle 打包 + 注册。
UIAppFonts:
- Fredoka-SemiBold.ttf
@@ -145,6 +147,7 @@ targets:
path: Widgets/Info.plist
properties:
CFBundleDisplayName: Commilitia Drop
CFBundleName: Commilitia Drop
NSExtension:
NSExtensionPointIdentifier: com.apple.widgetkit-extension
settings:
@@ -212,15 +215,15 @@ targets:
- zh-Hans
- zh-Hant
- en
# ASWebAuthenticationSession 的 OAuth 回调经 cdrop:// scheme 回到 app(同 iOS
# ASWebAuthenticationSession 的 OAuth 回调经 commilitia-drop:// scheme 回到 app。
CFBundleURLTypes:
- CFBundleURLName: net.commilitia.Commilitia-Drop
CFBundleURLSchemes:
- cdrop
- commilitia-drop
# 本地网络:libwebrtc 收集 host 候选、同内网直连须本地网络权限(macOS 15+ 亦弹窗)。
NSLocalNetworkUsageDescription: "Commilitia Drop 需要访问本地网络以发现同内网设备并建立直连传输。"
NSBonjourServices:
- "_cdrop._tcp"
- "_commilitia-drop._tcp"
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: net.commilitia.Commilitia-Drop
+2 -2
View File
@@ -1,4 +1,4 @@
# cdrop web ↔ iOS 同步检查单
# Commilitia Drop Web ↔ iOS 同步检查单
> 机制,不是清单快照。webReact)与 iOSSwiftUI)视图层**有意分叉**(液态玻璃决定不可能像素一致)。一致性**优先靠强约束同步**(单一真源、机械消费、漂移不可能,见 `ios/PLAN.md` §5);本文件**只装强约束管不住、需人判定的残余**。
> 关联:`ios/PLAN.md` 决策 D / §5。
@@ -56,7 +56,7 @@
| 2026-06-24 | 建档(iOS 未开工) | — | 初始:架构 A 定、引擎 / i18n / 品牌 / API 四强约束确立,检查单建立 |
| 2026-06-24 | 引擎入口 net/ios.ts + engine/main.ts(未提交) | — | web 侧无头引擎入口 + iOS 桥 + 接收 sink 接入 + Vite 多入口;tsc / build 双过 |
| 2026-06-24 | — | ios/CDrop/ scaffold(未提交) | iOS 原生壳:液态玻璃 TabView + 离屏引擎 WebView 宿主 + 桥 + 落盘管理器;编过 iphonesimulator26.5 |
| 2026-06-24 | — | ios/CDrop/ 登录+发送(未提交) | 扫码登录新设备侧(AuthManager + LoginView,对 prod qr/start 验通显码)+ 发送路径(cdrop-file scheme 供文件 + 文件选择器)+ 会话注入引擎 boot |
| 2026-06-24 | — | ios/CDrop/ 登录+发送(未提交) | 扫码登录新设备侧(AuthManager + LoginView,对 prod qr/start 验通显码)+ 发送路径(commilitia-drop-file scheme 供文件 + 文件选择器)+ 会话注入引擎 boot |
| 2026-06-24 | i18n locales + scripts/emit-i18n.mjs(未提交) | ios/CDrop i18n(未提交) | **强约束②落地**web locales 加 `ios.*` 键 → `emit-i18n.mjs` 产出 JSON → iOS `t()` 读(loader I18n.swift)。用户可见**去代号 cdrop**(品牌用 `app.brand`=Commilitia Drop),显示名改 Commilitia Drop。**改 locale 后须重跑 `node web/scripts/emit-i18n.mjs`**(已记入脚本头) |
| 2026-06-24 | engine/main.ts 加 presence 推送 + 初始快照(未提交) | RootView / EngineController / AuthManager 真数据绑定(未提交) | **A 任务**:引擎订阅 `store.devices``presence` 事件 + boot 后补发 presence/transfers 初始快照;原生 `EngineController` 解码进 `@Observable` devices/transfers/history`RootView` 三屏全替 demo→真数据 + 新增 `TransferDetailView`TODO #5)。i18n 加 `ios.detail.*` / `ios.settings.account|user|logout` / `ios.transfer.empty|incoming|outgoing` / `ios.send.noDevices` / `ios.devices.empty`,传输态文案复用共享 `transfer.state|phase|mode|bytesRate`。iOS `t()``{{name}}` 插值变体对齐 web。Keychain 会话持久化 + 设置页登出(闭合 TODO #4)。web tsc/build + xcodebuild iphonesimulator26.5 双过,模拟器三屏空态截图验。**已部署 prod**:`engine.html``engine-BB8iav7K.js`(含 presence 推送),真会话下即生效 |
| 2026-06-24 | engine/main.tstransfers 推送节流 ~3Hz + toWire 加 ice 摘要(未提交,**待部署** | EngineController 手动容忍解析 + ICE 字段;RootView 详情页 ICE 行 + 收到文件视图 + 设备名编辑 + 设备计数;DownloadManager listFiles/delete;新 DeviceNameStore / QuickLookPreviewproject.yml 加文件共享键 + 品牌化权限串(未提交) | **首轮真机测试反馈修复**:① 速度数字 ~10Hz 闪 → 节流活跃传输推送到 3Hz(iOS-only,亦降 P2P 接收主线程抖动);② 设备列表空 → 原生改手动容忍解析(严格 JSONDecoder 遇 bool/int 失配整组失败的隐患),加「已知设备」计数诊断;③ 收到文件无处开 / Files 不可见 / 阻塞发送 → `UIFileSharingEnabled`+`LSSupportsOpeningDocumentsInPlace`Documents 已是落地目录)+ 应用内「收到的文件」视图(QuickLook 预览 / 系统分享 / 转发在线设备 / 删除);④ 设备名不可改 → `DeviceNameStore`UserDefaults)+ 设置页编辑框(后端令牌烤名、改名下次登录生效);⑤ P2P <100KB/srelay 1MB/s)→ 详情页暴露选中 ICE 候选对诊断是否 TURN 中继(不盲改共享 p2p.ts 水位)。i18n 加 `ios.files.*`/`ios.settings.deviceName|deviceNameNote|deviceCount`ICE 行复用 `transfer.debug.*`。web 双过 + xcodebuild 编过 + 模拟器设置 / 传输屏截图验。**已部署 prod**:`engine.html``engine-BC6mZ9zP.js`(含节流 + ice 摘要) |
+4 -4
View File
@@ -1,4 +1,4 @@
# cdrop iOS 客户端实施计划
# Commilitia Drop iOS 客户端实施计划
> 原生 SwiftUI 界面(液态玻璃)· 离屏无头 WebView 复用传输引擎 · 控制中心剪贴板 + Share Extension + APNs
> 状态:**架构已定(2026-06-24 修订)**,账号无关部分即刻可开工;真机 / 推送 / 扩展实测硬等 Apple Developer 账号。
@@ -60,7 +60,7 @@
- **无头 WebView 此后只跑编排**:信令转发 / 会话 / presence / 消息 / 状态机 / relay`hub.ts` + `transfer.ts`),**不再承载 P2P 数据面**。决策 A 的“原生壳 + 无头引擎”仍成立,只是引擎的职责收窄到编排层。
- **缘由(U1→U2 翻案)**:先试 gomobile+pion(与桌面同引擎,U1),真机 POC **证伪**——Go raw BSD socket 不与 iOS Network framework 集成(连接时好时坏 + 中途断流)。按闸退到 **libwebrtc.framework**U2stasel/WebRTC M149 经 SPM、pin `exactVersion 149.0.0`)。完整决策依据与实测证据见 `desktop/NATIVE-TRANSFER.md` §7/§8。
- **收益**libwebrtc 原生集成 iOS 网络栈 + DcSCTP 默认 ~5MB rwnd——摆脱 WebKit 写死的 256KB 接收窗(R-iOS-3 的吞吐残差)对数据面的钳制,连通性与吞吐一并根治。
- **互通靠线协议逐字节对齐**`LibWebRtcEngine``desktop/engine``engine.go`/`session.go` 线协议的忠实 Swift 端口——DataChannel `cdrop-file`ordered)、`meta`/二进制分片(64KB)/`done`/`ack` 帧、16MB/4MB 水位背压、ack 追平完成、不冲突落盘。故 iOS↔桌面(pion)、iOS↔浏览器(JS)三端互通。
- **互通靠线协议逐字节对齐**`LibWebRtcEngine``desktop/engine``engine.go`/`session.go` 线协议的忠实 Swift 端口——DataChannel `commilitia-drop-file`ordered)、`meta`/二进制分片(64KB)/`done`/`ack` 帧、16MB/4MB 水位背压、ack 追平完成、不冲突落盘。故 iOS↔桌面(pion)、iOS↔浏览器(JS)三端互通。
- **桥与 JS 路由零改动**`IOS_NATIVE` 开关、`p2pIos.ts`/`net/ios.ts` 桥协议原样;只在 Swift 侧新增一条原生传输 RPC(`startOutgoing`/`startIncoming`/`handleSignal`/`cancel`completion-based,经 `resolveOnMain` 跳主线程 resolve)。**iOS 定向的引擎配置绝不回流 `desktop/engine` 共享包**。
- **验证**:环回端到端(两个引擎同进程交叉连、20MB 经完整线协议传输后逐字节比对)✅;模拟器全 app 编译 ✅;web typecheck/build ✅。真机 / 真网吞吐互通为剩余闸(须 deploy prod 让设备拿到 `IOS_NATIVE=true` 引擎 + 真机装机,二者 Touch ID 门控)。
@@ -93,7 +93,7 @@ Share Extension 落法(据上):**抓文件 → 写 App Group 容器 →
- **引擎 → 原生**:① 单向通知(`postMessage({ notify, payload })`):`ready` / `transfers` / `transferDone` / `sendStarted` / `error`;② 请求 / 响应 RPC`postMessage({ id, method, payload })`,原生回调 `window.__cdropEngineResolve(id, ok, value)`):接收落盘 `saveDownload` / `beginDownload` / `appendDownload` / `finalizeDownload` / `abortDownload`data 为 base64,复用桌面同形)。剪贴板不走此桥(决策 C 纯原生)。
- **复用边界**:接收侧字节桥沿用桌面已验证的 base64 分批、有界内存(iOS 上限更小,64MB/16MB 避 jetsam);**发送侧经 `url` 取回文件为唯一新实现点**——当前整文件 fetch 进内存(小 / 中文件可行),大文件按 slice 向原生拉取是真机优化点(R-iOS-4)。
- **已落地(web 侧,账号无关、`tsc`/`vite build` 双过、对现网零影响)**`net/ios.ts`(桥 + `isIOSShell` + 接收封装 + `notifyNative`)、`engine/main.ts`(无头入口)、`incomingSink.ts`(通用混合槽路由 iOS)、`engine.html` + `vite.config.ts` 多入口。
- **加载源(关键)**:无头引擎**必须从安全上下文加载**——`RTCPeerConnection` 在非安全 origin(如自定义 `cdrop://` schemeWebKit 视作 non-secure context)下可能被禁。故引擎页走 `https://`:线上 `drop.commilitia.net` 或应用内本地 https 服务的静态 bundle**自定义 scheme 仅用于 `WKURLSchemeHandler` 给 JS 流式喂文件字节**,不可用它直载引擎页。见 R-iOS-3。
- **加载源(关键)**:无头引擎**必须从安全上下文加载**——`RTCPeerConnection` 在非安全 origin(如自定义 `commilitia-drop://` schemeWebKit 视作 non-secure context)下可能被禁。故引擎页走 `https://`:线上 `drop.commilitia.net` 或应用内本地 https 服务的静态 bundle**自定义 scheme 仅用于 `WKURLSchemeHandler` 给 JS 流式喂文件字节**,不可用它直载引擎页。见 R-iOS-3。
---
@@ -124,7 +124,7 @@ Share Extension 落法(据上):**抓文件 → 写 App Group 容器 →
| **I6** APNs | 服务端通道(§3)+ 原生注册;不含剪贴板 | `.p8` + 真机硬等账号 |
| **I7** 打磨与分发 | 后台窗口;图标 / 启动屏(复用品牌资产);旁加载分发 | 硬等账号 |
> **实现状态(2026-06-27**I1/I2/I3/I4/I5/I6 的**代码**全部落地——发送端流式(R-iOS-4FileSource + 原生 Range,整文件不进 WebView 内存);后台续传(BGContinuedProcessingTask);APNs(后端 `internal/apns` ES256 + 原生注册经引擎桥);Share ExtensionApp Group 收件箱 + `cdrop://share` 深链);控制中心两控件(**专用 broker 设备会话**,纯原生 REST,不与引擎抢 refresh 轮换)。模拟器验 + ultracode 多 agent 审查修讫(0 HIGH,修 2 MED + 7 LOW)。**I0 账号 / 证书 + 真机签名 / 真发推送 / 旁加载分发**仍账号门控——手册 `ios/CDrop/REALDEVICE.md`ASC API Key 自动 provisioning + `just ios-device` 全 CLI 装机,`-allowProvisioningUpdates` 自动登记设备 / 建 profile)。包名 `net.commilitia.Commilitia-Drop`App Group `group.net.commilitia.Commilitia-Drop`。
> **实现状态(2026-06-27**I1/I2/I3/I4/I5/I6 的**代码**全部落地——发送端流式(R-iOS-4FileSource + 原生 Range,整文件不进 WebView 内存);后台续传(BGContinuedProcessingTask);APNs(后端 `internal/apns` ES256 + 原生注册经引擎桥);Share ExtensionApp Group 收件箱 + `commilitia-drop://share` 深链);控制中心两控件(**专用 broker 设备会话**,纯原生 REST,不与引擎抢 refresh 轮换)。模拟器验 + ultracode 多 agent 审查修讫(0 HIGH,修 2 MED + 7 LOW)。**I0 账号 / 证书 + 真机签名 / 真发推送 / 旁加载分发**仍账号门控——手册 `ios/CDrop/REALDEVICE.md`ASC API Key 自动 provisioning + `just ios-device` 全 CLI 装机,`-allowProvisioningUpdates` 自动登记设备 / 建 profile)。包名 `net.commilitia.Commilitia-Drop`App Group `group.net.commilitia.Commilitia-Drop`。
> **更新(2026-06-28**:① **P2P 数据面改原生 libwebrtc**(决策 F;完整依据 `desktop/NATIVE-TRANSFER.md` §7/§8)——已真机装机、用户反馈“传输总体良好”,真网吞吐互通为剩余闸;② **UX 改造**(发送预览直入 / 消息按设备会话 / 删除粒度,§10);③ **品牌与字体系统**logo + Orbit Gothic / Maple Mono / Fredoka,§11)。