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
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/bus"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||
)
|
||||
@@ -55,18 +56,28 @@ func (h *hub) streamChat(ctx context.Context, payload any) SubmitResult {
|
||||
if b, err := json.Marshal(payload); err == nil {
|
||||
_ = json.Unmarshal(b, &req)
|
||||
}
|
||||
if h.runner == nil || (strings.TrimSpace(req.Text) == "" && strings.TrimSpace(req.Image) == "") {
|
||||
emptyTurn := strings.TrimSpace(req.Text) == "" && strings.TrimSpace(req.Image) == "" && len(req.Documents) == 0
|
||||
if h.runner == nil || emptyTurn {
|
||||
h.broadcast(wsEvent{Type: "stream_end", TurnID: newTurnID(), Seq: int(h.streamSeq.Add(1)), SessionID: req.SessionID, Data: map[string]any{"stopReason": "idle"}})
|
||||
return SubmitResult{}
|
||||
}
|
||||
if req.SessionID == "" {
|
||||
req.SessionID = "web"
|
||||
}
|
||||
// 文档引用在本进程内解析(只认服务端存储名 + 确实存在),失败**可见**:
|
||||
// 回一条机器可读的 error 帧并中止本回合,绝不静默丢掉附件(design/24 §4.4)。
|
||||
docs, derr := h.resolveDocuments(req.Documents)
|
||||
if derr != nil {
|
||||
logger.WithSource(h.log, documentsLogSource).Warn("chat documents rejected",
|
||||
"session", req.SessionID, "count", len(req.Documents), "err", derr)
|
||||
h.broadcast(wsEvent{Type: "error", SessionID: req.SessionID, Data: map[string]any{"code": chatDocumentErrorCode(derr)}})
|
||||
return SubmitResult{}
|
||||
}
|
||||
// Web 主会话是 owner 的私有对话(JWT = owner),因此它绑定全权限
|
||||
// env:记忆读写,行为不变。角色来自会话的绑定(design/22 §4.5):
|
||||
// 切到角色 Y 的会话在下一轮以 Y 应答,而未绑定的/既有的会话继续
|
||||
// 解析为全局默认角色。
|
||||
msgs := userChatMessages(req.Text, req.Image)
|
||||
msgs := chatTurnMessages(req.Text, req.Image, docs)
|
||||
// 通过 ctx 把客户端的 deviceID、行为模式与可选幂等键串进
|
||||
// orchestrator,使持久化的用户轮次与助手回复都带上标记,并让重复到达的
|
||||
// clientMsgId 被判重(design/16 §4.5、§5.1;协同 G4)。空 clientMsgId 是
|
||||
@@ -206,6 +217,24 @@ type chatStreamRequest struct {
|
||||
// `chat_duplicate`(data 带既有 messageId)。它可选,省略时行为与加入该
|
||||
// 字段之前逐字相同。
|
||||
ClientMsgID string `json:"clientMsgId,omitempty"`
|
||||
// Documents 是本回合附带的文档引用(design/24 §6.1),与 REST 的
|
||||
// `chatRequest.documents` **同形**:只给服务端存储名,绝不接受 URL。
|
||||
// 上行帧仍有 4096 字节硬上限 ⇒ 文档实际上走 REST;这里保持同形解析,
|
||||
// 使两条入口不会分叉(解析见 resolveDocuments)。
|
||||
Documents []chatDocumentRef `json:"documents,omitempty"`
|
||||
}
|
||||
|
||||
// resolveDocuments 解析本回合携带的文档引用(design/24 §4.4/§6.1)。一条都不带
|
||||
// 是空操作;未接解析器(未接线的构建 / 单元测试构造的裸 hub)时**明确报错**,
|
||||
// 绝不把附件静默丢掉。
|
||||
func (h *hub) resolveDocuments(refs []chatDocumentRef) ([]model.Document, error) {
|
||||
if len(refs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if h.documents == nil {
|
||||
return nil, documentsUnavailableError()
|
||||
}
|
||||
return h.documents(refs)
|
||||
}
|
||||
|
||||
// newTurnID 返回新的随机十六进制标识,用于把一串流式 WS 事件关联到
|
||||
|
||||
Reference in new issue
Block a user