mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 08:27:46 +08:00
3820 lines
134 KiB
TypeScript
3820 lines
134 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_MODULE_STATUSES,
|
||||
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 AxiosRequestConfig
|
||||
}
|
||||
|
|
}
|
|||
|
|
|
|||
|
|
// 判断当前是否为管理员
|
|||
|
|
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_responses, gemini, gemini_cli
|
||||
const MOCK_ENDPOINT_STATUS = {
|
||||
|
|
generated_at: new Date().toISOString(),
|
|||
|
|
formats: [
|
|||
|
|
{
|
|||
api_format: 'claude:messages',
|
||||
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:messages',
|
||||
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:generate_content',
|
||||
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:generate_content',
|
||||
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:chat',
|
||||
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:responses',
|
||||
api_path: '/v1/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)
|
|||
},
|
||||
|
|
{
|
|||
|
|
api_format: 'openai:embedding',
|
|||
|
|
api_path: '/v1/embeddings',
|
|||
|
|
total_attempts: 620,
|
|||
|
|
success_count: 612,
|
|||
|
|
failed_count: 6,
|
|||
|
|
skipped_count: 2,
|
|||
|
|
success_rate: 0.987,
|
|||
|
|
provider_count: 1,
|
|||
|
|
key_count: 1,
|
|||
|
|
last_event_at: new Date().toISOString(),
|
|||
|
|
events: generateHealthEvents(40, 0.987, 0.01, 0.003, 320, 140)
|
|||
}
|
||||
|
|
]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 生成活跃热力图数据(最近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: '[email protected]' },
|
|||
|
|
{ id: 'demo-user-uuid-0002', username: 'Demo User', email: '[email protected]' },
|
|||
|
|
{ id: 'demo-user-uuid-0003', username: 'Alice Chen', email: '[email protected]' },
|
|||
|
|
{ id: 'demo-user-uuid-0004', username: 'Bob Zhang', email: '[email protected]' }
|
|||
|
|
]
|
|||
|
|
|
|||
const apiFormats = ['claude:messages', 'openai:chat', 'openai:responses', 'gemini:generate_content']
|
||||
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 = 'claude:messages'
|
||||
} else if (model.provider === 'openai') {
|
||||
apiFormat = Math.random() > 0.3 ? 'openai:responses' : 'openai:chat'
|
||||
} else {
|
||||
apiFormat = 'gemini:generate_content'
|
||||
}
|
||||
|
|
|
|||
|
|
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,
|
|||
api_key: {
|
||||
|
|
id: `key-${user.id}-${Math.ceil(Math.random() * 2)}`,
|
|||
|
|
name: `${user.username} Key ${Math.ceil(Math.random() * 3)}`,
|
|||
|
|
display: `sk-ae...${String(1000 + Math.floor(Math.random() * 9000))}`
|
|||
|
|
},
|
|||
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,
|
|||
model_version: model.provider === 'google' ? '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-003', target_global_model_name: 'claude-sonnet-4-5-20250929', target_global_model_display_name: 'Claude Sonnet 4.5', 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-5-20251101', target_global_model_display_name: 'Claude Opus 4.5', 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: 'gpt5', target_global_model_id: 'gm-006', target_global_model_name: 'gpt-5.1', target_global_model_display_name: 'GPT-5.1', 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-pro', target_global_model_id: 'gm-005', target_global_model_name: 'gemini-3-pro-preview', target_global_model_display_name: 'Gemini 3 Pro Preview', 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' }
|
|||
]
|
||||
|
|
|
|||
interface MockRoutingGroup {
|
||||
|
|
id: string
|
|||
|
|
name: string
|
|||
|
|
description: string | null
|
|||
|
|
enabled: boolean
|
|||
|
|
is_system_default: boolean
|
|||
|
|
config_json: Record<string, unknown>
|
|||
|
|
version: number
|
|||
|
|
created_at: number
|
|||
|
|
updated_at: number
|
|||
|
|
published_at: number | null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface MockRoutingGroupVersion {
|
|||
|
|
id: string
|
|||
|
|
group_id: string
|
|||
|
|
version: number
|
|||
|
|
config_json: Record<string, unknown>
|
|||
|
|
created_at: number
|
|||
|
|
created_by: string | null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface MockRoutingGroupBinding {
|
|||
|
|
id: string
|
|||
|
|
group_id: string
|
|||
|
|
subject_type: 'user' | 'api_key' | 'user_group'
|
|||
|
|
subject_id: string
|
|||
|
|
is_default: boolean
|
|||
|
|
allow_explicit_select: boolean
|
|||
|
|
created_at: number
|
|||
|
|
updated_at: number
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const mockRoutingNow = Math.floor(Date.now() / 1000)
|
|||
|
|
const MOCK_ROUTING_GROUPS: MockRoutingGroup[] = [
|
|||
|
|
{
|
|||
|
|
id: 'routing-default',
|
|||
|
|
name: '默认调度策略',
|
|||
|
|
description: '演示模式默认分组,保持 Provider 优先和缓存亲和',
|
|||
|
|
enabled: true,
|
|||
|
|
is_system_default: true,
|
|||
|
|
config_json: {
|
|||
|
|
allowed_models: [],
|
|||
|
|
default_policy: {
|
|||
|
|
priority_mode: 'provider',
|
|||
|
|
scheduling_mode: 'cache_affinity',
|
|||
|
|
keep_priority_on_conversion: false,
|
|||
|
|
},
|
|||
|
|
model_policies: [
|
|||
|
|
{
|
|||
|
|
model: 'gpt-5.1',
|
|||
|
|
allowed_providers: ['provider-002'],
|
|||
|
|
allowed_keys: [],
|
|||
|
|
provider_priority_overrides: { 'provider-002': 0 },
|
|||
|
|
key_priority_overrides: {},
|
|||
|
|
pool_policy_overrides: {},
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
rules: [],
|
|||
|
|
},
|
|||
|
|
version: 1,
|
|||
|
|
created_at: mockRoutingNow - 86400,
|
|||
|
|
updated_at: mockRoutingNow - 3600,
|
|||
|
|
published_at: mockRoutingNow - 3600,
|
|||
|
|
},
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
const MOCK_ROUTING_GROUP_VERSIONS: MockRoutingGroupVersion[] = [
|
|||
|
|
{
|
|||
|
|
id: 'routing-default-v1',
|
|||
|
|
group_id: 'routing-default',
|
|||
|
|
version: 1,
|
|||
|
|
config_json: MOCK_ROUTING_GROUPS[0].config_json,
|
|||
|
|
created_at: MOCK_ROUTING_GROUPS[0].published_at ?? mockRoutingNow,
|
|||
|
|
created_by: null,
|
|||
|
|
},
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
const MOCK_ROUTING_GROUP_BINDINGS: MockRoutingGroupBinding[] = []
|
|||
|
|
|
|||
|
|
function cloneMockRoutingGroup(group: MockRoutingGroup): MockRoutingGroup {
|
|||
|
|
return JSON.parse(JSON.stringify(group)) as MockRoutingGroup
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function cloneMockRoutingVersion(version: MockRoutingGroupVersion): MockRoutingGroupVersion {
|
|||
|
|
return JSON.parse(JSON.stringify(version)) as MockRoutingGroupVersion
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function unsetOtherMockRoutingDefaults(groupId: string): void {
|
|||
|
|
for (const group of MOCK_ROUTING_GROUPS) {
|
|||
|
|
if (group.id !== groupId) {
|
|||
|
|
group.is_system_default = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
function normalizeApiFormat(apiFormat: string): string {
|
||||
|
|
return apiFormat.toLowerCase().replace(/_/g, ':')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getMockEndpointExtras(apiFormat: string) {
|
|||
|
|
const normalizedFormat = normalizeApiFormat(apiFormat)
|
|||
const extras: Record<string, unknown> = {}
|
||||
|
||||
if (normalizedFormat === 'claude:messages') {
|
||||
extras.header_rules = [
|
||||
|
|
{ action: 'set', key: 'x-app-id', value: 'demo-app' },
|
|||
|
|
{ action: 'rename', from: 'x-client-id', to: 'x-client' },
|
|||
|
|
{ action: 'drop', key: 'x-debug' }
|
|||
|
|
]
|
|||
|
|
extras.body_rules = [
|
|||
|
|
{ action: 'set', path: 'metadata.user_id', value: 'demo-user' },
|
|||
|
|
{ action: 'insert', path: 'messages', index: 0, value: { role: 'system', content: 'You are a helpful assistant.' } },
|
|||
|
|
{ action: 'regex_replace', path: 'messages[0].content', pattern: '\\s+', replacement: ' ', flags: 'm', condition: { path: 'metadata.source', op: 'eq', value: 'internal' } }
|
|||
|
|
]
|
|||
|
|
} else if (normalizedFormat === 'openai:chat') {
|
|||
|
|
extras.custom_path = '/v1/chat/completions'
|
|||
|
|
extras.header_rules = [
|
|||
|
|
{ action: 'set', key: 'x-client', value: 'demo' }
|
|||
|
|
]
|
|||
|
|
extras.format_acceptance_config = {
|
|||
|
|
enabled: true,
|
|||
accept_formats: ['openai:chat', 'claude:messages']
|
||||
}
|
||||
|
|
extras.config = { upstream_stream_policy: 'force_stream' }
|
|||
} else if (normalizedFormat === 'openai:responses') {
|
||||
extras.config = { upstream_stream_policy: 'force_non_stream' }
|
||||
} else if (normalizedFormat === 'openai:embedding') {
|
||||
|
|
extras.custom_path = '/v1/embeddings'
|
|||
|
|
extras.config = { route_kind: 'embedding' }
|
|||
|
|
} else if (normalizedFormat === 'openai:rerank' || normalizedFormat === 'jina:rerank') {
|
|||
|
|
extras.custom_path = '/v1/rerank'
|
|||
|
|
extras.config = { route_kind: 'rerank' }
|
|||
} else if (normalizedFormat === 'gemini:generate_content') {
|
||||
extras.custom_path = '/v1beta/models/gemini-3-pro-preview:generateContent'
|
||||
|
|
extras.body_rules = [
|
|||
|
|
{ action: 'drop', path: 'metadata.debug' }
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return extras
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
// Mock Endpoint Keys
|
||||
|
|
const MOCK_ENDPOINT_KEYS = [
|
|||
{ id: 'ekey-001', provider_id: 'provider-001', api_formats: ['claude:messages'], api_key_masked: 'sk-ant...abc1', auth_type: 'api_key', name: 'Primary Key', rate_multiplier: 1.0, internal_priority: 1, health_score: 0.98, consecutive_failures: 0, request_count: 5000, success_count: 4950, error_count: 50, success_rate: 0.99, avg_response_time_ms: 1200, cache_ttl_minutes: 5, max_probe_interval_minutes: 32, is_active: true, created_at: '2024-01-01T00:00:00Z', updated_at: new Date().toISOString() },
|
||||
|
|
{ id: 'ekey-002', provider_id: 'provider-001', api_formats: ['claude:messages'], api_key_masked: 'sk-ant...def2', auth_type: 'api_key', name: 'Backup Key', rate_multiplier: 1.0, internal_priority: 2, health_score: 0.95, consecutive_failures: 1, request_count: 2000, success_count: 1950, error_count: 50, success_rate: 0.975, avg_response_time_ms: 1350, cache_ttl_minutes: 5, max_probe_interval_minutes: 32, is_active: true, created_at: '2024-02-01T00:00:00Z', updated_at: new Date().toISOString() },
|
|||
{ id: 'ekey-003', provider_id: 'provider-002', api_formats: ['openai:chat'], api_key_masked: 'sk-oai...ghi3', auth_type: 'oauth', name: 'OpenAI OAuth', oauth_email: '[email protected]', oauth_expires_at: Math.floor(Date.now() / 1000) + 6 * 3600, oauth_plan_type: 'pro', oauth_account_id: 'acct-demo-002', rate_multiplier: 1.0, internal_priority: 1, health_score: 0.97, consecutive_failures: 0, request_count: 3500, success_count: 3450, error_count: 50, success_rate: 0.986, avg_response_time_ms: 900, cache_ttl_minutes: 5, max_probe_interval_minutes: 32, 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:messages', base_url: 'https://api.anthropic.com', max_retries: 2, is_active: true, total_keys: 2, active_keys: 2, created_at: '2024-01-01T00:00:00Z', updated_at: new Date().toISOString(), ...getMockEndpointExtras('claude:messages') },
|
||||
{ id: 'ep-002', provider_id: 'provider-002', provider_name: 'openai', api_format: 'openai:chat', base_url: 'https://api.openai.com', max_retries: 2, is_active: true, total_keys: 1, active_keys: 1, created_at: '2024-01-01T00:00:00Z', updated_at: new Date().toISOString(), ...getMockEndpointExtras('openai:chat') },
|
||||
{ id: 'ep-003', provider_id: 'provider-003', provider_name: 'google', api_format: 'gemini:generate_content', base_url: 'https://generativelanguage.googleapis.com', max_retries: 2, is_active: true, total_keys: 1, active_keys: 1, created_at: '2024-01-15T00:00:00Z', updated_at: new Date().toISOString(), ...getMockEndpointExtras('gemini:generate_content') }
|
||||
]
|
||||
|
|
|
|||
|
|
// 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<unknown>>> = {
|
||||
// ========== 认证相关 ==========
|
||||
|
|
'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(getCurrentProfile())
|
||||
},
|
||||
|
|
|
|||
|
|
'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/sessions': async () => {
|
||||
|
|
await delay()
|
|||
|
|
return createMockResponse([
|
|||
|
|
{
|
|||
|
|
id: 'session-current',
|
|||
|
|
device_label: 'Chrome / macOS',
|
|||
|
|
device_type: 'desktop',
|
|||
|
|
browser_name: 'Chrome',
|
|||
|
|
browser_version: '134.0',
|
|||
|
|
os_name: 'macOS',
|
|||
|
|
os_version: '15.3',
|
|||
|
|
device_model: null,
|
|||
|
|
ip_address: '192.168.1.100',
|
|||
|
|
last_seen_at: new Date().toISOString(),
|
|||
|
|
created_at: new Date(Date.now() - 2 * 24 * 3600 * 1000).toISOString(),
|
|||
|
|
is_current: true,
|
|||
|
|
revoked_at: null,
|
|||
|
|
revoke_reason: null
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 'session-other',
|
|||
|
|
device_label: 'Safari / iPhone',
|
|||
|
|
device_type: 'mobile',
|
|||
|
|
browser_name: 'Safari',
|
|||
|
|
browser_version: '18.0',
|
|||
|
|
os_name: 'iOS',
|
|||
|
|
os_version: '18.3',
|
|||
|
|
device_model: 'iPhone',
|
|||
|
|
ip_address: '10.0.0.12',
|
|||
|
|
last_seen_at: new Date(Date.now() - 3 * 3600 * 1000).toISOString(),
|
|||
|
|
created_at: new Date(Date.now() - 5 * 24 * 3600 * 1000).toISOString(),
|
|||
|
|
is_current: false,
|
|||
|
|
revoked_at: null,
|
|||
|
|
revoke_reason: null
|
|||
|
|
}
|
|||
|
|
])
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
'DELETE /api/users/me/sessions/others': async () => {
|
|||
|
|
await delay()
|
|||
|
|
return createMockResponse({ message: '其他设备已退出登录(演示模式)', revoked_count: 1 })
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
'PATCH /api/users/me/sessions/:sessionId': async (config) => {
|
|||
|
|
await delay()
|
|||
|
|
const sessionId = config.url?.split('/').pop() || 'session'
|
|||
|
|
const body = JSON.parse(config.data || '{}')
|
|||
|
|
return createMockResponse({
|
|||
|
|
id: sessionId,
|
|||
|
|
device_label: body.device_label || '已重命名设备',
|
|||
|
|
device_type: 'desktop',
|
|||
|
|
browser_name: 'Chrome',
|
|||
|
|
browser_version: '134.0',
|
|||
|
|
os_name: 'macOS',
|
|||
|
|
os_version: '15.3',
|
|||
|
|
device_model: null,
|
|||
|
|
ip_address: '192.168.1.100',
|
|||
|
|
last_seen_at: new Date().toISOString(),
|
|||
|
|
created_at: new Date(Date.now() - 2 * 24 * 3600 * 1000).toISOString(),
|
|||
|
|
is_current: sessionId === 'session-current',
|
|||
|
|
revoked_at: null,
|
|||
|
|
revoke_reason: null
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
'DELETE /api/users/me/sessions/:sessionId': 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,
|
|||
billing: {
|
||||
|
|
id: 'wallet-demo-user',
|
|||
|
|
balance: Number((100 - totalCost * 20).toFixed(2)),
|
|||
|
|
recharge_balance: Number((100 - totalCost * 20).toFixed(2)),
|
|||
|
|
gift_balance: 0,
|
|||
|
|
refundable_balance: Number((100 - totalCost * 20).toFixed(2)),
|
|||
|
|
currency: 'USD',
|
|||
|
|
status: 'active',
|
|||
|
|
limit_mode: 'finite',
|
|||
|
|
unlimited: false,
|
|||
|
|
total_recharged: 100,
|
|||
|
|
total_consumed: Number((totalCost * 20).toFixed(2)),
|
|||
|
|
total_refunded: 0,
|
|||
|
|
total_adjusted: 0,
|
|||
|
|
updated_at: new Date().toISOString(),
|
|||
|
|
},
|
|||
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,
|
|||
|
|
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/available-models': async () => {
|
||||
|
|
await delay()
|
|||
|
|
const models = MOCK_GLOBAL_MODELS.filter(model => model.is_active).map(model => ({
|
|||
|
|
id: model.id,
|
|||
|
|
name: model.name,
|
|||
|
|
display_name: model.display_name,
|
|||
|
|
is_active: model.is_active,
|
|||
|
|
default_price_per_request: model.default_price_per_request ?? null,
|
|||
|
|
default_tiered_pricing: model.default_tiered_pricing,
|
|||
|
|
supported_capabilities: model.supported_capabilities ?? null,
|
|||
|
|
supports_embedding: model.supports_embedding ?? null,
|
|||
|
|
config: model.config ?? null,
|
|||
|
|
usage_count: model.usage_count ?? 0,
|
|||
|
|
}))
|
|||
|
|
return createMockResponse({ models, total: models.length })
|
|||
|
|
},
|
|||
|
|
|
|||
'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',
|
|||
unlimited: Boolean(body.unlimited),
|
||||
is_active: true,
|
||||
|
|
allowed_providers: null,
|
|||
allowed_api_formats: 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,
|
|||
|
|
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: Routing Profiles ==========
|
||||
|
|
'GET /api/admin/routing/groups': async () => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
return createMockResponse({
|
|||
|
|
items: MOCK_ROUTING_GROUPS.map(cloneMockRoutingGroup),
|
|||
|
|
total: MOCK_ROUTING_GROUPS.length,
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
'POST /api/admin/routing/groups': async (config) => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
const body = JSON.parse(config.data || '{}') as Partial<MockRoutingGroup>
|
|||
|
|
const now = Math.floor(Date.now() / 1000)
|
|||
|
|
const group: MockRoutingGroup = {
|
|||
|
|
id: body.id || `routing-demo-${Date.now()}`,
|
|||
|
|
name: body.name || '未命名调度策略',
|
|||
|
|
description: body.description ?? null,
|
|||
|
|
enabled: body.enabled ?? true,
|
|||
|
|
is_system_default: body.is_system_default ?? false,
|
|||
|
|
config_json: body.config_json ?? {},
|
|||
|
|
version: 1,
|
|||
|
|
created_at: now,
|
|||
|
|
updated_at: now,
|
|||
|
|
published_at: null,
|
|||
|
|
}
|
|||
|
|
if (group.is_system_default) {
|
|||
|
|
unsetOtherMockRoutingDefaults(group.id)
|
|||
|
|
}
|
|||
|
|
MOCK_ROUTING_GROUPS.unshift(group)
|
|||
|
|
return createMockResponse(cloneMockRoutingGroup(group))
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
'GET /api/admin/routing/bindings': async () => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
return createMockResponse({
|
|||
|
|
items: MOCK_ROUTING_GROUP_BINDINGS.map(binding => ({ ...binding })),
|
|||
|
|
total: MOCK_ROUTING_GROUP_BINDINGS.length,
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
'POST /api/admin/routing/bindings': async (config) => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
const body = JSON.parse(config.data || '{}') as Partial<MockRoutingGroupBinding>
|
|||
|
|
const now = Math.floor(Date.now() / 1000)
|
|||
|
|
const binding: MockRoutingGroupBinding = {
|
|||
|
|
id: body.id || `routing-binding-demo-${Date.now()}`,
|
|||
|
|
group_id: body.group_id || 'routing-default',
|
|||
|
|
subject_type: body.subject_type || 'api_key',
|
|||
|
|
subject_id: body.subject_id || 'demo',
|
|||
|
|
is_default: body.is_default ?? false,
|
|||
|
|
allow_explicit_select: body.allow_explicit_select ?? false,
|
|||
|
|
created_at: now,
|
|||
|
|
updated_at: now,
|
|||
|
|
}
|
|||
|
|
MOCK_ROUTING_GROUP_BINDINGS.unshift(binding)
|
|||
|
|
return createMockResponse({ ...binding })
|
|||
|
|
},
|
|||
|
|
|
|||
// ========== 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()
|
|||
let records = getUsageRecords()
|
||||
const params = config.params || {}
|
||||
|
|
const limit = parseInt(params.limit) || 20
|
|||
|
|
const offset = parseInt(params.offset) || 0
|
|||
|
||||
// 通用搜索:用户名、密钥名、模型名、提供商名
|
||||
|
|
// 支持空格分隔的组合搜索,多个关键词之间是 AND 关系
|
|||
|
|
if (typeof params.search === 'string' && params.search.trim()) {
|
|||
|
|
const keywords = params.search.trim().toLowerCase().split(/\s+/)
|
|||
|
|
records = records.filter(r => {
|
|||
|
|
// 每个关键词都要匹配至少一个字段
|
|||
|
|
return keywords.every((keyword: string) =>
|
|||
|
|
(r.username || '').toLowerCase().includes(keyword) ||
|
|||
|
|
(r.api_key?.name || '').toLowerCase().includes(keyword) ||
|
|||
|
|
(r.model || '').toLowerCase().includes(keyword) ||
|
|||
|
|
(r.provider || '').toLowerCase().includes(keyword)
|
|||
|
|
)
|
|||
|
|
})
|
|||
}
|
||||
|
|
|
|||
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: Modules ==========
|
||||
|
|
'GET /api/admin/modules/status': async () => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
return createMockResponse(MOCK_MODULE_STATUSES)
|
|||
|
|
},
|
|||
|
|
|
|||
// ========== 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,
|
|||
supports_embedding: m.supports_embedding,
|
||||
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<unknown>>
|
||||
params: Record<string, string>
|
||||
|
|
}
|
|||
|
|
|
|||
type DynamicHandler = (config: AxiosRequestConfig, params: Record<string, string>) => Promise<AxiosResponse<unknown>>
|
||||
|
||||
|
|
// 动态路由注册表
|
|||
|
|
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<unknown>>) | 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<unknown> | 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: unknown) {
|
||||
|
|
if ((error as Record<string, unknown>)?.response) {
|
|||
throw error
|
||||
|
|
}
|
|||
// eslint-disable-next-line no-console
|
||||
console.error('[Mock] Handler error:', error)
|
||||
|
|
throw { response: createMockResponse({ detail: '模拟请求处理失败' }, 500) }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 未匹配的请求返回默认响应
|
|||
// eslint-disable-next-line no-console
|
||||
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 normalizedFormat = normalizeApiFormat(format)
|
||||
const healthDetail = provider.endpoint_health_details.find(h => h.api_format === format)
|
||||
const baseUrl = normalizedFormat.includes('claude') ? 'https://api.anthropic.com' :
|
||||
|
|
normalizedFormat.includes('openai') ? 'https://api.openai.com' :
|
|||
|
|
'https://generativelanguage.googleapis.com'
|
|||
return {
|
||||
|
|
id: `ep-${providerId}-${index + 1}`,
|
|||
|
|
provider_id: providerId,
|
|||
|
|
provider_name: provider.name,
|
|||
|
|
api_format: format,
|
|||
base_url: baseUrl,
|
||||
max_retries: 2,
|
||||
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(),
|
||||
|
|
...getMockEndpointExtras(normalizedFormat)
|
|||
}
|
||||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
// 为 provider 生成 keys(Key 归属 Provider,通过 api_formats 关联)
|
||||
const PROVIDER_KEYS_CACHE: Record<string, Record<string, unknown>[]> = {}
|
||||
function generateMockKeysForProvider(providerId: string, count: number = 2) {
|
||||
|
|
const provider = MOCK_PROVIDERS.find(p => p.id === providerId)
|
|||
|
|
const formats = provider?.api_formats || []
|
|||
const nowSec = Math.floor(Date.now() / 1000)
|
||||
|
||||
return Array.from({ length: count }, (_, i) => {
|
||||
|
|
const isOAuth = i === 1
|
|||
|
|
const markInvalid = isOAuth && providerId.endsWith('3')
|
|||
|
|
const oauthFields = isOAuth ? {
|
|||
|
|
auth_type: 'oauth',
|
|||
|
|
oauth_email: '[email protected]',
|
|||
|
|
oauth_expires_at: markInvalid ? null : nowSec + 6 * 3600,
|
|||
|
|
oauth_invalid_at: markInvalid ? nowSec - 3600 : null,
|
|||
|
|
oauth_invalid_reason: markInvalid ? '[ACCOUNT_BLOCK] Demo verification required' : null,
|
|||
status_snapshot: {
|
||||
|
|
oauth: { code: 'valid', label: '有效', reason: null, expires_at: nowSec + 6 * 3600, invalid_at: null, requires_reauth: false, expiring_soon: false },
|
|||
|
|
account: markInvalid
|
|||
|
|
? { code: 'account_verification', label: '需要验证', reason: 'Demo verification required', blocked: true, source: 'oauth_invalid', recoverable: false }
|
|||
|
|
: { code: 'ok', label: null, reason: null, blocked: false, source: null, recoverable: false },
|
|||
|
|
quota: { code: 'unknown', label: null, reason: null, exhausted: false, usage_ratio: null, updated_at: null, reset_seconds: null, plan_type: null }
|
|||
|
|
},
|
|||
oauth_plan_type: 'pro',
|
||||
|
|
oauth_account_id: `acct-${providerId}`
|
|||
|
|
} : { auth_type: 'api_key' }
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
id: `key-${providerId}-${i + 1}`,
|
|||
|
|
provider_id: providerId,
|
|||
|
|
api_formats: i === 0 ? formats : formats.slice(0, 1),
|
|||
|
|
api_key_masked: `sk-***...${Math.random().toString(36).substring(2, 6)}`,
|
|||
|
|
name: i === 0 ? 'Primary Key' : `Backup Key ${i}`,
|
|||
|
|
...oauthFields,
|
|||
|
|
rate_multiplier: 1.0,
|
|||
|
|
internal_priority: i + 1,
|
|||
|
|
health_score: 0.90 + Math.random() * 0.10,
|
|||
|
|
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,
|
|||
|
|
avg_response_time_ms: 800 + Math.floor(Math.random() * 600),
|
|||
|
|
cache_ttl_minutes: 5,
|
|||
|
|
max_probe_interval_minutes: 32,
|
|||
|
|
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 hasEmbedding = provider.api_formats.some(f => f.endsWith(':embedding'))
|
||||
|
|
const hasRerank = provider.api_formats.some(f => f.endsWith(':rerank'))
|
|||
|
||||
const models: Record<string, unknown>[] = []
|
||||
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 (hasEmbedding) {
|
||||
|
|
models.push({
|
|||
|
|
id: `pm-${providerId}-embedding-1`,
|
|||
|
|
provider_id: providerId,
|
|||
|
|
global_model_id: 'gm-010',
|
|||
|
|
provider_model_name: 'text-embedding-3-small',
|
|||
|
|
global_model_name: 'text-embedding-3-small',
|
|||
|
|
global_model_display_name: 'text-embedding-3-small',
|
|||
|
|
effective_input_price: 0.02,
|
|||
|
|
effective_output_price: 0,
|
|||
|
|
supports_embedding: true,
|
|||
|
|
effective_supports_embedding: true,
|
|||
|
|
supports_streaming: false,
|
|||
|
|
effective_supports_streaming: false,
|
|||
|
|
config: {
|
|||
|
|
embedding: true,
|
|||
|
|
model_type: 'embedding',
|
|||
|
|
api_formats: ['openai:embedding'],
|
|||
|
|
},
|
|||
|
|
effective_config: {
|
|||
|
|
embedding: true,
|
|||
|
|
model_type: 'embedding',
|
|||
|
|
api_formats: ['openai:embedding'],
|
|||
|
|
streaming: false,
|
|||
|
|
},
|
|||
|
|
is_active: true,
|
|||
|
|
is_available: true,
|
|||
|
|
created_at: provider.created_at,
|
|||
|
|
updated_at: now
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
if (hasRerank) {
|
|||
|
|
models.push({
|
|||
|
|
id: `pm-${providerId}-rerank-1`,
|
|||
|
|
provider_id: providerId,
|
|||
|
|
global_model_id: 'gm-rerank-001',
|
|||
|
|
provider_model_name: 'bge-reranker-base',
|
|||
|
|
global_model_name: 'bge-reranker-base',
|
|||
|
|
global_model_display_name: 'bge-reranker-base',
|
|||
|
|
effective_input_price: 0.05,
|
|||
|
|
effective_output_price: 0,
|
|||
|
|
supports_streaming: false,
|
|||
|
|
effective_supports_streaming: false,
|
|||
|
|
config: {
|
|||
|
|
rerank: true,
|
|||
|
|
model_type: 'rerank',
|
|||
|
|
api_formats: ['openai:rerank'],
|
|||
|
|
},
|
|||
|
|
effective_config: {
|
|||
|
|
rerank: true,
|
|||
|
|
model_type: 'rerank',
|
|||
|
|
api_formats: ['openai:rerank'],
|
|||
|
|
streaming: false,
|
|||
|
|
},
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== 注册动态路由 ==========
|
|||
|
|
|
|||
// 系统配置详情
|
||||
|
|
registerDynamicRoute('GET', '/api/admin/system/configs/:configKey', async (_config, params) => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
const key = decodeURIComponent(params.configKey)
|
|||
|
|
const entry = MOCK_SYSTEM_CONFIGS.find(item => item.key === key)
|
|||
|
|
if (!entry) {
|
|||
|
|
throw { response: createMockResponse({ detail: `配置项 '${key}' 不存在` }, 404) }
|
|||
|
|
}
|
|||
if (key === 'module.server_chan_push.send_key' || key === 'module.bark_push.device_key') {
|
||||
return createMockResponse({
|
||||
|
|
key: entry.key,
|
|||
|
|
value: null,
|
|||
|
|
description: entry.description,
|
|||
|
|
is_set: typeof entry.value === 'string' && entry.value.trim() !== '',
|
|||
|
|
})
|
|||
|
|
}
|
|||
return createMockResponse({ key: entry.key, value: entry.value, description: entry.description })
|
||||
|
|
})
|
|||
|
|
|
|||
|
|
// 系统配置更新
|
|||
|
|
registerDynamicRoute('PUT', '/api/admin/system/configs/:configKey', async (config, params) => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
const key = decodeURIComponent(params.configKey)
|
|||
|
|
const body = JSON.parse(config.data || '{}') as { value?: unknown; description?: string }
|
|||
|
|
const index = MOCK_SYSTEM_CONFIGS.findIndex(item => item.key === key)
|
|||
|
|
const entry = {
|
|||
|
|
key,
|
|||
|
|
value: body.value ?? null,
|
|||
|
|
description: body.description,
|
|||
|
|
}
|
|||
|
|
if (index === -1) {
|
|||
|
|
MOCK_SYSTEM_CONFIGS.push(entry)
|
|||
|
|
} else {
|
|||
|
|
MOCK_SYSTEM_CONFIGS[index] = {
|
|||
|
|
...MOCK_SYSTEM_CONFIGS[index],
|
|||
|
|
...entry,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return createMockResponse(entry)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 模块状态详情
|
|||
|
|
registerDynamicRoute('GET', '/api/admin/modules/status/:moduleName', async (_config, params) => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
const moduleStatus = MOCK_MODULE_STATUSES[params.moduleName]
|
|||
|
|
if (!moduleStatus) {
|
|||
|
|
throw { response: createMockResponse({ detail: '模块不存在' }, 404) }
|
|||
|
|
}
|
|||
|
|
return createMockResponse(moduleStatus)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 模块启用状态更新
|
|||
|
|
registerDynamicRoute('PUT', '/api/admin/modules/status/:moduleName/enabled', async (config, params) => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
const moduleStatus = MOCK_MODULE_STATUSES[params.moduleName]
|
|||
|
|
if (!moduleStatus) {
|
|||
|
|
throw { response: createMockResponse({ detail: '模块不存在' }, 404) }
|
|||
|
|
}
|
|||
|
|
const body = JSON.parse(config.data || '{}') as { enabled?: boolean }
|
|||
|
|
const enabled = body.enabled === true
|
|||
|
|
const updated = {
|
|||
|
|
...moduleStatus,
|
|||
|
|
enabled,
|
|||
|
|
active: moduleStatus.available && enabled && moduleStatus.config_validated,
|
|||
|
|
}
|
|||
|
|
MOCK_MODULE_STATUSES[params.moduleName] = updated
|
|||
|
|
return createMockResponse(updated)
|
|||
|
|
})
|
|||
|
|
|
|||
// 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: '删除成功(演示模式)', affected_keys_count: 0 })
|
||||
})
|
||||
|
|
|
|||
// Provider Keys 列表
|
||||
|
|
registerDynamicRoute('GET', '/api/admin/endpoints/providers/:providerId/keys', async (_config, params) => {
|
|||
await delay()
|
||||
|
|
requireAdmin()
|
|||
if (!PROVIDER_KEYS_CACHE[params.providerId]) {
|
||||
|
|
PROVIDER_KEYS_CACHE[params.providerId] = generateMockKeysForProvider(params.providerId, 2)
|
|||
|
|
}
|
|||
|
|
return createMockResponse(PROVIDER_KEYS_CACHE[params.providerId])
|
|||
})
|
||||
|
|
|
|||
// 为 Provider 创建 Key
|
||||
|
|
registerDynamicRoute('POST', '/api/admin/endpoints/providers/:providerId/keys', async (config, params) => {
|
|||
await delay()
|
||||
|
|
requireAdmin()
|
|||
|
|
const body = JSON.parse(config.data || '{}')
|
|||
const apiKeyPlain = body.api_key || 'sk-demo'
|
||||
|
|
const masked = apiKeyPlain.length >= 12
|
|||
|
|
? `${apiKeyPlain.slice(0, 8)}***${apiKeyPlain.slice(-4)}`
|
|||
|
|
: 'sk-***...demo'
|
|||
|
|
|
|||
|
|
const newKey = {
|
|||
id: `key-demo-${Date.now()}`,
|
||||
provider_id: params.providerId,
|
||||
|
|
api_formats: body.api_formats || [],
|
|||
|
|
api_key_masked: masked,
|
|||
|
|
api_key_plain: null,
|
|||
auth_type: body.auth_type || 'api_key',
|
||||
name: body.name || 'New Key',
|
||||
|
|
note: body.note,
|
|||
|
|
rate_multiplier: body.rate_multiplier ?? 1.0,
|
|||
|
|
rate_multipliers: body.rate_multipliers ?? null,
|
|||
|
|
internal_priority: body.internal_priority ?? 50,
|
|||
|
|
global_priority: body.global_priority ?? null,
|
|||
|
|
rpm_limit: body.rpm_limit ?? null,
|
|||
|
|
allowed_models: body.allowed_models ?? null,
|
|||
|
|
capabilities: body.capabilities ?? null,
|
|||
|
|
cache_ttl_minutes: body.cache_ttl_minutes ?? 5,
|
|||
|
|
max_probe_interval_minutes: body.max_probe_interval_minutes ?? 32,
|
|||
|
|
health_score: 1.0,
|
|||
|
|
consecutive_failures: 0,
|
|||
|
|
request_count: 0,
|
|||
|
|
success_count: 0,
|
|||
|
|
error_count: 0,
|
|||
|
|
success_rate: 0.0,
|
|||
|
|
avg_response_time_ms: 0.0,
|
|||
|
|
is_active: true,
|
|||
|
|
created_at: new Date().toISOString(),
|
|||
|
|
updated_at: new Date().toISOString(),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!PROVIDER_KEYS_CACHE[params.providerId]) {
|
|||
|
|
PROVIDER_KEYS_CACHE[params.providerId] = []
|
|||
|
|
}
|
|||
|
|
PROVIDER_KEYS_CACHE[params.providerId].push(newKey)
|
|||
|
|
return createMockResponse(newKey)
|
|||
})
|
||||
|
|
|
|||
registerDynamicRoute('POST', '/api/admin/endpoints/providers/:providerId/refresh-quota', async (config, params) => {
|
||||
await delay()
|
||||
|
|
requireAdmin()
|
|||
|
|
if (!PROVIDER_KEYS_CACHE[params.providerId]) {
|
|||
|
|
PROVIDER_KEYS_CACHE[params.providerId] = generateMockKeysForProvider(params.providerId, 2)
|
|||
|
|
}
|
|||
const body = JSON.parse(config.data || '{}')
|
||||
|
|
const requestedKeyIds = Array.isArray(body.key_ids)
|
|||
|
|
? new Set(body.key_ids.map((id: unknown) => String(id).trim()).filter(Boolean))
|
|||
|
|
: null
|
|||
|
|
const keys = (PROVIDER_KEYS_CACHE[params.providerId] || [])
|
|||
|
|
.filter(key => !requestedKeyIds || requestedKeyIds.has(key.id))
|
|||
const results = keys.map(key => ({
|
||||
|
|
key_id: key.id,
|
|||
|
|
key_name: key.name || key.id.slice(0, 8),
|
|||
|
|
status: 'success',
|
|||
|
|
metadata: { updated_at: new Date().toISOString() }
|
|||
|
|
}))
|
|||
|
|
return createMockResponse({
|
|||
|
|
success: results.length,
|
|||
|
|
failed: 0,
|
|||
|
|
total: results.length,
|
|||
|
|
results
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
registerDynamicRoute('POST', '/api/admin/provider-oauth/keys/:keyId/refresh', async (_config, params) => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
return createMockResponse({
|
|||
|
|
provider_type: 'codex',
|
|||
|
|
expires_at: Math.floor(Date.now() / 1000) + 6 * 3600,
|
|||
|
|
has_refresh_token: true,
|
|||
|
|
email: '[email protected]',
|
|||
|
|
key_id: params.keyId
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
registerDynamicRoute('POST', '/api/admin/provider-oauth/providers/:providerId/start', async (_config, params) => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
return createMockResponse({
|
|||
|
|
authorization_url: `https://example.com/oauth/authorize?provider=${params.providerId}`,
|
|||
|
|
redirect_uri: 'https://aether.local/oauth/callback',
|
|||
|
|
provider_type: 'codex',
|
|||
|
|
instructions: 'Open the authorization URL and paste the callback URL here.'
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
registerDynamicRoute('POST', '/api/admin/provider-oauth/providers/:providerId/complete', async (config, _params) => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
const body = JSON.parse(config.data || '{}')
|
|||
|
|
return createMockResponse({
|
|||
|
|
key_id: `key-oauth-${Date.now()}`,
|
|||
|
|
provider_type: 'codex',
|
|||
|
|
expires_at: Math.floor(Date.now() / 1000) + 24 * 3600,
|
|||
|
|
has_refresh_token: true,
|
|||
|
|
email: body.name ? `${body.name}@demo.dev` : '[email protected]'
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
registerDynamicRoute('POST', '/api/admin/provider-oauth/providers/:providerId/import-refresh-token', async (config, _params) => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
const body = JSON.parse(config.data || '{}')
|
|||
|
|
return createMockResponse({
|
|||
|
|
key_id: `key-oauth-${Date.now()}`,
|
|||
|
|
provider_type: 'codex',
|
|||
|
|
expires_at: Math.floor(Date.now() / 1000) + 24 * 3600,
|
|||
|
|
has_refresh_token: true,
|
|||
|
|
email: body.name ? `${body.name}@demo.dev` : '[email protected]'
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
registerDynamicRoute('POST', '/api/admin/provider-oauth/providers/:providerId/batch-import', async (config, _params) => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
const body = JSON.parse(config.data || '{}')
|
|||
|
|
const raw = typeof body.credentials === 'string' ? body.credentials.trim() : ''
|
|||
|
|
const lines = raw ? raw.split('\n').filter(line => line.trim() && !line.trim().startsWith('#')) : []
|
|||
|
|
const total = Math.max(Math.min(lines.length, 5), 2)
|
|||
|
|
const results = []
|
|||
|
|
for (let index = 0; index < total; index++) {
|
|||
|
|
results.push({
|
|||
|
|
index,
|
|||
|
|
status: 'success',
|
|||
|
|
key_id: `key-oauth-${Date.now()}-${index}`,
|
|||
|
|
key_name: `Imported OAuth ${index + 1}`,
|
|||
|
|
auth_method: 'oauth'
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
return createMockResponse({
|
|||
|
|
total,
|
|||
|
|
success: results.length,
|
|||
|
|
failed: 0,
|
|||
|
|
results
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
|
|||
// 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: '删除成功(演示模式)' })
|
|||
|
|
})
|
|||
|
|
|
|||
// Key Reveal
|
||||
|
|
registerDynamicRoute('GET', '/api/admin/endpoints/keys/:keyId/reveal', async (_config, _params) => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
return createMockResponse({ api_key: 'sk-demo-reveal' })
|
|||
|
|
})
|
|||
|
|
|
|||
registerDynamicRoute('GET', '/api/admin/endpoints/keys/:keyId/export', async (_config, params) => {
|
||||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
return createMockResponse({
|
|||
|
|
key_id: params.keyId,
|
|||
|
|
provider_type: 'codex',
|
|||
|
|
auth_method: 'oauth',
|
|||
|
|
refresh_token: 'rt-demo',
|
|||
|
|
email: '[email protected]',
|
|||
|
|
exported_at: new Date().toISOString()
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
registerDynamicRoute('POST', '/api/admin/endpoints/keys/:keyId/clear-oauth-invalid', async (_config, params) => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
return createMockResponse({ message: 'OAuth invalid cleared (demo)', key_id: params.keyId })
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
|
|||
// Keys grouped by format
|
||||
|
|
mockHandlers['GET /api/admin/endpoints/keys/grouped-by-format'] = async () => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
|
|||
|
|
// 确保每个 provider 都有 key 数据
|
|||
|
|
for (const provider of MOCK_PROVIDERS) {
|
|||
|
|
if (!PROVIDER_KEYS_CACHE[provider.id]) {
|
|||
|
|
PROVIDER_KEYS_CACHE[provider.id] = generateMockKeysForProvider(provider.id, 2)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
const grouped: Record<string, Record<string, unknown>[]> = {}
|
||||
for (const provider of MOCK_PROVIDERS) {
|
||||
|
|
const endpoints = generateMockEndpointsForProvider(provider.id)
|
|||
|
|
const baseUrlByFormat = Object.fromEntries(endpoints.map(e => [e.api_format, e.base_url]))
|
|||
|
|
const keys = PROVIDER_KEYS_CACHE[provider.id] || []
|
|||
|
|
for (const key of keys) {
|
|||
|
|
const formats: string[] = key.api_formats || []
|
|||
|
|
for (const fmt of formats) {
|
|||
|
|
if (!grouped[fmt]) grouped[fmt] = []
|
|||
|
|
grouped[fmt].push({
|
|||
|
|
...key,
|
|||
|
|
api_format: fmt,
|
|||
|
|
provider_name: provider.name,
|
|||
|
|
endpoint_base_url: baseUrlByFormat[fmt],
|
|||
|
|
global_priority: key.global_priority ?? null,
|
|||
|
|
circuit_breaker_open: false,
|
|||
|
|
capabilities: [],
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return createMockResponse(grouped)
|
|||
|
|
}
|
|||
|
|
|
|||
// 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 || []) as Record<string, unknown>[]).map((m: Record<string, unknown>, 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)
|
|||
|
|
})
|
|||
|
|
|
|||
registerDynamicRoute('GET', '/api/admin/routing/groups/:groupId', async (_config, params) => {
|
||||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
const group = MOCK_ROUTING_GROUPS.find(item => item.id === params.groupId)
|
|||
|
|
if (!group) {
|
|||
|
|
throw { response: createMockResponse({ detail: '调度策略不存在' }, 404) }
|
|||
|
|
}
|
|||
|
|
return createMockResponse(cloneMockRoutingGroup(group))
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
registerDynamicRoute('PATCH', '/api/admin/routing/groups/:groupId', async (config, params) => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
const index = MOCK_ROUTING_GROUPS.findIndex(item => item.id === params.groupId)
|
|||
|
|
if (index < 0) {
|
|||
|
|
throw { response: createMockResponse({ detail: '调度策略不存在' }, 404) }
|
|||
|
|
}
|
|||
|
|
const body = JSON.parse(config.data || '{}') as Partial<MockRoutingGroup>
|
|||
|
|
const current = MOCK_ROUTING_GROUPS[index]
|
|||
|
|
const now = Math.floor(Date.now() / 1000)
|
|||
|
|
const updated: MockRoutingGroup = {
|
|||
|
|
...current,
|
|||
|
|
...body,
|
|||
|
|
id: current.id,
|
|||
|
|
config_json: body.config_json ?? current.config_json,
|
|||
|
|
version: body.config_json ? current.version + 1 : (body.version ?? current.version),
|
|||
|
|
updated_at: now,
|
|||
|
|
}
|
|||
|
|
if (updated.is_system_default) {
|
|||
|
|
unsetOtherMockRoutingDefaults(updated.id)
|
|||
|
|
}
|
|||
|
|
MOCK_ROUTING_GROUPS[index] = updated
|
|||
|
|
return createMockResponse(cloneMockRoutingGroup(updated))
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
registerDynamicRoute('DELETE', '/api/admin/routing/groups/:groupId', async (_config, params) => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
const index = MOCK_ROUTING_GROUPS.findIndex(item => item.id === params.groupId)
|
|||
|
|
if (index < 0) {
|
|||
|
|
throw { response: createMockResponse({ detail: '调度策略不存在' }, 404) }
|
|||
|
|
}
|
|||
|
|
MOCK_ROUTING_GROUPS.splice(index, 1)
|
|||
|
|
return createMockResponse({ message: '删除成功(演示模式)' })
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
registerDynamicRoute('POST', '/api/admin/routing/groups/:groupId/publish', async (_config, params) => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
const group = MOCK_ROUTING_GROUPS.find(item => item.id === params.groupId)
|
|||
|
|
if (!group) {
|
|||
|
|
throw { response: createMockResponse({ detail: '调度策略不存在' }, 404) }
|
|||
|
|
}
|
|||
|
|
const now = Math.floor(Date.now() / 1000)
|
|||
|
|
group.published_at = now
|
|||
|
|
group.updated_at = now
|
|||
|
|
MOCK_ROUTING_GROUP_VERSIONS.unshift({
|
|||
|
|
id: `${group.id}-v${group.version}-${now}`,
|
|||
|
|
group_id: group.id,
|
|||
|
|
version: group.version,
|
|||
|
|
config_json: group.config_json,
|
|||
|
|
created_at: now,
|
|||
|
|
created_by: null,
|
|||
|
|
})
|
|||
|
|
return createMockResponse(cloneMockRoutingGroup(group))
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
registerDynamicRoute('GET', '/api/admin/routing/groups/:groupId/versions', async (_config, params) => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
const versions = MOCK_ROUTING_GROUP_VERSIONS
|
|||
|
|
.filter(version => version.group_id === params.groupId)
|
|||
|
|
.map(cloneMockRoutingVersion)
|
|||
|
|
return createMockResponse({ items: versions, total: versions.length })
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
registerDynamicRoute('POST', '/api/admin/routing/groups/:groupId/dry-run', async (config, params) => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
const group = MOCK_ROUTING_GROUPS.find(item => item.id === params.groupId)
|
|||
|
|
if (!group) {
|
|||
|
|
throw { response: createMockResponse({ detail: '调度策略不存在' }, 404) }
|
|||
|
|
}
|
|||
|
|
const body = JSON.parse(config.data || '{}') as {
|
|||
|
|
model?: string
|
|||
|
|
resolved_model?: string
|
|||
|
|
api_format?: string
|
|||
|
|
headers?: Record<string, string>
|
|||
|
|
body?: unknown
|
|||
|
|
}
|
|||
|
|
const model = body.model || 'gpt-5.1'
|
|||
|
|
const resolvedModel = body.resolved_model || model
|
|||
|
|
const rules = Array.isArray(group.config_json.rules)
|
|||
|
|
? group.config_json.rules as Array<{ id?: unknown; enabled?: unknown }>
|
|||
|
|
: []
|
|||
|
|
const selectedRules = rules
|
|||
|
|
.filter(rule => rule.enabled !== false && typeof rule.id === 'string')
|
|||
|
|
.map(rule => String(rule.id))
|
|||
|
|
const traceSeed = {
|
|||
|
|
group_id: group.id,
|
|||
|
|
group_version: group.version,
|
|||
|
|
selection_source: 'admin_dry_run',
|
|||
|
|
selected_rules: selectedRules,
|
|||
|
|
original_model: model,
|
|||
|
|
resolved_model: resolvedModel,
|
|||
|
|
client_api_format: body.api_format || 'openai:chat',
|
|||
|
|
global_candidates: [
|
|||
|
|
{
|
|||
|
|
candidate_kind: 'provider',
|
|||
|
|
provider_id: 'provider-002',
|
|||
|
|
endpoint_id: 'ep-002',
|
|||
|
|
model_id: resolvedModel,
|
|||
|
|
key_id: 'ekey-003',
|
|||
|
|
ranking_vector: {
|
|||
|
|
provider_priority_before: 0,
|
|||
|
|
provider_priority_after: 0,
|
|||
|
|
key_priority_before: 0,
|
|||
|
|
key_priority_after: 0,
|
|||
|
|
},
|
|||
|
|
skip_reason: null,
|
|||
|
|
selected_order: 0,
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
pool_expansion: [],
|
|||
|
|
runtime_facts: {
|
|||
|
|
scheduler_mode: 'cache_affinity',
|
|||
|
|
priority_mode: 'provider',
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
return createMockResponse({
|
|||
|
|
group: cloneMockRoutingGroup(group),
|
|||
|
|
policy: {
|
|||
|
|
selected_rules: selectedRules,
|
|||
|
|
ranking_overlay: {},
|
|||
|
|
},
|
|||
|
|
trace_seed: traceSeed,
|
|||
|
|
patch_summary: { body_paths: [], header_names: [], failed_action: null },
|
|||
|
|
mutated_body: body.body ?? { model },
|
|||
|
|
mutated_headers: body.headers ?? {},
|
|||
|
|
candidate_preview: {
|
|||
|
|
status: 'policy_only',
|
|||
|
|
ranking_overlay: {},
|
|||
|
|
note: '演示模式候选预览',
|
|||
|
|
},
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
registerDynamicRoute('PATCH', '/api/admin/routing/bindings/:bindingId', async (config, params) => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
const index = MOCK_ROUTING_GROUP_BINDINGS.findIndex(item => item.id === params.bindingId)
|
|||
|
|
if (index < 0) {
|
|||
|
|
throw { response: createMockResponse({ detail: '调度绑定不存在' }, 404) }
|
|||
|
|
}
|
|||
|
|
const body = JSON.parse(config.data || '{}') as Partial<MockRoutingGroupBinding>
|
|||
|
|
MOCK_ROUTING_GROUP_BINDINGS[index] = {
|
|||
|
|
...MOCK_ROUTING_GROUP_BINDINGS[index],
|
|||
|
|
...body,
|
|||
|
|
id: MOCK_ROUTING_GROUP_BINDINGS[index].id,
|
|||
|
|
updated_at: Math.floor(Date.now() / 1000),
|
|||
|
|
}
|
|||
|
|
return createMockResponse({ ...MOCK_ROUTING_GROUP_BINDINGS[index] })
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
registerDynamicRoute('DELETE', '/api/admin/routing/bindings/:bindingId', async (_config, params) => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
const index = MOCK_ROUTING_GROUP_BINDINGS.findIndex(item => item.id === params.bindingId)
|
|||
|
|
if (index < 0) {
|
|||
|
|
throw { response: createMockResponse({ detail: '调度绑定不存在' }, 404) }
|
|||
|
|
}
|
|||
|
|
MOCK_ROUTING_GROUP_BINDINGS.splice(index, 1)
|
|||
|
|
return createMockResponse({ message: '删除成功(演示模式)' })
|
|||
|
|
})
|
|||
|
|
|
|||
// 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()
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
registerDynamicRoute('PATCH', '/api/admin/endpoints/health/keys', async () => {
|
||||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
return createMockResponse({
|
|||
|
|
message: 'All key health recovered (demo)',
|
|||
|
|
recovered_count: 2,
|
|||
|
|
recovered_keys: [
|
|||
|
|
{ key_id: 'key-demo-1', key_name: 'Primary Key', endpoint_id: 'ep-demo-1' },
|
|||
|
|
{ key_id: 'key-demo-2', key_name: 'Backup Key', endpoint_id: 'ep-demo-2' }
|
|||
|
|
]
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
// 重置 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)
|
|||
|
|
})
|
|||
|
|
|
|||
// 管理员 - 用户会话列表
|
||||
|
|
registerDynamicRoute('GET', '/api/admin/users/:userId/sessions', async (_config, _params) => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
return createMockResponse([
|
|||
|
|
{
|
|||
|
|
id: 'admin-session-1',
|
|||
|
|
device_label: 'Chrome / macOS',
|
|||
|
|
device_type: 'desktop',
|
|||
|
|
browser_name: 'Chrome',
|
|||
|
|
browser_version: '134.0',
|
|||
|
|
os_name: 'macOS',
|
|||
|
|
os_version: '15.3',
|
|||
|
|
device_model: null,
|
|||
|
|
ip_address: '192.168.1.100',
|
|||
|
|
last_seen_at: new Date().toISOString(),
|
|||
|
|
created_at: new Date(Date.now() - 2 * 24 * 3600 * 1000).toISOString(),
|
|||
|
|
is_current: false,
|
|||
|
|
revoked_at: null,
|
|||
|
|
revoke_reason: null
|
|||
|
|
}
|
|||
|
|
])
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 管理员 - 撤销用户单个会话
|
|||
|
|
registerDynamicRoute('DELETE', '/api/admin/users/:userId/sessions/:sessionId', async (_config, _params) => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
return createMockResponse({ message: '会话已撤销(演示模式)' })
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 管理员 - 撤销用户全部会话
|
|||
|
|
registerDynamicRoute('DELETE', '/api/admin/users/:userId/sessions', async (_config, _params) => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
return createMockResponse({ message: '全部会话已撤销(演示模式)', revoked_count: 1 })
|
|||
|
|
})
|
|||
|
|
|
|||
// 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('PUT', '/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: '[email protected]' },
|
|||
|
|
{ id: 'demo-user-uuid-0002', username: 'Demo User', email: '[email protected]' },
|
|||
|
|
{ id: 'demo-user-uuid-0003', username: 'Alice Chen', email: '[email protected]' },
|
|||
|
|
{ id: 'demo-user-uuid-0004', username: 'Bob Zhang', email: '[email protected]' }
|
|||
|
|
]
|
|||
|
|
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 AI 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: 'AI 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,
|
|||
ranking: {
|
||||
|
|
mode: record.cache_read_input_tokens > 0 ? 'CacheAffinity' : 'FixedOrder',
|
|||
|
|
priority_mode: 'Provider',
|
|||
|
|
index: i,
|
|||
|
|
priority_slot: i + 1,
|
|||
|
|
demoted_by: i > 0 ? 'cross_format' : undefined
|
|||
|
|
},
|
|||
|
|
extra_data: {
|
|||
|
|
ranking_mode: record.cache_read_input_tokens > 0 ? 'CacheAffinity' : 'FixedOrder',
|
|||
|
|
priority_mode: 'Provider',
|
|||
|
|
ranking_index: i,
|
|||
|
|
priority_slot: i + 1,
|
|||
|
|
demoted_by: i > 0 ? 'cross_format' : undefined
|
|||
|
|
},
|
|||
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,
|
|||
ranking: {
|
||||
|
|
mode: record.cache_read_input_tokens > 0 ? 'CacheAffinity' : 'FixedOrder',
|
|||
|
|
priority_mode: 'Provider',
|
|||
|
|
index: skipCount,
|
|||
|
|
priority_slot: record.cache_read_input_tokens > 0 ? 7 : skipCount + 1,
|
|||
|
|
promoted_by: record.cache_read_input_tokens > 0 ? 'cached_affinity' : undefined
|
|||
|
|
},
|
|||
|
|
extra_data: {
|
|||
|
|
ranking_mode: record.cache_read_input_tokens > 0 ? 'CacheAffinity' : 'FixedOrder',
|
|||
|
|
priority_mode: 'Provider',
|
|||
|
|
ranking_index: skipCount,
|
|||
|
|
priority_slot: record.cache_read_input_tokens > 0 ? 7 : skipCount + 1,
|
|||
|
|
promoted_by: record.cache_read_input_tokens > 0 ? 'cached_affinity' : undefined
|
|||
|
|
},
|
|||
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',
|
|||
ranking: {
|
||||
|
|
mode: 'FixedOrder',
|
|||
|
|
priority_mode: 'Provider',
|
|||
|
|
index: i,
|
|||
|
|
priority_slot: i + 1
|
|||
|
|
},
|
|||
|
|
extra_data: {
|
|||
|
|
ranking_mode: 'FixedOrder',
|
|||
|
|
priority_mode: 'Provider',
|
|||
|
|
ranking_index: i,
|
|||
|
|
priority_slot: i + 1
|
|||
|
|
},
|
|||
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,
|
|||
ranking: {
|
||||
|
|
mode: 'FixedOrder',
|
|||
|
|
priority_mode: 'Provider',
|
|||
|
|
index: 0,
|
|||
|
|
priority_slot: 1
|
|||
|
|
},
|
|||
|
|
extra_data: {
|
|||
|
|
ranking_mode: 'FixedOrder',
|
|||
|
|
priority_mode: 'Provider',
|
|||
|
|
ranking_index: 0,
|
|||
|
|
priority_slot: 1
|
|||
|
|
},
|
|||
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-5-20250929',
|
||||
|
|
'claude-haiku-4-5-20251001',
|
|||
|
|
'claude-opus-4-5-20251101',
|
|||
|
|
'gpt-5.1'
|
|||
]
|
||||
|
|
|
|||
// 生成模拟的请求间隔数据
|
||||
|
|
// 间隔时间分布:大部分在 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: '[email protected]' },
|
|||
|
|
{ id: 'demo-user-uuid-0002', username: 'Demo User', email: '[email protected]' },
|
|||
|
|
{ id: 'demo-user-uuid-0003', username: 'Alice Chen', email: '[email protected]' },
|
|||
|
|
{ id: 'demo-user-uuid-0004', username: 'Bob Zhang', email: '[email protected]' }
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
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)
|
|||
|
|
}
|
|||
|
||||
|
|
// ========== Admin: Stats / Leaderboard ==========
|
|||
|
|
mockHandlers['GET /api/admin/stats/leaderboard/users'] = async () => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
return createMockResponse({
|
|||
|
|
items: [
|
|||
|
|
{ rank: 1, id: 'user-1', name: 'Demo Admin', value: 1200, requests: 1200, tokens: 240000, cost: 123.4 },
|
|||
|
|
{ rank: 2, id: 'user-2', name: 'Demo User', value: 980, requests: 980, tokens: 180000, cost: 98.7 }
|
|||
|
|
],
|
|||
|
|
total: 2,
|
|||
|
|
metric: 'requests',
|
|||
|
|
start_date: '2026-02-01',
|
|||
|
|
end_date: '2026-02-07'
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
mockHandlers['GET /api/admin/stats/leaderboard/api-keys'] = async () => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
return createMockResponse({
|
|||
|
|
items: [
|
|||
|
|
{ rank: 1, id: 'key-1', name: 'Key A', value: 800, requests: 800, tokens: 160000, cost: 76.2 },
|
|||
|
|
{ rank: 2, id: 'key-2', name: 'Key B', value: 620, requests: 620, tokens: 120000, cost: 55.1 }
|
|||
|
|
],
|
|||
|
|
total: 2,
|
|||
|
|
metric: 'requests',
|
|||
|
|
start_date: '2026-02-01',
|
|||
|
|
end_date: '2026-02-07'
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
mockHandlers['GET /api/admin/stats/leaderboard/models'] = async () => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
return createMockResponse({
|
|||
|
|
items: [
|
|||
|
|
{ rank: 1, id: 'gpt-4', name: 'gpt-4', value: 500, requests: 500, tokens: 100000, cost: 44.2 },
|
|||
|
|
{ rank: 2, id: 'claude-3', name: 'claude-3', value: 420, requests: 420, tokens: 90000, cost: 40.1 }
|
|||
|
|
],
|
|||
|
|
total: 2,
|
|||
|
|
metric: 'requests',
|
|||
|
|
start_date: '2026-02-01',
|
|||
|
|
end_date: '2026-02-07'
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
mockHandlers['GET /api/admin/stats/cost/forecast'] = async () => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
return createMockResponse({
|
|||
|
|
history: [
|
|||
|
|
{ date: '2026-02-01', total_cost: 120 },
|
|||
|
|
{ date: '2026-02-02', total_cost: 132 },
|
|||
|
|
{ date: '2026-02-03', total_cost: 140 }
|
|||
|
|
],
|
|||
|
|
forecast: [
|
|||
|
|
{ date: '2026-02-04', total_cost: 150 },
|
|||
|
|
{ date: '2026-02-05', total_cost: 158 }
|
|||
|
|
],
|
|||
|
|
slope: 5.2,
|
|||
|
|
intercept: 110.5,
|
|||
|
|
start_date: '2026-02-01',
|
|||
|
|
end_date: '2026-02-03'
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
mockHandlers['GET /api/admin/stats/cost/savings'] = async () => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
return createMockResponse({
|
|||
|
|
cache_read_tokens: 120000,
|
|||
|
|
cache_read_cost: 8.2,
|
|||
|
|
cache_creation_cost: 3.4,
|
|||
|
|
estimated_full_cost: 82.0,
|
|||
|
|
cache_savings: 73.8
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
mockHandlers['GET /api/admin/stats/providers/quota-usage'] = async () => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
return createMockResponse({
|
|||
|
|
providers: [
|
|||
|
|
{
|
|||
|
|
id: 'prov-1',
|
|||
|
|
name: 'Provider A',
|
|||
|
|
quota_usd: 500,
|
|||
|
|
used_usd: 320,
|
|||
|
|
remaining_usd: 180,
|
|||
|
|
usage_percent: 64,
|
|||
|
|
quota_expires_at: null,
|
|||
|
|
estimated_exhaust_at: new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString()
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
mockHandlers['GET /api/admin/stats/performance/percentiles'] = async () => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
return createMockResponse([
|
|||
|
|
{
|
|||
|
|
date: '2026-02-01',
|
|||
|
|
p50_response_time_ms: 320,
|
|||
|
|
p90_response_time_ms: 560,
|
|||
|
|
p99_response_time_ms: 860,
|
|||
|
|
p50_first_byte_time_ms: 120,
|
|||
|
|
p90_first_byte_time_ms: 210,
|
|||
|
|
p99_first_byte_time_ms: 400
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
date: '2026-02-02',
|
|||
|
|
p50_response_time_ms: 300,
|
|||
|
|
p90_response_time_ms: 540,
|
|||
|
|
p99_response_time_ms: 820,
|
|||
|
|
p50_first_byte_time_ms: 110,
|
|||
|
|
p90_first_byte_time_ms: 200,
|
|||
|
|
p99_first_byte_time_ms: 380
|
|||
|
|
}
|
|||
|
|
])
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
mockHandlers['GET /api/admin/stats/errors/distribution'] = async () => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
return createMockResponse({
|
|||
|
|
distribution: [
|
|||
|
|
{ category: 'rate_limit', count: 24 },
|
|||
|
|
{ category: 'server_error', count: 12 },
|
|||
|
|
{ category: 'timeout', count: 6 }
|
|||
|
|
],
|
|||
|
|
trend: [
|
|||
|
|
{ date: '2026-02-01', total: 8, categories: { rate_limit: 5, server_error: 3 } },
|
|||
|
|
{ date: '2026-02-02', total: 6, categories: { rate_limit: 4, timeout: 2 } }
|
|||
|
|
]
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
mockHandlers['GET /api/admin/monitoring/system-status'] = async () => {
|
||||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
return createMockResponse({
|
|||
|
|
timestamp: new Date().toISOString(),
|
|||
|
|
users: {
|
|||
|
|
total: 124,
|
|||
|
|
active: 111
|
|||
|
|
},
|
|||
|
|
providers: {
|
|||
|
|
total: 18,
|
|||
|
|
active: 15
|
|||
|
|
},
|
|||
|
|
api_keys: {
|
|||
|
|
total: 263,
|
|||
|
|
active: 241
|
|||
|
|
},
|
|||
|
|
today_stats: {
|
|||
|
|
requests: 12483,
|
|||
|
|
tokens: 48751234,
|
|||
|
|
cost_usd: '$182.4631'
|
|||
|
|
},
|
|||
|
|
tunnel: {
|
|||
|
|
proxy_connections: 28,
|
|||
|
|
nodes: 6,
|
|||
|
|
active_streams: 164
|
|||
|
|
},
|
|||
|
|
internal_gateway: {
|
|||
|
|
status: 'rust_native_control_plane',
|
|||
|
|
path_prefixes: ['/api/', '/v1/', '/v1beta/', '/_gateway/']
|
|||
|
|
},
|
|||
|
|
recent_errors: 9
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
mockHandlers['GET /api/admin/monitoring/resilience-status'] = async () => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
return createMockResponse({
|
|||
|
|
timestamp: new Date().toISOString(),
|
|||
|
|
health_score: 86,
|
|||
|
|
status: 'healthy',
|
|||
|
|
error_statistics: {
|
|||
|
|
total_errors: 14,
|
|||
|
|
active_keys: 24,
|
|||
|
|
degraded_keys: 3,
|
|||
|
|
unhealthy_keys: 1,
|
|||
|
|
open_circuit_breakers: 1,
|
|||
|
|
circuit_breakers: {
|
|||
|
|
'provider-key-1': {
|
|||
|
|
state: 'open',
|
|||
|
|
provider_id: 'provider-openai',
|
|||
|
|
provider_name: 'OpenAI',
|
|||
|
|
key_name: 'prod-key-a',
|
|||
|
|
health_score: 0.42,
|
|||
|
|
consecutive_failures: 4,
|
|||
|
|
last_failure_at: new Date(Date.now() - 8 * 60 * 1000).toISOString(),
|
|||
|
|
open_formats: ['openai:chat']
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
recent_errors: [
|
|||
|
|
{
|
|||
|
|
error_id: 'usage-request-1',
|
|||
|
|
error_type: 'timeout',
|
|||
|
|
operation: 'OpenAI:openai:chat',
|
|||
|
|
timestamp: new Date(Date.now() - 3 * 60 * 1000).toISOString(),
|
|||
|
|
context: {
|
|||
|
|
request_id: 'req-live-001',
|
|||
|
|
provider_id: 'provider-openai',
|
|||
|
|
provider_name: 'OpenAI',
|
|||
|
|
model: 'gpt-5',
|
|||
|
|
api_format: 'openai:chat',
|
|||
|
|
status_code: 504,
|
|||
|
|
error_message: '上游响应超时,等待首字节超过阈值'
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
error_id: 'usage-request-2',
|
|||
|
|
error_type: 'server_error',
|
|||
operation: 'Anthropic:claude:messages',
|
||||
timestamp: new Date(Date.now() - 11 * 60 * 1000).toISOString(),
|
||||
|
|
context: {
|
|||
|
|
request_id: 'req-live-002',
|
|||
|
|
provider_id: 'provider-anthropic',
|
|||
|
|
provider_name: 'Anthropic',
|
|||
|
|
model: 'claude-sonnet-4-5',
|
|||
api_format: 'claude:messages',
|
||||
status_code: 502,
|
||||
|
|
error_message: '上游返回 502 Bad Gateway'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
recommendations: [
|
|||
|
|
'以下服务熔断器已打开:OpenAI/prod-key-a',
|
|||
|
|
'建议检查最近的 timeout 与 5xx 错误峰值',
|
|||
|
|
'当前整体健康度可接受,但需要关注单 Key 退化'
|
|||
|
|
]
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
mockHandlers['GET /api/admin/monitoring/resilience/circuit-history'] = async () => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
return createMockResponse({
|
|||
|
|
count: 2,
|
|||
|
|
items: [
|
|||
|
|
{
|
|||
|
|
event: 'opened',
|
|||
|
|
key_id: 'provider-key-1',
|
|||
|
|
provider_id: 'provider-openai',
|
|||
|
|
provider_name: 'OpenAI',
|
|||
|
|
key_name: 'prod-key-a',
|
|||
|
|
api_format: 'openai:chat',
|
|||
|
|
reason: '错误率过高',
|
|||
|
|
recovery_seconds: 300,
|
|||
|
|
timestamp: new Date(Date.now() - 10 * 60 * 1000).toISOString()
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
event: 'half_open',
|
|||
|
|
key_id: 'provider-key-7',
|
|||
|
|
provider_id: 'provider-gemini',
|
|||
|
|
provider_name: 'Gemini',
|
|||
|
|
key_name: 'gemini-burst',
|
|||
api_format: 'gemini:generate_content',
|
||||
reason: '正在探测恢复',
|
||||
|
|
recovery_seconds: 120,
|
|||
|
|
timestamp: new Date(Date.now() - 22 * 60 * 1000).toISOString()
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
mockHandlers['GET /_gateway/metrics'] = async () => {
|
|||
|
|
await delay(60)
|
|||
|
|
requireAdmin()
|
|||
|
|
return createMockResponse(`# HELP aether_gateway_service_up Whether the service process is currently up.
|
|||
|
|
# TYPE aether_gateway_service_up gauge
|
|||
|
|
aether_gateway_service_up{service="aether-gateway"} 1
|
|||
|
|
# HELP aether_gateway_concurrency_in_flight Current number of in-flight operations guarded by the concurrency gate.
|
|||
|
|
# TYPE aether_gateway_concurrency_in_flight gauge
|
|||
|
|
aether_gateway_concurrency_in_flight{gate="gateway_requests"} 82
|
|||
|
|
# HELP aether_gateway_concurrency_available_permits Currently available permits for the concurrency gate.
|
|||
|
|
# TYPE aether_gateway_concurrency_available_permits gauge
|
|||
|
|
aether_gateway_concurrency_available_permits{gate="gateway_requests"} 174
|
|||
|
|
# HELP aether_gateway_concurrency_high_watermark Highest observed in-flight count for the concurrency gate.
|
|||
|
|
# TYPE aether_gateway_concurrency_high_watermark gauge
|
|||
|
|
aether_gateway_concurrency_high_watermark{gate="gateway_requests"} 121
|
|||
|
|
# HELP aether_gateway_concurrency_rejected_total Number of operations rejected by the concurrency gate.
|
|||
|
|
# TYPE aether_gateway_concurrency_rejected_total counter
|
|||
|
|
aether_gateway_concurrency_rejected_total{gate="gateway_requests"} 6
|
|||
|
|
# HELP aether_gateway_concurrency_in_flight Current number of in-flight operations guarded by the concurrency gate.
|
|||
|
|
# TYPE aether_gateway_concurrency_in_flight gauge
|
|||
|
|
aether_gateway_concurrency_in_flight{gate="gateway_requests_distributed"} 94
|
|||
|
|
# HELP aether_gateway_concurrency_available_permits Currently available permits for the concurrency gate.
|
|||
|
|
# TYPE aether_gateway_concurrency_available_permits gauge
|
|||
|
|
aether_gateway_concurrency_available_permits{gate="gateway_requests_distributed"} 418
|
|||
|
|
# HELP aether_gateway_concurrency_high_watermark Highest observed in-flight count for the concurrency gate.
|
|||
|
|
# TYPE aether_gateway_concurrency_high_watermark gauge
|
|||
|
|
aether_gateway_concurrency_high_watermark{gate="gateway_requests_distributed"} 137
|
|||
|
|
# HELP aether_gateway_concurrency_rejected_total Number of operations rejected by the concurrency gate.
|
|||
|
|
# TYPE aether_gateway_concurrency_rejected_total counter
|
|||
|
|
aether_gateway_concurrency_rejected_total{gate="gateway_requests_distributed"} 11
|
|||
|
|
# HELP aether_gateway_tunnel_proxy_connections Current number of connected proxy sockets.
|
|||
|
|
# TYPE aether_gateway_tunnel_proxy_connections gauge
|
|||
|
|
aether_gateway_tunnel_proxy_connections 28
|
|||
|
|
# HELP aether_gateway_tunnel_nodes Current number of connected logical nodes.
|
|||
|
|
# TYPE aether_gateway_tunnel_nodes gauge
|
|||
|
|
aether_gateway_tunnel_nodes 6
|
|||
|
|
# HELP aether_gateway_tunnel_active_streams Current number of active local relay streams.
|
|||
|
|
# TYPE aether_gateway_tunnel_active_streams gauge
|
|||
|
|
aether_gateway_tunnel_active_streams 164
|
|||
|
|
# HELP aether_gateway_decision_remote_total Number of requests that fell back to Python decision endpoints.
|
|||
|
|
# TYPE aether_gateway_decision_remote_total counter
|
|||
|
|
aether_gateway_decision_remote_total{route_kind="chat",reason="local_decision_miss"} 4
|
|||
|
|
aether_gateway_decision_remote_total{route_kind="responses",reason="remote_decision_miss"} 2
|
|||
|
|
# HELP aether_gateway_plan_fallback_total Number of requests that fell back to Python plan endpoints.
|
|||
|
|
# TYPE aether_gateway_plan_fallback_total counter
|
|||
|
|
aether_gateway_plan_fallback_total{route_kind="chat",reason="scheduler_decision_unsupported"} 3
|
|||
|
|
# HELP aether_gateway_control_execute_fallback_total Number of requests that fell back to Python control execution.
|
|||
|
|
# TYPE aether_gateway_control_execute_fallback_total counter
|
|||
|
|
aether_gateway_control_execute_fallback_total{route_kind="chat",reason="control_execute_emergency"} 1
|
|||
|
|
# HELP aether_gateway_remote_execute_emergency_total Number of requests that used remote emergency execution fallback.
|
|||
|
|
# TYPE aether_gateway_remote_execute_emergency_total counter
|
|||
|
|
aether_gateway_remote_execute_emergency_total{route_kind="chat",reason="control_execute_emergency"} 2
|
|||
|
|
`)
|
|||
|
|
}
|
|||
|
|
|
|||
mockHandlers['GET /api/admin/stats/comparison'] = async () => {
|
||||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
return createMockResponse({
|
|||
|
|
current: {
|
|||
|
|
total_requests: 1200,
|
|||
|
|
total_tokens: 320000,
|
|||
|
|
total_cost: 180,
|
|||
|
|
actual_total_cost: 190,
|
|||
|
|
avg_response_time_ms: 350,
|
|||
|
|
error_requests: 42
|
|||
|
|
},
|
|||
|
|
comparison: {
|
|||
|
|
total_requests: 900,
|
|||
|
|
total_tokens: 260000,
|
|||
|
|
total_cost: 150,
|
|||
|
|
actual_total_cost: 160,
|
|||
|
|
avg_response_time_ms: 370,
|
|||
|
|
error_requests: 38
|
|||
|
|
},
|
|||
|
|
change_percent: {
|
|||
|
|
total_requests: 33.3,
|
|||
|
|
total_tokens: 23.1,
|
|||
|
|
total_cost: 20.0,
|
|||
|
|
actual_total_cost: 18.8,
|
|||
|
|
avg_response_time_ms: -5.4,
|
|||
|
|
error_requests: 10.5
|
|||
|
|
},
|
|||
|
|
current_start: '2026-02-01',
|
|||
|
|
current_end: '2026-02-07',
|
|||
|
|
comparison_start: '2026-01-25',
|
|||
|
|
comparison_end: '2026-01-31'
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
mockHandlers['GET /api/admin/stats/time-series'] = async () => {
|
|||
|
|
await delay()
|
|||
|
|
requireAdmin()
|
|||
|
|
return createMockResponse([
|
|||
|
|
{ date: '2026-02-01', total_requests: 120, input_tokens: 20000, output_tokens: 30000, total_cost: 12.3 },
|
|||
|
|
{ date: '2026-02-02', total_requests: 140, input_tokens: 22000, output_tokens: 32000, total_cost: 13.8 }
|
|||
|
|
])
|
|||
|
|
}
|