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>
139 lines
4.2 KiB
TypeScript
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'],
|
|
};
|
|
}
|