useChatAttachments.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import { message } from 'ant-design-vue';
  2. import { fetchAttachmentBlob, getSessionAttachments } from '/@/views/ventAI/manageAssistent/api';
  3. import type { SessionAttachment } from '/@/views/ventAI/manageAssistent/api';
  4. import { isCsvFile, isDwgFile, isDxfFile, isExcelFile, isTextFile, isWordFile, getPreviewMimeType } from '/@/views/ventAI/manageAssistent/lib';
  5. import type { AttachedFile, Message, Task } from '/@/views/ventAI/manageAssistent/types/index';
  6. /**
  7. * 会话附件历史:加载后按 message_id 挂回用户消息,无法匹配的进入兜底列表;
  8. * 附件内容按需下载(预览时才取二进制),下载/预览都带令牌走附件下载接口。
  9. */
  10. const toAttachedFile = (item: SessionAttachment): AttachedFile => ({
  11. id: item.file_id,
  12. fileId: item.file_id,
  13. messageId: item.message_id,
  14. name: item.filename,
  15. size: item.size_bytes,
  16. type: item.content_type || '',
  17. uploadTime: item.created_at,
  18. createdAt: item.created_at,
  19. downloadUrl: item.download_url,
  20. });
  21. const fileKey = (file: AttachedFile): string => file.fileId || file.id;
  22. const mergeAttachedFiles = (existing: AttachedFile[], incoming: AttachedFile[]): AttachedFile[] => {
  23. const map = new Map<string, AttachedFile>();
  24. for (const file of [...existing, ...incoming]) {
  25. map.set(fileKey(file), file);
  26. }
  27. return Array.from(map.values());
  28. };
  29. const saveBlob = (blob: Blob, filename: string) => {
  30. const url = URL.createObjectURL(blob);
  31. const a = document.createElement('a');
  32. a.href = url;
  33. a.download = filename || '附件';
  34. document.body.appendChild(a);
  35. a.click();
  36. document.body.removeChild(a);
  37. // 让浏览器先拿到下载任务再释放对象 URL
  38. setTimeout(() => URL.revokeObjectURL(url), 0);
  39. };
  40. const hasContent = (file: AttachedFile): boolean =>
  41. file.content !== undefined || file.arrayBuffer !== undefined || file.preview !== undefined || !!file.originalFile;
  42. /**
  43. * 预览用的 blob 换成真实类型:附件下载接口统一回 application/octet-stream(还带 Content-Disposition: attachment),
  44. * 浏览器凭这个类型认不出内容——PDF 放进 iframe 会被当成下载而不是显示。
  45. */
  46. const withPreviewType = (file: AttachedFile, blob: Blob): Blob => {
  47. const type = getPreviewMimeType(file.name, file.type || blob.type);
  48. return type && type !== blob.type ? new Blob([blob], { type }) : blob;
  49. };
  50. /** 历史附件没有原始内容,首次预览时按文件类型取回并缓存到附件对象上 */
  51. const ensureAttachmentContent = async (file: AttachedFile): Promise<AttachedFile> => {
  52. if (hasContent(file)) return file;
  53. if (!file.fileId) throw new Error('附件缺少下载标识');
  54. // dwg 历史附件不用把原始文件下载回来:预览面板会按 fileId 直接取后端已转换好的 svg
  55. //(见 DwgPreview 的 requestSvgText),下载几百 KB 只为了再上传转换一遍属于纯浪费。
  56. if (isDwgFile(file.name)) return file;
  57. const blob = await fetchAttachmentBlob(file.fileId, file.downloadUrl);
  58. if (isTextFile(file.name) || isCsvFile(file.name) || isDxfFile(file.name)) {
  59. file.content = await blob.text();
  60. } else if (isWordFile(file.name) || isExcelFile(file.name)) {
  61. file.arrayBuffer = await blob.arrayBuffer();
  62. } else {
  63. file.preview = URL.createObjectURL(withPreviewType(file, blob));
  64. }
  65. file.originalFile = new File([blob], file.name, {
  66. type: file.type || blob.type || 'application/octet-stream',
  67. });
  68. return file;
  69. };
  70. export const useChatAttachments = () => {
  71. /**
  72. * 拉取会话附件并挂到历史消息上。
  73. * - message_id 能匹配到用户消息:挂到该消息的 attachedFiles;
  74. * - message_id 为 null 或消息已合并/跳过:进入 task.orphanAttachments,由消息区底部兜底展示。
  75. * 附件接口失败不阻断历史消息展示(旧环境可能还没有这个接口)。
  76. */
  77. const loadHistoryAttachments = async (task: Task, sessionId: string, messages: Message[]): Promise<AttachedFile[]> => {
  78. try {
  79. const res = await getSessionAttachments(sessionId);
  80. const attachments = (res?.attachments || []).map(toAttachedFile);
  81. if (!attachments.length) {
  82. task.orphanAttachments = [];
  83. return [];
  84. }
  85. const userMessageById = new Map<number, Message>();
  86. for (const msg of messages) {
  87. if (msg.type === 'user' && msg.id != null) userMessageById.set(msg.id, msg);
  88. }
  89. const orphans: AttachedFile[] = [];
  90. for (const file of attachments) {
  91. const target = file.messageId != null ? userMessageById.get(file.messageId) : undefined;
  92. if (target) {
  93. if (!target.attachedFiles) target.attachedFiles = [];
  94. if (!target.attachedFiles.some((f) => fileKey(f) === fileKey(file))) {
  95. target.attachedFiles.push(file);
  96. }
  97. } else {
  98. orphans.push(file);
  99. }
  100. }
  101. task.orphanAttachments = orphans;
  102. task.attachedFiles = mergeAttachedFiles(task.attachedFiles, attachments);
  103. return attachments;
  104. } catch (e) {
  105. // 历史消息是主内容,附件是增强信息:失败只记录,不打断会话打开
  106. console.error('加载会话附件失败:', e);
  107. return [];
  108. }
  109. };
  110. const handleAttachmentPreview = async (file: AttachedFile, openPreview: (target: AttachedFile) => void) => {
  111. if (hasContent(file)) {
  112. openPreview(file);
  113. return;
  114. }
  115. if (!file.fileId) {
  116. message.warning('该附件暂不支持预览');
  117. return;
  118. }
  119. const closeLoading = message.loading('正在加载附件…', 0);
  120. try {
  121. await ensureAttachmentContent(file);
  122. openPreview(file);
  123. } catch (e) {
  124. console.error('加载附件失败:', e);
  125. message.error((e as Error)?.message || '附件加载失败');
  126. } finally {
  127. closeLoading();
  128. }
  129. };
  130. const handleAttachmentDownload = async (file: AttachedFile) => {
  131. try {
  132. if (file.fileId) {
  133. const closeLoading = message.loading('正在下载附件…', 0);
  134. try {
  135. const blob = await fetchAttachmentBlob(file.fileId, file.downloadUrl);
  136. saveBlob(blob, file.name);
  137. } finally {
  138. closeLoading();
  139. }
  140. return;
  141. }
  142. if (file.originalFile) {
  143. saveBlob(file.originalFile, file.name);
  144. return;
  145. }
  146. if (file.preview?.startsWith('blob:') || file.preview?.startsWith('data:')) {
  147. const blob = await (await fetch(file.preview)).blob();
  148. saveBlob(blob, file.name);
  149. return;
  150. }
  151. message.warning('该附件暂不支持下载');
  152. } catch (e) {
  153. console.error('下载附件失败:', e);
  154. message.error((e as Error)?.message || '附件下载失败');
  155. }
  156. };
  157. return { loadHistoryAttachments, handleAttachmentPreview, handleAttachmentDownload };
  158. };