| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491 |
- package admin
- import (
- "context"
- "encoding/json"
- "errors"
- "log/slog"
- "net/http"
- "reflect"
- "strconv"
- "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"
- "github.com/gin-gonic/gin/binding"
- )
- // UpdateSettingsRequest 更新设置请求
- type UpdateSettingsRequest struct {
- // 注册设置
- RegistrationEnabled bool `json:"registration_enabled"`
- EmailVerifyEnabled bool `json:"email_verify_enabled"`
- RegistrationEmailSuffixWhitelist []string `json:"registration_email_suffix_whitelist"`
- RegistrationEmailDomainQuotaEnabled *bool `json:"registration_email_domain_quota_enabled"` // 非白名单域名限量注册开关(省略=保持现值)
- 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 双因素认证
- PasskeyEnabled *bool `json:"passkey_enabled"` // Passkey 登录(省略=保持现值)
- SessionBindingEnabled *bool `json:"session_binding_enabled"` // 会话 IP/UA 绑定(省略=保持现值)
- StepUpEnabled *bool `json:"step_up_enabled"` // 敏感操作 step-up 2FA(省略=保持现值)
- AuditLogRetentionDays int `json:"audit_log_retention_days"` // 审计日志保留天数
- LoginAgreementEnabled bool `json:"login_agreement_enabled"`
- LoginAgreementMode string `json:"login_agreement_mode"`
- LoginAgreementUpdatedAt string `json:"login_agreement_updated_at"`
- LoginAgreementDocuments []dto.LoginAgreementDocument `json:"login_agreement_documents"`
- // 邮件服务设置
- 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"`
- // 腾讯天御验证码设置
- TencentCaptchaEnabled bool `json:"tencent_captcha_enabled"`
- TencentCaptchaAppID string `json:"tencent_captcha_app_id"`
- TencentCaptchaAppSecretKey string `json:"tencent_captcha_app_secret_key"`
- TencentCaptchaCloudSecretID string `json:"tencent_captcha_cloud_secret_id"`
- TencentCaptchaCloudSecretKey string `json:"tencent_captcha_cloud_secret_key"`
- TencentCaptchaRegion string `json:"tencent_captcha_region"`
- // 阿里云验证码 2.0 设置
- AliyunCaptchaEnabled bool `json:"aliyun_captcha_enabled"`
- AliyunCaptchaAccessKeyID string `json:"aliyun_captcha_access_key_id"`
- AliyunCaptchaAccessKeySecret string `json:"aliyun_captcha_access_key_secret"`
- AliyunCaptchaSceneID string `json:"aliyun_captcha_scene_id"`
- AliyunCaptchaPrefix string `json:"aliyun_captcha_prefix"`
- AliyunCaptchaRegion string `json:"aliyun_captcha_region"`
- // API Key IP 访问控制设置
- APIKeyACLTrustForwardedIP *bool `json:"api_key_acl_trust_forwarded_ip"`
- ForwardedClientIPHeaders *[]string `json:"forwarded_client_ip_headers"`
- // 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"`
- // DingTalk Connect OAuth 登录
- DingTalkConnectEnabled bool `json:"dingtalk_connect_enabled"`
- DingTalkConnectClientID string `json:"dingtalk_connect_client_id"`
- DingTalkConnectClientSecret string `json:"dingtalk_connect_client_secret"`
- DingTalkConnectRedirectURL string `json:"dingtalk_connect_redirect_url"`
- DingTalkConnectCorpRestrictionPolicy string `json:"dingtalk_connect_corp_restriction_policy"`
- DingTalkConnectInternalCorpID string `json:"dingtalk_connect_internal_corp_id"`
- DingTalkConnectBypassRegistration bool `json:"dingtalk_connect_bypass_registration"`
- DingTalkConnectSyncCorpEmail bool `json:"dingtalk_connect_sync_corp_email"`
- DingTalkConnectSyncDisplayName bool `json:"dingtalk_connect_sync_display_name"`
- DingTalkConnectSyncDept bool `json:"dingtalk_connect_sync_dept"`
- DingTalkConnectSyncCorpEmailAttrKey string `json:"dingtalk_connect_sync_corp_email_attr_key"`
- DingTalkConnectSyncDisplayNameAttrKey string `json:"dingtalk_connect_sync_display_name_attr_key"`
- DingTalkConnectSyncDeptAttrKey string `json:"dingtalk_connect_sync_dept_attr_key"`
- DingTalkConnectSyncCorpEmailAttrName string `json:"dingtalk_connect_sync_corp_email_attr_name"`
- DingTalkConnectSyncDisplayNameAttrName string `json:"dingtalk_connect_sync_display_name_attr_name"`
- DingTalkConnectSyncDeptAttrName string `json:"dingtalk_connect_sync_dept_attr_name"`
- // WeChat Connect OAuth 登录
- WeChatConnectEnabled bool `json:"wechat_connect_enabled"`
- WeChatConnectAppID string `json:"wechat_connect_app_id"`
- WeChatConnectAppSecret string `json:"wechat_connect_app_secret"`
- WeChatConnectOpenAppID string `json:"wechat_connect_open_app_id"`
- WeChatConnectOpenAppSecret string `json:"wechat_connect_open_app_secret"`
- WeChatConnectMPAppID string `json:"wechat_connect_mp_app_id"`
- WeChatConnectMPAppSecret string `json:"wechat_connect_mp_app_secret"`
- WeChatConnectMobileAppID string `json:"wechat_connect_mobile_app_id"`
- WeChatConnectMobileAppSecret string `json:"wechat_connect_mobile_app_secret"`
- WeChatConnectOpenEnabled bool `json:"wechat_connect_open_enabled"`
- WeChatConnectMPEnabled bool `json:"wechat_connect_mp_enabled"`
- WeChatConnectMobileEnabled bool `json:"wechat_connect_mobile_enabled"`
- WeChatConnectMode string `json:"wechat_connect_mode"`
- WeChatConnectScopes string `json:"wechat_connect_scopes"`
- WeChatConnectRedirectURL string `json:"wechat_connect_redirect_url"`
- WeChatConnectFrontendRedirectURL string `json:"wechat_connect_frontend_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"`
- GitHubOAuthEnabled bool `json:"github_oauth_enabled"`
- GitHubOAuthClientID string `json:"github_oauth_client_id"`
- GitHubOAuthClientSecret string `json:"github_oauth_client_secret"`
- GitHubOAuthRedirectURL string `json:"github_oauth_redirect_url"`
- GitHubOAuthFrontendRedirectURL string `json:"github_oauth_frontend_redirect_url"`
- GoogleOAuthEnabled bool `json:"google_oauth_enabled"`
- GoogleOAuthClientID string `json:"google_oauth_client_id"`
- GoogleOAuthClientSecret string `json:"google_oauth_client_secret"`
- GoogleOAuthRedirectURL string `json:"google_oauth_redirect_url"`
- GoogleOAuthFrontendRedirectURL string `json:"google_oauth_frontend_redirect_url"`
- // 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"`
- CompactHomeEnabled bool `json:"compact_home_enabled"`
- 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"`
- AffiliateRebateRate *float64 `json:"affiliate_rebate_rate"`
- AffiliateRebateFreezeHours *int `json:"affiliate_rebate_freeze_hours"`
- AffiliateRebateDurationDays *int `json:"affiliate_rebate_duration_days"`
- AffiliateRebatePerInviteeCap *float64 `json:"affiliate_rebate_per_invitee_cap"`
- AdminRechargeRebateEnabled *bool `json:"affiliate_admin_recharge_enabled"`
- DefaultUserRPMLimit int `json:"default_user_rpm_limit"`
- DefaultSubscriptions []dto.DefaultSubscriptionSetting `json:"default_subscriptions"`
- AuthSourceDefaultEmailBalance *float64 `json:"auth_source_default_email_balance"`
- AuthSourceDefaultEmailConcurrency *int `json:"auth_source_default_email_concurrency"`
- AuthSourceDefaultEmailSubscriptions *[]dto.DefaultSubscriptionSetting `json:"auth_source_default_email_subscriptions"`
- AuthSourceDefaultEmailGrantOnSignup *bool `json:"auth_source_default_email_grant_on_signup"`
- AuthSourceDefaultEmailGrantOnFirstBind *bool `json:"auth_source_default_email_grant_on_first_bind"`
- AuthSourceDefaultLinuxDoBalance *float64 `json:"auth_source_default_linuxdo_balance"`
- AuthSourceDefaultLinuxDoConcurrency *int `json:"auth_source_default_linuxdo_concurrency"`
- AuthSourceDefaultLinuxDoSubscriptions *[]dto.DefaultSubscriptionSetting `json:"auth_source_default_linuxdo_subscriptions"`
- AuthSourceDefaultLinuxDoGrantOnSignup *bool `json:"auth_source_default_linuxdo_grant_on_signup"`
- AuthSourceDefaultLinuxDoGrantOnFirstBind *bool `json:"auth_source_default_linuxdo_grant_on_first_bind"`
- AuthSourceDefaultOIDCBalance *float64 `json:"auth_source_default_oidc_balance"`
- AuthSourceDefaultOIDCConcurrency *int `json:"auth_source_default_oidc_concurrency"`
- AuthSourceDefaultOIDCSubscriptions *[]dto.DefaultSubscriptionSetting `json:"auth_source_default_oidc_subscriptions"`
- AuthSourceDefaultOIDCGrantOnSignup *bool `json:"auth_source_default_oidc_grant_on_signup"`
- AuthSourceDefaultOIDCGrantOnFirstBind *bool `json:"auth_source_default_oidc_grant_on_first_bind"`
- AuthSourceDefaultWeChatBalance *float64 `json:"auth_source_default_wechat_balance"`
- AuthSourceDefaultWeChatConcurrency *int `json:"auth_source_default_wechat_concurrency"`
- AuthSourceDefaultWeChatSubscriptions *[]dto.DefaultSubscriptionSetting `json:"auth_source_default_wechat_subscriptions"`
- AuthSourceDefaultWeChatGrantOnSignup *bool `json:"auth_source_default_wechat_grant_on_signup"`
- AuthSourceDefaultWeChatGrantOnFirstBind *bool `json:"auth_source_default_wechat_grant_on_first_bind"`
- AuthSourceDefaultGitHubBalance *float64 `json:"auth_source_default_github_balance"`
- AuthSourceDefaultGitHubConcurrency *int `json:"auth_source_default_github_concurrency"`
- AuthSourceDefaultGitHubSubscriptions *[]dto.DefaultSubscriptionSetting `json:"auth_source_default_github_subscriptions"`
- AuthSourceDefaultGitHubGrantOnSignup *bool `json:"auth_source_default_github_grant_on_signup"`
- AuthSourceDefaultGitHubGrantOnFirstBind *bool `json:"auth_source_default_github_grant_on_first_bind"`
- AuthSourceDefaultGoogleBalance *float64 `json:"auth_source_default_google_balance"`
- AuthSourceDefaultGoogleConcurrency *int `json:"auth_source_default_google_concurrency"`
- AuthSourceDefaultGoogleSubscriptions *[]dto.DefaultSubscriptionSetting `json:"auth_source_default_google_subscriptions"`
- AuthSourceDefaultGoogleGrantOnSignup *bool `json:"auth_source_default_google_grant_on_signup"`
- AuthSourceDefaultGoogleGrantOnFirstBind *bool `json:"auth_source_default_google_grant_on_first_bind"`
- AuthSourceDefaultDingTalkBalance *float64 `json:"auth_source_default_dingtalk_balance"`
- AuthSourceDefaultDingTalkConcurrency *int `json:"auth_source_default_dingtalk_concurrency"`
- AuthSourceDefaultDingTalkSubscriptions *[]dto.DefaultSubscriptionSetting `json:"auth_source_default_dingtalk_subscriptions"`
- AuthSourceDefaultDingTalkGrantOnSignup *bool `json:"auth_source_default_dingtalk_grant_on_signup"`
- AuthSourceDefaultDingTalkGrantOnFirstBind *bool `json:"auth_source_default_dingtalk_grant_on_first_bind"`
- ForceEmailOnThirdPartySignup *bool `json:"force_email_on_third_party_signup"`
- // 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
- OpenAITTFTMode *string `json:"openai_ttft_mode"`
- EnableFingerprintUnification *bool `json:"enable_fingerprint_unification"`
- EnableMetadataPassthrough *bool `json:"enable_metadata_passthrough"`
- EnableCCHSigning *bool `json:"enable_cch_signing"`
- EnableClaudeOAuthSystemPromptInjection *bool `json:"enable_claude_oauth_system_prompt_injection"`
- ClaudeOAuthSystemPrompt *string `json:"claude_oauth_system_prompt"`
- ClaudeOAuthSystemPromptBlocks *string `json:"claude_oauth_system_prompt_blocks"`
- EnableAnthropicCacheTTL1hInjection *bool `json:"enable_anthropic_cache_ttl_1h_injection"`
- RewriteMessageCacheControl *bool `json:"rewrite_message_cache_control"`
- EnableClientDatelineNormalization *bool `json:"enable_client_dateline_normalization"`
- AntigravityUserAgentVersion *string `json:"antigravity_user_agent_version"`
- OpenAICodexUserAgent *string `json:"openai_codex_user_agent"`
- OpenAICodexClientVersion *string `json:"openai_codex_client_version"`
- OpenAICodexVersionAutoSyncEnabled *bool `json:"openai_codex_version_auto_sync_enabled"`
- // codex_cli_only 加固(global-only)
- MinCodexVersion string `json:"min_codex_version"`
- MaxCodexVersion string `json:"max_codex_version"`
- CodexCLIOnlyBlacklist string `json:"codex_cli_only_blacklist"`
- CodexCLIOnlyWhitelist string `json:"codex_cli_only_whitelist"`
- CodexCLIOnlyAllowAppServerClients *bool `json:"codex_cli_only_allow_app_server_clients"`
- CodexCLIOnlyEngineFingerprintSignals string `json:"codex_cli_only_engine_fingerprint_signals"`
- // Payment visible method routing
- PaymentVisibleMethodAlipaySource *string `json:"payment_visible_method_alipay_source"`
- PaymentVisibleMethodWxpaySource *string `json:"payment_visible_method_wxpay_source"`
- PaymentVisibleMethodAlipayEnabled *bool `json:"payment_visible_method_alipay_enabled"`
- PaymentVisibleMethodWxpayEnabled *bool `json:"payment_visible_method_wxpay_enabled"`
- // OpenAI account scheduling
- OpenAILowUpstreamRatePriorityEnabled *bool `json:"openai_low_upstream_rate_priority_enabled"`
- OpenAIOAuthSchedulingRateMultiplier *float64 `json:"openai_oauth_scheduling_rate_multiplier"`
- OpenAIAdvancedSchedulerEnabled *bool `json:"openai_advanced_scheduler_enabled"`
- OpenAIAdvancedSchedulerStickyWeightedEnabled *bool `json:"openai_advanced_scheduler_sticky_weighted_enabled"`
- OpenAIAdvancedSchedulerSubscriptionPriorityEnabled *bool `json:"openai_advanced_scheduler_subscription_priority_enabled"`
- OpenAIAdvancedSchedulerLBTopK *string `json:"openai_advanced_scheduler_lb_top_k"`
- OpenAIAdvancedSchedulerWeightPriority *string `json:"openai_advanced_scheduler_weight_priority"`
- OpenAIAdvancedSchedulerWeightLoad *string `json:"openai_advanced_scheduler_weight_load"`
- OpenAIAdvancedSchedulerWeightQueue *string `json:"openai_advanced_scheduler_weight_queue"`
- OpenAIAdvancedSchedulerWeightErrorRate *string `json:"openai_advanced_scheduler_weight_error_rate"`
- OpenAIAdvancedSchedulerWeightTTFT *string `json:"openai_advanced_scheduler_weight_ttft"`
- OpenAIAdvancedSchedulerWeightReset *string `json:"openai_advanced_scheduler_weight_reset"`
- OpenAIAdvancedSchedulerWeightQuotaHeadroom *string `json:"openai_advanced_scheduler_weight_quota_headroom"`
- OpenAIAdvancedSchedulerWeightUpstreamCost *string `json:"openai_advanced_scheduler_weight_upstream_cost"`
- OpenAIAdvancedSchedulerWeightPreviousResponse *string `json:"openai_advanced_scheduler_weight_previous_response"`
- OpenAIAdvancedSchedulerWeightSessionSticky *string `json:"openai_advanced_scheduler_weight_session_sticky"`
- // 余额不足提醒
- BalanceLowNotifyEnabled *bool `json:"balance_low_notify_enabled"`
- BalanceLowNotifyThreshold *float64 `json:"balance_low_notify_threshold"`
- BalanceLowNotifyRechargeURL *string `json:"balance_low_notify_recharge_url"`
- SubscriptionExpiryNotifyEnabled *bool `json:"subscription_expiry_notify_enabled"`
- 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"`
- PaymentBalanceRechargeMultiplier *float64 `json:"payment_balance_recharge_multiplier"`
- PaymentSubscriptionUSDToCNYRate *float64 `json:"payment_subscription_usd_to_cny_rate"`
- PaymentRechargeFeeRate *float64 `json:"payment_recharge_fee_rate"`
- 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"`
- // Force Alipay mobile clients to use QR code payment instead of mobile redirect
- PaymentAlipayForceQRCode *bool `json:"payment_alipay_force_qrcode"`
- // Use Alipay face-to-face precreate and an app deep link on mobile clients.
- PaymentAlipayMobilePrecreateDeepLink *bool `json:"payment_alipay_mobile_precreate_deep_link"`
- // Channel Monitor feature switch
- ChannelMonitorEnabled *bool `json:"channel_monitor_enabled"`
- ChannelMonitorMode *string `json:"channel_monitor_mode"`
- ChannelMonitorDefaultIntervalSeconds *int `json:"channel_monitor_default_interval_seconds"`
- ChannelMonitorHideThroughput *bool `json:"channel_monitor_hide_throughput"`
- ChannelMonitorShowQuota *bool `json:"channel_monitor_show_quota"`
- ChannelMonitorHideUserRanking *bool `json:"channel_monitor_hide_user_ranking"`
- // Grok model mapping policy
- GrokDefaultTextModel *string `json:"grok_default_text_model"`
- GrokCrossClientModelMapEnabled *bool `json:"grok_cross_client_model_map_enabled"`
- GrokDefaultBaseURLMode *string `json:"grok_default_base_url_mode"`
- // Available Channels feature switch (user-facing)
- AvailableChannelsEnabled *bool `json:"available_channels_enabled"`
- // Model Plaza feature switches + description
- ModelPlazaEnabled *bool `json:"model_plaza_enabled"`
- ModelPlazaRequireAuth *bool `json:"model_plaza_require_auth"`
- ModelPlazaDescription *string `json:"model_plaza_description"`
- // Plugin management menu visibility switch; plugin runtime is unaffected.
- PluginManagementEnabled *bool `json:"plugin_management_enabled"`
- // Affiliate (邀请返利) feature switch
- AffiliateEnabled *bool `json:"affiliate_enabled"`
- // 风控中心功能开关
- RiskControlEnabled *bool `json:"risk_control_enabled"`
- // cyber 会话屏蔽开关 + TTL
- CyberSessionBlockEnabled *bool `json:"cyber_session_block_enabled"`
- CyberSessionBlockTTLSeconds *int `json:"cyber_session_block_ttl_seconds"`
- // OpenAI fast/flex policy (optional, only updated when provided)
- OpenAIFastPolicySettings *dto.OpenAIFastPolicySettings `json:"openai_fast_policy_settings,omitempty"`
- // 系统全局 platform quota 默认值(整体替换语义:nil = 不修改,non-nil = 整体覆盖)。
- DefaultPlatformQuotas map[string]*service.DefaultPlatformQuotaSetting `json:"default_platform_quotas"`
- // 各平台账号自动停调阈值(整体替换语义:nil = 不修改,non-nil = 整体覆盖)。
- AccountSchedulingThresholds map[string]int `json:"account_scheduling_thresholds"`
- // auth-source 层 platform quota 覆盖(override 语义:nil = 不修改,non-nil = 整体覆盖该 source 的 quota 配置)。
- AuthSourceEmailPlatformQuotas map[string]*service.DefaultPlatformQuotaSetting `json:"auth_source_default_email_platform_quotas"`
- AuthSourceLinuxDoPlatformQuotas map[string]*service.DefaultPlatformQuotaSetting `json:"auth_source_default_linuxdo_platform_quotas"`
- AuthSourceOIDCPlatformQuotas map[string]*service.DefaultPlatformQuotaSetting `json:"auth_source_default_oidc_platform_quotas"`
- AuthSourceWeChatPlatformQuotas map[string]*service.DefaultPlatformQuotaSetting `json:"auth_source_default_wechat_platform_quotas"`
- AuthSourceGitHubPlatformQuotas map[string]*service.DefaultPlatformQuotaSetting `json:"auth_source_default_github_platform_quotas"`
- AuthSourceGooglePlatformQuotas map[string]*service.DefaultPlatformQuotaSetting `json:"auth_source_default_google_platform_quotas"`
- AuthSourceDingTalkPlatformQuotas map[string]*service.DefaultPlatformQuotaSetting `json:"auth_source_default_dingtalk_platform_quotas"`
- AllowUserViewErrorRequests *bool `json:"allow_user_view_error_requests"`
- }
- // UpdateSettings 更新系统设置
- // PUT /api/v1/admin/settings
- // ensureActorTotpForStepUp 校验当前操作者具备开启 step-up 门控的条件:
- // 必须是真人管理员会话(admin API key 无法完成 TOTP step-up,拒绝)且本人已启用 TOTP。
- // 校验失败时写入错误响应并返回 false。
- func (h *SettingHandler) ensureActorTotpForStepUp(c *gin.Context) bool {
- if c.GetString("auth_method") == service.AuditAuthMethodAdminAPIKey {
- response.ErrorWithDetails(c, http.StatusForbidden,
- "Admin API key cannot enable step-up verification; use an admin session with TOTP enabled",
- "STEP_UP_ADMIN_API_KEY_FORBIDDEN", nil)
- return false
- }
- subject, ok := middleware.GetAuthSubjectFromContext(c)
- if !ok || subject.UserID <= 0 {
- response.ErrorWithDetails(c, http.StatusForbidden,
- "Enabling step-up verification requires an authenticated admin session",
- "STEP_UP_ENABLE_REQUIRES_TOTP", nil)
- return false
- }
- if h.userService == nil {
- response.InternalError(c, "Step-up precondition check unavailable")
- return false
- }
- user, err := h.userService.GetByID(c.Request.Context(), subject.UserID)
- if err != nil {
- response.ErrorFrom(c, err)
- return false
- }
- if !user.TotpEnabled {
- response.ErrorWithDetails(c, http.StatusBadRequest,
- "Enable two-factor authentication (TOTP) for your account before turning on step-up verification",
- "STEP_UP_ENABLE_REQUIRES_TOTP", nil)
- return false
- }
- return true
- }
- // settingKeyJSONAliases covers the request fields whose JSON name differs from
- // the setting key they persist to. Every other field of UpdateSettingsRequest
- // is named after its setting key.
- var settingKeyJSONAliases = map[string]string{
- "smtp_from_email": service.SettingKeySMTPFrom,
- }
- // settingKeyByJSONName maps the value-typed top-level JSON fields of
- // UpdateSettingsRequest to the setting key each one writes. Resolved once from
- // the struct tags so new fields are covered without touching this file.
- //
- // Pointer-typed fields are deliberately excluded: they already carry their own
- // "omitted = keep the stored value" merge in UpdateSettings, and some of them
- // rely on being rewritten on every save to re-normalize fail-closed security
- // state (see TestUpdateSettingsMalformedForwardedClientIPHeadersRemainFailClosedWhenOmitted).
- // Only the value-typed fields are indistinguishable from a deliberate clear.
- var settingKeyByJSONName = buildSettingKeyByJSONName()
- func buildSettingKeyByJSONName() map[string]string {
- t := reflect.TypeOf(UpdateSettingsRequest{})
- out := make(map[string]string, t.NumField())
- for i := 0; i < t.NumField(); i++ {
- field := t.Field(i)
- if field.Type.Kind() == reflect.Pointer {
- continue
- }
- name, _, _ := strings.Cut(field.Tag.Get("json"), ",")
- if name == "" || name == "-" {
- continue
- }
- if alias, ok := settingKeyJSONAliases[name]; ok {
- out[name] = alias
- continue
- }
- out[name] = name
- }
- return out
- }
- // omittedSettingKeys reports the setting keys this payload never mentioned.
- // Saving settings is a whole-document PUT, so without this a client that sends
- // only the one field it cares about resets every other field to a zero value.
- func omittedSettingKeys(sentFields map[string]json.RawMessage) service.OmittedSettingKeys {
- omitted := make(service.OmittedSettingKeys, len(settingKeyByJSONName))
- for jsonName, settingKey := range settingKeyByJSONName {
- if _, sent := sentFields[jsonName]; !sent {
- omitted[settingKey] = struct{}{}
- }
- }
- return omitted
- }
- func settingsAuditRequest(req UpdateSettingsRequest) UpdateSettingsRequest {
- req.TencentCaptchaAppSecretKey = strings.TrimSpace(req.TencentCaptchaAppSecretKey)
- req.TencentCaptchaCloudSecretID = strings.TrimSpace(req.TencentCaptchaCloudSecretID)
- req.TencentCaptchaCloudSecretKey = strings.TrimSpace(req.TencentCaptchaCloudSecretKey)
- req.AliyunCaptchaAccessKeySecret = strings.TrimSpace(req.AliyunCaptchaAccessKeySecret)
- return req
- }
- func (h *SettingHandler) UpdateSettings(c *gin.Context) {
- var sentFields map[string]json.RawMessage
- if err := c.ShouldBindBodyWith(&sentFields, binding.JSON); err != nil {
- response.BadRequest(c, "Invalid request: "+err.Error())
- return
- }
- var req UpdateSettingsRequest
- if err := c.ShouldBindBodyWith(&req, binding.JSON); err != nil {
- response.BadRequest(c, "Invalid request: "+err.Error())
- return
- }
- auditReq := settingsAuditRequest(req)
- omitted := omittedSettingKeys(sentFields)
- previousSettings, err := h.settingService.GetAllSettings(c.Request.Context())
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- previousAuthSourceDefaults, err := h.settingService.GetAuthSourceDefaultSettings(c.Request.Context())
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- // 两个安全开关的请求字段为指针:省略字段=保持现值,避免旧客户端/脚本
- // 用不含新字段的全量 payload 保存设置时把安全开关静默重置。
- sessionBindingEnabled := previousSettings.SessionBindingEnabled
- if req.SessionBindingEnabled != nil {
- sessionBindingEnabled = *req.SessionBindingEnabled
- }
- stepUpEnabled := previousSettings.StepUpEnabled
- if req.StepUpEnabled != nil {
- stepUpEnabled = *req.StepUpEnabled
- }
- passkeyEnabled := previousSettings.PasskeyEnabled
- if req.PasskeyEnabled != nil {
- passkeyEnabled = *req.PasskeyEnabled
- }
- registrationEmailDomainQuotaEnabled := previousSettings.RegistrationEmailDomainQuotaEnabled
- if req.RegistrationEmailDomainQuotaEnabled != nil {
- registrationEmailDomainQuotaEnabled = *req.RegistrationEmailDomainQuotaEnabled
- }
- if passkeyEnabled {
- configured, _, _ := h.settingService.PasskeyConfiguration()
- if !configured {
- response.BadRequest(c, "Passkey sign-in requires a valid WebAuthn RP ID and allowed HTTPS origins in the deployment configuration")
- return
- }
- }
- forwardedClientIPHeaders := append([]string(nil), previousSettings.ForwardedClientIPHeaders...)
- if req.ForwardedClientIPHeaders != nil {
- forwardedClientIPHeaders = append([]string(nil), (*req.ForwardedClientIPHeaders)...)
- }
- // 开启敏感操作 step-up 门控属自锁风险操作:仅允许本人已启用 TOTP 的管理员会话开启,
- // 否则开启后操作者立即被挡在所有敏感操作之外。仅在 false→true 的开启瞬间校验,
- // 保持开启状态的常规设置保存不受影响。
- if stepUpEnabled && !previousSettings.StepUpEnabled {
- if !h.ensureActorTotpForStepUp(c) {
- return
- }
- }
- // 关闭 step-up 门控本身就是敏感操作:防止拿到管理员会话的攻击者先关闸再执行导出/备份。
- // previousSettings 已证实开关处于开启状态,使用无条件门控变体,
- // 避免门控内部二次读取开关时因存储故障 fail-open(前端捕获 STEP_UP_REQUIRED 弹码重试)。
- if !stepUpEnabled && previousSettings.StepUpEnabled {
- if !middleware.EnforceStepUpAlways(c, h.totpService, h.userService) {
- return
- }
- }
- // 验证参数
- if req.DefaultConcurrency < 1 {
- req.DefaultConcurrency = 1
- }
- if req.DefaultBalance < 0 {
- req.DefaultBalance = 0
- }
- affiliateRebateRate := previousSettings.AffiliateRebateRate
- if req.AffiliateRebateRate != nil {
- affiliateRebateRate = *req.AffiliateRebateRate
- }
- if affiliateRebateRate < service.AffiliateRebateRateMin {
- affiliateRebateRate = service.AffiliateRebateRateMin
- }
- if affiliateRebateRate > service.AffiliateRebateRateMax {
- affiliateRebateRate = service.AffiliateRebateRateMax
- }
- affiliateRebateFreezeHours := previousSettings.AffiliateRebateFreezeHours
- if req.AffiliateRebateFreezeHours != nil {
- affiliateRebateFreezeHours = *req.AffiliateRebateFreezeHours
- }
- if affiliateRebateFreezeHours < 0 {
- affiliateRebateFreezeHours = service.AffiliateRebateFreezeHoursDefault
- }
- if affiliateRebateFreezeHours > service.AffiliateRebateFreezeHoursMax {
- affiliateRebateFreezeHours = service.AffiliateRebateFreezeHoursMax
- }
- affiliateRebateDurationDays := previousSettings.AffiliateRebateDurationDays
- if req.AffiliateRebateDurationDays != nil {
- affiliateRebateDurationDays = *req.AffiliateRebateDurationDays
- }
- if affiliateRebateDurationDays < 0 {
- affiliateRebateDurationDays = service.AffiliateRebateDurationDaysDefault
- }
- if affiliateRebateDurationDays > service.AffiliateRebateDurationDaysMax {
- affiliateRebateDurationDays = service.AffiliateRebateDurationDaysMax
- }
- affiliateRebatePerInviteeCap := previousSettings.AffiliateRebatePerInviteeCap
- if req.AffiliateRebatePerInviteeCap != nil {
- affiliateRebatePerInviteeCap = *req.AffiliateRebatePerInviteeCap
- }
- if affiliateRebatePerInviteeCap < 0 {
- affiliateRebatePerInviteeCap = service.AffiliateRebatePerInviteeCapDefault
- }
- adminRechargeRebateEnabled := previousSettings.AdminRechargeRebateEnabled
- if req.AdminRechargeRebateEnabled != nil {
- adminRechargeRebateEnabled = *req.AdminRechargeRebateEnabled
- }
- // 通用表格配置:兼容旧客户端未传字段时保留当前值。
- 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)
- req.TencentCaptchaAppID = strings.TrimSpace(req.TencentCaptchaAppID)
- req.TencentCaptchaAppSecretKey = strings.TrimSpace(req.TencentCaptchaAppSecretKey)
- req.TencentCaptchaCloudSecretID = strings.TrimSpace(req.TencentCaptchaCloudSecretID)
- req.TencentCaptchaCloudSecretKey = strings.TrimSpace(req.TencentCaptchaCloudSecretKey)
- if req.SMTPPort <= 0 {
- req.SMTPPort = 587
- }
- req.DefaultSubscriptions = normalizeDefaultSubscriptions(req.DefaultSubscriptions)
- req.AuthSourceDefaultEmailSubscriptions = normalizeOptionalDefaultSubscriptions(req.AuthSourceDefaultEmailSubscriptions)
- req.AuthSourceDefaultLinuxDoSubscriptions = normalizeOptionalDefaultSubscriptions(req.AuthSourceDefaultLinuxDoSubscriptions)
- req.AuthSourceDefaultOIDCSubscriptions = normalizeOptionalDefaultSubscriptions(req.AuthSourceDefaultOIDCSubscriptions)
- req.AuthSourceDefaultWeChatSubscriptions = normalizeOptionalDefaultSubscriptions(req.AuthSourceDefaultWeChatSubscriptions)
- req.AuthSourceDefaultDingTalkSubscriptions = normalizeOptionalDefaultSubscriptions(req.AuthSourceDefaultDingTalkSubscriptions)
- // 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
- }
- turnstileEnabled := req.TurnstileEnabled
- if _, sent := sentFields["turnstile_enabled"]; !sent {
- turnstileEnabled = previousSettings.TurnstileEnabled
- }
- tencentCaptchaEnabled := req.TencentCaptchaEnabled
- if _, sent := sentFields["tencent_captcha_enabled"]; !sent {
- tencentCaptchaEnabled = previousSettings.TencentCaptchaEnabled
- }
- aliyunCaptchaEnabled := req.AliyunCaptchaEnabled
- if _, sent := sentFields["aliyun_captcha_enabled"]; !sent {
- aliyunCaptchaEnabled = previousSettings.AliyunCaptchaEnabled
- }
- enabledCaptchaProviders := 0
- for _, enabled := range []bool{turnstileEnabled, tencentCaptchaEnabled, aliyunCaptchaEnabled} {
- if enabled {
- enabledCaptchaProviders++
- }
- }
- if enabledCaptchaProviders > 1 {
- response.BadRequest(c, "Multiple captcha providers (Cloudflare Turnstile / Tencent Captcha / Aliyun Captcha) cannot be enabled at the same time")
- return
- }
- // 阿里云地域 normalize:未发送保留已存值,非法值一律按中国内地落库
- if _, sent := sentFields["aliyun_captcha_region"]; !sent {
- req.AliyunCaptchaRegion = previousSettings.AliyunCaptchaRegion
- }
- if req.AliyunCaptchaRegion != service.AliyunCaptchaRegionSGP {
- req.AliyunCaptchaRegion = service.AliyunCaptchaRegionCN
- }
- // 天御站点 normalize:未发送保留已存值,非法值一律按中国站落库
- if _, sent := sentFields["tencent_captcha_region"]; !sent {
- req.TencentCaptchaRegion = previousSettings.TencentCaptchaRegion
- }
- if req.TencentCaptchaRegion != service.TencentCaptchaRegionINTL {
- req.TencentCaptchaRegion = service.TencentCaptchaRegionCN
- }
- // 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
- }
- }
- }
- if tencentCaptchaEnabled {
- if _, sent := sentFields["tencent_captcha_app_id"]; !sent {
- req.TencentCaptchaAppID = previousSettings.TencentCaptchaAppID
- }
- appID, err := strconv.ParseUint(req.TencentCaptchaAppID, 10, 64)
- if err != nil || appID == 0 {
- response.BadRequest(c, "Tencent Captcha CaptchaAppId must be a positive integer when enabled")
- return
- }
- if req.TencentCaptchaAppSecretKey == "" {
- req.TencentCaptchaAppSecretKey = previousSettings.TencentCaptchaAppSecretKey
- }
- if req.TencentCaptchaCloudSecretID == "" {
- req.TencentCaptchaCloudSecretID = previousSettings.TencentCaptchaCloudSecretID
- }
- if req.TencentCaptchaCloudSecretKey == "" {
- req.TencentCaptchaCloudSecretKey = previousSettings.TencentCaptchaCloudSecretKey
- }
- if req.TencentCaptchaAppSecretKey == "" {
- response.BadRequest(c, "Tencent Captcha AppSecretKey is required when enabled")
- return
- }
- if req.TencentCaptchaCloudSecretID == "" {
- response.BadRequest(c, "Tencent Cloud SecretId is required when Tencent Captcha is enabled")
- return
- }
- if req.TencentCaptchaCloudSecretKey == "" {
- response.BadRequest(c, "Tencent Cloud SecretKey is required when Tencent Captcha is enabled")
- return
- }
- }
- // 阿里云验证码 2.0 参数验证
- if aliyunCaptchaEnabled {
- if _, sent := sentFields["aliyun_captcha_scene_id"]; !sent {
- req.AliyunCaptchaSceneID = previousSettings.AliyunCaptchaSceneID
- }
- if _, sent := sentFields["aliyun_captcha_prefix"]; !sent {
- req.AliyunCaptchaPrefix = previousSettings.AliyunCaptchaPrefix
- }
- if _, sent := sentFields["aliyun_captcha_access_key_id"]; !sent {
- req.AliyunCaptchaAccessKeyID = previousSettings.AliyunCaptchaAccessKeyID
- }
- if req.AliyunCaptchaSceneID == "" {
- response.BadRequest(c, "Aliyun Captcha Scene ID is required when enabled")
- return
- }
- if req.AliyunCaptchaPrefix == "" {
- response.BadRequest(c, "Aliyun Captcha Prefix is required when enabled")
- return
- }
- if req.AliyunCaptchaAccessKeyID == "" {
- response.BadRequest(c, "Aliyun Captcha AccessKey ID is required when enabled")
- return
- }
- // 如果未提供 AccessKey Secret,使用已保存的值(留空保留当前值)
- if req.AliyunCaptchaAccessKeySecret == "" {
- if previousSettings.AliyunCaptchaAccessKeySecret == "" {
- response.BadRequest(c, "Aliyun Captcha AccessKey Secret is required when enabled")
- return
- }
- req.AliyunCaptchaAccessKeySecret = previousSettings.AliyunCaptchaAccessKeySecret
- }
- // 凭证任一变化时真实调用一次阿里云校验(避免配置错误导致无法登录)
- credentialsChanged := previousSettings.AliyunCaptchaAccessKeyID != req.AliyunCaptchaAccessKeyID ||
- previousSettings.AliyunCaptchaAccessKeySecret != req.AliyunCaptchaAccessKeySecret ||
- previousSettings.AliyunCaptchaSceneID != req.AliyunCaptchaSceneID ||
- previousSettings.AliyunCaptchaRegion != req.AliyunCaptchaRegion
- if credentialsChanged {
- if err := h.aliyunCaptchaService.ValidateCredentials(c.Request.Context(), req.AliyunCaptchaAccessKeyID, req.AliyunCaptchaAccessKeySecret, req.AliyunCaptchaSceneID, req.AliyunCaptchaRegion); 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
- }
- }
- loginAgreementMode := strings.ToLower(strings.TrimSpace(req.LoginAgreementMode))
- if loginAgreementMode == "" {
- loginAgreementMode = strings.ToLower(strings.TrimSpace(previousSettings.LoginAgreementMode))
- }
- switch loginAgreementMode {
- case "", "modal":
- loginAgreementMode = "modal"
- case "checkbox":
- default:
- response.BadRequest(c, "Login agreement mode must be modal or checkbox")
- return
- }
- loginAgreementUpdatedAt := strings.TrimSpace(req.LoginAgreementUpdatedAt)
- if loginAgreementUpdatedAt == "" {
- loginAgreementUpdatedAt = strings.TrimSpace(previousSettings.LoginAgreementUpdatedAt)
- }
- loginAgreementDocuments := loginAgreementDocumentsToService(req.LoginAgreementDocuments)
- if len(loginAgreementDocuments) == 0 {
- loginAgreementDocuments = previousSettings.LoginAgreementDocuments
- }
- for _, doc := range loginAgreementDocuments {
- if strings.TrimSpace(doc.Title) == "" {
- response.BadRequest(c, "Login agreement document title is required")
- return
- }
- if len(doc.Title) > 80 {
- response.BadRequest(c, "Login agreement document title is too long (max 80 characters)")
- return
- }
- if len(doc.ContentMD) > 200*1024 {
- response.BadRequest(c, "Login agreement document content is too large (max 200KB)")
- return
- }
- }
- if req.LoginAgreementEnabled && len(loginAgreementDocuments) == 0 {
- response.BadRequest(c, "Login agreement documents are required when enabled")
- 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
- }
- }
- // DingTalk Connect 参数验证
- // 防御性:任何写入路径上把已废弃的 corp_restriction_policy=whitelist 入参 coerce 为 none,
- // 避免任何直连 admin API 的客户端把死值写回 DB(前端 UI 已无此选项)。
- req.DingTalkConnectCorpRestrictionPolicy = service.CoerceDingTalkCorpPolicyForWrite(req.DingTalkConnectCorpRestrictionPolicy)
- if req.DingTalkConnectEnabled {
- req.DingTalkConnectClientID = strings.TrimSpace(req.DingTalkConnectClientID)
- req.DingTalkConnectClientSecret = strings.TrimSpace(req.DingTalkConnectClientSecret)
- req.DingTalkConnectRedirectURL = strings.TrimSpace(req.DingTalkConnectRedirectURL)
- req.DingTalkConnectCorpRestrictionPolicy = strings.TrimSpace(req.DingTalkConnectCorpRestrictionPolicy)
- req.DingTalkConnectInternalCorpID = strings.TrimSpace(req.DingTalkConnectInternalCorpID)
- if req.DingTalkConnectClientID == "" {
- response.BadRequest(c, "DingTalk Client ID is required when enabled")
- return
- }
- if req.DingTalkConnectRedirectURL == "" {
- response.BadRequest(c, "DingTalk Redirect URL is required when enabled")
- return
- }
- if err := config.ValidateAbsoluteHTTPURL(req.DingTalkConnectRedirectURL); err != nil {
- response.BadRequest(c, "DingTalk Redirect URL must be an absolute http(s) URL")
- return
- }
- // 如果未提供 client_secret,则保留现有值(如有)。
- if req.DingTalkConnectClientSecret == "" {
- if previousSettings.DingTalkConnectClientSecret == "" {
- response.BadRequest(c, "DingTalk Client Secret is required when enabled")
- return
- }
- req.DingTalkConnectClientSecret = previousSettings.DingTalkConnectClientSecret
- }
- // Corp 策略校验(V1/V4 fail-closed)
- dingTalkCfg := config.DingTalkConnectConfig{
- Enabled: true,
- DingTalkAppKind: "internal_app", // 硬编码:settings 层仅支持 internal_app
- AppType: "internal", // 对于 internal_only 策略的默认值
- CorpRestrictionPolicy: req.DingTalkConnectCorpRestrictionPolicy,
- InternalCorpID: req.DingTalkConnectInternalCorpID,
- }
- // 若未填 corp_restriction_policy,保留已有配置
- if dingTalkCfg.CorpRestrictionPolicy == "" {
- dingTalkCfg.CorpRestrictionPolicy = previousSettings.DingTalkConnectCorpRestrictionPolicy
- }
- // 对于 internal_only 策略,app_type 必须为 internal(V1 校验)
- if dingTalkCfg.CorpRestrictionPolicy == "internal_only" {
- dingTalkCfg.AppType = "internal"
- } else {
- dingTalkCfg.AppType = "public"
- }
- if err := config.ValidateDingTalkConfig(dingTalkCfg); err != nil {
- response.ErrorWithDetails(c, http.StatusBadRequest, err.Error(), mapDingTalkValidateError(err), nil)
- return
- }
- // bypass_registration 仅在 internal_only 模式下有意义;其它策略下强制为 false,
- // 防止 admin 在切换 policy 时把 bypass 残留在 DB 中(前端 UI 也已隐藏该开关)。
- if dingTalkCfg.CorpRestrictionPolicy != "internal_only" {
- req.DingTalkConnectBypassRegistration = false
- // 身份同步三开关同理:仅 internal_only 模式下有意义,其它策略强制 false。
- req.DingTalkConnectSyncCorpEmail = false
- req.DingTalkConnectSyncDisplayName = false
- req.DingTalkConnectSyncDept = false
- }
- // 身份同步目标 attr key:trimSpace + 空值 fallback 到默认值
- req.DingTalkConnectSyncCorpEmailAttrKey = strings.TrimSpace(req.DingTalkConnectSyncCorpEmailAttrKey)
- if req.DingTalkConnectSyncCorpEmailAttrKey == "" {
- req.DingTalkConnectSyncCorpEmailAttrKey = "dingtalk_email"
- }
- req.DingTalkConnectSyncDisplayNameAttrKey = strings.TrimSpace(req.DingTalkConnectSyncDisplayNameAttrKey)
- if req.DingTalkConnectSyncDisplayNameAttrKey == "" {
- req.DingTalkConnectSyncDisplayNameAttrKey = "dingtalk_name"
- }
- req.DingTalkConnectSyncDeptAttrKey = strings.TrimSpace(req.DingTalkConnectSyncDeptAttrKey)
- if req.DingTalkConnectSyncDeptAttrKey == "" {
- req.DingTalkConnectSyncDeptAttrKey = "dingtalk_department"
- }
- // 身份同步目标 attr 显示名称:trim + 空值 fallback 到默认中文名
- req.DingTalkConnectSyncCorpEmailAttrName = strings.TrimSpace(req.DingTalkConnectSyncCorpEmailAttrName)
- if req.DingTalkConnectSyncCorpEmailAttrName == "" {
- req.DingTalkConnectSyncCorpEmailAttrName = "钉钉企业邮箱"
- }
- req.DingTalkConnectSyncDisplayNameAttrName = strings.TrimSpace(req.DingTalkConnectSyncDisplayNameAttrName)
- if req.DingTalkConnectSyncDisplayNameAttrName == "" {
- req.DingTalkConnectSyncDisplayNameAttrName = "钉钉姓名"
- }
- req.DingTalkConnectSyncDeptAttrName = strings.TrimSpace(req.DingTalkConnectSyncDeptAttrName)
- if req.DingTalkConnectSyncDeptAttrName == "" {
- req.DingTalkConnectSyncDeptAttrName = "钉钉部门"
- }
- }
- if req.WeChatConnectEnabled {
- req.WeChatConnectAppID = strings.TrimSpace(req.WeChatConnectAppID)
- req.WeChatConnectAppSecret = strings.TrimSpace(req.WeChatConnectAppSecret)
- req.WeChatConnectOpenAppID = strings.TrimSpace(req.WeChatConnectOpenAppID)
- req.WeChatConnectOpenAppSecret = strings.TrimSpace(req.WeChatConnectOpenAppSecret)
- req.WeChatConnectMPAppID = strings.TrimSpace(req.WeChatConnectMPAppID)
- req.WeChatConnectMPAppSecret = strings.TrimSpace(req.WeChatConnectMPAppSecret)
- req.WeChatConnectMobileAppID = strings.TrimSpace(req.WeChatConnectMobileAppID)
- req.WeChatConnectMobileAppSecret = strings.TrimSpace(req.WeChatConnectMobileAppSecret)
- req.WeChatConnectMode = strings.ToLower(strings.TrimSpace(req.WeChatConnectMode))
- req.WeChatConnectScopes = strings.TrimSpace(req.WeChatConnectScopes)
- req.WeChatConnectRedirectURL = strings.TrimSpace(req.WeChatConnectRedirectURL)
- req.WeChatConnectFrontendRedirectURL = strings.TrimSpace(req.WeChatConnectFrontendRedirectURL)
- req.WeChatConnectAppID = strings.TrimSpace(firstNonEmpty(req.WeChatConnectAppID, previousSettings.WeChatConnectAppID))
- req.WeChatConnectRedirectURL = strings.TrimSpace(firstNonEmpty(req.WeChatConnectRedirectURL, previousSettings.WeChatConnectRedirectURL))
- req.WeChatConnectFrontendRedirectURL = strings.TrimSpace(firstNonEmpty(req.WeChatConnectFrontendRedirectURL, previousSettings.WeChatConnectFrontendRedirectURL))
- if req.WeChatConnectMode == "" {
- req.WeChatConnectMode = strings.ToLower(strings.TrimSpace(previousSettings.WeChatConnectMode))
- }
- if req.WeChatConnectScopes == "" {
- req.WeChatConnectScopes = strings.TrimSpace(previousSettings.WeChatConnectScopes)
- }
- if req.WeChatConnectMPEnabled && req.WeChatConnectMobileEnabled {
- response.BadRequest(c, "WeChat Official Account and Mobile App cannot be enabled at the same time")
- return
- }
- if req.WeChatConnectMode != "" {
- switch req.WeChatConnectMode {
- case "open", "mp", "mobile":
- default:
- response.BadRequest(c, "WeChat mode must be open, mp, or mobile")
- return
- }
- }
- if !req.WeChatConnectOpenEnabled && !req.WeChatConnectMPEnabled && !req.WeChatConnectMobileEnabled {
- switch req.WeChatConnectMode {
- case "mp":
- req.WeChatConnectMPEnabled = true
- case "mobile":
- req.WeChatConnectMobileEnabled = true
- default:
- req.WeChatConnectOpenEnabled = true
- }
- }
- if req.WeChatConnectMode == "" {
- if req.WeChatConnectMPEnabled {
- req.WeChatConnectMode = "mp"
- } else if req.WeChatConnectMobileEnabled {
- req.WeChatConnectMode = "mobile"
- } else {
- req.WeChatConnectMode = "open"
- }
- }
- req.WeChatConnectOpenAppID = strings.TrimSpace(firstNonEmpty(req.WeChatConnectOpenAppID, req.WeChatConnectAppID, previousSettings.WeChatConnectOpenAppID, previousSettings.WeChatConnectAppID))
- req.WeChatConnectMPAppID = strings.TrimSpace(firstNonEmpty(req.WeChatConnectMPAppID, req.WeChatConnectAppID, previousSettings.WeChatConnectMPAppID, previousSettings.WeChatConnectAppID))
- req.WeChatConnectMobileAppID = strings.TrimSpace(firstNonEmpty(req.WeChatConnectMobileAppID, req.WeChatConnectAppID, previousSettings.WeChatConnectMobileAppID, previousSettings.WeChatConnectAppID))
- if req.WeChatConnectOpenAppSecret == "" {
- req.WeChatConnectOpenAppSecret = strings.TrimSpace(firstNonEmpty(previousSettings.WeChatConnectOpenAppSecret, previousSettings.WeChatConnectAppSecret, req.WeChatConnectAppSecret))
- }
- if req.WeChatConnectMPAppSecret == "" {
- req.WeChatConnectMPAppSecret = strings.TrimSpace(firstNonEmpty(previousSettings.WeChatConnectMPAppSecret, previousSettings.WeChatConnectAppSecret, req.WeChatConnectAppSecret))
- }
- if req.WeChatConnectMobileAppSecret == "" {
- req.WeChatConnectMobileAppSecret = strings.TrimSpace(firstNonEmpty(previousSettings.WeChatConnectMobileAppSecret, previousSettings.WeChatConnectAppSecret, req.WeChatConnectAppSecret))
- }
- if req.WeChatConnectAppSecret == "" {
- req.WeChatConnectAppSecret = strings.TrimSpace(firstNonEmpty(req.WeChatConnectOpenAppSecret, req.WeChatConnectMPAppSecret, req.WeChatConnectMobileAppSecret, previousSettings.WeChatConnectAppSecret))
- }
- if req.WeChatConnectOpenEnabled {
- if req.WeChatConnectOpenAppID == "" {
- response.BadRequest(c, "WeChat PC App ID is required when enabled")
- return
- }
- if req.WeChatConnectOpenAppSecret == "" {
- response.BadRequest(c, "WeChat PC App Secret is required when enabled")
- return
- }
- }
- if req.WeChatConnectMPEnabled {
- if req.WeChatConnectMPAppID == "" {
- response.BadRequest(c, "WeChat Official Account App ID is required when enabled")
- return
- }
- if req.WeChatConnectMPAppSecret == "" {
- response.BadRequest(c, "WeChat Official Account App Secret is required when enabled")
- return
- }
- }
- if req.WeChatConnectMobileEnabled {
- if req.WeChatConnectMobileAppID == "" {
- response.BadRequest(c, "WeChat Mobile App ID is required when enabled")
- return
- }
- if req.WeChatConnectMobileAppSecret == "" {
- response.BadRequest(c, "WeChat Mobile App Secret is required when enabled")
- return
- }
- }
- if req.WeChatConnectScopes == "" {
- if req.WeChatConnectMPEnabled {
- req.WeChatConnectScopes = service.DefaultWeChatConnectScopesForMode("mp")
- } else {
- req.WeChatConnectScopes = service.DefaultWeChatConnectScopesForMode(req.WeChatConnectMode)
- }
- }
- if req.WeChatConnectOpenEnabled || req.WeChatConnectMPEnabled {
- if req.WeChatConnectRedirectURL == "" {
- response.BadRequest(c, "WeChat Redirect URL is required when web oauth is enabled")
- return
- }
- if err := config.ValidateAbsoluteHTTPURL(req.WeChatConnectRedirectURL); err != nil {
- response.BadRequest(c, "WeChat Redirect URL must be an absolute http(s) URL")
- return
- }
- if req.WeChatConnectFrontendRedirectURL == "" {
- req.WeChatConnectFrontendRedirectURL = "/auth/wechat/callback"
- }
- if err := config.ValidateFrontendRedirectURL(req.WeChatConnectFrontendRedirectURL); err != nil {
- response.BadRequest(c, "WeChat Frontend Redirect URL is invalid")
- return
- }
- }
- }
- // Generic OIDC 参数验证
- oidcUsePKCE, oidcValidateIDToken, err := h.settingService.OIDCSecurityWriteDefaults(c.Request.Context())
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- 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)
- req.OIDCConnectProviderName = strings.TrimSpace(firstNonEmpty(req.OIDCConnectProviderName, previousSettings.OIDCConnectProviderName, "OIDC"))
- req.OIDCConnectClientID = strings.TrimSpace(firstNonEmpty(req.OIDCConnectClientID, previousSettings.OIDCConnectClientID))
- req.OIDCConnectIssuerURL = strings.TrimSpace(firstNonEmpty(req.OIDCConnectIssuerURL, previousSettings.OIDCConnectIssuerURL))
- req.OIDCConnectDiscoveryURL = strings.TrimSpace(firstNonEmpty(req.OIDCConnectDiscoveryURL, previousSettings.OIDCConnectDiscoveryURL))
- req.OIDCConnectAuthorizeURL = strings.TrimSpace(firstNonEmpty(req.OIDCConnectAuthorizeURL, previousSettings.OIDCConnectAuthorizeURL))
- req.OIDCConnectTokenURL = strings.TrimSpace(firstNonEmpty(req.OIDCConnectTokenURL, previousSettings.OIDCConnectTokenURL))
- req.OIDCConnectUserInfoURL = strings.TrimSpace(firstNonEmpty(req.OIDCConnectUserInfoURL, previousSettings.OIDCConnectUserInfoURL))
- req.OIDCConnectJWKSURL = strings.TrimSpace(firstNonEmpty(req.OIDCConnectJWKSURL, previousSettings.OIDCConnectJWKSURL))
- req.OIDCConnectScopes = strings.TrimSpace(firstNonEmpty(req.OIDCConnectScopes, previousSettings.OIDCConnectScopes, "openid email profile"))
- req.OIDCConnectRedirectURL = strings.TrimSpace(firstNonEmpty(req.OIDCConnectRedirectURL, previousSettings.OIDCConnectRedirectURL))
- req.OIDCConnectFrontendRedirectURL = strings.TrimSpace(firstNonEmpty(req.OIDCConnectFrontendRedirectURL, previousSettings.OIDCConnectFrontendRedirectURL, "/auth/oidc/callback"))
- req.OIDCConnectTokenAuthMethod = strings.ToLower(strings.TrimSpace(firstNonEmpty(req.OIDCConnectTokenAuthMethod, previousSettings.OIDCConnectTokenAuthMethod, "client_secret_post")))
- req.OIDCConnectAllowedSigningAlgs = strings.TrimSpace(firstNonEmpty(req.OIDCConnectAllowedSigningAlgs, previousSettings.OIDCConnectAllowedSigningAlgs, "RS256,ES256,PS256"))
- req.OIDCConnectUserInfoEmailPath = strings.TrimSpace(firstNonEmpty(req.OIDCConnectUserInfoEmailPath, previousSettings.OIDCConnectUserInfoEmailPath))
- req.OIDCConnectUserInfoIDPath = strings.TrimSpace(firstNonEmpty(req.OIDCConnectUserInfoIDPath, previousSettings.OIDCConnectUserInfoIDPath))
- req.OIDCConnectUserInfoUsernamePath = strings.TrimSpace(firstNonEmpty(req.OIDCConnectUserInfoUsernamePath, previousSettings.OIDCConnectUserInfoUsernamePath))
- if req.OIDCConnectUsePKCE != nil {
- oidcUsePKCE = *req.OIDCConnectUsePKCE
- }
- if req.OIDCConnectValidateIDToken != nil {
- oidcValidateIDToken = *req.OIDCConnectValidateIDToken
- }
- if req.OIDCConnectClockSkewSeconds == 0 {
- req.OIDCConnectClockSkewSeconds = previousSettings.OIDCConnectClockSkewSeconds
- if req.OIDCConnectClockSkewSeconds == 0 {
- req.OIDCConnectClockSkewSeconds = 120
- }
- }
- 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.OIDCConnectClockSkewSeconds < 0 || req.OIDCConnectClockSkewSeconds > 600 {
- response.BadRequest(c, "OIDC clock skew seconds must be between 0 and 600")
- return
- }
- if oidcValidateIDToken && 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
- }
- urlTrimmed := strings.TrimSpace(item.URL)
- if strings.HasPrefix(urlTrimmed, "md:") {
- // Markdown page mode: URL = "md:<slug>"
- slug := strings.TrimPrefix(urlTrimmed, "md:")
- if slug == "" {
- response.BadRequest(c, "Custom menu item markdown slug cannot be empty (use md:slug format)")
- return
- }
- } else {
- if urlTrimmed == "" {
- response.BadRequest(c, "Custom menu item URL is required (use md:slug for markdown pages)")
- return
- }
- if len(item.URL) > maxMenuItemURLLen {
- response.BadRequest(c, "Custom menu item URL is too long (max 2048 characters)")
- return
- }
- if err := config.ValidateAbsoluteHTTPURL(urlTrimmed); err != nil {
- response.BadRequest(c, "Custom menu item URL must be an absolute http(s) URL or md:<slug>")
- 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.AntigravityUserAgentVersion != nil {
- normalized := strings.TrimSpace(*req.AntigravityUserAgentVersion)
- req.AntigravityUserAgentVersion = &normalized
- if normalized != "" && !semverPattern.MatchString(normalized) {
- response.Error(c, http.StatusBadRequest, "antigravity_user_agent_version must be empty or a valid semver (e.g. 1.23.2)")
- return
- }
- }
- if req.OpenAICodexUserAgent != nil {
- normalized := strings.TrimSpace(*req.OpenAICodexUserAgent)
- req.OpenAICodexUserAgent = &normalized
- // 仅做长度上限保护,不限制具体格式(运维需要可自由调整 codex 版本号)
- if len(normalized) > 512 {
- response.Error(c, http.StatusBadRequest, "openai_codex_user_agent must be at most 512 characters")
- return
- }
- }
- if req.OpenAICodexClientVersion != nil {
- // 该值会被拼进出站 User-Agent 与 version 头,必须是合法版本号;空串表示跟随自动同步。
- normalized := strings.TrimSpace(*req.OpenAICodexClientVersion)
- if normalized != "" && service.NormalizeCodexClientVersion(normalized) == "" {
- response.Error(c, http.StatusBadRequest, "openai_codex_client_version must be empty or a valid version (e.g. 0.146.0)")
- return
- }
- req.OpenAICodexClientVersion = &normalized
- }
- // codex_cli_only 加固:最低/最高 Codex 版本(空=禁用,或合法 semver;max>=min)
- if req.MinCodexVersion != "" && !semverPattern.MatchString(req.MinCodexVersion) {
- response.Error(c, http.StatusBadRequest, "min_codex_version must be empty or a valid semver (e.g. 0.141.0)")
- return
- }
- if req.MaxCodexVersion != "" && !semverPattern.MatchString(req.MaxCodexVersion) {
- response.Error(c, http.StatusBadRequest, "max_codex_version must be empty or a valid semver (e.g. 0.200.0)")
- return
- }
- if req.MinCodexVersion != "" && req.MaxCodexVersion != "" && service.CompareVersions(req.MaxCodexVersion, req.MinCodexVersion) < 0 {
- response.Error(c, http.StatusBadRequest, "max_codex_version must be greater than or equal to min_codex_version")
- return
- }
- // codex_cli_only 黑/白名单:非空须为合法 []AllowedClientEntry JSON。
- // 黑名单 OR 宽 deny(允许 originator-only);白名单双因子 AND,额外要求每条可命中(非空 originator + ua_contains)。
- if err := service.ValidateCodexClientEntriesJSON(req.CodexCLIOnlyBlacklist); err != nil {
- response.Error(c, http.StatusBadRequest, "codex_cli_only_blacklist "+err.Error())
- return
- }
- if err := service.ValidateCodexWhitelistEntriesJSON(req.CodexCLIOnlyWhitelist); err != nil {
- response.Error(c, http.StatusBadRequest, "codex_cli_only_whitelist "+err.Error())
- return
- }
- if err := service.ValidateEngineFingerprintSignalsJSON(req.CodexCLIOnlyEngineFingerprintSignals); err != nil {
- response.Error(c, http.StatusBadRequest, "codex_cli_only_engine_fingerprint_signals "+err.Error())
- 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
- }
- }
- // cyber 会话屏蔽 TTL 校验:提供时必须 > 0
- if req.CyberSessionBlockTTLSeconds != nil && *req.CyberSessionBlockTTLSeconds <= 0 {
- response.BadRequest(c, "cyber_session_block_ttl_seconds must be > 0")
- return
- }
- settings := &service.SystemSettings{
- // 系统全局 platform quota 默认值(整体替换语义)
- DefaultPlatformQuotas: req.DefaultPlatformQuotas,
- AccountSchedulingThresholds: req.AccountSchedulingThresholds,
- RegistrationEnabled: req.RegistrationEnabled,
- EmailVerifyEnabled: req.EmailVerifyEnabled,
- RegistrationEmailSuffixWhitelist: req.RegistrationEmailSuffixWhitelist,
- RegistrationEmailDomainQuotaEnabled: registrationEmailDomainQuotaEnabled,
- PromoCodeEnabled: req.PromoCodeEnabled,
- PasswordResetEnabled: req.PasswordResetEnabled,
- FrontendURL: req.FrontendURL,
- InvitationCodeEnabled: req.InvitationCodeEnabled,
- TotpEnabled: req.TotpEnabled,
- PasskeyEnabled: passkeyEnabled,
- SessionBindingEnabled: sessionBindingEnabled,
- StepUpEnabled: stepUpEnabled,
- AuditLogRetentionDays: req.AuditLogRetentionDays,
- LoginAgreementEnabled: req.LoginAgreementEnabled,
- LoginAgreementMode: loginAgreementMode,
- LoginAgreementUpdatedAt: loginAgreementUpdatedAt,
- LoginAgreementDocuments: loginAgreementDocuments,
- 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,
- TencentCaptchaEnabled: req.TencentCaptchaEnabled,
- TencentCaptchaAppID: req.TencentCaptchaAppID,
- TencentCaptchaAppSecretKey: req.TencentCaptchaAppSecretKey,
- TencentCaptchaCloudSecretID: req.TencentCaptchaCloudSecretID,
- TencentCaptchaCloudSecretKey: req.TencentCaptchaCloudSecretKey,
- TencentCaptchaRegion: req.TencentCaptchaRegion,
- AliyunCaptchaEnabled: req.AliyunCaptchaEnabled,
- AliyunCaptchaAccessKeyID: req.AliyunCaptchaAccessKeyID,
- AliyunCaptchaAccessKeySecret: req.AliyunCaptchaAccessKeySecret,
- AliyunCaptchaSceneID: req.AliyunCaptchaSceneID,
- AliyunCaptchaPrefix: req.AliyunCaptchaPrefix,
- AliyunCaptchaRegion: req.AliyunCaptchaRegion,
- APIKeyACLTrustForwardedIP: func() bool {
- if req.APIKeyACLTrustForwardedIP != nil {
- return *req.APIKeyACLTrustForwardedIP
- }
- return previousSettings.APIKeyACLTrustForwardedIP
- }(),
- ForwardedClientIPHeaders: forwardedClientIPHeaders,
- LinuxDoConnectEnabled: req.LinuxDoConnectEnabled,
- LinuxDoConnectClientID: req.LinuxDoConnectClientID,
- LinuxDoConnectClientSecret: req.LinuxDoConnectClientSecret,
- LinuxDoConnectRedirectURL: req.LinuxDoConnectRedirectURL,
- DingTalkConnectEnabled: req.DingTalkConnectEnabled,
- DingTalkConnectClientID: req.DingTalkConnectClientID,
- DingTalkConnectClientSecret: req.DingTalkConnectClientSecret,
- DingTalkConnectRedirectURL: req.DingTalkConnectRedirectURL,
- DingTalkConnectCorpRestrictionPolicy: req.DingTalkConnectCorpRestrictionPolicy,
- DingTalkConnectInternalCorpID: req.DingTalkConnectInternalCorpID,
- DingTalkConnectBypassRegistration: req.DingTalkConnectBypassRegistration,
- DingTalkConnectSyncCorpEmail: req.DingTalkConnectSyncCorpEmail,
- DingTalkConnectSyncDisplayName: req.DingTalkConnectSyncDisplayName,
- DingTalkConnectSyncDept: req.DingTalkConnectSyncDept,
- DingTalkConnectSyncCorpEmailAttrKey: req.DingTalkConnectSyncCorpEmailAttrKey,
- DingTalkConnectSyncDisplayNameAttrKey: req.DingTalkConnectSyncDisplayNameAttrKey,
- DingTalkConnectSyncDeptAttrKey: req.DingTalkConnectSyncDeptAttrKey,
- DingTalkConnectSyncCorpEmailAttrName: req.DingTalkConnectSyncCorpEmailAttrName,
- DingTalkConnectSyncDisplayNameAttrName: req.DingTalkConnectSyncDisplayNameAttrName,
- DingTalkConnectSyncDeptAttrName: req.DingTalkConnectSyncDeptAttrName,
- WeChatConnectEnabled: req.WeChatConnectEnabled,
- WeChatConnectAppID: req.WeChatConnectAppID,
- WeChatConnectAppSecret: req.WeChatConnectAppSecret,
- WeChatConnectOpenAppID: req.WeChatConnectOpenAppID,
- WeChatConnectOpenAppSecret: req.WeChatConnectOpenAppSecret,
- WeChatConnectMPAppID: req.WeChatConnectMPAppID,
- WeChatConnectMPAppSecret: req.WeChatConnectMPAppSecret,
- WeChatConnectMobileAppID: req.WeChatConnectMobileAppID,
- WeChatConnectMobileAppSecret: req.WeChatConnectMobileAppSecret,
- WeChatConnectOpenEnabled: req.WeChatConnectOpenEnabled,
- WeChatConnectMPEnabled: req.WeChatConnectMPEnabled,
- WeChatConnectMobileEnabled: req.WeChatConnectMobileEnabled,
- WeChatConnectMode: req.WeChatConnectMode,
- WeChatConnectScopes: req.WeChatConnectScopes,
- WeChatConnectRedirectURL: req.WeChatConnectRedirectURL,
- WeChatConnectFrontendRedirectURL: req.WeChatConnectFrontendRedirectURL,
- 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: oidcUsePKCE,
- OIDCConnectValidateIDToken: oidcValidateIDToken,
- OIDCConnectAllowedSigningAlgs: req.OIDCConnectAllowedSigningAlgs,
- OIDCConnectClockSkewSeconds: req.OIDCConnectClockSkewSeconds,
- OIDCConnectRequireEmailVerified: req.OIDCConnectRequireEmailVerified,
- OIDCConnectUserInfoEmailPath: req.OIDCConnectUserInfoEmailPath,
- OIDCConnectUserInfoIDPath: req.OIDCConnectUserInfoIDPath,
- OIDCConnectUserInfoUsernamePath: req.OIDCConnectUserInfoUsernamePath,
- GitHubOAuthEnabled: req.GitHubOAuthEnabled,
- GitHubOAuthClientID: req.GitHubOAuthClientID,
- GitHubOAuthClientSecret: req.GitHubOAuthClientSecret,
- GitHubOAuthRedirectURL: req.GitHubOAuthRedirectURL,
- GitHubOAuthFrontendRedirectURL: req.GitHubOAuthFrontendRedirectURL,
- GoogleOAuthEnabled: req.GoogleOAuthEnabled,
- GoogleOAuthClientID: req.GoogleOAuthClientID,
- GoogleOAuthClientSecret: req.GoogleOAuthClientSecret,
- GoogleOAuthRedirectURL: req.GoogleOAuthRedirectURL,
- GoogleOAuthFrontendRedirectURL: req.GoogleOAuthFrontendRedirectURL,
- SiteName: req.SiteName,
- SiteLogo: req.SiteLogo,
- SiteSubtitle: req.SiteSubtitle,
- APIBaseURL: req.APIBaseURL,
- ContactInfo: req.ContactInfo,
- DocURL: req.DocURL,
- HomeContent: req.HomeContent,
- CompactHomeEnabled: req.CompactHomeEnabled,
- HideCcsImportButton: req.HideCcsImportButton,
- PurchaseSubscriptionEnabled: purchaseEnabled,
- PurchaseSubscriptionURL: purchaseURL,
- TableDefaultPageSize: req.TableDefaultPageSize,
- TablePageSizeOptions: req.TablePageSizeOptions,
- CustomMenuItems: customMenuJSON,
- CustomEndpoints: customEndpointsJSON,
- DefaultConcurrency: req.DefaultConcurrency,
- DefaultBalance: req.DefaultBalance,
- AffiliateRebateRate: affiliateRebateRate,
- AffiliateRebateFreezeHours: affiliateRebateFreezeHours,
- AffiliateRebateDurationDays: affiliateRebateDurationDays,
- AffiliateRebatePerInviteeCap: affiliateRebatePerInviteeCap,
- AdminRechargeRebateEnabled: adminRechargeRebateEnabled,
- DefaultUserRPMLimit: req.DefaultUserRPMLimit,
- 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,
- AllowUserViewErrorRequests: func() bool {
- if req.AllowUserViewErrorRequests != nil {
- return *req.AllowUserViewErrorRequests
- }
- return previousSettings.AllowUserViewErrorRequests
- }(),
- 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
- }(),
- OpenAITTFTMode: func() string {
- if req.OpenAITTFTMode != nil {
- return *req.OpenAITTFTMode
- }
- return previousSettings.OpenAITTFTMode
- }(),
- 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
- }(),
- EnableClaudeOAuthSystemPromptInjection: func() bool {
- if req.EnableClaudeOAuthSystemPromptInjection != nil {
- return *req.EnableClaudeOAuthSystemPromptInjection
- }
- return previousSettings.EnableClaudeOAuthSystemPromptInjection
- }(),
- ClaudeOAuthSystemPrompt: func() string {
- if req.ClaudeOAuthSystemPrompt != nil {
- return *req.ClaudeOAuthSystemPrompt
- }
- return previousSettings.ClaudeOAuthSystemPrompt
- }(),
- ClaudeOAuthSystemPromptBlocks: func() string {
- if req.ClaudeOAuthSystemPromptBlocks != nil {
- return *req.ClaudeOAuthSystemPromptBlocks
- }
- return previousSettings.ClaudeOAuthSystemPromptBlocks
- }(),
- EnableAnthropicCacheTTL1hInjection: func() bool {
- if req.EnableAnthropicCacheTTL1hInjection != nil {
- return *req.EnableAnthropicCacheTTL1hInjection
- }
- return previousSettings.EnableAnthropicCacheTTL1hInjection
- }(),
- RewriteMessageCacheControl: func() bool {
- if req.RewriteMessageCacheControl != nil {
- return *req.RewriteMessageCacheControl
- }
- return previousSettings.RewriteMessageCacheControl
- }(),
- EnableClientDatelineNormalization: func() bool {
- if req.EnableClientDatelineNormalization != nil {
- return *req.EnableClientDatelineNormalization
- }
- return previousSettings.EnableClientDatelineNormalization
- }(),
- AntigravityUserAgentVersion: func() string {
- if req.AntigravityUserAgentVersion != nil {
- return *req.AntigravityUserAgentVersion
- }
- return previousSettings.AntigravityUserAgentVersion
- }(),
- OpenAICodexUserAgent: func() string {
- if req.OpenAICodexUserAgent != nil {
- return *req.OpenAICodexUserAgent
- }
- return previousSettings.OpenAICodexUserAgent
- }(),
- OpenAICodexClientVersion: func() string {
- if req.OpenAICodexClientVersion != nil {
- return *req.OpenAICodexClientVersion
- }
- return previousSettings.OpenAICodexClientVersion
- }(),
- // 同步值由自动同步任务独占写入,面板保存时原样带回,避免被清空。
- OpenAICodexClientVersionSynced: previousSettings.OpenAICodexClientVersionSynced,
- OpenAICodexVersionAutoSyncEnabled: func() bool {
- if req.OpenAICodexVersionAutoSyncEnabled != nil {
- return *req.OpenAICodexVersionAutoSyncEnabled
- }
- return previousSettings.OpenAICodexVersionAutoSyncEnabled
- }(),
- MinCodexVersion: strings.TrimSpace(req.MinCodexVersion),
- MaxCodexVersion: strings.TrimSpace(req.MaxCodexVersion),
- CodexCLIOnlyBlacklist: strings.TrimSpace(req.CodexCLIOnlyBlacklist),
- CodexCLIOnlyWhitelist: strings.TrimSpace(req.CodexCLIOnlyWhitelist),
- CodexCLIOnlyAllowAppServerClients: func() bool {
- if req.CodexCLIOnlyAllowAppServerClients != nil {
- return *req.CodexCLIOnlyAllowAppServerClients
- }
- return previousSettings.CodexCLIOnlyAllowAppServerClients
- }(),
- CodexCLIOnlyEngineFingerprintSignals: strings.TrimSpace(req.CodexCLIOnlyEngineFingerprintSignals),
- PaymentVisibleMethodAlipaySource: func() string {
- if req.PaymentVisibleMethodAlipaySource != nil {
- return strings.TrimSpace(*req.PaymentVisibleMethodAlipaySource)
- }
- return previousSettings.PaymentVisibleMethodAlipaySource
- }(),
- PaymentVisibleMethodWxpaySource: func() string {
- if req.PaymentVisibleMethodWxpaySource != nil {
- return strings.TrimSpace(*req.PaymentVisibleMethodWxpaySource)
- }
- return previousSettings.PaymentVisibleMethodWxpaySource
- }(),
- PaymentVisibleMethodAlipayEnabled: func() bool {
- if req.PaymentVisibleMethodAlipayEnabled != nil {
- return *req.PaymentVisibleMethodAlipayEnabled
- }
- return previousSettings.PaymentVisibleMethodAlipayEnabled
- }(),
- PaymentVisibleMethodWxpayEnabled: func() bool {
- if req.PaymentVisibleMethodWxpayEnabled != nil {
- return *req.PaymentVisibleMethodWxpayEnabled
- }
- return previousSettings.PaymentVisibleMethodWxpayEnabled
- }(),
- OpenAILowUpstreamRatePriorityEnabled: func() bool {
- if req.OpenAILowUpstreamRatePriorityEnabled != nil {
- return *req.OpenAILowUpstreamRatePriorityEnabled
- }
- return previousSettings.OpenAILowUpstreamRatePriorityEnabled
- }(),
- OpenAIOAuthSchedulingRateMultiplier: func() float64 {
- if req.OpenAIOAuthSchedulingRateMultiplier != nil {
- return *req.OpenAIOAuthSchedulingRateMultiplier
- }
- return previousSettings.OpenAIOAuthSchedulingRateMultiplier
- }(),
- OpenAIAdvancedSchedulerEnabled: func() bool {
- if req.OpenAIAdvancedSchedulerEnabled != nil {
- return *req.OpenAIAdvancedSchedulerEnabled
- }
- return previousSettings.OpenAIAdvancedSchedulerEnabled
- }(),
- OpenAIAdvancedSchedulerStickyWeightedEnabled: func() bool {
- if req.OpenAIAdvancedSchedulerStickyWeightedEnabled != nil {
- return *req.OpenAIAdvancedSchedulerStickyWeightedEnabled
- }
- return previousSettings.OpenAIAdvancedSchedulerStickyWeightedEnabled
- }(),
- OpenAIAdvancedSchedulerSubscriptionPriorityEnabled: func() bool {
- if req.OpenAIAdvancedSchedulerSubscriptionPriorityEnabled != nil {
- return *req.OpenAIAdvancedSchedulerSubscriptionPriorityEnabled
- }
- return previousSettings.OpenAIAdvancedSchedulerSubscriptionPriorityEnabled
- }(),
- OpenAIAdvancedSchedulerLBTopK: stringSetting(req.OpenAIAdvancedSchedulerLBTopK, previousSettings.OpenAIAdvancedSchedulerLBTopK),
- OpenAIAdvancedSchedulerWeightPriority: stringSetting(req.OpenAIAdvancedSchedulerWeightPriority, previousSettings.OpenAIAdvancedSchedulerWeightPriority),
- OpenAIAdvancedSchedulerWeightLoad: stringSetting(req.OpenAIAdvancedSchedulerWeightLoad, previousSettings.OpenAIAdvancedSchedulerWeightLoad),
- OpenAIAdvancedSchedulerWeightQueue: stringSetting(req.OpenAIAdvancedSchedulerWeightQueue, previousSettings.OpenAIAdvancedSchedulerWeightQueue),
- OpenAIAdvancedSchedulerWeightErrorRate: stringSetting(req.OpenAIAdvancedSchedulerWeightErrorRate, previousSettings.OpenAIAdvancedSchedulerWeightErrorRate),
- OpenAIAdvancedSchedulerWeightTTFT: stringSetting(req.OpenAIAdvancedSchedulerWeightTTFT, previousSettings.OpenAIAdvancedSchedulerWeightTTFT),
- OpenAIAdvancedSchedulerWeightReset: stringSetting(req.OpenAIAdvancedSchedulerWeightReset, previousSettings.OpenAIAdvancedSchedulerWeightReset),
- OpenAIAdvancedSchedulerWeightQuotaHeadroom: stringSetting(req.OpenAIAdvancedSchedulerWeightQuotaHeadroom, previousSettings.OpenAIAdvancedSchedulerWeightQuotaHeadroom),
- OpenAIAdvancedSchedulerWeightUpstreamCost: stringSetting(req.OpenAIAdvancedSchedulerWeightUpstreamCost, previousSettings.OpenAIAdvancedSchedulerWeightUpstreamCost),
- OpenAIAdvancedSchedulerWeightPreviousResponse: stringSetting(req.OpenAIAdvancedSchedulerWeightPreviousResponse, previousSettings.OpenAIAdvancedSchedulerWeightPreviousResponse),
- OpenAIAdvancedSchedulerWeightSessionSticky: stringSetting(req.OpenAIAdvancedSchedulerWeightSessionSticky, previousSettings.OpenAIAdvancedSchedulerWeightSessionSticky),
- 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
- }(),
- SubscriptionExpiryNotifyEnabled: func() bool {
- if req.SubscriptionExpiryNotifyEnabled != nil {
- return *req.SubscriptionExpiryNotifyEnabled
- }
- return previousSettings.SubscriptionExpiryNotifyEnabled
- }(),
- 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
- }(),
- ChannelMonitorEnabled: func() bool {
- if req.ChannelMonitorEnabled != nil {
- return *req.ChannelMonitorEnabled
- }
- return previousSettings.ChannelMonitorEnabled
- }(),
- ChannelMonitorMode: func() string {
- if req.ChannelMonitorMode != nil {
- return *req.ChannelMonitorMode
- }
- return previousSettings.ChannelMonitorMode
- }(),
- ChannelMonitorDefaultIntervalSeconds: func() int {
- if req.ChannelMonitorDefaultIntervalSeconds != nil {
- return *req.ChannelMonitorDefaultIntervalSeconds
- }
- return previousSettings.ChannelMonitorDefaultIntervalSeconds
- }(),
- ChannelMonitorHideThroughput: func() bool {
- if req.ChannelMonitorHideThroughput != nil {
- return *req.ChannelMonitorHideThroughput
- }
- return previousSettings.ChannelMonitorHideThroughput
- }(),
- ChannelMonitorShowQuota: func() bool {
- if req.ChannelMonitorShowQuota != nil {
- return *req.ChannelMonitorShowQuota
- }
- return previousSettings.ChannelMonitorShowQuota
- }(),
- ChannelMonitorHideUserRanking: func() bool {
- if req.ChannelMonitorHideUserRanking != nil {
- return *req.ChannelMonitorHideUserRanking
- }
- return previousSettings.ChannelMonitorHideUserRanking
- }(),
- GrokDefaultTextModel: func() string {
- if req.GrokDefaultTextModel != nil {
- return *req.GrokDefaultTextModel
- }
- return previousSettings.GrokDefaultTextModel
- }(),
- GrokCrossClientModelMapEnabled: func() bool {
- if req.GrokCrossClientModelMapEnabled != nil {
- return *req.GrokCrossClientModelMapEnabled
- }
- return previousSettings.GrokCrossClientModelMapEnabled
- }(),
- GrokDefaultBaseURLMode: func() string {
- if req.GrokDefaultBaseURLMode != nil {
- return strings.TrimSpace(*req.GrokDefaultBaseURLMode)
- }
- return previousSettings.GrokDefaultBaseURLMode
- }(),
- AvailableChannelsEnabled: func() bool {
- if req.AvailableChannelsEnabled != nil {
- return *req.AvailableChannelsEnabled
- }
- return previousSettings.AvailableChannelsEnabled
- }(),
- ModelPlazaEnabled: func() bool {
- if req.ModelPlazaEnabled != nil {
- return *req.ModelPlazaEnabled
- }
- return previousSettings.ModelPlazaEnabled
- }(),
- ModelPlazaRequireAuth: func() bool {
- if req.ModelPlazaRequireAuth != nil {
- return *req.ModelPlazaRequireAuth
- }
- return previousSettings.ModelPlazaRequireAuth
- }(),
- ModelPlazaDescription: func() string {
- if req.ModelPlazaDescription != nil {
- return *req.ModelPlazaDescription
- }
- return previousSettings.ModelPlazaDescription
- }(),
- PluginManagementEnabled: func() bool {
- if req.PluginManagementEnabled != nil {
- return *req.PluginManagementEnabled
- }
- return previousSettings.PluginManagementEnabled
- }(),
- AffiliateEnabled: func() bool {
- if req.AffiliateEnabled != nil {
- return *req.AffiliateEnabled
- }
- return previousSettings.AffiliateEnabled
- }(),
- RiskControlEnabled: func() bool {
- if req.RiskControlEnabled != nil {
- return *req.RiskControlEnabled
- }
- return previousSettings.RiskControlEnabled
- }(),
- CyberSessionBlockEnabled: func() bool {
- if req.CyberSessionBlockEnabled != nil {
- return *req.CyberSessionBlockEnabled
- }
- return previousSettings.CyberSessionBlockEnabled
- }(),
- CyberSessionBlockTTLSeconds: func() int {
- if req.CyberSessionBlockTTLSeconds != nil {
- return *req.CyberSessionBlockTTLSeconds
- }
- return previousSettings.CyberSessionBlockTTLSeconds
- }(),
- }
- // req.AuthSourceXxxPlatformQuotas 为 nil 表示本次请求未包含该 source 的 quota 配置(保留 previousAuthSourceDefaults 中的值);
- // non-nil(含 empty map)表示整体覆盖:empty map = 清空该 source 的所有 quota 配置。
- authSourceDefaults := &service.AuthSourceDefaultSettings{
- Email: service.ProviderDefaultGrantSettings{
- Balance: float64ValueOrDefault(req.AuthSourceDefaultEmailBalance, previousAuthSourceDefaults.Email.Balance),
- Concurrency: intValueOrDefault(req.AuthSourceDefaultEmailConcurrency, previousAuthSourceDefaults.Email.Concurrency),
- Subscriptions: defaultSubscriptionsValueOrDefault(req.AuthSourceDefaultEmailSubscriptions, previousAuthSourceDefaults.Email.Subscriptions),
- GrantOnSignup: boolValueOrDefault(req.AuthSourceDefaultEmailGrantOnSignup, previousAuthSourceDefaults.Email.GrantOnSignup),
- GrantOnFirstBind: boolValueOrDefault(req.AuthSourceDefaultEmailGrantOnFirstBind, previousAuthSourceDefaults.Email.GrantOnFirstBind),
- PlatformQuotas: platformQuotasValueOrDefault(req.AuthSourceEmailPlatformQuotas, previousAuthSourceDefaults.Email.PlatformQuotas),
- },
- LinuxDo: service.ProviderDefaultGrantSettings{
- Balance: float64ValueOrDefault(req.AuthSourceDefaultLinuxDoBalance, previousAuthSourceDefaults.LinuxDo.Balance),
- Concurrency: intValueOrDefault(req.AuthSourceDefaultLinuxDoConcurrency, previousAuthSourceDefaults.LinuxDo.Concurrency),
- Subscriptions: defaultSubscriptionsValueOrDefault(req.AuthSourceDefaultLinuxDoSubscriptions, previousAuthSourceDefaults.LinuxDo.Subscriptions),
- GrantOnSignup: boolValueOrDefault(req.AuthSourceDefaultLinuxDoGrantOnSignup, previousAuthSourceDefaults.LinuxDo.GrantOnSignup),
- GrantOnFirstBind: boolValueOrDefault(req.AuthSourceDefaultLinuxDoGrantOnFirstBind, previousAuthSourceDefaults.LinuxDo.GrantOnFirstBind),
- PlatformQuotas: platformQuotasValueOrDefault(req.AuthSourceLinuxDoPlatformQuotas, previousAuthSourceDefaults.LinuxDo.PlatformQuotas),
- },
- OIDC: service.ProviderDefaultGrantSettings{
- Balance: float64ValueOrDefault(req.AuthSourceDefaultOIDCBalance, previousAuthSourceDefaults.OIDC.Balance),
- Concurrency: intValueOrDefault(req.AuthSourceDefaultOIDCConcurrency, previousAuthSourceDefaults.OIDC.Concurrency),
- Subscriptions: defaultSubscriptionsValueOrDefault(req.AuthSourceDefaultOIDCSubscriptions, previousAuthSourceDefaults.OIDC.Subscriptions),
- GrantOnSignup: boolValueOrDefault(req.AuthSourceDefaultOIDCGrantOnSignup, previousAuthSourceDefaults.OIDC.GrantOnSignup),
- GrantOnFirstBind: boolValueOrDefault(req.AuthSourceDefaultOIDCGrantOnFirstBind, previousAuthSourceDefaults.OIDC.GrantOnFirstBind),
- PlatformQuotas: platformQuotasValueOrDefault(req.AuthSourceOIDCPlatformQuotas, previousAuthSourceDefaults.OIDC.PlatformQuotas),
- },
- WeChat: service.ProviderDefaultGrantSettings{
- Balance: float64ValueOrDefault(req.AuthSourceDefaultWeChatBalance, previousAuthSourceDefaults.WeChat.Balance),
- Concurrency: intValueOrDefault(req.AuthSourceDefaultWeChatConcurrency, previousAuthSourceDefaults.WeChat.Concurrency),
- Subscriptions: defaultSubscriptionsValueOrDefault(req.AuthSourceDefaultWeChatSubscriptions, previousAuthSourceDefaults.WeChat.Subscriptions),
- GrantOnSignup: boolValueOrDefault(req.AuthSourceDefaultWeChatGrantOnSignup, previousAuthSourceDefaults.WeChat.GrantOnSignup),
- GrantOnFirstBind: boolValueOrDefault(req.AuthSourceDefaultWeChatGrantOnFirstBind, previousAuthSourceDefaults.WeChat.GrantOnFirstBind),
- PlatformQuotas: platformQuotasValueOrDefault(req.AuthSourceWeChatPlatformQuotas, previousAuthSourceDefaults.WeChat.PlatformQuotas),
- },
- GitHub: service.ProviderDefaultGrantSettings{
- Balance: float64ValueOrDefault(req.AuthSourceDefaultGitHubBalance, previousAuthSourceDefaults.GitHub.Balance),
- Concurrency: intValueOrDefault(req.AuthSourceDefaultGitHubConcurrency, previousAuthSourceDefaults.GitHub.Concurrency),
- Subscriptions: defaultSubscriptionsValueOrDefault(req.AuthSourceDefaultGitHubSubscriptions, previousAuthSourceDefaults.GitHub.Subscriptions),
- GrantOnSignup: boolValueOrDefault(req.AuthSourceDefaultGitHubGrantOnSignup, previousAuthSourceDefaults.GitHub.GrantOnSignup),
- GrantOnFirstBind: boolValueOrDefault(req.AuthSourceDefaultGitHubGrantOnFirstBind, previousAuthSourceDefaults.GitHub.GrantOnFirstBind),
- PlatformQuotas: platformQuotasValueOrDefault(req.AuthSourceGitHubPlatformQuotas, previousAuthSourceDefaults.GitHub.PlatformQuotas),
- },
- Google: service.ProviderDefaultGrantSettings{
- Balance: float64ValueOrDefault(req.AuthSourceDefaultGoogleBalance, previousAuthSourceDefaults.Google.Balance),
- Concurrency: intValueOrDefault(req.AuthSourceDefaultGoogleConcurrency, previousAuthSourceDefaults.Google.Concurrency),
- Subscriptions: defaultSubscriptionsValueOrDefault(req.AuthSourceDefaultGoogleSubscriptions, previousAuthSourceDefaults.Google.Subscriptions),
- GrantOnSignup: boolValueOrDefault(req.AuthSourceDefaultGoogleGrantOnSignup, previousAuthSourceDefaults.Google.GrantOnSignup),
- GrantOnFirstBind: boolValueOrDefault(req.AuthSourceDefaultGoogleGrantOnFirstBind, previousAuthSourceDefaults.Google.GrantOnFirstBind),
- PlatformQuotas: platformQuotasValueOrDefault(req.AuthSourceGooglePlatformQuotas, previousAuthSourceDefaults.Google.PlatformQuotas),
- },
- DingTalk: service.ProviderDefaultGrantSettings{
- Balance: float64ValueOrDefault(req.AuthSourceDefaultDingTalkBalance, previousAuthSourceDefaults.DingTalk.Balance),
- Concurrency: intValueOrDefault(req.AuthSourceDefaultDingTalkConcurrency, previousAuthSourceDefaults.DingTalk.Concurrency),
- Subscriptions: defaultSubscriptionsValueOrDefault(req.AuthSourceDefaultDingTalkSubscriptions, previousAuthSourceDefaults.DingTalk.Subscriptions),
- GrantOnSignup: boolValueOrDefault(req.AuthSourceDefaultDingTalkGrantOnSignup, previousAuthSourceDefaults.DingTalk.GrantOnSignup),
- GrantOnFirstBind: boolValueOrDefault(req.AuthSourceDefaultDingTalkGrantOnFirstBind, previousAuthSourceDefaults.DingTalk.GrantOnFirstBind),
- PlatformQuotas: platformQuotasValueOrDefault(req.AuthSourceDingTalkPlatformQuotas, previousAuthSourceDefaults.DingTalk.PlatformQuotas),
- },
- ForceEmailOnThirdPartySignup: boolValueOrDefault(req.ForceEmailOnThirdPartySignup, previousAuthSourceDefaults.ForceEmailOnThirdPartySignup),
- }
- if err := h.settingService.UpdateSettingsWithAuthSourceDefaultsOmitting(c.Request.Context(), settings, authSourceDefaults, omitted); err != nil {
- response.ErrorFrom(c, err)
- return
- }
- if h.opsService != nil {
- h.opsService.SetMonitoringEnabled(settings.OpsMonitoringEnabled)
- }
- // Update OpenAI fast policy (stored under dedicated key, only when provided).
- if req.OpenAIFastPolicySettings != nil {
- if err := h.settingService.SetOpenAIFastPolicySettings(c.Request.Context(), openaiFastPolicySettingsFromDTO(req.OpenAIFastPolicySettings)); err != nil {
- response.BadRequest(c, err.Error())
- 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,
- BalanceRechargeMultiplier: req.PaymentBalanceRechargeMultiplier,
- SubscriptionUSDToCNYRate: req.PaymentSubscriptionUSDToCNYRate,
- RechargeFeeRate: req.PaymentRechargeFeeRate,
- 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,
- AlipayForceQRCode: req.PaymentAlipayForceQRCode,
- AlipayMobilePrecreateDeepLink: req.PaymentAlipayMobilePrecreateDeepLink,
- }
- 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, previousAuthSourceDefaults, authSourceDefaults, auditReq)
- // 重新获取设置返回
- updatedSettings, err := h.settingService.GetAllSettings(c.Request.Context())
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- h.ensureDingTalkSyncAttributes(c.Request.Context(), updatedSettings)
- updatedAuthSourceDefaults, err := h.settingService.GetAuthSourceDefaultSettings(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{}
- }
- passkeyConfigured, passkeyRPID, passkeyRPOrigins := h.settingService.PasskeyConfiguration()
- payload := dto.SystemSettings{
- RegistrationEnabled: updatedSettings.RegistrationEnabled,
- EmailVerifyEnabled: updatedSettings.EmailVerifyEnabled,
- RegistrationEmailSuffixWhitelist: updatedSettings.RegistrationEmailSuffixWhitelist,
- RegistrationEmailDomainQuotaEnabled: updatedSettings.RegistrationEmailDomainQuotaEnabled,
- PromoCodeEnabled: updatedSettings.PromoCodeEnabled,
- PasswordResetEnabled: updatedSettings.PasswordResetEnabled,
- FrontendURL: updatedSettings.FrontendURL,
- InvitationCodeEnabled: updatedSettings.InvitationCodeEnabled,
- TotpEnabled: updatedSettings.TotpEnabled,
- TotpEncryptionKeyConfigured: h.settingService.IsTotpEncryptionKeyConfigured(),
- PasskeyEnabled: updatedSettings.PasskeyEnabled,
- PasskeyConfigured: passkeyConfigured,
- PasskeyRPID: passkeyRPID,
- PasskeyRPOrigins: passkeyRPOrigins,
- SessionBindingEnabled: updatedSettings.SessionBindingEnabled,
- StepUpEnabled: updatedSettings.StepUpEnabled,
- AuditLogRetentionDays: updatedSettings.AuditLogRetentionDays,
- LoginAgreementEnabled: updatedSettings.LoginAgreementEnabled,
- LoginAgreementMode: updatedSettings.LoginAgreementMode,
- LoginAgreementUpdatedAt: updatedSettings.LoginAgreementUpdatedAt,
- LoginAgreementDocuments: loginAgreementDocumentsToDTO(updatedSettings.LoginAgreementDocuments),
- 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,
- TencentCaptchaEnabled: updatedSettings.TencentCaptchaEnabled,
- TencentCaptchaAppID: updatedSettings.TencentCaptchaAppID,
- TencentCaptchaAppSecretKeyConfigured: updatedSettings.TencentCaptchaAppSecretKeyConfigured,
- TencentCaptchaCloudSecretIDConfigured: updatedSettings.TencentCaptchaCloudSecretIDConfigured,
- TencentCaptchaCloudSecretKeyConfigured: updatedSettings.TencentCaptchaCloudSecretKeyConfigured,
- TencentCaptchaRegion: updatedSettings.TencentCaptchaRegion,
- AliyunCaptchaEnabled: updatedSettings.AliyunCaptchaEnabled,
- AliyunCaptchaAccessKeyID: updatedSettings.AliyunCaptchaAccessKeyID,
- AliyunCaptchaAccessKeySecretConfigured: updatedSettings.AliyunCaptchaAccessKeySecretConfigured,
- AliyunCaptchaSceneID: updatedSettings.AliyunCaptchaSceneID,
- AliyunCaptchaPrefix: updatedSettings.AliyunCaptchaPrefix,
- AliyunCaptchaRegion: updatedSettings.AliyunCaptchaRegion,
- APIKeyACLTrustForwardedIP: updatedSettings.APIKeyACLTrustForwardedIP,
- ForwardedClientIPHeaders: updatedSettings.ForwardedClientIPHeaders,
- LinuxDoConnectEnabled: updatedSettings.LinuxDoConnectEnabled,
- LinuxDoConnectClientID: updatedSettings.LinuxDoConnectClientID,
- LinuxDoConnectClientSecretConfigured: updatedSettings.LinuxDoConnectClientSecretConfigured,
- LinuxDoConnectRedirectURL: updatedSettings.LinuxDoConnectRedirectURL,
- DingTalkConnectEnabled: updatedSettings.DingTalkConnectEnabled,
- DingTalkConnectClientID: updatedSettings.DingTalkConnectClientID,
- DingTalkConnectClientSecretConfigured: updatedSettings.DingTalkConnectClientSecretConfigured,
- DingTalkConnectRedirectURL: updatedSettings.DingTalkConnectRedirectURL,
- DingTalkConnectCorpRestrictionPolicy: updatedSettings.DingTalkConnectCorpRestrictionPolicy,
- DingTalkConnectInternalCorpID: updatedSettings.DingTalkConnectInternalCorpID,
- DingTalkConnectBypassRegistration: updatedSettings.DingTalkConnectBypassRegistration,
- DingTalkConnectSyncCorpEmail: updatedSettings.DingTalkConnectSyncCorpEmail,
- DingTalkConnectSyncDisplayName: updatedSettings.DingTalkConnectSyncDisplayName,
- DingTalkConnectSyncDept: updatedSettings.DingTalkConnectSyncDept,
- DingTalkConnectSyncCorpEmailAttrKey: updatedSettings.DingTalkConnectSyncCorpEmailAttrKey,
- DingTalkConnectSyncDisplayNameAttrKey: updatedSettings.DingTalkConnectSyncDisplayNameAttrKey,
- DingTalkConnectSyncDeptAttrKey: updatedSettings.DingTalkConnectSyncDeptAttrKey,
- DingTalkConnectSyncCorpEmailAttrName: updatedSettings.DingTalkConnectSyncCorpEmailAttrName,
- DingTalkConnectSyncDisplayNameAttrName: updatedSettings.DingTalkConnectSyncDisplayNameAttrName,
- DingTalkConnectSyncDeptAttrName: updatedSettings.DingTalkConnectSyncDeptAttrName,
- WeChatConnectEnabled: updatedSettings.WeChatConnectEnabled,
- WeChatConnectAppID: updatedSettings.WeChatConnectAppID,
- WeChatConnectAppSecretConfigured: updatedSettings.WeChatConnectAppSecretConfigured,
- WeChatConnectOpenAppID: updatedSettings.WeChatConnectOpenAppID,
- WeChatConnectOpenAppSecretConfigured: updatedSettings.WeChatConnectOpenAppSecretConfigured,
- WeChatConnectMPAppID: updatedSettings.WeChatConnectMPAppID,
- WeChatConnectMPAppSecretConfigured: updatedSettings.WeChatConnectMPAppSecretConfigured,
- WeChatConnectMobileAppID: updatedSettings.WeChatConnectMobileAppID,
- WeChatConnectMobileAppSecretConfigured: updatedSettings.WeChatConnectMobileAppSecretConfigured,
- WeChatConnectOpenEnabled: updatedSettings.WeChatConnectOpenEnabled,
- WeChatConnectMPEnabled: updatedSettings.WeChatConnectMPEnabled,
- WeChatConnectMobileEnabled: updatedSettings.WeChatConnectMobileEnabled,
- WeChatConnectMode: updatedSettings.WeChatConnectMode,
- WeChatConnectScopes: updatedSettings.WeChatConnectScopes,
- WeChatConnectRedirectURL: updatedSettings.WeChatConnectRedirectURL,
- WeChatConnectFrontendRedirectURL: updatedSettings.WeChatConnectFrontendRedirectURL,
- 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,
- GitHubOAuthEnabled: updatedSettings.GitHubOAuthEnabled,
- GitHubOAuthClientID: updatedSettings.GitHubOAuthClientID,
- GitHubOAuthClientSecretConfigured: updatedSettings.GitHubOAuthClientSecretConfigured,
- GitHubOAuthRedirectURL: updatedSettings.GitHubOAuthRedirectURL,
- GitHubOAuthFrontendRedirectURL: updatedSettings.GitHubOAuthFrontendRedirectURL,
- GoogleOAuthEnabled: updatedSettings.GoogleOAuthEnabled,
- GoogleOAuthClientID: updatedSettings.GoogleOAuthClientID,
- GoogleOAuthClientSecretConfigured: updatedSettings.GoogleOAuthClientSecretConfigured,
- GoogleOAuthRedirectURL: updatedSettings.GoogleOAuthRedirectURL,
- GoogleOAuthFrontendRedirectURL: updatedSettings.GoogleOAuthFrontendRedirectURL,
- SiteName: updatedSettings.SiteName,
- SiteLogo: updatedSettings.SiteLogo,
- SiteSubtitle: updatedSettings.SiteSubtitle,
- APIBaseURL: updatedSettings.APIBaseURL,
- ContactInfo: updatedSettings.ContactInfo,
- DocURL: updatedSettings.DocURL,
- HomeContent: updatedSettings.HomeContent,
- CompactHomeEnabled: updatedSettings.CompactHomeEnabled,
- 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,
- AffiliateRebateRate: updatedSettings.AffiliateRebateRate,
- AffiliateRebateFreezeHours: updatedSettings.AffiliateRebateFreezeHours,
- AffiliateRebateDurationDays: updatedSettings.AffiliateRebateDurationDays,
- AffiliateRebatePerInviteeCap: updatedSettings.AffiliateRebatePerInviteeCap,
- AdminRechargeRebateEnabled: updatedSettings.AdminRechargeRebateEnabled,
- DefaultUserRPMLimit: updatedSettings.DefaultUserRPMLimit,
- 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,
- EnableClaudeOAuthSystemPromptInjection: updatedSettings.EnableClaudeOAuthSystemPromptInjection,
- ClaudeOAuthSystemPrompt: updatedSettings.ClaudeOAuthSystemPrompt,
- ClaudeOAuthSystemPromptBlocks: updatedSettings.ClaudeOAuthSystemPromptBlocks,
- EnableAnthropicCacheTTL1hInjection: updatedSettings.EnableAnthropicCacheTTL1hInjection,
- RewriteMessageCacheControl: updatedSettings.RewriteMessageCacheControl,
- EnableClientDatelineNormalization: updatedSettings.EnableClientDatelineNormalization,
- AntigravityUserAgentVersion: updatedSettings.AntigravityUserAgentVersion,
- OpenAICodexUserAgent: updatedSettings.OpenAICodexUserAgent,
- OpenAICodexClientVersion: updatedSettings.OpenAICodexClientVersion,
- OpenAICodexClientVersionSynced: updatedSettings.OpenAICodexClientVersionSynced,
- OpenAICodexVersionAutoSyncEnabled: updatedSettings.OpenAICodexVersionAutoSyncEnabled,
- MinCodexVersion: updatedSettings.MinCodexVersion,
- MaxCodexVersion: updatedSettings.MaxCodexVersion,
- CodexCLIOnlyBlacklist: updatedSettings.CodexCLIOnlyBlacklist,
- CodexCLIOnlyWhitelist: updatedSettings.CodexCLIOnlyWhitelist,
- CodexCLIOnlyAllowAppServerClients: updatedSettings.CodexCLIOnlyAllowAppServerClients,
- CodexCLIOnlyEngineFingerprintSignals: updatedSettings.CodexCLIOnlyEngineFingerprintSignals,
- PaymentVisibleMethodAlipaySource: updatedSettings.PaymentVisibleMethodAlipaySource,
- PaymentVisibleMethodWxpaySource: updatedSettings.PaymentVisibleMethodWxpaySource,
- PaymentVisibleMethodAlipayEnabled: updatedSettings.PaymentVisibleMethodAlipayEnabled,
- PaymentVisibleMethodWxpayEnabled: updatedSettings.PaymentVisibleMethodWxpayEnabled,
- OpenAILowUpstreamRatePriorityEnabled: updatedSettings.OpenAILowUpstreamRatePriorityEnabled,
- OpenAIOAuthSchedulingRateMultiplier: updatedSettings.OpenAIOAuthSchedulingRateMultiplier,
- OpenAIAdvancedSchedulerEnabled: updatedSettings.OpenAIAdvancedSchedulerEnabled,
- OpenAIAdvancedSchedulerStickyWeightedEnabled: updatedSettings.OpenAIAdvancedSchedulerStickyWeightedEnabled,
- OpenAIAdvancedSchedulerSubscriptionPriorityEnabled: updatedSettings.OpenAIAdvancedSchedulerSubscriptionPriorityEnabled,
- OpenAIAdvancedSchedulerLBTopK: updatedSettings.OpenAIAdvancedSchedulerLBTopK,
- OpenAIAdvancedSchedulerWeightPriority: updatedSettings.OpenAIAdvancedSchedulerWeightPriority,
- OpenAIAdvancedSchedulerWeightLoad: updatedSettings.OpenAIAdvancedSchedulerWeightLoad,
- OpenAIAdvancedSchedulerWeightQueue: updatedSettings.OpenAIAdvancedSchedulerWeightQueue,
- OpenAIAdvancedSchedulerWeightErrorRate: updatedSettings.OpenAIAdvancedSchedulerWeightErrorRate,
- OpenAIAdvancedSchedulerWeightTTFT: updatedSettings.OpenAIAdvancedSchedulerWeightTTFT,
- OpenAIAdvancedSchedulerWeightReset: updatedSettings.OpenAIAdvancedSchedulerWeightReset,
- OpenAIAdvancedSchedulerWeightQuotaHeadroom: updatedSettings.OpenAIAdvancedSchedulerWeightQuotaHeadroom,
- OpenAIAdvancedSchedulerWeightUpstreamCost: updatedSettings.OpenAIAdvancedSchedulerWeightUpstreamCost,
- OpenAIAdvancedSchedulerWeightPreviousResponse: updatedSettings.OpenAIAdvancedSchedulerWeightPreviousResponse,
- OpenAIAdvancedSchedulerWeightSessionSticky: updatedSettings.OpenAIAdvancedSchedulerWeightSessionSticky,
- OpenAIAdvancedSchedulerEffectiveLBTopK: updatedSettings.OpenAIAdvancedSchedulerEffectiveLBTopK,
- OpenAIAdvancedSchedulerEffectiveWeightPriority: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightPriority,
- OpenAIAdvancedSchedulerEffectiveWeightLoad: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightLoad,
- OpenAIAdvancedSchedulerEffectiveWeightQueue: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightQueue,
- OpenAIAdvancedSchedulerEffectiveWeightErrorRate: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightErrorRate,
- OpenAIAdvancedSchedulerEffectiveWeightTTFT: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightTTFT,
- OpenAIAdvancedSchedulerEffectiveWeightReset: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightReset,
- OpenAIAdvancedSchedulerEffectiveWeightQuotaHeadroom: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightQuotaHeadroom,
- OpenAIAdvancedSchedulerEffectiveWeightUpstreamCost: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightUpstreamCost,
- OpenAIAdvancedSchedulerEffectiveWeightPreviousResponse: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightPreviousResponse,
- OpenAIAdvancedSchedulerEffectiveWeightSessionSticky: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightSessionSticky,
- BalanceLowNotifyEnabled: updatedSettings.BalanceLowNotifyEnabled,
- BalanceLowNotifyThreshold: updatedSettings.BalanceLowNotifyThreshold,
- BalanceLowNotifyRechargeURL: updatedSettings.BalanceLowNotifyRechargeURL,
- SubscriptionExpiryNotifyEnabled: updatedSettings.SubscriptionExpiryNotifyEnabled,
- 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,
- PaymentBalanceRechargeMultiplier: updatedPaymentCfg.BalanceRechargeMultiplier,
- PaymentSubscriptionUSDToCNYRate: updatedPaymentCfg.SubscriptionUSDToCNYRate,
- PaymentRechargeFeeRate: updatedPaymentCfg.RechargeFeeRate,
- 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,
- PaymentAlipayForceQRCode: updatedPaymentCfg.AlipayForceQRCode,
- PaymentAlipayMobilePrecreateDeepLink: updatedPaymentCfg.AlipayMobilePrecreateDeepLink,
- ChannelMonitorEnabled: updatedSettings.ChannelMonitorEnabled,
- ChannelMonitorMode: updatedSettings.ChannelMonitorMode,
- ChannelMonitorDefaultIntervalSeconds: updatedSettings.ChannelMonitorDefaultIntervalSeconds,
- ChannelMonitorHideThroughput: updatedSettings.ChannelMonitorHideThroughput,
- ChannelMonitorShowQuota: updatedSettings.ChannelMonitorShowQuota,
- ChannelMonitorHideUserRanking: updatedSettings.ChannelMonitorHideUserRanking,
- GrokDefaultTextModel: updatedSettings.GrokDefaultTextModel,
- GrokCrossClientModelMapEnabled: updatedSettings.GrokCrossClientModelMapEnabled,
- GrokDefaultBaseURLMode: updatedSettings.GrokDefaultBaseURLMode,
- AvailableChannelsEnabled: updatedSettings.AvailableChannelsEnabled,
- ModelPlazaEnabled: updatedSettings.ModelPlazaEnabled,
- ModelPlazaRequireAuth: updatedSettings.ModelPlazaRequireAuth,
- ModelPlazaDescription: updatedSettings.ModelPlazaDescription,
- PluginManagementEnabled: updatedSettings.PluginManagementEnabled,
- AffiliateEnabled: updatedSettings.AffiliateEnabled,
- RiskControlEnabled: updatedSettings.RiskControlEnabled,
- CyberSessionBlockEnabled: updatedSettings.CyberSessionBlockEnabled,
- CyberSessionBlockTTLSeconds: updatedSettings.CyberSessionBlockTTLSeconds,
- AccountSchedulingThresholds: updatedSettings.AccountSchedulingThresholds,
- AllowUserViewErrorRequests: updatedSettings.AllowUserViewErrorRequests,
- }
- if fastPolicy, err := h.settingService.GetOpenAIFastPolicySettings(c.Request.Context()); err != nil {
- slog.Error("openai_fast_policy_settings_get_failed", "error", err)
- } else if fastPolicy != nil {
- payload.OpenAIFastPolicySettings = openaiFastPolicySettingsToDTO(fastPolicy)
- }
- // Default platform quotas(JSON map)—— 与 GetSettings 一致,避免保存后响应缺失该字段
- if platformQuotas, err := h.settingService.GetDefaultPlatformQuotas(c.Request.Context()); err != nil {
- slog.Error("default_platform_quotas_get_failed", "error", err)
- } else {
- payload.DefaultPlatformQuotas = platformQuotas
- }
- response.Success(c, systemSettingsResponseData(payload, updatedAuthSourceDefaults))
- }
- // hasPaymentFields returns true if any payment-related field was explicitly provided.
- // mapDingTalkValidateError maps ValidateDingTalkConfig errors to machine-readable reason codes.
- func mapDingTalkValidateError(err error) string {
- switch {
- case errors.Is(err, config.ErrDingTalkV1AppTypeMismatch):
- return "dingtalk_apptype_mismatch"
- case errors.Is(err, config.ErrDingTalkV4InvalidAppKind):
- return "dingtalk_app_kind_invalid"
- default:
- return "dingtalk_corp_config_invalid"
- }
- }
- 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.PaymentBalanceRechargeMultiplier != nil || req.PaymentSubscriptionUSDToCNYRate != nil ||
- req.PaymentRechargeFeeRate != 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 ||
- req.PaymentAlipayForceQRCode != nil || req.PaymentAlipayMobilePrecreateDeepLink != nil
- }
- // ensureDingTalkSyncAttributes 在保存 settings 后,按 admin 配置的 (attr key, attr name)
- // 兜底 upsert 对应 user attribute definition:不存在则创建;存在但 name 不同则更新 name
- // (type/options/required 不变)。仅 internal_only + 对应 sync 开关开启时执行。
- // 失败仅记录日志,不阻塞 settings 保存。
- func (h *SettingHandler) ensureDingTalkSyncAttributes(ctx context.Context, settings *service.SystemSettings) {
- if h.userAttributeService == nil || settings == nil {
- return
- }
- if settings.DingTalkConnectCorpRestrictionPolicy != "internal_only" {
- return
- }
- if settings.DingTalkConnectSyncDisplayName {
- h.ensureUserAttributeDefinition(ctx, settings.DingTalkConnectSyncDisplayNameAttrKey, settings.DingTalkConnectSyncDisplayNameAttrName, "钉钉 internal_only 登录时同步的钉钉姓名", service.AttributeTypeText)
- }
- if settings.DingTalkConnectSyncCorpEmail {
- h.ensureUserAttributeDefinition(ctx, settings.DingTalkConnectSyncCorpEmailAttrKey, settings.DingTalkConnectSyncCorpEmailAttrName, "钉钉 internal_only 登录时同步的企业邮箱", service.AttributeTypeEmail)
- }
- if settings.DingTalkConnectSyncDept {
- h.ensureUserAttributeDefinition(ctx, settings.DingTalkConnectSyncDeptAttrKey, settings.DingTalkConnectSyncDeptAttrName, "钉钉 internal_only 登录时同步的完整部门路径(如:公司/研发部)", service.AttributeTypeText)
- }
- }
- func (h *SettingHandler) ensureUserAttributeDefinition(ctx context.Context, key, name, description string, attrType service.UserAttributeType) {
- key = strings.TrimSpace(key)
- if key == "" {
- return
- }
- existing, err := h.userAttributeService.GetDefinitionByKey(ctx, key)
- if err == nil && existing != nil {
- if strings.TrimSpace(name) != "" && existing.Name != name {
- if _, err := h.userAttributeService.UpdateDefinition(ctx, existing.ID, service.UpdateAttributeDefinitionInput{
- Name: &name,
- }); err != nil {
- slog.Warn("dingtalk: update user attribute definition name failed", "key", key, "err", err.Error())
- return
- }
- slog.Info("dingtalk: updated user attribute definition name", "key", key, "name", name)
- }
- return
- }
- if _, err := h.userAttributeService.CreateDefinition(ctx, service.CreateAttributeDefinitionInput{
- Key: key,
- Name: name,
- Description: description,
- Type: attrType,
- Enabled: true,
- }); err != nil {
- slog.Warn("dingtalk: ensure user attribute definition failed", "key", key, "err", err.Error())
- return
- }
- slog.Info("dingtalk: created user attribute definition", "key", key, "name", name, "type", attrType)
- }
|