# FinOps Agent Platform — Phase 3 任务拆解

Phase 3 目标：**异常检测 + 飞书集成 + 用户画像**。完成后系统可自动检测费用异常并生成告警，通过飞书主动推送告警和报告，用户可在飞书内直接提问查询成本数据，平台为每个用户维护持续演进的个性化画像。

**前置文档**: [FinOps Agent 平台设计](./finops-agent-platform-design.md)

**Phase 依赖**: Phase 3 依赖 Phase 1（PostgreSQL + Prisma + User 模型 + 会话持久化）和 Phase 2（Notification 模型 + 报告引擎 + Cron 调度）已完成。

**验收标准**: 异常检测发现费用突增 → 飞书告警推送 → 用户在飞书内直接提问并得到回复

---

## 依赖关系

```
Step 1: 异常规则 CRUD (AnomalyRule)
  │
  └──→ Step 2: 异常检测引擎 (依赖 Rule 模型 + BigQuery 查询)
         │
         └──→ Step 3: 告警管理 + 通知推送 (依赖 Alert 模型 + Notification Hub)
                │
                ├──→ Step 6: 飞书主动推送 (依赖告警 + 通知基础设施)
                │
                └─── (并行) ───┐
                               │
Step 4: 飞书 Bot 接入 + WebSocket Client (无数据库依赖，可与 Step 1/2 并行)
  │                            │
  └──→ Step 5: 飞书被动查询 (依赖 WS Client + Query Engine)
         │                    │
         └────────────────────┤
                              │
Step 7: 统一身份关联 (依赖 Step 4 飞书 SDK + Step 5 事件处理)
  │
  └──→ Step 8: 用户画像系统 (依赖 User + UserProfile 模型 + 身份关联)
```

**可并行**: Step 4（飞书 Bot 接入）不依赖异常检测模块，可与 Step 1/2/3 并行开发。Step 1 和 Step 4 可同时启动。

---

## Step 1: 异常规则管理 (AnomalyRule CRUD)

**目标**: 实现异常检测规则的完整 CRUD，包含预置规则模板，为后续检测引擎提供规则数据基础。

### 任务清单

- [ ] **1.1** 新增 Prisma Migration — UserProfile + AnomalyRule + AnomalyAlert 模型
  - **Scope**: Phase 3 新增 3 个模型（Subscription/Report/Notification 已在 Phase 2 创建）
  - 在 `prisma/schema.prisma` 中添加:
    - `UserProfile` — 用户画像（1:1 关联 User）
    - `AnomalyRule` — 异常检测规则
    - `AnomalyAlert` — 异常告警记录
  - `npx prisma migrate dev --name add_anomaly_and_profile`
  - 确认索引: `anomaly_rules(enabled)`, `anomaly_alerts(rule_id, status)`, `anomaly_alerts(severity, status)`, `anomaly_alerts(detected_at DESC)`, `user_profiles(user_id)`
- [ ] **1.2** 定义规则类型枚举与配置 Schema
  - `data-agent/src/lib/anomaly/types.ts`
  - 定义 `RuleType = 'mom_increase' | 'wow_increase' | 'absolute_threshold' | 'new_service' | 'new_region' | 'tag_missing'`
  - 用 zod 定义每种 ruleType 对应的 config schema:
    - `mom_increase`: `{ threshold: number, min_amount: number }` — 默认 0.3 / $100
    - `wow_increase`: `{ threshold: number, min_amount: number }` — 默认 0.5 / $50
    - `absolute_threshold`: `{ daily_limit: number, service?: string }` — 自定义
    - `new_service`: `{ account_ids: string[] }` — 监控范围
    - `new_region`: `{ account_ids: string[] }` — 监控范围
    - `tag_missing`: `{ tag_name: string, max_missing_rate: number }` — 默认 'tenant' / 0.1
  - 定义 `Severity = 'info' | 'warning' | 'critical'`
  - 定义 `AlertStatus = 'open' | 'acknowledged' | 'resolved'`
- [ ] **1.3** 定义预置规则模板
  - `data-agent/src/lib/anomaly/rule-templates.ts`
  - 提供 4-6 个开箱即用的规则模板，用户创建时可选模板快速配置:
    - "月度费用突增": `mom_increase`, threshold: 0.3, severity: warning
    - "周度费用激增": `wow_increase`, threshold: 0.5, severity: warning
    - "日费用超限告警": `absolute_threshold`, daily_limit: 5000, severity: critical
    - "新服务出现": `new_service`, severity: info
    - "新区域出现": `new_region`, severity: info
    - "标签缺失": `tag_missing`, tag_name: 'tenant', max_missing_rate: 0.1, severity: warning
  - 每个模板包含: name, ruleType, config (默认值), severity, description
- [ ] **1.4** 实现 `/api/anomalies/rules` API
  - `GET /api/anomalies/rules` — 获取当前用户的规则列表（含 alert 统计：openCount）
  - `POST /api/anomalies/rules` — 创建规则
    - 请求体用 zod 校验: name, ruleType, config, accountIds[], services[], regions[], severity, channels[], enabled?
    - 根据 ruleType 动态校验 config 结构
    - 如果传入 `templateId`，从模板预填默认值
  - 返回创建的规则（含 id）
- [ ] **1.5** 实现 `/api/anomalies/rules/[id]` API
  - `PATCH /api/anomalies/rules/[id]` — 更新规则（部分更新）
    - 校验规则归属当前用户
    - 可更新字段: name, enabled, config, accountIds, services, regions, severity, channels
  - `DELETE /api/anomalies/rules/[id]` — 删除规则（级联删除关联 alerts）
    - 校验规则归属当前用户
- [ ] **1.6** 实现 API 请求校验
  - `data-agent/src/lib/anomaly/validators.ts`
  - 使用 zod 定义:
    - `createRuleSchema` — 创建规则请求校验
    - `updateRuleSchema` — 更新规则请求校验（所有字段可选）
    - `ruleConfigSchema` — 按 ruleType 分支的 config 校验（discriminated union）

### 涉及文件

| 文件 | 操作 |
|------|------|
| `data-agent/prisma/schema.prisma` | 修改 — 添加 AnomalyRule + AnomalyAlert 模型 |
| `data-agent/prisma/migrations/` | 新建 — Migration 文件 |
| `data-agent/src/lib/anomaly/types.ts` | 新建 — 规则类型 + 配置 Schema 定义 |
| `data-agent/src/lib/anomaly/rule-templates.ts` | 新建 — 预置规则模板 |
| `data-agent/src/lib/anomaly/validators.ts` | 新建 — zod 校验 Schema |
| `data-agent/src/app/api/anomalies/rules/route.ts` | 新建 — 规则列表 + 创建 |
| `data-agent/src/app/api/anomalies/rules/[id]/route.ts` | 新建 — 规则更新 + 删除 |

### 验证标准

- [ ] `npx prisma migrate dev` 执行成功，anomaly_rules 和 anomaly_alerts 表已创建
- [ ] `POST /api/anomalies/rules` 可创建各种 ruleType 的规则
- [ ] 创建 `mom_increase` 规则时，config 缺少 threshold 返回 400 + 明确字段错误
- [ ] 创建规则时传入不支持的 ruleType 返回 400
- [ ] `GET /api/anomalies/rules` 返回当前用户的规则列表
- [ ] `PATCH /api/anomalies/rules/[id]` 可更新 enabled、config 等字段
- [ ] 用户 A 无法修改/删除用户 B 的规则（返回 404）
- [ ] `DELETE /api/anomalies/rules/[id]` 删除规则后，关联的 alerts 一同删除
- [ ] 从模板创建规则（传入 templateId）→ 默认值正确填充
- [ ] 未登录状态访问所有端点返回 401
- [ ] `make lint` && `make build` 通过

---

## Step 2: 异常检测引擎 (Detection Engine)

**目标**: 实现规则评估引擎，读取启用的规则，查询 BigQuery 数据，检测异常，生成 AnomalyAlert 并去重。

### 任务清单

- [ ] **2.1** 实现检测引擎核心框架
  - `data-agent/src/lib/anomaly/detector.ts`
  - `detectAnomalies()` 主函数:
    1. 查询所有 `enabled: true` 的 AnomalyRule
    2. 按 ruleType 分组执行检测
    3. 对检测结果进行去重（同一 rule + account_id + service + month = 不重复）
    4. 批量创建 AnomalyAlert 记录
    5. 返回新生成的 alerts 列表
  - 错误处理: 单条规则执行失败不影响其他规则，记录错误日志
- [ ] **2.2** 实现 `mom_increase` 检测器
  - `data-agent/src/lib/anomaly/detectors/mom-increase.ts`
  - 查询 BigQuery: 当月 vs 上月，按 account_id + service 分组
  - 过滤条件: 变化比例 > threshold 且 当月金额 > min_amount
  - 返回异常列表: title, description, severity, dataSnapshot
  - dataSnapshot 包含: current_value, previous_value, change_pct, account_id, service, period
- [ ] **2.3** 实现 `wow_increase` 检测器
  - `data-agent/src/lib/anomaly/detectors/wow-increase.ts`
  - 查询逻辑类似 mom，但对比当周 vs 上周（按 ISO week）
  - 适合检测短期费用激增
- [ ] **2.4** 实现 `absolute_threshold` 检测器
  - `data-agent/src/lib/anomaly/detectors/absolute-threshold.ts`
  - 查询最近一天的费用汇总
  - 超过 daily_limit 的 account_id + service 组合触发告警
  - 如果 config 中指定了 service，只检测该服务
- [ ] **2.5** 实现 `new_service` 检测器
  - `data-agent/src/lib/anomaly/detectors/new-service.ts`
  - 查询当月出现但历史未出现的 service（对比过去 6 个月）
  - 按 account_ids 过滤范围
- [ ] **2.6** 实现 `new_region` 检测器
  - `data-agent/src/lib/anomaly/detectors/new-region.ts`
  - 查询当月出现但历史未出现的 region（对比过去 6 个月）
  - 按 account_ids 过滤范围
- [ ] **2.7** 实现 `tag_missing` 检测器
  - `data-agent/src/lib/anomaly/detectors/tag-missing.ts`
  - 查询 tag 覆盖率（基于 mart_cost_summary 或 mart_tag_compliance）
  - 缺失率超过 max_missing_rate 时触发告警
- [ ] **2.8** 实现去重逻辑
  - `data-agent/src/lib/anomaly/dedup.ts`
  - 去重键: ruleId + dataSnapshot.account_id + dataSnapshot.service + period（月）
  - 查询已有 open/acknowledged 状态的 alerts
  - 如果存在相同去重键的 alert → 跳过，不重复创建
  - 如果已有 alert 但状态为 resolved → 允许创建新 alert（新一轮检测）
- [ ] **2.9** 实现 `/api/cron/detect-anomalies` 端点
  - `POST /api/cron/detect-anomalies`
  - **安全鉴权**: 复用 Phase 2 的 `cron-auth.ts`（HMAC 签名验证）
  - **速率限制**: 每小时最多 1 次调用（异常检测查询开销大）
  - **幂等性**: 同一小时内重复调用返回上次结果
  - 调用 `detectAnomalies()` 引擎
  - 返回: 检测的规则数、新生成的告警数、跳过的重复数
  - 添加执行耗时日志
- [ ] **2.10** 编写检测器 SQL 模板
  - `data-agent/src/lib/anomaly/sql-templates.ts`
  - 集中管理所有检测器使用的 BigQuery SQL
  - 参数化查询，防止 SQL 注入
  - SQL 引用的表: `finops_cloud_billing.mart_cost_monthly_by_service`, `finops_cloud_billing.mart_cost_monthly_by_region_service`
- [ ] **2.11** BigQuery 成本优化
  - 批量 SQL 合并: 同一 ruleType 的多条规则合并为单次 BigQuery 查询
  - 中间结果缓存: 当月费用汇总数据在检测周期内缓存（避免多条规则重复查询相同数据）
  - 分区裁剪强制: 所有检测器 SQL 必须包含 `billing_period_month` 过滤条件
  - 查询预算检查: 复用 Phase 2 的 `bigquery-budget.ts`，单次检测周期上限 100GB

### 涉及文件

| 文件 | 操作 |
|------|------|
| `data-agent/src/lib/anomaly/detector.ts` | 新建 — 检测引擎主框架 |
| `data-agent/src/lib/anomaly/detectors/mom-increase.ts` | 新建 — 月环比检测器 |
| `data-agent/src/lib/anomaly/detectors/wow-increase.ts` | 新建 — 周环比检测器 |
| `data-agent/src/lib/anomaly/detectors/absolute-threshold.ts` | 新建 — 绝对阈值检测器 |
| `data-agent/src/lib/anomaly/detectors/new-service.ts` | 新建 — 新服务检测器 |
| `data-agent/src/lib/anomaly/detectors/new-region.ts` | 新建 — 新区域检测器 |
| `data-agent/src/lib/anomaly/detectors/tag-missing.ts` | 新建 — 标签缺失检测器 |
| `data-agent/src/lib/anomaly/dedup.ts` | 新建 — 去重逻辑 |
| `data-agent/src/lib/anomaly/sql-templates.ts` | 新建 — BigQuery SQL 模板 |
| `data-agent/src/app/api/cron/detect-anomalies/route.ts` | 新建 — Cron 触发端点 |

### 验证标准

- [ ] 手动创建一条 `mom_increase` 规则 → 调用 `POST /api/cron/detect-anomalies` → 检测引擎执行成功
- [ ] 检测到环比增长 > threshold 的 service → 自动创建 AnomalyAlert（status: open）
- [ ] dataSnapshot 包含完整的异常数据快照（current_value, previous_value, change_pct 等）
- [ ] 同一规则 + 同一 account_id + 同一 service + 同一月份 → 不重复创建告警（去重生效）
- [ ] 已有 resolved 状态的相同告警 → 新一轮检测可创建新的 open 告警
- [ ] 单条规则查询 BigQuery 失败 → 记录错误日志，不影响其他规则继续执行
- [ ] 无 CRON_SECRET 或 token 不匹配 → 返回 403
- [ ] 所有 6 种规则类型的检测器均可正常运行
- [ ] 检测引擎返回结果: `{ rulesEvaluated: N, alertsCreated: M, duplicatesSkipped: K }`
- [ ] `make lint` && `make build` 通过

---

## Step 3: 告警管理 + 通知推送

**目标**: 实现告警的查询、状态流转 API，并在告警生成时通过 Notification Hub 推送到用户配置的渠道。

### 任务清单

- [ ] **3.1** 实现 `/api/anomalies/alerts` 查询 API
  - `GET /api/anomalies/alerts` — 获取当前用户相关的告警列表
    - 通过 `rule.userId = currentUser.id` 关联查询
    - 支持筛选: `status` (open/acknowledged/resolved), `severity` (info/warning/critical), `ruleId`
    - 支持分页: `page`, `pageSize`（默认 20）
    - 按 `detectedAt` 降序排列
    - 返回告警列表 + 关联的规则名称
- [ ] **3.2** 实现 `/api/anomalies/alerts/[id]` 状态更新 API
  - `PATCH /api/anomalies/alerts/[id]` — 更新告警状态
    - 允许的状态流转:
      - `open` → `acknowledged` — 用户确认已知晓
      - `open` → `resolved` — 直接标记已解决
      - `acknowledged` → `resolved` — 确认后标记已解决
    - resolved 时自动填充 `resolvedAt` + `resolvedBy`（当前用户 ID）
    - 不允许回退（resolved → open）
  - 校验告警归属当前用户（通过 rule.userId）
- [ ] **3.3** 实现告警通知分发逻辑
  - `data-agent/src/lib/anomaly/alert-notifier.ts`
  - 检测引擎生成新告警后调用:
    1. 根据规则的 `channels[]` 决定推送渠道
    2. 为每个渠道创建 `Notification` 记录（type: 'anomaly', channel: 'web' | 'feishu'）
    3. Web 渠道: 仅创建 Notification 记录（前端轮询或 SSE 展示）
    4. Feishu 渠道: 调用飞书推送接口（Step 6 实现，此处预留接口）
  - 通知内容生成:
    - title: 告警标题（来自 AnomalyAlert.title）
    - body: 告警描述 + 关键数据（金额、变化百分比等）
- [ ] **3.4** 实现告警汇总统计 API（可选）
  - `GET /api/anomalies/summary` — 当前用户的告警统计
    - openCount, acknowledgedCount, criticalOpenCount
    - 最近 7 天新增告警数
    - 用于前端 Dashboard 展示告警概览
- [ ] **3.5** 集成到检测引擎
  - 修改 `detector.ts`，在创建 AnomalyAlert 后调用 `alert-notifier.ts` 分发通知
  - 保证通知分发失败不影响告警创建（catch + 日志）

### 涉及文件

| 文件 | 操作 |
|------|------|
| `data-agent/src/app/api/anomalies/alerts/route.ts` | 新建 — 告警列表查询 |
| `data-agent/src/app/api/anomalies/alerts/[id]/route.ts` | 新建 — 告警状态更新 |
| `data-agent/src/app/api/anomalies/summary/route.ts` | 新建 — 告警汇总统计 (可选) |
| `data-agent/src/lib/anomaly/alert-notifier.ts` | 新建 — 通知分发逻辑 |
| `data-agent/src/lib/anomaly/detector.ts` | 修改 — 集成通知分发 |

### 验证标准

- [ ] `GET /api/anomalies/alerts` 返回当前用户的告警列表（按 detectedAt 降序）
- [ ] 支持按 status、severity、ruleId 筛选
- [ ] 分页正确: page=2&pageSize=10 返回第 11-20 条
- [ ] `PATCH /api/anomalies/alerts/[id]` 将 open → acknowledged → resolved 状态流转正常
- [ ] resolved 时自动写入 resolvedAt 和 resolvedBy
- [ ] 不允许 resolved → open 的回退（返回 400）
- [ ] 用户 A 无法操作用户 B 的告警（返回 404）
- [ ] 新告警生成后，Notification 记录自动创建
- [ ] channels 包含 'web' → 创建 web 通知
- [ ] channels 包含 'feishu' → 预留飞书推送接口调用（Step 6 实现）
- [ ] 通知分发失败不影响告警创建
- [ ] `make lint` && `make build` 通过

---

## Step 4: 飞书 Bot 接入 + WebSocket Client

**目标**: 完成飞书企业自建应用配置，安装 SDK，实现 WebSocket 长连接客户端，验证 Bot 可正常接收消息事件。

### 任务清单

- [ ] **4.1** 飞书开放平台应用配置
  - 创建企业自建应用
  - 配置 Bot 能力: 群聊 + 单聊
  - 事件订阅方式: **长连接 (WebSocket)**（非 Webhook）
  - 订阅事件: `im.message.receive_v1`
  - 申请权限: `im:message`, `im:message.receive_v1`, `contact:user.email:readonly`
  - 记录 App ID 和 App Secret
- [ ] **4.2** 安装飞书 SDK
  - `npm install @larksuiteoapi/node-sdk`
  - 确认 SDK 版本支持 WebSocket 长连接（`WSClient`）
- [ ] **4.3** 添加飞书相关环境变量
  - `.env.example` 添加: `FEISHU_APP_ID`, `FEISHU_APP_SECRET`, `FEISHU_WS_ENABLED`
  - `docker-compose.yml` 添加环境变量传递
  - 本地 `.env.local` 配置实际凭据（不提交）
- [ ] **4.4** 实现飞书 REST Client 单例
  - `data-agent/src/lib/feishu/client.ts`
  - 使用 `@larksuiteoapi/node-sdk` 的 `Client` 类
  - 单例模式，全局共享
  - 封装常用操作:
    - `sendMessage(chatId, content)` — 发送文本消息
    - `sendInteractiveCard(chatId, card)` — 发送交互式卡片
    - `getUserByOpenId(openId)` — 获取用户信息（含企业邮箱）
    - `replyMessage(messageId, content)` — 回复消息
- [ ] **4.5** 实现 WebSocket 长连接客户端
  - `data-agent/src/lib/feishu/ws-client.ts`
  - 使用 `WSClient` 建立 WebSocket 连接
  - 注册 `im.message.receive_v1` 事件处理器
  - 初期事件处理: 收到消息 → 日志打印 sender + text → 回复 "收到"（echo 测试）
  - 连接状态日志: 启动、断连、重连
  - 环境变量缺失时优雅跳过（不影响 Web 服务启动）
- [ ] **4.6** 实现消息文本提取工具
  - `data-agent/src/lib/feishu/message-parser.ts`
  - 解析飞书消息体的 JSON 结构，提取纯文本
  - 处理 @Bot 前缀移除
  - 处理图片/文件消息（暂不支持，回复提示）
  - 处理空消息、系统消息等边界情况
- [ ] **4.7** 配置 WebSocket Client 生命周期 — 独立 feishu-worker 进程
  - **唯一方案**: `data-agent/src/feishu-worker.ts` — 独立进程（~~instrumentation.ts 已废弃~~）
  - **废弃原因**: Next.js instrumentation hook 与 HMR/重编译耦合，开发环境 WS 频繁断连；生产环境进程回收同样导致不可控断连
  - 独立进程实现:
    ```typescript
    // data-agent/src/feishu-worker.ts
    import { startFeishuWSClient } from './lib/feishu/ws-client'

    console.log('[feishu-worker] Starting standalone process...')
    startFeishuWSClient()

    process.on('SIGTERM', () => {
      console.log('[feishu-worker] Received SIGTERM, shutting down...')
      stopFeishuWSClient()
      process.exit(0)
    })
    ```
  - docker-compose 配置:
    ```yaml
    feishu-worker:
      build:
        context: ./data-agent
        dockerfile: Dockerfile
      command: ["node", "dist/feishu-worker.js"]
      environment:
        DATABASE_URL: postgresql://finops:${POSTGRES_PASSWORD:-finops_dev}@postgres:5432/finops_agent
        FEISHU_APP_ID: ${FEISHU_APP_ID}
        FEISHU_APP_SECRET: ${FEISHU_APP_SECRET}
      depends_on:
        - postgres
      restart: unless-stopped
    ```
  - 开发环境: `npx tsx src/feishu-worker.ts`（Makefile 添加 `make feishu-worker`）
  - Makefile 添加: `make feishu-worker` — 启动飞书 worker 独立进程
- [ ] **4.8** 实现消息卡片模板
  - `data-agent/src/lib/feishu/card-templates.ts`
  - 定义通用卡片构建器:
    - `buildQueryResultCard(question, sql, result, analysis?)` — 查询结果卡片
    - `buildAlertCard(alert)` — 异常告警卡片（红色 header）
    - `buildErrorCard(message)` — 错误提示卡片
    - `buildOnboardingCard()` — 首次交互引导卡片
    - `buildBindingCard(linkUrl)` — 身份绑定卡片
  - 飞书交互式消息卡片 JSON 格式

### 涉及文件

| 文件 | 操作 |
|------|------|
| `data-agent/package.json` | 修改 — 添加 @larksuiteoapi/node-sdk |
| `data-agent/src/lib/feishu/client.ts` | 新建 — 飞书 REST Client 单例 |
| `data-agent/src/lib/feishu/ws-client.ts` | 新建 — WebSocket 长连接客户端 |
| `data-agent/src/lib/feishu/message-parser.ts` | 新建 — 消息解析工具 |
| `data-agent/src/lib/feishu/card-templates.ts` | 新建 — 消息卡片模板 |
| `data-agent/src/feishu-worker.ts` | 新建 — 飞书 WebSocket 独立进程 |
| `data-agent/.env.example` | 修改 — 添加飞书环境变量 |
| `docker-compose.yml` | 修改 — 添加飞书环境变量传递 |

### 验证标准

- [ ] `npm install` 成功安装 `@larksuiteoapi/node-sdk`
- [ ] `make feishu-worker` 启动独立进程 → 控制台输出 `[feishu-worker] Starting standalone process...` + `[Feishu] WebSocket client started`
- [ ] feishu-worker 进程崩溃/重启不影响 data-agent Web 服务
- [ ] docker-compose up 同时启动 data-agent 和 feishu-worker 两个独立服务
- [ ] `FEISHU_APP_ID` / `FEISHU_APP_SECRET` 缺失 → 日志提示跳过，不抛错
- [ ] 在飞书内 @Bot 发送消息 → 控制台打印收到的消息事件
- [ ] Bot 回复 "收到" 文本（echo 测试通过）
- [ ] 消息解析可正确提取纯文本，移除 @Bot 前缀
- [ ] 卡片模板 JSON 结构正确，可通过飞书消息卡片搭建工具验证
- [ ] `make lint` && `make build` 通过

---

## Step 5: 飞书被动查询 (Passive Query)

**目标**: 用户在飞书内 @Bot 提问成本问题，Bot 调用查询引擎执行 NL→SQL→BigQuery 查询，将结果以交互式卡片返回。

### 任务清单

- [ ] **5.1** 实现飞书事件处理器
  - `data-agent/src/lib/feishu/event-handler.ts`
  - `handleMessageEvent(data)` 主函数:
    1. 解析消息文本（调用 message-parser）
    2. 过滤非文本消息（图片/文件等暂回复"暂不支持"）
    3. 解析用户身份（调用 resolveFeishuUser，Step 7 实现，此处先用 openId 标识）
    4. 调用查询引擎
    5. 格式化结果为飞书卡片
    6. 发送回复
  - 错误处理: 查询失败 → 发送错误卡片（含友好提示）
  - 超时处理: 查询超过 30s → 发送"查询中，请稍候"中间状态
- [ ] **5.2** 封装查询引擎调用接口
  - `data-agent/src/lib/feishu/query-bridge.ts`
  - 复用现有 `/api/chat` 的核心逻辑（LLM 编排 + SQL 生成 + BigQuery 执行）
  - 不走 HTTP，直接调用内部函数（避免 localhost 请求开销）
  - 入参: `{ question: string, userId?: string, mode: 'analyst' }`
  - 出参: `{ sql, result, analysis, error? }`
  - 默认使用分析模式（analyst），飞书场景下用户更需要洞察而非原始数据
- [ ] **5.3** 实现查询结果卡片格式化
  - `data-agent/src/lib/feishu/result-formatter.ts`
  - 将 QueryResult 转换为飞书 markdown 表格（飞书卡片支持 markdown）
  - 大表格截断: 行数 > 10 时截断 + "查看完整数据" 链接
  - 数值格式化: 金额保留 2 位小数 + $ 前缀，百分比保留 1 位
  - 如果有 analysis（分析模式），分析文本放在表格上方
  - SQL 可折叠展示
- [ ] **5.4** 修改 ws-client.ts 注册正式事件处理器
  - 替换 Step 4 的 echo 处理器为 `event-handler.ts`
  - 添加消息处理并发控制（避免大量消息同时触发查询）
  - 简单信号量: 同一用户最多 2 个并发查询
- [ ] **5.5** 实现飞书特殊命令处理
  - 在 event-handler 中识别特殊命令:
    - `帮助` / `help` → 发送功能说明卡片
    - `设置` / `settings` → 发送设置入口卡片（画像设置，Step 8 实现）
    - `告警` / `alerts` → 发送未处理告警摘要
  - 非特殊命令 → 作为成本查询处理

### 涉及文件

| 文件 | 操作 |
|------|------|
| `data-agent/src/lib/feishu/event-handler.ts` | 新建 — 消息事件处理器 |
| `data-agent/src/lib/feishu/query-bridge.ts` | 新建 — 查询引擎桥接 |
| `data-agent/src/lib/feishu/result-formatter.ts` | 新建 — 结果格式化为飞书卡片 |
| `data-agent/src/lib/feishu/ws-client.ts` | 修改 — 注册正式事件处理器 |
| `data-agent/src/app/api/chat/route.ts` | 修改 — 抽取核心逻辑为可复用函数 |

### 验证标准

- [ ] 飞书 @Bot 提问 "上个月 EC2 费用是多少" → Bot 回复包含 SQL + 数据表格 + 分析文本的卡片
- [ ] 查询结果表格超过 10 行 → 自动截断并提示 "查看完整数据"
- [ ] 金额显示格式正确: `$12,345.67`
- [ ] 飞书 @Bot 发送图片 → Bot 回复 "暂不支持图片查询"
- [ ] 查询失败（SQL 错误）→ Bot 回复友好的错误提示卡片
- [ ] 发送 "帮助" → Bot 回复功能说明
- [ ] 同一用户短时间内连续发送 3 条查询 → 第 3 条等待前面完成（并发控制）
- [ ] 查询超时 30s → Bot 先发送 "查询中" 提示
- [ ] `make lint` && `make build` 通过

---

## Step 6: 飞书主动推送 (Active Push)

**目标**: 实现异常告警和定期报告的飞书主动推送能力，根据用户通知偏好选择推送渠道。

### 任务清单

- [ ] **6.1** 实现飞书消息推送服务
  - `data-agent/src/lib/feishu/push-service.ts`
  - `pushToFeishu(userId, card)` — 推送消息到个人
    1. 查询 User 的 `feishuOpenId`
    2. 如果 `notifyFeishu = false` 或 `feishuOpenId` 为空 → 跳过
    3. 调用飞书 REST API 发送交互式卡片
    4. 记录发送结果（更新 Notification.sentAt）
  - `pushToFeishuGroup(chatId, card)` — 推送消息到群聊
  - 错误处理: 发送失败 → 日志记录，不阻断主流程
  - 限流: 单用户每小时最多 60 条推送（飞书 API 限制）
- [ ] **6.2** 实现告警推送集成
  - 修改 `alert-notifier.ts`（Step 3.3），对 channels 包含 'feishu' 的通知:
    1. 构建告警卡片（使用 `buildAlertCard()`）
    2. 调用 `pushToFeishu(userId, card)`
    3. 更新 Notification 的 sentAt
  - 告警卡片包含:
    - 红色 header（critical）/ 橙色 header（warning）/ 蓝色 header（info）
    - 告警详情: 账户、服务、异常描述、金额变化
    - 操作按钮: "查看详情"（跳转 Web）、"标记已处理"（回调）
- [ ] **6.3** 实现报告推送集成
  - `data-agent/src/lib/feishu/report-pusher.ts`
  - 报告生成完成后，对 channels 包含 'feishu' 的订阅:
    1. 构建报告摘要卡片（标题 + 关键数字 + "查看完整报告" 链接）
    2. 调用 `pushToFeishu(userId, card)`
  - 报告卡片限制: 飞书卡片内容有长度限制，仅推送摘要，完整报告引导到 Web
- [ ] **6.4** 实现卡片交互回调处理
  - `data-agent/src/lib/feishu/card-action-handler.ts`
  - 处理飞书卡片按钮回调:
    - "标记已处理" → 更新 AnomalyAlert 状态为 acknowledged
    - 回复确认消息
  - 在 WSClient 中注册 card action 事件处理器
- [ ] **6.5** 实现飞书群聊管理（可选）
  - 支持 Bot 被添加到群聊后，记录 chat_id
  - 支持告警推送到指定群聊（适合团队级告警）
  - 在规则 channels 中支持 `feishu_group:<chat_id>` 格式

### 涉及文件

| 文件 | 操作 |
|------|------|
| `data-agent/src/lib/feishu/push-service.ts` | 新建 — 飞书推送服务 |
| `data-agent/src/lib/feishu/report-pusher.ts` | 新建 — 报告推送逻辑 |
| `data-agent/src/lib/feishu/card-action-handler.ts` | 新建 — 卡片交互回调 |
| `data-agent/src/lib/anomaly/alert-notifier.ts` | 修改 — 集成飞书推送 |
| `data-agent/src/lib/feishu/ws-client.ts` | 修改 — 注册卡片回调处理器 |
| `data-agent/src/lib/feishu/card-templates.ts` | 修改 — 完善告警/报告卡片模板 |

### 验证标准

- [ ] 新告警生成（channels 含 'feishu'）→ 飞书个人消息收到告警卡片
- [ ] 告警卡片: critical 红色 header、warning 橙色 header、info 蓝色 header
- [ ] 告警卡片包含: 账户名、服务名、异常描述、金额变化百分比
- [ ] 点击告警卡片 "查看详情" → 跳转 Web 端告警页面
- [ ] 点击告警卡片 "标记已处理" → 告警状态更新为 acknowledged → Bot 回复确认
- [ ] 报告生成完成（channels 含 'feishu'）→ 飞书收到报告摘要卡片
- [ ] 用户 `notifyFeishu = false` → 不推送飞书消息
- [ ] 用户 `feishuOpenId` 为空 → 不推送，仅走 web 渠道
- [ ] 飞书推送失败 → 记录错误日志，不影响告警/报告创建
- [ ] `make lint` && `make build` 通过

---

## Step 7: 统一身份关联 (User Identity Linking)

**目标**: 实现 Google OAuth（Web）和飞书 Bot 两个渠道的用户自动关联，确保同一个人无论从哪个渠道进入都映射到同一个 User 记录。

### 任务清单

- [ ] **7.1** 实现飞书用户身份解析
  - 修改 `data-agent/src/lib/user-identity.ts`（Phase 1 中已创建占位接口）
  - 实现 `resolveFeishuUser(openId: string): Promise<User>` 完整逻辑:
    1. 按 `feishuOpenId` 查找 → 找到 → 直接返回 User
    2. 未找到 → 调用飞书 API `getUserByOpenId(openId)` 获取企业邮箱
    3. 按 `email` 匹配已有 User（Google OAuth 创建的）→ 找到 → 自动合并身份字段（feishuOpenId, feishuUnionId, feishuEmail, notifyFeishu=true）
    4. 邮箱也未匹配 → 检查邮箱域名:
       - 允许域名（@pingcap.com 等）→ 自动创建新 User
       - 不允许域名 → 抛出 `UnlinkedUserError`
  - 允许域名列表从环境变量 `ALLOWED_DOMAINS` 读取
- [ ] **7.2** 实现未关联用户的绑定流程
  - `data-agent/src/lib/feishu/binding-service.ts`
  - 当 `resolveFeishuUser` 抛出 `UnlinkedUserError` 时:
    1. 生成一次性绑定 token（UUID + 15 分钟过期）
    2. 存储到数据库或内存缓存: `{ token, feishuOpenId, expiresAt }`
    3. 构建绑定链接: `https://<domain>/api/auth/link?feishu_open_id=xxx&token=yyy`
    4. 通过飞书 Bot 发送绑定卡片（`buildBindingCard(linkUrl)`）
  - 绑定 token 存储方案:
    - 简单方案: 在 User 表添加 `bindingToken`, `bindingTokenExpiresAt` 临时字段
    - 或使用内存 Map（重启后过期，15 分钟足够）
- [ ] **7.3** 实现绑定链接 API
  - 修改 `data-agent/src/app/api/auth/link/route.ts`（Phase 1 中已创建占位）
  - `GET /api/auth/link?feishu_open_id=xxx&token=yyy`
    1. 校验 token 有效（存在 + 未过期 + feishu_open_id 匹配）
    2. 将 feishu_open_id 和 token 存入 session/cookie
    3. 重定向到 Google OAuth 登录
  - Google OAuth 回调后:
    1. 检查 session 中的 feishu_open_id
    2. 将 feishuOpenId 写入当前登录的 User
    3. 清除 token（一次性使用）
    4. 重定向到成功页面
    5. 通过飞书 Bot 发送绑定成功确认消息
- [ ] **7.4** 集成到飞书事件处理器
  - 修改 `event-handler.ts`，收到消息时:
    1. 调用 `resolveFeishuUser(openId)` 获取 User
    2. 如果成功 → 继续正常查询流程
    3. 如果抛出 `UnlinkedUserError` → 触发绑定流程，发送绑定卡片
  - 已绑定用户后续消息直接命中 feishuOpenId 查询（快路径）
- [ ] **7.5** 修改 NextAuth 回调支持身份合并
  - 修改 `data-agent/src/lib/auth.ts` 的 signIn 回调:
    - 如果 session 中有 pending 的 feishu_open_id → 绑定到当前 User
    - 确保 Google OAuth 登录时，如果 User 已存在（email 匹配）但 googleId 为空 → 自动填充

### 涉及文件

| 文件 | 操作 |
|------|------|
| `data-agent/src/lib/user-identity.ts` | 修改 — 实现 resolveFeishuUser 完整逻辑 |
| `data-agent/src/lib/feishu/binding-service.ts` | 新建 — 绑定 token 生成 + 管理 |
| `data-agent/src/app/api/auth/link/route.ts` | 修改 — 实现绑定链接逻辑 |
| `data-agent/src/lib/feishu/event-handler.ts` | 修改 — 集成身份解析 |
| `data-agent/src/lib/auth.ts` | 修改 — signIn 回调支持飞书绑定 |
| `data-agent/src/lib/feishu/card-templates.ts` | 修改 — 绑定卡片 + 绑定成功卡片 |

### 验证标准

- [ ] 飞书用户（@pingcap.com 邮箱）首次 @Bot → 自动创建 User 记录（email = 飞书企业邮箱）
- [ ] 同一邮箱的飞书用户和 Google OAuth 用户 → 合并为同一个 User 记录
- [ ] 合并后 User 记录同时包含 googleId 和 feishuOpenId
- [ ] 合并后 notifyFeishu 自动设为 true
- [ ] 飞书邮箱与 Google 邮箱不同 → Bot 发送绑定卡片
- [ ] 点击绑定链接 → Google OAuth 登录 → feishuOpenId 写入 User → Bot 发送绑定成功确认
- [ ] 绑定 token 15 分钟过期后使用 → 返回 "链接已过期"
- [ ] 绑定 token 二次使用 → 返回 "链接已使用"
- [ ] 已绑定用户再次 @Bot → 直接按 feishuOpenId 快速查找（不再调飞书 API）
- [ ] 不允许域名的飞书用户 → 仅发送绑定卡片，不自动创建
- [ ] `make lint` && `make build` 通过

---

## Step 8: 用户画像系统 (User Profile)

**目标**: 为每个用户维护持续演进的偏好画像，用于个性化查询范围、回复风格和通知内容。

### 任务清单

- [ ] **8.1** 验证 UserProfile 模型已就绪
  - 确认 Step 1.1 的 migration 已包含 `UserProfile` 模型
  - 验证 1:1 关联 User（userId unique）正常工作
  - 验证字段: focusAccountIds[], focusServices[], focusRegions[], focusClouds[], focusEnv, analysisDepth, responseStyle, preferredViz, language, learnedContext (Json?)
  - 无需新建 migration — UserProfile 已在 Step 1.1 中创建
- [ ] **8.2** 实现 UserProfile CRUD API
  - `GET /api/users/me/profile` — 获取当前用户画像（不存在则返回默认值）
  - `PUT /api/users/me/profile` — 创建或更新画像（upsert）
    - zod 校验所有字段
    - focusAccountIds: 字符串数组，可选校验是否为有效 account_id
    - focusEnv: 'production' | 'development' | 'all'
    - analysisDepth: 'summary' | 'detailed'
    - responseStyle: 'concise' | 'balanced' | 'detailed'
    - preferredViz: 'table' | 'chart' | 'both'
    - language: 'zh' | 'en'
  - `PATCH /api/users/me/profile` — 部分更新画像（仅更新传入的字段）
- [ ] **8.3** 实现 Onboarding 引导
  - `data-agent/src/lib/profile/onboarding.ts`
  - 首次交互检测: User 没有关联的 UserProfile → 触发 onboarding
  - 飞书侧 onboarding:
    - 发送交互式卡片（带 SelectMenu + Button）
    - 问题 1: 关注范围（全部/生产/开发/自定义）
    - 问题 2: 角色（管理层/工程 TL/FinOps）→ 映射 responseStyle
    - 问题 3: 关注粒度（整体趋势/服务级细节/两者）→ 映射 analysisDepth
    - 用户完成选择 → 创建 UserProfile
  - Web 侧 onboarding:
    - 首次登录弹出引导对话框（wizard 组件）
    - 3 步选择 → 创建 UserProfile
  - 跳过 onboarding 使用默认值
- [ ] **8.4** 实现画像注入 LLM System Prompt
  - `data-agent/src/lib/profile/prompt-builder.ts`
  - `buildPersonalizedSystemPrompt(basePrompt, profile): string`
  - 注入内容:
    - 关注范围 → "用户主要关注账户 X, Y 的生产环境费用，默认查询这些账户"
    - 关注服务 → "用户关注 AmazonEC2, AmazonS3 服务"
    - 回复风格 → "用户偏好简要回复/适度分析/详细分析"
    - 分析深度 → "用户关注整体趋势/细节钻取"
    - 语言 → "请用中文/英文回复"
    - 行为学习 → 注入 learnedContext.llm_summary（如有）
  - 集成到 `/api/chat` 路由和 `query-bridge.ts`
- [ ] **8.5** 实现画像自动学习
  - `data-agent/src/lib/profile/profile-learner.ts`
  - `updateProfileFromInteraction(userId, question, result)`:
    1. 提取查询涉及的实体（accounts, services, regions）
    2. 更新高频查询模式（frequently_queried，最多 20 项）
    3. 更新最近话题（last_topics，最多 10 项）
    4. 累计交互计数
    5. 每 20 次交互，调用 LLM 重新生成用户画像摘要（llm_summary）
  - 异步执行，不阻塞查询响应
  - 实体提取: 从 SQL 中解析 account_id IN (...)、service = '...' 等条件
  - **LLM 成本控制**:
    - `maxTokens: 500`（画像摘要不需要长文本）
    - 输入裁剪: 仅传入最近 10 次交互的摘要，总计 < 2000 tokens
    - 频率限制: 每用户每 4 小时最多 1 次 LLM 画像更新
    - 使用低成本模型（`llm-service.ts` 的 `summarize` 任务，自动选择 Gemini Flash / GPT-4o-mini）
- [ ] **8.6** 实现实体提取工具
  - `data-agent/src/lib/profile/entity-extractor.ts`
  - 从用户问题和 SQL 中提取:
    - account_ids（正则匹配 account_id IN/= 条件）
    - services（正则匹配 service IN/= 条件）
    - regions（正则匹配 region IN/= 条件）
    - 时间范围（月度/周度/日粒度）
  - 合并到 learnedContext.common_filters
- [ ] **8.7** 集成画像到查询链路
  - 修改 `/api/chat/route.ts`:
    1. 查询前加载 UserProfile
    2. 调用 `buildPersonalizedSystemPrompt()` 增强 system prompt
    3. 查询后调用 `updateProfileFromInteraction()`（异步）
  - 修改 `event-handler.ts`（飞书）:
    1. 解析身份后加载 UserProfile
    2. 传入 profile 的默认过滤条件
    3. 按 profile 的 preferredViz 和 responseStyle 调整回复
- [ ] **8.8** 实现画像重置功能
  - `DELETE /api/users/me/profile` — 删除画像，恢复默认
  - 飞书命令 "重置设置" → 删除 UserProfile → 重新触发 onboarding

### 涉及文件

| 文件 | 操作 |
|------|------|
| `data-agent/prisma/schema.prisma` | 修改 — 添加 UserProfile 模型 |
| `data-agent/prisma/migrations/` | 新建 — Migration 文件 |
| `data-agent/src/app/api/users/me/profile/route.ts` | 新建 — 画像 CRUD API |
| `data-agent/src/lib/profile/onboarding.ts` | 新建 — 首次交互引导 |
| `data-agent/src/lib/profile/prompt-builder.ts` | 新建 — 画像注入 System Prompt |
| `data-agent/src/lib/profile/profile-learner.ts` | 新建 — 画像自动学习 |
| `data-agent/src/lib/profile/entity-extractor.ts` | 新建 — 实体提取工具 |
| `data-agent/src/app/api/chat/route.ts` | 修改 — 集成画像到查询链路 |
| `data-agent/src/lib/feishu/event-handler.ts` | 修改 — 集成画像到飞书查询 |
| `data-agent/src/lib/feishu/card-templates.ts` | 修改 — 添加 onboarding 卡片 |

### 验证标准

- [ ] `npx prisma migrate dev` 执行成功，user_profiles 表已创建
- [ ] `GET /api/users/me/profile` 返回用户画像（不存在时返回默认值结构）
- [ ] `PUT /api/users/me/profile` 创建/更新画像成功
- [ ] focusEnv 传入 'invalid' → 返回 400（zod 校验）
- [ ] 新用户首次在飞书 @Bot → 收到 onboarding 引导卡片
- [ ] 完成 onboarding → UserProfile 创建 → 后续查询不再触发引导
- [ ] 用户画像 focusAccountIds: ['123'] → 提问 "上月费用" → SQL 自动加上 `account_id IN ('123')`
- [ ] 用户画像 responseStyle: 'concise' → LLM 回复更简短
- [ ] 用户画像 language: 'en' → LLM 用英文回复
- [ ] 查询完成后，learnedContext 中记录查询涉及的账户/服务/区域
- [ ] 交互满 20 次后，learnedContext.llm_summary 自动更新（LLM 重新生成）
- [ ] LLM 画像更新使用低成本模型（非主力模型）
- [ ] 单次画像 LLM 调用 token 消耗 < 1000（输入 + 输出）
- [ ] 飞书发送 "重置设置" → UserProfile 删除 → 下次交互重新触发 onboarding
- [ ] `DELETE /api/users/me/profile` 成功删除画像
- [ ] 画像自动学习异步执行，不增加查询响应延迟
- [ ] `make lint` && `make build` 通过

---

## 风险与注意事项

### 技术风险

| 风险 | 影响 | 缓解措施 |
|------|------|----------|
| 飞书 WebSocket 连接不稳定 | Bot 无法收到消息 | SDK 内置自动重连；监控连接状态；备选独立进程部署 |
| BigQuery 检测查询耗时过长 | Cron 超时、费用增加 | 限制查询范围（按规则 accountIds 过滤）；设置 BigQuery 查询超时 30s |
| 告警风暴（规则过于敏感） | 大量重复通知骚扰用户 | 去重策略（同 rule + account + service + month）；每日告警上限；severity 分级 |
| 飞书 API 限流 | 推送消息失败 | 单用户每小时 60 条限制；告警合并推送（批量摘要） |
| LLM 画像摘要生成成本 | Token 消耗 | 每 20 次交互才触发一次；限制输入 context 长度 |
| 身份关联冲突 | 一个飞书账号绑定多个 User | feishuOpenId 设为 unique；绑定前校验是否已绑定 |
| instrumentation hook 重启 | WebSocket 断连期间消息丢失 | 飞书服务端有消息缓冲；重连后自动补发；考虑独立进程 |

### 开发注意事项

1. **飞书 App Secret 安全**: 绝对不能提交到代码仓库，使用 `.env.local` 或环境变量注入
2. **WebSocket vs HTTP**: 被动接收事件用 WebSocket 长连接（`WSClient`），主动推送消息用飞书 REST API（`Client`），两者并存
3. **Prisma Migration 顺序**: Phase 3 的 migration 依赖 Phase 1 的 User 表，确保按顺序执行
4. **检测器查询优化**: 每个检测器的 SQL 应尽量使用分区裁剪（`billing_period_month` 是分区键），避免全表扫描
5. **卡片模板调试**: 使用飞书开放平台的 "消息卡片搭建工具" 预览卡片效果，再转为代码
6. **画像更新并发**: `updateProfileFromInteraction` 必须使用乐观锁或 upsert，避免并发覆盖
7. **onboarding 幂等性**: 即使 onboarding 中途中断（用户不回复），下次交互仍能正确检测并重新触发
8. **飞书消息格式**: 飞书卡片 markdown 与标准 markdown 略有差异（如不支持 HTML 标签），需实际测试
9. **Cron 端点幂等**: `POST /api/cron/detect-anomalies` 必须幂等——重复调用不产生重复告警（去重保证）
10. **独立进程备选**: 如果 instrumentation hook 在生产环境不稳定，切换到 `feishu-worker.ts` 独立进程方案，通过 docker-compose 单独运行

---

## 完成后检查

- [ ] `make lint` 通过
- [ ] `make build` 通过
- [ ] `docker-compose up -d --build` 完整启动成功（含 PostgreSQL、data-agent）
- [ ] 所有现有功能不受影响（Phase 1 + Phase 2 回归测试）
- [ ] 新增 API 路由全部需要认证（NextAuth session 或 CRON_SECRET）
- [ ] 无敏感信息硬编码（飞书凭据、CRON_SECRET 均通过环境变量）
- [ ] 端到端验收场景通过:
  1. 创建 `mom_increase` 异常规则（channels: ['web', 'feishu']）
  2. 手动触发 `POST /api/cron/detect-anomalies`
  3. 检测到费用突增 → AnomalyAlert 创建（status: open）
  4. 飞书收到告警推送卡片
  5. 用户在飞书 @Bot 提问 "这个账户上个月发生了什么" → 收到分析结果卡片
  6. 点击告警卡片 "标记已处理" → 告警状态更新为 acknowledged
- [ ] Prisma migration 可在全新数据库上从头执行成功
- [ ] 飞书 Bot 长连接在 dev 和 production 环境均可正常工作

---

*Document Status: Draft*
*Created: 2026-03-10*
