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:
Mas0nShi
2026-05-22 17:13:57 +08:00
220 changed files with 30324 additions and 2776 deletions
+30 -9
View File
@@ -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> {
+140 -1
View File
@@ -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 {
+3 -2
View File
@@ -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'
+29 -1
View File
@@ -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
}
+8
View File
@@ -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
}
/**