UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

158 lines (157 loc) 5.76 kB
/** * TokenUsageStore — Token 消耗持久化存储 (Drizzle ORM) * * 写入 AI 调用的 token 用量记录到 SQLite token_usage 表。 * 提供近 7 日按日/按来源的聚合查询。 * * Drizzle 迁移策略: * - INSERT 使用 drizzle 类型安全 API(列名编译期检查) * - 聚合查询保留预编译 raw SQL(DATE() / GROUP BY computed-column * 在 drizzle query builder 中不够直观,保持原有高效预编译语句) */ import { getDrizzle } from '../../infrastructure/database/drizzle/index.js'; import { tokenUsage } from '../../infrastructure/database/drizzle/schema.js'; import Logger from '../../infrastructure/logging/Logger.js'; const MAX_ROWS = 10000; // 自动清理: 保留最近 10000 条 export class TokenUsageStore { #drizzle; #db; #logger; #pruneStmt; #dailyStmt; #bySourceStmt; #summaryStmt; /** | null} */ #reportCache = null; /** @param db — raw better-sqlite3 instance */ constructor(db, drizzle) { this.#db = db; this.#drizzle = drizzle ?? getDrizzle(); this.#logger = Logger.getInstance(); // 聚合查询保留预编译语句(使用 SQLite 特有函数,drizzle query builder 不方便表达) this.#pruneStmt = this.#db.prepare(` DELETE FROM token_usage WHERE id NOT IN ( SELECT id FROM token_usage ORDER BY timestamp DESC LIMIT ? ) `); this.#dailyStmt = this.#db.prepare(` SELECT DATE(timestamp / 1000, 'unixepoch', 'localtime') AS date, SUM(input_tokens) AS input_tokens, SUM(output_tokens) AS output_tokens, SUM(total_tokens) AS total_tokens, COUNT(*) AS call_count FROM token_usage WHERE timestamp >= ? GROUP BY date ORDER BY date ASC `); this.#bySourceStmt = this.#db.prepare(` SELECT source, SUM(input_tokens) AS input_tokens, SUM(output_tokens) AS output_tokens, SUM(total_tokens) AS total_tokens, COUNT(*) AS call_count FROM token_usage WHERE timestamp >= ? GROUP BY source ORDER BY total_tokens DESC `); this.#summaryStmt = this.#db.prepare(` SELECT COALESCE(SUM(input_tokens), 0) AS input_tokens, COALESCE(SUM(output_tokens), 0) AS output_tokens, COALESCE(SUM(total_tokens), 0) AS total_tokens, COUNT(*) AS call_count FROM token_usage WHERE timestamp >= ? `); } // ─── 写入 ───────────────────────────────────────── /** * 记录一次 AI 调用的 token 消耗 * ★ 使用 drizzle 类型安全 INSERT — 列名拼写编译期检查 */ record(record) { try { const now = Date.now(); const total = (record.inputTokens || 0) + (record.outputTokens || 0); if (total === 0) { return; // 跳过无消耗的调用 } this.#drizzle .insert(tokenUsage) .values({ timestamp: now, source: record.source || 'unknown', dimension: record.dimension ?? null, provider: record.provider ?? null, model: record.model ?? null, inputTokens: record.inputTokens || 0, outputTokens: record.outputTokens || 0, totalTokens: total, durationMs: record.durationMs ?? null, toolCalls: record.toolCalls || 0, sessionId: record.sessionId ?? null, }) .run(); // 写入后使缓存失效 this.#reportCache = null; // 定期清理(每 100 次写入检查一次) if (Math.random() < 0.01) { this.#pruneStmt.run(MAX_ROWS); } } catch (err) { const message = err instanceof Error ? err.message : String(err); this.#logger.debug('[TokenUsageStore] record failed', { error: message }); } } // ─── 查询 ───────────────────────────────────────── /** * 近 7 日按日聚合统计 * @returns >} */ getLast7DaysDaily() { const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000; return this.#dailyStmt.all(sevenDaysAgo); } /** * 近 7 日按来源 (source) 聚合统计 * @returns >} */ getLast7DaysBySource() { const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000; return this.#bySourceStmt.all(sevenDaysAgo); } /** * 近 7 日总计 * @returns } */ getLast7DaysSummary() { const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000; const row = this.#summaryStmt.get(sevenDaysAgo); return { ...row, avg_per_call: row.call_count > 0 ? Math.round(row.total_tokens / row.call_count) : 0, }; } /** * 获取完整的 7 日报告(前端一次拉取) * 带 10s 内存缓存,避免高频请求重复查询 */ getLast7DaysReport() { const now = Date.now(); if (this.#reportCache && now < this.#reportCache.expireAt) { return this.#reportCache.data; } const data = { daily: this.getLast7DaysDaily(), bySource: this.getLast7DaysBySource(), summary: this.getLast7DaysSummary(), }; this.#reportCache = { data, expireAt: now + 10_000 }; // 10s 缓存 return data; } }