App Store Precheck
一个命令即可在每次 iOS App Store 提交前运行的门控。它通过静态扫描最常见的被拒场景、运行 Apple 自己的元数据检查器、监控指南漂移以及模拟对抗性审核,最大限度地降低被拒风险。
此技能是只读的。 它从不编辑代码、元数据或资源。它仅报告并写入一个通行令牌。详细方法(每个被拒向量、漂移检查机制)位于 references/methodology.md 中;当您需要了解某个检查的具体细节时,请阅读该文件。
何时运行
- 在 为 TestFlight 归档之前。
- 在 App Store Connect 中点击“提交以供审核”之前。
- 在 任何
fastlane deliver/release/pilot(可选的提交保护钩子会拦截此操作)之前。 - 每个小版本发布时。
请有目的地运行它。这是一个人工触发的门控,而不是自动的后台步骤。
配置(可选)
扫描器会自动检测标准的 fastlane + Xcode 项目布局,因此大多数项目无需任何配置。要覆盖检测,请将 config.example.json 复制到仓库根目录下的 .appstore-precheck.json。键值:bundleId(阶段 2 必需)、iosSourceDir、metadataDir、screenshotsDir、xcstringsPath、paywallGlobs、locales、disclosureKeys.{subscription,trial}、optionalChecks.familyControls、reviewPrepNotes。请参阅 config.example.json 获取完整的带注释列表。
输出契约
该技能会达到以下三种终态之一:
| 状态 | 含义 | .precheck-pass 令牌 | 保护行为 |
|---|---|---|---|
| GREEN | 无 FAIL,≤2 个 WARN | 已写入(有效 60 分钟) | 允许上传 |
| YELLOW | 无 FAIL 但 3+ 个 WARN | 未写入 | 保护阻止;要求明确确认 |
| RED | 至少 1 个 FAIL | 已移除 | 保护阻止;显示 FAIL 列表 |
当您向用户呈现判决时,首先以 一句 来自 Pierre(法国 App Review 评论家,见阶段 3)的符合角色台词开头,然后直接进入简洁、精确的分解。角色声音只是一个薄层。其下的数据——FAIL/WARN 列表、file:line 引用和修复建议——保持干净且忠于机器输出。切勿重写或改写 scan.sh 的输出,并将 Pierre 限定在一行内。
流程(5 个阶段:0–4)
阶段 0:实时指南漂移检查
将实时的 App Store 审核指南 部分编号与 guidelines-baseline.json 进行差异比较,以检测自上次核对后 Apple 新增或删除 的任何部分。始终非阻塞(最多 WARN)。 漂移是 我们 覆盖范围的缺口,绝非构建的错。页面在获取时会被截断,因此需要两次抓取技术;具体提示和核对程序见 references/methodology.md。基线绝不会自动更新。核对是需要人工介入的步骤。
阶段 1:静态扫描
bash skills/appstore-precheck/scripts/scan.sh
输出 FAIL: / WARN: / PASS: 行,覆盖 20 个被拒向量:隐私清单一致性(5.1.1(v))、用途字符串(5.1.1)、ATT(5.1.2)、其他平台提及(2.3.10)、元数据限制(2.3.1)、本地化一致性(2.3.7)、屏幕截图(2.3.3)、试用和自动续订披露(3.1.2)、恢复/条款/隐私链接(3.1.2)、私有 API(2.5.1)、最低功能要求(4.0)、Apple 登录一致性(4.8)、外部购买链接(3.1.1(a))、可选的 Screen Time / FamilyControls 证明(5.1.5)、不带 ATT 提示的跟踪/IDFA SDK(5.1.2)、出口合规密钥(ITSAppUsesNonExemptEncryption)、fastlane 元数据中的支持/隐私 URL(2.3)、分析 SDK 与 PrivacyInfo 数据类型一致(5.1.1),以及占位符/虚假元数据副本(2.1)。当没有应用内购买信号时,IAP 检查(8-10)会自动跳过。完整的检查表见 references/methodology.md。
扫描器是便携式 Bash 脚本,因此您也可以直接在任何代理之外运行它,用于快速 CI 或 pre-commit 检查。
阶段 2:Apple 官方的 fastlane precheck
需要在配置中提供 bundleId(或直接传递 app_identifier)以及 App Store Connect API 凭据。切勿提交密钥。 封装的脚本从您的环境变量中构建 ASC API 密钥 JSON,运行 precheck,然后在退出时删除密钥(使用 --dry-run 预览命令,无需凭据且不进行网络调用):
ASC_KEY_ID=… ASC_ISSUER_ID=… ASC_P8_PATH=/path/AuthKey.p8 \
bash skills/appstore-precheck/scripts/phase2-precheck.sh com.example.app
或者手动运行:从环境变量生成密钥 JSON,运行 precheck,然后删除它。
fastlane run precheck \
app_identifier:"<YOUR_BUNDLE_ID>" \
api_key_path:"/tmp/asc-key.json" \
include_in_app_purchases:false \
default_rule_level:":error"
rm -f /tmp/asc-key.json # 立即删除密钥
Apple 自己的规则引擎会检查 URL、GitHub 提及、脏话、Apple 商标、定价语言和 Beta 关键词。Result: true → PASS;任何违规行 → FAIL。IAP 已在阶段 1 中覆盖,因此 include_in_app_purchases:false 可以避免 API 密钥的 IAP 限制。
阶段 3:对抗性审核(最重要)
分派一个子代理,扮演 Pierre,一位持怀疑态度的 Apple App Review 审核员,具有法国评论家挑剔的品味。逐字使用以下提示,并填写应用的具体信息:
你是 Pierre,一位经验丰富的 Apple App Review 审核员,你的评论风格像法国评论家。你看过一万次被拒,对什么都不屑一顾。一份新的提交刚刚落在你的桌上。你的任务是 真实地尝试拒绝它,没有批准偏向。挑选 5 个随机的指南条目,在不同章节间分布(一个 2.x,两个 3.x 偏向付费墙,一个 4.x,一个 5.x);每次运行选择不同的组合(包含一个种子行,使重新运行结果不同)。对于每个条目:(A 轮)在相关文件中 grep 至少 2 条具体证据:2.3.x 的元数据;3.1.x 的付费墙视图 + 字符串目录;4.x 的核心/导航;5.1.x 的 Info.plist + PrivacyInfo。(B 轮)问自己“作为审核员,我有什么理由标记这个?”(C 轮)用 Apple 的真实口吻撰写一份拒绝草稿(Guideline X.Y.Z – Category / We noticed… / Specifically… / Next Steps… / Resources…)。为每个条目分配风险等级:REJECT-CERTAIN / REJECT-RISK / WARN / PASS。最后给出提交建议(HOLD / SUBMIT WITH WARNINGS / GO)和最关键的一个修复点。只读:绝不修改文件;如果找不到证据,就如实说,不要编造。保持在 500 字以内。包含至少一个 PASS 和至少一个高风险条目,以模拟真实分布。
阶段 4:汇总 + 令牌
GREEN/YELLOW/RED 判定和令牌操作是 确定性的,纯粹根据 FAIL/WARN 计数得出。scripts/verdict.sh 计算它们,使得判决结果可通过机器测试,而不仅仅是代理的判断;将扫描结果通过管道传入:
bash skills/appstore-precheck/scripts/verdict.sh < scan-output.txt # 输出 VERDICT / COUNTS / TOKEN
退出码:0 GREEN / 1 RED / 2 YELLOW,使用 --apply 时相应写入或删除 .precheck-pass(YELLOW 状态保留令牌以待用户明确确认)。阶段 0-3 仍生成叙述;verdict.sh 只固定阈值计算。
- 收集阶段 0-3 的输出;将 FAIL + WARN + PASS 统计到输出契约表格中。
- 对于每个 FAIL,提供
file:line引用和建议的修复方法。 - 用 一句 Pierre 的台词打开判决(每次运行变换措辞;保持简短,符合他冷漠的法国评论家风格),然后进入简洁的分解。决定:
- GREEN: Pierre, 勉强地: “哼。我没发现什么。可接受。别让我后悔。” 然后
date +%s > .precheck-pass && echo "token written"(有效 60 分钟)。 - YELLOW: Pierre:“一些小的瑕疵。我不会拒绝,但我注意到了。” 直接列出 WARN,询问用户“确认并仍然提交?”,仅在确认后写入令牌。
- RED: Pierre:“不。{n} 个问题。Apple 会发现更少。下一个。” 不写令牌;然后直接给出包含
file:line+ 修复的 FAIL 列标,并明确说明提交已被阻止。 Pierre 的行仅用于润色。其下的 FAIL/WARN 列表、file:line和修复建议保持简洁和精确,绝不改写。
- GREEN: Pierre, 勉强地: “哼。我没发现什么。可接受。别让我后悔。” 然后
- 打印最终手动检查清单(见
references/methodology.md)。
规则
- 只读: 绝不更改代码或资源。仅报告和写入令牌。
- 速度 > 穷尽:
scan.sh使用并行 grep/jq,在几秒内完成。 - 不吞没错误: 如果任何扫描命令失败,该行报告为 FAIL 并继续扫描。
- 令牌位置: 仓库根目录下的
.precheck-pass;保护机制使用mmin -60过滤器测试它。 - 仅本地: 设计用于手动本地运行;避免在 CI 中使用,以免产生误报信号。
已知限制
- 无运行时崩溃测试;那是 TestFlight 和崩溃报告工具的工作。仅静态分析。
- 多项检查是基于检测到的信号给出的咨询性 WARN(Apple 登录 4.8、外部购买 3.1.1(a)、不带 ATT 的跟踪/IDFA、分析与隐私清单一致性、元数据 URL 和占位符副本)。出口合规密钥在缺失时会被标记,但实际的加密答案仍应在 App Store Connect 中设置。
- 对抗性审核是一种启发式模拟,不能保证 Apple 的决定。
- 对原生 Swift / SwiftUI 最准确。元数据、隐私清单、屏幕截图和出口合规检查适用于任何 iOS 应用,但代码级检查读取 Swift 源代码,因此在 React Native(JavaScript)或 Flutter(Dart)上会检测不足,但不会误报。
- 仅限 iOS。
- 阶段 0 仅检测结构性漂移(添加/删除的章节编号);原因请参阅参考文档。
可选:提交保护钩子
hooks/fastlane-guard.sh 会阻止 fastlane deliver/pilot/release,除非存在新鲜的 .precheck-pass 令牌。在 Claude Code 中,它通过 hooks/hooks.json 自动连接。在其他环境中,请自行将其连接为前置命令检查,或将令牌视为手动通行/禁止信号。