9e0d12d3b0
The buttons were hidden because v-if only checked provider.api_key, which is always empty for saved providers (backend sanitizes it). Now also checks api_key_configured. Copy button is disabled when no actual key is available (only configured placeholder shown).
1989 lines
80 KiB
Go
1989 lines
80 KiB
Go
package admin
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
|
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
|
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// semverPattern 预编译 semver 格式校验正则
|
|
var semverPattern = regexp.MustCompile(`^\d+\.\d+\.\d+$`)
|
|
|
|
// menuItemIDPattern validates custom menu item IDs: alphanumeric, hyphens, underscores only.
|
|
var menuItemIDPattern = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
|
|
|
|
// generateMenuItemID generates a short random hex ID for a custom menu item.
|
|
func generateMenuItemID() (string, error) {
|
|
b := make([]byte, 8)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", fmt.Errorf("generate menu item ID: %w", err)
|
|
}
|
|
return hex.EncodeToString(b), nil
|
|
}
|
|
|
|
func scopesContainOpenID(scopes string) bool {
|
|
for _, scope := range strings.Fields(strings.ToLower(strings.TrimSpace(scopes))) {
|
|
if scope == "openid" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// SettingHandler 系统设置处理器
|
|
type SettingHandler struct {
|
|
settingService *service.SettingService
|
|
emailService *service.EmailService
|
|
turnstileService *service.TurnstileService
|
|
opsService *service.OpsService
|
|
paymentConfigService *service.PaymentConfigService
|
|
paymentService *service.PaymentService
|
|
}
|
|
|
|
// NewSettingHandler 创建系统设置处理器
|
|
func NewSettingHandler(settingService *service.SettingService, emailService *service.EmailService, turnstileService *service.TurnstileService, opsService *service.OpsService, paymentConfigService *service.PaymentConfigService, paymentService *service.PaymentService) *SettingHandler {
|
|
return &SettingHandler{
|
|
settingService: settingService,
|
|
emailService: emailService,
|
|
turnstileService: turnstileService,
|
|
opsService: opsService,
|
|
paymentConfigService: paymentConfigService,
|
|
paymentService: paymentService,
|
|
}
|
|
}
|
|
|
|
// GetSettings 获取所有系统设置
|
|
// GET /api/v1/admin/settings
|
|
func (h *SettingHandler) GetSettings(c *gin.Context) {
|
|
settings, err := h.settingService.GetAllSettings(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
// Check if ops monitoring is enabled (respects config.ops.enabled)
|
|
opsEnabled := h.opsService != nil && h.opsService.IsMonitoringEnabled(c.Request.Context())
|
|
defaultSubscriptions := make([]dto.DefaultSubscriptionSetting, 0, len(settings.DefaultSubscriptions))
|
|
for _, sub := range settings.DefaultSubscriptions {
|
|
defaultSubscriptions = append(defaultSubscriptions, dto.DefaultSubscriptionSetting{
|
|
GroupID: sub.GroupID,
|
|
ValidityDays: sub.ValidityDays,
|
|
})
|
|
}
|
|
|
|
// Load payment config
|
|
var paymentCfg *service.PaymentConfig
|
|
if h.paymentConfigService != nil {
|
|
paymentCfg, _ = h.paymentConfigService.GetPaymentConfig(c.Request.Context())
|
|
}
|
|
if paymentCfg == nil {
|
|
paymentCfg = &service.PaymentConfig{}
|
|
}
|
|
|
|
response.Success(c, dto.SystemSettings{
|
|
RegistrationEnabled: settings.RegistrationEnabled,
|
|
EmailVerifyEnabled: settings.EmailVerifyEnabled,
|
|
RegistrationEmailSuffixWhitelist: settings.RegistrationEmailSuffixWhitelist,
|
|
PromoCodeEnabled: settings.PromoCodeEnabled,
|
|
PasswordResetEnabled: settings.PasswordResetEnabled,
|
|
FrontendURL: settings.FrontendURL,
|
|
InvitationCodeEnabled: settings.InvitationCodeEnabled,
|
|
TotpEnabled: settings.TotpEnabled,
|
|
TotpEncryptionKeyConfigured: h.settingService.IsTotpEncryptionKeyConfigured(),
|
|
SMTPHost: settings.SMTPHost,
|
|
SMTPPort: settings.SMTPPort,
|
|
SMTPUsername: settings.SMTPUsername,
|
|
SMTPPasswordConfigured: settings.SMTPPasswordConfigured,
|
|
SMTPFrom: settings.SMTPFrom,
|
|
SMTPFromName: settings.SMTPFromName,
|
|
SMTPUseTLS: settings.SMTPUseTLS,
|
|
TurnstileEnabled: settings.TurnstileEnabled,
|
|
TurnstileSiteKey: settings.TurnstileSiteKey,
|
|
TurnstileSecretKeyConfigured: settings.TurnstileSecretKeyConfigured,
|
|
LinuxDoConnectEnabled: settings.LinuxDoConnectEnabled,
|
|
LinuxDoConnectClientID: settings.LinuxDoConnectClientID,
|
|
LinuxDoConnectClientSecretConfigured: settings.LinuxDoConnectClientSecretConfigured,
|
|
LinuxDoConnectRedirectURL: settings.LinuxDoConnectRedirectURL,
|
|
OIDCConnectEnabled: settings.OIDCConnectEnabled,
|
|
OIDCConnectProviderName: settings.OIDCConnectProviderName,
|
|
OIDCConnectClientID: settings.OIDCConnectClientID,
|
|
OIDCConnectClientSecretConfigured: settings.OIDCConnectClientSecretConfigured,
|
|
OIDCConnectIssuerURL: settings.OIDCConnectIssuerURL,
|
|
OIDCConnectDiscoveryURL: settings.OIDCConnectDiscoveryURL,
|
|
OIDCConnectAuthorizeURL: settings.OIDCConnectAuthorizeURL,
|
|
OIDCConnectTokenURL: settings.OIDCConnectTokenURL,
|
|
OIDCConnectUserInfoURL: settings.OIDCConnectUserInfoURL,
|
|
OIDCConnectJWKSURL: settings.OIDCConnectJWKSURL,
|
|
OIDCConnectScopes: settings.OIDCConnectScopes,
|
|
OIDCConnectRedirectURL: settings.OIDCConnectRedirectURL,
|
|
OIDCConnectFrontendRedirectURL: settings.OIDCConnectFrontendRedirectURL,
|
|
OIDCConnectTokenAuthMethod: settings.OIDCConnectTokenAuthMethod,
|
|
OIDCConnectUsePKCE: settings.OIDCConnectUsePKCE,
|
|
OIDCConnectValidateIDToken: settings.OIDCConnectValidateIDToken,
|
|
OIDCConnectAllowedSigningAlgs: settings.OIDCConnectAllowedSigningAlgs,
|
|
OIDCConnectClockSkewSeconds: settings.OIDCConnectClockSkewSeconds,
|
|
OIDCConnectRequireEmailVerified: settings.OIDCConnectRequireEmailVerified,
|
|
OIDCConnectUserInfoEmailPath: settings.OIDCConnectUserInfoEmailPath,
|
|
OIDCConnectUserInfoIDPath: settings.OIDCConnectUserInfoIDPath,
|
|
OIDCConnectUserInfoUsernamePath: settings.OIDCConnectUserInfoUsernamePath,
|
|
SiteName: settings.SiteName,
|
|
SiteLogo: settings.SiteLogo,
|
|
SiteSubtitle: settings.SiteSubtitle,
|
|
APIBaseURL: settings.APIBaseURL,
|
|
ContactInfo: settings.ContactInfo,
|
|
DocURL: settings.DocURL,
|
|
HomeContent: settings.HomeContent,
|
|
HideCcsImportButton: settings.HideCcsImportButton,
|
|
PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled,
|
|
PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL,
|
|
TableDefaultPageSize: settings.TableDefaultPageSize,
|
|
TablePageSizeOptions: settings.TablePageSizeOptions,
|
|
CustomMenuItems: dto.ParseCustomMenuItems(settings.CustomMenuItems),
|
|
CustomEndpoints: dto.ParseCustomEndpoints(settings.CustomEndpoints),
|
|
DefaultConcurrency: settings.DefaultConcurrency,
|
|
DefaultBalance: settings.DefaultBalance,
|
|
DefaultSubscriptions: defaultSubscriptions,
|
|
EnableModelFallback: settings.EnableModelFallback,
|
|
FallbackModelAnthropic: settings.FallbackModelAnthropic,
|
|
FallbackModelOpenAI: settings.FallbackModelOpenAI,
|
|
FallbackModelGemini: settings.FallbackModelGemini,
|
|
FallbackModelAntigravity: settings.FallbackModelAntigravity,
|
|
EnableIdentityPatch: settings.EnableIdentityPatch,
|
|
IdentityPatchPrompt: settings.IdentityPatchPrompt,
|
|
OpsMonitoringEnabled: opsEnabled && settings.OpsMonitoringEnabled,
|
|
OpsRealtimeMonitoringEnabled: settings.OpsRealtimeMonitoringEnabled,
|
|
OpsQueryModeDefault: settings.OpsQueryModeDefault,
|
|
OpsMetricsIntervalSeconds: settings.OpsMetricsIntervalSeconds,
|
|
MinClaudeCodeVersion: settings.MinClaudeCodeVersion,
|
|
MaxClaudeCodeVersion: settings.MaxClaudeCodeVersion,
|
|
AllowUngroupedKeyScheduling: settings.AllowUngroupedKeyScheduling,
|
|
BackendModeEnabled: settings.BackendModeEnabled,
|
|
EnableFingerprintUnification: settings.EnableFingerprintUnification,
|
|
EnableMetadataPassthrough: settings.EnableMetadataPassthrough,
|
|
EnableCCHSigning: settings.EnableCCHSigning,
|
|
WebSearchEmulationEnabled: settings.WebSearchEmulationEnabled,
|
|
BalanceLowNotifyEnabled: settings.BalanceLowNotifyEnabled,
|
|
BalanceLowNotifyThreshold: settings.BalanceLowNotifyThreshold,
|
|
BalanceLowNotifyRechargeURL: settings.BalanceLowNotifyRechargeURL,
|
|
AccountQuotaNotifyEnabled: settings.AccountQuotaNotifyEnabled,
|
|
AccountQuotaNotifyEmails: dto.NotifyEmailEntriesFromService(settings.AccountQuotaNotifyEmails),
|
|
PaymentEnabled: paymentCfg.Enabled,
|
|
PaymentMinAmount: paymentCfg.MinAmount,
|
|
PaymentMaxAmount: paymentCfg.MaxAmount,
|
|
PaymentDailyLimit: paymentCfg.DailyLimit,
|
|
PaymentOrderTimeoutMin: paymentCfg.OrderTimeoutMin,
|
|
PaymentMaxPendingOrders: paymentCfg.MaxPendingOrders,
|
|
PaymentEnabledTypes: paymentCfg.EnabledTypes,
|
|
PaymentBalanceDisabled: paymentCfg.BalanceDisabled,
|
|
PaymentLoadBalanceStrat: paymentCfg.LoadBalanceStrategy,
|
|
PaymentProductNamePrefix: paymentCfg.ProductNamePrefix,
|
|
PaymentProductNameSuffix: paymentCfg.ProductNameSuffix,
|
|
PaymentHelpImageURL: paymentCfg.HelpImageURL,
|
|
PaymentHelpText: paymentCfg.HelpText,
|
|
PaymentCancelRateLimitEnabled: paymentCfg.CancelRateLimitEnabled,
|
|
PaymentCancelRateLimitMax: paymentCfg.CancelRateLimitMax,
|
|
PaymentCancelRateLimitWindow: paymentCfg.CancelRateLimitWindow,
|
|
PaymentCancelRateLimitUnit: paymentCfg.CancelRateLimitUnit,
|
|
PaymentCancelRateLimitMode: paymentCfg.CancelRateLimitMode,
|
|
})
|
|
}
|
|
|
|
// UpdateSettingsRequest 更新设置请求
|
|
type UpdateSettingsRequest struct {
|
|
// 注册设置
|
|
RegistrationEnabled bool `json:"registration_enabled"`
|
|
EmailVerifyEnabled bool `json:"email_verify_enabled"`
|
|
RegistrationEmailSuffixWhitelist []string `json:"registration_email_suffix_whitelist"`
|
|
PromoCodeEnabled bool `json:"promo_code_enabled"`
|
|
PasswordResetEnabled bool `json:"password_reset_enabled"`
|
|
FrontendURL string `json:"frontend_url"`
|
|
InvitationCodeEnabled bool `json:"invitation_code_enabled"`
|
|
TotpEnabled bool `json:"totp_enabled"` // TOTP 双因素认证
|
|
|
|
// 邮件服务设置
|
|
SMTPHost string `json:"smtp_host"`
|
|
SMTPPort int `json:"smtp_port"`
|
|
SMTPUsername string `json:"smtp_username"`
|
|
SMTPPassword string `json:"smtp_password"`
|
|
SMTPFrom string `json:"smtp_from_email"`
|
|
SMTPFromName string `json:"smtp_from_name"`
|
|
SMTPUseTLS bool `json:"smtp_use_tls"`
|
|
|
|
// Cloudflare Turnstile 设置
|
|
TurnstileEnabled bool `json:"turnstile_enabled"`
|
|
TurnstileSiteKey string `json:"turnstile_site_key"`
|
|
TurnstileSecretKey string `json:"turnstile_secret_key"`
|
|
|
|
// LinuxDo Connect OAuth 登录
|
|
LinuxDoConnectEnabled bool `json:"linuxdo_connect_enabled"`
|
|
LinuxDoConnectClientID string `json:"linuxdo_connect_client_id"`
|
|
LinuxDoConnectClientSecret string `json:"linuxdo_connect_client_secret"`
|
|
LinuxDoConnectRedirectURL string `json:"linuxdo_connect_redirect_url"`
|
|
|
|
// Generic OIDC OAuth 登录
|
|
OIDCConnectEnabled bool `json:"oidc_connect_enabled"`
|
|
OIDCConnectProviderName string `json:"oidc_connect_provider_name"`
|
|
OIDCConnectClientID string `json:"oidc_connect_client_id"`
|
|
OIDCConnectClientSecret string `json:"oidc_connect_client_secret"`
|
|
OIDCConnectIssuerURL string `json:"oidc_connect_issuer_url"`
|
|
OIDCConnectDiscoveryURL string `json:"oidc_connect_discovery_url"`
|
|
OIDCConnectAuthorizeURL string `json:"oidc_connect_authorize_url"`
|
|
OIDCConnectTokenURL string `json:"oidc_connect_token_url"`
|
|
OIDCConnectUserInfoURL string `json:"oidc_connect_userinfo_url"`
|
|
OIDCConnectJWKSURL string `json:"oidc_connect_jwks_url"`
|
|
OIDCConnectScopes string `json:"oidc_connect_scopes"`
|
|
OIDCConnectRedirectURL string `json:"oidc_connect_redirect_url"`
|
|
OIDCConnectFrontendRedirectURL string `json:"oidc_connect_frontend_redirect_url"`
|
|
OIDCConnectTokenAuthMethod string `json:"oidc_connect_token_auth_method"`
|
|
OIDCConnectUsePKCE bool `json:"oidc_connect_use_pkce"`
|
|
OIDCConnectValidateIDToken bool `json:"oidc_connect_validate_id_token"`
|
|
OIDCConnectAllowedSigningAlgs string `json:"oidc_connect_allowed_signing_algs"`
|
|
OIDCConnectClockSkewSeconds int `json:"oidc_connect_clock_skew_seconds"`
|
|
OIDCConnectRequireEmailVerified bool `json:"oidc_connect_require_email_verified"`
|
|
OIDCConnectUserInfoEmailPath string `json:"oidc_connect_userinfo_email_path"`
|
|
OIDCConnectUserInfoIDPath string `json:"oidc_connect_userinfo_id_path"`
|
|
OIDCConnectUserInfoUsernamePath string `json:"oidc_connect_userinfo_username_path"`
|
|
|
|
// OEM设置
|
|
SiteName string `json:"site_name"`
|
|
SiteLogo string `json:"site_logo"`
|
|
SiteSubtitle string `json:"site_subtitle"`
|
|
APIBaseURL string `json:"api_base_url"`
|
|
ContactInfo string `json:"contact_info"`
|
|
DocURL string `json:"doc_url"`
|
|
HomeContent string `json:"home_content"`
|
|
HideCcsImportButton bool `json:"hide_ccs_import_button"`
|
|
PurchaseSubscriptionEnabled *bool `json:"purchase_subscription_enabled"`
|
|
PurchaseSubscriptionURL *string `json:"purchase_subscription_url"`
|
|
TableDefaultPageSize int `json:"table_default_page_size"`
|
|
TablePageSizeOptions []int `json:"table_page_size_options"`
|
|
CustomMenuItems *[]dto.CustomMenuItem `json:"custom_menu_items"`
|
|
CustomEndpoints *[]dto.CustomEndpoint `json:"custom_endpoints"`
|
|
|
|
// 默认配置
|
|
DefaultConcurrency int `json:"default_concurrency"`
|
|
DefaultBalance float64 `json:"default_balance"`
|
|
DefaultSubscriptions []dto.DefaultSubscriptionSetting `json:"default_subscriptions"`
|
|
|
|
// Model fallback configuration
|
|
EnableModelFallback bool `json:"enable_model_fallback"`
|
|
FallbackModelAnthropic string `json:"fallback_model_anthropic"`
|
|
FallbackModelOpenAI string `json:"fallback_model_openai"`
|
|
FallbackModelGemini string `json:"fallback_model_gemini"`
|
|
FallbackModelAntigravity string `json:"fallback_model_antigravity"`
|
|
|
|
// Identity patch configuration (Claude -> Gemini)
|
|
EnableIdentityPatch bool `json:"enable_identity_patch"`
|
|
IdentityPatchPrompt string `json:"identity_patch_prompt"`
|
|
|
|
// Ops monitoring (vNext)
|
|
OpsMonitoringEnabled *bool `json:"ops_monitoring_enabled"`
|
|
OpsRealtimeMonitoringEnabled *bool `json:"ops_realtime_monitoring_enabled"`
|
|
OpsQueryModeDefault *string `json:"ops_query_mode_default"`
|
|
OpsMetricsIntervalSeconds *int `json:"ops_metrics_interval_seconds"`
|
|
|
|
MinClaudeCodeVersion string `json:"min_claude_code_version"`
|
|
MaxClaudeCodeVersion string `json:"max_claude_code_version"`
|
|
|
|
// 分组隔离
|
|
AllowUngroupedKeyScheduling bool `json:"allow_ungrouped_key_scheduling"`
|
|
|
|
// Backend Mode
|
|
BackendModeEnabled bool `json:"backend_mode_enabled"`
|
|
|
|
// Gateway forwarding behavior
|
|
EnableFingerprintUnification *bool `json:"enable_fingerprint_unification"`
|
|
EnableMetadataPassthrough *bool `json:"enable_metadata_passthrough"`
|
|
EnableCCHSigning *bool `json:"enable_cch_signing"`
|
|
|
|
// Balance low notification
|
|
BalanceLowNotifyEnabled *bool `json:"balance_low_notify_enabled"`
|
|
BalanceLowNotifyThreshold *float64 `json:"balance_low_notify_threshold"`
|
|
BalanceLowNotifyRechargeURL *string `json:"balance_low_notify_recharge_url"`
|
|
AccountQuotaNotifyEnabled *bool `json:"account_quota_notify_enabled"`
|
|
AccountQuotaNotifyEmails *[]dto.NotifyEmailEntry `json:"account_quota_notify_emails"`
|
|
|
|
// Payment configuration (integrated into settings, full replace)
|
|
PaymentEnabled *bool `json:"payment_enabled"`
|
|
PaymentMinAmount *float64 `json:"payment_min_amount"`
|
|
PaymentMaxAmount *float64 `json:"payment_max_amount"`
|
|
PaymentDailyLimit *float64 `json:"payment_daily_limit"`
|
|
PaymentOrderTimeoutMin *int `json:"payment_order_timeout_minutes"`
|
|
PaymentMaxPendingOrders *int `json:"payment_max_pending_orders"`
|
|
PaymentEnabledTypes []string `json:"payment_enabled_types"`
|
|
PaymentBalanceDisabled *bool `json:"payment_balance_disabled"`
|
|
PaymentLoadBalanceStrat *string `json:"payment_load_balance_strategy"`
|
|
PaymentProductNamePrefix *string `json:"payment_product_name_prefix"`
|
|
PaymentProductNameSuffix *string `json:"payment_product_name_suffix"`
|
|
PaymentHelpImageURL *string `json:"payment_help_image_url"`
|
|
PaymentHelpText *string `json:"payment_help_text"`
|
|
|
|
// Cancel rate limit
|
|
PaymentCancelRateLimitEnabled *bool `json:"payment_cancel_rate_limit_enabled"`
|
|
PaymentCancelRateLimitMax *int `json:"payment_cancel_rate_limit_max"`
|
|
PaymentCancelRateLimitWindow *int `json:"payment_cancel_rate_limit_window"`
|
|
PaymentCancelRateLimitUnit *string `json:"payment_cancel_rate_limit_unit"`
|
|
PaymentCancelRateLimitMode *string `json:"payment_cancel_rate_limit_window_mode"`
|
|
}
|
|
|
|
// UpdateSettings 更新系统设置
|
|
// PUT /api/v1/admin/settings
|
|
func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
|
var req UpdateSettingsRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
|
|
previousSettings, err := h.settingService.GetAllSettings(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
// 验证参数
|
|
if req.DefaultConcurrency < 1 {
|
|
req.DefaultConcurrency = 1
|
|
}
|
|
if req.DefaultBalance < 0 {
|
|
req.DefaultBalance = 0
|
|
}
|
|
// 通用表格配置:兼容旧客户端未传字段时保留当前值。
|
|
if req.TableDefaultPageSize <= 0 {
|
|
req.TableDefaultPageSize = previousSettings.TableDefaultPageSize
|
|
}
|
|
if req.TablePageSizeOptions == nil {
|
|
req.TablePageSizeOptions = previousSettings.TablePageSizeOptions
|
|
}
|
|
req.SMTPHost = strings.TrimSpace(req.SMTPHost)
|
|
req.SMTPUsername = strings.TrimSpace(req.SMTPUsername)
|
|
req.SMTPPassword = strings.TrimSpace(req.SMTPPassword)
|
|
req.SMTPFrom = strings.TrimSpace(req.SMTPFrom)
|
|
req.SMTPFromName = strings.TrimSpace(req.SMTPFromName)
|
|
if req.SMTPPort <= 0 {
|
|
req.SMTPPort = 587
|
|
}
|
|
req.DefaultSubscriptions = normalizeDefaultSubscriptions(req.DefaultSubscriptions)
|
|
|
|
// SMTP 配置保护:如果请求中 smtp_host 为空但数据库中已有配置,则保留已有 SMTP 配置
|
|
// 防止前端加载设置失败时空表单覆盖已保存的 SMTP 配置
|
|
if req.SMTPHost == "" && previousSettings.SMTPHost != "" {
|
|
req.SMTPHost = previousSettings.SMTPHost
|
|
req.SMTPPort = previousSettings.SMTPPort
|
|
req.SMTPUsername = previousSettings.SMTPUsername
|
|
req.SMTPFrom = previousSettings.SMTPFrom
|
|
req.SMTPFromName = previousSettings.SMTPFromName
|
|
req.SMTPUseTLS = previousSettings.SMTPUseTLS
|
|
}
|
|
|
|
// Turnstile 参数验证
|
|
if req.TurnstileEnabled {
|
|
// 检查必填字段
|
|
if req.TurnstileSiteKey == "" {
|
|
response.BadRequest(c, "Turnstile Site Key is required when enabled")
|
|
return
|
|
}
|
|
// 如果未提供 secret key,使用已保存的值(留空保留当前值)
|
|
if req.TurnstileSecretKey == "" {
|
|
if previousSettings.TurnstileSecretKey == "" {
|
|
response.BadRequest(c, "Turnstile Secret Key is required when enabled")
|
|
return
|
|
}
|
|
req.TurnstileSecretKey = previousSettings.TurnstileSecretKey
|
|
}
|
|
|
|
// 当 site_key 或 secret_key 任一变化时验证(避免配置错误导致无法登录)
|
|
siteKeyChanged := previousSettings.TurnstileSiteKey != req.TurnstileSiteKey
|
|
secretKeyChanged := previousSettings.TurnstileSecretKey != req.TurnstileSecretKey
|
|
if siteKeyChanged || secretKeyChanged {
|
|
if err := h.turnstileService.ValidateSecretKey(c.Request.Context(), req.TurnstileSecretKey); err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// TOTP 双因素认证参数验证
|
|
// 只有手动配置了加密密钥才允许启用 TOTP 功能
|
|
if req.TotpEnabled && !previousSettings.TotpEnabled {
|
|
// 尝试启用 TOTP,检查加密密钥是否已手动配置
|
|
if !h.settingService.IsTotpEncryptionKeyConfigured() {
|
|
response.BadRequest(c, "Cannot enable TOTP: TOTP_ENCRYPTION_KEY environment variable must be configured first. Generate a key with 'openssl rand -hex 32' and set it in your environment.")
|
|
return
|
|
}
|
|
}
|
|
|
|
// LinuxDo Connect 参数验证
|
|
if req.LinuxDoConnectEnabled {
|
|
req.LinuxDoConnectClientID = strings.TrimSpace(req.LinuxDoConnectClientID)
|
|
req.LinuxDoConnectClientSecret = strings.TrimSpace(req.LinuxDoConnectClientSecret)
|
|
req.LinuxDoConnectRedirectURL = strings.TrimSpace(req.LinuxDoConnectRedirectURL)
|
|
|
|
if req.LinuxDoConnectClientID == "" {
|
|
response.BadRequest(c, "LinuxDo Client ID is required when enabled")
|
|
return
|
|
}
|
|
if req.LinuxDoConnectRedirectURL == "" {
|
|
response.BadRequest(c, "LinuxDo Redirect URL is required when enabled")
|
|
return
|
|
}
|
|
if err := config.ValidateAbsoluteHTTPURL(req.LinuxDoConnectRedirectURL); err != nil {
|
|
response.BadRequest(c, "LinuxDo Redirect URL must be an absolute http(s) URL")
|
|
return
|
|
}
|
|
|
|
// 如果未提供 client_secret,则保留现有值(如有)。
|
|
if req.LinuxDoConnectClientSecret == "" {
|
|
if previousSettings.LinuxDoConnectClientSecret == "" {
|
|
response.BadRequest(c, "LinuxDo Client Secret is required when enabled")
|
|
return
|
|
}
|
|
req.LinuxDoConnectClientSecret = previousSettings.LinuxDoConnectClientSecret
|
|
}
|
|
}
|
|
|
|
// Generic OIDC 参数验证
|
|
if req.OIDCConnectEnabled {
|
|
req.OIDCConnectProviderName = strings.TrimSpace(req.OIDCConnectProviderName)
|
|
req.OIDCConnectClientID = strings.TrimSpace(req.OIDCConnectClientID)
|
|
req.OIDCConnectClientSecret = strings.TrimSpace(req.OIDCConnectClientSecret)
|
|
req.OIDCConnectIssuerURL = strings.TrimSpace(req.OIDCConnectIssuerURL)
|
|
req.OIDCConnectDiscoveryURL = strings.TrimSpace(req.OIDCConnectDiscoveryURL)
|
|
req.OIDCConnectAuthorizeURL = strings.TrimSpace(req.OIDCConnectAuthorizeURL)
|
|
req.OIDCConnectTokenURL = strings.TrimSpace(req.OIDCConnectTokenURL)
|
|
req.OIDCConnectUserInfoURL = strings.TrimSpace(req.OIDCConnectUserInfoURL)
|
|
req.OIDCConnectJWKSURL = strings.TrimSpace(req.OIDCConnectJWKSURL)
|
|
req.OIDCConnectScopes = strings.TrimSpace(req.OIDCConnectScopes)
|
|
req.OIDCConnectRedirectURL = strings.TrimSpace(req.OIDCConnectRedirectURL)
|
|
req.OIDCConnectFrontendRedirectURL = strings.TrimSpace(req.OIDCConnectFrontendRedirectURL)
|
|
req.OIDCConnectTokenAuthMethod = strings.ToLower(strings.TrimSpace(req.OIDCConnectTokenAuthMethod))
|
|
req.OIDCConnectAllowedSigningAlgs = strings.TrimSpace(req.OIDCConnectAllowedSigningAlgs)
|
|
req.OIDCConnectUserInfoEmailPath = strings.TrimSpace(req.OIDCConnectUserInfoEmailPath)
|
|
req.OIDCConnectUserInfoIDPath = strings.TrimSpace(req.OIDCConnectUserInfoIDPath)
|
|
req.OIDCConnectUserInfoUsernamePath = strings.TrimSpace(req.OIDCConnectUserInfoUsernamePath)
|
|
|
|
if req.OIDCConnectProviderName == "" {
|
|
req.OIDCConnectProviderName = "OIDC"
|
|
}
|
|
if req.OIDCConnectClientID == "" {
|
|
response.BadRequest(c, "OIDC Client ID is required when enabled")
|
|
return
|
|
}
|
|
if req.OIDCConnectIssuerURL == "" {
|
|
response.BadRequest(c, "OIDC Issuer URL is required when enabled")
|
|
return
|
|
}
|
|
if err := config.ValidateAbsoluteHTTPURL(req.OIDCConnectIssuerURL); err != nil {
|
|
response.BadRequest(c, "OIDC Issuer URL must be an absolute http(s) URL")
|
|
return
|
|
}
|
|
if req.OIDCConnectDiscoveryURL != "" {
|
|
if err := config.ValidateAbsoluteHTTPURL(req.OIDCConnectDiscoveryURL); err != nil {
|
|
response.BadRequest(c, "OIDC Discovery URL must be an absolute http(s) URL")
|
|
return
|
|
}
|
|
}
|
|
if req.OIDCConnectAuthorizeURL != "" {
|
|
if err := config.ValidateAbsoluteHTTPURL(req.OIDCConnectAuthorizeURL); err != nil {
|
|
response.BadRequest(c, "OIDC Authorize URL must be an absolute http(s) URL")
|
|
return
|
|
}
|
|
}
|
|
if req.OIDCConnectTokenURL != "" {
|
|
if err := config.ValidateAbsoluteHTTPURL(req.OIDCConnectTokenURL); err != nil {
|
|
response.BadRequest(c, "OIDC Token URL must be an absolute http(s) URL")
|
|
return
|
|
}
|
|
}
|
|
if req.OIDCConnectUserInfoURL != "" {
|
|
if err := config.ValidateAbsoluteHTTPURL(req.OIDCConnectUserInfoURL); err != nil {
|
|
response.BadRequest(c, "OIDC UserInfo URL must be an absolute http(s) URL")
|
|
return
|
|
}
|
|
}
|
|
if req.OIDCConnectRedirectURL == "" {
|
|
response.BadRequest(c, "OIDC Redirect URL is required when enabled")
|
|
return
|
|
}
|
|
if err := config.ValidateAbsoluteHTTPURL(req.OIDCConnectRedirectURL); err != nil {
|
|
response.BadRequest(c, "OIDC Redirect URL must be an absolute http(s) URL")
|
|
return
|
|
}
|
|
if req.OIDCConnectFrontendRedirectURL == "" {
|
|
response.BadRequest(c, "OIDC Frontend Redirect URL is required when enabled")
|
|
return
|
|
}
|
|
if err := config.ValidateFrontendRedirectURL(req.OIDCConnectFrontendRedirectURL); err != nil {
|
|
response.BadRequest(c, "OIDC Frontend Redirect URL is invalid")
|
|
return
|
|
}
|
|
if !scopesContainOpenID(req.OIDCConnectScopes) {
|
|
response.BadRequest(c, "OIDC scopes must contain openid")
|
|
return
|
|
}
|
|
switch req.OIDCConnectTokenAuthMethod {
|
|
case "", "client_secret_post", "client_secret_basic", "none":
|
|
default:
|
|
response.BadRequest(c, "OIDC Token Auth Method must be one of client_secret_post/client_secret_basic/none")
|
|
return
|
|
}
|
|
if req.OIDCConnectTokenAuthMethod == "none" && !req.OIDCConnectUsePKCE {
|
|
response.BadRequest(c, "OIDC PKCE must be enabled when token_auth_method=none")
|
|
return
|
|
}
|
|
if req.OIDCConnectClockSkewSeconds < 0 || req.OIDCConnectClockSkewSeconds > 600 {
|
|
response.BadRequest(c, "OIDC clock skew seconds must be between 0 and 600")
|
|
return
|
|
}
|
|
if req.OIDCConnectValidateIDToken {
|
|
if req.OIDCConnectAllowedSigningAlgs == "" {
|
|
response.BadRequest(c, "OIDC Allowed Signing Algs is required when validate_id_token=true")
|
|
return
|
|
}
|
|
}
|
|
if req.OIDCConnectJWKSURL != "" {
|
|
if err := config.ValidateAbsoluteHTTPURL(req.OIDCConnectJWKSURL); err != nil {
|
|
response.BadRequest(c, "OIDC JWKS URL must be an absolute http(s) URL")
|
|
return
|
|
}
|
|
}
|
|
if req.OIDCConnectTokenAuthMethod == "" || req.OIDCConnectTokenAuthMethod == "client_secret_post" || req.OIDCConnectTokenAuthMethod == "client_secret_basic" {
|
|
if req.OIDCConnectClientSecret == "" {
|
|
if previousSettings.OIDCConnectClientSecret == "" {
|
|
response.BadRequest(c, "OIDC Client Secret is required when enabled")
|
|
return
|
|
}
|
|
req.OIDCConnectClientSecret = previousSettings.OIDCConnectClientSecret
|
|
}
|
|
}
|
|
}
|
|
|
|
// “购买订阅”页面配置验证
|
|
purchaseEnabled := previousSettings.PurchaseSubscriptionEnabled
|
|
if req.PurchaseSubscriptionEnabled != nil {
|
|
purchaseEnabled = *req.PurchaseSubscriptionEnabled
|
|
}
|
|
purchaseURL := previousSettings.PurchaseSubscriptionURL
|
|
if req.PurchaseSubscriptionURL != nil {
|
|
purchaseURL = strings.TrimSpace(*req.PurchaseSubscriptionURL)
|
|
}
|
|
|
|
// - 启用时要求 URL 合法且非空
|
|
// - 禁用时允许为空;若提供了 URL 也做基本校验,避免误配置
|
|
if purchaseEnabled {
|
|
if purchaseURL == "" {
|
|
response.BadRequest(c, "Purchase Subscription URL is required when enabled")
|
|
return
|
|
}
|
|
if err := config.ValidateAbsoluteHTTPURL(purchaseURL); err != nil {
|
|
response.BadRequest(c, "Purchase Subscription URL must be an absolute http(s) URL")
|
|
return
|
|
}
|
|
} else if purchaseURL != "" {
|
|
if err := config.ValidateAbsoluteHTTPURL(purchaseURL); err != nil {
|
|
response.BadRequest(c, "Purchase Subscription URL must be an absolute http(s) URL")
|
|
return
|
|
}
|
|
}
|
|
|
|
// Frontend URL 验证
|
|
req.FrontendURL = strings.TrimSpace(req.FrontendURL)
|
|
if req.FrontendURL != "" {
|
|
if err := config.ValidateAbsoluteHTTPURL(req.FrontendURL); err != nil {
|
|
response.BadRequest(c, "Frontend URL must be an absolute http(s) URL")
|
|
return
|
|
}
|
|
}
|
|
|
|
// 自定义菜单项验证
|
|
const (
|
|
maxCustomMenuItems = 20
|
|
maxMenuItemLabelLen = 50
|
|
maxMenuItemURLLen = 2048
|
|
maxMenuItemIconSVGLen = 10 * 1024 // 10KB
|
|
maxMenuItemIDLen = 32
|
|
)
|
|
|
|
customMenuJSON := previousSettings.CustomMenuItems
|
|
if req.CustomMenuItems != nil {
|
|
items := *req.CustomMenuItems
|
|
if len(items) > maxCustomMenuItems {
|
|
response.BadRequest(c, "Too many custom menu items (max 20)")
|
|
return
|
|
}
|
|
for i, item := range items {
|
|
if strings.TrimSpace(item.Label) == "" {
|
|
response.BadRequest(c, "Custom menu item label is required")
|
|
return
|
|
}
|
|
if len(item.Label) > maxMenuItemLabelLen {
|
|
response.BadRequest(c, "Custom menu item label is too long (max 50 characters)")
|
|
return
|
|
}
|
|
if strings.TrimSpace(item.URL) == "" {
|
|
response.BadRequest(c, "Custom menu item URL is required")
|
|
return
|
|
}
|
|
if len(item.URL) > maxMenuItemURLLen {
|
|
response.BadRequest(c, "Custom menu item URL is too long (max 2048 characters)")
|
|
return
|
|
}
|
|
if err := config.ValidateAbsoluteHTTPURL(strings.TrimSpace(item.URL)); err != nil {
|
|
response.BadRequest(c, "Custom menu item URL must be an absolute http(s) URL")
|
|
return
|
|
}
|
|
if item.Visibility != "user" && item.Visibility != "admin" {
|
|
response.BadRequest(c, "Custom menu item visibility must be 'user' or 'admin'")
|
|
return
|
|
}
|
|
if len(item.IconSVG) > maxMenuItemIconSVGLen {
|
|
response.BadRequest(c, "Custom menu item icon SVG is too large (max 10KB)")
|
|
return
|
|
}
|
|
// Auto-generate ID if missing
|
|
if strings.TrimSpace(item.ID) == "" {
|
|
id, err := generateMenuItemID()
|
|
if err != nil {
|
|
response.Error(c, http.StatusInternalServerError, "Failed to generate menu item ID")
|
|
return
|
|
}
|
|
items[i].ID = id
|
|
} else if len(item.ID) > maxMenuItemIDLen {
|
|
response.BadRequest(c, "Custom menu item ID is too long (max 32 characters)")
|
|
return
|
|
} else if !menuItemIDPattern.MatchString(item.ID) {
|
|
response.BadRequest(c, "Custom menu item ID contains invalid characters (only a-z, A-Z, 0-9, - and _ are allowed)")
|
|
return
|
|
}
|
|
}
|
|
// ID uniqueness check
|
|
seen := make(map[string]struct{}, len(items))
|
|
for _, item := range items {
|
|
if _, exists := seen[item.ID]; exists {
|
|
response.BadRequest(c, "Duplicate custom menu item ID: "+item.ID)
|
|
return
|
|
}
|
|
seen[item.ID] = struct{}{}
|
|
}
|
|
menuBytes, err := json.Marshal(items)
|
|
if err != nil {
|
|
response.BadRequest(c, "Failed to serialize custom menu items")
|
|
return
|
|
}
|
|
customMenuJSON = string(menuBytes)
|
|
}
|
|
|
|
// 自定义端点验证
|
|
const (
|
|
maxCustomEndpoints = 10
|
|
maxEndpointNameLen = 50
|
|
maxEndpointURLLen = 2048
|
|
maxEndpointDescriptionLen = 200
|
|
)
|
|
|
|
customEndpointsJSON := previousSettings.CustomEndpoints
|
|
if req.CustomEndpoints != nil {
|
|
endpoints := *req.CustomEndpoints
|
|
if len(endpoints) > maxCustomEndpoints {
|
|
response.BadRequest(c, "Too many custom endpoints (max 10)")
|
|
return
|
|
}
|
|
for _, ep := range endpoints {
|
|
if strings.TrimSpace(ep.Name) == "" {
|
|
response.BadRequest(c, "Custom endpoint name is required")
|
|
return
|
|
}
|
|
if len(ep.Name) > maxEndpointNameLen {
|
|
response.BadRequest(c, "Custom endpoint name is too long (max 50 characters)")
|
|
return
|
|
}
|
|
if strings.TrimSpace(ep.Endpoint) == "" {
|
|
response.BadRequest(c, "Custom endpoint URL is required")
|
|
return
|
|
}
|
|
if len(ep.Endpoint) > maxEndpointURLLen {
|
|
response.BadRequest(c, "Custom endpoint URL is too long (max 2048 characters)")
|
|
return
|
|
}
|
|
if err := config.ValidateAbsoluteHTTPURL(strings.TrimSpace(ep.Endpoint)); err != nil {
|
|
response.BadRequest(c, "Custom endpoint URL must be an absolute http(s) URL")
|
|
return
|
|
}
|
|
if len(ep.Description) > maxEndpointDescriptionLen {
|
|
response.BadRequest(c, "Custom endpoint description is too long (max 200 characters)")
|
|
return
|
|
}
|
|
}
|
|
endpointBytes, err := json.Marshal(endpoints)
|
|
if err != nil {
|
|
response.BadRequest(c, "Failed to serialize custom endpoints")
|
|
return
|
|
}
|
|
customEndpointsJSON = string(endpointBytes)
|
|
}
|
|
|
|
// Ops metrics collector interval validation (seconds).
|
|
if req.OpsMetricsIntervalSeconds != nil {
|
|
v := *req.OpsMetricsIntervalSeconds
|
|
if v < 60 {
|
|
v = 60
|
|
}
|
|
if v > 3600 {
|
|
v = 3600
|
|
}
|
|
req.OpsMetricsIntervalSeconds = &v
|
|
}
|
|
defaultSubscriptions := make([]service.DefaultSubscriptionSetting, 0, len(req.DefaultSubscriptions))
|
|
for _, sub := range req.DefaultSubscriptions {
|
|
defaultSubscriptions = append(defaultSubscriptions, service.DefaultSubscriptionSetting{
|
|
GroupID: sub.GroupID,
|
|
ValidityDays: sub.ValidityDays,
|
|
})
|
|
}
|
|
|
|
// 验证最低版本号格式(空字符串=禁用,或合法 semver)
|
|
if req.MinClaudeCodeVersion != "" {
|
|
if !semverPattern.MatchString(req.MinClaudeCodeVersion) {
|
|
response.Error(c, http.StatusBadRequest, "min_claude_code_version must be empty or a valid semver (e.g. 2.1.63)")
|
|
return
|
|
}
|
|
}
|
|
|
|
// 验证最高版本号格式(空字符串=禁用,或合法 semver)
|
|
if req.MaxClaudeCodeVersion != "" {
|
|
if !semverPattern.MatchString(req.MaxClaudeCodeVersion) {
|
|
response.Error(c, http.StatusBadRequest, "max_claude_code_version must be empty or a valid semver (e.g. 3.0.0)")
|
|
return
|
|
}
|
|
}
|
|
|
|
// 交叉验证:如果同时设置了最低和最高版本号,最高版本号必须 >= 最低版本号
|
|
if req.MinClaudeCodeVersion != "" && req.MaxClaudeCodeVersion != "" {
|
|
if service.CompareVersions(req.MaxClaudeCodeVersion, req.MinClaudeCodeVersion) < 0 {
|
|
response.Error(c, http.StatusBadRequest, "max_claude_code_version must be greater than or equal to min_claude_code_version")
|
|
return
|
|
}
|
|
}
|
|
|
|
settings := &service.SystemSettings{
|
|
RegistrationEnabled: req.RegistrationEnabled,
|
|
EmailVerifyEnabled: req.EmailVerifyEnabled,
|
|
RegistrationEmailSuffixWhitelist: req.RegistrationEmailSuffixWhitelist,
|
|
PromoCodeEnabled: req.PromoCodeEnabled,
|
|
PasswordResetEnabled: req.PasswordResetEnabled,
|
|
FrontendURL: req.FrontendURL,
|
|
InvitationCodeEnabled: req.InvitationCodeEnabled,
|
|
TotpEnabled: req.TotpEnabled,
|
|
SMTPHost: req.SMTPHost,
|
|
SMTPPort: req.SMTPPort,
|
|
SMTPUsername: req.SMTPUsername,
|
|
SMTPPassword: req.SMTPPassword,
|
|
SMTPFrom: req.SMTPFrom,
|
|
SMTPFromName: req.SMTPFromName,
|
|
SMTPUseTLS: req.SMTPUseTLS,
|
|
TurnstileEnabled: req.TurnstileEnabled,
|
|
TurnstileSiteKey: req.TurnstileSiteKey,
|
|
TurnstileSecretKey: req.TurnstileSecretKey,
|
|
LinuxDoConnectEnabled: req.LinuxDoConnectEnabled,
|
|
LinuxDoConnectClientID: req.LinuxDoConnectClientID,
|
|
LinuxDoConnectClientSecret: req.LinuxDoConnectClientSecret,
|
|
LinuxDoConnectRedirectURL: req.LinuxDoConnectRedirectURL,
|
|
OIDCConnectEnabled: req.OIDCConnectEnabled,
|
|
OIDCConnectProviderName: req.OIDCConnectProviderName,
|
|
OIDCConnectClientID: req.OIDCConnectClientID,
|
|
OIDCConnectClientSecret: req.OIDCConnectClientSecret,
|
|
OIDCConnectIssuerURL: req.OIDCConnectIssuerURL,
|
|
OIDCConnectDiscoveryURL: req.OIDCConnectDiscoveryURL,
|
|
OIDCConnectAuthorizeURL: req.OIDCConnectAuthorizeURL,
|
|
OIDCConnectTokenURL: req.OIDCConnectTokenURL,
|
|
OIDCConnectUserInfoURL: req.OIDCConnectUserInfoURL,
|
|
OIDCConnectJWKSURL: req.OIDCConnectJWKSURL,
|
|
OIDCConnectScopes: req.OIDCConnectScopes,
|
|
OIDCConnectRedirectURL: req.OIDCConnectRedirectURL,
|
|
OIDCConnectFrontendRedirectURL: req.OIDCConnectFrontendRedirectURL,
|
|
OIDCConnectTokenAuthMethod: req.OIDCConnectTokenAuthMethod,
|
|
OIDCConnectUsePKCE: req.OIDCConnectUsePKCE,
|
|
OIDCConnectValidateIDToken: req.OIDCConnectValidateIDToken,
|
|
OIDCConnectAllowedSigningAlgs: req.OIDCConnectAllowedSigningAlgs,
|
|
OIDCConnectClockSkewSeconds: req.OIDCConnectClockSkewSeconds,
|
|
OIDCConnectRequireEmailVerified: req.OIDCConnectRequireEmailVerified,
|
|
OIDCConnectUserInfoEmailPath: req.OIDCConnectUserInfoEmailPath,
|
|
OIDCConnectUserInfoIDPath: req.OIDCConnectUserInfoIDPath,
|
|
OIDCConnectUserInfoUsernamePath: req.OIDCConnectUserInfoUsernamePath,
|
|
SiteName: req.SiteName,
|
|
SiteLogo: req.SiteLogo,
|
|
SiteSubtitle: req.SiteSubtitle,
|
|
APIBaseURL: req.APIBaseURL,
|
|
ContactInfo: req.ContactInfo,
|
|
DocURL: req.DocURL,
|
|
HomeContent: req.HomeContent,
|
|
HideCcsImportButton: req.HideCcsImportButton,
|
|
PurchaseSubscriptionEnabled: purchaseEnabled,
|
|
PurchaseSubscriptionURL: purchaseURL,
|
|
TableDefaultPageSize: req.TableDefaultPageSize,
|
|
TablePageSizeOptions: req.TablePageSizeOptions,
|
|
CustomMenuItems: customMenuJSON,
|
|
CustomEndpoints: customEndpointsJSON,
|
|
DefaultConcurrency: req.DefaultConcurrency,
|
|
DefaultBalance: req.DefaultBalance,
|
|
DefaultSubscriptions: defaultSubscriptions,
|
|
EnableModelFallback: req.EnableModelFallback,
|
|
FallbackModelAnthropic: req.FallbackModelAnthropic,
|
|
FallbackModelOpenAI: req.FallbackModelOpenAI,
|
|
FallbackModelGemini: req.FallbackModelGemini,
|
|
FallbackModelAntigravity: req.FallbackModelAntigravity,
|
|
EnableIdentityPatch: req.EnableIdentityPatch,
|
|
IdentityPatchPrompt: req.IdentityPatchPrompt,
|
|
MinClaudeCodeVersion: req.MinClaudeCodeVersion,
|
|
MaxClaudeCodeVersion: req.MaxClaudeCodeVersion,
|
|
AllowUngroupedKeyScheduling: req.AllowUngroupedKeyScheduling,
|
|
BackendModeEnabled: req.BackendModeEnabled,
|
|
OpsMonitoringEnabled: func() bool {
|
|
if req.OpsMonitoringEnabled != nil {
|
|
return *req.OpsMonitoringEnabled
|
|
}
|
|
return previousSettings.OpsMonitoringEnabled
|
|
}(),
|
|
OpsRealtimeMonitoringEnabled: func() bool {
|
|
if req.OpsRealtimeMonitoringEnabled != nil {
|
|
return *req.OpsRealtimeMonitoringEnabled
|
|
}
|
|
return previousSettings.OpsRealtimeMonitoringEnabled
|
|
}(),
|
|
OpsQueryModeDefault: func() string {
|
|
if req.OpsQueryModeDefault != nil {
|
|
return *req.OpsQueryModeDefault
|
|
}
|
|
return previousSettings.OpsQueryModeDefault
|
|
}(),
|
|
OpsMetricsIntervalSeconds: func() int {
|
|
if req.OpsMetricsIntervalSeconds != nil {
|
|
return *req.OpsMetricsIntervalSeconds
|
|
}
|
|
return previousSettings.OpsMetricsIntervalSeconds
|
|
}(),
|
|
EnableFingerprintUnification: func() bool {
|
|
if req.EnableFingerprintUnification != nil {
|
|
return *req.EnableFingerprintUnification
|
|
}
|
|
return previousSettings.EnableFingerprintUnification
|
|
}(),
|
|
EnableMetadataPassthrough: func() bool {
|
|
if req.EnableMetadataPassthrough != nil {
|
|
return *req.EnableMetadataPassthrough
|
|
}
|
|
return previousSettings.EnableMetadataPassthrough
|
|
}(),
|
|
EnableCCHSigning: func() bool {
|
|
if req.EnableCCHSigning != nil {
|
|
return *req.EnableCCHSigning
|
|
}
|
|
return previousSettings.EnableCCHSigning
|
|
}(),
|
|
BalanceLowNotifyEnabled: func() bool {
|
|
if req.BalanceLowNotifyEnabled != nil {
|
|
return *req.BalanceLowNotifyEnabled
|
|
}
|
|
return previousSettings.BalanceLowNotifyEnabled
|
|
}(),
|
|
BalanceLowNotifyThreshold: func() float64 {
|
|
if req.BalanceLowNotifyThreshold != nil {
|
|
return *req.BalanceLowNotifyThreshold
|
|
}
|
|
return previousSettings.BalanceLowNotifyThreshold
|
|
}(),
|
|
BalanceLowNotifyRechargeURL: func() string {
|
|
if req.BalanceLowNotifyRechargeURL != nil {
|
|
return *req.BalanceLowNotifyRechargeURL
|
|
}
|
|
return previousSettings.BalanceLowNotifyRechargeURL
|
|
}(),
|
|
AccountQuotaNotifyEnabled: func() bool {
|
|
if req.AccountQuotaNotifyEnabled != nil {
|
|
return *req.AccountQuotaNotifyEnabled
|
|
}
|
|
return previousSettings.AccountQuotaNotifyEnabled
|
|
}(),
|
|
AccountQuotaNotifyEmails: func() []service.NotifyEmailEntry {
|
|
if req.AccountQuotaNotifyEmails != nil {
|
|
return dto.NotifyEmailEntriesToService(*req.AccountQuotaNotifyEmails)
|
|
}
|
|
return previousSettings.AccountQuotaNotifyEmails
|
|
}(),
|
|
}
|
|
|
|
if err := h.settingService.UpdateSettings(c.Request.Context(), settings); err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
// Update payment configuration (integrated into system settings).
|
|
// Skip if no payment fields were provided (prevents accidental wipe).
|
|
if h.paymentConfigService != nil && hasPaymentFields(req) {
|
|
paymentReq := service.UpdatePaymentConfigRequest{
|
|
Enabled: req.PaymentEnabled,
|
|
MinAmount: req.PaymentMinAmount,
|
|
MaxAmount: req.PaymentMaxAmount,
|
|
DailyLimit: req.PaymentDailyLimit,
|
|
OrderTimeoutMin: req.PaymentOrderTimeoutMin,
|
|
MaxPendingOrders: req.PaymentMaxPendingOrders,
|
|
EnabledTypes: req.PaymentEnabledTypes,
|
|
BalanceDisabled: req.PaymentBalanceDisabled,
|
|
LoadBalanceStrategy: req.PaymentLoadBalanceStrat,
|
|
ProductNamePrefix: req.PaymentProductNamePrefix,
|
|
ProductNameSuffix: req.PaymentProductNameSuffix,
|
|
HelpImageURL: req.PaymentHelpImageURL,
|
|
HelpText: req.PaymentHelpText,
|
|
CancelRateLimitEnabled: req.PaymentCancelRateLimitEnabled,
|
|
CancelRateLimitMax: req.PaymentCancelRateLimitMax,
|
|
CancelRateLimitWindow: req.PaymentCancelRateLimitWindow,
|
|
CancelRateLimitUnit: req.PaymentCancelRateLimitUnit,
|
|
CancelRateLimitMode: req.PaymentCancelRateLimitMode,
|
|
}
|
|
if err := h.paymentConfigService.UpdatePaymentConfig(c.Request.Context(), paymentReq); err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
// Refresh in-memory provider registry so config changes take effect immediately
|
|
if h.paymentService != nil {
|
|
h.paymentService.RefreshProviders(c.Request.Context())
|
|
}
|
|
}
|
|
|
|
h.auditSettingsUpdate(c, previousSettings, settings, req)
|
|
|
|
// 重新获取设置返回
|
|
updatedSettings, err := h.settingService.GetAllSettings(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
updatedDefaultSubscriptions := make([]dto.DefaultSubscriptionSetting, 0, len(updatedSettings.DefaultSubscriptions))
|
|
for _, sub := range updatedSettings.DefaultSubscriptions {
|
|
updatedDefaultSubscriptions = append(updatedDefaultSubscriptions, dto.DefaultSubscriptionSetting{
|
|
GroupID: sub.GroupID,
|
|
ValidityDays: sub.ValidityDays,
|
|
})
|
|
}
|
|
|
|
// Reload payment config for response
|
|
var updatedPaymentCfg *service.PaymentConfig
|
|
if h.paymentConfigService != nil {
|
|
updatedPaymentCfg, _ = h.paymentConfigService.GetPaymentConfig(c.Request.Context())
|
|
}
|
|
if updatedPaymentCfg == nil {
|
|
updatedPaymentCfg = &service.PaymentConfig{}
|
|
}
|
|
|
|
response.Success(c, dto.SystemSettings{
|
|
RegistrationEnabled: updatedSettings.RegistrationEnabled,
|
|
EmailVerifyEnabled: updatedSettings.EmailVerifyEnabled,
|
|
RegistrationEmailSuffixWhitelist: updatedSettings.RegistrationEmailSuffixWhitelist,
|
|
PromoCodeEnabled: updatedSettings.PromoCodeEnabled,
|
|
PasswordResetEnabled: updatedSettings.PasswordResetEnabled,
|
|
FrontendURL: updatedSettings.FrontendURL,
|
|
InvitationCodeEnabled: updatedSettings.InvitationCodeEnabled,
|
|
TotpEnabled: updatedSettings.TotpEnabled,
|
|
TotpEncryptionKeyConfigured: h.settingService.IsTotpEncryptionKeyConfigured(),
|
|
SMTPHost: updatedSettings.SMTPHost,
|
|
SMTPPort: updatedSettings.SMTPPort,
|
|
SMTPUsername: updatedSettings.SMTPUsername,
|
|
SMTPPasswordConfigured: updatedSettings.SMTPPasswordConfigured,
|
|
SMTPFrom: updatedSettings.SMTPFrom,
|
|
SMTPFromName: updatedSettings.SMTPFromName,
|
|
SMTPUseTLS: updatedSettings.SMTPUseTLS,
|
|
TurnstileEnabled: updatedSettings.TurnstileEnabled,
|
|
TurnstileSiteKey: updatedSettings.TurnstileSiteKey,
|
|
TurnstileSecretKeyConfigured: updatedSettings.TurnstileSecretKeyConfigured,
|
|
LinuxDoConnectEnabled: updatedSettings.LinuxDoConnectEnabled,
|
|
LinuxDoConnectClientID: updatedSettings.LinuxDoConnectClientID,
|
|
LinuxDoConnectClientSecretConfigured: updatedSettings.LinuxDoConnectClientSecretConfigured,
|
|
LinuxDoConnectRedirectURL: updatedSettings.LinuxDoConnectRedirectURL,
|
|
OIDCConnectEnabled: updatedSettings.OIDCConnectEnabled,
|
|
OIDCConnectProviderName: updatedSettings.OIDCConnectProviderName,
|
|
OIDCConnectClientID: updatedSettings.OIDCConnectClientID,
|
|
OIDCConnectClientSecretConfigured: updatedSettings.OIDCConnectClientSecretConfigured,
|
|
OIDCConnectIssuerURL: updatedSettings.OIDCConnectIssuerURL,
|
|
OIDCConnectDiscoveryURL: updatedSettings.OIDCConnectDiscoveryURL,
|
|
OIDCConnectAuthorizeURL: updatedSettings.OIDCConnectAuthorizeURL,
|
|
OIDCConnectTokenURL: updatedSettings.OIDCConnectTokenURL,
|
|
OIDCConnectUserInfoURL: updatedSettings.OIDCConnectUserInfoURL,
|
|
OIDCConnectJWKSURL: updatedSettings.OIDCConnectJWKSURL,
|
|
OIDCConnectScopes: updatedSettings.OIDCConnectScopes,
|
|
OIDCConnectRedirectURL: updatedSettings.OIDCConnectRedirectURL,
|
|
OIDCConnectFrontendRedirectURL: updatedSettings.OIDCConnectFrontendRedirectURL,
|
|
OIDCConnectTokenAuthMethod: updatedSettings.OIDCConnectTokenAuthMethod,
|
|
OIDCConnectUsePKCE: updatedSettings.OIDCConnectUsePKCE,
|
|
OIDCConnectValidateIDToken: updatedSettings.OIDCConnectValidateIDToken,
|
|
OIDCConnectAllowedSigningAlgs: updatedSettings.OIDCConnectAllowedSigningAlgs,
|
|
OIDCConnectClockSkewSeconds: updatedSettings.OIDCConnectClockSkewSeconds,
|
|
OIDCConnectRequireEmailVerified: updatedSettings.OIDCConnectRequireEmailVerified,
|
|
OIDCConnectUserInfoEmailPath: updatedSettings.OIDCConnectUserInfoEmailPath,
|
|
OIDCConnectUserInfoIDPath: updatedSettings.OIDCConnectUserInfoIDPath,
|
|
OIDCConnectUserInfoUsernamePath: updatedSettings.OIDCConnectUserInfoUsernamePath,
|
|
SiteName: updatedSettings.SiteName,
|
|
SiteLogo: updatedSettings.SiteLogo,
|
|
SiteSubtitle: updatedSettings.SiteSubtitle,
|
|
APIBaseURL: updatedSettings.APIBaseURL,
|
|
ContactInfo: updatedSettings.ContactInfo,
|
|
DocURL: updatedSettings.DocURL,
|
|
HomeContent: updatedSettings.HomeContent,
|
|
HideCcsImportButton: updatedSettings.HideCcsImportButton,
|
|
PurchaseSubscriptionEnabled: updatedSettings.PurchaseSubscriptionEnabled,
|
|
PurchaseSubscriptionURL: updatedSettings.PurchaseSubscriptionURL,
|
|
TableDefaultPageSize: updatedSettings.TableDefaultPageSize,
|
|
TablePageSizeOptions: updatedSettings.TablePageSizeOptions,
|
|
CustomMenuItems: dto.ParseCustomMenuItems(updatedSettings.CustomMenuItems),
|
|
CustomEndpoints: dto.ParseCustomEndpoints(updatedSettings.CustomEndpoints),
|
|
DefaultConcurrency: updatedSettings.DefaultConcurrency,
|
|
DefaultBalance: updatedSettings.DefaultBalance,
|
|
DefaultSubscriptions: updatedDefaultSubscriptions,
|
|
EnableModelFallback: updatedSettings.EnableModelFallback,
|
|
FallbackModelAnthropic: updatedSettings.FallbackModelAnthropic,
|
|
FallbackModelOpenAI: updatedSettings.FallbackModelOpenAI,
|
|
FallbackModelGemini: updatedSettings.FallbackModelGemini,
|
|
FallbackModelAntigravity: updatedSettings.FallbackModelAntigravity,
|
|
EnableIdentityPatch: updatedSettings.EnableIdentityPatch,
|
|
IdentityPatchPrompt: updatedSettings.IdentityPatchPrompt,
|
|
OpsMonitoringEnabled: updatedSettings.OpsMonitoringEnabled,
|
|
OpsRealtimeMonitoringEnabled: updatedSettings.OpsRealtimeMonitoringEnabled,
|
|
OpsQueryModeDefault: updatedSettings.OpsQueryModeDefault,
|
|
OpsMetricsIntervalSeconds: updatedSettings.OpsMetricsIntervalSeconds,
|
|
MinClaudeCodeVersion: updatedSettings.MinClaudeCodeVersion,
|
|
MaxClaudeCodeVersion: updatedSettings.MaxClaudeCodeVersion,
|
|
AllowUngroupedKeyScheduling: updatedSettings.AllowUngroupedKeyScheduling,
|
|
BackendModeEnabled: updatedSettings.BackendModeEnabled,
|
|
EnableFingerprintUnification: updatedSettings.EnableFingerprintUnification,
|
|
EnableMetadataPassthrough: updatedSettings.EnableMetadataPassthrough,
|
|
EnableCCHSigning: updatedSettings.EnableCCHSigning,
|
|
BalanceLowNotifyEnabled: updatedSettings.BalanceLowNotifyEnabled,
|
|
BalanceLowNotifyThreshold: updatedSettings.BalanceLowNotifyThreshold,
|
|
AccountQuotaNotifyEnabled: updatedSettings.AccountQuotaNotifyEnabled,
|
|
AccountQuotaNotifyEmails: dto.NotifyEmailEntriesFromService(updatedSettings.AccountQuotaNotifyEmails),
|
|
PaymentEnabled: updatedPaymentCfg.Enabled,
|
|
PaymentMinAmount: updatedPaymentCfg.MinAmount,
|
|
PaymentMaxAmount: updatedPaymentCfg.MaxAmount,
|
|
PaymentDailyLimit: updatedPaymentCfg.DailyLimit,
|
|
PaymentOrderTimeoutMin: updatedPaymentCfg.OrderTimeoutMin,
|
|
PaymentMaxPendingOrders: updatedPaymentCfg.MaxPendingOrders,
|
|
PaymentEnabledTypes: updatedPaymentCfg.EnabledTypes,
|
|
PaymentBalanceDisabled: updatedPaymentCfg.BalanceDisabled,
|
|
PaymentLoadBalanceStrat: updatedPaymentCfg.LoadBalanceStrategy,
|
|
PaymentProductNamePrefix: updatedPaymentCfg.ProductNamePrefix,
|
|
PaymentProductNameSuffix: updatedPaymentCfg.ProductNameSuffix,
|
|
PaymentHelpImageURL: updatedPaymentCfg.HelpImageURL,
|
|
PaymentHelpText: updatedPaymentCfg.HelpText,
|
|
PaymentCancelRateLimitEnabled: updatedPaymentCfg.CancelRateLimitEnabled,
|
|
PaymentCancelRateLimitMax: updatedPaymentCfg.CancelRateLimitMax,
|
|
PaymentCancelRateLimitWindow: updatedPaymentCfg.CancelRateLimitWindow,
|
|
PaymentCancelRateLimitUnit: updatedPaymentCfg.CancelRateLimitUnit,
|
|
PaymentCancelRateLimitMode: updatedPaymentCfg.CancelRateLimitMode,
|
|
})
|
|
}
|
|
|
|
// hasPaymentFields returns true if any payment-related field was explicitly provided.
|
|
func hasPaymentFields(req UpdateSettingsRequest) bool {
|
|
return req.PaymentEnabled != nil || req.PaymentMinAmount != nil ||
|
|
req.PaymentMaxAmount != nil || req.PaymentDailyLimit != nil ||
|
|
req.PaymentOrderTimeoutMin != nil || req.PaymentMaxPendingOrders != nil ||
|
|
req.PaymentEnabledTypes != nil || req.PaymentBalanceDisabled != nil ||
|
|
req.PaymentLoadBalanceStrat != nil || req.PaymentProductNamePrefix != nil ||
|
|
req.PaymentProductNameSuffix != nil || req.PaymentHelpImageURL != nil ||
|
|
req.PaymentHelpText != nil || req.PaymentCancelRateLimitEnabled != nil ||
|
|
req.PaymentCancelRateLimitMax != nil || req.PaymentCancelRateLimitWindow != nil ||
|
|
req.PaymentCancelRateLimitUnit != nil || req.PaymentCancelRateLimitMode != nil
|
|
}
|
|
|
|
func (h *SettingHandler) auditSettingsUpdate(c *gin.Context, before *service.SystemSettings, after *service.SystemSettings, req UpdateSettingsRequest) {
|
|
if before == nil || after == nil {
|
|
return
|
|
}
|
|
|
|
changed := diffSettings(before, after, req)
|
|
if len(changed) == 0 {
|
|
return
|
|
}
|
|
|
|
subject, _ := middleware.GetAuthSubjectFromContext(c)
|
|
role, _ := middleware.GetUserRoleFromContext(c)
|
|
slog.Info("settings updated",
|
|
"audit", true,
|
|
"user_id", subject.UserID,
|
|
"role", role,
|
|
"changed", changed,
|
|
)
|
|
}
|
|
|
|
func diffSettings(before *service.SystemSettings, after *service.SystemSettings, req UpdateSettingsRequest) []string {
|
|
changed := make([]string, 0, 20)
|
|
if before.RegistrationEnabled != after.RegistrationEnabled {
|
|
changed = append(changed, "registration_enabled")
|
|
}
|
|
if before.EmailVerifyEnabled != after.EmailVerifyEnabled {
|
|
changed = append(changed, "email_verify_enabled")
|
|
}
|
|
if !equalStringSlice(before.RegistrationEmailSuffixWhitelist, after.RegistrationEmailSuffixWhitelist) {
|
|
changed = append(changed, "registration_email_suffix_whitelist")
|
|
}
|
|
if before.PromoCodeEnabled != after.PromoCodeEnabled {
|
|
changed = append(changed, "promo_code_enabled")
|
|
}
|
|
if before.InvitationCodeEnabled != after.InvitationCodeEnabled {
|
|
changed = append(changed, "invitation_code_enabled")
|
|
}
|
|
if before.PasswordResetEnabled != after.PasswordResetEnabled {
|
|
changed = append(changed, "password_reset_enabled")
|
|
}
|
|
if before.FrontendURL != after.FrontendURL {
|
|
changed = append(changed, "frontend_url")
|
|
}
|
|
if before.TotpEnabled != after.TotpEnabled {
|
|
changed = append(changed, "totp_enabled")
|
|
}
|
|
if before.SMTPHost != after.SMTPHost {
|
|
changed = append(changed, "smtp_host")
|
|
}
|
|
if before.SMTPPort != after.SMTPPort {
|
|
changed = append(changed, "smtp_port")
|
|
}
|
|
if before.SMTPUsername != after.SMTPUsername {
|
|
changed = append(changed, "smtp_username")
|
|
}
|
|
if req.SMTPPassword != "" {
|
|
changed = append(changed, "smtp_password")
|
|
}
|
|
if before.SMTPFrom != after.SMTPFrom {
|
|
changed = append(changed, "smtp_from_email")
|
|
}
|
|
if before.SMTPFromName != after.SMTPFromName {
|
|
changed = append(changed, "smtp_from_name")
|
|
}
|
|
if before.SMTPUseTLS != after.SMTPUseTLS {
|
|
changed = append(changed, "smtp_use_tls")
|
|
}
|
|
if before.TurnstileEnabled != after.TurnstileEnabled {
|
|
changed = append(changed, "turnstile_enabled")
|
|
}
|
|
if before.TurnstileSiteKey != after.TurnstileSiteKey {
|
|
changed = append(changed, "turnstile_site_key")
|
|
}
|
|
if req.TurnstileSecretKey != "" {
|
|
changed = append(changed, "turnstile_secret_key")
|
|
}
|
|
if before.LinuxDoConnectEnabled != after.LinuxDoConnectEnabled {
|
|
changed = append(changed, "linuxdo_connect_enabled")
|
|
}
|
|
if before.LinuxDoConnectClientID != after.LinuxDoConnectClientID {
|
|
changed = append(changed, "linuxdo_connect_client_id")
|
|
}
|
|
if req.LinuxDoConnectClientSecret != "" {
|
|
changed = append(changed, "linuxdo_connect_client_secret")
|
|
}
|
|
if before.LinuxDoConnectRedirectURL != after.LinuxDoConnectRedirectURL {
|
|
changed = append(changed, "linuxdo_connect_redirect_url")
|
|
}
|
|
if before.OIDCConnectEnabled != after.OIDCConnectEnabled {
|
|
changed = append(changed, "oidc_connect_enabled")
|
|
}
|
|
if before.OIDCConnectProviderName != after.OIDCConnectProviderName {
|
|
changed = append(changed, "oidc_connect_provider_name")
|
|
}
|
|
if before.OIDCConnectClientID != after.OIDCConnectClientID {
|
|
changed = append(changed, "oidc_connect_client_id")
|
|
}
|
|
if req.OIDCConnectClientSecret != "" {
|
|
changed = append(changed, "oidc_connect_client_secret")
|
|
}
|
|
if before.OIDCConnectIssuerURL != after.OIDCConnectIssuerURL {
|
|
changed = append(changed, "oidc_connect_issuer_url")
|
|
}
|
|
if before.OIDCConnectDiscoveryURL != after.OIDCConnectDiscoveryURL {
|
|
changed = append(changed, "oidc_connect_discovery_url")
|
|
}
|
|
if before.OIDCConnectAuthorizeURL != after.OIDCConnectAuthorizeURL {
|
|
changed = append(changed, "oidc_connect_authorize_url")
|
|
}
|
|
if before.OIDCConnectTokenURL != after.OIDCConnectTokenURL {
|
|
changed = append(changed, "oidc_connect_token_url")
|
|
}
|
|
if before.OIDCConnectUserInfoURL != after.OIDCConnectUserInfoURL {
|
|
changed = append(changed, "oidc_connect_userinfo_url")
|
|
}
|
|
if before.OIDCConnectJWKSURL != after.OIDCConnectJWKSURL {
|
|
changed = append(changed, "oidc_connect_jwks_url")
|
|
}
|
|
if before.OIDCConnectScopes != after.OIDCConnectScopes {
|
|
changed = append(changed, "oidc_connect_scopes")
|
|
}
|
|
if before.OIDCConnectRedirectURL != after.OIDCConnectRedirectURL {
|
|
changed = append(changed, "oidc_connect_redirect_url")
|
|
}
|
|
if before.OIDCConnectFrontendRedirectURL != after.OIDCConnectFrontendRedirectURL {
|
|
changed = append(changed, "oidc_connect_frontend_redirect_url")
|
|
}
|
|
if before.OIDCConnectTokenAuthMethod != after.OIDCConnectTokenAuthMethod {
|
|
changed = append(changed, "oidc_connect_token_auth_method")
|
|
}
|
|
if before.OIDCConnectUsePKCE != after.OIDCConnectUsePKCE {
|
|
changed = append(changed, "oidc_connect_use_pkce")
|
|
}
|
|
if before.OIDCConnectValidateIDToken != after.OIDCConnectValidateIDToken {
|
|
changed = append(changed, "oidc_connect_validate_id_token")
|
|
}
|
|
if before.OIDCConnectAllowedSigningAlgs != after.OIDCConnectAllowedSigningAlgs {
|
|
changed = append(changed, "oidc_connect_allowed_signing_algs")
|
|
}
|
|
if before.OIDCConnectClockSkewSeconds != after.OIDCConnectClockSkewSeconds {
|
|
changed = append(changed, "oidc_connect_clock_skew_seconds")
|
|
}
|
|
if before.OIDCConnectRequireEmailVerified != after.OIDCConnectRequireEmailVerified {
|
|
changed = append(changed, "oidc_connect_require_email_verified")
|
|
}
|
|
if before.OIDCConnectUserInfoEmailPath != after.OIDCConnectUserInfoEmailPath {
|
|
changed = append(changed, "oidc_connect_userinfo_email_path")
|
|
}
|
|
if before.OIDCConnectUserInfoIDPath != after.OIDCConnectUserInfoIDPath {
|
|
changed = append(changed, "oidc_connect_userinfo_id_path")
|
|
}
|
|
if before.OIDCConnectUserInfoUsernamePath != after.OIDCConnectUserInfoUsernamePath {
|
|
changed = append(changed, "oidc_connect_userinfo_username_path")
|
|
}
|
|
if before.SiteName != after.SiteName {
|
|
changed = append(changed, "site_name")
|
|
}
|
|
if before.SiteLogo != after.SiteLogo {
|
|
changed = append(changed, "site_logo")
|
|
}
|
|
if before.SiteSubtitle != after.SiteSubtitle {
|
|
changed = append(changed, "site_subtitle")
|
|
}
|
|
if before.APIBaseURL != after.APIBaseURL {
|
|
changed = append(changed, "api_base_url")
|
|
}
|
|
if before.ContactInfo != after.ContactInfo {
|
|
changed = append(changed, "contact_info")
|
|
}
|
|
if before.DocURL != after.DocURL {
|
|
changed = append(changed, "doc_url")
|
|
}
|
|
if before.HomeContent != after.HomeContent {
|
|
changed = append(changed, "home_content")
|
|
}
|
|
if before.HideCcsImportButton != after.HideCcsImportButton {
|
|
changed = append(changed, "hide_ccs_import_button")
|
|
}
|
|
if before.DefaultConcurrency != after.DefaultConcurrency {
|
|
changed = append(changed, "default_concurrency")
|
|
}
|
|
if before.DefaultBalance != after.DefaultBalance {
|
|
changed = append(changed, "default_balance")
|
|
}
|
|
if !equalDefaultSubscriptions(before.DefaultSubscriptions, after.DefaultSubscriptions) {
|
|
changed = append(changed, "default_subscriptions")
|
|
}
|
|
if before.EnableModelFallback != after.EnableModelFallback {
|
|
changed = append(changed, "enable_model_fallback")
|
|
}
|
|
if before.FallbackModelAnthropic != after.FallbackModelAnthropic {
|
|
changed = append(changed, "fallback_model_anthropic")
|
|
}
|
|
if before.FallbackModelOpenAI != after.FallbackModelOpenAI {
|
|
changed = append(changed, "fallback_model_openai")
|
|
}
|
|
if before.FallbackModelGemini != after.FallbackModelGemini {
|
|
changed = append(changed, "fallback_model_gemini")
|
|
}
|
|
if before.FallbackModelAntigravity != after.FallbackModelAntigravity {
|
|
changed = append(changed, "fallback_model_antigravity")
|
|
}
|
|
if before.EnableIdentityPatch != after.EnableIdentityPatch {
|
|
changed = append(changed, "enable_identity_patch")
|
|
}
|
|
if before.IdentityPatchPrompt != after.IdentityPatchPrompt {
|
|
changed = append(changed, "identity_patch_prompt")
|
|
}
|
|
if before.OpsMonitoringEnabled != after.OpsMonitoringEnabled {
|
|
changed = append(changed, "ops_monitoring_enabled")
|
|
}
|
|
if before.OpsRealtimeMonitoringEnabled != after.OpsRealtimeMonitoringEnabled {
|
|
changed = append(changed, "ops_realtime_monitoring_enabled")
|
|
}
|
|
if before.OpsQueryModeDefault != after.OpsQueryModeDefault {
|
|
changed = append(changed, "ops_query_mode_default")
|
|
}
|
|
if before.OpsMetricsIntervalSeconds != after.OpsMetricsIntervalSeconds {
|
|
changed = append(changed, "ops_metrics_interval_seconds")
|
|
}
|
|
if before.MinClaudeCodeVersion != after.MinClaudeCodeVersion {
|
|
changed = append(changed, "min_claude_code_version")
|
|
}
|
|
if before.MaxClaudeCodeVersion != after.MaxClaudeCodeVersion {
|
|
changed = append(changed, "max_claude_code_version")
|
|
}
|
|
if before.AllowUngroupedKeyScheduling != after.AllowUngroupedKeyScheduling {
|
|
changed = append(changed, "allow_ungrouped_key_scheduling")
|
|
}
|
|
if before.BackendModeEnabled != after.BackendModeEnabled {
|
|
changed = append(changed, "backend_mode_enabled")
|
|
}
|
|
if before.PurchaseSubscriptionEnabled != after.PurchaseSubscriptionEnabled {
|
|
changed = append(changed, "purchase_subscription_enabled")
|
|
}
|
|
if before.PurchaseSubscriptionURL != after.PurchaseSubscriptionURL {
|
|
changed = append(changed, "purchase_subscription_url")
|
|
}
|
|
if before.TableDefaultPageSize != after.TableDefaultPageSize {
|
|
changed = append(changed, "table_default_page_size")
|
|
}
|
|
if !equalIntSlice(before.TablePageSizeOptions, after.TablePageSizeOptions) {
|
|
changed = append(changed, "table_page_size_options")
|
|
}
|
|
if before.CustomMenuItems != after.CustomMenuItems {
|
|
changed = append(changed, "custom_menu_items")
|
|
}
|
|
if before.CustomEndpoints != after.CustomEndpoints {
|
|
changed = append(changed, "custom_endpoints")
|
|
}
|
|
if before.EnableFingerprintUnification != after.EnableFingerprintUnification {
|
|
changed = append(changed, "enable_fingerprint_unification")
|
|
}
|
|
if before.EnableMetadataPassthrough != after.EnableMetadataPassthrough {
|
|
changed = append(changed, "enable_metadata_passthrough")
|
|
}
|
|
if before.EnableCCHSigning != after.EnableCCHSigning {
|
|
changed = append(changed, "enable_cch_signing")
|
|
}
|
|
// Balance & quota notification
|
|
if before.BalanceLowNotifyEnabled != after.BalanceLowNotifyEnabled {
|
|
changed = append(changed, "balance_low_notify_enabled")
|
|
}
|
|
if before.BalanceLowNotifyThreshold != after.BalanceLowNotifyThreshold {
|
|
changed = append(changed, "balance_low_notify_threshold")
|
|
}
|
|
if before.BalanceLowNotifyRechargeURL != after.BalanceLowNotifyRechargeURL {
|
|
changed = append(changed, "balance_low_notify_recharge_url")
|
|
}
|
|
if before.AccountQuotaNotifyEnabled != after.AccountQuotaNotifyEnabled {
|
|
changed = append(changed, "account_quota_notify_enabled")
|
|
}
|
|
if !equalNotifyEmailEntries(before.AccountQuotaNotifyEmails, after.AccountQuotaNotifyEmails) {
|
|
changed = append(changed, "account_quota_notify_emails")
|
|
}
|
|
return changed
|
|
}
|
|
|
|
func normalizeDefaultSubscriptions(input []dto.DefaultSubscriptionSetting) []dto.DefaultSubscriptionSetting {
|
|
if len(input) == 0 {
|
|
return nil
|
|
}
|
|
normalized := make([]dto.DefaultSubscriptionSetting, 0, len(input))
|
|
for _, item := range input {
|
|
if item.GroupID <= 0 || item.ValidityDays <= 0 {
|
|
continue
|
|
}
|
|
if item.ValidityDays > service.MaxValidityDays {
|
|
item.ValidityDays = service.MaxValidityDays
|
|
}
|
|
normalized = append(normalized, item)
|
|
}
|
|
return normalized
|
|
}
|
|
|
|
func equalStringSlice(a, b []string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func equalDefaultSubscriptions(a, b []service.DefaultSubscriptionSetting) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i].GroupID != b[i].GroupID || a[i].ValidityDays != b[i].ValidityDays {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func equalIntSlice(a, b []int) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func equalNotifyEmailEntries(a, b []service.NotifyEmailEntry) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i].Email != b[i].Email || a[i].Verified != b[i].Verified || a[i].Disabled != b[i].Disabled {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// TestSMTPRequest 测试SMTP连接请求
|
|
type TestSMTPRequest struct {
|
|
SMTPHost string `json:"smtp_host"`
|
|
SMTPPort int `json:"smtp_port"`
|
|
SMTPUsername string `json:"smtp_username"`
|
|
SMTPPassword string `json:"smtp_password"`
|
|
SMTPUseTLS bool `json:"smtp_use_tls"`
|
|
}
|
|
|
|
// TestSMTPConnection 测试SMTP连接
|
|
// POST /api/v1/admin/settings/test-smtp
|
|
func (h *SettingHandler) TestSMTPConnection(c *gin.Context) {
|
|
var req TestSMTPRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
|
|
req.SMTPHost = strings.TrimSpace(req.SMTPHost)
|
|
req.SMTPUsername = strings.TrimSpace(req.SMTPUsername)
|
|
|
|
var savedConfig *service.SMTPConfig
|
|
if cfg, err := h.emailService.GetSMTPConfig(c.Request.Context()); err == nil && cfg != nil {
|
|
savedConfig = cfg
|
|
}
|
|
|
|
if req.SMTPHost == "" && savedConfig != nil {
|
|
req.SMTPHost = savedConfig.Host
|
|
}
|
|
if req.SMTPPort <= 0 {
|
|
if savedConfig != nil && savedConfig.Port > 0 {
|
|
req.SMTPPort = savedConfig.Port
|
|
} else {
|
|
req.SMTPPort = 587
|
|
}
|
|
}
|
|
if req.SMTPUsername == "" && savedConfig != nil {
|
|
req.SMTPUsername = savedConfig.Username
|
|
}
|
|
password := strings.TrimSpace(req.SMTPPassword)
|
|
if password == "" && savedConfig != nil {
|
|
password = savedConfig.Password
|
|
}
|
|
if req.SMTPHost == "" {
|
|
response.BadRequest(c, "SMTP host is required")
|
|
return
|
|
}
|
|
|
|
config := &service.SMTPConfig{
|
|
Host: req.SMTPHost,
|
|
Port: req.SMTPPort,
|
|
Username: req.SMTPUsername,
|
|
Password: password,
|
|
UseTLS: req.SMTPUseTLS,
|
|
}
|
|
|
|
err := h.emailService.TestSMTPConnectionWithConfig(config)
|
|
if err != nil {
|
|
response.BadRequest(c, "SMTP connection test failed: "+err.Error())
|
|
return
|
|
}
|
|
|
|
response.Success(c, gin.H{"message": "SMTP connection successful"})
|
|
}
|
|
|
|
// SendTestEmailRequest 发送测试邮件请求
|
|
type SendTestEmailRequest struct {
|
|
Email string `json:"email" binding:"required,email"`
|
|
SMTPHost string `json:"smtp_host"`
|
|
SMTPPort int `json:"smtp_port"`
|
|
SMTPUsername string `json:"smtp_username"`
|
|
SMTPPassword string `json:"smtp_password"`
|
|
SMTPFrom string `json:"smtp_from_email"`
|
|
SMTPFromName string `json:"smtp_from_name"`
|
|
SMTPUseTLS bool `json:"smtp_use_tls"`
|
|
}
|
|
|
|
// SendTestEmail 发送测试邮件
|
|
// POST /api/v1/admin/settings/send-test-email
|
|
func (h *SettingHandler) SendTestEmail(c *gin.Context) {
|
|
var req SendTestEmailRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
|
|
req.SMTPHost = strings.TrimSpace(req.SMTPHost)
|
|
req.SMTPUsername = strings.TrimSpace(req.SMTPUsername)
|
|
req.SMTPFrom = strings.TrimSpace(req.SMTPFrom)
|
|
req.SMTPFromName = strings.TrimSpace(req.SMTPFromName)
|
|
|
|
var savedConfig *service.SMTPConfig
|
|
if cfg, err := h.emailService.GetSMTPConfig(c.Request.Context()); err == nil && cfg != nil {
|
|
savedConfig = cfg
|
|
}
|
|
|
|
if req.SMTPHost == "" && savedConfig != nil {
|
|
req.SMTPHost = savedConfig.Host
|
|
}
|
|
if req.SMTPPort <= 0 {
|
|
if savedConfig != nil && savedConfig.Port > 0 {
|
|
req.SMTPPort = savedConfig.Port
|
|
} else {
|
|
req.SMTPPort = 587
|
|
}
|
|
}
|
|
if req.SMTPUsername == "" && savedConfig != nil {
|
|
req.SMTPUsername = savedConfig.Username
|
|
}
|
|
password := strings.TrimSpace(req.SMTPPassword)
|
|
if password == "" && savedConfig != nil {
|
|
password = savedConfig.Password
|
|
}
|
|
if req.SMTPFrom == "" && savedConfig != nil {
|
|
req.SMTPFrom = savedConfig.From
|
|
}
|
|
if req.SMTPFromName == "" && savedConfig != nil {
|
|
req.SMTPFromName = savedConfig.FromName
|
|
}
|
|
if req.SMTPHost == "" {
|
|
response.BadRequest(c, "SMTP host is required")
|
|
return
|
|
}
|
|
|
|
config := &service.SMTPConfig{
|
|
Host: req.SMTPHost,
|
|
Port: req.SMTPPort,
|
|
Username: req.SMTPUsername,
|
|
Password: password,
|
|
From: req.SMTPFrom,
|
|
FromName: req.SMTPFromName,
|
|
UseTLS: req.SMTPUseTLS,
|
|
}
|
|
|
|
siteName := h.settingService.GetSiteName(c.Request.Context())
|
|
subject := "[" + siteName + "] Test Email"
|
|
body := `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<style>
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background-color: #f5f5f5; margin: 0; padding: 20px; }
|
|
.container { max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
|
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; }
|
|
.content { padding: 40px 30px; text-align: center; }
|
|
.success { color: #10b981; font-size: 48px; margin-bottom: 20px; }
|
|
.footer { background-color: #f8f9fa; padding: 20px; text-align: center; color: #999; font-size: 12px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>` + siteName + `</h1>
|
|
</div>
|
|
<div class="content">
|
|
<div class="success">✓</div>
|
|
<h2>Email Configuration Successful!</h2>
|
|
<p>This is a test email to verify your SMTP settings are working correctly.</p>
|
|
</div>
|
|
<div class="footer">
|
|
<p>This is an automated test message.</p>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`
|
|
|
|
if err := h.emailService.SendEmailWithConfig(config, req.Email, subject, body); err != nil {
|
|
response.BadRequest(c, "Failed to send test email: "+err.Error())
|
|
return
|
|
}
|
|
|
|
response.Success(c, gin.H{"message": "Test email sent successfully"})
|
|
}
|
|
|
|
// GetAdminAPIKey 获取管理员 API Key 状态
|
|
// GET /api/v1/admin/settings/admin-api-key
|
|
func (h *SettingHandler) GetAdminAPIKey(c *gin.Context) {
|
|
maskedKey, exists, err := h.settingService.GetAdminAPIKeyStatus(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, gin.H{
|
|
"exists": exists,
|
|
"masked_key": maskedKey,
|
|
})
|
|
}
|
|
|
|
// RegenerateAdminAPIKey 生成/重新生成管理员 API Key
|
|
// POST /api/v1/admin/settings/admin-api-key/regenerate
|
|
func (h *SettingHandler) RegenerateAdminAPIKey(c *gin.Context) {
|
|
key, err := h.settingService.GenerateAdminAPIKey(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, gin.H{
|
|
"key": key, // 完整 key 只在生成时返回一次
|
|
})
|
|
}
|
|
|
|
// DeleteAdminAPIKey 删除管理员 API Key
|
|
// DELETE /api/v1/admin/settings/admin-api-key
|
|
func (h *SettingHandler) DeleteAdminAPIKey(c *gin.Context) {
|
|
if err := h.settingService.DeleteAdminAPIKey(c.Request.Context()); err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, gin.H{"message": "Admin API key deleted"})
|
|
}
|
|
|
|
// GetOverloadCooldownSettings 获取529过载冷却配置
|
|
// GET /api/v1/admin/settings/overload-cooldown
|
|
func (h *SettingHandler) GetOverloadCooldownSettings(c *gin.Context) {
|
|
settings, err := h.settingService.GetOverloadCooldownSettings(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, dto.OverloadCooldownSettings{
|
|
Enabled: settings.Enabled,
|
|
CooldownMinutes: settings.CooldownMinutes,
|
|
})
|
|
}
|
|
|
|
// UpdateOverloadCooldownSettingsRequest 更新529过载冷却配置请求
|
|
type UpdateOverloadCooldownSettingsRequest struct {
|
|
Enabled bool `json:"enabled"`
|
|
CooldownMinutes int `json:"cooldown_minutes"`
|
|
}
|
|
|
|
// UpdateOverloadCooldownSettings 更新529过载冷却配置
|
|
// PUT /api/v1/admin/settings/overload-cooldown
|
|
func (h *SettingHandler) UpdateOverloadCooldownSettings(c *gin.Context) {
|
|
var req UpdateOverloadCooldownSettingsRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
|
|
settings := &service.OverloadCooldownSettings{
|
|
Enabled: req.Enabled,
|
|
CooldownMinutes: req.CooldownMinutes,
|
|
}
|
|
|
|
if err := h.settingService.SetOverloadCooldownSettings(c.Request.Context(), settings); err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
updatedSettings, err := h.settingService.GetOverloadCooldownSettings(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, dto.OverloadCooldownSettings{
|
|
Enabled: updatedSettings.Enabled,
|
|
CooldownMinutes: updatedSettings.CooldownMinutes,
|
|
})
|
|
}
|
|
|
|
// GetStreamTimeoutSettings 获取流超时处理配置
|
|
// GET /api/v1/admin/settings/stream-timeout
|
|
func (h *SettingHandler) GetStreamTimeoutSettings(c *gin.Context) {
|
|
settings, err := h.settingService.GetStreamTimeoutSettings(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, dto.StreamTimeoutSettings{
|
|
Enabled: settings.Enabled,
|
|
Action: settings.Action,
|
|
TempUnschedMinutes: settings.TempUnschedMinutes,
|
|
ThresholdCount: settings.ThresholdCount,
|
|
ThresholdWindowMinutes: settings.ThresholdWindowMinutes,
|
|
})
|
|
}
|
|
|
|
// GetRectifierSettings 获取请求整流器配置
|
|
// GET /api/v1/admin/settings/rectifier
|
|
func (h *SettingHandler) GetRectifierSettings(c *gin.Context) {
|
|
settings, err := h.settingService.GetRectifierSettings(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
patterns := settings.APIKeySignaturePatterns
|
|
if patterns == nil {
|
|
patterns = []string{}
|
|
}
|
|
response.Success(c, dto.RectifierSettings{
|
|
Enabled: settings.Enabled,
|
|
ThinkingSignatureEnabled: settings.ThinkingSignatureEnabled,
|
|
ThinkingBudgetEnabled: settings.ThinkingBudgetEnabled,
|
|
APIKeySignatureEnabled: settings.APIKeySignatureEnabled,
|
|
APIKeySignaturePatterns: patterns,
|
|
})
|
|
}
|
|
|
|
// UpdateRectifierSettingsRequest 更新整流器配置请求
|
|
type UpdateRectifierSettingsRequest struct {
|
|
Enabled bool `json:"enabled"`
|
|
ThinkingSignatureEnabled bool `json:"thinking_signature_enabled"`
|
|
ThinkingBudgetEnabled bool `json:"thinking_budget_enabled"`
|
|
APIKeySignatureEnabled bool `json:"apikey_signature_enabled"`
|
|
APIKeySignaturePatterns []string `json:"apikey_signature_patterns"`
|
|
}
|
|
|
|
// UpdateRectifierSettings 更新请求整流器配置
|
|
// PUT /api/v1/admin/settings/rectifier
|
|
func (h *SettingHandler) UpdateRectifierSettings(c *gin.Context) {
|
|
var req UpdateRectifierSettingsRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// 校验并清理自定义匹配关键词
|
|
const maxPatterns = 50
|
|
const maxPatternLen = 500
|
|
if len(req.APIKeySignaturePatterns) > maxPatterns {
|
|
response.BadRequest(c, "Too many signature patterns (max 50)")
|
|
return
|
|
}
|
|
var cleanedPatterns []string
|
|
for _, p := range req.APIKeySignaturePatterns {
|
|
p = strings.TrimSpace(p)
|
|
if p == "" {
|
|
continue
|
|
}
|
|
if len(p) > maxPatternLen {
|
|
response.BadRequest(c, "Signature pattern too long (max 500 characters)")
|
|
return
|
|
}
|
|
cleanedPatterns = append(cleanedPatterns, p)
|
|
}
|
|
|
|
settings := &service.RectifierSettings{
|
|
Enabled: req.Enabled,
|
|
ThinkingSignatureEnabled: req.ThinkingSignatureEnabled,
|
|
ThinkingBudgetEnabled: req.ThinkingBudgetEnabled,
|
|
APIKeySignatureEnabled: req.APIKeySignatureEnabled,
|
|
APIKeySignaturePatterns: cleanedPatterns,
|
|
}
|
|
|
|
if err := h.settingService.SetRectifierSettings(c.Request.Context(), settings); err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
// 重新获取设置返回
|
|
updatedSettings, err := h.settingService.GetRectifierSettings(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
updatedPatterns := updatedSettings.APIKeySignaturePatterns
|
|
if updatedPatterns == nil {
|
|
updatedPatterns = []string{}
|
|
}
|
|
response.Success(c, dto.RectifierSettings{
|
|
Enabled: updatedSettings.Enabled,
|
|
ThinkingSignatureEnabled: updatedSettings.ThinkingSignatureEnabled,
|
|
ThinkingBudgetEnabled: updatedSettings.ThinkingBudgetEnabled,
|
|
APIKeySignatureEnabled: updatedSettings.APIKeySignatureEnabled,
|
|
APIKeySignaturePatterns: updatedPatterns,
|
|
})
|
|
}
|
|
|
|
// GetBetaPolicySettings 获取 Beta 策略配置
|
|
// GET /api/v1/admin/settings/beta-policy
|
|
func (h *SettingHandler) GetBetaPolicySettings(c *gin.Context) {
|
|
settings, err := h.settingService.GetBetaPolicySettings(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
rules := make([]dto.BetaPolicyRule, len(settings.Rules))
|
|
for i, r := range settings.Rules {
|
|
rules[i] = dto.BetaPolicyRule(r)
|
|
}
|
|
response.Success(c, dto.BetaPolicySettings{Rules: rules})
|
|
}
|
|
|
|
// UpdateBetaPolicySettingsRequest 更新 Beta 策略配置请求
|
|
type UpdateBetaPolicySettingsRequest struct {
|
|
Rules []dto.BetaPolicyRule `json:"rules"`
|
|
}
|
|
|
|
// UpdateBetaPolicySettings 更新 Beta 策略配置
|
|
// PUT /api/v1/admin/settings/beta-policy
|
|
func (h *SettingHandler) UpdateBetaPolicySettings(c *gin.Context) {
|
|
var req UpdateBetaPolicySettingsRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
|
|
rules := make([]service.BetaPolicyRule, len(req.Rules))
|
|
for i, r := range req.Rules {
|
|
rules[i] = service.BetaPolicyRule(r)
|
|
}
|
|
|
|
settings := &service.BetaPolicySettings{Rules: rules}
|
|
if err := h.settingService.SetBetaPolicySettings(c.Request.Context(), settings); err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
// Re-fetch to return updated settings
|
|
updated, err := h.settingService.GetBetaPolicySettings(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
outRules := make([]dto.BetaPolicyRule, len(updated.Rules))
|
|
for i, r := range updated.Rules {
|
|
outRules[i] = dto.BetaPolicyRule(r)
|
|
}
|
|
response.Success(c, dto.BetaPolicySettings{Rules: outRules})
|
|
}
|
|
|
|
// UpdateStreamTimeoutSettingsRequest 更新流超时配置请求
|
|
type UpdateStreamTimeoutSettingsRequest struct {
|
|
Enabled bool `json:"enabled"`
|
|
Action string `json:"action"`
|
|
TempUnschedMinutes int `json:"temp_unsched_minutes"`
|
|
ThresholdCount int `json:"threshold_count"`
|
|
ThresholdWindowMinutes int `json:"threshold_window_minutes"`
|
|
}
|
|
|
|
// UpdateStreamTimeoutSettings 更新流超时处理配置
|
|
// PUT /api/v1/admin/settings/stream-timeout
|
|
func (h *SettingHandler) UpdateStreamTimeoutSettings(c *gin.Context) {
|
|
var req UpdateStreamTimeoutSettingsRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
|
|
settings := &service.StreamTimeoutSettings{
|
|
Enabled: req.Enabled,
|
|
Action: req.Action,
|
|
TempUnschedMinutes: req.TempUnschedMinutes,
|
|
ThresholdCount: req.ThresholdCount,
|
|
ThresholdWindowMinutes: req.ThresholdWindowMinutes,
|
|
}
|
|
|
|
if err := h.settingService.SetStreamTimeoutSettings(c.Request.Context(), settings); err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
// 重新获取设置返回
|
|
updatedSettings, err := h.settingService.GetStreamTimeoutSettings(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, dto.StreamTimeoutSettings{
|
|
Enabled: updatedSettings.Enabled,
|
|
Action: updatedSettings.Action,
|
|
TempUnschedMinutes: updatedSettings.TempUnschedMinutes,
|
|
ThresholdCount: updatedSettings.ThresholdCount,
|
|
ThresholdWindowMinutes: updatedSettings.ThresholdWindowMinutes,
|
|
})
|
|
}
|
|
|
|
// GetWebSearchEmulationConfig 获取 Web Search 模拟配置
|
|
// GET /api/v1/admin/settings/web-search-emulation
|
|
func (h *SettingHandler) GetWebSearchEmulationConfig(c *gin.Context) {
|
|
cfg, err := h.settingService.GetWebSearchEmulationConfig(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
response.Success(c, service.PopulateWebSearchUsage(c.Request.Context(), cfg))
|
|
}
|
|
|
|
// UpdateWebSearchEmulationConfig 更新 Web Search 模拟配置
|
|
// PUT /api/v1/admin/settings/web-search-emulation
|
|
func (h *SettingHandler) UpdateWebSearchEmulationConfig(c *gin.Context) {
|
|
var cfg service.WebSearchEmulationConfig
|
|
if err := c.ShouldBindJSON(&cfg); err != nil {
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
|
|
if err := h.settingService.SaveWebSearchEmulationConfig(c.Request.Context(), &cfg); err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
// Re-read (with sanitized api keys) to return current state
|
|
updated, err := h.settingService.GetWebSearchEmulationConfig(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
response.Success(c, service.SanitizeWebSearchConfig(c.Request.Context(), updated))
|
|
}
|
|
|
|
// TestWebSearchEmulation 测试 Web Search 搜索
|
|
// POST /api/v1/admin/settings/web-search-emulation/test
|
|
func (h *SettingHandler) TestWebSearchEmulation(c *gin.Context) {
|
|
var req struct {
|
|
Query string `json:"query"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
if strings.TrimSpace(req.Query) == "" {
|
|
req.Query = "搜索今年世界大事件"
|
|
}
|
|
|
|
result, err := service.TestWebSearch(c.Request.Context(), req.Query)
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
response.Success(c, result)
|
|
}
|