docs(api-reference): 修正与实现相反/滞后的陈述 + 明确权威归属(契约以 api-client 为准)
审计(DevDocs/audit/00-总览.md §4④ 及 02/05/09/10/11/15/16 报告)多路独立发现:本文件
自称「唯一详细参考 + 每变更必同步」,实际系统性滞后于实现,且有整段与实现逐条相反。
修正 15 类(每处都回代码核对行号):
- §0.1 与 §6(:986/:992/:1030/:1052/:1130/:1246):「WS 上行不鉴权 / history 回放对任何
已认证连接」→ 角色门口径(`route` 首行默认拒;`replayHistory` 首行门;下行仅 7 个状态机
信号全发),指向 design/11 §7.1/§7.2 与 api-client §3
- §5.26/§5.27 plugins install:501 → 真实 200 {installed,name,kind,transport,toolCount}
- §1.3 鉴权列:9 行 owner|admin → owner|admin|viewer(routes.go 实为 viewerAuth)
- §3.5 POST /sessions:「无 body」→ 可选 {personaId?}(未知角色 400;角色创建后不可变)
- §3.3 PersonaSummary:补 mood/thinking、修正 memoryNamespace(读侧恒 "")与示例
- §3.1 chat:补可选 mode;stream 非必填 | §4.4 kind 枚举改为 tool|skill|channel|mcp|iot
- §5.8/§5.9 IoT 501 文案、§5.29 personaId 缺省与 semantic 原因、§5.32 rotate-jwt 响应
- §7:「无 /upload 端点 / 无 REST 语音端点」→ 两者都存在并给形状(原与本文 §0.3 自相矛盾)
- §8.1/§8.4:移除已实现项;observability ×3 → ×4
结构性(根因治理):文件头新增**权威归属声明** —— 契约以 api-client 为准,本文件是内部/
运维参考,凡重复的契约描述改为引用 api-client(复述就是两份真相);并如实降级其自封的
「唯一详细参考」称号,注明"字段示例可能滞后"。
未补(留给后续「文档补全」批次,已登记):session_read、message_edited/deleted、
/admin/plugins/installed、/admin/observability/logs、/admin/notifications、
/sessions/{id}/read、upload/files/audio 概览行,以及若干字段示例滞后。
验证:git diff --stat -- Docs/ 仅本文件(+65/−46);未触碰任何 .go/.ts。
This commit is contained in:
1 parent
4bf1f7cac6
commit
32634feefa
1 file changed
+65
-46
+65
-46
@@ -1,9 +1,9 @@
|
||||
# Cyrene v2 后端 API 完整参考(api-reference.md)
|
||||
# Cyrene v2 后端 API 参考(api-reference.md)
|
||||
|
||||
> **用途**:本文档是 Cyrene v2 **全部后端 API(REST + WebSocket)的唯一详细参考**,供 Web 前端 / 移动端 / 原生客户端 / IM 桥接等**跨平台客户端开发者**使用。所有字段名、取值、鉴权规则、状态码均**以代码为准**(`internal/services/gateway/*.go`)。
|
||||
> **主文档关系**:`Docs/api-client.md` 为面向使用者的**摘要**;`Docs/api-reference.md` 为**超详细主文档**。
|
||||
> **维护铁律**:每实现/变更一个端点或 WS 事件必须同步更新本文档;契约变更版本化(REST 走 `/api/v1`,未来破坏性变更升 v2),不得破坏既有客户端。
|
||||
> **当前版本**:以代码为准(gateway 已在 M0–M16 若干里程碑落地;详见各端点「里程碑」注记)。
|
||||
> **权威归属(先读这条)**:**契约以 `Docs/api-client.md` 为准**——那是版本化的跨平台客户端契约,每个端点的形状 / 语义 / 版本变更都记在那里(当前 `v0.49`)。**本文件是内部与运维参考**:端点全集索引 + 示例 + 部署细节。**凡是与 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)。
|
||||
|
||||
---
|
||||
|
||||
@@ -29,7 +29,11 @@
|
||||
- 有效 token 但角色不足 → `403 {error:{code:"forbidden",...}}`。
|
||||
- `Authorize()`(无参)→ 任意**已认证**角色通过(only read metadata, e.g. GET /sessions、GET /personas)。
|
||||
- `Authorize(owner)` → 仅 owner;`Authorize(owner, admin)` → owner 或 admin。
|
||||
- **WS 鉴权**:`GET /ws/chat` 通过 `?token=<JWT>` 或 `Authorization: Bearer` 取 token(`wsToken`),`Verify` 通过即可升级;**不做角色门槛**(任何有效 token 均可连接),但 `history` 回放与 `proactive` 推送等按角色处理(见 §6)。
|
||||
- **WS 鉴权(连接门与帧门是两回事)**:`GET /ws/chat` 通过 `?token=<JWT>` 或 `Authorization: Bearer` 取 token(`wsToken`),`Verify` 通过即可升级——**连接本身不校验角色**(任意有效 token,含 `viewer`,都可连接)。但**连接之后的帧按角色分档**(design/11 §7.2 上行 + §7.1 下行,2026-10-03 落实;**权威描述见 api-client §3 顶部的「两把角色门」,本节不重复维护**):
|
||||
- **上行帧**:非 `owner`/`admin` 连接发来的**任何**上行 JSON 帧一律 **fail-closed 默认拒**,定向回 `{type:"error",data:{code:"forbidden"}}`——**不广播、不驱动模型、不落库**(`internal/services/gateway/ws_route.go` 的 `route` 首条语句即此门)。协议级帧(WS ping/pong/close)不受影响。
|
||||
- **握手 `history` 回放**:**仅 `owner`/`admin`** 连接收到(`internal/services/gateway/ws_history.go` 的 `replayHistory` 首行门)。
|
||||
- **下行帧**:只有 7 个**不含内容**的状态机信号(`session_busy`/`pending_ack`/`pending_drain`/`session_idle`/`session_interrupted`/`pending_full`/`session_read`)发给所有已认证连接;**其余全部**(正文 / 推理 / 工具 / 持久化与编辑删除的消息 / `graph_update` / `subagent_event` / `task_output` / `mood` / `device_update` / `reminder` …)**仅发 `owner`+`admin`**;未登记的新帧类型默认只发 owner/admin(`internal/services/gateway/ws_frame_scope.go` 的 `frameReachesAll`,fail-closed)。
|
||||
- **口径**:`viewer` = **可连接、可读 12 条只读监控 REST、可收 7 个状态机信号帧**;除此以外 WS 上拿不到任何内容、也发不出任何指令。
|
||||
- **API Key(`X-API-Key`)**:⚠️ **当前实现未接入**。认证/授权中间件 `Authorize` 只校验 `Authorization: Bearer <JWT>`。`POST /api/v1/admin/users/apikey` 会**签发**一个明文 API key(仅返回一次),`GET /api/v1/admin/users` 会列出其掩码,但**网关的鉴权中间件目前不接受 `X-API-Key` 头**(`auth.APIKey.Verify/Lookup` 仅在单元测试中调用)。本文档按代码如实标注:**所有端点实际鉴权 = Bearer JWT**。若未来把 API key 接入中间件,需同步更新本档。
|
||||
|
||||
### 0.2 X-API-Version 头
|
||||
@@ -72,27 +76,27 @@
|
||||
|
||||
### 1.3 管理后台(`/api/v1/admin/*`)
|
||||
|
||||
读(`owner|admin`)+ 写(`owner`,全程审计)。下表按注册顺序,标注鉴权与状态。
|
||||
> **鉴权分档(以 `internal/services/gateway/routes.go` 为准;权威矩阵见 `Docs/api-client.md` §2.0,本表已按其逐条核对)**:管理**读**分两层——**只读监控档 `owner|admin|viewer`**(12 条非敏感 GET:`personas` / `sessions` / `plugins` / `plugins/installed` / `channels` / `iot` / `automations` / `rag` / `observability/{metrics,trace,logs}` / `notifications`,后三条**尚未登记进下表**、见审计 `00-总览.md` §4④)与 **`owner|admin` 档**(含他人数据 / 消息正文 / 凭据面 / 审计流水);**写一律 `owner`** + 全程审计。下表按注册顺序,标注鉴权与状态。
|
||||
|
||||
| # | 方法 | 路径 | 鉴权 | 状态 | 说明 | 里程碑 |
|
||||
|---|------|------|------|:---:|------|:---:|
|
||||
| 9 | GET | `/api/v1/admin/personas` | owner\|admin | 200 | 人格列表 | M8 |
|
||||
| 10 | GET | `/api/v1/admin/sessions` | owner\|admin | 200 | 会话列表 | M16 |
|
||||
| 9 | GET | `/api/v1/admin/personas` | owner\|admin\|viewer | 200 | 人格列表 | M8 |
|
||||
| 10 | GET | `/api/v1/admin/sessions` | owner\|admin\|viewer | 200 | 会话列表 | M16 |
|
||||
| 11 | GET | `/api/v1/admin/memories` | owner\|admin | 200 | 记忆列表 | M8 |
|
||||
| 12 | GET | `/api/v1/admin/plugins` | owner\|admin | 200 | 插件/能力列表 | M8 |
|
||||
| 13 | GET | `/api/v1/admin/channels` | owner\|admin | 200 | 渠道列表 + 已配账号(掩码+bot 身份+capabilities) | M8/M17 |
|
||||
| 12 | GET | `/api/v1/admin/plugins` | owner\|admin\|viewer | 200 | 插件/能力列表 | M8 |
|
||||
| 13 | GET | `/api/v1/admin/channels` | owner\|admin\|viewer | 200 | 渠道列表 + 已配账号(掩码+bot 身份+capabilities) | M8/M17 |
|
||||
| 14 | GET | `/api/v1/admin/channels/messages` | owner\|admin | 200 | IM 适配器日志流(全量/平台/适配器三粒度) | M16 |
|
||||
| 15 | GET | `/api/v1/admin/channels/{platform}/messages` | owner\|admin | 200 | 某平台 IM 适配器日志流 | M16 |
|
||||
| 16 | GET | `/api/v1/admin/iot` | owner\|admin | 200 | IoT 设备列表 | M8 |
|
||||
| 16 | GET | `/api/v1/admin/iot` | owner\|admin\|viewer | 200 | IoT 设备列表 | M8 |
|
||||
| 17 | GET | `/api/v1/admin/llm` | owner\|admin | 200 | LLM 多厂商多模型组(APIKey 不回显) | M8/M15 |
|
||||
| 18 | GET | `/api/v1/admin/automations` | owner\|admin | 200 | 自动化/提醒列表 | M8 |
|
||||
| 18 | GET | `/api/v1/admin/automations` | owner\|admin\|viewer | 200 | 自动化/提醒列表 | M8 |
|
||||
| 19 | GET | `/api/v1/admin/users` | owner\|admin | 200 | 账号/API-key 列表 | M8 |
|
||||
| 20 | GET | `/api/v1/admin/backthink` | owner\|admin | 200 | 后台思考记录 | M9/M10 |
|
||||
| 21 | GET | `/api/v1/admin/rag` | owner\|admin | 200 | 知识库文档列表(可带 `?query=` 并入命中) | M10 |
|
||||
| 21 | GET | `/api/v1/admin/rag` | owner\|admin\|viewer | 200 | 知识库文档列表(可带 `?query=` 并入命中) | M10 |
|
||||
| 22 | POST | `/api/v1/admin/rag/query` | owner\|admin | 200 | 知识库检索 | M10 |
|
||||
| 23 | POST | `/api/v1/admin/rag/delete` | **owner** | 200/404 | 删除知识文档(项目索引条目 + 级联章节,真实删除;不存在 404) | 后 M12 |
|
||||
| 24 | GET | `/api/v1/admin/observability/metrics` | owner\|admin | 200 | 网关请求指标 | M10 |
|
||||
| 25 | GET | `/api/v1/admin/observability/trace` | owner\|admin | 200 | 最近 trace span | M10 |
|
||||
| 24 | GET | `/api/v1/admin/observability/metrics` | owner\|admin\|viewer | 200 | 网关请求指标 | M10 |
|
||||
| 25 | GET | `/api/v1/admin/observability/trace` | owner\|admin\|viewer | 200 | 最近 trace span | M10 |
|
||||
| 26 | GET | `/api/v1/admin/observability/audit` | owner\|admin | 200 | 审计记录 | M10 |
|
||||
| 27 | POST | `/api/v1/admin/plugins/policy` | **owner** | 200 | 写工具策略(审计) | M8 |
|
||||
| 28 | GET | `/api/v1/admin/sessions/{id}/export` | owner\|admin | 200 | 导出会话元数据 | M8 |
|
||||
@@ -206,7 +210,8 @@ curl -s -X POST http://<host>:8080/api/v1/auth/login \
|
||||
| `text` | string | 是* | 用户文本。`text` 与 `image` 至少一项非空 |
|
||||
| `image` | string | 否 | 视觉输入:base64 **data: URI** 或 http(s) URL;入 vision/OCR 或主模型多模态(design/06)。图文并存则为带图 + 文本的 turn |
|
||||
| `deviceId` | string | 否 | 客户端生成的端标识(design/16 §4.5,如 `web:chrome:a1b2c3`),落库到消息,可选 |
|
||||
| `stream` | bool | 是 | `true` = 经 WS 触发流式(`publishChatStream` 发 `ws.chat`),**立即返回**;`false` = 同步返回完整回复 |
|
||||
| `mode` | string | 否 | 行为模式(design/01 §3、design/16 §5.1,如 `chat`/`code`/…);随该轮落库供气泡渲染;**IM 恒 `chat`、缺省回落 `chat`**(`chatRequest.Mode`) |
|
||||
| `stream` | bool | 否 | `true` = 经 WS 触发流式(`publishChatStream` 发 `ws.chat`),**立即返回**;缺省 / `false` = 同步返回完整回复(Go bool 零值即 false,仍**无「必填」约束**) |
|
||||
|
||||
```json
|
||||
{"sessionId":"web","text":"你好","deviceId":"web:chrome:a1b2c3","stream":false}
|
||||
@@ -271,21 +276,24 @@ curl -s http://<host>:8080/api/v1/sessions -H "Authorization: Bearer $TOKEN" -H
|
||||
- **鉴权**:`Authorize()`(任意已认证角色)。
|
||||
- **响应**:`200` `{"data":[<PersonaSummary>...]}`(无 store 时 `[]`)。
|
||||
|
||||
`PersonaSummary` 字段:
|
||||
`PersonaSummary` 字段(**契约**见 api-client §1 行 9;用户侧 `GET /api/v1/personas` 与管理员侧 `GET /api/v1/admin/personas` 同形,**唯一差别是 admin 侧多一个 `thinking?`**):
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `id` | string | 人格 id(`PersonaID`,回退 `ID`) |
|
||||
| `name` | string | 显示名(`c.Name`) |
|
||||
| `character` | string | 角色名(当前恒 = `name`) |
|
||||
| `isDefault` | bool | 是否默认人格 |
|
||||
| `mode` | string | 首个行为模式(`modes[0]`,未设置为 `""`) |
|
||||
| `memoryNamespace` | string | 记忆命名空间(该人格自己的) |
|
||||
| `mode` | string | 首个行为模式(`modes[0]`,未设置为 `""`;内置默认人格未配置 `modes`,故实际为 `""`) |
|
||||
| `memoryNamespace` | string | 记忆命名空间。**读侧当前恒为空串 `""`**(`collectPersonas` 不填此字段——**已知实现事实,客户端不要依赖读侧的该字段**);**人格写响应**(`POST`/`PUT /admin/personas`)才填真实值,未显式配置时为派生默认 `persona.<id>`(`loader.Persona.MemoryNamespace()`) |
|
||||
| `avatar` | string | 视觉头像 URL(`omitempty`;该人格自己的,缺失则为空) |
|
||||
| `chatBackground` | object | `omitempty`;`{landscape, portrait}` 双朝向壁纸,仅该人格自己的 |
|
||||
| `mood` | string | **该角色当前情绪**(design/01 §2;恒返回,无 `omitempty`)。取值见 `GET /meta/enums` 的 `moods`;`""` = 该角色暂无被跟踪的情绪(**不伪造默认值**) |
|
||||
| `thinking` | object | **仅 `GET /admin/personas` 返回**(`omitempty`,未配置则**无该键**):`{budgetTokens,effort,showToUser,mode}` 推理/呈现旋钮;用户侧 `GET /personas` **不返回**(详见 api-client「人格 `thinking` 读侧契约」) |
|
||||
|
||||
```json
|
||||
{"data":[{"id":"cyrene","name":"Cyrene","character":"Cyrene","isDefault":true,"mode":"normal","memoryNamespace":"cyrene","avatar":"/assets/cyrene.png","chatBackground":{"landscape":"/bg/b1.png","portrait":"/bg/b1p.png"}}]}
|
||||
{"data":[{"id":"cyrene","name":"Cyrene","character":"Cyrene","isDefault":true,"mode":"","memoryNamespace":"","mood":"calm","avatar":"/images/Cyrene_Avatar/3rd_Form/Cyrene-3F-Q-Happy-1.png","chatBackground":{"landscape":"/images/Cyrene_ChatBackground/Landscape/3rd_Form/1.png","portrait":"/images/Cyrene_ChatBackground/Vertical/2nd_Form/1.png"}}]}
|
||||
```
|
||||
> 上例中 `memoryNamespace:""` 是**读侧的当前真实值**;`mood:"calm"` 来自默认人格 `configs/personas/cyrene.yaml` 的 `emotion.initialState`(其它角色可能为 `""`)。<!-- mood / memoryNamespace 的读写差异与 api-client §1 行 9 一致 -->
|
||||
|
||||
- **备注**:视觉资产按角色作用域读取,绝不返回别的角色的;缺失时前端回退中性默认。curl:
|
||||
|
||||
@@ -337,8 +345,9 @@ curl -s 'http://<host>:8080/api/v1/sessions/web/messages?limit=50&before=1753000
|
||||
|
||||
### 3.5 POST /api/v1/sessions
|
||||
- **鉴权**:`Authorize(owner, admin)`(写操作,变更持久态)。
|
||||
- **请求**:无 body(空)。可从 JWT claims 取 `ownerID` 作为子会话归属。
|
||||
- **响应**:`201` `{"data":{"sessionId":"web:<uuid>"}}`。
|
||||
- **请求 Body(可选)**:`{"personaId"?}`——**body 可完全省略或为空**。`personaId` 非空时把新会话绑定到该角色(未知角色回 `400 bad_request`);省略 / 空 = 不绑(`persona_id=''`,继承全局默认角色)。**会话角色只在创建时写入、之后不可变**(design/22 §8-1;`PATCH /sessions/{id}` 一旦收到 `personaId` 键即 `400`)。`ownerID` 从 JWT claims 取作子会话归属。契约见 api-client §1 行 5。
|
||||
- **响应**:`201` `{"data":{"sessionId":"web:<uuid>"}}`;`personaId` 非空时**回显**该字段(省略时响应形状与旧版一致)。
|
||||
- **另**:若注入的 `HistoryStore` **不支持角色绑定**且显式传了 `personaId`,回 `501 not_implemented`(不假装写入)。
|
||||
- **错误**:`501` `{"error":{"code":"not_implemented","message":"会话历史服务未配置(未注入 HistoryStore)"}}`;`500` `{"error":{"code":"session_failed",...}}`。
|
||||
- **备注**:新建次级会话(`NewSubSession`),独立上下文回溯。curl:
|
||||
|
||||
@@ -427,7 +436,7 @@ curl -s http://<host>:8080/api/v1/admin/memories -H "Authorization: Bearer $TOKE
|
||||
```
|
||||
|
||||
### 4.4 GET /api/v1/admin/plugins
|
||||
- **响应**:`200 {"data":[<PluginSummary>...]}`(`PluginSummary` = `{name, kind, description, allowInGroup, allowInStranger}`)。`kind` 为能力类型(如 `plugin|channel|tool|mcp`);`allowInGroup/allowInStranger` 为可达性策略。curl:
|
||||
- **响应**:`200 {"data":[<PluginSummary>...]}`(`PluginSummary` = `{name, kind, description, allowInGroup, allowInStranger}`)。`kind` 为能力类型,实际取值 = `tool|skill|channel|mcp|iot`(`internal/capability/registry` 的 `KindTool`/`KindSkill`/`KindChannel`/`KindMCP`/`KindIoT`);`allowInGroup/allowInStranger` 为可达性策略(**仅内存标记,重启复位**)。curl:
|
||||
|
||||
```bash
|
||||
curl -s http://<host>:8080/api/v1/admin/plugins -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'
|
||||
@@ -672,14 +681,14 @@ curl -s -X POST http://<host>:8080/api/v1/admin/iot/d1/control \
|
||||
```
|
||||
|
||||
### 5.8 POST /api/v1/admin/iot/scenarios
|
||||
- **响应**:`501 {"error":{"code":"not_implemented","message":"IoT 场景存储为 M10 提供(当前 IoT 仅有设备列表/控制/状态)"}}`。**审计**:`iot.scenario`(error)。curl:
|
||||
- **响应**:`501 {"error":{"code":"not_implemented","message":"IoT 场景未实现:IoT 子系统仅有设备列表/控制/状态(Provider 接口无场景模型与存储);场景 CRUD 需先新增模型 + 执行器"}}`。**审计**:`iot.scenario`(error)。curl:
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://<host>:8080/api/v1/admin/iot/scenarios -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{}'
|
||||
```
|
||||
|
||||
### 5.9 POST /api/v1/admin/iot/rules
|
||||
- **响应**:`501`(同 5.8,`iot.rule`)。curl:
|
||||
- **响应**:`501 {"error":{"code":"not_implemented","message":"IoT 自动化规则未实现:IoT 子系统无规则模型与存储(通用 automations 服务只跑 cron/事件规则,不含设备状态条件 + 设备动作执行器)"}}`(同 5.8,审计 `iot.rule`)。curl:
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://<host>:8080/api/v1/admin/iot/rules -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{}'
|
||||
@@ -879,16 +888,20 @@ curl -s -X DELETE 'http://<host>:8080/api/v1/admin/channels/kook/config?id=conf0
|
||||
```
|
||||
|
||||
### 5.26 POST /api/v1/admin/plugins/install
|
||||
- **响应**:`501 {"error":{"code":"not_implemented","message":"插件安装需插件源码清单(编译二进制/YAML)为 M10 提供;卸载可直接调用 registry"}}`。**审计**:`plugin.install`(error)。curl:
|
||||
- **Body**(`pluginInstallRequest`):`{"kind":"mcp","name":"<名>","config":{"transport":"stdio|http|sse","endpoint"?|"command"?, "args"?, "apiKey"?, "env"?}}`。`name` 必填;`kind` 缺省 `"mcp"`,**仅 `mcp` 可装**(其它回 `400`)。`endpoint` 的别名是 `url`。
|
||||
- **响应**:`200 {"data":{"installed":true,"name":"<名>","kind":"mcp","transport":"<t>","toolCount":<n>}}`——**真实安装**:connect + 发现工具 + 热注册进 capability registry(下一轮模型请求即注入,无需重启)→ 落盘 `plugins.json`(gitignore、0600)→ 持有 live server 供后续刷新/卸载。**同名重复安装 = 刷新**(先卸旧再装新,失败回滚旧)。
|
||||
- **错误**:`400`(缺 `name` / `kind` 非 `mcp` / transport 非法 / `stdio` 缺 `command` / `http|sse` 缺 `endpoint`);**`502 plugin_install_failed`**(连接/发现/注册失败,且**不落盘**,绝不装一半);`501 not_implemented`(未注入 plugin manager)。
|
||||
- **审计**:`plugin.install`。**契约见 api-client §2 的 `POST /admin/plugins/install` 条(本节为索引,不重复维护契约)**。curl:
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://<host>:8080/api/v1/admin/plugins/install \
|
||||
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"name":"x"}'
|
||||
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
||||
-d '{"kind":"mcp","name":"fs","config":{"transport":"stdio","command":"npx","args":["-y","@modelcontextprotocol/server-filesystem","/tmp"]}}'
|
||||
```
|
||||
|
||||
### 5.27 POST /api/v1/admin/plugins/uninstall
|
||||
- **Body**(`pluginActionRequest`):`{"name":"<能力名>"}`(必填)。
|
||||
- **响应**:`200 {"data":{"name":"<name>","uninstalled":true}}`——真实从 registry 移除。
|
||||
- **响应**:已装 MCP → `200 {"data":{"name":"<name>","uninstalled":true,"kind":"mcp"}}`(`Unregister` 移除其工具 + close + 删 `plugins.json` 条目);**非 MCP / 普通能力** → `200 {"data":{"name":"<name>","uninstalled":true}}`(走 `registry.Remove`)。
|
||||
- **错误**:`400`(name 缺失);`404`(不存在);`500`。
|
||||
- **审计**:`plugin.uninstall`。curl:
|
||||
|
||||
@@ -910,8 +923,8 @@ curl -s -X POST http://<host>:8080/api/v1/admin/memories \
|
||||
```
|
||||
|
||||
### 5.29 POST /api/v1/admin/memories/search
|
||||
- **Body**(`memorySearchRequest`):`{"query","personaId?","topK?"}`。`query` 必填;`personaId` 缺省 `"cyrene"`;`topK` ≤0 取 10。
|
||||
- **响应**:`200 {"data":{"query":"<query>","personaId":"<personaId>","semantic":false,"hits":[{"id","scope","text","score"}]}}`。真实检索(keyword×衰减);语义/向量路径需注入 vector index(`semantic` 当前恒 `false`,诚实反映未接向量器)。
|
||||
- **Body**(`memorySearchRequest`):`{"query","personaId?","topK?"}`。`query` 必填;`personaId` **省略时回落到库里注册的默认角色**(`defaultPersonaID()`,不再写死 `"cyrene"`),**既未传又无默认角色 → `400 bad_request`**(诚实报错,不拿不可能命中的 id 去查);`topK` ≤0 取 10。
|
||||
- **响应**:`200 {"data":{"query":"<query>","personaId":"<personaId>","semantic":false,"hits":[{"id","scope","text","score"}]}}`。真实检索(keyword × 衰减)。**`semantic` 当前仍恒为 `false`,但原因已变**:向量索引**已由组合根注入**(`cmd/cyrene/wire.go` 的 `memory.WithVectorIndex`),真实原因是**索引内当前无向量**(生产路径无向索引写入向量,仅测试有 `Index.Add`),故语义打分返回空;这是诚实反映,不是伪造。
|
||||
- **错误**:`400`(query 缺失);`500`(memory store 未配置 / 检索失败)。
|
||||
- **审计**:`memory.search`。curl:
|
||||
|
||||
@@ -946,7 +959,7 @@ curl -s -X POST http://<host>:8080/api/v1/admin/system/revoke-keys \
|
||||
|
||||
### 5.32 POST /api/v1/admin/system/rotate-jwt
|
||||
- **Body**(`rotateJWTRequest`):`{"newSecret":"<≥32 位>"}`(必填)。
|
||||
- **响应**:`200 {"data":{"rotated":true,"note":"现有 token 已全部失效,需重新登录"}}`。
|
||||
- **响应**:`200 {"data":{"rotated":true,"refreshTokensRevoked":<int>,"note":"现有 token 与 refresh token 已全部失效,需重新登录"}}`(`refreshTokensRevoked` = 一并删除的 refresh token 行数)。
|
||||
- **错误**:`400`(newSecret 缺失 / 长度 <32,`"新密钥长度不足 32 位"`);`500`(认证服务未配置 / 轮换失败)。
|
||||
- **审计**:`system.rotate_jwt`。**新密钥永不回显、不落日志**。curl:
|
||||
|
||||
@@ -983,17 +996,17 @@ curl -s -X POST http://<host>:8080/api/v1/admin/system/restore \
|
||||
|
||||
## 6. WebSocket 实时协议(`GET /ws/chat`)
|
||||
|
||||
> 唯一实时通道。**单二进制、单端口**:同一 `:8080` 上,REST/WS/admin 共用。生产者(loop/IoT/task/automation/reminder/subagent)经进程内 `bus` 发布,hub 订阅 `ws.>` 及桥接模式并规范化成 WS 帧广播给客户端。**WS 上行不鉴权角色门槛**,但特定下行按角色(`history` 回放对任何已认证连接,`proactive` 仅 owner/admin)。
|
||||
> 唯一实时通道。**单二进制、单端口**:同一 `:8080` 上,REST/WS/admin 共用。生产者(loop/IoT/task/automation/reminder/subagent)经进程内 `bus` 发布,hub 订阅 `ws.>` 及桥接模式并规范化成 WS 帧广播给客户端。**上行与下行都有角色门(fail-closed)**:① **上行帧**——只有 `owner`/`admin` 能发,非 owner/admin 连接发来的**任何**上行 JSON 帧被定向拒为 `{type:"error",data:{code:"forbidden"}}`(`ws_route.go` 的 `route` 首条语句,默认拒,不广播/不驱动模型/不落库);② **握手 `history` 回放**——**仅 `owner`/`admin`**(`ws_history.go` 的 `replayHistory` 首行);③ **下行帧**——只有 7 个不含内容的状态机信号(含 `session_read`)发给所有已认证连接,其余全部(含 `proactive_message`)**仅 `owner`/`admin`**(`ws_frame_scope.go`)。**权威口径与帧清单见 `Docs/api-client.md` §3 顶部的两把角色门**;本节其余部分只描述帧形状,鉴权以 api-client 为准。
|
||||
|
||||
### 6.1 握手与连接
|
||||
|
||||
- **URL**:`ws://<host>:8080/ws/chat?token=<JWT>`(或 `Authorization: Bearer <JWT>` 头)。
|
||||
- **方法**:必须 `GET`(否则握手前回 `400 {"error":{"code":"bad_request","message":"websocket requires GET"}}`)。
|
||||
- **鉴权**:`s.auth.Verify(wsToken(r))`。`wsToken` 先取 `?token=`,再取 `Authorization: Bearer`。失败 → **握手前**回 `401 {"error":{"code":"unauthorized",...}}`(浏览器/IM 客户端收到干净拒绝)。**不校验角色**(任意有效 JWT 可连)。
|
||||
- **鉴权**:`s.auth.Verify(wsToken(r))`。`wsToken` 先取 `?token=`,再取 `Authorization: Bearer`。失败 → **握手前**回 `401 {"error":{"code":"unauthorized",...}}`(浏览器/IM 客户端收到干净拒绝)。**连接层不校验角色**(任意有效 JWT——含 `viewer`——都可连)。**但连接之后的帧按角色分档**:上行仅 `owner`/`admin` 可发、`history` 回放仅 `owner`/`admin` 可收、下行除 7 个状态机信号外仅 `owner`/`admin`(见本节开头与 api-client §3 顶部两把角色门)。
|
||||
- **Origin 校验**:`CheckOrigin` 允许「同一 origin」+「同一 host」+ 白名单;`*` 全放行。同 host 放宽:production 内嵌前端页面 origin 即网关 Host,故 `PORT` 改变无需改 `.env`。**跨源客户端**(外部 IM / 另一 origin)需在 `ALLOWED_ORIGINS` 白名单。
|
||||
- **升级后流程**:注册连接 → `replayHistory(c, "web")`(首帧推 `history` 回放)→ 启动 `writePump`(唯一写者,含 ping/pong)与 `readPump`(读客户端帧)。
|
||||
- **心跳/限帧**:读上限 4096 字节/帧;写超时 10s;pong 等待 60s;ping 周期 54s。发送缓冲 32(慢客户端满则丢帧,生产者不阻塞)。
|
||||
- **`history` 回放(首帧)**:`{type:"history", sessionId:"web", data:{messages:[historyMessage...]}}`——主会话最近 50 条(`wsReplayLimit=50`),**`thinking` 已剥离**;回放仅在 `history != nil` 时发生,失败静默跳过。
|
||||
- **`history` 回放(首帧)**:`{type:"history", sessionId:"web", data:{messages:[historyMessage...]}}`——主会话最近 50 条(`wsReplayLimit=50`),**`thinking` 已剥离**;**仅 `owner`/`admin` 连接收到**(`replayHistory` 首行角色门,design/11 §7.2 A;`viewer` 连接建立后不收回放)。回放仅在 `history != nil` 时发生,失败静默跳过。
|
||||
|
||||
### 6.2 帧壳(Frame Shell)
|
||||
|
||||
@@ -1015,12 +1028,12 @@ curl -s -X POST http://<host>:8080/api/v1/admin/system/restore \
|
||||
|
||||
### 6.3 上行事件(客户端 → 服务端)
|
||||
|
||||
> 由 `wsConn.route(in)` 按 `in.Type` 分派。未知类型回 `{type:"error", data:{message:"unknown event type"}}`。
|
||||
> 由 `wsConn.route(in)` 按 `in.Type` 分派。未知类型回 `{type:"error", data:{message:"unknown event type"}}`。**角色门(design/11 §7.2,fail-closed)**:`route` 的**第一条语句**就是角色检查——非 `owner`/`admin` 连接发来的**任何**上行帧一律**定向**回 `{type:"error",data:{code:"forbidden"}}`,不广播、不驱动模型、不落库;因此下表**只对 owner/admin 连接生效**(`viewer` 连上也发不出任何一条)。权威帧清单与版本记录见 api-client §3。
|
||||
|
||||
| type | 数据负载 | 服务端行为 | 结果 |
|
||||
|------|----------|-----------|------|
|
||||
| `mode_switch` | 任意(如 `{"mode":"chat"}`) | 发布 `ws.mode_switch` → hub 规范化广播 | 下行 `mode_switch`(广播) |
|
||||
| `chat` | `{sessionId?, text, image?, deviceId?}` | 发布 `ws.chat` → `streamChat`(跑 loop 并流式) | 下行 `stream_chunk`/`stream_end` 或 `message.*/thinking.*/tool_call.*`(广播) |
|
||||
| `chat` | `{sessionId?, text, image?, deviceId?, mode?}` | 发布 `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`) |
|
||||
@@ -1028,7 +1041,7 @@ curl -s -X POST http://<host>:8080/api/v1/admin/system/restore \
|
||||
|
||||
**上行各负载字段**
|
||||
|
||||
- **`chat`**(`chatStreamRequest`):`{sessionId`(缺省`"web"`), `text`, `image?`(base64 data: URI 或 URL), `deviceId?`}。text+image 至少一项;`runner` 未注入或两者皆空 → 直接广播 `stream_end{stopReason:"idle"}`。主会话 `web` 绑定全量 env(`loop.OwnerEnv()`)。`chat` 进入时若该会话正忙 → 由编排器入队(见 §6.4),非阻塞。
|
||||
- **`chat`**(`chatStreamRequest`):`{sessionId`(缺省`"web"`), `text`, `image?`(base64 data: URI 或 URL), `deviceId?`, `mode?`(行为模式,缺省回落 `chat`)}。text+image 至少一项;`runner` 未注入或两者皆空 → 直接广播 `stream_end{stopReason:"idle"}`。主会话 `web` 绑定全量 env(`loop.OwnerEnv()`)。`chat` 进入时若该会话正忙 → 由编排器入队(见 §6.4),非阻塞。**仅 owner/admin 连接可发**(见本节角色门)。
|
||||
- **`interrupt`**:`{sessionId}`。命中空闲会话为 no-op。
|
||||
- **`voice_input`**:`{audio`(base64 或 data: URI), `format`(`wav|mp3|ogg|webm|mpeg`,空由 mime 猜,默认 `wav`), `text`(已转写则跳过 STT), `sessionId?`}。无 `voice` 服务 → `{type:"error",data:{message:"voice unavailable"}}`。
|
||||
- **`subagent_ask`**:`{id`(必填), `mode`(空/full/tail/keyword), `n`(tail 查询窗长), `query`(keyword 查询词)}。无 auditor → `error`;id 缺失 → `error`。
|
||||
@@ -1036,11 +1049,13 @@ 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**。
|
||||
>
|
||||
> **来源标记**:`[direct]` 网关直接 emit;`[relay]` hub 从 bus `ws.>` 规范化(任意 `ws.X` 主题 → 类型 `X`);`[bridge]` 桥接 producer 主题;`[target]` 仅定向特定连接。
|
||||
|
||||
| type | 来源 | 数据(`data`) | 用途 |
|
||||
|------|------|----------------|------|
|
||||
| `history` | direct | `{messages:[historyMessage]} ` | 连接首帧回放主会话最近 50 条(thinking 剥离) |
|
||||
| `history` | direct | `{messages:[historyMessage]} ` | 连接首帧回放主会话最近 50 条(thinking 剥离);**仅 owner/admin 连接收到**(`viewer` 不收) |
|
||||
| `message.start` | direct | `{index}` | 正文段开始 |
|
||||
| `message.delta` | direct | `{delta,index}` | 正文 token 增量(累加到当前正文) |
|
||||
| `message.end` | direct | `{index}` | 正文段结束(不结束整轮,由 `stream_end` 收尾) |
|
||||
@@ -1146,11 +1161,15 @@ TOKEN=$(curl -s -X POST http://<host>:8080/api/v1/auth/login \
|
||||
|
||||
---
|
||||
|
||||
## 7. 上传 / 媒体说明(重要检查结论)
|
||||
## 7. 上传 / 媒体说明
|
||||
|
||||
- **无 `/upload` 端点**:网关未注册任何文件上传路由。图片通过 `POST /api/v1/chat` 或 WS `chat` 的 `image` 字段以 **base64 data: URI** 或 **http(s) URL** 传入,入视觉真推理路径(vision/OCR 或主模型多模态,design/06)。文档导入走 `POST /api/v1/admin/rag` 的 JSON `content` 文本。
|
||||
- **无 REST 语音端点**:语音只走 WS —— 上行 `voice_input`(base64 音频或已转写 `text`),下行 `voice_output`(合成音频 base64 + 识别文本 + 回复文本,定向回请求连接)。REST 侧没有语音流。
|
||||
> ⚠️ **本节曾长期写「无 `/upload` 端点 / 无 REST 语音端点」,与实现恰好相反,已于 2026-10-03 修正**——正确口径见上文 §0.3 与 api-client §1 行 10–14。
|
||||
|
||||
- **`POST /api/v1/upload` 存在**(owner/admin + 审计):multipart `file`(光栅图片,5MB 上限)→ `201 {"data":{"url","size","mime","name"}}`;`url` 为**同源访问地址** `/api/v1/files/<name>`,可直接作 `chat.image`。
|
||||
- **`GET /api/v1/files/{name}` 存在**(**无需鉴权**——128-bit 随机能力 URL,`<img>`/`chat.image` 无法带 Bearer):从 `<dataDir>/uploads/` 读该文件;`name` 仅接受「32 位小写 hex + 白名单扩展名」,**路径穿越全形态拒绝**。契约见 api-client §1 行 14。
|
||||
- **REST 语音端点存在**(均 owner-only + 审计):`POST /api/v1/audio/{transcribe,speech,voice-clone}`,供原生/移动端绕开 WS 4KB 帧限;语音**也可**走 WS 上行 `voice_input` / 下行 `voice_output`(定向回请求连接)。
|
||||
- **图片 + 文本并存**:`image` 非空时 turn 变为「带图 + 可选文本」;`text` 空 + `image` 有 → 纯图像 turn。
|
||||
- **文档导入**:走 `POST /api/v1/admin/rag` 的 JSON `content` 文本,**不是**文件上传。
|
||||
- **敏感面**:`image`(base64)与 `bot_token`/`apiKey`/`newSecret` 等概不作为日志明文输出;wire 响应对 token/key 做掩码或不返回。
|
||||
|
||||
---
|
||||
@@ -1171,7 +1190,7 @@ TOKEN=$(curl -s -X POST http://<host>:8080/api/v1/auth/login \
|
||||
| 404 | `not_found` | 资源不存在 |
|
||||
| 409 | `chat_busy` / `duplicate` / `device_offline` / `needs_login` | 冲突:队列满 / 人格已存在 / 设备离线 / 渠道未登录 |
|
||||
| 500 | `internal` / `chat_failed` / `sessions_failed` / … | 服务端错误(含未注入依赖) |
|
||||
| 501 | `not_implemented` | 后端**确定未实现**(新增用户/IoT 场景·规则/插件安装/RAG 删除)——诚实返回,不伪造成功 |
|
||||
| 501 | `not_implemented` | 后端**确定未实现**(新增用户 / IoT 场景·规则 / 主人档案保存 / 编辑环境变量·重启服务·检查更新 / 完整会话转录导出等)——诚实返回,不伪造成功。**注:插件安装(§5.26)与知识库删除(§1.3 #23)已实现,不再返回 501** |
|
||||
|
||||
### 8.2 依赖注入与「未配置」行为
|
||||
|
||||
@@ -1194,15 +1213,15 @@ 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×3)/管理写(人格/API key/设备控制/自动化/LLM 保存·测试/渠道连接·断开·身份测试/渠道配置·删除/KOOK 面板/微信扫码/记忆添加·检索/插件安装·卸载/RAG 导入·删除/系统设置 4 项)。
|
||||
- **已真实实现**:登录/健康检查/聊天(同步+流式)/会话(列表/新建/删除/消息分页)/人格/管理读(personas/sessions/memories/plugins/channels/iot/llm/users/automations/backthink/rag/observability×4:metrics/trace/audit/logs)/管理写(人格/API key/设备控制/自动化/LLM 保存·测试/渠道连接·断开·身份测试/渠道配置·删除/KOOK 面板/微信扫码/记忆添加·检索/插件安装·卸载/RAG 导入·删除/系统设置 4 项)。
|
||||
- **仍为后置 M10(返回 `501 not_implemented`,不伪造)**:新增用户(无用户表/登录)、IoT 场景与规则(无模型/存储)、主人档案保存、编辑环境变量/重启服务/检查更新、完整会话转录导出(当前导出身份元数据)。
|
||||
- **语义/向量增强**:`memory.search` 的 `semantic` 恒为 `false`(未接向量器时诚实反映);RAG `embedded` 反映是否配置向量增强;配置了增强必须真实走语义/向量路径(红线,非空壳)。
|
||||
- **语义/向量增强**:`memory.search` 的 `semantic` 当前恒为 `false`——**向量索引已由组合根注入**(`cmd/cyrene/wire.go` 的 `memory.WithVectorIndex`),但**索引内当前无向量**(生产无写入),故语义打分返回空,属诚实反映而非伪造;RAG `embedded` 反映部署是否配置向量增强;**配置了增强必须真实走语义/向量路径(红线,非空壳)**。
|
||||
- **API key 认证**:`X-API-Key` 当前未被 `Authorize` 中间件校验(见 §0.1 注);key 的签发/吊销/列表已真实生效。
|
||||
|
||||
### 8.5 客户端接入快速索引
|
||||
|
||||
1. `POST /api/v1/auth/login` → `{token, expiresAt, role}`(恒 `owner`)。
|
||||
2. 之后每个 REST 请求带 `Authorization: Bearer <token>`;管理写需 `owner`;管理读需 `owner|admin`;`GET /sessions`、`GET /personas` 任意已认证。
|
||||
3. 实时:`ws://<host>:8080/ws/chat?token=<token>`;首帧 `history` 回放;上行 `chat`/`interrupt`/`voice_input`/`subagent_ask`/`task_output_query`/`mode_switch`;下行见 §6.4。
|
||||
3. 实时:`ws://<host>:8080/ws/chat?token=<token>`;首帧 `history` 回放(**仅 `owner`/`admin` 连接收到**);上行 `chat`/`interrupt`/`voice_input`/`subagent_ask`/`task_output_query`/`mode_switch`(**仅 `owner`/`admin` 连接可发**,`viewer` 发送一律被定向拒);下行见 §6.4 与 api-client §3。
|
||||
4. 身份判定永远来自后端 channel 层 actor 元数据 + 白名单(`configs/users/owner.yaml`),**客户端不要用提示词/文本判身份**;`thinking` 在公开回放/实时帧中已剥离,客户端不应依赖。
|
||||
5. 发送消息带 `deviceId`(推荐)以便端标识落库;`platform` 用于 IM 渠道过滤。
|
||||
Reference in new issue
Block a user