fix(channel): resolve attachment filename and prevent duplicate sends

- Derive attachment name from path basename when not explicitly set in
  parseAttachmentDelta, fixing the "file.bin" fallback on Telegram.
- Infer correct AttachmentType (image/audio/video) from MIME in
  applyAssetToAttachment instead of keeping the generic "file" type.
- Remove outboundAttachments re-attachment to final messages since
  attachments are already delivered during streaming via
  StreamEventAttachment, preventing duplicate file sends on platforms.
This commit is contained in:
Acbox
2026-03-11 17:00:07 +08:00
parent 30653fbdbf
commit 2debfb496c
+24 -21
View File
@@ -395,7 +395,6 @@ func (p *ChannelInboundProcessor) HandleInbound(ctx context.Context, cfg channel
var (
finalMessages []conversation.ModelMessage
outboundAttachments []channel.Attachment
streamErr error
)
for chunkCh != nil || streamErrCh != nil {
@@ -422,7 +421,6 @@ func (p *ChannelInboundProcessor) HandleInbound(ctx context.Context, cfg channel
if event.Type == channel.StreamEventAttachment && len(event.Attachments) > 0 {
ingested := p.ingestOutboundAttachments(ctx, strings.TrimSpace(identity.BotID), event.Attachments)
events[i].Attachments = ingested
outboundAttachments = append(outboundAttachments, ingested...)
assetMu.Lock()
for _, att := range ingested {
contentHash := strings.TrimSpace(att.ContentHash)
@@ -506,10 +504,9 @@ func (p *ChannelInboundProcessor) HandleInbound(ctx context.Context, cfg channel
}
outputs := flow.ExtractAssistantOutputs(finalMessages)
attachmentsApplied := false
for _, output := range outputs {
outMessage := buildChannelMessage(output, desc.Capabilities)
if outMessage.IsEmpty() && (len(outboundAttachments) == 0 || attachmentsApplied) {
if outMessage.IsEmpty() {
continue
}
plainText := strings.TrimSpace(outMessage.PlainText())
@@ -519,10 +516,6 @@ func (p *ChannelInboundProcessor) HandleInbound(ctx context.Context, cfg channel
if isMessagingToolDuplicate(plainText, sentTexts) {
continue
}
if !attachmentsApplied && len(outboundAttachments) > 0 {
outMessage.Attachments = append(outMessage.Attachments, outboundAttachments...)
attachmentsApplied = true
}
if outMessage.Reply == nil && sourceMessageID != "" {
outMessage.Reply = &channel.ReplyRef{
Target: target,
@@ -538,18 +531,6 @@ func (p *ChannelInboundProcessor) HandleInbound(ctx context.Context, cfg channel
return err
}
}
if !attachmentsApplied && len(outboundAttachments) > 0 {
attachMsg := channel.Message{Attachments: outboundAttachments}
if sourceMessageID != "" {
attachMsg.Reply = &channel.ReplyRef{Target: target, MessageID: sourceMessageID}
}
if err := stream.Push(ctx, channel.StreamEvent{
Type: channel.StreamEventFinal,
Final: &channel.StreamFinalizePayload{Message: attachMsg},
}); err != nil {
return err
}
}
if err := stream.Push(ctx, channel.StreamEvent{
Type: channel.StreamEventStatus,
Status: channel.StreamStatusCompleted,
@@ -1750,6 +1731,24 @@ func applyAssetToAttachment(asset media.Asset, botID string, item *channel.Attac
if item.Size == 0 && asset.SizeBytes > 0 {
item.Size = asset.SizeBytes
}
// Infer a better attachment type from MIME when the TS side sent a generic "file".
if item.Type == channel.AttachmentFile || item.Type == "" {
item.Type = inferAttachmentTypeFromMime(strings.TrimSpace(item.Mime))
}
}
func inferAttachmentTypeFromMime(mime string) channel.AttachmentType {
mime = strings.ToLower(strings.TrimSpace(mime))
switch {
case strings.HasPrefix(mime, "image/"):
return channel.AttachmentImage
case strings.HasPrefix(mime, "audio/"):
return channel.AttachmentAudio
case strings.HasPrefix(mime, "video/"):
return channel.AttachmentVideo
default:
return channel.AttachmentFile
}
}
// extractStorageKey derives the media storage key from a container-internal
@@ -1919,12 +1918,16 @@ func parseAttachmentDelta(raw json.RawMessage) []channel.Attachment {
if url == "" {
url = strings.TrimSpace(item.Path)
}
name := strings.TrimSpace(item.Name)
if name == "" && url != "" && !isDataURL(url) {
name = filepath.Base(url)
}
attachments = append(attachments, channel.Attachment{
Type: channel.AttachmentType(strings.TrimSpace(item.Type)),
URL: url,
PlatformKey: strings.TrimSpace(item.PlatformKey),
ContentHash: strings.TrimSpace(item.ContentHash),
Name: strings.TrimSpace(item.Name),
Name: name,
Mime: strings.TrimSpace(item.Mime),
Size: item.Size,
})