mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-30 08:22:34 +00:00
feat(grok): 支持 Web SSO 批量导入并转换为 Build OAuth
新增 Grok Web SSO → xAI Device Flow → Grok Build OAuth 导入链路, 支持管理员批量粘贴 SSO key 创建 OAuth 账号。 - 后端:ConvertSSOToBuild、ConvertFromSSO、POST /admin/grok/sso-to-oauth - 批量:3 worker 并发,失败跳过并汇总 created/failed,worker panic recover - 无 refresh_token 时写入 expires_at 并强制 auto_pause_on_expired - 前端:SSO Cookie 导入入口、动态超时、中英文案、部分成功不关弹窗 - 测试:pkg/service/handler/前端超时单测;本地 Docker 真实 SSO e2e 通过
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { post } = vi.hoisted(() => ({
|
||||
post: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/api/client', () => ({
|
||||
apiClient: { post },
|
||||
}))
|
||||
|
||||
import { createFromSSO, getGrokSSOImportTimeout } from '@/api/admin/grok'
|
||||
|
||||
describe('admin Grok SSO import API', () => {
|
||||
beforeEach(() => {
|
||||
post.mockReset()
|
||||
post.mockResolvedValue({ data: { created: [], failed: [] } })
|
||||
})
|
||||
|
||||
it.each([
|
||||
[1, 180_000],
|
||||
[3, 180_000],
|
||||
[4, 270_000],
|
||||
[7, 360_000],
|
||||
])('uses a timeout sized for %i keys', async (keyCount, expectedTimeout) => {
|
||||
expect(getGrokSSOImportTimeout(keyCount)).toBe(expectedTimeout)
|
||||
|
||||
await createFromSSO({
|
||||
sso_tokens: Array.from({ length: keyCount }, (_, index) => `sso-${index + 1}`),
|
||||
})
|
||||
|
||||
expect(post).toHaveBeenCalledWith(
|
||||
'/admin/grok/sso-to-oauth',
|
||||
expect.objectContaining({ sso_tokens: expect.any(Array) }),
|
||||
{ timeout: expectedTimeout },
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -34,11 +34,51 @@ export interface GrokTokenInfo {
|
||||
scope?: string
|
||||
client_id?: string
|
||||
email?: string
|
||||
sub?: string
|
||||
team_id?: string
|
||||
subscription_tier?: string
|
||||
entitlement_status?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface GrokSSOToOAuthRequest {
|
||||
sso_tokens: string[]
|
||||
name?: string
|
||||
notes?: string | null
|
||||
proxy_id?: number | null
|
||||
group_ids?: number[]
|
||||
credentials?: Record<string, unknown>
|
||||
extra?: Record<string, unknown>
|
||||
concurrency?: number
|
||||
load_factor?: number
|
||||
priority?: number
|
||||
rate_multiplier?: number
|
||||
expires_at?: number | null
|
||||
auto_pause_on_expired?: boolean
|
||||
}
|
||||
|
||||
export interface GrokSSOToOAuthItemResult {
|
||||
index: number
|
||||
name?: string
|
||||
email?: string
|
||||
account?: unknown
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface GrokSSOToOAuthResponse {
|
||||
created: GrokSSOToOAuthItemResult[]
|
||||
failed: GrokSSOToOAuthItemResult[]
|
||||
}
|
||||
|
||||
const GROK_SSO_IMPORT_CONCURRENCY = 3
|
||||
const GROK_SSO_IMPORT_TIMEOUT_PER_BATCH_MS = 90_000
|
||||
const GROK_SSO_IMPORT_TIMEOUT_BUFFER_MS = 90_000
|
||||
|
||||
export function getGrokSSOImportTimeout(keyCount: number): number {
|
||||
const batches = Math.ceil(Math.max(1, keyCount) / GROK_SSO_IMPORT_CONCURRENCY)
|
||||
return batches * GROK_SSO_IMPORT_TIMEOUT_PER_BATCH_MS + GROK_SSO_IMPORT_TIMEOUT_BUFFER_MS
|
||||
}
|
||||
|
||||
export interface GrokQuotaWindow {
|
||||
limit?: number | null
|
||||
remaining?: number | null
|
||||
@@ -119,4 +159,13 @@ export async function resetQuota(id: number): Promise<GrokQuotaResetResult> {
|
||||
return data
|
||||
}
|
||||
|
||||
export default { generateAuthUrl, exchangeCode, refreshGrokToken, queryQuota, resetQuota }
|
||||
export async function createFromSSO(payload: GrokSSOToOAuthRequest): Promise<GrokSSOToOAuthResponse> {
|
||||
const { data } = await apiClient.post<GrokSSOToOAuthResponse>(
|
||||
'/admin/grok/sso-to-oauth',
|
||||
payload,
|
||||
{ timeout: getGrokSSOImportTimeout(payload.sso_tokens.length) }
|
||||
)
|
||||
return data
|
||||
}
|
||||
|
||||
export default { generateAuthUrl, exchangeCode, refreshGrokToken, queryQuota, resetQuota, createFromSSO }
|
||||
|
||||
Reference in New Issue
Block a user