Files
heicode/docs/integration/heicode-desktop-client-api.md
T
chenchenandClaude Opus 4.8 292504735e feat(swarm): add goal_summary to swarm status view (#45/#28 consumer ask)
@Mem0ried 客户端 consumer 验收(#59)指出 GET /swarms/:id 缺 goal_summary —— Run 列表
只能显示 deployment_id/status,体验差。补:swarmDeploymentView 增加 goal_summary,从持久化
plan_json 顶层 objective 提取,折叠空白为单行 + 截断(200 rune)+ RedactText 兜底;取不到
(无 plan / 无 objective / 坏 JSON)返回空串,不臆造。list 与 detail 同走 swarmDeploymentView,
两处都带上。

文档 docs/integration/heicode-desktop-client-api.md §5.2 状态样例补 goal_summary 字段说明。
测试 TestSwarmGoalSummary 覆盖:空/坏 JSON/无 objective→空;多行多空格折叠;误入 sk- 被脱敏;
超长截断带省略号。go build ./... 与 controller 测试全绿。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 21:17:32 +08:00

515 lines
31 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 桌面客户端 对接接口文档(模板 Agent 模型 · v1)
> 更新时间:2026-06-04
> 适用:Heicode Desktop 对接 Heicode Manager(HM)的**模板 Agent 模型**。
> 生产地址:`https://code.xinghanlab.com`
> 状态图例:🟢 已实现并生产验证 · 🔴 待建(依赖 AM)
> **本文取代旧的桌面 sub 任务编排接口文档**(描述 tasks/workflow/display_status/git_ref/产物下载的那套已下线,相关 md 已删除)。新客户端一律按本文对接。
>
> **2026-06-04 二次复测:端到端已跑通**(`code.xinghanlab.com`,真实账号):部署→`running`→直连 `/health`/`/message/send`(带令牌任务 `completed`)→stop→delete 全通,`access_token` 为 HM 现签非空 UUID。**仅剩 AM 两项待加固**(不影响功能):① agent 端尚未真正启用令牌校验(无令牌也被放行);② 子域名目前是 `http://` 明文。详见 AM 契约 §0.1。
---
## 0. 模型总览(先读)
新模型很简单:
1. 用户在 **HM 网页控制台**绑定资源(git/vm/数据库/对象存储)、选「资源 + 模板」**部署一个常驻 agent**。
2. HM 把所选资源解出后注入 agent 的 `.env`(并**现签一把 per-agent 访问令牌**一起注入),交给 **AM** 启动;AM 返回**唯一子域名**。
3. **桌面客户端**从 HM 读 agent 列表 → 拿到**子域名 + 访问令牌**(HM 现签的 `access_token`)→ **直连 agent 子域名(SSE)对话使用**。**HM 不在对话回路里。**
4. **模型调用**:客户端自己用模型、以及 agent 用模型,**都走 HM `/v1/*`**(鉴权 + 计费)。
> 客户端在新模型里的核心动作 = **列出我的 agent → 直连用**。部署/管理主要在网页台完成(客户端也可调同一接口)。
**总原则**
- 客户端调 HM 用 **V2 设备签名**(与模型调用同一套,见 §1)。
- 列表里的 `access_token` 是连 agent 用的**专属访问令牌**(HM 给每个 agent 现签,只有部署它的你拿得到)。直连时带上它(`X-Agent-Access-Token` 头);**agent 校验「是不是该 agent 的所有者」**。它与模型 `api_key` 职责分离:`access_token` 管「谁能调这个 agent」,`api_key` 管「用谁的额度跑模型」。
- 模型永远走 HM `/v1/*`,不要直连上游。
---
## 1. 认证与加密 🟢
桌面客户端用**设备绑定密钥**对每个请求做 Ed25519 签名;有 body 的写请求额外用 X25519-ECDH + HKDF + ChaCha20-Poly1305 加密 body。**与调模型 `/v1/*` 用的是同一套**(`cc-haha/src/services/device/signRequest.ts`),无需新增协议。本节自包含,照此即可实现。
**每个请求都带的头:**
| 头 | 含义 |
|---|---|
| `X-Heicode-Device-Id` | 设备 id(配对时获得) |
| `X-Heicode-Timestamp` | unix 毫秒(服务端校验时钟偏移窗口) |
| `X-Heicode-Nonce` | 每次随机,防重放 |
| `X-Heicode-Fingerprint` | 设备指纹 |
| `X-Heicode-Eph-Pubkey` | 本次临时 X25519 公钥(base64),**每请求新生成** |
| `X-Heicode-Signature` | 下面 canonical 的签名(base64) |
| `Content-Encoding: heicode-aead-v1` | **仅有加密 body 的写请求带**;GET 不带 |
**签名 canonical(字节序固定,服务端按此重建,顺序不可改):**
```text
canonical = method + "\n" # "GET" / "POST" / "DELETE"
+ path_with_query + "\n" # 服务端实际收到的 path(含 query,不含 origin)
+ timestamp_ms + "\n"
+ nonce + "\n"
+ fingerprint + "\n"
+ eph_pubkey_b64 + "\n"
+ hex( sha256(body明文) ) # 无 body(GET) 填 hex(sha256("")) = e3b0c442…b855
signature = base64( ed25519_sign( device_priv, sha256(canonical) ) )
```
- **GET / 无 body**:不发 `Content-Encoding`、body 不加密,canonical 末项填空串哈希。
- **POST / DELETE 有 body**:发 `Content-Encoding: heicode-aead-v1`;body = 用 `eph_priv × 服务器公钥` 做 X25519-ECDH → HKDF → ChaCha20-Poly1305 加密;canonical 末项是**加密前明文 body** 的 sha256(hex)。
- 服务端中间件 `UserOrV2DeviceAuth` 解密 + 验签,从设备绑定 token 解析用户身份。
**鉴权失败时的响应头**(客户端据此给用户准确提示,而非笼统"token 失效"):
- `X-Heicode-Auth-Error`:`timestamp_drift`(时钟偏移)/ `nonce_replay` / `revoked` / `fingerprint_mismatch` / `signature_invalid` / `device_not_found` / `eph_pubkey_missing` …
- `X-Heicode-Server-Time`:服务器 unix 毫秒(用来校正本地时钟,解决 `timestamp_drift`)。
> 设备如何拿到 `device_id` + 设备密钥:走与模型调用相同的**设备配对/登录流程**(`cc-haha/src/services/device`、`/api/heicode-auth/*`),本文不重复。
### 1.1 注销登录(logout)🟢
桌面客户端用**设备绑定 token** 登录,注销 = **吊销当前设备的 token**(不是清会话 cookie):
| 方法 | 路径 | 鉴权 | 说明 |
|---|---|---|---|
| POST | `/api/devices/logout` | V2 设备签名 / 会话 | 吊销**当前设备**的 token |
- **设备客户端**:用 V2 设备签名调用即可(空 body 也行)——服务端按签名头 `X-Heicode-Device-Id` 找到**本设备**的 token 并吊销。**只能注销自己,动不了用户的其它设备。**
- **会话/JWT 调用方**:可在 body 传 `{"device_id":"..."}` 注销该用户名下某台设备。
- **幂等**:设备已不存在也返回 `{"success":true}`(注销已达成)。
- 调用成功后客户端应**同时删除本地设备密钥**;要再用需**重新配对**(`POST /api/devices/pair`)。
```json
// 成功
{ "success": true }
```
> 其它设备管理(会话/JWT):`GET /api/devices/`(列我的设备)、`PATCH /api/devices/:id`(改名)、`DELETE /api/devices/:id`(按 id 吊销某设备)。这些走 `UserAuth`,网页台「设备」页用;客户端自助注销用上面的 `/api/devices/logout`。
---
## 2. 账户与余额 🟢(可选;用**用户会话/JWT**,非设备签名)
| 方法 | 路径 | 鉴权 | 说明 |
|---|---|---|---|
| GET | `/api/user/self` | **`UserAuth`(会话 cookie / 用户 JWT)** | 当前用户:`quota`(剩余)、`used_quota`(累计已用)、`request_count`、分组 |
| GET | `/api/user/self/models` | 同上 | 当前用户可用模型 |
```json
// data 摘要
{ "id":22, "username":"chenchen", "quota":..., "used_quota":..., "request_count":... }
```
> ⚠️ **注意鉴权不同**:`/api/user/self*` 走 `UserAuth`,**不接受设备签名**——客户端要用登录拿到的**用户会话/JWT**(`Authorization: Bearer <jwt>` 或会话 cookie)调用。`/api/heicode/*`(§3–§5)走 `UserOrV2DeviceAuth`,设备签名即可。
> 余额仅用于展示用量,**非客户端核心流程**(部署在网页台);拿不到也不影响"列 agent → 直连用"。
---
## 3. 能力发现 🟢
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/api/heicode/capabilities` | 模型目录(渲染模型选择);免登录 |
```json
// 生产实测:models = [{"id":"gpt-5.4",...}](modes 仍返回 sub_agile/swarm,客户端忽略即可)
{ "success": true, "data": { "models": [{"id":"gpt-5.4","name":"gpt-5.4","available":true}] }}
```
> 客户端只需 `models`(当前可用 `gpt-5.4`);`modes` 是旧任务模式,新模型已无意义。
> ⚠️ **能力发现 vs 登录用户模型列表**:`/api/heicode/capabilities` 是**免登录**的总目录(渲染模型选择用)。**登录后客户端展示的「我能用哪些模型」必须走 §3.1 `/api/heicode/available-models`**——它按当前用户的分组/订阅服务端收口,且**只有** HM 这一个来源:客户端不得使用本地 preset,也不得从 CodeGW 渠道后台读取模型。
---
## 3.1 登录用户可用模型 🟢(模型列表收口)
| 方法 | 路径 | 鉴权 | 说明 |
|---|---|---|---|
| GET | `/api/heicode/available-models` | `UserOrV2DeviceAuth`(会话/JWT 或设备签名) | 当前登录用户**实际可用**的模型列表;服务端按用户可用分组 → 分组启用模型解析 |
```json
{ "success": true, "data": {
"available_models": [
{ "model_id": "gpt-5.4", "display_name": "gpt-5.4", "default": true }
]
}}
```
- 字段仅 `model_id` / `display_name` / `default`(默认模型)。
- **禁止暴露字段**:`channel_id`、`base_url`、`api_key`、供应商类型、价格/倍率等一律不返回。
- **唯一模型来源**:客户端登录后模型列表完全来自此接口,不使用本地 preset / 不读 CodeGW 渠道后台。
- 鉴权要求登录用户上下文(`id>0`);未登录返回 `authentication required`。
---
## 4. Agent 模板列表 🟢
供客户端展示"有哪些 agent 角色可用"。模板由 HM 维护(管理员可在控制台增改),**展示用中文名 + 中文简介**。
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/api/heicode/agent-templates` | 列出可部署的 agent 模板 |
```json
{ "success": true, "data": {
"total": 19,
"items": [
{"template_id":"architect","name":"架构顾问","description":"只读地分析代码、定位缺陷、给出架构与调试建议","model":"opus","status":"active"},
{"template_id":"code-reviewer","name":"代码审查员","description":"审查代码改动,找出 bug 与质量问题","model":"...","status":"active"}
]
}}
```
- `template_id` 是模板的稳定 key(如 `architect`),部署时回传。
- `name` / `description` 为中文,直接展示。
- `model` 是模板的**角色层级提示**(如 `opus`),**不是实际跑的模型**——agent 实际用网关模型 `gpt-5.4`(部署时 HM 注入)。客户端不用管这个字段。
---
## 4.1 启动前预检 / 执行摘要 🟢(preflight)
部署 agent 前,给用户看一份「执行摘要」:用哪些资源、还缺什么、有哪些高危操作、预算上限。用户**只确认摘要**,不必面对完整参数。
| 方法 | 路径 | 鉴权 | 说明 |
|---|---|---|---|
| GET | `/api/heicode/preflight?template_id=&binding_ids=1,2,3` | `UserOrV2DeviceAuth` | 返回缺失项 + 可读执行摘要 + `version` |
| POST | `/api/heicode/preflight/confirm` | `UserOrV2DeviceAuth` | 确认摘要 → 记审计 + 返回防篡改 `version`(#41) |
- `binding_ids` 同部署入参(逗号分隔或重复 key,可空)。
```json
{ "success": true, "data": {
"template_id": "architect",
"agent_role": { "template_id":"architect", "name":"架构顾问", "model":"opus" },
"resources": [
{ "binding_id":17, "type":"git", "provider":"github", "name":"my-repo", "status":"active", "has_secret":true }
],
"invalid_bindings": [],
"missing": [
{ "kind":"sk", "reason":"未绑定SK 资源包" },
{ "kind":"budget", "reason":"账户可用额度不足,请充值或开通订阅" }
],
"high_risk_ops": [
{ "op":"production_deploy", "label":"生产部署 / 代码改动", "requires_approval":true },
{ "op":"large_budget", "label":"大额预算消耗", "requires_approval":true }
],
"budget": { "remaining_quota":1234567, "quota_per_unit":500000, "tier_max_agents":5, "current_agents":1 },
"approval_policy": { "mode":"per_high_risk_op" },
"ready": false,
"version": "pfv1_3a9c…"
}}
```
- **`missing`**:必需类别(`git`/`sk`/`project_document`/`cloud_account`)未绑定、`budget`(余额≤0)、`agent_slot`(在跑数已达 tier 上限)。`ready=true` 当且仅当 `missing` 为空。
- **`high_risk_ops`**:固定 enum —— `production_deploy` / `db_write` / `cloud_resource_delete` / `production_secret` / `large_budget`;由已绑资源类型推导,均 `requires_approval`。
- **红线**:`resources` 只暴露 `type/provider/name/status/has_secret`(布尔),**绝不返回 `secret_ref`/`channelId`/`base_url`/价格**。
- **`invalid_bindings`**:请求里无效 / 非本人 / 非 active 的绑定 id(不阻断,供前端提示)。
- **`version`**:防篡改摘要版本(#41),由**稳定安全面**派生(template + 资源 + 高危 + 必需缺失项);**不含**易变的预算数字,故余额波动不会改版本。
### 4.1.1 确认 + 防篡改版本(#41)
`POST /api/heicode/preflight/confirm` body:`{ "template_id":"architect", "binding_ids":[17] }`
```json
{ "success": true, "data": {
"version": "pfv1_3a9c…", // 把它带到部署请求
"template_id": "architect",
"summary": { …同上执行摘要… },
"confirmed": true
}}
```
- 仅当 `ready=true` 才能确认;否则返回 `POLICY_REJECTED`(先补齐缺失项)。
- 确认会**持久化一条强一致的确认记录**(user+template+version,默认 TTL `HEICODE_PREFLIGHT_CONFIRMATION_TTL_SECONDS`=3600s),并附带写一条审计事件 `preflight.confirmed`。`version` 的派生**已纳入模板安全面**(definition/model/name)——管理员改了同一模板的 definition/model,旧确认即失效。
- **部署校验**:`POST /api/heicode/agents` 可带 `preflight_version`。HM 校验三件事:① 用**当前**资源/模板状态重算版本 == 传入(资源增删/改类型/改凭证、模板变更 → 不匹配即拒);② 当前摘要仍 `ready`(预算/agent_slot 等易变项重查,防 confirm 后余额耗尽仍启动);③ **存在该版本的未过期确认记录**(杜绝直接拿 GET 的 `version` 绕过 confirm)。
- 默认**仅在带了 `preflight_version` 时校验**(向后兼容,不带照常部署);设 `HEICODE_PREFLIGHT_REQUIRED=true` 则**强制**要求先 confirm。
---
## 5. 我的 Agent 🟢(已对接 AM,生产验证)
> **2026-06-04 生产实测**:部署 / 列表 / 详情 / 状态 / 删除均真实可用。
> - `subdomain` 由 AM 分配(如 `dep-xxxx.taijiagnet.com`);新建后 `status` 为 `Pending`,需等 agent 起来变 `running`(直连前先确认,见 §6)。
> - **`access_token` 是 HM 现签的 per-agent 专属访问令牌**(非空,UUID)——直连 agent 时带上(`X-Agent-Access-Token` 头),agent 据此判定「是不是本 agent 的所有者」(见 §6)。
> - `stop`/`delete` 调 AM:**2026-06-04 复测均已通**——`stop` 返回 `status:stopped`,`delete` 返回 `runtime_cleanup:"ok"`(旧版的 404/500 已修)。`delete` 仍保留容错:即便 AM 删除失败也会清掉 HM 本地记录并以 `runtime_cleanup:"failed"` 提示。
| 方法 | 路径 | 说明 | 状态 |
|---|---|---|---|
| GET | `/api/heicode/agents` | 列出我部署的 agent | 🟢 |
| GET | `/api/heicode/agents/{agent_id}` | agent 详情(读取时刷新状态) | 🟢 |
| GET | `/api/heicode/agents/{agent_id}/status` | 主动拉 agent 实时状态(是否运行/挂了) | 🟢 |
| POST | `/api/heicode/agents` | 部署:`{template_id, binding_ids:[...]}` | 🟢(调 AM 🔴) |
| POST | `/api/heicode/agents/{agent_id}/stop` | 停止 | 🟢 |
| DELETE | `/api/heicode/agents/{agent_id}` | 删除 | 🟢 |
**列表 / 详情 / 部署成功 返回的 agent 对象:**
```json
// 生产实测响应(GET /api/heicode/agents 的一项 / 部署成功返回)
{ "success": true, "data": {
"items": [
{
"agent_id": "dep_4bb07dc1e376", // HM 侧 agent 记录 id(部署/停删都用它)
"template_id": "architect", // 用了哪个模板
"subdomain": "dep-4bb07dc1e376.taijiagnet.com", // ★ 直连地址(AM 分配,主机名)
"access_token": "550e8400-e29b-41d4-a716-446655440000", // ★ per-agent 访问令牌(直连带上)
"binding_ids": [], // 挂了哪些资源绑定(网页台绑的)
"status": "Pending", // Pending | running | failed | stopped …
"runtime_id": "dep-4bb07dc1e376", // AM 侧运行时 id
"created_at": "2026-06-04T09:04:15Z",
"updated_at": "2026-06-04T09:04:15Z",
"security": { // ★ #55 A2A 直连安全元数据
"scheme": "http", // http | https
"security_profile": "none", // none | tls | mtls
"secure": false // = security_profile != none
}
}
],
"total": 1
}}
```
**部署请求** `POST /api/heicode/agents`:
```json
{ "template_id": "architect", "binding_ids": [17] }
```
- `binding_ids` 是网页台「资源绑定」里已绑资源的 id(可空数组,表示不挂资源)。
- HM 处理:加载模板 `.md` → 把所选资源按类型解成 env(非密配置 + KV 解出密钥)+ 注入模型环境(`OPENAI_BASE_URL`/`OPENAI_API_KEY`/`MODEL_NAME=gpt-5.4`)→ 交 AM 启动 → 存子域名返回。
**状态查询** `GET /api/heicode/agents/{id}/status`:
```json
{ "success": true, "data": { "agent_id":"dep_…", "status":"running", "updated_at":"2026-06-04T…" }}
```
---
## 5.1 Agent 模型用量 🟢(按部署 Agent 聚合)
| 方法 | 路径 | 鉴权 | 说明 |
|---|---|---|---|
| GET | `/api/heicode/agents/{deployment_id}/usage` | `UserOrV2DeviceAuth`,且只能查**自己**的 agent | 按该 agent 的隐藏模型 token(name=`agent:<deployment_id>`)在计费 logs 中聚合用量 |
查询参数(可选):`start` / `end` = unix 秒时间窗(缺省=全窗口)。
```json
{ "success": true, "data": {
"agent_id": "dep_4bb07dc1e376",
"quota": 12345, // 消耗的额度(配额单位)
"prompt_tokens": 8000,
"completion_tokens": 4000,
"call_count": 12,
"quota_per_unit": 500000, // 额度→货币换算分母(quota/quota_per_unit=美元额度)
"budget_remaining": 1234567 // ★ 用户钱包剩余额度(预算剩余;-1=读取失败,不阻断展示)
}}
```
- **空数据语义**:无调用记录时各计数为 `0`(仍返回 `success:true`,不是 404)。
- **`budget_remaining`(#9)**:用户剩余可用额度(同 `quota_per_unit` 口径换算)。Agent 模型调用经隐藏 token 计费到 `user.Quota`,故"本任务预算剩余"= 用户钱包剩余额度。
- **与 billing logs 的关系**:用量来自统一计费 logs(`SumAgentUsage`),按 token name `agent:<deployment_id>` 过滤聚合 —— 即 agent 走 HM `/v1/*` 的真实消耗,与用户钱包/订阅扣费同源。
- **计费归集语义(#30)**:每个部署的 agent,HM 为其 mint 一个**隐藏、不展示在用户 token 列表**的模型 token(`UnlimitedQuota:true`)。`UnlimitedQuota` 的含义是「**不对该 token 自身设单独的剩余额度上限**」——它**不**绕过用户额度:agent 经此 token 调 `/v1/*` 时,HM 仍先校验 `user.Quota`,并在结算时从 `user.Quota`(钱包)或订阅项扣费、写计费 log,完全经过计费表达式。停止/删除 agent 后该 token 被撤销,旧 token 无法再调 `/v1/*`。该 token 对普通用户隐藏,但审计/管理员可追踪。
---
## 5.2 Swarm 运行查询 🟡(#45 Phase1 · 只读预览)
多 Agent 蜂群运行(`agent_swarm` / HeiCode Swarm)的**只读查询**。数据全部来自 HM 已持久化的运行时回调(Swarm → HM 带签名回调),**无需实时调 Swarm**,故不受 `agent_swarm#2` 契约冻结阻塞。
| 方法 | 路径 | 说明 | 状态 |
|---|---|---|---|
| GET | `/api/heicode/swarms` | 列出我的 swarm 运行 | 🟢 本地数据 |
| GET | `/api/heicode/swarms/:id` | 单个运行状态(:id = deployment_id / swarm_id / correlation_id 任一) | 🟢 |
| GET | `/api/heicode/swarms/:id/events?after=&limit=` | 事件增量拉取(`after`=上次返回的 `next_after`,oldest-first) | 🟢 |
| GET | `/api/heicode/swarms/:id/artifacts` | 从已存事件派生的产物 | 🟢(派生) |
| POST | `/api/heicode/swarms/:id/stop` | 停止运行(**写**) | 🟡 待 `agent_swarm#2` 冻结 + `SWARM_RUNTIME_ENABLED=true` |
```json
// GET /api/heicode/swarms/:id
{ "success": true, "data": {
"deployment_id":"dep_…", "swarm_id":"swarm-…", "correlation_id":"…",
"status":"blocked", // 运行时真实状态(契约 §4)
"display_status":"degraded", // 客户端展示态(§4.1 映射:blocked→degraded,余直通)
"goal_summary":"…", // 单行目标(从 plan objective 提取,折叠/截断/脱敏;取不到为空串)
"phase":"…", "runtime_state":"…", "failure_reason":"",
"created_at":"…","updated_at":"…","runtime_last_sync_at":"…" }}
// GET /api/heicode/swarms/:id/events?after=120
{ "success": true, "data": {
"items":[ {"id":121,"sequence":42,"event_type":"task.completed","task_id":"…","result":"ok","occurred_at":"…","payload":{…}} ],
"next_after":121, "count":1 }}
// GET /api/heicode/swarms/:id/artifacts (从 artifact.created 事件派生,扁平)
{ "success": true, "data": {
"items":[ {"event_id":"evt-…","sequence":50,"task_id":"…","uri":"azblob://…","checksum":"sha256:…","created_at":"…"} ],
"total":1 }}
```
- 状态机(契约 §4):`waiting_approval → running →(blocked ⇄ running)→ completed/failed/stopped`;客户端展示用 `display_status`(§4.1:`blocked→degraded`;`preparing`/`verifying` 是运行时 running 子态,HM 未单独存,不臆造)。
- **事件**:`sequence` = agent_swarm event-schema v1 的 **per-swarm 严格递增序号**(每 swarm 从 1、无空洞),客户端用它去重/排序;`id`/`next_after` 是 HM 不透明分页游标(单调,兼容 `sequence` 尚未全量上线)。事件 `payload` **已脱敏**(递归剔除 `secret_ref`/`credential_ref`/`signing_secret_ref`/credentials/大字段 + `RedactText` 兜底)。查询按**当前用户**作用域。
- **新增事件类型**(已注册):`swarm.completed/failed/stopped`、`approval.approved/rejected`、`handoff.created`(event-schema v1)。
- **artifact**:从 `artifact.created` 派生扁平 `{uri,checksum,task_id,size_bytes?,created_at}`(**无 secret_ref**;size 未知则省略,不伪造)。
- `stop`(写):仍 gated,待 stop 真实接入 PR(复用运行时客户端 + `SWARM_RUNTIME_SERVICE_TOKEN`,契约 §3 已冻结)落地。
---
## 6. 直连 Agent(客户端 ↔ agent,A2A 协议)
> 客户端**直接连 agent 子域名**(§5 的 `subdomain`),不经过 HM。agent 是 AM 的 `coding_a2a_agent`,走 **A2A 协议**。
> ⚠️ **连之前先确认 agent 就绪**:新建后 `status=Pending`(还在拉起)。等 `GET /api/heicode/agents/{id}/status` 变 `running`、或 `GET {subdomain}/health` 返 200 再连。**2026-06-04 复测:数秒即 `running`,`/health` 200、`/message/send` 带令牌任务 `completed`,直连已通。**
>
> ⚠️ 当前 AM 侧两点(待加固,不影响调通):① **令牌校验尚未真正生效**——无 `X-Agent-Access-Token` 也被放行;客户端仍应规范地每请求都带,等 AM 开启校验即自动生效。② 子域名目前 `http://` 明文,令牌/`api_key` 会明文传输,等 AM 上 HTTPS。
>
> 🔒 **传输安全门(#55)**:agent 对象回传 `security`(`scheme` http/https、`security_profile` none/tls/mtls、`secure` 布尔)。客户端据此在生产强制 HTTPS(`HEICODE_AGENT_REQUIRE_SECURE=1`):`secure=false`(当前明文)→ 拒绝直连并提示。AM 上线 HTTPS/mTLS listener 后 `security_profile` 自动变 tls,客户端无需改包。HM 保守口径:无法确证 TLS 即标 `none`。
- **同步**:`POST {subdomain}/message/send`
- **流式**:`POST {subdomain}/message/stream`(返回 `text/event-stream`)
- **发现/健康**:`GET {subdomain}/.well-known/agent.json`、`GET {subdomain}/health`
请求头:`X-Agent-Access-Token: <§5 列表里的 access_token>` —— **访问鉴权**(证明你是该 agent 的所有者)。
请求体(JSON-RPC,A2A):
```json
{ "jsonrpc":"2.0", "id":"task-1", "method":"message/send",
"params": {
"api_key": "sk-…", // ★ 模型 key,agent 用它调 HM /v1 计费(客户端自带)
"model": "gpt-5.4",
"message": { "role":"user", "parts":[ {"kind":"text","text":"…"} ] },
"configuration": { "workspace": { "root_dir":"/workspace", "allowed_paths":["a.py"] } }
}
}
```
**两层鉴权,职责分离:**
- **访问鉴权 = `access_token`**(放 `X-Agent-Access-Token` 头,值取 §5 列表里那把 per-agent 令牌):证明「你是这个 agent 的所有者,有权调它」。**每个直连请求都要带**(`message/send`、`message/stream` 都带)。agent 端把它和自己 env 里的 `AGENT_ACCESS_TOKEN` 做常量时间比对,不符返回 401/403。
- **模型鉴权 = `params.api_key`**(模型 sk-,客户端自带的网关 key):agent 凭它调 HM `/v1/*`,计费到你账上。
- agent 用 `.env` 里注入的资源(git/mysql/postgres/azure-blob)自行干活;工具:`read_file/write_file/edit_file/run_command/git_*/run_database_query/list_blob_objects` 等。缺配置的资源工具调用会返回 `resource not configured`,不阻塞。
- 可在 `params.configuration.resources` 里按需覆盖资源(请求级覆盖启动级)。
> ℹ️ 安全:HM 为「客户端↔agent」提供按用户隔离 —— per-agent 访问令牌(`access_token`,部署时现签、注入 agent env、随列表下发),只有部署该 agent 的你拿得到 ⇒ 只有你能调。**接入方式 = agent 本地比对**(`X-Agent-Access-Token` == env `AGENT_ACCESS_TOKEN`),不回 HM、零往返。客户端职责很简单:**直连每个请求都带上 `X-Agent-Access-Token` 头**。通道加密走 agent 子域名的 HTTPS/TLS。详见 AM 契约 §3.1。
---
## 7. 模型调用 🟢
- 客户端自己用模型:`POST /v1/chat/completions` 等,base_url = HM,鉴权同模型调用。
- agent 用模型:agent 内部也打 HM `/v1/*`(用户身份计费)。
- 两边共用同一套模型接口,HM 统一鉴权 + 计费 + 路由。
---
## 7.1 客户端运行时配置 🟢
| 方法 | 路径 | 鉴权 | 说明 |
|---|---|---|---|
| GET | `/api/heicode/config` | 无(公开,非敏感全局配置) | 客户端 runtime config / telemetry 开关;客户端轮询以便会话内即时生效(无需重登) |
```json
{ "success": true, "data": {
"telemetry": {
"enabled": false, // 默认关闭(kill switch)
"endpoint": "/api/heicode/telemetry/events",
"max_batch": 20,
"flush_interval_sec": 30,
"retention_days": 30 // 服务端保留期:超期遥测被清理(#32)
}
}}
```
- 客户端必须以 `telemetry.enabled` 为准:为 `false` 时**停止上送**(摄入端点也会回 410)。
---
## 7.2 客户端错误遥测上送 🟡(默认关闭)
| 方法 | 路径 | 鉴权 | 说明 |
|---|---|---|---|
| POST | `/api/heicode/telemetry/events` | `UserOrV2DeviceAuth` + **V2 设备签名**(需配对设备) | 上送客户端错误遥测;**诊断流量,绝不计费、不进 consume log** |
- **默认关闭**:`HEICODE_TELEMETRY_ENABLED=false` 时返回 **410**(kill switch),客户端应停止上送。
- **鉴权**:需登录用户 + 已配对设备;请求头带 `X-Heicode-Device-Id`。会话-only(无设备)调用被拒(403)。
- **Body = 顶层 JSON 数组**(不是包裹对象),**1–20 条/批**,**≤256KB**。超限 413,非数组 400。
- **每条事件**字段(诊断用,无用户内容):`client_id`(须等于配对设备 id)、`schema_version`、`app_version`、`platform`、`os_version`、`arch`、`locale`、`error_category`、`error_code`、`error_message_hash`、`stack_hash`、`stack_top`(数组)、`context`(对象)、`timestamp`、`session_seq`。
- **服务端脱敏**:`stack_top` / `context` 即使客户端已脱敏,服务端仍二次 redaction(剥离 `sk-`/`Bearer`/URL token/JSON 密钥字段)。
- **context 字段白名单(#32)**:`context` 仅保留 `route` / `retryable` / `phase` / `exit_code` / `duration_ms` / `attempt`;其余键(含 email、完整文件路径、prompt、IP 原文等可识别信息)**一律丢弃**。`stack_top`/`context` 单字段脱敏后截断到 8KiB。
- **重试语义**:4xx(校验失败/超限/kill switch 410)**丢弃不重试**;5xx(持久化失败)可重试。
- **保留期(#32)**:服务端按 `HEICODE_TELEMETRY_RETENTION_DAYS`(默认 30)定期清理超期遥测。
> ⚠️ **上线前置门槛(#32)**:`device_id`/`client_id` 可关联账号,属隐私敏感。生产开启 `HEICODE_TELEMETRY_ENABLED=true` 前必须:隐私文档已如实披露「设备 ID 可关联账号的错误遥测」、产品/法务已确认、kill switch 已验证。隐私文档同步见 heicodeDocs(#34)。
```json
// 请求体(顶层数组,示意一条)
[
{ "client_id":"<paired-device-id>", "app_version":"0.5.0", "platform":"win32",
"error_category":"ui_crash", "error_code":"RENDERER_ERROR",
"error_message_hash":"9f2a7c1b4e8d", "stack_hash":"a1b2c3d4e5f6",
"stack_top":["at MessageList (MessageList.tsx:212:9)"],
"context":{"route":"chat"}, "timestamp":"2026-06-09T07:21:33.123Z", "session_seq":1 }
]
// 成功:{ "success": true, "accepted": 1 }
```
---
## 8. 响应 envelope 与错误码
**成功**:`{ "success": true, "data": {…} }`
**失败**:`{ "success": false, "message":"…", "error": { "code":"…", "message":"…", "retryable": false, "request_id":"…" } }`
> ⚠️ 两个易踩的点:
> - **失败也是 HTTP 200**(`success:false`)——客户端**必须读 `success` 字段判成败**,不能只看 HTTP 状态码(否则失败会被当成功)。
> - `error.retryable` **当前一律为 `false`,不要据它判重试**;按下表的 `code` 判。`request_id` 便于排查。
| code | 场景 | 处理 |
|---|---|---|
| `POLICY_REJECTED` | 参数非法 / 未认证 / 未知 `template_id` | 改参数,不重试 |
| `RESOURCE_BINDING_INVALID` | `binding_ids` 里有不存在/非本人的绑定 | 检查资源 |
| `RUNTIME_UNAVAILABLE` | 调 AM 失败(含 AM 接口未就绪) | AM 侧问题,**可稍后重试** |
| `DEPLOYMENT_CONFLICT` | agent 不存在 | — |
| `DEPLOYMENT_PERSIST_FAILED` | HM 落库失败 | — |
实时刷新:运行中轮询 `GET /agents` 或 `/agents/{id}/status`(3–5s);终态降频。
---
## 9. 客户端完整流程
```text
0. (设备绑定/登录) — 复用模型调用的 V2 设备签名
1. (可选) GET /api/heicode/capabilities 取模型目录
2. (可选) GET /api/user/self 取余额
3. GET /api/heicode/agents 列出我的 agent
- 想看有哪些角色: GET /api/heicode/agent-templates (中文名/简介)
- 部署(也可在网页台做): POST /api/heicode/agents {template_id, binding_ids}
4. 选一个 status=running 的 agent → 取 subdomain + access_token
5. 直连 subdomain (SSE) → 与 agent 对话/干活 ← 不经 HM
- 头带 X-Agent-Access-Token: <access_token> (访问鉴权,证明是该 agent 所有者)
- body params.api_key: <模型 sk> (模型鉴权,计费到你账上)
6. 期间用模型 → HM /v1/* (客户端与 agent 两端都是)
7. 管理: GET /agents/{id}/status | POST /agents/{id}/stop | DELETE /agents/{id}
```
---
## 10. 接口清单(覆盖核查)
| 接口 | 鉴权 | 状态 |
|---|---|---|
| `GET /api/heicode/capabilities` | 无(公开) | 🟢 |
| `GET /api/heicode/available-models` | 会话/设备 | 🟢(登录用户模型列表收口,唯一来源) |
| `GET /api/heicode/config` | 无(公开) | 🟢(runtime config / telemetry 开关) |
| `GET /api/user/self`、`/self/models` | **UserAuth(会话/JWT)** | 🟢 |
| `GET /api/heicode/agent-templates` | 会话/设备 | 🟢(生产已验证 19 中文模板) |
| `GET /api/heicode/agents` `/{id}` `/{id}/status` | 会话/设备 | 🟢 |
| `POST /api/heicode/agents` | 会话/设备 | 🟢(调 AM,AM 接口 🔴) |
| `POST /api/heicode/agents/{id}/stop`、`DELETE /{id}` | 会话/设备 | 🟢(调 AM 🔴) |
| `GET /api/heicode/agents/{id}/usage` | 会话/设备(仅自己的 agent) | 🟢(按 `agent:<id>` token 聚合计费 logs) |
| `POST /api/heicode/telemetry/events` | 会话/设备 + V2 设备签名 | 🟡 默认关闭(410 kill switch);不计费 |
| 模型 `/v1/*` | 同模型调用 | 🟢 |
| `POST /api/heicode/agent-access/verify` | 公开(令牌即凭据) | 🟢(可选兜底;接入用 ① 本地比对,默认不用此端点) |
| 直连 agent SSE(subdomain + X-Agent-Access-Token) | agent 本地比对令牌 | 🟢 直连已通 / ⚠️ 令牌校验待 AM 开启 |
> 端到端(部署→running→直连→stop→delete)已生产验证。AM 侧仅剩两项加固:**令牌校验真正生效** + **子域名 HTTPS**(见 AM 契约 §0.1)。代码隔离层见 `controller/agent_template_runtime.go`。