mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
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:
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
21
frontend/src/api/endpoints/types/__tests__/provider.spec.ts
Normal file
21
frontend/src/api/endpoints/types/__tests__/provider.spec.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
|
||||
@@ -301,7 +301,12 @@ import {
|
||||
import { Server, SquarePen } from 'lucide-vue-next'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useFormDialog } from '@/composables/useFormDialog'
|
||||
import { createProvider, updateProvider, type ProviderWithEndpointsSummary } from '@/api/endpoints'
|
||||
import {
|
||||
createProvider,
|
||||
normalizePoolAdvancedConfig,
|
||||
updateProvider,
|
||||
type ProviderWithEndpointsSummary,
|
||||
} from '@/api/endpoints'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { parseNumberInput } from '@/utils/form'
|
||||
|
||||
@@ -388,6 +393,7 @@ function resetForm() {
|
||||
// 加载提供商数据(编辑模式)
|
||||
function loadProviderData() {
|
||||
if (!props.provider) return
|
||||
const poolAdvanced = normalizePoolAdvancedConfig(props.provider.pool_advanced)
|
||||
|
||||
form.value = {
|
||||
name: props.provider.name,
|
||||
@@ -412,7 +418,7 @@ function loadProviderData() {
|
||||
stream_first_byte_timeout: props.provider.stream_first_byte_timeout ?? undefined,
|
||||
request_timeout: props.provider.request_timeout ?? undefined,
|
||||
// 号池模式
|
||||
pool_mode_enabled: !!props.provider.pool_advanced,
|
||||
pool_mode_enabled: poolAdvanced !== null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,6 +454,7 @@ const handleSubmit = async () => {
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const currentPoolAdvanced = normalizePoolAdvancedConfig(props.provider?.pool_advanced)
|
||||
const basePayload = {
|
||||
name: form.value.name,
|
||||
provider_type: form.value.provider_type,
|
||||
@@ -466,7 +473,7 @@ const handleSubmit = async () => {
|
||||
stream_first_byte_timeout: form.value.stream_first_byte_timeout ?? null,
|
||||
request_timeout: form.value.request_timeout ?? null,
|
||||
pool_advanced: form.value.pool_mode_enabled
|
||||
? (props.provider?.pool_advanced ?? {})
|
||||
? (currentPoolAdvanced ?? {})
|
||||
: null,
|
||||
}
|
||||
|
||||
|
||||
@@ -619,9 +619,32 @@ function openProviderDrawer(providerId: string) {
|
||||
providerDrawerOpen.value = true
|
||||
}
|
||||
|
||||
function mergeUpdatedProvider(updated: ProviderWithEndpointsSummary) {
|
||||
const index = providers.value.findIndex(p => p.id === updated.id)
|
||||
if (index !== -1) {
|
||||
providers.value[index] = updated
|
||||
loadBalances([updated], false)
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshProviderSnapshot(
|
||||
providerId: string,
|
||||
fallbackErrorMessage = '刷新提供商数据失败',
|
||||
): Promise<ProviderWithEndpointsSummary | null> {
|
||||
try {
|
||||
const updated = await getProvider(providerId)
|
||||
mergeUpdatedProvider(updated)
|
||||
return updated
|
||||
} catch (err) {
|
||||
showError(parseApiError(err, fallbackErrorMessage), '错误')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 打开编辑提供商对话框
|
||||
function openEditProviderDialog(provider: ProviderWithEndpointsSummary) {
|
||||
providerToEdit.value = provider
|
||||
async function openEditProviderDialog(provider: ProviderWithEndpointsSummary) {
|
||||
const latest = await refreshProviderSnapshot(provider.id, '刷新提供商状态失败')
|
||||
providerToEdit.value = latest ?? provider
|
||||
providerDialogOpen.value = true
|
||||
}
|
||||
|
||||
@@ -640,27 +663,13 @@ function handleOpsConfigSaved() {
|
||||
|
||||
// 处理提供商编辑完成
|
||||
function handleProviderUpdated(updated: ProviderWithEndpointsSummary) {
|
||||
const index = providers.value.findIndex(p => p.id === updated.id)
|
||||
if (index !== -1) {
|
||||
providers.value[index] = updated
|
||||
// 刷新该提供商的余额数据
|
||||
loadBalances([updated], false)
|
||||
}
|
||||
mergeUpdatedProvider(updated)
|
||||
}
|
||||
|
||||
// 处理详情抽屉内的刷新:只刷新当前查看的那一条提供商
|
||||
async function handleDrawerRefresh() {
|
||||
if (!selectedProviderId.value) return
|
||||
try {
|
||||
const updated = await getProvider(selectedProviderId.value)
|
||||
const index = providers.value.findIndex(p => p.id === updated.id)
|
||||
if (index !== -1) {
|
||||
providers.value[index] = updated
|
||||
loadBalances([updated], false)
|
||||
}
|
||||
} catch (err) {
|
||||
showError(parseApiError(err, '刷新提供商数据失败'), '错误')
|
||||
}
|
||||
await refreshProviderSnapshot(selectedProviderId.value)
|
||||
}
|
||||
|
||||
// 优先级保存成功回调
|
||||
|
||||
Reference in New Issue
Block a user