AI隐私同意弹窗时机与竞态

记录日期:2026-05-10(修订:统一门闸与场景清单)

by 于尘 ·
知与行 CloudBase

记录日期:2026-05-10(修订:统一门闸与场景清单)

1. 问题背景

App 为符合 Apple 审核条款 5.1.1 / 5.1.2,在调用第三方 AI(数据出境/共享)前需向用户说明并获取明确同意。前端通过 AiPrivacyConsentModal(「使用 AI 功能前须知」)与 useAiPrivacyConsent / useAiAccess + 本地键 ai_privacy_consent_v1zhiyuxing-app/src/lib/ai-privacy.ts)实现「同意一次、持久化」。

需要厘清:弹窗在什么时机出现,以及产品级场景枚举;并收敛异步加载竞态与多入口一致性。(聊天页不做路由预填 / 自动首条发送,以降低复杂度。)

涉及主要文件:

  • zhiyuxing-app/src/components/AiPrivacyConsentModal.tsx
  • zhiyuxing-app/src/hooks/useAiPrivacyConsent.ts(含 useAiAccess
  • zhiyuxing-app/src/lib/ai-privacy.tsgetAiAccesshasAiConsentAsync
  • zhiyuxing-app/app/(tabs)/agent-chat.tsx
  • zhiyuxing-app/app/(tabs)/zhi/self.tsx
  • zhiyuxing-app/app/(tabs)/zhi/vision/[id].tsx
  • zhiyuxing-app/src/lib/deepseek-json.tscallDeepSeekJson
  • zhiyuxing-app/src/hooks/useTheoryReportGeneration.ts

2. 问题列表

2.1 弹窗触发条件与产品预期是否一致

现象:合规策略为「即将调用 AI 时再要同意」,而非进入 Tab/页面即弹。

产品级场景(与实现一致)

场景 入口 触发动作 预期
a 神秘人聊天 agent-chat.tsx 用户点击发送(sendMessage 未同意则 AiPrivacyConsentModal;同意后才 callAgentChat
b 知页 · 自我 self.tsx 理论介绍弹窗内点击「立即生成」(handleGenerateFromModalstartGeneration 与 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 hasConsentconsentLoaded 未联合使用导致竞态

现象:存储中已同意的用户,若在异步读盘完成前操作,可能误弹窗;或误开弹窗后在读盘完成后不自动关闭

根因链

  • why1: Hook 初始 hasConsent = falsehasAiConsentAsync() 完成后才更新
  • 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 自动调用 sendMessageautoSentInitialRef 及相关 useEffect 已删除,代码与文档不留该路径。

采纳方案不实现深链首条;首条消息仅由用户在输入框编辑后手动发送


2.4 同意后不自动重试当前意图

现象:点「我已知晓,同意继续」后不会自动重发刚才那条或重新执行生成,需用户再点一次。

采纳方案暂不实施(产品保持二次点击即可)。


2.5 多入口对「未同意」处理不一致

现象:理论报告曾在 Hook 内仅写任务错误文案,与 a/c 弹窗体验不一致。

根因链:各功能独立接入,未共用弹窗 + 门闸。

短期方案:自我页在 handleGenerateFromModal 先走门闸 + 弹窗。

长远方案已采纳——与 2.2 相同:useAiAccess + AiPrivacyConsentModaluseTheoryReportGeneration 内保留 getAiAccess 双保险

采纳方案长远方案(统一门闸 + 弹窗)


2.6 删除 hasAiConsentSync

结论:全仓无 TS/TSX 引用时删除,无运行时行为变化(原实现恒为 false,无法表达真实状态)。

采纳方案已删除;同意判断统一为 getAiAccess / hasAiConsentAsyncuseAiAccess。验证:rg hasAiConsentSync zhiyuxing-app

3. 小结

  • 弹窗时机a / b / c 三处均为用户明确触发会调 AI 的动作时再弹,非进页即弹。
  • 门闸useAiAccesscanInvokeAiconsentLoaded)+ 异步 getAiAccess;修复竞态与多入口一致。
  • 聊天入口:无路由自动发首条;仅手动发送(见 2.3)。
  • 2.4:同意后不自动重试——暂不实施
← 所有文章