mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 20:35:19 +00:00
feat: 聊天记录功能支持上传zip
This commit is contained in:
parent
b4853a0673
commit
e3222752dc
11 changed files with 308 additions and 597 deletions
|
|
@ -6,7 +6,12 @@
|
|||
"Bash(git rev-parse:*)",
|
||||
"Bash(chmod:*)",
|
||||
"Bash(test:*)",
|
||||
"Bash(lsof:*)"
|
||||
"Bash(lsof:*)",
|
||||
"WebSearch",
|
||||
"Bash(npm install:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(npm run build:*)",
|
||||
"Bash(cat:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||
|
||||
## Project Overview
|
||||
|
||||
Open WebUI 是一个功能丰富的自托管 AI 平台,支持完全离线运行。核心技术栈:
|
||||
CyberLover 是一个功能丰富的自托管 AI 平台,支持完全离线运行。核心技术栈:
|
||||
- **前端**: SvelteKit 4 + TypeScript + Vite 5 + Tailwind CSS 4
|
||||
- **后端**: Python 3.11 + FastAPI + SQLAlchemy
|
||||
- **部署**: Docker 多阶段构建,生产环境前后端同容器运行
|
||||
|
|
|
|||
BIN
backend/open_webui/favicon.png
Normal file
BIN
backend/open_webui/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -2087,6 +2087,11 @@ async def healthcheck_with_db():
|
|||
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
||||
|
||||
|
||||
@app.get("/favicon.png")
|
||||
async def get_favicon():
|
||||
return FileResponse(os.path.join(STATIC_DIR, "favicon.png"))
|
||||
|
||||
|
||||
@app.get("/cache/{path:path}")
|
||||
async def serve_cache_file(
|
||||
path: str,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "Open WebUI",
|
||||
"short_name": "WebUI",
|
||||
"name": "CyberLover",
|
||||
"short_name": "Lover",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/web-app-manifest-192x192.png",
|
||||
|
|
|
|||
714
package-lock.json
generated
714
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -29,6 +29,7 @@
|
|||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/postcss": "^4.0.0",
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"@types/jszip": "^3.4.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.31.1",
|
||||
"@typescript-eslint/parser": "^8.31.1",
|
||||
"cypress": "^13.15.0",
|
||||
|
|
@ -107,6 +108,7 @@
|
|||
"idb": "^7.1.1",
|
||||
"js-sha256": "^0.10.1",
|
||||
"jspdf": "^3.0.0",
|
||||
"jszip": "^3.10.1",
|
||||
"katex": "^0.16.22",
|
||||
"kokoro-js": "^1.1.1",
|
||||
"leaflet": "^1.9.4",
|
||||
|
|
@ -141,6 +143,7 @@
|
|||
"vega-lite": "^6.4.1",
|
||||
"vite-plugin-static-copy": "^2.2.0",
|
||||
"y-prosemirror": "^1.3.7",
|
||||
"y-protocols": "^1.0.6",
|
||||
"yaml": "^2.7.1",
|
||||
"yjs": "^13.6.27"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
getPinnedChatList
|
||||
} from '$lib/apis/chats';
|
||||
import { getImportOrigin, convertOpenAIChats } from '$lib/utils';
|
||||
import { extractChatsFromFile } from '$lib/utils/chatImport';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
|
@ -41,22 +42,25 @@
|
|||
$: if (importFiles) {
|
||||
console.log(importFiles);
|
||||
|
||||
let reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
let chats = JSON.parse(event.target.result);
|
||||
console.log(chats);
|
||||
if (getImportOrigin(chats) == 'openai') {
|
||||
try {
|
||||
chats = convertOpenAIChats(chats);
|
||||
} catch (error) {
|
||||
console.log('Unable to import chats:', error);
|
||||
}
|
||||
}
|
||||
importChats(chats);
|
||||
};
|
||||
|
||||
if (importFiles.length > 0) {
|
||||
reader.readAsText(importFiles[0]);
|
||||
extractChatsFromFile(importFiles[0])
|
||||
.then((chats) => {
|
||||
console.log(chats);
|
||||
if (getImportOrigin(chats) == 'openai') {
|
||||
try {
|
||||
chats = convertOpenAIChats(chats);
|
||||
} catch (error) {
|
||||
console.log('Unable to import chats:', error);
|
||||
toast.error($i18n.t('Failed to convert OpenAI chats'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
importChats(chats);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Import error:', error);
|
||||
toast.error($i18n.t(error.message));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -134,7 +138,7 @@
|
|||
bind:this={chatImportInputElement}
|
||||
bind:files={importFiles}
|
||||
type="file"
|
||||
accept=".json"
|
||||
accept=".json,.zip"
|
||||
hidden
|
||||
/>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { getContext, createEventDispatcher, onMount, onDestroy } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
|
|
@ -9,6 +10,7 @@
|
|||
import Collapsible from './Collapsible.svelte';
|
||||
import Tooltip from './Tooltip.svelte';
|
||||
import Plus from '../icons/Plus.svelte';
|
||||
import { extractChatsFromFile } from '$lib/utils/chatImport';
|
||||
|
||||
export let open = true;
|
||||
|
||||
|
|
@ -48,26 +50,29 @@
|
|||
// If dropped items aren't files, reject them
|
||||
if (item.kind === 'file') {
|
||||
const file = item.getAsFile();
|
||||
if (file && file.type === 'application/json') {
|
||||
console.log('Dropped file is a JSON file!');
|
||||
const isJSON = file && file.type === 'application/json';
|
||||
const isZIP =
|
||||
file &&
|
||||
(file.type === 'application/zip' ||
|
||||
file.type === 'application/x-zip-compressed' ||
|
||||
file.name.endsWith('.zip'));
|
||||
|
||||
// Read the JSON file with FileReader
|
||||
const reader = new FileReader();
|
||||
reader.onload = async function (event) {
|
||||
try {
|
||||
const fileContent = JSON.parse(event.target.result);
|
||||
console.log('Parsed JSON Content: ', fileContent);
|
||||
if (file && (isJSON || isZIP)) {
|
||||
console.log('Dropped file is a JSON or ZIP file!');
|
||||
|
||||
extractChatsFromFile(file)
|
||||
.then((fileContent) => {
|
||||
console.log('Parsed Content: ', fileContent);
|
||||
open = true;
|
||||
dispatch('import', fileContent);
|
||||
} catch (error) {
|
||||
console.error('Error parsing JSON file:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Start reading the file
|
||||
reader.readAsText(file);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error processing file:', error);
|
||||
toast.error($i18n.t(error.message));
|
||||
});
|
||||
} else {
|
||||
console.error('Only JSON file types are supported.');
|
||||
console.error('Only JSON and ZIP file types are supported.');
|
||||
toast.error($i18n.t('Only JSON and ZIP file types are supported.'));
|
||||
}
|
||||
} else {
|
||||
open = true;
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@
|
|||
import Note from '../icons/Note.svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { getImportOrigin, convertOpenAIChats } from '$lib/utils';
|
||||
import { extractChatsFromFile } from '$lib/utils/chatImport';
|
||||
|
||||
const BREAKPOINT = 768;
|
||||
|
||||
|
|
@ -358,22 +359,25 @@
|
|||
$: if (importFiles) {
|
||||
console.log(importFiles);
|
||||
|
||||
let reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
let chats = JSON.parse(event.target.result);
|
||||
console.log(chats);
|
||||
if (getImportOrigin(chats) == 'openai') {
|
||||
try {
|
||||
chats = convertOpenAIChats(chats);
|
||||
} catch (error) {
|
||||
console.log('Unable to import chats:', error);
|
||||
}
|
||||
}
|
||||
importChatsHandler(chats);
|
||||
};
|
||||
|
||||
if (importFiles.length > 0) {
|
||||
reader.readAsText(importFiles[0]);
|
||||
extractChatsFromFile(importFiles[0])
|
||||
.then((chats) => {
|
||||
console.log(chats);
|
||||
if (getImportOrigin(chats) == 'openai') {
|
||||
try {
|
||||
chats = convertOpenAIChats(chats);
|
||||
} catch (error) {
|
||||
console.log('Unable to import chats:', error);
|
||||
toast.error($i18n.t('Failed to convert OpenAI chats'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
importChatsHandler(chats);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Import error:', error);
|
||||
toast.error($i18n.t(error.message));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1264,7 +1268,7 @@
|
|||
bind:this={chatImportInputElement}
|
||||
bind:files={importFiles}
|
||||
type="file"
|
||||
accept=".json"
|
||||
accept=".json,.zip"
|
||||
hidden
|
||||
/>
|
||||
|
||||
|
|
|
|||
61
src/lib/utils/chatImport.ts
Normal file
61
src/lib/utils/chatImport.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import JSZip from 'jszip';
|
||||
|
||||
/**
|
||||
* 从文件中提取聊天记录数据
|
||||
* 支持 JSON 和 ZIP 格式
|
||||
*
|
||||
* @param file - 用户上传的文件 (JSON 或 ZIP)
|
||||
* @returns Promise<any> - 解析后的聊天记录数据
|
||||
* @throws Error - 文件格式不支持、ZIP 解压失败、JSON 解析失败等错误
|
||||
*/
|
||||
export async function extractChatsFromFile(file: File): Promise<any> {
|
||||
const fileExtension = file.name.split('.').pop()?.toLowerCase();
|
||||
|
||||
if (fileExtension === 'json') {
|
||||
// 直接读取 JSON 文件
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const content = e.target?.result as string;
|
||||
const chats = JSON.parse(content);
|
||||
resolve(chats);
|
||||
} catch (error) {
|
||||
reject(new Error('Invalid JSON format'));
|
||||
}
|
||||
};
|
||||
reader.onerror = () => reject(new Error('Failed to read file'));
|
||||
reader.readAsText(file);
|
||||
});
|
||||
} else if (fileExtension === 'zip') {
|
||||
// 解压 ZIP 并提取 conversations.json
|
||||
try {
|
||||
const zip = await JSZip.loadAsync(file);
|
||||
const conversationsFile = zip.file('conversations.json');
|
||||
|
||||
if (!conversationsFile) {
|
||||
throw new Error('conversations.json not found in ZIP archive');
|
||||
}
|
||||
|
||||
const content = await conversationsFile.async('text');
|
||||
const chats = JSON.parse(content);
|
||||
return chats;
|
||||
} catch (error) {
|
||||
// 细化错误信息
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('not found')) {
|
||||
throw new Error('ZIP file must contain conversations.json in root directory');
|
||||
} else if (error.message.includes('Unexpected token')) {
|
||||
throw new Error('conversations.json contains invalid JSON format');
|
||||
} else if (error.message.includes('corrupted')) {
|
||||
throw new Error('ZIP file is corrupted or invalid');
|
||||
} else {
|
||||
throw new Error(`Failed to extract ZIP file: ${error.message}`);
|
||||
}
|
||||
}
|
||||
throw new Error('Failed to process ZIP file');
|
||||
}
|
||||
} else {
|
||||
throw new Error('Unsupported file format. Please upload .json or .zip file');
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue