v2.1.0: 一轮最高标准审计后的数据安全、隐私与健壮性加固
v2.1.0: 一轮最高标准审计后的数据安全、隐私与健壮性加固
这是一轮系统性代码审计后的集中修复版本:覆盖数据丢失、隐私泄漏、进程健壮性、发布链安全与无障碍打磨,共修复 29 项审计条目并新增回归测试(单元测试 116 → 160)。绝大多数用户无需任何操作即可获益;少数条目仅影响新保存的数据(见下)。
🔒 数据安全与隐私
- 多句听写不再只保留最后一句:跨长停顿的首句音频不再被裁掉。
- 实时听写(Bailian/Volcengine)在 socket 中途断开或收尾报错时,不再摧毁已采集的完整录音——有兜底时降级到本地/批量转写,无兜底时如实呈现原始错误。
- 「允许本地兜底」对实时独占供应商真正生效:空转写不再过早判定「无语音」而丢弃录音。
- Secure Input(密码框、
sudo、部分登录框)下不再静默丢字:粘贴被系统丢弃时保留剪贴板供手动粘贴,并在胶囊明确提示。 - 听写中途切换输入框,文字不再落进原来的框。
- 转写文本写入剪贴板时标记为「机密」(
org.nspasteboard.ConcealedType),Alfred / Paste / Raycast / Maccy / Pastebot 等剪贴板管理器不再长期留存听写内容。 - 敏感应用(密码管理器、银行等)保护统一覆盖插入 / 历史 / 日志 / 云推理四条出口——密码框里的听写不再被上传云端或写入历史。
- 删除历史 / 清空后不再在数据库与
-wal里残留明文。 - 保留策略(90 天 / 行数上限)现在有明确的界面告知。
🛠 健壮性
- 实时事件改为单消费者顺序处理,杜绝迟到的 partial 覆盖已定稿的 final 造成转写错乱。
停止增加看门狗,异常挂起也能在有界时间内收尾,不再永久卡在「停止中」。- 慢连接冷启动不再静默丢失开头语音;五家实时供应商的收尾等待统一到一致的超时预算。
- 本地模型服务器进程在崩溃/强退后会被回收,不再泄漏数 GB 内存;退出仅 SIGTERM 无效时升级到 SIGKILL。
- 本地模型下载改为校验和 + 原子替换,失败不再删掉已有可用模型。
- 供应商错误提示统一截断与脱敏,不再把上游 HTML 或回显的请求内容抛给用户。
- 初始化失败不再假装「就绪」:进入可重试的降级态;退出前对在途听写、剪贴板恢复、设置写入做有界 flush。
⚡️ 性能与打磨
- 听写时打开控制面板不再每帧全量重渲染(50Hz → 仅相关视图)。
- 设置改动改为去抖写盘,滑块拖拽不再每帧重写整块设置。
- 历史清理不再每次写入都跑一次全表反连接。
- 词汇规则(避免词 / 替换规则)真正生效:作为推理提示软约束 + 收尾文本后处理。
- 无障碍:胶囊 VoiceOver 播报覆盖全部相位;HUD 与控制面板尊重「减弱动态效果」「增强对比度」;小图标命中区达 44pt;引导页在拒绝辅助功能权限时可跳过。
🧰 发布链
- CI 增加 pbxproj/project.yml 漂移门禁、appcast「本次条目」校验、以及测试执行/跳过下限门禁。
- 发布产物校验断言 bundled 模型运行时与 MediaRemote 适配器确实在
.app内且架构匹配;DMG 打包改用唯一卷名与相对路径校验和。
v2.1.0: Data-safety, privacy, and robustness hardening after a top-standard audit
This is a consolidated fix release following a systematic code audit: it covers data loss, privacy leaks, process robustness, release-pipeline safety, and accessibility polish — 29 audit items fixed with regression tests added (unit tests 116 → 160). Nearly all users benefit with no action required; a few items affect only newly-saved data (see below).
🔒 Data safety & privacy
- Multi-utterance dictation no longer keeps only the last sentence — the first utterance across a long pause is no longer trimmed away.
- Realtime dictation (Bailian/Volcengine) no longer destroys the captured recording when the socket dies mid-session or finalize errors — it degrades to local/batch transcription when a fallback exists, and surfaces the real error otherwise.
- "Allow local fallback" now actually works for realtime-only providers: an empty transcript no longer prematurely reports "no speech" and discards the recording.
- Under Secure Input (password fields,
sudo, some login forms) text is no longer silently dropped — when the paste is rejected the transcript is kept on the clipboard for a manual paste and the capsule explains why. - Switching text fields mid-dictation no longer lands the text in the old field.
- Transcripts written to the clipboard are marked concealed (
org.nspasteboard.ConcealedType), so clipboard managers (Alfred / Paste / Raycast / Maccy / Pastebot) no longer retain dictation content. - Sensitive-app protection (password managers, banking, etc.) now covers all four sinks — insertion / history / logs / cloud reasoning — so a password dictated into a protected app is neither uploaded to the cloud nor stored in history.
- Clearing history no longer leaves plaintext behind in the database or the
-walsidecar. - The retention policy (90 days / row cap) is now disclosed in the UI.
🛠 Robustness
- Realtime events are now processed by a single ordered consumer, so a late partial can no longer overwrite a finalized result.
stop()gains a watchdog so a hung teardown still finalizes within a bounded time instead of parking in "stopping" forever.- Slow cold-start connects no longer silently drop leading audio; the finalize wait across all five realtime providers now shares one consistent timeout budget.
- Local model-server processes are reaped after a crash/force-quit instead of leaking multiple GB; quit escalates SIGTERM → SIGKILL when a child ignores SIGTERM.
- Local model downloads now verify a checksum and swap atomically, so a failed download no longer deletes an existing good model.
- Provider errors are uniformly truncated and sanitized, so upstream HTML or echoed request content no longer reaches the user.
- A failed init no longer pretends to be "ready" — it enters a retryable degraded state; quit performs a bounded flush of in-flight dictation, clipboard restore, and settings writes.
⚡️ Performance & polish
- Opening the control panel during dictation no longer re-renders the whole panel every frame (50 Hz → only the relevant views).
- Settings changes are debounced to disk, so a slider drag no longer rewrites the entire settings blob every frame.
- History pruning no longer runs a full-table anti-join on every single write.
- Vocabulary rules (avoided terms / replacement rules) now actually take effect: as a soft reasoning-prompt constraint plus final-text post-processing.
- Accessibility: the capsule's VoiceOver announcements cover every phase; the HUD and control panel honour Reduce Motion and Increase Contrast; small icon buttons meet the 44 pt hit-target minimum; onboarding can be advanced when Accessibility permission is declined.
🧰 Release pipeline
- CI gains a pbxproj/project.yml drift gate, an appcast "current release present" check, and a test execution/skip floor gate.
- Release artifact verification asserts the bundled model runtimes and the MediaRemote adapter are actually inside the
.appwith matching architecture; DMG packaging switched to a unique volume name and relative-path checksums.