# FinOps Agent Platform — Phase 4 任务拆解

Phase 4 目标：**高级功能 — 权限管理 + 多云支持 + 交互式图表 + AI 增强**。完成后平台支持基于 `access_level` 的数据行级权限控制，接入 GCP / Azure 计费数据实现多云统一分析，提供交互式 drill-down 图表与自定义仪表板，并通过 LLM 实现异常归因分析和成本优化建议。

**前置文档**: [FinOps Agent 平台设计](./finops-agent-platform-design.md) | [Phase 1 任务拆解](./finops-agent-phase1-tasks.md)

**前置依赖**: Phase 1 (基础设施 + 查询增强)、Phase 2 (订阅推送)、Phase 3 (异常检测 + 飞书集成 + 用户画像) 均已完成。

---

## 依赖关系

```
Step 1: 数据权限管理 (RBAC)
  │
  ├──→ Step 2: GCP 计费接入 (dbt + data-agent schema 感知)
  │      │
  │      └──→ Step 3: Azure 计费接入 (复用 Step 2 的多云框架)
  │
  ├──→ Step 4: 交互式下钻图表 (依赖 Step 1 权限过滤，可与 Step 2/3 并行)
  │      │
  │      └──→ Step 5: 自定义仪表板 (依赖 Step 4 的图表组件)
  │
  └──→ Step 6: AI 异常归因分析 (依赖 Step 1 权限 + Phase 3 异常检测)
         │
         └──→ Step 7: AI 成本优化建议 (依赖 Step 6 的 LLM 分析框架)
```

**可并行**:
- Step 2/3 (多云接入) 和 Step 4 (交互式图表) 可并行开发，无相互依赖
- Step 6 (AI 归因) 和 Step 4/5 (图表) 可并行，但 Step 6 需等 Step 1 完成
- Step 2 和 Step 3 可以并行，但推荐串行（Step 3 复用 Step 2 建立的多云框架模式）

---

## Step 1: 数据权限管理 (Data Access Control / RBAC)

**目标**: 基于 `account_mapping.access_level` 实现数据行级权限控制，确保不同角色的用户只能查看授权范围内的账户数据。

### 背景

`account_mapping.csv` 已有 `access_level` 字段，目前取值全部为 `public`。Phase 4 将引入更细粒度的权限级别:

| access_level | 含义 | 可见用户 |
|---|---|---|
| `public` | 公开账户 | 所有登录用户 |
| `team` | 团队可见 | 账户 owner 及其团队成员 |
| `restricted` | 受限账户 | 仅 FinOps admin 和明确授权的用户 |

### 任务清单

- [ ] **1.1** 扩展用户模型，添加权限字段
  - Prisma Schema 新增 `role` 字段到 `User` 模型: `'viewer' | 'analyst' | 'admin'`
  - `admin` 角色可查看所有账户（包括 restricted）
  - `analyst` 角色可查看 public + 所属团队 team 账户
  - `viewer` 角色仅可查看 public 账户
  - 创建对应的 Prisma migration
- [ ] **1.2** 创建用户-账户授权映射表
  - Prisma Schema 新增 `UserAccountAccess` 模型
  - 字段: `userId`, `accountId`, `cloudProvider`, `grantedBy`, `grantedAt`
  - 用于显式授权特定用户访问 `team` / `restricted` 账户
  - 管理员可通过 API 管理授权
- [ ] **1.3** 实现权限解析服务
  - `data-agent/src/lib/access-control.ts`
  - `resolveAccessibleAccounts(userId): Promise<AccountAccess>` — 返回当前用户可访问的账户列表
  - 逻辑: 查询 `account_mapping` 获取所有账户 → 按 `access_level` + 用户角色 + 显式授权过滤
  - 缓存策略: 结果缓存 5 分钟（内存缓存 / Redis），`account_mapping` 或授权变更时清除
- [ ] **1.4** 实现 SQL 重写中间件（核心安全层）
  - `data-agent/src/lib/sql-access-filter.ts`
  - **CTE 包装模式** — 在用户 SQL 外层包装权限过滤，不修改内部 SQL 结构:
    ```sql
    -- 原始 LLM 生成的 SQL 被包装为内层 CTE
    WITH _user_query AS (
      {original_sql}
    )
    SELECT * FROM _user_query
    WHERE account_id IN UNNEST(@authorized_account_ids)
    ```
  - 使用 BigQuery 参数化查询 `@authorized_account_ids`（数组参数），避免 SQL 拼接
  - 实现策略:
    - 检测原始 SQL 的结果列是否包含 `account_id`（通过 dry-run 获取 schema）
    - 如果包含 → CTE 包装 + WHERE 过滤
    - 如果不包含（如纯聚合查询）→ 在原始 SQL 的 FROM 子句中注入过滤条件
    - 如果用户是 `admin` 角色 → 跳过包装（全量访问）
  - **优势**: 不需要解析 LLM 生成的 SQL 结构（可能包含复杂 CTE、子查询），只在外层强制过滤
  - 防绕过: 即使用户在自然语言中指定了未授权的 account_id，外层 WHERE 也会强制过滤
- [ ] **1.5** 修改 `/api/bigquery` 路由集成权限过滤
  - 在 SQL 执行前调用 `resolveAccessibleAccounts` 获取可访问账户列表
  - 调用 `injectAccountFilter(sql, accessibleAccountIds)` 注入过滤
  - dry-run 和实际执行均需过滤
- [ ] **1.6** 修改 `/api/chat` 路由注入权限上下文
  - 在 LLM system prompt 中注入用户可访问的账户范围
  - 提示 LLM 生成 SQL 时限制在授权账户范围内
  - 双重保障: LLM 生成 + SQL 注入层强制过滤
- [ ] **1.7** 实现权限管理 API
  - `GET /api/admin/access` — 查看所有用户的权限配置（admin only）
  - `POST /api/admin/access` — 为用户授权特定账户（admin only）
  - `DELETE /api/admin/access/[id]` — 撤销授权（admin only）
  - `GET /api/users/me/accounts` — 当前用户可访问的账户列表
- [ ] **1.8** 更新 `account_mapping.csv` 支持多级 access_level
  - 将部分生产账户的 `access_level` 从 `public` 改为 `team` 或 `restricted`
  - 运行 `dbt seed --full-refresh` 重新加载
  - 验证 mart 表中的 `access_level` 字段已更新

### 涉及文件

| 文件 | 操作 |
|------|------|
| `data-agent/prisma/schema.prisma` | 修改 — User 添加 role 字段，新增 UserAccountAccess 模型 |
| `data-agent/prisma/migrations/` | 新建 — 权限相关 migration |
| `data-agent/src/lib/access-control.ts` | 新建 — 权限解析服务 |
| `data-agent/src/lib/sql-access-filter.ts` | 新建 — SQL 注入中间件 |
| `data-agent/src/app/api/bigquery/route.ts` | 修改 — 集成权限过滤 |
| `data-agent/src/app/api/chat/route.ts` | 修改 — 注入权限上下文到 LLM prompt |
| `data-agent/src/app/api/admin/access/route.ts` | 新建 — 权限管理 API |
| `data-agent/src/app/api/users/me/accounts/route.ts` | 新建 — 可访问账户查询 |
| `data-agent/src/lib/validators.ts` | 修改 — 新增权限相关 zod schema |
| `dbt/seeds/account_mapping.csv` | 修改 — 更新 access_level 值 |

### 验证标准

- [ ] `viewer` 角色用户查询 → SQL 自动注入 `account_id IN (public 账户列表)` → 只返回公开账户数据
- [ ] `analyst` 角色用户查询 → 可访问 public 账户 + 所属团队 team 账户数据
- [ ] `admin` 角色用户查询 → 可访问所有账户数据（包括 restricted）
- [ ] 用户在自然语言中指定未授权账户 → SQL 注入层强制过滤，返回空结果或友好提示
- [ ] `GET /api/users/me/accounts` 返回正确的可访问账户列表
- [ ] admin 通过 `/api/admin/access` 授权后，目标用户立即获得访问权限（缓存清除）
- [ ] 撤销授权后，目标用户无法访问对应账户数据
- [ ] dry-run 模式也遵守权限过滤（防止通过 dry-run 探测数据）
- [ ] `make lint` && `make build` 通过

---

## Step 2: GCP 计费接入 (GCP Billing Integration)

**目标**: 在 dbt 层新增 GCP 计费数据的 staging → intermediate → marts 模型，并更新 data-agent 的 schema 感知以支持 GCP 数据查询。

### 背景

- GCP 计费数据位于 `gcp-digital-bi.gcp_billing_v2` 数据集，BigQuery location 为 `US` (multi-region)
- 当前 dbt target 位于 `us-west2`，**跨 location 查询不支持**
- 需要先解决 location 对齐问题（方案: 将 GCP 数据复制到 `us-west2`，或将 dbt target 迁移到 `US`）
- GCP 计费字段映射已在 `dbt/design/finops-system-design.md` §3.3 中定义

### 任务清单

- [ ] **2.1** 解决 BigQuery location 对齐问题
  - 方案 A（推荐）: 在 `gcp-rd-research-dev` 的 `us-west2` 创建 `gcp_billing_replicated` 数据集，使用 BigQuery Data Transfer 定期复制 GCP 计费数据
  - 方案 B: 将 dbt target 数据集迁移到 `US` multi-region（影响现有 AWS 模型）
  - 方案 C: 在 `US` region 创建独立 GCP mart，通过 Authorized View 或 BigQuery BI Engine 跨区域访问
  - 记录最终决策到 `dbt/design/finops-system-design.md`
- [ ] **2.2** 配置 GCP 数据源
  - `dbt/models/staging/_sources.yml` 添加 GCP billing source 定义
  - source name: `gcp_billing`
  - 表: 按 `_TABLE_SUFFIX` 分区的 GCP 计费导出表
  - 添加 freshness 检查: `warn_after: {count: 48, period: hour}`
- [ ] **2.3** 创建 GCP staging 模型
  - `dbt/models/staging/stg_gcp_cost.sql`
  - 遵循统一 schema 的 30 个字段映射（参见 system-design §3.3）
  - 关键字段映射:
    - `cloud_provider` = `'gcp'`
    - `account_id` = `project.id`
    - `service` = `service.description`
    - `region` = `location.region`
    - `actual_cost_usd` = `cost + SUM(credits.amount)`（credits 为负值）
    - `list_cost_usd` = `COALESCE(cost_at_list, cost)`
    - `charge_type` = 基于 `cost_type` 映射
  - GCP 特有字段（gcp-specific，类似 AWS 的 9 个扩展字段）:
    - `gcp_project_name`, `gcp_sku_id`, `gcp_sku_description`
    - `gcp_invoice_month`, `gcp_cost_type`, `gcp_credits_total`
  - 标签提取: `labels` 和 `project.labels` 中的 `tenant`, `env`, `component` 等
- [ ] **2.4** 更新 account_mapping 添加 GCP 账户
  - `dbt/seeds/account_mapping.csv` 新增 GCP 项目的映射记录
  - `cloud_provider` = `gcp`, `account_id` = GCP project ID
  - 填充 `account_display_name`, `account_purpose`, `owner`, `access_level`
  - 运行 `dbt seed --full-refresh`
- [ ] **2.5** 创建 GCP intermediate 模型
  - `dbt/models/intermediate/int_gcp_billing_ledger.sql` — 全量 ledger（含 credits），join account_mapping
  - `dbt/models/intermediate/int_gcp_billing.sql` — usage-only 视图（排除纯 credit / tax 行）
  - 计算字段: `savings_amount`, `savings_rate`（GCP 使用 CUD/SUD 折扣）
- [ ] **2.6** 创建统一中间层
  - `dbt/models/intermediate/int_billing_unified.sql` — `UNION ALL` of `int_aws_billing` + `int_gcp_billing`
  - 确保两个数据源的字段完全对齐（30 个统一字段 + 各自特有字段 NULL 填充）
  - 此模型为 Phase 3 多云统一查询的基础
- [ ] **2.7** 更新 mart 模型支持多云数据
  - 修改 `mart_cost_monthly.sql` 的 FROM 子句: `ref('int_aws_billing')` → `ref('int_billing_unified')`
  - 修改 `mart_cost_summary.sql` 同理
  - `cloud_provider` 字段已存在于 mart grain 中，无需修改 GROUP BY
  - 验证增量策略在多云场景下仍然正确
- [ ] **2.8** 添加 dbt 测试
  - `dbt/tests/assert_gcp_cost_not_null.sql` — GCP 数据质量检查
  - `dbt/tests/assert_unified_no_null_cloud_provider.sql` — 统一层无空 cloud_provider
  - schema test: `stg_gcp_cost` 字段非空约束、accepted_values
- [ ] **2.9** 更新 data-agent schema 感知
  - 修改 `data-agent/src/lib/docs-loader.ts` 中的 schema 文档
  - 添加 GCP 特有维度（project.id, sku, CUD/SUD 等）到 LLM system prompt
  - 更新 few-shot 示例: 添加 2-3 个 GCP 相关查询示例
  - 确保 LLM 能正确区分 AWS / GCP 数据并生成带 `cloud_provider` 过滤的 SQL

### 涉及文件

| 文件 | 操作 |
|------|------|
| `dbt/models/staging/_sources.yml` | 修改 — 添加 GCP billing source |
| `dbt/models/staging/stg_gcp_cost.sql` | 新建 — GCP staging 视图 |
| `dbt/models/intermediate/int_gcp_billing_ledger.sql` | 新建 — GCP ledger intermediate |
| `dbt/models/intermediate/int_gcp_billing.sql` | 新建 — GCP usage-only intermediate |
| `dbt/models/intermediate/int_billing_unified.sql` | 新建 — 多云 UNION ALL |
| `dbt/models/marts/core/mart_cost_monthly.sql` | 修改 — FROM 切换到 unified |
| `dbt/models/marts/core/mart_cost_summary.sql` | 修改 — 下游自动继承变更 |
| `dbt/seeds/account_mapping.csv` | 修改 — 添加 GCP 项目映射 |
| `dbt/tests/` | 新建 — GCP 数据质量测试 |
| `data-agent/src/lib/docs-loader.ts` | 修改 — 添加 GCP schema 文档 |
| `data-agent/src/lib/few-shot-examples.ts` | 修改 — 添加 GCP 查询示例 |
| `dbt/design/finops-system-design.md` | 修改 — 记录 location 决策 |

### 验证标准

- [ ] BigQuery location 对齐方案已决策并实施，GCP 数据可在 `us-west2` 查询
- [ ] `dbt build` 成功执行，`stg_gcp_cost` 视图返回正确数据
- [ ] GCP staging 的 30 个统一字段与 AWS staging 对齐（字段名、类型一致）
- [ ] `int_billing_unified` 包含 AWS + GCP 数据，`cloud_provider` 分别为 `aws` / `gcp`
- [ ] `mart_cost_monthly` 和 `mart_cost_summary` 包含 GCP 数据
- [ ] dbt test 全部通过（GCP 数据质量 + 统一层完整性）
- [ ] data-agent 中提问 "GCP 上个月各项目的费用" → 返回正确的 GCP 计费数据
- [ ] data-agent 中提问 "各云的总费用对比" → 返回 AWS + GCP 的汇总对比
- [ ] `dbt build` && `make lint` && `make build` 通过

---

## Step 3: Azure 计费接入 (Azure Billing Integration)

**目标**: 按照 Step 2 建立的多云框架模式，接入 Azure 计费数据。

### 背景

- Azure 计费数据位于 `gcp-digital-bi.azure_billing` 数据集，表名格式 `azure_billing_cost_YYYYMM01`
- 数据位于 `us-west2`，与 dbt target 同区域，无需额外 location 对齐
- Azure 特有概念: Subscription (对应 account), Resource Group, Meter, Reservation/Savings Plan

### 任务清单

- [ ] **3.1** 配置 Azure 数据源
  - `dbt/models/staging/_sources.yml` 添加 Azure billing source 定义
  - source name: `azure_billing`
  - 表: `azure_billing_cost_*`（按 `_TABLE_SUFFIX` 分区）
  - freshness 检查
- [ ] **3.2** 创建 Azure staging 模型
  - `dbt/models/staging/stg_azure_cost.sql`
  - 关键字段映射:
    - `cloud_provider` = `'azure'`
    - `account_id` = `SubscriptionId`
    - `account_name` = `subscriptionName`
    - `service` = `consumedService`
    - `service_category` = `serviceFamily`
    - `region` = `COALESCE(resourceLocation, location, 'global')`
    - `actual_cost_usd` = `costInUsd`
    - `list_cost_usd` = `COALESCE(paygCostInUsd, costInUsd)`
    - `charge_type` = 基于 `chargeType` 映射 (Usage, Purchase, Tax, Refund 等)
    - `pricing_model` = 基于 `pricingModel` 映射
  - Azure 特有字段:
    - `azure_resource_group`, `azure_meter_id`, `azure_meter_category`
    - `azure_meter_sub_category`, `azure_reservation_id`, `azure_benefit_id`
  - 标签: 从 `tags` JSON 字符串中提取 `tenant`, `env`, `component` 等
- [ ] **3.3** 更新 account_mapping 添加 Azure 订阅
  - `dbt/seeds/account_mapping.csv` 新增 Azure Subscription 的映射
  - `cloud_provider` = `azure`, `account_id` = Azure Subscription ID
  - 运行 `dbt seed --full-refresh`
- [ ] **3.4** 创建 Azure intermediate 模型
  - `dbt/models/intermediate/int_azure_billing_ledger.sql` — 全量 ledger，join account_mapping
  - `dbt/models/intermediate/int_azure_billing.sql` — usage-only 视图
- [ ] **3.5** 更新统一中间层
  - `int_billing_unified.sql` 添加 `UNION ALL` Azure 数据
  - 现在 unified = AWS + GCP + Azure
  - mart 层无需修改（已在 Step 2 切换到 unified）
- [ ] **3.6** 添加 dbt 测试
  - Azure 数据质量测试
  - 三云统一层完整性测试
- [ ] **3.7** 更新 data-agent schema 感知
  - docs-loader 添加 Azure 特有维度文档
  - few-shot 示例添加 Azure 查询
  - 更新 LLM prompt 的多云上下文说明

### 涉及文件

| 文件 | 操作 |
|------|------|
| `dbt/models/staging/_sources.yml` | 修改 — 添加 Azure billing source |
| `dbt/models/staging/stg_azure_cost.sql` | 新建 — Azure staging 视图 |
| `dbt/models/intermediate/int_azure_billing_ledger.sql` | 新建 — Azure ledger |
| `dbt/models/intermediate/int_azure_billing.sql` | 新建 — Azure usage-only |
| `dbt/models/intermediate/int_billing_unified.sql` | 修改 — UNION ALL 添加 Azure |
| `dbt/seeds/account_mapping.csv` | 修改 — 添加 Azure 订阅映射 |
| `dbt/tests/` | 新建 — Azure 数据质量测试 |
| `data-agent/src/lib/docs-loader.ts` | 修改 — 添加 Azure schema |
| `data-agent/src/lib/few-shot-examples.ts` | 修改 — 添加 Azure 查询示例 |

### 验证标准

- [ ] `dbt build` 成功，`stg_azure_cost` 视图返回正确数据
- [ ] Azure staging 的 30 个统一字段与 AWS/GCP 对齐
- [ ] `int_billing_unified` 包含三云数据
- [ ] `mart_cost_monthly` 和 `mart_cost_summary` 包含 Azure 数据
- [ ] dbt test 全部通过
- [ ] data-agent 中提问 "Azure 各订阅的费用" → 返回正确 Azure 数据
- [ ] data-agent 中提问 "三朵云的费用对比" → 返回 AWS + GCP + Azure 汇总
- [ ] `dbt build` && `make lint` && `make build` 通过

---

## Step 4: 交互式下钻图表 (Interactive Drill-Down Charts)

**目标**: 在现有 ECharts 图表基础上实现交互式 drill-down 功能，用户可以通过点击图表元素逐层钻取到更细粒度的数据。

### 背景

Phase 1 已实现基础的 chart-suggester + ECharts 渲染。Phase 4 将扩展为:
- 点击柱状图中的某个 service → 自动查询该 service 的区域分布
- 点击饼图中的某个 region → 自动查询该 region 的服务明细
- 面包屑导航支持多级 drill-down 回溯

### 任务清单

- [ ] **4.1** 设计 drill-down 维度层次
  - `data-agent/src/lib/drill-down-schema.ts`
  - 定义可下钻的维度层次树:
    ```
    Total Cost
      → cloud_provider (AWS / GCP / Azure)
        → account_id (按账户)
          → service (按服务)
            → region (按区域)
              → tag_tenant / tag_project (按标签)
    ```
  - 每个维度节点定义: `dimension`, `displayName`, `children`, `sqlTemplate`
  - 支持自定义起始维度（用户可以从任意层级开始）
- [ ] **4.2** 实现 drill-down 查询生成器
  - `data-agent/src/lib/drill-down-query.ts`
  - `generateDrillDownSQL(parentDimension, parentValue, childDimension, filters): string`
  - 基于维度层次树和当前 drill-down 路径生成 SQL
  - 自动继承父级过滤条件（WHERE 链）
  - 集成 Step 1 的权限过滤（`account_id IN (...)` 自动注入）
- [ ] **4.3** 实现 `/api/drill-down` API 端点
  - `POST /api/drill-down`
  - 请求: `{ dimension, value, targetDimension, filters, timeRange }`
  - 响应: `{ data: QueryResult, breadcrumb: DrillDownPath[], childDimensions: string[] }`
  - 直接执行 BigQuery 查询（不经过 LLM，确保低延迟 < 3s）
  - 集成权限过滤
- [ ] **4.4** 扩展 ECharts 组件支持点击事件
  - 修改 `data-agent/src/components/result-chart.tsx`
  - 注册 ECharts `click` 事件监听
  - 点击数据项时触发 drill-down 回调: `onDrillDown(dimension, value)`
  - 点击空白区域无效
  - 鼠标 hover 时 cursor 变为 pointer（提示可点击）
- [ ] **4.5** 实现面包屑导航组件
  - `data-agent/src/components/drill-down-breadcrumb.tsx`
  - 展示当前 drill-down 路径: `总计 > AWS > 123456789 (Dev-Account) > AmazonEC2`
  - 点击任意节点可回退到该层级
  - 点击 "总计" 回到顶层
  - 与图表联动: 回退时重新渲染对应层级的图表
- [ ] **4.6** 实现 drill-down 图表容器
  - `data-agent/src/components/drill-down-chart.tsx`
  - 组合: 面包屑 + ECharts 图表 + 加载状态
  - 管理 drill-down 状态（路径栈、当前数据、过滤条件）
  - 过渡动画: drill-down 时图表平滑切换（ECharts `setOption` 带 `notMerge`）
  - 支持键盘 Escape 返回上一层
- [ ] **4.7** 集成到 result-view
  - `result-view.tsx` 新增第三个 Tab: "Drill-Down"
  - 仅当查询结果包含可下钻维度时显示
  - 默认从查询结果的最高聚合维度开始
  - 保留 Table / Chart Tab 不受影响

### 涉及文件

| 文件 | 操作 |
|------|------|
| `data-agent/src/lib/drill-down-schema.ts` | 新建 — 维度层次定义 |
| `data-agent/src/lib/drill-down-query.ts` | 新建 — drill-down SQL 生成 |
| `data-agent/src/app/api/drill-down/route.ts` | 新建 — drill-down API |
| `data-agent/src/components/result-chart.tsx` | 修改 — 添加 click 事件 |
| `data-agent/src/components/drill-down-breadcrumb.tsx` | 新建 — 面包屑导航 |
| `data-agent/src/components/drill-down-chart.tsx` | 新建 — drill-down 图表容器 |
| `data-agent/src/components/result-view.tsx` | 修改 — 新增 Drill-Down Tab |
| `data-agent/src/lib/validators.ts` | 修改 — drill-down 请求 zod schema |

### 验证标准

- [ ] 查询 "各服务的月度费用" → 柱状图中点击 "AmazonEC2" → 自动展示 EC2 的区域分布
- [ ] 面包屑显示: `总计 > AmazonEC2`，点击 "总计" 回到服务维度
- [ ] 支持多级 drill-down: 服务 → 区域 → 标签（3 层以上）
- [ ] drill-down 查询延迟 < 3s（直接 BigQuery，不经 LLM）
- [ ] 权限过滤在 drill-down 中生效（viewer 用户只看到 public 账户数据）
- [ ] 图表切换动画流畅，无闪烁
- [ ] Escape 键返回上一层正常工作
- [ ] Drill-Down Tab 仅在结果包含可下钻维度时显示
- [ ] `make lint` && `make build` 通过

---

## Step 5: 自定义仪表板 (Custom Dashboard Builder)

**目标**: 用户可以创建个人仪表板，将常用查询固定为图表卡片，支持自定义布局和自动刷新。

### 背景

与 Superset 等专业 BI 工具不同，此功能面向 data-agent 的轻量级使用场景:
- 用户将常用的 NL 查询保存为仪表板卡片
- 每个卡片是一个固定查询 + 图表配置
- 仪表板支持定期自动刷新（不依赖 cron，使用前端轮询或 SSE）

### 任务清单

- [ ] **5.1** 设计仪表板数据模型
  - Prisma Schema 新增:
    - `Dashboard` 模型: `id`, `userId`, `title`, `description`, `layout`, `isDefault`, `createdAt`, `updatedAt`
    - `DashboardCard` 模型: `id`, `dashboardId`, `title`, `query` (NL 原文), `sql` (固化的 SQL), `chartType`, `chartConfig` (ECharts option 覆盖), `position` (grid 位置), `refreshInterval`, `lastRefreshedAt`
  - `layout` 字段: JSON 格式存储 grid 布局（兼容 react-grid-layout）
  - 创建 migration
- [ ] **5.2** 实现仪表板 CRUD API
  - `GET /api/dashboards` — 用户的仪表板列表
  - `POST /api/dashboards` — 创建仪表板
  - `GET /api/dashboards/[id]` — 仪表板详情 + 所有卡片
  - `PATCH /api/dashboards/[id]` — 更新标题/布局
  - `DELETE /api/dashboards/[id]` — 删除仪表板
- [ ] **5.3** 实现卡片 CRUD API
  - `POST /api/dashboards/[id]/cards` — 添加卡片
  - `PATCH /api/dashboards/[id]/cards/[cardId]` — 更新卡片配置
  - `DELETE /api/dashboards/[id]/cards/[cardId]` — 删除卡片
  - `POST /api/dashboards/[id]/cards/[cardId]/refresh` — 手动刷新卡片数据
- [ ] **5.4** 实现 "保存到仪表板" 功能
  - 在查询结果的 result-view 中添加 "Pin to Dashboard" 按钮
  - 弹出选择: 选择目标仪表板 / 创建新仪表板
  - 自动保存: NL 查询 + 生成的 SQL + chart-suggester 推荐的图表类型
  - 用户可在保存前修改卡片标题和图表类型
- [ ] **5.5** 安装 react-grid-layout 并实现仪表板布局
  - `npm install react-grid-layout @types/react-grid-layout`
  - `data-agent/src/components/dashboard/dashboard-grid.tsx`
  - 拖拽调整卡片位置和大小
  - 响应式布局: 大屏 3 列、中屏 2 列、小屏 1 列
  - 编辑模式 / 查看模式切换
- [ ] **5.6** 实现仪表板卡片组件
  - `data-agent/src/components/dashboard/dashboard-card.tsx`
  - 每个卡片: 标题 + ECharts 图表 + 最后刷新时间
  - 卡片操作: 刷新、编辑、删除、查看详情（跳转到聊天界面重新提问）
  - 加载状态: skeleton loading
  - 错误状态: SQL 执行失败时显示错误信息 + 重试按钮
- [ ] **5.7** 实现仪表板页面
  - `data-agent/src/app/dashboard/page.tsx` — 仪表板列表页
  - `data-agent/src/app/dashboard/[id]/page.tsx` — 仪表板详情页
  - 导航栏添加 "Dashboard" 入口
  - 空状态引导: 无仪表板时提示 "从聊天中保存查询到仪表板"
- [ ] **5.8** 实现卡片自动刷新
  - 每个卡片可配置刷新间隔: 5min / 15min / 1h / 手动
  - 使用 `setInterval` + `visibilitychange` 事件（页面不可见时暂停）
  - 刷新时重新执行固化的 SQL（不重新调用 LLM）
  - 刷新结果与上次对比，有显著变化时视觉提示（数值变色）

### 涉及文件

| 文件 | 操作 |
|------|------|
| `data-agent/prisma/schema.prisma` | 修改 — 新增 Dashboard, DashboardCard 模型 |
| `data-agent/prisma/migrations/` | 新建 — 仪表板 migration |
| `data-agent/package.json` | 修改 — 添加 react-grid-layout 依赖 |
| `data-agent/src/app/api/dashboards/route.ts` | 新建 — 仪表板列表 + 创建 |
| `data-agent/src/app/api/dashboards/[id]/route.ts` | 新建 — 仪表板详情/更新/删除 |
| `data-agent/src/app/api/dashboards/[id]/cards/route.ts` | 新建 — 卡片 CRUD |
| `data-agent/src/app/api/dashboards/[id]/cards/[cardId]/route.ts` | 新建 — 单卡片操作 |
| `data-agent/src/app/api/dashboards/[id]/cards/[cardId]/refresh/route.ts` | 新建 — 卡片刷新 |
| `data-agent/src/components/dashboard/dashboard-grid.tsx` | 新建 — 网格布局 |
| `data-agent/src/components/dashboard/dashboard-card.tsx` | 新建 — 卡片组件 |
| `data-agent/src/app/dashboard/page.tsx` | 新建 — 仪表板列表页 |
| `data-agent/src/app/dashboard/[id]/page.tsx` | 新建 — 仪表板详情页 |
| `data-agent/src/components/result-view.tsx` | 修改 — 添加 "Pin to Dashboard" |
| `data-agent/src/lib/validators.ts` | 修改 — 仪表板相关 zod schema |

### 验证标准

- [ ] 创建仪表板 → 添加 3+ 卡片 → 拖拽调整布局 → 刷新页面布局保持
- [ ] 从聊天结果中 "Pin to Dashboard" → 卡片正确显示图表
- [ ] 卡片自动刷新 → 刷新间隔内数据自动更新
- [ ] 卡片手动刷新 → 点击刷新按钮 → SQL 重新执行 → 图表更新
- [ ] SQL 执行失败 → 卡片显示错误状态 + 重试按钮
- [ ] 删除仪表板 → 关联卡片一起删除
- [ ] 仪表板卡片遵守权限过滤（viewer 用户的卡片只显示授权数据）
- [ ] 响应式布局: 大屏 3 列 → 缩小窗口 → 自动变为 2 列 → 1 列
- [ ] 编辑模式下可拖拽，查看模式下不可拖拽
- [ ] 页面不可见时暂停自动刷新，切回时恢复
- [ ] `make lint` && `make build` 通过

---

## Step 6: AI 异常归因分析 (AI-Powered Anomaly Root Cause Analysis)

**目标**: 当 Phase 3 的异常检测引擎发现费用异常时，自动使用 LLM 分析异常的根因，生成结构化的归因报告。

### 背景

Phase 3 的异常检测能发现 "什么地方出了异常"（环比增长 +52%），但无法回答 "为什么"。Phase 4 通过 LLM 分析异常上下文数据，提供可能的根因解释:
- EC2 费用增长 → 可能是新实例启动、实例升配、SP 到期
- S3 费用增长 → 可能是数据传输激增、存储类型变更
- 新服务出现 → 可能是新项目上线

### 任务清单

- [ ] **6.1** 设计异常上下文数据采集
  - `data-agent/src/lib/anomaly-context.ts`
  - `collectAnomalyContext(alert: AnomalyAlert): Promise<AnomalyContext>`
  - 根据异常类型采集不同维度的上下文数据:
    - **环比增长**: 当月 vs 上月的日粒度趋势、按资源/实例类型拆分、按定价模型拆分
    - **新服务**: 该服务的资源列表、实例类型分布、关联标签
    - **标签缺失**: 未打标签的 Top N 资源 ID 和实例类型
  - 每种异常类型对应 2-3 个 BigQuery 辅助查询
  - 控制上下文数据量: 每个查询最多返回 50 行，总上下文 < 4000 tokens
- [ ] **6.2** 设计归因分析 LLM Prompt
  - `data-agent/src/lib/anomaly-prompts.ts`
  - 分异常类型的专用 prompt 模板:
    - `mom_increase` prompt: "以下是 {service} 在 {account} 的月环比费用变化数据..."
    - `new_service` prompt: "以下是新出现的服务 {service} 的资源使用情况..."
  - 要求 LLM 输出结构化 JSON:
    ```json
    {
      "root_causes": [
        { "cause": "新增 3 台 r5.2xlarge 实例", "confidence": "high", "evidence": "..." },
        { "cause": "SP-12345 于 3/1 到期，覆盖率下降", "confidence": "medium", "evidence": "..." }
      ],
      "summary": "费用增长主要由新增实例驱动，建议购买新的 Savings Plan...",
      "suggested_actions": ["购买 Compute SP 覆盖新实例", "Review 新实例的 tag 合规性"]
    }
    ```
  - 添加 FinOps 领域知识到 prompt: SP/RI 机制、常见费用增长模式
- [ ] **6.3** 实现归因分析引擎
  - `data-agent/src/lib/anomaly-analyzer.ts`
  - `analyzeAnomaly(alert: AnomalyAlert): Promise<AnomalyAnalysis>`
  - 流程: 采集上下文 → 构建 prompt → 调用 LLM → 解析结构化输出 → 存储结果
  - LLM 调用使用现有的 OpenRouter/Gemini 通道
  - 超时控制: 单次分析 < 30s，超时返回 "分析超时，请手动查看"
  - 错误处理: LLM 返回非结构化内容时 → 降级为原始文本存储
- [ ] **6.4** 扩展 AnomalyAlert 模型存储分析结果
  - Prisma Schema 在 `AnomalyAlert` 添加字段:
    - `analysis: Json?` — 归因分析结果
    - `analyzedAt: DateTime?` — 分析完成时间
    - `analysisStatus: String` — `'pending' | 'analyzing' | 'completed' | 'failed'`
  - 创建 migration
- [ ] **6.5** 集成到异常检测管道
  - 修改 `/api/cron/detect-anomalies` 或异常检测引擎
  - 检测到新异常后，异步触发归因分析
  - 分析完成后更新 alert 记录
  - 如果分析结果中 confidence 为 high 的 root_cause 存在 → 自动升级告警 severity
- [ ] **6.6** 实现归因分析展示
  - 修改异常告警详情页 / 告警卡片
  - 展示: 根因列表（按 confidence 排序）+ 摘要 + 建议操作
  - 飞书告警卡片增加 "AI 分析" 区块
  - Web 端告警详情页增加 "Root Cause Analysis" 区域
- [ ] **6.7** 实现手动触发归因分析 API
  - `POST /api/anomalies/alerts/[id]/analyze` — 对已有告警手动触发 AI 分析
  - 用于: 旧告警补充分析、分析失败后重试
  - 防重复: 如果已有 completed 分析结果，需要确认覆盖

### 涉及文件

| 文件 | 操作 |
|------|------|
| `data-agent/src/lib/anomaly-context.ts` | 新建 — 异常上下文数据采集 |
| `data-agent/src/lib/anomaly-prompts.ts` | 新建 — 归因分析 prompt 模板 |
| `data-agent/src/lib/anomaly-analyzer.ts` | 新建 — 归因分析引擎 |
| `data-agent/prisma/schema.prisma` | 修改 — AnomalyAlert 添加分析字段 |
| `data-agent/prisma/migrations/` | 新建 — 分析字段 migration |
| `data-agent/src/app/api/anomalies/alerts/[id]/analyze/route.ts` | 新建 — 手动分析 API |
| `data-agent/src/app/api/cron/detect-anomalies/route.ts` | 修改 — 集成异步分析 |
| `data-agent/src/components/anomaly/alert-analysis.tsx` | 新建 — 归因展示组件 |
| `data-agent/src/lib/validators.ts` | 修改 — 分析相关 zod schema |

### 验证标准

- [ ] 检测到 `mom_increase` 异常 → 自动触发 AI 分析 → 30s 内返回结构化归因结果
- [ ] 归因结果包含 1-3 个 root_cause，每个都有 confidence 和 evidence
- [ ] summary 为人类可读的中文摘要（100-200 字）
- [ ] suggested_actions 包含 1-3 个可操作的建议
- [ ] LLM 返回异常内容 → 降级为原始文本存储，不报错
- [ ] 飞书告警卡片包含 AI 分析区块（root cause + 建议）
- [ ] Web 端告警详情页正确展示归因分析
- [ ] 手动触发 `POST /api/anomalies/alerts/[id]/analyze` → 重新分析并更新结果
- [ ] 分析超时 → 返回友好提示，status 设为 `failed`
- [ ] `make lint` && `make build` 通过

---

## Step 7: AI 成本优化建议 (AI Cost Optimization Recommendations)

**目标**: 基于用户的历史查询数据和计费模式，LLM 生成可操作的成本优化建议。

### 背景

与 Step 6 的 "被动" 归因分析不同，Step 7 是 "主动" 的优化建议:
- 定期扫描用户关注范围内的成本数据
- 识别优化机会（SP/RI 覆盖不足、闲置资源、区域选择不优等）
- 生成量化的节省预估和操作步骤

### 任务清单

- [ ] **7.1** 设计优化场景识别器
  - `data-agent/src/lib/optimization-scanners.ts`
  - 实现多个扫描器（每个对应一种优化场景）:
    - **SP/RI 覆盖率扫描**: 分析 `pricing_model` 分布，识别 OnDemand 占比过高的服务/区域
    - **闲置资源扫描**: 识别低利用率实例（费用持续但使用量极低的资源）
    - **区域优化扫描**: 同一服务跨区域价差分析
    - **存储优化扫描**: S3/GCS 存储类型使用模式分析
  - 每个扫描器输出: `{ opportunity_type, affected_resources, current_cost, estimated_savings, context_data }`
- [ ] **7.2** 设计优化建议 LLM Prompt
  - `data-agent/src/lib/optimization-prompts.ts`
  - 分场景的专用 prompt 模板
  - 要求 LLM 输出结构化建议:
    ```json
    {
      "recommendations": [
        {
          "title": "为 us-east-1 EC2 购买 Compute Savings Plan",
          "priority": "high",
          "estimated_monthly_savings": 3200,
          "effort": "low",
          "description": "当前 us-east-1 的 EC2 OnDemand 费用占 68%...",
          "steps": [
            "1. 分析过去 30 天的 EC2 使用模式",
            "2. 在 AWS Cost Explorer 中计算推荐的 SP 承诺金额",
            "3. 购买 1 年 No Upfront Compute SP"
          ],
          "risk": "SP 承诺期内如果用量下降，承诺费用仍需支付"
        }
      ],
      "total_estimated_savings": 5800,
      "summary": "共识别 3 个优化机会，预计月度节省 $5,800..."
    }
    ```
  - 注入 FinOps 领域知识: SP/RI 对比、存储类型特性、区域定价差异
- [ ] **7.3** 实现优化建议引擎
  - `data-agent/src/lib/optimization-engine.ts`
  - `generateRecommendations(userId): Promise<OptimizationReport>`
  - 流程: 加载用户画像 → 确定扫描范围 → 运行扫描器 → 汇总优化机会 → LLM 生成建议
  - 画像集成: 基于 `UserProfile.focusAccountIds`, `focusServices` 确定扫描范围
  - 控制开销: 限制扫描器查询数 (< 10 个 BigQuery 查询)，LLM 输入 < 8000 tokens
- [ ] **7.4** 创建优化建议数据模型
  - Prisma Schema 新增:
    - `OptimizationReport` 模型: `id`, `userId`, `status`, `recommendations` (Json), `totalEstimatedSavings`, `generatedAt`, `expiresAt`
  - 报告有效期: 7 天（计费数据变化后建议可能过期）
  - 创建 migration
- [ ] **7.5** 实现优化建议 API
  - `GET /api/optimization` — 获取当前用户的最新优化报告
  - `POST /api/optimization/generate` — 手动触发优化分析
  - `PATCH /api/optimization/[id]/recommendations/[idx]` — 标记建议状态（accepted / dismissed）
  - `POST /api/cron/generate-optimizations` — 定期为活跃用户生成优化报告
- [ ] **7.6** 实现优化建议页面
  - `data-agent/src/app/optimization/page.tsx`
  - 展示: 节省总额概览 + 建议卡片列表（按 priority 排序）
  - 每张卡片: 标题 + 预估节省 + 实施步骤 + 风险说明
  - 操作按钮: "接受" / "忽略" / "查看详情"
  - 空状态: "暂无优化建议" 或 "正在分析中..."
- [ ] **7.7** 实现聊天式优化咨询
  - 在 `/api/chat` 中添加特殊意图识别
  - 用户提问 "有什么成本优化建议" / "怎么降低 EC2 费用" → 自动触发优化分析
  - 返回最新的优化报告或实时生成新建议
  - 支持追问: "SP 具体应该买多少" → 基于数据计算推荐承诺金额
- [ ] **7.8** 集成飞书推送
  - 优化报告生成后 → 推送摘要到用户飞书
  - 飞书卡片: 节省总额 + Top 3 建议 + "查看详情" 链接
  - 仅推送预估节省 > $500/月 的报告（避免噪音）

### 涉及文件

| 文件 | 操作 |
|------|------|
| `data-agent/src/lib/optimization-scanners.ts` | 新建 — 优化场景扫描器 |
| `data-agent/src/lib/optimization-prompts.ts` | 新建 — 优化建议 prompt 模板 |
| `data-agent/src/lib/optimization-engine.ts` | 新建 — 优化建议引擎 |
| `data-agent/prisma/schema.prisma` | 修改 — 新增 OptimizationReport 模型 |
| `data-agent/prisma/migrations/` | 新建 — 优化报告 migration |
| `data-agent/src/app/api/optimization/route.ts` | 新建 — 优化报告 API |
| `data-agent/src/app/api/optimization/generate/route.ts` | 新建 — 触发分析 |
| `data-agent/src/app/api/optimization/[id]/route.ts` | 新建 — 报告详情/更新 |
| `data-agent/src/app/api/cron/generate-optimizations/route.ts` | 新建 — 定期分析 |
| `data-agent/src/app/optimization/page.tsx` | 新建 — 优化建议页面 |
| `data-agent/src/components/optimization/recommendation-card.tsx` | 新建 — 建议卡片 |
| `data-agent/src/app/api/chat/route.ts` | 修改 — 添加优化意图识别 |
| `data-agent/src/lib/validators.ts` | 修改 — 优化相关 zod schema |

### 验证标准

- [ ] 运行优化扫描 → 识别出 SP 覆盖率不足的账户/服务
- [ ] LLM 生成的建议包含量化的 `estimated_monthly_savings`
- [ ] 建议步骤具体可操作（非泛泛的 "优化资源"）
- [ ] 优化报告页面正确展示所有建议，按 priority 排序
- [ ] 标记建议为 "accepted" / "dismissed" 后状态持久化
- [ ] 在聊天中提问 "有什么优化建议" → 触发分析并返回建议
- [ ] 追问 "SP 应该买多少" → LLM 基于数据给出量化推荐
- [ ] 飞书推送包含节省金额和 Top 建议
- [ ] 7 天过期机制: 过期报告标记为 expired，提示用户重新生成
- [ ] 优化扫描遵守权限过滤（用户只能看到授权账户的优化建议）
- [ ] `make lint` && `make build` 通过

---

## 风险与注意事项

### 技术风险

| 风险 | 影响 | 缓解措施 |
|------|------|----------|
| BigQuery location 不对齐（GCP 数据在 US，dbt 在 us-west2） | GCP 数据无法直接查询，阻塞 Step 2 | 提前调研 Data Transfer / 数据集复制方案，作为 Step 2.1 优先解决 |
| 多云统一 schema 字段映射不完全 | GCP/Azure 特有概念（CUD、Resource Group 等）丢失 | 保留 cloud-specific 扩展字段，统一字段仅覆盖公共维度 |
| SQL 注入式权限过滤被绕过 | 用户看到未授权数据 | 多层防御: LLM prompt 限制 + SQL 解析注入 + BigQuery 级别 Authorized View (终极方案) |
| drill-down 查询性能 | 多级下钻产生大量 BigQuery 查询 | 利用 mart 表的 cluster_by 优化，drill-down 查询结果缓存 5 分钟 |
| AI 分析结果幻觉（hallucination） | 给出错误的根因解释或优化建议 | 1) 所有 AI 输出标注 "AI 生成，仅供参考" 2) 要求 LLM 引用具体数据作为 evidence 3) confidence 标注 4) 人工确认流程 |
| AI 分析的 LLM 成本激增 | 异常检测 + 优化建议大量调用 LLM | 1) 控制扫描频率（优化建议每周一次） 2) 限制上下文数据量 3) 使用更经济的模型（Gemini Flash） |
| 自定义仪表板 SQL 注入风险 | 保存的 SQL 可能被篡改 | SQL 保存时固化，刷新时不经 LLM，直接执行固化 SQL（不接受用户修改 SQL） |

### 多云数据一致性注意事项

1. **成本字段语义映射表**:

   | 统一字段 | AWS 源字段 | GCP 源字段 | Azure 源字段 | 语义说明 |
   |---------|-----------|-----------|-------------|---------|
   | `actual_cost_usd` | `unblended_cost` | `cost + SUM(credits.amount)` | `costInUsd` | 实际支付成本（含折扣） |
   | `list_cost_usd` | `public_ondemand_cost` | `COALESCE(cost_at_list, cost)` | `COALESCE(paygCostInUsd, costInUsd)` | 按需公开价（无折扣基准） |
   | `savings_rate` | `(list - actual) / list` | `(list - actual) / list` | `(list - actual) / list` | **跨云不可直接比较**（见下方说明） |

   **`savings_rate` 跨云不可比性说明**:
   - AWS savings_rate 反映 SP/RI 折扣效果
   - GCP savings_rate 反映 CUD/SUD 折扣效果（SUD 为自动折扣，含义与 AWS SP 不同）
   - Azure savings_rate 反映 Reservation/Savings Plan 折扣
   - 跨云对比 savings_rate 时需附加说明: "各云折扣机制不同，savings_rate 仅在同一云内可比"
   - 前端展示跨云 savings_rate 对比时，添加 tooltip 提示差异

2. **时间对齐**:
   - AWS CUR 按月分区，数据有 24-48h 延迟
   - GCP 按日分区，数据近实时
   - Azure 按月分文件，延迟 1-3 天
   - 跨云对比时需注意时间窗口对齐

3. **账户层级差异**:
   - AWS: Payer Account → Linked Account
   - GCP: Billing Account → Project
   - Azure: Billing Account → Subscription → Resource Group
   - `account_mapping` 统一到 account_id 级别（AWS Account / GCP Project / Azure Subscription）

4. **标签覆盖率差异**:
   - AWS 资源标签覆盖率约 60-80%
   - GCP Label 和 Project Label 覆盖率差异大
   - Azure Tag 覆盖率因资源类型而异
   - 跨云标签分析需考虑不同覆盖率的影响

### AI 幻觉防控策略

#### Confidence 评级标准

| 级别 | 条件 | 前端展示 | Evidence 要求 |
|------|------|---------|-------------|
| **high** | LLM 的结论有直接数据支撑（具体数值引用） | 正常展示，绿色标识 | 必须引用至少 1 个具体数据点 |
| **medium** | LLM 基于数据模式推断，但无法完全确认 | 展示但标注 "推断"，黄色标识 | 需说明推断依据 |
| **low** | LLM 基于领域知识猜测，数据中无直接证据 | 折叠展示，需用户手动展开，灰色标识 | 明确标注 "基于经验推测" |

#### 防护措施

1. **数据锚定**: LLM 的输入必须包含真实查询数据，prompt 明确要求 "仅基于以下数据分析，不得编造不在数据中的事实"
2. **结构化输出**: 使用 JSON Schema 约束输出格式，每个 root_cause 必须包含 `cause`, `confidence`, `evidence` 三个字段
3. **Evidence 强制引用**: prompt 要求 LLM 在 evidence 字段中引用具体的数据行（如 "us-east-1 的 EC2 费用从 $8,000 增至 $12,160"），无法引用时 confidence 自动降为 low
4. **数据边界约束**: prompt 明确告知 "如果数据不足以判断根因，请回答'数据不足，建议从以下角度进一步排查'"
5. **人工反馈**: 用户可标记分析结果为 "准确" / "不准确"，用于后续 prompt 优化

#### 免责声明

所有 AI 生成的分析内容，前端必须展示以下声明（不可省略）:

```
⚠️ AI 分析结果仅供参考，基于历史数据模式推断。
实际根因需结合 AWS/GCP/Azure 控制台和变更记录确认。
```

前端实现:
- `data-agent/src/components/ai-disclaimer.tsx` — 统一免责声明组件
- 在告警归因分析、优化建议、报告洞察等所有 AI 输出处使用
- confidence 为 low 的结果额外添加: "此推断基于经验，数据中无直接证据支撑"

#### 前端 Confidence 可视化

```typescript
// data-agent/src/components/confidence-badge.tsx
interface ConfidenceBadgeProps {
  level: 'high' | 'medium' | 'low'
  evidence?: string
}
// high  → 🟢 + tooltip 显示 evidence
// medium → 🟡 + tooltip "推断依据: {evidence}"
// low   → 🔘 + 折叠展示 + "基于经验推测"
```

### 开发注意事项

1. **Step 1 权限是安全基础**: 必须在开放多云数据和 AI 功能前完成，否则存在数据泄漏风险
2. **dbt build 顺序**: GCP/Azure 新增模型后，`dbt build` 需按 staging → intermediate → unified → marts 顺序执行
3. **增量模型注意**: 切换 mart 的 FROM 到 unified 后，首次运行需要 `dbt run --full-refresh` 重建 mart 表
4. **drill-down API 不经 LLM**: 为保证低延迟，drill-down 查询直接构建 SQL 执行 BigQuery，不调用 LLM
5. **仪表板卡片 SQL 安全**: 保存的 SQL 是固化的，用户无法直接修改 SQL 内容，只能通过重新提问更新
6. **LLM 模型选择**: 异常归因和优化建议可使用 Gemini Flash (低成本) 而非 GPT-4 级别模型
7. **渐进式上线**: 建议先上线 Step 1-4，稳定后再上线 Step 5-7，避免一次性引入过多变化

---

## 完成后检查

- [ ] `make lint` 通过
- [ ] `make build` 通过
- [ ] `dbt build` 通过（含 GCP + Azure 新模型）
- [ ] `docker-compose up -d --build` 完整启动成功
- [ ] 所有现有功能不受影响（回归测试）
- [ ] 权限过滤在所有数据路径生效（聊天查询、drill-down、仪表板、优化建议）
- [ ] GCP / Azure 数据在 mart 中可查且与 AWS 数据统一格式
- [ ] 跨云汇总查询返回正确结果（总费用 = AWS + GCP + Azure 之和）
- [ ] drill-down 交互流畅，延迟 < 3s
- [ ] AI 分析结果附带免责声明和 confidence 标注
- [ ] 飞书推送包含 AI 归因分析和优化建议
- [ ] 新增 API 路由全部需要认证
- [ ] 无敏感信息硬编码（BigQuery 凭据、LLM API Key 等通过环境变量注入）
- [ ] Prisma migration 文件已提交（不依赖 `migrate dev` 自动创建）
- [ ] `dbt/design/finops-system-design.md` 已更新（BigQuery location 决策、多云 schema 变更）

---

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