import { message } from 'ant-design-vue'; import { fetchAttachmentBlob, getSessionAttachments } from '/@/views/ventAI/manageAssistent/api'; import type { SessionAttachment } from '/@/views/ventAI/manageAssistent/api'; import { isCsvFile, isDwgFile, isDxfFile, isExcelFile, isTextFile, isWordFile, getPreviewMimeType } from '/@/views/ventAI/manageAssistent/lib'; import type { AttachedFile, Message, Task } from '/@/views/ventAI/manageAssistent/types/index'; /** * 会话附件历史:加载后按 message_id 挂回用户消息,无法匹配的进入兜底列表; * 附件内容按需下载(预览时才取二进制),下载/预览都带令牌走附件下载接口。 */ const toAttachedFile = (item: SessionAttachment): AttachedFile => ({ id: item.file_id, fileId: item.file_id, messageId: item.message_id, name: item.filename, size: item.size_bytes, type: item.content_type || '', uploadTime: item.created_at, createdAt: item.created_at, downloadUrl: item.download_url, }); const fileKey = (file: AttachedFile): string => file.fileId || file.id; const mergeAttachedFiles = (existing: AttachedFile[], incoming: AttachedFile[]): AttachedFile[] => { const map = new Map(); for (const file of [...existing, ...incoming]) { map.set(fileKey(file), file); } return Array.from(map.values()); }; const saveBlob = (blob: Blob, filename: string) => { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename || '附件'; document.body.appendChild(a); a.click(); document.body.removeChild(a); // 让浏览器先拿到下载任务再释放对象 URL setTimeout(() => URL.revokeObjectURL(url), 0); }; const hasContent = (file: AttachedFile): boolean => file.content !== undefined || file.arrayBuffer !== undefined || file.preview !== undefined || !!file.originalFile; /** * 预览用的 blob 换成真实类型:附件下载接口统一回 application/octet-stream(还带 Content-Disposition: attachment), * 浏览器凭这个类型认不出内容——PDF 放进 iframe 会被当成下载而不是显示。 */ const withPreviewType = (file: AttachedFile, blob: Blob): Blob => { const type = getPreviewMimeType(file.name, file.type || blob.type); return type && type !== blob.type ? new Blob([blob], { type }) : blob; }; /** 历史附件没有原始内容,首次预览时按文件类型取回并缓存到附件对象上 */ const ensureAttachmentContent = async (file: AttachedFile): Promise => { if (hasContent(file)) return file; if (!file.fileId) throw new Error('附件缺少下载标识'); // dwg 历史附件不用把原始文件下载回来:预览面板会按 fileId 直接取后端已转换好的 svg //(见 DwgPreview 的 requestSvgText),下载几百 KB 只为了再上传转换一遍属于纯浪费。 if (isDwgFile(file.name)) return file; const blob = await fetchAttachmentBlob(file.fileId, file.downloadUrl); if (isTextFile(file.name) || isCsvFile(file.name) || isDxfFile(file.name)) { file.content = await blob.text(); } else if (isWordFile(file.name) || isExcelFile(file.name)) { file.arrayBuffer = await blob.arrayBuffer(); } else { file.preview = URL.createObjectURL(withPreviewType(file, blob)); } file.originalFile = new File([blob], file.name, { type: file.type || blob.type || 'application/octet-stream', }); return file; }; export const useChatAttachments = () => { /** * 拉取会话附件并挂到历史消息上。 * - message_id 能匹配到用户消息:挂到该消息的 attachedFiles; * - message_id 为 null 或消息已合并/跳过:进入 task.orphanAttachments,由消息区底部兜底展示。 * 附件接口失败不阻断历史消息展示(旧环境可能还没有这个接口)。 */ const loadHistoryAttachments = async (task: Task, sessionId: string, messages: Message[]): Promise => { try { const res = await getSessionAttachments(sessionId); const attachments = (res?.attachments || []).map(toAttachedFile); if (!attachments.length) { task.orphanAttachments = []; return []; } const userMessageById = new Map(); for (const msg of messages) { if (msg.type === 'user' && msg.id != null) userMessageById.set(msg.id, msg); } const orphans: AttachedFile[] = []; for (const file of attachments) { const target = file.messageId != null ? userMessageById.get(file.messageId) : undefined; if (target) { if (!target.attachedFiles) target.attachedFiles = []; if (!target.attachedFiles.some((f) => fileKey(f) === fileKey(file))) { target.attachedFiles.push(file); } } else { orphans.push(file); } } task.orphanAttachments = orphans; task.attachedFiles = mergeAttachedFiles(task.attachedFiles, attachments); return attachments; } catch (e) { // 历史消息是主内容,附件是增强信息:失败只记录,不打断会话打开 console.error('加载会话附件失败:', e); return []; } }; const handleAttachmentPreview = async (file: AttachedFile, openPreview: (target: AttachedFile) => void) => { if (hasContent(file)) { openPreview(file); return; } if (!file.fileId) { message.warning('该附件暂不支持预览'); return; } const closeLoading = message.loading('正在加载附件…', 0); try { await ensureAttachmentContent(file); openPreview(file); } catch (e) { console.error('加载附件失败:', e); message.error((e as Error)?.message || '附件加载失败'); } finally { closeLoading(); } }; const handleAttachmentDownload = async (file: AttachedFile) => { try { if (file.fileId) { const closeLoading = message.loading('正在下载附件…', 0); try { const blob = await fetchAttachmentBlob(file.fileId, file.downloadUrl); saveBlob(blob, file.name); } finally { closeLoading(); } return; } if (file.originalFile) { saveBlob(file.originalFile, file.name); return; } if (file.preview?.startsWith('blob:') || file.preview?.startsWith('data:')) { const blob = await (await fetch(file.preview)).blob(); saveBlob(blob, file.name); return; } message.warning('该附件暂不支持下载'); } catch (e) { console.error('下载附件失败:', e); message.error((e as Error)?.message || '附件下载失败'); } }; return { loadHistoryAttachments, handleAttachmentPreview, handleAttachmentDownload }; };