/** * 上游客户端 · 样例文件实现(实施计划 §6:仓库根实测响应快照 —— `api.log.token.json` * / `api.status.json`,未入库、绝不提交)。开发期以 `UPSTREAM_SOURCE=fixture` 启用。 * * 快照缺失的接口(`/api/usage/token` 无快照)就地按实测结构合成:总量类字段取自 * `api.log.token.json` 首条日志的 `other.subscription_*`(真实形状),保证 fixture * 下的登记结果与 seed 数据(阶段 4)同一形状。 * * 错误分支的仿真约定(key 内容大小写不敏感;仅 fixture 模式生效,见实施计划「实现假设」): * 含 `reject` → 401 key_rejected · 含 `forbidden` → 403 key_rejected · 含 `unavailable` → 网络故障 */ import { readFileSync } from 'node:fs' import { DEFAULT_QUOTA_PER_UNIT } from '@naku/shared' import { env } from '../lib/env' import { errorMessage, UpstreamError } from './errors' import type { UpstreamClient } from './client' import type { UpstreamLogRecord, UpstreamStatus, UpstreamTokenUsage } from './types' /** 仓库根(本文件位于 apps/server/src/upstream/)——样例快照的默认目录 */ const REPO_ROOT = new URL('../../../../', import.meta.url) export interface FixtureUpstreamOptions { /** 样例文件目录;缺省取 env.newapiFixtureDir,再退到仓库根 */ dir?: string | URL } function readSnapshot(dir: string | URL, name: string): T { try { return JSON.parse(readFileSync(new URL(name, dir), 'utf8')) as T } catch (err) { throw new UpstreamError( 'upstream_unavailable', `样例文件不可用(${name}):${errorMessage(err)}`, ) } } function asNumber(v: unknown, fallback: number): number { return typeof v === 'number' && Number.isFinite(v) ? v : fallback } export function createFixtureClient(options: FixtureUpstreamOptions = {}): UpstreamClient { // env.newapiFixtureDir 为「空 = 仓库根」的语义,故用 ||(空字符串要退到 REPO_ROOT) const dir = options.dir || env.newapiFixtureDir || REPO_ROOT /** 仿真约定:错误分支由 key 内容触发 */ function simulate(key: string): void { const k = key.toLowerCase() if (k.includes('reject')) throw new UpstreamError('key_rejected', 'fixture:模拟上游拒绝该 key(401)', 401) if (k.includes('forbidden')) throw new UpstreamError('key_rejected', 'fixture:模拟账号被封禁(403)', 403) if (k.includes('unavailable')) { throw new UpstreamError('upstream_unavailable', 'fixture:模拟上游不可达(网络故障)') } } function loadLogs(): UpstreamLogRecord[] { const snap = readSnapshot<{ data?: UpstreamLogRecord[] }>(dir, 'api.log.token.json') if (!Array.isArray(snap.data)) { throw new UpstreamError('upstream_unavailable', '样例文件结构异常(api.log.token.json 缺 data[])') } return snap.data } return { async fetchTokenUsage(plainKey: string): Promise { simulate(plainKey) const first = loadLogs()[0] let other: Record = {} if (first?.other) { try { other = JSON.parse(first.other) as Record } catch { other = {} // 样例 other 解析失败不致命:退到兜底值 } } const totalGranted = asNumber(other.subscription_total, 800_000_000) const totalUsed = asNumber(other.subscription_used, 85_579_458) return { name: first?.token_name ?? 'fixture-token', total_granted: totalGranted, total_used: totalUsed, total_available: totalGranted - totalUsed, unlimited_quota: false, model_limits: {}, model_limits_enabled: false, expires_at: 0, // 永不过期 } }, async fetchLogs(plainKey: string): Promise { simulate(plainKey) return loadLogs() }, async fetchStatus(): Promise { const snap = readSnapshot<{ data?: Record }>(dir, 'api.status.json') const data = snap.data if (!data || typeof data !== 'object') { throw new UpstreamError('upstream_unavailable', '样例文件结构异常(api.status.json 缺 data)') } return { ...data, quota_per_unit: asNumber(data.quota_per_unit, DEFAULT_QUOTA_PER_UNIT), } as UpstreamStatus }, } }