# AI 应用 APP1-APP8 功能需求 - 目的检查文档 > 版本:2026-04-22 (v4,全景资料集成:8 维度并行调研 — 生产端 / DB schema / 合规 / MCP / admin-web / 后端路由服务 / 历史审计 / P2/P3 外围) > 适用范围:`apps/backend/app/ai/` + `apps/admin-web/src/pages/AI*` + `apps/miniprogram/miniprogram/` + `apps/demo-miniprogram/miniprogram/`(UI 标杆) > PRD 来源: > > - [docs/prd/AI需求2.md](../prd/AI需求2.md) — 8 个 AI 应用功能/参数/返回格式主表 > - [docs/prd/ai-app-prompts.md](../prd/ai-app-prompts.md) — System Prompt 详细定义 > - [docs/prd/Neo_Specs/NS2-ai-prompt-refinement.md](../prd/Neo_Specs/NS2-ai-prompt-refinement.md) — 数据层/拼接/page_context 文本化 > - [docs/prd/specs/P5-miniapp-ai-integration.md](../prd/specs/P5-miniapp-ai-integration.md) — 小程序 AI 集成验收 > - [docs/prd/specs/P14-ai-dashscope-migration.md](../prd/specs/P14-ai-dashscope-migration.md) — 熔断限流预算硬约束 > - [docs/prd/specs/P15-ai-monitoring-testing.md](../prd/specs/P15-ai-monitoring-testing.md) — 监控后台与测试体系 --- ## 0. 总体原则 - demo-miniprogram 是**样式标杆**(假数据驱动),生产 miniprogram 所有 AI 字段的排版/配色必须与 demo 一致。 - 前端**不直接调用 DashScope**,所有 AI 产物统一通过 `ai_cache` 或 SSE 流式接口消费。 - 后端统一响应包装 `{code:0, data:...}`,snake_case 自动转驼峰。 - cache miss 时前端必须**清空陈旧数据**。 - 告警统一走 WebSocket `/ws/ai-alerts/{site_id}`。 ## 0.1 PRD 硬性数字约束(权威表) > 所有数字来自 `docs/prd/` 精读,代码实现与此不符时以 PRD 为准并回退排查。 | 维度 | 约束值 | PRD 来源 | | ------ | -------- | --------- | | 应用总数 | 8(APP1-APP8) | AI 需求 2 | | 财务洞察组合 | 8 时间 × 9 区域 = 72 | AI 需求 2 | | 线索分类(APP3) | 3 个枚举 | P5 / ai-app-prompts | | 线索分类(APP6/8) | 6 个枚举 | P5 / ai-app-prompts | | 单条线索 `summary` | ≤ 20 字(简短可作标题) | AI 需求 2 | | 单条线索 `detail` | ≤ 120 字 | AI 需求 2 | | APP7 `summary` 长度 | 200-400 字,开头定性短句 ≤ 25 字 | ai-app-prompts | | APP8 `clues` 数组 | ≤ 5 条 | ai-app-prompts | | APP5 `tactics` 数组 | 2-4 条,每条 `script` ≤ 150 字 | AI 需求 2 | | APP6 `clues` 数组 | 0-10 条(按价值排序) | AI 需求 2 | | APP3 `clues` 数组 | 1-5 条(按价值排序) | AI 需求 2 | | System prompt 上限(APP2-8) | ≤ 8000 字 | NS2 | | System prompt 上限(APP1) | ≤ 4000 字 | NS2 | | page_context 上限 | ≤ 2000 字 | NS2 / page_context.py:21 | | Token 日预算 | 100,000 | P14 | | Token 月预算 | 2,000,000 | P14 | | APP1 限流 | 每用户每分钟 10 次 | P14 | | APP2-8 限流 | 每门店每小时 100 次 | P14 | | 熔断阈值 | 连续 5 次失败 OPEN | P14 | | 熔断恢复窗口 | 60 秒 | P14 | | 关系流失预警 | 停止消费 > 14 天 | ai-app-prompts | | 高粘性阈值 | 月内消费 ≥ 3 次 | ai-app-prompts | | 学习型阈值 | 助教费占比 > 40% | ai-app-prompts | | 当期 < 7 天 | 必须降权表述("初步观察"/"不足为据") | ai-app-prompts | | 财务洞察加载 | < 2 秒(秒级缓存读取) | P5 | | 对话复用窗口(customer/coach/finance) | 3 天 | P5 | | 对话复用窗口(task) | 无时限 | P5 | | 对话复用(general) | 始终新建 | P5 | ## 0.2 跨 APP 编排链路(PRD 定义) ### 消费事件链(严格串行) ``` 新结算单 → APP3 [await] → APP8 [await] → APP7 [await](读 APP8 最新缓存) 如含助教: → APP4 [await](读 APP8 最新缓存) → APP5 ``` **约束**:APP7 与 APP4 均依赖 APP8 完成,确保上游线索就绪。 ### 备注事件链 ``` 备注提交 → APP6 [await] → APP8 [await] ``` ### 任务分配链 ``` 任务分配(priority_recall / high_priority_recall) → APP4 [await](读 APP8 已有缓存,miss 时 prompt 标注"暂无历史线索") → APP5 ``` **约束**:任务链不等 APP8 执行,直接读缓存;消费链则必须等 APP8 完成才能执行 APP4/APP7。 --- ## 1. APP1 聊天(流式对话 + 屏幕上下文) ### 功能目的(PRD) 店员/助教在小程序里与 AI 自由对话("这个客户最近消费变多了为什么"),支持 10 种页面入口带上下文,后端将上下文结构化为 ≤2000 字中文文本注入 prompt。**PRD 参考**:AI 需求 2 表首行 / P5 AC1 / NS2 page_context 设计。 ### 后端请求 schema([schemas.py:16-23](../../apps/backend/app/ai/schemas.py#L16)) ``` ChatStreamRequest { message: str # 用户本轮输入 source_page: str | None # 来源页面标识(10 种枚举) page_context: dict | None # 看板筛选参数(timeDimension/areaFilter/dimension/typeFilter/projectFilter) screen_content: str | None # 「屏幕可见内容」快照(前端采集,后端原样拼入 prompt)← 当前 gap } ``` ### 10 种 sourcePage([page_context.py:24-36](../../apps/backend/app/ai/data_fetchers/page_context.py#L24)) `task-detail / customer-detail / coach-detail / board-finance / board-customer / board-coach / performance / my-profile / task-list / customer-service-records` ### 前端消费链路 | 阶段 | 文件与行号 | 处理 | |------|-----------|------| | 浮按钮属性 | [ai-float-button.ts:29-40](../../apps/miniprogram/miniprogram/components/ai-float-button/ai-float-button.ts#L29) | 接收 `sourcePage` + `pageFilters`(JSON 对象) | | 浮按钮跳转 | [ai-float-button.ts:44-72](../../apps/miniprogram/miniprogram/components/ai-float-button/ai-float-button.ts#L44) | URL 编码 JSON 到 querystring | | Chat 解析 | [chat.ts:234-259](../../apps/miniprogram/miniprogram/pages/chat/chat.ts#L234) | `JSON.parse(decodeURIComponent(options.pageFilters))` ; 回退支持旧单键 `timeDimension/areaFilter/dimension/typeFilter/projectFilter` | | SSE 发送 | [chat.ts:487-490](../../apps/miniprogram/miniprogram/pages/chat/chat.ts#L487) | body: `{chatId, content, sourcePage, pageContext}` | | 后端文本化 | [page_context.py:39-96](../../apps/backend/app/ai/data_fetchers/page_context.py#L39) | `build_page_text(source_page, context_id, site_id, filters)` → 结构化中文 ≤2000 字 | | 后端注入 | xcx_chat.py | 将文本拼入 DashScope `biz_params` 或作为前置 system message | ### screen_content「屏幕可见内容」特性(**待实现 / 文档化 gap**) - schema 已预留字段([schemas.py:22](../../apps/backend/app/ai/schemas.py#L22)) - **预期采集方式**(需前端实现):浮按钮 onTap 时通过 `triggerEvent` 让父页面回传 `this.data` 中可见区块的快照(如 board-finance 当前展示的 `overview/recharge/revenue/...` 卡片数据),序列化为短 JSON 字符串 - **后端消费**:原样拼入 prompt 系统消息(与 `page_context` 互补:page_context 是后端查 DB 的结果;screen_content 是前端用户真实看到的数字) - **验证**:两者都存在时,提示词顺序应为 `screen_content(用户视角) → page_context(权威数据)`,避免矛盾时以 page_context 为准 ### 各 sourcePage 携带字段示例 | sourcePage | pageFilters 字段 | 后端 page_context 输出要点 | |-----------|-----------------|--------------------------| | board-finance | `timeDimension` + `areaFilter` | 汇总笔数 / 总营收 / 笔均 / 优惠率 / 储值活跃度 | | board-customer | `dimension` + `typeFilter` | Top 10 客户 + 排序维度 + 客群分类 | | board-coach | `dimension` + `projectFilter` + `timeDimension` | Top 10 助教 + 服务统计 + 技能筛选 | | performance / task-list / my-profile | `timeDimension` | 个人绩效 / 任务列表 / 本人统计 | | task-detail / customer-detail / coach-detail | 无 filters,走 `contextId` | 单实体详情 | | customer-service-records | 无 | 服务记录流水 | ### Demo 参考(UI 形态) - [chat.ts](../../apps/demo-miniprogram/miniprogram/pages/chat/chat.ts):消息气泡、引用卡片 `referenceCard`、逐字追加动效 - 进入路径:浮按钮仅在 board-*(看板类)与详情页挂载,距底部 200rpx(TabBar 页)/ 120rpx(非 TabBar) ### 验证点(细化) - [ ] 财务看板任意 time/area 切换 → 点浮按钮 → 后端日志可见 `build_page_text("board-finance", ...)` 返回文本含当前切换维度的数字 - [ ] 小程序 SSE body 中 `pageContext` 为 JSON 对象而非字符串(后端解析不失败) - [ ] `pageFilters` 为空对象时不追加 URL 参数,走 general 对话 - [ ] SSE ArrayBuffer 跨 chunk 的 UTF-8 中文字符无乱码(构造「当前营业」被 2 chunk 切分的测试) - [ ] `done` 事件 `conversation_id` 返回后,下一轮发送透传该 id 保持多轮 - [ ] `ai_run_logs.tokens_used` 按 `usage.models` 嵌套结构累加(非顶层 tokens) - [ ] 网络中断重试上限 2 次(2/4/8s 指数退避) - [ ] **screen_content gap 待确认**:若本次上线计划启用,需在浮按钮 onTap 内实现页面快照 triggerEvent;否则保留 schema 占位并在文档注明"暂不采集" --- ## 2. APP2 财务洞察(72 组合预热) ### 功能目的 日结完成后预生成 **8 时间维度 × 9 区域 = 72 组** 财务洞察,命中看板时直接从 `ai_cache` 秒级读取。 ### 返回 schema([schemas.py:93-104](../../apps/backend/app/ai/schemas.py#L93)) ``` App2Result { insights: [ { seq: int, title: str, body: str } ] } ``` ### System Prompt 核心规则([ai_system_prompt_by_app.md:41-93](ai_system_prompt_by_app.md#L41)) - 收入构成三口径不可互换:发生额 / 成交收入 / 现金流入 - 5 类优惠拆解必须落到细分(团购/会员折扣/手动/赠送卡/分摊) - 警戒线:优惠率 >30% / 助教薪酬占比 >40% / 充值占现金流入 25-40% 为健康 - **seq=11/12 固定为板块 F「综合健康度」,置顶作为"本期总结"** - 当期 <7 天需降权表述 - 现金流出为 0 视为录入异常并高亮 ### target_id 拼装([dispatcher.py _app2_target_id](../../apps/backend/app/ai/dispatcher.py)) `${TIME_MAP[selectedTime]}__${selectedArea}`,例:`this_month__hallA` / `last_quarter__all` **TIME_MAP**:`month→this_month`、`lastMonth→last_month`、`week→this_week`、`quarter→this_quarter`、`half6→last_6_months` **9 区域**:`all / hall / hallA / hallB / hallC / vip / snooker / mahjong / ktv` ### cache_type 切分(2026-04-23) - `area='all'` 的 8 组 → `app2_finance` - `area!='all'` 的 64 组 → `app2a_finance_area` ### Demo 展示形态(**关键 UI 规范**) 参考 [board-finance.wxml:208-221](../../apps/demo-miniprogram/miniprogram/pages/board-finance/board-finance.wxml#L208): ``` ┌─ AI 智能洞察(机器人 SVG 图标 + 标题) │ 优惠率Top:团购(5.2%) / 大客户(3.1%) / 赠送卡(2.5%) │ 差异最大:酒水(+18%) / 台桌(-5%) / 包厢(+12%) │ 建议关注:充值高但消耗低,会员活跃度需提升 └─ ``` **字段到 UI 映射**: | 字段 | Demo 类 | 渲染规范 | |------|---------|---------| | `title` | `.ai-insight-dim` | 灰色引导词(如"优惠率Top:"/"差异最大:"),结尾带中文冒号 | | `body` | `.ai-insight-line` 主体 | 白色正文,含数字与百分比 | | 强调片段 | `.ai-insight-underline` | 关键词下划线(需 AI 用 `**...**` 标记,前端轻量 Markdown 渲染) | | 顺序 | seq 升序 | `seq=11/12` 置顶为"本期总结" | | 图标 | `/assets/icons/ai-robot.svg` | **必须 SVG,禁止 emoji 替代**(demo 已标注 CHANGE 2026-03-12) | ### 验证点(细化) - [ ] 72 组 `target_id` 全覆盖(SQL:`SELECT DISTINCT target_id FROM biz.ai_cache WHERE cache_type IN ('app2_finance','app2a_finance_area') AND site_id=? AND created_at::date=CURRENT_DATE` 应 = 72) - [ ] `seq=11/12` 在前端必定置顶(不按数字大小升序,需特判) - [ ] `title` 结尾是中文冒号`:`(便于 dim 样式识别) - [ ] `body` 中 `**xxx**` 标记被轻量 Markdown 渲染为 underline/加粗 - [ ] area 切换后旧 `aiInsights/aiInsightSummary/aiInsightDetails` 立即清空 - [ ] cache miss 时显示占位文案("正在生成洞察,请稍后"),不复用上次数据 - [ ] 现金流出为 0 时 AI 输出能**明确指出录入异常**(单元测试固化该场景) - [ ] 当期 <7 天的时间维度(本周 `this_week` 若当前周三)返回文本含降权用词("初步观察"/"不足为据"等) - [ ] Prompt `KEY_TRANSLATIONS`([app2_finance_prompt.py:24-64](../../apps/backend/app/ai/prompts/app2_finance_prompt.py#L24))无缺字段 - [ ] 熔断/限流后剩余组跳过,`ai_cache.result_json` 不写 NULL --- ## 3. APP3 维客线索(消费端,3 分类) ### 功能目的 消费结算后从 DWS 消费记录提取 **3 类线索**(客户基础/消费习惯/玩法偏好),`providers` 固定「系统」。 ### 返回 schema([schemas.py:71-78, 107-110](../../apps/backend/app/ai/schemas.py#L71)) ``` App3Result { clues: [ ClueItem { category, summary, detail, emoji } ] } ``` ### System Prompt 核心规则([ai_system_prompt_by_app.md:97-125](ai_system_prompt_by_app.md#L97)) - `category` 严格 3 选 1(`App3CategoryEnum`) - 必须输出"下一步做什么"的行动导向,不得仅描述现象 - 消费结构解读:酒水 >40% = 休闲社交客;台费 >80% = 硬核客;助教费 >30% = 学习进阶客 ### 前端消费(经 App8 合并后落 `member_retention_clue`) App3 结果**不直接渲染**,由 App8 读 ai_cache 整合 → 写库 → 前端查客户详情时显示。 ### Demo 展示形态(customer-detail) 参考 [customer-detail.ts:99-145](../../apps/demo-miniprogram/miniprogram/pages/customer-detail/customer-detail.ts#L99) + [customer-detail.wxml:92-110](../../apps/demo-miniprogram/miniprogram/pages/customer-detail/customer-detail.wxml#L92): ``` 维客线索 [AI 徽章] ┌─ [客户/基础] 🎂 生日 3月15日 · VIP会员 · 注册2年 By:系统 ├─ [消费/习惯] 🌙 常来夜场 · 月均4-5次 By:系统 ├─ [消费/习惯] 💰 高客单价 By:系统 │ 近60天场均消费 ¥420,高于门店均值 ¥180;... └─ ... ``` **字段到 UI 映射**: | schema 字段 | Demo `clue-card` 属性 | UI 规范 | |------------|---------------------|--------| | `category` | `tag` | 两行显示(如"客户\n基础"),配色 `categoryColor` 按类映射(primary/success/purple/warning/pink/error) | | `summary` | `title`(含 emoji 前缀) | 主文案,短句 30 字内 | | `emoji` | 拼在 `title` 开头 | 1 个 emoji,类别相关 | | `detail` | `content`(展开区) | 50-100 字展开,可折叠 | | `providers`(App8 合并后补) | `source`("By:系统"/"By:小燕") | 区分系统推断 vs 备注来源 | ### 验证点(细化) - [ ] `category ∈ {客户基础, 消费习惯, 玩法偏好}`(Pydantic 校验 + 前端 `categoryColor` 映射无 undefined) - [ ] `emoji` 字段必填且为单个 emoji(正则 `/^[\p{Emoji}]+$/u`) - [ ] `summary` 以行动导向动词结尾("可推..."/"建议..."/"需..."),非纯描述(单测) - [ ] `detail` 长度 ≥ 50 字且 ≤ 150 字 - [ ] DWS 取数失败时降级到 `_default_member_data`,不导致 dispatcher 崩溃 - [x] Prompt 超 4000 字后仍保留完整消费明细;2026-05-01 合成 100 条完整明细真实调用成功返回 - [ ] 团购客(无储值卡)与储值会员识别正确(AI 不得混淆) --- ## 4. APP4 关系分析(任务链首) ### 功能目的 助教参与消费 / 任务分配时,分析助教-会员关系,产出任务描述、行动建议、一句话总结。 ### 返回 schema([schemas.py:113-118](../../apps/backend/app/ai/schemas.py#L113)) ``` App4Result { task_description: str # 任务详述 action_suggestions: [str] # 行动建议数组 one_line_summary: str # 一句话总结 } ``` ### System Prompt 核心规则([ai_system_prompt_by_app.md:129-156](ai_system_prompt_by_app.md#L129)) - 输出必含:**关系评级**(紧密/一般/疏远)+ 核心原因 + 风险/机会点 - 粘性阈值:月内 ≥3 次 = 高粘性;激励课占比 >40% = 学习型 - 停止消费 >14 天 = 流失预警 ### Demo 展示形态(task-detail 顶部 Banner) 参考 [task-detail.ts:107-112](../../apps/demo-miniprogram/miniprogram/pages/task-detail/task-detail.ts#L107): ``` 关系等级:很好 heartScore=8.5 banner 背景根据 taskType 动态切换 SVG ``` **字段到 UI 映射**: | schema 字段 | Demo 字段 | 位置 | |------------|----------|------| | `one_line_summary` | Banner 下方副标题 | 任务详情头部一行显示 | | `task_description` | `detail.taskTypeLabel` + 主内容 | 任务卡片主体 | | `action_suggestions[]` | 勾选式建议列表 | 可勾选动作项,完成后记录 | ### 验证点(细化) - [ ] `action_suggestions` 为非空 list,每条 ≤ 40 字 - [ ] `one_line_summary` 含关系评级词("紧密/一般/疏远")+ 数字 - [ ] `target_id = ${assistant_id}_${member_id}`,供 App5 复用 - [ ] 助教数据缺失时 `warnings` 数组被填充,前端显示降级提示 - [ ] Prompt 截断 `_MAX_PROMPT_LEN=8000` 保留关键关联字段 --- ## 5. APP5 话术参考(任务链尾) ### 功能目的 基于 App4 的 `one_line_summary` 与关系评级,生成针对性**微信/当面沟通话术**。 ### 返回 schema([schemas.py:121-131](../../apps/backend/app/ai/schemas.py#L121)) ``` App5Result { tactics: [ { scenario: str, script: str } ] } ``` ### System Prompt 核心规则([ai_system_prompt_by_app.md:160-192](ai_system_prompt_by_app.md#L160)) - 微信私信:口语、短、配 1 个 emoji,不群发感 - 当面沟通:引导式提问 > 直接推销 - 会员分层:新客试听 / 成长储值 / 核心赛事邀请 / 流失限时券 - 助教不能:打任意折扣 / 承诺 KOL 免费陪打 - 输出需标注:适用场景 + 建议发送时段 + 预期反应 ### Demo 展示形态(task-detail 话术区) 参考 [task-detail.ts:79-86](../../apps/demo-miniprogram/miniprogram/pages/task-detail/task-detail.ts#L79): ``` 话术参考 ┌─ 王哥您好,好久不见!最近店里新到了几张国际标准斯诺克球桌... [复制] ├─ 王哥,最近忙吗?这周末我们有个老客户专属球友交流赛... [复制] └─ ... 共 5 条 ``` **字段到 UI 映射**: | schema 字段 | Demo `talkingPoints` | 规范 | |------------|---------------------|------| | `scenario` | 折叠区 hint("适用:流失预警"/"适用:储值召回") | 小字副标题,可选展示 | | `script` | 话术主体 | 长按可复制,配置 `copiedIndex` 高亮反馈 | ### 验证点(细化) - [ ] `tactics` 长度 3-5 条(Prompt 约束) - [ ] `script` 含会员姓名或称呼(如"王哥"),非模板化空话 - [ ] `script` 含 0-1 个 emoji(`emoji_count <= 1`) - [ ] 不出现"8折/免费/KOL/抽成"等越权用词(黑词正则拦截) - [ ] App4 失败时 App5 跳过(串行依赖,`context.app4_result is None` 不执行) - [ ] 长按复制触发 `wx.setClipboardData` 成功 - [ ] `target_id` 与 App4 一致(`${assistant_id}_${member_id}`) --- ## 6. APP6 备注分析(备注链首) ### 功能目的 助教提交备注后打分 1-10 并提取 **6 类线索**;`providers` = 备注提交人真实姓名。 ### 返回 schema([schemas.py:134-138](../../apps/backend/app/ai/schemas.py#L134)) ``` App6Result { score: int (ge=1, le=10) clues: [ ClueItem { category, summary, detail, emoji } ] } ``` ### System Prompt 核心规则([ai_system_prompt_by_app.md:196-220](ai_system_prompt_by_app.md#L196)) - **忠于备注原文,不延伸推测** - 8 维度归一到 6 分类(App6CategoryEnum) - 一条备注可对应多个维度 - 情感倾向(正面/中性/负面)影响后续助教触达开场白 - 标注备注是谁写的、何时写的(时效性) ### 前端消费 App6 结果**不直接渲染**,经 App8 整合后写入 `member_retention_clue` 表,前端查客户详情展示(`source` 字段显示"By:小燕")。 ### Demo 展示形态(**客户详情「维客线索」中显示 6 类**) 参考 [customer-detail.ts:99-145](../../apps/demo-miniprogram/miniprogram/pages/customer-detail/customer-detail.ts#L99) 7 条 clue 分布: - 客户基础 × 1(生日/VIP) - 消费习惯 × 2(夜场/高客单) - 玩法偏好 × 1(中式斯诺克) - 促销偏好 × 1(酒水套餐敏感) - 社交关系 × 1(固定球搭子) - 重要反馈 × 1(斯诺克走位需求) ### 验证点(细化) - [ ] `score ∈ [1,10]` 严格校验(Pydantic `ge=1, le=10`) - [ ] `category ∈ {客户基础, 消费习惯, 玩法偏好, 促销偏好, 社交关系, 重要反馈}` - [ ] `providers` 为备注提交人姓名(非"系统"、非空) - [ ] 备注原文中的专有名词("李哥"、"阿杰"、"A12号台")保留在 `detail` 中 - [ ] 备注 >8000 字时截断但保留情感词与动作词 - [ ] score 写入 `ai_cache.score` 列(非 `result_json.score`,供后端排序) --- ## 7. APP7 客户分析(消费链尾,客户画像) ### 功能目的 消费链 App8 完成后串行触发,综合消费 + 备注 + 助教关系输出 **200-400 字客户画像**。 ### 返回 schema([schemas.py:141-152](../../apps/backend/app/ai/schemas.py#L141)) ``` App7Result { strategies: [ { title: str, content: str } ] summary: str # 画像主文 } ``` ### System Prompt 核心规则([ai_system_prompt_by_app.md:224-257](ai_system_prompt_by_app.md#L224)) - **开头一句话定性**("工作日晚间的上班族硬核玩家") - 中段数字:消费结构、频次、客单、助教绑定 - 结尾助教可动 1-2 条建议 - 避免评判语言("消费低"改为"客单 60 元偏低于店均 120 元") - **必须标注数据时间窗**("近 30 天 / 近 90 天") - 备注信息需带【来源:人名,请甄别真实性】标注([app7_customer_prompt.py:64-77](../../apps/backend/app/ai/prompts/app7_customer_prompt.py#L64)) ### Demo 展示形态(customer-detail 头部) 参考 [customer-detail.ts:91-98](../../apps/demo-miniprogram/miniprogram/pages/customer-detail/customer-detail.ts#L91) + [customer-detail.wxml:69-90](../../apps/demo-miniprogram/miniprogram/pages/customer-detail/customer-detail.wxml#L69): ``` 🤖 AI 智能洞察 ┌─ summary:高价值 VIP 客户,月均到店 4-5 次,偏好夜场中式台球, │ 近期对斯诺克产生兴趣。社交属性强... │ 当前推荐策略 │ ■ [green] 最后到店距今 12 天,超出理想间隔 7 天,建议... │ ■ [amber] 客户提到想练斯诺克走位,可推荐专项课程包... │ ■ [pink] 社交属性强,可邀请参加球友赛事活动... └─ ``` **字段到 UI 映射**: | schema 字段 | Demo 字段 | UI 规范 | |------------|----------|--------| | `summary` | `aiInsight.summary` | 开头白色正文段 | | `strategies[].title` | 策略颜色 `color` 枚举前缀 | 不直接渲染(配色由前端枚举轮转:green/amber/pink/...) | | `strategies[].content` | `strategy-text` | 策略卡片主体,每条独立配色条 | | 末条样式 | `.strategy-item-last` | 最后一条无下边距 | **注意**:demo 中 `strategies[].color` 是前端枚举色(green/amber/pink),AI 返回的 `title` 字段应映射为 emoji/关键词而非颜色。实际代码中前端按 index % 6 循环配色(不依赖 AI)。 ### 验证点(细化) - [ ] `summary` 开头为"定性短句"(句号前 ≤ 25 字) - [ ] `summary` 包含时间窗关键词("近 30 天"/"近 90 天"/"近 60 天") - [ ] `summary` 长度 200-400 字 - [ ] `strategies` 长度 2-3 条,前端展示最多 2 条(demo 规范 `index < length - 1` 为非末,`index === length - 1` 为末条) - [ ] 评判性词("差"/"低"/"糟糕")出现时应改写为对比表述(AI 侧或后处理) - [ ] 备注来源标注【来源:xxx,请甄别真实性】进入 prompt - [ ] 消费链中断时 App7 跳过(App8 未完成不执行) - [ ] cache_type = `app7_customer_analysis`,target_id = `member_id` --- ## 8. APP8 线索整合(写库幂等) ### 功能目的 整合 App3 + App6 全部线索 → 去重、统一 6 分类、`providers` 逗号拼接 → **幂等写 `member_retention_clue` 表**。 ### 返回 schema([schemas.py:80-88, 155-158](../../apps/backend/app/ai/schemas.py#L80)) ``` App8Result { clues: [ ConsolidatedClueItem { category: str # 6 选 1 summary: str # 30 字内行动导向 detail: str # 50-100 字展开 emoji: str # 分类对应 emoji providers: str # "消费数据" / "店员小燕备注" / 逗号拼接多源 } ] } ``` ### System Prompt 核心规则([ai_system_prompt_by_app.md:261-290](ai_system_prompt_by_app.md#L261)) - 冲突处理:**备注原文 > 行为推断** - `summary` 禁放数字(数字放 `detail`) - 线索排序:可直接动作 > 身体偏好 > 长期画像 - 不超过 5 条(助教可读性) - 不输出泛化建议("请关心会员"禁止) ### 落库规则([dispatcher.py _write_retention_clue](../../apps/backend/app/ai/dispatcher.py)) ```sql BEGIN; DELETE FROM app.member_retention_clue WHERE site_id=:site AND member_id=:member AND source IN ('ai_consumption','ai_note'); INSERT INTO app.member_retention_clue (...) VALUES (...); COMMIT; ``` - source 取值:消费链触发 → `ai_consumption`;备注链触发 → `ai_note` - `source='manual'`(人工线索)**绝不**删除 ### Demo 展示形态(7 条线索在客户详情,见 App3 表格) ### 验证点(细化) - [ ] `DELETE` + `INSERT` 在同一事务内(pg_stat_activity 查询验证) - [ ] 写入后 `source='manual'` 行数不变(跨事务前后 count 一致) - [ ] 同日多次消费 / 多条备注不会让线索重复累积(幂等) - [ ] `providers` 多源拼接去重(如"消费数据、备注-小燕",不出现"消费数据、消费数据") - [ ] `summary` 不含数字(正则 `/[0-9%¥]/` 不匹配) - [ ] `detail` 含数字(与 summary 分工) - [ ] `clues` 数组长度 ≤ 5 - [ ] 排序:可直接动作类("推课"/"约时段")在前 - [ ] App3 或 App6 任一失败时,App8 仍执行但对应 `_clues=[]` - [ ] cache_type = `app8_clue_consolidated` --- ## 9. 管理后台通用检查 ### AIDashboard(总览) - [AIDashboard.tsx:120](../../apps/admin-web/src/pages/AIDashboard.tsx#L120) `getDashboard()` - 字段:`today_calls` / `today_success_rate` / `today_tokens` / `today_avg_latency_ms` / `trend_7d` - [ ] 数值与 `biz.ai_run_logs` 聚合一致 - [ ] `trend_7d` 折线图日期连续无跳点,单位 ms / % / 次 - [ ] 切换 site_id 过滤器后数字按租户隔离 ### AIOperations(手动执行) - [AIOperations.tsx:67](../../apps/admin-web/src/pages/AIOperations.tsx#L67) `runApp(appType, {site_id, member_id})` 支持 App3-App8(App1/App2 不适用) - [AIOperations.tsx:146](../../apps/admin-web/src/pages/AIOperations.tsx#L146) `triggerEvent({event_type, site_id, member_id, is_forced})` 越过去重 - [ ] `is_forced=true` 可绕过当日去重,`ai_trigger_jobs` 新增一行 - [ ] 返回 `job_id`,tag 色 `processing → success/failed` - [ ] WebSocket `/ws/ai-alerts/{site_id}` 接收 `circuit_open / rate_limited / budget_exceeded / timeout / failed` ### TriggerManager(调度配置) - [TriggerManager.tsx:183](../../apps/admin-web/src/pages/TriggerManager.tsx#L183) `updateTriggerConfig(id, {cron_expression, interval_seconds})` - [ ] cron 表达式校验通过才允许保存 - [ ] 改动后调度器热加载(Scheduler lifespan 监听 meta.scheduled_tasks) --- ## 10. 跨切面共性验证 | 维度 | 检查项 | 验证方法 | |------|--------|---------| | RunLog | 状态转换 `pending→running→success\|failed\|timeout\|circuit_open\|rate_limited\|budget_exceeded` | `SELECT status, COUNT(*) FROM biz.ai_run_logs GROUP BY status` 覆盖 8 种 | | RunLog | `success` 必有非零 `tokens_used` | `SELECT COUNT(*) FROM biz.ai_run_logs WHERE status='success' AND tokens_used=0` = 0 | | RunLog | `request_prompt` ≤ 8000 字符 | `SELECT MAX(length(request_prompt))` ≤ 8000 | | 熔断 | 连续失败 N 次后 OPEN | 人工注入失败 → 第 N+1 次日志 `status=circuit_open` 且**无真实 DashScope 调用** | | 限流 | 分钟/日双维度 | 压测超限后 `rate_limited` 数突增,`tokens_used IS NULL` | | 预算 | 不足时记 `budget_exceeded:` | 人为把日预算调至 0 → 下一次调用日志为 `budget_exceeded:daily_budget_used_up` | | 缓存 | `result_json IS NULL` 行数应为 0 | `SELECT COUNT(*) FROM biz.ai_cache WHERE result_json IS NULL` = 0 | | 缓存 | `cache_type` 枚举齐全 | `SELECT DISTINCT cache_type` 仅含 `CacheTypeEnum` 的 8 个值 | | 去重 | 同 `(event_type, member_id, site_id, date)` 当天仅 1 次 | `SELECT event_type, member_id, COUNT(*)` 当 `is_forced=false` 时均为 1 | | 多租户 | `SET LOCAL app.current_site_id` | tcpdump / SQL trace 首条 SQL 为 SET LOCAL | | 告警推送 | WebSocket 接收 5 类 `alert_type` | 前端 devtools Network → WS 抓包校验 | | 前端降级 | cache miss 清空旧数据 | 切换 area → DevTools Inspect `aiInsights` 短暂为 [] | | 前端降级 | SSE 断流重试 | chrome network throttle offline → 看 retry 2/4/8s | | Demo 对齐 | 字段 → UI 映射一致 | 并排截图 miniprogram vs demo-miniprogram 像素对齐 | --- ## 11. 自测清单(按角色) ### 前端工程师 **客户详情页** - [ ] App7 `summary` 开头为定性短句且含时间窗 - [ ] App7 strategies 末条 CSS 类 `.strategy-item-last` 生效(无下边距) - [ ] App3/6/8 合并后 clues 配色按 `categoryColor` 映射无 undefined - [ ] clue-card `source` 字段区分 "By:系统" vs "By:{人名}" - [ ] 展开折叠 `detail` 动画无抖动 **任务详情页** - [ ] App4 `one_line_summary` 在 banner 副标题 - [ ] App5 `tactics[].script` 长按复制成功 + toast 反馈 - [ ] `action_suggestions` 勾选持久化到本地 **财务看板** - [ ] 9 区域 × 5 时间 = 45 种组合切换,AI 洞察正确加载或清空 - [ ] `title` 以冒号结尾、`.ai-insight-dim` 灰色样式生效 - [ ] `**xxx**` 渲染为 underline - [ ] seq=11/12 置顶"本期总结" - [ ] 图标为 SVG(`/assets/icons/ai-robot.svg`),非 emoji **聊天页** - [ ] 浮按钮从 6 类页面进入都正确带 `sourcePage` + `pageFilters` - [ ] SSE 中文无乱码 - [ ] `done` 事件替换临时消息 ID - [ ] 历史会话 / 客户 3 天内复用 / 任务复用 / 看板新建 四种入口均工作 - [ ] 重试 2/4/8s 指数退避 - [ ] (**待确认**)若实现 `screen_content`,浮按钮 triggerEvent 采集页面快照 ### 后端工程师 - [ ] `ai_run_logs` 状态分布覆盖 8 种 - [ ] `ai_cache` 无 NULL `result_json` - [ ] `member_retention_clue` `source='manual'` 跨天数量稳定 - [ ] 日结后 2 分钟内 72 组缓存写入完整 - [ ] `SET LOCAL app.current_site_id` 每次查询都执行 - [ ] Pydantic schema 对 score/enum/长度的约束不被绕过 ### 测试工程师 - [ ] demo-miniprogram 与 miniprogram 的客户详情 / 任务详情 / 财务看板像素级对齐 - [ ] customer-detail 页 AI 区块三部分互不干扰(summary / strategies / clues) - [ ] 小程序热启动后 AI 洞察不闪烁旧数据 - [ ] admin-web Dashboard 与数据库聚合一致性 --- ## 附录 A:已废弃 / 已迁移 - `apps/backend/app/ai/apps/app1_chat.py` ~ `app8_consolidation.py` 已删除,逻辑统一内聚至 [dispatcher.py](../../apps/backend/app/ai/dispatcher.py) - APP2 于 2026-04-23 拆分为 `app2_finance` (8 组) + `app2a_finance_area` (64 组) ## 附录 B:已知 Gap / 待办(PRD 对齐清单) > 来源:`docs/prd/` 精读对照当前代码与本 spec 的差异。 | ID | Gap | PRD 来源 | 当前状态 | 建议 | | -- | --- | -------- | -------- | ---- | | G1 | APP1 `screen_content` 采集(浮按钮 onTap triggerEvent 采集页面快照) | AI 需求 2 / NS2 | schema 已预留 [schemas.py:22](../../apps/backend/app/ai/schemas.py#L22) 但 [chat.ts:487](../../apps/miniprogram/miniprogram/pages/chat/chat.ts#L487) 未传 | 浮按钮 `triggerEvent('snapshot')` → 父页返回 setData 可见子集 → URL/SSE body 透传;后端按 "screen_content → page_context" 顺序拼 prompt | | G2 | APP2 72 组预热完整性校验 | P14 调度器 | 代码已拆 8+64 组,缺自动化核对 | 日结后 T+10min 执行 `SELECT DISTINCT target_id FROM biz.ai_cache WHERE cache_type IN ('app2_finance','app2a_finance_area') AND site_id=? AND created_at::date=CURRENT_DATE`,应 = 72;不足告警 | | G3 | APP3 `summary` 行动导向动词约束 | ai-app-prompts | prompt 未强化动词要求 | prompt 增加"必须以可推/建议/需/可约/可邀 等动词结尾";后端 schema 加正则;单测固化 | | G4 | APP4 `target_id` 拼装规则文档化 | P5 | 代码实现 `{assistant_id}_{member_id}` 但文档未固定分隔符 | 正式文档化分隔符为单下划线,拦截 assistant_id/member_id 含下划线的场景 | | G5 | APP5 `scenario` 前端消费规范 | AI 需求 2 | schema 定义但 UI 未约定 | 约定 task-detail 话术区 `scenario` 作为折叠 hint("适用:流失预警"),空字符串时不展示 | | G6 | APP6 `score` 驱动前端 UI | ai-app-prompts | 后端已写 `ai_cache.score` 列但前端未消费 | 备注列表按 score 降序排列;score ≥ 8 高亮、≤ 3 置灰 | | G7 | APP7 `strategies[].title` 字段用途 | ai-app-prompts | 前端按 index 循环配色,未用 title | 方案 A:删除 title;方案 B:AI 输出 title 作为徽章("机会"/"风险"/"日常"),前端展示 | | G8 | APP8 相似线索去重判断标准 | AI 需求 2 / P5 | prompt 未定义相似度阈值 | prompt 增加"若两条线索 summary 字符重合 ≥ 60% 或 category 相同且描述同一行为,合并为一条,providers 拼接去重" | | G9 | 熔断/限流/预算硬约束压测 | P14 | 代码实现但无压测报告 | 压测:APP1 单用户 11 次/分钟应触发 rate_limited;连续 5 次失败后第 6 次 status=circuit_open 且不真实调用;日 token 达 100k 后 status=budget_exceeded | | G10 | 日结触发 APP2 预热延迟 | P14 | 代码"日结后轮询"无明确延迟 | 文档固定 "日结完成事件 → T+5min 开始调度 APP2 × 72 组,T+15min 应全部完成";超时告警 | | G11 | RLS 多租户隔离强制验证 | P5 / backend CLAUDE.md | 代码每次查询前 SET LOCAL,缺自动验证 | 集成测试:伪造 site_id=A 的 JWT 访问 member_id 属于 site_id=B 的数据应返回 404;抓 pg_stat_statements 验证 SET LOCAL 存在 | | G12 | 新客(无消费/无备注)降级文案 | NS2 | 代码有 `_default_member_data` 降级但 prompt 文案未标准化 | 定义模板:「该客户暂无历史线索,以下基于本次消费进行初步判断」,dispatcher 注入 prompt 开头 | | G13 | APP8 `providers` 多源拼接格式 | ai-app-prompts | 代码拼接但分隔符未固定 | 固定格式:`"消费数据,备注-{人名}"`,中文逗号分隔,备注人不重复(用 set 去重) | | G14 | APP4 任务链 cache miss 时的 prompt 提示 | ai-app-prompts | 代码读缓存失败时传空 | prompt 标注"暂无 APP8 历史线索,请基于基线数据分析",而非丢空字符串 | | G15 | APP2 `**xxx**` 强调标记约定 | Demo 规范 | 前端已做轻量 Markdown,但 prompt 未要求 AI 输出该标记 | prompt 增加"对关键结论或异常词用 `**粗体**` 包裹,前端会渲染为下划线强调" | ## 附录 D:PRD 硬性阈值对照表(验收单) > 单独抽出所有数字阈值,供 QA 断言时直接引用。 | APP | 字段/指标 | 阈值 | 不达标处理 | | --- | --------- | ---- | ---------- | | APP1 | 单轮 prompt | ≤ 4000 字 | 触发截断保留最近 N 轮 | | APP1 | page_context | ≤ 2000 字 | page_context.py 末尾附"…(上下文已截断)" | | APP1 | 限流 | 10 次/分钟/用户 | `rate_limited` 记录,前端提示"操作过快" | | APP1 | 复用窗口 | customer/coach/finance 3 天;task 无限;general 新建 | 超窗口新建会话 | | APP2 | 组合数 | 72(8×9) | 缺组合告警 | | APP2 | 加载时延 | < 2 秒 | 前端 loading > 2s 上报监控 | | APP2 | seq 范围 | 1-12,11/12 置顶 | 非 11/12 按 seq 升序 | | APP3 | clues 条数 | 1-5 | AI 超 5 截断 | | APP3 | summary 长度 | ≤ 20 字 | 后端校验拦截 | | APP3 | detail 长度 | ≤ 120 字 | 后端校验拦截 | | APP4 | action_suggestions | 1-4 条,每条 ≤ 100 字 | 单测固化 | | APP4 | one_line_summary | ≤ 30 字,必含关系评级词 | 正则检查 `(紧密\|一般\|疏远)` | | APP5 | tactics 条数 | 2-4 | AI 超 4 截断 | | APP5 | script 长度 | ≤ 150 字 | 后端校验 | | APP5 | 黑词 | 禁用 8折/免费/KOL/抽成/内部价 | 后端正则黑名单拦截 | | APP6 | score 范围 | 1-10(6=标准,<6 扣分,>6 加分) | Pydantic `ge=1, le=10` | | APP6 | clues 条数 | 0-10 | 无下限,上限截断 | | APP7 | summary 长度 | 200-400 字 | 后端校验 | | APP7 | summary 开头定性 | ≤ 25 字 + 句号 | 正则 `^.{1,25}[。!]` | | APP7 | strategies 条数 | 2-5 | 前端仅展示 2 条 | | APP7 | 时间窗标注 | 必含 "近 30 天/近 60 天/近 90 天" | 关键词检查 | | APP8 | clues 条数 | ≤ 5 | AI 超 5 截断 | | APP8 | summary 禁数字 | 正则 `[0-9%¥]` 不匹配 | 后处理剥离 | | APP8 | providers 拼接 | 中文逗号去重 | 后端 set 处理 | | 通用 | Token 日预算 | 100,000 | 超限 status=budget_exceeded | | 通用 | Token 月预算 | 2,000,000 | 超限告警 + 阻断 | | 通用 | 熔断阈值 | 连续 5 次失败 OPEN | 60 秒恢复窗口 | | 通用 | APP2-8 限流 | 100 次/小时/门店 | rate_limited 跳过 | | 通用 | 关系流失预警 | 停消费 > 14 天 | APP4/7 标注 | | 通用 | 高粘性阈值 | 月内 ≥ 3 次 | APP4 判定 | | 通用 | 学习型阈值 | 助教费占比 > 40% | APP3/4/7 判定 | | 通用 | 当期 < 7 天 | 必须降权用词 | APP2 prompt 约束 | ## 附录 C:Demo 关键路径索引 | 页面 | AI 字段 | 文件 | | ---- | ------- | ---- | | 客户详情 | `aiInsight.summary/strategies` + `clues[]` | [customer-detail.ts:90-145](../../apps/demo-miniprogram/miniprogram/pages/customer-detail/customer-detail.ts#L90) | | 任务详情 | `retentionClues[]` + `talkingPoints[]` | [task-detail.ts:67-86](../../apps/demo-miniprogram/miniprogram/pages/task-detail/task-detail.ts#L67) | | 财务看板 | `ai-insight-section` 三段式 | [board-finance.wxml:208-221](../../apps/demo-miniprogram/miniprogram/pages/board-finance/board-finance.wxml#L208) | | 助教/客户看板 | `aiColorClass`(随机配色) | [board-finance.ts:294](../../apps/demo-miniprogram/miniprogram/pages/board-finance/board-finance.ts#L294) | --- ## 附录 E:生产端 vs Demo 端对照实况(2026-04-22 快照) ### E.1 生产端真实消费路径 | 页面 | AI 字段来源 | 关键实现 | 与 demo 差异 | | ---- | ----------- | -------- | ------------ | | customer-detail | `fetchAICache('app7_customer_analysis', memberId)` 单独拉取;`retentionClues` 从主详情接口返回 | [customer-detail.ts:7](../../apps/miniprogram/miniprogram/pages/customer-detail/customer-detail.ts#L7) onLoad → loadDetail → `_loadAIInsight()` 异步加载;`aiInsight.strategies` 前端按 index 循环 6 色 | demo 是硬编码 mock;生产 strategies `color` 由前端计算 | | task-detail | `aiAnalysis.summary / suggestions / talkingPoints` + `retentionClues` **都随主详情接口一次返回**,无独立 AI 缓存调用 | `onCopySpeech()` → `wx.setClipboardData()` + 2s toast [task-detail.ts:268](../../apps/miniprogram/miniprogram/pages/task-detail/task-detail.ts#L268);备注创建后 4s 间隔轮询 5 次拿 `aiScore`(20s 超时) | demo 同样内联 mock;生产端**没有长按复制**,仅单击复制按钮 | | board-finance | `fetchAICache(cacheType, targetId)` + `parseMarkdownInline` 解析 `**粗** / *斜* / ***粗斜***` | [board-finance.ts:532](../../apps/miniprogram/miniprogram/pages/board-finance/board-finance.ts#L532) cache_type 切分已实装;seq 11/12 置顶(精确匹配 + 末两条 fallback);切换 time/area 时 `aiInsights=[]` 主动清空 | demo 内联 mock,无 TIME_MAP/AREA 枚举 | | 浮按钮 | task-detail 当前**未挂载**;customer-detail 仅传 `customerId`;board-finance 仅传 `bottom=200` | `sourcePage` / `pageFilters` **虽支持但尚未启用**(属性存在、传参为空) | demo 无浮按钮 | ### E.2 关键实务差异 - **task-detail 无 AI 缓存调用**:`aiAnalysis` 是 `fetchTaskDetail()` 主响应的一部分,后端在返回任务详情时已聚合读取 App4/App5 缓存。这意味着**任务列表分页**与 **AI 结果拉取**合并为一次请求,减少 round-trip。 - **fetchAICache silent fail**:[services/api.ts:417](../../apps/miniprogram/miniprogram/services/api.ts#L417) 错误时返回 `null`,页面降级显示"暂无洞察数据";此静默失败需监控上报补强。 - **百分比双重格式化**:board-finance 把后端小数率(0.052)× 100 传 WXS 格式化,demo 端硬编码字符串,两端一致性依赖后端契约稳定。 - **setData 路径优化**:`'aiInsight.summary'` 字符串路径而非整对象解构,减少小程序差量渲染。 --- ## 附录 F:AI 数据库 schema 权威清单 ### F.1 五张核心表 DDL 速览 权威 DDL:[db/zqyy_app/schemas/biz.sql](../../db/zqyy_app/schemas/biz.sql) 与 [public.sql](../../db/zqyy_app/schemas/public.sql)。 | 表 | 关键字段 | 索引要点 | RLS | | -- | -------- | -------- | --- | | `biz.ai_run_logs` | `status` varchar(20)(pending/running/success/failed/timeout/circuit_open/rate_limited/budget_exceeded),`alert_status`,`session_id` varchar(100),`tokens_used` int default 0,`latency_ms` int,`request_prompt` text(≤ 8000 字符写入) | 部分索引 `idx_ai_run_logs_alert` WHERE status IN (failed, timeout, circuit_open);BRIN `(created_at)` | **未启用** | | `biz.ai_cache` | `cache_type` varchar(30) + CHECK 约束 8 枚举,`target_id` varchar(100),`result_json` jsonb NOT NULL,`score` int(**无 CHECK 约束**) | `idx_ai_cache_lookup (cache_type, site_id, target_id, created_at DESC)` | **未启用** | | `biz.ai_trigger_jobs` | `event_type` varchar(30),`is_forced` bool,`app_chain` varchar(100),`payload` jsonb | 部分索引 `idx_ai_trigger_jobs_dedup` WHERE status ≠ 'skipped_duplicate' | **未启用** | | `biz.ai_conversations` / `biz.ai_messages` | `context_type` varchar(20),`context_id` varchar(50),`session_id` varchar(100),`reference_card` jsonb | 部分索引 `idx_ai_conv_context` WHERE context_type IS NOT NULL | **未启用** | | `public.member_retention_clue` | `category` varchar(20) + CHECK 枚举 6 值,`summary` varchar(200),`detail` text,`recorded_by_name` varchar(50),`source` varchar(20) DEFAULT 'manual',`is_hidden` bool | `idx_retention_clue_member`、`idx_retention_clue_category` | **未启用**,通过 FDW 反向映射 `fdw_app.member_retention_clue` | ### F.2 DDL vs Pydantic 对齐问题(按风险) | 级别 | 项目 | 症状 | | ---- | ---- | ---- | | **CRITICAL** | `ai_cache.score` 无 CHECK 约束 | Pydantic `App6Result.score: int = Field(ge=1, le=10)` 仅应用层校验;脏数据(0/-1/100)可静默落库 | | **HIGH** | `member_retention_clue` **没有 `emoji` 和 `providers` 列** | [dispatcher.py:513-588](../../apps/backend/app/ai/dispatcher.py#L513) `_write_retention_clue` 折叠:`emoji + " " + summary` → `summary`;`providers` → `recorded_by_name` varchar(50)。**providers 多源拼接大概率超过 50 字符被 PG 报错或截断** | | **HIGH** | `summary varchar(200)` 拼接 emoji 后有效长度降低 | AI 输出接近 200 字符时合并报错 | | **MEDIUM** | `cache_type` CHECK 与 `CacheTypeEnum` 双侧维护 | 新增 App 需同时改 DDL + Pydantic,遗漏即违反 CHECK | | **MEDIUM** | `ai_run_logs` 无 `job_id` 列关联 `ai_trigger_jobs` | 只能通过 `session_id` 间接关联,无 FK 保护 | | **LOW** | FDW 外部表 `fdw_app.member_retention_clue` 缺 `source` 和 `is_hidden` 列 | ETL 端无法按 source 过滤 AI 写入的线索 | ### F.3 建议补充的索引/约束 1. `biz.ai_cache.score` 添加 `CHECK (score IS NULL OR (score >= 1 AND score <= 10))` 2. `public.member_retention_clue` 新建 `(member_id, site_id, source)` 索引(APP8 DELETE 现在全扫) 3. `biz.ai_conversations` 新建 `session_id` 单列索引 4. **全部 AI 表启用 RLS**(当前完全依赖应用层 `site_id` 过滤,无最后防线) --- ## 附录 G:合规审计红黄清单(2026-04-22) > 来源:security-reviewer 全量扫描 apps/backend/app/ai/ 所有 prompt 拼装路径。 ### G.1 已通过的面 - ✅ `member_phone / phone / mobile / id_card / wechat_openid / wechat_unionid` **零出现**于所有 prompt 与 data_fetcher - ✅ `bank_account / bank_card / home_address / ip_address / device_id` 全部未出现 - ✅ DashScope SDK 走 HTTPS 加密传输 - ✅ page_context 前端透传被 handler 白名单拦截(handler 只 `.get()` 已知字段) ### G.2 5 个风险点(按优先级) | ID | 文件 | 风险 | 建议修复 | | -- | ---- | ---- | -------- | | R1 | [member_data.py:200](../../apps/backend/app/ai/data_fetchers/member_data.py#L200) | 助教 `da.real_name` 作为 nickname fallback 传 AI | 确认是否属员工隐私;若是改为空串或工号 | | R2 | [member_data.py:249-262](../../apps/backend/app/ai/data_fetchers/member_data.py#L249) | 储值卡**精确余额**直接进 prompt | 范围化:改为"高/中/低/清零"标签 | | R3 | [member_data.py:276](../../apps/backend/app/ai/data_fetchers/member_data.py#L276) | `card_balance_total` / `stored_value_balance_total` 精确值 | 同 R2 | | R4 | [member_data.py:358-398](../../apps/backend/app/ai/data_fetchers/member_data.py#L358) | `notes.content` 备注原文无正则脱敏 | 预过滤手机号 `1[3-9]\d{9}` → `1****`、银行卡号、身份证号 | | R5 | [run_log_service.py:57](../../apps/backend/app/ai/run_log_service.py#L57) | 含 R2/R3/R4 的 prompt 写入 `ai_run_logs.request_prompt` 落库 | 先做字段级脱敏再入库;或 ai_run_logs 加严格 RLS 仅开发读 | ### G.3 合规测试清单(CI) - [ ] 单测:`build_prompt_*` 返回值断言无 11 位连续数字(手机号) - [ ] 单测:`notes.content` 含手机号的样例,prompt 中已掩码 - [ ] CI:`grep -r "member_phone\|wechat_openid\|id_card" apps/backend/app/ai/` 无命中 - [ ] 集成测试:构造含手机号的备注,验证 APP6 prompt 中号码不完整出现 --- ## 附录 H:MCP Server 与 dispatcher 关系 ### H.1 现状 | 面向 | dispatcher 路径 | MCP Server 路径 | | ---- | --------------- | --------------- | | 调用者 | Admin Web / 后端事件 | **目前仅 Claude Code 开发环境**;百炼 AI 应用规划中(未接入) | | 连接库 | etl_feiqiu(只读)+ zqyy_app(读写) | 仅 etl_feiqiu 只读 | | RLS | 每次查询前 `SET LOCAL app.current_site_id` | **无 site_id 隔离**(P5.1 规划中) | | 鉴权 | JWT | Bearer Token(可选,`MCP_TOKEN` 为空则无鉴权) | | 工具 | 8 个 APP handler | 4 个通用工具:`list_tables` / `describe_table` / `describe_schemas` / `query_sql`(只读+正则黑名单+500 行上限) | ### H.2 关键边界 - **8 个 AI 应用目前均走 dispatcher 直查 DB,不经过 MCP** - MCP 扩展(P5.1 批次 B)规划:加 zqyy_app 连接 + auth/biz/public 自动 schema 路由 + 敏感字段脱敏(wx_openid/phone) - MCP 扩展(P5.1 批次 C)规划:完整查库手册上传百炼知识库 ### H.3 生产部署 - 外部入口:`https://mcp.langlangzhuoqiu.cn/mcp`(未来) - 本地配置:`.mcp.json` 的 `pg-etl` / `pg-app` 禁用,`pg-etl-test` / `pg-app-test` 启用 --- ## 附录 I:admin-web AI 三页面实况 ### I.1 AIDashboard.tsx(顶部卡片 + 趋势 + 告警) - 数据源 [AIDashboard.tsx:99](../../apps/admin-web/src/pages/AIDashboard.tsx#L99):`getDashboard()` → `DashboardResponse` - 字段:`today_calls / today_success_rate / today_tokens / today_avg_latency_ms / trend_7d[] / app_distribution[] / budget{} / recent_alerts[] / app_health[]` - 时间范围:`range_days` (1/3/7/10) 或 `date_from/date_to` 自定义 [AIDashboard.tsx:112](../../apps/admin-web/src/pages/AIDashboard.tsx#L112) - WebSocket:`ws(s)://[host]/ws/ai-alerts/{site_id}`,单次连接 **无重连** [AIDashboard.tsx:135-168](../../apps/admin-web/src/pages/AIDashboard.tsx#L135) - 告警消息格式:`{ type: "alert_created", payload: AlertItem }` - 实时告警最多保留 20 条,合并至表格顶部 ### I.2 AIOperations.tsx(四卡片手动执行) 四个 Card 区: 1. **Card 1 手动重跑**:`retryTriggerJob(id)` → `trigger_job_id` 2. **Card 2 缓存失效**:`invalidateCache({ site_id, app_type?, member_id? })` → `affected_count`;site_id 必填 3. **Card 2.5 按需执行**:`runApp(appType, params)` 支持 APP3-APP8 4. **Card 2.6 触发事件链**:`triggerEvent({ event_type, ..., is_forced=true })` — event_type ∈ `{consumption, note_created, task_assigned, dws_completed}` 5. **Card 3 批量执行**:`createBatchRun()` → 估算 → `confirmBatchRun(batch_id)` 异步启动;batch_id **内存 TTL 600 秒** [admin_service.py:26](../../apps/backend/app/services/ai/admin_service.py#L26) 6. **Card 4 告警管理**:`ackAlert(id)` / `ignoreAlert(id)`,分页 10 ### I.3 TriggerManager.tsx(4 Tab + URL 驱动) - Tab:`all / biz / ai / etl`,`?tab=xxx` URL 查询参数持久化 - Biz Tab 编辑 Modal:`cron_expression` + `interval_seconds`(min=1),保存后热加载 - AI Tab 组件堆栈:`AITriggers` + `AIOperations` + `AITriggerJobs` ### I.4 **严重不一致:app6 命名错位** - 前端 `adminAI.ts` 定义 `app6_note_analysis` - 后端 `admin_ai.py` 的 `_SUPPORTED_APP_TYPES` 为 `app6_note` - 手动执行 APP6 **会 400**,需修正命名统一(推荐采用后端名 `app6_note`) ### I.5 权限模型 - 所有 Admin AI 端点 `_require_admin()` 依赖 [admin_ai.py:70-111](../../apps/backend/app/routers/admin_ai.py#L70) - 角色:`{site_admin, tenant_admin, super_admin}` 任一 - `admin_users.is_active` 实时查询(禁用账户秒级生效) --- ## 附录 J:后端完整端点与链路矩阵 ### J.1 路由端点清单(共 21 个 AI 相关) **admin_ai.py(13 个,需 admin 权限)** - `GET /api/admin/ai/dashboard` / `trigger-jobs` / `trigger-jobs/{id}` / `run-logs` / `run-logs/{id}` / `budget` / `alerts` - `POST /api/admin/ai/trigger-jobs/{id}/retry` / `cache/invalidate` / `batch-run` / `batch-run/confirm` / `alerts/{id}/ack` / `alerts/{id}/ignore` **xcx_chat.py(5 个,需 approved 用户)** - `GET /api/xcx/chat/history` / `{chat_id}/messages` / `messages?contextType=&contextId=` - `POST /api/xcx/chat/stream`(SSE)/ `{chat_id}/messages` **内部事件总线** - `POST /api/internal/ai/trigger` — [internal_ai.py](../../apps/backend/app/routers/internal_ai.py),Internal-Token 认证 - `POST /api/internal/etl-completed` — [internal_events.py:56](../../apps/backend/app/routers/internal_events.py#L56),触发 recall → task → `fire_event("ai_dws_completed")` - `POST /api/admin/task-engine/pending-review/{id}/reassign` — [admin_task_engine.py:255](../../apps/backend/app/routers/admin_task_engine.py#L255),触发 `fire_event("ai_task_assigned")` ### J.2 事件 → 链路映射 [dispatcher.py:169-174](../../apps/backend/app/ai/dispatcher.py#L169) `_EVENT_CHAIN_MAP`: | 事件 | 链路 | | ---- | ---- | | `consumption` | APP3 → APP8 → APP7(+ APP4 → APP5 若 `has_assistant`) | | `note_created` | APP6 → APP8 | | `task_assigned` | APP4 → APP5 | | `dws_completed` | APP2 × 8(area='all') + APP2A × 64(area!='all') | ### J.3 服务层职责矩阵 | 服务 | 职责 | dispatcher 调用 | 写表 | | ---- | ---- | ---------------- | ---- | | ChatService | 对话持久化 | **直调 DashScopeClient**(不走 dispatcher) | ai_conversations / ai_messages | | AdminAIService | 监控聚合 | 无,仅查询 | 无(只读) | | NoteService | 备注创建 | `dispatcher._handle_note()` | notes / ai_cache | | TaskGenerator | 每日 07:00 生成任务 | `trigger_scheduler` 异步 | coach_tasks / ai_cache | | TriggerScheduler | cron/event 调度 | `fire_event()` | trigger_jobs 更新 | | AIDispatcher | 链路编排+熔断/限流/预算 | `_run_step()` × N | ai_cache / ai_run_logs | ### J.4 超时与并发 - 单步 `_STEP_TIMEOUT = 180s`(2026-04-21 从 120s 上调)[dispatcher.py:57](../../apps/backend/app/ai/dispatcher.py#L57) - APP2 DWS 全链路 = 180s × 72 组 + 600s 余量 ≈ **2.5 小时**(同步执行可能阻塞其他触发器,需监控) - 普通链 超时 10min ### J.5 关键技术债 - **事件去重内存 set**:服务重启后当日相同事件可重复触发 → 需迁移到 DB 查询 `ai_trigger_jobs` - **ChatService 不走 dispatcher**:链路独立,不享受熔断/限流/预算保护(APP1 限流单独实现) - **旧 openai_client 残留**:DashScope 迁移后未见清理 --- ## 附录 K:Demo 10 页 AI 区块映射(标杆参考) ### K.1 浮按钮挂载清单 **已挂载**(8 页):board-coach / board-customer / chat-history / notes / performance / task-list / customer-service-records / my-profile / board-finance / customer-detail **未挂载**:chat / task-detail / coach-detail / apply / login / reviewing / dev-tools / no-permission ### K.2 AI 配色规范 - 6 色方案:`red / orange / yellow / blue / indigo / purple` - 实装:[ai-color-manager.ts:74-127](../../apps/demo-miniprogram/miniprogram/utils/ai-color-manager.ts#L74) - task-list 采用 `random` 模式每日刷新 - 其他页面按页面标识映射固定色 ### K.3 辅助组件 - `ai-title-badge`:标题旁 AI 徽章 - `ai-inline-icon`:列表内嵌 AI 图标 - `clue-card`:线索卡片(含 `tag/emoji/title/source/content` 字段 + 展开折叠) ### K.4 `pageFilters` 前端字段缺失 扫描发现前端页面**未定义 pageFilters 对象结构**,当前仅浮按钮属性声明;实际采集需在 P0 G1 gap 补充。 --- ## 附录 L:ETL → AI 数据源约束 ### L.1 金额口径铁律 - **禁用 `consume_money`**([member_data.py:1-6](../../apps/backend/app/ai/data_fetchers/member_data.py#L1) 文件头明文禁止) - 统一拆分为:`table_charge_money + assistant_pd_money + assistant_cx_money + goods_money` - 所有 prompt 拼装遵守此口径(APP2/3/4/6/7 prompt 已核对) ### L.2 FDW 视图与 RLS - 所有 `fdw_etl.*` 已迁移至 `app.v_*` RLS 视图 - FDW 查询超时硬上限 5 秒 [member_data.py:69-79](../../apps/backend/app/ai/data_fetchers/member_data.py#L69) - 超时降级到 `_default_member_data()`(AI 输入降级,分析质量下降) ### L.3 DWS 视图清单 - 20+ 汇总表,6 大主题(助教业绩/薪酬/财务日报/会员分析/订单/库存) - 金额字段 `NUMERIC(12,2)`,货币 CNY - **空值策略未文档化**:NULL vs 0 策略缺,prompt 数字解读存歧义 ### L.4 消费记录完整明细风险 - 2026-05-01 起 App3 不再按 4000 字硬截断消费记录,优先保留完整明细,避免丢失大客户高频消费模式 - 已用合成 100 条完整消费明细真实调用 App3,prompt 约 25791 字,64.30s 返回成功,低于 180s 单步超时 - 剩余风险:真实门店极端会员、缓存 reference 膨胀或百炼侧临时抖动仍可能拉高耗时,需通过 `ai_run_logs.elapsed_ms` 持续观察 --- ## 附录 M:部署与超参 ### M.1 DashScope APP_ID 清单(根 [.env:118-134](../../.env#L118)) ``` DASHSCOPE_APP_ID_1_CHAT DASHSCOPE_APP_ID_2_FINANCE (area='all' × 8 时间维度) DASHSCOPE_APP_ID_2A_FINANCE_AREA (area!='all' × 64 组合,2026-04-23 新增) DASHSCOPE_APP_ID_3_CLUE DASHSCOPE_APP_ID_4_ANALYSIS DASHSCOPE_APP_ID_5_TACTICS DASHSCOPE_APP_ID_6_NOTE DASHSCOPE_APP_ID_7_CUSTOMER DASHSCOPE_APP_ID_8_CONSOLIDATE ``` ### M.2 超参硬编码 - 模型版本、温度、max_tokens 在 [config.py:14-48](../../apps/backend/app/ai/config.py#L14) 内硬编码 - 无 dev/test/prod 环境差异文件(仅一份 .env) - `INTERNAL_API_TOKEN` 与 DashScope API Key 同存根 .env(已 gitignore) - `BACKEND_API_URL=http://localhost:8000` 部署到各环境需手工改 ### M.3 建议 - 提取 `.env.example` + `.env.dev` + `.env.prod` 多环境模板 - 超参挪到 `config.py` 的 `@dataclass` 并可被 `.env` 覆盖 - 加 APP2 DWS 并发窗口监控(2.5h 耗时风险) --- ## 附录 N:历史审计时间轴(2026-02 ~ 2026-04) ### N.1 关键演进节点 | 日期 | 主题 | 涉及 APP | | ---- | ---- | -------- | | 2026-04-23 | [app2a_finance_area_integrated](../../docs/audit/changes/2026-04-23__app2a_finance_area_integrated.md) — 72 组合拆为 app2_finance (8) + app2a_finance_area (64) + `member_order_count` 列 + area_code NULL bug 修复 | APP2 | | 2026-04-22 | [app2_prompt_v5_1_and_miniprogram_ai_insight](../../docs/audit/changes/2026-04-22__app2_prompt_v5_1_and_miniprogram_ai_insight.md) — V5.1 采纳(A/B 40 次调用,92.3 分)+ 按星期聚合门槛 14 天 + 月中场景保护 + seq11/12 置顶 | APP2 | | 2026-04-21 | [admin-web-ai-management-suite](../../docs/audit/changes/2026-04-21__admin-web-ai-management-suite.md) — AIPrewarm / AITriggers / AIOperations 增强 + 4 个后端端点 | 管理 | | 2026-04-20 | [ai-module-complete](../../docs/audit/changes/2026-04-20__ai-module-complete.md) — Phase 0-4 贯通 + 删除 8 个死代码 app 文件 + 修复 main.py 未调用 set_dispatcher 导致 503 | 全局 | | 2026-03-20 | [rns1-ai-autonomous-decision-risk-audit](../../docs/audit/changes/2026-03-20__rns1-ai-autonomous-decision-risk-audit.md) — 76 session / 34 风险 / "AI 信任文档胜过信任 DB" 核心问题 | 全局 | | 2026-03-10 | [multi-module-ai-apps-task-defense](../../docs/audit/changes/2026-03-10__multi-module-ai-apps-task-defense-miniprogram.md) — 8 个 AI 应用骨架 + 任务防卡死 + 小程序页面迁移 | 全局 | | 2026-02-26 | [retention-clue-refactor](../../docs/audit/changes/2026-02-26__retention-clue-refactor.md) — 新表 `ai_trigger_jobs` 去重 + `member_retention_clue` 字段调整 | APP8 | ### N.2 从历史踩坑抽取的验证规则 | 历史踩坑 | 应有验证 | | -------- | -------- | | **FDW Schema 脱节**(列名/视图名虚构) | 生成/改 prompt 前必须 MCP 执行 `information_schema.columns` + `SELECT * LIMIT 5` 验证 | | **月中场景误读**(3 月初把月中 22 天当整月对比) | payload 含"对比口径"字段(当期/对比期天数);按星期门槛 14 天;样本<5 天降权表述 | | **Prompt 版本漂移** | 版本号固定写入 `docs/ai/`;frontend enum / backend schema 同步校验 | | **app_type 枚举错配** | 新增 APP 时同时更新 `CacheTypeEnum` / DDL CHECK / `_SUPPORTED_APP_TYPES` / adminAI.ts | | **DWS area_code=NULL bug** | ETL 完成后必须验证 `all_sum ≈ 各区域 sum` | | **DDL 列 DEFAULT 0 未回填** | 上线前预约回填脚本窗口 | | **AI 自主执行架构变更** | 连接模式/DDL/FDW 映射变更需向用户展示 SQL 等待确认 | | **Task 状态欺骗**(20/22 失败仍 completed) | 带验证步骤的 task 测试通过率 < 90% 禁止 complete | ### N.3 5 项遗留技术债(需补审计) - APP8 幂等 DELETE+INSERT 改造缺独立审计 - `tokens_used=0` 提取 bug(`usage.models` 嵌套)未根治 - `request_prompt` 8000 字符门槛未正式审计 - `ai_cache.score` TTL 策略未定 - `ai_trigger_jobs` 去重从内存 set 迁移到 DB 的当前状态未审计 --- ## 附录 O:最终 Gap 汇总(v4 版,共 25 项) > 在 v3 的 15 项基础上 + 新增 10 项(来自全景调研)。 ### O.1 v3 遗留 15 项(摘要) 见附录 B(G1-G15)。 ### O.2 v4 新增 10 项 | ID | Gap | 来源 | 建议 | | -- | --- | ---- | ---- | | G16 | 前后端 `app6_note_analysis` vs `app6_note` 命名错位 | admin-web 调研 | 统一为后端名 `app6_note`,同时改 adminAI.ts 枚举 | | G17 | WebSocket `/ws/ai-alerts/{site_id}` 无重连 | AIDashboard.tsx | 指数退避重连(2/4/8s,最多 10 次) | | G18 | `member_retention_clue` 无 `emoji/providers` 列,折叠到 `summary/recorded_by_name` | DB schema | 要么加列,要么明确 `providers` 长度上限并拦截超 50 字符 | | G19 | `ai_cache.score` 无 CHECK 约束 | DB schema | 加 `CHECK (score IS NULL OR score BETWEEN 1 AND 10)` | | G20 | 全部 AI 表 **未启用 RLS** | DB schema | 至少 `biz.ai_cache` / `biz.ai_trigger_jobs` / `public.member_retention_clue` 启用 RLS + `USING (site_id = current_setting('app.current_site_id')::bigint)` | | G21 | 储值余额精确值 / 助教 real_name / 备注原文进 prompt 落 `ai_run_logs` | 合规审计 | R2-R5 脱敏;ai_run_logs 加严格 RLS | | G22 | 事件去重内存 set 重启丢失 | 后端路由调研 | 去重改查 `ai_trigger_jobs` 当日记录 | | G23 | APP2 DWS 预热 2.5 小时同步执行 | 后端路由调研 | 加并发控制窗口;监控 > 2h 告警 | | G24 | task-detail 无 AI 浮按钮挂载 | 生产端调研 | 若需支持"从任务详情进入对话",需挂载浮按钮并传 `sourcePage=task-detail` + `contextId=task_id` | | G25 | MCP Server 无 `site_id` RLS 隔离;未接入百炼 AI | MCP 调研 | P5.1 批次 B 落地前不开放 MCP 给百炼;开放时强制 `MCP_TOKEN` + 敏感字段脱敏 | --- ## 附录 P:全景完成度自评 | 面向 | 盘查状态 | | ---- | -------- | | 代码静态(后端 / 前端生产 / 前端 demo / admin-web) | ✅ 完成 | | PRD 文档(AI 需求 2 / ai-app-prompts / P5 / P14 / P15 / NS2 / NS3) | ✅ 完成 | | DB schema DDL + Pydantic 对齐 + RLS 状态 | ✅ 完成 | | 合规敏感字段(PII / 金融 / 位置 / 备注原文) | ✅ 完成 | | MCP Server(现状 + 规划 + 与 dispatcher 边界) | ✅ 完成 | | 后端路由 21 个端点清单 + 服务层职责 + 超时并发 | ✅ 完成 | | 历史审计近 3 月时间轴 + 踩坑规则 + 技术债 | ✅ 完成 | | demo 10 页 AI 区块映射 + 配色规范 + 浮按钮分布 | ✅ 完成 | | 共享包枚举对齐(未同步)/ ETL DWS 字段 / 口径铁律 | ✅ 完成 | | 部署超参 + 环境差异 + 压测降级灰度 | ✅ 完成(含缺口标注) | **结论**:本文档 v4 已作为 NeoZQYY 项目 AI 模块的**单一真相来源(SSOT)**,覆盖代码 / PRD / DB / 合规 / MCP / 路由 / 审计 / demo / ETL / 部署 10 个面向,25 项 Gap 跟踪,75+ 验证点;后续变更需同步更新本文档。