From ea43f07bce911679087b3efcc4535395f5d41837 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Tue, 6 Oct 2026 18:46:26 +0800 Subject: [PATCH] =?UTF-8?q?docs(contract):=20v0.80=20=E2=80=94=E2=80=94=20?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E8=BD=B4=E5=87=BA=E5=8F=A3=20+=20provider=20?= =?UTF-8?q?=E7=83=AD=E9=87=8D=E8=BD=BD=20+=20AMR=20=E8=90=BD=E5=89=AF?= =?UTF-8?q?=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 三批登记(S6 按代码逐条核对,30 条 文件:行 证据;我复核版本行与唯一当前版本): ① W1 GET /api/v1/admin/flow/timeline(时间正序/游标/retentionHours/truncated,计数 16→17、70→71) ② W3 热重载 + primary/fallback:新增错误码 primary_not_configured(400)/config_unstartable(409), 并改正 3 处已变假话的旧口径(『不热替换、需重启』『无主 provider 字段』) ③ W4 VoiceTranscoder 已注入 ⇒ AMR 落 wav 副本;6 处已变假话处改正 (S6 主动纠正一处口径:微信 iLink 语音刻意不下载字节,不算进『已修』) ⚠️ S6 发现既有记账错误:§2.0 条数自 v0.76(cb232e7) 起少 1(实测应为 14+18+40=72), 已在文档内如实标注请裁决,未擅自回补。 --- Docs/api-client.md | 138 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 105 insertions(+), 33 deletions(-) diff --git a/Docs/api-client.md b/Docs/api-client.md index 9e43e3e..2d8eb0d 100644 --- a/Docs/api-client.md +++ b/Docs/api-client.md @@ -1,12 +1,12 @@ # Cyrene v2 客户端 API 契约 -> **契约版本:`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`)。 +> **契约版本:`v0.80`**(上一版 `v0.79`,其条目见 §6;`v0.78` / `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.79 落地**(切片 L45:DTO `voice` + `GET /api/v1/media/{name}` + 副本落盘;残留缺口——WS 实时帧/握手回放不带 `voice`、AMR 不落副本——见 §4.5 / §7.2 / §7.4-29)。已在 §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 不落副本(**后者已由 v0.80 修复**)——见 §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` 语义都未改)。 @@ -22,9 +22,11 @@ > **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.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.80 是三批增量版(均**已实现**,**纯增量、无破坏性变更**)**:① **链路图时间轴出口**(切片 W1,`design/19 §7`)—— 新增 1 条管理只读端点 **`GET /api/v1/admin/flow/timeline`**(§2.2 B18 / §4.11.1),把「链路图」从「**最近一轮** + 已落盘轮次的**键列表**」扩到**按时间正序的多轮时间轴**(每轮含该轮**每一路**),支持 `limit`(缺省 **50** / 硬上限 **200**)与 `before` 游标向**更早**翻页;响应另给 `retentionHours` / `retentionEnabled` / `truncated`,供面板**如实**标注「更早的轮次已过保留期」。修的是主人 2026-10-06 的原话「web 后台的全链路流程图我要的是**所有消息按时间顺序串起来**的流程图,而不是只有最新一个来回」。老端点 `GET /api/v1/admin/flow/graph`(B13)**逐字未改**(老客户端零改动)。② **搜索 provider 热重载 + 主 provider / 降级链**(切片 W3;裁决 2026-10-06 #1 / #2)—— `plugin.saveProvider` 成功落盘后,组合根**按落盘后的配置重建 web-search 工具并原子替换能力注册表里的条目**(`registry.ReplaceTool`,保留运行期策略)⇒ **下一轮对话即用新配置、不需要重启**;若这次改动会让 web-search **没有可用后端**(下次启动会 fail-fast)⇒ **文件按字节回滚** + **`409 config_unstartable`**。同一次提交可带两个**文件级**字段 `primary` / `fallback[]`(缺位 = 不改动);响应新增机器可读事实 **`hotReload:{applied,registered}`**;新增错误码 **`primary_not_configured`(400)** / **`config_unstartable`(409)**。🔴 **本版把三处已成假话的旧口径改正**(§4.14(b)(c) / §7.2)—— 此前写的「面板保存后**不**热替换运行中的工具、需重启」与「面板**无**主 provider / 降级链字段」**都不再成立**。⚠️ **未确证(如实)**:真机面板上「新增/改一家 provider → 下一次搜索真的走它」的端到端、以及**真机探针打真实上游**,都没跑。③ **入站语音 AMR 也可回放**(切片 W4;裁决 #11)—— 组合根补齐 `channelbridge.Options.VoiceTranscoder` 注入(`cmd/cyrene/wire_channels.go`)⇒ **不可播格式的入站语音在落盘之前先转成 `.wav` 再落副本**(`mime: audio/wav`;本期唯一必需的转码路 = QQ OneBot v11 的 `amr → wav`,见 `internal/runtime/voice/playable.go`),客户端从此能拿到可回放的字节;此前走 `category=transcode_not_wired` 的诚实 WARN、**不落副本**。**形状零变化**(DTO `voice` 的键集合与语义一字未改;可播格式仍**原样落、一次转码都不发生**)。🔴 **同批改正 §4.5 / §7.1 / §7.2 / §7.4 里已成假话的「组合根未注入 `VoiceTranscoder` ⇒ AMR 不落副本」**(`transcode_not_wired` 从此只剩「引擎未装配 / 构造失败」一档;旧版本横幅保留当时状态,另加「已由 v0.80 修复」注)。⚠️ **未确证(如实)**:③ 的**生产端到端未跑**(红线:不重启后端)⇒ 本期证据是 `channelbridge` 的行为单测(真 AMR 夹具 → 真引擎 → RIFF/WAVE)与 `cmd/cyrene` 的**源码级接线用例**。详见 **§6**。 -> **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.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 不落副本**(可播格式照落)—— ✅ **AMR 这一项已由 v0.80(切片 W4)修复**。详见 **§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 那一项**已由 v0.80 修复**)见 **§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**。 @@ -140,16 +142,20 @@ | 档位 | 中间件 | 条数 | 说明 | |---|:--:|:--:|---| | **只读监控档** | `viewerAuth`(owner/admin/viewer) | **14** | 非敏感投影:无凭据、无他人账号数据、无消息正文。**v0.78 新增两条**:面板接入点快照(A13)与插件动作分发(A14,**写门落在 handler 内**,见 A14 行注) | -| **owner/admin 读档** | `writeAuth`(owner/admin) | **16** | viewer 不得看到的一切:记忆、LLM 栈、账号/key、后台思考、审计链、渠道消息日志、完整会话导出、配置事实面(v0.68)、最近一轮链路图快照(v0.71)、记忆整理试算(v0.75)、入库前审查留痕与回滚(**v0.79**,B16/B17) | +| **owner/admin 读档** | `writeAuth`(owner/admin) | **17** | viewer 不得看到的一切:记忆、LLM 栈、账号/key、后台思考、审计链、渠道消息日志、完整会话导出、配置事实面(v0.68)、最近一轮链路图快照(v0.71)、记忆整理试算(v0.75)、入库前审查留痕与回滚(**v0.79**,B16/B17)、**链路图时间轴(v0.80,B18)** | | **owner-only 档** | `ownerAuth`(owner) | **40** | 带审计的策略写、每个获取凭据的渠道端点、其余全部变更 | -**管理路由总数 = 14 + 16 + 40 = 70 条。** +**管理路由总数 = 14 + 17 + 40 = 71 条。** > **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.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**。逐条以代码为准。 +> **v0.80 计数变更**:`writeAuth` 段新增一条(B18 `GET /admin/flow/timeline`,切片 W1)⇒ 16 → **17**、总数 70 → **71**。它只读、不审计,与同族的 B13(`GET /admin/flow/graph`)**同门同档**。 + +> ⚠️ **已知滞后(如实登记,本版未改)**:按上面「计数口径」那句(= 三张清单**列出的行数**)核,**§2.2 当前实际列出 18 行**(B1–B18)、§2.1 = 14 行、§2.3 = 40 行 ⇒ 逐行相加应为 **14 + 18 + 40 = 72**,而本表的「条数」序列记的是 **17 / 71**。该 **−1 滞后自 v0.76 起就存在**(那一次新增 B15 时未同步条数,此后各版沿序列 +1 而未回补),**不是 v0.80 引入的**。本版**沿既有序列只 +1**、**不擅自回补**;**要不要改成 18 / 72(或修正历史序列)由主会话裁决**。 + +> **计数口径(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))**未计入本表**——这是历史口径,本版未改,也**未**把它算进上面的 **17**。逐条以代码为准。 > ⚠️ **与归档版不一致(已更正)**:`Docs/archive/api-client.v0.65.md` §2.0 写「12 + 11 + 38 = 61」(其 v0.62 条目又写「12 + 10 + 38 = 60」,自相矛盾)。**以代码为准 = 12 + 11 + 40 = 63**。 @@ -174,7 +180,7 @@ > **A13 / A14 的角色门依据**(`routes.go` 紧邻注释):快照端点对 viewer 开放是因为它回的**本来就是被裁掉写面的只读投影**;动作端点的读来源动作对 viewer 可见,**写门落在 handler 内**(路由中间件只能有一道门)。`panels_changed` 帧(§3.4.5)**不进** `frameReachesAll` 白名单 ⇒ **viewer 收不到该帧**,需自行刷新/轮询重拉 A13。 -### 2.2 owner/admin 读(16 条,`writeAuth`) +### 2.2 owner/admin 读(17 条,`writeAuth`) | # | 方法 | 路径 | 要点 | |---|---|---|---| @@ -196,6 +202,8 @@ | 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:""}}`;该行不在 `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` 即可) | +| B18 | GET | `/api/v1/admin/flow/timeline` | **链路图时间轴 / 多轮串联(v0.80 新增,切片 W1)**,只读、不审计。`?sessionId=`(省略 = 主会话 `web`)+ `?limit=`(缺省 **50**、> **200** 钳到 200、非整数 `400 bad_request`、`≤0` 同缺省)+ `?before=`(**游标**:取开始时刻**严格早于**它的轮次;省略 = 最新那一页;取值 = 响应里的 `nextBefore`,也接受 RFC3339 或 Unix 毫秒,非法 → `400 bad_request`)。响应 `{data:{sessionId,source,rounds[],nextBefore,hasMore,truncated,retentionHours,retentionEnabled}}`:`rounds` **按时间正序**(最早在前),每项 `{turnId,round,startedAt,endedAt,lanes[]}`(`lanes[]` 每项 `{turnId,graph}`,`graph` 与 `graph_update` 同形);`round` 为 `null` = **未知**(老行),`endedAt` 为空串 = **尚未收尾**(在飞轮);`source` ∈ `persisted`(本页至少一轮来自落盘)/`live`(本页全部来自内存)/`none`。未接线(内存 + 落盘都没接)→ `500 flow_unavailable`。语义与空态见 **§4.11.1** | + > **读取 `GET /admin/observability/{logs,metrics,trace}` 兼任可观测推送的订阅握手**(该凭据的连接全部断开即退订)。REST 响应形状不变。 ### 2.3 owner-only(40 条,`ownerAuth`) @@ -476,11 +484,11 @@ | 状态 | code(示例) | 场景 | |---|---|---| -| 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)) | +| 400 | `bad_request`、`image_too_large`、`too_many_documents`、`invalid_payload`(**v0.79**)、`unknown_provider`(**v0.79**)、`not_configured`(**v0.79**)、`primary_not_configured`(**v0.80**) | 请求体/字段非法、图片超限(`image` **字符串长度**,**仅 base64 / `data:` 形态会触发**,见 §4.5)、附件超条数;**插件动作的载荷形状错 / provider 名未登记 / 该家没配齐**(这三个码自 v0.79 起由动作处理器给出,见 §4.14(b)(c));**面板保存的 `primary` 指名一家「已登记但未配齐」的家**(**v0.80**,写盘**之前**就拒 —— 那份配置会让后端下次起不来,见 §4.14(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 登记、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)) | +| 409 | `chat_busy`、`account_not_registered`、`plugin_disabled`(**v0.78**)、`config_unstartable`(**v0.80**) | 待处理队列满;渠道账号已配置但没有已注册适配器(C19/C20,v0.66);**插件已被显式停用,其声明还在但入口与动作都下线**(v0.78,见 §4.14(b));**这次面板保存的结果会让 web-search 起不来**(主 provider 未配齐 / 未登记 ⇒ 下次启动 fail-fast)⇒ 已**按字节回滚**、运行中的工具保持原样(**v0.80**,见 §4.14(c)) | | 413 | `payload_too_large`、`document_too_large` | 上传/文档超限 | | 415 | `unsupported_media_type` | 图片/文档类型拒绝 | | 429 | `rate_limited` | 限流(框架级) | @@ -648,11 +656,11 @@ **入站语音的音频副本与播放端点(v0.67 登记;✅ **v0.79 已实现**,切片 L45;残留缺口见下)**: -> ✅ **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 副本 → 客户端取到字节」只有替身 + 临时库/临时目录级证据。 +> ✅ **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 / 下表)。🔴 **残留缺口(如实,不美化)**:① ✅ **已修(v0.80,切片 W4 / 裁决 #11)**:组合根**已注入** `VoiceTranscoder`(`cmd/cyrene/wire_channels.go` 的 `channelbridge.Options`)⇒ **不可播格式(`amr`)也落副本**(落盘前先转 `.wav`,`mime: audio/wav`);`transcode_not_wired` 只剩「引擎未装配 / 构造失败」一档。⚠️ **生产端到端仍未跑**;**可播格式(ogg/wav/mp3/m4a/aac/flac/webm/oga/opus)仍原样落、不转码**;② **WS 实时帧 / 握手回放不带 `voice`**(见 §3.4.4 与 §4.4 覆盖率);③ **消息 / 会话被删除时音频副本的引用计数不被释放**(`refs.go` 的增减点只认 `document` 列)⇒ 副本 `ref_count` **偏高**(后果是**晚回收 / 不回收**,绝不早删);④ **生产端到端未跑**(禁止重启后端)⇒ 「真机 IM 语音 → 盘上 0600 副本 → 客户端取到字节」只有替身 + 临时库/临时目录级证据。 > **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 等可播格式照落)。 +- **音频副本何时产生**:渠道适配器入站拿到语音字节后,转写在**流水线最前端**归一(见上),而**落盘在闸门之后** —— 被会话解析 / 黑白名单 / 最小打扰拦下的语音**盘上不留任何文件**(与 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.80 / 切片 W4 起)**:组合根**已注入 `VoiceTranscoder`** ⇒ `amr` 先转 `.wav` 再落副本(`mime: audio/wav`);可播格式仍原样落、**不转码**。⚠️ `transcode_not_wired` 从此只剩「引擎未装配 / 构造失败」一档(**生产装配下不再出现**)。 - **播放端点 `GET /api/v1/media/{name}`**(**owner/admin,必须 Bearer**,与 `GET /api/v1/documents/{name}` 同一套门): | 项 | 口径 | @@ -839,7 +847,7 @@ #### (c) 与 design/19 的关系 -`design/19 §4` 把「多轮历史回放」列为 **P3**;v0.71 落实了 P3 里**最小的一步**(每会话最近一轮的只读快照),**v0.73 把 P3 补全**(本地落盘 + 按 `turnId` 取任意保留期内的轮次 + 轮次列表);**v0.77(切片 FV2)补上 `lanes[]`,并把图按 `{会话}#{路}` 分桶**(此前同一会话并行两路会互相顶掉,见 §7.4-19)。三者都**不改** design/19 §1 的发布会话范围(`web` / `web:` / `sess-backthink`,裁决 01.4.5)、**不改**前端图桶的按会话分桶口径(裁决 16.4.2)。落盘载体是新表 `flow_graphs`(迁移 `0022`),GC 口径照 `events`/`documents`(env 键 + 启动期扫一次 + 定时扫 + 失败不退出)。 +`design/19 §4` 把「多轮历史回放」列为 **P3**;v0.71 落实了 P3 里**最小的一步**(每会话最近一轮的只读快照),**v0.73 把 P3 补全**(本地落盘 + 按 `turnId` 取任意保留期内的轮次 + 轮次列表);**v0.77(切片 FV2)补上 `lanes[]`,并把图按 `{会话}#{路}` 分桶**(此前同一会话并行两路会互相顶掉,见 §7.4-19)。三者都**不改** design/19 §1 的发布会话范围(`web` / `web:` / `sess-backthink`,裁决 01.4.5)、**不改**前端图桶的按会话分桶口径(裁决 16.4.2)。落盘载体是新表 `flow_graphs`(迁移 `0022`),GC 口径照 `events`/`documents`(env 键 + 启动期扫一次 + 定时扫 + 失败不退出)。**v0.80(切片 W1)**另开 **§4.11.1** 的**时间轴**端点(`GET /api/v1/admin/flow/timeline`)负责「**多轮按时间串联**」—— 本小节的语义(最近一轮 / 按 `turnId` 取某一轮 / `lanes[]` / `turns[]`)**一字未改**(两条出口读同一份落盘数据,分工见 §4.11.1(c))。 #### (d) `turnId` / `round` 的增量键与**缺席语义**(v0.77) @@ -855,6 +863,53 @@ --- +### 4.11.1 链路图**时间轴**(`GET /api/v1/admin/flow/timeline`;v0.80 新增,切片 W1) + +**为什么有这条**:`§4.11` 的老端点回答的是「**最近一轮**」与「**已落盘轮次的键列表**(`turns[]`)」—— 两者都不足以把多轮**按时间串起来**;`graph_update` 又只覆盖当前这一轮。于是面板上「所有消息按时间顺序串起来」这件事此前**没有任何数据出口**(主人 2026-10-06 原话)。本端点补上它,且**不改**老端点的任何语义。 + +#### (a) 请求与响应 + +| 项 | 值 | +|---|---| +| 方法 / 路径 | `GET /api/v1/admin/flow/timeline` | +| 鉴权档 | `writeAuth`(**owner / admin**;`viewer` → `403`,未认证 → `401`)。与 B13 / `design/19 §3` 同档 | +| 查询参数 | `sessionId`(**可选**;省略 / 空串 ⇒ 主会话 `web`)、`limit`(**可选**;缺省 **50**、硬上限 **200**(超上限即钳住)、`≤0` 同缺省、非整数 → `400 bad_request`)、`before`(**可选游标**;省略 = 最新那一页;给定时只返回**开始时刻严格早于它**的轮次;取值 = 上一次响应里的 `nextBefore`,也接受 RFC3339 或 Unix 毫秒;非法 → `400 bad_request`) | +| 响应体 | `{data:{sessionId,source,rounds[],nextBefore,hasMore,truncated,retentionHours,retentionEnabled}}` | +| `rounds[]` | **恒为非 null 数组**(没有轮次时是 `[]`),按**时间正序**(最早在前)。每项 `{turnId,round,startedAt,endedAt,lanes[]}`;`lanes[]` 恒为非 null 数组(每项 `{turnId,graph}`,按路的开始时刻升序) | +| `rounds[].turnId` | 这一轮**首路**的**图 id**(`graph.id`,真 id)。多路时它只代表首路,全貌在 `lanes[]`(每路的图 id 见 `lanes[].graph.id`、每路的**路标识**见 `lanes[].turnId`)⚠️ 与 `lanes[].turnId` **不同源**(前者是图 id = 老端点的 `?turnId=` 参数值;后者是路标识,见 §4.11(d)) | +| `rounds[].round` | number 或 **`null`**。`null` = **未知**(那批行早于轮次号落库)⇒ 客户端**不要**当成 `0`;**不保证连续**(轮次号 = 该轮首路的建图序号,真并行时中间会跳号) | +| `rounds[].startedAt` / `endedAt` | **UTC RFC3339 毫秒**(与本端点游标同一口径)。`endedAt` 为**空串** = 这一轮**尚未收尾**(内存里的在飞轮)⇒ 面板显示「进行中」,**不得**补一个结束时刻 | +| `rounds[].lanes[].graph` | **与 `graph_update` 帧的 `data` 逐字同形**(§3.4.5;`{id,nodes[],edges[],sessionId,turnId?,round?,startedAt,endedAt}`,节点 `props`/`log` 同 §1.1 的脱敏口径) | +| `source` | `"persisted"`(本页**至少有一轮**来自落盘;可能同时含内存里的在飞路)/`"live"`(本页**全部**来自内存:落盘源未接线,或该会话还没有落盘轮)/`"none"`(没有任何轮次) | +| `nextBefore` | 本页**最早一轮**的开始时刻(无轮次时 `""`)。`hasMore:true` 时**原样回带**成下一次请求的 `before` 即可继续向更早翻页 | +| `hasMore` | 库里**还有更早的轮次**没被本次返回(用 `nextBefore` 继续取) | +| `truncated` | 本次**不是全部**:`hasMore`(分页未到底)**或** 落盘源未接线(只剩内存最近一轮)。面板据此标注「更早的轮次未显示」,**绝不静默少一块** | +| `retentionHours` / `retentionEnabled` | 落盘保留期(小时,**下取整**)与它**是否启用**(`FLOW_GRAPH_RETENTION`,既有 env 键、默认 168h)。`retentionEnabled:false` ⇒ 「更早的轮次」**不是**被清扫掉的,**不要**说成过期 | +| 归档 | **不审计**(只读查询,与 B13 同档) | +| 失败 | 内存与落盘**都没接线** ⇒ `500 {"code":"flow_unavailable"}`(**不假装「这个会话没有轮次」**);落盘读取失败 ⇒ 同样 `500 flow_unavailable`(**不把「读库失败」伪装成「没有这一轮」**);`limit` / `before` 非法 ⇒ `400 bad_request` | + +#### (b) 语义硬约束(客户端必须照这个读) + +1. **它是「多轮并联」,不是「最近一轮」。** 老端点(§4.11)继续负责「最近一轮 + `lanes[]` + `turns[]`」,本端点负责「按时间串起来的多轮」。两者**读同一份落盘数据**,但**语义不同**:本端点的 `rounds[]` 是**时间轴**(正序、分页),老端点的 `graph` 是**你要的那一轮**。 +2. 🔴 **一句话轮(round)与路(leg)不可混用**(`design/19 §6.0`):`rounds[]` 的一格 = **一轮**(一条用户消息引出的全部工作),一轮里**每一路**在 `lanes[]` 里各有 `{turnId,graph}`。真并行时同一轮会有 2~3 条路 —— **不要把两条路画成两轮**。 +3. 🔴 **保留期只说真话**:过期轮次**不会**出现在 `rounds[]` 里(已被清扫),本端点**不猜、不补空块**。面板要说明「更早的轮次已过保留期」时,判据只能是 `retentionEnabled` + `retentionHours` + `truncated`(三者一起读),**不得**因为「返回的轮次比想象中少」就断言被清扫了。 +4. **分页不重不漏**:`before` 恒为「严格更早」,`nextBefore` 恒为「本页最早一轮」。逐页往更早翻时,**已返回过的轮次不会再出现**。⚠️ 游标精度到**毫秒**(与 `startedAt` 字符串同一口径);同一毫秒里启动的两个**不同**轮次在极端情况下可能被一并跳过 —— 实际不成立(同一会话的轮次启动间隔远大于 1ms),但这是**真实存在的理论边界**,不作隐瞒。正解(若将来需要)是复合游标 `(startedAt, roundKey)`,属**新契约**、本版未做。 +5. **内存里的在飞轮会出现**(`source` 可能是 `live`、`endedAt` 可能是空串),这是**有意**的:面板打开时最新一轮还在跑,不该在时间轴上缺席。它与刚落盘的那一份**同一条路只出现一次**(按图 id 去重,以落盘那份为准)。 +6. **只读、不发布、不落盘**:拉时间轴**不产生** `graph_update` 帧、不改任何会话的实时图、不动保留期、不写审计。 +7. **白名单外的会话是诚实空态**:隔离渠道会话(群聊/陌生人 `sess-xxx`)从不建图、也从不落盘 ⇒ `rounds:[]` + `source:"none"`。**这不是**「这一轮没有链路」。 +8. **不替代 `graph_update`**:实时帧仍是「正在跑的那一轮」的实时来源;本端点补的是「多轮 + 已错过帧」。 + +#### (c) 与老端点 `turns[]` 的分工(别把两者混成一条时间轴) + +`§4.11` 的 `turns[]` 是**按路(leg)**列的条目(一轮两路会显示两条,形状 `{turnId,createdAt}`);本端点的 `rounds[]` 是**按轮(round)**分组的时间轴(一轮两路并进同一格的 `lanes[]`)。⇒ 面板要「轮次列表(含各路)」就用本端点;要「按图 id 精确取某一轮」仍用老端点的 `?turnId=`。 + +#### (d) 未确证(如实) + +- **`graph_json` 破损行**:SQL 侧用嵌套 `json_valid` **保护**(坏行不会拖垮整条查询),但该行若落进本页,解码会**如实报错** ⇒ `500 flow_unavailable`(与既有 `graph` / `graph_turns` 口径一致:读不懂就报错,绝不把坏行当「没有图」)。**这条路径没有测试**。 +- **前端渲染与真机面板联调**不在本条证据内(后端出口的单测覆盖见切片 W1 报告);**未重启后端**、未跑真机。 + +--- + ### 4.12 记忆整理「试算」(`POST /api/v1/admin/memories/tidy/dry-run`;v0.75 新增) **为什么有这条**:记忆整理是一件**低峰定期后台任务**(`design/23 §4`):默认只在安静时段(`MEMORY_TIDY_QUIET_START_HOUR`–`_END_HOUR`,默认 `23:00–07:00`)由心跳开跑。于是在它第一次开跑之前,库里**既没有候选也没有轮次留痕** ⇒ 面板只能空着,主人无法判断「整理会不会乱合并」。本端点把**同一套规则**当场算一遍:候选直接回在响应里,**一行都不写**。 @@ -1037,9 +1092,9 @@ **审计**:**每一次结算都落一行**,`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.79 的生产路径事实(切片 L1/PLG-2 执行面接线)**:组合根在 `wirePanelHub` 里调用 `wireBuiltinPluginActions`,把内置 `web-search` 的**四个动作 + 下拉候选来源**登记到面板中枢 ⇒ **这些动作真的执行**(真读写可变源、探针真发一次 HTTP),**不再一律 501**;装配期另把「**声明了却没有实现**」的动作与候选来源各记一条 WARN(`action_not_implemented` 因此是**缺口信号**而非常态)。⚠️ **残留(如实,见 §7.2)**:面板保存后**不热替换运行中的 web-search 工具**(改的是可变源文件,工具仍用启动期接线 ⇒ 需重启生效,与面板文案一致);探针**只用 `httptest` 假上游**验证过,**未对真实上游做过一次连通验证**。 +> ✅ **v0.79 的生产路径事实(切片 L1/PLG-2 执行面接线)**:组合根在 `wirePanelHub` 里调用 `wireBuiltinPluginActions`,把内置 `web-search` 的**四个动作 + 下拉候选来源**登记到面板中枢 ⇒ **这些动作真的执行**(真读写可变源、探针真发一次 HTTP),**不再一律 501**;装配期另把「**声明了却没有实现**」的动作与候选来源各记一条 WARN(`action_not_implemented` 因此是**缺口信号**而非常态)。⚠️ **残留(如实,见 §7.2)**:✅ **v0.80 起已热重载**(保存后重建工具 + 原子替换注册表条目,**下一轮生效、不需要重启**;若新配置会让后端下次起不来则**按字节回滚**并回 **`409 config_unstartable`**);探针**只用 `httptest` 假上游**验证过,**未对真实上游做过一次连通验证**(此项仍未做)。 -#### (c) `web-search` 插件的动作面(v0.78 声明,**v0.79 起真可用**,切片 PLG-4 / L1;供上面 (b) 的分发面使用) +#### (c) `web-search` 插件的动作面(v0.78 声明,**v0.79 起真可用**,切片 PLG-4 / L1;**v0.80 起可配主 provider / 降级链 + 保存后热重载**,切片 W3;供上面 (b) 的分发面使用) 插件 id `web-search`;可变源落点 `$DATA_DIR/config/plugins/web-search.json`(**0600**、temp+rename)—— ⚠️ **不进客户端契约**(客户端一律走动作端点,不直连文件)。组合根「**先读可变源、缺失回落 `.env` 播种**」:文件一旦落盘,`.env` 的 `WEB_SEARCH_*` **只在文件缺失时**起作用(⚠️ **运维视角的行为变更**,见 §5)。🔴 **不新增任何 env 键**(`.env` 已冻结为引导面;本切片未改 `.env` / `.env.example`)。 @@ -1051,13 +1106,19 @@ |---|---|---|---|---| | `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.saveProvider` | 新增/修改一条(按 `provider` **upsert**);**同一次提交可带两个文件级字段** `primary` / `fallback`(主 provider 与降级链,**缺位 = 不改动**) | write(form `submit`) | `{"values":{"provider":"bocha","apiKey":"…","baseUrl":"https://…","enabled":true,"primary":"bocha","fallback":["searxng"]}}` | `{"saved":true,"provider":"bocha","created":true,"enabled":true,"primary":"bocha","fallback":["searxng"],"hotReload":{"applied":true,"registered":true}}`(`primary` / `fallback` 是**落盘后**的取值,没给也如实回显;`hotReload` 见下) | | `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.reauthProvider` | 重配凭据:把该条带回表单 | write(table `rowActions`) | `{"row":{"provider":"bocha"}}` | `{"values":{"provider":"bocha","apiKey":"****cdef","baseUrl":"https://api.bocha.cn","enabled":true,"primary":"bocha","fallback":["searxng"]}}`(**`apiKey` 只回掩码**;前端对 secret 字段一律不回填输入框,**空 = 不改动**)。⚠️ **v0.80 起 `values` 另带两个文件级字段** `primary`(字符串)/ `fallback`(字符串数组)—— **不带它们**的话,面板「清空降级链」在客户端三态写回里会落成「缺位 = 不改动」(**清不掉**) | `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`。 -落盘:`$DATA_DIR/config/plugins/web-search.json`(0600 + temp+rename),**重启后仍然生效**;⚠️ 本片**不**热替换运行中的 web-search 工具(面板保存后工具仍用启动期接线,需重启)。 +**(v0.80 新增)两个文件级字段的三态写回**(与条目字段同一套哨兵语义):`values.primary` **缺位** ⇒ 保留盘上值;给字符串(含空串 / `off` / `none`)⇒ 采用 —— **空串与 `off` / `none` 都是「关掉这把工具」的哨兵**(`off` / `none` 大小写与空白不敏感),`WEB_SEARCH_PROVIDER` 与面板**共用同一套关闭语义**(不另造第二套)。`values.primary` 指名一家**已登记但未配齐**的家 ⇒ **`400 primary_not_configured`**(**写盘之前**就拒,因为那份配置会让后端下次起不来)。`values.fallback` **缺位** ⇒ 保留;给数组(或逗号 / 顿号 / 分号 / 换行分隔的字符串)⇒ 采用(去空白、去重、丢空项);元素**未登记** ⇒ `400 unknown_provider`;给非数组 / 非字符串 ⇒ `400 invalid_payload`(**不静默当空数组** —— 那等于一次保存悄悄清空降级链)。降级链**允许**放本期已登记但**尚未配齐**的家(运行期会跳过它)。 + +落盘:`$DATA_DIR/config/plugins/web-search.json`(0600 + temp+rename),**重启后仍然生效**,且 **v0.80 起保存后立即生效(热重载)**:`plugin.saveProvider` 成功落盘后,组合根会**按落盘后的配置重建 web-search 工具并替换能力注册表里的条目** ⇒ **下一轮对话即用新接线,不需要重启后端**(design/09 §5.5 的端到端判据)。响应新增机器可读事实 **`hotReload:{applied,registered}`**:`applied=true` 表示这次保存真的触发了热重载(`false` = 宿主没有能力注册表可换,只落了盘);`registered` 报告保存**之后** web-search 在不在模型的工具面里(主 provider / 端点被显式关闭时为 `false` —— 这是设计语义,不是失败;⚠️ `applied:false` 时该键**恒为 `false`**、不携带信息)。 + +**保存是一次事务**:若这次改动会让 web-search **没有可用后端**(design/27 §3 的启动期 fail-fast:主 provider 未登记 / 未配齐),端点返回 **`409 config_unstartable`**,且**文件按字节回滚**、运行中的工具保持原样(决不留「盘上新、内存旧」,也决不留「重启后起不来」的配置)。⚠️ 回滚本身也失败时**如实**回 **`500 config_write_failed`** 并明说「盘上可能已经变了」,**不假装改动没发生**。 + +⚠️ **触发面只有本动作**:直接改写可变源文件(不经面板)**不会**触发热重载 ⇒ 仍需重启后端(或从面板点一次保存)。这是本动作的**明确边界**(不做文件监听)。 `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 与凭据(只给稳定码 + 固定文案)。 @@ -1138,17 +1199,28 @@ > 只保留最近若干版(**v0.60 起**);更早的全部历史见归档 `Docs/archive/api-client.v0.65.md` §5(v0.28 → v0.59)。 > 每版一句话 + 三档标注。**三档定义见 [§0.4](#04-三档标注惯例)。** -### v0.79(当前契约版本) +### v0.80(当前契约版本) + +**三批增量 —— ① 链路图时间轴出口(切片 W1)|② 搜索 provider 热重载 + 主 provider / 降级链(切片 W3)|③ 入站语音 AMR 也可回放(切片 W4)**(依据:主人 2026-10-06 要「web 后台的全链路流程图 = **所有消息按时间顺序串起来**,而不是只有最新一个来回」;面板的意义就是「不改 `.env` 不重启」⇒ 配置面必须真热生效;微信 / QQ 的 AMR 入站语音不落副本 = 无法回放 / 多端同步。) + +- **① 链路图时间轴出口(切片 W1,`design/19 §7`)**:新增 1 条 `writeAuth`(owner\|admin)**只读、不审计**端点 **`GET /api/v1/admin/flow/timeline`**(`?sessionId=&limit=&before=`)—— 把多轮链路图**按时间正序**返回,每轮 `{turnId,round,startedAt,endedAt,lanes[]}`(`lanes[]` 每项 `{turnId,graph}`,`graph` 与 `graph_update` 同形)。数据源 = **已落盘的 `flow_graphs`**(迁移 `0022`/`0023`,保留期 `FLOW_GRAPH_RETENTION` 默认 168h)**+ 内存里的最近一轮**(在飞 / 刚落盘失败的那一轮);`limit` 缺省 **50** / 硬上限 **200**(超上限钳住、`≤0` 同缺省、非整数 `400 bad_request`);`before` 游标取「开始时刻**严格早于**它」的轮次、响应回带 `nextBefore`(**原样回带即可连续翻页**,分页不重不漏);响应另给 `retentionHours` / `retentionEnabled` / `truncated` 供面板**如实**标注「更早的轮次已过保留期」。**未接线(内存 + 落盘都没接)⇒ `500 flow_unavailable`**,**绝不回空 `rounds`**(不假装「这个会话没有轮次」)。权限矩阵:`writeAuth` 16 → **17**、总数 70 → **71**。🔴 **老端点 `GET /admin/flow/graph`(B13)逐字未改**(`admin_flow.go` 零改动,`routes.go` 只加一行路由 + 注释)⇒ **老客户端零影响**。**§2.0 / §2.2 B18 / §4.11.1**。 +- **② 搜索 provider 热重载 + 主 provider / 降级链(切片 W3;裁决 2026-10-06 #1 / #2)**:`plugin.saveProvider` 成功落盘后,组合根**按落盘后的配置重建 web-search 工具并原子替换能力注册表里的条目**(新增窄方法 `registry.ReplaceTool` —— **不是** `Remove`+`Register`:中间无空窗,且**保留运行期策略**)⇒ **下一轮对话即用新配置,不需要重启**(design/09 §5.5)。**保存是一次事务**:快照文件 → 落盘 → 重建 → 换条目;③ 失败 ⇒ **文件按字节回滚**(temp+rename)+ **`409 config_unstartable`**(会 make-下次起不来的配置绝不留在盘上);回滚也失败 ⇒ 如实 `500 config_write_failed`。响应新增机器可读事实 **`hotReload:{applied,registered}`**。声明面**增补两个文件级字段** `primary`(`select`)/ `fallback`(`multiSelect`,候选来源 `plugin:providers`)—— **缺位 = 不改动**;`primary` 给空串 / `off` / `none` = **关掉这把工具**(与 `WEB_SEARCH_PROVIDER` 共用同一套关闭语义);点名「已登记但未配齐」的家 ⇒ **`400 primary_not_configured`**(**写盘之前**就拒);`fallback` 元素未登记 ⇒ `400 unknown_provider`、形状错 ⇒ `400 invalid_payload`。`plugin.reauthProvider` 的 `values` **追加** `primary` / `fallback`(否则面板「清空降级链」在客户端三态写回里清不掉)。**新增错误码**:`primary_not_configured`(400) / `config_unstartable`(409)。🔴 **本版改正三处已成假话的旧口径**(此前写「面板保存后**不**热替换运行中的工具、需重启」、「面板**无**主 provider / 降级链字段」):§4.14(b) / §4.14(c) / §7.2 / §7.4-26 / §7.4-27 已改。⚠️ **触发面只有本动作**:直接改写可变源文件(不经面板)**不**触发热重载,仍需重启。**§4.1 / §4.14(b)(c) / §7.2**。 +- **③ 入站语音 AMR 也可回放(切片 W4;裁决 #11)**:组合根补齐 `channelbridge.Options.VoiceTranscoder`(`cmd/cyrene/wire_channels.go`)—— 注入的是 `wireTranscode` 装配的**同一个**引擎实例(与工具面 `transcode` 工具同源,不是第二条降级链)。⇒ **不可播格式的入站语音在落盘之前先转成 `.wav` 再落副本**(`mime: audio/wav`;本期唯一必需的转码路 = QQ OneBot v11 的 `amr → wav`),客户端的 `voice` 从此带副本事实(`name` / `mime` / `size` / `durationMs` / `url`);**此前这些键缺席**(走 `category=transcode_not_wired` 的诚实 WARN、不落副本)。**形状零变化**(DTO `voice` 的键集合与语义一字未改);**可播格式**(ogg/wav/mp3/m4a/aac/flac/webm/oga/opus)仍**原样落、一次转码都不发生**。稳定 `category` 列表**不变**(10 类),但 `transcode_not_wired` 在生产装配下**不再出现**(它现在只剩「引擎未装配 / 构造失败」这一档)。🔴 **本版改正 6 处已成假话的「组合根未注入 `VoiceTranscoder` ⇒ AMR 不落副本」**:§4.5(缺口① + 现状行)/ §7.1(对照注)/ §7.2(语音面行 ②)/ §7.4-16(前向提示)/ §7.4-29(②)/ 旧版本横幅与 §6 旧版条目(v0.67 / v0.78 / v0.79)。**§4.5 / §7.2 / §7.4-29**。 +- **行为变更**:① 无(新端点;老端点逐字未改);② **有**(面板保存由「需重启生效」变为**下一轮即生效**;新增两个可配字段与两个错误码;`plugin.saveProvider` / `plugin.reauthProvider` 的响应形状**加键**);③ 无(DTO `voice` 的键集合与语义未变,只是**此前缺席的键现在会出现**)。 +- **破坏性**:**无**(新端点 / 新可选键 / 新错误码;新增的响应键都是**加性**的,老客户端不读即行为不变)。 +- ⚠️ **诚实边界**:① 时间轴的**前端渲染 / 真机面板联调**不在后端出口的证据内;`graph_json` 破损行的 `500` 路径**无测试**;游标精度到**毫秒**(同一毫秒起跑的两个不同轮次在极端情况下可能被一并跳过 —— 理论边界,已写进 §4.11.1(b)-4)。② 热重载的**真机面板端到端未跑**、**真机探针未打真实上游**;**启用态不跨重启持久化**。③ 语音**生产端到端未跑**(红线:不重启后端)⇒ 证据是 `channelbridge` 行为单测(真 AMR 夹具 → 真引擎 → RIFF/WAVE)与 `cmd/cyrene` 的**源码级接线用例**;WS 实时帧 / 握手回放**仍不带 `voice`**;删除消息 / 会话时音频副本引用计数**仍不释放**。逐条见 **§7.2 / §7.3 / §7.4**。 + +### 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**。 +- **① 插件面板动作真可用(切片 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,**重启仍生效**;**v0.80 起保存后即热重载**,见 §6 v0.80 ②)、探针**真发一次最小 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**。 +- **③ 语音消息的用户可见面(切片 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 不落副本**(可播格式照落)—— ✅ **该缺口已由 v0.80(切片 W4)修复**(组合根已注入,见 §6 v0.80 ③ / §4.5 / §7.2)。**§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**。 +- ⚠️ **诚实边界**:① 面板动作**热重载未做**(保存后运行中的 web-search 工具不换接线,需重启)—— ✅ **已由 v0.80(切片 W3)修复**(见 §6 v0.80 ②)、**探针只用 `httptest` 假上游**(未对真实上游连通,**仍未做**);② 回滚端点与 `design/23 §11` 的 owner-only 口径**有差异**(待裁决)、留痕列表**无分页游标**、`memory_ingest_reviews` 无 TTL/GC;③ 语音**生产端到端未跑**、WS 实时帧/握手回放不带 `voice`、AMR 不落副本(✅ **已由 v0.80 修复**)、删除消息/会话时音频副本引用计数不释放;④ 单路 `toolCalls[]` 的**HTTP 出口端到端未由本切片确证**(写侧已验,网关读侧由主会话补验)、`turns[]` 仍按**路**列条目、真机未验。逐条见 **§7.2 / §7.4**。 ### v0.78(上一版) @@ -1159,10 +1231,10 @@ - **③ 搜索 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}`)在 v0.78 时未实现,已由 v0.79 落地**(切片 L45,见 §6 v0.79 ③);**残留缺口**(WS 实时帧/握手回放不带 `voice`、AMR 不落副本)见 §4.5 / §7.2。唯一已生效的**客户端可见**变化: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 那一项**已由 v0.80 修复**)见 §4.5 / §7.2。唯一已生效的**客户端可见**变化:IM 语音在「格式不在可转写白名单内」时正文诚实提示改口径(**形状不变**,客户端本就不该按正文文案分支)。**§4.5 / §7.2**。 - **行为变更**:① 无(新可选键 + 新迁移列/索引);② 无(新端点 / 新帧 / 新可选键;老客户端零影响)—— ⚠️ 但**新增了 `502` 状态码**(仅插件动作执行器失败路径)与一批插件动作错误码;③ **有**(运维视角:`.env` 的 `WEB_SEARCH_*` 仅在可变源文件缺失时生效);④ 无(新帧 + 新事件行;REST 形状未变);⑤ 无(两个可选键;`status`/`botId`/`botToken`/`err` 与 v0.39 起逐字一致);⑥ **有(轻微)**:IM 语音「不可转写格式」的正文诚实提示**改口径**(**形状不变**)。 - **破坏性**:**无**。 -- ⚠️ **诚实边界**:① `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 不落副本。 +- ⚠️ **诚实边界**:① `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.80 修复**(见 §6 v0.80 ③)。 ### v0.77(上一版) @@ -1292,7 +1364,7 @@ **三批已在设计文档定稿的新增面一次性登记**:语音消息(播放端点 + 消息 DTO `voice` + 音频副本口径)、非消息事件查询面、账号在线状态(依据 `design/24 §10.11`、`design/26 §4.5` / `§2.6`)。登记时三批**都还没有实现**;其中**账号在线状态**(`52dac45`)与**非消息事件查询面**(本切片 E6)**已落地**(见下)。 -> ⚠️ **本版是「契约先行」登记**:登记的三批里,**账号在线状态**(见 §2.4(b))与**非消息事件查询面**(见 §2.4(a))**已落地**;**语音消息**当时未实现,那一批的形状**取自设计文档而非代码核对** —— **v0.79 已把用户可见面落地**(切片 L45,见 §6 v0.79 ③ / §4.5 / §7.2),**残留缺口**(WS 实时帧/握手回放不带 `voice`、AMR 不落副本)仍按「未确证/未实现」读。 +> ⚠️ **本版是「契约先行」登记**:登记的三批里,**账号在线状态**(见 §2.4(b))与**非消息事件查询面**(见 §2.4(a))**已落地**;**语音消息**当时未实现,那一批的形状**取自设计文档而非代码核对** —— **v0.79 已把用户可见面落地**(切片 L45,见 §6 v0.79 ③ / §4.5 / §7.2),**残留缺口**(WS 实时帧/握手回放不带 `voice`;AMR 的那一项**已由 v0.80 修复**)仍按「未确证/未实现」读。 - **纯增量**: ① 新端点 `GET /api/v1/media/{name}`(用户侧,**owner/admin 必须 Bearer**):`200` 字节流 / `206 Partial Content` / `304 Not Modified`;`Content-Type: audio/*`;`Content-Disposition: inline; filename=""`;`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)。 @@ -1391,7 +1463,7 @@ ### 7.1 在飞切片 -**v0.66 及更早登记的在飞项已全部冻结、客户端可直接依赖**(下表)。⚠️ **v0.67 新登记的三批中,账号在线状态(`accounts[].accountOnline`)与非消息事件查询面都已落地**(见 §7.2);**语音消息的用户可见面已由 v0.79 落地**(仍残留 WS 帧/握手回放不带 `voice`、AMR 不落副本 —— 见 §7.2 / §7.4-29),**不在本表**,逐条见 §7.2。 +**v0.66 及更早登记的在飞项已全部冻结、客户端可直接依赖**(下表)。⚠️ **v0.67 新登记的三批中,账号在线状态(`accounts[].accountOnline`)与非消息事件查询面都已落地**(见 §7.2);**语音消息的用户可见面已由 v0.79 落地**(仍残留 WS 帧/握手回放不带 `voice`;AMR 不落副本**已由 v0.80 修复** —— 见 §7.2 / §7.4-29),**不在本表**,逐条见 §7.2。 | 项 | 状态 | 冻结位置 | |---|---|---| @@ -1408,7 +1480,7 @@ | 项 | 现状 | |---|---| -| **✅ 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 /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`**;② ✅ **已修(v0.80,切片 W4 / 裁决 #11)**:组合根**已注入 `VoiceTranscoder`** ⇒ **AMR 也落副本**(落盘前转成 `.wav`,`mime: audio/wav`);`transcode_not_wired` 只剩「引擎未装配 / 构造失败」一档(**生产装配下不再出现**);③ **消息/会话被删除时音频副本引用计数不被释放**(`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 注 | @@ -1422,7 +1494,7 @@ | **记忆整理候选的 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`) | **已实现(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 插件动作的「执行器」**(`POST /admin/plugins/{id}/actions/{action}` 的动作本体) | **已接线(v0.79,切片 L1/PLG-2 执行面接线)**:插件管理器新增**动作处理器登记面**(`Manager.RegisterActions`;单一全局 `SetExecutor` 已删除),组合根在 `wirePanelHub` 里登记内置 `web-search` 的四个动作 + 候选来源 `plugin.providers` ⇒ **这些动作真执行**(真读写可变源、探针真发一次 HTTP)。**`501 action_not_implemented` 只在「已声明但宿主未登记处理器」时出现**(不再是兜底档)。✅ **v0.80(切片 W3)**:面板保存后**热重载**(重建工具 + 原子替换注册表条目,**下一轮生效、不需重启**;会让后端下次起不来的配置 ⇒ **按字节回滚** + `409 config_unstartable`);面板**已可配主 provider / 降级链**(`primary` / `fallback[]`)。⚠️ **残留**:**探针只用 `httptest` 假上游**(未对真实上游连通);**启用态不跨重启持久化**;**真机面板端到端未跑**。见 §4.14(b)(c) / §7.4-26 / §7.4-27 | | **🔴 v0.78 热装插件的面板声明来源**(`plugins//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` 登记理由(**不在本版**) | @@ -1473,7 +1545,7 @@ 13. **(v0.67 登记 · 已确证)`accounts[].accountOnline` 的字段名与是否有 `known` 同级键**:字段名 `accountOnline` 为本契约所拟(设计只规定「独立状态面 / `AccountStatus()` / `known` 三态」,未给管理面字段名),**实现沿用了它、未改名**;**不发** `known` 同级键 —— 「未知」由**键缺席**承载。哪些平台会带该键**已确证**:只有适配器实现了 `AccountStatus()` 的平台(当前仅 `qq-obv11`)。 14. **(v0.67 登记)音频各容器在各客户端的可播放性**:`amr` / `flac` / `opus` 在部分浏览器内核上播不了(design/24 §10.6 明确「未逐一实测」)⇒ 客户端**以实测为准**,播不了就照实提示。 15. **(v0.67 登记)`GET /admin/system/storage` 的 `documents` 段是否拆出语音副本**:设计**本期不拆**(不新增 `kind` 列 / 不按 ext 聚合)⇒ 该段**只增口径、不变形状**,客户端**不要**指望能分开统计。 -16. **(v0.67 登记 · 前向提示)`voice.url` 指向的可能不是原件,而是转码后的副本**:`design/24 §10.14` 已把「源格式不可播 ⇒ 转码成可播副本(唯一必需的路 = `amr → wav`)」立为口径,转码能力规格见 `design/25-转码能力.md`(**该能力尚未接线**)⇒ 客户端**不要假定 `url` 一定是某一种音频格式**,一律**按响应的 `Content-Type` / `mime` 如实处理**,播不了就照实提示(**绝不给一个播不了的 `url` 假装能播**)。⚠️ 本条**不在 `design/24 §10.11` 的差异清单内**(§10.11 与 §10.13 均无「零转码」绑定表述),登记它是因为「`url` 的字节格式」会直接影响播放端;**最终口径以 `design/25` 为准**。 +16. **(v0.67 登记 · 前向提示)`voice.url` 指向的可能不是原件,而是转码后的副本**:`design/24 §10.14` 已把「源格式不可播 ⇒ 转码成可播副本(唯一必需的路 = `amr → wav`)」立为口径,转码能力规格见 `design/25-转码能力.md`(**已接线**:工具面 `transcode` 工具(`cmd/cyrene/wire_transcode.go`)+ 入站语音副本归一;**v0.80 切片 W4 补齐了 `channelbridge` 侧的 `VoiceTranscoder` 注入** ⇒ `amr → wav` 这条路在生产装配下**真的会走**)⇒ 客户端**不要假定 `url` 一定是某一种音频格式**,一律**按响应的 `Content-Type` / `mime` 如实处理**,播不了就照实提示(**绝不给一个播不了的 `url` 假装能播**)。⚠️ 本条**不在 `design/24 §10.11` 的差异清单内**(§10.11 与 §10.13 均无「零转码」绑定表述),登记它是因为「`url` 的字节格式」会直接影响播放端;**最终口径以 `design/25` 为准**。 17. **(v0.68 登记 · v0.70 更正)`urgency` 的「下限」与「精确」之分**: - **v0.68 原文(当时成立,现已过期)**:`urgency` 一律是「档位下限」,`warning` 读作「至少 `high`(也可能是 `critical`)」;**窗口之外的轮次不下发该键**(`backthink.Record` 不带重要度、推送成功的行 `detail` 被投递说明覆盖 ⇒ 无损精确档位已不可能),要精确档位需 backthink 侧把每轮 `Import` 落进记录(当时未做)。 @@ -1498,14 +1570,14 @@ 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 登记 · 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` 的前缀拼接姿势(相对片段,前端拼 `/plugin//`)。**v0.79 新增未确证项**:① 面板动作(`plugin.listProviders` 等)在**真机上**真的回 200 + `result`(单测层已用 `httptest` 真发 HTTP 验过探针);② **真机探针未打过真实上游**(无 key);③ **面板保存 provider 后运行中的 web-search 工具不换接线**(需重启,未在真机验过重启后的生效)。另:`panels_changed` 的分组文案「插件」是**宿主文案**(design 未规定文案来源),改口径只改一个函数。⚠️ 且**未换产物前**,旧二进制里**没有**这些路由,属预期。 +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` 的前缀拼接姿势(相对片段,前端拼 `/plugin//`)。**v0.79 新增未确证项**:① 面板动作(`plugin.listProviders` 等)在**真机上**真的回 200 + `result`(单测层已用 `httptest` 真发 HTTP 验过探针);② **真机探针未打过真实上游**(无 key);③ ~~面板保存 provider 后运行中的 web-search 工具不换接线(需重启)~~ ⇒ **v0.80(切片 W3)已改为保存后热重载**(`hotReload:{applied,registered}`),**真机端到端仍未跑**(真机上「保存后下一轮真的走新配置」未验;未换产物前旧二进制里仍是启动期接线)。另:`panels_changed` 的分组文案「插件」是**宿主文案**(design 未规定文案来源),改口径只改一个函数。⚠️ 且**未换产物前**,旧二进制里**没有**这些路由,属预期。 -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 ①)。 +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 → **下一次搜索真的走它**」这条**端到端**(✅ v0.80 起**保存后工具热重载**:换注册表条目、**下一轮生效**(见 §4.14(c)),但**真机端到端仍未跑**,见 §7.2 / §6 v0.80 ②)。 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 登记 · 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` 语音**无副本、不可回放**是**刻意**的(平台自带转写就不白下载音频)。 +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 副本 → 客户端取到字节」只有**替身 + 临时库/临时目录**级证据;② ✅ **已修(v0.80,切片 W4 / 裁决 #11)**:组合根**已注入 `VoiceTranscoder`** ⇒ AMR 转 `.wav` 后落副本 ⇒ 「Web 上能点开听 **AMR** 语音」的**接线**已成立(① 的**生产端到端仍未跑**,未重启后端);③ **`message_persisted` / `channel_persisted` / 握手 `history` 回放不带 `voice`**(见 §4.4 / §7.3);④ `amr` / `flac` / `opus` 在部分浏览器内核上**可播放性未逐一实测**(判据是设计登记的 MDN 规范级结论);⑤ **删除消息/会话时音频副本引用计数不释放**(`ref_count` 偏高 ⇒ 晚回收,绝不早删)。`weixin-ilink` 语音**无副本、不可回放**是**刻意**的(平台自带转写就不白下载音频)。 --- -*本文件由「以代码为真源」维护(当前 `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/**` 与本文件。* +*本文件由「以代码为真源」维护(当前 `v0.80`;`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.80`(三批 —— W1 链路图时间轴出口 / W3 搜索 provider 热重载 + 主 provider·降级链 / W4 入站语音 AMR 转码落副本)同步了 `internal/runtime/flowgraph/**`(`timeline.go` / `timeline_test.go`)、`internal/services/gateway/**`(`admin_flow_timeline.go` / `admin_flow_timeline_test.go` / `history_toolcalls_gap_test.go` / `routes.go`)、`internal/capability/registry/**`(`registry.go` / `replacetool_test.go`)、`plugins/tools/io/search/**`(`panel.go` / `panel_test.go`)、`cmd/cyrene/**`(`websearch_reload.go` / `websearch_reload_test.go` / `websearch_selection.go` / `websearch_selection_test.go` / `websearch_testenv_test.go` / `websearch_action_data.go` / `wire_io_tools.go` / `wire_plugin_actions.go` / `wire_channels.go` / `wire_channels_test.go`)与本文件(§2.0 / §2.2 B18 / §4.1 / §4.5 / §4.11.1 / §4.14 / §6 / §7.1 / §7.2 / §7.3 / §7.4);`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/**` 与本文件。*