Files
sub2api/frontend/src/api/admin/settings.ts
2026-04-21 00:41:29 +08:00

778 lines
24 KiB
TypeScript

/**
* 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<AuthSourceType, AuthSourceDefaultsValue>
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<string, PaymentVisibleMethodSource>
> = {
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<SystemSettings>
): AuthSourceDefaultsState {
const raw = settings as Record<string, unknown>
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<string, unknown>
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<SystemSettings> {
const { data } = await apiClient.get<SystemSettings>('/admin/settings')
return data
}
/**
* Update system settings
* @param settings - Partial settings to update
* @returns Updated settings
*/
export async function updateSettings(settings: UpdateSettingsRequest): Promise<SystemSettings> {
const { data } = await apiClient.put<SystemSettings>('/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<AdminApiKeyStatus> {
const { data } = await apiClient.get<AdminApiKeyStatus>('/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<OverloadCooldownSettings> {
const { data } = await apiClient.get<OverloadCooldownSettings>('/admin/settings/overload-cooldown')
return data
}
export async function updateOverloadCooldownSettings(
settings: OverloadCooldownSettings
): Promise<OverloadCooldownSettings> {
const { data } = await apiClient.put<OverloadCooldownSettings>(
'/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<StreamTimeoutSettings> {
const { data } = await apiClient.get<StreamTimeoutSettings>('/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<StreamTimeoutSettings> {
const { data } = await apiClient.put<StreamTimeoutSettings>(
'/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<RectifierSettings> {
const { data } = await apiClient.get<RectifierSettings>('/admin/settings/rectifier')
return data
}
/**
* Update rectifier settings
* @param settings - Rectifier settings to update
* @returns Updated settings
*/
export async function updateRectifierSettings(
settings: RectifierSettings
): Promise<RectifierSettings> {
const { data } = await apiClient.put<RectifierSettings>(
'/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<BetaPolicySettings> {
const { data } = await apiClient.get<BetaPolicySettings>('/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<BetaPolicySettings> {
const { data } = await apiClient.put<BetaPolicySettings>(
'/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<WebSearchEmulationConfig> {
const { data } = await apiClient.get<WebSearchEmulationConfig>(
'/admin/settings/web-search-emulation'
)
return data
}
export async function updateWebSearchEmulationConfig(
config: WebSearchEmulationConfig
): Promise<WebSearchEmulationConfig> {
const { data } = await apiClient.put<WebSearchEmulationConfig>(
'/admin/settings/web-search-emulation',
config
)
return data
}
export async function testWebSearchEmulation(
query: string
): Promise<WebSearchTestResult> {
const { data } = await apiClient.post<WebSearchTestResult>(
'/admin/settings/web-search-emulation/test',
{ query }
)
return data
}
export async function resetWebSearchUsage(
payload: { provider_type: string }
): Promise<void> {
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