From 32634feefa26baa5d47e43feb700d389599b949d Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sun, 4 Oct 2026 10:37:45 +0800 Subject: [PATCH] =?UTF-8?q?docs(api-reference):=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E4=B8=8E=E5=AE=9E=E7=8E=B0=E7=9B=B8=E5=8F=8D/=E6=BB=9E?= =?UTF-8?q?=E5=90=8E=E7=9A=84=E9=99=88=E8=BF=B0=20+=20=E6=98=8E=E7=A1=AE?= =?UTF-8?q?=E6=9D=83=E5=A8=81=E5=BD=92=E5=B1=9E=EF=BC=88=E5=A5=91=E7=BA=A6?= =?UTF-8?q?=E4=BB=A5=20api-client=20=E4=B8=BA=E5=87=86=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 审计(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。 --- Docs/api-reference.md | 111 +++++++++++++++++++++++++----------------- 1 file changed, 65 insertions(+), 46 deletions(-) diff --git a/Docs/api-reference.md b/Docs/api-reference.md index 98b6b96..751eaaf 100644 --- a/Docs/api-reference.md +++ b/Docs/api-reference.md @@ -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=` 或 `Authorization: Bearer` 取 token(`wsToken`),`Verify` 通过即可升级;**不做角色门槛**(任何有效 token 均可连接),但 `history` 回放与 `proactive` 推送等按角色处理(见 §6)。 +- **WS 鉴权(连接门与帧门是两回事)**:`GET /ws/chat` 通过 `?token=` 或 `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 `。`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://: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://:8080/api/v1/sessions -H "Authorization: Bearer $TOKEN" -H - **鉴权**:`Authorize()`(任意已认证角色)。 - **响应**:`200` `{"data":[...]}`(无 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.`(`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`(其它角色可能为 `""`)。 - **备注**:视觉资产按角色作用域读取,绝不返回别的角色的;缺失时前端回退中性默认。curl: @@ -337,8 +345,9 @@ curl -s 'http://: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:"}}`。 +- **请求 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:"}}`;`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://:8080/api/v1/admin/memories -H "Authorization: Bearer $TOKE ``` ### 4.4 GET /api/v1/admin/plugins -- **响应**:`200 {"data":[...]}`(`PluginSummary` = `{name, kind, description, allowInGroup, allowInStranger}`)。`kind` 为能力类型(如 `plugin|channel|tool|mcp`);`allowInGroup/allowInStranger` 为可达性策略。curl: +- **响应**:`200 {"data":[...]}`(`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://:8080/api/v1/admin/plugins -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1' @@ -672,14 +681,14 @@ curl -s -X POST http://: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://: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://: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://: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":"","toolCount":}}`——**真实安装**: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://: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":"","uninstalled":true}}`——真实从 registry 移除。 +- **响应**:已装 MCP → `200 {"data":{"name":"","uninstalled":true,"kind":"mcp"}}`(`Unregister` 移除其工具 + close + 删 `plugins.json` 条目);**非 MCP / 普通能力** → `200 {"data":{"name":"","uninstalled":true}}`(走 `registry.Remove`)。 - **错误**:`400`(name 缺失);`404`(不存在);`500`。 - **审计**:`plugin.uninstall`。curl: @@ -910,8 +923,8 @@ curl -s -X POST http://: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":"","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":"","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://: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":,"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://: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://:8080/ws/chat?token=`(或 `Authorization: Bearer ` 头)。 - **方法**:必须 `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://: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://: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://: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://: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/`,可直接作 `chat.image`。 +- **`GET /api/v1/files/{name}` 存在**(**无需鉴权**——128-bit 随机能力 URL,``/`chat.image` 无法带 Bearer):从 `/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://: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://: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 `;管理写需 `owner`;管理读需 `owner|admin`;`GET /sessions`、`GET /personas` 任意已认证。 -3. 实时:`ws://:8080/ws/chat?token=`;首帧 `history` 回放;上行 `chat`/`interrupt`/`voice_input`/`subagent_ask`/`task_output_query`/`mode_switch`;下行见 §6.4。 +3. 实时:`ws://:8080/ws/chat?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 渠道过滤。