diff --git a/apps/desktop/main/index.ts b/apps/desktop/main/index.ts index e6977b0c..87ec271d 100644 --- a/apps/desktop/main/index.ts +++ b/apps/desktop/main/index.ts @@ -1,4 +1,4 @@ -import { app, shell, BrowserWindow, protocol, nativeTheme } from 'electron' +import { app, shell, BrowserWindow, protocol, nativeTheme, dialog } from 'electron' import { join } from 'path' import { optimizer, is, platform } from '@electron-toolkit/utils' import { checkUpdate } from './update' @@ -272,14 +272,19 @@ class MainProcess { // 记录日活(用于统计操作系统版本、客户端版本,便于更好的适配客户端) trackDailyActive() - // 启动 Internal API Server(在 createWindow 之前,确保 Renderer 加载时 endpoint 可用) + // 启动 Internal API Server(硬依赖:失败则退出应用) try { await startInternalServer(getPathProvider()) registerInternalApiIpc() console.log('[Main] Internal API Server ready') } catch (err) { - console.error('[Main] Internal API Server failed to start (falling back to IPC):', err) - registerInternalApiIpc() + console.error('[Main] Internal API Server failed to start:', err) + dialog.showErrorBox( + 'ChatLab Internal Server Error', + `Internal API Server failed to start. The application cannot continue.\n\n${err instanceof Error ? err.message : String(err)}` + ) + app.quit() + return } // 创建主窗口 diff --git a/apps/desktop/main/ipc/chat.ts b/apps/desktop/main/ipc/chat.ts index 62bafe4c..09501ea1 100644 --- a/apps/desktop/main/ipc/chat.ts +++ b/apps/desktop/main/ipc/chat.ts @@ -1,33 +1,29 @@ /** - * 聊天记录导入与分析 IPC 处理器 + * 聊天记录导入、迁移与摘要 IPC 处理器 + * + * 数据查询/分析/成员管理/SQL/会话索引等业务已迁移到 + * Internal HTTP Server (@openchatlab/http-routes)。 + * 本文件仅保留:数据库迁移、文件导入、摘要生成、临时导出。 */ import { ipcMain, app, dialog } from 'electron' -import { getConversationCountsBySession } from '../ai/conversations' import * as databaseCore from '../database/core' import * as worker from '../worker/workerManager' -import * as parser from '../parser' import { detectFormat, findEntryFileInDirectory, scanMultiChatFile, type ParseProgress } from '../parser' +import * as parser from '../parser' import type { IpcContext } from './types' import { CURRENT_SCHEMA_VERSION, getPendingMigrationInfos } from '../database/migrations' import { exportSessionToTempFile, cleanupTempExportFiles } from '../merger' import { t } from '../i18n' -/** - * 注册聊天记录相关 IPC 处理器 - */ export function registerChatHandlers(ctx: IpcContext): void { const { win } = ctx // ==================== 数据库迁移 ==================== - /** - * 检查是否需要数据库迁移 - */ ipcMain.handle('chat:checkMigration', async () => { try { const result = databaseCore.checkMigrationNeeded() - // 获取待执行的迁移信息(从最低版本开始) const pendingMigrations = getPendingMigrationInfos(result.lowestVersion) return { needsMigration: result.count > 0, @@ -41,24 +37,17 @@ export function registerChatHandlers(ctx: IpcContext): void { } }) - /** - * 执行数据库迁移 - */ ipcMain.handle('chat:runMigration', async () => { try { - const result = databaseCore.migrateAllDatabases() - return result + return databaseCore.migrateAllDatabases() } catch (error) { console.error('[IpcMain] Migration execution failed:', error) return { success: false, migratedCount: 0, error: String(error) } } }) - // ==================== 聊天记录导入与分析 ==================== + // ==================== 文件选择与格式检测 ==================== - /** - * 选择聊天记录文件 - */ ipcMain.handle('chat:selectFile', async () => { try { const { canceled, filePaths } = await dialog.showOpenDialog({ @@ -72,18 +61,12 @@ export function registerChatHandlers(ctx: IpcContext): void { buttonLabel: t('dialog.import'), }) - if (canceled || filePaths.length === 0) { - return null - } + if (canceled || filePaths.length === 0) return null const filePath = filePaths[0] - - // 检测文件格式(使用流式检测,只读取文件开头) const formatFeature = detectFormat(filePath) const format = formatFeature?.name || null - if (!format) { - return { error: 'error.unrecognized_format' } - } + if (!format) return { error: 'error.unrecognized_format' } return { filePath, format } } catch (error) { @@ -92,21 +75,42 @@ export function registerChatHandlers(ctx: IpcContext): void { } }) - /** - * 导入聊天记录(流式版本) - */ + ipcMain.handle('chat:detectFormat', async (_, filePath: string) => { + try { + const formatFeature = detectFormat(filePath) + if (!formatFeature) return null + return { + id: formatFeature.id, + name: formatFeature.name, + platform: formatFeature.platform, + multiChat: formatFeature.multiChat || false, + } + } catch { + return null + } + }) + + ipcMain.handle('chat:scanMultiChatFile', async (_, filePath: string) => { + try { + const chats = await scanMultiChatFile(filePath) + return { success: true, chats } + } catch (error) { + console.error('[IpcMain] Failed to scan multi-chat files:', error) + return { success: false, error: String(error), chats: [] } + } + }) + + ipcMain.handle('chat:getSupportedFormats', async () => { + return parser.getSupportedFormats() + }) + + // ==================== 导入 ==================== + ipcMain.handle('chat:import', async (_, filePath: string) => { try { - // Send progress: detecting format (message not used by frontend, stage-based translation) - win.webContents.send('chat:importProgress', { - stage: 'detecting', - progress: 5, - message: '', // Frontend translates based on stage - }) + 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, @@ -118,46 +122,23 @@ export function registerChatHandlers(ctx: IpcContext): void { }) if (result.success) { - console.log('[IpcMain] Stream import successful, sessionId:', result.sessionId) return { success: true, sessionId: result.sessionId, diagnostics: result.diagnostics } } else { - console.error('[IpcMain] Stream import failed:', result.error) - win.webContents.send('chat:importProgress', { - stage: 'error', - progress: 0, - message: result.error, - }) - + win.webContents.send('chat:importProgress', { stage: 'error', progress: 0, message: result.error }) return { success: false, error: result.error, diagnostics: result.diagnostics } } } catch (error) { - console.error('[IpcMain] Import failed:', error) - - win.webContents.send('chat:importProgress', { - stage: 'error', - progress: 0, - message: String(error), - }) - + win.webContents.send('chat:importProgress', { stage: 'error', progress: 0, message: String(error) }) return { success: false, error: String(error) } } }) - /** - * Import from a directory path: scan for entry file and import - */ ipcMain.handle('chat:importDirectory', async (_, dirPath: string) => { try { const entryPath = findEntryFileInDirectory(dirPath) - if (!entryPath) { - return { success: false, error: 'No recognizable import format found in directory' } - } + if (!entryPath) return { success: false, error: 'No recognizable import format found in directory' } - win.webContents.send('chat:importProgress', { - stage: 'detecting', - progress: 5, - message: '', - }) + win.webContents.send('chat:importProgress', { stage: 'detecting', progress: 5, message: '' }) const result = await worker.streamImport(entryPath, (progress: ParseProgress) => { win.webContents.send('chat:importProgress', { @@ -182,49 +163,9 @@ export function registerChatHandlers(ctx: IpcContext): void { } }) - /** - * 检测文件格式(轻量级,仅返回格式 ID、名称和是否多聊天) - */ - ipcMain.handle('chat:detectFormat', async (_, filePath: string) => { - try { - const formatFeature = detectFormat(filePath) - if (!formatFeature) return null - return { - id: formatFeature.id, - name: formatFeature.name, - platform: formatFeature.platform, - multiChat: formatFeature.multiChat || false, - } - } catch { - return null - } - }) - - /** - * 扫描多聊天文件中的聊天列表(通用) - * 自动检测格式并调用对应格式的 scanChats - */ - ipcMain.handle('chat:scanMultiChatFile', async (_, filePath: string) => { - try { - const chats = await scanMultiChatFile(filePath) - return { success: true, chats } - } catch (error) { - console.error('[IpcMain] Failed to scan multi-chat files:', error) - return { success: false, error: String(error), chats: [] } - } - }) - - /** - * 导入聊天记录(带格式选项) - * 用于多聊天格式等需要额外参数的场景(如指定 chatIndex) - */ ipcMain.handle('chat:importWithOptions', async (_, filePath: string, formatOptions: Record) => { try { - win.webContents.send('chat:importProgress', { - stage: 'detecting', - progress: 5, - message: '', - }) + win.webContents.send('chat:importProgress', { stage: 'detecting', progress: 5, message: '' }) const result = await worker.streamImport( filePath, @@ -242,558 +183,60 @@ export function registerChatHandlers(ctx: IpcContext): void { ) if (result.success) { - console.log('[IpcMain] Stream import (with options) successful, sessionId:', result.sessionId) return { success: true, sessionId: result.sessionId, diagnostics: result.diagnostics } } else { - console.error('[IpcMain] Stream import (with options) failed:', result.error) - win.webContents.send('chat:importProgress', { - stage: 'error', - progress: 0, - message: result.error, - }) + win.webContents.send('chat:importProgress', { stage: 'error', progress: 0, message: result.error }) return { success: false, error: result.error, diagnostics: result.diagnostics } } } catch (error) { - console.error('[IpcMain] Import with options failed:', error) - win.webContents.send('chat:importProgress', { - stage: 'error', - progress: 0, - message: String(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() + // ==================== 增量导入 ==================== - // 填充 AI 对话计数(AI 数据库在主进程管理) - try { - const aiCounts = getConversationCountsBySession() - for (const session of sessions) { - session.aiConversationCount = aiCounts.get(session.id) || 0 + ipcMain.handle('chat:analyzeIncrementalImport', async (_, sessionId: string, filePath: string) => { + try { + const formatFeature = detectFormat(filePath) + if (!formatFeature) return { error: 'error.unrecognized_format' } + return await worker.analyzeIncrementalImport(sessionId, filePath) + } catch (error) { + console.error('[IpcMain] Failed to analyze incremental import:', error) + return { error: String(error) } + } + }) + + ipcMain.handle('chat:incrementalImport', async (_, sessionId: string, filePath: string) => { + try { + win.webContents.send('chat:importProgress', { stage: 'saving', progress: 0, message: '' }) + + const result = await worker.incrementalImport(sessionId, filePath, (progress) => { + win.webContents.send('chat:importProgress', { + stage: progress.stage, + progress: progress.percentage, + message: progress.message, + }) + }) + + if (result.success) { + try { + await worker.generateIncrementalSessions(sessionId) + } catch (e) { + console.error('[IpcMain] Failed to incrementally generate session index:', e) } - } catch { - // AI 数据库未初始化时忽略 + worker.invalidateAnalysisCache(sessionId).catch(() => {}) } - 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('Failed to get session info:', error) - return null - } - }) - - /** - * 删除会话 - */ - ipcMain.handle('chat:deleteSession', async (_, sessionId: string) => { - try { - await worker.closeDatabase(sessionId) - const result = databaseCore.deleteSession(sessionId) return result } catch (error) { - console.error('Failed to delete session:', error) - return false + console.error('[IpcMain] Failed to execute incremental import:', error) + return { success: false, error: String(error) } } }) - /** - * 重命名会话 - */ - ipcMain.handle('chat:renameSession', async (_, sessionId: string, newName: string) => { - try { - // 先关闭 Worker 中的数据库连接(确保没有其他进程占用) - await worker.closeDatabase(sessionId) - // 执行重命名 - return databaseCore.renameSession(sessionId, newName) - } catch (error) { - console.error('Failed to rename session:', error) - return false - } - }) + // ==================== 会话索引(ElectronSessionIndexAdapter 依赖) ==================== - /** - * 获取可用年份列表 - */ - ipcMain.handle('chat:getAvailableYears', async (_, sessionId: string) => { - try { - return await worker.getAvailableYears(sessionId) - } catch (error) { - console.error('Failed to get available years:', 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('Failed to get member activity:', error) - return [] - } - } - ) - - /** - * 获取成员历史昵称 - */ - ipcMain.handle('chat:getMemberNameHistory', async (_, sessionId: string, memberId: number) => { - try { - return await worker.getMemberNameHistory(sessionId, memberId) - } catch (error) { - console.error('Failed to get member name history:', 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('Failed to get hourly activity:', 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('Failed to get daily activity:', 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('Failed to get weekday activity:', 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('Failed to get monthly activity:', error) - return [] - } - } - ) - - /** - * 获取年份活跃度分布 - */ - ipcMain.handle( - 'chat:getYearlyActivity', - async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { - try { - return await worker.getYearlyActivity(sessionId, filter) - } catch (error) { - console.error('Failed to get yearly activity:', error) - return [] - } - } - ) - - /** - * 获取消息长度分布 - */ - ipcMain.handle( - 'chat:getMessageLengthDistribution', - async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { - try { - return await worker.getMessageLengthDistribution(sessionId, filter) - } catch (error) { - console.error('Failed to get message length distribution:', 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('Failed to get message type distribution:', error) - return [] - } - } - ) - - /** - * 获取时间范围 - */ - ipcMain.handle('chat:getTimeRange', async (_, sessionId: string) => { - try { - return await worker.getTimeRange(sessionId) - } catch (error) { - console.error('Failed to get time range:', error) - return null - } - }) - - /** - * 获取数据库存储目录 - */ - ipcMain.handle('chat:getDbDirectory', async () => { - try { - return worker.getDbDirectory() - } catch (error) { - console.error('Failed to get database directory:', error) - return null - } - }) - - /** - * 获取支持的格式列表 - */ - ipcMain.handle('chat:getSupportedFormats', async () => { - return parser.getSupportedFormats() - }) - - /** - * 获取口头禅分析数据 - */ - ipcMain.handle( - 'chat:getCatchphraseAnalysis', - async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { - try { - return await worker.getCatchphraseAnalysis(sessionId, filter) - } catch (error) { - console.error('Failed to get catchphrase analysis:', error) - return { members: [] } - } - } - ) - - /** - * 获取语言偏好分析数据(私聊专用) - */ - ipcMain.handle( - 'chat:getLanguagePreferenceAnalysis', - async (_, sessionId: string, locale: string, filter?: { startTs?: number; endTs?: number }, dictType?: string) => { - try { - return await worker.getLanguagePreferenceAnalysis({ sessionId, locale, timeFilter: filter, dictType }) - } catch (error) { - console.error('Failed to get language preference analysis:', error) - return { members: [], sharedWords: [], similarityScore: 0 } - } - } - ) - - /** - * 获取 @ 互动分析数据 - */ - ipcMain.handle( - 'chat:getMentionAnalysis', - async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { - try { - return await worker.getMentionAnalysis(sessionId, filter) - } catch (error) { - console.error('Failed to get @mention analysis:', error) - return { topMentioners: [], topMentioned: [], oneWay: [], twoWay: [], totalMentions: 0, memberDetails: [] } - } - } - ) - - /** - * 获取 @ 互动关系图数据 - */ - ipcMain.handle( - 'chat:getMentionGraph', - async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { - try { - return await worker.getMentionGraph(sessionId, filter) - } catch (error) { - console.error('Failed to get @mention graph:', error) - return { nodes: [], links: [], maxLinkValue: 0 } - } - } - ) - - /** - * 获取小团体关系图数据(基于时间相邻共现) - */ - ipcMain.handle( - 'chat:getClusterGraph', - async ( - _, - sessionId: string, - filter?: { startTs?: number; endTs?: number }, - options?: { - lookAhead?: number - decaySeconds?: number - minScore?: number - topEdges?: number - } - ) => { - try { - return await worker.getClusterGraph(sessionId, filter, options) - } catch (error) { - console.error('Failed to get clique graph:', error) - return { - nodes: [], - links: [], - maxLinkValue: 0, - communities: [], - stats: { - totalMembers: 0, - totalMessages: 0, - involvedMembers: 0, - edgeCount: 0, - communityCount: 0, - }, - } - } - } - ) - - /** - * 获取含笑量分析数据 - */ - 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('Failed to get humor analysis:', error) - return { - rankByRate: [], - rankByCount: [], - typeDistribution: [], - totalLaughs: 0, - totalMessages: 0, - groupLaughRate: 0, - } - } - } - ) - - /** - * 获取关系主动性分析数据(私聊专属) - */ - ipcMain.handle( - 'chat:getRelationshipStats', - async ( - _, - sessionId: string, - filter?: { startTs?: number; endTs?: number }, - options?: { perseveranceThreshold?: number } - ) => { - try { - return await worker.getRelationshipStats(sessionId, filter, options) - } catch (error) { - console.error('Failed to get relationship stats:', error) - return { months: [], members: [], totalSessions: 0, hasSessionIndex: false } - } - } - ) - - // ==================== 成员管理 ==================== - - /** - * 获取所有成员列表(含消息数和别名) - */ - ipcMain.handle('chat:getMembers', async (_, sessionId: string) => { - try { - return await worker.getMembers(sessionId) - } catch (error) { - console.error('Failed to get member list:', error) - return [] - } - }) - - /** - * 获取成员列表(分页版本) - */ - ipcMain.handle( - 'chat:getMembersPaginated', - async ( - _, - sessionId: string, - params: { page: number; pageSize: number; search?: string; sortOrder?: 'asc' | 'desc' } - ) => { - try { - return await worker.getMembersPaginated(sessionId, params) - } catch (error) { - console.error('Failed to get member list (paginated):', error) - return { members: [], total: 0, page: 1, pageSize: 20, totalPages: 0 } - } - } - ) - - /** - * 更新成员别名 - */ - ipcMain.handle('chat:updateMemberAliases', async (_, sessionId: string, memberId: number, aliases: string[]) => { - try { - const result = await worker.updateMemberAliases(sessionId, memberId, aliases) - if (result) worker.invalidateAnalysisCache(sessionId).catch(() => {}) - return result - } catch (error) { - console.error('Failed to update member alias:', error) - return false - } - }) - - /** - * 合并成员(保留消息数更多的一方) - */ - ipcMain.handle('chat:mergeMembers', async (_, sessionId: string, memberId1: number, memberId2: number) => { - try { - await worker.closeDatabase(sessionId) - const result = await worker.mergeMembers(sessionId, memberId1, memberId2) - if (result) worker.invalidateAnalysisCache(sessionId).catch(() => {}) - return result - } catch (error) { - console.error('Failed to merge members:', error) - return false - } - }) - - /** - * 删除成员及其所有消息 - */ - ipcMain.handle('chat:deleteMember', async (_, sessionId: string, memberId: number) => { - try { - // 先关闭数据库连接 - await worker.closeDatabase(sessionId) - // 执行删除 - const result = await worker.deleteMember(sessionId, memberId) - if (result) worker.invalidateAnalysisCache(sessionId).catch(() => {}) - return result - } catch (error) { - console.error('Failed to delete member:', error) - return false - } - }) - - /** - * 更新会话的所有者(ownerId) - */ - ipcMain.handle('chat:updateSessionOwnerId', async (_, sessionId: string, ownerId: string | null) => { - try { - // 先关闭数据库连接 - await worker.closeDatabase(sessionId) - // 执行更新 - return databaseCore.updateSessionOwnerId(sessionId, ownerId) - } catch (error) { - console.error('Failed to update session owner:', error) - return false - } - }) - - // ==================== 插件系统 ==================== - - /** - * 插件参数化只读 SQL 查询 - */ - ipcMain.handle('chat:pluginQuery', async (_, sessionId: string, sql: string, params: any[]) => { - try { - return await worker.pluginQuery(sessionId, sql, params) - } catch (error) { - console.error('[IpcMain] Plugin query failed:', error) - throw error - } - }) - - /** - * 插件计算卸载(纯函数在 Worker 中执行) - */ - ipcMain.handle('chat:pluginCompute', async (_, fnString: string, input: any) => { - try { - return await worker.pluginCompute(fnString, input) - } catch (error) { - console.error('[IpcMain] Plugin compute failed:', error) - throw error - } - }) - - // ==================== SQL 实验室 ==================== - - /** - * 执行用户 SQL 查询 - */ - ipcMain.handle('chat:executeSQL', async (_, sessionId: string, sql: string) => { - try { - return await worker.executeRawSQL(sessionId, sql) - } catch (error) { - console.error('Failed to execute SQL:', error) - throw error - } - }) - - /** - * 获取数据库 Schema - */ - ipcMain.handle('chat:getSchema', async (_, sessionId: string) => { - try { - return await worker.getSchema(sessionId) - } catch (error) { - console.error('Failed to get schema:', error) - return [] - } - }) - - // ==================== 会话索引 ==================== - - /** - * 生成会话索引 - */ ipcMain.handle('session:generate', async (_, sessionId: string, gapThreshold?: number) => { try { return await worker.generateSessions(sessionId, gapThreshold) @@ -812,9 +255,6 @@ export function registerChatHandlers(ctx: IpcContext): void { } }) - /** - * 检查是否已生成会话索引 - */ ipcMain.handle('session:hasIndex', async (_, sessionId: string) => { try { return await worker.hasSessionIndex(sessionId) @@ -824,9 +264,6 @@ export function registerChatHandlers(ctx: IpcContext): void { } }) - /** - * 获取会话索引统计信息 - */ ipcMain.handle('session:getStats', async (_, sessionId: string) => { try { return await worker.getSessionStats(sessionId) @@ -845,9 +282,6 @@ export function registerChatHandlers(ctx: IpcContext): void { } }) - /** - * 清空会话索引 - */ ipcMain.handle('session:clear', async (_, sessionId: string) => { try { await worker.clearSessions(sessionId) @@ -858,9 +292,6 @@ export function registerChatHandlers(ctx: IpcContext): void { } }) - /** - * 更新会话切分阈值 - */ ipcMain.handle('session:updateGapThreshold', async (_, sessionId: string, gapThreshold: number | null) => { try { await worker.updateSessionGapThreshold(sessionId, gapThreshold) @@ -871,9 +302,6 @@ export function registerChatHandlers(ctx: IpcContext): void { } }) - /** - * 获取会话列表(用于时间线导航) - */ ipcMain.handle('session:getSessions', async (_, sessionId: string) => { try { return await worker.getSessions(sessionId) @@ -883,11 +311,26 @@ export function registerChatHandlers(ctx: IpcContext): void { } }) - // ==================== 会话摘要 ==================== + ipcMain.handle('session:getByTimeRange', async (_, dbSessionId: string, startTs: number, endTs: number) => { + try { + return await worker.getSessionsByTimeRange(dbSessionId, startTs, endTs) + } catch (error) { + console.error('Failed to query sessions by time range:', error) + return [] + } + }) + + ipcMain.handle('session:getRecent', async (_, dbSessionId: string, limit: number) => { + try { + return await worker.getRecentChatSessions(dbSessionId, limit) + } catch (error) { + console.error('Failed to query recent sessions:', error) + return [] + } + }) + + // ==================== 会话摘要(依赖 LLM,无共享路由) ==================== - /** - * 生成单个会话摘要 - */ ipcMain.handle( 'session:generateSummary', async ( @@ -898,24 +341,15 @@ export function registerChatHandlers(ctx: IpcContext): void { forceRegenerate?: boolean, strategy?: 'brief' | 'standard' ) => { - console.log('[IPC] session:generateSummary request received:', { - dbSessionId, - chatSessionId, - locale, - forceRegenerate, - strategy, - }) try { const { generateSessionSummary } = await import('../ai/summary') - const result = await generateSessionSummary( + return await generateSessionSummary( dbSessionId, chatSessionId, locale || 'zh-CN', forceRegenerate || false, strategy ) - console.log('[IPC] session:generateSummary result:', result) - return result } catch (error) { console.error('[IPC] Failed to generate session summary:', error) return { success: false, error: String(error) } @@ -923,9 +357,6 @@ export function registerChatHandlers(ctx: IpcContext): void { } ) - /** - * 批量生成会话摘要 - */ ipcMain.handle( 'session:generateSummaries', async (_, dbSessionId: string, chatSessionIds: number[], locale?: string) => { @@ -939,14 +370,10 @@ export function registerChatHandlers(ctx: IpcContext): void { } ) - /** - * 批量检查会话是否可以生成摘要 - */ ipcMain.handle('session:checkCanGenerateSummary', async (_, dbSessionId: string, chatSessionIds: number[]) => { try { const { checkSessionsCanGenerateSummary } = await import('../ai/summary') const results = checkSessionsCanGenerateSummary(dbSessionId, chatSessionIds) - // 将 Map 转换为普通对象以便 IPC 传输 const obj: Record = {} for (const [id, result] of results) { obj[id] = result @@ -958,95 +385,8 @@ export function registerChatHandlers(ctx: IpcContext): void { } }) - /** - * 根据时间范围查询会话列表 - */ - ipcMain.handle('session:getByTimeRange', async (_, dbSessionId: string, startTs: number, endTs: number) => { - try { - return await worker.getSessionsByTimeRange(dbSessionId, startTs, endTs) - } catch (error) { - console.error('Failed to query sessions by time range:', error) - return [] - } - }) + // ==================== 临时导出(合并用) ==================== - /** - * 获取最近 N 条会话 - */ - ipcMain.handle('session:getRecent', async (_, dbSessionId: string, limit: number) => { - try { - return await worker.getRecentChatSessions(dbSessionId, limit) - } catch (error) { - console.error('Failed to query recent sessions:', error) - return [] - } - }) - - // ==================== 增量导入 ==================== - - /** - * 分析增量导入(检测去重后能新增多少消息) - */ - ipcMain.handle('chat:analyzeIncrementalImport', async (_, sessionId: string, filePath: string) => { - try { - // 检测文件格式 - const formatFeature = detectFormat(filePath) - if (!formatFeature) { - return { error: 'error.unrecognized_format' } - } - - // 使用 Worker 分析 - const result = await worker.analyzeIncrementalImport(sessionId, filePath) - return result - } catch (error) { - console.error('[IpcMain] Failed to analyze incremental import:', error) - return { error: String(error) } - } - }) - - /** - * 执行增量导入 - */ - ipcMain.handle('chat:incrementalImport', async (_, sessionId: string, filePath: string) => { - try { - // 发送进度 - win.webContents.send('chat:importProgress', { - stage: 'saving', - progress: 0, - message: '', - }) - - const result = await worker.incrementalImport(sessionId, filePath, (progress) => { - win.webContents.send('chat:importProgress', { - stage: progress.stage, - progress: progress.percentage, - message: progress.message, - }) - }) - - if (result.success) { - // 增量生成会话索引(仅处理新增消息,保留已有会话和摘要) - try { - await worker.generateIncrementalSessions(sessionId) - } catch (e) { - console.error('[IpcMain] Failed to incrementally generate session index:', e) - } - // 数据变更后清除分析缓存 - worker.invalidateAnalysisCache(sessionId).catch(() => {}) - } - - return result - } catch (error) { - console.error('[IpcMain] Failed to execute incremental import:', error) - return { success: false, error: String(error) } - } - }) - - // ==================== 批量管理:导出会话为临时文件 ==================== - - /** - * 导出多个会话为临时文件(用于合并) - */ ipcMain.handle('chat:exportSessionsToTempFiles', async (_, sessionIds: string[]) => { try { const tempFiles: string[] = [] @@ -1061,9 +401,6 @@ export function registerChatHandlers(ctx: IpcContext): void { } }) - /** - * 清理临时导出文件 - */ ipcMain.handle('chat:cleanupTempExportFiles', async (_, filePaths: string[]) => { try { cleanupTempExportFiles(filePaths) diff --git a/apps/desktop/main/ipc/messages.ts b/apps/desktop/main/ipc/messages.ts index 801b4e94..4dfebfd9 100644 --- a/apps/desktop/main/ipc/messages.ts +++ b/apps/desktop/main/ipc/messages.ts @@ -1,6 +1,9 @@ /** - * 聊天记录查询 IPC 处理器 - * 提供通用的消息查询功能:搜索、筛选、上下文、无限滚动等 + * 消息查询/导出 IPC 处理器 + * + * 大部分消息查询已迁移到 Internal HTTP Server (FetchMessageAdapter)。 + * 保留 filterMessagesWithContext / getMultipleSessionsMessages(ElectronAIAdapter 依赖) + * 和导出功能(需要文件系统写入 + 进度推送)。 */ import { ipcMain } from 'electron' @@ -8,149 +11,7 @@ import type { IpcContext } from './types' import * as worker from '../worker/workerManager' export function registerMessagesHandlers({ win }: IpcContext): void { - console.log('[IPC] Registering Messages handlers...') - - /** - * 关键词搜索消息 - */ - ipcMain.handle( - 'ai:searchMessages', - async ( - _, - sessionId: string, - keywords: string[], - filter?: { startTs?: number; endTs?: number }, - limit?: number, - offset?: number, - senderId?: number - ) => { - try { - return await worker.searchMessages(sessionId, keywords, filter, limit, offset, senderId) - } catch (error) { - console.error('Failed to search messages:', error) - return { messages: [], total: 0 } - } - } - ) - - /** - * 获取消息上下文 - */ - ipcMain.handle( - 'ai:getMessageContext', - async (_, sessionId: string, messageIds: number | number[], contextSize?: number) => { - try { - return await worker.getMessageContext(sessionId, messageIds, contextSize) - } catch (error) { - console.error('Failed to get message context:', error) - return [] - } - } - ) - - /** - * 获取最近消息(AI Agent 专用) - */ - ipcMain.handle( - 'ai:getRecentMessages', - async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }, limit?: number) => { - try { - return await worker.getRecentMessages(sessionId, filter, limit) - } catch (error) { - console.error('Failed to get recent messages:', error) - return { messages: [], total: 0 } - } - } - ) - - /** - * 获取所有最近消息(消息查看器专用) - */ - ipcMain.handle( - 'ai:getAllRecentMessages', - async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }, limit?: number) => { - try { - return await worker.getAllRecentMessages(sessionId, filter, limit) - } catch (error) { - console.error('Failed to get all recent messages:', error) - return { messages: [], total: 0 } - } - } - ) - - /** - * 获取两人之间的对话 - */ - ipcMain.handle( - 'ai:getConversationBetween', - async ( - _, - sessionId: string, - memberId1: number, - memberId2: number, - filter?: { startTs?: number; endTs?: number }, - limit?: number - ) => { - try { - return await worker.getConversationBetween(sessionId, memberId1, memberId2, filter, limit) - } catch (error) { - console.error('Failed to get conversation:', error) - return { messages: [], total: 0, member1Name: '', member2Name: '' } - } - } - ) - - /** - * 获取指定消息之前的 N 条(用于向上无限滚动) - */ - ipcMain.handle( - 'ai:getMessagesBefore', - async ( - _, - sessionId: string, - beforeId: number, - limit?: number, - filter?: { startTs?: number; endTs?: number }, - senderId?: number, - keywords?: string[] - ) => { - try { - return await worker.getMessagesBefore(sessionId, beforeId, limit, filter, senderId, keywords) - } catch (error) { - console.error('Failed to get previous messages:', error) - return { messages: [], hasMore: false } - } - } - ) - - /** - * 获取指定消息之后的 N 条(用于向下无限滚动) - */ - ipcMain.handle( - 'ai:getMessagesAfter', - async ( - _, - sessionId: string, - afterId: number, - limit?: number, - filter?: { startTs?: number; endTs?: number }, - senderId?: number, - keywords?: string[] - ) => { - try { - return await worker.getMessagesAfter(sessionId, afterId, limit, filter, senderId, keywords) - } catch (error) { - console.error('Failed to get next messages:', error) - return { messages: [], hasMore: false } - } - } - ) - - // ==================== 自定义筛选 ==================== - - /** - * 按条件筛选消息并扩充上下文(支持分页) - */ + // ElectronAIAdapter 依赖的消息筛选/多会话查询 ipcMain.handle( 'ai:filterMessagesWithContext', async ( @@ -178,34 +39,29 @@ export function registerMessagesHandlers({ win }: IpcContext): void { return { blocks: [], stats: { totalMessages: 0, hitMessages: 0, totalChars: 0 }, - pagination: { page: page ?? 1, pageSize: pageSize ?? 50, totalBlocks: 0, totalHits: 0, hasMore: false }, + pagination: { page: 1, pageSize: 50, totalBlocks: 0, totalHits: 0, hasMore: false }, } } } ) - /** - * 获取多个会话的完整消息(支持分页) - */ ipcMain.handle( 'ai:getMultipleSessionsMessages', async (_, sessionId: string, chatSessionIds: number[], page?: number, pageSize?: number) => { try { return await worker.getMultipleSessionsMessages(sessionId, chatSessionIds, page, pageSize) } catch (error) { - console.error('Failed to get multi-session messages:', error) + console.error('Failed to get multiple sessions messages:', error) return { blocks: [], stats: { totalMessages: 0, hitMessages: 0, totalChars: 0 }, - pagination: { page: page ?? 1, pageSize: pageSize ?? 50, totalBlocks: 0, totalHits: 0, hasMore: false }, + pagination: { page: 1, pageSize: 50, totalBlocks: 0, totalHits: 0, hasMore: false }, } } } ) - /** - * 导出筛选结果到文件(后端生成,支持进度) - */ + // 导出(需要文件系统写入 + 进度推送) ipcMain.handle( 'ai:exportFilterResultToFile', async ( @@ -224,7 +80,6 @@ export function registerMessagesHandlers({ win }: IpcContext): void { ) => { try { return await worker.exportFilterResultToFile(params, (progress) => { - // 发送进度到渲染进程 win.webContents.send('ai:exportProgress', progress) }) } catch (error) { diff --git a/apps/desktop/main/ipc/nlp.ts b/apps/desktop/main/ipc/nlp.ts deleted file mode 100644 index 334dafd8..00000000 --- a/apps/desktop/main/ipc/nlp.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * NLP 功能 IPC 处理器 - * 提供词频统计、分词等 NLP 功能,以及词库管理 - */ - -import { ipcMain } from 'electron' -import * as worker from '../worker/workerManager' -import type { IpcContext } from './types' -import type { WordFrequencyParams, WordFrequencyResult, SupportedLocale, PosTagInfo } from '@openchatlab/core' -import { getDictList, downloadDict, deleteDict, isDictDownloaded, type DictInfo } from '../nlp/dictManager' - -/** - * 注册 NLP 相关 IPC 处理器 - */ -export function registerNlpHandlers(_ctx: IpcContext): void { - /** - * 获取词频统计 - * 用于词云展示 - */ - ipcMain.handle('nlp:getWordFrequency', async (_event, params: WordFrequencyParams): Promise => { - try { - const result = await worker.query('getWordFrequency', params) - return result as WordFrequencyResult - } catch (error) { - console.error('[NLP] Failed to get word frequency stats:', error) - return { - words: [], - totalWords: 0, - totalMessages: 0, - uniqueWords: 0, - } - } - }) - - /** - * 单文本分词 - * 用于调试或其他用途 - */ - ipcMain.handle( - 'nlp:segmentText', - async (_event, text: string, locale: SupportedLocale, minLength?: number): Promise => { - try { - const result = await worker.query('segmentText', { text, locale, minLength }) - return result as string[] - } catch (error) { - console.error('[NLP] Segmentation failed:', error) - return [] - } - } - ) - - /** - * 获取词性标签定义 - */ - ipcMain.handle('nlp:getPosTags', async (): Promise => { - try { - const result = await worker.query('getPosTags', {}) - return result as PosTagInfo[] - } catch (error) { - console.error('[NLP] Failed to get POS tags:', error) - return [] - } - }) - - // ==================== 词库管理 ==================== - - ipcMain.handle('nlp:getDictList', async (): Promise => { - return getDictList() - }) - - ipcMain.handle('nlp:isDictDownloaded', async (_event, dictId: string): Promise => { - return isDictDownloaded(dictId) - }) - - ipcMain.handle('nlp:downloadDict', async (_event, dictId: string): Promise<{ success: boolean; error?: string }> => { - return downloadDict(dictId) - }) - - ipcMain.handle('nlp:deleteDict', async (_event, dictId: string): Promise<{ success: boolean; error?: string }> => { - return deleteDict(dictId) - }) -} diff --git a/apps/desktop/main/ipc/preferences.ts b/apps/desktop/main/ipc/preferences.ts deleted file mode 100644 index b24b7997..00000000 --- a/apps/desktop/main/ipc/preferences.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Preferences IPC handlers - * - * Read/write ~/.chatlab/preferences.json and config.toml [ui] section. - */ - -import { ipcMain } from 'electron' -import type { IpcContext } from './types' -import { PreferencesManager, type Preferences } from '@openchatlab/node-runtime' -import { loadConfig, writeConfigField, type UiConfig } from '@openchatlab/config' -import { getSystemDataDir } from '../paths' - -let prefManager: PreferencesManager | null = null - -function getManager(): PreferencesManager { - if (!prefManager) { - prefManager = new PreferencesManager(getSystemDataDir()) - } - return prefManager -} - -export function registerPreferencesHandlers(_context: IpcContext): void { - console.log('[IpcMain] Registering preferences handlers...') - - ipcMain.handle('preferences:get', (): Preferences => { - return getManager().load() - }) - - ipcMain.handle('preferences:save', (_event, partial: Partial): { success: boolean; error?: string } => { - return getManager().save(partial) - }) - - ipcMain.handle('preferences:getUiConfig', (): UiConfig => { - const config = loadConfig() - return config.ui - }) - - ipcMain.handle( - 'preferences:saveUiConfig', - (_event, partial: Partial): { success: boolean; error?: string } => { - try { - for (const [key, value] of Object.entries(partial)) { - if (value !== undefined) { - writeConfigField('ui', key, value as string | number) - } - } - return { success: true } - } catch (err) { - return { success: false, error: err instanceof Error ? err.message : String(err) } - } - } - ) - - ipcMain.handle('preferences:getLocale', (): string => { - const config = loadConfig() - return config.locale.lang - }) - - ipcMain.handle('preferences:saveLocale', (_event, lang: string): { success: boolean; error?: string } => { - try { - writeConfigField('locale', 'lang', lang) - return { success: true } - } catch (err) { - return { success: false, error: err instanceof Error ? err.message : String(err) } - } - }) - - console.log('[IpcMain] Preferences handlers registered') -} diff --git a/apps/desktop/main/ipcMain.ts b/apps/desktop/main/ipcMain.ts index 276c5c41..5ce885d6 100644 --- a/apps/desktop/main/ipcMain.ts +++ b/apps/desktop/main/ipcMain.ts @@ -13,11 +13,9 @@ import { registerAIHandlers } from './ipc/ai' import { registerMessagesHandlers } from './ipc/messages' import { registerCacheHandlers } from './ipc/cache' 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' -import { registerPreferencesHandlers } from './ipc/preferences' // 导入 Worker 模块(用于异步分析查询和流式导入) import * as worker from './worker/workerManager' @@ -49,11 +47,9 @@ const mainIpcMain = (win: BrowserWindow) => { registerMessagesHandlers(context) registerCacheHandlers(context) registerNetworkHandlers(context) - registerNlpHandlers(context) registerAnalyticsHandlers() registerApiHandlers(context) registerDemoHandlers(context) - registerPreferencesHandlers(context) // 启动 ChatLab API 服务(异步,不阻塞 IPC 注册) initApiServer(context).catch((err) => { diff --git a/apps/desktop/preload/apis/ai.ts b/apps/desktop/preload/apis/ai.ts index 253a09db..45286894 100644 --- a/apps/desktop/preload/apis/ai.ts +++ b/apps/desktop/preload/apis/ai.ts @@ -6,22 +6,6 @@ import type { ExportProgress } from '../../../../src/types/base' // ==================== 类型定义 ==================== -// AI API 类型 -export interface SearchMessageResult { - id: number - senderId: number - senderName: string - senderPlatformId: string - senderAliases: string[] - senderAvatar: string | null - content: string - timestamp: number - type: number - replyToMessageId: string | null - replyToContent: string | null - replyToSenderName: string | null -} - export interface AIConversation { id: string sessionId: string @@ -210,108 +194,8 @@ export interface AIServiceConfigDisplay { // ==================== AI API ==================== export const aiApi = { - /** - * 搜索消息(关键词搜索) - * @param senderId 可选的发送者成员 ID,用于筛选特定成员的消息 - */ - searchMessages: ( - sessionId: string, - keywords: string[], - filter?: { startTs?: number; endTs?: number }, - limit?: number, - offset?: number, - senderId?: number - ): Promise<{ messages: SearchMessageResult[]; total: number }> => { - return ipcRenderer.invoke('ai:searchMessages', sessionId, keywords, filter, limit, offset, senderId) - }, + // ==================== 消息筛选/导出(IPC handler 保留) ==================== - /** - * 获取消息上下文 - * @param messageIds 支持单个或批量消息 ID - */ - getMessageContext: ( - sessionId: string, - messageIds: number | number[], - contextSize?: number - ): Promise => { - return ipcRenderer.invoke('ai:getMessageContext', sessionId, messageIds, contextSize) - }, - - /** - * 获取最近消息(AI Agent 专用) - */ - getRecentMessages: ( - sessionId: string, - filter?: { startTs?: number; endTs?: number }, - limit?: number - ): Promise<{ messages: SearchMessageResult[]; total: number }> => { - return ipcRenderer.invoke('ai:getRecentMessages', sessionId, filter, limit) - }, - - /** - * 获取所有最近消息(消息查看器专用) - */ - getAllRecentMessages: ( - sessionId: string, - filter?: { startTs?: number; endTs?: number }, - limit?: number - ): Promise<{ messages: SearchMessageResult[]; total: number }> => { - return ipcRenderer.invoke('ai:getAllRecentMessages', sessionId, filter, limit) - }, - - /** - * 获取两人之间的对话 - */ - getConversationBetween: ( - sessionId: string, - memberId1: number, - memberId2: number, - filter?: { startTs?: number; endTs?: number }, - limit?: number - ): Promise<{ messages: SearchMessageResult[]; total: number; member1Name: string; member2Name: string }> => { - return ipcRenderer.invoke('ai:getConversationBetween', sessionId, memberId1, memberId2, filter, limit) - }, - - /** - * 获取指定消息之前的 N 条(用于向上无限滚动) - */ - getMessagesBefore: ( - sessionId: string, - beforeId: number, - limit?: number, - filter?: { startTs?: number; endTs?: number }, - senderId?: number, - keywords?: string[] - ): Promise<{ messages: SearchMessageResult[]; hasMore: boolean }> => { - return ipcRenderer.invoke('ai:getMessagesBefore', sessionId, beforeId, limit, filter, senderId, keywords) - }, - - /** - * 获取指定消息之后的 N 条(用于向下无限滚动) - */ - getMessagesAfter: ( - sessionId: string, - afterId: number, - limit?: number, - filter?: { startTs?: number; endTs?: number }, - senderId?: number, - keywords?: string[] - ): Promise<{ messages: SearchMessageResult[]; hasMore: boolean }> => { - return ipcRenderer.invoke('ai:getMessagesAfter', sessionId, afterId, limit, filter, senderId, keywords) - }, - - // ==================== 自定义筛选(支持分页) ==================== - - /** - * 筛选结果消息类型 - */ - // FilterMessage 和 FilterResult 类型定义在下方 - - /** - * 按条件筛选消息并扩充上下文(支持分页) - * @param page 页码(从 1 开始,默认 1) - * @param pageSize 每页块数(默认 50) - */ filterMessagesWithContext: ( sessionId: string, keywords?: string[], diff --git a/apps/desktop/preload/apis/chat.ts b/apps/desktop/preload/apis/chat.ts index d2c9037e..93fe8f09 100644 --- a/apps/desktop/preload/apis/chat.ts +++ b/apps/desktop/preload/apis/chat.ts @@ -1,444 +1,67 @@ /** - * 聊天记录 API - 导入、分析、管理聊天记录 + * 聊天记录 Preload API — 导入、迁移、Demo、合并 + * + * 数据查询/分析/成员管理/SQL/NLP/偏好设置已迁移到 + * Internal HTTP Server (FetchDataAdapter, FetchPreferencesAdapter 等)。 */ import { ipcRenderer } from 'electron' -import type { AnalysisSession, MessageType, ImportProgress } from '../../../../src/types/base' -import type { - MemberActivity, - MemberNameHistory, - HourlyActivity, - DailyActivity, - WeekdayActivity, - MonthlyActivity, - CatchphraseAnalysis, - MentionAnalysis, - LaughAnalysis, - MemberWithStats, - ClusterGraphData, - ClusterGraphOptions, - RelationshipStats, -} from '../../../../src/types/analysis' -import type { LanguagePreferenceResult } from '../../../../src/types/quotes/languagePreference' +import type { ImportProgress } from '../../../../src/types/base' import type { FileParseInfo, ConflictCheckResult, MergeParams, MergeResult } from '../../../../src/types/format' -// Chat Analysis API export const chatApi = { // ==================== 数据库迁移 ==================== - /** - * 检查是否需要数据库迁移 - */ checkMigration: (): Promise<{ needsMigration: boolean count: number currentVersion: number pendingMigrations: Array<{ version: number; userMessage: string }> - }> => { - return ipcRenderer.invoke('chat:checkMigration') - }, + }> => ipcRenderer.invoke('chat:checkMigration'), - /** - * 执行数据库迁移 - */ - runMigration: (): Promise<{ success: boolean; migratedCount: number; error?: string }> => { - return ipcRenderer.invoke('chat:runMigration') - }, + runMigration: (): Promise<{ success: boolean; migratedCount: number; error?: string }> => + ipcRenderer.invoke('chat:runMigration'), - // ==================== 聊天记录导入与分析 ==================== + // ==================== 文件选择与导入 ==================== - /** - * 选择聊天记录文件 - */ - selectFile: (): Promise<{ filePath?: string; format?: string; error?: string } | null> => { - return ipcRenderer.invoke('chat:selectFile') - }, + selectFile: (): Promise<{ filePath?: string; format?: string; error?: string } | null> => + ipcRenderer.invoke('chat:selectFile'), - /** - * 导入聊天记录 - */ - import: (filePath: string): Promise<{ success: boolean; sessionId?: string; error?: string }> => { - return ipcRenderer.invoke('chat:import', filePath) - }, + import: (filePath: string): Promise<{ success: boolean; sessionId?: string; error?: string }> => + ipcRenderer.invoke('chat:import', filePath), - /** - * Import from a directory path (scans for entry file) - */ - importDirectory: (dirPath: string): Promise<{ success: boolean; sessionId?: string; error?: string }> => { - return ipcRenderer.invoke('chat:importDirectory', dirPath) - }, + importDirectory: (dirPath: string): Promise<{ success: boolean; sessionId?: string; error?: string }> => + ipcRenderer.invoke('chat:importDirectory', dirPath), - /** - * 检测文件格式(轻量级) - */ detectFormat: ( filePath: string - ): Promise<{ id: string; name: string; platform: string; multiChat: boolean } | null> => { - return ipcRenderer.invoke('chat:detectFormat', filePath) - }, + ): Promise<{ id: string; name: string; platform: string; multiChat: boolean } | null> => + ipcRenderer.invoke('chat:detectFormat', filePath), - /** - * 导入聊天记录(带格式选项) - * 用于多聊天格式等需要额外参数的场景(如指定 chatIndex) - */ importWithOptions: ( filePath: string, formatOptions: Record - ): Promise<{ success: boolean; sessionId?: string; error?: string }> => { - return ipcRenderer.invoke('chat:importWithOptions', filePath, formatOptions) - }, + ): Promise<{ success: boolean; sessionId?: string; error?: string }> => + ipcRenderer.invoke('chat:importWithOptions', filePath, formatOptions), - /** - * 扫描多聊天文件中的聊天列表(通用) - * 自动检测格式并调用对应格式的 scanChats - */ scanMultiChatFile: ( filePath: string ): Promise<{ success: boolean chats: Array<{ index: number; name: string; type: string; id: number; messageCount: number }> error?: string - }> => { - return ipcRenderer.invoke('chat:scanMultiChatFile', filePath) - }, + }> => ipcRenderer.invoke('chat:scanMultiChatFile', filePath), - /** - * 获取所有分析会话列表 - */ - getSessions: (): Promise => { - return ipcRenderer.invoke('chat:getSessions') - }, + getSupportedFormats: (): Promise> => + ipcRenderer.invoke('chat:getSupportedFormats'), - /** - * 获取单个会话信息 - */ - getSession: (sessionId: string): Promise => { - return ipcRenderer.invoke('chat:getSession', sessionId) - }, - - /** - * 删除会话 - */ - deleteSession: (sessionId: string): Promise => { - return ipcRenderer.invoke('chat:deleteSession', sessionId) - }, - - /** - * 重命名会话 - */ - renameSession: (sessionId: string, newName: string): Promise => { - return ipcRenderer.invoke('chat:renameSession', sessionId, newName) - }, - - /** - * 获取可用年份列表 - */ - getAvailableYears: (sessionId: string): Promise => { - return ipcRenderer.invoke('chat:getAvailableYears', sessionId) - }, - - /** - * 获取成员活跃度排行 - */ - getMemberActivity: (sessionId: string, filter?: { startTs?: number; endTs?: number }): Promise => { - return ipcRenderer.invoke('chat:getMemberActivity', sessionId, filter) - }, - - /** - * 获取成员历史昵称 - */ - getMemberNameHistory: (sessionId: string, memberId: number): Promise => { - return ipcRenderer.invoke('chat:getMemberNameHistory', sessionId, memberId) - }, - - /** - * 获取每小时活跃度分布 - */ - getHourlyActivity: (sessionId: string, filter?: { startTs?: number; endTs?: number }): Promise => { - return ipcRenderer.invoke('chat:getHourlyActivity', sessionId, filter) - }, - - /** - * 获取每日活跃度趋势 - */ - getDailyActivity: (sessionId: string, filter?: { startTs?: number; endTs?: number }): Promise => { - return ipcRenderer.invoke('chat:getDailyActivity', sessionId, filter) - }, - - /** - * 获取星期活跃度分布 - */ - getWeekdayActivity: ( - sessionId: string, - filter?: { startTs?: number; endTs?: number } - ): Promise => { - return ipcRenderer.invoke('chat:getWeekdayActivity', sessionId, filter) - }, - - /** - * 获取月份活跃度分布 - */ - getMonthlyActivity: ( - sessionId: string, - filter?: { startTs?: number; endTs?: number } - ): Promise => { - return ipcRenderer.invoke('chat:getMonthlyActivity', sessionId, filter) - }, - - /** - * 获取年份活跃度分布 - */ - getYearlyActivity: ( - sessionId: string, - filter?: { startTs?: number; endTs?: number } - ): Promise> => { - return ipcRenderer.invoke('chat:getYearlyActivity', sessionId, filter) - }, - - /** - * 获取消息长度分布 - */ - getMessageLengthDistribution: ( - sessionId: string, - filter?: { startTs?: number; endTs?: number } - ): Promise<{ - detail: Array<{ len: number; count: number }> - grouped: Array<{ range: string; count: number }> - }> => { - return ipcRenderer.invoke('chat:getMessageLengthDistribution', sessionId, filter) - }, - - /** - * 获取消息类型分布 - */ - getMessageTypeDistribution: ( - sessionId: string, - filter?: { startTs?: number; endTs?: number } - ): Promise> => { - return ipcRenderer.invoke('chat:getMessageTypeDistribution', sessionId, filter) - }, - - /** - * 获取时间范围 - */ - getTimeRange: (sessionId: string): Promise<{ start: number; end: number } | null> => { - return ipcRenderer.invoke('chat:getTimeRange', sessionId) - }, - - /** - * 获取数据库存储目录 - */ - getDbDirectory: (): Promise => { - return ipcRenderer.invoke('chat:getDbDirectory') - }, - - /** - * 获取支持的格式列表 - */ - getSupportedFormats: (): Promise> => { - return ipcRenderer.invoke('chat:getSupportedFormats') - }, - - /** - * 监听导入进度 - */ onImportProgress: (callback: (progress: ImportProgress) => void) => { - const handler = (_event: Electron.IpcRendererEvent, progress: ImportProgress) => { - callback(progress) - } + const handler = (_event: Electron.IpcRendererEvent, progress: ImportProgress) => callback(progress) ipcRenderer.on('chat:importProgress', handler) - return () => { - ipcRenderer.removeListener('chat:importProgress', handler) - } - }, - - /** - * 获取口头禅分析数据 - */ - getCatchphraseAnalysis: ( - sessionId: string, - filter?: { startTs?: number; endTs?: number } - ): Promise => { - return ipcRenderer.invoke('chat:getCatchphraseAnalysis', sessionId, filter) - }, - - /** - * 获取语言偏好分析数据(私聊专用) - */ - getLanguagePreferenceAnalysis: ( - sessionId: string, - locale: string, - filter?: { startTs?: number; endTs?: number }, - dictType?: string - ): Promise => { - return ipcRenderer.invoke('chat:getLanguagePreferenceAnalysis', sessionId, locale, filter, dictType) - }, - - /** - * 获取 @ 互动分析数据 - */ - getMentionAnalysis: (sessionId: string, filter?: { startTs?: number; endTs?: number }): Promise => { - return ipcRenderer.invoke('chat:getMentionAnalysis', sessionId, filter) - }, - - /** - * 获取 @ 互动关系图数据 - */ - getMentionGraph: ( - sessionId: string, - filter?: { startTs?: number; endTs?: number } - ): Promise<{ - nodes: Array<{ id: number; name: string; value: number; symbolSize: number }> - links: Array<{ source: string; target: string; value: number }> - maxLinkValue: number - }> => { - return ipcRenderer.invoke('chat:getMentionGraph', sessionId, filter) - }, - - /** - * 获取小团体关系图数据(基于时间相邻共现) - */ - getClusterGraph: ( - sessionId: string, - filter?: { startTs?: number; endTs?: number }, - options?: ClusterGraphOptions - ): Promise => { - return ipcRenderer.invoke('chat:getClusterGraph', sessionId, filter, options) - }, - - /** - * 获取含笑量分析数据 - */ - getLaughAnalysis: ( - sessionId: string, - filter?: { startTs?: number; endTs?: number }, - keywords?: string[] - ): Promise => { - return ipcRenderer.invoke('chat:getLaughAnalysis', sessionId, filter, keywords) - }, - - /** - * 获取关系主动性分析数据(私聊专属) - */ - getRelationshipStats: ( - sessionId: string, - filter?: { startTs?: number; endTs?: number }, - options?: { perseveranceThreshold?: number } - ): Promise => { - return ipcRenderer.invoke('chat:getRelationshipStats', sessionId, filter, options) - }, - - // ==================== 成员管理 ==================== - - /** - * 获取所有成员列表(含消息数和别名) - */ - getMembers: (sessionId: string): Promise => { - return ipcRenderer.invoke('chat:getMembers', sessionId) - }, - - /** - * 获取成员列表(分页版本) - */ - getMembersPaginated: ( - sessionId: string, - params: { page: number; pageSize: number; search?: string; sortOrder?: 'asc' | 'desc' } - ): Promise<{ - members: MemberWithStats[] - total: number - page: number - pageSize: number - totalPages: number - }> => { - return ipcRenderer.invoke('chat:getMembersPaginated', sessionId, params) - }, - - /** - * 更新成员别名 - */ - updateMemberAliases: (sessionId: string, memberId: number, aliases: string[]): Promise => { - return ipcRenderer.invoke('chat:updateMemberAliases', sessionId, memberId, aliases) - }, - - /** - * 合并成员(保留消息数更多的一方) - */ - mergeMembers: (sessionId: string, memberId1: number, memberId2: number): Promise => { - return ipcRenderer.invoke('chat:mergeMembers', sessionId, memberId1, memberId2) - }, - - /** - * 删除成员及其所有消息 - */ - deleteMember: (sessionId: string, memberId: number): Promise => { - return ipcRenderer.invoke('chat:deleteMember', sessionId, memberId) - }, - - /** - * 更新会话的所有者(ownerId) - * @param ownerId 成员的 platformId,设置为 null 则清除 - */ - updateSessionOwnerId: (sessionId: string, ownerId: string | null): Promise => { - return ipcRenderer.invoke('chat:updateSessionOwnerId', sessionId, ownerId) - }, - - // ==================== 插件系统 ==================== - - /** - * 插件参数化只读 SQL 查询 - */ - pluginQuery: >(sessionId: string, sql: string, params: any[] = []): Promise => { - return ipcRenderer.invoke('chat:pluginQuery', sessionId, sql, params) - }, - - /** - * 插件计算卸载(纯函数在 Worker 中执行) - */ - pluginCompute: (fnString: string, input: any): Promise => { - return ipcRenderer.invoke('chat:pluginCompute', fnString, input) - }, - - // ==================== SQL 实验室 ==================== - - /** - * 执行用户 SQL 查询 - */ - executeSQL: ( - sessionId: string, - sql: string - ): Promise<{ - columns: string[] - rows: any[][] - rowCount: number - duration: number - limited: boolean - }> => { - return ipcRenderer.invoke('chat:executeSQL', sessionId, sql) - }, - - /** - * 获取数据库 Schema - */ - getSchema: ( - sessionId: string - ): Promise< - Array<{ - name: string - columns: Array<{ - name: string - type: string - notnull: boolean - pk: boolean - }> - }> - > => { - return ipcRenderer.invoke('chat:getSchema', sessionId) + return () => ipcRenderer.removeListener('chat:importProgress', handler) }, // ==================== 增量导入 ==================== - /** - * 分析增量导入(检测去重后能新增多少消息) - */ analyzeIncrementalImport: ( sessionId: string, filePath: string @@ -448,113 +71,48 @@ export const chatApi = { totalInFile: number error?: string diagnosis?: { suggestion?: string } - }> => { - return ipcRenderer.invoke('chat:analyzeIncrementalImport', sessionId, filePath) - }, + }> => ipcRenderer.invoke('chat:analyzeIncrementalImport', sessionId, filePath), - /** - * 执行增量导入 - */ incrementalImport: ( sessionId: string, filePath: string - ): Promise<{ - success: boolean - newMessageCount: number - error?: string - }> => { - return ipcRenderer.invoke('chat:incrementalImport', sessionId, filePath) - }, + ): Promise<{ success: boolean; newMessageCount: number; error?: string }> => + ipcRenderer.invoke('chat:incrementalImport', sessionId, filePath), + + // ==================== 临时导出(合并用) ==================== - /** - * 导出多个会话为临时文件(用于批量管理中的合并) - */ exportSessionsToTempFiles: ( sessionIds: string[] - ): Promise<{ - success: boolean - tempFiles: string[] - error?: string - }> => { - return ipcRenderer.invoke('chat:exportSessionsToTempFiles', sessionIds) - }, + ): Promise<{ success: boolean; tempFiles: string[]; error?: string }> => + ipcRenderer.invoke('chat:exportSessionsToTempFiles', sessionIds), - /** - * 清理临时导出文件 - */ - cleanupTempExportFiles: ( - filePaths: string[] - ): Promise<{ - success: boolean - error?: string - }> => { - return ipcRenderer.invoke('chat:cleanupTempExportFiles', filePaths) - }, + cleanupTempExportFiles: (filePaths: string[]): Promise<{ success: boolean; error?: string }> => + ipcRenderer.invoke('chat:cleanupTempExportFiles', filePaths), - // ==================== Demo 示例数据 ==================== + // ==================== Demo ==================== - /** - * 下载并导入 Demo 示例数据 - */ importDemo: ( locale: string - ): Promise<{ - success: boolean - groupSessionId?: string - privateSessionIds?: string[] - error?: string - }> => { - return ipcRenderer.invoke('demo:downloadAndImport', locale) - }, + ): Promise<{ success: boolean; groupSessionId?: string; privateSessionIds?: string[]; error?: string }> => + 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) - } + ) => callback(progress) ipcRenderer.on('demo:progress', handler) - return () => { - ipcRenderer.removeListener('demo:progress', handler) - } + return () => ipcRenderer.removeListener('demo:progress', handler) }, } -// Merge API - 合并功能 +// Merge API export const mergeApi = { - /** - * 解析文件获取基本信息(用于合并预览) - * 解析后结果会被缓存,后续合并时无需再次读取原始文件 - */ - parseFileInfo: (filePath: string): Promise => { - return ipcRenderer.invoke('merge:parseFileInfo', filePath) - }, - - /** - * 检测合并冲突 - */ - checkConflicts: (filePaths: string[]): Promise => { - return ipcRenderer.invoke('merge:checkConflicts', filePaths) - }, - - /** - * 执行合并 - */ - mergeFiles: (params: MergeParams): Promise => { - return ipcRenderer.invoke('merge:mergeFiles', params) - }, - - /** - * 清理解析缓存 - * @param filePath 可选,指定文件路径则清理该文件的缓存,否则清理所有缓存 - */ - clearCache: (filePath?: string): Promise => { - return ipcRenderer.invoke('merge:clearCache', filePath) - }, + parseFileInfo: (filePath: string): Promise => ipcRenderer.invoke('merge:parseFileInfo', filePath), + checkConflicts: (filePaths: string[]): Promise => + ipcRenderer.invoke('merge:checkConflicts', filePaths), + mergeFiles: (params: MergeParams): Promise => ipcRenderer.invoke('merge:mergeFiles', params), + clearCache: (filePath?: string): Promise => ipcRenderer.invoke('merge:clearCache', filePath), } diff --git a/apps/desktop/preload/apis/preferences.ts b/apps/desktop/preload/apis/preferences.ts deleted file mode 100644 index 1bfa812c..00000000 --- a/apps/desktop/preload/apis/preferences.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Preferences Preload API - */ -import { ipcRenderer } from 'electron' - -export const preferencesApi = { - get: (): Promise> => { - return ipcRenderer.invoke('preferences:get') - }, - - save: (partial: Record): Promise<{ success: boolean; error?: string }> => { - return ipcRenderer.invoke('preferences:save', partial) - }, - - getUiConfig: (): Promise> => { - return ipcRenderer.invoke('preferences:getUiConfig') - }, - - saveUiConfig: (partial: Record): Promise<{ success: boolean; error?: string }> => { - return ipcRenderer.invoke('preferences:saveUiConfig', partial) - }, - - getLocale: (): Promise => { - return ipcRenderer.invoke('preferences:getLocale') - }, - - saveLocale: (lang: string): Promise<{ success: boolean; error?: string }> => { - return ipcRenderer.invoke('preferences:saveLocale', lang) - }, -} diff --git a/apps/desktop/preload/apis/utils.ts b/apps/desktop/preload/apis/utils.ts index ffd69757..4f548415 100644 --- a/apps/desktop/preload/apis/utils.ts +++ b/apps/desktop/preload/apis/utils.ts @@ -1,53 +1,10 @@ /** - * 工具类 API - NLP、网络、缓存、会话索引 + * 工具类 API - 网络、缓存、会话索引 */ import { ipcRenderer } from 'electron' // ==================== 类型定义 ==================== -// NLP API 类型 -export interface WordFrequencyItem { - word: string - count: number - percentage: number -} - -export interface WordFrequencyResult { - words: WordFrequencyItem[] - totalWords: number - totalMessages: number - uniqueWords: number -} - -export type PosFilterMode = 'all' | 'meaningful' | 'custom' - -export interface WordFrequencyParams { - sessionId: string - locale: 'zh-CN' | 'en-US' - timeFilter?: { startTs?: number; endTs?: number } - memberId?: number - topN?: number - minWordLength?: number - minCount?: number - /** 词性过滤模式:all=全部, meaningful=只保留有意义的词, custom=自定义 */ - posFilterMode?: PosFilterMode - /** 自定义词性过滤列表(posFilterMode='custom' 时使用) */ - customPosTags?: string[] - /** 是否启用停用词过滤,默认 true */ - enableStopwords?: boolean - /** 词库类型 */ - dictType?: string - /** 要排除的词语列表(词云过滤方案) */ - excludeWords?: string[] -} - -export interface PosTagInfo { - tag: string - name: string - description: string - meaningful: boolean -} - // Network API 类型 export type ProxyMode = 'off' | 'system' | 'manual' @@ -96,40 +53,6 @@ export interface ChatSessionItem { firstMessageId: number } -// ==================== NLP API ==================== - -export const nlpApi = { - getWordFrequency: (params: WordFrequencyParams): Promise => { - return ipcRenderer.invoke('nlp:getWordFrequency', params) - }, - - segmentText: (text: string, locale: 'zh-CN' | 'en-US', minLength?: number): Promise => { - return ipcRenderer.invoke('nlp:segmentText', text, locale, minLength) - }, - - getPosTags: (): Promise => { - return ipcRenderer.invoke('nlp:getPosTags') - }, - - getDictList: (): Promise< - Array<{ id: string; label: string; locale: string; downloaded: boolean; fileSize?: number }> - > => { - return ipcRenderer.invoke('nlp:getDictList') - }, - - isDictDownloaded: (dictId: string): Promise => { - return ipcRenderer.invoke('nlp:isDictDownloaded', dictId) - }, - - downloadDict: (dictId: string): Promise<{ success: boolean; error?: string }> => { - return ipcRenderer.invoke('nlp:downloadDict', dictId) - }, - - deleteDict: (dictId: string): Promise<{ success: boolean; error?: string }> => { - return ipcRenderer.invoke('nlp:deleteDict', dictId) - }, -} - // ==================== Network API ==================== export const networkApi = { diff --git a/apps/desktop/preload/index.d.ts b/apps/desktop/preload/index.d.ts index 83d9dc84..c2a7d4bd 100644 --- a/apps/desktop/preload/index.d.ts +++ b/apps/desktop/preload/index.d.ts @@ -1,34 +1,10 @@ import { ElectronAPI } from '@electron-toolkit/preload' -import type { AnalysisSession, MessageType, ImportProgress, ExportProgress } from '../../../src/types/base' +import type { ImportProgress, ExportProgress } from '../../../src/types/base' import type { TokenUsage, AgentRuntimeStatus, SerializedErrorInfo } from '../shared/types' -import type { - MemberActivity, - MemberNameHistory, - HourlyActivity, - DailyActivity, - WeekdayActivity, - MonthlyActivity, - CatchphraseAnalysis, - MentionAnalysis, - LaughAnalysis, - MemberWithStats, - ClusterGraphData, - ClusterGraphOptions, - RelationshipStats, -} from '../../../src/types/analysis' import type { FileParseInfo, ConflictCheckResult, MergeParams, MergeResult } from '../../../src/types/format' -import type { LanguagePreferenceResult } from '../../../src/types/quotes/languagePreference' -import type { TableSchema, SQLResult } from '../../../src/components/analysis/SQLLab/types' import type { TimeFilter } from '@openchatlab/shared-types' -// @ 互动关系图数据 -interface MentionGraphData { - nodes: Array<{ id: number; name: string; value: number; symbolSize: number }> - links: Array<{ source: string; target: string; value: number }> - maxLinkValue: number -} - // 迁移相关类型 interface MigrationInfo { version: number @@ -64,144 +40,43 @@ interface ImportDiagnostics { } } +/** + * ChatApi — 导入、迁移、Demo(数据查询/分析/成员/SQL 已迁移到 HTTP) + */ interface ChatApi { - selectFile: () => Promise<{ - filePath?: string - format?: string - error?: string - } | null> + selectFile: () => Promise<{ filePath?: string; format?: string; error?: string } | null> detectFormat: (filePath: string) => Promise<{ id: string; name: string; platform: string; multiChat: boolean } | null> - import: (filePath: string) => Promise<{ - success: boolean - sessionId?: string - error?: string - diagnostics?: ImportDiagnostics - }> - importDirectory: (dirPath: string) => Promise<{ - success: boolean - sessionId?: string - error?: string - diagnostics?: ImportDiagnostics - }> + import: (filePath: string) => Promise<{ success: boolean; sessionId?: string; error?: string }> + importDirectory: (dirPath: string) => Promise<{ success: boolean; sessionId?: string; error?: string }> importWithOptions: ( filePath: string, formatOptions: Record - ) => Promise<{ - success: boolean - sessionId?: string - error?: string - diagnostics?: ImportDiagnostics - }> + ) => Promise<{ success: boolean; sessionId?: string; error?: string }> scanMultiChatFile: (filePath: string) => Promise<{ success: boolean chats: Array<{ index: number; name: string; type: string; id: number; messageCount: number }> error?: string }> - getSessions: () => Promise - getSession: (sessionId: string) => Promise - deleteSession: (sessionId: string) => Promise - renameSession: (sessionId: string, newName: string) => Promise - // 迁移相关 checkMigration: () => Promise runMigration: () => Promise<{ success: boolean; error?: string }> - // 会话所有者 - updateSessionOwnerId: (sessionId: string, ownerId: string | null) => Promise - getAvailableYears: (sessionId: string) => Promise - getMemberActivity: (sessionId: string, filter?: TimeFilter) => Promise - getMemberNameHistory: (sessionId: string, memberId: number) => Promise - getHourlyActivity: (sessionId: string, filter?: TimeFilter) => Promise - getDailyActivity: (sessionId: string, filter?: TimeFilter) => Promise - getWeekdayActivity: (sessionId: string, filter?: TimeFilter) => Promise - getMonthlyActivity: (sessionId: string, filter?: TimeFilter) => Promise - getYearlyActivity: (sessionId: string, filter?: TimeFilter) => Promise> - getMessageLengthDistribution: ( - sessionId: string, - filter?: TimeFilter - ) => Promise<{ - detail: Array<{ len: number; count: number }> - grouped: Array<{ range: string; count: number }> - }> - getMessageTypeDistribution: ( - sessionId: string, - filter?: TimeFilter - ) => Promise> - getTimeRange: (sessionId: string) => Promise<{ start: number; end: number } | null> - getDbDirectory: () => Promise getSupportedFormats: () => Promise> onImportProgress: (callback: (progress: ImportProgress) => void) => () => void - getCatchphraseAnalysis: (sessionId: string, filter?: TimeFilter) => Promise - getLanguagePreferenceAnalysis: ( - sessionId: string, - locale: string, - filter?: TimeFilter, - dictType?: string - ) => Promise - getMentionAnalysis: (sessionId: string, filter?: TimeFilter) => Promise - getMentionGraph: (sessionId: string, filter?: TimeFilter) => Promise - getClusterGraph: (sessionId: string, filter?: TimeFilter, options?: ClusterGraphOptions) => Promise - getLaughAnalysis: (sessionId: string, filter?: TimeFilter, keywords?: string[]) => Promise - getRelationshipStats: ( - sessionId: string, - filter?: TimeFilter, - options?: { perseveranceThreshold?: number } - ) => Promise - // 成员管理 - getMembers: (sessionId: string) => Promise - getMembersPaginated: ( - sessionId: string, - params: { page: number; pageSize: number; search?: string; sortOrder?: 'asc' | 'desc' } - ) => Promise<{ - members: MemberWithStats[] - total: number - page: number - pageSize: number - totalPages: number - }> - updateMemberAliases: (sessionId: string, memberId: number, aliases: string[]) => Promise - mergeMembers: (sessionId: string, memberId1: number, memberId2: number) => Promise - deleteMember: (sessionId: string, memberId: number) => Promise - // 插件系统 - pluginQuery: >(sessionId: string, sql: string, params?: any[]) => Promise - pluginCompute: (fnString: string, input: any) => Promise - // SQL 实验室 - getSchema: (sessionId: string) => Promise - executeSQL: (sessionId: string, sql: string) => Promise - // 增量导入 analyzeIncrementalImport: ( sessionId: string, filePath: string - ) => Promise<{ - newMessageCount: number - duplicateCount: number - totalInFile: number - error?: string - diagnosis?: { suggestion?: string } - }> + ) => Promise<{ newMessageCount: number; duplicateCount: number; totalInFile: number; error?: string; diagnosis?: { suggestion?: string } }> incrementalImport: ( sessionId: string, filePath: string - ) => Promise<{ - success: boolean - newMessageCount: number - error?: string - }> - exportSessionsToTempFiles: (sessionIds: string[]) => Promise<{ - success: boolean - tempFiles: string[] - error?: string - }> - cleanupTempExportFiles: (filePaths: string[]) => Promise<{ - success: boolean - error?: string - }> - + ) => Promise<{ success: boolean; newMessageCount: number; error?: string }> + exportSessionsToTempFiles: (sessionIds: string[]) => Promise<{ success: boolean; tempFiles: string[]; error?: string }> + cleanupTempExportFiles: (filePaths: string[]) => Promise<{ success: boolean; error?: string }> importDemo: (locale: string) => Promise<{ success: boolean groupSessionId?: string privateSessionIds?: string[] error?: string }> - onDemoProgress: ( callback: (progress: { stage: string; current: number; total: number; message?: string }) => void ) => () => void @@ -238,22 +113,6 @@ interface MergeApi { clearCache: (filePath?: string) => Promise } -// AI 相关类型 -interface SearchMessageResult { - id: number - senderId: number - senderName: string - senderPlatformId: string - senderAliases: string[] - senderAvatar: string | null - content: string - timestamp: number - type: number - replyToMessageId: string | null - replyToContent: string | null - replyToSenderName: string | null -} - interface FilterMessage { id: number senderName: string @@ -352,52 +211,6 @@ interface AIMessage { } interface AiApi { - searchMessages: ( - sessionId: string, - keywords: string[], - filter?: TimeFilter, - limit?: number, - offset?: number, - senderId?: number - ) => Promise<{ messages: SearchMessageResult[]; total: number }> - getMessageContext: ( - sessionId: string, - messageIds: number | number[], - contextSize?: number - ) => Promise - getRecentMessages: ( - sessionId: string, - filter?: TimeFilter, - limit?: number - ) => Promise<{ messages: SearchMessageResult[]; total: number }> - getAllRecentMessages: ( - sessionId: string, - filter?: TimeFilter, - limit?: number - ) => Promise<{ messages: SearchMessageResult[]; total: number }> - getConversationBetween: ( - sessionId: string, - memberId1: number, - memberId2: number, - filter?: TimeFilter, - limit?: number - ) => Promise<{ messages: SearchMessageResult[]; total: number; member1Name: string; member2Name: string }> - getMessagesBefore: ( - sessionId: string, - beforeId: number, - limit?: number, - filter?: TimeFilter, - senderId?: number, - keywords?: string[] - ) => Promise<{ messages: SearchMessageResult[]; hasMore: boolean }> - getMessagesAfter: ( - sessionId: string, - afterId: number, - limit?: number, - filter?: TimeFilter, - senderId?: number, - keywords?: string[] - ) => Promise<{ messages: SearchMessageResult[]; hasMore: boolean }> createConversation: (sessionId: string, title: string | undefined, assistantId: string) => Promise getConversations: (sessionId: string) => Promise getConversation: (conversationId: string) => Promise @@ -937,79 +750,6 @@ interface NetworkApi { testProxyConnection: (proxyUrl: string) => Promise<{ success: boolean; error?: string }> } -// NLP API 类型 - 自然语言处理功能 -type SupportedLocale = 'zh-CN' | 'en-US' | 'zh-TW' | 'ja-JP' - -/** 词性过滤模式 */ -type PosFilterMode = 'all' | 'meaningful' | 'custom' - -interface WordFrequencyItem { - word: string - count: number - percentage: number -} - -interface PosTagStat { - tag: string - count: number -} - -interface WordFrequencyResult { - words: WordFrequencyItem[] - totalWords: number - totalMessages: number - uniqueWords: number - posTagStats?: PosTagStat[] -} - -type DictType = 'default' | 'zh-CN' | 'zh-TW' - -interface DictInfo { - id: string - label: string - locale: string - downloaded: boolean - fileSize?: number -} - -interface WordFrequencyParams { - sessionId: string - locale: SupportedLocale - timeFilter?: { startTs?: number; endTs?: number } - memberId?: number - topN?: number - minWordLength?: number - minCount?: number - /** 词性过滤模式:all=全部, meaningful=只保留有意义的词, custom=自定义 */ - posFilterMode?: PosFilterMode - /** 自定义词性过滤列表(posFilterMode='custom' 时使用) */ - customPosTags?: string[] - /** 是否启用停用词过滤,默认 true */ - enableStopwords?: boolean - /** 词库类型:default=内置简体中文, zh-TW=繁体中文 */ - dictType?: DictType - /** 要排除的词语列表(词云过滤方案) */ - excludeWords?: string[] -} - -/** 词性标签信息 */ -interface PosTagInfo { - tag: string - name: string - description: string - meaningful: boolean -} - -interface NlpApi { - getWordFrequency: (params: WordFrequencyParams) => Promise - segmentText: (text: string, locale: SupportedLocale, minLength?: number) => Promise - getPosTags: () => Promise - getDictList: () => Promise - isDictDownloaded: (dictId: string) => Promise - downloadDict: (dictId: string) => Promise<{ success: boolean; error?: string }> - deleteDict: (dictId: string) => Promise<{ success: boolean; error?: string }> -} - // ChatLab API 服务类型 interface ApiServerConfig { enabled: boolean @@ -1180,15 +920,6 @@ interface SessionApi { > } -interface PreferencesApi { - get: () => Promise> - save: (partial: Record) => Promise<{ success: boolean; error?: string }> - getUiConfig: () => Promise> - saveUiConfig: (partial: Record) => Promise<{ success: boolean; error?: string }> - getLocale: () => Promise - saveLocale: (lang: string) => Promise<{ success: boolean; error?: string }> -} - declare global { interface Window { electron: ElectronAPI @@ -1203,9 +934,7 @@ declare global { cacheApi: CacheApi networkApi: NetworkApi sessionApi: SessionApi - nlpApi: NlpApi apiServerApi: ApiServerApi - preferencesApi: PreferencesApi internalApi: InternalApi } } @@ -1242,9 +971,7 @@ export { BuiltinSkillInfo, CacheApi, NetworkApi, - NlpApi, ProxyConfig, - SearchMessageResult, AIConversation, AIMessage, LLMProviderInfo, @@ -1267,18 +994,11 @@ export { FilterMessage, ContextBlock, FilterResult, - WordFrequencyItem, - WordFrequencyResult, - WordFrequencyParams, - SupportedLocale, - PosFilterMode, - PosTagInfo, ApiServerApi, ApiServerConfig, ApiServerStatus, DataSource, ImportSession, - PreferencesApi, InternalApi, InternalEndpoint, } diff --git a/apps/desktop/preload/index.ts b/apps/desktop/preload/index.ts index 154aaf51..1a6f5396 100644 --- a/apps/desktop/preload/index.ts +++ b/apps/desktop/preload/index.ts @@ -9,9 +9,8 @@ import { electronAPI } from '@electron-toolkit/preload' import { extendedApi } from './apis/core' import { chatApi, mergeApi } from './apis/chat' import { aiApi, llmApi, agentApi, assistantApi, skillApi } from './apis/ai' -import { nlpApi, networkApi, cacheApi, sessionApi } from './apis/utils' +import { networkApi, cacheApi, sessionApi } from './apis/utils' import { apiServerApi } from './apis/api-server' -import { preferencesApi } from './apis/preferences' import { internalApi } from './apis/internal-api' // 为渲染进程提供统一的类型入口,避免 type-only import 指向无导出的运行时代码。 @@ -46,9 +45,7 @@ if (process.contextIsolated) { contextBridge.exposeInMainWorld('cacheApi', cacheApi) contextBridge.exposeInMainWorld('networkApi', networkApi) contextBridge.exposeInMainWorld('sessionApi', sessionApi) - contextBridge.exposeInMainWorld('nlpApi', nlpApi) contextBridge.exposeInMainWorld('apiServerApi', apiServerApi) - contextBridge.exposeInMainWorld('preferencesApi', preferencesApi) contextBridge.exposeInMainWorld('internalApi', internalApi) } catch (error) { console.error(error) @@ -79,11 +76,7 @@ if (process.contextIsolated) { // @ts-ignore (define in dts) window.sessionApi = sessionApi // @ts-ignore (define in dts) - window.nlpApi = nlpApi - // @ts-ignore (define in dts) window.apiServerApi = apiServerApi // @ts-ignore (define in dts) - window.preferencesApi = preferencesApi - // @ts-ignore (define in dts) window.internalApi = internalApi } diff --git a/src/components/analysis/quotes/SharedTopicsCard.vue b/src/components/analysis/quotes/SharedTopicsCard.vue index 424a45fc..f3724c89 100644 --- a/src/components/analysis/quotes/SharedTopicsCard.vue +++ b/src/components/analysis/quotes/SharedTopicsCard.vue @@ -6,8 +6,16 @@ import { useSettingsStore } from '@/stores/settings' import type { SharedWord } from '@/types/quotes/languagePreference' import type { MemberWithStats } from '@/types/analysis' import { useDataService } from '@/services' +import { post } from '@/services/utils/http' import type { TimeFilter } from '@openchatlab/shared-types' +interface WordFrequencyResult { + words: Array<{ word: string; count: number; percentage: number }> + totalWords: number + totalMessages: number + uniqueWords: number +} + const { t } = useI18n() const settingsStore = useSettingsStore() @@ -70,8 +78,8 @@ async function loadSharedWords() { } const [resultA, resultB] = await Promise.all([ - window.nlpApi.getWordFrequency({ ...baseParams, memberId: topTwo[0].id }), - window.nlpApi.getWordFrequency({ ...baseParams, memberId: topTwo[1].id }), + post('/nlp/word-frequency', { ...baseParams, memberId: topTwo[0].id }), + post('/nlp/word-frequency', { ...baseParams, memberId: topTwo[1].id }), ]) const wordsB = new Map(resultB.words.map((w) => [w.word, w.count])) diff --git a/src/components/analysis/quotes/WordcloudTab.vue b/src/components/analysis/quotes/WordcloudTab.vue index 281793b1..495fbc22 100644 --- a/src/components/analysis/quotes/WordcloudTab.vue +++ b/src/components/analysis/quotes/WordcloudTab.vue @@ -13,6 +13,7 @@ import { useSettingsStore } from '@/stores/settings' import { useLayoutStore } from '@/stores/layout' import { useWordFilterStore } from '@/stores/wordFilter' import { useToast } from '@/composables/useToast' +import { get, post } from '@/services/utils/http' import type { TimeFilter } from '@openchatlab/shared-types' const { t } = useI18n() @@ -28,6 +29,14 @@ interface PosTagInfo { meaningful: boolean } +interface WordFreqResponse { + words: Array<{ word: string; count: number; percentage: number }> + totalWords: number + totalMessages: number + uniqueWords: number + posTagStats?: Array<{ tag: string; count: number }> +} + type PosFilterMode = 'all' | 'meaningful' | 'custom' type DictType = 'default' | 'zh-CN' | 'zh-TW' @@ -145,7 +154,7 @@ const undownloadedDicts = computed(() => { async function refreshDictList() { try { - dictList.value = await window.nlpApi.getDictList() + dictList.value = await get('/nlp/dicts') // 繁体中文用户自动切换到 zh-TW(如已下载) if (isTraditionalChinese.value && selectedDictType.value === 'default') { const zhTW = dictList.value.find((d) => d.id === 'zh-TW') @@ -167,7 +176,7 @@ async function handleDownloadDict(dictId: string) { isDictDownloading.value = true downloadingDictId.value = dictId try { - const result = await window.nlpApi.downloadDict(dictId) + const result = await post<{ success: boolean; error?: string }>(`/nlp/dicts/${dictId}/download`) if (result.success) { await refreshDictList() selectedDictType.value = dictId as DictType @@ -234,7 +243,7 @@ const posTagOptions = computed(() => // 加载词性标签定义 async function loadPosTagDefinitions() { try { - const tags = await window.nlpApi.getPosTags() + const tags = await get('/nlp/pos-tags') posTagDefinitions.value = tags // 初始化自定义词性为有意义的词性 customPosTags.value = tags.filter((t) => t.meaningful).map((t) => t.tag) @@ -247,7 +256,7 @@ async function loadPosTagDefinitions() { async function loadTopicMiniWords() { if (!props.sessionId || !canAnalyzeWithoutDictBlocking.value) return try { - const result = await window.nlpApi.getWordFrequency({ + const result = await post('/nlp/word-frequency', { sessionId: props.sessionId, locale: locale.value, timeFilter: props.timeFilter ? { startTs: props.timeFilter.startTs, endTs: props.timeFilter.endTs } : undefined, @@ -277,7 +286,7 @@ async function loadWordFrequency() { isLoading.value = true try { - const result = await window.nlpApi.getWordFrequency({ + const result = await post('/nlp/word-frequency', { sessionId: props.sessionId, locale: locale.value, timeFilter: props.timeFilter ? { startTs: props.timeFilter.startTs, endTs: props.timeFilter.endTs } : undefined, diff --git a/src/features/charts-cluster/queries.ts b/src/features/charts-cluster/queries.ts index 5c8045c8..761c64ce 100644 --- a/src/features/charts-cluster/queries.ts +++ b/src/features/charts-cluster/queries.ts @@ -1,12 +1,10 @@ /** * chart-cluster 数据查询 * - * 将原 getClusterGraph 后端函数拆解为: - * 1. 两个 SQL 查询获取原始数据(成员 + 消息) - * 2. 一个 compute 在 Worker 中完成全部计算逻辑 - * (时间衰减评分、归一化、混合评分、排序、节点度数计算) + * SQL 获取成员+消息原始数据,compute 完成关系图计算 */ +import { useDataService } from '@/services/data/service' import type { MemberRow, MessageRow, ClusterGraphData, ClusterGraphOptions, BuildClusterInput } from './types' import type { TimeFilter } from '@openchatlab/shared-types' @@ -41,7 +39,7 @@ function buildFilter(filter?: TimeFilter): { conditions: string; params: any[] } * 查询所有成员(含消息数) */ async function queryMembers(sessionId: string): Promise { - return window.chatApi.pluginQuery( + return useDataService().pluginQuery( sessionId, `SELECT id, @@ -59,7 +57,7 @@ async function queryMembers(sessionId: string): Promise { async function queryMessages(sessionId: string, timeFilter?: TimeFilter): Promise { const { conditions, params } = buildFilter(timeFilter) - return window.chatApi.pluginQuery( + return useDataService().pluginQuery( sessionId, `SELECT msg.sender_id as senderId, msg.ts as ts FROM message msg @@ -320,7 +318,7 @@ export async function loadClusterGraph( ...userOptions, } - const result = await window.chatApi.pluginCompute(buildClusterGraph.toString(), { + const result = await useDataService().pluginCompute(buildClusterGraph.toString(), { members, messages, options: mergedOptions, diff --git a/src/features/charts-interaction/queries.ts b/src/features/charts-interaction/queries.ts index 6c709d30..b9d35a5e 100644 --- a/src/features/charts-interaction/queries.ts +++ b/src/features/charts-interaction/queries.ts @@ -1,11 +1,10 @@ /** * chart-interaction 数据查询 * - * 将原 getMentionGraph 后端函数拆解为: - * 1. 三个 SQL 查询获取原始数据 - * 2. 一个 compute 在 Worker 中完成关系图构建 + * 三个 SQL 查询获取原始数据 + compute 完成关系图构建 */ +import { useDataService } from '@/services/data/service' import type { MemberInfo, NameHistory, MentionMessage, MentionGraphData, BuildGraphInput } from './types' import type { TimeFilter } from '@openchatlab/shared-types' @@ -40,7 +39,7 @@ const SYSTEM_FILTER = "AND COALESCE(m.account_name, '') != '系统消息'" async function queryMembers(sessionId: string, timeFilter?: TimeFilter): Promise { const { conditions, params } = buildFilter(timeFilter) - return window.chatApi.pluginQuery( + return useDataService().pluginQuery( sessionId, `SELECT m.id, @@ -59,7 +58,7 @@ async function queryMembers(sessionId: string, timeFilter?: TimeFilter): Promise * 查询所有成员的历史昵称 */ async function queryNameHistory(sessionId: string): Promise { - return window.chatApi.pluginQuery( + return useDataService().pluginQuery( sessionId, `SELECT member_id as memberId, name FROM member_name_history` @@ -72,7 +71,7 @@ async function queryNameHistory(sessionId: string): Promise { async function queryMentionMessages(sessionId: string, timeFilter?: TimeFilter): Promise { const { conditions, params } = buildFilter(timeFilter) - return window.chatApi.pluginQuery( + return useDataService().pluginQuery( sessionId, `SELECT msg.sender_id as senderId, msg.content FROM message msg @@ -204,8 +203,7 @@ export async function loadMentionGraph(sessionId: string, timeFilter?: TimeFilte if (members.length === 0) return emptyResult - // 在 Worker 中构建关系图 - const result = await window.chatApi.pluginCompute(buildMentionGraph.toString(), { + const result = await useDataService().pluginCompute(buildMentionGraph.toString(), { members, nameHistory, messages, diff --git a/src/features/charts-message/queries.ts b/src/features/charts-message/queries.ts index c5e321b2..6a0cd077 100644 --- a/src/features/charts-message/queries.ts +++ b/src/features/charts-message/queries.ts @@ -1,8 +1,8 @@ /** * chart-message SQL 查询 - * 直接通过 window.chatApi.pluginQuery 执行(参数化 + readonly + Worker 线程) */ +import { useDataService } from '@/services/data/service' import type { HourlyActivity, DailyActivity, @@ -45,7 +45,7 @@ const SYSTEM_FILTER = "AND COALESCE(m.account_name, '') != '系统消息'" export async function queryMessageTypes(sessionId: string, timeFilter?: TimeFilter): Promise { const { conditions, params } = buildFilter(timeFilter) - return window.chatApi.pluginQuery( + return useDataService().pluginQuery( sessionId, `SELECT msg.type, COUNT(*) as count FROM message msg @@ -61,7 +61,7 @@ export async function queryMessageTypes(sessionId: string, timeFilter?: TimeFilt export async function queryHourlyActivity(sessionId: string, timeFilter?: TimeFilter): Promise { const { conditions, params } = buildFilter(timeFilter) - return window.chatApi.pluginQuery( + return useDataService().pluginQuery( sessionId, `SELECT CAST(strftime('%H', msg.ts, 'unixepoch', 'localtime') AS INTEGER) as hour, @@ -79,7 +79,7 @@ export async function queryHourlyActivity(sessionId: string, timeFilter?: TimeFi export async function queryDailyActivity(sessionId: string, timeFilter?: TimeFilter): Promise { const { conditions, params } = buildFilter(timeFilter) - return window.chatApi.pluginQuery( + return useDataService().pluginQuery( sessionId, `SELECT strftime('%Y-%m-%d', msg.ts, 'unixepoch', 'localtime') as date, @@ -97,7 +97,7 @@ export async function queryDailyActivity(sessionId: string, timeFilter?: TimeFil export async function queryWeekdayActivity(sessionId: string, timeFilter?: TimeFilter): Promise { const { conditions, params } = buildFilter(timeFilter) - return window.chatApi.pluginQuery( + return useDataService().pluginQuery( sessionId, `SELECT CASE @@ -118,7 +118,7 @@ export async function queryWeekdayActivity(sessionId: string, timeFilter?: TimeF export async function queryMonthlyActivity(sessionId: string, timeFilter?: TimeFilter): Promise { const { conditions, params } = buildFilter(timeFilter) - return window.chatApi.pluginQuery( + return useDataService().pluginQuery( sessionId, `SELECT CAST(strftime('%m', msg.ts, 'unixepoch', 'localtime') AS INTEGER) as month, @@ -136,7 +136,7 @@ export async function queryMonthlyActivity(sessionId: string, timeFilter?: TimeF export async function queryYearlyActivity(sessionId: string, timeFilter?: TimeFilter): Promise { const { conditions, params } = buildFilter(timeFilter) - return window.chatApi.pluginQuery( + return useDataService().pluginQuery( sessionId, `SELECT CAST(strftime('%Y', msg.ts, 'unixepoch', 'localtime') AS INTEGER) as year, @@ -154,7 +154,7 @@ export async function queryYearlyActivity(sessionId: string, timeFilter?: TimeFi export async function queryTextStats(sessionId: string, timeFilter?: TimeFilter): Promise { const { conditions, params } = buildFilter(timeFilter) - const rows = await window.chatApi.pluginQuery( + const rows = await useDataService().pluginQuery( sessionId, `SELECT COUNT(*) as textCount, @@ -179,7 +179,7 @@ export async function queryLongMessageCount( ): Promise { const { conditions, params } = buildFilter(timeFilter) - const rows = await window.chatApi.pluginQuery<{ cnt: number }>( + const rows = await useDataService().pluginQuery<{ cnt: number }>( sessionId, `SELECT COUNT(*) as cnt FROM message msg @@ -199,7 +199,7 @@ export async function queryTypeMonthlyTrend( ): Promise> { const { conditions, params } = buildFilter(timeFilter) - return window.chatApi.pluginQuery<{ month: string; type: number; count: number }>( + return useDataService().pluginQuery<{ month: string; type: number; count: number }>( sessionId, `SELECT strftime('%Y-%m', msg.ts, 'unixepoch', 'localtime') as month, @@ -221,7 +221,7 @@ export async function queryMemberMonthlyTrend( ): Promise> { const { conditions, params } = buildFilter(timeFilter) - return window.chatApi.pluginQuery<{ month: string; memberId: number; memberName: string; count: number }>( + return useDataService().pluginQuery<{ month: string; memberId: number; memberName: string; count: number }>( sessionId, `SELECT strftime('%Y-%m', msg.ts, 'unixepoch', 'localtime') as month, @@ -244,7 +244,7 @@ export async function queryTextLengthPercentiles( ): Promise<{ p25: number; p50: number; p75: number; p90: number }> { const { conditions, params } = buildFilter(timeFilter) - const rows = await window.chatApi.pluginQuery<{ len: number }>( + const rows = await useDataService().pluginQuery<{ len: number }>( sessionId, `SELECT LENGTH(msg.content) as len FROM message msg @@ -275,7 +275,7 @@ export async function queryTextLengthPercentiles( export async function queryLengthDistribution(sessionId: string, timeFilter?: TimeFilter): Promise { const { conditions, params } = buildFilter(timeFilter) - const rows = await window.chatApi.pluginQuery<{ len: number; count: number }>( + const rows = await useDataService().pluginQuery<{ len: number; count: number }>( sessionId, `SELECT LENGTH(msg.content) as len, COUNT(*) as count FROM message msg diff --git a/src/features/charts-ranking/RankingView.vue b/src/features/charts-ranking/RankingView.vue index df78173f..0c5c1a51 100644 --- a/src/features/charts-ranking/RankingView.vue +++ b/src/features/charts-ranking/RankingView.vue @@ -5,6 +5,7 @@ import { computed, ref, watch } from 'vue' import { PageAnchorsNav, TopNSelect } from '@/components/UI' import { usePageAnchors } from '@/composables' +import { useDataService } from '@/services/data/service' import type { MemberActivity } from './types' import { ActivityRank, CheckInRank, MemeBattleRank, RepeatSection, DivingRank, NightOwlRank } from './sections' import type { TimeFilter } from '@openchatlab/shared-types' @@ -22,9 +23,10 @@ const availableYears = ref([]) async function loadBaseData() { if (!props.sessionId) return + const dataService = useDataService() const [members, years] = await Promise.all([ - window.chatApi.getMemberActivity(props.sessionId, props.timeFilter), - window.chatApi.getAvailableYears(props.sessionId), + dataService.getMemberActivity(props.sessionId, props.timeFilter), + dataService.getAvailableYears(props.sessionId), ]) memberActivity.value = members availableYears.value = years diff --git a/src/features/charts-ranking/queries.ts b/src/features/charts-ranking/queries.ts index 3ee22ca2..c8667e1f 100644 --- a/src/features/charts-ranking/queries.ts +++ b/src/features/charts-ranking/queries.ts @@ -1,8 +1,8 @@ /** * chart-ranking SQL 查询与计算 - * 通过 pluginQuery (SQL) + pluginCompute (计算) 在 Worker 线程执行 */ +import { useDataService } from '@/services/data/service' import type { DragonKingAnalysis, DragonKingRankItem, @@ -42,7 +42,7 @@ export async function queryDragonKingAnalysis(sessionId: string, timeFilter?: Ti const { conditions, params } = buildFilter(timeFilter) const [rankRows, totalRow] = await Promise.all([ - window.chatApi.pluginQuery<{ + useDataService().pluginQuery<{ sender_id: number platform_id: string name: string @@ -73,7 +73,7 @@ export async function queryDragonKingAnalysis(sessionId: string, timeFilter?: Ti ORDER BY dragon_days DESC`, params ), - window.chatApi.pluginQuery<{ total: number }>( + useDataService().pluginQuery<{ total: number }>( sessionId, `SELECT COUNT(DISTINCT strftime('%Y-%m-%d', msg.ts, 'unixepoch', 'localtime')) as total FROM message msg @@ -100,7 +100,7 @@ export async function queryDragonKingAnalysis(sessionId: string, timeFilter?: Ti export async function queryDivingAnalysis(sessionId: string, timeFilter?: TimeFilter): Promise { const { conditions, params } = buildFilter(timeFilter) - const rows = await window.chatApi.pluginQuery<{ + const rows = await useDataService().pluginQuery<{ member_id: number platform_id: string name: string @@ -249,7 +249,7 @@ function computeCheckIn(input: { export async function queryCheckInAnalysis(sessionId: string, timeFilter?: TimeFilter): Promise { const { conditions, params } = buildFilter(timeFilter) - const dailyActivity = await window.chatApi.pluginQuery<{ + const dailyActivity = await useDataService().pluginQuery<{ senderId: number name: string day: string @@ -271,7 +271,7 @@ export async function queryCheckInAnalysis(sessionId: string, timeFilter?: TimeF return { streakRank: [], loyaltyRank: [], totalDays: 0 } } - return window.chatApi.pluginCompute(computeCheckIn.toString(), { dailyActivity }) + return useDataService().pluginCompute(computeCheckIn.toString(), { dailyActivity }) } // ==================== 斗图分析 ==================== @@ -403,7 +403,7 @@ function computeMemeBattle(input: { export async function queryMemeBattleAnalysis(sessionId: string, timeFilter?: TimeFilter): Promise { const { conditions, params } = buildFilter(timeFilter) - const messages = await window.chatApi.pluginQuery<{ + const messages = await useDataService().pluginQuery<{ senderId: number type: number ts: number @@ -428,7 +428,7 @@ export async function queryMemeBattleAnalysis(sessionId: string, timeFilter?: Ti return { topBattles: [], rankByCount: [], rankByImageCount: [], totalBattles: 0 } } - return window.chatApi.pluginCompute(computeMemeBattle.toString(), { messages }) + return useDataService().pluginCompute(computeMemeBattle.toString(), { messages }) } // ==================== 夜猫分析 ==================== @@ -710,7 +710,7 @@ function computeNightOwl(input: { export async function queryNightOwlAnalysis(sessionId: string, timeFilter?: TimeFilter): Promise { const { conditions, params } = buildFilter(timeFilter) - const messages = await window.chatApi.pluginQuery<{ + const messages = await useDataService().pluginQuery<{ id: number senderId: number ts: number @@ -742,7 +742,7 @@ export async function queryNightOwlAnalysis(sessionId: string, timeFilter?: Time } } - return window.chatApi.pluginCompute(computeNightOwl.toString(), { messages }) + return useDataService().pluginCompute(computeNightOwl.toString(), { messages }) } // ==================== 复读分析 ==================== @@ -942,7 +942,7 @@ function computeRepeat(input: { export async function queryRepeatAnalysis(sessionId: string, timeFilter?: TimeFilter): Promise { const { conditions, params } = buildFilter(timeFilter) - const messages = await window.chatApi.pluginQuery<{ + const messages = await useDataService().pluginQuery<{ id: number senderId: number content: string @@ -985,5 +985,5 @@ export async function queryRepeatAnalysis(sessionId: string, timeFilter?: TimeFi } } - return window.chatApi.pluginCompute(computeRepeat.toString(), { messages }) + return useDataService().pluginCompute(computeRepeat.toString(), { messages }) } diff --git a/src/services/data/electron.ts b/src/services/data/electron.ts deleted file mode 100644 index 451e4c37..00000000 --- a/src/services/data/electron.ts +++ /dev/null @@ -1,195 +0,0 @@ -/** - * ElectronDataAdapter — 包装 window.chatApi (Electron IPC) - * - * 所有方法直接代理到 window.chatApi 的对应方法。 - */ - -import type { AnalysisSession, MessageType } from '@/types/base' -import type { TimeFilter } from '@openchatlab/shared-types' -import type { - MemberActivity, - MemberWithStats, - MemberNameHistory, - HourlyActivity, - DailyActivity, - WeekdayActivity, - MonthlyActivity, - CatchphraseAnalysis, - MentionAnalysis, - LaughAnalysis, - ClusterGraphData, - ClusterGraphOptions, - RelationshipStats, -} from '@/types/analysis' -import type { LanguagePreferenceResult } from '@/types/quotes/languagePreference' -import type { - DataAdapter, - PaginationParams, - PaginatedResult, - SQLResult, - TableSchema, - MentionGraphData, - MessageLengthDistribution, -} from './types' - -export class ElectronDataAdapter implements DataAdapter { - // ==================== 会话管理 ==================== - - getSessions(): Promise { - return window.chatApi.getSessions() - } - - getSession(sessionId: string): Promise { - return window.chatApi.getSession(sessionId) - } - - deleteSession(sessionId: string): Promise { - return window.chatApi.deleteSession(sessionId) - } - - renameSession(sessionId: string, newName: string): Promise { - return window.chatApi.renameSession(sessionId, newName) - } - - updateSessionOwnerId(sessionId: string, ownerId: string | null): Promise { - return window.chatApi.updateSessionOwnerId(sessionId, ownerId) - } - - // ==================== 时间范围 ==================== - - getAvailableYears(sessionId: string): Promise { - return window.chatApi.getAvailableYears(sessionId) - } - - getTimeRange(sessionId: string): Promise<{ start: number; end: number } | null> { - return window.chatApi.getTimeRange(sessionId) - } - - // ==================== 统计分析 ==================== - - getMemberActivity(sessionId: string, filter?: TimeFilter): Promise { - return window.chatApi.getMemberActivity(sessionId, filter) - } - - getHourlyActivity(sessionId: string, filter?: TimeFilter): Promise { - return window.chatApi.getHourlyActivity(sessionId, filter) - } - - getDailyActivity(sessionId: string, filter?: TimeFilter): Promise { - return window.chatApi.getDailyActivity(sessionId, filter) - } - - getWeekdayActivity(sessionId: string, filter?: TimeFilter): Promise { - return window.chatApi.getWeekdayActivity(sessionId, filter) - } - - getMonthlyActivity(sessionId: string, filter?: TimeFilter): Promise { - return window.chatApi.getMonthlyActivity(sessionId, filter) - } - - getYearlyActivity(sessionId: string, filter?: TimeFilter): Promise> { - return window.chatApi.getYearlyActivity(sessionId, filter) - } - - getMessageLengthDistribution(sessionId: string, filter?: TimeFilter): Promise { - return window.chatApi.getMessageLengthDistribution(sessionId, filter) - } - - getMessageTypeDistribution( - sessionId: string, - filter?: TimeFilter - ): Promise> { - return window.chatApi.getMessageTypeDistribution(sessionId, filter) - } - - // ==================== 成员管理 ==================== - - getMembers(sessionId: string): Promise { - return window.chatApi.getMembers(sessionId) - } - - getMembersPaginated(sessionId: string, params: PaginationParams): Promise> { - return window.chatApi.getMembersPaginated(sessionId, params).then((result) => ({ - items: result.members, - total: result.total, - page: result.page, - pageSize: result.pageSize, - totalPages: result.totalPages, - })) - } - - getMemberNameHistory(sessionId: string, memberId: number): Promise { - return window.chatApi.getMemberNameHistory(sessionId, memberId) - } - - updateMemberAliases(sessionId: string, memberId: number, aliases: string[]): Promise { - return window.chatApi.updateMemberAliases(sessionId, memberId, aliases) - } - - mergeMembers(sessionId: string, memberId1: number, memberId2: number): Promise { - return window.chatApi.mergeMembers(sessionId, memberId1, memberId2) - } - - deleteMember(sessionId: string, memberId: number): Promise { - return window.chatApi.deleteMember(sessionId, memberId) - } - - // ==================== 社交分析 ==================== - - getCatchphraseAnalysis(sessionId: string, filter?: TimeFilter): Promise { - return window.chatApi.getCatchphraseAnalysis(sessionId, filter) - } - - getLanguagePreferenceAnalysis( - sessionId: string, - locale: string, - filter?: TimeFilter, - dictType?: string - ): Promise { - return window.chatApi.getLanguagePreferenceAnalysis(sessionId, locale, filter, dictType) - } - - getMentionAnalysis(sessionId: string, filter?: TimeFilter): Promise { - return window.chatApi.getMentionAnalysis(sessionId, filter) - } - - getMentionGraph(sessionId: string, filter?: TimeFilter): Promise { - return window.chatApi.getMentionGraph(sessionId, filter) - } - - getClusterGraph(sessionId: string, filter?: TimeFilter, options?: ClusterGraphOptions): Promise { - return window.chatApi.getClusterGraph(sessionId, filter, options) - } - - getLaughAnalysis(sessionId: string, filter?: TimeFilter, keywords?: string[]): Promise { - return window.chatApi.getLaughAnalysis(sessionId, filter, keywords) - } - - getRelationshipStats( - sessionId: string, - filter?: TimeFilter, - options?: { perseveranceThreshold?: number } - ): Promise { - return window.chatApi.getRelationshipStats(sessionId, filter, options) - } - - // ==================== SQL Lab ==================== - - executeSQL(sessionId: string, sql: string): Promise { - return window.chatApi.executeSQL(sessionId, sql) - } - - getSchema(sessionId: string): Promise { - return window.chatApi.getSchema(sessionId) - } - - // ==================== 插件系统 ==================== - - pluginQuery>(sessionId: string, sql: string, params?: unknown[]): Promise { - return window.chatApi.pluginQuery(sessionId, sql, params) - } - - pluginCompute(fnString: string, input: unknown): Promise { - return window.chatApi.pluginCompute(fnString, input) - } -} diff --git a/src/services/message/electron.ts b/src/services/message/electron.ts deleted file mode 100644 index ce161cbc..00000000 --- a/src/services/message/electron.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * ElectronMessageAdapter — wrap window.aiApi 消息查询方法 - */ - -import type { MessageAdapter, TimeFilter, PaginatedMessages, MessageRecord, SearchResult } from './types' - -export class ElectronMessageAdapter implements MessageAdapter { - async getMessagesBefore( - sessionId: string, - beforeId: number, - limit: number, - filter?: TimeFilter, - senderId?: number, - keywords?: string[] - ): Promise { - return window.aiApi.getMessagesBefore(sessionId, beforeId, limit, filter, senderId, keywords) - } - - async getMessagesAfter( - sessionId: string, - afterId: number, - limit: number, - filter?: TimeFilter, - senderId?: number, - keywords?: string[] - ): Promise { - return window.aiApi.getMessagesAfter(sessionId, afterId, limit, filter, senderId, keywords) - } - - async getMessageContext( - sessionId: string, - messageIds: number | number[], - contextSize?: number - ): Promise { - return window.aiApi.getMessageContext(sessionId, messageIds, contextSize) - } - - async searchMessages( - sessionId: string, - keywords: string[], - filter?: TimeFilter, - limit?: number, - offset?: number, - senderId?: number - ): Promise { - return window.aiApi.searchMessages(sessionId, keywords, filter, limit, offset, senderId) - } - - async getAllRecentMessages(sessionId: string, filter?: TimeFilter, limit?: number): Promise { - return window.aiApi.getAllRecentMessages(sessionId, filter, limit) - } -} diff --git a/src/services/preferences/electron.ts b/src/services/preferences/electron.ts deleted file mode 100644 index ae1d9149..00000000 --- a/src/services/preferences/electron.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { PreferencesAdapter, Preferences, UiConfig } from './types' - -export class ElectronPreferencesAdapter implements PreferencesAdapter { - async getPreferences(): Promise { - return (await window.preferencesApi.get()) as unknown as Preferences - } - - savePreferences(partial: Partial): Promise<{ success: boolean; error?: string }> { - return window.preferencesApi.save(partial as Record) - } - - async getUiConfig(): Promise { - return (await window.preferencesApi.getUiConfig()) as unknown as UiConfig - } - - saveUiConfig(partial: Partial): Promise<{ success: boolean; error?: string }> { - return window.preferencesApi.saveUiConfig(partial as Record) - } - - getLocale(): Promise { - return window.preferencesApi.getLocale() - } - - saveLocale(lang: string): Promise<{ success: boolean; error?: string }> { - return window.preferencesApi.saveLocale(lang) - } -} diff --git a/src/services/registry.ts b/src/services/registry.ts index 78ff2a8d..63731944 100644 --- a/src/services/registry.ts +++ b/src/services/registry.ts @@ -61,54 +61,26 @@ export async function initServices(): Promise { } /** - * Check if the Internal HTTP Server is available. - * When available, business adapters use Fetch instead of IPC. + * Electron adapters: Internal HTTP Server is a hard dependency. + * data/message/preferences use Fetch; import/session-index/ai stay on IPC. */ -function isInternalHttpAvailable(): boolean { - try { - return typeof window !== 'undefined' && typeof (window as any).internalApi?.getEndpoint === 'function' - } catch { - return false - } -} - async function initElectronAdapters(): Promise { - // Check if Internal API Server endpoint was configured in App.vue - const { getBaseUrl } = await import('./utils/http') - const useHttp = isInternalHttpAvailable() && getBaseUrl().startsWith('http') - - if (useHttp) { - await initElectronHttpAdapters() - } else { - await initElectronIpcAdapters() - } -} - -/** - * HTTP mode: business adapters use Fetch (via Internal API Server). - * Only platform + import + ai remain on IPC (routes not yet available). - */ -async function initElectronHttpAdapters(): Promise { const { FetchDataAdapter } = await import('./data/fetch') registerAdapter('data', new FetchDataAdapter()) - // Import stays on IPC: routes depend on CLI-specific modules const { ElectronImportAdapter } = await import('./import/electron') registerAdapter('import', new ElectronImportAdapter()) - // Session-index stays on IPC: summaries routes (generateSummary, checkCanGenerateSummary) - // depend on LLM config and are not registered in Internal Server's registerSharedRoutes() + // Session-index stays on IPC: summaries depend on LLM config const { ElectronSessionIndexAdapter } = await import('./session-index/electron') registerAdapter('session-index', new ElectronSessionIndexAdapter()) const { FetchMessageAdapter } = await import('./message/fetch') registerAdapter('message', new FetchMessageAdapter()) - // Platform always uses IPC (native capabilities) const { ElectronPlatformAdapter } = await import('./platform/electron') registerAdapter('platform', new ElectronPlatformAdapter()) - // AI stays on IPC: routes depend on CLI-specific modules const { ElectronAIAdapter } = await import('./ai/electron') registerAdapter('ai', new ElectronAIAdapter()) @@ -116,32 +88,6 @@ async function initElectronHttpAdapters(): Promise { registerAdapter('preferences', new FetchPreferencesAdapter()) } -/** - * IPC fallback mode: all adapters use Electron IPC (original behavior). - */ -async function initElectronIpcAdapters(): Promise { - const { ElectronDataAdapter } = await import('./data/electron') - registerAdapter('data', new ElectronDataAdapter()) - - const { ElectronImportAdapter } = await import('./import/electron') - registerAdapter('import', new ElectronImportAdapter()) - - const { ElectronSessionIndexAdapter } = await import('./session-index/electron') - registerAdapter('session-index', new ElectronSessionIndexAdapter()) - - const { ElectronMessageAdapter } = await import('./message/electron') - registerAdapter('message', new ElectronMessageAdapter()) - - const { ElectronPlatformAdapter } = await import('./platform/electron') - registerAdapter('platform', new ElectronPlatformAdapter()) - - const { ElectronAIAdapter } = await import('./ai/electron') - registerAdapter('ai', new ElectronAIAdapter()) - - const { ElectronPreferencesAdapter } = await import('./preferences/electron') - registerAdapter('preferences', new ElectronPreferencesAdapter()) -} - async function initWebServeAdapters(): Promise { const { FetchDataAdapter } = await import('./data/fetch') registerAdapter('data', new FetchDataAdapter()) @@ -170,10 +116,9 @@ async function initWebServeAdapters(): Promise { } /** - * chart-* modules (src/features/charts-ranking, charts-message, etc.) directly - * call window.chatApi.pluginQuery / pluginCompute / getMemberActivity / - * getAvailableYears. In Electron, these are injected by the preload script. - * In web-serve mode, we install equivalent shims backed by DataService. + * Web-serve shims: chart modules and other code that may still reference + * window.chatApi.pluginQuery / pluginCompute / getMemberActivity / getAvailableYears. + * In Electron these are no longer used (charts call useDataService() directly). */ async function installChartPluginShims(): Promise { const { useDataService } = await import('./data/service')