# FinOps 智能 Agent 平台设计文档

本文档描述将现有 data-agent（NL→SQL 查询工具）演进为多云 FinOps 智能 Agent 平台的架构设计。

---

## Changelog

| Version | Date | Changes |
|---------|------|---------|
| v1.2 | 2026-03-10 | 数据查询分层策略 — dbt mart 优先、原始表 admin 手动确认、LLM 上下文限制 |
| v1.1 | 2026-03-10 | Review 优化 — Prisma 分阶段迁移、Message 移除 queryResult、ECharts 替代 Recharts、exceljs 替代 xlsx、统一 LLM 服务层、BigQuery 成本预算、安全设计、WebSocket 独立进程方案、事件驱动通知模式 |
| v1.0 | 2026-03-10 | Initial draft — 架构设计、数据模型、API 路由、飞书集成、报告引擎、异常检测 |

---

## 1. 项目背景与目标

### 1.1 现状

当前 data-agent 是一个 Next.js 14 的 NL→SQL 查询工具，具备以下能力：

- Google OAuth 登录 + 域名/邮箱白名单
- 双 LLM 后端（OpenRouter / Gemini）将自然语言翻译为 BigQuery SQL
- SQL dry-run 校验 + 自动修复（最多 3 轮）
- 查询结果表格展示（TanStack React Table）
- 会话历史存储在浏览器 localStorage

**局限性：**
- 无图表可视化（已安装 Recharts 但未使用）
- 会话丢失（浏览器 localStorage 绑定设备，清除即丢失）
- 无主动推送（用户必须手动打开页面查询）
- 无异常检测（费用突增只能人工发现）
- 无飞书集成（团队主要沟通工具无法触达）
- 无报告能力（月度报告靠手工拼接）

### 1.2 目标

构建面向 FinOps 团队和工程团队的智能 Agent 平台，实现：

| 目标 | 描述 |
|------|------|
| **降低查询门槛** | 自然语言 + 图表可视化，非技术人员也能分析成本 |
| **主动感知异常** | 费用突增/新服务出现等异常自动检测并告警 |
| **定期推送报告** | 周报/月报自动生成并推送到飞书 |
| **双向交互** | 飞书内直接提问、接收告警、查看报告 |
| **会话持久化** | 查询历史服务端存储，跨设备可访问 |

### 1.3 目标用户

| 用户 | 场景 |
|------|------|
| FinOps 团队 | 日常成本监控、月度报告、异常排查 |
| 工程 TL | 按账户/服务查看团队成本、优化决策 |
| 管理层 | 周/月汇总报告、趋势一览 |
| 基础设施团队 | SP/RI 利用率跟踪、资源优化 |

---

## 2. 核心功能模块

### 2.1 模块总览

```
┌──────────────────────────────────────────────────────────────────┐
│                     FinOps Agent Platform                        │
├──────────┬──────────┬──────────┬──────────┬──────────┬──────────┤
│ NL 查询  │ 图表可视化│ 用户系统  │ 订阅推送  │ 异常检测  │ 飞书集成  │
│ (现有)   │ (Phase 1)│ (Phase 1)│ (Phase 2)│ (Phase 3)│ (Phase 3)│
├──────────┴──────────┴──────────┴──────────┴──────────┴──────────┤
│                    基础设施 (Phase 1)                             │
│          PostgreSQL + Prisma + 会话持久化 + 导出增强              │
└──────────────────────────────────────────────────────────────────┘
```

### 2.2 模块详述

**M1: NL→SQL 查询引擎（现有 + 增强）**
- 现有：自然语言 → BigQuery SQL → dry-run → 执行 → 表格结果
- 增强：
  - **双模式交互** — 用户可随时切换两种模式：
    - **分析模式（Analyst）**: 查询数据后，LLM 自动分析结果并给出自然语言洞察（趋势、异常、建议），适合非技术用户快速理解
    - **数据模式（Data）**: 查询数据后，直接展示原始结果表格 + CSV/Excel 导出，适合需要二次加工的专业用户
  - **持续对话** — 在同一会话中连续追问，LLM 基于上下文（前序 SQL + 结果摘要）进行增量修改
  - few-shot 示例库提升 SQL 质量
  - zod 校验重构

**数据查询策略（Data Query Policy）**

| 策略 | 规则 |
|------|------|
| **默认数据源** | 所有查询必须基于 `gcp-rd-research-dev.finops_cloud_billing` 数据集的 dbt mart 表 |
| **原始表访问** | 仅限 `admin` 角色用户；原始表查询不自动执行，必须 dry-run 后手动确认 |
| **LLM 约束** | system prompt 仅包含 dbt mart 表的 schema 和示例，不暴露原始表结构 |

- **Mart 表（默认）**: `mart_cost_summary`, `mart_cost_monthly_by_service`, `mart_cost_monthly_by_region`, `mart_cost_monthly_by_region_service` — 所有用户可查
- **原始表（受限）**: `gcp-digital-bi.aws_billing_no_payer.*`, `gcp_billing_v2.*` 等 — admin 手动确认后方可执行
- **执行流程差异**:
  - Mart 查询: NL → SQL → dry-run → 自动执行 → 返回结果
  - 原始表查询: NL → SQL → dry-run → **暂停，展示预估扫描量** → admin 手动点击确认 → 执行

**M2: 图表可视化**
- 查询结果自动推荐图表类型（折线/柱状/饼图/堆叠面积/热力图等）
- 使用 Apache ECharts（echarts + echarts-for-react）渲染，替代未使用的 Recharts
- ECharts 优势：图表类型丰富、交互能力强（tooltip/zoom/brush）、大数据量性能好、主题定制灵活
- 结果区域 Tab 切换（Table / Chart）— 在分析模式下默认展示 Chart，数据模式下默认展示 Table

**M3: 用户系统（统一身份）**
- **核心原则**: Google OAuth（Web 登录）和飞书（Bot 交互）必须关联到同一个 User 记录
- 身份关联策略（按优先级）:
  1. **邮箱自动匹配** — 飞书企业邮箱与 Google 邮箱相同时自动合并（最常见场景，公司统一用 @pingcap.com）
  2. **手动绑定** — 邮箱不同时，飞书 Bot 发送绑定链接，用户 OAuth 登录后关联
- 用户首次从任一渠道进入时自动创建 User 记录
- 通知偏好配置（web / 飞书 / 双通道）

**M4: 订阅推送**
- 用户订阅特定账户/服务的定期报告
- 配置频率（daily/weekly/monthly）
- 选择报告模板
- 推送到 Web 通知 + 飞书

**M5: 异常检测** *(TODO: 需要单独出详细设计文档，本文档仅为初步草案)*
- 规则驱动的异常检测引擎
- 支持多种规则类型（环比增幅、绝对阈值、新服务出现等）
- 检测结果生成告警，按 severity 分级
- 告警推送到订阅者

**M6: 飞书集成 + 用户画像**
- 被动模式：飞书 Bot 接收用户提问，调用查询引擎，返回结果
- 主动模式：告警和报告主动推送到飞书群/个人
- 用户映射：飞书 open_id ↔ Platform User
- **用户画像（Profile）** — 类似 OpenClaw 的设计，为每个用户维护一份持续演进的画像：
  - 关注范围：哪些云账户、哪些服务/资源类型、生产 vs 开发测试
  - 关注粒度：整体趋势 vs 细节钻取
  - 沟通偏好：简要数字 vs 详细分析、表格 vs 图表、中文 vs 英文
  - 自动学习：根据历史对话和查询模式持续更新画像
  - 用于个性化：查询默认过滤条件、报告内容裁剪、告警阈值建议、LLM system prompt 注入

---

## 3. 系统架构

### 3.1 整体架构图

```
┌─────────────────────────────────────────────────────────────────────────┐
│                          Clients                                        │
│                                                                         │
│   ┌─────────────┐   ┌─────────────┐   ┌─────────────────────────┐      │
│   │   Web UI     │   │  飞书 Bot    │   │  Cron Worker (内部)      │      │
│   │  (Next.js)   │   │  (Webhook)   │   │  (报告 + 异常检测)       │      │
│   └──────┬───────┘   └──────┬───────┘   └───────────┬─────────────┘      │
│          │                  │                       │                    │
└──────────┼──────────────────┼───────────────────────┼────────────────────┘
           │                  │                       │
           ▼                  ▼                       ▼
┌──────────────────────────────────────────────────────────────────────────┐
│                      API Layer (Next.js API Routes)                      │
│                                                                          │
│  /api/chat           — LLM 编排 + SQL 生成                               │
│  /api/bigquery       — SQL 执行 + dry-run                                │
│  /api/sessions       — 会话 CRUD                                        │
│  /api/users/me       — 用户信息 + 偏好                                   │
│  /api/subscriptions  — 订阅管理                                          │
│  /api/reports        — 报告生成 + 查看                                   │
│  /api/anomalies      — 异常规则 + 告警                                   │
│  /api/notifications  — 通知管理                                          │
│  Feishu WS Client   — WebSocket 长连接接收飞书事件 (非 HTTP)              │
│  /api/cron/*         — 定时任务触发端点                                   │
│                                                                          │
└─────────────┬────────────────────────────────────────────────────────────┘
              │
              ▼
┌──────────────────────────────────────────────────────────────────────────┐
│                      Core Services                                       │
│                                                                          │
│  ┌────────────────┐  ┌────────────────┐  ┌──────────────────────────┐   │
│  │ Query Engine   │  │ Report Engine  │  │ Anomaly Detector          │   │
│  │ (LLM + BQ)    │  │ (模板 + 调度)   │  │ (规则引擎 + 告警生成)     │   │
│  └────────────────┘  └────────────────┘  └──────────────────────────┘   │
│                                                                          │
│  ┌────────────────┐  ┌────────────────┐                                 │
│  │ Chart Renderer │  │ Notification   │                                 │
│  │ (ECharts SSR)  │  │ Hub            │                                 │
│  └────────────────┘  │ (Web + Feishu) │                                 │
│                      └────────────────┘                                 │
└─────────────┬────────────────────────────────────────────────────────────┘
              │
              ▼
┌──────────────────────────────────────────────────────────────────────────┐
│                        Data Layer                                        │
│                                                                          │
│  ┌──────────────┐   ┌──────────────┐   ┌──────────────┐                │
│  │  BigQuery     │   │ PostgreSQL   │   │ Redis (可选)  │                │
│  │  (billing     │   │ (users,      │   │ (rate limit, │                │
│  │   data marts) │   │  sessions,   │   │  job queue)  │                │
│  │              │   │  alerts,     │   │              │                │
│  │              │   │  reports)    │   │              │                │
│  └──────────────┘   └──────────────┘   └──────────────┘                │
│                                                                          │
└──────────────────────────────────────────────────────────────────────────┘
```

### 3.2 数据流

```
用户提问 (Web/飞书)
  → API Route 鉴权
    → Query Engine (LLM → SQL → dry-run → 执行)
      → 根据交互模式分支:
        ┌─ 分析模式 (Analyst):
        │    → LLM 分析查询结果 → 生成自然语言洞察 + 推荐图表
        │    → 返回 (洞察文本 + 图表 + 可折叠的原始数据)
        └─ 数据模式 (Data):
             → 返回 (原始结果表格 + CSV/Excel 导出)
      → 会话持久化 (PostgreSQL)

定时触发 (Cron)
  → Anomaly Detector (读取 BigQuery marts → 对比规则 → 检测异常)
    → 生成 AnomalyAlert
      → Notification Hub (Web 通知 + 飞书推送)

定时触发 (Cron)
  → Report Engine (读取订阅 → 查询 BigQuery → 填充模板 → 生成 HTML)
    → 存储 Report
      → Notification Hub (Web 通知 + 飞书推送)
```

**事件驱动通知模式：**

通知系统采用渐进式事件驱动架构：

- **Phase 2**: 使用进程内 `EventEmitter`（`NotificationHub`），报告生成完成后直接 `emit('report:completed', report)` → 通知创建。简单可靠，适合单进程部署。
- **Phase 3**: 如需跨进程通知（feishu-worker 独立部署时），可将 `EventEmitter` 替换为 Redis Pub/Sub，NotificationHub 接口不变。

```typescript
// data-agent/src/lib/notification-hub.ts
interface NotificationEvent {
  type: 'report:completed' | 'report:failed' | 'anomaly:detected'
  userId: string
  channels: ('web' | 'feishu')[]
  payload: Record<string, unknown>
}
```

---

## 4. 技术栈

### 4.1 现有技术

| 类别 | 技术 | 用途 |
|------|------|------|
| 框架 | Next.js 14 (App Router) | Web 应用框架 |
| UI | React 18 + TailwindCSS 3 | 前端 UI |
| 图标 | Lucide React | 图标库 |
| 表格 | TanStack React Table | 数据表格 |
| 图表 | ~~Recharts 2.13~~ → 移除 | 已安装未使用，替换为 ECharts |
| Markdown | react-markdown + rehype-highlight | 消息渲染 |
| 认证 | NextAuth 4.24 (Google OAuth) | 用户认证 |
| LLM | OpenAI SDK (OpenRouter) + @google/genai | 自然语言→SQL |
| 数据 | @google-cloud/bigquery | SQL 执行 |
| 部署 | Docker + docker-compose | 容器化部署 |

### 4.2 新增技术

| 类别 | 技术 | 用途 | Phase |
|------|------|------|-------|
| 图表 | echarts + echarts-for-react | 图表可视化（替代 Recharts） | 1 |
| ORM | Prisma | 数据库访问层 | 1 |
| 数据库 | PostgreSQL 16 | 用户/会话/告警持久化 | 1 |
| 校验 | zod | API 请求/响应校验 | 1 |
| 导出 | exceljs (MIT) | Excel 导出（MIT 许可证） | 1 |
| 飞书 SDK | @larksuiteoapi/node-sdk | 飞书 API 集成 | 3 |
| 队列 (可选) | BullMQ + Redis | 异步任务队列 | 3 |

### 4.3 技术决策

**为什么选 PostgreSQL？**
- 需要关系型数据存储（用户、订阅、告警有复杂关联）
- Prisma 对 PostgreSQL 支持最成熟
- 开发用 Docker Compose 本地运行，生产用 db9.ai 托管
- 不会替代 BigQuery（BigQuery 仍然是计费数据的唯一数据源）

**为什么选 Prisma？**
- Next.js 生态首选 ORM
- 类型安全（TypeScript 集成）
- Migration 管理
- 声明式 Schema

**为什么不引入独立的后端服务？**
- Next.js API Routes 已足够支撑当前规模
- 减少运维复杂度
- 保持部署简单性（单容器 + 数据库）
- 未来如果需要可拆分为独立服务

**BigQuery 数据源分层**

| 层级 | 数据集 | 用途 | 访问控制 |
|------|--------|------|----------|
| **分析层（dbt marts）** | `gcp-rd-research-dev.finops_cloud_billing.mart_*` | 所有用户 NL→SQL 查询的默认目标 | 所有已认证用户 |
| **原始层（raw billing tables）** | `gcp-digital-bi.aws_billing_no_payer.*`, `gcp_billing_v2.*` 等 | 仅作 admin 高级排查的 fallback 数据源 | 仅 admin 角色，需手动确认执行 |

NL→SQL 查询引擎的 LLM system prompt 默认仅注入 dbt mart 表的 schema 信息，不暴露原始表结构。admin 用户查询时，额外注入原始表 schema 上下文作为 fallback 数据源。

### 4.4 统一 LLM 服务架构

所有 LLM 调用统一通过 `llm-service.ts` 抽象层，避免各模块直接调用 OpenRouter/Gemini：

```typescript
// data-agent/src/lib/llm-service.ts

interface LLMServiceConfig {
  task: 'chat' | 'analyze' | 'summarize' | 'detect'  // 任务类型
  model?: string            // 可选指定模型，默认按 task 自动选择
  maxTokens?: number        // 输出 token 上限
  temperature?: number      // 默认 0.1（SQL 生成需要确定性）
}

// 模型选择策略:
// - chat/analyze: 主力模型（GPT-4o / Gemini Pro）
// - summarize/detect: 经济模型（GPT-4o-mini / Gemini Flash）

// 统一功能:
// - Token 计数与用量记录（按用户、按任务类型）
// - 指数退避重试（429/500 错误，最多 3 次）
// - 结构化错误处理（LLMError 类型）
// - 请求/响应日志（开发环境）
```

集成点:
- Phase 1: `/api/chat` 路由（chat + analyze 任务）
- Phase 2: 报告引擎（summarize 任务）
- Phase 3: 异常检测（detect 任务）+ 画像学习（summarize 任务）

---

## 5. 数据模型设计

### 5.1 ER 关系图

```
User ──── UserProfile (1:1 用户画像)
  │
  ├──< Subscription ──< Report
  │                         │
  ├──< ChatSession          │
  │      └──< Message       │
  │                         │
  ├──< AnomalyRule ──< AnomalyAlert
  │                         │
  └──< Notification ────────┘
```

### 5.2 Prisma Schema

```prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

// ─── User (统一身份) ───────────────────────────────────
// 同一个人通过 Google OAuth 和飞书 Bot 进入时，必须合并为同一条记录。
// 关联策略: email 自动匹配 > 手动绑定链接。
model User {
  id              String    @id @default(cuid())
  email           String    @unique              // 主邮箱 (Google OAuth 邮箱)
  name            String?
  image           String?

  // Google OAuth 身份
  googleId        String?   @unique @map("google_id")

  // 飞书身份
  feishuOpenId    String?   @unique @map("feishu_open_id")   // 应用内唯一 ID
  feishuUnionId   String?   @unique @map("feishu_union_id")  // 跨应用唯一 ID
  feishuEmail     String?   @map("feishu_email")             // 飞书企业邮箱 (用于自动匹配)

  // 通知偏好
  notifyWeb       Boolean   @default(true) @map("notify_web")
  notifyFeishu    Boolean   @default(false) @map("notify_feishu")

  createdAt       DateTime  @default(now()) @map("created_at")
  updatedAt       DateTime  @updatedAt @map("updated_at")

  sessions        ChatSession[]
  subscriptions   Subscription[]
  anomalyRules    AnomalyRule[]
  notifications   Notification[]
  profile         UserProfile?

  @@map("users")
}

// ─── UserProfile (用户画像) ────────────────────────────
// 记录用户的关注范围、沟通偏好和行为模式，用于个性化查询和推送。
// 初始值由用户首次交互时的 onboarding 设置，后续通过对话分析自动演进。
model UserProfile {
  id              String    @id @default(cuid())
  userId          String    @unique @map("user_id")

  // ── 关注范围 ──
  focusAccountIds String[]  @map("focus_account_ids")   // 关注的云账户 ID 列表
  focusServices   String[]  @map("focus_services")       // 关注的服务 (AmazonEC2, AmazonS3...)
  focusRegions    String[]  @map("focus_regions")        // 关注的区域
  focusClouds     String[]  @default(["aws"]) @map("focus_clouds") // 关注的云 (aws/gcp/azure/alibaba)
  focusEnv        String    @default("all") @map("focus_env")
  // 'production' | 'development' | 'all'
  // 对应 account_mapping.account_purpose

  // ── 关注粒度 ──
  analysisDepth   String    @default("summary") @map("analysis_depth")
  // 'summary'  — 关注整体趋势、月度汇总、Top N
  // 'detailed' — 关注细节钻取、按资源/标签拆分、日粒度

  // ── 沟通偏好 ──
  responseStyle   String    @default("balanced") @map("response_style")
  // 'concise'  — 简要数字 + 结论，少解释 (适合管理层)
  // 'balanced' — 关键洞察 + 适度分析 (适合 TL)
  // 'detailed' — 完整分析 + SQL + 建议 (适合 FinOps)
  preferredViz    String    @default("chart") @map("preferred_viz")
  // 'table' | 'chart' | 'both'
  language        String    @default("zh") // 'zh' | 'en'

  // ── 行为学习 (LLM 自动更新) ──
  learnedContext  Json?     @map("learned_context")
  // 从历史对话中提取的结构化画像，示例:
  // {
  //   "frequently_queried": ["EC2 by region", "monthly trend"],
  //   "common_filters": { "account_ids": ["123"], "services": ["AmazonEC2"] },
  //   "last_topics": ["SP coverage", "us-east-1 cost spike"],
  //   "updated_at": "2026-03-10T..."
  // }

  createdAt       DateTime  @default(now()) @map("created_at")
  updatedAt       DateTime  @updatedAt @map("updated_at")

  user            User      @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@map("user_profiles")
}

// ─── ChatSession ───────────────────────────────────────
model ChatSession {
  id        String    @id @default(cuid())
  userId    String    @map("user_id")
  title     String
  pinned    Boolean   @default(false)
  createdAt DateTime  @default(now()) @map("created_at")
  updatedAt DateTime  @updatedAt @map("updated_at")

  user      User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  messages  Message[]

  @@index([userId, updatedAt(sort: Desc)])
  @@map("chat_sessions")
}

// ─── Message ───────────────────────────────────────────
model Message {
  id          String    @id @default(cuid())
  sessionId   String    @map("session_id")
  role        String    // 'user' | 'assistant'
  content     String
  sql         String?
  dryRun      Json?     @map("dry_run")     // DryRunResult
  // 注意: 不存储 queryResult — 查询结果体积大（可达数 MB）且可通过 SQL 重新执行获取。
  // 前端仅在当前会话内内存中持有查询结果，刷新后需重新执行。
  model       String?   // LLM model used
  createdAt   DateTime  @default(now()) @map("created_at")

  session     ChatSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)

  @@index([sessionId, createdAt])
  @@map("messages")
}

// ─── Subscription ──────────────────────────────────────
model Subscription {
  id            String    @id @default(cuid())
  userId        String    @map("user_id")
  name          String
  enabled       Boolean   @default(true)

  // 过滤条件
  accountIds    String[]  @map("account_ids")     // 订阅的账户 ID 列表
  services      String[]                          // 订阅的服务列表
  regions       String[]                          // 订阅的区域列表

  // 调度
  frequency     String    // 'daily' | 'weekly' | 'monthly'
  cronExpr      String?   @map("cron_expr")       // 自定义 cron 表达式
  timezone      String    @default("Asia/Shanghai")

  // 报告模板
  templateId    String    @map("template_id")     // 'cost_summary' | 'service_breakdown' | 'trend_analysis'

  // 通知渠道
  channels      String[]  // ['web', 'feishu']

  createdAt     DateTime  @default(now()) @map("created_at")
  updatedAt     DateTime  @updatedAt @map("updated_at")

  user          User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  reports       Report[]

  @@index([userId])
  @@index([enabled, frequency])
  @@map("subscriptions")
}

// ─── AnomalyRule ───────────────────────────────────────
model AnomalyRule {
  id            String    @id @default(cuid())
  userId        String    @map("user_id")
  name          String
  enabled       Boolean   @default(true)

  // 规则类型
  ruleType      String    @map("rule_type")
  // 'mom_increase'     — 月环比增幅超阈值
  // 'wow_increase'     — 周环比增幅超阈值
  // 'absolute_threshold' — 绝对金额超阈值
  // 'new_service'      — 新服务出现
  // 'new_region'       — 新区域出现
  // 'tag_missing'      — 标签缺失率超阈值

  // 规则参数
  config        Json
  // mom_increase:       { threshold: 0.3, min_amount: 100 }
  // absolute_threshold: { daily_limit: 5000, service?: "AmazonEC2" }
  // new_service:        { account_ids: ["123"] }
  // tag_missing:        { tag_name: "tenant", max_missing_rate: 0.1 }

  // 过滤范围
  accountIds    String[]  @map("account_ids")
  services      String[]
  regions       String[]

  // 通知
  severity      String    @default("warning")    // 'info' | 'warning' | 'critical'
  channels      String[]  // ['web', 'feishu']

  createdAt     DateTime  @default(now()) @map("created_at")
  updatedAt     DateTime  @updatedAt @map("updated_at")

  user          User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  alerts        AnomalyAlert[]

  @@index([enabled])
  @@map("anomaly_rules")
}

// ─── AnomalyAlert ──────────────────────────────────────
model AnomalyAlert {
  id            String    @id @default(cuid())
  ruleId        String    @map("rule_id")
  severity      String    // 'info' | 'warning' | 'critical'
  status        String    @default("open") // 'open' | 'acknowledged' | 'resolved'

  // 异常详情
  title         String
  description   String
  detectedAt    DateTime  @map("detected_at")
  dataSnapshot  Json      @map("data_snapshot")  // 异常数据快照
  // { current_value: 12000, previous_value: 8000, change_pct: 0.5,
  //   account_id: "123", service: "AmazonEC2", period: "2026-03" }

  resolvedAt    DateTime? @map("resolved_at")
  resolvedBy    String?   @map("resolved_by")

  createdAt     DateTime  @default(now()) @map("created_at")

  rule          AnomalyRule @relation(fields: [ruleId], references: [id], onDelete: Cascade)

  @@index([ruleId, status])
  @@index([severity, status])
  @@index([detectedAt(sort: Desc)])
  @@map("anomaly_alerts")
}

// ─── Report ────────────────────────────────────────────
model Report {
  id              String    @id @default(cuid())
  subscriptionId  String    @map("subscription_id")
  status          String    @default("pending")  // 'pending' | 'generating' | 'completed' | 'failed'

  // 报告内容
  templateId      String    @map("template_id")
  parameters      Json      // 生成时使用的参数 (date range, filters, etc.)
  data            Json?     // 查询结果数据
  htmlContent     String?   @map("html_content")  // 渲染后的 HTML
  errorMessage    String?   @map("error_message")

  // 时间范围
  periodStart     DateTime  @map("period_start")
  periodEnd       DateTime  @map("period_end")

  generatedAt     DateTime? @map("generated_at")
  createdAt       DateTime  @default(now()) @map("created_at")

  subscription    Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)

  @@index([subscriptionId, createdAt(sort: Desc)])
  @@index([status])
  @@map("reports")
}

// ─── Notification ──────────────────────────────────────
model Notification {
  id          String    @id @default(cuid())
  userId      String    @map("user_id")
  channel     String    // 'web' | 'feishu'
  type        String    // 'report' | 'anomaly' | 'system'

  title       String
  body        String
  read        Boolean   @default(false)
  sentAt      DateTime? @map("sent_at")

  // 通用多态关联 — 替代 alertId/reportId 多列方案
  sourceType  String?   @map("source_type")  // 'report' | 'alert' | null
  sourceId    String?   @map("source_id")    // 关联记录的 ID

  createdAt   DateTime  @default(now()) @map("created_at")

  user        User      @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([userId, read, createdAt(sort: Desc)])
  @@index([channel, sentAt])
  @@index([sourceType, sourceId])
  @@map("notifications")
}
```

### 5.3 模型说明

| 模型 | 记录量级 | 说明 |
|------|----------|------|
| User | ~50 | 内部 FinOps 用户，初始量少 |
| UserProfile | ~50 | 1:1 对应 User，用户画像 |
| ChatSession | ~1K/月 | 每用户每天 ~1 会话 |
| Message | ~5K/月 | 每会话 ~5 消息 |
| Subscription | ~100 | 用户自定义订阅 |
| AnomalyRule | ~50 | 预配置 + 用户自定义规则 |
| AnomalyAlert | ~100/月 | 取决于规则数和数据波动 |
| Report | ~200/月 | 取决于订阅频率 |
| Notification | ~500/月 | 每个告警/报告按用户和渠道展开 |

### 5.4 分阶段 Schema 迁移计划

每个 Phase 仅创建该阶段需要的模型，避免空表和提前耦合：

| Phase | Migration 名称 | 新增模型 | 说明 |
|-------|---------------|---------|------|
| Phase 1 | `init` | User, ChatSession, Message | 基础用户系统 + 会话持久化 |
| Phase 2 | `add_subscription_report_notification` | Subscription, Report, Notification | 订阅推送 + 报告 + 通知 |
| Phase 3 | `add_anomaly_and_profile` | UserProfile, AnomalyRule, AnomalyAlert | 异常检测 + 用户画像 |
| Phase 4 | `add_rbac_and_dashboard` | UserAccessGrant, Dashboard, DashboardWidget, OptimizationReport | 权限管理 + 自定义仪表板 + AI 优化建议 |

**原则**:
- 每个 Phase 的 migration 独立且可增量执行
- 后续 Phase 的 migration 依赖前序 Phase 已完成
- 生产环境使用 `npx prisma migrate deploy`（不是 `migrate dev`）
- Phase 2 的 Notification 模型使用 `sourceType` + `sourceId` 通用多态模式（见下方说明）

---

## 6. API 设计

### 6.1 现有 API（增强）

| Method | Path | 描述 |
|--------|------|------|
| POST | `/api/chat` | LLM 编排，NL→SQL（新增 `mode` 参数） |
| POST | `/api/bigquery` | SQL dry-run / 执行 |
| GET/POST | `/api/auth/[...nextauth]` | NextAuth 认证 |

**`/api/chat` 请求增强：**

```typescript
// 请求体
{
  messages: ChatMessage[],
  model?: string,
  mode: 'analyst' | 'data',   // 新增：交互模式
  sessionId?: string,          // 新增：会话 ID（用于持续对话上下文）
}

// 响应体 — 分析模式
{
  content: string,      // LLM 洞察文本 (markdown)
  sql?: string,         // 生成的 SQL
  analysis?: string,    // 对查询结果的分析总结
  model: string,
}

// 响应体 — 数据模式
{
  content: string,      // 简要说明（SQL 解释）
  sql?: string,
  model: string,
  // 不做结果分析，前端直接展示 raw data
}
```

**分析模式流程**: NL→SQL→执行→将结果传回 LLM→LLM 输出分析洞察（趋势、异常点、建议）
**数据模式流程**: NL→SQL→执行→直接返回原始数据，前端提供 Table + CSV/Excel 导出

### 6.2 新增 API (Phase 1)

| Method | Path | 描述 |
|--------|------|------|
| GET | `/api/users/me` | 获取当前用户信息 + 偏好 |
| PATCH | `/api/users/me` | 更新用户偏好 |
| GET | `/api/sessions` | 获取用户会话列表（分页） |
| POST | `/api/sessions` | 创建新会话 |
| GET | `/api/sessions/[id]` | 获取会话详情 + 消息 |
| PATCH | `/api/sessions/[id]` | 更新会话标题/pin |
| DELETE | `/api/sessions/[id]` | 删除会话 |
| POST | `/api/sessions/[id]/messages` | 追加消息到会话 |

**API 命名约定：**

| 模式 | 示例 | 用途 |
|------|------|------|
| RESTful 资源 | `GET/POST /api/sessions` | 标准 CRUD 操作 |
| 操作类端点 | `POST /api/reports/generate` | 非 CRUD 的业务操作（动词后缀） |
| Cron 触发 | `POST /api/cron/generate-reports` | 外部调度触发（Bearer token 鉴权） |

### 6.3 新增 API (Phase 2)

| Method | Path | 描述 |
|--------|------|------|
| GET | `/api/subscriptions` | 获取用户订阅列表 |
| POST | `/api/subscriptions` | 创建订阅 |
| PATCH | `/api/subscriptions/[id]` | 更新订阅配置 |
| DELETE | `/api/subscriptions/[id]` | 删除订阅 |
| GET | `/api/reports` | 获取报告列表 |
| GET | `/api/reports/[id]` | 获取报告详情 + HTML |
| POST | `/api/reports/generate` | 手动触发报告生成 |
| GET | `/api/notifications` | 获取通知列表 |
| PATCH | `/api/notifications/[id]` | 标记已读 |
| POST | `/api/notifications/read-all` | 全部标记已读 |

### 6.4 新增 API (Phase 3)

| Method | Path | 描述 |
|--------|------|------|
| GET | `/api/anomalies/rules` | 获取异常规则列表 |
| POST | `/api/anomalies/rules` | 创建异常规则 |
| PATCH | `/api/anomalies/rules/[id]` | 更新规则 |
| DELETE | `/api/anomalies/rules/[id]` | 删除规则 |
| GET | `/api/anomalies/alerts` | 获取告警列表 |
| PATCH | `/api/anomalies/alerts/[id]` | 更新告警状态 |
| POST | `/api/cron/detect-anomalies` | 触发异常检测 |
| POST | `/api/cron/generate-reports` | 触发报告生成 |

> **飞书事件接收**: 不再使用 HTTP Webhook 端点。改为应用内 WebSocket 长连接（`@larksuiteoapi/node-sdk` 的 `WSClient`），通过独立 `feishu-worker` 进程启动（~~instrumentation.ts 已废弃~~）。主动推送消息仍使用飞书 REST API。

### 6.5 认证与授权

所有新 API 均使用 NextAuth session 校验：

```typescript
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'

export async function GET(req: Request) {
  const session = await getServerSession(authOptions)
  if (!session?.user?.email) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 })
  }
  // ... 业务逻辑
}
```

Cron 端点使用 Bearer token 保护：

```typescript
// /api/cron/detect-anomalies
const token = req.headers.get('Authorization')?.replace('Bearer ', '')
if (token !== process.env.CRON_SECRET) {
  return Response.json({ error: 'Forbidden' }, { status: 403 })
}
```

---

## 7. 飞书集成设计

### 7.1 集成模式

采用飞书官方推荐的 **WebSocket 长连接**方式接收事件，替代传统 Webhook 回调。

**WebSocket vs Webhook 对比：**

| | WebSocket (采用) | Webhook (不采用) |
|--|--|--|
| 网络要求 | 无需公网 IP / 域名 | 必须有公网可达的 HTTPS URL |
| 开发调试 | 本地直接连接，无需 ngrok | 需要内网穿透工具 |
| 可靠性 | 飞书服务端保证消息投递 + 自动重连 | 需要自行处理重试、签名校验 |
| 部署 | 应用进程内启动 client 即可 | 需要额外暴露端口、配置反向代理 |
| 官方推荐 | 是 (长连接模式) | 传统方式 |

```
┌─────────────────────────────────────────────────────────────────┐
│                      飞书集成架构 (WebSocket)                     │
│                                                                  │
│  被动模式 (用户 → 平台)                                            │
│  ┌──────────┐    ┌──────────────┐  WebSocket  ┌──────────────┐  │
│  │ 飞书用户  │───→│ 飞书 Server  │ ◄══════════►│ Feishu WS    │  │
│  │ @Bot 提问 │    │              │  长连接推送  │ Client       │  │
│  └──────────┘    └──────────────┘             │ (应用内进程)   │  │
│                                               └──────┬───────┘  │
│                                                      │          │
│                                            Query Engine (画像感知)│
│                                                      │          │
│                                               返回结果卡片        │
│                                                                  │
│  主动模式 (平台 → 用户)                                            │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────┐           │
│  │ Anomaly      │───→│ Notification │───→│ 飞书 API  │           │
│  │ Detector     │    │ Hub          │    │ REST 推送  │           │
│  ├──────────────┤    │              │    └──────────┘           │
│  │ Report       │───→│              │                           │
│  │ Engine       │    │              │                           │
│  └──────────────┘    └──────────────┘                           │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
```

> **注意**: 被动接收事件用 WebSocket 长连接，主动推送消息仍用飞书 REST API。

### 7.2 飞书 Bot 配置

| 配置项 | 值 |
|--------|------|
| App Type | 企业自建应用 |
| 事件订阅方式 | **长连接 (WebSocket)** — 非 Webhook |
| Event Subscription | `im.message.receive_v1` |
| Bot Capability | 群聊 + 单聊 |
| 权限 | `im:message`、`im:message.receive_v1`、`contact:user.email:readonly` |

飞书开放平台配置路径: 应用管理 → 事件订阅 → 选择 "使用长连接接收事件"。

### 7.3 WebSocket Client 启动与事件处理

**SDK**: 使用 `@larksuiteoapi/node-sdk`，内置 WebSocket 长连接支持（`WSClient`）。

```typescript
// data-agent/src/lib/feishu/ws-client.ts
import { WSClient } from '@larksuiteoapi/node-sdk'

let wsClient: WSClient | null = null

export function startFeishuWSClient() {
  if (!process.env.FEISHU_APP_ID || !process.env.FEISHU_APP_SECRET) {
    console.log('[Feishu] WS client disabled: missing credentials')
    return
  }

  wsClient = new WSClient({
    appId: process.env.FEISHU_APP_ID,
    appSecret: process.env.FEISHU_APP_SECRET,
    logLevel: process.env.NODE_ENV === 'production' ? 2 : 4, // INFO / DEBUG
  })

  // 注册消息事件处理器
  wsClient.on('im.message.receive_v1', handleMessageEvent)

  wsClient.start()
  console.log('[Feishu] WebSocket client started')
}

export function stopFeishuWSClient() {
  // 优雅关闭 (进程退出时调用)
  wsClient = null
}
```

**事件处理（画像感知）：**

```typescript
// data-agent/src/lib/feishu/event-handler.ts

async function handleMessageEvent(data: FeishuMessageEvent) {
  const { message, sender } = data.event
  const text = extractText(message)
  if (!text) return

  // 1. 解析用户身份 (统一身份关联)
  const user = await resolveFeishuUser(sender.sender_id.open_id)
  const profile = await getOrCreateProfile(user.id)

  // 2. 用画像增强查询上下文
  const result = await queryEngine.ask(text, user, {
    defaultFilters: {
      accountIds: profile.focusAccountIds,
      services: profile.focusServices,
      env: profile.focusEnv,
    },
    analysisDepth: profile.analysisDepth,
    responseStyle: profile.responseStyle,
    language: profile.language,
  })

  // 3. 按用户偏好格式化并发送结果卡片
  const card = formatResultCard(result, profile)
  await feishuClient.sendInteractiveCard(message.chat_id, card)

  // 4. 异步更新画像
  updateProfileFromInteraction(user.id, text, result).catch(console.error)
}
```

**生命周期管理：**

WebSocket client 作为应用内长驻进程启动，与 Next.js 服务共存：

```typescript
// data-agent/src/lib/feishu/index.ts
// 在 Next.js instrumentation hook 或自定义 server 中启动

// 方式 1: Next.js instrumentation (已废弃 — 与 Next.js 生命周期耦合，HMR/重编译导致 WS 断连)
// data-agent/src/instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    const { startFeishuWSClient } = await import('@/lib/feishu/ws-client')
    startFeishuWSClient()
  }
}

// 方式 2: 独立进程 (唯一方案 — 开发 + 生产环境统一使用)
// data-agent/src/feishu-worker.ts
// 通过 docker-compose 单独运行: node dist/feishu-worker.js
```

| 启动方式 | 状态 | 说明 |
|----------|------|------|
| ~~instrumentation hook~~ | **已废弃** | Next.js HMR / 重编译会杀死 WS 连接，开发体验极差；生产环境进程回收同样导致断连 |
| **独立进程 feishu-worker** | **唯一方案** | 开发 + 生产统一使用，docker-compose 单独服务，独立伸缩、独立重启 |

**docker-compose feishu-worker 服务配置：**

```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
```

### 7.4 消息卡片模板

**查询结果卡片：**
```json
{
  "header": { "title": "💰 成本查询结果" },
  "elements": [
    { "tag": "markdown", "content": "**问题**: 上月 EC2 各区域费用是多少？" },
    { "tag": "markdown", "content": "```sql\nSELECT region, SUM(actual_cost_usd)...\n```" },
    { "tag": "markdown", "content": "| Region | Cost |\n|---|---|\n| us-east-1 | $12,345 |" },
    { "tag": "note", "content": "数据来源: mart_cost_summary | 查询耗时: 2.3s" }
  ]
}
```

**异常告警卡片：**
```json
{
  "header": { "title": "🚨 费用异常告警", "template": "red" },
  "elements": [
    { "tag": "markdown", "content": "**账户**: 123456789 (Dev-Account)\n**服务**: AmazonEC2\n**异常**: 环比增长 **+52%** ($8,000 → $12,160)" },
    { "tag": "action", "actions": [
      { "tag": "button", "text": "查看详情", "url": "https://..." },
      { "tag": "button", "text": "标记已处理", "value": "ack:alert_id" }
    ]}
  ]
}
```

### 7.5 统一身份关联

**核心目标**: 无论用户从 Web（Google OAuth）还是飞书（Bot 消息）进入，都解析到同一个 User 记录。

**关联流程图：**

```
飞书 Bot 收到消息 (sender.open_id)
  │
  ├─ 1. 按 feishuOpenId 查找 → 找到 → 使用该 User ✓
  │
  └─ 2. 未找到 → 调用飞书 API 获取用户企业邮箱
       │
       ├─ 3. 按 email 匹配 → 找到已有 User (Google OAuth 创建的)
       │    → 自动合并: 填充 feishuOpenId + feishuUnionId + feishuEmail
       │    → 设置 notifyFeishu = true
       │    → 使用该 User ✓
       │
       └─ 4. 邮箱也未匹配 → 两种策略:
            │
            ├─ 4a. 自动创建新 User (email = feishuEmail)
            │      适用于: 飞书邮箱在允许域名内
            │
            └─ 4b. 发送绑定卡片 → 用户点击链接 → OAuth 登录 → 关联
                   适用于: 飞书邮箱与 Google 邮箱不同
```

**Web 端 Google OAuth 登录：**

```
Google OAuth 回调 (email, googleId)
  │
  ├─ 1. 按 email 查找 → 找到 → 填充 googleId (如果为空) → 使用该 User ✓
  │     (可能是飞书先创建的记录)
  │
  └─ 2. 未找到 → 创建新 User (email, googleId, name, image)
```

**关键实现：**

```typescript
// data-agent/src/lib/user-identity.ts

async function resolveFeishuUser(openId: string): Promise<User> {
  // 1. 直接匹配 open_id
  let user = await prisma.user.findUnique({ where: { feishuOpenId: openId } })
  if (user) return user

  // 2. 获取飞书用户企业邮箱
  const feishuProfile = await feishuClient.getUser(openId)
  const feishuEmail = feishuProfile.enterprise_email || feishuProfile.email

  // 3. 按邮箱匹配已有 User (Google OAuth 创建的)
  if (feishuEmail) {
    user = await prisma.user.findUnique({ where: { email: feishuEmail } })
    if (user) {
      // 自动合并身份
      return prisma.user.update({
        where: { id: user.id },
        data: {
          feishuOpenId: openId,
          feishuUnionId: feishuProfile.union_id,
          feishuEmail,
          notifyFeishu: true,
        },
      })
    }
  }

  // 4. 全新用户 — 从飞书侧创建
  if (feishuEmail && isAllowedDomain(feishuEmail)) {
    return prisma.user.create({
      data: {
        email: feishuEmail,
        name: feishuProfile.name,
        feishuOpenId: openId,
        feishuUnionId: feishuProfile.union_id,
        feishuEmail,
        notifyFeishu: true,
      },
    })
  }

  // 5. 无法自动关联 — 发送绑定卡片
  throw new UnlinkedUserError(openId)
}
```

**绑定链接流程（邮箱不同时的兜底）：**

| 步骤 | 操作 |
|------|------|
| 1 | Bot 回复绑定卡片，含 `https://<domain>/api/auth/link?feishu_open_id=xxx&token=yyy` |
| 2 | 用户点击 → 跳转 Web → Google OAuth 登录 |
| 3 | OAuth 回调中检测 `feishu_open_id` 参数 → 将其写入当前 User |
| 4 | 绑定完成 → Bot 回复确认消息 |

**安全措施：**
- 绑定 token 一次性使用，15 分钟过期
- 绑定操作记录审计日志
- 同一 feishuOpenId 不可绑定到多个 User

### 7.6 用户画像系统

用户画像（UserProfile）为每个用户维护一份持续演进的偏好档案，影响查询、报告、告警的全链路个性化。

**画像维度：**

| 维度 | 字段 | 影响 |
|------|------|------|
| 关注账户 | `focusAccountIds` | 模糊查询自动补充 WHERE account_id 过滤 |
| 关注服务 | `focusServices` | "费用多少" → 自动限定到关注的服务范围 |
| 关注区域 | `focusRegions` | 区域维度查询的默认范围 |
| 关注云 | `focusClouds` | 多云环境下的默认过滤 |
| 环境偏好 | `focusEnv` | production / development / all，过滤 account_purpose |
| 分析深度 | `analysisDepth` | summary: 月度汇总级 / detailed: 按资源/标签钻取 |
| 回复风格 | `responseStyle` | concise / balanced / detailed，控制 LLM 回复详略 |
| 可视化偏好 | `preferredViz` | table / chart / both，控制默认展示方式 |
| 语言 | `language` | zh / en |

**画像在查询链路中的应用：**

```
用户提问: "上个月费用怎么样"
  │
  ├─ 无画像 → LLM 生成: 全量 mart_cost_summary 汇总 (范围太大)
  │
  └─ 有画像 (focusAccountIds: ["123", "456"], focusEnv: "production",
  │          analysisDepth: "summary", responseStyle: "concise")
       │
       → LLM system prompt 注入:
         "用户主要关注账户 123, 456 的生产环境费用。
          偏好简要汇总，给出关键数字和环比变化即可。"
       → 生成 SQL 自动加上 account_id IN ('123','456')
          AND account_purpose = 'Production'
       → 回复精简: "生产环境 2 月总费用 $45,230，环比 +8%。
          EC2 占 62%，同比持平。S3 占 18%，增长 +15%。"
```

**画像初始化（Onboarding）：**

首次交互时，Bot/Web 引导用户完成快速设置:

```
Bot: 👋 欢迎使用 FinOps Agent！为了更好地为你服务，请快速设置:

1️⃣ 你主要关注哪些账户？
   □ 全部  □ 生产环境  □ 开发测试  □ 自定义选择

2️⃣ 你的角色是？
   □ 管理层 (简要汇总)  □ 工程 TL (关键洞察)  □ FinOps (详细分析)

3️⃣ 你更关注？
   □ 整体趋势  □ 服务级细节  □ 两者都要

> 随时可以通过 "设置" 命令修改偏好。
```

飞书侧使用交互式消息卡片（Button + SelectMenu），Web 侧使用 onboarding wizard 页面。

**画像自动学习：**

```typescript
// data-agent/src/lib/profile-learner.ts

async function updateProfileFromInteraction(
  userId: string,
  question: string,
  result: QueryResult
) {
  const profile = await prisma.userProfile.findUnique({ where: { userId } })
  if (!profile) return

  const learned = (profile.learnedContext as LearnedContext) ?? {
    frequently_queried: [],
    common_filters: {},
    last_topics: [],
  }

  // 1. 提取本次查询涉及的实体
  const entities = extractEntities(question, result.sql)
  // { accounts: ["123"], services: ["AmazonEC2"], regions: ["us-east-1"] }

  // 2. 更新高频查询模式
  learned.frequently_queried = updateFrequencyList(
    learned.frequently_queried,
    entities,
    { maxItems: 20 }
  )

  // 3. 更新最近话题
  learned.last_topics = [
    summarizeTopic(question),
    ...learned.last_topics.slice(0, 9),
  ]

  // 4. 定期（每 20 次交互）用 LLM 重新总结画像
  const interactionCount = (learned.interaction_count ?? 0) + 1
  if (interactionCount % 20 === 0) {
    learned.llm_summary = await generateProfileSummary(userId)
    // "该用户是基础设施 TL，主要关注 AWS 生产账户的 EC2 和 EKS 费用，
    //  偏好按区域维度分析，关注 SP 覆盖率变化。"
  }
  learned.interaction_count = interactionCount
  learned.updated_at = new Date().toISOString()

  await prisma.userProfile.update({
    where: { userId },
    data: { learnedContext: learned },
  })
}
```

**画像注入 LLM System Prompt：**

```typescript
function buildPersonalizedSystemPrompt(
  basePrompt: string,
  profile: UserProfile
): string {
  const sections: string[] = [basePrompt]

  // 关注范围
  if (profile.focusAccountIds.length > 0) {
    sections.push(
      `用户主要关注以下账户: ${profile.focusAccountIds.join(', ')}。` +
      `如果用户没有指定账户范围，默认查询这些账户。`
    )
  }
  if (profile.focusEnv !== 'all') {
    sections.push(
      `用户关注 ${profile.focusEnv} 环境，默认按 account_purpose = '${
        profile.focusEnv === 'production' ? 'Production' : 'Development'
      }' 过滤。`
    )
  }

  // 回复风格
  const styleGuide = {
    concise: '用户偏好简要回复。给出关键数字和结论，不需要详细解释过程。',
    balanced: '用户偏好适度分析。给出关键洞察和简要解释。',
    detailed: '用户偏好详细分析。包括数据解读、趋势分析、优化建议。',
  }
  sections.push(styleGuide[profile.responseStyle])

  // 行为学习
  const learned = profile.learnedContext as LearnedContext | null
  if (learned?.llm_summary) {
    sections.push(`用户背景: ${learned.llm_summary}`)
  }

  return sections.join('\n\n')
}
```

---

## 8. 报告引擎设计

### 8.1 报告模板

| 模板 ID | 名称 | 数据粒度 | 适用频率 |
|---------|------|----------|----------|
| `cost_summary` | 成本总览 | 按月/按账户汇总 | weekly, monthly |
| `service_breakdown` | 服务拆解 | 按服务/区域明细 | weekly, monthly |
| `trend_analysis` | 趋势分析 | 时间序列 + 环比 | monthly |
| `savings_report` | 节省报告 | SP/RI 覆盖率 + 节省金额 | monthly |
| `tag_compliance` | 标签合规 | 标签覆盖率统计 | weekly |

### 8.2 报告生成管道

```
Cron 触发 (/api/cron/generate-reports)
  │
  ├─ 1. 查询到期的 Subscription (frequency + 上次生成时间)
  │
  ├─ 2. 为每个 Subscription 创建 Report (status: 'pending')
  │
  ├─ 3. 执行查询
  │     └─ 根据 templateId 选择预定义 SQL 模板
  │     └─ 填充参数 (accountIds, services, date range)
  │     └─ 执行 BigQuery 查询
  │     └─ 存储结果 data (JSON)
  │
  ├─ 4. 渲染 HTML
  │     └─ 加载 HTML 模板
  │     └─ 注入数据 + 图表（ECharts SVG 服务端渲染，echarts/ssr）
  │     └─ 存储 htmlContent
  │     └─ 更新 status: 'completed'
  │
  └─ 5. 推送通知
        └─ 创建 Notification (channel: 'web')
        └─ 如果 channels 包含 'feishu'，推送飞书消息卡片
```

### 8.3 SQL 模板示例 (cost_summary)

```sql
SELECT
  billing_period_month,
  account_id,
  account_display_name,
  SUM(actual_cost_usd) AS actual_cost,
  SUM(list_cost_usd) AS list_cost,
  SAFE_DIVIDE(
    SUM(list_cost_usd) - SUM(actual_cost_usd),
    SUM(list_cost_usd)
  ) AS savings_rate
FROM `finops_cloud_billing.mart_cost_summary`
WHERE billing_period_month BETWEEN @period_start AND @period_end
  AND (@account_ids IS NULL OR account_id IN UNNEST(@account_ids))
GROUP BY 1, 2, 3
ORDER BY actual_cost DESC
```

### 8.4 调度策略

| 频率 | 触发时间 | 报告期间 |
|------|----------|----------|
| daily | 每天 09:00 CST | 前一天 |
| weekly | 每周一 09:00 CST | 上周一到周日 |
| monthly | 每月 3 日 09:00 CST | 上月整月（等 CUR 数据稳定） |

外部 Cron 调度（docker-compose 或 db9.ai 平台 cron）调用内部 API 端点触发，避免在 Next.js 内维护长驻进程。

---

## 9. 异常检测设计

> **Note**: 本节为初步草案，规则类型、检测逻辑、告警策略等需要单独出详细设计文档，结合实际数据分布和业务场景深入设计。

### 9.1 检测规则类型

| 规则类型 | 描述 | 默认阈值 | 检测频率 |
|----------|------|----------|----------|
| `mom_increase` | 月环比增幅超阈值 | 30% 且 > $100 | daily |
| `wow_increase` | 周环比增幅超阈值 | 50% 且 > $50 | daily |
| `absolute_threshold` | 日费用超绝对阈值 | 自定义 | daily |
| `new_service` | 新服务首次出现 | N/A | daily |
| `new_region` | 新区域首次出现 | N/A | daily |
| `tag_missing` | 标签缺失率超阈值 | 10% | weekly |

### 9.2 检测逻辑 (mom_increase 示例)

```typescript
async function detectMomIncrease(rule: AnomalyRule) {
  const { threshold, min_amount } = rule.config as { threshold: number; min_amount: number }

  const sql = `
    WITH current_month AS (
      SELECT account_id, service, SUM(actual_cost_usd) AS cost
      FROM mart_cost_summary
      WHERE billing_period_month = DATE_TRUNC(CURRENT_DATE(), MONTH)
        AND (ARRAY_LENGTH(@account_ids) = 0 OR account_id IN UNNEST(@account_ids))
      GROUP BY 1, 2
    ),
    previous_month AS (
      SELECT account_id, service, SUM(actual_cost_usd) AS cost
      FROM mart_cost_summary
      WHERE billing_period_month = DATE_SUB(DATE_TRUNC(CURRENT_DATE(), MONTH), INTERVAL 1 MONTH)
        AND (ARRAY_LENGTH(@account_ids) = 0 OR account_id IN UNNEST(@account_ids))
      GROUP BY 1, 2
    )
    SELECT
      c.account_id, c.service,
      c.cost AS current_cost,
      p.cost AS previous_cost,
      SAFE_DIVIDE(c.cost - p.cost, p.cost) AS change_pct
    FROM current_month c
    JOIN previous_month p USING (account_id, service)
    WHERE c.cost > @min_amount
      AND SAFE_DIVIDE(c.cost - p.cost, p.cost) > @threshold
    ORDER BY c.cost - p.cost DESC
  `

  const rows = await bigquery.query(sql, {
    account_ids: rule.accountIds,
    min_amount,
    threshold,
  })

  return rows.map(row => ({
    title: `${row.service} 月环比增长 ${(row.change_pct * 100).toFixed(0)}%`,
    description: `账户 ${row.account_id} 的 ${row.service} 费用从 $${row.previous_cost.toFixed(0)} 增至 $${row.current_cost.toFixed(0)}`,
    severity: row.change_pct > threshold * 2 ? 'critical' : 'warning',
    dataSnapshot: row,
  }))
}
```

### 9.3 告警生命周期

```
检测到异常 → AnomalyAlert (status: open)
  → 推送通知 (web + feishu)
    → 用户查看 → acknowledged
      → 用户处理 → resolved (记录 resolvedBy + resolvedAt)
```

### 9.4 去重策略

同一规则 + 同一 account_id + 同一 service + 同一月份，不重复生成告警。通过查询已有 open/acknowledged alerts 判断。

---

## 10. 部署架构

### 10.1 开发环境 (Docker Compose)

```yaml
# docker-compose.yml 新增内容
services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: finops_agent
      POSTGRES_USER: finops
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-finops_dev}
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data

  data-agent:
    # ... 现有配置 ...
    environment:
      # ... 现有环境变量 ...
      DATABASE_URL: postgresql://finops:${POSTGRES_PASSWORD:-finops_dev}@postgres:5432/finops_agent
      CRON_SECRET: ${CRON_SECRET:-dev_cron_secret}
      FEISHU_APP_ID: ${FEISHU_APP_ID:-}
      FEISHU_APP_SECRET: ${FEISHU_APP_SECRET:-}
    depends_on:
      - postgres

volumes:
  pgdata:
```

### 10.2 生产环境 (db9.ai)

```
┌──────────────────────────────────────────────────┐
│                   db9.ai Platform                 │
│                                                   │
│  ┌──────────────┐    ┌──────────────────────┐    │
│  │ data-agent   │───→│ PostgreSQL (Managed)  │    │
│  │ (container)  │    │ DATABASE_URL=...      │    │
│  └──────┬───────┘    └──────────────────────┘    │
│         │                                         │
│         ├──→ BigQuery (via ADC / SA key)          │
│         ├──→ OpenRouter / Gemini (LLM)            │
│         └──→ 飞书 API (通知推送)                    │
│                                                   │
│  ┌──────────────┐                                 │
│  │ Cron Jobs    │──→ /api/cron/* (Bearer token)   │
│  └──────────────┘                                 │
│                                                   │
└──────────────────────────────────────────────────┘
```

### 10.3 环境变量新增

| 变量 | 必需 | 默认值 | 描述 |
|------|------|--------|------|
| `DATABASE_URL` | Phase 1 | - | PostgreSQL 连接字符串 |
| `CRON_SECRET` | Phase 2 | - | Cron 端点鉴权 token |
| `FEISHU_APP_ID` | Phase 3 | - | 飞书应用 ID (WebSocket + REST API 共用) |
| `FEISHU_APP_SECRET` | Phase 3 | - | 飞书应用 Secret |
| `FEISHU_WS_ENABLED` | Phase 3 | `false` | 是否启动飞书 WebSocket 长连接 |

### 10.4 BigQuery 成本预算策略

防止 LLM 生成的 SQL 或报告查询消耗过多 BigQuery 资源：

| 层级 | 策略 | 说明 |
|------|------|------|
| 查询预检 | `dry-run` 预估扫描量 | 每次查询前强制 dry-run，扫描量 > 10GB 拒绝执行并提示用户缩小范围 |
| 用户日预算 | 每用户每日 50GB 扫描上限 | 记录每次查询扫描量，累计超限后当日禁止新查询 |
| 查询结果缓存 | 相同 SQL 24h 内复用 | BigQuery 自带查询缓存 + 应用层可选 Redis 缓存 |
| Cron 预算 | 单次 cron 最多 500GB | 报告/异常检测 cron 累计扫描量上限 |
| 月度总预算 | 告警阈值 $500/月 | 通过 BigQuery Billing Export 监控，超阈值发送管理员告警 |

实现文件: `data-agent/src/lib/bigquery-budget.ts`

### 10.5 安全设计

| 层级 | 措施 | 说明 |
|------|------|------|
| SQL 注入防护 | 双层机制 | 1) LLM prompt 明确禁止 DML/DDL 2) 正则过滤 INSERT/UPDATE/DELETE/DROP/ALTER |
| API 入口校验 | zod schema | 所有 API 路由使用 zod 校验请求体，拒绝非法输入 |
| 权限隔离 | NextAuth session | 所有数据 API 校验 userId ownership，跨用户访问返回 404 |
| Cron 端点保护 | Bearer token | `/api/cron/*` 使用 HMAC 签名（timestamp + secret），防重放 |
| XSS 防护 | DOMPurify | 报告 HTML 渲染后二次消毒，dangerouslySetInnerHTML 仅用于服务端生成的内容 |
| 密钥管理 | 环境变量 | 所有密钥通过 `.env.local` / docker-compose env 注入，不硬编码 |
| 原始表访问控制 | 角色 + 手动确认 | 原始计费表（`gcp-digital-bi.*`）仅限 admin 角色查询；查询不自动执行，dry-run 展示预估扫描量后需 admin 手动确认 |
| 数据源隔离 | LLM 上下文分离 | 非 admin 用户的 LLM system prompt 不包含原始表 schema，从源头防止生成原始表 SQL |

---

## 11. 分阶段路线图

### Phase 1: 基础设施 + 查询增强

| 模块 | 内容 |
|------|------|
| 基础设施 | PostgreSQL + Prisma 集成、docker-compose 配置 |
| 用户系统 | NextAuth 回调自动创建 User 记录 |
| 会话持久化 | `/api/sessions` CRUD，前端从 localStorage 迁移到 API |
| 图表可视化 | chart-suggester 推荐图表类型，result-chart 渲染，Tab 切换 |
| SQL 质量 | few-shot 示例库、多轮迭代支持 |
| 导出增强 | exceljs 导出、zod 校验重构 |

**验收标准**: 用户可以用自然语言查询 → 看到表格和图表 → 会话跨设备同步 → 导出 Excel

### Phase 2: 订阅推送 + 报告

| 模块 | 内容 |
|------|------|
| 订阅系统 | Subscription CRUD，模板选择，频率配置 |
| 报告引擎 | SQL 模板、BigQuery 查询、HTML 渲染 |
| 通知系统 | Web 通知列表（应用内），通知偏好 |
| Cron 调度 | 外部 cron 触发 /api/cron/* 端点 |

**验收标准**: 用户订阅后 → 按频率自动生成报告 → Web 通知提醒 → 可查看历史报告

### Phase 3: 异常检测 + 飞书集成

| 模块 | 内容 |
|------|------|
| 异常规则 | AnomalyRule CRUD，预置规则模板 |
| 异常检测引擎 | 规则评估、告警生成、去重 |
| 飞书 Bot | WebSocket 长连接（独立 feishu-worker 进程）、被动查询、消息卡片 |
| 飞书推送 | 告警推送、报告推送 |
| 用户映射 | 飞书 open_id 绑定 |

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

### Phase 4: 高级功能

| 模块 | 内容 |
|------|------|
| 权限管理 | 基于 account_mapping.access_level 的数据权限 |
| 多云支持 | GCP / Azure 数据接入（依赖 dbt Phase 3） |
| 高级图表 | 交互式 drill-down、自定义仪表板 |
| AI 增强 | 异常归因分析、优化建议生成 |

---

## Related Documents

- [dbt System Design](../../dbt/design/finops-system-design.md) — dbt 数据层架构和 Schema 设计
- [dbt MVP Phase 1](../../dbt/design/finops-mvp-phase1.md) — dbt 层 AWS MVP 实现详情
- [Phase 1 Tasks](./finops-agent-phase1-tasks.md) — Phase 1 详细任务拆解与验证标准

---

## Appendix A: 可选组件触发条件

| 组件 | 默认状态 | 变为必需的条件 |
|------|----------|---------------|
| Redis | 可选 | 用户数 > 100 或需要跨进程事件总线（Phase 3 feishu-worker） |
| BullMQ | 可选 | 报告生成耗时 > 30s 或需要任务重试和持久化队列 |
| feishu-worker | 可选 (Phase 3) | 启用飞书 Bot 集成时必需 |
| Cron Worker | 可选 (Phase 2) | 启用订阅报告 / 异常检测时必需 |

---

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