feat(gateway): 重构 usage 数据层、迁移系统与系统导入

数据库迁移:
- 引入 baseline v2 bootstrap,空库首次启动自动初始化
- 服务启动不再自动执行迁移,需显式 `--migrate` 运行
- 新增 pending migration 检测,schema 落后时拒绝启动

Usage 数据层:
- usage body 存储外部化为独立 blob 表
- 新增 HTTP audit 表拆分存储请求/响应头与 body ref
- 后台清理任务支持 legacy body ref 元数据迁移
- usage runtime 写入迁移到专用 tokio runtime(独立线程池, 8MB 栈)

系统导入/导出:
- 支持用户、API Keys、钱包数据的完整导入
- 兼容 legacy 与 v1.3+ 两种导出格式

其他改进:
- executor outcome 增加 runtime miss 诊断上下文
- 主 tokio runtime 栈大小调整为 8MB
- 前端 provider 管理支持 base URL 配置
- dev.sh 支持 --migrate 参数
This commit is contained in:
fawney19
2026-04-13 14:01:22 +08:00
parent 3698e5a833
commit 5bb08e6aa4
106 changed files with 21736 additions and 1529 deletions

View File

@@ -7,6 +7,7 @@ import type {
ProviderWithEndpointsSummary,
ProxyConfig,
} from './types'
import { normalizePoolAdvancedConfig as normalizePoolAdvanced } from './types'
interface ProviderRequestOptions {
timeout?: number
@@ -31,6 +32,15 @@ export interface ProviderSummaryPageResponse {
items: ProviderWithEndpointsSummary[]
}
function normalizeProviderSummary(
provider: ProviderWithEndpointsSummary,
): ProviderWithEndpointsSummary {
return {
...provider,
pool_advanced: normalizePoolAdvanced(provider.pool_advanced),
}
}
export async function getProvidersSummary(
params: ProviderSummaryQuery = {},
): Promise<ProviderSummaryPageResponse> {
@@ -38,7 +48,10 @@ export async function getProvidersSummary(
'/api/admin/providers/summary',
{ params },
)
return response.data
return {
...response.data,
items: response.data.items.map(normalizeProviderSummary),
}
}
/**
@@ -46,7 +59,7 @@ export async function getProvidersSummary(
*/
export async function getProvider(providerId: string): Promise<ProviderWithEndpointsSummary> {
const response = await client.get<ProviderWithEndpointsSummary>(`/api/admin/providers/${providerId}/summary`)
return response.data
return normalizeProviderSummary(response.data)
}
/**
@@ -81,7 +94,7 @@ export async function updateProvider(
requestOptions?: ProviderRequestOptions,
): Promise<ProviderWithEndpointsSummary> {
const response = await client.patch(`/api/admin/providers/${providerId}`, data, requestOptions)
return response.data
return normalizeProviderSummary(response.data)
}
/**

View File

@@ -0,0 +1,21 @@
import { describe, expect, it } from 'vitest'
import { normalizePoolAdvancedConfig } from '@/api/endpoints/types'
describe('normalizePoolAdvancedConfig', () => {
it('keeps object payloads, including empty objects', () => {
expect(normalizePoolAdvancedConfig({})).toEqual({})
expect(normalizePoolAdvancedConfig({ global_priority: 5 })).toEqual({ global_priority: 5 })
})
it('maps legacy boolean payloads to the current object semantics', () => {
expect(normalizePoolAdvancedConfig(true)).toEqual({})
expect(normalizePoolAdvancedConfig(false)).toBeNull()
})
it('drops unsupported payload shapes', () => {
expect(normalizePoolAdvancedConfig(null)).toBeNull()
expect(normalizePoolAdvancedConfig('enabled')).toBeNull()
expect(normalizePoolAdvancedConfig(['lru'])).toBeNull()
})
})

View File

@@ -517,6 +517,17 @@ export interface PoolAdvancedConfig {
auto_remove_banned_keys?: boolean
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
export function normalizePoolAdvancedConfig(value: unknown): PoolAdvancedConfig | null {
if (value == null || value === false) return null
if (value === true) return {}
if (!isPlainObject(value)) return null
return { ...value } as PoolAdvancedConfig
}
export interface FailoverRuleItem {
pattern: string
description?: string