From f181c704029bee30dcf2c3e82efe6295a1af6449 Mon Sep 17 00:00:00 2001 From: digua Date: Wed, 6 May 2026 21:04:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E7=94=A8=E6=88=B7=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=9F=A5=E7=9C=8BDemo=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/main/ipc/demo.ts | 124 ++++++++++++++++++ electron/main/ipcMain.ts | 2 + electron/preload/apis/chat.ts | 34 +++++ electron/preload/index.d.ts | 11 ++ .../AIChat/chat/ConversationList.vue | 14 +- 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/DemoImportButton.vue | 90 +++++++++++++ src/pages/home/components/ImportArea.vue | 13 +- 11 files changed, 307 insertions(+), 5 deletions(-) create mode 100644 electron/main/ipc/demo.ts create mode 100644 src/pages/home/components/DemoImportButton.vue diff --git a/electron/main/ipc/demo.ts b/electron/main/ipc/demo.ts new file mode 100644 index 00000000..ffa97472 --- /dev/null +++ b/electron/main/ipc/demo.ts @@ -0,0 +1,124 @@ +/** + * Demo 示例数据下载与导入 IPC 处理器 + */ + +import { ipcMain, app } from 'electron' +import * as fs from 'fs' +import * as path from 'path' +import axios from 'axios' +import * as worker from '../worker/workerManager' +import type { IpcContext } from './types' + +const DEMO_BASE_URL = 'https://chatlab.fun/assets/demo' + +interface DemoProgress { + stage: 'downloading' | 'importing' | 'done' | 'error' + /** 当前处理的文件序号 (1-based) */ + current: number + total: number + message?: string +} + +function getDemoTempDir(): string { + const tempDir = path.join(app.getPath('userData'), 'temp', 'demo') + fs.mkdirSync(tempDir, { recursive: true }) + return tempDir +} + +async function downloadFile(url: string, destPath: string): Promise { + const tmpPath = destPath + '.tmp' + const response = await axios.get(url, { + responseType: 'arraybuffer', + timeout: 60_000, + }) + + const buffer = Buffer.from(response.data) + if (buffer.length < 100) { + throw new Error(`Downloaded file too small (${buffer.length} bytes)`) + } + + fs.writeFileSync(tmpPath, buffer) + fs.renameSync(tmpPath, destPath) +} + +function cleanupDemoTemp(tempDir: string): void { + try { + if (fs.existsSync(tempDir)) { + for (const file of fs.readdirSync(tempDir)) { + fs.unlinkSync(path.join(tempDir, file)) + } + fs.rmdirSync(tempDir) + } + } catch { + // best-effort cleanup + } +} + +export function registerDemoHandlers(ctx: IpcContext): void { + const { win } = ctx + + /** + * 下载并导入 Demo 示例数据 + * 返回群聊和私聊的 sessionId + */ + ipcMain.handle( + 'demo:downloadAndImport', + async ( + _, + locale: string + ): Promise<{ + success: boolean + groupSessionId?: string + privateSessionId?: string + error?: string + }> => { + const tempDir = getDemoTempDir() + const groupPath = path.join(tempDir, 'demo-group.json') + const privatePath = path.join(tempDir, 'demo-private.json') + + const sendProgress = (progress: DemoProgress) => { + win.webContents.send('demo:progress', progress) + } + + try { + // Phase 1: Download + sendProgress({ stage: 'downloading', current: 1, total: 2 }) + await downloadFile(`${DEMO_BASE_URL}/${locale}/demo-group.json`, groupPath) + + sendProgress({ stage: 'downloading', current: 2, total: 2 }) + await downloadFile(`${DEMO_BASE_URL}/${locale}/demo-private.json`, privatePath) + + // Phase 2: Import group chat + sendProgress({ stage: 'importing', current: 1, total: 2 }) + const groupResult = await worker.streamImport(groupPath) + + if (!groupResult.success || !groupResult.sessionId) { + throw new Error(groupResult.error || 'Failed to import group demo') + } + + // Phase 3: Import private chat + sendProgress({ stage: 'importing', current: 2, total: 2 }) + const privateResult = await worker.streamImport(privatePath) + + if (!privateResult.success || !privateResult.sessionId) { + throw new Error(privateResult.error || 'Failed to import private demo') + } + + sendProgress({ stage: 'done', current: 2, total: 2 }) + + return { + success: true, + groupSessionId: groupResult.sessionId, + privateSessionId: privateResult.sessionId, + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + console.error('[Demo] Download and import failed:', message) + sendProgress({ stage: 'error', current: 0, total: 2, message }) + return { success: false, error: message } + } finally { + cleanupDemoTemp(tempDir) + } + } + ) +} diff --git a/electron/main/ipcMain.ts b/electron/main/ipcMain.ts index 7165fcaf..6121859b 100644 --- a/electron/main/ipcMain.ts +++ b/electron/main/ipcMain.ts @@ -16,6 +16,7 @@ import { registerNetworkHandlers } from './ipc/network' import { registerNlpHandlers } from './ipc/nlp' import { registerAnalyticsHandlers } from './analytics' import { registerApiHandlers, initApiServer, cleanupApiServer } from './ipc/api' +import { registerDemoHandlers } from './ipc/demo' // 导入 Worker 模块(用于异步分析查询和流式导入) import * as worker from './worker/workerManager' @@ -50,6 +51,7 @@ const mainIpcMain = (win: BrowserWindow) => { registerNlpHandlers(context) registerAnalyticsHandlers() registerApiHandlers(context) + registerDemoHandlers(context) // 启动 ChatLab API 服务(异步,不阻塞 IPC 注册) initApiServer(context).catch((err) => { diff --git a/electron/preload/apis/chat.ts b/electron/preload/apis/chat.ts index e5a8370b..236acd0a 100644 --- a/electron/preload/apis/chat.ts +++ b/electron/preload/apis/chat.ts @@ -483,6 +483,40 @@ export const chatApi = { }> => { return ipcRenderer.invoke('chat:cleanupTempExportFiles', filePaths) }, + + // ==================== Demo 示例数据 ==================== + + /** + * 下载并导入 Demo 示例数据 + */ + importDemo: ( + locale: string + ): Promise<{ + success: boolean + groupSessionId?: string + privateSessionId?: string + error?: string + }> => { + return ipcRenderer.invoke('demo:downloadAndImport', locale) + }, + + /** + * 监听 Demo 导入进度 + */ + onDemoProgress: ( + callback: (progress: { stage: string; current: number; total: number; message?: string }) => void + ) => { + const handler = ( + _event: Electron.IpcRendererEvent, + progress: { stage: string; current: number; total: number; message?: string } + ) => { + callback(progress) + } + ipcRenderer.on('demo:progress', handler) + return () => { + ipcRenderer.removeListener('demo:progress', handler) + } + }, } // Merge API - 合并功能 diff --git a/electron/preload/index.d.ts b/electron/preload/index.d.ts index 1554dcff..ff6495f0 100644 --- a/electron/preload/index.d.ts +++ b/electron/preload/index.d.ts @@ -192,6 +192,17 @@ interface ChatApi { success: boolean error?: string }> + + importDemo: (locale: string) => Promise<{ + success: boolean + groupSessionId?: string + privateSessionId?: string + error?: string + }> + + onDemoProgress: ( + callback: (progress: { stage: string; current: number; total: number; message?: string }) => void + ) => () => void } interface Api { diff --git a/src/components/AIChat/chat/ConversationList.vue b/src/components/AIChat/chat/ConversationList.vue index ae8dfa17..01590a1b 100644 --- a/src/components/AIChat/chat/ConversationList.vue +++ b/src/components/AIChat/chat/ConversationList.vue @@ -127,6 +127,16 @@ async function saveTitle(convId: string) { editingId.value = null } +function handleMenuRename(conv: Conversation) { + menuOpenId.value = null + startEditing(conv) +} + +function handleMenuDelete(convId: string) { + menuOpenId.value = null + handleDelete(convId) +} + // 删除对话 async function handleDelete(convId: string) { if (props.disabled) return @@ -283,7 +293,7 @@ defineExpose({