Files
ChatLab/electron/main/worker/query/advanced/behavior.ts
2026-02-02 01:00:15 +08:00

195 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 行为分析模块
* 包含:斗图分析
*/
import { openDatabase, buildTimeFilter, type TimeFilter } from '../../core'
// ==================== 斗图分析 ====================
/**
* 获取斗图分析数据
* 斗图定义至少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,
COALESCE(m.group_nickname, m.account_name, m.platform_id) as 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,
}
}