/** * seed 脚本(实施计划 §6):从仓库根的实测响应快照灌入开发数据——**幂等**,可重复执行。 * * 用法:在 `apps/server` 下 `bun run seed`(环境变量自动经 `src/lib/env.ts` 加载仓库根 `.env`)。 * * 灌入内容: * 1. 一个占位 `api_keys` 行(假 key 常量 `SEED_KEY`,哈希 / 密文走真实 crypto 逻辑—— * 查询面可用该常量 key 命中已登记 key) * 2. `usage_logs`:`api.log.token.json` 的 1000 条样例日志(`ON CONFLICT DO NOTHING`) * 3. `qpu_history` 基线:`api.status.json` 的 `quota_per_unit`(已有数据则跳过) * 4. 一条示例 `purchases`(订阅路径;**金额为占位值**,用于验证折算链) * * 注:快照文件未入库(`.gitignore` 的 `/api*.json`),本脚本只在本地开发环境使用。 */ import { readFileSync } from 'node:fs' import { eq } from 'drizzle-orm' import { DEFAULT_QUOTA_PER_UNIT } from '@naku/shared' import { getDb } from '../src/db/client' import { apiKeys, purchases, qpuHistory, usageLogs } from '../src/db/schema' import { getKeySecrets } from '../src/lib/keys' import { fromUnixSeconds } from '../src/lib/time' import { parseOther, toLogRow } from '../src/sync/log-row' import type { UpstreamLogRecord } from '../src/upstream/types' /** 假 key 常量:查看面 / 管理面用它做本地验证(可用 env SEED_KEY 覆盖) */ const SEED_KEY = process.env.SEED_KEY ?? 'sk-seed-demo-key-0001' /** 仓库根(本文件位于 apps/server/scripts/) */ const REPO_ROOT = new URL('../../../', import.meta.url) function readSnapshot(name: string): T { return JSON.parse(readFileSync(new URL(name, REPO_ROOT), 'utf8')) as T } function asNumber(v: unknown, fallback: number): number { return typeof v === 'number' && Number.isFinite(v) ? v : fallback } const db = getDb() const secrets = getKeySecrets() // ---------- 样例快照 ---------- const logs = readSnapshot<{ data?: UpstreamLogRecord[] }>('api.log.token.json').data if (!Array.isArray(logs) || logs.length === 0) { console.error('[seed] api.log.token.json 缺失或结构异常(缺 data[])——请确认样例文件在仓库根') process.exit(1) } const status = readSnapshot<{ data?: Record }>('api.status.json').data ?? {} // ---------- 1. api_keys ---------- const keyHash = secrets.keyHash(SEED_KEY) let [keyRow] = await db.select().from(apiKeys).where(eq(apiKeys.keyHash, keyHash)).limit(1) if (!keyRow) { const first = logs[0]! const other = parseOther(first.other) const totalGranted = asNumber(other.subscription_total, 800_000_000) const totalUsed = asNumber(other.subscription_used, 85_579_458) const now = new Date() ;[keyRow] = await db .insert(apiKeys) .values({ keyHash, keyCiphertext: secrets.encryptKey(SEED_KEY), status: 'active', disabledSource: null, consecutive401s: 0, logSyncedAt: now, usageSyncedAt: now, tokenName: first.token_name ?? 'seed-token', totalGranted, totalUsed, totalAvailable: totalGranted - totalUsed, unlimitedQuota: false, modelLimits: {}, modelLimitsEnabled: false, expiresAt: null, }) .returning() console.log(`[seed] api_keys:已插入 id=${keyRow!.id}(token_name=${keyRow!.tokenName})`) } else { console.log(`[seed] api_keys:已存在(id=${keyRow.id}),跳过`) } // ---------- 2. usage_logs ---------- const rows = logs.map((log) => toLogRow(keyRow!.id, log)) const inserted = await db .insert(usageLogs) .values(rows) .onConflictDoNothing({ target: usageLogs.dedupeKey }) .returning({ id: usageLogs.id }) console.log(`[seed] usage_logs:新增 ${inserted.length} 条(样例共 ${logs.length} 条)`) // ---------- 3. qpu_history 基线 ---------- const [qpuRow] = await db.select().from(qpuHistory).limit(1) if (!qpuRow) { const qpu = asNumber(status.quota_per_unit, DEFAULT_QUOTA_PER_UNIT) await db.insert(qpuHistory).values({ qpu, effectiveFrom: new Date() }) console.log(`[seed] qpu_history:已插入基线 qpu=${qpu}`) } else { console.log(`[seed] qpu_history:已有数据(qpu=${qpuRow.qpu}),跳过`) } // ---------- 4. 示例 purchases(金额为占位值) ---------- const [purchaseRow] = await db.select().from(purchases).limit(1) if (!purchaseRow) { const first = logs[0]! const other = parseOther(first.other) const earliestSeconds = Math.min(...logs.map((l) => l.created_at)) const earliest = fromUnixSeconds(earliestSeconds) await db.insert(purchases).values({ kind: 'subscription', paidAt: earliest, paidAmount: 168.5, // 占位值:实付金额只有作者知道 note: 'seed 示例(金额为占位值,非真实)', periodStart: earliest, periodEnd: null, subscriptionId: asNumber(other.subscription_id, 90), quotaGranted: asNumber(other.subscription_total, 800_000_000), }) console.log('[seed] purchases:已插入示例一条(折算链验证用)') } else { console.log(`[seed] purchases:已有数据(id=${purchaseRow.id}),跳过`) } console.log(`[seed] 完成。查询面可用假 key「${SEED_KEY}」命中已登记 key。`)