core:DTO/错误信封/时间口径/微分折算/dedupe_key/加密与 PBKDF2/契约 zod/logtape/上游客户端。 db:八表 Drizzle schema + 仓储 + drizzle 配置。
66 lines
1.9 KiB
TypeScript
66 lines
1.9 KiB
TypeScript
/**
|
||
* usage_logs.dedupe_key 生成(usage_logs表字段设计 §3.1)
|
||
* 消费行 type=2: `${api_key_id}:${request_id}`
|
||
* 非消费行: `${api_key_id}:${request_id}:${sha256指纹}`
|
||
* request_id 空:退化为纯内容指纹
|
||
*/
|
||
|
||
export interface DedupeInput {
|
||
api_key_id: number;
|
||
request_id: string | null | undefined;
|
||
type: number;
|
||
created_at: string | number | Date;
|
||
model_name: string;
|
||
quota: number;
|
||
prompt_tokens: number;
|
||
completion_tokens: number;
|
||
use_time: number;
|
||
content: string;
|
||
}
|
||
|
||
const LOG_TYPE_CONSUME = 2;
|
||
|
||
function toUnix(created_at: string | number | Date): number {
|
||
if (typeof created_at === "number") return created_at;
|
||
if (created_at instanceof Date)
|
||
return Math.floor(created_at.getTime() / 1000);
|
||
return Math.floor(new Date(created_at).getTime() / 1000);
|
||
}
|
||
|
||
async function sha256Hex(input: string): Promise<string> {
|
||
const digest = await crypto.subtle.digest(
|
||
"SHA-256",
|
||
new TextEncoder().encode(input),
|
||
);
|
||
return [...new Uint8Array(digest)]
|
||
.map((b) => b.toString(16).padStart(2, "0"))
|
||
.join("");
|
||
}
|
||
|
||
/** 内容指纹(错误行含 content;空 request_id 的消费行不含 content) */
|
||
export async function contentFingerprint(row: DedupeInput): Promise<string> {
|
||
const parts = [
|
||
String(row.api_key_id),
|
||
String(toUnix(row.created_at)),
|
||
String(row.type),
|
||
row.model_name,
|
||
String(row.quota),
|
||
String(row.prompt_tokens),
|
||
String(row.completion_tokens),
|
||
String(row.use_time),
|
||
];
|
||
if (row.type !== LOG_TYPE_CONSUME || !row.request_id) {
|
||
parts.push(row.content);
|
||
}
|
||
return sha256Hex(parts.join("\n"));
|
||
}
|
||
|
||
export async function buildDedupeKey(row: DedupeInput): Promise<string> {
|
||
const rid = row.request_id?.trim() ?? "";
|
||
if (row.type === LOG_TYPE_CONSUME && rid) {
|
||
return `${row.api_key_id}:${rid}`;
|
||
}
|
||
const fp = await contentFingerprint(row);
|
||
return rid ? `${row.api_key_id}:${rid}:${fp}` : `${row.api_key_id}:${fp}`;
|
||
}
|