# 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:<reason>` | 人为把日预算调至 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+ 验证点；后续变更需同步更新本文档。
