mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-04-23 01:39:37 +08:00
880 lines
27 KiB
TypeScript
880 lines
27 KiB
TypeScript
/**
|
||
* 聊天记录查询模块
|
||
* 提供通用的消息查询功能:搜索、筛选、上下文、无限滚动等
|
||
* 在 Worker 线程中执行
|
||
*/
|
||
|
||
import { openDatabase, buildTimeFilter, type TimeFilter } from '../core'
|
||
import { ensureAvatarColumn } from './basic'
|
||
import { hasFtsIndex } from './fts'
|
||
import { tokenizeQueryForFts } from '../../nlp/ftsTokenizer'
|
||
|
||
// ==================== 类型定义 ====================
|
||
|
||
/**
|
||
* 消息查询结果类型
|
||
*/
|
||
export interface MessageResult {
|
||
id: number
|
||
senderId: number
|
||
senderName: string
|
||
senderPlatformId: string
|
||
senderAliases: string[]
|
||
senderAvatar: string | null
|
||
content: string
|
||
timestamp: number
|
||
type: number
|
||
replyToMessageId: string | null
|
||
replyToContent: string | null
|
||
replyToSenderName: string | null
|
||
}
|
||
|
||
/**
|
||
* 分页消息结果
|
||
*/
|
||
export interface PaginatedMessages {
|
||
messages: MessageResult[]
|
||
hasMore: boolean
|
||
}
|
||
|
||
/**
|
||
* 带总数的消息结果
|
||
*/
|
||
export interface MessagesWithTotal {
|
||
messages: MessageResult[]
|
||
total: number
|
||
}
|
||
|
||
// ==================== 工具函数 ====================
|
||
|
||
/**
|
||
* 数据库行类型(包含 aliases JSON 字符串和头像)
|
||
*/
|
||
interface DbMessageRow {
|
||
id: number
|
||
senderId: number
|
||
senderName: string
|
||
senderPlatformId: string
|
||
aliases: string | null
|
||
avatar: string | null
|
||
content: string
|
||
timestamp: number
|
||
type: number
|
||
reply_to_message_id: string | null
|
||
replyToContent: string | null
|
||
replyToSenderName: string | null
|
||
}
|
||
|
||
/**
|
||
* 将数据库行转换为可序列化的 MessageResult
|
||
* 处理 BigInt 等类型,确保 IPC 传输安全
|
||
*/
|
||
function sanitizeMessageRow(row: DbMessageRow): MessageResult {
|
||
// 解析别名 JSON
|
||
let aliases: string[] = []
|
||
if (row.aliases) {
|
||
try {
|
||
aliases = JSON.parse(row.aliases)
|
||
} catch {
|
||
aliases = []
|
||
}
|
||
}
|
||
|
||
return {
|
||
id: Number(row.id),
|
||
senderId: Number(row.senderId),
|
||
senderName: String(row.senderName || ''),
|
||
senderPlatformId: String(row.senderPlatformId || ''),
|
||
senderAliases: aliases,
|
||
senderAvatar: row.avatar || null,
|
||
content: row.content != null ? String(row.content) : '',
|
||
timestamp: Number(row.timestamp),
|
||
type: Number(row.type),
|
||
replyToMessageId: row.reply_to_message_id || null,
|
||
replyToContent: row.replyToContent || null,
|
||
replyToSenderName: row.replyToSenderName || null,
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 构建通用的发送者筛选条件
|
||
*/
|
||
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 != ''"
|
||
|
||
// ==================== 查询函数 ====================
|
||
|
||
/**
|
||
* 获取最近的消息(AI Agent 专用,只返回文本消息)
|
||
* @param sessionId 会话 ID
|
||
* @param filter 时间过滤器
|
||
* @param limit 返回数量限制
|
||
*/
|
||
export function getRecentMessages(sessionId: string, filter?: TimeFilter, limit: number = 100): MessagesWithTotal {
|
||
// 确保数据库有 avatar 字段(兼容旧数据库)
|
||
ensureAvatarColumn(sessionId)
|
||
|
||
const db = openDatabase(sessionId)
|
||
if (!db) return { messages: [], total: 0 }
|
||
|
||
// 构建时间过滤条件(使用 'msg' 表别名避免多表 JOIN 时的列名歧义)
|
||
const { clause: timeClause, params: timeParams } = buildTimeFilter(filter, 'msg')
|
||
const timeCondition = timeClause ? timeClause.replace('WHERE', 'AND') : ''
|
||
|
||
// 查询总数
|
||
const countSql = `
|
||
SELECT COUNT(*) as total
|
||
FROM message msg
|
||
JOIN member m ON msg.sender_id = m.id
|
||
WHERE 1=1
|
||
${timeCondition}
|
||
${SYSTEM_FILTER}
|
||
${TEXT_ONLY_FILTER}
|
||
`
|
||
const totalRow = db.prepare(countSql).get(...timeParams) as { total: number }
|
||
const total = totalRow?.total || 0
|
||
|
||
// 查询最近消息(按时间降序)
|
||
const sql = `
|
||
SELECT
|
||
msg.id,
|
||
m.id as senderId,
|
||
COALESCE(m.group_nickname, m.account_name, m.platform_id) as senderName,
|
||
m.platform_id as senderPlatformId,
|
||
m.aliases,
|
||
m.avatar,
|
||
msg.content,
|
||
msg.ts as timestamp,
|
||
msg.type,
|
||
msg.reply_to_message_id,
|
||
reply_msg.content as replyToContent,
|
||
COALESCE(reply_m.group_nickname, reply_m.account_name, reply_m.platform_id) as replyToSenderName
|
||
FROM message msg
|
||
JOIN member m ON msg.sender_id = m.id
|
||
LEFT JOIN message reply_msg ON msg.reply_to_message_id = reply_msg.platform_message_id
|
||
LEFT JOIN member reply_m ON reply_msg.sender_id = reply_m.id
|
||
WHERE 1=1
|
||
${timeCondition}
|
||
${SYSTEM_FILTER}
|
||
${TEXT_ONLY_FILTER}
|
||
ORDER BY msg.ts DESC
|
||
LIMIT ?
|
||
`
|
||
|
||
const rows = db.prepare(sql).all(...timeParams, limit) as DbMessageRow[]
|
||
|
||
// 返回时按时间正序排列(便于阅读)
|
||
return {
|
||
messages: rows.map(sanitizeMessageRow).reverse(),
|
||
total,
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取所有最近的消息(消息查看器专用,包含所有类型消息)
|
||
* @param sessionId 会话 ID
|
||
* @param filter 时间过滤器
|
||
* @param limit 返回数量限制
|
||
*/
|
||
export function getAllRecentMessages(sessionId: string, filter?: TimeFilter, limit: number = 100): MessagesWithTotal {
|
||
// 确保数据库有 avatar 字段(兼容旧数据库)
|
||
ensureAvatarColumn(sessionId)
|
||
|
||
const db = openDatabase(sessionId)
|
||
if (!db) return { messages: [], total: 0 }
|
||
|
||
// 构建时间过滤条件(使用 'msg' 表别名避免多表 JOIN 时的列名歧义)
|
||
const { clause: timeClause, params: timeParams } = buildTimeFilter(filter, 'msg')
|
||
const timeCondition = timeClause ? timeClause.replace('WHERE', 'AND') : ''
|
||
|
||
// 查询总数
|
||
const countSql = `
|
||
SELECT COUNT(*) as total
|
||
FROM message msg
|
||
JOIN member m ON msg.sender_id = m.id
|
||
WHERE 1=1
|
||
${timeCondition}
|
||
`
|
||
const totalRow = db.prepare(countSql).get(...timeParams) as { total: number }
|
||
const total = totalRow?.total || 0
|
||
|
||
// 查询最近消息(按时间降序)
|
||
const sql = `
|
||
SELECT
|
||
msg.id,
|
||
m.id as senderId,
|
||
COALESCE(m.group_nickname, m.account_name, m.platform_id) as senderName,
|
||
m.platform_id as senderPlatformId,
|
||
m.aliases,
|
||
m.avatar,
|
||
msg.content,
|
||
msg.ts as timestamp,
|
||
msg.type,
|
||
msg.reply_to_message_id,
|
||
reply_msg.content as replyToContent,
|
||
COALESCE(reply_m.group_nickname, reply_m.account_name, reply_m.platform_id) as replyToSenderName
|
||
FROM message msg
|
||
JOIN member m ON msg.sender_id = m.id
|
||
LEFT JOIN message reply_msg ON msg.reply_to_message_id = reply_msg.platform_message_id
|
||
LEFT JOIN member reply_m ON reply_msg.sender_id = reply_m.id
|
||
WHERE 1=1
|
||
${timeCondition}
|
||
ORDER BY msg.ts DESC
|
||
LIMIT ?
|
||
`
|
||
|
||
const rows = db.prepare(sql).all(...timeParams, limit) as DbMessageRow[]
|
||
|
||
// 返回时按时间正序排列(便于阅读)
|
||
return {
|
||
messages: rows.map(sanitizeMessageRow).reverse(),
|
||
total,
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 关键词搜索消息
|
||
* @param sessionId 会话 ID
|
||
* @param keywords 关键词数组(OR 逻辑),可以为空数组
|
||
* @param filter 时间过滤器
|
||
* @param limit 返回数量限制
|
||
* @param offset 偏移量(分页)
|
||
* @param senderId 可选的发送者成员 ID
|
||
*/
|
||
export function searchMessages(
|
||
sessionId: string,
|
||
keywords: string[],
|
||
filter?: TimeFilter,
|
||
limit: number = 20,
|
||
offset: number = 0,
|
||
senderId?: number
|
||
): MessagesWithTotal {
|
||
// 确保数据库有 avatar 字段(兼容旧数据库)
|
||
ensureAvatarColumn(sessionId)
|
||
|
||
const db = openDatabase(sessionId)
|
||
if (!db) return { messages: [], total: 0 }
|
||
|
||
const useFts = keywords.length > 0 && hasFtsIndex(sessionId)
|
||
let matchQuery = ''
|
||
if (useFts) {
|
||
matchQuery = tokenizeQueryForFts(keywords)
|
||
}
|
||
|
||
// FTS5 路径:使用倒排索引加速搜索
|
||
if (useFts && matchQuery) {
|
||
return searchMessagesWithFts(db, sessionId, matchQuery, filter, limit, offset, senderId)
|
||
}
|
||
|
||
// LIKE 路径(fallback):旧数据库无 FTS 索引或无关键词
|
||
return searchMessagesWithLike(db, keywords, filter, limit, offset, senderId)
|
||
}
|
||
|
||
/**
|
||
* FTS5 搜索路径
|
||
*/
|
||
function searchMessagesWithFts(
|
||
db: ReturnType<typeof openDatabase> & object,
|
||
_sessionId: string,
|
||
matchQuery: string,
|
||
filter?: TimeFilter,
|
||
limit: number = 20,
|
||
offset: number = 0,
|
||
senderId?: number
|
||
): MessagesWithTotal {
|
||
const { clause: timeClause, params: timeParams } = buildTimeFilter(filter, 'msg')
|
||
const timeCondition = timeClause ? timeClause.replace('WHERE', 'AND') : ''
|
||
const { condition: senderCondition, params: senderParams } = buildSenderCondition(senderId)
|
||
|
||
try {
|
||
const countSql = `
|
||
SELECT COUNT(*) as total
|
||
FROM message msg
|
||
JOIN member m ON msg.sender_id = m.id
|
||
WHERE msg.id IN (SELECT rowid FROM message_fts WHERE content MATCH ?)
|
||
${timeCondition}
|
||
${senderCondition}
|
||
`
|
||
const totalRow = db.prepare(countSql).get(matchQuery, ...timeParams, ...senderParams) as { total: number }
|
||
const total = totalRow?.total || 0
|
||
|
||
const sql = `
|
||
SELECT
|
||
msg.id,
|
||
m.id as senderId,
|
||
COALESCE(m.group_nickname, m.account_name, m.platform_id) as senderName,
|
||
m.platform_id as senderPlatformId,
|
||
m.aliases,
|
||
m.avatar,
|
||
msg.content,
|
||
msg.ts as timestamp,
|
||
msg.type,
|
||
msg.reply_to_message_id,
|
||
reply_msg.content as replyToContent,
|
||
COALESCE(reply_m.group_nickname, reply_m.account_name, reply_m.platform_id) as replyToSenderName
|
||
FROM message msg
|
||
JOIN member m ON msg.sender_id = m.id
|
||
LEFT JOIN message reply_msg ON msg.reply_to_message_id = reply_msg.platform_message_id
|
||
LEFT JOIN member reply_m ON reply_msg.sender_id = reply_m.id
|
||
WHERE msg.id IN (SELECT rowid FROM message_fts WHERE content MATCH ?)
|
||
${timeCondition}
|
||
${senderCondition}
|
||
ORDER BY msg.ts DESC
|
||
LIMIT ? OFFSET ?
|
||
`
|
||
|
||
const rows = db.prepare(sql).all(matchQuery, ...timeParams, ...senderParams, limit, offset) as DbMessageRow[]
|
||
|
||
return {
|
||
messages: rows.map(sanitizeMessageRow),
|
||
total,
|
||
}
|
||
} catch (error) {
|
||
console.error('[FTS] searchMessages FTS path failed, falling back to LIKE:', error)
|
||
return searchMessagesWithLike(db, [], filter, limit, offset, senderId)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* LIKE 搜索路径(fallback 或 deep_search 使用)
|
||
*/
|
||
export function searchMessagesWithLike(
|
||
db: ReturnType<typeof openDatabase> & object,
|
||
keywords: string[],
|
||
filter?: TimeFilter,
|
||
limit: number = 20,
|
||
offset: number = 0,
|
||
senderId?: number
|
||
): MessagesWithTotal {
|
||
let keywordCondition = '1=1'
|
||
const keywordParams: string[] = []
|
||
if (keywords.length > 0) {
|
||
keywordCondition = `(${keywords.map(() => `msg.content LIKE ?`).join(' OR ')})`
|
||
keywordParams.push(...keywords.map((k) => `%${k}%`))
|
||
}
|
||
|
||
const { clause: timeClause, params: timeParams } = buildTimeFilter(filter, 'msg')
|
||
const timeCondition = timeClause ? timeClause.replace('WHERE', 'AND') : ''
|
||
const { condition: senderCondition, params: senderParams } = buildSenderCondition(senderId)
|
||
|
||
const countSql = `
|
||
SELECT COUNT(*) as total
|
||
FROM message msg
|
||
JOIN member m ON msg.sender_id = m.id
|
||
WHERE ${keywordCondition}
|
||
${timeCondition}
|
||
${senderCondition}
|
||
`
|
||
const totalRow = db.prepare(countSql).get(...keywordParams, ...timeParams, ...senderParams) as { total: number }
|
||
const total = totalRow?.total || 0
|
||
|
||
const sql = `
|
||
SELECT
|
||
msg.id,
|
||
m.id as senderId,
|
||
COALESCE(m.group_nickname, m.account_name, m.platform_id) as senderName,
|
||
m.platform_id as senderPlatformId,
|
||
m.aliases,
|
||
m.avatar,
|
||
msg.content,
|
||
msg.ts as timestamp,
|
||
msg.type,
|
||
msg.reply_to_message_id,
|
||
reply_msg.content as replyToContent,
|
||
COALESCE(reply_m.group_nickname, reply_m.account_name, reply_m.platform_id) as replyToSenderName
|
||
FROM message msg
|
||
JOIN member m ON msg.sender_id = m.id
|
||
LEFT JOIN message reply_msg ON msg.reply_to_message_id = reply_msg.platform_message_id
|
||
LEFT JOIN member reply_m ON reply_msg.sender_id = reply_m.id
|
||
WHERE ${keywordCondition}
|
||
${timeCondition}
|
||
${senderCondition}
|
||
ORDER BY msg.ts DESC
|
||
LIMIT ? OFFSET ?
|
||
`
|
||
|
||
const rows = db.prepare(sql).all(...keywordParams, ...timeParams, ...senderParams, limit, offset) as DbMessageRow[]
|
||
|
||
return {
|
||
messages: rows.map(sanitizeMessageRow),
|
||
total,
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 深度搜索消息(LIKE 子串匹配,速度较慢但不会遗漏)
|
||
* 始终使用 LIKE 路径,不经过 FTS5。
|
||
*/
|
||
export function deepSearchMessages(
|
||
sessionId: string,
|
||
keywords: string[],
|
||
filter?: TimeFilter,
|
||
limit: number = 20,
|
||
offset: number = 0,
|
||
senderId?: number
|
||
): MessagesWithTotal {
|
||
ensureAvatarColumn(sessionId)
|
||
const db = openDatabase(sessionId)
|
||
if (!db) return { messages: [], total: 0 }
|
||
return searchMessagesWithLike(db, keywords, filter, limit, offset, senderId)
|
||
}
|
||
|
||
/**
|
||
* 获取消息上下文(指定消息前后的消息)
|
||
* 使用消息 ID 方式获取精确的前后 N 条消息
|
||
*
|
||
* @param sessionId 会话 ID
|
||
* @param messageIds 消息 ID 列表(支持单个或批量)
|
||
* @param contextSize 上下文大小,前后各多少条消息,默认 20
|
||
*/
|
||
export function getMessageContext(
|
||
sessionId: string,
|
||
messageIds: number | number[],
|
||
contextSize: number = 20
|
||
): MessageResult[] {
|
||
// 确保数据库有 avatar 字段(兼容旧数据库)
|
||
ensureAvatarColumn(sessionId)
|
||
|
||
const db = openDatabase(sessionId)
|
||
if (!db) return []
|
||
|
||
// 统一转为数组
|
||
const ids = Array.isArray(messageIds) ? messageIds : [messageIds]
|
||
if (ids.length === 0) return []
|
||
|
||
// 收集所有上下文消息的 ID(使用 Set 去重)
|
||
const contextIds = new Set<number>()
|
||
|
||
for (const messageId of ids) {
|
||
// 添加目标消息本身
|
||
contextIds.add(messageId)
|
||
|
||
// 获取前 contextSize 条消息(id < messageId,按 id 降序取前 N 个)
|
||
const beforeSql = `
|
||
SELECT id FROM message
|
||
WHERE id < ?
|
||
ORDER BY id DESC
|
||
LIMIT ?
|
||
`
|
||
const beforeRows = db.prepare(beforeSql).all(messageId, contextSize) as { id: number }[]
|
||
beforeRows.forEach((row) => contextIds.add(row.id))
|
||
|
||
// 获取后 contextSize 条消息(id > messageId,按 id 升序取前 N 个)
|
||
const afterSql = `
|
||
SELECT id FROM message
|
||
WHERE id > ?
|
||
ORDER BY id ASC
|
||
LIMIT ?
|
||
`
|
||
const afterRows = db.prepare(afterSql).all(messageId, contextSize) as { id: number }[]
|
||
afterRows.forEach((row) => contextIds.add(row.id))
|
||
}
|
||
|
||
// 如果没有找到任何消息
|
||
if (contextIds.size === 0) return []
|
||
|
||
// 批量查询所有上下文消息
|
||
const idList = Array.from(contextIds)
|
||
const placeholders = idList.map(() => '?').join(', ')
|
||
|
||
const sql = `
|
||
SELECT
|
||
msg.id,
|
||
m.id as senderId,
|
||
COALESCE(m.group_nickname, m.account_name, m.platform_id) as senderName,
|
||
m.platform_id as senderPlatformId,
|
||
m.aliases,
|
||
m.avatar,
|
||
msg.content,
|
||
msg.ts as timestamp,
|
||
msg.type,
|
||
msg.reply_to_message_id,
|
||
reply_msg.content as replyToContent,
|
||
COALESCE(reply_m.group_nickname, reply_m.account_name, reply_m.platform_id) as replyToSenderName
|
||
FROM message msg
|
||
JOIN member m ON msg.sender_id = m.id
|
||
LEFT JOIN message reply_msg ON msg.reply_to_message_id = reply_msg.platform_message_id
|
||
LEFT JOIN member reply_m ON reply_msg.sender_id = reply_m.id
|
||
WHERE msg.id IN (${placeholders})
|
||
ORDER BY msg.id ASC
|
||
`
|
||
|
||
const rows = db.prepare(sql).all(...idList) as DbMessageRow[]
|
||
|
||
return rows.map(sanitizeMessageRow)
|
||
}
|
||
|
||
/**
|
||
* 获取搜索结果的上下文消息(会话感知 + 区间合并去重)
|
||
* 用于 search_messages / deep_search_messages 自动扩展上下文。
|
||
* 当存在会话索引时,上下文不跨会话边界;否则按 message.id 顺序取前后 N 条。
|
||
*
|
||
* @param sessionId 数据库会话 ID
|
||
* @param messageIds 搜索命中的消息 ID 列表
|
||
* @param contextBefore 每条命中消息向前取多少条上下文
|
||
* @param contextAfter 每条命中消息向后取多少条上下文
|
||
*/
|
||
export function getSearchMessageContext(
|
||
sessionId: string,
|
||
messageIds: number[],
|
||
contextBefore: number = 2,
|
||
contextAfter: number = 2
|
||
): MessageResult[] {
|
||
ensureAvatarColumn(sessionId)
|
||
const db = openDatabase(sessionId)
|
||
if (!db) return []
|
||
if (messageIds.length === 0) return []
|
||
|
||
const contextIds = new Set<number>()
|
||
|
||
const hasSessionData =
|
||
(db.prepare('SELECT 1 FROM message_context LIMIT 1').get() as { 1: number } | undefined) !== undefined
|
||
|
||
for (const messageId of messageIds) {
|
||
contextIds.add(messageId)
|
||
|
||
if (hasSessionData) {
|
||
const sessionRow = db.prepare('SELECT session_id FROM message_context WHERE message_id = ?').get(messageId) as
|
||
| { session_id: number }
|
||
| undefined
|
||
|
||
if (sessionRow) {
|
||
if (contextBefore > 0) {
|
||
const rows = db
|
||
.prepare(
|
||
`SELECT mc.message_id as id
|
||
FROM message_context mc
|
||
WHERE mc.session_id = ? AND mc.message_id < ?
|
||
ORDER BY mc.message_id DESC
|
||
LIMIT ?`
|
||
)
|
||
.all(sessionRow.session_id, messageId, contextBefore) as { id: number }[]
|
||
rows.forEach((r) => contextIds.add(r.id))
|
||
}
|
||
if (contextAfter > 0) {
|
||
const rows = db
|
||
.prepare(
|
||
`SELECT mc.message_id as id
|
||
FROM message_context mc
|
||
WHERE mc.session_id = ? AND mc.message_id > ?
|
||
ORDER BY mc.message_id ASC
|
||
LIMIT ?`
|
||
)
|
||
.all(sessionRow.session_id, messageId, contextAfter) as { id: number }[]
|
||
rows.forEach((r) => contextIds.add(r.id))
|
||
}
|
||
continue
|
||
}
|
||
}
|
||
|
||
// Fallback: no session data or message not indexed — use simple id-based context
|
||
if (contextBefore > 0) {
|
||
const rows = db
|
||
.prepare('SELECT id FROM message WHERE id < ? ORDER BY id DESC LIMIT ?')
|
||
.all(messageId, contextBefore) as { id: number }[]
|
||
rows.forEach((r) => contextIds.add(r.id))
|
||
}
|
||
if (contextAfter > 0) {
|
||
const rows = db
|
||
.prepare('SELECT id FROM message WHERE id > ? ORDER BY id ASC LIMIT ?')
|
||
.all(messageId, contextAfter) as { id: number }[]
|
||
rows.forEach((r) => contextIds.add(r.id))
|
||
}
|
||
}
|
||
|
||
if (contextIds.size === 0) return []
|
||
|
||
const idList = Array.from(contextIds)
|
||
const placeholders = idList.map(() => '?').join(', ')
|
||
|
||
const sql = `
|
||
SELECT
|
||
msg.id,
|
||
m.id as senderId,
|
||
COALESCE(m.group_nickname, m.account_name, m.platform_id) as senderName,
|
||
m.platform_id as senderPlatformId,
|
||
m.aliases,
|
||
m.avatar,
|
||
msg.content,
|
||
msg.ts as timestamp,
|
||
msg.type,
|
||
msg.reply_to_message_id,
|
||
reply_msg.content as replyToContent,
|
||
COALESCE(reply_m.group_nickname, reply_m.account_name, reply_m.platform_id) as replyToSenderName
|
||
FROM message msg
|
||
JOIN member m ON msg.sender_id = m.id
|
||
LEFT JOIN message reply_msg ON msg.reply_to_message_id = reply_msg.platform_message_id
|
||
LEFT JOIN member reply_m ON reply_msg.sender_id = reply_m.id
|
||
WHERE msg.id IN (${placeholders})
|
||
ORDER BY msg.ts ASC, msg.id ASC
|
||
`
|
||
|
||
const rows = db.prepare(sql).all(...idList) as DbMessageRow[]
|
||
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 {
|
||
// 确保数据库有 avatar 字段(兼容旧数据库)
|
||
ensureAvatarColumn(sessionId)
|
||
|
||
const db = openDatabase(sessionId)
|
||
if (!db) return { messages: [], hasMore: false }
|
||
|
||
// 构建时间过滤条件(使用 'msg' 表别名避免多表 JOIN 时的列名歧义)
|
||
const { clause: timeClause, params: timeParams } = buildTimeFilter(filter, 'msg')
|
||
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,
|
||
m.id as senderId,
|
||
COALESCE(m.group_nickname, m.account_name, m.platform_id) as senderName,
|
||
m.platform_id as senderPlatformId,
|
||
m.aliases,
|
||
m.avatar,
|
||
msg.content,
|
||
msg.ts as timestamp,
|
||
msg.type,
|
||
msg.reply_to_message_id,
|
||
reply_msg.content as replyToContent,
|
||
COALESCE(reply_m.group_nickname, reply_m.account_name, reply_m.platform_id) as replyToSenderName
|
||
FROM message msg
|
||
JOIN member m ON msg.sender_id = m.id
|
||
LEFT JOIN message reply_msg ON msg.reply_to_message_id = reply_msg.platform_message_id
|
||
LEFT JOIN member reply_m ON reply_msg.sender_id = reply_m.id
|
||
WHERE msg.id < ?
|
||
${timeCondition}
|
||
${keywordCondition}
|
||
${senderCondition}
|
||
ORDER BY msg.id DESC
|
||
LIMIT ?
|
||
`
|
||
|
||
const rows = db
|
||
.prepare(sql)
|
||
.all(beforeId, ...timeParams, ...keywordParams, ...senderParams, limit + 1) as DbMessageRow[]
|
||
|
||
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 {
|
||
// 确保数据库有 avatar 字段(兼容旧数据库)
|
||
ensureAvatarColumn(sessionId)
|
||
|
||
const db = openDatabase(sessionId)
|
||
if (!db) return { messages: [], hasMore: false }
|
||
|
||
// 构建时间过滤条件(使用 'msg' 表别名避免多表 JOIN 时的列名歧义)
|
||
const { clause: timeClause, params: timeParams } = buildTimeFilter(filter, 'msg')
|
||
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,
|
||
m.id as senderId,
|
||
COALESCE(m.group_nickname, m.account_name, m.platform_id) as senderName,
|
||
m.platform_id as senderPlatformId,
|
||
m.aliases,
|
||
m.avatar,
|
||
msg.content,
|
||
msg.ts as timestamp,
|
||
msg.type,
|
||
msg.reply_to_message_id,
|
||
reply_msg.content as replyToContent,
|
||
COALESCE(reply_m.group_nickname, reply_m.account_name, reply_m.platform_id) as replyToSenderName
|
||
FROM message msg
|
||
JOIN member m ON msg.sender_id = m.id
|
||
LEFT JOIN message reply_msg ON msg.reply_to_message_id = reply_msg.platform_message_id
|
||
LEFT JOIN member reply_m ON reply_msg.sender_id = reply_m.id
|
||
WHERE msg.id > ?
|
||
${timeCondition}
|
||
${keywordCondition}
|
||
${senderCondition}
|
||
ORDER BY msg.id ASC
|
||
LIMIT ?
|
||
`
|
||
|
||
const rows = db
|
||
.prepare(sql)
|
||
.all(afterId, ...timeParams, ...keywordParams, ...senderParams, limit + 1) as DbMessageRow[]
|
||
|
||
const hasMore = rows.length > limit
|
||
const resultRows = hasMore ? rows.slice(0, limit) : rows
|
||
|
||
return {
|
||
messages: resultRows.map(sanitizeMessageRow),
|
||
hasMore,
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取两个成员之间的对话
|
||
* 提取两人相邻发言形成的对话片段
|
||
* @param sessionId 会话 ID
|
||
* @param memberId1 成员1的 ID
|
||
* @param memberId2 成员2的 ID
|
||
* @param filter 时间过滤器
|
||
* @param limit 返回消息数量限制
|
||
*/
|
||
export function getConversationBetween(
|
||
sessionId: string,
|
||
memberId1: number,
|
||
memberId2: number,
|
||
filter?: TimeFilter,
|
||
limit: number = 100
|
||
): MessagesWithTotal & { member1Name: string; member2Name: string } {
|
||
// 确保数据库有 avatar 字段(兼容旧数据库)
|
||
ensureAvatarColumn(sessionId)
|
||
|
||
const db = openDatabase(sessionId)
|
||
if (!db) return { messages: [], total: 0, member1Name: '', member2Name: '' }
|
||
|
||
// 获取成员名称
|
||
const member1 = db
|
||
.prepare(
|
||
`
|
||
SELECT COALESCE(group_nickname, account_name, platform_id) as name
|
||
FROM member WHERE id = ?
|
||
`
|
||
)
|
||
.get(memberId1) as { name: string } | undefined
|
||
|
||
const member2 = db
|
||
.prepare(
|
||
`
|
||
SELECT COALESCE(group_nickname, account_name, platform_id) as name
|
||
FROM member WHERE id = ?
|
||
`
|
||
)
|
||
.get(memberId2) as { name: string } | undefined
|
||
|
||
if (!member1 || !member2) {
|
||
return { messages: [], total: 0, member1Name: '', member2Name: '' }
|
||
}
|
||
|
||
// 构建时间过滤条件(使用 'msg' 表别名避免多表 JOIN 时的列名歧义)
|
||
const { clause: timeClause, params: timeParams } = buildTimeFilter(filter, 'msg')
|
||
const timeCondition = timeClause ? timeClause.replace('WHERE', 'AND') : ''
|
||
|
||
// 查询两人之间的所有消息
|
||
const countSql = `
|
||
SELECT COUNT(*) as total
|
||
FROM message msg
|
||
JOIN member m ON msg.sender_id = m.id
|
||
WHERE msg.sender_id IN (?, ?)
|
||
${timeCondition}
|
||
AND msg.content IS NOT NULL AND msg.content != ''
|
||
`
|
||
const totalRow = db.prepare(countSql).get(memberId1, memberId2, ...timeParams) as { total: number }
|
||
const total = totalRow?.total || 0
|
||
|
||
// 查询消息
|
||
const sql = `
|
||
SELECT
|
||
msg.id,
|
||
m.id as senderId,
|
||
COALESCE(m.group_nickname, m.account_name, m.platform_id) as senderName,
|
||
m.platform_id as senderPlatformId,
|
||
m.aliases,
|
||
m.avatar,
|
||
msg.content,
|
||
msg.ts as timestamp,
|
||
msg.type,
|
||
msg.reply_to_message_id,
|
||
reply_msg.content as replyToContent,
|
||
COALESCE(reply_m.group_nickname, reply_m.account_name, reply_m.platform_id) as replyToSenderName
|
||
FROM message msg
|
||
JOIN member m ON msg.sender_id = m.id
|
||
LEFT JOIN message reply_msg ON msg.reply_to_message_id = reply_msg.platform_message_id
|
||
LEFT JOIN member reply_m ON reply_msg.sender_id = reply_m.id
|
||
WHERE msg.sender_id IN (?, ?)
|
||
${timeCondition}
|
||
AND msg.content IS NOT NULL AND msg.content != ''
|
||
ORDER BY msg.ts DESC
|
||
LIMIT ?
|
||
`
|
||
|
||
const rows = db.prepare(sql).all(memberId1, memberId2, ...timeParams, limit) as DbMessageRow[]
|
||
|
||
// 返回时按时间正序排列(便于阅读对话)
|
||
return {
|
||
messages: rows.map(sanitizeMessageRow).reverse(),
|
||
total,
|
||
member1Name: member1.name,
|
||
member2Name: member2.name,
|
||
}
|
||
}
|