mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-26 02:04:37 +08:00
fix(import): guide users when file format is unrecognized
This commit is contained in:
1 parent
faed80c898
commit
11aa6a37c4
7 files changed
+60
-13
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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件ずつインポートしてください",
|
||||
|
||||
@@ -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": "压缩包需要单独选择并导入",
|
||||
|
||||
@@ -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": "壓縮檔需要單獨選擇並匯入",
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user