From 878507ba8a81eeb4ece0d27b0b30e9d970b547fc Mon Sep 17 00:00:00 2001 From: digua Date: Mon, 2 Feb 2026 23:51:12 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dwindows=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E8=87=AA=E5=AE=9A=E4=B9=89=E7=AD=9B=E9=80=89=E6=97=B6?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E9=87=8F=E8=BF=87=E5=A4=A7=E5=AF=BC=E8=87=B4?= =?UTF-8?q?=E8=BD=AF=E4=BB=B6=E5=B4=A9=E6=BA=83=E7=9A=84BUG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/main/ipc/messages.ts | 76 ++++- electron/main/worker/dbWorker.ts | 10 +- electron/main/worker/workerManager.ts | 90 +++++- electron/preload/apis/ai.ts | 81 +++++- electron/preload/index.d.ts | 43 ++- src/components/analysis/Filter/FilterTab.vue | 262 +++++++++++++----- .../analysis/Filter/LocalAnalysisModal.vue | 29 +- .../analysis/Filter/PreviewPanel.vue | 71 ++++- src/i18n/locales/en-US/analysis.json | 9 +- src/i18n/locales/en-US/common.json | 3 +- src/i18n/locales/zh-CN/analysis.json | 9 +- src/i18n/locales/zh-CN/common.json | 3 +- src/types/base.ts | 11 + 13 files changed, 588 insertions(+), 109 deletions(-) diff --git a/electron/main/ipc/messages.ts b/electron/main/ipc/messages.ts index b7a0b74..ba82e28 100644 --- a/electron/main/ipc/messages.ts +++ b/electron/main/ipc/messages.ts @@ -149,7 +149,7 @@ export function registerMessagesHandlers({ win }: IpcContext): void { // ==================== 自定义筛选 ==================== /** - * 按条件筛选消息并扩充上下文 + * 按条件筛选消息并扩充上下文(支持分页) */ ipcMain.handle( 'ai:filterMessagesWithContext', @@ -159,26 +159,78 @@ export function registerMessagesHandlers({ win }: IpcContext): void { keywords?: string[], timeFilter?: { startTs: number; endTs: number }, senderIds?: number[], - contextSize?: number + contextSize?: number, + page?: number, + pageSize?: number ) => { try { - return await worker.filterMessagesWithContext(sessionId, keywords, timeFilter, senderIds, contextSize) + return await worker.filterMessagesWithContext( + sessionId, + keywords, + timeFilter, + senderIds, + contextSize, + page, + pageSize + ) } catch (error) { console.error('筛选消息失败:', error) - return { blocks: [], stats: { totalMessages: 0, hitMessages: 0, totalChars: 0 } } + return { + blocks: [], + stats: { totalMessages: 0, hitMessages: 0, totalChars: 0 }, + pagination: { page: page ?? 1, pageSize: pageSize ?? 50, totalBlocks: 0, totalHits: 0, hasMore: false }, + } } } ) /** - * 获取多个会话的完整消息 + * 获取多个会话的完整消息(支持分页) */ - ipcMain.handle('ai:getMultipleSessionsMessages', async (_, sessionId: string, chatSessionIds: number[]) => { - try { - return await worker.getMultipleSessionsMessages(sessionId, chatSessionIds) - } catch (error) { - console.error('获取多个会话消息失败:', error) - return { blocks: [], stats: { totalMessages: 0, hitMessages: 0, totalChars: 0 } } + ipcMain.handle( + 'ai:getMultipleSessionsMessages', + async (_, sessionId: string, chatSessionIds: number[], page?: number, pageSize?: number) => { + try { + return await worker.getMultipleSessionsMessages(sessionId, chatSessionIds, page, pageSize) + } catch (error) { + console.error('获取多个会话消息失败:', error) + return { + blocks: [], + stats: { totalMessages: 0, hitMessages: 0, totalChars: 0 }, + pagination: { page: page ?? 1, pageSize: pageSize ?? 50, totalBlocks: 0, totalHits: 0, hasMore: false }, + } + } } - }) + ) + + /** + * 导出筛选结果到文件(后端生成,支持进度) + */ + ipcMain.handle( + 'ai:exportFilterResultToFile', + async ( + _, + params: { + sessionId: string + sessionName: string + outputDir: string + filterMode: 'condition' | 'session' + keywords?: string[] + timeFilter?: { startTs: number; endTs: number } + senderIds?: number[] + contextSize?: number + chatSessionIds?: number[] + } + ) => { + try { + return await worker.exportFilterResultToFile(params, (progress) => { + // 发送进度到渲染进程 + win.webContents.send('ai:exportProgress', progress) + }) + } catch (error) { + console.error('导出筛选结果失败:', error) + return { success: false, error: String(error) } + } + } + ) } diff --git a/electron/main/worker/dbWorker.ts b/electron/main/worker/dbWorker.ts index 54ab3f4..fea1cff 100644 --- a/electron/main/worker/dbWorker.ts +++ b/electron/main/worker/dbWorker.ts @@ -62,6 +62,7 @@ import { // 自定义筛选 filterMessagesWithContext, getMultipleSessionsMessages, + exportFilterResultToFile, // NLP 查询 getWordFrequency, segmentText, @@ -148,10 +149,11 @@ const syncHandlers: Record any> = { searchSessions: (p) => searchSessions(p.sessionId, p.keywords, p.timeFilter, p.limit, p.previewCount), getSessionMessages: (p) => getSessionMessages(p.sessionId, p.chatSessionId, p.limit), - // 自定义筛选 + // 自定义筛选(支持分页) filterMessagesWithContext: (p) => - filterMessagesWithContext(p.sessionId, p.keywords, p.timeFilter, p.senderIds, p.contextSize), - getMultipleSessionsMessages: (p) => getMultipleSessionsMessages(p.sessionId, p.chatSessionIds), + filterMessagesWithContext(p.sessionId, p.keywords, p.timeFilter, p.senderIds, p.contextSize, p.page, p.pageSize), + getMultipleSessionsMessages: (p) => + getMultipleSessionsMessages(p.sessionId, p.chatSessionIds, p.page, p.pageSize), // NLP 查询 getWordFrequency: (p) => getWordFrequency(p), @@ -168,6 +170,8 @@ const asyncHandlers: Record Promise // 增量导入 analyzeIncrementalImport: (p, id) => analyzeIncrementalImport(p.sessionId, p.filePath, id), incrementalImport: (p, id) => incrementalImport(p.sessionId, p.filePath, id), + // 导出筛选结果到文件(支持进度报告) + exportFilterResultToFile: async (p, id) => exportFilterResultToFile(p, id), } // 处理消息 diff --git a/electron/main/worker/workerManager.ts b/electron/main/worker/workerManager.ts index 4f4e154..7c94c1c 100644 --- a/electron/main/worker/workerManager.ts +++ b/electron/main/worker/workerManager.ts @@ -122,7 +122,7 @@ export function initWorker(): void { /** * 发送消息到 Worker 并等待响应 */ -function sendToWorker(type: string, payload: any): Promise { +function sendToWorker(type: string, payload: any, timeoutMs: number = 30000): Promise { return new Promise((resolve, reject) => { if (!worker) { try { @@ -139,13 +139,13 @@ function sendToWorker(type: string, payload: any): Promise { worker!.postMessage({ id, type, payload }) - // 设置超时(30秒) + // 设置超时 setTimeout(() => { if (pendingRequests.has(id)) { pendingRequests.delete(id) reject(new Error(`Worker request timeout: ${type}`)) } - }, 30000) + }, timeoutMs) }) } @@ -810,23 +810,93 @@ export interface FilterResult { } /** - * 按条件筛选消息并扩充上下文 + * 分页信息类型 + */ +export interface PaginationInfo { + page: number + pageSize: number + totalBlocks: number + totalHits: number + hasMore: boolean +} + +/** + * 带分页的筛选结果类型 + */ +export interface FilterResultWithPagination extends FilterResult { + pagination: PaginationInfo +} + +/** + * 按条件筛选消息并扩充上下文(支持分页) */ export async function filterMessagesWithContext( sessionId: string, keywords?: string[], timeFilter?: { startTs: number; endTs: number }, senderIds?: number[], - contextSize?: number -): Promise { - return sendToWorker('filterMessagesWithContext', { sessionId, keywords, timeFilter, senderIds, contextSize }) + contextSize?: number, + page?: number, + pageSize?: number +): Promise { + return sendToWorker('filterMessagesWithContext', { + sessionId, + keywords, + timeFilter, + senderIds, + contextSize, + page, + pageSize, + }) } /** - * 获取多个会话的完整消息 + * 获取多个会话的完整消息(支持分页) */ -export async function getMultipleSessionsMessages(sessionId: string, chatSessionIds: number[]): Promise { - return sendToWorker('getMultipleSessionsMessages', { sessionId, chatSessionIds }) +export async function getMultipleSessionsMessages( + sessionId: string, + chatSessionIds: number[], + page?: number, + pageSize?: number +): Promise { + return sendToWorker('getMultipleSessionsMessages', { sessionId, chatSessionIds, page, pageSize }) +} + +/** + * 导出筛选结果参数 + */ +export interface ExportFilterParams { + sessionId: string + sessionName: string + outputDir: string + filterMode: 'condition' | 'session' + keywords?: string[] + timeFilter?: { startTs: number; endTs: number } + senderIds?: number[] + contextSize?: number + chatSessionIds?: number[] +} + +/** + * 导出进度回调类型 + */ +export interface ExportProgress { + stage: 'preparing' | 'exporting' | 'done' | 'error' + currentBlock: number + totalBlocks: number + percentage: number + message: string +} + +/** + * 导出筛选结果到文件(后端生成) + * 使用 10 分钟超时,支持大数据量导出和进度回调 + */ +export async function exportFilterResultToFile( + params: ExportFilterParams, + onProgress?: (progress: ExportProgress) => void +): Promise<{ success: boolean; filePath?: string; error?: string }> { + return sendToWorkerWithProgress('exportFilterResultToFile', params, onProgress as any, 600000) } // ==================== 增量导入 ==================== diff --git a/electron/preload/apis/ai.ts b/electron/preload/apis/ai.ts index cdd653f..3cf5d35 100644 --- a/electron/preload/apis/ai.ts +++ b/electron/preload/apis/ai.ts @@ -2,6 +2,7 @@ * AI 相关 API - AI 对话、LLM 服务、Agent、Embedding */ import { ipcRenderer } from 'electron' +import type { ExportProgress } from '../../../src/types/base' // ==================== 类型定义 ==================== @@ -237,17 +238,26 @@ export const aiApi = { return ipcRenderer.invoke('ai:getMessagesAfter', sessionId, afterId, limit, filter, senderId, keywords) }, - // ==================== 自定义筛选 ==================== + // ==================== 自定义筛选(支持分页) ==================== /** - * 按条件筛选消息并扩充上下文 + * 筛选结果消息类型 + */ + // FilterMessage 和 FilterResult 类型定义在下方 + + /** + * 按条件筛选消息并扩充上下文(支持分页) + * @param page 页码(从 1 开始,默认 1) + * @param pageSize 每页块数(默认 50) */ filterMessagesWithContext: ( sessionId: string, keywords?: string[], timeFilter?: { startTs: number; endTs: number }, senderIds?: number[], - contextSize?: number + contextSize?: number, + page?: number, + pageSize?: number ): Promise<{ blocks: Array<{ startTs: number @@ -273,16 +283,36 @@ export const aiApi = { hitMessages: number totalChars: number } + pagination: { + page: number + pageSize: number + totalBlocks: number + totalHits: number + hasMore: boolean + } }> => { - return ipcRenderer.invoke('ai:filterMessagesWithContext', sessionId, keywords, timeFilter, senderIds, contextSize) + return ipcRenderer.invoke( + 'ai:filterMessagesWithContext', + sessionId, + keywords, + timeFilter, + senderIds, + contextSize, + page, + pageSize + ) }, /** - * 获取多个会话的完整消息 + * 获取多个会话的完整消息(支持分页) + * @param page 页码(从 1 开始,默认 1) + * @param pageSize 每页块数(默认 50) */ getMultipleSessionsMessages: ( sessionId: string, - chatSessionIds: number[] + chatSessionIds: number[], + page?: number, + pageSize?: number ): Promise<{ blocks: Array<{ startTs: number @@ -308,8 +338,45 @@ export const aiApi = { hitMessages: number totalChars: number } + pagination: { + page: number + pageSize: number + totalBlocks: number + totalHits: number + hasMore: boolean + } }> => { - return ipcRenderer.invoke('ai:getMultipleSessionsMessages', sessionId, chatSessionIds) + return ipcRenderer.invoke('ai:getMultipleSessionsMessages', sessionId, chatSessionIds, page, pageSize) + }, + + /** + * 导出筛选结果到文件(后端生成,支持大数据量) + */ + exportFilterResultToFile: (params: { + sessionId: string + sessionName: string + outputDir: string + filterMode: 'condition' | 'session' + keywords?: string[] + timeFilter?: { startTs: number; endTs: number } + senderIds?: number[] + contextSize?: number + chatSessionIds?: number[] + }): Promise<{ success: boolean; filePath?: string; error?: string }> => { + return ipcRenderer.invoke('ai:exportFilterResultToFile', params) + }, + + /** + * 监听导出进度 + */ + onExportProgress: (callback: (progress: ExportProgress) => void) => { + const handler = (_event: Electron.IpcRendererEvent, progress: ExportProgress) => { + callback(progress) + } + ipcRenderer.on('ai:exportProgress', handler) + return () => { + ipcRenderer.removeListener('ai:exportProgress', handler) + } }, /** diff --git a/electron/preload/index.d.ts b/electron/preload/index.d.ts index baa0d2f..74e3108 100644 --- a/electron/preload/index.d.ts +++ b/electron/preload/index.d.ts @@ -1,5 +1,5 @@ import { ElectronAPI } from '@electron-toolkit/preload' -import type { AnalysisSession, MessageType, ImportProgress } from '../../src/types/base' +import type { AnalysisSession, MessageType, ImportProgress, ExportProgress } from '../../src/types/base' import type { MemberActivity, MemberNameHistory, @@ -252,6 +252,20 @@ interface FilterResult { } } +// 分页信息类型 +interface PaginationInfo { + page: number + pageSize: number + totalBlocks: number + totalHits: number + hasMore: boolean +} + +// 带分页的筛选结果类型 +interface FilterResultWithPagination extends FilterResult { + pagination: PaginationInfo +} + interface AIConversation { id: string sessionId: string @@ -348,15 +362,36 @@ interface AiApi { getMessages: (conversationId: string) => Promise deleteMessage: (messageId: string) => Promise showAiLogFile: () => Promise<{ success: boolean; path?: string; error?: string }> - // 自定义筛选 + // 自定义筛选(支持分页) filterMessagesWithContext: ( sessionId: string, keywords?: string[], timeFilter?: TimeFilter, senderIds?: number[], + contextSize?: number, + page?: number, + pageSize?: number + ) => Promise + getMultipleSessionsMessages: ( + sessionId: string, + chatSessionIds: number[], + page?: number, + pageSize?: number + ) => Promise + // 导出筛选结果到文件 + exportFilterResultToFile: (params: { + sessionId: string + sessionName: string + outputDir: string + filterMode: 'condition' | 'session' + keywords?: string[] + timeFilter?: TimeFilter + senderIds?: number[] contextSize?: number - ) => Promise - getMultipleSessionsMessages: (sessionId: string, chatSessionIds: number[]) => Promise + chatSessionIds?: number[] + }) => Promise<{ success: boolean; filePath?: string; error?: string }> + // 监听导出进度 + onExportProgress: (callback: (progress: ExportProgress) => void) => () => void } // LLM 相关类型 diff --git a/src/components/analysis/Filter/FilterTab.vue b/src/components/analysis/Filter/FilterTab.vue index b03e947..29163d5 100644 --- a/src/components/analysis/Filter/FilterTab.vue +++ b/src/components/analysis/Filter/FilterTab.vue @@ -6,10 +6,13 @@ * 支持两种互斥的筛选模式: * 1. 条件筛选:按关键词、时间、发送者筛选,并自动扩展上下文 * 2. 会话筛选:直接选择已有的会话(对话段落) + * + * 支持分页加载,避免大数据量时内存溢出 */ import { ref, computed, watch, toRaw } from 'vue' import { useI18n } from 'vue-i18n' +import { useToast } from '@nuxt/ui/runtime/composables/useToast.js' import { useSessionStore } from '@/stores/session' import ConditionPanel from './ConditionPanel.vue' import SessionPanel from './SessionPanel.vue' @@ -18,6 +21,7 @@ import FilterHistory from './FilterHistory.vue' import LocalAnalysisModal from './LocalAnalysisModal.vue' const { t } = useI18n() +const toast = useToast() const sessionStore = useSessionStore() // 筛选模式:'condition' | 'session' @@ -55,7 +59,16 @@ interface FilterMessage { isHit: boolean } -// 筛选结果 +// 分页信息类型 +interface PaginationInfo { + page: number + pageSize: number + totalBlocks: number + totalHits: number + hasMore: boolean +} + +// 筛选结果(带分页) const filterResult = ref<{ blocks: Array<{ startTs: number @@ -68,13 +81,18 @@ const filterResult = ref<{ hitMessages: number totalChars: number } + pagination: PaginationInfo } | null>(null) // 加载状态 const isFiltering = ref(false) +const isLoadingMore = ref(false) const showHistory = ref(false) const showAnalysisModal = ref(false) +// 每页块数 +const PAGE_SIZE = 50 + // 估算 Token 数 // 中文:1 字符 ≈ 1.5 token(因为中文分词后每个字符可能产生 1-2 个 token) // 考虑到消息格式(时间、发送人等),使用 1.5 作为估算系数 @@ -109,7 +127,7 @@ const canExecuteFilter = computed(() => { } }) -// 执行筛选 +// 执行筛选(首次加载) async function executeFilter() { const sessionId = sessionStore.currentSessionId if (!sessionId) return @@ -133,14 +151,16 @@ async function executeFilter() { keywords, timeFilter, senderIds, - contextSize + contextSize, + 1, // 第一页 + PAGE_SIZE ) filterResult.value = result } else { // 会话筛选模式 if (selectedSessionIds.value.length === 0) return const sessionIds = [...toRaw(selectedSessionIds.value)] - const result = await window.aiApi.getMultipleSessionsMessages(sessionId, sessionIds) + const result = await window.aiApi.getMultipleSessionsMessages(sessionId, sessionIds, 1, PAGE_SIZE) filterResult.value = result } } catch (error) { @@ -150,69 +170,159 @@ async function executeFilter() { } } -// 导出投喂包(Markdown 格式) +// 加载更多块 +async function loadMoreBlocks() { + const sessionId = sessionStore.currentSessionId + if (!sessionId || !filterResult.value || !filterResult.value.pagination.hasMore || isLoadingMore.value) return + + isLoadingMore.value = true + const nextPage = filterResult.value.pagination.page + 1 + + try { + let result + if (filterMode.value === 'condition') { + const rawFilter = toRaw(conditionFilter.value) + const keywords = rawFilter.keywords.length > 0 ? [...rawFilter.keywords] : undefined + const timeFilter = rawFilter.timeRange + ? { startTs: rawFilter.timeRange.start, endTs: rawFilter.timeRange.end } + : undefined + const senderIds = rawFilter.senderIds.length > 0 ? [...rawFilter.senderIds] : undefined + const contextSize = rawFilter.contextSize + + result = await window.aiApi.filterMessagesWithContext( + sessionId, + keywords, + timeFilter, + senderIds, + contextSize, + nextPage, + PAGE_SIZE + ) + } else { + const sessionIds = [...toRaw(selectedSessionIds.value)] + result = await window.aiApi.getMultipleSessionsMessages(sessionId, sessionIds, nextPage, PAGE_SIZE) + } + + // 合并新加载的块到现有结果 + if (result && result.blocks.length > 0) { + filterResult.value = { + blocks: [...filterResult.value.blocks, ...result.blocks], + stats: filterResult.value.stats, // 统计信息保持不变(第一页已获取) + pagination: result.pagination, + } + } + } catch (error) { + console.error('加载更多失败:', error) + } finally { + isLoadingMore.value = false + } +} + +// 导出状态 +const isExporting = ref(false) +const exportProgress = ref<{ + percentage: number + message: string +} | null>(null) + +// 监听导出进度 +let unsubscribeExportProgress: (() => void) | null = null + +function startExportProgressListener() { + unsubscribeExportProgress = window.aiApi.onExportProgress((progress) => { + exportProgress.value = { + percentage: progress.percentage, + message: progress.message, + } + // 如果完成或出错,不再需要监听 + if (progress.stage === 'done' || progress.stage === 'error') { + exportProgress.value = null + } + }) +} + +function stopExportProgressListener() { + if (unsubscribeExportProgress) { + unsubscribeExportProgress() + unsubscribeExportProgress = null + } + exportProgress.value = null +} + +// 导出投喂包(后端生成 Markdown 文件,支持大数据量) async function exportFeedPack() { if (!filterResult.value || filterResult.value.blocks.length === 0) return + const sessionId = sessionStore.currentSessionId + if (!sessionId) return + const sessionInfo = sessionStore.currentSession const sessionName = sessionInfo?.name || '未知会话' - // 构建 Markdown 内容 - let markdown = `# ${sessionName} - 聊天记录筛选结果\n\n` - markdown += `> 导出时间: ${new Date().toLocaleString()}\n\n` + // 让用户选择保存目录 + const dialogResult = await window.api.dialog.showOpenDialog({ + title: '选择保存目录', + properties: ['openDirectory', 'createDirectory'], + }) + if (dialogResult.canceled || !dialogResult.filePaths[0]) return + const outputDir = dialogResult.filePaths[0] - // 筛选条件摘要 - markdown += `## 筛选条件\n\n` - if (filterMode.value === 'condition') { - if (conditionFilter.value.keywords.length > 0) { - markdown += `- 关键词: ${conditionFilter.value.keywords.join(', ')}\n` + isExporting.value = true + exportProgress.value = { percentage: 0, message: t('analysis.filter.exportPreparing') } + + // 开始监听进度 + startExportProgressListener() + + try { + // 构建导出参数 + const rawFilter = toRaw(conditionFilter.value) + const exportParams = { + sessionId, + sessionName, + outputDir, + filterMode: filterMode.value, + keywords: rawFilter.keywords.length > 0 ? [...rawFilter.keywords] : undefined, + timeFilter: rawFilter.timeRange + ? { startTs: rawFilter.timeRange.start, endTs: rawFilter.timeRange.end } + : undefined, + senderIds: rawFilter.senderIds.length > 0 ? [...rawFilter.senderIds] : undefined, + contextSize: rawFilter.contextSize, + chatSessionIds: + filterMode.value === 'session' ? [...toRaw(selectedSessionIds.value)] : undefined, } - if (conditionFilter.value.timeRange) { - const start = new Date(conditionFilter.value.timeRange.start * 1000).toLocaleString() - const end = new Date(conditionFilter.value.timeRange.end * 1000).toLocaleString() - markdown += `- 时间范围: ${start} ~ ${end}\n` + + // 调用后端导出 + const exportResult = await window.aiApi.exportFilterResultToFile(exportParams) + + if (exportResult.success && exportResult.filePath) { + // 导出成功 + toast.add({ + title: t('analysis.filter.exportSuccess'), + description: exportResult.filePath, + color: 'green', + icon: 'i-heroicons-check-circle', + }) + } else { + // 导出失败 + toast.add({ + title: t('analysis.filter.exportFailed'), + description: exportResult.error || t('common.error.unknown'), + color: 'red', + icon: 'i-heroicons-x-circle', + }) } - markdown += `- 上下文扩展: ±${conditionFilter.value.contextSize} 条消息\n` - } else { - markdown += `- 模式: 会话筛选\n` - markdown += `- 选中会话数: ${selectedSessionIds.value.length}\n` + } catch (error) { + console.error('导出失败:', error) + toast.add({ + title: t('analysis.filter.exportFailed'), + description: String(error), + color: 'red', + icon: 'i-heroicons-x-circle', + }) + } finally { + stopExportProgressListener() + isExporting.value = false } - - // 统计信息 - markdown += `\n## 统计信息\n\n` - markdown += `- 消息总数: ${filterResult.value.stats.totalMessages}\n` - markdown += `- 命中消息: ${filterResult.value.stats.hitMessages}\n` - markdown += `- 字符数: ${filterResult.value.stats.totalChars}\n` - markdown += `- 预估 Token: ${estimatedTokens.value}\n` - - // 对话内容 - markdown += `\n## 对话内容\n\n` - - for (let i = 0; i < filterResult.value.blocks.length; i++) { - const block = filterResult.value.blocks[i] - const startTime = new Date(block.startTs * 1000).toLocaleString() - const endTime = new Date(block.endTs * 1000).toLocaleString() - - markdown += `### 对话块 ${i + 1} (${startTime} ~ ${endTime})\n\n` - - for (const msg of block.messages) { - const time = new Date(msg.timestamp * 1000).toLocaleTimeString() - const hitMark = msg.isHit ? ' ⭐' : '' - const content = msg.content || '[非文本消息]' - markdown += `${time} ${msg.senderName}${hitMark}: ${content}\n` - } - } - - // 创建并下载文件 - const blob = new Blob([markdown], { type: 'text/markdown;charset=utf-8' }) - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url - a.download = `${sessionName}_筛选结果_${Date.now()}.md` - document.body.appendChild(a) - a.click() - document.body.removeChild(a) - URL.revokeObjectURL(url) } // 打开本地 AI 分析 @@ -313,21 +423,45 @@ function loadHistoryCondition(condition: {
- - {{ t('analysis.filter.export') }} - - - {{ t('analysis.filter.localAnalysis') }} - + +
+
+ {{ exportProgress.message }} + {{ exportProgress.percentage }}% +
+
+
+
+
+ +
+ + {{ t('analysis.filter.export') }} + + + {{ t('analysis.filter.localAnalysis') }} + +
diff --git a/src/components/analysis/Filter/LocalAnalysisModal.vue b/src/components/analysis/Filter/LocalAnalysisModal.vue index a7649a0..fb684e1 100644 --- a/src/components/analysis/Filter/LocalAnalysisModal.vue +++ b/src/components/analysis/Filter/LocalAnalysisModal.vue @@ -56,6 +56,15 @@ const props = defineProps<{ const open = defineModel('open', { default: false }) +// 数据量阈值(超过此数量时提示数据量过大) +const DATA_TOO_LARGE_THRESHOLD = 5000 + +// 检查数据量是否过大 +const isDataTooLarge = computed(() => { + if (!props.filterResult) return false + return props.filterResult.stats.totalMessages > DATA_TOO_LARGE_THRESHOLD +}) + // 分析模式:'preset' | 'custom' const analysisMode = ref<'preset' | 'custom'>('preset') @@ -224,6 +233,24 @@ watch(open, (val) => { + +
+
+ +
+

+ {{ t('analysis.filter.dataTooLarge') }} +

+

+ {{ t('analysis.filter.dataTooLargeThreshold', { count: DATA_TOO_LARGE_THRESHOLD }) }} +

+
+
+
+