From 340ff50d3a3ef008cc5ce1cc182836c2346716bc Mon Sep 17 00:00:00 2001 From: xinyan Date: Sat, 6 Dec 2025 23:43:38 +0800 Subject: [PATCH] =?UTF-8?q?=E8=81=8A=E5=A4=A9=E8=AE=B0=E5=BD=95=E6=8F=90?= =?UTF-8?q?=E5=8F=96=E5=99=A8=E9=80=BB=E8=BE=91=20=09modified:=20=20=20src?= =?UTF-8?q?/lib/components/layout/ImportChatsModal.svelte=20=09modified:?= =?UTF-8?q?=20=20=20src/lib/utils/index.ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/layout/ImportChatsModal.svelte | 94 +++++++++------- src/lib/utils/index.ts | 104 +++++++++++++++++- 2 files changed, 159 insertions(+), 39 deletions(-) diff --git a/src/lib/components/layout/ImportChatsModal.svelte b/src/lib/components/layout/ImportChatsModal.svelte index e2bc76feb1..32aa85edaf 100644 --- a/src/lib/components/layout/ImportChatsModal.svelte +++ b/src/lib/components/layout/ImportChatsModal.svelte @@ -3,7 +3,7 @@ import Modal from '../common/Modal.svelte'; import Spinner from '../common/Spinner.svelte'; import { extractChatsFromFile } from '$lib/utils/chatImport'; - import { getImportOrigin, convertOpenAIChats } from '$lib/utils'; + import { getImportOrigin, convertOpenAIChats, convertDeepseekChats } from '$lib/utils'; export let show = false; export let onImport: (chats: any[]) => Promise; @@ -41,17 +41,29 @@ .filter((l) => l.length > 0); if (lines.length === 0) { - throw new Error('文件为空,无法解析'); + throw new Error('File is empty, nothing to parse'); } try { return lines.map((line) => JSON.parse(line)); } catch (lineError) { - throw new Error('纯文本/JSONL 文件需包含有效的 JSON 或逐行 JSON 对象'); + throw new Error('Plain text JSONL must contain one valid JSON object per line'); } } }; + const normalizeChats = (chats: any) => { + if (Array.isArray(chats)) return chats; + + if (chats && typeof chats === 'object') { + if (Array.isArray((chats as any).conversations)) { + return (chats as any).conversations; + } + } + + throw new Error('File content must be a JSON array of chats'); + }; + const handleFiles = async (files: FileList | File[]) => { if (!files || files.length === 0) return; const file = files[0]; @@ -68,12 +80,18 @@ chats = await extractChatsFromFile(file); } - if (getImportOrigin(chats) === 'openai') { - chats = convertOpenAIChats(chats); + chats = normalizeChats(chats); + + if (chats.length === 0) { + throw new Error('File contained zero chat records'); } - if (!Array.isArray(chats)) { - throw new Error('文件内容需为 JSON 数组'); + const origin = getImportOrigin(chats); + + if (origin === 'openai') { + chats = convertOpenAIChats(chats); + } else if (origin === 'deepseek') { + chats = convertDeepseekChats(chats); } rawChats = chats; @@ -105,14 +123,14 @@ const confirmImport = async () => { if (!rawChats.length) { - toast.error('请先上传对话记录文件'); + toast.error('Please upload a chat history file first'); return; } const chatsToImport = selectedIndices.size > 0 ? rawChats.filter((_, idx) => selectedIndices.has(idx)) : rawChats; if (!chatsToImport.length) { - toast.error('未选择任何记录'); + toast.error('No records selected'); return; } @@ -131,7 +149,7 @@ await onImport(chatsToImport); show = false; - toast.success('开始导入筛选后的对话记录'); + toast.success('Starting import for the filtered chats'); } catch (error) { console.error(error); toast.error(error instanceof Error ? error.message : `${error}`); @@ -148,7 +166,7 @@ chat?.title ?? chat?.chat?.title ?? meta?.subject ?? - '未命名对话'; + 'Untitled chat'; const date = meta?.inserted_at ?? meta?.created_at ?? @@ -178,15 +196,15 @@
-
对话记录导入中心
+
Chat Import Center
- 完成准备、筛选后再执行导入,提升成功率与速度。 + Upload your exported history, filter the records you need, then import.
@@ -194,27 +212,25 @@
-
- 1. 准备您的对话记录 -
+
1. Prepare file
-

请确保导出文件格式为 JSON (.json)纯文本 (.txt)

- 请前往原平台导出历史对话,如 DeepSeek — 系统设置 — 数据管理 — 导出所有历史对话, - ChatGPT — 设置 — 数据管理 — 导出数据。 + Supported formats: JSON (.json), JSONL (.jsonl/.txt), or + OpenAI ZIP export (auto-converted).

-

如果导出的文件内容过多或过大,建议使用下方的筛选功能生成新的导入文件,以加快导入速度。

+

Large exports can be filtered below to speed up the import.

-
2. 筛选记录
+
2. Filter records
@@ -233,7 +249,7 @@ >
- {fileName ? `已选择:${fileName}` : '拖拽文件到此处,或点击上传'} + {fileName ? `Selected: ${fileName}` : 'Drag a file here or click to upload'}
- 支持 .json / .txt,OpenAI 导出支持自动转换 + Supports .json / .jsonl / .txt / .zip exports
{#if loading}
- 正在解析文件... + Parsing file...
{/if} {#if errorMsg} @@ -270,8 +286,10 @@
- 总数:{rawChats.length} | 已选:{selectedIndices.size} - {selectedIndices.size === 0 && rawChats.length > 0 ? '(未选则默认导入全部)' : ''} + Total: {rawChats.length} | Selected: {selectedIndices.size} + {selectedIndices.size === 0 && rawChats.length > 0 + ? ' (none selected will import all)' + : ''}
@@ -289,16 +307,16 @@ - - - + + + {#if !rawChats.length} {:else} @@ -330,9 +348,9 @@
-
3. 导入记录
+
3. Import
- 确认后将按筛选结果导入到当前账户的对话列表中。 + Confirmed records will be imported into your current account.
diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index f4818f340a..72eb54ee42 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -660,9 +660,19 @@ export const calculateSHA256 = async (file) => { export const getImportOrigin = (_chats) => { // Check what external service chat imports are from - if ('mapping' in _chats[0]) { + const first = Array.isArray(_chats) ? _chats[0] : null; + if (!first || typeof first !== 'object') return 'webui'; + + if ('mapping' in first) { + // DeepSeek exports use mapping + fragments instead of content.parts/text + const mappingValues = Object.values(first.mapping || {}); + const hasFragments = mappingValues.some( + (entry: any) => entry?.message && Array.isArray(entry.message.fragments) + ); + if (hasFragments) return 'deepseek'; return 'openai'; } + return 'webui'; }; @@ -747,6 +757,75 @@ const convertOpenAIMessages = (convo) => { return chat; }; +const fragmentsToContent = (fragments: any) => { + if (!Array.isArray(fragments)) return ''; + return fragments + .map((frag) => (typeof frag?.content === 'string' ? frag.content : '')) + .filter(Boolean) + .join('\n\n'); +}; + +const convertDeepseekMessages = (convo) => { + // Parse DeepSeek chat messages (mapping + fragments) into chat dictionary + const mapping = convo['mapping']; + const messages = []; + let currentId = ''; + let lastId = null; + + for (const message_id in mapping) { + const message = mapping[message_id]; + currentId = message_id; + try { + const fragments = message?.message?.fragments; + const content = fragmentsToContent(fragments); + + if (messages.length === 0 && (!content || content === '')) { + continue; + } + + const inferredRole = (() => { + if (Array.isArray(fragments)) { + const firstType = fragments.find((f) => typeof f?.type === 'string')?.type; + if (firstType === 'REQUEST') return 'user'; + if (firstType === 'RESPONSE') return 'assistant'; + } + return message?.message?.author?.role !== 'user' ? 'assistant' : 'user'; + })(); + + const new_chat = { + id: message_id, + parentId: lastId, + childrenIds: message['children'] || [], + role: inferredRole, + content, + model: message?.message?.model || 'deepseek-chat', + done: true, + context: null + }; + messages.push(new_chat); + lastId = currentId; + } catch (error) { + console.log('Error with DeepSeek message', message, '\nError:', error); + } + } + + const history: Record = {}; + messages.forEach((obj) => (history[obj.id] = obj)); + + const chat = { + history: { + currentId: currentId, + messages: history + }, + models: [messages[0]?.model || 'deepseek-chat'], + messages: messages, + options: {}, + timestamp: convo['inserted_at'] || convo['updated_at'] || convo['create_time'], + title: convo['title'] ?? 'New Chat' + }; + return chat; +}; + const validateChat = (chat) => { // Because ChatGPT sometimes has features we can't use like DALL-E or might have corrupted messages, need to validate const messages = chat.messages; @@ -801,6 +880,29 @@ export const convertOpenAIChats = (_chats) => { return chats; }; +export const convertDeepseekChats = (_chats) => { + const chats = []; + let failed = 0; + + for (const convo of _chats) { + const chat = convertDeepseekMessages(convo); + + if (validateChat(chat)) { + chats.push({ + id: convo['id'], + user_id: '', + title: convo['title'], + chat: chat, + timestamp: convo['inserted_at'] || convo['updated_at'] || convo['create_time'] + }); + } else { + failed++; + } + } + console.log(failed, 'DeepSeek conversations could not be imported'); + return chats; +}; + export const isValidHttpUrl = (string: string) => { let url;
选择标题 / 摘要时间PickTitle / SummaryTimestamp
- 请先上传文件以查看可筛选的记录 + Upload a file to see filterable records