diff --git a/Yi.Ai.Vue3/pnpm-lock.yaml b/Yi.Ai.Vue3/pnpm-lock.yaml index 64af91a3..b34b10eb 100644 --- a/Yi.Ai.Vue3/pnpm-lock.yaml +++ b/Yi.Ai.Vue3/pnpm-lock.yaml @@ -3467,6 +3467,9 @@ packages: mlly@1.7.4: resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -5770,7 +5773,7 @@ snapshots: jiti: 2.4.2 klona: 2.0.6 knitwork: 1.2.0 - mlly: 1.7.4 + mlly: 1.8.0 ohash: 2.0.11 pathe: 2.0.3 pkg-types: 2.1.0 @@ -8943,6 +8946,14 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.1 + mlly@1.8.0: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + optional: true + mri@1.2.0: {} mrmime@2.0.1: {} @@ -10330,7 +10341,7 @@ snapshots: estree-walker: 3.0.3 local-pkg: 1.1.1 magic-string: 0.30.17 - mlly: 1.7.4 + mlly: 1.8.0 pathe: 2.0.3 picomatch: 4.0.3 pkg-types: 2.1.0 diff --git a/Yi.Ai.Vue3/src/components/FilesSelect/index.vue b/Yi.Ai.Vue3/src/components/FilesSelect/index.vue index 8f22ca86..e91f0d9b 100644 --- a/Yi.Ai.Vue3/src/components/FilesSelect/index.vue +++ b/Yi.Ai.Vue3/src/components/FilesSelect/index.vue @@ -413,24 +413,49 @@ onChange(async (files) => { // 处理图片文件 if (isImage) { try { - // 先压缩图片 - const compressedBlob = await compressImage(file, 1024, 1024, 0.8); - // 再转换为 base64 - const base64 = await blobToBase64(compressedBlob); + // 多级压缩策略:逐步降低质量和分辨率 + const compressionLevels = [ + { maxWidth: 800, maxHeight: 800, quality: 0.6 }, + { maxWidth: 600, maxHeight: 600, quality: 0.5 }, + { maxWidth: 400, maxHeight: 400, quality: 0.4 }, + ]; - // 检查总长度(base64 保守估计占用) - const estimatedLength = Math.floor(base64.length * 0.5); - if (totalContentLength + estimatedLength > MAX_TOTAL_CONTENT_LENGTH) { - ElMessage.error(`添加 ${file.name} 会超过消息总长度限制(${MAX_TOTAL_CONTENT_LENGTH} 字符),已跳过`); - continue; + let compressedBlob: Blob | null = null; + let base64 = ''; + let compressionLevel = 0; + + // 尝试不同级别的压缩 + for (const level of compressionLevels) { + compressionLevel++; + compressedBlob = await compressImage(file, level.maxWidth, level.maxHeight, level.quality); + base64 = await blobToBase64(compressedBlob); + + // 检查是否满足总长度限制 + const estimatedLength = Math.floor(base64.length * 0.5); + if (totalContentLength + estimatedLength <= MAX_TOTAL_CONTENT_LENGTH) { + // 满足限制,使用当前压缩级别 + totalContentLength += estimatedLength; + break; + } + + // 如果是最后一级压缩仍然超限,则跳过 + if (compressionLevel === compressionLevels.length) { + const fileSizeMB = (file.size / 1024 / 1024).toFixed(2); + ElMessage.error(`${file.name} (${fileSizeMB}MB) 即使压缩后仍超过总内容限制,已跳过`); + compressedBlob = null; + break; + } } - totalContentLength += estimatedLength; + // 如果压缩失败,跳过此文件 + if (!compressedBlob) { + continue; + } // 计算压缩比例 const originalSize = (file.size / 1024).toFixed(2); const compressedSize = (compressedBlob.size / 1024).toFixed(2); - console.log(`图片压缩: ${file.name} - 原始: ${originalSize}KB, 压缩后: ${compressedSize}KB`); + console.log(`图片压缩: ${file.name} - 原始: ${originalSize}KB, 压缩后: ${compressedSize}KB (级别${compressionLevel})`); arr.push({ uid: crypto.randomUUID(), @@ -458,13 +483,23 @@ onChange(async (files) => { try { const result = await parseExcel(file); - // 检查总长度 - if (totalContentLength + result.content.length > MAX_TOTAL_CONTENT_LENGTH) { - ElMessage.error(`添加 ${file.name} 会超过消息总长度限制(${MAX_TOTAL_CONTENT_LENGTH} 字符),已跳过`); + // 动态裁剪内容以适应剩余空间 + let finalContent = result.content; + let wasTruncated = result.totalRows > MAX_EXCEL_ROWS; + + // 如果超过总内容限制,裁剪内容 + const remainingSpace = MAX_TOTAL_CONTENT_LENGTH - totalContentLength; + if (result.content.length > remainingSpace && remainingSpace > 1000) { + // 至少保留1000字符才有意义 + finalContent = result.content.substring(0, remainingSpace); + wasTruncated = true; + } else if (remainingSpace <= 1000) { + const fileSizeKB = (file.size / 1024).toFixed(2); + ElMessage.error(`${file.name} (${fileSizeKB}KB) 会超过总内容限制,已跳过`); continue; } - totalContentLength += result.content.length; + totalContentLength += finalContent.length; arr.push({ uid: crypto.randomUUID(), @@ -475,16 +510,17 @@ onChange(async (files) => { showDelIcon: true, imgPreview: false, isUploaded: true, - fileContent: result.content, + fileContent: finalContent, fileType: 'text', }); // 提示信息 - if (result.totalRows > MAX_EXCEL_ROWS) { - ElMessage.warning(`${file.name} 共 ${result.totalRows} 行,已提取前 ${result.extractedRows} 行`); + if (wasTruncated) { + const fileSizeKB = (file.size / 1024).toFixed(2); + ElMessage.warning(`${file.name} (${fileSizeKB}KB) 内容过大,已自动截取部分内容`); } - console.log(`Excel 解析: ${file.name} - 大小: ${(file.size / 1024).toFixed(2)}KB, 总行数: ${result.totalRows}, 已提取: ${result.extractedRows} 行, 内容长度: ${result.content.length} 字符`); + console.log(`Excel 解析: ${file.name} - 大小: ${(file.size / 1024).toFixed(2)}KB, 总行数: ${result.totalRows}, 已提取: ${result.extractedRows} 行, 内容长度: ${finalContent.length} 字符`); } catch (error) { console.error('解析 Excel 失败:', error); @@ -497,13 +533,22 @@ onChange(async (files) => { try { const result = await parseWord(file); - // 检查总长度 - if (totalContentLength + result.content.length > MAX_TOTAL_CONTENT_LENGTH) { - ElMessage.error(`添加 ${file.name} 会超过消息总长度限制(${MAX_TOTAL_CONTENT_LENGTH} 字符),已跳过`); + // 动态裁剪内容以适应剩余空间 + let finalContent = result.content; + let wasTruncated = result.extracted; + + // 如果超过总内容限制,裁剪内容 + const remainingSpace = MAX_TOTAL_CONTENT_LENGTH - totalContentLength; + if (result.content.length > remainingSpace && remainingSpace > 1000) { + finalContent = result.content.substring(0, remainingSpace); + wasTruncated = true; + } else if (remainingSpace <= 1000) { + const fileSizeKB = (file.size / 1024).toFixed(2); + ElMessage.error(`${file.name} (${fileSizeKB}KB) 会超过总内容限制,已跳过`); continue; } - totalContentLength += result.content.length; + totalContentLength += finalContent.length; arr.push({ uid: crypto.randomUUID(), @@ -514,16 +559,17 @@ onChange(async (files) => { showDelIcon: true, imgPreview: false, isUploaded: true, - fileContent: result.content, + fileContent: finalContent, fileType: 'text', }); // 提示信息 - if (result.extracted) { - ElMessage.warning(`${file.name} 共 ${result.totalLength} 字符,已提取前 ${MAX_WORD_LENGTH} 字符`); + if (wasTruncated) { + const fileSizeKB = (file.size / 1024).toFixed(2); + ElMessage.warning(`${file.name} (${fileSizeKB}KB) 内容过大,已自动截取部分内容`); } - console.log(`Word 解析: ${file.name} - 大小: ${(file.size / 1024).toFixed(2)}KB, 总长度: ${result.totalLength}, 已提取: ${result.content.length} 字符`); + console.log(`Word 解析: ${file.name} - 大小: ${(file.size / 1024).toFixed(2)}KB, 总长度: ${result.totalLength}, 已提取: ${finalContent.length} 字符`); } catch (error) { console.error('解析 Word 失败:', error); @@ -536,13 +582,22 @@ onChange(async (files) => { try { const result = await parsePDF(file); - // 检查总长度 - if (totalContentLength + result.content.length > MAX_TOTAL_CONTENT_LENGTH) { - ElMessage.error(`添加 ${file.name} 会超过消息总长度限制(${MAX_TOTAL_CONTENT_LENGTH} 字符),已跳过`); + // 动态裁剪内容以适应剩余空间 + let finalContent = result.content; + let wasTruncated = result.totalPages > MAX_PDF_PAGES; + + // 如果超过总内容限制,裁剪内容 + const remainingSpace = MAX_TOTAL_CONTENT_LENGTH - totalContentLength; + if (result.content.length > remainingSpace && remainingSpace > 1000) { + finalContent = result.content.substring(0, remainingSpace); + wasTruncated = true; + } else if (remainingSpace <= 1000) { + const fileSizeKB = (file.size / 1024).toFixed(2); + ElMessage.error(`${file.name} (${fileSizeKB}KB) 会超过总内容限制,已跳过`); continue; } - totalContentLength += result.content.length; + totalContentLength += finalContent.length; arr.push({ uid: crypto.randomUUID(), @@ -553,16 +608,17 @@ onChange(async (files) => { showDelIcon: true, imgPreview: false, isUploaded: true, - fileContent: result.content, + fileContent: finalContent, fileType: 'text', }); // 提示信息 - if (result.totalPages > MAX_PDF_PAGES) { - ElMessage.warning(`${file.name} 共 ${result.totalPages} 页,已提取前 ${result.extractedPages} 页`); + if (wasTruncated) { + const fileSizeKB = (file.size / 1024).toFixed(2); + ElMessage.warning(`${file.name} (${fileSizeKB}KB) 内容过大,已自动截取部分内容`); } - console.log(`PDF 解析: ${file.name} - 大小: ${(file.size / 1024).toFixed(2)}KB, 总页数: ${result.totalPages}, 已提取: ${result.extractedPages} 页, 内容长度: ${result.content.length} 字符`); + console.log(`PDF 解析: ${file.name} - 大小: ${(file.size / 1024).toFixed(2)}KB, 总页数: ${result.totalPages}, 已提取: ${result.extractedPages} 页, 内容长度: ${finalContent.length} 字符`); } catch (error) { console.error('解析 PDF 失败:', error); @@ -584,9 +640,14 @@ onChange(async (files) => { truncated = true; } - // 检查总长度 - if (totalContentLength + finalContent.length > MAX_TOTAL_CONTENT_LENGTH) { - ElMessage.error(`添加 ${file.name} 会超过消息总长度限制(${MAX_TOTAL_CONTENT_LENGTH} 字符),已跳过`); + // 动态裁剪内容以适应剩余空间 + const remainingSpace = MAX_TOTAL_CONTENT_LENGTH - totalContentLength; + if (finalContent.length > remainingSpace && remainingSpace > 1000) { + finalContent = finalContent.substring(0, remainingSpace); + truncated = true; + } else if (remainingSpace <= 1000) { + const fileSizeKB = (file.size / 1024).toFixed(2); + ElMessage.error(`${file.name} (${fileSizeKB}KB) 会超过总内容限制,已跳过`); continue; } @@ -607,7 +668,8 @@ onChange(async (files) => { // 提示信息 if (truncated) { - ElMessage.warning(`${file.name} 共 ${content.length} 字符,已提取前 ${MAX_TEXT_FILE_LENGTH} 字符`); + const fileSizeKB = (file.size / 1024).toFixed(2); + ElMessage.warning(`${file.name} (${fileSizeKB}KB) 内容过大,已自动截取部分内容`); } console.log(`文本文件读取: ${file.name} - 大小: ${(file.size / 1024).toFixed(2)}KB, 内容长度: ${content.length} 字符`); @@ -627,7 +689,7 @@ onChange(async (files) => { if (arr.length > 0) { filesStore.setFilesList([...filesStore.filesList, ...arr]); - ElMessage.success(`已添加 ${arr.length} 个文件,当前总内容长度约 ${totalContentLength} 字符`); + ElMessage.success(`已添加 ${arr.length} 个文件`); } // 重置文件选择器 diff --git a/Yi.Ai.Vue3/src/pages/chat/layouts/chatWithId/index.vue b/Yi.Ai.Vue3/src/pages/chat/layouts/chatWithId/index.vue index 6389a3cf..7087c544 100644 --- a/Yi.Ai.Vue3/src/pages/chat/layouts/chatWithId/index.vue +++ b/Yi.Ai.Vue3/src/pages/chat/layouts/chatWithId/index.vue @@ -274,6 +274,7 @@ async function startSSE(chatContent: string) { type: 'image_url', image_url: { url: fileItem.base64, // 使用base64 + name: fileItem.name, // 保存图片名称 }, }); } @@ -479,7 +480,6 @@ function handleImagePreview(url: string) { {{ file.name }} - {{ (file.size / 1024).toFixed(2) }} KB diff --git a/Yi.Ai.Vue3/src/stores/modules/chat.ts b/Yi.Ai.Vue3/src/stores/modules/chat.ts index af92ebf6..b17373b4 100644 --- a/Yi.Ai.Vue3/src/stores/modules/chat.ts +++ b/Yi.Ai.Vue3/src/stores/modules/chat.ts @@ -17,17 +17,101 @@ export const useChatStore = defineStore('chat', () => { // 会议ID对应-聊天记录 map对象 const chatMap = ref>({}); + /** + * 解析消息内容,提取文本、图片和文件信息 + * @param content - 消息内容,可能是字符串或数组格式的JSON字符串 + * @returns 解析后的文本内容、图片列表和文件列表 + */ + function parseMessageContent(content: string | any): { + text: string; + images: Array<{ url: string; name?: string }>; + files: Array<{ name: string; size: number }>; + } { + let text = ''; + const images: Array<{ url: string; name?: string }> = []; + const files: Array<{ name: string; size: number }> = []; + + try { + // 如果 content 是字符串,尝试解析为 JSON + let contentArray: any; + if (typeof content === 'string') { + // 尝试解析 JSON 数组格式 + if (content.trim().startsWith('[')) { + contentArray = JSON.parse(content); + } + else { + // 普通文本 + text = content; + return { text, images, files }; + } + } + else { + contentArray = content; + } + + // 如果不是数组,直接返回 + if (!Array.isArray(contentArray)) { + text = String(content); + return { text, images, files }; + } + + // 遍历数组,提取文本和图片 + for (const item of contentArray) { + if (item.type === 'text') { + text += item.text || ''; + } + else if (item.type === 'image_url') { + if (item.image_url?.url) { + images.push({ + url: item.image_url.url, + name: item.image_url.name, + }); + } + } + } + + // 从文本中提取文件信息(如果有 ATTACHMENT_FILE 标签) + const fileMatches = text.matchAll(/[\s\S]*?(.*?)<\/FILE_NAME>[\s\S]*?<\/ATTACHMENT_FILE>/g); + for (const match of fileMatches) { + const fileName = match[1]; + files.push({ + name: fileName, + size: 0, // 从历史记录中无法获取文件大小 + }); + } + + // 从文本中移除 ATTACHMENT_FILE 标签及其内容,只保留文件卡片显示 + text = text.replace(/[\s\S]*?<\/ATTACHMENT_FILE>/g, '').trim(); + + return { text, images, files }; + } + catch (error) { + console.error('解析消息内容失败:', error); + // 解析失败,返回原始内容 + return { + text: String(content), + images: [], + files: [], + }; + } + } + const setChatMap = (id: string, data: ChatMessageVo[]) => { chatMap.value[id] = data?.map((item: ChatMessageVo) => { const isUser = item.role === 'user'; - const thinkContent = extractThkContent(item.content as string); + + // 解析消息内容 + const { text, images, files } = parseMessageContent(item.content as string); + + // 处理思考内容 + const thinkContent = extractThkContent(text); + const finalContent = extractThkContentAfter(text); + return { ...item, key: item.id, placement: isUser ? 'end' : 'start', isMarkdown: !isUser, - // variant: 'shadow', - // shape: 'corner', avatar: isUser ? getUserProfilePicture() : systemProfilePicture, @@ -35,11 +119,11 @@ export const useChatStore = defineStore('chat', () => { typing: false, reasoning_content: thinkContent, thinkingStatus: 'end', - content: extractThkContentAfter(item.content as string), + content: finalContent, thinlCollapse: false, - // 保留图片和文件信息 - images: item.images, - files: item.files, + // 保留图片和文件信息(优先使用解析出来的,如果没有则使用原有的) + images: images.length > 0 ? images : item.images, + files: files.length > 0 ? files : item.files, }; }); };