docs(contract): v0.79 —— 面板动作真可用 + 记忆审查/语音可见面 + 回合标识全覆盖
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

四批一并登记(DOC-3 按代码逐条核对,含 文件:行 证据;我复核版本行/唯一当前版本/端点面):
① PLG-2 动作真可用:SetExecutor 已删、生产执行器接线(wire_frontend.go:205 →
   wire_plugin_actions.go:61-89);错误码按代码重写 —— action_not_implemented(501) 只剩
   「已声明未登记处理器」,兜底是 502 action_failed;并纠正一处输入说法:网关层非法 JSON
   回 bad_request(非 invalid_payload),两个码都登记
② CODE-TC-2 回合标识全覆盖:StartRoundWithTurnID 是唯一生产建图点;graph.id 对单路也追加
   #{回合标识} ⇒ 契约明写「一律用响应里的 id,不要自拼」
③ L45:新增 B16 GET /admin/memories/reviews + B17 POST /admin/memories/{id}/revert(writeAuth)
   + U19 GET /api/v1/media/{name} 与消息 DTO voice(覆盖率按实际填充面写,帧/握手回放标注不带)
④ L2/SK-5 零契约变更(已在报告说明)
计数:writeAuth 14→16、总数 68→70。

⚠️ DOC-3 指出的易误用点(已在文档区分):turns[].turnId(=graph.id) 与 lanes[].turnId(=权威 turn-N) 不同源。
未确证:documents.SaveAudio / UpdateMessageVoice / RevertMerged 本体未逐行核(标「转述自 L45」);真机未跑。
This commit is contained in:
AskaEth committed 2026-10-06 14:20:08 +08:00
1 parent 1420e8aaa6
commit 78e99d9f4c
1 file changed
+107 -70
+107 -70
View File
@@ -1,12 +1,12 @@
# Cyrene v2 客户端 API 契约
> **契约版本:`v0.78`**(上一版 `v0.77`,其条目见 §6;`v0.76` / `v0.75` / `v0.74` / `v0.73` / `v0.72` / `v0.71` 及更早见本文件 §6,`v0.67` 及更早的基线已归档、只读:`Docs/archive/api-client.v0.65.md`)。
> **契约版本:`v0.79`**(上一版 `v0.78`,其条目见 §6;`v0.77` / `v0.76` / `v0.75` / `v0.74` / `v0.73` / `v0.72` / `v0.71` 及更早见本文件 §6,`v0.67` 及更早的基线已归档、只读:`Docs/archive/api-client.v0.65.md`)。
> **读者**:Web 前端 / 移动端原生客户端(含 Android)/ IM 桥接等**跨平台客户端开发者**。
> **唯一真源 = 当前代码**(`internal/services/gateway/*` 等)。本文档由代码逐条核对得出;与代码不符时**以代码为准**,并回改本文档。
> **旧的超详细版已归档、只读**:`Docs/archive/api-client.v0.65.md`(412 KB,含 v0.28→v0.65 全部历史条目)与 `Docs/archive/api-reference.v0.65.md`(170 KB,后端全量字段级文档)。归档的原因是**结构性臃肿 + 历史条目并列 + 长期漂移**;本文档**不复制**那段历史,需要考古时去归档。
> **维护铁律**:每实现/变更一个 REST 端点、WS 事件、admin API **立即改本文档**;契约变更必须版本化,不破坏既有客户端。
> **准确性 > 详尽度**:宁少写也别写错;**写不了确证的写「未确证」**,见 [§7](#7-在飞切片与未实现未确证)。
> ⚠️ **v0.67 是「契约先行」登记版**:本版新增的三批 —— **语音消息**(播放端点 + 消息 DTO `voice`)、**非消息事件查询面**、**账号在线状态** —— 登记时**都还没有实现**,形状**取自设计文档**(`design/24 §10.11`、`design/26 §4.5` / `§2.6`)**而非代码核对**。**已落地**:账号在线状态(`52dac45`)、非消息事件查询面(切片 E6)—— 见 §2.4;**语音消息的「用户可见面」截至目前(v0.78)仍未实现**(v0.78 只落地了落库列 `0020` 与入站归一链,见 §4.5 / §7.2)。已在 §7.2 逐条如实标注。**本文档其余内容仍以代码为真源**。
> ⚠️ **v0.67 是「契约先行」登记版**:本版新增的三批 —— **语音消息**(播放端点 + 消息 DTO `voice`)、**非消息事件查询面**、**账号在线状态** —— 登记时**都还没有实现**,形状**取自设计文档**(`design/24 §10.11`、`design/26 §4.5` / `§2.6`)**而非代码核对**。**已落地**:账号在线状态(`52dac45`)、非消息事件查询面(切片 E6)—— 见 §2.4;**语音消息的「用户可见面」已由 v0.79 落地**(切片 L45:DTO `voice` + `GET /api/v1/media/{name}` + 副本落盘;残留缺口——WS 实时帧/握手回放不带 `voice`、AMR 不落副本——见 §4.5 / §7.2 / §7.4-29)。已在 §7.2 逐条如实标注。**本文档其余内容仍以代码为真源**。
> **v0.68 是纯增量版(已实现,非契约先行)**:新增 1 条管理只读端点 `GET /api/v1/admin/config-facts`(配置事实面,见 §2.2 B12),并给 `GET /api/v1/admin/backthink` 的条目加 2 个可选字段(`urgency` / `quietHoursBypassed`,见 §2.2 B8)。**无破坏性变更**,老客户端零影响。
> **v0.69 是缺口修复版(已实现)**:`PUT /api/v1/admin/channels/{platform}/config` 现在**接收并原样落盘**账号顶层 `directMsgPerMin`,`GET /api/v1/admin/channels` 的账号元素也**如实回显**它(§2.2 A5 / §2.3 C28 注)。修的是「面板保存一次渠道配置,就把主人手配的**关闭流控(-1)**静默重置回 `0` = 保护性默认 6 条/分」。**无破坏性变更**(老客户端不带该键,行为与 v0.68 相同)。
> **v0.70 是精确化版(已实现,纯增量)**:`GET /api/v1/admin/backthink` 的 `urgency` **默认就是该轮实际生效的精确档位**(随记录落库),另加可选 `urgencyLowerBound:true` 专门标记**旧行的下限推导值**;实时 `backthink` 帧自本版起也带记录里的 `urgency`(§2.2 B8 / §4.10(b),并更正 §4.10(b) 原「实时帧不带这两键」的表述)。**无破坏性变更**(新键可选,老客户端不读即零影响;`urgency` 的既有取值域与 `quietHoursBypassed` 语义都未改)。
@@ -18,11 +18,13 @@
> **v0.74 是 IM 路由主动消息的提醒版(已实现,纯增量)**:`message_persisted` 的 `data` 新增**可选**结构化判据 `proactive:true` —— IM 路由(`im_bridge`)的主动推送在落库之后**追加**一条带该键的同类帧,客户端据此在**非前台**补一发浏览器通知(此前通知只挂在 `proactive_message` 上,而 `auto` 提示在主人有已连接 IM 渠道时**优先走 IM** ⇒ 实际最可能发生的路径恰恰不弹)。判据是**结构化字段**(不是正文/提示词):普通消息(Web 回合 / IM 回复 / IM 镜像)的帧**永不**带该键 ⇒ 不可能被误判成主动消息。同一行仍照旧发一条**不带**该键的普通镜像帧(v0.72 起的行为逐字不变,两帧同 id ⇒ 客户端按 id 去重后仍是一个气泡、仍只提醒一次)。详见 **§3.4.4 后的「`message_persisted` 的 `proactive` 判据」**。
> **v0.76 是三面增量版(⚠️ **逐条状态不同**:**① 已在 v0.78 实现(覆盖率受限)**;**②③ 已实现**)**:① **消息读取面新增 `toolCalls[]`(工具回看面)—— ✅ 已实现(v0.78,切片 CODE-TC),但覆盖率受限**:`GET /api/v1/sessions/{id}/messages` 的消息对象加可选 `toolCalls`(`omitempty`),每条含工具名 / 状态(`ok`/`error`/`unknown`)/ 耗时毫秒 / **已脱敏**的入参与结果摘要;数据源是**已落盘的轮次链路图**(`flow_graphs`,迁移 `0022`;**v0.78 加迁移 `0023` 的按路查询键**,保留期 168h),**不从正文反解**。🔴 **缺席语义**:图取不到(超保留期 / 该轮未落盘 / 隔离渠道会话从不入库 / 该路标识查不到图)⇒ 字段**整体缺席**,**绝不回空数组**冒充「这一轮没调过工具」。⚠️ **历史说明(v0.76 登记时未实现的原因,实测确认非推断)**:`chat_messages.turn_id` 与 `flow_graphs.turn_id` **不同源** ⇒ 两者无法按消息字面 join,按时间窗猜映射项目口径不做;补齐需先让链路图带上**按路的回合标识**、并补一个**按路查图**的读侧入口。**v0.78 已把这两件都补上** ⇒ `toolCalls[]` 已实现;但🔴 **只有真并行路能回看**(普通单路回合的图当前不带权威 `turnId`)—— 见 **§4.4、§7.2、§7.4-23 与 §6 的 v0.78 ①**。② **未读面(✅ 已实现)** —— `message_persisted` 的 `data` 加**可选** `unreadCount`(`omitempty`:只在 **>0 且确实算得出来**时出现;**缺席 = 老行为**),`GET /api/v1/sessions` 列表项同键(该键**自 D4 起就已在**,本版未改它)。口径 = **绝对未读 + 主人单一已读游标(`chat_sessions.read_at`)⇒ 多端共享**(Web 读了移动端也清),这是与安卓侧已对齐的**取舍**,不是缺陷。详见 **§3.4.4、§4.13**。③ **记忆整理试算加性异步** —— `POST /api/v1/admin/memories/tidy/dry-run?async=1` ⇒ `202` + `runId`;新增 `GET /api/v1/admin/memories/tidy/dry-run/{runId}` 轮询;**不带 `async` 仍是原同步行为**。**无破坏性变更**(新键 / 新可选参数 / 新端点;老客户端零影响)。详见 §1(会话列表与消息读取)、§2.2 B15 / §4.12.1、§3.4.4。
> **v0.76 是三面增量版(⚠️ **逐条状态不同**:**① 已在 v0.78 实现、覆盖率又由 v0.79 补齐**;**②③ 已实现**)**:① **消息读取面新增 `toolCalls[]`(工具回看面)—— ✅ 已实现(v0.78,切片 CODE-TC),覆盖率已于 v0.79 补齐至全部编排回合**:`GET /api/v1/sessions/{id}/messages` 的消息对象加可选 `toolCalls`(`omitempty`),每条含工具名 / 状态(`ok`/`error`/`unknown`)/ 耗时毫秒 / **已脱敏**的入参与结果摘要;数据源是**已落盘的轮次链路图**(`flow_graphs`,迁移 `0022`;**v0.78 加迁移 `0023` 的按路查询键**,保留期 168h),**不从正文反解**。🔴 **缺席语义**:图取不到(超保留期 / 该轮未落盘 / 隔离渠道会话从不入库 / 该路标识查不到图)⇒ 字段**整体缺席**,**绝不回空数组**冒充「这一轮没调过工具」。⚠️ **历史说明(v0.76 登记时未实现的原因,实测确认非推断)**:`chat_messages.turn_id` 与 `flow_graphs.turn_id` **不同源** ⇒ 两者无法按消息字面 join,按时间窗猜映射项目口径不做;补齐需先让链路图带上**按路的回合标识**、并补一个**按路查图**的读侧入口。**v0.78 已把这两件都补上** ⇒ `toolCalls[]` 已实现;🔴 **v0.78 时只有真并行路能回看**(普通单路回合的图当时不带权威 `turnId`)—— **已由 v0.79(切片 CODE-TC-2)补齐**(见 **§4.4、§7.2、§7.4-23 与 §6 的 v0.78 ① / v0.79 ④**)。② **未读面(✅ 已实现)** —— `message_persisted` 的 `data` 加**可选** `unreadCount`(`omitempty`:只在 **>0 且确实算得出来**时出现;**缺席 = 老行为**),`GET /api/v1/sessions` 列表项同键(该键**自 D4 起就已在**,本版未改它)。口径 = **绝对未读 + 主人单一已读游标(`chat_sessions.read_at`)⇒ 多端共享**(Web 读了移动端也清),这是与安卓侧已对齐的**取舍**,不是缺陷。详见 **§3.4.4、§4.13**。③ **记忆整理试算加性异步** —— `POST /api/v1/admin/memories/tidy/dry-run?async=1` ⇒ `202` + `runId`;新增 `GET /api/v1/admin/memories/tidy/dry-run/{runId}` 轮询;**不带 `async` 仍是原同步行为**。**无破坏性变更**(新键 / 新可选参数 / 新端点;老客户端零影响)。详见 §1(会话列表与消息读取)、§2.2 B15 / §4.12.1、§3.4.4。
> **v0.77 是链路图按路分桶 + 记忆整理护栏/异步落地版(已实现;纯增量)**:① **`graph_update` 按路分桶**(切片 FV2)—— 帧 `data` 新增可选 `turnId`(该图的**路**的权威回合标识)与 `round`(会话轮次号);真并行(`parallel:true`)时同一会话**每一路各发自己的帧**(`data.id` / `data.turnId` 各不相同),两路不再互相顶掉;`GET /api/v1/admin/flow/graph` 响应新增 `lanes[]`(**最近一轮每一路**的图,恒为非 null 数组),`graph` 语义不变(最新一路 / 指定轮 / `null` 诚实空态)⇒ **老的读取方零改动**。**无破坏性变更**(两个新键可选;单路时 `turnId` 缺席 ⇒ 客户端行为与 v0.75 逐字相同)。详见 **§2.2 B13 / §3.4.5 / §4.11 / §7.4-19**。② **记忆整理护栏抬高 + 诚实截断 + 试算异步落地**(切片 MT-A)—— 每分区候选上限 `MEMORY_TIDY_CANDIDATE_LIMIT` 默认值 `20 → 200`、两两比对上限改为 env 键新键 `MEMORY_TIDY_COMPARE_LIMIT`(默认 `2000`,原先**写死** `400`);配置事实面 **12 → 13 条**;`truncated` 新增第 4 种标签 **`(budget)`**(墙钟时间预算截断);**v0.76 ③ 的加性异步已落地**(`?async=1` + B15 轮询可用;新错误码 **`tidy_run_not_found`**)。详见 **§2.2 B14/B15 / §4.10(a) / §4.12 / §4.12.1 / §5**。③ **「入库前审查」的 4 个 env 键登记**(切片 MT-B,纯增量)—— `MEMORY_REVIEW_ENABLED`(`true`)/ `MEMORY_REVIEW_QUEUE_SIZE`(`256`)/ `MEMORY_REVIEW_SCAN_LIMIT`(`200`)/ `MEMORY_REVIEW_TIMEOUT_SEC`(`20`);⚠️ 该切片的**面板读侧 / 回滚端点目前没有 HTTP 入口**(见 §7.2)。详见 **§5**。
> **v0.78 是六面增量版(⚠️ **①~⑤ 已实现;⑥ 只到落库面**(用户可见面仍未实现);以纯增量为主,无破坏性变更)**:① **消息读取面 `toolCalls[]` 落地**(切片 CODE-TC)—— v0.76 ① 从「未实现」改为**已实现**(数据源 `flow_graphs` + 迁移 `0023` 的**按路查询键** `lane_turn_id`);🔴 **覆盖率受限(如实)**:只有**真并行路**的图带权威 `turnId`,普通单路回合**查不到图 ⇒ 字段缺席**(契约允许,但不是想要的覆盖率)。详见 **§4.4 / §7.2 / §7.4-23**。② **插件管理面板接入点(管理面新增两条路由 + 一条 WS 帧)**(切片 PLG-2)—— 新增 `GET /api/v1/admin/panels`(**按调用者角色过滤**后的接入点快照,viewer 可读)与 `POST /api/v1/admin/plugins/{id}/actions/{action}`(**只接受该插件声明过的动作名**,未声明 ⇒ `404 action_not_declared`;写动作 `writeAuth` + 审计);WS 下行帧 **`panels_changed`**(插件生命周期变化后推,**只发 owner/admin**)。⚠️ **动作执行器无生产实现 ⇒ 所有已声明动作当前回 `501 action_not_implemented`**(见 §7.2)。详见 **§2.1 A13/A14 / §3.4.5 / §4.14**。③ **搜索 provider 可变源**(切片 PLG-4)—— 插件 `web-search` 的配置从 `.env` 迁到 `$DATA_DIR/config/plugins/web-search.json`(0600,文件名 = 插件 id),组合根「**先可变源、缺失回落 `.env` 播种**」;面板数据面 = `plugin.listProviders` / `plugin.saveProvider` / `plugin.testProvider` / `plugin.reauthProvider` 四个动作。🔴 **不新增任何 env 键**(`.env` 已冻结为引导面)。详见 **§4.14(c)**。④ **账号上下线落事件 + 渠道事件实时帧**(切片 E7)—— 账号 online/offline 迁移落进 `internal/runtime/events`(复用迁移 `0021`,`tier=t2` / `category=lifecycle`),并经**既有进程内 bus** 推成 WS 下行帧 **`channel_event`**(**只发 owner/admin**,viewer 走 REST 只读档)。⚠️ **当前只覆盖账号状态迁移这一路**;覆盖其余全部渠道事件的装饰器**未接线**(见 §7.2)。详见 **§3.4.5**。⑤ **微信 iLink 账号状态三态**(切片 CODE-F3)—— `GET /api/v1/admin/channels/{platform}/login-status` 新增可选 `authState` / `authDetail`(**有效 / 被拒 / 未知**);🔴 **`errcode=-14` 是「被拒」不是「过期」**,**未知同样不许写成「过期」**。⚠️ `GET /admin/channels` 的 `accounts[]` **每账号**三态**未实现**(见 §7.2)。详见 **§2.4(c)**。⑥ **语音消息落库面**(切片 CODE-VOICE)—— 迁移 `0020` 给 `chat_messages` 加 `voice` 列(`TEXT NOT NULL DEFAULT ''`);🔴 **用户可见面(DTO `voice` + `GET /api/v1/media/{name}`)仍未实现**(生产接线未补,见 §7.2)。详见 **§4.5 / §7.2**。**无破坏性变更**(新端点 / 新可选键 / 新帧;老客户端零影响)。
> **v0.79 是「面板动作真可用 + 记忆审查/语音用户可见面 + 链路图回合标识全覆盖」版(四批,均**已实现**;**唯一一处形状变化**)**:① **插件面板动作真可用**(切片 L1/PLG-2 的执行面接线)—— `POST /api/v1/admin/plugins/{id}/actions/{action}` **不再一律 501**:web-search 的四个动作 + 下拉候选来源(`plugin.providers`)都已登记**生产处理器**,**真读写** `$DATA_DIR/config/plugins/web-search.json`、探针**真发一次 HTTP**;失败回**结构化非 2xx**(错误码/文案/状态由插件侧给出,宿主只透传),新增 `invalid_payload` / `unknown_provider` / `not_configured` / `config_unreadable` / `config_write_failed` 与一批探针码(502);**只有「已声明但宿主未登记处理器」才回 `501 action_not_implemented`**(不再是兜底档)。审计新增附加字段 `code`(**不进 DB 记录体、不含请求体**)。详见 **§2.1 A14 / §4.1 / §4.14 / §7.2**。② **「入库前审查」(D3)的管理面落地**(切片 L45)—— 新增 2 条 `writeAuth` 端点 **`GET /api/v1/admin/memories/reviews?limit=`**(留痕列表,**不含记忆正文**;`limit` 缺省 100 / `≤0` 同缺省 / >500 钳位 / 非整数 `400`)与 **`POST /api/v1/admin/memories/{id}/revert`**(撤销一次软合并,**无损可逆**,落审计 `memory.review.revert`);未接线 ⇒ `500 memory_review_unavailable`(**绝不回空数组**假装没有留痕)。详见 **§2.2 B16/B17 / §4.1**。③ **语音消息的用户可见面落地**(切片 L45)—— 消息 DTO 的 **`voice`** 现在真的填充(REST 读取面 + `message_edited`),新增 **`GET /api/v1/media/{name}`**(200/206/304/416/`HEAD`;`audio/*` 内联 + `nosniff` + `Accept-Ranges`;**无 `ETag`**;名字非法 / 穿越 / 缺失统一 `404 media_not_found`),副本真落 `$DATA_DIR`(`documents.SaveAudio`,内容寻址、0600)。⚠️ **残留缺口(如实)**:`message_persisted` / `channel_persisted` 帧与握手 `history` 回放**仍不带 `voice`**;组合根**未注入 `VoiceTranscoder`** ⇒ **AMR 不落副本**(可播格式照落)。详见 **§1.2 U19 / §3.4.4 / §4.1 / §4.4 / §4.5 / §7.2**。④ 🔴 **链路图的权威回合标识覆盖到全部编排回合**(切片 CODE-TC-2)—— 编排器驱动的**每一路**(含**普通单路回合**与队首续跑路)的图现在都带权威 `turnId`,取值 = 该轮消息行的 `chat_messages.turn_id` ⇒ `GET /sessions/{id}/messages` 的 **`toolCalls[]` 覆盖率从「只有真并行路」扩大到「全部编排回合」**(普通单路聊天与队首续跑回复现在也能回看工具)。🔴 **唯一形状变化**:`graph.id` 的 `#{回合标识}` 后缀现在**单路也追加**(`web-7` → `web-7#turn-…`)⇒ 客户端**一律使用响应里给出的 `graph.id` / `turns[].turnId`**,**不要自己拼** `{会话}-{序号}`。**缺席语义一字未改**:拿不到权威标识的写入路径(未接编排器的调用方、**后台思考** `sess-backthink`、旧后端)`turnId` 仍缺席、`toolCalls` 仍整体缺席;**v0.79 之前的单路历史行不回填**。详见 **§4.4 / §4.11 / §7.2 / §7.4-23**。**无破坏性变更**(新端点 / 新可选键 / 新错误码;老客户端若不自己拼 `graph.id` 则零改动)。
> **v0.78 是六面增量版(⚠️ **①~⑤ 已实现;⑥ 只到落库面**(用户可见面**已由 v0.79 补齐**);以纯增量为主,无破坏性变更)**:① **消息读取面 `toolCalls[]` 落地**(切片 CODE-TC)—— v0.76 ① 从「未实现」改为**已实现**(数据源 `flow_graphs` + 迁移 `0023` 的**按路查询键** `lane_turn_id`);🔴 **v0.78 时覆盖率受限(如实)**:只有**真并行路**的图带权威 `turnId`,普通单路回合**查不到图 ⇒ 字段缺席**(契约允许,但不是想要的覆盖率)—— **已由 v0.79 补齐**(见 §6 v0.79 ④)。详见 **§4.4 / §7.2 / §7.4-23**。② **插件管理面板接入点(管理面新增两条路由 + 一条 WS 帧)**(切片 PLG-2)—— 新增 `GET /api/v1/admin/panels`(**按调用者角色过滤**后的接入点快照,viewer 可读)与 `POST /api/v1/admin/plugins/{id}/actions/{action}`(**只接受该插件声明过的动作名**,未声明 ⇒ `404 action_not_declared`;写动作 `writeAuth` + 审计);WS 下行帧 **`panels_changed`**(插件生命周期变化后推,**只发 owner/admin**)。⚠️ **动作执行器在 v0.78 无生产实现 ⇒ 当时所有已声明动作都回 `501 action_not_implemented`** —— **已由 v0.79 接线**(见 §6 v0.79 ① 与 §4.14)。详见 **§2.1 A13/A14 / §3.4.5 / §4.14**。③ **搜索 provider 可变源**(切片 PLG-4)—— 插件 `web-search` 的配置从 `.env` 迁到 `$DATA_DIR/config/plugins/web-search.json`(0600,文件名 = 插件 id),组合根「**先可变源、缺失回落 `.env` 播种**」;面板数据面 = `plugin.listProviders` / `plugin.saveProvider` / `plugin.testProvider` / `plugin.reauthProvider` 四个动作。🔴 **不新增任何 env 键**(`.env` 已冻结为引导面)。详见 **§4.14(c)**。④ **账号上下线落事件 + 渠道事件实时帧**(切片 E7)—— 账号 online/offline 迁移落进 `internal/runtime/events`(复用迁移 `0021`,`tier=t2` / `category=lifecycle`),并经**既有进程内 bus** 推成 WS 下行帧 **`channel_event`**(**只发 owner/admin**,viewer 走 REST 只读档)。⚠️ **当前只覆盖账号状态迁移这一路**;覆盖其余全部渠道事件的装饰器**未接线**(见 §7.2)。详见 **§3.4.5**。⑤ **微信 iLink 账号状态三态**(切片 CODE-F3)—— `GET /api/v1/admin/channels/{platform}/login-status` 新增可选 `authState` / `authDetail`(**有效 / 被拒 / 未知**);🔴 **`errcode=-14` 是「被拒」不是「过期」**,**未知同样不许写成「过期」**。⚠️ `GET /admin/channels` 的 `accounts[]` **每账号**三态**未实现**(见 §7.2)。详见 **§2.4(c)**。⑥ **语音消息落库面**(切片 CODE-VOICE)—— 迁移 `0020` 给 `chat_messages` 加 `voice` 列(`TEXT NOT NULL DEFAULT ''`);**用户可见面(DTO `voice` + `GET /api/v1/media/{name}`)在 v0.78 时未实现,已由 v0.79 落地**(切片 L45,见 §6 v0.79 ③);残留缺口(WS 实时帧 / 握手回放不带 `voice`、AMR 不落副本)见 **§4.5 / §7.2**。**无破坏性变更**(新端点 / 新可选键 / 新帧;老客户端零影响)。
> **v0.75 是记忆整理试算版(已实现,纯增量)**:新增 1 条管理端点 **`POST /api/v1/admin/memories/tidy/dry-run`**(**只读语义的 POST**,`writeAuth` = owner/admin)—— 跑**同一套**记忆整理规则(召回门 + 包含判定 + 规则否决,与低峰定时轮共用代码路径),把候选**直接回在响应里**,**一行都不写**(不写 `memory_tidy_candidates` / `memory_tidy_runs`,绝不改 `memory_items`)。修的是「整理器只在 23:00–07:00 跑(design/23 §4),在它第一次开跑前面板只能空着」——主人要「现在就想看到在 736 条上它会建议合并哪些」。**零副作用是硬约束**(后端有逐行快照单测钉死);失败**一律如实作答**(`tidy_unavailable` / `tidy_disabled` / `tidy_recall_unavailable` / `tidy_timeout`),**绝不回空候选数组假装「没有建议」**。**无破坏性变更**(新端点;既有端点/帧一字未改)。详见 **§2.2 B14 / §4.12**。
@@ -56,9 +58,9 @@
- 成功:`{"data": <载荷>}`(个别端点直接回二进制,见 §4.5 语音)。
- 失败:`{"error":{"code":"<机器可读码>","message":"<人读文案>"}}`。
- 状态码全集(代码实际使用):`200 201 202 400 401 403 404 409 413 415 429 500 501 502 503`。
- 状态码全集(代码实际使用):`200 201 202 206 304 400 401 403 404 409 413 415 416 429 500 501 502 503`。
- ⚠️ **`502` 是 v0.78 引入**(插件动作执行失败 ⇒ `POST /admin/plugins/{id}/actions/{action}` 的 `action_failed`,见 §4.14(b));其余状态码集合未变。
- ⚠️ v0.67 登记的 `GET /api/v1/media/{name}`(**尚未实现**,见 §7.2)会引入 **`206` / `304`**,以及一条**非信封**的 **`416`**(`Range` 错误由 Go 标准库直写 `text/plain` + `Content-Range: bytes */<size>`,**不套** `{"error":…}`)。这三者**不在**上面那份「代码实际使用」集合内,实现后才加入;客户端**不要**拿 `error.code` 去解析 `416`。
- ⚠️ **`206` / `304` / `416` 是 v0.79 起「实际使用」**(`GET /api/v1/media/{name}` 落地,见 §1.2 U19 / §4.5):`206`(`Range` 部分内容)/ `304`(条件请求)由 Go 标准库给出;`416`(`Range` 语法非法或范围不可满足)是**非信封**的 —— 标准库直写 `text/plain` + `Content-Range: bytes */<size>`,**不套** `{"error":…}` ⇒ 客户端**不要**拿 `error.code` 去解析 `416`。
- **客户端按 `error.code` 分支,不要按 `message` 文本**(文案会随修改变动)。
### 0.4 三档标注惯例
@@ -119,7 +121,7 @@
| U16 | POST | `/api/v1/sessions/{id}/read` | owner/admin | 200 / 400 / 404 / 500 / 501 | 推进已读游标。body `{before?}`(毫秒;缺省=现在)→ `{data:{sessionId,readAt}}`。游标**会话级单所有者、只进不退**(读一处 = 该账号所有端已读)。成功广播 `session_read` 帧;失败不广播。 |
| U17 | PATCH | `/api/v1/messages/{id}` | owner/admin | 200 / 400 / 404 / 500 / 501 | 编辑单条消息。body `{content}`(非空、≤4000 rune)→ **`{data:<消息 DTO>}`**(`data` 直接就是完整消息,**不再包一层 `message`**)。只改 `content`。成功广播 `message_edited`。 |
| U18 | DELETE | `/api/v1/messages/{id}` | owner/admin | 200 / 404 / 500 / 501 | 删除单条消息 → `{data:{deleted:<id>}}`。成功广播 `message_deleted`。 |
| U19 | GET | `/api/v1/media/{name}` | **owner/admin(必须 Bearer)** | 200 / 206 / 304 / 401 / 403 / 404 / 416 | **语音副本播放 / 取字节**(**v0.67 登记,尚未实现**,见 §7.2)。`Content-Type: audio/*`;`Content-Disposition: inline; filename="<name>"`;`X-Content-Type-Options: nosniff`;`Accept-Ranges: bytes`;支持 `Range` 与 `HEAD`;名字非法 / 穿越 / 不存在统一 `404 media_not_found`。⚠️ **`416` 不是 JSON 信封**(标准库直写)。详见 §4.5 语音。 |
| U19 | GET | `/api/v1/media/{name}` | **owner/admin(必须 Bearer)** | 200 / 206 / 304 / 401 / 403 / 404 / 416 | **语音副本播放 / 取字节**(**v0.67 登记,v0.79 已实现**,切片 L45)。`Content-Type: audio/*`(按**存储名扩展名**映射,不查库、不嗅探);`Content-Disposition: inline; filename="<name>"`;`X-Content-Type-Options: nosniff`;`Accept-Ranges: bytes`;`Cache-Control: private, max-age=86400, immutable`;**无 `ETag`**(条件请求走 `Last-Modified`);支持 `Range` 与 `HEAD`;名字非法 / 穿越 / 不存在统一 `404 media_not_found`。⚠️ **`416` 不是 JSON 信封**(标准库直写 `text/plain` + `Content-Range: bytes */<size>`)。详见 §4.5 语音。 |
> **`GET /api/v1/files/{name}` 无鉴权**(能力 URL);**`GET /api/v1/media/{name}` 与 `GET /api/v1/documents/{name}` 必须 Bearer owner/admin**(与图片读侧「128-bit 名即凭据」的取向**刻意相反**);其余全部用户端点至少要求已认证。
@@ -138,14 +140,16 @@
| 档位 | 中间件 | 条数 | 说明 |
|---|:--:|:--:|---|
| **只读监控档** | `viewerAuth`(owner/admin/viewer) | **14** | 非敏感投影:无凭据、无他人账号数据、无消息正文。**v0.78 新增两条**:面板接入点快照(A13)与插件动作分发(A14,**写门落在 handler 内**,见 A14 行注) |
| **owner/admin 读档** | `writeAuth`(owner/admin) | **14** | viewer 不得看到的一切:记忆、LLM 栈、账号/key、后台思考、审计链、渠道消息日志、完整会话导出、配置事实面(v0.68)、最近一轮链路图快照(v0.71)、记忆整理试算(v0.75) |
| **owner/admin 读档** | `writeAuth`(owner/admin) | **16** | viewer 不得看到的一切:记忆、LLM 栈、账号/key、后台思考、审计链、渠道消息日志、完整会话导出、配置事实面(v0.68)、最近一轮链路图快照(v0.71)、记忆整理试算(v0.75)、入库前审查留痕与回滚(**v0.79**,B16/B17) |
| **owner-only 档** | `ownerAuth`(owner) | **40** | 带审计的策略写、每个获取凭据的渠道端点、其余全部变更 |
**管理路由总数 = 14 + 14 + 40 = 68 条。**
**管理路由总数 = 14 + 16 + 40 = 70 条。**
> **v0.78 计数变更**:`viewerAuth` 段新增两条(A13 `GET /admin/panels`、A14 `POST /admin/plugins/{id}/actions/{action}`)⇒ 12 → **14**、总数 66 → **68**。A14 走 `viewerAuth` 中间件,但**写动作在 handler 内按 owner/admin 收紧**(路由中间件只能有一道门)—— 因此它**是**本表的一行,读写面却不同档,详见 §2.1 A14 与 §4.14。
> **计数口径(v0.71 明确)**:总数是 §2.1 / §2.2 / §2.3 三张清单里**列出的行数**(与 v0.68 起的口径一致)。⚠️ `internal/services/gateway/routes.go` 里实际经 `writeAuth` 挂载的 **admin** 路由比 §2.2 多:v0.67 登记的 `GET /admin/channels/events` 与 `.../counters`(见 §2.4(a))**未计入本表**——这是历史口径,本版未改,也**未**把它算进上面的 14。逐条以代码为准。
> **v0.79 计数变更**:`writeAuth` 段新增两条(B16 `GET /admin/memories/reviews`、B17 `POST /admin/memories/{id}/revert`,切片 L45)⇒ 14 → **16**、总数 68 → **70**。⚠️ 同批落地的 `GET /api/v1/media/{name}`(§1.2 U19)是**用户侧**端点,**不**进入本矩阵。
> **计数口径(v0.71 明确)**:总数是 §2.1 / §2.2 / §2.3 三张清单里**列出的行数**(与 v0.68 起的口径一致)。⚠️ `internal/services/gateway/routes.go` 里实际经 `writeAuth` 挂载的 **admin** 路由比 §2.2 多:v0.67 登记的 `GET /admin/channels/events` 与 `.../counters`(见 §2.4(a))**未计入本表**——这是历史口径,本版未改,也**未**把它算进上面的 **16**。逐条以代码为准。
> ⚠️ **与归档版不一致(已更正)**:`Docs/archive/api-client.v0.65.md` §2.0 写「12 + 11 + 38 = 61」(其 v0.62 条目又写「12 + 10 + 38 = 60」,自相矛盾)。**以代码为准 = 12 + 11 + 40 = 63**。
@@ -166,11 +170,11 @@
| A11 | GET | `/api/v1/admin/observability/logs` | 后端日志尾部(最新优先,`?level=&limit=`;缓冲未注入时 501) |
| A12 | GET | `/api/v1/admin/notifications` | 统一通知 feed(审计链 + 后台思考推送的只读投影) |
| A13 | GET | `/api/v1/admin/panels` | **插件管理面板接入点快照(v0.78 新增,切片 PLG-2)**:**按调用者角色过滤**后的导航条目集合 —— viewer 只拿到只读区块(`status`/`log`/`table`,且 table 的行内动作被摘空、`form` 整体不出现、区块全空的面板整条不出现)。响应 `{data:{role,groups[],pluginIds[]}}`。未接面板中枢 → **`501 panels_unavailable`**(**绝不回空列表**)。形状与两条给客户端的口径见 §4.14(a) |
| A14 | POST | `/api/v1/admin/plugins/{id}/actions/{action}` | **插件动作分发(v0.78 新增,切片 PLG-2)**。⚠️ **读写面不同档**:路由走 `viewerAuth`,但**写动作在 handler 内按 owner/admin 收紧**(`form.submit` / `table.rowActions` 是写;`table`/`log` 的 `source` 是读,对 viewer 开放)。**只接受该插件声明过的动作名**(未声明 ⇒ `404 action_not_declared`,**不是**「没权限」)。请求体可选 JSON(空体 ⇒ `payload=null`);**宿主不接受任何来自声明的请求地址**(不给插件开 SSRF 面)。⚠️ **当前无生产执行器 ⇒ 所有已声明动作回 `501 action_not_implemented`**(见 §7.2)。错误码与响应形状见 §4.14(b) |
| A14 | POST | `/api/v1/admin/plugins/{id}/actions/{action}` | **插件动作分发(v0.78 新增,切片 PLG-2)**。⚠️ **读写面不同档**:路由走 `viewerAuth`,但**写动作在 handler 内按 owner/admin 收紧**(`form.submit` / `table.rowActions` 是写;`table`/`log` 的 `source` 与下拉 `optionsFrom` 是读,对 viewer 开放)。**只接受该插件声明过的动作名**(未声明 ⇒ `404 action_not_declared`,**不是**「没权限」)。请求体可选 JSON(空体 ⇒ `payload=null`);**宿主不接受任何来自声明的请求地址**(不给插件开 SSRF 面)。✅ **v0.79:生产执行器已接线**(切片 L1/PLG-2)—— 内置 `web-search` 的四个动作 + 候选来源 `plugin.providers` 都已登记生产处理器,**真可用**(真读写可变源、探针真发一次 HTTP);**只有「已声明但宿主未登记处理器」才回 `501 action_not_implemented`**(不再是兜底档)。错误码与响应形状见 §4.14(b)(c) |
> **A13 / A14 的角色门依据**(`routes.go` 紧邻注释):快照端点对 viewer 开放是因为它回的**本来就是被裁掉写面的只读投影**;动作端点的读来源动作对 viewer 可见,**写门落在 handler 内**(路由中间件只能有一道门)。`panels_changed` 帧(§3.4.5)**不进** `frameReachesAll` 白名单 ⇒ **viewer 收不到该帧**,需自行刷新/轮询重拉 A13。
### 2.2 owner/admin 读(14 条,`writeAuth`)
### 2.2 owner/admin 读(16 条,`writeAuth`)
| # | 方法 | 路径 | 要点 |
|---|---|---|---|
@@ -189,6 +193,8 @@
| B13 | GET | `/api/v1/admin/flow/graph` | **链路图快照 / 多轮历史回放(v0.71 新增;v0.73 增量;v0.77 增量)**,只读、不审计。`?sessionId=`(省略 = 主会话 `web`)+ **v0.73 新增 `?turnId=`**(省略 = 最近一轮,**先内存后落盘**)。响应 `{data:{sessionId,turnId,source,graph,lanes[],turns[]}}`:`graph` 是**一份 §3.4.5 `graph_update` 同形快照**,没有该轮时**如实为 `null`**(**绝不画假图**);`source` ∈ `live`(内存最近一轮)/`persisted`(落盘的历史轮)/`none`;**v0.77 新增 `lanes[]`**(**最近一轮每一路**的图,每项 `{turnId,graph}`,按路的开始时间升序,恒为非 null 数组 —— 真并行时这才是全貌,`graph` 只给最新一路),`graph` 的既有语义**一字未改** ⇒ 老读取方零改动;`turns[]`(`{turnId,createdAt}`,最新在前)供面板选历史轮(**v0.73**)。未接线图源 → `500 flow_unavailable`。语义见 §4.11 |
| B14 | POST | `/api/v1/admin/memories/tidy/dry-run` | **记忆整理「试算」(v0.75 新增,只读语义的 POST)**:跑同一套整理规则、把候选**直接回在响应里**,**零副作用**(不写任何表)。请求体可为空。响应 `{data:{dryRun:true,recallMode,durationMs,ruleVersion,partitions,scanned,compared,pairs,vetoed,vetoReasons,truncated?,candidates[]}}`;`candidates` 恒为非 null 数组(空数组 = 「算过了,没有建议」,与「没算出来」的错误响应**必须能区分**)。失败:未接线 → `500 tidy_unavailable`、配置关闭 → `409 tidy_disabled`、语义召回不可用 → `500 tidy_recall_unavailable`、超时 → `503 tidy_timeout`。**不审计**(只读计算)。语义与形状见 §4.12 |
| B15 | GET | `/api/v1/admin/memories/tidy/dry-run/{runId}` | **异步试算结果查询(v0.76 新增 · ✅ 已实现(v0.77 校正))**:配合 `POST …/memories/tidy/dry-run?async=1`(后者仍属 B14,`async=1` 时回 `202` + `{data:{runId,status:"running",startedAt}}`)轮询取回结果。响应 `{data:{status:"running"|"done"|"failed", result?, error?}}`:**`running` 时 `result`/`error` 两键都缺席**(绝不回一个空 `result` 冒充「跑完了但没有建议」);`done` 时 `result` 与 §4.12 同步响应的 `data` **逐字节同形**(**同一套 JSON,不是第二份**);`failed` 时 `error` 为 `{code,message}`(`code` 沿用 §4.12 的失败码)。⚠️ **结果只活在进程内存**(单二进制、零跨进程):TTL **30 分钟**(从**完成时刻**起算)、同时只允许 **1** 个在跑(**第二个请求复用既有 `runId`**,不引入新错误码);进程重启后旧 `runId` **如实失效** ⇒ `404 tidy_run_not_found`,**绝不假装还在跑**。鉴权与 B14 同档(`writeAuth` = owner/admin)、同样**不审计**。语义见 §4.12.1 |
| B16 | GET | `/api/v1/admin/memories/reviews` | **入库前审查(D3)留痕列表(v0.79 新增,切片 L45;✅ 已实现)**。`?limit=`:缺省 **100**、`≤0` 同缺省、> **500** 钳到 500、非整数 → `400 bad_request`(钳位不报错,与渠道事件查询面同一约定)。响应 `{data:[…]}`,元素键 camelCase:`id` / `itemId` / `personaId` / `scope` / `projectId`(可选)/ `decision` ∈ `skip\|merge\|modify\|create` / `reason`(可选,规则依据)/ `veto`(可选,命中的否决规则)/ `targetId`(可选,命中的既有条目)/ `mergedIds`(**恒为数组**)/ `score` / `recallMode`(可选)/ `degraded` / `ruleVersion` / `sessionId`(可选)/ `createdAt`(**UTC RFC3339**)。🔴 **不含记忆正文**(命中的 id 由客户端回表取原文)。未接线 → `500 memory_review_unavailable`(**绝不回空数组**假装没有留痕);读取失败 → `500 reviews_failed`。只读、不审计 |
| B17 | POST | `/api/v1/admin/memories/{id}/revert` | **撤销一次软合并(v0.79 新增,切片 L45;✅ 已实现;design/23 §8「回滚」)**。**无请求体**。200 `{data:{reverted:"<id>"}}`;该行不在 `merged` 状态(id 拼错 / 已回滚过)→ `404 not_found`;未接线 → `500 memory_review_unavailable`;存储失败 → `500 revert_failed`。**无损可逆**:被吸收行复原为 `active` + `merged_into=''`,并从存活行 `merged_from` 摘除、`merge_count` 减 1(下限 1);**行永不物理删除**。**落审计** `memory.review.revert`(who / result ∈ `ok\|error`;**只记 item id,不记正文**)。⚠️ 门是 `writeAuth`(owner\|admin);`design/23 §11` 表格对同族 `candidates/{id}/revert` 标的是 owner-only —— **口径差异待主会话裁决**(若以设计为准,路由换成 `ownerAuth` 即可) |
> **读取 `GET /admin/observability/{logs,metrics,trace}` 兼任可观测推送的订阅握手**(该凭据的连接全部断开即退订)。REST 响应形状不变。
@@ -314,7 +320,7 @@
{"type":"history","sessionId":"web","data":{"messages":[ <historyMessage> ... ]}}
```
- 内容 = 主会话最近 **50** 条(`wsReplayLimit`),`thinking` 恒缺席(见 §3.4.4 口径更正);每条**可能**带可选 `turnId`(v0.66,空则键省略)与可选 `voice`(**v0.67 登记、尚未实现**;形状见 §4.4)。
- 内容 = 主会话最近 **50** 条(`wsReplayLimit`),`thinking` 恒缺席(见 §3.4.4 口径更正);每条**可能**带可选 `turnId`(v0.66,空则键省略)。🔴 **`voice` 不在握手回放里**(v0.67 登记时曾计划覆盖):v0.79 的 `voice` 只填在 4 个 REST 读取面 + `message_edited`,握手 `history` 回放**不带该键**(见 §6 v0.79 ③ / §4.4 / §7.2)。
- **viewer 连接完成握手时完全不做回放**(其只读窗口是 REST 监控面)。
- 回放是尽力而为:存储未接或读出错静默跳过。
@@ -415,7 +421,7 @@
> **思维链的唯一出口是实时帧**:§3.4.2 的 `thinking.start` / `thinking.delta` / `thinking.end`(owner/admin 档)。客户端**不要**为「从历史里读回思维链」写任何代码。
> ⚠️ 本行**更正**本文档 v0.65 及更早的写法(旧版称「历史思维链只有 `GET /sessions/{id}/messages`(owner/admin)返回」)—— **那是文档写错了**,与实现不符。见 §7.3。
> 另注:管理面板人格清单里的 `thinking` 是**人格旋钮**(`GET /admin/personas` 的 `thinking` 块),与消息 DTO 的 `thinking` 不是同一个字段。
> **`voice` 覆盖同一批读取面(v0.67 登记,尚未实现)**:消息 DTO 新增的可选 `voice` 对象一次覆盖 `message_persisted` / `channel_persisted` / `message_edited` 与握手 `history` 回放(形状与语义见 §4.4)。**WS 协议本身零变化**:无新帧、无新上行字段;`voice` 只是既有 DTO 的新可选键。
> **`voice` 覆盖同一批读取面(v0.67 登记;🔴 **v0.79 只落地了其中一部分**)**:消息 DTO 的可选 `voice` 对象**设计上**一次覆盖 `message_persisted` / `channel_persisted` / `message_edited` 与握手 `history` 回放(形状与语义见 §4.4)。🔴 **v0.79 实测覆盖**:只有 `message_edited`(与 §4.4 列出的 4 个 REST 读取面)会带它;**`message_persisted` / `channel_persisted` 与握手 `history` 回放仍不带 `voice`**(补齐需给 `history.Message` 加字段,**未做**)⇒ 客户端**不要在实时帧 / 握手回放上等 `voice`**。**WS 协议本身零变化**:无新帧、无新上行字段;`voice` 只是既有 DTO 的新可选键。
#### 3.4.5 子代理 / 任务 / 系统帧(owner+admin)
@@ -470,21 +476,21 @@
| 状态 | code(示例) | 场景 |
|---|---|---|
| 400 | `bad_request`、`image_too_large`、`too_many_documents` | 请求体/字段非法、图片超限(`image` **字符串长度**,**仅 base64 / `data:` 形态会触发**,见 §4.5)、附件超条数 |
| 400 | `bad_request`、`image_too_large`、`too_many_documents`、`invalid_payload`(**v0.79**)、`unknown_provider`(**v0.79**)、`not_configured`(**v0.79**) | 请求体/字段非法、图片超限(`image` **字符串长度**,**仅 base64 / `data:` 形态会触发**,见 §4.5)、附件超条数;**插件动作的载荷形状错 / provider 名未登记 / 该家没配齐**(后三个码自 v0.79 起由动作处理器给出,见 §4.14(b)(c)) |
| 401 | `unauthorized`、`invalid_grant` | 无/坏凭据;refresh token 非法 |
| 403 | `forbidden`、`unknown_role`(**v0.78**,面板快照的角色无法识别,理论不可达、fail-closed) | 角色不足;主会话不可删;**插件写动作的调用者不是 owner/admin**(v0.78,见 §4.14(b)) |
| 404 | `not_found`、`session_not_found`、`document_not_found`、`account_not_found`、`media_not_found`(**v0.67 登记、尚未实现**)、`tidy_run_not_found`(**v0.77 登记、已实现**)、`action_not_declared`(**v0.78 登记、已实现**) | 资源缺失;消息列表的会话不存在;文档缺失(含名字非法/穿越);渠道账号 id 不存在(C19/C20);**语音副本缺失**(含名字非法/穿越,三者同一响应,见 §4.5);**异步试算的 `runId` 未知 / 超 TTL / 进程重启后查不到**(三种原因刻意不区分,见 §4.12.1);**插件动作名未在该插件声明里出现过**(**不是**「没权限」,见 §4.14(b)) |
| 404 | `not_found`、`session_not_found`、`document_not_found`、`account_not_found`、`media_not_found`(**v0.67 登记、v0.79 已实现**)、`tidy_run_not_found`(**v0.77 登记、已实现**)、`action_not_declared`(**v0.78 登记、已实现**) | 资源缺失;消息列表的会话不存在;文档缺失(含名字非法/穿越);渠道账号 id 不存在(C19/C20);**语音副本缺失**(含名字非法/穿越,三者同一响应,见 §4.5);**异步试算的 `runId` 未知 / 超 TTL / 进程重启后查不到**(三种原因刻意不区分,见 §4.12.1);**插件动作名未在该插件声明里出现过**(**不是**「没权限」,见 §4.14(b));**回滚的记忆条目不在被合并状态**(`not_found`,v0.79,见 §2.2 B17) |
| 409 | `chat_busy`、`account_not_registered`、`plugin_disabled`(**v0.78**) | 待处理队列满;渠道账号已配置但没有已注册适配器(C19/C20,v0.66);**插件已被显式停用,其声明还在但入口与动作都下线**(v0.78,见 §4.14(b)) |
| 413 | `payload_too_large`、`document_too_large` | 上传/文档超限 |
| 415 | `unsupported_media_type` | 图片/文档类型拒绝 |
| 429 | `rate_limited` | 限流(框架级) |
| 500 | `internal`、`chat_unavailable`、`chat_failed`、`voice_unavailable`、`stt_failed`、`tts_failed`、`tts_empty`、`voice_clone_failed`、`usage_unavailable`、`storage_unavailable`、`channel_events_unavailable`(**v0.67 已实现**,见 §2.4(a))、`refresh_unavailable`、`sessions_failed`、`messages_failed`、`session_failed`、`message_failed`、`document_upload_failed`、`login_failed` | 内部错误或依赖未接线(**绝不伪装成功**) |
| 501 | `not_implemented`、`panels_unavailable`(**v0.78**)、`action_not_implemented`(**v0.78**) | HistoryStore 等未注入;**插件面板中枢未接线**(见 §4.14(a));**动作已声明、已启用,但宿主没接线执行器** —— ⚠️ **当前生产路径上所有已声明动作都是这一档**(见 §4.14(b) / §7.2) |
| 502 | `action_failed`(**v0.78**) | **插件动作执行器返回错误**(结构化错误码 + 文案,详细原因只进后端日志),见 §4.14(b) |
| 500 | `internal`、`chat_unavailable`、`chat_failed`、`voice_unavailable`、`stt_failed`、`tts_failed`、`tts_empty`、`voice_clone_failed`、`usage_unavailable`、`storage_unavailable`、`channel_events_unavailable`(**v0.67 已实现**,见 §2.4(a))、`refresh_unavailable`、`sessions_failed`、`messages_failed`、`session_failed`、`message_failed`、`document_upload_failed`、`login_failed`、`memory_review_unavailable`(**v0.79 新增**,切片 L45:入库前审查留痕存储未接线 —— 列表与回滚**共用**)、`reviews_failed`(**v0.79**:读留痕失败)、`revert_failed`(**v0.79**:回滚写失败)、`config_unreadable`(**v0.79**:面板可变源存在但读不出来,**不覆盖**原文件)、`config_write_failed`(**v0.79**:可变源写不进去,本次改动**没有**生效) | 内部错误或依赖未接线(**绝不伪装成功**) |
| 501 | `not_implemented`、`panels_unavailable`(**v0.78**)、`action_not_implemented`(**v0.78**) | HistoryStore 等未注入;**插件面板中枢未接线**(见 §4.14(a));**动作已声明,但宿主没有登记它的处理器** —— 🔴 **v0.79 起不再是兜底档**:内置 `web-search` 的动作已全部登记(见 §4.14(b)(c)),这条只在「声明写了却没实现」时出现 |
| 502 | `action_failed`(**v0.78**)、探针码 `unauthorized` / `rate_limited` / `quota_exceeded` / `network` / `upstream` / `bad_response` / `no_results` / `unknown`(**v0.79**) | **插件动作执行失败**(结构化错误码 + 文案,详细原因只进后端日志);后一组是 `plugin.testProvider` 探针的**上游结论**(其中 **`no_results` = 连通正常但没查到结果**),见 §4.14(b)(c) |
| 503 | `timeout`、`chat_dispatch_failed`、`catalog_unavailable` | 派发/上游不可用 |
> **未确证**:`conflict`(409) 的通用码存在(框架级 `ErrConflict`),但除了 `chat_busy` 之外,本文档未逐一确证还有哪些端点会带它。
> **⚠️ `416` 不是错误信封(v0.67 登记,尚未实现)**:`GET /api/v1/media/{name}` 的 `Range` 语法非法或范围不可满足时,**由 Go 标准库直写** `416`(`text/plain` + `Content-Range: bytes */<size>`),**不套** `{"error":{…}}` ⇒ 客户端**不要**拿 `error.code` 去解析它(见 §4.5)。这是字节流端点的既有事实(`/audio/speech` 的 `200`、`/documents/{name}` 的 `200` 同样不是 JSON 信封),**不是**本版新引入的怪癖。
> **⚠️ `416` 不是错误信封(v0.67 登记,**v0.79 已实现**)**:`GET /api/v1/media/{name}` 的 `Range` 语法非法或范围不可满足时,**由 Go 标准库直写** `416`(`text/plain` + `Content-Range: bytes */<size>`),**不套** `{"error":{…}}` ⇒ 客户端**不要**拿 `error.code` 去解析它(见 §4.5)。这是字节流端点的既有事实(`/audio/speech` 的 `200`、`/documents/{name}` 的 `200` 同样不是 JSON 信封),**不是**本版新引入的怪癖。
> **`400` 的渠道事件用法(v0.67 已实现,见 §2.4(a))**:`GET /admin/channels/events` 对 `platform` 未知、`tier` 非法、`since`/`until` 跨度 > 90 天、`since`/`until` 时间点非法、`limit`/`offset` 非整数都回 `400`(**落地用的 `code` 是通用的 `bad_request`**,见 §7.4-11)。
### 4.2 分页与游标
@@ -536,7 +542,7 @@
{ "name": "web_search", "status": "ok", "durationMs": 1200,
"argsSummary": "{\"query\":\"明天天气\"}", "resultSummary": "晴,18~26℃" }
],
"voice": { // v0.67 登记,**尚未实现**(用户可见面;见 §7.2);非语音消息键缺席;🎤 判据 = 本键存在
"voice": { // v0.67 登记,v0.79 已实现(切片 L45);非语音消息键缺席;🎤 判据 = 本键存在
"source": "asr", // "asr"(服务端识别,含云端)| "platform"(渠道平台自带转写)
"status": "transcribed", // "transcribed" | "empty" | "failed"(闭集三态)
"name": "a1b2c3….ogg", // 音频副本存储名;无副本 ⇒ 键省略
@@ -569,10 +575,11 @@
- 单条消息最多 **20** 条(超出按**图序**取前 20 条)。
- 🔴 **缺席语义(契约核心)**:`toolCalls` 出现 ⇔ 这条消息所属的那一轮**确实调用过工具**;图取不到(超保留期 / 该轮未落盘 / 隔离渠道会话从不入库 / **该路标识查不到图** / 图源未接线)**以及**这一轮没调过工具,**都是键缺席**,**绝不回 `[]` 冒充「没调过工具」** ⇒ 客户端只做**正向断言**(有键 ⇒ 调过工具),**不得**把「没有这个键」读成「没调过工具」。
- 数据源接线:`Deps.FlowGraphs`(`*flowgraph.SQLStore`)为 nil 时**只是没有这一面** —— 端点仍 `200`、正文一字不变(**不**像管理端点那样回 `500 flow_unavailable`)。查图失败只记一条带来源的 WARN 并让字段缺席(**不让可视化存档的故障把整页历史变成 500**)。
- 🔴 **覆盖率受限(如实,v0.78)**:只有**真并行路**(上行 `chat.parallel` 且照准)的图带权威 `turnId` ⇒ **只有这些回复能回看到工具**;**普通单路回合**(Web 单路聊天、队首续跑)的图**当前不带** `turnId`,落盘后 `flow_graphs.lane_turn_id` 为空 ⇒ 查不到图 ⇒ `toolCalls` **缺席**。这是契约允许的「图取不到」,但不是需求本意;补齐需落在 `internal/runtime/loop/**`(写侧),**不在本版**(见 §7.2、§7.4-23)。
- ✅ **覆盖率已补齐(v0.79,切片 CODE-TC-2)**:编排器驱动的**每一路**(含**普通单路回合**与**队首续跑路**)的图现在都带权威 `turnId`(= 该轮消息行的 `chat_messages.turn_id`)⇒ **这些回复都能回看到工具**(此前只有真并行路可以)。仍**缺席**的只剩两类:① **拿不到权威标识的写入路径** —— 未接编排器的调用方、**后台思考**(`sess-backthink` 在发布会话白名单内,但它的 ctx 上没有权威标识)、旧后端;② **v0.79 之前落盘的历史单路行**(`lane_turn_id` 为空且**无可回填来源** —— 那时的图里根本没存这个标识,用会话 id / 时间戳 / 消息表 join 去补都是**编造身份**)。见 §7.2、§7.4-23、§7.4-25。
- 🔴 **一处形状变化(v0.79)**:`graph.id` 的 `#{回合标识}` 后缀现在**单路也追加**(`web-7` → `web-7#turn-…`)—— 客户端若要给 `GET /admin/flow/graph?turnId=` 取值,**一律用响应里的 `graph.id` / `turns[].turnId`**,**不要自己拼** `{会话}-{序号}`(老客户端自拼的 `web-7` **查不到新行**)。见 §4.11(a)(d)。
- 新增的读侧依赖:`FlowGraphStore` 增只读方法 `GraphsByTurnID(ctx, sessionID, laneTurnIDs)`(一页一次 `IN` 批量查,禁 N+1);新表列 `flow_graphs.lane_turn_id`(迁移 `0023`,等值索引 `idx_flow_graphs_lane_turn_id (session_id, lane_turn_id)`)。**老行**(v0.77 之前的落盘)该列回填后仍为空 ⇒ 如实查不到。
**`voice` 语义(v0.67 登记;🔴 **用户可见面尚未实现** —— 见 §7.2)**:
**`voice` 语义(v0.67 登记;✅ **v0.79 已实现**,切片 L45 —— 覆盖率见本节末)**:
- 形状:`{source,status,name?,mime?,size?,durationMs?,url?}`,`omitempty`(**非语音消息键缺席**,与引入语音前逐字相同)。
- **🎤 判据 = `voice` 键存在**(这条消息来自一条语音消息)。客户端**绝不**从 `content` 正文里认媒体类型:QQ 语音正文里那个 `[语音]` 占位标记自 v0.67 起**不再下发**(见 §6 的 v0.67 行为变更),正文里也可能出现用户自己打出的 `[语音]` 字样。**只认 `voice` 键**。
@@ -584,6 +591,8 @@
- 落库形状 = DTO 形状**减去** `url`(列 `chat_messages.voice TEXT NOT NULL DEFAULT ''`,JSON **单对象**;**用空串不用 NULL**,与 `turn_id` 同一口径)。音频原件**复用文档那一套存储 / 元数据表 / 引用计数 / GC**(同一 `<DataDir>/uploads/documents` 目录),但**不进文档的抽取 / 注入链路**:不被抽取、不进 `documents` DTO 数组、不占注入预算、不出现在附件清单行里。
- **一条消息只落一份音频副本**(= 被转写的那一条语音);同一条消息里的第 2..N 条语音既不转写也不落盘,DTO 里也没有它们(**刻意的边界**,不是遗漏)。
- ⚠️ **微信(`weixin-ilink`)语音没有音频副本、不可回放**:按既有裁决「平台自带转写就不白下载那份音频」⇒ `source:"platform"` 且**无 `name`/`mime`/`size`/`url`**,只保留 🎤 标记。
- ✅ **覆盖率(v0.79,如实)**:`voice` 已在**这 4 个读取面**填充 —— `GET /sessions/{id}/messages`、`GET /admin/channels/messages`、`GET /admin/channels/{platform}/messages`、`PATCH /messages/{id}` 的**响应与 `message_edited` 广播帧**。🔴 **尚未填充**:`message_persisted` / `channel_persisted`(`broadcast.go`)与握手 `history` 回放(`ws_history.go`)—— 需要给 `history.Message` 加 `voice` 字段(**未做**);且入站语音的 `voice` 列是**消息落库之后**回填的,`message_persisted` 那一帧在**落库那一刻**发出,即使加了字段也拿不到值。⇒ **客户端不要在实时帧上等 `voice`**(见 §7.2、§7.4-29)。
- ⚠️ **列被外部改坏时可见地缺席**:非法 JSON / 闭集破坏时**不静默当「不是语音」** —— 记一条带来源与消息 id(**不含正文**)的 WARN,并让该字段缺席。契约的 `voice` 没有「读不出」这一态,因此缺席是唯一诚实的表达(与 `documents` 回一条 `unreadable` 不同)。
**`documents` 数组语义(核心)**:
@@ -637,11 +646,13 @@
- WS 语音回合(`voice_input`):**与文本回合走同一条路**(进编排器 → 落库 → 流式 → 跨端同步 → 忙碌/排队语义一致)。转写文本落库为用户消息、助手回复落库;`deviceId`/`clientMsgId` 与 `chat` 帧同形。
- `voice_output` 在**该回合结算时**才定向回发起连接(可能晚于若干状态帧,甚至由续跑回合或被 steer 注入的那一回合产生)。空正文 / TTS 失败/降级都走 `provider` 降级信号(`silent` 等),**不把 `stopReason` 当语音内容**;连接断开不补投,客户端带回同一 `clientMsgId` 可安全重试。
**入站语音的音频副本与播放端点(v0.67 登记;🔴 **用户可见面尚未实现**;v0.78 只落地了「落库列 + 入站归一链」)**:
**入站语音的音频副本与播放端点(v0.67 登记;✅ **v0.79 已实现**,切片 L45;残留缺口见下)**:
> ⚠️ **v0.78 精确进展(切片 CODE-VOICE,如实登记)**:**已实现**——① 迁移 `0020_chat_message_voice` 给 `chat_messages` 加 `voice TEXT NOT NULL DEFAULT ''` 列(JSON 单对象,形状 = 下面的 DTO `voice` **减去 `url`**);② `internal/runtime/voice/**` 新包(`Source`/`Status` 闭集、`Ref` 编解码、格式级可播表 + `amr→wav` 转码归一);③ `channelbridge` 入站语音「归一(最前端)→ 闸门**之后**落副本 → 落库后写 `voice` 列」全链打通(落盘走内容寻址、复用文档存储与 GC,但**不进文档抽取/注入链路**)。**尚未实现/未接线**——`documents.SaveAudio`(音频存储面)、`history` 的 `voice` 列写视图、组合根注入转码器 `Options.VoiceTranscoder` **三者都还缺** ⇒ 生产上**副本落不进盘、`voice` 列写不进库**(都走**如实的 WARN**,不是假成功);**网关侧的 DTO `voice`(含 `url`)与 `GET /api/v1/media/{name}` 端点仍未实现**。⇒ **客户端一律按「未实现」对待**,不要提前按可用编程(见 §7.2)。唯一已生效的**客户端可见**变化是:IM 语音在「音频格式不在可转写白名单内」时正文诚实提示由「…(需要先转码,当前未做)」改为「…(服务端只把白名单内的音频送语音识别),这条语音没有转写。」——**形状不变**(正文仍是转写文本或一行诚实提示;客户端本就**不该**按正文文案分支)。
> ✅ **v0.79 落地进展(切片 L45,如实登记)**:**已实现** —— ① `documents.Store.SaveAudio`(音频白名单 / 内容寻址 / 同字节去重 / 目录 0700、**文件 0600** / 元数据写同一张 `documents` 表、引用计数并入同一套 `documents.ref_count`);② `history.Store.UpdateMessageVoice` + `VoiceColumns`(`chat_messages.voice` 列的**读写两条路**)⇒ channelbridge 的入站副本链**端到端可落地**;③ **网关 DTO `voice`(含唯一派生字段 `url`)** 与 **`GET /api/v1/media/{name}`**(§1.2 U19 / 下表)。🔴 **残留缺口(如实,不美化)**:① 组合根**未注入 `VoiceTranscoder`**(`cmd/cyrene/wire_channels.go` 少一行 `VoiceTranscoder: a.transcoder,`)⇒ **AMR 不落副本**(走 `category=transcode_not_wired` 的诚实 WARN);**可播格式(ogg/wav/mp3/m4a/aac/flac/webm/oga/opus)照落**;② **WS 实时帧 / 握手回放不带 `voice`**(见 §3.4.4 与 §4.4 覆盖率);③ **消息 / 会话被删除时音频副本的引用计数不被释放**(`refs.go` 的增减点只认 `document` 列)⇒ 副本 `ref_count` **偏高**(后果是**晚回收 / 不回收**,绝不早删);④ **生产端到端未跑**(禁止重启后端)⇒ 「真机 IM 语音 → 盘上 0600 副本 → 客户端取到字节」只有替身 + 临时库/临时目录级证据。
- **音频副本何时产生**:渠道适配器入站拿到语音字节后,转写在**流水线最前端**归一(见上),而**落盘在闸门之后** —— 被会话解析 / 黑白名单 / 最小打扰拦下的语音**盘上不留任何文件**(与 IM 附件同一纪律)。落盘 = 内容寻址(`sha256(bytes)[:32] + ext`,同字节同扩展名天然去重),产出该消息的 `voice` 引用(DTO 见 §4.4)。**不可播格式(`amr`)在落盘之前先转成可播副本(`amr→wav`)**;转码不可用时**不落副本、不打假 url、消息一个字不丢**(带稳定 `category` 的 WARN;10 类:`no_bytes`/`not_playable`/`transcode_not_wired`/`unsupported_format`/`input_too_large`/`output_too_large`/`decode_failed`/`encode_failed`/`timeout`/`busy`)。
> **v0.78 已落地的基础件(切片 CODE-VOICE;v0.79 已在其上补齐用户可见面)**:① 迁移 `0020_chat_message_voice` 给 `chat_messages` 加 `voice TEXT NOT NULL DEFAULT ''` 列(JSON 单对象,形状 = 下面的 DTO `voice` **减去 `url`**);② `internal/runtime/voice/**` 新包(`Source`/`Status` 闭集、`Ref` 编解码、格式级可播表 + `amr→wav` 转码归一);③ `channelbridge` 入站语音「归一(最前端)→ 闸门**之后**落副本 → 落库后写 `voice` 列」全链打通(落盘内容寻址、复用文档存储与 GC,但**不进文档抽取/注入链路**)。唯一已生效且**客户端可见**的既有变化:IM 语音在「音频格式不在可转写白名单内」时正文诚实提示由「…(需要先转码,当前未做)」改为「…(服务端只把白名单内的音频送语音识别),这条语音没有转写。」—— **形状不变**(正文仍是转写文本或一行诚实提示;客户端本就**不该**按正文文案分支)。
- **音频副本何时产生**:渠道适配器入站拿到语音字节后,转写在**流水线最前端**归一(见上),而**落盘在闸门之后** —— 被会话解析 / 黑白名单 / 最小打扰拦下的语音**盘上不留任何文件**(与 IM 附件同一纪律)。落盘 = 内容寻址(`sha256(bytes)[:32] + ext`,同字节同扩展名天然去重),产出该消息的 `voice` 引用(DTO 见 §4.4)。**不可播格式(`amr`)在落盘之前先转成可播副本(`amr→wav`)**;转码不可用时**不落副本、不打假 url、消息一个字不丢**(带稳定 `category` 的 WARN;10 类:`no_bytes`/`not_playable`/`transcode_not_wired`/`unsupported_format`/`input_too_large`/`output_too_large`/`decode_failed`/`encode_failed`/`timeout`/`busy`)。⚠️ **v0.79 现状**:组合根**未注入 `VoiceTranscoder`** ⇒ `amr` 走 `transcode_not_wired`,**当前不落副本**(ogg/wav/mp3/m4a/aac/flac/webm/oga/opus 等可播格式照落)。
- **播放端点 `GET /api/v1/media/{name}`**(**owner/admin,必须 Bearer**,与 `GET /api/v1/documents/{name}` 同一套门):
| 项 | 口径 |
@@ -661,7 +672,7 @@
|---|---|---|---|---|
| `GET /api/v1/files/{name}` | **无**(能力 URL) | `inline`(按图 MIME) | 128-bit 随机名 + 图片扩展名 | 图片,给 `<img>` / provider 直接吃 |
| `GET /api/v1/documents/{name}` | **Bearer owner/admin** | `attachment` | 32 hex + **文档白名单**(23 项) | 文档,**永不内联渲染** |
| `GET /api/v1/media/{name}`(v0.67 登记) | **Bearer owner/admin** | `inline; filename=…` | 32 hex + **音频白名单**(10 项) | 语音副本,供播放器消费 |
| `GET /api/v1/media/{name}`(v0.67 登记 · **v0.79 已实现**) | **Bearer owner/admin** | `inline; filename=…` | 32 hex + **音频白名单**(10 项) | 语音副本,供播放器消费 |
- ⚠️ **音频 URL 不能直接贴进 `<audio src>`**:读侧要 Bearer(与图片读侧「128-bit 名即凭据」**刻意相反** —— 语音是私密内容,不是「谁拿到 URL 谁就能听」)⇒ 客户端须**带 token 取字节**(Web:`fetch` → `blob:` → 播放;原生:认证 HTTP 客户端)。这是**有意**的设计,不是缺陷。
- ⚠️ **可播放性未逐一实测**:`amr` / `flac` / `opus` 在部分浏览器内核上播不了 ⇒ 客户端**以实测为准**,播不了就照实提示(不假装;**绝不给一个播不了的 `url`**)。「能播」必须是**格式级**结论,不得靠「扩展名看起来像音频」就宣称可播。
@@ -809,7 +820,7 @@
|---|---|
| 方法 / 路径 | `GET /api/v1/admin/flow/graph` |
| 鉴权档 | `writeAuth`(**owner / admin**;`viewer` → `403`,未认证 → `401`)。与 `design/19 §3`「flow 可视化仅 owner/admin」一致 |
| 查询参数 | `sessionId`(**可选**)。省略 / 空串 ⇒ 主会话 `web`(`internal/runtime/history.MainSessionID`)。其它取值就是该会话 id,如 `sess-backthink`(后台思考,design/20 §2)、`web:<uuid>`(次级会话)。**v0.73 新增** `turnId`(**可选**):省略 / 空串 ⇒ **最近一轮**(**先查内存 `loop.FlowPublisher.LastGraph`,miss 才回退落盘** ⇒ 最新一轮永远是最新的);给了就是该会话已落盘的某一轮(形如 `web-7`,取自 `turns[].turnId` / `graph.id`) |
| 查询参数 | `sessionId`(**可选**)。省略 / 空串 ⇒ 主会话 `web`(`internal/runtime/history.MainSessionID`)。其它取值就是该会话 id,如 `sess-backthink`(后台思考,design/20 §2)、`web:<uuid>`(次级会话)。**v0.73 新增** `turnId`(**可选**):省略 / 空串 ⇒ **最近一轮**(**先查内存 `loop.FlowPublisher.LastGraph`,miss 才回退落盘** ⇒ 最新一轮永远是最新的);给了就是该会话已落盘的某一轮 —— **取值一律用响应里的 `turns[].turnId` / `graph.id`**(🔴 **v0.79 起单路也带 `#{回合标识}` 后缀**,形如 `web-7#turn-3f9c…`;**不要自己拼** `{会话}-{序号}`),且该值**可能含 `#`**、作 query 值必须 URL 编码 |
| 响应体 | `{data:{sessionId, turnId, source, graph, lanes, turns}}`(**v0.73**;v0.71 只有 `{sessionId,graph}`;**v0.77 新增 `lanes`**)。`sessionId` = 解析后的会话;`turnId` = **实际返回的那一轮**的标识(无图时 `""`);`source` ∈ `"live"`(内存最近一轮)/`"persisted"`(落盘的历史轮)/`"none"`(没有图);`graph` = **该轮**的图或 `null`;`lanes` = **最近一轮每一路**的图 `[{turnId,graph}]`,按路的开始时间升序(最早的在前)—— 与 `?turnId=` **无关**(翻历史轮时 `lanes` 仍报「当前这一轮的各路」,`graph` 才是你指定的那一轮),恒为非 null 数组(图源未接线 / 没有路时是 `[]`);`turns` = 该会话**已落盘轮次**摘要 `[{turnId,createdAt}]`,**最新在前**(未接线持久化或没有轮次时是 `[]`,恒为非 null 数组) |
| `graph` / `lanes[].graph` 形状 | **与 `graph_update` 帧的 `data` 逐字同形**(§3.4.5:`{id,nodes[],edges[],sessionId,turnId?,round?,startedAt,endedAt}`,节点 `props`/`log` 同 §1.1 的脱敏口径)。`turnId` / `round` 为 **v0.77 新增的可选键**(缺席语义见下方 (d)) |
| 归档 | **不审计**(只读查询,与同档的 `/admin/llm`、`/admin/config-facts` 一致) |
@@ -832,15 +843,15 @@
#### (d) `turnId` / `round` 的增量键与**缺席语义**(v0.77)
`graph_update` 帧的 `data` 与本端点返回的每张图(`graph` / `lanes[].graph`)**同一个结构**;v0.77 起多两个**可选**键(老客户端忽略未知键仍可用;单路时两个键的行为与 v0.75 逐字相同):
`graph_update` 帧的 `data` 与本端点返回的每张图(`graph` / `lanes[].graph`)**同一个结构**;v0.77 起多两个**可选**键(老客户端忽略未知键仍可用)。⚠️ **v0.79 变化**:`turnId` **不再只对真并行路出现** —— 编排器驱动的**每一路**(含**普通单路回合**与队首续跑路)都带它,且 `graph.id` 据此对单路也追加 `#{回合标识}`(见下);`round` 的语义**一字未改**。
| 键 | 类型 | 语义 |
|---|---|---|
| `turnId` | string | 这张图所属**路**的权威回合标识(与 `turn_begin` / `steer_ack` / 流帧帧壳的 `turnId` **同值**,design/15 §2.5.9)。**缺席**语义:这一路**没有权威标识**(单路的历史帧、未接编排器的调用方、旧后端)—— 此时客户端按「该会话那一张图」处理,**不得**臆造一个 id。⚠️ 它**不是** `?turnId=` 的参数(后者是 `graph.id`,两者不同源,见 §7.4-23) |
| `turnId` | string | 这张图所属**路**的权威回合标识(与 `turn_begin` / `steer_ack` / 流帧帧壳的 `turnId` **同值**,design/15 §2.5.9)。**缺席**语义:这一路**没有权威标识**(未接编排器的调用方、**后台思考 `sess-backthink`**、旧后端,以及 **v0.79 之前的单路历史帧**)—— 此时客户端按「该会话那一张图」处理,**不得**臆造一个 id。⚠️ **v0.79 起**编排器驱动的**每一路**(含**普通单路回合**与队首续跑路)都带该键 ⇒ 它**不再只对真并行路出现**。⚠️ 它**不是** `?turnId=` 的参数(后者是 `graph.id`,两者不同源,见 §7.4-23) |
| `round` | number | 这张图所属的**会话轮次号**(每会话单调;同一条消息引出的多条路**共享**同一个值)。客户端据此只保留最新一轮的泳道、丢掉更早轮次的路。**缺席**(`0`)语义:旧后端 / 未设置 —— 客户端保守处理(保留已收到的路,**不按轮次淘汰**),**不得**臆造轮次号 |
- 真并行(上行 `chat.parallel`)时同一会话**每一路各发自己的帧**(`data.id` / `data.turnId` 各不相同),因此两路不再互相覆盖;每路的图也**各占一行落盘**(`flow_graphs` 的唯一键 `(session_id, turn_id)` 因此不再把另一路整行顶掉)。
- `graph.id` 的形状自 v0.77 起是 `{会话}-{序号}[#{路}]`(序号是发布器**进程内**的建图序号、重启即从头开始;路标识只在有权威标识时追加)—— 它是 `?turnId=` 的参数值,**不透明**,且**可能含 `#`**,作为 query 值必须 URL 编码(Web 前端用 `URLSearchParams`,已正确编码)。
- `graph.id` 的形状自 v0.77 起是 `{会话}-{序号}[#{权威回合标识}]`(序号是发布器**进程内**的建图序号、重启即从头开始)。🔴 **v0.79 起**后缀对**单路也追加**(此前只对真并行路)—— 即**每一条编排回合**的 `graph.id` 都含 `#`(`web-7` → `web-7#turn-3f9c…`);**老行**(v0.79 之前落盘的**单路**行)仍是 `{会话}-{序号}`。它是 `?turnId=` 的参数值,**不透明**,**可能含 `#`** ⇒ 作 query 值必须 URL 编码(Web 前端用 `URLSearchParams`,已正确编码),且**客户端一律用响应给出的值、不要自拼**。
---
@@ -1010,36 +1021,47 @@
| 动作名未在声明里出现过 | 404 | `action_not_declared` | §5.2.1「只接受该插件声明过的动作名」;**未声明不是「没权限」** |
| 插件已被显式停用 | 409 | `plugin_disabled` | 声明还在,入口与动作都下线 |
| `kind=write` 且调用者不是 owner/admin | 403 | `forbidden` | 写面 = `form.submit` + `table.rowActions`(§5.4.1) |
| 已声明、已启用,但宿主没接线执行器 | 501 | `action_not_implemented` | ⚠️ **当前生产路径就是这一档**(见下) |
| 已声明,但宿主**没有登记它的处理器** | 501 | `action_not_implemented` | ✅ **v0.79 起不是兜底档**:内置 web-search 的动作已全部登记 ⇒ 这条只在「声明写了却没实现」时出现 |
| 请求体不是合法 JSON,或超过 1 MiB | 400 | `bad_request` | 绝不把坏体当空体继续跑 |
| 执行器返回错误 | 502 | `action_failed` | 结构化错误码 + 文案(§5.4.6),详细原因只进后端日志 |
| 载荷形状不对(缺 `values` / 缺 `provider` / 类型错) | 400 | `invalid_payload` | **v0.79**;由动作处理器给出(`pluginmgr.ActionCodeInvalidPayload`) |
| provider 名本期未登记 | 400 | `unknown_provider` | **v0.79**;`plugin.saveProvider` / `plugin.testProvider` / `plugin.reauthProvider` |
| 该家没配齐(缺 key 或缺接口地址) | 400 | `not_configured` | **v0.79**;`plugin.testProvider` |
| 可变源存在但读不出来(损坏/权限) | 500 | `config_unreadable` | **v0.79**;**不覆盖**原文件 |
| 可变源写不进去(磁盘/权限) | 500 | `config_write_failed` | **v0.79**;本次改动**没有**生效 |
| 探针(`plugin.testProvider`)的上游结论 | 502 | `unauthorized` / `rate_limited` / `quota_exceeded` / `network` / `upstream` / `bad_response` / `no_results` / `unknown` | **v0.79**;`no_results` 的文案明说「连通正常但没查到结果」 |
| 其它执行失败(内部错误) | 502 | `action_failed` | **兜底档**;详细原因只进后端日志 |
**请求体**:可选 JSON。**空体 ⇒ `payload=null`**(读来源动作通常无参)。**原样交给执行器**,宿主不解释插件的数据形状、**不认识任何 URL**(**不给插件开 SSRF 面**)。
**响应 `data`**:`{pluginId,action,kind,result}` —— `kind ∈ {read,write}`(`read` = `table`/`log` 的 `source`;`write` = `form.submit` / `table.rowActions`;同名动作同时出现在读面与写面时**一律按 write**);`result` = 执行器返回值**原样透传**(未接线时该键不存在)。
**响应 `data`**:`{pluginId,action,kind,result}` —— `kind ∈ {read,write}`(`read` = `table`/`log` 的 `source` **与字段下拉的 `optionsFrom`**;`write` = `form.submit` / `table.rowActions`;同名动作同时出现在读面与写面时**一律按 write**);`result` = 动作处理器返回值**原样透传**。
**审计**:**每一次结算都落一行**,`what = "plugin.action"`,`result ∈ {ok,error,denied,not_declared,unimplemented,unavailable,bad_request}`。⚠️ 持久化审计记录仍只有 `who/what/when/result` 四字段 ⇒ **未含 pluginId / action**(它们只在**结构化日志行**里,见 §7.2)。
**审计**:**每一次结算都落一行**,`what = "plugin.action"`,`result ∈ {ok,error,denied,not_declared,unimplemented,unavailable,bad_request}`。⚠️ 持久化审计记录仍只有 `who/what/when/result` 四字段 ⇒ **未含 pluginId / action**(它们只在**结构化日志行**里,见 §7.2);**v0.79 新增**:服务端日志行附加结构化失败码 `code` —— **只进日志,不进 DB 记录体、不含请求体**。
> 🔴 **当前生产路径的硬事实(v0.78,不美化)**:**没有任何生产实现的动作执行器**(`Panels.SetExecutor` 只在测试里被调用)⇒ **所有已声明动作都回 `501 action_not_implemented`** —— 搜索提供商页的**表格拉不到数据**(`plugin.listProviders` 是读来源动作)、表单保存不了。**客户端不得把「端点返回某个状态码」当成「数据可用」**:只有拿到 `result` 才算动作真的执行了。见 §7.2。
> ✅ **v0.79 的生产路径事实(切片 L1/PLG-2 执行面接线)**:组合根在 `wirePanelHub` 里调用 `wireBuiltinPluginActions`,把内置 `web-search` 的**四个动作 + 下拉候选来源**登记到面板中枢 ⇒ **这些动作真的执行**(真读写可变源、探针真发一次 HTTP),**不再一律 501**;装配期另把「**声明了却没有实现**」的动作与候选来源各记一条 WARN(`action_not_implemented` 因此是**缺口信号**而非常态)。⚠️ **残留(如实,见 §7.2)**:面板保存后**不热替换运行中的 web-search 工具**(改的是可变源文件,工具仍用启动期接线 ⇒ 需重启生效,与面板文案一致);探针**只用 `httptest` 假上游**验证过,**未对真实上游做过一次连通验证**。
#### (c) `web-search` 插件的动作面(v0.78,切片 PLG-4;供上面 (b) 的分发面使用)
#### (c) `web-search` 插件的动作面(v0.78 声明,**v0.79 起真可用**,切片 PLG-4 / L1;供上面 (b) 的分发面使用)
插件 id `web-search`;可变源落点 `$DATA_DIR/config/plugins/web-search.json`(**0600**、temp+rename)—— ⚠️ **不进客户端契约**(客户端一律走动作端点,不直连文件)。组合根「**先读可变源、缺失回落 `.env` 播种**」:文件一旦落盘,`.env` 的 `WEB_SEARCH_*` **只在文件缺失时**起作用(⚠️ **运维视角的行为变更**,见 §5)。🔴 **不新增任何 env 键**(`.env` 已冻结为引导面;本切片未改 `.env` / `.env.example`)。
`plugin.listProviders` 返回的表格行(`rows[]` 元素):`{ "provider": "bocha", "state": "ok", "enabled": true }`(**凭据与地址不回**)。`state` 取值(闭集)与判定顺序(根因在前):`unknown`(名字本期没登记)→ `disabled`(显式停用)→ `not_configured`(名字已登记但没配齐:云家缺 key / searxng 缺地址)→ `ok`(其余)。⚠️「凭据失效」**不是** `state` 取值(它只能由一次真实调用得知,属 `plugin.testProvider` 的结论)。
`plugin.listProviders` 返回的表格行(`rows[]` 元素):`{ "provider": "bocha", "state": "ok", "enabled": true }`(**凭据与地址都不回**)。`state` 取值(闭集)与判定顺序(根因在前):`unknown`(名字本期没登记)→ `disabled`(显式停用)→ `not_configured`(名字已登记但没配齐:云家缺 key / searxng 缺地址)→ `ok`(其余)。⚠️「凭据失效」**不是** `state` 取值(它只能由一次真实调用得知,属 `plugin.testProvider` 的结论)。
四个动作(**本插件声明的全部动作**):
四个动作 + 一个候选来源(**本插件声明的全部动作**;全部已登记生产处理器):
| 动作名 | 语义 | 请求体 | 响应体 |
|---|---|---|---|
| `plugin.listProviders` | 列出各条 provider 配置(**读**来源) | `{}` | `{ "rows": [ {provider,state,enabled} ] }`(凭据/地址不回) |
| `plugin.saveProvider` | 新增/修改一条(按 `provider` **upsert**;**写**) | `{ "provider":"bocha","apiKey":"…","baseUrl":"https://…","enabled":true }` | `{ "saved":true,"provider":"bocha" }` |
| `plugin.testProvider` | 测试连通(**真发一次最小检索**;**写**面 rowAction) | `{ "provider":"bocha" }` | `ProbeResult`,见下 |
| `plugin.reauthProvider` | 重配凭据:把该条带回表单(**只读回显掩码值**;**写**面 rowAction) | `{ "provider":"bocha" }` | `{ "provider":…,"apiKey":"****1234","baseUrl":…,"enabled":true }` |
| 动作名 | 语义 | 面 | 请求体 | 成功响应 `result` |
|---|---|---|---|---|
| `plugin.listProviders` | 列出各条 provider 配置 | read(table `source`) | `{}` | `{"rows":[{"provider":"searxng","state":"ok\|not_configured\|disabled\|unknown","enabled":true}]}`(**凭据/地址不回**) |
| `plugin.providers` | 下拉候选:本期**全部已登记**的 provider 名 | read(字段 `optionsFrom:"plugin:providers"`) | `{}` | `{"options":["aliyun_iqs","baidu_qianfan","bocha","searxng","volcengine","zhipu"]}`(稳定排序,**含尚未配置的家**) |
| `plugin.saveProvider` | 新增/修改一条(按 `provider` **upsert**) | write(form `submit`) | `{"values":{"provider":"bocha","apiKey":"…","baseUrl":"https://…","enabled":true}}` | `{"saved":true,"provider":"bocha","created":true,"enabled":true}` |
| `plugin.testProvider` | 测试连通(**真发一次最小检索**) | write(table `rowActions`) | `{"row":{"provider":"bocha"}}`(亦可给 `{"values":{…}}` 或顶层 `provider`) | `{"ok":true,"provider":"bocha","count":1,"latencyMs":123}`;**没通 = 非 2xx**(见错误表),**不在 200 里回 `ok:false`** |
| `plugin.reauthProvider` | 重配凭据:把该条带回表单 | write(table `rowActions`) | `{"row":{"provider":"bocha"}}` | `{"values":{"provider":"bocha","apiKey":"****cdef","baseUrl":"https://api.bocha.cn","enabled":true}}`(**`apiKey` 只回掩码**;前端对 secret 字段一律不回填输入框,**空 = 不改动**) |
`plugin.testProvider` 响应体:`{"ok":true,"provider":"bocha","count":1,"latencyMs":412}`;失败时如 `{"ok":false,…,"errorCode":"unauthorized","error":"上游拒绝了凭据(401/403):请核对 API Key 是否有效"}`。`errorCode` 闭集:`unknown_provider` / `not_configured` / `unauthorized` / `rate_limited` / `quota_exceeded` / `network` / `upstream` / `bad_response` / `no_results` / `unknown`。🔴 **`errorCode=no_results` 时 `ok=false` 但「连通是好的」**(空结果 ≠ 不通)⇒ 前端文案**必须**与真正的失败区分开,别把它显示成「该家不可用」。🔴 响应体**绝不**含上游 URL 与凭据。
`plugin.saveProvider` 的**三态写回哨兵**(写侧语义,前端须照此送值):`values.apiKey` **缺位** ⇒ 保留盘上原值;**空串** ⇒ 显式清空;非空 ⇒ 覆盖。`values.baseUrl` / `values.enabled` **缺位** ⇒ 保留;给出(含 `""` / `false`)⇒ 采用(`enabled` 给非布尔值 ⇒ `400 invalid_payload`,**不静默保留**)。`values.provider` **必填**,且必须是本期已登记的接入位(`searxng` / `zhipu` / `aliyun_iqs` / `bocha` / `volcengine` / `baidu_qianfan`),否则 `400 unknown_provider`。
`optionsFrom: plugin:providers` 的选项值 = `search.ProviderOptions()` = 本期**全部已登记**的 provider 名(含尚未配置的家,稳定排序)。
落盘:`$DATA_DIR/config/plugins/web-search.json`(0600 + temp+rename),**重启后仍然生效**;⚠️ 本片**不**热替换运行中的 web-search 工具(面板保存后工具仍用启动期接线,需重启)。
`plugin.testProvider` 失败时**不**回 200:走上面 (b) 的错误表(`unknown_provider` / `not_configured` ⇒ 400;`unauthorized` / `rate_limited` / `quota_exceeded` / `network` / `upstream` / `bad_response` / `no_results` / `unknown` ⇒ 502)。🔴 **`no_results` 表示「连通正常但没查到结果」**(空结果 ≠ 不通)⇒ 前端文案**必须**与真正的失败区分开,别把它显示成「该家不可用」。🔴 错误响应**绝不**含上游 URL 与凭据(只给稳定码 + 固定文案)。
> ⚠️ **v0.79 行为变化(登记)**:`optionsFrom` 声明的候选来源现在会进**动作索引** ⇒ `plugin.providers` 从「`404 action_not_declared`」变为**可调用的读动作**。前端此前对该动作 404 的降级分支(文本框 + 「候选项未能加载」)因此**不再触发**。⚠️ 另有一条**前端缺口**(不属本契约面):行内动作的成功分支只显示「已完成,正在刷新」,丢弃 `result` ⇒ 探针成功时的 `count` / `latencyMs` 在界面上仍不可见。
---
@@ -1116,19 +1138,31 @@
> 只保留最近若干版(**v0.60 起**);更早的全部历史见归档 `Docs/archive/api-client.v0.65.md` §5(v0.28 → v0.59)。
> 每版一句话 + 三档标注。**三档定义见 [§0.4](#04-三档标注惯例)。**
### v0.78(当前契约版本)
### v0.79(当前契约版本)
**四批增量 —— ① 插件面板动作真可用(切片 L1/PLG-2 的执行面接线)|② 「入库前审查」(D3)管理面落地(切片 L45)|③ 语音消息的用户可见面落地(切片 L45)|④ 链路图权威回合标识覆盖到全部编排回合(切片 CODE-TC-2,**唯一一处形状变化**)**(依据:面板动作此前一律 501 ⇒ 面板「点了没反应」;主人要「入库前审查可查、可回滚」;安卓/Web 要「语音能回放、消息能标出语音来源」;`toolCalls[]` 覆盖率此前只到真并行路 ⇒ 普通单路聊天与队首续跑回复回看不了工具。)
- **① 插件面板动作真可用(切片 L1/PLG-2)**:插件管理器新增**动作处理器登记面**(`Manager.RegisterActions` → `Panels.RegisterActions`,取代单一全局执行器;**声明仍是唯一闸门** —— 登记≠可执行);组合根在 `wirePanelHub` 里登记内置 `web-search` 的**四个动作 + 下拉候选来源**(`plugin.providers`)⇒ `POST /api/v1/admin/plugins/{id}/actions/{action}` **不再一律 501** —— 真读写 `$DATA_DIR/config/plugins/web-search.json`(0600 + temp+rename,**重启仍生效**)、探针**真发一次最小 HTTP 检索**。失败回**结构化非 2xx**(码/文案/状态由动作处理器给出,宿主只透传、状态越界回落 502):新增 `invalid_payload`(400) / `unknown_provider`(400) / `not_configured`(400) / `config_unreadable`(500) / `config_write_failed`(500),以及探针码 `unauthorized` / `rate_limited` / `quota_exceeded` / `network` / `upstream` / `bad_response` / `no_results` / `unknown`(502);**`501 action_not_implemented` 只在「已声明但宿主未登记处理器」时出现**(**不再是兜底档**);兜底执行失败仍是 `502 action_failed`。审计仍 `what="plugin.action"`,**服务端日志新增附加字段 `code`**(**不进 DB 记录体、不含请求体**)。**§2.1 A14 / §4.1 / §4.14**。
- **② 「入库前审查」(D3)的管理面(切片 L45)**:新增 2 条 `writeAuth`(owner\|admin)端点 —— `GET /api/v1/admin/memories/reviews?limit=`(留痕列表:`limit` 缺省 100、`≤0` 同缺省、>500 钳位、非整数 `400`;元素含 `decision ∈ skip\|merge\|modify\|create`、`mergedIds` 恒为非 null 数组、`createdAt` UTC RFC3339;🔴 **不含记忆正文**)与 `POST /api/v1/admin/memories/{id}/revert`(撤销一次**软合并**:被吸收行复原 `active`、从存活行 `merged_from` 摘除、`merge_count` 减 1;**行永不物理删除**)。未接线 ⇒ `500 memory_review_unavailable`(**绝不回空数组**);回滚落审计新动词 **`memory.review.revert`**(只记 item id)。权限矩阵:`writeAuth` 14 → **16**、总数 68 → **70**。**§2.0 / §2.2 B16/B17 / §4.1**。
- **③ 语音消息的用户可见面(切片 L45)**:消息 DTO 的 **`voice`**(含服务端唯一派生的 `url`)在 **4 个 REST 读取面 + `message_edited`** 真实填充;新增 **`GET /api/v1/media/{name}`**(`writeAuth`;`audio/*` 内联 + `nosniff` + `Accept-Ranges` + `Cache-Control: private, max-age=86400, immutable`;**无 `ETag`**;`Range` ⇒ 206、条件请求 ⇒ 304、支持 `HEAD`、不可满足 `Range` ⇒ 416(**非 JSON 信封**);名字非法 / 穿越 / 缺失统一 `404 media_not_found`);副本落盘走新增 `documents.Store.SaveAudio`(内容寻址、0600、元数据进同一张 `documents` 表、并入同一套引用计数),`chat_messages.voice` 列读写两条路补齐(`history.Store.UpdateMessageVoice` / `VoiceColumns`)。⚠️ **残留缺口(如实)**:`message_persisted` / `channel_persisted` 帧与握手 `history` 回放**仍不带 `voice`**(需给 `history.Message` 加字段,未做;且 `message_persisted` 在落库那一刻发出,天生拿不到落库后才回填的值);组合根**未注入 `VoiceTranscoder`** ⇒ **AMR 不落副本**(可播格式照落)。**§0.3 / §1.2 U19 / §3.4.4 / §4.1 / §4.4 / §4.5**。
- **④ 🔴 链路图权威回合标识覆盖到全部编排回合(切片 CODE-TC-2)**:新增 `loop.FlowPublisher.StartRoundWithTurnID(ctx, owner, text)`(权威回合标识取自 ctx 的 `TurnIDFrom`),`run.go` 的**唯一生产建图点**改走它 ⇒ 编排器驱动的**每一路**(**普通单路回合**、队首续跑路、真并行路)的图都带与 `chat_messages.turn_id` **同值的 `turnId`** ⇒ 消息面 **`toolCalls[]` 覆盖率从「只有真并行路」扩大到「全部编排回合」**(普通单路聊天与队首续跑回复现在也能回看工具)。🔴 **唯一形状变化**:`graph.id` 的 `#{权威回合标识}` 后缀现在**单路也追加**(`web-7` → `web-7#turn-…`)—— 该键仍**不透明**、仍可能含 `#`;客户端**一律使用响应里的 `graph.id` / `turns[].turnId`**,**不要自己拼** `{会话}-{序号}`(自拼的老客户端**查不到新行**)。**缺席语义一字未改**:拿不到权威标识的写入路径(未接编排器的调用方、**后台思考** `sess-backthink`、旧后端)`turnId` 仍缺席、`toolCalls` 仍**整体缺席**(**绝不回空数组**);**v0.79 之前的单路历史行不回填**(`lane_turn_id` 为空且无可回填来源)。**§4.4 / §4.11 / §7.2 / §7.4-23**。
- **行为变更**:① **有**(面板动作由「全 501」变为**真执行**)+ 新增一批错误码;② 无(新端点;老客户端零影响);③ 无(`voice` 是既有 DTO 的可选键、`GET /media/{name}` 是新端点);④ **有(唯一一处形状变化)**:`graph.id` 对单路也追加后缀 ⇒ 自拼 id 的读取方须改用响应值(**其余形状与缺席语义一字未改**)。
- **破坏性**:**无**(新端点 / 新可选键 / 新错误码;④的形状变化只影响「自己拼 `graph.id`」的读取方,而本文档自 v0.73 起就写明该键**不透明、应从响应取**)。
- ⚠️ **诚实边界**:① 面板动作**热重载未做**(保存后运行中的 web-search 工具不换接线,需重启)、**探针只用 `httptest` 假上游**(未对真实上游连通);② 回滚端点与 `design/23 §11` 的 owner-only 口径**有差异**(待裁决)、留痕列表**无分页游标**、`memory_ingest_reviews` 无 TTL/GC;③ 语音**生产端到端未跑**、WS 实时帧/握手回放不带 `voice`、AMR 不落副本、删除消息/会话时音频副本引用计数不释放;④ 单路 `toolCalls[]` 的**HTTP 出口端到端未由本切片确证**(写侧已验,网关读侧由主会话补验)、`turns[]` 仍按**路**列条目、真机未验。逐条见 **§7.2 / §7.4**。
### v0.78(上一版)
**六面增量 —— ① 消息读取面 `toolCalls[]` 落地(覆盖率受限)|② 插件管理面板接入点(`GET /admin/panels` + 插件动作分发端点 + `panels_changed` 帧)|③ 搜索 provider 可变源(`web-search` 插件)|④ 账号上下线落事件 + 渠道事件实时帧 `channel_event`|⑤ 微信 iLink 凭据三态(`authState`/`authDetail`)|⑥ 语音消息落库面(迁移 `0020`)**(依据:安卓侧要「回看这条回复当时调了什么工具」;design/09 §5.2.1 的插件面板接入点 + §5.4 的安全与权限;design/27 的网络搜索提供商 + design/13 §2.1 的插件配置落点;design/26 §2.6/§4.5 的账号状态事件与实时面;`errcode=-14` 被读成「过期」的误判风险;design/24 §10 的语音落库面。)
- **① 消息读取面 `toolCalls[]` 落地(切片 CODE-TC)**:`GET /api/v1/sessions/{id}/messages` 的消息对象新增**可选** `toolCalls`(`omitempty`),每条 `{name,status,durationMs?,argsSummary?,resultSummary?}`(`name`/`status` 恒在,`status ∈ {ok,error,unknown}` 闭集三态;后三键图上给不出时**缺席**)。数据源 = **已落盘的轮次链路图** `flow_graphs`;🔴 **缺席语义**:图取不到 ⇒ 字段**整体缺席**,**绝不回 `[]`**(客户端只做正向断言)。**为打通读侧补齐**:迁移 `0023_flow_graphs_lane_turn_id` 给 `flow_graphs` 加**按路查询键** `lane_turn_id`(等值索引 `idx_flow_graphs_lane_turn_id (session_id, lane_turn_id)`)+ 回填;`FlowGraphStore` 增只读方法 `GraphsByTurnID`(一页一次 `IN` 批量查,禁 N+1);`history.go` 的 `handleListMessages` 在编码前调 `attachToolCalls`。**只在该出口填充**(其余 DTO 出口逐字不变,有单测钉住)。⚠️ **覆盖率受限(如实)**:只有**真并行路**的图带权威 `turnId` ⇒ **普通单路回合的回复查不到图、`toolCalls` 缺席**(补齐在 `internal/runtime/loop/**` 写侧,**不在本版**)。**§4.4 / §7.2 / §7.4-23 / §2.2 B13**。
- **② 插件管理面板接入点(切片 PLG-2,纯增量为主)**:新增 1 条 viewer 读端点 **`GET /api/v1/admin/panels`**(**按调用者角色过滤**后的接入点快照 `{role,groups[],pluginIds[]}`,viewer 只拿只读区块;未接线 ⇒ `501 panels_unavailable`)与 1 条动作分发端点 **`POST /api/v1/admin/plugins/{id}/actions/{action}`**(**只接受该插件声明过的动作名**,未声明 ⇒ `404 action_not_declared`;写动作 owner/admin 收紧 ⇒ 未授权 `403 forbidden`;停用插件 ⇒ `409 plugin_disabled`;**每次结算都落审计** `what="plugin.action"`;**插件只给动作名、不给 URL** ⇒ 不开 SSRF 面);WS 下行帧 **`panels_changed`**(`{reason,pluginId}`,`reason ∈ {declare,install,uninstall,enable,disable}`,**只发 owner/admin**,**不带内容**⇒ 客户端按角色重拉快照)。权限矩阵:`viewerAuth` 12 → **14**、总数 66 → **68**。🔴 **当前生产路径硬事实**:**无任何生产动作执行器** ⇒ **所有已声明动作都回 `501 action_not_implemented`**(见 §7.2)。**§2.1 A13/A14 / §3.4.5 / §4.14(a)(b)**。
- **① 消息读取面 `toolCalls[]` 落地(切片 CODE-TC)**:`GET /api/v1/sessions/{id}/messages` 的消息对象新增**可选** `toolCalls`(`omitempty`),每条 `{name,status,durationMs?,argsSummary?,resultSummary?}`(`name`/`status` 恒在,`status ∈ {ok,error,unknown}` 闭集三态;后三键图上给不出时**缺席**)。数据源 = **已落盘的轮次链路图** `flow_graphs`;🔴 **缺席语义**:图取不到 ⇒ 字段**整体缺席**,**绝不回 `[]`**(客户端只做正向断言)。**为打通读侧补齐**:迁移 `0023_flow_graphs_lane_turn_id` 给 `flow_graphs` 加**按路查询键** `lane_turn_id`(等值索引 `idx_flow_graphs_lane_turn_id (session_id, lane_turn_id)`)+ 回填;`FlowGraphStore` 增只读方法 `GraphsByTurnID`(一页一次 `IN` 批量查,禁 N+1);`history.go` 的 `handleListMessages` 在编码前调 `attachToolCalls`。**只在该出口填充**(其余 DTO 出口逐字不变,有单测钉住)。⚠️ **覆盖率受限(如实,v0.78 时)**:只有**真并行路**的图带权威 `turnId` ⇒ **普通单路回合的回复查不到图、`toolCalls` 缺席**(补齐在 `internal/runtime/loop/**` 写侧,**不在本版**)。→ **✅ v0.79 已补齐**(切片 CODE-TC-2:编排器驱动的每一路都带权威 `turnId` ⇒ 覆盖率扩大到**全部编排回合**;见 §6 v0.79 ④)。**§4.4 / §7.2 / §7.4-23 / §2.2 B13**。
- **② 插件管理面板接入点(切片 PLG-2,纯增量为主)**:新增 1 条 viewer 读端点 **`GET /api/v1/admin/panels`**(**按调用者角色过滤**后的接入点快照 `{role,groups[],pluginIds[]}`,viewer 只拿只读区块;未接线 ⇒ `501 panels_unavailable`)与 1 条动作分发端点 **`POST /api/v1/admin/plugins/{id}/actions/{action}`**(**只接受该插件声明过的动作名**,未声明 ⇒ `404 action_not_declared`;写动作 owner/admin 收紧 ⇒ 未授权 `403 forbidden`;停用插件 ⇒ `409 plugin_disabled`;**每次结算都落审计** `what="plugin.action"`;**插件只给动作名、不给 URL** ⇒ 不开 SSRF 面);WS 下行帧 **`panels_changed`**(`{reason,pluginId}`,`reason ∈ {declare,install,uninstall,enable,disable}`,**只发 owner/admin**,**不带内容**⇒ 客户端按角色重拉快照)。权限矩阵:`viewerAuth` 12 → **14**、总数 66 → **68**。🔴 **v0.78 时生产路径硬事实**:**无任何生产动作执行器** ⇒ **当时所有已声明动作都回 `501 action_not_implemented`** —— **已由 v0.79 接线**(见 §6 v0.79 ① 与 §4.14)。**§2.1 A13/A14 / §3.4.5 / §4.14(a)(b)**。
- **③ 搜索 provider 可变源(切片 PLG-4)**:插件 `web-search` 新增可变源 `$DATA_DIR/config/plugins/web-search.json`(**0600**、temp+rename、文件名 = 插件 id),组合根「**先可变源、缺失回落 `.env` 播种**」;面板数据面 = 四个动作 `plugin.listProviders` / `plugin.saveProvider` / `plugin.testProvider` / `plugin.reauthProvider`(表格行 `{provider,state,enabled}`,`state ∈ {unknown,disabled,not_configured,ok}`;`testProvider` 的 `errorCode` 闭集十项,⚠️ **`no_results` 时「空结果 ≠ 不通」**)。🔴 **不新增任何 env 键**(`.env` 已冻结);**`.env` 的 `WEB_SEARCH_*` 自此只在文件缺失时生效**(运维视角的行为变更,如实登记)。**§4.14(c) / §5**。
- **④ 账号上下线落事件 + 渠道事件实时帧(切片 E7)**:`internal/runtime/events` 增「账号状态迁移」这一路 —— 三态归一(**未知也照常落行**,不整类丢弃)→ 落一行 `tier=t2` / `category=lifecycle`(**复用迁移 `0021`**)→ 经**既有进程内 bus** 的主题 `channel.event` 推成 WS 下行帧 **`channel_event`**(`data.event` 与 REST `events[]` **同一个对象**;⚠️ **实时帧的 `id` 键可能整缺**;**只发 owner/admin**,**viewer 走 REST 只读档**)。采样常驻(约 60s 一拍,**不看面板也在跑**)。⚠️ **当前只覆盖账号状态迁移这一路**(其余渠道事件的实时装饰器**未接线**,见 §7.2);采样通道目前唯一来源是 `qq-obv11` 的探活。**§3.4.5**。
- **⑤ 微信 iLink 凭据三态(切片 CODE-F3)**:`GET /api/v1/admin/channels/{platform}/login-status` 新增**可选** `authState` / `authDetail`(`"active"` 有效 / `"rejected"` **被拒且需重扫** / `"unknown"` 未知;**闭集**)。🔴 **文案口径写死**:**`errcode=-14` 是「被拒」不是「过期」**、**未知原因不许写成「过期」/「未连接」**(响应体全文由测试钉住)。**键缺席 = 该账号没有这条状态面**,≠ `unknown`。**无新端点、无新路由**(沿用 v0.39 起的端点)。⚠️ `GET /admin/channels` 的 `accounts[]` **每账号**三态**未实现**(见 §7.2)。**§2.4(c)**。
- **⑥ 语音消息落库面(切片 CODE-VOICE)**:迁移 `0020_chat_message_voice` 给 `chat_messages` 加 `voice TEXT NOT NULL DEFAULT ''`(JSON 单对象,形状 = DTO `voice` 减去 `url`;**刻意不建索引**);`internal/runtime/voice/**` 新包(`Source`/`Status` 闭集、`Ref` 编解码、格式级可播表 + `amr→wav` 转码归一);`channelbridge` 入站语音「归一 → **闸门之后**落副本 → 落库后写 `voice` 列」全链打通(落盘内容寻址、复用文档存储与 GC、**不进文档抽取/注入链路**)。🔴 **用户可见面(DTO `voice` + `GET /api/v1/media/{name}`)仍未实现**(缺 `documents.SaveAudio` / `history` 写视图 / 组合根注入转码器三件接线)⇒ **客户端按「未实现」对待**。唯一已生效的**客户端可见**变化:IM 语音在「格式不在可转写白名单内」时正文诚实提示改口径(**形状不变**,客户端本就不该按正文文案分支)。**§4.5 / §7.2**。
- **⑥ 语音消息落库面(切片 CODE-VOICE)**:迁移 `0020_chat_message_voice` 给 `chat_messages` 加 `voice TEXT NOT NULL DEFAULT ''`(JSON 单对象,形状 = DTO `voice` 减去 `url`;**刻意不建索引**);`internal/runtime/voice/**` 新包(`Source`/`Status` 闭集、`Ref` 编解码、格式级可播表 + `amr→wav` 转码归一);`channelbridge` 入站语音「归一 → **闸门之后**落副本 → 落库后写 `voice` 列」全链打通(落盘内容寻址、复用文档存储与 GC、**不进文档抽取/注入链路**)。⚠️ **用户可见面(DTO `voice` + `GET /api/v1/media/{name}`)在 v0.78 时未实现,已由 v0.79 落地**(切片 L45,见 §6 v0.79 ③);**残留缺口**(WS 实时帧/握手回放不带 `voice`、AMR 不落副本)见 §4.5 / §7.2。唯一已生效的**客户端可见**变化:IM 语音在「格式不在可转写白名单内」时正文诚实提示改口径(**形状不变**,客户端本就不该按正文文案分支)。**§4.5 / §7.2**。
- **行为变更**:① 无(新可选键 + 新迁移列/索引);② 无(新端点 / 新帧 / 新可选键;老客户端零影响)—— ⚠️ 但**新增了 `502` 状态码**(仅插件动作执行器失败路径)与一批插件动作错误码;③ **有**(运维视角:`.env` 的 `WEB_SEARCH_*` 仅在可变源文件缺失时生效);④ 无(新帧 + 新事件行;REST 形状未变);⑤ 无(两个可选键;`status`/`botId`/`botToken`/`err` 与 v0.39 起逐字一致);⑥ **有(轻微)**:IM 语音「不可转写格式」的正文诚实提示**改口径**(**形状不变**)。
- **破坏性**:**无**。
- ⚠️ **诚实边界**:① `toolCalls[]` **覆盖率只到真并行路**(普通单路回合查不到图 ⇒ 缺席),且**未做真机端到端**(真并行路的 `lane_turn_id` 与 `chat_messages.turn_id` 逐字相等仍是**代码级**推论);② 插件动作**生产无执行器**(全回 501)、热装插件的声明来源缺、`panels_changed` 对 viewer 不可见、启用态**不跨重启持久化**、审计持久化记录**未含 pluginId/action**(都在 §7.2);③ `web-search` 声明已就绪但**「面板加一家 provider」未做真机端到端**;④ `channel_event` **只覆盖账号状态迁移这一路**(组合根装饰器未接线),且**未做真机端到端**;⑤ iLink `rejected` 分支**只有单测/httptest 证据**(真实 `errcode=-14` 至今 0 次观测);⑥ 语音**生产端到端未跑**(`voice` 列写不进库、副本落不进盘、模型仍不知道文字来自语音)。
- ⚠️ **诚实边界**:① `toolCalls[]` **v0.78 时覆盖率只到真并行路**(普通单路回合查不到图 ⇒ 缺席)—— **已由 v0.79 补齐**(见 §6 v0.79 ④),且**未做真机端到端**(真并行路的 `lane_turn_id` 与 `chat_messages.turn_id` 逐字相等仍是**代码级**推论);② 插件动作**生产无执行器**(全回 501)—— **已由 v0.79 接线**(见 §6 v0.79 ①)、热装插件的声明来源缺、`panels_changed` 对 viewer 不可见、启用态**不跨重启持久化**、审计持久化记录**未含 pluginId/action**(都在 §7.2);③ `web-search` 声明已就绪但**「面板加一家 provider」未做真机端到端**;④ `channel_event` **只覆盖账号状态迁移这一路**(组合根装饰器未接线),且**未做真机端到端**;⑤ iLink `rejected` 分支**只有单测/httptest 证据**(真实 `errcode=-14` 至今 0 次观测);⑥ 语音**生产端到端未跑** —— **用户可见面已由 v0.79 落地**(见 §6 v0.79 ③),但 WS 帧/握手回放仍不带 `voice`、AMR 不落副本。
### v0.77(上一版)
@@ -1145,7 +1179,7 @@
**三面增量 —— ① 消息读取面 `toolCalls[]`(工具回看 · **✅ 已由 v0.78 落地**)|② 未读面 `unreadCount`(多端共享单游标 · **已实现**)|③ 记忆整理试算的加性异步(**已由 v0.77 落地校正**)**(依据:安卓侧 `Cyrene-For-Android/devdocs/02` 明确「工具回看面比未读面更重要、且要带工具输出/摘要」,以及主人 2026-10-05 裁定的「抬高 + 治入库太宽」导致试算更慢)。
- **① 工具回看面(✅ 已在 v0.78 实现 —— 见 §6 v0.78 ① 与 §4.4;**覆盖率受限**)**:`GET /api/v1/sessions/{id}/messages` 的消息对象加可选 `toolCalls`(`omitempty`)—— 每条含 `name` / `status`(`ok`|`error`|`unknown`)/ `durationMs` / `argsSummary` / `resultSummary`(后两者是**图里已有的、已脱敏**内容并按上限截断)。数据源 = **已落盘的轮次链路图**(`flow_graphs`,迁移 `0022`;**v0.78 补迁移 `0023` 的按路查询键**,保留期 168h),**不从正文反解、不从内存态猜**。🔴 **缺席语义**:图取不到 ⇒ 字段**整体缺席**(**绝不回空数组**冒充「没调过工具」)。修的是安卓侧「回看这条回复时看不到它当时调了什么」。**⚠️ 历史说明(v0.76 登记时未实现的原因,实测确认非推断)**:`chat_messages.turn_id` 与 `flow_graphs.turn_id` **不同源**(前者 = 编排器生成的随机路标识 `turn-<16 hex>`,后者 = `trace.FlowGraph.ID`,进程内递增的建图序号)⇒ **无法按消息 join 到它那一轮的图**。⚠️ **v0.78 校正(切片 CODE-TC)**:v0.77 已给链路图加**按路的回合标识**,v0.78 又补上**按路查图的读侧入口**(迁移 `0023` 的 `lane_turn_id` + `GraphsByTurnID`)⇒ `toolCalls[]` **已实现**;但🔴 **只有真并行路能回看**(普通单路回合的图当前不带权威 `turnId`),补齐需落在 `internal/runtime/loop/**` 写侧,**不在本版**(见 **§4.4**、§7.2、§7.4-23)。
- **① 工具回看面(✅ 已在 v0.78 实现 —— 见 §6 v0.78 ① 与 §4.4;**覆盖率受限**)**:`GET /api/v1/sessions/{id}/messages` 的消息对象加可选 `toolCalls`(`omitempty`)—— 每条含 `name` / `status`(`ok`|`error`|`unknown`)/ `durationMs` / `argsSummary` / `resultSummary`(后两者是**图里已有的、已脱敏**内容并按上限截断)。数据源 = **已落盘的轮次链路图**(`flow_graphs`,迁移 `0022`;**v0.78 补迁移 `0023` 的按路查询键**,保留期 168h),**不从正文反解、不从内存态猜**。🔴 **缺席语义**:图取不到 ⇒ 字段**整体缺席**(**绝不回空数组**冒充「没调过工具」)。修的是安卓侧「回看这条回复时看不到它当时调了什么」。**⚠️ 历史说明(v0.76 登记时未实现的原因,实测确认非推断)**:`chat_messages.turn_id` 与 `flow_graphs.turn_id` **不同源**(前者 = 编排器生成的随机路标识 `turn-<16 hex>`,后者 = `trace.FlowGraph.ID`,进程内递增的建图序号)⇒ **无法按消息 join 到它那一轮的图**。⚠️ **v0.78 校正(切片 CODE-TC)**:v0.77 已给链路图加**按路的回合标识**,v0.78 又补上**按路查图的读侧入口**(迁移 `0023` 的 `lane_turn_id` + `GraphsByTurnID`)⇒ `toolCalls[]` **已实现**;但🔴 **v0.78 时只有真并行路能回看**(普通单路回合的图当时不带权威 `turnId`)—— **已由 v0.79(切片 CODE-TC-2)补齐**:编排器驱动的每一路都带权威标识 ⇒ 覆盖率扩大到**全部编排回合**(见 §6 v0.79 ④ / **§4.4**、§7.2、§7.4-23)。
- **② 未读面(纯增量 · ✅ 已实现)**:`message_persisted` 的 `data` 加可选 `unreadCount`(`omitempty`,**只在 >0 且确实算得出来时**出现;**缺席 = 老行为**),`GET /api/v1/sessions` 的列表项同键(**该键自 D4 起就已在**,本版未改它)。口径 = **绝对未读 + 主人单一已读游标(`chat_sessions.read_at`,迁移 `0010`)⇒ 多端共享**:Web 读了,移动端的红点也清 —— 这是与安卓侧**已对齐**的取舍(他们明确赞成),**不是缺陷**,不许改成「每端独立未读」而不经裁定。形状、缺席语义与三种缺席情形见 **§4.13**;帧表已同步(**§3.4.4**)。
- **③ 记忆整理试算加性异步**:`POST …/memories/tidy/dry-run?async=1` ⇒ `202` + `runId`(新增 B15 轮询端点)。**不带 `async` 仍是同步** ⇒ 老客户端(含现有面板)逐字不变。结果活在**进程内存**(TTL 30 min、并发 1、重启即失效 ⇒ 如实 `404`)。⚠️ **v0.77 落地校正**:本项登记的时点标注「契约先行」,**切片 MT-A 已实现**,实现口径见 **§4.12.1**(并发=**复用**既有 `runId`、与同步**同一个 3 分钟**墙钟、`result` 与同步 `data` **逐字节同形**、新错误码 **`tidy_run_not_found`**)。
- **行为变更**:无(②只在既有帧里加可选键;③只在显式带参时改变路径;①未落地 ⇒ 无变更)。
@@ -1258,11 +1292,11 @@
**三批已在设计文档定稿的新增面一次性登记**:语音消息(播放端点 + 消息 DTO `voice` + 音频副本口径)、非消息事件查询面、账号在线状态(依据 `design/24 §10.11`、`design/26 §4.5` / `§2.6`)。登记时三批**都还没有实现**;其中**账号在线状态**(`52dac45`)与**非消息事件查询面**(本切片 E6)**已落地**(见下)。
> ⚠️ **本版是「契约先行」登记**:登记的三批里,**账号在线状态**(见 §2.4(b))与**非消息事件查询面**(见 §2.4(a))**已落地**;**语音消息尚未实现**,那一批的形状**取自设计文档而非代码核对**。客户端**先按形状对齐、不要按「已可用」编程**;实现落地后本版会补记 commit 并把 §7.2 相应行改为上线。
> ⚠️ **本版是「契约先行」登记**:登记的三批里,**账号在线状态**(见 §2.4(b))与**非消息事件查询面**(见 §2.4(a))**已落地**;**语音消息**当时未实现,那一批的形状**取自设计文档而非代码核对** —— **v0.79 已把用户可见面落地**(切片 L45,见 §6 v0.79 ③ / §4.5 / §7.2),**残留缺口**(WS 实时帧/握手回放不带 `voice`、AMR 不落副本)仍按「未确证/未实现」读。
- **纯增量**:
① 新端点 `GET /api/v1/media/{name}`(用户侧,**owner/admin 必须 Bearer**):`200` 字节流 / `206 Partial Content` / `304 Not Modified`;`Content-Type: audio/*`;`Content-Disposition: inline; filename="<name>"`;`nosniff`;`Accept-Ranges: bytes`;`Cache-Control: private, max-age=86400, immutable`;支持 `Range` 与 `HEAD`;`404 media_not_found`;`401`/`403` 由中间件给出。⚠️ **`416` 是标准库直写的非 JSON 响应**(见 §0.3、§4.1、§4.5)。
② 消息 DTO 增**可选** `voice` 对象(`omitempty`;**非语音消息键缺席**):`{source,status,name?,mime?,size?,durationMs?,url?}`;`source` ∈ `asr`/`platform`,`status` ∈ `transcribed`/`empty`/`failed`,`name`/`mime`/`size`/`url` **四者同生共死**(有副本时才在);`url` 是服务端相对路径 `/api/v1/media/<name>`。**🎤 判据 = `voice` 键存在**。一次覆盖全部读取面:`GET /sessions/{id}/messages`、`PATCH /messages/{id}` 响应、`GET /admin/channels[/{platform}]/messages`、`message_persisted` / `channel_persisted` / `message_edited`、握手 `history` 回放(§3.2、§3.4.4、§4.4)。
② 消息 DTO 增**可选** `voice` 对象(`omitempty`;**非语音消息键缺席**):`{source,status,name?,mime?,size?,durationMs?,url?}`;`source` ∈ `asr`/`platform`,`status` ∈ `transcribed`/`empty`/`failed`,`name`/`mime`/`size`/`url` **四者同生共死**(有副本时才在);`url` 是服务端相对路径 `/api/v1/media/<name>`。**🎤 判据 = `voice` 键存在**。⚠️ **v0.79 校正(实测覆盖)**:真正会带 `voice` 的读取面**只有** `GET /sessions/{id}/messages`、`GET /admin/channels[/{platform}]/messages`、`PATCH /messages/{id}` 响应与 `message_edited`;**`message_persisted` / `channel_persisted` 与握手 `history` 回放不带**(见 §3.2、§3.4.4、§4.4 覆盖率的「尚未填充」)。
③ 新错误码 **`media_not_found`(404)** → 加进 §4.1 的 404 行(与 `document_not_found` 并列;**刻意不复用**它,否则客户端分不清缺的是文档还是语音)。
④ 新错误形态 **`416`**(**非信封**,标准库直写)→ §0.3 / §4.1 / §4.5。
⑤ 新 admin 端点 **`GET /api/v1/admin/channels/events`**(非消息事件列表,倒序;`?platform=&tier=&category=&channelId=&since=&until=&limit=&offset=`;响应 `{data:{events:[…],total}}`;`since`/`until` 按 UTC ISO-8601 定宽字符串**字典序**比较、**跨度上限 90 天**;不含正文、不含媒体 URL、不含任何凭据 / `flag`)—— §2.4(a)。**已落地**(`owner|admin` 档;元素键名 camelCase,逐字见 §2.4(a);未给窗口 ⇒ 不注入窗口)。
@@ -1357,7 +1391,7 @@
### 7.1 在飞切片
**v0.66 及更早登记的在飞项已全部冻结、客户端可直接依赖**(下表)。⚠️ **v0.67 新登记的三批中,账号在线状态(`accounts[].accountOnline`)与非消息事件查询面都已落地**(见 §7.2);**语音消息尚未实现**,**不在本表** —— 逐条见 §7.2。
**v0.66 及更早登记的在飞项已全部冻结、客户端可直接依赖**(下表)。⚠️ **v0.67 新登记的三批中,账号在线状态(`accounts[].accountOnline`)与非消息事件查询面都已落地**(见 §7.2);**语音消息的用户可见面已由 v0.79 落地**(仍残留 WS 帧/握手回放不带 `voice`、AMR 不落副本 —— 见 §7.2 / §7.4-29),**不在本表**,逐条见 §7.2。
| 项 | 状态 | 冻结位置 |
|---|---|---|
@@ -1374,21 +1408,21 @@
| 项 | 现状 |
|---|---|
| **🔴 v0.67 登记的「语音消息面」**(`GET /api/v1/media/{name}` + 消息 DTO `voice` + 入站语音音频副本落盘) | **用户可见面仍未实现**(契约已先行登记;v0.78 只落地「落库面」):端点 / 字段 / 副本口径见 §1.2 U19、§4.4、§4.5。**v0.78(切片 CODE-VOICE)已实现**:迁移 `0020` 的 `chat_messages.voice` 列、`internal/runtime/voice/**`(`Source`/`Status` 闭集 + `Ref` 编解码 + 格式级可播表 + `amr→wav` 归一)、`channelbridge` 入站语音「归一 → **闸门之后**落副本 → 落库后写 `voice` 列」全链。**仍未实现 / 未接线**:`documents.SaveAudio`(音频存储面)、`history` 的 `voice` 列写视图、组合根注入 `Options.VoiceTranscoder` **三件都缺** ⇒ 生产上**副本落不进盘、`voice` 列写不进库**(都走**如实的 WARN**,**不是假成功**);**网关 DTO `voice`(含 `url`)与 `GET /api/v1/media/{name}` 端点仍未实现**。⇒ **客户端一律按「未实现」对待**。权威设计 `design/24 §10`。⚠️ 同批的**行为变更**(QQ 语音 `content` 不再含 `[语音]`)与**口径变更**(`/admin/system/storage` 的 `documents` 段含语音副本)仍**尚未生效**;**已生效**的只有 IM 语音「不可转写格式」正文提示改口径(**形状不变**,见 §4.5) |
| **✅ v0.67 登记的「语音消息面」**(`GET /api/v1/media/{name}` + 消息 DTO `voice` + 入站语音音频副本落盘) | **用户可见面已实现(v0.79,切片 L45)**:端点 / 字段 / 副本口径见 §1.2 U19、§4.4、§4.5。落地:`documents.Store.SaveAudio`(音频白名单 / 内容寻址 / 0600)、`history.Store.UpdateMessageVoice` + `VoiceColumns`(`chat_messages.voice` 列读写两条路)、网关 DTO `voice`(含唯一派生 `url`)与 `GET /api/v1/media/{name}`。🔴 **残留缺口(如实)**:① **`message_persisted` / `channel_persisted` 帧与握手 `history` 回放仍不带 `voice`**(需给 `history.Message` 加字段,**未做**;且 `message_persisted` 在落库那一刻发出,天生拿不到落库后才回填的值)⇒ **客户端不要在实时帧/握手回放上等 `voice`**;② 组合根**未注入 `VoiceTranscoder`** ⇒ **AMR 不落副本**(走 `transcode_not_wired` 的 WARN;可播格式照落);③ **消息/会话被删除时音频副本引用计数不被释放**(`refs.go` 只认 `document` 列 ⇒ `ref_count` 偏高,后果是**晚回收**,绝不早删);④ **生产端到端未跑**(未重启后端)。⚠️ 同批的**口径变更**(`/admin/system/storage` 的 `documents` 段含语音副本)仍**尚未生效**;**已生效**的是 IM 语音「不可转写格式」正文提示改口径(**形状不变**,见 §4.5)。权威设计 `design/24 §10` |
| **✅ v0.67 登记的「非消息事件查询面」**(`GET /admin/channels/events`、`GET /admin/channels/events/counters`) | **已实现**(v0.67 登记项本切片落地):形状与错误码见 §2.4(a)(已按代码核对:元素键名 camelCase、`approx` 与 `counters` 同级、`400 bad_request`、`owner\|admin` 档);权威设计 `design/26 §4.5`。未接线存储 ⇒ `500 channel_events_unavailable`。⚠️ 本行留在此表仅为与上一版对照,下版改文档时应移入 §7.1 在飞切片表并回填 §2.1/§2.2 的端点计数 |
| **✅ v0.67 登记的「账号在线状态」**(`GET /admin/channels` 的 `accounts[].accountOnline`) | **已实现**(v0.67 登记项本次落地;落地前基线 `f91a83f`,本切片 commit 由主会话补记):形状见 §2.4(b)。管理面按**可选接口断言**读 `AccountStatus()`(`plugins/channels/qq_obv11/probe.go`,`a79fe96`);适配器**没有**该状态面、或实现了但 `known=false` ⇒ **键缺席**(未知 ≠ 离线,绝不用 `false` 冒充)。当前只有 `qq-obv11` 会带该键。⚠️ 本行留在此表仅为与上一版对照,下版改文档时应移入 §7.1 在飞切片表 |
| **✅ P1-T:`directMsgPerMin` 的 gateway 写入路径**(`GET /admin/channels` 的 `accounts[].directMsgPerMin` + `PUT .../config` 的顶层 `directMsgPerMin`) | **已实现**(v0.69):缺口 = gateway 面没让该字段通过 ⇒ **面板保存一次渠道配置就把主人手配的「关闭流控(-1)」静默重置回 `0` = 保护性默认 6 条/分**。现 5 处透传(`AdminChannelAccount` DTO / `channelConfigRequest` / `config.ChannelAccount` 拷贝 / `adminChannelAccountOf` / 微信扫码重建账号)。形状见 §2.2 A5 与 §2.3 C28 注 |
| **✅ v0.78 落地的「工具回看面」**(消息 DTO `toolCalls[]`) | **已实现(覆盖率受限)**(v0.78,切片 CODE-TC):形状、缺席语义与**覆盖率边界**见 **§4.4**;读侧依赖 = 迁移 `0023` 的 `flow_graphs.lane_turn_id`(等值索引)+ `FlowGraphStore.GraphsByTurnID`(一页一次 `IN` 批量查,禁 N+1)。⚠️ **只有真并行路能回看** —— 普通单路回合(Web 单路聊天 / 队首续跑)的图**当前不带权威 `turnId`** ⇒ 查不到图 ⇒ 字段**缺席**(补齐需落在 `internal/runtime/loop/**` 写侧,**不在本版**)。⚠️ **未做真机端到端**(见 §7.4-23 / §7.4-25) |
| **✅ 「工具回看面」**(消息 DTO `toolCalls[]`) | **已实现(v0.78 落地,v0.79 起覆盖全部编排回合)**(切片 CODE-TC + CODE-TC-2):形状、缺席语义与**覆盖率**见 **§4.4**;读侧依赖 = 迁移 `0023` 的 `flow_graphs.lane_turn_id`(等值索引)+ `FlowGraphStore.GraphsByTurnID`(一页一次 `IN` 批量查,禁 N+1)。✅ **v0.79**:编排器驱动的**每一路**(含普通单路回合与队首续跑路)的图都带权威 `turnId` ⇒ **普通单路聊天与队首续跑回复现在也能回看工具**。⚠️ **仍缺席**:后台思考(`sess-backthink`)、未接编排器的调用方、v0.79 之前的单路历史行。⚠️ 单路在 **HTTP 出口**的端到端**未由本切片确证**(写侧已验,gateway 读侧待补)。见 §7.4-23 / §7.4-25 |
| **✅ v0.76 登记的「未读面」**(`message_persisted.data.unreadCount`) | **已实现**(本切片):形状、缺席语义与三条缺席路径见 **§4.13**,帧表见 §3.4.4。⚠️ `GET /api/v1/sessions` 的同名键**自 D4 起就已在**(`historySessionSummary.UnreadCount`),本片**未改**它 —— 所以「加同键」这句对列表面其实是**既有事实**,不是本版新增 |
| **`graph_update` 按路分桶** | **✅ 已实现(v0.77,切片 FV2)**:帧 `data` 新增每路的 `turnId` 与 `round`,图按 `{会话}#{路}` 分桶 ⇒ **每一路各发自己的帧、各落一行**(不再互相顶掉);管理面 `GET /admin/flow/graph` 新增 `lanes[]`。形状与缺席语义见 §3.4.5 / §4.11(a)(d),登记见 §7.4-19。⚠️ 真并行**未做真机端到端**(见 §7.4-24) |
| **第二路进行态 system 摘要**(`WithParallelNotice`) | **未实现**:全仓无该符号;`session_parallel.go` 明示留待后续切片 |
| **env `CHAT_MAX_PARALLEL_TURNS`** | **未接线**:`Deps.MaxParallelTurns` 是构造期旋钮,但组合根未从任何 env 读它 ⇒ **生产恒为默认 2** |
| **`tool_execution.update` 的生产者** | wire 映射已实现,但工具分发器暂无中途进度上报源(全仓无 `StreamKindToolExecutionUpdate` 的生产者)⇒ 实际**暂无生产者**(`start`/`end` 真实发出) |
| **`chat_messages.turn_id` 上的 IM 行** | IM 渠道桥**不走**网关编排器 ⇒ 其行 `turn_id` 留空属**预期**(契约里该键缺席),不是缺陷 |
| **记忆整理候选的 apply / reject / revert + 候选列表 + 运行历史**(`design/23 §11`) | **未实现**(v0.75 **只**加了「试算」B14):试算只「算给我看」、**不改任何记忆**。`GET /admin/memories/candidates`、`POST .../candidates/{id}/{apply,reject,revert}`、`GET /admin/memories/tidy/runs`、`POST /admin/memories/tidy/run` **均尚未实现**。客户端**不要**假定它们存在(`design/23` 的第一版口径本就是「只产建议,apply 才写记忆」,apply 未做的前置原因见该节开放点) |
| **记忆整理候选的 apply / reject / revert + 候选列表 + 运行历史**(`design/23 §11`) | **未实现**(v0.75 **只**加了「试算」B14):试算只「算给我看」、**不改任何记忆**。`GET /admin/memories/candidates`、`POST .../candidates/{id}/{apply,reject,revert}`、`GET /admin/memories/tidy/runs`、`POST /admin/memories/tidy/run` **均尚未实现**。客户端**不要**假定它们存在(`design/23` 的第一版口径本就是「只产建议,apply 才写记忆」,apply 未做的前置原因见该节开放点)。⚠️ **别与 v0.79 的 `POST /admin/memories/{id}/revert` 混淆**:后者是**入库前审查(D3)软合并**的回滚(§2.2 B17),与「整理候选」是**两件事** |
| **`GET /api/v1/admin/memories` 的整理相关字段**(`kind` / `tags` / `confidence` / `createdAt` / `updatedAt` / `sourceConversation` / `status` / `mergedInto` / `mergeCount`) | **未实现**:该端点当前仍只回 `{id,scope,text}`(`design/23 §11` 登记的读侧缺口)。v0.75 **未**改它的形状 ⇒ 面板暂时无法在这一页按 kind/status 过滤(试算端点 B14 的候选自带这些分区键与 kind) |
| **🔴 记忆「入库前审查」的面板面**(`GET /api/v1/admin/memories/reviews`、`POST /api/v1/admin/memories/{id}/revert`) | **未实现 —— 目前没有任何 HTTP 入口**(v0.77 登记 · 切片 MT-B):审查结论已逐条落表 `memory_ingest_reviews`(读侧能力 `ListIngestReviews` 已就绪),`Store.RevertMerged` 的回滚能力也在,但 `internal/services/gateway/routes.go` 里**没有**对应路由 ⇒ **客户端不得假定这两个端点存在**(请求只会拿到 404)。同样**未登记**为可用的是 MT-B 报告建议的审计动词 `memory.review.ingest`。⚠️ 本行是「未实现」清单,**不是**可用端点清单 |
| **🔴 v0.78 插件动作的「执行器」**(`POST /admin/plugins/{id}/actions/{action}` 的动作本体) | **无生产实现 —— 所有已声明动作当前都回 `501 action_not_implemented`**(切片 PLG-2):端点是**真的**(鉴权 / 白名单 / 审计 / 错误码都实现),但 `Panels.SetExecutor` **只在测试里被调用**(`grep` 全仓无生产调用点)⇒ **`plugin.listProviders` 拉不到数据、`plugin.saveProvider` / `plugin.testProvider` / `plugin.reauthProvider` 都执行不了**。**客户端不得把「端点存在」当成「数据可用」**:只有拿到 `result` 才算执行。见 §4.14(b) |
| **✅ 记忆「入库前审查」的面板面**(`GET /api/v1/admin/memories/reviews`、`POST /api/v1/admin/memories/{id}/revert`) | **已实现(v0.79,切片 L45;v0.77 登记时无 HTTP 入口)**:两条都走 `writeAuth`(owner\|admin),形状见 §2.2 B16/B17。⚠️ **残留**:① 留痕列表**只有 `limit`、没有分页游标**(无 `before`/`offset`);② `memory_ingest_reviews` **无 TTL / GC**;③ 回滚**只回滚「被吸收行」这一侧**(不恢复存活行被抬高的 `confidence`);④ 门与 `design/23 §11` 表格的 owner-only **有差异**(待裁决)。同样是**新审计动词** `memory.review.revert`(`design/23 §16.4` 表里还没有它,需设计侧补记) |
| **✅ v0.78 插件动作的「执行器」**(`POST /admin/plugins/{id}/actions/{action}` 的动作本体) | **已接线(v0.79,切片 L1/PLG-2 执行面接线)**:插件管理器新增**动作处理器登记面**(`Manager.RegisterActions`;单一全局 `SetExecutor` 已删除),组合根在 `wirePanelHub` 里登记内置 `web-search` 的四个动作 + 候选来源 `plugin.providers` ⇒ **这些动作真执行**(真读写可变源、探针真发一次 HTTP)。**`501 action_not_implemented` 只在「已声明但宿主未登记处理器」时出现**(不再是兜底档)。⚠️ **残留**:面板保存后**不热替换运行中的 web-search 工具**(需重启生效);**探针只用 `httptest` 假上游**(未对真实上游连通);面板**无「主 provider / 降级链」字段**;启用态**不跨重启持久化**。见 §4.14(b)(c) |
| **🔴 v0.78 热装插件的面板声明来源**(`plugins/<id>/manifest.json` 读取) | **未实现**(切片 PLG-2 / PLG-1):目录能键热装插件、生命周期已接,但**没人喂声明** ⇒ 热装插件在 `GET /admin/panels` 的 `pluginIds` / `groups` 里**恒为「未声明管理页面」**。当前唯一有声明的是内置 `web-search`(组合根装配期 `Declare`) |
| **`PUT/POST /api/v1/admin/plugins/{id}/enable\|disable`** | **未实现**(切片 PLG-2):`Manager.SetEnabled` 已实现并有测试,但**没有 HTTP 面** ⇒ 客户端**不得假定**能经 REST 启停插件(`panels_changed` 的 `enable`/`disable` 两种 `reason` 因此当前**不会**在运行期出现)。⚠️ 且启用态**不跨重启持久化**(`PluginConfig` 无 `enabled` 字段,`SetEnabled(false)` 只在进程内生效,重启按「已安装」恢复为启用) |
| **`panels_changed` 帧对 `viewer` 可见** | **未实现(刻意)**:该帧**未**登记进 `frameReachesAll` 白名单 ⇒ 按 fail-closed 默认档**只发 owner/admin**,**viewer 收不到**(须自行刷新/轮询重拉 A13)。放开需先改 `ws_frame_scope.go` + 在 `design/11 §7.1` 登记理由(**不在本版**) |
@@ -1415,7 +1449,9 @@
- **`416` 不是 JSON 信封(v0.67 登记)**:`GET /api/v1/media/{name}` 的 `Range` 错误由 Go 标准库直写 `text/plain`,**不套** `{"error":{…}}` ⇒ 客户端别拿 `error.code` 解析它(§0.3 / §4.1 / §4.5)。
- **账号级限流字段是「值语义」,不是三态(v0.69 明确)**:`groupMsgPerMin`/`msgPerMin`/`maxMessages`/`directMsgPerMin` 在请求体里**省略即 `0`(未设置)**—— 一次「只改 token」的 PUT 若不带这四个键,会把已设限额(含 `directMsgPerMin:-1`)重置为 `0`。要保住就必须回送(面板已如此做)。这是 v0.68 起既有的顶层字段口径,v0.69 **未改**。
- **后台思考行的实时帧只带记录里的字段(v0.70 边界)**:`urgency` 在实时帧上一定是**精确档位**(记录落库值),`urgencyLowerBound` / `quietHoursBypassed` **不在记录里**、因此实时帧不带 —— 它们由网关读历史时按安静时段窗口推导(见 §4.10(b))。⇒ 实时行的「穿透安静时段」标记要等重开页面/历史拉取才出现,**这不等于**「这一轮没有穿透」。
- **`toolCalls` 只在 `GET /sessions/{id}/messages` 填充(v0.78 边界)**:共享 DTO 上的新键,但**只有该 handler** 会填它;其余出口(`PATCH /messages/{id}` 响应、`GET /admin/channels[/{platform}]/messages`、`message_persisted` / `channel_persisted` / `message_edited`、握手 `history` 回放)**逐字不变**(有单测钉住)。且**覆盖率只到真并行路**(见 §4.4 / §7.2)。
- **`toolCalls` 只在 `GET /sessions/{id}/messages` 填充(v0.78 边界;覆盖率 v0.79 已补齐)**:共享 DTO 上的新键,但**只有该 handler** 会填它;其余出口(`PATCH /messages/{id}` 响应、`GET /admin/channels[/{platform}]/messages`、`message_persisted` / `channel_persisted` / `message_edited`、握手 `history` 回放)**逐字不变**(有单测钉住)。⚠️ **v0.78 时覆盖率只到真并行路** —— **已由 v0.79(切片 CODE-TC-2)补齐至全部编排回合**;仍缺席的是后台思考会话与 v0.79 之前的单路历史行(见 §4.4 / §7.2)。
- **`voice` 只在 4 个 REST 读取面 + `message_edited` 填充(v0.79 边界)**:**`message_persisted` / `channel_persisted` 帧与握手 `history` 回放仍不带 `voice`**(需给 `history.Message` 加字段,未做);且入站语音的 `voice` 列是**落库之后**回填的 ⇒ 即使加了字段,`message_persisted`(在落库那一刻发出)也拿不到值。**客户端不要在实时帧 / 握手回放上等 `voice`**(见 §3.4.4 / §4.4 / §4.5)。
- **`memory.review.revert` 是新审计动词(v0.79)**:`design/23 §16.4` 的 `memory.*` 表里还没有它(需设计侧补记);回滚**只记 item id、不记正文**。
- **`web-search` 的「`.env` 只播种」(v0.78 行为变更)**:可变源文件 `$DATA_DIR/config/plugins/web-search.json` 一旦落盘,`.env` 的 `WEB_SEARCH_*` **只在文件缺失时**起作用 ⇒ **改 `.env` 不再影响运行中的部署**(这是 design/09 §5.7 E 要的「`.env` 只播种、面板是配置面」,但运维视角上是行为变更)。🔴 **不是** env 键新增/删除。
- **`channel_event` 是 best-effort 增量提示、且当前只覆盖一路(v0.78 边界)**:帧可能在 hub 发送缓冲满时**丢掉该连接的那一帧** ⇒ 客户端**必须**保留 REST 轮询兜底(以轮询结果为准重排)。且目前**只有账号状态迁移**会产帧(其余渠道事件的实时装饰器**未接线**,见 §7.2);帧里那行的 `id` **可能整缺**(刚落库、id 尚未回读)。
- **`panels_changed` 只发 owner/admin(v0.78 边界)**:该帧**不在** `frameReachesAll` 白名单 ⇒ **viewer 收不到**「插件面板变了」的通知,须自行刷新/轮询重拉 `GET /admin/panels`。且当前运行期只可能在**装配期**推一次(内置声明 `declare`),`enable`/`disable`/`install`/`uninstall` 四种 `reason` 需要**没有 HTTP 面的** `SetEnabled` / 插件安装端点才会出现(见 §7.2)。
@@ -1451,24 +1487,25 @@
21. **(v0.74 登记)`message_persisted` 的 `proactive` 判据只活在实时帧上,不落库**:它是「这条是她主动说的话」在线上**唯一**的结构化判据(帧顶布尔键,由 `gateway.BroadcastProactivePersisted` 打上),**没有对应数据库列**(本片不动 schema)。因此 ① `GET /sessions/{id}/messages`、握手 `history` 回放、`GET /admin/channels/**` 读到的历史行**不带**它(预期:通知只对实时到达有意义);② 客户端**不得**从 `platform` / `adapterId` / `turnId` 等行字段反推「是不是主动消息」—— 主会话里主动推送写的是**解析后的真实平台**(例如 `weixin-ilink`),与普通回复行同一形状,反推必然误伤。**落库失败时不发该帧**(没有权威 `id` 的 `message_persisted` 会被客户端整帧丢弃)。
22. **(v0.75 登记)试算端点(B14)是「只读计算」,它**不改**任何状态**:它不写 `memory_tidy_candidates` / `memory_tidy_runs`、不改 `memory_items`、不写审计链、**也不推进心跳**(不写 `lastRun`、不影响 `nextDelay`)。因此 ① 试算**不会**让「候选队列」(尚未实现,见 §7.2)多出任何东西;② 试算结果**不是缓存**:每次调用都重新算一遍,两次调用之间数据变了结果就可能不同(这是「同一套规则」的正常语义,不是不确定性),客户端**不要**把它当成会稳定的列表去 diff 或自动轮询;③ **`recallMode:"lexical"` 是降级信号**:它表示这轮没有走到语义向量(进程没接 embedder),此时召回更保守(命中更少)—— 若进程**接了** embedder 而它不可用,试算**直接报错**(`tidy_recall_unavailable`),**不会**在响应里假装成 lexical。
23. **(v0.76 登记 · v0.77 更新)消息 DTO 的 `turnId` 与链路图的轮次键:图**已带上每路的回合标识**,但两者仍是两套标识**:
23. **(v0.76 登记 · v0.77 更新 · v0.79 更新)消息 DTO 的 `turnId` 与链路图的轮次键:图**已带上每路的回合标识**,但两者仍是两套标识**:
- **消息侧**:`chat_messages.turn_id` = 编排器发的那一路的**随机路标识**(`turn-` + 16 位十六进制;同路的 user 行与 assistant 行同值)。
- **图侧两个键**:① **`graph.turnId`(v0.77 新增)=该图所属**路**的权威回合标识,与 `turn_begin` / `steer_ack` / 流帧帧壳的 `turnId` **同源同值** ⇒ 客户端**可以**用它把图与消息行**按路配对**;② `graph.id` / `flow_graphs.turn_id` 仍是**图侧轮次键**(形状 `{会话}-{序号}[#{路}]`,**不透明**;序号是发布器**进程内**递增的建图序号、重启即从头开始),也就是 `GET /admin/flow/graph?turnId=` 的参数值。
- **图侧两个键**:① **`graph.turnId`(v0.77 新增)=该图所属**路**的权威回合标识,与 `turn_begin` / `steer_ack` / 流帧帧壳的 `turnId` **同源同值** ⇒ 客户端**可以**用它把图与消息行**按路配对**;**v0.79 起编排器驱动的每一路(含普通单路回合)都带它**;② `graph.id` / `flow_graphs.turn_id` 仍是**图侧轮次键**(形状 `{会话}-{序号}[#{权威回合标识}]`,**不透明**;序号是发布器**进程内**递增的建图序号、重启即从头开始),也就是 `GET /admin/flow/graph?turnId=` 的参数值。
- **客户端三条硬约束**:① 要路标识就**读 `graph.turnId`**,**不得**解析 `graph.id` 去取它;② **仍不得**拿消息的 `turnId` 当 `?turnId=` 用(两者不同源);③ 也**不得**把图上的序号当成会话里的回合序号(重启后序号会重复)。
- **`toolCalls[]` 已由 v0.78 实现(覆盖率受限)**:v0.77 起每路各落一行、图带 `graph.turnId`;v0.78 又补上**按路查图的读侧入口**(迁移 `0023` 的 `flow_graphs.lane_turn_id` + `FlowGraphStore.GraphsByTurnID`)⇒ 「按消息回看那一轮调了什么工具」**已可用**。⚠️ **但仍只覆盖真并行路的回复** —— 普通单路回合的图**不带**权威 `turnId` ⇒ 查不到图 ⇒ `toolCalls` 缺席;且 `graph.id` **仍不等于**消息的 `turn_id`(两者不同源,不能用 `graph.id` 当 `chat_messages.turn_id`)。见 §4.4、§7.2。
- 🔴 **`graph.id` 形状变化(v0.79,唯一一处)**:`#{权威回合标识}` 后缀现在**单路也追加**(`web-7` → `web-7#turn-…`);**老行**(v0.79 前落盘的单路行)仍是 `{会话}-{序号}`。⇒ **一律使用响应里的 `graph.id` / `turns[].turnId`**,**不要自己拼**(自拼的 `web-7` 查不到新行);该值可能含 `#`,作 query 值须 URL 编码。
- **`toolCalls[]` 覆盖率(v0.79 已补齐)**:v0.77 起每路各落一行、图带 `graph.turnId`;v0.78 补上**按路查图的读侧入口**(迁移 `0023` 的 `flow_graphs.lane_turn_id` + `FlowGraphStore.GraphsByTurnID`);v0.79(切片 CODE-TC-2)让**每一路**(含普通单路回合与队首续跑路)都带权威标识 ⇒ **普通单路聊天与队首续跑的回复现在也能回看工具**。⚠️ **仍缺席**:后台思考(`sess-backthink`)、未接编排器的调用方、v0.79 之前的单路历史行。⚠️ `graph.id` **仍不等于**消息的 `turn_id`(两者不同源,不能用 `graph.id` 当 `chat_messages.turn_id`)。见 §4.4、§7.2、§7.4-25。
24. **(v0.77 登记)「按路分桶」尚未做真机端到端**:真并行(上行 `chat.parallel`)的修复是在**单测层**验证的(真 `Loop.Run` × 2 + 网关照同的 ctx 通道;网关侧走真 HTTP 处理器 + 真发布器),**没有**对运行中的后端发过一次真实 `chat.parallel` —— 因为线上产物是旧构建、且换二进制 / 重启有专门流程。因此**未确证**:真 WS 客户端在真并行下看到的两路帧**时序**(形状与分桶已由单测钉死,时序上等价但无实测)。另有一条**已知的窄竞态**(未观测、也未加锁消除):若一路恰好在另一路起跑的瞬间收尾,第二条路可能被算作**新的一轮**(此时两路仍各有各的图与落盘行,只是面板不把它们并排展示)。
25. **(v0.78 登记)`toolCalls[]` 的覆盖率与端到端未确证**:① **覆盖率只到真并行路** —— 普通单路回合(Web 单路聊天 / 队首续跑)的图当前**不带权威 `turnId`**(`loop.flowOwner` 取 `ParallelLegFrom(ctx)`,而 `WithParallelLeg` **只在真并行路挂**),落盘后 `flow_graphs.lane_turn_id` 为空 ⇒ 查不到图 ⇒ 字段缺席。这是**代码级确证**(切片 CODE-TC 有一条**钉住现状**的用例 `TestSingleLegRoundLacksLaneTurnID_现状缺口`,当前通过)。② **「存库里的 `lane_turn_id` 与 `chat_messages.turn_id` 逐字相等」仍是代码级推论**(两处都取 `turnLeg.turnID` / `ParallelLeg`),**没有**对运行中的后端发过一次真并行消息来实测 `GET /sessions/web/messages` 拿到 `toolCalls`。③ **老行**(v0.77 之前的落盘)该列回填后仍为空,**不可能**补齐(那时的图里没有该字段)。④ 生产库 `data/cyrene.db` 现存行的回填命中率**未统计**(未碰生产库)。
25. **(v0.78 登记 · v0.79 更新)`toolCalls[]` 的覆盖率与端到端未确证**:✅ **覆盖率已补齐(v0.79,切片 CODE-TC-2)** —— 编排器驱动的**每一路**(含普通单路回合与队首续跑路)的图都带权威 `turnId` ⇒ 字段不再只覆盖真并行路。⚠️ **仍未确证 / 仍缺席的点**:① **单路在 HTTP 出口的端到端未由本切片确证** —— 写侧(`loop` 的 3 个新用例,含真 `Run` → 真库 → 按消息那个 id 查回)已验,但网关读侧(`internal/services/gateway/history_toolcalls_gap_test.go` 那条「钉住现状缺口」的用例走的是**老入口** `StartRound`,其前提句已失效)由主会话补验;**没有**对运行中的后端发过一次真消息来实测 `GET /sessions/web/messages` 拿到单路 `toolCalls`。② **「存库里的 `lane_turn_id` 与 `chat_messages.turn_id` 逐字相等」仍是代码级结论**(两处都取 ctx 上的权威标识)。③ **老行**(v0.79 之前的单路落盘)该列仍为空,**不可能**补齐(那时的图里没有该字段)。④ **后台思考**(`sess-backthink`)仍缺席。⑤ 生产库 `data/cyrene.db` 现存行的回填命中率**未统计**(未碰生产库)。
26. **(v0.78 登记)插件管理面板接入点的真机面未确证**:`GET /api/v1/admin/panels` 与 `POST /admin/plugins/{id}/actions/{action}` 的**真实运行期响应未在 `:10942` 上验过**(换产物 / 重启有专门流程,本切片禁止)。所有证据来自 `httptest` + 进程内 bus。⇒ **未确证**:真机上 `data.pluginIds == ["web-search"]`、`groups[0].items[0].sections` 的两段声明、`route` 的前缀拼接姿势(相对片段,前端拼 `<prefix>/plugin/<pluginId>/<panelId>`)。另:`panels_changed` 的分组文案「插件」是**宿主文案**(design 未规定文案来源),改口径只改一个函数。⚠️ 且**未换产物前**,旧二进制里**没有**这两条路由(会 404),属预期。
26. **(v0.78 登记 · v0.79 更新)插件管理面板接入点 / 动作执行面的真机未确证**:`GET /api/v1/admin/panels` 与 `POST /admin/plugins/{id}/actions/{action}` 的**真实运行期响应未在 `:10942` 上验过**(换产物 / 重启有专门流程,本切片禁止)。所有证据来自 `httptest` + 进程内 bus。⇒ **未确证**:真机上 `data.pluginIds == ["web-search"]`、`groups[0].items[0].sections` 的两段声明、`route` 的前缀拼接姿势(相对片段,前端拼 `<prefix>/plugin/<pluginId>/<panelId>`)。**v0.79 新增未确证项**:① 面板动作(`plugin.listProviders` 等)在**真机上**真的回 200 + `result`(单测层已用 `httptest` 真发 HTTP 验过探针);② **真机探针未打过真实上游**(无 key);③ **面板保存 provider 后运行中的 web-search 工具不换接线**(需重启,未在真机验过重启后的生效)。另:`panels_changed` 的分组文案「插件」是**宿主文案**(design 未规定文案来源),改口径只改一个函数。⚠️ 且**未换产物前**,旧二进制里**没有**这些路由,属预期。
27. **(v0.78 登记)`web-search` 可变源的运行期口径未确证**:组合根「先可变源、缺失回落 `.env` 播种」已有单测钉住(含「凭据不复活」「损坏文件不改写」「停用主 provider ⇒ 启动 fail-fast」等 7 条),并有一条**真机 SearXNG e2e**(`provider=searxng` 真返回结果)证明既有链路未被打断;但**「在面板上新增一家 provider」的端到端未跑**(面板入口是动作端点,而**动作执行器无生产实现**,见 §7.2)。⇒ 未确证:`plugin.saveProvider` 的真实落盘 + 生效链路在真机上走通。
27. **(v0.78 登记 · v0.79 更新)`web-search` 可变源的运行期口径未确证**:组合根「先可变源、缺失回落 `.env` 播种」已有单测钉住(含「凭据不复活」「损坏文件不改写」「停用主 provider ⇒ 启动 fail-fast」等 7 条),并有一条**真机 SearXNG e2e**(`provider=searxng` 真返回结果)证明既有链路未被打断。**v0.79**:动作执行器已接线 ⇒ `plugin.saveProvider` 的**落盘链路**在 `cmd/cyrene` 的单测里真跑过(临时目录 + 真 `runtimeconfig.SaveWebSearch`)。⚠️ **仍未确证**:「在**真机面板**上新增一家 provider → 下一次搜索真的走它」这条**端到端**(保存后工具**不热重载**,需重启才生效,见 §7.2 / §6 v0.79 ①)。
28. **(v0.78 登记)渠道事件实时面未做真机端到端**:`channel_event` 的「采样 → 落行 → 推帧 → 只投 owner/admin」由**真 SQLite + 真 bus + 真 WS 连接**的单测/端到端用例覆盖,但**运行中的 `cyrene.exe` 未重启**(红线:主会话换产物)⇒ **没有真机日志证据**。且当前 QQ 已停连 ⇒ 若真机启动,`qq-obv11` 账号为 `known=false`,首拍会落一行 `账号状态未知(尚未探到)`。另:**心跳通道(`meta_event.heartbeat`/`lifecycle`)驱动的迁移仍未接**(需适配器把状态投影进状态面,属 `plugins/**`),当前**唯一**迁移来源是探活(`qq_obv11/probe.go`)。
29. **(v0.78 登记)语音落库面的端到端未确证**:`internal/runtime/voice/**` 与 `channelbridge` 入站副本链有**真 `transcode.Engine`**(真 AMR 夹具 ⇒ RIFF/WAVE + 时长可确证)的走通用例,但**生产端到端未跑**(缺 §4.5 所述三件接线,且禁止重启后端)。⇒ **「Web 上能点开听 AMR 语音 / `voice` 列有值 / 副本在盘上」这三条**目前**都不成立**(走如实的 WARN)。`amr` / `flac` / `opus` 在部分浏览器内核上**可播放性未逐一实测**(判据是设计登记的 MDN 规范级结论);`weixin-ilink` 语音**无副本、不可回放**是**刻意**的(平台自带转写就不白下载音频)。
29. **(v0.78 登记 · v0.79 更新)语音的端到端未确证**:`internal/runtime/voice/**` 与 `channelbridge` 入站副本链有**真 `transcode.Engine`**(真 AMR 夹具 ⇒ RIFF/WAVE + 时长可确证)的走通用例;**v0.79** 补齐了网关侧(`media_test.go` 用真 `httptest` 真发 `GET`/`Range`/`HEAD`/条件请求,断言响应头与字节逐字相符 ⇒ **端点可用是「真跑过」的**),并新增 `documents.SaveAudio`(含 0600 断言)+ `history` 的 `voice` 列读写。⚠️ **仍未确证**:① **生产端到端未跑**(禁止重启后端)⇒ 「真机 IM 语音 → 盘上有 0600 副本 → 客户端取到字节」只有**替身 + 临时库/临时目录**级证据;② **AMR 当前不落副本**(组合根未注入 `VoiceTranscoder`,走 `transcode_not_wired` 的 WARN)⇒ 「Web 上能点开听 **AMR** 语音」这条**目前不成立**(可播格式照落);③ **`message_persisted` / `channel_persisted` / 握手 `history` 回放不带 `voice`**(见 §4.4 / §7.3);④ `amr` / `flac` / `opus` 在部分浏览器内核上**可播放性未逐一实测**(判据是设计登记的 MDN 规范级结论);⑤ **删除消息/会话时音频副本引用计数不释放**(`ref_count` 偏高 ⇒ 晚回收,绝不早删)。`weixin-ilink` 语音**无副本、不可回放**是**刻意**的(平台自带转写就不白下载音频)。
---
*本文件由「以代码为真源」维护(当前 `v0.78`;`v0.77` / `v0.76` / `v0.75` / `v0.74` / `v0.73` / `v0.72` / `v0.71` / `v0.70` / `v0.69` / `v0.68` 见本文件 §6,`v0.67` 及更早见本文件 §6 与 `Docs/archive/`)。范围:`v0.78`(六面 —— CODE-TC `toolCalls[]` / PLG-2 插件面板接入点 / PLG-4 搜索 provider 可变源 / E7 账号状态事件与 `channel_event` / CODE-F3 iLink 凭据三态 / CODE-VOICE 语音落库面)同步了 `internal/db/migrations/0023_flow_graphs_lane_turn_id.*`、`internal/db/migrations/0020_chat_message_voice.*`、`internal/runtime/flowgraph/**`、`internal/services/gateway/**`(`history.go` / `history_toolcalls.go` / `contracts.go` / `admin_panels.go` / `admin_channels_weixin.go` / `admin_channel_status.go` / `ws_channel_events.go` / `ws_bridge.go` / `ws_hub.go` / `routes.go`)、`internal/capability/plugin/**`(`panels.go` / `manager.go`)、`internal/capability/paneldecl/**`、`internal/config/runtimeconfig/websearch.go`、`internal/config/websearch.go`、`plugins/tools/io/search/**`(`panel.go` / `paneldata.go` / `probe.go`)、`plugins/channels/weixin_ilink/**`(`session_status.go` / `poll.go` / `send.go`)、`internal/runtime/events/**`、`internal/runtime/voice/**`、`internal/services/channelbridge/**`(`inbound_voice*.go`)、`pkg/plugins/sdk/panel.go`、`cmd/cyrene/**`(`wire_frontend.go` / `wire_io_tools.go`)与本文件(§0.3 / §1.2 U19 / §2.0 / §2.1 A13-A14 / §2.4(c) / §3.2 / §3.4.4 / §3.4.5 / §4.1 / §4.4 / §4.5 / §4.14 / §5 / §6 / §7.2 / §7.3 / §7.4);`v0.77`(链路图按路分桶 FV2 + 记忆整理护栏/异步 MT-A + 入库前审查 env 键 MT-B)同步了 `internal/runtime/trace/graph.go`、`internal/runtime/loop/**`(`flow.go` / `flow_branch.go` / `run.go` / `steer.go`)、`internal/runtime/orchestration/**`、`internal/runtime/flowgraph/**`、`internal/runtime/memory/**`(`tidy*.go` / `preinsert*.go`)、`internal/config/**`(`config.go` / `memory_review.go`)、`internal/services/gateway/**`(`admin_flow.go` / `contracts.go` / `admin_config_facts.go` / `admin_memory_tidy*.go` / `routes.go`)、`frontend/web/src/store/**`、`frontend/web/src/admin/modules/flow/**` 与本文件(§2.2 B12/B13/B15 / §3.4.5 / §4.1 / §4.10(a) / §4.11 / §4.12 / §4.12.1 / §5 / §6 / §7.2 / §7.4);`v0.76` 的**未读面(②)**同步了 `internal/services/gateway/**`(`broadcast.go` / `session_read.go`)、`internal/runtime/history/session_read.go` 与本文件(§3.4.4 / §4.13 / §6 / §7.2 / §7.4),并**按代码 + 库中真实数据实测确证**了 `chat_messages.turn_id` 与 `flow_graphs.turn_id` **不同源** —— 该阻塞**已由 v0.78(CODE-TC 的迁移 `0023` + `GraphsByTurnID`)解除**,`toolCalls[]` 已实现(覆盖率受限);v0.76 的 ③ 试算加性异步**已由 v0.77(MT-A)落地校正**;`v0.75` 同步了 `internal/services/gateway/**`(`admin_memory_tidy.go` / `routes.go` / `deps.go` / `server.go`)、`internal/runtime/memory/**`(`tidy_dryrun.go` / `tidy_recall.go` / `tidy.go`)、`cmd/cyrene/wire_frontend.go`、`frontend/web/src/**` 与本文件;`v0.74` 同步了 `internal/services/gateway/**`(`proactive.go` / `broadcast.go`)、`internal/services/channelbridge/**`(`proactive.go` / `persist.go` / `proactive_notify.go`)、`cmd/cyrene/wire_frontend.go`、`frontend/web/src/lib/ws/handlers/**` 与本文件;`v0.73` 同步了 `internal/services/gateway/**`、`internal/runtime/loop/flow.go`、`internal/runtime/flowgraph/**`、`internal/db/migrations/0022_flow_graphs.*`、`frontend/web/src/**` 与本文件;`v0.72` 同步了 `internal/services/gateway/**`(`proactive*`)、`internal/services/backthink/**`、`frontend/web/src/**` 与本文件;`v0.71` 同步了 `internal/services/gateway/**`、`internal/runtime/loop/flow.go`、`frontend/web/src/**` 与本文件。*
*本文件由「以代码为真源」维护(当前 `v0.79`;`v0.78` / `v0.77` / `v0.76` / `v0.75` / `v0.74` / `v0.73` / `v0.72` / `v0.71` / `v0.70` / `v0.69` / `v0.68` 见本文件 §6,`v0.67` 及更早见本文件 §6 与 `Docs/archive/`)。范围:`v0.79`(四批 —— L1/PLG-2 插件动作执行面接线 / L45 入库前审查管理面 / L45 语音用户可见面 / CODE-TC-2 链路图回合标识覆盖全部编排回合)同步了 `internal/capability/plugin/**`(`actions.go` / `panelactions.go` / `panels.go`)、`internal/services/gateway/**`(`admin_panels.go` / `admin_memory_review.go` / `history.go` / `history_write.go` / `history_voice.go` / `media.go` / `routes.go`)、`internal/runtime/voice/media.go`、`internal/runtime/documents/store_audio.go`、`internal/runtime/history/voice_column.go`、`internal/runtime/loop/**`(`flow.go` / `run.go`)、`internal/runtime/trace/graph.go`、`internal/runtime/flowgraph/flowgraph.go`、`plugins/tools/io/search/**`(`paneldata.go` / `probe.go`)、`cmd/cyrene/**`(`wire_plugin_actions.go` / `websearch_action_data.go` / `websearch_probe.go` / `wire_frontend.go`)与本文件(§0.3 / §1.2 U19 / §2.0 / §2.1 A13-A14 / §2.2 B16-B17 / §3.4.4 / §4.1 / §4.4 / §4.5 / §4.11 / §4.14 / §6 / §7.2 / §7.3 / §7.4);`v0.78`(六面 —— CODE-TC `toolCalls[]` / PLG-2 插件面板接入点 / PLG-4 搜索 provider 可变源 / E7 账号状态事件与 `channel_event` / CODE-F3 iLink 凭据三态 / CODE-VOICE 语音落库面)同步了 `internal/db/migrations/0023_flow_graphs_lane_turn_id.*`、`internal/db/migrations/0020_chat_message_voice.*`、`internal/runtime/flowgraph/**`、`internal/services/gateway/**`(`history.go` / `history_toolcalls.go` / `contracts.go` / `admin_panels.go` / `admin_channels_weixin.go` / `admin_channel_status.go` / `ws_channel_events.go` / `ws_bridge.go` / `ws_hub.go` / `routes.go`)、`internal/capability/plugin/**`(`panels.go` / `manager.go`)、`internal/capability/paneldecl/**`、`internal/config/runtimeconfig/websearch.go`、`internal/config/websearch.go`、`plugins/tools/io/search/**`(`panel.go` / `paneldata.go` / `probe.go`)、`plugins/channels/weixin_ilink/**`(`session_status.go` / `poll.go` / `send.go`)、`internal/runtime/events/**`、`internal/runtime/voice/**`、`internal/services/channelbridge/**`(`inbound_voice*.go`)、`pkg/plugins/sdk/panel.go`、`cmd/cyrene/**`(`wire_frontend.go` / `wire_io_tools.go`)与本文件(§0.3 / §1.2 U19 / §2.0 / §2.1 A13-A14 / §2.4(c) / §3.2 / §3.4.4 / §3.4.5 / §4.1 / §4.4 / §4.5 / §4.14 / §5 / §6 / §7.2 / §7.3 / §7.4);`v0.77`(链路图按路分桶 FV2 + 记忆整理护栏/异步 MT-A + 入库前审查 env 键 MT-B)同步了 `internal/runtime/trace/graph.go`、`internal/runtime/loop/**`(`flow.go` / `flow_branch.go` / `run.go` / `steer.go`)、`internal/runtime/orchestration/**`、`internal/runtime/flowgraph/**`、`internal/runtime/memory/**`(`tidy*.go` / `preinsert*.go`)、`internal/config/**`(`config.go` / `memory_review.go`)、`internal/services/gateway/**`(`admin_flow.go` / `contracts.go` / `admin_config_facts.go` / `admin_memory_tidy*.go` / `routes.go`)、`frontend/web/src/store/**`、`frontend/web/src/admin/modules/flow/**` 与本文件(§2.2 B12/B13/B15 / §3.4.5 / §4.1 / §4.10(a) / §4.11 / §4.12 / §4.12.1 / §5 / §6 / §7.2 / §7.4);`v0.76` 的**未读面(②)**同步了 `internal/services/gateway/**`(`broadcast.go` / `session_read.go`)、`internal/runtime/history/session_read.go` 与本文件(§3.4.4 / §4.13 / §6 / §7.2 / §7.4),并**按代码 + 库中真实数据实测确证**了 `chat_messages.turn_id` 与 `flow_graphs.turn_id` **不同源** —— 该阻塞**已由 v0.78(CODE-TC 的迁移 `0023` + `GraphsByTurnID`)解除**,`toolCalls[]` 已实现(覆盖率**已由 v0.79 的 CODE-TC-2 补齐至全部编排回合**);v0.76 的 ③ 试算加性异步**已由 v0.77(MT-A)落地校正**;`v0.75` 同步了 `internal/services/gateway/**`(`admin_memory_tidy.go` / `routes.go` / `deps.go` / `server.go`)、`internal/runtime/memory/**`(`tidy_dryrun.go` / `tidy_recall.go` / `tidy.go`)、`cmd/cyrene/wire_frontend.go`、`frontend/web/src/**` 与本文件;`v0.74` 同步了 `internal/services/gateway/**`(`proactive.go` / `broadcast.go`)、`internal/services/channelbridge/**`(`proactive.go` / `persist.go` / `proactive_notify.go`)、`cmd/cyrene/wire_frontend.go`、`frontend/web/src/lib/ws/handlers/**` 与本文件;`v0.73` 同步了 `internal/services/gateway/**`、`internal/runtime/loop/flow.go`、`internal/runtime/flowgraph/**`、`internal/db/migrations/0022_flow_graphs.*`、`frontend/web/src/**` 与本文件;`v0.72` 同步了 `internal/services/gateway/**`(`proactive*`)、`internal/services/backthink/**`、`frontend/web/src/**` 与本文件;`v0.71` 同步了 `internal/services/gateway/**`、`internal/runtime/loop/flow.go`、`frontend/web/src/**`Line truncated