import path from 'node:path'; import { InstanceContext, NapCatCore } from '@/napcat-core/index'; import { checkSqliteAvailable, DatabaseHandle, openDatabase, decryptAndOpen, readSingleDatabase, scanAllDatabases, formatScanResults, decryptDatabaseFile, type DatabaseScanResult, type DatabaseReadOptions, type TableInfo, } from 'napcat-database'; type SQLInputValue = import('node:sqlite').SQLInputValue; export class NTQQDatabaseApi { context: InstanceContext; core: NapCatCore; constructor (context: InstanceContext, core: NapCatCore) { this.context = context; this.core = core; } /** node:sqlite 是否可用 */ async isSqliteAvailable (): Promise { return checkSqliteAvailable(); } /** 获取当前账号的 nt_db 目录 */ getNtDbDir (): string { return path.resolve(this.core.dataPath, this.core.selfInfo.uin, 'nt_qq', 'nt_db'); } /** * 获取解密缓存目录 * 默认: /db_/ * 可通过 customDir 完全自定义 */ getDefaultCacheDir (customDir?: string): string { if (customDir) return customDir; return path.resolve(this.core.NapCatDataPath, `db_${this.core.selfInfo.uin}`); } /** 当前是否已获取到数据库 passphrase */ hasPassphrase (): boolean { return !!this.core.dbPassphrase; } /** 获取 passphrase Buffer,未获取时返回 null */ getPassphraseBuffer (): Buffer | null { return this.core.dbPassphrase ? Buffer.from(this.core.dbPassphrase) : null; } // ======================== SQLite 数据库操作 ======================== /** * 打开一个已解密的 SQLite 数据库文件 * @param filePath 解密后 .db 文件路径 * @param readOnly 是否只读 (默认 true) * @returns DatabaseHandle 实例,使用完毕后需调用 .close() */ openDecryptedDb (filePath: string, readOnly = true): DatabaseHandle { return openDatabase(filePath, readOnly); } /** * 解密并打开一个 NTQQ 加密数据库 * @param dbName 数据库文件名 (如 'nt_msg.db') 或完整路径 * @param readOnly 是否只读 (默认 true) * @param cacheDir 自定义缓存目录,默认 db_ * @returns DatabaseHandle 实例,使用完毕后需调用 .close() */ openDatabase (dbName: string, readOnly = true, cacheDir?: string): DatabaseHandle | null { const passphrase = this.getPassphraseBuffer(); if (!passphrase) return null; const dbPath = path.isAbsolute(dbName) ? dbName : path.resolve(this.getNtDbDir(), dbName); const resolvedCacheDir = this.getDefaultCacheDir(cacheDir); return decryptAndOpen(dbPath, passphrase, resolvedCacheDir, readOnly); } /** * 对加密数据库执行查询并自动关闭连接 * @param dbName 数据库文件名或完整路径 * @param sql SQL 查询语句 * @param params 绑定参数 * @returns 查询结果行数组,passphrase 不可用时返回 null */ query> ( dbName: string, sql: string, params?: SQLInputValue[] | Record ): T[] | null { const db = this.openDatabase(dbName); if (!db) return null; try { return db.query(sql, params); } finally { db.close(); } } /** * 对加密数据库执行查询,仅返回第一行 * @param dbName 数据库文件名或完整路径 * @param sql SQL 查询语句 * @param params 绑定参数 */ queryOne> ( dbName: string, sql: string, params?: SQLInputValue[] | Record ): T | undefined | null { const db = this.openDatabase(dbName); if (!db) return null; try { return db.queryOne(sql, params); } finally { db.close(); } } /** * 对加密数据库执行非查询 SQL (INSERT/UPDATE/DELETE 等) 并自动关闭 * @param dbName 数据库文件名或完整路径 * @param sql SQL 语句 * @param params 绑定参数 */ execute ( dbName: string, sql: string, params?: SQLInputValue[] | Record ): { changes: number | bigint; lastInsertRowid: number | bigint; } | null { const db = this.openDatabase(dbName, false); if (!db) return null; try { return db.execute(sql, params); } finally { db.close(); } } // ======================== 批量扫描 ======================== /** * 读取单个加密数据库的表信息 * @param dbName 数据库文件名 (如 'nt_msg.db'),或完整路径 * @param cacheDir 自定义缓存目录,默认 db_ * @param options 其他选项 */ readDatabase (dbName: string, cacheDir?: string, options?: Omit): DatabaseScanResult | null { const passphrase = this.getPassphraseBuffer(); if (!passphrase) return null; const dbPath = path.isAbsolute(dbName) ? dbName : path.resolve(this.getNtDbDir(), dbName); const resolvedCacheDir = this.getDefaultCacheDir(cacheDir); return readSingleDatabase(dbPath, passphrase, { cacheDir: resolvedCacheDir, ...options }); } /** * 扫描所有加密数据库 * @param cacheDir 自定义缓存目录,默认 db_ * @param options 其他选项 */ scanDatabases (cacheDir?: string, options?: Omit): DatabaseScanResult[] { const passphrase = this.getPassphraseBuffer(); if (!passphrase) return []; const resolvedCacheDir = this.getDefaultCacheDir(cacheDir); return scanAllDatabases(this.getNtDbDir(), passphrase, { cacheDir: resolvedCacheDir, ...options }); } /** * 仅解密数据库文件(不读取表信息,不需要 node:sqlite) * @param dbName 数据库文件名或完整路径 * @param outputPath 输出路径,默认保存到 db_/ */ decryptDatabase (dbName: string, outputPath?: string): string | null { const passphrase = this.getPassphraseBuffer(); if (!passphrase) return null; const dbPath = path.isAbsolute(dbName) ? dbName : path.resolve(this.getNtDbDir(), dbName); const outPath = outputPath ?? path.resolve(this.getDefaultCacheDir(), path.basename(dbName)); return decryptDatabaseFile(dbPath, passphrase, outPath); } /** 格式化扫描结果为可读字符串 */ formatResults (results: DatabaseScanResult[]): string { return formatScanResults(results); } } export { DatabaseHandle }; export type { DatabaseScanResult, DatabaseReadOptions, TableInfo };