package channelbridge // 本文件承载入站内容的**共享制品与挂载点**:文档路(inbound_documents.go)与图片路 // (inbound_images.go)各产出一份 `inboundContent`(要挂到用户回合上的部件 + 「读不到」的 // 诚实提示行),由 attachInboundContent 逐组挂到**同一条 RoleUser 消息**上。 // // 为什么两条路共用一个制品类型而不是各写一份挂载循环:两者的产物形状逐字相同 // (部件 + 提示行),差别只在部件怎么来(文档:落库引用;图片:data: URI)与落哪一列 // (`document` / `image`)。写第二份循环只会让「挂在哪条消息上」「空文本部件绝不塞」 // 这类口径开始漂移 —— 而用户内容只进用户角色消息是项目红线,这份逻辑只该有一处。 import ( "strings" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" ) // inboundContent 是一次入站内容摄取/内联的产物。 type inboundContent struct { // parts 是要挂到 RoleUser 消息上的内容部件(文档部件,或 data: URI 图片部件)。 parts []model.Content // notices 是「读不到」的诚实提示行,顺序与消息里的媒体顺序一致。 notices []string } // noticeText 把提示行拼成一段文本;没有提示时返回空串(调用方据此不塞空文本部件)。 func (c inboundContent) noticeText() string { return strings.Join(c.notices, "\n") } // attachInboundContent 把入站内容制品挂到消息列表里的**用户回合**上(design/24 §3/§4.4): // 部件与「读不到」的诚实提示行都只进 `RoleUser`,且挂在那条已构建好的用户消息上 —— // 来源系统行、第三方边界、SenderName 等既有装配因此逐字不变(本切片不重排消息,只追加 // 部件)。 // // groups 按给出顺序逐组追加(文档路先、图片路后),空组是空操作 —— 两条路互不影响: // 同一条消息既带文档又带图片时,两条都挂得上,各自只贡献自己的部件与自己的提示行。 // 一个回合只有一条用户消息(buildLoopMessages 的既有形状),因此挂载点只有一处。 // 文本部件为空且既有文本也为空时**不塞空文本部件**(provider 会拒):部件本身或提示行 // 就是这条消息的内容。 func attachInboundContent(msgs []model.Message, groups ...inboundContent) { idx := userTurnIndex(msgs) if idx < 0 { return } for _, g := range groups { if len(g.parts) == 0 && len(g.notices) == 0 { continue } msgs[idx].Content = append(msgs[idx].Content, g.parts...) if notice := g.noticeText(); notice != "" { msgs[idx].Content = append(msgs[idx].Content, model.Content{Type: model.ContentTypeText, Text: notice}) } } } // userTurnIndex 返回消息列表里第一条 RoleUser 消息的下标(没有则 -1)。 func userTurnIndex(msgs []model.Message) int { for i := range msgs { if msgs[i].Role == model.RoleUser { return i } } return -1 }