mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-03 00:02:28 +08:00
feat: 前端新增 Demo 模式支持
- 新增 demo 配置模块,支持静态托管环境(GitHub Pages/Vercel/Netlify) - API client 集成自定义 adapter,Demo 模式下自动拦截请求返回 mock 数据 - 登录对话框适配 Demo 模式,显示演示账号提示信息 - 添加完整的 mock 数据处理器(用户、配额、用量、供应商等) - 同步 token 状态到 mock handler,支持页面刷新后恢复会话
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
import axios from 'axios'
|
import axios, { getAdapter } from 'axios'
|
||||||
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig, AxiosAdapter } from 'axios'
|
||||||
import { NETWORK_CONFIG, AUTH_CONFIG } from '@/config/constants'
|
import { NETWORK_CONFIG, AUTH_CONFIG } from '@/config/constants'
|
||||||
|
import { isDemoMode } from '@/config/demo'
|
||||||
|
import { handleMockRequest, setMockUserToken } from '@/mocks'
|
||||||
import { log } from '@/utils/logger'
|
import { log } from '@/utils/logger'
|
||||||
|
|
||||||
// 在开发环境下使用代理,生产环境使用环境变量
|
// 在开发环境下使用代理,生产环境使用环境变量
|
||||||
@@ -42,6 +44,39 @@ function isRefreshableAuthError(errorDetail: string): boolean {
|
|||||||
return !nonRefreshableErrors.some((msg) => errorDetail.includes(msg))
|
return !nonRefreshableErrors.some((msg) => errorDetail.includes(msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Demo 模式的自定义 adapter
|
||||||
|
* 在 Demo 模式下拦截请求并返回 mock 数据
|
||||||
|
*/
|
||||||
|
function createDemoAdapter(defaultAdapter: AxiosAdapter) {
|
||||||
|
return async (config: InternalAxiosRequestConfig): Promise<AxiosResponse> => {
|
||||||
|
if (isDemoMode()) {
|
||||||
|
try {
|
||||||
|
const mockResponse = await handleMockRequest({
|
||||||
|
method: config.method?.toUpperCase(),
|
||||||
|
url: config.url,
|
||||||
|
data: config.data,
|
||||||
|
params: config.params,
|
||||||
|
})
|
||||||
|
if (mockResponse) {
|
||||||
|
// 确保响应包含 config
|
||||||
|
mockResponse.config = config
|
||||||
|
return mockResponse
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
// Mock 错误需要附加 config,否则 handleResponseError 会崩溃
|
||||||
|
if (error.response) {
|
||||||
|
error.config = config
|
||||||
|
error.response.config = config
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 非 Demo 模式或没有 mock 响应时,使用默认 adapter
|
||||||
|
return defaultAdapter(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ApiClient {
|
class ApiClient {
|
||||||
private client: AxiosInstance
|
private client: AxiosInstance
|
||||||
private token: string | null = null
|
private token: string | null = null
|
||||||
@@ -57,6 +92,10 @@ class ApiClient {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 设置自定义 adapter 处理 Demo 模式
|
||||||
|
const defaultAdapter = getAdapter(this.client.defaults.adapter)
|
||||||
|
this.client.defaults.adapter = createDemoAdapter(defaultAdapter)
|
||||||
|
|
||||||
this.setupInterceptors()
|
this.setupInterceptors()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +103,7 @@ class ApiClient {
|
|||||||
* 配置请求和响应拦截器
|
* 配置请求和响应拦截器
|
||||||
*/
|
*/
|
||||||
private setupInterceptors(): void {
|
private setupInterceptors(): void {
|
||||||
// 请求拦截器
|
// 请求拦截器 - 仅处理认证
|
||||||
this.client.interceptors.request.use(
|
this.client.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
const requiresAuth = !isPublicEndpoint(config.url, config.method) &&
|
const requiresAuth = !isPublicEndpoint(config.url, config.method) &&
|
||||||
@@ -207,11 +246,19 @@ class ApiClient {
|
|||||||
setToken(token: string): void {
|
setToken(token: string): void {
|
||||||
this.token = token
|
this.token = token
|
||||||
localStorage.setItem('access_token', token)
|
localStorage.setItem('access_token', token)
|
||||||
|
// 同步到 mock handler
|
||||||
|
if (isDemoMode()) {
|
||||||
|
setMockUserToken(token)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getToken(): string | null {
|
getToken(): string | null {
|
||||||
if (!this.token) {
|
if (!this.token) {
|
||||||
this.token = localStorage.getItem('access_token')
|
this.token = localStorage.getItem('access_token')
|
||||||
|
// 页面刷新时,从 localStorage 恢复 token 到 mock handler
|
||||||
|
if (this.token && isDemoMode()) {
|
||||||
|
setMockUserToken(this.token)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return this.token
|
return this.token
|
||||||
}
|
}
|
||||||
@@ -220,12 +267,18 @@ class ApiClient {
|
|||||||
this.token = null
|
this.token = null
|
||||||
localStorage.removeItem('access_token')
|
localStorage.removeItem('access_token')
|
||||||
localStorage.removeItem('refresh_token')
|
localStorage.removeItem('refresh_token')
|
||||||
|
// 同步清除 mock token
|
||||||
|
if (isDemoMode()) {
|
||||||
|
setMockUserToken(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshToken(refreshToken: string): Promise<AxiosResponse> {
|
async refreshToken(refreshToken: string): Promise<AxiosResponse> {
|
||||||
|
// refreshToken 会通过 adapter 处理 Demo 模式
|
||||||
return this.client.post('/api/auth/refresh', { refresh_token: refreshToken })
|
return this.client.post('/api/auth/refresh', { refresh_token: refreshToken })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 以下方法直接委托给 axios client,Demo 模式由 adapter 统一处理
|
||||||
async request<T = any>(config: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
async request<T = any>(config: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||||
return this.client.request<T>(config)
|
return this.client.request<T>(config)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export interface EndpointHealthDetail {
|
|||||||
api_format: string
|
api_format: string
|
||||||
health_score: number
|
health_score: number
|
||||||
is_active: boolean
|
is_active: boolean
|
||||||
|
active_keys?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EndpointHealthEvent {
|
export interface EndpointHealthEvent {
|
||||||
|
|||||||
37
frontend/src/config/demo.ts
Normal file
37
frontend/src/config/demo.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Demo Mode Configuration
|
||||||
|
* 用于 GitHub Pages 等静态托管环境的演示模式
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 检测是否为演示模式环境
|
||||||
|
export function isDemoMode(): boolean {
|
||||||
|
const hostname = window.location.hostname
|
||||||
|
return (
|
||||||
|
hostname.includes('github.io') ||
|
||||||
|
hostname.includes('vercel.app') ||
|
||||||
|
hostname.includes('netlify.app') ||
|
||||||
|
hostname.includes('pages.dev') ||
|
||||||
|
import.meta.env.VITE_DEMO_MODE === 'true'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Demo 账号配置
|
||||||
|
export const DEMO_ACCOUNTS = {
|
||||||
|
admin: {
|
||||||
|
email: 'admin@demo.aether.io',
|
||||||
|
password: 'demo123',
|
||||||
|
hint: '管理员账号'
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
email: 'user@demo.aether.io',
|
||||||
|
password: 'demo123',
|
||||||
|
hint: '普通用户'
|
||||||
|
}
|
||||||
|
} as const
|
||||||
|
|
||||||
|
// Demo 模式提示信息
|
||||||
|
export const DEMO_MODE_INFO = {
|
||||||
|
title: '演示模式',
|
||||||
|
description: '当前处于演示模式,所有数据均为模拟数据,不会产生实际调用。',
|
||||||
|
accountHint: '可使用以下演示账号登录:'
|
||||||
|
} as const
|
||||||
@@ -11,6 +11,43 @@
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Demo 模式提示 -->
|
||||||
|
<div v-if="isDemo" class="rounded-lg border border-primary/20 dark:border-primary/30 bg-primary/5 dark:bg-primary/10 p-4">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<div class="flex-shrink-0 text-primary dark:text-primary/90">
|
||||||
|
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="text-sm font-medium text-foreground">
|
||||||
|
演示模式
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 text-xs text-muted-foreground">
|
||||||
|
当前处于演示模式,所有数据均为模拟数据。
|
||||||
|
</p>
|
||||||
|
<div class="mt-3 space-y-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="fillDemoAccount('admin')"
|
||||||
|
class="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors group"
|
||||||
|
>
|
||||||
|
<span class="inline-flex items-center justify-center w-5 h-5 rounded bg-primary/20 dark:bg-primary/30 text-primary text-[10px] font-bold group-hover:bg-primary/30 dark:group-hover:bg-primary/40 transition-colors">A</span>
|
||||||
|
<span>管理员:admin@demo.aether.io / demo123</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="fillDemoAccount('user')"
|
||||||
|
class="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors group"
|
||||||
|
>
|
||||||
|
<span class="inline-flex items-center justify-center w-5 h-5 rounded bg-muted text-muted-foreground text-[10px] font-bold group-hover:bg-muted/80 transition-colors">U</span>
|
||||||
|
<span>普通用户:user@demo.aether.io / demo123</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 登录表单 -->
|
<!-- 登录表单 -->
|
||||||
<form @submit.prevent="handleLogin" class="space-y-4">
|
<form @submit.prevent="handleLogin" class="space-y-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
@@ -32,14 +69,14 @@
|
|||||||
v-model="form.password"
|
v-model="form.password"
|
||||||
type="password"
|
type="password"
|
||||||
required
|
required
|
||||||
placeholder="••••••••"
|
placeholder="********"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
@keyup.enter="handleLogin"
|
@keyup.enter="handleLogin"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 提示信息 -->
|
<!-- 提示信息 -->
|
||||||
<p class="text-xs text-slate-400 dark:text-muted-foreground/80">
|
<p v-if="!isDemo" class="text-xs text-slate-400 dark:text-muted-foreground/80">
|
||||||
如需开通账户,请联系管理员配置访问权限
|
如需开通账户,请联系管理员配置访问权限
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
@@ -66,7 +103,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch, computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { Dialog } from '@/components/ui'
|
import { Dialog } from '@/components/ui'
|
||||||
import Button from '@/components/ui/button.vue'
|
import Button from '@/components/ui/button.vue'
|
||||||
@@ -74,6 +111,7 @@ import Input from '@/components/ui/input.vue'
|
|||||||
import Label from '@/components/ui/label.vue'
|
import Label from '@/components/ui/label.vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
|
import { isDemoMode, DEMO_ACCOUNTS } from '@/config/demo'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
@@ -88,6 +126,7 @@ const authStore = useAuthStore()
|
|||||||
const { success: showSuccess, warning: showWarning, error: showError } = useToast()
|
const { success: showSuccess, warning: showWarning, error: showError } = useToast()
|
||||||
|
|
||||||
const isOpen = ref(props.modelValue)
|
const isOpen = ref(props.modelValue)
|
||||||
|
const isDemo = computed(() => isDemoMode())
|
||||||
|
|
||||||
watch(() => props.modelValue, (val) => {
|
watch(() => props.modelValue, (val) => {
|
||||||
isOpen.value = val
|
isOpen.value = val
|
||||||
@@ -109,6 +148,12 @@ const form = ref({
|
|||||||
password: ''
|
password: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function fillDemoAccount(type: 'admin' | 'user') {
|
||||||
|
const account = DEMO_ACCOUNTS[type]
|
||||||
|
form.value.email = account.email
|
||||||
|
form.value.password = account.password
|
||||||
|
}
|
||||||
|
|
||||||
async function handleLogin() {
|
async function handleLogin() {
|
||||||
if (!form.value.email || !form.value.password) {
|
if (!form.value.email || !form.value.password) {
|
||||||
showWarning('请输入邮箱和密码')
|
showWarning('请输入邮箱和密码')
|
||||||
|
|||||||
868
frontend/src/mocks/data.ts
Normal file
868
frontend/src/mocks/data.ts
Normal file
@@ -0,0 +1,868 @@
|
|||||||
|
/**
|
||||||
|
* Demo Mode Mock Data
|
||||||
|
* 演示模式的模拟数据
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { User, LoginResponse } from '@/api/auth'
|
||||||
|
import type { DashboardStatsResponse, RecentRequest, ProviderStatus, DailyStatsResponse } from '@/api/dashboard'
|
||||||
|
import type { User as AdminUser, ApiKey } from '@/api/users'
|
||||||
|
import type { AdminApiKey, AdminApiKeysResponse } from '@/api/admin'
|
||||||
|
import type { Profile, UsageResponse } from '@/api/me'
|
||||||
|
import type { ProviderWithEndpointsSummary, GlobalModelResponse } from '@/api/endpoints/types'
|
||||||
|
|
||||||
|
// ========== 用户数据 ==========
|
||||||
|
|
||||||
|
export const MOCK_ADMIN_USER: User = {
|
||||||
|
id: 'demo-admin-uuid-0001',
|
||||||
|
username: 'Demo Admin',
|
||||||
|
email: 'admin@demo.aether.io',
|
||||||
|
role: 'admin',
|
||||||
|
is_active: true,
|
||||||
|
quota_usd: null,
|
||||||
|
used_usd: 156.78,
|
||||||
|
total_usd: 1234.56,
|
||||||
|
allowed_providers: null,
|
||||||
|
allowed_endpoints: null,
|
||||||
|
allowed_models: null,
|
||||||
|
created_at: '2024-01-01T00:00:00Z',
|
||||||
|
last_login_at: new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MOCK_NORMAL_USER: User = {
|
||||||
|
id: 'demo-user-uuid-0002',
|
||||||
|
username: 'Demo User',
|
||||||
|
email: 'user@demo.aether.io',
|
||||||
|
role: 'user',
|
||||||
|
is_active: true,
|
||||||
|
quota_usd: 100,
|
||||||
|
used_usd: 45.32,
|
||||||
|
total_usd: 245.32,
|
||||||
|
allowed_providers: null,
|
||||||
|
allowed_endpoints: null,
|
||||||
|
allowed_models: null,
|
||||||
|
created_at: '2024-06-01T00:00:00Z',
|
||||||
|
last_login_at: new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MOCK_LOGIN_RESPONSE_ADMIN: LoginResponse = {
|
||||||
|
access_token: 'demo-access-token-admin',
|
||||||
|
refresh_token: 'demo-refresh-token-admin',
|
||||||
|
token_type: 'bearer',
|
||||||
|
expires_in: 3600,
|
||||||
|
user_id: MOCK_ADMIN_USER.id,
|
||||||
|
email: MOCK_ADMIN_USER.email,
|
||||||
|
username: MOCK_ADMIN_USER.username,
|
||||||
|
role: 'admin'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MOCK_LOGIN_RESPONSE_USER: LoginResponse = {
|
||||||
|
access_token: 'demo-access-token-user',
|
||||||
|
refresh_token: 'demo-refresh-token-user',
|
||||||
|
token_type: 'bearer',
|
||||||
|
expires_in: 3600,
|
||||||
|
user_id: MOCK_NORMAL_USER.id,
|
||||||
|
email: MOCK_NORMAL_USER.email,
|
||||||
|
username: MOCK_NORMAL_USER.username,
|
||||||
|
role: 'user'
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Profile 数据 ==========
|
||||||
|
|
||||||
|
export const MOCK_ADMIN_PROFILE: Profile = {
|
||||||
|
id: MOCK_ADMIN_USER.id!,
|
||||||
|
email: MOCK_ADMIN_USER.email!,
|
||||||
|
username: MOCK_ADMIN_USER.username,
|
||||||
|
role: 'admin',
|
||||||
|
is_active: true,
|
||||||
|
quota_usd: null,
|
||||||
|
used_usd: 156.78,
|
||||||
|
total_usd: 1234.56,
|
||||||
|
created_at: '2024-01-01T00:00:00Z',
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
last_login_at: new Date().toISOString(),
|
||||||
|
preferences: {
|
||||||
|
theme: 'auto',
|
||||||
|
language: 'zh-CN'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MOCK_USER_PROFILE: Profile = {
|
||||||
|
id: MOCK_NORMAL_USER.id!,
|
||||||
|
email: MOCK_NORMAL_USER.email!,
|
||||||
|
username: MOCK_NORMAL_USER.username,
|
||||||
|
role: 'user',
|
||||||
|
is_active: true,
|
||||||
|
quota_usd: 100,
|
||||||
|
used_usd: 45.32,
|
||||||
|
total_usd: 245.32,
|
||||||
|
created_at: '2024-06-01T00:00:00Z',
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
last_login_at: new Date().toISOString(),
|
||||||
|
preferences: {
|
||||||
|
theme: 'auto',
|
||||||
|
language: 'zh-CN'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Dashboard 数据 ==========
|
||||||
|
|
||||||
|
export const MOCK_DASHBOARD_STATS: DashboardStatsResponse = {
|
||||||
|
stats: [
|
||||||
|
{
|
||||||
|
name: '今日请求',
|
||||||
|
value: '1,234',
|
||||||
|
subValue: '成功率 99.2%',
|
||||||
|
change: '+12.5%',
|
||||||
|
changeType: 'increase',
|
||||||
|
icon: 'Activity'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '今日 Token',
|
||||||
|
value: '2.5M',
|
||||||
|
subValue: '输入 1.8M / 输出 0.7M',
|
||||||
|
change: '+8.3%',
|
||||||
|
changeType: 'increase',
|
||||||
|
icon: 'Zap'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '今日费用',
|
||||||
|
value: '$45.67',
|
||||||
|
subValue: '节省 $12.34 (21%)',
|
||||||
|
change: '-5.2%',
|
||||||
|
changeType: 'decrease',
|
||||||
|
icon: 'DollarSign'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '活跃用户',
|
||||||
|
value: '28',
|
||||||
|
subValue: '总用户 156',
|
||||||
|
change: '+3',
|
||||||
|
changeType: 'increase',
|
||||||
|
icon: 'Users'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
today: {
|
||||||
|
requests: 1234,
|
||||||
|
tokens: 2500000,
|
||||||
|
cost: 45.67,
|
||||||
|
actual_cost: 33.33,
|
||||||
|
cache_creation_tokens: 50000,
|
||||||
|
cache_read_tokens: 200000
|
||||||
|
},
|
||||||
|
api_keys: {
|
||||||
|
total: 45,
|
||||||
|
active: 38
|
||||||
|
},
|
||||||
|
tokens: {
|
||||||
|
month: 75000000
|
||||||
|
},
|
||||||
|
system_health: {
|
||||||
|
avg_response_time: 1.23,
|
||||||
|
error_rate: 0.8,
|
||||||
|
error_requests: 10,
|
||||||
|
fallback_count: 5,
|
||||||
|
total_requests: 1234
|
||||||
|
},
|
||||||
|
cost_stats: {
|
||||||
|
total_cost: 45.67,
|
||||||
|
total_actual_cost: 33.33,
|
||||||
|
cost_savings: 12.34
|
||||||
|
},
|
||||||
|
cache_stats: {
|
||||||
|
cache_creation_tokens: 50000,
|
||||||
|
cache_read_tokens: 200000,
|
||||||
|
cache_creation_cost: 0.25,
|
||||||
|
cache_read_cost: 0.10,
|
||||||
|
cache_hit_rate: 0.35,
|
||||||
|
total_cache_tokens: 250000
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
total: 156,
|
||||||
|
active: 28
|
||||||
|
},
|
||||||
|
token_breakdown: {
|
||||||
|
input: 1800000,
|
||||||
|
output: 700000,
|
||||||
|
cache_creation: 50000,
|
||||||
|
cache_read: 200000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MOCK_RECENT_REQUESTS: RecentRequest[] = [
|
||||||
|
{ id: 'req-001', user: 'alice', model: 'claude-sonnet-4-20250514', tokens: 15234, time: '2 分钟前' },
|
||||||
|
{ id: 'req-002', user: 'bob', model: 'gpt-4o', tokens: 8765, time: '5 分钟前' },
|
||||||
|
{ id: 'req-003', user: 'charlie', model: 'claude-opus-4-20250514', tokens: 32100, time: '8 分钟前' },
|
||||||
|
{ id: 'req-004', user: 'diana', model: 'gemini-2.0-flash', tokens: 4521, time: '12 分钟前' },
|
||||||
|
{ id: 'req-005', user: 'eve', model: 'claude-sonnet-4-20250514', tokens: 9876, time: '15 分钟前' },
|
||||||
|
{ id: 'req-006', user: 'frank', model: 'gpt-4o-mini', tokens: 2345, time: '18 分钟前' },
|
||||||
|
{ id: 'req-007', user: 'grace', model: 'claude-haiku-3-5-20241022', tokens: 6789, time: '22 分钟前' },
|
||||||
|
{ id: 'req-008', user: 'henry', model: 'gemini-2.5-pro', tokens: 12345, time: '25 分钟前' }
|
||||||
|
]
|
||||||
|
|
||||||
|
export const MOCK_PROVIDER_STATUS: ProviderStatus[] = [
|
||||||
|
{ name: 'Anthropic Official', status: 'active', requests: 456 },
|
||||||
|
{ name: 'OpenAI Official', status: 'active', requests: 389 },
|
||||||
|
{ name: 'Google AI', status: 'active', requests: 234 },
|
||||||
|
{ name: 'AWS Bedrock', status: 'active', requests: 89 },
|
||||||
|
{ name: 'Azure OpenAI', status: 'inactive', requests: 0 },
|
||||||
|
{ name: 'Vertex AI', status: 'active', requests: 66 }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 生成过去7天的每日统计数据
|
||||||
|
function generateDailyStats(): DailyStatsResponse {
|
||||||
|
const dailyStats = []
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
for (let i = 6; i >= 0; i--) {
|
||||||
|
const date = new Date(now)
|
||||||
|
date.setDate(date.getDate() - i)
|
||||||
|
const dateStr = date.toISOString().split('T')[0]
|
||||||
|
|
||||||
|
const baseRequests = 800 + Math.floor(Math.random() * 600)
|
||||||
|
const baseTokens = 1500000 + Math.floor(Math.random() * 1500000)
|
||||||
|
const baseCost = 30 + Math.random() * 30
|
||||||
|
|
||||||
|
dailyStats.push({
|
||||||
|
date: dateStr,
|
||||||
|
requests: baseRequests,
|
||||||
|
tokens: baseTokens,
|
||||||
|
cost: Number(baseCost.toFixed(2)),
|
||||||
|
avg_response_time: 0.8 + Math.random() * 0.8,
|
||||||
|
unique_models: 8 + Math.floor(Math.random() * 5),
|
||||||
|
unique_providers: 4 + Math.floor(Math.random() * 3),
|
||||||
|
model_breakdown: [
|
||||||
|
{ model: 'claude-sonnet-4-20250514', requests: Math.floor(baseRequests * 0.35), tokens: Math.floor(baseTokens * 0.35), cost: Number((baseCost * 0.35).toFixed(2)) },
|
||||||
|
{ model: 'gpt-4o', requests: Math.floor(baseRequests * 0.25), tokens: Math.floor(baseTokens * 0.25), cost: Number((baseCost * 0.25).toFixed(2)) },
|
||||||
|
{ model: 'claude-opus-4-20250514', requests: Math.floor(baseRequests * 0.15), tokens: Math.floor(baseTokens * 0.15), cost: Number((baseCost * 0.20).toFixed(2)) },
|
||||||
|
{ model: 'gemini-2.0-flash', requests: Math.floor(baseRequests * 0.15), tokens: Math.floor(baseTokens * 0.15), cost: Number((baseCost * 0.10).toFixed(2)) },
|
||||||
|
{ model: 'claude-haiku-3-5-20241022', requests: Math.floor(baseRequests * 0.10), tokens: Math.floor(baseTokens * 0.10), cost: Number((baseCost * 0.10).toFixed(2)) }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
daily_stats: dailyStats,
|
||||||
|
model_summary: [
|
||||||
|
{ model: 'claude-sonnet-4-20250514', requests: 2456, tokens: 8500000, cost: 125.45, avg_response_time: 1.2, cost_per_request: 0.051, tokens_per_request: 3461 },
|
||||||
|
{ model: 'gpt-4o', requests: 1823, tokens: 6200000, cost: 98.32, avg_response_time: 0.9, cost_per_request: 0.054, tokens_per_request: 3401 },
|
||||||
|
{ model: 'claude-opus-4-20250514', requests: 987, tokens: 4100000, cost: 156.78, avg_response_time: 2.1, cost_per_request: 0.159, tokens_per_request: 4154 },
|
||||||
|
{ model: 'gemini-2.0-flash', requests: 1234, tokens: 3800000, cost: 28.56, avg_response_time: 0.6, cost_per_request: 0.023, tokens_per_request: 3079 },
|
||||||
|
{ model: 'claude-haiku-3-5-20241022', requests: 2100, tokens: 5200000, cost: 32.10, avg_response_time: 0.5, cost_per_request: 0.015, tokens_per_request: 2476 }
|
||||||
|
],
|
||||||
|
period: {
|
||||||
|
start_date: dailyStats[0].date,
|
||||||
|
end_date: dailyStats[dailyStats.length - 1].date,
|
||||||
|
days: 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MOCK_DAILY_STATS = generateDailyStats()
|
||||||
|
|
||||||
|
// ========== 用户管理数据 ==========
|
||||||
|
|
||||||
|
export const MOCK_ALL_USERS: AdminUser[] = [
|
||||||
|
{
|
||||||
|
id: 'demo-admin-uuid-0001',
|
||||||
|
username: 'Demo Admin',
|
||||||
|
email: 'admin@demo.aether.io',
|
||||||
|
role: 'admin',
|
||||||
|
is_active: true,
|
||||||
|
quota_usd: null,
|
||||||
|
used_usd: 156.78,
|
||||||
|
total_usd: 1234.56,
|
||||||
|
allowed_providers: null,
|
||||||
|
allowed_endpoints: null,
|
||||||
|
allowed_models: null,
|
||||||
|
created_at: '2024-01-01T00:00:00Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'demo-user-uuid-0002',
|
||||||
|
username: 'Demo User',
|
||||||
|
email: 'user@demo.aether.io',
|
||||||
|
role: 'user',
|
||||||
|
is_active: true,
|
||||||
|
quota_usd: 100,
|
||||||
|
used_usd: 45.32,
|
||||||
|
total_usd: 245.32,
|
||||||
|
allowed_providers: null,
|
||||||
|
allowed_endpoints: null,
|
||||||
|
allowed_models: null,
|
||||||
|
created_at: '2024-06-01T00:00:00Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'demo-user-uuid-0003',
|
||||||
|
username: 'Alice Wang',
|
||||||
|
email: 'alice@example.com',
|
||||||
|
role: 'user',
|
||||||
|
is_active: true,
|
||||||
|
quota_usd: 50,
|
||||||
|
used_usd: 23.45,
|
||||||
|
total_usd: 123.45,
|
||||||
|
allowed_providers: null,
|
||||||
|
allowed_endpoints: null,
|
||||||
|
allowed_models: null,
|
||||||
|
created_at: '2024-03-15T00:00:00Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'demo-user-uuid-0004',
|
||||||
|
username: 'Bob Zhang',
|
||||||
|
email: 'bob@example.com',
|
||||||
|
role: 'user',
|
||||||
|
is_active: true,
|
||||||
|
quota_usd: 200,
|
||||||
|
used_usd: 89.12,
|
||||||
|
total_usd: 589.12,
|
||||||
|
allowed_providers: null,
|
||||||
|
allowed_endpoints: null,
|
||||||
|
allowed_models: null,
|
||||||
|
created_at: '2024-02-20T00:00:00Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'demo-user-uuid-0005',
|
||||||
|
username: 'Charlie Li',
|
||||||
|
email: 'charlie@example.com',
|
||||||
|
role: 'user',
|
||||||
|
is_active: false,
|
||||||
|
quota_usd: 30,
|
||||||
|
used_usd: 30.00,
|
||||||
|
total_usd: 30.00,
|
||||||
|
allowed_providers: null,
|
||||||
|
allowed_endpoints: null,
|
||||||
|
allowed_models: null,
|
||||||
|
created_at: '2024-04-10T00:00:00Z'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// ========== API Key 数据 ==========
|
||||||
|
|
||||||
|
export const MOCK_USER_API_KEYS: ApiKey[] = [
|
||||||
|
{
|
||||||
|
id: 'key-uuid-001',
|
||||||
|
key_display: 'sk-ae...x7f9',
|
||||||
|
name: '开发环境',
|
||||||
|
created_at: '2024-06-15T00:00:00Z',
|
||||||
|
last_used_at: new Date().toISOString(),
|
||||||
|
is_active: true,
|
||||||
|
is_standalone: false,
|
||||||
|
total_requests: 1234,
|
||||||
|
total_cost_usd: 45.67
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'key-uuid-002',
|
||||||
|
key_display: 'sk-ae...m2k8',
|
||||||
|
name: '生产环境',
|
||||||
|
created_at: '2024-07-01T00:00:00Z',
|
||||||
|
last_used_at: new Date().toISOString(),
|
||||||
|
is_active: true,
|
||||||
|
is_standalone: false,
|
||||||
|
total_requests: 5678,
|
||||||
|
total_cost_usd: 123.45
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'key-uuid-003',
|
||||||
|
key_display: 'sk-ae...p9q1',
|
||||||
|
name: '测试用途',
|
||||||
|
created_at: '2024-08-01T00:00:00Z',
|
||||||
|
is_active: false,
|
||||||
|
is_standalone: false,
|
||||||
|
total_requests: 100,
|
||||||
|
total_cost_usd: 2.34
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export const MOCK_ADMIN_API_KEYS: AdminApiKeysResponse = {
|
||||||
|
api_keys: [
|
||||||
|
{
|
||||||
|
id: 'standalone-key-001',
|
||||||
|
user_id: 'demo-user-uuid-0002',
|
||||||
|
user_email: 'user@demo.aether.io',
|
||||||
|
username: 'Demo User',
|
||||||
|
name: '独立余额 Key #1',
|
||||||
|
key_display: 'sk-sa...abc1',
|
||||||
|
is_active: true,
|
||||||
|
is_standalone: true,
|
||||||
|
balance_used_usd: 25.50,
|
||||||
|
current_balance_usd: 74.50,
|
||||||
|
total_requests: 500,
|
||||||
|
total_tokens: 1500000,
|
||||||
|
total_cost_usd: 25.50,
|
||||||
|
created_at: '2024-09-01T00:00:00Z',
|
||||||
|
last_used_at: new Date().toISOString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'standalone-key-002',
|
||||||
|
user_id: 'demo-user-uuid-0003',
|
||||||
|
user_email: 'alice@example.com',
|
||||||
|
username: 'Alice Wang',
|
||||||
|
name: '独立余额 Key #2',
|
||||||
|
key_display: 'sk-sa...def2',
|
||||||
|
is_active: true,
|
||||||
|
is_standalone: true,
|
||||||
|
balance_used_usd: 45.00,
|
||||||
|
current_balance_usd: 55.00,
|
||||||
|
total_requests: 800,
|
||||||
|
total_tokens: 2400000,
|
||||||
|
total_cost_usd: 45.00,
|
||||||
|
rate_limit: 60,
|
||||||
|
created_at: '2024-08-15T00:00:00Z',
|
||||||
|
last_used_at: new Date().toISOString()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
total: 2,
|
||||||
|
limit: 20,
|
||||||
|
skip: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Provider 数据 ==========
|
||||||
|
|
||||||
|
export const MOCK_PROVIDERS: ProviderWithEndpointsSummary[] = [
|
||||||
|
{
|
||||||
|
id: 'provider-001',
|
||||||
|
name: 'duck_coding_free',
|
||||||
|
display_name: 'DuckCodingFree',
|
||||||
|
description: '',
|
||||||
|
website: 'https://duckcoding.com',
|
||||||
|
provider_priority: 1,
|
||||||
|
billing_type: 'free_tier',
|
||||||
|
monthly_used_usd: 0.0,
|
||||||
|
is_active: true,
|
||||||
|
total_endpoints: 3,
|
||||||
|
active_endpoints: 3,
|
||||||
|
total_keys: 3,
|
||||||
|
active_keys: 3,
|
||||||
|
total_models: 7,
|
||||||
|
active_models: 7,
|
||||||
|
avg_health_score: 0.91,
|
||||||
|
unhealthy_endpoints: 0,
|
||||||
|
api_formats: ['CLAUDE_CLI', 'GEMINI_CLI', 'OPENAI_CLI'],
|
||||||
|
endpoint_health_details: [
|
||||||
|
{ api_format: 'CLAUDE_CLI', health_score: 0.73, is_active: true, active_keys: 1 },
|
||||||
|
{ api_format: 'GEMINI_CLI', health_score: 1.0, is_active: true, active_keys: 1 },
|
||||||
|
{ api_format: 'OPENAI_CLI', health_score: 1.0, is_active: true, active_keys: 1 }
|
||||||
|
],
|
||||||
|
created_at: '2024-12-09T14:10:36.446217+08:00',
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'provider-002',
|
||||||
|
name: 'open_claude_code',
|
||||||
|
display_name: 'OpenClaudeCode',
|
||||||
|
description: '',
|
||||||
|
website: 'https://www.openclaudecode.cn',
|
||||||
|
provider_priority: 2,
|
||||||
|
billing_type: 'pay_as_you_go',
|
||||||
|
monthly_used_usd: 545.18,
|
||||||
|
is_active: true,
|
||||||
|
total_endpoints: 2,
|
||||||
|
active_endpoints: 2,
|
||||||
|
total_keys: 3,
|
||||||
|
active_keys: 3,
|
||||||
|
total_models: 3,
|
||||||
|
active_models: 1,
|
||||||
|
avg_health_score: 0.825,
|
||||||
|
unhealthy_endpoints: 0,
|
||||||
|
api_formats: ['CLAUDE', 'CLAUDE_CLI'],
|
||||||
|
endpoint_health_details: [
|
||||||
|
{ api_format: 'CLAUDE', health_score: 1.0, is_active: true, active_keys: 2 },
|
||||||
|
{ api_format: 'CLAUDE_CLI', health_score: 0.65, is_active: true, active_keys: 1 }
|
||||||
|
],
|
||||||
|
created_at: '2024-12-07T22:58:15.044538+08:00',
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'provider-003',
|
||||||
|
name: '88_code',
|
||||||
|
display_name: '88Code',
|
||||||
|
description: '',
|
||||||
|
website: 'https://www.88code.org/',
|
||||||
|
provider_priority: 3,
|
||||||
|
billing_type: 'pay_as_you_go',
|
||||||
|
monthly_used_usd: 33.36,
|
||||||
|
is_active: true,
|
||||||
|
total_endpoints: 2,
|
||||||
|
active_endpoints: 2,
|
||||||
|
total_keys: 2,
|
||||||
|
active_keys: 2,
|
||||||
|
total_models: 5,
|
||||||
|
active_models: 5,
|
||||||
|
avg_health_score: 1.0,
|
||||||
|
unhealthy_endpoints: 0,
|
||||||
|
api_formats: ['CLAUDE_CLI', 'OPENAI_CLI'],
|
||||||
|
endpoint_health_details: [
|
||||||
|
{ api_format: 'CLAUDE_CLI', health_score: 1.0, is_active: true, active_keys: 1 },
|
||||||
|
{ api_format: 'OPENAI_CLI', health_score: 1.0, is_active: true, active_keys: 1 }
|
||||||
|
],
|
||||||
|
created_at: '2024-12-07T22:56:46.361092+08:00',
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'provider-004',
|
||||||
|
name: 'ikun_code',
|
||||||
|
display_name: 'IKunCode',
|
||||||
|
description: '',
|
||||||
|
website: 'https://api.ikuncode.cc',
|
||||||
|
provider_priority: 4,
|
||||||
|
billing_type: 'pay_as_you_go',
|
||||||
|
monthly_used_usd: 268.65,
|
||||||
|
is_active: true,
|
||||||
|
total_endpoints: 4,
|
||||||
|
active_endpoints: 4,
|
||||||
|
total_keys: 3,
|
||||||
|
active_keys: 3,
|
||||||
|
total_models: 7,
|
||||||
|
active_models: 7,
|
||||||
|
avg_health_score: 1.0,
|
||||||
|
unhealthy_endpoints: 0,
|
||||||
|
api_formats: ['CLAUDE_CLI', 'GEMINI', 'GEMINI_CLI', 'OPENAI_CLI'],
|
||||||
|
endpoint_health_details: [
|
||||||
|
{ api_format: 'CLAUDE_CLI', health_score: 1.0, is_active: true, active_keys: 1 },
|
||||||
|
{ api_format: 'GEMINI', health_score: 1.0, is_active: true, active_keys: 1 },
|
||||||
|
{ api_format: 'GEMINI_CLI', health_score: 1.0, is_active: true, active_keys: 1 },
|
||||||
|
{ api_format: 'OPENAI_CLI', health_score: 1.0, is_active: true, active_keys: 1 }
|
||||||
|
],
|
||||||
|
created_at: '2024-12-07T15:16:55.807595+08:00',
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'provider-005',
|
||||||
|
name: 'duck_coding',
|
||||||
|
display_name: 'DuckCoding',
|
||||||
|
description: '',
|
||||||
|
website: 'https://duckcoding.com',
|
||||||
|
provider_priority: 5,
|
||||||
|
billing_type: 'pay_as_you_go',
|
||||||
|
monthly_used_usd: 5.29,
|
||||||
|
is_active: true,
|
||||||
|
total_endpoints: 6,
|
||||||
|
active_endpoints: 6,
|
||||||
|
total_keys: 11,
|
||||||
|
active_keys: 11,
|
||||||
|
total_models: 8,
|
||||||
|
active_models: 8,
|
||||||
|
avg_health_score: 0.863,
|
||||||
|
unhealthy_endpoints: 1,
|
||||||
|
api_formats: ['CLAUDE', 'CLAUDE_CLI', 'GEMINI', 'GEMINI_CLI', 'OPENAI', 'OPENAI_CLI'],
|
||||||
|
endpoint_health_details: [
|
||||||
|
{ api_format: 'CLAUDE', health_score: 1.0, is_active: true, active_keys: 2 },
|
||||||
|
{ api_format: 'CLAUDE_CLI', health_score: 0.48, is_active: true, active_keys: 2 },
|
||||||
|
{ api_format: 'GEMINI', health_score: 1.0, is_active: true, active_keys: 2 },
|
||||||
|
{ api_format: 'GEMINI_CLI', health_score: 0.85, is_active: true, active_keys: 2 },
|
||||||
|
{ api_format: 'OPENAI', health_score: 0.85, is_active: true, active_keys: 2 },
|
||||||
|
{ api_format: 'OPENAI_CLI', health_score: 1.0, is_active: true, active_keys: 1 }
|
||||||
|
],
|
||||||
|
created_at: '2024-12-07T22:56:09.712806+08:00',
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'provider-006',
|
||||||
|
name: 'privnode',
|
||||||
|
display_name: 'Privnode',
|
||||||
|
description: '',
|
||||||
|
website: 'https://privnode.com',
|
||||||
|
provider_priority: 6,
|
||||||
|
billing_type: 'pay_as_you_go',
|
||||||
|
monthly_used_usd: 0.0,
|
||||||
|
is_active: true,
|
||||||
|
total_endpoints: 0,
|
||||||
|
active_endpoints: 0,
|
||||||
|
total_keys: 0,
|
||||||
|
active_keys: 0,
|
||||||
|
total_models: 6,
|
||||||
|
active_models: 6,
|
||||||
|
avg_health_score: 1.0,
|
||||||
|
unhealthy_endpoints: 0,
|
||||||
|
api_formats: [],
|
||||||
|
endpoint_health_details: [],
|
||||||
|
created_at: '2024-12-07T22:57:18.069024+08:00',
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'provider-007',
|
||||||
|
name: 'undying_api',
|
||||||
|
display_name: 'UndyingAPI',
|
||||||
|
description: '',
|
||||||
|
website: 'https://vip.undyingapi.com',
|
||||||
|
provider_priority: 7,
|
||||||
|
billing_type: 'pay_as_you_go',
|
||||||
|
monthly_used_usd: 6.6,
|
||||||
|
is_active: true,
|
||||||
|
total_endpoints: 1,
|
||||||
|
active_endpoints: 1,
|
||||||
|
total_keys: 1,
|
||||||
|
active_keys: 1,
|
||||||
|
total_models: 1,
|
||||||
|
active_models: 1,
|
||||||
|
avg_health_score: 1.0,
|
||||||
|
unhealthy_endpoints: 0,
|
||||||
|
api_formats: ['GEMINI'],
|
||||||
|
endpoint_health_details: [
|
||||||
|
{ api_format: 'GEMINI', health_score: 1.0, is_active: true, active_keys: 1 }
|
||||||
|
],
|
||||||
|
created_at: '2024-12-07T23:00:42.559105+08:00',
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// ========== GlobalModel 数据 ==========
|
||||||
|
|
||||||
|
export const MOCK_GLOBAL_MODELS: GlobalModelResponse[] = [
|
||||||
|
{
|
||||||
|
id: 'gm-001',
|
||||||
|
name: 'claude-haiku-4-5-20251001',
|
||||||
|
display_name: 'claude-haiku-4-5',
|
||||||
|
description: 'Anthropic 最快速的 Claude 4 系列模型',
|
||||||
|
is_active: true,
|
||||||
|
default_tiered_pricing: {
|
||||||
|
tiers: [{ up_to: null, input_price_per_1m: 1.00, output_price_per_1m: 5.00, cache_creation_price_per_1m: 1.25, cache_read_price_per_1m: 0.1 }]
|
||||||
|
},
|
||||||
|
default_supports_vision: true,
|
||||||
|
default_supports_function_calling: true,
|
||||||
|
default_supports_streaming: true,
|
||||||
|
default_supports_extended_thinking: true,
|
||||||
|
provider_count: 3,
|
||||||
|
alias_count: 2,
|
||||||
|
created_at: '2024-01-01T00:00:00Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gm-002',
|
||||||
|
name: 'claude-opus-4-5-20251101',
|
||||||
|
display_name: 'claude-opus-4-5',
|
||||||
|
description: 'Anthropic 最强大的模型',
|
||||||
|
is_active: true,
|
||||||
|
default_tiered_pricing: {
|
||||||
|
tiers: [{ up_to: null, input_price_per_1m: 5.00, output_price_per_1m: 25.00, cache_creation_price_per_1m: 6.25, cache_read_price_per_1m: 0.5 }]
|
||||||
|
},
|
||||||
|
default_supports_vision: true,
|
||||||
|
default_supports_function_calling: true,
|
||||||
|
default_supports_streaming: true,
|
||||||
|
default_supports_extended_thinking: true,
|
||||||
|
provider_count: 2,
|
||||||
|
alias_count: 1,
|
||||||
|
created_at: '2024-01-01T00:00:00Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gm-003',
|
||||||
|
name: 'claude-sonnet-4-5-20250929',
|
||||||
|
display_name: 'claude-sonnet-4-5',
|
||||||
|
description: 'Anthropic 平衡型模型,支持 1h 缓存和 CLI 1M 上下文',
|
||||||
|
is_active: true,
|
||||||
|
default_tiered_pricing: {
|
||||||
|
tiers: [
|
||||||
|
{
|
||||||
|
"up_to": 200000,
|
||||||
|
"input_price_per_1m": 3,
|
||||||
|
"output_price_per_1m": 15,
|
||||||
|
"cache_creation_price_per_1m": 3.75,
|
||||||
|
"cache_read_price_per_1m": 0.3,
|
||||||
|
"cache_ttl_pricing": [
|
||||||
|
{
|
||||||
|
"ttl_minutes": 60,
|
||||||
|
"cache_creation_price_per_1m": 6
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"up_to": null,
|
||||||
|
"input_price_per_1m": 6,
|
||||||
|
"output_price_per_1m": 22.5,
|
||||||
|
"cache_creation_price_per_1m": 7.5,
|
||||||
|
"cache_read_price_per_1m": 0.6,
|
||||||
|
"cache_ttl_pricing": [
|
||||||
|
{
|
||||||
|
"ttl_minutes": 60,
|
||||||
|
"cache_creation_price_per_1m": 12
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
default_supports_vision: true,
|
||||||
|
default_supports_function_calling: true,
|
||||||
|
default_supports_streaming: true,
|
||||||
|
default_supports_extended_thinking: true,
|
||||||
|
supported_capabilities: ['cache_1h', 'cli_1m'],
|
||||||
|
provider_count: 3,
|
||||||
|
alias_count: 2,
|
||||||
|
created_at: '2024-01-01T00:00:00Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gm-004',
|
||||||
|
name: 'gemini-3-pro-image-preview',
|
||||||
|
display_name: 'gemini-3-pro-image-preview',
|
||||||
|
description: 'Google Gemini 3 Pro 图像生成预览版',
|
||||||
|
is_active: true,
|
||||||
|
default_price_per_request: 0.300,
|
||||||
|
default_tiered_pricing: {
|
||||||
|
tiers: []
|
||||||
|
},
|
||||||
|
default_supports_vision: true,
|
||||||
|
default_supports_function_calling: false,
|
||||||
|
default_supports_streaming: true,
|
||||||
|
default_supports_image_generation: true,
|
||||||
|
provider_count: 1,
|
||||||
|
alias_count: 0,
|
||||||
|
created_at: '2024-01-01T00:00:00Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gm-005',
|
||||||
|
name: 'gemini-3-pro-preview',
|
||||||
|
display_name: 'gemini-3-pro-preview',
|
||||||
|
description: 'Google Gemini 3 Pro 预览版',
|
||||||
|
is_active: true,
|
||||||
|
default_tiered_pricing: {
|
||||||
|
tiers: [{ up_to: null, input_price_per_1m: 2.00, output_price_per_1m: 12.00 }]
|
||||||
|
},
|
||||||
|
default_supports_vision: true,
|
||||||
|
default_supports_function_calling: true,
|
||||||
|
default_supports_streaming: true,
|
||||||
|
default_supports_extended_thinking: true,
|
||||||
|
provider_count: 1,
|
||||||
|
alias_count: 0,
|
||||||
|
created_at: '2024-01-01T00:00:00Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gm-006',
|
||||||
|
name: 'gpt-5.1',
|
||||||
|
display_name: 'gpt-5.1',
|
||||||
|
description: 'OpenAI GPT-5.1 模型',
|
||||||
|
is_active: true,
|
||||||
|
default_tiered_pricing: {
|
||||||
|
tiers: [{ up_to: null, input_price_per_1m: 1.25, output_price_per_1m: 10.00 }]
|
||||||
|
},
|
||||||
|
default_supports_vision: true,
|
||||||
|
default_supports_function_calling: true,
|
||||||
|
default_supports_streaming: true,
|
||||||
|
default_supports_extended_thinking: true,
|
||||||
|
provider_count: 2,
|
||||||
|
alias_count: 1,
|
||||||
|
created_at: '2024-01-01T00:00:00Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gm-007',
|
||||||
|
name: 'gpt-5.1-codex',
|
||||||
|
display_name: 'gpt-5.1-codex',
|
||||||
|
description: 'OpenAI GPT-5.1 Codex 代码专用模型',
|
||||||
|
is_active: true,
|
||||||
|
default_tiered_pricing: {
|
||||||
|
tiers: [{ up_to: null, input_price_per_1m: 1.25, output_price_per_1m: 10.00 }]
|
||||||
|
},
|
||||||
|
default_supports_vision: true,
|
||||||
|
default_supports_function_calling: true,
|
||||||
|
default_supports_streaming: true,
|
||||||
|
default_supports_extended_thinking: true,
|
||||||
|
provider_count: 2,
|
||||||
|
alias_count: 0,
|
||||||
|
created_at: '2024-01-01T00:00:00Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gm-008',
|
||||||
|
name: 'gpt-5.1-codex-max',
|
||||||
|
display_name: 'gpt-5.1-codex-max',
|
||||||
|
description: 'OpenAI GPT-5.1 Codex Max 代码专用增强版',
|
||||||
|
is_active: true,
|
||||||
|
default_tiered_pricing: {
|
||||||
|
tiers: [{ up_to: null, input_price_per_1m: 1.25, output_price_per_1m: 10.00 }]
|
||||||
|
},
|
||||||
|
default_supports_vision: true,
|
||||||
|
default_supports_function_calling: true,
|
||||||
|
default_supports_streaming: true,
|
||||||
|
default_supports_extended_thinking: true,
|
||||||
|
provider_count: 2,
|
||||||
|
alias_count: 0,
|
||||||
|
created_at: '2024-01-01T00:00:00Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gm-009',
|
||||||
|
name: 'gpt-5.1-codex-mini',
|
||||||
|
display_name: 'gpt-5.1-codex-mini',
|
||||||
|
description: 'OpenAI GPT-5.1 Codex Mini 轻量代码模型',
|
||||||
|
is_active: true,
|
||||||
|
default_tiered_pricing: {
|
||||||
|
tiers: [{ up_to: null, input_price_per_1m: 1.25, output_price_per_1m: 10.00 }]
|
||||||
|
},
|
||||||
|
default_supports_vision: true,
|
||||||
|
default_supports_function_calling: true,
|
||||||
|
default_supports_streaming: true,
|
||||||
|
default_supports_extended_thinking: true,
|
||||||
|
provider_count: 2,
|
||||||
|
alias_count: 0,
|
||||||
|
created_at: '2024-01-01T00:00:00Z'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// ========== Usage 数据 ==========
|
||||||
|
|
||||||
|
export const MOCK_USAGE_RESPONSE: UsageResponse = {
|
||||||
|
total_requests: 1234,
|
||||||
|
total_input_tokens: 1800000,
|
||||||
|
total_output_tokens: 700000,
|
||||||
|
total_tokens: 2500000,
|
||||||
|
total_cost: 45.67,
|
||||||
|
total_actual_cost: 33.33,
|
||||||
|
avg_response_time: 1.23,
|
||||||
|
quota_usd: 100,
|
||||||
|
used_usd: 45.32,
|
||||||
|
summary_by_model: [
|
||||||
|
{ model: 'claude-sonnet-4-20250514', requests: 456, input_tokens: 650000, output_tokens: 250000, total_tokens: 900000, total_cost_usd: 18.50, actual_total_cost_usd: 13.50 },
|
||||||
|
{ model: 'gpt-4o', requests: 312, input_tokens: 480000, output_tokens: 180000, total_tokens: 660000, total_cost_usd: 12.30, actual_total_cost_usd: 9.20 },
|
||||||
|
{ model: 'claude-haiku-3-5-20241022', requests: 289, input_tokens: 420000, output_tokens: 170000, total_tokens: 590000, total_cost_usd: 8.50, actual_total_cost_usd: 6.30 },
|
||||||
|
{ model: 'gemini-2.0-flash', requests: 177, input_tokens: 250000, output_tokens: 100000, total_tokens: 350000, total_cost_usd: 6.37, actual_total_cost_usd: 4.33 }
|
||||||
|
],
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
id: 'usage-001',
|
||||||
|
provider: 'anthropic',
|
||||||
|
model: 'claude-sonnet-4-20250514',
|
||||||
|
input_tokens: 1500,
|
||||||
|
output_tokens: 800,
|
||||||
|
total_tokens: 2300,
|
||||||
|
cost: 0.0165,
|
||||||
|
response_time_ms: 1234,
|
||||||
|
is_stream: true,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
status_code: 200,
|
||||||
|
input_price_per_1m: 3,
|
||||||
|
output_price_per_1m: 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'usage-002',
|
||||||
|
provider: 'openai',
|
||||||
|
model: 'gpt-4o',
|
||||||
|
input_tokens: 2000,
|
||||||
|
output_tokens: 500,
|
||||||
|
total_tokens: 2500,
|
||||||
|
cost: 0.01,
|
||||||
|
response_time_ms: 890,
|
||||||
|
is_stream: false,
|
||||||
|
created_at: new Date(Date.now() - 300000).toISOString(),
|
||||||
|
status_code: 200,
|
||||||
|
input_price_per_1m: 2.5,
|
||||||
|
output_price_per_1m: 10
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 系统配置 ==========
|
||||||
|
|
||||||
|
export const MOCK_SYSTEM_CONFIGS = [
|
||||||
|
{ key: 'rate_limit_enabled', value: true, description: '是否启用速率限制' },
|
||||||
|
{ key: 'default_rate_limit', value: 60, description: '默认速率限制(请求/分钟)' },
|
||||||
|
{ key: 'cache_enabled', value: true, description: '是否启用缓存' },
|
||||||
|
{ key: 'default_cache_ttl', value: 3600, description: '默认缓存 TTL(秒)' },
|
||||||
|
{ key: 'fallback_enabled', value: true, description: '是否启用故障转移' },
|
||||||
|
{ key: 'max_fallback_attempts', value: 3, description: '最大故障转移次数' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// ========== API 格式 ==========
|
||||||
|
|
||||||
|
export const MOCK_API_FORMATS = {
|
||||||
|
formats: [
|
||||||
|
{ value: 'claude', label: 'Claude API', default_path: '/v1/messages', aliases: [] },
|
||||||
|
{ value: 'claude_cli', label: 'Claude CLI', default_path: '/v1/messages', aliases: [] },
|
||||||
|
{ value: 'openai', label: 'OpenAI API', default_path: '/v1/chat/completions', aliases: [] },
|
||||||
|
{ value: 'openai_cli', label: 'OpenAI Responses API', default_path: '/v1/responses', aliases: [] },
|
||||||
|
{ value: 'gemini', label: 'Gemini API', default_path: '/v1beta/models', aliases: [] },
|
||||||
|
{ value: 'gemini_cli', label: 'Gemini CLI', default_path: '/v1beta/models', aliases: [] }
|
||||||
|
]
|
||||||
|
}
|
||||||
2157
frontend/src/mocks/handler.ts
Normal file
2157
frontend/src/mocks/handler.ts
Normal file
File diff suppressed because it is too large
Load Diff
6
frontend/src/mocks/index.ts
Normal file
6
frontend/src/mocks/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Mock Module Index
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { handleMockRequest, setMockUserToken, getMockUserToken } from './handler'
|
||||||
|
export * from './data'
|
||||||
Reference in New Issue
Block a user