mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-04 04:01:16 +08:00
refactor: 重构部分架构
This commit is contained in:
@@ -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
519
electron/main/ipc/ai.ts
Normal 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()
|
||||
})
|
||||
|
||||
// ==================== 兼容旧 API(deprecated)====================
|
||||
|
||||
/**
|
||||
* @deprecated 使用 llm:getAllConfigs 代替
|
||||
* 获取当前 LLM 配置
|
||||
*/
|
||||
ipcMain.handle('llm:getConfig', async () => {
|
||||
const config = llm.getActiveConfig()
|
||||
if (!config) return null
|
||||
return {
|
||||
provider: config.provider,
|
||||
apiKey: config.apiKey ? `${config.apiKey.slice(0, 8)}...${config.apiKey.slice(-4)}` : '',
|
||||
apiKeySet: !!config.apiKey,
|
||||
model: config.model,
|
||||
baseUrl: config.baseUrl,
|
||||
maxTokens: config.maxTokens,
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* @deprecated 使用 llm:addConfig 或 llm:updateConfig 代替
|
||||
* 保存 LLM 配置
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'llm:saveConfig',
|
||||
async (
|
||||
_,
|
||||
config: { provider: llm.LLMProvider; apiKey: string; model?: string; baseUrl?: string; maxTokens?: number }
|
||||
) => {
|
||||
try {
|
||||
const activeConfig = llm.getActiveConfig()
|
||||
|
||||
if (activeConfig) {
|
||||
// 更新现有配置
|
||||
const updates: Record<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
213
electron/main/ipc/cache.ts
Normal 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
523
electron/main/ipc/chat.ts
Normal 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
133
electron/main/ipc/merge.ts
Normal 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
|
||||
})
|
||||
}
|
||||
|
||||
18
electron/main/ipc/types.ts
Normal file
18
electron/main/ipc/types.ts
Normal 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
129
electron/main/ipc/window.ts
Normal 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
@@ -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'
|
||||
Reference in New Issue
Block a user