fix(import): guide users when file format is unrecognized

This commit is contained in:
digua committed 2026-10-22 16:02:18 +08:00
1 parent faed80c898
commit 11aa6a37c4
7 files changed
+60 -13

No files matched your search

+32 -13
View File
@@ -26,7 +26,11 @@ const props = defineProps<{
backendFeatures: boolean
}>()
const { t } = useI18n()
const { t, locale } = useI18n()
const conversionGuideUrl = computed(() => {
const prefix = locale.value === 'zh-CN' ? '/cn' : locale.value === 'zh-TW' ? '/tw' : ''
return `https://docs.chatlab.fun${prefix}/standard/ai-converter`
})
const toast = useToast()
const sessionStore = useSessionStore()
const acceptedExtensions = computed(() =>
@@ -94,6 +98,7 @@ function showImportOutcome(result: ImportResult) {
}
const importError = ref<string | null>(null)
const isUnrecognizedFormat = computed(() => importError.value === 'error.unrecognized_format')
const hasImportLog = ref(false)
const importDiagnostics = ref<{
logFile: string | null
@@ -290,7 +295,7 @@ async function processWebFiles(files: File[]) {
await importSingleWebFile(file)
} else {
if (files.some(isArchiveFile)) {
importError.value = translateError('error.archive_single_file_required')
importError.value = 'error.archive_single_file_required'
return
}
await sessionStore.importFiles(files)
@@ -320,7 +325,7 @@ async function importSingleWebFile(file: File, options?: ImportOptions) {
showImportOutcome(result)
await navigateToSession(result.sessionId)
} else {
importError.value = translateError(result.error || 'error.import_failed')
importError.value = result.error || 'error.import_failed'
}
} catch (error) {
importError.value = String(error)
@@ -391,7 +396,7 @@ async function importDirectory(source: File[] | string) {
showImportOutcome(result)
await navigateToSession(result.sessionId)
} else {
importError.value = translateError(result.error || 'error.import_failed')
importError.value = result.error || 'error.import_failed'
}
} catch (error) {
importError.value = String(error)
@@ -407,7 +412,7 @@ async function importDirectory(source: File[] | string) {
async function processFilePaths(paths: string[]) {
if (paths.some(isArchiveFile)) {
if (paths.length !== 1) {
importError.value = translateError('error.archive_single_file_required')
importError.value = 'error.archive_single_file_required'
return
}
await prepareArchiveImport(paths[0])
@@ -434,7 +439,7 @@ async function processFilePaths(paths: string[]) {
// 单文件导入
const result = await sessionStore.importFileFromPath(paths[0])
if (!result.success && result.error) {
importError.value = translateError(result.error)
importError.value = result.error
// 格式无法识别时,记住文件路径以便手动选择格式
if (result.error === 'error.unrecognized_format') {
formatSelectorFilePath.value = paths[0]
@@ -504,7 +509,7 @@ async function handleFormatSelect(formatId: string) {
showImportOutcome(result)
await navigateToSession(result.sessionId)
} else {
importError.value = translateError(result.error || 'error.import_failed')
importError.value = result.error || 'error.import_failed'
}
} catch (error) {
importError.value = String(error)
@@ -543,10 +548,10 @@ async function handleChatSelect(selectedChats: ChatInfo[]) {
showImportOutcome(result)
await navigateToSession(result.sessionId)
} else {
importError.value = translateError(result.error || 'error.import_failed')
importError.value = result.error || 'error.import_failed'
}
} catch (error) {
importError.value = translateError(error instanceof Error ? error.message : String(error))
importError.value = error instanceof Error ? error.message : String(error)
} finally {
try {
await useImportService().releaseImportSource(source.sourceId)
@@ -662,7 +667,7 @@ async function handleChatSelect(selectedChats: ChatInfo[]) {
showImportOutcome(result)
await navigateToSession(result.sessionId)
} else {
importError.value = translateError(result.error || 'error.import_failed')
importError.value = result.error || 'error.import_failed'
}
} catch (error) {
importError.value = String(error)
@@ -1197,7 +1202,7 @@ const getMergeFileProgressText = (file: MergeFileInfo) =>
>
<div class="flex items-center gap-2 text-sm text-red-600 dark:text-red-400">
<UIcon name="i-heroicons-exclamation-circle" class="h-5 w-5 shrink-0" />
<span>{{ importError }}</span>
<span>{{ translateError(importError) }}</span>
</div>
<!-- 诊断信息(如果有) -->
<div
@@ -1218,13 +1223,27 @@ const getMergeFileProgressText = (file: MergeFileInfo) =>
</div>
</div>
</div>
<p v-if="formatSelectorFilePath || hasImportLog" class="text-xs text-gray-500 dark:text-gray-400">
<p v-if="isUnrecognizedFormat" class="text-xs leading-relaxed text-gray-500 dark:text-gray-400">
{{ t('home.import.errors.unrecognizedFormatHint') }}
</p>
<p v-else-if="formatSelectorFilePath || hasImportLog" class="text-xs text-gray-500 dark:text-gray-400">
{{ t('home.import.errors.actionHint') }}
</p>
<div class="flex gap-2">
<div class="flex flex-wrap justify-center gap-2">
<UButton v-if="formatSelectorFilePath" size="xs" @click="showFormatSelector = true">
{{ t('home.formatSelector.manualSelect') }}
</UButton>
<UButton
v-if="isUnrecognizedFormat"
:href="conversionGuideUrl"
target="_blank"
rel="noopener noreferrer"
size="xs"
variant="soft"
icon="i-heroicons-arrow-top-right-on-square"
>
{{ t('home.import.viewConversionGuide') }}
</UButton>
<UButton v-if="hasImportLog" size="xs" variant="soft" @click="openLatestImportLog">
{{ t('home.import.viewLog') }}
</UButton>
+2
View File
@@ -38,11 +38,13 @@
"processed": "{count} messages processed",
"viewLog": "View import log",
"cannotReadPath": "Cannot read file path",
"viewConversionGuide": "View conversion guide",
"errors": {
"no_file_selected": "No file selected",
"import_failed": "Import failed",
"import_in_progress": "Another import is already in progress. Please try again later.",
"unrecognized_format": "Unrecognized file format",
"unrecognizedFormatHint": "TXT and JSON are file types; their contents must follow a chat record format supported by ChatLab. You can select a format manually or follow the guide to convert your records to ChatLab Format using AI.",
"no_messages": "No messages parsed. Please check if the file format is correct.",
"no_chat_selected": "No chat selected",
"archive_single_file_required": "Import archive files one at a time",
+2
View File
@@ -38,11 +38,13 @@
"processed": "{count} 件のメッセージを処理しました",
"viewLog": "インポートログを表示",
"cannotReadPath": "ファイルパスを読み取れません",
"viewConversionGuide": "変換ガイドを見る",
"errors": {
"no_file_selected": "ファイルが選択されていません",
"import_failed": "インポートに失敗しました",
"import_in_progress": "別のインポートが実行中です。しばらくしてから再試行してください。",
"unrecognized_format": "認識できないファイル形式です",
"unrecognizedFormatHint": "TXT や JSON はファイルの種類であり、内容も ChatLab が対応するチャット記録形式に従う必要があります。形式を手動で選択するか、ガイドに従って AI で ChatLab 標準形式に変換できます。",
"no_messages": "メッセージを検出できませんでした。ファイル形式が正しいか確認してください",
"no_chat_selected": "チャットが選択されていません",
"archive_single_file_required": "アーカイブは1件ずつインポートしてください",
+2
View File
@@ -38,11 +38,13 @@
"processed": "已处理 {count} 条消息",
"viewLog": "查看导入日志",
"cannotReadPath": "无法读取文件路径",
"viewConversionGuide": "查看转换指南",
"errors": {
"no_file_selected": "未选择文件",
"import_failed": "导入失败",
"import_in_progress": "已有导入任务正在进行,请稍后重试",
"unrecognized_format": "无法识别的文件格式",
"unrecognizedFormatHint": "TXT、JSON 只是文件类型,内容还需要符合 ChatLab 支持的聊天记录格式。你可以手动选择格式,或参考指南使用 AI 转换为 ChatLab 标准格式。",
"no_messages": "未解析到任何消息,请检查文件格式是否正确",
"no_chat_selected": "未选择聊天",
"archive_single_file_required": "压缩包需要单独选择并导入",
+2
View File
@@ -38,11 +38,13 @@
"processed": "已處理 {count} 則訊息",
"viewLog": "檢視匯入日誌",
"cannotReadPath": "無法讀取檔案路徑",
"viewConversionGuide": "查看轉換指南",
"errors": {
"no_file_selected": "未選擇檔案",
"import_failed": "匯入失敗",
"import_in_progress": "已有匯入工作正在進行,請稍後再試",
"unrecognized_format": "無法識別的檔案格式",
"unrecognizedFormatHint": "TXT、JSON 只是檔案類型,內容仍需符合 ChatLab 支援的聊天記錄格式。你可以手動選擇格式,或參考指南使用 AI 轉換為 ChatLab 標準格式。",
"no_messages": "未解析到任何訊息,請檢查檔案格式是否正確",
"no_chat_selected": "未選擇聊天",
"archive_single_file_required": "壓縮檔需要單獨選擇並匯入",
+16
View File
@@ -7,6 +7,7 @@ import type {
WebRuntimeTaskType,
} from '@openchatlab/web-runtime'
import { BrowserImportAdapter } from './browser'
import { WebRuntimeRpcError } from '@openchatlab/web-runtime'
function createFile(name = 'fixture.json', content = '{}', type = 'application/json'): File {
const blob = new Blob([content], { type })
@@ -52,6 +53,21 @@ function createDemoDocument(name: string, timestamps: number[]): string {
}
describe('BrowserImportAdapter', () => {
it('exposes unsupported formats for localized conversion guidance without masking other failures', async () => {
for (const [code, expected] of [
['UNSUPPORTED_IMPORT_FORMAT', 'error.unrecognized_format'],
['DATABASE_ERROR', 'Original failure'],
]) {
const adapter = new BrowserImportAdapter({
async request() {
throw new WebRuntimeRpcError({ name: 'WebRuntimeError', code, message: 'Original failure' })
},
dispose: () => undefined,
})
assert.deepEqual(await adapter.importFile(createFile()), { success: false, error: expected })
}
})
it('forwards detection, supported formats, import progress, and the result through RPC', async () => {
const requests: WebRuntimeTaskType[] = []
const rpc = {
+4
View File
@@ -1,5 +1,6 @@
import {
normalizeBrowserImportFormatId,
WebRuntimeRpcError,
type BrowserImportFormatId,
type BrowserParseSource,
type RpcProgressPayload,
@@ -60,6 +61,9 @@ export class BrowserImportAdapter implements ImportAdapter {
onProgress
)
} catch (error) {
if (error instanceof WebRuntimeRpcError && error.code === 'UNSUPPORTED_IMPORT_FORMAT') {
return { success: false, error: 'error.unrecognized_format' }
}
return { success: false, error: error instanceof Error ? error.message : String(error) }
} finally {
if (this.activeImport === controller) this.activeImport = undefined