fix(slack): correct thread timestamp and improve file upload handling (#447)

Root cause: uploadJobResultFiles was passing msgTs (placeholder reply timestamp)
as thread_ts for file uploads. Slack's API rejects this - it requires the
parent thread timestamp, not a reply's timestamp.

Changes:
1. Use 'ts' (thread root timestamp) instead of 'msgTs' in replyToUpdateMessage
2. Fix type handling in attachmentsFromMetadataOnly to handle []interface{}
3. Download and upload generated images (e.g., DALL-E) as files instead of
   just adding link attachments, to preserve temporary URLs
4. Remove dead code: generateAttachmentsFromJobResponse was never called

This matches Telegram connector behavior where files are properly uploaded
rather than just referenced by URL.

Co-authored-by: localai-bot <localai-bot@noreply.github.com>
This commit is contained in:
LocalAI [bot]
2026-03-08 15:59:37 +01:00
committed by GitHub
parent 5a27c471ca
commit 34db83dc32
+50 -27
View File
@@ -4,7 +4,9 @@ import (
"bytes"
"encoding/base64"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
@@ -176,25 +178,21 @@ func attachmentsFromMetadataOnly(metadata map[string]interface{}) (attachments [
return nil
}
if urls, exists := metadata[actions.MetadataUrls]; exists {
if sl, ok := urls.([]string); ok {
for _, url := range xstrings.UniqueSlice(sl) {
attachments = append(attachments, slack.Attachment{
Title: "URL",
TitleLink: url,
Text: url,
})
}
for _, url := range xstrings.UniqueSlice(stringSliceFromMetadata(urls)) {
attachments = append(attachments, slack.Attachment{
Title: "URL",
TitleLink: url,
Text: url,
})
}
}
if imagesUrls, exists := metadata[actions.MetadataImages]; exists {
if sl, ok := imagesUrls.([]string); ok {
for _, url := range xstrings.UniqueSlice(sl) {
attachments = append(attachments, slack.Attachment{
Title: "Image",
TitleLink: url,
ImageURL: url,
})
}
for _, url := range xstrings.UniqueSlice(stringSliceFromMetadata(imagesUrls)) {
attachments = append(attachments, slack.Attachment{
Title: "Image",
TitleLink: url,
ImageURL: url,
})
}
}
return attachments
@@ -295,6 +293,39 @@ func uploadFilesFromMetadata(metadata map[string]interface{}, api *slack.Client,
}
}
}
// Handle generated images (download from URL and upload as file, so temporary URLs like DALL-E are preserved)
if imageUrls, exists := metadata[actions.MetadataImages]; exists {
sl := stringSliceFromMetadata(imageUrls)
for _, imgURL := range xstrings.UniqueSlice(sl) {
resp, err := http.Get(imgURL)
if err != nil {
xlog.Error("Error downloading image for Slack upload", "url", imgURL, "error", err)
continue
}
data, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
xlog.Error("Error reading image body for Slack upload", "url", imgURL, "error", err)
continue
}
if len(data) == 0 {
xlog.Error("Empty image body for Slack upload", "url", imgURL)
continue
}
_, err = api.UploadFileV2(slack.UploadFileV2Parameters{
Reader: bytes.NewReader(data),
FileSize: len(data),
ThreadTimestamp: threadTs,
Channel: channelID,
Filename: "image.png",
Title: "Generated image",
InitialComment: "Generated image",
})
if err != nil {
xlog.Error("Slack UploadFileV2 failed for image", "error", err, "url", imgURL)
}
}
}
}
// attachmentsAndUploadsFromMetadata returns link/image attachments and uploads files (songs, PDFs)
@@ -329,16 +360,6 @@ func uploadJobResultFiles(res *types.JobResult, api *slack.Client, channelID, th
}
}
func generateAttachmentsFromJobResponse(j *types.JobResult, api *slack.Client, channelID, ts string) (attachments []slack.Attachment) {
if j == nil {
return nil
}
for _, state := range j.State {
attachments = append(attachments, attachmentsAndUploadsFromMetadata(state.Metadata, api, channelID, ts)...)
}
return attachments
}
// ImageData represents a single image with its metadata
type ImageData struct {
Data []byte
@@ -641,7 +662,9 @@ func replyWithPostMessage(finalResponse string, api *slack.Client, ev *slackeven
func replyToUpdateMessage(finalResponse string, api *slack.Client, ev *slackevents.AppMentionEvent, msgTs string, ts string, postMessageParams slack.PostMessageParameters, res *types.JobResult) {
attachments := attachmentsFromJobResponseOnly(res)
uploadJobResultFiles(res, api, ev.Channel, msgTs)
// Use the thread root timestamp (ts), not the placeholder reply timestamp (msgTs).
// Slack API: "Never use a reply's ts value; use its parent instead."
uploadJobResultFiles(res, api, ev.Channel, ts)
if len(finalResponse) > 3000 {
messages := xstrings.SplitParagraph(finalResponse, 3000)