/** * 聊天记录查询模块 * 提供通用的消息查询功能:搜索、筛选、上下文、无限滚动等 * 在 Worker 线程中执行 */ import { openDatabase, buildTimeFilter, type TimeFilter } from '../core' import { ensureAvatarColumn } from './basic' // ==================== 类型定义 ==================== /** * 消息查询结果类型 */ export interface MessageResult { id: 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 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), 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, 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, 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 } // 构建关键词条件(OR 逻辑) 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}%`)) } // 构建时间过滤条件(使用 'msg' 表别名避免多表 JOIN 时的列名歧义) 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, 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, } } /** * 获取消息上下文(指定消息前后的消息) * 使用消息 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() 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, 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) } /** * 获取指定消息之前的 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, 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, 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, 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, } }