# O11Y Alert 自动诊断工作流

## 目标

当产生新的 O11Y Alert 时，自动创建 Investibook workspace 并开始诊断；当需要人工介入时，通知 oncall，并授予其继续诊断的编辑权限。

---

## 系统边界

### 外部服务负责（Guardian）

| 功能 | 说明 |
|------|------|
| Alert webhook 接收 | 接收 O11Y 系统告警事件，按类型分流：changefeed/scale/磁盘走 fake-analyzer，其余走 Investibook |
| 告警分流 | 根据 `AlertHandlerRouter.has_handler(alert_name)` 判断：命中则走 fake-analyzer，未命中则走 Investibook |
| Investibook 编排 | 调用 `POST /api/workspaces/from-alert` 创建 workspace；若携带 `auto_questions` 则先写 diagnostics=`pending`，待 source prepare 完成后再切到 `running` 并启动 AutoChat；`auto_questions` 固定为：1) Use tidb-clinic-diagnosis... 2) Summarize the analysis (status/timeline/root cause/impact/next actions) and sync to issue comment. |
| Oncall 解析 | 查询排班系统得到当前 oncaller（现阶段 mock） |
| 飞书通知 | 调飞书 API 发送通知与 workspace 链接 |
| callback 处理 | 接收 Investibook callback（`needs_intervention` 等状态），`assignee_email` 为空时取 `auto_handled_alerts.assignee_email`（来自 Jira issue assignee）通知、授予 workspace 编辑权限 |
| 持久化 | `auto_handled_alerts` 表记录 workspace 映射、callback 状态、通知状态 |

### Investibook 负责

| 功能 | 说明 |
|------|------|
| Workspace provisioning（from-alert） | `POST /api/workspaces/from-alert` 基于 alert 创建 workspace、初始化目录、写入 Jira issue source |
| 权限授权（workspace_members） | `POST /api/workspaces/{workspace_id}/members`（仅 owner 可写）为指定用户授予 `editor`/`viewer`，支持传 `user_id` 或 `email` |
| AutoChat（阶段二） | 通过 `POST/GET/POST(auto-chat)` 触发并执行后台自动问答状态机（逐题注入 user 问句、追加 assistant 回复、判定升级） |
| callback（阶段二） | 当 diagnostics 状态变化时，若 workspace 配置了 `callback_url`，由后台 runner 主动 `POST callback_url` 发送 payload（`needs_intervention` 时 `assignee_email` 留空，Guardian 取 `auto_handled_alerts.assignee_email`（来自 Jira issue assignee）通知） |
| Chat API（阶段一 / MVP） | Phase A 需要外部服务“驱动多轮 chat”时，可基于 `GET /api/chat/stream` 的 SSE 文本输出做是否需要人工的判定 |

---

## 两阶段实施

## 方案 A（MVP，先上线）

外部服务驱动多轮 chat；Investibook 提供 from-alert、members，并提供 `GET /api/chat/stream`（SSE）用于流式输出。

### 流程图（A）

```text
O11Y Alert
   │
   ▼
POST /guardian/alerts/auto-handle（Guardian 入口）
   │
   ├─ 是 fake-analyzer 类型？（changefeed/scale/磁盘）
   │      ├─ 是 → fake_analyzer 处理
   │      └─ 否 → 继续
   │
   ├─ POST /api/workspaces/from-alert
   │
   ├─ 循环调用 chat API：GET /api/chat/stream（SSE，message=Qx，可携带 session_id）
   │      ├─ 判定是否需要人工：检查 assistant 文本是否包含升级关键词
   │      │   （need human / needs human / 需要人工 / 无法自动判断 / 请人工确认 / <NEED_HUMAN>）
   │      ├─ 不需要人工：继续（提出下一问 Qx+1）
   │      └─ 需要人工：
   │           ├─ PUT /api/workspaces/{id}/members（editor）
   │           └─ 飞书通知 oncall
   │
   └─ oncall 登录 Investibook 继续提问
```

### Investibook 新增能力（A）

- `POST /api/workspaces/from-alert`
- `GET /api/chat/stream`（SSE）
- `PUT /api/workspaces/{id}/members`
- `GET /api/workspaces/{id}/members`
- `require_workspace_editor` 权限依赖（当携带 `workspace_id` 时）
- `workspace_members` 表

---

## 方案 B（产品化演进）

AutoChat 下沉到 Investibook，外部服务只编排事件与通知。

### 流程图（B）

```text
O11Y Alert
   │
   ▼
POST /guardian/alerts/auto-handle（Guardian 入口）
   │
   ├─ 是 fake-analyzer 类型？（changefeed/scale/磁盘）
   │      ├─ 是 → fake_analyzer 处理
   │      └─ 否 → 继续
   │
   ├─ POST /api/workspaces/from-alert（填 `auto_questions` + `callback_url`）
   │      └─ 写入 diagnostics.status=pending（暂不启动）
   │
   ├─ source prepare 完成（source=ready + issueDir）
   │      └─ diagnostics 切到 running，启动 AutoChat runner
   │
   ├─ Investibook 后台自动问答
   │      ├─ completed：结束
   │      └─ 完成后由 agent 评估是否需要人工：
   │            ├─ 否：callback status=completed
   │            └─ 是：callback status=needs_intervention（assignee_email 留空）
   │
   ├─ callback 处理：
   │      ├─ assignee_email 为空，取 auto_handled_alerts.assignee_email（Jira issue assignee）
   │      ├─ PUT /api/workspaces/{id}/members (editor)
   │      └─ 飞书 IM 通知 assignee
   │
   └─ oncall 登录 Investibook 继续提问
```

### Investibook 新增能力（B）

- `POST /api/workspaces/{id}/auto-chat/start`
- `GET /api/workspaces/{id}/diagnostics/status`
- `POST /api/workspaces/{id}/auto-chat/stop`
- `POST /api/workspaces/{id}/auto-chat/continue`
- `workspace_diagnostics` 表
- `needs_intervention` callback

---

## 数据模型建议

## 1) Phase A

```sql
CREATE TABLE IF NOT EXISTS workspace_members (
    workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
    user_id TEXT NOT NULL REFERENCES users(id),
    role TEXT NOT NULL CHECK (role IN ('owner', 'editor', 'viewer')),
    granted_by TEXT REFERENCES users(id),
    created_at TEXT NOT NULL,
    PRIMARY KEY (workspace_id, user_id)
);
```

> owner 建议先使用 service/bot 用户 + API key，不强依赖 `owner=null`。

## 2) Phase B

```sql
CREATE TABLE IF NOT EXISTS workspace_diagnostics (
    workspace_id TEXT PRIMARY KEY REFERENCES workspaces(id) ON DELETE CASCADE,
    alert_id TEXT NOT NULL,
    questions TEXT,
    current_index INTEGER NOT NULL DEFAULT 0,
    callback_url TEXT,
    last_callback_at TEXT,
    last_callback_status TEXT,
    last_callback_error TEXT,
    status TEXT NOT NULL DEFAULT 'pending'
        CHECK (status IN ('pending', 'running', 'needs_intervention', 'completed', 'resolved', 'failed')),
    last_response TEXT,
    intervention_reason TEXT,
    oncall_user_id TEXT,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);
```

---

## API 设计

## 1) `POST /api/workspaces/from-alert`

```python
class CreateAlertWorkspaceRequest(BaseModel):
    alert_id: str
    alert_type: str = "o11y"
    cluster_id: str | None = None
    owner_user_id: str | None = None  # 默认 token 对应 service user
    auto_questions: list[str] = []    # Phase B：非空时创建 workspace_diagnostics（先 pending）；source prepare 完成后才启动 AutoChat 并发 running callback
    callback_url: str | None = None   # Phase B：AutoChat runner 的 callback_url（用于 POST callback_url）
    idempotency_key: str | None = None
```

```json
{
  "workspace_id": "ws-xxx",
  "status": "created",
  "source_prepare_task_id": "task-xxx",
  "owner_user_id": "o11y-service"
}
```

- 幂等说明：当 `idempotency_key` 命中时，返回 `status="existing"`，并且 `source_prepare_task_id=null`（不会重复创建 diagnostics / source）。

## 2) `POST /api/workspaces/{workspace_id}/members`

```python
class UpdateWorkspaceMembersRequest(BaseModel):
    members: list[dict]  # [{"user_id": "...", "role": "editor"|"viewer"}] 或 [{"email": "...", "role": "editor"|"viewer"}]
```

> `POST /api/workspaces/{workspace_id}/members` 仅允许 workspace owner 授予成员权限；`role` 只能是 `editor` 或 `viewer`（owner 由系统隐式拥有）。
> 每个 member 必须且只能提供一个标识：`user_id` 或 `email`。若按 `email` 查不到已注册用户，接口返回 `400 User not found by email`（默认不自动建号）。
> 调用语义为 **append/upsert**：仅更新请求中出现的成员，不会清空既有成员；同一用户再次传入会更新其 `role`。

## 3) AutoChat APIs（B）

- `POST /api/workspaces/{workspace_id}/auto-chat/start`
- `GET /api/workspaces/{workspace_id}/diagnostics/status`
- `POST /api/workspaces/{workspace_id}/auto-chat/stop`
- `POST /api/workspaces/{workspace_id}/auto-chat/continue`

- `POST /api/workspaces/{workspace_id}/auto-chat/stop`：将 diagnostics 状态置为 `resolved`，并触发 callback（若配置了 `callback_url`）。

---

## AutoChat 实现约束（关键）

不要在 `core/auto_chat.py` 里直接操作 `session.client.queue` 或 `_prompt_lock`。

应在 `core/acp.py` 封装统一接口：

```python
class AskResult(BaseModel):
    text: str
    finish_reason: Literal["completed", "timeout", "cancelled", "error"]
    error: str | None = None

async def ask_and_wait(
    workspace_id: str,
    prompt: str,
    *,
    user_email: str,
    timeout_s: int = 900,
    idle_timeout_s: int = 120,
    active_mcp_servers: Optional[str] = None,
) -> AskResult:
    ...
```

`core/auto_chat.py` 只处理状态机和持久化，不处理底层事件队列。

---
## callback payload（Phase B）

当后台 AutoChat runner 调用 `POST callback_url` 时，payload 如下：

```json
{
  "event_id": "evt-xxxx",
  "workspace_id": "ws-xxxx",
  "alert_id": "O11Y-12345",
  "status": "running|needs_intervention|completed|failed|resolved",
  "current_index": 0,
  "total_questions": 3,
  "reason": "AutoChat timeout|<升级关键词命中原因>|...",
  "last_response": "assistant 最终文本（可能为空）",
  "occurred_at": "2026-03-25T12:34:56+00:00",
  "assignee_email": ""
}
```

- `running`：在进入诊断运行时触发（从 `auto-chat/start` 启动，或 `from-alert` 在 issue source 准备完成后启动）。
- `needs_intervention/failed/completed`：在 runner 逐题问答过程中触发；其中「问题问完」后会先进行一次 agent 评估，再决定发 `completed` 还是 `needs_intervention`；`needs_intervention` 时 `assignee_email` 留空，Guardian 直接取 `auto_handled_alerts.assignee_email`（来自 Jira issue assignee）通知。
- `resolved`：调用 `POST /api/workspaces/{workspace_id}/auto-chat/stop` 后触发。

### callback 鉴权（配置项）

Investibook callback 请求支持以下鉴权配置（按优先级选择一个）：

1. 自定义 Header：
   - `INVESTIBOOK_CALLBACK_AUTH_HEADER_NAME`
   - `INVESTIBOOK_CALLBACK_AUTH_HEADER_VALUE`
2. Bearer：
   - `INVESTIBOOK_CALLBACK_BEARER_TOKEN`（发送 `Authorization: Bearer ...`）
3. API Key：
   - `INVESTIBOOK_CALLBACK_API_KEY`（发送 `X-API-Key: ...`）

---

## 鉴权与 service 用户

- 使用现有 API key 机制：
  1. `POST /api/auth/apikey` 获取 token
  2. 调用业务 API（Bearer token）
- 建议单独创建 `o11y-service@investibook` 用户，便于审计。

---

## 待办清单

## Investibook（Phase A）

- [x] `workspace_members` migration
- [x] `require_workspace_editor`（写/聊天权限）与 `require_workspace_access`（viewer 只读）
- [x] `POST /api/workspaces/from-alert`
- [x] `PUT/GET /api/workspaces/{workspace_id}/members`
- [x] from-alert 幂等（`idempotency_key`）

## Investibook（Phase B）

- [x] `workspace_diagnostics` migration + db queries
- [x] AutoChat 四个 API（start/status/stop/continue）
- [x] `core/acp.py` 的 `ask_and_wait`
- [x] `_run_auto_chat` 状态机
- [ ] callback（签名、重试、审计）（基本 `POST callback_url` 已实现）

## 外部服务（Guardian）

### API

- `POST /guardian/alerts/auto-handle`：O11Y Alert 入口，内部做两路分流
  - changefeed/scale/磁盘：走 fake-analyzer
  - 其他：走 Investibook AutoChat（方案 B）
- `POST /guardian/investibook/callback`：Investibook AutoChat callback 端点

### 数据模型

#### `auto_handled_alerts` 表

```sql
CREATE TABLE IF NOT EXISTS auto_handled_alerts (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    issue_key VARCHAR(50) NOT NULL UNIQUE,
    alert_name VARCHAR(100) NOT NULL,
    cluster_id VARCHAR(50) NOT NULL,
    tenant_id VARCHAR(50) NOT NULL,
    workspace_id VARCHAR(100),
    investibook_status VARCHAR(50) NOT NULL DEFAULT 'pending'
        CHECK (investibook_status IN ('pending', 'running', 'needs_intervention', 'completed', 'failed', 'resolved')),
    oncall_email VARCHAR(255),
    oncall_user_id VARCHAR(100),
    assignee_email VARCHAR(255),
    last_callback_at DATETIME,
    last_callback_status VARCHAR(50),
    last_callback_reason TEXT,
    notified_at DATETIME,
    lark_message_id VARCHAR(100),
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL,
    INDEX idx_issue_key (issue_key),
    INDEX idx_investibook_status (investibook_status)
);
```

#### `POST /guardian/investibook/callback` payload

```json
{
  "event_id": "evt-xxxx",
  "workspace_id": "ws-xxxx",
  "alert_id": "O11Y-12345",
  "status": "running|needs_intervention|completed|failed|resolved",
  "current_index": 0,
  "total_questions": 3,
  "reason": "AutoChat timeout|<升级关键词命中原因>|...",
  "last_response": "assistant 最终文本（可能为空）",
  "occurred_at": "2026-03-25T12:34:56+00:00",
  "assignee_email": ""
}
```

### 待办

- [x] Alert webhook 入口（`POST /guardian/alerts/auto-handle`）
- [x] 告警分流逻辑（fake-analyzer vs Investibook）
- [x] `auto_handled_alerts` 表
- [ ] oncall 查询（mock）
- [x] Investibook client（create workspace + update members）
- [x] callback 处理（`needs_intervention` 时通知 assignee（Jira issue assignee）+ 授权）
- [ ] 飞书通知卡片样式优化
- [ ] 方案 A 的 chat 循环编排（备用）

---

## 相关文件

### Investibook Backend

- `platform/investibook/backend/investibook/api/workspaces.py` - Workspace API (含 from-alert)
- `platform/investibook/backend/investibook/api/deps.py` - 权限依赖
- `platform/investibook/backend/investibook/db/queries.py` - 数据库查询
- `platform/investibook/backend/investibook/core/acp.py` - ACP 会话管理 (含 `ask_and_wait`)
- `platform/investibook/backend/investibook/core/auto_chat.py` - **新增 (Phase B)**: 自动诊断任务逻辑
- `platform/investibook/backend/investibook/api/auto_chat.py` - **新增 (Phase B)**: AutoChat API 路由
- `platform/investibook/backend/investibook/models/requests.py` - 请求模型

### 外部服务（Guardian）

- `aiops/service/guardian/service.py` - GuardianService（告警分流 + 触发 Investibook）
- `aiops/infra/clients/investibook/client.py` - Investibook API client
- `aiops/service/guardian/repo/auto_handled_alert_repo.py` - auto_handled_alerts CRUD
- `aiops/service/guardian/oncall_resolver.py` - oncall 查询（mock）
- `aiops/service/guardian/investibook_lark_notifier.py` - 飞书通知
- `aiops/models/guardian/auto_handled_alert.py` - `auto_handled_alerts` 表模型
- `aiops/entity/guardian/auto_handled_alert.py` - 实体 + callback payload
- `aiops/app/server/routes/guardian.py` - API 路由（`/alerts/auto-handle` + `/investibook/callback`）
