# FinOps Agent Platform — Phase 1 任务拆解

Phase 1 目标：**基础设施搭建 + 查询体验增强**。完成后用户可以用自然语言查询成本数据，查看表格和图表结果，会话跨设备同步，并导出 Excel。

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

---

## 依赖关系

```
Step 0: page.tsx 重构（前置准备，建议先行）
  │
Step 1: PostgreSQL + Prisma 基础 + zod
  │
  ├──→ Step 2: 用户系统集成 (依赖 Prisma Client)
  │      │
  │      └──→ Step 3: 会话持久化迁移 (依赖 User 模型)
  │             │
  │             └──→ Step 5B: 持续对话 (依赖 Step 3 会话 DB)
  │
  ├──→ Step 4: 图表可视化 (无数据库依赖，可与 Step 2/3 并行)
  │
  ├──→ Step 5A: 双模式 + few-shot (无数据库依赖，可与 Step 2/3 并行)
  │
  └──→ Step 6: 导出增强 + 校验 (依赖 Step 4 的 Tab 组件)

Step 7: 自动化测试 (在所有 Step 完成后执行)
```

**可并行**: Step 4、Step 5A 不依赖数据库，可与 Step 2/3 并行开发。Step 5B（持续对话）依赖 Step 3 的会话 DB。

---

## Step 0: page.tsx 重构（前置准备）

**目标**: 将现有的 `page.tsx`（当前 ~600 行）拆分为可维护的 Hook + 组件结构，降至 <250 行，为后续 Step 3（会话持久化）和 Step 5（双模式交互）打基础。

### 任务清单

- [ ] **0.1** 提取 `useSessionManager` Hook
  - `data-agent/src/hooks/useSessionManager.ts`
  - 管理会话列表状态：当前会话、会话切换、新建会话、删除会话
  - 当前从 localStorage 读取，Step 3 时切换为 API 调用（接口保持不变）
- [ ] **0.2** 提取 `useChatMessages` Hook
  - `data-agent/src/hooks/useChatMessages.ts`
  - 管理消息列表状态：发送消息、接收流式响应、消息历史
  - 封装与 `/api/chat` 的交互逻辑
- [ ] **0.3** 提取 `useQueryExecution` Hook
  - `data-agent/src/hooks/useQueryExecution.ts`
  - 管理查询执行状态：SQL dry-run、BigQuery 执行、结果存储
  - 封装与 `/api/bigquery` 的交互逻辑
- [ ] **0.4** 重构 `page.tsx`
  - 组合三个 Hook，`page.tsx` 仅包含布局和组件组装
  - 目标 <250 行
  - 确保所有现有功能不受影响（回归测试）

### 涉及文件

| 文件 | 操作 |
|------|------|
| `data-agent/src/hooks/useSessionManager.ts` | 新建 — 会话管理 Hook |
| `data-agent/src/hooks/useChatMessages.ts` | 新建 — 消息管理 Hook |
| `data-agent/src/hooks/useQueryExecution.ts` | 新建 — 查询执行 Hook |
| `data-agent/src/app/page.tsx` | 修改 — 重构为 Hook 组合 |

### 验证标准

- [ ] `page.tsx` 行数 < 250
- [ ] 所有现有功能正常（NL 查询、结果展示、会话切换、历史面板）
- [ ] `make lint` && `make build` 通过

---

## Step 1: PostgreSQL + Prisma 基础

**目标**: 建立数据库基础设施，所有后续 Step 依赖此步。

### 任务清单

- [ ] **1.1** 安装 Prisma 依赖
  - `npm install prisma @prisma/client zod`
  - `npx prisma init`
- [ ] **1.2** 编写 Prisma Schema
  - 定义 Phase 1 需要的 3 个模型: `User`, `ChatSession`, `Message`
  - Phase 2+ 的模型（Subscription, AnomalyRule 等）暂不创建，避免空表
  - **Phase 1 精简 Schema（3 模型）：**
    ```prisma
    model User {
      id           String    @id @default(cuid())
      email        String    @unique
      name         String?
      image        String?
      googleId     String?   @unique @map("google_id")
      feishuOpenId String?   @unique @map("feishu_open_id")
      feishuUnionId String?  @unique @map("feishu_union_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[]
      @@map("users")
    }

    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")
    }

    // 注意: 不存储 queryResult — 体积大（可达数 MB）且可通过 SQL 重新执行获取
    model Message {
      id        String    @id @default(cuid())
      sessionId String    @map("session_id")
      role      String
      content   String
      sql       String?
      dryRun    Json?     @map("dry_run")
      model     String?
      createdAt DateTime  @default(now()) @map("created_at")
      session   ChatSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
      @@index([sessionId, createdAt])
      @@map("messages")
    }
    ```
  - 添加必要的索引（已内含在上方 schema 中）
- [ ] **1.3** 配置 docker-compose 添加 PostgreSQL
  - 添加 `postgres` 服务（PostgreSQL 16 Alpine）
  - 添加 `pgdata` volume
  - `data-agent` 添加 `DATABASE_URL` 环境变量和 `depends_on`
- [ ] **1.4** 创建 Prisma Client 单例
  - `data-agent/src/lib/db.ts` — 遵循 Next.js 推荐的全局单例模式
- [ ] **1.5** 生成初始 Migration
  - `npx prisma migrate dev --name init`
- [ ] **1.6** 添加 Prisma 到 Makefile
  - `make db-migrate` — 运行 migration
  - `make db-studio` — 启动 Prisma Studio
  - `make db-generate` — 重新生成 client
- [ ] **1.7** 修改 Dockerfile
  - 在 `npm run build` 之前添加 `COPY prisma/ ./prisma/`
  - 添加 `RUN npx prisma generate` 步骤
  - 确保 Docker 构建时 Prisma Client 正确生成

### 涉及文件

| 文件 | 操作 |
|------|------|
| `data-agent/package.json` | 添加 prisma, @prisma/client 依赖 |
| `data-agent/prisma/schema.prisma` | 新建 — Schema 定义 |
| `data-agent/prisma/migrations/` | 新建 — Migration 文件 |
| `data-agent/src/lib/db.ts` | 新建 — Prisma Client 单例 |
| `docker-compose.yml` | 修改 — 添加 postgres 服务 |
| `data-agent/Makefile` | 修改 — 添加 db 相关命令 |
| `data-agent/.env.example` | 新建或修改 — 添加 DATABASE_URL |
| `data-agent/Dockerfile` | 修改 — 添加 prisma generate 步骤 |

### 验证标准

- [ ] `docker-compose up -d postgres` 启动成功
- [ ] `npx prisma migrate dev` 执行成功，表已创建
- [ ] `npx prisma studio` 可以打开并看到 3 个空表
- [ ] `make build` 通过（Prisma Client 正确生成）
- [ ] `make lint` 通过

---

## Step 2: 用户系统集成（统一身份）

**目标**: Google OAuth 和飞书两个渠道的用户关联到同一个 User 记录。Phase 1 先完成 Google OAuth → User 创建 + 身份解析基础设施，飞书关联在 Phase 3 接入时生效。

### 任务清单

- [ ] **2.1** 修改 NextAuth 回调，登录时自动创建/更新 User
  - 在 `signIn` 回调中调用 `prisma.user.upsert()`
  - 用 `email` 作为唯一标识（飞书未来也按 email 匹配合并）
  - 从 Google profile 获取 name, image, googleId
  - 如果 User 已存在但 googleId 为空（飞书先创建的记录），填充 googleId
- [ ] **2.2** 在 JWT/session 回调中注入 userId
  - JWT 回调：token.userId = user.id
  - Session 回调：session.user.id = token.userId
- [ ] **2.3** 扩展 NextAuth 类型声明
  - 扩展 `Session` 类型，添加 `user.id: string`
  - 文件: `data-agent/src/types/next-auth.d.ts`
- [ ] **2.4** 实现 `/api/users/me` 端点
  - GET: 返回当前用户信息（含 feishu 绑定状态）
  - PATCH: 更新通知偏好 (notifyWeb, notifyFeishu)
- [ ] **2.5** 实现统一身份解析模块
  - `data-agent/src/lib/user-identity.ts`
  - `resolveGoogleUser(email, googleId, profile)` — Google OAuth 路径
  - `resolveFeishuUser(openId)` — 飞书路径（Phase 3 使用，Phase 1 先写接口占位）
  - 核心逻辑: 按 email 匹配已有 User → 合并身份字段 → 或创建新 User
- [ ] **2.6** 创建 `getCurrentUser` 辅助函数
  - `data-agent/src/lib/user.ts`
  - 封装 getServerSession + prisma.user.findUnique
- [ ] **2.7** 实现 Next.js middleware 集中 API 认证
  - `data-agent/src/middleware.ts`
  - 匹配 `/api/sessions`, `/api/users/me`, `/api/subscriptions`, `/api/reports`, `/api/notifications`, `/api/anomalies` 路径
  - 校验 NextAuth session，未登录返回 401
  - `/api/cron/*` 路径使用 Bearer token 校验
  - `/api/auth/*` 和 `/api/chat`, `/api/bigquery` 路径跳过（已有各自的认证逻辑）
- [ ] **2.8** 实现绑定链接 API（Phase 3 使用，Phase 1 可跳过）
  - `GET /api/auth/link?feishu_open_id=xxx&token=yyy`
  - OAuth 登录后将 feishuOpenId 写入当前 User
  - token 一次性使用，15 分钟过期

### 涉及文件

| 文件 | 操作 |
|------|------|
| `data-agent/src/lib/auth.ts` | 修改 — signIn/jwt/session 回调 + 身份合并 |
| `data-agent/src/lib/user-identity.ts` | 新建 — 统一身份解析 (Google + 飞书) |
| `data-agent/src/lib/user.ts` | 新建 — getCurrentUser 辅助函数 |
| `data-agent/src/types/next-auth.d.ts` | 新建 — NextAuth 类型扩展 |
| `data-agent/src/app/api/users/me/route.ts` | 新建 — 用户信息 API |
| `data-agent/src/app/api/auth/link/route.ts` | 新建 — 飞书绑定链接 (Phase 3) |
| `data-agent/src/middleware.ts` | 新建 — API 路由集中认证 |

### 验证标准

- [ ] Google OAuth 登录 → `users` 表自动创建记录（email + googleId）
- [ ] 二次登录不会创建重复记录（upsert by email）
- [ ] 如果 User 已存在（email 匹配）但 googleId 为空 → 自动填充 googleId
- [ ] `GET /api/users/me` 返回当前用户 JSON（含 feishuOpenId 绑定状态）
- [ ] `PATCH /api/users/me` 可以更新 notifyWeb/notifyFeishu
- [ ] 未登录状态访问 `/api/users/me` 返回 401
- [ ] `user-identity.ts` 的 `resolveFeishuUser` 接口已定义（Phase 3 实现）
- [ ] `make lint` 通过

---

## Step 3: 会话持久化迁移

**目标**: 将聊天会话从 localStorage 迁移到服务端 PostgreSQL，支持跨设备访问。

### 任务清单

- [ ] **3.1** 实现 `/api/sessions` CRUD API
  - `GET /api/sessions` — 分页查询用户会话列表（按 updatedAt 降序）
  - `POST /api/sessions` — 创建新会话
  - `GET /api/sessions/[id]` — 获取会话详情 + 所有消息
  - `PATCH /api/sessions/[id]` — 更新标题 / pin 状态
  - `DELETE /api/sessions/[id]` — 删除会话（级联删除消息）
- [ ] **3.2** 实现 `/api/sessions/[id]/messages` API
  - `POST` — 追加消息（role, content, sql?, dryRun?）
  - 返回创建的消息（含 id 和 createdAt）
- [ ] **3.3** 创建客户端 session API 封装
  - `data-agent/src/lib/session-api.ts`
  - 封装 fetch 调用，返回类型化的 ChatSession/Message
  - 提供与现有 localStorage 接口兼容的 API
- [ ] **3.4** 修改 `page.tsx` 使用服务端会话
  - 替换 localStorage 读写为 API 调用
  - 直接替换 localStorage 为 API 调用（不保留 fallback，登录用户使用服务端存储）
  - 新消息发送后立即通过 API 持久化
- [ ] **3.5** 修改 `history-panel.tsx` 使用服务端会话
  - 会话列表从 API 加载
  - 删除/pin 操作调用 API
- [ ] **3.6** 修改 `/api/chat` 路由
  - 在 LLM 返回后，自动将 assistant 消息持久化
  - 如果 sessionId 不存在，自动创建会话
- [ ] **3.7** 一次性数据迁移
  - 前端检测到 localStorage 有旧数据时，显示 "Import History" 按钮
  - 点击后批量上传 localStorage 数据到服务端
  - 导入完成后清理 localStorage
  - 仅在首次登录时检测，不保留 localStorage 作为持续的 fallback

### 涉及文件

| 文件 | 操作 |
|------|------|
| `data-agent/src/app/api/sessions/route.ts` | 新建 — 会话列表 + 创建 |
| `data-agent/src/app/api/sessions/[id]/route.ts` | 新建 — 会话详情/更新/删除 |
| `data-agent/src/app/api/sessions/[id]/messages/route.ts` | 新建 — 消息追加 |
| `data-agent/src/lib/session-api.ts` | 新建 — 客户端 API 封装 |
| `data-agent/src/lib/chat-session.ts` | 修改 — 保留类型定义，移除 localStorage 逻辑 |
| `data-agent/src/app/page.tsx` | 修改 — localStorage → API 调用 |
| `data-agent/src/components/chat/history-panel.tsx` | 修改 — 使用 API 加载会话 |
| `data-agent/src/app/api/chat/route.ts` | 修改 — 添加消息持久化 |

### 验证标准

- [ ] 创建新会话 → 刷新页面 → 会话仍在
- [ ] 发送消息 → 切换设备 → 消息可见
- [ ] 会话列表支持分页加载
- [ ] 删除会话 → 关联消息一起删除
- [ ] Pin 会话 → 排列到顶部
- [ ] localStorage 旧数据可通过 "Import History" 按钮一次性导入到服务端
- [ ] `make lint` && `make build` 通过

---

## Step 4: 图表可视化 (Apache ECharts)

**目标**: 使用 Apache ECharts 实现查询结果的自动图表推荐，用户可在 Table/Chart 间切换。

### 任务清单

- [ ] **4.1** 安装 ECharts 依赖并移除 Recharts
  - `npm install echarts echarts-for-react`
  - `npm uninstall recharts` （已安装但未使用）
  - echarts-for-react 提供 React 组件封装，简化集成
  - **Tree-shaking 配置**: 使用 `echarts/core` 按需导入，仅引入 LineChart, BarChart, PieChart 和必要组件
    ```typescript
    // data-agent/src/lib/echarts-setup.ts
    import * as echarts from 'echarts/core'
    import { LineChart, BarChart, PieChart } from 'echarts/charts'
    import { TitleComponent, TooltipComponent, LegendComponent, DataZoomComponent, GridComponent } from 'echarts/components'
    import { CanvasRenderer } from 'echarts/renderers'

    echarts.use([LineChart, BarChart, PieChart, TitleComponent, TooltipComponent, LegendComponent, DataZoomComponent, GridComponent, CanvasRenderer])
    export default echarts
    ```
  - Tree-shaking 后预计 ~300KB（gzipped ~90KB），远小于全量 ~800KB
- [ ] **4.2** 实现 chart-suggester 逻辑
  - `data-agent/src/lib/chart-suggester.ts`
  - 输入: QueryResult（列名 + 数据行）
  - 输出: ECharts option 对象 + chartType 元信息
  - 推荐规则:
    - 有时间列 + 数值列 → 折线图 (line)
    - 有分类列 + 数值列 → 柱状图 (bar)
    - 只有 1 个分类列 + 1 个数值列且行数 ≤ 10 → 饼图 (pie)
    - 有时间列 + 分类列 + 数值列 → 堆叠面积图 (line + areaStyle)
    - 无法推荐 → 返回 null（仅展示表格）
  - 列类型推断增强 — 通过列名关键词匹配辅助类型判断:
    - 时间列: 列名包含 `date`, `month`, `period`, `time`, `year`, `day`
    - 数值列: 列名包含 `cost`, `amount`, `rate`, `count`, `total`, `sum`, `avg`, `savings`
    - 分类列: 列名包含 `name`, `id`, `type`, `region`, `service`, `account`, `provider`
  - 自动配置 ECharts 交互特性: tooltip、legend、dataZoom（数据量 > 20 时）
- [ ] **4.3** 定义 ECharts 主题
  - `data-agent/src/lib/echarts-theme.ts`
  - 与现有 warm 色系一致的自定义主题
  - 通过 `echarts.registerTheme()` 注册
  - 支持 light/dark 模式
- [ ] **4.4** 实现 result-chart 组件
  - `data-agent/src/components/result-chart.tsx`
  - 使用 `echarts-for-react` 的 `ReactECharts` 组件
  - 接收 chart-suggester 输出的 ECharts option
  - `next/dynamic` 懒加载（ECharts 体积较大 ~800KB，需避免首屏加载）
  - 自适应容器宽度，监听 resize
- [ ] **4.5** 实现 result-view Tab 组件
  - `data-agent/src/components/result-view.tsx`
  - Tab 切换: Table | Chart
  - Table tab: 复用现有 `result-table.tsx`
  - Chart tab: 使用 `result-chart.tsx`
  - 如果 chart-suggester 返回 null，隐藏 Chart tab
- [ ] **4.6** 集成到 message-bubble-content
  - 替换现有的直接 `<ResultTable>` 引用为 `<ResultView>`
  - 保持现有的 auto-collapse SQL 行为

### 涉及文件

| 文件 | 操作 |
|------|------|
| `data-agent/package.json` | 修改 — 添加 echarts + echarts-for-react，移除 recharts |
| `data-agent/src/lib/chart-suggester.ts` | 新建 — 图表推荐 + ECharts option 生成 |
| `data-agent/src/lib/echarts-theme.ts` | 新建 — 自定义主题 |
| `data-agent/src/components/result-chart.tsx` | 新建 — ECharts 图表组件 |
| `data-agent/src/components/result-view.tsx` | 新建 — Tab 切换容器 |
| `data-agent/src/components/chat/message-bubble-content.tsx` | 修改 — 使用 ResultView |

### 验证标准

- [ ] 时间序列查询结果 → 自动展示折线图（带 tooltip + dataZoom）
- [ ] 按服务分组的查询 → 自动展示柱状图
- [ ] 少分类单数值查询 → 展示饼图（带 legend）
- [ ] Tab 切换 Table ↔ Chart 流畅
- [ ] 无法推荐图表时仅显示 Table tab
- [ ] 图表 responsive，窗口缩放正常
- [ ] ECharts 交互功能正常（tooltip 悬浮、legend 点击过滤、dataZoom 缩放）
- [ ] 颜色方案与现有 UI 一致
- [ ] ECharts 懒加载生效（首屏不加载图表 JS）
- [ ] `make lint` && `make build` 通过

---

## Step 5A: 双模式交互 + few-shot（独立，无数据库依赖）

**目标**: 实现分析模式/数据模式双模式切换，通过 few-shot 示例提升 SQL 质量。不依赖数据库，可与 Step 2/3 并行。

### 任务清单

- [ ] **5A.1** 定义交互模式类型与 UI 切换
  - `data-agent/src/lib/chat-mode.ts` — 定义 `ChatMode = 'analyst' | 'data'`
  - 前端添加模式切换控件（输入框旁的 toggle 或 dropdown）
  - 模式偏好跟随用户设置（存入 User.preferences / localStorage）
  - 会话级别可覆盖全局默认
- [ ] **5A.2** 实现分析模式（Analyst）— 服务端自包含管道
  - 修改 `/api/chat/route.ts`，接受 `mode` 参数
  - **关键设计**: 分析模式为服务端自包含管道，前端只需一次 API 调用：
    1. `/api/chat` 接收用户问题 + `mode: 'analyst'`
    2. 服务端内部: NL→SQL→调用 BigQuery 执行→将结果传回 LLM 做二次分析
    3. 返回完整结果: `{ sql, queryResult, analysis }`
    4. 前端无需额外调用 `/api/bigquery`
  - 二次分析 prompt: "根据以下查询结果，总结关键发现、趋势和建议"
  - 返回 `analysis` 字段（markdown 格式的洞察文本）
  - 前端展示: 洞察文本 + 图表（默认展开）+ 原始数据（可折叠）
- [ ] **5A.3** 实现数据模式（Data）
  - 数据模式流程: NL→SQL→执行→直接返回原始数据
  - 不调用二次 LLM 分析（省 token、低延迟）
  - 前端展示: 原始数据表格（默认展开）+ 导出按钮突出
  - SQL 说明仅保留简短解释，不做数据分析
- [ ] **5A.4** 构建 few-shot 示例库
  - `data-agent/src/lib/few-shot-examples.ts`
  - 10-15 个高频查询示例，覆盖:
    - 月度成本汇总（按账户/服务/区域）
    - 环比变化（MoM）
    - Top N 排名
    - SP/RI 节省率
    - 标签维度分析
  - 每个示例包含: `{ question, sql, explanation }`
  - 基于用户问题关键词匹配 2-3 个相关示例注入 system prompt
  - 示例格式:
    ```typescript
    {
      question: "上个月各服务的费用是多少",
      sql: "SELECT service, SUM(actual_cost_usd) AS cost FROM mart_cost_summary WHERE billing_period_month = DATE_SUB(CURRENT_DATE(), INTERVAL 1 MONTH) GROUP BY 1 ORDER BY 2 DESC",
      explanation: "按服务维度汇总上月费用，降序排列"
    }
    ```
- [ ] **5A.5** 更新系统提示词，切换默认数据源为 dbt marts
  - 修改 `SYSTEM_PROMPT_TEMPLATE`，将 `gcp-digital-bi` 原始数据集引用替换为 `gcp-rd-research-dev.finops_cloud_billing` dbt marts
  - 告知 LLM 可用的 mart 表及其粒度
  - **完全移除原始表引用** — 非 admin 用户的 LLM 上下文中不应包含原始表信息
  - admin 用户查询时，额外注入原始表 schema 上下文（作为 fallback 数据源）
- [ ] **5A.6** 创建 dbt mart schema 参考文档
  - 在 `docs/` 目录创建 `dbt-marts-schema.md`
  - 包含每个 mart 表的字段定义、粒度、分区键、可用维度和度量
  - 格式对标现有 `aws-billing-guide.md`，便于 `docs-loader.ts` 加载
- [ ] **5A.7** 更新 docs-loader 加载策略
  - 修改 `docs-loader.ts`，默认加载 dbt mart schema 文档
  - 原始 CUR 字段文档仅在 admin 用户查询时加载
  - 更新 `multi-cloud-billing-core.md` 中的 dataset/table references 指向 dbt marts
- [ ] **5A.8** 实现原始表查询的 admin 限制与手动确认机制
  - 修改 `bigquery-request.ts` 中的 `detectLocationsFromSql()` 或新增 `detectDatasetTier(sql)` 函数
    - 检测 SQL 引用的数据集是否为原始表（`gcp-digital-bi.*`）或 dbt marts（`finops_cloud_billing.*`）
  - 修改 `/api/bigquery/route.ts`：
    - 如果 SQL 引用原始表 + 用户非 admin → 返回 403 + 错误消息 "原始表仅限 admin 用户查询，请使用 dbt mart 表"
    - 如果 SQL 引用原始表 + 用户是 admin → 仅执行 dry-run，返回预估扫描量 + `requiresConfirmation: true`
    - 前端收到 `requiresConfirmation: true` 后展示确认对话框（预估扫描量 + 确认/取消按钮）
    - admin 确认后，前端发送第二次请求（带 `confirmed: true` 参数）执行实际查询
  - 涉及前端修改：在查询结果区域增加确认交互组件
- [ ] **5A.9** 增强 SQL 提取逻辑
  - 改进 `extractSQL` 正则，处理更多 markdown 格式变体
  - 添加对 `WITH ... AS` CTE 的识别
  - 处理 LLM 返回多个 SQL block 的情况（取最后一个）

### 涉及文件

| 文件 | 操作 |
|------|------|
| `data-agent/src/lib/chat-mode.ts` | 新建 — 模式定义 + 偏好管理 |
| `data-agent/src/lib/few-shot-examples.ts` | 新建 — 示例库 |
| `data-agent/src/app/api/chat/route.ts` | 修改 — 双模式分支 + few-shot 注入 |
| `data-agent/src/lib/chat-request.ts` | 修改 — 增强 SQL 提取 |
| `data-agent/src/app/page.tsx` | 修改 — 模式切换 UI |
| `data-agent/src/components/chat/message-bubble-content.tsx` | 修改 — 按模式展示不同内容布局 |
| `data-agent/src/lib/bigquery-request.ts` | 修改 — 新增 `detectDatasetTier()` 数据源分层检测 |
| `data-agent/src/app/api/bigquery/route.ts` | 修改 — 新增原始表权限检查和手动确认机制 |
| `data-agent/src/lib/docs-loader.ts` | 修改 — 按角色加载不同 schema 文档 |
| `docs/dbt-marts-schema.md` | 新建 — dbt mart 表 schema 参考文档 |

### 验证标准

- [ ] 分析模式: 前端一次 API 调用 → 返回 `{ sql, queryResult, analysis }` → 展示洞察 + 图表 + 可折叠原始数据
- [ ] 数据模式: 提问 → 返回原始数据表格 + 导出按钮
- [ ] 切换模式 → 下次提问立即生效
- [ ] 提问 "上月各服务的费用" → SQL 直接可用（few-shot 命中）
- [ ] 分析模式延迟合理（< 15s，含二次 LLM 调用）
- [ ] 数据模式延迟更低（< 8s，无二次分析）
- [ ] 非 admin 用户提问导致 LLM 生成引用原始表的 SQL → API 返回 403
- [ ] admin 用户查询原始表 → dry-run 结果 + 确认提示 → 确认后执行
- [ ] 所有 few-shot 示例引用 dbt mart 表（`finops_cloud_billing.mart_*`）
- [ ] docs-loader 非 admin 模式不加载原始表 schema
- [ ] `make lint` 通过

---

## Step 5B: 持续对话上下文（依赖 Step 3）

**目标**: 实现同一会话内的持续对话，LLM 基于前序消息上下文进行增量修改。依赖 Step 3 的会话数据库。

### 任务清单

- [ ] **5B.1** 持续对话上下文管理
  - 修改 `/api/chat/route.ts`，支持 `sessionId` 参数
  - 从数据库加载同一会话的前序消息作为上下文
  - 将上一轮的 SQL + 结果摘要（前 5 行 + 列名 + 行数）注入对话历史
  - 用户追问 "按区域拆分" → LLM 基于前序 SQL 增量修改
  - 上下文窗口控制: 最多携带最近 5 轮对话，超出时只保留最近的 SQL 和摘要

### 涉及文件

| 文件 | 操作 |
|------|------|
| `data-agent/src/app/api/chat/route.ts` | 修改 — sessionId 上下文加载 |
| `data-agent/src/app/page.tsx` | 修改 — 传递 sessionId 到 API |

### 验证标准

- [ ] 同一会话追问 "按区域拆分" → LLM 基于前序 SQL 增量修改
- [ ] 追问 "去掉 global 区域" → SQL 正确添加 WHERE 条件
- [ ] 上下文窗口: 6 轮对话后，第 1 轮的详细上下文被截断
- [ ] `make lint` 通过

---

## Step 6: 导出增强 + 校验

**目标**: 支持 Excel 导出，用 zod 重构 API 校验。

### 任务清单

- [ ] **6.1** 添加 Excel 导出功能
  - `npm install exceljs`（MIT 许可证，替代 SheetJS 的许可证限制）
  - `data-agent/src/lib/export.ts` — 封装 exceljs 导出逻辑
  - 接收 QueryResult，生成 .xlsx 文件
  - 自动设置列宽、数字格式
  - 文件名格式: `finops-query-{timestamp}.xlsx`
- [ ] **6.2** 添加导出按钮到 result-view
  - 在 Table/Chart tab 旁添加 "Export" 下拉
  - 选项: CSV（现有？）/ Excel (.xlsx)
  - 导出当前查询结果
  - 导出操作集中在 `result-view.tsx`，不在 `result-table.tsx` 中单独提供导出功能
- [ ] **6.3** 用 zod 重构 API 请求校验
  - `data-agent/src/lib/validators.ts` — 定义 zod schema（zod 已在 Step 1.1 安装，此处仅 retrofit 现有 API）
  - 重构 `/api/chat` 请求校验（messages array, model string）
  - 重构 `/api/bigquery` 请求校验（sql string, mode enum）
  - 重构 `/api/sessions` 请求校验
  - 重构 `/api/users/me` 请求校验
- [ ] **6.4** 统一 API 错误响应格式
  - 定义标准错误格式: `{ error: string, details?: unknown }`
  - zod 校验失败返回 400 + 具体字段错误
  - 认证失败返回 401
  - 权限不足返回 403

### 涉及文件

| 文件 | 操作 |
|------|------|
| `data-agent/package.json` | 修改 — 添加 exceljs 依赖 |
| `data-agent/src/lib/export.ts` | 新建 — Excel 导出逻辑 |
| `data-agent/src/lib/validators.ts` | 新建 — zod schema 定义 |
| `data-agent/src/components/result-view.tsx` | 修改 — 添加导出按钮 |
| `data-agent/src/app/api/chat/route.ts` | 修改 — 使用 zod 校验 |
| `data-agent/src/app/api/bigquery/route.ts` | 修改 — 使用 zod 校验 |
| `data-agent/src/app/api/sessions/route.ts` | 修改 — 使用 zod 校验 |
| `data-agent/src/app/api/users/me/route.ts` | 修改 — 使用 zod 校验 |

### 验证标准

- [ ] 查询结果可导出为 .xlsx 文件
- [ ] Excel 文件在 Numbers / Excel / Google Sheets 中打开正常
- [ ] 数值列保留数字格式（非文本）
- [ ] 传入非法 JSON 到 `/api/chat` → 400 + 具体字段错误
- [ ] 传入 empty sql 到 `/api/bigquery` → 400 + 明确错误消息
- [ ] `make lint` && `make build` 通过

---

## Step 7: 自动化测试

**目标**: 为核心模块添加单元测试，建立测试基线。

### 任务清单

- [ ] **7.1** 配置 vitest
  - `npm install -D vitest @testing-library/react`
  - 添加 `vitest.config.ts`
  - `package.json` 添加 `"test": "vitest"` script
  - `Makefile` 添加 `make test`
- [ ] **7.2** chart-suggester 测试
  - 测试各种数据模式的图表推荐逻辑
  - 时间列 + 数值列 → 折线图
  - 分类列 + 数值列 → 柱状图
  - 无法推荐 → 返回 null
- [ ] **7.3** user-identity 测试
  - 测试 `resolveGoogleUser` 的 upsert 逻辑
  - 新用户创建、已有用户更新、googleId 填充
- [ ] **7.4** validators 测试
  - 测试 zod schema 的校验边界
  - 合法输入通过、非法输入返回具体错误

### 验证标准

- [ ] `make test` 通过
- [ ] 测试覆盖 chart-suggester、user-identity、validators
- [ ] CI 可集成 `vitest run`

---

## 风险与注意事项

### 技术风险

| 风险 | 影响 | 缓解措施 |
|------|------|----------|
| Prisma 首次引入增加构建复杂度 | 构建时间增加，Docker 镜像变大 | 在 Dockerfile 中优化 prisma generate 步骤 |
| 会话迁移中数据丢失 | 用户历史数据无法恢复 | 提供一次性 "Import History" 按钮迁移数据 |
| chart-suggester 推荐不准确 | 用户看到无意义的图表 | 保守策略：不确定时返回 null，仅显示表格 |
| few-shot 示例增加 context 长度 | LLM 成本增加 | 控制在 2-3 个示例，总量 < 2000 tokens |

### 开发注意事项

1. **Prisma Client 生成**: Dockerfile 中需要在 `npm run build` 之前执行 `npx prisma generate`
2. **Migration in prod**: 生产部署前需要运行 `npx prisma migrate deploy`（不是 `migrate dev`）
3. **会话迁移是一次性的**: 提供 "Import History" 按钮，导入完成后清理 localStorage
4. **图表组件用 dynamic import**: ECharts tree-shaking 后 ~300KB，使用 `next/dynamic` 避免首屏加载
5. **zod 已在 Step 1 安装**: Step 1.1 中已包含 `npm install zod`，后续 Step 无需重复安装

### 完成后检查

- [ ] `make lint` 通过
- [ ] `make build` 通过
- [ ] `docker-compose up -d --build` 完整启动成功
- [ ] 所有现有功能不受影响（回归测试）
- [ ] 新增 API 路由全部需要认证
- [ ] 无敏感信息硬编码

**性能基线**:
- [ ] Chart 渲染 < 2s（ECharts tree-shaking 后 ~300KB）
- [ ] Analyst 模式端到端 < 15s（含二次 LLM 调用）
- [ ] Data 模式端到端 < 8s（无二次分析）
- [ ] 会话列表加载 < 500ms

**安全检查**:
- [ ] 报告 HTML 无 XSS 风险（服务端模板生成，非用户输入）
- [ ] SQL 注入防护: LLM 生成的 SQL 经过 dry-run 校验
- [ ] API 权限: 跨用户访问 session/message 返回 404（非 403，不泄漏信息）
- [ ] 所有新 API 路由经过 middleware 认证校验

---

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