mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
refactor: 统一代理配置优先级链(key>provider>系统默认)并复用 HTTP 连接池
- 引入 resolve_proxy_param / build_proxy_client_kwargs 工具函数,统一 httpx 客户端的代理+SSL+超时配置,替换各模块中零散的 get_ssl_context() 调用 - 所有涉及上游请求的模块(provider_query, usage replay, endpoint check, model fetch, OAuth, Vertex Auth, Gemini Files/Video 等)改用 resolve_effective_proxy 按 key > provider > 系统默认优先级解析代理 - 流式请求改用 HTTPClientPool.get_upstream_client 复用连接池,移除各处 http_client.aclose() 避免关闭共享客户端 - StreamProcessor._cleanup 不再关闭池中客户端,仅清理响应上下文 - 前端 EndpointFormDialog 增加 body_rules 帮助说明 Popover - Mock handler 补充 OAuth 字段、endpoint extras 及新增 mock 路由
This commit is contained in:
@@ -294,9 +294,71 @@
|
||||
|
||||
<div
|
||||
v-if="getEndpointEditBodyRules(endpoint.id).length > 0"
|
||||
class="text-xs text-muted-foreground px-2"
|
||||
class="flex items-center gap-1 text-xs text-muted-foreground px-2"
|
||||
>
|
||||
<code class="bg-muted px-1 rounded">.</code> 嵌套字段 / <code class="bg-muted px-1 rounded">[N]</code> 数组索引;值为 JSON 格式
|
||||
<span><code class="bg-muted px-1 rounded">.</code> 嵌套字段 / <code class="bg-muted px-1 rounded">[N]</code> 数组索引;值为 JSON 格式</span>
|
||||
<div class="flex-1" />
|
||||
<Popover
|
||||
:open="bodyRuleHelpOpenEndpointId === endpoint.id"
|
||||
@update:open="(v: boolean) => setBodyRuleHelpOpen(endpoint.id, v)"
|
||||
>
|
||||
<PopoverTrigger as-child>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 h-6 w-6 inline-flex items-center justify-center rounded-md hover:bg-muted/60"
|
||||
title="规则说明"
|
||||
aria-label="规则说明"
|
||||
>
|
||||
<HelpCircle class="w-3.5 h-3.5 text-muted-foreground/60" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side="bottom"
|
||||
align="end"
|
||||
:side-offset="6"
|
||||
class="w-80 p-3 !z-[90]"
|
||||
>
|
||||
<div class="text-xs space-y-2">
|
||||
<div>
|
||||
<div class="font-medium mb-0.5">
|
||||
路径语法
|
||||
</div>
|
||||
<div class="text-muted-foreground">
|
||||
<code>metadata.user_id</code> 嵌套字段<br>
|
||||
<code>messages[0].content</code> 数组索引<br>
|
||||
<code>config\.v1.key</code> 转义点号
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium mb-0.5">
|
||||
值格式 (JSON)
|
||||
</div>
|
||||
<div class="text-muted-foreground">
|
||||
<code>123</code> 数字 / <code>"text"</code> 字符串 / <code>true</code> 布尔<br>
|
||||
<code>{"k":"v"}</code> 对象 / <code>[1,2]</code> 数组 / <code>null</code><br>
|
||||
<code v-pre>{{$original}}</code> 引用原值
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium mb-0.5">
|
||||
条件运算符
|
||||
</div>
|
||||
<div class="text-muted-foreground">
|
||||
<code>eq</code> <code>neq</code> 等于/不等于<br>
|
||||
<code>gt</code> <code>lt</code> <code>gte</code> <code>lte</code> 大小比较<br>
|
||||
<code>starts_with</code> <code>ends_with</code> <code>contains</code> 字符串匹配<br>
|
||||
<code>matches</code> 正则匹配<br>
|
||||
<code>exists</code> <code>not_exists</code> 字段存在性<br>
|
||||
<code>in</code> 在列表中(值填 <code>["a","b"]</code>)<br>
|
||||
<code>type_is</code> 类型判断(string/number/boolean/array/object/null)
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted-foreground">
|
||||
规则按顺序执行,前面的修改对后续规则可见。
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<!-- 请求体规则列表 - 次要色边框 -->
|
||||
@@ -338,6 +400,16 @@
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7 shrink-0"
|
||||
:class="rule.conditionEnabled ? 'text-primary' : ''"
|
||||
title="条件触发"
|
||||
@click="toggleBodyRuleCondition(endpoint.id, index)"
|
||||
>
|
||||
<Filter class="w-3 h-3" />
|
||||
</Button>
|
||||
<template v-if="rule.action === 'set'">
|
||||
<Input
|
||||
:model-value="rule.path"
|
||||
@@ -452,16 +524,6 @@
|
||||
:title="getRegexPatternValidationTip(rule)"
|
||||
/>
|
||||
</template>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7 shrink-0"
|
||||
:class="rule.conditionEnabled ? 'text-primary' : ''"
|
||||
title="条件触发"
|
||||
@click="toggleBodyRuleCondition(endpoint.id, index)"
|
||||
>
|
||||
<Filter class="w-3 h-3" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -664,8 +726,11 @@ import {
|
||||
Collapsible,
|
||||
CollapsibleTrigger,
|
||||
CollapsibleContent,
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
} from '@/components/ui'
|
||||
import { Settings, Trash2, Check, X, Power, ChevronRight, Plus, Shuffle, RotateCcw, Radio, CheckCircle, Save, Filter } from 'lucide-vue-next'
|
||||
import { Settings, Trash2, Check, X, Power, ChevronRight, Plus, Shuffle, RotateCcw, Radio, CheckCircle, Save, Filter, HelpCircle } from 'lucide-vue-next'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { log } from '@/utils/logger'
|
||||
import AlertDialog from '@/components/common/AlertDialog.vue'
|
||||
@@ -815,6 +880,13 @@ const endpointRulesExpanded = ref<Record<string, boolean>>({})
|
||||
// 请求体规则 Select 的展开状态
|
||||
const bodyRuleSelectOpen = ref<Record<string, boolean>>({})
|
||||
|
||||
// 请求体规则说明 Popover 的展开状态
|
||||
const bodyRuleHelpOpenEndpointId = ref<string | null>(null)
|
||||
|
||||
function setBodyRuleHelpOpen(endpointId: string, open: boolean) {
|
||||
bodyRuleHelpOpenEndpointId.value = open ? endpointId : null
|
||||
}
|
||||
|
||||
// 每个端点的编辑状态(内联编辑)
|
||||
const endpointEditStates = ref<Record<string, EndpointEditState>>({})
|
||||
|
||||
@@ -1783,6 +1855,7 @@ onMounted(() => {
|
||||
|
||||
// 监听 props 变化
|
||||
watch(() => props.modelValue, (open) => {
|
||||
bodyRuleHelpOpenEndpointId.value = null
|
||||
if (open) {
|
||||
localEndpoints.value = [...(props.endpoints || [])]
|
||||
// 清空编辑状态,重新从端点加载
|
||||
|
||||
@@ -416,18 +416,60 @@ const MOCK_ALIASES = [
|
||||
{ id: 'alias-004', source_model: 'gemini-pro', target_global_model_id: 'gm-005', target_global_model_name: 'gemini-3-pro-preview', target_global_model_display_name: 'Gemini 3 Pro Preview', provider_id: null, provider_name: null, scope: 'global', mapping_type: 'alias', is_active: true, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z' }
|
||||
]
|
||||
|
||||
function normalizeApiFormat(apiFormat: string): string {
|
||||
return apiFormat.toLowerCase().replace(/_/g, ':')
|
||||
}
|
||||
|
||||
function getMockEndpointExtras(apiFormat: string) {
|
||||
const normalizedFormat = normalizeApiFormat(apiFormat)
|
||||
const extras: Record<string, any> = {}
|
||||
|
||||
if (normalizedFormat === 'claude:chat') {
|
||||
extras.header_rules = [
|
||||
{ action: 'set', key: 'x-app-id', value: 'demo-app' },
|
||||
{ action: 'rename', from: 'x-client-id', to: 'x-client' },
|
||||
{ action: 'drop', key: 'x-debug' }
|
||||
]
|
||||
extras.body_rules = [
|
||||
{ action: 'set', path: 'metadata.user_id', value: 'demo-user' },
|
||||
{ action: 'insert', path: 'messages', index: 0, value: { role: 'system', content: 'You are a helpful assistant.' } },
|
||||
{ action: 'regex_replace', path: 'messages[0].content', pattern: '\\s+', replacement: ' ', flags: 'm', condition: { path: 'metadata.source', op: 'eq', value: 'internal' } }
|
||||
]
|
||||
} else if (normalizedFormat === 'openai:chat') {
|
||||
extras.custom_path = '/v1/chat/completions'
|
||||
extras.header_rules = [
|
||||
{ action: 'set', key: 'x-client', value: 'demo' }
|
||||
]
|
||||
extras.format_acceptance_config = {
|
||||
enabled: true,
|
||||
accept_formats: ['openai:chat', 'claude:chat']
|
||||
}
|
||||
extras.config = { upstream_stream_policy: 'force_stream' }
|
||||
} else if (normalizedFormat === 'openai:cli') {
|
||||
extras.config = { upstream_stream_policy: 'force_non_stream' }
|
||||
} else if (normalizedFormat === 'gemini:chat') {
|
||||
extras.custom_path = '/v1beta/models/gemini-3-pro-preview:generateContent'
|
||||
extras.body_rules = [
|
||||
{ action: 'drop', path: 'metadata.debug' }
|
||||
]
|
||||
}
|
||||
|
||||
return extras
|
||||
}
|
||||
|
||||
|
||||
// Mock Endpoint Keys
|
||||
const MOCK_ENDPOINT_KEYS = [
|
||||
{ id: 'ekey-001', provider_id: 'provider-001', api_formats: ['claude:chat'], api_key_masked: 'sk-ant...abc1', name: 'Primary Key', rate_multiplier: 1.0, internal_priority: 1, health_score: 0.98, consecutive_failures: 0, request_count: 5000, success_count: 4950, error_count: 50, success_rate: 0.99, avg_response_time_ms: 1200, cache_ttl_minutes: 5, max_probe_interval_minutes: 32, is_active: true, created_at: '2024-01-01T00:00:00Z', updated_at: new Date().toISOString() },
|
||||
{ id: 'ekey-002', provider_id: 'provider-001', api_formats: ['claude:chat'], api_key_masked: 'sk-ant...def2', name: 'Backup Key', rate_multiplier: 1.0, internal_priority: 2, health_score: 0.95, consecutive_failures: 1, request_count: 2000, success_count: 1950, error_count: 50, success_rate: 0.975, avg_response_time_ms: 1350, cache_ttl_minutes: 5, max_probe_interval_minutes: 32, is_active: true, created_at: '2024-02-01T00:00:00Z', updated_at: new Date().toISOString() },
|
||||
{ id: 'ekey-003', provider_id: 'provider-002', api_formats: ['openai:chat'], api_key_masked: 'sk-oai...ghi3', name: 'OpenAI Main', rate_multiplier: 1.0, internal_priority: 1, health_score: 0.97, consecutive_failures: 0, request_count: 3500, success_count: 3450, error_count: 50, success_rate: 0.986, avg_response_time_ms: 900, cache_ttl_minutes: 5, max_probe_interval_minutes: 32, is_active: true, created_at: '2024-01-15T00:00:00Z', updated_at: new Date().toISOString() }
|
||||
{ id: 'ekey-001', provider_id: 'provider-001', api_formats: ['claude:chat'], api_key_masked: 'sk-ant...abc1', auth_type: 'api_key', name: 'Primary Key', rate_multiplier: 1.0, internal_priority: 1, health_score: 0.98, consecutive_failures: 0, request_count: 5000, success_count: 4950, error_count: 50, success_rate: 0.99, avg_response_time_ms: 1200, cache_ttl_minutes: 5, max_probe_interval_minutes: 32, is_active: true, created_at: '2024-01-01T00:00:00Z', updated_at: new Date().toISOString() },
|
||||
{ id: 'ekey-002', provider_id: 'provider-001', api_formats: ['claude:chat'], api_key_masked: 'sk-ant...def2', auth_type: 'api_key', name: 'Backup Key', rate_multiplier: 1.0, internal_priority: 2, health_score: 0.95, consecutive_failures: 1, request_count: 2000, success_count: 1950, error_count: 50, success_rate: 0.975, avg_response_time_ms: 1350, cache_ttl_minutes: 5, max_probe_interval_minutes: 32, is_active: true, created_at: '2024-02-01T00:00:00Z', updated_at: new Date().toISOString() },
|
||||
{ id: 'ekey-003', provider_id: 'provider-002', api_formats: ['openai:chat'], api_key_masked: 'sk-oai...ghi3', auth_type: 'oauth', name: 'OpenAI OAuth', oauth_email: 'oauth-demo@aether.dev', oauth_expires_at: Math.floor(Date.now() / 1000) + 6 * 3600, oauth_plan_type: 'pro', oauth_account_id: 'acct-demo-002', rate_multiplier: 1.0, internal_priority: 1, health_score: 0.97, consecutive_failures: 0, request_count: 3500, success_count: 3450, error_count: 50, success_rate: 0.986, avg_response_time_ms: 900, cache_ttl_minutes: 5, max_probe_interval_minutes: 32, is_active: true, created_at: '2024-01-15T00:00:00Z', updated_at: new Date().toISOString() }
|
||||
]
|
||||
|
||||
// Mock Endpoints
|
||||
const MOCK_ENDPOINTS = [
|
||||
{ id: 'ep-001', provider_id: 'provider-001', provider_name: 'anthropic', api_format: 'claude:chat', base_url: 'https://api.anthropic.com', max_retries: 2, is_active: true, total_keys: 2, active_keys: 2, created_at: '2024-01-01T00:00:00Z', updated_at: new Date().toISOString() },
|
||||
{ id: 'ep-002', provider_id: 'provider-002', provider_name: 'openai', api_format: 'openai:chat', base_url: 'https://api.openai.com', max_retries: 2, is_active: true, total_keys: 1, active_keys: 1, created_at: '2024-01-01T00:00:00Z', updated_at: new Date().toISOString() },
|
||||
{ id: 'ep-003', provider_id: 'provider-003', provider_name: 'google', api_format: 'gemini:chat', base_url: 'https://generativelanguage.googleapis.com', max_retries: 2, is_active: true, total_keys: 1, active_keys: 1, created_at: '2024-01-15T00:00:00Z', updated_at: new Date().toISOString() }
|
||||
{ id: 'ep-001', provider_id: 'provider-001', provider_name: 'anthropic', api_format: 'claude:chat', base_url: 'https://api.anthropic.com', max_retries: 2, is_active: true, total_keys: 2, active_keys: 2, created_at: '2024-01-01T00:00:00Z', updated_at: new Date().toISOString(), ...getMockEndpointExtras('claude:chat') },
|
||||
{ id: 'ep-002', provider_id: 'provider-002', provider_name: 'openai', api_format: 'openai:chat', base_url: 'https://api.openai.com', max_retries: 2, is_active: true, total_keys: 1, active_keys: 1, created_at: '2024-01-01T00:00:00Z', updated_at: new Date().toISOString(), ...getMockEndpointExtras('openai:chat') },
|
||||
{ id: 'ep-003', provider_id: 'provider-003', provider_name: 'google', api_format: 'gemini:chat', base_url: 'https://generativelanguage.googleapis.com', max_retries: 2, is_active: true, total_keys: 1, active_keys: 1, created_at: '2024-01-15T00:00:00Z', updated_at: new Date().toISOString(), ...getMockEndpointExtras('gemini:chat') }
|
||||
]
|
||||
|
||||
// Mock 能力定义
|
||||
@@ -1212,21 +1254,24 @@ function generateMockEndpointsForProvider(providerId: string) {
|
||||
if (!provider || provider.api_formats.length === 0) return []
|
||||
|
||||
return provider.api_formats.map((format, index) => {
|
||||
const normalizedFormat = normalizeApiFormat(format)
|
||||
const healthDetail = provider.endpoint_health_details.find(h => h.api_format === format)
|
||||
const baseUrl = normalizedFormat.includes('claude') ? 'https://api.anthropic.com' :
|
||||
normalizedFormat.includes('openai') ? 'https://api.openai.com' :
|
||||
'https://generativelanguage.googleapis.com'
|
||||
return {
|
||||
id: `ep-${providerId}-${index + 1}`,
|
||||
provider_id: providerId,
|
||||
provider_name: provider.name,
|
||||
api_format: format,
|
||||
base_url: format.includes('claude') ? 'https://api.anthropic.com' :
|
||||
format.includes('openai') ? 'https://api.openai.com' :
|
||||
'https://generativelanguage.googleapis.com',
|
||||
base_url: baseUrl,
|
||||
max_retries: 2,
|
||||
is_active: healthDetail?.is_active ?? true,
|
||||
total_keys: Math.ceil(Math.random() * 3) + 1,
|
||||
active_keys: Math.ceil(Math.random() * 2) + 1,
|
||||
created_at: provider.created_at,
|
||||
updated_at: new Date().toISOString()
|
||||
updated_at: new Date().toISOString(),
|
||||
...getMockEndpointExtras(normalizedFormat)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1236,28 +1281,44 @@ const PROVIDER_KEYS_CACHE: Record<string, any[]> = {}
|
||||
function generateMockKeysForProvider(providerId: string, count: number = 2) {
|
||||
const provider = MOCK_PROVIDERS.find(p => p.id === providerId)
|
||||
const formats = provider?.api_formats || []
|
||||
const nowSec = Math.floor(Date.now() / 1000)
|
||||
|
||||
return Array.from({ length: count }, (_, i) => ({
|
||||
id: `key-${providerId}-${i + 1}`,
|
||||
provider_id: providerId,
|
||||
api_formats: i === 0 ? formats : formats.slice(0, 1),
|
||||
api_key_masked: `sk-***...${Math.random().toString(36).substring(2, 6)}`,
|
||||
name: i === 0 ? 'Primary Key' : `Backup Key ${i}`,
|
||||
rate_multiplier: 1.0,
|
||||
internal_priority: i + 1,
|
||||
health_score: 0.90 + Math.random() * 0.10, // 0.90-1.00
|
||||
consecutive_failures: Math.random() > 0.8 ? 1 : 0,
|
||||
request_count: 1000 + Math.floor(Math.random() * 5000),
|
||||
success_count: 950 + Math.floor(Math.random() * 4800),
|
||||
error_count: Math.floor(Math.random() * 100),
|
||||
success_rate: 0.95 + Math.random() * 0.04, // 0.95-0.99
|
||||
avg_response_time_ms: 800 + Math.floor(Math.random() * 600),
|
||||
cache_ttl_minutes: 5,
|
||||
max_probe_interval_minutes: 32,
|
||||
is_active: true,
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: new Date().toISOString()
|
||||
}))
|
||||
return Array.from({ length: count }, (_, i) => {
|
||||
const isOAuth = i === 1
|
||||
const markInvalid = isOAuth && providerId.endsWith('3')
|
||||
const oauthFields = isOAuth ? {
|
||||
auth_type: 'oauth',
|
||||
oauth_email: 'oauth-demo@aether.dev',
|
||||
oauth_expires_at: markInvalid ? null : nowSec + 6 * 3600,
|
||||
oauth_invalid_at: markInvalid ? nowSec - 3600 : null,
|
||||
oauth_invalid_reason: markInvalid ? '[ACCOUNT_BLOCK] Demo verification required' : null,
|
||||
oauth_plan_type: 'pro',
|
||||
oauth_account_id: `acct-${providerId}`
|
||||
} : { auth_type: 'api_key' }
|
||||
|
||||
return {
|
||||
id: `key-${providerId}-${i + 1}`,
|
||||
provider_id: providerId,
|
||||
api_formats: i === 0 ? formats : formats.slice(0, 1),
|
||||
api_key_masked: `sk-***...${Math.random().toString(36).substring(2, 6)}`,
|
||||
name: i === 0 ? 'Primary Key' : `Backup Key ${i}`,
|
||||
...oauthFields,
|
||||
rate_multiplier: 1.0,
|
||||
internal_priority: i + 1,
|
||||
health_score: 0.90 + Math.random() * 0.10,
|
||||
consecutive_failures: Math.random() > 0.8 ? 1 : 0,
|
||||
request_count: 1000 + Math.floor(Math.random() * 5000),
|
||||
success_count: 950 + Math.floor(Math.random() * 4800),
|
||||
error_count: Math.floor(Math.random() * 100),
|
||||
success_rate: 0.95 + Math.random() * 0.04,
|
||||
avg_response_time_ms: 800 + Math.floor(Math.random() * 600),
|
||||
cache_ttl_minutes: 5,
|
||||
max_probe_interval_minutes: 32,
|
||||
is_active: true,
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: new Date().toISOString()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 为 provider 生成 models
|
||||
@@ -1492,6 +1553,7 @@ registerDynamicRoute('POST', '/api/admin/endpoints/providers/:providerId/keys',
|
||||
api_formats: body.api_formats || [],
|
||||
api_key_masked: masked,
|
||||
api_key_plain: null,
|
||||
auth_type: body.auth_type || 'api_key',
|
||||
name: body.name || 'New Key',
|
||||
note: body.note,
|
||||
rate_multiplier: body.rate_multiplier ?? 1.0,
|
||||
@@ -1522,6 +1584,102 @@ registerDynamicRoute('POST', '/api/admin/endpoints/providers/:providerId/keys',
|
||||
return createMockResponse(newKey)
|
||||
})
|
||||
|
||||
registerDynamicRoute('POST', '/api/admin/endpoints/providers/:providerId/refresh-quota', async (_config, params) => {
|
||||
await delay()
|
||||
requireAdmin()
|
||||
if (!PROVIDER_KEYS_CACHE[params.providerId]) {
|
||||
PROVIDER_KEYS_CACHE[params.providerId] = generateMockKeysForProvider(params.providerId, 2)
|
||||
}
|
||||
const keys = PROVIDER_KEYS_CACHE[params.providerId] || []
|
||||
const results = keys.map(key => ({
|
||||
key_id: key.id,
|
||||
key_name: key.name || key.id.slice(0, 8),
|
||||
status: 'success',
|
||||
metadata: { updated_at: new Date().toISOString() }
|
||||
}))
|
||||
return createMockResponse({
|
||||
success: results.length,
|
||||
failed: 0,
|
||||
total: results.length,
|
||||
results
|
||||
})
|
||||
})
|
||||
|
||||
registerDynamicRoute('POST', '/api/admin/provider-oauth/keys/:keyId/refresh', async (_config, params) => {
|
||||
await delay()
|
||||
requireAdmin()
|
||||
return createMockResponse({
|
||||
provider_type: 'codex',
|
||||
expires_at: Math.floor(Date.now() / 1000) + 6 * 3600,
|
||||
has_refresh_token: true,
|
||||
email: 'oauth-demo@aether.dev',
|
||||
key_id: params.keyId
|
||||
})
|
||||
})
|
||||
|
||||
registerDynamicRoute('POST', '/api/admin/provider-oauth/providers/:providerId/start', async (_config, params) => {
|
||||
await delay()
|
||||
requireAdmin()
|
||||
return createMockResponse({
|
||||
authorization_url: `https://example.com/oauth/authorize?provider=${params.providerId}`,
|
||||
redirect_uri: 'https://aether.local/oauth/callback',
|
||||
provider_type: 'codex',
|
||||
instructions: 'Open the authorization URL and paste the callback URL here.'
|
||||
})
|
||||
})
|
||||
|
||||
registerDynamicRoute('POST', '/api/admin/provider-oauth/providers/:providerId/complete', async (config, _params) => {
|
||||
await delay()
|
||||
requireAdmin()
|
||||
const body = JSON.parse(config.data || '{}')
|
||||
return createMockResponse({
|
||||
key_id: `key-oauth-${Date.now()}`,
|
||||
provider_type: 'codex',
|
||||
expires_at: Math.floor(Date.now() / 1000) + 24 * 3600,
|
||||
has_refresh_token: true,
|
||||
email: body.name ? `${body.name}@demo.dev` : 'oauth-demo@aether.dev'
|
||||
})
|
||||
})
|
||||
|
||||
registerDynamicRoute('POST', '/api/admin/provider-oauth/providers/:providerId/import-refresh-token', async (config, _params) => {
|
||||
await delay()
|
||||
requireAdmin()
|
||||
const body = JSON.parse(config.data || '{}')
|
||||
return createMockResponse({
|
||||
key_id: `key-oauth-${Date.now()}`,
|
||||
provider_type: 'codex',
|
||||
expires_at: Math.floor(Date.now() / 1000) + 24 * 3600,
|
||||
has_refresh_token: true,
|
||||
email: body.name ? `${body.name}@demo.dev` : 'oauth-demo@aether.dev'
|
||||
})
|
||||
})
|
||||
|
||||
registerDynamicRoute('POST', '/api/admin/provider-oauth/providers/:providerId/batch-import', async (config, _params) => {
|
||||
await delay()
|
||||
requireAdmin()
|
||||
const body = JSON.parse(config.data || '{}')
|
||||
const raw = typeof body.credentials === 'string' ? body.credentials.trim() : ''
|
||||
const lines = raw ? raw.split('\n').filter(line => line.trim() && !line.trim().startsWith('#')) : []
|
||||
const total = Math.max(Math.min(lines.length, 5), 2)
|
||||
const results = []
|
||||
for (let index = 0; index < total; index++) {
|
||||
results.push({
|
||||
index,
|
||||
status: 'success',
|
||||
key_id: `key-oauth-${Date.now()}-${index}`,
|
||||
key_name: `Imported OAuth ${index + 1}`,
|
||||
auth_method: 'oauth'
|
||||
})
|
||||
}
|
||||
return createMockResponse({
|
||||
total,
|
||||
success: results.length,
|
||||
failed: 0,
|
||||
results
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
// Key 更新
|
||||
registerDynamicRoute('PUT', '/api/admin/endpoints/keys/:keyId', async (config, params) => {
|
||||
await delay()
|
||||
@@ -1544,6 +1702,26 @@ registerDynamicRoute('GET', '/api/admin/endpoints/keys/:keyId/reveal', async (_c
|
||||
return createMockResponse({ api_key: 'sk-demo-reveal' })
|
||||
})
|
||||
|
||||
registerDynamicRoute('GET', '/api/admin/endpoints/keys/:keyId/export', async (_config, params) => {
|
||||
await delay()
|
||||
requireAdmin()
|
||||
return createMockResponse({
|
||||
key_id: params.keyId,
|
||||
provider_type: 'codex',
|
||||
auth_method: 'oauth',
|
||||
refresh_token: 'rt-demo',
|
||||
email: 'oauth-demo@aether.dev',
|
||||
exported_at: new Date().toISOString()
|
||||
})
|
||||
})
|
||||
|
||||
registerDynamicRoute('POST', '/api/admin/endpoints/keys/:keyId/clear-oauth-invalid', async (_config, params) => {
|
||||
await delay()
|
||||
requireAdmin()
|
||||
return createMockResponse({ message: 'OAuth invalid cleared (demo)', key_id: params.keyId })
|
||||
})
|
||||
|
||||
|
||||
// Keys grouped by format
|
||||
mockHandlers['GET /api/admin/endpoints/keys/grouped-by-format'] = async () => {
|
||||
await delay()
|
||||
@@ -1765,6 +1943,19 @@ registerDynamicRoute('GET', '/api/admin/endpoints/health/key/:keyId', async (_co
|
||||
})
|
||||
})
|
||||
|
||||
registerDynamicRoute('PATCH', '/api/admin/endpoints/health/keys', async () => {
|
||||
await delay()
|
||||
requireAdmin()
|
||||
return createMockResponse({
|
||||
message: 'All key health recovered (demo)',
|
||||
recovered_count: 2,
|
||||
recovered_keys: [
|
||||
{ key_id: 'key-demo-1', key_name: 'Primary Key', endpoint_id: 'ep-demo-1' },
|
||||
{ key_id: 'key-demo-2', key_name: 'Backup Key', endpoint_id: 'ep-demo-2' }
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
// 重置 Key Health
|
||||
registerDynamicRoute('PATCH', '/api/admin/endpoints/health/keys/:keyId', async (_config, params) => {
|
||||
await delay()
|
||||
|
||||
Reference in New Issue
Block a user