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
This commit is contained in:
時壹
2026-04-15 16:52:14 +09:00
committed by GitHub
parent cb91dfb6f7
commit 7c39abc6b5

View File

@@ -86,6 +86,7 @@ export function useMessages(options: UseMessagesOptions) {
const messagesBySession = reactive<Record<string, ChatRecord[]>>({});
const loadedSessions = reactive<Record<string, boolean>>({});
const activeConnections = reactive<Record<string, ActiveConnection>>({});
const attachmentBlobCache = new Map<string, Promise<string>>();
const sessionProjects = reactive<Record<string, ChatSessionProject | null>>(
{},
);
@@ -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<void> {
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<void>[] = [];
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);
}
}
}