feat(chat): chat_messages.turn_id 列 + DTO(写入接线留待下一步)
迁移 0018:ALTER TABLE chat_messages ADD COLUMN turn_id TEXT NOT NULL DEFAULT ''
(与 persona_id / client_msg_id / document 同族:不用 NULL,空串 = 不属于任何已知回合 ⇒ 旧行零迁移、键省略)。
存储层全链路带 turn_id(列清单唯一一份 ⇒ List/ListAfter/Recent/Search/FindByClientMsgID 自动同源);
新增 history/turn_id.go:ensureTurnIDColumn(既有库幂等前滚)+ SetMessagesTurnID
(第二段补写:单事务参数化、先到先得 WHERE turn_id = ''、幂等、未知 id 忽略)。
DTO:historyMessage.TurnID(json turnId,omitempty)⇒ REST 列表 / admin 渠道日志 / WS 握手回放 /
message_persisted / channel_persisted / message_edited 一次全覆盖。
写入面:persistUserMessages 读 ctx 回合标识;persistAssistantReply 增可选尾参(既有调用点零改动);
withTurnID / turnIDFrom / assignTurnID / turnIDOfLeg。
⚠️ 编排器把 turnId 交给落库的 4 处(新起一路 / 续跑路 / 被 steer 吃进 / 助手回复)尚未接线
(那些文件当时被另一路占用);当前生产行为 turn_id 仍为 ''。清单已在切片报告里精确到文件:行。
13 新用例 + 5 条变异(INSERT 不带列 / DTO 不回填 / 去掉 omitempty / 去掉先到先得 / 不回滚前滚)。
为守 session_persist.go ≤500 行,turnId 一节拆到新文件 session_turn_id.go。
This commit is contained in:
1 parent
354b08a45f
commit
d64b26d499
12 files changed
+950
-16
No files matched your search
@@ -0,0 +1,71 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// turnIDMigrationID 是回合标识列的迁移 id(design/15 §2.5.9)。
|
||||
const turnIDMigrationID = "0018_chat_message_turn_id"
|
||||
|
||||
// TestMigrateTurnIDColumnReversible 验证 0018(chat_messages.turn_id,回合标识列)
|
||||
// 可上下翻滚:up 建列、重复 up 是空操作、down 删列、再 up 恢复。它用自己的内存库,
|
||||
// 绝不碰 data/cyrene.db。
|
||||
func TestMigrateTurnIDColumnReversible(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
conn := openMemory(t)
|
||||
|
||||
if err := MigrateUp(ctx, conn, testLogger()); err != nil {
|
||||
t.Fatalf("MigrateUp: %v", err)
|
||||
}
|
||||
if !columnExists(t, ctx, conn, "chat_messages", "turn_id") {
|
||||
t.Fatal("chat_messages.turn_id should exist after MigrateUp")
|
||||
}
|
||||
|
||||
// 重复 up:已记录的迁移被跳过,列仍在且不报错(幂等)。
|
||||
if err := MigrateUp(ctx, conn, testLogger()); err != nil {
|
||||
t.Fatalf("MigrateUp second call: %v", err)
|
||||
}
|
||||
if !columnExists(t, ctx, conn, "chat_messages", "turn_id") {
|
||||
t.Fatal("chat_messages.turn_id should still exist after idempotent re-run")
|
||||
}
|
||||
|
||||
// 恰好回滚到 0018(含它之后新加的迁移),因此日后再加迁移也不会让本测试失效。
|
||||
ids := upMigrationIDs(t)
|
||||
idx := -1
|
||||
for i, id := range ids {
|
||||
if id == turnIDMigrationID {
|
||||
idx = i
|
||||
}
|
||||
}
|
||||
if idx < 0 {
|
||||
t.Fatalf("%s not found in %v", turnIDMigrationID, ids)
|
||||
}
|
||||
if err := MigrateDown(ctx, conn, len(ids)-idx, testLogger()); err != nil {
|
||||
t.Fatalf("MigrateDown(%d): %v", len(ids)-idx, err)
|
||||
}
|
||||
if columnExists(t, ctx, conn, "chat_messages", "turn_id") {
|
||||
t.Fatal("chat_messages.turn_id should be dropped by the 0018 down")
|
||||
}
|
||||
|
||||
// 重新 up 恢复端到端可用:默认空串意味着「这一行不属于任何已知回合」。
|
||||
if err := MigrateUp(ctx, conn, testLogger()); err != nil {
|
||||
t.Fatalf("re-MigrateUp: %v", err)
|
||||
}
|
||||
if !columnExists(t, ctx, conn, "chat_messages", "turn_id") {
|
||||
t.Fatal("chat_messages.turn_id should exist again after re-MigrateUp")
|
||||
}
|
||||
if _, err := conn.ExecContext(ctx,
|
||||
`INSERT INTO chat_messages (id, session_id, role, content, created_at) VALUES (?, ?, ?, ?, ?)`,
|
||||
"m1", "web", "user", "hi", 1000); err != nil {
|
||||
t.Fatalf("insert without turn id: %v", err)
|
||||
}
|
||||
var turnID string
|
||||
if err := conn.QueryRowContext(ctx, `SELECT turn_id FROM chat_messages WHERE id = ?`, "m1").Scan(&turnID); err != nil {
|
||||
t.Fatalf("select turn_id: %v", err)
|
||||
}
|
||||
if turnID != "" {
|
||||
t.Fatalf("turn_id = %q, want the empty default", turnID)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
-- 0018_chat_message_turn_id down: 回滚回合标识列。
|
||||
--
|
||||
-- turn_id 只被 chat_messages 自己引用(无索引、无视图、无约束牵涉),因此直接删列
|
||||
-- 即可。回滚后的状态是「服务端不再记得哪条回复在答哪句提问」:客户端退回按顺序
|
||||
-- 渲染(本列引入之前的行为),行里的分组信息随列一起丢弃 —— 它只是**派生的展示
|
||||
-- 元数据**,真实回合身份仍由流帧(帧壳 turnId)携带,删列不会让任何在跑的回合
|
||||
-- 失去自己的身份。
|
||||
|
||||
ALTER TABLE chat_messages DROP COLUMN turn_id;
|
||||
@@ -0,0 +1,27 @@
|
||||
-- 0018_chat_message_turn_id: 给聊天消息挂上「它属于哪一路回合」(design/15 §2.5.9)。
|
||||
--
|
||||
-- 并行回合(design/15 §2.5)落地后,一个会话可以同时有两条在飞的**路**,同一个
|
||||
-- 会话的行因此不再能靠「顺序」唯一回答「这条回复在答哪一句提问」。turn_id 就是
|
||||
-- 编排器生成、该路全部流帧与 `turn_begin` 帧壳同值的那一份权威回合标识
|
||||
-- (internal/services/gateway 的 turnLeg.turnID,经 loop.WithTurnID 盖在帧上)。
|
||||
--
|
||||
-- 用途(客户端依赖):① 「回复 ↔ 提问」精确配对 —— 同一 turn_id 的 user 行与
|
||||
-- assistant 行互为配对;② 重连 / 回放时从历史行恢复「回复侧分组小标」。
|
||||
--
|
||||
-- 它**可选**:'' 意味着「这一行不属于任何已知的回合」—— 加入本列之前的全部历史行、
|
||||
-- 以及未经编排器的写入路径(IM 镜像等)都是这种情形,读起来因此与加入本列之前
|
||||
-- 逐字相同(DTO 里该键缺席)。列口径与同族可选列(persona_id / client_msg_id /
|
||||
-- document)完全一致:NOT NULL DEFAULT '',**绝不用 NULL** —— 于是「旧行」与
|
||||
-- 「没写」是同一个值,读取侧只有一种空情形要处理。这一列因此**不需要索引**:
|
||||
-- 它只被随行读出,没有任何按它过滤/排序的查询。
|
||||
--
|
||||
-- 写入是**两段式**(与 document 列同一手法):消息在它所属的那一路已经存在时
|
||||
-- (新起的一路、以及助手回复)就把 turn_id 写进 INSERT;排队 / 被转向吃进的消息
|
||||
-- 在落库那一刻还没有自己的路,由它**实际被处理的那一路**在起跑/消费时补写同一
|
||||
-- 列(先到先得,见 internal/runtime/history/turn_id.go 的 SetMessagesTurnID)。
|
||||
--
|
||||
-- 0003 的 chat_messages 表在此已存在,故 ALTER ADD COLUMN 是非破坏性的正确一步;
|
||||
-- 全新库由 internal/runtime/history.NewStore 幂等地前滚(含该列)。表名沿用复数
|
||||
-- 口径(援引 AGENTS §10 已登记的例外)。
|
||||
|
||||
ALTER TABLE chat_messages ADD COLUMN turn_id TEXT NOT NULL DEFAULT '';
|
||||
Reference in new issue
Block a user