mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-12 22:20:19 +08:00
Merge origin/main into fix/gemini-cli-v1internal
This commit is contained in:
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
@@ -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
|
||||
},
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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 client,Demo 模式由 adapter 统一处理
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 } : {}
|
||||
|
||||
@@ -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
|
||||
}>
|
||||
}> {
|
||||
|
||||
@@ -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 ?? '',
|
||||
|
||||
Reference in New Issue
Block a user