From e7fcbbce5aae1b533f963a13988d690c18cc3faf Mon Sep 17 00:00:00 2001 From: digua Date: Fri, 23 Jan 2026 23:34:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AF=BC=E5=85=A5=E8=81=8A=E5=A4=A9?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E6=8A=A5=E9=94=99=E6=97=B6=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E6=9B=B4=E8=AF=A6=E7=BB=86=E7=9A=84=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/main/ipc/chat.ts | 5 +- electron/main/worker/import/streamImport.ts | 116 ++++++++++++++++++-- electron/preload/index.d.ts | 22 ++++ src/i18n/locales/en-US/home.json | 6 + src/i18n/locales/zh-CN/home.json | 6 + src/pages/home/components/ImportArea.vue | 43 +++++++- src/stores/session.ts | 19 +++- 7 files changed, 206 insertions(+), 11 deletions(-) diff --git a/electron/main/ipc/chat.ts b/electron/main/ipc/chat.ts index 3604fd1..5a0275e 100644 --- a/electron/main/ipc/chat.ts +++ b/electron/main/ipc/chat.ts @@ -128,7 +128,7 @@ export function registerChatHandlers(ctx: IpcContext): void { if (result.success) { console.log('[IpcMain] Stream import successful, sessionId:', result.sessionId) - return { success: true, sessionId: result.sessionId } + return { success: true, sessionId: result.sessionId, diagnostics: result.diagnostics } } else { console.error('[IpcMain] Stream import failed:', result.error) win.webContents.send('chat:importProgress', { @@ -150,10 +150,11 @@ export function registerChatHandlers(ctx: IpcContext): void { missingFields: m.missingFields, })), }, + diagnostics: result.diagnostics, } } - return { success: false, error: result.error } + return { success: false, error: result.error, diagnostics: result.diagnostics } } } catch (error) { console.error('[IpcMain] Import failed:', error) diff --git a/electron/main/worker/import/streamImport.ts b/electron/main/worker/import/streamImport.ts index a968118..71c1949 100644 --- a/electron/main/worker/import/streamImport.ts +++ b/electron/main/worker/import/streamImport.ts @@ -16,14 +16,40 @@ import { type ParsedMessage, } from '../../parser' import { getDbDir } from '../core' -import { initPerfLog, logPerf, logPerfDetail, resetPerfLog, logInfo, logError, logSummary } from '../core' +import { initPerfLog, logPerf, logPerfDetail, resetPerfLog, logInfo, logError, logSummary, getCurrentLogFile } from '../core' import { sendProgress, generateSessionId, getDbPath, createDatabaseWithoutIndexes, createIndexes } from './utils' +/** 跳过消息的原因统计 */ +export interface SkipReasons { + noSenderId: number + noAccountName: number + invalidTimestamp: number + noType: number +} + +/** 导入诊断信息 */ +export interface ImportDiagnostics { + /** 日志文件路径 */ + logFile: string | null + /** 检测到的格式 */ + detectedFormat: string | null + /** 收到的消息数 */ + messagesReceived: number + /** 写入的消息数 */ + messagesWritten: number + /** 跳过的消息数 */ + messagesSkipped: number + /** 跳过原因统计 */ + skipReasons: SkipReasons +} + /** 流式导入结果 */ export interface StreamImportResult { success: boolean sessionId?: string error?: string + /** 诊断信息(成功或失败时都返回) */ + diagnostics?: ImportDiagnostics } // ==================== 临时数据库相关(用于合并功能) ==================== @@ -257,16 +283,35 @@ export async function streamImport(filePath: string, requestId: string): Promise let shouldDeleteDb = false let importError: string | null = null + // 统计回调调用次数(用于诊断) + let callbackStats = { + onProgressCalls: 0, + onLogCalls: 0, + onMetaCalls: 0, + onMembersCalls: 0, + onMessageBatchCalls: 0, + totalMembersReceived: 0, + totalMessagesReceived: 0, + skippedNoSenderId: 0, + skippedNoAccountName: 0, + skippedInvalidTimestamp: 0, + skippedNoType: 0, + } + + logInfo('开始调用 streamParseFile...') + try { await streamParseFile(actualFilePath, { batchSize: 5000, onProgress: (progress) => { + callbackStats.onProgressCalls++ // 转发进度到主进程 sendProgress(requestId, progress) }, onLog: (level, message) => { + callbackStats.onLogCalls++ // 将解析器日志写入导入日志文件 if (level === 'error') { logError(message) @@ -276,8 +321,9 @@ export async function streamImport(filePath: string, requestId: string): Promise }, onMeta: (meta: ParsedMeta) => { + callbackStats.onMetaCalls++ if (!metaInserted) { - logInfo(`写入 meta: name=${meta.name}, type=${meta.type}`) + logInfo(`写入 meta: name=${meta.name}, type=${meta.type}, platform=${meta.platform}`) insertMeta.run( meta.name, meta.platform, @@ -292,6 +338,9 @@ export async function streamImport(filePath: string, requestId: string): Promise }, onMembers: (members: ParsedMember[]) => { + callbackStats.onMembersCalls++ + callbackStats.totalMembersReceived += members.length + logInfo(`收到成员批次: ${members.length} 个成员`) for (const member of members) { insertMember.run( member.platformId, @@ -308,6 +357,13 @@ export async function streamImport(filePath: string, requestId: string): Promise }, onMessageBatch: (messages: ParsedMessage[]) => { + callbackStats.onMessageBatchCalls++ + callbackStats.totalMessagesReceived += messages.length + // 每收到 10 批消息记录一次日志 + if (callbackStats.onMessageBatchCalls <= 3 || callbackStats.onMessageBatchCalls % 10 === 0) { + logInfo(`收到消息批次 #${callbackStats.onMessageBatchCalls}: ${messages.length} 条消息`) + } + // 分阶段计时 let memberLookupTime = 0 let memberInsertTime = 0 @@ -318,14 +374,21 @@ export async function streamImport(filePath: string, requestId: string): Promise let nicknameChangeCount = 0 for (const msg of messages) { - // 数据验证:跳过无效消息 - if (!msg.senderPlatformId || !msg.senderAccountName) { + // 数据验证:跳过无效消息(带统计) + if (!msg.senderPlatformId) { + callbackStats.skippedNoSenderId++ + continue + } + if (!msg.senderAccountName) { + callbackStats.skippedNoAccountName++ continue } if (msg.timestamp === undefined || msg.timestamp === null || isNaN(msg.timestamp)) { + callbackStats.skippedInvalidTimestamp++ continue } if (msg.type === undefined || msg.type === null) { + callbackStats.skippedNoType++ continue } @@ -572,12 +635,32 @@ export async function streamImport(filePath: string, requestId: string): Promise logPerf('WAL checkpoint 完成', totalMessageCount) logPerf('导入完成', totalMessageCount) + // 记录解析器回调统计(诊断信息) + logInfo(`=== 解析器回调统计 ===`) + logInfo(`onProgress 调用次数: ${callbackStats.onProgressCalls}`) + logInfo(`onLog 调用次数: ${callbackStats.onLogCalls}`) + logInfo(`onMeta 调用次数: ${callbackStats.onMetaCalls}`) + logInfo(`onMembers 调用次数: ${callbackStats.onMembersCalls}, 总成员数: ${callbackStats.totalMembersReceived}`) + logInfo(`onMessageBatch 调用次数: ${callbackStats.onMessageBatchCalls}, 总消息数: ${callbackStats.totalMessagesReceived}`) + if ( + callbackStats.skippedNoSenderId > 0 || + callbackStats.skippedNoAccountName > 0 || + callbackStats.skippedInvalidTimestamp > 0 || + callbackStats.skippedNoType > 0 + ) { + logInfo(`=== 消息跳过统计 ===`) + if (callbackStats.skippedNoSenderId > 0) logInfo(` 无 senderPlatformId: ${callbackStats.skippedNoSenderId}`) + if (callbackStats.skippedNoAccountName > 0) logInfo(` 无 senderAccountName: ${callbackStats.skippedNoAccountName}`) + if (callbackStats.skippedInvalidTimestamp > 0) logInfo(` 无效 timestamp: ${callbackStats.skippedInvalidTimestamp}`) + if (callbackStats.skippedNoType > 0) logInfo(` 无 type: ${callbackStats.skippedNoType}`) + } + // 写入日志摘要 logSummary(totalMessageCount, memberIdMap.size) // 检查消息数量,如果为 0 则视为导入失败 if (totalMessageCount === 0) { - logError('导入失败:未解析到任何消息,可能是文件格式不匹配或内容为空') + logError(`导入失败:未解析到任何消息 (收到 ${callbackStats.totalMessagesReceived} 条消息,全部被跳过或未收到任何消息)`) // 标记需要删除数据库文件(将在 finally 中执行,确保数据库已关闭) shouldDeleteDb = true importError = 'error.no_messages' @@ -629,11 +712,30 @@ export async function streamImport(filePath: string, requestId: string): Promise } } + // 构造诊断信息 + const diagnostics: ImportDiagnostics = { + logFile: getCurrentLogFile(), + detectedFormat: formatFeature ? `${formatFeature.name} (${formatFeature.id})` : null, + messagesReceived: callbackStats.totalMessagesReceived, + messagesWritten: totalMessageCount, + messagesSkipped: + callbackStats.skippedNoSenderId + + callbackStats.skippedNoAccountName + + callbackStats.skippedInvalidTimestamp + + callbackStats.skippedNoType, + skipReasons: { + noSenderId: callbackStats.skippedNoSenderId, + noAccountName: callbackStats.skippedNoAccountName, + invalidTimestamp: callbackStats.skippedInvalidTimestamp, + noType: callbackStats.skippedNoType, + }, + } + // 返回结果(移到 try-catch-finally 之外) if (importError) { - return { success: false, error: importError } + return { success: false, error: importError, diagnostics } } - return { success: true, sessionId } + return { success: true, sessionId, diagnostics } } /** 流式解析文件信息的返回结果 */ diff --git a/electron/preload/index.d.ts b/electron/preload/index.d.ts index 77569fc..443f6e9 100644 --- a/electron/preload/index.d.ts +++ b/electron/preload/index.d.ts @@ -58,6 +58,27 @@ interface FormatDiagnosisSimple { }> } +// 导入诊断信息 +interface ImportDiagnostics { + /** 日志文件路径 */ + logFile: string | null + /** 检测到的格式 */ + detectedFormat: string | null + /** 收到的消息数 */ + messagesReceived: number + /** 写入的消息数 */ + messagesWritten: number + /** 跳过的消息数 */ + messagesSkipped: number + /** 跳过原因统计 */ + skipReasons: { + noSenderId: number + noAccountName: number + invalidTimestamp: number + noType: number + } +} + interface ChatApi { selectFile: () => Promise<{ filePath?: string @@ -70,6 +91,7 @@ interface ChatApi { sessionId?: string error?: string diagnosis?: FormatDiagnosisSimple + diagnostics?: ImportDiagnostics }> getSessions: () => Promise getSession: (sessionId: string) => Promise diff --git a/src/i18n/locales/en-US/home.json b/src/i18n/locales/en-US/home.json index c8c6bdf..3fd854d 100644 --- a/src/i18n/locales/en-US/home.json +++ b/src/i18n/locales/en-US/home.json @@ -41,6 +41,12 @@ "unrecognized_format": "Unrecognized file format", "no_messages": "No messages parsed. Please check if the file format is correct." }, + "diagnostics": { + "format": "Format: ", + "received": "Received: ", + "written": " / Written: ", + "skipped": " / Skipped: " + }, "batch": { "importing": "Batch importing", "progressCount": "{current} of {total}", diff --git a/src/i18n/locales/zh-CN/home.json b/src/i18n/locales/zh-CN/home.json index 860a534..6c7a552 100644 --- a/src/i18n/locales/zh-CN/home.json +++ b/src/i18n/locales/zh-CN/home.json @@ -41,6 +41,12 @@ "unrecognized_format": "无法识别的文件格式", "no_messages": "未解析到任何消息,请检查文件格式是否正确" }, + "diagnostics": { + "format": "检测格式:", + "received": "收到消息:", + "written": " / 写入:", + "skipped": " / 跳过:" + }, "batch": { "importing": "正在批量导入", "progressCount": "第 {current} 个 / 共 {total} 个", diff --git a/src/pages/home/components/ImportArea.vue b/src/pages/home/components/ImportArea.vue index 2fab69b..ba62fc7 100644 --- a/src/pages/home/components/ImportArea.vue +++ b/src/pages/home/components/ImportArea.vue @@ -26,6 +26,13 @@ const { const importError = ref(null) const diagnosisSuggestion = ref(null) const hasImportLog = ref(false) +const importDiagnostics = ref<{ + logFile: string | null + detectedFormat: string | null + messagesReceived: number + messagesWritten: number + messagesSkipped: number +} | null>(null) const router = useRouter() @@ -88,6 +95,7 @@ async function handleClickImport() { importError.value = null diagnosisSuggestion.value = null hasImportLog.value = false + importDiagnostics.value = null // 使用系统对话框选择多个文件 const result = await window.api.dialog.showOpenDialog({ @@ -116,6 +124,7 @@ async function handleFileDrop({ paths }: { files: File[]; paths: string[] }) { importError.value = null diagnosisSuggestion.value = null hasImportLog.value = false + importDiagnostics.value = null await processFilePaths(paths) } @@ -132,7 +141,20 @@ async function processFilePaths(paths: string[]) { if (result.diagnosisSuggestion) { diagnosisSuggestion.value = result.diagnosisSuggestion } - await checkImportLog() + // 保存诊断信息 + if (result.diagnostics) { + importDiagnostics.value = { + logFile: result.diagnostics.logFile, + detectedFormat: result.diagnostics.detectedFormat, + messagesReceived: result.diagnostics.messagesReceived, + messagesWritten: result.diagnostics.messagesWritten, + messagesSkipped: result.diagnostics.messagesSkipped, + } + // 如果有日志文件,显示查看日志按钮 + hasImportLog.value = !!result.diagnostics.logFile + } else { + await checkImportLog() + } } else if (result.success && sessionStore.currentSessionId) { await navigateToSession(sessionStore.currentSessionId) } @@ -559,6 +581,25 @@ const getMergeFileProgressText = (file: MergeFileInfo) => {{ importError }} + +
+
+ +
+
+ {{ t('home.import.diagnostics.format') }}{{ importDiagnostics.detectedFormat }} +
+
+ {{ t('home.import.diagnostics.received') }}{{ importDiagnostics.messagesReceived }} + {{ t('home.import.diagnostics.written') }}{{ importDiagnostics.messagesWritten }} + {{ t('home.import.diagnostics.skipped') }}{{ importDiagnostics.messagesSkipped }} +
+
+
+
{ try { isImporting.value = true @@ -278,7 +294,7 @@ export const useSessionStore = defineStore( // 不阻断导入流程,用户可以手动生成 } - return { success: true } + return { success: true, diagnostics: importResult.diagnostics } } else { // 传递诊断信息(如果有) const diagnosisSuggestion = importResult.diagnosis?.suggestion @@ -286,6 +302,7 @@ export const useSessionStore = defineStore( success: false, error: importResult.error || 'error.import_failed', diagnosisSuggestion, + diagnostics: importResult.diagnostics, } } } catch (error) {