mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-12 22:20:19 +08:00
Merge remote-tracking branch 'origin/main' into fix/gemini-cli-v1internal
# Conflicts: # apps/aether-gateway/src/ai_serving/transport.rs # apps/aether-gateway/src/handlers/admin/provider/oauth/dispatch/batch/parse.rs # apps/aether-gateway/src/handlers/shared/catalog.rs # crates/aether-admin/src/provider/quota.rs # crates/aether-model-fetch/src/strategy.rs # crates/aether-provider-pool/src/lib.rs # crates/aether-provider-pool/src/service.rs # crates/aether-provider-transport/src/provider_types.rs # frontend/src/features/providers/components/ProviderDetailDrawer.vue # frontend/src/utils/__tests__/providerKeyQuota.spec.ts # frontend/src/utils/providerKeyQuota.ts # frontend/src/views/admin/PoolManagement.vue
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
import apiClient from './client'
|
||||
import type { ModelTestCapabilities } from './endpoints/types'
|
||||
import axios from 'axios'
|
||||
import axios, { type AxiosRequestConfig } from 'axios'
|
||||
import { cachedRequest, buildCacheKey } from '@/utils/cache'
|
||||
import type { BillingSummary } from './auth'
|
||||
import type { ApiKeyInstallSession, InstallSessionTargetSystem, InstallTargetCli } from './me'
|
||||
|
||||
const SYSTEM_DATA_IMPORT_TIMEOUT_MS = 10 * 60 * 1000
|
||||
|
||||
export interface SystemDataImportOptions {
|
||||
onUploadProgress?: AxiosRequestConfig['onUploadProgress']
|
||||
}
|
||||
|
||||
function extractConflictPayload(error: unknown): ManualUsageCleanupConflict | null {
|
||||
if (!axios.isAxiosError(error) || error.response?.status !== 409) {
|
||||
return null
|
||||
@@ -859,11 +863,11 @@ export const adminApi = {
|
||||
},
|
||||
|
||||
// 导入配置
|
||||
async importConfig(data: ConfigImportRequest): Promise<ConfigImportResponse> {
|
||||
async importConfig(data: ConfigImportRequest, options: SystemDataImportOptions = {}): Promise<ConfigImportResponse> {
|
||||
const response = await apiClient.post<ConfigImportResponse>(
|
||||
'/api/admin/system/config/import',
|
||||
data,
|
||||
{ timeout: SYSTEM_DATA_IMPORT_TIMEOUT_MS }
|
||||
{ timeout: SYSTEM_DATA_IMPORT_TIMEOUT_MS, ...options }
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
@@ -875,27 +879,27 @@ export const adminApi = {
|
||||
},
|
||||
|
||||
// 导入用户数据
|
||||
async importUsers(data: UsersImportRequest): Promise<UsersImportResponse> {
|
||||
async importUsers(data: UsersImportRequest, options: SystemDataImportOptions = {}): Promise<UsersImportResponse> {
|
||||
const response = await apiClient.post<UsersImportResponse>(
|
||||
'/api/admin/system/users/import',
|
||||
data,
|
||||
{ timeout: SYSTEM_DATA_IMPORT_TIMEOUT_MS }
|
||||
{ timeout: SYSTEM_DATA_IMPORT_TIMEOUT_MS, ...options }
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 导出聚合数据(配置数据 + 用户数据)
|
||||
// 导出完整备份(配置数据 + 用户数据)
|
||||
async exportAggregateData(): Promise<AggregateExportData> {
|
||||
const response = await apiClient.get<AggregateExportData>('/api/admin/system/data/export')
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 导入聚合数据(配置数据 + 用户数据)
|
||||
async importAggregateData(data: AggregateImportRequest): Promise<AggregateImportResponse> {
|
||||
// 导入完整备份(配置数据 + 用户数据)
|
||||
async importAggregateData(data: AggregateImportRequest, options: SystemDataImportOptions = {}): Promise<AggregateImportResponse> {
|
||||
const response = await apiClient.post<AggregateImportResponse>(
|
||||
'/api/admin/system/data/import',
|
||||
data,
|
||||
{ timeout: SYSTEM_DATA_IMPORT_TIMEOUT_MS }
|
||||
{ timeout: SYSTEM_DATA_IMPORT_TIMEOUT_MS, ...options }
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
@@ -918,6 +922,23 @@ export const adminApi = {
|
||||
return response.data
|
||||
},
|
||||
|
||||
async testImportantNotification(options: 'all' | 'email' | 'server_chan' | 'bark' | {
|
||||
channel?: 'all' | 'email' | 'server_chan' | 'bark'
|
||||
item_key?: string
|
||||
} = 'all'): Promise<{
|
||||
success: boolean
|
||||
message: string
|
||||
channels: Array<{ channel: string; success: boolean; message: string }>
|
||||
}> {
|
||||
const payload = typeof options === 'string' ? { channel: options } : options
|
||||
const response = await apiClient.post<{
|
||||
success: boolean
|
||||
message: string
|
||||
channels: Array<{ channel: string; success: boolean; message: string }>
|
||||
}>('/api/admin/system/important-notification/test', payload)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 邮件模板相关
|
||||
// 获取所有邮件模板
|
||||
async getEmailTemplates(): Promise<EmailTemplatesResponse> {
|
||||
|
||||
@@ -79,6 +79,137 @@ export interface OAuthBatchImportTaskStatusResponse {
|
||||
updated_at: number
|
||||
}
|
||||
|
||||
export type BatchImportCredentialsNormalization =
|
||||
| { ok: true; isBatch: boolean; credentials: string }
|
||||
| { ok: false; message: string }
|
||||
|
||||
function getImportCredentialLines(text: string): Array<{ lineNumber: number; text: string }> {
|
||||
return text
|
||||
.split('\n')
|
||||
.map((line, index) => ({ lineNumber: index + 1, text: line.trim() }))
|
||||
.filter(line => line.text && !line.text.startsWith('#'))
|
||||
}
|
||||
|
||||
function jsonParseErrorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
|
||||
function normalizeBatchImportItem(
|
||||
value: unknown,
|
||||
location: string,
|
||||
): { ok: true; value: string | Record<string, unknown> } | { ok: false; message: string } {
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim()
|
||||
if (trimmed) return { ok: true, value: trimmed }
|
||||
return { ok: false, message: `${location} 不能为空字符串` }
|
||||
}
|
||||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||
return { ok: true, value: value as Record<string, unknown> }
|
||||
}
|
||||
return { ok: false, message: `${location} 必须是 JSON 对象或字符串` }
|
||||
}
|
||||
|
||||
function normalizeBatchImportArray(items: unknown[]): BatchImportCredentialsNormalization {
|
||||
if (items.length === 0) {
|
||||
return { ok: false, message: 'JSON 数组不能为空' }
|
||||
}
|
||||
|
||||
const normalized: Array<string | Record<string, unknown>> = []
|
||||
for (const [index, item] of items.entries()) {
|
||||
const result = normalizeBatchImportItem(item, `JSON 数组第 ${index + 1} 项`)
|
||||
if (!result.ok) return result
|
||||
normalized.push(result.value)
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
isBatch: true,
|
||||
credentials: JSON.stringify(normalized),
|
||||
}
|
||||
}
|
||||
|
||||
function parseImportCredentialLines(
|
||||
lines: Array<{ lineNumber: number; text: string }>,
|
||||
): BatchImportCredentialsNormalization {
|
||||
const normalized: Array<string | Record<string, unknown>> = []
|
||||
|
||||
for (const line of lines) {
|
||||
const firstChar = line.text[0]
|
||||
if (firstChar === '{' || firstChar === '[') {
|
||||
let parsed: unknown
|
||||
try {
|
||||
parsed = JSON.parse(line.text)
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
message: `JSON Lines 格式无效,请检查第 ${line.lineNumber} 行: ${jsonParseErrorMessage(error)}`,
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(parsed)) {
|
||||
for (const [index, item] of parsed.entries()) {
|
||||
const result = normalizeBatchImportItem(item, `第 ${line.lineNumber} 行数组第 ${index + 1} 项`)
|
||||
if (!result.ok) return result
|
||||
normalized.push(result.value)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const result = normalizeBatchImportItem(parsed, `第 ${line.lineNumber} 行`)
|
||||
if (!result.ok) return result
|
||||
normalized.push(result.value)
|
||||
continue
|
||||
}
|
||||
|
||||
normalized.push(line.text)
|
||||
}
|
||||
|
||||
return normalizeBatchImportArray(normalized)
|
||||
}
|
||||
|
||||
export function normalizeBatchImportCredentials(text: string): BatchImportCredentialsNormalization {
|
||||
const trimmed = text.trim()
|
||||
if (!trimmed) {
|
||||
return { ok: false, message: '请输入凭据数据' }
|
||||
}
|
||||
|
||||
const firstChar = trimmed[0]
|
||||
if (firstChar === '[') {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(trimmed)
|
||||
if (!Array.isArray(parsed)) {
|
||||
return { ok: false, message: 'JSON 批量凭据必须是数组' }
|
||||
}
|
||||
return normalizeBatchImportArray(parsed)
|
||||
} catch (error) {
|
||||
return { ok: false, message: `JSON 数组格式无效: ${jsonParseErrorMessage(error)}` }
|
||||
}
|
||||
}
|
||||
|
||||
if (firstChar === '{') {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(trimmed)
|
||||
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
||||
return { ok: true, isBatch: false, credentials: trimmed }
|
||||
}
|
||||
return { ok: false, message: '单条 JSON 凭据必须是对象' }
|
||||
} catch (error) {
|
||||
const lines = getImportCredentialLines(trimmed)
|
||||
if (lines.length > 1) {
|
||||
return parseImportCredentialLines(lines)
|
||||
}
|
||||
return { ok: false, message: `JSON 格式无效: ${jsonParseErrorMessage(error)}` }
|
||||
}
|
||||
}
|
||||
|
||||
const lines = getImportCredentialLines(trimmed)
|
||||
if (lines.length > 1) {
|
||||
return parseImportCredentialLines(lines)
|
||||
}
|
||||
|
||||
return { ok: true, isBatch: false, credentials: trimmed }
|
||||
}
|
||||
|
||||
export async function refreshProviderOAuth(keyId: string): Promise<ProviderOAuthCompleteResponse> {
|
||||
const resp = await client.post(`/api/admin/provider-oauth/keys/${keyId}/refresh`)
|
||||
return resp.data
|
||||
@@ -102,8 +233,14 @@ export async function completeProviderLevelOAuth(
|
||||
export async function importProviderRefreshToken(
|
||||
providerId: string,
|
||||
data: {
|
||||
api_key?: string
|
||||
apiKey?: string
|
||||
token?: string
|
||||
auth_token?: string
|
||||
authToken?: string
|
||||
refresh_token?: string
|
||||
access_token?: string
|
||||
password?: string
|
||||
expires_at?: number
|
||||
name?: string
|
||||
proxy_node_id?: string
|
||||
@@ -150,7 +287,8 @@ export async function getBatchImportOAuthTaskStatus(
|
||||
export interface DeviceAuthorizeRequest {
|
||||
start_url?: string
|
||||
region?: string
|
||||
auth_type?: 'builder_id' | 'identity_center' | 'google' | 'github'
|
||||
auth_type?: 'builder_id' | 'identity_center' | 'google' | 'github' | 'browser'
|
||||
login_option?: 'google' | 'github' | 'default'
|
||||
redirect_uri?: string
|
||||
proxy_node_id?: string
|
||||
}
|
||||
@@ -170,6 +308,7 @@ export interface DeviceAuthorizeResponse {
|
||||
export interface DevicePollRequest {
|
||||
session_id: string
|
||||
callback_url?: string
|
||||
token?: string
|
||||
}
|
||||
|
||||
export interface DevicePollResponse {
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
FailoverRulesConfig,
|
||||
PoolAdvancedConfig,
|
||||
ProviderConfig,
|
||||
ProviderType,
|
||||
ProviderWithEndpointsSummary,
|
||||
ProxyConfig,
|
||||
} from './types'
|
||||
@@ -92,7 +93,7 @@ export async function updateProvider(
|
||||
providerId: string,
|
||||
data: Partial<{
|
||||
name: string
|
||||
provider_type: 'custom' | 'vertex_ai' | 'claude_code' | 'codex' | 'chatgpt_web' | 'gemini_cli' | 'antigravity' | 'kiro' | 'grok'
|
||||
provider_type: ProviderType
|
||||
description: string | null
|
||||
website: string
|
||||
provider_priority: number
|
||||
@@ -127,7 +128,7 @@ export async function updateProvider(
|
||||
export async function createProvider(
|
||||
data: {
|
||||
name: string
|
||||
provider_type?: 'custom' | 'vertex_ai' | 'claude_code' | 'codex' | 'chatgpt_web' | 'gemini_cli' | 'antigravity' | 'kiro' | 'grok'
|
||||
provider_type?: ProviderType
|
||||
description?: string
|
||||
website?: string
|
||||
billing_type?: 'monthly_quota' | 'pay_as_you_go' | 'free_tier'
|
||||
|
||||
@@ -370,6 +370,32 @@ export interface KiroUpstreamMetadata {
|
||||
banned_at?: number // 封禁时间(Unix 时间戳,秒)
|
||||
}
|
||||
|
||||
// Windsurf 上游配额信息
|
||||
export interface WindsurfUpstreamMetadata {
|
||||
updated_at?: number
|
||||
plan_name?: string
|
||||
daily_remaining_percent?: number | null
|
||||
weekly_remaining_percent?: number | null
|
||||
daily_reset_at?: number | null
|
||||
weekly_reset_at?: number | null
|
||||
prompt_used?: number | null
|
||||
prompt_limit?: number | null
|
||||
prompt_remaining?: number | null
|
||||
flex_used?: number | null
|
||||
flex_limit?: number | null
|
||||
flex_remaining?: number | null
|
||||
allowed_models_count?: number | null
|
||||
models?: Array<{
|
||||
model_uid?: string | null
|
||||
label?: string | null
|
||||
provider?: string | null
|
||||
supports_images?: boolean | null
|
||||
credit_multiplier?: number | null
|
||||
}> | null
|
||||
rate_limit?: Record<string, unknown> | null
|
||||
last_error?: string | null
|
||||
}
|
||||
|
||||
export interface ChatGPTWebUpstreamMetadata {
|
||||
updated_at?: number // Unix 时间戳(秒)
|
||||
plan_type?: string | null
|
||||
@@ -456,6 +482,7 @@ export interface UpstreamMetadata {
|
||||
codex?: CodexUpstreamMetadata
|
||||
antigravity?: AntigravityUpstreamMetadata
|
||||
kiro?: KiroUpstreamMetadata
|
||||
windsurf?: WindsurfUpstreamMetadata
|
||||
chatgpt_web?: ChatGPTWebUpstreamMetadata
|
||||
grok?: GrokUpstreamMetadata
|
||||
gemini_cli?: GeminiCliUpstreamMetadata
|
||||
@@ -586,7 +613,7 @@ export interface PublicEndpointStatusMonitorResponse {
|
||||
formats: PublicEndpointStatusMonitor[]
|
||||
}
|
||||
|
||||
export type ProviderType = 'custom' | 'claude_code' | 'codex' | 'chatgpt_web' | 'gemini_cli' | 'antigravity' | 'kiro' | 'grok' | 'vertex_ai'
|
||||
export type ProviderType = 'custom' | 'claude_code' | 'codex' | 'chatgpt_web' | 'gemini_cli' | 'antigravity' | 'kiro' | 'grok' | 'windsurf' | 'vertex_ai'
|
||||
|
||||
export interface ClaudeCodeAdvancedConfig {
|
||||
// 会话数量控制:null/undefined 表示不限制
|
||||
@@ -736,6 +763,7 @@ export interface ProviderWithEndpointsSummary {
|
||||
ops_configured: boolean // 是否配置了扩展操作(余额监控等)
|
||||
ops_architecture_id?: string // 扩展操作使用的架构 ID(如 cubence, anyrouter)
|
||||
kiro_simulated_cache_enabled?: boolean
|
||||
ops_quota_alert_enabled?: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
@@ -71,6 +71,8 @@ export interface QuotaStatusSnapshot {
|
||||
plan_type?: string | null
|
||||
pool_tier?: string | null
|
||||
credits?: QuotaCreditsSnapshot | null
|
||||
allowed_models_count?: number | null
|
||||
rate_limit?: Record<string, unknown> | null
|
||||
windows?: QuotaWindowSnapshot[] | null
|
||||
}
|
||||
|
||||
|
||||
@@ -127,12 +127,19 @@ export interface ActionConfigRequest {
|
||||
}
|
||||
|
||||
/** 保存配置请求 */
|
||||
export interface QuotaAlertConfig {
|
||||
enabled: boolean
|
||||
threshold_amount: number
|
||||
fetch_interval_seconds: number
|
||||
}
|
||||
|
||||
export interface SaveConfigRequest {
|
||||
architecture_id: string
|
||||
base_url?: string
|
||||
connector: ConnectorConfigRequest
|
||||
actions: Record<string, ActionConfigRequest>
|
||||
schedule: Record<string, string>
|
||||
quota_alert?: QuotaAlertConfig
|
||||
}
|
||||
|
||||
/** 连接请求 */
|
||||
@@ -190,6 +197,7 @@ export interface ProviderOpsConfigResponse {
|
||||
config: Record<string, unknown>
|
||||
credentials: Record<string, unknown>
|
||||
}
|
||||
quota_alert?: QuotaAlertConfig
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user