mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-21 22:00:39 +08:00
feat: 移除诊断建议,并新增提示
This commit is contained in:
@@ -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 分析
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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} 格式,但内容结构不符合预期`
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查特征是否匹配
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
// ==================== 工具类型 ====================
|
||||
|
||||
/**
|
||||
|
||||
Vendored
-11
@@ -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: (
|
||||
|
||||
@@ -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: ",
|
||||
|
||||
@@ -39,7 +39,8 @@
|
||||
"no_file_selected": "ファイルが選択されていません",
|
||||
"import_failed": "インポートに失敗しました",
|
||||
"unrecognized_format": "認識できないファイル形式です",
|
||||
"no_messages": "メッセージを検出できませんでした。ファイル形式が正しいか確認してください"
|
||||
"no_messages": "メッセージを検出できませんでした。ファイル形式が正しいか確認してください",
|
||||
"actionHint": "下記の手動マッチングまたはログの確認をお試しください:"
|
||||
},
|
||||
"diagnostics": {
|
||||
"format": "検出フォーマット:",
|
||||
|
||||
@@ -39,7 +39,8 @@
|
||||
"no_file_selected": "未选择文件",
|
||||
"import_failed": "导入失败",
|
||||
"unrecognized_format": "无法识别的文件格式",
|
||||
"no_messages": "未解析到任何消息,请检查文件格式是否正确"
|
||||
"no_messages": "未解析到任何消息,请检查文件格式是否正确",
|
||||
"actionHint": "你还可以尝试下方手动匹配或查看日志:"
|
||||
},
|
||||
"diagnostics": {
|
||||
"format": "检测格式:",
|
||||
|
||||
@@ -39,7 +39,8 @@
|
||||
"no_file_selected": "未選擇檔案",
|
||||
"import_failed": "匯入失敗",
|
||||
"unrecognized_format": "無法識別的檔案格式",
|
||||
"no_messages": "未解析到任何訊息,請檢查檔案格式是否正確"
|
||||
"no_messages": "未解析到任何訊息,請檢查檔案格式是否正確",
|
||||
"actionHint": "你還可以嘗試下方手動匹配或查看日誌:"
|
||||
},
|
||||
"diagnostics": {
|
||||
"format": "偵測格式:",
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user