Files
ChatLab/electron/main/database/sessionCache.ts
2026-04-02 23:29:06 +08:00

196 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 会话级通用 JSON 缓存模块
*
* 每个 session 对应一个 {sessionId}.cache.json内部按 key 分区存储。
* 与数据库 schema 完全解耦,读取失败自动重建,无需版本管理。
*
* 文件路径: {cacheDir}/{sessionId}.cache.json
*/
import * as fs from 'fs'
import * as path from 'path'
import type Database from 'better-sqlite3'
// ==================== 通用缓存基础设施 ====================
interface CacheEntry<T = unknown> {
data: T
ts: number
}
type CacheFile = Record<string, CacheEntry>
export function getCachePath(sessionId: string, cacheDir: string): string {
return path.join(cacheDir, `${sessionId}.cache.json`)
}
function readCacheFile(cachePath: string): CacheFile | null {
try {
if (!fs.existsSync(cachePath)) return null
return JSON.parse(fs.readFileSync(cachePath, 'utf-8')) as CacheFile
} catch {
return null
}
}
function writeCacheFile(cachePath: string, content: CacheFile): void {
try {
const dir = path.dirname(cachePath)
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true })
}
fs.writeFileSync(cachePath, JSON.stringify(content), 'utf-8')
} catch {
// 写入失败不影响主流程
}
}
/**
* 读取缓存分区
*/
export function getCache<T>(sessionId: string, key: string, cacheDir: string): T | null {
const cachePath = getCachePath(sessionId, cacheDir)
const file = readCacheFile(cachePath)
if (!file || !file[key]) return null
return file[key].data as T
}
/**
* 写入缓存分区
*/
export function setCache<T>(sessionId: string, key: string, data: T, cacheDir: string): void {
const cachePath = getCachePath(sessionId, cacheDir)
const file = readCacheFile(cachePath) ?? {}
file[key] = { data, ts: Math.floor(Date.now() / 1000) }
writeCacheFile(cachePath, file)
}
/**
* 使缓存失效(不传 key 则清空整个文件)
*/
export function invalidateCache(sessionId: string, cacheDir: string, key?: string): void {
const cachePath = getCachePath(sessionId, cacheDir)
try {
if (!key) {
if (fs.existsSync(cachePath)) fs.unlinkSync(cachePath)
} else {
const file = readCacheFile(cachePath)
if (file && file[key]) {
delete file[key]
writeCacheFile(cachePath, file)
}
}
} catch {
// 忽略失败
}
}
/**
* 删除 session 的所有缓存
*/
export function deleteSessionCache(sessionId: string, cacheDir: string): void {
const cachePath = getCachePath(sessionId, cacheDir)
try {
if (fs.existsSync(cachePath)) fs.unlinkSync(cachePath)
} catch {
// 忽略删除失败
}
}
// ==================== Overview 缓存(聚合统计) ====================
export const CACHE_KEY_OVERVIEW = 'overview'
export interface OverviewCache {
totalMessages: number
totalMembers: number
firstMessageTs: number | null
lastMessageTs: number | null
}
/**
* 从数据库计算概览统计并写入缓存
*/
export function computeAndSetOverviewCache(
db: Database.Database,
sessionId: string,
cacheDir: string
): OverviewCache {
const msgStats = db.prepare('SELECT MIN(ts) as first_ts, MAX(ts) as last_ts FROM message').get() as {
first_ts: number | null
last_ts: number | null
}
const 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 = (
db.prepare(`SELECT COUNT(*) as count FROM member WHERE COALESCE(account_name, '') != '系统消息'`).get() as {
count: number
}
).count
const data: OverviewCache = {
totalMessages,
totalMembers,
firstMessageTs: msgStats.first_ts,
lastMessageTs: msgStats.last_ts,
}
setCache(sessionId, CACHE_KEY_OVERVIEW, data, cacheDir)
// 同步生成成员缓存
computeAndSetMembersCache(db, sessionId, cacheDir)
return data
}
// ==================== Members 缓存(成员维度) ====================
export const CACHE_KEY_MEMBERS = 'members'
export interface MemberStat {
name: string
count: number
}
export interface MembersCache {
/** member.id -> { name, count } */
members: Record<number, MemberStat>
}
/**
* 从数据库计算成员统计并写入缓存
*/
export function computeAndSetMembersCache(
db: Database.Database,
sessionId: string,
cacheDir: string
): MembersCache {
const rows = db
.prepare(
`SELECT msg.sender_id, COUNT(*) as count,
COALESCE(m.group_nickname, m.account_name, m.platform_id) as name
FROM message msg
JOIN member m ON msg.sender_id = m.id
GROUP BY msg.sender_id`
)
.all() as Array<{ sender_id: number; count: number; name: string }>
const members: Record<number, MemberStat> = {}
for (const row of rows) {
members[row.sender_id] = { name: row.name, count: row.count }
}
const data: MembersCache = { members }
setCache(sessionId, CACHE_KEY_MEMBERS, data, cacheDir)
return data
}