feat(channels): IM 入站图片接线(纯图片不再被丢 / data URI 进模型视图 / 诚实提示 / 来源白名单)
This commit is contained in:
1 parent
0cbc9ff886
commit
b73a1276c6
18 files changed
+1224
-121
No files matched your search
@@ -140,9 +140,10 @@ func (b *Bridge) processFrom(ctx context.Context, msg adapter.UnifiedMessage, a
|
||||
b.normalizeInboundVoice(ctx, &msg)
|
||||
|
||||
text := strings.TrimSpace(msg.Content)
|
||||
// 无文本且无**任何可用媒体** ⇒ 没有可处理的内容。带附件的「纯文件消息」与带语音的
|
||||
// 「纯语音消息」必须继续往下走:附件/语音就是它的内容(design/24 §4.4 的
|
||||
// 「不许静默丢」;语音那一路的归一已保证它要么有文本、要么有诚实提示)。
|
||||
// 无文本且无**任何可用媒体** ⇒ 没有可处理的内容。带附件的「纯文件消息」、带语音的
|
||||
// 「纯语音消息」与带图片的「纯图片消息」(微信/QQ 发图常常没有配文)必须继续往下走:
|
||||
// 附件/语音/图片就是它的内容(design/24 §4.4 的「不许静默丢」;语音那一路的归一已
|
||||
// 保证它要么有文本、要么有诚实提示,图片那一路的失败也留一行中文诚实提示)。
|
||||
if text == "" && !hasUsableMedia(msg) {
|
||||
b.log.Debug("inbound message has no chat text, skipped",
|
||||
"platform", msg.Platform, "channel", msg.ChannelID)
|
||||
@@ -230,9 +231,10 @@ func inboundTargetFor(sess session.Session, platform string) inboundTarget {
|
||||
|
||||
// runInboundTurn 为被放行的消息驱动 agent 轮次:解析
|
||||
// 路由目标、把消息携带的文件类附件**落库并挂成文档部件**
|
||||
// (design/24 §3;切片 M1)、构建模型可见的消息列表、在轮次之前持久化入站
|
||||
// 用户消息**连同它们的 `document` 引用列**(因此即使渠道回复失败它也已落地,
|
||||
// 客户端也有附件芯片可点),并在轮次之后把抽取结论补回同一列,运行
|
||||
// (design/24 §3;切片 M1)、把图片媒体内联成 data: URI 部件挂上既有视觉链路
|
||||
// (切片 IM-IMG;design/24 §4.2 末)、构建模型可见的消息列表、在轮次之前持久化入站
|
||||
// 用户消息**连同它们的 `document` 引用列与 `image` 列**(因此即使渠道回复失败它也已落地,
|
||||
// 客户端也有附件芯片与图片可看),并在轮次之后把抽取结论补回同一列,运行
|
||||
// 循环并把结果交给 deliverReply。
|
||||
func (b *Bridge) runInboundTurn(ctx context.Context, msg adapter.UnifiedMessage, a adapter.PlatformAdapter,
|
||||
text string, actor adapter.Actor, sess session.Session) {
|
||||
@@ -242,6 +244,9 @@ func (b *Bridge) runInboundTurn(ctx context.Context, msg adapter.UnifiedMessage,
|
||||
// 最小打扰拦下的消息绝不在盘上留下文件。成功者挂成文档部件,装配期
|
||||
// resolveDocuments 照既有链路抽取/注入;读不到的走诚实提示。
|
||||
attachments := b.ingestInboundDocuments(ctx, msg)
|
||||
// 图片内联与文档摄取**并列**、互不影响:文档走存储与抽取,图片走 data: URI 部件
|
||||
// (同一条用户消息上两条都能挂)。它没有 ctx —— 这里没有任何 I/O(字节由适配器取回)。
|
||||
images := b.ingestInboundImages(msg)
|
||||
|
||||
// 第三方边界只在受限(群聊/陌生人)上下文中生效:
|
||||
// 记忆只读(MemWritable=false),并携带发送者名,
|
||||
@@ -257,23 +262,25 @@ func (b *Bridge) runInboundTurn(ctx context.Context, msg adapter.UnifiedMessage,
|
||||
runEnv.PersonaID = b.resolvePersona(ctx, sess, target, adapterName(a))
|
||||
senderName := senderDisplayName(msg, actor)
|
||||
loopMsgs := b.buildLoopMessages(ctx, text, runEnv, actor, senderName, msg)
|
||||
attachInboundAttachments(loopMsgs, attachments)
|
||||
attachInboundContent(loopMsgs, attachments, images)
|
||||
|
||||
// 附件引用必须同批落进用户消息的 `document` 列(design/24 §2.4):只把部件挂在
|
||||
// 内存里的模型可见消息上,客户端读到的那条消息就是一片空白 —— 附件芯片只认库
|
||||
// 里的那一列。这里写的是引用与身份,结论要等回合跑完(下一段)。
|
||||
// 附件引用与图片必须同批落进用户消息各自的列(design/24 §2.4 / design/16 §4.4):
|
||||
// 只把部件挂在内存里的模型可见消息上,客户端读到的那条消息就是一片空白 —— 附件芯片
|
||||
// 只认 `document` 列,图片只认 `image` 列(**图片绝不进 `document` 列**)。这里写的是
|
||||
// 引用与身份,文档结论要等回合跑完(下一段)。
|
||||
document := b.inboundDocumentColumn(msg, attachments)
|
||||
image := inboundImageColumn(images)
|
||||
|
||||
// 主人私聊消息落在 Web 主会话并广播给 Web 客户端;
|
||||
// 受限消息落在其隔离会话(适配器日志页),
|
||||
// 以 `channel_persisted` 事件广播(绝不进入
|
||||
// 主/`web` 会话或 Web 聊天)。
|
||||
// 无论有没有文本都要落库:附件就是纯文件消息的内容(design/24 §4.4)。
|
||||
// 无论有没有文本都要落库:附件/图片就是纯媒体消息的内容(design/24 §4.4)。
|
||||
var userMsg history.Message
|
||||
if target.private {
|
||||
userMsg = b.persistToMain(ctx, history.RoleUser, text, "", document, target.platform, deviceIDFor(msg), adapterName(a))
|
||||
userMsg = b.persistToMain(ctx, history.RoleUser, text, "", document, image, target.platform, deviceIDFor(msg), adapterName(a))
|
||||
} else {
|
||||
userMsg = b.persistIsolated(ctx, history.RoleUser, text, document, deviceIDFor(msg), sess, msg.Platform, senderName, adapterName(a))
|
||||
userMsg = b.persistIsolated(ctx, history.RoleUser, text, document, image, deviceIDFor(msg), sess, msg.Platform, senderName, adapterName(a))
|
||||
}
|
||||
|
||||
res, err := b.run.Run(loop.WithSessionEnv(ctx, runEnv), target.sessionID, loopMsgs)
|
||||
@@ -315,9 +322,9 @@ func (b *Bridge) deliverReply(ctx context.Context, msg adapter.UnifiedMessage, a
|
||||
// 广播;受限 → 隔离日志 + channel_persisted 广播),随后
|
||||
// 送回渠道。
|
||||
if target.private {
|
||||
b.persistToMain(ctx, history.RoleAssistant, reply, mood, "", target.platform, deviceIDFor(msg), adapterName(a))
|
||||
b.persistToMain(ctx, history.RoleAssistant, reply, mood, "", "", target.platform, deviceIDFor(msg), adapterName(a))
|
||||
} else {
|
||||
b.persistIsolated(ctx, history.RoleAssistant, reply, "", "", sess, msg.Platform, "", adapterName(a))
|
||||
b.persistIsolated(ctx, history.RoleAssistant, reply, "", "", "", sess, msg.Platform, "", adapterName(a))
|
||||
}
|
||||
if err := b.sendReply(ctx, msg, a, reply); err != nil {
|
||||
if errors.Is(err, adapter.ErrUnsupported) {
|
||||
@@ -346,30 +353,6 @@ func deviceIDFor(msg adapter.UnifiedMessage) string {
|
||||
return msg.Platform + ":" + msg.ChannelID
|
||||
}
|
||||
|
||||
// attachInboundAttachments 把入站附件摄取的结果挂到消息列表里的**用户回合**上
|
||||
// (design/24 §3/§4.4;切片 M1):文档部件与「读不到」的诚实提示行都只进
|
||||
// `RoleUser`,且挂在那条已构建好的用户消息上 —— 来源系统行、第三方边界、
|
||||
// SenderName 等既有装配因此逐字不变(本切片不重排消息,只追加部件)。
|
||||
//
|
||||
// 一个回合只有一条用户消息(buildLoopMessages 的既有形状),因此找到第一条即
|
||||
// 挂载并返回;没有附件产物时是空操作。文本部件为空且既有文本为空时**不塞空文本
|
||||
// 部件**(provider 会拒):附件部件本身就是这条消息的内容。
|
||||
func attachInboundAttachments(msgs []model.Message, attach inboundAttachments) {
|
||||
if len(attach.parts) == 0 && len(attach.notices) == 0 {
|
||||
return
|
||||
}
|
||||
for i := range msgs {
|
||||
if msgs[i].Role != model.RoleUser {
|
||||
continue
|
||||
}
|
||||
msgs[i].Content = append(msgs[i].Content, attach.parts...)
|
||||
if notice := attach.noticeText(); notice != "" {
|
||||
msgs[i].Content = append(msgs[i].Content, model.Content{Type: model.ContentTypeText, Text: notice})
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// buildLoopMessages 为一次入站轮次构建模型可见的消息列表。它
|
||||
// 用 "[来源: <platform> · <conversation>]" 前缀装饰用户消息,使
|
||||
// 模型能看到消息来自哪个平台、哪个会话(design/16
|
||||
@@ -382,9 +365,9 @@ func attachInboundAttachments(msgs []model.Message, attach inboundAttachments) {
|
||||
// 保持不动(构建带前缀的副本),因此持久化内容与
|
||||
// 来源平台投递的内容一致。
|
||||
//
|
||||
// 入站附件摄取的产物(已落库的文档部件与「读不到」的诚实提示行)由
|
||||
// attachInboundAttachments 挂到**同一条 RoleUser 消息**上 —— 用户内容只进用户
|
||||
// 角色消息是项目红线。
|
||||
// 入站内容摄取的产物(已落库的文档部件、内联成 data: URI 的图片部件,以及两条路上
|
||||
// 「读不到」的诚实提示行)由 attachInboundContent(inbound_content.go)挂到**同一条
|
||||
// RoleUser 消息**上 —— 用户内容只进用户角色消息是项目红线。
|
||||
func (b *Bridge) buildLoopMessages(_ context.Context, text string, env loop.SessionEnv, actor adapter.Actor, senderName string, src adapter.UnifiedMessage) []model.Message {
|
||||
msg := userTurnMessage(text)
|
||||
thirdParty := env.Restricted() && !actor.IsOwner() && senderName != ""
|
||||
|
||||
Reference in new issue
Block a user