Files
taiji-AI-PAD/Docs/Heicode-接口契约文档.md
T
chenchenandClaude Opus 4.7 610fde5d03 feat(mcp-server): Heicode integration + register transaction hardening
== Heicode integration (~41 endpoints across 5 modules) ==
- §2 ResourceBinding (5 endpoints) — resources.py / resource_grants.py
- §4 NewAPI metadata proxy (4 endpoints) — heicode_proxy.py + heicode_client.py
- §5 Agnet platform stub (12 endpoints, in-memory mock) — agnet_stub.py
- §6 Task orchestration (5 endpoints + 3 extension endpoints) — heicode_tasks.py
  6.1-6.5: intent / list / get / answer / messages
  6.6-6.8: execution / delivery / audit?tab=... (Slice 8/9/10)
- §7 SSE single channel + approvals (4 endpoints + 5 event types) —
  heicode_events.py + event_bus.py
- §7.8.1 internal billing-provider PUT endpoint — auth.py (routes)

== Schema changes ==
- migrations/026 heicode_tasks (orchestration state)
- migrations/027 users.billing_provider (litellm | newapi switch)
- migrations/028 heicode_approvals (high-risk approval queue)

== Register transaction hardening (P0 + P1 + P2) ==
routes/auth.py register():
- Pre-existing P0: failed register returned IntegrityError str verbatim
  (leaking SQL params + ~50 plaintext LiteLLM keys per attempt).
  Now logs exc_info, returns {code: REGISTER_FAILED, message: ...}.
- Pre-existing P0: model dedupe — two ModelProvider rows with overlapping
  supported_models (e.g. taiji/gpt-4o-mini in both taiji and azure providers)
  collide on uq_tenant_model. seen_models set deduplicates within the loop.
- New P1: track created_litellm_keys; on any failure call delete_key() for
  each — prevents remote orphan keys when DB rollback fires.
- New P1: replace verify_code with peek_verification_code at the start;
  only call verify_code (which consumes) after commit succeeds. Failed
  registrations no longer burn the user's one-shot code.
- New P2: narrow inner `except (LiteLLMClientError, Exception)` to just
  LiteLLMClientError so SQLAlchemy errors bubble to the outer rollback
  instead of being silently swallowed into a half-allocated 200 response.
- New P2: same narrowing on outer `except (AgentManagerError, Exception)`.

== Auth middleware ==
- app/auth.py: allow /api/auth/internal/billing-provider and
  /api/auth/internal/approvals to bypass user JWT (service-token auth
  via HEICODE_INTERNAL_SERVICE_TOKEN, validated in-route).

== Docs ==
- Heicode-接口契约文档.md v2.2 (41 endpoints + SSE schema + 6.6-6.8)
- Heicode-对接进度与待办.md (through §7.14 SSE + 7.8.2 delivery回执)
- Heicode-完整调用流程图.md (sequence + routing diagrams)
- Agent-Manager-Heicode对接需求文档.md
- HEICODE_API_INTEGRATION.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:43:10 +08:00

1578 lines
49 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Heicode ↔ mcp-server 接口契约文档
**版本**: v2.2
**生效日期**: 2026-05-08
**状态**: 已上线生产,可直接对接
**当前镜像**: `taiji.azurecr.io/mcp-server:heicode-7.8.2-tasks-ext-20260508`
> 本文档列出 Heicode 客户端可调用的全部 mcp-server 接口、入参与出参。
> 所有接口已在生产环境通过端到端测试。
**v2.2 修订**(2026-05-08,本次):
- 新增 §6.6 / §6.7 / §6.8(execution / delivery / audit)— Slice 8/9/10 配套,3 端点全过 13/13 smoke
- **接口总数:38 → 41**
**v2.1 修订**(2026-05-08):
- 新增 §7 Server-Sent Events(5 类 event + 配套 REST)— §7.8.3 SSE 单通道全过 12/12 smoke
- **接口总数:34 → 38**
**v2.0 修订**(2026-05-08):
- 新增 §4 NewAPI 元数据透传(4 接口)
- 新增 §5 Agnet 平台本地 stub(12 接口,mock 数据,前端可立即联调)
- 新增 §6 任务编排(5 接口,cc-haha 任务驾驶舱 / 工作台 配套)
- **接口总数:13 → 34**
> 📍 调用关系全景见 [`Heicode-完整调用流程图.md`](./Heicode-完整调用流程图.md)。
> 📍 进度与待办见 [`Heicode-对接进度与待办.md`](./Heicode-对接进度与待办.md)。
---
## 0. 接入信息
### Base URL
```
https://apimtaiji.azure-api.net/api/mcp
```
完整路径示例:
```
POST https://apimtaiji.azure-api.net/api/mcp/api/auth/login
GET https://apimtaiji.azure-api.net/api/mcp/api/resources
```
### ⚠️ 重要:登录接口被两类调用方共用
经过代码核实(2026-05-05),mcp-server 的登录接口实际被以下两个调用方依赖:
| 调用方 | 调用方式 | 关键依赖字段 |
|---|---|---|
| **heicode 前端** (`web/default/src/features/auth/api.ts`) | 浏览器跨域,`Authorization: Bearer` | `data.token`, `data.refreshToken`, `data.user.{id,name,email,role,channelId}` |
| **heicode 后端** (`controller/heicode_agnet_session.go`) | 服务端调 `/me` `/refresh`,验证前端给的 token | `data.{id,email,name,role,channelId,status}` |
字段形态**任何变更都会破坏对接**。增加字段安全;删/改字段名/字段类型 = breaking change。
### 通用请求头
| Header | 必填 | 说明 |
|---|---|---|
| `Content-Type: application/json` | POST/PUT 时是 | 请求体 JSON 格式 |
| `Authorization: Bearer <accessToken>` | 受保护接口必填 | JWT access token |
| `X-Request-Id: <uuid>` | 建议 | 客户端生成,便于排障反查日志 |
### 通用响应封装
**成功**:
```json
{
"success": true,
"data": { ... }
}
```
部分接口含 `message`:
```json
{
"success": true,
"data": { ... },
"message": "操作成功"
}
```
**失败(FastAPI 默认)**:
```json
{
"detail": "邮箱或密码错误"
}
```
**失败(结构化错误)**:
```json
{
"detail": {
"code": "RESOURCE_GRANT_INVALID",
"message": "allowed_actions 超出对应 binding.permission_scope: ['repo:admin']"
}
}
```
### Token 模型
| Token | TTL | 用途 |
|---|---|---|
| Access Token | 24 小时 | 调业务接口(含 /me、/logout) |
| Refresh Token | 7 天 | 仅用于 `/refresh` 换新 access |
JWT claims:`sub`(user_id)、`email`、`role`、`channelId`、`type`(access/refresh)、`iat`、`exp`。
### Role 字段值与 heicode 前端解析
mcp-server 现有 role 取值(按用户类型):`user`, `channel_admin`, `billing_admin`, `operations_admin`, `admin`, `super_admin`, `provider_admin`。
heicode 前端 (`api.ts:259-261`) **硬编码**解析为 3 档:
| mcp-server role 值 | heicode 前端解析 | 数值 |
|---|---|---|
| `root` | root | 100 |
| `admin` | admin | 10 |
| `user` | user | 1 |
| 其他(channel_admin, super_admin, provider_admin 等) | 默认 user | 1 |
**对 Heicode 客户端的实际影响**:
- Heicode 客户端预期登录的角色 = `user`,前端解析为 1 ✓
- 高权限角色(admin/root)由 **heicode 后端独立白名单** (`HEICODE_ROOT_EMAILS` / `HEICODE_ADMIN_EMAILS` 环境变量) 决定,**不信任** mcp-server 返回的 role 字段
- mcp-server 这边**无需调整** role 取值,保持现有取值即可
### CORS 当前状态
mcp-server 生产环境 `CORS_ORIGINS` 未显式配置 → fallback 到 `["*"]`。代码同时设置 `allow_credentials=True`。
| 调用方式 | 当前是否能用 |
|---|---|
| `Authorization: Bearer <token>` | ✅ 能用(heicode 前端走这条) |
| 浏览器 cookie + credentials | ❌ 浏览器规范禁止 `*` + credentials 同用 |
**Heicode 前端用 Bearer,不受影响**。如需后续硬化(去掉 `*`),需 heicode 团队提供前端实际部署域名清单。
---
## 1. 登录认证(4 接口)
### 1.1 POST /api/auth/login
**用途**: 账号密码登录,换取 token 对。
**请求**
```http
POST /api/auth/login
Content-Type: application/json
```
```json
{
"email": "user@example.com",
"password": "YourPassword123",
"role": "user"
}
```
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `email` | string | 是 | 用户邮箱 |
| `password` | string | 是 | 密码 |
| `role` | string | 是 | Heicode 客户端固定传 `"user"` |
**成功响应 200**
```json
{
"success": true,
"data": {
"token": "eyJhbGc...",
"refreshToken": "eyJhbGc...",
"user": {
"id": "b00a7b8e-9e8b-463d-9593-a3b4d0006778",
"name": "张三",
"email": "user@example.com",
"role": "user",
"channelId": "6e6fc470-76f8-4bb1-8ea4-625dc5b12bc6"
}
}
}
```
**错误响应**
| HTTP | 含义 |
|---|---|
| 401 | `邮箱或密码错误` |
| 403 | `账户已被禁用` / `权限不足` |
| 422 | 请求体 Pydantic 校验失败(邮箱格式不合法等) |
| 429 | 登录尝试过于频繁;响应头含 `Retry-After: 60` |
**限流**: 每 IP 每 60 秒最多 5 次(不区分成功失败)。
---
### 1.2 GET /api/auth/me
**用途**: 校验 access token 有效性,返回当前用户资料。
> ⚠️ **高频调用 + 双调用方依赖**:
> - heicode 前端:客户端启动 + token 续期后立即调一次
> - heicode 后端:每次走 `from-agnet` 流程时调一次(即每次"建立 Manager 会话")
> - 字段形态被 hardcode 解析(id/email/name/role/channelId/status),**绝不能改**
> - 性能敏感:响应时间影响登录用户体验
**请求**
```http
GET /api/auth/me
Authorization: Bearer <accessToken>
```
**成功响应 200**
```json
{
"success": true,
"data": {
"id": "b00a7b8e-9e8b-463d-9593-a3b4d0006778",
"email": "user@example.com",
"name": "张三",
"role": "user",
"channelId": "6e6fc470-76f8-4bb1-8ea4-625dc5b12bc6",
"status": "active",
"subscriptionTier": "free",
"lastLoginAt": "2026-05-05T06:01:23.456789"
}
}
```
**错误响应**
| HTTP | 含义 |
|---|---|
| 401 | token 无效 / 过期 / 已登出 / 用户不存在 |
| 403 | 账户已被禁用 |
---
### 1.3 POST /api/auth/refresh
**用途**: 用 refresh token 换取新的 access + refresh token 对。
**请求**
```http
POST /api/auth/refresh
Authorization: Bearer <refreshToken>
```
> ⚠️ 必须传 **refresh token**,传 access token 会被拒绝。
**成功响应 200**
```json
{
"success": true,
"data": {
"token": "eyJhbGc...",
"refreshToken": "eyJhbGc..."
}
}
```
客户端收到新 token 对后**应替换本地缓存**(旧的也作废)。
**错误响应**
| HTTP | 含义 |
|---|---|
| 401 | refresh token 无效 / 过期 / 错传了 access token(`必须使用 refresh token 调用此接口`) |
---
### 1.4 POST /api/auth/logout
**用途**: 登出,把当前 access token 加入黑名单。
**请求**
```http
POST /api/auth/logout
Authorization: Bearer <accessToken>
```
**成功响应 200**
```json
{
"success": true,
"data": null,
"message": "登出成功"
}
```
**说明**: 容错性强,token 黑名单写入失败也会返回 200。客户端总是清理本地 token + 跳登录页。
> ⚠️ **登出双调用约定**:heicode 前端登出时会**同时**调用:
> 1. `GET /api/user/logout`(heicode 后端,清 heicode session cookie)
> 2. `POST /api/auth/logout`(mcp-server,本接口;token 加黑名单)
>
> 两次调用都做容错处理(不阻断登出体验)。前端最后总是清 localStorage 跳登录页。
---
## 2. 资源绑定 ResourceBinding(5 接口)
### 2.0 公共说明
**对象语义**: 用户绑定到 Heicode 的外部资源(Git 仓库 / SK / 项目文档 / 云账号 / 云资源)。
**安全红线**:
- DB **只保存元数据** + `secret_ref`,**不保存明文密钥**
- `metadata` / `constraints` / `permission_scope` 中**不得**出现 key 包含以下关键词的字段(不区分大小写):
```
password, token, secret, private_key, access_key, credential
```
违反返回 **422 `RESOURCE_GRANT_SECRET_REJECTED`**
- 例外:key 名为 `secret_ref` 视为引用(如 `vault://...`),允许通过
**枚举值**:
| 字段 | 允许值 |
|---|---|
| `type` | `git` / `sk` / `project_doc` / `cloud_account` / `cloud_resource` |
| `status` | `pending` / `active` / `disabled` / `revoked` |
---
### 2.1 POST /api/resources
**用途**: 创建资源绑定。
**请求**
```http
POST /api/resources
Authorization: Bearer <accessToken>
Content-Type: application/json
```
```json
{
"type": "git",
"name": "heicode-main-repo",
"external_ref": "https://example.com/org/heicode.git",
"metadata": {
"provider": "gitee",
"default_branch": "main"
},
"permission_scope": ["repo:read", "repo:write:current-branch"],
"constraints": {
"ref": "main",
"allowed_paths": "services/**,docs/**"
},
"secret_ref": "vault://secret/users/{user_id}/bindings/heicode-main-repo",
"status": "active"
}
```
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `type` | string (enum) | 是 | 资源类型,见 §2.0 |
| `name` | string (≤255) | 是 | 用户可见名称 |
| `external_ref` | string | 否 | 外部资源定位(repo URL、subscription ID 等) |
| `metadata` | object | 否,默认 `{}` | 元数据;不得含敏感 key |
| `permission_scope` | string[] | 否,默认 `[]` | 该资源允许的最大动作集合 |
| `constraints` | object | 否,默认 `{}` | 路径/分支/网络等限制 |
| `secret_ref` | string (≤500) | 否 | 凭证引用(`vault://...`),不是密钥本身 |
| `status` | string (enum) | 否,默认 `pending` | 见 §2.0 |
**成功响应 200**
```json
{
"success": true,
"data": {
"id": "88963d56-5a95-4863-8a0b-91a83e456028",
"user_id": "b00a7b8e-9e8b-463d-9593-a3b4d0006778",
"type": "git",
"name": "heicode-main-repo",
"external_ref": "https://example.com/org/heicode.git",
"metadata": { "provider": "gitee", "default_branch": "main" },
"permission_scope": ["repo:read", "repo:write:current-branch"],
"constraints": { "ref": "main", "allowed_paths": "services/**,docs/**" },
"secret_ref": "vault://secret/users/.../bindings/heicode-main-repo",
"status": "active",
"created_by": "b00a7b8e-9e8b-463d-9593-a3b4d0006778",
"updated_by": "b00a7b8e-9e8b-463d-9593-a3b4d0006778",
"created_at": "2026-05-05T06:10:00.123456",
"updated_at": "2026-05-05T06:10:00.123456"
}
}
```
**错误响应**
| HTTP | code | 含义 |
|---|---|---|
| 401 | — | 未登录 |
| 422 | — | type/status 枚举不合法 |
| 422 | `RESOURCE_GRANT_SECRET_REJECTED` | metadata/constraints/permission_scope 含敏感字段 |
---
### 2.2 GET /api/resources
**用途**: 列出当前登录用户的资源绑定。
**请求**
```http
GET /api/resources?type=git&status=active&limit=100&offset=0
Authorization: Bearer <accessToken>
```
| Query | 类型 | 默认 | 说明 |
|---|---|---|---|
| `type` | string | — | 按类型过滤 |
| `status` | string | — | 按状态过滤 |
| `limit` | int (1-500) | 100 | 返回上限 |
| `offset` | int (≥0) | 0 | 偏移 |
**成功响应 200**
```json
{
"success": true,
"data": {
"items": [
{ "id": "...", "type": "git", "name": "...", ... }
],
"total": 1,
"offset": 0,
"limit": 100
}
}
```
> `items` 元素结构同 §2.1 的成功响应 `data` 字段。
---
### 2.3 GET /api/resources/{binding_id}
**用途**: 获取单个资源绑定详情。
**请求**
```http
GET /api/resources/88963d56-5a95-4863-8a0b-91a83e456028
Authorization: Bearer <accessToken>
```
**成功响应 200**: 同 §2.1 的 `data` 字段。
**错误响应**
| HTTP | 含义 |
|---|---|
| 400 | binding_id 不是有效 UUID |
| 401 | 未登录 |
| 403 | 该资源不属于当前用户 |
| 404 | 资源不存在 |
---
### 2.4 PUT /api/resources/{binding_id}
**用途**: 更新资源绑定(部分字段)。
**请求**
```http
PUT /api/resources/88963d56-5a95-4863-8a0b-91a83e456028
Authorization: Bearer <accessToken>
Content-Type: application/json
```
```json
{
"name": "heicode-main-repo-renamed",
"permission_scope": ["repo:read"],
"status": "active"
}
```
可更新字段(全部可选):`name` / `external_ref` / `metadata` / `permission_scope` / `constraints` / `secret_ref` / `status`。
未传的字段保持原值。
**成功响应 200**: 同 §2.1(含更新后字段)。
**错误响应**: 同 §2.3 + §2.1 的 422。
---
### 2.5 DELETE /api/resources/{binding_id}
**用途**: 软删除资源绑定(设 `status=revoked`,保留历史)。
**请求**
```http
DELETE /api/resources/88963d56-5a95-4863-8a0b-91a83e456028
Authorization: Bearer <accessToken>
```
**成功响应 200**
```json
{
"success": true,
"data": {
"id": "88963d56-5a95-4863-8a0b-91a83e456028",
"status": "revoked"
}
}
```
**说明**: 软删除后 `GET /api/resources/{id}` 仍可读到(status=revoked)。基于该 binding 已存在的 grant 不会自动撤销 — 业务层自行判断或调 `DELETE /api/resource-grants/{id}`。
---
## 3. 资源授权 ResourceGrant(4 接口)
### 3.0 公共说明
**对象语义**: 把某个 ResourceBinding 授给某个角色 / 子 Agent 使用。
**核心约束**:
- `allowed_actions` 必须是对应 binding 的 `permission_scope` 的**子集**
- 违反 → **400 `RESOURCE_GRANT_INVALID`**
- `constraints` 不得放宽对应 binding 的 `constraints`(应用层校验)
- 仅能基于**自己的** binding 创建 grant
- 同一类敏感字段拒绝规则适用于 `constraints` / `allowed_actions`
**枚举值**:
| 字段 | 允许值 |
|---|---|
| `status` | `active` / `suspended` / `revoked` / `expired` |
| `role`(建议值) | `product` / `frontend` / `backend` / `reviewer` / `ops` / 自定义字符串 |
---
### 3.1 POST /api/resource-grants
**用途**: 创建资源授权。
**请求**
```http
POST /api/resource-grants
Authorization: Bearer <accessToken>
Content-Type: application/json
```
```json
{
"resource_id": "88963d56-5a95-4863-8a0b-91a83e456028",
"binding_scope": "main",
"role": "backend",
"agent_id": null,
"allowed_actions": ["repo:read"],
"constraints": {
"ref": "main",
"allowed_paths": "services/api/**"
},
"expires_at": "2026-06-05T00:00:00",
"status": "active"
}
```
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `resource_id` | string (UUID) | 是 | 对应 ResourceBinding ID |
| `binding_scope` | string (≤255) | 是 | repo/ref/path 或云资源引用 |
| `role` | string | 否 | 子 Agent 角色 |
| `agent_id` | string (UUID) | 否 | 子 Agent ID;空表示授下次该角色部署 |
| `allowed_actions` | string[] | 否 | 必须是 binding.permission_scope 的子集 |
| `constraints` | object | 否 | 限制条件 |
| `expires_at` | string (ISO 8601) | 否 | 过期时间;高危资源建议必填 |
| `status` | string (enum) | 否,默认 `active` | 见 §3.0 |
**成功响应 200**
```json
{
"success": true,
"data": {
"id": "1e8d7a4d-3a2c-4d5e-9b1f-12c3d4e5f6a7",
"user_id": "b00a7b8e-9e8b-463d-9593-a3b4d0006778",
"binding_scope": "main",
"resource_id": "88963d56-5a95-4863-8a0b-91a83e456028",
"role": "backend",
"agent_id": null,
"allowed_actions": ["repo:read"],
"constraints": { "ref": "main", "allowed_paths": "services/api/**" },
"status": "active",
"expires_at": "2026-06-05T00:00:00",
"created_by": "b00a7b8e-9e8b-463d-9593-a3b4d0006778",
"revoked_by": null,
"created_at": "2026-05-05T06:15:00.123456",
"revoked_at": null
}
}
```
**错误响应**
| HTTP | code | 含义 |
|---|---|---|
| 400 | — | resource_id / agent_id 非合法 UUID |
| 400 | `RESOURCE_GRANT_INVALID` | binding 状态不允许新建授权(如 disabled/revoked),或 `allowed_actions` 超出 binding.permission_scope |
| 401 | — | 未登录 |
| 403 | `FORBIDDEN_SCOPE` | 不能基于他人的 binding 创建授权 |
| 404 | `NOT_FOUND` | 对应 binding 不存在 |
| 422 | — | status 枚举不合法 |
| 422 | `RESOURCE_GRANT_SECRET_REJECTED` | constraints/allowed_actions 含敏感字段 |
---
### 3.2 GET /api/resource-grants
**用途**: 列出当前用户的资源授权。
**请求**
```http
GET /api/resource-grants?resource_id=...&role=backend&binding_scope=main&status=active&limit=100&offset=0
Authorization: Bearer <accessToken>
```
| Query | 类型 | 默认 | 说明 |
|---|---|---|---|
| `resource_id` | string (UUID) | — | 按 binding 过滤 |
| `role` | string | — | 按角色过滤 |
| `binding_scope` | string | — | 按作用域过滤 |
| `status` | string | — | 按状态过滤 |
| `limit` | int (1-500) | 100 | 返回上限 |
| `offset` | int (≥0) | 0 | 偏移 |
**成功响应 200**
```json
{
"success": true,
"data": {
"items": [ { "id": "...", ... } ],
"total": 1,
"offset": 0,
"limit": 100
}
}
```
---
### 3.3 GET /api/resource-grants/{grant_id}
**用途**: 获取单个授权详情。
**请求**
```http
GET /api/resource-grants/1e8d7a4d-3a2c-4d5e-9b1f-12c3d4e5f6a7
Authorization: Bearer <accessToken>
```
**成功响应 200**: 同 §3.1 的 `data` 字段。
**错误响应**:
| HTTP | code | 含义 |
|---|---|---|
| 400 | — | grant_id 非合法 UUID |
| 401 | — | 未登录 |
| 403 | `FORBIDDEN_SCOPE` | 不属于当前用户 |
| 404 | `NOT_FOUND` | 授权不存在 |
---
### 3.4 DELETE /api/resource-grants/{grant_id}
**用途**: 撤销授权(软删除:`status=revoked` + `revoked_at` + `revoked_by`)。**幂等**:重复撤销返回 200。
**请求**
```http
DELETE /api/resource-grants/1e8d7a4d-3a2c-4d5e-9b1f-12c3d4e5f6a7
Authorization: Bearer <accessToken>
```
**成功响应 200**: 同 §3.1 的 `data` 字段(含 revoked_at / revoked_by)。
**错误响应**: 同 §3.3。
---
## 4. 完整使用流程示例
### 4.1 客户端启动
```
1. 读取本地缓存的 access token
2. GET /api/auth/me
- 200 → 进入主界面,刷新本地用户资料
- 401 → 看是否有 refresh token
├─ 有 → POST /api/auth/refresh
│ ├─ 200 → 替换 token 对,重试 /me
│ └─ 401 → 清理本地 token,跳登录页
└─ 无 → 跳登录页
```
### 4.2 用户登录
```
POST /api/auth/login {email, password, role: "user"}
├─ 200 → 存 token 对 + user 资料,进主界面
├─ 401 → 显示"邮箱或密码错误"
├─ 429 → 显示"尝试过于频繁,请 N 秒后再试"(N = Retry-After)
└─ 其他 → 通用错误提示
```
### 4.3 绑定 Git 资源
```
POST /api/resources
{
"type": "git",
"name": "my-repo",
"external_ref": "https://example.com/org/my-repo.git",
"permission_scope": ["repo:read", "repo:write:current-branch"],
"constraints": { "ref": "main" },
"secret_ref": "vault://secret/users/{user_id}/bindings/my-repo",
"status": "active"
}
→ 200, data.id = <binding_id>
```
### 4.4 把绑定授给一个 backend 子 Agent 角色
```
POST /api/resource-grants
{
"resource_id": "<binding_id>",
"binding_scope": "main",
"role": "backend",
"allowed_actions": ["repo:read"]
}
→ 200, data.id = <grant_id>
```
### 4.5 撤销与清理
```
DELETE /api/resource-grants/{grant_id} # 撤销授权
DELETE /api/resources/{binding_id} # 软删除 binding
```
### 4.6 业务请求中 token 过期
```
任意业务接口返回 401
└→ POST /api/auth/refresh (用 refresh token)
├─ 200 → 替换 token,重试原请求
└─ 401 → 清理 token,跳登录页
```
### 4.7 用户登出
```
POST /api/auth/logout
→ 不论结果都清理本地 token + 跳登录页
```
---
## 5. 错误码索引
| code | HTTP | 接口 | 含义 |
|---|---|---|---|
| `RESOURCE_GRANT_INVALID` | 400 | resource-grants | binding 状态非法 / `allowed_actions` 超出 binding.permission_scope |
| `RESOURCE_GRANT_SECRET_REJECTED` | 422 | resources / resource-grants | metadata/constraints/permission_scope/allowed_actions 含敏感字段 |
| `FORBIDDEN_SCOPE` | 403 | resources / resource-grants | 跨用户访问别人的资源/授权 |
| `NOT_FOUND` | 404 | resource-grants | 引用的 binding 或 grant 不存在 |
| —(detail 字符串) | 401 | 全部 | 未登录 / token 失效 / 已登出 |
| —(detail 字符串) | 403 | 登录 / /me | 账户已禁用 / 权限不足 |
| —(detail 字符串) | 429 | login | 限流(每 IP 5 次/60s) |
| —(detail 数组) | 422 | 全部 | Pydantic 校验失败(邮箱格式、枚举值等) |
---
## 6. 字段速查
### 资源类型枚举
| `type` | 子 Agent 可见内容 | `permission_scope` 示例 |
|---|---|---|
| `git` | repo URL、ref、允许路径、读写范围 | `repo:read`, `repo:write:current-branch` |
| `sk` | SK 来源、版本、允许/禁止策略 | `sk:read`, `sk:list` |
| `project_doc` | 文档引用、版本、可读范围 | `doc:read`, `doc:list` |
| `cloud_account` | account/project/subscription 元数据、允许动作 | `cloud:list`, `cloud:read` |
| `cloud_resource` | 资源 ID、环境、网络边界、允许动作 | `vm:ssh:approved-window`, `service:restart` |
### 状态机
**ResourceBinding**:
```
pending ─→ active ─┬→ disabled ─→ active ...
└→ revoked (终态)
```
**ResourceGrant**:
```
active ─┬→ suspended ─→ active ...
├→ revoked (终态,由 DELETE 设置)
└→ expired (终态,由 expires_at 触发,当前由调用方判断)
```
---
## 7. 安全与配额
| 项 | 数值/规则 |
|---|---|
| 登录限流 | 每 IP 每 60 秒 ≤ 5 次(不区分成功失败) |
| Access token TTL | 24 小时 |
| Refresh token TTL | 7 天 |
| API 列表分页上限 | `limit ≤ 500` |
| `name` 长度上限 | 255 字符 |
| `secret_ref` 长度上限 | 500 字符 |
| 敏感字段拒绝模式 | key 含 `password\|token\|secret\|private_key\|access_key\|credential`(不区分大小写)→ 422 |
| 例外 key | `secret_ref` 不算敏感(视为引用) |
---
## 8. 测试账号(仅联调)
| 角色 | 邮箱 | 密码 |
|---|---|---|
| 普通用户 | `55@55.com` | `By@123456.` |
> Heicode 客户端固定使用 `role="user"`。其他角色(channel/admin)由 mcp-server 现有业务使用,不开放给 Heicode。
---
## 9. 排障
如发现接口行为与本文档不一致:
1. 客户端**应在所有请求带 `X-Request-Id` 头**(UUID 格式)
2. 报告内容:
- 完整请求 URL + Method + Headers + Body
- 完整响应 HTTP 状态 + Body
- `X-Request-Id` 值
- 时间戳(精确到秒)
3. 后端按 `X-Request-Id` 反查日志和审计
---
## 4. NewAPI 元数据透传(4 接口)
### 4.0 公共说明
Heicode 决策 ④(透传方案):mcp-server 包一层 `/api/user/heicode/*`,背后调 NewAPI(`code.xinghanlab.com`)admin token,按当前登录用户的 email 反查 NewAPI 本地 user_id 后透传。
**前端只对接 mcp-server 一个域**,不直连 `code.xinghanlab.com`,零 CORS 问题。
**当前状态**:代码已上线 + 4 端点全注册;**未配置 admin token 时返回 503 `HEICODE_NEWAPI_NOT_CONFIGURED`**。Heicode 配齐 token 后立即生效,**无需重新构建/部署**。
### 4.1 GET /api/user/heicode/balance
**响应 200**:
```json
{
"success": true,
"data": {
"heicodeUserId": 123,
"email": "user@example.com",
"username": "user",
"displayName": "张三",
"group": "default",
"status": 1,
"quota": 100000,
"usedQuota": 12345,
"requestCount": 78
}
}
```
### 4.2 GET /api/user/heicode/models
**响应 200**:
```json
{
"success": true,
"data": {
"heicodeUserId": 123,
"email": "...",
"items": [/* NewAPI 全部模型清单(admin 视角)*/],
"count": 28
}
}
```
> 客户端结合 `/balance` 的 `group` 字段做用户可见性过滤。
### 4.3 GET /api/user/heicode/usage
Query: `days` (1-90,默认 30)
**响应 200**:
```json
{
"success": true,
"data": {
"heicodeUserId": 123,
"email": "...",
"days": 30,
"items": [/* NewAPI 按日 quota 统计 */],
"count": 30
}
}
```
### 4.4 GET /api/user/heicode/logs
Query: `limit` (1-200,默认 50),`page` (默认 1)
**响应 200**:
```json
{
"success": true,
"data": {
"heicodeUserId": 123,
"email": "...",
"page": 1,
"limit": 50,
"items": [/* NewAPI 调用日志 */],
"count": 50
}
}
```
### 4.x 共通错误码
| HTTP | code | 含义 |
|---|---|---|
| 401 | — | 未登录 |
| 404 | `HEICODE_USER_NOT_FOUND` | 在 NewAPI 找不到 email 的用户(用户需先在 Heicode 完成首次登录建账) |
| 502 | `HEICODE_NEWAPI_UPSTREAM_ERROR` | NewAPI 调用失败 |
| 503 | `HEICODE_NEWAPI_NOT_CONFIGURED` | mcp-server 未配置 admin token(见 §B-1) |
---
## 5. Agnet 平台本地 stub(12 接口)
### 5.0 公共说明
按 Heicode 仓库 `docs/integration/agnet-platform-request-contract.md` 字段形态返回 mock 数据。**用于前端联调**,真实 agent-manager 12 接口落地后切换为出站调用,**字段 / 路径 / 错误码 0 改动**。
存储:内存 dict,重启清空(仅联调用)。
**通用 Header**:
| Header | 必填 | 说明 |
|---|---|---|
| `Authorization: Bearer <accessToken>` | 是 | 复用登录 token |
| `X-Correlation-Id` | 建议 | mcp-server 生成;全链路追踪 ID |
| `X-Request-Id` | 建议 | 单次 HTTP 请求追踪 |
| `Idempotency-Key` | 创建类建议 | 同 key 返回原响应(避免重复创建) |
**通用错误码**:
| HTTP | business code |
|---|---|
| 400 | `POLICY_REJECTED` (字段缺/非法 / 高危无审批) |
| 403 | `MODEL_NOT_ALLOWED` / `FORBIDDEN_SCOPE` / `POLICY_REJECTED` (高危) |
| 404 | `NOT_FOUND` |
| 409 | `DEPLOYMENT_CONFLICT` |
| 422 | `RESOURCE_GRANT_INVALID` / `RESOURCE_GRANT_SECRET_REJECTED` / `RESOURCE_GRANT_SECRET_REF_REQUIRED` |
### 5.1 POST /api/agnet/deployments
**请求体**(详见 heicode 仓库 `agnet-platform-request-contract.md §2`,stub 严格按其字段校验):
```json
{
"orchestration_plan": {
"intent_id": "intent_xxx",
"template_hint": "manager-resource-binding",
"objective": "...",
"risk_level": "low",
"budget": {"max_tokens": 100000, "max_cost_usd": 30, "max_duration_sec": 7200},
"user_context": {"user_id": "...", "channel_id": "..."},
"billing_context": {"provider": "newapi"},
"agent_runtime": {"platform": "agnet", "agents": [...]},
"metadata": {"correlation_id": "corr_xxx"},
"agents": [{
"role_template": "builder",
"goal": "...",
"default_model_id": "...",
"resource_grants": [{
"grant_id": "...", "resource_id": "...", "resource_type": "git",
"user_id": "...", "binding_scope": "main",
"target_role": "builder", "target_agent_ref": "...",
"permission_scope": ["repo:read"], "status": "active",
"secret_ref": "vault://...",
"constraints": {}, "metadata": {}
}]
}]
}
}
```
**响应 200**:
```json
{"success": true, "data": {
"deployment_id": "dep_xxx",
"status": "accepted",
"agent_instances": [{"instance_id": "agi_xxx", "role": "builder", "phase": "pending"}]
}}
```
**校验规则(stub 已实现)**:
- `risk_level=high` 必须有 `resource_grants[].constraints.approval_id` 或 `audit.approval_id`
- `billing_context.provider` ∈ {`newapi`, `litellm`}
- 凭据型资源(git/sk/cloud_*)必须有 `secret_ref`
- `metadata` / `constraints` / `audit` key 不得含敏感词(password/token/secret/private_key/access_key/credential)→ 422 `RESOURCE_GRANT_SECRET_REJECTED`
- `target_role` 必须等于所属 agent 的 `role_template`
### 5.2 GET /api/agnet/deployments
Query: `user_id`, `binding_scope`, `status`, `limit` (1-500)
**响应 200**: `{success, data: {items: [...], total: N}}`
### 5.3 GET /api/agnet/deployments/{id}
**响应 200**: 完整 deployment 详情(含 agent_instances / resource_grants_summary / budget_consumed)
### 5.4 POST /api/agnet/deployments/{id}/stop
请求体可空。**幂等**:已 stopped 重复调返 200。
### 5.5 GET /api/agnet/deployments/{id}/logs
Query: `agent_instance_id`, `stream` (stdout/stderr/system/audit), `since`, `limit` (1-1000), `cursor`
**响应**: `{items: [{log_id, deployment_id, agent_instance_id, stream, level, message, redacted, occurred_at}], next_cursor, total}`
### 5.6 GET /api/agnet/deployments/{id}/logs/stream (SSE)
返回 `text/event-stream`,事件类型:`log` / `heartbeat` / `done`。
### 5.7 GET /api/agnet/projects/{binding_scope}/dashboard-snapshot
> 路径名 `projects` 是契约保留旧名,参数实际是 `binding_scope`。
**响应**: `{project_id, binding_scope, active_instances, phase_distribution, failure_rate_1h, avg_task_duration, budget, resource_usage, updated_at}`
### 5.8 GET /api/agnet/deployments/{id}/metrics
Query: `window` (默认 15m), `step` (默认 60s)
**响应**: `{deployment_id, window, step, series: [{metric, unit, points: [[time, value], ...]}, ...]}`
返回 9 个 metric: `tokens_used` / `cost_usd` / `duration_sec` / `cpu_millicores` / `memory_mb` / `restart_count` / `tool_call_count` / `error_count` / `queue_latency_ms`
### 5.9 GET /api/agnet/deployments/{id}/events
事件名(mock 已 seed): `deployment.accepted` / `instance.phase_changed` / `sk_snapshot_refreshed` / `resource_grant.attached` / `resource_grant.revoked` / `budget.threshold_reached` / `deployment.failed` / `deployment.stopped`
**响应**: `{items: [{event_id, event, schema_version, user_id, channel_id, binding_scope, deployment_id, correlation_id, occurred_at}], next_cursor, total}`
### 5.10 GET /api/agnet/audit-logs
Query: `user_id`, `binding_scope`, `actor`, `action`, `since`, `limit` (1-1000), `cursor`
### 5.11 POST /api/agnet/sk-snapshots/resolve
**请求体**: `{"deployment_id": "dep_xxx"}`
**响应**: `{deployment_id, items: [snapshot], total: 1}`
snapshot 形态: `{snapshot_id, deployment_id, user_id, binding_scope, source_type, source_ref, artifact_ref, checksum, status, resolved_at}`
### 5.12 GET /api/agnet/deployments/{id}/sk-snapshots
Query: `source_type`, `limit`, `cursor`
---
## 6. 任务编排(5 接口) — cc-haha 任务驾驶舱 / 工作台 配套
字段形态严格对齐 `cc-haha/desktop/src/stores/heicodeTaskStore.ts`。
**对象语义**:用户输入想法(intent)→ Heicode 追问(followups)→ 全部答完 → 生成任务卡(TaskCard)→ 进入"准备资源 / 部署"阶段。
**状态机**:
```
draft → configuring → running → (后续 awaiting_approval / completed / failed / paused)
```
### 6.1 POST /api/user/tasks/intent
**请求体**:
```json
{ "intent": "做一个面向小团队的任务协作 SaaS", "name": "可选" }
```
**响应 200** (`HeicodeTask`):
```json
{
"success": true,
"data": {
"id": "uuid",
"user_id": "uuid",
"name": "做一个面向小团队的任务协作 SaaS",
"status": "configuring",
"status_caption": "等待你回答几个问题",
"intent": "做一个面向小团队的任务协作 SaaS",
"thread": [
{"kind": "user", "text": "...", "at": 1715168400000},
{"kind": "heicode", "text": "好的。我先问你两个问题...", "at": 1715168400001,
"followups": [
{"id": "scope", "question": "你希望第一阶段交付到什么程度?",
"options": [
{"id": "mvp", "label": "MVP:能跑通主流程"},
{"id": "polish", "label": "完整功能 + UI 细节打磨"},
{"id": "prod", "label": "直接上生产", "risk": "high-risk"}
]},
{"id": "tech", "question": "技术栈倾向?",
"options": [
{"id": "modern_web", "label": "现代 Web(React + Node/Python)"},
{"id": "py_backend", "label": "Python 后端为主"},
{"id": "let_heicode", "label": "让 Heicode 决定"}
]}
]}
],
"card": null,
"created_at": 1715168400000,
"updated_at": 1715168400000
}
}
```
### 6.2 GET /api/user/tasks
Query: `status`, `limit` (1-200), `offset`
**响应**: `{items: [HeicodeTask], total, offset, limit}`
### 6.3 GET /api/user/tasks/{id}
**响应**: 单个 `HeicodeTask`
**错误**: 404 `NOT_FOUND` / 403 `FORBIDDEN_SCOPE`(跨用户)
### 6.4 POST /api/user/tasks/{id}/answer
**请求体**: `{"question_id": "scope", "option_id": "mvp"}`
**响应 200**:返回更新后的 `HeicodeTask`。
- 还有未答的 followup → `status` 仍 `configuring`,`card` 仍 null
- 所有 followup 都答完 → `status` → `running`,**`card` 自动生成**:
```json
{
"card": {
"goal": "做一个面向小团队的任务协作 SaaS",
"scope": ["MVP 范围:核心功能跑通", "技术债务可后续清理", "暂不做高级 UI/性能优化"],
"generated_artifacts": ["产品说明", "原型描述", "技术方案", "代码骨架(React + Node/Python 后端)"],
"manager_actions": [
{"label": "去 Manager 准备资源", "deeplink": "/manager/resources?from=task"},
{"label": "查看团队建议", "deeplink": "/manager/team?from=task"}
]
}
}
```
**错误**:
- 400 `INVALID_OPTION` — option_id 不在该 followup 的 options 列表里
- 404 `QUESTION_NOT_FOUND` — question_id 不存在
- 400 `TASK_STATE_INVALID` — task 当前 status 不允许再答
### 6.5 POST /api/user/tasks/{id}/messages
**请求体**: `{"text": "再补充:希望支持飞书集成"}`
**响应 200**:thread 追加 user 消息 + heicode ack("收到。我会把这条要求纳入任务上下文。")。MVP 不再触发新 followups。
### 6.6 GET /api/user/tasks/{id}/execution(Slice 8 — 执行反馈)
**请求**: 仅需 `Authorization: Bearer <jwt>`,无 query。
**响应 200**:`ExecutionState`(字段对齐 `cc-haha/desktop/src/stores/heicodeTaskStore.ts:ExecutionState`)
```jsonc
{
"success": true,
"data": {
"sub_steps": [
{"id":"step_1","title":"需求分解","status":"done","caption":"已完成","at": 1778237397000}
// status ∈ done|running|waiting|failed|skipped
],
"sk_tool_calls": [
{"id":"tc_1","name":"read_codebase","status":"done","summary":"...","at": 1778237397200}
],
"events": [
{"id":"ev_1","level":"info","message":"任务启动...","at": 1778237397100}
// level ∈ info|warn|error
],
"artifacts": [
{"id":"art_1","kind":"doc","label":"产品说明(PRD)","url":"/manager/artifacts/<task_id>/prd.md"}
// kind ∈ doc|api|diff|report
],
"spend_today": "¥12.30" // 可空
}
}
```
**生命周期约束**:仅 `task.status ∈ {running, awaiting_approval, completed}` 才有数据;其他状态返回**空 ExecutionState**(5 个字段都是空数组 / spend_today=null)。前端可据此显示"等待启动"占位。
**错误**: 404 `NOT_FOUND` / 403 `FORBIDDEN_SCOPE` / 401。
### 6.7 GET /api/user/tasks/{id}/delivery(Slice 9 — 交付结果)
**响应 200**:`DeliveryResult`
```jsonc
{
"success": true,
"data": {
"summary": "任务「...」已交付。包含产品说明、代码骨架、测试报告 3 大类产物...",
"deliverables": [
{
"id":"deliv_1","kind":"product-spec","title":"产品说明 PRD v1",
"primary_action": {"label":"查看文档","deeplink":"/manager/artifacts/<task_id>/prd.md"},
"secondary_action": {"label":"...","deeplink":"..."} // 可选
}
// kind ∈ product-spec|code-diff|test-env|prod-env
],
"quality": [
{"id":"q_1","label":"单元测试覆盖率","status":"pass","detail":"82%"}
// status ∈ pass|warn|fail
],
"next_actions": [
{"label":"去 Manager 准备资源","intent":"manager.resources"}
]
}
}
```
**生命周期约束**:仅 `task.status == "completed"` 时返回完整数据;其他状态所有字段都为空字符串 / 空数组。前端可据此显示"任务尚未完成"占位。
**错误**: 404 / 403 / 401。
### 6.8 GET /api/user/tasks/{id}/audit(Slice 10 — 任务详情抽屉)
**Query**: `tab` 可选,∈ `usage | resources | approvals | security`。**不传**则一次性返回 4 个 tab 全量;**传**则只填该 tab,其他 tab 字段返回空数组(schema 始终稳定)。
**响应 200**:`TaskAudit`
```jsonc
{
"success": true,
"data": {
"usage": [{"id":"u_1","model":"gpt-4o-mini","input_tokens":12400,"output_tokens":3200,"cost":"¥4.20","at_iso":"2026-05-08T..."}],
"resources": [{"id":"r_1","resource":"git/heicode-frontend","scope":"read","last_used_iso":"2026-05-08T..."}],
"approvals": [{"id":"<approval_uuid>","operation":"deploy","target_resource":"prod-cluster","risk_level":"high","decision":"approve","enqueued_iso":"...","resolved_iso":"..."}],
"security": [{"id":"s_1","event":"task_created","level":"info","at_iso":"..."}]
}
}
```
**`approvals` tab 是真表查询**(heicode_approvals where task_id=...,按 enqueued_at DESC,limit 50),其他 3 tab 当前是 deterministic mock,后续可接入真实数据源(usage 接 NewAPI logs / resources 接 resource_grants 使用日志 / security 接 audit_logs)。
**错误**:
- 400 `INVALID_TAB` — tab 不在允许枚举内
- 404 / 403 / 401
---
## 7. Server-Sent Events(单通道事件推送)
> **状态**:契约定义(mcp-server 端实现待发,预计 Day 2.5 上线 — 见 [`Heicode-对接进度与待办.md §7.11.1`](./Heicode-对接进度与待办.md))。本节字段形态**已冻结**,cc-haha 可立即按此搭 Slice 15 SSE 订阅骨架。
### 7.0 公共说明
mcp-server 提供**唯一一条 SSE 长连接**给前端订阅所有用户级实时事件(高危审批、任务状态、执行进度、保活)。
合并成单通道的好处:
- 客户端只维护 1 个 EventSource(节省资源)
- 后端只管 1 个推送泵
- 不同事件类型按 SSE `event:` 字段路由,互不冲突
### 7.1 GET /api/user/events/stream
**用途**:当前登录用户的实时事件流。
**请求**:
```http
GET /api/user/events/stream
Accept: text/event-stream
Authorization: Bearer <accessToken>
```
> ⚠️ 浏览器 `EventSource` 不支持自定义 header。前端建议用 `fetch` + `ReadableStream` 手动解析 SSE,或使用支持 header 的 polyfill(如 `eventsource-polyfill`)。
**响应**: `200 OK`,`Content-Type: text/event-stream`,长连接。
**事件路由**(按 SSE `event:` 字段):
| event 类型 | 用途 |
|---|---|
| `approval.requested` | 高危审批请求 — 弹审批弹窗 |
| `approval.resolved` | 审批已被响应(其他设备处理 / 过期)— 关闭对应弹窗 |
| `task.status_changed` | 任务 status 变化 — 切右侧面板 |
| `task.execution_progress` | sub_step 状态变化 — 刷新执行反馈面板 |
| `heartbeat` | 保活(每 25s)|
### 7.2 event: approval.requested
字段对齐 cc-haha `desktop/src/stores/approvalStore.ts:ApprovalRequest`。
**SSE 帧**:
```text
event: approval.requested
data: {"approval_id":"apr_xxx","task_id":"task_xxx","task_name":"…","operation":"在生产环境部署服务","target_resource":"vm://newapi-prod","requesting_role":"operator","risk_level":"high","impact_summary":["重启服务","回滚预案已备好"],"heicode_suggestion":"approve","derives_short_lived_credential":true,"ttl_minutes":60,"enqueued_at":"2026-05-08T10:00:00Z"}
```
**字段**:
| 字段 | 类型 | 说明 |
|---|---|---|
| `approval_id` | string | 审批唯一 ID |
| `task_id` | string | 关联任务 ID(可空,如系统级审批)|
| `task_name` | string | 任务名(弹窗显示)|
| `operation` | string | 待批准的操作描述 |
| `target_resource` | string | 受影响资源(如 `vm://newapi-prod` / `repo://org/foo`)|
| `requesting_role` | string | 发起请求的子 Agent 角色(builder / operator / reviewer 等)|
| `risk_level` | enum | `low` / `medium` / `high` |
| `impact_summary` | string[] | 影响摘要 bullet 列表 |
| `heicode_suggestion` | enum | Heicode 给出的建议:`approve` / `reject` / `delegate` |
| `derives_short_lived_credential` | bool | 批准后是否会生成短期凭据注入 Pod |
| `ttl_minutes` | int | 审批有效时长(minutes),过期自动 expired |
| `enqueued_at` | string (ISO 8601) | 入队时间 |
### 7.3 event: approval.resolved
**SSE 帧**:
```text
event: approval.resolved
data: {"approval_id":"apr_xxx","decision":"approve","resolved_by":"user_55","resolved_at":"2026-05-08T10:01:23Z"}
```
| 字段 | 类型 | 说明 |
|---|---|---|
| `approval_id` | string | 与 `approval.requested` 对应 |
| `decision` | enum | `approve` / `reject` / `expired` |
| `resolved_by` | string \| null | 谁做的决定(其他设备同步时使用;`expired` 时为 null)|
| `resolved_at` | string (ISO 8601) | 决定时间 |
客户端收到该事件应**关闭对应 approval_id 的弹窗**(避免多设备登录时弹窗鬼影)。
### 7.4 event: task.status_changed
**SSE 帧**:
```text
event: task.status_changed
data: {"task_id":"task_xxx","old_status":"running","new_status":"completed","status_caption":"已交付","at":"2026-05-08T10:05:00Z"}
```
| 字段 | 类型 | 说明 |
|---|---|---|
| `task_id` | string | 任务 ID |
| `old_status` | string | 之前的 status(参见 §6.1 task.status 枚举)|
| `new_status` | string | 当前 status |
| `status_caption` | string \| null | 副标题(如"等待生产部署审批")|
| `at` | string (ISO 8601) | 变化时间 |
客户端应据此切换右侧面板(running/awaiting_approval → ExecutionFeedbackPanel;completed → DeliveryResultPanel)。
### 7.5 event: task.execution_progress
**SSE 帧**:
```text
event: task.execution_progress
data: {"task_id":"task_xxx","sub_step_id":"step_compile","status":"done","caption":"编译通过","at":"2026-05-08T10:03:12Z"}
```
| 字段 | 类型 | 说明 |
|---|---|---|
| `task_id` | string | 任务 ID |
| `sub_step_id` | string | 步骤 ID(与 `/api/user/tasks/{id}/execution` 返回的 `sub_steps[].id` 对应)|
| `status` | enum | `waiting` / `running` / `done` / `failed` / `skipped` |
| `caption` | string \| null | 步骤当前描述 |
| `at` | string (ISO 8601) | 变化时间 |
客户端应据此**就地刷新** ExecutionFeedbackPanel 中对应 sub_step 行(不需要重拉整个 execution)。
### 7.6 event: heartbeat
**SSE 帧**:
```text
event: heartbeat
data: {"server_time":"2026-05-08T10:00:25Z"}
```
| 字段 | 类型 | 说明 |
|---|---|---|
| `server_time` | string (ISO 8601) | 服务端时间,便于客户端检测时钟漂移 |
发送频率:每 **25 秒**一次。客户端 60 秒未收到 heartbeat 应主动重连。
### 7.7 配套 REST 端点
实时通道之外,前端启动时需要拉一次"当前未处理审批",并提供"用户响应审批"入口:
#### 7.7.1 GET /api/user/approvals
**用途**:拉取当前用户**待响应**的审批列表(启动时调一次,后续靠 SSE 增量)。
**响应 200**:
```json
{
"success": true,
"data": {
"items": [/* ApprovalRequest 对象数组,字段同 §7.2 */],
"total": 0
}
}
```
#### 7.7.2 POST /api/user/approvals/{approval_id}/decision
**用途**:用户在弹窗里选择批准/拒绝。
**请求体**:
```json
{ "decision": "approve" }
```
`decision` ∈ `approve` / `reject`(不能传 `expired`,那是服务端自动设置)。
**响应 200**: 返回更新后的 `ApprovalRequest` 对象(含 `decision` / `resolved_by` / `resolved_at`)。
mcp-server 此时会**广播** `approval.resolved` 事件到该用户所有 SSE 连接(其他设备的弹窗会自动关闭)。
**错误响应**:
| HTTP | code | 含义 |
|---|---|---|
| 404 | `NOT_FOUND` | approval_id 不存在 |
| 403 | `FORBIDDEN_SCOPE` | 该审批不属于当前用户 |
| 410 | `APPROVAL_EXPIRED` | 已超时,决定无效 |
| 409 | `ALREADY_RESOLVED` | 已被响应(其他设备处理)|
### 7.8 错误处理(SSE)
| 场景 | mcp-server 行为 | 客户端建议 |
|---|---|---|
| 401(token 过期) | 立即关闭流 | 调 `/api/auth/refresh` 后重连 |
| 503 / 网络抖动 | — | 指数退避重连(1s / 2s / 5s / 10s)|
| 60s 无 heartbeat | — | 主动重连 |
### 7.9 客户端订阅最简实现(参考)
```ts
// cc-haha/desktop/src/lib/heicodeEventStream.ts
async function subscribe(token: string, handlers: { [evt: string]: (data: any) => void }) {
const res = await fetch(`${BASE}/api/user/events/stream`, {
headers: { Authorization: `Bearer ${token}`, Accept: 'text/event-stream' },
})
const reader = res.body!.getReader()
const decoder = new TextDecoder()
let buf = ''
while (true) {
const { value, done } = await reader.read()
if (done) break
buf += decoder.decode(value, { stream: true })
let idx
while ((idx = buf.indexOf('\n\n')) !== -1) {
const frame = buf.slice(0, idx); buf = buf.slice(idx + 2)
const evtLine = frame.split('\n').find(l => l.startsWith('event:'))
const dataLine = frame.split('\n').find(l => l.startsWith('data:'))
if (!evtLine || !dataLine) continue
const event = evtLine.slice(6).trim()
const data = JSON.parse(dataLine.slice(5).trim())
handlers[event]?.(data)
}
}
}
```
---
## 10. 版本历史
| 版本 | 日期 | 变更 |
|---|---|---|
| v1.0 | 2026-05-05 | 初版:登录 4 接口 + P1 资源模型 9 接口 |
| v2.0 | 2026-05-08 | + NewAPI 元数据透传 4 接口(§4) + Agnet stub 12 接口(§5) + 任务编排 5 接口(§6)。**总计 34 接口** |
| v2.1 | 2026-05-08 | + SSE 单通道事件推送契约(§7)+ 配套审批 REST 2 接口(§7.7)。**冻结字段形态供 cc-haha Slice 15 起骨架;mcp-server 实现 Day 2.5 上线** |
待开发(参考 [Heicode-对接进度与待办.md](./Heicode-对接进度与待办.md)):
- 高危审批 SSE 推送(cc-haha 客户端 ApprovalDialog 配套)
- P2 Secret Broker(等 Vault 部署)
- P5 出站调用真实 Agnet 平台(等 agent-manager 12 接口)
---
## B. Heicode 团队待办清单
> 按 mcp-server 当前已交付的能力,Heicode 这边需要做以下 5 件事让全链路真跑起来。括号内是优先级与阻塞影响。
### B-1 配置 NewAPI service token(🔴 高优 — 阻塞 P4 真跑)
**Heicode 端要做**:
1. 在 NewAPI 创建一个专用账号 `mcp-server-service`,角色 = admin
2. 生成永久 access token
3. 把 token + admin user_id 给 mcp-server 团队(**通过安全通道,绝不写 Git/聊天/邮件**)
**mcp-server 端要做**(拿到后 ~30 分钟):
- 通过 K8s secret 注入 env:`HEICODE_NEWAPI_SERVICE_TOKEN` + `HEICODE_NEWAPI_ADMIN_USER_ID`
- `kubectl rollout restart` 即可生效(**无需重新构建镜像**)
### B-2 答复 P4 2 个细节(🟡 中优 — 不阻塞但影响实现细节)
详见 [`Heicode-对接进度与待办.md §2.3.2`](./Heicode-对接进度与待办.md)。两个问题:
- 细节①:mcp-server 调 NewAPI 时 `New-Api-User` 怎么映射到 heicode 本地 user_id?(mcp-server 当前用 admin search by email,工作但有 1 次往返开销)
- 细节②:子 Agent 部署 `billing_context.provider` 怎么决定?(mcp-server 默认 `litellm`,需 Heicode 用户场景默认 `newapi` 时给规则)
### B-3 协调 heicode web 团队对接 P1 资源绑定 UI(🟡 中优)
cc-haha 桌面客户端切片 5 已下线 P1 UI(按 Heicode 产品包 08-client-guide.md 边界)。资源绑定 UI 现在归 **heicode web (`code.xinghanlab.com`)**。需要:
- heicode web 团队接 mcp-server 的 9 个 P1 接口(§2 / §3)
- 否则用户从客户端任务卡点"去 Manager 准备"会 404
### B-4 推动 agent-manager 团队实现 12 真实接口(🟢 中优 — 阻塞 P5 出站联调)
agent-manager 12 接口契约见:
- Heicode 仓库 `docs/integration/agnet-platform-request-contract.md`
- mcp-server 仓库 `Docs/Agent-Manager-Heicode对接需求文档.md`
mcp-server 已上线 stub(§5)字段形态完全一致,agent-manager 实装时可直接对照。
**Heicode 这边要给 mcp-server**:
- agent-manager 团队对接负责人
- staging 联调环境地址(建议 `staging-agnet.xinghanlab.com`)
- 服务令牌方案确认(建议 A pre-shared bearer 起步)
### B-5 联调测试(🟢 低优 — 可并行)
cc-haha 客户端 / heicode web 拿到本文档后即可开始联调:
- 登录链路(§1)已实测,无需 mcp-server 配合
- P1 资源(§2 / §3)已实测,无需 mcp-server 配合
- Agnet stub(§5)已实测,**前端可立即开始任务执行联调**
- 任务编排(§6)已实测,**cc-haha 可立即把任务驾驶舱 / 工作台从 mock 切真**
- NewAPI 透传(§4)等 B-1 完成后真跑
如发现接口问题,按 §9 排障流程提交。
---
## C. 总账速查
| 模块 | 章节 | 端点数 | 状态 |
|---|---|---|---|
| 登录认证 | §1 | 4 | ✅ 上线 + Heicode 已对接 |
| P1 资源绑定 | §2 | 5 | ✅ 上线 |
| P1 资源授权 | §3 | 4 | ✅ 上线 |
| P4 NewAPI 透传 | §4 | 4 | ✅ 上线(待 B-1 token 真跑)|
| P5 Agnet stub | §5 | 12 | ✅ 上线(mock 数据) |
| 任务编排 | §6 | 5 | ✅ 上线 |
| **合计** | — | **34** | **全部上线** |