diff --git a/electron/main/ipc/chat.ts b/electron/main/ipc/chat.ts index e439498..27a5726 100644 --- a/electron/main/ipc/chat.ts +++ b/electron/main/ipc/chat.ts @@ -9,6 +9,7 @@ import * as parser from '../parser' import { detectFormat, diagnoseFormat, type ParseProgress } from '../parser' import type { IpcContext } from './types' import { CURRENT_SCHEMA_VERSION, getPendingMigrationInfos, type MigrationInfo } from '../database/migrations' +import { exportSessionToTempFile, cleanupTempExportFiles } from '../merger' /** * 注册聊天记录相关 IPC 处理器 @@ -978,4 +979,36 @@ export function registerChatHandlers(ctx: IpcContext): void { return { success: false, error: String(error) } } }) + + // ==================== 批量管理:导出会话为临时文件 ==================== + + /** + * 导出多个会话为临时文件(用于合并) + */ + ipcMain.handle('chat:exportSessionsToTempFiles', async (_, sessionIds: string[]) => { + try { + const tempFiles: string[] = [] + for (const sessionId of sessionIds) { + const tempPath = await exportSessionToTempFile(sessionId) + tempFiles.push(tempPath) + } + return { success: true, tempFiles } + } catch (error) { + console.error('[IpcMain] 导出会话失败:', error) + return { success: false, error: String(error), tempFiles: [] } + } + }) + + /** + * 清理临时导出文件 + */ + ipcMain.handle('chat:cleanupTempExportFiles', async (_, filePaths: string[]) => { + try { + cleanupTempExportFiles(filePaths) + return { success: true } + } catch (error) { + console.error('[IpcMain] 清理临时文件失败:', error) + return { success: false, error: String(error) } + } + }) } diff --git a/electron/main/merger/index.ts b/electron/main/merger/index.ts index 647f179..c26279a 100644 --- a/electron/main/merger/index.ts +++ b/electron/main/merger/index.ts @@ -599,3 +599,128 @@ export async function mergeFilesWithTempDb( } } } + +// ==================== 从会话数据库导出 ==================== + +import Database from 'better-sqlite3' +import { getDbPath } from '../database/core' + +/** + * 从已导入的会话数据库导出为临时 JSON 文件 + * 用于批量管理中的合并功能 + */ +export async function exportSessionToTempFile(sessionId: string): Promise { + const dbPath = getDbPath(sessionId) + if (!fs.existsSync(dbPath)) { + throw new Error(`会话数据库不存在: ${sessionId}`) + } + + const db = new Database(dbPath, { readonly: true }) + + try { + // 读取 meta + const meta = db.prepare('SELECT * FROM meta').get() as { + name: string + platform: string + type: string + group_id?: string + group_avatar?: string + } + + if (!meta) { + throw new Error('无法读取会话元信息') + } + + // 读取 members + const members = db + .prepare('SELECT platform_id, account_name, group_nickname, avatar FROM member') + .all() as Array<{ + platform_id: string + account_name?: string + group_nickname?: string + avatar?: string + }> + + // 读取 messages(通过 JOIN 获取发送者信息) + const messages = db + .prepare( + `SELECT + m.platform_id as sender, + msg.sender_account_name as accountName, + msg.sender_group_nickname as groupNickname, + msg.ts as timestamp, + msg.type, + msg.content + FROM message msg + JOIN member m ON msg.sender_id = m.id + ORDER BY msg.ts` + ) + .all() as Array<{ + sender: string + accountName?: string + groupNickname?: string + timestamp: number + type: number + content?: string + }> + + // 构建 ChatLab 格式数据 + const chatLabData: ChatLabFormat = { + chatlab: { + version: '0.0.1', + exportedAt: Math.floor(Date.now() / 1000), + generator: 'ChatLab Export', + description: `导出自会话: ${meta.name}`, + }, + meta: { + name: meta.name, + platform: meta.platform as ChatPlatform, + type: meta.type as ChatType, + groupId: meta.group_id, + groupAvatar: meta.group_avatar, + }, + members: members.map((m) => ({ + platformId: m.platform_id, + accountName: m.account_name, + groupNickname: m.group_nickname, + avatar: m.avatar, + })), + messages: messages.map((msg) => ({ + sender: msg.sender, + accountName: msg.accountName, + groupNickname: msg.groupNickname, + timestamp: msg.timestamp, + type: msg.type, + content: msg.content, + })), + } + + // 写入临时文件 + const tempDir = path.join(getDefaultOutputDir(), '.chatlab_temp') + ensureOutputDir(tempDir) + const tempFilePath = path.join(tempDir, `export_${sessionId}_${Date.now()}.json`) + fs.writeFileSync(tempFilePath, JSON.stringify(chatLabData, null, 2), 'utf-8') + + console.log(`[Merger] 导出会话到临时文件: ${tempFilePath}, 消息数: ${messages.length}`) + + return tempFilePath + } finally { + db.close() + } +} + +/** + * 清理临时导出文件 + */ +export function cleanupTempExportFiles(filePaths: string[]): void { + for (const filePath of filePaths) { + try { + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath) + console.log(`[Merger] 清理临时文件: ${filePath}`) + } + } catch (err) { + console.error(`[Merger] 清理临时文件失败: ${filePath}`, err) + } + } +} diff --git a/electron/preload/index.d.ts b/electron/preload/index.d.ts index 6525ae7..43fa284 100644 --- a/electron/preload/index.d.ts +++ b/electron/preload/index.d.ts @@ -165,6 +165,15 @@ interface ChatApi { newMessageCount: number error?: string }> + exportSessionsToTempFiles: (sessionIds: string[]) => Promise<{ + success: boolean + tempFiles: string[] + error?: string + }> + cleanupTempExportFiles: (filePaths: string[]) => Promise<{ + success: boolean + error?: string + }> } interface Api { @@ -193,7 +202,6 @@ interface MergeApi { checkConflicts: (filePaths: string[]) => Promise mergeFiles: (params: MergeParams) => Promise clearCache: (filePath?: string) => Promise - onParseProgress: (callback: (data: { filePath: string; progress: ImportProgress }) => void) => () => void } // AI 相关类型 diff --git a/electron/preload/index.ts b/electron/preload/index.ts index f261929..733d4de 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -452,6 +452,31 @@ const chatApi = { }> => { return ipcRenderer.invoke('chat:incrementalImport', sessionId, filePath) }, + + /** + * 导出多个会话为临时文件(用于批量管理中的合并) + */ + exportSessionsToTempFiles: ( + sessionIds: string[] + ): Promise<{ + success: boolean + tempFiles: string[] + error?: string + }> => { + return ipcRenderer.invoke('chat:exportSessionsToTempFiles', sessionIds) + }, + + /** + * 清理临时导出文件 + */ + cleanupTempExportFiles: ( + filePaths: string[] + ): Promise<{ + success: boolean + error?: string + }> => { + return ipcRenderer.invoke('chat:cleanupTempExportFiles', filePaths) + }, } // Merge API - 合并功能 @@ -485,19 +510,6 @@ const mergeApi = { clearCache: (filePath?: string): Promise => { return ipcRenderer.invoke('merge:clearCache', filePath) }, - - /** - * 监听解析进度(用于大文件) - */ - onParseProgress: (callback: (data: { filePath: string; progress: ImportProgress }) => void) => { - const handler = (_event: Electron.IpcRendererEvent, data: { filePath: string; progress: ImportProgress }) => { - callback(data) - } - ipcRenderer.on('merge:parseProgress', handler) - return () => { - ipcRenderer.removeListener('merge:parseProgress', handler) - } - }, } // AI API - AI 功能 diff --git a/src/components/common/sidebar/SidebarFooter.vue b/src/components/common/sidebar/SidebarFooter.vue index d6ab6be..a2c41ff 100644 --- a/src/components/common/sidebar/SidebarFooter.vue +++ b/src/components/common/sidebar/SidebarFooter.vue @@ -1,17 +1,31 @@