mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 08:27:46 +08:00
feat(usage): enrich audit metadata and detail views
This commit is contained in:
+345
-11
@@ -705,6 +705,77 @@ function getActivityHeatmap() {
|
||||
return cachedHeatmap
|
||||
}
|
||||
|
||||
const MOCK_CYBER_POLICY_USAGE_ID = 'usage-cyber-risk-demo'
|
||||
const MOCK_CYBER_POLICY_ERROR_MESSAGE = 'This content was flagged for possible cybersecurity risk. If this seems wrong, try rephrasing your request. To get authorized for security work, join the Trusted Access for Cyber program: https://chatgpt.com/cyber'
|
||||
const MOCK_CYBER_POLICY_ERROR_BODY = {
|
||||
error: {
|
||||
type: 'invalid_request',
|
||||
message: MOCK_CYBER_POLICY_ERROR_MESSAGE,
|
||||
code: 400
|
||||
}
|
||||
}
|
||||
|
||||
interface MockManagedUserApiKey {
|
||||
id: string
|
||||
fullKey: string
|
||||
key_display: string
|
||||
name: string
|
||||
created_at: string
|
||||
last_used_at?: string
|
||||
is_active: boolean
|
||||
is_locked: boolean
|
||||
is_standalone: false
|
||||
feature_settings?: Record<string, unknown> | null
|
||||
rate_limit?: number | null
|
||||
concurrent_limit?: number | null
|
||||
ip_rules?: string[] | null
|
||||
total_requests: number
|
||||
total_cost_usd: number
|
||||
force_capabilities?: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
const mockManagedUserApiKeysByUserId = new Map<string, MockManagedUserApiKey[]>([
|
||||
[MOCK_NORMAL_USER.id ?? '', MOCK_USER_API_KEYS.map((key, index) => ({
|
||||
...key,
|
||||
fullKey: `sk-ae-demo-user-${index + 1}`,
|
||||
is_locked: false,
|
||||
is_standalone: false as const,
|
||||
}))],
|
||||
])
|
||||
let mockManagedUserApiKeySequence = 0
|
||||
|
||||
function mockManagedUserApiKeys(userId: string): MockManagedUserApiKey[] {
|
||||
if (!MOCK_ALL_USERS.some(user => user.id === userId)) {
|
||||
throw { response: createMockResponse({ detail: '用户不存在' }, 404) }
|
||||
}
|
||||
let keys = mockManagedUserApiKeysByUserId.get(userId)
|
||||
if (!keys) {
|
||||
keys = []
|
||||
mockManagedUserApiKeysByUserId.set(userId, keys)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
function publicMockManagedUserApiKey(key: MockManagedUserApiKey) {
|
||||
const { fullKey: _fullKey, ...publicKey } = key
|
||||
void _fullKey
|
||||
return publicKey
|
||||
}
|
||||
|
||||
function mockRequestObject(config: AxiosRequestConfig): Record<string, unknown> {
|
||||
if (typeof config.data === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(config.data)
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
return config.data && typeof config.data === 'object' && !Array.isArray(config.data)
|
||||
? config.data as Record<string, unknown>
|
||||
: {}
|
||||
}
|
||||
|
||||
// 生成更真实的使用记录
|
||||
function generateMockUsageRecords(count: number = 100) {
|
||||
const records = []
|
||||
@@ -798,6 +869,47 @@ function generateMockUsageRecords(count: number = 100) {
|
||||
})
|
||||
}
|
||||
|
||||
// 固定在首屏的失败记录,用于预览候选链路中的实际上游错误响应。
|
||||
records.unshift({
|
||||
id: MOCK_CYBER_POLICY_USAGE_ID,
|
||||
user_id: 'demo-admin-uuid-0001',
|
||||
username: 'Demo Admin',
|
||||
user_email: '[email protected]',
|
||||
api_key: {
|
||||
id: 'key-demo-cyber-risk',
|
||||
name: 'OpenAI Cyber Risk Demo',
|
||||
display: 'sk-ae...demo'
|
||||
},
|
||||
provider: 'openai',
|
||||
api_key_name: 'openai-cyber-risk-demo',
|
||||
rate_multiplier: 1.0,
|
||||
model: 'gpt-5',
|
||||
target_model: 'gpt-5.1',
|
||||
requested_reasoning_effort: 'xhigh',
|
||||
reasoning_effort: 'max',
|
||||
service_tier: 'priority',
|
||||
// Deliberately conflicts with the final provider request. UI and billing
|
||||
// must use the request-side `service_tier`, never this response fact.
|
||||
actual_service_tier: 'default',
|
||||
api_format: 'openai:responses',
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
total_tokens: 0,
|
||||
cost: 0,
|
||||
actual_cost: 0,
|
||||
response_time_ms: 428,
|
||||
is_stream: true,
|
||||
status_code: 400,
|
||||
error_message: MOCK_CYBER_POLICY_ERROR_MESSAGE,
|
||||
status: 'failed',
|
||||
created_at: new Date(now).toISOString(),
|
||||
updated_at: new Date(now).toISOString(),
|
||||
has_fallback: false,
|
||||
model_version: undefined
|
||||
})
|
||||
|
||||
return records
|
||||
}
|
||||
|
||||
@@ -1330,6 +1442,15 @@ const mockHandlers: Record<string, (config: AxiosRequestConfig) => Promise<Axios
|
||||
return createMockResponse(MOCK_ALL_USERS)
|
||||
},
|
||||
|
||||
'GET /api/admin/user-groups': async () => {
|
||||
await delay()
|
||||
requireAdmin()
|
||||
return createMockResponse({
|
||||
items: [],
|
||||
default_group_id: null,
|
||||
})
|
||||
},
|
||||
|
||||
'POST /api/admin/users': async (config) => {
|
||||
await delay()
|
||||
requireAdmin()
|
||||
@@ -1378,7 +1499,12 @@ const mockHandlers: Record<string, (config: AxiosRequestConfig) => Promise<Axios
|
||||
'GET /api/admin/providers/summary': async () => {
|
||||
await delay()
|
||||
requireAdmin()
|
||||
return createMockResponse(MOCK_PROVIDERS)
|
||||
return createMockResponse({
|
||||
total: MOCK_PROVIDERS.length,
|
||||
page: 1,
|
||||
page_size: MOCK_PROVIDERS.length,
|
||||
items: MOCK_PROVIDERS,
|
||||
})
|
||||
},
|
||||
|
||||
'GET /api/admin/providers': async () => {
|
||||
@@ -2411,6 +2537,26 @@ registerDynamicRoute('GET', '/api/admin/providers/:providerId/summary', async (_
|
||||
return createMockResponse(provider)
|
||||
})
|
||||
|
||||
// Provider 模型映射预览
|
||||
registerDynamicRoute('GET', '/api/admin/providers/:providerId/mapping-preview', 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_id: provider.id,
|
||||
provider_name: provider.name,
|
||||
keys: [],
|
||||
total_keys: 0,
|
||||
total_matches: 0,
|
||||
truncated: false,
|
||||
truncated_keys: 0,
|
||||
truncated_models: 0,
|
||||
})
|
||||
})
|
||||
|
||||
// Provider 更新
|
||||
registerDynamicRoute('PATCH', '/api/admin/providers/:providerId', async (config, params) => {
|
||||
await delay()
|
||||
@@ -2486,13 +2632,37 @@ registerDynamicRoute('DELETE', '/api/admin/endpoints/:endpointId', async (_confi
|
||||
})
|
||||
|
||||
// Provider Keys 列表
|
||||
registerDynamicRoute('GET', '/api/admin/endpoints/providers/:providerId/keys', async (_config, params) => {
|
||||
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])
|
||||
const keys = PROVIDER_KEYS_CACHE[params.providerId]
|
||||
const query = config.params || {}
|
||||
|
||||
// 当前详情抽屉使用 page/page_size 分页;其他调用仍使用 skip/limit 并期望裸数组。
|
||||
if (query.page !== undefined || query.page_size !== undefined) {
|
||||
const rawPage = Number(query.page)
|
||||
const rawPageSize = Number(query.page_size)
|
||||
const page = Number.isFinite(rawPage) && rawPage >= 1 ? Math.floor(rawPage) : 1
|
||||
const pageSize = Number.isFinite(rawPageSize) && rawPageSize >= 1
|
||||
? Math.floor(rawPageSize)
|
||||
: 20
|
||||
const start = (page - 1) * pageSize
|
||||
return createMockResponse({
|
||||
total: keys.length,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
keys: keys.slice(start, start + pageSize),
|
||||
})
|
||||
}
|
||||
|
||||
const rawSkip = Number(query.skip)
|
||||
const rawLimit = Number(query.limit)
|
||||
const skip = Number.isFinite(rawSkip) && rawSkip >= 0 ? Math.floor(rawSkip) : 0
|
||||
const limit = Number.isFinite(rawLimit) && rawLimit >= 1 ? Math.floor(rawLimit) : keys.length
|
||||
return createMockResponse(keys.slice(skip, skip + limit))
|
||||
})
|
||||
|
||||
// 为 Provider 创建 Key
|
||||
@@ -3206,10 +3376,122 @@ registerDynamicRoute('DELETE', '/api/admin/users/:userId', async (_config, param
|
||||
})
|
||||
|
||||
// 用户 API Keys
|
||||
registerDynamicRoute('GET', '/api/admin/users/:userId/api-keys', async (_config, _params) => {
|
||||
registerDynamicRoute('GET', '/api/admin/users/:userId/api-keys', async (_config, params) => {
|
||||
await delay()
|
||||
requireAdmin()
|
||||
return createMockResponse(MOCK_USER_API_KEYS)
|
||||
const apiKeys = mockManagedUserApiKeys(params.userId).map(publicMockManagedUserApiKey)
|
||||
return createMockResponse({
|
||||
api_keys: apiKeys,
|
||||
total: apiKeys.length,
|
||||
})
|
||||
})
|
||||
|
||||
registerDynamicRoute('POST', '/api/admin/users/:userId/api-keys', async (config, params) => {
|
||||
await delay()
|
||||
requireAdmin()
|
||||
const keys = mockManagedUserApiKeys(params.userId)
|
||||
const body = mockRequestObject(config)
|
||||
const sequence = ++mockManagedUserApiKeySequence
|
||||
const fullKey = `sk-ae-demo-${params.userId.slice(0, 8)}-${sequence}`
|
||||
const key: MockManagedUserApiKey = {
|
||||
id: `managed-key-${params.userId}-${sequence}`,
|
||||
fullKey,
|
||||
key_display: `${fullKey.slice(0, 10)}...${fullKey.slice(-4)}`,
|
||||
name: typeof body.name === 'string' && body.name.trim()
|
||||
? body.name.trim()
|
||||
: `Key-${sequence}`,
|
||||
created_at: new Date().toISOString(),
|
||||
is_active: true,
|
||||
is_locked: false,
|
||||
is_standalone: false,
|
||||
feature_settings: body.feature_settings && typeof body.feature_settings === 'object'
|
||||
? body.feature_settings as Record<string, unknown>
|
||||
: null,
|
||||
rate_limit: typeof body.rate_limit === 'number' ? body.rate_limit : 0,
|
||||
concurrent_limit: typeof body.concurrent_limit === 'number'
|
||||
? body.concurrent_limit
|
||||
: null,
|
||||
ip_rules: Array.isArray(body.ip_rules)
|
||||
? body.ip_rules.filter((value): value is string => typeof value === 'string')
|
||||
: null,
|
||||
total_requests: 0,
|
||||
total_cost_usd: 0,
|
||||
force_capabilities: null,
|
||||
}
|
||||
keys.unshift(key)
|
||||
return createMockResponse({
|
||||
...publicMockManagedUserApiKey(key),
|
||||
key: fullKey,
|
||||
message: 'API Key创建成功,请妥善保存完整密钥',
|
||||
})
|
||||
})
|
||||
|
||||
registerDynamicRoute('PUT', '/api/admin/users/:userId/api-keys/:keyId', async (config, params) => {
|
||||
await delay()
|
||||
requireAdmin()
|
||||
const keys = mockManagedUserApiKeys(params.userId)
|
||||
const index = keys.findIndex(key => key.id === params.keyId)
|
||||
if (index < 0) {
|
||||
throw { response: createMockResponse({ detail: 'API Key 不存在' }, 404) }
|
||||
}
|
||||
const body = mockRequestObject(config)
|
||||
const existing = keys[index]
|
||||
const updated: MockManagedUserApiKey = {
|
||||
...existing,
|
||||
...(typeof body.name === 'string' ? { name: body.name.trim() } : {}),
|
||||
...(typeof body.rate_limit === 'number' ? { rate_limit: body.rate_limit } : {}),
|
||||
...(typeof body.concurrent_limit === 'number' || body.concurrent_limit === null
|
||||
? { concurrent_limit: body.concurrent_limit }
|
||||
: {}),
|
||||
...(Array.isArray(body.ip_rules) || body.ip_rules === null
|
||||
? { ip_rules: body.ip_rules as string[] | null }
|
||||
: {}),
|
||||
...('feature_settings' in body
|
||||
? { feature_settings: body.feature_settings as Record<string, unknown> | null }
|
||||
: {}),
|
||||
}
|
||||
keys[index] = updated
|
||||
return createMockResponse({
|
||||
...publicMockManagedUserApiKey(updated),
|
||||
message: 'API Key更新成功',
|
||||
})
|
||||
})
|
||||
|
||||
registerDynamicRoute('DELETE', '/api/admin/users/:userId/api-keys/:keyId', async (_config, params) => {
|
||||
await delay()
|
||||
requireAdmin()
|
||||
const keys = mockManagedUserApiKeys(params.userId)
|
||||
const index = keys.findIndex(key => key.id === params.keyId)
|
||||
if (index < 0) {
|
||||
throw { response: createMockResponse({ detail: 'API Key 不存在' }, 404) }
|
||||
}
|
||||
keys.splice(index, 1)
|
||||
return createMockResponse({ message: 'API Key删除成功' })
|
||||
})
|
||||
|
||||
registerDynamicRoute('PATCH', '/api/admin/users/:userId/api-keys/:keyId/lock', async (_config, params) => {
|
||||
await delay()
|
||||
requireAdmin()
|
||||
const key = mockManagedUserApiKeys(params.userId).find(key => key.id === params.keyId)
|
||||
if (!key) {
|
||||
throw { response: createMockResponse({ detail: 'API Key 不存在' }, 404) }
|
||||
}
|
||||
key.is_locked = !key.is_locked
|
||||
return createMockResponse({
|
||||
id: key.id,
|
||||
is_locked: key.is_locked,
|
||||
message: key.is_locked ? 'API Key已锁定' : 'API Key已解锁',
|
||||
})
|
||||
})
|
||||
|
||||
registerDynamicRoute('GET', '/api/admin/users/:userId/api-keys/:keyId/full-key', async (_config, params) => {
|
||||
await delay()
|
||||
requireAdmin()
|
||||
const key = mockManagedUserApiKeys(params.userId).find(key => key.id === params.keyId)
|
||||
if (!key) {
|
||||
throw { response: createMockResponse({ detail: 'API Key 不存在' }, 404) }
|
||||
}
|
||||
return createMockResponse({ key: key.fullKey })
|
||||
})
|
||||
|
||||
// 管理员 - 用户会话列表
|
||||
@@ -3295,10 +3577,13 @@ registerDynamicRoute('DELETE', '/api/users/me/api-keys/:keyId', async (_config,
|
||||
})
|
||||
|
||||
// 使用记录详情 - /api/admin/usage/:requestId
|
||||
registerDynamicRoute('GET', '/api/admin/usage/:requestId', async (_config, params) => {
|
||||
registerDynamicRoute('GET', '/api/admin/usage/:requestId', async (config, params) => {
|
||||
await delay()
|
||||
requireAdmin()
|
||||
|
||||
const includeBodies = config.params?.include_bodies !== false
|
||||
&& config.params?.include_bodies !== 'false'
|
||||
|
||||
const records = getUsageRecords()
|
||||
const record = records.find(r => r.id === params.requestId)
|
||||
|
||||
@@ -3318,6 +3603,9 @@ registerDynamicRoute('GET', '/api/admin/usage/:requestId', async (_config, param
|
||||
// 生成模拟的请求/响应数据
|
||||
const mockRequestBody = {
|
||||
model: record.model,
|
||||
...(record.requested_reasoning_effort
|
||||
? { reasoning: { effort: record.requested_reasoning_effort } }
|
||||
: {}),
|
||||
max_tokens: 4096,
|
||||
messages: [
|
||||
{
|
||||
@@ -3328,7 +3616,27 @@ registerDynamicRoute('GET', '/api/admin/usage/:requestId', async (_config, param
|
||||
stream: record.is_stream
|
||||
}
|
||||
|
||||
const mockResponseBody = record.status === 'failed' ? {
|
||||
const mockProviderRequestBody = record.id === MOCK_CYBER_POLICY_USAGE_ID
|
||||
? {
|
||||
model: record.target_model || record.model,
|
||||
reasoning: { effort: record.reasoning_effort },
|
||||
service_tier: record.service_tier,
|
||||
input: [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Help me with an authorized cybersecurity research task.'
|
||||
}
|
||||
],
|
||||
stream: record.is_stream
|
||||
}
|
||||
: {
|
||||
...mockRequestBody,
|
||||
model: record.target_model || record.model
|
||||
}
|
||||
|
||||
const mockResponseBody = record.id === MOCK_CYBER_POLICY_USAGE_ID
|
||||
? MOCK_CYBER_POLICY_ERROR_BODY
|
||||
: record.status === 'failed' ? {
|
||||
error: {
|
||||
type: 'api_error',
|
||||
message: record.error_message || 'An error occurred'
|
||||
@@ -3376,6 +3684,10 @@ registerDynamicRoute('GET', '/api/admin/usage/:requestId', async (_config, param
|
||||
api_format: record.api_format,
|
||||
model: record.model,
|
||||
target_model: record.target_model,
|
||||
requested_reasoning_effort: record.requested_reasoning_effort,
|
||||
reasoning_effort: record.reasoning_effort,
|
||||
service_tier: record.service_tier,
|
||||
actual_service_tier: record.actual_service_tier,
|
||||
tokens: {
|
||||
input: record.input_tokens,
|
||||
output: record.output_tokens,
|
||||
@@ -3406,6 +3718,7 @@ registerDynamicRoute('GET', '/api/admin/usage/:requestId', async (_config, param
|
||||
error_message: record.error_message,
|
||||
response_time_ms: record.response_time_ms,
|
||||
created_at: record.created_at,
|
||||
updated_at: record.updated_at ?? record.created_at,
|
||||
request_headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer sk-aether-***',
|
||||
@@ -3414,7 +3727,12 @@ registerDynamicRoute('GET', '/api/admin/usage/:requestId', async (_config, param
|
||||
'Accept': 'application/json',
|
||||
'X-Request-ID': `req_${record.id}`
|
||||
},
|
||||
request_body: mockRequestBody,
|
||||
has_request_body: true,
|
||||
has_provider_request_body: true,
|
||||
has_response_body: true,
|
||||
has_client_response_body: false,
|
||||
request_body: includeBodies ? mockRequestBody : null,
|
||||
provider_request_body: includeBodies ? mockProviderRequestBody : null,
|
||||
provider_request_headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer sk-${record.provider}-***`,
|
||||
@@ -3428,7 +3746,9 @@ registerDynamicRoute('GET', '/api/admin/usage/:requestId', async (_config, param
|
||||
'X-RateLimit-Remaining': '999',
|
||||
'X-RateLimit-Reset': new Date(Date.now() + 60000).toISOString()
|
||||
},
|
||||
response_body: mockResponseBody,
|
||||
response_body: includeBodies ? mockResponseBody : null,
|
||||
client_response_body: null,
|
||||
body_load_errors: null,
|
||||
metadata: {
|
||||
client_ip: '192.168.1.100',
|
||||
user_agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
|
||||
@@ -3576,7 +3896,8 @@ registerDynamicRoute('GET', '/api/admin/monitoring/trace/:requestId', async (_co
|
||||
})
|
||||
} else if (record.status === 'failed') {
|
||||
// 失败请求:多个候选都失败
|
||||
const attemptCount = 2 + Math.floor(Math.random() * 2)
|
||||
const isCyberPolicyDemo = record.id === MOCK_CYBER_POLICY_USAGE_ID
|
||||
const attemptCount = isCyberPolicyDemo ? 1 : 2 + Math.floor(Math.random() * 2)
|
||||
|
||||
for (let i = 0; i < attemptCount; i++) {
|
||||
const attemptStarted = new Date(now.getTime() + i * 200)
|
||||
@@ -3611,7 +3932,20 @@ registerDynamicRoute('GET', '/api/admin/monitoring/trace/:requestId', async (_co
|
||||
ranking_mode: 'FixedOrder',
|
||||
priority_mode: 'Provider',
|
||||
ranking_index: i,
|
||||
priority_slot: i + 1
|
||||
priority_slot: i + 1,
|
||||
...(isCyberPolicyDemo ? {
|
||||
upstream_response: {
|
||||
source: 'upstream_response',
|
||||
status_code: 400,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'x-request-id': `req_${MOCK_CYBER_POLICY_USAGE_ID}`
|
||||
},
|
||||
body: MOCK_CYBER_POLICY_ERROR_BODY,
|
||||
body_ref: `usage://request/req_${MOCK_CYBER_POLICY_USAGE_ID}/response_body`,
|
||||
body_state: 'reference'
|
||||
}
|
||||
} : {})
|
||||
},
|
||||
latency_ms: attemptLatency,
|
||||
created_at: attemptStarted.toISOString(),
|
||||
|
||||
Reference in New Issue
Block a user