Files
new-api-key-usage/apps/server/scripts/seed.ts
T
skycurtain e1c6d2153e feat(server): 同步引擎与 seed(阶段 4)
- 引擎:tick(5 分钟 + 防重入 + 启动即跑)· 单轮串行遍历 active key · 日志整批单事务入库(ON CONFLICT DO NOTHING + log_synced_at 取请求发起时刻)· 额度采集(上游字段全组,独立 try)· QPU 采样(基线 / 值变化才追加)· 每轮摘要与超期 warning
- 401 状态机:N=3 摘除(disabled_source=auto_401);403/500/网络不计数;解密失败跳过不计数
- 纯逻辑抽层:policy(计数 / 合并判定)· dedupe(去重键与内容指纹)· log-row(日志映射,与 seed 共用)
- seed 脚本(幂等):假 key + 984 条日志 + QPU 基线 + 示例购买
- 实测:401 端到端 1→2→3 摘除且摘除后不再轮询;幂等重跑新增 0 条
- 实证发现:错误行 request_id 不唯一(8 组 ×3)——记入实施计划「待定问题 #5」与 upstream 文档,待作者裁决
- AGENTS.md / 根 package.json 增 seed 命令
2026-09-15 00:46:46 +08:00

128 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<T>(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<string, unknown> }>('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。`)