# FinOps Agent 平台架构评审报告

> **评审文档**: `data-agent/design/finops-agent-platform-design.md` v1.0 → v1.1
> **初次评审**: 2026-03-10
> **二次评审**: 2026-03-10
> **评审角色**: Principal Architect, Cross-Phase Architecture Review
> **评审范围**: 全 Phase 架构一致性、关键技术决策、跨阶段依赖、性能/安全/技术债务

---

## 二次评审结论 (v1.1)

**总体评级: A- (优秀，通过)**

设计文档 v1.1 已针对初次评审的 12 项发现全部完成修复，无阻塞项。

### 修复追踪

| # | Finding | 严重度 | 状态 | 修复位置 |
|---|---------|--------|------|----------|
| 1 | Next.js 单体架构极限 | Critical | ✅ 已解决 | §7.3 废弃 `instrumentation.ts`，Phase 3 Step 4 飞书 WS 独立进程方案 |
| 2 | LLM 缺统一抽象层 | Critical | ✅ 已解决 | §4.4 统一 LLM 服务层，Phase 1 Task 5A.3 实现 |
| 3 | 通知架构不可扩展 | Critical | ✅ 已解决 | §5.4 事件驱动 NotificationHub + Phase 2 Step 5 实现 |
| 4a | Message.queryResult 膨胀 | Major | ✅ 已解决 | §5.2 已移除 queryResult 字段，Phase 1 inline schema 无此字段 |
| 4b | Prisma 分阶段策略缺失 | Major | ✅ 已解决 | §5.4 迁移计划表，Phase 1/2/3/4 migration 名称独立 |
| 4c | Notification 多态关联反模式 | Major | ✅ 已解决 | §5.2 Notification 改用 `sourceType` + `sourceId`，移除 `alertId`/`reportId` |
| 5 | Cron 端点安全不足 | Major | ✅ 已解决 | Phase 2 Step 6 HMAC 签名 + 幂等检查 + 速率限制 |
| 6 | BigQuery 成本不可控 | Major | ✅ 已解决 | §10.4 预算策略，Phase 2 `bigquery-budget.ts` |
| 7 | LLM 后端选择逻辑脆弱 | Major | ✅ 已解决 | 统一 LLM 服务层解决（Finding #2 一并覆盖） |
| 8 | 会话迁移缺过渡方案 | Minor | ✅ 已解决 | Phase 1 Task 3.6 localStorage 一键迁移 |
| 9 | ECharts 替换策略不完整 | Minor | ✅ 已解决 | 全文统一为 ECharts，架构图已修正，SSR 方案明确 |
| 10 | Redis 触发条件不明确 | Minor | ✅ 已解决 | §4.3 明确触发条件列表 |
| 11 | API 命名不一致 | Minor | ✅ 已解决 | Phase 2/3 任务文档路径已统一 |
| 12 | 多租户数据隔离 | Info | ✅ 已解决 | §10.5 安全设计，Phase 4 CTE 包装模式 |

### 评级变化说明

| 维度 | v1.0 评级 | v1.1 评级 | 说明 |
|------|----------|----------|------|
| 架构完整性 | B | A- | WebSocket 独立进程、LLM 统一层、事件驱动通知三大结构性问题已解决 |
| 数据模型 | B+ | A | 分阶段迁移、多态关联、queryResult 存储三项均已修正 |
| 安全设计 | C+ | A- | 新增 §10.5 安全设计、HMAC cron 鉴权、CTE 权限过滤、BigQuery 预算 |
| 跨 Phase 一致性 | B | A | Phase 1-4 任务文档与平台设计文档的 schema、文件名、migration 名称完全对齐 |

### 剩余关注项（非阻塞）

以下为实施阶段需注意的执行质量问题，不影响设计评审通过：

1. **LLM prompt 管理**: 随 Phase 推进，多场景 prompt 需建立版本管理机制（可在 Phase 2 启动时评估）
2. **HTML 报告模板维护**: Phase 2 实施时评估是否引入模板框架替代手写 HTML
3. **Phase 3 架构再评审**: 飞书集成 + 异常检测上线前建议做一次轻量级实施评审
4. **数据查询分层策略（v1.2 补充）**: Phase 1 起强制执行 dbt mart 优先策略。所有 NL→SQL 查询默认目标为 `finops_cloud_billing` 数据集的 dbt mart 表。原始计费表（`gcp-digital-bi.*`）访问仅限 admin 角色，且需手动确认执行。此策略从安全（防止非授权用户访问原始数据）、成本（mart 表已预聚合，扫描量远小于原始表）、一致性（所有查询基于统一的 dbt 数据口径）三个维度保障数据查询质量。实现详见平台设计文档 §2.2 M1 数据查询策略和 Phase 1 任务 5A.5-5A.8。

---

## 初次评审结论 (v1.0)

> 以下为 v1.0 初次评审的原始记录，保留作为历史参考。

**总体评级: B+ (良好，有条件通过)**

设计文档整体质量较高，对现有系统的局限性分析到位，演进路径清晰。数据模型设计完整、API 分层合理、飞书集成采用 WebSocket 长连接方案是正确的选择。用户画像系统的设计思路有前瞻性。

但存在以下需要关注的结构性风险：

1. Next.js 单体在 Phase 3 面临架构极限，需要提前规划拆分边界
2. LLM 编排层缺乏统一抽象，Phase 2/3 新增的 LLM 调用场景会导致代码碎片化
3. 通知架构采用直写模式，扩展性不足
4. 缺少关键的运行时保护机制（成本熔断、并发控制）

建议在 Phase 1 实施时同步解决 Finding #1、#2、#4，其余可在对应 Phase 启动前处理。

---

## 关键发现

### Finding #1: Next.js 单体在 Phase 3 将达到架构极限

**严重度: Critical**

设计文档第 4.3 节明确提出"Next.js API Routes 已足够支撑当前规模"，这对 Phase 1/2 成立，但 Phase 3 引入了三类本质不同的运行时需求：

| 运行时需求 | Next.js 适配性 | 问题 |
|-----------|--------------|------|
| HTTP API (CRUD) | 原生支持 | 无 |
| WebSocket 长连接 (飞书 WS Client) | 需 hack (`instrumentation.ts`) | Next.js 的 serverless 模型与长连接天然矛盾；`instrumentation.ts` 在 edge runtime 下不可用 |
| Cron Worker (异常检测 + 报告生成) | 通过 HTTP 端点模拟 | 依赖外部 cron 触发，无法保证 exactly-once 语义；长耗时任务可能超出 API Route 超时限制 |

**核心矛盾**: 飞书 WebSocket 长连接是一个有状态的、常驻内存的进程。Next.js 的设计哲学是无状态、请求驱动的。在 `instrumentation.ts` 中启动 WS Client 意味着：
- Next.js 进程重启 = WS 断连（部署、OOM、health check 失败均会触发）
- 多实例部署时每个实例都会建立 WS 连接，造成消息重复处理
- WS Client 的内存泄漏会拖垮整个 Web 服务

**建议**:
- 在 Phase 1 的 `docker-compose.yml` 中预留 `feishu-worker` service 占位（即使不实现）
- Phase 3 必须采用独立进程方案（设计文档已提及"方式 2: 独立进程"，应将其升级为唯一方案而非备选）
- 制定明确的拆分判定标准：当以下任一条件成立时必须拆分
  1. 需要长连接（WebSocket/gRPC streaming）
  2. 任务耗时超过 30 秒
  3. 需要独立伸缩（CPU-bound 异常检测 vs IO-bound Web API）

---

### Finding #2: LLM 编排缺乏统一抽象层

**严重度: Critical**

当前代码（`src/app/api/chat/route.ts`）中 LLM 调用逻辑直接内联在 API Route 中，OpenRouter 和 Gemini 作为两个 if-else 分支处理。设计文档新增了至少四个 LLM 调用场景：

| Phase | 场景 | LLM 用途 |
|-------|------|---------|
| 1 | 分析模式 (Analyst) | 对查询结果进行二次分析 |
| 1 | 图表推荐 (chart-suggester) | 推荐图表类型 |
| 3 | 异常 RCA | 异常根因分析 |
| 3 | 画像学习 (profile-learner) | 从对话中提取用户偏好 |

每个场景需要不同的 system prompt、温度、max tokens，但都需要共用以下基础能力：
- 多后端路由（OpenRouter / Gemini / 未来可能的 Claude）
- 重试与降级策略
- Token 用量追踪与成本计量
- 请求/响应日志

**风险**: 没有统一抽象层，每个场景都会 copy-paste LLM 调用代码，token 成本完全不可控。设计文档中 `updateProfileFromInteraction` 每 20 次交互调用一次 LLM 做画像总结，若缺乏 token 预算控制，自动分析模式 + 画像学习 + 异常 RCA 三者叠加可能导致 LLM 成本失控。

**建议**:
在 Phase 1 实现统一 LLM Service 层：

```
src/lib/llm/
  index.ts         -- LLMService 统一入口
  providers/
    openrouter.ts  -- OpenRouter 适配器
    gemini.ts      -- Gemini 适配器
  types.ts         -- 统一请求/响应类型
  cost-tracker.ts  -- Token 用量追踪
  rate-limiter.ts  -- 每用户/每日 token 预算
```

关键接口设计：
- `llm.complete({ prompt, systemPrompt, model?, temperature?, maxTokens?, budget? })`
- `budget` 参数用于单次调用的 token 上限，防止无限输出
- 所有 LLM 调用通过此层，统一记录到 PostgreSQL（Phase 1 已有），便于成本分析

---

### Finding #3: 通知架构缺乏事件驱动设计

**严重度: Major**

当前 `Notification` 模型采用直写模式：异常检测/报告生成完成后，直接 `prisma.notification.create()` 写入通知。这种设计在 Phase 2（仅 Web 通知）可以工作，但 Phase 3 加入飞书推送、Phase 4 可能加入 Email/Slack 后，存在以下问题：

1. **通知扇出逻辑耦合在业务代码中** -- 异常检测引擎需要知道"给哪些用户推送"、"通过哪个渠道"、"用户通知偏好是什么"，职责边界模糊
2. **缺少重试机制** -- 飞书 API 推送失败后无法自动重试（直写模式下重试 = 重跑整个业务流程）
3. **渠道扩展成本高** -- 每新增一个通知渠道，需要修改所有产生通知的代码路径

**建议**:
引入轻量级事件总线（不必是 Redis/BullMQ，可以是进程内的 EventEmitter + 持久化 outbox）：

```
业务层: anomalyDetector.detect() → emit('anomaly.detected', { alertId, severity, ... })
通知层: NotificationHub.on('anomaly.detected') → 查询订阅者 → 按渠道分发 → 记录状态
```

Phase 2 可先实现进程内 EventEmitter，Phase 3 需要跨进程时再升级为 Redis Pub/Sub 或 BullMQ。关键是在 Phase 2 就建立事件契约（event schema），而非直接调用。

---

### Finding #4: 数据模型跨 Phase 一致性有隐患

**严重度: Major**

数据模型演进路线中存在以下问题：

**(a) Message 表存储查询结果的膨胀风险**

`Message.queryResult` 字段存储为 `Json` 类型，设计文档中 BigQuery 查询最多返回 1000 行（`BIGQUERY_MAX_ROWS`）。假设每行 20 列、每列平均 50 字符，单条 Message 的 queryResult 约 1MB。按设计文档估算 5K messages/月，queryResult 存储量 = ~5TB/年（即使只有 20% 的消息包含结果也有 ~1TB）。

**建议**:
- `queryResult` 不应存储在 Message 表中，应独立为 `QueryResultCache` 表，设置 TTL（如 30 天自动清理）
- Message 只存储 `queryResultId` 引用
- 或者将 queryResult 压缩存储（gzip JSON），并设置大小上限

**(b) UserProfile 在 Phase 1 vs Phase 3 的时序矛盾**

设计文档将 `UserProfile` 放在 Prisma Schema 中（暗示 Phase 1 建表），但画像学习功能在 Phase 3 实现。Phase 1 的 Prisma migration 会创建 `user_profiles` 表，但 Phase 1 没有任何代码读写此表。

**建议**:
- Phase 1 的 Prisma schema 只包含 Phase 1 需要的模型（User, ChatSession, Message）
- Phase 2 migration 新增 Subscription, Report, Notification
- Phase 3 migration 新增 AnomalyRule, AnomalyAlert, UserProfile
- 每个 Phase 的 migration 独立管理，避免预建空表

**(c) Notification 的多态关联设计**

`Notification` 通过可选的 `alertId` 和 `reportId` 实现多态关联。当 Phase 4 引入新的通知来源（如"优化建议"），需要继续加字段（`recommendationId?`）。这是经典的多态关联反模式。

**建议**:
改用 `sourceType` + `sourceId` 的模式：
```prisma
model Notification {
  sourceType  String   // 'anomaly_alert' | 'report' | 'system' | 'recommendation'
  sourceId    String?  // 关联实体的 ID
  // 删除 alertId, reportId
}
```

---

### Finding #5: Cron 端点安全性不足

**严重度: Major**

Cron 端点（`/api/cron/detect-anomalies`, `/api/cron/generate-reports`）仅使用静态 Bearer token 保护：

```typescript
const token = req.headers.get('Authorization')?.replace('Bearer ', '')
if (token !== process.env.CRON_SECRET) { ... }
```

问题：
1. **无 IP 白名单** -- 任何知道 token 的人都可以从任何位置触发
2. **无频率限制** -- 恶意调用可以触发大量 BigQuery 查询，产生高额费用
3. **无幂等性保证** -- 重复触发会重复生成报告/告警
4. **Token 轮换困难** -- 更换 token 需要同时更新环境变量和所有 cron 调度配置

**建议**:
- 增加请求签名机制（timestamp + HMAC），防止 token 泄露后的重放攻击
- 增加执行锁（PostgreSQL advisory lock 或简单的 `last_executed_at` 检查），保证幂等性
- 增加 per-endpoint 的速率限制（如异常检测最多每小时执行一次）
- 生产环境考虑仅允许从内部网络（docker network / VPC）访问 cron 端点

---

### Finding #6: BigQuery 成本控制缺少系统性保护

**严重度: Major**

设计文档引入了三个新的 BigQuery 查询路径，但缺少系统性的成本控制：

| 查询路径 | 频率 | 成本风险 |
|---------|------|---------|
| 用户交互查询 | 按需 | 已有 dry-run 校验 + 1000 行限制，风险低 |
| 异常检测 (6 种规则 x N 账户) | daily | 每日扫描全部 mart 表，查询量与规则数线性增长 |
| 报告生成 (K 个订阅 x 模板 SQL) | daily/weekly/monthly | 查询量与订阅数线性增长 |

按设计文档的异常检测示例 SQL，`mom_increase` 规则需要全表扫描两个月的数据。50 个规则 x 日均运行 = 50 次全表扫描/天。以 `mart_cost_summary` 表 ~100GB 估算，仅异常检测每月 BigQuery 成本 = 50 x 30 x 100GB x $5/TB = ~$750/月。

**建议**:
1. **查询预算机制**: 在 BigQuery 查询前执行 dry-run，检查 `totalBytesProcessed`。设置每日/每月查询字节数预算，超限则跳过并告警
2. **SQL 优化**: 异常检测 SQL 应聚合为单次查询（一次扫描检测所有规则），而非每条规则独立查询
3. **结果缓存**: 多个规则可能查询相同的底层数据（如同月份的 cost by account+service），应缓存中间结果
4. **BigQuery BI Engine / 物化视图**: 对高频查询的 mart 表启用 BI Engine 缓存或创建物化视图

---

### Finding #7: 现有代码中的 LLM 后端选择逻辑脆弱

**严重度: Major**

当前 `src/app/api/chat/route.ts` 中的 LLM 后端选择逻辑：

```typescript
if (process.env.OPENROUTER_API_KEY) {
  // 使用 OpenRouter
} else if (process.env.GEMINI_API_KEY) {
  // 使用 Gemini
} else {
  return error
}
```

这是一个隐性的优先级：谁的环境变量先存在就用谁。问题：
- 同时配置两个 key 时，Gemini 永远不会被使用（但用户可能在请求中指定了 Gemini 模型）
- 没有降级机制：OpenRouter 不可用时不会自动 fallback 到 Gemini
- 现有代码与设计文档的多模型未来（Phase 3 画像学习可能需要不同模型）不兼容

**建议**: 此问题应在 Phase 1 的 LLM 抽象层（Finding #2）中一并解决。路由策略应基于请求中的 `model` 参数而非环境变量存在性。

---

### Finding #8: 会话持久化迁移缺少过渡方案

**严重度: Minor**

当前所有会话存储在浏览器 `localStorage`（key: `finops.chatHistory.v1`），Phase 1 要迁移到 PostgreSQL。设计文档未描述迁移过渡策略：
- 已有的 localStorage 数据如何导入 PostgreSQL？
- 迁移期间是否需要双写（localStorage + API）？
- 前端如何判断使用哪个存储？

**建议**:
- 提供一次性 "Import History" 功能：用户首次登录 Phase 1 版本时，前端检测 localStorage 数据，通过 API 批量上传到 PostgreSQL，成功后清除 localStorage
- 当前已有 `handleExportHistory` / `handleImportHistory`（JSON 文件导入导出），可以复用这个机制
- 设置迁移截止期（如 Phase 1 上线 30 天后移除 localStorage 读取逻辑）

---

### Finding #9: ECharts vs Recharts 替换策略不完整

**严重度: Minor**

设计文档提出用 ECharts 替换 Recharts，理由充分（图表类型丰富、大数据量性能好）。但存在以下问题：

1. **Bundle size**: ECharts 全量包约 1MB（gzipped ~300KB），Recharts 约 100KB。对于 SPA 首屏加载影响显著
2. **SSR 兼容性**: 设计文档第 3.1 节架构图中出现 "Chart Renderer (Recharts SSR)"，但正文说替换为 ECharts。ECharts 的 SSR 支持需要 `echarts/ssr` + canvas/svg 模式，配置更复杂
3. **报告引擎中的图表渲染**: Phase 2 报告需要服务端渲染图表为静态图片（用于飞书卡片和 HTML 邮件），ECharts 的 Node.js 端渲染需要 `node-canvas` 或 headless browser

**建议**:
- ECharts 采用按需引入（tree-shaking）：`import * as echarts from 'echarts/core'` + 仅注册需要的图表类型，可将 bundle 缩小到 ~150KB
- 修正架构图中的 "Recharts SSR" 为 "ECharts"，保持文档一致性
- 报告引擎的服务端图表渲染建议使用 ECharts 的 SVG 模式（无需 canvas 依赖），输出 SVG 字符串嵌入 HTML

---

### Finding #10: Redis "可选" 定位需要明确触发条件

**严重度: Minor**

Redis 在设计文档中标记为"可选"，对应 BullMQ 队列。但没有明确在什么条件下从"可选"变为"必需"。

**触发条件建议**:
| 条件 | 触发 Redis 引入 |
|------|---------------|
| Cron 任务耗时超过 API Route 超时限制 (60s) | 需要 BullMQ 异步处理 |
| 需要任务重试 + 死信队列 | 报告生成失败需要自动重试 |
| 多实例部署 | 需要分布式锁防止重复执行 |
| 飞书消息处理需要排队 | 高并发时防止消息丢失 |
| API 需要速率限制 | 基于 Redis 的 sliding window |

建议在 Phase 2 的 Cron 任务中评估是否需要引入 Redis。如果生产环境使用 db9.ai 托管，确认该平台是否支持 Redis 服务。

---

### Finding #11: API 命名约定存在不一致

**严重度: Minor**

| API | 风格 | 问题 |
|-----|------|------|
| `/api/sessions/[id]` | RESTful 资源路径 | 正确 |
| `/api/anomalies/rules` | 嵌套资源 | `anomalies` 是父资源？还是 namespace？混淆了 anomaly alert 和 anomaly rule 的关系 |
| `/api/cron/detect-anomalies` | RPC 风格 (动词) | 与 RESTful 风格不一致 |
| `/api/notifications/read-all` | RPC 风格 (动词) | 与 PATCH 语义冲突 |
| `/api/reports/generate` | RPC 风格 (动词) | 同上 |

**建议**:
- Cron 端点保持 RPC 风格（它们本质上是命令，不是资源操作），但统一前缀：`/api/internal/cron/...`（`internal` 标识这些端点不面向用户）
- 批量操作统一用 `POST /api/notifications/bulk` + body `{ action: 'markRead', ids: [...] }`
- Anomaly 路径改为 `/api/anomaly-rules` 和 `/api/anomaly-alerts`，避免 anomalies 这个歧义前缀

---

### Finding #12: 多租户数据隔离需要 defense-in-depth

**严重度: Info**

设计文档的用户量级约 50 人，均为内部用户。但以下场景仍需数据隔离：
- 工程 TL 只应看到自己团队的账户数据
- 管理层可以看到全部数据
- BigQuery 查询的 SQL 是 LLM 生成的，无法保证包含正确的 account_id 过滤

Phase 4 提到"基于 account_mapping.access_level 的数据权限"，但此时 Phase 1-3 已经运行了大量无权限控制的查询。

**建议**:
- Phase 1 实施时在 BigQuery 查询层增加一个 SQL 后处理步骤：检查 LLM 生成的 SQL 是否包含 account_id 过滤，如果用户配置了 `focusAccountIds`，自动注入 WHERE 条件
- 这不是权限控制（Phase 4），而是一个"默认安全"的最小保护措施
- 同时，BigQuery 查询应使用参数化查询而非字符串拼接，防止 SQL 注入

---

## 跨 Phase 依赖分析

### Phase 依赖图

```
Phase 1 (基础设施 + 查询增强)
  │
  ├──→ Phase 2 (订阅推送 + 报告)
  │      │  依赖: User, PostgreSQL, Notification 基础
  │      │
  │      └──→ Phase 3 (异常检测 + 飞书)
  │             │  依赖: Notification Hub, Cron 基础设施, User 体系
  │             │
  │             └──→ Phase 4 (高级功能)
  │                    依赖: 全部
  │
  └──→ Phase 3 的飞书 Bot (被动查询) 实际只依赖 Phase 1 的 Query Engine
```

### Phase 2 被跳过时的影响

如果 Phase 2 被延期：
- **Phase 3 异常检测**: AnomalyAlert 可以独立工作，但告警推送依赖 Notification 模型（Phase 2 引入）。**需要将 Notification 模型前移到 Phase 1 或 Phase 3 启动时。**
- **Phase 3 飞书 Bot 被动查询**: 不依赖 Phase 2，可独立启动。
- **Phase 3 飞书主动推送**: 依赖 Notification Hub（Phase 2），如果跳过 Phase 2 则飞书推送无法实现。

**建议**: 将 Notification 基础模型（不含订阅/报告逻辑）从 Phase 2 下沉到 Phase 1，作为核心基础设施。这样 Phase 2 和 Phase 3 可以真正独立交付。

### 可考虑的 Phase 调整

| 调整 | 理由 |
|------|------|
| 飞书 Bot 被动查询移入 Phase 2 | 被动查询只依赖 Query Engine (Phase 1)，技术难度低，用户价值高 |
| 用户画像移入 Phase 4 | Phase 3 已经很重（异常检测 + 飞书推送），画像学习是锦上添花 |
| ECharts 图表简化为 Phase 1.5 | Phase 1 可先用静态图表（基于现有 Recharts），Phase 1.5 切换 ECharts |

---

## 性能与扩展性分析

### BigQuery 查询延迟预算

| 场景 | 预算 | 瓶颈 |
|------|------|------|
| 交互查询 (数据模式) | < 10s E2E | BigQuery 查询 3-8s（冷启动更长），可接受 |
| 交互查询 (分析模式) | < 20s E2E | BigQuery 3-8s + LLM 分析 5-10s = 13-18s，紧张但可接受 |
| 分析模式双 LLM 调用 | < 25s | NL→SQL (LLM1) + 执行 + 结果分析 (LLM2)，可能需要流式响应 |
| 异常检测 (全规则集) | < 5min | 50 规则顺序执行，可优化为批量 SQL |
| 报告生成 | < 2min/报告 | 查询 + 渲染，可并行处理多订阅 |

**关键风险**: 分析模式涉及两次 LLM 调用，总延迟可能超过用户耐心阈值。建议：
- 第一次 LLM 调用（NL→SQL）返回后立即展示 SQL + 表格
- 第二次 LLM 调用（结果分析）采用 SSE 流式推送，渐进展示分析文本

### PostgreSQL 连接池

以 ~50 用户、Next.js 单实例部署为前提：
- 并发查询峰值估计 ~10
- Prisma 默认连接池大小 = `num_cpus * 2 + 1`（2 核 = 5 连接）
- Phase 3 飞书 WS Client 独立进程需要额外连接池
- **建议**: 显式配置 `DATABASE_URL` 的 `connection_limit` 参数，开发环境 5，生产环境 10-20

### 前端 Bundle Size

当前 `package.json` 依赖分析：

| 包 | 估计 gzipped size | 备注 |
|---|---|---|
| react + react-dom | ~45KB | 基础 |
| next | ~90KB | 框架 |
| recharts (现有) | ~100KB | 将被替换 |
| echarts (计划) | ~300KB (全量) | 需要按需引入降到 ~150KB |
| @tanstack/react-table | ~15KB | |
| react-markdown + rehype | ~30KB | |
| xlsx (SheetJS) | ~90KB | Phase 1 新增，需要 tree-shaking |

总计约 ~720KB gzipped（含 ECharts 全量）。建议：
- ECharts 和 xlsx 通过 `next/dynamic` 延迟加载，不进入首屏 bundle
- 目标首屏 bundle < 350KB gzipped

---

## 安全审查

### SQL 注入防护

当前 BigQuery 查询（`src/app/api/bigquery/route.ts`）直接接受前端传入的 SQL 字符串执行。虽然有 `isReadOnlySql()` 检查防止写操作，但存在以下风险：

1. **数据泄露**: 恶意 SQL 可以查询非 FinOps 数据集（用户构造 `SELECT * FROM other_project.other_dataset.sensitive_table`）
2. **资源耗尽**: 构造无 partition filter 的全表扫描

**建议**:
- 增加 SQL 白名单检查：只允许查询特定的 BigQuery project/dataset
- BigQuery 查询使用专用 Service Account，权限限制为只读 + 仅可访问 FinOps 相关 dataset
- 设置 BigQuery 查询的 `maximumBytesBilled` 参数作为硬性上限

### 飞书 WebSocket 安全

WebSocket 长连接使用 `appId` + `appSecret` 认证，由 `@larksuiteoapi/node-sdk` 内部处理。安全性取决于：
- `FEISHU_APP_SECRET` 的保密性（环境变量注入，不入代码库）
- SDK 内部的 TLS 支持（飞书官方 SDK 默认使用 wss://）
- 消息来源验证（SDK 内部处理，无需额外签名校验）

**评估**: 安全性充分，无需额外措施。

### 绑定链接安全

设计文档的飞书-Google 账号绑定流程中，绑定链接包含 `feishu_open_id` 和 `token`。需要确保：
- Token 使用加密安全的随机数生成
- Token 存储在 PostgreSQL 中，带过期时间（15 分钟，已记录）
- 绑定完成后 token 立即失效
- 防止 CSRF：绑定操作需要验证 OAuth session

**评估**: 设计合理，实现时注意上述细节。

---

## 技术债务预测

### 高逆转成本决策

以下决策一旦实施，后续修改代价较大：

| 决策 | 逆转成本 | 缓解措施 |
|------|---------|---------|
| Prisma 作为 ORM | 高 -- Schema 迁移紧密耦合 | 确保 Prisma schema 是唯一 truth，不直接操作数据库 |
| PostgreSQL 而非其他数据库 | 中 -- Prisma 支持多数据库，但 migration 不可复用 | 当前选择正确，无需缓解 |
| ECharts 替代 Recharts | 中 -- 图表组件全部重写 | 封装图表组件接口，降低具体库耦合 |
| 飞书 WebSocket 而非 Webhook | 低 -- 两种模式可互切 | 当前选择正确 |
| NextAuth 作为认证层 | 高 -- 全部 API Route 依赖 session 模式 | Phase 1 实施时考虑是否需要升级到 Auth.js v5 |

### 预期技术债务

| 时期 | 债务类型 | 来源 |
|------|---------|------|
| Phase 1 完成后 | localStorage 残留代码 | 迁移过渡期的双读逻辑 |
| Phase 2 完成后 | HTML 报告模板维护负担 | 手写 HTML 模板不可持续，需要模板引擎 |
| Phase 3 完成后 | LLM prompt 碎片化 | 各模块各自维护 system prompt，缺乏统一管理 |
| Phase 3 完成后 | Next.js 单体的运维复杂度 | 长连接 + Cron + Web 混合运行 |
| Phase 4 开始前 | 权限补丁 | Phase 1-3 无权限控制，Phase 4 需要全量改造 |

### 缓解策略

1. **Phase 1 投资**: 在 Phase 1 建立 LLM 抽象层和通知事件契约，这是后续 Phase 的地基
2. **Phase 2 重构窗口**: 报告引擎采用 React Email 或 MJML 等模板框架，而非手写 HTML
3. **Phase 3 前评审**: 在 Phase 3 启动前重新评审架构，决定是否拆分为独立进程
4. **持续监控**: 每个 Phase 上线后监控 LLM token 消耗和 BigQuery 费用趋势

---

## 改进建议汇总

### 优先级 P0 (Phase 1 必须完成)

| # | 建议 | 关联 Finding |
|---|------|-------------|
| 1 | 实现统一 LLM Service 抽象层 | #2, #7 |
| 2 | Prisma schema 分 Phase 管理，Phase 1 只建 User + ChatSession + Message | #4b |
| 3 | queryResult 独立存储，设置 TTL 和大小上限 | #4a |
| 4 | BigQuery 查询增加 dataset 白名单 + `maximumBytesBilled` 硬限制 | #6, #12 |

### 优先级 P1 (Phase 2 启动前完成)

| # | 建议 | 关联 Finding |
|---|------|-------------|
| 5 | 引入事件驱动的通知架构（至少定义事件契约） | #3 |
| 6 | Notification 模型改用 sourceType + sourceId 多态 | #4c |
| 7 | Cron 端点增加执行锁 + 签名机制 | #5 |
| 8 | 前端 ECharts / xlsx 延迟加载 | #9 |

### 优先级 P2 (Phase 3 启动前完成)

| # | 建议 | 关联 Finding |
|---|------|-------------|
| 9 | 飞书 WS Client 确定为独立进程方案（非 instrumentation.ts） | #1 |
| 10 | 异常检测 SQL 批量化，增加查询预算机制 | #6 |
| 11 | 明确 Redis 引入触发条件 | #10 |
| 12 | API 路径命名统一化 | #11 |

### 优先级 P3 (可选优化)

| # | 建议 | 关联 Finding |
|---|------|-------------|
| 13 | 飞书 Bot 被动查询从 Phase 3 前移至 Phase 2 | 跨 Phase 依赖 |
| 14 | 用户画像从 Phase 3 后移至 Phase 4 | 跨 Phase 依赖 |
| 15 | localStorage 历史一键迁移功能 | #8 |

---

## 附录

### A. 当前代码库结构分析

```
data-agent/src/
  app/
    api/
      auth/[...nextauth]/route.ts  -- NextAuth 认证端点
      bigquery/route.ts            -- BigQuery 执行 (dry-run + execute)
      chat/route.ts                -- LLM 编排 (NL→SQL)
    auth/signin/                   -- 登录页面
    page.tsx                       -- 主页面 (聊天 UI + localStorage 管理)
    layout.tsx                     -- 根布局
    providers.tsx                  -- SessionProvider 包装
  components/
    chat/
      history-panel.tsx            -- 会话历史侧边栏
      message-bubble-content.tsx   -- 消息气泡内容渲染
    result-table.tsx               -- 查询结果表格 (TanStack)
    sql-viewer.tsx                 -- SQL 代码展示
  hooks/
    use-auto-scroll.ts             -- 自动滚动 hook
  lib/
    auth.ts                        -- NextAuth 配置
    auth-allowed.ts                -- 邮箱/域名白名单
    bigquery-request.ts            -- BigQuery 请求解析/校验
    chat-request.ts                -- Chat 请求解析/校验
    chat-session.ts                -- 会话类型定义 + localStorage 工具
    docs-loader.ts                 -- 文档上下文加载器 (LLM prompt 注入)
  middleware.ts                    -- NextAuth 中间件
```

**代码量评估**: 约 1,500 行 TypeScript。代码质量良好，函数职责单一，类型定义清晰。`page.tsx` (765 行) 是最大的文件，承载了过多的状态管理逻辑，Phase 1 迁移到 PostgreSQL 时应拆分为自定义 hooks。

### B. 数据模型实体关系完整性检查

| 关系 | FK | Cascade | 完整性 |
|------|------|---------|--------|
| User → ChatSession | userId | onDelete: Cascade | OK |
| User → Subscription | userId | onDelete: Cascade | OK |
| User → AnomalyRule | userId | onDelete: Cascade | OK |
| User → Notification | userId | onDelete: Cascade | OK |
| User → UserProfile | userId | onDelete: Cascade | OK (1:1) |
| ChatSession → Message | sessionId | onDelete: Cascade | OK |
| Subscription → Report | subscriptionId | onDelete: Cascade | OK |
| AnomalyRule → AnomalyAlert | ruleId | onDelete: Cascade | OK |
| Notification.sourceType/sourceId | 多态引用 | 无 FK 约束 | OK (通用多态，v1.1 替代 alertId/reportId) |

**结论**: 无循环依赖，外键关系完整，级联删除策略合理。Notification 的 `sourceType` + `sourceId` 采用通用多态模式（无 FK 约束），通过应用层维护引用完整性，支持未来扩展新的通知来源而无需 migration。

### C. 环境变量增长趋势

| Phase | 新增环境变量 | 累计 |
|-------|------------|------|
| 现有 | 10 (Google OAuth, LLM, BigQuery) | 10 |
| Phase 1 | 1 (DATABASE_URL) | 11 |
| Phase 2 | 1 (CRON_SECRET) | 12 |
| Phase 3 | 3 (FEISHU_APP_ID, FEISHU_APP_SECRET, FEISHU_WS_ENABLED) | 15 |

环境变量增长可控。建议使用 `.env.example` 文件记录所有变量及其说明，每个 Phase 更新。

### D. docker-compose.yml 现状 vs 设计对比

**现状** (`docker-compose.yml`):
- 仅包含 `data-agent` 单服务
- 无 PostgreSQL、无 Redis
- Volume 挂载 SA key 文件

**设计目标**:
- 新增 `postgres` 服务（Phase 1）
- 新增 `feishu-worker` 服务（Phase 3，建议）
- 可选 `redis` 服务（Phase 3）
- 新增 `pgdata` volume

差距合理，符合渐进式演进。

---

*初次评审完成: 2026-03-10 (v1.0, 评级 B+)*
*二次评审完成: 2026-03-10 (v1.1, 评级 A-, 12/12 项已解决)*
