Files
Agent-System/web/src/hooks/useTaskRunAttempts.ts
T
weichenandCursor fd4b95030f feat(web): score write-path matches by workflow relation
The document list now reports WORKFLOW_SCOPE_MODE, and weighted mode only blocks a cross-workflow write when that title outscores the active workflow.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-21 09:22:13 +00:00

908 lines
33 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
import {
autoRetryDelayMs,
shouldAutoRetryChat,
shouldAutoRetryTask,
} from '../api/chatRetry';
import { mapErrorInfo, streamChat, type StreamEvent } from '../api/chatStream';
import { fetchChatTurnStatus } from '../api/chatTurn';
import { mapTaskStatusToMessageStatus } from '../api/chatWindow';
import { listProjectWorkflowDocuments } from '../api/projectDocuments';
import { createTaskRun, getTaskRun, resumeTaskRun, subscribeTaskRunEvents } from '../api/taskRuns';
import { buildTaskRequest, streamTask } from '../api/taskStream';
import { applyPhaseEvent } from '../engine/phaseEventMapper';
import { resolveTaskTemplateForMessage, wrongWorkflowGuidance } from '../engine/templateMatch';
import { GOVERNANCE_BLOCKED_HINT, isBlockedGovernanceStatus } from '../lib/blockedGovernanceCopy';
import {
controlledHydrateCandidateIds,
planControlledContentInvalidation,
type ControlledInvalidationPlan,
} from '../lib/controlledContentInvalidation';
import { mergeWorkflowDocumentsFromServer } from '../lib/deliverableSync';
import { createEditorHydrationGate } from '../lib/editorHydrationGate';
import {
pollUntilPostTaskServerDoc,
shouldSwitchTemplateSlotToServer,
} from '../lib/postTaskDeliverableSync';
import {
buildCancelledTaskErrorInfo,
buildFailedTaskErrorInfo,
buildInterruptedTaskErrorInfo,
buildNetworkErrorInfo,
isTaskStageDegradeCode,
readRemoteTaskRun,
} from '../lib/taskRunErrorInfo';
import {
isRemoteTaskTerminal,
nextAttemptOpts,
resolveReconnectRunId,
shouldSubscribeExistingRun,
} from '../lib/taskRunReconnect';
import type { DocumentAST } from '../types/block';
import type { UserProfile, UserRole } from '../types/docgov';
import type {
AssistantMessageStatus,
ChatErrorInfo,
ChatMessage,
DeliverableCategory,
DeliverableItem,
PhaseEvent,
ProjectSession,
QuotedMessage,
TaskRunContext,
} from '../types/workbench';
import { useTaskRunStream } from './useTaskRunStream';
import { useTurnRuntime } from './useTurnRuntime';
type Gate = ReturnType<typeof createEditorHydrationGate>;
type TurnRuntime = ReturnType<typeof useTurnRuntime>;
type TaskStream = ReturnType<typeof useTaskRunStream>;
export interface TaskRunAttemptsInput {
activeSessionId: string;
activeDocId: string;
currentProjectId: number;
currentUser: UserProfile;
currentDocRole: UserRole;
sessions: ProjectSession[];
messages: ChatMessage[];
deliverables: DeliverableItem[];
visibleDeliverables: DeliverableItem[];
currentProjectDeliverables: DeliverableItem[];
activeWorkflowKey?: string;
workflowScopeMode?: 'strict' | 'weighted';
deliverablesRef: MutableRefObject<DeliverableItem[]>;
activeDocIdRef: MutableRefObject<string>;
editorHydrationGateRef: MutableRefObject<Gate>;
turnRuntime: TurnRuntime;
taskRunStream: TaskStream;
chatWindow: {
replaceMessageServerId: (tempId: string, serverId: number) => void;
refresh: () => Promise<void>;
allMessages: ChatMessage[];
updateLiveMessage: (id: string, patch: Partial<ChatMessage>) => void;
};
setMessages: Dispatch<SetStateAction<ChatMessage[]>>;
setDeliverables: Dispatch<SetStateAction<DeliverableItem[]>>;
setDocCache: Dispatch<SetStateAction<Record<string, DocumentAST>>>;
setActiveDocId: Dispatch<SetStateAction<string>>;
setActiveCategory: Dispatch<SetStateAction<DeliverableCategory>>;
applyControlledInvalidationPlan: (plan: ControlledInvalidationPlan) => void;
}
export function useTaskRunAttempts(input: TaskRunAttemptsInput) {
const {
activeSessionId,
activeDocId,
currentProjectId,
currentUser,
currentDocRole,
sessions,
messages,
deliverables,
visibleDeliverables,
currentProjectDeliverables,
activeWorkflowKey,
workflowScopeMode,
deliverablesRef,
activeDocIdRef,
editorHydrationGateRef,
turnRuntime,
taskRunStream,
chatWindow,
setMessages,
setDeliverables,
setDocCache,
setActiveDocId,
setActiveCategory,
applyControlledInvalidationPlan,
} = input;
// 驱动一次真实后端 SSE 流式对话尝试:气泡状态机 pending → streaming_status → completed/failed/cancelled。
// 闲聊路径在无 client_turn_id 幂等前禁止自动重放 POST;失败仅展示手动「重试」。
type StreamAttemptOpts = {
userMsgId?: string;
clientTurnId?: string;
quote?: QuotedMessage;
};
const runStreamAttempt = (
userText: string,
asstId: string,
attemptIndex: number,
opts?: StreamAttemptOpts
) => {
const quote = opts?.quote;
const controller = new AbortController();
const streamSessionId = activeSessionId;
if (opts?.clientTurnId) {
turnRuntime.registerTurn({
clientTurnId: opts.clientTurnId,
sessionId: streamSessionId,
userMsgId: opts.userMsgId || '',
asstId,
status: 'pending',
controller,
startedAt: Date.now(),
});
}
const currentProj = sessions.find((s) => s.sessionId === streamSessionId);
let accumulated = '';
let settled = false;
const updateAsst = (patch: Partial<ChatMessage>) => {
setMessages((prev) => prev.map((m) => (m.id === asstId ? { ...m, ...patch } : m)));
};
const scheduleRetry = (delayMs: number) => {
setTimeout(() => {
if (controller.signal.aborted) {
updateAsst({ status: 'cancelled', livePhaseLabel: undefined });
if (opts?.clientTurnId) {
turnRuntime.updateTurnStatus(streamSessionId, opts.clientTurnId, 'cancelled');
}
return;
}
runStreamAttempt(userText, asstId, attemptIndex + 1, opts);
}, delayMs);
};
const finishTurn = (status: 'completed' | 'failed' | 'cancelled') => {
if (opts?.clientTurnId) {
turnRuntime.updateTurnStatus(streamSessionId, opts.clientTurnId, status);
}
};
const failOrRetry = (info: ChatErrorInfo) => {
settled = true;
if (shouldAutoRetryChat(info, attemptIndex)) {
const delayMs = autoRetryDelayMs(attemptIndex);
updateAsst({
status: 'auto_retrying',
livePhaseLabel: `连接异常,${Math.round(delayMs / 1000)}s 后自动重试…`,
});
scheduleRetry(delayMs);
return;
}
updateAsst({
status: 'failed',
errorInfo: {
...info,
userMessage:
attemptIndex > 0
? `${info.userMessage}(请确认上一请求是否已在处理后再手动重试)`
: info.userMessage,
},
livePhaseLabel: undefined,
});
finishTurn('failed');
};
updateAsst({
status: 'pending',
livePhaseLabel: attemptIndex > 0 ? '正在手动重试…' : '助手正在处理…',
content: '',
errorInfo: undefined,
});
if (opts?.clientTurnId) {
turnRuntime.updateTurnStatus(streamSessionId, opts.clientTurnId, 'pending');
}
const bindUserServerId = (serverId: number) => {
if (!opts?.userMsgId) return;
chatWindow.replaceMessageServerId(opts.userMsgId, serverId);
setMessages((prev) =>
prev.map((m) => (m.id === opts.userMsgId ? { ...m, serverId } : m))
);
};
streamChat(
{
session_id: streamSessionId,
user_id: currentUser.id,
project_id: currentProj?.projectId || currentProjectId || 1,
project_name: currentProj?.title || '肌骨超声AI辅助诊断系统',
device_type: currentProj?.deviceType || '肌骨超声',
stage: currentProj?.stage || 'V0.5',
user_name: currentUser.name,
user_role: currentDocRole,
message: userText,
client_turn_id: opts?.clientTurnId,
quote: quote,
},
{
onEvent: (ev) => {
if (controller.signal.aborted || settled) return;
switch (ev.type) {
case 'turn_ack': {
const userServerId = ev.turn?.user_message_id ?? ev.user_message_id;
if (typeof userServerId === 'number' && userServerId > 0) {
bindUserServerId(userServerId);
}
break;
}
case 'phase':
case 'status': {
const label = ev.phase?.message || ev.status?.message;
updateAsst({ status: 'streaming_status', livePhaseLabel: label || '助手正在处理…' });
if (opts?.clientTurnId) {
turnRuntime.updateTurnStatus(streamSessionId, opts.clientTurnId, 'streaming');
}
break;
}
case 'delta': {
accumulated += ev.delta || '';
updateAsst({ status: 'streaming_status', content: accumulated });
if (opts?.clientTurnId) {
turnRuntime.updateTurnStatus(streamSessionId, opts.clientTurnId, 'streaming');
}
break;
}
case 'error': {
failOrRetry(mapErrorInfo(ev));
break;
}
case 'done': {
settled = true;
const userServerId = ev.done?.user_message_id ?? ev.user_message_id;
const serverMsgId = ev.done?.message_id ?? ev.message_id ?? (ev as { messageId?: number }).messageId;
if (typeof userServerId === 'number' && userServerId > 0) {
bindUserServerId(userServerId);
}
if (typeof serverMsgId === 'number' && serverMsgId > 0) {
chatWindow.replaceMessageServerId(asstId, serverMsgId);
}
const exportCap = ev.export_capability || ev.done?.export_capability;
updateAsst({
status: 'completed',
livePhaseLabel: undefined,
serverId: typeof serverMsgId === 'number' ? serverMsgId : undefined,
exportCapability: exportCap,
});
finishTurn('completed');
break;
}
default:
break;
}
},
},
controller.signal,
opts?.clientTurnId ? { idempotencyKey: opts.clientTurnId } : undefined
).catch(async (_err) => {
if (settled) return;
if (controller.signal.aborted) {
updateAsst({ status: 'cancelled', livePhaseLabel: undefined });
finishTurn('cancelled');
return;
}
if (opts?.clientTurnId) {
try {
const turnStatus = await fetchChatTurnStatus(streamSessionId, opts.clientTurnId);
if (turnStatus?.status === 'completed' && turnStatus.assistantMessageId) {
settled = true;
if (turnStatus.userMessageId) bindUserServerId(turnStatus.userMessageId);
chatWindow.replaceMessageServerId(asstId, turnStatus.assistantMessageId);
updateAsst({
status: 'completed',
livePhaseLabel: undefined,
serverId: turnStatus.assistantMessageId,
});
finishTurn('completed');
return;
}
if (turnStatus?.status === 'processing') {
updateAsst({
status: 'streaming_status',
livePhaseLabel: '连接中断,服务端仍在处理…',
});
if (opts?.clientTurnId) {
turnRuntime.updateTurnStatus(streamSessionId, opts.clientTurnId, 'streaming');
}
return;
}
} catch {
// fall through to structured failure
}
}
const httpStatus =
typeof _err === 'object' && _err !== null && 'httpStatus' in _err
? Number((_err as { httpStatus?: number }).httpStatus)
: undefined;
const errMeta = _err as { errorCode?: string; detail?: string; message?: string };
failOrRetry(
buildNetworkErrorInfo(httpStatus, {
errorCode: errMeta.errorCode,
detail: errMeta.detail || errMeta.message?.split(': ').slice(1).join(': '),
}),
);
});
};
// P2:长任务尝试模式。'run' 新任务 | 'resume' 续跑失败工作区 | 'restart' 按原意图新工作区
const runTaskAttempt = (
task: TaskRunContext,
asstId: string,
mode: 'run' | 'resume' | 'restart',
attemptIndex: number,
opts?: { phaseEvents?: PhaseEvent[]; workDir?: string; reconnectRunId?: string }
) => {
const sessionId = task.sessionId || activeSessionId;
let retainedRunId = resolveReconnectRunId({
attemptIndex,
optsReconnectRunId: opts?.reconnectRunId,
});
taskRunStream.abortSession(sessionId);
// 重连保留显式 runId;禁止无条件 setRunId(null) 导致自动重试丢失
if (retainedRunId) {
taskRunStream.setRunId(sessionId, retainedRunId);
} else {
taskRunStream.setRunId(sessionId, null);
}
const controller = new AbortController();
taskRunStream.setController(sessionId, controller);
let accumulated = '';
let settled = false;
const historyOnly = !messages.some((m) => m.id === asstId);
let historySnapshot = historyOnly
? chatWindow.allMessages.find((m) => m.id === asstId)
: undefined;
const patchAsst = (patch: Partial<ChatMessage> | ((m: ChatMessage) => Partial<ChatMessage>)) => {
const applyTo = (m: ChatMessage): ChatMessage => ({
...m,
...(typeof patch === 'function' ? patch(m) : patch),
});
if (historyOnly) {
if (!historySnapshot) return;
historySnapshot = applyTo(historySnapshot);
chatWindow.updateLiveMessage(asstId, historySnapshot);
return;
}
setMessages((prev) => prev.map((m) => (m.id === asstId ? applyTo(m) : m)));
};
const scheduleRetry = (delayMs: number) => {
setTimeout(() => {
if (controller.signal.aborted) {
patchAsst({ status: 'cancelled', livePhaseLabel: undefined });
taskRunStream.setThinking(sessionId, false);
return;
}
runTaskAttempt(task, asstId, mode, attemptIndex + 1, nextAttemptOpts(opts, retainedRunId));
}, delayMs);
};
const failOrRetry = (info: ChatErrorInfo) => {
settled = true;
if (shouldAutoRetryTask(info, attemptIndex)) {
const delayMs = autoRetryDelayMs(attemptIndex);
patchAsst({
status: 'auto_retrying',
livePhaseLabel: `连接异常,${Math.round(delayMs / 1000)}s 后自动重试…`,
});
scheduleRetry(delayMs);
return;
}
patchAsst({ status: 'failed', errorInfo: info, livePhaseLabel: undefined });
taskRunStream.setThinking(sessionId, false);
};
const syncControlledDocsAfterTask = async (taskKey: string, preferredDocId?: string | null) => {
if (!task.projectId || task.projectId <= 0 || !taskKey) return;
const { merged, serverDocId } = await pollUntilPostTaskServerDoc({
listDocs: () => listProjectWorkflowDocuments(task.projectId!).catch(() => []),
getDeliverables: () => deliverablesRef.current,
merge: (prev, docs) =>
mergeWorkflowDocumentsFromServer(prev, docs, task.projectId!, currentUser),
projectId: task.projectId,
taskKey,
preferredDocId,
maxAttempts: 10,
delayMs: 800,
onMerged: (next) => {
deliverablesRef.current = next;
setDeliverables(next);
applyControlledInvalidationPlan(
planControlledContentInvalidation({
reason: 'post_task',
candidateIds: controlledHydrateCandidateIds(next, task.projectId),
activeDocId: activeDocIdRef.current,
editorOpen: editorHydrationGateRef.current.isEditorOpen(),
}),
);
},
});
if (
serverDocId &&
shouldSwitchTemplateSlotToServer({
activeDocId,
deliverables: merged,
projectId: task.projectId,
taskKey,
serverDocId,
})
) {
setActiveDocId(serverDocId);
setActiveCategory('workflow');
}
};
// 等待态:resume 保留已有 phaseEvents 续接;run/restart 重置正文与步骤条
patchAsst({
status: 'pending',
livePhaseLabel:
mode === 'resume' ? '正在继续任务…' : mode === 'restart' ? '正在重新开始任务…' : '任务执行中…',
errorInfo: undefined,
...(mode === 'resume' || attemptIndex > 0
? {}
: { content: '', phaseEvents: [], taskWorkspaceId: undefined }),
});
taskRunStream.setThinking(sessionId, true);
const onEvent = (ev: StreamEvent) => {
if (controller.signal.aborted || settled) return;
switch (ev.type) {
case 'phase': {
patchAsst((m) => ({
status: 'streaming_status',
livePhaseLabel: ev.phase?.message || m.livePhaseLabel,
phaseEvents: applyPhaseEvent(opts?.phaseEvents || m.phaseEvents || [], ev.phase),
}));
break;
}
case 'status': {
if (ev.status?.message) {
patchAsst({ status: 'streaming_status', livePhaseLabel: ev.status.message });
}
break;
}
case 'delta': {
accumulated += ev.delta || '';
patchAsst({ status: 'streaming_status', content: accumulated });
break;
}
case 'deliverable': {
const title = ev.deliverable?.title;
if (title) {
accumulated += (accumulated ? '\n\n' : '') + `📄 已生成文档:**${title}**`;
patchAsst({
status: 'streaming_status',
content: accumulated,
taskArtifact: {
...(typeof ev.deliverable?.task_key === 'string'
? { task_key: ev.deliverable.task_key }
: {}),
title,
quality_score:
typeof ev.deliverable?.quality_score === 'number'
? ev.deliverable.quality_score
: undefined,
},
});
}
break;
}
case 'governance': {
const governance = ev.governance;
const docId = governance?.doc_id;
if (docId) {
const leaseReleased = governance.action === 'lease_released';
const editLease = leaseReleased
? undefined
: { holderType: 'squad' as const, holderId: taskRunStream.activeTaskRunId || undefined };
setDeliverables((prev) =>
prev.map((doc) => (doc.docId === docId ? { ...doc, editLease } : doc)),
);
setDocCache((prev) => {
const ast = prev[docId];
return ast ? { ...prev, [docId]: { ...ast, editLease } } : prev;
});
}
if (governance?.notification_msg) {
patchAsst({
status: 'streaming_status',
livePhaseLabel: governance.notification_msg,
});
}
break;
}
case 'error': {
failOrRetry(mapErrorInfo(ev));
break;
}
case 'done': {
settled = true;
const finishedRunId = retainedRunId || taskRunStream.activeTaskRunId;
taskRunStream.setRunId(sessionId, null);
taskRunStream.setThinking(sessionId, false);
void (async () => {
let nextStatus: AssistantMessageStatus = 'completed';
let preferredDocId: string | null = null;
if (finishedRunId) {
const remote = await getTaskRun(finishedRunId).catch(() => null);
const view = readRemoteTaskRun(remote);
if (view.status) {
const mapped = mapTaskStatusToMessageStatus(view.status, 'assistant');
if (mapped) nextStatus = mapped;
}
}
patchAsst({
status: nextStatus,
livePhaseLabel: undefined,
taskWorkspaceId: ev.done?.workspace_id,
taskDurationMs:
typeof ev.done?.duration_ms === 'number' ? ev.done.duration_ms : undefined,
...(finishedRunId ? { taskRunId: finishedRunId } : {}),
});
// quality_failed:未过门禁稿不得当作正式受控版本激活/同步切换
if (nextStatus === 'completed' || nextStatus === 'pending_author_confirmation') {
await syncControlledDocsAfterTask(task.taskKey || 'intake', preferredDocId);
}
await chatWindow.refresh().catch(() => undefined);
})();
break;
}
default:
break;
}
};
const startObserver = async () => {
if (mode === 'resume') {
const runId = opts?.reconnectRunId?.trim();
if (!runId) {
settled = true;
patchAsst({
status: 'failed',
errorInfo: {
userMessage: '找不到可继续的任务记录,请重新开始',
code: 'TASK_INTERRUPTED',
class: 'fatal',
actions: ['restart'],
},
});
console.warn('continue ignored: missing run id', asstId);
taskRunStream.setThinking(sessionId, false);
return;
}
let workspaceId = opts?.workDir?.trim() || '';
if (!workspaceId) {
const remote = await getTaskRun(runId).catch(() => null);
workspaceId = readRemoteTaskRun(remote).workspaceId;
}
if (!workspaceId) {
settled = true;
patchAsst({
status: 'failed',
livePhaseLabel: undefined,
errorInfo: {
userMessage: '任务没有可恢复的工作区,请重新开始',
code: 'ERR_NO_WORKSPACE',
class: 'fatal',
actions: ['restart'],
},
});
console.warn('continue ignored: missing workspace', runId);
taskRunStream.setThinking(sessionId, false);
return;
}
const resumed = await resumeTaskRun(runId);
retainedRunId = resumed.run_id || runId;
taskRunStream.setRunId(sessionId, retainedRunId);
taskRunStream.setThinking(sessionId, true);
patchAsst({
status: 'streaming_status',
content: '任务执行中…',
errorInfo: undefined,
taskRunId: retainedRunId,
taskWorkspaceId: resumed.workspace_id || workspaceId,
livePhaseLabel: '正在继续任务…',
});
await subscribeTaskRunEvents(retainedRunId, { onEvent }, controller.signal);
return;
}
const body = buildTaskRequest(task);
if (task.sessionId && task.projectId && task.projectId > 0 && mode === 'run') {
// 断线重试:显式 reconnectRunId 保留;禁止 setMessages updater 取 runId
const existingRunId = resolveReconnectRunId({
attemptIndex,
optsReconnectRunId: retainedRunId || opts?.reconnectRunId,
});
if (attemptIndex > 0 && existingRunId) {
const remote = await getTaskRun(existingRunId).catch(() => null);
const view = readRemoteTaskRun(remote);
if (remote && shouldSubscribeExistingRun(view.status)) {
retainedRunId = existingRunId;
taskRunStream.setRunId(sessionId, existingRunId);
patchAsst({ taskRunId: existingRunId, livePhaseLabel: '正在重连任务…' });
await subscribeTaskRunEvents(existingRunId, { onEvent }, controller.signal);
return;
}
if (remote && isRemoteTaskTerminal(view.status)) {
settled = true;
const mapped = mapTaskStatusToMessageStatus(view.status, 'assistant') || 'failed';
patchAsst({
status: mapped,
livePhaseLabel: undefined,
taskWorkspaceId: view.workspaceId,
taskRunId: existingRunId,
errorInfo:
view.status === 'interrupted'
? buildInterruptedTaskErrorInfo(view.errorMessage, view.workspaceId)
: undefined,
});
taskRunStream.setThinking(sessionId, false);
if (mapped === 'completed' || mapped === 'pending_author_confirmation') {
await syncControlledDocsAfterTask(task.taskKey || 'intake');
}
return;
}
}
const created = await createTaskRun({
...body,
session_id: task.sessionId,
project_id: task.projectId,
user_id: task.userId,
client_turn_id: task.clientTurnId,
user_message: task.userMessage,
});
retainedRunId = created.run_id;
taskRunStream.setRunId(sessionId, created.run_id);
patchAsst({ taskRunId: created.run_id });
if (isBlockedGovernanceStatus(created.status)) {
settled = true;
patchAsst({
status: 'blocked_pending_governance',
content: GOVERNANCE_BLOCKED_HINT,
livePhaseLabel: undefined,
taskRunId: created.run_id,
});
taskRunStream.setRunId(sessionId, null);
taskRunStream.setThinking(sessionId, false);
await chatWindow.refresh().catch(() => undefined);
return;
}
await subscribeTaskRunEvents(created.run_id, { onEvent }, controller.signal);
return;
}
await streamTask(body, { onEvent }, controller.signal);
};
void startObserver().catch(async (_err) => {
if (settled) return;
if (controller.signal.aborted) {
patchAsst({ status: 'cancelled', livePhaseLabel: undefined });
taskRunStream.setThinking(sessionId, false);
return;
}
const runId = retainedRunId || taskRunStream.activeTaskRunId || undefined;
if (runId) {
try {
const remote = await getTaskRun(runId);
const view = readRemoteTaskRun(remote);
if (view.status === 'quality_failed') {
settled = true;
patchAsst({
status: 'quality_failed',
livePhaseLabel: undefined,
taskWorkspaceId: view.workspaceId,
taskRunId: runId,
});
taskRunStream.setThinking(sessionId, false);
return;
}
if (isBlockedGovernanceStatus(view.status)) {
settled = true;
patchAsst({
status: 'blocked_pending_governance',
content: GOVERNANCE_BLOCKED_HINT,
livePhaseLabel: undefined,
taskWorkspaceId: view.workspaceId,
taskRunId: runId,
});
taskRunStream.setThinking(sessionId, false);
return;
}
if (view.status === 'completed') {
settled = true;
patchAsst({
status: 'completed',
livePhaseLabel: undefined,
taskWorkspaceId: view.workspaceId,
taskRunId: runId,
});
taskRunStream.setThinking(sessionId, false);
await syncControlledDocsAfterTask(task.taskKey || 'intake');
return;
}
if (view.status === 'pending_author_confirmation') {
settled = true;
patchAsst({
status: 'pending_author_confirmation',
livePhaseLabel: undefined,
taskWorkspaceId: view.workspaceId,
taskRunId: runId,
});
taskRunStream.setThinking(sessionId, false);
await syncControlledDocsAfterTask(task.taskKey || 'intake');
await chatWindow.refresh().catch(() => undefined);
return;
}
if (shouldSubscribeExistingRun(view.status)) {
// 原 run 仍在跑:重订阅,不 POST 新 run
settled = false;
retainedRunId = runId;
patchAsst({ livePhaseLabel: '正在重连任务…', taskRunId: runId });
await subscribeTaskRunEvents(runId, { onEvent }, controller.signal);
return;
}
if (view.status === 'failed' || view.status === 'finalizing_failed') {
failOrRetry(buildFailedTaskErrorInfo(view.errorMessage));
return;
}
if (view.status === 'interrupted') {
settled = true;
patchAsst({
status: 'failed',
livePhaseLabel: undefined,
taskWorkspaceId: view.workspaceId,
taskRunId: runId,
errorInfo: buildInterruptedTaskErrorInfo(view.errorMessage, view.workspaceId),
});
taskRunStream.setThinking(sessionId, false);
return;
}
if (view.status === 'cancelled') {
failOrRetry(buildCancelledTaskErrorInfo());
return;
}
} catch {
// fall through to network retry path
}
}
const httpStatus =
typeof _err === 'object' && _err !== null && 'httpStatus' in _err
? Number((_err as { httpStatus?: number }).httpStatus)
: undefined;
const errMeta = _err as { errorCode?: string; detail?: string; message?: string };
// 阶段/作用域未解析:降级聊天一次,由后端意图确认引导,替代仅弹错误气泡。
if (
mode === 'run' &&
attemptIndex === 0 &&
isTaskStageDegradeCode(errMeta.errorCode) &&
task.userMessage?.trim()
) {
settled = true;
patchAsst({
status: 'pending',
livePhaseLabel: '改由对话确认引导…',
errorInfo: undefined,
taskRequest: undefined,
content: '',
});
taskRunStream.setThinking(sessionId, false);
runStreamAttempt(task.userMessage.trim(), asstId, 0, {
clientTurnId: task.clientTurnId,
});
return;
}
failOrRetry(
buildNetworkErrorInfo(httpStatus, {
errorCode: errMeta.errorCode,
detail: errMeta.detail || errMeta.message?.split(': ').slice(1).join(': '),
}),
);
});
};
// 结构化错误 / 已停止会话上的用户手动恢复动作 (P2:retry / continue / restart 分型恢复)
const handleErrorAction = (messageId: string, action: 'retry' | 'continue' | 'restart') => {
const target =
messages.find((m) => m.id === messageId) ||
chatWindow.allMessages.find((m) => m.id === messageId);
if (!target) {
console.warn('task error action ignored: message not found', messageId, action);
return;
}
// 长任务路径:按 taskRequest 重发 / 续跑工作区 / 按原意图重启
if (target.taskRequest) {
if (action === 'retry') {
const userText = target.taskRequest.userMessage || '';
const retryActive =
deliverables.find((d) => d.docId === activeDocId) ||
visibleDeliverables.find((d) => d.docId === activeDocId);
const reResolved = resolveTaskTemplateForMessage(
userText,
currentProjectDeliverables,
retryActive,
activeWorkflowKey,
workflowScopeMode,
);
if (reResolved?.wrongWorkflow) {
chatWindow.updateLiveMessage(messageId, {
content: wrongWorkflowGuidance(reResolved.wrongWorkflow),
livePhaseLabel: undefined,
status: 'completed',
});
return;
}
const retryTask = reResolved?.taskKey
? {
...target.taskRequest,
taskKey: reResolved.taskKey,
docId: reResolved.docId || target.taskRequest.docId,
}
: target.taskRequest;
runTaskAttempt(retryTask, messageId, 'run', 0);
} else if (action === 'continue') {
const runId = target.taskRunId;
if (!runId) {
console.warn('continue ignored: history bubble has no run id', messageId);
chatWindow.updateLiveMessage(messageId, {
livePhaseLabel: '找不到可继续的任务记录,请重新开始',
});
return;
}
runTaskAttempt(target.taskRequest, messageId, 'resume', 0, {
phaseEvents: target.phaseEvents,
workDir: target.errorInfo?.workspaceId || target.taskWorkspaceId,
reconnectRunId: runId,
});
} else {
if (!window.confirm('将放弃当前工作区并按原意图重新开始任务,确认?')) return;
runTaskAttempt(target.taskRequest, messageId, 'restart', 0);
}
return;
}
// 闲聊路径(P1 行为不变:仅 retry)
if (action !== 'retry') {
console.warn('task error action ignored: no task context', messageId, action);
chatWindow.updateLiveMessage(messageId, {
livePhaseLabel: '这条消息没有可重跑的任务上下文',
});
return;
}
const sourceMsg = target.sourceUserMessageId
? messages.find((m) => m.id === target.sourceUserMessageId)
: undefined;
const originalText = sourceMsg?.content?.trim();
if (!originalText) return;
runStreamAttempt(originalText, messageId, 0, {
userMsgId: sourceMsg?.id,
clientTurnId: target.clientTurnId || sourceMsg?.clientTurnId,
});
};
return { runStreamAttempt, runTaskAttempt, handleErrorAction };
}