新增系统设置 allow_ungrouped_key_scheduling(默认关闭), 未分组的 API Key 在网关请求时直接返回 403, 由 RequireGroupAssignment 中间件统一拦截, 支持 Anthropic / Google 两种错误格式响应。 全栈实现:常量 → 结构体 → 解析/更新/初始化 → DTO → 管理接口 → 中间件 → 路由注册 → 前端设置界面 + i18n。
431 lines
11 KiB
TypeScript
431 lines
11 KiB
TypeScript
/**
|
|
* Admin Settings API endpoints
|
|
* Handles system settings management for administrators
|
|
*/
|
|
|
|
import { apiClient } from '../client'
|
|
import type { CustomMenuItem } from '@/types'
|
|
|
|
export interface DefaultSubscriptionSetting {
|
|
group_id: number
|
|
validity_days: number
|
|
}
|
|
|
|
/**
|
|
* System settings interface
|
|
*/
|
|
export interface SystemSettings {
|
|
// Registration settings
|
|
registration_enabled: boolean
|
|
email_verify_enabled: boolean
|
|
promo_code_enabled: boolean
|
|
password_reset_enabled: boolean
|
|
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[]
|
|
// 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
|
|
purchase_subscription_enabled: boolean
|
|
purchase_subscription_url: string
|
|
sora_client_enabled: boolean
|
|
custom_menu_items: CustomMenuItem[]
|
|
// 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
|
|
|
|
// 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
|
|
|
|
// 分组隔离
|
|
allow_ungrouped_key_scheduling: boolean
|
|
}
|
|
|
|
export interface UpdateSettingsRequest {
|
|
registration_enabled?: boolean
|
|
email_verify_enabled?: boolean
|
|
promo_code_enabled?: boolean
|
|
password_reset_enabled?: boolean
|
|
invitation_code_enabled?: boolean
|
|
totp_enabled?: boolean // TOTP 双因素认证
|
|
default_balance?: number
|
|
default_concurrency?: number
|
|
default_subscriptions?: DefaultSubscriptionSetting[]
|
|
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
|
|
purchase_subscription_enabled?: boolean
|
|
purchase_subscription_url?: string
|
|
sora_client_enabled?: boolean
|
|
custom_menu_items?: CustomMenuItem[]
|
|
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
|
|
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
|
|
allow_ungrouped_key_scheduling?: boolean
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
}
|
|
|
|
// ==================== Sora S3 Settings ====================
|
|
|
|
export interface SoraS3Settings {
|
|
enabled: boolean
|
|
endpoint: string
|
|
region: string
|
|
bucket: string
|
|
access_key_id: string
|
|
secret_access_key_configured: boolean
|
|
prefix: string
|
|
force_path_style: boolean
|
|
cdn_url: string
|
|
default_storage_quota_bytes: number
|
|
}
|
|
|
|
export interface SoraS3Profile {
|
|
profile_id: string
|
|
name: string
|
|
is_active: boolean
|
|
enabled: boolean
|
|
endpoint: string
|
|
region: string
|
|
bucket: string
|
|
access_key_id: string
|
|
secret_access_key_configured: boolean
|
|
prefix: string
|
|
force_path_style: boolean
|
|
cdn_url: string
|
|
default_storage_quota_bytes: number
|
|
updated_at: string
|
|
}
|
|
|
|
export interface ListSoraS3ProfilesResponse {
|
|
active_profile_id: string
|
|
items: SoraS3Profile[]
|
|
}
|
|
|
|
export interface UpdateSoraS3SettingsRequest {
|
|
profile_id?: string
|
|
enabled: boolean
|
|
endpoint: string
|
|
region: string
|
|
bucket: string
|
|
access_key_id: string
|
|
secret_access_key?: string
|
|
prefix: string
|
|
force_path_style: boolean
|
|
cdn_url: string
|
|
default_storage_quota_bytes: number
|
|
}
|
|
|
|
export interface CreateSoraS3ProfileRequest {
|
|
profile_id: string
|
|
name: string
|
|
set_active?: boolean
|
|
enabled: boolean
|
|
endpoint: string
|
|
region: string
|
|
bucket: string
|
|
access_key_id: string
|
|
secret_access_key?: string
|
|
prefix: string
|
|
force_path_style: boolean
|
|
cdn_url: string
|
|
default_storage_quota_bytes: number
|
|
}
|
|
|
|
export interface UpdateSoraS3ProfileRequest {
|
|
name: string
|
|
enabled: boolean
|
|
endpoint: string
|
|
region: string
|
|
bucket: string
|
|
access_key_id: string
|
|
secret_access_key?: string
|
|
prefix: string
|
|
force_path_style: boolean
|
|
cdn_url: string
|
|
default_storage_quota_bytes: number
|
|
}
|
|
|
|
export interface TestSoraS3ConnectionRequest {
|
|
profile_id?: string
|
|
enabled: boolean
|
|
endpoint: string
|
|
region: string
|
|
bucket: string
|
|
access_key_id: string
|
|
secret_access_key?: string
|
|
prefix: string
|
|
force_path_style: boolean
|
|
cdn_url: string
|
|
default_storage_quota_bytes?: number
|
|
}
|
|
|
|
export async function getSoraS3Settings(): Promise<SoraS3Settings> {
|
|
const { data } = await apiClient.get<SoraS3Settings>('/admin/settings/sora-s3')
|
|
return data
|
|
}
|
|
|
|
export async function updateSoraS3Settings(settings: UpdateSoraS3SettingsRequest): Promise<SoraS3Settings> {
|
|
const { data } = await apiClient.put<SoraS3Settings>('/admin/settings/sora-s3', settings)
|
|
return data
|
|
}
|
|
|
|
export async function testSoraS3Connection(
|
|
settings: TestSoraS3ConnectionRequest
|
|
): Promise<{ message: string }> {
|
|
const { data } = await apiClient.post<{ message: string }>('/admin/settings/sora-s3/test', settings)
|
|
return data
|
|
}
|
|
|
|
export async function listSoraS3Profiles(): Promise<ListSoraS3ProfilesResponse> {
|
|
const { data } = await apiClient.get<ListSoraS3ProfilesResponse>('/admin/settings/sora-s3/profiles')
|
|
return data
|
|
}
|
|
|
|
export async function createSoraS3Profile(request: CreateSoraS3ProfileRequest): Promise<SoraS3Profile> {
|
|
const { data } = await apiClient.post<SoraS3Profile>('/admin/settings/sora-s3/profiles', request)
|
|
return data
|
|
}
|
|
|
|
export async function updateSoraS3Profile(profileID: string, request: UpdateSoraS3ProfileRequest): Promise<SoraS3Profile> {
|
|
const { data } = await apiClient.put<SoraS3Profile>(`/admin/settings/sora-s3/profiles/${profileID}`, request)
|
|
return data
|
|
}
|
|
|
|
export async function deleteSoraS3Profile(profileID: string): Promise<void> {
|
|
await apiClient.delete(`/admin/settings/sora-s3/profiles/${profileID}`)
|
|
}
|
|
|
|
export async function setActiveSoraS3Profile(profileID: string): Promise<SoraS3Profile> {
|
|
const { data } = await apiClient.post<SoraS3Profile>(`/admin/settings/sora-s3/profiles/${profileID}/activate`)
|
|
return data
|
|
}
|
|
|
|
export const settingsAPI = {
|
|
getSettings,
|
|
updateSettings,
|
|
testSmtpConnection,
|
|
sendTestEmail,
|
|
getAdminApiKey,
|
|
regenerateAdminApiKey,
|
|
deleteAdminApiKey,
|
|
getStreamTimeoutSettings,
|
|
updateStreamTimeoutSettings,
|
|
getSoraS3Settings,
|
|
updateSoraS3Settings,
|
|
testSoraS3Connection,
|
|
listSoraS3Profiles,
|
|
createSoraS3Profile,
|
|
updateSoraS3Profile,
|
|
deleteSoraS3Profile,
|
|
setActiveSoraS3Profile
|
|
}
|
|
|
|
export default settingsAPI
|