From b8a0e9e8da8ca1c44d4f6247442fa1951e8786ae Mon Sep 17 00:00:00 2001 From: digua Date: Fri, 10 Apr 2026 20:58:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E6=8C=87=E5=AE=9A?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/main/parser/index.ts | 9 + electron/main/worker/import/streamImport.ts | 11 ++ electron/preload/apis/chat.ts | 4 +- electron/preload/index.d.ts | 2 +- src/i18n/locales/en-US/home.json | 6 + src/i18n/locales/ja-JP/home.json | 6 + src/i18n/locales/zh-CN/home.json | 6 + src/i18n/locales/zh-TW/home.json | 6 + .../home/components/FormatSelectorModal.vue | 174 ++++++++++++++++++ src/pages/home/components/ImportArea.vue | 72 +++++++- 10 files changed, 293 insertions(+), 3 deletions(-) create mode 100644 src/pages/home/components/FormatSelectorModal.vue diff --git a/electron/main/parser/index.ts b/electron/main/parser/index.ts index 97b50d7..8717532 100644 --- a/electron/main/parser/index.ts +++ b/electron/main/parser/index.ts @@ -73,6 +73,15 @@ export function getSupportedFormats(): FormatFeature[] { return sniffer.getSupportedFormats() } +/** + * 根据格式 ID 获取格式特征 + * 用于手动指定格式时跳过嗅探 + */ +export function getFormatFeatureById(formatId: string): FormatFeature | null { + const all = sniffer.getSupportedFormats() + return all.find((f) => f.id === formatId) ?? null +} + /** * 获取格式的预处理器(如果有) */ diff --git a/electron/main/worker/import/streamImport.ts b/electron/main/worker/import/streamImport.ts index 89130a2..99858bc 100644 --- a/electron/main/worker/import/streamImport.ts +++ b/electron/main/worker/import/streamImport.ts @@ -10,6 +10,7 @@ import { streamParseFile, detectFormat, detectAllFormats, + getFormatFeatureById, getPreprocessor, needsPreprocess, type ParsedMeta, @@ -144,6 +145,16 @@ export async function streamImport( requestId: string, formatOptions?: Record ): Promise { + // 用户手动指定格式时,跳过自动检测直接使用指定的 Parser + if (formatOptions?.formatId) { + const formatId = formatOptions.formatId as string + const feature = getFormatFeatureById(formatId) + if (!feature) { + return { success: false, error: 'error.unknown_format_id' } + } + return streamImportSingle(filePath, requestId, feature, formatOptions) + } + // 检测所有匹配的格式(按优先级排序) const candidates = detectAllFormats(filePath) if (candidates.length === 0) { diff --git a/electron/preload/apis/chat.ts b/electron/preload/apis/chat.ts index 92760d1..69e6804 100644 --- a/electron/preload/apis/chat.ts +++ b/electron/preload/apis/chat.ts @@ -226,7 +226,9 @@ export const chatApi = { /** * 获取支持的格式列表 */ - getSupportedFormats: (): Promise> => { + getSupportedFormats: (): Promise< + Array<{ id: string; name: string; platform: string; extensions: string[] }> + > => { return ipcRenderer.invoke('chat:getSupportedFormats') }, diff --git a/electron/preload/index.d.ts b/electron/preload/index.d.ts index 67c3c4f..651b276 100644 --- a/electron/preload/index.d.ts +++ b/electron/preload/index.d.ts @@ -135,7 +135,7 @@ interface ChatApi { ) => Promise> getTimeRange: (sessionId: string) => Promise<{ start: number; end: number } | null> getDbDirectory: () => Promise - getSupportedFormats: () => Promise> + getSupportedFormats: () => Promise> onImportProgress: (callback: (progress: ImportProgress) => void) => () => void getCatchphraseAnalysis: (sessionId: string, filter?: TimeFilter) => Promise getMentionAnalysis: (sessionId: string, filter?: TimeFilter) => Promise diff --git a/src/i18n/locales/en-US/home.json b/src/i18n/locales/en-US/home.json index b10e5fe..66d929a 100644 --- a/src/i18n/locales/en-US/home.json +++ b/src/i18n/locales/en-US/home.json @@ -100,6 +100,12 @@ "import": "Import ({count})", "noChats": "No chats found" }, + "formatSelector": { + "title": "Select Chat Format", + "hint": "Unable to auto-detect the file format. Please manually select the corresponding chat format.", + "manualSelect": "Select format manually", + "confirm": "Import with this format" + }, "footer": { "website": "Website", "terms": "Terms of Use" diff --git a/src/i18n/locales/ja-JP/home.json b/src/i18n/locales/ja-JP/home.json index 595bd19..7f46230 100644 --- a/src/i18n/locales/ja-JP/home.json +++ b/src/i18n/locales/ja-JP/home.json @@ -100,6 +100,12 @@ "import": "インポート ({count})", "noChats": "チャットが見つかりません" }, + "formatSelector": { + "title": "チャット形式を選択", + "hint": "ファイル形式を自動検出できませんでした。対応するチャット形式を手動で選択してください。", + "manualSelect": "形式を手動で選択", + "confirm": "この形式でインポート" + }, "footer": { "website": "公式サイト", "terms": "利用規約" diff --git a/src/i18n/locales/zh-CN/home.json b/src/i18n/locales/zh-CN/home.json index fb12106..729f912 100644 --- a/src/i18n/locales/zh-CN/home.json +++ b/src/i18n/locales/zh-CN/home.json @@ -100,6 +100,12 @@ "import": "导入 ({count})", "noChats": "未发现任何聊天" }, + "formatSelector": { + "title": "选择聊天记录格式", + "hint": "无法自动识别此文件的格式,请手动选择对应的聊天记录格式。", + "manualSelect": "手动选择格式", + "confirm": "使用此格式导入" + }, "footer": { "website": "官网", "terms": "使用条款" diff --git a/src/i18n/locales/zh-TW/home.json b/src/i18n/locales/zh-TW/home.json index b7088fb..9faf80a 100644 --- a/src/i18n/locales/zh-TW/home.json +++ b/src/i18n/locales/zh-TW/home.json @@ -100,6 +100,12 @@ "import": "匯入 ({count})", "noChats": "未發現任何聊天" }, + "formatSelector": { + "title": "選擇聊天記錄格式", + "hint": "無法自動識別此檔案的格式,請手動選擇對應的聊天記錄格式。", + "manualSelect": "手動選擇格式", + "confirm": "使用此格式匯入" + }, "footer": { "website": "官網", "terms": "使用條款" diff --git a/src/pages/home/components/FormatSelectorModal.vue b/src/pages/home/components/FormatSelectorModal.vue new file mode 100644 index 0000000..8d43b00 --- /dev/null +++ b/src/pages/home/components/FormatSelectorModal.vue @@ -0,0 +1,174 @@ + + + diff --git a/src/pages/home/components/ImportArea.vue b/src/pages/home/components/ImportArea.vue index 2ee1453..52e8f89 100644 --- a/src/pages/home/components/ImportArea.vue +++ b/src/pages/home/components/ImportArea.vue @@ -2,6 +2,7 @@ import { FileDropZone } from '@/components/UI' import FileListItem from './FileListItem.vue' import ChatSelector, { type ChatInfo } from './ChatSelector.vue' +import FormatSelectorModal from './FormatSelectorModal.vue' import { storeToRefs } from 'pinia' import { ref, computed } from 'vue' import { useRouter } from 'vue-router' @@ -29,6 +30,10 @@ const { const showChatSelector = ref(false) const chatSelectorFilePath = ref('') +// 格式选择器状态(自动检测失败时的手动兜底) +const showFormatSelector = ref(false) +const formatSelectorFilePath = ref('') + // 自动生成会话索引(与 importFileFromPath 保持一致) async function autoGenerateSessionIndex(sessionId: string) { try { @@ -163,6 +168,10 @@ async function processFilePaths(paths: string[]) { const result = await sessionStore.importFileFromPath(paths[0]) if (!result.success && result.error) { importError.value = translateError(result.error) + // 格式无法识别时,记住文件路径以便手动选择格式 + if (result.error === 'error.unrecognized_format') { + formatSelectorFilePath.value = paths[0] + } if (result.diagnosisSuggestion) { diagnosisSuggestion.value = result.diagnosisSuggestion } @@ -194,6 +203,49 @@ async function processFilePaths(paths: string[]) { await sessionStore.mergeImportFiles(paths) } +// 手动格式选择后的导入处理 +async function handleFormatSelect(formatId: string) { + const filePath = formatSelectorFilePath.value + if (!filePath) return + + importError.value = null + diagnosisSuggestion.value = null + importDiagnostics.value = null + isImporting.value = true + importProgress.value = { stage: 'detecting', progress: 0, message: '' } + + const unsubscribe = window.chatApi.onImportProgress((progress) => { + if (progress.stage === 'done') return + importProgress.value = progress + }) + + try { + const result = await window.chatApi.importWithOptions(filePath, { formatId }) + unsubscribe() + + if (importProgress.value) { + importProgress.value.progress = 100 + } + await new Promise((resolve) => setTimeout(resolve, 300)) + + if (result.success && result.sessionId) { + await sessionStore.loadSessions() + sessionStore.selectSession(result.sessionId) + await autoGenerateSessionIndex(result.sessionId) + await navigateToSession(result.sessionId) + } else { + importError.value = translateError(result.error || 'error.import_failed') + } + } catch (error) { + importError.value = String(error) + } finally { + isImporting.value = false + setTimeout(() => { + importProgress.value = null + }, 500) + } +} + // 聊天选择后的导入处理(通用,适用于 Telegram 等多聊天格式) async function handleChatSelect(selectedChats: ChatInfo[]) { if (selectedChats.length === 0) return @@ -743,7 +795,18 @@ const getMergeFileProgressText = (file: MergeFileInfo) => {{ diagnosisSuggestion }} - {{ t('home.import.viewLog') }} +
+ {{ t('home.import.viewLog') }} + + {{ t('home.formatSelector.manualSelect') }} + +
@@ -752,5 +815,12 @@ const getMergeFileProgressText = (file: MergeFileInfo) => + + +