mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-03 00:02:28 +08:00
将模型配置从多个固定字段(description, official_url, icon_url, default_supports_* 等) 统一为灵活的 config JSON 字段,提高扩展性。同时优化前端模型创建表单,支持从 models-dev 列表直接选择模型快速填充。 主要变更: - 后端:模型表迁移,支持 config JSON 存储模型能力和元信息 - 前端:GlobalModelFormDialog 支持两种创建方式(列表选择/手动填写) - API 类型更新,对齐新的数据结构
2463 lines
88 KiB
TypeScript
2463 lines
88 KiB
TypeScript
/**
|
||
* Mock API Handler
|
||
* 演示模式的 API 请求拦截和模拟响应
|
||
*/
|
||
|
||
import type { AxiosRequestConfig, AxiosResponse } from 'axios'
|
||
import { isDemoMode, DEMO_ACCOUNTS } from '@/config/demo'
|
||
import {
|
||
MOCK_ADMIN_USER,
|
||
MOCK_NORMAL_USER,
|
||
MOCK_LOGIN_RESPONSE_ADMIN,
|
||
MOCK_LOGIN_RESPONSE_USER,
|
||
MOCK_ADMIN_PROFILE,
|
||
MOCK_USER_PROFILE,
|
||
MOCK_DASHBOARD_STATS,
|
||
MOCK_RECENT_REQUESTS,
|
||
MOCK_PROVIDER_STATUS,
|
||
MOCK_DAILY_STATS,
|
||
MOCK_ALL_USERS,
|
||
MOCK_USER_API_KEYS,
|
||
MOCK_ADMIN_API_KEYS,
|
||
MOCK_PROVIDERS,
|
||
MOCK_GLOBAL_MODELS,
|
||
MOCK_SYSTEM_CONFIGS,
|
||
MOCK_API_FORMATS
|
||
} from './data'
|
||
|
||
// 当前登录用户的 token(用于判断角色)
|
||
let currentUserToken: string | null = null
|
||
|
||
// 模拟网络延迟
|
||
function delay(ms: number = 150): Promise<void> {
|
||
return new Promise(resolve => setTimeout(resolve, ms + Math.random() * 200))
|
||
}
|
||
|
||
// 创建模拟响应
|
||
function createMockResponse<T>(data: T, status: number = 200): AxiosResponse<T> {
|
||
return {
|
||
data,
|
||
status,
|
||
statusText: status === 200 ? 'OK' : 'Error',
|
||
headers: {},
|
||
config: {} as any
|
||
}
|
||
}
|
||
|
||
// 判断当前是否为管理员
|
||
function isCurrentUserAdmin(): boolean {
|
||
return currentUserToken === 'demo-access-token-admin'
|
||
}
|
||
|
||
// 获取当前用户
|
||
function getCurrentUser() {
|
||
return isCurrentUserAdmin() ? MOCK_ADMIN_USER : MOCK_NORMAL_USER
|
||
}
|
||
|
||
// 获取当前用户 Profile
|
||
function getCurrentProfile() {
|
||
return isCurrentUserAdmin() ? MOCK_ADMIN_PROFILE : MOCK_USER_PROFILE
|
||
}
|
||
|
||
// 检查管理员权限
|
||
function requireAdmin() {
|
||
if (!isCurrentUserAdmin()) {
|
||
throw { response: createMockResponse({ detail: '需要管理员权限' }, 403) }
|
||
}
|
||
}
|
||
|
||
// Mock 公告数据
|
||
const MOCK_ANNOUNCEMENTS = [
|
||
{
|
||
id: 'ann-001',
|
||
title: '系统升级通知',
|
||
content: '系统将于本周六凌晨 2:00-4:00 进行维护升级,届时服务将暂停访问。',
|
||
type: 'maintenance',
|
||
priority: 100,
|
||
is_pinned: true,
|
||
is_active: true,
|
||
author: { id: 'demo-admin-uuid-0001', username: 'Demo Admin' },
|
||
created_at: '2024-12-01T00:00:00Z',
|
||
updated_at: '2024-12-01T00:00:00Z',
|
||
is_read: false
|
||
},
|
||
{
|
||
id: 'ann-002',
|
||
title: '新模型上线:Claude Sonnet 4',
|
||
content: 'Anthropic 最新模型 Claude Sonnet 4 已上线,支持更长上下文和更强推理能力。',
|
||
type: 'info',
|
||
priority: 50,
|
||
is_pinned: false,
|
||
is_active: true,
|
||
author: { id: 'demo-admin-uuid-0001', username: 'Demo Admin' },
|
||
created_at: '2024-11-28T00:00:00Z',
|
||
updated_at: '2024-11-28T00:00:00Z',
|
||
is_read: true
|
||
}
|
||
]
|
||
|
||
// 生成模拟健康事件
|
||
// status: success(绿), failed(红), skipped(黄)
|
||
// 无事件的时间段会显示为灰色
|
||
function generateHealthEvents(
|
||
count: number,
|
||
successRate: number,
|
||
failRate: number,
|
||
_skipRate: number,
|
||
baseLatency: number,
|
||
latencyVariance: number
|
||
) {
|
||
const events = []
|
||
const now = Date.now()
|
||
// 6小时内随机分布事件,留一些空白时段(灰色)
|
||
const timeSpan = 6 * 60 * 60 * 1000
|
||
// skipRate 由 1 - successRate - failRate 隐含计算
|
||
for (let i = 0; i < count; i++) {
|
||
const rand = Math.random()
|
||
let status: string
|
||
let statusCode: number
|
||
if (rand < successRate) {
|
||
status = 'success'
|
||
statusCode = 200
|
||
} else if (rand < successRate + failRate) {
|
||
status = 'failed'
|
||
statusCode = [500, 502, 503, 429, 400][Math.floor(Math.random() * 5)]
|
||
} else {
|
||
status = 'skipped'
|
||
statusCode = 0
|
||
}
|
||
events.push({
|
||
timestamp: new Date(now - Math.random() * timeSpan).toISOString(),
|
||
status,
|
||
status_code: statusCode,
|
||
latency_ms: Math.round(baseLatency + Math.random() * latencyVariance),
|
||
error_type: status === 'failed' ? ['RateLimitError', 'TimeoutError', 'ServerError'][Math.floor(Math.random() * 3)] : undefined
|
||
})
|
||
}
|
||
// 按时间排序
|
||
return events.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())
|
||
}
|
||
|
||
// Mock 端点健康数据
|
||
// 注意:success_rate 使用 0-1 之间的小数,前端会乘以 100 显示为百分比
|
||
// 事件的成功/失败/跳过比例必须与 success_rate 保持一致
|
||
// 覆盖所有 API 格式:claude, claude_cli, openai, openai_cli, gemini, gemini_cli
|
||
const MOCK_ENDPOINT_STATUS = {
|
||
generated_at: new Date().toISOString(),
|
||
formats: [
|
||
{
|
||
api_format: 'CLAUDE',
|
||
api_path: '/v1/messages',
|
||
total_attempts: 2580,
|
||
success_count: 2540,
|
||
failed_count: 30,
|
||
skipped_count: 10,
|
||
success_rate: 0.984,
|
||
provider_count: 2,
|
||
key_count: 4,
|
||
last_event_at: new Date().toISOString(),
|
||
// 98.4% 成功率:successRate=0.984, failRate=0.012, skipRate=0.004
|
||
events: generateHealthEvents(80, 0.984, 0.012, 0.004, 900, 500)
|
||
},
|
||
{
|
||
api_format: 'CLAUDE_CLI',
|
||
api_path: '/v1/messages',
|
||
total_attempts: 1890,
|
||
success_count: 1780,
|
||
failed_count: 85,
|
||
skipped_count: 25,
|
||
success_rate: 0.942,
|
||
provider_count: 5,
|
||
key_count: 9,
|
||
last_event_at: new Date().toISOString(),
|
||
// 94.2% 成功率:successRate=0.942, failRate=0.045, skipRate=0.013
|
||
events: generateHealthEvents(120, 0.942, 0.045, 0.013, 1200, 800)
|
||
},
|
||
{
|
||
api_format: 'GEMINI',
|
||
api_path: '/v1beta/models',
|
||
total_attempts: 890,
|
||
success_count: 890,
|
||
failed_count: 0,
|
||
skipped_count: 0,
|
||
success_rate: 1.0,
|
||
provider_count: 3,
|
||
key_count: 3,
|
||
last_event_at: new Date().toISOString(),
|
||
// 100% 成功率:全部成功
|
||
events: generateHealthEvents(45, 1.0, 0, 0, 400, 200)
|
||
},
|
||
{
|
||
api_format: 'GEMINI_CLI',
|
||
api_path: '/v1beta/models',
|
||
total_attempts: 456,
|
||
success_count: 450,
|
||
failed_count: 4,
|
||
skipped_count: 2,
|
||
success_rate: 0.987,
|
||
provider_count: 3,
|
||
key_count: 3,
|
||
last_event_at: new Date().toISOString(),
|
||
// 98.7% 成功率:successRate=0.987, failRate=0.009, skipRate=0.004
|
||
events: generateHealthEvents(25, 0.987, 0.009, 0.004, 500, 300)
|
||
},
|
||
{
|
||
api_format: 'OPENAI',
|
||
api_path: '/v1/chat/completions',
|
||
total_attempts: 1560,
|
||
success_count: 1520,
|
||
failed_count: 35,
|
||
skipped_count: 5,
|
||
success_rate: 0.974,
|
||
provider_count: 1,
|
||
key_count: 2,
|
||
last_event_at: new Date().toISOString(),
|
||
// 97.4% 成功率:successRate=0.974, failRate=0.022, skipRate=0.004
|
||
events: generateHealthEvents(60, 0.974, 0.022, 0.004, 700, 400)
|
||
},
|
||
{
|
||
api_format: 'OPENAI_CLI',
|
||
api_path: '/responses',
|
||
total_attempts: 2340,
|
||
success_count: 2200,
|
||
failed_count: 100,
|
||
skipped_count: 40,
|
||
success_rate: 0.940,
|
||
provider_count: 4,
|
||
key_count: 5,
|
||
last_event_at: new Date().toISOString(),
|
||
// 94.0% 成功率:successRate=0.940, failRate=0.043, skipRate=0.017
|
||
events: generateHealthEvents(100, 0.940, 0.043, 0.017, 800, 600)
|
||
}
|
||
]
|
||
}
|
||
|
||
// 生成活跃热力图数据(最近365天)
|
||
function generateActivityHeatmap() {
|
||
const days: Array<{
|
||
date: string
|
||
requests: number
|
||
total_tokens: number
|
||
total_cost: number
|
||
actual_total_cost?: number
|
||
}> = []
|
||
|
||
const now = new Date()
|
||
const startDate = new Date(now)
|
||
startDate.setDate(startDate.getDate() - 364) // 365天数据(一年)
|
||
|
||
let maxRequests = 0
|
||
|
||
// 生成每天的数据
|
||
for (let i = 0; i < 365; i++) {
|
||
const date = new Date(startDate)
|
||
date.setDate(startDate.getDate() + i)
|
||
const dateStr = date.toISOString().split('T')[0]
|
||
|
||
// 工作日请求量更高
|
||
const dayOfWeek = date.getDay()
|
||
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6
|
||
|
||
// 基础请求量 + 随机波动 + 周末减少
|
||
// 加入一些趋势:越近的日期请求量可能越高
|
||
const trendFactor = 0.7 + (i / 365) * 0.5 // 从0.7到1.2的增长趋势
|
||
const baseRequests = isWeekend ? 40 : 120
|
||
const variance = Math.floor(Math.random() * 80)
|
||
// 有些天可能没有请求(约5%的天数)
|
||
const noActivity = Math.random() < 0.05
|
||
const requests = noActivity ? 0 : Math.round((baseRequests + variance) * trendFactor)
|
||
|
||
if (requests > maxRequests) maxRequests = requests
|
||
|
||
// 根据请求量计算 tokens 和 cost
|
||
const avgTokensPerRequest = 3000 + Math.floor(Math.random() * 2000)
|
||
const totalTokens = requests * avgTokensPerRequest
|
||
const avgCostPerRequest = 0.02 + Math.random() * 0.03
|
||
const totalCost = Number((requests * avgCostPerRequest).toFixed(2))
|
||
const actualTotalCost = Number((totalCost * 0.8).toFixed(2)) // 实际成本约为 80%
|
||
|
||
days.push({
|
||
date: dateStr,
|
||
requests,
|
||
total_tokens: totalTokens,
|
||
total_cost: totalCost,
|
||
actual_total_cost: actualTotalCost
|
||
})
|
||
}
|
||
|
||
return {
|
||
start_date: days[0].date,
|
||
end_date: days[days.length - 1].date,
|
||
total_days: days.length,
|
||
max_requests: maxRequests,
|
||
days
|
||
}
|
||
}
|
||
|
||
// 缓存热力图数据(避免每次请求都重新生成)
|
||
let cachedHeatmap: ReturnType<typeof generateActivityHeatmap> | null = null
|
||
function getActivityHeatmap() {
|
||
if (!cachedHeatmap) {
|
||
cachedHeatmap = generateActivityHeatmap()
|
||
}
|
||
return cachedHeatmap
|
||
}
|
||
|
||
// 生成更真实的使用记录
|
||
function generateMockUsageRecords(count: number = 100) {
|
||
const records = []
|
||
const now = Date.now()
|
||
|
||
const models = [
|
||
{ name: 'claude-sonnet-4-5-20250929', provider: 'anthropic', inputPrice: 3, outputPrice: 15 },
|
||
{ name: 'claude-haiku-4-5-20251001', provider: 'anthropic', inputPrice: 1, outputPrice: 5 },
|
||
{ name: 'claude-opus-4-5-20251101', provider: 'anthropic', inputPrice: 15, outputPrice: 75 },
|
||
{ name: 'gpt-5.1', provider: 'openai', inputPrice: 2.5, outputPrice: 10 },
|
||
{ name: 'gpt-5.1-codex', provider: 'openai', inputPrice: 2.5, outputPrice: 10 },
|
||
{ name: 'gemini-3-pro-preview', provider: 'google', inputPrice: 2, outputPrice: 12 }
|
||
]
|
||
|
||
const users = [
|
||
{ id: 'demo-admin-uuid-0001', username: 'Demo Admin', email: 'admin@demo.aether.ai' },
|
||
{ id: 'demo-user-uuid-0002', username: 'Demo User', email: 'user@demo.aether.ai' },
|
||
{ id: 'demo-user-uuid-0003', username: 'Alice Chen', email: 'alice@demo.aether.ai' },
|
||
{ id: 'demo-user-uuid-0004', username: 'Bob Zhang', email: 'bob@demo.aether.ai' }
|
||
]
|
||
|
||
const apiFormats = ['CLAUDE', 'CLAUDE_CLI', 'OPENAI', 'OPENAI_CLI', 'GEMINI', 'GEMINI_CLI']
|
||
const statusOptions: Array<'completed' | 'failed' | 'streaming'> = ['completed', 'completed', 'completed', 'completed', 'failed', 'streaming']
|
||
|
||
for (let i = 0; i < count; i++) {
|
||
const model = models[Math.floor(Math.random() * models.length)]
|
||
const user = users[Math.floor(Math.random() * users.length)]
|
||
const status = statusOptions[Math.floor(Math.random() * statusOptions.length)]
|
||
|
||
// 根据模型类型选择 API 格式
|
||
let apiFormat = apiFormats[0]
|
||
if (model.provider === 'anthropic') {
|
||
apiFormat = Math.random() > 0.3 ? 'CLAUDE_CLI' : 'CLAUDE'
|
||
} else if (model.provider === 'openai') {
|
||
apiFormat = Math.random() > 0.3 ? 'OPENAI_CLI' : 'OPENAI'
|
||
} else {
|
||
apiFormat = Math.random() > 0.3 ? 'GEMINI_CLI' : 'GEMINI'
|
||
}
|
||
|
||
const inputTokens = 500 + Math.floor(Math.random() * 10000)
|
||
const outputTokens = 200 + Math.floor(Math.random() * 4000)
|
||
const cacheCreation = Math.random() > 0.7 ? Math.floor(Math.random() * 2000) : 0
|
||
const cacheRead = Math.random() > 0.5 ? Math.floor(Math.random() * 5000) : 0
|
||
const totalTokens = inputTokens + outputTokens
|
||
|
||
// 计算成本(每百万 token)
|
||
const inputCost = (inputTokens / 1000000) * model.inputPrice
|
||
const outputCost = (outputTokens / 1000000) * model.outputPrice
|
||
const cost = Number((inputCost + outputCost).toFixed(6))
|
||
const actualCost = Number((cost * (0.7 + Math.random() * 0.3)).toFixed(6))
|
||
|
||
// 时间分布:最近的记录更密集
|
||
const timeOffset = Math.pow(i / count, 1.5) * 7 * 24 * 60 * 60 * 1000 // 7天内
|
||
const createdAt = new Date(now - timeOffset)
|
||
|
||
// 响应时间:根据模型和 token 数量
|
||
const baseResponseTime = model.name.includes('opus') ? 2000 : model.name.includes('haiku') ? 500 : 1000
|
||
const responseTime = status === 'failed' ? null : baseResponseTime + Math.floor(Math.random() * outputTokens * 0.5)
|
||
|
||
records.push({
|
||
id: `usage-${String(i + 1).padStart(4, '0')}`,
|
||
user_id: user.id,
|
||
username: user.username,
|
||
user_email: user.email,
|
||
provider: model.provider,
|
||
api_key_name: `${model.provider}-key-${Math.ceil(Math.random() * 3)}`,
|
||
rate_multiplier: 1.0,
|
||
model: model.name,
|
||
target_model: model.name,
|
||
api_format: apiFormat,
|
||
input_tokens: inputTokens,
|
||
output_tokens: outputTokens,
|
||
cache_creation_input_tokens: cacheCreation,
|
||
cache_read_input_tokens: cacheRead,
|
||
total_tokens: totalTokens,
|
||
cost,
|
||
actual_cost: actualCost,
|
||
response_time_ms: responseTime,
|
||
is_stream: apiFormat.includes('CLI'),
|
||
status_code: status === 'failed' ? [500, 502, 429, 400][Math.floor(Math.random() * 4)] : 200,
|
||
error_message: status === 'failed' ? ['Rate limit exceeded', 'Internal server error', 'Model overloaded'][Math.floor(Math.random() * 3)] : undefined,
|
||
status,
|
||
created_at: createdAt.toISOString(),
|
||
has_fallback: Math.random() > 0.9,
|
||
request_metadata: model.provider === 'google' ? { model_version: 'gemini-3-pro-preview-2025-01' } : undefined
|
||
})
|
||
}
|
||
|
||
return records
|
||
}
|
||
|
||
// 缓存使用记录
|
||
let cachedUsageRecords: ReturnType<typeof generateMockUsageRecords> | null = null
|
||
function getUsageRecords() {
|
||
if (!cachedUsageRecords) {
|
||
cachedUsageRecords = generateMockUsageRecords(100)
|
||
}
|
||
return cachedUsageRecords
|
||
}
|
||
|
||
// Mock 别名数据
|
||
const MOCK_ALIASES = [
|
||
{ id: 'alias-001', source_model: 'claude-4-sonnet', target_global_model_id: 'gm-001', target_global_model_name: 'claude-sonnet-4-20250514', target_global_model_display_name: 'Claude Sonnet 4', provider_id: null, provider_name: null, scope: 'global', mapping_type: 'alias', is_active: true, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z' },
|
||
{ id: 'alias-002', source_model: 'claude-4-opus', target_global_model_id: 'gm-002', target_global_model_name: 'claude-opus-4-20250514', target_global_model_display_name: 'Claude Opus 4', provider_id: null, provider_name: null, scope: 'global', mapping_type: 'alias', is_active: true, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z' },
|
||
{ id: 'alias-003', source_model: 'gpt4o', target_global_model_id: 'gm-004', target_global_model_name: 'gpt-4o', target_global_model_display_name: 'GPT-4o', provider_id: null, provider_name: null, scope: 'global', mapping_type: 'alias', is_active: true, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z' },
|
||
{ id: 'alias-004', source_model: 'gemini-flash', target_global_model_id: 'gm-005', target_global_model_name: 'gemini-2.0-flash', target_global_model_display_name: 'Gemini 2.0 Flash', provider_id: null, provider_name: null, scope: 'global', mapping_type: 'alias', is_active: true, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z' }
|
||
]
|
||
|
||
// Mock Endpoint Keys
|
||
const MOCK_ENDPOINT_KEYS = [
|
||
{ id: 'ekey-001', endpoint_id: 'ep-001', api_key_masked: 'sk-ant...abc1', name: 'Primary Key', rate_multiplier: 1.0, internal_priority: 1, health_score: 98, consecutive_failures: 0, request_count: 5000, success_count: 4950, error_count: 50, success_rate: 99, avg_response_time_ms: 1200, is_active: true, created_at: '2024-01-01T00:00:00Z', updated_at: new Date().toISOString() },
|
||
{ id: 'ekey-002', endpoint_id: 'ep-001', api_key_masked: 'sk-ant...def2', name: 'Backup Key', rate_multiplier: 1.0, internal_priority: 2, health_score: 95, consecutive_failures: 1, request_count: 2000, success_count: 1950, error_count: 50, success_rate: 97.5, avg_response_time_ms: 1350, is_active: true, created_at: '2024-02-01T00:00:00Z', updated_at: new Date().toISOString() },
|
||
{ id: 'ekey-003', endpoint_id: 'ep-002', api_key_masked: 'sk-oai...ghi3', name: 'OpenAI Main', rate_multiplier: 1.0, internal_priority: 1, health_score: 97, consecutive_failures: 0, request_count: 3500, success_count: 3450, error_count: 50, success_rate: 98.6, avg_response_time_ms: 900, is_active: true, created_at: '2024-01-15T00:00:00Z', updated_at: new Date().toISOString() }
|
||
]
|
||
|
||
// Mock Endpoints
|
||
const MOCK_ENDPOINTS = [
|
||
{ id: 'ep-001', provider_id: 'provider-001', provider_name: 'anthropic', api_format: 'claude', base_url: 'https://api.anthropic.com', auth_type: 'bearer', timeout: 120, max_retries: 3, priority: 100, weight: 100, health_score: 98, consecutive_failures: 0, is_active: true, total_keys: 2, active_keys: 2, created_at: '2024-01-01T00:00:00Z', updated_at: new Date().toISOString() },
|
||
{ id: 'ep-002', provider_id: 'provider-002', provider_name: 'openai', api_format: 'openai', base_url: 'https://api.openai.com', auth_type: 'bearer', timeout: 60, max_retries: 3, priority: 90, weight: 100, health_score: 97, consecutive_failures: 0, is_active: true, total_keys: 1, active_keys: 1, created_at: '2024-01-01T00:00:00Z', updated_at: new Date().toISOString() },
|
||
{ id: 'ep-003', provider_id: 'provider-003', provider_name: 'google', api_format: 'gemini', base_url: 'https://generativelanguage.googleapis.com', auth_type: 'api_key', timeout: 60, max_retries: 3, priority: 80, weight: 100, health_score: 96, consecutive_failures: 0, is_active: true, total_keys: 1, active_keys: 1, created_at: '2024-01-15T00:00:00Z', updated_at: new Date().toISOString() }
|
||
]
|
||
|
||
// Mock 能力定义
|
||
const MOCK_CAPABILITIES = [
|
||
{ name: 'cache_1h', display_name: '1小时缓存', description: '支持1小时prompt缓存', match_mode: 'exclusive', short_name: '1h' },
|
||
{ name: 'context_1m', display_name: '1M上下文', description: '支持1M上下文窗口', match_mode: 'compatible', short_name: '1M' }
|
||
]
|
||
|
||
/**
|
||
* Mock API 路由处理器
|
||
*/
|
||
const mockHandlers: Record<string, (config: AxiosRequestConfig) => Promise<AxiosResponse<any>>> = {
|
||
// ========== 认证相关 ==========
|
||
'POST /api/auth/login': async (config) => {
|
||
await delay()
|
||
const body = JSON.parse(config.data || '{}')
|
||
const { email, password } = body
|
||
|
||
if (email === DEMO_ACCOUNTS.admin.email && password === DEMO_ACCOUNTS.admin.password) {
|
||
currentUserToken = 'demo-access-token-admin'
|
||
return createMockResponse(MOCK_LOGIN_RESPONSE_ADMIN)
|
||
}
|
||
|
||
if (email === DEMO_ACCOUNTS.user.email && password === DEMO_ACCOUNTS.user.password) {
|
||
currentUserToken = 'demo-access-token-user'
|
||
return createMockResponse(MOCK_LOGIN_RESPONSE_USER)
|
||
}
|
||
|
||
throw { response: createMockResponse({ detail: '邮箱或密码错误' }, 401) }
|
||
},
|
||
|
||
'POST /api/auth/logout': async () => {
|
||
await delay(100)
|
||
currentUserToken = null
|
||
return createMockResponse({ message: '已登出' })
|
||
},
|
||
|
||
'POST /api/auth/refresh': async () => {
|
||
await delay(100)
|
||
if (isCurrentUserAdmin()) {
|
||
return createMockResponse(MOCK_LOGIN_RESPONSE_ADMIN)
|
||
}
|
||
return createMockResponse(MOCK_LOGIN_RESPONSE_USER)
|
||
},
|
||
|
||
// ========== 用户信息 ==========
|
||
'GET /api/users/me': async () => {
|
||
await delay()
|
||
return createMockResponse(getCurrentUser())
|
||
},
|
||
|
||
'PUT /api/users/me': async () => {
|
||
await delay()
|
||
return createMockResponse({ message: '更新成功(演示模式)' })
|
||
},
|
||
|
||
'PATCH /api/users/me/password': async () => {
|
||
await delay()
|
||
return createMockResponse({ message: '密码修改成功(演示模式)' })
|
||
},
|
||
|
||
'GET /api/users/me/api-keys': async () => {
|
||
await delay()
|
||
return createMockResponse(MOCK_USER_API_KEYS)
|
||
},
|
||
|
||
'POST /api/users/me/api-keys': async (config) => {
|
||
await delay()
|
||
const body = JSON.parse(config.data || '{}')
|
||
const newKey = {
|
||
id: `key-demo-${Date.now()}`,
|
||
key: `sk-aether-demo-${Math.random().toString(36).substring(2, 15)}`,
|
||
key_display: 'sk-ae...demo',
|
||
name: body.name || '新密钥(演示)',
|
||
created_at: new Date().toISOString(),
|
||
is_active: true,
|
||
is_standalone: false,
|
||
total_requests: 0,
|
||
total_cost_usd: 0
|
||
}
|
||
return createMockResponse(newKey)
|
||
},
|
||
|
||
'GET /api/users/me/usage': async () => {
|
||
await delay()
|
||
const heatmap = getActivityHeatmap()
|
||
const records = getUsageRecords()
|
||
// 只返回当前用户的数据
|
||
const userRecords = records.filter(r => r.user_id === getCurrentUser().id)
|
||
const totalRequests = userRecords.length
|
||
const totalTokens = userRecords.reduce((sum, r) => sum + r.total_tokens, 0)
|
||
const totalInputTokens = userRecords.reduce((sum, r) => sum + r.input_tokens, 0)
|
||
const totalOutputTokens = userRecords.reduce((sum, r) => sum + r.output_tokens, 0)
|
||
const totalCost = userRecords.reduce((sum, r) => sum + r.cost, 0)
|
||
const totalActualCost = userRecords.reduce((sum, r) => sum + (r.actual_cost || 0), 0)
|
||
const avgResponseTime = userRecords.filter(r => r.response_time_ms).reduce((sum, r) => sum + (r.response_time_ms || 0), 0) / userRecords.filter(r => r.response_time_ms).length / 1000
|
||
|
||
// 按模型聚合
|
||
const modelStats = new Map<string, { requests: number; input_tokens: number; output_tokens: number; total_tokens: number; total_cost_usd: number; actual_total_cost_usd: number }>()
|
||
for (const r of userRecords) {
|
||
const existing = modelStats.get(r.model) || { requests: 0, input_tokens: 0, output_tokens: 0, total_tokens: 0, total_cost_usd: 0, actual_total_cost_usd: 0 }
|
||
existing.requests++
|
||
existing.input_tokens += r.input_tokens
|
||
existing.output_tokens += r.output_tokens
|
||
existing.total_tokens += r.total_tokens
|
||
existing.total_cost_usd += r.cost
|
||
existing.actual_total_cost_usd += r.actual_cost || 0
|
||
modelStats.set(r.model, existing)
|
||
}
|
||
|
||
return createMockResponse({
|
||
total_requests: totalRequests * 20,
|
||
total_input_tokens: totalInputTokens * 20,
|
||
total_output_tokens: totalOutputTokens * 20,
|
||
total_tokens: totalTokens * 20,
|
||
total_cost: Number((totalCost * 20).toFixed(2)),
|
||
total_actual_cost: Number((totalActualCost * 20).toFixed(2)),
|
||
avg_response_time: Number(avgResponseTime.toFixed(2)) || 1.23,
|
||
quota_usd: 100,
|
||
used_usd: Number((totalCost * 20).toFixed(2)),
|
||
activity_heatmap: heatmap,
|
||
summary_by_model: Array.from(modelStats.entries()).map(([model, stats]) => ({
|
||
model,
|
||
requests: stats.requests * 20,
|
||
input_tokens: stats.input_tokens * 20,
|
||
output_tokens: stats.output_tokens * 20,
|
||
total_tokens: stats.total_tokens * 20,
|
||
total_cost_usd: Number((stats.total_cost_usd * 20).toFixed(2)),
|
||
actual_total_cost_usd: Number((stats.actual_total_cost_usd * 20).toFixed(2))
|
||
})),
|
||
records: userRecords.slice(0, 10).map(r => ({
|
||
id: r.id,
|
||
provider: r.provider,
|
||
model: r.model,
|
||
input_tokens: r.input_tokens,
|
||
output_tokens: r.output_tokens,
|
||
total_tokens: r.total_tokens,
|
||
cost: r.cost,
|
||
response_time_ms: r.response_time_ms,
|
||
is_stream: r.is_stream,
|
||
created_at: r.created_at,
|
||
status_code: r.status_code,
|
||
input_price_per_1m: 3,
|
||
output_price_per_1m: 15
|
||
}))
|
||
})
|
||
},
|
||
|
||
'GET /api/users/me/providers': async () => {
|
||
await delay()
|
||
return createMockResponse(MOCK_PROVIDERS.map(p => ({
|
||
id: p.id,
|
||
name: p.name,
|
||
display_name: p.display_name,
|
||
is_active: p.is_active
|
||
})))
|
||
},
|
||
|
||
'GET /api/users/me/endpoint-status': async () => {
|
||
await delay()
|
||
return createMockResponse(MOCK_ENDPOINTS.map(e => ({
|
||
api_format: e.api_format,
|
||
health_score: e.health_score,
|
||
is_active: e.is_active
|
||
})))
|
||
},
|
||
|
||
'GET /api/users/me/preferences': async () => {
|
||
await delay()
|
||
return createMockResponse(getCurrentProfile().preferences || { theme: 'auto', language: 'zh-CN' })
|
||
},
|
||
|
||
'PUT /api/users/me/preferences': async () => {
|
||
await delay()
|
||
return createMockResponse({ message: '偏好设置已更新(演示模式)' })
|
||
},
|
||
|
||
'GET /api/users/me/model-capabilities': async () => {
|
||
await delay()
|
||
return createMockResponse({ model_capability_settings: {} })
|
||
},
|
||
|
||
'PUT /api/users/me/model-capabilities': async () => {
|
||
await delay()
|
||
return createMockResponse({ message: '已更新', model_capability_settings: {} })
|
||
},
|
||
|
||
// ========== Dashboard ==========
|
||
'GET /api/dashboard/stats': async () => {
|
||
await delay()
|
||
return createMockResponse(MOCK_DASHBOARD_STATS)
|
||
},
|
||
|
||
'GET /api/dashboard/recent-requests': async () => {
|
||
await delay()
|
||
return createMockResponse({ requests: MOCK_RECENT_REQUESTS })
|
||
},
|
||
|
||
'GET /api/dashboard/provider-status': async () => {
|
||
await delay()
|
||
return createMockResponse({ providers: MOCK_PROVIDER_STATUS })
|
||
},
|
||
|
||
'GET /api/dashboard/daily-stats': async () => {
|
||
await delay()
|
||
return createMockResponse(MOCK_DAILY_STATS)
|
||
},
|
||
|
||
// ========== 公告 ==========
|
||
'GET /api/announcements': async () => {
|
||
await delay()
|
||
return createMockResponse({ items: MOCK_ANNOUNCEMENTS, total: MOCK_ANNOUNCEMENTS.length, unread_count: 1 })
|
||
},
|
||
|
||
'GET /api/announcements/active': async () => {
|
||
await delay()
|
||
return createMockResponse({ items: MOCK_ANNOUNCEMENTS.filter(a => a.is_active), total: MOCK_ANNOUNCEMENTS.filter(a => a.is_active).length, unread_count: 1 })
|
||
},
|
||
|
||
'GET /api/announcements/users/me/unread-count': async () => {
|
||
await delay()
|
||
return createMockResponse({ unread_count: 1 })
|
||
},
|
||
|
||
'PATCH /api/announcements': async () => {
|
||
await delay()
|
||
return createMockResponse({ message: '已标记为已读' })
|
||
},
|
||
|
||
'POST /api/announcements': async (config) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const body = JSON.parse(config.data || '{}')
|
||
return createMockResponse({ id: `ann-demo-${Date.now()}`, title: body.title, message: '公告已创建(演示模式)' })
|
||
},
|
||
|
||
'POST /api/announcements/read-all': async () => {
|
||
await delay()
|
||
return createMockResponse({ message: '已全部标记为已读' })
|
||
},
|
||
|
||
// ========== Admin: 用户管理 ==========
|
||
'GET /api/admin/users': async () => {
|
||
await delay()
|
||
requireAdmin()
|
||
return createMockResponse(MOCK_ALL_USERS)
|
||
},
|
||
|
||
'POST /api/admin/users': async (config) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const body = JSON.parse(config.data || '{}')
|
||
const newUser = {
|
||
id: `user-demo-${Date.now()}`,
|
||
username: body.username,
|
||
email: body.email,
|
||
role: body.role || 'user',
|
||
is_active: true,
|
||
quota_usd: body.quota_usd || null,
|
||
used_usd: 0,
|
||
total_usd: 0,
|
||
allowed_providers: null,
|
||
allowed_endpoints: null,
|
||
allowed_models: null,
|
||
created_at: new Date().toISOString()
|
||
}
|
||
return createMockResponse(newUser)
|
||
},
|
||
|
||
// ========== Admin: API Keys ==========
|
||
'GET /api/admin/api-keys': async () => {
|
||
await delay()
|
||
requireAdmin()
|
||
return createMockResponse(MOCK_ADMIN_API_KEYS)
|
||
},
|
||
|
||
'POST /api/admin/api-keys': async (config) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const body = JSON.parse(config.data || '{}')
|
||
const newKey = {
|
||
id: `standalone-demo-${Date.now()}`,
|
||
key: `sk-sa-demo-${Math.random().toString(36).substring(2, 15)}`,
|
||
user_id: 'demo-user-uuid-0002',
|
||
name: body.name || '新独立 Key(演示)',
|
||
key_display: 'sk-sa...demo',
|
||
is_active: true,
|
||
is_standalone: true,
|
||
balance_used_usd: 0,
|
||
current_balance_usd: body.initial_balance_usd || 100,
|
||
total_requests: 0,
|
||
created_at: new Date().toISOString()
|
||
}
|
||
return createMockResponse(newKey)
|
||
},
|
||
|
||
// ========== Admin: Providers ==========
|
||
'GET /api/admin/providers/summary': async () => {
|
||
await delay()
|
||
requireAdmin()
|
||
return createMockResponse(MOCK_PROVIDERS)
|
||
},
|
||
|
||
'GET /api/admin/providers': async () => {
|
||
await delay()
|
||
requireAdmin()
|
||
return createMockResponse(MOCK_PROVIDERS)
|
||
},
|
||
|
||
'POST /api/admin/providers': async (config) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const body = JSON.parse(config.data || '{}')
|
||
return createMockResponse({ ...body, id: `provider-demo-${Date.now()}`, created_at: new Date().toISOString() })
|
||
},
|
||
|
||
// ========== Admin: Endpoints ==========
|
||
'GET /api/admin/endpoints/providers': async () => {
|
||
await delay()
|
||
requireAdmin()
|
||
return createMockResponse(MOCK_ENDPOINTS)
|
||
},
|
||
|
||
'GET /api/admin/endpoints': async () => {
|
||
await delay()
|
||
requireAdmin()
|
||
return createMockResponse(MOCK_ENDPOINTS)
|
||
},
|
||
|
||
'GET /api/admin/endpoints/health/summary': async () => {
|
||
await delay()
|
||
requireAdmin()
|
||
return createMockResponse({
|
||
endpoints: { total: 6, active: 5, unhealthy: 1 },
|
||
keys: { total: 15, active: 12, unhealthy: 3 }
|
||
})
|
||
},
|
||
|
||
'GET /api/admin/endpoints/health/api-formats': async () => {
|
||
await delay()
|
||
requireAdmin()
|
||
return createMockResponse(MOCK_ENDPOINT_STATUS)
|
||
},
|
||
|
||
'GET /api/admin/endpoints/keys': async () => {
|
||
await delay()
|
||
requireAdmin()
|
||
return createMockResponse(MOCK_ENDPOINT_KEYS)
|
||
},
|
||
|
||
// ========== Admin: Global Models ==========
|
||
'GET /api/admin/models/global': async () => {
|
||
await delay()
|
||
requireAdmin()
|
||
return createMockResponse({ models: MOCK_GLOBAL_MODELS, total: MOCK_GLOBAL_MODELS.length })
|
||
},
|
||
|
||
'POST /api/admin/models/global': async (config) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const body = JSON.parse(config.data || '{}')
|
||
return createMockResponse({ ...body, id: `gm-demo-${Date.now()}`, created_at: new Date().toISOString() })
|
||
},
|
||
|
||
// ========== Admin: Model Mappings / Aliases ==========
|
||
'GET /api/admin/models/mappings': async () => {
|
||
await delay()
|
||
requireAdmin()
|
||
return createMockResponse(MOCK_ALIASES)
|
||
},
|
||
|
||
'POST /api/admin/models/mappings': async (config) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const body = JSON.parse(config.data || '{}')
|
||
return createMockResponse({ ...body, id: `alias-demo-${Date.now()}`, created_at: new Date().toISOString(), updated_at: new Date().toISOString() })
|
||
},
|
||
|
||
// ========== Admin: Usage ==========
|
||
'GET /api/admin/usage/stats': async () => {
|
||
await delay()
|
||
requireAdmin()
|
||
const heatmap = getActivityHeatmap()
|
||
const records = getUsageRecords()
|
||
const totalRequests = records.length
|
||
const totalTokens = records.reduce((sum, r) => sum + r.total_tokens, 0)
|
||
const totalCost = records.reduce((sum, r) => sum + r.cost, 0)
|
||
const totalActualCost = records.reduce((sum, r) => sum + (r.actual_cost || 0), 0)
|
||
const avgResponseTime = records.filter(r => r.response_time_ms).reduce((sum, r) => sum + (r.response_time_ms || 0), 0) / records.filter(r => r.response_time_ms).length / 1000
|
||
|
||
// 今日数据
|
||
const today = new Date().toISOString().split('T')[0]
|
||
const todayRecords = records.filter(r => r.created_at.startsWith(today))
|
||
|
||
return createMockResponse({
|
||
total_requests: totalRequests * 100, // 放大显示
|
||
total_tokens: totalTokens * 100,
|
||
total_cost: Number((totalCost * 100).toFixed(2)),
|
||
total_actual_cost: Number((totalActualCost * 100).toFixed(2)),
|
||
avg_response_time: Number(avgResponseTime.toFixed(2)),
|
||
today: {
|
||
requests: todayRecords.length * 10,
|
||
tokens: todayRecords.reduce((sum, r) => sum + r.total_tokens, 0) * 10,
|
||
cost: Number((todayRecords.reduce((sum, r) => sum + r.cost, 0) * 10).toFixed(2))
|
||
},
|
||
activity_heatmap: heatmap
|
||
})
|
||
},
|
||
|
||
'GET /api/admin/usage/records': async (config) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const records = getUsageRecords()
|
||
const params = config.params || {}
|
||
const limit = parseInt(params.limit) || 20
|
||
const offset = parseInt(params.offset) || 0
|
||
return createMockResponse({
|
||
records: records.slice(offset, offset + limit),
|
||
total: records.length,
|
||
limit,
|
||
offset
|
||
})
|
||
},
|
||
|
||
'GET /api/admin/usage/aggregation/stats': async (config) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const params = config.params || {}
|
||
const groupBy = params.group_by || 'model'
|
||
const records = getUsageRecords()
|
||
|
||
if (groupBy === 'model') {
|
||
// 按模型聚合
|
||
const modelStats = new Map<string, { request_count: number; total_tokens: number; total_cost: number }>()
|
||
for (const r of records) {
|
||
const existing = modelStats.get(r.model) || { request_count: 0, total_tokens: 0, total_cost: 0 }
|
||
existing.request_count++
|
||
existing.total_tokens += r.total_tokens
|
||
existing.total_cost += r.cost
|
||
modelStats.set(r.model, existing)
|
||
}
|
||
return createMockResponse(
|
||
Array.from(modelStats.entries()).map(([model, stats]) => ({
|
||
model,
|
||
request_count: stats.request_count * 50,
|
||
total_tokens: stats.total_tokens * 50,
|
||
total_cost: Number((stats.total_cost * 50).toFixed(2))
|
||
}))
|
||
)
|
||
}
|
||
|
||
if (groupBy === 'provider') {
|
||
const providerStats = new Map<string, { request_count: number; total_tokens: number; total_cost: number; actual_cost: number; response_times: number[]; errors: number }>()
|
||
for (const r of records) {
|
||
const existing = providerStats.get(r.provider) || { request_count: 0, total_tokens: 0, total_cost: 0, actual_cost: 0, response_times: [], errors: 0 }
|
||
existing.request_count++
|
||
existing.total_tokens += r.total_tokens
|
||
existing.total_cost += r.cost
|
||
existing.actual_cost += r.actual_cost || 0
|
||
if (r.response_time_ms) existing.response_times.push(r.response_time_ms)
|
||
if (r.status === 'failed') existing.errors++
|
||
providerStats.set(r.provider, existing)
|
||
}
|
||
return createMockResponse(
|
||
Array.from(providerStats.entries()).map(([provider, stats]) => ({
|
||
provider_id: `provider-${provider}`,
|
||
provider,
|
||
request_count: stats.request_count * 50,
|
||
total_tokens: stats.total_tokens * 50,
|
||
total_cost: Number((stats.total_cost * 50).toFixed(2)),
|
||
actual_cost: Number((stats.actual_cost * 50).toFixed(2)),
|
||
avg_response_time_ms: Math.round(stats.response_times.reduce((a, b) => a + b, 0) / stats.response_times.length || 0),
|
||
success_rate: (stats.request_count - stats.errors) / stats.request_count,
|
||
error_count: stats.errors * 50
|
||
}))
|
||
)
|
||
}
|
||
|
||
if (groupBy === 'user') {
|
||
const userStats = new Map<string, { user_id: string; username: string; email: string; request_count: number; total_tokens: number; total_cost: number }>()
|
||
for (const r of records) {
|
||
const existing = userStats.get(r.user_id) || { user_id: r.user_id, username: r.username, email: r.user_email || '', request_count: 0, total_tokens: 0, total_cost: 0 }
|
||
existing.request_count++
|
||
existing.total_tokens += r.total_tokens
|
||
existing.total_cost += r.cost
|
||
userStats.set(r.user_id, existing)
|
||
}
|
||
return createMockResponse(
|
||
Array.from(userStats.values()).map(stats => ({
|
||
user_id: stats.user_id,
|
||
email: stats.email,
|
||
username: stats.username,
|
||
request_count: stats.request_count * 50,
|
||
total_tokens: stats.total_tokens * 50,
|
||
total_cost: Number((stats.total_cost * 50).toFixed(2))
|
||
}))
|
||
)
|
||
}
|
||
|
||
if (groupBy === 'api_format') {
|
||
const formatStats = new Map<string, { request_count: number; total_tokens: number; total_cost: number; actual_cost: number; response_times: number[] }>()
|
||
for (const r of records) {
|
||
const existing = formatStats.get(r.api_format) || { request_count: 0, total_tokens: 0, total_cost: 0, actual_cost: 0, response_times: [] }
|
||
existing.request_count++
|
||
existing.total_tokens += r.total_tokens
|
||
existing.total_cost += r.cost
|
||
existing.actual_cost += r.actual_cost || 0
|
||
if (r.response_time_ms) existing.response_times.push(r.response_time_ms)
|
||
formatStats.set(r.api_format, existing)
|
||
}
|
||
return createMockResponse(
|
||
Array.from(formatStats.entries()).map(([api_format, stats]) => ({
|
||
api_format,
|
||
request_count: stats.request_count * 50,
|
||
total_tokens: stats.total_tokens * 50,
|
||
total_cost: Number((stats.total_cost * 50).toFixed(2)),
|
||
actual_cost: Number((stats.actual_cost * 50).toFixed(2)),
|
||
avg_response_time_ms: Math.round(stats.response_times.reduce((a, b) => a + b, 0) / stats.response_times.length || 0)
|
||
}))
|
||
)
|
||
}
|
||
|
||
return createMockResponse([])
|
||
},
|
||
|
||
'GET /api/admin/usage/active': async () => {
|
||
await delay()
|
||
requireAdmin()
|
||
return createMockResponse({ requests: [] })
|
||
},
|
||
|
||
// ========== Admin: System ==========
|
||
'GET /api/admin/system/configs': async () => {
|
||
await delay()
|
||
requireAdmin()
|
||
return createMockResponse(MOCK_SYSTEM_CONFIGS)
|
||
},
|
||
|
||
'GET /api/admin/system/api-formats': async () => {
|
||
await delay()
|
||
return createMockResponse(MOCK_API_FORMATS)
|
||
},
|
||
|
||
'GET /api/admin/system/stats': async () => {
|
||
await delay()
|
||
requireAdmin()
|
||
return createMockResponse({
|
||
total_requests_today: 1234,
|
||
total_requests_month: 45678,
|
||
total_users: 156,
|
||
active_users_today: 28,
|
||
total_cost_today: 45.67,
|
||
total_cost_month: 1234.56,
|
||
uptime_hours: 720,
|
||
cache_hit_rate: 0.35
|
||
})
|
||
},
|
||
|
||
// ========== 能力接口 ==========
|
||
'GET /api/capabilities': async () => {
|
||
await delay()
|
||
return createMockResponse({ capabilities: MOCK_CAPABILITIES })
|
||
},
|
||
|
||
'GET /api/capabilities/user-configurable': async () => {
|
||
await delay()
|
||
return createMockResponse({ capabilities: MOCK_CAPABILITIES.filter(c => c.match_mode === 'exclusive') })
|
||
},
|
||
|
||
// ========== 公开接口 ==========
|
||
'GET /api/public/global-models': async () => {
|
||
await delay()
|
||
return createMockResponse({
|
||
models: MOCK_GLOBAL_MODELS.map(m => ({
|
||
id: m.id,
|
||
name: m.name,
|
||
display_name: m.display_name,
|
||
is_active: m.is_active,
|
||
default_tiered_pricing: m.default_tiered_pricing,
|
||
default_price_per_request: m.default_price_per_request,
|
||
supported_capabilities: m.supported_capabilities,
|
||
config: m.config
|
||
})),
|
||
total: MOCK_GLOBAL_MODELS.length
|
||
})
|
||
},
|
||
|
||
'GET /api/public/models': async () => {
|
||
await delay()
|
||
return createMockResponse({
|
||
models: MOCK_GLOBAL_MODELS.map(m => ({
|
||
name: m.name,
|
||
display_name: m.display_name,
|
||
description: m.description
|
||
}))
|
||
})
|
||
},
|
||
|
||
'GET /api/public/health': async () => {
|
||
await delay(50)
|
||
return createMockResponse({ status: 'healthy', demo_mode: true })
|
||
},
|
||
|
||
'GET /api/public/health/api-formats': async () => {
|
||
await delay()
|
||
return createMockResponse({
|
||
generated_at: new Date().toISOString(),
|
||
formats: MOCK_ENDPOINT_STATUS.formats.map(f => ({
|
||
api_format: f.api_format,
|
||
api_path: f.api_path,
|
||
total_attempts: f.total_attempts,
|
||
success_count: f.success_count,
|
||
failed_count: f.failed_count,
|
||
skipped_count: f.skipped_count,
|
||
success_rate: f.success_rate,
|
||
last_event_at: f.last_event_at,
|
||
events: f.events.slice(0, 10)
|
||
}))
|
||
})
|
||
}
|
||
}
|
||
|
||
// 动态路由匹配器 - 支持 :id 形式的参数
|
||
interface RouteMatch {
|
||
handler: (config: AxiosRequestConfig, params: Record<string, string>) => Promise<AxiosResponse<any>>
|
||
params: Record<string, string>
|
||
}
|
||
|
||
type DynamicHandler = (config: AxiosRequestConfig, params: Record<string, string>) => Promise<AxiosResponse<any>>
|
||
|
||
// 动态路由注册表
|
||
const dynamicRoutes: Array<{
|
||
method: string
|
||
pattern: RegExp
|
||
paramNames: string[]
|
||
handler: DynamicHandler
|
||
}> = []
|
||
|
||
/**
|
||
* 注册动态路由
|
||
*/
|
||
function registerDynamicRoute(
|
||
method: string,
|
||
path: string,
|
||
handler: DynamicHandler
|
||
) {
|
||
// 将 :param 形式转换为正则
|
||
const paramNames: string[] = []
|
||
const regexStr = path.replace(/:([^/]+)/g, (_, paramName) => {
|
||
paramNames.push(paramName)
|
||
return '([^/]+)'
|
||
})
|
||
dynamicRoutes.push({
|
||
method: method.toUpperCase(),
|
||
pattern: new RegExp(`^${regexStr}$`),
|
||
paramNames,
|
||
handler
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 匹配动态路由
|
||
*/
|
||
function matchDynamicRoute(method: string, url: string): RouteMatch | null {
|
||
const cleanUrl = url.split('?')[0]
|
||
const upperMethod = method.toUpperCase()
|
||
|
||
for (const route of dynamicRoutes) {
|
||
if (route.method !== upperMethod) continue
|
||
const match = cleanUrl.match(route.pattern)
|
||
if (match) {
|
||
const params: Record<string, string> = {}
|
||
route.paramNames.forEach((name, index) => {
|
||
params[name] = match[index + 1]
|
||
})
|
||
return { handler: route.handler, params }
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
/**
|
||
* 匹配请求到 handler
|
||
*/
|
||
function matchHandler(method: string, url: string): ((config: AxiosRequestConfig) => Promise<AxiosResponse<any>>) | null {
|
||
// 移除查询参数
|
||
const cleanUrl = url.split('?')[0]
|
||
const upperMethod = method.toUpperCase()
|
||
|
||
// 精确匹配
|
||
const exactKey = `${upperMethod} ${cleanUrl}`
|
||
if (mockHandlers[exactKey]) {
|
||
return mockHandlers[exactKey]
|
||
}
|
||
|
||
// 动态路由匹配
|
||
const dynamicMatch = matchDynamicRoute(method, url)
|
||
if (dynamicMatch) {
|
||
return (config) => dynamicMatch.handler(config, dynamicMatch.params)
|
||
}
|
||
|
||
// 路径前缀匹配(按优先级排序)
|
||
const sortedPatterns = Object.keys(mockHandlers).sort((a, b) => b.length - a.length)
|
||
|
||
for (const pattern of sortedPatterns) {
|
||
const [patternMethod, patternPath] = pattern.split(' ')
|
||
if (patternMethod !== upperMethod) continue
|
||
|
||
// 检查是否为前缀匹配(用于处理带 ID 的路由)
|
||
if (cleanUrl.startsWith(patternPath) || patternPath === cleanUrl) {
|
||
return mockHandlers[pattern]
|
||
}
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
/**
|
||
* 处理 Mock 请求
|
||
*/
|
||
export async function handleMockRequest(config: AxiosRequestConfig): Promise<AxiosResponse<any> | null> {
|
||
if (!isDemoMode()) {
|
||
return null
|
||
}
|
||
|
||
const method = config.method?.toUpperCase() || 'GET'
|
||
const url = config.url || ''
|
||
|
||
// 尝试匹配 handler
|
||
const handler = matchHandler(method, url)
|
||
|
||
if (handler) {
|
||
try {
|
||
return await handler(config)
|
||
} catch (error: any) {
|
||
if (error.response) {
|
||
throw error
|
||
}
|
||
console.error('[Mock] Handler error:', error)
|
||
throw { response: createMockResponse({ detail: '模拟请求处理失败' }, 500) }
|
||
}
|
||
}
|
||
|
||
// 未匹配的请求返回默认响应
|
||
console.warn(`[Mock] Unhandled request: ${method} ${url}`)
|
||
return createMockResponse({ message: '演示模式:该接口暂未模拟', demo_mode: true })
|
||
}
|
||
|
||
/**
|
||
* 设置当前用户 token(供 client 初始化使用)
|
||
*/
|
||
export function setMockUserToken(token: string | null): void {
|
||
currentUserToken = token
|
||
}
|
||
|
||
/**
|
||
* 获取当前 mock token
|
||
*/
|
||
export function getMockUserToken(): string | null {
|
||
return currentUserToken
|
||
}
|
||
|
||
// ========== Mock Provider Endpoints 数据 ==========
|
||
// 为每个 provider 生成对应的 endpoints
|
||
function generateMockEndpointsForProvider(providerId: string) {
|
||
const provider = MOCK_PROVIDERS.find(p => p.id === providerId)
|
||
if (!provider || provider.api_formats.length === 0) return []
|
||
|
||
return provider.api_formats.map((format, index) => {
|
||
const healthDetail = provider.endpoint_health_details.find(h => h.api_format === format)
|
||
return {
|
||
id: `ep-${providerId}-${index + 1}`,
|
||
provider_id: providerId,
|
||
provider_name: provider.name,
|
||
api_format: format,
|
||
base_url: format.includes('CLAUDE') ? 'https://api.anthropic.com' :
|
||
format.includes('OPENAI') ? 'https://api.openai.com' :
|
||
'https://generativelanguage.googleapis.com',
|
||
auth_type: format.includes('GEMINI') ? 'api_key' : 'bearer',
|
||
timeout: 120,
|
||
max_retries: 3,
|
||
priority: 100 - index * 10,
|
||
weight: 100,
|
||
health_score: healthDetail?.health_score ?? 1.0,
|
||
consecutive_failures: healthDetail?.health_score && healthDetail.health_score < 0.7 ? 2 : 0,
|
||
is_active: healthDetail?.is_active ?? true,
|
||
total_keys: Math.ceil(Math.random() * 3) + 1,
|
||
active_keys: Math.ceil(Math.random() * 2) + 1,
|
||
created_at: provider.created_at,
|
||
updated_at: new Date().toISOString()
|
||
}
|
||
})
|
||
}
|
||
|
||
// 为 endpoint 生成 keys
|
||
function generateMockKeysForEndpoint(endpointId: string, count: number = 2) {
|
||
return Array.from({ length: count }, (_, i) => ({
|
||
id: `key-${endpointId}-${i + 1}`,
|
||
endpoint_id: endpointId,
|
||
api_key_masked: `sk-***...${Math.random().toString(36).substring(2, 6)}`,
|
||
name: i === 0 ? 'Primary Key' : `Backup Key ${i}`,
|
||
rate_multiplier: 1.0,
|
||
internal_priority: i + 1,
|
||
health_score: 0.90 + Math.random() * 0.10, // 0.90-1.00
|
||
consecutive_failures: Math.random() > 0.8 ? 1 : 0,
|
||
request_count: 1000 + Math.floor(Math.random() * 5000),
|
||
success_count: 950 + Math.floor(Math.random() * 4800),
|
||
error_count: Math.floor(Math.random() * 100),
|
||
success_rate: 0.95 + Math.random() * 0.04, // 0.95-0.99
|
||
avg_response_time_ms: 800 + Math.floor(Math.random() * 600),
|
||
is_active: true,
|
||
created_at: '2024-01-01T00:00:00Z',
|
||
updated_at: new Date().toISOString()
|
||
}))
|
||
}
|
||
|
||
// 为 provider 生成 models
|
||
function generateMockModelsForProvider(providerId: string) {
|
||
const provider = MOCK_PROVIDERS.find(p => p.id === providerId)
|
||
if (!provider) return []
|
||
|
||
// 基于 provider 的 api_formats 选择合适的模型
|
||
const hasClaude = provider.api_formats.some(f => f.includes('CLAUDE'))
|
||
const hasOpenAI = provider.api_formats.some(f => f.includes('OPENAI'))
|
||
const hasGemini = provider.api_formats.some(f => f.includes('GEMINI'))
|
||
|
||
const models: any[] = []
|
||
const now = new Date().toISOString()
|
||
|
||
if (hasClaude) {
|
||
models.push(
|
||
{
|
||
id: `pm-${providerId}-claude-1`,
|
||
provider_id: providerId,
|
||
global_model_id: 'gm-003',
|
||
provider_model_name: 'claude-sonnet-4-5-20250929',
|
||
global_model_name: 'claude-sonnet-4-5-20250929',
|
||
global_model_display_name: 'claude-sonnet-4-5',
|
||
effective_input_price: 3.0,
|
||
effective_output_price: 15.0,
|
||
effective_supports_vision: true,
|
||
effective_supports_function_calling: true,
|
||
effective_supports_streaming: true,
|
||
effective_supports_extended_thinking: true,
|
||
is_active: true,
|
||
is_available: true,
|
||
created_at: provider.created_at,
|
||
updated_at: now
|
||
},
|
||
{
|
||
id: `pm-${providerId}-claude-2`,
|
||
provider_id: providerId,
|
||
global_model_id: 'gm-001',
|
||
provider_model_name: 'claude-haiku-4-5-20251001',
|
||
global_model_name: 'claude-haiku-4-5-20251001',
|
||
global_model_display_name: 'claude-haiku-4-5',
|
||
effective_input_price: 1.0,
|
||
effective_output_price: 5.0,
|
||
effective_supports_vision: true,
|
||
effective_supports_function_calling: true,
|
||
effective_supports_streaming: true,
|
||
effective_supports_extended_thinking: true,
|
||
is_active: true,
|
||
is_available: true,
|
||
created_at: provider.created_at,
|
||
updated_at: now
|
||
}
|
||
)
|
||
}
|
||
if (hasOpenAI) {
|
||
models.push(
|
||
{
|
||
id: `pm-${providerId}-openai-1`,
|
||
provider_id: providerId,
|
||
global_model_id: 'gm-006',
|
||
provider_model_name: 'gpt-5.1',
|
||
global_model_name: 'gpt-5.1',
|
||
global_model_display_name: 'gpt-5.1',
|
||
effective_input_price: 1.25,
|
||
effective_output_price: 10.0,
|
||
effective_supports_vision: true,
|
||
effective_supports_function_calling: true,
|
||
effective_supports_streaming: true,
|
||
effective_supports_extended_thinking: true,
|
||
is_active: true,
|
||
is_available: true,
|
||
created_at: provider.created_at,
|
||
updated_at: now
|
||
},
|
||
{
|
||
id: `pm-${providerId}-openai-2`,
|
||
provider_id: providerId,
|
||
global_model_id: 'gm-007',
|
||
provider_model_name: 'gpt-5.1-codex',
|
||
global_model_name: 'gpt-5.1-codex',
|
||
global_model_display_name: 'gpt-5.1-codex',
|
||
effective_input_price: 1.25,
|
||
effective_output_price: 10.0,
|
||
effective_supports_vision: true,
|
||
effective_supports_function_calling: true,
|
||
effective_supports_streaming: true,
|
||
effective_supports_extended_thinking: true,
|
||
is_active: true,
|
||
is_available: true,
|
||
created_at: provider.created_at,
|
||
updated_at: now
|
||
}
|
||
)
|
||
}
|
||
if (hasGemini) {
|
||
models.push(
|
||
{
|
||
id: `pm-${providerId}-gemini-1`,
|
||
provider_id: providerId,
|
||
global_model_id: 'gm-005',
|
||
provider_model_name: 'gemini-3-pro-preview',
|
||
global_model_name: 'gemini-3-pro-preview',
|
||
global_model_display_name: 'gemini-3-pro-preview',
|
||
effective_input_price: 2.0,
|
||
effective_output_price: 12.0,
|
||
effective_supports_vision: true,
|
||
effective_supports_function_calling: true,
|
||
effective_supports_streaming: true,
|
||
effective_supports_extended_thinking: true,
|
||
is_active: true,
|
||
is_available: true,
|
||
created_at: provider.created_at,
|
||
updated_at: now
|
||
}
|
||
)
|
||
}
|
||
|
||
return models
|
||
}
|
||
|
||
// ========== 注册动态路由 ==========
|
||
|
||
// Provider 详情
|
||
registerDynamicRoute('GET', '/api/admin/providers/:providerId/summary', async (_config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const provider = MOCK_PROVIDERS.find(p => p.id === params.providerId)
|
||
if (!provider) {
|
||
throw { response: createMockResponse({ detail: '提供商不存在' }, 404) }
|
||
}
|
||
return createMockResponse(provider)
|
||
})
|
||
|
||
// Provider 更新
|
||
registerDynamicRoute('PATCH', '/api/admin/providers/:providerId', async (config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const provider = MOCK_PROVIDERS.find(p => p.id === params.providerId)
|
||
if (!provider) {
|
||
throw { response: createMockResponse({ detail: '提供商不存在' }, 404) }
|
||
}
|
||
const body = JSON.parse(config.data || '{}')
|
||
return createMockResponse({ ...provider, ...body, updated_at: new Date().toISOString() })
|
||
})
|
||
|
||
// Provider 删除
|
||
registerDynamicRoute('DELETE', '/api/admin/providers/:providerId', async (_config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const provider = MOCK_PROVIDERS.find(p => p.id === params.providerId)
|
||
if (!provider) {
|
||
throw { response: createMockResponse({ detail: '提供商不存在' }, 404) }
|
||
}
|
||
return createMockResponse({ message: '删除成功(演示模式)' })
|
||
})
|
||
|
||
// Provider Endpoints 列表
|
||
registerDynamicRoute('GET', '/api/admin/endpoints/providers/:providerId/endpoints', async (_config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const endpoints = generateMockEndpointsForProvider(params.providerId)
|
||
return createMockResponse(endpoints)
|
||
})
|
||
|
||
// 创建 Endpoint
|
||
registerDynamicRoute('POST', '/api/admin/endpoints/providers/:providerId/endpoints', async (config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const body = JSON.parse(config.data || '{}')
|
||
return createMockResponse({
|
||
id: `ep-demo-${Date.now()}`,
|
||
provider_id: params.providerId,
|
||
...body,
|
||
created_at: new Date().toISOString()
|
||
})
|
||
})
|
||
|
||
// Endpoint 详情
|
||
registerDynamicRoute('GET', '/api/admin/endpoints/:endpointId', async (_config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
// 从所有 providers 的 endpoints 中查找
|
||
for (const provider of MOCK_PROVIDERS) {
|
||
const endpoints = generateMockEndpointsForProvider(provider.id)
|
||
const endpoint = endpoints.find(e => e.id === params.endpointId)
|
||
if (endpoint) {
|
||
return createMockResponse(endpoint)
|
||
}
|
||
}
|
||
throw { response: createMockResponse({ detail: '端点不存在' }, 404) }
|
||
})
|
||
|
||
// Endpoint 更新
|
||
registerDynamicRoute('PUT', '/api/admin/endpoints/:endpointId', async (config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const body = JSON.parse(config.data || '{}')
|
||
return createMockResponse({ id: params.endpointId, ...body, updated_at: new Date().toISOString() })
|
||
})
|
||
|
||
// Endpoint 删除
|
||
registerDynamicRoute('DELETE', '/api/admin/endpoints/:endpointId', async (_config, _params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
return createMockResponse({ message: '删除成功(演示模式)' })
|
||
})
|
||
|
||
// Endpoint Keys 列表
|
||
registerDynamicRoute('GET', '/api/admin/endpoints/:endpointId/keys', async (_config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const keys = generateMockKeysForEndpoint(params.endpointId, 2)
|
||
return createMockResponse(keys)
|
||
})
|
||
|
||
// 创建 Key
|
||
registerDynamicRoute('POST', '/api/admin/endpoints/:endpointId/keys', async (config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const body = JSON.parse(config.data || '{}')
|
||
return createMockResponse({
|
||
id: `key-demo-${Date.now()}`,
|
||
endpoint_id: params.endpointId,
|
||
api_key_masked: 'sk-***...demo',
|
||
...body,
|
||
created_at: new Date().toISOString()
|
||
})
|
||
})
|
||
|
||
// Key 更新
|
||
registerDynamicRoute('PUT', '/api/admin/endpoints/keys/:keyId', async (config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const body = JSON.parse(config.data || '{}')
|
||
return createMockResponse({ id: params.keyId, ...body, updated_at: new Date().toISOString() })
|
||
})
|
||
|
||
// Key 删除
|
||
registerDynamicRoute('DELETE', '/api/admin/endpoints/keys/:keyId', async (_config, _params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
return createMockResponse({ message: '删除成功(演示模式)' })
|
||
})
|
||
|
||
// Provider Models 列表
|
||
registerDynamicRoute('GET', '/api/admin/providers/:providerId/models', async (_config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const models = generateMockModelsForProvider(params.providerId)
|
||
return createMockResponse(models)
|
||
})
|
||
|
||
// Provider Model 详情
|
||
registerDynamicRoute('GET', '/api/admin/providers/:providerId/models/:modelId', async (_config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const models = generateMockModelsForProvider(params.providerId)
|
||
const model = models.find(m => m.id === params.modelId)
|
||
if (!model) {
|
||
throw { response: createMockResponse({ detail: '模型不存在' }, 404) }
|
||
}
|
||
return createMockResponse(model)
|
||
})
|
||
|
||
// 创建 Provider Model
|
||
registerDynamicRoute('POST', '/api/admin/providers/:providerId/models', async (config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const body = JSON.parse(config.data || '{}')
|
||
return createMockResponse({
|
||
id: `pm-demo-${Date.now()}`,
|
||
provider_id: params.providerId,
|
||
...body,
|
||
created_at: new Date().toISOString()
|
||
})
|
||
})
|
||
|
||
// 更新 Provider Model
|
||
registerDynamicRoute('PATCH', '/api/admin/providers/:providerId/models/:modelId', async (config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const body = JSON.parse(config.data || '{}')
|
||
return createMockResponse({ id: params.modelId, provider_id: params.providerId, ...body, updated_at: new Date().toISOString() })
|
||
})
|
||
|
||
// 删除 Provider Model
|
||
registerDynamicRoute('DELETE', '/api/admin/providers/:providerId/models/:modelId', async (_config, _params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
return createMockResponse({ message: '删除成功(演示模式)' })
|
||
})
|
||
|
||
// 批量创建 Provider Models
|
||
registerDynamicRoute('POST', '/api/admin/providers/:providerId/models/batch', async (config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const body = JSON.parse(config.data || '{}')
|
||
const models = (body.models || []).map((m: any, i: number) => ({
|
||
id: `pm-demo-${Date.now()}-${i}`,
|
||
provider_id: params.providerId,
|
||
...m,
|
||
created_at: new Date().toISOString()
|
||
}))
|
||
return createMockResponse({ models, created_count: models.length })
|
||
})
|
||
|
||
// Provider 可用源模型
|
||
registerDynamicRoute('GET', '/api/admin/providers/:providerId/available-source-models', async (_config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const provider = MOCK_PROVIDERS.find(p => p.id === params.providerId)
|
||
if (!provider) {
|
||
throw { response: createMockResponse({ detail: '提供商不存在' }, 404) }
|
||
}
|
||
// 返回一些可用的源模型
|
||
const availableModels = [
|
||
'claude-sonnet-4-5-20250929',
|
||
'claude-haiku-4-5-20251001',
|
||
'claude-opus-4-5-20251101',
|
||
'gpt-5.1',
|
||
'gpt-5.1-codex',
|
||
'gemini-3-pro-preview'
|
||
]
|
||
return createMockResponse({ models: availableModels })
|
||
})
|
||
|
||
// 分配 GlobalModels 到 Provider
|
||
registerDynamicRoute('POST', '/api/admin/providers/:providerId/assign-global-models', async (config, _params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const body = JSON.parse(config.data || '{}')
|
||
const result = {
|
||
success: (body.global_model_ids || []).map((id: string) => ({
|
||
global_model_id: id,
|
||
provider_model_id: `pm-demo-${Date.now()}-${id}`
|
||
})),
|
||
errors: []
|
||
}
|
||
return createMockResponse(result)
|
||
})
|
||
|
||
// GlobalModel 详情
|
||
registerDynamicRoute('GET', '/api/admin/models/global/:modelId', async (_config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const model = MOCK_GLOBAL_MODELS.find(m => m.id === params.modelId)
|
||
if (!model) {
|
||
throw { response: createMockResponse({ detail: '模型不存在' }, 404) }
|
||
}
|
||
return createMockResponse(model)
|
||
})
|
||
|
||
// GlobalModel 更新
|
||
registerDynamicRoute('PATCH', '/api/admin/models/global/:modelId', async (config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const model = MOCK_GLOBAL_MODELS.find(m => m.id === params.modelId)
|
||
if (!model) {
|
||
throw { response: createMockResponse({ detail: '模型不存在' }, 404) }
|
||
}
|
||
const body = JSON.parse(config.data || '{}')
|
||
return createMockResponse({ ...model, ...body, updated_at: new Date().toISOString() })
|
||
})
|
||
|
||
// GlobalModel 删除
|
||
registerDynamicRoute('DELETE', '/api/admin/models/global/:modelId', async (_config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const model = MOCK_GLOBAL_MODELS.find(m => m.id === params.modelId)
|
||
if (!model) {
|
||
throw { response: createMockResponse({ detail: '模型不存在' }, 404) }
|
||
}
|
||
return createMockResponse({ message: '删除成功(演示模式)' })
|
||
})
|
||
|
||
// GlobalModel 批量分配到 Providers
|
||
registerDynamicRoute('POST', '/api/admin/models/global/:modelId/assign-to-providers', async (config, _params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const body = JSON.parse(config.data || '{}')
|
||
const result = {
|
||
success: (body.provider_ids || []).map((providerId: string) => {
|
||
const provider = MOCK_PROVIDERS.find(p => p.id === providerId)
|
||
return {
|
||
provider_id: providerId,
|
||
provider_name: provider?.name || 'unknown',
|
||
model_id: `pm-demo-${Date.now()}-${providerId}`
|
||
}
|
||
}),
|
||
errors: []
|
||
}
|
||
return createMockResponse(result)
|
||
})
|
||
|
||
// Endpoint Health 详情
|
||
registerDynamicRoute('GET', '/api/admin/endpoints/health/endpoint/:endpointId', async (_config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
return createMockResponse({
|
||
endpoint_id: params.endpointId,
|
||
health_score: 0.95,
|
||
total_requests: 5000,
|
||
success_count: 4750,
|
||
failed_count: 250,
|
||
success_rate: 0.95,
|
||
avg_response_time_ms: 1200,
|
||
last_success_at: new Date().toISOString(),
|
||
last_failure_at: new Date(Date.now() - 3600000).toISOString()
|
||
})
|
||
})
|
||
|
||
// Key Health 详情
|
||
registerDynamicRoute('GET', '/api/admin/endpoints/health/key/:keyId', async (_config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
return createMockResponse({
|
||
key_id: params.keyId,
|
||
health_score: 0.92,
|
||
total_requests: 2000,
|
||
success_count: 1840,
|
||
failed_count: 160,
|
||
success_rate: 0.92,
|
||
avg_response_time_ms: 1100,
|
||
last_success_at: new Date().toISOString(),
|
||
last_failure_at: new Date(Date.now() - 7200000).toISOString()
|
||
})
|
||
})
|
||
|
||
// 重置 Key Health
|
||
registerDynamicRoute('PATCH', '/api/admin/endpoints/health/keys/:keyId', async (_config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
return createMockResponse({
|
||
key_id: params.keyId,
|
||
message: '健康状态已重置(演示模式)'
|
||
})
|
||
})
|
||
|
||
// Alias/Mapping 详情
|
||
registerDynamicRoute('GET', '/api/admin/models/mappings/:mappingId', async (_config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const alias = MOCK_ALIASES.find(a => a.id === params.mappingId)
|
||
if (!alias) {
|
||
throw { response: createMockResponse({ detail: '别名不存在' }, 404) }
|
||
}
|
||
return createMockResponse(alias)
|
||
})
|
||
|
||
// Alias/Mapping 更新
|
||
registerDynamicRoute('PATCH', '/api/admin/models/mappings/:mappingId', async (config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const alias = MOCK_ALIASES.find(a => a.id === params.mappingId)
|
||
if (!alias) {
|
||
throw { response: createMockResponse({ detail: '别名不存在' }, 404) }
|
||
}
|
||
const body = JSON.parse(config.data || '{}')
|
||
return createMockResponse({ ...alias, ...body, updated_at: new Date().toISOString() })
|
||
})
|
||
|
||
// Alias/Mapping 删除
|
||
registerDynamicRoute('DELETE', '/api/admin/models/mappings/:mappingId', async (_config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const alias = MOCK_ALIASES.find(a => a.id === params.mappingId)
|
||
if (!alias) {
|
||
throw { response: createMockResponse({ detail: '别名不存在' }, 404) }
|
||
}
|
||
return createMockResponse({ message: '删除成功(演示模式)' })
|
||
})
|
||
|
||
// 公告详情
|
||
registerDynamicRoute('GET', '/api/announcements/:announcementId', async (_config, params) => {
|
||
await delay()
|
||
const announcement = MOCK_ANNOUNCEMENTS.find(a => a.id === params.announcementId)
|
||
if (!announcement) {
|
||
throw { response: createMockResponse({ detail: '公告不存在' }, 404) }
|
||
}
|
||
return createMockResponse(announcement)
|
||
})
|
||
|
||
// 公告更新
|
||
registerDynamicRoute('PATCH', '/api/announcements/:announcementId', async (config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const announcement = MOCK_ANNOUNCEMENTS.find(a => a.id === params.announcementId)
|
||
if (!announcement) {
|
||
throw { response: createMockResponse({ detail: '公告不存在' }, 404) }
|
||
}
|
||
const body = JSON.parse(config.data || '{}')
|
||
return createMockResponse({ ...announcement, ...body, updated_at: new Date().toISOString() })
|
||
})
|
||
|
||
// 公告删除
|
||
registerDynamicRoute('DELETE', '/api/announcements/:announcementId', async (_config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const announcement = MOCK_ANNOUNCEMENTS.find(a => a.id === params.announcementId)
|
||
if (!announcement) {
|
||
throw { response: createMockResponse({ detail: '公告不存在' }, 404) }
|
||
}
|
||
return createMockResponse({ message: '删除成功(演示模式)' })
|
||
})
|
||
|
||
// 用户详情
|
||
registerDynamicRoute('GET', '/api/admin/users/:userId', async (_config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const user = MOCK_ALL_USERS.find(u => u.id === params.userId)
|
||
if (!user) {
|
||
throw { response: createMockResponse({ detail: '用户不存在' }, 404) }
|
||
}
|
||
return createMockResponse(user)
|
||
})
|
||
|
||
// 用户更新
|
||
registerDynamicRoute('PATCH', '/api/admin/users/:userId', async (config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const user = MOCK_ALL_USERS.find(u => u.id === params.userId)
|
||
if (!user) {
|
||
throw { response: createMockResponse({ detail: '用户不存在' }, 404) }
|
||
}
|
||
const body = JSON.parse(config.data || '{}')
|
||
return createMockResponse({ ...user, ...body })
|
||
})
|
||
|
||
// 用户删除
|
||
registerDynamicRoute('DELETE', '/api/admin/users/:userId', async (_config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const user = MOCK_ALL_USERS.find(u => u.id === params.userId)
|
||
if (!user) {
|
||
throw { response: createMockResponse({ detail: '用户不存在' }, 404) }
|
||
}
|
||
return createMockResponse({ message: '删除成功(演示模式)' })
|
||
})
|
||
|
||
// 用户 API Keys
|
||
registerDynamicRoute('GET', '/api/admin/users/:userId/api-keys', async (_config, _params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
return createMockResponse(MOCK_USER_API_KEYS)
|
||
})
|
||
|
||
// API Key 详情
|
||
registerDynamicRoute('GET', '/api/admin/api-keys/:keyId', async (_config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const key = MOCK_ADMIN_API_KEYS.api_keys.find(k => k.id === params.keyId)
|
||
if (!key) {
|
||
throw { response: createMockResponse({ detail: 'API Key 不存在' }, 404) }
|
||
}
|
||
return createMockResponse(key)
|
||
})
|
||
|
||
// API Key 更新
|
||
registerDynamicRoute('PATCH', '/api/admin/api-keys/:keyId', async (config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const key = MOCK_ADMIN_API_KEYS.api_keys.find(k => k.id === params.keyId)
|
||
if (!key) {
|
||
throw { response: createMockResponse({ detail: 'API Key 不存在' }, 404) }
|
||
}
|
||
const body = JSON.parse(config.data || '{}')
|
||
return createMockResponse({ ...key, ...body })
|
||
})
|
||
|
||
// API Key 删除
|
||
registerDynamicRoute('DELETE', '/api/admin/api-keys/:keyId', async (_config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const key = MOCK_ADMIN_API_KEYS.api_keys.find(k => k.id === params.keyId)
|
||
if (!key) {
|
||
throw { response: createMockResponse({ detail: 'API Key 不存在' }, 404) }
|
||
}
|
||
return createMockResponse({ message: '删除成功(演示模式)' })
|
||
})
|
||
|
||
// 用户 API Key 删除
|
||
registerDynamicRoute('DELETE', '/api/users/me/api-keys/:keyId', async (_config, params) => {
|
||
await delay()
|
||
const key = MOCK_USER_API_KEYS.find(k => k.id === params.keyId)
|
||
if (!key) {
|
||
throw { response: createMockResponse({ detail: 'API Key 不存在' }, 404) }
|
||
}
|
||
return createMockResponse({ message: '删除成功(演示模式)' })
|
||
})
|
||
|
||
// 使用记录详情 - /api/admin/usage/:requestId
|
||
registerDynamicRoute('GET', '/api/admin/usage/:requestId', async (_config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
|
||
const records = getUsageRecords()
|
||
const record = records.find(r => r.id === params.requestId)
|
||
|
||
if (!record) {
|
||
throw { response: createMockResponse({ detail: '请求记录不存在' }, 404) }
|
||
}
|
||
|
||
// 生成详细的请求信息
|
||
const users = [
|
||
{ id: 'demo-admin-uuid-0001', username: 'Demo Admin', email: 'admin@demo.aether.ai' },
|
||
{ id: 'demo-user-uuid-0002', username: 'Demo User', email: 'user@demo.aether.ai' },
|
||
{ id: 'demo-user-uuid-0003', username: 'Alice Chen', email: 'alice@demo.aether.ai' },
|
||
{ id: 'demo-user-uuid-0004', username: 'Bob Zhang', email: 'bob@demo.aether.ai' }
|
||
]
|
||
const user = users.find(u => u.id === record.user_id) || users[0]
|
||
|
||
// 生成模拟的请求/响应数据
|
||
const mockRequestBody = {
|
||
model: record.model,
|
||
max_tokens: 4096,
|
||
messages: [
|
||
{
|
||
role: 'user',
|
||
content: 'Hello! Can you help me understand how API gateways work?'
|
||
}
|
||
],
|
||
stream: record.is_stream
|
||
}
|
||
|
||
const mockResponseBody = record.status === 'failed' ? {
|
||
error: {
|
||
type: 'api_error',
|
||
message: record.error_message || 'An error occurred'
|
||
}
|
||
} : {
|
||
id: `msg_${record.id}`,
|
||
type: 'message',
|
||
role: 'assistant',
|
||
content: [
|
||
{
|
||
type: 'text',
|
||
text: 'API gateways are middleware services that sit between clients and backend services. They handle routing, authentication, rate limiting, and more...'
|
||
}
|
||
],
|
||
model: record.model,
|
||
stop_reason: 'end_turn',
|
||
usage: {
|
||
input_tokens: record.input_tokens,
|
||
output_tokens: record.output_tokens
|
||
}
|
||
}
|
||
|
||
// 计算费用明细
|
||
const inputPricePer1M = record.model.includes('opus') ? 15 : record.model.includes('haiku') ? 1 : 3
|
||
const outputPricePer1M = record.model.includes('opus') ? 75 : record.model.includes('haiku') ? 5 : 15
|
||
const inputCost = (record.input_tokens / 1000000) * inputPricePer1M
|
||
const outputCost = (record.output_tokens / 1000000) * outputPricePer1M
|
||
const cacheCreationCost = (record.cache_creation_input_tokens / 1000000) * (inputPricePer1M * 1.25)
|
||
const cacheReadCost = (record.cache_read_input_tokens / 1000000) * (inputPricePer1M * 0.1)
|
||
|
||
const detail = {
|
||
id: record.id,
|
||
request_id: `req_${record.id}`,
|
||
user: {
|
||
id: user.id,
|
||
username: user.username,
|
||
email: user.email
|
||
},
|
||
api_key: {
|
||
id: `key-${record.api_key_name}`,
|
||
name: record.api_key_name,
|
||
display: `sk-***${record.api_key_name.slice(-4)}`
|
||
},
|
||
provider: record.provider,
|
||
api_format: record.api_format,
|
||
model: record.model,
|
||
target_model: record.target_model,
|
||
tokens: {
|
||
input: record.input_tokens,
|
||
output: record.output_tokens,
|
||
total: record.total_tokens
|
||
},
|
||
cost: {
|
||
input: inputCost,
|
||
output: outputCost,
|
||
total: record.cost
|
||
},
|
||
input_tokens: record.input_tokens,
|
||
output_tokens: record.output_tokens,
|
||
total_tokens: record.total_tokens,
|
||
cache_creation_input_tokens: record.cache_creation_input_tokens,
|
||
cache_read_input_tokens: record.cache_read_input_tokens,
|
||
input_cost: inputCost,
|
||
output_cost: outputCost,
|
||
total_cost: record.cost,
|
||
cache_creation_cost: cacheCreationCost,
|
||
cache_read_cost: cacheReadCost,
|
||
input_price_per_1m: inputPricePer1M,
|
||
output_price_per_1m: outputPricePer1M,
|
||
cache_creation_price_per_1m: inputPricePer1M * 1.25,
|
||
cache_read_price_per_1m: inputPricePer1M * 0.1,
|
||
request_type: record.is_stream ? 'stream' : 'standard',
|
||
is_stream: record.is_stream,
|
||
status_code: record.status_code,
|
||
error_message: record.error_message,
|
||
response_time_ms: record.response_time_ms,
|
||
created_at: record.created_at,
|
||
request_headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': 'Bearer sk-aether-***',
|
||
'X-Api-Key': 'sk-***',
|
||
'User-Agent': 'Aether-Client/1.0',
|
||
'Accept': 'application/json',
|
||
'X-Request-ID': `req_${record.id}`
|
||
},
|
||
request_body: mockRequestBody,
|
||
provider_request_headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer sk-${record.provider}-***`,
|
||
'anthropic-version': '2024-01-01',
|
||
'X-Request-ID': `req_${record.id}`
|
||
},
|
||
response_headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Request-ID': `req_${record.id}`,
|
||
'X-RateLimit-Limit': '1000',
|
||
'X-RateLimit-Remaining': '999',
|
||
'X-RateLimit-Reset': new Date(Date.now() + 60000).toISOString()
|
||
},
|
||
response_body: mockResponseBody,
|
||
metadata: {
|
||
client_ip: '192.168.1.100',
|
||
user_agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
|
||
request_path: `/v1/messages`,
|
||
provider_endpoint: `https://api.${record.provider}.com/v1/messages`,
|
||
gateway_version: '1.0.0',
|
||
processing_time_ms: Math.floor((record.response_time_ms || 1000) * 0.1)
|
||
},
|
||
tiered_pricing: {
|
||
total_input_context: record.input_tokens + record.cache_creation_input_tokens + record.cache_read_input_tokens,
|
||
tier_index: 0,
|
||
source: 'provider',
|
||
tiers: [
|
||
{
|
||
up_to: 200000,
|
||
input_price_per_1m: inputPricePer1M,
|
||
output_price_per_1m: outputPricePer1M,
|
||
cache_creation_price_per_1m: inputPricePer1M * 1.25,
|
||
cache_read_price_per_1m: inputPricePer1M * 0.1
|
||
},
|
||
{
|
||
up_to: null,
|
||
input_price_per_1m: inputPricePer1M * 0.5,
|
||
output_price_per_1m: outputPricePer1M * 0.5,
|
||
cache_creation_price_per_1m: inputPricePer1M * 0.625,
|
||
cache_read_price_per_1m: inputPricePer1M * 0.05
|
||
}
|
||
]
|
||
}
|
||
}
|
||
|
||
return createMockResponse(detail)
|
||
})
|
||
|
||
// 请求链路追踪 - /api/admin/monitoring/trace/:requestId
|
||
registerDynamicRoute('GET', '/api/admin/monitoring/trace/:requestId', async (_config, params) => {
|
||
await delay()
|
||
requireAdmin()
|
||
|
||
const requestId = params.requestId
|
||
// 从 usage-xxxx 格式中提取记录
|
||
const records = getUsageRecords()
|
||
const recordId = requestId.startsWith('req_') ? requestId.replace('req_', '') : requestId
|
||
const record = records.find(r => r.id === recordId)
|
||
|
||
if (!record) {
|
||
throw { response: createMockResponse({ detail: '请求记录不存在' }, 404) }
|
||
}
|
||
|
||
// 生成候选记录
|
||
const now = new Date(record.created_at)
|
||
const baseLatency = record.response_time_ms || 1000
|
||
|
||
// 根据请求状态生成不同的候选链路
|
||
const candidates = []
|
||
const providerNames = ['AlphaAI', 'BetaClaude', 'GammaCode', 'DeltaAPI']
|
||
|
||
if (record.status === 'completed') {
|
||
// 成功请求:可能有1-2个跳过的候选,最后一个成功
|
||
const skipCount = Math.random() > 0.5 ? 1 : 0
|
||
|
||
for (let i = 0; i < skipCount; i++) {
|
||
const skipStarted = new Date(now.getTime() + i * 50)
|
||
candidates.push({
|
||
id: `candidate-${requestId}-${i}`,
|
||
request_id: requestId,
|
||
candidate_index: i,
|
||
retry_index: 0,
|
||
provider_id: `provider-${i + 1}`,
|
||
provider_name: providerNames[i % providerNames.length],
|
||
provider_website: `https://${providerNames[i % providerNames.length].toLowerCase()}.com`,
|
||
endpoint_id: `endpoint-${i + 1}`,
|
||
endpoint_name: record.api_format,
|
||
key_id: `key-${i + 1}`,
|
||
key_name: `${record.provider}-key-${i + 1}`,
|
||
key_preview: `sk-***${Math.random().toString(36).substring(2, 6)}`,
|
||
key_capabilities: { 'cache_1h': true, 'vision': true },
|
||
required_capabilities: { 'cache_1h': record.cache_read_input_tokens > 0 },
|
||
status: 'skipped',
|
||
skip_reason: ['并发限制已满', '健康分数过低', '倍率不匹配'][i % 3],
|
||
is_cached: false,
|
||
latency_ms: 10 + Math.floor(Math.random() * 20),
|
||
created_at: skipStarted.toISOString(),
|
||
started_at: skipStarted.toISOString(),
|
||
finished_at: new Date(skipStarted.getTime() + 10).toISOString()
|
||
})
|
||
}
|
||
|
||
// 成功的候选
|
||
const successStarted = new Date(now.getTime() + skipCount * 50)
|
||
candidates.push({
|
||
id: `candidate-${requestId}-success`,
|
||
request_id: requestId,
|
||
candidate_index: skipCount,
|
||
retry_index: 0,
|
||
provider_id: `provider-${record.provider}`,
|
||
provider_name: record.provider === 'anthropic' ? 'AlphaAI' : record.provider === 'openai' ? 'BetaClaude' : 'GammaCode',
|
||
provider_website: `https://api.${record.provider}.com`,
|
||
endpoint_id: `endpoint-${record.provider}`,
|
||
endpoint_name: record.api_format,
|
||
key_id: `key-${record.api_key_name}`,
|
||
key_name: record.api_key_name,
|
||
key_preview: `sk-***${Math.random().toString(36).substring(2, 6)}`,
|
||
key_capabilities: { 'cache_1h': true, 'vision': true, 'extended_thinking': true },
|
||
required_capabilities: {
|
||
'cache_1h': record.cache_read_input_tokens > 0,
|
||
'vision': false,
|
||
'extended_thinking': false
|
||
},
|
||
status: 'success',
|
||
is_cached: record.cache_read_input_tokens > 0,
|
||
status_code: 200,
|
||
latency_ms: baseLatency,
|
||
created_at: successStarted.toISOString(),
|
||
started_at: successStarted.toISOString(),
|
||
finished_at: new Date(successStarted.getTime() + baseLatency).toISOString()
|
||
})
|
||
} else if (record.status === 'failed') {
|
||
// 失败请求:多个候选都失败
|
||
const attemptCount = 2 + Math.floor(Math.random() * 2)
|
||
|
||
for (let i = 0; i < attemptCount; i++) {
|
||
const attemptStarted = new Date(now.getTime() + i * 200)
|
||
const attemptLatency = 100 + Math.floor(Math.random() * 500)
|
||
candidates.push({
|
||
id: `candidate-${requestId}-${i}`,
|
||
request_id: requestId,
|
||
candidate_index: i,
|
||
retry_index: 0,
|
||
provider_id: `provider-${i + 1}`,
|
||
provider_name: providerNames[i % providerNames.length],
|
||
provider_website: `https://${providerNames[i % providerNames.length].toLowerCase()}.com`,
|
||
endpoint_id: `endpoint-${i + 1}`,
|
||
endpoint_name: record.api_format,
|
||
key_id: `key-${i + 1}`,
|
||
key_name: `${record.provider}-key-${i + 1}`,
|
||
key_preview: `sk-***${Math.random().toString(36).substring(2, 6)}`,
|
||
key_capabilities: { 'cache_1h': true },
|
||
required_capabilities: {},
|
||
status: 'failed',
|
||
is_cached: false,
|
||
status_code: record.status_code,
|
||
error_type: ['rate_limit_error', 'api_error', 'timeout_error'][i % 3],
|
||
error_message: record.error_message || 'Request failed',
|
||
latency_ms: attemptLatency,
|
||
created_at: attemptStarted.toISOString(),
|
||
started_at: attemptStarted.toISOString(),
|
||
finished_at: new Date(attemptStarted.getTime() + attemptLatency).toISOString()
|
||
})
|
||
}
|
||
} else {
|
||
// 进行中的请求
|
||
candidates.push({
|
||
id: `candidate-${requestId}-0`,
|
||
request_id: requestId,
|
||
candidate_index: 0,
|
||
retry_index: 0,
|
||
provider_id: `provider-${record.provider}`,
|
||
provider_name: record.provider === 'anthropic' ? 'AlphaAI' : record.provider === 'openai' ? 'BetaClaude' : 'GammaCode',
|
||
provider_website: `https://api.${record.provider}.com`,
|
||
endpoint_id: `endpoint-${record.provider}`,
|
||
endpoint_name: record.api_format,
|
||
key_id: `key-${record.api_key_name}`,
|
||
key_name: record.api_key_name,
|
||
key_preview: `sk-***${Math.random().toString(36).substring(2, 6)}`,
|
||
key_capabilities: { 'cache_1h': true, 'vision': true },
|
||
required_capabilities: {},
|
||
status: 'streaming',
|
||
is_cached: false,
|
||
latency_ms: undefined,
|
||
created_at: now.toISOString(),
|
||
started_at: now.toISOString(),
|
||
finished_at: undefined
|
||
})
|
||
}
|
||
|
||
const totalLatency = candidates.reduce((sum, c) => sum + (c.latency_ms || 0), 0)
|
||
|
||
return createMockResponse({
|
||
request_id: requestId,
|
||
total_candidates: candidates.length,
|
||
final_status: record.status === 'completed' ? 'success' : record.status === 'failed' ? 'failed' : 'streaming',
|
||
total_latency_ms: totalLatency,
|
||
candidates
|
||
})
|
||
})
|
||
|
||
// ========== 请求间隔时间线 Mock 数据 ==========
|
||
|
||
// 生成请求间隔时间线数据(用于散点图)
|
||
function generateIntervalTimelineData(
|
||
hours: number = 24,
|
||
limit: number = 5000,
|
||
includeUserInfo: boolean = false
|
||
) {
|
||
const now = Date.now()
|
||
const startTime = now - hours * 60 * 60 * 1000
|
||
const points: Array<{ x: string; y: number; user_id?: string; model?: string }> = []
|
||
|
||
// 用户列表(用于管理员视图)
|
||
const users = [
|
||
{ id: 'demo-admin-uuid-0001', username: 'Demo Admin' },
|
||
{ id: 'demo-user-uuid-0002', username: 'Demo User' },
|
||
{ id: 'demo-user-uuid-0003', username: 'Alice Chen' },
|
||
{ id: 'demo-user-uuid-0004', username: 'Bob Zhang' }
|
||
]
|
||
|
||
// 模型列表(用于按模型区分颜色)
|
||
const models = [
|
||
'claude-sonnet-4-20250514',
|
||
'claude-3-5-sonnet-20241022',
|
||
'claude-3-5-haiku-20241022',
|
||
'claude-opus-4-20250514'
|
||
]
|
||
|
||
// 生成模拟的请求间隔数据
|
||
// 间隔时间分布:大部分在 0-10 分钟,少量在 10-60 分钟,极少数在 60-120 分钟
|
||
const pointCount = Math.min(limit, Math.floor(hours * 80)) // 每小时约 80 个数据点
|
||
|
||
let currentTime = startTime + Math.random() * 60 * 1000 // 从起始时间后随机开始
|
||
|
||
for (let i = 0; i < pointCount && currentTime < now; i++) {
|
||
// 生成间隔时间(分钟),使用指数分布模拟真实场景
|
||
let interval: number
|
||
const rand = Math.random()
|
||
if (rand < 0.7) {
|
||
// 70% 的请求间隔在 0-5 分钟
|
||
interval = Math.random() * 5
|
||
} else if (rand < 0.9) {
|
||
// 20% 的请求间隔在 5-30 分钟
|
||
interval = 5 + Math.random() * 25
|
||
} else if (rand < 0.98) {
|
||
// 8% 的请求间隔在 30-90 分钟
|
||
interval = 30 + Math.random() * 60
|
||
} else {
|
||
// 2% 的请求间隔在 90-120 分钟
|
||
interval = 90 + Math.random() * 30
|
||
}
|
||
|
||
// 添加一些工作时间的模式(工作时间间隔更短)
|
||
const hour = new Date(currentTime).getHours()
|
||
if (hour >= 9 && hour <= 18) {
|
||
interval *= 0.6 // 工作时间间隔更短
|
||
} else if (hour >= 22 || hour <= 6) {
|
||
interval *= 1.5 // 夜间间隔更长
|
||
}
|
||
|
||
// 确保间隔不超过 120 分钟
|
||
interval = Math.min(interval, 120)
|
||
|
||
const point: { x: string; y: number; user_id?: string; model?: string } = {
|
||
x: new Date(currentTime).toISOString(),
|
||
y: Math.round(interval * 100) / 100,
|
||
model: models[Math.floor(Math.random() * models.length)]
|
||
}
|
||
|
||
if (includeUserInfo) {
|
||
// 管理员视图:添加用户信息
|
||
const user = users[Math.floor(Math.random() * users.length)]
|
||
point.user_id = user.id
|
||
}
|
||
|
||
points.push(point)
|
||
|
||
// 下一个请求时间 = 当前时间 + 间隔 + 一些随机抖动
|
||
currentTime += interval * 60 * 1000 + Math.random() * 30 * 1000
|
||
}
|
||
|
||
// 按时间排序
|
||
points.sort((a, b) => new Date(a.x).getTime() - new Date(b.x).getTime())
|
||
|
||
// 收集出现的模型
|
||
const usedModels = [...new Set(points.map(p => p.model).filter(Boolean))] as string[]
|
||
|
||
const response: {
|
||
analysis_period_hours: number
|
||
total_points: number
|
||
points: typeof points
|
||
users?: Record<string, string>
|
||
models?: string[]
|
||
} = {
|
||
analysis_period_hours: hours,
|
||
total_points: points.length,
|
||
points,
|
||
models: usedModels
|
||
}
|
||
|
||
if (includeUserInfo) {
|
||
response.users = Object.fromEntries(users.map(u => [u.id, u.username]))
|
||
}
|
||
|
||
return response
|
||
}
|
||
|
||
// 用户 interval-timeline 接口
|
||
mockHandlers['GET /api/users/me/usage/interval-timeline'] = async (config) => {
|
||
await delay()
|
||
const params = config.params || {}
|
||
const hours = parseInt(params.hours) || 24
|
||
const limit = parseInt(params.limit) || 5000
|
||
const data = generateIntervalTimelineData(hours, limit, false)
|
||
return createMockResponse(data)
|
||
}
|
||
|
||
// 管理员 interval-timeline 接口
|
||
mockHandlers['GET /api/admin/usage/cache-affinity/interval-timeline'] = async (config) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const params = config.params || {}
|
||
const hours = parseInt(params.hours) || 24
|
||
const limit = parseInt(params.limit) || 10000
|
||
const userId = params.user_id
|
||
const includeUserInfo = params.include_user_info === 'true' || params.include_user_info === true
|
||
|
||
// 如果指定了 user_id,则不包含用户信息
|
||
const data = generateIntervalTimelineData(hours, limit, includeUserInfo && !userId)
|
||
return createMockResponse(data)
|
||
}
|
||
|
||
// ========== TTL 分析 Mock 数据 ==========
|
||
|
||
// 生成 TTL 分析数据
|
||
function generateTTLAnalysisData(hours: number = 168) {
|
||
const users = [
|
||
{ id: 'demo-admin-uuid-0001', username: 'Demo Admin', email: 'admin@demo.aether.io' },
|
||
{ id: 'demo-user-uuid-0002', username: 'Demo User', email: 'user@demo.aether.io' },
|
||
{ id: 'demo-user-uuid-0003', username: 'Alice Chen', email: 'alice@demo.aether.io' },
|
||
{ id: 'demo-user-uuid-0004', username: 'Bob Zhang', email: 'bob@demo.aether.io' }
|
||
]
|
||
|
||
const usersAnalysis = users.map(user => {
|
||
// 为每个用户生成不同的使用模式
|
||
const requestCount = 50 + Math.floor(Math.random() * 500)
|
||
|
||
// 根据用户特性生成不同的间隔分布
|
||
let within5min, within15min, within30min, within60min, over60min
|
||
let p50, p75, p90
|
||
let recommendedTtl: number
|
||
let recommendationReason: string
|
||
|
||
const userType = Math.random()
|
||
if (userType < 0.3) {
|
||
// 高频用户 (30%)
|
||
within5min = Math.floor(requestCount * (0.6 + Math.random() * 0.2))
|
||
within15min = Math.floor(requestCount * (0.1 + Math.random() * 0.1))
|
||
within30min = Math.floor(requestCount * (0.05 + Math.random() * 0.05))
|
||
within60min = Math.floor(requestCount * (0.02 + Math.random() * 0.03))
|
||
over60min = requestCount - within5min - within15min - within30min - within60min
|
||
p50 = 1.5 + Math.random() * 2
|
||
p75 = 3 + Math.random() * 3
|
||
p90 = 4 + Math.random() * 2
|
||
recommendedTtl = 5
|
||
recommendationReason = `高频用户:90% 的请求间隔在 ${p90.toFixed(1)} 分钟内`
|
||
} else if (userType < 0.6) {
|
||
// 中频用户 (30%)
|
||
within5min = Math.floor(requestCount * (0.3 + Math.random() * 0.15))
|
||
within15min = Math.floor(requestCount * (0.25 + Math.random() * 0.15))
|
||
within30min = Math.floor(requestCount * (0.15 + Math.random() * 0.1))
|
||
within60min = Math.floor(requestCount * (0.1 + Math.random() * 0.05))
|
||
over60min = requestCount - within5min - within15min - within30min - within60min
|
||
p50 = 5 + Math.random() * 5
|
||
p75 = 10 + Math.random() * 8
|
||
p90 = 18 + Math.random() * 10
|
||
recommendedTtl = 15
|
||
recommendationReason = `中高频用户:75% 的请求间隔在 ${p75.toFixed(1)} 分钟内`
|
||
} else if (userType < 0.85) {
|
||
// 中低频用户 (25%)
|
||
within5min = Math.floor(requestCount * (0.15 + Math.random() * 0.1))
|
||
within15min = Math.floor(requestCount * (0.2 + Math.random() * 0.1))
|
||
within30min = Math.floor(requestCount * (0.25 + Math.random() * 0.1))
|
||
within60min = Math.floor(requestCount * (0.15 + Math.random() * 0.1))
|
||
over60min = requestCount - within5min - within15min - within30min - within60min
|
||
p50 = 12 + Math.random() * 8
|
||
p75 = 22 + Math.random() * 10
|
||
p90 = 35 + Math.random() * 15
|
||
recommendedTtl = 30
|
||
recommendationReason = `中频用户:75% 的请求间隔在 ${p75.toFixed(1)} 分钟内`
|
||
} else {
|
||
// 低频用户 (15%)
|
||
within5min = Math.floor(requestCount * (0.05 + Math.random() * 0.1))
|
||
within15min = Math.floor(requestCount * (0.1 + Math.random() * 0.1))
|
||
within30min = Math.floor(requestCount * (0.15 + Math.random() * 0.1))
|
||
within60min = Math.floor(requestCount * (0.25 + Math.random() * 0.1))
|
||
over60min = requestCount - within5min - within15min - within30min - within60min
|
||
p50 = 25 + Math.random() * 15
|
||
p75 = 45 + Math.random() * 20
|
||
p90 = 70 + Math.random() * 30
|
||
recommendedTtl = 60
|
||
recommendationReason = `低频用户:75% 的请求间隔为 ${p75.toFixed(1)} 分钟,建议使用长 TTL`
|
||
}
|
||
|
||
// 确保没有负数
|
||
over60min = Math.max(0, over60min)
|
||
|
||
const avgInterval = (within5min * 2.5 + within15min * 10 + within30min * 22 + within60min * 45 + over60min * 80) / requestCount
|
||
|
||
return {
|
||
group_id: user.id,
|
||
username: user.username,
|
||
email: user.email,
|
||
request_count: requestCount,
|
||
interval_distribution: {
|
||
within_5min: within5min,
|
||
within_15min: within15min,
|
||
within_30min: within30min,
|
||
within_60min: within60min,
|
||
over_60min: over60min
|
||
},
|
||
interval_percentages: {
|
||
within_5min: Math.round(within5min / requestCount * 1000) / 10,
|
||
within_15min: Math.round(within15min / requestCount * 1000) / 10,
|
||
within_30min: Math.round(within30min / requestCount * 1000) / 10,
|
||
within_60min: Math.round(within60min / requestCount * 1000) / 10,
|
||
over_60min: Math.round(over60min / requestCount * 1000) / 10
|
||
},
|
||
percentiles: {
|
||
p50: Math.round(p50 * 100) / 100,
|
||
p75: Math.round(p75 * 100) / 100,
|
||
p90: Math.round(p90 * 100) / 100
|
||
},
|
||
avg_interval_minutes: Math.round(avgInterval * 100) / 100,
|
||
min_interval_minutes: Math.round((0.1 + Math.random() * 0.5) * 100) / 100,
|
||
max_interval_minutes: Math.round((80 + Math.random() * 40) * 100) / 100,
|
||
recommended_ttl_minutes: recommendedTtl,
|
||
recommendation_reason: recommendationReason
|
||
}
|
||
})
|
||
|
||
// 汇总 TTL 分布
|
||
const ttlDistribution = {
|
||
'5min': usersAnalysis.filter(u => u.recommended_ttl_minutes === 5).length,
|
||
'15min': usersAnalysis.filter(u => u.recommended_ttl_minutes === 15).length,
|
||
'30min': usersAnalysis.filter(u => u.recommended_ttl_minutes === 30).length,
|
||
'60min': usersAnalysis.filter(u => u.recommended_ttl_minutes === 60).length
|
||
}
|
||
|
||
return {
|
||
analysis_period_hours: hours,
|
||
total_users_analyzed: usersAnalysis.length,
|
||
ttl_distribution: ttlDistribution,
|
||
users: usersAnalysis
|
||
}
|
||
}
|
||
|
||
// 生成缓存命中分析数据
|
||
function generateCacheHitAnalysisData(hours: number = 168) {
|
||
const totalRequests = 5000 + Math.floor(Math.random() * 10000)
|
||
const requestsWithCacheHit = Math.floor(totalRequests * (0.25 + Math.random() * 0.35))
|
||
const totalInputTokens = totalRequests * (2000 + Math.floor(Math.random() * 3000))
|
||
const totalCacheReadTokens = Math.floor(totalInputTokens * (0.15 + Math.random() * 0.25))
|
||
const totalCacheCreationTokens = Math.floor(totalInputTokens * (0.05 + Math.random() * 0.1))
|
||
|
||
// 缓存读取成本:按每百万 token $0.30 计算
|
||
const cacheReadCostPer1M = 0.30
|
||
const cacheCreationCostPer1M = 3.75
|
||
const totalCacheReadCost = (totalCacheReadTokens / 1000000) * cacheReadCostPer1M
|
||
const totalCacheCreationCost = (totalCacheCreationTokens / 1000000) * cacheCreationCostPer1M
|
||
|
||
// 缓存读取节省了 90% 的成本
|
||
const estimatedSavings = totalCacheReadCost * 9
|
||
|
||
const tokenCacheHitRate = totalCacheReadTokens / (totalInputTokens + totalCacheReadTokens) * 100
|
||
|
||
return {
|
||
analysis_period_hours: hours,
|
||
total_requests: totalRequests,
|
||
requests_with_cache_hit: requestsWithCacheHit,
|
||
request_cache_hit_rate: Math.round(requestsWithCacheHit / totalRequests * 10000) / 100,
|
||
total_input_tokens: totalInputTokens,
|
||
total_cache_read_tokens: totalCacheReadTokens,
|
||
total_cache_creation_tokens: totalCacheCreationTokens,
|
||
token_cache_hit_rate: Math.round(tokenCacheHitRate * 100) / 100,
|
||
total_cache_read_cost_usd: Math.round(totalCacheReadCost * 10000) / 10000,
|
||
total_cache_creation_cost_usd: Math.round(totalCacheCreationCost * 10000) / 10000,
|
||
estimated_savings_usd: Math.round(estimatedSavings * 10000) / 10000
|
||
}
|
||
}
|
||
|
||
// TTL 分析接口
|
||
mockHandlers['GET /api/admin/usage/cache-affinity/ttl-analysis'] = async (config) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const params = config.params || {}
|
||
const hours = parseInt(params.hours) || 168
|
||
const data = generateTTLAnalysisData(hours)
|
||
return createMockResponse(data)
|
||
}
|
||
|
||
// 缓存命中分析接口
|
||
mockHandlers['GET /api/admin/usage/cache-affinity/hit-analysis'] = async (config) => {
|
||
await delay()
|
||
requireAdmin()
|
||
const params = config.params || {}
|
||
const hours = parseInt(params.hours) || 168
|
||
const data = generateCacheHitAnalysisData(hours)
|
||
return createMockResponse(data)
|
||
}
|