mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-04-29 00:03:07 +08:00
feat: 新增自言自语榜
This commit is contained in:
@@ -25,6 +25,9 @@ import type {
|
||||
DragonKingRankItem,
|
||||
DivingAnalysis,
|
||||
DivingRankItem,
|
||||
MonologueAnalysis,
|
||||
MonologueRankItem,
|
||||
MaxComboRecord,
|
||||
} from '../../../src/types/chat'
|
||||
import { openDatabase } from './core'
|
||||
|
||||
@@ -1151,3 +1154,189 @@ export function getDivingAnalysis(sessionId: string, filter?: TimeFilter): Divin
|
||||
db.close()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取自言自语分析
|
||||
* 定义:一个人连续发言超过三次(间隔<=5分钟)
|
||||
* 分类:3-4句(加特林)、5-9句(小作文)、10+句(无人区广播)
|
||||
*/
|
||||
export function getMonologueAnalysis(sessionId: string, filter?: TimeFilter): MonologueAnalysis {
|
||||
const db = openDatabase(sessionId)
|
||||
const emptyResult: MonologueAnalysis = {
|
||||
rank: [],
|
||||
maxComboRecord: null,
|
||||
}
|
||||
|
||||
if (!db) return emptyResult
|
||||
|
||||
try {
|
||||
const { clause, params } = buildTimeFilter(filter)
|
||||
|
||||
// 构建 WHERE 子句:只统计文本消息
|
||||
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 }>()
|
||||
|
||||
// 连击统计:memberId -> { totalStreaks, maxCombo, lowStreak, midStreak, highStreak }
|
||||
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
|
||||
|
||||
// 连击间隔限制:5分钟 = 300秒
|
||||
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)
|
||||
|
||||
// 分类统计:3-4句、5-9句、10+句
|
||||
if (currentStreak.count >= 10) {
|
||||
stats.highStreak++
|
||||
} else if (currentStreak.count >= 5) {
|
||||
stats.midStreak++
|
||||
} else {
|
||||
stats.lowStreak++ // 3-4句
|
||||
}
|
||||
|
||||
// 更新全群最高纪录
|
||||
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: MonologueRankItem[] = []
|
||||
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: MaxComboRecord | null = 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 }
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export {
|
||||
getNightOwlAnalysis,
|
||||
getDragonKingAnalysis,
|
||||
getDivingAnalysis,
|
||||
getMonologueAnalysis,
|
||||
} from './analysis'
|
||||
|
||||
// 类型导出
|
||||
|
||||
Reference in New Issue
Block a user