feat(gateway): chat.documents 解析 + 修两处静默吞掉(design/24 S3-B)
- REST(同步 + stream:true)与 WS 两条入口同形解析 documents:只收服务端存储名(形态 + 真实存在 + 嗅探 MIME + 本地 URL + 大小),客户端谎报 mime/url/size 一律被服务端覆盖 ⇒ 不重开 SSRF 面(有「真实可命中 URL,命中次数 0」的用例钉住)。 - 拒绝口径对齐 S1:超过 5 条 → 400 too_many_documents(且先于任何 FS 访问);形态非法/穿越/不存在 → 404 document_not_found。 - 挂载:一回合一条 RoleUser 消息,文档部件追加其后(纯附件回合不塞空文本块,空文本会被 provider 拒)。 - 修 S2 移交的两处静默吞掉:session_transcript 用 history.DecodeDocument 把 h.Document 还原为部件(坏列带来源 WARN 不静默、也不卡会话);session_tail 的 messagesEmpty 计入 ContentTypeDocument(纯附件不再被判空回合)。 - 拆文件/拆函数(AGENTS:文件≤500 行、函数≤80 行):new chat_documents.go(rest.go 原 499/500 行塞不下)、handleChat 尾段 54 行原样搬成 submitChatTurn(handleChat 90→40 行),ws_hub 增 documents 依赖(与既有 envResolver 同接法)。 - 主会话独立验收(隔离 worktree = 当前 HEAD + 仅 B 的文件 ⇒ 正是 A+B 合流态):build/vet 绿、全树 59 包 0 FAIL、13 条新用例 PASS、linux/darwin 交叉编译绿;变异独立复现(摘掉条数上限判定 ⇒ 红)。 - 主会话补的测试钉子:TestMaxChatDocumentsMatchesDesign —— 原条数用例是用常量自身算输入条数的,改常量它照样绿(实测 5→1000 全包仍绿);补钉后改 10 时新钉子红、旧用例仍绿。
This commit is contained in:
1 parent
a8851dc0a2
commit
a4789ec13e
8 files changed
+859
-87
No files matched your search
@@ -2,7 +2,9 @@ package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||
)
|
||||
@@ -86,7 +88,8 @@ func (o *Orchestrator) sessionTranscript(ctx context.Context, sessionID string)
|
||||
// persona_id)作为其说话者标签,因此即使会话被切换过,
|
||||
// 上下文也能把它归属到那个角色;非助手消息保持其
|
||||
// 已存发送者标签(design/16 §4.6 的第三方显示名),绝不
|
||||
// 被重新标为说话角色。
|
||||
// 被重新标为说话角色。文档引用一并还原(design/24 §9.5):
|
||||
// 少了它,附件会在发起它的那一回合之后的所有回合里消失。
|
||||
func transcriptMessage(h history.Message) model.Message {
|
||||
role := historyRoleToModel(h.Role)
|
||||
msg := model.Message{
|
||||
@@ -96,6 +99,7 @@ func transcriptMessage(h history.Message) model.Message {
|
||||
if h.Image != "" {
|
||||
msg.Content = append(msg.Content, model.Content{Type: model.ContentTypeImage, ImageURL: h.Image})
|
||||
}
|
||||
msg.Content = append(msg.Content, transcriptDocumentParts(role, h)...)
|
||||
if role == model.RoleAssistant {
|
||||
msg.SenderName = h.PersonaID
|
||||
return msg
|
||||
@@ -104,6 +108,39 @@ func transcriptMessage(h history.Message) model.Message {
|
||||
return msg
|
||||
}
|
||||
|
||||
// transcriptDocumentParts 把已存 document 列还原为该回合的文档部件(design/24
|
||||
// §9.5)。部件只交给 RoleUser 回合(§2.3-③):文档在任何别的角色上都会让编码器
|
||||
// fail-loud,而一行坏数据不该把整个会话卡死。
|
||||
//
|
||||
// 列被写坏(或附件落在非用户行上)时**不静默**:打一条带来源的 WARN 并省掉
|
||||
// 该部件,回合照常继续 —— 附件凭空消失正是这条清单要修的缺陷,因此必须留下
|
||||
// 痕迹。日志只带消息 id 与错误,绝不带正文。
|
||||
func transcriptDocumentParts(role model.Role, h history.Message) []model.Content {
|
||||
if h.Document == "" {
|
||||
return nil
|
||||
}
|
||||
if role != model.RoleUser {
|
||||
transcriptDocumentLog().Warn("transcript document on non-user row", "message", h.ID, "role", h.Role)
|
||||
return nil
|
||||
}
|
||||
ref, err := history.DecodeDocument(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
|
||||
}
|
||||
doc := model.Document{
|
||||
Name: ref.Name,
|
||||
MIME: ref.MIME,
|
||||
URL: documentFilePrefix + ref.Name,
|
||||
Size: ref.Size,
|
||||
}
|
||||
return model.DocumentMessage(model.RoleUser, "", doc).Content
|
||||
}
|
||||
|
||||
// historyRoleToModel 把已存聊天角色映射为模型的角色词汇,
|
||||
// 未知角色默认归到用户通道。
|
||||
func historyRoleToModel(role string) model.Role {
|
||||
@@ -127,3 +164,15 @@ func assistantTailMessage(text, personaID string) model.Message {
|
||||
msg.SenderName = personaID
|
||||
return msg
|
||||
}
|
||||
|
||||
// transcriptLogSource 是转录重建的日志来源标签。
|
||||
const transcriptLogSource = "gateway/transcript"
|
||||
|
||||
// transcriptDocumentLog 返回标注来源的子日志器。转录重建是纯函数(编排器的
|
||||
// 播种与转向两条路径都只拿得到 history.Message,没有 logger 在作用域内),
|
||||
// 因此以进程默认 logger 为基底再盖上来源标签,使坏数据的 WARN 既可见又可
|
||||
// 追溯(与 internal/auth 的 apikey/jwt 同一手法)。它只用于「数据被写坏」
|
||||
// 这类冷路径,不在每回合的正常路径上打任何日志。
|
||||
func transcriptDocumentLog() *slog.Logger {
|
||||
return logger.WithSource(slog.Default(), transcriptLogSource)
|
||||
}
|
||||
Reference in new issue
Block a user