From 7c39abc6b5ce0e61c10ed2d7c41a4079d771d6cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=82=E5=A3=B9?= <137363396+KBVsent@users.noreply.github.com> Date: Wed, 15 Apr 2026 16:52:14 +0900 Subject: [PATCH] fix(dashboard): resolve chat attachment 401 (#7569) * fix(dashboard): resolve chat attachment 401 by restoring axios blob URL fetch * chore: cache blob URL promises to prevent duplicate requests and memory leaks --- dashboard/src/composables/useMessages.ts | 59 +++++++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/dashboard/src/composables/useMessages.ts b/dashboard/src/composables/useMessages.ts index af6228e06..78b6c0e51 100644 --- a/dashboard/src/composables/useMessages.ts +++ b/dashboard/src/composables/useMessages.ts @@ -86,6 +86,7 @@ export function useMessages(options: UseMessagesOptions) { const messagesBySession = reactive>({}); const loadedSessions = reactive>({}); const activeConnections = reactive>({}); + const attachmentBlobCache = new Map>(); const sessionProjects = reactive>( {}, ); @@ -98,6 +99,10 @@ export function useMessages(options: UseMessagesOptions) { onBeforeUnmount(() => { cleanupConnections(); + for (const promise of attachmentBlobCache.values()) { + promise.then((url) => URL.revokeObjectURL(url)).catch(() => {}); + } + attachmentBlobCache.clear(); }); function isSessionRunning(sessionId: string) { @@ -129,6 +134,47 @@ export function useMessages(options: UseMessagesOptions) { return !isUserMessage(msg) && msgIndex === activeMessages.value.length - 1; } + async function resolvePartMedia(part: MessagePart): Promise { + if (part.embedded_url) return; + let url: string; + let cacheKey: string; + if (part.attachment_id) { + cacheKey = `att:${part.attachment_id}`; + url = `/api/chat/get_attachment?attachment_id=${encodeURIComponent(part.attachment_id)}`; + } else if (part.filename) { + cacheKey = `file:${part.filename}`; + url = `/api/chat/get_file?filename=${encodeURIComponent(part.filename)}`; + } else { + return; + } + let promise = attachmentBlobCache.get(cacheKey); + if (!promise) { + promise = axios + .get(url, { responseType: "blob" }) + .then((resp) => URL.createObjectURL(resp.data)); + attachmentBlobCache.set(cacheKey, promise); + } + try { + part.embedded_url = await promise; + } catch (e) { + attachmentBlobCache.delete(cacheKey); + console.error("Failed to resolve media:", cacheKey, e); + } + } + + async function resolveRecordMedia(records: ChatRecord[]) { + const mediaTypes = ["image", "record", "video"]; + const tasks: Promise[] = []; + for (const record of records) { + for (const part of record.content?.message || []) { + if (mediaTypes.includes(part.type) && !part.embedded_url && (part.attachment_id || part.filename)) { + tasks.push(resolvePartMedia(part)); + } + } + } + await Promise.all(tasks); + } + async function loadSessionMessages(sessionId: string) { if (!sessionId) return; loadingMessages.value = true; @@ -138,7 +184,9 @@ export function useMessages(options: UseMessagesOptions) { }); const payload = response.data?.data || {}; const history = payload.history || []; - messagesBySession[sessionId] = history.map(normalizeHistoryRecord); + const records = history.map(normalizeHistoryRecord); + await resolveRecordMedia(records); + messagesBySession[sessionId] = records; sessionProjects[sessionId] = normalizeSessionProject(payload.project); loadedSessions[sessionId] = true; } catch (error) { @@ -438,7 +486,14 @@ export function useMessages(options: UseMessagesOptions) { .replace("[FILE]", "") .replace("[VIDEO]", "") .split("|", 1)[0]; - messageContent(botRecord).message.push({ type: msgType, filename }); + const mediaPart: MessagePart = { type: msgType, filename }; + if (msgType !== "file") { + resolvePartMedia(mediaPart).then(() => { + messageContent(botRecord).message.push(mediaPart); + }); + } else { + messageContent(botRecord).message.push(mediaPart); + } } }