fix(runner): record failed task attempts

This commit is contained in:
2026-05-17 20:50:20 +08:00
parent ae197a742f
commit 90c3315468
4 changed files with 287 additions and 29 deletions
+55 -21
View File
@@ -971,28 +971,33 @@ function TaskAttemptPopoverContent(props: { task: GatewayTask }) {
const attempts = props.task.attempts ?? [];
return (
<span className="taskRecordAttemptPopover" role="tooltip">
{attempts.map((attempt) => (
<span
key={attempt.id || `${props.task.id}-${attempt.attemptNo}`}
className={`taskRecordAttemptDetail ${attempt.status === 'failed' ? 'failed' : attempt.status === 'succeeded' ? 'succeeded' : ''}`}
>
<span className="taskRecordAttemptDetailHeader">
<strong>#{attempt.attemptNo} {taskAttemptTarget(attempt)}</strong>
<Badge variant={attempt.status === 'succeeded' ? 'success' : attempt.status === 'failed' ? 'destructive' : 'secondary'}>{taskAttemptStatusText(attempt.status)}</Badge>
</span>
<small>{taskAttemptMeta(attempt)}</small>
{attempt.status === 'failed' && <span className="taskRecordAttemptError">{taskAttemptFailureReason(attempt)}</span>}
{taskAttemptTrace(attempt).length > 0 && (
<span className="taskRecordAttemptTrace">
{taskAttemptTrace(attempt).map((entry, index) => (
<span key={`${attempt.id || attempt.attemptNo}-trace-${index}`} className="taskRecordAttemptTraceItem">
{taskAttemptTraceText(entry)}
</span>
))}
{attempts.map((attempt) => {
const trace = taskAttemptTrace(attempt);
const rateLimitText = taskAttemptRateLimitText(attempt);
return (
<span
key={attempt.id || `${props.task.id}-${attempt.attemptNo}`}
className={`taskRecordAttemptDetail ${attempt.status === 'failed' ? 'failed' : attempt.status === 'succeeded' ? 'succeeded' : ''}`}
>
<span className="taskRecordAttemptDetailHeader">
<strong>#{attempt.attemptNo} {taskAttemptTarget(attempt)}</strong>
<Badge variant={attempt.status === 'succeeded' ? 'success' : attempt.status === 'failed' ? 'destructive' : 'secondary'}>{taskAttemptStatusText(attempt.status)}</Badge>
</span>
)}
</span>
))}
<small>{taskAttemptMeta(attempt)}</small>
{attempt.status === 'failed' && <span className="taskRecordAttemptError">{taskAttemptFailureReason(attempt)}</span>}
{(rateLimitText || trace.length > 0) && (
<span className="taskRecordAttemptTrace">
{rateLimitText && <span className="taskRecordAttemptTraceItem">{rateLimitText}</span>}
{trace.map((entry, index) => (
<span key={`${attempt.id || attempt.attemptNo}-trace-${index}`} className="taskRecordAttemptTraceItem">
{taskAttemptTraceText(entry)}
</span>
))}
</span>
)}
</span>
);
})}
</span>
);
}
@@ -1055,6 +1060,29 @@ function taskAttemptTrace(attempt: NonNullable<GatewayTask['attempts']>[number])
return raw.filter((item): item is Record<string, unknown> => Boolean(item) && typeof item === 'object' && !Array.isArray(item));
}
function taskAttemptRateLimitText(attempt: NonNullable<GatewayTask['attempts']>[number]) {
const detail = metadataObject(attempt.metrics, 'rateLimit');
if (!Object.keys(detail).length) return '';
const scopeName = objectString(detail, 'scopeName') || objectString(detail, 'scopeKey') || '限流对象';
const metric = objectString(detail, 'metric') || 'rate_limit';
const current = metadataNumber(detail, 'current');
const amount = metadataNumber(detail, 'amount');
const projected = metadataNumber(detail, 'projected');
const limit = metadataNumber(detail, 'limit');
const windowSeconds = metadataNumber(detail, 'windowSeconds');
const retryAfterMs = metadataNumber(detail, 'retryAfterMs');
const values = [
`限流 ${scopeName} · ${metric}`,
current !== null ? `当前 ${formatCellValue(current)}` : '',
amount !== null ? `本次 ${formatCellValue(amount)}` : '',
projected !== null ? `预计 ${formatCellValue(projected)}` : '',
limit !== null ? `限制 ${formatCellValue(limit)}` : '',
windowSeconds !== null ? `窗口 ${Math.trunc(windowSeconds)}` : '',
retryAfterMs !== null ? `${formatDuration(Math.trunc(retryAfterMs))} 后可重试` : '',
].filter(Boolean);
return values.join(' · ');
}
function taskAttemptTraceText(entry: Record<string, unknown>) {
const event = objectString(entry, 'event');
const action = objectString(entry, 'action');
@@ -1116,6 +1144,12 @@ function taskAttemptTraceReasonLabel(reason: string) {
client_retryable: '客户端标记可重试',
client_non_retryable: '客户端标记不可重试',
same_client_max_attempts: '达到本平台最大尝试次数',
request_validation_failed: '请求校验失败',
candidate_selection_failed: '候选模型选择失败',
parameter_preprocessing_failed: '参数预处理失败',
wallet_balance_check_failed: '余额校验失败',
local_rate_limit_blocked: '本地限流拦截',
pre_provider_failed: '调用上游前失败',
local_rate_limit_wait_queue: '本地限流排队等待',
failover_time_budget_exceeded: '超过全局切换时间预算',
runner_policy_disabled: '全局调度策略停用',