From 4fb92556052a092f79131e248dd2f1580a2b4f22 Mon Sep 17 00:00:00 2001 From: digua Date: Fri, 5 Dec 2025 01:11:37 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E9=83=A8?= =?UTF-8?q?=E5=88=86=E6=9E=B6=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/main/database/index.ts | 28 - electron/main/ipc/ai.ts | 519 +++++++ electron/main/ipc/cache.ts | 213 +++ electron/main/ipc/chat.ts | 523 +++++++ electron/main/ipc/merge.ts | 133 ++ electron/main/ipc/types.ts | 18 + electron/main/ipc/window.ts | 129 ++ electron/main/ipcMain.ts | 1322 +---------------- electron/main/worker/index.ts | 48 - electron/preload/index.d.ts | 29 + electron/preload/index.ts | 45 + src/components.d.ts | 1 - src/components/analysis/OverviewTab.vue | 39 +- src/components/analysis/ai/ChatExplorer.vue | 2 +- src/components/analysis/ai/ChatInput.vue | 4 +- src/components/common/SettingModal.vue | 121 +- .../common/settings/CacheManageTab.vue | 231 +++ src/components/common/settings/index.ts | 1 + 18 files changed, 1903 insertions(+), 1503 deletions(-) delete mode 100644 electron/main/database/index.ts create mode 100644 electron/main/ipc/ai.ts create mode 100644 electron/main/ipc/cache.ts create mode 100644 electron/main/ipc/chat.ts create mode 100644 electron/main/ipc/merge.ts create mode 100644 electron/main/ipc/types.ts create mode 100644 electron/main/ipc/window.ts delete mode 100644 electron/main/worker/index.ts create mode 100644 src/components/common/settings/CacheManageTab.vue diff --git a/electron/main/database/index.ts b/electron/main/database/index.ts deleted file mode 100644 index 0bc634f..0000000 --- a/electron/main/database/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * 数据库服务模块 - * 统一导出入口 - */ - -// 核心功能 -export { importData, getAllSessions, getSession, deleteSession, getDbDirectory, openDatabase, getDbPath } from './core' - -// 分析查询 -export { - getAvailableYears, - getMemberActivity, - getHourlyActivity, - getDailyActivity, - getWeekdayActivity, - getMessageTypeDistribution, - getTimeRange, - getMemberNameHistory, - getRepeatAnalysis, - getCatchphraseAnalysis, - getNightOwlAnalysis, - getDragonKingAnalysis, - getDivingAnalysis, - getMonologueAnalysis, -} from './analysis' - -// 类型导出 -export type { TimeFilter } from './analysis' diff --git a/electron/main/ipc/ai.ts b/electron/main/ipc/ai.ts new file mode 100644 index 0000000..af551f3 --- /dev/null +++ b/electron/main/ipc/ai.ts @@ -0,0 +1,519 @@ +// electron/main/ipc/ai.ts +import { ipcMain, BrowserWindow } from 'electron' +import * as aiConversations from '../ai/conversations' +import * as llm from '../ai/llm' +import { aiLogger } from '../ai/logger' +import { Agent, type AgentStreamChunk } from '../ai/agent' +import type { ToolContext } from '../ai/tools/types' +import type { IpcContext } from './types' + +// ==================== AI Agent 请求追踪 ==================== +// 用于跟踪活跃的 Agent 请求,支持中止操作 +const activeAgentRequests = new Map() + +export function registerAIHandlers({ win }: IpcContext): void { + console.log('[IPC] Registering AI handlers...') + + // ==================== AI 对话管理 ==================== + + /** + * 创建新的 AI 对话 + */ + ipcMain.handle( + 'ai:createConversation', + async ( + _, + title: string, + sessionId?: string, + dataSource?: { type: 'chat' | 'member'; id: string; name?: string } + ) => { + try { + return aiConversations.createConversation(title, sessionId, dataSource) + } catch (error) { + console.error('创建 AI 对话失败:', error) + throw error + } + } + ) + + /** + * 获取所有 AI 对话列表 + */ + ipcMain.handle('ai:getConversations', async (_, sessionId?: string) => { + try { + return aiConversations.getConversations(sessionId) + } catch (error) { + console.error('获取 AI 对话列表失败:', error) + return [] + } + }) + + /** + * 获取单个对话详情 + */ + ipcMain.handle('ai:getConversation', async (_, conversationId: string) => { + try { + return aiConversations.getConversation(conversationId) + } catch (error) { + console.error('获取 AI 对话详情失败:', error) + return null + } + }) + + /** + * 更新 AI 对话标题 + */ + ipcMain.handle('ai:updateConversationTitle', async (_, conversationId: string, title: string) => { + try { + return aiConversations.updateConversationTitle(conversationId, title) + } catch (error) { + console.error('更新 AI 对话标题失败:', error) + return false + } + }) + + /** + * 删除 AI 对话 + */ + ipcMain.handle('ai:deleteConversation', async (_, conversationId: string) => { + try { + return aiConversations.deleteConversation(conversationId) + } catch (error) { + console.error('删除 AI 对话失败:', error) + return false + } + }) + + /** + * 添加 AI 消息 + */ + ipcMain.handle( + 'ai:addMessage', + async ( + _, + conversationId: string, + role: 'user' | 'assistant', + content: string, + dataKeywords?: string[], + dataMessageCount?: number + ) => { + try { + return aiConversations.addMessage(conversationId, role, content, dataKeywords, dataMessageCount) + } catch (error) { + console.error('添加 AI 消息失败:', error) + throw error + } + } + ) + + /** + * 获取 AI 对话的所有消息 + */ + ipcMain.handle('ai:getMessages', async (_, conversationId: string) => { + try { + return aiConversations.getMessages(conversationId) + } catch (error) { + console.error('获取 AI 消息失败:', error) + return [] + } + }) + + /** + * 删除 AI 消息 + */ + ipcMain.handle('ai:deleteMessage', async (_, messageId: string) => { + try { + return aiConversations.deleteMessage(messageId) + } catch (error) { + console.error('删除 AI 消息失败:', error) + return false + } + }) + + // ==================== LLM 服务(多配置管理)==================== + + /** + * 获取所有支持的 LLM 提供商 + */ + ipcMain.handle('llm:getProviders', async () => { + return llm.PROVIDERS + }) + + /** + * 获取所有配置列表 + */ + ipcMain.handle('llm:getAllConfigs', async () => { + const configs = llm.getAllConfigs() + // 脱敏 API Key + return configs.map((c) => ({ + ...c, + apiKey: c.apiKey ? `${c.apiKey.slice(0, 4)}****${c.apiKey.slice(-4)}` : '', + apiKeySet: !!c.apiKey, + })) + }) + + /** + * 获取当前激活的配置 ID + */ + ipcMain.handle('llm:getActiveConfigId', async () => { + const config = llm.getActiveConfig() + return config?.id || null + }) + + /** + * 添加新配置 + */ + ipcMain.handle( + 'llm:addConfig', + async ( + _, + config: { + name: string + provider: llm.LLMProvider + apiKey: string + model?: string + baseUrl?: string + maxTokens?: number + } + ) => { + try { + const result = llm.addConfig(config) + if (result.success && result.config) { + return { + success: true, + config: { + ...result.config, + apiKey: result.config.apiKey + ? `${result.config.apiKey.slice(0, 4)}****${result.config.apiKey.slice(-4)}` + : '', + apiKeySet: !!result.config.apiKey, + }, + } + } + return result + } catch (error) { + console.error('添加 LLM 配置失败:', error) + return { success: false, error: String(error) } + } + } + ) + + /** + * 更新配置 + */ + ipcMain.handle( + 'llm:updateConfig', + async ( + _, + id: string, + updates: { + name?: string + provider?: llm.LLMProvider + apiKey?: string + model?: string + baseUrl?: string + maxTokens?: number + } + ) => { + try { + // 如果 apiKey 为空字符串,表示不更新 API Key + const cleanUpdates = { ...updates } + if (cleanUpdates.apiKey === '') { + delete cleanUpdates.apiKey + } + + return llm.updateConfig(id, cleanUpdates) + } catch (error) { + console.error('更新 LLM 配置失败:', error) + return { success: false, error: String(error) } + } + } + ) + + /** + * 删除配置 + */ + ipcMain.handle('llm:deleteConfig', async (_, id?: string) => { + try { + // 兼容旧 API:如果没有传 id,删除当前激活的配置 + if (!id) { + const activeConfig = llm.getActiveConfig() + if (activeConfig) { + return llm.deleteConfig(activeConfig.id) + } + return { success: false, error: '没有激活的配置' } + } + return llm.deleteConfig(id) + } catch (error) { + console.error('删除 LLM 配置失败:', error) + return { success: false, error: String(error) } + } + }) + + /** + * 设置激活的配置 + */ + ipcMain.handle('llm:setActiveConfig', async (_, id: string) => { + try { + return llm.setActiveConfig(id) + } catch (error) { + console.error('设置激活配置失败:', error) + return { success: false, error: String(error) } + } + }) + + /** + * 验证 API Key(支持自定义 baseUrl 和 model) + */ + ipcMain.handle( + 'llm:validateApiKey', + async (_, provider: llm.LLMProvider, apiKey: string, baseUrl?: string, model?: string) => { + console.log('[LLM:validateApiKey] 开始验证:', { provider, baseUrl, model, apiKeyLength: apiKey?.length }) + try { + const service = llm.createLLMService({ provider, apiKey, baseUrl, model }) + const result = await service.validateApiKey() + console.log('[LLM:validateApiKey] 验证结果:', result) + return result + } catch (error) { + console.error('[LLM:validateApiKey] 验证失败:', error) + return false + } + } + ) + + /** + * 检查是否已配置 LLM(是否有激活的配置) + */ + ipcMain.handle('llm:hasConfig', async () => { + return llm.hasActiveConfig() + }) + + // ==================== 兼容旧 API(deprecated)==================== + + /** + * @deprecated 使用 llm:getAllConfigs 代替 + * 获取当前 LLM 配置 + */ + ipcMain.handle('llm:getConfig', async () => { + const config = llm.getActiveConfig() + if (!config) return null + return { + provider: config.provider, + apiKey: config.apiKey ? `${config.apiKey.slice(0, 8)}...${config.apiKey.slice(-4)}` : '', + apiKeySet: !!config.apiKey, + model: config.model, + baseUrl: config.baseUrl, + maxTokens: config.maxTokens, + } + }) + + /** + * @deprecated 使用 llm:addConfig 或 llm:updateConfig 代替 + * 保存 LLM 配置 + */ + ipcMain.handle( + 'llm:saveConfig', + async ( + _, + config: { provider: llm.LLMProvider; apiKey: string; model?: string; baseUrl?: string; maxTokens?: number } + ) => { + try { + const activeConfig = llm.getActiveConfig() + + if (activeConfig) { + // 更新现有配置 + const updates: Record = { ...config } + if (!config.apiKey || config.apiKey.trim() === '') { + delete updates.apiKey + } + return llm.updateConfig(activeConfig.id, updates) + } else { + // 创建新配置 + if (!config.apiKey || config.apiKey.trim() === '') { + return { success: false, error: '请输入 API Key' } + } + const providerInfo = llm.getProviderInfo(config.provider) + return llm.addConfig({ + name: providerInfo?.name || config.provider, + ...config, + }) + } + } catch (error) { + console.error('保存 LLM 配置失败:', error) + return { success: false, error: String(error) } + } + } + ) + + /** + * 发送 LLM 聊天请求(非流式) + */ + ipcMain.handle('llm:chat', async (_, messages: llm.ChatMessage[], options?: llm.ChatOptions) => { + aiLogger.info('IPC', '收到非流式 LLM 请求', { + messagesCount: messages.length, + firstMsgRole: messages[0]?.role, + firstMsgContentLen: messages[0]?.content?.length, + options, + }) + try { + const response = await llm.chat(messages, options) + aiLogger.info('IPC', '非流式 LLM 请求成功', { responseLength: response.length }) + return { success: true, content: response } + } catch (error) { + aiLogger.error('IPC', '非流式 LLM 请求失败', { error: String(error) }) + console.error('LLM 聊天失败:', error) + return { success: false, error: String(error) } + } + }) + + /** + * 发送 LLM 聊天请求(流式) + * 使用 IPC 事件发送流式数据 + */ + ipcMain.handle( + 'llm:chatStream', + async (_, requestId: string, messages: llm.ChatMessage[], options?: llm.ChatOptions) => { + aiLogger.info('IPC', `收到流式聊天请求: ${requestId}`, { + messagesCount: messages.length, + options, + }) + + try { + const generator = llm.chatStream(messages, options) + aiLogger.info('IPC', `创建流式生成器: ${requestId}`) + + // 异步处理流式响应 + ;(async () => { + let chunkIndex = 0 + try { + aiLogger.info('IPC', `开始迭代流式响应: ${requestId}`) + for await (const chunk of generator) { + chunkIndex++ + aiLogger.debug('IPC', `发送 chunk #${chunkIndex}: ${requestId}`, { + contentLength: chunk.content?.length, + isFinished: chunk.isFinished, + finishReason: chunk.finishReason, + }) + win.webContents.send('llm:streamChunk', { requestId, chunk }) + } + aiLogger.info('IPC', `流式响应完成: ${requestId}`, { totalChunks: chunkIndex }) + } catch (error) { + aiLogger.error('IPC', `流式响应出错: ${requestId}`, { + error: String(error), + chunkIndex, + }) + win.webContents.send('llm:streamChunk', { + requestId, + chunk: { content: '', isFinished: true, finishReason: 'error' }, + error: String(error), + }) + } + })() + + return { success: true } + } catch (error) { + aiLogger.error('IPC', `创建流式请求失败: ${requestId}`, { error: String(error) }) + console.error('LLM 流式聊天失败:', error) + return { success: false, error: String(error) } + } + } + ) + + // ==================== AI Agent API ==================== + + /** + * 执行 Agent 对话(流式) + * Agent 会自动调用工具并返回最终结果 + */ + ipcMain.handle('agent:runStream', async (_, requestId: string, userMessage: string, context: ToolContext) => { + aiLogger.info('IPC', `收到 Agent 流式请求: ${requestId}`, { + userMessage: userMessage.slice(0, 100), + sessionId: context.sessionId, + }) + + try { + // 创建 AbortController 并存储 + const abortController = new AbortController() + activeAgentRequests.set(requestId, abortController) + + const agent = new Agent(context, { abortSignal: abortController.signal }) + + // 异步执行,通过事件发送流式数据 + ;(async () => { + try { + const result = await agent.executeStream(userMessage, (chunk: AgentStreamChunk) => { + // 如果已中止,不再发送 + if (abortController.signal.aborted) { + return + } + aiLogger.debug('IPC', `Agent chunk: ${requestId}`, { + type: chunk.type, + contentLength: chunk.content?.length, + toolName: chunk.toolName, + }) + win.webContents.send('agent:streamChunk', { requestId, chunk }) + }) + + // 如果已中止,不发送完成信息 + if (abortController.signal.aborted) { + aiLogger.info('IPC', `Agent 已中止,跳过完成信息: ${requestId}`) + return + } + + // 发送完成信息 + win.webContents.send('agent:complete', { + requestId, + result: { + content: result.content, + toolsUsed: result.toolsUsed, + toolRounds: result.toolRounds, + }, + }) + + aiLogger.info('IPC', `Agent 执行完成: ${requestId}`, { + toolsUsed: result.toolsUsed, + toolRounds: result.toolRounds, + contentLength: result.content.length, + }) + } catch (error) { + // 如果是中止错误,不报告为错误 + if (error instanceof Error && error.name === 'AbortError') { + aiLogger.info('IPC', `Agent 请求已中止: ${requestId}`) + return + } + aiLogger.error('IPC', `Agent 执行出错: ${requestId}`, { error: String(error) }) + win.webContents.send('agent:streamChunk', { + requestId, + chunk: { type: 'error', error: String(error), isFinished: true }, + }) + } finally { + // 清理请求追踪 + activeAgentRequests.delete(requestId) + } + })() + + return { success: true } + } catch (error) { + aiLogger.error('IPC', `创建 Agent 请求失败: ${requestId}`, { error: String(error) }) + return { success: false, error: String(error) } + } + }) + + /** + * 中止 Agent 请求 + */ + ipcMain.handle('agent:abort', async (_, requestId: string) => { + aiLogger.info('IPC', `收到中止请求: ${requestId}`) + + const abortController = activeAgentRequests.get(requestId) + if (abortController) { + abortController.abort() + activeAgentRequests.delete(requestId) + aiLogger.info('IPC', `已中止 Agent 请求: ${requestId}`) + return { success: true } + } else { + aiLogger.warn('IPC', `未找到 Agent 请求: ${requestId}`) + return { success: false, error: '未找到该请求' } + } + }) +} diff --git a/electron/main/ipc/cache.ts b/electron/main/ipc/cache.ts new file mode 100644 index 0000000..8a613be --- /dev/null +++ b/electron/main/ipc/cache.ts @@ -0,0 +1,213 @@ +// electron/main/ipc/cache.ts +import { ipcMain, app, shell } from 'electron' +import * as fs from 'fs/promises' +import * as fsSync from 'fs' +import * as path from 'path' +import type { IpcContext } from './types' + +/** + * 获取 ChatLab 数据目录 + */ +function getChatLabDir(): string { + try { + const docPath = app.getPath('documents') + return path.join(docPath, 'ChatLab') + } catch { + return path.join(process.cwd(), 'ChatLab') + } +} + +/** + * 递归计算目录大小 + */ +async function getDirSize(dirPath: string): Promise { + let totalSize = 0 + try { + const exists = fsSync.existsSync(dirPath) + if (!exists) return 0 + + const files = await fs.readdir(dirPath, { withFileTypes: true }) + for (const file of files) { + const filePath = path.join(dirPath, file.name) + if (file.isDirectory()) { + totalSize += await getDirSize(filePath) + } else { + const stat = await fs.stat(filePath) + totalSize += stat.size + } + } + } catch (error) { + console.error('[Cache] Error getting dir size:', dirPath, error) + } + return totalSize +} + +/** + * 获取目录中的文件数量 + */ +async function getFileCount(dirPath: string): Promise { + let count = 0 + try { + const exists = fsSync.existsSync(dirPath) + if (!exists) return 0 + + const files = await fs.readdir(dirPath, { withFileTypes: true }) + for (const file of files) { + const filePath = path.join(dirPath, file.name) + if (file.isDirectory()) { + count += await getFileCount(filePath) + } else { + count++ + } + } + } catch (error) { + console.error('[Cache] Error getting file count:', dirPath, error) + } + return count +} + +export function registerCacheHandlers(_context: IpcContext): void { + console.log('[IPC] Registering cache handlers...') + + /** + * 获取所有缓存目录信息 + */ + ipcMain.handle('cache:getInfo', async () => { + const chatLabDir = getChatLabDir() + + // 定义缓存目录 + const cacheDirectories = [ + { + id: 'databases', + name: '聊天记录数据库', + description: '导入的聊天记录分析数据', + path: path.join(chatLabDir, 'databases'), + icon: 'i-heroicons-circle-stack', + canClear: false, // 不允许一键清理,因为是重要数据 + }, + { + id: 'ai', + name: 'AI 数据', + description: 'AI 对话历史和配置文件', + path: path.join(chatLabDir, 'ai'), + icon: 'i-heroicons-sparkles', + canClear: false, // 不允许一键清理 + }, + { + id: 'temp', + name: '临时文件', + description: '合并功能产生的临时数据库', + path: path.join(chatLabDir, 'temp'), + icon: 'i-heroicons-clock', + canClear: true, // 可以清理 + }, + { + id: 'logs', + name: '日志文件', + description: 'AI 调试日志', + path: path.join(chatLabDir, 'logs'), + icon: 'i-heroicons-document-text', + canClear: true, // 可以清理 + }, + ] + + // 获取每个目录的信息 + const results = await Promise.all( + cacheDirectories.map(async (dir) => { + const size = await getDirSize(dir.path) + const fileCount = await getFileCount(dir.path) + const exists = fsSync.existsSync(dir.path) + + return { + ...dir, + size, + fileCount, + exists, + } + }) + ) + + return { + baseDir: chatLabDir, + directories: results, + totalSize: results.reduce((sum, dir) => sum + dir.size, 0), + } + }) + + /** + * 清理指定缓存目录 + */ + ipcMain.handle('cache:clear', async (_, cacheId: string) => { + const chatLabDir = getChatLabDir() + + // 只允许清理 temp 和 logs + const allowedDirs: Record = { + temp: path.join(chatLabDir, 'temp'), + logs: path.join(chatLabDir, 'logs'), + } + + const dirPath = allowedDirs[cacheId] + if (!dirPath) { + return { success: false, error: '不允许清理此目录' } + } + + try { + const exists = fsSync.existsSync(dirPath) + if (!exists) { + return { success: true, message: '目录不存在,无需清理' } + } + + // 删除目录下的所有文件 + const files = await fs.readdir(dirPath) + for (const file of files) { + const filePath = path.join(dirPath, file) + const stat = await fs.stat(filePath) + if (stat.isDirectory()) { + await fs.rm(filePath, { recursive: true }) + } else { + await fs.unlink(filePath) + } + } + + console.log(`[Cache] Cleared directory: ${dirPath}`) + return { success: true } + } catch (error) { + console.error('[Cache] Error clearing cache:', error) + return { success: false, error: String(error) } + } + }) + + /** + * 在文件管理器中打开缓存目录 + */ + ipcMain.handle('cache:openDir', async (_, cacheId: string) => { + const chatLabDir = getChatLabDir() + + const dirPaths: Record = { + base: chatLabDir, + databases: path.join(chatLabDir, 'databases'), + ai: path.join(chatLabDir, 'ai'), + temp: path.join(chatLabDir, 'temp'), + logs: path.join(chatLabDir, 'logs'), + } + + const dirPath = dirPaths[cacheId] + if (!dirPath) { + return { success: false, error: '未知的目录' } + } + + try { + // 确保目录存在 + if (!fsSync.existsSync(dirPath)) { + await fs.mkdir(dirPath, { recursive: true }) + } + + await shell.openPath(dirPath) + return { success: true } + } catch (error) { + console.error('[Cache] Error opening directory:', error) + return { success: false, error: String(error) } + } + }) +} + diff --git a/electron/main/ipc/chat.ts b/electron/main/ipc/chat.ts new file mode 100644 index 0000000..8d4187e --- /dev/null +++ b/electron/main/ipc/chat.ts @@ -0,0 +1,523 @@ +/** + * 聊天记录导入与分析 IPC 处理器 + */ + +import { ipcMain, app, dialog } from 'electron' +import * as databaseCore from '../database/core' +import * as worker from '../worker/workerManager' +import * as parser from '../parser' +import { detectFormat, type ParseProgress } from '../parser' +import type { IpcContext } from './types' + +/** + * 注册聊天记录相关 IPC 处理器 + */ +export function registerChatHandlers(ctx: IpcContext): void { + const { win } = ctx + + // ==================== 聊天记录导入与分析 ==================== + + /** + * 选择聊天记录文件 + */ + ipcMain.handle('chat:selectFile', async () => { + try { + const { canceled, filePaths } = await dialog.showOpenDialog({ + title: '选择聊天记录文件', + defaultPath: app.getPath('documents'), + properties: ['openFile'], + filters: [ + { name: '聊天记录', extensions: ['json', 'txt'] }, + { name: '所有文件', extensions: ['*'] }, + ], + buttonLabel: '导入', + }) + + if (canceled || filePaths.length === 0) { + return null + } + + const filePath = filePaths[0] + console.log('[IpcMain] File selected:', filePath) + + // 检测文件格式(使用流式检测,只读取文件开头) + const formatFeature = detectFormat(filePath) + const format = formatFeature?.name || null + console.log('[IpcMain] Detected format:', format) + if (!format) { + return { error: '无法识别的文件格式' } + } + + return { filePath, format } + } catch (error) { + console.error('[IpcMain] Error selecting file:', error) + return { error: String(error) } + } + }) + + /** + * 导入聊天记录(流式版本) + */ + ipcMain.handle('chat:import', async (_, filePath: string) => { + console.log('[IpcMain] chat:import called with:', filePath) + + try { + // 发送进度:开始检测格式 + win.webContents.send('chat:importProgress', { + stage: 'detecting', + progress: 5, + message: '正在检测文件格式...', + }) + + // 使用流式导入(在 Worker 线程中执行) + const result = await worker.streamImport(filePath, (progress: ParseProgress) => { + // 转发进度到渲染进程 + win.webContents.send('chat:importProgress', { + stage: progress.stage, + progress: progress.percentage, + message: progress.message, + bytesRead: progress.bytesRead, + totalBytes: progress.totalBytes, + messagesProcessed: progress.messagesProcessed, + }) + }) + + if (result.success) { + console.log('[IpcMain] Stream import successful, sessionId:', result.sessionId) + return { success: true, sessionId: result.sessionId } + } else { + console.error('[IpcMain] Stream import failed:', result.error) + win.webContents.send('chat:importProgress', { + stage: 'error', + progress: 0, + message: result.error, + }) + return { success: false, error: result.error } + } + } catch (error) { + console.error('[IpcMain] Import failed:', error) + + win.webContents.send('chat:importProgress', { + stage: 'error', + progress: 0, + message: String(error), + }) + + return { success: false, error: String(error) } + } + }) + + /** + * 获取所有分析会话列表 + */ + ipcMain.handle('chat:getSessions', async () => { + try { + const sessions = await worker.getAllSessions() + return sessions + } catch (error) { + console.error('[IpcMain] Error getting sessions:', error) + return [] + } + }) + + /** + * 获取单个会话信息 + */ + ipcMain.handle('chat:getSession', async (_, sessionId: string) => { + try { + return await worker.getSession(sessionId) + } catch (error) { + console.error('获取会话信息失败:', error) + return null + } + }) + + /** + * 删除会话 + */ + ipcMain.handle('chat:deleteSession', async (_, sessionId: string) => { + try { + // 先关闭 Worker 中的数据库连接 + await worker.closeDatabase(sessionId) + // 然后删除文件(使用核心模块) + return databaseCore.deleteSession(sessionId) + } catch (error) { + console.error('删除会话失败:', error) + return false + } + }) + + /** + * 重命名会话 + */ + ipcMain.handle('chat:renameSession', async (_, sessionId: string, newName: string) => { + try { + // 先关闭 Worker 中的数据库连接(确保没有其他进程占用) + await worker.closeDatabase(sessionId) + // 执行重命名 + return databaseCore.renameSession(sessionId, newName) + } catch (error) { + console.error('重命名会话失败:', error) + return false + } + }) + + /** + * 获取可用年份列表 + */ + ipcMain.handle('chat:getAvailableYears', async (_, sessionId: string) => { + try { + return await worker.getAvailableYears(sessionId) + } catch (error) { + console.error('获取可用年份失败:', error) + return [] + } + }) + + /** + * 获取成员活跃度排行 + */ + ipcMain.handle( + 'chat:getMemberActivity', + async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { + try { + return await worker.getMemberActivity(sessionId, filter) + } catch (error) { + console.error('获取成员活跃度失败:', error) + return [] + } + } + ) + + /** + * 获取成员历史昵称 + */ + ipcMain.handle('chat:getMemberNameHistory', async (_, sessionId: string, memberId: number) => { + try { + return await worker.getMemberNameHistory(sessionId, memberId) + } catch (error) { + console.error('获取成员历史昵称失败:', error) + return [] + } + }) + + /** + * 获取每小时活跃度分布 + */ + ipcMain.handle( + 'chat:getHourlyActivity', + async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { + try { + return await worker.getHourlyActivity(sessionId, filter) + } catch (error) { + console.error('获取小时活跃度失败:', error) + return [] + } + } + ) + + /** + * 获取每日活跃度趋势 + */ + ipcMain.handle( + 'chat:getDailyActivity', + async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { + try { + return await worker.getDailyActivity(sessionId, filter) + } catch (error) { + console.error('获取日活跃度失败:', error) + return [] + } + } + ) + + /** + * 获取星期活跃度分布 + */ + ipcMain.handle( + 'chat:getWeekdayActivity', + async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { + try { + return await worker.getWeekdayActivity(sessionId, filter) + } catch (error) { + console.error('获取星期活跃度失败:', error) + return [] + } + } + ) + + /** + * 获取月份活跃度分布 + */ + ipcMain.handle( + 'chat:getMonthlyActivity', + async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { + try { + return await worker.getMonthlyActivity(sessionId, filter) + } catch (error) { + console.error('获取月份活跃度失败:', error) + return [] + } + } + ) + + /** + * 获取消息类型分布 + */ + ipcMain.handle( + 'chat:getMessageTypeDistribution', + async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { + try { + return await worker.getMessageTypeDistribution(sessionId, filter) + } catch (error) { + console.error('获取消息类型分布失败:', error) + return [] + } + } + ) + + /** + * 获取时间范围 + */ + ipcMain.handle('chat:getTimeRange', async (_, sessionId: string) => { + try { + return await worker.getTimeRange(sessionId) + } catch (error) { + console.error('获取时间范围失败:', error) + return null + } + }) + + /** + * 获取数据库存储目录 + */ + ipcMain.handle('chat:getDbDirectory', async () => { + try { + return worker.getDbDirectory() + } catch (error) { + console.error('获取数据库目录失败:', error) + return null + } + }) + + /** + * 获取支持的格式列表 + */ + ipcMain.handle('chat:getSupportedFormats', async () => { + return parser.getSupportedFormats() + }) + + /** + * 获取复读分析数据 + */ + ipcMain.handle( + 'chat:getRepeatAnalysis', + async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { + try { + return await worker.getRepeatAnalysis(sessionId, filter) + } catch (error) { + console.error('获取复读分析失败:', error) + return { originators: [], initiators: [], breakers: [], totalRepeatChains: 0 } + } + } + ) + + /** + * 获取口头禅分析数据 + */ + ipcMain.handle( + 'chat:getCatchphraseAnalysis', + async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { + try { + return await worker.getCatchphraseAnalysis(sessionId, filter) + } catch (error) { + console.error('获取口头禅分析失败:', error) + return { members: [] } + } + } + ) + + /** + * 获取夜猫分析数据 + */ + ipcMain.handle( + 'chat:getNightOwlAnalysis', + async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { + try { + return await worker.getNightOwlAnalysis(sessionId, filter) + } catch (error) { + console.error('获取夜猫分析失败:', error) + return { + nightOwlRank: [], + lastSpeakerRank: [], + firstSpeakerRank: [], + consecutiveRecords: [], + champions: [], + totalDays: 0, + } + } + } + ) + + /** + * 获取龙王分析数据 + */ + ipcMain.handle( + 'chat:getDragonKingAnalysis', + async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { + try { + return await worker.getDragonKingAnalysis(sessionId, filter) + } catch (error) { + console.error('获取龙王分析失败:', error) + return { rank: [], totalDays: 0 } + } + } + ) + + /** + * 获取潜水分析数据 + */ + ipcMain.handle( + 'chat:getDivingAnalysis', + async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { + try { + return await worker.getDivingAnalysis(sessionId, filter) + } catch (error) { + console.error('获取潜水分析失败:', error) + return { rank: [] } + } + } + ) + + /** + * 获取自言自语分析数据 + */ + ipcMain.handle( + 'chat:getMonologueAnalysis', + async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { + try { + return await worker.getMonologueAnalysis(sessionId, filter) + } catch (error) { + console.error('获取自言自语分析失败:', error) + return { rank: [], maxComboRecord: null } + } + } + ) + + /** + * 获取 @ 互动分析数据 + */ + ipcMain.handle( + 'chat:getMentionAnalysis', + async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { + try { + return await worker.getMentionAnalysis(sessionId, filter) + } catch (error) { + console.error('获取 @ 互动分析失败:', error) + return { topMentioners: [], topMentioned: [], oneWay: [], twoWay: [], totalMentions: 0, memberDetails: [] } + } + } + ) + + /** + * 获取含笑量分析数据 + */ + ipcMain.handle( + 'chat:getLaughAnalysis', + async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }, keywords?: string[]) => { + try { + return await worker.getLaughAnalysis(sessionId, filter, keywords) + } catch (error) { + console.error('获取含笑量分析失败:', error) + return { + rankByRate: [], + rankByCount: [], + typeDistribution: [], + totalLaughs: 0, + totalMessages: 0, + groupLaughRate: 0, + } + } + } + ) + + /** + * 获取斗图分析数据 + */ + ipcMain.handle( + 'chat:getMemeBattleAnalysis', + async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { + try { + return await worker.getMemeBattleAnalysis(sessionId, filter) + } catch (error) { + console.error('获取斗图分析失败:', error) + return { + longestBattle: null, + rankByCount: [], + rankByImageCount: [], + totalBattles: 0, + } + } + } + ) + + /** + * 获取打卡分析数据(火花榜 + 忠臣榜) + */ + ipcMain.handle( + 'chat:getCheckInAnalysis', + async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { + try { + return await worker.getCheckInAnalysis(sessionId, filter) + } catch (error) { + console.error('获取打卡分析失败:', error) + return { + streakRank: [], + loyaltyRank: [], + totalDays: 0, + } + } + } + ) + + // ==================== 成员管理 ==================== + + /** + * 获取所有成员列表(含消息数和别名) + */ + ipcMain.handle('chat:getMembers', async (_, sessionId: string) => { + try { + return await worker.getMembers(sessionId) + } catch (error) { + console.error('获取成员列表失败:', error) + return [] + } + }) + + /** + * 更新成员别名 + */ + ipcMain.handle('chat:updateMemberAliases', async (_, sessionId: string, memberId: number, aliases: string[]) => { + try { + return await worker.updateMemberAliases(sessionId, memberId, aliases) + } catch (error) { + console.error('更新成员别名失败:', error) + return false + } + }) + + /** + * 删除成员及其所有消息 + */ + ipcMain.handle('chat:deleteMember', async (_, sessionId: string, memberId: number) => { + try { + // 先关闭数据库连接 + await worker.closeDatabase(sessionId) + // 执行删除 + return await worker.deleteMember(sessionId, memberId) + } catch (error) { + console.error('删除成员失败:', error) + return false + } + }) +} diff --git a/electron/main/ipc/merge.ts b/electron/main/ipc/merge.ts new file mode 100644 index 0000000..4e091f1 --- /dev/null +++ b/electron/main/ipc/merge.ts @@ -0,0 +1,133 @@ +/** + * 合并功能 IPC 处理器 + */ + +import { ipcMain } from 'electron' +import * as worker from '../worker/workerManager' +import * as merger from '../merger' +import { deleteTempDatabase, cleanupAllTempDatabases } from '../merger/tempCache' +import type { ParseProgress } from '../parser' +import type { MergeParams } from '../../../src/types/chat' +import type { IpcContext } from './types' + +// ==================== 临时数据库缓存 ==================== +// 用于合并功能:缓存文件对应的临时数据库路径 +// 这样用户删除本地文件后仍然可以进行合并(数据已存入临时数据库) +const tempDbCache = new Map() + +/** + * 清理指定文件的缓存(删除临时数据库) + */ +function clearTempDbCache(filePath: string): void { + const tempDbPath = tempDbCache.get(filePath) + if (tempDbPath) { + deleteTempDatabase(tempDbPath) + tempDbCache.delete(filePath) + } +} + +/** + * 清理所有缓存(删除所有临时数据库) + */ +function clearAllTempDbCache(): void { + for (const tempDbPath of tempDbCache.values()) { + deleteTempDatabase(tempDbPath) + } + tempDbCache.clear() + console.log('[IpcMain] 已清理所有临时数据库缓存') +} + +/** + * 初始化合并模块(清理残留临时数据库) + */ +export function initMergeModule(): void { + cleanupAllTempDatabases() +} + +/** + * 注册合并功能 IPC 处理器 + */ +export function registerMergeHandlers(ctx: IpcContext): void { + const { win } = ctx + + /** + * 解析文件获取基本信息(用于合并预览) + * 使用流式解析,数据写入临时数据库,避免内存溢出 + */ + ipcMain.handle('merge:parseFileInfo', async (_, filePath: string) => { + try { + // 使用流式解析,写入临时数据库 + const result = await worker.streamParseFileInfo(filePath, (progress: ParseProgress) => { + // 可选:发送进度到渲染进程 + win.webContents.send('merge:parseProgress', { + filePath, + progress, + }) + }) + + // 缓存临时数据库路径(用于后续合并) + // 这样即使用户删除本地文件,也能继续合并(数据已在临时数据库中) + if (result.tempDbPath) { + tempDbCache.set(filePath, result.tempDbPath) + console.log(`[IpcMain] 已缓存临时数据库: ${filePath} -> ${result.tempDbPath}`) + } + + // 返回基本信息 + return { + name: result.name, + format: result.format, + platform: result.platform, + messageCount: result.messageCount, + memberCount: result.memberCount, + fileSize: result.fileSize, + } + } catch (error) { + console.error('解析文件信息失败:', error) + throw error + } + }) + + /** + * 检测合并冲突(使用临时数据库) + */ + ipcMain.handle('merge:checkConflicts', async (_, filePaths: string[]) => { + try { + return merger.checkConflictsWithTempDb(filePaths, tempDbCache) + } catch (error) { + console.error('检测冲突失败:', error) + throw error + } + }) + + /** + * 执行合并(使用临时数据库) + */ + ipcMain.handle('merge:mergeFiles', async (_, params: MergeParams) => { + try { + const result = await merger.mergeFilesWithTempDb(params, tempDbCache) + // 合并完成后清理缓存 + if (result.success) { + for (const filePath of params.filePaths) { + clearTempDbCache(filePath) + } + } + return result + } catch (error) { + console.error('合并失败:', error) + return { success: false, error: String(error) } + } + }) + + /** + * 清理合并缓存(用于用户移除文件时) + */ + ipcMain.handle('merge:clearCache', async (_, filePath?: string) => { + if (filePath) { + clearTempDbCache(filePath) + } else { + clearAllTempDbCache() + } + return true + }) +} + diff --git a/electron/main/ipc/types.ts b/electron/main/ipc/types.ts new file mode 100644 index 0000000..6e4d3b9 --- /dev/null +++ b/electron/main/ipc/types.ts @@ -0,0 +1,18 @@ +/** + * IPC 处理器共享类型 + */ + +import type { BrowserWindow } from 'electron' + +/** + * IPC 处理器注册上下文 + */ +export interface IpcContext { + /** 主窗口 */ + win: BrowserWindow +} + +/** + * IPC 处理器注册函数类型 + */ +export type IpcHandlerRegister = (ctx: IpcContext) => void diff --git a/electron/main/ipc/window.ts b/electron/main/ipc/window.ts new file mode 100644 index 0000000..27f8131 --- /dev/null +++ b/electron/main/ipc/window.ts @@ -0,0 +1,129 @@ +/** + * 窗口和文件系统操作 IPC 处理器 + */ + +import { ipcMain, app, dialog, clipboard, shell } from 'electron' +import { autoUpdater } from 'electron-updater' +import * as fs from 'fs/promises' +import type { IpcContext } from './types' + +/** + * 注册窗口和文件系统操作 IPC 处理器 + */ +export function registerWindowHandlers(ctx: IpcContext): void { + const { win } = ctx + + // ==================== 窗口操作 ==================== + ipcMain.on('window-min', (ev) => { + ev.preventDefault() + win.minimize() + }) + + ipcMain.on('window-maxOrRestore', (ev) => { + const winSizeState = win.isMaximized() + winSizeState ? win.restore() : win.maximize() + ev.reply('windowState', win.isMaximized()) + }) + + ipcMain.on('window-restore', () => { + win.restore() + }) + + ipcMain.on('window-hide', () => { + win.hide() + }) + + ipcMain.on('window-close', () => { + win.close() + // @ts-ignore + app.isQuitting = true + app.quit() + }) + + ipcMain.on('window-resize', (_, data) => { + if (data.resize) { + win.setResizable(true) + } else { + win.setSize(1180, 752) + win.setResizable(false) + } + }) + + ipcMain.on('open-devtools', () => { + win.webContents.openDevTools() + }) + + // ==================== 更新检查 ==================== + ipcMain.on('check-update', () => { + autoUpdater.checkForUpdates() + }) + + // ==================== 通用工具 ==================== + ipcMain.handle('show-message', (event, args) => { + event.sender.send('show-message', args) + }) + + // 复制到剪贴板 + ipcMain.handle('copyData', async (_, data) => { + try { + clipboard.writeText(data) + return true + } catch (error) { + console.error('复制操作出错:', error) + return false + } + }) + + // ==================== 文件系统操作 ==================== + // 选择文件夹 + ipcMain.handle('selectDir', async (_, defaultPath = '') => { + try { + const { canceled, filePaths } = await dialog.showOpenDialog({ + title: '选择目录', + defaultPath: defaultPath || app.getPath('documents'), + properties: ['openDirectory', 'createDirectory'], + buttonLabel: '选择文件夹', + }) + if (!canceled) { + return filePaths[0] + } + return null + } catch (err) { + console.error('选择文件夹时发生错误:', err) + throw err + } + }) + + // 检查文件是否存在 + ipcMain.handle('checkFileExist', async (_, filePath) => { + try { + await fs.access(filePath) + return true + } catch { + return false + } + }) + + // 在文件管理器中打开 + ipcMain.handle('openInFolder', async (_, path) => { + try { + await fs.access(path) + await shell.showItemInFolder(path) + return true + } catch (error) { + console.error('打开目录时出错:', error) + return false + } + }) + + // 显示打开对话框(通用) + ipcMain.handle('dialog:showOpenDialog', async (_, options) => { + try { + return await dialog.showOpenDialog(options) + } catch (error) { + console.error('显示对话框失败:', error) + throw error + } + }) +} + diff --git a/electron/main/ipcMain.ts b/electron/main/ipcMain.ts index 272b54f..ddce522 100644 --- a/electron/main/ipcMain.ts +++ b/electron/main/ipcMain.ts @@ -1,67 +1,28 @@ -import { ipcMain, app, dialog, clipboard, shell, BrowserWindow } from 'electron' -import { autoUpdater } from 'electron-updater' -import * as fs from 'fs/promises' -import * as fsSync from 'fs' +/** + * IPC 主入口文件 + * 模块化结构,各功能模块位于 ./ipc/ 目录下 + */ +import { BrowserWindow } from 'electron' +import type { IpcContext } from './ipc/types' -// 导入数据库核心模块(用于导入和删除操作) -import * as databaseCore from './database/core' +// 导入各功能模块 +import { registerWindowHandlers } from './ipc/window' +import { registerChatHandlers } from './ipc/chat' +import { registerMergeHandlers, initMergeModule } from './ipc/merge' +import { registerAIHandlers } from './ipc/ai' +import { registerCacheHandlers } from './ipc/cache' // 导入 Worker 模块(用于异步分析查询和流式导入) -import * as worker from './worker' -// 导入解析器模块 -import * as parser from './parser' -import { detectFormat, type ParseProgress } from './parser' -// 导入合并模块 -import * as merger from './merger' -import { deleteTempDatabase, cleanupAllTempDatabases } from './merger/tempCache' -// 导入 AI 对话管理模块 -import * as aiConversations from './ai/conversations' -// 导入 LLM 服务模块 -import * as llm from './ai/llm' -// 导入 AI 日志模块 -import { aiLogger } from './ai/logger' -// 导入 AI Agent 模块 -import { Agent, type AgentStreamChunk } from './ai/agent' -import type { ToolContext } from './ai/tools/types' -import type { MergeParams } from '../../src/types/chat' - -console.log('[IpcMain] Database, Worker and Parser modules imported') - -// ==================== 临时数据库缓存 ==================== -// 用于合并功能:缓存文件对应的临时数据库路径 -// 这样用户删除本地文件后仍然可以进行合并(数据已存入临时数据库) -const tempDbCache = new Map() - -// ==================== AI Agent 请求追踪 ==================== -// 用于跟踪活跃的 Agent 请求,支持中止操作 -const activeAgentRequests = new Map() +import * as worker from './worker/workerManager' /** - * 清理指定文件的缓存(删除临时数据库) + * 初始化所有 IPC 处理器 + * @param win - 主窗口实例 */ -function clearTempDbCache(filePath: string): void { - const tempDbPath = tempDbCache.get(filePath) - if (tempDbPath) { - deleteTempDatabase(tempDbPath) - tempDbCache.delete(filePath) - } -} - -/** - * 清理所有缓存(删除所有临时数据库) - */ -function clearAllTempDbCache(): void { - for (const tempDbPath of tempDbCache.values()) { - deleteTempDatabase(tempDbPath) - } - tempDbCache.clear() - console.log('[IpcMain] 已清理所有临时数据库缓存') -} - const mainIpcMain = (win: BrowserWindow) => { console.log('[IpcMain] Registering IPC handlers...') - // 清理残留的临时数据库(上次崩溃可能残留) - cleanupAllTempDatabases() + // 初始化合并模块(清理残留的临时数据库) + initMergeModule() // 初始化 Worker try { @@ -70,1250 +31,17 @@ const mainIpcMain = (win: BrowserWindow) => { } catch (error) { console.error('[IpcMain] Failed to initialize worker:', error) } - // ==================== 窗口操作 ==================== - ipcMain.on('window-min', (ev) => { - ev.preventDefault() - win.minimize() - }) - ipcMain.on('window-maxOrRestore', (ev) => { - const winSizeState = win.isMaximized() - winSizeState ? win.restore() : win.maximize() - ev.reply('windowState', win.isMaximized()) - }) + const context: IpcContext = { win } - ipcMain.on('window-restore', () => { - win.restore() - }) + // 注册各模块的处理器 + registerWindowHandlers(context) + registerChatHandlers(context) + registerMergeHandlers(context) + registerAIHandlers(context) + registerCacheHandlers(context) - ipcMain.on('window-hide', () => { - win.hide() - }) - - ipcMain.on('window-close', () => { - win.close() - // @ts-ignore - app.isQuitting = true - app.quit() - }) - - ipcMain.on('window-resize', (_, data) => { - if (data.resize) { - win.setResizable(true) - } else { - win.setSize(1180, 752) - win.setResizable(false) - } - }) - - ipcMain.on('open-devtools', () => { - win.webContents.openDevTools() - }) - - // ==================== 更新检查 ==================== - ipcMain.on('check-update', () => { - autoUpdater.checkForUpdates() - }) - - // ==================== 通用工具 ==================== - ipcMain.handle('show-message', (event, args) => { - event.sender.send('show-message', args) - }) - - // 复制到剪贴板 - ipcMain.handle('copyData', async (_, data) => { - try { - clipboard.writeText(data) - return true - } catch (error) { - console.error('复制操作出错:', error) - return false - } - }) - - // ==================== 文件系统操作 ==================== - // 选择文件夹 - ipcMain.handle('selectDir', async (_, defaultPath = '') => { - try { - const { canceled, filePaths } = await dialog.showOpenDialog({ - title: '选择目录', - defaultPath: defaultPath || app.getPath('documents'), - properties: ['openDirectory', 'createDirectory'], - buttonLabel: '选择文件夹', - }) - if (!canceled) { - return filePaths[0] - } - return null - } catch (err) { - console.error('选择文件夹时发生错误:', err) - throw err - } - }) - - // 检查文件是否存在 - ipcMain.handle('checkFileExist', async (_, filePath) => { - try { - await fs.access(filePath) - return true - } catch { - return false - } - }) - - // 在文件管理器中打开 - ipcMain.handle('openInFolder', async (_, path) => { - try { - await fs.access(path) - await shell.showItemInFolder(path) - return true - } catch (error) { - console.error('打开目录时出错:', error) - return false - } - }) - - // ==================== 聊天记录导入与分析 ==================== - - /** - * 选择聊天记录文件 - */ - ipcMain.handle('chat:selectFile', async () => { - try { - const { canceled, filePaths } = await dialog.showOpenDialog({ - title: '选择聊天记录文件', - defaultPath: app.getPath('documents'), - properties: ['openFile'], - filters: [ - { name: '聊天记录', extensions: ['json', 'txt'] }, - { name: '所有文件', extensions: ['*'] }, - ], - buttonLabel: '导入', - }) - - if (canceled || filePaths.length === 0) { - return null - } - - const filePath = filePaths[0] - console.log('[IpcMain] File selected:', filePath) - - // 检测文件格式(使用流式检测,只读取文件开头) - const formatFeature = detectFormat(filePath) - const format = formatFeature?.name || null - console.log('[IpcMain] Detected format:', format) - if (!format) { - return { error: '无法识别的文件格式' } - } - - return { filePath, format } - } catch (error) { - console.error('[IpcMain] Error selecting file:', error) - return { error: String(error) } - } - }) - - /** - * 导入聊天记录(流式版本) - */ - ipcMain.handle('chat:import', async (_, filePath: string) => { - console.log('[IpcMain] chat:import called with:', filePath) - - try { - // 发送进度:开始检测格式 - win.webContents.send('chat:importProgress', { - stage: 'detecting', - progress: 5, - message: '正在检测文件格式...', - }) - - // 使用流式导入(在 Worker 线程中执行) - const result = await worker.streamImport(filePath, (progress: ParseProgress) => { - // 转发进度到渲染进程 - win.webContents.send('chat:importProgress', { - stage: progress.stage, - progress: progress.percentage, - message: progress.message, - bytesRead: progress.bytesRead, - totalBytes: progress.totalBytes, - messagesProcessed: progress.messagesProcessed, - }) - }) - - if (result.success) { - console.log('[IpcMain] Stream import successful, sessionId:', result.sessionId) - return { success: true, sessionId: result.sessionId } - } else { - console.error('[IpcMain] Stream import failed:', result.error) - win.webContents.send('chat:importProgress', { - stage: 'error', - progress: 0, - message: result.error, - }) - return { success: false, error: result.error } - } - } catch (error) { - console.error('[IpcMain] Import failed:', error) - - win.webContents.send('chat:importProgress', { - stage: 'error', - progress: 0, - message: String(error), - }) - - return { success: false, error: String(error) } - } - }) - - /** - * 获取所有分析会话列表 - */ - ipcMain.handle('chat:getSessions', async () => { - try { - const sessions = await worker.getAllSessions() - return sessions - } catch (error) { - console.error('[IpcMain] Error getting sessions:', error) - return [] - } - }) - - /** - * 获取单个会话信息 - */ - ipcMain.handle('chat:getSession', async (_, sessionId: string) => { - try { - return await worker.getSession(sessionId) - } catch (error) { - console.error('获取会话信息失败:', error) - return null - } - }) - - /** - * 删除会话 - */ - ipcMain.handle('chat:deleteSession', async (_, sessionId: string) => { - try { - // 先关闭 Worker 中的数据库连接 - await worker.closeDatabase(sessionId) - // 然后删除文件(使用核心模块) - return databaseCore.deleteSession(sessionId) - } catch (error) { - console.error('删除会话失败:', error) - return false - } - }) - - /** - * 重命名会话 - */ - ipcMain.handle('chat:renameSession', async (_, sessionId: string, newName: string) => { - try { - // 先关闭 Worker 中的数据库连接(确保没有其他进程占用) - await worker.closeDatabase(sessionId) - // 执行重命名 - return databaseCore.renameSession(sessionId, newName) - } catch (error) { - console.error('重命名会话失败:', error) - return false - } - }) - - /** - * 获取可用年份列表 - */ - ipcMain.handle('chat:getAvailableYears', async (_, sessionId: string) => { - try { - return await worker.getAvailableYears(sessionId) - } catch (error) { - console.error('获取可用年份失败:', error) - return [] - } - }) - - /** - * 获取成员活跃度排行 - */ - ipcMain.handle( - 'chat:getMemberActivity', - async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { - try { - return await worker.getMemberActivity(sessionId, filter) - } catch (error) { - console.error('获取成员活跃度失败:', error) - return [] - } - } - ) - - /** - * 获取成员历史昵称 - */ - ipcMain.handle('chat:getMemberNameHistory', async (_, sessionId: string, memberId: number) => { - try { - return await worker.getMemberNameHistory(sessionId, memberId) - } catch (error) { - console.error('获取成员历史昵称失败:', error) - return [] - } - }) - - /** - * 获取每小时活跃度分布 - */ - ipcMain.handle( - 'chat:getHourlyActivity', - async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { - try { - return await worker.getHourlyActivity(sessionId, filter) - } catch (error) { - console.error('获取小时活跃度失败:', error) - return [] - } - } - ) - - /** - * 获取每日活跃度趋势 - */ - ipcMain.handle( - 'chat:getDailyActivity', - async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { - try { - return await worker.getDailyActivity(sessionId, filter) - } catch (error) { - console.error('获取日活跃度失败:', error) - return [] - } - } - ) - - /** - * 获取星期活跃度分布 - */ - ipcMain.handle( - 'chat:getWeekdayActivity', - async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { - try { - return await worker.getWeekdayActivity(sessionId, filter) - } catch (error) { - console.error('获取星期活跃度失败:', error) - return [] - } - } - ) - - /** - * 获取月份活跃度分布 - */ - ipcMain.handle( - 'chat:getMonthlyActivity', - async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { - try { - return await worker.getMonthlyActivity(sessionId, filter) - } catch (error) { - console.error('获取月份活跃度失败:', error) - return [] - } - } - ) - - /** - * 获取消息类型分布 - */ - ipcMain.handle( - 'chat:getMessageTypeDistribution', - async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { - try { - return await worker.getMessageTypeDistribution(sessionId, filter) - } catch (error) { - console.error('获取消息类型分布失败:', error) - return [] - } - } - ) - - /** - * 获取时间范围 - */ - ipcMain.handle('chat:getTimeRange', async (_, sessionId: string) => { - try { - return await worker.getTimeRange(sessionId) - } catch (error) { - console.error('获取时间范围失败:', error) - return null - } - }) - - /** - * 获取数据库存储目录 - */ - ipcMain.handle('chat:getDbDirectory', async () => { - try { - return worker.getDbDirectory() - } catch (error) { - console.error('获取数据库目录失败:', error) - return null - } - }) - - /** - * 获取支持的格式列表 - */ - ipcMain.handle('chat:getSupportedFormats', async () => { - return parser.getSupportedFormats() - }) - - /** - * 获取复读分析数据 - */ - ipcMain.handle( - 'chat:getRepeatAnalysis', - async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { - try { - return await worker.getRepeatAnalysis(sessionId, filter) - } catch (error) { - console.error('获取复读分析失败:', error) - return { originators: [], initiators: [], breakers: [], totalRepeatChains: 0 } - } - } - ) - - /** - * 获取口头禅分析数据 - */ - ipcMain.handle( - 'chat:getCatchphraseAnalysis', - async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { - try { - return await worker.getCatchphraseAnalysis(sessionId, filter) - } catch (error) { - console.error('获取口头禅分析失败:', error) - return { members: [] } - } - } - ) - - /** - * 获取夜猫分析数据 - */ - ipcMain.handle( - 'chat:getNightOwlAnalysis', - async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { - try { - return await worker.getNightOwlAnalysis(sessionId, filter) - } catch (error) { - console.error('获取夜猫分析失败:', error) - return { - nightOwlRank: [], - lastSpeakerRank: [], - firstSpeakerRank: [], - consecutiveRecords: [], - champions: [], - totalDays: 0, - } - } - } - ) - - /** - * 获取龙王分析数据 - */ - ipcMain.handle( - 'chat:getDragonKingAnalysis', - async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { - try { - return await worker.getDragonKingAnalysis(sessionId, filter) - } catch (error) { - console.error('获取龙王分析失败:', error) - return { rank: [], totalDays: 0 } - } - } - ) - - /** - * 获取潜水分析数据 - */ - ipcMain.handle( - 'chat:getDivingAnalysis', - async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { - try { - return await worker.getDivingAnalysis(sessionId, filter) - } catch (error) { - console.error('获取潜水分析失败:', error) - return { rank: [] } - } - } - ) - - /** - * 获取自言自语分析数据 - */ - ipcMain.handle( - 'chat:getMonologueAnalysis', - async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { - try { - return await worker.getMonologueAnalysis(sessionId, filter) - } catch (error) { - console.error('获取自言自语分析失败:', error) - return { rank: [], maxComboRecord: null } - } - } - ) - - /** - * 获取 @ 互动分析数据 - */ - ipcMain.handle( - 'chat:getMentionAnalysis', - async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { - try { - return await worker.getMentionAnalysis(sessionId, filter) - } catch (error) { - console.error('获取 @ 互动分析失败:', error) - return { topMentioners: [], topMentioned: [], oneWay: [], twoWay: [], totalMentions: 0, memberDetails: [] } - } - } - ) - - /** - * 获取含笑量分析数据 - */ - ipcMain.handle( - 'chat:getLaughAnalysis', - async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }, keywords?: string[]) => { - try { - return await worker.getLaughAnalysis(sessionId, filter, keywords) - } catch (error) { - console.error('获取含笑量分析失败:', error) - return { - rankByRate: [], - rankByCount: [], - typeDistribution: [], - totalLaughs: 0, - totalMessages: 0, - groupLaughRate: 0, - } - } - } - ) - - /** - * 获取斗图分析数据 - */ - ipcMain.handle( - 'chat:getMemeBattleAnalysis', - async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { - try { - return await worker.getMemeBattleAnalysis(sessionId, filter) - } catch (error) { - console.error('获取斗图分析失败:', error) - return { - longestBattle: null, - rankByCount: [], - rankByImageCount: [], - totalBattles: 0, - } - } - } - ) - - /** - * 获取打卡分析数据(火花榜 + 忠臣榜) - */ - ipcMain.handle( - 'chat:getCheckInAnalysis', - async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { - try { - return await worker.getCheckInAnalysis(sessionId, filter) - } catch (error) { - console.error('获取打卡分析失败:', error) - return { - streakRank: [], - loyaltyRank: [], - totalDays: 0, - } - } - } - ) - - // ==================== 成员管理 ==================== - - /** - * 获取所有成员列表(含消息数和别名) - */ - ipcMain.handle('chat:getMembers', async (_, sessionId: string) => { - try { - return await worker.getMembers(sessionId) - } catch (error) { - console.error('获取成员列表失败:', error) - return [] - } - }) - - /** - * 更新成员别名 - */ - ipcMain.handle('chat:updateMemberAliases', async (_, sessionId: string, memberId: number, aliases: string[]) => { - try { - return await worker.updateMemberAliases(sessionId, memberId, aliases) - } catch (error) { - console.error('更新成员别名失败:', error) - return false - } - }) - - /** - * 删除成员及其所有消息 - */ - ipcMain.handle('chat:deleteMember', async (_, sessionId: string, memberId: number) => { - try { - // 先关闭数据库连接 - await worker.closeDatabase(sessionId) - // 执行删除 - return await worker.deleteMember(sessionId, memberId) - } catch (error) { - console.error('删除成员失败:', error) - return false - } - }) - - // ==================== 合并功能 ==================== - - /** - * 解析文件获取基本信息(用于合并预览) - * 使用流式解析,数据写入临时数据库,避免内存溢出 - */ - ipcMain.handle('merge:parseFileInfo', async (_, filePath: string) => { - try { - // 使用流式解析,写入临时数据库 - const result = await worker.streamParseFileInfo(filePath, (progress: ParseProgress) => { - // 可选:发送进度到渲染进程 - win.webContents.send('merge:parseProgress', { - filePath, - progress, - }) - }) - - // 缓存临时数据库路径(用于后续合并) - // 这样即使用户删除本地文件,也能继续合并(数据已在临时数据库中) - if (result.tempDbPath) { - tempDbCache.set(filePath, result.tempDbPath) - console.log(`[IpcMain] 已缓存临时数据库: ${filePath} -> ${result.tempDbPath}`) - } - - // 返回基本信息 - return { - name: result.name, - format: result.format, - platform: result.platform, - messageCount: result.messageCount, - memberCount: result.memberCount, - fileSize: result.fileSize, - } - } catch (error) { - console.error('解析文件信息失败:', error) - throw error - } - }) - - /** - * 检测合并冲突(使用临时数据库) - */ - ipcMain.handle('merge:checkConflicts', async (_, filePaths: string[]) => { - try { - return merger.checkConflictsWithTempDb(filePaths, tempDbCache) - } catch (error) { - console.error('检测冲突失败:', error) - throw error - } - }) - - /** - * 执行合并(使用临时数据库) - */ - ipcMain.handle('merge:mergeFiles', async (_, params: MergeParams) => { - try { - const result = await merger.mergeFilesWithTempDb(params, tempDbCache) - // 合并完成后清理缓存 - if (result.success) { - for (const filePath of params.filePaths) { - clearTempDbCache(filePath) - } - } - return result - } catch (error) { - console.error('合并失败:', error) - return { success: false, error: String(error) } - } - }) - - /** - * 清理合并缓存(用于用户移除文件时) - */ - ipcMain.handle('merge:clearCache', async (_, filePath?: string) => { - if (filePath) { - clearTempDbCache(filePath) - } else { - clearAllTempDbCache() - } - return true - }) - - /** - * 显示打开对话框(通用) - */ - ipcMain.handle('dialog:showOpenDialog', async (_, options) => { - try { - return await dialog.showOpenDialog(options) - } catch (error) { - console.error('显示对话框失败:', error) - throw error - } - }) - - // ==================== AI 功能 ==================== - - /** - * 搜索消息(关键词搜索) - */ - ipcMain.handle( - 'ai:searchMessages', - async ( - _, - sessionId: string, - keywords: string[], - filter?: { startTs?: number; endTs?: number }, - limit?: number, - offset?: number - ) => { - aiLogger.info('IPC', '收到搜索消息请求', { - sessionId, - keywords, - filter, - limit, - offset, - }) - try { - const result = await worker.searchMessages(sessionId, keywords, filter, limit, offset) - aiLogger.info('IPC', '搜索消息完成', { - total: result.total, - returned: result.messages.length, - }) - return result - } catch (error) { - aiLogger.error('IPC', '搜索消息失败', { error: String(error) }) - console.error('搜索消息失败:', error) - return { messages: [], total: 0 } - } - } - ) - - /** - * 获取消息上下文 - */ - ipcMain.handle('ai:getMessageContext', async (_, sessionId: string, messageId: number, contextSize?: number) => { - try { - return await worker.getMessageContext(sessionId, messageId, contextSize) - } catch (error) { - console.error('获取消息上下文失败:', error) - return [] - } - }) - - /** - * 创建 AI 对话 - */ - ipcMain.handle('ai:createConversation', async (_, sessionId: string, title?: string) => { - try { - return aiConversations.createConversation(sessionId, title) - } catch (error) { - console.error('创建 AI 对话失败:', error) - throw error - } - }) - - /** - * 获取会话的所有 AI 对话列表 - */ - ipcMain.handle('ai:getConversations', async (_, sessionId: string) => { - try { - return aiConversations.getConversations(sessionId) - } catch (error) { - console.error('获取 AI 对话列表失败:', error) - return [] - } - }) - - /** - * 获取单个 AI 对话 - */ - ipcMain.handle('ai:getConversation', async (_, conversationId: string) => { - try { - return aiConversations.getConversation(conversationId) - } catch (error) { - console.error('获取 AI 对话失败:', error) - return null - } - }) - - /** - * 更新 AI 对话标题 - */ - ipcMain.handle('ai:updateConversationTitle', async (_, conversationId: string, title: string) => { - try { - return aiConversations.updateConversationTitle(conversationId, title) - } catch (error) { - console.error('更新 AI 对话标题失败:', error) - return false - } - }) - - /** - * 删除 AI 对话 - */ - ipcMain.handle('ai:deleteConversation', async (_, conversationId: string) => { - try { - return aiConversations.deleteConversation(conversationId) - } catch (error) { - console.error('删除 AI 对话失败:', error) - return false - } - }) - - /** - * 添加 AI 消息 - */ - ipcMain.handle( - 'ai:addMessage', - async ( - _, - conversationId: string, - role: 'user' | 'assistant', - content: string, - dataKeywords?: string[], - dataMessageCount?: number - ) => { - try { - return aiConversations.addMessage(conversationId, role, content, dataKeywords, dataMessageCount) - } catch (error) { - console.error('添加 AI 消息失败:', error) - throw error - } - } - ) - - /** - * 获取 AI 对话的所有消息 - */ - ipcMain.handle('ai:getMessages', async (_, conversationId: string) => { - try { - return aiConversations.getMessages(conversationId) - } catch (error) { - console.error('获取 AI 消息失败:', error) - return [] - } - }) - - /** - * 删除 AI 消息 - */ - ipcMain.handle('ai:deleteMessage', async (_, messageId: string) => { - try { - return aiConversations.deleteMessage(messageId) - } catch (error) { - console.error('删除 AI 消息失败:', error) - return false - } - }) - - // ==================== LLM 服务(多配置管理)==================== - - /** - * 获取所有支持的 LLM 提供商 - */ - ipcMain.handle('llm:getProviders', async () => { - return llm.PROVIDERS - }) - - /** - * 获取所有配置列表 - */ - ipcMain.handle('llm:getAllConfigs', async () => { - const configs = llm.getAllConfigs() - // 脱敏 API Key - return configs.map((c) => ({ - ...c, - apiKey: c.apiKey ? `${c.apiKey.slice(0, 4)}****${c.apiKey.slice(-4)}` : '', - apiKeySet: !!c.apiKey, - })) - }) - - /** - * 获取当前激活的配置 ID - */ - ipcMain.handle('llm:getActiveConfigId', async () => { - const config = llm.getActiveConfig() - return config?.id || null - }) - - /** - * 添加新配置 - */ - ipcMain.handle( - 'llm:addConfig', - async ( - _, - config: { - name: string - provider: llm.LLMProvider - apiKey: string - model?: string - baseUrl?: string - maxTokens?: number - } - ) => { - try { - const result = llm.addConfig(config) - if (result.success && result.config) { - return { - success: true, - config: { - ...result.config, - apiKey: result.config.apiKey - ? `${result.config.apiKey.slice(0, 4)}****${result.config.apiKey.slice(-4)}` - : '', - apiKeySet: !!result.config.apiKey, - }, - } - } - return result - } catch (error) { - console.error('添加 LLM 配置失败:', error) - return { success: false, error: String(error) } - } - } - ) - - /** - * 更新配置 - */ - ipcMain.handle( - 'llm:updateConfig', - async ( - _, - id: string, - updates: { - name?: string - provider?: llm.LLMProvider - apiKey?: string - model?: string - baseUrl?: string - maxTokens?: number - } - ) => { - try { - // 如果 apiKey 为空字符串,表示不更新 API Key - const cleanUpdates = { ...updates } - if (cleanUpdates.apiKey === '') { - delete cleanUpdates.apiKey - } - - return llm.updateConfig(id, cleanUpdates) - } catch (error) { - console.error('更新 LLM 配置失败:', error) - return { success: false, error: String(error) } - } - } - ) - - /** - * 删除配置 - */ - ipcMain.handle('llm:deleteConfig', async (_, id?: string) => { - try { - // 兼容旧 API:如果没有传 id,删除当前激活的配置 - if (!id) { - const activeConfig = llm.getActiveConfig() - if (activeConfig) { - return llm.deleteConfig(activeConfig.id) - } - return { success: false, error: '没有激活的配置' } - } - return llm.deleteConfig(id) - } catch (error) { - console.error('删除 LLM 配置失败:', error) - return { success: false, error: String(error) } - } - }) - - /** - * 设置激活的配置 - */ - ipcMain.handle('llm:setActiveConfig', async (_, id: string) => { - try { - return llm.setActiveConfig(id) - } catch (error) { - console.error('设置激活配置失败:', error) - return { success: false, error: String(error) } - } - }) - - /** - * 验证 API Key(支持自定义 baseUrl 和 model) - */ - ipcMain.handle( - 'llm:validateApiKey', - async (_, provider: llm.LLMProvider, apiKey: string, baseUrl?: string, model?: string) => { - console.log('[LLM:validateApiKey] 开始验证:', { provider, baseUrl, model, apiKeyLength: apiKey?.length }) - try { - const service = llm.createLLMService({ provider, apiKey, baseUrl, model }) - const result = await service.validateApiKey() - console.log('[LLM:validateApiKey] 验证结果:', result) - return result - } catch (error) { - console.error('[LLM:validateApiKey] 验证失败:', error) - return false - } - } - ) - - /** - * 检查是否已配置 LLM(是否有激活的配置) - */ - ipcMain.handle('llm:hasConfig', async () => { - return llm.hasActiveConfig() - }) - - // ==================== 兼容旧 API(deprecated)==================== - - /** - * @deprecated 使用 llm:getAllConfigs 代替 - * 获取当前 LLM 配置 - */ - ipcMain.handle('llm:getConfig', async () => { - const config = llm.getActiveConfig() - if (!config) return null - return { - provider: config.provider, - apiKey: config.apiKey ? `${config.apiKey.slice(0, 8)}...${config.apiKey.slice(-4)}` : '', - apiKeySet: !!config.apiKey, - model: config.model, - baseUrl: config.baseUrl, - maxTokens: config.maxTokens, - } - }) - - /** - * @deprecated 使用 llm:addConfig 或 llm:updateConfig 代替 - * 保存 LLM 配置 - */ - ipcMain.handle( - 'llm:saveConfig', - async ( - _, - config: { provider: llm.LLMProvider; apiKey: string; model?: string; baseUrl?: string; maxTokens?: number } - ) => { - try { - const activeConfig = llm.getActiveConfig() - - if (activeConfig) { - // 更新现有配置 - const updates: Record = { ...config } - if (!config.apiKey || config.apiKey.trim() === '') { - delete updates.apiKey - } - return llm.updateConfig(activeConfig.id, updates) - } else { - // 创建新配置 - if (!config.apiKey || config.apiKey.trim() === '') { - return { success: false, error: '请输入 API Key' } - } - const providerInfo = llm.getProviderInfo(config.provider) - return llm.addConfig({ - name: providerInfo?.name || config.provider, - ...config, - }) - } - } catch (error) { - console.error('保存 LLM 配置失败:', error) - return { success: false, error: String(error) } - } - } - ) - - /** - * 发送 LLM 聊天请求(非流式) - */ - ipcMain.handle('llm:chat', async (_, messages: llm.ChatMessage[], options?: llm.ChatOptions) => { - aiLogger.info('IPC', '收到非流式 LLM 请求', { - messagesCount: messages.length, - firstMsgRole: messages[0]?.role, - firstMsgContentLen: messages[0]?.content?.length, - options, - }) - try { - const response = await llm.chat(messages, options) - aiLogger.info('IPC', '非流式 LLM 请求成功', { responseLength: response.length }) - return { success: true, content: response } - } catch (error) { - aiLogger.error('IPC', '非流式 LLM 请求失败', { error: String(error) }) - console.error('LLM 聊天失败:', error) - return { success: false, error: String(error) } - } - }) - - /** - * 发送 LLM 聊天请求(流式) - * 使用 IPC 事件发送流式数据 - */ - ipcMain.handle( - 'llm:chatStream', - async (_, requestId: string, messages: llm.ChatMessage[], options?: llm.ChatOptions) => { - aiLogger.info('IPC', `收到流式聊天请求: ${requestId}`, { - messagesCount: messages.length, - options, - }) - - try { - const generator = llm.chatStream(messages, options) - aiLogger.info('IPC', `创建流式生成器: ${requestId}`) - - // 异步处理流式响应 - ;(async () => { - let chunkIndex = 0 - try { - aiLogger.info('IPC', `开始迭代流式响应: ${requestId}`) - for await (const chunk of generator) { - chunkIndex++ - aiLogger.debug('IPC', `发送 chunk #${chunkIndex}: ${requestId}`, { - contentLength: chunk.content?.length, - isFinished: chunk.isFinished, - finishReason: chunk.finishReason, - }) - win.webContents.send('llm:streamChunk', { requestId, chunk }) - } - aiLogger.info('IPC', `流式响应完成: ${requestId}`, { totalChunks: chunkIndex }) - } catch (error) { - aiLogger.error('IPC', `流式响应出错: ${requestId}`, { - error: String(error), - chunkIndex, - }) - win.webContents.send('llm:streamChunk', { - requestId, - chunk: { content: '', isFinished: true, finishReason: 'error' }, - error: String(error), - }) - } - })() - - return { success: true } - } catch (error) { - aiLogger.error('IPC', `创建流式请求失败: ${requestId}`, { error: String(error) }) - console.error('LLM 流式聊天失败:', error) - return { success: false, error: String(error) } - } - } - ) - - // ==================== AI Agent API ==================== - - /** - * 执行 Agent 对话(流式) - * Agent 会自动调用工具并返回最终结果 - */ - ipcMain.handle('agent:runStream', async (_, requestId: string, userMessage: string, context: ToolContext) => { - aiLogger.info('IPC', `收到 Agent 流式请求: ${requestId}`, { - userMessage: userMessage.slice(0, 100), - sessionId: context.sessionId, - }) - - try { - // 创建 AbortController 并存储 - const abortController = new AbortController() - activeAgentRequests.set(requestId, abortController) - - const agent = new Agent(context, { abortSignal: abortController.signal }) - - // 异步执行,通过事件发送流式数据 - ;(async () => { - try { - const result = await agent.executeStream(userMessage, (chunk: AgentStreamChunk) => { - // 如果已中止,不再发送 - if (abortController.signal.aborted) { - return - } - aiLogger.debug('IPC', `Agent chunk: ${requestId}`, { - type: chunk.type, - contentLength: chunk.content?.length, - toolName: chunk.toolName, - }) - win.webContents.send('agent:streamChunk', { requestId, chunk }) - }) - - // 如果已中止,不发送完成信息 - if (abortController.signal.aborted) { - aiLogger.info('IPC', `Agent 已中止,跳过完成信息: ${requestId}`) - return - } - - // 发送完成信息 - win.webContents.send('agent:complete', { - requestId, - result: { - content: result.content, - toolsUsed: result.toolsUsed, - toolRounds: result.toolRounds, - }, - }) - - aiLogger.info('IPC', `Agent 执行完成: ${requestId}`, { - toolsUsed: result.toolsUsed, - toolRounds: result.toolRounds, - contentLength: result.content.length, - }) - } catch (error) { - // 如果是中止错误,不报告为错误 - if (error instanceof Error && error.name === 'AbortError') { - aiLogger.info('IPC', `Agent 请求已中止: ${requestId}`) - return - } - aiLogger.error('IPC', `Agent 执行出错: ${requestId}`, { error: String(error) }) - win.webContents.send('agent:streamChunk', { - requestId, - chunk: { type: 'error', error: String(error), isFinished: true }, - }) - } finally { - // 清理请求追踪 - activeAgentRequests.delete(requestId) - } - })() - - return { success: true } - } catch (error) { - aiLogger.error('IPC', `创建 Agent 请求失败: ${requestId}`, { error: String(error) }) - return { success: false, error: String(error) } - } - }) - - /** - * 中止 Agent 请求 - */ - ipcMain.handle('agent:abort', async (_, requestId: string) => { - aiLogger.info('IPC', `收到中止请求: ${requestId}`) - - const abortController = activeAgentRequests.get(requestId) - if (abortController) { - abortController.abort() - activeAgentRequests.delete(requestId) - aiLogger.info('IPC', `已中止 Agent 请求: ${requestId}`) - return { success: true } - } else { - aiLogger.warn('IPC', `未找到 Agent 请求: ${requestId}`) - return { success: false, error: '未找到该请求' } - } - }) + console.log('[IpcMain] All IPC handlers registered successfully') } export default mainIpcMain diff --git a/electron/main/worker/index.ts b/electron/main/worker/index.ts deleted file mode 100644 index 843f184..0000000 --- a/electron/main/worker/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Worker 模块入口 - * 导出 Worker 管理器的所有 API - */ - -export { - initWorker, - closeWorker, - getDbDirectory, - // 分析查询 API(异步) - getAvailableYears, - getMemberActivity, - getHourlyActivity, - getDailyActivity, - getWeekdayActivity, - getMonthlyActivity, - getMessageTypeDistribution, - getTimeRange, - getMemberNameHistory, - getRepeatAnalysis, - getCatchphraseAnalysis, - getNightOwlAnalysis, - getDragonKingAnalysis, - getDivingAnalysis, - getMonologueAnalysis, - getMentionAnalysis, - getLaughAnalysis, - getMemeBattleAnalysis, - getCheckInAnalysis, - // 会话管理 API(异步) - getAllSessions, - getSession, - closeDatabase, - // 成员管理 API - getMembers, - updateMemberAliases, - deleteMember, - // 文件解析 API(已废弃,使用流式版本) - parseFileInfo, - // 流式导入 API - streamImport, - streamParseFileInfo, - // AI 查询 API - searchMessages, - getMessageContext, -} from './workerManager' - -export type { SearchMessageResult, MemberWithStats } from './workerManager' diff --git a/electron/preload/index.d.ts b/electron/preload/index.d.ts index 5fe3437..41f4dbb 100644 --- a/electron/preload/index.d.ts +++ b/electron/preload/index.d.ts @@ -278,6 +278,31 @@ interface AgentApi { abort: (requestId: string) => Promise<{ success: boolean; error?: string }> } +// Cache API 类型 +interface CacheDirectoryInfo { + id: string + name: string + description: string + path: string + icon: string + canClear: boolean + size: number + fileCount: number + exists: boolean +} + +interface CacheInfo { + baseDir: string + directories: CacheDirectoryInfo[] + totalSize: number +} + +interface CacheApi { + getInfo: () => Promise + clear: (cacheId: string) => Promise<{ success: boolean; error?: string; message?: string }> + openDir: (cacheId: string) => Promise<{ success: boolean; error?: string }> +} + declare global { interface Window { electron: ElectronAPI @@ -287,6 +312,7 @@ declare global { aiApi: AiApi llmApi: LlmApi agentApi: AgentApi + cacheApi: CacheApi } } @@ -297,6 +323,7 @@ export { AiApi, LlmApi, AgentApi, + CacheApi, SearchMessageResult, AIConversation, AIMessage, @@ -309,4 +336,6 @@ export { AgentStreamChunk, AgentResult, ToolContext, + CacheDirectoryInfo, + CacheInfo, } diff --git a/electron/preload/index.ts b/electron/preload/index.ts index a927860..ac75217 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -797,6 +797,48 @@ const agentApi = { }, } +// Cache API - 缓存管理 +interface CacheDirectoryInfo { + id: string + name: string + description: string + path: string + icon: string + canClear: boolean + size: number + fileCount: number + exists: boolean +} + +interface CacheInfo { + baseDir: string + directories: CacheDirectoryInfo[] + totalSize: number +} + +const cacheApi = { + /** + * 获取所有缓存目录信息 + */ + getInfo: (): Promise => { + return ipcRenderer.invoke('cache:getInfo') + }, + + /** + * 清理指定缓存目录 + */ + clear: (cacheId: string): Promise<{ success: boolean; error?: string; message?: string }> => { + return ipcRenderer.invoke('cache:clear', cacheId) + }, + + /** + * 在文件管理器中打开缓存目录 + */ + openDir: (cacheId: string): Promise<{ success: boolean; error?: string }> => { + return ipcRenderer.invoke('cache:openDir', cacheId) + }, +} + // 扩展 api,添加 dialog 功能 const extendedApi = { ...api, @@ -819,6 +861,7 @@ if (process.contextIsolated) { contextBridge.exposeInMainWorld('aiApi', aiApi) contextBridge.exposeInMainWorld('llmApi', llmApi) contextBridge.exposeInMainWorld('agentApi', agentApi) + contextBridge.exposeInMainWorld('cacheApi', cacheApi) } catch (error) { console.error(error) } @@ -837,4 +880,6 @@ if (process.contextIsolated) { window.llmApi = llmApi // @ts-ignore (define in dts) window.agentApi = agentApi + // @ts-ignore (define in dts) + window.cacheApi = cacheApi } diff --git a/src/components.d.ts b/src/components.d.ts index a1f695e..cfcc8f6 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -25,7 +25,6 @@ declare module 'vue' { UInputTags: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/InputTags.vue')['default'] UModal: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Modal.vue')['default'] UProgress: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Progress.vue')['default'] - USelectMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/SelectMenu.vue')['default'] USwitch: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Switch.vue')['default'] UTabs: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Tabs.vue')['default'] UTooltip: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Tooltip.vue')['default'] diff --git a/src/components/analysis/OverviewTab.vue b/src/components/analysis/OverviewTab.vue index f045066..121563c 100644 --- a/src/components/analysis/OverviewTab.vue +++ b/src/components/analysis/OverviewTab.vue @@ -229,42 +229,29 @@ const weekdayVsWeekend = computed(() => {
-
-
-
-

{{ session.name }}

- - {{ session.platform.toUpperCase() }} - -
-

- {{ session.memberCount > 2 ? '群聊' : '私聊' }} · - 数据分析报告 -

-
-
- +
+
+

{{ session.name }}

+ + {{ session.platform.toUpperCase() }} +
+

+ {{ session.memberCount > 2 ? '群聊' : '私聊' }} · + 数据分析报告 +

-
+

{{ displayMessageCount.toLocaleString() }}

{{ selectedYear ? '筛选消息' : '消息总数' }}

-
+

{{ displayMemberCount.toLocaleString() }}

{{ selectedYear ? '活跃成员' : '成员总数' }}

-
+

{{ durationDays }}

跨度天数

diff --git a/src/components/analysis/ai/ChatExplorer.vue b/src/components/analysis/ai/ChatExplorer.vue index 890c135..579bccd 100644 --- a/src/components/analysis/ai/ChatExplorer.vue +++ b/src/components/analysis/ai/ChatExplorer.vue @@ -430,7 +430,7 @@ watch(
- AI 对话探索 + 探索 {{ sessionName }} 的聊天记录
diff --git a/src/components/analysis/ai/ChatInput.vue b/src/components/analysis/ai/ChatInput.vue index 2c8b4a9..0f2139e 100644 --- a/src/components/analysis/ai/ChatInput.vue +++ b/src/components/analysis/ai/ChatInput.vue @@ -40,8 +40,8 @@ function handleStop() {