import { query } from "./db"; import { CACHE_CREATION, CACHE_READ, REAL_MODEL, cacheKey, cached, timeWhere } from "./query-shared"; // ── 趋势 ────────────────────────────────────────────────────── export interface TrendPoint { date: string; calls: number; prompt_tokens: number; completion_tokens: number; cache_creation_tokens: number; cache_read_tokens: number; total_tokens: number; quota: number; } export type TrendGranularity = "hour" | "day" | "week" | "month"; export interface TrendFilters { username?: string; model?: string; channelId?: number; } function appendTrendFilters( where: string, params: (string | number | boolean | null)[], filters: TrendFilters = {} ): string { if (filters.username) { params.push(filters.username); where += ` AND username = $${params.length}`; } if (filters.model) { params.push(filters.model); where += ` AND ${REAL_MODEL} = $${params.length}`; } if (filters.channelId !== undefined) { params.push(filters.channelId); where += ` AND channel_id = $${params.length}`; } return where; } export function getTrends( granularity: TrendGranularity = "day", startTs?: number, endTs?: number, filters: TrendFilters = {} ): Promise { return cached(cacheKey("trends", granularity, startTs, endTs, filters.username, filters.model, filters.channelId), async () => { const params: (string | number | boolean | null)[] = []; const where = appendTrendFilters(timeWhere(params, startTs, endTs), params, filters); const truncExpr = granularity === "hour" ? `to_char(date_trunc('hour', to_timestamp(created_at) AT TIME ZONE 'Asia/Shanghai'), 'YYYY-MM-DD HH24:00')` : granularity === "day" ? `((to_timestamp(created_at) AT TIME ZONE 'Asia/Shanghai')::date)::text` : `(date_trunc('${granularity}', to_timestamp(created_at) AT TIME ZONE 'Asia/Shanghai')::date)::text`; const rows = await query( `SELECT ${truncExpr} as date, COUNT(*)::int as calls, COALESCE(SUM(prompt_tokens), 0)::bigint as prompt_tokens, COALESCE(SUM(completion_tokens), 0)::bigint as completion_tokens, COALESCE(SUM(${CACHE_CREATION}), 0)::bigint as cache_creation_tokens, COALESCE(SUM(${CACHE_READ}), 0)::bigint as cache_read_tokens, COALESCE(SUM(quota), 0)::bigint as quota FROM logs WHERE ${where} GROUP BY date ORDER BY date`, params ); return rows.map((r) => ({ date: String(r.date).slice(0, granularity === "hour" ? 16 : 10), calls: Number(r.calls), prompt_tokens: Number(r.prompt_tokens), completion_tokens: Number(r.completion_tokens), cache_creation_tokens: Number(r.cache_creation_tokens), cache_read_tokens: Number(r.cache_read_tokens), total_tokens: Number(r.prompt_tokens) + Number(r.completion_tokens) + Number(r.cache_creation_tokens) + Number(r.cache_read_tokens), quota: Number(r.quota), })); }); }