mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-14 10:29:15 +08:00
feat: 新增斗图榜
This commit is contained in:
@@ -536,6 +536,26 @@ const mainIpcMain = (win: BrowserWindow) => {
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取斗图分析数据
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'chat:getMemeBattleAnalysis',
|
||||
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
|
||||
try {
|
||||
return await worker.getMemeBattleAnalysis(sessionId, filter)
|
||||
} catch (error) {
|
||||
console.error('获取斗图分析失败:', error)
|
||||
return {
|
||||
longestBattle: null,
|
||||
rankByCount: [],
|
||||
rankByImageCount: [],
|
||||
totalBattles: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export default mainIpcMain
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
getMonologueAnalysis,
|
||||
getMentionAnalysis,
|
||||
getLaughAnalysis,
|
||||
getMemeBattleAnalysis,
|
||||
} from './queryAdvanced'
|
||||
|
||||
// 初始化数据库目录
|
||||
@@ -80,6 +81,7 @@ const handlers: Record<string, (payload: any) => any> = {
|
||||
getMonologueAnalysis: (p) => getMonologueAnalysis(p.sessionId, p.filter),
|
||||
getMentionAnalysis: (p) => getMentionAnalysis(p.sessionId, p.filter),
|
||||
getLaughAnalysis: (p) => getLaughAnalysis(p.sessionId, p.filter, p.keywords),
|
||||
getMemeBattleAnalysis: (p) => getMemeBattleAnalysis(p.sessionId, p.filter),
|
||||
}
|
||||
|
||||
// 处理消息
|
||||
|
||||
@@ -25,6 +25,7 @@ export {
|
||||
getMonologueAnalysis,
|
||||
getMentionAnalysis,
|
||||
getLaughAnalysis,
|
||||
getMemeBattleAnalysis,
|
||||
// 会话管理 API(异步)
|
||||
getAllSessions,
|
||||
getSession,
|
||||
|
||||
@@ -1434,3 +1434,191 @@ export function getLaughAnalysis(sessionId: string, filter?: TimeFilter, keyword
|
||||
groupLaughRate: Math.round((totalLaughs / totalMessages) * 10000) / 100,
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 斗图分析 ====================
|
||||
|
||||
/**
|
||||
* 获取斗图分析数据
|
||||
* 斗图定义:至少2人参与,总共发了3张图(图片或表情),中间无文本打断
|
||||
*/
|
||||
export function getMemeBattleAnalysis(sessionId: string, filter?: TimeFilter): any {
|
||||
const db = openDatabase(sessionId)
|
||||
const emptyResult = {
|
||||
topBattles: [],
|
||||
rankByCount: [],
|
||||
rankByImageCount: [],
|
||||
totalBattles: 0,
|
||||
}
|
||||
|
||||
if (!db) return emptyResult
|
||||
|
||||
const { clause, params } = buildTimeFilter(filter)
|
||||
|
||||
// 排除系统消息 (type=6)
|
||||
// 斗图只看图片(1)和表情(5),其他类型(如文本0, 语音2等)视为打断
|
||||
// 我们查询所有非系统消息,在内存中遍历判断
|
||||
let whereClause = clause
|
||||
if (whereClause.includes('WHERE')) {
|
||||
whereClause += ' AND msg.type != 6'
|
||||
} else {
|
||||
whereClause = ' WHERE msg.type != 6'
|
||||
}
|
||||
|
||||
const messages = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
msg.sender_id as senderId,
|
||||
msg.type,
|
||||
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<{
|
||||
senderId: number
|
||||
type: number
|
||||
ts: number
|
||||
platformId: string
|
||||
name: string
|
||||
}>
|
||||
|
||||
const battles: Array<{
|
||||
startTime: number
|
||||
endTime: number
|
||||
msgs: Array<{ senderId: number; name: string; platformId: string }>
|
||||
}> = []
|
||||
|
||||
let currentChain: Array<{ senderId: number; name: string; platformId: string; ts: number }> = []
|
||||
|
||||
// 辅助函数:处理当前链
|
||||
const processChain = () => {
|
||||
if (currentChain.length >= 3) {
|
||||
const senders = new Set(currentChain.map((m) => m.senderId))
|
||||
if (senders.size >= 2) {
|
||||
// 满足条件:至少3张图,至少2人
|
||||
battles.push({
|
||||
startTime: currentChain[0].ts,
|
||||
endTime: currentChain[currentChain.length - 1].ts,
|
||||
msgs: currentChain.map(({ senderId, name, platformId }) => ({ senderId, name, platformId })),
|
||||
})
|
||||
}
|
||||
}
|
||||
currentChain = []
|
||||
}
|
||||
|
||||
for (const msg of messages) {
|
||||
// 1=图片, 5=表情
|
||||
if (msg.type === 1 || msg.type === 5) {
|
||||
currentChain.push({
|
||||
senderId: msg.senderId,
|
||||
name: msg.name,
|
||||
platformId: msg.platformId,
|
||||
ts: msg.ts,
|
||||
})
|
||||
} else {
|
||||
// 其他类型消息(文本、语音等)打断斗图
|
||||
processChain()
|
||||
}
|
||||
}
|
||||
// 处理最后一条链
|
||||
processChain()
|
||||
|
||||
if (battles.length === 0) return emptyResult
|
||||
|
||||
// 1. 史诗级斗图榜(前30)
|
||||
const topBattles = battles
|
||||
.map((battle) => ({
|
||||
startTime: battle.startTime,
|
||||
endTime: battle.endTime,
|
||||
totalImages: battle.msgs.length,
|
||||
participantCount: new Set(battle.msgs.map((m) => m.senderId)).size,
|
||||
participants: Object.values(
|
||||
battle.msgs.reduce(
|
||||
(acc, curr) => {
|
||||
if (!acc[curr.senderId]) {
|
||||
acc[curr.senderId] = { memberId: curr.senderId, name: curr.name, imageCount: 0 }
|
||||
}
|
||||
acc[curr.senderId].imageCount++
|
||||
return acc
|
||||
},
|
||||
{} as Record<number, { memberId: number; name: string; imageCount: number }>
|
||||
)
|
||||
).sort((a, b) => b.imageCount - a.imageCount),
|
||||
}))
|
||||
.sort((a, b) => b.totalImages - a.totalImages)
|
||||
.slice(0, 30)
|
||||
|
||||
// 2. 统计达人榜
|
||||
const memberStats = new Map<
|
||||
number,
|
||||
{
|
||||
memberId: number
|
||||
platformId: string
|
||||
name: string
|
||||
battleCount: number // 参与场次
|
||||
imageCount: number // 发图总数
|
||||
}
|
||||
>()
|
||||
|
||||
for (const battle of battles) {
|
||||
const participantsInBattle = new Set<number>()
|
||||
|
||||
for (const msg of battle.msgs) {
|
||||
if (!memberStats.has(msg.senderId)) {
|
||||
memberStats.set(msg.senderId, {
|
||||
memberId: msg.senderId,
|
||||
platformId: msg.platformId,
|
||||
name: msg.name,
|
||||
battleCount: 0,
|
||||
imageCount: 0,
|
||||
})
|
||||
}
|
||||
const stats = memberStats.get(msg.senderId)!
|
||||
stats.imageCount++
|
||||
participantsInBattle.add(msg.senderId)
|
||||
}
|
||||
|
||||
// 参与场次+1
|
||||
for (const memberId of participantsInBattle) {
|
||||
const stats = memberStats.get(memberId)!
|
||||
stats.battleCount++
|
||||
}
|
||||
}
|
||||
|
||||
const allStats = Array.from(memberStats.values())
|
||||
|
||||
// 按参与场次排名
|
||||
const rankByCount = [...allStats]
|
||||
.sort((a, b) => b.battleCount - a.battleCount)
|
||||
.map((item) => ({
|
||||
memberId: item.memberId,
|
||||
platformId: item.platformId,
|
||||
name: item.name,
|
||||
count: item.battleCount,
|
||||
percentage: battles.length > 0 ? Math.round((item.battleCount / battles.length) * 10000) / 100 : 0,
|
||||
}))
|
||||
|
||||
// 按图片总数排名
|
||||
const totalBattleImages = battles.reduce((sum, b) => sum + b.msgs.length, 0)
|
||||
const rankByImageCount = [...allStats]
|
||||
.sort((a, b) => b.imageCount - a.imageCount)
|
||||
.map((item) => ({
|
||||
memberId: item.memberId,
|
||||
platformId: item.platformId,
|
||||
name: item.name,
|
||||
count: item.imageCount,
|
||||
percentage: totalBattleImages > 0 ? Math.round((item.imageCount / totalBattleImages) * 10000) / 100 : 0,
|
||||
}))
|
||||
|
||||
return {
|
||||
topBattles,
|
||||
rankByCount,
|
||||
rankByImageCount,
|
||||
totalBattles: battles.length,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,6 +239,10 @@ export async function getLaughAnalysis(sessionId: string, filter?: any, keywords
|
||||
return sendToWorker('getLaughAnalysis', { sessionId, filter, keywords })
|
||||
}
|
||||
|
||||
export async function getMemeBattleAnalysis(sessionId: string, filter?: any): Promise<any> {
|
||||
return sendToWorker('getMemeBattleAnalysis', { sessionId, filter })
|
||||
}
|
||||
|
||||
export async function getAllSessions(): Promise<any[]> {
|
||||
return sendToWorker('getAllSessions', {})
|
||||
}
|
||||
|
||||
Vendored
+2
@@ -17,6 +17,7 @@ import type {
|
||||
MonologueAnalysis,
|
||||
MentionAnalysis,
|
||||
LaughAnalysis,
|
||||
MemeBattleAnalysis,
|
||||
} from '../../src/types/chat'
|
||||
|
||||
interface TimeFilter {
|
||||
@@ -53,6 +54,7 @@ interface ChatApi {
|
||||
getMonologueAnalysis: (sessionId: string, filter?: TimeFilter) => Promise<MonologueAnalysis>
|
||||
getMentionAnalysis: (sessionId: string, filter?: TimeFilter) => Promise<MentionAnalysis>
|
||||
getLaughAnalysis: (sessionId: string, filter?: TimeFilter, keywords?: string[]) => Promise<LaughAnalysis>
|
||||
getMemeBattleAnalysis: (sessionId: string, filter?: TimeFilter) => Promise<MemeBattleAnalysis>
|
||||
}
|
||||
|
||||
interface Api {
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
MonologueAnalysis,
|
||||
MentionAnalysis,
|
||||
LaughAnalysis,
|
||||
MemeBattleAnalysis,
|
||||
} from '../../src/types/chat'
|
||||
|
||||
// Custom APIs for renderer
|
||||
@@ -254,6 +255,16 @@ const chatApi = {
|
||||
): Promise<LaughAnalysis> => {
|
||||
return ipcRenderer.invoke('chat:getLaughAnalysis', sessionId, filter, keywords)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取斗图分析数据
|
||||
*/
|
||||
getMemeBattleAnalysis: (
|
||||
sessionId: string,
|
||||
filter?: { startTs?: number; endTs?: number }
|
||||
): Promise<MemeBattleAnalysis> => {
|
||||
return ipcRenderer.invoke('chat:getMemeBattleAnalysis', sessionId, filter)
|
||||
},
|
||||
}
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
|
||||
Reference in New Issue
Block a user