feat(usage): enrich audit metadata and detail views

This commit is contained in:
elky
2026-07-17 19:20:16 +08:00
parent 0be380243b
commit 664c063a06
79 changed files with 6968 additions and 1294 deletions
+345 -11
View File
@@ -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(),