Files
Commilitia-Drop/ios/PLAN.md
T
admin 10cf36ecee 鉴权并入 Auth Broker:委派设备会话统一模型 + 四端迁移
后端(委托 Auth Broker,路径 A):
- 删自建鉴权(OIDC exchange / 自签会话 / step-up / shortcut / web_sessions / accounts),cdrop 不再存任何凭证;鉴权中间件改读边缘注入的 X-Auth-Subject/Scope/Meta/Name/Roles 头(dev 旁路保留);Claims 加 Tier() / Guest()
- internal/brokerclient:mint / revoke(带 X-Broker-App)/ refresh / ListSessions(R1 列举),直连内网、吊销幂等

统一会话模型“委派设备会话”(Delegated Device Sessions):
- 每个客户端(浏览器 / 桌面 / 扫码设备)=一条带 meta(device_id) + label 的 broker 机器会话;Broker 作设备会话唯一注册表(R1 按用户+app 列举 + R2 按 (user,app,meta) 幂等铸造),cdrop 退化为薄覆盖层、不再自存权威会话表
- 新增代铸端点 POST /api/auth/device-session:凭边缘已验明的 X-Auth-Subject 委托 broker 铸 / 轮换设备会话(meta=device_id、按调用方 tier 防越权、sameOrigin CSRF、per-IP 限流);R2 幂等保证同一 device_id 重登原地轮换、不堆重复设备
- 会话列表=R1 权威 + 叠加 type(本地缓存)/ online(Hub presence,按设备名)/ current(meta 匹配本请求 X-Auth-Meta)+ 过滤 meta=""(device-authorize 引导会话残留);devices 表降级为 type/presence 薄缓存(非会话权威),device_id 主键、upsert 按 user 限定
- 吊销按 device_id → 缓存优先 / R1 兜底解析 sid → broker 吊销 + X-Broker-App;扫码登录保留三密钥编排,collect 改委托 broker 铸 + 落缓存行

Web 前端:
- 登录走 broker 全局 SSO 代跳(/api/auth/login 302);bootstrap 经 /api/me 注入身份后代铸设备会话(稳定 device_id 存 localStorage、Web Locks 跨 tab 串行防重复铸造);refresh 走 /api/auth/refresh
- 设备管理按 device_id;改名=同 device_id 重代铸(R2 原地轮换换 label、不产生重复行);登录页反应式守卫修登录回环
- 去 OIDC PKCE / step-up(删 oauth.callback / stepUp)

桌面客户端(Wails):
- loopback PKCE(RFC 8252)改指 broker 设备授权流(/device/authorize + /device/token)拿引导令牌,再代铸出带 meta 的托管设备会话——与浏览器同模型、同管理、同吊销;身份取自代铸响应(修“显示名显示为 UUID”);refresh 保留显示名;稳定 device_id 入桌面配置

iOS 客户端(arch A,原生 SwiftUI + 离屏无头 WebView 引擎 + 原生↔JS 桥):
- 引擎 / 文件管理 / 设备管理 / 应用图标 / 本地化(此前实现,随本次落入版本库)
- 鉴权=引擎自刷(boot 注入 refresh_token)+ broker 轮换经 sessionRotated 回报原生更新 Keychain;去 cookie 同步;Session 加 refreshToken / deviceId

实时 / 健壮性:
- presence 走 Hub union(设备表行 ∪ 表外实时连接,按名去重、live-only 标在线)
- Hub 通道 close 一律在写锁内、非阻塞 send 一律在读锁内,消除 close-vs-send 闭通道 send panic(revoke 每次 Kick 后该路径变热)

配置 / 删旧栈:
- config 改 broker 接入(CDROP_BROKER_* / CDROP_PUBLIC_URL / 按档 TTL),prod 强校验 broker 配置 + PUBLIC_URL(CSRF Origin 守卫不失效)
- 删 auth.go / selftoken.go / shortcut.go / jwks.go + 三表(web_sessions / accounts / shortcut_tokens)及验证链;.env.example / compose.snippet.yaml / Caddyfile.snippet 更新为 broker 模型(人机分流 + 公开端点放行 + X-Auth-Meta 透传)
- 测试全重写:QR / 会话含 mock broker(R1 列举 + R2 幂等);hub 加 close-vs-send 并发回归;config 加 prod 必填校验
2026-06-26 22:10:19 +08:00

249 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# cdrop iOS 客户端实施计划
> 原生 SwiftUI 界面(液态玻璃)· 离屏无头 WebView 复用传输引擎 · 控制中心剪贴板 + Share Extension + APNs
> 状态:**架构已定(2026-06-24 修订)**,账号无关部分即刻可开工;真机 / 推送 / 扩展实测硬等 Apple Developer 账号。
> 关联:根 `README.md`、`desktop/PLAN.md`、`internal/push`、`ios/PARITY.md`web↔iOS 同步检查单)。记忆 `cdrop-state` / `cdrop-deploy-pointers`。
---
## 0. 定位与关键决策(2026-06-24 修订)
定位变更:iOS 客户端**不再**是“web UI 装进 WebView”,而是“**原生 SwiftUI 界面 + 复用的无头传输引擎**”。起因——新增“服从 iOS 设计语言(液态玻璃)”硬要求,与“WebView 里跑 React UI”直接冲突:液态玻璃是 iOS 26 系统级材质,只有原生 SwiftUI 的 `.glassEffect` 拿得到,网页只能 CSS 仿。故 UI 转原生,但**保留引擎复用**。
### 决策 A(修订):架构 = 原生 SwiftUI 壳 + 离屏无头 WebView 引擎
取代 2026-06-18 的“WebView 复用 React UI”决策。
- 所有可见 UI 用**原生 SwiftUI**iOS 26 液态玻璃;旧版优雅回落,见 §7)。
- 文件传输的 **P2P + relay + 会话 + 信令逻辑 = 现有 `web/src` 的 JS 引擎本体**,跑在**离屏(无 UI)WKWebView** 里,原生经桥驱动。零重写、保住同内网 P2P、行为与 web 天然同构(上轮刚修的“按已交付字节算进度”“抽干再标完成”等自动一致)。
- **兜底方案 B**:若真机上无头 WKWebView 跑 WebRTC DataChannel 不稳,退“全原生 + relay-only 重写 + 放弃 P2P”。该验证留到真机可测时优先做(R-iOS-3)。
**桥方向与桌面相反(关键,易踩坑)**
- 桌面(Wails):React UI 在 WebView 顶层,调 DOWN 到 Go 拿原生能力。
- iOSarch A):SwiftUI 原生在顶层,调 DOWN 到离屏 WebView 的 JS 引擎跑传输。
- 即 iOS 的 WebView 是“**无头 worker**”,不是“UI 容器”。需要一个**独立的无头引擎入口**(不加载 React、只装传输模块 + RPC 桥),而非复用 React app 再隐藏 UI。
### 决策 B(修订):分发 = 非 App Store,但仍守苹果开发标准
- **必须付费 Apple Developer ProgramADP$99/年)**cdrop 四项能力——APNs、App Groups、Keychain 共享、Associated Domains——**免费 Personal Team 一律不支持**(账号策略,非 OS 封锁),且免费证书 7 天过期。故“等账号”=等**付费 ADP**。
- **分发走 Ad Hoc**(最贴近桌面 Dropbox 旁加载的等价物):100 台 / 年上限、设备 UDID 须预先登记、profile 年度更新。自用(自己几台设备)足够;比桌面“下载即用”多一道 UDID 登记摩擦——iOS 无更省的合规途径。
- 备选 **TestFlight**:体验最好(无需 UDID、可达万级),但**需经 Apple Beta 审核**、build 90 天过期——与“不上架”初衷部分相悖,留作扩面再议。
- **EnterpriseADEP $299/年)**:无设备上限,但仅限“内部员工”,对外分发违反协议、证书可被吊销——不取。
- **不过审 ≠ 不守标准**:仍遵守 HIG、液态玻璃规范、App Intents / WidgetKit 约定、隐私(权限说明 / just-in-time 请求)、entitlements 最小化。原 R-iOS-5Guideline 4.2 过审风险)在 Ad Hoc 下作废;若走 TestFlight 则 Beta 审核仍在。
### 决策 C(新):剪贴板 = 控制中心两控件,零推送
- 通道用**持久的 `/api/clipboard`**(REST、LWW、版本探针、作用域令牌可访问),不用 ephemeral 的 `/api/message`
- iOS 18 控制中心控件(`ControlWidget` + App Intent)提供“上 / 下”两件:**上** = 读 `UIPasteboard``PUT /api/clipboard`**下** = `GET` → 写 `UIPasteboard`。用户主动点、**零推送**(频繁剪贴板变动推送干扰极大)。
- 平台天花板:程序化**读**剪贴板弹一次系统横幅(写不弹);后台无法监听复制。故“后台自动同步”降级为“一键发送 / 一键拉取”。
- 这条链**纯原生 URLSession,不依赖无头 WebView**——与文件传输引擎彻底解耦,互不拖累。
### 决策 D(新):视图层分叉,先吃强约束、残余进检查单
接受 webReact)与 iOS(SwiftUI)视图层是两套实现(液态玻璃决定不可能像素一致)。一致性按两层管,详见 §5:
- **强约束同步**(机制保证、漂移不可能):① 传输行为靠 arch A 共享引擎;② i18n 文案单一源(iOS 读 web 那份 JSON);③ 品牌资产 + 主色单一源;④ 后端 API 即共享契约。
- **检查单**(机制管不住、人判定的残余):元素 / 屏 / 功能存在性对等、非引擎驱动的新流程、后端字段变更提醒、允许分叉登记。
- **明确不做**:设计令牌 codegen 管线(过度工程,且与液态玻璃系统材质 / 语义色 / SF Symbol 母语相冲)——间距 / 圆角 / 字阶归“允许分叉”,仅品牌主色 + logo 作共享源。
### 决策 E(新):权限策略 = 持久权限首启显式询问 + 缺失则降级或阻塞提示
- **持久型权限(本地网络、相机)在首次启动的引导中显式询问**:先用一屏说明用途、再触发系统弹窗——既满足“首启显式获取”,又不踩 HIG 的“无上下文裸弹窗”反模式。这类权限**一次授予长期持久**(iOS 18 有重装后状态不重置的已知 bug,需 UI 兜底)。
- **缺失 / 拒绝时按特性二选一**:① **有兜底则降级**——本地网络→回退中继并提示“仅中继、无法同内网直连”;通知→无后台推送但前台仍可用。② **无兜底则提示“无权限”并阻塞该特性**——相机→扫码不可用,提示 + 跳系统设置;阻塞的是**该特性**、非整个 app。
- 不拿任一权限当**整个 app** 的门槛(HIG 5.1.2)。详见 §9.2 逐权限映射。
---
## 1. 后端现状(决定 iOS 能做什么,2026-06-24 扫描)
| 能力 | 结论 | 对 iOS 的含义 |
|---|---|---|
| 文件传输 | 纯实时(P2P / 实时 relayrelay 是内存环 64MiB、2 分钟空闲释放),**零落盘暂存** | Share Extension 不靠服务端暂存;接收方须同时在线 |
| 剪贴板 `/api/clipboard` | 持久 LWW + REST,作用域令牌可访问 | 决策 C 的纯原生链路 |
| 消息 `/api/message` | 临时、不持久、不可轮询 | 文本同步走剪贴板通道,消息仅尽力而为实时 |
| 作用域令牌 | 仅 `clipboard` 一个 scope`rejectScoped` 挡死 `/transfer` | 发送须完整会话;Share Extension 用交接而非自带令牌 |
| 推送 | 仅 Web PushVAPID),无 APNs | APNs 须新建(§3 |
| 原生鉴权 | IdP bearer(RS256)/ 扫码自签会话;设备隐式登记(`X-Device-Name`) | 原生 app 可正常登录持会话 |
Share Extension 落法(据上):**抓文件 → 写 App Group 容器 → 深链唤起主程序**选设备发送(接收方须在线、大文件主程序稳跑、扩展不持完整令牌更安全)。
---
## 2. 桥协议(arch A 的接口契约,账号无关、可先冻结)
原生 ↔ 无头 JS 引擎的 RPC`WKScriptMessageHandler`JS→Swift+ `evaluateJavaScript`Swift→JS),类比 Wails 的 `EventsEmit` / bound methods,但**方向相反**(原生在顶层)。
- **平台判定**`isIOSShell()``web/src/net/ios.ts`)查 `window.webkit.messageHandlers.cdropEngine` 在场(类比 `isDesktop()``window.go.main.App`)。无头入口(`web/src/engine/main.ts`)据此装配桥。
- **注入**:原生在加载 bundle 前经 WKUserScript 注入 `window.__CDROP_BOOT__ = { session, device_name, api_base, device_type:"ios" }`store 在 import 时即水合(沿桌面同一注入范式);JS 永不持有长寿命密钥(refresh_token 只在原生 Keychain)。
- **原生 → 引擎命令**(原生 `evaluateJavaScript``window.__cdropEngineEvent(name, payload)`):`sendFile { target, url, name, size, type? }``cancelTransfer { sessionId }``switchToRelay { sessionId }``session { access_token, user }`(续期令牌)、`shutdown`
- **引擎 → 原生**:① 单向通知(`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。
---
## 3. 服务端 APNs 通道(账号无关的结构,`.p8` 到位再落代码)
新增 `internal/apns``internal/push` 并列,只换发送后端:
- **库**APNs 走 HTTP/2 + JWT`.p8` 的 ES256 签名),可用 `github.com/sideshow/apns2`,或自手写(与现有 go-jose 一致)。
- **配置**koanf`CDROP_APNS_*`,全可选,缺失即 iOS 推送惰性关闭):`APNS_KEY_PATH``APNS_KEY_ID``APNS_TEAM_ID``APNS_TOPIC`bundle id)、`APNS_ENV`
- **存储**`push_subscriptions``platform` 列,iOS 行存 APNs token。`SubscriptionID` 仍用 token 哈希保幂等 upsert。
- **端点**`POST /api/push/apns/register {device_token, locale}`(鉴权 + `X-Device-Name`,与 `/api/push/subscribe` 同组)。
- **触发**:在 `transfer:incoming` / `transfer:done|failed` / `message` 的“未投递”分支按 `platform` 分流。文案复用 `internal/push``localize`。**注意——剪贴板不触发推送**(决策 C 走控制中心拉取)。
代码结构账号无关,但真发 / 真测要 `.p8`,故与 iOS 客户端同期落(账号到位后),避免悬空实现。
---
## 4. 里程碑(修订)
| 里程碑 | 内容 | 账号依赖 |
|---|---|---|
| **I0** 账号与证书前置 | **付费 ADP($99/年,4 项能力全要)**;主 app + 各扩展 App ID(扩展 id 须主 app 前缀);开启 Push / App Groups / Keychain / Associated Domains;注册 App Group IDAPNs Auth Key `.p8`**仅一次下载**);Distribution 证书 + Ad Hoc Profile(登记设备 UDID | 硬等付费账号 |
| **I1** 原生骨架 + 无头引擎装配 | Xcode 工程(SwiftUI);离屏 WKWebView 载无头引擎入口;`isIOSShell` 注入桥 | 模拟器可跑 UI;引擎入口 web 侧账号无关 |
| **I2** 登录态 | `ASWebAuthenticationSession` PKCE / 扫码;refresh_token 存 Keychainsession 注入 JS | 模拟器可跑 |
| **I3** 传输(核心) | 无头引擎桥(send 自定义 scheme 流式 / receive 复用 `Begin`-`Append`-`Finalize`)+ 原生传输 UI(液态玻璃、含进度 / 速度 / 状态) | 真机硬等账号 |
| **I4** 剪贴板 | 控制中心两控件 + App Intents;纯原生 `/api/clipboard` 读写 + 版本探针 | 控件真机硬等账号;REST 链路可先写 |
| **I5** Share Extension | 抓文件 → App Group → 深链主程序选设备发送 | 真机硬等账号 |
| **I6** APNs | 服务端通道(§3)+ 原生注册;不含剪贴板 | `.p8` + 真机硬等账号 |
| **I7** 打磨与分发 | 后台窗口;图标 / 启动屏(复用品牌资产);旁加载分发 | 硬等账号 |
---
## 5. 一致性:强约束同步 vs 检查单(决策 D 展开)
**检查单是兜底,不是默认。** 先尽量用强约束(单一真源、两端机械消费、漂移不可能),管不住的残余才进检查单(见 `ios/PARITY.md`)。
### 强约束(机制保证)
1. **传输行为 → arch A 共享 JS 引擎。** 整套状态机 / 进度 / 完成 / 错误 / 协议无法漂移,零额外成本。
2. **i18n 文案 → 单一源。** iOS 原生侧薄 loader 读 web 那份 i18n JSON(随 bundle 或从服务端取),不写转换器、不做 `.xcstrings` 镜像。缺键即露。
3. **品牌资产 + 主色 → 单一源。** 资产沿用 Dropbox 那套(web+desktop 已接);品牌主色 + logo 作小共享常量。
4. **后端 API → 后端即共享契约。** 两端瘦客户端对齐同一 JSON 形状,不加 codegen(过度工程);字段变更的人工残余进检查单。
### 检查单(人判定的残余)
`ios/PARITY.md`:① 元素 / 屏 / 功能存在性对等;② 非引擎驱动的新流程 / 状态;③ 后端 API 字段变更提醒;④ 允许分叉登记。
### 明确不做
设计令牌 codegen 管线——过度工程,且与液态玻璃系统材质 / 语义色 / SF Symbol 母语相冲。间距 / 圆角 / 字阶归“允许分叉”。
---
## 6. 现在能做 vs 等账号(修订)
**账号无关、即刻可做**
- 本计划 + `ios/PARITY.md` 同步检查单(✔ 本轮)。
- web 侧**无头引擎入口**(✔ 已实现,账号无关):`net/ios.ts`(桥 + `isIOSShell()` + 接收封装 + `notifyNative`)、`engine/main.ts`(不加载 React 的独立入口:水合 store → `startHub` → 暴露 RPC + 订阅推事件)、`incomingSink.ts`(通用混合槽路由 iOS,内存上限 64MB/16MB 避 jetsam)、`engine.html` + `vite.config.ts` 多入口。`tsc` / `vite build` 双过、对现网 web / 桌面零影响。
- 桥协议契约冻结(§2,✔ 已与实现对齐)。
- i18n 单一源:现有 catalog 已是扁平 `{ key: string }` 结构(`src/i18n/locales/*.ts`),数据形态即可导出 JSON;原生侧薄 loader + 构建期 emit 待原生工程就位时同期落(避免向无消费方空导出)。
- 剪贴板 REST 链路(`/api/clipboard` 读写 + 版本探针)的数据流设计(控件实现等账号,协议可先定)。
- **iOS 原生 scaffold**(✔ 已实现,账号无关):`ios/CDrop/`xcodegen `project.yml`)——液态玻璃壳(TabView + `.tabBarMinimizeBehavior` + `.buttonStyle(.glassProminent)`+ 离屏引擎 `WKWebView` 宿主 + 桥 `EngineController`(对侧契约同 `net/ios.ts`+ `DownloadManager`(落沙盒)。**已编过 `iphonesimulator26.5`iOS 26 SDK,含液态玻璃)**——证 arch A Swift 侧成立。**启动 / 截图待 iOS 26 模拟器运行时(约 7GB,账号无关)下载**(`xcodebuild -downloadPlatform iOS`)。
- **明确不做**(避免悬空 / 过度工程):设计令牌 codegen;APNs 发送代码(`.p8` 到位再落)。(注:原“Xcode 工程等账号”已作废——**模拟器构建不需签名 / Provisioning**,工程已建且 compile-verified;只有真机签名 / APNs 真发 / 分发才等付费 ADP。)
**硬等付费 ADP 账号**:真机签名 / 跑、APNs 真发、Share Extension、控制中心控件真机、旁加载分发。
---
## 7. 风险与未决(修订)
- **R-iOS-1 后台挂起 / JS 全停**app 退后台即挂起、WKWebView JS(含 WebRTC 事件循环)停摆,DataChannel 无媒体轨后台不保活。文件传输本需前台(双方在线、用户看进度),可接受;用户主动触发的传输用 iOS 26 `BGContinuedProcessingTask` 续跑(带系统进度 UI)。剪贴板走控制中心 App Intent,不依赖常驻 WebView。
- **R-iOS-2 剪贴板后台限制**:读弹横幅、后台不可监听 → 控制中心手动两控件已据此设计;app 内读用 `UIPasteControl`、探测用 `hasStrings`(均不弹横幅)。
- **R-iOS-3 无头 WebRTC 真机稳定性 + 安全上下文**:arch A 最大未验证点——① `RTCPeerConnection` 须在安全 origin;② `BGContinuedProcessingTask` 能否让 WebView JS 不被挂起待验。**2026-06-24 模拟器已部分证实**iOS 26 WKWebView 从 `http://127.0.0.1``isSecureContext=true`,loopback = 潜在可信源)跑 `RTCPeerConnection` + DataChannel + `createOffer` **RTC:ok**、STUN srflx 候选正常、原生↔JS 桥往返正常 → **arch A 核心成立、不转 B**。注:安全源不止 https,loopback 亦可(可选应用内本地 https/loopback 服务内嵌 bundle,离线可用)。**余真机验**:host / 同内网直连候选 + 本地网络权限(R-iOS-6)、后台挂起续传(BGContinuedProcessingTask)。
- **R-iOS-4 字节桥**:接收侧复用桌面 `Begin`/`Append`/`Finalize`(base64 分批、有界)已验证;发送侧自定义 scheme(`WKURLSchemeHandler`)流式喂 JS 为新实现点。
- **R-iOS-5**(原 App Store 过审)在 Ad Hoc 旁加载下作废;若改走 TestFlight 则 Beta 审核仍在。
- **R-iOS-6 本地网络权限(同内网 P2P 硬墙)**WebRTC host 候选 / mDNS 触发 `NSLocalNetworkUsageDescription`(须配 `NSBonjourServices`),拒绝即只剩中继候选 → 回退 relay(cdrop 已有兜底);mDNS 失败不可区分“无服务”,UI 须降级。**不可拿它当核心功能门槛**。
- **R-iOS-7 付费账号 + 分发摩擦**:4 项能力 + 分发全需付费 ADPAd Hoc 100 台 + UDID 预登记是旁加载硬约束——比桌面 Dropbox“下载即用”多一道摩擦。
---
## 8. iOS 设计规范要点(HIG · 液态玻璃)
抓取自 Apple HIG / WWDC252026-06-24),只取对 cdrop 界面有约束力的重点。
### 8.1 液态玻璃(材质用法)
- **两层模型**:玻璃只用于**功能 / 导航层**Tab bar / Nav bar / Toolbar / 浮动控件 / sheet 边框);**内容层禁用**——列表、设备卡、**传输卡属内容层,不加玻璃**。
-**iOS 26 SDK 编译,Tab bar / Nav bar / Toolbar / sheet 自动玻璃化**;同时**移除所有自定义 bar 背景 / 装饰色**(否则破坏系统玻璃)。
- 自定义悬浮控件(如传输卡上的取消 / 暂停 pill)才手动 `.glassEffect(.regular.interactive())`,多块玻璃须放进 `GlassEffectContainer` 共享采样;主操作用 `.buttonStyle(.glassProminent)`
- **禁忌**:玻璃叠玻璃、滚动内容区铺玻璃、大面积内容玻璃背景、非主操作滥用 tint。
- Tab bar 滚动收起 `.tabBarMinimizeBehavior(.onScrollDown)`;全局传输状态可置 `.tabViewBottomAccessory`(类 Apple Music 迷你条)。圆角走同心 `.rect(cornerRadius: .containerConcentric)`sheet / popover 系统自动同心。
### 8.2 基础(色彩 / 字体 / 图标 / 布局)
- **色彩**:用**语义色**`label` / `secondaryLabel``systemBackground` 层级、`systemFill``separator`),**不硬编码 hex**——暗色 / 对比度 / vibrancy 免费。**品牌主色以 Asset Catalog 的 Color Set(含 Dark 变体)接入**`Color("BrandPrimary")`)——这正是强约束③(§5)在 iOS 侧的落地形态。
- **字体 / Dynamic Type**:系统 text stylesBody 17 / Headline / Title…)+ **必须支持 Dynamic Type**(正文 / 标题 / 主按钮可缩放)。与“字体归允许分叉”不矛盾——分叉的是**字体选择**,Dynamic Type 支持是**硬要求**。
- **SF Symbols**:优先系统符号(自动与文字对齐 / 字重匹配),四种渲染模式(mono / hierarchical / palette / multicolor);品牌专属才自定义 symbol(须注解四模式以获 Dynamic Type / 无障碍)。
- **布局**:内容 / 控件留安全区,**最小命中区 44×44pt**,对比度 **4.5:1**(正文)/ **3:1**(大字 / 非文字),不硬编码宽度。
### 8.3 控件 / App Intents(剪贴板两控件落地)
- 剪贴板“上 / 下”用 **`ControlWidgetButton`**(一次性动作);**SF Symbol 必备**(锁屏 / 操作按钮只显示图标,须独立达意)。跨设备状态靠**推送 reload**(`ControlCenter.reloadControls`)、**不轮询**;控件内**不发网络请求**,经 App Group 容器读主 app 写入。
- **App Intent**:动词+宾语命名、`title` 编译期常量本地化串、**默认后台执行**(返回 dialog / snippet、不开 app);iOS 26 **Interactive Snippets** 可在不开 app 下做“选设备”二级操作。
### 8.4 分享(Share Extension
- 扩展只做轻:内容预览 + 选设备 + 验证 → 写 **App Group 容器** + 交接主 app;**不在扩展内跑传输引擎**(内存 ~120MB,见 §9.1)。宽度系统固定不可改;完成即“已提交”退出,不等传输完成。
### 8.5 通知(APNs
- 文案具体(“文件 X 已接收(3.2MB)”);中断级别用 **`.active`**(传输完成 / 失败),**不滥用 `.timeSensitive`**`threadIdentifier` 按“完成 / 失败 / 消息”分组;**首个有价值操作后**再 just-in-time 请求权限,按类别细分开关。**剪贴板不推送**(决策 C)。
### 8.6 无障碍(玻璃语境)
- Reduce Transparency / Increase Contrast / Reduce Motion:系统对 Tab / Nav 自动回落;**自定义玻璃控件须自行提供不透明回落 + 关动效**。传输进度动画检测 `accessibilityReduceMotion` 降级为渐隐 / 变色。VoiceOver:图标按钮 `accessibilityLabel` 用动词(“发送文件”而非“箭头向上”)。
### 8.7 App 图标(iOS 26
-**Icon Composer** 做分层图标(logo 前景 + 品牌色渐变背景),系统材质自动出高光 / 折射 / 阴影——源稿**不要**烘焙圆角 / 阴影 / 斜面。须验明 / 暗 / clear / tinted 各变体(暗背景下 logo 填充色可辨)。接品牌资产源(强约束③)。
---
## 9. 开发禁区 / 平台约束(硬性 vs 审核 / 账号)
分级:**【OS 硬】**=系统强制、绕不过;**【账号】**=付费 / 账号策略;**【审核】**=仅 App Store / TestFlight 审核约束(Ad Hoc 旁加载不触发)。
### 9.1 运行 / 生命周期
- **【OS 硬】后台一律挂起、JS 全停**:app 退后台即被挂起,WKWebView 的 JS(含 WebRTC 事件循环)停摆,DataChannel(无媒体轨)后台不保活 → 传输**必须前台发起**;用户主动触发的传输用 iOS 26 **`BGContinuedProcessingTask`** 续跑(带系统进度 UI,能否让 WebView JS 不被挂起须真机验,R-iOS-3)。
- **【OS 硬】无后台常驻监听**:剪贴板 / 消息**不能**后台轮询监听 → 决策 C 的控制中心手动触发正据此。
- **【OS 硬】自定义 scheme = 非安全上下文 + OPFS 单文件 ~10MB**:故引擎从 https 安全源加载(§2)、大文件**不走 OPFS**、经原生桥 `Begin`/`Append`/`Finalize` 落沙盒(接收侧已是此设计)。
- **【OS 硬】Share Extension ~120MB jetsam**:扩展内**装不下** WKWebView + WebRTC 引擎 → 只做交接(§8.4)。
### 9.2 权限 / 隐私 / 网络(落实决策 E 的逐权限映射)
- **【OS 硬】本地网络权限(持久)**:同内网 P2P 直连(WebRTC host 候选 / mDNS)触发 `NSLocalNetworkUsageDescription`**须配 `NSBonjourServices`**;一次授予长期持久 → **首启引导显式询问**(决策 E)。缺失静默失败、拒绝则只剩中继候选 → **降级回退 relay 并提示“仅中继”**(cdrop 已有兜底;mDNS 失败不可区分“无服务”,UI 须降级)。
- **【OS 硬】相机权限(持久,扫码登录)**:`NSCameraUsageDescription` 缺失即崩;首启 / 首次扫码时显式询问。**无兜底 → 拒绝则提示“无权限”并阻塞“扫码”特性**(跳系统设置);登录可走 OIDC 等其他途径,阻塞的是扫码、非整个 app。
- **【OS 硬】Info.plist 用途说明缺失即崩**:相机、相册键(若选图)必声明;剪贴板**无** iOS plist key(横幅系统自动、关不掉)。
- **【OS 硬】ATS 强制 HTTPS / TLS1.2+**cdrop 已全 https,无需例外。
- **【OS 硬】剪贴板**:后台不可读、读内容弹横幅(关不掉)、写不弹、`hasStrings` 探测不弹、`UIPasteControl` 读不弹 → “上”控件接受一次横幅、app 内读用 `UIPasteControl`、“下”控件写无感。
- **【审核】隐私清单 `PrivacyInfo.xcprivacy` + Required Reason API**UserDefaults `CA92.1` 等):上架 / TestFlight 才强制;Ad Hoc 非硬性,但第三方 WebRTC SDK 自带清单仍建议补。
### 9.3 能力 / 扩展 / 签名 / 分发
- **【账号】4 项能力全需付费 ADP**APNs / App Groups / Keychain 共享 / Associated Domains——免费 Personal Team 一律不支持(决策 B / I0)。
- **【OS 硬】扩展归属**Share Extension / ControlWidget 的 bundle id 须**主 app 前缀**、同 Team 签名;与主 app 共享数据**唯一合法途径 = App Group 容器**。
- **【OS 硬 + 账号】Ad Hoc 100 台 / 年 + UDID 预登记**:旁加载的硬上限与摩擦(决策 B)。
- **【OS 硬】APNs 环境隔离**dev profilesandbox、ad-hoc / 分发=productiontoken 不互通。
---
## 附:与桌面端策略对照(修订)
| 维度 | 桌面(Wails | iOSarch A |
|---|---|---|
| UI | 复用 `web/src` | 原生 SwiftUI(分叉,PARITY 管) |
| 引擎 | Go 原生 | 复用 `web/src` JS(无头) |
| 桥方向 | JS-UI 顶层 → Go | SwiftUI 顶层 → 无头 JS |
| refresh_token | Go keyring | iOS Keychain |
| 登录 | loopback PKCE | `ASWebAuthenticationSession` / 扫码 |
| 后台通知 | Go 常驻进程 + 原生 API | 服务端 APNs |
| 剪贴板 | Go Monitor 实时双向 | 控制中心手动两控件(纯原生 REST) |
| 平台判定 | `isDesktop()``window.runtime` | `isIOSShell()``window.webkit.messageHandlers` |
| 分发 | Dropbox 旁加载 | 旁加载(非 App Store |