feat: 重构架构,新增Worker查询线程

This commit is contained in:
digua
2025-11-28 22:00:40 +08:00
parent 642b2b3d17
commit 0598699318
10 changed files with 1854 additions and 40 deletions
+37 -23
View File
@@ -2,14 +2,25 @@ import { ipcMain, app, dialog, clipboard, shell, BrowserWindow } from 'electron'
import { autoUpdater } from 'electron-updater'
import * as fs from 'fs/promises'
// 导入数据库和解析器模块
import * as database from './database'
// 导入数据库核心模块(用于导入和删除操作)
import * as databaseCore from './database/core'
// 导入 Worker 模块(用于异步分析查询)
import * as worker from './worker'
// 导入解析器模块
import * as parser from './parser'
console.log('[IpcMain] Database and Parser modules imported')
console.log('[IpcMain] Database, Worker and Parser modules imported')
const mainIpcMain = (win: BrowserWindow) => {
console.log('[IpcMain] Registering IPC handlers...')
// 初始化 Worker
try {
worker.initWorker()
console.log('[IpcMain] Worker initialized successfully')
} catch (error) {
console.error('[IpcMain] Failed to initialize worker:', error)
}
// ==================== 窗口操作 ====================
ipcMain.on('window-min', (ev) => {
ev.preventDefault()
@@ -182,8 +193,8 @@ const mainIpcMain = (win: BrowserWindow) => {
})
console.log('[IpcMain] Importing to database...')
// 导入到数据库
const sessionId = database.importData(parseResult)
// 导入到数据库(使用核心模块,同步操作)
const sessionId = databaseCore.importData(parseResult)
console.log('[IpcMain] Import successful, sessionId:', sessionId)
// 发送进度:完成
@@ -213,7 +224,7 @@ const mainIpcMain = (win: BrowserWindow) => {
ipcMain.handle('chat:getSessions', async () => {
console.log('[IpcMain] chat:getSessions called')
try {
const sessions = database.getAllSessions()
const sessions = await worker.getAllSessions()
console.log('[IpcMain] Found sessions:', sessions.length)
return sessions
} catch (error) {
@@ -227,7 +238,7 @@ const mainIpcMain = (win: BrowserWindow) => {
*/
ipcMain.handle('chat:getSession', async (_, sessionId: string) => {
try {
return database.getSession(sessionId)
return await worker.getSession(sessionId)
} catch (error) {
console.error('获取会话信息失败:', error)
return null
@@ -239,7 +250,10 @@ const mainIpcMain = (win: BrowserWindow) => {
*/
ipcMain.handle('chat:deleteSession', async (_, sessionId: string) => {
try {
return database.deleteSession(sessionId)
// 先关闭 Worker 中的数据库连接
await worker.closeDatabase(sessionId)
// 然后删除文件(使用核心模块)
return databaseCore.deleteSession(sessionId)
} catch (error) {
console.error('删除会话失败:', error)
return false
@@ -251,7 +265,7 @@ const mainIpcMain = (win: BrowserWindow) => {
*/
ipcMain.handle('chat:getAvailableYears', async (_, sessionId: string) => {
try {
return database.getAvailableYears(sessionId)
return await worker.getAvailableYears(sessionId)
} catch (error) {
console.error('获取可用年份失败:', error)
return []
@@ -265,7 +279,7 @@ const mainIpcMain = (win: BrowserWindow) => {
'chat:getMemberActivity',
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
try {
return database.getMemberActivity(sessionId, filter)
return await worker.getMemberActivity(sessionId, filter)
} catch (error) {
console.error('获取成员活跃度失败:', error)
return []
@@ -278,7 +292,7 @@ const mainIpcMain = (win: BrowserWindow) => {
*/
ipcMain.handle('chat:getMemberNameHistory', async (_, sessionId: string, memberId: number) => {
try {
return database.getMemberNameHistory(sessionId, memberId)
return await worker.getMemberNameHistory(sessionId, memberId)
} catch (error) {
console.error('获取成员历史昵称失败:', error)
return []
@@ -292,7 +306,7 @@ const mainIpcMain = (win: BrowserWindow) => {
'chat:getHourlyActivity',
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
try {
return database.getHourlyActivity(sessionId, filter)
return await worker.getHourlyActivity(sessionId, filter)
} catch (error) {
console.error('获取小时活跃度失败:', error)
return []
@@ -307,7 +321,7 @@ const mainIpcMain = (win: BrowserWindow) => {
'chat:getDailyActivity',
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
try {
return database.getDailyActivity(sessionId, filter)
return await worker.getDailyActivity(sessionId, filter)
} catch (error) {
console.error('获取日活跃度失败:', error)
return []
@@ -322,7 +336,7 @@ const mainIpcMain = (win: BrowserWindow) => {
'chat:getWeekdayActivity',
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
try {
return database.getWeekdayActivity(sessionId, filter)
return await worker.getWeekdayActivity(sessionId, filter)
} catch (error) {
console.error('获取星期活跃度失败:', error)
return []
@@ -337,7 +351,7 @@ const mainIpcMain = (win: BrowserWindow) => {
'chat:getMessageTypeDistribution',
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
try {
return database.getMessageTypeDistribution(sessionId, filter)
return await worker.getMessageTypeDistribution(sessionId, filter)
} catch (error) {
console.error('获取消息类型分布失败:', error)
return []
@@ -350,7 +364,7 @@ const mainIpcMain = (win: BrowserWindow) => {
*/
ipcMain.handle('chat:getTimeRange', async (_, sessionId: string) => {
try {
return database.getTimeRange(sessionId)
return await worker.getTimeRange(sessionId)
} catch (error) {
console.error('获取时间范围失败:', error)
return null
@@ -362,7 +376,7 @@ const mainIpcMain = (win: BrowserWindow) => {
*/
ipcMain.handle('chat:getDbDirectory', async () => {
try {
return database.getDbDirectory()
return worker.getDbDirectory()
} catch (error) {
console.error('获取数据库目录失败:', error)
return null
@@ -383,7 +397,7 @@ const mainIpcMain = (win: BrowserWindow) => {
'chat:getRepeatAnalysis',
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
try {
return database.getRepeatAnalysis(sessionId, filter)
return await worker.getRepeatAnalysis(sessionId, filter)
} catch (error) {
console.error('获取复读分析失败:', error)
return { originators: [], initiators: [], breakers: [], totalRepeatChains: 0 }
@@ -398,7 +412,7 @@ const mainIpcMain = (win: BrowserWindow) => {
'chat:getCatchphraseAnalysis',
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
try {
return database.getCatchphraseAnalysis(sessionId, filter)
return await worker.getCatchphraseAnalysis(sessionId, filter)
} catch (error) {
console.error('获取口头禅分析失败:', error)
return { members: [] }
@@ -413,7 +427,7 @@ const mainIpcMain = (win: BrowserWindow) => {
'chat:getNightOwlAnalysis',
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
try {
return database.getNightOwlAnalysis(sessionId, filter)
return await worker.getNightOwlAnalysis(sessionId, filter)
} catch (error) {
console.error('获取夜猫分析失败:', error)
return {
@@ -435,7 +449,7 @@ const mainIpcMain = (win: BrowserWindow) => {
'chat:getDragonKingAnalysis',
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
try {
return database.getDragonKingAnalysis(sessionId, filter)
return await worker.getDragonKingAnalysis(sessionId, filter)
} catch (error) {
console.error('获取龙王分析失败:', error)
return { rank: [], totalDays: 0 }
@@ -450,7 +464,7 @@ const mainIpcMain = (win: BrowserWindow) => {
'chat:getDivingAnalysis',
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
try {
return database.getDivingAnalysis(sessionId, filter)
return await worker.getDivingAnalysis(sessionId, filter)
} catch (error) {
console.error('获取潜水分析失败:', error)
return { rank: [] }
@@ -465,7 +479,7 @@ const mainIpcMain = (win: BrowserWindow) => {
'chat:getMonologueAnalysis',
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
try {
return database.getMonologueAnalysis(sessionId, filter)
return await worker.getMonologueAnalysis(sessionId, filter)
} catch (error) {
console.error('获取自言自语分析失败:', error)
return { rank: [], maxComboRecord: null }
+121
View File
@@ -0,0 +1,121 @@
/**
* 数据库核心工具模块
* 提供数据库连接管理和通用工具函数
*/
import Database from 'better-sqlite3'
import * as fs from 'fs'
import * as path from 'path'
// 数据库目录(由 Worker 初始化时设置)
let DB_DIR: string = ''
// 数据库连接缓存
const dbCache = new Map<string, Database.Database>()
/**
* 初始化数据库目录
*/
export function initDbDir(dir: string): void {
DB_DIR = dir
}
/**
* 获取数据库文件路径
*/
export function getDbPath(sessionId: string): string {
return path.join(DB_DIR, `${sessionId}.db`)
}
/**
* 打开数据库(带缓存)
*/
export function openDatabase(sessionId: string): Database.Database | null {
// 检查缓存
if (dbCache.has(sessionId)) {
return dbCache.get(sessionId)!
}
const dbPath = getDbPath(sessionId)
if (!fs.existsSync(dbPath)) {
return null
}
const db = new Database(dbPath, { readonly: true })
db.pragma('journal_mode = WAL')
// 缓存连接
dbCache.set(sessionId, db)
return db
}
/**
* 关闭指定会话的数据库连接
*/
export function closeDatabase(sessionId: string): void {
const db = dbCache.get(sessionId)
if (db) {
db.close()
dbCache.delete(sessionId)
}
}
/**
* 关闭所有数据库连接
*/
export function closeAllDatabases(): void {
for (const [sessionId, db] of dbCache.entries()) {
db.close()
dbCache.delete(sessionId)
}
}
/**
* 获取数据库目录
*/
export function getDbDir(): string {
return DB_DIR
}
// ==================== 时间过滤工具 ====================
export interface TimeFilter {
startTs?: number
endTs?: number
}
/**
* 构建时间过滤 WHERE 子句
*/
export function buildTimeFilter(filter?: TimeFilter): { clause: string; params: number[] } {
const conditions: string[] = []
const params: number[] = []
if (filter?.startTs !== undefined) {
conditions.push('ts >= ?')
params.push(filter.startTs)
}
if (filter?.endTs !== undefined) {
conditions.push('ts <= ?')
params.push(filter.endTs)
}
return {
clause: conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : '',
params,
}
}
/**
* 构建排除系统消息的过滤条件
*/
export function buildSystemMessageFilter(existingClause: string): string {
const systemFilter = "m.name != '系统消息'"
if (existingClause.includes('WHERE')) {
return existingClause + ' AND ' + systemFilter
} else {
return ' WHERE ' + systemFilter
}
}
+103
View File
@@ -0,0 +1,103 @@
/**
* 数据库 Worker 线程
* 在独立线程中执行数据库操作,避免阻塞主进程
*
* 本文件作为 Worker 入口,负责:
* 1. 初始化数据库目录
* 2. 接收主进程消息
* 3. 分发到对应的查询模块
* 4. 返回结果
*/
import { parentPort, workerData } from 'worker_threads'
import { initDbDir, closeDatabase, closeAllDatabases } from './dbCore'
import {
getAvailableYears,
getMemberActivity,
getHourlyActivity,
getDailyActivity,
getWeekdayActivity,
getMessageTypeDistribution,
getTimeRange,
getMemberNameHistory,
getAllSessions,
getSession,
} from './queryBasic'
import {
getRepeatAnalysis,
getCatchphraseAnalysis,
getNightOwlAnalysis,
getDragonKingAnalysis,
getDivingAnalysis,
getMonologueAnalysis,
} from './queryAdvanced'
// 初始化数据库目录
initDbDir(workerData.dbDir)
// ==================== 消息处理 ====================
interface WorkerMessage {
id: string
type: string
payload: any
}
// 消息类型到处理函数的映射
const handlers: Record<string, (payload: any) => any> = {
// 基础查询
getAvailableYears: (p) => getAvailableYears(p.sessionId),
getMemberActivity: (p) => getMemberActivity(p.sessionId, p.filter),
getHourlyActivity: (p) => getHourlyActivity(p.sessionId, p.filter),
getDailyActivity: (p) => getDailyActivity(p.sessionId, p.filter),
getWeekdayActivity: (p) => getWeekdayActivity(p.sessionId, p.filter),
getMessageTypeDistribution: (p) => getMessageTypeDistribution(p.sessionId, p.filter),
getTimeRange: (p) => getTimeRange(p.sessionId),
getMemberNameHistory: (p) => getMemberNameHistory(p.sessionId, p.memberId),
// 会话管理
getAllSessions: () => getAllSessions(),
getSession: (p) => getSession(p.sessionId),
closeDatabase: (p) => {
closeDatabase(p.sessionId)
return true
},
closeAll: () => {
closeAllDatabases()
return true
},
// 高级分析
getRepeatAnalysis: (p) => getRepeatAnalysis(p.sessionId, p.filter),
getCatchphraseAnalysis: (p) => getCatchphraseAnalysis(p.sessionId, p.filter),
getNightOwlAnalysis: (p) => getNightOwlAnalysis(p.sessionId, p.filter),
getDragonKingAnalysis: (p) => getDragonKingAnalysis(p.sessionId, p.filter),
getDivingAnalysis: (p) => getDivingAnalysis(p.sessionId, p.filter),
getMonologueAnalysis: (p) => getMonologueAnalysis(p.sessionId, p.filter),
}
// 处理消息
parentPort?.on('message', (message: WorkerMessage) => {
const { id, type, payload } = message
try {
const handler = handlers[type]
if (!handler) {
throw new Error(`Unknown message type: ${type}`)
}
const result = handler(payload)
parentPort?.postMessage({ id, success: true, result })
} catch (error) {
parentPort?.postMessage({
id,
success: false,
error: error instanceof Error ? error.message : String(error),
})
}
})
// 进程退出时关闭所有数据库连接
process.on('exit', () => {
closeAllDatabases()
})
+30
View File
@@ -0,0 +1,30 @@
/**
* Worker 模块入口
* 导出 Worker 管理器的所有 API
*/
export {
initWorker,
closeWorker,
getDbDirectory,
// 分析查询 API(异步)
getAvailableYears,
getMemberActivity,
getHourlyActivity,
getDailyActivity,
getWeekdayActivity,
getMessageTypeDistribution,
getTimeRange,
getMemberNameHistory,
getRepeatAnalysis,
getCatchphraseAnalysis,
getNightOwlAnalysis,
getDragonKingAnalysis,
getDivingAnalysis,
getMonologueAnalysis,
// 会话管理 API(异步)
getAllSessions,
getSession,
closeDatabase,
} from './workerManager'
+902
View File
@@ -0,0 +1,902 @@
/**
* 高级分析查询模块
* 提供复读、口头禅、夜猫、龙王等复杂分析
*/
import {
openDatabase,
buildTimeFilter,
buildSystemMessageFilter,
type TimeFilter,
} from './dbCore'
// ==================== 复读分析 ====================
/**
* 获取复读分析数据
*/
export function getRepeatAnalysis(sessionId: string, filter?: TimeFilter): any {
const db = openDatabase(sessionId)
const emptyResult = {
originators: [],
initiators: [],
breakers: [],
originatorRates: [],
initiatorRates: [],
breakerRates: [],
chainLengthDistribution: [],
hotContents: [],
avgChainLength: 0,
totalRepeatChains: 0,
}
if (!db) return emptyResult
const { clause, params } = buildTimeFilter(filter)
let whereClause = clause
if (whereClause.includes('WHERE')) {
whereClause +=
" AND m.name != '系统消息' AND msg.type = 0 AND msg.content IS NOT NULL AND TRIM(msg.content) != ''"
} else {
whereClause =
" WHERE m.name != '系统消息' AND msg.type = 0 AND msg.content IS NOT NULL AND TRIM(msg.content) != ''"
}
const messages = db
.prepare(
`
SELECT
msg.id,
msg.sender_id as senderId,
msg.content,
msg.ts,
m.platform_id as platformId,
m.name
FROM message msg
JOIN member m ON msg.sender_id = m.id
${whereClause}
ORDER BY msg.ts ASC, msg.id ASC
`
)
.all(...params) as Array<{
id: number
senderId: number
content: string
ts: number
platformId: string
name: string
}>
const originatorCount = new Map<number, number>()
const initiatorCount = new Map<number, number>()
const breakerCount = new Map<number, number>()
const memberMessageCount = new Map<number, number>()
const memberInfo = new Map<number, { platformId: string; name: string }>()
const chainLengthCount = new Map<number, number>()
const contentStats = new Map<
string,
{ count: number; maxChainLength: number; originatorId: number; lastTs: number }
>()
let currentContent: string | null = null
let repeatChain: Array<{ senderId: number; content: string; ts: number }> = []
let totalRepeatChains = 0
let totalChainLength = 0
const processRepeatChain = (
chain: Array<{ senderId: number; content: string; ts: number }>,
breakerId?: number
) => {
if (chain.length < 3) return
totalRepeatChains++
const chainLength = chain.length
totalChainLength += chainLength
const originatorId = chain[0].senderId
originatorCount.set(originatorId, (originatorCount.get(originatorId) || 0) + 1)
const initiatorId = chain[1].senderId
initiatorCount.set(initiatorId, (initiatorCount.get(initiatorId) || 0) + 1)
if (breakerId !== undefined) {
breakerCount.set(breakerId, (breakerCount.get(breakerId) || 0) + 1)
}
chainLengthCount.set(chainLength, (chainLengthCount.get(chainLength) || 0) + 1)
const content = chain[0].content
const chainTs = chain[0].ts
const existing = contentStats.get(content)
if (existing) {
existing.count++
existing.lastTs = Math.max(existing.lastTs, chainTs)
if (chainLength > existing.maxChainLength) {
existing.maxChainLength = chainLength
existing.originatorId = originatorId
}
} else {
contentStats.set(content, { count: 1, maxChainLength: chainLength, originatorId, lastTs: chainTs })
}
}
for (const msg of messages) {
if (!memberInfo.has(msg.senderId)) {
memberInfo.set(msg.senderId, { platformId: msg.platformId, name: msg.name })
}
memberMessageCount.set(msg.senderId, (memberMessageCount.get(msg.senderId) || 0) + 1)
const content = msg.content.trim()
if (content === currentContent) {
const lastSender = repeatChain[repeatChain.length - 1]?.senderId
if (lastSender !== msg.senderId) {
repeatChain.push({ senderId: msg.senderId, content, ts: msg.ts })
}
} else {
processRepeatChain(repeatChain, msg.senderId)
currentContent = content
repeatChain = [{ senderId: msg.senderId, content, ts: msg.ts }]
}
}
processRepeatChain(repeatChain)
const buildRankList = (countMap: Map<number, number>, total: number): any[] => {
const items: any[] = []
for (const [memberId, count] of countMap.entries()) {
const info = memberInfo.get(memberId)
if (info) {
items.push({
memberId,
platformId: info.platformId,
name: info.name,
count,
percentage: total > 0 ? Math.round((count / total) * 10000) / 100 : 0,
})
}
}
return items.sort((a, b) => b.count - a.count)
}
const buildRateList = (countMap: Map<number, number>): any[] => {
const items: any[] = []
for (const [memberId, count] of countMap.entries()) {
const info = memberInfo.get(memberId)
const totalMessages = memberMessageCount.get(memberId) || 0
if (info && totalMessages > 0) {
items.push({
memberId,
platformId: info.platformId,
name: info.name,
count,
totalMessages,
rate: Math.round((count / totalMessages) * 10000) / 100,
})
}
}
return items.sort((a, b) => b.rate - a.rate)
}
const chainLengthDistribution: any[] = []
for (const [length, count] of chainLengthCount.entries()) {
chainLengthDistribution.push({ length, count })
}
chainLengthDistribution.sort((a, b) => a.length - b.length)
const hotContents: any[] = []
for (const [content, stats] of contentStats.entries()) {
const originatorInfo = memberInfo.get(stats.originatorId)
hotContents.push({
content,
count: stats.count,
maxChainLength: stats.maxChainLength,
originatorName: originatorInfo?.name || '未知',
lastTs: stats.lastTs,
})
}
hotContents.sort((a, b) => b.maxChainLength - a.maxChainLength)
const top10HotContents = hotContents.slice(0, 10)
return {
originators: buildRankList(originatorCount, totalRepeatChains),
initiators: buildRankList(initiatorCount, totalRepeatChains),
breakers: buildRankList(breakerCount, totalRepeatChains),
originatorRates: buildRateList(originatorCount),
initiatorRates: buildRateList(initiatorCount),
breakerRates: buildRateList(breakerCount),
chainLengthDistribution,
hotContents: top10HotContents,
avgChainLength: totalRepeatChains > 0 ? Math.round((totalChainLength / totalRepeatChains) * 100) / 100 : 0,
totalRepeatChains,
}
}
// ==================== 口头禅分析 ====================
/**
* 获取口头禅分析数据
*/
export function getCatchphraseAnalysis(sessionId: string, filter?: TimeFilter): any {
const db = openDatabase(sessionId)
if (!db) return { members: [] }
const { clause, params } = buildTimeFilter(filter)
let whereClause = clause
if (whereClause.includes('WHERE')) {
whereClause +=
" AND m.name != '系统消息' AND msg.type = 0 AND msg.content IS NOT NULL AND LENGTH(TRIM(msg.content)) >= 2"
} else {
whereClause =
" WHERE m.name != '系统消息' AND msg.type = 0 AND msg.content IS NOT NULL AND LENGTH(TRIM(msg.content)) >= 2"
}
const rows = db
.prepare(
`
SELECT
m.id as memberId,
m.platform_id as platformId,
m.name,
TRIM(msg.content) as content,
COUNT(*) as count
FROM message msg
JOIN member m ON msg.sender_id = m.id
${whereClause}
GROUP BY m.id, TRIM(msg.content)
ORDER BY m.id, count DESC
`
)
.all(...params) as Array<{
memberId: number
platformId: string
name: string
content: string
count: number
}>
const memberMap = new Map<
number,
{
memberId: number
platformId: string
name: string
catchphrases: Array<{ content: string; count: number }>
}
>()
for (const row of rows) {
if (!memberMap.has(row.memberId)) {
memberMap.set(row.memberId, {
memberId: row.memberId,
platformId: row.platformId,
name: row.name,
catchphrases: [],
})
}
const member = memberMap.get(row.memberId)!
if (member.catchphrases.length < 5) {
member.catchphrases.push({
content: row.content,
count: row.count,
})
}
}
const members = Array.from(memberMap.values())
members.sort((a, b) => {
const aTotal = a.catchphrases.reduce((sum, c) => sum + c.count, 0)
const bTotal = b.catchphrases.reduce((sum, c) => sum + c.count, 0)
return bTotal - aTotal
})
return { members }
}
// ==================== 夜猫分析 ====================
/**
* 根据深夜发言数获取称号
*/
function getNightOwlTitleByCount(count: number): string {
if (count === 0) return '养生达人'
if (count <= 20) return '偶尔失眠'
if (count <= 50) return '夜猫子'
if (count <= 100) return '秃头预备役'
if (count <= 200) return '修仙练习生'
if (count <= 500) return '守夜冠军'
return '不睡觉の神'
}
/**
* 将时间戳转换为"调整后的日期"(以凌晨5点为界)
*/
function getAdjustedDate(ts: number): string {
const date = new Date(ts * 1000)
const hour = date.getHours()
if (hour < 5) {
date.setDate(date.getDate() - 1)
}
return date.toISOString().split('T')[0]
}
/**
* 格式化分钟数为 HH:MM
*/
function formatMinutes(minutes: number): string {
const h = Math.floor(minutes / 60)
const m = Math.round(minutes % 60)
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`
}
/**
* 获取夜猫分析数据
*/
export function getNightOwlAnalysis(sessionId: string, filter?: TimeFilter): any {
const db = openDatabase(sessionId)
const emptyResult = {
nightOwlRank: [],
lastSpeakerRank: [],
firstSpeakerRank: [],
consecutiveRecords: [],
champions: [],
totalDays: 0,
}
if (!db) return emptyResult
const { clause, params } = buildTimeFilter(filter)
const clauseWithSystem = buildSystemMessageFilter(clause)
const messages = db
.prepare(
`
SELECT
msg.id,
msg.sender_id as senderId,
msg.ts,
m.platform_id as platformId,
m.name
FROM message msg
JOIN member m ON msg.sender_id = m.id
${clauseWithSystem}
ORDER BY msg.ts ASC
`
)
.all(...params) as Array<{
id: number
senderId: number
ts: number
platformId: string
name: string
}>
if (messages.length === 0) return emptyResult
const memberInfo = new Map<number, { platformId: string; name: string }>()
const nightStats = new Map<
number,
{
total: number
h23: number
h0: number
h1: number
h2: number
h3to4: number
totalMessages: number
}
>()
const dailyMessages = new Map<
string,
Array<{ senderId: number; ts: number; hour: number; minute: number }>
>()
const memberNightDays = new Map<number, Set<string>>()
for (const msg of messages) {
if (!memberInfo.has(msg.senderId)) {
memberInfo.set(msg.senderId, { platformId: msg.platformId, name: msg.name })
}
const date = new Date(msg.ts * 1000)
const hour = date.getHours()
const minute = date.getMinutes()
const adjustedDate = getAdjustedDate(msg.ts)
if (!nightStats.has(msg.senderId)) {
nightStats.set(msg.senderId, { total: 0, h23: 0, h0: 0, h1: 0, h2: 0, h3to4: 0, totalMessages: 0 })
}
const stats = nightStats.get(msg.senderId)!
stats.totalMessages++
if (hour === 23) {
stats.h23++
stats.total++
} else if (hour === 0) {
stats.h0++
stats.total++
} else if (hour === 1) {
stats.h1++
stats.total++
} else if (hour === 2) {
stats.h2++
stats.total++
} else if (hour >= 3 && hour < 5) {
stats.h3to4++
stats.total++
}
if (hour >= 23 || hour < 5) {
if (!memberNightDays.has(msg.senderId)) {
memberNightDays.set(msg.senderId, new Set())
}
memberNightDays.get(msg.senderId)!.add(adjustedDate)
}
if (!dailyMessages.has(adjustedDate)) {
dailyMessages.set(adjustedDate, [])
}
dailyMessages.get(adjustedDate)!.push({ senderId: msg.senderId, ts: msg.ts, hour, minute })
}
const totalDays = dailyMessages.size
// 构建修仙排行榜
const nightOwlRank: any[] = []
for (const [memberId, stats] of nightStats.entries()) {
if (stats.total === 0) continue
const info = memberInfo.get(memberId)!
nightOwlRank.push({
memberId,
platformId: info.platformId,
name: info.name,
totalNightMessages: stats.total,
title: getNightOwlTitleByCount(stats.total),
hourlyBreakdown: {
h23: stats.h23,
h0: stats.h0,
h1: stats.h1,
h2: stats.h2,
h3to4: stats.h3to4,
},
percentage: stats.totalMessages > 0 ? Math.round((stats.total / stats.totalMessages) * 10000) / 100 : 0,
})
}
nightOwlRank.sort((a, b) => b.totalNightMessages - a.totalNightMessages)
// 最晚/最早发言
const lastSpeakerStats = new Map<number, { count: number; times: number[] }>()
const firstSpeakerStats = new Map<number, { count: number; times: number[] }>()
for (const [, dayMessages] of dailyMessages.entries()) {
if (dayMessages.length === 0) continue
const lastMsg = dayMessages[dayMessages.length - 1]
if (!lastSpeakerStats.has(lastMsg.senderId)) {
lastSpeakerStats.set(lastMsg.senderId, { count: 0, times: [] })
}
const lastStats = lastSpeakerStats.get(lastMsg.senderId)!
lastStats.count++
lastStats.times.push(lastMsg.hour * 60 + lastMsg.minute)
const firstMsg = dayMessages[0]
if (!firstSpeakerStats.has(firstMsg.senderId)) {
firstSpeakerStats.set(firstMsg.senderId, { count: 0, times: [] })
}
const firstStats = firstSpeakerStats.get(firstMsg.senderId)!
firstStats.count++
firstStats.times.push(firstMsg.hour * 60 + firstMsg.minute)
}
// 构建排行
const lastSpeakerRank: any[] = []
for (const [memberId, stats] of lastSpeakerStats.entries()) {
const info = memberInfo.get(memberId)!
const avgMinutes = stats.times.reduce((a, b) => a + b, 0) / stats.times.length
const maxMinutes = Math.max(...stats.times)
lastSpeakerRank.push({
memberId,
platformId: info.platformId,
name: info.name,
count: stats.count,
avgTime: formatMinutes(avgMinutes),
extremeTime: formatMinutes(maxMinutes),
percentage: totalDays > 0 ? Math.round((stats.count / totalDays) * 10000) / 100 : 0,
})
}
lastSpeakerRank.sort((a, b) => b.count - a.count)
const firstSpeakerRank: any[] = []
for (const [memberId, stats] of firstSpeakerStats.entries()) {
const info = memberInfo.get(memberId)!
const avgMinutes = stats.times.reduce((a, b) => a + b, 0) / stats.times.length
const minMinutes = Math.min(...stats.times)
firstSpeakerRank.push({
memberId,
platformId: info.platformId,
name: info.name,
count: stats.count,
avgTime: formatMinutes(avgMinutes),
extremeTime: formatMinutes(minMinutes),
percentage: totalDays > 0 ? Math.round((stats.count / totalDays) * 10000) / 100 : 0,
})
}
firstSpeakerRank.sort((a, b) => b.count - a.count)
// 连续修仙天数
const consecutiveRecords: any[] = []
for (const [memberId, nightDaysSet] of memberNightDays.entries()) {
if (nightDaysSet.size === 0) continue
const info = memberInfo.get(memberId)!
const sortedDays = Array.from(nightDaysSet).sort()
let maxStreak = 1
let currentStreak = 1
for (let i = 1; i < sortedDays.length; i++) {
const prevDate = new Date(sortedDays[i - 1])
const currDate = new Date(sortedDays[i])
const diffDays = (currDate.getTime() - prevDate.getTime()) / (1000 * 60 * 60 * 24)
if (diffDays === 1) {
currentStreak++
maxStreak = Math.max(maxStreak, currentStreak)
} else {
currentStreak = 1
}
}
const lastDay = sortedDays[sortedDays.length - 1]
const today = new Date().toISOString().split('T')[0]
const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0]
const isCurrentStreak = lastDay === today || lastDay === yesterday
consecutiveRecords.push({
memberId,
platformId: info.platformId,
name: info.name,
maxConsecutiveDays: maxStreak,
currentStreak: isCurrentStreak ? currentStreak : 0,
})
}
consecutiveRecords.sort((a, b) => b.maxConsecutiveDays - a.maxConsecutiveDays)
// 综合排名
const championScores = new Map<number, { nightMessages: number; lastSpeakerCount: number; consecutiveDays: number }>()
for (const item of nightOwlRank) {
if (!championScores.has(item.memberId)) {
championScores.set(item.memberId, { nightMessages: 0, lastSpeakerCount: 0, consecutiveDays: 0 })
}
championScores.get(item.memberId)!.nightMessages = item.totalNightMessages
}
for (const item of lastSpeakerRank) {
if (!championScores.has(item.memberId)) {
championScores.set(item.memberId, { nightMessages: 0, lastSpeakerCount: 0, consecutiveDays: 0 })
}
championScores.get(item.memberId)!.lastSpeakerCount = item.count
}
for (const item of consecutiveRecords) {
if (!championScores.has(item.memberId)) {
championScores.set(item.memberId, { nightMessages: 0, lastSpeakerCount: 0, consecutiveDays: 0 })
}
championScores.get(item.memberId)!.consecutiveDays = item.maxConsecutiveDays
}
const champions: any[] = []
for (const [memberId, scores] of championScores.entries()) {
const info = memberInfo.get(memberId)!
const score = scores.nightMessages * 1 + scores.lastSpeakerCount * 10 + scores.consecutiveDays * 20
if (score > 0) {
champions.push({
memberId,
platformId: info.platformId,
name: info.name,
score,
nightMessages: scores.nightMessages,
lastSpeakerCount: scores.lastSpeakerCount,
consecutiveDays: scores.consecutiveDays,
})
}
}
champions.sort((a, b) => b.score - a.score)
return {
nightOwlRank,
lastSpeakerRank,
firstSpeakerRank,
consecutiveRecords,
champions,
totalDays,
}
}
// ==================== 龙王分析 ====================
/**
* 获取龙王排名
*/
export function getDragonKingAnalysis(sessionId: string, filter?: TimeFilter): any {
const db = openDatabase(sessionId)
const emptyResult = { rank: [], totalDays: 0 }
if (!db) return emptyResult
const { clause, params } = buildTimeFilter(filter)
const clauseWithSystem = buildSystemMessageFilter(clause)
const dailyTopSpeakers = db
.prepare(
`
WITH daily_counts AS (
SELECT
strftime('%Y-%m-%d', msg.ts, 'unixepoch', 'localtime') as date,
msg.sender_id,
m.platform_id,
m.name,
COUNT(*) as msg_count
FROM message msg
JOIN member m ON msg.sender_id = m.id
${clauseWithSystem}
GROUP BY date, msg.sender_id
),
daily_max AS (
SELECT date, MAX(msg_count) as max_count
FROM daily_counts
GROUP BY date
)
SELECT dc.sender_id, dc.platform_id, dc.name, COUNT(*) as dragon_days
FROM daily_counts dc
JOIN daily_max dm ON dc.date = dm.date AND dc.msg_count = dm.max_count
GROUP BY dc.sender_id
ORDER BY dragon_days DESC
`
)
.all(...params) as Array<{
sender_id: number
platform_id: string
name: string
dragon_days: number
}>
const totalDaysRow = db
.prepare(
`
SELECT COUNT(DISTINCT strftime('%Y-%m-%d', msg.ts, 'unixepoch', 'localtime')) as total
FROM message msg
JOIN member m ON msg.sender_id = m.id
${clauseWithSystem}
`
)
.get(...params) as { total: number }
const totalDays = totalDaysRow.total
const rank = dailyTopSpeakers.map((item) => ({
memberId: item.sender_id,
platformId: item.platform_id,
name: item.name,
count: item.dragon_days,
percentage: totalDays > 0 ? Math.round((item.dragon_days / totalDays) * 10000) / 100 : 0,
}))
return { rank, totalDays }
}
// ==================== 潜水分析 ====================
/**
* 获取潜水排名
*/
export function getDivingAnalysis(sessionId: string, filter?: TimeFilter): any {
const db = openDatabase(sessionId)
const emptyResult = { rank: [] }
if (!db) return emptyResult
const { clause, params } = buildTimeFilter(filter)
const clauseWithSystem = buildSystemMessageFilter(clause)
const lastMessages = db
.prepare(
`
SELECT
m.id as member_id,
m.platform_id,
m.name,
MAX(msg.ts) as last_ts
FROM member m
JOIN message msg ON m.id = msg.sender_id
${clauseWithSystem.replace('msg.', 'msg.')}
GROUP BY m.id
ORDER BY last_ts ASC
`
)
.all(...params) as Array<{
member_id: number
platform_id: string
name: string
last_ts: number
}>
const now = Math.floor(Date.now() / 1000)
const rank = lastMessages.map((item) => ({
memberId: item.member_id,
platformId: item.platform_id,
name: item.name,
lastMessageTs: item.last_ts,
daysSinceLastMessage: Math.floor((now - item.last_ts) / 86400),
}))
return { rank }
}
// ==================== 自言自语分析 ====================
/**
* 获取自言自语分析
*/
export function getMonologueAnalysis(sessionId: string, filter?: TimeFilter): any {
const db = openDatabase(sessionId)
const emptyResult = { rank: [], maxComboRecord: null }
if (!db) return emptyResult
const { clause, params } = buildTimeFilter(filter)
let whereClause = clause
if (whereClause.includes('WHERE')) {
whereClause += " AND m.name != '系统消息' AND msg.type = 0"
} else {
whereClause = " WHERE m.name != '系统消息' AND msg.type = 0"
}
const messages = db
.prepare(
`
SELECT
msg.id,
msg.sender_id as senderId,
msg.ts,
m.platform_id as platformId,
m.name
FROM message msg
JOIN member m ON msg.sender_id = m.id
${whereClause}
ORDER BY msg.ts ASC
`
)
.all(...params) as Array<{
id: number
senderId: number
ts: number
platformId: string
name: string
}>
if (messages.length === 0) return emptyResult
const memberInfo = new Map<number, { platformId: string; name: string }>()
const memberStats = new Map<
number,
{
totalStreaks: number
maxCombo: number
lowStreak: number
midStreak: number
highStreak: number
}
>()
let globalMaxCombo: { memberId: number; comboLength: number; startTs: number } | null = null
const MAX_INTERVAL = 300
let currentStreak = {
senderId: -1,
count: 0,
startTs: 0,
lastTs: 0,
}
const finishStreak = () => {
if (currentStreak.count >= 3) {
const memberId = currentStreak.senderId
if (!memberStats.has(memberId)) {
memberStats.set(memberId, {
totalStreaks: 0,
maxCombo: 0,
lowStreak: 0,
midStreak: 0,
highStreak: 0,
})
}
const stats = memberStats.get(memberId)!
stats.totalStreaks++
stats.maxCombo = Math.max(stats.maxCombo, currentStreak.count)
if (currentStreak.count >= 10) {
stats.highStreak++
} else if (currentStreak.count >= 5) {
stats.midStreak++
} else {
stats.lowStreak++
}
if (!globalMaxCombo || currentStreak.count > globalMaxCombo.comboLength) {
globalMaxCombo = {
memberId,
comboLength: currentStreak.count,
startTs: currentStreak.startTs,
}
}
}
}
for (const msg of messages) {
if (!memberInfo.has(msg.senderId)) {
memberInfo.set(msg.senderId, { platformId: msg.platformId, name: msg.name })
}
const isSameSender = msg.senderId === currentStreak.senderId
const isWithinInterval = msg.ts - currentStreak.lastTs <= MAX_INTERVAL
if (isSameSender && isWithinInterval) {
currentStreak.count++
currentStreak.lastTs = msg.ts
} else {
finishStreak()
currentStreak = {
senderId: msg.senderId,
count: 1,
startTs: msg.ts,
lastTs: msg.ts,
}
}
}
finishStreak()
const rank: any[] = []
for (const [memberId, stats] of memberStats.entries()) {
const info = memberInfo.get(memberId)!
rank.push({
memberId,
platformId: info.platformId,
name: info.name,
totalStreaks: stats.totalStreaks,
maxCombo: stats.maxCombo,
lowStreak: stats.lowStreak,
midStreak: stats.midStreak,
highStreak: stats.highStreak,
})
}
rank.sort((a, b) => b.totalStreaks - a.totalStreaks)
let maxComboRecord: any = null
if (globalMaxCombo) {
const info = memberInfo.get(globalMaxCombo.memberId)!
maxComboRecord = {
memberId: globalMaxCombo.memberId,
platformId: info.platformId,
memberName: info.name,
comboLength: globalMaxCombo.comboLength,
startTs: globalMaxCombo.startTs,
}
}
return { rank, maxComboRecord }
}
+389
View File
@@ -0,0 +1,389 @@
/**
* 基础查询模块
* 提供活跃度、时段分布等基础统计查询
*/
import Database from 'better-sqlite3'
import * as fs from 'fs'
import * as path from 'path'
import {
openDatabase,
getDbDir,
getDbPath,
buildTimeFilter,
buildSystemMessageFilter,
type TimeFilter,
} from './dbCore'
// ==================== 基础查询 ====================
/**
* 获取可用的年份列表
*/
export function getAvailableYears(sessionId: string): number[] {
const db = openDatabase(sessionId)
if (!db) return []
const rows = db
.prepare(
`
SELECT DISTINCT CAST(strftime('%Y', ts, 'unixepoch', 'localtime') AS INTEGER) as year
FROM message
ORDER BY year DESC
`
)
.all() as Array<{ year: number }>
return rows.map((r) => r.year)
}
/**
* 获取成员活跃度排行
*/
export function getMemberActivity(sessionId: string, filter?: TimeFilter): any[] {
const db = openDatabase(sessionId)
if (!db) return []
const { clause, params } = buildTimeFilter(filter)
const msgFilterBase = clause ? clause.replace('WHERE', 'AND') : ''
const msgFilterWithSystem = msgFilterBase + " AND m.name != '系统消息'"
const totalClauseWithSystem = buildSystemMessageFilter(clause)
const totalMessages = (
db
.prepare(
`SELECT COUNT(*) as count
FROM message msg
JOIN member m ON msg.sender_id = m.id
${totalClauseWithSystem}`
)
.get(...params) as { count: number }
).count
const rows = db
.prepare(
`
SELECT
m.id as memberId,
m.platform_id as platformId,
m.name,
COUNT(msg.id) as messageCount
FROM member m
LEFT JOIN message msg ON m.id = msg.sender_id ${msgFilterWithSystem}
WHERE m.name != '系统消息'
GROUP BY m.id
HAVING messageCount > 0
ORDER BY messageCount DESC
`
)
.all(...params) as Array<{
memberId: number
platformId: string
name: string
messageCount: number
}>
return rows.map((row) => ({
memberId: row.memberId,
platformId: row.platformId,
name: row.name,
messageCount: row.messageCount,
percentage: totalMessages > 0 ? Math.round((row.messageCount / totalMessages) * 10000) / 100 : 0,
}))
}
/**
* 获取每小时活跃度分布
*/
export function getHourlyActivity(sessionId: string, filter?: TimeFilter): any[] {
const db = openDatabase(sessionId)
if (!db) return []
const { clause, params } = buildTimeFilter(filter)
const clauseWithSystem = buildSystemMessageFilter(clause)
const rows = db
.prepare(
`
SELECT
CAST(strftime('%H', msg.ts, 'unixepoch', 'localtime') AS INTEGER) as hour,
COUNT(*) as messageCount
FROM message msg
JOIN member m ON msg.sender_id = m.id
${clauseWithSystem}
GROUP BY hour
ORDER BY hour
`
)
.all(...params) as Array<{ hour: number; messageCount: number }>
const result: any[] = []
for (let h = 0; h < 24; h++) {
const found = rows.find((r) => r.hour === h)
result.push({
hour: h,
messageCount: found ? found.messageCount : 0,
})
}
return result
}
/**
* 获取每日活跃度趋势
*/
export function getDailyActivity(sessionId: string, filter?: TimeFilter): any[] {
const db = openDatabase(sessionId)
if (!db) return []
const { clause, params } = buildTimeFilter(filter)
const clauseWithSystem = buildSystemMessageFilter(clause)
const rows = db
.prepare(
`
SELECT
strftime('%Y-%m-%d', msg.ts, 'unixepoch', 'localtime') as date,
COUNT(*) as messageCount
FROM message msg
JOIN member m ON msg.sender_id = m.id
${clauseWithSystem}
GROUP BY date
ORDER BY date
`
)
.all(...params) as Array<{ date: string; messageCount: number }>
return rows
}
/**
* 获取星期活跃度分布
*/
export function getWeekdayActivity(sessionId: string, filter?: TimeFilter): any[] {
const db = openDatabase(sessionId)
if (!db) return []
const { clause, params } = buildTimeFilter(filter)
const clauseWithSystem = buildSystemMessageFilter(clause)
const rows = db
.prepare(
`
SELECT
CASE
WHEN CAST(strftime('%w', msg.ts, 'unixepoch', 'localtime') AS INTEGER) = 0 THEN 7
ELSE CAST(strftime('%w', msg.ts, 'unixepoch', 'localtime') AS INTEGER)
END as weekday,
COUNT(*) as messageCount
FROM message msg
JOIN member m ON msg.sender_id = m.id
${clauseWithSystem}
GROUP BY weekday
ORDER BY weekday
`
)
.all(...params) as Array<{ weekday: number; messageCount: number }>
const result: any[] = []
for (let w = 1; w <= 7; w++) {
const found = rows.find((r) => r.weekday === w)
result.push({
weekday: w,
messageCount: found ? found.messageCount : 0,
})
}
return result
}
/**
* 获取消息类型分布
*/
export function getMessageTypeDistribution(sessionId: string, filter?: TimeFilter): any[] {
const db = openDatabase(sessionId)
if (!db) return []
const { clause, params } = buildTimeFilter(filter)
const clauseWithSystem = buildSystemMessageFilter(clause)
const rows = db
.prepare(
`
SELECT msg.type, COUNT(*) as count
FROM message msg
JOIN member m ON msg.sender_id = m.id
${clauseWithSystem}
GROUP BY msg.type
ORDER BY count DESC
`
)
.all(...params) as Array<{ type: number; count: number }>
return rows.map((r) => ({
type: r.type,
count: r.count,
}))
}
/**
* 获取时间范围
*/
export function getTimeRange(sessionId: string): { start: number; end: number } | null {
const db = openDatabase(sessionId)
if (!db) return null
const row = db
.prepare(
`
SELECT MIN(ts) as start, MAX(ts) as end FROM message
`
)
.get() as { start: number | null; end: number | null }
if (row.start === null || row.end === null) return null
return { start: row.start, end: row.end }
}
/**
* 获取成员的历史昵称记录
*/
export function getMemberNameHistory(
sessionId: string,
memberId: number
): any[] {
const db = openDatabase(sessionId)
if (!db) return []
const rows = db
.prepare(
`
SELECT name, start_ts as startTs, end_ts as endTs
FROM member_name_history
WHERE member_id = ?
ORDER BY start_ts DESC
`
)
.all(memberId) as Array<{ name: string; startTs: number; endTs: number | null }>
return rows
}
// ==================== 会话管理 ====================
interface DbMeta {
name: string
platform: string
type: string
imported_at: number
}
/**
* 获取所有会话列表
*/
export function getAllSessions(): any[] {
const dbDir = getDbDir()
if (!fs.existsSync(dbDir)) {
return []
}
const sessions: any[] = []
const files = fs.readdirSync(dbDir).filter((f) => f.endsWith('.db'))
for (const file of files) {
const sessionId = file.replace('.db', '')
const dbPath = path.join(dbDir, file)
try {
const db = new Database(dbPath)
db.pragma('journal_mode = WAL')
const meta = db.prepare('SELECT * FROM meta LIMIT 1').get() as DbMeta | undefined
if (meta) {
const messageCount = (
db
.prepare(
`SELECT COUNT(*) as count
FROM message msg
JOIN member m ON msg.sender_id = m.id
WHERE m.name != '系统消息'`
)
.get() as { count: number }
).count
const memberCount = (
db
.prepare(
`SELECT COUNT(*) as count
FROM member
WHERE name != '系统消息'`
)
.get() as { count: number }
).count
sessions.push({
id: sessionId,
name: meta.name,
platform: meta.platform,
type: meta.type,
importedAt: meta.imported_at,
messageCount,
memberCount,
dbPath,
})
}
db.close()
} catch (error) {
console.error(`[Worker] Failed to read database ${file}:`, error)
}
}
return sessions.sort((a, b) => b.importedAt - a.importedAt)
}
/**
* 获取单个会话信息
*/
export function getSession(sessionId: string): any | null {
const db = openDatabase(sessionId)
if (!db) return null
const meta = db.prepare('SELECT * FROM meta LIMIT 1').get() as DbMeta | undefined
if (!meta) return null
const messageCount = (
db
.prepare(
`SELECT COUNT(*) as count
FROM message msg
JOIN member m ON msg.sender_id = m.id
WHERE m.name != '系统消息'`
)
.get() as { count: number }
).count
const memberCount = (
db
.prepare(
`SELECT COUNT(*) as count
FROM member
WHERE name != '系统消息'`
)
.get() as { count: number }
).count
return {
id: sessionId,
name: meta.name,
platform: meta.platform,
type: meta.type,
importedAt: meta.imported_at,
messageCount,
memberCount,
dbPath: getDbPath(sessionId),
}
}
+248
View File
@@ -0,0 +1,248 @@
/**
* Worker 管理器
* 负责创建、管理 Worker 线程,并处理与主进程的通信
*/
import { Worker } from 'worker_threads'
import { app } from 'electron'
import * as path from 'path'
import * as fs from 'fs'
// Worker 实例
let worker: Worker | null = null
// 等待中的请求 Map
const pendingRequests = new Map<
string,
{
resolve: (value: any) => void
reject: (error: Error) => void
}
>()
// 请求 ID 计数器
let requestIdCounter = 0
// 数据库目录
let dbDir: string | null = null
/**
* 获取数据库目录
*/
function getDbDir(): string {
if (dbDir) return dbDir
try {
const docPath = app.getPath('documents')
dbDir = path.join(docPath, 'ChatLab', 'databases')
} catch (error) {
console.error('[WorkerManager] Error getting documents path:', error)
dbDir = path.join(process.cwd(), 'databases')
}
// 确保目录存在
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true })
}
return dbDir
}
/**
* 获取 Worker 文件路径
* 开发环境和生产环境路径不同
*/
function getWorkerPath(): string {
// 检查是否在开发环境
const isDev = !app.isPackaged
if (isDev) {
// 开发环境:编译后的 JS 文件在 out/main 目录
return path.join(__dirname, 'worker', 'dbWorker.js')
} else {
// 生产环境:打包后的路径
return path.join(__dirname, 'worker', 'dbWorker.js')
}
}
/**
* 初始化 Worker
*/
export function initWorker(): void {
if (worker) {
console.log('[WorkerManager] Worker already initialized')
return
}
const workerPath = getWorkerPath()
console.log('[WorkerManager] Initializing worker at:', workerPath)
try {
worker = new Worker(workerPath, {
workerData: {
dbDir: getDbDir(),
},
})
// 监听 Worker 消息
worker.on('message', (message) => {
const { id, success, result, error } = message
const pending = pendingRequests.get(id)
if (pending) {
pendingRequests.delete(id)
if (success) {
pending.resolve(result)
} else {
pending.reject(new Error(error))
}
}
})
// 监听 Worker 错误
worker.on('error', (error) => {
console.error('[WorkerManager] Worker error:', error)
})
// 监听 Worker 退出
worker.on('exit', (code) => {
console.log('[WorkerManager] Worker exited with code:', code)
worker = null
// 拒绝所有等待中的请求
for (const [id, pending] of pendingRequests.entries()) {
pending.reject(new Error('Worker exited unexpectedly'))
pendingRequests.delete(id)
}
})
console.log('[WorkerManager] Worker initialized successfully')
} catch (error) {
console.error('[WorkerManager] Failed to initialize worker:', error)
throw error
}
}
/**
* 发送消息到 Worker 并等待响应
*/
function sendToWorker<T>(type: string, payload: any): Promise<T> {
return new Promise((resolve, reject) => {
if (!worker) {
// 尝试初始化 Worker
try {
initWorker()
} catch (error) {
reject(new Error('Worker not initialized'))
return
}
}
const id = `req_${++requestIdCounter}`
pendingRequests.set(id, { resolve, reject })
worker!.postMessage({ id, type, payload })
// 设置超时(30秒)
setTimeout(() => {
if (pendingRequests.has(id)) {
pendingRequests.delete(id)
reject(new Error(`Worker request timeout: ${type}`))
}
}, 30000)
})
}
/**
* 关闭 Worker
*/
export function closeWorker(): void {
if (worker) {
// 先关闭所有数据库连接
sendToWorker('closeAll', {}).catch(() => {})
worker.terminate()
worker = null
console.log('[WorkerManager] Worker terminated')
}
}
// ==================== 导出的异步 API ====================
export async function getAvailableYears(sessionId: string): Promise<number[]> {
return sendToWorker('getAvailableYears', { sessionId })
}
export async function getMemberActivity(sessionId: string, filter?: any): Promise<any[]> {
return sendToWorker('getMemberActivity', { sessionId, filter })
}
export async function getHourlyActivity(sessionId: string, filter?: any): Promise<any[]> {
return sendToWorker('getHourlyActivity', { sessionId, filter })
}
export async function getDailyActivity(sessionId: string, filter?: any): Promise<any[]> {
return sendToWorker('getDailyActivity', { sessionId, filter })
}
export async function getWeekdayActivity(sessionId: string, filter?: any): Promise<any[]> {
return sendToWorker('getWeekdayActivity', { sessionId, filter })
}
export async function getMessageTypeDistribution(sessionId: string, filter?: any): Promise<any[]> {
return sendToWorker('getMessageTypeDistribution', { sessionId, filter })
}
export async function getTimeRange(sessionId: string): Promise<{ start: number; end: number } | null> {
return sendToWorker('getTimeRange', { sessionId })
}
export async function getMemberNameHistory(sessionId: string, memberId: number): Promise<any[]> {
return sendToWorker('getMemberNameHistory', { sessionId, memberId })
}
export async function getRepeatAnalysis(sessionId: string, filter?: any): Promise<any> {
return sendToWorker('getRepeatAnalysis', { sessionId, filter })
}
export async function getCatchphraseAnalysis(sessionId: string, filter?: any): Promise<any> {
return sendToWorker('getCatchphraseAnalysis', { sessionId, filter })
}
export async function getNightOwlAnalysis(sessionId: string, filter?: any): Promise<any> {
return sendToWorker('getNightOwlAnalysis', { sessionId, filter })
}
export async function getDragonKingAnalysis(sessionId: string, filter?: any): Promise<any> {
return sendToWorker('getDragonKingAnalysis', { sessionId, filter })
}
export async function getDivingAnalysis(sessionId: string, filter?: any): Promise<any> {
return sendToWorker('getDivingAnalysis', { sessionId, filter })
}
export async function getMonologueAnalysis(sessionId: string, filter?: any): Promise<any> {
return sendToWorker('getMonologueAnalysis', { sessionId, filter })
}
export async function getAllSessions(): Promise<any[]> {
return sendToWorker('getAllSessions', {})
}
export async function getSession(sessionId: string): Promise<any | null> {
return sendToWorker('getSession', { sessionId })
}
export async function closeDatabase(sessionId: string): Promise<void> {
return sendToWorker('closeDatabase', { sessionId })
}
/**
* 获取数据库目录(供外部使用)
*/
export function getDbDirectory(): string {
return getDbDir()
}