feat: 新增 cancelled 状态区分客户端主动断开连接

- 新增 cancelled 请求状态,与系统失败 (failed) 区分
- 客户端主动断开连接 (HTTP 499) 不计入失败率统计
- 前后端同步更新状态枚举、UI 样式和统计逻辑
This commit is contained in:
fawney19
2026-01-20 15:40:16 +08:00
parent ffca11bc18
commit 4eeba69381
11 changed files with 211 additions and 15 deletions

View File

@@ -15,7 +15,7 @@ export interface CandidateRecord {
key_preview?: string // 密钥脱敏预览(如 sk-***abc
key_capabilities?: Record<string, boolean> | null // Key 支持的能力
required_capabilities?: Record<string, boolean> | null // 请求实际需要的能力标签
status: 'pending' | 'streaming' | 'success' | 'failed' | 'skipped'
status: 'pending' | 'streaming' | 'success' | 'failed' | 'skipped' | 'cancelled'
skip_reason?: string
is_cached: boolean
// 执行结果字段
@@ -33,7 +33,7 @@ export interface CandidateRecord {
export interface RequestTrace {
request_id: string
total_candidates: number
final_status: 'success' | 'failed' | 'streaming' | 'pending'
final_status: 'success' | 'failed' | 'streaming' | 'pending' | 'cancelled'
total_latency_ms: number
candidates: CandidateRecord[]
}
@@ -42,6 +42,7 @@ export interface ProviderStats {
total_attempts: number
success_count: number
failed_count: number
cancelled_count: number
skipped_count: number
pending_count: number
available_count: number

View File

@@ -734,6 +734,7 @@ const getStatusLabel = (status: string) => {
streaming: '传输中',
success: '成功',
failed: '失败',
cancelled: '已取消',
skipped: '跳过'
}
return labels[status] || status
@@ -747,6 +748,7 @@ const getStatusColorClass = (status: string) => {
streaming: 'status-pending',
success: 'status-success',
failed: 'status-failed',
cancelled: 'status-cancelled',
skipped: 'status-skipped'
}
return classes[status] || 'status-available'
@@ -901,6 +903,7 @@ const getStatusColorClass = (status: string) => {
/* 子节点状态颜色 */
.sub-dot.status-success { background: #22c55e; color: #22c55e; }
.sub-dot.status-failed { background: #ef4444; color: #ef4444; }
.sub-dot.status-cancelled { background: #f59e0b; color: #f59e0b; }
.sub-dot.status-pending { background: #3b82f6; color: #3b82f6; }
.sub-dot.status-skipped { background: #1f2937; color: #1f2937; }
.sub-dot.status-available { background: #d1d5db; color: #d1d5db; }
@@ -963,6 +966,7 @@ const getStatusColorClass = (status: string) => {
/* 状态颜色 - 同心圆使用 color */
.node-dot.status-success { color: #22c55e; }
.node-dot.status-failed { color: #ef4444; }
.node-dot.status-cancelled { color: #f59e0b; }
.node-dot.status-pending { color: #3b82f6; }
.node-dot.status-skipped { color: #1f2937; }
.node-dot.status-available { color: #d1d5db; }
@@ -1010,6 +1014,7 @@ const getStatusColorClass = (status: string) => {
.title-dot.status-success { background: #22c55e; }
.title-dot.status-failed { background: #ef4444; }
.title-dot.status-cancelled { background: #f59e0b; }
.title-dot.status-pending { background: #3b82f6; }
.title-dot.status-skipped { background: #1f2937; }
.title-dot.status-available { background: #d1d5db; }
@@ -1093,6 +1098,11 @@ const getStatusColorClass = (status: string) => {
color: #dc2626;
}
.status-tag.status-cancelled {
background: #f59e0b20;
color: #d97706;
}
.status-tag.status-pending {
background: #3b82f620;
color: #2563eb;

View File

@@ -54,7 +54,7 @@ export interface ApiFormatStatsItem {
// 请求记录
// 请求状态类型
export type RequestStatus = 'pending' | 'streaming' | 'completed' | 'failed'
export type RequestStatus = 'pending' | 'streaming' | 'completed' | 'failed' | 'cancelled'
export interface UsageRecord {
id: string
@@ -104,7 +104,7 @@ export interface DateRangeParams {
export type PeriodValue = 'today' | 'yesterday' | 'last7days' | 'last30days' | 'last90days'
// 筛选状态(包含新的请求状态值)
export type FilterStatusValue = '__all__' | 'stream' | 'standard' | 'error' | 'active' | 'pending' | 'streaming' | 'completed' | 'failed'
export type FilterStatusValue = '__all__' | 'stream' | 'standard' | 'error' | 'active' | 'pending' | 'streaming' | 'completed' | 'failed' | 'cancelled'
// 默认统计状态
export function createDefaultStats(): UsageStatsState {

View File

@@ -1219,8 +1219,8 @@ function generateMockEndpointsForProvider(providerId: string) {
provider_name: provider.name,
api_format: format,
base_url: format.includes('CLAUDE') ? 'https://api.anthropic.com' :
format.includes('OPENAI') ? 'https://api.openai.com' :
'https://generativelanguage.googleapis.com',
format.includes('OPENAI') ? 'https://api.openai.com' :
'https://generativelanguage.googleapis.com',
max_retries: 2,
is_active: healthDetail?.is_active ?? true,
total_keys: Math.ceil(Math.random() * 3) + 1,