diff --git a/Docs/api-client.md b/Docs/api-client.md index c00ac5f..c5a2f5e 100644 --- a/Docs/api-client.md +++ b/Docs/api-client.md @@ -14,6 +14,8 @@ - **认证**: `Authorization: Bearer `(RBAC:`owner`/`admin`/`viewer`,权限矩阵见 §2.0);**`X-API-Key` 已接线(v0.51)**:所有已认证 API(管理 `/api/v1/admin/*` 与用户侧 `/api/v1/*`)同时接受 `X-API-Key`;命中签发表即通过,且**一律 `owner` 级**(不再按角色分档,裁决 15.4.3)。请求带该头时**只走 key 分支**——未知/已吊销/空值回 `401` 且**不回落 Bearer**(fail-closed)。**WS 握手仍只认 Bearer**。 - **响应统一**: `{"data": ...}` / `{"error": {"code": , "message": }}`;状态码 200/201/204/400/401/403/404/429/500。 - **错误码**:业务错误带 `code` 字符串(如 `unauthorized` / `not_found`),便于客户端分支。 +- **数据目录 `DATA_DIR`(v0.55)**:默认 `./data`;**新增接受保留字 `platform`(大小写不敏感)** → 平台标准数据目录(Windows `%APPDATA%\Cyrene`、macOS `~/Library/Application Support/Cyrene`、Linux `~/.local/share/cyrene`)。**默认 `./data` 与显式路径行为不变**;**显式空值行为也不变**(仍走原兜底,不会因升级突然换库)。 +- **管理面板安全入口 `ADMIN_PATH`(v0.55,前端路由面,不是 REST 契约)**:面板**前端**路由前缀 = `/` + `ADMIN_PATH`(默认 `admin` → `/admin`;`xxx/admin` → `/xxx/admin`;**非法值回退 `/admin`**)。HTTP 行为:`/` 与其深链返回**注入了 `` 与 `window.__CYRENE_ADMIN_ENTRY__={"prefix":"..."}` 的 index.html**(`Cache-Control: no-store`);**形如面板入口(路径里出现 `admin` 段)但前缀不符的路径 → `404`**(**不回落用户端聊天壳**);`/` 若恰好命中 `dist` 真实文件则仍走文件服务器。**API 不藏**:`/api/v1/*`、`/api/v1/admin/*`、`/ws/chat`、webhook 一律原样,**无新端点、无帧变更**(安全靠 JWT + 角色门,design/07 §0.1)。**口径**:用户端 HTML **永不携带该前缀串**(后端只在命中前缀时注入)⇒ 用户端既无管理入口链接、也解析不出面板路由(design/07 §0.2)。 --- @@ -110,7 +112,7 @@ - Web 端镜像实现在 `frontend/web/src/lib/enums.ts`(`labelFor(kind, value)` 对未知值返回原值),权威仍以本端点为准。 - **反漂移保证(后端)**:后端有测试断言「每个内置适配器的 `Name()` 都在 `platforms` 表内」「`moods` 与 persona 情绪状态机的内置状态一致」「`modes` 与前端 `MODES` 九模式一致」——漏登记即 CI FAIL,客户端拿到的表不会滞后于代码。 -> **契约版本 `v0.54`**(**渠道配置写路径改 patch 语义(修 P0 级凭据静默丢失) + 阶段 6 前端批的纯前端行为登记**,依据 design/04 §3.1、design/22 §2 方案 b(与 `personaId` 同构的裁决延续,commit `4ae7148`)与裁决 16.4.1 / 16.4.2 / 16.4.3 / 16.4.4 / 16.4.5 / 16.4.6(commit `f454273` / `b480b04` / `c1a98d7` / `8d70f6d`)):① **`PUT /api/v1/admin/channels/{platform}/config` 的 `config` 由「整条替换」改为「按 id 打补丁」**(`runtimeconfig.PatchChannel`,**与 `personaId` 同构的三态**)——**键省略(该键不在 `config` 里,或 `config` 整体缺席 / 为 `null`)= 保留该账号已存值**、**给出非空值 = 覆盖该键**、**显式 `null` 或 `""` = 清除该键**(`0` / `false` 是真实取值,**不算空**);顶层便捷字段(`token` / `baseURL` / …)是普通 `string`,**只在非空时**并入补丁 ⇒ **要清除 `bot_token` 必须走 `config` 的显式 `null` / `""`**;`allowGroups` / `denyGroups` / `allowUsers` / `denyUsers` 与三个限流键**仍是整值替换**(省略 = 清空)。② **破坏性提示**:按旧文实现、依赖「省略某 `config` 键 = 删掉它」的客户端,必须改为**显式传 `null` / `""`** 才能清除该键。③ **修掉的数据安全问题**:面板只发送用户本次碰过的平台字段,后端却整条替换 ⇒ 保存**微信(iLink)**账号卡会抹掉 `bot_token` / `bot_id` / `owner_id` / `get_updates_buf`(**iLink token 是长期凭据**)、**Telegram** 只填 `webhook_url` 而不重填 token 会让 `cleanAccounts` **丢掉整个账号**。④ **未改**:token 要求型平台**显式**清 token 仍按 `cleanAccounts` 丢账号;「本次一个 `config` 键都没给出」在**非 `weixin-ilink`** 平台仍回 `400`。⑤ **无新端点 / 新字段 / 响应形状变更**。**同版另含阶段 6 前端批(纯前端行为,无 API 变化)**:① **16.4.1 静默/活动态与预约切换**——唯一入口 `frontend/web/src/components/modeSwitch.ts`:她活动时用户切换只登记「已预约」、静默时落地;② **16.4.5 会话级消息缓存**——帧按自己的 `sessionId` 落进该会话的消息桶(切会话不清空),**只有单槽忙态**仍按激活会话判定;`graph_update` 按 `data.sessionId` 入**图桶**,管理端 Flow 页取激活会话、Backthink 页取 `sess-backthink`(裁决 16.4.2);③ **16.4.3 / 16.4.6 虚拟滚动与专属页只读卡**——长列表超阈值窗口化、专属页只读展示该平台完整字段;④ **16.4.4 双档 UI 模式**——性能档 = 现状(默认)、华丽档落 `:root.ui-mode-fancy`,持久化键 `cyrene.ui-mode`(纯前端,对应 `design/07 §0.1`,不改后端契约)。**上一版 `v0.53`**:(**阶段 5 落地:模型目录端点 + per-persona 路由 / 本地三级 / capacity L3 / 视觉·OCR 独立模型 / `image-crop` 工具 / 按 provider 形状下发推理开关**,依据 design/04 §3/§3.1/§3.2(裁决 03.4.1 / 03.4.2 / 03.4.3 / 08.4.1)、design/06 §0/§3(裁决 14.4.1 / 14.4.5)、design/14 §2.2/§3/§4(裁决 14.4.2 / 14.4.3 / 14.4.4),2026-10-04 落实):① **新增管理只读端点 `GET /api/v1/admin/llm/catalog`**(**owner|admin** 读,`?refresh=1` 强制在线刷新)→ `200 {data:{source:"cache"|"models.dev", fetchedAt?, stale?, warning?, providers:[{id,name,api?,doc?,env?,npm?,models:[{id,name,attachment,reasoning,toolCall,contextLimit?,outputLimit?}]}]}}`;**既无本地快照又抓不到** → `503 {error:{code:"catalog_unavailable", message}}`;客户端带 `Accept-Encoding: gzip` 时响应带 `Content-Encoding: gzip`(**信封形状不变**)。**副作用只有一处**:落盘快照 `/llm_catalog.json`(**不含任何凭据**、`data/` 已 gitignore、TTL 24h),**不改 `models.json`**;**抓取只发生在本端点内,不进运行时热路径**(模型请求链路从不读外网目录)。详见 §2「模型配置契约(v0.53)」。② **`models.json` 组级新增键**(载体是组,**可热更新**,未声明时该键不出现):`reasoning: {kind: "auto"|"deepseek"|"openai"|"vllm"|"dashscope"}` 决定该组用哪种**提供商开关形状**下发推理指令(**非法值被 `Group.Validate` 直接拒绝**;anthropic 组上声明 openai 系形状同样拒绝)、`personaModels: {personaId: modelId}` 与 `personaProviders: {personaId: "openai"|"anthropic"}` 为**角色级模型/提供商路由**(**非法 provider 值同样拒绝**;未绑定/未知角色回落该组默认)。③ **语义修正(重要)**:**`multimodal` = 主模型自身能否收图**;`LLM_VISION_MODEL` / `LLM_OCR_MODEL` **只配视觉/OCR 工具用的模型**——两者互不代表(见 §1 行 3)。④ **`.env` 新增 9 键**(见 §2「模型配置契约(v0.53)」表与 `.env.example`):`LLM_REASONING_KIND`(默认空 = `auto`)、`LLM_OCR_MODEL`、`LLM_LOCAL_ENABLED`(默认 `false`)/`LLM_LOCAL_PROVIDER`/`LLM_LOCAL_BASE_URL`/`LLM_LOCAL_API_KEY`/`LLM_LOCAL_MODEL`、`CAPACITY_L3_ENABLED`(默认 `false`)/`CAPACITY_L3_CACHE_TTL`(默认 `5m`)。⑤ **内置工具新增 `image-crop`**(按区域裁切图片:`x`/`y`/`width`/`height` → PNG data URI + 裁切尺寸;供大图分区精读;**策略默认 owner-only**)。⑥ **推理开关按形状下发(旧陈述修正)**:v0.41 的「Qwen 一类只 WARN、请求一字不改」**已作废**——形状由 `reasoning.kind` 声明后**真下发**(→ 字段名映射表见 changelog v0.53 条),**只有形状未知/有歧义时才 WARN 且一个字段都不发**;`reasoning_effort` 的非法值由「降级到最近一档」改为**丢弃并记 WARN(不降档)**,`effort:none` 一律翻成**显式关思考**。**未做**:DashScope 形状的 `auto` 推断(只能在显式声明 `reasoning.kind=dashscope` 时下发,绝不从模型名/BaseURL 猜)、Qwen 的 `/think` 前导消息软开关(与 design 口径冲突,未实现)。**上一版 `v0.52`**:(**工具策略落盘 + 主代理代审小字 + 流程图节点参数摘要**,依据 design/11 §3/§4/§8(裁决 1.3 / 10.4.2 / 13.4.3)与 design/19 §1/§1.1(裁决 11.4.2 / 11.4.3 / 13.4.1),2026-10-04 落实):① **工具策略(`allowInGroup`/`allowInStranger`)落盘 SQLite `tool_policies`(迁移 `0012_tool_policy`),重启保留**——`registry.SetPolicy` **先落库后改内存**,落盘失败则内存保持旧值并返回错误(见 §2 行 5);② 新增下行帧 **`approval_notice`**(§3.11):敏感操作经主代理代审后推一行小字(`explain` 由**后端产出**、客户端原样渲染),审计口径 `who`=角色名 / `what`=`approval.<工具名>` / `result`=`granted|denied`,**仅 `owner` + `admin`**(fail-closed 默认);③ `graph_update` 节点 `props` 新增四类**纯增量可选**键(tool `args_keys`/`args_summary`、guardrail `action`/`allow`/`elapsed_ms`、memory `action`/`scope`/`hits`/`bytes`/`count`/`failed`、orchestration `subagent`/`persona`),`memory`/`guardrail`/`orchestration` 旁路节点 span **首次真进图**,值在发布前已脱敏——**帧名、事件名、顶层字段集一字未动**(见 §3.5)。**上一版 `v0.51`**:(**后台子代理结果登 WS 帧 + 回填父会话**,依据 design/05 §9「子代理完成 → 结果回填主会话并提示用户」,裁决 02.4.3,2026-10-04 落实):新增下行帧 **`subagent_result`**(`{type, sessionId, data:{sessionId, status:"done", summary, agents:[{id, goal, role?, mode?, status, summary?, text?, steps?}]}}`,见 §3.10)——`subagent_spawn` 的批次转后台后结算时,编排层发布在 bus 主题 `subagent.result` 上的合并结果,此前**没有任何消费者**(中继既非 `ws.*` 也不认该 4 段以外的 subject)⇒ 结果上不了前台、只剩进度帧;现在该 subject 被登记为中继并成形为这一帧,**帧壳 `sessionId` = 派生该批次的父会话**,结果因此落回**父会话的实时流**。**角色门**:新帧属内容帧 → **只发 `owner` + `admin`**(fail-closed 默认,未扩 7 个状态机信号白名单)。**向后兼容**:纯新增帧,老客户端按未知帧忽略即可,既有帧/端点/鉴权一律不变。**诚实边界**:结果**不落库**(父会话转录里没有新消息、重连回放不补发)、**未**经 design/15 会话消息编排器续接、**前端尚无该帧 handler**(今天的效果是「帧到达 owner/admin 连接」)。**同版另含管理面(D 组,2026-10-04 落实)**:① **`X-API-Key` 真正接线**(裁决 15.4.2):管理/用户侧已认证端点同时接受该头,命中签发表即通过且**一律 `owner` 级**(裁决 15.4.3 取消 role 分级),带该头时只走 key 分支、未知/已吊销/空值回 `401` 且不回落 Bearer(fail-closed),WS 握手仍只认 Bearer;② **新增主人白名单档案读写** `GET`/`PUT /api/v1/admin/owner-profile`(裁决 10.4.5,**owner-only**,写带审计 `owner.profile.update`)——面板的「保存」从此是**真保存**(落盘 `configs/users/owner.yaml` 0600 原子写 + 热替换活档案,渠道 actor 判定立即生效),不再是只改本地 state 的假成功;③ **persona `identity.systemPrompt` 改必填**(裁决 08.4.2):缺失/空白回 `400 bad_request`,**不再派生默认说明串**,写出的 YAML 必能被载入期同一规则重新载入。见下方「管理面板写操作契约」。**上一版 `v0.50`**:(**事件批:流式帧的结构化补齐**,依据 design/04 §1.1,裁决 01.4.1 / 01.4.2 / 01.4.3 / 03.4.4,2026-10-04 落实):① **`tool_execution` 拆三事件**——旧单帧 `tool_execution.update {id,index,delta}`(phase 与工具名拼在 `delta` 字符串里)被 **`tool_execution.start`**(进入执行,`phase:"running"`)/ **`tool_execution.update`**(运行中进度/局部输出,可多帧)/ **`tool_execution.end`**(完成或失败,`phase:"done"|"error"`)替换,载荷改为结构化 `{tool, phase, id?}`(`tool` = 工具名;`phase` = 细粒度执行阶段,与 §3.5 流程图节点 status 同一套 `running/done/error` 枚举;`id` = provider 工具调用 id,可与 `tool_call.*` 帧关联)——**phase 不再塞进 `delta`**;loop 现已真实发出 start/end(update 的 wire 映射已实现并验证,但工具分发器尚无中途进度上报源,**暂无生产者**,见 §3.1)。② **`streamId`(模型流帧 `data` 内新增可选字段)**:同一次模型流(一次 ChatStream 调用)的多帧共享同一 `streamId`,多步工具轮的第二次模型调用取新 id;与帧壳的 `turnId`(整个回合恒定)分工——`turnId` 标记「这轮回答」,`streamId` 标记「这轮里的哪一条流式消息」。③ **`turn.start` / `turn.end`(新帧)**:loop 在回合边界发出、经 ws_bridge 下行;**无 `data` 载荷**(帧壳的 `sessionId`/`turnId`/`seq` 即边界信号;回合终态信息仍由既有 `stream_end` 携带);`turn.end` 覆盖回合的所有出口(最终答案 / 工具循环安全网终止 / 预算耗尽 / 步骤出错),被静音的内部流程(backthink / 子代理)不发出。④ **向后兼容**:`turn.start/end` 与 `streamId` 为纯新增,老客户端可安全忽略;**破坏性**在旧 `tool_execution.update` 单帧——loop 不再产生它,按旧形状(解析 `delta` 里的 `phase:工具名` 拼串)消费的客户端必须迁移到三事件;现有 Web 前端未消费这些帧,行为不变。⑤ 本批新帧全部落在 v0.48 下行帧门的「仅 `owner` + `admin`」档(fail-closed 默认,无需扩白名单)。⑥ **`graph_update` 会话范围口径修正**(裁决 01.4.5,随实现事实补记):`web` / `web:` / `sess-backthink` 三范围(见 §3.5)。见 §3.1 与 v0.50 变更记录。**上一版 `v0.49`**:(**WS 握手回放与上行帧的角色门(design/11 §7.2)**:补上 §7.1 下行门之外的**同一类缺口的另两处**,对 `viewer` **继续收紧**,`owner` / `admin` 行为不变,**无端点、无字段形状变更**——① **握手 `history` 回放改为仅 `owner` + `admin`**:此前任意角色一连上 `/ws/chat` 就会收到主会话最近 50 条消息正文(`thinking` 已剥离,但**正文本身**就是主人的对话),`viewer` 从此**不收**;② **上行 JSON 帧按角色授权(fail-closed)**:非 `owner`/`admin` 连接发来的**任何**上行 JSON 帧一律拒绝,**定向**(只回发送者)回 `{type:"error",data:{code:"forbidden"}}`,**不广播、不驱动模型、不落库、不返回任何内容**——被拒帧含 `chat`(此前 `viewer` 可**真的驱动模型并落库**)、`interrupt`、`subagent_ask` / `task_output_query`(其应答是**定向**帧、能绕过 §7.1 下行门把子代理上下文/任务输出直接回给 `viewer`)、`voice_input`、`mode_switch`,且**未登记的新上行帧默认拒**;**协议级帧(ping/pong、close)不受影响**。③ **合口径**:`viewer` = **可连接、可读 12 条只读监控 REST(v0.40)、可收 7 个状态机信号帧(v0.48)**;**除此以外 WS 上拿不到任何内容、也发不出任何指令**。见 §3 开头的「上行帧与握手回放的角色门」与 v0.49 变更记录。**上一版 `v0.48`**:(**WS 下行帧的角色门(design/11 §7.1)**:hub 的下行现在是**按帧类型分档**的——只有 7 个**状态机信号**(`session_busy` / `pending_ack` / `pending_drain` / `session_idle` / `session_interrupted` / `pending_full` / `session_read`,**均不含任何内容**)继续发给**所有已认证连接**(含 `viewer`),**其余全部下行帧**(流式内容 `message.*` / `thinking.*` / `tool_call.*` / `tool_execution.update` / `stream_chunk` / `stream_end` / `thinking`;持久化 `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`)**改为仅发 `owner` + `admin`**)。**对 `viewer` 是收紧**:此前它连上 `/ws/chat` 就能收到角色回复正文与推理内容,现在收不到;**`owner` / `admin` 行为不变**;**无端点、无字段形状变更**——`viewer` 的 12 条只读监控 REST 端点(`viewerAuth`,v0.40)与 `/ws/chat` 的连接鉴权都**照旧**;**新帧类型默认只发 owner/admin**(fail-closed)。见 §3 开头的「下行帧的角色门」与 v0.48 变更记录。**上一版 `v0.47`**:(**`graph_update` 子代理泳道根节点新增可选 `props.persona`**:派发子代理时若指定了目标角色,该分支根节点的 `props` 就多一个 `persona` 键,标注它**正在扮演的角色**——用途是让流程图能显示「这个子代理是以谁的身份在替班」,而不只是子代理 id;**纯增量**:不带角色派发时该键不出现,老客户端忽略未知键即可,见 §3.5 与 v0.47 变更记录)。**上一版 `v0.46`**:(**`PATCH /api/v1/sessions/{id}` 的 `personaId` 由「可重新绑定该会话的角色」改为「出现即 `400`」**:**会话角色在创建时确定、之后不可变**,要换角色请新开会话(design/22 §8-1)——**明确拒绝**而非静默忽略/200 空操作;**键出现**(任何取值:`"yuki"` / `""` / `null` / 未知 id / 空白串)一律 `400 bad_request` 且消息点名 `personaId`,该检查**先于**会话存在性检查(未知会话 + `personaId` 也回 `400` 而不是 `404`)。**破坏性**:按 v0.42–v0.45 旧文实现、依赖「PATCH 换角色 / 显式 `""` 解绑」的客户端必须改为**新建会话**选角色;`title`/`pinned` 行为与 `POST /sessions {personaId}`(创建时绑定)**完全不变**,不再有 `501/200 {personaId}` 的 PATCH 路径)。上一版 `v0.45`:`PUT /admin/channels/{platform}/config` 的 `personaId` 由「省略/`""` 都 = 解绑」修订为三态(**省略(或 `null`)= 保留已存绑定**、**显式 `""` = 解绑**、**非空 id = 绑定**,未知角色仍 `400`)。 +> **契约版本 `v0.55`**(**阶段 7 落地项登记:路径式安全入口 + 数据目录 `platform` + OneBot v11 四端 + 可观测实时推送**,依据裁决 1.6 / 16.4.7 / 12.4.2(commit `a398dc9`)、1.4 / 12.4.1(commit `9253fef`)、05.4.3 + 16.4.3-WS(commit `0e1f163`),以及 13.4.5 面内三条(commit `74b12d8`)、MCP 官方 SDK(commit `a1083a3`),2026-10-04 落实):**A. 安全入口 `ADMIN_PATH`(裁决 1.6 / 16.4.7,前端路由面)**——① env 新增 **`ADMIN_PATH`**(默认 `admin`):面板**前端**路由前缀 = `/` + 值(`admin` → `/admin`,`xxx/admin` → `/xxx/admin`;**非法值回退 `/admin`**——容忍首尾斜杠与空白,拒绝 `.`/`..` 段与 `?`/`#`)。② HTTP 行为:`/` 与其深链返回**注入了 `` 与 `window.__CYRENE_ADMIN_ENTRY__={"prefix":"..."}` 的 index.html**(`Cache-Control: no-store`);**形如面板入口(路径里出现 `admin` 段)但前缀不符的路径 → `404`**(不回落用户端聊天壳、不泄露正确前缀);`/` 若恰好命中 `dist` 真实文件则仍走文件服务器。③ **API 不藏**:`/api/v1/*`、`/api/v1/admin/*`、`/ws/chat`、webhook 一律原样,**无新端点、无帧变更**。④ **口径**:用户端 HTML **永不携带该前缀串**(后端只在命中前缀时注入)⇒ 用户端既无管理入口链接、也解析不出面板路由(design/07 §0.2;前端路由改**路径式**:`frontend/web/src/admin/router.ts` 只读注入、无硬编兜底、移除 hash 入口)。**B. 数据目录(同 commit,裁决 12.4.2)**——`DATA_DIR` 新增可接受值 **`platform`**(大小写不敏感)→ 平台标准目录;默认 `./data` 与显式路径行为不变;**显式空值行为不变**(仍走原兜底)。**C. OneBot v11 四端(`9253fef`,裁决 1.4 / 12.4.1)**——① 配置面(channels.json 账号 `config` + 适配器 `Config()` 快照)新增 `transport`(`ws`|`http`,默认 `ws`,未知值回落 `ws`)/ `api_base_url` / `report_path`(默认 `/`);快照 camelCase 为 `transport`/`apiBaseURL`/`reportPath`;`remoteURL` 快照值 = `remote_url`、为空时回落 `api_base_url`(语义即「出站远端地址」),**既有键与值不变**。② env 兜底 `CHANNEL_QQ_OBV11_TRANSPORT`(默认 `ws`)/ `CHANNEL_QQ_OBV11_API_BASE_URL` / `CHANNEL_QQ_OBV11_REPORT_PATH`。③ 四端语义:`ws+client` Cyrene 拨号、收发同一 WS;`ws+server` OneBot 反连、收发同一 WS;`http+client` Cyrene→OneBot(`POST {api_base_url}/{action}`,纯出站);`http+server` OneBot→Cyrene(POST 到 `listen_addr`+`report_path`,纯入站,fail-closed 鉴权、10MiB 体上限)。④ 取舍:`mode` 决定**哪一半必需**(server 必需 `listen_addr`、client 必需 `api_base_url`),另一半给齐字段就一并启用 ⇒ 单个适配器即可「收上报 + 回消息」。⑤ 端口例外:`http+server` 用 `listen_addr`,属 `AGENTS.md` §9 已登记的**同一豁免**(design/13 §0 原文即写「WS server / HTTP server」);`listen_addr` 为空则报错、**绝不开端口**。**D. 可观测实时推送(`0e1f163`,裁决 05.4.3 + 16.4.3-WS)——新增一节 §3.12**:三个**下行新帧** `observability_log`(`{entries:[{time,level,source,msg}…]}`,最新在前,上限 200 条)/ `observability_trace`(`{spans:[…]}`,上限 100 条)/ `observability_metrics`(`AdminMetrics` 本体、扁平),均**纯新增**(既有帧名/形状零改动)、帧壳**不带** `sessionId`/`turnId`/`seq`;三者**不在** `frameReachesAll` 白名单 ⇒ fail-closed **只发 owner+admin**(viewer 继续走 REST 只读档);订阅 = owner/admin 凭据**读取** `GET /admin/observability/{logs,metrics,trace}` 任一个(该次读取照常返回完整引导数据),该凭据 WS 连接全断即退订并停循环(**不是轮询、无常驻 goroutine**);**REST 端点完全不变**,`?level=` 分级过滤改由**客户端本地**做(帧广播给所有 `owner`/`admin` 连接,订阅只决定循环跑不跑)。**E. 其余(各一句)**:① **13.4.5 面内三条**(`74b12d8`)——续接窗口 `Recent` 在同毫秒并列时改为取**最新** n 条且按插入序(**无 API 变化**,但客户端可观察到的「握手回放 / 续接上下文」顺序更正确);上行 `chat` 不再经**可丢弃**的 bus(**帧序列不变**);被**取消**回合的 checkpoint 不再顶掉下一条消息,新增内部常量 `loop.StopReasonCanceled`(`checkpoints.state.stop_reason` 可能出现该值,**不经 REST/WS 暴露**)。② **MCP**(`a1083a3`)——协议层换成**官方 Model Context Protocol Go SDK v1.8.0**,**REST/WS/env/数据目录零变更**(`MCP_SERVERS_JSON`、`MCP_*`、`transport` 枚举 `stdio|http|streamable|sse` 全不变)。**未做(诚实边界)**:DashScope 形状的 `auto` 推断、Qwen 的 `/think` 前导消息软开关、`get_group_list` 枚举、上游显式退订帧(当前退订只发生在订阅凭据的连接全断时)、审计 tab 仍 5s REST 轮询(未做 `observability_audit` 帧)。**上一版 `v0.54`**:(**渠道配置写路径改 patch 语义(修 P0 级凭据静默丢失) + 阶段 6 前端批的纯前端行为登记**,依据 design/04 §3.1、design/22 §2 方案 b(与 `personaId` 同构的裁决延续,commit `4ae7148`)与裁决 16.4.1 / 16.4.2 / 16.4.3 / 16.4.4 / 16.4.5 / 16.4.6(commit `f454273` / `b480b04` / `c1a98d7` / `8d70f6d`)):① **`PUT /api/v1/admin/channels/{platform}/config` 的 `config` 由「整条替换」改为「按 id 打补丁」**(`runtimeconfig.PatchChannel`,**与 `personaId` 同构的三态**)——**键省略(该键不在 `config` 里,或 `config` 整体缺席 / 为 `null`)= 保留该账号已存值**、**给出非空值 = 覆盖该键**、**显式 `null` 或 `""` = 清除该键**(`0` / `false` 是真实取值,**不算空**);顶层便捷字段(`token` / `baseURL` / …)是普通 `string`,**只在非空时**并入补丁 ⇒ **要清除 `bot_token` 必须走 `config` 的显式 `null` / `""`**;`allowGroups` / `denyGroups` / `allowUsers` / `denyUsers` 与三个限流键**仍是整值替换**(省略 = 清空)。② **破坏性提示**:按旧文实现、依赖「省略某 `config` 键 = 删掉它」的客户端,必须改为**显式传 `null` / `""`** 才能清除该键。③ **修掉的数据安全问题**:面板只发送用户本次碰过的平台字段,后端却整条替换 ⇒ 保存**微信(iLink)**账号卡会抹掉 `bot_token` / `bot_id` / `owner_id` / `get_updates_buf`(**iLink token 是长期凭据**)、**Telegram** 只填 `webhook_url` 而不重填 token 会让 `cleanAccounts` **丢掉整个账号**。④ **未改**:token 要求型平台**显式**清 token 仍按 `cleanAccounts` 丢账号;「本次一个 `config` 键都没给出」在**非 `weixin-ilink`** 平台仍回 `400`。⑤ **无新端点 / 新字段 / 响应形状变更**。**同版另含阶段 6 前端批(纯前端行为,无 API 变化)**:① **16.4.1 静默/活动态与预约切换**——唯一入口 `frontend/web/src/components/modeSwitch.ts`:她活动时用户切换只登记「已预约」、静默时落地;② **16.4.5 会话级消息缓存**——帧按自己的 `sessionId` 落进该会话的消息桶(切会话不清空),**只有单槽忙态**仍按激活会话判定;`graph_update` 按 `data.sessionId` 入**图桶**,管理端 Flow 页取激活会话、Backthink 页取 `sess-backthink`(裁决 16.4.2);③ **16.4.3 / 16.4.6 虚拟滚动与专属页只读卡**——长列表超阈值窗口化、专属页只读展示该平台完整字段;④ **16.4.4 双档 UI 模式**——性能档 = 现状(默认)、华丽档落 `:root.ui-mode-fancy`,持久化键 `cyrene.ui-mode`(纯前端,对应 `design/07 §0.1`,不改后端契约)。**上一版 `v0.53`**:(**阶段 5 落地:模型目录端点 + per-persona 路由 / 本地三级 / capacity L3 / 视觉·OCR 独立模型 / `image-crop` 工具 / 按 provider 形状下发推理开关**,依据 design/04 §3/§3.1/§3.2(裁决 03.4.1 / 03.4.2 / 03.4.3 / 08.4.1)、design/06 §0/§3(裁决 14.4.1 / 14.4.5)、design/14 §2.2/§3/§4(裁决 14.4.2 / 14.4.3 / 14.4.4),2026-10-04 落实):① **新增管理只读端点 `GET /api/v1/admin/llm/catalog`**(**owner|admin** 读,`?refresh=1` 强制在线刷新)→ `200 {data:{source:"cache"|"models.dev", fetchedAt?, stale?, warning?, providers:[{id,name,api?,doc?,env?,npm?,models:[{id,name,attachment,reasoning,toolCall,contextLimit?,outputLimit?}]}]}}`;**既无本地快照又抓不到** → `503 {error:{code:"catalog_unavailable", message}}`;客户端带 `Accept-Encoding: gzip` 时响应带 `Content-Encoding: gzip`(**信封形状不变**)。**副作用只有一处**:落盘快照 `/llm_catalog.json`(**不含任何凭据**、`data/` 已 gitignore、TTL 24h),**不改 `models.json`**;**抓取只发生在本端点内,不进运行时热路径**(模型请求链路从不读外网目录)。详见 §2「模型配置契约(v0.53)」。② **`models.json` 组级新增键**(载体是组,**可热更新**,未声明时该键不出现):`reasoning: {kind: "auto"|"deepseek"|"openai"|"vllm"|"dashscope"}` 决定该组用哪种**提供商开关形状**下发推理指令(**非法值被 `Group.Validate` 直接拒绝**;anthropic 组上声明 openai 系形状同样拒绝)、`personaModels: {personaId: modelId}` 与 `personaProviders: {personaId: "openai"|"anthropic"}` 为**角色级模型/提供商路由**(**非法 provider 值同样拒绝**;未绑定/未知角色回落该组默认)。③ **语义修正(重要)**:**`multimodal` = 主模型自身能否收图**;`LLM_VISION_MODEL` / `LLM_OCR_MODEL` **只配视觉/OCR 工具用的模型**——两者互不代表(见 §1 行 3)。④ **`.env` 新增 9 键**(见 §2「模型配置契约(v0.53)」表与 `.env.example`):`LLM_REASONING_KIND`(默认空 = `auto`)、`LLM_OCR_MODEL`、`LLM_LOCAL_ENABLED`(默认 `false`)/`LLM_LOCAL_PROVIDER`/`LLM_LOCAL_BASE_URL`/`LLM_LOCAL_API_KEY`/`LLM_LOCAL_MODEL`、`CAPACITY_L3_ENABLED`(默认 `false`)/`CAPACITY_L3_CACHE_TTL`(默认 `5m`)。⑤ **内置工具新增 `image-crop`**(按区域裁切图片:`x`/`y`/`width`/`height` → PNG data URI + 裁切尺寸;供大图分区精读;**策略默认 owner-only**)。⑥ **推理开关按形状下发(旧陈述修正)**:v0.41 的「Qwen 一类只 WARN、请求一字不改」**已作废**——形状由 `reasoning.kind` 声明后**真下发**(→ 字段名映射表见 changelog v0.53 条),**只有形状未知/有歧义时才 WARN 且一个字段都不发**;`reasoning_effort` 的非法值由「降级到最近一档」改为**丢弃并记 WARN(不降档)**,`effort:none` 一律翻成**显式关思考**。**未做**:DashScope 形状的 `auto` 推断(只能在显式声明 `reasoning.kind=dashscope` 时下发,绝不从模型名/BaseURL 猜)、Qwen 的 `/think` 前导消息软开关(与 design 口径冲突,未实现)。**上一版 `v0.52`**:(**工具策略落盘 + 主代理代审小字 + 流程图节点参数摘要**,依据 design/11 §3/§4/§8(裁决 1.3 / 10.4.2 / 13.4.3)与 design/19 §1/§1.1(裁决 11.4.2 / 11.4.3 / 13.4.1),2026-10-04 落实):① **工具策略(`allowInGroup`/`allowInStranger`)落盘 SQLite `tool_policies`(迁移 `0012_tool_policy`),重启保留**——`registry.SetPolicy` **先落库后改内存**,落盘失败则内存保持旧值并返回错误(见 §2 行 5);② 新增下行帧 **`approval_notice`**(§3.11):敏感操作经主代理代审后推一行小字(`explain` 由**后端产出**、客户端原样渲染),审计口径 `who`=角色名 / `what`=`approval.<工具名>` / `result`=`granted|denied`,**仅 `owner` + `admin`**(fail-closed 默认);③ `graph_update` 节点 `props` 新增四类**纯增量可选**键(tool `args_keys`/`args_summary`、guardrail `action`/`allow`/`elapsed_ms`、memory `action`/`scope`/`hits`/`bytes`/`count`/`failed`、orchestration `subagent`/`persona`),`memory`/`guardrail`/`orchestration` 旁路节点 span **首次真进图**,值在发布前已脱敏——**帧名、事件名、顶层字段集一字未动**(见 §3.5)。**上一版 `v0.51`**:(**后台子代理结果登 WS 帧 + 回填父会话**,依据 design/05 §9「子代理完成 → 结果回填主会话并提示用户」,裁决 02.4.3,2026-10-04 落实):新增下行帧 **`subagent_result`**(`{type, sessionId, data:{sessionId, status:"done", summary, agents:[{id, goal, role?, mode?, status, summary?, text?, steps?}]}}`,见 §3.10)——`subagent_spawn` 的批次转后台后结算时,编排层发布在 bus 主题 `subagent.result` 上的合并结果,此前**没有任何消费者**(中继既非 `ws.*` 也不认该 4 段以外的 subject)⇒ 结果上不了前台、只剩进度帧;现在该 subject 被登记为中继并成形为这一帧,**帧壳 `sessionId` = 派生该批次的父会话**,结果因此落回**父会话的实时流**。**角色门**:新帧属内容帧 → **只发 `owner` + `admin`**(fail-closed 默认,未扩 7 个状态机信号白名单)。**向后兼容**:纯新增帧,老客户端按未知帧忽略即可,既有帧/端点/鉴权一律不变。**诚实边界**:结果**不落库**(父会话转录里没有新消息、重连回放不补发)、**未**经 design/15 会话消息编排器续接、**前端尚无该帧 handler**(今天的效果是「帧到达 owner/admin 连接」)。**同版另含管理面(D 组,2026-10-04 落实)**:① **`X-API-Key` 真正接线**(裁决 15.4.2):管理/用户侧已认证端点同时接受该头,命中签发表即通过且**一律 `owner` 级**(裁决 15.4.3 取消 role 分级),带该头时只走 key 分支、未知/已吊销/空值回 `401` 且不回落 Bearer(fail-closed),WS 握手仍只认 Bearer;② **新增主人白名单档案读写** `GET`/`PUT /api/v1/admin/owner-profile`(裁决 10.4.5,**owner-only**,写带审计 `owner.profile.update`)——面板的「保存」从此是**真保存**(落盘 `configs/users/owner.yaml` 0600 原子写 + 热替换活档案,渠道 actor 判定立即生效),不再是只改本地 state 的假成功;③ **persona `identity.systemPrompt` 改必填**(裁决 08.4.2):缺失/空白回 `400 bad_request`,**不再派生默认说明串**,写出的 YAML 必能被载入期同一规则重新载入。见下方「管理面板写操作契约」。**上一版 `v0.50`**:(**事件批:流式帧的结构化补齐**,依据 design/04 §1.1,裁决 01.4.1 / 01.4.2 / 01.4.3 / 03.4.4,2026-10-04 落实):① **`tool_execution` 拆三事件**——旧单帧 `tool_execution.update {id,index,delta}`(phase 与工具名拼在 `delta` 字符串里)被 **`tool_execution.start`**(进入执行,`phase:"running"`)/ **`tool_execution.update`**(运行中进度/局部输出,可多帧)/ **`tool_execution.end`**(完成或失败,`phase:"done"|"error"`)替换,载荷改为结构化 `{tool, phase, id?}`(`tool` = 工具名;`phase` = 细粒度执行阶段,与 §3.5 流程图节点 status 同一套 `running/done/error` 枚举;`id` = provider 工具调用 id,可与 `tool_call.*` 帧关联)——**phase 不再塞进 `delta`**;loop 现已真实发出 start/end(update 的 wire 映射已实现并验证,但工具分发器尚无中途进度上报源,**暂无生产者**,见 §3.1)。② **`streamId`(模型流帧 `data` 内新增可选字段)**:同一次模型流(一次 ChatStream 调用)的多帧共享同一 `streamId`,多步工具轮的第二次模型调用取新 id;与帧壳的 `turnId`(整个回合恒定)分工——`turnId` 标记「这轮回答」,`streamId` 标记「这轮里的哪一条流式消息」。③ **`turn.start` / `turn.end`(新帧)**:loop 在回合边界发出、经 ws_bridge 下行;**无 `data` 载荷**(帧壳的 `sessionId`/`turnId`/`seq` 即边界信号;回合终态信息仍由既有 `stream_end` 携带);`turn.end` 覆盖回合的所有出口(最终答案 / 工具循环安全网终止 / 预算耗尽 / 步骤出错),被静音的内部流程(backthink / 子代理)不发出。④ **向后兼容**:`turn.start/end` 与 `streamId` 为纯新增,老客户端可安全忽略;**破坏性**在旧 `tool_execution.update` 单帧——loop 不再产生它,按旧形状(解析 `delta` 里的 `phase:工具名` 拼串)消费的客户端必须迁移到三事件;现有 Web 前端未消费这些帧,行为不变。⑤ 本批新帧全部落在 v0.48 下行帧门的「仅 `owner` + `admin`」档(fail-closed 默认,无需扩白名单)。⑥ **`graph_update` 会话范围口径修正**(裁决 01.4.5,随实现事实补记):`web` / `web:` / `sess-backthink` 三范围(见 §3.5)。见 §3.1 与 v0.50 变更记录。**上一版 `v0.49`**:(**WS 握手回放与上行帧的角色门(design/11 §7.2)**:补上 §7.1 下行门之外的**同一类缺口的另两处**,对 `viewer` **继续收紧**,`owner` / `admin` 行为不变,**无端点、无字段形状变更**——① **握手 `history` 回放改为仅 `owner` + `admin`**:此前任意角色一连上 `/ws/chat` 就会收到主会话最近 50 条消息正文(`thinking` 已剥离,但**正文本身**就是主人的对话),`viewer` 从此**不收**;② **上行 JSON 帧按角色授权(fail-closed)**:非 `owner`/`admin` 连接发来的**任何**上行 JSON 帧一律拒绝,**定向**(只回发送者)回 `{type:"error",data:{code:"forbidden"}}`,**不广播、不驱动模型、不落库、不返回任何内容**——被拒帧含 `chat`(此前 `viewer` 可**真的驱动模型并落库**)、`interrupt`、`subagent_ask` / `task_output_query`(其应答是**定向**帧、能绕过 §7.1 下行门把子代理上下文/任务输出直接回给 `viewer`)、`voice_input`、`mode_switch`,且**未登记的新上行帧默认拒**;**协议级帧(ping/pong、close)不受影响**。③ **合口径**:`viewer` = **可连接、可读 12 条只读监控 REST(v0.40)、可收 7 个状态机信号帧(v0.48)**;**除此以外 WS 上拿不到任何内容、也发不出任何指令**。见 §3 开头的「上行帧与握手回放的角色门」与 v0.49 变更记录。**上一版 `v0.48`**:(**WS 下行帧的角色门(design/11 §7.1)**:hub 的下行现在是**按帧类型分档**的——只有 7 个**状态机信号**(`session_busy` / `pending_ack` / `pending_drain` / `session_idle` / `session_interrupted` / `pending_full` / `session_read`,**均不含任何内容**)继续发给**所有已认证连接**(含 `viewer`),**其余全部下行帧**(流式内容 `message.*` / `thinking.*` / `tool_call.*` / `tool_execution.update` / `stream_chunk` / `stream_end` / `thinking`;持久化 `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`)**改为仅发 `owner` + `admin`**)。**对 `viewer` 是收紧**:此前它连上 `/ws/chat` 就能收到角色回复正文与推理内容,现在收不到;**`owner` / `admin` 行为不变**;**无端点、无字段形状变更**——`viewer` 的 12 条只读监控 REST 端点(`viewerAuth`,v0.40)与 `/ws/chat` 的连接鉴权都**照旧**;**新帧类型默认只发 owner/admin**(fail-closed)。见 §3 开头的「下行帧的角色门」与 v0.48 变更记录。**上一版 `v0.47`**:(**`graph_update` 子代理泳道根节点新增可选 `props.persona`**:派发子代理时若指定了目标角色,该分支根节点的 `props` 就多一个 `persona` 键,标注它**正在扮演的角色**——用途是让流程图能显示「这个子代理是以谁的身份在替班」,而不只是子代理 id;**纯增量**:不带角色派发时该键不出现,老客户端忽略未知键即可,见 §3.5 与 v0.47 变更记录)。**上一版 `v0.46`**:(**`PATCH /api/v1/sessions/{id}` 的 `personaId` 由「可重新绑定该会话的角色」改为「出现即 `400`」**:**会话角色在创建时确定、之后不可变**,要换角色请新开会话(design/22 §8-1)——**明确拒绝**而非静默忽略/200 空操作;**键出现**(任何取值:`"yuki"` / `""` / `null` / 未知 id / 空白串)一律 `400 bad_request` 且消息点名 `personaId`,该检查**先于**会话存在性检查(未知会话 + `personaId` 也回 `400` 而不是 `404`)。**破坏性**:按 v0.42–v0.45 旧文实现、依赖「PATCH 换角色 / 显式 `""` 解绑」的客户端必须改为**新建会话**选角色;`title`/`pinned` 行为与 `POST /sessions {personaId}`(创建时绑定)**完全不变**,不再有 `501/200 {personaId}` 的 PATCH 路径)。上一版 `v0.45`:`PUT /admin/channels/{platform}/config` 的 `personaId` 由「省略/`""` 都 = 解绑」修订为三态(**省略(或 `null`)= 保留已存绑定**、**显式 `""` = 解绑**、**非空 id = 绑定**,未知角色仍 `400`)。 ## 2. REST 端点(管理,`/api/v1/admin/*`) @@ -244,6 +246,15 @@ - `POST /admin/channels/{platform}/message/test?accountId=` body `{targetId,content}` → `200 {data:{msg_id,msg_timestamp}}`。真实向 `targetId`(频道 id)发一条 kmarkdown 测试消息(KOOK `POST /message/create`),验证 bot 可达性。 - `GET /admin/channels` 每条账号附 **`capabilities`**(可选 `[]string`,如 kook 的 `["friends","user_chats","channels","guilds","message_test"]`),供面板按平台渲染专属功能卡片。 - **KOOK「token 即用、其余自动获取」**(适配器契约,非端点):填 KOOK 渠道时**只必填 `token`**(bot_token)。`base_url` 留空即用官方默认 `https://www.kookapp.cn/api/v3`;`receive_mode` 空默认 `ws`(WS 模式地址由 `GET /api/v3/gateway/index?compress=0` 自动获取,已接入);`webhook_path` 空默认 `/webhook/kook`。高级值(改用 webhook、`webhook_verify_token`、`webhook_encrypt_key`,仅 webhook 模式需要)经 PUT 的 `config` JSON 传入(前端走「额外 config」,如 `{"receive_mode":"webhook","webhook_verify_token":"..."}`)。**Connect 拿到 token 后自动调用 `GET /api/v3/user/me`**(Bearer 鉴权)解析 bot 账号(id/username/nickname),存到 adapter 并随 `GET /admin/channels` 账号的 `botId`/`botUsername`/`botNickname` 显示;user/me 失败仅记日志、bot 身份为空,**不中断**连接/收发。token 只在 header / 写文件,**不打印、不进日志**。 +- **OneBot v11 四端契约(v0.55,适配器契约 + 配置面;design/08 §4 / design/13 §0,裁决 1.4 / 12.4.1)**:`qq-obv11` 适配器按 `transport`(ws|http) × `mode`(client|server) 提供**四个端**,谁向谁发数据各不相同: + - **`ws + client`**:Cyrene 主动拨号 OneBot 的 WS 端点,收发同一个长连接;**`ws + server`**:OneBot 反连 Cyrene 的 `listen_addr`,收发同一个长连接(两端均沿用改造前语义)。 + - **`http + client`**:Cyrene 作为 HTTP 客户端调 OneBot 的 HTTP API——`POST {api_base_url}/{action}`、等响应、按 `retcode` 判成败(**纯出站**,无长连接、无监听端口);token 只走 `Authorization: Bearer`。 + - **`http + server`**:OneBot 主动 `POST` 到 Cyrene 的 `listen_addr` + `report_path`(**纯入站**);**fail-closed 鉴权**(配了 `access_token` 时 `Authorization: Bearer` 或 `?access_token=` 必须匹配,否则 `403`)、**10MiB 请求体上限**(超出 `413`)、方法非 `POST` 回 `405`,成功回 `204`。 + - **配置面**(channels.json 账号 `config` + 适配器 `Config()` 快照)新增:`transport`(`ws`|`http`,默认 `ws`,**未知值一律回落 `ws`**)、`api_base_url`、`report_path`(默认 `/`,无前导 `/` 时补上)。快照里 camelCase 为 `transport`/`apiBaseURL`/`reportPath`;`remoteURL` 快照值 = `remote_url`,**为空时回落 `api_base_url`**(语义即「出站远端地址」)。**既有键与值不变**(不给出任何新配置时行为与改造前一致:`transport=ws`、`mode=server`)。 + - **env 兜底**:`CHANNEL_QQ_OBV11_TRANSPORT`(默认 `ws`)/ `CHANNEL_QQ_OBV11_API_BASE_URL` / `CHANNEL_QQ_OBV11_REPORT_PATH`。 + - **关键取舍**:`mode` 决定**哪一半必需**(`server` 必需 `listen_addr`、`client` 必需 `api_base_url`),另一半只要字段给齐就**一并启用** ⇒ 单个适配器即可完成「收上报 + 回消息」闭环;四个端仍各自可独立选择与测试。任一端配置有误即 `Connect` 失败,**绝不留下半开的监听端口**。 + - **端口例外**:`http + server` 会用 `listen_addr`,属 `AGENTS.md` §9 已登记的**同一豁免**(design/13 §0 原文即写「WS server / HTTP server」);`listen_addr` 为空则报错、**绝不开端口**、**无默认值**。 + - **未接**:`get_group_list` 枚举(后续);`access_token` 为空时上报路径**放行**(与既有 `ws + server` 同口径,尚无 fail-closed 开关)。 - `PUT /admin/channels/{platform}/config`(owner-only,审计)body `{id?,token?,baseURL?,remoteURL?,listenAddr?,mode?,accessToken?,selfID?,config?,allowGroups?,denyGroups?,allowUsers?,denyUsers?,groupMsgPerMin?,msgPerMin?,maxMessages?,personaId?}` → `201 {data:{persisted:true,path,platform,accountId}}`。**真实持久化 + 热更新**(design/04 §3.1,无需重启):读当前有效配置(channels.json→env 兜底)→ **按 id 打补丁**(`PatchChannel`)→ 落盘 `channels.json`(本地、gitignore,0600)→ `bridge.Reload(platform)` 热替换该平台 adapter(进行中消息用旧、新消息用新);token 写入文件但**永不在响应/日志回显**(GET 列表为掩码)。`platform` 必填;空配置回 `400`;未注入渠道配置服务回 `500 channels_unavailable`、写盘失败 `500 channel_persist_failed`、热重载失败 `500 channel_reload_failed`。`.env`(`CHANNELS_JSON` / `CHANNEL_*_TOKEN`)为兜底。 - **`config`(v0.54 起为 patch 语义,与 `personaId` 同构)**:`config` 是**只含本次请求显式给出的键**的补丁,**不是**一整套完整配置。**取值三态**:**键省略(该键不在 `config` 里,或 `config` 整体缺席 / 为 `null`)= 保留该账号已存值**(未给出的键**绝不被抹掉**);**给出非空值 = 覆盖该键**;**显式 `null` 或 `""` = 清除该键**(适配器随后回落到自己的零值;`0` / `false` 是真实取值,**不算空**)。顶层便捷字段(`token` / `baseURL` / `remoteURL` / `listenAddr` / `mode` / `accessToken` / `selfID`)是普通 `string`,**只在非空时**并入补丁(普通 string 分不出「缺席」与「显式空串」)⇒ **要清除 `bot_token` 之类的键,必须走 `config` 里的显式 `null` / `""`**。**为什么**:面板只发送用户本次**碰过**的平台字段——若把「键不存在」当成清除,主人打开一张账号卡直接保存就会**静默抹掉**面板不渲染的凭据(**微信(iLink)**的 `bot_token`/`bot_id`/`owner_id`/`get_updates_buf`,iLink token 是长期凭据;**Telegram** 只填 `webhook_url` 会连 token 一起丢、账号被 `cleanAccounts` 整个删掉)。**仍是整值替换(不走三态)**:`allowGroups` / `denyGroups` / `allowUsers` / `denyUsers` 与 `groupMsgPerMin` / `msgPerMin` / `maxMessages`(线上是值语义)——**省略即清空**,客户端须回传**完整列表 / 非零值**。**未改**:token 要求型平台(kook / telegram)上**显式**清 `bot_token` 仍按 `cleanAccounts` 丢掉该账号(这是明确要求清除凭据,不是误删);「本次请求一个 `config` 键都没给出」在**非 `weixin-ilink`** 平台仍回 `400`(`weixin-ilink` 豁免,以便先建账号再扫码)。 - **`personaId`(v0.44 新增,v0.45 起为三态)**:=该账号**答话时的角色 id**(design/22 §2 方案 b,取值来自 `GET /personas`)。**取值三态**:**键省略(或 `null`)= 保留该账号已存绑定**(不动);**显式 `""` = 解绑**(写盘时该账号不带 `personaId` 键);**非空 id = 绑定该角色**。**未知角色 id 回 `400 bad_request`**(不绑一个答不了话的角色;校验只针对**本次真的传了值**的情形,省略继承的已存绑定在写入时已校验过)。**为什么是三态**:面板只在主人**动过**角色下拉时才发这个键——若把「键不存在」也当成解绑,主人只改 token 就会**静默清掉**账号的角色绑定(两端语义相反即丢数据)。**写盘即生效**:保存后 `Reload(platform)` 会重建该平台 adapter,新角色在下一批入站消息上生效(无需重启)。**它不是身份/权限输入**——谁能做什么仍由 channel 层 actor 元数据 + 白名单决定(design/11 §0)。**优先级见 §4.2「角色解析优先级」**:渠道绑定**只**作用于该账号的**主人私聊**,且仅当主会话自身未绑角色;群聊/陌生人会话**强制默认角色**。 @@ -288,8 +299,8 @@ ## 3. WebSocket `/ws/chat`(实时) - **握手**: `ws://:8080/ws/chat?token=` 或 `Authorization: Bearer`;未认证在握手前回 `401` JSON。 -- **下行(服务端→客户端)**: 经 bus 广播 `{type:"", data:..., turnId?, seq?, sessionId?}`(`turnId`/`seq` 为流式关联字段,见 §3.1;`sessionId` 为**会话持久化(design/16)新增**,能确定会话的事件带上,供客户端把该帧路由到**它自己那个会话**的消息桶(**v0.54 起:非激活会话的帧不再被丢弃**);**v0.36 起结构化流帧 `message.*` / `thinking.*` / `tool_call.*` / `tool_execution.*` 也带 `sessionId` + `turnId`**——多会话并发流式时客户端据此把 delta 落到各帧自身 `sessionId` 的消息桶,见 §3.1)——`message.start/delta/end` / `thinking.start/delta/end` / `tool_call.start/delta/end`(以上模型流帧的 `data` 另带 `streamId`,v0.50)/ `tool_execution.start/update/end`(工具执行三事件,v0.50)/ `turn.start` / `turn.end`(回合边界帧,v0.50)(结构化流,design/04 §1.1),以及向后兼容的 `stream_chunk` / `stream_end` / `thinking`(思维链) / `mood`(情绪变化,见 §3.7) / `reminder` / `device_update` / `tool_used`(工具回显) / `subagent_event`(逐代理进度) / **`subagent_result`**(后台批次的最终结果,见 §3.10) / **`approval_notice`**(主代理代审小字,见 §3.11) / `graph_update` / **`message_persisted`**(会话持久化增量,见 §3.3)/ **`message_edited`**、**`message_deleted`**(消息编辑/删除增量,见 §3.8)/ **`session_read`**(已读游标推进,见 §3.9)。**会话编排状态(design/15,均为广播,`data.sessionId` 区分会话)**:`session_busy` / `pending_ack` / `pending_drain` / `session_idle` / `session_interrupted` / `pending_full`(见 §3.2)。语音回复走 `voice_output`(定向回请求连接,非广播)。 -- **下行帧的角色门(v0.48,design/11 §7.1)**: 下行帧按**类型**分两类,由后端**唯一一处**收口判定(**不在白名单里的帧默认只发 `owner` + `admin`**,fail-closed):① **状态机信号**(白名单,**不含任何内容**)→ **所有已认证连接**(含 `viewer`):`session_busy` / `pending_ack` / `pending_drain` / `session_idle` / `session_interrupted` / `pending_full`(§3.2)、`session_read`(§3.9);② **其余全部** → **仅 `owner` + `admin`**:流式内容 `message.*` / `thinking.*` / `tool_call.*` / `tool_execution.start/update/end` / `turn.start` / `turn.end`(**v0.50 新增的这几种同属本档**——工具进度与回合边界对 `viewer` 也是内容相邻信息,fail-closed 默认即只发 owner+admin,未扩白名单)/ `stream_chunk` / `stream_end` / `thinking`(§3.1)、持久化 `message_persisted`(§3.3)/ `channel_persisted`(§3.4)/ `message_edited` / `message_deleted`(§3.8)、系统内部 `graph_update`(§3.5)/ `subagent_event` / **`subagent_result`(§3.10,v0.51 新增的这帧同属本档)** / **`approval_notice`(§3.11,v0.52 新增,同属本档)** / `task_output` / `task_done` / `tool_used` / `capacity_awaken` / `mode_switch`、私有状态 `mood`(§3.7)/ `device_update` / `reminder`。 +- **下行(服务端→客户端)**: 经 bus 广播 `{type:"", data:..., turnId?, seq?, sessionId?}`(`turnId`/`seq` 为流式关联字段,见 §3.1;`sessionId` 为**会话持久化(design/16)新增**,能确定会话的事件带上,供客户端把该帧路由到**它自己那个会话**的消息桶(**v0.54 起:非激活会话的帧不再被丢弃**);**v0.36 起结构化流帧 `message.*` / `thinking.*` / `tool_call.*` / `tool_execution.*` 也带 `sessionId` + `turnId`**——多会话并发流式时客户端据此把 delta 落到各帧自身 `sessionId` 的消息桶,见 §3.1)——`message.start/delta/end` / `thinking.start/delta/end` / `tool_call.start/delta/end`(以上模型流帧的 `data` 另带 `streamId`,v0.50)/ `tool_execution.start/update/end`(工具执行三事件,v0.50)/ `turn.start` / `turn.end`(回合边界帧,v0.50)(结构化流,design/04 §1.1),以及向后兼容的 `stream_chunk` / `stream_end` / `thinking`(思维链) / `mood`(情绪变化,见 §3.7) / `reminder` / `device_update` / `tool_used`(工具回显) / `subagent_event`(逐代理进度) / **`subagent_result`**(后台批次的最终结果,见 §3.10) / **`approval_notice`**(主代理代审小字,见 §3.11) / `graph_update` / **`message_persisted`**(会话持久化增量,见 §3.3)/ **`message_edited`**、**`message_deleted`**(消息编辑/删除增量,见 §3.8)/ **`session_read`**(已读游标推进,见 §3.9)/ **`observability_log`**、**`observability_trace`**、**`observability_metrics`**(可观测实时推送,见 §3.12,v0.55)。**会话编排状态(design/15,均为广播,`data.sessionId` 区分会话)**:`session_busy` / `pending_ack` / `pending_drain` / `session_idle` / `session_interrupted` / `pending_full`(见 §3.2)。语音回复走 `voice_output`(定向回请求连接,非广播)。 +- **下行帧的角色门(v0.48,design/11 §7.1)**: 下行帧按**类型**分两类,由后端**唯一一处**收口判定(**不在白名单里的帧默认只发 `owner` + `admin`**,fail-closed):① **状态机信号**(白名单,**不含任何内容**)→ **所有已认证连接**(含 `viewer`):`session_busy` / `pending_ack` / `pending_drain` / `session_idle` / `session_interrupted` / `pending_full`(§3.2)、`session_read`(§3.9);② **其余全部** → **仅 `owner` + `admin`**:流式内容 `message.*` / `thinking.*` / `tool_call.*` / `tool_execution.start/update/end` / `turn.start` / `turn.end`(**v0.50 新增的这几种同属本档**——工具进度与回合边界对 `viewer` 也是内容相邻信息,fail-closed 默认即只发 owner+admin,未扩白名单)/ `stream_chunk` / `stream_end` / `thinking`(§3.1)、持久化 `message_persisted`(§3.3)/ `channel_persisted`(§3.4)/ `message_edited` / `message_deleted`(§3.8)、系统内部 `graph_update`(§3.5)/ `subagent_event` / **`subagent_result`(§3.10,v0.51 新增的这帧同属本档)** / **`approval_notice`(§3.11,v0.52 新增,同属本档)** / `task_output` / `task_done` / `tool_used` / `capacity_awaken` / `mode_switch`、私有状态 `mood`(§3.7)/ `device_update` / `reminder`、**可观测推送 `observability_log` / `observability_trace` / `observability_metrics`(§3.12,v0.55 新增,同属本档——未进 7 个状态机信号白名单,fail-closed 只发 owner+admin)**。 - **上行帧与握手回放的角色门(v0.49,design/11 §7.2)**: 与上面那条下行门**成对**,收的是同一个缺口的两处残余——① **客户端→服务端(上行)JSON 帧**:只有 `owner` / `admin` 连接可以发;非 `owner`/`admin`(即 `viewer`)连接发来的**任何**上行 JSON 帧都**默认被拒**,回复是**定向**(只回发送者)的 `{type:"error", data:{code:"forbidden"}}`,**不广播、不驱动模型、不落库、不返回任何内容**。被拒帧:`chat` / `interrupt` / `subagent_ask` / `task_output_query` / `voice_input` / `mode_switch`——**未登记的新上行帧同样默认拒**,要放行必须先在 design/11 §7.2 登记白名单(实现上那把「默认拒」也必须同步改为登记表,不能直接删)。② **握手 `history` 回放**:仅 `owner` + `admin` 连接收到;`viewer` 连接建立后**不收**回放。**协议级帧不受影响**:WS ping/pong 与正常 close 照常(否则连接会掉),被拒的上行帧也不会关掉连接。 > **这是「谁被主动推送」的门,不是数据可见性**:`viewer` 的 12 条只读监控端点(`viewerAuth`,v0.40)**照旧**(含 `GET /personas` 的角色级 `mood`);`viewer` **仍可连** `/ws/chat`(连接鉴权不变),只是收不到第 ② 类帧。定向回请求连接的帧(`voice_output` 等)不受影响。**上一条的门只管「推给谁」;「谁能发、谁能拿回放」见上面 v0.49 那条**——`viewer` 连上后**发不出任何指令**、也**收不到 `history` 回放**。 @@ -415,6 +426,26 @@ - **客户端处理**:与流式帧同一会话过滤规则(`sessionId === activeSessionId`),把 `explain` 作为聊天流里的一行小字渲染、`detail` 作副文本(现 Web 前端已按此实现,见 `frontend/web/src/components/status.tsx` 的 `ApprovalNoticeHint`)。 - **约束**:不新增端点、不改既有帧;老客户端按未知帧忽略即可。 +### 3.12 可观测实时推送(`observability_log` / `observability_trace` / `observability_metrics`,v0.55) + +> **角色门**:三个帧**都不在** `frameReachesAll` 白名单 ⇒ fail-closed **只发 `owner` + `admin`**;`viewer` 收不到,继续走既有 12 条只读监控 REST 档(v0.40)。未进 7 个状态机信号白名单(要进须先在 design/11 §7.1 登记「不含内容」的理由——它们是**运行时数据面**,不属于状态信号)。 +- **取代关系**:可观测面(日志 / 指标 / trace)从「面板 1s 轮询 REST」改为 **WS 帧推送**(裁决 05.4.3 + 16.4.3-WS,design/10 §5)。**既有 REST 端点完全不变**(路径 / RBAC / `level` / `limit` 语义 / 响应形状一字未动),仍然可用,并在被读取时兼任「订阅握手」。 +- **订阅(推送开启条件)**:owner/admin 凭据**读取** `GET /api/v1/admin/observability/{logs,metrics,trace}` **任一个**即建立订阅(绑定**该次登录凭据**,键 = sub+role+iat)。该凭据的 WS 连接**全部断开**即退订;订阅表空后推送循环自行退出(**无常驻 goroutine、无周期性开销**)。**这不是轮询**:那次读取本身照常返回完整引导数据,此后吃推送。 +- **节拍**:≤1s 一拍,**无新数据不发帧**(空转时一帧都不发)。 +- **三个下行新帧**(**纯新增**,既有帧名/形状零改动;帧壳 `{type,data}`,**不带** `sessionId` / `turnId` / `seq`——它们不属任何会话): + +| 帧名 | 载荷 `data` | 上限 | 触发 | +|---|---|---|---| +| `observability_log` | `{entries:[{time,level,source,msg}…]}`(**最新在前**,与 `GET /admin/observability/logs` 同形) | 200 条 | 订阅后**新增**日志;一拍最多一帧,无新条目不发 | +| `observability_trace` | `{spans:[…]}`(**最新在前**,与 `GET /admin/observability/trace` 同形) | 100 条 | 订阅后**新完成**的 span | +| `observability_metrics` | `AdminMetrics` **本体(扁平)**,与 REST 同形:`totalRequests,qps,avgLatencyMs,maxLatencyMs,perMethod,perPath,statusCounts,llmCalls,subagentEvents` | — | 与**上次发出的**不同才发(序列化指纹比较,键排序确定) | + +- **初始锚点**:订阅建立时以「当前最新」为锚(日志取最新条目时间、trace 取当前写入序号、指标取当前快照指纹),因此循环**不会**把环形缓冲里的历史整段灌成巨型帧——历史由面板那次引导读取提供。 +- **单帧上限的语义**:一拍的突发超过上限时只发**最新**的那些(更旧的条目本就落在面板保留窗口之外,与 REST 尾部 `limit` 上限同口径),不丢「看得见」的数据。 +- **分级过滤改在客户端本地做**:这三个帧经 bus 广播给**所有 `owner`/`admin` 连接**(hub 无按连接订阅、也无按连接分级——订阅只决定「推送循环跑不跑」),因此 `?level=` 那种服务端过滤不适用于推送;面板按本地等级选择过滤即可。 +- **前端落点**:`frontend/web/src/lib/ws.ts` 的 `subscribeObservabilityFrames()`(帧**不进应用 store**,只有打开着面板的视图在听,其余时候安静丢弃);面板挂载时各读一次对应 REST 作为引导(logs 用 `limit=200`),推送帧**按业务键去重合并、倒序、窗口 200**(`frontend/web/src/admin/modules/observabilityStream.ts`)。 +- **向后兼容**:纯新增帧,老客户端按未知帧忽略即可;既有 REST 与其它帧/端点/鉴权一律不变。 + ## 4. 客户端接入指引(Web / 移动 / 原生 / IM bridge 通用) ### 4.1 一次性接入流程(三步走) @@ -459,6 +490,8 @@ - 语音:上行 `voice_input`(base64 音频或已转写 text),下行 `voice_output`(合成音频 base64 + text)。 ## 5. 变更记录 +- `v0.55`(**阶段 7 落地项登记:路径式安全入口 + 数据目录 `platform` + OneBot v11 四端 + 可观测实时推送**,依据裁决 1.6 / 16.4.7 / 12.4.2(commit `a398dc9`)、1.4 / 12.4.1(commit `9253fef`)、05.4.3 + 16.4.3-WS(commit `0e1f163`),以及 13.4.5 面内三条(commit `74b12d8`)、MCP 官方 SDK(commit `a1083a3`),2026-10-04 落实;**本版全部是已实现并已推库行为的登记,无新代码**)。**A. 安全入口(`ADMIN_PATH`,裁决 1.6 / 16.4.7,**前端路由面——不是 REST 契约**)**——① **新增 env `ADMIN_PATH`**(默认 `admin`):面板前端路由前缀 = `/` + 值(`admin` → `/admin`;`xxx/admin` → `/xxx/admin`);**非法值回退 `/admin`**——容忍首尾斜杠与空白,拒绝空段、`.`/`..` 段与含 `?`/`#` 的段(宁可挂回默认,也不挂到不可预测的路径上)。② **HTTP 行为**:`/` 与其深链(`/<前缀>/
`)返回**注入了 `` 与 `window.__CYRENE_ADMIN_ENTRY__={"prefix":"..."}` 的 index.html**(`Cache-Control: no-store`;`` 必在资源引用之前,否则多段前缀/深链下 Vite 的 `./assets/...` 会解析到错误目录);**形如面板入口但前缀不符的路径 → `404`**(判定 = 路径里出现 `admin` 段;既不回落用户端聊天壳、也不回显正确前缀);`/` 若**恰好命中 `dist` 真实文件**则仍走文件服务器(面板入口页绝不顶掉 SPA 资源)。③ **API 不藏**:`/api/v1/*`、`/api/v1/admin/*`、`/ws/chat`、webhook 一律原样——**无新端点、无帧变更**,安全靠 JWT + 角色门(design/07 §0.1)。④ **口径**:**用户端 HTML 永不携带该前缀串**(后端只在命中前缀时注入)⇒ 用户端既无管理入口链接、也解析不出面板路由(design/07 §0.2)。前端路由随之改**路径式**(`frontend/web/src/admin/router.ts` 只读注入、**无硬编兜底**、移除 hash 入口;模块切换走 history)。**B. 数据目录(同 commit,裁决 12.4.2)**——`DATA_DIR` 新增可接受值 **`platform`**(大小写不敏感)→ 平台标准数据目录(Windows `%APPDATA%\Cyrene`、macOS `~/Library/Application Support/Cyrene`、Linux `~/.local/share/cyrene`);**默认 `./data` 与显式路径行为不变**;**显式空值行为不变**(仍走组合根原兜底,避免正在运行的实例突然换库)。**C. OneBot v11 四端(`9253fef`,裁决 1.4 / 12.4.1)**——① **配置面**(channels.json 账号 `config` + 适配器 `Config()` 快照)新增:`transport`(`ws`|`http`,默认 `ws`;**未知值一律回落 `ws`**,绝不静默改走 HTTP)、`api_base_url`(OneBot HTTP API 基址,必须显式给出——**不从 `remoteURL` 推导**,v1 那种「剥掉最后一段路径」的推导是有损的)、`report_path`(默认 `/`,无前导 `/` 时补齐);快照里 camelCase 为 `transport` / `apiBaseURL` / `reportPath`;`remoteURL` 快照值 = `remote_url`、为空时回落 `api_base_url`(语义即「**出站远端地址**」)。**既有键与值不变**——不给出任何新配置时行为与改造前逐字节一致(`transport=ws`、`mode=server`)。② **env 兜底**:`CHANNEL_QQ_OBV11_TRANSPORT`(默认 `ws`)/ `CHANNEL_QQ_OBV11_API_BASE_URL` / `CHANNEL_QQ_OBV11_REPORT_PATH`。③ **四端语义(谁向谁发数据)**:`ws+client` Cyrene 拨号 OneBot、收发同一 WS;`ws+server` OneBot 反连 Cyrene、收发同一 WS;`http+client` Cyrene→OneBot(`POST {api_base_url}/{action}`、等响应、按 `retcode` 判成败,**纯出站**、无长连接、token 只走 `Authorization: Bearer`);`http+server` OneBot→Cyrene(POST 到 `listen_addr` + `report_path`,**纯入站**;**fail-closed 鉴权**——配了 `access_token` 时 `Authorization: Bearer` 或 `?access_token=` 不匹配回 `403`,`access_token` 为空则放行,与既有 `ws+server` 同口径)、**10MiB 请求体上限**(超出 `413`)、方法非 `POST` 回 `405`、成功回 `204`。④ **关键取舍**:`mode` 决定**哪一半必需**(server 必需 `listen_addr`、client 必需 `api_base_url`),另一半只要给齐字段就**一并启用** ⇒ **单个适配器即可完成「收上报 + 回消息」闭环**,四端仍各自可独立选择与测试;任一端配置有误即 `Connect` 失败(先做纯配置校验并构造出站客户端,再绑监听端口),**绝不留下半开的端口**。⑤ **端口例外**:`http+server` 会用 `listen_addr`,属 `AGENTS.md` §9 已登记的**同一豁免**(design/13 §0 原文即写「WS server / HTTP server」);`listen_addr` 为空则报错、**绝不开端口**(无默认值)。⑥ **未接**:`get_group_list` 枚举(后续)。**D. 可观测实时推送(`0e1f163`,裁决 05.4.3 + 16.4.3-WS)——新增 §3.12**:日志 / 指标 / trace 从「面板 1s 轮询 REST」改为 **WS 帧推送**,**三个下行新帧**(**纯新增**,既有帧名/形状零改动):`observability_log`(`{entries:[{time,level,source,msg}…]}`,**最新在前**、与 `GET /admin/observability/logs` 同形,单帧上限 **200 条**,触发 = 订阅后**新增**日志)、`observability_trace`(`{spans:[…]}`,与 `GET /admin/observability/trace` 同形,上限 **100 条**,触发 = 订阅后**新完成**的 span)、`observability_metrics`(`AdminMetrics` **本体、扁平**,与 REST 同形:`totalRequests,qps,avgLatencyMs,maxLatencyMs,perMethod,perPath,statusCounts,llmCalls,subagentEvents`,**与上次发出的不同才发**)。**帧壳 `{type,data}` 不带** `sessionId`/`turnId`/`seq`(不属任何会话)。**角色门**:三者**都不在** `frameReachesAll` 白名单 ⇒ fail-closed **只发 `owner` + `admin`**(`viewer` 收不到、继续走既有 12 条只读监控 REST 档)。**订阅(推送开启条件)**:owner/admin 凭据**读取** `GET /admin/observability/{logs,metrics,trace}` **任一个**即建立订阅(绑定**该次登录凭据**,键 = sub+role+iat,不把 token 串留在内存);该凭据的 WS 连接**全部断开**即退订,订阅表空后推送循环自行退出(**无常驻 goroutine**)。**这不是轮询**:读取本身照常返回完整引导数据,此后吃推送;循环启动时以「当前最新」为锚(日志最新条目时间 / trace 写入序号 / 指标快照指纹),因此**不会**把环形缓冲里的历史整段灌成巨型帧。≤1s 一拍、**无新数据不发帧**(空转一帧都不发)。**既有 REST 端点完全不变**(路径 / RBAC / `level` / `limit` 语义 / 响应形状一字未动);`?level=` 那种服务端分级过滤不适用于推送(帧广播给所有 `owner`/`admin` 连接——订阅只决定推送循环跑不跑,不决定谁收帧),改由**客户端本地**做。**前端落点**:`frontend/web/src/lib/ws.ts` 的 `subscribeObservabilityFrames()`(帧不进应用 store,只有打开着面板的视图在听);面板挂载时各读一次对应 REST 作引导(logs `limit=200`),推送帧按业务键**去重合并、倒序、窗口 200**(`admin/modules/observabilityStream.ts` + `observabilityViews.tsx`)。**E. 其余**——① **13.4.5 面内三条**(`74b12d8`):(a) 续接窗口 `Recent` 在同毫秒并列时改为取**最新** n 条且按插入序(Windows `time.Now()` 步长约 0.44ms,连发必然同毫秒并列;修复前并列取**更早**的 ⇒ 丢掉刚发出的最新 n 条且同毫秒内反插入序)——**无 API 变化**,但客户端可观察到的「握手回放 / 续接上下文」顺序更正确;(b) 上行 `chat` 不再经**可丢弃**的 bus,改为直达会话编排器的非阻塞 `Submit`(design/15 §2.1「会话消息不能被 drop」+ §4「不阻塞是硬约束」;编排器未注入时回退 bus)——**帧序列不变**;(c) 被**取消**(中断)的回合把该会话 checkpoint 落定为不可恢复(`context.WithoutCancel` 写,快照保留可查询),**不再顶掉下一条消息的上下文**,新增内部常量 **`loop.StopReasonCanceled`**(`checkpoints.state.stop_reason` 可能出现该值,**不经 REST/WS 暴露**)。② **MCP**(`a1083a3`):协议层换成**官方 Model Context Protocol Go SDK v1.8.0**(+ `jsonschema-go`),删自研 rpc/stdio/http/sse,改用 SDK 的传输 / 协议版本协商与 `-32022` 回退 / keepalive / `tools/list_changed` 订阅 / 能力协商——**REST/WS/env/数据目录零变更**(`MCP_SERVERS_JSON`、`MCP_*`、`transport` 枚举 `stdio|http|streamable|sse` 全不变;`POST /admin/plugins/install` 请求/响应形状不变)。**实现落点**:A = `internal/config/config.go`(`AdminPathPrefix` + `resolveConfiguredDataDir`)+ `internal/services/gateway/routes.go`(`adminEntryHatch`/`adminEntryHTML`)+ `frontend/web/src/admin/router.ts`;B = `internal/config/config.go` + `internal/platform`;C = `plugins/channels/qq_obv11/{adapter,http_client,http_server,session,ws_client,ws_server}.go` + `cmd/cyrene/channels.go`;D = `internal/services/gateway/observability_push.go` + `ws_bridge.go` + `frontend/web/src/admin/modules/observabilityStream.ts`。**未做(诚实边界)**:DashScope 形状的 `auto` 推断(须显式声明 `reasoning.kind=dashscope`)、Qwen 的 `/think` 前导消息软开关、`get_group_list` 枚举、**上游显式退订帧**(当前退订只发生在订阅凭据的连接全断时;WS 上行暂无 unsubscribe 帧)、**审计 tab 仍 5s REST 轮询**(未做 `observability_audit` 帧)。**向后兼容**:A/B 是配置面新增(默认值即现状)、C 全是**纯新增配置键**(默认 `ws+server` 行为不变)、D 是**纯新增帧**——老客户端按未知帧/未知键忽略即可;既有端点 / 帧 / 字段形状**无破坏性变更**。 + - `v0.54`(**契约债一次补齐:渠道配置写路径改 patch 语义(修 P0 级凭据静默丢失) + 阶段 6 前端批登记 + 三处过时陈述修正**,依据 design/04 §3.1、design/22 §2 方案 b 与裁决 16.4.1 / 16.4.2 / 16.4.3 / 16.4.4 / 16.4.5 / 16.4.6;commit `4ae7148` / `f454273` / `b480b04` / `c1a98d7` / `8d70f6d`,2026-10-04 落实)。**A. 渠道配置写路径由「整条替换」改为「按 id 打补丁」(P0 级数据安全,破坏性语义变更)**——① **语义变更**:`PUT /api/v1/admin/channels/{platform}/config` 的 `config` 从「整条替换该账号配置」改为「**按 id 打补丁**」(`runtimeconfig.PatchChannel`,**与 `personaId` 同构的三态**):**键省略(该键不在 `config` 里,或 `config` 整体缺席 / 为 `null`)= 保留该账号已存值**、**给出非空值 = 覆盖该键**、**显式 `null` 或 `""` = 清除该键**(适配器回落零值;`0` / `false` 是真实取值,**不算空**)。**破坏性提示**:按旧文实现、依赖「省略某 `config` 键 = 删掉它」的客户端,**必须改为显式传 `null` / `""`** 才能清除该键。② **修掉的数据安全问题**:面板只发送用户本次**碰过**的平台字段,后端却整条替换 ⇒ ⓐ 保存**微信(iLink)**账号卡会抹掉 `bot_token` / `bot_id` / `owner_id` / `get_updates_buf`(**iLink token 是长期凭据**,被抹掉即掉线且不可恢复);ⓑ **Telegram** 只填 `webhook_url` 而不重填 token ⇒ `bot_token` 变空 ⇒ `cleanAccounts` **丢掉整个账号**。两条在修复前都是「面板保存成功、盘上凭据没了」的静默丢失。③ **保留清除能力**:显式 `null` / `""` 仍能真正删除该键,未触碰的兄弟键不受影响。④ **未改**:token 要求型平台(kook / telegram)上**显式**清 `bot_token` 仍按 `cleanAccounts` 丢掉该账号(明确要求清除凭据,不是误删);「本次请求一个 `config` 键都没给出」在**非 `weixin-ilink`** 平台仍回 `400`(`weixin-ilink` 豁免以便先建账号再扫码)。⑤ **无新端点 / 新字段 / 响应形状变更**:请求体与 `201 {data:{persisted,path,platform,accountId}}` 一字未动,只有 `config` 的解释规则变了;`allowGroups` / `denyGroups` / `allowUsers` / `denyUsers` 与 `groupMsgPerMin` / `msgPerMin` / `maxMessages` **仍是整值替换**(省略 = 清空,客户端须回传完整列表 / 非零值)。⑥ **实现落点**:`internal/config/runtimeconfig/channels.go` 新增 `PatchChannel` / `mergeChannelConfig` / `isEmptyConfigValue`;`internal/services/gateway/admin_write_channel_config.go` 的 `adminSaveChannelConfig` 改调 `PatchChannel`(`channelConfigFromRequest` 返回**补丁**而非完整配置)。**验证**:`go test ./internal/services/gateway/ -run TestAdminSaveChannelConfigKeeps -v`(微信 iLink 凭据保留 / 空补丁三形态 / Telegram 账号存活 / 显式清除 / 无基线新建)+ `go test ./internal/config/... -run TestPatchChannel -v`;**变异验证 3 条**——把写路径改回**整条替换**(旧 upsert 语义)后,「省略键保留」「Telegram 账号存活」「空补丁不改凭据」三类断言**精确复现** `bot_token` / `get_updates_buf` 被抹掉与账号被 `cleanAccounts` 删掉的旧缺陷 ⇒ 变异必红。**B. 阶段 6 前端批(纯前端行为,无 API 变化)**——① **16.4.1 静默 / 活动态与预约切换**(`f454273`,帧缺口补 `b480b04`):输入模式切换的唯一入口 `frontend/web/src/components/modeSwitch.ts`——**她活动**(`sessionBusy || 屏上 streaming || sending`)时用户切换只登记「**已预约**」(不改当前模式、不上行、不清除既有预约),**她静默**时立即生效并上行 `mode_switch`;提前切 → **下次静默态落地**(徽章「已预约 · X(她静默后生效)」);预约状态**纯前端**、不进后端契约。② **16.4.5 会话级消息缓存**(`b480b04`):**帧按自己的 `sessionId` 落进该会话的消息桶**(新增 `frontend/web/src/store/sessionCache.ts` 纯函数 + `useAppStore.sessionMessages`),**切会话「存回 / 取出」不再清空**、非激活会话的帧**不再被丢弃**;**只有「单槽忙态」**(`session_busy` / `session_idle` / `session_interrupted` / `stream_end` 的 `sessionBusy` 收口)仍按激活会话判定(见 §3.1 / §3.2)。③ **16.4.2 导图口径**(`b480b04`):`graph_update` 按 `data.sessionId` 入**图桶**(上限 12 张)——管理端 Flow / Agents 页取**激活会话**的图、**Backthink 页取 `sess-backthink`** 的图,后台思考图不再顶掉「当前会话」图(见 §3.5)。④ **16.4.3 / 16.4.6 虚拟滚动与专属页只读卡**(`c1a98d7`):管理端长列表超过阈值走**窗口化**(`AdminTable` / 新增可复用 `VirtualList` / 适配器消息日志,DOM 行数与数据量解耦);专属页**只读展示该平台完整配置字段**(`AdapterConfigSummary`,此前专属页一个配置字段都不显示)。纯管理端 UI。⑤ **16.4.4 双档 UI 模式**(`8d70f6d`):**性能档 = 现状**(默认,Smooth Corner 走 `border-radius` 近似 + 单向高光)、**华丽档**对齐 Miuix 六大核心(连续曲率 k=0.55 + 完整 Liquid Glass)——**华丽档落 `:root.ui-mode-fancy`**(性能档 `ui-mode-performance`,另有 `data-ui-mode` 属性),档位**持久化键 `cyrene.ui-mode`**(localStorage + 跨标签页同步),`main.tsx` 在 `createRoot` 前 `initUiMode()` 保证不闪一帧。**纯前端行为、无 API 变化**(裁决 Q12),对应 **`design/07 §0.1`「双档呈现(裁决 16.4.4)」**。**C. 过时陈述修正(随实现事实改文)**——① §3.1 / §3.3 / §3.8 / §4.2 删去「**非激活会话的帧被忽略 / 仅当会话为激活态才处理或渲染**」的旧口径,改为「帧按其 `sessionId` 路由到该会话的消息桶」(16.4.5 的实现事实,旧文与实现相反);② §3.1 / §3.2 补「**单槽忙态**仍按激活会话判定」;③ §3.5 补 `graph_update` 的**按会话入桶 + 管理端按页取图**口径(16.4.2);`Docs/api-reference.md` 的对应处(§5.24 / §6.2 帧壳)同步。 - `v0.53`(**阶段 5 落地:模型目录端点 + per-persona 路由 / 本地三级 / capacity L3 / 视觉·OCR 独立模型 / `image-crop` 工具 / 按 provider 形状下发推理开关**,依据 design/04 §3/§3.1/§3.2(裁决 03.4.1 / 03.4.2 / 03.4.3 / 08.4.1)、design/06 §0/§3(裁决 14.4.1 / 14.4.5)、design/14 §2.2/§3/§4(裁决 14.4.2 / 14.4.3 / 14.4.4),2026-10-04 落实):① **新增管理只读端点 `GET /api/v1/admin/llm/catalog`(owner|admin)**——面板「添加提供商 / 添加模型」的候选目录(baseURL / 模型 id / 能力位 / 输出上限),数据源 `https://models.dev/api.json`;`?refresh=1` 强制在线刷新,否则 24h TTL 内的本地快照直接复用。`200 {data:{source:"cache"|"models.dev", fetchedAt?, stale?, warning?, providers:[{id,name,api?,doc?,env?,npm?,models:[{id,name,attachment,reasoning,toolCall,contextLimit?,outputLimit?}]}]}}`;**既无快照又抓不到** → `503 {error:{code:"catalog_unavailable"}}`;**有快照但刷新失败** → 仍 `200` + `stale:true` + `warning`(离线兜底,不编造候选);带 `Accept-Encoding: gzip` 时响应带 `Content-Encoding: gzip`(信封形状不变)。**副作用只有一处**:落盘快照 `/llm_catalog.json`(**无凭据**、`data/` 已 gitignore、TTL 24h),**不改 `models.json`**;**抓取只发生在本端点内,不进运行时热路径**。审计动作 `llm.catalog`(`ok`/`stale`/`error`)。详见 §2「模型配置契约(v0.53)」。② **`models.json` 组级新增三键**(可热更新,`GET`/`POST /api/v1/admin/llm` 已同步读写;**未声明时该键不出现**):`reasoning: {kind}` 声明该组的推理开关**形状**;`personaModels: {personaId: modelId}` 与 `personaProviders: {personaId: "openai"|"anthropic"}` 是**角色级模型/协议路由**(「角色」= 该回合答话的 persona,不是消息作者 role)。**校验在 `Group.Validate`**:非法 `kind`(只认 `auto`/`deepseek`/`openai`/`vllm`/`dashscope`)、anthropic 组上声明 openai 系形状、`personaProviders` 的非法协议名、空 persona id/空值 → **写端点回 `400 bad_request` 且不落盘**;persona 表键值会 trim,**trim 后撞键直接报错**(不静默丢条目)。**请求语义**:三键缺省 = 保留已存值(`reasoning` 整块给出即整块替换,persona 表给出即**整表替换**——否则面板无法删除一条已存路由);**响应语义**:每组的 `reasoning` **恒下发**(`kind:""` = 未声明形状,不是「后端没报告」),`personaModels`/`personaProviders` 为空则省略;顶层新增 **`reasoningKinds`** 与 **`personaProviderOptions`** 两个取值域数组(由后端常量直接生成,面板无需硬编码枚举)。**回落**:未绑定/未知 persona → 该组默认模型;`personaProviders` 声明的协议在 `models.json` 里没有对应组 → 记 WARN 并跳过(该角色仍走默认链,**不半建一条只会失败的链**)。③ **语义修正(重要,此前文档写反了)**:**`multimodal` = 主模型自身能否收图**(不是「本部署有没有视觉能力」);`LLM_VISION_MODEL` / `LLM_OCR_MODEL` **只配视觉/OCR 工具用的模型**(OCR 空则复用视觉模型,两者都空则复用主模型),与 `multimodal` **互不代表**(裁决 14.4.1)。④ **`.env` 新增 9 键**:`LLM_REASONING_KIND`(默认空 = `auto`;**只在无有效 `models.json`(走 env 合成组)时生效**,有 `models.json` 时以该组 `reasoning.kind` 为准)、`LLM_OCR_MODEL`、`LLM_LOCAL_ENABLED`(默认 `false`)/`LLM_LOCAL_PROVIDER`(默认 `openai`)/`LLM_LOCAL_BASE_URL`/`LLM_LOCAL_API_KEY`/`LLM_LOCAL_MODEL`(本地第三级:`Enabled` + `BaseURL` + `Model` 三者齐备才成为链尾一步,只开开关则 WARN 并跳过)、`CAPACITY_L3_ENABLED`(默认 `false`)/`CAPACITY_L3_CACHE_TTL`(默认 `5m`)。⑤ **内置工具新增 `image-crop`**:按区域裁切图片(`image` + `x`/`y`/`width`/`height`,0 基像素、区域须完整落在图内)→ PNG data URI + 裁切尺寸 + 源图尺寸;供**大图被上游压缩后分区精读**(design/06 §3)。上限:源图 ≤50MiB、区域 ≤16MP、载荷 ≤8MiB(超出**报错**,不截断);源可为本地路径(file 工具同一沙箱)/ http(s) URL(同 SSRF 守卫)/ base64 data URI。**策略默认 owner-only**(`tool_policies` 无行时 fail-closed)。⑥ **推理开关按 provider 形状下发(替换 v0.41 的「只 WARN 不发」)**——形状由 `reasoning.kind` 声明(未声明 = `auto`:按模型名/baseURL 推断厂商,**形状有歧义就不下发**),字段名映射表: @@ -523,6 +556,6 @@ - `v0.6`(可观测性):新增 `GET /api/v1/admin/observability/{metrics,trace,audit}`(owner/admin)——metrics 返回网关实时请求计数/QPS/延迟/方法·路径·状态分布 + LLM/子代理 span 计数;trace 返回 gateway 环形缓冲里的最近 span(经 TraceSink 对接 runtime tracer,映射为 `AdminTraceSpan`);audit 返回持久化审计(SQLite `audit_logs`,未注入时退化为内存)。`llmCalls`/`subagentEvents` 由实际观测 span 累计;Token/成本后端不统计、不返回(不伪造指标)。 - `v0.5`(批次B·视觉真推理 + 语音端):`POST /api/v1/chat` 增 `image` 字段(base64 data: URI 或 URL),入 loop 视觉路由(vision/OCR 工具或主模型多模态,design/06 §0「先看图再对话」);WS 上行 `voice_input` 接 STT→聊天→TTS 管道,下行新增 `voice_output`(定向回请求连接,含合成音频 + 识别文本 + 回复文本)。 - `v0.4`(结构化流):WS 下行新增 `message.*` / `thinking.*` / `tool_call.*` / `tool_execution.update` 结构化流事件(event shell 补 `turnId`/`seq`,`data` 类型化),对齐 OpenClaw AssistantMessageEvent;保留 `stream_chunk`/`stream_end` 向后兼容。 -- `v0.3`(M10):管理面板 `/admin` 落地——按 §2 契约封装 admin 只读端点(personas/sessions/memories/plugins/channels/iot)+ owner-only `plugins/policy` 写;其余 M10 端点以契约路径封装、后端未实现时前端显式"待实现"壳。 +- `v0.3`(M10):管理面板 `/admin` 落地——按 §2 契约封装 admin 只读端点(personas/sessions/memories/plugins/channels/iot)+ owner-only `plugins/policy` 写;其余 M10 端点以契约路径封装、后端未实现时前端显式"待实现"壳。(**注**:面板**前端**入口自 v0.55 起为**路径式安全入口**,前缀由 `ADMIN_PATH` 决定、默认即 `/admin`;本节其余关于面板的叙述都不含前端路由前缀语义。) - `v0.2`(M8):gateway 落地——登录/health/chat/sessions/personas + admin 只读端点 + `plugins/policy` 写 + WS `/ws/chat`(JWT + bus 广播)。 - `v0.1`(契约先行):骨架 + 端点登记表。 diff --git a/Docs/api-reference.md b/Docs/api-reference.md index 79026a5..27e9246 100644 --- a/Docs/api-reference.md +++ b/Docs/api-reference.md @@ -1,6 +1,6 @@ # Cyrene v2 后端 API 参考(api-reference.md) -> **权威归属(先读这条)**:**契约以 `Docs/api-client.md` 为准**——那是版本化的跨平台客户端契约,每个端点的形状 / 语义 / 版本变更都记在那里(当前 `v0.53`)。**本文件是内部与运维参考**:端点全集索引 + 示例 + 部署细节。**凡是与 api-client 重复的契约性描述,一律以 api-client 为准(本文件不再维护第二份契约);发现两者不一致时,改本文件而不是改 api-client。** +> **权威归属(先读这条)**:**契约以 `Docs/api-client.md` 为准**——那是版本化的跨平台客户端契约,每个端点的形状 / 语义 / 版本变更都记在那里(当前 `v0.55`)。**本文件是内部与运维参考**:端点全集索引 + 示例 + 部署细节。**凡是与 api-client 重复的契约性描述,一律以 api-client 为准(本文件不再维护第二份契约);发现两者不一致时,改本文件而不是改 api-client。** > **本文件的正确用法**:查「有哪些端点、大致形状、错误码、部署注意事项」可以;**写客户端代码时请以 `Docs/api-client.md` 为准**,本文件的字段表 / 示例可能滞后。 > **历史包袱(如实)**:本文件长期自称「全部后端 API 的唯一详细参考 / 超详细主文档 / 每变更必同步」,但实际**系统性滞后于实现与 api-client**(2026-10-03 的 16 路 design-vs-code 审计结论)。本次已修正与实现**直接矛盾**的陈述、并把若干复述型段落改为引用 api-client;**仍未登记的端点 / WS 事件清单见 `DevDocs/audit/00-总览.md` §4④**,属后续「文档补全」批次,本文件暂不补全。 > **其余仍成立**:所有字段名、取值、鉴权规则、状态码最终**以代码为准**(`internal/services/gateway/*.go`);契约变更版本化(REST 走 `/api/v1`,破坏性变更升 v2),不得破坏既有客户端(版本化承诺本身记在 api-client §5)。 @@ -18,6 +18,9 @@ - **CORS**:`Access-Control-Allow-Origin` 为配置的 `ALLOWED_ORIGINS` 白名单(`*` 时放开);`Allow-Methods: GET, POST, PUT, DELETE, OPTIONS`;`Allow-Headers: Authorization, Content-Type, X-API-Version`;`Access-Control-Expose-Headers: X-Voice-Provider`(跨域浏览器可读语音降级信号头);`Max-Age: 600`。`OPTIONS` 预检直接回 `204`。同源/无 Origin 请求放行。**注意:`X-API-Version` 仅作**advisory**惯例头(见 §0.2)——CORS 已放行但它**不参与服务端版本协商**,服务端当前不校验、不据此改变行为。** - **请求日志**:每个请求由 `logging` 中间件记录一行(method/path/status/duration_ms),并计入可观测性指标。panic 由 `recover` 中间件兜底为 `500 {error:{code:internal,...}}`,不崩溃进程。 - **超时**:WS 连接 ping 周期 54s、pong 等待 60s、写超时 10s;读上限 4096 字节/帧。 +- **数据目录 `DATA_DIR`(v0.55)**:默认 `./data`;**新增保留字哨兵 `platform`(大小写不敏感)** → 平台标准数据目录(Windows `%APPDATA%\Cyrene`、macOS `~/Library/Application Support/Cyrene`、Linux `~/.local/share/cyrene`,`internal/platform.DefaultDataDir()`)。显式路径与**显式空值**行为均不变(空值仍走组合根原兜底)。 +- **管理面板安全入口 `ADMIN_PATH`(v0.55,env 默认 `admin`)**:面板**前端**挂在 `/`(可配成 `xxx/admin`;**非法值回退 `/admin`**,见 `config.ServerConfig.AdminPathPrefix()`)。命中前缀(含深链)→ 返回**注入了 `` 与 `window.__CYRENE_ADMIN_ENTRY__={"prefix":"..."}` 的 `index.html`**(`Cache-Control: no-store`);**形如面板入口但前缀不符的路径 → `404`**(不回落用户端聊天壳);前缀恰好命中 dist 真实文件则仍走文件服务器。**REST/WS 路径不随它变化**——`/api/v1/*`、`/api/v1/admin/*`、`/ws/chat`、webhook 一律原样(**API 不藏**,安全靠 JWT + 角色门,design/07 §0.1);该前缀串**只注入面板入口页**,用户端 bundle 不含它(design/07 §0.2)。 +- **额外监听端口的唯一豁免(v0.55 重述)**:`qq-obv11` 渠道的 **OneBot 反连 / HTTP 上报**模式需要独立 `net.Listen` 端口(`listen_addr`),这是 `AGENTS.md` §9 登记的例外(design/13 §0 原文即写「WS server / HTTP server」);**必须由配置显式给出**,为空则报错、**绝不默认开启**。其余渠道/模式一律不得新开监听端口。 ### 0.1 鉴权模型(RBAC) @@ -570,6 +573,7 @@ curl -s 'http://:8080/api/v1/admin/observability/audit?limit=100' -H "Auth ### 4.16 GET /api/v1/admin/observability/metrics - **Query**:无(不接收 limit)。 +- **副作用(v0.55)**:owner/admin 凭据**读取本端点即建立可观测推送订阅**(`observability_metrics` 帧此后由 WS 推来,面板不再轮询;`observability_push.go`)——该凭据的 WS 连接全断即退订。同理见 §4.17 与 `GET /api/v1/admin/observability/logs`(三者的读取都是订阅握手)。响应形状**一字未变**。 - **响应**:`200 {"data":}`。`AdminMetrics` = `{totalRequests, qps, avgLatencyMs, maxLatencyMs, perMethod{}, perPath{}, statusCounts{}, llmCalls, subagentEvents}`。 - `qps`:近 1 秒窗口计数;`avgLatencyMs/maxLatencyMs`:均值/最大耗时(毫秒)。 - `perMethod/perPath/statusCounts`:分布计数。 @@ -582,6 +586,7 @@ curl -s http://:8080/api/v1/admin/observability/metrics -H "Authorization: ### 4.17 GET /api/v1/admin/observability/trace - **Query**:`?limit=N`(默认 50,上限 500)。 +- **副作用(v0.55)**:读取本端点同样建立推送订阅(`observability_trace` 帧随后推来),详见 §4.16。 - **响应**:`200 {"data":[...]}`(newest first)。`AdminTraceSpan` = `{id, parentId?, name, kind, status, startedAt, endedAt?, durationNs, props?, events?}`。`kind` ∈ `llm_call|tool_call|orchestration|state_transition|subagent|memory|guardrail`;`events` 为 `[{time,type,props?}]`;时间 RFC3339。 - **旁路节点的 span 现已真实产生**(裁决 11.4.2 / 11.4.3,2026-10-04):`memory.recall` / `memory.write` / `guardrail.input` / `guardrail.output` / `orchestration.spawn` 均由真实生产者开出,因此 `kind` 取值集里的 `memory|guardrail|orchestration` 不再只是枚举——trace 列表中会真的出现这三类 span(此前三者无生产者,全仓只有 `llm_call`/`tool_call`/`subagent`/`state_transition`)。它们只带元数据(记忆作用域/条数/字节数、护栏裁决与耗时、被派发的子代理 id 与目标角色 id),**不含记忆正文、不含提示词、不含密钥**;见 `design/19 §1`。 - 来源:环形缓冲(默认 512 条),由 `ObserveSpan`/`TraceSink` 注入。curl: @@ -868,7 +873,7 @@ curl -s -X POST 'http://:8080/api/v1/admin/channels/kook/message/test?acco | `mode` | string | → `config["mode"]` | | `accessToken` | string | → `config["access_token"]` | | `selfID` | string | → `config["self_id"]` | - | `config` | object | 平台专属键值(如 `bot_token`/`base_url`/`remote_url`/`endpointPath`/`receive_mode`/`webhook_verify_token`...) | + | `config` | object | 平台专属键值(如 `bot_token`/`base_url`/`remote_url`/`endpointPath`/`receive_mode`/`webhook_verify_token`/`transport`/`api_base_url`/`report_path`...) | | `allowGroups`/`denyGroups` | string[] | 黑白名单(群) | | `allowUsers`/`denyUsers` | string[] | 黑白名单(用户) | | `groupMsgPerMin` | int | 群消息限流 | @@ -876,6 +881,7 @@ curl -s -X POST 'http://:8080/api/v1/admin/channels/kook/message/test?acco | `maxMessages` | int | 单账号最大消息数 | > 空配置时:`weixin-ilink` 豁免(先建账号再扫码),其它平台 `400`。 > **`config` 是补丁(v0.54,与 `personaId` 同构)**:只含本次请求显式给出的键——键省略(`config` 里没有该键 / `config` 整体缺席或 `null`)= **保留已存值**、给非空值 = 覆盖、显式 `null`/`""` = **清除**(`0`/`false` 不算空);顶层便捷字段(`token`/`baseURL`/…)是普通 string,只在**非空**时并入 ⇒ 清除 `bot_token` 须走 `config` 的显式 `null`/`""`。`allowGroups`/`denyGroups`/`allowUsers`/`denyUsers` 与三个限流键仍是**整值替换**(省略 = 清空)。 + > **`qq-obv11` 的 `config` 新增三键(v0.55,OneBot v11 四端,裁决 1.4 / 12.4.1)**:`transport`(`ws`|`http`,默认 `ws`,未知值回落 `ws`)、`api_base_url`(OneBot HTTP API 基址,**必须显式给出、不从 `remote_url` 推导**)、`report_path`(http+server 收上报的路径,默认 `/`)。`mode` 决定哪一半必需:`server` 必需 `listen_addr`(**独立监听端口,`AGENTS.md` §9 登记的唯一豁免,为空则 `Connect` 报错、绝不开端口**)、`client` 必需 `api_base_url`;另一半给齐字段就一并启用(单个适配器即可「收上报 + 回消息」)。env 兜底 `CHANNEL_QQ_OBV11_{TRANSPORT,API_BASE_URL,REPORT_PATH}`。四端完整语义见 `api-client.md` §2 的「OneBot v11 四端契约(v0.55)」。 - **响应**:`201 {"data":{"persisted":true,"path":"","platform":"","accountId":""}}`。**真实持久化 + 热更新**(读 channels.json→**按 id 打补丁**(`PatchChannel`)→落盘→`Reload(platform)` 热替换 adapter,无需重启;**v0.54 起 `config` 为 patch 三态**(与 `personaId` 同构):键省略=保留该账号已存值 / 给出非空值=覆盖该键 / 显式 `null`或`""`=清除该键,详见 `api-client.md` §2)。token 写入文件但**不回显**(GET 列表掩码)。 - **错误**:`400`(platform 缺失 / 空配置);`500`(渠道配置服务未注入 / 写盘失败 / 热重载失败)。 - **审计**:`channel.save`。curl: @@ -1026,7 +1032,7 @@ curl -s -X POST http://:8080/api/v1/admin/system/restore \ | `type` | string | 是 | 事件类型(见 §6.3 表) | | `turnId` | string | 否 | 一轮交换的关联 id(流式;`turn-`);OnStream 结构化流路径**不带** | | `seq` | int | 否 | hub 级单调递增序号,供客户端排序关联流式突发 | - | `sessionId` | string | 否 | 能确定会话的事件带上,客户端按它把帧路由到该会话的消息桶(v0.54 起非激活会话的帧不再丢弃;只有单槽忙态仍按激活会话判定,见 `api-client.md` §3.1/§3.2/§3.3) | + | `sessionId` | string | 否 | 能确定会话的事件带上,客户端按它把帧路由到该会话的消息桶(v0.54 起非激活会话的帧不再丢弃;只有单槽忙态仍按激活会话判定,见 `api-client.md` §3.1/§3.2/§3.3);可观测推送三帧(`observability_log`/`observability_trace`/`observability_metrics`,v0.55)**不带**该字段,帧壳只有 `{type,data}` | | `data` | any | 否 | 类型化负载(每个事件见 §6.3) | - **上行(客户端→服务端)** `wsInbound`: @@ -1043,7 +1049,7 @@ curl -s -X POST http://:8080/api/v1/admin/system/restore \ | type | 数据负载 | 服务端行为 | 结果 | |------|----------|-----------|------| | `mode_switch` | 任意(如 `{"mode":"chat"}`) | 发布 `ws.mode_switch` → hub 规范化广播 | 下行 `mode_switch`(广播) | -| `chat` | `{sessionId?, text, image?, deviceId?, mode?}` | 发布 `ws.chat` → `streamChat`(跑 loop 并流式) | 下行 `stream_chunk`/`stream_end` 或 `message.*/thinking.*/tool_call.*`(广播) | +| `chat` | `{sessionId?, text, image?, deviceId?, mode?}` | **直达会话编排器的非阻塞 `Submit`(v0.55 起:上行 chat 不再经可丢弃的 bus——会话消息不得被 drop;编排器未注入时回退发布 `ws.chat`)** → `streamChat`(跑 loop 并流式) | 下行 `stream_chunk`/`stream_end` 或 `message.*/thinking.*/tool_call.*`(广播) | | `interrupt` | `{sessionId}` | 会话编排器中断:取消在跑 turn + 清空 pending | 下行 `session_interrupted` | | `voice_input` | `{audio?, format?, text?, sessionId?}` | STT→chat→TTS(缺省 sessionId=`web`) | 下行 `voice_output`(**定向回请求连接**) | | `subagent_ask` | `{id, mode?(""/full/tail/keyword), n, query?}` | 审计子代理上下文(缺省 `mode`=full) | 下行 `subagent_event`(或 `error`) | @@ -1059,7 +1065,7 @@ curl -s -X POST http://:8080/api/v1/admin/system/restore \ ### 6.4 下行事件全表 -> **角色门(design/11 §7.1,fail-closed)**:下行帧按类型分档,收口在 `internal/services/gateway/ws_frame_scope.go` 的 `frameReachesAll`(**唯一一处判定**,各调用点不自行决定可见性)。**只有 7 个不含内容的状态机信号**发给**所有已认证连接**(含 `viewer`):`session_busy` / `pending_ack` / `pending_drain` / `session_idle` / `session_interrupted` / `pending_full` / **`session_read`**。**其余全部只发 `owner`+`admin`**:流式内容(`message.*` / `thinking.*` / `tool_call.*` / `tool_execution.update` / `stream_chunk` / `stream_end`)、持久化(`message_persisted` / `message_edited` / `message_deleted` / `channel_persisted`)、系统内部(`graph_update` / `subagent_event` / `task_output` / `task_done` / `tool_used` / `capacity_awaken` / `mode_switch`)、私有状态(`mood` / `device_update` / `reminder` / `proactive_message`);**未登记的新帧类型默认只发 owner/admin**。**本表并非全表**:`session_read` / `message_edited` / `message_deleted` 等已实现事件尚未逐行登记于此;**权威清单与逐帧口径见 `Docs/api-client.md` §3**。 +> **角色门(design/11 §7.1,fail-closed)**:下行帧按类型分档,收口在 `internal/services/gateway/ws_frame_scope.go` 的 `frameReachesAll`(**唯一一处判定**,各调用点不自行决定可见性)。**只有 7 个不含内容的状态机信号**发给**所有已认证连接**(含 `viewer`):`session_busy` / `pending_ack` / `pending_drain` / `session_idle` / `session_interrupted` / `pending_full` / **`session_read`**。**其余全部只发 `owner`+`admin`**:流式内容(`message.*` / `thinking.*` / `tool_call.*` / `tool_execution.update` / `stream_chunk` / `stream_end`)、持久化(`message_persisted` / `message_edited` / `message_deleted` / `channel_persisted`)、系统内部(`graph_update` / `subagent_event` / `task_output` / `task_done` / `tool_used` / `capacity_awaken` / `mode_switch`)、私有状态(`mood` / `device_update` / `reminder` / `proactive_message`)、**可观测推送(`observability_log` / `observability_trace` / `observability_metrics`,v0.55)**;**未登记的新帧类型默认只发 owner/admin**。**本表并非全表**:`session_read` / `message_edited` / `message_deleted` 等已实现事件尚未逐行登记于此;**权威清单与逐帧口径见 `Docs/api-client.md` §3**。 > > **来源标记**:`[direct]` 网关直接 emit;`[relay]` hub 从 bus `ws.>` 规范化(任意 `ws.X` 主题 → 类型 `X`);`[bridge]` 桥接 producer 主题;`[target]` 仅定向特定连接。 @@ -1097,6 +1103,9 @@ curl -s -X POST http://:8080/api/v1/admin/system/restore \ | `reminder` | bridge | producer payload | 提醒触发(`reminder.trigger`) | | `mode_switch` | direct | 上行原样(任意) | 行为模式切换回显(`ws.mode_switch`) | | `mood`/`tool_used`/`graph_update`/… | relay | `ws.X` 主题负载(`ws.mood`→`mood`,`ws.tool_used`→`tool_used`,`ws.graph_update`→`graph_update`,…) | 生产者在 bus 上发 `ws.X` 即规范化成类型 `X` 广播 | +| `observability_log` | bridge | `{entries:[{time,level,source,msg}…]}`(最新在前) | **可观测推送(v0.55)**:订阅后新增日志,≤1s 一拍最多一帧、无新条目不发;单帧上限 200 条;**不带 `sessionId`** | +| `observability_trace` | bridge | `{spans:[AdminTraceSpan…]}`(最新在前) | **可观测推送(v0.55)**:订阅后新完成的 span;单帧上限 100 条;**不带 `sessionId`** | +| `observability_metrics` | bridge | ``(扁平,与 REST 同形) | **可观测推送(v0.55)**:与上次发出的不同才发;**不带 `sessionId`** | | `error` | direct | `{message}` | 非法 JSON / 未知类型 / 语音·子代理·任务错误等 | > **`auth`**(旧 design/07 列表):当前**不**由网关直接 emit;若未来有 `ws.auth` 生产者主题,则按 `[relay]` 规则广播。本文档以代码为准。