App Store Connect requires screenshots at 1290x2796 (6.7") and
1179x2556 (6.1") for the iPhone 17 Pro Max / 17 / Pro lineup,
with at least 3 and at most 10 images per size. The repository
previously had no screenshots in docs/.
This commit adds:
- scripts/generate_screenshot_placeholders.py - PIL-based
generator that produces 5x 6.7" + 5x 6.1" placeholders with
status bar, mock device frame, keyboard mock, and headline /
subtitle text.
- docs/screenshots/6.7/ and 6.1/ with 5 placeholder PNGs each
(10 total, all at the correct Apple-mandated dimensions).
- docs/screenshots/README.md explaining the dimensions,
generation script, and how to capture real Simulator
screenshots via `xcrun simctl io booted screenshot`.
- docs/APPSTORE_METADATA.md - a single source of truth for
every field in App Store Connect: name, subtitle, URLs,
pricing, description (<=4000 chars), promotional text
(<=170 chars), keywords (<=100 chars), release notes
(<=4000 chars), what's new, App Privacy answers
(Data Not Collected), encryption declaration, and reviewer
notes.
- AUDIT_APPSTORE.md - the v0.1.2 pre-launch audit report
with 6 P0 items, 5 P1 items, 4 P2 items, and a 9-item
"cross-cutting observations" section addressing the items
in the original brief that turned out to be non-issues
(CFBundleURLTypes in keyboard ext, NSSupportsLiveText,
AppGroup fatalError inconsistency, AudioCaptureService
interaction with AVAudioSession, ASRService SFSpeechRecognizer
references). Includes an Appendix A with the local
xcodebuild build + test results.
Important: the placeholder PNGs are intentionally bland - they
exist so App Store Connect accepts the dimensions. They MUST
be replaced with real Simulator screenshots before the actual
upload, see docs/screenshots/README.md for the workflow.
Refs: AUDIT_APPSTORE.md P0-5
18 KiB
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 三个 job(lint / 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-14→macos-15(GitHub-hosted runner 自带 Xcode 26.x) - 删除 matrix 中
xcode: "16.0"字段,改为macos-15自带的 Xcode - 改用
sudo xcode-select -s /Applications/Xcode_16.4.app(macos-15 预装的版本号;26 系列需要 macos-26 runner 上线后才稳定;先锁 16.4 是 CI 可复现的稳妥选择,并增加select-xcode步骤用xcversion或agvtool探测)
实际修复策略: macos-14 保留但改用
maxim-lobanov/setup-xcode@v1action 自动选最高稳定版,或者升级到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:60 的 NSLog。
App Store 审核对 print 的态度: 严格说不查日志语句本身,App Review 只关心功能/隐私/UI。但苹果审核员在控制台中看到的 NSLog 输出,会被列为"non-issues"或"建议优化",不会因此被拒。但我们仍做 P1 优化(替换为 os.Logger,可在 release 下也输出但经 OSLog 隐私控制)。
真 P0 问题: AppGroup.swift:60 的 NSLog 会出现在 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.swift(typealias)、DictationCaptureView.swift、KeyboardPreviewSheet.swift、PreviewASRControllerStateTests.swift |
❌ 在用 | 不可删 |
结论: "Legacy" 是相对概念。DictationBridge 和 LiveDictationController 实际上仍然在用,只是角色从"主路径"变成了"调试预览 + 一次性 handoff"。
修复:
- ✅ 删除
AudioCaptureService.swift(真正无人调用的死代码) - ❌ 保留
DictationBridge.swift(KeyboardViewController 仍消费) - ❌ 保留
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 截图 — 项目声明 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.yml 已 disabled_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/polish)3) 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:60 的 NSLog 是唯一 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 provider:OpenAI / 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 单项里的杂项)
-
键盘扩展无
CFBundleURLTypes(太子列为 P0-12) — 核实后非问题。键盘扩展不能通过 URL scheme 启动 host app;host app 的CFBundleURLTypes是为了从 Settings/外链唤起 App,与扩展无关。已确认无需修。 -
App Group fallback 行为不一致(太子列为 P0-13) — 实际是设计意图。
AppGroup.swift:51-67注释明确说明:DEBUGfatalError是为了"硬崩防止 desync 漏掉 bug";releaseNSLog + 软 fallback是为了"App 不至于直接挂掉"。审核员不会因为 release 软 fallback 而拒。已确认无需改 P0,归 P1-5 一起处理。 -
FlowContinuousCapture依赖.playAndRecordaudio session — 实际已处理。OSGKeyboardExt用的是 host App 的FlowSessionManager(在主 App target 而非 ext),不冲突。键盘扩展内 AudioCaptureService 是死代码(P0-3 已删)。已确认无问题。 -
NSSupportsLiveTextFrequentUpdates(太子列为 P0-11) — 核实。Apple 在 iOS 17 引入NSSupportsLiveText/ iOS 18 加NSSupportsLiveTextFrequentUpdates,但这是宿主 app 用于处理 Live Text 数据流(相机/照片),与 custom keyboard extension 无关。键盘扩展加这个 key 不会被苹果拒,但也没用。已确认无需加。 -
ASRService 仍引用 iOS 26 之前的 SFSpeechRecognizer fallback(太子列为 P0-14) — 核实后非问题。ASRService 的 ASR 后端只有 SpeechAnalyzer(iOS 26 only),但
SettingsView和AppPermissions用SFSpeechRecognizer.supportedLocales()/authorizationStatus()等元数据 API(这些 API 在 iOS 26 仍存在且无 deprecation),完全合法。已确认无需修。 -
测试运行 destination 是 iPhone 17(CI
ci.yml:79) — 核实。Apple 2026 年 Simulator 列表中 iPhone 17 是 stable;本机是 iPhone 17 Pro。CI 用iPhone 17(无 Pro),是 Apple 公开的标准 simulator 名,应该能跑。已确认 OK。 -
Privacy manifest 完整度 —
OSGKeyboard/PrivacyInfo.xcprivacy和OSGKeyboardExt/PrivacyInfo.xcprivacy都声明了NSPrivacyAccessedAPICategoryUserDefaults(CA92.1) 原因。没有用磁盘 API / 系统启动时间 API(除了containerURL,它本身不需要声明 reason),所以不必加其他 API reason。已确认合规。 -
AppIcon.appiconset 1024x1024 PNG — ✅ 已确认
Group 24.png1024×1024,App Store 上传最低要求。 -
CHANGELOG 0.1.0 已有 —
## [0.1.0] - 2026-06-17,v0.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。
真人/外部事项(需皇上或太子手动处理)
- App Store Connect 上传 — 需人工在 Xcode → Product → Archive → Distribute App,需要 Apple Developer Team 登录
- 真实截图替换 —
docs/screenshots/*.png当前是脚本生成占位;需在 iPhone 17 Pro / Pro Max 模拟器跑出真实 UI 截图 - App Store Connect 元数据填写 —
docs/APPSTORE_METADATA.md含全部文案,需复制到 App Store Connect 后台 - Privacy Nutrition Labels — App Store Connect 隐私标签:勾选 "Data Not Collected",并在 "Health & Fitness / Data Not Used for Tracking" 等子项确认
- Encryption 出口合规 — 加了
ITSAppUsesNonExemptEncryption=NO后问卷会跳过;如出现,需要在 ITC 提交 self-classification report - GitHub Release —
git push origin v0.1.2后需到 GitHub Releases 页面写 release notes - 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.plist内ITSAppUsesNonExemptEncryption=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-groupsentitlement 注入LLMClientTests(3 tests) — 间接依赖 Keychain
根因: CODE_SIGNING_ALLOWED=NO 时 xcodebuild 不注入 entitlement,但 Keychain tests 调用了 kSecAttrAccessGroup 共享 keychain。这是测试工程问题,不是代码 bug,不影响上架。
修法(不本审计范围内,仅记录):
- 在 CI 中允许临时 ad-hoc signing(
CODE_SIGN_IDENTITY=-),保留 entitlement - 或在
KeychainTests中使用URL(fileURLWithPath:)mock Keychain,绕过真 keychain query - 或在
OSGKeyboardTests/Info.plist同样加keychain-access-groups数组
推荐方案 1(最小改动;CI 加 CODE_SIGN_IDENTITY: "-" + CODE_SIGN_STYLE: Manual)。
本审计到此结束。中书省。