feat(gateway): 文档多值化 + 抽取结论回写(design/24 S3.5)
- 缺口①:「一回合最多 5 个附件、落库只留 1 个」——document 列语义升级为 ≤5 个引用的**数组**(列名/字段名不变、无需迁移,旧单对象行可读回);落库收集全部引用、转录还原全部部件。 - 缺口②:抽取结论 status/chars/truncated 经 completeTurn 回合后**回写**该回合用户消息的同一列(id 按代次配对,忙时消息挂 pendingMsg.persisted 并在 drain 时以新 gen 重新配对);历史 DTO 改为 **documents: []**(omitempty ⇒ 0 个附件时省略)。 - 防御:编/解码超 5 都报 ErrTooManyDocuments(报错、绝不裁剪);坏列走既有不静默路径(转录 WARN + DTO unreadable 最小引用);>5 上限值被独立用具例钉死。 - 顺手修 S3-B 遗留的函数越界:submitInternal 81 → 79 行(播种块抽成 seedTurnLocked);session_orchestrator.go 484 行。 - 主会话独立验收:隔离 worktree(HEAD + 本切片 12 文件)build/vet 绿、全树 59 包 0 FAIL、linux+darwin 交叉编译绿、gateway+history 437 PASS;变异独立复现(只留第一个附件 ⇒ 2 条红,含 HTTP 端到端 want 3 条)。 - 遗留(已登记):chat_documents_test.go 510 行(S3-B 遗留越界,本次未拆);中断/被取代的回合不回写结论;回写后不补发实时帧(status 需重新拉取/回放可见)。
This commit is contained in:
1 parent
20c1edb36a
commit
5b2b385524
12 files changed
+1255
-166
No files matched your search
@@ -58,6 +58,23 @@ func (o *Orchestrator) startTurnMessages(ctx context.Context, sessionID string,
|
||||
return concatMessages(seed, messages...)
|
||||
}
|
||||
|
||||
// seedTurnLocked 为新回合播种对话(持久化转录在刚发送的消息之前),并把播种后的
|
||||
// 尾部记进会话状态。**必须在 o.mu 下调用**(它读写 st.tail;与 admitLocked /
|
||||
// claimClientMsgLocked 同一约定),调用方负责解锁 —— 因此它从
|
||||
// session_orchestrator.go 的 submitInternal 里拆出来,让那个函数留在「函数 ≤80
|
||||
// 行」的硬约束内。
|
||||
//
|
||||
// 播种的时刻是刻意的:它在 persistUserMessages 写入新消息**之前**,因此刚发送的
|
||||
// 消息绝不在上下文中重复(design/15 §2.2/§4、design/22 §7-5)。没有历史(或空
|
||||
// 会话)的存储让该回合只使用调用方给出的消息。它是在状态锁下的有界索引读取
|
||||
// (transcriptContextCount 行),而状态锁从不会被运行中的回合持有,因此它无法把
|
||||
// Submit 堵在回合后面。
|
||||
func (o *Orchestrator) seedTurnLocked(ctx context.Context, st *sessionState, sessionID string, messages []model.Message) []model.Message {
|
||||
turn := o.startTurnMessages(ctx, sessionID, messages)
|
||||
st.tail = o.boundedTail(turn)
|
||||
return turn
|
||||
}
|
||||
|
||||
// sessionTranscript 读取该会话最近的持久化消息并把它们
|
||||
// 映射为模型消息(最旧优先)。nil/不可读的存储或读错误
|
||||
// 都得到 nil,因此聊天绝不被元数据查找阻塞;没有
|
||||
@@ -88,7 +105,7 @@ func (o *Orchestrator) sessionTranscript(ctx context.Context, sessionID string)
|
||||
// persona_id)作为其说话者标签,因此即使会话被切换过,
|
||||
// 上下文也能把它归属到那个角色;非助手消息保持其
|
||||
// 已存发送者标签(design/16 §4.6 的第三方显示名),绝不
|
||||
// 被重新标为说话角色。文档引用一并还原(design/24 §9.5):
|
||||
// 被重新标为说话角色。文档引用**全部**一并还原(design/24 §9.5):
|
||||
// 少了它,附件会在发起它的那一回合之后的所有回合里消失。
|
||||
func transcriptMessage(h history.Message) model.Message {
|
||||
role := historyRoleToModel(h.Role)
|
||||
@@ -108,9 +125,13 @@ func transcriptMessage(h history.Message) model.Message {
|
||||
return msg
|
||||
}
|
||||
|
||||
// transcriptDocumentParts 把已存 document 列还原为该回合的文档部件(design/24
|
||||
// §9.5)。部件只交给 RoleUser 回合(§2.3-③):文档在任何别的角色上都会让编码器
|
||||
// fail-loud,而一行坏数据不该把整个会话卡死。
|
||||
// transcriptDocumentParts 把已存 document 列里的**全部**引用还原为该回合的文档
|
||||
// 部件(design/24 §9.5)。部件只交给 RoleUser 回合(§2.3-③):文档在任何别的
|
||||
// 角色上都会让编码器 fail-loud,而一行坏数据不该把整个会话卡死。
|
||||
//
|
||||
// 列里是引用数组(一个回合最多 5 个附件,缺口①),因此这里逐条还原、保持部件
|
||||
// 顺序 —— 少还原一条,那个附件就会在发起它的回合之后的所有回合里消失,与
|
||||
// 「清单恒在」的承诺自相矛盾。
|
||||
//
|
||||
// 列被写坏(或附件落在非用户行上)时**不静默**:打一条带来源的 WARN 并省掉
|
||||
// 该部件,回合照常继续 —— 附件凭空消失正是这条清单要修的缺陷,因此必须留下
|
||||
@@ -123,22 +144,26 @@ func transcriptDocumentParts(role model.Role, h history.Message) []model.Content
|
||||
transcriptDocumentLog().Warn("transcript document on non-user row", "message", h.ID, "role", h.Role)
|
||||
return nil
|
||||
}
|
||||
ref, err := history.DecodeDocument(h.Document)
|
||||
refs, err := history.DecodeDocuments(h.Document)
|
||||
if err != nil {
|
||||
transcriptDocumentLog().Warn("transcript document decode", "message", h.ID, "err", err)
|
||||
return nil
|
||||
}
|
||||
if ref == nil || ref.Name == "" {
|
||||
transcriptDocumentLog().Warn("transcript document without name", "message", h.ID)
|
||||
return nil
|
||||
parts := make([]model.Content, 0, len(refs))
|
||||
for _, ref := range refs {
|
||||
if ref.Name == "" {
|
||||
transcriptDocumentLog().Warn("transcript document without name", "message", h.ID)
|
||||
continue
|
||||
}
|
||||
doc := model.Document{
|
||||
Name: ref.Name,
|
||||
MIME: ref.MIME,
|
||||
URL: documentFilePrefix + ref.Name,
|
||||
Size: ref.Size,
|
||||
}
|
||||
parts = append(parts, model.DocumentMessage(model.RoleUser, "", doc).Content...)
|
||||
}
|
||||
doc := model.Document{
|
||||
Name: ref.Name,
|
||||
MIME: ref.MIME,
|
||||
URL: documentFilePrefix + ref.Name,
|
||||
Size: ref.Size,
|
||||
}
|
||||
return model.DocumentMessage(model.RoleUser, "", doc).Content
|
||||
return parts
|
||||
}
|
||||
|
||||
// historyRoleToModel 把已存聊天角色映射为模型的角色词汇,
|
||||
|
||||
Reference in new issue
Block a user