feat: 移除诊断建议,并新增提示

This commit is contained in:
digua
2026-04-10 21:18:20 +08:00
parent b8a0e9e8da
commit 62b4816060
11 changed files with 22 additions and 286 deletions
+3 -38
View File
@@ -7,7 +7,7 @@ import { getConversationCountsBySession } from '../ai/conversations'
import * as databaseCore from '../database/core'
import * as worker from '../worker/workerManager'
import * as parser from '../parser'
import { detectFormat, diagnoseFormat, scanMultiChatFile, type ParseProgress } from '../parser'
import { detectFormat, scanMultiChatFile, type ParseProgress } from '../parser'
import type { IpcContext } from './types'
import { CURRENT_SCHEMA_VERSION, getPendingMigrationInfos } from '../database/migrations'
import { exportSessionToTempFile, cleanupTempExportFiles } from '../merger'
@@ -82,19 +82,7 @@ export function registerChatHandlers(ctx: IpcContext): void {
const formatFeature = detectFormat(filePath)
const format = formatFeature?.name || null
if (!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 { error: 'error.unrecognized_format' }
}
return { filePath, format }
@@ -140,23 +128,6 @@ export function registerChatHandlers(ctx: IpcContext): void {
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,
})),
},
diagnostics: result.diagnostics,
}
}
return { success: false, error: result.error, diagnostics: result.diagnostics }
}
} catch (error) {
@@ -1008,13 +979,7 @@ export function registerChatHandlers(ctx: IpcContext): void {
// 检测文件格式
const formatFeature = detectFormat(filePath)
if (!formatFeature) {
const diagnosis = diagnoseFormat(filePath)
return {
error: 'error.unrecognized_format',
diagnosis: {
suggestion: diagnosis.suggestion,
},
}
return { error: 'error.unrecognized_format' }
}
// 使用 Worker 分析
-12
View File
@@ -17,7 +17,6 @@ import type {
ParsedMeta,
ParsedMember,
ParsedMessage,
FormatDiagnosis,
MultiChatInfo,
} from './types'
@@ -47,16 +46,6 @@ export function detectAllFormats(filePath: string): FormatFeature[] {
return sniffer.sniffAll(filePath)
}
/**
* 诊断文件格式
* 当检测失败时,返回详细的诊断信息,帮助用户了解问题所在
* @param filePath 文件路径
* @returns 诊断结果,包含每个格式的匹配详情和建议
*/
export function diagnoseFormat(filePath: string): FormatDiagnosis {
return sniffer.diagnose(filePath)
}
/**
* 获取文件对应的解析器
* @param filePath 文件路径
@@ -275,7 +264,6 @@ export type {
ParsedMeta,
ParsedMember,
ParsedMessage,
FormatDiagnosis,
MultiChatInfo,
}
+1 -150
View File
@@ -5,7 +5,7 @@
import * as fs from 'fs'
import * as path from 'path'
import type { FormatFeature, FormatModule, Parser, FormatMatchCheck, FormatDiagnosis } from './types'
import type { FormatFeature, FormatModule, Parser } from './types'
/** 文件头检测大小 (64KB) - 考虑到现代聊天记录文件可能包含 base64 头像等大数据 */
const HEAD_SIZE = 64 * 1024
@@ -55,26 +55,6 @@ 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,
}
}
/**
* 格式嗅探器
* 管理所有格式特征,负责检测文件格式
@@ -191,135 +171,6 @@ 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} 格式,但内容结构不符合预期`
}
/**
* 检查特征是否匹配
*/
-38
View File
@@ -175,44 +175,6 @@ export interface FormatModule {
scanChats?: (filePath: string) => Promise<MultiChatInfo[]>
}
// ==================== 诊断结果类型 ====================
/**
* 单个格式的匹配检查结果
*/
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
}
// ==================== 工具类型 ====================
/**
-11
View File
@@ -46,15 +46,6 @@ interface MigrationCheckResult {
pendingMigrations: MigrationInfo[]
}
// 格式诊断信息(简化版,用于前端显示)
interface FormatDiagnosisSimple {
suggestion: string
partialMatches: Array<{
formatName: string
missingFields: string[]
}>
}
// 导入诊断信息
interface ImportDiagnostics {
/** 日志文件路径 */
@@ -81,14 +72,12 @@ interface ChatApi {
filePath?: string
format?: string
error?: string
diagnosis?: FormatDiagnosisSimple
} | null>
detectFormat: (filePath: string) => Promise<{ id: string; name: string; platform: string; multiChat: boolean } | null>
import: (filePath: string) => Promise<{
success: boolean
sessionId?: string
error?: string
diagnosis?: FormatDiagnosisSimple
diagnostics?: ImportDiagnostics
}>
importWithOptions: (
+2 -1
View File
@@ -39,7 +39,8 @@
"no_file_selected": "No file selected",
"import_failed": "Import failed",
"unrecognized_format": "Unrecognized file format",
"no_messages": "No messages parsed. Please check if the file format is correct."
"no_messages": "No messages parsed. Please check if the file format is correct.",
"actionHint": "You can try manual format matching or view the log below:"
},
"diagnostics": {
"format": "Format: ",
+2 -1
View File
@@ -39,7 +39,8 @@
"no_file_selected": "ファイルが選択されていません",
"import_failed": "インポートに失敗しました",
"unrecognized_format": "認識できないファイル形式です",
"no_messages": "メッセージを検出できませんでした。ファイル形式が正しいか確認してください"
"no_messages": "メッセージを検出できませんでした。ファイル形式が正しいか確認してください",
"actionHint": "下記の手動マッチングまたはログの確認をお試しください:"
},
"diagnostics": {
"format": "検出フォーマット:",
+2 -1
View File
@@ -39,7 +39,8 @@
"no_file_selected": "未选择文件",
"import_failed": "导入失败",
"unrecognized_format": "无法识别的文件格式",
"no_messages": "未解析到任何消息,请检查文件格式是否正确"
"no_messages": "未解析到任何消息,请检查文件格式是否正确",
"actionHint": "你还可以尝试下方手动匹配或查看日志:"
},
"diagnostics": {
"format": "检测格式:",
+2 -1
View File
@@ -39,7 +39,8 @@
"no_file_selected": "未選擇檔案",
"import_failed": "匯入失敗",
"unrecognized_format": "無法識別的檔案格式",
"no_messages": "未解析到任何訊息,請檢查檔案格式是否正確"
"no_messages": "未解析到任何訊息,請檢查檔案格式是否正確",
"actionHint": "你還可以嘗試下方手動匹配或查看日誌:"
},
"diagnostics": {
"format": "偵測格式:",
+9 -24
View File
@@ -46,7 +46,6 @@ async function autoGenerateSessionIndex(sessionId: string) {
}
const importError = ref<string | null>(null)
const diagnosisSuggestion = ref<string | null>(null)
const hasImportLog = ref(false)
const importDiagnostics = ref<{
logFile: string | null
@@ -115,7 +114,6 @@ async function checkImportLog() {
// 处理文件选择(点击选择)- 支持多选
async function handleClickImport() {
importError.value = null
diagnosisSuggestion.value = null
hasImportLog.value = false
importDiagnostics.value = null
@@ -144,7 +142,6 @@ async function handleFileDrop({ paths }: { files: File[]; paths: string[] }) {
}
importError.value = null
diagnosisSuggestion.value = null
hasImportLog.value = false
importDiagnostics.value = null
@@ -172,9 +169,6 @@ async function processFilePaths(paths: string[]) {
if (result.error === 'error.unrecognized_format') {
formatSelectorFilePath.value = paths[0]
}
if (result.diagnosisSuggestion) {
diagnosisSuggestion.value = result.diagnosisSuggestion
}
// 保存诊断信息
if (result.diagnostics) {
importDiagnostics.value = {
@@ -209,7 +203,6 @@ async function handleFormatSelect(formatId: string) {
if (!filePath) return
importError.value = null
diagnosisSuggestion.value = null
importDiagnostics.value = null
isImporting.value = true
importProgress.value = { stage: 'detecting', progress: 0, message: '' }
@@ -785,27 +778,19 @@ const getMergeFileProgressText = (file: MergeFileInfo) =>
</div>
</div>
</div>
<!-- 诊断建议如果有 -->
<div
v-if="diagnosisSuggestion"
class="w-full rounded-md bg-amber-50 px-3 py-2 text-sm text-amber-800 dark:bg-amber-900/30 dark:text-amber-200"
<p
v-if="formatSelectorFilePath || hasImportLog"
class="text-xs text-gray-500 dark:text-gray-400"
>
<div class="flex items-start gap-2">
<UIcon name="i-heroicons-light-bulb" class="mt-0.5 h-4 w-4 shrink-0" />
<span>{{ diagnosisSuggestion }}</span>
</div>
</div>
{{ t('home.import.errors.actionHint') }}
</p>
<div class="flex gap-2">
<UButton v-if="hasImportLog" size="xs" @click="openLatestImportLog">{{ t('home.import.viewLog') }}</UButton>
<UButton
v-if="formatSelectorFilePath"
size="xs"
variant="soft"
icon="i-heroicons-list-bullet"
@click="showFormatSelector = true"
>
<UButton v-if="formatSelectorFilePath" size="xs" @click="showFormatSelector = true">
{{ t('home.formatSelector.manualSelect') }}
</UButton>
<UButton v-if="hasImportLog" size="xs" variant="soft" @click="openLatestImportLog">
{{ t('home.import.viewLog') }}
</UButton>
</div>
</div>
+1 -9
View File
@@ -29,7 +29,6 @@ export interface BatchFileInfo {
status: BatchFileStatus
progress?: ImportProgress
error?: string
diagnosisSuggestion?: string
sessionId?: string
}
@@ -171,7 +170,6 @@ export const useSessionStore = defineStore(
async function importFile(): Promise<{
success: boolean
error?: string
diagnosisSuggestion?: string
}> {
try {
const result = await window.chatApi.selectFile()
@@ -181,8 +179,7 @@ export const useSessionStore = defineStore(
}
// 有错误(如格式不识别)- 优先检查错误,因为此时可能没有 filePath
if (result.error) {
const diagnosisSuggestion = result.diagnosis?.suggestion
return { success: false, error: result.error, diagnosisSuggestion }
return { success: false, error: result.error }
}
// 没有文件路径(用户取消)
if (!result.filePath) {
@@ -215,7 +212,6 @@ export const useSessionStore = defineStore(
async function importFileFromPath(filePath: string): Promise<{
success: boolean
error?: string
diagnosisSuggestion?: string
diagnostics?: ImportDiagnosticsInfo
}> {
try {
@@ -296,12 +292,9 @@ export const useSessionStore = defineStore(
return { success: true, diagnostics: importResult.diagnostics }
} else {
// 传递诊断信息(如果有)
const diagnosisSuggestion = importResult.diagnosis?.suggestion
return {
success: false,
error: importResult.error || 'error.import_failed',
diagnosisSuggestion,
diagnostics: importResult.diagnostics,
}
}
@@ -460,7 +453,6 @@ export const useSessionStore = defineStore(
} else {
file.status = 'failed'
file.error = importResult.error || 'error.import_failed'
file.diagnosisSuggestion = importResult.diagnosis?.suggestion
failedCount++
}
} catch (error) {