feat(ios,android): P2 wave, git panel, token UX, per-host WS token, docs
App layer, four sequential slices (a shared .xcodeproj means adding files regenerates it, so these could not run in parallel): - token UX end to end: pairing prompts for a token when a host 401s, POST /auth validates it, and 204-without-Set-Cookie is correctly read as "this server has auth disabled" rather than "authenticated". A host paired before the token was turned on recovers by re-pairing in place. Remove-host now exists and finally gives PushRegistrar.handleHostRemoved a caller. - project git panel + worktree lifecycle (T-iOS-32) + claude --resume history — the parity gap with Android and the web front end. - terminal search (T-iOS-33) and voice PTT (T-iOS-31) with an epoch guard so a session switch between dictation and confirm cannot inject into the wrong session. - theme + Dynamic Type (T-iOS-34) and web ?join= interop (T-iOS-35). RootView no longer hard-locks .preferredColorScheme(.dark). Also unpins SwiftTerm to 1.15.0 by dropping the local hasActiveSelection that collided with the upstream one, verified green from a fresh derivedDataPath. Includes the two HIGH fixes the security review found: - iOS resolved the WS token host-independently, so a token-gated host sitting next to an open one could never open a terminal and no on-screen remedy could fix it. Now one transport per host; cross-host leakage is structurally impossible since both read paths return only that host's own value. - Android reported the host's own git-credential 401 (git-ops.ts:108, "Push authentication required on the host.") as "your access token is wrong", because a blanket 401 mapping ran ahead of the per-route one. Git-write routes are now ROUTE_DEFINED and keep the server's message. And the doc sync: README/ios README no longer claim the client is unmerged on feat/ios-client, the Clients section finally lists Android, and the plan checkboxes reflect what is actually built. iOS 534 app tests + 452 package tests; Android 687 tests.
This commit is contained in:
@@ -2,7 +2,10 @@
|
||||
|
||||
> 落地方案文档。目标:给 web-terminal 做一个 **iPhone 原生 App**,把 vibe-coding 的"走开—被叫回—两次手势处理完"闭环装进口袋。
|
||||
> 拓扑/框架选型:**Phased-Native —— SwiftUI + SwiftTerm,4 个纯 SwiftPM 包 + 薄 App 胶水;前台会话单条活 WS,其余 HTTP 轮询;服务器零改动(P0 零触点;P1 仅声明的附加触点,见 §0.3)**。
|
||||
> 状态:**规划中(2026-07-04,未开工)**。
|
||||
> 状态:**已交付并合入 `develop`**(P0 + P1 + P2 全部落地;`feat/ios-client` 早已 merge 进 `develop`,
|
||||
> `git merge-base --is-ancestor feat/ios-client develop` 为真,勿再按"分支未合"叙述)。
|
||||
> 逐任务状态见 **§7 开头的状态总表**(2026-07-30 由 ios-completion 收尾波按源码核对重建);
|
||||
> 真机 / 付费 Apple 账号相关项一律 **DEFERRED** 并在表中标明。
|
||||
> 本文是「怎么做」的蓝图,配合 [TECH_DOC.md](./TECH_DOC.md)(why)+ [ARCHITECTURE.md](./ARCHITECTURE.md)(how);桌面版先例见 [DESKTOP_PLAN.md](./DESKTOP_PLAN.md);远程访问/中继演进见 [PLAN_RELAY_INDEX.md](./PLAN_RELAY_INDEX.md)。
|
||||
> 完成情况记录在 [PROGRESS_LOG.md](./PROGRESS_LOG.md)。工作流约束见 [CLAUDE.md](../CLAUDE.md)(查 PLAN → 做子任务(TDD) → 验证 → 更新 LOG)。
|
||||
> **G1 日志铁律**:`PROGRESS_LOG.md` 由 **orchestrator 独写**,不在任何任务的 `Owns:` 里;被派的 subagent **不写 LOG**,而是在最终返回消息末尾附上**可直接粘贴的日志条目**(状态 / 改动文件与函数 / 验证命令+结果 / 决策与偏差 / 阻塞 / 下一步),由主会话统一追加。
|
||||
@@ -62,6 +65,11 @@
|
||||
|
||||
除此之外**服务器 byte-for-byte 零改动**。若实施中发现 server 侧缺陷,修复归属对应模块(route/session 文件的 owner),按 CLAUDE.md 记 `PROGRESS_LOG.md`——iOS 任务不越界改 server。
|
||||
|
||||
> **补记(ios-completion 收尾波)**:客户端另外开始消费**早已存在**的两处服务器能力,**均无新触点**——
|
||||
> ① 访问令牌门(`POST /auth` + `webterm_auth` cookie,w5-access-token,真源 `src/http/auth.ts`);
|
||||
> ② 项目 git 面板/worktree 的 13 个 `/projects*` 路由(w4/w6,真源 `src/http/projects.ts` + `src/server.ts:507-1320`)。
|
||||
> 二者都是"服务器先有、iOS 后接",故 §0.3 的"P0 零触点 / P1 两触点"结论不变。
|
||||
|
||||
---
|
||||
|
||||
## 1. 整体架构 / 进程模型
|
||||
@@ -156,6 +164,11 @@ web-terminal/
|
||||
└── LiveServerTests.swift
|
||||
```
|
||||
|
||||
> **树的现状差异(2026-07-30 核对,不改本计划的设计意图)**:`Packages/` 下现有 **6** 个包——本文的 4 个 gated 包
|
||||
> + `TestSupport` + **`ClientTLS`**(设备客户端证书/mTLS,由 [PLAN_NATIVE_TUNNEL.md](./PLAN_NATIVE_TUNNEL.md) 引入,
|
||||
> 不属于本计划)。`App/WebTerm/` 也比本文的示意树多出 `DesignSystem/`、`Wiring/`、`Push/` 三个目录
|
||||
> (分别来自 UX 打磨、iPad 适配/接线、P1 推送)。依赖方向仍严格单向向下,`WireProtocol` 仍是唯一冻结契约。
|
||||
|
||||
**构建管线**(本地与 CI 同路径):
|
||||
1. `brew install xcodegen && cd ios && xcodegen generate` → `WebTerm.xcodeproj`(不入库)。
|
||||
2. 各包独立测试:`swift test --package-path ios/Packages/<Pkg>`(纯 mac 侧,无模拟器,秒级)。
|
||||
@@ -420,13 +433,19 @@ NSCameraUsageDescription: "扫描 web 终端的配对二维码" # P0 必需:T-
|
||||
```
|
||||
|
||||
> P2 前置:T-iOS-31(语音 PTT)开工前需另加 `NSMicrophoneUsageDescription` + `NSSpeechRecognitionUsageDescription`(属于该任务的前置,不属于 P0)。
|
||||
> **已落地**(ios-completion 收尾波 A1,`project.yml` 是单一 owner,故一次性加齐):两条 usage description +
|
||||
> `UIBackgroundModes: [remote-notification]`(背景**模式**不是 capability,免费 team 不受限;受限的是
|
||||
> `aps-environment` **entitlement**,故它走 `WEBTERM_PUSH_ENTITLEMENTS` 开关)。**产物层复核仍归 T-iOS-19,尚未做。**
|
||||
|
||||
- MagicDNS 名(`*.ts.net`)是 FQDN → ATS 全额适用 → 用 100.x IP、加例外域,或 `tailscale serve`(https/wss,最优)。
|
||||
- Local Network 弹窗被拒 → 连接报 POSIX "Network is down";映射到 `PairingError.localNetworkDenied` 并引导去 设置→隐私→本地网络(iOS 18 有需重启的已知 bug,话术里写明)。
|
||||
|
||||
### 5.3 凭据与本地存储
|
||||
|
||||
- **Keychain**:host 列表(含未来 authMaterial 占位)——`kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`,不进 iCloud 同步。
|
||||
- **Keychain**:host 列表 **+ 每主机访问令牌**(`WEBTERM_TOKEN`,原"未来 authMaterial 占位"已由 ios-completion
|
||||
收尾波填实)——`kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`、**无** `kSecAttrSynchronizable`(不进 iCloud 同步、
|
||||
不随备份离机,`SecItemShim.swift:77-86`)。`AccessToken` 类型在边界校验字符集/长度,`description`/`debugDescription`/
|
||||
`customMirror` 全部脱敏,且**故意不 `Codable`** —— 插值、`dump()`、反射式崩溃上报都拿不到它;令牌绝不进日志、绝不进 URL query。
|
||||
- **UserDefaults**:仅非机密(per-host lastSessionId、UI prefs)。
|
||||
- `/hook/decision` 的 capability `token` **只经 push payload 到达、用后即弃**(服务器侧本就单次有效+过期,src/server.ts:503-525;限频 10/min/IP);App 端绝不落盘。
|
||||
- App 内无任何硬编码 host/密钥;首个 host 必经配对流程。
|
||||
@@ -490,15 +509,95 @@ W5 验收(report-only, 并行)
|
||||
|
||||
## 7. 任务清单
|
||||
|
||||
> 状态图例:`[ ]` TODO · `[~]` 进行中 · `[x]` 完成 · `[!]` 受阻。ID 稳定,永不重编号。
|
||||
> 状态图例:`[ ]` TODO · `[~]` **部分完成**(下一行必写缺口)· `[x]` 完成 · `[!]` 受阻。ID 稳定,永不重编号。
|
||||
> **`[x]` 的判据(本文统一约定)**:代码+测试已交付,且**本环境可机器验证的部分全绿**;需要真机 / 付费 Apple 账号
|
||||
> 才能做的验收项,在该行标 **DEFERRED** 而**不**把任务降级为 `[~]`(否则每条都是"部分",完成度又读不出来)。
|
||||
> `[~]` 只留给**在本环境本可做却没做**的缺口,或本身就是真机走查的任务。
|
||||
> 每个任务自带 TDD(测试与实现同 agent 同文件组)。测试框架:Swift Testing(`@Test`/`#expect`);XCTest 仅 XCUITest。
|
||||
> 覆盖率验证命令(各包):`swift test --package-path ios/Packages/<Pkg> --enable-code-coverage` + `xcrun llvm-cov report`(阈值 80%,见 §9)。
|
||||
> 覆盖率验证命令(各包):`swift test --package-path ios/Packages/<Pkg> --enable-code-coverage` + `xcrun llvm-cov report`(阈值 80%,见 §9);
|
||||
> 实际 CI 用的是修正口径脚本 `ios/IntegrationTests/scripts/coverage-gate.sh <Pkg>`。
|
||||
>
|
||||
> **权威状态在下面这张总表**(2026-07-30 ios-completion 收尾波按**源码**逐项核对重建)。
|
||||
> 各任务标题上的方框与总表一致;**Steps 里的 `[ ]` 是原始规格清单,不是状态**——逐条执行记录在
|
||||
> `PROGRESS_LOG.md` 对应条目里,不在本文重复勾选(此前"任务已交付但满屏 `[ ]`"正是完成度读不出来的原因)。
|
||||
|
||||
### 7.0 状态总表(逐项对源码核对)
|
||||
|
||||
| ID | 状态 | 证据(文件 / 测试)与缺口 |
|
||||
|---|---|---|
|
||||
| T-iOS-1 脚手架 | `[x]` | `ios/project.yml`、`ios/.gitignore`、6 个 `Package.swift`、`.github/workflows/ios.yml` |
|
||||
| T-iOS-2 Day-1 双 spike | `[x]` | 四条自动化断言**已常驻化**:`IntegrationTests/OriginGuardTests.swift`(3) + `ReplayTests.swift`(2,含 ESC/C0 对抗)。Owns 的临时文件 `OriginSpikeTests`/`SpikeTerminalScreen` 已按计划吸收/删除(树中不存在)。**DEFERRED**:真机键盘/IME/选择 smoke |
|
||||
| T-iOS-3 WireProtocol 契约 | `[x]` | `Packages/WireProtocol/Sources` 10 文件 + **59** `@Test` |
|
||||
| T-iOS-4 TestSupport | `[x]` | `FakeTransport`/`FakeClock`/`FakeHTTPTransport` + 3 `@Test` |
|
||||
| T-iOS-5 Reconnect+Ping | `[x]` | `SessionCore/{ReconnectMachine,PingScheduler}.swift` + 对应 Tests |
|
||||
| T-iOS-6 Gate+Digest | `[x]` | `SessionCore/{GateState,AwayDigest}.swift` + 对应 Tests |
|
||||
| T-iOS-7 HostRegistry | `[x]` | 包内 8 源文件(含 `SecItemShim`/`InMemoryHostStore`)+ **73** `@Test`;覆盖率 92.49%(commit `850531f`) |
|
||||
| T-iOS-8 APIClient + 探针 | `[x]` | `PairingProbe`/`Endpoints`/`Models` 等 + 包内共 **125** `@Test`;覆盖率 92.22% |
|
||||
| T-iOS-9 URLSessionTermTransport | `[x]` | `SessionCore/URLSessionTermTransport.swift` + Tests(含 `ScriptedWSServer`) |
|
||||
| T-iOS-10 SessionEngine | `[x]` | `SessionCore/{SessionEngine,SessionEvent}.swift` + `SessionEngineTests`;包共 **108** `@Test`,覆盖率 96.74% |
|
||||
| T-iOS-11 Terminal+KeyBar | `[x]` | `Screens/TerminalScreen.swift`、`Components/{KeyBar,ReconnectBanner}.swift`、`SessionCore/KeyByteMap.swift` + `KeyBarTests`/`TerminalViewModelTests` |
|
||||
| T-iOS-12 Pairing | `[x]` | `Screens/PairingScreen.swift`+`PairingCopy.swift`、`ViewModels/PairingViewModel.swift` + `PairingViewModelTests`(本波再加令牌态,见 §7.1) |
|
||||
| T-iOS-13 SessionList | `[x]` | `Screens/SessionListScreen.swift`、`Components/TelemetryChips.swift`、`ViewModels/SessionListViewModel.swift` + Tests |
|
||||
| T-iOS-14 Gate/Digest UI | `[x]` | `Components/{GateBanner,PlanGateSheet,AwayDigestView}.swift`、`ViewModels/GateViewModel.swift` + `GateViewModelTests` |
|
||||
| T-iOS-15 App 接线+生命周期 | `[x]` | `Wiring/{RootView,AppCoordinator,PrivacyShade,ColdStartPolicy,TerminalContainerView}.swift` + `PrivacyShadeTests`/`ColdStartPolicyTests` |
|
||||
| T-iOS-16 集成 CI | `[x]` | `IntegrationTests/**` **10** `@Test` + `scripts/coverage-gate.sh` + `ios.yml` 六个 job(含 iPad 单测腿/iPad UI 腿/iOS-17 底线腿)。**未核实**:GH Actions 平台侧的运行结果(本环境 `gh` 未登录) |
|
||||
| T-iOS-17 ntfy 桥验证+文档 | `[x]` | `ios/README.md` ntfy 章节(逐条引 `setup-hooks.mjs` 行号,只读验证,未动用户 hook 配置)。**DEFERRED**:手机端到端 |
|
||||
| T-iOS-18 F 走查(真机) | `[~]` | 机器可执行项已执行并指认测试名(P0 收官条目)。**缺口**:F-iOS 的真机项(QR 扫码/IME/震动/切换器遮罩目检/ntfy 端到端)仍 DEFERRED,手工清单在 LOG |
|
||||
| T-iOS-19 安全核对 | `[~]` | P0 面已逐条核(Origin 单点、G/RO 分界、五段 CIDR + 零 ArbitraryLoads、Keychain 属性)。**缺口**:P2 新增的 `NSMicrophoneUsageDescription`/`NSSpeechRecognitionUsageDescription` 尚未在**产物层**复核;release ipa 层核对仍未做(免费 team 无分发通道) |
|
||||
| T-iOS-20 server APNs | `[x]` | `src/push/apns.ts` + `test/push-apns.test.ts`(含本地 h2c 假 APNs 的双 e2e;具体测试数以 `npm test` 输出为准,LOG 记 65)。**DEFERRED**:真机端到端需付费账号 |
|
||||
| T-iOS-37 server lastOutputAt | `[x]` | `src/types.ts:315` 可选字段 + `src/session/manager.ts:197` 映射 + 测试 |
|
||||
| T-iOS-38 APIClient P1 契约 | `[x]` | `APIClient/{ApnsToken,Projects,Prefs}.swift` + `ApnsTokenTests`/`ProjectsTests`/`PrefsRoundTripTests` |
|
||||
| T-iOS-21 PushRegistrar + 锁屏 | `[x]` | `Push/{PushRegistrar,NotificationActionHandler}.swift` + `PushRegistrarTests`/`NotificationActionHandlerTests`/`NotificationActionParseTests`。**DEFERRED**:真机锁屏 Allow + Face ID;`aps-environment` 现为 env 开关(免费 team 开不了,见 `ios/README.md`) |
|
||||
| T-iOS-22 DeepLinkRouter | `[x]` | `DeepLinkRouter.swift` + `DeepLinkRouterTests` |
|
||||
| T-iOS-23 多会话切换器 | `[x]` | `SessionCore/{UnreadLedger,TitleSanitizer}.swift` + `Wiring/UnreadWatermarkStore.swift` + `SessionSwitcherTests`/`UnreadLedgerTests`/`TitleSanitizerTests` |
|
||||
| T-iOS-24 Timeline sheet | `[x]` | `Screens/TimelineSheet.swift`、`ViewModels/TimelineViewModel.swift` + `TimelineSheetTests` |
|
||||
| T-iOS-25 Quick-reply | `[x]` | `Components/{QuickReply,QuickReplyStore}.swift` + `QuickReplyTests` |
|
||||
| T-iOS-26 Projects | `[x]` | `Screens/{ProjectsScreen,ProjectDetailScreen}.swift`、`ViewModels/{ProjectsViewModel,ProjectDetailViewModel,ProjectGrouping}.swift` + 3 组 Tests |
|
||||
| T-iOS-27 Diff 查看器 | `[x]` | `Screens/DiffScreen.swift`、`ViewModels/DiffViewModel.swift`、`DiffFetcher.swift` + `DiffViewModelTests`/`DiffFetcherTests` |
|
||||
| T-iOS-28 会话缩略图 | `[x]` | `Components/{SessionThumbnail,SessionThumbnailRenderer}.swift` + `SessionThumbnailTests` |
|
||||
| T-iOS-29 new-in-cwd + 退出清理 | `[x]` | `TerminalScreen`/`TerminalContainerView` 增量 + `NewSessionInCwdTests` |
|
||||
| T-iOS-30 P1 验收+安全复核 | `[x]` | report-only 双 PASS 零 findings(LOG 条目)。**DEFERRED**:真机锁屏/unread 目视等,手工清单在 LOG |
|
||||
| T-iOS-31 语音 PTT | `[x]` | `Components/{VoicePTT,VoicePTTBanner,SpeechDictation}.swift` + `KeyBar` 🎤 键 + `VoicePTTTests` **40** `@Test`。**DEFERRED**:真机口述→确认→注入 |
|
||||
| T-iOS-32 Worktree + `--resume` 历史 | `[~]` | `Screens/{WorktreeSheet,ResumeHistorySheet}.swift`、`ViewModels/{WorktreeViewModel,ResumeHistoryViewModel}.swift` + `WorktreeViewModelTests`(22)/`ResumeHistoryViewModelTests`(12)/`ProjectResumeLaunchTests`(7)。**缺口**:Accept 的"端到端一次"(对真服务器真建/真删 worktree)无自动化腿、本环境也未手工跑 |
|
||||
| T-iOS-33 终端内搜索 | `[x]` | `Components/TerminalSearchBar.swift` + `TerminalScreen` 绑定 SwiftTerm 搜索 API + `TerminalSearchTests` **18**(含 2 条真 view 高亮断言) |
|
||||
| T-iOS-34 主题 + Dynamic Type | `[~]` | `DesignSystem/{AppTheme,TerminalPalette}.swift`、`Screens/SettingsScreen.swift`、`Tokens/Typography` 浅色档 + `AppThemeTests`(24)/`DynamicTypeLayoutTests`(8)。**缺口(已实测、未修)**:AX5 下键栏需 ~108.24pt 而 `KeyBarMetrics.barHeight` 固定 52pt → 键帽裁切,以 `withKnownIssue` 钉住(`KeyBar.swift` 不在该任务 Owns) |
|
||||
| T-iOS-35 web `?join=` 互通 | `[x]` | `DeepLinkRouter.swift` 的 `.joinShared` 分支 + `DeepLinkJoinTests` **19**(含改写后的既有拒绝用例) |
|
||||
| T-iOS-36 P2 验收 | `[~]` | 本波 Wave D 的验收 agent 正在执行;**结果与真实数字以 `PROGRESS_LOG.md` 的该条目为准**,本文不预写结论 |
|
||||
|
||||
**说明**:上表的"测试数"是 `@Test` **声明**静态计数(`grep -rh '@Test'`),与 commit `850531f`/`a5fa843` 里实跑数字一致;
|
||||
参数化用例实跑会更多。App bundle 侧共 **532** 条 `@Test` 声明。
|
||||
|
||||
### 7.1 ios-completion 收尾波(2026-07-29/30)新增能力 —— 无既有任务 ID
|
||||
|
||||
审计出的缺口用一波收尾波补齐,这些交付**不属于**上表任何 ID(不新编 `T-iOS-*` 号,避免与冻结 ID 冲突):
|
||||
|
||||
- **访问令牌(`WEBTERM_TOKEN`)两端接入**:iOS(`APIClient/AccessToken.swift`、`HostRegistry/AccessToken.swift`、
|
||||
`SessionCore/AuthCookie.swift`、`Wiring/URLSessionHTTPTransport.swift`、配对页令牌态)与 Android
|
||||
(`AuthCookie`/`AccessTokenStore`/OkHttp 侧)走**同一冻结契约**(`docs/plans/ios-completion.md` §1.1):
|
||||
手写 `Cookie: webterm_auth=<t>`、`POST /auth` 四态、Keychain/Keystore 存储、WS 401 = 终态不进退避环。
|
||||
服务端真源 `src/http/auth.ts` / `src/server.ts:394-420`。**诚实边界照抄服务端注释**:抬高门槛≠替代 TLS/Tailscale。
|
||||
- **项目 git 面板 + worktree 生命周期(iOS)**:`Screens/{GitPanelScreen,GitPanelViews}.swift`、
|
||||
`ViewModels/{GitPanelPresentation,GitPanelViewModel}.swift` + `GitPanelPresentationTests`(19)/`GitPanelViewModelTests`(15);
|
||||
消费的 13 个 `/projects*` 端点**逐条对齐 `src/server.ts` 实现**(core 核对:`/projects/log`、`/projects/pr`、
|
||||
`/projects/worktree/state`、`git/{stage,commit,push,fetch}`、`worktree` 建/删/prune、`GET /sessions`),
|
||||
与 Android 参考实现无不一致。
|
||||
- **主机移除通路**:`PairingViewModel` 的已配对主机管理 + `PushRegistrar.handleHostRemoved` 从死钩子变成真调用点
|
||||
(`Wiring/AppEnvironment.swift:212-231`)+ `HostRemovalTests`(6)。
|
||||
- **ClientTLS 纳入覆盖率门**:48→84 测、55.76%→89.49%,`coverage-gate.sh` 的门集从 4 包扩到 5 包(commit `a5fa843`)。
|
||||
- **CI 三条死腿修复**:app/iPad 腿缺 `npm ci` 导致 `LiveServerSmokeTests` 硬失败;iOS-17 腿在缺 runtime 时静默报绿;
|
||||
另补 iPad UI-test 腿(同 commit)。
|
||||
- **签名解锁**:`DEVELOPMENT_TEAM` + target 级 `CODE_SIGN_IDENTITY` + `WEBTERM_PUSH_ENTITLEMENTS` env 开关
|
||||
(commit `c4f8b5b`;真机构建在免费 team 上 `BUILD SUCCEEDED`,7 天临时 profile)。
|
||||
- **已知未闭合缺口(供后续任务领走)**:① AX5 键栏裁切(见 T-iOS-34 行);② **无端到端令牌腿**——
|
||||
`IntegrationTests` 与模拟器内 live-server smoke 都不带 `WEBTERM_TOKEN` 起服务器(grep 无 `WEBTERM_TOKEN`/`webterm_auth`),
|
||||
令牌路径目前只有单元级覆盖;③ T-iOS-19 的产物层复核未覆盖 P2 新增的两条 usage description;
|
||||
④ worktree 建/删的**端到端**一次(T-iOS-32 Accept)既无自动化腿也未手工跑。
|
||||
|
||||
### P0 — 每日可用("口袋里能开终端、能批准")· 合计 ~13 人天
|
||||
|
||||
#### W0 · 基础(串行)
|
||||
|
||||
#### T-iOS-1 · `ios/` 脚手架 + XcodeGen 工程 `[ ]` · ~0.5 pd
|
||||
#### T-iOS-1 · `ios/` 脚手架 + XcodeGen 工程 `[x]` · ~0.5 pd
|
||||
- **Wave/阶段**: W0 / P0 · **Owns**: `ios/project.yml`、`ios/.gitignore`、5 个 `Package.swift` 空壳(4 个 gated 包 + `ios/IntegrationTests`;TestSupport 的 manifest 归 T-iOS-4 的 `**` glob)、`ios/App/WebTerm/WebTermApp.swift`(空窗)、CI workflow 骨架(`.github/workflows/ios.yml`)
|
||||
- **Depends**: 无 · **Parallel-safe**: 无(必须最先)
|
||||
- **Steps**:
|
||||
@@ -508,7 +607,10 @@ W5 验收(report-only, 并行)
|
||||
- **Accept**: `xcodegen generate && xcodebuild … build` 通过;空 App 在模拟器启动
|
||||
- **安全注**: ATS 键从本文 §5.2 逐字誊写,不许"先放开回头再收"。
|
||||
|
||||
#### T-iOS-2 · Day-1 双 spike(评审强制)`[ ]` · ~1 pd
|
||||
#### T-iOS-2 · Day-1 双 spike(评审强制)`[x]` · ~1 pd
|
||||
- **落地现状**:结论"URLSessionWebSocketTask 可发自定义 Origin"已定音(不切 Starscream);四条自动化断言**已常驻化**为
|
||||
`IntegrationTests/OriginGuardTests.swift`(3) + `ReplayTests.swift`(2),故 Owns 里的两个临时文件按计划**已删除/吸收**(树中不存在,不是丢失)。
|
||||
**DEFERRED**:真机 smoke(键盘/first-responder、中文 IME、`inputAccessoryView`、文本选择)——无真机。
|
||||
- **Wave/阶段**: W0 / P0 · **Owns**: `ios/IntegrationTests/OriginSpikeTests.swift`、`ios/App/WebTerm/Screens/SpikeTerminalScreen.swift`(临时文件,W4 删)
|
||||
- **Depends**: T-iOS-1 · **Parallel-safe**: T-iOS-3
|
||||
- **Steps(测试先行——spike 本身就是测试)**:
|
||||
@@ -517,7 +619,7 @@ W5 验收(report-only, 并行)
|
||||
- **Accept**: 4 条自动化断言绿(其中 ③ 的"复现失败"分支用 `withKnownIssue` 记录);真机清单逐项有结论
|
||||
- **安全注**: 这是对 "URLSessionWebSocketTask 可发自定义 Origin"(MED 置信度)的一锤定音;若失败 → `[!] BLOCKED`,orchestrator 决策切 Starscream 备胎,**不得自行引入依赖**。
|
||||
|
||||
#### T-iOS-3 · `WireProtocol` 契约包(冻结,含共享 I/O 边界类型)`[ ]` · ~1.25 pd
|
||||
#### T-iOS-3 · `WireProtocol` 契约包(冻结,含共享 I/O 边界类型)`[x]` · ~1.25 pd
|
||||
- **Wave/阶段**: W0 / P0 · **Owns**: `ios/Packages/WireProtocol/**`(Sources + Tests 全部,含 `HostEndpoint/TermTransport/HTTPTransport/TimelineEvent/Tunables`)
|
||||
- **Depends**: T-iOS-1 · **Parallel-safe**: T-iOS-2
|
||||
- **Steps(测试先行, RED)** — `Tests/WireProtocolTests/CodecRoundtripTests.swift`、`HostEndpointTests.swift`、`ServerVectorTests.swift`:
|
||||
@@ -535,7 +637,7 @@ W5 验收(report-only, 并行)
|
||||
- **Accept**: `swift test --package-path ios/Packages/WireProtocol` 全绿;覆盖率 ≥ 80%
|
||||
- **安全注**: 服务器是不可信输入源——decode 对模糊输入永不 crash(用随机字节 fuzz 一轮)。
|
||||
|
||||
#### T-iOS-4 · TestSupport 测试替身 `[ ]` · ~0.25 pd
|
||||
#### T-iOS-4 · TestSupport 测试替身 `[x]` · ~0.25 pd
|
||||
- **Wave/阶段**: W0 / P0 · **Owns**: `ios/Packages/TestSupport/**`(含本包 `Package.swift`,仅声明对 WireProtocol 的依赖——故 W0 即可编译)
|
||||
- **Depends**: T-iOS-3(接口) · **Parallel-safe**: W1 全部
|
||||
- **Steps**: [ ] `FakeTransport`(实现 WireProtocol 的 `TermTransport`;可手动灌帧 `emit(frame:)`/`emitError`、记录 send/close 调用)[ ] `FakeClock`(手动推进)[ ] `FakeHTTPTransport`(实现 WireProtocol 的 `HTTPTransport`,按 URL 排队响应)[ ] 各带 1 条 smoke 测试(`InMemoryHostStore` 归 T-iOS-7 的 HostRegistry 包——HostStore 协议在 W1 才存在)
|
||||
@@ -543,7 +645,7 @@ W5 验收(report-only, 并行)
|
||||
|
||||
#### W1 · 叶子包(全部并行)
|
||||
|
||||
#### T-iOS-5 · `ReconnectMachine` + `PingScheduler` `[ ]` · ~0.5 pd
|
||||
#### T-iOS-5 · `ReconnectMachine` + `PingScheduler` `[x]` · ~0.5 pd
|
||||
- **Wave/阶段**: W1 / P0 · **Owns**: `SessionCore/Sources/…/{ReconnectMachine,PingScheduler}.swift`、`Tests/…/{ReconnectMachineTests,PingSchedulerTests}.swift`
|
||||
- **Depends**: T-iOS-3、T-iOS-4(FakeClock) · **Parallel-safe**: T-iOS-6/7/8
|
||||
- **Steps(测试先行, RED)**:
|
||||
@@ -555,7 +657,7 @@ W5 验收(report-only, 并行)
|
||||
- **Steps(实现, GREEN)**: [ ] §3.2 签名 [ ] 常量一律读 `Tunables`(WireProtocol,T-iOS-3 所有;值见 §3.2.1——需新增常量 → 回 T-iOS-3 改契约,无魔法数字)
|
||||
- **Accept**: `swift test --package-path ios/Packages/SessionCore --filter Reconnect` 等全绿
|
||||
|
||||
#### T-iOS-6 · `GateState` + `AwayDigest` reducer `[ ]` · ~0.5 pd
|
||||
#### T-iOS-6 · `GateState` + `AwayDigest` reducer `[x]` · ~0.5 pd
|
||||
- **Wave/阶段**: W1 / P0 · **Owns**: `SessionCore/Sources/…/{GateState,AwayDigest}.swift`、对应 Tests
|
||||
- **Depends**: T-iOS-3 · **Parallel-safe**: T-iOS-5/7/8
|
||||
- **Steps(测试先行, RED)**:
|
||||
@@ -567,7 +669,7 @@ W5 验收(report-only, 并行)
|
||||
- [ ] `limit` 截断 recent;空 events → 全零 digest(UI 可据此不渲染)
|
||||
- **Accept**: 对应 filter 全绿;reducer 纯函数、不可变
|
||||
|
||||
#### T-iOS-7 · `HostRegistry` 包 `[ ]` · ~0.5 pd
|
||||
#### T-iOS-7 · `HostRegistry` 包 `[x]` · ~0.5 pd
|
||||
- **Wave/阶段**: W1 / P0 · **Owns**: `ios/Packages/HostRegistry/**`(含 `SecItemShim.swift` 与测试替身 `InMemoryHostStore.swift`——放 Sources,供本包与 App 层 VM 测试 import)
|
||||
- **Depends**: T-iOS-3 · **Parallel-safe**: T-iOS-5/6/8
|
||||
- **Steps(测试先行, RED)** — 用 InMemory 替身测协议契约;Keychain 实现经 `SecItemShim` 缝测:
|
||||
@@ -580,7 +682,7 @@ W5 验收(report-only, 并行)
|
||||
- **Accept**: `swift test --package-path ios/Packages/HostRegistry` 全绿(覆盖率计法见 §9:KeychainHostStore 以 shim 注入计入)
|
||||
- **安全注**: Keychain 属性错一个字 = 凭据可被备份带走;review 时对照 §5.3。
|
||||
|
||||
#### T-iOS-8 · `APIClient` 包 + 配对探针 `[ ]` · ~1 pd
|
||||
#### T-iOS-8 · `APIClient` 包 + 配对探针 `[x]` · ~1 pd
|
||||
- **Wave/阶段**: W1 / P0 · **Owns**: `ios/Packages/APIClient/**`
|
||||
- **Depends**: T-iOS-3、T-iOS-4 · **Parallel-safe**: T-iOS-5/6/7
|
||||
- **Steps(测试先行, RED)** — `Tests/APIClientTests/{RequestBuilderTests,PairingProbeTests,ModelDecodingTests}.swift`:
|
||||
@@ -598,7 +700,7 @@ W5 验收(report-only, 并行)
|
||||
|
||||
#### W2 · 连接核心
|
||||
|
||||
#### T-iOS-9 · `URLSessionTermTransport` `[ ]` · ~1 pd
|
||||
#### T-iOS-9 · `URLSessionTermTransport` `[x]` · ~1 pd
|
||||
- **Wave/阶段**: W2 / P0 · **Owns**: `SessionCore/Sources/…/URLSessionTermTransport.swift`、`Tests/…/URLSessionTermTransportTests.swift`
|
||||
- **Depends**: T-iOS-2(spike 结论)、T-iOS-3 · **Parallel-safe**: T-iOS-10(接口并行)
|
||||
- **Steps(测试先行, RED)** — 对 in-process 本地 WS echo(测试内起 `NWListener` 或复用 IntegrationTests 服务器):
|
||||
@@ -613,7 +715,7 @@ W5 验收(report-only, 并行)
|
||||
- **Accept**: filter 全绿;T-iOS-16 的真服务器测试是它的最终验收
|
||||
- **安全注**: Origin 单点取自 `HostEndpoint.originHeader`,本文件出现字符串拼接 origin = review CRITICAL。
|
||||
|
||||
#### T-iOS-10 · `SessionEngine` actor `[ ]` · ~1.5 pd
|
||||
#### T-iOS-10 · `SessionEngine` actor `[x]` · ~1.5 pd
|
||||
- **Wave/阶段**: W2 / P0 · **Owns**: `SessionCore/Sources/…/{SessionEngine,SessionEvent}.swift`、`Tests/…/SessionEngineTests.swift`
|
||||
- **Depends**: T-iOS-3/4/5/6(接口)、T-iOS-9(集成汇合) · **Parallel-safe**: T-iOS-9
|
||||
- **Steps(测试先行, RED)** — 全部对 FakeTransport:
|
||||
@@ -633,7 +735,7 @@ W5 验收(report-only, 并行)
|
||||
|
||||
#### W3 · UI 胶水(全部并行;只依赖 §3 接口 + 替身)
|
||||
|
||||
#### T-iOS-11 · `TerminalScreen` + `KeyBar` `[ ]` · ~1 pd
|
||||
#### T-iOS-11 · `TerminalScreen` + `KeyBar` `[x]` · ~1 pd
|
||||
- **Wave/阶段**: W3 / P0 · **Owns**: `App/WebTerm/Screens/TerminalScreen.swift`、`Components/{KeyBar,ReconnectBanner}.swift`、`ViewModels/TerminalViewModel.swift`、`SessionCore/Sources/SessionCore/KeyByteMap.swift` + `SessionCore/Tests/…/KeyByteMapTests.swift`(字节表为纯数据,源与测试都在包内——本任务是 W3 唯一持有 SessionCore 文件者:T-iOS-12/13/14 不碰 SessionCore、W1/W2 的 SessionCore owner 已完工,并行安全;纯数据+测试计入 SessionCore 覆盖率门,只帮不损)
|
||||
- **Depends**: T-iOS-10(接口) · **Parallel-safe**: T-iOS-12/13/14
|
||||
- **Steps(测试先行, RED)**:
|
||||
@@ -645,7 +747,7 @@ W5 验收(report-only, 并行)
|
||||
- **Steps(实现, GREEN)**: [ ] `UIViewRepresentable` 包 `SwiftTerm.TerminalView`,delegate `send`→`engine.send(.input)`、`sizeChanged`→`.resize` [ ] KeyBar 为 `inputAccessoryView`,直发 engine(绕开 TerminalView 避免软键盘弹出逻辑干扰)[ ] 硬件键盘 `UIKeyCommand` 同映射 [ ] KeyBar 按钮与 `UIKeyCommand` 的标签→字节解析**一律经 `KeyByteMap` 常量**(单一事实源,镜像 `public/keybar.ts`)[ ] IME:不加自己的 keydown 拦截(SwiftTerm 自管 composition)
|
||||
- **Accept**: 字节表测试全绿;模拟器人工冒烟(真机压在 T-iOS-18)
|
||||
|
||||
#### T-iOS-12 · `PairingScreen`(QR + 手输 + 探针 UI)`[ ]` · ~0.5 pd
|
||||
#### T-iOS-12 · `PairingScreen`(QR + 手输 + 探针 UI)`[x]` · ~0.5 pd
|
||||
- **Wave/阶段**: W3 / P0 · **Owns**: `App/WebTerm/Screens/PairingScreen.swift`、`ViewModels/PairingViewModel.swift`
|
||||
- **Depends**: T-iOS-7/8(接口) · **Parallel-safe**: T-iOS-11/13/14
|
||||
- **Steps(测试先行, RED)** — VM 层(探针逻辑已在 T-iOS-8 测过,这里测状态映射):
|
||||
@@ -657,7 +759,7 @@ W5 验收(report-only, 并行)
|
||||
- **Steps(实现, GREEN)**: [ ] `DataScannerViewController`(真机 only,模拟器隐藏入口;依赖 §5.2 `NSCameraUsageDescription`)读 web UI `qr.ts` 的 origin URL [ ] 手输表单 fallback(用户自己输入的 URL 身份已知,可直连探针;复用确认态亦可)[ ] 多 host 切换入口(列表页 header 用)
|
||||
- **Accept**: VM 测试全绿;模拟器手输路径可配对本机服务器
|
||||
|
||||
#### T-iOS-13 · `SessionListScreen`(合并 chooser + dashboard)`[ ]` · ~1 pd
|
||||
#### T-iOS-13 · `SessionListScreen`(合并 chooser + dashboard)`[x]` · ~1 pd
|
||||
- **Wave/阶段**: W3 / P0 · **Owns**: `App/WebTerm/Screens/SessionListScreen.swift`、`Components/TelemetryChips.swift`、`ViewModels/SessionListViewModel.swift`
|
||||
- **Depends**: T-iOS-8(接口) · **Parallel-safe**: T-iOS-11/12/14
|
||||
- **Steps(测试先行, RED)** — VM 对 FakeHTTPTransport:
|
||||
@@ -670,7 +772,7 @@ W5 验收(report-only, 并行)
|
||||
- **Steps(实现, GREEN)**: [ ] 下拉刷新 [ ] host 切换 header [ ] 空态(无会话/未配对)
|
||||
- **Accept**: VM 测试全绿
|
||||
|
||||
#### T-iOS-14 · `GateBanner` + `PlanGateSheet` + `AwayDigestView` `[ ]` · ~1 pd
|
||||
#### T-iOS-14 · `GateBanner` + `PlanGateSheet` + `AwayDigestView` `[x]` · ~1 pd
|
||||
- **Wave/阶段**: W3 / P0 · **Owns**: `App/WebTerm/Components/{GateBanner,PlanGateSheet,AwayDigestView}.swift`、`ViewModels/GateViewModel.swift`(**独立 VM**,`@MainActor @Observable`,消费 SessionEvent 的 `.gate/.digest`——不是 TerminalViewModel 的扩展,与 T-iOS-11 真并行;接入 TerminalScreen 的 wiring 归 T-iOS-15)+ 对应 Tests
|
||||
- **Depends**: T-iOS-6/10(接口) · **Parallel-safe**: T-iOS-11/12/13
|
||||
- **Steps(测试先行, RED)**:
|
||||
@@ -683,14 +785,14 @@ W5 验收(report-only, 并行)
|
||||
|
||||
#### W4 · 集成(汇合点)
|
||||
|
||||
#### T-iOS-15 · App 接线 + 生命周期 `[ ]` · ~0.5 pd
|
||||
#### T-iOS-15 · App 接线 + 生命周期 `[x]` · ~0.5 pd
|
||||
- **Wave/阶段**: W4 / P0 · **Owns**: `App/WebTerm/WebTermApp.swift`(改)、导航组装、删除 T-iOS-2 的 SpikeTerminalScreen
|
||||
- **Depends**: T-iOS-11–14 全部 · **Parallel-safe**: T-iOS-16/17
|
||||
- **Steps**: [ ] Pairing→List→Terminal 导航 + 依赖注入(真实现 wiring;含 `GateViewModel`(T-iOS-14)接入 TerminalScreen)[ ] `scenePhase == .active` → `engine.notifyForegrounded(dims:)`(重连 + 补发 resize;**这是"换设备夺回全屏"的关键**)[ ] `.background` → 主动 `close()`(干净 detach,不留半死 socket)[ ] **隐私遮罩**:`scenePhase != .active` 时终端覆盖不透明遮罩、`.active` 恢复(**必须用 `!= .active`,不能只判 `.inactive`**——覆盖切换器进入的 .inactive 与快照发生的 .background 两态;iOS 后台快照会把终端内容(API key/token/源码)写盘并展示在多任务切换器)[ ] 冷启动:有 lastSessionId 的 host → 列表页高亮"继续上次"
|
||||
- **Accept**: 模拟器全流程手工走查:配对→列表→attach→后台→前台重连回放;**切后台开切换器 → 卡片显示遮罩而非终端内容**
|
||||
- **安全注**: 组装点核对一次:所有 G 调用来自 APIClient(无绕过)、debug ATS 设置未漏进 release scheme。录屏暴露面:iOS 无公开 API 把窗口排除出截屏/录屏——只可选做 `UIScreen.isCaptured` 检测(录屏时可选拉黑终端);除此之外记为已接受残余风险(本地信任模型),**不得**计划 isSecureTextEntry 层这类非受支持 hack。
|
||||
|
||||
#### T-iOS-16 · 集成 CI(对真 Node 服务器)`[ ]` · ~0.5 pd
|
||||
#### T-iOS-16 · 集成 CI(对真 Node 服务器)`[x]` · ~0.5 pd
|
||||
- **Wave/阶段**: W4 / P0(仅依赖 W2——可提前并入第 6 批,见 §8) · **Owns**: `ios/IntegrationTests/**`(含吸收 T-iOS-2 的 OriginSpikeTests)、`.github/workflows/ios.yml`(改)
|
||||
- **Depends**: T-iOS-9/10 · **Parallel-safe**: T-iOS-11–15/17
|
||||
- **Steps(测试清单)** — macOS runner:`npm ci` → 临时端口 `npm start`(`ALLOWED_ORIGINS` 注入)→ Swift Testing:
|
||||
@@ -705,7 +807,8 @@ W5 验收(report-only, 并行)
|
||||
- **Accept**: CI job 绿;覆盖率门**演示过一次红**(故意把某包压到 80% 下或抬高阈值)再回绿;这是"客户端复刻的协议契约"的持续防漂移闸门
|
||||
- **安全注**: 本任务是 §5.1 的自动化化身;任何"为了过 CI 放宽 Origin 断言"= CRITICAL。
|
||||
|
||||
#### T-iOS-17 · ntfy 桥验证 + 文档(P0 临时通知,**零新代码**)`[ ]` · ~0.1 pd
|
||||
#### T-iOS-17 · ntfy 桥验证 + 文档(P0 临时通知,**零新代码**)`[x]` · ~0.1 pd
|
||||
- **落地现状**:`ios/README.md` 的 ntfy 章节(逐条引 `scripts/setup-hooks.mjs` 行号;只读验证,**未触碰用户真实 hook 配置**)。**DEFERRED**:手机端到端(需用户手机 + 改用户 hook 配置)。
|
||||
- **Wave/阶段**: W4 / P0 · **Owns**: iOS README 的 ntfy 章节(文档;**不建任何脚本文件**——桥已随 `npm run setup-hooks` 出货:安装逻辑 scripts/setup-hooks.mjs:227-238、env 门 :262-264,重装时 marker 自清理,见 §0.3)
|
||||
- **Depends**: 无(与 App 解耦) · **Parallel-safe**: T-iOS-15/16
|
||||
- **Steps**: [ ] 验证既有桥:设 `WEBTERM_NTFY_URL` + `WEBTERM_NTFY_TOPIC`(可选 `WEBTERM_NTFY_TOKEN`)→ `npm run setup-hooks` → 确认日志 "Installed ntfy bridge (NEEDS-INPUT=high, DONE=low)" [ ] README 写明:env 未设即完全无副作用(默认关闭,已是出货行为);topic 生成建议随机串(topic 即密码)[ ] 记录:**STUCK 不在 P0 信号内**(服务器 sweepStuck 派生态、无 hook 事件,桥发不出——P1 APNs 经事件总线补上)
|
||||
@@ -714,10 +817,12 @@ W5 验收(report-only, 并行)
|
||||
|
||||
#### W5 · 验收(report-only,G4:只报告不改码,findings 标 owning task 派回)
|
||||
|
||||
#### T-iOS-18 · 验收走查 F-iOS-1…13(真机)`[ ]` · ~0.25 pd
|
||||
#### T-iOS-18 · 验收走查 F-iOS-1…13(真机)`[~]` · ~0.25 pd
|
||||
- **缺口**:机器可执行项已执行并指认测试名;真机项(QR 扫码 / IME / 震动 / 切换器遮罩目检 / ntfy 端到端)**DEFERRED**,手工清单在 `PROGRESS_LOG.md`。
|
||||
- **Depends**: T-iOS-15/16/17 · **Owns**: 无源码(report-only)
|
||||
- **Steps**: 按 §9 验收脚本逐条执行、记录结论与录屏。
|
||||
#### T-iOS-19 · 安全核对 `[ ]` · ~0.25 pd
|
||||
#### T-iOS-19 · 安全核对 `[~]` · ~0.25 pd
|
||||
- **缺口**:P0 面已逐条核;**P2 新增的 `NSMicrophoneUsageDescription`/`NSSpeechRecognitionUsageDescription` 未在产物层复核**,release ipa 层核对仍缺(免费个人 team 无分发通道)。本波 D1 只覆盖令牌路径与 entitlements 开关。
|
||||
- **Depends**: T-iOS-15 · **Owns**: 无源码(report-only)
|
||||
- **Steps**: [ ] 对照 TECH_DOC §7 + 本文 §5 逐条核:Origin 单点派生、G/RO 分界、ATS 键 release 实况(拆 ipa 验 Info.plist——**五段 CIDR 逐段核对**,debug `NSAllowsArbitraryLoads` 会掩盖缺段)、隐私 usage description 与实际使用的 capability **一一对应**(相机/本地网络;P2 加麦克风/语音识别——拆 ipa 核对)、Keychain 属性(模拟器测试断言 `kSecAttrAccessible`)、ntfy payload 最小化、无硬编码 host/密钥、警告分层文案在位(公网阻断 + RFC1918 明文提示 + Tailscale 豁免)、**真机核:切后台开切换器 → 快照卡片是遮罩非终端内容**。
|
||||
|
||||
@@ -729,68 +834,70 @@ W5 验收(report-only, 并行)
|
||||
|
||||
> 波次:**W6(服务器触点:T-iOS-20 ∥ T-iOS-37,串行入库各自 repo 流程)与 W7 并行开跑**——W7 里只有 T-iOS-21 依赖 T-iOS-20(payload 形状)、T-iOS-23 依赖 T-iOS-37(lastOutputAt 字段),其余 W7 任务(T-iOS-22/24/25/27/28/29)不等 W6;T-iOS-38(APIClient P1 契约增量)在 W7 首发,T-iOS-21/26 依赖它 → W8(验收)。任务粒度与 P0 同规格;此处 Steps(测试) 列关键用例,细化在开工时由任务 owner 补足(不改接口)。
|
||||
|
||||
#### T-iOS-20 · server: APNs sender + token 注册端点 `[ ]` · ~2 pd
|
||||
#### T-iOS-20 · server: APNs sender + token 注册端点 `[x]` · ~2 pd
|
||||
- **落地现状**:`src/push/apns.ts` + `test/push-apns.test.ts`(含本地 h2c 假 APNs 的双 e2e);env 三件套缺失即整体 disabled、启动不 crash、密钥材料零日志。**DEFERRED**:对真 APNs 的端到端需付费账号 + `.p8`。
|
||||
- **Wave**: W6 · **Owns**: `src/push/apns.ts`(新)、`src/server.ts` 增量 route、`test/push-apns.test.ts`(**服务器触点,TypeScript 任务**,遵循根仓库 PLAN 工作流)
|
||||
- **Depends**: 无 · **Parallel-safe**: T-iOS-37/38 及 W7 除 T-iOS-21 外全部(接口先行)
|
||||
- **Steps(测试先行)**: [ ] `.p8` 缺失 → 功能整体 disabled、启动不 crash [ ] hook 事件 → APNs payload 形状(含 `/hook/decision` 用的 capability token + category)[ ] token 注册端点:G 守卫 403、限频 429、幂等注册/注销 [ ] 与既有 web-push 并行互不干扰
|
||||
- **Steps(实现)**: [ ] HTTP/2 到 `api.push.apple.com`,`.p8` 走 env 路径(无硬编码密钥)[ ] 复用 `src/push/` 的事件订阅点
|
||||
- **安全注**: capability token 语义不变(单次、过期、10/min 限频,src/server.ts:503-525);APNs payload 不含命令内容。
|
||||
|
||||
#### T-iOS-37 · server: `LiveSessionInfo.lastOutputAt` 字段 `[ ]` · ~0.25 pd
|
||||
#### T-iOS-37 · server: `LiveSessionInfo.lastOutputAt` 字段 `[x]` · ~0.25 pd
|
||||
- **Wave**: W6 · **Owns**: `src/types.ts` 的 `LiveSessionInfo` 增量字段、`src/session/manager.ts` `list()` 一行映射(并更新其 "omitted by design" 注释)、对应测试增量(**声明的服务器触点,TypeScript 任务**,遵循根仓库 PLAN 工作流;见 §0.3)
|
||||
- **Depends**: 无 · **Parallel-safe**: T-iOS-20、W7 全部
|
||||
- **Steps(测试先行)**: [ ] `GET /live-sessions` 响应含 `lastOutputAt`(服务器已逐 `pty.onData` 维护,src/types.ts:211/M3——只是序列化出来)[ ] 旧客户端兼容:字段为**新增可选**,web 前端不受影响
|
||||
- **Accept**: 根仓库 `npm test` 全绿;T-iOS-23 的 unread 水位有数据源
|
||||
|
||||
#### T-iOS-38 · `APIClient` P1 契约增量(W7 首发,其余 W7 任务的 APIClient 单一 owner)`[ ]` · ~0.5 pd
|
||||
#### T-iOS-38 · `APIClient` P1 契约增量(W7 首发,其余 W7 任务的 APIClient 单一 owner)`[x]` · ~0.5 pd
|
||||
- **Wave**: W7(首发) · **Owns**: `ios/Packages/APIClient/**` 的 **全部 P1 增量**(APNs token 注册 builder、`/projects`、`/projects/detail?path=`、`GET/PUT /prefs` builders 与解码 + Tests)——W7 期间 APIClient 文件**只有本任务可改**(对齐 T-iOS-3 冻结契约模式,避免 T-iOS-21/26 同波踩踏)
|
||||
- **Depends**: T-iOS-8;T-iOS-20(仅 token 注册 builder 的端点形状——可接口先行并行编码,形状定稿时汇合) · **Parallel-safe**: T-iOS-20/22/23/24/25/27/28/29(均不碰 APIClient 文件)
|
||||
- **Steps(测试先行)**: [ ] token 注册 builder(G,带 Origin)[ ] projects/detail/prefs builders(PUT 带 Origin)与解码 [ ] doc comment 端点约束:push subscribe body ≤ 8 KB、≤5 次/分/IP(src/server.ts:73,461-466);`PUT /prefs` ≤ 64 KB(src/server.ts:278)
|
||||
- **Accept**: `swift test --package-path ios/Packages/APIClient` 全绿;覆盖率 ≥ 80%
|
||||
|
||||
#### T-iOS-21 · PushRegistrar + 锁屏 Allow/Deny `[ ]` · ~1.5 pd
|
||||
#### T-iOS-21 · PushRegistrar + 锁屏 Allow/Deny `[x]` · ~1.5 pd
|
||||
- **落地现状**:`Push/{PushRegistrar,NotificationActionHandler}.swift` + 三组 Tests;`handleHostRemoved` 在收尾波接上真调用点(`Wiring/AppEnvironment.swift`)。**DEFERRED**:真机锁屏 Allow + Face ID 走查;`aps-environment` 需付费 team(`WEBTERM_PUSH_ENTITLEMENTS` 开关,见 `ios/README.md`)。
|
||||
- **Wave**: W7 · **Owns**: `App/WebTerm/Push/{PushRegistrar,NotificationActionHandler}.swift` + 对应 Tests(APIClient 的 token 注册 builder 归 T-iOS-38)
|
||||
- **Depends**: T-iOS-20(payload 形状)、T-iOS-38(token 注册 builder) · **Parallel-safe**: T-iOS-22–29
|
||||
- **Steps(测试先行)**: [ ] `UNNotificationCategory` Allow/Deny 注册形状——**Allow 动作必须带 `UNNotificationActionOptions.authenticationRequired`**(锁屏批准 = 授权主机执行命令;旁观者拿到锁屏手机只能 Deny——fail-safe。断言注册 category 的 Allow 选项含 `.authenticationRequired`,且两动作均**不含** `.foreground`)[ ] action handler:从 payload 取 `{sessionId, token}` → `POST /hook/decision`(带 Origin)→ **不启动 App UI**(Face ID 设备上"两次手势 + 一瞥"闭环)[ ] token 用后即弃不落盘 [ ] 决策失败(token 过期 403)→ 补一条本地通知提示进 App 处理
|
||||
- **安全注**: Allow/Deny 由系统**后台拉起主 App**、送达 `UNUserNotificationCenterDelegate.userNotificationCenter(_:didReceive:withCompletionHandler:)`——**无 extension 参与**(本工程没有 notification extension target;Service Extension 只能改写来押通知、收不到 action tap)。handler 用 `beginBackgroundTask/endBackgroundTask` 包住 POST,请求完成/失败后才调 completion handler;失败必须可见(本地通知兜底),绝不静默吞。
|
||||
|
||||
#### T-iOS-22 · DeepLinkRouter `[ ]` · ~1 pd
|
||||
#### T-iOS-22 · DeepLinkRouter `[x]` · ~1 pd
|
||||
- **Wave**: W7 · **Owns**: `App/WebTerm/DeepLinkRouter.swift` + Tests
|
||||
- **Steps(测试先行)**: [ ] `webterminal://open?host=<id>&join=<uuid>`:UUID v4 校验(复用 `Validation`),非法 → 忽略并留日志 [ ] 未知 host id → 落到配对页并提示 [ ] 冷/热启动两路径都直达 gated 会话 [ ] push tap → 同一路由
|
||||
- **安全注**: deep link 是外部输入——全字段白名单校验,绝不据此直接拼 URL 请求。
|
||||
|
||||
#### T-iOS-23 · 多会话切换器(unread dots + OSC 标题)`[ ]` · ~2 pd
|
||||
#### T-iOS-23 · 多会话切换器(unread dots + OSC 标题)`[x]` · ~2 pd
|
||||
- **Wave**: W7 · **Owns**: `SessionListScreen` 增强(**W7 内该文件唯一 owner**,含 T-iOS-29 移交的列表侧入口)、`SessionCore` 的 unread 记账(`UnreadLedger.swift`)、标题净化器(`TitleSanitizer.swift`)+ Tests
|
||||
- **Depends**: T-iOS-37(`lastOutputAt` 字段) · **Parallel-safe**: T-iOS-21/22/24/25/26/27/28
|
||||
- **Steps(测试先行)**: [ ] 单活 WS 不变:切会话 = close→open,回放恢复 [ ] unread 判定:`/live-sessions` 快照的 `lastOutputAt`(T-iOS-37 新增字段)> 本地 last-seen 水位 → unread 点 [ ] OSC 标题经 SwiftTerm `setTerminalTitle` delegate 上浮到列表——**标题是主机/攻击者可控输入,入列表前过净化器**:截断 `Tunables.titleMaxLength`(256);剥 Unicode 双向覆写与零宽字符(U+200B–200F、U+202A–202E、U+2066–2069——OSC 字符串解析已排除 C0,真正的仿冒向量是 bidi/零宽);渲染用 `Text(verbatim:)`(绝不走 LocalizedStringKey/Markdown)+ `.lineLimit(1)` 截断 [ ] 敌意标题解码测试:超长、U+202E payload、emoji 洪泛 → 断言净化输出 [ ] 切换 <1s 观感(回放解析在后台、feed 在 main)
|
||||
|
||||
#### T-iOS-24 · Timeline sheet(完整时间线钻取)`[ ]` · ~1 pd
|
||||
#### T-iOS-24 · Timeline sheet(完整时间线钻取)`[x]` · ~1 pd
|
||||
- **Wave**: W7 · **Owns**: `App/WebTerm/Screens/TimelineSheet.swift` + VM 测试
|
||||
- **Steps(测试先行)**: [ ] `/live-sessions/:id/events` 全量渲染(class → 图标/颜色映射)[ ] timeline disabled(空数组)→ 空态而非错误 [ ] 从 digest "展开"入口进入
|
||||
|
||||
#### T-iOS-25 · Quick-reply chips + 常用语面板 `[ ]` · ~1.5 pd
|
||||
#### T-iOS-25 · Quick-reply chips + 常用语面板 `[x]` · ~1.5 pd
|
||||
- **Wave**: W7 · **Owns**: `App/WebTerm/Components/QuickReply.swift`、本地存储(UserDefaults)+ Tests
|
||||
- **Steps(测试先行)**: [ ] chip 点击 → `input` 帧(文本 + `\r`)[ ] 自定义面板增删改序 [ ] waiting 状态才浮出(对齐 web `quick-reply.ts` 行为)
|
||||
|
||||
#### T-iOS-26 · Projects:列表 + 详情 + 在仓库起 Claude `[ ]` · ~2 pd
|
||||
#### T-iOS-26 · Projects:列表 + 详情 + 在仓库起 Claude `[x]` · ~2 pd
|
||||
- **Wave**: W7 · **Owns**: `App/WebTerm/Screens/{ProjectsScreen,ProjectDetailScreen}.swift` + VM Tests(projects/detail/prefs 的 APIClient builders 归 T-iOS-38,本任务只消费)
|
||||
- **Depends**: T-iOS-38(builders) · **Parallel-safe**: T-iOS-21/22/23/24/25/27/28/29
|
||||
- **Steps(测试先行)**: [ ] VM 消费 `/projects`、`/projects/detail?path=`、`GET/PUT /prefs`(builder 与解码测试在 T-iOS-38)[ ] favourites 同步 [ ] "在此仓库开新会话" = `attach(null, cwd)` + 注入 `claude\r` [ ] detail 的 400/404/500 `{error}` 显式路径
|
||||
|
||||
#### T-iOS-27 · Diff 查看器(只读)`[ ]` · ~1.5 pd
|
||||
#### T-iOS-27 · Diff 查看器(只读)`[x]` · ~1.5 pd
|
||||
- **Wave**: W7 · **Owns**: `App/WebTerm/Screens/DiffScreen.swift` + VM 测试
|
||||
- **Steps(测试先行)**: [ ] `DiffResult{files,staged,truncated}` 渲染、truncated 提示 [ ] staged/unstaged 切换 [ ] path 非法 404 → 友好错误
|
||||
|
||||
#### T-iOS-28 · 会话缩略图(offscreen SwiftTerm)`[ ]` · ~1.5 pd
|
||||
#### T-iOS-28 · 会话缩略图(offscreen SwiftTerm)`[x]` · ~1.5 pd
|
||||
- **Wave**: W7 · **Owns**: `App/WebTerm/Components/SessionThumbnail.swift` + 快照测试
|
||||
- **Steps(测试先行)**: [ ] `GET /live-sessions/:id/preview`(`{id,cols,rows,data}`,24KB tail)→ 离屏 TerminalView feed → 快照图 [ ] 列表滚动不掉帧(离屏渲染限并发)[ ] 404 → 占位图
|
||||
|
||||
#### T-iOS-29 · 杂项闭环:new-in-cwd + 退出会话清理 `[ ]` · ~1 pd
|
||||
#### T-iOS-29 · 杂项闭环:new-in-cwd + 退出会话清理 `[x]` · ~1 pd
|
||||
- **Wave**: W7 · **Owns**: `TerminalScreen` 的小增量 + Tests(**不碰 `SessionListScreen`**——列表侧入口/行项变更移交 T-iOS-23,该文件 W7 内单一 owner)
|
||||
- **Depends**: T-iOS-23(列表侧入口) · **Parallel-safe**: T-iOS-21/22/24/25/26/27/28
|
||||
- **Steps(测试先行)**: [ ] "在当前会话 cwd 开新会话"(`attach(null, cwd)`)[ ] exited 会话点开 → 回放 + exit 横幅 + "开新会话"动作(src/session/manager.ts:145-153 语义)
|
||||
|
||||
#### T-iOS-30 · P1 验收 + 安全复核 `[ ]` · ~1 pd(report-only)
|
||||
#### T-iOS-30 · P1 验收 + 安全复核 `[x]` · ~1 pd(report-only)
|
||||
- **Steps**: [ ] F-iOS-14/15(§9)真机走查 [ ] 安全:APNs payload 审计、token 生命周期、deep link fuzz、通知在锁屏的预览泄露面(默认隐藏内容验证)、**锁屏 Allow 动作必须 `.authenticationRequired`(真机验:锁屏 Allow 弹 Face ID/通行码;Deny 无需解锁)**。
|
||||
|
||||
**P1 合计 ≈ 17.5 人天**(含新增 T-iOS-37 0.25 + T-iOS-38 0.5;T-iOS-21/26 相应减负)。
|
||||
@@ -800,13 +907,25 @@ W5 验收(report-only, 并行)
|
||||
### P2 — 打磨 · 合计 ~8 人天
|
||||
|
||||
> P2 任务此处只给**分派必需元数据**(Wave/Owns/Depends/Accept);完整 RED 测试清单在开工时由任务 owner 按 P0 规格扩写(不改 §3 接口)。**未扩写前不得按下述描述直接分派**(§4 TDD 强制与 §6 Owns 铁律同样适用)。
|
||||
> **该前置已履行**:T-iOS-31/32/33/34/35 的逐条 RED 清单由各 builder 开工第一步写进
|
||||
> [`docs/plans/ios-completion.md`](./plans/ios-completion.md) §4(共 100+ 条,含 T-iOS-32 的 35 条、
|
||||
> T-iOS-33 的 18 条、T-iOS-31 的 38 条、T-iOS-34 的 29 条、T-iOS-35 的 21 条),再进 GREEN。
|
||||
|
||||
- **T-iOS-31** · 语音 PTT + 确认(端口匹配器 / 1.5s 撤销 / epoch 防误发)`[ ]` ~2.5 pd。**Wave**: W9 · **Owns**: `App/WebTerm/Components/VoicePTT.swift` + VM Tests(epoch 防误发若需 SessionCore 新接口 → 经 T-iOS-6 owner 回 SessionCore 加,不直改)· **Depends**: T-iOS-6/11;**前置**:Info.plist 加 `NSMicrophoneUsageDescription` + `NSSpeechRecognitionUsageDescription`(§5.2 注)· **Accept**: VM 测试 + 真机口述→确认→注入 input。
|
||||
- **T-iOS-32** · Worktree 创建(`POST /projects/worktree`,G)+ `claude --resume <id>` 历史(`GET /sessions`)`[ ]` ~1.5 pd。**Wave**: W9 · **Owns**: `App/WebTerm/Screens/WorktreeSheet.swift` + Tests(APIClient builders 经 T-iOS-38 owner 模式回 APIClient 加)· **Depends**: T-iOS-26/38 · **Accept**: builder 测试 + 端到端一次。
|
||||
- **T-iOS-33** · 终端内搜索 `[ ]` ~1 pd。**Wave**: W9 · **Owns**: `App/WebTerm/Components/TerminalSearchBar.swift` + Tests · **Depends**: T-iOS-11 · **Accept**: SwiftTerm search API 命中高亮。
|
||||
- **T-iOS-34** · 主题 + Dynamic Type `[ ]` ~1.5 pd。**Wave**: W9 · **Owns**: 主题/字号小增量(与同波任务文件不相交,开工时列明文件清单)· **Depends**: T-iOS-11/13 · **Accept**: 亮暗主题 + 最大字号不破版。
|
||||
- **T-iOS-35** · web `?join=` 互通(分享 QR 双向)`[ ]` ~0.5 pd。**Wave**: W9 · **Owns**: `DeepLinkRouter.swift` 增量(`?join=` 解析)· **Depends**: T-iOS-22 · **Accept**: 手机扫 web 分享 QR 直达同会话。
|
||||
- **T-iOS-36** · P2 验收 `[ ]` ~1 pd(report-only)。**Wave**: W10 · **Owns**: 无源码 · **Depends**: T-iOS-31–35。
|
||||
- **T-iOS-31** · 语音 PTT + 确认(端口匹配器 / 1.5s 撤销 / epoch 防误发)`[x]` ~2.5 pd。**Wave**: W9 · **Owns**: `App/WebTerm/Components/VoicePTT.swift` + VM Tests(epoch 防误发若需 SessionCore 新接口 → 经 T-iOS-6 owner 回 SessionCore 加,不直改)· **Depends**: T-iOS-6/11;**前置**:Info.plist 加 `NSMicrophoneUsageDescription` + `NSSpeechRecognitionUsageDescription`(§5.2 注)· **Accept**: VM 测试 + 真机口述→确认→注入 input。
|
||||
- **落地**: `Components/{VoicePTT,VoicePTTBanner,SpeechDictation}.swift` + `KeyBar` 末位 🎤 键(前 17 键顺序/标签零变化)+ `VoicePTTTests` 40 测(转写清洗 / 匹配器 / 1.5s 撤销窗 / 双道 epoch 闸 / 权限失败路径 / 键栏零回归)。注入内容**不以 `\r` 结尾**,确认前零注入。
|
||||
- **DEFERRED**: 真机口述→确认→注入(需真机麦克风与语音识别授权)。
|
||||
- **T-iOS-32** · Worktree 创建(`POST /projects/worktree`,G)+ `claude --resume <id>` 历史(`GET /sessions`)`[~]` ~1.5 pd。**Wave**: W9 · **Owns**: `App/WebTerm/Screens/WorktreeSheet.swift` + Tests(APIClient builders 经 T-iOS-38 owner 模式回 APIClient 加)· **Depends**: T-iOS-26/38 · **Accept**: builder 测试 + 端到端一次。
|
||||
- **落地**: `Screens/{WorktreeSheet,ResumeHistorySheet}.swift` + `ViewModels/{WorktreeViewModel,ResumeHistoryViewModel}.swift`;分支名 9 条规则客户端先校验(非法名零请求)、prune 幂等文案、remove 两级确认(409→显式 force 确认,绝不自动重试)、`--resume` id 白名单后才拼命令行;`WorktreeViewModelTests`(22)/`ResumeHistoryViewModelTests`(12)/`ProjectResumeLaunchTests`(7)。
|
||||
- **未做**: Accept 里的"端到端一次"(对真服务器真建/真删一个 worktree)没有自动化腿,也未在本环境手工跑过。
|
||||
- **T-iOS-33** · 终端内搜索 `[x]` ~1 pd。**Wave**: W9 · **Owns**: `App/WebTerm/Components/TerminalSearchBar.swift` + Tests · **Depends**: T-iOS-11 · **Accept**: SwiftTerm search API 命中高亮。
|
||||
- **T-iOS-34** · 主题 + Dynamic Type `[~]` ~1.5 pd。**Wave**: W9 · **Owns**: 主题/字号小增量(与同波任务文件不相交,开工时列明文件清单)· **Depends**: T-iOS-11/13 · **Accept**: 亮暗主题 + 最大字号不破版。
|
||||
- **落地**: `DesignSystem/{AppTheme,TerminalPalette}.swift` + `Screens/SettingsScreen.swift`(齿轮入口在 `ProjectsToolbarItem` 内,stack 与 split 两根视图共享)+ Tokens/Typography 浅色档;`AppThemeTests`(24)/`DynamicTypeLayoutTests`(8)。默认仍是深色(零回归)。
|
||||
- **缺口(已实测、未修)**: AX5 下键栏两行需 **108.24pt**,`KeyBarMetrics.barHeight` 固定 **52pt**(44+8)→ 键帽裁切;以 `withKnownIssue` 钉在 `DynamicTypeLayoutTests`(修好后该用例会报 "known issue was not recorded",提醒删标记)。`Components/KeyBar.swift` 不在该任务 Owns,需另派。
|
||||
- **T-iOS-35** · web `?join=` 互通(分享 QR 双向)`[x]` ~0.5 pd。**Wave**: W9 · **Owns**: `DeepLinkRouter.swift` 增量(`?join=` 解析)· **Depends**: T-iOS-22 · **Accept**: 手机扫 web 分享 QR 直达同会话。
|
||||
- **落地**: `DeepLinkRouter` 增 `.joinShared` 分支 + `DeepLinkJoinTests` 19 测(含把原"scheme 不是 webterminal"的拒绝理由改写为"web 形状只许单一 `join` 键");主机身份只经 `HostStore`+`HostEndpoint.originHeader` 解析,未配对 origin 一律走配对页且 hint 不回显链接内容。
|
||||
- **DEFERRED**: 用真手机相机扫 web 分享 QR 的目视走查(模拟器无相机)。
|
||||
- **T-iOS-36** · P2 验收 `[~]` ~1 pd(report-only)。**Wave**: W10 · **Owns**: 无源码 · **Depends**: T-iOS-31–35。
|
||||
- **缺口**: 由 ios-completion 收尾波的 Wave D 验收 agent 执行中;**真实数字与结论以 `PROGRESS_LOG.md` 的该条目为准**,本文不预写"通过"。
|
||||
|
||||
**总计:P0 13 + P1 17.5 + P2 8 ≈ 38.5 人天。**
|
||||
|
||||
@@ -863,7 +982,15 @@ W5 验收(report-only, 并行)
|
||||
|
||||
每任务:**RED**(照 Steps(测试) 先写失败测试)→ **GREEN**(最小实现过测)→ **REFACTOR**(对照 §4 清单)。测试命名讲行为(`test("未知 UUID attach 后采用服务器新发的 id")`),AAA 结构。
|
||||
|
||||
### 覆盖率门(≥ 80%,只量 4 个包)
|
||||
### 覆盖率门(≥ 80%;原定 4 个包,**现为 5 个**)
|
||||
|
||||
> **现状(2026-07-30)**:门集已扩到 **5** 个包——原 4 个 + **`ClientTLS`**(ios-completion 收尾波 B4:
|
||||
> 48→84 测、55.76%→**89.49%**,此前是树里唯一未进门的包,却最安全敏感)。
|
||||
> CI 实际执行的是**修正口径**脚本 `ios/IntegrationTests/scripts/coverage-gate.sh <Pkg>`
|
||||
> (下面这段裸 `llvm-cov` 命令读的是 export TOTALS,会把静态链入的**依赖包源码**一起计进去——
|
||||
> 这个缺陷由 T-iOS-16 修掉,脚本只保留 `Packages/<P>/Sources/` 并排除 `*Placeholder*`)。
|
||||
> 最近一次记录在案的 own-sources 数字:APIClient 92.22% · HostRegistry 92.49% · SessionCore 96.74% ·
|
||||
> ClientTLS 89.49% · WireProtocol 100%。
|
||||
|
||||
```bash
|
||||
for p in WireProtocol SessionCore HostRegistry APIClient; do
|
||||
@@ -931,9 +1058,16 @@ XCUITest 只保一条 happy path:配对→attach→输入→gate approve(脆
|
||||
| 单 WS 设计与多会话切换器(P1)的张力 | 低 | 切换 = 重放恢复,成本近零;若实测不适再评估观察者 WS |
|
||||
|
||||
**待你拍板的开放项**:
|
||||
1. Bundle id / 产品名 / 图标(App 叫什么?`webterminal://` scheme 是否可用/要改名?)
|
||||
2. Apple 付费开发者账号($99/年)何时开——决定 P1 APNs 与 TestFlight 分发起点;P0 期间接受自签 sideload?
|
||||
3. 最低系统版本:iOS 17(可分发下限)还是 18/26(个人工具,availability 噪音最小)?
|
||||
1. ~~Bundle id / 产品名 / 图标~~ **已定**:`com.yaojia.webterm` / WebTerm / "Orbit" 图标(`project.yml`;
|
||||
deep-link scheme `webterminal://` 已注册并在用,另接受 web 的 `http(s)://…/?join=` 形状,T-iOS-35)。
|
||||
2. ~~Apple 付费开发者账号何时开~~ **现状已定、后果已量化**:用的是**免费个人 team**(`DEVELOPMENT_TEAM=C738Z66SRW`)。
|
||||
真机构建可用(7 天临时 profile,`-allowProvisioningUpdates`,实测 `BUILD SUCCEEDED`);
|
||||
**Push Notifications capability 免费 team 不支持** —— `aps-environment` 因此做成 `WEBTERM_PUSH_ENTITLEMENTS`
|
||||
env 开关(默认不挂;挂上则真机构建报
|
||||
`Personal development teams, including "Yaojia Wang", do not support the Push Notifications capability`)。
|
||||
⇒ **APNs 真机端到端 + TestFlight 仍待付费账号**;release 构建还需把 `aps-environment` 翻成 `production`。
|
||||
操作细节见 [`ios/README.md`](../ios/README.md#signing-device-builds-and-the-push-entitlements-switch)。
|
||||
3. ~~最低系统版本~~ **已定**:iOS **17.0**(`project.yml` `deploymentTarget`),CI 另有一条 iOS-17 底线腿。
|
||||
4. ~~P0 的 ntfy 桥要不要做成默认关闭~~ **已解决**:桥已随 `npm run setup-hooks` 出货且默认关闭(`WEBTERM_NTFY_URL`/`WEBTERM_NTFY_TOPIC` 未设即完全无副作用)——无需新做,T-iOS-17 只验证/写文档。
|
||||
5. ~~Tailscale 场景是否直接推荐 `tailscale serve`~~ **已定**:推荐 `tailscale serve`(wss)为标准部署话术(配对 UI/README 采用;绕开全部 ATS 例外与明文嗅探面,见 §5.4)。
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
# PLAN_IOS_IPAD.md — iPad 适配(自适应布局,非分叉)
|
||||
|
||||
> 落地方案文档。目标:让已完成的 iPhone 客户端([PLAN_IOS_CLIENT.md](./PLAN_IOS_CLIENT.md),P0+P1 已交付,分支 `feat/ios-client`)**原生适配 iPad**——大屏分栏、双向布局、指针/硬件键盘,而**不分叉出第二套 UI**。
|
||||
> 落地方案文档。目标:让已完成的 iPhone 客户端([PLAN_IOS_CLIENT.md](./PLAN_IOS_CLIENT.md),P0+P1+P2 已交付,**已合入 `develop`**)**原生适配 iPad**——大屏分栏、双向布局、指针/硬件键盘,而**不分叉出第二套 UI**。
|
||||
> 拓扑决策:**单一代码库 + size-class 自适应**——`NavigationSplitView` 在 regular 宽度(iPad 全屏/大分屏)给 sidebar+detail,在 compact 宽度(iPhone、iPad Slide Over/小分屏)**自动退化为现有 stack**。iPhone 行为字节级不变。
|
||||
> 状态:**规划中(2026-07-05,未开工)**。
|
||||
> 状态:**已交付并合入 `develop`**(T-iPad-1…4 全部落地;T-iPad-5 验收 PASS_WITH_FINDINGS,4/4 findings 已修,真机项 DEFERRED)。
|
||||
> 逐任务状态见 §5 各任务标题上的方框;**Steps 里的 `[ ]` 是原始规格清单,不是状态**(执行记录在 `PROGRESS_LOG.md`)。
|
||||
> 2026-07-30 由 ios-completion 收尾波按源码核对:`Wiring/{AdaptiveRootView,SplitRootView,LayoutMode}.swift`、
|
||||
> `Components/TerminalContextMenu.swift`、`Screens/ProjectsLayout.swift` 与 `LayoutPolicyTests`/`SidebarSelectionTests`/
|
||||
> `KeyBarVisibilityTests`/`TerminalContextMenuTests`/`ProjectsLayoutTests`/`ProjectsLayoutUITests` 均在树内;
|
||||
> `project.yml` 三个 target 的 `TARGETED_DEVICE_FAMILY` 均为 `"1,2"`;`ios.yml` 有 iPad 单测腿与 iPad UI-test 腿。
|
||||
> 本文是 iPhone 计划之上的**布局适配层**,不改协议/会话模型/纯逻辑包;沿用 [PLAN_IOS_CLIENT.md](./PLAN_IOS_CLIENT.md) 的 §3 契约、§4 工程标准、§5 安全模型、§6 并行规则。冲突以 iPhone 计划为准。
|
||||
> **G1 日志铁律**:`PROGRESS_LOG.md` 由 orchestrator 独写;被派 subagent 不写 LOG,在返回消息末尾附可粘贴条目。
|
||||
|
||||
@@ -118,7 +123,7 @@ enum SidebarItem: Hashable { case session(UUID), newSession, projects }
|
||||
|
||||
### W0 · 可安装性(串行,先行)
|
||||
|
||||
#### T-iPad-1 · device family + iPad plist/方向 `[ ]` · ~0.5 pd
|
||||
#### T-iPad-1 · device family + iPad plist/方向 `[x]` · ~0.5 pd
|
||||
- **Owns**: `ios/project.yml`(device family、iPad 方向、必要 plist)、`.github/workflows/ios.yml`(加 iPad 模拟器测试腿)
|
||||
- **Depends**: 无
|
||||
- **Steps(测试先行)**:
|
||||
@@ -132,7 +137,7 @@ enum SidebarItem: Hashable { case session(UUID), newSession, projects }
|
||||
|
||||
### W1 · 自适应导航壳(核心)
|
||||
|
||||
#### T-iPad-2 · AdaptiveRootView + NavigationSplitView + LayoutPolicy `[ ]` · ~2 pd
|
||||
#### T-iPad-2 · AdaptiveRootView + NavigationSplitView + LayoutPolicy `[x]` · ~2 pd
|
||||
- **Owns**: `Wiring/{AdaptiveRootView,SplitRootView,LayoutMode}.swift`(新)、`Wiring/RootView.swift`(改:现有 stack 抽成 `StackRootView` 子视图供 compact 复用)、`Wiring/AppCoordinator.swift`(改:sidebar 选中 ↔ 路由最小桥)、对应测试
|
||||
- **Depends**: T-iPad-1
|
||||
- **Steps(测试先行, RED)** — `LayoutPolicyTests` + `SidebarSelectionTests`:
|
||||
@@ -149,7 +154,7 @@ enum SidebarItem: Hashable { case session(UUID), newSession, projects }
|
||||
|
||||
### W2 · 逐面适配(并行,文件互斥)
|
||||
|
||||
#### T-iPad-3 · 终端面板:KeyBar 自适应 + 指针上下文菜单 `[ ]` · ~1 pd
|
||||
#### T-iPad-3 · 终端面板:KeyBar 自适应 + 指针上下文菜单 `[x]` · ~1 pd
|
||||
- **Owns**: `Components/{KeyBar,TerminalContextMenu}.swift`、`Screens/TerminalScreen.swift`(增量)、测试
|
||||
- **Depends**: T-iPad-2 · **Parallel-safe**: T-iPad-4
|
||||
- **Steps(测试先行)**:
|
||||
@@ -159,7 +164,7 @@ enum SidebarItem: Hashable { case session(UUID), newSession, projects }
|
||||
- **Accept**: iPad 接键盘时 KeyBar 自动隐、去键盘复现;右键菜单在 iPad 生效;iPhone 无硬件键盘时 KeyBar 行为不变
|
||||
- **安全注**: 上下文菜单的 kill/开会话是既有 G/RO 通道的又一触发面——测试名标「复用 APIClient,无绕过」
|
||||
|
||||
#### T-iPad-4 · Projects/Timeline/sheet 大屏化 `[ ]` · ~1 pd
|
||||
#### T-iPad-4 · Projects/Timeline/sheet 大屏化 `[x]` · ~1 pd
|
||||
- **Owns**: `Screens/ProjectsScreen.swift`(增量)、Projects/Timeline 呈现方式(regular 下 `.sheet` → 列/`.presentationDetents` 或 sidebar section)、测试
|
||||
- **Depends**: T-iPad-2 · **Parallel-safe**: T-iPad-3
|
||||
- **Steps(测试先行)**:
|
||||
@@ -170,7 +175,9 @@ enum SidebarItem: Hashable { case session(UUID), newSession, projects }
|
||||
|
||||
### W3 · 验收(report-only, G4)
|
||||
|
||||
#### T-iPad-5 · iPad 验收 + iPhone 回归 + 安全核对 `[ ]` · ~0.5 pd
|
||||
#### T-iPad-5 · iPad 验收 + iPhone 回归 + 安全核对 `[~]` · ~0.5 pd
|
||||
- **已执行**:模拟器侧 PASS_WITH_FINDINGS(iPhone 16 / iPad Pro 11 双套件绿,iPhone 零回归硬门守住),4 个 findings(2 MED / 2 LOW)由 orchestrator 修完 4/4;iPad 分栏 sidebar+detail 截图确认。
|
||||
- **缺口**:① 真机 iPad(分栏手势 / 硬件键盘全键位 / 指针 hover 右键 / Stage Manager)**DEFERRED**,手工清单在 `PROGRESS_LOG.md`;② iPad happy-path XCUITest **接受推迟**(split 选中逻辑已由 `SidebarSelectionTests` 覆盖,单跑 7–11 min 且脆);③ **release ipa 层**核对未做(免费个人 team 无分发通道)——与 T-iOS-19 同一缺口,且 P2 新增的麦克风/语音识别 usage description 也应一并核。
|
||||
- **Depends**: T-iPad-2/3/4 · **Owns**: 无源码(report-only,findings 派回 owner)
|
||||
- **Steps**:
|
||||
- [ ] **F-iPad 走查**(§6 清单)逐条:分栏、横竖屏、Split View/Slide Over/Stage Manager 尺寸切换、硬件键盘/指针、终端更宽列数、遮罩覆盖 detail
|
||||
@@ -214,10 +221,10 @@ enum SidebarItem: Hashable { case session(UUID), newSession, projects }
|
||||
| SwiftTerm 在超宽 detail 的性能/选择手感 | 低 | 复用 iPhone 已测路径;真机目视 |
|
||||
| 多窗口诱惑导致 scope 膨胀 | 中 | 本期明确单场景(§0 非目标);多窗口另立计划 |
|
||||
|
||||
**待你拍板**:
|
||||
1. iPad 最低系统版本:iPadOS 17(与 iPhone 一致)还是抬到 18/26?
|
||||
2. 本期是否要指针右键上下文菜单(T-iPad-3 后半)——纯锦上添花,可砍到后续。
|
||||
3. 多窗口(拖会话开新窗并排两终端)确认放到下一期?
|
||||
**待你拍板** —— 三项均已落定(2026-07-30 按源码核对):
|
||||
1. ~~iPad 最低系统版本~~ **已定**:iPadOS **17**,与 iPhone 一致(`project.yml` 单一 `deploymentTarget: iOS 17.0`,无独立 iPad 下限)。
|
||||
2. ~~本期是否要指针右键上下文菜单~~ **已做**:`Components/TerminalContextMenu.swift`(复制选区 / 在 cwd 开新会话 / 结束会话,全部路由既有通道,kill 仍经 APIClient 带 Origin)+ `TerminalContextMenuTests`。
|
||||
3. ~~多窗口~~ **确认推迟**:本期单场景(§0 非目标);拖会话开新窗并排两终端仍未开工,另立计划。
|
||||
|
||||
**工作量合计**:W0 0.5 + W1 2 + W2(3∥4)2 + W3 0.5 ≈ **5 人日**(并行后墙钟更短)。
|
||||
|
||||
|
||||
@@ -146,4 +146,284 @@ A1 必须一次把**后续所有波次需要的 Info.plist 键**都加好(它
|
||||
|
||||
## 4. P2 RED 测试清单(由各 P2 builder 在开工第一步追加到本节)
|
||||
|
||||
<!-- C3 / C4 在此追加 T-iOS-31/33/34/35 的 RED 清单 -->
|
||||
### T-iOS-32 · Worktree 生命周期(create/prune/remove)+ `claude --resume` 历史 —— C2 追加
|
||||
|
||||
真源:`src/http/worktrees.ts` / `src/server.ts:1095-1183`、`docs/plans/w4-worktree-lifecycle.md`、
|
||||
web 参照实现 `public/projects.ts`(`validateBranchNameClient` :982、`confirmAndRemoveWorktree` :431、
|
||||
`confirmAndPruneWorktrees` :455、`renderNewWorktreeForm` :998、`newTabForResume` `public/tabs.ts:918`)。
|
||||
APIClient 侧 builder/状态码映射已在 B1 测过(125 tests),故以下全部是 **App 层归约**测试,
|
||||
文件 `ios/App/WebTermTests/WorktreeViewModelTests.swift` / `ResumeHistoryViewModelTests.swift`。
|
||||
|
||||
**A. 分支名校验(纯函数 `WorktreeBranchRule.validate`,逐条镜像 web 的 9 条规则)**
|
||||
1. RED:空串 → 非 nil 错误文案(不得放行到网络)。
|
||||
2. RED:长度 > 250 → 报错;== 250 → 通过。
|
||||
3. RED:含空格 / 控制字符(`\u{0}`…`\u{20}`、`\u{7f}`)→ 报错。
|
||||
4. RED:以 `-` 开头 → 报错(否则会变成 git flag)。
|
||||
5. RED:含 `..`、以 `.lock` 结尾 → 报错。
|
||||
6. RED:含 `~ ^ : ? * [ \` 任一 → 报错。
|
||||
7. RED:含 `@{` → 报错。
|
||||
8. RED:以 `/` 开头、以 `/` 结尾、含 `//` → 报错。
|
||||
9. RED:合法名(`feat/x`、`v1.2-fix`)→ nil。
|
||||
|
||||
**B. 创建 worktree(`POST /projects/worktree`,G)**
|
||||
10. RED:非法分支名 → **不发请求**(记录一次调用计数 == 0),只显示校验错误。
|
||||
11. RED:`.ok(CreateWorktreeResult)` → `phase == .created(path:branch:)`,且把服务器返回的
|
||||
canonical `path`/`branch` 当真相(不回显本地输入)。
|
||||
12. RED:`base` 留空 → 请求体的 `base` 为 nil(服务器读作「从 HEAD 切」),**不得**送 `""`。
|
||||
13. RED:`.rejected(status:403, message:"…")` → 原样显示服务器安全文案(不得吞、不得自造)。
|
||||
14. RED:`.rejected(status:500, message:nil)` → 兜底中文文案(非空、可读)。
|
||||
15. RED:`.rateLimited` → 专用「请稍后再试」文案,且**不自动重试**。
|
||||
16. RED:抛出的 `APIClientError.unauthorized` → 引导补令牌的文案(不与 500 混淆)。
|
||||
17. RED:创建进行中 `isBusy == true` 且重复提交只发一次请求。
|
||||
|
||||
**C. prune(`POST /projects/worktree/prune`,G,破坏性 → 需确认)**
|
||||
18. RED:未确认(`confirmPrune` 未调用)→ 零请求。
|
||||
19. RED:确认后 `.ok(pruned: [])` → 幂等文案「没有可回收的 worktree」,非错误态。
|
||||
20. RED:`.ok(pruned: [a,b])` → 文案含数量 2。
|
||||
21. RED:`.rejected(404, msg)` → 显示服务器文案。
|
||||
|
||||
**D. remove(`DELETE /projects/worktree`,G,两级确认)**
|
||||
22. RED:主 worktree(`isMain`)→ UI 不提供 remove(`canRemove == false`);locked 同样 false。
|
||||
23. RED:第一次确认 → 以 `force: false` 发一次请求。
|
||||
24. RED:`.rejected(409, msg)` → 进入 `.forceConfirming`(**不自动**重试),并带上服务器文案。
|
||||
25. RED:`.forceConfirming` 下用户取消 → 零第二次请求。
|
||||
26. RED:`.forceConfirming` 下用户确认 → 第二次请求 `force: true`。
|
||||
27. RED:`.rejected(400)`(非 409)→ 直接错误态,**不**进入 force 分支。
|
||||
|
||||
**E. `claude --resume <id>` 历史(`GET /sessions`)**
|
||||
28. RED:`GET /sessions` 成功 → 仅保留 cwd 落在本项目路径内的会话
|
||||
(`cwd == path` 或 `cwd.hasPrefix(path + "/")`),其余过滤掉。
|
||||
29. RED:路径前缀不得半路匹配:`/repos/web-terminal-old` 不属于 `/repos/web-terminal`。
|
||||
30. RED:服务器顺序(mtime 新→旧)保持不变,客户端不重排。
|
||||
31. RED:空结果 → `.empty` 而非 `.failed`(服务器 `~/.claude/projects` 缺失时回 `[]`)。
|
||||
32. RED:加载失败 → `.failed(可重试)`,`load()` 可重试成功。
|
||||
33. RED(**安全,web 侧缺失的校验**):`ProjectResumeCommand.bootstrapInput(sessionId:)`
|
||||
对 id 做 `[A-Za-z0-9._-]{1,128}` 白名单 —— `abc; rm -rf ~`、含空格/反引号/`$(`/`\n` 的 id
|
||||
→ 返回 nil(**绝不**拼进 PTY 命令行);合法 UUID stem → `"claude --resume <id>\r"`,
|
||||
结尾必须是 `\r`(0x0D,不是 `\n`)。
|
||||
34. RED:非法 id 的历史行 `canResume == false`(UI 不提供恢复按钮)。
|
||||
35. RED:cwd 非绝对路径的历史条目 → 不可恢复(`Validation.isAbsoluteCwd` 纪律)。
|
||||
|
||||
### 附:C2 git 面板(非 P2,但同批交付)RED 清单
|
||||
|
||||
`ios/App/WebTermTests/GitPanelPresentationTests.swift` / `GitPanelViewModelTests.swift`。
|
||||
真源 `docs/plans/w6-project-git-panel.md` + `public/projects.ts:615-745 makeSyncBand`。
|
||||
|
||||
36. RED:`sync == nil`(非 git 目录)→ 无同步带(nil),不发任何 git 请求。
|
||||
37. RED:`↑0 ↓0` + fetch 在 1h 内 → **唯一**允许的绿色「已同步」。
|
||||
38. RED:`↑0 ↓0` + `lastFetchMs` 超过 `FETCH_STALE_MS`(=3600_000) → **不绿**,`↓` 带「未核实」。
|
||||
39. RED:`lastFetchMs == nil`(从未 fetch)→ 视为 stale,不绿。
|
||||
40. RED:`upstream == nil` → 「无上游」,无 `↑↓`,**不绿**(最易犯的 bug)。
|
||||
41. RED:`detached == true` → 「分离 HEAD」,无 `↑↓`,fetch 按钮禁用(`canFetch == false`)。
|
||||
42. RED:`ahead == nil`(读不到)→ 显示 `↑ —` 而非 `↑ 0`,且不绿。
|
||||
43. RED:`dirtyCount == nil` → 「未检查」,**不是** clean;`0` → clean;`3` → 3。
|
||||
44. RED:git log 未推送边界:`upstream != nil` 时边界画在最后一个 `unpushed == true` 之后,
|
||||
且只画一次;`unpushed` 只在严格 `true` 时生效。
|
||||
45. RED:`upstream == nil` → 完全不画边界。
|
||||
46. RED:未推送提交排在已推送之下(老日期 merge)→ 边界仍在最后一个 unpushed 之后(Set 为准,非行号)。
|
||||
47. RED:stage/commit/push/fetch 的 `.rejected` 一律把服务器 `error` 原样显示;`nil` message → 兜底文案。
|
||||
48. RED:commit 成功 → 清空输入框 + 显示短 sha;`.rejected(409)`(无暂存)→ 保留输入框内容。
|
||||
49. RED:push `.rejected(401)` → 「主机上的 git 凭据」文案,**不**与访问令牌 401 混淆
|
||||
(`unauthorizedPolicy == .routeDefined`,§1.1)。
|
||||
50. RED:任一写操作进行中 → `isBusy`,重复点击只发一次。
|
||||
51. RED:重命名文件 stage → 请求体同时含 `oldPath` 与 `newPath`(镜像 web)。
|
||||
|
||||
### T-iOS-33 · 终端内搜索 —— C3 追加
|
||||
|
||||
真源:SwiftTerm 1.13.0 已带搜索 API(`TerminalViewSearch.swift`:`findNext(_:options:scrollToResult:)`、
|
||||
`findPrevious(...)`、`clearSearch()`,`SearchOptions(caseSensitive:false, regex:false, wholeWord:false)`
|
||||
默认值与 xterm.js search addon 一致)。web 参照 `public/search.ts`(Enter=next / Shift+Enter=prev /
|
||||
Esc=关闭并 `hooks.clear()`;`public/terminal-session.ts:547-553` 把三个 hook 落到 SearchAddon;
|
||||
`style.css:812` searchbox 固定在**右上**)。文件 `ios/App/WebTermTests/TerminalSearchTests.swift`。
|
||||
|
||||
**A. 查询提交策略(纯函数 `TerminalSearchQuery.isSubmittable`)**
|
||||
1. RED:空串 → 不可提交,且**零引擎调用**(不把空词丢给 SwiftTerm)。
|
||||
2. RED:单个空格 `" "` → **可**提交(镜像 web:`input.value` 原样进 `findNext`,空格是合法搜索词)。
|
||||
3. RED:`" foo "` → 原样传(**不 trim、不改大小写**,逐字符等于用户输入)。
|
||||
|
||||
**B. 方向与引擎调用(`TerminalSearchModel` over fake `TerminalSearching`)**
|
||||
4. RED:`find(.next)` → 恰一次 `searchNext(term:)`,term 逐字符等于 query,零 `searchPrevious`。
|
||||
5. RED:`find(.previous)` → 恰一次 `searchPrevious`,零 `searchNext`。
|
||||
6. RED:引擎回 true → `outcome == .found`。
|
||||
7. RED:引擎回 false → `outcome == .notFound`,且有非空「无匹配」文案。
|
||||
8. RED:连续三次 `find(.next)` → 三次引擎调用(搜索游标由 SwiftTerm 自己推进,客户端不缓存)。
|
||||
9. RED:未 attach 引擎(`searcher == nil`)→ 不崩、`outcome` 保持 `.idle`(预览/测试环境)。
|
||||
|
||||
**C. 打开/关闭生命周期**
|
||||
10. RED:初始 `isPresented == false` 且 `outcome == .idle`。
|
||||
11. RED:`present()`→true,`dismiss()`→false。
|
||||
12. RED:`dismiss()` → 恰一次 `searchClear()`(镜像 web `hide() → hooks.clear()`)+ 清空 query + `outcome == .idle`。
|
||||
13. RED:`present()` **不**调用任何引擎方法(开面板 ≠ 搜索)。
|
||||
14. RED:改 query → `outcome` 复位 `.idle`(旧的「无匹配」不得挂在新词上)。
|
||||
|
||||
**D. 不变式:搜索是纯读**
|
||||
15. RED:整个搜索流程后真 `TerminalViewModel.forwardedSendCount == 0`(零 PTY 字节,byte-shuttle 不变)。
|
||||
16. RED:终端已 `.exited`(read-only)时搜索照常可用(`find` 仍打引擎)。
|
||||
|
||||
**E. SwiftTerm 绑定(Accept:命中并高亮)**
|
||||
17. RED:真 `KeyCommandTerminalView` feed 一段文本后 `searchNext(term:)` 命中 → 返回 true **且**
|
||||
`hasActiveSelection == true`(= SwiftTerm 选区高亮);搜不存在的词 → false。
|
||||
18. RED:`searchClear()` → `hasActiveSelection == false`(高亮清掉)。
|
||||
|
||||
### T-iOS-31 · 语音 PTT + 确认 —— C3 追加
|
||||
|
||||
真源(三件套逐条移植 web,plan §7「端口匹配器 / 1.5s 撤销 / epoch 防误发」= port `voice-commands.ts` 的匹配器):
|
||||
`public/voice.ts`(PTT 生命周期、`autoSend:false` 默认 = **不自动回车**)、
|
||||
`public/voice-commands.ts`(整句精确匹配器 + 否定守卫 + `MIN_APPROVE_CONFIDENCE=0.6`)、
|
||||
`public/voice-confirm.ts`(`DEFAULT_WINDOW_MS = 1500` 可撤销窗口)、
|
||||
`SessionCore/GateState.swift`(epoch 防陈旧决策的既有先例 `canDecide(epoch:)`)。
|
||||
文件 `ios/App/WebTermTests/VoicePTTTests.swift`。
|
||||
|
||||
**A. 转写清洗(安全边界,纯函数 `VoiceTranscript`)**
|
||||
19. RED:普通文本 → 首尾 trim 后原样。
|
||||
20. RED:含 `\r`(0x0D)→ **剥除**(口述绝不自己回车执行命令;等价 web `autoSend:false`)。
|
||||
21. RED:含 `\n`/`\t`/ESC `\u{1b}`/其它 C0-C1 控制字符 → 全部剥除。
|
||||
22. RED:多行 → 折成单行(换行变空格 + 合并连续空白)。
|
||||
23. RED:超 `maxLength` → 截断到上限。
|
||||
24. RED:清洗后为空 → `isInjectable == false`(不进确认态)。
|
||||
|
||||
**B. 匹配器移植(`VoiceCommandMatcher`,逐条镜像 `voice-commands.ts`)**
|
||||
25. RED:无 held gate → 任何话语都是 `.text`(含「确认」)。
|
||||
26. RED:gate 是 `.plan` → `.text`(v1 只作用 tool gate)。
|
||||
27. RED:tool gate + 「确认」/「批准」/`ok`/`go ahead` → `.approve`。
|
||||
28. RED:归一化(小写 + 去标点 + 合并空白):`OK.`/「好的!」命中;`don't` → `dont`。
|
||||
29. RED:**整句精确**:「确认一下这个」→ `.text`(绝不子串匹配,否则顺口一句就批了 shell)。
|
||||
30. RED:tool gate + 「拒绝」/「取消」/`stop` → `.reject`。
|
||||
31. RED:否定守卫:「不批准」→ `.reject`;「不清楚」→ `.text`;`now` **不**被 `no` 前缀否定 → `.text`。
|
||||
32. RED:置信度门:`.approve` 且 confidence < 0.6 → 降级 `.text`;`.reject` 不受置信度影响。
|
||||
33. RED:空/纯标点话语 → `.text`。
|
||||
|
||||
**C. 确认 + 1.5s 撤销窗口(FakeClock,零真睡)**
|
||||
34. RED:`pressDown()` → `.listening`;partial 回调更新 `.listening(partial:)`。
|
||||
35. RED:`pressUp()` 得空转写 → `.idle` + `lastResolution == .empty`,**零注入**。
|
||||
36. RED:`pressUp()` 得有效转写 → `.confirming(...)`,**零注入**(「确认前绝不注入」)。
|
||||
37. RED:`.confirming` 下 `cancel()` → `.idle` + `.discarded` + 零注入。
|
||||
38. RED:`confirm()` → `.undoWindow`,此刻**仍零注入**。
|
||||
39. RED:时钟 +1.4s → 仍零注入;到 1.5s → 恰一次注入,内容 == 清洗后的转写,`lastResolution == .committed(.text(...))`。
|
||||
40. RED:`.undoWindow` 内 `undo()` → 零注入 + `.undone`;再推进 10s **也不**注入(任务已取消)。
|
||||
41. RED:注入内容**不以 `\r` 结尾**(用户自己按 ⏎ —— 误听绝不自动执行)。
|
||||
42. RED:非 `.confirming` 态调 `confirm()` → 无副作用。
|
||||
43. RED:重复 `confirm()` → 只 arm 一次、只注入一次。
|
||||
|
||||
**D. epoch 防误发(两道闸)**
|
||||
44. RED:口述→确认之间 epoch 变了 → `confirm()` 直接丢弃:零注入 + `.staleEpoch`。
|
||||
45. RED:epoch 在**撤销窗口内**变(confirm 已过、1.5s 内切会话)→ 到期仍零注入 + `.staleEpoch`。
|
||||
46. RED:epoch 不变 → 正常注入(防止闸门过严的回归保护)。
|
||||
47. RED:口述时 sessionId 还没 adopt(nil)、确认时已 adopt → 视为变化 → 零注入。
|
||||
48. RED:`VoiceEpochPolicy.invalidates`:`.reconnecting` → true(重连后服务器可能给的是新 PTY,
|
||||
客户端分辨不了 → 走安全方向);`.connecting`/`.none` → false。`VoiceEpochSource` 累加 generation。
|
||||
|
||||
**E. 权限与失败路径**
|
||||
49. RED:麦克风授权被拒 → `.denied(.microphone)`,文案指向 设置→隐私→麦克风;零录音零注入。
|
||||
50. RED:语音识别授权被拒 → `.denied(.speech)`,文案指向语音识别开关。
|
||||
51. RED:识别器抛错 → `.failed(message:)`,零注入,可再按重试。
|
||||
52. RED:终端只读(`.exited`)→ `pressDown()` 拒绝(`.readOnly`),**不启动录音**(`startCount == 0`)。
|
||||
|
||||
**F. 键栏集成(KeyBar 零回归)**
|
||||
53. RED:不提供语音闭包 → 按钮数 == `KeyBarLayout.buttons.count`(17),无麦克风键。
|
||||
54. RED:提供语音闭包 → 末尾多一个 🎤/「语音」键,前 17 键顺序与 accessibilityLabel **完全不变**。
|
||||
55. RED:麦克风键 touchDown → `onVoiceDown` 恰一次;touchUpInside → `onVoiceUp` 恰一次;
|
||||
**绝不**经 `onKey`(麦克风不映射任何 `KeyByteMap` 字节)。
|
||||
56. RED:匹配到 `.approve` 且注入了 gate 桥 → 走同一 1.5s 窗口,到期调 `decide(.approve)` 而**不**注入文本。
|
||||
|
||||
### T-iOS-34 · 主题(跟随系统/深色/浅色)+ Dynamic Type —— C4 追加
|
||||
|
||||
真源:`Wiring/RootView.swift:30` 今天**硬锁** `.preferredColorScheme(.dark)`(无浅色通路);
|
||||
`DesignSystem/Tokens.swift` 已声明浅色 accent `#C9892F`(web `--accent-2`)但状态色/时间线色/
|
||||
`accentSoft`/终端画布仍是**单值深色专用**;终端主题只在 `Screens/TerminalScreen.swift:223-235`
|
||||
`makeUIView` 时套一次。web 参照 `public/settings.ts`(`DEFAULT_SETTINGS.theme='dark'`、
|
||||
`THEMES.light = {background:'#f6f7f9', foreground:'#1a1d24'}`)。
|
||||
文件 `ios/App/WebTermTests/AppThemeTests.swift` / `DynamicTypeLayoutTests.swift`。
|
||||
|
||||
**A. 主题模型(纯值 `AppTheme`)**
|
||||
1. RED:`.system.colorScheme == nil`(= 交给系统),`.dark → .dark`,`.light → .light`。
|
||||
2. RED:三档 label / SF Symbol 各自非空且**互不相同**(选择器要能区分)。
|
||||
3. RED:`AppTheme.allCases` 顺序 = 跟随系统 → 深色 → 浅色(选择器顺序即此顺序,UI 不重排)。
|
||||
4. RED:`AppTheme(rawValue:)` 白名单:`"system"/"dark"/"light"` 命中,其它(含空串、`"Dark"`)→ nil。
|
||||
|
||||
**B. 持久化(`AppThemeCodec` 纯函数 + `ThemeStore` over fake defaults)**
|
||||
5. RED:`decode(nil)`(从未设置)→ **`.dark`** —— 默认必须等于今天硬锁的深色(零回归)。
|
||||
6. RED:`decode("solarized")` / `decode("")` / `decode("DARK")` → `.dark`(未知值不崩、不落 system)。
|
||||
7. RED:三档 `encode → decode` 往返恒等。
|
||||
8. RED:`ThemeStore` 空 defaults → `.dark`,且**不写盘**(首次读不产生写)。
|
||||
9. RED:`select(.light)` → `theme == .light` 且 defaults 落 `"light"`;同一 defaults 新建 store → `.light`(跨启动)。
|
||||
10. RED:`select` 同一档两次 → 只写一次(不产生多余写)。
|
||||
11. RED:defaults 里被塞脏值 → store 读作 `.dark`,并**不**清空用户其它键。
|
||||
|
||||
**C. 有效配色解析(`AppTheme.resolvedScheme(system:)`)**
|
||||
12. RED:`.system` + 系统 `.light` → `.light`;`.system` + 系统 `.dark` → `.dark`(透传)。
|
||||
13. RED:`.dark`/`.light` 无视系统值 → 强制自身。
|
||||
|
||||
**D. 浅色通路的 token 审计(`UIColor.resolvedColor(with:)` 逐档解析)**
|
||||
14. RED:`statusWorking`/`statusWaiting`/`statusStuck`/`timelineTool`/`timelineUser` 在 **light** 与
|
||||
**dark** 下解析值**不同**(即:真的有浅色变体,不是解锁开关就完事)。
|
||||
15. RED:深色档的值**逐字节不变**(#46D07F / #F5B14C / #FF6B6B / #5E9EFF / #AF7BFF)—— 深色零回归。
|
||||
16. RED:上述 5 色在**两档**下相对该档 `systemBackground` 的 WCAG 对比度 **≥ 3:1**
|
||||
(非文本 UI 组件门槛,1.4.11)。今天的 `#46D07F`/`#F5B14C`/`#FF6B6B` 在白底只有
|
||||
1.96/1.60/2.74:1 —— 这是 RED 的核心。
|
||||
17. RED:critical 三色(working/waiting/stuck)在 **light** 档仍两两可辨(不因变暗而糊成一片)。
|
||||
18. RED:`accentSoft` 两档不同,且**都仍是 wash**(alpha < 0.3,不许变成实心块)。
|
||||
19. RED:`onAccent` 两档**相同**(金色填充在两档都要深色墨,故它是固定值),且与 accent 对比 ≥ 4.5:1。
|
||||
|
||||
**E. 终端画布跟随主题(`TerminalPalette`)**
|
||||
20. RED:`colors(for: .dark).background == #100F0D`、`.foreground == #ECE9E3`(桌面 web `--bg/--text`,零回归)。
|
||||
21. RED:`colors(for: .light).background == #F6F7F9`、`.foreground == #1A1D24`(镜像 web `THEMES.light`)。
|
||||
22. RED:两档 fg↔bg 对比度 ≥ 7:1(终端是正文,AAA 门槛)。
|
||||
23. RED:caret / selection 用该档 accent 解析值(不写死深色档的金)。
|
||||
24. RED:`DS.Palette.terminalBackgroundUIColor()` 是**动态** UIColor:light/dark 解析值不同
|
||||
(今天是单值常量 → RED)。
|
||||
|
||||
**F. Dynamic Type 不破版(`UIHostingController.sizeThatFits` / UIKit `systemLayoutSizeFitting`,AX5)**
|
||||
25. RED:`GateBanner` 在 320pt 宽容器、`.accessibility5` 下:宽度 ≤ 320(无横向溢出)、
|
||||
高度有限且 > 标准字号高度(= 真的长高而不是被裁)。
|
||||
26. RED:`TelemetryChip`(`lineLimit(1)` 的等宽数字)在 AX5 下宽度 ≤ 320,且**AX2 与 AX5 同高**
|
||||
(`DS.Typography.numericClamp` 夹取生效 —— 表格数字不许炸版;落地时把原计划的
|
||||
"增幅 ≤ 2.2×"换成了这条更强、更确定的等式断言)。
|
||||
27. RED:`dsMetaText()` 的元信息行在 AX5 下同样受夹取(与 26 同一策略,单一出处)。
|
||||
28. RED:`DSButtonStyle` 在 AX5 下高度 ≥ `DS.Layout.minHitTarget` 且标签可换行不横向溢出。
|
||||
29. RED(**外部件度量,越界只报不改**):AX5 下键帽两行所需高度 vs 固定 `barHeight`(44+8)。
|
||||
实测 **108.24pt vs 52pt → 裁切**(footnote 行高 52.51 + caption2 行高 47.73 + 内衬 8)。
|
||||
`Components/KeyBar.swift` 不在 C4 Owns,故以 `withKnownIssue` 记为已知缺口(修好后用例会
|
||||
报 "known issue was not recorded",提醒删标记)。
|
||||
注:不能用 `performAsCurrent { KeyBarView() }` 量 —— `UIFont.preferredFont(forTextStyle:)`
|
||||
读 App 级 content size category,不看 `UITraitCollection.current`,那样量出的是默认字号
|
||||
(38pt)的**假绿**;改用 `compatibleWith:` 取真实 AX5 行高。
|
||||
|
||||
### T-iOS-35 · web 分享 QR(`?join=`)互通 —— C4 追加
|
||||
|
||||
真源:`public/share.ts:55` 的 URL 形状 **`${location.origin}/?join=${sessionId}`**;
|
||||
`src/server.ts` 的 `GET /?join=<id>`;`DeepLinkRouter.swift:56-65` 今天只认 `webterminal://open`,
|
||||
http(s) 一律 `.ignore`(`DeepLinkRouterTests.swift:84` 钉住了它 —— 本任务**有意**改写该用例)。
|
||||
主机身份仍**只**经 `HostStore` 解析(`HostEndpoint.originHeader` 是冻结的唯一 origin 派生点)。
|
||||
文件 `ios/App/WebTermTests/DeepLinkRouterTests.swift`(增补 + 有意改写 1 例)。
|
||||
|
||||
**A. 接受 web 分享形状**
|
||||
30. RED:`http://192.168.1.5:3000/?join=<v4>` → `.joinShared(origin:"http://192.168.1.5:3000", sessionId:)`。
|
||||
31. RED:`https://mac.ts.net/?join=<v4>` → origin 省略默认端口(`:443` 不出现)。
|
||||
32. RED:`HTTP://192.168.1.5:3000/?join=<v4>`(大写 scheme/host)→ origin 归一化为小写。
|
||||
33. RED:path 为 `""` 与 `"/"` 都接受(`location.origin + "/?join="` 产出 `/`)。
|
||||
|
||||
**B. 白名单纪律(http(s) 是比自定义 scheme 大得多的输入面,故**更严**)**
|
||||
34. RED:`join` 值非 v4(`Validation.isValidSessionId` 判定)→ `.ignore`。
|
||||
35. RED:重复 `join` 键 → `.ignore`(歧义输入绝不部分应用)。
|
||||
36. RED:**多余 query 键**(`/?join=<v4>&x=1`)→ `.ignore`(web 形状精确只有一个键)。
|
||||
37. RED:非空 path(`/manage.html?join=<v4>`)→ `.ignore`。
|
||||
38. RED:带 fragment(`/?join=<v4>#x`)→ `.ignore`。
|
||||
39. RED:带 userinfo(`http://u:p@host/?join=<v4>`)→ `.ignore`(二维码钓鱼形状)。
|
||||
40. RED:非 http(s) scheme(`ftp://`、`file://`、`javascript:`)→ `.ignore`。
|
||||
41. RED:`?join=` 但**无 host**(`http:///?join=<v4>`)→ `.ignore`。
|
||||
42. RED(**有意改写既有用例**):`https://open?host=<v4>&join=<v4>` 仍 `.ignore`
|
||||
—— 但理由从"scheme 不是 webterminal"变成"web 形状只许**单一** `join` 键"。
|
||||
原用例名/注释同步改写,避免留下已失效的断言语义。
|
||||
43. RED:`webterminal://open?host=&join=` 等既有拒绝路径**逐条不变**(自定义 scheme 分支零回归)。
|
||||
|
||||
**C. 解析主机:只认已配对(二维码是不可信输入,绝不静默配对)**
|
||||
44. RED:origin 命中已配对主机 → `openSession(host, sessionId)` 恰一次,无 hint。
|
||||
45. RED:origin 未配对 → **零** open、`showPairing` 一次、hint == `DeepLinkCopy.unknownHostHint`
|
||||
(**不得**据链接内容自动新增主机)。
|
||||
46. RED:hint 文案**不回显**链接内容(不含 origin 串、不含 sessionId)—— 防钓鱼/防日志注入。
|
||||
47. RED:origin 比较走 `HostEndpoint.originHeader`:`http://host:3000` 与 `http://HOST:3000/` 视为同一主机;
|
||||
`http://host:3001`(端口不同)**不是**同一主机。
|
||||
48. RED:`https://host/` 与 `http://host/`(scheme 不同)**不是**同一主机。
|
||||
49. RED:冷启动 stash 对 `.joinShared` 同样生效(ready 前 handle → markReady 后恰应用一次)。
|
||||
50. RED:非法 web 链接 → `ignoredCount + 1`,且**不入 stash**(与既有 `.ignore` 同一通路)。
|
||||
|
||||
Reference in New Issue
Block a user