diff --git a/electron/main/ipc/chat.ts b/electron/main/ipc/chat.ts index 75486e1..8fd31a9 100644 --- a/electron/main/ipc/chat.ts +++ b/electron/main/ipc/chat.ts @@ -6,7 +6,7 @@ import { ipcMain, app, dialog } from 'electron' import * as databaseCore from '../database/core' import * as worker from '../worker/workerManager' import * as parser from '../parser' -import { detectFormat, type ParseProgress } from '../parser' +import { detectFormat, diagnoseFormat, type ParseProgress } from '../parser' import type { IpcContext } from './types' import { CURRENT_SCHEMA_VERSION, getPendingMigrationInfos, type MigrationInfo } from '../database/migrations' @@ -74,14 +74,24 @@ export function registerChatHandlers(ctx: IpcContext): void { } const filePath = filePaths[0] - console.log('[IpcMain] File selected:', filePath) // 检测文件格式(使用流式检测,只读取文件开头) const formatFeature = detectFormat(filePath) const format = formatFeature?.name || null - console.log('[IpcMain] Detected format:', format) if (!format) { - return { error: 'error.unrecognized_format' } + // 使用诊断功能获取详细的错误信息 + const diagnosis = diagnoseFormat(filePath) + // 返回详细的错误信息 + return { + error: 'error.unrecognized_format', + diagnosis: { + suggestion: diagnosis.suggestion, + partialMatches: diagnosis.partialMatches.map((m) => ({ + formatName: m.formatName, + missingFields: m.missingFields, + })), + }, + } } return { filePath, format } @@ -95,8 +105,6 @@ export function registerChatHandlers(ctx: IpcContext): void { * 导入聊天记录(流式版本) */ ipcMain.handle('chat:import', async (_, filePath: string) => { - console.log('[IpcMain] chat:import called with:', filePath) - try { // Send progress: detecting format (message not used by frontend, stage-based translation) win.webContents.send('chat:importProgress', { @@ -128,6 +136,23 @@ export function registerChatHandlers(ctx: IpcContext): void { progress: 0, message: result.error, }) + + // 如果是格式不识别错误,提供诊断信息 + if (result.error === 'error.unrecognized_format') { + const diagnosis = diagnoseFormat(filePath) + return { + success: false, + error: result.error, + diagnosis: { + suggestion: diagnosis.suggestion, + partialMatches: diagnosis.partialMatches.map((m) => ({ + formatName: m.formatName, + missingFields: m.missingFields, + })), + }, + } + } + return { success: false, error: result.error } } } catch (error) { diff --git a/electron/main/parser/index.ts b/electron/main/parser/index.ts index 46707cb..0b1a593 100644 --- a/electron/main/parser/index.ts +++ b/electron/main/parser/index.ts @@ -16,6 +16,7 @@ import type { ParsedMeta, ParsedMember, ParsedMessage, + FormatDiagnosis, } from './types' // ==================== 全局嗅探器实例 ==================== @@ -34,6 +35,16 @@ export function detectFormat(filePath: string): FormatFeature | null { return sniffer.sniff(filePath) } +/** + * 诊断文件格式 + * 当检测失败时,返回详细的诊断信息,帮助用户了解问题所在 + * @param filePath 文件路径 + * @returns 诊断结果,包含每个格式的匹配详情和建议 + */ +export function diagnoseFormat(filePath: string): FormatDiagnosis { + return sniffer.diagnose(filePath) +} + /** * 获取文件对应的解析器 * @param filePath 文件路径 @@ -199,6 +210,7 @@ export type { ParsedMeta, ParsedMember, ParsedMessage, + FormatDiagnosis, } // ==================== 导出嗅探器(高级用法) ==================== diff --git a/electron/main/parser/sniffer.ts b/electron/main/parser/sniffer.ts index d3b6efb..052c5c7 100644 --- a/electron/main/parser/sniffer.ts +++ b/electron/main/parser/sniffer.ts @@ -5,7 +5,7 @@ import * as fs from 'fs' import * as path from 'path' -import type { FormatFeature, FormatModule, Parser } from './types' +import type { FormatFeature, FormatModule, Parser, FormatMatchCheck, FormatDiagnosis } from './types' /** 文件头检测大小 (8KB) */ const HEAD_SIZE = 8 * 1024 @@ -47,6 +47,29 @@ function matchRequiredFields(headContent: string, fields: string[]): boolean { }) } +/** + * 检查必需字段并返回详细结果 + */ +function checkRequiredFieldsDetail( + headContent: string, + fields: string[] +): { allMatch: boolean; missing: string[] } { + const missing: string[] = [] + + for (const field of fields) { + const pattern = new RegExp(`"${field.replace('.', '"\\s*:\\s*.*"')}"\\s*:`) + const found = pattern.test(headContent) || headContent.includes(`"${field}"`) + if (!found) { + missing.push(field) + } + } + + return { + allMatch: missing.length === 0, + missing, + } +} + /** * 格式嗅探器 * 管理所有格式特征,负责检测文件格式 @@ -123,6 +146,135 @@ export class FormatSniffer { return this.formats.map((m) => m.feature) } + /** + * 诊断文件格式 + * 返回详细的匹配信息,用于提供更好的错误提示 + * @param filePath 文件路径 + * @returns 诊断结果,包含每个格式的匹配详情 + */ + diagnose(filePath: string): FormatDiagnosis { + const ext = getExtension(filePath) + const headContent = readFileHead(filePath) + + const checks: FormatMatchCheck[] = [] + const partialMatches: FormatMatchCheck[] = [] + let matchedFormat: FormatFeature | null = null + + for (const { feature } of this.formats) { + const check = this.checkFeatureDetail(feature, ext, headContent) + checks.push(check) + + if (check.fullMatch && !matchedFormat) { + matchedFormat = feature + } else if (check.extensionMatch && !check.fullMatch) { + partialMatches.push(check) + } + } + + // 生成诊断建议 + const suggestion = this.generateSuggestion(ext, partialMatches, headContent) + + return { + recognized: matchedFormat !== null, + matchedFormat, + checks, + partialMatches, + suggestion, + } + } + + /** + * 检查单个格式的匹配详情 + */ + private checkFeatureDetail(feature: FormatFeature, ext: string, headContent: string): FormatMatchCheck { + const result: FormatMatchCheck = { + formatId: feature.id, + formatName: feature.name, + extensionMatch: feature.extensions.includes(ext), + headSignatureMatch: null, + requiredFieldsMatch: null, + missingFields: [], + fullMatch: false, + } + + // 扩展名不匹配,直接返回 + if (!result.extensionMatch) { + return result + } + + const { signatures } = feature + + // 检查文件头签名 + if (signatures.head && signatures.head.length > 0) { + result.headSignatureMatch = matchHeadSignatures(headContent, signatures.head) + } + + // 检查必需字段 + if (signatures.requiredFields && signatures.requiredFields.length > 0) { + const { allMatch, missing } = checkRequiredFieldsDetail(headContent, signatures.requiredFields) + result.requiredFieldsMatch = allMatch + result.missingFields = missing + } + + // 检查字段值模式 + let fieldPatternsMatch = true + if (signatures.fieldPatterns) { + for (const [, pattern] of Object.entries(signatures.fieldPatterns)) { + if (!pattern.test(headContent)) { + fieldPatternsMatch = false + break + } + } + } + + // 判断是否完全匹配 + result.fullMatch = + result.extensionMatch && + (result.headSignatureMatch === null || result.headSignatureMatch) && + (result.requiredFieldsMatch === null || result.requiredFieldsMatch) && + fieldPatternsMatch + + return result + } + + /** + * 生成诊断建议信息 + */ + private generateSuggestion(ext: string, partialMatches: FormatMatchCheck[], headContent: string): string { + if (partialMatches.length === 0) { + return `没有找到匹配扩展名 "${ext}" 的格式,请检查文件类型是否正确` + } + + // 找到最可能的格式(按优先级排序后的第一个部分匹配) + const mostLikely = partialMatches[0] + + // 构建详细的建议信息 + const issues: string[] = [] + + if (mostLikely.headSignatureMatch === false) { + issues.push('文件头签名不匹配') + } + + if (mostLikely.missingFields.length > 0) { + issues.push(`缺少必需字段: ${mostLikely.missingFields.join(', ')}`) + } + + if (issues.length > 0) { + return `文件疑似 ${mostLikely.formatName} 格式,但存在以下问题:${issues.join(';')}` + } + + // 如果是 JSON 文件,提供额外提示 + if (ext === '.json') { + // 检查文件头是否能看到有效的 JSON 结构 + const trimmed = headContent.trim() + if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) { + return '文件内容不是有效的 JSON 格式' + } + } + + return `扩展名匹配 ${mostLikely.formatName} 格式,但内容结构不符合预期` + } + /** * 检查特征是否匹配 */ diff --git a/electron/main/parser/types.ts b/electron/main/parser/types.ts index 4fc7a92..116846d 100644 --- a/electron/main/parser/types.ts +++ b/electron/main/parser/types.ts @@ -147,6 +147,44 @@ export interface FormatModule { preprocessor?: Preprocessor } +// ==================== 诊断结果类型 ==================== + +/** + * 单个格式的匹配检查结果 + */ +export interface FormatMatchCheck { + /** 格式 ID */ + formatId: string + /** 格式显示名称 */ + formatName: string + /** 扩展名是否匹配 */ + extensionMatch: boolean + /** 文件头签名是否匹配(如果定义了) */ + headSignatureMatch: boolean | null + /** 必需字段是否匹配(如果定义了) */ + requiredFieldsMatch: boolean | null + /** 缺失的必需字段(如果有) */ + missingFields: string[] + /** 是否完全匹配 */ + fullMatch: boolean +} + +/** + * 格式诊断结果 + */ +export interface FormatDiagnosis { + /** 是否成功识别到格式 */ + recognized: boolean + /** 识别到的格式(如果有) */ + matchedFormat: FormatFeature | null + /** 所有格式的检查详情 */ + checks: FormatMatchCheck[] + /** 部分匹配的格式(扩展名匹配但内容不匹配) */ + partialMatches: FormatMatchCheck[] + /** 诊断建议信息 */ + suggestion: string +} + // ==================== 工具类型 ==================== /** diff --git a/electron/preload/index.d.ts b/electron/preload/index.d.ts index c2818a9..abd6232 100644 --- a/electron/preload/index.d.ts +++ b/electron/preload/index.d.ts @@ -41,9 +41,28 @@ interface MigrationCheckResult { pendingMigrations: MigrationInfo[] } +// 格式诊断信息(简化版,用于前端显示) +interface FormatDiagnosisSimple { + suggestion: string + partialMatches: Array<{ + formatName: string + missingFields: string[] + }> +} + interface ChatApi { - selectFile: () => Promise<{ filePath?: string; format?: string; error?: string } | null> - import: (filePath: string) => Promise<{ success: boolean; sessionId?: string; error?: string }> + selectFile: () => Promise<{ + filePath?: string + format?: string + error?: string + diagnosis?: FormatDiagnosisSimple + } | null> + import: (filePath: string) => Promise<{ + success: boolean + sessionId?: string + error?: string + diagnosis?: FormatDiagnosisSimple + }> getSessions: () => Promise getSession: (sessionId: string) => Promise deleteSession: (sessionId: string) => Promise diff --git a/src/pages/home/components/ImportArea.vue b/src/pages/home/components/ImportArea.vue index 3e491bf..0caac9d 100644 --- a/src/pages/home/components/ImportArea.vue +++ b/src/pages/home/components/ImportArea.vue @@ -11,6 +11,7 @@ const sessionStore = useSessionStore() const { isImporting, importProgress } = storeToRefs(sessionStore) const importError = ref(null) +const diagnosisSuggestion = ref(null) const hasImportLog = ref(false) const router = useRouter() @@ -48,11 +49,16 @@ async function checkImportLog() { // 处理文件选择(点击选择) async function handleClickImport() { importError.value = null + diagnosisSuggestion.value = null hasImportLog.value = false const result = await sessionStore.importFile() // Skip showing error if user just cancelled the file dialog if (!result.success && result.error && result.error !== 'error.no_file_selected') { importError.value = translateError(result.error) + // 保存诊断建议(如果有) + if (result.diagnosisSuggestion) { + diagnosisSuggestion.value = result.diagnosisSuggestion + } await checkImportLog() } else if (result.success && sessionStore.currentSessionId) { await navigateToSession(sessionStore.currentSessionId) @@ -67,10 +73,15 @@ async function handleFileDrop({ paths }: { files: File[]; paths: string[] }) { } importError.value = null + diagnosisSuggestion.value = null hasImportLog.value = false const result = await sessionStore.importFileFromPath(paths[0]) if (!result.success && result.error) { importError.value = translateError(result.error) + // 保存诊断建议(如果有) + if (result.diagnosisSuggestion) { + diagnosisSuggestion.value = result.diagnosisSuggestion + } await checkImportLog() } else if (result.success && sessionStore.currentSessionId) { await navigateToSession(sessionStore.currentSessionId) @@ -187,11 +198,24 @@ function getProgressDetail(): string { -
+
{{ importError }}
+ +
+
+ + {{ diagnosisSuggestion }} +
+
{{ t('home.import.viewLog') }}