From 20f82c9679ee2449dd491c9fda884099de1bf167 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Wed, 7 Oct 2026 09:34:46 +0800 Subject: [PATCH] =?UTF-8?q?docs(contract):=20v0.82=20=E2=80=94=E2=80=94=20?= =?UTF-8?q?resultSummary=EF=BC=88=E5=A4=B1=E8=B4=A5=E4=B9=9F=E5=B8=A6?= =?UTF-8?q?=EF=BC=89+=20=E8=87=AA=E6=8E=92=E6=88=90=E6=9C=AC=E5=8F=AF?= =?UTF-8?q?=E8=A7=81=E9=9D=A2=20+=20browser=20=E9=9D=A2=E6=9D=BF=EF=BC=88Z?= =?UTF-8?q?4=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tool_execution.end 的 resultSummary(含失败口径、≤500 rune、空 ⇒ 缺席) - GET /admin/backthink 加 ?trigger=(闭集 + 未知/空 400 invalid_trigger + 下推 SQL)+ 新端点 GET /api/v1/admin/backthink/self-wake(writeAuth、三态、gate 快照、*IsDefault 语义) - GET /admin/panels 新增 browser/settings + 六个动作(3 读 3 写)+ 错误码 + turnAttachment - request_think 可达性改正(已上线);计数 72→73(writeAuth 18→19) --- Docs/api-client.md | 217 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 191 insertions(+), 26 deletions(-) diff --git a/Docs/api-client.md b/Docs/api-client.md index d709180..e5bab58 100644 --- a/Docs/api-client.md +++ b/Docs/api-client.md @@ -1,6 +1,6 @@ # Cyrene v2 客户端 API 契约 -> **契约版本:`v0.81`**(上一版 `v0.80`,其条目见 §6;`v0.79` / `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`)。 +> **契约版本:`v0.82`**(上一版 `v0.81`,其条目见 §6;`v0.80` / `v0.79` / `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,后端全量字段级文档)。归档的原因是**结构性臃肿 + 历史条目并列 + 长期漂移**;本文档**不复制**那段历史,需要考古时去归档。 @@ -22,7 +22,9 @@ > **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.81 是「她自排轮的用户可见面 + 时间轴保留期信号 + `round` 跨重启单调修复」版(切片 C1-1 `94da6bc` / C1-2 `ed50053`(自排)+ 切片 X1(时间轴两处真缺陷);**无新端点、无破坏性变更**)**:① **`GET /api/v1/admin/backthink` 的行 `trigger` 新增取值 `"self_wake"`**(= 这一轮是**她自己排起**的思考,`design/12 §N C`)—— `trigger` 的取值域**不封闭**(`heartbeat` 周期心跳 / `startup` 启动轮 / 事件轮 = 事件主题 / `self_wake` 自排),客户端**按字面值显示、不要当闭集校验**(见 §4.10(d))。⚠️ **可达性**:她的入口工具 `request_think` **尚未接线** ⇒ 该取值与 ② 的动词在生产里**目前不会出现**(形状先登记)。② **新增 4 条审计动词** `backthink.self_wake.{queued,coalesced,capped,denied}`,经既有 `GET /api/v1/admin/observability/audit`(B9)可查,`who` 恒为 `backthink`(加性,见 §4.10(d))。③ **时间轴新增 `retentionBoundaryReached`(bool)并与 `truncated` 分意**:`truncated:true` **只**表示「本次不是全部」(分页未到底 **或** 落盘源未接线),**不能**据此断言「更早的轮次已被保留期清扫」——那件事由 **`retentionBoundaryReached:true`** 单独回答(判据三条缺一不可 + **三个盲区如实登记**,见 §4.11.1(a)(b)-3)。④ **`round` 跨重启不再重复**(发布器先从落盘播种计数器):修的是「重启后轮次号从 1 重来 ⇒ 时间轴把重启前后两轮并成一块」的真机缺陷,契约里「`round` 每会话单调」这句**从此真的成立**(见 §4.11.1(e))。⑤ **计数**:本版**无新端点** ⇒ §2.0 权限矩阵维持 **14 + 18 + 40 = 72**(§2.2 段内 B1–B18 共 **18** 条)。⚠️ **同批对客户端零形状影响(如实说明)**:技能渐进披露接线(L2 `6475eb8`)、示例技能与端到端验收(V3 `4a04ad4`)、浏览器 B1 接线 + B2 薄层工具面(V5 `f1fbc24`)**都不改变任何 REST / WS / admin 形状**(浏览器是**模型面工具**,不进客户端契约面)。⚠️ **③④ 的形状取自切片 X1 的工作树实现,登记时尚未提交** ⇒ 提交后请按 `git show` 复核键名与判据。 +> **v0.82 是三面增量版(① 已实现(切片 Y1 `215eedf`);② 已实现(切片 Y3 `4213b26`);③ 形状已定、代码在树(切片 Y4 `e0a6bca` + 组合根挂点);**纯增量、无破坏性变更**)**:① **`tool_execution.end` 的 `data` 新增可选 `resultSummary`(string)** —— 工具卡从此能在**实时**显示「这次调用拿到了什么」,不必等事后翻回看面。它修的是「实时只看到工具名与状态、结果要事后才知道」。🔴 **缺席语义(安卓端按此编程)**:**键在场 ⇒ 渲染结果区;键缺席 ⇒ 只显示工具名与状态** —— **内容与回看面 `toolCalls[].resultSummary` 同源同口径**(同一份**已脱敏**文本 `trace.RedactText`:凭据形状清洗 + 截断)、**同一个上限 `≤500` rune**(超长按 rune 截断、末位 `…`);**空值 ⇒ 整键缺席,绝不用空串顶位**(空串会让客户端渲染出一个空的结果区)。**只有「真的没有结果」才缺席**:调用成功但结果为空(nil / 空串)、或错误文本本身为空;`tool_execution.start` / `.update` **不带**该键。⚠️ **失败也带**(本版口径):调用失败 / 取消 / 超时的 `.end` 帧带的是**该次错误文本的脱敏渲染** —— 与回看面(那里失败调用回的是图上的 `error` 摘要)**给同一份说法**,客户端不必为「实时」与「回看」写两套判据。详见 **§3.4.2**。② **自排的成本可见面**(切片 Y3,design/12 §N C.5/C.6)—— `GET /api/v1/admin/backthink` 新增**可选** `?trigger=` **服务端筛选**(取值域 = **闭集**:三个内建标签 + 本服务订阅的事件主题,真源 `backthink.TriggerFilterValues()`;值**未知或为空** ⇒ **`400 invalid_trigger`**,**绝不静默忽略** —— 静默忽略会让面板在显示全部的同时声称「正在筛选」;合法值**下推 SQL**,因此 `limit` 数的是**匹配**条数);新增 1 条 `writeAuth` 只读端点 **`GET /api/v1/admin/backthink/self-wake`**(§2.2 B19 / §4.10(e),**三态** `available`/`running`/`reason` + `gate` 快照 + `configFile`)。⚠️ **`trigger` 的「显示口径」仍是开放的**(客户端按字面值显示、不做闭集校验)—— **筛选白名单**与**显示取值域**是两件事,别混(见 §4.10(d))。③ **`browser` 插件管理面板(B4)**(切片 Y4,design/28 §8)—— `GET /api/v1/admin/panels` 的面板目录新增 `browser` / `settings`(「浏览器与截图」,4 个区块:配置表单 + 状态 / 指纹 / 截图三张只读表),该插件声明**六个动作**(3 读 3 写,见 §4.14(d));**不开任何新路由**(走既有 A14 通用动作分发),因此 §2.0 计数**只**因 ② 的 self-wake 端点 +1。**模型面**(非客户端契约面)另有一条:`browser` 工具的 `screenshot` 返回值新增 **`turnAttachment`**(闭集 `queued` / `no-session` / `outbox-full` / `disabled`)。**④ 本版把 §4.10(d) 一句已成假话的口径改正**:`request_think` **已接线**(`cmd/cyrene/wire_services.go` 的 `wireSelfWakeTool`;启动日志 `self wake tool registered tool=request_think`)⇒ `trigger="self_wake"` 与 4 条 `backthink.self_wake.*` 审计动词**在生产里会真的出现**。**⑤ 计数**:`writeAuth` 段 18 → **19**、管理路由总数 72 → **73**(`14 + 19 + 40`)。⚠️ **登记时的工作树状态(如实)**:③ 的**组合根两个挂点**(`cmd/cyrene/wire_frontend.go` 的 `wireBrowserPanel(hub, a, a.log)`、`cmd/cyrene/wire_runtime.go` 的 `TransformContext: a.browserShotTransform(visionResolver),`)与 ① 的**失败也带摘要**(`internal/runtime/loop/tool_frame_summary.go`)在本版登记时**仍在工作树、未提交** ⇒ 提交前,跑着的后端里**没有**这些行为;提交后请按 `git show` 逐字复核。 + +> **v0.81 是「她自排轮的用户可见面 + 时间轴保留期信号 + `round` 跨重启单调修复」版(切片 C1-1 `94da6bc` / C1-2 `ed50053`(自排)+ 切片 X1(时间轴两处真缺陷);**无新端点、无破坏性变更**)**:① **`GET /api/v1/admin/backthink` 的行 `trigger` 新增取值 `"self_wake"`**(= 这一轮是**她自己排起**的思考,`design/12 §N C`)—— `trigger` 的取值域**不封闭**(`heartbeat` 周期心跳 / `startup` 启动轮 / 事件轮 = 事件主题 / `self_wake` 自排),客户端**按字面值显示、不要当闭集校验**(见 §4.10(d))。⚠️ **可达性**:她的入口工具 `request_think` **尚未接线** ⇒ 该取值与 ② 的动词在生产里**目前不会出现**(形状先登记)。—— ✅ **本条已过期,已由 v0.82 ④ 改正**:`request_think` 已接线(`cmd/cyrene/wire_services.go` 的 `wireSelfWakeTool`,切片 X2 `2d211c2`),生产里**会真的出现**。② **新增 4 条审计动词** `backthink.self_wake.{queued,coalesced,capped,denied}`,经既有 `GET /api/v1/admin/observability/audit`(B9)可查,`who` 恒为 `backthink`(加性,见 §4.10(d))。③ **时间轴新增 `retentionBoundaryReached`(bool)并与 `truncated` 分意**:`truncated:true` **只**表示「本次不是全部」(分页未到底 **或** 落盘源未接线),**不能**据此断言「更早的轮次已被保留期清扫」——那件事由 **`retentionBoundaryReached:true`** 单独回答(判据三条缺一不可 + **三个盲区如实登记**,见 §4.11.1(a)(b)-3)。④ **`round` 跨重启不再重复**(发布器先从落盘播种计数器):修的是「重启后轮次号从 1 重来 ⇒ 时间轴把重启前后两轮并成一块」的真机缺陷,契约里「`round` 每会话单调」这句**从此真的成立**(见 §4.11.1(e))。⑤ **计数**:本版**无新端点** ⇒ §2.0 权限矩阵维持 **14 + 18 + 40 = 72**(§2.2 段内 B1–B18 共 **18** 条)。⚠️ **同批对客户端零形状影响(如实说明)**:技能渐进披露接线(L2 `6475eb8`)、示例技能与端到端验收(V3 `4a04ad4`)、浏览器 B1 接线 + B2 薄层工具面(V5 `f1fbc24`)**都不改变任何 REST / WS / admin 形状**。⚠️ 括注原文写「浏览器是**模型面工具**,不进客户端契约面」—— **对 B1/B2 成立,但已部分过期**:v0.82 的 **B4**(浏览器管理面板,切片 Y4)**进**客户端契约面(`GET /admin/panels` 多一条目录项 + 六个动作走既有 A14),见 **§4.14(d)**。⚠️ **③④ 的形状取自切片 X1 的工作树实现,登记时尚未提交** ⇒ 提交后请按 `git show` 复核键名与判据。 > **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**。 @@ -144,10 +146,10 @@ | 档位 | 中间件 | 条数 | 说明 | |---|:--:|:--:|---| | **只读监控档** | `viewerAuth`(owner/admin/viewer) | **14** | 非敏感投影:无凭据、无他人账号数据、无消息正文。**v0.78 新增两条**:面板接入点快照(A13)与插件动作分发(A14,**写门落在 handler 内**,见 A14 行注) | -| **owner/admin 读档** | `writeAuth`(owner/admin) | **18** | viewer 不得看到的一切:记忆、LLM 栈、账号/key、后台思考、审计链、渠道消息日志、完整会话导出、配置事实面(v0.68)、最近一轮链路图快照(v0.71)、记忆整理试算(v0.75)、入库前审查留痕与回滚(**v0.79**,B16/B17)、**链路图时间轴(v0.80,B18)** | +| **owner/admin 读档** | `writeAuth`(owner/admin) | **19** | viewer 不得看到的一切:记忆、LLM 栈、账号/key、后台思考、审计链、渠道消息日志、完整会话导出、配置事实面(v0.68)、最近一轮链路图快照(v0.71)、记忆整理试算(v0.75)、入库前审查留痕与回滚(**v0.79**,B16/B17)、链路图时间轴(**v0.80**,B18)、**自排闸只读快照(v0.82,B19)** | | **owner-only 档** | `ownerAuth`(owner) | **40** | 带审计的策略写、每个获取凭据的渠道端点、其余全部变更 | -**管理路由总数 = 14 + 18 + 40 = 72 条。** +**管理路由总数 = 14 + 19 + 40 = 73 条。** > **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。 @@ -158,7 +160,9 @@ > ✅ **记账滞后已修正(2026-10-06,本版回补)**:按上一段「计数口径」那句(= 三张清单**列出的行数**)核,**§2.2 实有 18 行**(B1–B18)、**§2.1 实有 14 行**(A1–A14)、**§2.3 实有 40 行** ⇒ 逐行相加 **14 + 18 + 40 = 72**,与上表「条数」序列(`writeAuth` **18** / 总数 **72**)**已一致**(`routes.go` 交叉核对:admin 侧经 `writeAuth` 挂载 20 条,减去 §2.4(a) 按历史口径不计入本表的 `channels/events` 与 `.../counters` ⇒ **18**)。**根因**(S6 报告 §6.1 发现):v0.76 新增 B15 时**漏把条数 +1**,此后 v0.78 / v0.79 / v0.80 各版沿旧序列 +1 而未回补 ⇒ 那个 **−1 滞后从 v0.76 一路带到 v0.80 的登记值**。 > ⚠️ **上面 v0.78 / v0.79 两个计数段的绝对值仍沿用当时未回补的写法**(其**增量正确** —— `viewerAuth` 12 → 14、`writeAuth` 各版 +1 / +2 —— 但**绝对值比真实行数少 1**);**§6 里各版条目(v0.76 ~ v0.80)的同类计数同理**。按本文件「历史条目保留当时状态」的惯例**不逐版改数**,**以本表 + 三张清单的行数为准**。 -> **计数口径(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))**未计入本表**——这是历史口径,本版未改,也**未**把它算进上面的 **18**。逐条以代码为准。 +> **v0.82 计数变更(实数,已按 `routes.go` 核对)**:`writeAuth` 段新增一条(**B19 `GET /admin/backthink/self-wake`**,切片 Y3)⇒ 段内 **18 → 19**、总数 **72 → 73**。三张清单的行数同步为 **§2.1 14 行**(A1–A14)/ **§2.2 19 行**(B1–B19)/ **§2.3 40 行**(C1–C40)。**核对方式(可复现)**:`internal/services/gateway/routes.go` 里经 `writeAuth` 挂载的 **admin** 路由**实有 21 条**,按历史口径减去 §2.4(a) 的 `channels/events` 与 `.../counters` ⇒ **19**,与 §2.2 的行数一致;`viewerAuth` 挂载 **14**、`ownerAuth` 挂载 **40**(逐条列举见 `grep -c 'mux.Handle("[A-Z]* /api/v1/admin.*'`)。⚠️ **本版另一个「看似该 +1、其实不动计数」的面**:`browser` 插件的六个动作(§4.14(d))**不开任何新路由** —— 它走**既有** A14 `POST /admin/plugins/{id}/actions/{action}`,因此 §2.1 的 14 与总数都**不变**(A14 那一行本就写明「写门落在 handler 内」,动作名由**声明**白名单决定,声明多一个插件不新增路由)。 + +> **计数口径(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))**未计入本表**——这是历史口径,本版未改,也**未**把它算进上表的 **writeAuth 条数**(v0.82 为 **19**)。逐条以代码为准。 > ⚠️ **与归档版不一致(已更正)**:`Docs/archive/api-client.v0.65.md` §2.0 写「12 + 11 + 38 = 61」(其 v0.62 条目又写「12 + 10 + 38 = 60」,自相矛盾)。**以代码为准 = 12 + 11 + 40 = 63**。 @@ -178,12 +182,12 @@ | A10 | GET | `/api/v1/admin/observability/trace` | 运行时 span | | 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) | +| 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)。**v0.82 起目录里多一条内置页**:`browser` / `settings`(「浏览器与截图」,切片 Y4,见 §4.14(d))⇒ 声明接线后 `pluginIds` 为 `["browser","web-search"]`(**已排序**)—— ⚠️ **登记时该挂点仍在工作树、未提交**,因此**在跑的那份产物里没有它**(真机已核:`:10942` 上 `GET /admin/panels` 需 owner 凭据,但**启动日志全篇 0 条 `browser panel declared`** ⇒ 该面板未接线)| | 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 读(18 条,`writeAuth`) +### 2.2 owner/admin 读(19 条,`writeAuth`) | # | 方法 | 路径 | 要点 | |---|---|---|---| @@ -194,7 +198,7 @@ | B5 | GET | `/api/v1/admin/llm/usage` | LLM 用量汇总(切片 U1,v0.64)。`?since=&until=&purpose=`;缺省近 24h、跨度上限 90 天(超 → `400`);`hitRate = cacheHit/(cacheHit+cacheMiss)`,分母 0 ⇒ `0`。**不含凭据与消息正文**。未接线 → `500 usage_unavailable` | | B6 | GET | `/api/v1/admin/llm/catalog` | 模型目录候选(`?refresh=1` 强制在线刷新)。既无快照又抓不到 → `503 catalog_unavailable` | | B7 | GET | `/api/v1/admin/users` | 账号/API key 清单 | -| B8 | GET | `/api/v1/admin/backthink` | 后台思考记录。每条**可能**带可选 `urgency`(`"routine"`/`"warning"`/`"critical"`,`internal/urgency` 词汇;**默认是精确档位**,带 `urgencyLowerBound:true` 时才是下限,见 §4.10(b))与可选 `quietHoursBypassed:true`(该轮**穿透了安静时段**)—— **v0.68 新增;档位的精确语义与 `urgencyLowerBound` 为 v0.70**。⚠️ 每条的 `trigger` 是**开放的**取值域(`heartbeat` 周期心跳 / `startup` 启动轮 / 事件轮的**事件主题** / **`self_wake`(v0.81 登记)**)= 她自己排起的一轮思考),客户端**按字面值显示**、不做闭集校验,见 §4.10(d) | +| B8 | GET | `/api/v1/admin/backthink` | 后台思考记录。每条**可能**带可选 `urgency`(`"routine"`/`"warning"`/`"critical"`,`internal/urgency` 词汇;**默认是精确档位**,带 `urgencyLowerBound:true` 时才是下限,见 §4.10(b))与可选 `quietHoursBypassed:true`(该轮**穿透了安静时段**)—— **v0.68 新增;档位的精确语义与 `urgencyLowerBound` 为 v0.70**。⚠️ 每条的 `trigger` 是**开放的**取值域(`heartbeat` 周期心跳 / `startup` 启动轮 / 事件轮的**事件主题** / **`self_wake`(v0.81 登记)** = 她自己排起的一轮思考),客户端**按字面值显示**、不做闭集校验,见 §4.10(d)。🆕 **v0.82 新增可选 `?trigger=` 服务端筛选**(切片 Y3):取值域 = **闭集**(= 上面那些取值,真源 `backthink.TriggerFilterValues()`);**未知值或空值 ⇒ `400 invalid_trigger`**(**绝不静默忽略**);合法值**下推 SQL**,`limit` 数的是**匹配**条数。⚠️ **「筛选白名单是闭集」与「显示取值域是开放集」不矛盾**,见 §4.10(d)(e) | | B9 | GET | `/api/v1/admin/observability/audit` | 审计链 | | B10 | GET | `/api/v1/admin/sessions/{id}/export` | 完整会话导出 | | B11 | POST | `/api/v1/admin/rag/query` | 知识库检索(带审计的读型 POST) | @@ -204,8 +208,8 @@ | 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:""}}`;该行不在 `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;v0.81 增量)**,只读、不审计。`?sessionId=`(省略 = 主会话 `web`)+ `?limit=`(缺省 **50**、> **200** 钳到 200、非整数 `400 bad_request`、`≤0` 同缺省)+ `?before=`(**游标**:取开始时刻**严格早于**它的轮次;省略 = 最新那一页;取值 = 响应里的 `nextBefore`,也接受 RFC3339 或 Unix 毫秒,非法 → `400 bad_request`)。响应 `{data:{sessionId,source,rounds[],nextBefore,hasMore,truncated,retentionBoundaryReached,retentionHours,retentionEnabled}}`:`rounds` **按时间正序**(最早在前),每项 `{turnId,round,startedAt,endedAt,lanes[]}`(`lanes[]` 每项 `{turnId,graph}`,`graph` 与 `graph_update` 同形);`round` 为 `null` = **未知**(老行),`endedAt` 为空串 = **尚未收尾**(在飞轮);`source` ∈ `persisted`(本页至少一轮来自落盘)/`live`(本页全部来自内存)/`none`。🆕 **v0.81(切片 X1)**:① 新增 **`retentionBoundaryReached`**(bool)= 「该会话确有更早的轮次已被保留期清扫」——**与 `truncated` 分意**,只有它 `true` 时面板才可说「更早的轮次已过保留期」(判据/盲区见 **§4.11.1(a)**);② **`round` 跨重启不再重复**(发布器先从落盘播种计数器,见 **§4.11.1(e)**)⇒ 按 `round` 分组不再把重启前后两轮并成一块。未接线(内存 + 落盘都没接)→ `500 flow_unavailable`。语义与空态见 **§4.11.1** | +| B19 | GET | `/api/v1/admin/backthink/self-wake` | **自排闸只读快照(v0.82 新增,切片 Y3;design/12 §N C.5 的读侧)**,只读、不审计、不写库。**无参数**。响应 `{data:{available,running,reason?,configFile?,gate?}}` —— **三态必须可区分**(面板**不得**把「看不到」画成「0 次」):① `available:false` ⇒ 本进程给不出快照(后台思考未接线 / 实现不提供读口 / 闸不在),此时 **`gate` 缺席**、`reason` 说明原因(**HTTP 仍是 `200`**,不假装有一个 0/0 的闸);② `available:true, running:false` ⇒ 已接线但不在运行,`gate` **仍带**配置面数字(上限/窗口/代码默认值),`reason` 说明「此刻她排不出新的一轮」;③ `available:true, running:true` ⇒ 正常态,`reason` 缺席。`gate` 快照 = `{pending,used,limit,windowSec,defaultLimit,defaultWindowSec,limitIsDefault,windowIsDefault}`(`used` 与工具收据 `SelfWakeReceipt.Used` **同一口径**;`limit`/`windowSec` 是**进程生效**值,已 normalize)。🔴 **`limitIsDefault` / `windowIsDefault` 的语义**:报告生效值**恰好等于**代码默认值 —— **不是**「配置文件里没写这个键」的断言(网关**不读** `backthink.json`,与 config-facts 的 `isDefault` 同一条诚实口径:无从区分「配了 4」与「没配」)。`configFile` = **相对落点**(`config/backthink.json`,相对 `$DATA_DIR`)。⚠️ **它不含敏感信息、也不是写口**:改上限/窗口要改配置文件 + 重启(面板写路径**未做**)。语义与三态口径见 **§4.10(e)**。✅ **真机已核(路由存在性,无需凭据)**:`:10942` 上 `POST /api/v1/admin/backthink/self-wake` → **`405` + `Allow: GET, HEAD`**、`GET` 同路径无 token → **`401`**(负对照:同法打一个不存在的 admin 路径 → `404`)⇒ 这条路由**确在**运行中的产物里(⚠️ **不代表**已用真凭据拿到过 `data`;后者**未验**) | > **读取 `GET /admin/observability/{logs,metrics,trace}` 兼任可观测推送的订阅握手**(该凭据的连接全部断开即退订)。REST 响应形状不变。 @@ -392,13 +396,26 @@ | `message.start` / `message.delta` / `message.end` | `{index, delta?, streamId?}` | | `thinking.start` / `thinking.delta` / `thinking.end` | `{index, delta?, streamId?}` | | `tool_call.start` / `tool_call.delta` / `tool_call.end` | `{id, index, delta?, streamId?}` | -| `tool_execution.start` / `tool_execution.update` / `tool_execution.end` | `{tool, phase, id?, delta?}`(`phase` ∈ `running/done/error`;**phase 绝不塞进 delta**) | +| `tool_execution.start` / `tool_execution.update` / `tool_execution.end` | `{tool, phase, id?, delta?, resultSummary?}`(`phase` ∈ `running/done/error`;**phase 绝不塞进 delta**;**`resultSummary` 只在 `end` 帧、且只在「真的有结论」时在场**(v0.82)—— **成功 ⇒ 结果摘要;失败/取消/超时 ⇒ 该次错误文本的摘要**(两边都走同一份脱敏路径);**只有「真的没有结果」才整键缺席**(成功但结果为空、或错误文本为空),**绝不用空串顶位**。上限 **≤500** rune(超长按 rune 截断、末位 `…`),与回看面 `toolCalls[].resultSummary` 同源同口径,读侧不做二次脱敏也不放宽) | | `turn.start` / `turn.end` | 无 data(帧壳即边界信号) | | `stream_chunk` | `{delta}`(非流式路径的单段文本) | | `stream_end` | `{stopReason, steps?, error?}` | > `streamId`(可选)= 同一次模型流(一次 ChatStream 调用)的多帧关联键;`turnId` = 整个回合恒定。两者分工不同。 > `tool_execution.update`:wire 映射已实现,但**工具分发器目前没有中途进度上报源**,故实际**暂无生产者**(`start`/`end` 真实发出)。 +> +> **`resultSummary` 的缺席语义(v0.82,安卓端按此编程)**:它是 `tool_execution.end` 的**可选**键 —— **键在场 ⇔ 这次调用有了结论**;客户端**只做正向断言**,人缺席时显示「工具名 + 状态」即可。映射表(**客户端不要按 `phase` 反推键在不在**,只按「键在不在」分支): + +| 情形 | `.end` 帧上的 `data.resultSummary` | +|---|---| +| 调用成功且结果非空 | **在场**(已脱敏 + 已截断;图侧同源文本通常 ≤80 rune,网关再按 ≤500 兜底) | +| 调用失败 / 取消 / 超时(`Invoke` 返回 error) | **在场** —— **该次错误文本的脱敏渲染**(与回看面「失败调用给图上的 `error` 摘要」同一说法) | +| 调用成功但结果为空(nil / 空串) | **缺席**(整键不出现) | +| 错误文本本身为空 | **缺席** | +| `tool_execution.start` / `.update`(即使有人误挂摘要) | **缺席**(契约只在 `end` 帧给) | +| 被静音的内部流程(backthink / 子代理) | **整帧不发**(既有行为,一字未改) | + +> 「绝不回空串顶位」有两道保险:loop 侧留空 + 网关侧 `s != ""` 才写键(切片 Y1 的变异 A 证明这道守卫真的在起作用)。⚠️ **与回看面的差异(v0.82 起已收窄)**:v0.82 之前本帧**失败时整键缺席**、而回看面对失败调用给的是 `error` 摘要 ⇒ 同一次调用「实时看不到原因、事后看得到」。v0.82 把两侧统一为「失败也带该次错误文本的摘要」,**客户端从此只需一套判据**。⚠️ **未确证**:本键的**真机**(`:10942` 发布产物)端到端**未跑**,见 §7.4-30。 #### 3.4.3 编排帧(owner+admin) @@ -487,17 +504,17 @@ | 状态 | code(示例) | 场景 | |---|---|---| -| 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)) | +| 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**)、`invalid_trigger`(**v0.82**) | 请求体/字段非法、图片超限(`image` **字符串长度**,**仅 base64 / `data:` 形态会触发**,见 §4.5)、附件超条数;**插件动作的载荷形状错 / provider 名未登记 / 该家没配齐**(这三个码自 v0.79 起由动作处理器给出,见 §4.14(b)(c));**面板保存的 `primary` 指名一家「已登记但未配齐」的家**(**v0.80**,写盘**之前**就拒 —— 那份配置会让后端下次起不来,见 §4.14(c));**`GET /admin/backthink?trigger=` 的值未知或为空**(**v0.82**,见 §4.10(e-1);`browser` 面板的**未知字段**也走 `invalid_payload`,见 §4.14(d)) | | 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**)、`config_unstartable`(**v0.80**) | 待处理队列满;渠道账号已配置但没有已注册适配器(C19/C20,v0.66);**插件已被显式停用,其声明还在但入口与动作都下线**(v0.78,见 §4.14(b));**这次面板保存的结果会让 web-search 起不来**(主 provider 未配齐 / 未登记 ⇒ 下次启动 fail-fast)⇒ 已**按字节回滚**、运行中的工具保持原样(**v0.80**,见 §4.14(c)) | +| 409 | `chat_busy`、`account_not_registered`、`plugin_disabled`(**v0.78**)、`config_unstartable`(**v0.80**)、`browser_unavailable` / `browser_session_not_open` / `config_rejected`(**v0.82**) | 待处理队列满;渠道账号已配置但没有已注册适配器(C19/C20,v0.66);**插件已被显式停用,其声明还在但入口与动作都下线**(v0.78,见 §4.14(b));**这次面板保存的结果会让 web-search 起不来**(主 provider 未配齐 / 未登记 ⇒ 下次启动 fail-fast)⇒ 已**按字节回滚**、运行中的工具保持原样(**v0.80**,见 §4.14(c));**`browser` 面板(v0.82,见 §4.14(d))**:本机没有可用浏览器来源 / 当前没有打开的页面(面板**不替模型开页**)/ 这次保存没能生效**已按字节回滚** | | 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`、`memory_review_unavailable`(**v0.79 新增**,切片 L45:入库前审查留痕存储未接线 —— 列表与回滚**共用**)、`reviews_failed`(**v0.79**:读留痕失败)、`revert_failed`(**v0.79**:回滚写失败)、`config_unreadable`(**v0.79**:面板可变源存在但读不出来,**不覆盖**原文件)、`config_write_failed`(**v0.79**:可变源写不进去,本次改动**没有**生效) | 内部错误或依赖未接线(**绝不伪装成功**) | +| 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**:可变源写不进去,本次改动**没有**生效;⚠️ **回滚本身失败**时也用这个码并明说「盘上可能已经变了」—— **v0.82 的 `browser` 面板复用这两个码**,见 §4.14(d)) | 内部错误或依赖未接线(**绝不伪装成功**) | | 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) | +| 502 | `action_failed`(**v0.78**)、探针码 `unauthorized` / `rate_limited` / `quota_exceeded` / `network` / `upstream` / `bad_response` / `no_results` / `unknown`(**v0.79**)、`browser_action_failed`(**v0.82**) | **插件动作执行失败**(结构化错误码 + 文案,详细原因只进后端日志);中间一组是 `plugin.testProvider` 探针的**上游结论**(其中 **`no_results` = 连通正常但没查到结果**);末一个 = `browser` 面板的截图 / 指纹自检**执行失败**(**v0.82**,见 §4.14(b)(d)) | | 503 | `timeout`、`chat_dispatch_failed`、`catalog_unavailable` | 派发/上游不可用 | > **未确证**:`conflict`(409) 的通用码存在(框架级 `ErrConflict`),但除了 `chat_busy` 之外,本文档未逐一确证还有哪些端点会带它。 @@ -823,8 +840,8 @@ #### (d) v0.81 新增:她自排轮(`self_wake`)的用户可见面 -> 依据 `design/12 §N C`(自排:结构化 self-initiated 触发 + 她可调用的「自排一次思考」工具 + 每窗口上限 + 审计)。**本版无新端点、无新字段表**,只有两处**取值域加性扩张**与一条**口径澄清**。 -> 🔴 **可达性(先读这条,别误判)**:本版落地的是**判定侧(C1-1 `94da6bc`)与自排本体(C1-2 `ed50053`)** —— **她的入口工具 `request_think` 尚未接线**(属**下一片**,在办)⇒ **在生产里 `trigger="self_wake"` 与那 4 条审计动词目前还不会真的出现**。本节**登记的是形状**(写侧代码已在树里),客户端可以先把字段读进去,但**不要**把它当「一定会有」的活跃数据(同 §7.1 的登记口径)。 +> 依据 `design/12 §N C`(自排:结构化 self-initiated 触发 + 她可调用的「自排一次思考」工具 + 每窗口上限 + 审计)。**v0.81 当时无新端点、无新字段表**,只有两处**取值域加性扩张**与一条**口径澄清**。 +> ✅ **可达性(v0.82 已改正,别按旧口径读)**:v0.81 落地的是**判定侧(C1-1 `94da6bc`)与自排本体(C1-2 `ed50053`)**,当时她的入口工具 `request_think` **尚未接线** ⇒ 那一版写的是「生产里还不会真的出现」。🔴 **这句已过期**:`request_think` **已接线**(`cmd/cyrene/wire_services.go` 的 `wireSelfWakeTool`,切片 X2 `2d211c2`;启动日志自证 `self wake tool registered tool=request_think`)⇒ **`trigger="self_wake"` 与那 4 条审计动词在生产里会真的出现**。客户端可以把它当活跃数据读(仍**不保证每轮都有** —— 自排要过闸,见下)。 **(d-1) `GET /api/v1/admin/backthink` 的 `trigger` 出现新取值 `"self_wake"`** @@ -845,7 +862,65 @@ | `backthink.self_wake.denied` | `backthink` | `非主人环境,拒绝自排(actor=%s)` | - 只有 `queued` 会**真的**新增一轮思考;`coalesced` = 已有一轮在排队 ⇒ 合并进去(**不额外占额度**);`capped` = 触到每窗口上限(**默认 4 次/小时**);`denied` = 非主人环境(安全闸拦下,**不做事**)。 -- ⚠️ **上限值不在契约里冻结**:它的真实来源是 **`$DATA_DIR/config/backthink.json`**(`selfWakePerWindow` / `selfWakeWindowSec`,**启动期读一次、改文件需重启**;**不新增 env 键** —— `.env` 已冻结,见 §5 与 `AGENTS §10`),且**目前没有 admin 端点可读可写**(写路径属后续切片 C2,未做)⇒ 客户端/面板**不要假定**能查到或改到这个值;要显示就用审计行 `result` 的原文。 +- ⚠️ **上限值不在契约里冻结**:它的真实来源是 **`$DATA_DIR/config/backthink.json`**(`selfWakePerWindow` / `selfWakeWindowSec`,**启动期读一次、改文件需重启**;**不新增 env 键** —— `.env` 已冻结,见 §5 与 `AGENTS §10`)。✅ **v0.82 起有只读出口了**:新增 **`GET /api/v1/admin/backthink/self-wake`**(§2.2 B19 / §4.10(e))给出「本窗口已用 N / 上限 M / 窗口秒数」的**真读数**,不必再从审计行 `result` 的原文里抠。🔴 **仍没有写口**:改上限/窗口依旧只能改配置文件 + 重启(面板写路径**未做**)⇒ 客户端/面板**不得假定**能改到这个值。 + +#### (e) v0.82 新增:自排的成本可见面(读侧) + +> 依据 `design/12 §N C.5`(每窗口上限 + 容量 1 待跑槽)与 `§N C.6`(服务端筛选)。**两条都只读**。 + +**(e-1) `GET /api/v1/admin/backthink?trigger=<值>` —— 服务端筛选(v0.82)** + +| 项 | 值 | +|---|---| +| 查询参数 | `trigger`(**可选**)。**缺席** ⇒ 不过滤(既有行为,老客户端零影响);**在场但为空** ⇒ **`400 invalid_trigger`**;**不在白名单** ⇒ **`400 invalid_trigger`**(错误文案里**列出可选取值**,客户端可直接显示) | +| 白名单(**闭集**) | `heartbeat` / `startup` / `self_wake` + **本服务订阅的事件主题**。真源 = `internal/services/backthink/trigger_domain.go` 的 `TriggerFilterValues()`(内建标签 + `EventSubjects()`)—— **网关不另抄一份清单**(否则新增订阅主题时会出现「数据里有、筛选却 400」的静默漂移) | +| 筛选方式 | **下推 SQL**(`ListByTrigger`),因此 `limit` 数的是**匹配**条数,而不是「取一页再筛掉不匹配的」(后者会给面板一个**静默的假空值**:「0 条」而库里明明有) | + +🔴 **为什么未知值必须报错、不能静默忽略**:静默忽略会让面板在「显示的是全部」的同时声称「正在筛选自我唤醒」—— 那是一个**假的筛选结果**。**空值同理**(客户端把想筛选的值弄丢了却以为在筛)。 + +⚠️ **与 (d) 的「取值域不封闭」不矛盾(这是本版最容易被误读的一处)**: +- **显示口径**((d),仍然成立):`Record.Trigger` 的取值**对客户端是开放的** —— 事件主题那一支本来就是开放的;客户端**按字面值显示、不做闭集校验、不隐藏不认识的取值**; +- **筛选白名单**((e-1)):`?trigger=` 是**客户端传进来的参数**,打错了必须报错,而不是静默返回一个空列表(静默的空列表会被读成「这段时间没有这一类的思考」)。 +- 两者面向的是**不同方向的数据流**(一个是我们给出去、一个是客户端传进来),因此**不冲突**;判据的真源仍是**同一处**(`trigger_domain.go`)。 + +**(e-2) `GET /api/v1/admin/backthink/self-wake` —— 自排闸只读快照(v0.82 新增)** + +**为什么有它**:`C.5` 的「本窗口已用 N / 上限 M」此前**只**出现在**被拒那一轮**的审计文案里(`backthink.self_wake.capped` 的 `result`)—— 也就是**「已经被拒过一次」才第一次看得见**。面板要能**提前**告诉主人「还能自排几次」,就得有一个只读快照出口。 + +**鉴权**:`writeAuth`(**owner / admin**;viewer → `403`)。理由:它含**她自排的成本口径与运行状态**,不是给 viewer 的监控投影(与列表端点 B8 同门同档)。**只读、不审计、不写库**。 + +**响应**:`{data:{available,running,reason?,configFile?,gate?}}` + +| 键 | 类型 | 说明 | +|---|---|---| +| `available` | bool,**恒在** | 本进程**能不能**给出快照。`false` 的含义是「看不到」,**不是**「0 次」 | +| `running` | bool,**恒在** | 自排此刻**是否真的能排**(未 Start / 已 Stop ⇒ `false`:此刻 `WakeNow` 会返回 `ErrSelfWakeUnavailable`) | +| `reason` | string,**可选**(缺席 = 正常态) | 人类可读的原因。三种非正常态各一句:未接线 / 实现不提供读口 / 闸不在 / 未在运行 | +| `configFile` | string,**可选** | 要改哪个文件 —— **相对落点** `config/backthink.json`(相对 `$DATA_DIR`)。绝对路径随部署而变,接口只给相对落点 | +| `gate` | object,**可选**(`available:false` 时**整键缺席**) | 见下表 | + +`gate` 子对象: + +| 键 | 类型 | 说明 | +|---|---|---| +| `pending` | bool | 容量 **1** 的待跑槽是否被占用(有一轮自排排着队还没跑) | +| `used` | int | 本窗口已用额度,与工具收据 `SelfWakeReceipt.Used` **同一口径**(同一次取数:过期的记录先被丢弃,再计数) | +| `limit` / `windowSec` | int | **进程生效**的上限与窗口(已 normalize:`≤0` 的配置不会走到这里)。窗口单位是**秒** | +| `defaultLimit` / `defaultWindowSec` | int | 代码默认值 | +| `limitIsDefault` / `windowIsDefault` | bool | 生效值**恰好等于**代码默认值 | + +🔴 **`*IsDefault` 不是「配置里没写这个键」(诚实边界,与 B12 config-facts 同一条口径)**:网关**不读** `backthink.json`,因此**无从区分**「配了 4」与「没配(默认也是 4)」⇒ 客户端**只能**把它读作「生效值是否等于默认值」,**不得**显示成「未配置 / 已自定义」。要表达「未配置」需要另一次取数(当前**没有**)。 + +**三态(面板按此显示,别把「看不到」画成「0 次」)**: + +| `available` | `running` | `gate` | `reason` | 面板该说什么 | +|:--:|:--:|:--:|---|---| +| `false` | `false` | **缺席** | 有 | 「读不到自排状态」+ 原因原文。**不要**显示 0 / 0 | +| `true` | `false` | 有(配置面数字) | 有 | 「后台思考未在运行:此刻她排不出新的一轮」+ 仍显示生效上限/窗口 | +| `true` | `true` | 有 | **缺席** | 正常:显示「本窗口已用 `used` / 上限 `limit`」+ 待跑槽占用 | + +- **只在 `running:true` 时才可以说「她还能排 N 次」**:`used` 是**已用**额度(`limit - used` 是剩余,但闸的判定始终由滑窗与待跑槽做,**读本快照不参与判定、也不改变闸的状态** ⇒ 读它永远不会消耗额度)。 +- **`available:false` 的 HTTP 状态是 `200`**(不是 5xx):这是「本进程给不出这张快照」的**如实答复**,与 B8「未接线时返回空列表」同族 —— 面板据 `reason` 显示即可,**不要**当成请求失败重试。 ### 4.11 链路图快照 + 多轮历史回放(`GET /api/v1/admin/flow/graph`;v0.71 新增,v0.73 增量,v0.77 增量) @@ -1085,11 +1160,20 @@ "order": 20, "route": "plugin/web-search/providers", // **相对**管理面板前缀 "sections": [ /* design/09 §5.2 的 section 声明**原样**下发(字段名逐字同形) */ ] + }, + { + "pluginId": "browser", // **v0.82 新增**(切片 Y4,见 §4.14(d)) + "id": "settings", + "title": "浏览器与截图", + "icon": "browser", // 前端把它映射到侧边栏的「图片」图标(图标表闭集里没有专门的浏览器图标) + "order": 30, + "route": "plugin/browser/settings", + "sections": [ /* 4 个区块:form 配置 + 3 张只读 table(状态 / 指纹 / 截图) */ ] } ] } ], - "pluginIds": ["web-search"] // **声明过面板**的插件 id(已排序);≠ 已安装列表 + "pluginIds": ["browser", "web-search"] // **声明过面板**的插件 id(**已排序**);≠ 已安装列表。⚠️ v0.82 的 `browser` 项在本版**登记时仍待提交**,提交前真机上是 `["web-search"]` } ``` @@ -1099,7 +1183,11 @@ **错误**:`501 {error:{code:"panels_unavailable"}}`(未接线);`403 unknown_role`(角色无法识别,理论不可达,fail-closed)。 -> ⚠️ **当前运行期快照不是空的**:内置插件 `web-search`(`plugins/tools/io/search` 的 `AdminPanels()`)在**组合根装配期**声明给中枢 ⇒ 正常部署下 `pluginIds == ["web-search"]`。热装插件(MCP 等)**目前恒为「未声明管理页面」**(声明来源 `manifest.json` 的读取未实现,见 §7.2)。 +> ⚠️ **当前运行期快照不是空的**:内置插件在**组合根装配期**声明给中枢 ⇒ 正常部署下有两个条目: +> - `web-search`(`plugins/tools/io/search` 的 `AdminPanels()`,随 v0.78 起); +> - `browser` / `settings`(`plugins/tools/browser` 的 `AdminPanels()`,**v0.82 新增**,切片 Y4,见 §4.14(d))—— 它写在 `cmd/cyrene/wire_frontend.go` 的 `wirePanelHub` 里(`wireBrowserPanel(hub, a, a.log)`,**登记时该挂点仍在工作树、未提交**);删掉这一行**没有任何编译期或运行期症状**,只是目录里少一页、六个动作全 `404 action_not_declared`,因此有一条**源码级**用例盯着它。🔴 **在跑的那份产物里没有它**(真机已核:启动日志全篇 **0 条 `browser panel declared`**,而该函数成功声明时必打这一行;同一次启动里 `self wake tool registered tool=request_think` 是有的)⇒ 那一刻真机上 `pluginIds == ["web-search"]`。 +> +> ⇒ 接线后 `pluginIds == ["browser","web-search"]`(**已排序**)。热装插件(MCP 等)**目前恒为「未声明管理页面」**(声明来源 `manifest.json` 的读取未实现,见 §7.2)。 #### (b) `POST /api/v1/admin/plugins/{id}/actions/{action}` —— 插件动作分发(**v0.78**) @@ -1161,6 +1249,52 @@ > ⚠️ **v0.79 行为变化(登记)**:`optionsFrom` 声明的候选来源现在会进**动作索引** ⇒ `plugin.providers` 从「`404 action_not_declared`」变为**可调用的读动作**。前端此前对该动作 404 的降级分支(文本框 + 「候选项未能加载」)因此**不再触发**。⚠️ 另有一条**前端缺口**(不属本契约面):行内动作的成功分支只显示「已完成,正在刷新」,丢弃 `result` ⇒ 探针成功时的 `count` / `latencyMs` 在界面上仍不可见。 +#### (d) `browser` 插件的动作面(**v0.82 新增**,切片 Y4;design/28 §8 B4) + +插件 id `browser`,面板 id `settings`(「浏览器与截图」,`route = plugin/browser/settings`)。**不开任何新路由** —— 六个动作全部走上面 (b) 的通用分发端点 `POST /api/v1/admin/plugins/browser/actions/{action}`;可变源落点 `$DATA_DIR/config/plugins/browser.json`(**0600**、temp+rename;**文件名 = 插件 id**)—— ⚠️ **不进客户端契约**(客户端一律走动作端点,不直连文件)。 + +**页面构成(4 个区块)**:① `form` 配置表单(保存即热重载);② / ③ / ④ 三张只读 `table`(运行状态 / 指纹自检 / 最近截图)。 + +> **为什么三块活数据都用 `table` 而不是 `status`**:`status` 区块按 §5.2.1 的接入点表**不消费 `source`**(没有来源就没有活数据),硬要用只能给一份声明期的静态文本 —— 那才是真正的「看起来有、其实没有」。`table` 是声明面里唯一**既有来源又能带按钮**的形状。 + +**六个动作(3 读 3 写;全部已登记生产处理器)**: + +| 动作名 | 语义 | 面 | 请求体 | 成功响应 `result` | +|---|---|---|---|---| +| `plugin.getBrowserState` | 运行状态只读事实(**不启动浏览器**) | read(table `source`) | `{}` | `{"rows":[{"source","available","headless","viewport","profileDir","concurrency","idleTimeoutSeconds","shotMaxBytes"}]}`。**恒为 1 行**(事实只有一份;并发上限恒为 1)⇒ 多行会诱导读者以为「有多个浏览器在跑」 | +| `plugin.getFingerprint` | **上一次**指纹自检的读数(**不启动浏览器**) | read(table `source`) | `{}` | `{"rows":[{"webdriver","screen","timezone","language","userAgent","checkedAt"}]}`。**从未自检过 ⇒ 一行如实写「未自检」**(不编造数字、不回空数组) | +| `plugin.listShots` | 最近截图**统计**(**最新在前**,最多 20 条) | read(table `source`) | `{}` | `{"rows":[{"at","bytes","size","format","degraded"}]}`。🔴 **不含路径、不含内容、不含 URL**(面板是只读投影,viewer 也看得到表格行 ⇒ 把主人的浏览内容投到那里是越界)。没截过图时**如实为空表** `{"rows":[]}` | +| `plugin.saveBrowserConfig` | 保存浏览器配置(落盘 + **热重载**) | write(form `submit`) | `{"values":{…}}`(**只发改动过的字段**;缺位的键**保持原值**,**绝不**把「没提到」当「清空」) | `{"saved":true,"applied":,"registered":,"reason":"<原因码/文案>"}`。`applied=true` = 这次保存**真的触发了热重载**(按落盘后的配置重建工具 + 原子替换注册表条目 ⇒ **下一轮生效、不需重启**);`applied=false` 且保存前**本来没有**可用工具 ⇒ 文件留下、如实报 `false` + `reason`(那种情形没有能力可丢,报错反而会让主人**没法先配好**) | +| `plugin.shootPage` | 拍一张**当前页面** | write(table `rowActions`) | `{"row":{…}}`(载荷不被解释,只为触发) | `{"shot":{…}}`,`shot` = `browser` 工具 `screenshot` 的返回值原样(见下面「模型面」一节),含 `path` / `bytes` / `sha256` / `format` / `width` / `height` / `dimensionsKnown` / `degraded` / `turnAttachment` / `hint` | +| `plugin.checkFingerprint` | 重新做一次指纹自检(**会启动浏览器**) | write(table `rowActions`) | `{"row":{…}}` | `{"rows":[…]}`(与 `plugin.getFingerprint` 同形,刷新后的读数) | + +**字段名(`values` 的键,逐字与 `$DATA_DIR/config/plugins/browser.json` 的 JSON tag 一致)**:`headless`(bool)、`executable`(string,留空 = 自动探测本机 Chrome/Chromium)、`cdpEndpoint`(string,显式给出时取代自动探测)、`profileDir`(string)、`viewportWidth` / `viewportHeight`(number,声明面已给 min/max)、`idleTimeoutSeconds`(number)、`concurrency`(number,**声明面 min = max = 1**:同一 profile 起两个实例会互锁)。🔴 **未知字段一律拒(`400 invalid_payload`),绝不静默忽略** —— 静默忽略会让「我改了但它没生效」变成查不出的谜,而且不该在盘上留下半份改动。 + +**错误码(本插件在 (b) 通用表之上追加的,宿主只透传「码 + 文案 + HTTP 状态」)**: + +| 状态 | `error.code` | 触发条件 | 客户端应做什么 | +|---|---|---|---| +| `400` | `invalid_payload` | 载荷不是对象 / 缺 `values` / `values` 不是对象 / **未知字段** / 字段类型错 | 显示文案原文;**不要**当成「保存成功」 | +| `409` | `browser_unavailable` | 本机**没有**可用浏览器来源(未探测到 Chrome/Chromium,也没有在跑的 CDP 端点)⇒ `plugin.shootPage` / `plugin.checkFingerprint` 做不了 | 提示「先去配置里填可执行文件或 CDP 端点并保存」;面板可以配 ⇒ 不是死路 | +| `409` | `browser_session_not_open` | `plugin.shootPage` 时浏览器**没有打开的页面** | 提示「先让她打开一个页面(或在有头窗口里人工接管)」—— **面板刻意不替模型开页**(否则它就成了「任意 URL 的截图器」) | +| `409` | `config_rejected` | 这次保存**没能生效**(新配置会让工具不可用),**已按字节回滚**到保存前那一份 | 显示原因原文;**盘上没有变**,不必再手动改回 | +| `500` | `config_unreadable` | 面板配置存在但**读不出来**(损坏 / 权限)⇒ 本次**没有**改动它 | 显示「配置读不出来」,指向服务端日志;**不要**重试保存 | +| `500` | `config_write_failed` | 配置写不进去(磁盘 / 权限);⚠️ **回滚也失败**时也用这个码,文案明说「盘上的文件可能已经变了」 | **不假装改动没发生**;提示可能需要人工检查盘上文件 | +| `502` | `browser_action_failed` | 浏览器动作本身执行失败(截图 / 自检) | 详细原因只进后端日志;显示「执行失败」+ 文案 | + +> **`applied:false` + `reason` 是正常结论,不是错误**:保存前**本来就没有**可用工具时(例如第一次配一个还没起来的容器端点),文件留下、`200` + `applied:false`。客户端**不要**把 `applied:false` 渲染成失败 —— 要说的是「已保存,但这次没有东西可换/没生效」+ `reason`。 + +**模型面(非客户端契约面,登记供跨端对照)**:`browser` 工具的 `screenshot` 返回值新增 **`turnAttachment`**(**闭集**,v0.82): + +| 取值 | 含义 | +|---|---| +| `queued` | 截图已入**本会话的待送附件箱**,将在本回合的下一次模型调用里作为附件送出(主模型收不了图时以视觉/OCR 的文字形态) | +| `no-session` | 本次调用拿不到会话 id(子代理 / 未接编排器的调用),**未入箱** ⇒ 由模型自己按 `path` 去读 | +| `outbox-full` | 该会话的箱子已满(**每会话最多 4 张**、TTL 10 分钟),**丢掉了最旧的那张**(如实报,不静默) | +| `disabled` | 本工具没有附件箱(`New(nil)` 这类夹具),未入箱 | + +配套的 `hint` 字段是一句**如实**的下一步提示(`queued` 时说「已经作为图片附件送出」;其余档说「图没进上下文,按路径读」)。**「入箱」到「进上下文」靠 `loop.Options.TransformContext` 上挂的钩子**(`cmd/cyrene/wire_browser_context.go`)—— 它**取走即消费**(同一张图不会被送两次),也因此**不会**进 checkpoint、不会进后台记忆提取的输入面。⚠️ **该挂点在 v0.82 登记时仍在工作树、未提交**;缺了它,`turnAttachment` 仍会照报 `queued` 而图**一张都不会进上下文**(**零症状**的坏法,因此有一条源码级用例盯着 `wire_runtime.go` 那一行)。 + --- ## 5. env 与模型配置(与契约相关) @@ -1236,7 +1370,26 @@ > 只保留最近若干版(**v0.60 起**);更早的全部历史见归档 `Docs/archive/api-client.v0.65.md` §5(v0.28 → v0.59)。 > 每版一句话 + 三档标注。**三档定义见 [§0.4](#04-三档标注惯例)。** -### v0.81(当前契约版本) +### v0.82(当前契约版本) + +**三面增量 —— ① `tool_execution.end` 带可选 `resultSummary`(工具执行结果实时可见,切片 Y1 `215eedf`)|② 自排的成本可见面:`?trigger=` 服务端筛选 + 新端点 `GET /admin/backthink/self-wake`(切片 Y3 `4213b26`)|③ `browser` 插件的管理面板与六个动作(切片 Y4 `e0a6bca`)**(依据:安卓端按 ① 把工具卡 UI 定死;`design/12 §N C.5/C.6` 的自排成本口径与筛选;`design/28 §8 B4` 的面板入口。) + +- **① `tool_execution.end` 的 `data` 新增可选 `resultSummary`(string)**:`{tool, phase, id?, delta?, resultSummary?}`。数据来源 = **loop 侧**在工具返回值落定的那一刻生成(`internal/runtime/loop/tool_frame_summary.go`),走的是**与出图同一条**脱敏路径(`trace.RedactText`:凭据形状清洗 + 截断),**不从上游原文反解、不新开脱敏通道**。修的是「实时只看到工具名与状态、要事后翻回看面才知道结果」。**§3.4.2** +- 🔴 **缺席语义(安卓端按此编程)**:**键在场 ⇒ 渲染结果区;键缺席 ⇒ 只显示工具名与状态**。**只有「真的没有结果」才缺席**:调用成功但结果为空(nil / 空串)、或错误文本本身为空。**绝不用空串顶位**(空串会让客户端渲染出一个空的结果区)。`tool_execution.start` / `.update` **不带**该键。 +- 🔴 **失败也带(本版口径)**:调用**失败 / 取消 / 超时**的 `.end` 帧带的是**该次错误文本的脱敏渲染** —— 与**回看面**(那里失败调用回的是图上的 `error` 摘要)**给同一份说法**,客户端不必为「实时」与「回看」写两套判据。⚠️ **本版之前**(切片 Y1 的首版实现)失败时是**整键缺席**、回看面却有 `error` 摘要 ⇒ 同一次调用「实时看不到原因、事后看得到」;本版把这处张力收掉了。 +- **预算**:与回看面 `toolCalls[].resultSummary` **共用同一个常量与同一个截断函数**(`maxToolCallSummaryRunes = 500`,超长按 rune 截断、末位 `…`)。网关侧**只截断、不做二次脱敏**(与回看面「只搬运、不加工」同一口径)。 +- **② 自排的成本可见面(切片 Y3)**: + - `GET /api/v1/admin/backthink` 新增**可选** `?trigger=` **服务端筛选**(design/12 §N C.6)。取值域 = **闭集**(`heartbeat` / `startup` / `self_wake` + 本服务订阅的事件主题,真源 `backthink.TriggerFilterValues()`);**值未知或为空 ⇒ `400 invalid_trigger`**(**绝不静默忽略** —— 那会让面板在显示全部的同时声称「正在筛选」);合法值**下推 SQL** ⇒ `limit` 数的是**匹配**条数。🔴 **「筛选白名单是闭集」与「`trigger` 的显示取值域不封闭」不矛盾**(前者约束客户端传进来的参数、后者约束我们发出去的显示值),见 §4.10(e-1)。 + - 新增 1 条 `writeAuth` 只读端点 **`GET /api/v1/admin/backthink/self-wake`**(design/12 §N C.5 的读侧)。响应 `{data:{available,running,reason?,configFile?,gate?}}`,`gate` = `{pending,used,limit,windowSec,defaultLimit,defaultWindowSec,limitIsDefault,windowIsDefault}`。**三态必须可区分**(面板不得把「看不到」画成「0 次」);`available:false` 时 `gate` **整键缺席**且 `reason` 说明原因、**HTTP 仍是 `200`**。🔴 **`limitIsDefault` / `windowIsDefault` 只说「生效值恰好等于代码默认值」**,**不是**「配置文件里没写这个键」(网关不读 `backthink.json`,与 B12 的 `isDefault` 同一诚实口径)。🔴 **它只读、不审计、不写库**,也**不含任何可改配置的入口**(改上限/窗口仍要改文件 + 重启)。**§2.2 B19 / §4.10(e-2)** + - 🔴 **同版改正一处已成假话的旧口径**:v0.81 写的「`request_think` **尚未接线** ⇒ `self_wake` 与 4 条审计动词在生产里不会出现」**已过期** —— 入口工具**已接线**(`cmd/cyrene/wire_services.go` 的 `wireSelfWakeTool`,切片 X2 `2d211c2`;启动日志自证 `self wake tool registered tool=request_think`)⇒ **它们在生产里会真的出现**。§4.10(d) 与 §7.1 已同步改。 +- **③ `browser` 插件的管理面板与六个动作(切片 Y4;design/28 §8 B4)**:`GET /api/v1/admin/panels` 的目录新增 **`browser` / `settings`**(「浏览器与截图」,`route = plugin/browser/settings`,4 个区块 = 1 个 `form` 配置 + 3 张只读 `table`),该插件声明 **6 个动作**(3 读 `plugin.getBrowserState` / `plugin.listShots` / `plugin.getFingerprint`;3 写 `plugin.saveBrowserConfig` / `plugin.shootPage` / `plugin.checkFingerprint`),**全部已登记生产处理器**,**不开任何新路由**(走既有 A14 通用动作分发)⇒ **§2.1 的 14 与总数不因本项变化**。保存 = **落盘 + 热重载**(重建工具 + 原子替换注册表条目 ⇒ 下一轮生效);保存前**本来有**可用工具而新配置让它不可用 ⇒ **回滚文件** + **`409 config_rejected`**;保存前**本来没有**可用工具 ⇒ 文件留下 + `200` + `applied:false` + `reason`(**不报错**,否则主人没法先配好)。🔴 **读面绝不启动浏览器**(面板的只读区块连 viewer 都能看,让「看一眼」产生进程外副作用是越界);真正会起浏览器的只有 `plugin.shootPage` / `plugin.checkFingerprint` 两个写动作(`plugin.saveBrowserConfig` 的「热重载」只在**重建工具**时按配置探测可执行文件)。**§4.14(d)** +- **③ 附:模型面(非客户端契约面,登记供跨端对照)**:`browser` 工具的 `screenshot` 返回值新增 **`turnAttachment`**(闭集 `queued` / `no-session` / `outbox-full` / `disabled`,配一句如实的 `hint`)—— 让**模型与面板**都能看出「这张图到底进没进上下文」,而不是只在日志里。⚠️ **`queued` 与「进上下文」之间还有一环**:`loop.Options.TransformContext` 上挂的装配钩子(`cmd/cyrene/wire_browser_context.go`)负责**取走即消费**地把它并进本轮视图。 +- **行为变更**:**无**(既有键逐字未改;新增的都是**可选键 / 新端点 / 新声明**)。 +- **破坏性**:**无**(`resultSummary` 是新可选键,老客户端不读即行为逐字不变;`?trigger=` 缺席即不过滤;`GET /admin/panels` 多一条目录项 —— 按 `pluginIds` 差集渲染「未声明管理页面」的客户端零影响)。 +- **计数**:`writeAuth` 段 **18 → 19**(新增 B19 `GET /admin/backthink/self-wake`)、管理路由总数 **72 → 73**(`14 + 19 + 40`);§2.2 行数 **B1–B19**。**本版无其它新路由**(③ 走既有 A14)。 +- ⚠️ **诚实边界**:① **①③ 的形状取自「工作树实现」** —— 登记时 ① 的「失败也带」(`internal/runtime/loop/tool_frame_summary.go`)与 ③ 的**两个组合根挂点**(`cmd/cyrene/wire_frontend.go` 的 `wireBrowserPanel(hub, a, a.log)`、`cmd/cyrene/wire_runtime.go` 的 `TransformContext: a.browserShotTransform(visionResolver),`)**都还在工作树、尚未提交** ⇒ **提交前生产里没有这些行为**;提交后请按 `git show` 逐字复核(见 §7.4-30 / §7.4-31);② ① 的**真机**(`:10942` 发布产物)端到端**未跑**;③ ② 的**真机面板端到端未跑**(面板写路径**未做**,只有读侧);④ ③ 的**真机 e2e 未跑**(既有 `wire_browser_e2e_test.go` 默认 `Skip`,需要真 Chrome + 可执行文件路径)。 + +### v0.81(上一版) **一批增量 + 两处真缺陷修复 —— ① 她自排轮(`self_wake`)的用户可见面(形状登记;生产暂不可达)|② 时间轴新增 `retentionBoundaryReached` 并与 `truncated` 分意(切片 X1)|③ `round` 跨重启不再重复(切片 X1 的行为修复)**(依据:`design/12 §N C` 的自排切片 C1-1 `94da6bc` / C1-2 `ed50053`;`design/19 §7` 时间轴的保留期诚实口径与 §6.1 的「`round` 每会话单调」;`AGENTS §9.5` 的对照要求。) @@ -1246,7 +1399,7 @@ - **行为变更**:**有(但都是「变真」而非「变形」)**:① 两处取值域**加性**扩张(只显示 `trigger` 原文的客户端零影响);② ③ 是**行为修复**(重启后轮次号不再重复 ⇒ 分组不再并块);② 是**字段加性**扩张 + 对既有 `truncated` 的**读数收紧**(此前把它当清扫信号读的客户端会发现自己原来读到的是假话)。 - **破坏性**:**无**(无键被删、无键被改义;`truncated` 的**定义**未变,变的是「不得拿它当清扫信号」这句口径)。 - **计数**:**无新端点** ⇒ §2.0 权限矩阵维持 **14 + 18 + 40 = 72**(§2.2 B1–B18 共 **18** 条;②③ 都是往既有 B18 的响应/行为上加东西)。 -- ⚠️ **诚实边界**:① 自排的**端到端生产验证未做**(入口工具未接线,见 ①);② **②③ 的形状与判据取自切片 X1 的「工作树实现」**(本版登记时该切片**尚未提交**,`f1fbc24` 之后仍在工作树)⇒ **commit 后请按 `git show` 逐字复核键名与判据**;③ ② 的三条盲区**没有测试能覆盖到「整段被清扫」那一支**(属设计登记的诚实边界);④ 时间轴**真机面板端到端**仍未跑(同 v0.80 ①);⑤ 同波其余切片(技能渐进披露 L2 `6475eb8`、示例技能 V3 `4a04ad4`、浏览器 B1/B2 V5 `f1fbc24`)**对客户端零形状影响**,故未逐条登记(浏览器是**模型面工具**,不进客户端契约面)。 +- ⚠️ **诚实边界**:① 自排的**端到端生产验证未做**(入口工具未接线,见 ①);② **②③ 的形状与判据取自切片 X1 的「工作树实现」**(本版登记时该切片**尚未提交**,`f1fbc24` 之后仍在工作树)⇒ **commit 后请按 `git show` 逐字复核键名与判据**;③ ② 的三条盲区**没有测试能覆盖到「整段被清扫」那一支**(属设计登记的诚实边界);④ 时间轴**真机面板端到端**仍未跑(同 v0.80 ①);⑤ 同波其余切片(技能渐进披露 L2 `6475eb8`、示例技能 V3 `4a04ad4`、浏览器 B1/B2 V5 `f1fbc24`)**对客户端零形状影响**,故未逐条登记。⚠️ 括注原文写「浏览器是**模型面工具**,不进客户端契约面」—— **对 B1/B2 成立,但已部分过期**:v0.82 的 **B4**(浏览器管理面板,切片 Y4)**进**客户端契约面,见 §4.14(d)。 ### v0.80(上一版) @@ -1521,10 +1674,16 @@ | **`interrupt.turnId`** | **已上线**(`c0dc435`) | §4.8;`ws_hub_turn.go`、`session_interrupt.go` | | **消息 DTO `turnId`**(含 `chat_messages.turn_id` 迁移 `0018`) | **已上线**(`d64b26d` + `d804c4e`) | §4.4;`history.go`、`session_turn_id.go` | | **写类工具文件级互斥**(读共享 / 读写与写写互斥,被占住回 `ErrFileBusy` 文案给模型) | **已上线**(`354b08a`、`ef3dcb6`)—— **纯服务端行为,无客户端可见契约面** | `internal/runtime/tool/{fileclaims.go,legfile.go,tool.go}`、`loop/tools.go` | -| **保留期清扫的「可分辨键」`retentionBoundaryReached`**(v0.81 新增,切片 X1) | 🟡 **已实现;登记时仍在工作树、待提交** | 作用:把「更早的轮次**被保留期清扫**」与「分页还有更多 / 本来就没有更早的轮次」**分开**(`truncated` 只答「本次是不是全部」)。形状 / 判据 / 三个盲区见 **§4.11.1(a)(b)-3**;`round` 跨重启单调的同期修复见 **§4.11.1(e)**。⚠️ **它是在本版登记之后才 commit 的**(本片无提交权限)⇒ 客户端可用,但**提交后请按 `git show` 逐字复核键名与判据** | -| **她自排的入口工具 `request_think`**(→ 使 `GET /admin/backthink` 的 `trigger="self_wake"` 与 4 条 `backthink.self_wake.*` 审计动词**真的可能出现**;v0.81 登记) | 🔴 **未落地(下一片在办;判定侧与自排本体已落地)** | 已落地:判定侧 C1-1 `94da6bc`、自排本体 C1-2 `ed50053`(含窗口上限 + 容量 1 待跑槽 + 4 条审计)。**缺的是她的入口工具**(`request_think`,`internal/services/backthink/thinktool.go` 尚未提交)⇒ **生产里目前不会产生自排轮**。形状见 §4.10(d);客户端**不要**依赖该取值一定出现 | +| **保留期清扫的「可分辨键」`retentionBoundaryReached`**(v0.81 新增,切片 X1) | ✅ **已上线(`2bf3b53`)** | 作用:把「更早的轮次**被保留期清扫**」与「分页还有更多 / 本来就没有更早的轮次」**分开**(`truncated` 只答「本次是不是全部」)。形状 / 判据 / 三个盲区见 **§4.11.1(a)(b)-3**;`round` 跨重启单调的同期修复见 **§4.11.1(e)**。⚠️ v0.81 登记时它**仍在工作树**(该版已如实标注);**现已提交** ⇒ 按 `git show 2bf3b53` 复核键名与判据 | +| **她自排的入口工具 `request_think`**(→ 使 `GET /admin/backthink` 的 `trigger="self_wake"` 与 4 条 `backthink.self_wake.*` 审计动词**真的可能出现**) | ✅ **已接线(切片 X2 `2d211c2`)** —— v0.81 登记时的「🔴 未落地」**已作废** | 判定侧 C1-1 `94da6bc` + 自排本体 C1-2 `ed50053`(窗口上限 + 容量 1 待跑槽 + 4 条审计)+ **入口工具 `request_think` 已注册**(`cmd/cyrene/wire_services.go` 的 `wireSelfWakeTool`,`internal/services/backthink/thinktool.go`)⇒ **生产里会真的产生自排轮**;启动日志自证 `self wake tool registered tool=request_think`。形状见 §4.10(d);⚠️ 自排轮仍要过既有 `decide` 级联 + `PushGate`,**不保证每轮都有**。**读它用了多少额度的出口**见 **§4.10(e-2) / §2.2 B19** | +| **`tool_execution.end` 的 `resultSummary`**(v0.82 新增,切片 Y1 `215eedf`;**「失败也带」在工作树**) | 🟡 **已实现;「失败也带」登记时仍在工作树、待提交** | 形状与缺席语义见 **§3.4.2**。⚠️ 提交前跑着的后端里**没有**该键;提交后请按 `git show` 复核「失败路径给 error 摘要」这条口径(`internal/runtime/loop/tool_frame_summary.go` 的 `toolEndFrameSummary`) | +| **`browser` 面板与截图闭环的组合根挂点**(v0.82 新增,切片 Y4 `e0a6bca`) | 🔴 **登记时未接线**(代码已落库,两个生产挂点在工作树) | 声明与六个动作见 **§4.14(d)**;`wire_frontend.go` 的 `wireBrowserPanel(hub, a, a.log)` 与 `wire_runtime.go` 的 `TransformContext: a.browserShotTransform(visionResolver),` 是**登记时**才进工作树的(删掉都**零症状**,各有源码级用例盯着)。🔴 **在跑的那份产物里没有它们**(真机已核:启动日志全篇 0 条 `browser panel declared`)⇒ 那一刻 `pluginIds == ["web-search"]`、`browser` 动作全 `404 action_not_declared`、`turnAttachment` 照报 `queued` 但图不进上下文 | -> ⚠️ **工作树另有未提交改动**:2026-10-06 晚的工作树上有**并行切片**的在途改动 —— 其中 **X1 的时间轴两项**(`retentionBoundaryReached` + `round` 跨重启播种)**已按「登记形状 + 明标未提交」进上表**;其余(她自排的入口工具 `request_think`、浏览器 SSRF 收口、`capacity` / 面板 `Backthink.tsx` 等)**尚未冻结、本表不登记** ⇒ 客户端**不要**按当前工作树形态编程。⚠️ 本注原举的 `

` 段落规整(`internal/runtime/turnreply/`)与前端 `turn` 分桶**均已提交**(不再是在途项),故删除该例。 +> ⚠️ **工作树另有未提交改动(v0.82 登记时的实测状态)**:**与本版两个面相关**的在途文件共 **5 个**(此外工作树还有同波其它切片的在途改动,与本契约无关、不影响本文档): +> - **① `resultSummary` 的「失败也带」**:`internal/runtime/loop/tool_frame_summary.go` + `..._test.go`(已按「登记形状 + 明标未提交」进上表); +> - **③ `browser` 的两个生产挂点**:`cmd/cyrene/wire_runtime.go`、`cmd/cyrene/wire_frontend.go`、`cmd/cyrene/wire_browser_context_test.go`(同样已进上表)。 +> +> 其余在途项(若有)**尚未冻结、本表不登记** ⇒ 客户端**不要**按当前工作树形态编程。⚠️ v0.81 时代本注举的项(X1 的时间轴两项、`request_think`、浏览器 SSRF 收口、`capacity` / 面板 `Backthink.tsx`)**均已提交**(`2bf3b53` / `2d211c2` / `a820d99`),不再是在途项。 > 归档版 v0.65 §J 曾把上表前四项列为「未实现」——那是**归档时的代码状态**,**已过期**。 ### 7.2 确认为「未实现」(别当已有) @@ -1546,7 +1705,9 @@ | **`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` 只在「已声明但宿主未登记处理器」时出现**(不再是兜底档)。✅ **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`) | +| **🔴 v0.78 热装插件的面板声明来源**(`plugins//manifest.json` 读取) | **未实现**(切片 PLG-2 / PLG-1):目录能键热装插件、生命周期已接,但**没人喂声明** ⇒ 热装插件在 `GET /admin/panels` 的 `pluginIds` / `groups` 里**恒为「未声明管理页面」**。有声明的是**内置**插件(组合根装配期 `Declare`):`web-search`(v0.78 起)、**`browser` / `settings`(v0.82 起,切片 Y4)** | +| **🔴 v0.82 `browser` 面板与 B3 截图闭环的「生产挂点」** | **登记时未接线**(切片 Y4 `e0a6bca`):声明与六个动作的代码已落库(§4.14(d)),但**组合根两行**(`wire_frontend.go` 的 `wireBrowserPanel(hub, a, a.log)`、`wire_runtime.go` 的 `TransformContext`)**在工作树、尚未提交** ⇒ 提交前:真机上 `pluginIds == ["web-search"]`、`POST /admin/plugins/browser/actions/*` **全部 `404 action_not_declared`**、`browser` 工具的 `screenshot.turnAttachment` 照报 `queued` 而图**不会**进上下文。**客户端不要**按未提交形态编程(同 §7.1 那一行) | +| **🔴 v0.82 `browser` 面板 / `resultSummary` 的「真机端到端」** | **未确证**:两者都只有测试进程内的证据(`httptest` / 真 hub / 真 loop),`:10942` 上的发布产物是旧构建 ⇒ 真机端到端一律未跑。见 **§7.4-30 / §7.4-31** | | **`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` 登记理由(**不在本版**) | | **插件动作审计的持久化粒度** | **未含 pluginId / action**(切片 PLG-2):持久化审计记录沿用既有 `AuditStore.Append(ctx, who, what, result, ts)` 四字段 ⇒ 只落 `what="plugin.action"`;插件 id 与动作名只在**结构化日志行**里。要进持久化记录需改 `AuditStore` 签名(**不在本版**) | @@ -1629,6 +1790,10 @@ 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` 语音**无副本、不可回放**是**刻意**的(平台自带转写就不白下载音频)。 +30. **(v0.82 登记)`tool_execution.end` 的 `resultSummary` 未做真机端到端**:证据止于**测试进程内**的「真 loop + 真工具 + 真 tracer/flow 图 → 真 bus → 真 relay → 真 hub → **真 WS 连接**」帧(`internal/services/gateway/ws_tool_result_summary_test.go` 用真 `httptest` + `websocket.Dialer` 读回帧),以及 loop 侧「帧上摘要是**从真图快照反读**的同一份文本」的同源钉(`TestRun_ToolEndFrameSummaryMatchesGraphResultSummary`)。⚠️ **仍未确证**:① 对 `:10942` 上的发布产物发一条**真会调工具**的消息、看客户端收到的 `.end` 帧是否带该键(换产物前不可能验);② **失败路径**上「实时帧的错误摘要」与「回看面的图上 `error` 摘要」是否**逐字一致**(同源链是代码级结论:两侧都过 `trace.RedactText`,但没有真机样例);③ 真机上「一次调用拿到超长结果」时帧上那份摘要的长度(代码级是 `RedactText` 的 ≤80 rune;测试造的是 >500 的假值,用来钉网关那道 ≤500 的兜底上限)。**为什么验不了**:本波禁换产物 / 禁重启后端(红线:后端常驻)。 + +31. **(v0.82 登记)`browser` 面板 / B3 截图闭环未做真机端到端**:证据止于 `cmd/cyrene` 的真 hub + 真能力注册表用例(`wire_browser_panel_test.go`:声明进目录、六个动作真能分发、读写面分类、保存真热重载 + 回滚;`wire_browser_context_test.go`:真 loop + 探针适配器 → 模型请求里确实出现 image 部件)。⚠️ **仍未确证**:① 真机面板上点开 `/admin/plugin/browser/settings` 能看到 4 个区块与真实行(需 owner 凭据 + 换产物);② 真机上保存一次浏览器配置后**下一轮**真的用新配置(热重载的真机确认);③ 真机**有头**窗口里「拍一张 → 图真的进下一轮上下文」的端到端(既有 `wire_browser_e2e_test.go` **默认 `Skip`**,它需要真 Chrome + 可执行文件路径 + 真模型);④ **登记时两个挂点尚未提交** ⇒ 上述三项在提交前**不可能**成立。**为什么验不了**:同上(禁换产物 / 禁重启后端),且本机浏览器来源与凭据未在测试环境确证。 + --- -*本文件由「以代码为真源」维护(当前 `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/**` 与本文件。* +*本文件由「以代码为真源」维护(当前 `v0.82`;`v0.81` / `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.82`(三面 —— Y1 `resultSummary` / Y3 自排成本可见面(`?trigger=` + `/admin/backthink/self-wake`)/ Y4 `browser` 面板与六个动作)同步了 `internal/runtime/model/stream.go`、`internal/runtime/loop/**`(`tools.go` / `tool_frame_summary.go` / `tool_frame_summary_test.go`)、`internal/services/gateway/**`(`ws_bridge.go` / `ws_tool_result_summary_test.go` / `admin_backthink_filter.go` / `admin_backthink_filter_test.go` / `admin_backthink_selfwake.go` / `admin_query.go` / `routes.go`)、`internal/services/backthink/**`(`backthink.go` / `store.go` / `trigger.go` / `trigger_domain.go` / `selfwake_status.go` 及其测试)、`internal/config/runtimeconfig/backthink.go`、`plugins/tools/browser/**`(`panel.go` / `paneldata.go` / `attach.go` / `tool.go` 及测试)、`internal/capability/browser/**`(`state.go` / `manager.go`)、`internal/runtime/multimodal/vision/**`(`shot.go` / `resolver.go`)、`cmd/cyrene/**`(`wire_browser_panel.go` / `wire_browser_panel_data.go` / `wire_browser_context.go` 及测试)`frontend/web/src/admin/modules/Backthink.tsx`、`frontend/web/src/admin/plugin/nav.ts`、`frontend/web/src/lib/api/adminSystem.ts` 与本文件(§2.0 / §2.1 A13 / §2.2 B8·B19 / §3.4.2 / §4.10(d)(e) / §4.14(a)(d) / §6 / §7.1 / §7.2 / §7.4);`v0.81`(自排轮可见面 + 时间轴 `retentionBoundaryReached` + `round` 跨重启单调)同步了 `internal/services/backthink/**`(`trigger.go` / `selfwake*.go` / `store.go`)、`internal/runtime/flowgraph/**`(`timeline.go` / `timeline_test.go` / `flowgraph.go`)、`internal/services/gateway/**`(`admin_flow_timeline.go` 及测试 / `routes.go`)与本文件(§0.1 / §2.2 B18 / §4.10(d) / §4.11.1 / §6 / §7.1);`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/**` 与本文件。*