grok_oauth_handler.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. package admin
  2. import (
  3. "context"
  4. "fmt"
  5. "log/slog"
  6. "strconv"
  7. "strings"
  8. "sync"
  9. "time"
  10. "github.com/Wei-Shaw/sub2api/internal/handler/dto"
  11. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  12. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  13. "github.com/Wei-Shaw/sub2api/internal/pkg/xai"
  14. "github.com/Wei-Shaw/sub2api/internal/service"
  15. "github.com/gin-gonic/gin"
  16. )
  17. const grokSSOImportConcurrency = 3
  18. type GrokOAuthHandler struct {
  19. grokOAuthService *service.GrokOAuthService
  20. adminService service.AdminService
  21. quotaService *service.GrokQuotaService
  22. importProber grokImportProber
  23. reconciler service.GrokOAuthReconciler
  24. }
  25. func NewGrokOAuthHandler(
  26. grokOAuthService *service.GrokOAuthService,
  27. adminService service.AdminService,
  28. quotaService *service.GrokQuotaService,
  29. reconciler service.GrokOAuthReconciler,
  30. ) *GrokOAuthHandler {
  31. return &GrokOAuthHandler{
  32. grokOAuthService: grokOAuthService,
  33. adminService: adminService,
  34. quotaService: quotaService,
  35. importProber: quotaService,
  36. reconciler: reconciler,
  37. }
  38. }
  39. type GrokGenerateAuthURLRequest struct {
  40. ProxyID *int64 `json:"proxy_id"`
  41. RedirectURI string `json:"redirect_uri"`
  42. }
  43. func (h *GrokOAuthHandler) GetCapabilities(c *gin.Context) {
  44. response.Success(c, h.grokOAuthService.GetCapabilities())
  45. }
  46. func (h *GrokOAuthHandler) GenerateAuthURL(c *gin.Context) {
  47. var req GrokGenerateAuthURLRequest
  48. if err := c.ShouldBindJSON(&req); err != nil {
  49. req = GrokGenerateAuthURLRequest{}
  50. }
  51. result, err := h.grokOAuthService.GenerateAuthURL(c.Request.Context(), req.ProxyID, req.RedirectURI)
  52. if err != nil {
  53. response.ErrorFrom(c, err)
  54. return
  55. }
  56. response.Success(c, result)
  57. }
  58. type GrokExchangeCodeRequest struct {
  59. SessionID string `json:"session_id" binding:"required"`
  60. Code string `json:"code" binding:"required"`
  61. State string `json:"state"`
  62. RedirectURI string `json:"redirect_uri"`
  63. ProxyID *int64 `json:"proxy_id"`
  64. }
  65. func (h *GrokOAuthHandler) ExchangeCode(c *gin.Context) {
  66. var req GrokExchangeCodeRequest
  67. if err := c.ShouldBindJSON(&req); err != nil {
  68. response.BadRequest(c, "Invalid request: "+err.Error())
  69. return
  70. }
  71. tokenInfo, err := h.grokOAuthService.ExchangeCode(c.Request.Context(), &service.GrokExchangeCodeInput{
  72. SessionID: req.SessionID,
  73. Code: req.Code,
  74. State: req.State,
  75. RedirectURI: req.RedirectURI,
  76. ProxyID: req.ProxyID,
  77. })
  78. if err != nil {
  79. response.ErrorFrom(c, err)
  80. return
  81. }
  82. response.Success(c, tokenInfo)
  83. }
  84. type GrokRefreshTokenRequest struct {
  85. RefreshToken string `json:"refresh_token"`
  86. RT string `json:"rt"`
  87. ClientID string `json:"client_id"`
  88. ProxyID *int64 `json:"proxy_id"`
  89. }
  90. type GrokSSOTokenRequest struct {
  91. SSOToken string `json:"sso_token"`
  92. ProxyID *int64 `json:"proxy_id"`
  93. }
  94. type GrokPasswordAuthorizeRequest struct {
  95. Email string `json:"email"`
  96. Password string `json:"password"`
  97. ProxyID *int64 `json:"proxy_id"`
  98. }
  99. func (h *GrokOAuthHandler) RefreshToken(c *gin.Context) {
  100. var req GrokRefreshTokenRequest
  101. if err := c.ShouldBindJSON(&req); err != nil {
  102. response.BadRequest(c, "Invalid request: "+err.Error())
  103. return
  104. }
  105. refreshToken := strings.TrimSpace(req.RefreshToken)
  106. if refreshToken == "" {
  107. refreshToken = strings.TrimSpace(req.RT)
  108. }
  109. if refreshToken == "" {
  110. response.BadRequest(c, "refresh_token is required")
  111. return
  112. }
  113. var proxyURL string
  114. if req.ProxyID != nil {
  115. proxy, err := h.adminService.GetProxy(c.Request.Context(), *req.ProxyID)
  116. if err != nil {
  117. response.ErrorFrom(c, err)
  118. return
  119. }
  120. if proxy == nil {
  121. response.BadRequest(c, "GROK_OAUTH_PROXY_NOT_FOUND: proxy not found")
  122. return
  123. }
  124. proxyURL = proxy.URL()
  125. }
  126. tokenInfo, err := h.grokOAuthService.RefreshToken(c.Request.Context(), refreshToken, proxyURL, req.ClientID)
  127. if err != nil {
  128. response.ErrorFrom(c, err)
  129. return
  130. }
  131. response.Success(c, tokenInfo)
  132. }
  133. // ValidateSSOToken converts a Web SSO cookie into Build OAuth tokens.
  134. // Response contains OAuth token info only — never echoes sso_token.
  135. func (h *GrokOAuthHandler) ValidateSSOToken(c *gin.Context) {
  136. var req GrokSSOTokenRequest
  137. if err := c.ShouldBindJSON(&req); err != nil {
  138. response.BadRequest(c, "Invalid request: "+err.Error())
  139. return
  140. }
  141. tokenInfo, err := h.grokOAuthService.ValidateSSOToken(c.Request.Context(), req.SSOToken, req.ProxyID)
  142. if err != nil {
  143. response.ErrorFrom(c, err)
  144. return
  145. }
  146. response.Success(c, tokenInfo)
  147. }
  148. // AuthorizePassword exchanges email/password for Build OAuth tokens via SSO conversion.
  149. // Response never includes password or raw sso_token.
  150. func (h *GrokOAuthHandler) AuthorizePassword(c *gin.Context) {
  151. var req GrokPasswordAuthorizeRequest
  152. if err := c.ShouldBindJSON(&req); err != nil {
  153. response.BadRequest(c, "Invalid request: "+err.Error())
  154. return
  155. }
  156. tokenInfo, err := h.grokOAuthService.AuthorizePassword(c.Request.Context(), req.Email, req.Password, req.ProxyID)
  157. if err != nil {
  158. response.ErrorFrom(c, err)
  159. return
  160. }
  161. response.Success(c, tokenInfo)
  162. }
  163. func (h *GrokOAuthHandler) RefreshAccountToken(c *gin.Context) {
  164. accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  165. if err != nil {
  166. response.BadRequest(c, "Invalid account ID")
  167. return
  168. }
  169. account, err := h.adminService.GetAccount(c.Request.Context(), accountID)
  170. if err != nil {
  171. response.ErrorFrom(c, err)
  172. return
  173. }
  174. if account.Platform != service.PlatformGrok {
  175. response.BadRequest(c, "Account platform does not match Grok OAuth endpoint")
  176. return
  177. }
  178. if !account.IsOAuth() {
  179. response.BadRequest(c, "Cannot refresh non-OAuth account credentials")
  180. return
  181. }
  182. tokenInfo, err := h.grokOAuthService.RefreshAccountToken(c.Request.Context(), account)
  183. if err != nil {
  184. response.ErrorFrom(c, err)
  185. return
  186. }
  187. newCredentials := h.grokOAuthService.BuildAccountCredentials(tokenInfo)
  188. newCredentials = service.MergeCredentials(account.Credentials, newCredentials)
  189. if baseURL := strings.TrimSpace(account.GetCredential("base_url")); baseURL != "" {
  190. newCredentials["base_url"] = baseURL
  191. }
  192. updatedAccount, err := h.adminService.UpdateAccount(c.Request.Context(), accountID, &service.UpdateAccountInput{
  193. Credentials: newCredentials,
  194. })
  195. if err != nil {
  196. response.ErrorFrom(c, err)
  197. return
  198. }
  199. response.Success(c, dto.AccountFromService(updatedAccount))
  200. }
  201. type GrokOAuthReconcileRequest struct {
  202. DryRun *bool `json:"dry_run"`
  203. Apply bool `json:"apply"`
  204. AfterID int64 `json:"after_id"`
  205. Limit int `json:"limit"`
  206. RefreshWindowSeconds int64 `json:"refresh_window_seconds"`
  207. }
  208. func (h *GrokOAuthHandler) ReconcileOAuthAccounts(c *gin.Context) {
  209. var req GrokOAuthReconcileRequest
  210. if err := c.ShouldBindJSON(&req); err != nil {
  211. response.BadRequest(c, "Invalid request")
  212. return
  213. }
  214. dryRun := true
  215. if req.DryRun != nil {
  216. dryRun = *req.DryRun
  217. }
  218. if req.Apply == dryRun {
  219. response.ErrorFrom(c, service.ErrGrokOAuthReconcileMode)
  220. return
  221. }
  222. if req.RefreshWindowSeconds < 0 || req.RefreshWindowSeconds > int64((24*time.Hour)/time.Second) {
  223. response.ErrorFrom(c, service.ErrGrokOAuthReconcileWindow)
  224. return
  225. }
  226. if h.reconciler == nil {
  227. response.InternalError(c, "Grok OAuth reconciliation service is unavailable")
  228. return
  229. }
  230. result, err := h.reconciler.ReconcileGrokOAuth(c.Request.Context(), service.GrokOAuthReconcileInput{
  231. DryRun: dryRun,
  232. Apply: req.Apply,
  233. AfterID: req.AfterID,
  234. Limit: req.Limit,
  235. RefreshWindow: time.Duration(req.RefreshWindowSeconds) * time.Second,
  236. })
  237. if err != nil {
  238. response.ErrorFrom(c, err)
  239. return
  240. }
  241. response.Success(c, result)
  242. }
  243. func (h *GrokOAuthHandler) CreateAccountFromOAuth(c *gin.Context) {
  244. var req struct {
  245. SessionID string `json:"session_id" binding:"required"`
  246. Code string `json:"code" binding:"required"`
  247. State string `json:"state"`
  248. RedirectURI string `json:"redirect_uri"`
  249. ProxyID *int64 `json:"proxy_id"`
  250. Name string `json:"name"`
  251. Concurrency int `json:"concurrency"`
  252. Priority int `json:"priority"`
  253. GroupIDs []int64 `json:"group_ids"`
  254. }
  255. if err := c.ShouldBindJSON(&req); err != nil {
  256. response.BadRequest(c, "Invalid request: "+err.Error())
  257. return
  258. }
  259. tokenInfo, err := h.grokOAuthService.ExchangeCode(c.Request.Context(), &service.GrokExchangeCodeInput{
  260. SessionID: req.SessionID,
  261. Code: req.Code,
  262. State: req.State,
  263. RedirectURI: req.RedirectURI,
  264. ProxyID: req.ProxyID,
  265. })
  266. if err != nil {
  267. response.ErrorFrom(c, err)
  268. return
  269. }
  270. credentials := h.grokOAuthService.BuildAccountCredentials(tokenInfo)
  271. name := strings.TrimSpace(req.Name)
  272. if name == "" && tokenInfo.Email != "" {
  273. name = tokenInfo.Email
  274. }
  275. if name == "" {
  276. name = "Grok OAuth Account"
  277. }
  278. account, err := h.adminService.CreateAccount(c.Request.Context(), &service.CreateAccountInput{
  279. Name: name,
  280. Platform: service.PlatformGrok,
  281. Type: service.AccountTypeOAuth,
  282. Credentials: credentials,
  283. ProxyID: req.ProxyID,
  284. Concurrency: req.Concurrency,
  285. Priority: req.Priority,
  286. GroupIDs: req.GroupIDs,
  287. })
  288. if err != nil {
  289. response.ErrorFrom(c, err)
  290. return
  291. }
  292. h.scheduleGrokImportProbe(account)
  293. response.Success(c, dto.AccountFromService(account))
  294. }
  295. type GrokSSOToOAuthRequest struct {
  296. SSOTokens []string `json:"sso_tokens"`
  297. SSOToken string `json:"sso_token"`
  298. Name string `json:"name"`
  299. Notes *string `json:"notes"`
  300. ProxyID *int64 `json:"proxy_id"`
  301. GroupIDs []int64 `json:"group_ids"`
  302. Credentials map[string]any `json:"credentials"`
  303. Extra map[string]any `json:"extra"`
  304. Concurrency int `json:"concurrency"`
  305. LoadFactor *int `json:"load_factor"`
  306. Priority int `json:"priority"`
  307. RateMultiplier *float64 `json:"rate_multiplier"`
  308. ExpiresAt *int64 `json:"expires_at"`
  309. AutoPauseOnExpired *bool `json:"auto_pause_on_expired"`
  310. }
  311. type GrokSSOToOAuthItemResult struct {
  312. Index int `json:"index"`
  313. Name string `json:"name,omitempty"`
  314. Email string `json:"email,omitempty"`
  315. Account *dto.Account `json:"account,omitempty"`
  316. Error string `json:"error,omitempty"`
  317. }
  318. type GrokSSOToOAuthResponse struct {
  319. Created []GrokSSOToOAuthItemResult `json:"created"`
  320. Failed []GrokSSOToOAuthItemResult `json:"failed"`
  321. }
  322. type grokSSOImportJob struct {
  323. index int
  324. token string
  325. }
  326. type grokSSOImportWorkerResult struct {
  327. created bool
  328. item GrokSSOToOAuthItemResult
  329. }
  330. func (h *GrokOAuthHandler) CreateAccountsFromSSO(c *gin.Context) {
  331. var req GrokSSOToOAuthRequest
  332. if err := c.ShouldBindJSON(&req); err != nil {
  333. response.BadRequest(c, "Invalid request: "+err.Error())
  334. return
  335. }
  336. tokens := normalizeSSOImportTokens(req.SSOTokens, req.SSOToken)
  337. if len(tokens) == 0 {
  338. response.BadRequest(c, "sso_tokens is required")
  339. return
  340. }
  341. ctx := c.Request.Context()
  342. workerCount := grokSSOImportConcurrency
  343. if len(tokens) < workerCount {
  344. workerCount = len(tokens)
  345. }
  346. jobs := make(chan grokSSOImportJob)
  347. items := make([]grokSSOImportWorkerResult, len(tokens))
  348. var wg sync.WaitGroup
  349. for i := 0; i < workerCount; i++ {
  350. wg.Add(1)
  351. go func() {
  352. defer wg.Done()
  353. for job := range jobs {
  354. items[job.index] = h.safeCreateAccountFromSSOToken(ctx, req, job.token, job.index+1, len(tokens))
  355. }
  356. }()
  357. }
  358. for i, token := range tokens {
  359. jobs <- grokSSOImportJob{index: i, token: token}
  360. }
  361. close(jobs)
  362. wg.Wait()
  363. result := GrokSSOToOAuthResponse{
  364. Created: make([]GrokSSOToOAuthItemResult, 0, len(tokens)),
  365. Failed: make([]GrokSSOToOAuthItemResult, 0),
  366. }
  367. for _, item := range items {
  368. if item.created {
  369. result.Created = append(result.Created, item.item)
  370. } else {
  371. result.Failed = append(result.Failed, item.item)
  372. }
  373. }
  374. response.Success(c, result)
  375. }
  376. func (h *GrokOAuthHandler) safeCreateAccountFromSSOToken(ctx context.Context, req GrokSSOToOAuthRequest, token string, index, total int) (result grokSSOImportWorkerResult) {
  377. defer func() {
  378. if recovered := recover(); recovered != nil {
  379. slog.Error("grok_sso_import_worker_panic", "index", index, "recover", recovered)
  380. result = grokSSOImportWorkerResult{
  381. item: GrokSSOToOAuthItemResult{
  382. Index: index,
  383. Error: fmt.Sprintf("internal worker panic: %v", recovered),
  384. },
  385. }
  386. }
  387. }()
  388. return h.createAccountFromSSOToken(ctx, req, token, index, total)
  389. }
  390. func (h *GrokOAuthHandler) createAccountFromSSOToken(ctx context.Context, req GrokSSOToOAuthRequest, token string, index, total int) grokSSOImportWorkerResult {
  391. tokenInfo, err := h.grokOAuthService.ConvertFromSSO(ctx, token, req.ProxyID)
  392. if err != nil {
  393. return grokSSOImportWorkerResult{item: GrokSSOToOAuthItemResult{Index: index, Error: grokSSOImportErrorMessage(err)}}
  394. }
  395. credentials := grokSSOImportCredentials(h.grokOAuthService.BuildAccountCredentials(tokenInfo), req.Credentials)
  396. name := grokSSOImportAccountName(req.Name, tokenInfo, index, total)
  397. expiresAt, autoPauseOnExpired := grokSSOImportExpiry(req.ExpiresAt, req.AutoPauseOnExpired, tokenInfo)
  398. account, err := h.adminService.CreateAccount(ctx, &service.CreateAccountInput{
  399. Name: name,
  400. Notes: req.Notes,
  401. Platform: service.PlatformGrok,
  402. Type: service.AccountTypeOAuth,
  403. Credentials: credentials,
  404. Extra: cloneGrokSSOMap(req.Extra),
  405. ProxyID: req.ProxyID,
  406. Concurrency: req.Concurrency,
  407. LoadFactor: req.LoadFactor,
  408. Priority: req.Priority,
  409. RateMultiplier: req.RateMultiplier,
  410. GroupIDs: append([]int64(nil), req.GroupIDs...),
  411. ExpiresAt: expiresAt,
  412. AutoPauseOnExpired: autoPauseOnExpired,
  413. })
  414. if err != nil {
  415. return grokSSOImportWorkerResult{item: GrokSSOToOAuthItemResult{Index: index, Name: name, Email: tokenInfo.Email, Error: grokSSOImportErrorMessage(err)}}
  416. }
  417. h.scheduleGrokImportProbe(account)
  418. return grokSSOImportWorkerResult{
  419. created: true,
  420. item: GrokSSOToOAuthItemResult{
  421. Index: index,
  422. Name: name,
  423. Email: tokenInfo.Email,
  424. Account: dto.AccountFromService(account),
  425. },
  426. }
  427. }
  428. // grokSSOImportCredentials 合并 SSO 兑换出的凭据与导入请求携带的运营侧配置。
  429. // token 字段以 BuildAccountCredentials 为准(请求不可覆盖);但 base_url 是运营侧
  430. // 配置且 Build 恒写官方地址,会吞掉导入时指定的自定义转发地址——与
  431. // RefreshAccountToken 的保留逻辑对齐,请求显式提供时以请求为准。
  432. func grokSSOImportCredentials(built map[string]any, reqCredentials map[string]any) map[string]any {
  433. // Only merge operator config from the request — never free-form secrets
  434. // (password / sso_token / cookie / etc.) into stored credentials.
  435. allowedReqKeys := map[string]struct{}{
  436. "base_url": {}, "model_mapping": {},
  437. "header_override": {}, "header_overrides": {}, "header_override_enabled": {},
  438. "custom_headers": {},
  439. }
  440. ops := map[string]any{}
  441. for k, v := range reqCredentials {
  442. if _, ok := allowedReqKeys[k]; !ok {
  443. continue
  444. }
  445. if service.IsSensitiveCredentialKey(k) {
  446. continue
  447. }
  448. ops[k] = v
  449. }
  450. credentials := service.MergeCredentials(ops, built)
  451. // Strip any sensitive keys that might have slipped in via older callers.
  452. for k := range credentials {
  453. if service.IsSensitiveCredentialKey(k) {
  454. // Keep only keys produced by BuildAccountCredentials (tokens).
  455. if k == "access_token" || k == "refresh_token" || k == "id_token" {
  456. continue
  457. }
  458. delete(credentials, k)
  459. }
  460. }
  461. if reqBaseURL, ok := reqCredentials["base_url"].(string); ok && strings.TrimSpace(reqBaseURL) != "" {
  462. credentials["base_url"] = strings.TrimSpace(reqBaseURL)
  463. }
  464. return service.SanitizeStoredCredentials(service.PlatformGrok, credentials)
  465. }
  466. func grokSSOImportExpiry(requestExpiresAt *int64, requestAutoPause *bool, tokenInfo *service.GrokTokenInfo) (*int64, *bool) {
  467. if tokenInfo == nil || strings.TrimSpace(tokenInfo.RefreshToken) != "" || tokenInfo.ExpiresAt <= 0 {
  468. return requestExpiresAt, requestAutoPause
  469. }
  470. expiresAt := tokenInfo.ExpiresAt
  471. if requestExpiresAt != nil && *requestExpiresAt > 0 && *requestExpiresAt < expiresAt {
  472. expiresAt = *requestExpiresAt
  473. }
  474. autoPause := true
  475. return &expiresAt, &autoPause
  476. }
  477. func cloneGrokSSOMap(source map[string]any) map[string]any {
  478. if source == nil {
  479. return nil
  480. }
  481. clone := make(map[string]any, len(source))
  482. for key, value := range source {
  483. clone[key] = cloneGrokSSOValue(value)
  484. }
  485. return clone
  486. }
  487. func cloneGrokSSOValue(value any) any {
  488. switch v := value.(type) {
  489. case map[string]any:
  490. return cloneGrokSSOMap(v)
  491. case []any:
  492. clone := make([]any, len(v))
  493. for i, item := range v {
  494. clone[i] = cloneGrokSSOValue(item)
  495. }
  496. return clone
  497. default:
  498. return value
  499. }
  500. }
  501. func normalizeSSOImportTokens(tokens []string, single string) []string {
  502. items := make([]string, 0, len(tokens)+1)
  503. if strings.TrimSpace(single) != "" {
  504. items = append(items, single)
  505. }
  506. items = append(items, tokens...)
  507. seen := make(map[string]struct{}, len(items))
  508. result := make([]string, 0, len(items))
  509. for _, item := range items {
  510. parts := strings.Split(strings.NewReplacer(",", "\n", "\r", "\n").Replace(item), "\n")
  511. for _, token := range parts {
  512. if token = xai.NormalizeSSOToken(token); token == "" {
  513. continue
  514. }
  515. if _, ok := seen[token]; ok {
  516. continue
  517. }
  518. seen[token] = struct{}{}
  519. result = append(result, token)
  520. }
  521. }
  522. return result
  523. }
  524. func grokSSOImportAccountName(base string, tokenInfo *service.GrokTokenInfo, index, total int) string {
  525. base = strings.TrimSpace(base)
  526. if base == "" && tokenInfo != nil {
  527. base = strings.TrimSpace(tokenInfo.Email)
  528. }
  529. if base == "" {
  530. base = "Grok OAuth Account"
  531. }
  532. if total > 1 {
  533. return base + " #" + strconv.Itoa(index)
  534. }
  535. return base
  536. }
  537. func grokSSOImportErrorMessage(err error) string {
  538. status := infraerrors.FromError(err)
  539. if status == nil {
  540. return ""
  541. }
  542. if status.Reason != "" {
  543. return status.Reason + ": " + status.Message
  544. }
  545. return status.Message
  546. }
  547. func (h *GrokOAuthHandler) QueryQuota(c *gin.Context) {
  548. accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  549. if err != nil {
  550. response.BadRequest(c, "Invalid account ID")
  551. return
  552. }
  553. if h.quotaService == nil {
  554. response.BadRequest(c, "grok quota service is not enabled")
  555. return
  556. }
  557. result, err := h.quotaService.QueryQuota(c.Request.Context(), accountID)
  558. if err != nil {
  559. response.ErrorFrom(c, err)
  560. return
  561. }
  562. response.Success(c, result)
  563. }
  564. func (h *GrokOAuthHandler) ResetQuota(c *gin.Context) {
  565. accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  566. if err != nil {
  567. response.BadRequest(c, "Invalid account ID")
  568. return
  569. }
  570. if h.quotaService == nil {
  571. response.BadRequest(c, "grok quota service is not enabled")
  572. return
  573. }
  574. // ResetQuota 恒返回 GROK_QUOTA_RESET_UNSUPPORTED(xAI 无 OAuth 配额重置接口),err != nil 恒真为预期。
  575. //nolint:staticcheck // SA4023
  576. result, err := h.quotaService.ResetQuota(c.Request.Context(), accountID)
  577. if err != nil { //nolint:staticcheck // SA4023
  578. response.ErrorFrom(c, err)
  579. return
  580. }
  581. response.Success(c, result)
  582. }
  583. func (h *GrokOAuthHandler) RuntimeSanity(c *gin.Context) {
  584. response.Success(c, xai.RuntimeSanity())
  585. }