mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
feat: add file dedup and signature check from master attachment handling
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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<string>("");
|
||||
const stagedFiles = ref<StagedFileInfo[]>([]);
|
||||
const mediaCache = ref<Record<string, string>>({});
|
||||
const pendingFileSignatures = new Set<string>();
|
||||
|
||||
async function getFileSignature(file: File): Promise<string> {
|
||||
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<string> {
|
||||
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 = [];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user