Merge origin/main into fix/gemini-cli-v1internal

This commit is contained in:
Mas0nShi
2026-05-28 11:58:00 +08:00
410 changed files with 38026 additions and 6621 deletions
@@ -1,12 +1,14 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { postMock } = vi.hoisted(() => ({
const { postMock, setTokenMock } = vi.hoisted(() => ({
postMock: vi.fn(),
setTokenMock: vi.fn(),
}))
vi.mock('@/api/client', () => ({
default: {
post: postMock,
setToken: setTokenMock,
},
}))
@@ -15,6 +17,7 @@ import { authApi } from '@/api/auth'
describe('authApi turnstile payloads', () => {
beforeEach(() => {
postMock.mockReset()
setTokenMock.mockReset()
postMock.mockResolvedValue({ data: {} })
})
@@ -42,4 +45,13 @@ describe('authApi turnstile payloads', () => {
turnstile_token: 'turnstile-token',
})
})
it('refreshes auth token without a request body', async () => {
postMock.mockResolvedValue({ data: { access_token: 'new-access-token' } })
await authApi.refreshToken()
expect(postMock).toHaveBeenCalledWith('/api/auth/refresh')
expect(setTokenMock).toHaveBeenCalledWith('new-access-token')
})
})
+34
View File
@@ -1,7 +1,12 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { AxiosAdapter, AxiosInstance, InternalAxiosRequestConfig } from 'axios'
import apiClient, { AUTH_STATE_CHANGE_EVENT } from '@/api/client'
type TestableApiClient = typeof apiClient & {
client: AxiosInstance
}
describe('apiClient auth state change event', () => {
beforeEach(() => {
localStorage.clear()
@@ -28,4 +33,33 @@ describe('apiClient auth state change event', () => {
window.removeEventListener(AUTH_STATE_CHANGE_EVENT, handler as EventListener)
})
it('sends auth refresh without a request body', async () => {
const rawClient = apiClient as TestableApiClient
const previousAdapter = rawClient.client.defaults.adapter
const requests: InternalAxiosRequestConfig[] = []
rawClient.client.defaults.adapter = (async (config: InternalAxiosRequestConfig) => {
requests.push(config)
return {
data: { access_token: 'new-access-token' },
status: 200,
statusText: 'OK',
headers: {},
config,
}
}) as AxiosAdapter
try {
const response = await apiClient.refreshToken()
expect(response.data.access_token).toBe('new-access-token')
expect(requests).toHaveLength(1)
expect(requests[0].url).toBe('/api/auth/refresh')
expect(requests[0].method).toBe('post')
expect(requests[0].data).toBeUndefined()
} finally {
rawClient.client.defaults.adapter = previousAdapter
}
})
})
+52
View File
@@ -0,0 +1,52 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { getMock, cachedRequestMock } = vi.hoisted(() => ({
getMock: vi.fn(),
cachedRequestMock: vi.fn(async (_key: string, fn: () => Promise<unknown>) => fn()),
}))
vi.mock('@/api/client', () => ({
default: {
get: getMock,
},
}))
vi.mock('@/utils/cache', () => ({
cachedRequest: cachedRequestMock,
}))
import { usersApi } from '@/api/users'
describe('usersApi admin list query', () => {
beforeEach(() => {
getMock.mockReset()
cachedRequestMock.mockClear()
getMock.mockResolvedValue({
data: {
items: [],
total: 0,
skip: 0,
limit: 20,
has_more: false,
},
})
})
it('passes creation-time sort parameters to the admin users endpoint', async () => {
await usersApi.getAllUsersPage({
skip: 20,
limit: 10,
sort_by: 'created_at',
sort_order: 'desc',
})
expect(getMock).toHaveBeenCalledWith('/api/admin/users', {
params: {
skip: 20,
limit: 10,
sort_by: 'created_at',
sort_order: 'desc',
},
})
})
})
+220 -2
View File
@@ -96,6 +96,7 @@ export interface UsersExportData {
user_groups?: UserGroupExport[]
users: UserExport[]
standalone_keys?: StandaloneKeyExport[]
usage_aggregates?: UsageAggregateSnapshot
}
export interface AggregateExportData {
@@ -105,6 +106,18 @@ export interface AggregateExportData {
user_data: UsersExportData
}
export type S3BackupScope = 'config' | 'users' | 'data'
export interface S3BackupRunResponse {
message: string
task: {
id: string
task_key: string
status: string
progress_message?: string
}
}
export interface UserGroupExport {
id?: string
name: string
@@ -120,6 +133,7 @@ export interface UserGroupExport {
}
export interface UserExport {
id?: string
email: string
email_verified?: boolean
username: string
@@ -140,10 +154,13 @@ export interface UserExport {
unlimited?: boolean
wallet?: BillingSummary | null
is_active: boolean
request_count?: number
total_tokens?: number
api_keys: UserApiKeyExport[]
}
export interface UserApiKeyExport {
api_key_id?: string
key?: string | null
key_hash: string
key_encrypted?: string | null
@@ -161,15 +178,80 @@ export interface UserApiKeyExport {
expires_at?: string | null
auto_delete_on_expiry?: boolean
total_requests?: number
total_tokens?: number
total_cost_usd?: number
}
// 独立余额 Key 导出结构(与 UserApiKeyExport 相同,但不包含 is_standalone
export type StandaloneKeyExport = Omit<UserApiKeyExport, 'is_standalone'>
export interface StatsDailyAggregateExport {
date_unix_secs: number
total_requests: number
success_requests: number
error_requests: number
input_tokens: number
output_tokens: number
cache_creation_tokens: number
cache_read_tokens: number
total_cost: number
actual_total_cost: number
is_complete: boolean
aggregated_at_unix_secs?: number | null
}
export interface StatsUserDailyAggregateExport {
user_id: string
username?: string | null
date_unix_secs: number
total_requests: number
success_requests: number
error_requests: number
input_tokens: number
output_tokens: number
cache_creation_tokens: number
cache_read_tokens: number
total_cost: number
}
export interface StatsDailyApiKeyAggregateExport {
api_key_id: string
api_key_name?: string | null
date_unix_secs: number
total_requests: number
success_requests: number
error_requests: number
input_tokens: number
output_tokens: number
cache_creation_tokens: number
cache_read_tokens: number
total_cost: number
}
export interface UsageAggregateSnapshot {
stats_daily?: StatsDailyAggregateExport[]
stats_user_daily?: StatsUserDailyAggregateExport[]
stats_daily_api_key?: StatsDailyApiKeyAggregateExport[]
}
export interface UsageAggregateImportCounter {
created: number
updated: number
skipped: number
}
export interface UsageAggregateImportSummary {
stats_daily: UsageAggregateImportCounter
stats_user_daily: UsageAggregateImportCounter
stats_daily_api_key: UsageAggregateImportCounter
skipped_unmapped_user_daily: number
skipped_unmapped_api_key_daily: number
}
export interface GlobalModelExport {
name: string
display_name: string
usage_count?: number | null
default_price_per_request?: number | null
default_tiered_pricing: Record<string, unknown>
supported_capabilities?: string[] | null
@@ -371,12 +453,80 @@ export interface CheckUpdateResponse {
current_version: string
latest_version: string | null
has_update: boolean
updatable: boolean
update_blocker: string | null
release_url: string | null
release_notes: string | null
published_at: string | null
error: string | null
}
export interface SystemUpdateCapabilityResponse {
supported: boolean
build_type: string
update_strategy?: 'self' | 'docker' | 'manual' | string
strategy?: 'self' | 'docker' | 'manual' | string
deployment_topology?: 'single-node' | 'multi-node' | string
topology?: 'single-node' | 'multi-node' | string
enabled: boolean
rollback_available: boolean
task_status: string
task_error: string | null
install_root?: string
base_dir?: string
data_dir?: string
logs_dir?: string
docker_update_command?: string | null
message: string
}
export interface UpdateTaskStatusResponse {
phase: string
error: string | null
output: string | null
progress_label?: string | null
downloaded_bytes?: number | null
total_bytes?: number | null
progress_percent?: number | null
}
export interface UpdateHistoryEntry {
timestamp: string
operation: string
success: boolean
error: string | null
output_tail: string | null
}
export interface UpdateHistoryResponse {
entries: UpdateHistoryEntry[]
}
export interface ApplySystemUpdateResponse {
message: string
started: boolean
need_restart: boolean
}
export interface ReleaseEntry {
version: string
release_url: string | null
release_notes: string | null
published_at: string | null
tarball_url?: string | null
sha256sums_url?: string | null
is_current: boolean
is_newer: boolean
updatable: boolean
update_blocker: string | null
}
export interface ReleasesListResponse {
current_version: string
releases: ReleaseEntry[]
error: string | null
}
// LDAP 配置响应
export interface LdapConfigResponse {
server_url: string | null
@@ -430,6 +580,7 @@ export interface ProviderModelsQueryResponse {
model_test_capabilities?: ModelTestCapabilities | null
}>
error?: string
warning?: string
from_cache?: boolean
}
provider: {
@@ -454,6 +605,7 @@ export interface UsersImportResponse {
users: { created: number; updated: number; skipped: number }
api_keys: { created: number; updated?: number; skipped: number }
standalone_keys?: { created: number; updated?: number; skipped: number }
usage_aggregates?: UsageAggregateImportSummary
errors: string[]
}
}
@@ -904,6 +1056,14 @@ export const adminApi = {
return response.data
},
// 立即执行 S3 备份
async runS3Backup(): Promise<S3BackupRunResponse> {
const response = await apiClient.post<S3BackupRunResponse>(
'/api/admin/system/backups/s3/run'
)
return response.data
},
// 查询 Provider 可用模型(从上游 API 获取)
async queryProviderModels(providerId: string, apiKeyId?: string, forceRefresh = false): Promise<ProviderModelsQueryResponse> {
const response = await apiClient.post<ProviderModelsQueryResponse>(
@@ -995,9 +1155,67 @@ export const adminApi = {
},
// 检查系统更新
async checkUpdate(): Promise<CheckUpdateResponse> {
async checkUpdate(force = false): Promise<CheckUpdateResponse> {
const response = await apiClient.get<CheckUpdateResponse>(
'/api/admin/system/check-update'
'/api/admin/system/check-update',
force ? { params: { force: 'true' } } : undefined
)
return response.data
},
async getSystemReleases(force = false): Promise<ReleasesListResponse> {
const response = await apiClient.get<ReleasesListResponse>(
'/api/admin/system/releases',
force ? { params: { force: 'true' } } : undefined
)
return response.data
},
// 获取一键更新能力
async getSystemUpdateCapability(): Promise<SystemUpdateCapabilityResponse> {
const response = await apiClient.get<SystemUpdateCapabilityResponse>(
'/api/admin/system/update-capability'
)
return response.data
},
// 准备系统一键更新(下载并校验 release 包)
async prepareSystemUpdate(version?: string | null): Promise<ApplySystemUpdateResponse> {
const response = await apiClient.post<ApplySystemUpdateResponse>(
'/api/admin/system/prepare-update',
version ? { version } : undefined
)
return response.data
},
// 触发系统一键重启(切换 release 并退出等待进程管理器拉起)
async applySystemUpdate(version?: string | null): Promise<ApplySystemUpdateResponse> {
const response = await apiClient.post<ApplySystemUpdateResponse>(
'/api/admin/system/apply-update',
version ? { version } : undefined
)
return response.data
},
// 回滚到上一个版本
async rollbackSystemUpdate(): Promise<ApplySystemUpdateResponse> {
const response = await apiClient.post<ApplySystemUpdateResponse>(
'/api/admin/system/rollback'
)
return response.data
},
// 查询更新任务状态
async getUpdateStatus(): Promise<UpdateTaskStatusResponse> {
const response = await apiClient.get<UpdateTaskStatusResponse>(
'/api/admin/system/update-status'
)
return response.data
},
async getUpdateHistory(): Promise<UpdateHistoryResponse> {
const response = await apiClient.get<UpdateHistoryResponse>(
'/api/admin/system/update-history'
)
return response.data
},
+1 -1
View File
@@ -164,7 +164,7 @@ export const authApi = {
},
async refreshToken(): Promise<LoginResponse> {
const response = await apiClient.post<LoginResponse>('/api/auth/refresh', {})
const response = await apiClient.post<LoginResponse>('/api/auth/refresh')
apiClient.setToken(response.data.access_token)
return response.data
},
+1 -1
View File
@@ -323,7 +323,7 @@ class ApiClient {
}
async refreshToken(): Promise<AxiosResponse> {
return this.client.post('/api/auth/refresh', {})
return this.client.post('/api/auth/refresh')
}
// 以下方法直接委托给 axios clientDemo 模式由 adapter 统一处理
+2
View File
@@ -196,6 +196,7 @@ export interface TestModelRequest {
provider_id: string
model_name: string
api_key_id?: string
api_key_ids?: string[]
endpoint_id?: string
message?: string
api_format?: string
@@ -249,6 +250,7 @@ export interface TestModelFailoverRequest {
mode: 'global' | 'direct' | 'pool'
model_name: string
failover_models?: string[]
api_key_ids?: string[]
api_format?: string
endpoint_id?: string
message?: string
+4
View File
@@ -54,6 +54,8 @@ export interface UsageRecordDetail {
id: string
provider?: string // 仅管理员可见
model: string
reasoning_effort?: string | null
service_tier?: string | null
input_tokens: number
effective_input_tokens?: number
output_tokens: number
@@ -351,6 +353,8 @@ export const meApi = {
has_format_conversion?: boolean | null
has_fallback?: boolean | null
target_model?: string | null
reasoning_effort?: string | null
service_tier?: string | null
}>
}> {
const params = ids ? { ids } : {}
+4
View File
@@ -14,6 +14,8 @@ export interface UsageRecord {
provider_id?: string // UUID
provider_name?: string
model: string
reasoning_effort?: string | null
service_tier?: string | null
input_tokens: number
effective_input_tokens?: number
output_tokens: number
@@ -522,6 +524,8 @@ export const usageApi = {
has_format_conversion?: boolean | null
has_fallback?: boolean | null
target_model?: string | null
reasoning_effort?: string | null
service_tier?: string | null
image_progress?: ImageProgress | null
}>
}> {
+8
View File
@@ -6,6 +6,8 @@ import type { BillingPlan, UserPlanEntitlement } from './billing'
export type UserRole = 'admin' | 'audit_admin' | 'user'
export type ListPolicyMode = 'inherit' | 'unrestricted' | 'specific' | 'deny_all'
export type RateLimitPolicyMode = 'inherit' | 'system' | 'custom'
export type AdminUserSortBy = 'created_at'
export type AdminUserSortOrder = 'asc' | 'desc'
export type FeatureSettings = Record<string, unknown>
export interface UserGroupSummary {
@@ -261,6 +263,8 @@ export interface GetAllUsersOptions {
role?: UserRole
is_active?: boolean
group_id?: string
sort_by?: AdminUserSortBy
sort_order?: AdminUserSortOrder
skip?: number
limit?: number
cacheTtlMs?: number
@@ -298,6 +302,8 @@ export const usersApi = {
if (options.role) params.role = options.role
if (options.is_active !== undefined) params.is_active = options.is_active ? 'true' : 'false'
if (options.group_id) params.group_id = options.group_id
if (options.sort_by) params.sort_by = options.sort_by
if (options.sort_order) params.sort_order = options.sort_order
if (options.skip !== undefined) params.skip = options.skip
if (options.limit !== undefined) params.limit = options.limit
@@ -309,6 +315,8 @@ export const usersApi = {
options.role ?? '',
options.is_active ?? '',
options.group_id ?? '',
options.sort_by ?? '',
options.sort_order ?? '',
options.skip ?? '',
options.limit ?? '',
options.cacheKeySuffix ?? '',