87 lines
3.0 KiB
TypeScript
87 lines
3.0 KiB
TypeScript
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<TrendPoint[]> {
|
|
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),
|
|
}));
|
|
});
|
|
} |