docs(api): api-client → v0.55(安全入口 / OneBot 四端 / 可观测 WS 推送 / 数据目录哨兵)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- ADMIN_PATH 安全入口整段口径(路径式前缀、命中即注入入口页、错前缀 404、API 不藏、用户端永不携带该串)
- DATA_DIR 新增 platform 哨兵(默认/显式路径/显式空值行为不变)
- OneBot v11 四端契约:配置面 transport/api_base_url/report_path + env 兜底 + 四端语义 + mode 取舍 +
  端口豁免边界;api-reference §5.24 同步
- 新增 §3.12 可观测实时推送:三个新帧(observability_log/trace/metrics)载荷-上限-触发、
  角色门 fail-closed(仅 owner+admin)、"读即订阅"语义、节拍、前端落点、兼容性
- 补记 13.4.5 面内三条与 MCP 换官方 SDK(REST/WS/env 零变更)
- 按实现纠正 5 处:api-reference 版本指针滞后两版、上行 chat 已直达编排器(不再经 bus)、
  http+server 的鉴权实为「配了 access_token 才校验」(未配则放行,与 ws+server 同口径,尚无 fail-closed 开关)、
  推送帧实际触达所有 owner/admin 连接(订阅只决定循环跑不跑)、remoteURL 回落口径
This commit is contained in:
AskaEth committed 2026-10-04 14:08:10 +08:00
1 parent 852c327360
commit 5fb0cf5e19
2 files changed
+51 -9

No files matched your search

+14 -5
View File
@@ -1,6 +1,6 @@
# Cyrene v2 后端 API 参考(api-reference.md)
> **权威归属(先读这条)**:**契约以 `Docs/api-client.md` 为准**——那是版本化的跨平台客户端契约,每个端点的形状 / 语义 / 版本变更都记在那里(当前 `v0.53`)。**本文件是内部与运维参考**:端点全集索引 + 示例 + 部署细节。**凡是与 api-client 重复的契约性描述,一律以 api-client 为准(本文件不再维护第二份契约);发现两者不一致时,改本文件而不是改 api-client。**
> **权威归属(先读这条)**:**契约以 `Docs/api-client.md` 为准**——那是版本化的跨平台客户端契约,每个端点的形状 / 语义 / 版本变更都记在那里(当前 `v0.55`)。**本文件是内部与运维参考**:端点全集索引 + 示例 + 部署细节。**凡是与 api-client 重复的契约性描述,一律以 api-client 为准(本文件不再维护第二份契约);发现两者不一致时,改本文件而不是改 api-client。**
> **本文件的正确用法**:查「有哪些端点、大致形状、错误码、部署注意事项」可以;**写客户端代码时请以 `Docs/api-client.md` 为准**,本文件的字段表 / 示例可能滞后。
> **历史包袱(如实)**:本文件长期自称「全部后端 API 的唯一详细参考 / 超详细主文档 / 每变更必同步」,但实际**系统性滞后于实现与 api-client**(2026-10-03 的 16 路 design-vs-code 审计结论)。本次已修正与实现**直接矛盾**的陈述、并把若干复述型段落改为引用 api-client;**仍未登记的端点 / WS 事件清单见 `DevDocs/audit/00-总览.md` §4④**,属后续「文档补全」批次,本文件暂不补全。
> **其余仍成立**:所有字段名、取值、鉴权规则、状态码最终**以代码为准**(`internal/services/gateway/*.go`);契约变更版本化(REST 走 `/api/v1`,破坏性变更升 v2),不得破坏既有客户端(版本化承诺本身记在 api-client §5)。
@@ -18,6 +18,9 @@
- **CORS**:`Access-Control-Allow-Origin` 为配置的 `ALLOWED_ORIGINS` 白名单(`*` 时放开);`Allow-Methods: GET, POST, PUT, DELETE, OPTIONS`;`Allow-Headers: Authorization, Content-Type, X-API-Version`;`Access-Control-Expose-Headers: X-Voice-Provider`(跨域浏览器可读语音降级信号头);`Max-Age: 600`。`OPTIONS` 预检直接回 `204`。同源/无 Origin 请求放行。**注意:`X-API-Version` 仅作**advisory**惯例头(见 §0.2)——CORS 已放行但它**不参与服务端版本协商**,服务端当前不校验、不据此改变行为。**
- **请求日志**:每个请求由 `logging` 中间件记录一行(method/path/status/duration_ms),并计入可观测性指标。panic 由 `recover` 中间件兜底为 `500 {error:{code:internal,...}}`,不崩溃进程。
- **超时**:WS 连接 ping 周期 54s、pong 等待 60s、写超时 10s;读上限 4096 字节/帧。
- **数据目录 `DATA_DIR`(v0.55)**:默认 `./data`;**新增保留字哨兵 `platform`(大小写不敏感)** → 平台标准数据目录(Windows `%APPDATA%\Cyrene`、macOS `~/Library/Application Support/Cyrene`、Linux `~/.local/share/cyrene`,`internal/platform.DefaultDataDir()`)。显式路径与**显式空值**行为均不变(空值仍走组合根原兜底)。
- **管理面板安全入口 `ADMIN_PATH`(v0.55,env 默认 `admin`)**:面板**前端**挂在 `/<ADMIN_PATH>`(可配成 `xxx/admin`;**非法值回退 `/admin`**,见 `config.ServerConfig.AdminPathPrefix()`)。命中前缀(含深链)→ 返回**注入了 `<base href="/">` 与 `window.__CYRENE_ADMIN_ENTRY__={"prefix":"..."}` 的 `index.html`**(`Cache-Control: no-store`);**形如面板入口但前缀不符的路径 → `404`**(不回落用户端聊天壳);前缀恰好命中 dist 真实文件则仍走文件服务器。**REST/WS 路径不随它变化**——`/api/v1/*`、`/api/v1/admin/*`、`/ws/chat`、webhook 一律原样(**API 不藏**,安全靠 JWT + 角色门,design/07 §0.1);该前缀串**只注入面板入口页**,用户端 bundle 不含它(design/07 §0.2)。
- **额外监听端口的唯一豁免(v0.55 重述)**:`qq-obv11` 渠道的 **OneBot 反连 / HTTP 上报**模式需要独立 `net.Listen` 端口(`listen_addr`),这是 `AGENTS.md` §9 登记的例外(design/13 §0 原文即写「WS server / HTTP server」);**必须由配置显式给出**,为空则报错、**绝不默认开启**。其余渠道/模式一律不得新开监听端口。
### 0.1 鉴权模型(RBAC)
@@ -570,6 +573,7 @@ curl -s 'http://<host>:8080/api/v1/admin/observability/audit?limit=100' -H "Auth
### 4.16 GET /api/v1/admin/observability/metrics
- **Query**:无(不接收 limit)。
- **副作用(v0.55)**:owner/admin 凭据**读取本端点即建立可观测推送订阅**(`observability_metrics` 帧此后由 WS 推来,面板不再轮询;`observability_push.go`)——该凭据的 WS 连接全断即退订。同理见 §4.17 与 `GET /api/v1/admin/observability/logs`(三者的读取都是订阅握手)。响应形状**一字未变**。
- **响应**:`200 {"data":<AdminMetrics>}`。`AdminMetrics` = `{totalRequests, qps, avgLatencyMs, maxLatencyMs, perMethod{}, perPath{}, statusCounts{}, llmCalls, subagentEvents}`。
- `qps`:近 1 秒窗口计数;`avgLatencyMs/maxLatencyMs`:均值/最大耗时(毫秒)。
- `perMethod/perPath/statusCounts`:分布计数。
@@ -582,6 +586,7 @@ curl -s http://<host>:8080/api/v1/admin/observability/metrics -H "Authorization:
### 4.17 GET /api/v1/admin/observability/trace
- **Query**:`?limit=N`(默认 50,上限 500)。
- **副作用(v0.55)**:读取本端点同样建立推送订阅(`observability_trace` 帧随后推来),详见 §4.16。
- **响应**:`200 {"data":[<AdminTraceSpan>...]}`(newest first)。`AdminTraceSpan` = `{id, parentId?, name, kind, status, startedAt, endedAt?, durationNs, props?, events?}`。`kind` ∈ `llm_call|tool_call|orchestration|state_transition|subagent|memory|guardrail`;`events` 为 `[{time,type,props?}]`;时间 RFC3339。
- **旁路节点的 span 现已真实产生**(裁决 11.4.2 / 11.4.3,2026-10-04):`memory.recall` / `memory.write` / `guardrail.input` / `guardrail.output` / `orchestration.spawn` 均由真实生产者开出,因此 `kind` 取值集里的 `memory|guardrail|orchestration` 不再只是枚举——trace 列表中会真的出现这三类 span(此前三者无生产者,全仓只有 `llm_call`/`tool_call`/`subagent`/`state_transition`)。它们只带元数据(记忆作用域/条数/字节数、护栏裁决与耗时、被派发的子代理 id 与目标角色 id),**不含记忆正文、不含提示词、不含密钥**;见 `design/19 §1`。
- 来源:环形缓冲(默认 512 条),由 `ObserveSpan`/`TraceSink` 注入。curl:
@@ -868,7 +873,7 @@ curl -s -X POST 'http://<host>:8080/api/v1/admin/channels/kook/message/test?acco
| `mode` | string | → `config["mode"]` |
| `accessToken` | string | → `config["access_token"]` |
| `selfID` | string | → `config["self_id"]` |
| `config` | object | 平台专属键值(如 `bot_token`/`base_url`/`remote_url`/`endpointPath`/`receive_mode`/`webhook_verify_token`...) |
| `config` | object | 平台专属键值(如 `bot_token`/`base_url`/`remote_url`/`endpointPath`/`receive_mode`/`webhook_verify_token`/`transport`/`api_base_url`/`report_path`...) |
| `allowGroups`/`denyGroups` | string[] | 黑白名单(群) |
| `allowUsers`/`denyUsers` | string[] | 黑白名单(用户) |
| `groupMsgPerMin` | int | 群消息限流 |
@@ -876,6 +881,7 @@ curl -s -X POST 'http://<host>:8080/api/v1/admin/channels/kook/message/test?acco
| `maxMessages` | int | 单账号最大消息数 |
> 空配置时:`weixin-ilink` 豁免(先建账号再扫码),其它平台 `400`。
> **`config` 是补丁(v0.54,与 `personaId` 同构)**:只含本次请求显式给出的键——键省略(`config` 里没有该键 / `config` 整体缺席或 `null`)= **保留已存值**、给非空值 = 覆盖、显式 `null`/`""` = **清除**(`0`/`false` 不算空);顶层便捷字段(`token`/`baseURL`/…)是普通 string,只在**非空**时并入 ⇒ 清除 `bot_token` 须走 `config` 的显式 `null`/`""`。`allowGroups`/`denyGroups`/`allowUsers`/`denyUsers` 与三个限流键仍是**整值替换**(省略 = 清空)。
> **`qq-obv11` 的 `config` 新增三键(v0.55,OneBot v11 四端,裁决 1.4 / 12.4.1)**:`transport`(`ws`|`http`,默认 `ws`,未知值回落 `ws`)、`api_base_url`(OneBot HTTP API 基址,**必须显式给出、不从 `remote_url` 推导**)、`report_path`(http+server 收上报的路径,默认 `/`)。`mode` 决定哪一半必需:`server` 必需 `listen_addr`(**独立监听端口,`AGENTS.md` §9 登记的唯一豁免,为空则 `Connect` 报错、绝不开端口**)、`client` 必需 `api_base_url`;另一半给齐字段就一并启用(单个适配器即可「收上报 + 回消息」)。env 兜底 `CHANNEL_QQ_OBV11_{TRANSPORT,API_BASE_URL,REPORT_PATH}`。四端完整语义见 `api-client.md` §2 的「OneBot v11 四端契约(v0.55)」。
- **响应**:`201 {"data":{"persisted":true,"path":"<channels.json 路径>","platform":"<platform>","accountId":"<accountId>"}}`。**真实持久化 + 热更新**(读 channels.json→**按 id 打补丁**(`PatchChannel`)→落盘→`Reload(platform)` 热替换 adapter,无需重启;**v0.54 起 `config` 为 patch 三态**(与 `personaId` 同构):键省略=保留该账号已存值 / 给出非空值=覆盖该键 / 显式 `null`或`""`=清除该键,详见 `api-client.md` §2)。token 写入文件但**不回显**(GET 列表掩码)。
- **错误**:`400`(platform 缺失 / 空配置);`500`(渠道配置服务未注入 / 写盘失败 / 热重载失败)。
- **审计**:`channel.save`。curl:
@@ -1026,7 +1032,7 @@ curl -s -X POST http://<host>:8080/api/v1/admin/system/restore \
| `type` | string | 是 | 事件类型(见 §6.3 表) |
| `turnId` | string | 否 | 一轮交换的关联 id(流式;`turn-<hex>`);OnStream 结构化流路径**不带** |
| `seq` | int | 否 | hub 级单调递增序号,供客户端排序关联流式突发 |
| `sessionId` | string | 否 | 能确定会话的事件带上,客户端按它把帧路由到该会话的消息桶(v0.54 起非激活会话的帧不再丢弃;只有单槽忙态仍按激活会话判定,见 `api-client.md` §3.1/§3.2/§3.3) |
| `sessionId` | string | 否 | 能确定会话的事件带上,客户端按它把帧路由到该会话的消息桶(v0.54 起非激活会话的帧不再丢弃;只有单槽忙态仍按激活会话判定,见 `api-client.md` §3.1/§3.2/§3.3);可观测推送三帧(`observability_log`/`observability_trace`/`observability_metrics`,v0.55)**不带**该字段,帧壳只有 `{type,data}` |
| `data` | any | 否 | 类型化负载(每个事件见 §6.3) |
- **上行(客户端→服务端)** `wsInbound`:
@@ -1043,7 +1049,7 @@ curl -s -X POST http://<host>:8080/api/v1/admin/system/restore \
| type | 数据负载 | 服务端行为 | 结果 |
|------|----------|-----------|------|
| `mode_switch` | 任意(如 `{"mode":"chat"}`) | 发布 `ws.mode_switch` → hub 规范化广播 | 下行 `mode_switch`(广播) |
| `chat` | `{sessionId?, text, image?, deviceId?, mode?}` | 发布 `ws.chat` → `streamChat`(跑 loop 并流式) | 下行 `stream_chunk`/`stream_end` 或 `message.*/thinking.*/tool_call.*`(广播) |
| `chat` | `{sessionId?, text, image?, deviceId?, mode?}` | **直达会话编排器的非阻塞 `Submit`(v0.55 起:上行 chat 不再经可丢弃的 bus——会话消息不得被 drop;编排器未注入时回退发布 `ws.chat`)** → `streamChat`(跑 loop 并流式) | 下行 `stream_chunk`/`stream_end` 或 `message.*/thinking.*/tool_call.*`(广播) |
| `interrupt` | `{sessionId}` | 会话编排器中断:取消在跑 turn + 清空 pending | 下行 `session_interrupted` |
| `voice_input` | `{audio?, format?, text?, sessionId?}` | STT→chat→TTS(缺省 sessionId=`web`) | 下行 `voice_output`(**定向回请求连接**) |
| `subagent_ask` | `{id, mode?(""/full/tail/keyword), n, query?}` | 审计子代理上下文(缺省 `mode`=full) | 下行 `subagent_event`(或 `error`) |
@@ -1059,7 +1065,7 @@ curl -s -X POST http://<host>:8080/api/v1/admin/system/restore \
### 6.4 下行事件全表
> **角色门(design/11 §7.1,fail-closed)**:下行帧按类型分档,收口在 `internal/services/gateway/ws_frame_scope.go` 的 `frameReachesAll`(**唯一一处判定**,各调用点不自行决定可见性)。**只有 7 个不含内容的状态机信号**发给**所有已认证连接**(含 `viewer`):`session_busy` / `pending_ack` / `pending_drain` / `session_idle` / `session_interrupted` / `pending_full` / **`session_read`**。**其余全部只发 `owner`+`admin`**:流式内容(`message.*` / `thinking.*` / `tool_call.*` / `tool_execution.update` / `stream_chunk` / `stream_end`)、持久化(`message_persisted` / `message_edited` / `message_deleted` / `channel_persisted`)、系统内部(`graph_update` / `subagent_event` / `task_output` / `task_done` / `tool_used` / `capacity_awaken` / `mode_switch`)、私有状态(`mood` / `device_update` / `reminder` / `proactive_message`);**未登记的新帧类型默认只发 owner/admin**。**本表并非全表**:`session_read` / `message_edited` / `message_deleted` 等已实现事件尚未逐行登记于此;**权威清单与逐帧口径见 `Docs/api-client.md` §3**。
> **角色门(design/11 §7.1,fail-closed)**:下行帧按类型分档,收口在 `internal/services/gateway/ws_frame_scope.go` 的 `frameReachesAll`(**唯一一处判定**,各调用点不自行决定可见性)。**只有 7 个不含内容的状态机信号**发给**所有已认证连接**(含 `viewer`):`session_busy` / `pending_ack` / `pending_drain` / `session_idle` / `session_interrupted` / `pending_full` / **`session_read`**。**其余全部只发 `owner`+`admin`**:流式内容(`message.*` / `thinking.*` / `tool_call.*` / `tool_execution.update` / `stream_chunk` / `stream_end`)、持久化(`message_persisted` / `message_edited` / `message_deleted` / `channel_persisted`)、系统内部(`graph_update` / `subagent_event` / `task_output` / `task_done` / `tool_used` / `capacity_awaken` / `mode_switch`)、私有状态(`mood` / `device_update` / `reminder` / `proactive_message`)、**可观测推送(`observability_log` / `observability_trace` / `observability_metrics`,v0.55)**;**未登记的新帧类型默认只发 owner/admin**。**本表并非全表**:`session_read` / `message_edited` / `message_deleted` 等已实现事件尚未逐行登记于此;**权威清单与逐帧口径见 `Docs/api-client.md` §3**。
>
> **来源标记**:`[direct]` 网关直接 emit;`[relay]` hub 从 bus `ws.>` 规范化(任意 `ws.X` 主题 → 类型 `X`);`[bridge]` 桥接 producer 主题;`[target]` 仅定向特定连接。
@@ -1097,6 +1103,9 @@ curl -s -X POST http://<host>:8080/api/v1/admin/system/restore \
| `reminder` | bridge | producer payload | 提醒触发(`reminder.trigger`) |
| `mode_switch` | direct | 上行原样(任意) | 行为模式切换回显(`ws.mode_switch`) |
| `mood`/`tool_used`/`graph_update`/… | relay | `ws.X` 主题负载(`ws.mood`→`mood`,`ws.tool_used`→`tool_used`,`ws.graph_update`→`graph_update`,…) | 生产者在 bus 上发 `ws.X` 即规范化成类型 `X` 广播 |
| `observability_log` | bridge | `{entries:[{time,level,source,msg}…]}`(最新在前) | **可观测推送(v0.55)**:订阅后新增日志,≤1s 一拍最多一帧、无新条目不发;单帧上限 200 条;**不带 `sessionId`** |
| `observability_trace` | bridge | `{spans:[AdminTraceSpan…]}`(最新在前) | **可观测推送(v0.55)**:订阅后新完成的 span;单帧上限 100 条;**不带 `sessionId`** |
| `observability_metrics` | bridge | `<AdminMetrics>`(扁平,与 REST 同形) | **可观测推送(v0.55)**:与上次发出的不同才发;**不带 `sessionId`** |
| `error` | direct | `{message}` | 非法 JSON / 未知类型 / 语音·子代理·任务错误等 |
> **`auth`**(旧 design/07 列表):当前**不**由网关直接 emit;若未来有 `ws.auth` 生产者主题,则按 `[relay]` 规则广播。本文档以代码为准。