docs(api): 契约 v0.65(忙时并行基础管线 + 六条登记)+ 修三条过期陈述
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

升 v0.65(依据 design/15 §2.5 与 commit 89e13cf),一并登记 679cee0(IM 入站附件引用落库)
/ 59a1ac4(llm_usage.session_id 接线)/ a918819(IM 各渠道语音可被读到),
cb564fe / 15181e7 / 7d2ff74 为纯内部修(无契约变化)。
三档:纯增量 = chat.parallel / activeTurns / cancelledTurns / pending_ack.reason /
两条 REST 应答形状 / VOICE_STT_TIMEOUT_SEC;行为变更 = IM 附件引用落库(无新字段)/
IM 语音进上下文 / session_idle 时点收紧(仅并行时);破坏性 = 无。
修正(客户端抓到的三条,属实):① session_id 「恒为 NULL」→ 已接线(端点仍不按会话过滤/分组)
② 「契约版本」标题并列三处 → v0.65 唯一,其余降为「上一版」
③ 按提交逐个核对补齐(mtime 落后问题)。
未实现项写明:turn_begin / steer_ack 帧壳 turnId / interrupt.turnId / chat_messages.turn_id /
graph_update 按路分桶 / 写类工具文件级互斥(tool/legfile.go 仅定死形状,无实现无接线)/ 第二路 system 摘要。
env CHAT_MAX_PARALLEL_TURNS 如实标注「未接线,生产恒为默认 2」。
This commit is contained in:
AskaEth committed 2026-10-05 09:23:46 +08:00
1 parent 89e13cf53d
commit 879669cc59
2 files changed
+41 -29

No files matched your search

+18 -13
View File
@@ -1,6 +1,6 @@
# Cyrene v2 后端 API 参考(api-reference.md)
> **权威归属(先读这条)**:**契约以 `Docs/api-client.md` 为准**——那是版本化的跨平台客户端契约,每个端点的形状 / 语义 / 版本变更都记在那里(当前 `v0.61`)。**本文件是内部与运维参考**:端点全集索引 + 示例 + 部署细节。**凡是与 api-client 重复的契约性描述,一律以 api-client 为准(本文件不再维护第二份契约);发现两者不一致时,改本文件而不是改 api-client。**
> **权威归属(先读这条)**:**契约以 `Docs/api-client.md` 为准**——那是版本化的跨平台客户端契约,每个端点的形状 / 语义 / 版本变更都记在那里(当前 `v0.65`)。**本文件是内部与运维参考**:端点全集索引 + 示例 + 部署细节。**凡是与 api-client 重复的契约性描述,一律以 api-client 为准(本文件不再维护第二份契约);发现两者不一致时,改本文件而不是改 api-client。**
> **本文件的正确用法**:查「有哪些端点、大致形状、错误码、部署注意事项」可以;**写客户端代码时请以 `Docs/api-client.md` 为准**,本文件的字段表 / 示例可能滞后。
> **历史包袱(如实)**:本文件长期自称「全部后端 API 的唯一详细参考 / 超详细主文档 / 每变更必同步」,但实际**系统性滞后于实现与 api-client**(2026-10-03 的 16 路 design-vs-code 审计结论)。本次已修正与实现**直接矛盾**的陈述、并把若干复述型段落改为引用 api-client;**§1 的 REST 端点表已于 2026-10-04 按 `internal/services/gateway/routes.go` 逐条核对并补登记**(补上此前缺失的 `POST /auth/refresh`、`POST /auth/logout`、`POST /upload`、`GET /files/{name}`、三支 `/audio/*`、`GET /admin/llm/catalog`、`GET /admin/observability/logs`、`GET /admin/notifications`、`GET /admin/plugins/installed`、`GET|PUT /admin/owner-profile`、`POST /admin/automations/reminders`)。**仍未补全的是 WS 事件帧清单**:§6.4 自述「并非全表」,`DevDocs/audit/00-总览.md` §4④ 列出的 `session_read` / `message_edited` / `message_deleted` 等仍未逐行登记。
> **其余仍成立**:所有字段名、取值、鉴权规则、状态码最终**以代码为准**(`internal/services/gateway/*.go`);契约变更版本化(REST 走 `/api/v1`,破坏性变更升 v2),不得破坏既有客户端(版本化承诺本身记在 api-client §5)。
@@ -69,7 +69,7 @@
| # | 方法 | 路径 | 鉴权 | 状态 | 说明 | 里程碑 |
|---|------|------|------|:---:|------|:---:|
| 3 | POST | `/api/v1/chat` | owner\|admin | 200/202/400/404/409/500/503 | 发消息(同步回复或触发 WS 流式);**v0.56**:`stream:true` 受理成功才回(`202` 续接排队 / `409` 队列满 / `500 chat_unavailable` 未接 loop / `503 chat_dispatch_failed` 兜底派发失败)、`image` 超上限 → `400 image_too_large`(上限 = `CHAT_IMAGE_MAX_BYTES`,默认 5 MiB 与 `/upload` 同口径;≤0 视为用默认)、`clientMsgId` 重复 → `200 {duplicate:true}`;**v0.59**:可选 `documents:[{name}]`(≤5 条、只收服务端存储名)→ 超限 `400 too_many_documents`、名非法/不存在 `404 document_not_found`(见 §3.8) | M8/M15 |
| 3 | POST | `/api/v1/chat` | owner\|admin | 200/202/400/404/409/500/503 | 发消息(同步回复或触发 WS 流式);**v0.56**:`stream:true` 受理成功才回(`202` 续接排队 / `409` 队列满 / `500 chat_unavailable` 未接 loop / `503 chat_dispatch_failed` 兜底派发失败)、`image` 超上限 → `400 image_too_large`(上限 = `CHAT_IMAGE_MAX_BYTES`,默认 5 MiB 与 `/upload` 同口径;≤0 视为用默认)、`clientMsgId` 重复 → `200 {duplicate:true}`;**v0.59**:可选 `documents:[{name}]`(≤5 条、只收服务端存储名)→ 超限 `400 too_many_documents`、名非法/不存在 `404 document_not_found`(见 §3.8);**v0.65**:可选 `parallel:true`(忙时显式声明把本条作为**独立一路**;照准则同步入口 `202 {parallel:true,turnId}`、`stream:true` 的 `200 {stream:true,parallel:true,turnId}`;未照准 / 省略时形状不变) | M8/M15 |
| 4 | GET | `/api/v1/sessions` | JWT | 200 | 会话列表(持久化会话 or 渠道会话回退) | M16 |
| 5 | GET | `/api/v1/personas` | JWT | 200 | 人格列表 | M8 |
| 6 | GET | `/api/v1/sessions/{id}/messages` | owner\|admin | 200/**404** | 会话消息热分页;**v0.63**:会话**不存在/已删除** ⇒ `404 session_not_found`,**存在但没有消息仍 `200 {"data":[]}`**(见 §3.4) | M16 |
@@ -241,6 +241,7 @@ curl -s -X POST http://<host>:8080/api/v1/auth/login \
| `mode` | string | 否 | 行为模式(design/01 §3、design/16 §5.1,如 `chat`/`code`/…);随该轮落库供气泡渲染;**IM 恒 `chat`、缺省回落 `chat`**(`chatRequest.Mode`) |
| `clientMsgId` | string | 否 | **客户端幂等键(v0.56,G4)**:同一会话内同一个非空值只承认第一条,重复到达回 `200 {"data":{"duplicate":true,"messageId":...,"sessionId":...}}` 且**不落库、不起回合**;省略 = 与加入该字段前逐字相同(老客户端零影响)。原生端用它把本地乐观气泡对齐到服务端真相 |
| `documents` | array | 否 | **文档附件引用(v0.59)**:`[{"name":"<32位hex>.<ext>"}]`,**≤5 条**、**只收服务端存储名**(`POST /api/v1/documents` 的响应 `name`),**绝不接受任意 URL**;客户端多带的 `mime`/`url`/`size` 一律被服务端覆盖。超出 → `400 too_many_documents`(**先于任何文件系统访问**);名非法/穿越/不存在 → `404 document_not_found`;`text`/`image`/`documents` **至少一项非空**(纯附件回合成立)。WS `chat` 帧同形(见 §6.3) |
| `parallel` | bool | 否 | **独立一路的显式声明(v0.65)**:`true` 且会话正忙、单会话并发额度未满 ⇒ 本条**升为一条独立的并行路立即起**(不排队);空闲时无效果;**省略 = 与加入该字段前逐字不变**(服务端**不做**启发式自动升级)。额度:单会话默认 2、硬上限 3、`<2` = 关闭真并行(此时仍可传,只是落回排队并回 `pending_ack{reason:"parallel_limit"}`)。WS `chat` 帧同形(见 §6.3) |
| `stream` | bool | 否 | `true` = **直达会话编排器**的流式提交(v0.56/G2:接了编排器就直接 `Submit`,**不再经可丢弃的 bus**;未接编排器才回退 `publishChatStream` 发 `ws.chat`),**受理成功后才回响应**;缺省 / `false` = 同步返回完整回复(Go bool 零值即 false,仍**无「必填」约束**) |
```json
@@ -250,8 +251,9 @@ curl -s -X POST http://<host>:8080/api/v1/auth/login \
- **响应**:
- 同步(`stream=false`,会话空闲)`200` `{"data":{"reply":"<助手文本>","steps":<int>,"stopReason":"<string>"}}`(`chatResponse`)。`steps` 为工具循环步数,`stopReason` 为停止原因(如 `stop`/`idle`/`error`)。
- 同步但会话**正忙**(`SubmitSync` 返回 `Pending`)`202` `{"data":{"queued":true,"queueDepth":<int>}}`(非阻塞 ack,稍后按上下文续接)。
- 同步、**已升为独立并行路**(`SubmitSync` 返回 `Parallel`,**v0.65**,仅在客户端发了 `parallel:true` 且额度未满时出现)`202` `{"data":{"parallel":true,"turnId":"turn-…"}}`(**不是 `queued`**:第二条路立即起、不排队,本次请求拿不到同步 `reply`,文本照旧经 WS 流式到达;`turnId` = 那一路的权威回合标识、与该路全部流帧同值)。
- 同步且 **`clientMsgId` 重复到达**(`SubmitSync` 返回 `Duplicate`)`200` `{"data":{"duplicate":true,"messageId":<id>,"sessionId":<id>}}`(**v0.56/G4**:不落库、不起回合,`messageId` 是既有那条的 id)。
- 流式(`stream=true`,**v0.56 行为变更**):**受理成功后才回** —— 空闲已起回合 `200` `{"data":{"stream":true}}`;正忙已续接排队 `202` `{"data":{"stream":true,"queued":true,"queueDepth":<int>}}`;待处理满 `409` `{"error":{"code":"chat_busy",...}}`;幂等键重复 `200` `{"data":{"duplicate":true,"messageId":<id>,"sessionId":<id>}}`(**不落库、不起回合**)。实际文本经 WS 推送(见 §6)。**修复前**该分支在问编排器之前就回 `200 {"data":{"stream":true}}`,故 `202`/`409` 永不出现;**未接 loop 时由 `200 {stream:true}` 改为 `500 chat_unavailable`**(与同步路径同口径)。
- 流式(`stream=true`,**v0.56 行为变更**):**受理成功后才回** —— 空闲已起回合 `200` `{"data":{"stream":true}}`;正忙已续接排队 `202` `{"data":{"stream":true,"queued":true,"queueDepth":<int>}}`;待处理满 `409` `{"error":{"code":"chat_busy",...}}`;幂等键重复 `200` `{"data":{"duplicate":true,"messageId":<id>,"sessionId":<id>}}`(**不落库、不起回合**)。;**v0.65**:已升为独立并行路 ⇒ `200 {"data":{"stream":true,"parallel":true,"turnId":"turn-…"}}`(仅 `parallel:true` 且额度未满时)。实际文本经 WS 推送(见 §6)。**修复前**该分支在问编排器之前就回 `200 {"data":{"stream":true}}`,故 `202`/`409` 永不出现;**未接 loop 时由 `200 {stream:true}` 改为 `500 chat_unavailable`**(与同步路径同口径)。
- **错误**:
- `400` `{"error":{"code":"bad_request","message":"text or image is required"}}`(空消息)。
- `400` `{"error":{"code":"image_too_large",...}}`(**v0.56/G3**:`image` 解码后超上限;上限 = `CHAT_IMAGE_MAX_BYTES`(默认 **5 MiB**,`≤0` 用默认),base64 字符串上限 `ceil(n/3)*4+64`,与 `POST /api/v1/upload` 同口径,文案指路该端点)。
@@ -766,7 +768,7 @@ curl -s http://<host>:8080/api/v1/admin/sessions/web:0123.../export -H "Authoriz
- **字段**(逐字对齐 `internal/services/gateway/admin_llm_usage.go`):`window` 回显补齐后的 UTC RFC3339 时间窗与 `purpose`(`""` = 未过滤);`totals` 恒为六个键(`calls`/`prompt`/`completion`/`cacheHit`/`cacheMiss`/`hitRate`);`byPurpose[].purpose` 与 `byModel[].model` 是分组键,其余同上;**排序 = 调用次数降序、同次数按键升序**;无数据时两个数组是 `[]` **不是 `null`**。
- **`hitRate` 口径**:`cacheHit / (cacheHit + cacheMiss)`;**分母为 0 ⇒ `0`**(口径与 `model.Response.CacheHitRate` 同源)。客户端应把「分母 0」与「真 0% 命中」分开(前者显示 `—`)。
- **错误**:`400 bad_request`(时间形态非法 / `since` 不早于 `until` / 跨度超 90 天);`401`;`403`;`500 {"error":{"code":"usage_unavailable","message":"读取 LLM 用量汇总失败"}}`(汇总出口未接线或查库失败 —— **绝不回全 0 假汇总**)。
- **数据来源**:每次**成功**模型调用把一条非敏感样本异步落进 `llm_usage`(迁移 `0017`;写入口径见 §4.19 的契约与 changelog v0.64)——**有界队列 + 单写者 + 批量事务,队列满丢弃并限流 WARN,绝不阻塞模型调用**;**凭据与消息正文绝不入库**。⚠️ `session_id` 当前恒为 `NULL`(模型层无会话概念),本端点也**不按会话分组**。保留期 `LLM_USAGE_RETENTION`(默认 `720h`;`≤0` = 关闭清理)。
- **数据来源**:每次**成功**模型调用把一条非敏感样本异步落进 `llm_usage`(迁移 `0017`;写入口径见 §4.19 的契约与 changelog v0.64)——**有界队列 + 单写者 + 批量事务,队列满丢弃并限流 WARN,绝不阻塞模型调用**;**凭据与消息正文绝不入库**。✅ **`session_id` 已接线(v0.65 更正)**:loop 在 `Run` 顶部把会话 id 播种进 ctx、适配器落库时带上它(空串映射 `NULL`)——v0.64 写的「恒为 `NULL`」已过期;但本端点**仍不按会话过滤 / 分组**,响应形状不变。保留期 `LLM_USAGE_RETENTION`(默认 `720h`;`≤0` = 关闭清理)。
- **curl**:
```bash
@@ -1281,7 +1283,7 @@ curl -s http://<host>:8080/api/v1/admin/system/storage \
| type | 数据负载 | 服务端行为 | 结果 |
|------|----------|-----------|------|
| `mode_switch` | 任意(如 `{"mode":"chat"}`) | 发布 `ws.mode_switch` → hub 规范化广播 | 下行 `mode_switch`(广播) |
| `chat` | `{sessionId?, text, image?, deviceId?, mode?, clientMsgId?, documents?}` | **直达会话编排器的非阻塞 `Submit`(v0.55 起:上行 chat 不再经可丢弃的 bus——会话消息不得被 drop;编排器未注入时回退发布 `ws.chat`)** → `streamChat`(跑 loop 并流式)。`clientMsgId`(**v0.56/G4**)非空且重复到达 → **不落库、不起回合**,只回下行 `chat_duplicate`。**`documents?`(v0.59)** = 文档附件引用数组(≤5、只收服务端存储名;解析失败回 `error` 帧带 `data.code` 并中止本回合) | 下行 `stream_chunk`/`stream_end` 或 `message.*/thinking.*/tool_call.*`(广播);重复时下行 `chat_duplicate` |
| `chat` | `{sessionId?, text, image?, deviceId?, mode?, clientMsgId?, documents?, parallel?}` | **直达会话编排器的非阻塞 `Submit`(v0.55 起:上行 chat 不再经可丢弃的 bus——会话消息不得被 drop;编排器未注入时回退发布 `ws.chat`)** → `streamChat`(跑 loop 并流式)。`clientMsgId`(**v0.56/G4**)非空且重复到达 → **不落库、不起回合**,只回下行 `chat_duplicate`。**`documents?`(v0.59)** = 文档附件引用数组(≤5、只收服务端存储名;解析失败回 `error` 帧带 `data.code` 并中止本回合)。**`parallel?`(v0.65)** = 显式声明「把本条作为独立一路」:会话正忙且额度未满 ⇒ 立即起第二条路(忙态帧随之带 `activeTurns`);否则落回排队并在 `pending_ack` 带 `reason:"parallel_limit"`;空闲时无效果;**省略 = 逐字不变** | 下行 `stream_chunk`/`stream_end` 或 `message.*/thinking.*/tool_call.*`(广播);重复时下行 `chat_duplicate`;并行路照准时本轮帧的 `turnId` 即该路 |
| `interrupt` | `{sessionId, scope?}` | 按 `scope` 中断(**v0.56/A2**):省略 / `""` / `"all"` = 取消在跑 turn + **清空 pending**(现状语义);`"current"` = **只取消在跑 turn、保留 pending**(队列按编排器续跑规则继续执行);**其它取值 → 定向回 `error{bad_request}`,不打断任何会话** | 下行 `session_interrupted`(`{sessionId,mode,dropped}`;`dropped` = **真的被丢弃**的 pending 条数,`scope:"current"` 时为 `0` 且 `mode` 为当时的忙碌通道) |
| `voice_input` | `{audio?, format?, text?, sessionId?, deviceId?, clientMsgId?}` | STT→**会话编排器**→结算时 TTS(缺省 sessionId=`web`);`deviceId` 落库、`platform` 走存储默认 `web`;**`clientMsgId?`(v0.59)** 非空且重复到达 → 回下行 `chat_duplicate`,**不产生新回合、不发音频** | 下行 `voice_output`(**定向回请求连接**,回合结算时才到);与文本同一条编排路径(忙时排队 / 跨端同步 / 落库) |
| `subagent_ask` | `{id, mode?(""/full/tail/keyword), n, query?}` | 审计子代理上下文(缺省 `mode`=full) | 下行 `subagent_event`(或 `error`) |
@@ -1289,7 +1291,7 @@ curl -s http://<host>:8080/api/v1/admin/system/storage \
**上行各负载字段**
- **`chat`**(`chatStreamRequest`):`{sessionId`(缺省`"web"`), `text`, `image?`(base64 data: URI 或 URL), `deviceId?`, `mode?`(行为模式,缺省回落 `chat`), `clientMsgId?`(客户端幂等键,**v0.56/G4**:同一会话内同一非空值只承认第一条,重复到达回下行 `chat_duplicate` `{sessionId,clientMsgId,messageId}`,且**不落库、不起回合**;省略 = 老行为逐字不变), `documents?`(文档附件引用数组,**v0.59**:`[{name}]`,≤5 条、只收服务端存储名;超出 `too_many_documents`、名非法/不存在 `document_not_found`,两处入口共用同一份解析)}。text / image / documents 至少一项非空;`runner` 未注入或三者皆空 → 直接广播 `stream_end{stopReason:"idle"}`。主会话 `web` 绑定全量 env(`loop.OwnerEnv()`)。`chat` 进入时若该会话正忙 → 由编排器入队(见 §6.4),非阻塞。**仅 owner/admin 连接可发**(见本节角色门)。
- **`chat`**(`chatStreamRequest`):`{sessionId`(缺省`"web"`), `text`, `image?`(base64 data: URI 或 URL), `deviceId?`, `mode?`(行为模式,缺省回落 `chat`), `clientMsgId?`(客户端幂等键,**v0.56/G4**:同一会话内同一非空值只承认第一条,重复到达回下行 `chat_duplicate` `{sessionId,clientMsgId,messageId}`,且**不落库、不起回合**;省略 = 老行为逐字不变), `documents?`(文档附件引用数组,**v0.59**:`[{name}]`,≤5 条、只收服务端存储名;超出 `too_many_documents`、名非法/不存在 `document_not_found`,两处入口共用同一份解析),`parallel?`(独立一路的显式声明,**v0.65**:会话正忙且额度未满 ⇒ 立即起第二条路;省略 = 逐字不变,服务端不做启发式升级)}。text / image / documents 至少一项非空;`runner` 未注入或三者皆空 → 直接广播 `stream_end{stopReason:"idle"}`。主会话 `web` 绑定全量 env(`loop.OwnerEnv()`)。`chat` 进入时若该会话正忙 → 由编排器入队(见 §6.4),非阻塞。**仅 owner/admin 连接可发**(见本节角色门)。
- **`interrupt`**(**v0.56/A2**):`{sessionId, scope?}`。`scope` 省略 / `""` / `"all"` = 取消当前任务 + 清空待处理队列(现状语义);`"current"` = **只取消当前任务、保留待处理队列**(队列按编排器续跑规则继续执行);**其它取值 → 定向回** `{type:"error",data:{code:"bad_request"}}`,**不广播、不打断任何会话**。命中空闲会话为 no-op(两种 scope 都是)。
- **`voice_input`**:`{audio`(base64 或 data: URI), `format`(`wav|mp3|ogg|webm|mpeg`,空由 mime 猜,默认 `wav`), `text`(已转写则跳过 STT), `sessionId?`, `deviceId?`, `clientMsgId?`}。**v0.59**:`deviceId` 随语音轮次落库(`platform` 走存储默认 `web`)、`clientMsgId` 为**可选幂等键**(重复到达回下行 `chat_duplicate`:不产生新回合、不发音频);**语音回合与文本回合走同一条编排路径**(落库 / 流式 / 跨端 / 忙时排队),音频在**回合结算时**定向回发起连接(`{audio,format,text,reply,sessionId,deviceId?,provider?}`)。无 `voice` 服务 → `{type:"error",data:{message:"voice unavailable"}}`。
- **`subagent_ask`**:`{id`(必填), `mode`(空/full/tail/keyword), `n`(tail 查询窗长), `query`(keyword 查询词)}。无 auditor → `error`;id 缺失 → `error`。
@@ -1316,11 +1318,11 @@ curl -s http://<host>:8080/api/v1/admin/system/storage \
| `tool_execution.update` | direct | `{id,index,delta}` | 工具执行进度/局部输出 |
| `stream_chunk` | direct | `{delta}` | **向后兼容整段文本**(仅非流式回退路径;流式路径不 emit) |
| `stream_end` | direct | `{stopReason, steps?}` / `{stopReason:"idle"}` / `{stopReason:"error", error}` | 一轮结束 |
| `session_busy` | direct(编排) | `{sessionId, mode}` | 会话进入忙碌态(mode∈idle/thinking/executing/subagent) |
| `pending_ack` | direct(编排) | `{sessionId, mode, queueDepth}` | 忙时新消息已入队「收到,稍后回」 |
| `pending_drain` | direct(编排) | `{sessionId, mode, queueDepth}` | 忙完开始消费下一待处理消息 |
| `session_idle` | direct(编排) | `{sessionId, mode}` | 会话回到空闲、队列清空 |
| `session_interrupted` | direct(编排) | `{sessionId, mode, dropped}` | 用户 `interrupt`。**v0.56/A2**:帧形状零变化——`scope:"all"`(省略 / 空串同义)= 任务取消 + pending 清空(`mode=idle`、`dropped`=清空条数);`scope:"current"` = **只取消任务、保留 pending**(`dropped` 恒 `0`、`mode` 为当时忙碌通道) |
| `session_busy` | direct(编排) | `{sessionId, mode, activeTurns}` | 会话进入忙碌态(mode∈idle/thinking/executing/subagent)。**v0.65**:`activeTurns` = 该会话当前**在途路数**(含正在收尾的路;单路时非 0 即 1),纯增量 |
| `pending_ack` | direct(编排) | `{sessionId, mode, queueDepth, reason?}` | 忙时新消息已入队「收到,稍后回」。**v0.65**:可选 `reason:"parallel_limit"`,**只在客户端显式声明了并行(`parallel:true`)却落回排队时出现**(额度已满 / 真并行被 `<2` 关掉),其它情况该键缺席 |
| `pending_drain` | direct(编排) | `{sessionId, mode, queueDepth, activeTurns}` | 忙完开始消费下一待处理消息。**v0.65**:增 `activeTurns` |
| `session_idle` | direct(编排) | `{sessionId, mode, activeTurns}` | 会话回到空闲。**v0.65**:增 `activeTurns`;时点收紧为「**所有在途路收尾且 pending 空**」(**仅并行时生效**,单路逐字不变) |
| `session_interrupted` | direct(编排) | `{sessionId, mode, dropped, cancelledTurns, activeTurns}` | 用户 `interrupt`。**v0.56/A2**:`scope:"all"`(省略 / 空串同义)= 任务取消 + pending 清空(`mode=idle`、`dropped`=清空条数);`scope:"current"` = **取消全部在途路、保留 pending**(`dropped` 恒 `0`、`mode` 为当时忙碌通道)。**v0.65 纯增量两键**:`cancelledTurns` = 本次真的取消了几路;`activeTurns` = 清点后仍在途的路(`scope:"all"` 恒 `0`)。`scope:"all"` **不发 `session_idle`**(客户端靠本帧收口) |
| `pending_full` | direct(编排) | `{sessionId, mode, max, clientMsgId?, messageId?}` | 单会话 pending 达上限(默认 5)拒绝入队。**v0.59/G5 纯增量两键**:`clientMsgId` = **被拒那条消息**的幂等键(该消息未携带则整个键缺席);`messageId` = **仅当该消息此前已落库/已认领时**才回填——**绝不在此处新铸 id**(队列满的拒绝发生在幂等键认领之前,代铸会让客户端重试被判成「重复到达」而永远进不来)。**另**:同一时刻向**发送者那条连接**定向回 `{type:"error",data:{code:"chat_busy",message,sessionId,clientMsgId?,messageId?}}`(只发发送者、不广播;`code` 与 REST `409 chat_busy` 同码) |
| `chat_duplicate` | direct(编排) | `{sessionId, clientMsgId, messageId}` | **v0.56/G4(新增帧)**:同一会话内**重复到达**的 `clientMsgId` 的明确回执——**不落库、不起回合、不发任何内容帧**。帧壳带 `sessionId`;**未进 7 个状态机信号白名单 ⇒ fail-closed 只发 `owner`+`admin`**(发过该 `clientMsgId` 的客户端用它把本地乐观气泡对齐到既有 `messageId`,其它客户端按值自行忽略) |
| `steer_ack` | direct(编排) | `{messageId, atStep}`(帧壳带 `sessionId`,`data` 内不重复该键) | **v0.57(新增帧)**:忙时会话里新到的用户消息**已被在飞回合吃进同一回合**(已从 pending 移除、**尚未被正面回答**)——`messageId` = 那条用户消息的**权威 id**(与 `message_persisted` 的 `data.message.id` 同源)、`atStep` = 吃进它的**模型步序号(1 基)**;一批吃进 N 条发 N 帧(不合并)。客户端把对应气泡渲染为「已读、未答」的**灰态**,直到该回合的 `message.*`/`turn.end` 到达(不需要第二个确认帧)。**只在回合内边界**产生(一次模型步与其工具批次都已结束、下一次模型调用尚未开始;无匹配消息则一帧不发);**不带** `turnId`/`seq`,经 hub 直接广播、**不经 bus**;**未进 7 个状态机信号白名单 ⇒ fail-closed 只发 `owner`+`admin`**(`viewer` 收不到)。详见 api-client §3.14 |
@@ -1374,11 +1376,12 @@ curl -s http://<host>:8080/api/v1/admin/system/storage \
- **续接**:忙完重开时新回合**带上上一轮上下文**(`tail` 默认 20 条),非清空重来。
- **mode** 取值:`idle` / `thinking` / `executing` / `subagent`。
- REST 同步路径(§3.1)在忙时返回 `202 queued` / 满 `409 chat_busy`,与这些 WS 状态事件一致。
- **忙时并行(v0.65,基础管线)**:上行 `chat.parallel:true`(WS 帧 / REST 三入口同形)+ 单会话额度未满(默认 2 / 硬上限 3 / `<2` 关闭)⇒ 本条升为**独立一路立即起**(不排队)。忙态帧随之带 `activeTurns`、`session_interrupted` 带 `cancelledTurns`、`pending_ack` 在「声明了并行却落回排队」时带 `reason:"parallel_limit"`;`session_idle` 时点收紧为「所有在途路收尾且 pending 空」(**仅并行时生效**,单路逐字不变)。**未实现**:`turn_begin` / `steer_ack.turnId` / `interrupt.turnId` / `chat_messages.turn_id` / `graph_update` 按路分桶 / 写类工具文件级互斥 / 第二路进行态 system 摘要;env `CHAT_MAX_PARALLEL_TURNS` **未接线**(生产恒为默认 2)。详见 api-client §5 v0.65。
- **队列满的身份回填与定向回执(v0.59/G5)**:`pending_full` 的**广播**载荷增 `clientMsgId?` / `messageId?`(前者 = 被拒那条消息的幂等键,未携带则整个键缺席;后者**仅当该消息此前已落库/已认领**时回填,**绝不新铸 id**);同时向**发送者那条连接**定向回 `{type:"error",data:{code:"chat_busy",…}}`(`code` 与 REST `409 chat_busy` **同码**,客户端一个 code 覆盖两条入口;只发发送者、不广播)。详见 api-client §3.2。
### 6.6 流式关联(`turnId` / `seq`)
- `turnId`:`runTurn` 每次生成 `turn-<hex>`(crypto/rand),用于关联一轮流式突发;`stream_end`/`stream_chunk`(回退路径)带 `turnId`。**结构化流路径(`message.*` / `thinking.*` / `tool_call.*`)自 v0.36 起同样带 `sessionId` + `turnId`**——`broadcastStream` 直接把 `model.StreamEvent` 的 `SessionID`/`TurnID` 填进帧壳,loop 在回合边界把 `turnId` 挂进运行上下文(同轮所有帧一致)。**旧文曾说这条路径只有 `seq`、缺轮次标识,是错的,本版按实现更正**(与 api-client 的 v0.36 条目一致)。v0.50 起这些帧的 `data` 内另带 `streamId`(同一次模型流的多帧共享,多步工具轮的第二次模型调用取新 id);`tool_execution.*` 与 `turn.start/end` 帧**不带** `streamId`。
- `turnId`:**v0.65 起由编排器生成、loop 复用**——编排器在启动某一路前生成 `turn-<hex>`(crypto/rand)并经 `loop.WithTurnID` 挂进该路运行上下文,`runTurn` **ctx 上已有则复用、没有才新铸**(未接编排器的回退路径 / 子代理 / backthink 仍新铸)。单路时帧上仍是每回合全新的随机 id ⇒ **对客户端无可见差异**。用于关联一轮流式突发;`stream_end`/`stream_chunk`(回退路径)带 `turnId`。**结构化流路径(`message.*` / `thinking.*` / `tool_call.*`)自 v0.36 起同样带 `sessionId` + `turnId`**——`broadcastStream` 直接把 `model.StreamEvent` 的 `SessionID`/`TurnID` 填进帧壳,loop 在回合边界把 `turnId` 挂进运行上下文(同轮所有帧一致)。**旧文曾说这条路径只有 `seq`、缺轮次标识,是错的,本版按实现更正**(与 api-client 的 v0.36 条目一致)。v0.50 起这些帧的 `data` 内另带 `streamId`(同一次模型流的多帧共享,多步工具轮的第二次模型调用取新 id);`tool_execution.*` 与 `turn.start/end` 帧**不带** `streamId`。
- `seq`:hub 级单调递增 `atomic.Uint64`,保证一次突发有序,即便 loop 多 goroutine 触发。
### 6.7 WS 示例
@@ -1467,6 +1470,7 @@ TOKEN=$(curl -s -X POST http://<host>:8080/api/v1/auth/login \
- `LLM` 未注入:`/admin/llm` 按 `.env` 合成单组;`POST /admin/llm` 回 `500 llm_unavailable`;`POST /admin/llm/reset-from-env`(v0.62)同样回 `500 llm_unavailable`(不假装重置成功)。
- `Deps.EnvPath`(v0.62):组合根注入的启动用 `.env` 路径,**只**供 `POST /admin/llm/reset-from-env` 按既有解析入口重读 env 默认层(网关不 fopen `.env`);为空 = 只看进程环境变量(生产 systemd/env_file 形态)。`.env` 读不出(例如路径是个目录)→ `500 env_defaults_unreadable`,**不改任何文件**。
- `Voice` 未注入:WS `voice_input` 回 `error {message:"voice unavailable"}`。
- **IM 入站语音(v0.65)**:`channelbridge` 的 `VoiceSTT` 由组合根注入(与 Web 侧 `voice_input` **同一条** `voice.Chain`,不另建一条);超时旋钮 **`VOICE_STT_TIMEOUT_SEC`**(秒,默认 **20**,`≤0` 用默认)**只约束 IM 渠道入站语音的这一次转写**(流水线要为它一直举着一个入站并发槽;Web 客户端的 `voice_input` 不受它约束)。转写不成功(未配置 / 鉴权失败 / 欠费 / 限流 / 超时 / 静默档位)时**消息面落一行中文诚实提示**(按原因分类、含临时熔断退避),**绝不留空消息、绝不假装成功**。
- `Scheduler(auditor)` 未注入:WS `subagent_ask` 回 `error {message:"subagent audit unavailable"}`。
- `Tasks` 未注入:WS `task_output_query` 回 `error {message:"task output unavailable"}`。
- `AuditStore` 未注入:审计退化为进程内存(端点仍返回数据)。
@@ -1481,7 +1485,8 @@ TOKEN=$(curl -s -X POST http://<host>:8080/api/v1/auth/login \
- **已真实实现**:登录/健康检查/聊天(同步+流式)/会话(列表/新建/删除/消息分页)/人格/**文档存储与摄取(v0.59:`POST`/`GET /api/v1/documents` + `chat.documents` + `documents` DTO + `document-read` 工具;**v0.61:PDF 文本层抽取**,白名单 23 项含 `.pdf`、成对裁决、扫描件诚实拒绝)**/**语音回合进编排(v0.59:落库 / 流式 / 跨端 / 忙时排队,`voice_output` 归属键)**/管理读(personas/sessions/memories/plugins/channels/iot/llm/users/automations/backthink/rag/observability×4:metrics/trace/audit/logs)/管理写(人格/API key/设备控制/自动化/LLM 保存·测试·**重置为 env 默认(v0.62,owner-only)**/渠道连接·断开·身份测试/渠道配置·删除/KOOK 面板/微信扫码/记忆添加·检索/插件安装·卸载/RAG 导入·删除/主人档案读写(`GET`/`PUT /api/v1/admin/owner-profile`,v0.51)/系统设置 4 项)。
- **`thinking` 可达性(v0.59 口径,不改行为)**:消息体 `thinking` 字段在**实时帧**(`message_persisted`/`channel_persisted`/`message_edited`)与**握手回放**中**一律剥离**(有意默认);**实时思维链**走 `thinking.*` 流帧(owner/admin);**历史思维链**只有 `GET /sessions/{id}/messages`(owner/admin)返回。
- **仍为后置 M10(返回 `501 not_implemented`,不伪造)**:新增用户(无用户表/登录)、IoT 场景与规则(无模型/存储)、编辑环境变量/重启服务/检查更新、完整会话转录导出(当前导出身份元数据)。**文档侧后置**:配额与引用计数/GC(design/24 S7)、**Office 解析(`.doc`/`.docx`/`.xls`/`.xlsx`/`.ppt`/`.pptx`/`.odt`/`.ods`/`.odp`/`.rtf`,仍诚实拒绝)**、**扫描件/图片型 PDF 的 OCR**(v0.61 已做文本层 PDF,无文本层者如实报 `unreadable`)、IM 侧 `msg.Media` 摄取(`channelbridge` 只取 `msg.Content`)。**已知缺口**:design/24 §6.2 的 `document_unreadable`(400) 无 REST 落点(只有 `document-read` 工具错误形态;客户端在 DTO 里看到的是 `documents[].status = "unreadable"`)。
- **仍为后置 M10(返回 `501 not_implemented`,不伪造)**:新增用户(无用户表/登录)、IoT 场景与规则(无模型/存储)、编辑环境变量/重启服务/检查更新、完整会话转录导出(当前导出身份元数据)。**文档侧后置**:配额与引用计数/GC(design/24 S7)、**Office 解析(`.doc`/`.docx`/`.xls`/`.xlsx`/`.ppt`/`.pptx`/`.odt`/`.ods`/`.odp`/`.rtf`,仍诚实拒绝)**、**扫描件/图片型 PDF 的 OCR**(v0.61 已做文本层 PDF,无文本层者如实报 `unreadable`)、IM 侧**富媒体摄取**(v0.65 起 `channelbridge` 已摄取入站**内联附件字节**——图片/文件引用写进用户消息 `documents` 列——与入站**语音**字节(经 ASR 归一为文本,见 §8.2);其余媒体类型仍未做)。**已知缺口**:design/24 §6.2 的 `document_unreadable`(400) 无 REST 落点(只有 `document-read` 工具错误形态;客户端在 DTO 里看到的是 `documents[].status = "unreadable"`)。
- **忙时并行(v0.65,基础管线)的未实现集合(诚实标注)**:`turn_begin` 帧、`steer_ack` 的帧壳 `turnId`、`interrupt.turnId`、`chat_messages.turn_id` 列与 DTO 键、`graph_update` 按路分桶、**写类工具文件级互斥**(`internal/runtime/tool/legfile.go` 的 `FileClaimer`/`ErrFileBusy`/`WithLegID` 仅**定死形状、无实现无接线**)、第二路进行态 system 摘要(`loop.WithParallelNotice`)**均未实现**;env `CHAT_MAX_PARALLEL_TURNS` **未接线**(`Deps.MaxParallelTurns` 无人从 env 读 ⇒ **生产恒为默认 2**,`.env` 配了不生效)。**已实现的是「基础管线」**:上行 `chat.parallel`(WS/REST 三入口同形)、忙态帧 `activeTurns`、`session_interrupted.cancelledTurns`、`pending_ack.reason:"parallel_limit"`、两条 REST 应答形状(同步 `202 {parallel:true,turnId}` / `stream:true` 的 `200 {stream:true,parallel:true,turnId}`)、`session_idle` 时点收紧(仅并行时生效)。详见 api-client §5 v0.65。
- **语义/向量增强**:`memory.search` 的 `semantic` 当前恒为 `false`——**向量索引已由组合根注入**(`cmd/cyrene/wire.go` 的 `memory.WithVectorIndex`),但**索引内当前无向量**(生产无写入),故语义打分返回空,属诚实反映而非伪造;RAG `embedded` 反映部署是否配置向量增强;**配置了增强必须真实走语义/向量路径(红线,非空壳)**。
- **API key 认证**:`X-API-Key` **已接入鉴权**(v0.51,见 §0.1 注与 api-client.md §认证)——命中签发表即通过、一律 `owner` 级(取消 role 分级),带该头时只走 key 分支(坏 key 回 `401`、不回落 Bearer);签发/吊销/列表同样真实生效。