docs(api): 契约 v0.59 —— 文档进对话 + 上下文预算按窗口推导 + 语音进编排 + thinking 口径
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- api-client 顶部 changelog 新增 v0.59(上一版指向 v0.58),三档标注齐全;新增 §1.2 文档存储与摄取契约、§3.15 语音回合、§2 上下文窗口与预算契约(v0.59);api-reference 端点表/帧表/错误码同步(+235/-33)。

- 登记要点:两条 documents 端点(含重复上传 200 + deduped:true、读侧必须 Bearer)、4 个 REST 错误码、chat.documents(≤5、只收存储名、三入口同形)、DTO document→documents 数组(单数形状从未发布 ⇒ 无兼容期)、chars/truncated 改为「本回合注入侧」、document-read 工具面、contextWindow + 3 个新 env 键 + CONTEXT_MAX_TOKENS 三态(1M ⇒ 699050)+ materialize 语义(欠账)、语音进编排/voice_input.clientMsgId/voice_output 归属键与降级信号、pending_full 两键 + 新增定向 error{code:chat_busy}、thinking 实时/回放剥离与历史可查的口径。

- 子代理如实报出并已就地更正的三处:白名单实为 22 项(design/24 §9.2 写 23,已改设计)、document_unreadable 本期无 REST 落点(只登记为工具错误形态,已改设计 §6.2)、§6.1 单数 DTO 作废。

- 主会话复核:Docs 里 v0.59 出现 22/32 处、四个新小节标题齐、引用的 6 个 commit 全为真实提交;另补一句「预算推导只在启动时算一次 ⇒ 改窗口后需重启重算」与面板文案对齐。
This commit is contained in:
AskaEth committed 2026-10-04 18:11:44 +08:00
1 parent 47e56ca13c
commit b19d798a2d
2 files changed
+235 -33

No files matched your search

+91 -21
View File
@@ -14,7 +14,7 @@
- **响应统一信封**:
- 成功:`{"data": <payload>}`(HTTP 200 / 201 / 202 / 204)。
- 失败:`{"error": {"code": <string>, "message": <string>}}`(HTTP 400/401/403/404/409/500/501 等)。
- **错误码**:业务错误带稳定字符串 `code`(如 `unauthorized` / `not_found` / `bad_request` / `chat_busy` / `not_implemented`,以及 **v0.56** 新增的 `image_too_large`(`chat.image` 超 5 MiB)与 `chat_dispatch_failed`(流式兜底派发失败)),便于客户端分支;`message` 面向人读。
- **错误码**:业务错误带稳定字符串 `code`(如 `unauthorized` / `not_found` / `bad_request` / `chat_busy` / `not_implemented`,**v0.56** 新增的 `image_too_large`(`chat.image` 超 5 MiB)与 `chat_dispatch_failed`(流式兜底派发失败),**v0.59** 新增的文档四码 `unsupported_media_type`(415) / `document_too_large`(413) / `document_not_found`(404) / `too_many_documents`(400)),便于客户端分支;`message` 面向人读。
- **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 字节/帧。
@@ -46,7 +46,8 @@
### 0.3 上传 / 媒体(重要确认)
- **`POST /api/v1/upload`**(B1)上传图片 → 返回**同源 URL**(`/api/v1/files/<name>`),可直接作 `chat.image`;也可继续用 **base64 data: URI** 或 http(s) URL 走 `POST /api/v1/chat` / WS `chat` 的 `image` 字段(入视觉路由,见 §6 流式与直连)。**语义(v0.53 修正,见 api-client §1 行 3)**:**主模型能否收图 = 生效 `models.json` 组的 `multimodal`**(不是「有没有配独立视觉模型」);`LLM_VISION_MODEL` / `LLM_OCR_MODEL` **只配视觉/OCR 工具用的模型**;**env-only 部署(无有效 `models.json`,走 `.env` 合成组)用 `LLM_MULTIMODAL`(默认 `false`,v0.58)声明这个能力**——它才是该组 `multimodal` 的 env 等价物,且**非 bool 值启动报错**;主模型不收图时由 vision/OCR 工具返回**文本描述**(另有 `image-crop` 工具可裁区域后重读,便于大图分区精读)。**内联上限(v0.56,2026-10-04 起可配)**:`chat.image` 解码后默认 **5 MiB**(与 `POST /api/v1/upload` 同口径;换算成字符串上限 `ceil(n/3)*4 + 64` 字符,已含 data: 前缀余量),超限 `400 {"error":{"code":"image_too_large",...}}`;字节上限由 `.env` 的 **`CHAT_IMAGE_MAX_BYTES`** 配置(默认 `5242880`;**≤0 视为「用默认」**,不存在「禁用上限」的取值),换配置时错误文案里的 MiB 数字会自动跟随;WS `chat` 帧受 4096 字节读上限约束(超限断连),故 WS 发大图必须走「上传 → URL」。**视觉链路的 URL 取回(v0.56,实现事实)**:`chat.image` 的 http(s) URL(含 `/api/v1/files/<name>`)由**服务端自己取回并内联为 `data:` URI** 再交 provider,**不再把 URL 原样转给上游**(同源 upload 产物优先走**本地文件读取**,`vision.WithUploadsDir`;取回沿用 http/https 白名单 + 50MiB 上限 + 取回超时)。**类型裁决(同一批实现事实)**:**字节嗅探是唯一裁决权**——按真实字节判白名单光栅图(png/jpeg/gif/webp/bmp),声明的 Content-Type / 扩展名只作参考且可被否决;嗅探为文本(HTML/PROXY 错误页、SVG)或判不定且来源没声明 `image/*` → **明确报错**(`vision.ErrNotImage`),**绝不静默回退成 `image/png`**(旧文若写「非图片响应会被当 PNG」即按此更正)。
- **REST 语音端点已存在**:`POST /api/v1/audio/transcribe`(STT)、`POST /api/v1/audio/speech`(TTS)、`POST /api/v1/audio/voice-clone`(声音复刻),均 owner-only + 审计,绕开 WS 4KB 帧限。语音也可走 WS `voice_input`(上行)/`voice_output`(下行)。
- **文档存储端点已存在(v0.59)**:`POST /api/v1/documents`(owner/admin + 审计 `document.upload`,multipart 字段 `file`,上限 `CHAT_DOCUMENT_MAX_BYTES` 默认 **20 MiB**)与 `GET /api/v1/documents/{name}`(**必须 Bearer owner/admin**,`attachment` + `nosniff`,与图片读侧刻意相反)。**类型裁决 = 字节嗅探(唯一)∧ 扩展名白名单(22 项)**,`mime` 为嗅探基类型(`data.json` → `text/plain`)、`url` 为**相对路径**;同字节同扩展名重复上传回 `200` + 同一 `name` + `deduped:true`。回合引用走 `chat` 的可选 `documents:[{name}]`(**≤5 条、只收服务端存储名**);历史/实时 DTO 是 **`documents` 数组**(元素 `{name,mime,size,chars,truncated,status}`,无附件时省略该键)。契约见 §3.8 与 api-client §1.2。
- **REST 语音端点已存在**:`POST /api/v1/audio/transcribe`(STT)、`POST /api/v1/audio/speech`(TTS)、`POST /api/v1/audio/voice-clone`(声音复刻),均 owner-only + 审计,绕开 WS 4KB 帧限。语音也可走 WS `voice_input`(上行)/`voice_output`(下行);**v0.59 起语音回合进会话编排器**(落库 / 流式 / 跨端 / 忙时排队),`voice_output` 增 `sessionId`/`deviceId?`、`voice_input` 增 `clientMsgId?`,见 §6.3/§6.4 与 api-client §3.15。
- 管理端导入知识文档(RAG)走 JSON body 文本(`POST /api/v1/admin/rag`),非文件上传。
---
@@ -68,7 +69,7 @@
| # | 方法 | 路径 | 鉴权 | 状态 | 说明 | 里程碑 |
|---|------|------|------|:---:|------|:---:|
| 3 | POST | `/api/v1/chat` | owner\|admin | 200/202/400/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}` | 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) | 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 | 会话消息热分页 | M16 |
@@ -84,6 +85,8 @@
| 16 | POST | `/api/v1/audio/transcribe` | **owner** | 200 | 语音转写(STT,绕开 WS 4KB 帧限);静音/空结果时响应带 `provider` | M17 |
| 17 | POST | `/api/v1/audio/speech` | **owner** | 200 | 语音合成(TTS);响应头 `X-Voice-Provider` 报告实际提供方(降级可见) | M17 |
| 18 | POST | `/api/v1/audio/voice-clone` | **owner** | 200 | 声音复刻(百炼 create_voice) | M17 |
| 19 | POST | `/api/v1/documents` | owner\|admin | 201/200/400/413/415 | **文档上传(v0.59)**:multipart 字段 `file`(上限 `CHAT_DOCUMENT_MAX_BYTES`,默认 20 MiB)→ `201 {name,url,mime,size,sha256,deduped:false}`;**同字节同扩展名重复上传 → `200` + 同一 `name` + `deduped:true`**;字节嗅探(唯一裁决)∧ 扩展名白名单(22 项),`mime` 为嗅探基类型、`url` 为**相对路径**;错误码 `400 bad_request`/`413 document_too_large`/`415 unsupported_media_type`(见 §3.8) | M-S(design/24 S1) |
| 20 | GET | `/api/v1/documents/{name}` | **owner\|admin** | 200/404 | **文档读取(v0.59)**:**必须带 Bearer**(与图片读侧免鉴权能力 URL 刻意相反);`name` = 32 位小写 hex + 白名单扩展名,路径穿越全形态拒绝;`Content-Disposition: attachment` + `X-Content-Type-Options: nosniff`;嗅探不出放行类型 → `application/octet-stream`;统一 `404 document_not_found`(见 §3.8) | M-S(design/24 S1) |
### 1.3 管理后台(`/api/v1/admin/*`)
@@ -154,7 +157,7 @@
**另**:外部 webhook 回调(如 `webhook` 渠道的 HMAC 回调)由 composition root 通过 `Deps.Webhooks` 动态挂载,**不走网关鉴权**(handler 自校验签名/授权);路径由调用方决定,不在上表枚举。
**补登记说明(2026-10-04)**:上表中 `#62`–`#68` 是本次按 `routes.go` 逐条核对后新增的行(`plugins/installed` / `observability/logs` / `notifications` / `llm/catalog` / `owner-profile` GET+PUT / `automations/reminders`),插在各自的鉴权分档里、**既有编号一律未动**。§1.1 的 `#3`/`#4`(`auth/refresh`、`auth/logout`)与 §1.2 的 `#14`–`#18`(`upload`、`files/{name}`、三支 `audio/*`)同理。**仍未登记的是 WS 事件帧**(见 §6.4 与本文档开头的历史包袱说明)。
**补登记说明(2026-10-04)**:上表中 `#62`–`#68` 是本次按 `routes.go` 逐条核对后新增的行(`plugins/installed` / `observability/logs` / `notifications` / `llm/catalog` / `owner-profile` GET+PUT / `automations/reminders`),插在各自的鉴权分档里、**既有编号一律未动**。§1.1 的 `#3`/`#4`(`auth/refresh`、`auth/logout`)与 §1.2 的 `#14`–`#18`(`upload`、`files/{name}`、三支 `audio/*`)同理。**v0.59 再补两行**:§1.2 的 `#19`/`#20`(`POST /api/v1/documents`、`GET /api/v1/documents/{name}`,均按 `routes.go` 的 `writeAuth` 注册核对)——同样取本表下一个可用号、不动既有编号。**仍未登记的是 WS 事件帧**(见 §6.4 与本文档开头的历史包袱说明)。
**WS**:`GET /ws/chat`(握手,JWT,见 §6)。
@@ -234,6 +237,7 @@ curl -s -X POST http://<host>:8080/api/v1/auth/login \
| `deviceId` | string | 否 | 客户端生成的端标识(design/16 §4.5,如 `web:chrome:a1b2c3`),落库到消息,可选 |
| `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) |
| `stream` | bool | 否 | `true` = **直达会话编排器**的流式提交(v0.56/G2:接了编排器就直接 `Submit`,**不再经可丢弃的 bus**;未接编排器才回退 `publishChatStream` 发 `ws.chat`),**受理成功后才回响应**;缺省 / `false` = 同步返回完整回复(Go bool 零值即 false,仍**无「必填」约束**) |
```json
@@ -248,6 +252,8 @@ curl -s -X POST http://<host>:8080/api/v1/auth/login \
- **错误**:
- `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` 同口径,文案指路该端点)。
- `400` `{"error":{"code":"too_many_documents",...}}`(**v0.59**:`documents` 超过 5 条,先于任何文件系统访问判定)。
- `404` `{"error":{"code":"document_not_found",...}}`(**v0.59**:`documents[].name` 形态非法 / 路径穿越 / 文件不存在——三种情况同一响应)。
- `409` `{"error":{"code":"chat_busy","message":"会话正忙,待处理消息已满"}}`(pending 队列满,`PendingFull`)。
- `500` `{"error":{"code":"chat_unavailable",...}}`(`runner` 未注入)/ `{"error":{"code":"chat_failed",...}}`(回合运行出错)。
- `503` `{"error":{"code":"chat_dispatch_failed",...}}`(**v0.56/G2**:仅**未接编排器**时的总线兜底派发失败——本轮**未提交**,不再是静默 200)。
@@ -358,7 +364,8 @@ curl -s http://<host>:8080/api/v1/personas -H "Authorization: Bearer $TOKEN" -H
| `mode` | string | 该条的模式(`omitempty`,供渲染气泡不用猜) |
| `mood` | string | 情绪标记(`omitempty`) |
| `image` | string | 图像引用(`omitempty`) |
| `thinking` | string | 思维链(`omitempty`;**本端点 owner/admin-only,故可达;公开回放/持久化广播中已剥离**) |
| `documents` | array | **文档附件与抽取统计**(`omitempty`,**v0.59**):`[{name,mime,size,chars,truncated,status}]`,**≤5 条**、顺序 = 客户端发送顺序;**0 个附件时该键省略**;`status ∈ extracted|listed|unreadable`。⚠️ **`chars`/`truncated` 是「本回合注入侧」事实**——`chars` = 本回合**注入**上下文的字符数、`truncated` = **注入**是否被截断(**不是**「文件是否被截断」);**全文长度**只有 `document-read` 的 `totalChars` 能看到。**不含正文**。同一字段也出现在 `message_persisted` / `channel_persisted` / `message_edited` / 握手回放(同一结构体、四处同时生效)。⚠️ 单数 `document`(对象)形状**从未随任何已发布版本号发布**,本版起一律是 `documents` 数组 |
| `thinking` | string | 思维链(`omitempty`;**本端点 owner/admin-only,故可达**;**公开回放 / 实时持久化帧中已剥离**)。**v0.59 口径(有意默认,非缺陷)**:消息体的 `thinking` 字段在 `message_persisted` / `channel_persisted` / `message_edited` 与握手 `history` 回放中**恒为空**;**历史消息的思维链只有本端点(owner\|admin)返回**。实时思维链另走 `thinking.*` 流帧(§6.4,owner/admin 档) |
| `platform` | string | 来源平台(`web` / `telegram` / `weixin-ilink` / `qq-obv11`...,`omitempty`) |
| `deviceId` | string | 客户端端标识(`omitempty`,如 `web:chrome:a1b2c3`) |
| `senderName` | string | 第三方发送者昵称(`omitempty`;群聊/陌生人消息带,主人私聊为空) |
@@ -482,6 +489,63 @@ curl -s -X POST http://<host>:8080/api/v1/sessions/web/read \
# => {"data":{"platforms":[...],"moods":[...],"modes":[...],"unknown":"display-raw"}}
```
### 3.8 POST /api/v1/documents · GET /api/v1/documents/{name}(v0.59,design/24 §3/§6)
> **一句话**:文档 = 内容寻址存储 + 服务端抽取 + 清单/正文注入。**读侧要 Bearer**(与图片读侧「随机名即能力 URL」刻意相反);客户端只要这两个端点 + `chat` 的 `documents` 字段。权威口径与边界表见 api-client §1.2。
**A) 上传 `POST /api/v1/documents`(owner\|admin + 审计 `document.upload`)**
- **请求**:`multipart/form-data`,字段 `file`(单个)。**单文件上限** = `CHAT_DOCUMENT_MAX_BYTES`(默认 **20971520 = 20 MiB**;`≤0` 视为用默认,**不存在「禁用上限」的取值**)。它与图片的 `CHAT_IMAGE_MAX_BYTES` **刻意解耦**(改一边不影响另一边)。请求体额外留 1 MiB multipart 信封余量,**文件本身按确切上限校验两次**(部件大小 + 流式写入时多读 1 字节)。
- **响应**:
```json
{"data":{"name":"a1b2…(32位hex).md","url":"/api/v1/documents/a1b2….md","mime":"text/plain","size":12345,"sha256":"<64位hex>","deduped":false}}
```
首次 `201`(`deduped:false`);**同字节同扩展名重复上传 → `200` + 同一个 `name` + `deduped:true`**(落盘名 = `sha256(bytes)[:32] + ext`,**内容寻址**,盘上只留一份;元数据行 `ON CONFLICT(name) DO NOTHING` —— 首次的 uploader / 时间戳是权威)。客户端可把 `200` 当成功处理。
- **落盘位置**:`<DataDir>/uploads/documents/`(目录 0700、文件 0600);客户端原始文件名**只用来取扩展名**,永不参与路径。
- **类型裁决(唯一裁决权 = 字节嗅探)**:
- **扩展名白名单(deny-by-default,22 个)**:`.txt .md .markdown .csv .tsv .json .jsonl .yaml .yml .log .ini .cfg .toml .go .ts .tsx .java .c .h .cpp .rs .sql`;
- **字节嗅探**(`http.DetectContentType`)的**基类型必须恰为 `text/plain`**;
- 二者是**合取**:`.js` / `.py` 即便嗅探为文本也拒(不在白名单);改扩展名也换不了类型;
- **明令拒绝**:`text/html`、`image/svg+xml` 与 XML、`application/zip` 及封装(docx/xlsx)、可执行/脚本(exe/dll/bat/cmd/ps1/js/mjs/sh/py)、判不定者。
- 响应里的 **`mime` 是嗅探所得基类型**(去参数),故 `data.json` → `text/plain`(**不是** `application/json`);**`url` 是相对路径**(读侧要 Bearer,绝对化没有意义)。
- **错误**:`400 bad_request`(缺 `file` 部件 / 空文件 / 非 multipart);`413 document_too_large`(文件或信封超限,文案带生效上限字节数);`415 unsupported_media_type`(扩展名或嗅探任一不过,文案同时点明两条口径);`401` / `403`(未认证 / 角色不足)。**审计**:成功 `document.upload`+`ok`、失败 +`error`(成对,不遗漏)。
```bash
curl -s -X POST http://<host>:8080/api/v1/documents \
-H "Authorization: Bearer $TOKEN" -F 'file=@notes.md'
# => 201 {"data":{"name":"…(32hex).md","url":"/api/v1/documents/….md","mime":"text/plain","size":1234,"sha256":"…","deduped":false}}
# 再传同一份:=> 200 {"data":{…同一 name…,"deduped":true}}
```
**B) 读取 `GET /api/v1/documents/{name}`(owner\|admin,不审计)**
- **鉴权**:**必须带 `Authorization: Bearer <JWT>`**(`owner`/`admin`);无 token → `401`,角色不足 → `403`。这是与 `GET /api/v1/files/{name}` 的**关键差异**——文档摄取在服务端完成,不需要被 `<img>` / provider 直接消费,因此不开「名字即凭据」的免鉴权读侧,也不放开内联服务 HTML/SVG 的口子。
- **`name` 形态**:`32 位小写十六进制 + '.' + 白名单扩展名`;**路径穿越全形态拒绝**(`..` / `%2e%2e` / `%2f` 等,含 ServeMux 清理前的守卫)。
- **响应头**:`Content-Type` = **读时重新嗅探**的基类型(嗅不出放行类型 → `application/octet-stream`);`Content-Disposition: attachment; filename="<name>"`;`X-Content-Type-Options: nosniff`;`Cache-Control: private, max-age=86400, immutable`(内容寻址 ⇒ 名字变则内容变);支持 Range。
- **错误**:`404 document_not_found` —— **名字非法 / 路径逃逸 / 文件不存在三种情况同一响应**(不泄露「是否存在」以外的信息)。
```bash
curl -s -D - -o /tmp/doc.md http://<host>:8080/api/v1/documents/<32hex>.md \
-H "Authorization: Bearer $TOKEN"
# => 200,头含 Content-Disposition: attachment、X-Content-Type-Options: nosniff
```
**C) `chat.documents`(REST 同步 / REST `stream:true` / WS `chat` 三处同形)**
```json
{"sessionId":"web","text":"帮我看看这份","documents":[{"name":"a1b2….md"},{"name":"c3d4….csv"}]}
```
- **只给 `name`**(服务端存储名);客户端多带的 `mime` / `url` / `size` **一律被服务端覆盖**(MIME 与大小取自落盘字节),**绝不接受任意 http(s) URL**(不重开 SSRF 面)。
- **≤5 条/回合**:超出 → `400 too_many_documents`,**先于任何文件系统访问**判定;名非法 / 穿越 / 不存在 → `404 document_not_found`;**任一条不合规都让整个请求被拒**(绝不静默丢掉附件)。
- 附件部件挂在**同一条 `user` 消息**上;**纯附件回合**(无文本、无图片)成立。WS 上行帧仍有 **4096 字节**硬上限 ⇒ 现实中文档走 REST;两条入口共用同一份解析与校验,不会分叉。
- **WS 侧失败**:经 `error` 帧下发 `data.code`(`too_many_documents` / `document_not_found` / `chat_documents_unavailable`)并**中止本回合**,绝不静默。
- **历史 / 实时 DTO(四处同源)**:`historyMessage.documents`(见 §3.4 表)——`[{name,mime,size,chars,truncated,status}]`,`status ∈ extracted|listed|unreadable`,**0 个附件时省略该键**;⚠️ `chars` / `truncated` 是**本回合注入侧**事实(全文长度只有 `document-read` 的 `totalChars` 可见)。
**D) 内置工具 `document-read`(模型侧)**:`{name, offset?, limit?}` → `{name,status,offset,limit,totalChars,truncated,nextOffset,text,extractTruncated?}`;`limit` 默认 **8000**、上限 **64K** 字符(`≤0` 用缺省、超上限夹取);`offset` 为 **0 基 rune 偏移**、起点 = sidecar 正文(含 CSV/TSV 摘要行);`offset<0` 或 `>totalChars` → 工具错误(`==totalChars` 合法);`extractTruncated=true` 专指「抽取期撞上 **8 MiB** sidecar 安全上限」。只读 sidecar、**绝不重抽**;策略默认 **owner-only**。**诚实边界**:design/24 §6.2 列的 `document_unreadable`(400) **没有对应的 REST 错误码**,它只以本工具的错误形态存在(客户端在 DTO 里看到的是 `status:"unreadable"`)。
---
## 4. 管理后台 · 读端点(`owner|admin`)
@@ -577,6 +641,7 @@ curl -s http://<host>:8080/api/v1/admin/iot -H "Authorization: Bearer $TOKEN" -H
- `personaModels` / `personaProviders`(v0.53,`omitempty`):**角色级模型 / 协议路由表**(`{personaId: modelId}` / `{personaId: "openai"|"anthropic"}`);未配置 = 该键不出现。
- `multimodal`:**主模型自己能否收图**(= 面板的「多模态」勾选);视觉/OCR **工具**的模型是 `.env` 的 `LLM_VISION_MODEL`/`LLM_OCR_MODEL`,与本字段无关(裁决 14.4.1 修语义);**env-only 部署(无有效 `models.json`,走 `.env` 合成组)由 `LLM_MULTIMODAL`(默认 `false`,v0.58)声明本字段**。
- **API key 永不返回**(wire 无 key 字段)。
- `contextWindow`(v0.59,`omitempty`):该组声明的**模型上下文窗口**(token)。0 / 键缺失 = **未声明** ⇒ 运行时按回落窗口(`DefaultModelContextWindow`,131072)保守推导并记一条 WARN;负数在校验阶段被拒。**它是 `.env` 侧 `LLM_CONTEXT_WINDOW` 的对偶**(per-group 播种)。语义见 api-client §2「上下文窗口与预算契约(v0.59)」。
- 数据源:`models.json` 各组(index 0 = 生效组);无 `models.json` 时按 `.env` 合成单组。curl:
```bash
@@ -812,9 +877,9 @@ curl -s -X DELETE http://<host>:8080/api/v1/admin/automations/<id> -H "Authoriza
```
### 5.13 POST /api/v1/admin/llm
- **Body**(`llmProviderRequest`):`{"id?","name?","provider?","baseUrl","apiKey?","model","multimodal?","fallback?","maxTokens?","reasoning?","personaModels?","personaProviders?"}`。`baseUrl`+`model` 必填;`provider` 空默认 `openai`(仅允许 `openai|anthropic`);`maxTokens` 缺省/`0` = 未设,负数回 `400`。**v0.53 三键**缺省 = 保留已存值(`reasoning` 整块给出即整块替换;两张 persona 表给出即整表替换);非法 `reasoning.kind`、非法协议名、anthropic 组声明 openai 系形状 → `400 bad_request`(`Group.Validate` 报错,不落盘)。契约见 api-client「模型配置契约(v0.53)」。
- **Body**(`llmProviderRequest`):`{"id?","name?","provider?","baseUrl","apiKey?","model","multimodal?","fallback?","maxTokens?","contextWindow?","reasoning?","personaModels?","personaProviders?"}`。`baseUrl`+`model` 必填;`provider` 空默认 `openai`(仅允许 `openai|anthropic`);`maxTokens` 缺省/`0` = 未设,负数回 `400`。**v0.59 `contextWindow?`**:该组声明的**模型上下文窗口**(token)——**缺失 = 保留已存窗口**、**显式 `0` 与缺失同义**(都 = 未声明 ⇒ 按回落窗口 131072 推导)、**负数回 `400` 且不落盘**;保存响应的 `<AdminLlmProvider>` 会把它回显(`omitempty`)。**v0.53 三键**缺省 = 保留已存值(`reasoning` 整块给出即整块替换;两张 persona 表给出即整表替换);非法 `reasoning.kind`、非法协议名、anthropic 组声明 openai 系形状 → `400 bad_request`(`Group.Validate` 报错,不落盘)。契约见 api-client「模型配置契约(v0.53)」与「上下文窗口与预算契约(v0.59)」。
- **响应**:`201 {"data":{"provider":<AdminLlmProvider>,"persisted":true,"path":"<models.json 绝对路径>"}}`。**真实持久化**:读-改-写 `models.json`,保存组**提升为生效组**(index 0),并**热重载**运行中的 LLM stack(无需重启)。
- **错误**:`400`(缺 baseUrl/model / invalid provider);`500`(LLM 服务未注入 / 写盘失败 / 热重载失败)。**API key 写入文件但永不回显、不落日志**。
- **错误**:`400`(缺 baseUrl/model / invalid provider / `contextWindow` 负值);`500`(LLM 服务未注入 / 写盘失败 / 热重载失败)。**API key 写入文件但永不回显、不落日志**。
- **审计**:`llm.addProvider`。curl:
```bash
@@ -1122,17 +1187,17 @@ 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?, clientMsgId?}` | **直达会话编排器的非阻塞 `Submit`(v0.55 起:上行 chat 不再经可丢弃的 bus——会话消息不得被 drop;编排器未注入时回退发布 `ws.chat`)** → `streamChat`(跑 loop 并流式)。`clientMsgId`(**v0.56/G4**)非空且重复到达 → **不落库、不起回合**,只回下行 `chat_duplicate` | 下行 `stream_chunk`/`stream_end` 或 `message.*/thinking.*/tool_call.*`(广播);重复时下行 `chat_duplicate` |
| `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` |
| `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?}` | STT→chat→TTS(缺省 sessionId=`web`) | 下行 `voice_output`(**定向回请求连接**) |
| `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`) |
| `task_output_query` | `{taskId}` | 查询后台任务输出 | 下行 `task_output`(或 `error`) |
**上行各负载字段**
- **`chat`**(`chatStreamRequest`):`{sessionId`(缺省`"web"`), `text`, `image?`(base64 data: URI 或 URL), `deviceId?`, `mode?`(行为模式,缺省回落 `chat`), `clientMsgId?`(客户端幂等键,**v0.56/G4**:同一会话内同一非空值只承认第一条,重复到达回下行 `chat_duplicate` `{sessionId,clientMsgId,messageId}`,且**不落库、不起回合**;省略 = 老行为逐字不变)}。text+image 至少一项;`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`,两处入口共用同一份解析)}。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?`}。无 `voice` 服务 → `{type:"error",data:{message:"voice unavailable"}}`。
- **`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`。
- **`task_output_query`**:`{taskId}`。无 task store → `error`;运行中任务返回截至目前输出;输出拼接 `stdout + "\n" + stderr`。
@@ -1162,12 +1227,12 @@ curl -s -X POST http://<host>:8080/api/v1/admin/system/restore \
| `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` 为当时忙碌通道) |
| `pending_full` | direct(编排) | `{sessionId, mode, max}` | 单会话 pending 达上限(默认 5)拒绝入队 |
| `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 |
| `message_persisted` | direct | `{message:historyMessage}`(sessionId 设) | 每条 user/assistant 消息落库后广播(主会话 `web`/`web:<uuid>`),thinking 剥离 |
| `channel_persisted` | direct | `{message:historyMessage}`(sessionId=`sess-xxx`) | 群聊/陌生人(非主人)消息落库后广播,供 IM 适配器日志页 |
| `voice_output` | direct/target | `{audio, format, text, reply}` | 语音合成回复(定向回请求连接) |
| `voice_output` | direct/target | `{audio, format, text, reply, sessionId, deviceId?, provider?}` | 语音合成回复(**定向回发起 `voice_input` 的那条连接**;**不按 `deviceId` 路由**、断线**不补投**)。**v0.59 纯增量**:`sessionId`(恒有)与 `deviceId`(非空才有)标注归属;`provider` 在**空正文**或 **TTS 失败/降级为空音频**时出现(如 `"silent"`),该帧仍照常发出、不是业务错误。**时点**:该回合**结算时**才到(可能晚于若干状态帧,也可能由续跑或被 steer 注入的那一回合产生) |
| `subagent_event` | direct+relay | 审计结果 或 `{id,name,status,role,mode}` | 子代理上下文查询应答 / 子代理进度 |
| `task_output` | direct+bridge | `{taskId, output, status}` | 后台任务输出查询应答 / task.output 桥接 |
| `capacity_awaken` | direct | `{fromState, toState, tool?}` | 能力状态迁移(design/14 §5,`sessionId` 设) |
@@ -1181,7 +1246,7 @@ curl -s -X POST http://<host>:8080/api/v1/admin/system/restore \
| `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 / 未知类型 / 语音·子代理·任务错误等 |
| `error` | direct | `{message}` / `{code, message, sessionId?, clientMsgId?, messageId?}` | 非法 JSON / 未知类型 / 语音·子代理·任务错误等。**v0.59 新增一种定向用法**:队列满时向**发送者**回 `{code:"chat_busy", …}`(G5,见上表 `pending_full` 行);`chat` 帧的文档引用被拒时回 `{code:"too_many_documents"|"document_not_found"|"chat_documents_unavailable", sessionId}` 并中止本回合 |
> **`auth`**(旧 design/07 列表):当前**不**由网关直接 emit;若未来有 `ws.auth` 生产者主题,则按 `[relay]` 规则广播。本文档以代码为准。
@@ -1196,7 +1261,8 @@ curl -s -X POST http://<host>:8080/api/v1/admin/system/restore \
| `mode` | string | `omitempty` |
| `mood` | string | `omitempty` |
| `image` | string | `omitempty` |
| `thinking` | string | **`omitempty`,但在这些实时帧中恒为空**(`BroadcastPersisted`/`replayHistory` 均置空;`/messages` owner/admin 端点才带) |
| `documents` | array | **`omitempty`(v0.59)**:`[{name,mime,size,chars,truncated,status}]`,**≤5 条**、顺序 = 客户端发送顺序;**0 个附件时该键省略**;`status ∈ extracted|listed|unreadable`;**不含正文**。⚠️ `chars` = 本回合**注入**上下文的字符数、`truncated` = **注入**是否被截断(**不是**文件是否被截断;全文长度只有 `document-read` 的 `totalChars` 可见)。同一字段也出现在 `GET /sessions/{id}/messages`(§3.4)与 `message_edited`;单数 `document` 形状**从未随任何已发布版本发布** |
| `thinking` | string | **`omitempty`,但在这些实时帧中恒为空**(`BroadcastPersisted`/`replayHistory` 均置空;`/messages` owner/admin 端点才带)。**v0.59 口径(有意默认)**:实时帧与握手回放的该字段**一律剥离**;实时思维链另走 `thinking.*` 流帧(owner/admin 档),**历史思维链只有 owner/admin 的历史端点返回** |
| `platform` | string | `omitempty`(`web`/`telegram`/`weixin-ilink`/`qq-obv11`...) |
| `deviceId` | string | `omitempty` |
| `senderName` | string | `omitempty`(第三方昵称,主人私聊为空) |
@@ -1214,6 +1280,7 @@ curl -s -X POST http://<host>:8080/api/v1/admin/system/restore \
- **续接**:忙完重开时新回合**带上上一轮上下文**(`tail` 默认 20 条),非清空重来。
- **mode** 取值:`idle` / `thinking` / `executing` / `subagent`。
- REST 同步路径(§3.1)在忙时返回 `202 queued` / 满 `409 chat_busy`,与这些 WS 状态事件一致。
- **队列满的身份回填与定向回执(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`)
@@ -1283,14 +1350,16 @@ TOKEN=$(curl -s -X POST http://<host>:8080/api/v1/auth/login \
| 状态码 | code(示例) | 说明 |
|:---:|------|------|
| 200 | - | 成功 |
| 201 | - | 创建成功(personas/子会话/apikey/automations/llm/channels config/memories/rag) |
| 201 | - | 创建成功(personas/子会话/apikey/automations/llm/channels config/memories/rag/documents) |
| 202 | - | 已接受(chat 排队 `queued:true,queueDepth`) |
| 204 | - | CORS 预检 OPTIONS 成功无内容 |
| 400 | `bad_request` / `invalid_cron` / `invalid_rule` / `invalid_role` / `unsupported_action` / `invalid_param` / `invalid_provider` / `invalid_scope` | 请求体/参数非法 |
| 400 | `bad_request` / `invalid_cron` / `invalid_rule` / `invalid_role` / `unsupported_action` / `invalid_param` / `invalid_provider` / `invalid_scope` / `too_many_documents`(v0.59) | 请求体/参数非法 |
| 401 | `unauthorized` | 认证缺失/失效/过期(REST Bearer 与 WS) |
| 403 | `forbidden` | 角色不足(viewer 读管理/主会话删除/写操作);主会话不可删 |
| 404 | `not_found` | 资源不存在 |
| 409 | `chat_busy` / `duplicate` / `device_offline` / `needs_login` | 冲突:队列满 / 人格已存在 / 设备离线 / 渠道未登录 |
| 404 | `not_found` / `document_not_found`(v0.59) | 资源不存在(文档:名字非法 / 路径穿越 / 文件不存在**同一响应**) |
| 409 | `chat_busy` / `duplicate` / `device_offline` / `needs_login` | 冲突:队列满(**REST 响应体与 WS 定向 `error` 帧同码**,v0.59/G5)/ 人格已存在 / 设备离线 / 渠道未登录 |
| 413 | `document_too_large`(v0.59) | 文档超 `CHAT_DOCUMENT_MAX_BYTES`(默认 20 MiB),文案带生效上限字节数 |
| 415 | `unsupported_media_type`(v0.59) | 文档类型不在白名单——**扩展名白名单(22 项)与字节嗅探的合取**任一不过 |
| 500 | `internal` / `chat_failed` / `sessions_failed` / … | 服务端错误(含未注入依赖) |
| 501 | `not_implemented` | 后端**确定未实现**(新增用户 / IoT 场景·规则 / 编辑环境变量·重启服务·检查更新 / 完整会话转录导出等)——诚实返回,不伪造成功。**注:插件安装(§5.26)、知识库删除(§1.3 #23)、主人档案保存(`PUT /api/v1/admin/owner-profile`,§1.3 #67)均已实现,不再返回 501** |
@@ -1315,8 +1384,9 @@ TOKEN=$(curl -s -X POST http://<host>:8080/api/v1/auth/login \
### 8.4 里程碑与未实现集合(诚实标注)
- **已真实实现**:登录/健康检查/聊天(同步+流式)/会话(列表/新建/删除/消息分页)/人格/管理读(personas/sessions/memories/plugins/channels/iot/llm/users/automations/backthink/rag/observability×4:metrics/trace/audit/logs)/管理写(人格/API key/设备控制/自动化/LLM 保存·测试/渠道连接·断开·身份测试/渠道配置·删除/KOOK 面板/微信扫码/记忆添加·检索/插件安装·卸载/RAG 导入·删除/主人档案读写(`GET`/`PUT /api/v1/admin/owner-profile`,v0.51)/系统设置 4 项)。
- **仍为后置 M10(返回 `501 not_implemented`,不伪造)**:新增用户(无用户表/登录)、IoT 场景与规则(无模型/存储)、编辑环境变量/重启服务/检查更新、完整会话转录导出(当前导出身份元数据)。
- **已真实实现**:登录/健康检查/聊天(同步+流式)/会话(列表/新建/删除/消息分页)/人格/**文档存储与摄取(v0.59:`POST`/`GET /api/v1/documents` + `chat.documents` + `documents` DTO + `document-read` 工具)**/**语音回合进编排(v0.59:落库 / 流式 / 跨端 / 忙时排队,`voice_output` 归属键)**/管理读(personas/sessions/memories/plugins/channels/iot/llm/users/automations/backthink/rag/observability×4:metrics/trace/audit/logs)/管理写(人格/API key/设备控制/自动化/LLM 保存·测试/渠道连接·断开·身份测试/渠道配置·删除/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)、PDF/Office 解析(S6,待裁决)、IM 侧 `msg.Media` 摄取(`channelbridge` 只取 `msg.Content`)。**已知缺口**:design/24 §6.2 的 `document_unreadable`(400) 无 REST 落点(只有 `document-read` 工具错误形态)。
- **语义/向量增强**:`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);签发/吊销/列表同样真实生效。