config.go 188 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863
  1. // Package config provides configuration loading, defaults, and validation.
  2. package config
  3. import (
  4. "crypto/rand"
  5. "encoding/hex"
  6. "fmt"
  7. "log/slog"
  8. "math"
  9. "net/textproto"
  10. "net/url"
  11. "os"
  12. "strings"
  13. "sync/atomic"
  14. "time"
  15. "github.com/spf13/viper"
  16. "golang.org/x/net/http/httpguts"
  17. )
  18. const (
  19. RunModeStandard = "standard"
  20. RunModeSimple = "simple"
  21. )
  22. // 使用量记录队列溢出策略
  23. const (
  24. UsageRecordOverflowPolicyDrop = "drop"
  25. UsageRecordOverflowPolicySample = "sample"
  26. UsageRecordOverflowPolicySync = "sync"
  27. )
  28. // DefaultCSPPolicy is the default Content-Security-Policy with nonce support
  29. // __CSP_NONCE__ will be replaced with actual nonce at request time by the SecurityHeaders middleware
  30. const DefaultCSPPolicy = "default-src 'self'; worker-src 'self' blob:; script-src 'self' __CSP_NONCE__ https://challenges.cloudflare.com https://*.alicdn.com https://static.cloudflareinsights.com https://turing.captcha.qcloud.com https://turing.captcha.gtimg.com https://ca.turing.captcha.qcloud.com https://global.turing.captcha.gtimg.com https://www.tycaptcha.com https://cloudcache.tencentcs.com https://*.stripe.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; style-src 'self' 'unsafe-inline' https://*.captcha.gtimg.com https://fonts.googleapis.com https://*.alicdn.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; img-src 'self' data: blob: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://turing.captcha.qcloud.com https://www.tycaptcha.com https://rce.tencentrio.com https:; frame-src 'self' https://challenges.cloudflare.com https://turing.captcha.qcloud.com https://ca.turing.captcha.qcloud.com https://www.tycaptcha.com https://*.stripe.com https://checkout.airwallex.com https://checkout-demo.airwallex.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
  31. // UMQ(用户消息队列)模式常量
  32. const (
  33. // UMQModeSerialize: 账号级串行锁 + RPM 自适应延迟
  34. UMQModeSerialize = "serialize"
  35. // UMQModeThrottle: 仅 RPM 自适应前置延迟,不阻塞并发
  36. UMQModeThrottle = "throttle"
  37. )
  38. // 连接池隔离策略常量
  39. // 用于控制上游 HTTP 连接池的隔离粒度,影响连接复用和资源消耗
  40. const (
  41. // ConnectionPoolIsolationProxy: 按代理隔离
  42. // 同一代理地址共享连接池,适合代理数量少、账户数量多的场景
  43. ConnectionPoolIsolationProxy = "proxy"
  44. // ConnectionPoolIsolationAccount: 按账户隔离
  45. // 每个账户独立连接池,适合账户数量少、需要严格隔离的场景
  46. ConnectionPoolIsolationAccount = "account"
  47. // ConnectionPoolIsolationAccountProxy: 按账户+代理组合隔离(默认)
  48. // 同一账户+代理组合共享连接池,提供最细粒度的隔离
  49. ConnectionPoolIsolationAccountProxy = "account_proxy"
  50. )
  51. // DefaultUpstreamResponseReadMaxBytes 上游非流式响应体的默认读取上限。
  52. // 128 MB 可容纳 2-3 张 4K PNG(base64 膨胀 33%,单张 4K PNG 最坏约 67MB base64)。
  53. // 可通过 gateway.upstream_response_read_max_bytes 配置项覆盖。
  54. const DefaultUpstreamResponseReadMaxBytes int64 = 128 * 1024 * 1024
  55. // DefaultModelsListReadMaxBytes 上游模型列表响应体的默认读取上限。
  56. // 可通过 gateway.models_list_read_max_bytes 配置项覆盖。
  57. const DefaultModelsListReadMaxBytes int64 = 8 * 1024 * 1024
  58. type Config struct {
  59. Server ServerConfig `mapstructure:"server"`
  60. Log LogConfig `mapstructure:"log"`
  61. CORS CORSConfig `mapstructure:"cors"`
  62. Security SecurityConfig `mapstructure:"security"`
  63. Billing BillingConfig `mapstructure:"billing"`
  64. Turnstile TurnstileConfig `mapstructure:"turnstile"`
  65. Database DatabaseConfig `mapstructure:"database"`
  66. Redis RedisConfig `mapstructure:"redis"`
  67. Ops OpsConfig `mapstructure:"ops"`
  68. JWT JWTConfig `mapstructure:"jwt"`
  69. Totp TotpConfig `mapstructure:"totp"`
  70. WebAuthn WebAuthnConfig `mapstructure:"webauthn"`
  71. LinuxDo LinuxDoConnectConfig `mapstructure:"linuxdo_connect"`
  72. WeChat WeChatConnectConfig `mapstructure:"wechat_connect"`
  73. OIDC OIDCConnectConfig `mapstructure:"oidc_connect"`
  74. DingTalk DingTalkConnectConfig `mapstructure:"dingtalk_connect"`
  75. GitHubOAuth EmailOAuthProviderConfig `mapstructure:"github_oauth"`
  76. GoogleOAuth EmailOAuthProviderConfig `mapstructure:"google_oauth"`
  77. Default DefaultConfig `mapstructure:"default"`
  78. RateLimit RateLimitConfig `mapstructure:"rate_limit"`
  79. Pricing PricingConfig `mapstructure:"pricing"`
  80. Gateway GatewayConfig `mapstructure:"gateway"`
  81. APIKeyAuth APIKeyAuthCacheConfig `mapstructure:"api_key_auth_cache"`
  82. SubscriptionCache SubscriptionCacheConfig `mapstructure:"subscription_cache"`
  83. SubscriptionMaintenance SubscriptionMaintenanceConfig `mapstructure:"subscription_maintenance"`
  84. Dashboard DashboardCacheConfig `mapstructure:"dashboard_cache"`
  85. DashboardAgg DashboardAggregationConfig `mapstructure:"dashboard_aggregation"`
  86. UsageCleanup UsageCleanupConfig `mapstructure:"usage_cleanup"`
  87. Concurrency ConcurrencyConfig `mapstructure:"concurrency"`
  88. TokenRefresh TokenRefreshConfig `mapstructure:"token_refresh"`
  89. RunMode string `mapstructure:"run_mode" yaml:"run_mode"`
  90. Timezone string `mapstructure:"timezone"` // e.g. "Asia/Shanghai", "UTC"
  91. Gemini GeminiConfig `mapstructure:"gemini"`
  92. Update UpdateConfig `mapstructure:"update"`
  93. Idempotency IdempotencyConfig `mapstructure:"idempotency"`
  94. BatchImage BatchImageConfig `mapstructure:"batch_image"`
  95. ImageStorage ImageStorageConfig `mapstructure:"image_storage"`
  96. Plugins PluginConfig `mapstructure:"plugins"`
  97. }
  98. // PluginConfig 控制管理员手动上传的本地进程插件。
  99. // 默认不包含插件,也不允许安装未签名插件;TrustedPublishers 用于追加第三方发布者。
  100. type PluginConfig struct {
  101. DataDir string `mapstructure:"data_dir"`
  102. AllowUnsigned bool `mapstructure:"allow_unsigned"`
  103. TrustedPublishers map[string]string `mapstructure:"trusted_publishers"`
  104. MaxUploadBytes int64 `mapstructure:"max_upload_bytes"`
  105. MaxUncompressedBytes int64 `mapstructure:"max_uncompressed_bytes"`
  106. StartTimeoutSeconds int `mapstructure:"start_timeout_seconds"`
  107. }
  108. type LogConfig struct {
  109. Level string `mapstructure:"level"`
  110. Format string `mapstructure:"format"`
  111. ServiceName string `mapstructure:"service_name"`
  112. Environment string `mapstructure:"env"`
  113. Caller bool `mapstructure:"caller"`
  114. StacktraceLevel string `mapstructure:"stacktrace_level"`
  115. Output LogOutputConfig `mapstructure:"output"`
  116. Rotation LogRotationConfig `mapstructure:"rotation"`
  117. Sampling LogSamplingConfig `mapstructure:"sampling"`
  118. }
  119. type LogOutputConfig struct {
  120. ToStdout bool `mapstructure:"to_stdout"`
  121. ToFile bool `mapstructure:"to_file"`
  122. FilePath string `mapstructure:"file_path"`
  123. }
  124. type LogRotationConfig struct {
  125. MaxSizeMB int `mapstructure:"max_size_mb"`
  126. MaxBackups int `mapstructure:"max_backups"`
  127. MaxAgeDays int `mapstructure:"max_age_days"`
  128. Compress bool `mapstructure:"compress"`
  129. LocalTime bool `mapstructure:"local_time"`
  130. }
  131. type LogSamplingConfig struct {
  132. Enabled bool `mapstructure:"enabled"`
  133. Initial int `mapstructure:"initial"`
  134. Thereafter int `mapstructure:"thereafter"`
  135. }
  136. type GeminiConfig struct {
  137. OAuth GeminiOAuthConfig `mapstructure:"oauth"`
  138. Quota GeminiQuotaConfig `mapstructure:"quota"`
  139. }
  140. type GeminiOAuthConfig struct {
  141. ClientID string `mapstructure:"client_id"`
  142. ClientSecret string `mapstructure:"client_secret"`
  143. Scopes string `mapstructure:"scopes"`
  144. }
  145. type GeminiQuotaConfig struct {
  146. Tiers map[string]GeminiTierQuotaConfig `mapstructure:"tiers"`
  147. Policy string `mapstructure:"policy"`
  148. }
  149. type GeminiTierQuotaConfig struct {
  150. ProRPD *int64 `mapstructure:"pro_rpd" json:"pro_rpd"`
  151. FlashRPD *int64 `mapstructure:"flash_rpd" json:"flash_rpd"`
  152. CooldownMinutes *int `mapstructure:"cooldown_minutes" json:"cooldown_minutes"`
  153. }
  154. type UpdateConfig struct {
  155. // ProxyURL 用于访问 GitHub 的代理地址
  156. // 支持 http/https/socks5/socks5h 协议
  157. // 例如: "http://127.0.0.1:7890", "socks5://127.0.0.1:1080"
  158. ProxyURL string `mapstructure:"proxy_url"`
  159. }
  160. type IdempotencyConfig struct {
  161. // ObserveOnly 为 true 时处于观察期:未携带 Idempotency-Key 的请求继续放行。
  162. ObserveOnly bool `mapstructure:"observe_only"`
  163. // DefaultTTLSeconds 关键写接口的幂等记录默认 TTL(秒)。
  164. DefaultTTLSeconds int `mapstructure:"default_ttl_seconds"`
  165. // SystemOperationTTLSeconds 系统操作接口的幂等记录 TTL(秒)。
  166. SystemOperationTTLSeconds int `mapstructure:"system_operation_ttl_seconds"`
  167. // ProcessingTimeoutSeconds processing 状态锁超时(秒)。
  168. ProcessingTimeoutSeconds int `mapstructure:"processing_timeout_seconds"`
  169. // FailedRetryBackoffSeconds 失败退避窗口(秒)。
  170. FailedRetryBackoffSeconds int `mapstructure:"failed_retry_backoff_seconds"`
  171. // MaxStoredResponseLen 持久化响应体最大长度(字节)。
  172. MaxStoredResponseLen int `mapstructure:"max_stored_response_len"`
  173. // CleanupIntervalSeconds 过期记录清理周期(秒)。
  174. CleanupIntervalSeconds int `mapstructure:"cleanup_interval_seconds"`
  175. // CleanupBatchSize 每次清理的最大记录数。
  176. CleanupBatchSize int `mapstructure:"cleanup_batch_size"`
  177. }
  178. type BatchImageConfig struct {
  179. Enabled bool `mapstructure:"enabled"`
  180. MaxItemsPerJobDefault int `mapstructure:"max_items_per_job_default"`
  181. MaxItemsPerJobTrial int `mapstructure:"max_items_per_job_trial"`
  182. MaxOutputImagesPerJob int `mapstructure:"max_output_images_per_job"`
  183. MaxOutputImagesPerItem int `mapstructure:"max_output_images_per_item"`
  184. MaxPromptCharsPerItem int `mapstructure:"max_prompt_chars_per_item"`
  185. MaxReferenceImagesPerJob int `mapstructure:"max_reference_images_per_job"`
  186. MaxReferenceInlineBytesPerJob int `mapstructure:"max_reference_inline_bytes_per_job"`
  187. DefaultResponseMimeType string `mapstructure:"default_response_mime_type"`
  188. DefaultImageSize string `mapstructure:"default_image_size"`
  189. MaxDownloadItemsZip int `mapstructure:"max_download_items_zip"`
  190. MaxDownloadBytesPerRequest int64 `mapstructure:"max_download_bytes_per_request"`
  191. MaxDownloadDurationSeconds int `mapstructure:"max_download_duration_seconds"`
  192. MaxDownloadConcurrencyPerUser int `mapstructure:"max_download_concurrency_per_user"`
  193. InputRetentionAfterTerminalHours int `mapstructure:"input_retention_after_terminal_hours"`
  194. OutputRetentionAfterTerminalHours int `mapstructure:"output_retention_after_terminal_hours"`
  195. OutputRetentionMaxDays int `mapstructure:"output_retention_max_days"`
  196. CleanupIntervalMinutes int `mapstructure:"cleanup_interval_minutes"`
  197. CleanupBatchSize int `mapstructure:"cleanup_batch_size"`
  198. QueueEnabled bool `mapstructure:"queue_enabled"`
  199. QueueReadyKey string `mapstructure:"queue_ready_key"`
  200. QueueDelayedKey string `mapstructure:"queue_delayed_key"`
  201. QueueActiveKey string `mapstructure:"queue_active_key"`
  202. InflightKeyPrefix string `mapstructure:"inflight_key_prefix"`
  203. LockKeyPrefix string `mapstructure:"lock_key_prefix"`
  204. IdempotencyKeyPrefix string `mapstructure:"idempotency_key_prefix"`
  205. InflightTTLSeconds int `mapstructure:"inflight_ttl_seconds"`
  206. JobLockTTLSeconds int `mapstructure:"job_lock_ttl_seconds"`
  207. DefaultRequeueDelaySeconds int `mapstructure:"default_requeue_delay_seconds"`
  208. ErrorRetryDelaySeconds int `mapstructure:"error_retry_delay_seconds"`
  209. LockConflictDelaySeconds int `mapstructure:"lock_conflict_delay_seconds"`
  210. StaleActiveAfterSeconds int `mapstructure:"stale_active_after_seconds"`
  211. DelayedMoverIntervalSeconds int `mapstructure:"delayed_mover_interval_seconds"`
  212. RecoveryIntervalSeconds int `mapstructure:"recovery_interval_seconds"`
  213. DelayedMoveLimit int `mapstructure:"delayed_move_limit"`
  214. RecoverLimit int `mapstructure:"recover_limit"`
  215. VertexEnabled bool `mapstructure:"vertex_enabled"`
  216. VertexProjectID string `mapstructure:"vertex_project_id"`
  217. VertexLocation string `mapstructure:"vertex_location"`
  218. // VertexManagedGCSBucket is a server-owned bucket for batch JSONL input/output.
  219. // Disable Cloud Storage soft delete on this bucket to avoid retaining deleted batch objects.
  220. VertexManagedGCSBucket string `mapstructure:"vertex_managed_gcs_bucket"`
  221. VertexManagedGCSPrefix string `mapstructure:"vertex_managed_gcs_prefix"`
  222. VertexInputRetentionHours int `mapstructure:"vertex_input_retention_hours"`
  223. VertexOutputRetentionHours int `mapstructure:"vertex_output_retention_hours"`
  224. VertexBatchPredictionBaseURL string `mapstructure:"vertex_batch_prediction_base_url"`
  225. VertexGCSBaseURL string `mapstructure:"vertex_gcs_base_url"`
  226. }
  227. // ImageStorageConfig 配置异步图片任务结果上传的 S3 兼容对象存储。
  228. // Enabled 同时作为异步图片任务功能的总开关:未启用或未配置完整凭证时,
  229. // 异步生图接口整体禁用,避免把上游返回的大 base64 结果塞进 Redis。
  230. type ImageStorageConfig struct {
  231. Enabled bool `mapstructure:"enabled"`
  232. Endpoint string `mapstructure:"endpoint"` // e.g. https://<account_id>.r2.cloudflarestorage.com
  233. Region string `mapstructure:"region"` // R2 用 "auto"
  234. Bucket string `mapstructure:"bucket"`
  235. AccessKeyID string `mapstructure:"access_key_id"`
  236. SecretAccessKey string `mapstructure:"secret_access_key"`
  237. Prefix string `mapstructure:"prefix"` // S3 key 前缀,如 "images/"
  238. ForcePathStyle bool `mapstructure:"force_path_style"` // MinIO/路径风格桶
  239. PublicBaseURL string `mapstructure:"public_base_url"` // 配了则返回 public_base_url/key 直链;否则 presigned
  240. PresignExpiry int `mapstructure:"presign_expiry_hours"` // public_base_url 为空时的 presigned 过期时长(小时)
  241. MaxDownloadByte int64 `mapstructure:"max_download_bytes"` // 下载上游 url 图片的字节上限
  242. }
  243. // IsConfigured 检查对象存储必要字段是否已配置
  244. func (c *ImageStorageConfig) IsConfigured() bool {
  245. return c.Bucket != "" && c.AccessKeyID != "" && c.SecretAccessKey != ""
  246. }
  247. // Active 返回异步图片任务是否可用:开关打开且凭证齐全
  248. func (c *ImageStorageConfig) Active() bool {
  249. return c.Enabled && c.IsConfigured()
  250. }
  251. // MissingCredentialKeys 返回 IsConfigured 所缺的配置键名。
  252. // 用于启动日志:只说"凭证不完整"会让运维以为自己漏填了,而实际可能是值填了却没被读到。
  253. func (c *ImageStorageConfig) MissingCredentialKeys() []string {
  254. var missing []string
  255. if c.Bucket == "" {
  256. missing = append(missing, "image_storage.bucket")
  257. }
  258. if c.AccessKeyID == "" {
  259. missing = append(missing, "image_storage.access_key_id")
  260. }
  261. if c.SecretAccessKey == "" {
  262. missing = append(missing, "image_storage.secret_access_key")
  263. }
  264. return missing
  265. }
  266. type LinuxDoConnectConfig struct {
  267. Enabled bool `mapstructure:"enabled"`
  268. ClientID string `mapstructure:"client_id"`
  269. ClientSecret string `mapstructure:"client_secret"`
  270. AuthorizeURL string `mapstructure:"authorize_url"`
  271. TokenURL string `mapstructure:"token_url"`
  272. UserInfoURL string `mapstructure:"userinfo_url"`
  273. Scopes string `mapstructure:"scopes"`
  274. RedirectURL string `mapstructure:"redirect_url"` // 后端回调地址(需在提供方后台登记)
  275. FrontendRedirectURL string `mapstructure:"frontend_redirect_url"` // 前端接收 token 的路由(默认:/auth/linuxdo/callback)
  276. TokenAuthMethod string `mapstructure:"token_auth_method"` // client_secret_post / client_secret_basic / none
  277. UsePKCE bool `mapstructure:"use_pkce"`
  278. // 可选:用于从 userinfo JSON 中提取字段的 gjson 路径。
  279. // 为空时,服务端会尝试一组常见字段名。
  280. UserInfoEmailPath string `mapstructure:"userinfo_email_path"`
  281. UserInfoIDPath string `mapstructure:"userinfo_id_path"`
  282. UserInfoUsernamePath string `mapstructure:"userinfo_username_path"`
  283. }
  284. type WeChatConnectConfig struct {
  285. Enabled bool `mapstructure:"enabled"`
  286. AppID string `mapstructure:"app_id"`
  287. AppSecret string `mapstructure:"app_secret"`
  288. OpenAppID string `mapstructure:"open_app_id"`
  289. OpenAppSecret string `mapstructure:"open_app_secret"`
  290. MPAppID string `mapstructure:"mp_app_id"`
  291. MPAppSecret string `mapstructure:"mp_app_secret"`
  292. MobileAppID string `mapstructure:"mobile_app_id"`
  293. MobileAppSecret string `mapstructure:"mobile_app_secret"`
  294. OpenEnabled bool `mapstructure:"open_enabled"`
  295. MPEnabled bool `mapstructure:"mp_enabled"`
  296. MobileEnabled bool `mapstructure:"mobile_enabled"`
  297. Mode string `mapstructure:"mode"`
  298. Scopes string `mapstructure:"scopes"`
  299. RedirectURL string `mapstructure:"redirect_url"`
  300. FrontendRedirectURL string `mapstructure:"frontend_redirect_url"`
  301. }
  302. type OIDCConnectConfig struct {
  303. Enabled bool `mapstructure:"enabled"`
  304. ProviderName string `mapstructure:"provider_name"` // 显示名: "Keycloak" 等
  305. ClientID string `mapstructure:"client_id"`
  306. ClientSecret string `mapstructure:"client_secret"`
  307. IssuerURL string `mapstructure:"issuer_url"`
  308. DiscoveryURL string `mapstructure:"discovery_url"`
  309. AuthorizeURL string `mapstructure:"authorize_url"`
  310. TokenURL string `mapstructure:"token_url"`
  311. UserInfoURL string `mapstructure:"userinfo_url"`
  312. JWKSURL string `mapstructure:"jwks_url"`
  313. Scopes string `mapstructure:"scopes"` // 默认 "openid email profile"
  314. RedirectURL string `mapstructure:"redirect_url"` // 后端回调地址(需在提供方后台登记)
  315. FrontendRedirectURL string `mapstructure:"frontend_redirect_url"` // 前端接收 token 的路由(默认:/auth/oidc/callback)
  316. TokenAuthMethod string `mapstructure:"token_auth_method"` // client_secret_post / client_secret_basic / none
  317. UsePKCE bool `mapstructure:"use_pkce"`
  318. ValidateIDToken bool `mapstructure:"validate_id_token"`
  319. UsePKCEExplicit bool `mapstructure:"-" yaml:"-"`
  320. ValidateIDTokenExplicit bool `mapstructure:"-" yaml:"-"`
  321. AllowedSigningAlgs string `mapstructure:"allowed_signing_algs"` // 默认 "RS256,ES256,PS256"
  322. ClockSkewSeconds int `mapstructure:"clock_skew_seconds"` // 默认 120
  323. RequireEmailVerified bool `mapstructure:"require_email_verified"` // 默认 false
  324. // 可选:用于从 userinfo JSON 中提取字段的 gjson 路径。
  325. // 为空时,服务端会尝试一组常见字段名。
  326. UserInfoEmailPath string `mapstructure:"userinfo_email_path"`
  327. UserInfoIDPath string `mapstructure:"userinfo_id_path"`
  328. UserInfoUsernamePath string `mapstructure:"userinfo_username_path"`
  329. }
  330. type DingTalkConnectConfig struct {
  331. Enabled bool `mapstructure:"enabled"`
  332. ClientID string `mapstructure:"client_id"`
  333. ClientSecret string `mapstructure:"client_secret"`
  334. AuthorizeURL string `mapstructure:"authorize_url"`
  335. TokenURL string `mapstructure:"token_url"`
  336. UserInfoURL string `mapstructure:"userinfo_url"`
  337. Scopes string `mapstructure:"scopes"`
  338. RedirectURL string `mapstructure:"redirect_url"`
  339. FrontendRedirectURL string `mapstructure:"frontend_redirect_url"`
  340. // 平台底座 + 业务行为
  341. DingTalkAppKind string `mapstructure:"dingtalk_app_kind"` // 仅 "internal_app"(V4 fail-closed)
  342. AppType string `mapstructure:"app_type"` // "public" (default) | "internal"
  343. // Corp 限定(none | internal_only)
  344. CorpRestrictionPolicy string `mapstructure:"corp_restriction_policy"`
  345. InternalCorpID string `mapstructure:"internal_corp_id"`
  346. BypassRegistration bool `mapstructure:"bypass_registration"`
  347. SyncCorpEmail bool `mapstructure:"sync_corp_email"`
  348. SyncDisplayName bool `mapstructure:"sync_display_name"`
  349. SyncDept bool `mapstructure:"sync_dept"`
  350. SyncCorpEmailAttrKey string `mapstructure:"sync_corp_email_attr_key"`
  351. SyncDisplayNameAttrKey string `mapstructure:"sync_display_name_attr_key"`
  352. SyncDeptAttrKey string `mapstructure:"sync_dept_attr_key"`
  353. SyncCorpEmailAttrName string `mapstructure:"sync_corp_email_attr_name"`
  354. SyncDisplayNameAttrName string `mapstructure:"sync_display_name_attr_name"`
  355. SyncDeptAttrName string `mapstructure:"sync_dept_attr_name"`
  356. // 邮箱 + Username
  357. RequireEmail bool `mapstructure:"require_email"`
  358. UsernameOverwritePolicy string `mapstructure:"username_overwrite_policy"`
  359. // Attribute(私有版扩展点;开源版仅声明)
  360. UsernameAttributeKey string `mapstructure:"username_attribute_key"`
  361. EnableAttributeMatching bool `mapstructure:"enable_attribute_matching"`
  362. EnableAttributeSync bool `mapstructure:"enable_attribute_sync"`
  363. AttributeSyncFields []string `mapstructure:"attribute_sync_fields"`
  364. AttributeSyncOverwritePolicy string `mapstructure:"attribute_sync_overwrite_policy"`
  365. }
  366. type EmailOAuthProviderConfig struct {
  367. Enabled bool `mapstructure:"enabled"`
  368. ClientID string `mapstructure:"client_id"`
  369. ClientSecret string `mapstructure:"client_secret"`
  370. AuthorizeURL string `mapstructure:"authorize_url"`
  371. TokenURL string `mapstructure:"token_url"`
  372. UserInfoURL string `mapstructure:"userinfo_url"`
  373. EmailsURL string `mapstructure:"emails_url"`
  374. Scopes string `mapstructure:"scopes"`
  375. RedirectURL string `mapstructure:"redirect_url"`
  376. FrontendRedirectURL string `mapstructure:"frontend_redirect_url"`
  377. }
  378. const (
  379. defaultWeChatConnectMode = "open"
  380. defaultWeChatConnectScopes = "snsapi_login"
  381. defaultWeChatConnectFrontendRedirect = "/auth/wechat/callback"
  382. )
  383. func firstNonEmptyString(values ...string) string {
  384. for _, value := range values {
  385. if trimmed := strings.TrimSpace(value); trimmed != "" {
  386. return trimmed
  387. }
  388. }
  389. return ""
  390. }
  391. func normalizeWeChatConnectMode(raw string) string {
  392. switch strings.ToLower(strings.TrimSpace(raw)) {
  393. case "mp":
  394. return "mp"
  395. case "mobile":
  396. return "mobile"
  397. default:
  398. return defaultWeChatConnectMode
  399. }
  400. }
  401. func normalizeWeChatConnectStoredMode(openEnabled, mpEnabled, mobileEnabled bool, mode string) string {
  402. mode = normalizeWeChatConnectMode(mode)
  403. switch mode {
  404. case "open":
  405. if openEnabled {
  406. return "open"
  407. }
  408. case "mp":
  409. if mpEnabled {
  410. return "mp"
  411. }
  412. case "mobile":
  413. if mobileEnabled {
  414. return "mobile"
  415. }
  416. }
  417. switch {
  418. case openEnabled:
  419. return "open"
  420. case mpEnabled:
  421. return "mp"
  422. case mobileEnabled:
  423. return "mobile"
  424. default:
  425. return mode
  426. }
  427. }
  428. func defaultWeChatConnectScopesForMode(mode string) string {
  429. switch normalizeWeChatConnectMode(mode) {
  430. case "mp":
  431. return "snsapi_userinfo"
  432. case "mobile":
  433. return ""
  434. default:
  435. return defaultWeChatConnectScopes
  436. }
  437. }
  438. func normalizeWeChatConnectScopes(raw, mode string) string {
  439. switch normalizeWeChatConnectMode(mode) {
  440. case "mp":
  441. switch strings.TrimSpace(raw) {
  442. case "snsapi_base":
  443. return "snsapi_base"
  444. case "snsapi_userinfo":
  445. return "snsapi_userinfo"
  446. default:
  447. return defaultWeChatConnectScopesForMode(mode)
  448. }
  449. case "mobile":
  450. return ""
  451. default:
  452. return defaultWeChatConnectScopes
  453. }
  454. }
  455. func shouldApplyLegacyWeChatEnv(configKey, envKey string) bool {
  456. if viper.InConfig(configKey) {
  457. return false
  458. }
  459. _, hasNewEnv := os.LookupEnv(envKey)
  460. return !hasNewEnv
  461. }
  462. func hasExplicitConfigOrEnv(configKey, envKey string) bool {
  463. if viper.InConfig(configKey) {
  464. return true
  465. }
  466. _, ok := os.LookupEnv(envKey)
  467. return ok
  468. }
  469. func applyLegacyWeChatConnectEnvCompatibility(cfg *WeChatConnectConfig) {
  470. if cfg == nil {
  471. return
  472. }
  473. legacyOpenAppID := ""
  474. if shouldApplyLegacyWeChatEnv("wechat_connect.open_app_id", "WECHAT_CONNECT_OPEN_APP_ID") &&
  475. shouldApplyLegacyWeChatEnv("wechat_connect.app_id", "WECHAT_CONNECT_APP_ID") {
  476. legacyOpenAppID = strings.TrimSpace(os.Getenv("WECHAT_OAUTH_OPEN_APP_ID"))
  477. if legacyOpenAppID != "" {
  478. cfg.OpenAppID = legacyOpenAppID
  479. }
  480. }
  481. legacyOpenAppSecret := ""
  482. if shouldApplyLegacyWeChatEnv("wechat_connect.open_app_secret", "WECHAT_CONNECT_OPEN_APP_SECRET") &&
  483. shouldApplyLegacyWeChatEnv("wechat_connect.app_secret", "WECHAT_CONNECT_APP_SECRET") {
  484. legacyOpenAppSecret = strings.TrimSpace(os.Getenv("WECHAT_OAUTH_OPEN_APP_SECRET"))
  485. if legacyOpenAppSecret != "" {
  486. cfg.OpenAppSecret = legacyOpenAppSecret
  487. }
  488. }
  489. legacyMPAppID := ""
  490. if shouldApplyLegacyWeChatEnv("wechat_connect.mp_app_id", "WECHAT_CONNECT_MP_APP_ID") &&
  491. shouldApplyLegacyWeChatEnv("wechat_connect.app_id", "WECHAT_CONNECT_APP_ID") {
  492. legacyMPAppID = strings.TrimSpace(os.Getenv("WECHAT_OAUTH_MP_APP_ID"))
  493. if legacyMPAppID != "" {
  494. cfg.MPAppID = legacyMPAppID
  495. }
  496. }
  497. legacyMPAppSecret := ""
  498. if shouldApplyLegacyWeChatEnv("wechat_connect.mp_app_secret", "WECHAT_CONNECT_MP_APP_SECRET") &&
  499. shouldApplyLegacyWeChatEnv("wechat_connect.app_secret", "WECHAT_CONNECT_APP_SECRET") {
  500. legacyMPAppSecret = strings.TrimSpace(os.Getenv("WECHAT_OAUTH_MP_APP_SECRET"))
  501. if legacyMPAppSecret != "" {
  502. cfg.MPAppSecret = legacyMPAppSecret
  503. }
  504. }
  505. if shouldApplyLegacyWeChatEnv("wechat_connect.frontend_redirect_url", "WECHAT_CONNECT_FRONTEND_REDIRECT_URL") {
  506. if legacyFrontend := strings.TrimSpace(os.Getenv("WECHAT_OAUTH_FRONTEND_REDIRECT_URL")); legacyFrontend != "" {
  507. cfg.FrontendRedirectURL = legacyFrontend
  508. }
  509. }
  510. hasLegacyOpen := legacyOpenAppID != "" && legacyOpenAppSecret != ""
  511. hasLegacyMP := legacyMPAppID != "" && legacyMPAppSecret != ""
  512. if shouldApplyLegacyWeChatEnv("wechat_connect.enabled", "WECHAT_CONNECT_ENABLED") && (hasLegacyOpen || hasLegacyMP) {
  513. cfg.Enabled = true
  514. }
  515. if shouldApplyLegacyWeChatEnv("wechat_connect.open_enabled", "WECHAT_CONNECT_OPEN_ENABLED") && hasLegacyOpen {
  516. cfg.OpenEnabled = true
  517. }
  518. if shouldApplyLegacyWeChatEnv("wechat_connect.mp_enabled", "WECHAT_CONNECT_MP_ENABLED") && hasLegacyMP {
  519. cfg.MPEnabled = true
  520. }
  521. if shouldApplyLegacyWeChatEnv("wechat_connect.mode", "WECHAT_CONNECT_MODE") {
  522. switch {
  523. case hasLegacyMP && !hasLegacyOpen:
  524. cfg.Mode = "mp"
  525. case hasLegacyOpen:
  526. cfg.Mode = "open"
  527. }
  528. }
  529. if shouldApplyLegacyWeChatEnv("wechat_connect.scopes", "WECHAT_CONNECT_SCOPES") {
  530. switch {
  531. case hasLegacyMP && !hasLegacyOpen:
  532. cfg.Scopes = defaultWeChatConnectScopesForMode("mp")
  533. case hasLegacyOpen:
  534. cfg.Scopes = defaultWeChatConnectScopesForMode("open")
  535. }
  536. }
  537. }
  538. func normalizeWeChatConnectConfig(cfg *WeChatConnectConfig) {
  539. if cfg == nil {
  540. return
  541. }
  542. cfg.AppID = strings.TrimSpace(cfg.AppID)
  543. cfg.AppSecret = strings.TrimSpace(cfg.AppSecret)
  544. cfg.OpenAppID = strings.TrimSpace(cfg.OpenAppID)
  545. cfg.OpenAppSecret = strings.TrimSpace(cfg.OpenAppSecret)
  546. cfg.MPAppID = strings.TrimSpace(cfg.MPAppID)
  547. cfg.MPAppSecret = strings.TrimSpace(cfg.MPAppSecret)
  548. cfg.MobileAppID = strings.TrimSpace(cfg.MobileAppID)
  549. cfg.MobileAppSecret = strings.TrimSpace(cfg.MobileAppSecret)
  550. cfg.Mode = normalizeWeChatConnectMode(cfg.Mode)
  551. cfg.RedirectURL = strings.TrimSpace(cfg.RedirectURL)
  552. cfg.FrontendRedirectURL = strings.TrimSpace(cfg.FrontendRedirectURL)
  553. cfg.AppID = firstNonEmptyString(cfg.AppID, cfg.OpenAppID, cfg.MPAppID, cfg.MobileAppID)
  554. cfg.AppSecret = firstNonEmptyString(cfg.AppSecret, cfg.OpenAppSecret, cfg.MPAppSecret, cfg.MobileAppSecret)
  555. cfg.OpenAppID = firstNonEmptyString(cfg.OpenAppID, cfg.AppID)
  556. cfg.OpenAppSecret = firstNonEmptyString(cfg.OpenAppSecret, cfg.AppSecret)
  557. cfg.MPAppID = firstNonEmptyString(cfg.MPAppID, cfg.AppID)
  558. cfg.MPAppSecret = firstNonEmptyString(cfg.MPAppSecret, cfg.AppSecret)
  559. cfg.MobileAppID = firstNonEmptyString(cfg.MobileAppID, cfg.AppID)
  560. cfg.MobileAppSecret = firstNonEmptyString(cfg.MobileAppSecret, cfg.AppSecret)
  561. if !cfg.OpenEnabled && !cfg.MPEnabled && !cfg.MobileEnabled && cfg.Enabled {
  562. switch cfg.Mode {
  563. case "mp":
  564. cfg.MPEnabled = true
  565. case "mobile":
  566. cfg.MobileEnabled = true
  567. default:
  568. cfg.OpenEnabled = true
  569. }
  570. }
  571. cfg.Mode = normalizeWeChatConnectStoredMode(cfg.OpenEnabled, cfg.MPEnabled, cfg.MobileEnabled, cfg.Mode)
  572. cfg.Scopes = normalizeWeChatConnectScopes(cfg.Scopes, cfg.Mode)
  573. if cfg.FrontendRedirectURL == "" {
  574. cfg.FrontendRedirectURL = defaultWeChatConnectFrontendRedirect
  575. }
  576. }
  577. // TokenRefreshConfig OAuth token自动刷新配置
  578. type TokenRefreshConfig struct {
  579. // 是否启用自动刷新
  580. Enabled bool `mapstructure:"enabled"`
  581. // 检查间隔(分钟)
  582. CheckIntervalMinutes int `mapstructure:"check_interval_minutes"`
  583. // 提前刷新时间(小时),在token过期前多久开始刷新
  584. RefreshBeforeExpiryHours float64 `mapstructure:"refresh_before_expiry_hours"`
  585. // 最大重试次数
  586. MaxRetries int `mapstructure:"max_retries"`
  587. // 重试退避基础时间(秒)
  588. RetryBackoffSeconds int `mapstructure:"retry_backoff_seconds"`
  589. // 每次从数据库读取的候选账号上限
  590. CandidatePageSize int `mapstructure:"candidate_page_size"`
  591. // 每个平台允许的并发刷新数
  592. ProviderConcurrency int `mapstructure:"provider_concurrency"`
  593. // 每个平台、每个进程允许的刷新请求速率
  594. ProviderQPS int `mapstructure:"provider_qps"`
  595. // 一个周期内连续临时失败达到此值后停止该平台
  596. ProviderFailureThreshold int `mapstructure:"provider_failure_threshold"`
  597. // 单次上游刷新尝试的超时(秒)
  598. AttemptTimeoutSeconds int `mapstructure:"attempt_timeout_seconds"`
  599. // 单个后台刷新周期的总超时(秒)
  600. CycleTimeoutSeconds int `mapstructure:"cycle_timeout_seconds"`
  601. }
  602. type PricingConfig struct {
  603. // 价格数据远程URL(默认使用LiteLLM镜像)
  604. RemoteURL string `mapstructure:"remote_url"`
  605. // 哈希校验文件URL
  606. HashURL string `mapstructure:"hash_url"`
  607. // 本地数据目录
  608. DataDir string `mapstructure:"data_dir"`
  609. // 回退文件路径
  610. FallbackFile string `mapstructure:"fallback_file"`
  611. // 覆盖补丁文件路径(可选):条目按字段浅合并覆盖目录/回退数据,优先级最高
  612. OverrideFile string `mapstructure:"override_file"`
  613. // 更新间隔(小时)
  614. UpdateIntervalHours int `mapstructure:"update_interval_hours"`
  615. // 哈希校验间隔(分钟)
  616. HashCheckIntervalMinutes int `mapstructure:"hash_check_interval_minutes"`
  617. }
  618. type ServerConfig struct {
  619. Host string `mapstructure:"host"`
  620. Port int `mapstructure:"port"`
  621. Mode string `mapstructure:"mode"` // debug/release
  622. EnableServerTiming bool `mapstructure:"enable_server_timing"` // Admin UI Server-Timing response header
  623. FrontendURL string `mapstructure:"frontend_url"` // 前端基础 URL,用于生成邮件中的外部链接
  624. ReadHeaderTimeout int `mapstructure:"read_header_timeout"` // 读取请求头超时(秒)
  625. MaxHeaderBytes int `mapstructure:"max_header_bytes"` // 请求头最大字节数(HTTP/2 映射为 header-list 上限)
  626. IdleTimeout int `mapstructure:"idle_timeout"` // 空闲连接超时(秒)
  627. TrustedProxies []string `mapstructure:"trusted_proxies"` // 可信代理列表(CIDR/IP)
  628. TrustedProxiesConfigured bool `mapstructure:"-" json:"-" yaml:"-"` // 是否显式配置了可信代理列表
  629. MaxRequestBodySize int64 `mapstructure:"max_request_body_size"` // 全局最大请求体限制
  630. H2C H2CConfig `mapstructure:"h2c"` // HTTP/2 Cleartext 配置
  631. }
  632. // H2CConfig HTTP/2 Cleartext 配置
  633. type H2CConfig struct {
  634. Enabled bool `mapstructure:"enabled"` // 是否启用 H2C
  635. MaxConcurrentStreams uint32 `mapstructure:"max_concurrent_streams"` // 最大并发流数量
  636. IdleTimeout int `mapstructure:"idle_timeout"` // 空闲超时(秒)
  637. MaxReadFrameSize int `mapstructure:"max_read_frame_size"` // 最大帧大小(字节)
  638. MaxUploadBufferPerConnection int `mapstructure:"max_upload_buffer_per_connection"` // 每个连接的上传缓冲区(字节)
  639. MaxUploadBufferPerStream int `mapstructure:"max_upload_buffer_per_stream"` // 每个流的上传缓冲区(字节)
  640. }
  641. type CORSConfig struct {
  642. AllowedOrigins []string `mapstructure:"allowed_origins"`
  643. AllowCredentials bool `mapstructure:"allow_credentials"`
  644. }
  645. // WebAuthnConfig configures this deployment as a WebAuthn relying party.
  646. // RPID and RPOrigins are security boundaries and must never be inferred from
  647. // untrusted request Host or Origin headers.
  648. type WebAuthnConfig struct {
  649. Enabled bool `mapstructure:"enabled"`
  650. RPDisplayName string `mapstructure:"rp_display_name"`
  651. RPID string `mapstructure:"rp_id"`
  652. RPOrigins []string `mapstructure:"rp_origins"`
  653. }
  654. const MaxForwardedClientIPHeaders = 16
  655. type ForwardedClientIPSettings struct {
  656. TrustForwardedIP bool
  657. Headers []string
  658. }
  659. type SecurityConfig struct {
  660. URLAllowlist URLAllowlistConfig `mapstructure:"url_allowlist"`
  661. ResponseHeaders ResponseHeaderConfig `mapstructure:"response_headers"`
  662. CSP CSPConfig `mapstructure:"csp"`
  663. ProxyFallback ProxyFallbackConfig `mapstructure:"proxy_fallback"`
  664. ProxyProbe ProxyProbeConfig `mapstructure:"proxy_probe"`
  665. // TrustForwardedIPForAPIKeyACL enables legacy raw forwarded-header takeover.
  666. // When disabled, server.trusted_proxies is authoritative for all client-IP consumers.
  667. TrustForwardedIPForAPIKeyACL bool `mapstructure:"trust_forwarded_ip_for_api_key_acl"`
  668. ForwardedClientIPHeaders []string `mapstructure:"forwarded_client_ip_headers" json:"forwarded_client_ip_headers" yaml:"forwarded_client_ip_headers"`
  669. forwardedClientIPSettingsLive *atomic.Pointer[ForwardedClientIPSettings] `mapstructure:"-" json:"-" yaml:"-"`
  670. }
  671. func NormalizeForwardedClientIPHeaders(headers []string) ([]string, error) {
  672. normalized := make([]string, 0, len(headers))
  673. seen := make(map[string]struct{}, len(headers))
  674. for _, header := range headers {
  675. header = strings.TrimSpace(header)
  676. if !httpguts.ValidHeaderFieldName(header) {
  677. return nil, fmt.Errorf("invalid HTTP header field name %q", header)
  678. }
  679. canonical := textproto.CanonicalMIMEHeaderKey(header)
  680. key := strings.ToLower(canonical)
  681. if _, exists := seen[key]; exists {
  682. continue
  683. }
  684. if len(normalized) == MaxForwardedClientIPHeaders {
  685. return nil, fmt.Errorf("forwarded client IP headers must contain at most %d unique names", MaxForwardedClientIPHeaders)
  686. }
  687. seen[key] = struct{}{}
  688. normalized = append(normalized, canonical)
  689. }
  690. return normalized, nil
  691. }
  692. func cloneForwardedClientIPHeaders(headers []string) []string {
  693. if len(headers) == 0 {
  694. return []string{}
  695. }
  696. return append([]string(nil), headers...)
  697. }
  698. func (c *Config) ForwardedClientIPSettings() ForwardedClientIPSettings {
  699. if c == nil {
  700. return ForwardedClientIPSettings{Headers: []string{}}
  701. }
  702. live := c.Security.forwardedClientIPSettingsLive
  703. if live != nil {
  704. if snapshot := live.Load(); snapshot != nil {
  705. return ForwardedClientIPSettings{
  706. TrustForwardedIP: snapshot.TrustForwardedIP,
  707. Headers: cloneForwardedClientIPHeaders(snapshot.Headers),
  708. }
  709. }
  710. }
  711. return ForwardedClientIPSettings{
  712. TrustForwardedIP: c.Security.TrustForwardedIPForAPIKeyACL,
  713. Headers: cloneForwardedClientIPHeaders(c.Security.ForwardedClientIPHeaders),
  714. }
  715. }
  716. func (c *Config) TrustForwardedIPForAPIKeyACL() bool {
  717. return c.ForwardedClientIPSettings().TrustForwardedIP
  718. }
  719. // ForwardedClientIPTrustEnabled reports whether the legacy forwarded-header
  720. // compatibility mode currently overrides server.trusted_proxies.
  721. func (c *Config) ForwardedClientIPTrustEnabled() bool {
  722. return c != nil && c.TrustForwardedIPForAPIKeyACL()
  723. }
  724. func (c *Config) SetForwardedClientIPSettings(enabled bool, headers []string) {
  725. if c == nil {
  726. return
  727. }
  728. headers = cloneForwardedClientIPHeaders(headers)
  729. if c.Security.forwardedClientIPSettingsLive == nil {
  730. c.Security.forwardedClientIPSettingsLive = &atomic.Pointer[ForwardedClientIPSettings]{}
  731. }
  732. c.Security.forwardedClientIPSettingsLive.Store(&ForwardedClientIPSettings{
  733. TrustForwardedIP: enabled,
  734. Headers: headers,
  735. })
  736. }
  737. func (c *Config) SetTrustForwardedIPForAPIKeyACL(enabled bool) {
  738. if c == nil {
  739. return
  740. }
  741. c.SetForwardedClientIPSettings(enabled, c.ForwardedClientIPSettings().Headers)
  742. }
  743. type URLAllowlistConfig struct {
  744. Enabled bool `mapstructure:"enabled"`
  745. UpstreamHosts []string `mapstructure:"upstream_hosts"`
  746. PricingHosts []string `mapstructure:"pricing_hosts"`
  747. CRSHosts []string `mapstructure:"crs_hosts"`
  748. AllowPrivateHosts bool `mapstructure:"allow_private_hosts"`
  749. // 关闭 URL 白名单校验时,是否允许 http URL(默认只允许 https)
  750. AllowInsecureHTTP bool `mapstructure:"allow_insecure_http"`
  751. }
  752. type ResponseHeaderConfig struct {
  753. Enabled bool `mapstructure:"enabled"`
  754. AdditionalAllowed []string `mapstructure:"additional_allowed"`
  755. ForceRemove []string `mapstructure:"force_remove"`
  756. }
  757. type CSPConfig struct {
  758. Enabled bool `mapstructure:"enabled"`
  759. Policy string `mapstructure:"policy"`
  760. }
  761. type ProxyFallbackConfig struct {
  762. // AllowDirectOnError 当辅助服务的代理初始化失败时是否允许回退直连。
  763. // 仅影响以下非 AI 账号连接的辅助服务:
  764. // - GitHub Release 更新检查
  765. // - 定价数据拉取
  766. // 不影响 AI 账号网关连接(Claude/OpenAI/Gemini/Antigravity),
  767. // 这些关键路径的代理失败始终返回错误,不会回退直连。
  768. // 默认 false:避免因代理配置错误导致服务器真实 IP 泄露。
  769. AllowDirectOnError bool `mapstructure:"allow_direct_on_error"`
  770. }
  771. type ProxyProbeConfig struct {
  772. InsecureSkipVerify bool `mapstructure:"insecure_skip_verify"` // 已禁用:禁止跳过 TLS 证书验证
  773. // URLs 按优先级排列的自定义探测 URL 列表。
  774. // 留空时使用内置默认列表(ip-api → ipify)。
  775. // 某些 AI API 专用代理只允许访问特定域名,配置多个备选可提高探测成功率。
  776. URLs []ProbeURLConfig `mapstructure:"urls"`
  777. }
  778. // ProbeURLConfig 描述一个探测端点及其响应解析方式。
  779. type ProbeURLConfig struct {
  780. URL string `mapstructure:"url"`
  781. Parser string `mapstructure:"parser"` // "ip-api" / "ipify" / "chatgpt-trace"
  782. }
  783. func normalizeProxyProbeURLs(targets []ProbeURLConfig) ([]ProbeURLConfig, error) {
  784. if len(targets) == 0 {
  785. return nil, nil
  786. }
  787. normalized := make([]ProbeURLConfig, 0, len(targets))
  788. for i, target := range targets {
  789. rawURL := strings.TrimSpace(target.URL)
  790. parser := strings.ToLower(strings.TrimSpace(target.Parser))
  791. if rawURL == "" {
  792. return nil, fmt.Errorf("entry %d: url is required", i)
  793. }
  794. if parser == "" {
  795. return nil, fmt.Errorf("entry %d: parser is required", i)
  796. }
  797. switch parser {
  798. case "ip-api", "ipify", "chatgpt-trace":
  799. default:
  800. return nil, fmt.Errorf("entry %d: unsupported parser %q", i, target.Parser)
  801. }
  802. parsed, err := url.Parse(rawURL)
  803. if err != nil || parsed.Host == "" {
  804. return nil, fmt.Errorf("entry %d: invalid url %q", i, target.URL)
  805. }
  806. if parsed.Scheme != "http" && parsed.Scheme != "https" {
  807. return nil, fmt.Errorf("entry %d: url scheme must be http or https", i)
  808. }
  809. normalized = append(normalized, ProbeURLConfig{
  810. URL: rawURL,
  811. Parser: parser,
  812. })
  813. }
  814. return normalized, nil
  815. }
  816. type BillingConfig struct {
  817. CircuitBreaker CircuitBreakerConfig `mapstructure:"circuit_breaker"`
  818. // MinimumBalanceReserve is the conservative preflight floor for balance billing.
  819. // Requests in balance mode are rejected when the cached balance is below this
  820. // amount, even if it is still positive. Set to 0 to keep the legacy balance > 0 gate.
  821. MinimumBalanceReserve float64 `mapstructure:"minimum_balance_reserve"`
  822. // UserPlatformQuotaCacheTTLSeconds 用户 × 平台 quota 缓存 TTL(秒),默认 86400=1天,覆盖典型 daily 窗口。
  823. // 消费点:
  824. // - billing_cache_service.cacheWriteWorker 异步累加
  825. // - billing_cache_service.checkUserPlatformQuotaEligibility 首次缓存装载
  826. // 读写两端必须共用同一 TTL,避免缓存生命周期不一致导致 quota 计数漂移。
  827. UserPlatformQuotaCacheTTLSeconds int `mapstructure:"user_platform_quota_cache_ttl_seconds"`
  828. // UserPlatformQuotaSentinelTTLSeconds sentinel(无 limit 占位)entry 的 TTL,
  829. // 显著短于 quota cache 默认 86400s 以控 Redis 内存;默认 3600=1h。
  830. UserPlatformQuotaSentinelTTLSeconds int `mapstructure:"user_platform_quota_sentinel_ttl_seconds"`
  831. }
  832. type CircuitBreakerConfig struct {
  833. Enabled bool `mapstructure:"enabled"`
  834. FailureThreshold int `mapstructure:"failure_threshold"`
  835. ResetTimeoutSeconds int `mapstructure:"reset_timeout_seconds"`
  836. HalfOpenRequests int `mapstructure:"half_open_requests"`
  837. }
  838. type ConcurrencyConfig struct {
  839. // PingInterval: 并发等待期间的 SSE ping 间隔(秒)
  840. PingInterval int `mapstructure:"ping_interval"`
  841. }
  842. type ImageConcurrencyConfig struct {
  843. // Enabled: 是否启用图片生成独立并发限制,默认关闭以保持现有行为
  844. Enabled bool `mapstructure:"enabled"`
  845. // MaxConcurrentRequests: 当前进程允许同时处理的图片生成请求数,0表示不限制
  846. MaxConcurrentRequests int `mapstructure:"max_concurrent_requests"`
  847. // OverflowMode: 图片并发达到上限后的处理方式:reject/wait
  848. OverflowMode string `mapstructure:"overflow_mode"`
  849. // WaitTimeoutSeconds: overflow_mode=wait 时等待图片并发槽位的超时时间(秒)
  850. WaitTimeoutSeconds int `mapstructure:"wait_timeout_seconds"`
  851. // MaxWaitingRequests: overflow_mode=wait 时当前进程允许排队等待的图片请求数
  852. MaxWaitingRequests int `mapstructure:"max_waiting_requests"`
  853. }
  854. const (
  855. ImageConcurrencyOverflowModeReject = "reject"
  856. ImageConcurrencyOverflowModeWait = "wait"
  857. )
  858. // GatewayConfig API网关相关配置
  859. type GatewayConfig struct {
  860. // 等待上游响应头的超时时间(秒),0表示无超时
  861. // 注意:这不影响流式数据传输,只控制等待响应头的时间
  862. ResponseHeaderTimeout int `mapstructure:"response_header_timeout"`
  863. // OpenAIResponseHeaderTimeout: OpenAI/Codex 上游等待响应头的超时时间(秒),0表示无超时
  864. // OpenAI/Codex 请求可能在上游排队较久;默认不使用通用响应头超时截断。
  865. OpenAIResponseHeaderTimeout int `mapstructure:"openai_response_header_timeout"`
  866. // GrokResponseHeaderTimeout bounds the pre-first-byte wait for xAI/Grok.
  867. // A zero value uses the provider-safe default instead of the generic gateway timeout.
  868. GrokResponseHeaderTimeout int `mapstructure:"grok_response_header_timeout"`
  869. // OpenAIFirstOutputTimeoutSeconds: native HTTP Responses 首个语义输出超时(秒),0表示禁用。
  870. OpenAIFirstOutputTimeoutSeconds int `mapstructure:"openai_first_output_timeout_seconds"`
  871. // OpenAIHighEffortFirstOutputTimeoutSeconds: high/xhigh/max 推理的首个语义输出超时(秒)。
  872. // 0 表示回退到 OpenAIFirstOutputTimeoutSeconds。
  873. OpenAIHighEffortFirstOutputTimeoutSeconds int `mapstructure:"openai_high_effort_first_output_timeout_seconds"`
  874. // 请求体最大字节数,用于网关请求体大小限制
  875. MaxBodySize int64 `mapstructure:"max_body_size"`
  876. // TextMaxBodySize limits endpoints that cannot carry inline image/video payloads.
  877. TextMaxBodySize int64 `mapstructure:"text_max_body_size"`
  878. // 非流式上游响应体读取上限(字节),用于防止无界读取导致内存放大
  879. UpstreamResponseReadMaxBytes int64 `mapstructure:"upstream_response_read_max_bytes"`
  880. // 上游模型列表响应体读取上限(字节)
  881. ModelsListReadMaxBytes int64 `mapstructure:"models_list_read_max_bytes"`
  882. // 代理探测响应体读取上限(字节)
  883. ProxyProbeResponseReadMaxBytes int64 `mapstructure:"proxy_probe_response_read_max_bytes"`
  884. // Gemini 上游响应头调试日志开关(默认关闭,避免高频日志开销)
  885. GeminiDebugResponseHeaders bool `mapstructure:"gemini_debug_response_headers"`
  886. // ConnectionPoolIsolation: 上游连接池隔离策略(proxy/account/account_proxy)
  887. ConnectionPoolIsolation string `mapstructure:"connection_pool_isolation"`
  888. // ForceCodexCLI: 强制将 OpenAI `/v1/responses` 请求按 Codex CLI 处理。
  889. // 用于网关未透传/改写 User-Agent 时的兼容兜底(默认关闭,避免影响其他客户端)。
  890. ForceCodexCLI bool `mapstructure:"force_codex_cli"`
  891. // DisableCodexIdentityEnforcement: 关闭「强制统一 Codex 出站身份」。上游 /backend-api/codex
  892. // 在容量紧张时按客户端身份分优先级降载,被降载的请求会拿到 HTTP 200 + 流内
  893. // server_is_overloaded,该次请求失败。默认强制统一出口:所有 OAuth 出站的
  894. // User-Agent / originator / version 都改写为网关规范身份,确保没有请求带着第三方或陈旧身份
  895. // 出站。置 true 后退回「仅按最终 User-Agent 配对 originator」的收口语义,供上游策略变动时回滚。
  896. //
  897. // 取反义命名是为了让零值安全:该开关会发布为进程级快照,未经 viper 加载而手工构造的
  898. // Config(测试、工具)其零值必须落在「强制统一开启」这一侧,否则会静默丢掉这层保护。
  899. DisableCodexIdentityEnforcement bool `mapstructure:"disable_codex_identity_enforcement"`
  900. // DisableCodexOriginatorNormalization: 已废弃,等价于 DisableCodexIdentityEnforcement。
  901. // 保留以兼容既有配置文件;加载时会折叠进新键,不要在新代码里直接读取。
  902. DisableCodexOriginatorNormalization bool `mapstructure:"disable_codex_originator_normalization"`
  903. // CodexImageGenerationBridgeEnabled: 是否为 Codex `/v1/responses` 自动注入 image_generation 工具和桥接指令。
  904. // 默认关闭,避免纯文本 Codex 请求被意外改写;显式携带 image_generation 工具的请求仍按分组能力转发。
  905. CodexImageGenerationBridgeEnabled bool `mapstructure:"codex_image_generation_bridge_enabled"`
  906. // ForcedCodexInstructionsTemplateFile: 服务端强制附加到 Codex 顶层 instructions 的模板文件路径。
  907. // 模板渲染后会直接覆盖最终 instructions;若需要保留客户端 system 转换结果,请在模板中显式引用 {{ .ExistingInstructions }}。
  908. ForcedCodexInstructionsTemplateFile string `mapstructure:"forced_codex_instructions_template_file"`
  909. // ForcedCodexInstructionsTemplate: 启动时从模板文件读取并缓存的模板内容。
  910. // 该字段不直接参与配置反序列化,仅用于请求热路径避免重复读盘。
  911. ForcedCodexInstructionsTemplate string `mapstructure:"-"`
  912. // OpenAIPassthroughAllowTimeoutHeaders: OpenAI 透传模式是否放行客户端超时头
  913. // 关闭(默认)可避免 x-stainless-timeout 等头导致上游提前断流。
  914. OpenAIPassthroughAllowTimeoutHeaders bool `mapstructure:"openai_passthrough_allow_timeout_headers"`
  915. // OpenAICompactModel: /responses/compact 上游使用的模型。
  916. // compact 端点支持模型滞后于普通 /responses 时,可用该配置降级规避上游错误。
  917. OpenAICompactModel string `mapstructure:"openai_compact_model"`
  918. // OpenAIWS: OpenAI Responses WebSocket 配置(默认开启,可按需回滚到 HTTP)
  919. OpenAIWS GatewayOpenAIWSConfig `mapstructure:"openai_ws"`
  920. // Live: ChatGPT Frameless Live 会话配置。
  921. Live GatewayLiveConfig `mapstructure:"live"`
  922. // OpenAIScheduler: OpenAI 高级调度器粘性逃逸配置
  923. OpenAIScheduler GatewayOpenAISchedulerConfig `mapstructure:"openai_scheduler"`
  924. // OpenAIHTTP2: OpenAI HTTP 上游协议策略(默认启用 HTTP/2,可按代理能力回退 HTTP/1.1)
  925. OpenAIHTTP2 GatewayOpenAIHTTP2Config `mapstructure:"openai_http2"`
  926. // OpenAIProxyStreamCircuit: Responses SSE 代理断流熔断策略。
  927. OpenAIProxyStreamCircuit GatewayOpenAIProxyStreamCircuitConfig `mapstructure:"openai_proxy_stream_circuit"`
  928. // ImageConcurrency: 图片生成独立并发限制配置(默认关闭)
  929. ImageConcurrency ImageConcurrencyConfig `mapstructure:"image_concurrency"`
  930. // HTTP 上游连接池配置(性能优化:支持高并发场景调优)
  931. // MaxIdleConns: 所有主机的最大空闲连接总数
  932. MaxIdleConns int `mapstructure:"max_idle_conns"`
  933. // MaxIdleConnsPerHost: 每个主机的最大空闲连接数(关键参数,影响连接复用率)
  934. MaxIdleConnsPerHost int `mapstructure:"max_idle_conns_per_host"`
  935. // MaxConnsPerHost: 每个主机的最大连接数(包括活跃+空闲),0表示无限制
  936. MaxConnsPerHost int `mapstructure:"max_conns_per_host"`
  937. // IdleConnTimeoutSeconds: 空闲连接超时时间(秒)
  938. IdleConnTimeoutSeconds int `mapstructure:"idle_conn_timeout_seconds"`
  939. // MaxUpstreamClients: 上游连接池客户端最大缓存数量
  940. // 当使用连接池隔离策略时,系统会为不同的账户/代理组合创建独立的 HTTP 客户端
  941. // 此参数限制缓存的客户端数量,超出后会淘汰最久未使用的客户端
  942. // 建议值:预估的活跃账户数 * 1.2(留有余量)
  943. MaxUpstreamClients int `mapstructure:"max_upstream_clients"`
  944. // ClientIdleTTLSeconds: 上游连接池客户端空闲回收阈值(秒)
  945. // 超过此时间未使用的客户端会被标记为可回收
  946. // 建议值:根据用户访问频率设置,一般 10-30 分钟
  947. ClientIdleTTLSeconds int `mapstructure:"client_idle_ttl_seconds"`
  948. // ConcurrencySlotTTLMinutes: 并发槽位过期时间(分钟)
  949. // 应大于最长 LLM 请求时间,防止请求完成前槽位过期
  950. ConcurrencySlotTTLMinutes int `mapstructure:"concurrency_slot_ttl_minutes"`
  951. // SessionIdleTimeoutMinutes: 会话空闲超时时间(分钟),默认 5 分钟
  952. // 用于 Anthropic OAuth/SetupToken 账号的会话数量限制功能
  953. // 空闲超过此时间的会话将被自动释放
  954. SessionIdleTimeoutMinutes int `mapstructure:"session_idle_timeout_minutes"`
  955. // StreamDataIntervalTimeout: 流数据间隔超时(秒),0表示禁用
  956. StreamDataIntervalTimeout int `mapstructure:"stream_data_interval_timeout"`
  957. // StreamKeepaliveInterval: 流式 keepalive 间隔(秒),0表示禁用
  958. StreamKeepaliveInterval int `mapstructure:"stream_keepalive_interval"`
  959. // ImageStreamDataIntervalTimeout: 图片流数据间隔超时(秒),0表示禁用
  960. ImageStreamDataIntervalTimeout int `mapstructure:"image_stream_data_interval_timeout"`
  961. // ImageStreamKeepaliveInterval: 图片流式 keepalive 间隔(秒),0表示禁用
  962. ImageStreamKeepaliveInterval int `mapstructure:"image_stream_keepalive_interval"`
  963. // ImageNonstreamKeepaliveInterval: 图片非流式 JSON keepalive 间隔(秒),0表示禁用
  964. ImageNonstreamKeepaliveInterval int `mapstructure:"image_nonstream_keepalive_interval"`
  965. // MaxLineSize: 上游 SSE 单行最大字节数(0使用默认值)
  966. MaxLineSize int `mapstructure:"max_line_size"`
  967. // 是否记录上游错误响应体摘要(避免输出请求内容)
  968. LogUpstreamErrorBody bool `mapstructure:"log_upstream_error_body"`
  969. // 上游错误响应体记录最大字节数(超过会截断)
  970. LogUpstreamErrorBodyMaxBytes int `mapstructure:"log_upstream_error_body_max_bytes"`
  971. // API-key 账号在客户端未提供 anthropic-beta 时,是否按需自动补齐(默认关闭以保持兼容)
  972. InjectBetaForAPIKey bool `mapstructure:"inject_beta_for_apikey"`
  973. // 是否允许对部分 400 错误触发 failover(默认关闭以避免改变语义)
  974. FailoverOn400 bool `mapstructure:"failover_on_400"`
  975. // 账户切换最大次数(遇到上游错误时切换到其他账户的次数上限)
  976. MaxAccountSwitches int `mapstructure:"max_account_switches"`
  977. // Gemini 账户切换最大次数(Gemini 平台单独配置,因 API 限制更严格)
  978. MaxAccountSwitchesGemini int `mapstructure:"max_account_switches_gemini"`
  979. // Antigravity 429 fallback 限流时间(分钟),解析重置时间失败时使用
  980. AntigravityFallbackCooldownMinutes int `mapstructure:"antigravity_fallback_cooldown_minutes"`
  981. // Scheduling: 账号调度相关配置
  982. Scheduling GatewaySchedulingConfig `mapstructure:"scheduling"`
  983. // TLSFingerprint: TLS指纹伪装配置
  984. TLSFingerprint TLSFingerprintConfig `mapstructure:"tls_fingerprint"`
  985. // UsageRecord: 使用量记录异步队列配置(有界队列 + 固定 worker)
  986. UsageRecord GatewayUsageRecordConfig `mapstructure:"usage_record"`
  987. // UserGroupRateCacheTTLSeconds: 用户分组倍率热路径缓存 TTL(秒)
  988. UserGroupRateCacheTTLSeconds int `mapstructure:"user_group_rate_cache_ttl_seconds"`
  989. // ModelsListCacheTTLSeconds: /v1/models 模型列表短缓存 TTL(秒)
  990. ModelsListCacheTTLSeconds int `mapstructure:"models_list_cache_ttl_seconds"`
  991. // UserMessageQueue: 用户消息串行队列配置
  992. // 对 role:"user" 的真实用户消息实施账号级串行化 + RPM 自适应延迟
  993. UserMessageQueue UserMessageQueueConfig `mapstructure:"user_message_queue"`
  994. // Grok: Grok/xAI gateway scheduling and free-tier soft-gate settings.
  995. Grok GatewayGrokConfig `mapstructure:"grok"`
  996. // CNProviders: 国产 OpenAI 兼容供应商(kimi/zhipu/deepseek)的余额检测配置。
  997. // 仅作用于 payg(按量付费)账号:周期探测余额,低于阈值则临时停调。
  998. CNProviders GatewayCNProvidersConfig `mapstructure:"cn_providers"`
  999. }
  1000. // GatewayGrokConfig holds Grok-specific gateway scheduling knobs.
  1001. //
  1002. // Free-quota soft gate keys (gateway.grok.*):
  1003. // - free_quota_soft_gate_enabled: enable local rolling-window scheduling guard for
  1004. // OAuth accounts whose subscription_tier/plan_type is explicitly "free".
  1005. // Default true is safe only because free-tier detection is strict (unknown/paid fail open).
  1006. // - free_quota_token_limit: nominal rolling-window token allowance.
  1007. // - free_quota_soft_gate_percent: stop new scheduling before the nominal limit (1-100).
  1008. // - free_quota_window_hours: local usage rolling window length in hours.
  1009. // - free_quota_stats_cache_seconds: cache TTL for free-tier usage stats
  1010. // (hot path never blocks on DB; misses fail open and refresh in background).
  1011. type GatewayGrokConfig struct {
  1012. // PasswordAuthEnabled controls the optional password-to-SSO OAuth flow.
  1013. // It defaults to false and must be explicitly enabled by the operator.
  1014. // When true, POST /admin/grok/oauth/password is functional (not ignored).
  1015. PasswordAuthEnabled bool `mapstructure:"password_auth_enabled"`
  1016. // FreeQuotaSoftGateEnabled enables a local rolling-window scheduling guard
  1017. // for explicitly free Grok OAuth accounts only.
  1018. FreeQuotaSoftGateEnabled bool `mapstructure:"free_quota_soft_gate_enabled"`
  1019. // FreeQuotaTokenLimit is the nominal rolling-window allowance.
  1020. FreeQuotaTokenLimit int64 `mapstructure:"free_quota_token_limit"`
  1021. // FreeQuotaSoftGatePercent stops new scheduling before the nominal limit.
  1022. FreeQuotaSoftGatePercent int `mapstructure:"free_quota_soft_gate_percent"`
  1023. // FreeQuotaWindowHours controls the local rolling usage window.
  1024. FreeQuotaWindowHours int `mapstructure:"free_quota_window_hours"`
  1025. // FreeQuotaStatsCacheSeconds is the soft-gate stats cache TTL. Hot path never
  1026. // waits on usage_logs; misses fail open and refresh asynchronously.
  1027. FreeQuotaStatsCacheSeconds int `mapstructure:"free_quota_stats_cache_seconds"`
  1028. }
  1029. // GatewayCNProvidersConfig 国产 OpenAI 兼容供应商(kimi/zhipu/deepseek)的余额检测配置。
  1030. //
  1031. // 仅作用于 payg(按量付费)账号(kimi/deepseek 有公开余额端点;zhipu 无,仅靠响应式 429/402)。
  1032. // - balance_check_enabled: 是否启用周期余额检测(默认 true)
  1033. // - balance_threshold: 余额低于此值(账户货币单位,默认 0.5)触发临时停调
  1034. // - balance_check_interval_minutes: 余额检测周期(分钟,默认 10)
  1035. type GatewayCNProvidersConfig struct {
  1036. BalanceCheckEnabled bool `mapstructure:"balance_check_enabled"`
  1037. BalanceThreshold float64 `mapstructure:"balance_threshold"`
  1038. BalanceCheckIntervalMinutes int `mapstructure:"balance_check_interval_minutes"`
  1039. }
  1040. type GatewayLiveConfig struct {
  1041. // MaxSessionDurationSeconds 是 Live 会话的硬上限。
  1042. MaxSessionDurationSeconds int `mapstructure:"max_session_duration_seconds"`
  1043. }
  1044. // GatewayOpenAIHTTP2Config OpenAI HTTP 上游协议配置。
  1045. // 默认启用 HTTP/2;在部分代理不兼容时按策略回退 HTTP/1.1。
  1046. type GatewayOpenAIHTTP2Config struct {
  1047. // Enabled: 是否启用 OpenAI HTTP/2 优先策略
  1048. Enabled bool `mapstructure:"enabled"`
  1049. // AllowProxyFallbackToHTTP1: HTTP/HTTPS 代理出现明确 H2 兼容错误时,临时回退 HTTP/1.1
  1050. AllowProxyFallbackToHTTP1 bool `mapstructure:"allow_proxy_fallback_to_http1"`
  1051. // FallbackErrorThreshold: 回退窗口内累计多少次兼容错误后触发回退
  1052. FallbackErrorThreshold int `mapstructure:"fallback_error_threshold"`
  1053. // FallbackWindowSeconds: 统计兼容错误的时间窗口(秒)
  1054. FallbackWindowSeconds int `mapstructure:"fallback_window_seconds"`
  1055. // FallbackTTLSeconds: 触发后回退 HTTP/1.1 的持续时间(秒)
  1056. FallbackTTLSeconds int `mapstructure:"fallback_ttl_seconds"`
  1057. }
  1058. // GatewayOpenAIProxyStreamCircuitConfig controls the bounded, in-process
  1059. // proxy-ID circuit used for incomplete OpenAI Responses SSE streams.
  1060. type GatewayOpenAIProxyStreamCircuitConfig struct {
  1061. // Disabled: 完全关闭代理断流熔断(默认开启)。
  1062. Disabled bool `mapstructure:"disabled"`
  1063. // FailureThreshold: 统计窗口内多少次断流后隔离代理。
  1064. FailureThreshold int `mapstructure:"failure_threshold"`
  1065. // WindowSeconds: 断流统计窗口(秒)。
  1066. WindowSeconds int `mapstructure:"window_seconds"`
  1067. // TTLSeconds: 代理隔离持续时间(秒)。
  1068. TTLSeconds int `mapstructure:"ttl_seconds"`
  1069. }
  1070. // UserMessageQueueConfig 用户消息串行队列配置
  1071. // 用于 Anthropic OAuth/SetupToken 账号的用户消息串行化发送
  1072. type UserMessageQueueConfig struct {
  1073. // Mode: 模式选择
  1074. // "serialize" = 账号级串行锁 + RPM 自适应延迟
  1075. // "throttle" = 仅 RPM 自适应前置延迟,不阻塞并发
  1076. // "" = 禁用(默认)
  1077. Mode string `mapstructure:"mode"`
  1078. // Enabled: 已废弃,仅向后兼容(等同于 mode: "serialize")
  1079. Enabled bool `mapstructure:"enabled"`
  1080. // LockTTLMs: 串行锁 TTL(毫秒),应大于最长请求时间
  1081. LockTTLMs int `mapstructure:"lock_ttl_ms"`
  1082. // WaitTimeoutMs: 等待获取锁的超时时间(毫秒)
  1083. WaitTimeoutMs int `mapstructure:"wait_timeout_ms"`
  1084. // MinDelayMs: RPM 自适应延迟下限(毫秒)
  1085. MinDelayMs int `mapstructure:"min_delay_ms"`
  1086. // MaxDelayMs: RPM 自适应延迟上限(毫秒)
  1087. MaxDelayMs int `mapstructure:"max_delay_ms"`
  1088. // CleanupIntervalSeconds: 孤儿锁清理间隔(秒),0 表示禁用
  1089. CleanupIntervalSeconds int `mapstructure:"cleanup_interval_seconds"`
  1090. }
  1091. // WaitTimeout 返回等待超时的 time.Duration
  1092. func (c *UserMessageQueueConfig) WaitTimeout() time.Duration {
  1093. if c.WaitTimeoutMs <= 0 {
  1094. return 30 * time.Second
  1095. }
  1096. return time.Duration(c.WaitTimeoutMs) * time.Millisecond
  1097. }
  1098. // GetEffectiveMode 返回生效的模式
  1099. // 注意:Mode 字段已在 load() 中做过白名单校验和规范化,此处无需重复验证
  1100. func (c *UserMessageQueueConfig) GetEffectiveMode() string {
  1101. if c.Mode == UMQModeSerialize || c.Mode == UMQModeThrottle {
  1102. return c.Mode
  1103. }
  1104. if c.Enabled {
  1105. return UMQModeSerialize // 向后兼容
  1106. }
  1107. return ""
  1108. }
  1109. // DefaultOpenAIWSClientFirstMessageTimeoutSeconds preserves the legacy ingress deadline.
  1110. const DefaultOpenAIWSClientFirstMessageTimeoutSeconds = 30
  1111. // GatewayOpenAIWSConfig OpenAI Responses WebSocket 配置。
  1112. // 注意:默认全局开启;如需回滚可使用 force_http 或关闭 enabled。
  1113. type GatewayOpenAIWSConfig struct {
  1114. // ModeRouterV2Enabled: 新版 WS mode 路由开关(默认 false;关闭时保持 legacy 行为)
  1115. ModeRouterV2Enabled bool `mapstructure:"mode_router_v2_enabled"`
  1116. // IngressModeDefault: ingress 默认模式(off/ctx_pool/passthrough/http_bridge)
  1117. IngressModeDefault string `mapstructure:"ingress_mode_default"`
  1118. // ClientFirstMessageTimeoutSeconds bounds the total time to read and decompress
  1119. // the first client response.create message after the WebSocket upgrade.
  1120. ClientFirstMessageTimeoutSeconds int `mapstructure:"client_first_message_timeout_seconds"`
  1121. // IngressInterTurnIdleTimeoutSeconds bounds the time a client may remain idle
  1122. // between completed ingress turns. Zero disables this protection.
  1123. IngressInterTurnIdleTimeoutSeconds int `mapstructure:"ingress_inter_turn_idle_timeout_seconds"`
  1124. // MaxIngressConnectionsPerAPIKey bounds live client WebSocket ingress sessions
  1125. // per API key across all instances. Zero disables this protection.
  1126. MaxIngressConnectionsPerAPIKey int `mapstructure:"max_ingress_connections_per_api_key"`
  1127. // Enabled: 全局总开关(默认 true)
  1128. Enabled bool `mapstructure:"enabled"`
  1129. // OAuthEnabled: 是否允许 OpenAI OAuth 账号使用 WS
  1130. OAuthEnabled bool `mapstructure:"oauth_enabled"`
  1131. // APIKeyEnabled: 是否允许 OpenAI API Key 账号使用 WS
  1132. APIKeyEnabled bool `mapstructure:"apikey_enabled"`
  1133. // ForceHTTP: 全局强制 HTTP(用于紧急回滚)
  1134. ForceHTTP bool `mapstructure:"force_http"`
  1135. // AllowStoreRecovery: 允许在 WSv2 下按策略恢复 store=true(默认 false)
  1136. AllowStoreRecovery bool `mapstructure:"allow_store_recovery"`
  1137. // IngressPreviousResponseRecoveryEnabled: ingress 模式收到 previous_response_not_found 时,是否允许自动去掉 previous_response_id 重试一次(默认 true)
  1138. IngressPreviousResponseRecoveryEnabled bool `mapstructure:"ingress_previous_response_recovery_enabled"`
  1139. // StoreDisabledConnMode: store=false 且无可复用会话连接时的建连策略(strict/adaptive/off)
  1140. // - strict: 强制新建连接(隔离优先)
  1141. // - adaptive: 仅在高风险失败后强制新建连接(性能与隔离折中)
  1142. // - off: 不强制新建连接(复用优先)
  1143. StoreDisabledConnMode string `mapstructure:"store_disabled_conn_mode"`
  1144. // StoreDisabledForceNewConn: store=false 且无可复用粘连连接时是否强制新建连接(默认 true,保障会话隔离)
  1145. // 兼容旧配置;当 StoreDisabledConnMode 为空时才生效。
  1146. StoreDisabledForceNewConn bool `mapstructure:"store_disabled_force_new_conn"`
  1147. // PrewarmGenerateEnabled: 是否启用 WSv2 generate=false 预热(默认 false)
  1148. PrewarmGenerateEnabled bool `mapstructure:"prewarm_generate_enabled"`
  1149. // ClientReadLimitBytes: 入站客户端 WS 单帧读取上限。
  1150. ClientReadLimitBytes int64 `mapstructure:"client_read_limit_bytes"`
  1151. // HTTPBridgeEnabled: 首包过大时,保持客户端 WS,改用 HTTP Responses 上游。
  1152. HTTPBridgeEnabled bool `mapstructure:"http_bridge_enabled"`
  1153. // HTTPBridgeThresholdBytes: 触发 HTTP bridge 的入站 WS payload 阈值。
  1154. HTTPBridgeThresholdBytes int64 `mapstructure:"http_bridge_threshold_bytes"`
  1155. // Feature 开关:v2 优先于 v1
  1156. ResponsesWebsockets bool `mapstructure:"responses_websockets"`
  1157. ResponsesWebsocketsV2 bool `mapstructure:"responses_websockets_v2"`
  1158. // 连接池参数
  1159. MaxConnsPerAccount int `mapstructure:"max_conns_per_account"`
  1160. MinIdlePerAccount int `mapstructure:"min_idle_per_account"`
  1161. MaxIdlePerAccount int `mapstructure:"max_idle_per_account"`
  1162. // DynamicMaxConnsByAccountConcurrencyEnabled: 是否按账号并发动态计算连接池上限
  1163. DynamicMaxConnsByAccountConcurrencyEnabled bool `mapstructure:"dynamic_max_conns_by_account_concurrency_enabled"`
  1164. // OAuthMaxConnsFactor: OAuth 账号连接池系数(effective=ceil(concurrency*factor))
  1165. OAuthMaxConnsFactor float64 `mapstructure:"oauth_max_conns_factor"`
  1166. // APIKeyMaxConnsFactor: API Key 账号连接池系数(effective=ceil(concurrency*factor))
  1167. APIKeyMaxConnsFactor float64 `mapstructure:"apikey_max_conns_factor"`
  1168. DialTimeoutSeconds int `mapstructure:"dial_timeout_seconds"`
  1169. ReadTimeoutSeconds int `mapstructure:"read_timeout_seconds"`
  1170. WriteTimeoutSeconds int `mapstructure:"write_timeout_seconds"`
  1171. PoolTargetUtilization float64 `mapstructure:"pool_target_utilization"`
  1172. QueueLimitPerConn int `mapstructure:"queue_limit_per_conn"`
  1173. // EventFlushBatchSize: WS 流式写出批量 flush 阈值(事件条数)
  1174. EventFlushBatchSize int `mapstructure:"event_flush_batch_size"`
  1175. // EventFlushIntervalMS: WS 流式写出最大等待时间(毫秒);0 表示仅按 batch 触发
  1176. EventFlushIntervalMS int `mapstructure:"event_flush_interval_ms"`
  1177. // PrewarmCooldownMS: 连接池预热触发冷却时间(毫秒)
  1178. PrewarmCooldownMS int `mapstructure:"prewarm_cooldown_ms"`
  1179. // FallbackCooldownSeconds: WS 回退冷却窗口,避免 WS/HTTP 抖动;0 表示关闭冷却
  1180. FallbackCooldownSeconds int `mapstructure:"fallback_cooldown_seconds"`
  1181. // RetryBackoffInitialMS: WS 重试初始退避(毫秒);<=0 表示关闭退避
  1182. RetryBackoffInitialMS int `mapstructure:"retry_backoff_initial_ms"`
  1183. // RetryBackoffMaxMS: WS 重试最大退避(毫秒)
  1184. RetryBackoffMaxMS int `mapstructure:"retry_backoff_max_ms"`
  1185. // RetryJitterRatio: WS 重试退避抖动比例(0-1)
  1186. RetryJitterRatio float64 `mapstructure:"retry_jitter_ratio"`
  1187. // RetryTotalBudgetMS: WS 单次请求重试总预算(毫秒);0 表示关闭预算限制
  1188. RetryTotalBudgetMS int `mapstructure:"retry_total_budget_ms"`
  1189. // PayloadLogSampleRate: payload_schema 日志采样率(0-1)
  1190. PayloadLogSampleRate float64 `mapstructure:"payload_log_sample_rate"`
  1191. // 账号调度与粘连参数
  1192. LBTopK int `mapstructure:"lb_top_k"`
  1193. // StickySessionTTLSeconds: session_hash -> account_id 粘连 TTL
  1194. StickySessionTTLSeconds int `mapstructure:"sticky_session_ttl_seconds"`
  1195. // SessionHashReadOldFallback: 会话哈希迁移期是否允许“新 key 未命中时回退读旧 SHA-256 key”
  1196. SessionHashReadOldFallback bool `mapstructure:"session_hash_read_old_fallback"`
  1197. // SessionHashDualWriteOld: 会话哈希迁移期是否双写旧 SHA-256 key(短 TTL)
  1198. SessionHashDualWriteOld bool `mapstructure:"session_hash_dual_write_old"`
  1199. // MetadataBridgeEnabled: RequestMetadata 迁移期是否保留旧 ctxkey.* 兼容桥接
  1200. MetadataBridgeEnabled bool `mapstructure:"metadata_bridge_enabled"`
  1201. // StickyResponseIDTTLSeconds: response_id -> account_id 粘连 TTL
  1202. StickyResponseIDTTLSeconds int `mapstructure:"sticky_response_id_ttl_seconds"`
  1203. // StickyPreviousResponseTTLSeconds: 兼容旧键(当新键未设置时回退)
  1204. StickyPreviousResponseTTLSeconds int `mapstructure:"sticky_previous_response_ttl_seconds"`
  1205. SchedulerScoreWeights GatewayOpenAIWSSchedulerScoreWeights `mapstructure:"scheduler_score_weights"`
  1206. }
  1207. // GatewayOpenAIWSSchedulerScoreWeights 账号调度打分权重。
  1208. type GatewayOpenAIWSSchedulerScoreWeights struct {
  1209. Priority float64 `mapstructure:"priority"`
  1210. Load float64 `mapstructure:"load"`
  1211. Queue float64 `mapstructure:"queue"`
  1212. ErrorRate float64 `mapstructure:"error_rate"`
  1213. TTFT float64 `mapstructure:"ttft"`
  1214. // Reset 倾向「会话窗口最早重置」的账号(use-it-or-lose-it)。
  1215. // >0 时,剩余重置时间越短的账号得分越高,从而被优先用尽。默认 0(关闭,不改变原有行为)。
  1216. Reset float64 `mapstructure:"reset"`
  1217. // QuotaHeadroom 倾向 7d 剩余额度更健康的账号;默认 0(关闭,不改变原有行为)。
  1218. QuotaHeadroom float64 `mapstructure:"quota_headroom"`
  1219. // UpstreamCost 倾向上游声明倍率更低的账号;默认 0(关闭,不改变原有行为)。
  1220. UpstreamCost float64 `mapstructure:"upstream_cost"`
  1221. // PreviousResponse/SessionSticky 仅在开启 OpenAI 高级调度的粘性加权时生效。
  1222. PreviousResponse float64 `mapstructure:"previous_response"`
  1223. SessionSticky float64 `mapstructure:"session_sticky"`
  1224. }
  1225. func (w GatewayOpenAIWSSchedulerScoreWeights) BaseWeightSum() float64 {
  1226. return w.Priority + w.Load + w.Queue + w.ErrorRate + w.TTFT + w.Reset + w.QuotaHeadroom + w.UpstreamCost
  1227. }
  1228. func (w GatewayOpenAIWSSchedulerScoreWeights) TotalWeightSum() float64 {
  1229. return w.BaseWeightSum() + w.PreviousResponse + w.SessionSticky
  1230. }
  1231. func (w GatewayOpenAIWSSchedulerScoreWeights) IsValid() bool {
  1232. for _, weight := range []float64{
  1233. w.Priority, w.Load, w.Queue, w.ErrorRate, w.TTFT, w.Reset,
  1234. w.QuotaHeadroom, w.UpstreamCost, w.PreviousResponse, w.SessionSticky,
  1235. } {
  1236. if weight < 0 || math.IsNaN(weight) || math.IsInf(weight, 0) {
  1237. return false
  1238. }
  1239. }
  1240. baseSum := w.BaseWeightSum()
  1241. return baseSum > 0 && !math.IsNaN(baseSum) && !math.IsInf(baseSum, 0) &&
  1242. !math.IsNaN(w.TotalWeightSum()) && !math.IsInf(w.TotalWeightSum(), 0)
  1243. }
  1244. // GatewayOpenAISchedulerConfig OpenAI 高级调度器配置。
  1245. type GatewayOpenAISchedulerConfig struct {
  1246. // StickyEscapeEnabled: 是否允许 session_hash sticky 在账号健康度劣化时临时逃逸
  1247. StickyEscapeEnabled bool `mapstructure:"sticky_escape_enabled"`
  1248. // StickyEscapeTTFTMs: TTFT EWMA 超过该阈值时跳过 sticky
  1249. StickyEscapeTTFTMs int `mapstructure:"sticky_escape_ttft_ms"`
  1250. // StickyEscapeErrorRate: 错误率 EWMA 超过该阈值时跳过 sticky
  1251. StickyEscapeErrorRate float64 `mapstructure:"sticky_escape_error_rate"`
  1252. }
  1253. // GatewayUsageRecordConfig 使用量记录异步队列配置
  1254. type GatewayUsageRecordConfig struct {
  1255. // WorkerCount: worker 初始数量(自动扩缩容开启时作为初始并发上限)
  1256. WorkerCount int `mapstructure:"worker_count"`
  1257. // QueueSize: 队列容量(有界)
  1258. QueueSize int `mapstructure:"queue_size"`
  1259. // TaskTimeoutSeconds: 单个使用量记录任务超时(秒)
  1260. TaskTimeoutSeconds int `mapstructure:"task_timeout_seconds"`
  1261. // OverflowPolicy: 队列满时策略(drop/sample/sync)
  1262. OverflowPolicy string `mapstructure:"overflow_policy"`
  1263. // OverflowSamplePercent: sample 策略下,同步回写采样百分比(1-100)
  1264. OverflowSamplePercent int `mapstructure:"overflow_sample_percent"`
  1265. // AutoScaleEnabled: 是否启用 worker 自动扩缩容
  1266. AutoScaleEnabled bool `mapstructure:"auto_scale_enabled"`
  1267. // AutoScaleMinWorkers: 自动扩缩容最小 worker 数
  1268. AutoScaleMinWorkers int `mapstructure:"auto_scale_min_workers"`
  1269. // AutoScaleMaxWorkers: 自动扩缩容最大 worker 数
  1270. AutoScaleMaxWorkers int `mapstructure:"auto_scale_max_workers"`
  1271. // AutoScaleUpQueuePercent: 队列占用率达到该阈值时触发扩容
  1272. AutoScaleUpQueuePercent int `mapstructure:"auto_scale_up_queue_percent"`
  1273. // AutoScaleDownQueuePercent: 队列占用率低于该阈值时触发缩容
  1274. AutoScaleDownQueuePercent int `mapstructure:"auto_scale_down_queue_percent"`
  1275. // AutoScaleUpStep: 每次扩容步长
  1276. AutoScaleUpStep int `mapstructure:"auto_scale_up_step"`
  1277. // AutoScaleDownStep: 每次缩容步长
  1278. AutoScaleDownStep int `mapstructure:"auto_scale_down_step"`
  1279. // AutoScaleCheckIntervalSeconds: 自动扩缩容检测间隔(秒)
  1280. AutoScaleCheckIntervalSeconds int `mapstructure:"auto_scale_check_interval_seconds"`
  1281. // AutoScaleCooldownSeconds: 自动扩缩容冷却时间(秒)
  1282. AutoScaleCooldownSeconds int `mapstructure:"auto_scale_cooldown_seconds"`
  1283. }
  1284. // TLSFingerprintConfig TLS指纹伪装配置
  1285. // 用于模拟 Claude CLI (Node.js) 的 TLS 握手特征,避免被识别为非官方客户端
  1286. type TLSFingerprintConfig struct {
  1287. // Enabled: 是否全局启用TLS指纹功能
  1288. Enabled bool `mapstructure:"enabled"`
  1289. // Profiles: 预定义的TLS指纹配置模板
  1290. // key 为模板名称,如 "claude_cli_v2", "chrome_120" 等
  1291. Profiles map[string]TLSProfileConfig `mapstructure:"profiles"`
  1292. }
  1293. // TLSProfileConfig 单个TLS指纹模板的配置
  1294. // 所有列表字段为空时使用内置默认值(Claude CLI 2.x / Node.js 20.x)
  1295. // 建议通过 TLS 指纹采集工具 (tests/tls-fingerprint-web) 获取完整配置
  1296. type TLSProfileConfig struct {
  1297. // Name: 模板显示名称
  1298. Name string `mapstructure:"name"`
  1299. // EnableGREASE: 是否启用GREASE扩展(Chrome使用,Node.js不使用)
  1300. EnableGREASE bool `mapstructure:"enable_grease"`
  1301. // CipherSuites: TLS加密套件列表
  1302. CipherSuites []uint16 `mapstructure:"cipher_suites"`
  1303. // Curves: 椭圆曲线列表
  1304. Curves []uint16 `mapstructure:"curves"`
  1305. // PointFormats: 点格式列表
  1306. PointFormats []uint16 `mapstructure:"point_formats"`
  1307. // SignatureAlgorithms: 签名算法列表
  1308. SignatureAlgorithms []uint16 `mapstructure:"signature_algorithms"`
  1309. // ALPNProtocols: ALPN协议列表(如 ["h2", "http/1.1"])
  1310. ALPNProtocols []string `mapstructure:"alpn_protocols"`
  1311. // SupportedVersions: 支持的TLS版本列表(如 [0x0304, 0x0303] 即 TLS1.3, TLS1.2)
  1312. SupportedVersions []uint16 `mapstructure:"supported_versions"`
  1313. // KeyShareGroups: Key Share中发送的曲线组(如 [29] 即 X25519)
  1314. KeyShareGroups []uint16 `mapstructure:"key_share_groups"`
  1315. // PSKModes: PSK密钥交换模式(如 [1] 即 psk_dhe_ke)
  1316. PSKModes []uint16 `mapstructure:"psk_modes"`
  1317. // Extensions: TLS扩展类型ID列表,按发送顺序排列
  1318. // 空则使用内置默认顺序 [0,11,10,35,16,22,23,13,43,45,51]
  1319. // GREASE值(如0x0a0a)会自动插入GREASE扩展
  1320. Extensions []uint16 `mapstructure:"extensions"`
  1321. }
  1322. // GatewaySchedulingConfig accounts scheduling configuration.
  1323. type GatewaySchedulingConfig struct {
  1324. // 粘性会话排队配置
  1325. StickySessionMaxWaiting int `mapstructure:"sticky_session_max_waiting"`
  1326. StickySessionWaitTimeout time.Duration `mapstructure:"sticky_session_wait_timeout"`
  1327. // 兜底排队配置
  1328. FallbackWaitTimeout time.Duration `mapstructure:"fallback_wait_timeout"`
  1329. FallbackMaxWaiting int `mapstructure:"fallback_max_waiting"`
  1330. // 兜底层账户选择策略: "last_used"(按最后使用时间排序,默认) 或 "random"(随机)
  1331. FallbackSelectionMode string `mapstructure:"fallback_selection_mode"`
  1332. // PreferSoonestReset 开启后,负载感知选择会优先选用「会话窗口最早重置」的账号
  1333. // (use-it-or-lose-it:先用尽即将重置的账号,保留重置时间还很久的账号)。
  1334. // 默认 false,保持原有「优先级 → 负载率 → LRU」行为不变。
  1335. PreferSoonestReset bool `mapstructure:"prefer_soonest_reset"`
  1336. // 负载计算
  1337. LoadBatchEnabled bool `mapstructure:"load_batch_enabled"`
  1338. LoadBatchCacheTTLMS int `mapstructure:"load_batch_cache_ttl_ms"`
  1339. // 快照桶读取时的 MGET 分块大小
  1340. SnapshotMGetChunkSize int `mapstructure:"snapshot_mget_chunk_size"`
  1341. // 快照重建时的缓存写入分块大小
  1342. SnapshotWriteChunkSize int `mapstructure:"snapshot_write_chunk_size"`
  1343. // 过期槽位清理周期(0 表示禁用)
  1344. SlotCleanupInterval time.Duration `mapstructure:"slot_cleanup_interval"`
  1345. // 受控回源配置
  1346. DbFallbackEnabled bool `mapstructure:"db_fallback_enabled"`
  1347. // 受控回源超时(秒),0 表示不额外收紧超时
  1348. DbFallbackTimeoutSeconds int `mapstructure:"db_fallback_timeout_seconds"`
  1349. // 受控回源限流(实例级 QPS),0 表示不限制
  1350. DbFallbackMaxQPS int `mapstructure:"db_fallback_max_qps"`
  1351. // Outbox 轮询与滞后阈值配置
  1352. // Outbox 轮询周期(秒)
  1353. OutboxPollIntervalSeconds int `mapstructure:"outbox_poll_interval_seconds"`
  1354. // Outbox 滞后告警阈值(秒)
  1355. OutboxLagWarnSeconds int `mapstructure:"outbox_lag_warn_seconds"`
  1356. // Outbox 触发强制重建阈值(秒)
  1357. OutboxLagRebuildSeconds int `mapstructure:"outbox_lag_rebuild_seconds"`
  1358. // Outbox 连续滞后触发次数
  1359. OutboxLagRebuildFailures int `mapstructure:"outbox_lag_rebuild_failures"`
  1360. // Outbox 积压触发重建阈值(行数)
  1361. OutboxBacklogRebuildRows int `mapstructure:"outbox_backlog_rebuild_rows"`
  1362. // 全量重建周期配置
  1363. // 全量重建周期(秒),0 表示禁用
  1364. FullRebuildIntervalSeconds int `mapstructure:"full_rebuild_interval_seconds"`
  1365. }
  1366. func (s *ServerConfig) Address() string {
  1367. return fmt.Sprintf("%s:%d", s.Host, s.Port)
  1368. }
  1369. // DatabaseConfig 数据库连接配置
  1370. // 性能优化:新增连接池参数,避免频繁创建/销毁连接
  1371. type DatabaseConfig struct {
  1372. Host string `mapstructure:"host"`
  1373. Port int `mapstructure:"port"`
  1374. User string `mapstructure:"user"`
  1375. Password string `mapstructure:"password"`
  1376. DBName string `mapstructure:"dbname"`
  1377. SSLMode string `mapstructure:"sslmode"`
  1378. // 连接池配置(性能优化:可配置化连接池参数)
  1379. // MaxOpenConns: 最大打开连接数,控制数据库连接上限,防止资源耗尽
  1380. MaxOpenConns int `mapstructure:"max_open_conns"`
  1381. // MaxIdleConns: 最大空闲连接数,保持热连接减少建连延迟
  1382. MaxIdleConns int `mapstructure:"max_idle_conns"`
  1383. // ConnMaxLifetimeMinutes: 连接最大存活时间,防止长连接导致的资源泄漏
  1384. ConnMaxLifetimeMinutes int `mapstructure:"conn_max_lifetime_minutes"`
  1385. // ConnMaxIdleTimeMinutes: 空闲连接最大存活时间,及时释放不活跃连接
  1386. ConnMaxIdleTimeMinutes int `mapstructure:"conn_max_idle_time_minutes"`
  1387. // UserPlatformQuotaFlusherEnabled: 是否启用 user×platform 配额写聚合 flusher
  1388. UserPlatformQuotaFlusherEnabled bool `mapstructure:"user_platform_quota_flusher_enabled"`
  1389. // UserPlatformQuotaFlushIntervalMs: flusher 刷写间隔(毫秒)
  1390. UserPlatformQuotaFlushIntervalMs int `mapstructure:"user_platform_quota_flush_interval_ms"`
  1391. // UserPlatformQuotaFlushBatchSize: flusher 单批最大条数
  1392. // 建议 ≤ 6000(单条 UPSERT 原子上限)
  1393. UserPlatformQuotaFlushBatchSize int `mapstructure:"user_platform_quota_flush_batch_size"`
  1394. }
  1395. func (d *DatabaseConfig) DSN() string {
  1396. // 当密码为空时不包含 password 参数,避免 libpq 解析错误
  1397. if d.Password == "" {
  1398. return fmt.Sprintf(
  1399. "host=%s port=%d user=%s dbname=%s sslmode=%s",
  1400. d.Host, d.Port, d.User, d.DBName, d.SSLMode,
  1401. )
  1402. }
  1403. return fmt.Sprintf(
  1404. "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
  1405. d.Host, d.Port, d.User, d.Password, d.DBName, d.SSLMode,
  1406. )
  1407. }
  1408. // DSNWithTimezone returns DSN with timezone setting
  1409. func (d *DatabaseConfig) DSNWithTimezone(tz string) string {
  1410. if tz == "" {
  1411. tz = "Asia/Shanghai"
  1412. }
  1413. // 当密码为空时不包含 password 参数,避免 libpq 解析错误
  1414. if d.Password == "" {
  1415. return fmt.Sprintf(
  1416. "host=%s port=%d user=%s dbname=%s sslmode=%s TimeZone=%s",
  1417. d.Host, d.Port, d.User, d.DBName, d.SSLMode, tz,
  1418. )
  1419. }
  1420. return fmt.Sprintf(
  1421. "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s TimeZone=%s",
  1422. d.Host, d.Port, d.User, d.Password, d.DBName, d.SSLMode, tz,
  1423. )
  1424. }
  1425. // RedisConfig Redis 连接配置
  1426. // 性能优化:新增连接池和超时参数,提升高并发场景下的吞吐量
  1427. type RedisConfig struct {
  1428. Host string `mapstructure:"host"`
  1429. Port int `mapstructure:"port"`
  1430. Username string `mapstructure:"username"`
  1431. Password string `mapstructure:"password"`
  1432. DB int `mapstructure:"db"`
  1433. // 连接池与超时配置(性能优化:可配置化连接池参数)
  1434. // DialTimeoutSeconds: 建立连接超时,防止慢连接阻塞
  1435. DialTimeoutSeconds int `mapstructure:"dial_timeout_seconds"`
  1436. // ReadTimeoutSeconds: 读取超时,避免慢查询阻塞连接池
  1437. ReadTimeoutSeconds int `mapstructure:"read_timeout_seconds"`
  1438. // WriteTimeoutSeconds: 写入超时,避免慢写入阻塞连接池
  1439. WriteTimeoutSeconds int `mapstructure:"write_timeout_seconds"`
  1440. // PoolSize: 连接池大小,控制最大并发连接数
  1441. PoolSize int `mapstructure:"pool_size"`
  1442. // MinIdleConns: 最小空闲连接数,保持热连接减少冷启动延迟
  1443. MinIdleConns int `mapstructure:"min_idle_conns"`
  1444. // EnableTLS: 是否启用 TLS/SSL 连接
  1445. EnableTLS bool `mapstructure:"enable_tls"`
  1446. }
  1447. func (r *RedisConfig) Address() string {
  1448. return fmt.Sprintf("%s:%d", r.Host, r.Port)
  1449. }
  1450. type OpsConfig struct {
  1451. // Enabled controls whether ops features should run.
  1452. //
  1453. // NOTE: vNext still has a DB-backed feature flag (ops_monitoring_enabled) for runtime on/off.
  1454. // This config flag is the "hard switch" for deployments that want to disable ops completely.
  1455. Enabled bool `mapstructure:"enabled"`
  1456. // UsePreaggregatedTables prefers ops_metrics_hourly/daily for long-window dashboard queries.
  1457. UsePreaggregatedTables bool `mapstructure:"use_preaggregated_tables"`
  1458. // Cleanup controls periodic deletion of old ops data to prevent unbounded growth.
  1459. Cleanup OpsCleanupConfig `mapstructure:"cleanup"`
  1460. // MetricsCollectorCache controls Redis caching for expensive per-window collector queries.
  1461. MetricsCollectorCache OpsMetricsCollectorCacheConfig `mapstructure:"metrics_collector_cache"`
  1462. // Pre-aggregation configuration.
  1463. Aggregation OpsAggregationConfig `mapstructure:"aggregation"`
  1464. }
  1465. type OpsCleanupConfig struct {
  1466. Enabled bool `mapstructure:"enabled"`
  1467. Schedule string `mapstructure:"schedule"`
  1468. // Retention days. Error and metrics targets accept 0 as an explicit truncate;
  1469. // system logs require a positive value because their runtime setting is bounded.
  1470. ErrorLogRetentionDays int `mapstructure:"error_log_retention_days"`
  1471. SystemLogRetentionDays int `mapstructure:"system_log_retention_days"`
  1472. MinuteMetricsRetentionDays int `mapstructure:"minute_metrics_retention_days"`
  1473. HourlyMetricsRetentionDays int `mapstructure:"hourly_metrics_retention_days"`
  1474. }
  1475. type OpsAggregationConfig struct {
  1476. Enabled bool `mapstructure:"enabled"`
  1477. }
  1478. type OpsMetricsCollectorCacheConfig struct {
  1479. Enabled bool `mapstructure:"enabled"`
  1480. TTL time.Duration `mapstructure:"ttl"`
  1481. }
  1482. type JWTConfig struct {
  1483. Secret string `mapstructure:"secret"`
  1484. ExpireHour int `mapstructure:"expire_hour"`
  1485. // AccessTokenExpireMinutes: Access Token有效期(分钟)
  1486. // - >0: 使用分钟配置(优先级高于 ExpireHour)
  1487. // - =0: 回退使用 ExpireHour(向后兼容旧配置)
  1488. AccessTokenExpireMinutes int `mapstructure:"access_token_expire_minutes"`
  1489. // RefreshTokenExpireDays: Refresh Token有效期(天),默认30天
  1490. RefreshTokenExpireDays int `mapstructure:"refresh_token_expire_days"`
  1491. // RefreshWindowMinutes: 刷新窗口(分钟),在Access Token过期前多久开始允许刷新
  1492. RefreshWindowMinutes int `mapstructure:"refresh_window_minutes"`
  1493. }
  1494. // TotpConfig TOTP 双因素认证配置
  1495. type TotpConfig struct {
  1496. // EncryptionKey 用于加密 TOTP 密钥的 AES-256 密钥(32 字节 hex 编码)
  1497. // 如果为空,将自动生成一个随机密钥(仅适用于开发环境)
  1498. EncryptionKey string `mapstructure:"encryption_key"`
  1499. // EncryptionKeyConfigured 标记加密密钥是否为手动配置(非自动生成)
  1500. // 只有手动配置了密钥才允许在管理后台启用 TOTP 功能
  1501. EncryptionKeyConfigured bool `mapstructure:"-"`
  1502. }
  1503. type TurnstileConfig struct {
  1504. Required bool `mapstructure:"required"`
  1505. }
  1506. type DefaultConfig struct {
  1507. AdminEmail string `mapstructure:"admin_email"`
  1508. AdminPassword string `mapstructure:"admin_password"`
  1509. UserConcurrency int `mapstructure:"user_concurrency"`
  1510. UserBalance float64 `mapstructure:"user_balance"`
  1511. APIKeyPrefix string `mapstructure:"api_key_prefix"`
  1512. RateMultiplier float64 `mapstructure:"rate_multiplier"`
  1513. }
  1514. type RateLimitConfig struct {
  1515. OverloadCooldownMinutes int `mapstructure:"overload_cooldown_minutes"` // 529过载冷却时间(分钟)
  1516. OAuth401CooldownMinutes int `mapstructure:"oauth_401_cooldown_minutes"` // OAuth 401临时不可调度冷却(分钟)
  1517. }
  1518. // APIKeyAuthCacheConfig API Key 认证缓存配置
  1519. type APIKeyAuthCacheConfig struct {
  1520. L1Size int `mapstructure:"l1_size"`
  1521. L1TTLSeconds int `mapstructure:"l1_ttl_seconds"`
  1522. L2TTLSeconds int `mapstructure:"l2_ttl_seconds"`
  1523. NegativeTTLSeconds int `mapstructure:"negative_ttl_seconds"`
  1524. JitterPercent int `mapstructure:"jitter_percent"`
  1525. Singleflight bool `mapstructure:"singleflight"`
  1526. LookupConcurrency int `mapstructure:"lookup_concurrency"`
  1527. InvalidAbuse InvalidAuthAbuseConfig `mapstructure:"invalid_abuse"`
  1528. }
  1529. type InvalidAuthAbuseConfig struct {
  1530. Enabled bool `mapstructure:"enabled"`
  1531. Threshold int `mapstructure:"threshold"`
  1532. WindowSeconds int `mapstructure:"window_seconds"`
  1533. BlockSeconds int `mapstructure:"block_seconds"`
  1534. Capacity int `mapstructure:"capacity"`
  1535. }
  1536. // SubscriptionCacheConfig 订阅认证 L1 缓存配置
  1537. type SubscriptionCacheConfig struct {
  1538. L1Size int `mapstructure:"l1_size"`
  1539. L1TTLSeconds int `mapstructure:"l1_ttl_seconds"`
  1540. JitterPercent int `mapstructure:"jitter_percent"`
  1541. }
  1542. // SubscriptionMaintenanceConfig 订阅窗口维护后台任务配置。
  1543. // 用于将“请求路径触发的维护动作”有界化,避免高并发下 goroutine 膨胀。
  1544. type SubscriptionMaintenanceConfig struct {
  1545. WorkerCount int `mapstructure:"worker_count"`
  1546. QueueSize int `mapstructure:"queue_size"`
  1547. }
  1548. // DashboardCacheConfig 仪表盘统计缓存配置
  1549. type DashboardCacheConfig struct {
  1550. // Enabled: 是否启用仪表盘缓存
  1551. Enabled bool `mapstructure:"enabled"`
  1552. // KeyPrefix: Redis key 前缀,用于多环境隔离
  1553. KeyPrefix string `mapstructure:"key_prefix"`
  1554. // StatsFreshTTLSeconds: 缓存命中认为“新鲜”的时间窗口(秒)
  1555. StatsFreshTTLSeconds int `mapstructure:"stats_fresh_ttl_seconds"`
  1556. // StatsTTLSeconds: Redis 缓存总 TTL(秒)
  1557. StatsTTLSeconds int `mapstructure:"stats_ttl_seconds"`
  1558. // StatsRefreshTimeoutSeconds: 异步刷新超时(秒)
  1559. StatsRefreshTimeoutSeconds int `mapstructure:"stats_refresh_timeout_seconds"`
  1560. }
  1561. // DashboardAggregationConfig 仪表盘预聚合配置
  1562. type DashboardAggregationConfig struct {
  1563. // Enabled: 是否启用预聚合作业
  1564. Enabled bool `mapstructure:"enabled"`
  1565. // IntervalSeconds: 聚合刷新间隔(秒)
  1566. IntervalSeconds int `mapstructure:"interval_seconds"`
  1567. // LookbackSeconds: 回看窗口(秒)
  1568. LookbackSeconds int `mapstructure:"lookback_seconds"`
  1569. // BackfillEnabled: 是否允许全量回填
  1570. BackfillEnabled bool `mapstructure:"backfill_enabled"`
  1571. // BackfillMaxDays: 回填最大跨度(天)
  1572. BackfillMaxDays int `mapstructure:"backfill_max_days"`
  1573. // Retention: 各表保留窗口(天)
  1574. Retention DashboardAggregationRetentionConfig `mapstructure:"retention"`
  1575. // RecomputeDays: 启动时重算最近 N 天
  1576. RecomputeDays int `mapstructure:"recompute_days"`
  1577. }
  1578. // DashboardAggregationRetentionConfig 预聚合保留窗口
  1579. type DashboardAggregationRetentionConfig struct {
  1580. UsageLogsDays int `mapstructure:"usage_logs_days"`
  1581. UsageBillingDedupDays int `mapstructure:"usage_billing_dedup_days"`
  1582. HourlyDays int `mapstructure:"hourly_days"`
  1583. DailyDays int `mapstructure:"daily_days"`
  1584. }
  1585. // UsageCleanupConfig 使用记录清理任务配置
  1586. type UsageCleanupConfig struct {
  1587. // Enabled: 是否启用清理任务执行器
  1588. Enabled bool `mapstructure:"enabled"`
  1589. // MaxRangeDays: 单次任务允许的最大时间跨度(天)
  1590. MaxRangeDays int `mapstructure:"max_range_days"`
  1591. // BatchSize: 单批删除数量
  1592. BatchSize int `mapstructure:"batch_size"`
  1593. // WorkerIntervalSeconds: 后台任务轮询间隔(秒)
  1594. WorkerIntervalSeconds int `mapstructure:"worker_interval_seconds"`
  1595. // TaskTimeoutSeconds: 单次任务最大执行时长(秒)
  1596. TaskTimeoutSeconds int `mapstructure:"task_timeout_seconds"`
  1597. }
  1598. func NormalizeRunMode(value string) string {
  1599. normalized := strings.ToLower(strings.TrimSpace(value))
  1600. switch normalized {
  1601. case RunModeStandard, RunModeSimple:
  1602. return normalized
  1603. default:
  1604. return RunModeStandard
  1605. }
  1606. }
  1607. // Load 读取并校验完整配置(要求 jwt.secret 已显式提供)。
  1608. func Load() (*Config, error) {
  1609. return load(false)
  1610. }
  1611. // LoadForBootstrap 读取启动阶段配置。
  1612. //
  1613. // 启动阶段允许 jwt.secret 先留空,后续由数据库初始化流程补齐并再次完整校验。
  1614. func LoadForBootstrap() (*Config, error) {
  1615. return load(true)
  1616. }
  1617. func load(allowMissingJWTSecret bool) (*Config, error) {
  1618. viper.SetConfigName("config")
  1619. viper.SetConfigType("yaml")
  1620. configureConfigSource(viper.SetConfigFile, viper.AddConfigPath)
  1621. // 环境变量支持
  1622. viper.AutomaticEnv()
  1623. viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
  1624. if tz, ok := os.LookupEnv("TZ"); ok && strings.TrimSpace(tz) != "" {
  1625. // AutomaticEnv 会先把 timezone 映射到 TIMEZONE;显式 Set 保证标准 TZ 变量优先。
  1626. viper.Set("timezone", strings.TrimSpace(tz))
  1627. }
  1628. if err := viper.BindEnv("server.enable_server_timing", "ENABLE_SERVER_TIMING"); err != nil {
  1629. return nil, fmt.Errorf("bind ENABLE_SERVER_TIMING: %w", err)
  1630. }
  1631. // 默认值
  1632. setDefaults()
  1633. if err := viper.ReadInConfig(); err != nil {
  1634. if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
  1635. return nil, fmt.Errorf("read config error: %w", err)
  1636. }
  1637. // 配置文件不存在时使用默认值
  1638. }
  1639. trustedProxiesEnv, trustedProxiesEnvConfigured := os.LookupEnv("SERVER_TRUSTED_PROXIES")
  1640. forwardedClientIPHeadersEnv, forwardedClientIPHeadersEnvConfigured := os.LookupEnv("SECURITY_FORWARDED_CLIENT_IP_HEADERS")
  1641. trustedProxiesConfigured := viper.InConfig("server.trusted_proxies") ||
  1642. viper.IsSet("server.trusted_proxies") || trustedProxiesEnvConfigured
  1643. var cfg Config
  1644. if err := viper.Unmarshal(&cfg); err != nil {
  1645. return nil, fmt.Errorf("unmarshal config error: %w", err)
  1646. }
  1647. if trustedProxiesEnvConfigured {
  1648. cfg.Server.TrustedProxies = normalizeStringSlice(strings.Split(trustedProxiesEnv, ","))
  1649. }
  1650. if forwardedClientIPHeadersEnvConfigured {
  1651. cfg.Security.ForwardedClientIPHeaders = normalizeStringSlice(strings.Split(forwardedClientIPHeadersEnv, ","))
  1652. }
  1653. cfg.Server.TrustedProxiesConfigured = trustedProxiesConfigured
  1654. if cfg.Gateway.OpenAIScheduler.StickyEscapeTTFTMs == 0 {
  1655. cfg.Gateway.OpenAIScheduler.StickyEscapeTTFTMs = 15000
  1656. }
  1657. if cfg.Gateway.OpenAIScheduler.StickyEscapeErrorRate == 0 {
  1658. cfg.Gateway.OpenAIScheduler.StickyEscapeErrorRate = 0.5
  1659. }
  1660. // Kept as a backstop: setEnvReachableDefaults now registers this key with its
  1661. // effective default (true), so IsSet always reports true and this branch no
  1662. // longer fires. It still guards the default if that registration is dropped.
  1663. if !cfg.Gateway.OpenAIScheduler.StickyEscapeEnabled && !viper.IsSet("gateway.openai_scheduler.sticky_escape_enabled") {
  1664. cfg.Gateway.OpenAIScheduler.StickyEscapeEnabled = true
  1665. }
  1666. cfg.RunMode = NormalizeRunMode(cfg.RunMode)
  1667. cfg.Server.Mode = strings.ToLower(strings.TrimSpace(cfg.Server.Mode))
  1668. if cfg.Server.Mode == "" {
  1669. cfg.Server.Mode = "debug"
  1670. }
  1671. cfg.Server.FrontendURL = strings.TrimSpace(cfg.Server.FrontendURL)
  1672. cfg.JWT.Secret = strings.TrimSpace(cfg.JWT.Secret)
  1673. cfg.LinuxDo.ClientID = strings.TrimSpace(cfg.LinuxDo.ClientID)
  1674. cfg.LinuxDo.ClientSecret = strings.TrimSpace(cfg.LinuxDo.ClientSecret)
  1675. cfg.LinuxDo.AuthorizeURL = strings.TrimSpace(cfg.LinuxDo.AuthorizeURL)
  1676. cfg.LinuxDo.TokenURL = strings.TrimSpace(cfg.LinuxDo.TokenURL)
  1677. cfg.LinuxDo.UserInfoURL = strings.TrimSpace(cfg.LinuxDo.UserInfoURL)
  1678. cfg.LinuxDo.Scopes = strings.TrimSpace(cfg.LinuxDo.Scopes)
  1679. cfg.LinuxDo.RedirectURL = strings.TrimSpace(cfg.LinuxDo.RedirectURL)
  1680. cfg.LinuxDo.FrontendRedirectURL = strings.TrimSpace(cfg.LinuxDo.FrontendRedirectURL)
  1681. cfg.LinuxDo.TokenAuthMethod = strings.ToLower(strings.TrimSpace(cfg.LinuxDo.TokenAuthMethod))
  1682. cfg.LinuxDo.UserInfoEmailPath = strings.TrimSpace(cfg.LinuxDo.UserInfoEmailPath)
  1683. cfg.LinuxDo.UserInfoIDPath = strings.TrimSpace(cfg.LinuxDo.UserInfoIDPath)
  1684. cfg.LinuxDo.UserInfoUsernamePath = strings.TrimSpace(cfg.LinuxDo.UserInfoUsernamePath)
  1685. applyLegacyWeChatConnectEnvCompatibility(&cfg.WeChat)
  1686. normalizeWeChatConnectConfig(&cfg.WeChat)
  1687. cfg.OIDC.ProviderName = strings.TrimSpace(cfg.OIDC.ProviderName)
  1688. cfg.OIDC.ClientID = strings.TrimSpace(cfg.OIDC.ClientID)
  1689. cfg.OIDC.ClientSecret = strings.TrimSpace(cfg.OIDC.ClientSecret)
  1690. cfg.OIDC.IssuerURL = strings.TrimSpace(cfg.OIDC.IssuerURL)
  1691. cfg.OIDC.DiscoveryURL = strings.TrimSpace(cfg.OIDC.DiscoveryURL)
  1692. cfg.OIDC.AuthorizeURL = strings.TrimSpace(cfg.OIDC.AuthorizeURL)
  1693. cfg.OIDC.TokenURL = strings.TrimSpace(cfg.OIDC.TokenURL)
  1694. cfg.OIDC.UserInfoURL = strings.TrimSpace(cfg.OIDC.UserInfoURL)
  1695. cfg.OIDC.JWKSURL = strings.TrimSpace(cfg.OIDC.JWKSURL)
  1696. cfg.OIDC.Scopes = strings.TrimSpace(cfg.OIDC.Scopes)
  1697. cfg.OIDC.RedirectURL = strings.TrimSpace(cfg.OIDC.RedirectURL)
  1698. cfg.OIDC.FrontendRedirectURL = strings.TrimSpace(cfg.OIDC.FrontendRedirectURL)
  1699. cfg.OIDC.TokenAuthMethod = strings.ToLower(strings.TrimSpace(cfg.OIDC.TokenAuthMethod))
  1700. cfg.OIDC.AllowedSigningAlgs = strings.TrimSpace(cfg.OIDC.AllowedSigningAlgs)
  1701. cfg.OIDC.UserInfoEmailPath = strings.TrimSpace(cfg.OIDC.UserInfoEmailPath)
  1702. cfg.OIDC.UserInfoIDPath = strings.TrimSpace(cfg.OIDC.UserInfoIDPath)
  1703. cfg.OIDC.UserInfoUsernamePath = strings.TrimSpace(cfg.OIDC.UserInfoUsernamePath)
  1704. cfg.OIDC.UsePKCEExplicit = hasExplicitConfigOrEnv("oidc_connect.use_pkce", "OIDC_CONNECT_USE_PKCE")
  1705. cfg.OIDC.ValidateIDTokenExplicit = hasExplicitConfigOrEnv("oidc_connect.validate_id_token", "OIDC_CONNECT_VALIDATE_ID_TOKEN")
  1706. cfg.Dashboard.KeyPrefix = strings.TrimSpace(cfg.Dashboard.KeyPrefix)
  1707. cfg.CORS.AllowedOrigins = normalizeStringSlice(cfg.CORS.AllowedOrigins)
  1708. cfg.Security.ResponseHeaders.AdditionalAllowed = normalizeStringSlice(cfg.Security.ResponseHeaders.AdditionalAllowed)
  1709. cfg.Security.ResponseHeaders.ForceRemove = normalizeStringSlice(cfg.Security.ResponseHeaders.ForceRemove)
  1710. cfg.Security.CSP.Policy = strings.TrimSpace(cfg.Security.CSP.Policy)
  1711. forwardedClientIPHeaders, err := NormalizeForwardedClientIPHeaders(cfg.Security.ForwardedClientIPHeaders)
  1712. if err != nil {
  1713. return nil, fmt.Errorf("security.forwarded_client_ip_headers: %w", err)
  1714. }
  1715. cfg.Security.ForwardedClientIPHeaders = forwardedClientIPHeaders
  1716. cfg.SetForwardedClientIPSettings(cfg.Security.TrustForwardedIPForAPIKeyACL, forwardedClientIPHeaders)
  1717. cfg.Log.Level = strings.ToLower(strings.TrimSpace(cfg.Log.Level))
  1718. cfg.Log.Format = strings.ToLower(strings.TrimSpace(cfg.Log.Format))
  1719. cfg.Log.ServiceName = strings.TrimSpace(cfg.Log.ServiceName)
  1720. cfg.Log.Environment = strings.TrimSpace(cfg.Log.Environment)
  1721. cfg.Log.StacktraceLevel = strings.ToLower(strings.TrimSpace(cfg.Log.StacktraceLevel))
  1722. cfg.Log.Output.FilePath = strings.TrimSpace(cfg.Log.Output.FilePath)
  1723. cfg.Gateway.ForcedCodexInstructionsTemplateFile = strings.TrimSpace(cfg.Gateway.ForcedCodexInstructionsTemplateFile)
  1724. if cfg.Gateway.ForcedCodexInstructionsTemplateFile != "" {
  1725. content, err := os.ReadFile(cfg.Gateway.ForcedCodexInstructionsTemplateFile)
  1726. if err != nil {
  1727. return nil, fmt.Errorf("read forced codex instructions template %q: %w", cfg.Gateway.ForcedCodexInstructionsTemplateFile, err)
  1728. }
  1729. cfg.Gateway.ForcedCodexInstructionsTemplate = string(content)
  1730. }
  1731. // 兼容旧键 gateway.disable_codex_originator_normalization:语义已被
  1732. // disable_codex_identity_enforcement 取代(身份改写升级为强制统一出口),
  1733. // 任一为 true 即关闭强制统一。
  1734. if cfg.Gateway.DisableCodexOriginatorNormalization {
  1735. cfg.Gateway.DisableCodexIdentityEnforcement = true
  1736. }
  1737. // 兼容旧键 gateway.openai_ws.sticky_previous_response_ttl_seconds。
  1738. // 新键未配置(<=0)时回退旧键;新键优先。
  1739. if cfg.Gateway.OpenAIWS.StickyResponseIDTTLSeconds <= 0 && cfg.Gateway.OpenAIWS.StickyPreviousResponseTTLSeconds > 0 {
  1740. cfg.Gateway.OpenAIWS.StickyResponseIDTTLSeconds = cfg.Gateway.OpenAIWS.StickyPreviousResponseTTLSeconds
  1741. }
  1742. // Normalize UMQ mode: 白名单校验,非法值在加载时一次性 warn 并清空
  1743. if m := cfg.Gateway.UserMessageQueue.Mode; m != "" && m != UMQModeSerialize && m != UMQModeThrottle {
  1744. slog.Warn("invalid user_message_queue mode, disabling",
  1745. "mode", m,
  1746. "valid_modes", []string{UMQModeSerialize, UMQModeThrottle})
  1747. cfg.Gateway.UserMessageQueue.Mode = ""
  1748. }
  1749. // Auto-generate TOTP encryption key if not set (32 bytes = 64 hex chars for AES-256)
  1750. cfg.Totp.EncryptionKey = strings.TrimSpace(cfg.Totp.EncryptionKey)
  1751. if cfg.Totp.EncryptionKey == "" {
  1752. key, err := generateJWTSecret(32) // Reuse the same random generation function
  1753. if err != nil {
  1754. return nil, fmt.Errorf("generate totp encryption key error: %w", err)
  1755. }
  1756. cfg.Totp.EncryptionKey = key
  1757. cfg.Totp.EncryptionKeyConfigured = false
  1758. slog.Warn("TOTP encryption key auto-generated. Consider setting a fixed key for production.")
  1759. } else {
  1760. cfg.Totp.EncryptionKeyConfigured = true
  1761. }
  1762. originalJWTSecret := cfg.JWT.Secret
  1763. if allowMissingJWTSecret && originalJWTSecret == "" {
  1764. // 启动阶段允许先无 JWT 密钥,后续在数据库初始化后补齐。
  1765. cfg.JWT.Secret = strings.Repeat("0", 32)
  1766. }
  1767. if err := cfg.Validate(); err != nil {
  1768. return nil, fmt.Errorf("validate config error: %w", err)
  1769. }
  1770. if allowMissingJWTSecret && originalJWTSecret == "" {
  1771. cfg.JWT.Secret = ""
  1772. }
  1773. if !cfg.Security.URLAllowlist.Enabled {
  1774. slog.Warn("security.url_allowlist.enabled=false; allowlist/SSRF checks disabled (minimal format validation only).")
  1775. }
  1776. if !cfg.Security.ResponseHeaders.Enabled {
  1777. slog.Warn("security.response_headers.enabled=false; configurable header filtering disabled (default allowlist only).")
  1778. }
  1779. if cfg.JWT.Secret != "" && isWeakJWTSecret(cfg.JWT.Secret) {
  1780. slog.Warn("JWT secret appears weak; use a 32+ character random secret in production.")
  1781. }
  1782. if len(cfg.Security.ResponseHeaders.AdditionalAllowed) > 0 || len(cfg.Security.ResponseHeaders.ForceRemove) > 0 {
  1783. slog.Info("response header policy configured",
  1784. "additional_allowed", cfg.Security.ResponseHeaders.AdditionalAllowed,
  1785. "force_remove", cfg.Security.ResponseHeaders.ForceRemove,
  1786. )
  1787. }
  1788. return &cfg, nil
  1789. }
  1790. func configureConfigSource(setConfigFile, addConfigPath func(string)) {
  1791. if configFile := strings.TrimSpace(os.Getenv("CONFIG_FILE")); configFile != "" {
  1792. setConfigFile(configFile)
  1793. return
  1794. }
  1795. // Add config paths in priority order.
  1796. if dataDir := strings.TrimSpace(os.Getenv("DATA_DIR")); dataDir != "" {
  1797. addConfigPath(dataDir)
  1798. }
  1799. addConfigPath("/app/data")
  1800. addConfigPath(".")
  1801. addConfigPath("./config")
  1802. addConfigPath("/etc/sub2api")
  1803. }
  1804. func setDefaults() {
  1805. viper.SetDefault("run_mode", RunModeStandard)
  1806. // Server
  1807. viper.SetDefault("server.host", "0.0.0.0")
  1808. viper.SetDefault("server.port", 8080)
  1809. viper.SetDefault("server.mode", "release")
  1810. viper.SetDefault("server.enable_server_timing", false)
  1811. viper.SetDefault("server.frontend_url", "")
  1812. viper.SetDefault("server.read_header_timeout", 10) // 10秒读取请求头
  1813. viper.SetDefault("server.max_header_bytes", 64*1024)
  1814. viper.SetDefault("server.idle_timeout", 120) // 120秒空闲超时
  1815. viper.SetDefault("server.max_request_body_size", int64(256*1024*1024))
  1816. // H2C 默认配置
  1817. viper.SetDefault("server.h2c.enabled", false)
  1818. viper.SetDefault("server.h2c.max_concurrent_streams", uint32(50)) // 50 个并发流
  1819. viper.SetDefault("server.h2c.idle_timeout", 75) // 75 秒
  1820. viper.SetDefault("server.h2c.max_read_frame_size", 1<<20) // 1MB(够用)
  1821. viper.SetDefault("server.h2c.max_upload_buffer_per_connection", 2<<20) // 2MB
  1822. viper.SetDefault("server.h2c.max_upload_buffer_per_stream", 512<<10) // 512KB
  1823. // Log
  1824. viper.SetDefault("log.level", "info")
  1825. viper.SetDefault("log.format", "console")
  1826. viper.SetDefault("log.service_name", "sub2api")
  1827. viper.SetDefault("log.env", "production")
  1828. viper.SetDefault("log.caller", true)
  1829. viper.SetDefault("log.stacktrace_level", "error")
  1830. viper.SetDefault("log.output.to_stdout", true)
  1831. viper.SetDefault("log.output.to_file", true)
  1832. viper.SetDefault("log.output.file_path", "")
  1833. viper.SetDefault("log.rotation.max_size_mb", 100)
  1834. viper.SetDefault("log.rotation.max_backups", 10)
  1835. viper.SetDefault("log.rotation.max_age_days", 7)
  1836. viper.SetDefault("log.rotation.compress", true)
  1837. viper.SetDefault("log.rotation.local_time", true)
  1838. viper.SetDefault("log.sampling.enabled", false)
  1839. viper.SetDefault("log.sampling.initial", 100)
  1840. viper.SetDefault("log.sampling.thereafter", 100)
  1841. // CORS
  1842. viper.SetDefault("cors.allowed_origins", []string{})
  1843. viper.SetDefault("cors.allow_credentials", true)
  1844. // WebAuthn / Passkeys are opt-in because every deployment must explicitly
  1845. // declare its relying-party domain and trusted browser origins.
  1846. viper.SetDefault("webauthn.enabled", false)
  1847. viper.SetDefault("webauthn.rp_display_name", "Sub2API")
  1848. viper.SetDefault("webauthn.rp_id", "")
  1849. viper.SetDefault("webauthn.rp_origins", []string{})
  1850. // Security
  1851. viper.SetDefault("security.url_allowlist.enabled", false)
  1852. viper.SetDefault("security.url_allowlist.upstream_hosts", []string{
  1853. "api.openai.com",
  1854. "api.anthropic.com",
  1855. "api.kimi.com",
  1856. "api.moonshot.ai",
  1857. "api.moonshot.cn",
  1858. "open.bigmodel.cn",
  1859. "api.minimaxi.com", // MiniMax CN quota + inference
  1860. "api.minimax.io", // MiniMax intl; frozen allowlists must add this host to use the intl site
  1861. "generativelanguage.googleapis.com",
  1862. "cloudcode-pa.googleapis.com",
  1863. "*.openai.azure.com",
  1864. })
  1865. viper.SetDefault("security.url_allowlist.pricing_hosts", []string{
  1866. "raw.githubusercontent.com",
  1867. })
  1868. viper.SetDefault("security.url_allowlist.crs_hosts", []string{})
  1869. viper.SetDefault("security.url_allowlist.allow_private_hosts", true)
  1870. viper.SetDefault("security.url_allowlist.allow_insecure_http", true)
  1871. viper.SetDefault("security.response_headers.enabled", true)
  1872. viper.SetDefault("security.response_headers.additional_allowed", []string{})
  1873. viper.SetDefault("security.response_headers.force_remove", []string{})
  1874. viper.SetDefault("security.csp.enabled", true)
  1875. viper.SetDefault("security.csp.policy", DefaultCSPPolicy)
  1876. viper.SetDefault("security.proxy_probe.insecure_skip_verify", false)
  1877. viper.SetDefault("security.trust_forwarded_ip_for_api_key_acl", true)
  1878. // Security - disable direct fallback on proxy error
  1879. viper.SetDefault("security.proxy_fallback.allow_direct_on_error", false)
  1880. // Billing
  1881. viper.SetDefault("billing.circuit_breaker.enabled", true)
  1882. viper.SetDefault("billing.circuit_breaker.failure_threshold", 5)
  1883. viper.SetDefault("billing.circuit_breaker.reset_timeout_seconds", 30)
  1884. viper.SetDefault("billing.circuit_breaker.half_open_requests", 3)
  1885. viper.SetDefault("billing.minimum_balance_reserve", 0.000001)
  1886. viper.SetDefault("billing.user_platform_quota_cache_ttl_seconds", 86400)
  1887. viper.SetDefault("billing.user_platform_quota_sentinel_ttl_seconds", 3600)
  1888. // Turnstile
  1889. viper.SetDefault("turnstile.required", false)
  1890. // LinuxDo Connect OAuth 登录
  1891. viper.SetDefault("linuxdo_connect.enabled", false)
  1892. viper.SetDefault("linuxdo_connect.client_id", "")
  1893. viper.SetDefault("linuxdo_connect.client_secret", "")
  1894. viper.SetDefault("linuxdo_connect.authorize_url", "https://connect.linux.do/oauth2/authorize")
  1895. viper.SetDefault("linuxdo_connect.token_url", "https://connect.linux.do/oauth2/token")
  1896. viper.SetDefault("linuxdo_connect.userinfo_url", "https://connect.linux.do/api/user")
  1897. viper.SetDefault("linuxdo_connect.scopes", "user")
  1898. viper.SetDefault("linuxdo_connect.redirect_url", "")
  1899. viper.SetDefault("linuxdo_connect.frontend_redirect_url", "/auth/linuxdo/callback")
  1900. viper.SetDefault("linuxdo_connect.token_auth_method", "client_secret_post")
  1901. viper.SetDefault("linuxdo_connect.use_pkce", false)
  1902. viper.SetDefault("linuxdo_connect.userinfo_email_path", "")
  1903. viper.SetDefault("linuxdo_connect.userinfo_id_path", "")
  1904. viper.SetDefault("linuxdo_connect.userinfo_username_path", "")
  1905. // WeChat Connect OAuth 登录
  1906. viper.SetDefault("wechat_connect.enabled", false)
  1907. viper.SetDefault("wechat_connect.app_id", "")
  1908. viper.SetDefault("wechat_connect.app_secret", "")
  1909. viper.SetDefault("wechat_connect.open_app_id", "")
  1910. viper.SetDefault("wechat_connect.open_app_secret", "")
  1911. viper.SetDefault("wechat_connect.mp_app_id", "")
  1912. viper.SetDefault("wechat_connect.mp_app_secret", "")
  1913. viper.SetDefault("wechat_connect.mobile_app_id", "")
  1914. viper.SetDefault("wechat_connect.mobile_app_secret", "")
  1915. viper.SetDefault("wechat_connect.open_enabled", false)
  1916. viper.SetDefault("wechat_connect.mp_enabled", false)
  1917. viper.SetDefault("wechat_connect.mobile_enabled", false)
  1918. viper.SetDefault("wechat_connect.mode", defaultWeChatConnectMode)
  1919. viper.SetDefault("wechat_connect.scopes", defaultWeChatConnectScopes)
  1920. viper.SetDefault("wechat_connect.redirect_url", "")
  1921. viper.SetDefault("wechat_connect.frontend_redirect_url", defaultWeChatConnectFrontendRedirect)
  1922. // Generic OIDC OAuth 登录
  1923. viper.SetDefault("oidc_connect.enabled", false)
  1924. viper.SetDefault("oidc_connect.provider_name", "OIDC")
  1925. viper.SetDefault("oidc_connect.client_id", "")
  1926. viper.SetDefault("oidc_connect.client_secret", "")
  1927. viper.SetDefault("oidc_connect.issuer_url", "")
  1928. viper.SetDefault("oidc_connect.discovery_url", "")
  1929. viper.SetDefault("oidc_connect.authorize_url", "")
  1930. viper.SetDefault("oidc_connect.token_url", "")
  1931. viper.SetDefault("oidc_connect.userinfo_url", "")
  1932. viper.SetDefault("oidc_connect.jwks_url", "")
  1933. viper.SetDefault("oidc_connect.scopes", "openid email profile")
  1934. viper.SetDefault("oidc_connect.redirect_url", "")
  1935. viper.SetDefault("oidc_connect.frontend_redirect_url", "/auth/oidc/callback")
  1936. viper.SetDefault("oidc_connect.token_auth_method", "client_secret_post")
  1937. viper.SetDefault("oidc_connect.use_pkce", true)
  1938. viper.SetDefault("oidc_connect.validate_id_token", true)
  1939. viper.SetDefault("oidc_connect.allowed_signing_algs", "RS256,ES256,PS256")
  1940. viper.SetDefault("oidc_connect.clock_skew_seconds", 120)
  1941. viper.SetDefault("oidc_connect.require_email_verified", false)
  1942. viper.SetDefault("oidc_connect.userinfo_email_path", "")
  1943. viper.SetDefault("oidc_connect.userinfo_id_path", "")
  1944. viper.SetDefault("oidc_connect.userinfo_username_path", "")
  1945. // DingTalk Connect OAuth 登录
  1946. viper.SetDefault("dingtalk_connect.enabled", false)
  1947. viper.SetDefault("dingtalk_connect.authorize_url", "https://login.dingtalk.com/oauth2/auth")
  1948. viper.SetDefault("dingtalk_connect.token_url", "https://api.dingtalk.com/v1.0/oauth2/userAccessToken")
  1949. viper.SetDefault("dingtalk_connect.userinfo_url", "https://api.dingtalk.com/v1.0/contact/users/me")
  1950. viper.SetDefault("dingtalk_connect.scopes", "openid")
  1951. viper.SetDefault("dingtalk_connect.frontend_redirect_url", "/auth/dingtalk/callback")
  1952. viper.SetDefault("dingtalk_connect.dingtalk_app_kind", "internal_app")
  1953. viper.SetDefault("dingtalk_connect.app_type", "public")
  1954. viper.SetDefault("dingtalk_connect.corp_restriction_policy", "none")
  1955. viper.SetDefault("dingtalk_connect.require_email", true)
  1956. viper.SetDefault("dingtalk_connect.username_overwrite_policy", "if_empty")
  1957. // Database
  1958. viper.SetDefault("database.host", "localhost")
  1959. viper.SetDefault("database.port", 5432)
  1960. viper.SetDefault("database.user", "postgres")
  1961. viper.SetDefault("database.password", "postgres")
  1962. viper.SetDefault("database.dbname", "sub2api")
  1963. viper.SetDefault("database.sslmode", "prefer")
  1964. viper.SetDefault("database.max_open_conns", 256)
  1965. viper.SetDefault("database.max_idle_conns", 128)
  1966. viper.SetDefault("database.conn_max_lifetime_minutes", 30)
  1967. viper.SetDefault("database.conn_max_idle_time_minutes", 5)
  1968. viper.SetDefault("database.user_platform_quota_flusher_enabled", false)
  1969. viper.SetDefault("database.user_platform_quota_flush_interval_ms", 2000)
  1970. viper.SetDefault("database.user_platform_quota_flush_batch_size", 1000)
  1971. // Redis
  1972. viper.SetDefault("redis.host", "localhost")
  1973. viper.SetDefault("redis.port", 6379)
  1974. viper.SetDefault("redis.username", "")
  1975. viper.SetDefault("redis.password", "")
  1976. viper.SetDefault("redis.db", 0)
  1977. viper.SetDefault("redis.dial_timeout_seconds", 5)
  1978. viper.SetDefault("redis.read_timeout_seconds", 3)
  1979. viper.SetDefault("redis.write_timeout_seconds", 3)
  1980. viper.SetDefault("redis.pool_size", 1024)
  1981. viper.SetDefault("redis.min_idle_conns", 128)
  1982. viper.SetDefault("redis.enable_tls", false)
  1983. // Batch Image queue
  1984. viper.SetDefault("batch_image.enabled", false)
  1985. viper.SetDefault("batch_image.max_items_per_job_default", 200)
  1986. viper.SetDefault("batch_image.max_items_per_job_trial", 50)
  1987. viper.SetDefault("batch_image.max_output_images_per_job", 200)
  1988. viper.SetDefault("batch_image.max_output_images_per_item", 4)
  1989. viper.SetDefault("batch_image.max_prompt_chars_per_item", 8000)
  1990. viper.SetDefault("batch_image.max_reference_images_per_job", 1000)
  1991. viper.SetDefault("batch_image.max_reference_inline_bytes_per_job", 134217728)
  1992. viper.SetDefault("batch_image.default_response_mime_type", "image/png")
  1993. viper.SetDefault("batch_image.default_image_size", "1K")
  1994. viper.SetDefault("batch_image.max_download_items_zip", 200)
  1995. viper.SetDefault("batch_image.max_download_bytes_per_request", 536870912)
  1996. viper.SetDefault("batch_image.max_download_duration_seconds", 600)
  1997. viper.SetDefault("batch_image.max_download_concurrency_per_user", 1)
  1998. viper.SetDefault("batch_image.input_retention_after_terminal_hours", 24)
  1999. viper.SetDefault("batch_image.output_retention_after_terminal_hours", 72)
  2000. viper.SetDefault("batch_image.output_retention_max_days", 7)
  2001. viper.SetDefault("batch_image.cleanup_interval_minutes", 30)
  2002. viper.SetDefault("batch_image.cleanup_batch_size", 100)
  2003. viper.SetDefault("batch_image.queue_enabled", false)
  2004. viper.SetDefault("batch_image.queue_ready_key", "batch_image:queue:ready")
  2005. viper.SetDefault("batch_image.queue_delayed_key", "batch_image:queue:delayed")
  2006. viper.SetDefault("batch_image.queue_active_key", "batch_image:queue:active")
  2007. viper.SetDefault("batch_image.inflight_key_prefix", "batch_image:queue:inflight:")
  2008. viper.SetDefault("batch_image.lock_key_prefix", "batch_image:queue:lock:")
  2009. viper.SetDefault("batch_image.idempotency_key_prefix", "batch_image:queue:idem:")
  2010. viper.SetDefault("batch_image.inflight_ttl_seconds", 604800)
  2011. viper.SetDefault("batch_image.job_lock_ttl_seconds", 300)
  2012. viper.SetDefault("batch_image.default_requeue_delay_seconds", 30)
  2013. viper.SetDefault("batch_image.error_retry_delay_seconds", 60)
  2014. viper.SetDefault("batch_image.lock_conflict_delay_seconds", 5)
  2015. viper.SetDefault("batch_image.stale_active_after_seconds", 600)
  2016. viper.SetDefault("batch_image.delayed_mover_interval_seconds", 5)
  2017. viper.SetDefault("batch_image.recovery_interval_seconds", 300)
  2018. viper.SetDefault("batch_image.delayed_move_limit", 100)
  2019. viper.SetDefault("batch_image.recover_limit", 100)
  2020. viper.SetDefault("batch_image.vertex_enabled", false)
  2021. viper.SetDefault("batch_image.vertex_project_id", "")
  2022. viper.SetDefault("batch_image.vertex_location", "global")
  2023. viper.SetDefault("batch_image.vertex_managed_gcs_bucket", "")
  2024. viper.SetDefault("batch_image.vertex_managed_gcs_prefix", "batch-image/{env}/{batch_id}")
  2025. viper.SetDefault("batch_image.vertex_input_retention_hours", 24)
  2026. viper.SetDefault("batch_image.vertex_output_retention_hours", 72)
  2027. viper.SetDefault("batch_image.vertex_batch_prediction_base_url", "")
  2028. viper.SetDefault("batch_image.vertex_gcs_base_url", "")
  2029. // Image storage (async image task result offload to S3-compatible object storage)
  2030. viper.SetDefault("image_storage.enabled", false)
  2031. viper.SetDefault("image_storage.region", "auto")
  2032. viper.SetDefault("image_storage.prefix", "images/")
  2033. viper.SetDefault("image_storage.force_path_style", false)
  2034. viper.SetDefault("image_storage.presign_expiry_hours", 24)
  2035. viper.SetDefault("image_storage.max_download_bytes", 33554432)
  2036. // Registered with empty defaults so AutomaticEnv can reach them: viper only
  2037. // decodes keys present in AllKeys(), so a credential that is supplied purely
  2038. // via IMAGE_STORAGE_* and never appears in config.yaml would be dropped and
  2039. // silently disable the whole async image feature.
  2040. viper.SetDefault("image_storage.endpoint", "")
  2041. viper.SetDefault("image_storage.bucket", "")
  2042. viper.SetDefault("image_storage.access_key_id", "")
  2043. viper.SetDefault("image_storage.secret_access_key", "")
  2044. viper.SetDefault("image_storage.public_base_url", "")
  2045. // Ops (vNext)
  2046. viper.SetDefault("ops.enabled", true)
  2047. viper.SetDefault("ops.use_preaggregated_tables", true)
  2048. viper.SetDefault("ops.cleanup.enabled", true)
  2049. viper.SetDefault("ops.cleanup.schedule", "0 2 * * *")
  2050. // Retention days: vNext defaults to 30 days across ops datasets.
  2051. viper.SetDefault("ops.cleanup.error_log_retention_days", 30)
  2052. viper.SetDefault("ops.cleanup.system_log_retention_days", 30)
  2053. viper.SetDefault("ops.cleanup.minute_metrics_retention_days", 30)
  2054. viper.SetDefault("ops.cleanup.hourly_metrics_retention_days", 30)
  2055. viper.SetDefault("ops.aggregation.enabled", true)
  2056. viper.SetDefault("ops.metrics_collector_cache.enabled", true)
  2057. // TTL should be slightly larger than collection interval (1m) to maximize cross-replica cache hits.
  2058. viper.SetDefault("ops.metrics_collector_cache.ttl", 65*time.Second)
  2059. // JWT
  2060. viper.SetDefault("jwt.secret", "")
  2061. viper.SetDefault("jwt.expire_hour", 24)
  2062. viper.SetDefault("jwt.access_token_expire_minutes", 0) // 0 表示回退到 expire_hour
  2063. viper.SetDefault("jwt.refresh_token_expire_days", 30) // 30天Refresh Token有效期
  2064. viper.SetDefault("jwt.refresh_window_minutes", 2) // 过期前2分钟开始允许刷新
  2065. // TOTP
  2066. viper.SetDefault("totp.encryption_key", "")
  2067. // Default
  2068. // Admin credentials are created via the setup flow (web wizard / CLI / AUTO_SETUP).
  2069. // Do not ship fixed defaults here to avoid insecure "known credentials" in production.
  2070. viper.SetDefault("default.admin_email", "")
  2071. viper.SetDefault("default.admin_password", "")
  2072. viper.SetDefault("default.user_concurrency", 5)
  2073. viper.SetDefault("default.user_balance", 0)
  2074. viper.SetDefault("default.api_key_prefix", "sk-")
  2075. viper.SetDefault("default.rate_multiplier", 1.0)
  2076. // RateLimit
  2077. viper.SetDefault("rate_limit.overload_cooldown_minutes", 10)
  2078. viper.SetDefault("rate_limit.oauth_401_cooldown_minutes", 10)
  2079. // Pricing - 从 model-price-repo main 分支同步模型定价和上下文窗口数据
  2080. viper.SetDefault("pricing.remote_url", "https://raw.githubusercontent.com/Wei-Shaw/model-price-repo/main/model_prices_and_context_window.json")
  2081. viper.SetDefault("pricing.hash_url", "https://raw.githubusercontent.com/Wei-Shaw/model-price-repo/main/model_prices_and_context_window.sha256")
  2082. viper.SetDefault("pricing.data_dir", "./data")
  2083. viper.SetDefault("pricing.fallback_file", "./resources/model-pricing/model_prices_and_context_window.json")
  2084. viper.SetDefault("pricing.override_file", "")
  2085. viper.SetDefault("pricing.update_interval_hours", 24)
  2086. viper.SetDefault("pricing.hash_check_interval_minutes", 10)
  2087. // 本地进程插件。插件必须由管理员手动上传,项目默认不携带任何插件能力。
  2088. viper.SetDefault("plugins.data_dir", "")
  2089. viper.SetDefault("plugins.allow_unsigned", false)
  2090. viper.SetDefault("plugins.trusted_publishers", map[string]string{})
  2091. viper.SetDefault("plugins.max_upload_bytes", int64(128*1024*1024))
  2092. viper.SetDefault("plugins.max_uncompressed_bytes", int64(256*1024*1024))
  2093. viper.SetDefault("plugins.start_timeout_seconds", 15)
  2094. // Timezone (default to Asia/Shanghai for Chinese users)
  2095. viper.SetDefault("timezone", "Asia/Shanghai")
  2096. // API Key auth cache
  2097. viper.SetDefault("api_key_auth_cache.l1_size", 65535)
  2098. viper.SetDefault("api_key_auth_cache.l1_ttl_seconds", 15)
  2099. viper.SetDefault("api_key_auth_cache.l2_ttl_seconds", 300)
  2100. viper.SetDefault("api_key_auth_cache.negative_ttl_seconds", 30)
  2101. viper.SetDefault("api_key_auth_cache.jitter_percent", 10)
  2102. viper.SetDefault("api_key_auth_cache.singleflight", true)
  2103. viper.SetDefault("api_key_auth_cache.lookup_concurrency", 64)
  2104. viper.SetDefault("api_key_auth_cache.invalid_abuse.enabled", true)
  2105. viper.SetDefault("api_key_auth_cache.invalid_abuse.threshold", 120)
  2106. viper.SetDefault("api_key_auth_cache.invalid_abuse.window_seconds", 60)
  2107. viper.SetDefault("api_key_auth_cache.invalid_abuse.block_seconds", 60)
  2108. viper.SetDefault("api_key_auth_cache.invalid_abuse.capacity", 16384)
  2109. // Subscription auth L1 cache
  2110. viper.SetDefault("subscription_cache.l1_size", 16384)
  2111. viper.SetDefault("subscription_cache.l1_ttl_seconds", 10)
  2112. viper.SetDefault("subscription_cache.jitter_percent", 10)
  2113. // Dashboard cache
  2114. viper.SetDefault("dashboard_cache.enabled", true)
  2115. viper.SetDefault("dashboard_cache.key_prefix", "sub2api:")
  2116. viper.SetDefault("dashboard_cache.stats_fresh_ttl_seconds", 15)
  2117. viper.SetDefault("dashboard_cache.stats_ttl_seconds", 30)
  2118. viper.SetDefault("dashboard_cache.stats_refresh_timeout_seconds", 30)
  2119. // Dashboard aggregation
  2120. viper.SetDefault("dashboard_aggregation.enabled", true)
  2121. viper.SetDefault("dashboard_aggregation.interval_seconds", 60)
  2122. viper.SetDefault("dashboard_aggregation.lookback_seconds", 120)
  2123. viper.SetDefault("dashboard_aggregation.backfill_enabled", false)
  2124. viper.SetDefault("dashboard_aggregation.backfill_max_days", 31)
  2125. viper.SetDefault("dashboard_aggregation.retention.usage_logs_days", 90)
  2126. viper.SetDefault("dashboard_aggregation.retention.usage_billing_dedup_days", 365)
  2127. viper.SetDefault("dashboard_aggregation.retention.hourly_days", 180)
  2128. viper.SetDefault("dashboard_aggregation.retention.daily_days", 730)
  2129. viper.SetDefault("dashboard_aggregation.recompute_days", 2)
  2130. // Usage cleanup task
  2131. viper.SetDefault("usage_cleanup.enabled", true)
  2132. viper.SetDefault("usage_cleanup.max_range_days", 31)
  2133. viper.SetDefault("usage_cleanup.batch_size", 5000)
  2134. viper.SetDefault("usage_cleanup.worker_interval_seconds", 10)
  2135. viper.SetDefault("usage_cleanup.task_timeout_seconds", 1800)
  2136. // Idempotency
  2137. viper.SetDefault("idempotency.observe_only", true)
  2138. viper.SetDefault("idempotency.default_ttl_seconds", 86400)
  2139. viper.SetDefault("idempotency.system_operation_ttl_seconds", 3600)
  2140. viper.SetDefault("idempotency.processing_timeout_seconds", 30)
  2141. viper.SetDefault("idempotency.failed_retry_backoff_seconds", 5)
  2142. viper.SetDefault("idempotency.max_stored_response_len", 64*1024)
  2143. viper.SetDefault("idempotency.cleanup_interval_seconds", 60)
  2144. viper.SetDefault("idempotency.cleanup_batch_size", 500)
  2145. // Gateway
  2146. viper.SetDefault("gateway.response_header_timeout", 600) // 600秒(10分钟)等待上游响应头,LLM高负载时可能排队较久
  2147. viper.SetDefault("gateway.openai_response_header_timeout", 0)
  2148. viper.SetDefault("gateway.grok_response_header_timeout", 120)
  2149. viper.SetDefault("gateway.openai_first_output_timeout_seconds", 0)
  2150. viper.SetDefault("gateway.openai_high_effort_first_output_timeout_seconds", 0)
  2151. viper.SetDefault("gateway.log_upstream_error_body", true)
  2152. viper.SetDefault("gateway.log_upstream_error_body_max_bytes", 2048)
  2153. viper.SetDefault("gateway.inject_beta_for_apikey", false)
  2154. viper.SetDefault("gateway.failover_on_400", false)
  2155. viper.SetDefault("gateway.max_account_switches", 10)
  2156. viper.SetDefault("gateway.max_account_switches_gemini", 3)
  2157. viper.SetDefault("gateway.force_codex_cli", false)
  2158. viper.SetDefault("gateway.disable_codex_identity_enforcement", false)
  2159. viper.SetDefault("gateway.disable_codex_originator_normalization", false)
  2160. viper.SetDefault("gateway.codex_image_generation_bridge_enabled", false)
  2161. viper.SetDefault("gateway.openai_passthrough_allow_timeout_headers", false)
  2162. viper.SetDefault("gateway.openai_compact_model", "gpt-5.4")
  2163. viper.SetDefault("gateway.live.max_session_duration_seconds", 3600)
  2164. // OpenAI Responses WebSocket(默认开启;可通过 force_http 紧急回滚)
  2165. viper.SetDefault("gateway.openai_ws.enabled", true)
  2166. viper.SetDefault("gateway.openai_ws.mode_router_v2_enabled", false)
  2167. viper.SetDefault("gateway.openai_ws.ingress_mode_default", "ctx_pool")
  2168. viper.SetDefault("gateway.openai_ws.client_first_message_timeout_seconds", DefaultOpenAIWSClientFirstMessageTimeoutSeconds)
  2169. viper.SetDefault("gateway.openai_ws.ingress_inter_turn_idle_timeout_seconds", 300)
  2170. viper.SetDefault("gateway.openai_ws.max_ingress_connections_per_api_key", 64)
  2171. viper.SetDefault("gateway.openai_ws.oauth_enabled", true)
  2172. viper.SetDefault("gateway.openai_ws.apikey_enabled", true)
  2173. viper.SetDefault("gateway.openai_ws.force_http", false)
  2174. viper.SetDefault("gateway.openai_ws.allow_store_recovery", false)
  2175. viper.SetDefault("gateway.openai_ws.ingress_previous_response_recovery_enabled", true)
  2176. viper.SetDefault("gateway.openai_ws.store_disabled_conn_mode", "strict")
  2177. viper.SetDefault("gateway.openai_ws.store_disabled_force_new_conn", true)
  2178. viper.SetDefault("gateway.openai_ws.prewarm_generate_enabled", false)
  2179. viper.SetDefault("gateway.openai_ws.client_read_limit_bytes", 64*1024*1024)
  2180. viper.SetDefault("gateway.openai_ws.http_bridge_enabled", true)
  2181. viper.SetDefault("gateway.openai_ws.http_bridge_threshold_bytes", 15*1024*1024)
  2182. viper.SetDefault("gateway.openai_ws.responses_websockets", false)
  2183. viper.SetDefault("gateway.openai_ws.responses_websockets_v2", true)
  2184. viper.SetDefault("gateway.openai_ws.max_conns_per_account", 128)
  2185. viper.SetDefault("gateway.openai_ws.min_idle_per_account", 4)
  2186. viper.SetDefault("gateway.openai_ws.max_idle_per_account", 12)
  2187. viper.SetDefault("gateway.openai_ws.dynamic_max_conns_by_account_concurrency_enabled", true)
  2188. viper.SetDefault("gateway.openai_ws.oauth_max_conns_factor", 1.0)
  2189. viper.SetDefault("gateway.openai_ws.apikey_max_conns_factor", 1.0)
  2190. viper.SetDefault("gateway.openai_ws.dial_timeout_seconds", 10)
  2191. viper.SetDefault("gateway.openai_ws.read_timeout_seconds", 900)
  2192. viper.SetDefault("gateway.openai_ws.write_timeout_seconds", 120)
  2193. viper.SetDefault("gateway.openai_ws.pool_target_utilization", 0.7)
  2194. viper.SetDefault("gateway.openai_ws.queue_limit_per_conn", 64)
  2195. viper.SetDefault("gateway.openai_ws.event_flush_batch_size", 1)
  2196. viper.SetDefault("gateway.openai_ws.event_flush_interval_ms", 10)
  2197. viper.SetDefault("gateway.openai_ws.prewarm_cooldown_ms", 300)
  2198. viper.SetDefault("gateway.openai_ws.fallback_cooldown_seconds", 30)
  2199. viper.SetDefault("gateway.openai_ws.retry_backoff_initial_ms", 120)
  2200. viper.SetDefault("gateway.openai_ws.retry_backoff_max_ms", 2000)
  2201. viper.SetDefault("gateway.openai_ws.retry_jitter_ratio", 0.2)
  2202. viper.SetDefault("gateway.openai_ws.retry_total_budget_ms", 5000)
  2203. viper.SetDefault("gateway.openai_ws.payload_log_sample_rate", 0.2)
  2204. viper.SetDefault("gateway.openai_ws.lb_top_k", 7)
  2205. viper.SetDefault("gateway.openai_ws.sticky_session_ttl_seconds", 3600)
  2206. viper.SetDefault("gateway.openai_ws.session_hash_read_old_fallback", true)
  2207. viper.SetDefault("gateway.openai_ws.session_hash_dual_write_old", true)
  2208. viper.SetDefault("gateway.openai_ws.metadata_bridge_enabled", true)
  2209. viper.SetDefault("gateway.openai_ws.sticky_response_id_ttl_seconds", 3600)
  2210. viper.SetDefault("gateway.openai_ws.sticky_previous_response_ttl_seconds", 3600)
  2211. viper.SetDefault("gateway.openai_ws.scheduler_score_weights.priority", 1.0)
  2212. viper.SetDefault("gateway.openai_ws.scheduler_score_weights.load", 1.0)
  2213. viper.SetDefault("gateway.openai_ws.scheduler_score_weights.queue", 0.7)
  2214. viper.SetDefault("gateway.openai_ws.scheduler_score_weights.error_rate", 0.8)
  2215. viper.SetDefault("gateway.openai_ws.scheduler_score_weights.ttft", 0.5)
  2216. viper.SetDefault("gateway.openai_ws.scheduler_score_weights.reset", 0.0)
  2217. viper.SetDefault("gateway.openai_ws.scheduler_score_weights.quota_headroom", 0.0)
  2218. viper.SetDefault("gateway.openai_ws.scheduler_score_weights.upstream_cost", 0.0)
  2219. viper.SetDefault("gateway.openai_ws.scheduler_score_weights.previous_response", 5.0)
  2220. viper.SetDefault("gateway.openai_ws.scheduler_score_weights.session_sticky", 3.0)
  2221. // OpenAI HTTP upstream protocol strategy
  2222. viper.SetDefault("gateway.openai_http2.enabled", true)
  2223. viper.SetDefault("gateway.openai_http2.allow_proxy_fallback_to_http1", true)
  2224. viper.SetDefault("gateway.openai_http2.fallback_error_threshold", 2)
  2225. viper.SetDefault("gateway.openai_http2.fallback_window_seconds", 60)
  2226. viper.SetDefault("gateway.openai_http2.fallback_ttl_seconds", 600)
  2227. viper.SetDefault("gateway.openai_proxy_stream_circuit.disabled", false)
  2228. viper.SetDefault("gateway.openai_proxy_stream_circuit.failure_threshold", 2)
  2229. viper.SetDefault("gateway.openai_proxy_stream_circuit.window_seconds", 60)
  2230. viper.SetDefault("gateway.openai_proxy_stream_circuit.ttl_seconds", 600)
  2231. // Grok free-tier local soft gate (scheduler-only; admin QueryQuota does not use this).
  2232. // Enabled by default because free detection requires an explicit free tier marker.
  2233. viper.SetDefault("gateway.grok.free_quota_soft_gate_enabled", true)
  2234. viper.SetDefault("gateway.grok.password_auth_enabled", false)
  2235. // Free soft-gate nominal limit: 500k tokens / rolling 24h (operator policy).
  2236. viper.SetDefault("gateway.grok.free_quota_token_limit", int64(500_000))
  2237. viper.SetDefault("gateway.grok.free_quota_soft_gate_percent", 95)
  2238. viper.SetDefault("gateway.grok.free_quota_window_hours", 24)
  2239. viper.SetDefault("gateway.grok.free_quota_stats_cache_seconds", 60)
  2240. // 国产供应商余额检测(kimi/deepseek payg;zhipu 无余额端点,仅靠响应式 429/402)。
  2241. viper.SetDefault("gateway.cn_providers.balance_check_enabled", true)
  2242. viper.SetDefault("gateway.cn_providers.balance_threshold", 0.5)
  2243. viper.SetDefault("gateway.cn_providers.balance_check_interval_minutes", 10)
  2244. viper.SetDefault("gateway.image_concurrency.enabled", false)
  2245. viper.SetDefault("gateway.image_concurrency.max_concurrent_requests", 0)
  2246. viper.SetDefault("gateway.image_concurrency.overflow_mode", ImageConcurrencyOverflowModeReject)
  2247. viper.SetDefault("gateway.image_concurrency.wait_timeout_seconds", 30)
  2248. viper.SetDefault("gateway.image_concurrency.max_waiting_requests", 100)
  2249. viper.SetDefault("gateway.antigravity_fallback_cooldown_minutes", 1)
  2250. viper.SetDefault("gateway.antigravity_extra_retries", 10)
  2251. viper.SetDefault("gateway.max_body_size", int64(256*1024*1024))
  2252. viper.SetDefault("gateway.text_max_body_size", int64(32*1024*1024))
  2253. viper.SetDefault("gateway.upstream_response_read_max_bytes", DefaultUpstreamResponseReadMaxBytes)
  2254. viper.SetDefault("gateway.models_list_read_max_bytes", DefaultModelsListReadMaxBytes)
  2255. viper.SetDefault("gateway.proxy_probe_response_read_max_bytes", int64(1024*1024))
  2256. viper.SetDefault("gateway.gemini_debug_response_headers", false)
  2257. viper.SetDefault("gateway.connection_pool_isolation", ConnectionPoolIsolationAccountProxy)
  2258. // HTTP 上游连接池配置(针对 5000+ 并发用户优化)
  2259. viper.SetDefault("gateway.max_idle_conns", 2560) // 最大空闲连接总数(高并发场景可调大)
  2260. viper.SetDefault("gateway.max_idle_conns_per_host", 120) // 每主机最大空闲连接(HTTP/2 场景默认)
  2261. viper.SetDefault("gateway.max_conns_per_host", 1024) // 每主机最大连接数(含活跃;流式/HTTP1.1 场景可调大,如 2400+)
  2262. viper.SetDefault("gateway.idle_conn_timeout_seconds", 90) // 空闲连接超时(秒)
  2263. viper.SetDefault("gateway.max_upstream_clients", 5000)
  2264. viper.SetDefault("gateway.client_idle_ttl_seconds", 900)
  2265. viper.SetDefault("gateway.concurrency_slot_ttl_minutes", 30) // 并发槽位过期时间(支持超长请求)
  2266. viper.SetDefault("gateway.stream_data_interval_timeout", 180)
  2267. viper.SetDefault("gateway.stream_keepalive_interval", 10)
  2268. viper.SetDefault("gateway.image_stream_data_interval_timeout", 900)
  2269. viper.SetDefault("gateway.image_stream_keepalive_interval", 10)
  2270. viper.SetDefault("gateway.image_nonstream_keepalive_interval", 0)
  2271. viper.SetDefault("gateway.max_line_size", 500*1024*1024)
  2272. viper.SetDefault("gateway.scheduling.sticky_session_max_waiting", 3)
  2273. viper.SetDefault("gateway.scheduling.sticky_session_wait_timeout", 120*time.Second)
  2274. viper.SetDefault("gateway.scheduling.fallback_wait_timeout", 30*time.Second)
  2275. viper.SetDefault("gateway.scheduling.fallback_max_waiting", 100)
  2276. viper.SetDefault("gateway.scheduling.fallback_selection_mode", "last_used")
  2277. viper.SetDefault("gateway.scheduling.prefer_soonest_reset", false)
  2278. viper.SetDefault("gateway.scheduling.load_batch_enabled", true)
  2279. viper.SetDefault("gateway.scheduling.load_batch_cache_ttl_ms", 200)
  2280. viper.SetDefault("gateway.scheduling.snapshot_mget_chunk_size", 128)
  2281. viper.SetDefault("gateway.scheduling.snapshot_write_chunk_size", 256)
  2282. viper.SetDefault("gateway.scheduling.slot_cleanup_interval", 30*time.Second)
  2283. viper.SetDefault("gateway.scheduling.db_fallback_enabled", true)
  2284. viper.SetDefault("gateway.scheduling.db_fallback_timeout_seconds", 0)
  2285. viper.SetDefault("gateway.scheduling.db_fallback_max_qps", 0)
  2286. viper.SetDefault("gateway.scheduling.outbox_poll_interval_seconds", 1)
  2287. viper.SetDefault("gateway.scheduling.outbox_lag_warn_seconds", 5)
  2288. viper.SetDefault("gateway.scheduling.outbox_lag_rebuild_seconds", 10)
  2289. viper.SetDefault("gateway.scheduling.outbox_lag_rebuild_failures", 3)
  2290. viper.SetDefault("gateway.scheduling.outbox_backlog_rebuild_rows", 10000)
  2291. viper.SetDefault("gateway.scheduling.full_rebuild_interval_seconds", 300)
  2292. viper.SetDefault("gateway.usage_record.worker_count", 128)
  2293. viper.SetDefault("gateway.usage_record.queue_size", 16384)
  2294. viper.SetDefault("gateway.usage_record.task_timeout_seconds", 5)
  2295. // 默认 sync:队列满时由提交方内联执行(提交点在响应写出之后,不阻塞客户端)。
  2296. // sample/drop 会在溢出时静默丢弃计费任务,造成扣费与 usage_logs 对账缺口(issue #3656),
  2297. // 仅供显式配置的运维场景使用。
  2298. viper.SetDefault("gateway.usage_record.overflow_policy", UsageRecordOverflowPolicySync)
  2299. viper.SetDefault("gateway.usage_record.overflow_sample_percent", 10)
  2300. viper.SetDefault("gateway.usage_record.auto_scale_enabled", true)
  2301. viper.SetDefault("gateway.usage_record.auto_scale_min_workers", 128)
  2302. viper.SetDefault("gateway.usage_record.auto_scale_max_workers", 512)
  2303. viper.SetDefault("gateway.usage_record.auto_scale_up_queue_percent", 70)
  2304. viper.SetDefault("gateway.usage_record.auto_scale_down_queue_percent", 15)
  2305. viper.SetDefault("gateway.usage_record.auto_scale_up_step", 32)
  2306. viper.SetDefault("gateway.usage_record.auto_scale_down_step", 16)
  2307. viper.SetDefault("gateway.usage_record.auto_scale_check_interval_seconds", 3)
  2308. viper.SetDefault("gateway.usage_record.auto_scale_cooldown_seconds", 10)
  2309. viper.SetDefault("gateway.user_group_rate_cache_ttl_seconds", 30)
  2310. viper.SetDefault("gateway.models_list_cache_ttl_seconds", 15)
  2311. // TLS指纹伪装配置(默认关闭,需要账号级别单独启用)
  2312. // 用户消息串行队列默认值
  2313. viper.SetDefault("gateway.user_message_queue.enabled", false)
  2314. viper.SetDefault("gateway.user_message_queue.lock_ttl_ms", 120000)
  2315. viper.SetDefault("gateway.user_message_queue.wait_timeout_ms", 30000)
  2316. viper.SetDefault("gateway.user_message_queue.min_delay_ms", 200)
  2317. viper.SetDefault("gateway.user_message_queue.max_delay_ms", 2000)
  2318. viper.SetDefault("gateway.user_message_queue.cleanup_interval_seconds", 60)
  2319. viper.SetDefault("gateway.tls_fingerprint.enabled", true)
  2320. viper.SetDefault("concurrency.ping_interval", 10)
  2321. // TokenRefresh
  2322. viper.SetDefault("token_refresh.enabled", true)
  2323. viper.SetDefault("token_refresh.check_interval_minutes", 5) // 每5分钟检查一次
  2324. viper.SetDefault("token_refresh.refresh_before_expiry_hours", 0.5) // 提前30分钟刷新(适配Google 1小时token)
  2325. viper.SetDefault("token_refresh.max_retries", 3) // 最多重试3次
  2326. viper.SetDefault("token_refresh.retry_backoff_seconds", 2) // 重试退避基础2秒
  2327. viper.SetDefault("token_refresh.candidate_page_size", 200)
  2328. viper.SetDefault("token_refresh.provider_concurrency", 4)
  2329. viper.SetDefault("token_refresh.provider_qps", 2)
  2330. viper.SetDefault("token_refresh.provider_failure_threshold", 3)
  2331. viper.SetDefault("token_refresh.attempt_timeout_seconds", 15)
  2332. viper.SetDefault("token_refresh.cycle_timeout_seconds", 240)
  2333. // Gemini OAuth - configure via environment variables or config file
  2334. // GEMINI_OAUTH_CLIENT_ID and GEMINI_OAUTH_CLIENT_SECRET
  2335. // Default: uses Gemini CLI public credentials (set via environment)
  2336. viper.SetDefault("gemini.oauth.client_id", "")
  2337. viper.SetDefault("gemini.oauth.client_secret", "")
  2338. viper.SetDefault("gemini.oauth.scopes", "")
  2339. viper.SetDefault("gemini.quota.policy", "")
  2340. // Subscription Maintenance (bounded queue + worker pool)
  2341. viper.SetDefault("subscription_maintenance.worker_count", 2)
  2342. viper.SetDefault("subscription_maintenance.queue_size", 1024)
  2343. setEnvReachableDefaults()
  2344. }
  2345. // setEnvReachableDefaults registers zero-valued defaults for keys that are
  2346. // documented in deploy/config.example.yaml but had no default of their own.
  2347. //
  2348. // viper.Unmarshal only decodes the keys returned by AllKeys(), which unions
  2349. // SetDefault keys, config-file keys and explicitly bound BindEnv keys.
  2350. // AutomaticEnv can override a key already in that union, but it never adds one,
  2351. // and the viper_bind_struct escape hatch is compiled out (we build with
  2352. // -tags embed). So a key that lives only in the example file was unreachable by
  2353. // environment variable: the value was read from the process environment and
  2354. // then silently dropped. Deployments driven purely by env — which is what
  2355. // deploy/docker-compose.yml does — got the zero value with no warning.
  2356. //
  2357. // The values below are deliberately zero rather than the documented example
  2358. // values: an absent key already unmarshalled to the zero value, so registering
  2359. // zero keeps behavior identical while making the key addressable from the
  2360. // environment. Any subsystem that wants a richer default still applies it after
  2361. // unmarshal, exactly as before.
  2362. func setEnvReachableDefaults() {
  2363. viper.SetDefault("gateway.forced_codex_instructions_template_file", "")
  2364. viper.SetDefault("gateway.session_idle_timeout_minutes", 0)
  2365. viper.SetDefault("gateway.user_message_queue.mode", "")
  2366. viper.SetDefault("update.proxy_url", "")
  2367. // sticky_escape_enabled is the one exception to the zero-value rule: its
  2368. // effective default is true, applied post-unmarshal via a viper.IsSet guard.
  2369. // Registering false would make IsSet always report true and permanently
  2370. // disable sticky escape, so register the effective default instead. An
  2371. // explicit false in config or env still wins.
  2372. viper.SetDefault("gateway.openai_scheduler.sticky_escape_enabled", true)
  2373. viper.SetDefault("gateway.openai_scheduler.sticky_escape_error_rate", 0.0)
  2374. viper.SetDefault("gateway.openai_scheduler.sticky_escape_ttft_ms", 0)
  2375. // server.trusted_proxies and security.forwarded_client_ip_headers are the
  2376. // other exception: load() distinguishes explicit configuration from absence
  2377. // (issue #4600), and viper.IsSet also reports registered defaults, so a
  2378. // SetDefault would make trusted proxies look permanently configured. Both
  2379. // environment variables are parsed by hand in load() via os.LookupEnv and
  2380. // were never silently dropped; binding them here records that reachability
  2381. // where AllKeys() — and the env-reachability guard — can see it, without
  2382. // affecting IsSet while the variables are absent. BindEnv only errors when
  2383. // called without arguments.
  2384. _ = viper.BindEnv("server.trusted_proxies", "SERVER_TRUSTED_PROXIES")
  2385. _ = viper.BindEnv("security.forwarded_client_ip_headers", "SECURITY_FORWARDED_CLIENT_IP_HEADERS")
  2386. // Third-party login providers. These carry client secrets and are exactly
  2387. // the settings an operator expects to inject via the environment, but every
  2388. // key here was previously unreachable that way.
  2389. for _, provider := range []string{"github_oauth", "google_oauth"} {
  2390. viper.SetDefault(provider+".enabled", false)
  2391. viper.SetDefault(provider+".client_id", "")
  2392. viper.SetDefault(provider+".client_secret", "")
  2393. viper.SetDefault(provider+".authorize_url", "")
  2394. viper.SetDefault(provider+".token_url", "")
  2395. viper.SetDefault(provider+".userinfo_url", "")
  2396. viper.SetDefault(provider+".emails_url", "")
  2397. viper.SetDefault(provider+".scopes", "")
  2398. viper.SetDefault(provider+".redirect_url", "")
  2399. viper.SetDefault(provider+".frontend_redirect_url", "")
  2400. }
  2401. viper.SetDefault("dingtalk_connect.client_id", "")
  2402. viper.SetDefault("dingtalk_connect.client_secret", "")
  2403. viper.SetDefault("dingtalk_connect.internal_corp_id", "")
  2404. viper.SetDefault("dingtalk_connect.redirect_url", "")
  2405. viper.SetDefault("dingtalk_connect.bypass_registration", false)
  2406. viper.SetDefault("dingtalk_connect.username_attribute_key", "")
  2407. viper.SetDefault("dingtalk_connect.enable_attribute_matching", false)
  2408. viper.SetDefault("dingtalk_connect.enable_attribute_sync", false)
  2409. viper.SetDefault("dingtalk_connect.attribute_sync_fields", []string{})
  2410. viper.SetDefault("dingtalk_connect.attribute_sync_overwrite_policy", "")
  2411. viper.SetDefault("dingtalk_connect.sync_display_name", false)
  2412. viper.SetDefault("dingtalk_connect.sync_display_name_attr_key", "")
  2413. viper.SetDefault("dingtalk_connect.sync_display_name_attr_name", "")
  2414. viper.SetDefault("dingtalk_connect.sync_dept", false)
  2415. viper.SetDefault("dingtalk_connect.sync_dept_attr_key", "")
  2416. viper.SetDefault("dingtalk_connect.sync_dept_attr_name", "")
  2417. viper.SetDefault("dingtalk_connect.sync_corp_email", false)
  2418. viper.SetDefault("dingtalk_connect.sync_corp_email_attr_key", "")
  2419. viper.SetDefault("dingtalk_connect.sync_corp_email_attr_name", "")
  2420. }
  2421. func (c *Config) Validate() error {
  2422. forwardedClientIPHeaders, err := NormalizeForwardedClientIPHeaders(c.Security.ForwardedClientIPHeaders)
  2423. if err != nil {
  2424. return fmt.Errorf("security.forwarded_client_ip_headers: %w", err)
  2425. }
  2426. c.Security.ForwardedClientIPHeaders = forwardedClientIPHeaders
  2427. c.SetForwardedClientIPSettings(c.Security.TrustForwardedIPForAPIKeyACL, forwardedClientIPHeaders)
  2428. proxyProbeURLs, err := normalizeProxyProbeURLs(c.Security.ProxyProbe.URLs)
  2429. if err != nil {
  2430. return fmt.Errorf("security.proxy_probe.urls: %w", err)
  2431. }
  2432. c.Security.ProxyProbe.URLs = proxyProbeURLs
  2433. if c.Plugins.MaxUploadBytes <= 0 || c.Plugins.MaxUploadBytes > 1024*1024*1024 {
  2434. return fmt.Errorf("plugins.max_upload_bytes must be between 1 and 1073741824")
  2435. }
  2436. if c.Plugins.MaxUncompressedBytes < c.Plugins.MaxUploadBytes || c.Plugins.MaxUncompressedBytes > 2*1024*1024*1024 {
  2437. return fmt.Errorf("plugins.max_uncompressed_bytes must be between max_upload_bytes and 2147483648")
  2438. }
  2439. if c.Plugins.StartTimeoutSeconds < 1 || c.Plugins.StartTimeoutSeconds > 120 {
  2440. return fmt.Errorf("plugins.start_timeout_seconds must be between 1 and 120")
  2441. }
  2442. if c.Server.ReadHeaderTimeout < 1 || c.Server.ReadHeaderTimeout > 60 {
  2443. return fmt.Errorf("server.read_header_timeout must be between 1 and 60 seconds")
  2444. }
  2445. if c.Server.MaxHeaderBytes < 8*1024 || c.Server.MaxHeaderBytes > 1024*1024 {
  2446. return fmt.Errorf("server.max_header_bytes must be between 8192 and 1048576 bytes")
  2447. }
  2448. if c.Server.IdleTimeout <= 0 {
  2449. return fmt.Errorf("server.idle_timeout must be positive")
  2450. }
  2451. if c.Server.MaxRequestBodySize < 0 {
  2452. return fmt.Errorf("server.max_request_body_size must be non-negative")
  2453. }
  2454. if c.Server.H2C.Enabled {
  2455. if c.Server.H2C.MaxConcurrentStreams == 0 {
  2456. return fmt.Errorf("server.h2c.max_concurrent_streams must be positive")
  2457. }
  2458. if c.Server.H2C.IdleTimeout <= 0 {
  2459. return fmt.Errorf("server.h2c.idle_timeout must be positive")
  2460. }
  2461. if c.Server.H2C.MaxReadFrameSize < 16*1024 || c.Server.H2C.MaxReadFrameSize > 16*1024*1024-1 {
  2462. return fmt.Errorf("server.h2c.max_read_frame_size must be between 16384 and 16777215 bytes")
  2463. }
  2464. if c.Server.H2C.MaxUploadBufferPerConnection < 65535 {
  2465. return fmt.Errorf("server.h2c.max_upload_buffer_per_connection must be at least 65535 bytes")
  2466. }
  2467. if c.Server.H2C.MaxUploadBufferPerStream <= 0 {
  2468. return fmt.Errorf("server.h2c.max_upload_buffer_per_stream must be positive")
  2469. }
  2470. }
  2471. if c.APIKeyAuth.InvalidAbuse.Enabled {
  2472. if c.APIKeyAuth.InvalidAbuse.Threshold < 10 {
  2473. return fmt.Errorf("api_key_auth_cache.invalid_abuse.threshold must be at least 10")
  2474. }
  2475. if c.APIKeyAuth.InvalidAbuse.WindowSeconds < 1 || c.APIKeyAuth.InvalidAbuse.WindowSeconds > 3600 {
  2476. return fmt.Errorf("api_key_auth_cache.invalid_abuse.window_seconds must be between 1 and 3600")
  2477. }
  2478. if c.APIKeyAuth.InvalidAbuse.BlockSeconds < 1 || c.APIKeyAuth.InvalidAbuse.BlockSeconds > 3600 {
  2479. return fmt.Errorf("api_key_auth_cache.invalid_abuse.block_seconds must be between 1 and 3600")
  2480. }
  2481. if c.APIKeyAuth.InvalidAbuse.Capacity < 256 || c.APIKeyAuth.InvalidAbuse.Capacity > 1_000_000 {
  2482. return fmt.Errorf("api_key_auth_cache.invalid_abuse.capacity must be between 256 and 1000000")
  2483. }
  2484. }
  2485. jwtSecret := strings.TrimSpace(c.JWT.Secret)
  2486. if jwtSecret == "" {
  2487. return fmt.Errorf("jwt.secret is required")
  2488. }
  2489. // NOTE: 按 UTF-8 编码后的字节长度计算。
  2490. // 选择 bytes 而不是 rune 计数,确保二进制/随机串的长度语义更接近“熵”而非“字符数”。
  2491. if len([]byte(jwtSecret)) < 32 {
  2492. return fmt.Errorf("jwt.secret must be at least 32 bytes")
  2493. }
  2494. switch c.Log.Level {
  2495. case "debug", "info", "warn", "error":
  2496. case "":
  2497. return fmt.Errorf("log.level is required")
  2498. default:
  2499. return fmt.Errorf("log.level must be one of: debug/info/warn/error")
  2500. }
  2501. switch c.Log.Format {
  2502. case "json", "console":
  2503. case "":
  2504. return fmt.Errorf("log.format is required")
  2505. default:
  2506. return fmt.Errorf("log.format must be one of: json/console")
  2507. }
  2508. switch c.Log.StacktraceLevel {
  2509. case "none", "error", "fatal":
  2510. case "":
  2511. return fmt.Errorf("log.stacktrace_level is required")
  2512. default:
  2513. return fmt.Errorf("log.stacktrace_level must be one of: none/error/fatal")
  2514. }
  2515. if !c.Log.Output.ToStdout && !c.Log.Output.ToFile {
  2516. return fmt.Errorf("log.output.to_stdout and log.output.to_file cannot both be false")
  2517. }
  2518. if c.Log.Rotation.MaxSizeMB <= 0 {
  2519. return fmt.Errorf("log.rotation.max_size_mb must be positive")
  2520. }
  2521. if c.Log.Rotation.MaxBackups < 0 {
  2522. return fmt.Errorf("log.rotation.max_backups must be non-negative")
  2523. }
  2524. if c.Log.Rotation.MaxAgeDays < 0 {
  2525. return fmt.Errorf("log.rotation.max_age_days must be non-negative")
  2526. }
  2527. if c.Log.Sampling.Enabled {
  2528. if c.Log.Sampling.Initial <= 0 {
  2529. return fmt.Errorf("log.sampling.initial must be positive when sampling is enabled")
  2530. }
  2531. if c.Log.Sampling.Thereafter <= 0 {
  2532. return fmt.Errorf("log.sampling.thereafter must be positive when sampling is enabled")
  2533. }
  2534. } else {
  2535. if c.Log.Sampling.Initial < 0 {
  2536. return fmt.Errorf("log.sampling.initial must be non-negative")
  2537. }
  2538. if c.Log.Sampling.Thereafter < 0 {
  2539. return fmt.Errorf("log.sampling.thereafter must be non-negative")
  2540. }
  2541. }
  2542. if c.SubscriptionMaintenance.WorkerCount < 0 {
  2543. return fmt.Errorf("subscription_maintenance.worker_count must be non-negative")
  2544. }
  2545. if c.SubscriptionMaintenance.QueueSize < 0 {
  2546. return fmt.Errorf("subscription_maintenance.queue_size must be non-negative")
  2547. }
  2548. // Gemini OAuth 配置校验:client_id 与 client_secret 必须同时设置或同时留空。
  2549. // 留空时表示使用内置的 Gemini CLI OAuth 客户端(其 client_secret 通过环境变量注入)。
  2550. geminiClientID := strings.TrimSpace(c.Gemini.OAuth.ClientID)
  2551. geminiClientSecret := strings.TrimSpace(c.Gemini.OAuth.ClientSecret)
  2552. if (geminiClientID == "") != (geminiClientSecret == "") {
  2553. return fmt.Errorf("gemini.oauth.client_id and gemini.oauth.client_secret must be both set or both empty")
  2554. }
  2555. if strings.TrimSpace(c.Server.FrontendURL) != "" {
  2556. if err := ValidateAbsoluteHTTPURL(c.Server.FrontendURL); err != nil {
  2557. return fmt.Errorf("server.frontend_url invalid: %w", err)
  2558. }
  2559. u, err := url.Parse(strings.TrimSpace(c.Server.FrontendURL))
  2560. if err != nil {
  2561. return fmt.Errorf("server.frontend_url invalid: %w", err)
  2562. }
  2563. if u.RawQuery != "" || u.ForceQuery {
  2564. return fmt.Errorf("server.frontend_url invalid: must not include query")
  2565. }
  2566. if u.User != nil {
  2567. return fmt.Errorf("server.frontend_url invalid: must not include userinfo")
  2568. }
  2569. warnIfInsecureURL("server.frontend_url", c.Server.FrontendURL)
  2570. }
  2571. if c.WebAuthn.Enabled {
  2572. c.WebAuthn.RPDisplayName = strings.TrimSpace(c.WebAuthn.RPDisplayName)
  2573. c.WebAuthn.RPID = strings.ToLower(strings.TrimSpace(c.WebAuthn.RPID))
  2574. c.WebAuthn.RPOrigins = normalizeStringSlice(c.WebAuthn.RPOrigins)
  2575. if c.WebAuthn.RPDisplayName == "" {
  2576. return fmt.Errorf("webauthn.rp_display_name is required when passkeys are enabled")
  2577. }
  2578. if c.WebAuthn.RPID == "" {
  2579. return fmt.Errorf("webauthn.rp_id is required when passkeys are enabled")
  2580. }
  2581. if strings.Contains(c.WebAuthn.RPID, "://") || strings.ContainsAny(c.WebAuthn.RPID, "/:") {
  2582. return fmt.Errorf("webauthn.rp_id must be a domain without scheme, port, or path")
  2583. }
  2584. if len(c.WebAuthn.RPOrigins) == 0 {
  2585. return fmt.Errorf("webauthn.rp_origins must contain at least one origin when passkeys are enabled")
  2586. }
  2587. for i, origin := range c.WebAuthn.RPOrigins {
  2588. u, err := url.Parse(origin)
  2589. if err != nil || u.Scheme == "" || u.Host == "" {
  2590. return fmt.Errorf("webauthn.rp_origins contains invalid origin %q", origin)
  2591. }
  2592. if u.User != nil || u.RawQuery != "" || u.Fragment != "" || u.Path != "" {
  2593. return fmt.Errorf("webauthn.rp_origins entry %q must not include userinfo, path, query, or fragment", origin)
  2594. }
  2595. u.Scheme = strings.ToLower(u.Scheme)
  2596. u.Host = strings.ToLower(u.Host)
  2597. host := strings.ToLower(u.Hostname())
  2598. localDevelopment := host == "localhost" || host == "127.0.0.1" || host == "::1"
  2599. if u.Scheme != "https" && (u.Scheme != "http" || !localDevelopment) {
  2600. return fmt.Errorf("webauthn.rp_origins entry %q must use HTTPS (HTTP is allowed only for localhost)", origin)
  2601. }
  2602. if host != c.WebAuthn.RPID && !strings.HasSuffix(host, "."+c.WebAuthn.RPID) {
  2603. return fmt.Errorf("webauthn.rp_origins entry %q is not within relying party ID %q", origin, c.WebAuthn.RPID)
  2604. }
  2605. c.WebAuthn.RPOrigins[i] = u.Scheme + "://" + u.Host
  2606. }
  2607. }
  2608. if c.JWT.ExpireHour <= 0 {
  2609. return fmt.Errorf("jwt.expire_hour must be positive")
  2610. }
  2611. if c.JWT.ExpireHour > 168 {
  2612. return fmt.Errorf("jwt.expire_hour must be <= 168 (7 days)")
  2613. }
  2614. if c.JWT.ExpireHour > 24 {
  2615. slog.Warn("jwt.expire_hour is high; consider shorter expiration for security", "expire_hour", c.JWT.ExpireHour)
  2616. }
  2617. // JWT Refresh Token配置验证
  2618. if c.JWT.AccessTokenExpireMinutes < 0 {
  2619. return fmt.Errorf("jwt.access_token_expire_minutes must be non-negative")
  2620. }
  2621. if c.JWT.AccessTokenExpireMinutes > 720 {
  2622. slog.Warn("jwt.access_token_expire_minutes is high; consider shorter expiration for security", "access_token_expire_minutes", c.JWT.AccessTokenExpireMinutes)
  2623. }
  2624. if c.JWT.RefreshTokenExpireDays <= 0 {
  2625. return fmt.Errorf("jwt.refresh_token_expire_days must be positive")
  2626. }
  2627. if c.JWT.RefreshTokenExpireDays > 90 {
  2628. slog.Warn("jwt.refresh_token_expire_days is high; consider shorter expiration for security", "refresh_token_expire_days", c.JWT.RefreshTokenExpireDays)
  2629. }
  2630. if c.JWT.RefreshWindowMinutes < 0 {
  2631. return fmt.Errorf("jwt.refresh_window_minutes must be non-negative")
  2632. }
  2633. if c.Security.CSP.Enabled && strings.TrimSpace(c.Security.CSP.Policy) == "" {
  2634. return fmt.Errorf("security.csp.policy is required when CSP is enabled")
  2635. }
  2636. if c.LinuxDo.Enabled {
  2637. if strings.TrimSpace(c.LinuxDo.ClientID) == "" {
  2638. return fmt.Errorf("linuxdo_connect.client_id is required when linuxdo_connect.enabled=true")
  2639. }
  2640. if strings.TrimSpace(c.LinuxDo.AuthorizeURL) == "" {
  2641. return fmt.Errorf("linuxdo_connect.authorize_url is required when linuxdo_connect.enabled=true")
  2642. }
  2643. if strings.TrimSpace(c.LinuxDo.TokenURL) == "" {
  2644. return fmt.Errorf("linuxdo_connect.token_url is required when linuxdo_connect.enabled=true")
  2645. }
  2646. if strings.TrimSpace(c.LinuxDo.UserInfoURL) == "" {
  2647. return fmt.Errorf("linuxdo_connect.userinfo_url is required when linuxdo_connect.enabled=true")
  2648. }
  2649. if strings.TrimSpace(c.LinuxDo.RedirectURL) == "" {
  2650. return fmt.Errorf("linuxdo_connect.redirect_url is required when linuxdo_connect.enabled=true")
  2651. }
  2652. method := strings.ToLower(strings.TrimSpace(c.LinuxDo.TokenAuthMethod))
  2653. switch method {
  2654. case "", "client_secret_post", "client_secret_basic", "none":
  2655. default:
  2656. return fmt.Errorf("linuxdo_connect.token_auth_method must be one of: client_secret_post/client_secret_basic/none")
  2657. }
  2658. if (method == "" || method == "client_secret_post" || method == "client_secret_basic") &&
  2659. strings.TrimSpace(c.LinuxDo.ClientSecret) == "" {
  2660. return fmt.Errorf("linuxdo_connect.client_secret is required when linuxdo_connect.enabled=true and token_auth_method is client_secret_post/client_secret_basic")
  2661. }
  2662. if strings.TrimSpace(c.LinuxDo.FrontendRedirectURL) == "" {
  2663. return fmt.Errorf("linuxdo_connect.frontend_redirect_url is required when linuxdo_connect.enabled=true")
  2664. }
  2665. if err := ValidateAbsoluteHTTPURL(c.LinuxDo.AuthorizeURL); err != nil {
  2666. return fmt.Errorf("linuxdo_connect.authorize_url invalid: %w", err)
  2667. }
  2668. if err := ValidateAbsoluteHTTPURL(c.LinuxDo.TokenURL); err != nil {
  2669. return fmt.Errorf("linuxdo_connect.token_url invalid: %w", err)
  2670. }
  2671. if err := ValidateAbsoluteHTTPURL(c.LinuxDo.UserInfoURL); err != nil {
  2672. return fmt.Errorf("linuxdo_connect.userinfo_url invalid: %w", err)
  2673. }
  2674. if err := ValidateAbsoluteHTTPURL(c.LinuxDo.RedirectURL); err != nil {
  2675. return fmt.Errorf("linuxdo_connect.redirect_url invalid: %w", err)
  2676. }
  2677. if err := ValidateFrontendRedirectURL(c.LinuxDo.FrontendRedirectURL); err != nil {
  2678. return fmt.Errorf("linuxdo_connect.frontend_redirect_url invalid: %w", err)
  2679. }
  2680. warnIfInsecureURL("linuxdo_connect.authorize_url", c.LinuxDo.AuthorizeURL)
  2681. warnIfInsecureURL("linuxdo_connect.token_url", c.LinuxDo.TokenURL)
  2682. warnIfInsecureURL("linuxdo_connect.userinfo_url", c.LinuxDo.UserInfoURL)
  2683. warnIfInsecureURL("linuxdo_connect.redirect_url", c.LinuxDo.RedirectURL)
  2684. warnIfInsecureURL("linuxdo_connect.frontend_redirect_url", c.LinuxDo.FrontendRedirectURL)
  2685. }
  2686. if c.WeChat.Enabled {
  2687. weChat := c.WeChat
  2688. normalizeWeChatConnectConfig(&weChat)
  2689. if weChat.OpenEnabled {
  2690. if strings.TrimSpace(weChat.OpenAppID) == "" {
  2691. return fmt.Errorf("wechat_connect.open_app_id is required when wechat_connect.open_enabled=true")
  2692. }
  2693. if strings.TrimSpace(weChat.OpenAppSecret) == "" {
  2694. return fmt.Errorf("wechat_connect.open_app_secret is required when wechat_connect.open_enabled=true")
  2695. }
  2696. }
  2697. if weChat.MPEnabled {
  2698. if strings.TrimSpace(weChat.MPAppID) == "" {
  2699. return fmt.Errorf("wechat_connect.mp_app_id is required when wechat_connect.mp_enabled=true")
  2700. }
  2701. if strings.TrimSpace(weChat.MPAppSecret) == "" {
  2702. return fmt.Errorf("wechat_connect.mp_app_secret is required when wechat_connect.mp_enabled=true")
  2703. }
  2704. }
  2705. if weChat.MobileEnabled {
  2706. if strings.TrimSpace(weChat.MobileAppID) == "" {
  2707. return fmt.Errorf("wechat_connect.mobile_app_id is required when wechat_connect.mobile_enabled=true")
  2708. }
  2709. if strings.TrimSpace(weChat.MobileAppSecret) == "" {
  2710. return fmt.Errorf("wechat_connect.mobile_app_secret is required when wechat_connect.mobile_enabled=true")
  2711. }
  2712. }
  2713. if v := strings.TrimSpace(weChat.RedirectURL); v != "" {
  2714. if err := ValidateAbsoluteHTTPURL(v); err != nil {
  2715. return fmt.Errorf("wechat_connect.redirect_url invalid: %w", err)
  2716. }
  2717. warnIfInsecureURL("wechat_connect.redirect_url", v)
  2718. }
  2719. if err := ValidateFrontendRedirectURL(weChat.FrontendRedirectURL); err != nil {
  2720. return fmt.Errorf("wechat_connect.frontend_redirect_url invalid: %w", err)
  2721. }
  2722. warnIfInsecureURL("wechat_connect.frontend_redirect_url", weChat.FrontendRedirectURL)
  2723. }
  2724. if c.OIDC.Enabled {
  2725. if strings.TrimSpace(c.OIDC.ClientID) == "" {
  2726. return fmt.Errorf("oidc_connect.client_id is required when oidc_connect.enabled=true")
  2727. }
  2728. if strings.TrimSpace(c.OIDC.IssuerURL) == "" {
  2729. return fmt.Errorf("oidc_connect.issuer_url is required when oidc_connect.enabled=true")
  2730. }
  2731. if strings.TrimSpace(c.OIDC.RedirectURL) == "" {
  2732. return fmt.Errorf("oidc_connect.redirect_url is required when oidc_connect.enabled=true")
  2733. }
  2734. if strings.TrimSpace(c.OIDC.FrontendRedirectURL) == "" {
  2735. return fmt.Errorf("oidc_connect.frontend_redirect_url is required when oidc_connect.enabled=true")
  2736. }
  2737. if !scopeContainsOpenID(c.OIDC.Scopes) {
  2738. return fmt.Errorf("oidc_connect.scopes must contain openid")
  2739. }
  2740. method := strings.ToLower(strings.TrimSpace(c.OIDC.TokenAuthMethod))
  2741. switch method {
  2742. case "", "client_secret_post", "client_secret_basic", "none":
  2743. default:
  2744. return fmt.Errorf("oidc_connect.token_auth_method must be one of: client_secret_post/client_secret_basic/none")
  2745. }
  2746. if (method == "" || method == "client_secret_post" || method == "client_secret_basic") &&
  2747. strings.TrimSpace(c.OIDC.ClientSecret) == "" {
  2748. return fmt.Errorf("oidc_connect.client_secret is required when oidc_connect.enabled=true and token_auth_method is client_secret_post/client_secret_basic")
  2749. }
  2750. if c.OIDC.ClockSkewSeconds < 0 || c.OIDC.ClockSkewSeconds > 600 {
  2751. return fmt.Errorf("oidc_connect.clock_skew_seconds must be between 0 and 600")
  2752. }
  2753. if c.OIDC.ValidateIDToken && strings.TrimSpace(c.OIDC.AllowedSigningAlgs) == "" {
  2754. return fmt.Errorf("oidc_connect.allowed_signing_algs is required when oidc_connect.validate_id_token=true")
  2755. }
  2756. if err := ValidateAbsoluteHTTPURL(c.OIDC.IssuerURL); err != nil {
  2757. return fmt.Errorf("oidc_connect.issuer_url invalid: %w", err)
  2758. }
  2759. if v := strings.TrimSpace(c.OIDC.DiscoveryURL); v != "" {
  2760. if err := ValidateAbsoluteHTTPURL(v); err != nil {
  2761. return fmt.Errorf("oidc_connect.discovery_url invalid: %w", err)
  2762. }
  2763. }
  2764. if v := strings.TrimSpace(c.OIDC.AuthorizeURL); v != "" {
  2765. if err := ValidateAbsoluteHTTPURL(v); err != nil {
  2766. return fmt.Errorf("oidc_connect.authorize_url invalid: %w", err)
  2767. }
  2768. }
  2769. if v := strings.TrimSpace(c.OIDC.TokenURL); v != "" {
  2770. if err := ValidateAbsoluteHTTPURL(v); err != nil {
  2771. return fmt.Errorf("oidc_connect.token_url invalid: %w", err)
  2772. }
  2773. }
  2774. if v := strings.TrimSpace(c.OIDC.UserInfoURL); v != "" {
  2775. if err := ValidateAbsoluteHTTPURL(v); err != nil {
  2776. return fmt.Errorf("oidc_connect.userinfo_url invalid: %w", err)
  2777. }
  2778. }
  2779. if v := strings.TrimSpace(c.OIDC.JWKSURL); v != "" {
  2780. if err := ValidateAbsoluteHTTPURL(v); err != nil {
  2781. return fmt.Errorf("oidc_connect.jwks_url invalid: %w", err)
  2782. }
  2783. }
  2784. if err := ValidateAbsoluteHTTPURL(c.OIDC.RedirectURL); err != nil {
  2785. return fmt.Errorf("oidc_connect.redirect_url invalid: %w", err)
  2786. }
  2787. if err := ValidateFrontendRedirectURL(c.OIDC.FrontendRedirectURL); err != nil {
  2788. return fmt.Errorf("oidc_connect.frontend_redirect_url invalid: %w", err)
  2789. }
  2790. warnIfInsecureURL("oidc_connect.issuer_url", c.OIDC.IssuerURL)
  2791. warnIfInsecureURL("oidc_connect.discovery_url", c.OIDC.DiscoveryURL)
  2792. warnIfInsecureURL("oidc_connect.authorize_url", c.OIDC.AuthorizeURL)
  2793. warnIfInsecureURL("oidc_connect.token_url", c.OIDC.TokenURL)
  2794. warnIfInsecureURL("oidc_connect.userinfo_url", c.OIDC.UserInfoURL)
  2795. warnIfInsecureURL("oidc_connect.jwks_url", c.OIDC.JWKSURL)
  2796. warnIfInsecureURL("oidc_connect.redirect_url", c.OIDC.RedirectURL)
  2797. warnIfInsecureURL("oidc_connect.frontend_redirect_url", c.OIDC.FrontendRedirectURL)
  2798. }
  2799. if c.Billing.CircuitBreaker.Enabled {
  2800. if c.Billing.CircuitBreaker.FailureThreshold <= 0 {
  2801. return fmt.Errorf("billing.circuit_breaker.failure_threshold must be positive")
  2802. }
  2803. if c.Billing.CircuitBreaker.ResetTimeoutSeconds <= 0 {
  2804. return fmt.Errorf("billing.circuit_breaker.reset_timeout_seconds must be positive")
  2805. }
  2806. if c.Billing.CircuitBreaker.HalfOpenRequests <= 0 {
  2807. return fmt.Errorf("billing.circuit_breaker.half_open_requests must be positive")
  2808. }
  2809. }
  2810. if c.Billing.MinimumBalanceReserve < 0 {
  2811. return fmt.Errorf("billing.minimum_balance_reserve must be non-negative")
  2812. }
  2813. if c.Database.MaxOpenConns <= 0 {
  2814. return fmt.Errorf("database.max_open_conns must be positive")
  2815. }
  2816. if c.Database.MaxIdleConns < 0 {
  2817. return fmt.Errorf("database.max_idle_conns must be non-negative")
  2818. }
  2819. if c.Database.MaxIdleConns > c.Database.MaxOpenConns {
  2820. return fmt.Errorf("database.max_idle_conns cannot exceed database.max_open_conns")
  2821. }
  2822. if c.Database.ConnMaxLifetimeMinutes < 0 {
  2823. return fmt.Errorf("database.conn_max_lifetime_minutes must be non-negative")
  2824. }
  2825. if c.Database.ConnMaxIdleTimeMinutes < 0 {
  2826. return fmt.Errorf("database.conn_max_idle_time_minutes must be non-negative")
  2827. }
  2828. if c.Redis.DialTimeoutSeconds <= 0 {
  2829. return fmt.Errorf("redis.dial_timeout_seconds must be positive")
  2830. }
  2831. if c.Redis.ReadTimeoutSeconds <= 0 {
  2832. return fmt.Errorf("redis.read_timeout_seconds must be positive")
  2833. }
  2834. if c.Redis.WriteTimeoutSeconds <= 0 {
  2835. return fmt.Errorf("redis.write_timeout_seconds must be positive")
  2836. }
  2837. if c.Redis.PoolSize <= 0 {
  2838. return fmt.Errorf("redis.pool_size must be positive")
  2839. }
  2840. if c.Redis.MinIdleConns < 0 {
  2841. return fmt.Errorf("redis.min_idle_conns must be non-negative")
  2842. }
  2843. if c.Redis.MinIdleConns > c.Redis.PoolSize {
  2844. return fmt.Errorf("redis.min_idle_conns cannot exceed redis.pool_size")
  2845. }
  2846. if c.BatchImage.QueueEnabled {
  2847. if strings.TrimSpace(c.BatchImage.QueueReadyKey) == "" {
  2848. return fmt.Errorf("batch_image.queue_ready_key must not be empty")
  2849. }
  2850. if strings.TrimSpace(c.BatchImage.QueueDelayedKey) == "" {
  2851. return fmt.Errorf("batch_image.queue_delayed_key must not be empty")
  2852. }
  2853. if strings.TrimSpace(c.BatchImage.QueueActiveKey) == "" {
  2854. return fmt.Errorf("batch_image.queue_active_key must not be empty")
  2855. }
  2856. if strings.TrimSpace(c.BatchImage.InflightKeyPrefix) == "" {
  2857. return fmt.Errorf("batch_image.inflight_key_prefix must not be empty")
  2858. }
  2859. if strings.TrimSpace(c.BatchImage.LockKeyPrefix) == "" {
  2860. return fmt.Errorf("batch_image.lock_key_prefix must not be empty")
  2861. }
  2862. if c.BatchImage.InflightTTLSeconds <= 0 {
  2863. return fmt.Errorf("batch_image.inflight_ttl_seconds must be positive")
  2864. }
  2865. if c.BatchImage.JobLockTTLSeconds <= 0 {
  2866. return fmt.Errorf("batch_image.job_lock_ttl_seconds must be positive")
  2867. }
  2868. if c.BatchImage.StaleActiveAfterSeconds <= 0 {
  2869. return fmt.Errorf("batch_image.stale_active_after_seconds must be positive")
  2870. }
  2871. if c.BatchImage.DelayedMoveLimit <= 0 {
  2872. return fmt.Errorf("batch_image.delayed_move_limit must be positive")
  2873. }
  2874. if c.BatchImage.RecoverLimit <= 0 {
  2875. return fmt.Errorf("batch_image.recover_limit must be positive")
  2876. }
  2877. }
  2878. if c.BatchImage.VertexEnabled {
  2879. if strings.TrimSpace(c.BatchImage.VertexManagedGCSBucket) == "" {
  2880. return fmt.Errorf("batch_image.vertex_managed_gcs_bucket must not be empty when vertex is enabled")
  2881. }
  2882. if strings.Contains(c.BatchImage.VertexManagedGCSBucket, "://") {
  2883. return fmt.Errorf("batch_image.vertex_managed_gcs_bucket must be a bucket name, not a URI")
  2884. }
  2885. if strings.TrimSpace(c.BatchImage.VertexLocation) == "" {
  2886. return fmt.Errorf("batch_image.vertex_location must not be empty when vertex is enabled")
  2887. }
  2888. if strings.TrimSpace(c.BatchImage.VertexManagedGCSPrefix) == "" {
  2889. return fmt.Errorf("batch_image.vertex_managed_gcs_prefix must not be empty when vertex is enabled")
  2890. }
  2891. if !strings.Contains(c.BatchImage.VertexManagedGCSPrefix, "{batch_id}") {
  2892. return fmt.Errorf("batch_image.vertex_managed_gcs_prefix must contain {batch_id}")
  2893. }
  2894. if c.BatchImage.VertexInputRetentionHours <= 0 {
  2895. return fmt.Errorf("batch_image.vertex_input_retention_hours must be positive")
  2896. }
  2897. if c.BatchImage.VertexOutputRetentionHours <= 0 {
  2898. return fmt.Errorf("batch_image.vertex_output_retention_hours must be positive")
  2899. }
  2900. }
  2901. if c.Dashboard.Enabled {
  2902. if c.Dashboard.StatsFreshTTLSeconds <= 0 {
  2903. return fmt.Errorf("dashboard_cache.stats_fresh_ttl_seconds must be positive")
  2904. }
  2905. if c.Dashboard.StatsTTLSeconds <= 0 {
  2906. return fmt.Errorf("dashboard_cache.stats_ttl_seconds must be positive")
  2907. }
  2908. if c.Dashboard.StatsRefreshTimeoutSeconds <= 0 {
  2909. return fmt.Errorf("dashboard_cache.stats_refresh_timeout_seconds must be positive")
  2910. }
  2911. if c.Dashboard.StatsFreshTTLSeconds > c.Dashboard.StatsTTLSeconds {
  2912. return fmt.Errorf("dashboard_cache.stats_fresh_ttl_seconds must be <= dashboard_cache.stats_ttl_seconds")
  2913. }
  2914. } else {
  2915. if c.Dashboard.StatsFreshTTLSeconds < 0 {
  2916. return fmt.Errorf("dashboard_cache.stats_fresh_ttl_seconds must be non-negative")
  2917. }
  2918. if c.Dashboard.StatsTTLSeconds < 0 {
  2919. return fmt.Errorf("dashboard_cache.stats_ttl_seconds must be non-negative")
  2920. }
  2921. if c.Dashboard.StatsRefreshTimeoutSeconds < 0 {
  2922. return fmt.Errorf("dashboard_cache.stats_refresh_timeout_seconds must be non-negative")
  2923. }
  2924. }
  2925. if c.DashboardAgg.Enabled {
  2926. if c.DashboardAgg.IntervalSeconds <= 0 {
  2927. return fmt.Errorf("dashboard_aggregation.interval_seconds must be positive")
  2928. }
  2929. if c.DashboardAgg.LookbackSeconds < 0 {
  2930. return fmt.Errorf("dashboard_aggregation.lookback_seconds must be non-negative")
  2931. }
  2932. if c.DashboardAgg.BackfillMaxDays < 0 {
  2933. return fmt.Errorf("dashboard_aggregation.backfill_max_days must be non-negative")
  2934. }
  2935. if c.DashboardAgg.BackfillEnabled && c.DashboardAgg.BackfillMaxDays == 0 {
  2936. return fmt.Errorf("dashboard_aggregation.backfill_max_days must be positive")
  2937. }
  2938. if c.DashboardAgg.Retention.UsageLogsDays <= 0 {
  2939. return fmt.Errorf("dashboard_aggregation.retention.usage_logs_days must be positive")
  2940. }
  2941. if c.DashboardAgg.Retention.UsageBillingDedupDays <= 0 {
  2942. return fmt.Errorf("dashboard_aggregation.retention.usage_billing_dedup_days must be positive")
  2943. }
  2944. if c.DashboardAgg.Retention.UsageBillingDedupDays < c.DashboardAgg.Retention.UsageLogsDays {
  2945. return fmt.Errorf("dashboard_aggregation.retention.usage_billing_dedup_days must be greater than or equal to usage_logs_days")
  2946. }
  2947. if c.DashboardAgg.Retention.HourlyDays <= 0 {
  2948. return fmt.Errorf("dashboard_aggregation.retention.hourly_days must be positive")
  2949. }
  2950. if c.DashboardAgg.Retention.DailyDays <= 0 {
  2951. return fmt.Errorf("dashboard_aggregation.retention.daily_days must be positive")
  2952. }
  2953. if c.DashboardAgg.RecomputeDays < 0 {
  2954. return fmt.Errorf("dashboard_aggregation.recompute_days must be non-negative")
  2955. }
  2956. } else {
  2957. if c.DashboardAgg.IntervalSeconds < 0 {
  2958. return fmt.Errorf("dashboard_aggregation.interval_seconds must be non-negative")
  2959. }
  2960. if c.DashboardAgg.LookbackSeconds < 0 {
  2961. return fmt.Errorf("dashboard_aggregation.lookback_seconds must be non-negative")
  2962. }
  2963. if c.DashboardAgg.BackfillMaxDays < 0 {
  2964. return fmt.Errorf("dashboard_aggregation.backfill_max_days must be non-negative")
  2965. }
  2966. if c.DashboardAgg.Retention.UsageLogsDays < 0 {
  2967. return fmt.Errorf("dashboard_aggregation.retention.usage_logs_days must be non-negative")
  2968. }
  2969. if c.DashboardAgg.Retention.UsageBillingDedupDays < 0 {
  2970. return fmt.Errorf("dashboard_aggregation.retention.usage_billing_dedup_days must be non-negative")
  2971. }
  2972. if c.DashboardAgg.Retention.UsageBillingDedupDays > 0 &&
  2973. c.DashboardAgg.Retention.UsageLogsDays > 0 &&
  2974. c.DashboardAgg.Retention.UsageBillingDedupDays < c.DashboardAgg.Retention.UsageLogsDays {
  2975. return fmt.Errorf("dashboard_aggregation.retention.usage_billing_dedup_days must be greater than or equal to usage_logs_days")
  2976. }
  2977. if c.DashboardAgg.Retention.HourlyDays < 0 {
  2978. return fmt.Errorf("dashboard_aggregation.retention.hourly_days must be non-negative")
  2979. }
  2980. if c.DashboardAgg.Retention.DailyDays < 0 {
  2981. return fmt.Errorf("dashboard_aggregation.retention.daily_days must be non-negative")
  2982. }
  2983. if c.DashboardAgg.RecomputeDays < 0 {
  2984. return fmt.Errorf("dashboard_aggregation.recompute_days must be non-negative")
  2985. }
  2986. }
  2987. if c.UsageCleanup.Enabled {
  2988. if c.UsageCleanup.MaxRangeDays <= 0 {
  2989. return fmt.Errorf("usage_cleanup.max_range_days must be positive")
  2990. }
  2991. if c.UsageCleanup.BatchSize <= 0 {
  2992. return fmt.Errorf("usage_cleanup.batch_size must be positive")
  2993. }
  2994. if c.UsageCleanup.WorkerIntervalSeconds <= 0 {
  2995. return fmt.Errorf("usage_cleanup.worker_interval_seconds must be positive")
  2996. }
  2997. if c.UsageCleanup.TaskTimeoutSeconds <= 0 {
  2998. return fmt.Errorf("usage_cleanup.task_timeout_seconds must be positive")
  2999. }
  3000. } else {
  3001. if c.UsageCleanup.MaxRangeDays < 0 {
  3002. return fmt.Errorf("usage_cleanup.max_range_days must be non-negative")
  3003. }
  3004. if c.UsageCleanup.BatchSize < 0 {
  3005. return fmt.Errorf("usage_cleanup.batch_size must be non-negative")
  3006. }
  3007. if c.UsageCleanup.WorkerIntervalSeconds < 0 {
  3008. return fmt.Errorf("usage_cleanup.worker_interval_seconds must be non-negative")
  3009. }
  3010. if c.UsageCleanup.TaskTimeoutSeconds < 0 {
  3011. return fmt.Errorf("usage_cleanup.task_timeout_seconds must be non-negative")
  3012. }
  3013. }
  3014. if c.Idempotency.DefaultTTLSeconds <= 0 {
  3015. return fmt.Errorf("idempotency.default_ttl_seconds must be positive")
  3016. }
  3017. if c.Idempotency.SystemOperationTTLSeconds <= 0 {
  3018. return fmt.Errorf("idempotency.system_operation_ttl_seconds must be positive")
  3019. }
  3020. if c.Idempotency.ProcessingTimeoutSeconds <= 0 {
  3021. return fmt.Errorf("idempotency.processing_timeout_seconds must be positive")
  3022. }
  3023. if c.Idempotency.FailedRetryBackoffSeconds <= 0 {
  3024. return fmt.Errorf("idempotency.failed_retry_backoff_seconds must be positive")
  3025. }
  3026. if c.Idempotency.MaxStoredResponseLen <= 0 {
  3027. return fmt.Errorf("idempotency.max_stored_response_len must be positive")
  3028. }
  3029. if c.Idempotency.CleanupIntervalSeconds <= 0 {
  3030. return fmt.Errorf("idempotency.cleanup_interval_seconds must be positive")
  3031. }
  3032. if c.Idempotency.CleanupBatchSize <= 0 {
  3033. return fmt.Errorf("idempotency.cleanup_batch_size must be positive")
  3034. }
  3035. if c.Gateway.MaxBodySize <= 0 {
  3036. return fmt.Errorf("gateway.max_body_size must be positive")
  3037. }
  3038. if c.Gateway.TextMaxBodySize <= 0 || c.Gateway.TextMaxBodySize > c.Gateway.MaxBodySize {
  3039. return fmt.Errorf("gateway.text_max_body_size must be positive and no greater than gateway.max_body_size")
  3040. }
  3041. if c.Gateway.UpstreamResponseReadMaxBytes <= 0 {
  3042. return fmt.Errorf("gateway.upstream_response_read_max_bytes must be positive")
  3043. }
  3044. if c.Gateway.ModelsListReadMaxBytes <= 0 {
  3045. return fmt.Errorf("gateway.models_list_read_max_bytes must be positive")
  3046. }
  3047. if c.Gateway.ProxyProbeResponseReadMaxBytes <= 0 {
  3048. return fmt.Errorf("gateway.proxy_probe_response_read_max_bytes must be positive")
  3049. }
  3050. if c.Gateway.ResponseHeaderTimeout < 0 {
  3051. return fmt.Errorf("gateway.response_header_timeout must be non-negative")
  3052. }
  3053. if c.Gateway.OpenAIResponseHeaderTimeout < 0 {
  3054. return fmt.Errorf("gateway.openai_response_header_timeout must be non-negative")
  3055. }
  3056. if c.Gateway.GrokResponseHeaderTimeout < 0 || c.Gateway.GrokResponseHeaderTimeout > 1800 {
  3057. return fmt.Errorf("gateway.grok_response_header_timeout must be between 0-1800 seconds")
  3058. }
  3059. if c.Gateway.OpenAIFirstOutputTimeoutSeconds < 0 || c.Gateway.OpenAIFirstOutputTimeoutSeconds > 600 ||
  3060. (c.Gateway.OpenAIFirstOutputTimeoutSeconds > 0 && c.Gateway.OpenAIFirstOutputTimeoutSeconds < 30) {
  3061. return fmt.Errorf("gateway.openai_first_output_timeout_seconds must be 0 or between 30-600 seconds")
  3062. }
  3063. if c.Gateway.OpenAIHighEffortFirstOutputTimeoutSeconds < 0 || c.Gateway.OpenAIHighEffortFirstOutputTimeoutSeconds > 1800 ||
  3064. (c.Gateway.OpenAIHighEffortFirstOutputTimeoutSeconds > 0 && c.Gateway.OpenAIHighEffortFirstOutputTimeoutSeconds < 30) {
  3065. return fmt.Errorf("gateway.openai_high_effort_first_output_timeout_seconds must be 0 or between 30-1800 seconds")
  3066. }
  3067. if c.Gateway.Live.MaxSessionDurationSeconds <= 0 {
  3068. c.Gateway.Live.MaxSessionDurationSeconds = 3600
  3069. }
  3070. if strings.TrimSpace(c.Gateway.ConnectionPoolIsolation) != "" {
  3071. switch c.Gateway.ConnectionPoolIsolation {
  3072. case ConnectionPoolIsolationProxy, ConnectionPoolIsolationAccount, ConnectionPoolIsolationAccountProxy:
  3073. default:
  3074. return fmt.Errorf("gateway.connection_pool_isolation must be one of: %s/%s/%s",
  3075. ConnectionPoolIsolationProxy, ConnectionPoolIsolationAccount, ConnectionPoolIsolationAccountProxy)
  3076. }
  3077. }
  3078. if c.Gateway.ImageConcurrency.MaxConcurrentRequests < 0 {
  3079. return fmt.Errorf("gateway.image_concurrency.max_concurrent_requests must be non-negative")
  3080. }
  3081. switch strings.TrimSpace(c.Gateway.ImageConcurrency.OverflowMode) {
  3082. case "", ImageConcurrencyOverflowModeReject, ImageConcurrencyOverflowModeWait:
  3083. default:
  3084. return fmt.Errorf("gateway.image_concurrency.overflow_mode must be one of: %s/%s",
  3085. ImageConcurrencyOverflowModeReject, ImageConcurrencyOverflowModeWait)
  3086. }
  3087. if c.Gateway.ImageConcurrency.WaitTimeoutSeconds < 0 {
  3088. return fmt.Errorf("gateway.image_concurrency.wait_timeout_seconds must be non-negative")
  3089. }
  3090. if c.Gateway.ImageConcurrency.MaxWaitingRequests < 0 {
  3091. return fmt.Errorf("gateway.image_concurrency.max_waiting_requests must be non-negative")
  3092. }
  3093. if c.Gateway.MaxIdleConns <= 0 {
  3094. return fmt.Errorf("gateway.max_idle_conns must be positive")
  3095. }
  3096. if c.Gateway.MaxIdleConnsPerHost <= 0 {
  3097. return fmt.Errorf("gateway.max_idle_conns_per_host must be positive")
  3098. }
  3099. if c.Gateway.MaxConnsPerHost < 0 {
  3100. return fmt.Errorf("gateway.max_conns_per_host must be non-negative")
  3101. }
  3102. if c.Gateway.IdleConnTimeoutSeconds <= 0 {
  3103. return fmt.Errorf("gateway.idle_conn_timeout_seconds must be positive")
  3104. }
  3105. if c.Gateway.IdleConnTimeoutSeconds > 180 {
  3106. slog.Warn("gateway.idle_conn_timeout_seconds is high; consider 60-120 seconds for better connection reuse", "idle_conn_timeout_seconds", c.Gateway.IdleConnTimeoutSeconds)
  3107. }
  3108. if c.Gateway.MaxUpstreamClients <= 0 {
  3109. return fmt.Errorf("gateway.max_upstream_clients must be positive")
  3110. }
  3111. if c.Gateway.ClientIdleTTLSeconds <= 0 {
  3112. return fmt.Errorf("gateway.client_idle_ttl_seconds must be positive")
  3113. }
  3114. if c.Gateway.ConcurrencySlotTTLMinutes <= 0 {
  3115. return fmt.Errorf("gateway.concurrency_slot_ttl_minutes must be positive")
  3116. }
  3117. if c.Gateway.StreamDataIntervalTimeout < 0 {
  3118. return fmt.Errorf("gateway.stream_data_interval_timeout must be non-negative")
  3119. }
  3120. if c.Gateway.StreamDataIntervalTimeout != 0 &&
  3121. (c.Gateway.StreamDataIntervalTimeout < 30 || c.Gateway.StreamDataIntervalTimeout > 300) {
  3122. return fmt.Errorf("gateway.stream_data_interval_timeout must be 0 or between 30-300 seconds")
  3123. }
  3124. if c.Gateway.StreamKeepaliveInterval < 0 {
  3125. return fmt.Errorf("gateway.stream_keepalive_interval must be non-negative")
  3126. }
  3127. if c.Gateway.StreamKeepaliveInterval != 0 &&
  3128. (c.Gateway.StreamKeepaliveInterval < 5 || c.Gateway.StreamKeepaliveInterval > 30) {
  3129. return fmt.Errorf("gateway.stream_keepalive_interval must be 0 or between 5-30 seconds")
  3130. }
  3131. if c.Gateway.ImageStreamDataIntervalTimeout < 0 {
  3132. return fmt.Errorf("gateway.image_stream_data_interval_timeout must be non-negative")
  3133. }
  3134. if c.Gateway.ImageStreamDataIntervalTimeout != 0 &&
  3135. (c.Gateway.ImageStreamDataIntervalTimeout < 60 || c.Gateway.ImageStreamDataIntervalTimeout > 1800) {
  3136. return fmt.Errorf("gateway.image_stream_data_interval_timeout must be 0 or between 60-1800 seconds")
  3137. }
  3138. if c.Gateway.ImageStreamKeepaliveInterval < 0 {
  3139. return fmt.Errorf("gateway.image_stream_keepalive_interval must be non-negative")
  3140. }
  3141. if c.Gateway.ImageStreamKeepaliveInterval != 0 &&
  3142. (c.Gateway.ImageStreamKeepaliveInterval < 5 || c.Gateway.ImageStreamKeepaliveInterval > 60) {
  3143. return fmt.Errorf("gateway.image_stream_keepalive_interval must be 0 or between 5-60 seconds")
  3144. }
  3145. if c.Gateway.ImageNonstreamKeepaliveInterval < 0 {
  3146. return fmt.Errorf("gateway.image_nonstream_keepalive_interval must be non-negative")
  3147. }
  3148. if c.Gateway.ImageNonstreamKeepaliveInterval != 0 &&
  3149. (c.Gateway.ImageNonstreamKeepaliveInterval < 5 || c.Gateway.ImageNonstreamKeepaliveInterval > 60) {
  3150. return fmt.Errorf("gateway.image_nonstream_keepalive_interval must be 0 or between 5-60 seconds")
  3151. }
  3152. // 兼容旧键 sticky_previous_response_ttl_seconds
  3153. if c.Gateway.OpenAIWS.StickyResponseIDTTLSeconds <= 0 && c.Gateway.OpenAIWS.StickyPreviousResponseTTLSeconds > 0 {
  3154. c.Gateway.OpenAIWS.StickyResponseIDTTLSeconds = c.Gateway.OpenAIWS.StickyPreviousResponseTTLSeconds
  3155. }
  3156. if c.Gateway.OpenAIWS.MaxConnsPerAccount <= 0 {
  3157. return fmt.Errorf("gateway.openai_ws.max_conns_per_account must be positive")
  3158. }
  3159. if c.Gateway.OpenAIWS.ClientFirstMessageTimeoutSeconds <= 0 {
  3160. return fmt.Errorf("gateway.openai_ws.client_first_message_timeout_seconds must be positive")
  3161. }
  3162. if c.Gateway.OpenAIWS.IngressInterTurnIdleTimeoutSeconds < 0 {
  3163. return fmt.Errorf("gateway.openai_ws.ingress_inter_turn_idle_timeout_seconds must be non-negative")
  3164. }
  3165. if c.Gateway.OpenAIWS.MaxIngressConnectionsPerAPIKey < 0 {
  3166. return fmt.Errorf("gateway.openai_ws.max_ingress_connections_per_api_key must be non-negative")
  3167. }
  3168. if c.Gateway.OpenAIWS.MinIdlePerAccount < 0 {
  3169. return fmt.Errorf("gateway.openai_ws.min_idle_per_account must be non-negative")
  3170. }
  3171. if c.Gateway.OpenAIWS.MaxIdlePerAccount < 0 {
  3172. return fmt.Errorf("gateway.openai_ws.max_idle_per_account must be non-negative")
  3173. }
  3174. if c.Gateway.OpenAIWS.MinIdlePerAccount > c.Gateway.OpenAIWS.MaxIdlePerAccount {
  3175. return fmt.Errorf("gateway.openai_ws.min_idle_per_account must be <= max_idle_per_account")
  3176. }
  3177. if c.Gateway.OpenAIWS.MaxIdlePerAccount > c.Gateway.OpenAIWS.MaxConnsPerAccount {
  3178. return fmt.Errorf("gateway.openai_ws.max_idle_per_account must be <= max_conns_per_account")
  3179. }
  3180. if c.Gateway.OpenAIWS.OAuthMaxConnsFactor <= 0 {
  3181. return fmt.Errorf("gateway.openai_ws.oauth_max_conns_factor must be positive")
  3182. }
  3183. if c.Gateway.OpenAIWS.APIKeyMaxConnsFactor <= 0 {
  3184. return fmt.Errorf("gateway.openai_ws.apikey_max_conns_factor must be positive")
  3185. }
  3186. if c.Gateway.OpenAIWS.DialTimeoutSeconds <= 0 {
  3187. return fmt.Errorf("gateway.openai_ws.dial_timeout_seconds must be positive")
  3188. }
  3189. if c.Gateway.OpenAIWS.ReadTimeoutSeconds <= 0 {
  3190. return fmt.Errorf("gateway.openai_ws.read_timeout_seconds must be positive")
  3191. }
  3192. if c.Gateway.OpenAIWS.WriteTimeoutSeconds <= 0 {
  3193. return fmt.Errorf("gateway.openai_ws.write_timeout_seconds must be positive")
  3194. }
  3195. if c.Gateway.OpenAIWS.PoolTargetUtilization <= 0 || c.Gateway.OpenAIWS.PoolTargetUtilization > 1 {
  3196. return fmt.Errorf("gateway.openai_ws.pool_target_utilization must be within (0,1]")
  3197. }
  3198. if c.Gateway.OpenAIWS.QueueLimitPerConn <= 0 {
  3199. return fmt.Errorf("gateway.openai_ws.queue_limit_per_conn must be positive")
  3200. }
  3201. if c.Gateway.OpenAIWS.EventFlushBatchSize <= 0 {
  3202. return fmt.Errorf("gateway.openai_ws.event_flush_batch_size must be positive")
  3203. }
  3204. if c.Gateway.OpenAIWS.EventFlushIntervalMS < 0 {
  3205. return fmt.Errorf("gateway.openai_ws.event_flush_interval_ms must be non-negative")
  3206. }
  3207. if c.Gateway.OpenAIWS.PrewarmCooldownMS < 0 {
  3208. return fmt.Errorf("gateway.openai_ws.prewarm_cooldown_ms must be non-negative")
  3209. }
  3210. if c.Gateway.OpenAIWS.ClientReadLimitBytes <= 0 {
  3211. return fmt.Errorf("gateway.openai_ws.client_read_limit_bytes must be positive")
  3212. }
  3213. if c.Gateway.OpenAIWS.HTTPBridgeThresholdBytes < 0 {
  3214. return fmt.Errorf("gateway.openai_ws.http_bridge_threshold_bytes must be non-negative")
  3215. }
  3216. if c.Gateway.OpenAIWS.HTTPBridgeEnabled && c.Gateway.OpenAIWS.HTTPBridgeThresholdBytes == 0 {
  3217. return fmt.Errorf("gateway.openai_ws.http_bridge_threshold_bytes must be positive when http_bridge_enabled is true")
  3218. }
  3219. if c.Gateway.OpenAIWS.FallbackCooldownSeconds < 0 {
  3220. return fmt.Errorf("gateway.openai_ws.fallback_cooldown_seconds must be non-negative")
  3221. }
  3222. if c.Gateway.OpenAIWS.RetryBackoffInitialMS < 0 {
  3223. return fmt.Errorf("gateway.openai_ws.retry_backoff_initial_ms must be non-negative")
  3224. }
  3225. if c.Gateway.OpenAIWS.RetryBackoffMaxMS < 0 {
  3226. return fmt.Errorf("gateway.openai_ws.retry_backoff_max_ms must be non-negative")
  3227. }
  3228. if c.Gateway.OpenAIWS.RetryBackoffInitialMS > 0 && c.Gateway.OpenAIWS.RetryBackoffMaxMS > 0 &&
  3229. c.Gateway.OpenAIWS.RetryBackoffMaxMS < c.Gateway.OpenAIWS.RetryBackoffInitialMS {
  3230. return fmt.Errorf("gateway.openai_ws.retry_backoff_max_ms must be >= retry_backoff_initial_ms")
  3231. }
  3232. if c.Gateway.OpenAIWS.RetryJitterRatio < 0 || c.Gateway.OpenAIWS.RetryJitterRatio > 1 {
  3233. return fmt.Errorf("gateway.openai_ws.retry_jitter_ratio must be within [0,1]")
  3234. }
  3235. if c.Gateway.OpenAIWS.RetryTotalBudgetMS < 0 {
  3236. return fmt.Errorf("gateway.openai_ws.retry_total_budget_ms must be non-negative")
  3237. }
  3238. if mode := strings.ToLower(strings.TrimSpace(c.Gateway.OpenAIWS.IngressModeDefault)); mode != "" {
  3239. switch mode {
  3240. case "off", "ctx_pool", "passthrough", "http_bridge":
  3241. case "shared", "dedicated":
  3242. slog.Warn("gateway.openai_ws.ingress_mode_default is deprecated, treating as ctx_pool; please update to off|ctx_pool|passthrough|http_bridge", "value", mode)
  3243. default:
  3244. return fmt.Errorf("gateway.openai_ws.ingress_mode_default must be one of off|ctx_pool|passthrough|http_bridge")
  3245. }
  3246. }
  3247. if mode := strings.ToLower(strings.TrimSpace(c.Gateway.OpenAIWS.StoreDisabledConnMode)); mode != "" {
  3248. switch mode {
  3249. case "strict", "adaptive", "off":
  3250. default:
  3251. return fmt.Errorf("gateway.openai_ws.store_disabled_conn_mode must be one of strict|adaptive|off")
  3252. }
  3253. }
  3254. if c.Gateway.OpenAIWS.PayloadLogSampleRate < 0 || c.Gateway.OpenAIWS.PayloadLogSampleRate > 1 {
  3255. return fmt.Errorf("gateway.openai_ws.payload_log_sample_rate must be within [0,1]")
  3256. }
  3257. if c.Gateway.OpenAIWS.LBTopK <= 0 {
  3258. return fmt.Errorf("gateway.openai_ws.lb_top_k must be positive")
  3259. }
  3260. if c.Gateway.OpenAIWS.StickySessionTTLSeconds <= 0 {
  3261. return fmt.Errorf("gateway.openai_ws.sticky_session_ttl_seconds must be positive")
  3262. }
  3263. if c.Gateway.OpenAIWS.StickyResponseIDTTLSeconds <= 0 {
  3264. return fmt.Errorf("gateway.openai_ws.sticky_response_id_ttl_seconds must be positive")
  3265. }
  3266. if c.Gateway.OpenAIWS.StickyPreviousResponseTTLSeconds < 0 {
  3267. return fmt.Errorf("gateway.openai_ws.sticky_previous_response_ttl_seconds must be non-negative")
  3268. }
  3269. if c.Gateway.OpenAIHTTP2.FallbackErrorThreshold < 0 {
  3270. return fmt.Errorf("gateway.openai_http2.fallback_error_threshold must be non-negative")
  3271. }
  3272. if c.Gateway.OpenAIHTTP2.FallbackWindowSeconds < 0 {
  3273. return fmt.Errorf("gateway.openai_http2.fallback_window_seconds must be non-negative")
  3274. }
  3275. if c.Gateway.OpenAIHTTP2.FallbackTTLSeconds < 0 {
  3276. return fmt.Errorf("gateway.openai_http2.fallback_ttl_seconds must be non-negative")
  3277. }
  3278. if c.Gateway.OpenAIProxyStreamCircuit.FailureThreshold < 0 {
  3279. return fmt.Errorf("gateway.openai_proxy_stream_circuit.failure_threshold must be non-negative")
  3280. }
  3281. if c.Gateway.OpenAIProxyStreamCircuit.WindowSeconds < 0 {
  3282. return fmt.Errorf("gateway.openai_proxy_stream_circuit.window_seconds must be non-negative")
  3283. }
  3284. if c.Gateway.OpenAIProxyStreamCircuit.TTLSeconds < 0 {
  3285. return fmt.Errorf("gateway.openai_proxy_stream_circuit.ttl_seconds must be non-negative")
  3286. }
  3287. weights := c.Gateway.OpenAIWS.SchedulerScoreWeights
  3288. for _, weight := range []float64{
  3289. weights.Priority, weights.Load, weights.Queue, weights.ErrorRate, weights.TTFT,
  3290. weights.Reset, weights.QuotaHeadroom, weights.UpstreamCost,
  3291. weights.PreviousResponse, weights.SessionSticky,
  3292. } {
  3293. if weight < 0 || math.IsNaN(weight) || math.IsInf(weight, 0) {
  3294. return fmt.Errorf("gateway.openai_ws.scheduler_score_weights.* must be non-negative and finite")
  3295. }
  3296. }
  3297. weightSum := weights.BaseWeightSum()
  3298. if weightSum <= 0 {
  3299. return fmt.Errorf("gateway.openai_ws.scheduler_score_weights must not all be zero")
  3300. }
  3301. if math.IsNaN(weightSum) || math.IsInf(weightSum, 0) {
  3302. return fmt.Errorf("gateway.openai_ws.scheduler_score_weights base-weight sum must be finite")
  3303. }
  3304. if totalWeightSum := weights.TotalWeightSum(); math.IsNaN(totalWeightSum) || math.IsInf(totalWeightSum, 0) {
  3305. return fmt.Errorf("gateway.openai_ws.scheduler_score_weights total-weight sum must be finite")
  3306. }
  3307. if c.Gateway.OpenAIScheduler.StickyEscapeTTFTMs <= 0 {
  3308. return fmt.Errorf("gateway.openai_scheduler.sticky_escape_ttft_ms must be positive")
  3309. }
  3310. if c.Gateway.OpenAIScheduler.StickyEscapeErrorRate < 0 || c.Gateway.OpenAIScheduler.StickyEscapeErrorRate > 1 {
  3311. return fmt.Errorf("gateway.openai_scheduler.sticky_escape_error_rate must be between 0 and 1")
  3312. }
  3313. if c.Gateway.MaxLineSize < 0 {
  3314. return fmt.Errorf("gateway.max_line_size must be non-negative")
  3315. }
  3316. if c.Gateway.MaxLineSize != 0 && c.Gateway.MaxLineSize < 1024*1024 {
  3317. return fmt.Errorf("gateway.max_line_size must be at least 1MB")
  3318. }
  3319. if c.Gateway.UsageRecord.WorkerCount <= 0 {
  3320. return fmt.Errorf("gateway.usage_record.worker_count must be positive")
  3321. }
  3322. if c.Gateway.UsageRecord.QueueSize <= 0 {
  3323. return fmt.Errorf("gateway.usage_record.queue_size must be positive")
  3324. }
  3325. if c.Gateway.UsageRecord.TaskTimeoutSeconds <= 0 {
  3326. return fmt.Errorf("gateway.usage_record.task_timeout_seconds must be positive")
  3327. }
  3328. switch strings.ToLower(strings.TrimSpace(c.Gateway.UsageRecord.OverflowPolicy)) {
  3329. case UsageRecordOverflowPolicyDrop, UsageRecordOverflowPolicySample, UsageRecordOverflowPolicySync:
  3330. default:
  3331. return fmt.Errorf("gateway.usage_record.overflow_policy must be one of: %s/%s/%s",
  3332. UsageRecordOverflowPolicyDrop, UsageRecordOverflowPolicySample, UsageRecordOverflowPolicySync)
  3333. }
  3334. if c.Gateway.UsageRecord.OverflowSamplePercent < 0 || c.Gateway.UsageRecord.OverflowSamplePercent > 100 {
  3335. return fmt.Errorf("gateway.usage_record.overflow_sample_percent must be between 0-100")
  3336. }
  3337. if strings.EqualFold(strings.TrimSpace(c.Gateway.UsageRecord.OverflowPolicy), UsageRecordOverflowPolicySample) &&
  3338. c.Gateway.UsageRecord.OverflowSamplePercent <= 0 {
  3339. return fmt.Errorf("gateway.usage_record.overflow_sample_percent must be positive when overflow_policy=sample")
  3340. }
  3341. if c.Gateway.UsageRecord.AutoScaleEnabled {
  3342. if c.Gateway.UsageRecord.AutoScaleMinWorkers <= 0 {
  3343. return fmt.Errorf("gateway.usage_record.auto_scale_min_workers must be positive")
  3344. }
  3345. if c.Gateway.UsageRecord.AutoScaleMaxWorkers <= 0 {
  3346. return fmt.Errorf("gateway.usage_record.auto_scale_max_workers must be positive")
  3347. }
  3348. if c.Gateway.UsageRecord.AutoScaleMaxWorkers < c.Gateway.UsageRecord.AutoScaleMinWorkers {
  3349. return fmt.Errorf("gateway.usage_record.auto_scale_max_workers must be >= auto_scale_min_workers")
  3350. }
  3351. if c.Gateway.UsageRecord.WorkerCount < c.Gateway.UsageRecord.AutoScaleMinWorkers ||
  3352. c.Gateway.UsageRecord.WorkerCount > c.Gateway.UsageRecord.AutoScaleMaxWorkers {
  3353. return fmt.Errorf("gateway.usage_record.worker_count must be between auto_scale_min_workers and auto_scale_max_workers")
  3354. }
  3355. if c.Gateway.UsageRecord.AutoScaleUpQueuePercent <= 0 || c.Gateway.UsageRecord.AutoScaleUpQueuePercent > 100 {
  3356. return fmt.Errorf("gateway.usage_record.auto_scale_up_queue_percent must be between 1-100")
  3357. }
  3358. if c.Gateway.UsageRecord.AutoScaleDownQueuePercent < 0 || c.Gateway.UsageRecord.AutoScaleDownQueuePercent >= 100 {
  3359. return fmt.Errorf("gateway.usage_record.auto_scale_down_queue_percent must be between 0-99")
  3360. }
  3361. if c.Gateway.UsageRecord.AutoScaleDownQueuePercent >= c.Gateway.UsageRecord.AutoScaleUpQueuePercent {
  3362. return fmt.Errorf("gateway.usage_record.auto_scale_down_queue_percent must be less than auto_scale_up_queue_percent")
  3363. }
  3364. if c.Gateway.UsageRecord.AutoScaleUpStep <= 0 {
  3365. return fmt.Errorf("gateway.usage_record.auto_scale_up_step must be positive")
  3366. }
  3367. if c.Gateway.UsageRecord.AutoScaleDownStep <= 0 {
  3368. return fmt.Errorf("gateway.usage_record.auto_scale_down_step must be positive")
  3369. }
  3370. if c.Gateway.UsageRecord.AutoScaleCheckIntervalSeconds <= 0 {
  3371. return fmt.Errorf("gateway.usage_record.auto_scale_check_interval_seconds must be positive")
  3372. }
  3373. if c.Gateway.UsageRecord.AutoScaleCooldownSeconds < 0 {
  3374. return fmt.Errorf("gateway.usage_record.auto_scale_cooldown_seconds must be non-negative")
  3375. }
  3376. }
  3377. if c.Gateway.UserGroupRateCacheTTLSeconds <= 0 {
  3378. return fmt.Errorf("gateway.user_group_rate_cache_ttl_seconds must be positive")
  3379. }
  3380. if c.Gateway.ModelsListCacheTTLSeconds < 10 || c.Gateway.ModelsListCacheTTLSeconds > 30 {
  3381. return fmt.Errorf("gateway.models_list_cache_ttl_seconds must be between 10-30")
  3382. }
  3383. if c.Gateway.Scheduling.StickySessionMaxWaiting <= 0 {
  3384. return fmt.Errorf("gateway.scheduling.sticky_session_max_waiting must be positive")
  3385. }
  3386. if c.Gateway.Scheduling.StickySessionWaitTimeout <= 0 {
  3387. return fmt.Errorf("gateway.scheduling.sticky_session_wait_timeout must be positive")
  3388. }
  3389. if c.Gateway.Scheduling.FallbackWaitTimeout <= 0 {
  3390. return fmt.Errorf("gateway.scheduling.fallback_wait_timeout must be positive")
  3391. }
  3392. if c.Gateway.Scheduling.FallbackMaxWaiting <= 0 {
  3393. return fmt.Errorf("gateway.scheduling.fallback_max_waiting must be positive")
  3394. }
  3395. if c.Gateway.Scheduling.LoadBatchCacheTTLMS < 0 {
  3396. return fmt.Errorf("gateway.scheduling.load_batch_cache_ttl_ms must be non-negative")
  3397. }
  3398. if c.Gateway.Scheduling.SnapshotMGetChunkSize <= 0 {
  3399. return fmt.Errorf("gateway.scheduling.snapshot_mget_chunk_size must be positive")
  3400. }
  3401. if c.Gateway.Scheduling.SnapshotWriteChunkSize <= 0 {
  3402. return fmt.Errorf("gateway.scheduling.snapshot_write_chunk_size must be positive")
  3403. }
  3404. if c.Gateway.Scheduling.SlotCleanupInterval < 0 {
  3405. return fmt.Errorf("gateway.scheduling.slot_cleanup_interval must be non-negative")
  3406. }
  3407. if c.Gateway.Scheduling.DbFallbackTimeoutSeconds < 0 {
  3408. return fmt.Errorf("gateway.scheduling.db_fallback_timeout_seconds must be non-negative")
  3409. }
  3410. if c.Gateway.Scheduling.DbFallbackMaxQPS < 0 {
  3411. return fmt.Errorf("gateway.scheduling.db_fallback_max_qps must be non-negative")
  3412. }
  3413. if c.Gateway.Scheduling.OutboxPollIntervalSeconds <= 0 {
  3414. return fmt.Errorf("gateway.scheduling.outbox_poll_interval_seconds must be positive")
  3415. }
  3416. if c.Gateway.Scheduling.OutboxLagWarnSeconds < 0 {
  3417. return fmt.Errorf("gateway.scheduling.outbox_lag_warn_seconds must be non-negative")
  3418. }
  3419. if c.Gateway.Scheduling.OutboxLagRebuildSeconds < 0 {
  3420. return fmt.Errorf("gateway.scheduling.outbox_lag_rebuild_seconds must be non-negative")
  3421. }
  3422. if c.Gateway.Scheduling.OutboxLagRebuildFailures <= 0 {
  3423. return fmt.Errorf("gateway.scheduling.outbox_lag_rebuild_failures must be positive")
  3424. }
  3425. if c.Gateway.Scheduling.OutboxBacklogRebuildRows < 0 {
  3426. return fmt.Errorf("gateway.scheduling.outbox_backlog_rebuild_rows must be non-negative")
  3427. }
  3428. if c.Gateway.Scheduling.FullRebuildIntervalSeconds < 0 {
  3429. return fmt.Errorf("gateway.scheduling.full_rebuild_interval_seconds must be non-negative")
  3430. }
  3431. if c.Gateway.Scheduling.OutboxLagWarnSeconds > 0 &&
  3432. c.Gateway.Scheduling.OutboxLagRebuildSeconds > 0 &&
  3433. c.Gateway.Scheduling.OutboxLagRebuildSeconds < c.Gateway.Scheduling.OutboxLagWarnSeconds {
  3434. return fmt.Errorf("gateway.scheduling.outbox_lag_rebuild_seconds must be >= outbox_lag_warn_seconds")
  3435. }
  3436. if c.Ops.MetricsCollectorCache.TTL < 0 {
  3437. return fmt.Errorf("ops.metrics_collector_cache.ttl must be non-negative")
  3438. }
  3439. if c.Ops.Cleanup.ErrorLogRetentionDays < 0 {
  3440. return fmt.Errorf("ops.cleanup.error_log_retention_days must be non-negative")
  3441. }
  3442. if c.Ops.Cleanup.Enabled && c.Ops.Cleanup.SystemLogRetentionDays <= 0 {
  3443. return fmt.Errorf("ops.cleanup.system_log_retention_days must be positive when ops cleanup is enabled")
  3444. }
  3445. if c.Ops.Cleanup.MinuteMetricsRetentionDays < 0 {
  3446. return fmt.Errorf("ops.cleanup.minute_metrics_retention_days must be non-negative")
  3447. }
  3448. if c.Ops.Cleanup.HourlyMetricsRetentionDays < 0 {
  3449. return fmt.Errorf("ops.cleanup.hourly_metrics_retention_days must be non-negative")
  3450. }
  3451. if c.Ops.Cleanup.Enabled && strings.TrimSpace(c.Ops.Cleanup.Schedule) == "" {
  3452. return fmt.Errorf("ops.cleanup.schedule is required when ops.cleanup.enabled=true")
  3453. }
  3454. if c.Concurrency.PingInterval < 5 || c.Concurrency.PingInterval > 30 {
  3455. return fmt.Errorf("concurrency.ping_interval must be between 5-30 seconds")
  3456. }
  3457. if c.Gateway.Grok.FreeQuotaSoftGateEnabled {
  3458. if c.Gateway.Grok.FreeQuotaTokenLimit <= 0 {
  3459. return fmt.Errorf("gateway.grok.free_quota_token_limit must be positive")
  3460. }
  3461. if c.Gateway.Grok.FreeQuotaSoftGatePercent < 1 || c.Gateway.Grok.FreeQuotaSoftGatePercent > 100 {
  3462. return fmt.Errorf("gateway.grok.free_quota_soft_gate_percent must be between 1 and 100")
  3463. }
  3464. if c.Gateway.Grok.FreeQuotaWindowHours <= 0 {
  3465. return fmt.Errorf("gateway.grok.free_quota_window_hours must be positive")
  3466. }
  3467. }
  3468. if c.Gateway.Grok.FreeQuotaStatsCacheSeconds < 0 {
  3469. return fmt.Errorf("gateway.grok.free_quota_stats_cache_seconds must be non-negative")
  3470. }
  3471. if err := ValidateDingTalkConfig(c.DingTalk); err != nil {
  3472. return fmt.Errorf("dingtalk_connect: %w", err)
  3473. }
  3474. return nil
  3475. }
  3476. func normalizeStringSlice(values []string) []string {
  3477. if len(values) == 0 {
  3478. return values
  3479. }
  3480. normalized := make([]string, 0, len(values))
  3481. for _, v := range values {
  3482. trimmed := strings.TrimSpace(v)
  3483. if trimmed == "" {
  3484. continue
  3485. }
  3486. normalized = append(normalized, trimmed)
  3487. }
  3488. return normalized
  3489. }
  3490. func isWeakJWTSecret(secret string) bool {
  3491. lower := strings.ToLower(strings.TrimSpace(secret))
  3492. if lower == "" {
  3493. return true
  3494. }
  3495. weak := map[string]struct{}{
  3496. "change-me-in-production": {},
  3497. "changeme": {},
  3498. "secret": {},
  3499. "password": {},
  3500. "123456": {},
  3501. "12345678": {},
  3502. "admin": {},
  3503. "jwt-secret": {},
  3504. }
  3505. _, exists := weak[lower]
  3506. return exists
  3507. }
  3508. func generateJWTSecret(byteLength int) (string, error) {
  3509. if byteLength <= 0 {
  3510. byteLength = 32
  3511. }
  3512. buf := make([]byte, byteLength)
  3513. if _, err := rand.Read(buf); err != nil {
  3514. return "", err
  3515. }
  3516. return hex.EncodeToString(buf), nil
  3517. }
  3518. // GetServerAddress returns the server address (host:port) from config file or environment variable.
  3519. // This is a lightweight function that can be used before full config validation,
  3520. // such as during setup wizard startup.
  3521. // Priority: environment variables > config file > defaults.
  3522. func GetServerAddress() string {
  3523. v := viper.New()
  3524. v.SetConfigName("config")
  3525. v.SetConfigType("yaml")
  3526. configureConfigSource(v.SetConfigFile, v.AddConfigPath)
  3527. // Support SERVER_HOST and SERVER_PORT environment variables
  3528. v.AutomaticEnv()
  3529. v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
  3530. v.SetDefault("server.host", "0.0.0.0")
  3531. v.SetDefault("server.port", 8080)
  3532. // Try to read config file (ignore errors if not found)
  3533. _ = v.ReadInConfig()
  3534. host := v.GetString("server.host")
  3535. port := v.GetInt("server.port")
  3536. return fmt.Sprintf("%s:%d", host, port)
  3537. }
  3538. // ValidateAbsoluteHTTPURL 验证是否为有效的绝对 HTTP(S) URL
  3539. func ValidateAbsoluteHTTPURL(raw string) error {
  3540. raw = strings.TrimSpace(raw)
  3541. if raw == "" {
  3542. return fmt.Errorf("empty url")
  3543. }
  3544. u, err := url.Parse(raw)
  3545. if err != nil {
  3546. return err
  3547. }
  3548. if !u.IsAbs() {
  3549. return fmt.Errorf("must be absolute")
  3550. }
  3551. if !isHTTPScheme(u.Scheme) {
  3552. return fmt.Errorf("unsupported scheme: %s", u.Scheme)
  3553. }
  3554. if strings.TrimSpace(u.Host) == "" {
  3555. return fmt.Errorf("missing host")
  3556. }
  3557. if u.Fragment != "" {
  3558. return fmt.Errorf("must not include fragment")
  3559. }
  3560. return nil
  3561. }
  3562. // ValidateFrontendRedirectURL 验证前端重定向 URL(可以是绝对 URL 或相对路径)
  3563. func ValidateFrontendRedirectURL(raw string) error {
  3564. raw = strings.TrimSpace(raw)
  3565. if raw == "" {
  3566. return fmt.Errorf("empty url")
  3567. }
  3568. if strings.ContainsAny(raw, "\r\n") {
  3569. return fmt.Errorf("contains invalid characters")
  3570. }
  3571. if strings.HasPrefix(raw, "/") {
  3572. if strings.HasPrefix(raw, "//") {
  3573. return fmt.Errorf("must not start with //")
  3574. }
  3575. return nil
  3576. }
  3577. u, err := url.Parse(raw)
  3578. if err != nil {
  3579. return err
  3580. }
  3581. if !u.IsAbs() {
  3582. return fmt.Errorf("must be absolute http(s) url or relative path")
  3583. }
  3584. if !isHTTPScheme(u.Scheme) {
  3585. return fmt.Errorf("unsupported scheme: %s", u.Scheme)
  3586. }
  3587. if strings.TrimSpace(u.Host) == "" {
  3588. return fmt.Errorf("missing host")
  3589. }
  3590. if u.Fragment != "" {
  3591. return fmt.Errorf("must not include fragment")
  3592. }
  3593. return nil
  3594. }
  3595. func scopeContainsOpenID(scopes string) bool {
  3596. for _, scope := range strings.Fields(strings.ToLower(strings.TrimSpace(scopes))) {
  3597. if scope == "openid" {
  3598. return true
  3599. }
  3600. }
  3601. return false
  3602. }
  3603. // isHTTPScheme 检查是否为 HTTP 或 HTTPS 协议
  3604. func isHTTPScheme(scheme string) bool {
  3605. return strings.EqualFold(scheme, "http") || strings.EqualFold(scheme, "https")
  3606. }
  3607. func warnIfInsecureURL(field, raw string) {
  3608. u, err := url.Parse(strings.TrimSpace(raw))
  3609. if err != nil {
  3610. return
  3611. }
  3612. if strings.EqualFold(u.Scheme, "http") {
  3613. slog.Warn("url uses http scheme; use https in production to avoid token leakage", "field", field)
  3614. }
  3615. }