feat(core,db): 业务核心与数据访问层

core:DTO/错误信封/时间口径/微分折算/dedupe_key/加密与 PBKDF2/契约 zod/logtape/上游客户端。
db:八表 Drizzle schema + 仓储 + drizzle 配置。
This commit is contained in:
2026-10-06 01:30:51 +08:00
parent 4c55c3f15f
commit 6980d09750
17 changed files with 1839 additions and 0 deletions
+65
View File
@@ -0,0 +1,65 @@
/**
* 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}`;
}