Files
Agent-System/web/src/lib/taskRunErrorInfo.ts
T
weichenandCursor 943a07b2a4 fix(web): surface real task failure reason instead of NETWORK_ERROR fallback
Show error_message for failed runs, a distinct cancelled state, and business 4xx codes instead of a generic network message.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-21 03:05:49 +00:00

139 lines
4.2 KiB
TypeScript

import { fallbackErrorClass } from '../api/chatRetry';
import type { ChatErrorInfo } from '../types/workbench';
/** GET /api/v1/tasks/runs/{id} 的最小视图。兼容修复前的 PascalCase 响应。 */
export type RemoteTaskRunView = {
status?: string;
Status?: string;
error_message?: string;
ErrorMessage?: string;
workspace_id?: string;
WorkspaceID?: string;
};
export function readRemoteTaskRun(remote: RemoteTaskRunView | null | undefined): {
status: string;
errorMessage: string;
workspaceId: string;
} {
if (!remote) {
return { status: '', errorMessage: '', workspaceId: '' };
}
return {
status: remote.status || remote.Status || '',
errorMessage: (remote.error_message || remote.ErrorMessage || '').trim(),
workspaceId: remote.workspace_id || remote.WorkspaceID || '',
};
}
const fatalDenialCopy: Record<string, string> = {
DOC_TASK_DISABLED: '该文档任务未启用,当前请求未执行',
WORKFLOW_NOT_ACTIVATED: '当前项目尚未激活工作流,文档任务不会启动',
WORKFLOW_INSTANCE_CLOSED: '工作流实例已关闭,无法执行该文档任务',
DOC_DUTY_NOT_MANAGED: '该文档未配置职责,无法执行',
SESSION_FORBIDDEN: '无权访问该会话',
};
export function buildNetworkErrorInfo(
httpStatus?: number,
opts?: { errorCode?: string; detail?: string },
): ChatErrorInfo {
const code = opts?.errorCode || '';
const detail = opts?.detail || '';
if (code === 'DOC_BUSY' || code === 'document_busy') {
return {
userMessage: detail || '该文档正在被其他用户或项目助手编辑,当前请求未创建新任务',
code: 'DOC_BUSY',
class: 'fatal',
actions: [],
};
}
if (code === 'DOC_TASK_NOT_IN_ACTIVE_STAGE') {
return {
userMessage: detail || '该文档任务不在当前活动阶段,请先完成当前阶段工作流后再试',
code,
class: 'transient',
actions: ['retry'],
};
}
if (code === 'DOC_DUTY_FORBIDDEN') {
return {
userMessage: detail || '当前账号不是该文档任务的职责人,无法执行',
code,
class: 'fatal',
actions: ['retry'],
};
}
if (fatalDenialCopy[code]) {
const retryable = code === 'WORKFLOW_NOT_ACTIVATED' || code === 'DOC_DUTY_NOT_MANAGED';
return {
userMessage: detail || fatalDenialCopy[code],
code,
class: 'fatal',
actions: retryable ? ['retry'] : [],
};
}
if (httpStatus === 401) {
return {
userMessage: '登录已失效,请重新登录后再试',
code: 'ERR_UNAUTHENTICATED',
class: 'fatal',
actions: ['retry'],
};
}
if (httpStatus === 403) {
if (code === 'ERR_FORBIDDEN' || code === 'AUTHZ') {
return {
userMessage: detail || '无权访问该项目,请确认登录账号与项目成员身份',
code: code || 'ERR_FORBIDDEN',
class: 'fatal',
actions: ['retry'],
};
}
return {
userMessage: detail || '无权执行该操作,请确认账号权限与工作流阶段',
code: code || 'ERR_FORBIDDEN',
class: 'fatal',
actions: ['retry'],
};
}
if (httpStatus !== undefined && httpStatus >= 400 && httpStatus < 500) {
const shown = code || `HTTP_${httpStatus}`;
return {
userMessage: detail || `请求失败(${shown}),请重试或联系管理员`,
code: shown,
class: 'fatal',
actions: ['retry'],
};
}
return {
userMessage: '网络连接异常,请检查网络后重试',
code: 'NETWORK_ERROR',
class: fallbackErrorClass({ network: true }),
actions: ['retry'],
};
}
/** 服务端已判定 failed:有 error_message 时展示真实原因,否则才回落到网络错误。 */
export function buildFailedTaskErrorInfo(errorMessage: string): ChatErrorInfo {
const message = errorMessage.trim();
if (!message) {
return buildNetworkErrorInfo();
}
return {
userMessage: message,
code: 'TASK_FAILED',
class: 'fatal',
actions: ['retry', 'restart'],
};
}
export function buildCancelledTaskErrorInfo(): ChatErrorInfo {
return {
userMessage: '任务已取消',
code: 'TASK_CANCELLED',
class: 'cancelled',
actions: ['restart'],
};
}