refactor: 重构部分架构

This commit is contained in:
digua
2025-12-05 01:11:37 +08:00
parent 04dc2a79c1
commit 4fb9255605
18 changed files with 1903 additions and 1503 deletions

View File

@@ -1,28 +0,0 @@
/**
* 数据库服务模块
* 统一导出入口
*/
// 核心功能
export { importData, getAllSessions, getSession, deleteSession, getDbDirectory, openDatabase, getDbPath } from './core'
// 分析查询
export {
getAvailableYears,
getMemberActivity,
getHourlyActivity,
getDailyActivity,
getWeekdayActivity,
getMessageTypeDistribution,
getTimeRange,
getMemberNameHistory,
getRepeatAnalysis,
getCatchphraseAnalysis,
getNightOwlAnalysis,
getDragonKingAnalysis,
getDivingAnalysis,
getMonologueAnalysis,
} from './analysis'
// 类型导出
export type { TimeFilter } from './analysis'

519
electron/main/ipc/ai.ts Normal file
View File

@@ -0,0 +1,519 @@
// electron/main/ipc/ai.ts
import { ipcMain, BrowserWindow } from 'electron'
import * as aiConversations from '../ai/conversations'
import * as llm from '../ai/llm'
import { aiLogger } from '../ai/logger'
import { Agent, type AgentStreamChunk } from '../ai/agent'
import type { ToolContext } from '../ai/tools/types'
import type { IpcContext } from './types'
// ==================== AI Agent 请求追踪 ====================
// 用于跟踪活跃的 Agent 请求,支持中止操作
const activeAgentRequests = new Map<string, AbortController>()
export function registerAIHandlers({ win }: IpcContext): void {
console.log('[IPC] Registering AI handlers...')
// ==================== AI 对话管理 ====================
/**
* 创建新的 AI 对话
*/
ipcMain.handle(
'ai:createConversation',
async (
_,
title: string,
sessionId?: string,
dataSource?: { type: 'chat' | 'member'; id: string; name?: string }
) => {
try {
return aiConversations.createConversation(title, sessionId, dataSource)
} catch (error) {
console.error('创建 AI 对话失败:', error)
throw error
}
}
)
/**
* 获取所有 AI 对话列表
*/
ipcMain.handle('ai:getConversations', async (_, sessionId?: string) => {
try {
return aiConversations.getConversations(sessionId)
} catch (error) {
console.error('获取 AI 对话列表失败:', error)
return []
}
})
/**
* 获取单个对话详情
*/
ipcMain.handle('ai:getConversation', async (_, conversationId: string) => {
try {
return aiConversations.getConversation(conversationId)
} catch (error) {
console.error('获取 AI 对话详情失败:', error)
return null
}
})
/**
* 更新 AI 对话标题
*/
ipcMain.handle('ai:updateConversationTitle', async (_, conversationId: string, title: string) => {
try {
return aiConversations.updateConversationTitle(conversationId, title)
} catch (error) {
console.error('更新 AI 对话标题失败:', error)
return false
}
})
/**
* 删除 AI 对话
*/
ipcMain.handle('ai:deleteConversation', async (_, conversationId: string) => {
try {
return aiConversations.deleteConversation(conversationId)
} catch (error) {
console.error('删除 AI 对话失败:', error)
return false
}
})
/**
* 添加 AI 消息
*/
ipcMain.handle(
'ai:addMessage',
async (
_,
conversationId: string,
role: 'user' | 'assistant',
content: string,
dataKeywords?: string[],
dataMessageCount?: number
) => {
try {
return aiConversations.addMessage(conversationId, role, content, dataKeywords, dataMessageCount)
} catch (error) {
console.error('添加 AI 消息失败:', error)
throw error
}
}
)
/**
* 获取 AI 对话的所有消息
*/
ipcMain.handle('ai:getMessages', async (_, conversationId: string) => {
try {
return aiConversations.getMessages(conversationId)
} catch (error) {
console.error('获取 AI 消息失败:', error)
return []
}
})
/**
* 删除 AI 消息
*/
ipcMain.handle('ai:deleteMessage', async (_, messageId: string) => {
try {
return aiConversations.deleteMessage(messageId)
} catch (error) {
console.error('删除 AI 消息失败:', error)
return false
}
})
// ==================== LLM 服务(多配置管理)====================
/**
* 获取所有支持的 LLM 提供商
*/
ipcMain.handle('llm:getProviders', async () => {
return llm.PROVIDERS
})
/**
* 获取所有配置列表
*/
ipcMain.handle('llm:getAllConfigs', async () => {
const configs = llm.getAllConfigs()
// 脱敏 API Key
return configs.map((c) => ({
...c,
apiKey: c.apiKey ? `${c.apiKey.slice(0, 4)}****${c.apiKey.slice(-4)}` : '',
apiKeySet: !!c.apiKey,
}))
})
/**
* 获取当前激活的配置 ID
*/
ipcMain.handle('llm:getActiveConfigId', async () => {
const config = llm.getActiveConfig()
return config?.id || null
})
/**
* 添加新配置
*/
ipcMain.handle(
'llm:addConfig',
async (
_,
config: {
name: string
provider: llm.LLMProvider
apiKey: string
model?: string
baseUrl?: string
maxTokens?: number
}
) => {
try {
const result = llm.addConfig(config)
if (result.success && result.config) {
return {
success: true,
config: {
...result.config,
apiKey: result.config.apiKey
? `${result.config.apiKey.slice(0, 4)}****${result.config.apiKey.slice(-4)}`
: '',
apiKeySet: !!result.config.apiKey,
},
}
}
return result
} catch (error) {
console.error('添加 LLM 配置失败:', error)
return { success: false, error: String(error) }
}
}
)
/**
* 更新配置
*/
ipcMain.handle(
'llm:updateConfig',
async (
_,
id: string,
updates: {
name?: string
provider?: llm.LLMProvider
apiKey?: string
model?: string
baseUrl?: string
maxTokens?: number
}
) => {
try {
// 如果 apiKey 为空字符串,表示不更新 API Key
const cleanUpdates = { ...updates }
if (cleanUpdates.apiKey === '') {
delete cleanUpdates.apiKey
}
return llm.updateConfig(id, cleanUpdates)
} catch (error) {
console.error('更新 LLM 配置失败:', error)
return { success: false, error: String(error) }
}
}
)
/**
* 删除配置
*/
ipcMain.handle('llm:deleteConfig', async (_, id?: string) => {
try {
// 兼容旧 API如果没有传 id删除当前激活的配置
if (!id) {
const activeConfig = llm.getActiveConfig()
if (activeConfig) {
return llm.deleteConfig(activeConfig.id)
}
return { success: false, error: '没有激活的配置' }
}
return llm.deleteConfig(id)
} catch (error) {
console.error('删除 LLM 配置失败:', error)
return { success: false, error: String(error) }
}
})
/**
* 设置激活的配置
*/
ipcMain.handle('llm:setActiveConfig', async (_, id: string) => {
try {
return llm.setActiveConfig(id)
} catch (error) {
console.error('设置激活配置失败:', error)
return { success: false, error: String(error) }
}
})
/**
* 验证 API Key支持自定义 baseUrl 和 model
*/
ipcMain.handle(
'llm:validateApiKey',
async (_, provider: llm.LLMProvider, apiKey: string, baseUrl?: string, model?: string) => {
console.log('[LLM:validateApiKey] 开始验证:', { provider, baseUrl, model, apiKeyLength: apiKey?.length })
try {
const service = llm.createLLMService({ provider, apiKey, baseUrl, model })
const result = await service.validateApiKey()
console.log('[LLM:validateApiKey] 验证结果:', result)
return result
} catch (error) {
console.error('[LLM:validateApiKey] 验证失败:', error)
return false
}
}
)
/**
* 检查是否已配置 LLM是否有激活的配置
*/
ipcMain.handle('llm:hasConfig', async () => {
return llm.hasActiveConfig()
})
// ==================== 兼容旧 APIdeprecated====================
/**
* @deprecated 使用 llm:getAllConfigs 代替
* 获取当前 LLM 配置
*/
ipcMain.handle('llm:getConfig', async () => {
const config = llm.getActiveConfig()
if (!config) return null
return {
provider: config.provider,
apiKey: config.apiKey ? `${config.apiKey.slice(0, 8)}...${config.apiKey.slice(-4)}` : '',
apiKeySet: !!config.apiKey,
model: config.model,
baseUrl: config.baseUrl,
maxTokens: config.maxTokens,
}
})
/**
* @deprecated 使用 llm:addConfig 或 llm:updateConfig 代替
* 保存 LLM 配置
*/
ipcMain.handle(
'llm:saveConfig',
async (
_,
config: { provider: llm.LLMProvider; apiKey: string; model?: string; baseUrl?: string; maxTokens?: number }
) => {
try {
const activeConfig = llm.getActiveConfig()
if (activeConfig) {
// 更新现有配置
const updates: Record<string, unknown> = { ...config }
if (!config.apiKey || config.apiKey.trim() === '') {
delete updates.apiKey
}
return llm.updateConfig(activeConfig.id, updates)
} else {
// 创建新配置
if (!config.apiKey || config.apiKey.trim() === '') {
return { success: false, error: '请输入 API Key' }
}
const providerInfo = llm.getProviderInfo(config.provider)
return llm.addConfig({
name: providerInfo?.name || config.provider,
...config,
})
}
} catch (error) {
console.error('保存 LLM 配置失败:', error)
return { success: false, error: String(error) }
}
}
)
/**
* 发送 LLM 聊天请求(非流式)
*/
ipcMain.handle('llm:chat', async (_, messages: llm.ChatMessage[], options?: llm.ChatOptions) => {
aiLogger.info('IPC', '收到非流式 LLM 请求', {
messagesCount: messages.length,
firstMsgRole: messages[0]?.role,
firstMsgContentLen: messages[0]?.content?.length,
options,
})
try {
const response = await llm.chat(messages, options)
aiLogger.info('IPC', '非流式 LLM 请求成功', { responseLength: response.length })
return { success: true, content: response }
} catch (error) {
aiLogger.error('IPC', '非流式 LLM 请求失败', { error: String(error) })
console.error('LLM 聊天失败:', error)
return { success: false, error: String(error) }
}
})
/**
* 发送 LLM 聊天请求(流式)
* 使用 IPC 事件发送流式数据
*/
ipcMain.handle(
'llm:chatStream',
async (_, requestId: string, messages: llm.ChatMessage[], options?: llm.ChatOptions) => {
aiLogger.info('IPC', `收到流式聊天请求: ${requestId}`, {
messagesCount: messages.length,
options,
})
try {
const generator = llm.chatStream(messages, options)
aiLogger.info('IPC', `创建流式生成器: ${requestId}`)
// 异步处理流式响应
;(async () => {
let chunkIndex = 0
try {
aiLogger.info('IPC', `开始迭代流式响应: ${requestId}`)
for await (const chunk of generator) {
chunkIndex++
aiLogger.debug('IPC', `发送 chunk #${chunkIndex}: ${requestId}`, {
contentLength: chunk.content?.length,
isFinished: chunk.isFinished,
finishReason: chunk.finishReason,
})
win.webContents.send('llm:streamChunk', { requestId, chunk })
}
aiLogger.info('IPC', `流式响应完成: ${requestId}`, { totalChunks: chunkIndex })
} catch (error) {
aiLogger.error('IPC', `流式响应出错: ${requestId}`, {
error: String(error),
chunkIndex,
})
win.webContents.send('llm:streamChunk', {
requestId,
chunk: { content: '', isFinished: true, finishReason: 'error' },
error: String(error),
})
}
})()
return { success: true }
} catch (error) {
aiLogger.error('IPC', `创建流式请求失败: ${requestId}`, { error: String(error) })
console.error('LLM 流式聊天失败:', error)
return { success: false, error: String(error) }
}
}
)
// ==================== AI Agent API ====================
/**
* 执行 Agent 对话(流式)
* Agent 会自动调用工具并返回最终结果
*/
ipcMain.handle('agent:runStream', async (_, requestId: string, userMessage: string, context: ToolContext) => {
aiLogger.info('IPC', `收到 Agent 流式请求: ${requestId}`, {
userMessage: userMessage.slice(0, 100),
sessionId: context.sessionId,
})
try {
// 创建 AbortController 并存储
const abortController = new AbortController()
activeAgentRequests.set(requestId, abortController)
const agent = new Agent(context, { abortSignal: abortController.signal })
// 异步执行,通过事件发送流式数据
;(async () => {
try {
const result = await agent.executeStream(userMessage, (chunk: AgentStreamChunk) => {
// 如果已中止,不再发送
if (abortController.signal.aborted) {
return
}
aiLogger.debug('IPC', `Agent chunk: ${requestId}`, {
type: chunk.type,
contentLength: chunk.content?.length,
toolName: chunk.toolName,
})
win.webContents.send('agent:streamChunk', { requestId, chunk })
})
// 如果已中止,不发送完成信息
if (abortController.signal.aborted) {
aiLogger.info('IPC', `Agent 已中止,跳过完成信息: ${requestId}`)
return
}
// 发送完成信息
win.webContents.send('agent:complete', {
requestId,
result: {
content: result.content,
toolsUsed: result.toolsUsed,
toolRounds: result.toolRounds,
},
})
aiLogger.info('IPC', `Agent 执行完成: ${requestId}`, {
toolsUsed: result.toolsUsed,
toolRounds: result.toolRounds,
contentLength: result.content.length,
})
} catch (error) {
// 如果是中止错误,不报告为错误
if (error instanceof Error && error.name === 'AbortError') {
aiLogger.info('IPC', `Agent 请求已中止: ${requestId}`)
return
}
aiLogger.error('IPC', `Agent 执行出错: ${requestId}`, { error: String(error) })
win.webContents.send('agent:streamChunk', {
requestId,
chunk: { type: 'error', error: String(error), isFinished: true },
})
} finally {
// 清理请求追踪
activeAgentRequests.delete(requestId)
}
})()
return { success: true }
} catch (error) {
aiLogger.error('IPC', `创建 Agent 请求失败: ${requestId}`, { error: String(error) })
return { success: false, error: String(error) }
}
})
/**
* 中止 Agent 请求
*/
ipcMain.handle('agent:abort', async (_, requestId: string) => {
aiLogger.info('IPC', `收到中止请求: ${requestId}`)
const abortController = activeAgentRequests.get(requestId)
if (abortController) {
abortController.abort()
activeAgentRequests.delete(requestId)
aiLogger.info('IPC', `已中止 Agent 请求: ${requestId}`)
return { success: true }
} else {
aiLogger.warn('IPC', `未找到 Agent 请求: ${requestId}`)
return { success: false, error: '未找到该请求' }
}
})
}

213
electron/main/ipc/cache.ts Normal file
View File

@@ -0,0 +1,213 @@
// electron/main/ipc/cache.ts
import { ipcMain, app, shell } from 'electron'
import * as fs from 'fs/promises'
import * as fsSync from 'fs'
import * as path from 'path'
import type { IpcContext } from './types'
/**
* 获取 ChatLab 数据目录
*/
function getChatLabDir(): string {
try {
const docPath = app.getPath('documents')
return path.join(docPath, 'ChatLab')
} catch {
return path.join(process.cwd(), 'ChatLab')
}
}
/**
* 递归计算目录大小
*/
async function getDirSize(dirPath: string): Promise<number> {
let totalSize = 0
try {
const exists = fsSync.existsSync(dirPath)
if (!exists) return 0
const files = await fs.readdir(dirPath, { withFileTypes: true })
for (const file of files) {
const filePath = path.join(dirPath, file.name)
if (file.isDirectory()) {
totalSize += await getDirSize(filePath)
} else {
const stat = await fs.stat(filePath)
totalSize += stat.size
}
}
} catch (error) {
console.error('[Cache] Error getting dir size:', dirPath, error)
}
return totalSize
}
/**
* 获取目录中的文件数量
*/
async function getFileCount(dirPath: string): Promise<number> {
let count = 0
try {
const exists = fsSync.existsSync(dirPath)
if (!exists) return 0
const files = await fs.readdir(dirPath, { withFileTypes: true })
for (const file of files) {
const filePath = path.join(dirPath, file.name)
if (file.isDirectory()) {
count += await getFileCount(filePath)
} else {
count++
}
}
} catch (error) {
console.error('[Cache] Error getting file count:', dirPath, error)
}
return count
}
export function registerCacheHandlers(_context: IpcContext): void {
console.log('[IPC] Registering cache handlers...')
/**
* 获取所有缓存目录信息
*/
ipcMain.handle('cache:getInfo', async () => {
const chatLabDir = getChatLabDir()
// 定义缓存目录
const cacheDirectories = [
{
id: 'databases',
name: '聊天记录数据库',
description: '导入的聊天记录分析数据',
path: path.join(chatLabDir, 'databases'),
icon: 'i-heroicons-circle-stack',
canClear: false, // 不允许一键清理,因为是重要数据
},
{
id: 'ai',
name: 'AI 数据',
description: 'AI 对话历史和配置文件',
path: path.join(chatLabDir, 'ai'),
icon: 'i-heroicons-sparkles',
canClear: false, // 不允许一键清理
},
{
id: 'temp',
name: '临时文件',
description: '合并功能产生的临时数据库',
path: path.join(chatLabDir, 'temp'),
icon: 'i-heroicons-clock',
canClear: true, // 可以清理
},
{
id: 'logs',
name: '日志文件',
description: 'AI 调试日志',
path: path.join(chatLabDir, 'logs'),
icon: 'i-heroicons-document-text',
canClear: true, // 可以清理
},
]
// 获取每个目录的信息
const results = await Promise.all(
cacheDirectories.map(async (dir) => {
const size = await getDirSize(dir.path)
const fileCount = await getFileCount(dir.path)
const exists = fsSync.existsSync(dir.path)
return {
...dir,
size,
fileCount,
exists,
}
})
)
return {
baseDir: chatLabDir,
directories: results,
totalSize: results.reduce((sum, dir) => sum + dir.size, 0),
}
})
/**
* 清理指定缓存目录
*/
ipcMain.handle('cache:clear', async (_, cacheId: string) => {
const chatLabDir = getChatLabDir()
// 只允许清理 temp 和 logs
const allowedDirs: Record<string, string> = {
temp: path.join(chatLabDir, 'temp'),
logs: path.join(chatLabDir, 'logs'),
}
const dirPath = allowedDirs[cacheId]
if (!dirPath) {
return { success: false, error: '不允许清理此目录' }
}
try {
const exists = fsSync.existsSync(dirPath)
if (!exists) {
return { success: true, message: '目录不存在,无需清理' }
}
// 删除目录下的所有文件
const files = await fs.readdir(dirPath)
for (const file of files) {
const filePath = path.join(dirPath, file)
const stat = await fs.stat(filePath)
if (stat.isDirectory()) {
await fs.rm(filePath, { recursive: true })
} else {
await fs.unlink(filePath)
}
}
console.log(`[Cache] Cleared directory: ${dirPath}`)
return { success: true }
} catch (error) {
console.error('[Cache] Error clearing cache:', error)
return { success: false, error: String(error) }
}
})
/**
* 在文件管理器中打开缓存目录
*/
ipcMain.handle('cache:openDir', async (_, cacheId: string) => {
const chatLabDir = getChatLabDir()
const dirPaths: Record<string, string> = {
base: chatLabDir,
databases: path.join(chatLabDir, 'databases'),
ai: path.join(chatLabDir, 'ai'),
temp: path.join(chatLabDir, 'temp'),
logs: path.join(chatLabDir, 'logs'),
}
const dirPath = dirPaths[cacheId]
if (!dirPath) {
return { success: false, error: '未知的目录' }
}
try {
// 确保目录存在
if (!fsSync.existsSync(dirPath)) {
await fs.mkdir(dirPath, { recursive: true })
}
await shell.openPath(dirPath)
return { success: true }
} catch (error) {
console.error('[Cache] Error opening directory:', error)
return { success: false, error: String(error) }
}
})
}

523
electron/main/ipc/chat.ts Normal file
View File

@@ -0,0 +1,523 @@
/**
* 聊天记录导入与分析 IPC 处理器
*/
import { ipcMain, app, dialog } from 'electron'
import * as databaseCore from '../database/core'
import * as worker from '../worker/workerManager'
import * as parser from '../parser'
import { detectFormat, type ParseProgress } from '../parser'
import type { IpcContext } from './types'
/**
* 注册聊天记录相关 IPC 处理器
*/
export function registerChatHandlers(ctx: IpcContext): void {
const { win } = ctx
// ==================== 聊天记录导入与分析 ====================
/**
* 选择聊天记录文件
*/
ipcMain.handle('chat:selectFile', async () => {
try {
const { canceled, filePaths } = await dialog.showOpenDialog({
title: '选择聊天记录文件',
defaultPath: app.getPath('documents'),
properties: ['openFile'],
filters: [
{ name: '聊天记录', extensions: ['json', 'txt'] },
{ name: '所有文件', extensions: ['*'] },
],
buttonLabel: '导入',
})
if (canceled || filePaths.length === 0) {
return null
}
const filePath = filePaths[0]
console.log('[IpcMain] File selected:', filePath)
// 检测文件格式(使用流式检测,只读取文件开头)
const formatFeature = detectFormat(filePath)
const format = formatFeature?.name || null
console.log('[IpcMain] Detected format:', format)
if (!format) {
return { error: '无法识别的文件格式' }
}
return { filePath, format }
} catch (error) {
console.error('[IpcMain] Error selecting file:', error)
return { error: String(error) }
}
})
/**
* 导入聊天记录(流式版本)
*/
ipcMain.handle('chat:import', async (_, filePath: string) => {
console.log('[IpcMain] chat:import called with:', filePath)
try {
// 发送进度:开始检测格式
win.webContents.send('chat:importProgress', {
stage: 'detecting',
progress: 5,
message: '正在检测文件格式...',
})
// 使用流式导入(在 Worker 线程中执行)
const result = await worker.streamImport(filePath, (progress: ParseProgress) => {
// 转发进度到渲染进程
win.webContents.send('chat:importProgress', {
stage: progress.stage,
progress: progress.percentage,
message: progress.message,
bytesRead: progress.bytesRead,
totalBytes: progress.totalBytes,
messagesProcessed: progress.messagesProcessed,
})
})
if (result.success) {
console.log('[IpcMain] Stream import successful, sessionId:', result.sessionId)
return { success: true, sessionId: result.sessionId }
} else {
console.error('[IpcMain] Stream import failed:', result.error)
win.webContents.send('chat:importProgress', {
stage: 'error',
progress: 0,
message: result.error,
})
return { success: false, error: result.error }
}
} catch (error) {
console.error('[IpcMain] Import failed:', error)
win.webContents.send('chat:importProgress', {
stage: 'error',
progress: 0,
message: String(error),
})
return { success: false, error: String(error) }
}
})
/**
* 获取所有分析会话列表
*/
ipcMain.handle('chat:getSessions', async () => {
try {
const sessions = await worker.getAllSessions()
return sessions
} catch (error) {
console.error('[IpcMain] Error getting sessions:', error)
return []
}
})
/**
* 获取单个会话信息
*/
ipcMain.handle('chat:getSession', async (_, sessionId: string) => {
try {
return await worker.getSession(sessionId)
} catch (error) {
console.error('获取会话信息失败:', error)
return null
}
})
/**
* 删除会话
*/
ipcMain.handle('chat:deleteSession', async (_, sessionId: string) => {
try {
// 先关闭 Worker 中的数据库连接
await worker.closeDatabase(sessionId)
// 然后删除文件(使用核心模块)
return databaseCore.deleteSession(sessionId)
} catch (error) {
console.error('删除会话失败:', error)
return false
}
})
/**
* 重命名会话
*/
ipcMain.handle('chat:renameSession', async (_, sessionId: string, newName: string) => {
try {
// 先关闭 Worker 中的数据库连接(确保没有其他进程占用)
await worker.closeDatabase(sessionId)
// 执行重命名
return databaseCore.renameSession(sessionId, newName)
} catch (error) {
console.error('重命名会话失败:', error)
return false
}
})
/**
* 获取可用年份列表
*/
ipcMain.handle('chat:getAvailableYears', async (_, sessionId: string) => {
try {
return await worker.getAvailableYears(sessionId)
} catch (error) {
console.error('获取可用年份失败:', error)
return []
}
})
/**
* 获取成员活跃度排行
*/
ipcMain.handle(
'chat:getMemberActivity',
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
try {
return await worker.getMemberActivity(sessionId, filter)
} catch (error) {
console.error('获取成员活跃度失败:', error)
return []
}
}
)
/**
* 获取成员历史昵称
*/
ipcMain.handle('chat:getMemberNameHistory', async (_, sessionId: string, memberId: number) => {
try {
return await worker.getMemberNameHistory(sessionId, memberId)
} catch (error) {
console.error('获取成员历史昵称失败:', error)
return []
}
})
/**
* 获取每小时活跃度分布
*/
ipcMain.handle(
'chat:getHourlyActivity',
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
try {
return await worker.getHourlyActivity(sessionId, filter)
} catch (error) {
console.error('获取小时活跃度失败:', error)
return []
}
}
)
/**
* 获取每日活跃度趋势
*/
ipcMain.handle(
'chat:getDailyActivity',
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
try {
return await worker.getDailyActivity(sessionId, filter)
} catch (error) {
console.error('获取日活跃度失败:', error)
return []
}
}
)
/**
* 获取星期活跃度分布
*/
ipcMain.handle(
'chat:getWeekdayActivity',
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
try {
return await worker.getWeekdayActivity(sessionId, filter)
} catch (error) {
console.error('获取星期活跃度失败:', error)
return []
}
}
)
/**
* 获取月份活跃度分布
*/
ipcMain.handle(
'chat:getMonthlyActivity',
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
try {
return await worker.getMonthlyActivity(sessionId, filter)
} catch (error) {
console.error('获取月份活跃度失败:', error)
return []
}
}
)
/**
* 获取消息类型分布
*/
ipcMain.handle(
'chat:getMessageTypeDistribution',
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
try {
return await worker.getMessageTypeDistribution(sessionId, filter)
} catch (error) {
console.error('获取消息类型分布失败:', error)
return []
}
}
)
/**
* 获取时间范围
*/
ipcMain.handle('chat:getTimeRange', async (_, sessionId: string) => {
try {
return await worker.getTimeRange(sessionId)
} catch (error) {
console.error('获取时间范围失败:', error)
return null
}
})
/**
* 获取数据库存储目录
*/
ipcMain.handle('chat:getDbDirectory', async () => {
try {
return worker.getDbDirectory()
} catch (error) {
console.error('获取数据库目录失败:', error)
return null
}
})
/**
* 获取支持的格式列表
*/
ipcMain.handle('chat:getSupportedFormats', async () => {
return parser.getSupportedFormats()
})
/**
* 获取复读分析数据
*/
ipcMain.handle(
'chat:getRepeatAnalysis',
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
try {
return await worker.getRepeatAnalysis(sessionId, filter)
} catch (error) {
console.error('获取复读分析失败:', error)
return { originators: [], initiators: [], breakers: [], totalRepeatChains: 0 }
}
}
)
/**
* 获取口头禅分析数据
*/
ipcMain.handle(
'chat:getCatchphraseAnalysis',
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
try {
return await worker.getCatchphraseAnalysis(sessionId, filter)
} catch (error) {
console.error('获取口头禅分析失败:', error)
return { members: [] }
}
}
)
/**
* 获取夜猫分析数据
*/
ipcMain.handle(
'chat:getNightOwlAnalysis',
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
try {
return await worker.getNightOwlAnalysis(sessionId, filter)
} catch (error) {
console.error('获取夜猫分析失败:', error)
return {
nightOwlRank: [],
lastSpeakerRank: [],
firstSpeakerRank: [],
consecutiveRecords: [],
champions: [],
totalDays: 0,
}
}
}
)
/**
* 获取龙王分析数据
*/
ipcMain.handle(
'chat:getDragonKingAnalysis',
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
try {
return await worker.getDragonKingAnalysis(sessionId, filter)
} catch (error) {
console.error('获取龙王分析失败:', error)
return { rank: [], totalDays: 0 }
}
}
)
/**
* 获取潜水分析数据
*/
ipcMain.handle(
'chat:getDivingAnalysis',
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
try {
return await worker.getDivingAnalysis(sessionId, filter)
} catch (error) {
console.error('获取潜水分析失败:', error)
return { rank: [] }
}
}
)
/**
* 获取自言自语分析数据
*/
ipcMain.handle(
'chat:getMonologueAnalysis',
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
try {
return await worker.getMonologueAnalysis(sessionId, filter)
} catch (error) {
console.error('获取自言自语分析失败:', error)
return { rank: [], maxComboRecord: null }
}
}
)
/**
* 获取 @ 互动分析数据
*/
ipcMain.handle(
'chat:getMentionAnalysis',
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
try {
return await worker.getMentionAnalysis(sessionId, filter)
} catch (error) {
console.error('获取 @ 互动分析失败:', error)
return { topMentioners: [], topMentioned: [], oneWay: [], twoWay: [], totalMentions: 0, memberDetails: [] }
}
}
)
/**
* 获取含笑量分析数据
*/
ipcMain.handle(
'chat:getLaughAnalysis',
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }, keywords?: string[]) => {
try {
return await worker.getLaughAnalysis(sessionId, filter, keywords)
} catch (error) {
console.error('获取含笑量分析失败:', error)
return {
rankByRate: [],
rankByCount: [],
typeDistribution: [],
totalLaughs: 0,
totalMessages: 0,
groupLaughRate: 0,
}
}
}
)
/**
* 获取斗图分析数据
*/
ipcMain.handle(
'chat:getMemeBattleAnalysis',
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
try {
return await worker.getMemeBattleAnalysis(sessionId, filter)
} catch (error) {
console.error('获取斗图分析失败:', error)
return {
longestBattle: null,
rankByCount: [],
rankByImageCount: [],
totalBattles: 0,
}
}
}
)
/**
* 获取打卡分析数据(火花榜 + 忠臣榜)
*/
ipcMain.handle(
'chat:getCheckInAnalysis',
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
try {
return await worker.getCheckInAnalysis(sessionId, filter)
} catch (error) {
console.error('获取打卡分析失败:', error)
return {
streakRank: [],
loyaltyRank: [],
totalDays: 0,
}
}
}
)
// ==================== 成员管理 ====================
/**
* 获取所有成员列表(含消息数和别名)
*/
ipcMain.handle('chat:getMembers', async (_, sessionId: string) => {
try {
return await worker.getMembers(sessionId)
} catch (error) {
console.error('获取成员列表失败:', error)
return []
}
})
/**
* 更新成员别名
*/
ipcMain.handle('chat:updateMemberAliases', async (_, sessionId: string, memberId: number, aliases: string[]) => {
try {
return await worker.updateMemberAliases(sessionId, memberId, aliases)
} catch (error) {
console.error('更新成员别名失败:', error)
return false
}
})
/**
* 删除成员及其所有消息
*/
ipcMain.handle('chat:deleteMember', async (_, sessionId: string, memberId: number) => {
try {
// 先关闭数据库连接
await worker.closeDatabase(sessionId)
// 执行删除
return await worker.deleteMember(sessionId, memberId)
} catch (error) {
console.error('删除成员失败:', error)
return false
}
})
}

133
electron/main/ipc/merge.ts Normal file
View File

@@ -0,0 +1,133 @@
/**
* 合并功能 IPC 处理器
*/
import { ipcMain } from 'electron'
import * as worker from '../worker/workerManager'
import * as merger from '../merger'
import { deleteTempDatabase, cleanupAllTempDatabases } from '../merger/tempCache'
import type { ParseProgress } from '../parser'
import type { MergeParams } from '../../../src/types/chat'
import type { IpcContext } from './types'
// ==================== 临时数据库缓存 ====================
// 用于合并功能:缓存文件对应的临时数据库路径
// 这样用户删除本地文件后仍然可以进行合并(数据已存入临时数据库)
const tempDbCache = new Map<string, string>()
/**
* 清理指定文件的缓存(删除临时数据库)
*/
function clearTempDbCache(filePath: string): void {
const tempDbPath = tempDbCache.get(filePath)
if (tempDbPath) {
deleteTempDatabase(tempDbPath)
tempDbCache.delete(filePath)
}
}
/**
* 清理所有缓存(删除所有临时数据库)
*/
function clearAllTempDbCache(): void {
for (const tempDbPath of tempDbCache.values()) {
deleteTempDatabase(tempDbPath)
}
tempDbCache.clear()
console.log('[IpcMain] 已清理所有临时数据库缓存')
}
/**
* 初始化合并模块(清理残留临时数据库)
*/
export function initMergeModule(): void {
cleanupAllTempDatabases()
}
/**
* 注册合并功能 IPC 处理器
*/
export function registerMergeHandlers(ctx: IpcContext): void {
const { win } = ctx
/**
* 解析文件获取基本信息(用于合并预览)
* 使用流式解析,数据写入临时数据库,避免内存溢出
*/
ipcMain.handle('merge:parseFileInfo', async (_, filePath: string) => {
try {
// 使用流式解析,写入临时数据库
const result = await worker.streamParseFileInfo(filePath, (progress: ParseProgress) => {
// 可选:发送进度到渲染进程
win.webContents.send('merge:parseProgress', {
filePath,
progress,
})
})
// 缓存临时数据库路径(用于后续合并)
// 这样即使用户删除本地文件,也能继续合并(数据已在临时数据库中)
if (result.tempDbPath) {
tempDbCache.set(filePath, result.tempDbPath)
console.log(`[IpcMain] 已缓存临时数据库: ${filePath} -> ${result.tempDbPath}`)
}
// 返回基本信息
return {
name: result.name,
format: result.format,
platform: result.platform,
messageCount: result.messageCount,
memberCount: result.memberCount,
fileSize: result.fileSize,
}
} catch (error) {
console.error('解析文件信息失败:', error)
throw error
}
})
/**
* 检测合并冲突(使用临时数据库)
*/
ipcMain.handle('merge:checkConflicts', async (_, filePaths: string[]) => {
try {
return merger.checkConflictsWithTempDb(filePaths, tempDbCache)
} catch (error) {
console.error('检测冲突失败:', error)
throw error
}
})
/**
* 执行合并(使用临时数据库)
*/
ipcMain.handle('merge:mergeFiles', async (_, params: MergeParams) => {
try {
const result = await merger.mergeFilesWithTempDb(params, tempDbCache)
// 合并完成后清理缓存
if (result.success) {
for (const filePath of params.filePaths) {
clearTempDbCache(filePath)
}
}
return result
} catch (error) {
console.error('合并失败:', error)
return { success: false, error: String(error) }
}
})
/**
* 清理合并缓存(用于用户移除文件时)
*/
ipcMain.handle('merge:clearCache', async (_, filePath?: string) => {
if (filePath) {
clearTempDbCache(filePath)
} else {
clearAllTempDbCache()
}
return true
})
}

View File

@@ -0,0 +1,18 @@
/**
* IPC 处理器共享类型
*/
import type { BrowserWindow } from 'electron'
/**
* IPC 处理器注册上下文
*/
export interface IpcContext {
/** 主窗口 */
win: BrowserWindow
}
/**
* IPC 处理器注册函数类型
*/
export type IpcHandlerRegister = (ctx: IpcContext) => void

129
electron/main/ipc/window.ts Normal file
View File

@@ -0,0 +1,129 @@
/**
* 窗口和文件系统操作 IPC 处理器
*/
import { ipcMain, app, dialog, clipboard, shell } from 'electron'
import { autoUpdater } from 'electron-updater'
import * as fs from 'fs/promises'
import type { IpcContext } from './types'
/**
* 注册窗口和文件系统操作 IPC 处理器
*/
export function registerWindowHandlers(ctx: IpcContext): void {
const { win } = ctx
// ==================== 窗口操作 ====================
ipcMain.on('window-min', (ev) => {
ev.preventDefault()
win.minimize()
})
ipcMain.on('window-maxOrRestore', (ev) => {
const winSizeState = win.isMaximized()
winSizeState ? win.restore() : win.maximize()
ev.reply('windowState', win.isMaximized())
})
ipcMain.on('window-restore', () => {
win.restore()
})
ipcMain.on('window-hide', () => {
win.hide()
})
ipcMain.on('window-close', () => {
win.close()
// @ts-ignore
app.isQuitting = true
app.quit()
})
ipcMain.on('window-resize', (_, data) => {
if (data.resize) {
win.setResizable(true)
} else {
win.setSize(1180, 752)
win.setResizable(false)
}
})
ipcMain.on('open-devtools', () => {
win.webContents.openDevTools()
})
// ==================== 更新检查 ====================
ipcMain.on('check-update', () => {
autoUpdater.checkForUpdates()
})
// ==================== 通用工具 ====================
ipcMain.handle('show-message', (event, args) => {
event.sender.send('show-message', args)
})
// 复制到剪贴板
ipcMain.handle('copyData', async (_, data) => {
try {
clipboard.writeText(data)
return true
} catch (error) {
console.error('复制操作出错:', error)
return false
}
})
// ==================== 文件系统操作 ====================
// 选择文件夹
ipcMain.handle('selectDir', async (_, defaultPath = '') => {
try {
const { canceled, filePaths } = await dialog.showOpenDialog({
title: '选择目录',
defaultPath: defaultPath || app.getPath('documents'),
properties: ['openDirectory', 'createDirectory'],
buttonLabel: '选择文件夹',
})
if (!canceled) {
return filePaths[0]
}
return null
} catch (err) {
console.error('选择文件夹时发生错误:', err)
throw err
}
})
// 检查文件是否存在
ipcMain.handle('checkFileExist', async (_, filePath) => {
try {
await fs.access(filePath)
return true
} catch {
return false
}
})
// 在文件管理器中打开
ipcMain.handle('openInFolder', async (_, path) => {
try {
await fs.access(path)
await shell.showItemInFolder(path)
return true
} catch (error) {
console.error('打开目录时出错:', error)
return false
}
})
// 显示打开对话框(通用)
ipcMain.handle('dialog:showOpenDialog', async (_, options) => {
try {
return await dialog.showOpenDialog(options)
} catch (error) {
console.error('显示对话框失败:', error)
throw error
}
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,48 +0,0 @@
/**
* Worker 模块入口
* 导出 Worker 管理器的所有 API
*/
export {
initWorker,
closeWorker,
getDbDirectory,
// 分析查询 API异步
getAvailableYears,
getMemberActivity,
getHourlyActivity,
getDailyActivity,
getWeekdayActivity,
getMonthlyActivity,
getMessageTypeDistribution,
getTimeRange,
getMemberNameHistory,
getRepeatAnalysis,
getCatchphraseAnalysis,
getNightOwlAnalysis,
getDragonKingAnalysis,
getDivingAnalysis,
getMonologueAnalysis,
getMentionAnalysis,
getLaughAnalysis,
getMemeBattleAnalysis,
getCheckInAnalysis,
// 会话管理 API异步
getAllSessions,
getSession,
closeDatabase,
// 成员管理 API
getMembers,
updateMemberAliases,
deleteMember,
// 文件解析 API已废弃使用流式版本
parseFileInfo,
// 流式导入 API
streamImport,
streamParseFileInfo,
// AI 查询 API
searchMessages,
getMessageContext,
} from './workerManager'
export type { SearchMessageResult, MemberWithStats } from './workerManager'