account_handler.go 104 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311
  1. // Package admin provides HTTP handlers for administrative operations.
  2. package admin
  3. import (
  4. "context"
  5. "crypto/sha256"
  6. "encoding/hex"
  7. "encoding/json"
  8. "errors"
  9. "fmt"
  10. "log"
  11. "log/slog"
  12. "net/http"
  13. "sort"
  14. "strconv"
  15. "strings"
  16. "sync"
  17. "time"
  18. "github.com/Wei-Shaw/sub2api/internal/config"
  19. "github.com/Wei-Shaw/sub2api/internal/domain"
  20. "github.com/Wei-Shaw/sub2api/internal/handler/dto"
  21. "github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
  22. "github.com/Wei-Shaw/sub2api/internal/pkg/claude"
  23. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  24. "github.com/Wei-Shaw/sub2api/internal/pkg/geminicli"
  25. "github.com/Wei-Shaw/sub2api/internal/pkg/openai"
  26. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  27. "github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
  28. "github.com/Wei-Shaw/sub2api/internal/pkg/xai"
  29. "github.com/Wei-Shaw/sub2api/internal/service"
  30. "github.com/gin-gonic/gin"
  31. "golang.org/x/sync/errgroup"
  32. )
  33. // OAuthHandler handles OAuth-related operations for accounts
  34. type OAuthHandler struct {
  35. oauthService *service.OAuthService
  36. }
  37. // NewOAuthHandler creates a new OAuth handler
  38. func NewOAuthHandler(oauthService *service.OAuthService) *OAuthHandler {
  39. return &OAuthHandler{
  40. oauthService: oauthService,
  41. }
  42. }
  43. // AccountHandler handles admin account management
  44. type AccountHandler struct {
  45. adminService service.AdminService
  46. oauthService *service.OAuthService
  47. openaiOAuthService *service.OpenAIOAuthService
  48. geminiOAuthService *service.GeminiOAuthService
  49. antigravityOAuthService *service.AntigravityOAuthService
  50. grokOAuthService service.GrokOAuthTokenService
  51. rateLimitService *service.RateLimitService
  52. accountUsageService *service.AccountUsageService
  53. accountTestService *service.AccountTestService
  54. concurrencyService *service.ConcurrencyService
  55. crsSyncService *service.CRSSyncService
  56. sessionLimitCache service.SessionLimitCache
  57. rpmCache service.RPMCache
  58. tokenCacheInvalidator service.TokenCacheInvalidator
  59. grokImportProber grokImportProber
  60. upstreamBillingProbe *service.UpstreamBillingProbeService
  61. ollamaCloudUsage *service.OllamaCloudUsageService
  62. accountTraffic *AccountTrafficHandler
  63. cfg *config.Config
  64. }
  65. // SetUpstreamBillingProbeService attaches the optional remote billing probe service.
  66. func (h *AccountHandler) SetUpstreamBillingProbeService(probe *service.UpstreamBillingProbeService) {
  67. h.upstreamBillingProbe = probe
  68. }
  69. func (h *AccountHandler) SetOllamaCloudUsageService(usage *service.OllamaCloudUsageService) {
  70. h.ollamaCloudUsage = usage
  71. }
  72. func (h *AccountHandler) SetAccountTrafficHandler(traffic *AccountTrafficHandler) {
  73. h.accountTraffic = traffic
  74. }
  75. // NewAccountHandler creates a new admin account handler
  76. func NewAccountHandler(
  77. adminService service.AdminService,
  78. oauthService *service.OAuthService,
  79. openaiOAuthService *service.OpenAIOAuthService,
  80. geminiOAuthService *service.GeminiOAuthService,
  81. antigravityOAuthService *service.AntigravityOAuthService,
  82. grokOAuthService service.GrokOAuthTokenService,
  83. rateLimitService *service.RateLimitService,
  84. accountUsageService *service.AccountUsageService,
  85. accountTestService *service.AccountTestService,
  86. concurrencyService *service.ConcurrencyService,
  87. crsSyncService *service.CRSSyncService,
  88. sessionLimitCache service.SessionLimitCache,
  89. rpmCache service.RPMCache,
  90. tokenCacheInvalidator service.TokenCacheInvalidator,
  91. ) *AccountHandler {
  92. return &AccountHandler{
  93. adminService: adminService,
  94. oauthService: oauthService,
  95. openaiOAuthService: openaiOAuthService,
  96. geminiOAuthService: geminiOAuthService,
  97. antigravityOAuthService: antigravityOAuthService,
  98. grokOAuthService: grokOAuthService,
  99. rateLimitService: rateLimitService,
  100. accountUsageService: accountUsageService,
  101. accountTestService: accountTestService,
  102. concurrencyService: concurrencyService,
  103. crsSyncService: crsSyncService,
  104. sessionLimitCache: sessionLimitCache,
  105. rpmCache: rpmCache,
  106. tokenCacheInvalidator: tokenCacheInvalidator,
  107. }
  108. }
  109. // CreateAccountRequest represents create account request
  110. type CreateAccountRequest struct {
  111. Name string `json:"name" binding:"required"`
  112. Notes *string `json:"notes"`
  113. Platform string `json:"platform" binding:"required"`
  114. Type string `json:"type" binding:"required,oneof=oauth setup-token apikey upstream bedrock service_account"`
  115. Credentials map[string]any `json:"credentials" binding:"required"`
  116. Extra map[string]any `json:"extra"`
  117. ProxyID *int64 `json:"proxy_id"`
  118. Concurrency int `json:"concurrency"`
  119. Priority int `json:"priority"`
  120. RateMultiplier *float64 `json:"rate_multiplier"`
  121. LoadFactor *int `json:"load_factor"`
  122. GroupIDs []int64 `json:"group_ids"`
  123. ExpiresAt *int64 `json:"expires_at"`
  124. AutoPauseOnExpired *bool `json:"auto_pause_on_expired"`
  125. ProbeEnabled *bool `json:"upstream_billing_probe_enabled"`
  126. ConfirmMixedChannelRisk *bool `json:"confirm_mixed_channel_risk"` // 用户确认混合渠道风险
  127. }
  128. // UpdateAccountRequest represents update account request
  129. // 使用指针类型来区分"未提供"和"设置为0"
  130. type UpdateAccountRequest struct {
  131. Name string `json:"name"`
  132. Notes *string `json:"notes"`
  133. Type string `json:"type" binding:"omitempty,oneof=oauth setup-token apikey upstream bedrock service_account"`
  134. Credentials map[string]any `json:"credentials"`
  135. Extra map[string]any `json:"extra"`
  136. ProxyID *int64 `json:"proxy_id"`
  137. Concurrency *int `json:"concurrency"`
  138. Priority *int `json:"priority"`
  139. RateMultiplier *float64 `json:"rate_multiplier"`
  140. LoadFactor *int `json:"load_factor"`
  141. Status string `json:"status" binding:"omitempty,oneof=active inactive error"`
  142. GroupIDs *[]int64 `json:"group_ids"`
  143. ExpiresAt *int64 `json:"expires_at"`
  144. AutoPauseOnExpired *bool `json:"auto_pause_on_expired"`
  145. ProbeEnabled *bool `json:"upstream_billing_probe_enabled"`
  146. RateSyncEnabled *bool `json:"upstream_billing_rate_sync_enabled"`
  147. ConfirmMixedChannelRisk *bool `json:"confirm_mixed_channel_risk"` // 用户确认混合渠道风险
  148. }
  149. // BulkUpdateAccountsRequest represents the payload for bulk editing accounts
  150. type BulkUpdateAccountsRequest struct {
  151. AccountIDs []int64 `json:"account_ids"`
  152. Filters *BulkUpdateAccountFilters `json:"filters"`
  153. Name string `json:"name"`
  154. ProxyID *int64 `json:"proxy_id"`
  155. Concurrency *int `json:"concurrency"`
  156. Priority *int `json:"priority"`
  157. RateMultiplier *float64 `json:"rate_multiplier"`
  158. LoadFactor *int `json:"load_factor"`
  159. Status string `json:"status" binding:"omitempty,oneof=active inactive error"`
  160. Schedulable *bool `json:"schedulable"`
  161. GroupIDs *[]int64 `json:"group_ids"`
  162. Credentials map[string]any `json:"credentials"`
  163. Extra map[string]any `json:"extra"`
  164. ProbeEnabled *bool `json:"upstream_billing_probe_enabled"`
  165. ConfirmMixedChannelRisk *bool `json:"confirm_mixed_channel_risk"` // 用户确认混合渠道风险
  166. }
  167. type BulkUpdateAccountFilters struct {
  168. Platform string `json:"platform"`
  169. Type string `json:"type"`
  170. Status string `json:"status"`
  171. Group string `json:"group"`
  172. Search string `json:"search"`
  173. PrivacyMode string `json:"privacy_mode"`
  174. }
  175. // CheckMixedChannelRequest represents check mixed channel risk request
  176. type CheckMixedChannelRequest struct {
  177. Platform string `json:"platform" binding:"required"`
  178. GroupIDs []int64 `json:"group_ids"`
  179. AccountID *int64 `json:"account_id"`
  180. }
  181. // AccountWithConcurrency extends Account with real-time concurrency info
  182. type AccountWithConcurrency struct {
  183. *dto.Account
  184. simpleMode bool `json:"-"`
  185. CurrentConcurrency int `json:"current_concurrency"`
  186. SchedulerScore *AccountSchedulerScore `json:"scheduler_score,omitempty"`
  187. SchedulerScores []AccountSchedulerGroupScore `json:"scheduler_scores,omitempty"`
  188. // 以下字段仅对 Anthropic OAuth/SetupToken 账号有效,且仅在启用相应功能时返回
  189. CurrentWindowCost *float64 `json:"current_window_cost,omitempty"` // 当前窗口费用
  190. ActiveSessions *int `json:"active_sessions,omitempty"` // 当前活跃会话数
  191. CurrentRPM *int `json:"current_rpm,omitempty"` // 当前分钟 RPM 计数
  192. }
  193. // AccountListItemWithConcurrency is the compact account-list envelope used
  194. // for lite=1. It embeds dto.AccountListItem instead of the full dto.Account,
  195. // so groups/account_groups never appear in the list payload.
  196. type AccountListItemWithConcurrency struct {
  197. *dto.AccountListItem
  198. CurrentConcurrency int `json:"current_concurrency"`
  199. SchedulerScore *AccountSchedulerScore `json:"scheduler_score,omitempty"`
  200. SchedulerScores []AccountSchedulerGroupScore `json:"scheduler_scores,omitempty"`
  201. CurrentWindowCost *float64 `json:"current_window_cost,omitempty"`
  202. ActiveSessions *int `json:"active_sessions,omitempty"`
  203. CurrentRPM *int `json:"current_rpm,omitempty"`
  204. }
  205. type simpleModeGroupReference struct {
  206. ID int64 `json:"id"`
  207. Name string `json:"name"`
  208. Platform string `json:"platform"`
  209. Status string `json:"status"`
  210. }
  211. type simpleModeAccountGroupReference struct {
  212. AccountID int64 `json:"account_id"`
  213. GroupID int64 `json:"group_id"`
  214. Priority int `json:"priority"`
  215. CreatedAt time.Time `json:"created_at"`
  216. Group *simpleModeGroupReference `json:"group,omitempty"`
  217. }
  218. func simpleModeGroupReferenceFromDTO(group *dto.Group) *simpleModeGroupReference {
  219. if group == nil {
  220. return nil
  221. }
  222. return &simpleModeGroupReference{ID: group.ID, Name: group.Name, Platform: group.Platform, Status: group.Status}
  223. }
  224. func simpleModeCompositeGroupIDs(account *dto.Account) map[int64]struct{} {
  225. hidden := make(map[int64]struct{})
  226. if account == nil {
  227. return hidden
  228. }
  229. for _, group := range account.Groups {
  230. if group != nil && group.Platform == service.PlatformComposite {
  231. hidden[group.ID] = struct{}{}
  232. }
  233. }
  234. for _, accountGroup := range account.AccountGroups {
  235. if accountGroup.Group != nil && accountGroup.Group.Platform == service.PlatformComposite {
  236. hidden[accountGroup.GroupID] = struct{}{}
  237. }
  238. }
  239. return hidden
  240. }
  241. func filterSimpleModeGroupIDs(groupIDs []int64, hidden map[int64]struct{}) []int64 {
  242. visible := make([]int64, 0, len(groupIDs))
  243. for _, groupID := range groupIDs {
  244. if _, ok := hidden[groupID]; !ok {
  245. visible = append(visible, groupID)
  246. }
  247. }
  248. return visible
  249. }
  250. func simpleModeCompositeServiceGroupIDs(account *service.Account) map[int64]struct{} {
  251. hidden := make(map[int64]struct{})
  252. if account == nil {
  253. return hidden
  254. }
  255. for _, group := range account.Groups {
  256. if group != nil && group.Platform == service.PlatformComposite {
  257. hidden[group.ID] = struct{}{}
  258. }
  259. }
  260. for _, accountGroup := range account.AccountGroups {
  261. if accountGroup.Group != nil && accountGroup.Group.Platform == service.PlatformComposite {
  262. hidden[accountGroup.GroupID] = struct{}{}
  263. }
  264. }
  265. return hidden
  266. }
  267. func (a AccountWithConcurrency) MarshalJSON() ([]byte, error) {
  268. type alias AccountWithConcurrency
  269. if !a.simpleMode || a.Account == nil {
  270. return json.Marshal(alias(a))
  271. }
  272. groups := make([]simpleModeGroupReference, 0, len(a.Groups))
  273. compositeIDs := simpleModeCompositeGroupIDs(a.Account)
  274. for _, group := range a.Groups {
  275. if group != nil && group.Platform == service.PlatformComposite {
  276. continue
  277. }
  278. if ref := simpleModeGroupReferenceFromDTO(group); ref != nil {
  279. groups = append(groups, *ref)
  280. }
  281. }
  282. accountGroups := make([]simpleModeAccountGroupReference, 0, len(a.AccountGroups))
  283. for _, accountGroup := range a.AccountGroups {
  284. if accountGroup.Group != nil && accountGroup.Group.Platform == service.PlatformComposite {
  285. continue
  286. }
  287. if _, hidden := compositeIDs[accountGroup.GroupID]; hidden {
  288. continue
  289. }
  290. accountGroups = append(accountGroups, simpleModeAccountGroupReference{
  291. AccountID: accountGroup.AccountID, GroupID: accountGroup.GroupID, Priority: accountGroup.Priority,
  292. CreatedAt: accountGroup.CreatedAt, Group: simpleModeGroupReferenceFromDTO(accountGroup.Group),
  293. })
  294. }
  295. return json.Marshal(struct {
  296. alias
  297. GroupIDs []int64 `json:"group_ids,omitempty"`
  298. Groups []simpleModeGroupReference `json:"groups"`
  299. AccountGroups []simpleModeAccountGroupReference `json:"account_groups"`
  300. }{alias: alias(a), GroupIDs: filterSimpleModeGroupIDs(a.GroupIDs, compositeIDs), Groups: groups, AccountGroups: accountGroups})
  301. }
  302. type AccountSchedulerScore struct {
  303. BaseScore float64 `json:"base_score"`
  304. StickyScore float64 `json:"sticky_score"`
  305. StickyScoreInfinity bool `json:"sticky_score_infinity"`
  306. StickyWeightedEnabled bool `json:"sticky_weighted_enabled"`
  307. }
  308. type AccountSchedulerGroupScore struct {
  309. GroupID *int64 `json:"group_id"`
  310. GroupName string `json:"group_name,omitempty"`
  311. GroupPriority *int `json:"group_priority,omitempty"`
  312. AccountSchedulerScore
  313. }
  314. const accountListGroupUngroupedQueryValue = "ungrouped"
  315. func (h *AccountHandler) accountResponseFromService(account *service.Account) *dto.Account {
  316. out := dto.AccountFromService(account)
  317. if h != nil && h.ollamaCloudUsage != nil && out != nil {
  318. h.ollamaCloudUsage.EnrichState(out.OllamaCloudUsage)
  319. }
  320. return out
  321. }
  322. func (h *AccountHandler) accountListResponseFromService(account *service.Account) *dto.Account {
  323. out := dto.AccountFromServiceShallow(account)
  324. if out != nil && account != nil {
  325. out.Proxy = dto.ProxyFromService(account.Proxy)
  326. }
  327. if h != nil && h.ollamaCloudUsage != nil && out != nil {
  328. h.ollamaCloudUsage.EnrichState(out.OllamaCloudUsage)
  329. }
  330. return out
  331. }
  332. func (h *AccountHandler) isSimpleMode() bool {
  333. return h != nil && h.cfg != nil && h.cfg.RunMode == config.RunModeSimple
  334. }
  335. func (h *AccountHandler) buildAccountResponseWithRuntime(ctx context.Context, account *service.Account) AccountWithConcurrency {
  336. item := AccountWithConcurrency{
  337. Account: h.accountResponseFromService(account),
  338. simpleMode: h.isSimpleMode(),
  339. CurrentConcurrency: 0,
  340. }
  341. if account == nil {
  342. return item
  343. }
  344. if h.concurrencyService != nil {
  345. if counts, err := h.concurrencyService.GetAccountConcurrencyBatch(ctx, []int64{account.ID}); err == nil {
  346. item.CurrentConcurrency = counts[account.ID]
  347. }
  348. }
  349. if account.IsAnthropicOAuthOrSetupToken() {
  350. if h.accountUsageService != nil && account.GetWindowCostLimit() > 0 {
  351. startTime := account.GetCurrentWindowStartTime()
  352. if stats, err := h.accountUsageService.GetAccountWindowStats(ctx, account.ID, startTime); err == nil && stats != nil {
  353. cost := stats.StandardCost
  354. item.CurrentWindowCost = &cost
  355. }
  356. }
  357. if h.sessionLimitCache != nil && account.GetMaxSessions() > 0 {
  358. idleTimeout := time.Duration(account.GetSessionIdleTimeoutMinutes()) * time.Minute
  359. idleTimeouts := map[int64]time.Duration{account.ID: idleTimeout}
  360. if sessions, err := h.sessionLimitCache.GetActiveSessionCountBatch(ctx, []int64{account.ID}, idleTimeouts); err == nil {
  361. if count, ok := sessions[account.ID]; ok {
  362. item.ActiveSessions = &count
  363. }
  364. }
  365. }
  366. if h.rpmCache != nil && account.GetBaseRPM() > 0 {
  367. if rpm, err := h.rpmCache.GetRPM(ctx, account.ID); err == nil {
  368. item.CurrentRPM = &rpm
  369. }
  370. }
  371. }
  372. h.enrichShadowParents(ctx, []AccountWithConcurrency{item})
  373. return item
  374. }
  375. // scoreOpenAIAccountSchedulerPool 对池内 OpenAI 账号计算调度分数快照。
  376. // loadMap 为共享的账号负载数据(含池内全部账号即可,多余条目无害);传 nil 时自行批查。
  377. func (h *AccountHandler) scoreOpenAIAccountSchedulerPool(ctx context.Context, accounts []service.Account, loadMap map[int64]*service.AccountLoadInfo) map[int64]AccountSchedulerScore {
  378. if len(accounts) == 0 {
  379. return nil
  380. }
  381. openAIAccounts := make([]*service.Account, 0, len(accounts))
  382. for i := range accounts {
  383. account := &accounts[i]
  384. if account.Platform != service.PlatformOpenAI {
  385. continue
  386. }
  387. openAIAccounts = append(openAIAccounts, account)
  388. }
  389. if len(openAIAccounts) == 0 {
  390. return nil
  391. }
  392. if loadMap == nil {
  393. loadMap = h.fetchOpenAIAccountLoadMap(ctx, openAIAccounts)
  394. }
  395. var scores map[int64]service.OpenAIAccountSchedulerScoreSnapshot
  396. if h.rateLimitService != nil {
  397. scores = h.rateLimitService.BuildOpenAIAccountSchedulerScoreSnapshot(ctx, openAIAccounts, loadMap)
  398. } else {
  399. scores = service.BuildOpenAIAccountSchedulerScoreSnapshot(openAIAccounts, loadMap)
  400. }
  401. result := make(map[int64]AccountSchedulerScore, len(scores))
  402. for accountID, score := range scores {
  403. result[accountID] = AccountSchedulerScore{
  404. BaseScore: score.BaseScore,
  405. StickyScore: score.StickyScore,
  406. StickyScoreInfinity: score.StickyScoreInfinity,
  407. StickyWeightedEnabled: score.StickyWeightedEnabled,
  408. }
  409. }
  410. return result
  411. }
  412. // fetchOpenAIAccountLoadMap 一次性批查给定 OpenAI 账号的负载数据;
  413. // 失败时记录日志并返回空表(分数按零负载计算,属可接受降级)。
  414. func (h *AccountHandler) fetchOpenAIAccountLoadMap(ctx context.Context, openAIAccounts []*service.Account) map[int64]*service.AccountLoadInfo {
  415. loadMap := map[int64]*service.AccountLoadInfo{}
  416. if h.concurrencyService == nil || len(openAIAccounts) == 0 {
  417. return loadMap
  418. }
  419. seen := make(map[int64]struct{}, len(openAIAccounts))
  420. loadReq := make([]service.AccountWithConcurrency, 0, len(openAIAccounts))
  421. for _, account := range openAIAccounts {
  422. if account == nil {
  423. continue
  424. }
  425. if _, ok := seen[account.ID]; ok {
  426. continue
  427. }
  428. seen[account.ID] = struct{}{}
  429. loadReq = append(loadReq, service.AccountWithConcurrency{
  430. ID: account.ID,
  431. MaxConcurrency: account.EffectiveLoadFactor(),
  432. })
  433. }
  434. if batchLoad, err := h.concurrencyService.GetAccountsLoadBatch(ctx, loadReq); err != nil {
  435. slog.Warn("openai_scheduler_score_load_batch_failed", "error", err)
  436. } else if batchLoad != nil {
  437. loadMap = batchLoad
  438. }
  439. return loadMap
  440. }
  441. func (h *AccountHandler) buildOpenAIAccountSchedulerScores(
  442. ctx context.Context,
  443. accounts []service.Account,
  444. filterPool []service.Account,
  445. ) (map[int64]*AccountSchedulerScore, map[int64][]AccountSchedulerGroupScore) {
  446. if len(accounts) == 0 {
  447. return nil, nil
  448. }
  449. if len(filterPool) == 0 {
  450. filterPool = accounts
  451. }
  452. pageOpenAIAccountIDs := make(map[int64]struct{})
  453. groupIDs := make(map[int64]struct{})
  454. for i := range accounts {
  455. account := &accounts[i]
  456. if account.Platform != service.PlatformOpenAI {
  457. continue
  458. }
  459. pageOpenAIAccountIDs[account.ID] = struct{}{}
  460. if len(account.AccountGroups) == 0 && len(account.GroupIDs) == 0 {
  461. continue
  462. }
  463. for _, accountGroup := range account.AccountGroups {
  464. if accountGroup.GroupID > 0 {
  465. groupIDs[accountGroup.GroupID] = struct{}{}
  466. }
  467. }
  468. for _, groupID := range account.GroupIDs {
  469. if groupID > 0 {
  470. groupIDs[groupID] = struct{}{}
  471. }
  472. }
  473. }
  474. if len(pageOpenAIAccountIDs) == 0 {
  475. return nil, nil
  476. }
  477. // 先取各分组池,再对"过滤池 ∪ 分组池"的账号并集做一次负载批查,
  478. // 避免每个池各查一次 Redis 的 N+1。
  479. groupIDList := make([]int64, 0, len(groupIDs))
  480. for groupID := range groupIDs {
  481. groupIDList = append(groupIDList, groupID)
  482. }
  483. sort.Slice(groupIDList, func(i, j int) bool { return groupIDList[i] < groupIDList[j] })
  484. groupPools := make(map[int64][]service.Account, len(groupIDList))
  485. if h.adminService != nil {
  486. for _, groupID := range groupIDList {
  487. gid := groupID
  488. pool, err := h.adminService.ListOpenAISchedulableAccountsForSchedulerScore(ctx, &gid)
  489. if err != nil {
  490. slog.Warn("openai_scheduler_group_score_pool_failed", "group_id", gid, "error", err)
  491. continue
  492. }
  493. groupPools[gid] = pool
  494. }
  495. }
  496. loadUnion := make([]*service.Account, 0, len(filterPool))
  497. collectOpenAIAccounts := func(pool []service.Account) {
  498. for i := range pool {
  499. if pool[i].Platform == service.PlatformOpenAI {
  500. loadUnion = append(loadUnion, &pool[i])
  501. }
  502. }
  503. }
  504. collectOpenAIAccounts(filterPool)
  505. for _, pool := range groupPools {
  506. collectOpenAIAccounts(pool)
  507. }
  508. loadMap := h.fetchOpenAIAccountLoadMap(ctx, loadUnion)
  509. baseScores := make(map[int64]*AccountSchedulerScore)
  510. for accountID, score := range h.scoreOpenAIAccountSchedulerPool(ctx, filterPool, loadMap) {
  511. copiedScore := score
  512. baseScores[accountID] = &copiedScore
  513. }
  514. groupScoresByAccount := make(map[int64][]AccountSchedulerGroupScore)
  515. scoreGroupPool := func(groupID *int64, groupNameByID map[int64]string, groupPriorityByAccount map[int64]int, pool []service.Account) {
  516. if len(pool) == 0 {
  517. return
  518. }
  519. scores := h.scoreOpenAIAccountSchedulerPool(ctx, pool, loadMap)
  520. for accountID, schedulerScore := range scores {
  521. if _, ok := pageOpenAIAccountIDs[accountID]; !ok {
  522. continue
  523. }
  524. groupScore := AccountSchedulerGroupScore{
  525. GroupID: groupID,
  526. AccountSchedulerScore: schedulerScore,
  527. }
  528. if groupID != nil {
  529. groupScore.GroupName = groupNameByID[*groupID]
  530. if priority, ok := groupPriorityByAccount[accountID]; ok {
  531. groupScore.GroupPriority = &priority
  532. }
  533. }
  534. groupScoresByAccount[accountID] = append(groupScoresByAccount[accountID], groupScore)
  535. }
  536. }
  537. for _, groupID := range groupIDList {
  538. gid := groupID
  539. pool, ok := groupPools[gid]
  540. if !ok {
  541. continue
  542. }
  543. groupNameByID := make(map[int64]string)
  544. groupPriorityByAccount := make(map[int64]int)
  545. for i := range pool {
  546. account := &pool[i]
  547. for _, accountGroup := range account.AccountGroups {
  548. if accountGroup.GroupID != gid {
  549. continue
  550. }
  551. groupPriorityByAccount[account.ID] = accountGroup.Priority
  552. if accountGroup.Group != nil {
  553. groupNameByID[gid] = accountGroup.Group.Name
  554. }
  555. }
  556. }
  557. scoreGroupPool(&gid, groupNameByID, groupPriorityByAccount, pool)
  558. }
  559. for accountID := range groupScoresByAccount {
  560. sort.SliceStable(groupScoresByAccount[accountID], func(i, j int) bool {
  561. left := groupScoresByAccount[accountID][i]
  562. right := groupScoresByAccount[accountID][j]
  563. return *left.GroupID < *right.GroupID
  564. })
  565. }
  566. return baseScores, groupScoresByAccount
  567. }
  568. func (h *AccountHandler) listAccountSchedulerScoreFilterPool(
  569. ctx context.Context,
  570. platform, accountType, status, search string,
  571. groupID int64,
  572. privacyMode string,
  573. ) []service.Account {
  574. if h.adminService == nil || (platform != "" && platform != service.PlatformOpenAI) {
  575. return nil
  576. }
  577. // 池只用于 OpenAI 分数计算(非 OpenAI 账号会在打分时被丢弃),
  578. // 无论列表页平台过滤为何,查询一律限定 openai,避免无过滤时全表扫描。
  579. accounts, err := h.adminService.ListAccountsForSchedulerScoreFilter(ctx, service.PlatformOpenAI, accountType, status, search, groupID, privacyMode)
  580. if err != nil {
  581. slog.Warn("openai_scheduler_filter_score_pool_failed", "error", err)
  582. return nil
  583. }
  584. return accounts
  585. }
  586. // List handles listing all accounts with pagination
  587. // GET /api/v1/admin/accounts
  588. func (h *AccountHandler) List(c *gin.Context) {
  589. page, pageSize := response.ParsePagination(c)
  590. platform := c.Query("platform")
  591. accountType := c.Query("type")
  592. status := c.Query("status")
  593. search := c.Query("search")
  594. privacyMode := strings.TrimSpace(c.Query("privacy_mode"))
  595. sortBy := c.DefaultQuery("sort_by", "name")
  596. sortOrder := c.DefaultQuery("sort_order", "asc")
  597. // 标准化和验证 search 参数
  598. search = strings.TrimSpace(search)
  599. if len(search) > 100 {
  600. search = search[:100]
  601. }
  602. lite := parseBoolQueryWithDefault(c.Query("lite"), false)
  603. // 调度分需要跨候选池批量打分并读取负载,默认列表不计算;只有前端列可见时才显式开启。
  604. includeSchedulerScore := parseBoolQueryWithDefault(c.Query("include_scheduler_score"), false)
  605. var groupID int64
  606. if groupIDStr := c.Query("group"); groupIDStr != "" {
  607. if groupIDStr == accountListGroupUngroupedQueryValue {
  608. groupID = service.AccountListGroupUngrouped
  609. } else {
  610. parsedGroupID, parseErr := strconv.ParseInt(groupIDStr, 10, 64)
  611. if parseErr != nil {
  612. response.ErrorFrom(c, infraerrors.BadRequest("INVALID_GROUP_FILTER", "invalid group filter"))
  613. return
  614. }
  615. if parsedGroupID < 0 {
  616. response.ErrorFrom(c, infraerrors.BadRequest("INVALID_GROUP_FILTER", "invalid group filter"))
  617. return
  618. }
  619. groupID = parsedGroupID
  620. }
  621. }
  622. accounts, total, err := h.adminService.ListAccounts(c.Request.Context(), page, pageSize, platform, accountType, status, search, groupID, privacyMode, sortBy, sortOrder)
  623. if err != nil {
  624. response.ErrorFrom(c, err)
  625. return
  626. }
  627. if h.ollamaCloudUsage != nil && len(accounts) > 0 {
  628. accountPointers := make([]*service.Account, len(accounts))
  629. for index := range accounts {
  630. accountPointers[index] = &accounts[index]
  631. }
  632. if err := h.ollamaCloudUsage.ResolveAccounts(c.Request.Context(), accountPointers); err != nil {
  633. response.ErrorFrom(c, err)
  634. return
  635. }
  636. }
  637. // Get current concurrency counts for all accounts
  638. accountIDs := make([]int64, len(accounts))
  639. for i, acc := range accounts {
  640. accountIDs[i] = acc.ID
  641. }
  642. concurrencyCounts := make(map[int64]int)
  643. var windowCosts map[int64]float64
  644. var activeSessions map[int64]int
  645. var rpmCounts map[int64]int
  646. // 双重门控:用户要看该列,且当前页确实有 OpenAI 账号,才进入昂贵的候选池打分路径。
  647. var schedulerScores map[int64]*AccountSchedulerScore
  648. var schedulerGroupScores map[int64][]AccountSchedulerGroupScore
  649. pageHasOpenAIAccounts := false
  650. for i := range accounts {
  651. if accounts[i].Platform == service.PlatformOpenAI {
  652. pageHasOpenAIAccounts = true
  653. break
  654. }
  655. }
  656. if includeSchedulerScore && pageHasOpenAIAccounts {
  657. schedulerFilterPool := h.listAccountSchedulerScoreFilterPool(c.Request.Context(), platform, accountType, status, search, groupID, privacyMode)
  658. schedulerScores, schedulerGroupScores = h.buildOpenAIAccountSchedulerScores(c.Request.Context(), accounts, schedulerFilterPool)
  659. }
  660. // 始终获取并发数(Redis ZCARD,极低开销)
  661. if h.concurrencyService != nil {
  662. if cc, ccErr := h.concurrencyService.GetAccountConcurrencyBatch(c.Request.Context(), accountIDs); ccErr == nil && cc != nil {
  663. concurrencyCounts = cc
  664. }
  665. }
  666. // 识别需要查询窗口费用、会话数和 RPM 的账号(Anthropic OAuth/SetupToken 且启用了相应功能)
  667. windowCostAccountIDs := make([]int64, 0)
  668. sessionLimitAccountIDs := make([]int64, 0)
  669. rpmAccountIDs := make([]int64, 0)
  670. sessionIdleTimeouts := make(map[int64]time.Duration) // 各账号的会话空闲超时配置
  671. for i := range accounts {
  672. acc := &accounts[i]
  673. if acc.IsAnthropicOAuthOrSetupToken() {
  674. if acc.GetWindowCostLimit() > 0 {
  675. windowCostAccountIDs = append(windowCostAccountIDs, acc.ID)
  676. }
  677. if acc.GetMaxSessions() > 0 {
  678. sessionLimitAccountIDs = append(sessionLimitAccountIDs, acc.ID)
  679. sessionIdleTimeouts[acc.ID] = time.Duration(acc.GetSessionIdleTimeoutMinutes()) * time.Minute
  680. }
  681. if acc.GetBaseRPM() > 0 {
  682. rpmAccountIDs = append(rpmAccountIDs, acc.ID)
  683. }
  684. }
  685. }
  686. // 始终获取 RPM 计数(Redis GET,极低开销)
  687. if len(rpmAccountIDs) > 0 && h.rpmCache != nil {
  688. rpmCounts, _ = h.rpmCache.GetRPMBatch(c.Request.Context(), rpmAccountIDs)
  689. if rpmCounts == nil {
  690. rpmCounts = make(map[int64]int)
  691. }
  692. }
  693. // 始终获取活跃会话数(Redis ZCARD,低开销)
  694. if len(sessionLimitAccountIDs) > 0 && h.sessionLimitCache != nil {
  695. activeSessions, _ = h.sessionLimitCache.GetActiveSessionCountBatch(c.Request.Context(), sessionLimitAccountIDs, sessionIdleTimeouts)
  696. if activeSessions == nil {
  697. activeSessions = make(map[int64]int)
  698. }
  699. }
  700. // 始终获取窗口费用(PostgreSQL 聚合查询)
  701. if len(windowCostAccountIDs) > 0 {
  702. windowCosts = make(map[int64]float64)
  703. var mu sync.Mutex
  704. g, gctx := errgroup.WithContext(c.Request.Context())
  705. g.SetLimit(10) // 限制并发数
  706. for i := range accounts {
  707. acc := &accounts[i]
  708. if !acc.IsAnthropicOAuthOrSetupToken() || acc.GetWindowCostLimit() <= 0 {
  709. continue
  710. }
  711. accCopy := acc // 闭包捕获
  712. g.Go(func() error {
  713. // 使用统一的窗口开始时间计算逻辑(考虑窗口过期情况)
  714. startTime := accCopy.GetCurrentWindowStartTime()
  715. stats, err := h.accountUsageService.GetAccountWindowStats(gctx, accCopy.ID, startTime)
  716. if err == nil && stats != nil {
  717. mu.Lock()
  718. windowCosts[accCopy.ID] = stats.StandardCost // 使用标准费用
  719. mu.Unlock()
  720. }
  721. return nil // 不返回错误,允许部分失败
  722. })
  723. }
  724. _ = g.Wait()
  725. }
  726. // Build response with concurrency info
  727. result := make([]AccountWithConcurrency, len(accounts))
  728. for i := range accounts {
  729. acc := &accounts[i]
  730. accountResponse := h.accountResponseFromService(acc)
  731. if lite {
  732. accountResponse = h.accountListResponseFromService(acc)
  733. if h.isSimpleMode() {
  734. accountResponse.GroupIDs = filterSimpleModeGroupIDs(accountResponse.GroupIDs, simpleModeCompositeServiceGroupIDs(acc))
  735. }
  736. }
  737. item := AccountWithConcurrency{
  738. Account: accountResponse,
  739. simpleMode: h.isSimpleMode(),
  740. CurrentConcurrency: concurrencyCounts[acc.ID],
  741. SchedulerScore: schedulerScores[acc.ID],
  742. SchedulerScores: schedulerGroupScores[acc.ID],
  743. }
  744. // 添加窗口费用(仅当启用时)
  745. if windowCosts != nil {
  746. if cost, ok := windowCosts[acc.ID]; ok {
  747. item.CurrentWindowCost = &cost
  748. }
  749. }
  750. // 添加活跃会话数(仅当启用时)
  751. if activeSessions != nil {
  752. if count, ok := activeSessions[acc.ID]; ok {
  753. item.ActiveSessions = &count
  754. }
  755. }
  756. // 添加 RPM 计数(仅当启用时)
  757. if rpmCounts != nil {
  758. if rpm, ok := rpmCounts[acc.ID]; ok {
  759. item.CurrentRPM = &rpm
  760. }
  761. }
  762. result[i] = item
  763. }
  764. h.enrichShadowParents(c.Request.Context(), result)
  765. if lite {
  766. compact := make([]AccountListItemWithConcurrency, len(result))
  767. for i := range result {
  768. item := result[i]
  769. compact[i] = AccountListItemWithConcurrency{
  770. AccountListItem: dto.AccountListItemFromAccount(item.Account),
  771. CurrentConcurrency: item.CurrentConcurrency,
  772. SchedulerScore: item.SchedulerScore,
  773. SchedulerScores: item.SchedulerScores,
  774. CurrentWindowCost: item.CurrentWindowCost,
  775. ActiveSessions: item.ActiveSessions,
  776. CurrentRPM: item.CurrentRPM,
  777. }
  778. }
  779. etag := buildAccountsListETag(compact, total, page, pageSize, platform, accountType, status, search, true)
  780. if etag != "" {
  781. c.Header("ETag", etag)
  782. c.Header("Vary", "If-None-Match")
  783. if ifNoneMatchMatched(c.GetHeader("If-None-Match"), etag) {
  784. c.Status(http.StatusNotModified)
  785. return
  786. }
  787. }
  788. response.Paginated(c, compact, total, page, pageSize)
  789. return
  790. }
  791. etag := buildAccountsListETag(result, total, page, pageSize, platform, accountType, status, search, false)
  792. if etag != "" {
  793. c.Header("ETag", etag)
  794. c.Header("Vary", "If-None-Match")
  795. if ifNoneMatchMatched(c.GetHeader("If-None-Match"), etag) {
  796. c.Status(http.StatusNotModified)
  797. return
  798. }
  799. }
  800. response.Paginated(c, result, total, page, pageSize)
  801. }
  802. func buildAccountsListETag[T any](
  803. items []T,
  804. total int64,
  805. page, pageSize int,
  806. platform, accountType, status, search string,
  807. lite bool,
  808. ) string {
  809. payload := struct {
  810. Total int64 `json:"total"`
  811. Page int `json:"page"`
  812. PageSize int `json:"page_size"`
  813. Platform string `json:"platform"`
  814. AccountType string `json:"type"`
  815. Status string `json:"status"`
  816. Search string `json:"search"`
  817. Lite bool `json:"lite"`
  818. Items []T `json:"items"`
  819. }{
  820. Total: total,
  821. Page: page,
  822. PageSize: pageSize,
  823. Platform: platform,
  824. AccountType: accountType,
  825. Status: status,
  826. Search: search,
  827. Lite: lite,
  828. Items: items,
  829. }
  830. raw, err := json.Marshal(payload)
  831. if err != nil {
  832. return ""
  833. }
  834. sum := sha256.Sum256(raw)
  835. return "\"" + hex.EncodeToString(sum[:]) + "\""
  836. }
  837. func ifNoneMatchMatched(ifNoneMatch, etag string) bool {
  838. if etag == "" || ifNoneMatch == "" {
  839. return false
  840. }
  841. for _, token := range strings.Split(ifNoneMatch, ",") {
  842. candidate := strings.TrimSpace(token)
  843. if candidate == "*" {
  844. return true
  845. }
  846. if candidate == etag {
  847. return true
  848. }
  849. if strings.HasPrefix(candidate, "W/") && strings.TrimPrefix(candidate, "W/") == etag {
  850. return true
  851. }
  852. }
  853. return false
  854. }
  855. // GetByID handles getting an account by ID
  856. // GET /api/v1/admin/accounts/:id
  857. func (h *AccountHandler) GetByID(c *gin.Context) {
  858. accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  859. if err != nil {
  860. response.BadRequest(c, "Invalid account ID")
  861. return
  862. }
  863. account, err := h.adminService.GetAccount(c.Request.Context(), accountID)
  864. if err != nil {
  865. response.ErrorFrom(c, err)
  866. return
  867. }
  868. if h.ollamaCloudUsage != nil {
  869. if err := h.ollamaCloudUsage.ResolveAccounts(c.Request.Context(), []*service.Account{account}); err != nil {
  870. response.ErrorFrom(c, err)
  871. return
  872. }
  873. }
  874. response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), account))
  875. }
  876. // CheckMixedChannel handles checking mixed channel risk for account-group binding.
  877. // POST /api/v1/admin/accounts/check-mixed-channel
  878. func (h *AccountHandler) CheckMixedChannel(c *gin.Context) {
  879. var req CheckMixedChannelRequest
  880. if err := c.ShouldBindJSON(&req); err != nil {
  881. response.BadRequest(c, "Invalid request: "+err.Error())
  882. return
  883. }
  884. if len(req.GroupIDs) == 0 {
  885. response.Success(c, gin.H{"has_risk": false})
  886. return
  887. }
  888. accountID := int64(0)
  889. if req.AccountID != nil {
  890. accountID = *req.AccountID
  891. }
  892. err := h.adminService.CheckMixedChannelRisk(c.Request.Context(), accountID, req.Platform, req.GroupIDs)
  893. if err != nil {
  894. var mixedErr *service.MixedChannelError
  895. if errors.As(err, &mixedErr) {
  896. response.Success(c, gin.H{
  897. "has_risk": true,
  898. "error": "mixed_channel_warning",
  899. "message": mixedErr.Error(),
  900. "details": gin.H{
  901. "group_id": mixedErr.GroupID,
  902. "group_name": mixedErr.GroupName,
  903. "current_platform": mixedErr.CurrentPlatform,
  904. "other_platform": mixedErr.OtherPlatform,
  905. },
  906. })
  907. return
  908. }
  909. response.ErrorFrom(c, err)
  910. return
  911. }
  912. response.Success(c, gin.H{"has_risk": false})
  913. }
  914. // Create handles creating a new account
  915. // POST /api/v1/admin/accounts
  916. func (h *AccountHandler) Create(c *gin.Context) {
  917. var req CreateAccountRequest
  918. if err := c.ShouldBindJSON(&req); err != nil {
  919. response.BadRequest(c, "Invalid request: "+err.Error())
  920. return
  921. }
  922. if err := service.ValidateOpenAILongContextBillingExtra(req.Platform, req.Extra); err != nil {
  923. response.ErrorFrom(c, err)
  924. return
  925. }
  926. if req.RateMultiplier != nil && *req.RateMultiplier < 0 {
  927. response.BadRequest(c, "rate_multiplier must be >= 0")
  928. return
  929. }
  930. // base_rpm 输入校验:负值归零,超过 10000 截断
  931. sanitizeExtraBaseRPM(req.Extra)
  932. if err := service.ValidateUpstreamRequestIDHeaderExtra(req.Extra); err != nil {
  933. response.ErrorFrom(c, err)
  934. return
  935. }
  936. // 确定是否跳过混合渠道检查
  937. skipCheck := req.ConfirmMixedChannelRisk != nil && *req.ConfirmMixedChannelRisk
  938. // 捕获闭包内创建的账号引用,用于创建成功后触发异步探测。
  939. // 幂等重放时闭包不会执行 → createdAccount 为 nil → 不重复调度。
  940. var createdAccount *service.Account
  941. result, err := executeAdminIdempotent(c, "admin.accounts.create", req, service.DefaultWriteIdempotencyTTL(), func(ctx context.Context) (any, error) {
  942. account, execErr := h.adminService.CreateAccount(ctx, &service.CreateAccountInput{
  943. Name: req.Name,
  944. Notes: req.Notes,
  945. Platform: req.Platform,
  946. Type: req.Type,
  947. Credentials: req.Credentials,
  948. Extra: req.Extra,
  949. ProxyID: req.ProxyID,
  950. Concurrency: req.Concurrency,
  951. Priority: req.Priority,
  952. RateMultiplier: req.RateMultiplier,
  953. LoadFactor: req.LoadFactor,
  954. GroupIDs: req.GroupIDs,
  955. ExpiresAt: req.ExpiresAt,
  956. AutoPauseOnExpired: req.AutoPauseOnExpired,
  957. ProbeEnabled: req.ProbeEnabled,
  958. SkipMixedChannelCheck: skipCheck,
  959. })
  960. if execErr != nil {
  961. return nil, execErr
  962. }
  963. createdAccount = account
  964. // Antigravity OAuth: 新账号直接设置隐私
  965. h.adminService.ForceAntigravityPrivacy(ctx, account)
  966. // OpenAI OAuth: 新账号直接设置隐私
  967. h.adminService.ForceOpenAIPrivacy(ctx, account)
  968. return h.buildAccountResponseWithRuntime(ctx, account), nil
  969. })
  970. if err != nil {
  971. // 检查是否为混合渠道错误
  972. var mixedErr *service.MixedChannelError
  973. if errors.As(err, &mixedErr) {
  974. // 创建接口仅返回最小必要字段,详细信息由专门检查接口提供
  975. c.JSON(409, gin.H{
  976. "error": "mixed_channel_warning",
  977. "message": mixedErr.Error(),
  978. })
  979. return
  980. }
  981. if retryAfter := service.RetryAfterSecondsFromError(err); retryAfter > 0 {
  982. c.Header("Retry-After", strconv.Itoa(retryAfter))
  983. }
  984. response.ErrorFrom(c, err)
  985. return
  986. }
  987. if result != nil && result.Replayed {
  988. c.Header("X-Idempotency-Replayed", "true")
  989. }
  990. // OpenAI APIKey 账号创建后异步探测上游 /v1/responses 能力。
  991. // 探测失败不影响账号创建响应。
  992. h.scheduleOpenAIResponsesProbe(createdAccount)
  993. h.scheduleGrokImportProbe(createdAccount)
  994. response.Success(c, result.Data)
  995. }
  996. // Duplicate handles creating an independent account from an existing account's configuration.
  997. // POST /api/v1/admin/accounts/:id/duplicate
  998. func (h *AccountHandler) Duplicate(c *gin.Context) {
  999. accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  1000. if err != nil {
  1001. response.BadRequest(c, "Invalid account ID")
  1002. return
  1003. }
  1004. actorScope := adminActorScope(c)
  1005. result, err := executeAdminIdempotent(
  1006. c,
  1007. "admin.accounts.duplicate",
  1008. struct {
  1009. AccountID int64 `json:"account_id"`
  1010. }{AccountID: accountID},
  1011. service.DefaultWriteIdempotencyTTL(),
  1012. func(ctx context.Context) (any, error) {
  1013. account, execErr := h.adminService.DuplicateAccount(ctx, accountID, actorScope, c.GetHeader("Idempotency-Key"))
  1014. if execErr != nil {
  1015. return nil, execErr
  1016. }
  1017. return h.buildAccountResponseWithRuntime(ctx, account), nil
  1018. },
  1019. )
  1020. if err != nil {
  1021. reason := infraerrors.Reason(err)
  1022. if reason == infraerrors.Reason(service.ErrIdempotencyInProgress) || reason == infraerrors.Reason(service.ErrIdempotencyStoreUnavail) {
  1023. recovered, recoverErr := h.adminService.RecoverDuplicateAccount(c.Request.Context(), accountID, actorScope, c.GetHeader("Idempotency-Key"))
  1024. if recoverErr != nil {
  1025. slog.Warn("account_duplicate_recovery_failed", "account_id", accountID, "actor_scope", actorScope, "reason", reason, "error", recoverErr)
  1026. } else if recovered != nil {
  1027. c.Header("X-Idempotency-Recovered", "true")
  1028. response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), recovered))
  1029. return
  1030. }
  1031. }
  1032. response.ErrorFrom(c, err)
  1033. return
  1034. }
  1035. if result != nil && result.Replayed {
  1036. c.Header("X-Idempotency-Replayed", "true")
  1037. }
  1038. response.Success(c, result.Data)
  1039. }
  1040. // Update handles updating an account
  1041. // PUT /api/v1/admin/accounts/:id
  1042. func (h *AccountHandler) Update(c *gin.Context) {
  1043. accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  1044. if err != nil {
  1045. response.BadRequest(c, "Invalid account ID")
  1046. return
  1047. }
  1048. var req UpdateAccountRequest
  1049. if err := c.ShouldBindJSON(&req); err != nil {
  1050. response.BadRequest(c, "Invalid request: "+err.Error())
  1051. return
  1052. }
  1053. if req.RateMultiplier != nil && *req.RateMultiplier < 0 {
  1054. response.BadRequest(c, "rate_multiplier must be >= 0")
  1055. return
  1056. }
  1057. // base_rpm 输入校验:负值归零,超过 10000 截断
  1058. sanitizeExtraBaseRPM(req.Extra)
  1059. if err := service.ValidateUpstreamRequestIDHeaderExtra(req.Extra); err != nil {
  1060. response.ErrorFrom(c, err)
  1061. return
  1062. }
  1063. // 确定是否跳过混合渠道检查
  1064. skipCheck := req.ConfirmMixedChannelRisk != nil && *req.ConfirmMixedChannelRisk
  1065. account, err := h.adminService.UpdateAccount(c.Request.Context(), accountID, &service.UpdateAccountInput{
  1066. Name: req.Name,
  1067. Notes: req.Notes,
  1068. Type: req.Type,
  1069. Credentials: req.Credentials,
  1070. Extra: req.Extra,
  1071. ProxyID: req.ProxyID,
  1072. Concurrency: req.Concurrency, // 指针类型,nil 表示未提供
  1073. Priority: req.Priority, // 指针类型,nil 表示未提供
  1074. RateMultiplier: req.RateMultiplier,
  1075. LoadFactor: req.LoadFactor,
  1076. Status: req.Status,
  1077. GroupIDs: req.GroupIDs,
  1078. ExpiresAt: req.ExpiresAt,
  1079. AutoPauseOnExpired: req.AutoPauseOnExpired,
  1080. ProbeEnabled: req.ProbeEnabled,
  1081. RateSyncEnabled: req.RateSyncEnabled,
  1082. SkipMixedChannelCheck: skipCheck,
  1083. })
  1084. if err != nil {
  1085. // 检查是否为混合渠道错误
  1086. var mixedErr *service.MixedChannelError
  1087. if errors.As(err, &mixedErr) {
  1088. // 更新接口仅返回最小必要字段,详细信息由专门检查接口提供
  1089. c.JSON(409, gin.H{
  1090. "error": "mixed_channel_warning",
  1091. "message": mixedErr.Error(),
  1092. })
  1093. return
  1094. }
  1095. response.ErrorFrom(c, err)
  1096. return
  1097. }
  1098. // OpenAI APIKey: credentials 修改后重新探测上游能力(base_url/api_key 可能变更)。
  1099. if h.accountTraffic != nil && account.Extra[service.AccountTrafficPolicyKey] != nil {
  1100. if err := h.accountTraffic.traffic.Sync(c.Request.Context(), account); err != nil {
  1101. slog.Warn("account_traffic_sync_after_edit_failed", "account_id", account.ID, "error", err)
  1102. c.Header("X-Account-Traffic-State", "unavailable")
  1103. }
  1104. }
  1105. // 异步执行,探测失败不影响账号更新响应。
  1106. if len(req.Credentials) > 0 {
  1107. h.scheduleOpenAIResponsesProbe(account)
  1108. }
  1109. response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), account))
  1110. }
  1111. // scheduleOpenAIResponsesProbe 异步触发 OpenAI APIKey 账号的 Responses API 能力探测。
  1112. //
  1113. // 仅对 platform=openai && type=apikey 账号生效;其他账号无操作。
  1114. // 探测本身在 goroutine 中执行(会发一次 HTTP 请求到上游),不会阻塞
  1115. // 当前请求。探测错误仅记录日志,不向上下文传播:探测失败时标记保持缺失,
  1116. // 网关会按"现状即证据"默认走 Responses。
  1117. func (h *AccountHandler) scheduleOpenAIResponsesProbe(account *service.Account) {
  1118. if account == nil || account.Type != service.AccountTypeAPIKey ||
  1119. (account.Platform != service.PlatformOpenAI && !service.IsCNProvider(account.Platform)) {
  1120. return
  1121. }
  1122. if h.accountTestService == nil {
  1123. return
  1124. }
  1125. accountID := account.ID
  1126. go func() {
  1127. defer func() {
  1128. if r := recover(); r != nil {
  1129. slog.Error("openai_responses_probe_panic", "account_id", accountID, "recover", r)
  1130. }
  1131. }()
  1132. h.accountTestService.ProbeOpenAIAPIKeyResponsesSupport(context.Background(), accountID)
  1133. }()
  1134. }
  1135. // Delete handles deleting an account
  1136. // DELETE /api/v1/admin/accounts/:id
  1137. func (h *AccountHandler) Delete(c *gin.Context) {
  1138. accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  1139. if err != nil {
  1140. response.BadRequest(c, "Invalid account ID")
  1141. return
  1142. }
  1143. err = h.adminService.DeleteAccount(c.Request.Context(), accountID)
  1144. if err != nil {
  1145. response.ErrorFrom(c, err)
  1146. return
  1147. }
  1148. response.Success(c, gin.H{"message": "Account deleted successfully"})
  1149. }
  1150. // TestAccountRequest represents the request body for testing an account
  1151. type TestAccountRequest struct {
  1152. ModelID string `json:"model_id"`
  1153. Prompt string `json:"prompt"`
  1154. Mode string `json:"mode"`
  1155. // Optional media for Grok (and future) real generation tests.
  1156. // ImageDataURL / AudioDataURL are data:<mime>;base64,... payloads.
  1157. ImageDataURL string `json:"image_data_url"`
  1158. AudioDataURL string `json:"audio_data_url"`
  1159. }
  1160. type SyncFromCRSRequest struct {
  1161. BaseURL string `json:"base_url" binding:"required"`
  1162. Username string `json:"username" binding:"required"`
  1163. Password string `json:"password" binding:"required"`
  1164. SyncProxies *bool `json:"sync_proxies"`
  1165. SelectedAccountIDs []string `json:"selected_account_ids"`
  1166. }
  1167. type PreviewFromCRSRequest struct {
  1168. BaseURL string `json:"base_url" binding:"required"`
  1169. Username string `json:"username" binding:"required"`
  1170. Password string `json:"password" binding:"required"`
  1171. }
  1172. // Test handles testing account connectivity with SSE streaming
  1173. // POST /api/v1/admin/accounts/:id/test
  1174. func (h *AccountHandler) Test(c *gin.Context) {
  1175. accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  1176. if err != nil {
  1177. response.BadRequest(c, "Invalid account ID")
  1178. return
  1179. }
  1180. var req TestAccountRequest
  1181. // Allow empty body, model_id is optional
  1182. _ = c.ShouldBindJSON(&req)
  1183. opts := service.AccountTestOptions{
  1184. ImageDataURL: req.ImageDataURL,
  1185. AudioDataURL: req.AudioDataURL,
  1186. }
  1187. // Use AccountTestService to test the account with SSE streaming
  1188. if err := h.accountTestService.TestAccountConnection(c, accountID, req.ModelID, req.Prompt, req.Mode, opts); err != nil {
  1189. // Error already sent via SSE, just log
  1190. return
  1191. }
  1192. if h.rateLimitService != nil {
  1193. if _, err := h.rateLimitService.RecoverAccountAfterSuccessfulTest(c.Request.Context(), accountID); err != nil {
  1194. _ = c.Error(err)
  1195. }
  1196. }
  1197. }
  1198. // RecoverState handles unified recovery of recoverable account runtime state.
  1199. // POST /api/v1/admin/accounts/:id/recover-state
  1200. func (h *AccountHandler) RecoverState(c *gin.Context) {
  1201. accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  1202. if err != nil {
  1203. response.BadRequest(c, "Invalid account ID")
  1204. return
  1205. }
  1206. if h.rateLimitService == nil {
  1207. response.Error(c, http.StatusServiceUnavailable, "Rate limit service unavailable")
  1208. return
  1209. }
  1210. if _, err := h.rateLimitService.RecoverAccountState(c.Request.Context(), accountID, service.AccountRecoveryOptions{
  1211. InvalidateToken: true,
  1212. }); err != nil {
  1213. response.ErrorFrom(c, err)
  1214. return
  1215. }
  1216. account, err := h.adminService.GetAccount(c.Request.Context(), accountID)
  1217. if err != nil {
  1218. response.ErrorFrom(c, err)
  1219. return
  1220. }
  1221. response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), account))
  1222. }
  1223. // SyncFromCRS handles syncing accounts from claude-relay-service (CRS)
  1224. // POST /api/v1/admin/accounts/sync/crs
  1225. func (h *AccountHandler) SyncFromCRS(c *gin.Context) {
  1226. var req SyncFromCRSRequest
  1227. if err := c.ShouldBindJSON(&req); err != nil {
  1228. response.BadRequest(c, "Invalid request: "+err.Error())
  1229. return
  1230. }
  1231. // Default to syncing proxies (can be disabled by explicitly setting false)
  1232. syncProxies := true
  1233. if req.SyncProxies != nil {
  1234. syncProxies = *req.SyncProxies
  1235. }
  1236. result, err := h.crsSyncService.SyncFromCRS(c.Request.Context(), service.SyncFromCRSInput{
  1237. BaseURL: req.BaseURL,
  1238. Username: req.Username,
  1239. Password: req.Password,
  1240. SyncProxies: syncProxies,
  1241. SelectedAccountIDs: req.SelectedAccountIDs,
  1242. })
  1243. if err != nil {
  1244. // Provide detailed error message for CRS sync failures
  1245. response.InternalError(c, "CRS sync failed: "+err.Error())
  1246. return
  1247. }
  1248. response.Success(c, result)
  1249. }
  1250. // PreviewFromCRS handles previewing accounts from CRS before sync
  1251. // POST /api/v1/admin/accounts/sync/crs/preview
  1252. func (h *AccountHandler) PreviewFromCRS(c *gin.Context) {
  1253. var req PreviewFromCRSRequest
  1254. if err := c.ShouldBindJSON(&req); err != nil {
  1255. response.BadRequest(c, "Invalid request: "+err.Error())
  1256. return
  1257. }
  1258. result, err := h.crsSyncService.PreviewFromCRS(c.Request.Context(), service.SyncFromCRSInput{
  1259. BaseURL: req.BaseURL,
  1260. Username: req.Username,
  1261. Password: req.Password,
  1262. })
  1263. if err != nil {
  1264. response.InternalError(c, "CRS preview failed: "+err.Error())
  1265. return
  1266. }
  1267. response.Success(c, result)
  1268. }
  1269. // refreshSingleAccount refreshes credentials for a single OAuth account.
  1270. // Returns (updatedAccount, warning, error) where warning is used for Antigravity ProjectIDMissing scenario.
  1271. func (h *AccountHandler) refreshSingleAccount(ctx context.Context, account *service.Account) (*service.Account, string, error) {
  1272. if !account.IsOAuth() {
  1273. return nil, "", infraerrors.BadRequest("NOT_OAUTH", "cannot refresh non-OAuth account")
  1274. }
  1275. // spark 影子凭据由母账号管理、自身恒空,刷新无意义且会先打上游;在调用上游前早拒
  1276. // (覆盖单账号与批量两入口;批量侧将其计为 failed 并附说明)(外审第6轮)。
  1277. if account.IsCredentialShadow() {
  1278. return nil, "", infraerrors.BadRequest("SPARK_SHADOW_NO_REFRESH",
  1279. "cannot refresh spark shadow account; its credentials are managed by the parent account")
  1280. }
  1281. var newCredentials map[string]any
  1282. if account.IsOpenAI() {
  1283. tokenInfo, err := h.openaiOAuthService.RefreshAccountToken(ctx, account)
  1284. if err != nil {
  1285. // 刷新失败但 access_token 可能仍有效,尝试设置隐私
  1286. h.adminService.EnsureOpenAIPrivacy(ctx, account)
  1287. return nil, "", err
  1288. }
  1289. newCredentials = h.openaiOAuthService.BuildAccountCredentials(tokenInfo)
  1290. for k, v := range account.Credentials {
  1291. if _, exists := newCredentials[k]; !exists {
  1292. newCredentials[k] = v
  1293. }
  1294. }
  1295. newCredentials = service.NormalizeOpenAIPersonalAccessTokenCredentials(account, tokenInfo, newCredentials)
  1296. } else if account.Platform == service.PlatformGemini {
  1297. tokenInfo, err := h.geminiOAuthService.RefreshAccountToken(ctx, account)
  1298. if err != nil {
  1299. return nil, "", fmt.Errorf("failed to refresh credentials: %w", err)
  1300. }
  1301. newCredentials = h.geminiOAuthService.BuildAccountCredentials(tokenInfo)
  1302. for k, v := range account.Credentials {
  1303. if _, exists := newCredentials[k]; !exists {
  1304. newCredentials[k] = v
  1305. }
  1306. }
  1307. } else if account.Platform == service.PlatformAntigravity {
  1308. tokenInfo, err := h.antigravityOAuthService.RefreshAccountToken(ctx, account)
  1309. if err != nil {
  1310. return nil, "", err
  1311. }
  1312. newCredentials = h.antigravityOAuthService.BuildAccountCredentials(tokenInfo)
  1313. for k, v := range account.Credentials {
  1314. if _, exists := newCredentials[k]; !exists {
  1315. newCredentials[k] = v
  1316. }
  1317. }
  1318. // 特殊处理 project_id:如果新值为空但旧值非空,保留旧值
  1319. // 这确保了即使 LoadCodeAssist 失败,project_id 也不会丢失
  1320. if newProjectID, _ := newCredentials["project_id"].(string); newProjectID == "" {
  1321. if oldProjectID := strings.TrimSpace(account.GetCredential("project_id")); oldProjectID != "" {
  1322. newCredentials["project_id"] = oldProjectID
  1323. }
  1324. }
  1325. // 如果 project_id 获取失败,更新凭证但不标记为 error
  1326. if tokenInfo.ProjectIDMissing {
  1327. updatedAccount, updateErr := h.adminService.UpdateAccount(ctx, account.ID, &service.UpdateAccountInput{
  1328. Credentials: newCredentials,
  1329. })
  1330. if updateErr != nil {
  1331. return nil, "", fmt.Errorf("failed to update credentials: %w", updateErr)
  1332. }
  1333. h.adminService.EnsureAntigravityPrivacy(ctx, updatedAccount)
  1334. return updatedAccount, "missing_project_id_temporary", nil
  1335. }
  1336. // 成功获取到 project_id,如果之前是 missing_project_id 错误则清除
  1337. if account.Status == service.StatusError && strings.Contains(account.ErrorMessage, "missing_project_id:") {
  1338. if _, clearErr := h.adminService.ClearAccountError(ctx, account.ID); clearErr != nil {
  1339. return nil, "", fmt.Errorf("failed to clear account error: %w", clearErr)
  1340. }
  1341. }
  1342. } else if account.Platform == service.PlatformGrok {
  1343. if h.grokOAuthService == nil {
  1344. return nil, "", fmt.Errorf("grok oauth service is not configured")
  1345. }
  1346. tokenInfo, err := h.grokOAuthService.RefreshAccountToken(ctx, account)
  1347. if err != nil {
  1348. return nil, "", fmt.Errorf("failed to refresh Grok credentials: %w", err)
  1349. }
  1350. newCredentials = service.MergeCredentials(account.Credentials, h.grokOAuthService.BuildAccountCredentials(tokenInfo))
  1351. if baseURL := strings.TrimSpace(account.GetCredential("base_url")); baseURL != "" {
  1352. newCredentials["base_url"] = baseURL
  1353. }
  1354. } else {
  1355. // Use Anthropic/Claude OAuth service to refresh token
  1356. tokenInfo, err := h.oauthService.RefreshAccountToken(ctx, account)
  1357. if err != nil {
  1358. return nil, "", err
  1359. }
  1360. // Copy existing credentials to preserve non-token settings (e.g., intercept_warmup_requests)
  1361. newCredentials = make(map[string]any)
  1362. for k, v := range account.Credentials {
  1363. newCredentials[k] = v
  1364. }
  1365. // Update token-related fields
  1366. newCredentials["access_token"] = tokenInfo.AccessToken
  1367. newCredentials["token_type"] = tokenInfo.TokenType
  1368. newCredentials["expires_in"] = strconv.FormatInt(tokenInfo.ExpiresIn, 10)
  1369. newCredentials["expires_at"] = strconv.FormatInt(tokenInfo.ExpiresAt, 10)
  1370. if strings.TrimSpace(tokenInfo.RefreshToken) != "" {
  1371. newCredentials["refresh_token"] = tokenInfo.RefreshToken
  1372. }
  1373. if strings.TrimSpace(tokenInfo.Scope) != "" {
  1374. newCredentials["scope"] = tokenInfo.Scope
  1375. }
  1376. }
  1377. updatedAccount, err := h.adminService.UpdateAccount(ctx, account.ID, &service.UpdateAccountInput{
  1378. Credentials: newCredentials,
  1379. })
  1380. if err != nil {
  1381. return nil, "", err
  1382. }
  1383. // 刷新成功后,清除 token 缓存,确保下次请求使用新 token
  1384. if h.tokenCacheInvalidator != nil {
  1385. if invalidateErr := h.tokenCacheInvalidator.InvalidateToken(ctx, updatedAccount); invalidateErr != nil {
  1386. log.Printf("[WARN] Failed to invalidate token cache for account %d: %v", updatedAccount.ID, invalidateErr)
  1387. }
  1388. }
  1389. // OpenAI OAuth: 刷新成功后检查并设置 privacy_mode
  1390. h.adminService.EnsureOpenAIPrivacy(ctx, updatedAccount)
  1391. // Antigravity OAuth: 刷新成功后检查并设置 privacy_mode
  1392. h.adminService.EnsureAntigravityPrivacy(ctx, updatedAccount)
  1393. return updatedAccount, "", nil
  1394. }
  1395. // Refresh handles refreshing account credentials
  1396. // POST /api/v1/admin/accounts/:id/refresh
  1397. func (h *AccountHandler) Refresh(c *gin.Context) {
  1398. accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  1399. if err != nil {
  1400. response.BadRequest(c, "Invalid account ID")
  1401. return
  1402. }
  1403. // Get account
  1404. account, err := h.adminService.GetAccount(c.Request.Context(), accountID)
  1405. if err != nil {
  1406. response.NotFound(c, "Account not found")
  1407. return
  1408. }
  1409. updatedAccount, warning, err := h.refreshSingleAccount(c.Request.Context(), account)
  1410. if err != nil {
  1411. response.ErrorFrom(c, err)
  1412. return
  1413. }
  1414. if warning == "missing_project_id_temporary" {
  1415. response.Success(c, gin.H{
  1416. "message": "Token refreshed successfully, but project_id could not be retrieved (will retry automatically)",
  1417. "warning": "missing_project_id_temporary",
  1418. })
  1419. return
  1420. }
  1421. response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), updatedAccount))
  1422. }
  1423. // ApplyOAuthCredentialsRequest is the payload for persisting re-authorized OAuth credentials.
  1424. type ApplyOAuthCredentialsRequest struct {
  1425. Type string `json:"type" binding:"required,oneof=oauth setup-token"`
  1426. Credentials map[string]any `json:"credentials" binding:"required"`
  1427. Extra map[string]any `json:"extra"`
  1428. }
  1429. // ApplyOAuthCredentials 将"重新授权"得到的新凭据原子落库。
  1430. // POST /api/v1/admin/accounts/:id/apply-oauth-credentials
  1431. //
  1432. // 与通用 PUT /:id (Update) 接口的关键区别:
  1433. // - 仅接收 type / credentials / extra 三个字段(不接受 concurrency / rpm / quota_* 等可能误传的字段)
  1434. // - Extra 走 UpdateAccountExtra(JSONB key 级合并),**绝不**全量覆盖;
  1435. // 避免 base_rpm / window_cost_limit / max_sessions / quota_* / privacy_mode
  1436. // 等持久化配置在重新授权后丢失
  1437. // - 内置 ClearError + InvalidateToken,避免前端额外两次调用,
  1438. // 并修复旧路径未失效 token 缓存导致重新授权后立即 401 的隐性 bug
  1439. //
  1440. // 与 /refresh 的区别:/refresh 用现有 refresh_token 换 access_token(无用户交互),
  1441. // 本接口承接前端完成完整 OAuth 流程后的落库步骤。
  1442. func (h *AccountHandler) ApplyOAuthCredentials(c *gin.Context) {
  1443. accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  1444. if err != nil {
  1445. response.BadRequest(c, "Invalid account ID")
  1446. return
  1447. }
  1448. var req ApplyOAuthCredentialsRequest
  1449. if err := c.ShouldBindJSON(&req); err != nil {
  1450. response.BadRequest(c, "Invalid request: "+err.Error())
  1451. return
  1452. }
  1453. ctx := c.Request.Context()
  1454. // 预检查账号存在 + OAuth 类型(与 Refresh handler 语义一致,提供更友好的错误信息)。
  1455. existing, err := h.adminService.GetAccount(ctx, accountID)
  1456. if err != nil {
  1457. response.NotFound(c, "Account not found")
  1458. return
  1459. }
  1460. if !existing.IsOAuth() {
  1461. response.ErrorFrom(c, infraerrors.BadRequest("NOT_OAUTH", "cannot apply oauth credentials to non-OAuth account"))
  1462. return
  1463. }
  1464. if err := service.ValidateOpenAILongContextBillingExtra(existing.Platform, req.Extra); err != nil {
  1465. response.ErrorFrom(c, err)
  1466. return
  1467. }
  1468. if err := service.ValidateUpstreamRequestIDHeaderExtra(req.Extra); err != nil {
  1469. response.ErrorFrom(c, err)
  1470. return
  1471. }
  1472. // Drop SSO/password residue; re-auth must leave only OAuth tokens on disk.
  1473. req.Credentials = service.SanitizeStoredCredentials(existing.Platform, req.Credentials)
  1474. updatedAccount, err := h.adminService.UpdateAccount(ctx, accountID, &service.UpdateAccountInput{
  1475. Type: req.Type,
  1476. Credentials: req.Credentials,
  1477. })
  1478. if err != nil {
  1479. response.ErrorFrom(c, err)
  1480. return
  1481. }
  1482. // 增量合并 Extra(JSONB key 级 merge,绝不覆盖 base_rpm / window_cost_limit /
  1483. // max_sessions / quota_* / privacy_mode 等持久化键)。
  1484. // best-effort:失败仅记日志;下方 ClearAccountError 会从 DB 重新读取最新 account,
  1485. // 因此响应里的 extra 始终以 DB 为准——这里不需要手动维护内存快照。
  1486. if len(req.Extra) > 0 {
  1487. if extraErr := h.adminService.UpdateAccountExtra(ctx, accountID, req.Extra); extraErr != nil {
  1488. extraKeys := make([]string, 0, len(req.Extra))
  1489. for k := range req.Extra {
  1490. extraKeys = append(extraKeys, k)
  1491. }
  1492. slog.Error("apply_oauth_credentials.update_extra_failed",
  1493. "account_id", accountID,
  1494. "extra_keys", extraKeys,
  1495. "err", extraErr,
  1496. )
  1497. }
  1498. }
  1499. // Successful re-auth clears the soft spending-limit reauth flag for Grok.
  1500. if existing.Platform == service.PlatformGrok {
  1501. if clearErr := h.adminService.UpdateAccountExtra(ctx, accountID, map[string]any{
  1502. "grok_needs_reauth": false,
  1503. "grok_needs_reauth_reason": "",
  1504. "grok_needs_reauth_at": "",
  1505. }); clearErr != nil {
  1506. slog.Warn("apply_oauth_credentials.clear_grok_reauth_failed",
  1507. "account_id", accountID,
  1508. "err", clearErr,
  1509. )
  1510. }
  1511. }
  1512. if cleared, clearErr := h.adminService.ClearAccountError(ctx, accountID); clearErr != nil {
  1513. slog.Warn("apply_oauth_credentials.clear_error_failed",
  1514. "account_id", accountID,
  1515. "err", clearErr,
  1516. )
  1517. } else if cleared != nil {
  1518. updatedAccount = cleared
  1519. }
  1520. if h.tokenCacheInvalidator != nil && updatedAccount.IsOAuth() {
  1521. if invalidateErr := h.tokenCacheInvalidator.InvalidateToken(ctx, updatedAccount); invalidateErr != nil {
  1522. slog.Warn("apply_oauth_credentials.invalidate_token_failed",
  1523. "account_id", accountID,
  1524. "err", invalidateErr,
  1525. )
  1526. }
  1527. }
  1528. response.Success(c, h.buildAccountResponseWithRuntime(ctx, updatedAccount))
  1529. }
  1530. // GetStats handles getting account statistics
  1531. // GET /api/v1/admin/accounts/:id/stats
  1532. func (h *AccountHandler) GetStats(c *gin.Context) {
  1533. accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  1534. if err != nil {
  1535. response.BadRequest(c, "Invalid account ID")
  1536. return
  1537. }
  1538. // Parse days parameter (default 30)
  1539. days := 30
  1540. if daysStr := c.Query("days"); daysStr != "" {
  1541. if d, err := strconv.Atoi(daysStr); err == nil && d > 0 && d <= 90 {
  1542. days = d
  1543. }
  1544. }
  1545. // Calculate time range
  1546. now := timezone.Now()
  1547. endTime := timezone.StartOfDay(now.AddDate(0, 0, 1))
  1548. startTime := timezone.StartOfDay(now.AddDate(0, 0, -days+1))
  1549. stats, err := h.accountUsageService.GetAccountUsageStats(c.Request.Context(), accountID, startTime, endTime)
  1550. if err != nil {
  1551. response.ErrorFrom(c, err)
  1552. return
  1553. }
  1554. response.Success(c, stats)
  1555. }
  1556. // ClearError handles clearing account error
  1557. // POST /api/v1/admin/accounts/:id/clear-error
  1558. func (h *AccountHandler) ClearError(c *gin.Context) {
  1559. accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  1560. if err != nil {
  1561. response.BadRequest(c, "Invalid account ID")
  1562. return
  1563. }
  1564. account, err := h.adminService.ClearAccountError(c.Request.Context(), accountID)
  1565. if err != nil {
  1566. response.ErrorFrom(c, err)
  1567. return
  1568. }
  1569. // 清除错误后,同时清除 token 缓存,确保下次请求会获取最新的 token(触发刷新或从 DB 读取)
  1570. // 这解决了管理员重置账号状态后,旧的失效 token 仍在缓存中导致立即再次 401 的问题
  1571. if h.tokenCacheInvalidator != nil && account.IsOAuth() {
  1572. if invalidateErr := h.tokenCacheInvalidator.InvalidateToken(c.Request.Context(), account); invalidateErr != nil {
  1573. log.Printf("[WARN] Failed to invalidate token cache for account %d: %v", accountID, invalidateErr)
  1574. }
  1575. }
  1576. response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), account))
  1577. }
  1578. // RevertProxyFallback handles reverting account proxy to original before fallback.
  1579. // POST /api/v1/admin/accounts/:id/revert-proxy-fallback
  1580. func (h *AccountHandler) RevertProxyFallback(c *gin.Context) {
  1581. id, err := strconv.ParseInt(c.Param("id"), 10, 64)
  1582. if err != nil {
  1583. response.BadRequest(c, "Invalid account ID")
  1584. return
  1585. }
  1586. if err := h.adminService.RevertAccountProxyFallback(c.Request.Context(), id); err != nil {
  1587. response.ErrorFrom(c, err)
  1588. return
  1589. }
  1590. response.Success(c, gin.H{"message": "reverted"})
  1591. }
  1592. // BatchDelete handles deleting multiple accounts with bounded concurrency.
  1593. // POST /api/v1/admin/accounts/batch-delete
  1594. func (h *AccountHandler) BatchDelete(c *gin.Context) {
  1595. var req struct {
  1596. AccountIDs []int64 `json:"account_ids"`
  1597. }
  1598. if err := c.ShouldBindJSON(&req); err != nil {
  1599. response.BadRequest(c, "Invalid request: "+err.Error())
  1600. return
  1601. }
  1602. accountIDs := normalizeInt64IDList(req.AccountIDs)
  1603. if len(accountIDs) == 0 {
  1604. response.BadRequest(c, "account_ids is required")
  1605. return
  1606. }
  1607. accounts, err := h.adminService.GetAccountsByIDs(c.Request.Context(), accountIDs)
  1608. if err != nil {
  1609. response.ErrorFrom(c, err)
  1610. return
  1611. }
  1612. type deleteError struct {
  1613. AccountID int64 `json:"account_id"`
  1614. Error string `json:"error"`
  1615. }
  1616. requestedIDs := make(map[int64]struct{}, len(accountIDs))
  1617. for _, accountID := range accountIDs {
  1618. requestedIDs[accountID] = struct{}{}
  1619. }
  1620. accountsByID := make(map[int64]*service.Account, len(accounts))
  1621. for _, account := range accounts {
  1622. if account != nil {
  1623. accountsByID[account.ID] = account
  1624. }
  1625. }
  1626. rootIDs := make([]int64, 0, len(accountIDs))
  1627. dependentIDs := make(map[int64][]int64)
  1628. failedIDs := make([]int64, 0)
  1629. errorsByAccount := make([]deleteError, 0)
  1630. for _, accountID := range accountIDs {
  1631. account := accountsByID[accountID]
  1632. if account == nil {
  1633. failedIDs = append(failedIDs, accountID)
  1634. errorsByAccount = append(errorsByAccount, deleteError{
  1635. AccountID: accountID,
  1636. Error: "account not found",
  1637. })
  1638. continue
  1639. }
  1640. rootID := accountID
  1641. visited := map[int64]struct{}{accountID: {}}
  1642. for {
  1643. current := accountsByID[rootID]
  1644. if current == nil || current.ParentAccountID == nil {
  1645. break
  1646. }
  1647. parentID := *current.ParentAccountID
  1648. if _, selected := requestedIDs[parentID]; !selected {
  1649. break
  1650. }
  1651. if _, exists := accountsByID[parentID]; !exists {
  1652. break
  1653. }
  1654. if _, cyclic := visited[parentID]; cyclic {
  1655. rootID = accountID
  1656. break
  1657. }
  1658. visited[parentID] = struct{}{}
  1659. rootID = parentID
  1660. }
  1661. if rootID != accountID {
  1662. dependentIDs[rootID] = append(dependentIDs[rootID], accountID)
  1663. continue
  1664. }
  1665. rootIDs = append(rootIDs, accountID)
  1666. }
  1667. const maxConcurrency = 5
  1668. g, gctx := errgroup.WithContext(c.Request.Context())
  1669. g.SetLimit(maxConcurrency)
  1670. var mu sync.Mutex
  1671. successIDs := make([]int64, 0, len(accountIDs))
  1672. // Every worker returns nil so one account failure does not cancel the remaining deletions.
  1673. for _, id := range rootIDs {
  1674. accountID := id
  1675. g.Go(func() error {
  1676. err := h.adminService.DeleteAccount(gctx, accountID)
  1677. mu.Lock()
  1678. defer mu.Unlock()
  1679. affectedIDs := append([]int64{accountID}, dependentIDs[accountID]...)
  1680. if err != nil {
  1681. for _, affectedID := range affectedIDs {
  1682. failedIDs = append(failedIDs, affectedID)
  1683. errorsByAccount = append(errorsByAccount, deleteError{
  1684. AccountID: affectedID,
  1685. Error: err.Error(),
  1686. })
  1687. }
  1688. return nil
  1689. }
  1690. successIDs = append(successIDs, affectedIDs...)
  1691. return nil
  1692. })
  1693. }
  1694. if err := g.Wait(); err != nil {
  1695. response.ErrorFrom(c, err)
  1696. return
  1697. }
  1698. sort.Slice(successIDs, func(i, j int) bool { return successIDs[i] < successIDs[j] })
  1699. sort.Slice(failedIDs, func(i, j int) bool { return failedIDs[i] < failedIDs[j] })
  1700. sort.Slice(errorsByAccount, func(i, j int) bool {
  1701. return errorsByAccount[i].AccountID < errorsByAccount[j].AccountID
  1702. })
  1703. response.Success(c, gin.H{
  1704. "total": len(accountIDs),
  1705. "success": len(successIDs),
  1706. "failed": len(failedIDs),
  1707. "success_ids": successIDs,
  1708. "failed_ids": failedIDs,
  1709. "errors": errorsByAccount,
  1710. })
  1711. }
  1712. // BatchClearError handles batch clearing account errors
  1713. // POST /api/v1/admin/accounts/batch-clear-error
  1714. func (h *AccountHandler) BatchClearError(c *gin.Context) {
  1715. var req struct {
  1716. AccountIDs []int64 `json:"account_ids"`
  1717. }
  1718. if err := c.ShouldBindJSON(&req); err != nil {
  1719. response.BadRequest(c, "Invalid request: "+err.Error())
  1720. return
  1721. }
  1722. if len(req.AccountIDs) == 0 {
  1723. response.BadRequest(c, "account_ids is required")
  1724. return
  1725. }
  1726. ctx := c.Request.Context()
  1727. const maxConcurrency = 10
  1728. g, gctx := errgroup.WithContext(ctx)
  1729. g.SetLimit(maxConcurrency)
  1730. var mu sync.Mutex
  1731. var successCount, failedCount int
  1732. var errors []gin.H
  1733. // 注意:所有 goroutine 必须 return nil,避免 errgroup cancel 其他并发任务
  1734. for _, id := range req.AccountIDs {
  1735. accountID := id // 闭包捕获
  1736. g.Go(func() error {
  1737. account, err := h.adminService.ClearAccountError(gctx, accountID)
  1738. if err != nil {
  1739. mu.Lock()
  1740. failedCount++
  1741. errors = append(errors, gin.H{
  1742. "account_id": accountID,
  1743. "error": err.Error(),
  1744. })
  1745. mu.Unlock()
  1746. return nil
  1747. }
  1748. // 清除错误后,同时清除 token 缓存
  1749. if h.tokenCacheInvalidator != nil && account.IsOAuth() {
  1750. if invalidateErr := h.tokenCacheInvalidator.InvalidateToken(gctx, account); invalidateErr != nil {
  1751. log.Printf("[WARN] Failed to invalidate token cache for account %d: %v", accountID, invalidateErr)
  1752. }
  1753. }
  1754. mu.Lock()
  1755. successCount++
  1756. mu.Unlock()
  1757. return nil
  1758. })
  1759. }
  1760. if err := g.Wait(); err != nil {
  1761. response.ErrorFrom(c, err)
  1762. return
  1763. }
  1764. response.Success(c, gin.H{
  1765. "total": len(req.AccountIDs),
  1766. "success": successCount,
  1767. "failed": failedCount,
  1768. "errors": errors,
  1769. })
  1770. }
  1771. // BatchRefresh handles batch refreshing account credentials
  1772. // POST /api/v1/admin/accounts/batch-refresh
  1773. func (h *AccountHandler) BatchRefresh(c *gin.Context) {
  1774. var req struct {
  1775. AccountIDs []int64 `json:"account_ids"`
  1776. }
  1777. if err := c.ShouldBindJSON(&req); err != nil {
  1778. response.BadRequest(c, "Invalid request: "+err.Error())
  1779. return
  1780. }
  1781. if len(req.AccountIDs) == 0 {
  1782. response.BadRequest(c, "account_ids is required")
  1783. return
  1784. }
  1785. ctx := c.Request.Context()
  1786. accounts, err := h.adminService.GetAccountsByIDs(ctx, req.AccountIDs)
  1787. if err != nil {
  1788. response.ErrorFrom(c, err)
  1789. return
  1790. }
  1791. // 建立已获取账号的 ID 集合,检测缺失的 ID
  1792. foundIDs := make(map[int64]bool, len(accounts))
  1793. for _, acc := range accounts {
  1794. if acc != nil {
  1795. foundIDs[acc.ID] = true
  1796. }
  1797. }
  1798. const maxConcurrency = 10
  1799. g, gctx := errgroup.WithContext(ctx)
  1800. g.SetLimit(maxConcurrency)
  1801. var mu sync.Mutex
  1802. var successCount, failedCount int
  1803. var errors []gin.H
  1804. var warnings []gin.H
  1805. // 将不存在的账号 ID 标记为失败
  1806. for _, id := range req.AccountIDs {
  1807. if !foundIDs[id] {
  1808. failedCount++
  1809. errors = append(errors, gin.H{
  1810. "account_id": id,
  1811. "error": "account not found",
  1812. })
  1813. }
  1814. }
  1815. // 注意:所有 goroutine 必须 return nil,避免 errgroup cancel 其他并发任务
  1816. for _, account := range accounts {
  1817. acc := account // 闭包捕获
  1818. if acc == nil {
  1819. continue
  1820. }
  1821. g.Go(func() error {
  1822. _, warning, err := h.refreshSingleAccount(gctx, acc)
  1823. mu.Lock()
  1824. if err != nil {
  1825. failedCount++
  1826. errors = append(errors, gin.H{
  1827. "account_id": acc.ID,
  1828. "error": err.Error(),
  1829. })
  1830. } else {
  1831. successCount++
  1832. if warning != "" {
  1833. warnings = append(warnings, gin.H{
  1834. "account_id": acc.ID,
  1835. "warning": warning,
  1836. })
  1837. }
  1838. }
  1839. mu.Unlock()
  1840. return nil
  1841. })
  1842. }
  1843. if err := g.Wait(); err != nil {
  1844. response.ErrorFrom(c, err)
  1845. return
  1846. }
  1847. response.Success(c, gin.H{
  1848. "total": len(req.AccountIDs),
  1849. "success": successCount,
  1850. "failed": failedCount,
  1851. "errors": errors,
  1852. "warnings": warnings,
  1853. })
  1854. }
  1855. // BatchCreate handles batch creating accounts
  1856. // POST /api/v1/admin/accounts/batch
  1857. func (h *AccountHandler) BatchCreate(c *gin.Context) {
  1858. var req struct {
  1859. Accounts []CreateAccountRequest `json:"accounts" binding:"required,min=1"`
  1860. }
  1861. if err := c.ShouldBindJSON(&req); err != nil {
  1862. response.BadRequest(c, "Invalid request: "+err.Error())
  1863. return
  1864. }
  1865. for _, item := range req.Accounts {
  1866. if err := service.ValidateOpenAILongContextBillingExtra(item.Platform, item.Extra); err != nil {
  1867. response.ErrorFrom(c, err)
  1868. return
  1869. }
  1870. }
  1871. groupIDs := make([]int64, 0)
  1872. for _, item := range req.Accounts {
  1873. groupIDs = append(groupIDs, item.GroupIDs...)
  1874. }
  1875. if err := h.adminService.ValidateAccountGroupBindings(c.Request.Context(), groupIDs); err != nil {
  1876. response.ErrorFrom(c, err)
  1877. return
  1878. }
  1879. executeAdminIdempotentJSON(c, "admin.accounts.batch_create", req, service.DefaultWriteIdempotencyTTL(), func(ctx context.Context) (any, error) {
  1880. success := 0
  1881. failed := 0
  1882. results := make([]gin.H, 0, len(req.Accounts))
  1883. // 收集需要异步设置隐私的 OAuth 账号
  1884. var antigravityPrivacyAccounts []*service.Account
  1885. var openaiPrivacyAccounts []*service.Account
  1886. for _, item := range req.Accounts {
  1887. if item.RateMultiplier != nil && *item.RateMultiplier < 0 {
  1888. failed++
  1889. results = append(results, gin.H{
  1890. "name": item.Name,
  1891. "success": false,
  1892. "error": "rate_multiplier must be >= 0",
  1893. })
  1894. continue
  1895. }
  1896. // base_rpm 输入校验:负值归零,超过 10000 截断
  1897. sanitizeExtraBaseRPM(item.Extra)
  1898. if err := service.ValidateUpstreamRequestIDHeaderExtra(item.Extra); err != nil {
  1899. failed++
  1900. results = append(results, gin.H{
  1901. "name": item.Name,
  1902. "success": false,
  1903. "error": err.Error(),
  1904. })
  1905. continue
  1906. }
  1907. skipCheck := item.ConfirmMixedChannelRisk != nil && *item.ConfirmMixedChannelRisk
  1908. account, err := h.adminService.CreateAccount(ctx, &service.CreateAccountInput{
  1909. Name: item.Name,
  1910. Notes: item.Notes,
  1911. Platform: item.Platform,
  1912. Type: item.Type,
  1913. Credentials: item.Credentials,
  1914. Extra: item.Extra,
  1915. ProxyID: item.ProxyID,
  1916. Concurrency: item.Concurrency,
  1917. Priority: item.Priority,
  1918. RateMultiplier: item.RateMultiplier,
  1919. GroupIDs: item.GroupIDs,
  1920. ExpiresAt: item.ExpiresAt,
  1921. AutoPauseOnExpired: item.AutoPauseOnExpired,
  1922. SkipMixedChannelCheck: skipCheck,
  1923. })
  1924. if err != nil {
  1925. failed++
  1926. results = append(results, gin.H{
  1927. "name": item.Name,
  1928. "success": false,
  1929. "error": err.Error(),
  1930. })
  1931. continue
  1932. }
  1933. // 收集需要异步设置隐私的 OAuth 账号
  1934. if account.Type == service.AccountTypeOAuth {
  1935. switch account.Platform {
  1936. case service.PlatformAntigravity:
  1937. antigravityPrivacyAccounts = append(antigravityPrivacyAccounts, account)
  1938. case service.PlatformOpenAI:
  1939. openaiPrivacyAccounts = append(openaiPrivacyAccounts, account)
  1940. }
  1941. }
  1942. // OpenAI APIKey 账号异步探测 /v1/responses 能力。
  1943. h.scheduleOpenAIResponsesProbe(account)
  1944. h.scheduleGrokImportProbe(account)
  1945. success++
  1946. results = append(results, gin.H{
  1947. "name": item.Name,
  1948. "id": account.ID,
  1949. "success": true,
  1950. })
  1951. }
  1952. // 异步设置隐私,避免批量创建时阻塞请求
  1953. adminSvc := h.adminService
  1954. if len(antigravityPrivacyAccounts) > 0 {
  1955. accounts := antigravityPrivacyAccounts
  1956. go func() {
  1957. defer func() {
  1958. if r := recover(); r != nil {
  1959. slog.Error("batch_create_antigravity_privacy_panic", "recover", r)
  1960. }
  1961. }()
  1962. bgCtx := context.Background()
  1963. for _, acc := range accounts {
  1964. adminSvc.ForceAntigravityPrivacy(bgCtx, acc)
  1965. }
  1966. }()
  1967. }
  1968. if len(openaiPrivacyAccounts) > 0 {
  1969. accounts := openaiPrivacyAccounts
  1970. go func() {
  1971. defer func() {
  1972. if r := recover(); r != nil {
  1973. slog.Error("batch_create_openai_privacy_panic", "recover", r)
  1974. }
  1975. }()
  1976. bgCtx := context.Background()
  1977. for _, acc := range accounts {
  1978. adminSvc.ForceOpenAIPrivacy(bgCtx, acc)
  1979. }
  1980. }()
  1981. }
  1982. return gin.H{
  1983. "success": success,
  1984. "failed": failed,
  1985. "results": results,
  1986. }, nil
  1987. })
  1988. }
  1989. // BatchUpdateCredentialsRequest represents batch credentials update request
  1990. type BatchUpdateCredentialsRequest struct {
  1991. AccountIDs []int64 `json:"account_ids" binding:"required,min=1"`
  1992. Field string `json:"field" binding:"required,oneof=account_uuid org_uuid intercept_warmup_requests"`
  1993. Value any `json:"value"`
  1994. }
  1995. // BatchUpdateCredentials handles batch updating credentials fields
  1996. // POST /api/v1/admin/accounts/batch-update-credentials
  1997. func (h *AccountHandler) BatchUpdateCredentials(c *gin.Context) {
  1998. var req BatchUpdateCredentialsRequest
  1999. if err := c.ShouldBindJSON(&req); err != nil {
  2000. response.BadRequest(c, "Invalid request: "+err.Error())
  2001. return
  2002. }
  2003. // Validate value type based on field
  2004. if req.Field == "intercept_warmup_requests" {
  2005. // Must be boolean
  2006. if _, ok := req.Value.(bool); !ok {
  2007. response.BadRequest(c, "intercept_warmup_requests must be boolean")
  2008. return
  2009. }
  2010. } else {
  2011. // account_uuid and org_uuid can be string or null
  2012. if req.Value != nil {
  2013. if _, ok := req.Value.(string); !ok {
  2014. response.BadRequest(c, req.Field+" must be string or null")
  2015. return
  2016. }
  2017. }
  2018. }
  2019. ctx := c.Request.Context()
  2020. // 阶段一:预验证所有账号存在,收集 credentials
  2021. type accountUpdate struct {
  2022. ID int64
  2023. Credentials map[string]any
  2024. }
  2025. updates := make([]accountUpdate, 0, len(req.AccountIDs))
  2026. for _, accountID := range req.AccountIDs {
  2027. account, err := h.adminService.GetAccount(ctx, accountID)
  2028. if err != nil {
  2029. response.Error(c, 404, fmt.Sprintf("Account %d not found", accountID))
  2030. return
  2031. }
  2032. if account.Credentials == nil {
  2033. account.Credentials = make(map[string]any)
  2034. }
  2035. account.Credentials[req.Field] = req.Value
  2036. updates = append(updates, accountUpdate{ID: accountID, Credentials: account.Credentials})
  2037. }
  2038. // 阶段二:依次更新,返回每个账号的成功/失败明细,便于调用方重试
  2039. success := 0
  2040. failed := 0
  2041. successIDs := make([]int64, 0, len(updates))
  2042. failedIDs := make([]int64, 0, len(updates))
  2043. results := make([]gin.H, 0, len(updates))
  2044. for _, u := range updates {
  2045. updateInput := &service.UpdateAccountInput{Credentials: u.Credentials}
  2046. if _, err := h.adminService.UpdateAccount(ctx, u.ID, updateInput); err != nil {
  2047. failed++
  2048. failedIDs = append(failedIDs, u.ID)
  2049. results = append(results, gin.H{
  2050. "account_id": u.ID,
  2051. "success": false,
  2052. "error": err.Error(),
  2053. })
  2054. continue
  2055. }
  2056. success++
  2057. successIDs = append(successIDs, u.ID)
  2058. results = append(results, gin.H{
  2059. "account_id": u.ID,
  2060. "success": true,
  2061. })
  2062. }
  2063. response.Success(c, gin.H{
  2064. "success": success,
  2065. "failed": failed,
  2066. "success_ids": successIDs,
  2067. "failed_ids": failedIDs,
  2068. "results": results,
  2069. })
  2070. }
  2071. // BulkUpdate handles bulk updating accounts with selected fields/credentials.
  2072. // POST /api/v1/admin/accounts/bulk-update
  2073. func (h *AccountHandler) BulkUpdate(c *gin.Context) {
  2074. var req BulkUpdateAccountsRequest
  2075. if err := c.ShouldBindJSON(&req); err != nil {
  2076. response.BadRequest(c, "Invalid request: "+err.Error())
  2077. return
  2078. }
  2079. if req.RateMultiplier != nil && *req.RateMultiplier < 0 {
  2080. response.BadRequest(c, "rate_multiplier must be >= 0")
  2081. return
  2082. }
  2083. if len(req.AccountIDs) == 0 && req.Filters == nil {
  2084. response.BadRequest(c, "account_ids or filters is required")
  2085. return
  2086. }
  2087. // base_rpm 输入校验:负值归零,超过 10000 截断
  2088. sanitizeExtraBaseRPM(req.Extra)
  2089. if err := service.ValidateUpstreamRequestIDHeaderExtra(req.Extra); err != nil {
  2090. response.ErrorFrom(c, err)
  2091. return
  2092. }
  2093. // 确定是否跳过混合渠道检查
  2094. skipCheck := req.ConfirmMixedChannelRisk != nil && *req.ConfirmMixedChannelRisk
  2095. hasUpdates := req.Name != "" ||
  2096. req.ProxyID != nil ||
  2097. req.Concurrency != nil ||
  2098. req.Priority != nil ||
  2099. req.RateMultiplier != nil ||
  2100. req.LoadFactor != nil ||
  2101. req.Status != "" ||
  2102. req.Schedulable != nil ||
  2103. req.GroupIDs != nil ||
  2104. len(req.Credentials) > 0 ||
  2105. len(req.Extra) > 0 ||
  2106. req.ProbeEnabled != nil
  2107. if !hasUpdates {
  2108. response.BadRequest(c, "No updates provided")
  2109. return
  2110. }
  2111. result, err := h.adminService.BulkUpdateAccounts(c.Request.Context(), &service.BulkUpdateAccountsInput{
  2112. AccountIDs: req.AccountIDs,
  2113. Filters: toServiceBulkUpdateAccountFilters(req.Filters),
  2114. Name: req.Name,
  2115. ProxyID: req.ProxyID,
  2116. Concurrency: req.Concurrency,
  2117. Priority: req.Priority,
  2118. RateMultiplier: req.RateMultiplier,
  2119. LoadFactor: req.LoadFactor,
  2120. Status: req.Status,
  2121. Schedulable: req.Schedulable,
  2122. GroupIDs: req.GroupIDs,
  2123. Credentials: req.Credentials,
  2124. Extra: req.Extra,
  2125. ProbeEnabled: req.ProbeEnabled,
  2126. SkipMixedChannelCheck: skipCheck,
  2127. })
  2128. if err != nil {
  2129. var mixedErr *service.MixedChannelError
  2130. if errors.As(err, &mixedErr) {
  2131. c.JSON(409, gin.H{
  2132. "error": "mixed_channel_warning",
  2133. "message": mixedErr.Error(),
  2134. "details": gin.H{
  2135. "group_id": mixedErr.GroupID,
  2136. "group_name": mixedErr.GroupName,
  2137. "current_platform": mixedErr.CurrentPlatform,
  2138. "other_platform": mixedErr.OtherPlatform,
  2139. },
  2140. })
  2141. return
  2142. }
  2143. response.ErrorFrom(c, err)
  2144. return
  2145. }
  2146. response.Success(c, result)
  2147. }
  2148. func toServiceBulkUpdateAccountFilters(filters *BulkUpdateAccountFilters) *service.BulkUpdateAccountFilters {
  2149. if filters == nil {
  2150. return nil
  2151. }
  2152. return &service.BulkUpdateAccountFilters{
  2153. Platform: filters.Platform,
  2154. Type: filters.Type,
  2155. Status: filters.Status,
  2156. Group: filters.Group,
  2157. Search: filters.Search,
  2158. PrivacyMode: filters.PrivacyMode,
  2159. }
  2160. }
  2161. // ========== OAuth Handlers ==========
  2162. // GenerateAuthURLRequest represents the request for generating auth URL
  2163. type GenerateAuthURLRequest struct {
  2164. ProxyID *int64 `json:"proxy_id"`
  2165. }
  2166. // GenerateAuthURL generates OAuth authorization URL with full scope
  2167. // POST /api/v1/admin/accounts/generate-auth-url
  2168. func (h *OAuthHandler) GenerateAuthURL(c *gin.Context) {
  2169. var req GenerateAuthURLRequest
  2170. if err := c.ShouldBindJSON(&req); err != nil {
  2171. // Allow empty body
  2172. req = GenerateAuthURLRequest{}
  2173. }
  2174. result, err := h.oauthService.GenerateAuthURL(c.Request.Context(), req.ProxyID)
  2175. if err != nil {
  2176. response.ErrorFrom(c, err)
  2177. return
  2178. }
  2179. response.Success(c, result)
  2180. }
  2181. // GenerateSetupTokenURL generates OAuth authorization URL for setup token (inference only)
  2182. // POST /api/v1/admin/accounts/generate-setup-token-url
  2183. func (h *OAuthHandler) GenerateSetupTokenURL(c *gin.Context) {
  2184. var req GenerateAuthURLRequest
  2185. if err := c.ShouldBindJSON(&req); err != nil {
  2186. // Allow empty body
  2187. req = GenerateAuthURLRequest{}
  2188. }
  2189. result, err := h.oauthService.GenerateSetupTokenURL(c.Request.Context(), req.ProxyID)
  2190. if err != nil {
  2191. response.ErrorFrom(c, err)
  2192. return
  2193. }
  2194. response.Success(c, result)
  2195. }
  2196. // ExchangeCodeRequest represents the request for exchanging auth code
  2197. type ExchangeCodeRequest struct {
  2198. SessionID string `json:"session_id" binding:"required"`
  2199. Code string `json:"code" binding:"required"`
  2200. ProxyID *int64 `json:"proxy_id"`
  2201. }
  2202. // ExchangeCode exchanges authorization code for tokens
  2203. // POST /api/v1/admin/accounts/exchange-code
  2204. func (h *OAuthHandler) ExchangeCode(c *gin.Context) {
  2205. var req ExchangeCodeRequest
  2206. if err := c.ShouldBindJSON(&req); err != nil {
  2207. response.BadRequest(c, "Invalid request: "+err.Error())
  2208. return
  2209. }
  2210. tokenInfo, err := h.oauthService.ExchangeCode(c.Request.Context(), &service.ExchangeCodeInput{
  2211. SessionID: req.SessionID,
  2212. Code: req.Code,
  2213. ProxyID: req.ProxyID,
  2214. })
  2215. if err != nil {
  2216. response.ErrorFrom(c, err)
  2217. return
  2218. }
  2219. response.Success(c, tokenInfo)
  2220. }
  2221. // ExchangeSetupTokenCode exchanges authorization code for setup token
  2222. // POST /api/v1/admin/accounts/exchange-setup-token-code
  2223. func (h *OAuthHandler) ExchangeSetupTokenCode(c *gin.Context) {
  2224. var req ExchangeCodeRequest
  2225. if err := c.ShouldBindJSON(&req); err != nil {
  2226. response.BadRequest(c, "Invalid request: "+err.Error())
  2227. return
  2228. }
  2229. tokenInfo, err := h.oauthService.ExchangeCode(c.Request.Context(), &service.ExchangeCodeInput{
  2230. SessionID: req.SessionID,
  2231. Code: req.Code,
  2232. ProxyID: req.ProxyID,
  2233. })
  2234. if err != nil {
  2235. response.ErrorFrom(c, err)
  2236. return
  2237. }
  2238. response.Success(c, tokenInfo)
  2239. }
  2240. // CookieAuthRequest represents the request for cookie-based authentication
  2241. type CookieAuthRequest struct {
  2242. SessionKey string `json:"code" binding:"required"` // Using 'code' field as sessionKey (frontend sends it this way)
  2243. ProxyID *int64 `json:"proxy_id"`
  2244. }
  2245. // CookieAuth performs OAuth using sessionKey (cookie-based auto-auth)
  2246. // POST /api/v1/admin/accounts/cookie-auth
  2247. func (h *OAuthHandler) CookieAuth(c *gin.Context) {
  2248. var req CookieAuthRequest
  2249. if err := c.ShouldBindJSON(&req); err != nil {
  2250. response.BadRequest(c, "Invalid request: "+err.Error())
  2251. return
  2252. }
  2253. tokenInfo, err := h.oauthService.CookieAuth(c.Request.Context(), &service.CookieAuthInput{
  2254. SessionKey: req.SessionKey,
  2255. ProxyID: req.ProxyID,
  2256. Scope: "full",
  2257. })
  2258. if err != nil {
  2259. response.ErrorFrom(c, err)
  2260. return
  2261. }
  2262. response.Success(c, tokenInfo)
  2263. }
  2264. // SetupTokenCookieAuth performs OAuth using sessionKey for setup token (inference only)
  2265. // POST /api/v1/admin/accounts/setup-token-cookie-auth
  2266. func (h *OAuthHandler) SetupTokenCookieAuth(c *gin.Context) {
  2267. var req CookieAuthRequest
  2268. if err := c.ShouldBindJSON(&req); err != nil {
  2269. response.BadRequest(c, "Invalid request: "+err.Error())
  2270. return
  2271. }
  2272. tokenInfo, err := h.oauthService.CookieAuth(c.Request.Context(), &service.CookieAuthInput{
  2273. SessionKey: req.SessionKey,
  2274. ProxyID: req.ProxyID,
  2275. Scope: "inference",
  2276. })
  2277. if err != nil {
  2278. response.ErrorFrom(c, err)
  2279. return
  2280. }
  2281. response.Success(c, tokenInfo)
  2282. }
  2283. // GetUsage handles getting account usage information
  2284. // GET /api/v1/admin/accounts/:id/usage?source=passive|active&force=true
  2285. func (h *AccountHandler) GetUsage(c *gin.Context) {
  2286. accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  2287. if err != nil {
  2288. response.BadRequest(c, "Invalid account ID")
  2289. return
  2290. }
  2291. source := c.DefaultQuery("source", "active")
  2292. force := c.Query("force") == "true"
  2293. var usage *service.UsageInfo
  2294. if source == "passive" {
  2295. usage, err = h.accountUsageService.GetPassiveUsage(c.Request.Context(), accountID)
  2296. } else {
  2297. usage, err = h.accountUsageService.GetUsage(c.Request.Context(), accountID, force)
  2298. }
  2299. if err != nil {
  2300. response.ErrorFrom(c, err)
  2301. return
  2302. }
  2303. response.Success(c, usage)
  2304. }
  2305. // ClearRateLimit handles clearing account rate limit status
  2306. // POST /api/v1/admin/accounts/:id/clear-rate-limit
  2307. func (h *AccountHandler) ClearRateLimit(c *gin.Context) {
  2308. accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  2309. if err != nil {
  2310. response.BadRequest(c, "Invalid account ID")
  2311. return
  2312. }
  2313. err = h.rateLimitService.ClearRateLimit(c.Request.Context(), accountID)
  2314. if err != nil {
  2315. response.ErrorFrom(c, err)
  2316. return
  2317. }
  2318. account, err := h.adminService.GetAccount(c.Request.Context(), accountID)
  2319. if err != nil {
  2320. response.ErrorFrom(c, err)
  2321. return
  2322. }
  2323. response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), account))
  2324. }
  2325. // ResetQuota handles resetting account quota usage
  2326. // POST /api/v1/admin/accounts/:id/reset-quota
  2327. func (h *AccountHandler) ResetQuota(c *gin.Context) {
  2328. accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  2329. if err != nil {
  2330. response.BadRequest(c, "Invalid account ID")
  2331. return
  2332. }
  2333. if err := h.adminService.ResetAccountQuota(c.Request.Context(), accountID); err != nil {
  2334. response.ErrorFrom(c, err)
  2335. return
  2336. }
  2337. account, err := h.adminService.GetAccount(c.Request.Context(), accountID)
  2338. if err != nil {
  2339. response.ErrorFrom(c, err)
  2340. return
  2341. }
  2342. response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), account))
  2343. }
  2344. // GetTempUnschedulable handles getting temporary unschedulable status
  2345. // GET /api/v1/admin/accounts/:id/temp-unschedulable
  2346. func (h *AccountHandler) GetTempUnschedulable(c *gin.Context) {
  2347. accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  2348. if err != nil {
  2349. response.BadRequest(c, "Invalid account ID")
  2350. return
  2351. }
  2352. state, err := h.rateLimitService.GetTempUnschedStatus(c.Request.Context(), accountID)
  2353. if err != nil {
  2354. response.ErrorFrom(c, err)
  2355. return
  2356. }
  2357. if state == nil || state.UntilUnix <= time.Now().Unix() {
  2358. response.Success(c, gin.H{"active": false})
  2359. return
  2360. }
  2361. response.Success(c, gin.H{
  2362. "active": true,
  2363. "state": state,
  2364. })
  2365. }
  2366. // ClearTempUnschedulable handles clearing temporary unschedulable status
  2367. // DELETE /api/v1/admin/accounts/:id/temp-unschedulable
  2368. func (h *AccountHandler) ClearTempUnschedulable(c *gin.Context) {
  2369. accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  2370. if err != nil {
  2371. response.BadRequest(c, "Invalid account ID")
  2372. return
  2373. }
  2374. if err := h.rateLimitService.ClearTempUnschedulable(c.Request.Context(), accountID); err != nil {
  2375. response.ErrorFrom(c, err)
  2376. return
  2377. }
  2378. response.Success(c, gin.H{"message": "Temp unschedulable cleared successfully"})
  2379. }
  2380. // GetTodayStats handles getting account today statistics
  2381. // GET /api/v1/admin/accounts/:id/today-stats
  2382. func (h *AccountHandler) GetTodayStats(c *gin.Context) {
  2383. accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  2384. if err != nil {
  2385. response.BadRequest(c, "Invalid account ID")
  2386. return
  2387. }
  2388. stats, err := h.accountUsageService.GetTodayStats(c.Request.Context(), accountID)
  2389. if err != nil {
  2390. response.ErrorFrom(c, err)
  2391. return
  2392. }
  2393. response.Success(c, stats)
  2394. }
  2395. // BatchTodayStatsRequest 批量今日统计请求体。
  2396. type BatchTodayStatsRequest struct {
  2397. AccountIDs []int64 `json:"account_ids" binding:"required"`
  2398. }
  2399. type BatchUsageRequest struct {
  2400. AccountIDs []int64 `json:"account_ids" binding:"required"`
  2401. Force bool `json:"force"`
  2402. }
  2403. // GetBatchTodayStats 批量获取多个账号的今日统计。
  2404. // POST /api/v1/admin/accounts/today-stats/batch
  2405. func (h *AccountHandler) GetBatchTodayStats(c *gin.Context) {
  2406. var req BatchTodayStatsRequest
  2407. if err := c.ShouldBindJSON(&req); err != nil {
  2408. response.BadRequest(c, "Invalid request: "+err.Error())
  2409. return
  2410. }
  2411. accountIDs := normalizeInt64IDList(req.AccountIDs)
  2412. if len(accountIDs) == 0 {
  2413. response.Success(c, gin.H{"stats": map[string]any{}})
  2414. return
  2415. }
  2416. cacheKey := buildAccountTodayStatsBatchCacheKey(accountIDs)
  2417. if cached, ok := accountTodayStatsBatchCache.Get(cacheKey); ok {
  2418. if cached.ETag != "" {
  2419. c.Header("ETag", cached.ETag)
  2420. c.Header("Vary", "If-None-Match")
  2421. if ifNoneMatchMatched(c.GetHeader("If-None-Match"), cached.ETag) {
  2422. c.Status(http.StatusNotModified)
  2423. return
  2424. }
  2425. }
  2426. c.Header("X-Snapshot-Cache", "hit")
  2427. response.Success(c, cached.Payload)
  2428. return
  2429. }
  2430. stats, err := h.accountUsageService.GetTodayStatsBatch(c.Request.Context(), accountIDs)
  2431. if err != nil {
  2432. response.ErrorFrom(c, err)
  2433. return
  2434. }
  2435. payload := gin.H{"stats": stats}
  2436. cached := accountTodayStatsBatchCache.Set(cacheKey, payload)
  2437. if cached.ETag != "" {
  2438. c.Header("ETag", cached.ETag)
  2439. c.Header("Vary", "If-None-Match")
  2440. }
  2441. c.Header("X-Snapshot-Cache", "miss")
  2442. response.Success(c, payload)
  2443. }
  2444. // GetBatchUsage 批量获取多个账号的 current usage。
  2445. // POST /api/v1/admin/accounts/usage/batch
  2446. func (h *AccountHandler) GetBatchUsage(c *gin.Context) {
  2447. var req BatchUsageRequest
  2448. if err := c.ShouldBindJSON(&req); err != nil {
  2449. response.BadRequest(c, "Invalid request: "+err.Error())
  2450. return
  2451. }
  2452. accountIDs := normalizeInt64IDList(req.AccountIDs)
  2453. if len(accountIDs) == 0 {
  2454. response.Success(c, gin.H{
  2455. "usage": map[string]any{},
  2456. "errors": map[string]string{},
  2457. })
  2458. return
  2459. }
  2460. usageByAccount, errorsByAccount, err := h.accountUsageService.GetUsageBatch(c.Request.Context(), accountIDs, req.Force)
  2461. if err != nil {
  2462. response.ErrorFrom(c, err)
  2463. return
  2464. }
  2465. response.Success(c, gin.H{
  2466. "usage": usageByAccount,
  2467. "errors": errorsByAccount,
  2468. })
  2469. }
  2470. // SetSchedulableRequest represents the request body for setting schedulable status
  2471. type SetSchedulableRequest struct {
  2472. Schedulable bool `json:"schedulable"`
  2473. }
  2474. // SetSchedulable handles toggling account schedulable status
  2475. // POST /api/v1/admin/accounts/:id/schedulable
  2476. func (h *AccountHandler) SetSchedulable(c *gin.Context) {
  2477. accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  2478. if err != nil {
  2479. response.BadRequest(c, "Invalid account ID")
  2480. return
  2481. }
  2482. var req SetSchedulableRequest
  2483. if err := c.ShouldBindJSON(&req); err != nil {
  2484. response.BadRequest(c, "Invalid request: "+err.Error())
  2485. return
  2486. }
  2487. account, err := h.adminService.SetAccountSchedulable(c.Request.Context(), accountID, req.Schedulable)
  2488. if err != nil {
  2489. response.ErrorFrom(c, err)
  2490. return
  2491. }
  2492. response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), account))
  2493. }
  2494. // GetAvailableModels handles getting available models for an account
  2495. // GET /api/v1/admin/accounts/:id/models
  2496. func (h *AccountHandler) GetAvailableModels(c *gin.Context) {
  2497. accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  2498. if err != nil {
  2499. response.BadRequest(c, "Invalid account ID")
  2500. return
  2501. }
  2502. account, err := h.adminService.GetAccount(c.Request.Context(), accountID)
  2503. if err != nil {
  2504. response.NotFound(c, "Account not found")
  2505. return
  2506. }
  2507. // Handle OpenAI accounts
  2508. if account.IsOpenAI() {
  2509. // Prefer the shared, account-keyed upstream catalog. If discovery fails,
  2510. // retain the legacy local catalog below so the test dialog remains usable.
  2511. if h.accountTestService != nil {
  2512. if models, fetchErr := h.accountTestService.FetchOpenAIAccountModels(c.Request.Context(), account); fetchErr == nil {
  2513. response.Success(c, models)
  2514. return
  2515. }
  2516. }
  2517. // OpenAI 自动透传会绕过常规模型改写,测试/模型列表也应回落到默认模型集。
  2518. if account.IsOpenAIPassthroughEnabled() {
  2519. response.Success(c, openai.DefaultModels)
  2520. return
  2521. }
  2522. mapping := account.GetModelMapping()
  2523. if len(mapping) == 0 {
  2524. response.Success(c, openai.DefaultModels)
  2525. return
  2526. }
  2527. // Return mapped models
  2528. var models []openai.Model
  2529. for requestedModel := range mapping {
  2530. var found bool
  2531. for _, dm := range openai.DefaultModels {
  2532. if dm.ID == requestedModel {
  2533. models = append(models, dm)
  2534. found = true
  2535. break
  2536. }
  2537. }
  2538. if !found {
  2539. models = append(models, openai.Model{
  2540. ID: requestedModel,
  2541. Object: "model",
  2542. Type: "model",
  2543. DisplayName: requestedModel,
  2544. })
  2545. }
  2546. }
  2547. response.Success(c, models)
  2548. return
  2549. }
  2550. // Handle Gemini accounts
  2551. if account.IsGemini() {
  2552. // Consumer Google One OAuth still uses the legacy Gemini CLI / Code
  2553. // Assist channel. Do not advertise newer 3.x or image models that the
  2554. // channel cannot serve.
  2555. if account.IsOAuth() {
  2556. if account.IsGeminiGoogleOne() {
  2557. response.Success(c, geminicli.GoogleOneModels)
  2558. return
  2559. }
  2560. response.Success(c, geminicli.DefaultModels)
  2561. return
  2562. }
  2563. // For API Key accounts: return models based on model_mapping
  2564. mapping := account.GetModelMapping()
  2565. if len(mapping) == 0 {
  2566. response.Success(c, geminicli.DefaultModels)
  2567. return
  2568. }
  2569. var models []geminicli.Model
  2570. for requestedModel := range mapping {
  2571. var found bool
  2572. for _, dm := range geminicli.DefaultModels {
  2573. if dm.ID == requestedModel {
  2574. models = append(models, dm)
  2575. found = true
  2576. break
  2577. }
  2578. }
  2579. if !found {
  2580. models = append(models, geminicli.Model{
  2581. ID: requestedModel,
  2582. Type: "model",
  2583. DisplayName: requestedModel,
  2584. CreatedAt: "",
  2585. })
  2586. }
  2587. }
  2588. response.Success(c, models)
  2589. return
  2590. }
  2591. // Handle Antigravity accounts: return Claude + Gemini models
  2592. if account.Platform == service.PlatformAntigravity {
  2593. // 直接复用 antigravity.DefaultModels(),与 /v1/models 端点保持同步
  2594. response.Success(c, antigravity.DefaultModels())
  2595. return
  2596. }
  2597. // Handle Grok accounts
  2598. if account.Platform == service.PlatformGrok {
  2599. defaultModels := xai.DefaultModels()
  2600. hasExplicitMapping := false
  2601. switch rawMapping := account.Credentials["model_mapping"].(type) {
  2602. case map[string]any:
  2603. hasExplicitMapping = len(rawMapping) > 0
  2604. case map[string]string:
  2605. hasExplicitMapping = len(rawMapping) > 0
  2606. }
  2607. if !hasExplicitMapping {
  2608. response.Success(c, defaultModels)
  2609. return
  2610. }
  2611. mapping := account.GetModelMapping()
  2612. if len(mapping) == 0 {
  2613. response.Success(c, defaultModels)
  2614. return
  2615. }
  2616. defaultByID := make(map[string]xai.Model, len(defaultModels))
  2617. for _, model := range defaultModels {
  2618. defaultByID[model.ID] = model
  2619. }
  2620. requestedModels := make([]string, 0, len(mapping))
  2621. for requestedModel := range mapping {
  2622. requestedModels = append(requestedModels, requestedModel)
  2623. }
  2624. sort.Strings(requestedModels)
  2625. var models []xai.Model
  2626. for _, requestedModel := range requestedModels {
  2627. if defaultModel, found := defaultByID[requestedModel]; found {
  2628. models = append(models, defaultModel)
  2629. continue
  2630. }
  2631. models = append(models, xai.Model{
  2632. ID: requestedModel,
  2633. Object: "model",
  2634. OwnedBy: "xai",
  2635. DisplayName: requestedModel,
  2636. })
  2637. }
  2638. response.Success(c, models)
  2639. return
  2640. }
  2641. // Handle Claude/Anthropic accounts
  2642. // For OAuth and Setup-Token accounts: return default models
  2643. if account.IsOAuth() {
  2644. response.Success(c, claude.DefaultModels)
  2645. return
  2646. }
  2647. // For API Key accounts: return models based on model_mapping
  2648. mapping := account.GetModelMapping()
  2649. if len(mapping) == 0 {
  2650. // No mapping configured, return default models
  2651. response.Success(c, claude.DefaultModels)
  2652. return
  2653. }
  2654. // Return mapped models (keys of the mapping are the available model IDs)
  2655. var models []claude.Model
  2656. for requestedModel := range mapping {
  2657. // Try to find display info from default models
  2658. var found bool
  2659. for _, dm := range claude.DefaultModels {
  2660. if dm.ID == requestedModel {
  2661. models = append(models, dm)
  2662. found = true
  2663. break
  2664. }
  2665. }
  2666. // If not found in defaults, create a basic entry
  2667. if !found {
  2668. models = append(models, claude.Model{
  2669. ID: requestedModel,
  2670. Type: "model",
  2671. DisplayName: requestedModel,
  2672. CreatedAt: "",
  2673. })
  2674. }
  2675. }
  2676. response.Success(c, models)
  2677. }
  2678. // SyncUpstreamModels handles syncing live supported models from an account's upstream.
  2679. // POST /api/v1/admin/accounts/:id/models/sync-upstream
  2680. func (h *AccountHandler) SyncUpstreamModels(c *gin.Context) {
  2681. accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  2682. if err != nil {
  2683. response.BadRequest(c, "Invalid account ID")
  2684. return
  2685. }
  2686. account, err := h.adminService.GetAccount(c.Request.Context(), accountID)
  2687. if err != nil {
  2688. response.NotFound(c, "Account not found")
  2689. return
  2690. }
  2691. if h.accountTestService == nil {
  2692. response.InternalError(c, "Account test service is not configured")
  2693. return
  2694. }
  2695. catalog, err := h.accountTestService.SyncUpstreamModelCatalog(c.Request.Context(), account)
  2696. if err != nil {
  2697. var syncErr *service.UpstreamModelSyncError
  2698. if errors.As(err, &syncErr) {
  2699. switch syncErr.Kind {
  2700. case service.UpstreamModelSyncErrorConfiguration, service.UpstreamModelSyncErrorUnsupported:
  2701. response.BadRequest(c, syncErr.SafeMessage())
  2702. case service.UpstreamModelSyncErrorInternal:
  2703. response.InternalError(c, syncErr.SafeMessage())
  2704. default:
  2705. slog.Warn("sync_upstream_models_failed", "account_id", accountID, "kind", syncErr.Kind)
  2706. response.Error(c, http.StatusBadGateway, syncErr.SafeMessage())
  2707. }
  2708. return
  2709. }
  2710. slog.Warn("sync_upstream_models_failed", "account_id", accountID)
  2711. response.Error(c, http.StatusBadGateway, "Failed to sync upstream models from upstream")
  2712. return
  2713. }
  2714. response.Success(c, catalog)
  2715. }
  2716. // SyncUpstreamModelsPreview handles syncing live supported models using provided credentials (no account ID needed).
  2717. // POST /api/v1/admin/accounts/models/sync-upstream-preview
  2718. func (h *AccountHandler) SyncUpstreamModelsPreview(c *gin.Context) {
  2719. var req struct {
  2720. Platform string `json:"platform" binding:"required"`
  2721. Type string `json:"type" binding:"required"`
  2722. BaseURL string `json:"base_url"`
  2723. APIKey string `json:"api_key" binding:"required"`
  2724. ModelMapping map[string]string `json:"model_mapping"`
  2725. }
  2726. if err := c.ShouldBindJSON(&req); err != nil {
  2727. response.BadRequest(c, "Invalid request: "+err.Error())
  2728. return
  2729. }
  2730. modelMapping := make(map[string]any, len(req.ModelMapping))
  2731. for sourceModel, upstreamModel := range req.ModelMapping {
  2732. modelMapping[sourceModel] = upstreamModel
  2733. }
  2734. tempAccount := &service.Account{
  2735. Platform: req.Platform,
  2736. Type: req.Type,
  2737. Credentials: map[string]any{
  2738. "api_key": req.APIKey,
  2739. "base_url": req.BaseURL,
  2740. "model_mapping": modelMapping,
  2741. },
  2742. }
  2743. if h.accountTestService == nil {
  2744. response.InternalError(c, "Account test service is not configured")
  2745. return
  2746. }
  2747. catalog, err := h.accountTestService.SyncUpstreamModelCatalog(c.Request.Context(), tempAccount)
  2748. if err != nil {
  2749. var syncErr *service.UpstreamModelSyncError
  2750. if errors.As(err, &syncErr) {
  2751. switch syncErr.Kind {
  2752. case service.UpstreamModelSyncErrorConfiguration, service.UpstreamModelSyncErrorUnsupported:
  2753. response.BadRequest(c, syncErr.SafeMessage())
  2754. case service.UpstreamModelSyncErrorInternal:
  2755. response.InternalError(c, syncErr.SafeMessage())
  2756. default:
  2757. slog.Warn("sync_upstream_models_preview_failed", "platform", req.Platform, "kind", syncErr.Kind)
  2758. response.Error(c, http.StatusBadGateway, syncErr.SafeMessage())
  2759. }
  2760. return
  2761. }
  2762. slog.Warn("sync_upstream_models_preview_failed", "platform", req.Platform)
  2763. response.Error(c, http.StatusBadGateway, "Failed to sync upstream models from upstream")
  2764. return
  2765. }
  2766. response.Success(c, catalog)
  2767. }
  2768. // SetPrivacy handles setting privacy for a single OpenAI/Antigravity OAuth account
  2769. // POST /api/v1/admin/accounts/:id/set-privacy
  2770. func (h *AccountHandler) SetPrivacy(c *gin.Context) {
  2771. accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  2772. if err != nil {
  2773. response.BadRequest(c, "Invalid account ID")
  2774. return
  2775. }
  2776. account, err := h.adminService.GetAccount(c.Request.Context(), accountID)
  2777. if err != nil {
  2778. response.NotFound(c, "Account not found")
  2779. return
  2780. }
  2781. if account.Type != service.AccountTypeOAuth {
  2782. response.BadRequest(c, "Only OAuth accounts support privacy setting")
  2783. return
  2784. }
  2785. var mode string
  2786. switch account.Platform {
  2787. case service.PlatformOpenAI:
  2788. mode = h.adminService.ForceOpenAIPrivacy(c.Request.Context(), account)
  2789. case service.PlatformAntigravity:
  2790. mode = h.adminService.ForceAntigravityPrivacy(c.Request.Context(), account)
  2791. default:
  2792. response.BadRequest(c, "Only OpenAI and Antigravity OAuth accounts support privacy setting")
  2793. return
  2794. }
  2795. if mode == "" {
  2796. response.BadRequest(c, "Cannot set privacy: missing access_token")
  2797. return
  2798. }
  2799. // 从 DB 重新读取以确保返回最新状态
  2800. updated, err := h.adminService.GetAccount(c.Request.Context(), accountID)
  2801. if err != nil {
  2802. // 隐私已设置成功但读取失败,回退到内存更新
  2803. if account.Extra == nil {
  2804. account.Extra = make(map[string]any)
  2805. }
  2806. account.Extra["privacy_mode"] = mode
  2807. response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), account))
  2808. return
  2809. }
  2810. response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), updated))
  2811. }
  2812. // RefreshTier handles refreshing Google One tier for a single account
  2813. // POST /api/v1/admin/accounts/:id/refresh-tier
  2814. func (h *AccountHandler) RefreshTier(c *gin.Context) {
  2815. accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  2816. if err != nil {
  2817. response.BadRequest(c, "Invalid account ID")
  2818. return
  2819. }
  2820. ctx := c.Request.Context()
  2821. account, err := h.adminService.GetAccount(ctx, accountID)
  2822. if err != nil {
  2823. response.NotFound(c, "Account not found")
  2824. return
  2825. }
  2826. if account.Platform != service.PlatformGemini || account.Type != service.AccountTypeOAuth {
  2827. response.BadRequest(c, "Only Gemini OAuth accounts support tier refresh")
  2828. return
  2829. }
  2830. oauthType, _ := account.Credentials["oauth_type"].(string)
  2831. if oauthType != "google_one" {
  2832. response.BadRequest(c, "Only google_one OAuth accounts support tier refresh")
  2833. return
  2834. }
  2835. tierID, extra, creds, err := h.geminiOAuthService.RefreshAccountGoogleOneTier(ctx, account)
  2836. if err != nil {
  2837. response.ErrorFrom(c, err)
  2838. return
  2839. }
  2840. _, updateErr := h.adminService.UpdateAccount(ctx, accountID, &service.UpdateAccountInput{
  2841. Credentials: creds,
  2842. Extra: extra,
  2843. })
  2844. if updateErr != nil {
  2845. response.ErrorFrom(c, updateErr)
  2846. return
  2847. }
  2848. response.Success(c, gin.H{
  2849. "tier_id": tierID,
  2850. "storage_info": extra,
  2851. "drive_storage_limit": extra["drive_storage_limit"],
  2852. "drive_storage_usage": extra["drive_storage_usage"],
  2853. "updated_at": extra["drive_tier_updated_at"],
  2854. })
  2855. }
  2856. // BatchRefreshTierRequest represents batch tier refresh request
  2857. type BatchRefreshTierRequest struct {
  2858. AccountIDs []int64 `json:"account_ids"`
  2859. }
  2860. // BatchRefreshTier handles batch refreshing Google One tier
  2861. // POST /api/v1/admin/accounts/batch-refresh-tier
  2862. func (h *AccountHandler) BatchRefreshTier(c *gin.Context) {
  2863. var req BatchRefreshTierRequest
  2864. if err := c.ShouldBindJSON(&req); err != nil {
  2865. req = BatchRefreshTierRequest{}
  2866. }
  2867. ctx := c.Request.Context()
  2868. accounts := make([]*service.Account, 0)
  2869. if len(req.AccountIDs) == 0 {
  2870. allAccounts, _, err := h.adminService.ListAccounts(ctx, 1, 10000, "gemini", "oauth", "", "", 0, "", "name", "asc")
  2871. if err != nil {
  2872. response.ErrorFrom(c, err)
  2873. return
  2874. }
  2875. for i := range allAccounts {
  2876. acc := &allAccounts[i]
  2877. oauthType, _ := acc.Credentials["oauth_type"].(string)
  2878. if oauthType == "google_one" {
  2879. accounts = append(accounts, acc)
  2880. }
  2881. }
  2882. } else {
  2883. fetched, err := h.adminService.GetAccountsByIDs(ctx, req.AccountIDs)
  2884. if err != nil {
  2885. response.ErrorFrom(c, err)
  2886. return
  2887. }
  2888. for _, acc := range fetched {
  2889. if acc == nil {
  2890. continue
  2891. }
  2892. if acc.Platform != service.PlatformGemini || acc.Type != service.AccountTypeOAuth {
  2893. continue
  2894. }
  2895. oauthType, _ := acc.Credentials["oauth_type"].(string)
  2896. if oauthType != "google_one" {
  2897. continue
  2898. }
  2899. accounts = append(accounts, acc)
  2900. }
  2901. }
  2902. const maxConcurrency = 10
  2903. g, gctx := errgroup.WithContext(ctx)
  2904. g.SetLimit(maxConcurrency)
  2905. var mu sync.Mutex
  2906. var successCount, failedCount int
  2907. var errors []gin.H
  2908. for _, account := range accounts {
  2909. acc := account // 闭包捕获
  2910. g.Go(func() error {
  2911. _, extra, creds, err := h.geminiOAuthService.RefreshAccountGoogleOneTier(gctx, acc)
  2912. if err != nil {
  2913. mu.Lock()
  2914. failedCount++
  2915. errors = append(errors, gin.H{
  2916. "account_id": acc.ID,
  2917. "error": err.Error(),
  2918. })
  2919. mu.Unlock()
  2920. return nil
  2921. }
  2922. _, updateErr := h.adminService.UpdateAccount(gctx, acc.ID, &service.UpdateAccountInput{
  2923. Credentials: creds,
  2924. Extra: extra,
  2925. })
  2926. mu.Lock()
  2927. if updateErr != nil {
  2928. failedCount++
  2929. errors = append(errors, gin.H{
  2930. "account_id": acc.ID,
  2931. "error": updateErr.Error(),
  2932. })
  2933. } else {
  2934. successCount++
  2935. }
  2936. mu.Unlock()
  2937. return nil
  2938. })
  2939. }
  2940. if err := g.Wait(); err != nil {
  2941. response.ErrorFrom(c, err)
  2942. return
  2943. }
  2944. results := gin.H{
  2945. "total": len(accounts),
  2946. "success": successCount,
  2947. "failed": failedCount,
  2948. "errors": errors,
  2949. }
  2950. response.Success(c, results)
  2951. }
  2952. // GetAntigravityDefaultModelMapping 获取 Antigravity 平台的默认模型映射
  2953. // GET /api/v1/admin/accounts/antigravity/default-model-mapping
  2954. func (h *AccountHandler) GetAntigravityDefaultModelMapping(c *gin.Context) {
  2955. response.Success(c, domain.DefaultAntigravityModelMapping)
  2956. }
  2957. // sanitizeExtraBaseRPM 对 extra map 中的 base_rpm 值进行范围校验和归一化。
  2958. // 负值归零,超过 10000 截断为 10000。extra 为 nil 或不含 base_rpm 时无操作。
  2959. func sanitizeExtraBaseRPM(extra map[string]any) {
  2960. if extra == nil {
  2961. return
  2962. }
  2963. raw, ok := extra["base_rpm"]
  2964. if !ok {
  2965. return
  2966. }
  2967. v := service.ParseExtraInt(raw)
  2968. if v < 0 {
  2969. v = 0
  2970. } else if v > 10000 {
  2971. v = 10000
  2972. }
  2973. extra["base_rpm"] = v
  2974. }