From 2c3bceb2e6ddcb123a9a92c693da68eff62e29a4 Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Wed, 29 Apr 2026 03:00:13 +0800 Subject: [PATCH] feat: add file dedup and signature check from master attachment handling --- .../src/components/shared/StyledMenu.vue | 8 ++ dashboard/src/composables/useMediaHandling.ts | 79 +++++++++++-------- 2 files changed, 55 insertions(+), 32 deletions(-) diff --git a/dashboard/src/components/shared/StyledMenu.vue b/dashboard/src/components/shared/StyledMenu.vue index 8807afd33..0d9663e41 100644 --- a/dashboard/src/components/shared/StyledMenu.vue +++ b/dashboard/src/components/shared/StyledMenu.vue @@ -36,6 +36,10 @@ withDefaults( backdrop-filter: blur(10px); } +.styled-menu-card-borderless { + border: 0 !important; +} + .styled-menu-list { background: transparent !important; } @@ -65,6 +69,10 @@ withDefaults( border: 1px solid rgba(var(--v-theme-primary), 0.2) !important; } +.v-theme--PurpleThemeDark .styled-menu-card-borderless { + border: 0 !important; +} + /* 深色模式下的列表项悬停效果 */ .v-theme--PurpleThemeDark .styled-menu-item:hover { background: rgba(var(--v-theme-primary), 0.12) !important; diff --git a/dashboard/src/composables/useMediaHandling.ts b/dashboard/src/composables/useMediaHandling.ts index 4043274d2..8b8818bf8 100644 --- a/dashboard/src/composables/useMediaHandling.ts +++ b/dashboard/src/composables/useMediaHandling.ts @@ -7,12 +7,33 @@ export interface StagedFileInfo { original_name: string; url: string; // blob URL for preview type: string; // image, record, file, video + signature?: string; } export function useMediaHandling() { const stagedAudioUrl = ref(""); const stagedFiles = ref([]); const mediaCache = ref>({}); + const pendingFileSignatures = new Set(); + + async function getFileSignature(file: File): Promise { + if (crypto?.subtle) { + const buffer = await file.arrayBuffer(); + const digest = await crypto.subtle.digest("SHA-256", buffer); + const hash = Array.from(new Uint8Array(digest)) + .map((byte) => byte.toString(16).padStart(2, "0")) + .join(""); + return `sha256:${hash}`; + } + return `meta:${file.name}:${file.size}:${file.type}:${file.lastModified}`; + } + + function isDuplicateFile(signature: string) { + return ( + pendingFileSignatures.has(signature) || + stagedFiles.value.some((file) => file.signature === signature) + ); + } async function getMediaFile(filename: string): Promise { if (mediaCache.value[filename]) { @@ -34,31 +55,11 @@ export function useMediaHandling() { } } - async function processAndUploadImage(file: File) { - const formData = new FormData(); - formData.append("file", file); - - try { - const response = await axios.post("/api/chat/post_file", formData, { - headers: { - "Content-Type": "multipart/form-data", - }, - }); - - const { attachment_id, filename, type } = response.data.data; - stagedFiles.value.push({ - attachment_id, - filename, - original_name: file.name, - url: URL.createObjectURL(file), - type, - }); - } catch (err) { - console.error("Error uploading image:", err); - } - } - - async function processAndUploadFile(file: File) { + async function uploadStagedFile(file: File) { + const signature = await getFileSignature(file); + if (isDuplicateFile(signature)) return; + + pendingFileSignatures.add(signature); const formData = new FormData(); formData.append("file", file); @@ -76,12 +77,23 @@ export function useMediaHandling() { original_name: file.name, url: URL.createObjectURL(file), type, + signature, }); } catch (err) { console.error("Error uploading file:", err); + } finally { + pendingFileSignatures.delete(signature); } } + async function processAndUploadImage(file: File) { + await uploadStagedFile(file); + } + + async function processAndUploadFile(file: File) { + await uploadStagedFile(file); + } + async function handlePaste(event: ClipboardEvent) { const items = event.clipboardData?.items; if (!items) return; @@ -136,14 +148,17 @@ export function useMediaHandling() { } } - function clearStaged() { + function clearStaged(options: { revokeUrls?: boolean } = {}) { + const { revokeUrls = true } = options; stagedAudioUrl.value = ""; - // 清理文件的 blob URLs - stagedFiles.value.forEach((file) => { - if (file.url.startsWith("blob:")) { - URL.revokeObjectURL(file.url); - } - }); + if (revokeUrls) { + // 清理文件的 blob URLs + stagedFiles.value.forEach((file) => { + if (file.url.startsWith("blob:")) { + URL.revokeObjectURL(file.url); + } + }); + } stagedFiles.value = []; }