/** * Admin Settings API endpoints * Handles system settings management for administrators */ import { apiClient } from '../client' import type { CustomMenuItem, CustomEndpoint, NotifyEmailEntry } from '@/types' export interface DefaultSubscriptionSetting { group_id: number validity_days: number } export type AuthSourceType = 'email' | 'linuxdo' | 'oidc' | 'wechat' export interface AuthSourceDefaultsValue { balance: number concurrency: number subscriptions: DefaultSubscriptionSetting[] grant_on_signup: boolean grant_on_first_bind: boolean } export type AuthSourceDefaultsState = Record export type PaymentVisibleMethod = 'alipay' | 'wxpay' export type PaymentVisibleMethodSource = | '' | 'official_alipay' | 'easypay_alipay' | 'official_wxpay' | 'easypay_wxpay' export interface PaymentVisibleMethodSourceOption { value: PaymentVisibleMethodSource labelZh: string labelEn: string } const AUTH_SOURCE_TYPES: AuthSourceType[] = ['email', 'linuxdo', 'oidc', 'wechat'] const AUTH_SOURCE_DEFAULT_BALANCE = 0 const AUTH_SOURCE_DEFAULT_CONCURRENCY = 5 const PAYMENT_VISIBLE_METHOD_SOURCE_OPTIONS: Record< PaymentVisibleMethod, PaymentVisibleMethodSourceOption[] > = { alipay: [ { value: '', labelZh: '未配置', labelEn: 'Not configured' }, { value: 'official_alipay', labelZh: '支付宝官方', labelEn: 'Official Alipay' }, { value: 'easypay_alipay', labelZh: '易支付支付宝', labelEn: 'EasyPay Alipay' }, ], wxpay: [ { value: '', labelZh: '未配置', labelEn: 'Not configured' }, { value: 'official_wxpay', labelZh: '微信官方', labelEn: 'Official WeChat Pay' }, { value: 'easypay_wxpay', labelZh: '易支付微信', labelEn: 'EasyPay WeChat Pay' }, ], } const PAYMENT_VISIBLE_METHOD_SOURCE_ALIASES: Record< PaymentVisibleMethod, Record > = { alipay: { official_alipay: 'official_alipay', alipay: 'official_alipay', alipay_direct: 'official_alipay', official: 'official_alipay', easypay_alipay: 'easypay_alipay', easypay: 'easypay_alipay', }, wxpay: { official_wxpay: 'official_wxpay', wxpay: 'official_wxpay', wxpay_direct: 'official_wxpay', wechat: 'official_wxpay', official: 'official_wxpay', easypay_wxpay: 'easypay_wxpay', easypay: 'easypay_wxpay', }, } export function normalizeDefaultSubscriptionSettings( subscriptions: DefaultSubscriptionSetting[] | null | undefined ): DefaultSubscriptionSetting[] { if (!Array.isArray(subscriptions)) return [] return subscriptions .filter((item) => item.group_id > 0 && item.validity_days > 0) .map((item) => ({ group_id: Math.floor(item.group_id), validity_days: Math.min(36500, Math.max(1, Math.floor(item.validity_days))) })) } export function buildAuthSourceDefaultsState( settings: Partial ): AuthSourceDefaultsState { const raw = settings as Record return AUTH_SOURCE_TYPES.reduce((acc, source) => { const subscriptions = raw[`auth_source_default_${source}_subscriptions`] acc[source] = { balance: Number(raw[`auth_source_default_${source}_balance`] ?? AUTH_SOURCE_DEFAULT_BALANCE), concurrency: Math.max( 1, Number(raw[`auth_source_default_${source}_concurrency`] ?? AUTH_SOURCE_DEFAULT_CONCURRENCY) ), subscriptions: normalizeDefaultSubscriptionSettings( Array.isArray(subscriptions) ? (subscriptions as DefaultSubscriptionSetting[]) : [] ), grant_on_signup: raw[`auth_source_default_${source}_grant_on_signup`] !== false, grant_on_first_bind: raw[`auth_source_default_${source}_grant_on_first_bind`] === true, } return acc }, {} as AuthSourceDefaultsState) } export function appendAuthSourceDefaultsToUpdateRequest( payload: UpdateSettingsRequest, authSourceDefaults: AuthSourceDefaultsState ): UpdateSettingsRequest { const target = payload as Record for (const source of AUTH_SOURCE_TYPES) { const current = authSourceDefaults[source] target[`auth_source_default_${source}_balance`] = Number(current.balance) || 0 target[`auth_source_default_${source}_concurrency`] = Math.max( 1, Math.floor(Number(current.concurrency) || AUTH_SOURCE_DEFAULT_CONCURRENCY) ) target[`auth_source_default_${source}_subscriptions`] = normalizeDefaultSubscriptionSettings( current.subscriptions ) target[`auth_source_default_${source}_grant_on_signup`] = current.grant_on_signup target[`auth_source_default_${source}_grant_on_first_bind`] = current.grant_on_first_bind } return payload } export function getPaymentVisibleMethodSourceOptions( method: PaymentVisibleMethod ): PaymentVisibleMethodSourceOption[] { return PAYMENT_VISIBLE_METHOD_SOURCE_OPTIONS[method] } export function normalizePaymentVisibleMethodSource( method: PaymentVisibleMethod, source: unknown ): PaymentVisibleMethodSource { if (typeof source !== 'string') return '' const normalized = source.trim().toLowerCase() if (!normalized) return '' return PAYMENT_VISIBLE_METHOD_SOURCE_ALIASES[method][normalized] ?? '' } /** * System settings interface */ export interface SystemSettings { // Registration settings registration_enabled: boolean email_verify_enabled: boolean registration_email_suffix_whitelist: string[] promo_code_enabled: boolean password_reset_enabled: boolean frontend_url: string invitation_code_enabled: boolean totp_enabled: boolean // TOTP 双因素认证 totp_encryption_key_configured: boolean // TOTP 加密密钥是否已配置 // Default settings default_balance: number default_concurrency: number default_subscriptions: DefaultSubscriptionSetting[] auth_source_default_email_balance?: number auth_source_default_email_concurrency?: number auth_source_default_email_subscriptions?: DefaultSubscriptionSetting[] auth_source_default_email_grant_on_signup?: boolean auth_source_default_email_grant_on_first_bind?: boolean auth_source_default_linuxdo_balance?: number auth_source_default_linuxdo_concurrency?: number auth_source_default_linuxdo_subscriptions?: DefaultSubscriptionSetting[] auth_source_default_linuxdo_grant_on_signup?: boolean auth_source_default_linuxdo_grant_on_first_bind?: boolean auth_source_default_oidc_balance?: number auth_source_default_oidc_concurrency?: number auth_source_default_oidc_subscriptions?: DefaultSubscriptionSetting[] auth_source_default_oidc_grant_on_signup?: boolean auth_source_default_oidc_grant_on_first_bind?: boolean auth_source_default_wechat_balance?: number auth_source_default_wechat_concurrency?: number auth_source_default_wechat_subscriptions?: DefaultSubscriptionSetting[] auth_source_default_wechat_grant_on_signup?: boolean auth_source_default_wechat_grant_on_first_bind?: boolean force_email_on_third_party_signup?: boolean // OEM settings site_name: string site_logo: string site_subtitle: string api_base_url: string contact_info: string doc_url: string home_content: string hide_ccs_import_button: boolean table_default_page_size: number table_page_size_options: number[] backend_mode_enabled: boolean custom_menu_items: CustomMenuItem[] custom_endpoints: CustomEndpoint[] // SMTP settings smtp_host: string smtp_port: number smtp_username: string smtp_password_configured: boolean smtp_from_email: string smtp_from_name: string smtp_use_tls: boolean // Cloudflare Turnstile settings turnstile_enabled: boolean turnstile_site_key: string turnstile_secret_key_configured: boolean // LinuxDo Connect OAuth settings linuxdo_connect_enabled: boolean linuxdo_connect_client_id: string linuxdo_connect_client_secret_configured: boolean linuxdo_connect_redirect_url: string // Generic OIDC OAuth settings oidc_connect_enabled: boolean oidc_connect_provider_name: string oidc_connect_client_id: string oidc_connect_client_secret_configured: boolean oidc_connect_issuer_url: string oidc_connect_discovery_url: string oidc_connect_authorize_url: string oidc_connect_token_url: string oidc_connect_userinfo_url: string oidc_connect_jwks_url: string oidc_connect_scopes: string oidc_connect_redirect_url: string oidc_connect_frontend_redirect_url: string oidc_connect_token_auth_method: string oidc_connect_use_pkce: boolean oidc_connect_validate_id_token: boolean oidc_connect_allowed_signing_algs: string oidc_connect_clock_skew_seconds: number oidc_connect_require_email_verified: boolean oidc_connect_userinfo_email_path: string oidc_connect_userinfo_id_path: string oidc_connect_userinfo_username_path: string // Model fallback configuration enable_model_fallback: boolean fallback_model_anthropic: string fallback_model_openai: string fallback_model_gemini: string fallback_model_antigravity: string // Identity patch configuration (Claude -> Gemini) enable_identity_patch: boolean identity_patch_prompt: string // Ops Monitoring (vNext) ops_monitoring_enabled: boolean ops_realtime_monitoring_enabled: boolean ops_query_mode_default: 'auto' | 'raw' | 'preagg' | string ops_metrics_interval_seconds: number // Claude Code version check min_claude_code_version: string max_claude_code_version: string // 分组隔离 allow_ungrouped_key_scheduling: boolean // Gateway forwarding behavior enable_fingerprint_unification: boolean enable_metadata_passthrough: boolean enable_cch_signing: boolean web_search_emulation_enabled?: boolean // Payment configuration payment_enabled: boolean payment_min_amount: number payment_max_amount: number payment_daily_limit: number payment_order_timeout_minutes: number payment_max_pending_orders: number payment_enabled_types: string[] payment_balance_disabled: boolean payment_balance_recharge_multiplier: number payment_recharge_fee_rate: number payment_load_balance_strategy: string payment_product_name_prefix: string payment_product_name_suffix: string payment_help_image_url: string payment_help_text: string payment_cancel_rate_limit_enabled: boolean payment_cancel_rate_limit_max: number payment_cancel_rate_limit_window: number payment_cancel_rate_limit_unit: string payment_cancel_rate_limit_window_mode: string payment_visible_method_alipay_source?: string payment_visible_method_wxpay_source?: string payment_visible_method_alipay_enabled?: boolean payment_visible_method_wxpay_enabled?: boolean openai_advanced_scheduler_enabled?: boolean // Balance & quota notification balance_low_notify_enabled: boolean balance_low_notify_threshold: number balance_low_notify_recharge_url: string account_quota_notify_enabled: boolean account_quota_notify_emails: NotifyEmailEntry[] } export interface UpdateSettingsRequest { registration_enabled?: boolean email_verify_enabled?: boolean registration_email_suffix_whitelist?: string[] promo_code_enabled?: boolean password_reset_enabled?: boolean frontend_url?: string invitation_code_enabled?: boolean totp_enabled?: boolean // TOTP 双因素认证 default_balance?: number default_concurrency?: number default_subscriptions?: DefaultSubscriptionSetting[] auth_source_default_email_balance?: number auth_source_default_email_concurrency?: number auth_source_default_email_subscriptions?: DefaultSubscriptionSetting[] auth_source_default_email_grant_on_signup?: boolean auth_source_default_email_grant_on_first_bind?: boolean auth_source_default_linuxdo_balance?: number auth_source_default_linuxdo_concurrency?: number auth_source_default_linuxdo_subscriptions?: DefaultSubscriptionSetting[] auth_source_default_linuxdo_grant_on_signup?: boolean auth_source_default_linuxdo_grant_on_first_bind?: boolean auth_source_default_oidc_balance?: number auth_source_default_oidc_concurrency?: number auth_source_default_oidc_subscriptions?: DefaultSubscriptionSetting[] auth_source_default_oidc_grant_on_signup?: boolean auth_source_default_oidc_grant_on_first_bind?: boolean auth_source_default_wechat_balance?: number auth_source_default_wechat_concurrency?: number auth_source_default_wechat_subscriptions?: DefaultSubscriptionSetting[] auth_source_default_wechat_grant_on_signup?: boolean auth_source_default_wechat_grant_on_first_bind?: boolean force_email_on_third_party_signup?: boolean site_name?: string site_logo?: string site_subtitle?: string api_base_url?: string contact_info?: string doc_url?: string home_content?: string hide_ccs_import_button?: boolean table_default_page_size?: number table_page_size_options?: number[] backend_mode_enabled?: boolean custom_menu_items?: CustomMenuItem[] custom_endpoints?: CustomEndpoint[] smtp_host?: string smtp_port?: number smtp_username?: string smtp_password?: string smtp_from_email?: string smtp_from_name?: string smtp_use_tls?: boolean turnstile_enabled?: boolean turnstile_site_key?: string turnstile_secret_key?: string linuxdo_connect_enabled?: boolean linuxdo_connect_client_id?: string linuxdo_connect_client_secret?: string linuxdo_connect_redirect_url?: string oidc_connect_enabled?: boolean oidc_connect_provider_name?: string oidc_connect_client_id?: string oidc_connect_client_secret?: string oidc_connect_issuer_url?: string oidc_connect_discovery_url?: string oidc_connect_authorize_url?: string oidc_connect_token_url?: string oidc_connect_userinfo_url?: string oidc_connect_jwks_url?: string oidc_connect_scopes?: string oidc_connect_redirect_url?: string oidc_connect_frontend_redirect_url?: string oidc_connect_token_auth_method?: string oidc_connect_use_pkce?: boolean oidc_connect_validate_id_token?: boolean oidc_connect_allowed_signing_algs?: string oidc_connect_clock_skew_seconds?: number oidc_connect_require_email_verified?: boolean oidc_connect_userinfo_email_path?: string oidc_connect_userinfo_id_path?: string oidc_connect_userinfo_username_path?: string enable_model_fallback?: boolean fallback_model_anthropic?: string fallback_model_openai?: string fallback_model_gemini?: string fallback_model_antigravity?: string enable_identity_patch?: boolean identity_patch_prompt?: string ops_monitoring_enabled?: boolean ops_realtime_monitoring_enabled?: boolean ops_query_mode_default?: 'auto' | 'raw' | 'preagg' | string ops_metrics_interval_seconds?: number min_claude_code_version?: string max_claude_code_version?: string allow_ungrouped_key_scheduling?: boolean enable_fingerprint_unification?: boolean enable_metadata_passthrough?: boolean enable_cch_signing?: boolean // Payment configuration payment_enabled?: boolean payment_min_amount?: number payment_max_amount?: number payment_daily_limit?: number payment_order_timeout_minutes?: number payment_max_pending_orders?: number payment_enabled_types?: string[] payment_balance_disabled?: boolean payment_balance_recharge_multiplier?: number payment_recharge_fee_rate?: number payment_load_balance_strategy?: string payment_product_name_prefix?: string payment_product_name_suffix?: string payment_help_image_url?: string payment_help_text?: string payment_cancel_rate_limit_enabled?: boolean payment_cancel_rate_limit_max?: number payment_cancel_rate_limit_window?: number payment_cancel_rate_limit_unit?: string payment_cancel_rate_limit_window_mode?: string payment_visible_method_alipay_source?: string payment_visible_method_wxpay_source?: string payment_visible_method_alipay_enabled?: boolean payment_visible_method_wxpay_enabled?: boolean openai_advanced_scheduler_enabled?: boolean // Balance & quota notification balance_low_notify_enabled?: boolean balance_low_notify_threshold?: number balance_low_notify_recharge_url?: string account_quota_notify_enabled?: boolean account_quota_notify_emails?: NotifyEmailEntry[] } /** * Get all system settings * @returns System settings */ export async function getSettings(): Promise { const { data } = await apiClient.get('/admin/settings') return data } /** * Update system settings * @param settings - Partial settings to update * @returns Updated settings */ export async function updateSettings(settings: UpdateSettingsRequest): Promise { const { data } = await apiClient.put('/admin/settings', settings) return data } /** * Test SMTP connection request */ export interface TestSmtpRequest { smtp_host: string smtp_port: number smtp_username: string smtp_password: string smtp_use_tls: boolean } /** * Test SMTP connection with provided config * @param config - SMTP configuration to test * @returns Test result message */ export async function testSmtpConnection(config: TestSmtpRequest): Promise<{ message: string }> { const { data } = await apiClient.post<{ message: string }>('/admin/settings/test-smtp', config) return data } /** * Send test email request */ export interface SendTestEmailRequest { email: string smtp_host: string smtp_port: number smtp_username: string smtp_password: string smtp_from_email: string smtp_from_name: string smtp_use_tls: boolean } /** * Send test email with provided SMTP config * @param request - Email address and SMTP config * @returns Test result message */ export async function sendTestEmail(request: SendTestEmailRequest): Promise<{ message: string }> { const { data } = await apiClient.post<{ message: string }>( '/admin/settings/send-test-email', request ) return data } /** * Admin API Key status response */ export interface AdminApiKeyStatus { exists: boolean masked_key: string } /** * Get admin API key status * @returns Status indicating if key exists and masked version */ export async function getAdminApiKey(): Promise { const { data } = await apiClient.get('/admin/settings/admin-api-key') return data } /** * Regenerate admin API key * @returns The new full API key (only shown once) */ export async function regenerateAdminApiKey(): Promise<{ key: string }> { const { data } = await apiClient.post<{ key: string }>('/admin/settings/admin-api-key/regenerate') return data } /** * Delete admin API key * @returns Success message */ export async function deleteAdminApiKey(): Promise<{ message: string }> { const { data } = await apiClient.delete<{ message: string }>('/admin/settings/admin-api-key') return data } // ==================== Overload Cooldown Settings ==================== /** * Overload cooldown settings interface (529 handling) */ export interface OverloadCooldownSettings { enabled: boolean cooldown_minutes: number } export async function getOverloadCooldownSettings(): Promise { const { data } = await apiClient.get('/admin/settings/overload-cooldown') return data } export async function updateOverloadCooldownSettings( settings: OverloadCooldownSettings ): Promise { const { data } = await apiClient.put( '/admin/settings/overload-cooldown', settings ) return data } // ==================== Stream Timeout Settings ==================== /** * Stream timeout settings interface */ export interface StreamTimeoutSettings { enabled: boolean action: 'temp_unsched' | 'error' | 'none' temp_unsched_minutes: number threshold_count: number threshold_window_minutes: number } /** * Get stream timeout settings * @returns Stream timeout settings */ export async function getStreamTimeoutSettings(): Promise { const { data } = await apiClient.get('/admin/settings/stream-timeout') return data } /** * Update stream timeout settings * @param settings - Stream timeout settings to update * @returns Updated settings */ export async function updateStreamTimeoutSettings( settings: StreamTimeoutSettings ): Promise { const { data } = await apiClient.put( '/admin/settings/stream-timeout', settings ) return data } // ==================== Rectifier Settings ==================== /** * Rectifier settings interface */ export interface RectifierSettings { enabled: boolean thinking_signature_enabled: boolean thinking_budget_enabled: boolean apikey_signature_enabled: boolean apikey_signature_patterns: string[] } /** * Get rectifier settings * @returns Rectifier settings */ export async function getRectifierSettings(): Promise { const { data } = await apiClient.get('/admin/settings/rectifier') return data } /** * Update rectifier settings * @param settings - Rectifier settings to update * @returns Updated settings */ export async function updateRectifierSettings( settings: RectifierSettings ): Promise { const { data } = await apiClient.put( '/admin/settings/rectifier', settings ) return data } // ==================== Beta Policy Settings ==================== /** * Beta policy rule interface */ export interface BetaPolicyRule { beta_token: string action: 'pass' | 'filter' | 'block' scope: 'all' | 'oauth' | 'apikey' | 'bedrock' error_message?: string model_whitelist?: string[] fallback_action?: 'pass' | 'filter' | 'block' fallback_error_message?: string } /** * Beta policy settings interface */ export interface BetaPolicySettings { rules: BetaPolicyRule[] } /** * Get beta policy settings * @returns Beta policy settings */ export async function getBetaPolicySettings(): Promise { const { data } = await apiClient.get('/admin/settings/beta-policy') return data } /** * Update beta policy settings * @param settings - Beta policy settings to update * @returns Updated settings */ export async function updateBetaPolicySettings( settings: BetaPolicySettings ): Promise { const { data } = await apiClient.put( '/admin/settings/beta-policy', settings ) return data } // --- Web Search Emulation Config --- export interface WebSearchProviderConfig { type: 'brave' | 'tavily' api_key: string api_key_configured: boolean quota_limit: number | null subscribed_at: number | null quota_used?: number proxy_id: number | null expires_at: number | null } export interface WebSearchEmulationConfig { enabled: boolean providers: WebSearchProviderConfig[] } export interface WebSearchTestResult { provider: string results: { url: string; title: string; snippet: string; page_age?: string }[] query: string } export async function getWebSearchEmulationConfig(): Promise { const { data } = await apiClient.get( '/admin/settings/web-search-emulation' ) return data } export async function updateWebSearchEmulationConfig( config: WebSearchEmulationConfig ): Promise { const { data } = await apiClient.put( '/admin/settings/web-search-emulation', config ) return data } export async function testWebSearchEmulation( query: string ): Promise { const { data } = await apiClient.post( '/admin/settings/web-search-emulation/test', { query } ) return data } export async function resetWebSearchUsage( payload: { provider_type: string } ): Promise { await apiClient.post('/admin/settings/web-search-emulation/reset-usage', payload) } export const settingsAPI = { getSettings, updateSettings, testSmtpConnection, sendTestEmail, getAdminApiKey, regenerateAdminApiKey, deleteAdminApiKey, getOverloadCooldownSettings, updateOverloadCooldownSettings, getStreamTimeoutSettings, updateStreamTimeoutSettings, getRectifierSettings, updateRectifierSettings, getBetaPolicySettings, updateBetaPolicySettings, getWebSearchEmulationConfig, updateWebSearchEmulationConfig, testWebSearchEmulation, resetWebSearchUsage } export default settingsAPI