Keep weak write verbs off the task shortcut path, and once-only fall back to chat when TASK_SCOPE_UNRESOLVED or DOC_TASK_NOT_IN_ACTIVE_STAGE. Co-authored-by: Cursor <cursoragent@cursor.com>
158 lines
5.1 KiB
TypeScript
158 lines
5.1 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: '无权访问该会话',
|
|
TASK_SCOPE_UNRESOLVED: '无法定位该文档任务的工作流阶段,请检查任务名或联系管理员',
|
|
};
|
|
|
|
/** 任务直通道失败后应降级到聊天确认引导的错误码(仅降级一次,防循环)。 */
|
|
export function isTaskStageDegradeCode(code: string | undefined | null): boolean {
|
|
return code === 'TASK_SCOPE_UNRESOLVED' || code === 'DOC_TASK_NOT_IN_ACTIVE_STAGE';
|
|
}
|
|
|
|
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' || code === 'TASK_SCOPE_UNRESOLVED';
|
|
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'],
|
|
};
|
|
}
|
|
|
|
/** interrupted 且有工作区时可继续;否则只允许按原请求重开。 */
|
|
export function buildInterruptedTaskErrorInfo(errorMessage: string, workspaceId: string): ChatErrorInfo {
|
|
const message = errorMessage.trim() || '任务执行中断,可继续或重新开始';
|
|
const resumable = workspaceId.trim() !== '';
|
|
return {
|
|
userMessage: message,
|
|
code: 'TASK_INTERRUPTED',
|
|
class: resumable ? 'resumable' : 'fatal',
|
|
workspaceId: resumable ? workspaceId : undefined,
|
|
actions: resumable ? ['continue', 'restart'] : ['restart'],
|
|
};
|
|
}
|
|
|
|
/** 服务端已判定 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'],
|
|
};
|
|
}
|