feat: Implement initial full-stack application structure including frontend pages, components, hooks, API integration, and backend services for account pooling and management.
This commit is contained in:
294
frontend/src/api/chatgpt.test.ts
Normal file
294
frontend/src/api/chatgpt.test.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { ChatGPTClient, getStatusText, getStatusColor, createChatGPTClient } from './chatgpt'
|
||||
import type { AccountInput } from '../types'
|
||||
|
||||
describe('ChatGPTClient', () => {
|
||||
let client: ChatGPTClient
|
||||
let fetchMock: ReturnType<typeof vi.fn>
|
||||
|
||||
beforeEach(() => {
|
||||
client = new ChatGPTClient()
|
||||
fetchMock = vi.fn()
|
||||
globalThis.fetch = fetchMock as typeof fetch
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('checkAccount', () => {
|
||||
it('should return active status for HTTP 200 with account info', async () => {
|
||||
const mockResponse = {
|
||||
accounts: [
|
||||
{
|
||||
account_id: 'test-account-id',
|
||||
entitlement: {
|
||||
subscription_plan: 'plus',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
})
|
||||
|
||||
const result = await client.checkAccount('valid-token')
|
||||
|
||||
expect(result.status).toBe('active')
|
||||
expect(result.accountId).toBe('test-account-id')
|
||||
expect(result.planType).toBe('plus')
|
||||
expect(result.error).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return active status for HTTP 200 without account info', async () => {
|
||||
const mockResponse = {
|
||||
accounts: [],
|
||||
}
|
||||
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
})
|
||||
|
||||
const result = await client.checkAccount('valid-token')
|
||||
|
||||
expect(result.status).toBe('active')
|
||||
expect(result.accountId).toBeUndefined()
|
||||
expect(result.planType).toBe('unknown')
|
||||
})
|
||||
|
||||
it('should return token_expired status for HTTP 401', async () => {
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
status: 401,
|
||||
statusText: 'Unauthorized',
|
||||
json: () => Promise.reject(new Error('No JSON')),
|
||||
})
|
||||
|
||||
const result = await client.checkAccount('expired-token')
|
||||
|
||||
expect(result.status).toBe('token_expired')
|
||||
expect(result.error).toBe('Token 已过期')
|
||||
})
|
||||
|
||||
it('should return banned status for HTTP 403', async () => {
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
status: 403,
|
||||
statusText: 'Forbidden',
|
||||
json: () => Promise.reject(new Error('No JSON')),
|
||||
})
|
||||
|
||||
const result = await client.checkAccount('banned-token')
|
||||
|
||||
expect(result.status).toBe('banned')
|
||||
expect(result.error).toBe('账号已被封禁')
|
||||
})
|
||||
|
||||
it('should return error status for other HTTP codes', async () => {
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
json: () => Promise.reject(new Error('No JSON')),
|
||||
})
|
||||
|
||||
const result = await client.checkAccount('some-token')
|
||||
|
||||
expect(result.status).toBe('error')
|
||||
expect(result.error).toBe('HTTP 500: Internal Server Error')
|
||||
})
|
||||
|
||||
it('should return error status for network errors', async () => {
|
||||
fetchMock.mockRejectedValueOnce(new Error('Network error'))
|
||||
|
||||
const result = await client.checkAccount('some-token')
|
||||
|
||||
expect(result.status).toBe('error')
|
||||
expect(result.error).toBe('Network error')
|
||||
})
|
||||
|
||||
it('should return error status for empty token', async () => {
|
||||
const result = await client.checkAccount('')
|
||||
|
||||
expect(result.status).toBe('error')
|
||||
expect(result.error).toBe('缺少 token')
|
||||
expect(fetchMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return error status for whitespace-only token', async () => {
|
||||
const result = await client.checkAccount(' ')
|
||||
|
||||
expect(result.status).toBe('error')
|
||||
expect(result.error).toBe('缺少 token')
|
||||
expect(fetchMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should use correct API endpoint and headers', async () => {
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
json: () => Promise.resolve({ accounts: [] }),
|
||||
})
|
||||
|
||||
await client.checkAccount('test-token')
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/chatgpt/accounts/check/v4-2023-04-27', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: 'Bearer test-token',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle JSON parse errors gracefully for HTTP 200', async () => {
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
json: () => Promise.reject(new Error('Invalid JSON')),
|
||||
})
|
||||
|
||||
const result = await client.checkAccount('valid-token')
|
||||
|
||||
// Should still return active since HTTP 200
|
||||
expect(result.status).toBe('active')
|
||||
expect(result.planType).toBe('unknown')
|
||||
})
|
||||
})
|
||||
|
||||
describe('batchCheck', () => {
|
||||
it('should return empty array for empty input', async () => {
|
||||
const results = await client.batchCheck([], { concurrency: 5 })
|
||||
|
||||
expect(results).toEqual([])
|
||||
expect(fetchMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should check all accounts and return results in order', async () => {
|
||||
const accounts: AccountInput[] = [
|
||||
{ account: 'user1@test.com', password: 'pass1', token: 'token1' },
|
||||
{ account: 'user2@test.com', password: 'pass2', token: 'token2' },
|
||||
{ account: 'user3@test.com', password: 'pass3', token: 'token3' },
|
||||
]
|
||||
|
||||
// Mock responses for each account
|
||||
fetchMock
|
||||
.mockResolvedValueOnce({
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
accounts: [{ account_id: 'id1', entitlement: { subscription_plan: 'plus' } }],
|
||||
}),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
status: 401,
|
||||
statusText: 'Unauthorized',
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
status: 403,
|
||||
statusText: 'Forbidden',
|
||||
})
|
||||
|
||||
const results = await client.batchCheck(accounts, { concurrency: 3 })
|
||||
|
||||
expect(results).toHaveLength(3)
|
||||
expect(results[0].status).toBe('active')
|
||||
expect(results[0].accountId).toBe('id1')
|
||||
expect(results[1].status).toBe('token_expired')
|
||||
expect(results[2].status).toBe('banned')
|
||||
})
|
||||
|
||||
it('should call onProgress callback for each account', async () => {
|
||||
const accounts: AccountInput[] = [
|
||||
{ account: 'user1@test.com', password: 'pass1', token: 'token1' },
|
||||
{ account: 'user2@test.com', password: 'pass2', token: 'token2' },
|
||||
]
|
||||
|
||||
fetchMock
|
||||
.mockResolvedValueOnce({
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
json: () => Promise.resolve({ accounts: [] }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
json: () => Promise.resolve({ accounts: [] }),
|
||||
})
|
||||
|
||||
const onProgress = vi.fn()
|
||||
|
||||
await client.batchCheck(accounts, { concurrency: 2, onProgress })
|
||||
|
||||
expect(onProgress).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should respect concurrency limit', async () => {
|
||||
const accounts: AccountInput[] = Array.from({ length: 10 }, (_, i) => ({
|
||||
account: `user${i}@test.com`,
|
||||
password: `pass${i}`,
|
||||
token: `token${i}`,
|
||||
}))
|
||||
|
||||
let maxConcurrent = 0
|
||||
let currentConcurrent = 0
|
||||
|
||||
fetchMock.mockImplementation(async () => {
|
||||
currentConcurrent++
|
||||
maxConcurrent = Math.max(maxConcurrent, currentConcurrent)
|
||||
|
||||
// Simulate network delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
|
||||
currentConcurrent--
|
||||
return {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
json: () => Promise.resolve({ accounts: [] }),
|
||||
}
|
||||
})
|
||||
|
||||
await client.batchCheck(accounts, { concurrency: 3 })
|
||||
|
||||
// Max concurrent should not exceed the concurrency limit
|
||||
expect(maxConcurrent).toBeLessThanOrEqual(3)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getStatusText', () => {
|
||||
it('should return correct Chinese text for each status', () => {
|
||||
expect(getStatusText('pending')).toBe('待检查')
|
||||
expect(getStatusText('checking')).toBe('检查中')
|
||||
expect(getStatusText('active')).toBe('正常')
|
||||
expect(getStatusText('banned')).toBe('封禁')
|
||||
expect(getStatusText('token_expired')).toBe('过期')
|
||||
expect(getStatusText('error')).toBe('错误')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getStatusColor', () => {
|
||||
it('should return correct color for each status', () => {
|
||||
expect(getStatusColor('pending')).toBe('gray')
|
||||
expect(getStatusColor('checking')).toBe('blue')
|
||||
expect(getStatusColor('active')).toBe('green')
|
||||
expect(getStatusColor('banned')).toBe('red')
|
||||
expect(getStatusColor('token_expired')).toBe('orange')
|
||||
expect(getStatusColor('error')).toBe('yellow')
|
||||
})
|
||||
})
|
||||
|
||||
describe('createChatGPTClient', () => {
|
||||
it('should create a new ChatGPTClient instance', () => {
|
||||
const client = createChatGPTClient()
|
||||
expect(client).toBeInstanceOf(ChatGPTClient)
|
||||
})
|
||||
|
||||
it('should create a client with custom base URL', () => {
|
||||
const client = createChatGPTClient('https://custom.api.com')
|
||||
expect(client).toBeInstanceOf(ChatGPTClient)
|
||||
})
|
||||
})
|
||||
253
frontend/src/api/chatgpt.ts
Normal file
253
frontend/src/api/chatgpt.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import type { AccountInput, AccountStatus, CheckedAccount, CheckResult } from '../types'
|
||||
import type { ChatGPTCheckResponse } from './types'
|
||||
|
||||
/**
|
||||
* ChatGPT API 检查端点
|
||||
* 通过 nginx 代理访问,避免 CORS 问题
|
||||
* 原始 API: https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27
|
||||
*/
|
||||
const CHATGPT_CHECK_API = '/api/chatgpt/accounts/check/v4-2023-04-27'
|
||||
|
||||
/**
|
||||
* HTTP 状态码到账号状态的映射
|
||||
* 根据 requirements.md A3 定义:
|
||||
* - HTTP 200 → active (账号正常)
|
||||
* - HTTP 401 → token_expired (Token 已过期)
|
||||
* - HTTP 403 → banned (账号被封禁)
|
||||
* - 其他 → error (网络错误等)
|
||||
*/
|
||||
function mapHttpStatusToAccountStatus(httpStatus: number): AccountStatus {
|
||||
switch (httpStatus) {
|
||||
case 200:
|
||||
return 'active'
|
||||
case 401:
|
||||
return 'token_expired'
|
||||
case 403:
|
||||
return 'banned'
|
||||
default:
|
||||
return 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 HTTP 状态码对应的错误消息
|
||||
*/
|
||||
function getErrorMessageForStatus(httpStatus: number, statusText: string): string | undefined {
|
||||
switch (httpStatus) {
|
||||
case 200:
|
||||
return undefined
|
||||
case 401:
|
||||
return 'Token 已过期'
|
||||
case 403:
|
||||
return '账号已被封禁'
|
||||
default:
|
||||
return `HTTP ${httpStatus}: ${statusText}`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ChatGPT API 客户端
|
||||
* 用于检查 ChatGPT 账号状态
|
||||
*/
|
||||
export class ChatGPTClient {
|
||||
private baseUrl: string
|
||||
|
||||
constructor(baseUrl: string = '') {
|
||||
this.baseUrl = baseUrl
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查单个账号状态
|
||||
* @param token - ChatGPT access_token
|
||||
* @returns CheckResult 包含状态、account_id、plan_type 等信息
|
||||
*
|
||||
* API: GET https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27
|
||||
* Headers: Authorization: Bearer {token}
|
||||
*
|
||||
* 状态映射 (requirements.md A3):
|
||||
* - HTTP 200 → active
|
||||
* - HTTP 401 → token_expired
|
||||
* - HTTP 403 → banned
|
||||
* - 其他 → error
|
||||
*/
|
||||
async checkAccount(token: string): Promise<CheckResult> {
|
||||
// 处理空 token 的情况
|
||||
if (!token || token.trim() === '') {
|
||||
return {
|
||||
status: 'error',
|
||||
error: '缺少 token',
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}${CHATGPT_CHECK_API}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
const status = mapHttpStatusToAccountStatus(response.status)
|
||||
const errorMessage = getErrorMessageForStatus(response.status, response.statusText)
|
||||
|
||||
if (response.status === 200) {
|
||||
try {
|
||||
const data: ChatGPTCheckResponse = await response.json()
|
||||
const accountInfo = data.accounts?.[0]
|
||||
|
||||
if (accountInfo) {
|
||||
return {
|
||||
status: 'active',
|
||||
accountId: accountInfo.account_id,
|
||||
planType: accountInfo.entitlement?.subscription_plan || 'free',
|
||||
}
|
||||
}
|
||||
|
||||
// 200 响应但没有账号信息
|
||||
return {
|
||||
status: 'active',
|
||||
accountId: undefined,
|
||||
planType: 'unknown',
|
||||
}
|
||||
} catch {
|
||||
// JSON 解析失败,但 HTTP 200 仍视为 active
|
||||
return {
|
||||
status: 'active',
|
||||
accountId: undefined,
|
||||
planType: 'unknown',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status,
|
||||
error: errorMessage,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'error',
|
||||
error: error instanceof Error ? error.message : '网络错误',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量检查账号(带并发控制)
|
||||
* @param accounts - 待检查的账号列表
|
||||
* @param options.concurrency - 并发数量(默认 20)
|
||||
* @param options.onProgress - 进度回调,每检查完一个账号调用一次
|
||||
* @returns 检查完成的账号列表
|
||||
*
|
||||
* 使用队列 + Promise 实现并发控制,确保任意时刻活跃请求数 ≤ concurrency
|
||||
*/
|
||||
async batchCheck(
|
||||
accounts: AccountInput[],
|
||||
options: {
|
||||
concurrency: number
|
||||
onProgress?: (result: CheckedAccount, index: number) => void
|
||||
}
|
||||
): Promise<CheckedAccount[]> {
|
||||
const { concurrency, onProgress } = options
|
||||
|
||||
// 空数组直接返回
|
||||
if (accounts.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const results: CheckedAccount[] = new Array(accounts.length)
|
||||
const queue: number[] = [...Array(accounts.length).keys()]
|
||||
let activeCount = 0
|
||||
let completedCount = 0
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const processNext = () => {
|
||||
// 所有任务完成
|
||||
if (completedCount === accounts.length) {
|
||||
resolve(results)
|
||||
return
|
||||
}
|
||||
|
||||
// 启动新任务,直到达到并发限制或队列为空
|
||||
while (activeCount < concurrency && queue.length > 0) {
|
||||
const index = queue.shift()!
|
||||
activeCount++
|
||||
|
||||
// 异步处理单个账号
|
||||
this.processAccount(accounts[index], index)
|
||||
.then((checkedAccount) => {
|
||||
results[index] = checkedAccount
|
||||
onProgress?.(checkedAccount, index)
|
||||
})
|
||||
.finally(() => {
|
||||
activeCount--
|
||||
completedCount++
|
||||
// 继续处理下一个
|
||||
processNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 开始处理
|
||||
processNext()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单个账号检查
|
||||
* @private
|
||||
*/
|
||||
private async processAccount(account: AccountInput, index: number): Promise<CheckedAccount> {
|
||||
const checkResult = await this.checkAccount(account.token)
|
||||
|
||||
return {
|
||||
...account,
|
||||
id: index,
|
||||
status: checkResult.status,
|
||||
accountId: checkResult.accountId,
|
||||
planType: checkResult.planType,
|
||||
error: checkResult.error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析账号状态为中文描述
|
||||
*/
|
||||
export function getStatusText(status: AccountStatus): string {
|
||||
const statusMap: Record<AccountStatus, string> = {
|
||||
pending: '待检查',
|
||||
checking: '检查中',
|
||||
active: '正常',
|
||||
banned: '封禁',
|
||||
token_expired: '过期',
|
||||
error: '错误',
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态对应的颜色类名
|
||||
*/
|
||||
export function getStatusColor(status: AccountStatus): string {
|
||||
const colorMap: Record<AccountStatus, string> = {
|
||||
pending: 'gray',
|
||||
checking: 'blue',
|
||||
active: 'green',
|
||||
banned: 'red',
|
||||
token_expired: 'orange',
|
||||
error: 'yellow',
|
||||
}
|
||||
return colorMap[status] || 'gray'
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 ChatGPT 客户端实例
|
||||
* @param baseUrl - 可选的基础 URL,默认为空(使用相对路径)
|
||||
*/
|
||||
export function createChatGPTClient(baseUrl: string = ''): ChatGPTClient {
|
||||
return new ChatGPTClient(baseUrl)
|
||||
}
|
||||
|
||||
// 导出默认客户端实例(使用相对路径,通过 nginx 代理)
|
||||
export const chatGPTClient = new ChatGPTClient()
|
||||
4
frontend/src/api/index.ts
Normal file
4
frontend/src/api/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// API Layer barrel export
|
||||
export * from './types'
|
||||
export * from './s2a'
|
||||
export * from './chatgpt'
|
||||
155
frontend/src/api/s2a.ts
Normal file
155
frontend/src/api/s2a.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import type {
|
||||
DashboardStatsResponse,
|
||||
DashboardTrendResponse,
|
||||
AccountListResponse,
|
||||
AccountResponse,
|
||||
CreateAccountPayload,
|
||||
OAuthCreatePayload,
|
||||
GroupResponse,
|
||||
ProxyResponse,
|
||||
TestAccountResponse,
|
||||
} from './types'
|
||||
import type { AccountListParams } from '../types'
|
||||
|
||||
// 使用后端代理 API 来避免 CORS 问题
|
||||
const PROXY_BASE = 'http://localhost:8088/api/s2a/proxy'
|
||||
|
||||
export class S2AClient {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
constructor(_config: { baseUrl: string; apiKey: string }) {
|
||||
// 不再使用直接配置,通过后端代理
|
||||
}
|
||||
|
||||
private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
||||
// 将 /api/v1/admin/* 转换为代理路径
|
||||
const proxyEndpoint = endpoint.replace('/api/v1/admin', '')
|
||||
const url = `${PROXY_BASE}${proxyEndpoint}`
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
throw new Error(
|
||||
errorData.message || errorData.error || `HTTP ${response.status}: ${response.statusText}`
|
||||
)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
// Dashboard APIs
|
||||
async getDashboardStats(): Promise<DashboardStatsResponse> {
|
||||
return this.request<DashboardStatsResponse>('/dashboard/stats')
|
||||
}
|
||||
|
||||
async getDashboardTrend(granularity: 'day' | 'hour' = 'day'): Promise<DashboardTrendResponse> {
|
||||
return this.request<DashboardTrendResponse>(
|
||||
`/dashboard/trend?granularity=${granularity}`
|
||||
)
|
||||
}
|
||||
|
||||
// Account APIs
|
||||
async getAccounts(params: AccountListParams = {}): Promise<AccountListResponse> {
|
||||
const searchParams = new URLSearchParams()
|
||||
if (params.page) searchParams.set('page', params.page.toString())
|
||||
if (params.page_size) searchParams.set('page_size', params.page_size.toString())
|
||||
if (params.platform) searchParams.set('platform', params.platform)
|
||||
if (params.type) searchParams.set('type', params.type)
|
||||
if (params.status) searchParams.set('status', params.status)
|
||||
if (params.search) searchParams.set('search', params.search)
|
||||
|
||||
const queryString = searchParams.toString()
|
||||
const endpoint = `/accounts${queryString ? `?${queryString}` : ''}`
|
||||
return this.request<AccountListResponse>(endpoint)
|
||||
}
|
||||
|
||||
async getAccount(id: number): Promise<AccountResponse> {
|
||||
return this.request<AccountResponse>(`/accounts/${id}`)
|
||||
}
|
||||
|
||||
async createAccount(data: CreateAccountPayload): Promise<AccountResponse> {
|
||||
return this.request<AccountResponse>('/accounts', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
}
|
||||
|
||||
async createFromOAuth(data: OAuthCreatePayload): Promise<AccountResponse> {
|
||||
return this.request<AccountResponse>('/openai/create-from-oauth', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
}
|
||||
|
||||
async updateAccount(id: number, data: Partial<CreateAccountPayload>): Promise<AccountResponse> {
|
||||
return this.request<AccountResponse>(`/accounts/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
}
|
||||
|
||||
async deleteAccount(id: number): Promise<void> {
|
||||
await this.request<void>(`/accounts/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
async testAccount(id: number): Promise<TestAccountResponse> {
|
||||
return this.request<TestAccountResponse>(`/accounts/${id}/test`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
async refreshAccountToken(id: number): Promise<AccountResponse> {
|
||||
return this.request<AccountResponse>(`/accounts/${id}/refresh`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
async clearAccountError(id: number): Promise<AccountResponse> {
|
||||
return this.request<AccountResponse>(`/accounts/${id}/clear-error`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
// Group APIs
|
||||
async getGroups(): Promise<GroupResponse[]> {
|
||||
const response = await this.request<{ data: GroupResponse[] }>('/groups/all')
|
||||
return response.data || []
|
||||
}
|
||||
|
||||
// Proxy APIs
|
||||
async getProxies(): Promise<ProxyResponse[]> {
|
||||
const response = await this.request<{ data: ProxyResponse[] }>('/proxies/all')
|
||||
return response.data || []
|
||||
}
|
||||
|
||||
async testProxy(id: number): Promise<TestAccountResponse> {
|
||||
return this.request<TestAccountResponse>(`/proxies/${id}/test`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
// Connection test
|
||||
async testConnection(): Promise<boolean> {
|
||||
try {
|
||||
await this.getDashboardStats()
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建默认客户端实例的工厂函数
|
||||
export function createS2AClient(baseUrl: string, apiKey: string): S2AClient {
|
||||
return new S2AClient({ baseUrl, apiKey })
|
||||
}
|
||||
522
frontend/src/api/types.ts
Normal file
522
frontend/src/api/types.ts
Normal file
@@ -0,0 +1,522 @@
|
||||
// =============================================================================
|
||||
// S2A API 响应类型
|
||||
// 基于 requirements.md Appendix A2 定义
|
||||
// =============================================================================
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Dashboard 相关接口响应
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Dashboard Stats 响应
|
||||
* API: GET /api/v1/admin/dashboard/stats
|
||||
* 获取号池统计(账号数、请求数、Token消耗等)
|
||||
*/
|
||||
export interface DashboardStatsResponse {
|
||||
total_accounts: number
|
||||
normal_accounts: number
|
||||
error_accounts: number
|
||||
ratelimit_accounts: number
|
||||
overload_accounts: number
|
||||
today_requests: number
|
||||
today_tokens: number
|
||||
today_cost: number
|
||||
total_requests: number
|
||||
total_tokens: number
|
||||
total_cost: number
|
||||
rpm: number
|
||||
tpm: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard Trend 响应
|
||||
* API: GET /api/v1/admin/dashboard/trend
|
||||
* 获取使用趋势(支持 granularity=day/hour)
|
||||
*/
|
||||
export interface DashboardTrendResponse {
|
||||
data: TrendDataPoint[]
|
||||
}
|
||||
|
||||
export interface TrendDataPoint {
|
||||
date: string
|
||||
requests: number
|
||||
tokens: number
|
||||
cost: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard Models 响应
|
||||
* API: GET /api/v1/admin/dashboard/models
|
||||
* 获取模型使用统计
|
||||
*/
|
||||
export interface DashboardModelsResponse {
|
||||
data: ModelUsageStats[]
|
||||
}
|
||||
|
||||
export interface ModelUsageStats {
|
||||
model: string
|
||||
requests: number
|
||||
tokens: number
|
||||
cost: number
|
||||
percentage: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard Users Trend 响应
|
||||
* API: GET /api/v1/admin/dashboard/users-trend
|
||||
* 获取用户使用趋势
|
||||
*/
|
||||
export interface DashboardUsersTrendResponse {
|
||||
data: UsersTrendDataPoint[]
|
||||
}
|
||||
|
||||
export interface UsersTrendDataPoint {
|
||||
date: string
|
||||
active_users: number
|
||||
new_users: number
|
||||
total_users: number
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 账号管理接口响应
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 账号列表响应
|
||||
* API: GET /api/v1/admin/accounts
|
||||
* 获取账号列表(支持分页、筛选)
|
||||
*/
|
||||
export interface AccountListResponse {
|
||||
data: AccountResponse[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 单个账号响应
|
||||
* API: GET /api/v1/admin/accounts/:id
|
||||
* 对应 requirements.md A5 Account 数据结构
|
||||
*/
|
||||
export interface AccountResponse {
|
||||
id: number
|
||||
name: string
|
||||
notes?: string
|
||||
platform: 'openai' | 'anthropic' | 'gemini'
|
||||
type: 'oauth' | 'access_token' | 'apikey' | 'setup-token'
|
||||
credentials: Record<string, unknown>
|
||||
extra?: Record<string, unknown>
|
||||
proxy_id?: number
|
||||
concurrency: number
|
||||
priority: number
|
||||
rate_multiplier?: number
|
||||
status: 'active' | 'inactive' | 'error'
|
||||
error_message?: string
|
||||
schedulable: boolean
|
||||
last_used_at?: string
|
||||
expires_at?: string
|
||||
auto_pause_on_expired: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
current_concurrency?: number
|
||||
current_window_cost?: number
|
||||
active_sessions?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 账号统计响应
|
||||
* API: GET /api/v1/admin/accounts/:id/stats
|
||||
* 获取账号使用统计
|
||||
*/
|
||||
export interface AccountStatsResponse {
|
||||
account_id: number
|
||||
total_requests: number
|
||||
total_tokens: number
|
||||
total_cost: number
|
||||
today_requests: number
|
||||
today_tokens: number
|
||||
today_cost: number
|
||||
last_used_at?: string
|
||||
error_count: number
|
||||
success_rate: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建账号请求
|
||||
* API: POST /api/v1/admin/accounts
|
||||
* 对应 requirements.md A4 access_token 类型账号
|
||||
*/
|
||||
export interface CreateAccountPayload {
|
||||
name: string
|
||||
platform: 'openai' | 'anthropic' | 'gemini'
|
||||
type: 'access_token'
|
||||
credentials: {
|
||||
access_token: string
|
||||
refresh_token?: string
|
||||
email?: string
|
||||
}
|
||||
concurrency?: number
|
||||
priority?: number
|
||||
group_ids?: number[]
|
||||
proxy_id?: number | null
|
||||
auto_pause_on_expired?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新账号请求
|
||||
* API: POST /api/v1/admin/accounts/bulk-update
|
||||
*/
|
||||
export interface BulkUpdateAccountsPayload {
|
||||
ids: number[]
|
||||
updates: {
|
||||
status?: 'active' | 'inactive'
|
||||
concurrency?: number
|
||||
priority?: number
|
||||
group_ids?: number[]
|
||||
proxy_id?: number | null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新账号响应
|
||||
*/
|
||||
export interface BulkUpdateAccountsResponse {
|
||||
success: boolean
|
||||
updated_count: number
|
||||
failed_count: number
|
||||
errors?: string[]
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// OpenAI OAuth 接口响应
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* OAuth 创建账号请求
|
||||
* API: POST /api/v1/admin/openai/create-from-oauth
|
||||
* 对应 requirements.md A4 OAuth 类型账号
|
||||
*/
|
||||
export interface OAuthCreatePayload {
|
||||
session_id: string
|
||||
code: string
|
||||
name?: string
|
||||
concurrency?: number
|
||||
priority?: number
|
||||
group_ids?: number[]
|
||||
proxy_id?: number | null
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 OAuth 授权 URL 请求
|
||||
* API: POST /api/v1/admin/openai/generate-auth-url
|
||||
*/
|
||||
export interface GenerateAuthUrlPayload {
|
||||
redirect_uri?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 OAuth 授权 URL 响应
|
||||
*/
|
||||
export interface GenerateAuthUrlResponse {
|
||||
auth_url: string
|
||||
session_id: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 交换授权码请求
|
||||
* API: POST /api/v1/admin/openai/exchange-code
|
||||
*/
|
||||
export interface ExchangeCodePayload {
|
||||
session_id: string
|
||||
code: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 交换授权码响应
|
||||
*/
|
||||
export interface ExchangeCodeResponse {
|
||||
access_token: string
|
||||
refresh_token?: string
|
||||
expires_in?: number
|
||||
token_type: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新 Token 请求
|
||||
* API: POST /api/v1/admin/openai/refresh-token
|
||||
*/
|
||||
export interface RefreshTokenPayload {
|
||||
refresh_token: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新 Token 响应
|
||||
*/
|
||||
export interface RefreshTokenResponse {
|
||||
access_token: string
|
||||
refresh_token?: string
|
||||
expires_in?: number
|
||||
token_type: string
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 分组管理接口响应
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 分组响应
|
||||
* API: GET /api/v1/admin/groups, GET /api/v1/admin/groups/all
|
||||
*/
|
||||
export interface GroupResponse {
|
||||
id: number
|
||||
name: string
|
||||
description?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 分组统计响应
|
||||
* API: GET /api/v1/admin/groups/:id/stats
|
||||
*/
|
||||
export interface GroupStatsResponse {
|
||||
group_id: number
|
||||
total_accounts: number
|
||||
active_accounts: number
|
||||
error_accounts: number
|
||||
total_requests: number
|
||||
total_tokens: number
|
||||
total_cost: number
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 代理管理接口响应
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 代理响应
|
||||
* API: GET /api/v1/admin/proxies, GET /api/v1/admin/proxies/all
|
||||
*/
|
||||
export interface ProxyResponse {
|
||||
id: number
|
||||
name: string
|
||||
url: string
|
||||
status: 'active' | 'inactive' | 'error'
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试代理/账号响应
|
||||
* API: POST /api/v1/admin/proxies/:id/test, POST /api/v1/admin/accounts/:id/test
|
||||
*/
|
||||
export interface TestAccountResponse {
|
||||
success: boolean
|
||||
message?: string
|
||||
latency?: number
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 运维监控接口响应
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 并发统计响应
|
||||
* API: GET /api/v1/admin/ops/concurrency
|
||||
*/
|
||||
export interface OpsConcurrencyResponse {
|
||||
total_concurrency: number
|
||||
used_concurrency: number
|
||||
available_concurrency: number
|
||||
accounts: AccountConcurrencyInfo[]
|
||||
}
|
||||
|
||||
export interface AccountConcurrencyInfo {
|
||||
account_id: number
|
||||
account_name: string
|
||||
max_concurrency: number
|
||||
current_concurrency: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 账号可用性响应
|
||||
* API: GET /api/v1/admin/ops/account-availability
|
||||
*/
|
||||
export interface OpsAccountAvailabilityResponse {
|
||||
total_accounts: number
|
||||
available_accounts: number
|
||||
unavailable_accounts: number
|
||||
availability_rate: number
|
||||
accounts: AccountAvailabilityInfo[]
|
||||
}
|
||||
|
||||
export interface AccountAvailabilityInfo {
|
||||
account_id: number
|
||||
account_name: string
|
||||
status: 'available' | 'unavailable' | 'rate_limited' | 'error'
|
||||
reason?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 实时流量响应
|
||||
* API: GET /api/v1/admin/ops/realtime-traffic
|
||||
*/
|
||||
export interface OpsRealtimeTrafficResponse {
|
||||
current_rpm: number
|
||||
current_tpm: number
|
||||
peak_rpm: number
|
||||
peak_tpm: number
|
||||
requests_last_minute: number
|
||||
tokens_last_minute: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 运维仪表盘概览响应
|
||||
* API: GET /api/v1/admin/ops/dashboard/overview
|
||||
*/
|
||||
export interface OpsDashboardOverviewResponse {
|
||||
health_score: number
|
||||
total_accounts: number
|
||||
healthy_accounts: number
|
||||
warning_accounts: number
|
||||
error_accounts: number
|
||||
current_load: number
|
||||
max_load: number
|
||||
alerts: OpsAlert[]
|
||||
}
|
||||
|
||||
export interface OpsAlert {
|
||||
id: string
|
||||
level: 'info' | 'warning' | 'error' | 'critical'
|
||||
message: string
|
||||
timestamp: string
|
||||
account_id?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误趋势响应
|
||||
* API: GET /api/v1/admin/ops/dashboard/error-trend
|
||||
*/
|
||||
export interface OpsErrorTrendResponse {
|
||||
data: ErrorTrendDataPoint[]
|
||||
}
|
||||
|
||||
export interface ErrorTrendDataPoint {
|
||||
date: string
|
||||
total_errors: number
|
||||
rate_limit_errors: number
|
||||
auth_errors: number
|
||||
network_errors: number
|
||||
other_errors: number
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ChatGPT API 响应类型
|
||||
// 基于 requirements.md Requirement 1.2 定义
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* 账号检查响应
|
||||
* API: GET https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27
|
||||
* Headers: Authorization: Bearer {token}
|
||||
*/
|
||||
export interface ChatGPTCheckResponse {
|
||||
accounts: ChatGPTAccountInfo[]
|
||||
}
|
||||
|
||||
export interface ChatGPTAccountInfo {
|
||||
account_id: string
|
||||
account: ChatGPTAccountDetails
|
||||
features: string[]
|
||||
entitlement: ChatGPTEntitlement
|
||||
last_active_subscription: ChatGPTLastActiveSubscription
|
||||
is_eligible_for_yearly_plus_subscription: boolean
|
||||
}
|
||||
|
||||
export interface ChatGPTAccountDetails {
|
||||
account_user_id: string
|
||||
processor: {
|
||||
a001: {
|
||||
has_customer_object: boolean
|
||||
}
|
||||
}
|
||||
account_user_role: string
|
||||
plan_type: string
|
||||
is_most_recent_expired_subscription_gratis: boolean
|
||||
has_previously_paid_subscription: boolean
|
||||
name: string | null
|
||||
profile_picture_id: string | null
|
||||
profile_picture_url: string | null
|
||||
structure: string
|
||||
is_deactivated: boolean
|
||||
is_disabled: boolean
|
||||
// SAM (Security Account Management) 相关字段
|
||||
is_sam_enforced: boolean
|
||||
is_sam_enabled: boolean
|
||||
is_sam_compliant: boolean
|
||||
is_sam_grace_period: boolean
|
||||
is_sam_grace_period_expired: boolean
|
||||
is_sam_grace_period_expiring_soon: boolean
|
||||
is_sam_grace_period_expiring_today: boolean
|
||||
is_sam_grace_period_expiring_tomorrow: boolean
|
||||
is_sam_grace_period_expiring_in_two_days: boolean
|
||||
is_sam_grace_period_expiring_in_three_days: boolean
|
||||
is_sam_grace_period_expiring_in_four_days: boolean
|
||||
is_sam_grace_period_expiring_in_five_days: boolean
|
||||
is_sam_grace_period_expiring_in_six_days: boolean
|
||||
is_sam_grace_period_expiring_in_seven_days: boolean
|
||||
}
|
||||
|
||||
export interface ChatGPTEntitlement {
|
||||
subscription_id: string | null
|
||||
has_active_subscription: boolean
|
||||
subscription_plan: string
|
||||
expires_at: string | null
|
||||
}
|
||||
|
||||
export interface ChatGPTLastActiveSubscription {
|
||||
subscription_id: string | null
|
||||
purchase_origin_platform: string
|
||||
will_renew: boolean
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 通用 API 类型
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* API 错误响应
|
||||
* 通用错误响应格式
|
||||
*/
|
||||
export interface ApiErrorResponse {
|
||||
error: string
|
||||
message?: string
|
||||
code?: string
|
||||
details?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页请求参数
|
||||
*/
|
||||
export interface PaginationParams {
|
||||
page?: number
|
||||
page_size?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页响应包装
|
||||
*/
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
total_pages: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用列表响应包装
|
||||
*/
|
||||
export interface ListResponse<T> {
|
||||
data: T[]
|
||||
}
|
||||
Reference in New Issue
Block a user