| 02-P-NBL/plan.md | ●●●●● patch | view | raw | blame | history | |
| 02-P-NBL/x-asr-realtime-insertion-report.md | ●●●●● patch | view | raw | blame | history | |
| AGENTS.md | ●●●●● patch | view | raw | blame | history | |
| Docs/KM2.项目管理-非业务直接相关/AppStore审核拒绝-20260610.md | ●●●●● patch | view | raw | blame | history | |
| Docs/KM2.项目管理-非业务直接相关/AppStore打包连续返工经验教训-20260611.md | ●●●●● patch | view | raw | blame | history | |
| Docs/KM2.项目管理-非业务直接相关/QA工作-经验与教训.md | ●●●●● patch | view | raw | blame | history | |
| README.md | ●●●●● patch | view | raw | blame | history |
02-P-NBL/plan.md
@@ -7,6 +7,7 @@ - `product-plan.md` - `engineering-plan.md` - `x-asr-realtime-insertion-report.md` ## 1. 分期原则 @@ -439,6 +440,8 @@ 评估 `Gilgamesh-J/X-ASR` 的 `X-ASR-zh-en` 是否值得纳入未来模型选项,重点不是“再加一个模型”,而是判断它能否带来当前模型没有覆盖好的实时输入价值。 后续如要推进“真实实时上屏”,应先阅读独立技术报告:`x-asr-realtime-insertion-report.md`。该报告记录了只对 X-ASR 生效的隔离策略、实时写入当前输入框的技术路径、目标 App 行为差异风险、fallback 设计和 QA 矩阵。 ### 9.2 产品假设 X-ASR 的价值主要来自: 02-P-NBL/x-asr-realtime-insertion-report.md
New file @@ -0,0 +1,433 @@ # X-ASR 真实实时上屏技术路径与风险报告 状态:未来候选方案,不进入当前版本开发 日期:2026-06-07 关联基线:`v2.1.30-build20260607.1127-xasr-live-caption` ## 1. 结论 真实实时上屏可以做,但不建议放进当前版本。下一次如要继续推进,应作为 X-ASR 专属实验功能开发,默认关闭,不影响 SenseVoice、Qwen3-ASR、Moonshine、Parakeet 的现有“录完后最终粘贴”路径。 当前已经具备做实验版的基础: - X-ASR 已通过 `StreamingEngine` / `StreamingSession` 持续产出 partial 文本。 - Recorder 已支持录音中增量读取 PCM 样本。 - 浮窗实时字幕已经验证为基本可用。 - X-ASR 最终识别已加入尾部静音 padding 和最后非空结果保护。 - 现有最终粘贴流程仍可作为失败兜底。 仍不具备直接做成默认功能的原因: - 真实实时上屏需要持续修改当前目标 App 的文本内容,不只是识别模型问题。 - 不同目标 App 对粘贴、删除、选区、撤销栈、焦点变化和安全权限的处理不同。 - 一旦 partial 回改处理不稳,可能把用户正在编辑的文本改乱。 ## 2. 目标与非目标 ### 2.1 目标 实现 X-ASR 专属的实验模式: ```text 按下热键 -> 开始录音 -> X-ASR 持续输出 partial -> partial 实时写入当前输入框 松开热键 -> X-ASR final 校准 -> 输入框留下最终文本 异常发生 -> 停止实时上屏 -> 回退到当前最终粘贴流程 ``` 用户可感知价值: - 边说边看到文字进入输入框。 - 长句输入等待感降低。 - 中英混输时用户能更早发现识别方向是否正确。 - 让 X-ASR 的流式能力区别于 SenseVoice/Qwen3-ASR 的传统听写体验。 ### 2.2 非目标 第一版不做: - 不影响 SenseVoice、Qwen3-ASR、Moonshine、Parakeet。 - 不把实时上屏设为默认。 - 不做所有 App 的完美兼容。 - 不做复杂文本编辑器协议适配。 - 不在 App Store 正式发布线启用。 ## 3. 隔离策略 真实实时上屏必须同时满足以下条件才启用: ```text SelectedModelID == x-asr-zh-en-960ms Engine implements StreamingEngine Config.RealtimeInsertExperimental == true 当前录音模式允许实验上屏 辅助功能权限可用 ``` 任何条件不满足,走原路径: ```text 录音 -> StopAndGetSamples -> Recognize -> PostProcess -> userdict -> Paste ``` 建议新增配置: ```go type Config struct { RealtimeInsertExperimental bool `json:"RealtimeInsertExperimental"` } ``` 建议前端展示为实验开关,不放在普通模型卡片主流程里: ```text X-ASR 实验功能 [ ] 边说边写入当前输入框 ``` 开关说明只表达风险,不应承诺所有 App 都稳定兼容。 ## 4. 技术路径 ### 4.1 保留现有最终粘贴路径 第一版不要删除现有最终粘贴逻辑。真实实时上屏失败时必须能回到当前已验证路径。 当前基线: - `privatevoice.src/internal/engine/engine.go` 已有 `StreamingEngine` 和 `StreamingSession`。 - `privatevoice.src/internal/audio/recorder.go` 已有 `ReadSamplesSince()`。 - `privatevoice.src/app.go` 已有录音中 live caption loop。 - `privatevoice.src/internal/engine/engine_darwin.go` 的 X-ASR final 已做 padding。 下一步应新增一个独立组件,而不是把实时输入逻辑塞进 engine: ```text app.go -> live streaming loop -> RealtimeInserter -> input backend ``` ### 4.2 新增 RealtimeInserter 建议新增包: ```text privatevoice.src/internal/realtimeinput/ ``` 核心接口: ```go type Inserter interface { Start(target Target) error Update(partial string) error Finish(final string) error Cancel() error } ``` 内部状态: ```go type Session struct { targetAppID string targetWindowID string insertedText string lastPartial string startedAt time.Time totalEdits int fallbackRequired bool } ``` 第一版只要求“对自己插入的文本负责”,不要尝试理解用户输入框里已有内容。 ### 4.3 partial 更新策略 优先采用增量 diff,不每次全删全贴。 规则: 1. 对 `lastPartial` 和 `nextPartial` 求最长公共前缀。 2. 如果 `nextPartial` 只是追加,直接输入新增部分。 3. 如果尾部回改,删除上一段尾部,再输入新尾部。 4. 如果回改跨度超过阈值,停止实时上屏,回退最终粘贴。 建议阈值: ```text 单次删除字符数 <= 24 累计删除字符数 <= 80 单次 partial 长度 <= 300 更新时间间隔 >= 120 ms ``` 示例: ```text last: 我想去苹果市场 next: 我想去 App Store 公共前缀: 我想去 删除: 苹果市场 输入: App Store ``` ### 4.4 输入动作策略 第一版建议只用最保守的模拟输入动作: - 插入文本:复用现有 paste/type backend。 - 删除文本:优先模拟 `DeleteBackward` / `Backspace` 指定次数。 - final 校准:按 diff 修正尾部,不直接清空整段。 不要第一版就引入复杂剪贴板替换和选区控制混合策略,否则 QA 面会扩大。 需要记录: ```text 本次实时上屏插入了多少字符 删除了多少字符 最后一次 partial 是什么 最终 final 是什么 是否触发 fallback ``` ### 4.5 焦点与目标 App 保护 开始录音时记录当前目标: ```text frontmost app bundle id frontmost window title / pid ``` 每次准备写入前检查目标是否仍一致。 如果目标变化: ```text 停止实时上屏 不再修改输入框 继续录音和最终识别 松开后是否最终粘贴需谨慎:默认不粘贴,或提示/记录 fallback ``` 第一版可以采用更保守策略: ```text 焦点变化 -> 取消本次实时上屏 -> 不做 final 粘贴 ``` 这样可以避免文字进入错误 App。 ### 4.6 final 校准 松开热键后: 1. 停止接受新的 partial。 2. 调用 X-ASR `Finish()` 或现有 final `Recognize()`。 3. 对输入框中已插入文本做 final diff。 4. 只修正本次 session 插入的尾部。 5. 完成后清理 session 状态。 如果 final 和 partial 差异过大: ```text 不做大规模删除 保留已上屏内容 记录错误 必要时显示“请检查文本” ``` ## 5. 主要风险 ### 5.1 目标 App 行为差异 不同 App 对模拟输入的处理不同: - 有的 App 对粘贴稳定,但对多次退格慢。 - 有的 App 会把每次 partial 更新写入撤销栈。 - 有的 App 在富文本输入框中会自动改格式。 - 有的网页输入框会拦截粘贴事件。 - 有的 Electron App 对焦点和输入事件处理延迟明显。 影响范围只在 X-ASR 实时上屏开启时,不影响其他模型。 ### 5.2 partial 回改导致文本错乱 流式模型可能修正前文。小幅尾部修正可处理,大幅回改风险高。 风险例子: ```text partial 1: 今天下午开会讨论苹果市场 partial 2: 今天下午开会讨论 App Store 上架材料 ``` 如果删除跨度大,模拟删除容易误删用户原有文本或删不干净。 ### 5.3 焦点变化 用户说话时可能切换窗口、点击别处、系统弹窗抢焦点。实时上屏如果继续写入,会进入错误位置。 必须有焦点检查和快速停止。 ### 5.4 剪贴板和撤销栈 如果实时上屏依赖粘贴: - 可能短暂覆盖用户剪贴板。 - 目标 App 的撤销栈可能变成多次小操作。 - 用户按 Cmd+Z 的结果可能不符合预期。 第一版应优先复用现有剪贴板保护逻辑,并记录是否能够恢复剪贴板。 ### 5.5 权限与安全 实时上屏依赖辅助功能权限。权限失效时: - 不能继续模拟输入。 - 必须停止实时上屏。 - 不能影响录音和最终识别。 ### 5.6 性能与功耗 实时上屏比“录完识别”多了持续 decode 和持续 UI/input 操作。 需要观察: - CPU 占用 - 内存增长 - 长句 3-5 分钟输入稳定性 - 浮窗动画是否卡顿 - 目标 App 是否出现输入延迟 ## 6. 降风险设计 第一版必须具备以下保护: - 默认关闭。 - 只对 `x-asr-zh-en-960ms` 生效。 - 单 session 最大实时插入长度限制。 - partial 大幅回改时停止实时上屏。 - 焦点变化时停止实时上屏。 - 删除失败或权限失败时停止实时上屏。 - 保留最终识别兜底。 - 日志记录每次 fallback 原因。 推荐 fallback reason: ```text focus_changed large_partial_rewrite delete_failed paste_failed permission_missing session_timeout target_app_unsupported ``` ## 7. QA 计划 ### 7.1 目标 App 矩阵 第一轮至少覆盖: | 类型 | App / 场景 | 目的 | |---|---|---| | 系统纯文本 | TextEdit 纯文本模式 | 基础输入、删除、final 校准 | | 系统富文本 | Notes / TextEdit 富文本 | 富文本输入稳定性 | | 浏览器 | Safari / Chrome textarea | 网页输入框 | | Electron | VS Code / 飞书 / Slack | 高频工作 App | | 聊天 | 微信 / 飞书输入框 | 中文即时通讯 | | 开发者工具 | VS Code editor | 英文和符号混输 | 第二轮再扩展: - Word - Pages - App Store Connect 网页表单 - 终端 - 全屏和 Split View ### 7.2 用例矩阵 必须覆盖: - 按住热键短句输入。 - 按住热键长句输入。 - 点按模式开始/停止。 - 录音中立即取消。 - 录音中切换焦点。 - partial 小幅追加。 - partial 小幅尾部回改。 - partial 大幅回改触发 fallback。 - 中英混输。 - 英文技术词。 - 中文长句标点。 - 连续快速触发两次。 - 辅助功能权限缺失。 ### 7.3 验收标准 进入下一阶段前至少满足: - 其他模型路径完全不变。 - X-ASR 实验开关关闭时行为完全等同当前版本。 - X-ASR 开关打开时,基础纯文本 App 连续 20 次输入不乱序、不误删。 - 焦点变化不会把文字写到错误 App。 - fallback 后不会继续实时修改目标输入框。 - final 校准不会删除用户原本已有文本。 - QA 日志能明确每次 fallback 原因。 ## 8. 建议实施顺序 ### Phase 1:不可见底座 - 新增 `RealtimeInsertExperimental` 配置,默认 false。 - 新增 `realtimeinput` 包和纯单元测试。 - 实现 diff 算法、删除/插入计划生成。 - 不接真实 App 输入。 ### Phase 2:受控输入实验 - 只在 TextEdit 纯文本模式手工开启。 - 只支持追加,不支持回改删除。 - 验证焦点检查和 session 生命周期。 ### Phase 3:尾部回改和 final 校准 - 加入有限删除。 - 加入 final diff。 - 大幅回改 fallback。 ### Phase 4:App 矩阵 QA - 扩展到浏览器、聊天、Electron。 - 补 QA 记录和兼容表。 - 决定是否进入产品 UI。 ## 9. 需要保留的产品判断 X-ASR 的价值不是替代现有模型,而是提供另一种输入体验: ```text SenseVoice / Qwen3-ASR / Moonshine / Parakeet: 更适合录完后最终识别 X-ASR: 更适合探索实时反馈、实时字幕、实时上屏 ``` 因此,真实实时上屏应当是 X-ASR 的实验性增强,不应成为所有模型的共享默认行为。 ## 10. 下一次开工检查清单 - [ ] 当前代码仍保留 `StreamingEngine` / `StreamingSession`。 - [ ] X-ASR profile 仍为实验模型,未进入正式 App Store 线。 - [ ] `v2.1.30-build20260607.1127-xasr-live-caption` 可作为回滚基线。 - [ ] 新增配置默认关闭,并可在 UI 或调试入口打开。 - [ ] 先完成 diff 纯单元测试,再接真实 input backend。 - [ ] 先做 TextEdit 纯文本 QA,再扩展目标 App。 AGENTS.md
@@ -26,3 +26,31 @@ 执行打包前仍必须遵守发布版本规则:递增 patch 版本号、生成新的 `YYYYMMDD.HHMM` build 编号、更新 App 元数据与 `CHANGELOG.md`,并生成可分发的 macOS `.dmg`。 如果只有开发自测通过,但 QA 尚未验证通过,则不得把该版本作为测试人员可用包交付;此时只说明当前处于待 QA 状态。 ## 4. Mac App Store 出口合规规则 如果本 App 未实现、未打包、未接入非 Apple 操作系统提供的加密算法,Mac App Store 构建的 `Info.plist` 应声明: ```xml <key>ITSAppUsesNonExemptEncryption</key> <false/> ``` 提交 App Store Connect 前必须检查该键是否存在并为 `false`,以避免每次上传 build 后出现 `Missing Compliance` 并需要手动回答加密出口合规问题。 如果后续新增自实现加密、第三方加密库、非系统提供的标准加密算法或专有加密算法,必须重新评估出口合规,不得沿用上述声明。 ## 5. 对话记忆保存规则 本项目需要保存重要会话记忆,但不得为了普通会话记录频繁打断用户审批。 - 首选保存目录:项目根目录下的 `.codex-sessionhistory/`,即 `/Users/ar/Projects/PrivateVoice/.codex-sessionhistory/`。 - 只在发生以下情况时保存会话记录:关键决策、文件修改、发布 / 构建 / QA 结论、重大问题排查、用户明确要求保存、阶段性收尾。 - 纯询问、路径确认、状态查询、简单说明、无实质决策或无文件变更的对话,不强制保存。 - 文件名格式:`YYYY-MM-DD-主题.md`;主题必须概括本轮 session 的核心内容,控制在 20 个字符以内,优先使用中文短语,避免空格、`/`、`:` 等不适合作为文件名的字符。 - 如果同一天同主题已有记录文件,必须创建新文件并追加序号,避免覆盖:`YYYY-MM-DD-主题-01.md`、`YYYY-MM-DD-主题-02.md`。 - 保存内容至少包括:用户目标、已完成事项、关键决策、文件变更、未决问题、后续建议。 - 如果首选目录在当前沙盒下不可写,不得发起额外权限审批;必须改存到当前 Codex 工作区的 `.codex-sessionhistory/PrivateVoice/`,并在最终答复中说明实际保存位置。 - 只有首选目录和当前工作区备用目录都不可写时,才允许跳过保存,并在最终答复中说明原因。 - 如果确需保存,应一次性保存本轮完整摘要,避免为了会话记录反复修正。 - 跨项目对话以主要项目保存;如果多个项目都有实质决策或改动,应按同样的首选目录 / 当前工作区备用目录规则分别保存摘要。 Docs/KM2.项目管理-非业务直接相关/AppStore审核拒绝-20260610.md
New file @@ -0,0 +1,257 @@ # App Store 审核拒绝排查资料|2026-06-10 ## 结论摘要 本次拒绝不是之前邮件里的 `ITMS-91109: com.apple.quarantine` 上传包校验问题,而是 Apple App Review 在审核阶段无法启动 App。 被拒提交: - App:私语输入法 / PrivateVoice Dictation - App Apple ID:`6776681824` - 版本:`2.1.28` - Build:App Store Connect 显示为 `20260605.353`,本地构建号为 `20260605.0353`,应为同一个 build - Submission ID:`73d56c2f-ebb5-4c89-b6d5-e83251681828` - Review date:`June 09, 2026` - Review Device:MacBook Pro 14-inch, Nov 2024 - OS version:macOS `26.5` - 拒绝分类:`Guideline 2.1(a) - Performance - App Completeness` Apple 原文核心内容: ```text The app exhibited one or more bugs that would negatively impact App Store users. Bug description: We were unable to launch the app. Review device details: - Device type: MacBook Pro (14-inch, Nov 2024) - OS version: macOS 26.5 - Internet Connection: Active ``` ## 本地复核证据 复核对象: ```text /Users/ar/Projects/PrivateVoice/Release/PrivateVoice-Dictation-2.1.28-build20260605.0353/PrivateVoice-Dictation-2.1.28-build20260605.0353-universal-macappstore.pkg ``` 关键复核结果: 1. `.pkg` 内没有发现 `com.apple.quarantine`,所以这次拒绝不应按旧的 `ITMS-91109` 处理。 2. 展开 `.pkg` 后,对包内 `.app` 做签名验证失败。 3. 打包前目录中的 `universal/PrivateVoice Dictation.app` 当前也验证失败。 4. 当前本机系统也是 macOS `26.5`,与 Apple 审核环境一致,说明本地具备复现/拦截条件。 复核命令与关键输出: ```bash pkgutil --expand-full \ "Release/PrivateVoice-Dictation-2.1.28-build20260605.0353/PrivateVoice-Dictation-2.1.28-build20260605.0353-universal-macappstore.pkg" \ "$TMP/pkg" codesign --verify --deep --strict --verbose=4 \ "$TMP/pkg/com.shanghai3168.privatevoicedictation.pkg/Payload/PrivateVoice Dictation.app" ``` 输出: ```text invalid signature (code or signature have been modified) In architecture: arm64 ``` 继续检查 entitlements: ```bash codesign -d --entitlements :- \ "$TMP/pkg/com.shanghai3168.privatevoicedictation.pkg/Payload/PrivateVoice Dictation.app" ``` 输出: ```text warning: binary contains an invalid entitlements blob. The OS will ignore these entitlements. ``` ## 疑似根因 ### 1. 最可疑:错误写入 `keychain-access-groups` `privatevoice.src/scripts/build-macappstore-pkg-macos.sh` 当前会读取 provisioning profile 里的 `keychain-access-groups`,并写入最终签名 entitlements: ```text privatevoice.src/scripts/build-macappstore-pkg-macos.sh:207 PROFILE_KEYCHAIN_GROUP=... privatevoice.src/scripts/build-macappstore-pkg-macos.sh:216-220 if [[ -n "$PROFILE_KEYCHAIN_GROUP" ]]; then Delete :keychain-access-groups Add :keychain-access-groups array Add :keychain-access-groups:0 string $PROFILE_KEYCHAIN_GROUP fi ``` 已解码的 provisioning profile 中该值为: ```text keychain-access-groups = CR3J54M8BQ.* ``` 而构建日志显示最终签名 entitlements 确实包含: ```xml <key>keychain-access-groups</key> <array> <string>CR3J54M8BQ.*</string> </array> ``` 这个通配 Keychain group 很可能在 macOS 26.5 的签名校验/启动环境中被视为无效 entitlements blob。代码检索没有发现 App 实际使用 Keychain,因此建议不要把该 entitlement 签入 App。 ### 2. 风险点:签名后仍修改 App Bundle 脚本签名后又执行了扩展属性清理和 AppleDouble 删除: ```text privatevoice.src/scripts/build-macappstore-pkg-macos.sh:272-276 Code signing for Mac App Store privatevoice.src/scripts/build-macappstore-pkg-macos.sh:278-282 Removing extended attributes after signing ``` 签名后修改 `.app` bundle 是高风险行为。即使本次更直接的错误是 invalid entitlements blob,发布脚本也应改成: - 签名前完成 xattr / AppleDouble / `.DS_Store` 清理; - 签名后只做只读校验; - 签名后不得再修改 bundle 内容或元数据。 ### 3. 当前门禁缺口:没有验证“最终 pkg 展开后的 app” 脚本目前在 `productbuild` 后只做: ```text pkgutil --check-signature "$PKG_PATH" assert_pkg_no_quarantine_attributes "$PKG_PATH" ``` 这不足以证明用户/Apple 安装出来的 `.app` 能启动。必须补充: - `pkgutil --expand-full` 最终 `.pkg`; - 对展开后的 `Payload/PrivateVoice Dictation.app` 执行 `codesign --verify --deep --strict`; - 提取 entitlements,并将任何 `warning` 视为失败; - 必要时安装到干净 `/Applications` 后做启动烟测。 ## 给开发的具体修改建议 ### 必改 1:不要写入通配 `keychain-access-groups` 建议调整 `privatevoice.src/scripts/build-macappstore-pkg-macos.sh`: - 如果 App 不使用 Keychain,直接不要向 `SIGNING_ENTITLEMENTS` 写入 `keychain-access-groups`。 - 不要把 provisioning profile 里的 `CR3J54M8BQ.*` 原样写入签名 entitlements。 - 如果未来确实需要 Keychain,再创建明确、非通配、被 App ID/profile 支持的 Keychain access group,并做单独验证。 ### 必改 2:签名后禁止修改 bundle 调整脚本顺序: - `strip_extended_attributes` - `remove_appledouble_files` - `assert_no_quarantine_attributes` - `assert_no_appledouble_files` - `install_name_tool` - `codesign` - 只读验证 - `productbuild` - 展开最终 pkg 再验证 签名后不要再执行 `xattr -cr` 或删除文件。 ### 必改 3:把最终展开包验证纳入发布门禁 新增一个发布验证步骤,至少检查: ```bash pkgutil --expand-full "$PKG_PATH" "$TMP/pkg" codesign --verify --deep --strict --verbose=4 \ "$TMP/pkg/com.shanghai3168.privatevoicedictation.pkg/Payload/PrivateVoice Dictation.app" codesign -d --entitlements :- \ "$TMP/pkg/com.shanghai3168.privatevoicedictation.pkg/Payload/PrivateVoice Dictation.app" xattr -lr \ "$TMP/pkg/com.shanghai3168.privatevoicedictation.pkg/Payload/PrivateVoice Dictation.app" \ | grep 'com.apple.quarantine' ``` 验收标准: - `codesign --verify --deep --strict` 零错误; - entitlements 提取无 `invalid entitlements blob` warning; - 不包含 `com.apple.quarantine`; - bundle identifier 为 `com.shanghai3168.privatevoicedictation`; - signed application identifier 为 `CR3J54M8BQ.com.shanghai3168.privatevoicedictation`; - `CFBundleShortVersionString` 与 App Store Connect 版本一致; - `CFBundleVersion` 为新的、更高 build number; - `ITSAppUsesNonExemptEncryption=false` 存在并正确。 ### 必改 4:重新出新 build,不要复用被拒 build 不要重新提交 `20260605.0353/353`。 建议: - 继续使用 App Store Connect 上的版本 `2.1.28`,但生成新的 `CFBundleVersion`,例如 `20260610.HHMM`。 - 如果项目发布规则要求递增 patch,则按项目规则升到下一个 patch 版本。 - 上传新 `.pkg` 后,在 App Store Connect 当前版本中选择新 build,再重新提交审核。 ## 建议补充的真机/审核前验收 在 macOS `26.5` 上执行: 1. 展开最终 `.pkg`,验证包内 `.app` 签名。 2. 用 `installer` 安装到干净位置或 `/Applications`。 3. 删除旧版本 App 后启动新安装版本。 4. 确认首次启动不会立即退出。 5. 确认菜单栏/后台 UI 可见或符合预期。 6. 确认麦克风权限弹窗能正常出现。 7. 确认无模型时不会崩溃,而是显示下载/选择模型流程。 8. 安装一个本地模型后做一次基础识别烟测。 建议记录: - build number; - `.pkg` SHA256; - `sw_vers`; - `codesign` 验证输出; - 展开包验证输出; - 启动烟测截图或录屏; - App Store Connect 选择的新 build 截图。 ## 可以回复 Apple 的草稿 修复后重新提交时,可在 App Review Notes 或 Resolution Center 中写: ```text Hello, Thank you for the review. We identified a packaging/signing issue in the previously submitted macOS build that could prevent the app from launching on macOS 26.5. We have rebuilt the app with corrected Mac App Store signing entitlements and verified the final expanded package with codesign --verify --deep --strict on macOS 26.5. The app does not require sign-in. Reviewers can launch the app directly. If no local speech model is installed, please use the in-app model download flow first. After a model is installed, speech recognition runs locally on the Mac. Thank you. ``` ## 本次不能再遗漏的点 - 不要把 `pkgutil --check-signature` 当成 App 可启动的证明;它只证明 installer package 签名,不证明 Payload 里的 `.app` 签名有效。 - 不要只检查 `.pkg` 外层;必须检查最终展开后的 Payload。 - 不要忽略 `codesign -d --entitlements :-` 的 warning;本次 `invalid entitlements blob` 应视为硬失败。 - 不要再提交旧 build。 Docs/KM2.项目管理-非业务直接相关/AppStore打包连续返工经验教训-20260611.md
New file @@ -0,0 +1,246 @@ # App Store 打包连续返工经验教训 日期:2026-06-11 对象:PrivateVoice Dictation macOS App Store 发布包 ## 结论 这两次被 Apple 拒绝,本质不是产品功能问题,而是发布工程质量问题。失败点都发生在最终上传给 App Store Connect 的 `.pkg` 包内,说明之前的验证没有以 Apple 实际接收的最终产物为唯一对象,也没有覆盖 Mac App Store 签名链路的关键约束。 以后只要是 Mac App Store 上传包,不能再靠“本地能构建”“本地 codesign 看起来通过”“某个中间 `.app` 看起来正常”来判断可提交。唯一有效判断对象是 `productbuild` 后的最终 `.pkg`,必须展开最终 `.pkg` 后逐项验证。 ## 两次失败 ### 第一次:`ITMS-91109` quarantine 属性 Apple 反馈: ```text ITMS-91109: Invalid package contents - The package contains one or more files with the com.apple.quarantine extended file attribute, such as “.../Payload/PrivateVoice Dictation.app/Contents/embedded.provisionprofile”. ``` 根因: - 最终 `.pkg` payload 内存在 `com.apple.quarantine` 扩展属性。 - 旧流程没有把“展开最终 `.pkg` 并扫描 payload 全部文件 xattr”作为硬性门禁。 - 检查重心放在源码目录或构建中间产物上,没覆盖 Apple 实际处理的最终包内容。 应该提前挡住的检查: ```bash pkgutil --expand-full FINAL.pkg expanded xattr -lr expanded | grep com.apple.quarantine ``` 如果命中,必须失败,不能上传。 ### 第二次:`Validation failed (409)` 签名证书不在 profile 内 Apple 反馈: ```text Invalid Code Signing. The executable '.../Payload/PrivateVoice Dictation.app/Contents/Frameworks/libonnxruntime.1.24.4.dylib' must be signed with the certificate that is contained in the provisioning profile. ``` 根因: - 失败 build `20260611.1636` 使用 app signing certificate: `404B695635AF53AAF0A65956B935115BD5A9DD50` - embedded provisioning profile 的 `DeveloperCertificates` 只允许: `0D7D5484FDE4EC1CA2C9225F1E71E65A256A1E7C` - 同一个 Team 下存在多个同名/相近用途证书,脚本允许传错证书。 - 旧验证只检查了 entitlements、Team ID、App ID、quarantine,没有检查所有可执行代码的签名叶证书是否属于 profile。 应该提前挡住的检查: ```bash codesign -v -R='certificate leaf = H"<PROFILE_CERT_SHA1>"' "Payload/PrivateVoice Dictation.app" codesign -v -R='certificate leaf = H"<PROFILE_CERT_SHA1>"' "Payload/PrivateVoice Dictation.app/Contents/Frameworks/libonnxruntime.1.24.4.dylib" ``` payload app、主可执行、`Contents/Frameworks` 下所有 dylib / executable 都必须通过。 ## 直接责任 这两次返工暴露出同一个问题:发布验证标准不完整。 具体失误: - 把“中间产物验证”当成“最终上传包验证”。 - 没有把 Apple 邮件里的错误类型转化为项目内可重复执行的阻断脚本。 - 第一次修 quarantine 后,没有系统性扩大 Mac App Store 签名校验矩阵。 - 对 provisioning profile、entitlements、codesign identity 三者之间的关系检查不足。 - 没有强制要求“新 build 上传前必须通过稳定脚本”,导致问题在 App Store Connect 侧才暴露。 这不是偶发问题,属于发布流程门禁缺失。 ## 以后必须执行的硬性门禁 以下检查不通过,禁止上传 App Store Connect。 ### 1. 只验证最终 `.pkg` 必须以最终上传的 `.pkg` 为唯一验收对象。 禁止用以下对象代替: - build 目录里的 `.app` - 签名前的 `.app` - productbuild 前的 staging 目录 - 本地 `open` 启动结果 - 旧包或相邻 build 的验证结果 ### 2. build 编号必须唯一 每次重新打包都必须生成新的 build 编号,格式: ```text YYYYMMDD.HHMM ``` 禁止复用已经上传、失败、废弃或无法确认状态的 build 编号。 ### 3. 最终 `.pkg` 必须展开验证 必须执行项目稳定脚本: ```bash privatevoice.src/scripts/verify-macappstore-pkg-macos.sh FINAL.pkg \ --profile "PATH/TO/embedded.provisionprofile" \ --expected-bundle-id com.shanghai3168.privatevoicedictation \ --expected-version 2.1.28 \ --expected-build YYYYMMDD.HHMM ``` 脚本必须至少覆盖: - package 签名存在且有效 - payload app `codesign --verify --deep --strict` 通过 - signed entitlements 可以正常提取,不能出现 `invalid entitlements blob` - signed entitlements 与 embedded profile 的 App ID / Team ID 匹配 - signed entitlements 不包含错误的 `keychain-access-groups` - payload 不含 `com.apple.quarantine` - `Info.plist` 中 `ITSAppUsesNonExemptEncryption=false` - payload app、主可执行、Frameworks 下 dylib 的签名证书都属于 profile `DeveloperCertificates` ### 4. 构建脚本必须开工前检查 profile 证书 构建脚本不能等到上传后才发现证书不匹配。开工前必须: - 解码 provisioning profile - 提取 `DeveloperCertificates` - 解析传入的 `PRIVATEVOICE_APPSTORE_APP_IDENTITY` - 确认 app signing identity 的 SHA-1 在 profile 允许列表内 如果不在列表内,必须立即失败。 ### 5. 签名后不能再修改 `.app` 签名完成后,不能再向 `.app` bundle 写入、复制、删除或调整任何文件。 尤其不能在签名后修改: - `Contents/Info.plist` - `Contents/embedded.provisionprofile` - `Contents/Frameworks/*.dylib` - `Contents/Resources/*` - `_CodeSignature/CodeResources` 任何签名后修改都必须回到“重新签名 -> productbuild -> 展开终包验证”的完整流程。 ### 6. App Store 包和普通 DMG 包必须分开 Mac App Store `.pkg` 与普通分发 `.dmg` 不是同一种产物,不能共用验收结论。 Mac App Store 包必须重点验证: - App Sandbox entitlements - embedded provisioning profile - MAS application identifier - Installer certificate - App certificate 与 profile 证书匹配 - 最终 `.pkg` payload xattr 普通 DMG 验证不能替代 MAS 验证。 ## 发布前检查清单 每次上传前必须在 release manifest 中记录以下内容: - version - build - git branch - git commit - 构建命令 - 验证命令 - 最终 `.pkg` 绝对路径 - SHA256 - app signing identity SHA-1 - installer signing identity SHA-1 - provisioning profile 来源 - `verify-macappstore-pkg-macos.sh` PASS 结果 - 已废弃 build 列表 没有 release manifest,不得称为可上传包。 ## 错误处理规则 如果 Apple 返回新错误: 1. 先把 Apple 原文完整保存到项目文档。 2. 判断错误发生在源码、签名、打包、上传处理、审核运行环境中的哪一层。 3. 不要只修当前表象;必须反向补一条项目内稳定脚本门禁。 4. 新包必须使用新 build 编号。 5. 旧失败 build 必须在 release 文档中标记为 deprecated。 6. 重新上传前必须跑完整稳定验证脚本。 ## 当前已沉淀的脚本门禁 现有稳定入口: ```text privatevoice.src/scripts/build-macappstore-pkg-macos.sh privatevoice.src/scripts/verify-macappstore-pkg-macos.sh ``` 以后 Mac App Store 上传包只能通过这条链路生成和验证。临时拼 `codesign`、`pkgutil`、`xattr`、GUI 截图或手动观察只能用于排查,不能作为发布验收依据。 ## 本轮明确废弃的 build 以下 build 不得再次上传: - `20260605.353` - `20260610.1247` - `20260611.1636` 当前替代 build: - `20260611.1907` 最终上传包: ```text /Users/ar/Projects/PrivateVoice/.worktrees/appstore-2.1.28-launch-hotfix/privatevoice.src/build/macappstore-hotfix-20260611.1907/universal/PrivateVoice-Dictation-2.1.28-build20260611.1907-universal-macappstore.pkg ``` SHA256: ```text f5283fbe25689bfa6b7ead585318072c39acd9f91f87f34bd8053653342da90f ``` ## 以后对 AI / Codex 的要求 处理 App Store 发布任务时,Codex 必须默认执行以下动作: - 先读取全局和项目 `AGENTS.md`。 - 先确认当前分支、worktree、git status。 - 不碰与发布无关的产品功能、UI、识别逻辑、宣传物料。 - 只通过项目稳定脚本构建和 QA。 - 不用临时命令替代发布门禁。 - 不把中间产物当最终产物。 - 不在缺少 manifest、tag、SHA256、验证记录时说“可以上传”。 - Apple 每暴露一个新类型错误,都必须反向补进稳定验证脚本或 release checklist。 这条规则以后优先级高于“赶紧打一个包”。发布质量不过门,越快上传只会越快返工。 Docs/KM2.项目管理-非业务直接相关/QA工作-经验与教训.md
@@ -13,3 +13,11 @@ - 原因:QA 判断混淆了 macOS packaging 元数据表现形式和 Transporter `91109` 的实际拒绝条件。 - 改进:以后检查 Mac App Store `.pkg` 时,必须展开包并检查具体 xattr 名称;`91109` 的硬拦截项是 `com.apple.quarantine`,不能仅凭 `pkgutil --payload-files` 里出现 `._*` 下结论。 - 关联:`02-P-NBL/freeze/20260605-mac-app-store-clean-rebuild.md`、`02-P-NBL/freeze/20260605-mac-app-store-upload-preflight.md`。 ## 2026-06-23|Mac App Store QA 不能只看启动日志 - 触发:用户指出 `2.1.28 (20260614.0102)` 第四次被 Apple 拒绝,拒绝原因为 Accessibility keystrokes 非辅助用途和启动后空白屏。 - 失误:AI 把 `Wails application started` 日志和包签名验证当成可提交证据,没有验证审核员真实看到的首屏 UI,也没有把 MAS forbidden API / 权限用途纳入发布门禁。 - 原因:QA 门禁仍偏向包体和进程层,没有覆盖 App Store 规则层、TCC 权限用途、WebView 首屏渲染、干净账号首次启动和 TestFlight/MAS 安装路径。 - 改进:以后 Mac App Store build 必须同时通过政策门禁和视觉门禁:禁止未解释或非辅助用途的 Accessibility/keystroke 路径进入 MAS 包;最终安装后必须截图或录屏证明窗口非空白、首屏可交互、无模型/无权限/干净 TCC 状态可恢复。 - 关联:`Release/AppStore-2.1.28-20260614.0102-launchfix/RELEASE_MANIFEST.md`、`privatevoice.src/internal/hotkey/hotkey_darwin.go`、`privatevoice.src/internal/input/paste_darwin.go`、`privatevoice.src/frontend/src/App.svelte`。 README.md
@@ -1,10 +1,10 @@ # VoiceSnap 语闪 # PrivateVoice > 长按说话,松手即输 —— 离线 · 极速 · 跨平台   VoiceSnap 是一款离线语音转文字工具。按住快捷键说话,松开即识别并输入文字到任意应用。无需联网,无需注册,开箱即用。 PrivateVoice 是一款离线语音转文字工具。按住快捷键说话,松开即识别并输入文字到任意应用。无需联网,无需注册,开箱即用。 ## 核心特性 @@ -28,12 +28,39 @@ - **系统托盘常驻** — 关闭窗口不退出 - **开机自启** — 可选 ## 支持的输入语言 当前 PrivateVoice 的产品语言版本为 **中文**、**English**、**French**、**German**、**Spanish**、**Italian**、**Portuguese**、**Japanese** 和 **Korean**。识别模型已经扩展到 SenseVoice、Moonshine English、Parakeet English、Parakeet TDT v3、日语 Zipformer、韩语 Zipformer、Qwen3-ASR 和 X-ASR zh-en 960ms;不同模型覆盖的输入语言和成熟度不同。 | 输入语言 / 场景 | 当前支持状态 | 默认 / 可选模型 | 说明 | |---|---|---|---| | 中文(普通话、简体 / 繁体) | 正式支持 | 默认:SenseVoice;可选高级:Qwen3-ASR;实验:X-ASR zh-en 960ms | 中文语言版本默认使用 SenseVoice,适合中文听写和日常输入。 | | English | 正式支持 | 默认:Moonshine English;可选高级:Parakeet English;实验:X-ASR zh-en 960ms | English 语言版本默认使用 Moonshine English,Parakeet 适合更高准确率的英文输入。 | | French / German / Spanish / Italian / Portuguese | 正式支持 | 默认:Parakeet TDT v3 | 欧洲五语统一使用 NVIDIA / sherpa-onnx 的 Parakeet TDT v3 多语种离线模型。Portuguese 覆盖 `pt-PT` 和 `pt-BR` 的基础识别路径。 | | 中英混输 | 正式支持场景 | SenseVoice、Qwen3-ASR、X-ASR zh-en 960ms | 适合中文句子中夹杂英文术语、产品名、代码词等场景。 | | 粤语(Cantonese) | 模型支持,尚非独立语言版本 | SenseVoice、Qwen3-ASR、X-ASR zh-en 960ms | 底层模型包含粤语能力;当前建议在中文语言版本下使用。 | | 日语(日本語) | 正式支持 | 默认:Zipformer Japanese | 日语语言版本默认使用 ReazonSpeech 日语专项 Zipformer 离线模型。 | | 韩语(한국어) | 正式支持 | 默认:Zipformer Korean | 韩语语言版本默认使用韩语专项 Zipformer 离线模型。 | | 欧洲其他语种 | 模型支持,尚非独立语言版本 | Parakeet TDT v3 | Parakeet TDT v3 支持 25 种欧洲语言;当前只把 French、German、Spanish、Italian、Portuguese 作为正式产品语言版本。 | | 其他语言 | 暂未正式支持 | 无独立默认模型 | 系统语言不在当前正式语言范围内时,当前产品会回落到 English 语言版本。 | | 识别模型 | 模型状态 | 覆盖的输入语言 / 场景 | 所属语言版本 | 说明 | |---|---|---|---|---| | SenseVoice | 默认轻量模型 | 中文、粤语、English、日语、韩语 | 中文默认 | 轻量模型,当前推荐用于中文和中英混输,也提供日语、韩语等底层识别能力。 | | Qwen3-ASR | 高级模型 | 中文、简繁中文、粤语、中英混输 | 中文可选 | 体积更大,适合中文长句、技术词和更高准确率需求。 | | Moonshine English | 默认轻量模型 | English | English 默认 | 轻量英文离线模型,适合英文日常听写。 | | Parakeet English | 高级模型 | English | English 可选 | 当前接入的是 `parakeet-unified-en-0.6b` 英文模型,适合长句、技术词和更高准确率需求。 | | Parakeet TDT v3 | 默认多语种模型 | French、German、Spanish、Italian、Portuguese | 欧洲五语默认 | 一个模型包覆盖欧洲五语,适合离线听写;发布前基准对比 Canary-1B-v2 和 Whisper large-v3-turbo。 | | Zipformer Japanese | 默认专项模型 | Japanese | 日语默认 | 基于 ReazonSpeech 的日语专项离线模型。 | | Zipformer Korean | 默认专项模型 | Korean | 韩语默认 | 韩语专项离线模型。 | | X-ASR zh-en 960ms | 实验模型 | 中文、English、粤语、中英混输 | 中文 / English 可选实验项 | 实验性中英流式模型,用于评估更低延迟和未来“边说边出字”体验。 | ## 快速开始 ### 用户 1. 从 [Releases](https://github.com/vorojar/VoiceSnap/releases) 下载最新版本 2. **Windows**:解压到任意目录,双击 `voicesnap.exe` 1. 从项目 Releases 页面下载最新版本 2. **Windows**:解压到任意目录,双击 `privatevoice.exe` 3. **macOS**:打开 `.dmg`,拖入 Applications,首次启动需授予辅助功能权限 4. 首次启动自动下载语音模型(~200 MB) 5. 就绪后,在任意输入框中按住 **右 Ctrl**(macOS: **右 Command**)说话,松开即输入 @@ -47,8 +74,8 @@ # - CGO 编译器 (Windows: LLVM MinGW UCRT, macOS: Xcode Command Line Tools) # 克隆 git clone https://github.com/vorojar/VoiceSnap.git cd VoiceSnap/VoiceSnapGo git clone <PrivateVoice 仓库地址> cd PrivateVoice/privatevoice.src # 安装前端依赖 cd frontend && npm install && cd .. @@ -57,16 +84,16 @@ wails3 dev # 生产构建 (Windows) windres voicesnap.rc -o voicesnap.syso CGO_ENABLED=1 go build -ldflags "-H windowsgui -s -w" -o voicesnap.exe . windres privatevoice.rc -o privatevoice.syso CGO_ENABLED=1 go build -ldflags "-H windowsgui -s -w" -o privatevoice.exe . # 生产构建 (macOS) CGO_ENABLED=1 go build -o voicesnap . CGO_ENABLED=1 go build -o privatevoice . ``` ## 使用方法 1. 系统托盘出现 VoiceSnap 图标,引擎加载完成后就绪 1. 系统托盘出现 PrivateVoice 图标,引擎加载完成后就绪 2. **长按模式**:按住快捷键说话,松开即识别并粘贴 3. **自由说话**:短按一下开始,连续说话,再短按一下停止 4. **取消录音**:录音中按 Esc @@ -78,7 +105,7 @@ | 文件 | 大小 | 说明 | |---|---|---| | `voicesnap.exe` | ~15 MB | 主程序 | | `privatevoice.exe` | ~15 MB | 主程序 | | `onnxruntime.dll` | ~15 MB | ONNX Runtime | | `sherpa-onnx-c-api.dll` | ~4 MB | sherpa-onnx C API | | `sherpa-onnx-cxx-api.dll` | ~248 KB | sherpa-onnx C++ API | @@ -88,13 +115,13 @@ | 文件 | 大小 | 说明 | |---|---|---| | `VoiceSnap.app` | ~15 MB | 应用包(含 dylib) | | `PrivateVoice Dictation.app` | ~15 MB | 应用包(含 dylib) | | `models/sensevoice/` | ~200 MB | 语音模型(首次自动下载) | ## 项目结构 ``` VoiceSnapGo/ privatevoice.src/ ├── main.go # 入口:单实例 + Wails 启动 ├── app.go # 编排:热键 → 录音 → 识别 → 粘贴 ├── internal/