feat(runtime): document 内容类型与持久化(design/24 S2)
- model 新增 ContentTypeDocument + Content 的四字段 + DocumentMessage 构造器 + ValidateDocumentParts(文档部件只允许 RoleUser,非 user ⇒ ErrDocumentOnNonUser,fail-loud)。 - 两个编码器入口(buildMessages / buildAnthropicMessages)对文档部件报 ErrBadRequest: 本期 document 永不抵达编码器,且绝不静默丢块。 - history:拆出 types.go(Message 增 Document 列),新增 document.go(DocumentRef 同时是 落库 JSON 与 DTO 形状,6 字段含 status ⇒ §4.4 的「读不到要可见」;坏列回 unreadable 不静默)。 - chat_messages 加 document 列(迁移 0015 + NewStore 幂等前滚);historyMessage.Document DTO; session_persist 的 messageTextImage → messageTextImageDocument(保留窄视图以免牵连 steer 配对)。 - 只落「引用 + 统计」,绝不落抽取正文(TestPersistedDocumentHoldsNoBody 把全部列拼起来扫正文)。 验收(主会话独立复跑,非自报):隔离 worktree(HEAD + 本切片)go build/vet 绿、 全树 58 包 0 FAIL、linux/amd64 + darwin/arm64 交叉编译绿、18 条 Document* 测试 PASS。 变异独立复现:ValidateDocumentParts 开头 return nil ⇒ 非 user 负例红。
This commit is contained in:
1 parent
407c934a05
commit
6b2b07cbef
15 files changed
+895
-101
No files matched your search
@@ -0,0 +1,71 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// documentMigrationID 是文档引用列的迁移 id(design/24 §2.4)。
|
||||
const documentMigrationID = "0015_chat_message_document"
|
||||
|
||||
// TestMigrateDocumentColumnReversible 验证 0015(chat_messages.document,文档
|
||||
// 引用列)可上下翻滚:up 建列、重复 up 是空操作、down 删列、再 up 恢复。
|
||||
// 它用自己的内存库,绝不碰 data/cyrene.db。
|
||||
func TestMigrateDocumentColumnReversible(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", "document") {
|
||||
t.Fatal("chat_messages.document 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", "document") {
|
||||
t.Fatal("chat_messages.document should still exist after idempotent re-run")
|
||||
}
|
||||
|
||||
// 恰好回滚到 0015(含它之后新加的迁移),因此日后再加迁移也不会让本测试失效。
|
||||
ids := upMigrationIDs(t)
|
||||
idx := -1
|
||||
for i, id := range ids {
|
||||
if id == documentMigrationID {
|
||||
idx = i
|
||||
}
|
||||
}
|
||||
if idx < 0 {
|
||||
t.Fatalf("%s not found in %v", documentMigrationID, 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", "document") {
|
||||
t.Fatal("chat_messages.document should be dropped by the 0015 down")
|
||||
}
|
||||
|
||||
// 重新 up 恢复端到端可用:默认空串意味着「这条消息没有附件」。
|
||||
if err := MigrateUp(ctx, conn, testLogger()); err != nil {
|
||||
t.Fatalf("re-MigrateUp: %v", err)
|
||||
}
|
||||
if !columnExists(t, ctx, conn, "chat_messages", "document") {
|
||||
t.Fatal("chat_messages.document 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 document: %v", err)
|
||||
}
|
||||
var doc string
|
||||
if err := conn.QueryRowContext(ctx, `SELECT document FROM chat_messages WHERE id = ?`, "m1").Scan(&doc); err != nil {
|
||||
t.Fatalf("select document: %v", err)
|
||||
}
|
||||
if doc != "" {
|
||||
t.Fatalf("document = %q, want the empty default", doc)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
-- 0015_chat_message_document down: 回滚文档引用列。
|
||||
--
|
||||
-- document 只被 chat_messages 自己引用(无索引、无视图、无约束牵涉),因此
|
||||
-- 直接删列即可。回滚后的状态是「服务端不再记得这条消息带过附件」:列里的引用
|
||||
-- 与统计随列一起丢弃,而文档本体(uploads/extracted)与元数据表不在本迁移的
|
||||
-- 范围内,由各自的切片/GC 处置。
|
||||
|
||||
ALTER TABLE chat_messages DROP COLUMN document;
|
||||
@@ -0,0 +1,19 @@
|
||||
-- 0015_chat_message_document: 给聊天消息挂上「文档引用」(design/24 §2.4)。
|
||||
--
|
||||
-- 「分享到 App · 超限走文件」把长文/文件做成服务端文档,再由 chat 以
|
||||
-- document 引用它。这条消息需要在历史里看得见「她读过这个文件」,因此
|
||||
-- chat_messages 增一列 document,存一个 JSON 小对象:
|
||||
-- {name, mime, size, chars, truncated, status}
|
||||
-- name 是服务端存储名(内容寻址、32 位十六进制)、mime 是服务端字节嗅探所得、
|
||||
-- chars/truncated 是抽取统计、status 是抽取状态(extracted|listed|unreadable)。
|
||||
--
|
||||
-- 它**刻意只存引用与统计,绝不存抽取正文**:正文一旦落库,每翻一页历史都要
|
||||
-- 背着它重传(G3 把 base64 原样落库的教训),而且附件正文是提示词注入面
|
||||
-- (design/11 §10)。正文留在 <DataDir>/extracted 的 sidecar 里,装配期按
|
||||
-- 引用读(design/24 §4.3:只抽一次,不每回合重算)。
|
||||
--
|
||||
-- 0003 的 chat_messages 表在此已存在,故 ALTER ADD COLUMN 是非破坏性的正确
|
||||
-- 一步;全新库由 internal/runtime/history.NewStore 幂等地前滚(含该列)。
|
||||
-- 表名沿用复数口径(援引 AGENTS §10 已登记的例外)。
|
||||
|
||||
ALTER TABLE chat_messages ADD COLUMN document TEXT NOT NULL DEFAULT '';
|
||||
Reference in new issue
Block a user