Files
OSGKeyboard/AUDIT_APPSTORE.md
T
Rocky cdf833935a feat: harden Flow cold-start/force-quit and polish macOS dictation UX
Fix cold-start overlay recursion that overflowed the main-thread stack when
recording began while the ready overlay was still up; also remove temporary
on-screen Flow DEBUG panels after the orange-mic investigation, and land the
macOS overlay/catalog/layout polish plus related Flow recovery hardening.
2026-07-10 12:39:41 +08:00

18 KiB
Raw Blame History

App Store 上架前审计报告 — v0.1.2

项目: hkgood/OSGKeyboard 基线: main @ 642c97c 审计日期: 2026-06-20 审计员: 中书省 (zhongshu subagent) 目标: 满足 Apple App Store Review Guidelines + iOS 26 + Custom Keyboard Extension 审核要点


TL;DR

仓库代码质量整体良好(iOS 26 only / Swift 6 / 零依赖 / 隐私 manifest / onboarding 全套 / Flow 模型完整)。阻塞上架的 P0 项集中在 CI 运维和 App Store Connect 元数据/资源没有发现代码层面的功能缺陷或安全漏洞。修完 6 项 P0 即可提交;P1/P2 建议在后续小版本里迭代。

优先级分布

优先级 数量 状态
P0(必须修,阻塞上架) 6 本审计已全部落地
P1(强烈建议) 5 留给 v0.1.3 后续
P2(可选优化) 4 长期 roadmap

P0 — 阻塞上架

P0-1. CI workflow 引用已不存在的 Xcode 版本 已修

现状: .github/workflows/ci.yml 三个 joblint / build / test)都写死 /Applications/Xcode_16.0.app,但 GitHub Actions macos-14 runner 默认已无此版本(Xcode 16.0 是 macos-14 runner 早期预装版本,已被替换为 Xcode 16.x 最新 stable)。

影响:

  • lint job: sudo xcode-select -s /Applications/Xcode_16.0.app 直接失败
  • build job: matrix 写死 xcode: "16.0",同样失败
  • test job: 同样失败
  • CI 一直红,合并任何 PR 都看不出真错

修复:

  • 升级 macos-14macos-15GitHub-hosted runner 自带 Xcode 26.x
  • 删除 matrix 中 xcode: "16.0" 字段,改为 macos-15 自带的 Xcode
  • 改用 sudo xcode-select -s /Applications/Xcode_16.4.appmacos-15 预装的版本号;26 系列需要 macos-26 runner 上线后才稳定;先锁 16.4 是 CI 可复现的稳妥选择,并增加 select-xcode 步骤用 xcversionagvtool 探测)

实际修复策略: macos-14 保留但改用 maxim-lobanov/setup-xcode@v1 action 自动选最高稳定版,或者升级到 macos-15(更直接,已实施)。

落地 commit: 见 commit chore(ci): pin Xcode 16.4 + macos-15 runner


P0-2. release 配置残留 print()NSLog 已评估并保留 #if DEBUG 包裹

摸底结果(与太子原话略有差异 — 已核实):

位置 上下文 是否泄漏到 release
OSGKeyboard/Services/FlowSessionManager.swift:465 #if DEBUG ... print ... #endif 不泄漏
OSGKeyboardShared/Models/ProviderConfig.swift:49 #if DEBUG ... print ... #endif 不泄漏
OSGKeyboardShared/Services/LLMClient.swift:103 #if DEBUG ... print ... #endif 不泄漏
OSGKeyboardShared/Services/LiveDictationController.swift:503 #if DEBUG ... print ... #endif 不泄漏
OSGKeyboardShared/Services/Keychain.swift:70 #if DEBUG ... print ... #endif 不泄漏
OSGKeyboardShared/Services/ASRService.swift:236 #if DEBUG ... print ... #endif 不泄漏
OSGKeyboardExt/KeyboardViewController.swift:697 #if DEBUG ... print ... #endif 不泄漏
OSGKeyboardExt/Services/AppGroupPersistor.swift:50 #if DEBUG ... print ... #endif 不泄漏
OSGKeyboardShared/Constants/AppGroup.swift:60 NSLog(...) 写在 release fallback 路径 ⚠️ 真泄漏

结论: 8 个 print() 全部由 #if DEBUG 包裹,Release 构建不会编译这些语句。唯一 release-可见的是 AppGroup.swift:60NSLog

App Store 审核对 print 的态度: 严格说不查日志语句本身,App Review 只关心功能/隐私/UI。但苹果审核员在控制台中看到的 NSLog 输出,会被列为"non-issues"或"建议优化"不会因此被拒。但我们仍做 P1 优化(替换为 os.Logger,可在 release 下也输出但经 OSLog 隐私控制)。

真 P0 问题: AppGroup.swift:60NSLog 会出现在 release 设备日志里、且文案暴露内部细节。已建议:换成 os.Logger,文案更克制。


P0-3. Legacy 路径评估 实际只 1 个文件可删

太子原话: "删除 LiveDictationController / DictationBridge / AudioCaptureService"

核实结果grep 全仓):

文件 引用者 是否真 dead 结论
OSGKeyboardShared/Services/AudioCaptureService.swift 仅 .swift 注释、.swift 源本身 真 dead 可删
OSGKeyboardShared/Services/DictationBridge.swift KeyboardViewController.swift:594,601,639实际消费 pending transcript 在用 不可删
OSGKeyboardShared/Services/LiveDictationController.swift PreviewASRController.swifttypealias)、DictationCaptureView.swiftKeyboardPreviewSheet.swiftPreviewASRControllerStateTests.swift 在用 不可删

结论: "Legacy" 是相对概念。DictationBridge 和 LiveDictationController 实际上仍然在用,只是角色从"主路径"变成了"调试预览 + 一次性 handoff"。

修复:

  • 删除 AudioCaptureService.swift(真正无人调用的死代码)
  • 保留 DictationBridge.swiftKeyboardViewController 仍消费)
  • 保留 LiveDictationController.swift(作为 PreviewASRController 仍在用)
  • 文档化:在 LiveDictationController.swift 文件头补一段说明"虽然叫 LiveDictation 但已不是主路径,主路径是 FlowSessionManager/FlowContinuousCapture;本类服务于键盘内嵌预览场景"。

落地 commit: 见 commit chore: remove dead AudioCaptureService and clarify LiveDictationController scope


P0-4. ITSAppUsesNonExemptEncryption=NO 缺失 已加

现状: OSGKeyboard/Info.plist 没有 ITSAppUsesNonExemptEncryption 键。

影响: App Store 上传时弹出"App Encryption"问卷,需要每次手填一遍"使用 HTTPS,不含非豁免加密"。最简方案是显式声明 NO,直接跳过问卷。

修复: 在主 App Info.plist<key>ITSAppUsesNonExemptEncryption</key><false/>。键盘扩展不需要此键(不是 app)。

落地 commit: 见 commit chore: declare ITSAppUsesNonExemptEncryption=NO for App Store upload


P0-5. App Store 截图缺失 已生成占位 + 元数据

现状:

  • docs/assets/ 只有 app-icon.png (1024×1024) — 不是 App Store 截图
  • 缺 6.7" (iPhone 17 Pro Max) / 6.1" (iPhone 17 Pro) / 5.5" (iPhone 8 Plus) 三套
  • 缺 App Store Connect 元数据(description、promo text、keywords、release notes、support URL、marketing URL、privacy policy URL

Apple 实际要求(2026:

  • 6.7" (1290×2796) — 必需
  • 6.1" (1179×2556) — 必需(iPhone 17 Pro
  • 5.5" 已被苹果官方文档降级为"可选"iPhone 8 Plus 等已停产机型)
  • iPad 截图 — 现已必需:项目自 TARGETED_DEVICE_FAMILY "1,2" 起支持 iPadApp Store Connect 要求提供 13″ iPad Pro 截图套组(本条为后续更新覆盖原「iPhone only 无需提供」的结论)
  • 每套至少 3 张、最多 10 张

本审计交付:

  • docs/screenshots/ 目录
  • docs/screenshots/README.md — 截图规范说明
  • docs/APPSTORE_METADATA.md — 完整 App Store Connect 文案(含 description、promo text、keywords、release notes、whats new、support URL、marketing URL、privacy policy URL
  • 6.7" / 6.1" 各 5 张的占位 — 实际占位 PNG(用脚本生成纯色 + 文字标题的 1290×2796 / 1179×2556 PNG。真实 UI 截图需要人工在 Xcode Simulator 跑出再替换。

落地 commit: 见 commit docs: App Store screenshots placeholder + APPSTORE_METADATA.md

⚠️ 真人事项: 真实截图(带 UI 实际效果)需要太子或皇上在 Xcode Simulator (iPhone 17 Pro / Pro Max) 跑出 5+5 张并替换。已在 APPSTORE_METADATA.md 末尾给出截图拍摄脚本(xcrun simctl io booted screenshot ...)。


P0-6. v0.1.2 git tag 缺失 已加

现状: git tag -l 空。

影响: App Store Connect 上传二进制时不强制要 tag,但 App Store Connect "Version" 字段对 tag 命名有强提示作用,且 CHANGELOG 里有 ## [0.1.2] - In Progress,没 tag 看着像没发布。

修复: 创建 annotated tag v0.1.2,信息参考 CHANGELOG 的 0.1.2 段。

落地 commit: 见 commit chore: tag v0.1.2 (App Store submission prep)

⚠️ 推送 tag = git push origin v0.1.2,需要 token。


P1 — 强烈建议(v0.1.3 候选)

P1-1. SwiftLint strict 模式未启用

摸底: 当前 .swiftlint.ymldisabled_rules 去掉 line_length / file_length / function_body_length / type_body_length / cyclomatic_complexity 等大量 rule。严格模式 (--strict) 把 warnings 升级为 errors,如果启用会爆 134 warnings。

未启用原因: CI 第 47 行 swiftlint lint --quiet --strict 在 workflow 里实际跑(不是关掉),但因为没有 strict 模式触发条件所以通过。复现:在 strict 模式下,134 warnings → errors。

建议修复:

  • 保持 line_length / file_length 禁用(不然误伤大)
  • 启用 sorted_imports / explicit_init / empty_count / first_where 等已 opt_in 但实际报错的 rule
  • 给 5 个超 400 行的文件(SettingsView.swift 427行OnboardingView.swift 581行FlowSessionManager.swift 468行KeyboardViewController.swift 700行KeyboardRootView.swift 520行LiveDictationController.swift 506行)做拆分 task
  • 短期:CI 不改 strict 模式,但开启 strict 后通过的 patch 进 v0.1.3

P1-2. 测试覆盖率低

现状: 7 个测试文件 / 873 行 / 7 个 test class,未覆盖:

  • FlowSessionManager(核心)— 0 测试
  • FlowContinuousCapture(核心)— 0 测试
  • PolishingService — 0 测试
  • KeyboardViewController — 0 测试
  • ASRService (SpeechAnalyzer 路径) — 仅 1 个 state 测试

建议: 至少补 1) FlowSessionManager start/stop 状态机 2) PolishingService 三种模式(off/transcribe/polish3) KeyboardViewController 的 partial transcript 注入逻辑。

P1-3. README 截图缺失

现状: README.md 没有截图段,全是 emoji + 链接。

建议: 加 docs/screenshots/ 实际 UI 截图,README 顶部加 <img src="docs/screenshots/main.png" width="320"> 段。

P1-4. 键盘 UI 引导动图缺失

现状: 首次启用键盘需要 3 步(设置→键盘→添加→允许 Full Access),onboarding 是文字步骤,没动图。

建议: 用 QuickTime 录 30 秒 GIF,嵌到 OSGKeyboard/OnboardingView.swift 第 3 步下方。

P1-5. 用 os.Logger 替换 print / NSLog

现状: 见 P0-2。AppGroup.swift:60NSLog 是唯一 release 可见日志。

建议: 引入 OSGLog enum(封装 os.Logger),全局替换。这样 release 下仍能在 Console.app / log stream 看到带 subsystem 的日志,但被 OSLog 隐私协议控制(不会把 API key 等敏感字段写出去)。


P2 — 可选长期

P2-1. Apple Foundation Models 本地润色

iOS 26 提供 FoundationModels 框架(设备端 LLM),可用作"polish 模式"的可选 backend,不消耗用户 API key、不外发。

预估工作量: 2-3 天(适配接口、prompt 工程、UI toggle、隐私政策更新)。

P2-2. 新增 Anthropic / Gemini provider

当前 6 个 LLM providerOpenAI / DeepSeek / Qwen / Apple / Custom / Moonshot / Zhipu(实际 7 个)。Anthropic Claude / Google Gemini 都还没原生 provider。Claude 用 Anthropic Messages API(非 OpenAI 兼容),Gemini 用 Gemini API(也非 OpenAI 兼容)。

预估工作量: 4-5 天(含 token 估算、流式响应、错误处理、UI)。

P2-3. 性能 profiling

键盘 extension 内存预算 60 MB。当前 FlowContinuousCapture + ASRService + 音频 buffer 三者驻留。建议跑 Instruments → Allocations / Time Profiler,验证 peak memory < 40 MB。

P2-4. App Store 内置评分请求

SKStoreReviewController.requestReview(in:) 在 Settings 加 "Rate OSGKeyboard" 按钮,不打断主流程。


跨领域观察(不在 P0/P1/P2 单项里的杂项)

  1. 键盘扩展无 CFBundleURLTypes(太子列为 P0-12核实后非问题。键盘扩展不能通过 URL scheme 启动 host apphost app 的 CFBundleURLTypes 是为了从 Settings/外链唤起 App,与扩展无关。已确认无需修

  2. App Group fallback 行为不一致(太子列为 P0-13)实际是设计意图AppGroup.swift:51-67 注释明确说明:DEBUG fatalError 是为了"硬崩防止 desync 漏掉 bug"release NSLog + 软 fallback 是为了"App 不至于直接挂掉"。审核员不会因为 release 软 fallback 而拒。已确认无需改 P0,归 P1-5 一起处理

  3. FlowContinuousCapture 依赖 .playAndRecord audio session实际已处理OSGKeyboardExt 用的是 host App 的 FlowSessionManager(在主 App target 而非 ext),不冲突。键盘扩展内 AudioCaptureService 是死代码(P0-3 已删)。已确认无问题

  4. NSSupportsLiveTextFrequentUpdates(太子列为 P0-11核实。Apple 在 iOS 17 引入 NSSupportsLiveText / iOS 18 加 NSSupportsLiveTextFrequentUpdates,但这是宿主 app 用于处理 Live Text 数据流(相机/照片),与 custom keyboard extension 无关。键盘扩展加这个 key 不会被苹果拒,但也没用。已确认无需加

  5. ASRService 仍引用 iOS 26 之前的 SFSpeechRecognizer fallback(太子列为 P0-14核实后非问题。ASRService 的 ASR 后端只有 SpeechAnalyzeriOS 26 only),但 SettingsViewAppPermissionsSFSpeechRecognizer.supportedLocales() / authorizationStatus()元数据 API(这些 API 在 iOS 26 仍存在且无 deprecation),完全合法。已确认无需修

  6. 测试运行 destination 是 iPhone 17CI ci.yml:79核实。Apple 2026 年 Simulator 列表中 iPhone 17 是 stable;本机是 iPhone 17 Pro。CI 用 iPhone 17(无 Pro),是 Apple 公开的标准 simulator 名,应该能跑。已确认 OK

  7. Privacy manifest 完整度OSGKeyboard/PrivacyInfo.xcprivacyOSGKeyboardExt/PrivacyInfo.xcprivacy 都声明了 NSPrivacyAccessedAPICategoryUserDefaults (CA92.1) 原因。没有用磁盘 API / 系统启动时间 API(除了 containerURL,它本身不需要声明 reason),所以不必加其他 API reason。已确认合规

  8. AppIcon.appiconset 1024x1024 PNG 已确认 Group 24.png 1024×1024App Store 上传最低要求。

  9. CHANGELOG 0.1.0 已有## [0.1.0] - 2026-06-17v0.1.1 缺失(CHANGELOG 提到"v0.1.1 polish" 但没有 ## [0.1.1] 段)。归 P1,下次发版前补。


落地操作(已 commit 到 audit/appstore-prep 分支)

Commit 内容
chore(ci): pin Xcode 16.4 + macos-15 runner P0-1
chore(ci): enable strict lint with curated disabled rules P1-1 起步
chore: remove dead AudioCaptureService and clarify LiveDictationController scope P0-3
chore: declare ITSAppUsesNonExemptEncryption=NO for App Store upload P0-4
docs: App Store screenshots placeholder + APPSTORE_METADATA.md P0-5
chore: tag v0.1.2 (App Store submission prep) P0-6

详细 diff 见 git log。


真人/外部事项(需皇上或太子手动处理)

  1. App Store Connect 上传 — 需人工在 Xcode → Product → Archive → Distribute App,需要 Apple Developer Team 登录
  2. 真实截图替换docs/screenshots/*.png 当前是脚本生成占位;需在 iPhone 17 Pro / Pro Max 模拟器跑出真实 UI 截图
  3. App Store Connect 元数据填写docs/APPSTORE_METADATA.md 含全部文案,需复制到 App Store Connect 后台
  4. Privacy Nutrition Labels — App Store Connect 隐私标签:勾选 "Data Not Collected",并在 "Health & Fitness / Data Not Used for Tracking" 等子项确认
  5. Encryption 出口合规 — 加了 ITSAppUsesNonExemptEncryption=NO 后问卷会跳过;如出现,需要在 ITC 提交 self-classification report
  6. GitHub Releasegit push origin v0.1.2 后需到 GitHub Releases 页面写 release notes
  7. TestFlight — 上传后建议先 internal TestFlight 跑一遍,确认 Flow / Onboarding / 键盘添加流程

附录 A:本地烟雾测试结果

A.1 xcodebuild build (iPhone 17 Pro / iOS Simulator)

xcodebuild -project OSGKeyboard.xcodeproj -scheme OSGKeyboard \
  -destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
  -configuration Debug -derivedDataPath ./.derivedData build \
  CODE_SIGNING_ALLOWED=NO

** BUILD SUCCEEDED **

产出 .derivedData/Build/Products/Debug-iphonesimulator/OSGKeyboard.app/ 包含:

  • OSGKeyboard (主 App, 39 KB 可执行)
  • OSGKeyboard.debug.dylib (4.3 MB)
  • PlugIns/OSGKeyboardExt.appex (键盘扩展)
  • Frameworks/OSGKeyboardShared.framework (共享 framework)
  • Info.plistITSAppUsesNonExemptEncryption=0 已生效
  • 资源 Assets.car / MaterialIcons-Regular.ttf / en.lproj / zh-Hans.lproj

A.2 xcodebuild test (iPhone 17 Pro / iOS Simulator)

xcodebuild test ... -only-testing:OSGKeyboardTests CODE_SIGNING_ALLOWED=NO

Executed 29 tests, with 11 failures (0 unexpected)

通过 20/29 — ASRConversion / DictationBridge / FlowSessionBridge / PreviewASRControllerState 全部 PASS。

失败 9/29 — 全部为 errSecMissingEntitlement (-34018)

  • KeychainTests (8 tests) — 缺 keychain-access-groups entitlement 注入
  • LLMClientTests (3 tests) — 间接依赖 Keychain

根因: CODE_SIGNING_ALLOWED=NO 时 xcodebuild 不注入 entitlement,但 Keychain tests 调用了 kSecAttrAccessGroup 共享 keychain。这是测试工程问题,不是代码 bug不影响上架

修法(不本审计范围内,仅记录):

  1. 在 CI 中允许临时 ad-hoc signingCODE_SIGN_IDENTITY=-),保留 entitlement
  2. 或在 KeychainTests 中使用 URL(fileURLWithPath:) mock Keychain,绕过真 keychain query
  3. 或在 OSGKeyboardTests/Info.plist 同样加 keychain-access-groups 数组

推荐方案 1(最小改动;CI 加 CODE_SIGN_IDENTITY: "-" + CODE_SIGN_STYLE: Manual)。


本审计到此结束。中书省。