AI隐私同意弹窗时机与竞态
记录日期:2026-05-10(修订:统一门闸与场景清单)
记录日期:2026-05-10(修订:统一门闸与场景清单)
1. 问题背景
App 为符合 Apple 审核条款 5.1.1 / 5.1.2,在调用第三方 AI(数据出境/共享)前需向用户说明并获取明确同意。前端通过 AiPrivacyConsentModal(「使用 AI 功能前须知」)与 useAiPrivacyConsent / useAiAccess + 本地键 ai_privacy_consent_v1(zhiyuxing-app/src/lib/ai-privacy.ts)实现「同意一次、持久化」。
需要厘清:弹窗在什么时机出现,以及产品级场景枚举;并收敛异步加载竞态与多入口一致性。(聊天页不做路由预填 / 自动首条发送,以降低复杂度。)
涉及主要文件:
zhiyuxing-app/src/components/AiPrivacyConsentModal.tsxzhiyuxing-app/src/hooks/useAiPrivacyConsent.ts(含useAiAccess)zhiyuxing-app/src/lib/ai-privacy.ts(getAiAccess、hasAiConsentAsync)zhiyuxing-app/app/(tabs)/agent-chat.tsxzhiyuxing-app/app/(tabs)/zhi/self.tsxzhiyuxing-app/app/(tabs)/zhi/vision/[id].tsxzhiyuxing-app/src/lib/deepseek-json.ts(callDeepSeekJson)zhiyuxing-app/src/hooks/useTheoryReportGeneration.ts
2. 问题列表
2.1 弹窗触发条件与产品预期是否一致
现象:合规策略为「即将调用 AI 时再要同意」,而非进入 Tab/页面即弹。
产品级场景(与实现一致):
| 场景 | 入口 | 触发动作 | 预期 |
|---|---|---|---|
| a | 神秘人聊天 | agent-chat.tsx 用户点击发送(sendMessage) |
未同意则 AiPrivacyConsentModal;同意后才 callAgentChat |
| b | 知页 · 自我 | self.tsx 理论介绍弹窗内点击「立即生成」(handleGenerateFromModal → startGeneration) |
与 a/c 相同弹窗与门闸;未同意不关闭理论弹窗、不发起生成 |
| c | 知页 · 愿景 · 愿望详情 | vision/[id].tsx「生成规划」或「重新规划」(handleGenerateWishPlan) |
未同意则弹窗;再走 DeepSeek / 后续链路 |
根因链:
- why1: 弹窗由页面在发起 AI 请求前根据门闸状态打开,而非路由进入即弹
- why2: Agent 对话在
sendMessage内拦截;Vision 在handleGenerateWishPlan内拦截;自我在handleGenerateFromModal内拦截
短期方案:维持上表场景;QA 覆盖「未同意拦截 / 已同意直通」。
长远方案:若产品改为「进入 AI 功能区即披露」,可叠加 Tab 聚焦预检 + 发送前硬闸门。
采纳方案:维持「即将调用 AI 再同意」;场景以 a / b / c 为准。
补充用例:
- 未同意:发送或生成 → 弹窗 →「不同意」关窗 → 再次操作仍弹
- 已同意:同路径不弹,直接请求
2.2 hasConsent 与 consentLoaded 未联合使用导致竞态
现象:存储中已同意的用户,若在异步读盘完成前操作,可能误弹窗;或误开弹窗后在读盘完成后不自动关闭。
根因链:
- why1: Hook 初始
hasConsent = false,hasAiConsentAsync()完成后才更新 - why2: 仅判断
!hasConsent而未区分「未加载」与「未同意」 - why3: 缺少「已同意则关弹窗」的同步副作用
短期方案:
- 发送/生成前:
!consentLoaded则直接 return(不弹窗、不请求) consentLoaded && !hasConsent时再setShowAiConsentModal(true)hasConsent && consentLoaded时将弹窗置为不可见
长远方案:收敛为单一异步入口 getAiAccess(): Promise<{ hasConsent: boolean }>(读存储),与 React 侧 useAiAccess() 派生 canInvokeAi = consentLoaded && hasConsent,页面不手写组合判断。
采纳方案:已采纳长远方案(getAiAccess + useAiAccess),并在 a/b/c 页面落地门闸与关窗逻辑。
补充用例:
- 冷启动后立刻发送:未 loaded 时不应出现「假未同意」弹窗
- 读盘完成后已同意:不应残留误开弹窗
2.3 路由自动首条(已移除)
结论:为降低复杂度,神秘人聊天页不再支持通过路由参数(原 initialMessage / initialFromRoute)预填输入框或 effect 自动调用 sendMessage;autoSentInitialRef 及相关 useEffect 已删除,代码与文档不留该路径。
采纳方案:不实现深链首条;首条消息仅由用户在输入框编辑后手动发送。
2.4 同意后不自动重试当前意图
现象:点「我已知晓,同意继续」后不会自动重发刚才那条或重新执行生成,需用户再点一次。
采纳方案:暂不实施(产品保持二次点击即可)。
2.5 多入口对「未同意」处理不一致
现象:理论报告曾在 Hook 内仅写任务错误文案,与 a/c 弹窗体验不一致。
根因链:各功能独立接入,未共用弹窗 + 门闸。
短期方案:自我页在 handleGenerateFromModal 先走门闸 + 弹窗。
长远方案:已采纳——与 2.2 相同:useAiAccess + AiPrivacyConsentModal;useTheoryReportGeneration 内保留 getAiAccess 双保险。
采纳方案:长远方案(统一门闸 + 弹窗)。
2.6 删除 hasAiConsentSync
结论:全仓无 TS/TSX 引用时删除,无运行时行为变化(原实现恒为 false,无法表达真实状态)。
采纳方案:已删除;同意判断统一为 getAiAccess / hasAiConsentAsync 与 useAiAccess。验证:rg hasAiConsentSync zhiyuxing-app。
3. 小结
- 弹窗时机:a / b / c 三处均为用户明确触发会调 AI 的动作时再弹,非进页即弹。
- 门闸:
useAiAccess(canInvokeAi、consentLoaded)+ 异步getAiAccess;修复竞态与多入口一致。 - 聊天入口:无路由自动发首条;仅手动发送(见 2.3)。
- 2.4:同意后不自动重试——暂不实施。