feat: 新增聊天记录查看器

This commit is contained in:
digua
2025-12-15 10:12:52 +08:00
parent 84af222b25
commit 266f8644e1
20 changed files with 1385 additions and 48 deletions

View File

@@ -70,17 +70,17 @@ export function getRepeatAnalysis(sessionId: string, filter?: TimeFilter): any {
const chainLengthCount = new Map<number, number>()
const contentStats = new Map<
string,
{ count: number; maxChainLength: number; originatorId: number; lastTs: number }
{ count: number; maxChainLength: number; originatorId: number; lastTs: number; firstMessageId: number }
>()
let currentContent: string | null = null
let repeatChain: Array<{ senderId: number; content: string; ts: number }> = []
let repeatChain: Array<{ id: number; senderId: number; content: string; ts: number }> = []
let totalRepeatChains = 0
let totalChainLength = 0
const fastestRepeaterStats = new Map<number, { totalDiff: number; count: number }>()
const processRepeatChain = (chain: Array<{ senderId: number; content: string; ts: number }>, breakerId?: number) => {
const processRepeatChain = (chain: Array<{ id: number; senderId: number; content: string; ts: number }>, breakerId?: number) => {
if (chain.length < 3) return
totalRepeatChains++
@@ -101,6 +101,7 @@ export function getRepeatAnalysis(sessionId: string, filter?: TimeFilter): any {
const content = chain[0].content
const chainTs = chain[0].ts
const firstMsgId = chain[0].id
const existing = contentStats.get(content)
if (existing) {
existing.count++
@@ -108,9 +109,10 @@ export function getRepeatAnalysis(sessionId: string, filter?: TimeFilter): any {
if (chainLength > existing.maxChainLength) {
existing.maxChainLength = chainLength
existing.originatorId = originatorId
existing.firstMessageId = firstMsgId
}
} else {
contentStats.set(content, { count: 1, maxChainLength: chainLength, originatorId, lastTs: chainTs })
contentStats.set(content, { count: 1, maxChainLength: chainLength, originatorId, lastTs: chainTs, firstMessageId: firstMsgId })
}
// 计算反应时间 (Fastest Follower)
@@ -144,13 +146,13 @@ export function getRepeatAnalysis(sessionId: string, filter?: TimeFilter): any {
if (content === currentContent) {
const lastSender = repeatChain[repeatChain.length - 1]?.senderId
if (lastSender !== msg.senderId) {
repeatChain.push({ senderId: msg.senderId, content, ts: msg.ts })
repeatChain.push({ id: msg.id, senderId: msg.senderId, content, ts: msg.ts })
}
} else {
processRepeatChain(repeatChain, msg.senderId)
currentContent = content
repeatChain = [{ senderId: msg.senderId, content, ts: msg.ts }]
repeatChain = [{ id: msg.id, senderId: msg.senderId, content, ts: msg.ts }]
}
}
@@ -227,6 +229,7 @@ export function getRepeatAnalysis(sessionId: string, filter?: TimeFilter): any {
maxChainLength: stats.maxChainLength,
originatorName: originatorInfo?.name || '未知',
lastTs: stats.lastTs,
firstMessageId: stats.firstMessageId,
})
}
hotContents.sort((a, b) => b.maxChainLength - a.maxChainLength)

View File

@@ -36,8 +36,18 @@ export {
getLaughAnalysis,
} from './advanced'
// AI 查询
export { searchMessages, getMessageContext, getRecentMessages, getConversationBetween } from './ai'
// 聊天记录查询
export {
searchMessages,
getMessageContext,
getRecentMessages,
getConversationBetween,
getMessagesBefore,
getMessagesAfter,
} from './messages'
// 聊天记录查询类型
export type { MessageResult, PaginatedMessages, MessagesWithTotal } from './messages'
// SQL 实验室
export { executeRawSQL, getSchema } from './sql'

View File

@@ -1,16 +1,17 @@
/**
* AI
* Worker 线
*
*
* Worker 线
*/
import { openDatabase, buildTimeFilter, type TimeFilter } from '../core'
// ==================== 消息搜索 ====================
// ==================== 类型定义 ====================
/**
*
*
*/
export interface SearchMessageResult {
export interface MessageResult {
id: number
senderName: string
senderPlatformId: string
@@ -20,7 +21,70 @@ export interface SearchMessageResult {
}
/**
*
*
*/
export interface PaginatedMessages {
messages: MessageResult[]
hasMore: boolean
}
/**
*
*/
export interface MessagesWithTotal {
messages: MessageResult[]
total: number
}
// ==================== 工具函数 ====================
/**
* MessageResult
* BigInt IPC
*/
function sanitizeMessageRow(row: MessageResult): MessageResult {
return {
id: Number(row.id),
senderName: String(row.senderName || ''),
senderPlatformId: String(row.senderPlatformId || ''),
content: row.content != null ? String(row.content) : '',
timestamp: Number(row.timestamp),
type: Number(row.type),
}
}
/**
*
*/
function buildSenderCondition(senderId?: number): { condition: string; params: number[] } {
if (senderId === undefined) {
return { condition: '', params: [] }
}
return { condition: 'AND msg.sender_id = ?', params: [senderId] }
}
/**
* OR
*/
function buildKeywordCondition(keywords?: string[]): { condition: string; params: string[] } {
if (!keywords || keywords.length === 0) {
return { condition: '', params: [] }
}
const condition = `AND (${keywords.map(() => `msg.content LIKE ?`).join(' OR ')})`
const params = keywords.map((k) => `%${k}%`)
return { condition, params }
}
// 排除系统消息的通用过滤条件
const SYSTEM_FILTER = "AND COALESCE(m.account_name, '') != '系统消息'"
// 只获取文本消息的过滤条件
const TEXT_ONLY_FILTER = 'AND msg.type = 0 AND msg.content IS NOT NULL AND msg.content != \'\''
// ==================== 查询函数 ====================
/**
*
* @param sessionId ID
* @param filter
* @param limit
@@ -29,7 +93,7 @@ export function getRecentMessages(
sessionId: string,
filter?: TimeFilter,
limit: number = 100
): { messages: SearchMessageResult[]; total: number } {
): MessagesWithTotal {
const db = openDatabase(sessionId)
if (!db) return { messages: [], total: 0 }
@@ -37,9 +101,6 @@ export function getRecentMessages(
const { clause: timeClause, params: timeParams } = buildTimeFilter(filter)
const timeCondition = timeClause ? timeClause.replace('WHERE', 'AND') : ''
// 排除系统消息只获取文本消息type=0
const systemFilter = "AND COALESCE(m.account_name, '') != '系统消息' AND msg.type = 0 AND msg.content IS NOT NULL AND msg.content != ''"
// 查询总数
const countSql = `
SELECT COUNT(*) as total
@@ -47,7 +108,8 @@ export function getRecentMessages(
JOIN member m ON msg.sender_id = m.id
WHERE 1=1
${timeCondition}
${systemFilter}
${SYSTEM_FILTER}
${TEXT_ONLY_FILTER}
`
const totalRow = db.prepare(countSql).get(...timeParams) as { total: number }
const total = totalRow?.total || 0
@@ -65,15 +127,19 @@ export function getRecentMessages(
JOIN member m ON msg.sender_id = m.id
WHERE 1=1
${timeCondition}
${systemFilter}
${SYSTEM_FILTER}
${TEXT_ONLY_FILTER}
ORDER BY msg.ts DESC
LIMIT ?
`
const rows = db.prepare(sql).all(...timeParams, limit) as SearchMessageResult[]
const rows = db.prepare(sql).all(...timeParams, limit) as MessageResult[]
// 返回时按时间正序排列(便于阅读)
return { messages: rows.reverse(), total }
return {
messages: rows.map(sanitizeMessageRow).reverse(),
total,
}
}
/**
@@ -83,7 +149,7 @@ export function getRecentMessages(
* @param filter
* @param limit
* @param offset
* @param senderId ID
* @param senderId ID
*/
export function searchMessages(
sessionId: string,
@@ -92,7 +158,7 @@ export function searchMessages(
limit: number = 20,
offset: number = 0,
senderId?: number
): { messages: SearchMessageResult[]; total: number } {
): MessagesWithTotal {
const db = openDatabase(sessionId)
if (!db) return { messages: [], total: 0 }
@@ -108,16 +174,8 @@ export function searchMessages(
const { clause: timeClause, params: timeParams } = buildTimeFilter(filter)
const timeCondition = timeClause ? timeClause.replace('WHERE', 'AND') : ''
// 排除系统消息
const systemFilter = "AND COALESCE(m.account_name, '') != '系统消息'"
// 构建发送者筛选条件
let senderCondition = ''
const senderParams: number[] = []
if (senderId !== undefined) {
senderCondition = 'AND msg.sender_id = ?'
senderParams.push(senderId)
}
const { condition: senderCondition, params: senderParams } = buildSenderCondition(senderId)
// 查询总数
const countSql = `
@@ -126,7 +184,7 @@ export function searchMessages(
JOIN member m ON msg.sender_id = m.id
WHERE ${keywordCondition}
${timeCondition}
${systemFilter}
${SYSTEM_FILTER}
${senderCondition}
`
const totalRow = db.prepare(countSql).get(...keywordParams, ...timeParams, ...senderParams) as { total: number }
@@ -145,15 +203,18 @@ export function searchMessages(
JOIN member m ON msg.sender_id = m.id
WHERE ${keywordCondition}
${timeCondition}
${systemFilter}
${SYSTEM_FILTER}
${senderCondition}
ORDER BY msg.ts DESC
LIMIT ? OFFSET ?
`
const rows = db.prepare(sql).all(...keywordParams, ...timeParams, ...senderParams, limit, offset) as SearchMessageResult[]
const rows = db.prepare(sql).all(...keywordParams, ...timeParams, ...senderParams, limit, offset) as MessageResult[]
return { messages: rows, total }
return {
messages: rows.map(sanitizeMessageRow),
total,
}
}
/**
@@ -168,7 +229,7 @@ export function getMessageContext(
sessionId: string,
messageIds: number | number[],
contextSize: number = 20
): SearchMessageResult[] {
): MessageResult[] {
const db = openDatabase(sessionId)
if (!db) return []
@@ -225,9 +286,130 @@ export function getMessageContext(
ORDER BY msg.id ASC
`
const rows = db.prepare(sql).all(...idList) as SearchMessageResult[]
const rows = db.prepare(sql).all(...idList) as MessageResult[]
return rows
return rows.map(sanitizeMessageRow)
}
/**
* N
* @param sessionId ID
* @param beforeId ID
* @param limit
* @param filter
* @param senderId
* @param keywords
*/
export function getMessagesBefore(
sessionId: string,
beforeId: number,
limit: number = 50,
filter?: TimeFilter,
senderId?: number,
keywords?: string[]
): PaginatedMessages {
const db = openDatabase(sessionId)
if (!db) return { messages: [], hasMore: false }
// 构建时间过滤条件
const { clause: timeClause, params: timeParams } = buildTimeFilter(filter)
const timeCondition = timeClause ? timeClause.replace('WHERE', 'AND') : ''
// 构建关键词条件
const { condition: keywordCondition, params: keywordParams } = buildKeywordCondition(keywords)
// 构建发送者筛选条件
const { condition: senderCondition, params: senderParams } = buildSenderCondition(senderId)
const sql = `
SELECT
msg.id,
COALESCE(m.group_nickname, m.account_name, m.platform_id) as senderName,
m.platform_id as senderPlatformId,
msg.content,
msg.ts as timestamp,
msg.type
FROM message msg
JOIN member m ON msg.sender_id = m.id
WHERE msg.id < ?
${timeCondition}
${keywordCondition}
${senderCondition}
${SYSTEM_FILTER}
ORDER BY msg.id DESC
LIMIT ?
`
const rows = db.prepare(sql).all(beforeId, ...timeParams, ...keywordParams, ...senderParams, limit + 1) as MessageResult[]
const hasMore = rows.length > limit
const resultRows = hasMore ? rows.slice(0, limit) : rows
// 返回时按 ID 升序排列
return {
messages: resultRows.map(sanitizeMessageRow).reverse(),
hasMore,
}
}
/**
* N
* @param sessionId ID
* @param afterId ID
* @param limit
* @param filter
* @param senderId
* @param keywords
*/
export function getMessagesAfter(
sessionId: string,
afterId: number,
limit: number = 50,
filter?: TimeFilter,
senderId?: number,
keywords?: string[]
): PaginatedMessages {
const db = openDatabase(sessionId)
if (!db) return { messages: [], hasMore: false }
// 构建时间过滤条件
const { clause: timeClause, params: timeParams } = buildTimeFilter(filter)
const timeCondition = timeClause ? timeClause.replace('WHERE', 'AND') : ''
// 构建关键词条件
const { condition: keywordCondition, params: keywordParams } = buildKeywordCondition(keywords)
// 构建发送者筛选条件
const { condition: senderCondition, params: senderParams } = buildSenderCondition(senderId)
const sql = `
SELECT
msg.id,
COALESCE(m.group_nickname, m.account_name, m.platform_id) as senderName,
m.platform_id as senderPlatformId,
msg.content,
msg.ts as timestamp,
msg.type
FROM message msg
JOIN member m ON msg.sender_id = m.id
WHERE msg.id > ?
${timeCondition}
${keywordCondition}
${senderCondition}
${SYSTEM_FILTER}
ORDER BY msg.id ASC
LIMIT ?
`
const rows = db.prepare(sql).all(afterId, ...timeParams, ...keywordParams, ...senderParams, limit + 1) as MessageResult[]
const hasMore = rows.length > limit
const resultRows = hasMore ? rows.slice(0, limit) : rows
return {
messages: resultRows.map(sanitizeMessageRow),
hasMore,
}
}
/**
@@ -245,7 +427,7 @@ export function getConversationBetween(
memberId2: number,
filter?: TimeFilter,
limit: number = 100
): { messages: SearchMessageResult[]; total: number; member1Name: string; member2Name: string } {
): MessagesWithTotal & { member1Name: string; member2Name: string } {
const db = openDatabase(sessionId)
if (!db) return { messages: [], total: 0, member1Name: '', member2Name: '' }
@@ -298,11 +480,11 @@ export function getConversationBetween(
LIMIT ?
`
const rows = db.prepare(sql).all(memberId1, memberId2, ...timeParams, limit) as SearchMessageResult[]
const rows = db.prepare(sql).all(memberId1, memberId2, ...timeParams, limit) as MessageResult[]
// 返回时按时间正序排列(便于阅读对话)
return {
messages: rows.reverse(),
messages: rows.map(sanitizeMessageRow).reverse(),
total,
member1Name: member1.name,
member2Name: member2.name,