mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-11 16:41:02 +08:00
feat: 重构架构,新增Worker查询线程
This commit is contained in:
+37
-23
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
@@ -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'
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user