feat(gateway): 面 1 补齐 —— 消息读取面 toolCalls[](工具回看)

前提由 FV2 提供(图自 v0.77 起带每路 turnId、每路各落一行)。本片:
- 迁移 0023:flow_graphs 加「按路查询键」(写入时回填图的 TurnID),老行按缺席处理
- GET /api/v1/sessions/{id}/messages 加**可选** toolCalls[],形状严格照 Docs/api-client.md
  v0.76 ① 已登记的:{name, status(ok|error|unknown), durationMs, argsSummary, resultSummary}
- 🔴 图取不到 ⇒ **字段整体缺席**(绝不回空数组冒充「没调过工具」)
- 一条批量 SQL(按页内 turnId 集合),禁 N+1;单条 ≤20 个工具、resultSummary ≤500 字符(常量+依据)
- 只放图里已有的、已脱敏内容,不放宽脱敏通道
This commit is contained in:
AskaEth committed 2026-10-06 09:03:55 +08:00
1 parent 99a644d545
commit c907a34510
10 files changed
+1429 -16

No files matched your search

@@ -0,0 +1,8 @@
-- 0023 的回滚:先删索引、再删列(SQLite 3.35+ 的 DROP COLUMN;索引必须先走,
-- 否则「被索引的列不能删」会让回滚失败)。
--
-- 本迁移只**加**一列与一个索引,不改任何既有列/行语义 ⇒ 回滚不触碰 0022 的
-- 表结构、唯一键与保留期索引,也不丢图(图仍在 graph_json 里)。
DROP INDEX IF EXISTS idx_flow_graphs_lane_turn_id;
ALTER TABLE flow_graphs DROP COLUMN lane_turn_id;
@@ -0,0 +1,48 @@
-- 0023_flow_graphs_lane_turn_id: 给 `flow_graphs` 一张**按路查询的键**(切片 CODE-TC)。
--
-- 动因(安卓侧需求,主人 2026-10-05 裁定):`GET /api/v1/sessions/{id}/messages`
-- 要能回看「这条回复当时调了什么工具」,数据源是**已落盘的轮次链路图**
-- (`flow_graphs`,迁移 0022),**不从正文反解、不靠时间窗猜**。要做到这一点,
-- 消息行里的 `chat_messages.turn_id`(= 编排器生成的**路标识**,迁移 0018)必须能
-- 按**等值**查到它那一轮的图 —— 而 0022 的 `turn_id` 列存的是 `trace.FlowGraph.ID`
-- (形如 `web-7` / `web-7#turn-a`),与消息的 turn_id **不同源**。这正是
-- Docs/api-client.md v0.76 把 `toolCalls[]` 登记为「未实现」的实测阻塞。
--
-- 列名刻意**不叫** `turn_id`(那个名字已被 0022 占用,语义是图 id)。`lane_turn_id`
-- 的语义一句话:**这一路**的权威回合标识,与 `chat_messages.turn_id` 同值同源
-- (`trace.FlowGraph.TurnID` / design/15 §2.5.9)。写入侧 `RecordRound` 每次都盖;
-- 它为空时该行**不可按路查到**,读取侧据此让 `toolCalls` 整体缺席。
--
-- 兼容老行(三类,都不做假):
-- ① **v0.77 起写入的真并行路行**:图 JSON 里已带 `$.turnId` ⇒ 本迁移用
-- `json_extract` 就地回填,不必重跑任何轮次;
-- ② **v0.77 之前写入的行**:图里根本没有这个字段(那时还没加)⇒ 回填后仍是 `''`;
-- ③ **单路回合的行**:它的图**当前不带** `$.turnId`(该键只对真并行路存在,
-- 见 `internal/runtime/loop/flow.go` 的 `flowOwner`)⇒ 同样回填成 `''`。
-- ②③ 的空串是**诚实的空**:读侧因此查不到这一轮的工具、`toolCalls` 整体缺席 ——
-- 正是契约登记的语义(「图取不到 ⇒ 字段缺席」,**绝不回空数组**冒充「没调过工具」)。
-- 这三类都不是本迁移能补齐的(缺口在写侧),因此本迁移只**如实落列**、不编造值。
--
-- 列口径与 0018 的 `chat_messages.turn_id` 逐字一致:`TEXT NOT NULL DEFAULT ''`,
-- **绝不用 NULL** ⇒「旧行」与「没写」是同一个值,读侧只有一种空情形要处理。
-- 命名沿用既有迁移口径(表名复数、索引 idx_{table}_{col};援引 AGENTS §10 的
-- 已登记例外)。
ALTER TABLE flow_graphs ADD COLUMN lane_turn_id TEXT NOT NULL DEFAULT '';
-- 一次性回填:只碰**还空着**的行。本迁移是第一次写这一列,此刻全空;条件仍写上,
-- 使本语句重复执行时幂等,且日后绝不会抹掉 RecordRound 写进去的值。
-- `json_type(...) = 'text'` 把「不是字符串的值」(畸形外部改动)排除在外;
-- `json_valid` 兜底被写坏的 JSON。两者都不成立的行保持 `''` —— 让整次迁移因一条
-- 坏行而回滚、后端起不来,是远超它应有代价的处置。
UPDATE flow_graphs
SET lane_turn_id = json_extract(graph_json, '$.turnId')
WHERE lane_turn_id = ''
AND json_valid(graph_json)
AND json_type(graph_json, '$.turnId') = 'text';
-- 读侧的**唯一**依赖:一页消息的路标识集合一次 `IN` 查完(禁 N+1)。
-- 前导列是 session_id —— 所有查询都带 `session_id = ?` 收敛到本会话,只索引
-- lane_turn_id 一列会让规划器退化成扫本会话全表。
CREATE INDEX IF NOT EXISTS idx_flow_graphs_lane_turn_id
ON flow_graphs (session_id, lane_turn_id);