/** * 会话管理查询模块 * 负责会话列表与单会话基础信息查询 */ import Database from 'better-sqlite3' import * as fs from 'fs' import * as path from 'path' import { openDatabase, getDbDir, getDbPath, getCacheDir } from '../core' import { getCache, computeAndSetOverviewCache, computeAndSetMembersCache, CACHE_KEY_OVERVIEW, CACHE_KEY_MEMBERS, type OverviewCache, type MembersCache, } from '../../database/sessionCache' interface DbMeta { name: string platform: string type: string imported_at: number group_id: string | null group_avatar: string | null owner_id: string | null } /** * 判断是否为聊天会话数据库 * 通过核心三表(meta/member/message)存在性快速识别 */ function isChatSessionDb(db: Database.Database): boolean { const requiredTableCount = db .prepare("SELECT COUNT(*) as cnt FROM sqlite_master WHERE type='table' AND name IN ('meta', 'member', 'message')") .get() as { cnt: number } return requiredTableCount.cnt === 3 } /** * 获取私聊对方成员的头像 * 逻辑参考 private-chat/index.vue 的 otherMemberAvatar */ function getPrivateChatMemberAvatar(db: Database.Database, sessionName: string, ownerId: string | null): string | null { // 获取所有非系统消息成员(按消息数排序) const members = db .prepare( `SELECT m.platform_id as platformId, COALESCE(m.group_nickname, m.account_name, m.platform_id) as name, m.avatar FROM member m WHERE COALESCE(m.account_name, '') != '系统消息' ORDER BY (SELECT COUNT(*) FROM message WHERE sender_id = m.id) DESC` ) .all() as Array<{ platformId: string; name: string; avatar: string | null }> if (members.length === 0) return null // 1. 优先排除 ownerId,找到另一成员的头像 if (ownerId) { const other = members.find((m) => m.platformId !== ownerId) if (other?.avatar) return other.avatar } // 2. 尝试匹配会话名称(私聊名称通常是对方昵称) const sameName = members.find((m) => m.name === sessionName) if (sameName?.avatar) return sameName.avatar // 3. 如果只有两个成员且有 ownerId,取另一个(即使没有头像也返回 null) // 如果没有 ownerId,返回第一个有头像的成员 const firstWithAvatar = members.find((m) => m.avatar) return firstWithAvatar?.avatar || null } /** * 获取所有会话列表 */ export function getAllSessions(): any[] { const dbDir = getDbDir() if (!fs.existsSync(dbDir)) { return [] } const sessions: any[] = [] const files = fs.readdirSync(dbDir).filter((f) => f.endsWith('.db')) for (const file of files) { const sessionId = file.replace('.db', '') const dbPath = path.join(dbDir, file) try { const db = new Database(dbPath) db.pragma('journal_mode = WAL') // 跳过非聊天会话数据库(例如内部索引库) if (!isChatSessionDb(db)) { db.close() continue } const meta = db.prepare('SELECT * FROM meta LIMIT 1').get() as DbMeta | undefined if (meta) { // 优先从缓存读取,未命中则实时查询并生成缓存 const cacheDir = getCacheDir() let overview = getCache(sessionId, CACHE_KEY_OVERVIEW, cacheDir) if (!overview && cacheDir) { try { overview = computeAndSetOverviewCache(db, sessionId, cacheDir) } catch { // 缓存计算失败不影响正常流程 } } const messageCount = overview?.totalMessages ?? ( db .prepare( `SELECT COUNT(*) as count FROM message msg JOIN member m ON msg.sender_id = m.id WHERE COALESCE(m.account_name, '') != '系统消息'` ) .get() as { count: number } ).count const memberCount = overview?.totalMembers ?? ( db .prepare( `SELECT COUNT(*) as count FROM member WHERE COALESCE(account_name, '') != '系统消息'` ) .get() as { count: number } ).count // 私聊:获取对方成员头像 let memberAvatar: string | null = null if (meta.type === 'private') { memberAvatar = getPrivateChatMemberAvatar(db, meta.name, meta.owner_id) } // 摘要数量:安全查询(chat_session 表可能不存在) let summaryCount = 0 try { const hasChatSessionTable = db .prepare("SELECT COUNT(*) as cnt FROM sqlite_master WHERE type='table' AND name='chat_session'") .get() as { cnt: number } if (hasChatSessionTable.cnt > 0) { summaryCount = ( db .prepare("SELECT COUNT(*) as count FROM chat_session WHERE summary IS NOT NULL AND summary != ''") .get() as { count: number } ).count } } catch { // 忽略查询错误 } sessions.push({ id: sessionId, name: meta.name, platform: meta.platform, type: meta.type, importedAt: meta.imported_at, messageCount, memberCount, dbPath, groupId: meta.group_id || null, groupAvatar: meta.group_avatar || null, ownerId: meta.owner_id || null, memberAvatar, // 私聊对方头像 summaryCount, aiConversationCount: 0, // 将在 IPC 层由主进程填充 }) } db.close() } catch (error) { console.error(`[Worker] Failed to read database ${file}:`, error) } } return sessions.sort((a, b) => b.importedAt - a.importedAt) } /** * 获取单个会话信息 */ export function getSession(sessionId: string): any | null { const db = openDatabase(sessionId) if (!db) return null const meta = db.prepare('SELECT * FROM meta LIMIT 1').get() as DbMeta | undefined if (!meta) return null const overview = getCache(sessionId, CACHE_KEY_OVERVIEW, getCacheDir()) const messageCount = overview?.totalMessages ?? ( db .prepare( `SELECT COUNT(*) as count FROM message msg JOIN member m ON msg.sender_id = m.id WHERE COALESCE(m.account_name, '') != '系统消息'` ) .get() as { count: number } ).count const memberCount = overview?.totalMembers ?? ( db .prepare( `SELECT COUNT(*) as count FROM member WHERE COALESCE(account_name, '') != '系统消息'` ) .get() as { count: number } ).count return { id: sessionId, name: meta.name, platform: meta.platform, type: meta.type, importedAt: meta.imported_at, messageCount, memberCount, dbPath: getDbPath(sessionId), groupId: meta.group_id || null, groupAvatar: meta.group_avatar || null, ownerId: meta.owner_id || null, } } /** * 获取聊天概览(AI 工具使用) * 优先从缓存读取,miss 则计算回填 */ export function getChatOverview(sessionId: string, topN: number = 10) { const db = openDatabase(sessionId) if (!db) return null const meta = db.prepare('SELECT * FROM meta LIMIT 1').get() as DbMeta | undefined if (!meta) return null const cacheDir = getCacheDir() // 读取 overview 缓存 let overview = getCache(sessionId, CACHE_KEY_OVERVIEW, cacheDir) if (!overview) { try { overview = computeAndSetOverviewCache(db, sessionId, cacheDir) } catch { // fallback: 实时查询 } } // 读取 members 缓存 let membersCache = getCache(sessionId, CACHE_KEY_MEMBERS, cacheDir) if (!membersCache) { try { membersCache = computeAndSetMembersCache(db, sessionId, cacheDir) } catch { // fallback: 无成员数据 } } // 从缓存计算 Top N 活跃成员 let topMembers: Array<{ id: number; name: string; count: number }> = [] if (membersCache?.members) { topMembers = Object.entries(membersCache.members) .map(([id, stat]) => ({ id: Number(id), name: stat.name, count: stat.count })) .sort((a, b) => b.count - a.count) .slice(0, topN) } // fallback 统计值 const totalMessages = overview?.totalMessages ?? ( db .prepare( `SELECT COUNT(*) as count FROM message msg JOIN member m ON msg.sender_id = m.id WHERE COALESCE(m.account_name, '') != '系统消息'` ) .get() as { count: number } ).count const totalMembers = overview?.totalMembers ?? ( db.prepare(`SELECT COUNT(*) as count FROM member WHERE COALESCE(account_name, '') != '系统消息'`).get() as { count: number } ).count return { name: meta.name, platform: meta.platform, type: meta.type, totalMessages, totalMembers, firstMessageTs: overview?.firstMessageTs ?? null, lastMessageTs: overview?.lastMessageTs ?? null, topMembers, } }