# 北极星路线图：独立调试一条 TiDB 集群 Alert

> 最后更新：2026-03-04（代码分析：Gap 1/2/3/5 核心实现已落地，剩余卡点见下方）  
> 目标：从用户在 Investibook 中输入一个 Alert ID（如 `O11Y-1956041`），到 Agent 能在 Chat 中真正调用 O11Y / Clinic MCP 分析该集群，全程不需要手动操作文件系统或配置文件。

---

## 北极星指标定义

**用户旅程（Happy Path）**：

1. 用户打开 Investibook，创建或进入一个 Workspace
2. 在 Sources 面板点击 "+" → 输入 Alert ID（如 `O11Y-1956041`）→ 点击 Add
3. 系统自动拉取该 Alert 关联的集群元数据（cluster_id、告警时间窗口等）
4. Agent 启动时自动加载集群上下文 + 真实 MCP 工具
5. 用户在 Chat 中发问（如"分析这次慢查询的根因"）
6. Agent 调用 O11Y / Clinic MCP 查询真实集群数据，返回带图表/截图的分析结论

**验收标准**：
- [ ] Agent 回答中能引用到真实的 `cluster_id` 和时间窗口数据
- [ ] Agent 能调用至少一个 O11Y 或 Clinic MCP 工具并返回实际结果
- [ ] 整个流程不需要手动 ssh / 写文件

---

## 分阶段里程碑

达成以下里程碑可以逐步建立链路可信度，不需要等所有 Gap 全部关闭：

| 里程碑 | 条件 | 意义 |
|--------|------|------|
| **M1：MCP 首次连通** | Gap 3 + Gap 2（任意 1 个 MCP）完成 | 证明 Agent 能访问 TiDB Cloud 内部平台真实数据；用户手动在 Chat 中提供 cluster_id 即可调试任意告警，端到端链路可行 |
| **M2：Alert 数据自动入库** | Gap 1 完成（cluster_meta 自动写入） | Alert ID → cluster_id / tenant_id 自动化，无需用户手动提供；M1 + M2 = 完整自助体验 |
| **M3：完整分析链路** | M1 + M2 + Gap 4 | session 重启后不失忆，支持跨多天的 Alert 长线分析 |
| **M4：体验闭环** | M3 + Gap 5 | Sources 面板有进度反馈，用户无需猜测数据准备状态 |

> **当前优先级建议**：**M1 优先于 M2**。Gap 3 + Gap 2（最小 MCP 验证）可独立推进，不依赖 Gap 1；一旦 MCP 通了，哪怕 Gap 1 还没做，用户已经可以在 Chat 中提供 cluster_id 手动调试告警，实质价值已经存在。

---

## 当前状态（2026-03-04）

### ✅ 已就绪

| 模块 | 说明 |
|------|------|
| Alert UI 入口 | `AddSourceModal` 接受 Jira ID 格式，提交后调用 `POST /api/sources/prepare`，source 置 `loading` 状态 |
| cluster_meta.json 读取 | `_load_cluster_metadata()` 能从 workspace 目录解析集群 ID / 时间窗口，自动注入 system prompt |
| MCP config 解析 + **注入** | `_load_mcp_servers_config()` 已实现并**实际调用**，`new_session`/`resume_session` 传入真实 `mcp_servers` |
| `.opencode/opencode.json` 模板 | `templates/example/.opencode/opencode.json` 已存在（含 clinic-screenshot / clinic / devops / resource-worker-mcp） |
| System prompt MCP 文字上下文 | agent 会收到 "你有哪些 MCP 可用" 的提示文字，每条消息也附加 MCP 工具使用策略 |
| ACP Session 基础设施 | OpenCode / Claude agent subprocess 管理、SSE 流式输出、截图渲染全部就绪 |
| SQLite 持久化 | workspace / sources / artifacts / messages / session 全部持久化 |
| Alert 数据准备后端 | `services/source_preparer.py` 完整实现（Jira→cluster_id、Clinic→cluster_meta、Guardian/S3→insight） |
| Prepare API | `POST /api/sources/prepare` + `GET /api/sources/prepare/{task_id}` 已实现 |
| 凭证配置 | `services/config.py` 支持 `INVESTIBOOK_` 前缀环境变量和 YAML 文件两种注入方式 |
| SourceCard 三态 UI | loading spinner / ready ✓ / error ⚠ 已实现；`Source` interface 含 `status`、`statusMessage`、`taskId` |

### ❌ 剩余卡点（当前阻塞项）

见下节详细拆解。

---

## Gap 分析与任务清单

### Gap 1 — Alert 数据准备后端（自动化体验，非 MCP 前置条件）

**Alert Source 数据来源设计**（已明确）：

一条 Alert Source 的数据分两层，优先级不同：

| 层级 | 数据来源 | 内容 | 可用性 |
|------|----------|------|--------|
| **必须** | Jira API | 告警基础信息（cluster_id、tenant_id、alert_type、alert_time、告警描述） | 所有 alert 都有 |
| **可选** | S3（Guardian Insight） | `diagnostic_package.json`（原始诊断数据）、`insight.json`（分析报告） | 仅当 Guardian Insight 服务对该 alert 跑过分析时存在 |

**状态：核心代码已完成，凭证配置待验证。**

**已实现**：`backend/services/source_preparer.py` 完整实现了三步流水线：
1. Jira API（`customfield_10160 → labels.tidb_cluster_id / o11y_tenant_id`）
2. Clinic HTTP API 拉取集群元数据
3. Guardian API + S3 下载 insight/diagnostic（路径模式：`guardian_service/analysis/{tenant}/{cluster}/{YYYY_MM}/{uuid}/insight.en.json`）

**任务**：

- [x] **`backend/services/source_preparer.py`** — `prepare_alert_source()` 完整实现（Jira + Clinic + Guardian/S3）
- [x] **`POST /api/sources/prepare`** — 已实现，异步启动准备任务，返回 `{task_id}`
- [x] **`GET /api/sources/prepare/{task_id}`** — 已实现，返回 task 状态
- [x] **凭证配置** — `services/config.py` 支持 `INVESTIBOOK_JIRA_*` / `INVESTIBOOK_CLINIC_*` / `INVESTIBOOK_S3_*` 环境变量

**剩余**：
- [ ] **生产凭证注入** — `INVESTIBOOK_JIRA_API_TOKEN`、`INVESTIBOOK_CLINIC_API_KEY`、`INVESTIBOOK_S3_BUCKET` 需写入 K8s Secret 并挂载到 backend pod
- [ ] **端到端验证** — 在真实环境用实际 Alert ID 跑一次，确认 `cluster_meta.json` 内容正确

**验证方式**：准备完成后，`data/workspaces/{ws-id}/cluster_meta.json` 里有正确的 `cluster_id` 和 `tenant_id`；无 Guardian Insight 的 alert 也能正常完成（不报错）。

---

### Gap 2 — MCP servers 没有实际注册给 Agent【可行性里程碑关键点】

> **为什么重要**：MCP 接入是整条链路可行性的核心信心来源。哪怕只连通一个 TiDB Cloud 内部平台（如 Ops Portal / Clinic），就能证明"Agent 能查询真实集群数据"的端到端链路是通的。在此之前，所有 Chat 输出都只是 Agent 的推断，没有真实数据支撑。

**状态：代码修复已完成，实际连通性待验证。**

`ACPSession.initialize()` 已修复，`_load_mcp_servers_config()` 现在被正确调用，`new_session` 和 `resume_session` 都传入了从 `.opencode/opencode.json` 读取的真实 `mcp_servers`。

**任务**：

- [x] **`ACPSession.initialize()` 注入真实 MCP servers** — 已修复，`new_session`/`resume_session` 均传入 `mcp_servers`
- [ ] **最小可行验证（先跑通 1 个 MCP）** — 优先选最容易接入的一个服务（如 `devops` MCP），在真实 K8s pod 中验证 Agent 能调用工具并返回真实数据
- [ ] **验证 MCP 进程在 pod 内可达** — 确认：
  - `clinic-screenshot`（`/opt/aiops/.venv/bin/python -m aiops.service.diagnostic.mcp_server`）在 pod 内可执行
  - `clinic` / `devops`（`/opt/toolset/aiops-mcp-demo/`）在 pod 内可执行
  - 所需 Python 包是否已打包进 backend 镜像（或 sidecar）

**验证方式**：Chat 中发问后，backend 日志出现 `[OpenCode stderr] tool_call: ...`（任意真实 MCP 工具），前端 tool call 卡片正常展示，返回真实集群数据。

---

### Gap 3 — `.opencode/opencode.json` 没有自动生成（配置基础设施缺失）

**状态：静态复制已实现，但 token 渲染缺失——当前是最高优先级卡点。**

`source_preparer.py` 在 Alert 准备完成后会 `shutil.copytree` 模板 `.opencode/` 到 workspace 目录，路径已通。但 `templates/example/.opencode/opencode.json` 里的 token 是硬编码占位符：
- `clinic` MCP：`CLINIC_COOKIE=clinic.session_id=REPLACE_ME`，`CLINIC_CSRF_TOKEN=REPLACE_ME`
- `devops` MCP：`DEVOPS_API_KEY=${DEVOPS_API_KEY}`（shell 语法，Python 不会渲染）

同时存在一个 `opencode.json.template`（含 `{{AIOPS_ROOT}}` 占位符）但**未被任何代码使用**。

**需要确认**：
- token 从 K8s Secret 注入的变量名约定（如 `CLINIC_API_KEY`、`DEVOPS_API_KEY`）
- 是否需要按用户 email 映射不同 token

**任务**：

- [x] `source_preparer.py` 复制 `.opencode/` 模板到 workspace 目录
- [x] `templates/example/.opencode/opencode.json.template` 已存在（含占位符）
- [ ] **🔥 token 渲染** — `source_preparer.py` 复制完 `.opencode/opencode.json` 后，按 `settings`（`config.py`）中的值替换 `CLINIC_COOKIE`、`CLINIC_CSRF_TOKEN`、`DEVOPS_API_KEY` 等占位符；或直接 `generate_opencode_json()` 函数动态生成文件
- [ ] **K8s Secret 接入** — `deployment/kustomize/` 新增或更新 Secret，将 Clinic / Devops token 以 `INVESTIBOOK_CLINIC_*` / `INVESTIBOOK_DEVOPS_*` 环境变量形式注入 backend pod

**验证方式**：创建 workspace 后，`cat data/workspaces/{ws-id}/.opencode/opencode.json` 中 token 字段为真实值（非 `REPLACE_ME`）。

---

### Gap 4 — OpenCode Session 上下文丢失（Pod 重启后 Agent 失忆）

**问题**：当前 `ACPSession` 是纯内存对象，backend pod 重启后所有 `sessions` 字典清空。虽然 SQLite 里存了 `acp_session_id`，resume 时也会尝试 `conn.resume_session(...)`，但 OpenCode 进程本身是重新启动的新进程，**它的内部状态（工具调用历史、reasoning context）并不会从磁盘恢复**。

结果：用户在界面上能看到历史聊天记录（来自 `workspace_messages` 表），但 Agent 侧是全新 context，不知道之前分析了哪些数据、得出了什么结论。对于 Alert 长线分析（可能跨多个工作日），这是严重的上下文断裂。

**根因参考**：ACP [Stateful Agents](https://agentcommunicationprotocol.dev/core-concepts/stateful-agents) — session 持久化依赖 agent 进程将状态写入磁盘，resume 时从磁盘恢复，而不是依赖 backend 进程内存。OpenCode 是否支持此模式需确认。

**需要确认**：
- OpenCode 是否支持 ACP session 磁盘持久化？session 数据写到哪个目录（`cwd` 下的 `.opencode/` 还是其他路径）？
- `resume_session` 的成功率——目前代码中 resume 失败后会静默 fallback 到 `new_session`，实际生产中 resume 成功过吗？
- Claude agent 是否有同样的问题？

**任务**：

- [ ] **确认 OpenCode session 磁盘持久化机制** — 查阅 OpenCode ACP 文档或源码，确认 session 文件存储路径；如果 `cwd`（即 `data/workspaces/{workspace-id}/`）下已有 session 文件，resume 是否能真正恢复 context
- [ ] **`ACPSession.initialize()` 增强 resume 日志** — 区分 "resume 成功恢复 context" 和 "resume 失败 fallback 到 new_session" 两种情况，前端通过 SSE 提示用户（如 "Session restored" vs "New session started"）
- [ ] **Pod 重启恢复验证** — 写一个集成测试：发送 5 轮对话 → kill backend 进程 → 重启 → 发送第 6 轮，验证 Agent 能引用第 1-5 轮的内容
- [ ] **Fallback 策略** — 若 resume 不可行（OpenCode 不支持），则在 resume 失败时自动将 `workspace_messages` 历史以 "conversation replay" 方式重新注入新 session（作为 system prompt 附录）

**验证方式**：重启 backend pod 后，在 Chat 中问 "我们之前分析到哪了？"，Agent 能正确引用之前讨论的 cluster_id 和异常结论。

---

### Gap 5 — SourceCard 状态 UI（用户体验阻塞）

**状态：UI 层已完成，前端轮询逻辑缺失——source 状态永远停留在 loading。**

**任务**：

- [x] **`lib/workspace.tsx`** — `Source` interface 新增 `status`、`statusMessage`、`taskId` 字段
- [x] **`components/SourcesPanel/SourceCard.tsx`** — 三态展示：`loading` → spinner + "Preparing data…"；`ready` → 绿色 ✓；`error` → 红色 ⚠ + `statusMessage`
- [x] **`components/SourcesPanel/AddSourceModal.tsx`** — 提交后调用 `POST /api/sources/prepare`，将 source 状态置 `loading`，`taskId` 写入 source
- [ ] **🔥 `WorkspaceProvider` 内启动轮询** — 对所有 `status === 'loading'` 且有 `taskId` 的 source，每 2s 轮询 `GET /api/sources/prepare/{task_id}`；完成后调用 `updateSource()` 将状态更新为 `ready`（或 `error`），并触发文件树刷新

---

## 依赖链

```
【当前状态 2026-03-04】

  ✅ Gap 1（核心代码完成）        ✅ Gap 2（代码修复完成）
  Jira → cluster_meta.json ✅     new_session(mcp_servers=[...]) ✅
  Clinic → cluster_meta ✅        ❌ Gap 3：token 占位符未渲染 ← 🔥最高优先级
  Guardian/S3 → insight ✅        .opencode/opencode.json token = REPLACE_ME
          │                                    │
          │  ❌ 生产凭证未注入                │
          │  (JIRA_API_TOKEN 等)              │  需渲染 CLINIC/DEVOPS token
          │                                   │
          └──────────────┬────────────────────┘
                         ▼
             Agent 有集群上下文 + MCP 配置文件存在
             但 MCP 进程鉴权 token 是 REPLACE_ME ❌
                         │
                         ▼
             ❌ 需验证：MCP 进程在 K8s pod 内可启动
                         │  /opt/aiops / /opt/toolset 路径是否存在？
                         ▼
             Agent 调用真实 MCP tool ← 目标
                         │
                         ▼
             ❌ Gap 4: Pod 重启后 session context 丢失（fallback 未实现）
                         │
                         ▼
             ❌ Gap 5: 前端轮询缺失，source 状态永远停在 loading ← 🔥
                         │
                         ▼
             ✅ Chat 流式返回分析结论 + 截图渲染
```

---

## 工作量估算（更新至 2026-03-04）

| # | 任务 | 状态 | 剩余工时 |
|---|------|------|----------|
| Gap 3 | `opencode.json` token 渲染（`source_preparer.py` + K8s Secret） | ❌ 🔥 最高优先级 | **~0.5 人天** |
| Gap 5 | `WorkspaceProvider` 轮询 `GET /api/sources/prepare/{task_id}` | ❌ 🔥 纯前端 | **~0.5 人天** |
| Gap 2 | 验证至少 1 个 MCP（devops）在 K8s pod 内端到端跑通 | ❌ 待验证 | **~1 人天** |
| Gap 1 | 生产凭证注入（Jira/Clinic/S3 secret 写入 K8s） | ❌ 待部署 | **~0.5 人天** |
| Gap 4 | Resume fallback：失败时将历史消息 replay 注入新 session | ❌ 未开始 | **~1.5 人天** |
| — | 端到端集成测试 | ❌ 未开始 | **~1 人天** |

**剩余关键路径：~5 人天**（Gap 3 + Gap 5 最快可以立即动手，无外部依赖）

> 原估算中 Gap 1（~3天）、Gap 2+3 注入部分（~1天）、Gap 5 UI 部分（~0.5天）已完成，节省约 4.5 人天。

---

## 待确认外部依赖（更新至 2026-03-04）

已解决：
- ✅ Jira API 字段映射：`customfield_10160`（raw alert JSON）→ `labels.tidb_cluster_id` / `labels.o11y_tenant_id`
- ✅ S3 路径规则：`guardian_service/analysis/{tenant_id}/{cluster_id}/{YYYY_MM}/{analysis_uuid}/insight.en.json`
- ✅ 配置方式：`INVESTIBOOK_` 前缀环境变量或 `config.yaml`

仍需确认：

1. **MCP token K8s Secret** — `CLINIC_COOKIE`/`CLINIC_CSRF_TOKEN` 或 `CLINIC_API_KEY` 的实际变量名和 Secret 名称；`DEVOPS_API_KEY` / `DEVOPS_BASE_URL` 的值；是否已有 External Secret 可复用？
2. **镜像依赖** — `aiops.service.diagnostic.mcp_server`（路径 `/opt/aiops`）和 `aiops_mcp_demo`（路径 `/opt/toolset/aiops-mcp-demo`）是否已打包进 backend 镜像？还是需要 sidecar？
3. **OpenCode Session 持久化** — `resume_session` 在新进程中能否真正恢复上下文？session 文件写入 `cwd`（workspace 目录）下哪个位置？（影响 Gap 4 fallback 方案选型）

---

## 参考文件

- 告警数据模板：[backend/templates/example/cluster_meta.json](../backend/templates/example/cluster_meta.json)
- MCP 配置文件模板：[backend/templates/example/.opencode/opencode.json](../backend/templates/example/.opencode/opencode.json)
- Alert 数据准备逻辑：[backend/services/source_preparer.py](../backend/services/source_preparer.py)
- 凭证配置：[backend/services/config.py](../backend/services/config.py)
- MCP 配置指南：[docs/MCP_CONFIGURATION.md](./MCP_CONFIGURATION.md)
- Backlog 完整列表：[docs/backlog.md](./backlog.md)（"Alert Source 接入"、"MCP 集成" 两节）
- ACP Session 核心代码：[backend/main.py](../backend/main.py)（`ACPSession.initialize()`）
- MCP config 解析 + 注入：[backend/main.py](../backend/main.py)（`_load_mcp_servers_config()`）
- SourceCard UI：[components/SourcesPanel/SourceCard.tsx](../components/SourcesPanel/SourceCard.tsx)
- Source 状态类型：[lib/workspace.tsx](../lib/workspace.tsx)（`Source` interface，`status` / `taskId` 字段）
- ACP Stateful Agents 规范：https://agentcommunicationprotocol.dev/core-concepts/stateful-agents
- Backlog Session 持久化条目：[docs/backlog.md](./backlog.md)（"ACP Session & Chat 生命周期" → "OpenCode Session 持久化"）
