@tea-agent/loop-agent 0.31.1 → 0.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +46 -0
- package/README.md +1 -1
- package/dist/shared/operator/capabilities.js +22 -1
- package/dist/shared/operator/command-lifecycle.js +94 -0
- package/dist/shared/operator/index.js +1 -0
- package/dist/worker/cli.js +10 -24
- package/dist/worker/console/doctor.js +11 -4
- package/dist/worker/console/observe-health-match.js +18 -15
- package/dist/worker/console/operator-actions.js +2 -1
- package/dist/worker/console/static/assets/index-Bpa2qrc-.js +29 -0
- package/dist/worker/console/static/assets/index-BqfFDdnG.css +1 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +140 -0
- package/dist/worker/console/static-src/app/useConsoleShell.js +101 -0
- package/dist/worker/console/static-src/app/useOperatorActions.js +304 -0
- package/dist/worker/console/static-src/app/usePrdImport.js +171 -0
- package/dist/worker/console/static-src/app/useRecoveryActions.js +257 -0
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +334 -0
- package/dist/worker/console/static-src/app/useTaskWizard.js +229 -0
- package/dist/worker/console/static-src/chat-view-types.js +2 -0
- package/dist/worker/console/static-src/night/night-types.js +24 -0
- package/dist/worker/console/static-src/night/useNightBoard.js +125 -0
- package/dist/worker/console/static-src/night/useNightWizard.js +171 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +253 -0
- package/dist/worker/console/static-src/operator-chat/format.js +71 -0
- package/dist/worker/console/static-src/operator-chat/refs.js +47 -0
- package/dist/worker/console/static-src/operator-chat/tools-catalog.js +77 -0
- package/dist/worker/console/static-src/operator-chat/types.js +1 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +320 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +209 -0
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +218 -0
- package/dist/worker/console/static-src/operator-chat/useComposer.js +125 -0
- package/dist/worker/console/static-src/operator-chat/useInterview.js +108 -0
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +123 -0
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +207 -0
- package/dist/worker/feature/acceptance-policy.js +227 -0
- package/dist/worker/feature/decision-loader.js +1 -1
- package/dist/worker/feature/next-action.js +56 -6
- package/dist/worker/feature/profile-schema.js +3 -0
- package/dist/worker/feature/reducer.js +1 -0
- package/dist/worker/feature/review.js +72 -8
- package/dist/worker/feature/scaffold.js +14 -0
- package/dist/worker/loop-agent/controller-protocol.js +143 -0
- package/dist/worker/materialize/harness-task-lifecycle-probe.js +126 -0
- package/dist/worker/materialize/harness-task-lineage.js +220 -0
- package/dist/worker/materialize/harness-task-materializer.js +350 -80
- package/dist/worker/observability/progress-composite.js +1 -0
- package/dist/worker/observability/read-model.js +66 -19
- package/dist/worker/pool/attempt-identity.js +41 -0
- package/dist/worker/pool/attempt-lease.js +184 -0
- package/dist/worker/pool/attempt-transition.js +210 -0
- package/dist/worker/pool/begin-attempt-with-lease.js +26 -0
- package/dist/worker/pool/begin-attempt.js +35 -0
- package/dist/worker/pool/failure-routing.js +49 -0
- package/dist/worker/pool/recovery-decision.js +163 -0
- package/dist/worker/pool/run-owner-store.js +126 -0
- package/dist/worker/pool/run-store.js +32 -46
- package/dist/worker/pool/runtime-reconcile-inventory.js +127 -0
- package/dist/worker/pool/state-projection.js +57 -0
- package/dist/worker/run-task/run-task.js +31 -4
- package/dist/worker/runner/run-ready.js +64 -14
- package/dist/worker/runner/single-task-attempt.js +42 -13
- package/dist/worker/task-graph/acceptance-schema.js +3 -0
- package/docs/README.md +3 -1
- package/docs/architecture/evolution.md +1 -1
- package/docs/operations/README.md +1 -0
- package/docs/templates/README.md +1 -0
- package/docs/templates/agent-worker-production-readiness-checklist.md +47 -0
- package/docs/templates/evaluation/agents-map-slim-v1.md +1 -1
- package/docs/templates/evaluation/agents-map-verbose-v0.md +1 -1
- package/docs/templates/init-managed-agents.md +1 -1
- package/docs/templates/product-line/scaffold-samples/backend-only/acceptance.yaml +3 -0
- package/docs/templates/product-line/scaffold-samples/backend-only/feature.yaml +11 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/acceptance.yaml +3 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/feature.yaml +11 -0
- package/docs/templates/product-line/scaffold-samples/frontend-only/acceptance.yaml +3 -0
- package/docs/templates/product-line/scaffold-samples/frontend-only/feature.yaml +11 -0
- package/package.json +5 -4
- package/skills/agent-worker/SKILL.md +1 -1
- package/skills/agent-worker/references/agent-worker-operator.md +1 -1
- package/skills/loop-agent/references/command-reference.md +2 -2
- package/skills/loop-agent/references/harness-policy.md +1 -1
- package/dist/worker/console/static/assets/index-D9gnJn_l.js +0 -29
- package/dist/worker/console/static/assets/index-rajoXwkM.css +0 -1
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { isAutoRecoveryReason } from "../../recovery-selection.js";
|
|
3
|
+
import { formatDagRerunIneligible, postOperator, requestId, } from "./console-types.js";
|
|
4
|
+
/** Recovery CTA dispatch: diagnose/report/inspect shortcuts, reason-gated
|
|
5
|
+
* confirm flow with dagRerunPlan preflight, and gated reruns/retries. */
|
|
6
|
+
export function useRecoveryActions(params) {
|
|
7
|
+
const { shell, actions, taskId, dagRunId, fromNodeId, featureId, recoveryReason, recoveryReasonTouched, recoveryConfirm, recoveryPlanSummary, setRecoveryConfirm, setRecoveryPlanSummary, setRecoveryReport, setRecoveryReason, setRecoveryReasonTouched, buildCurrentRecoveryReason, openFullCanvas, } = params;
|
|
8
|
+
const { setOpError, setOpErrorCode, setOpMessage, setBusy } = shell;
|
|
9
|
+
const { runAction, isActionSuccess } = actions;
|
|
10
|
+
const handleRecoveryCta = useCallback(async (cta) => {
|
|
11
|
+
// Leaving a confirm flow when picking diagnose / advisory CTAs.
|
|
12
|
+
if (!cta.requiresReason) {
|
|
13
|
+
setRecoveryConfirm(null);
|
|
14
|
+
setRecoveryPlanSummary(null);
|
|
15
|
+
}
|
|
16
|
+
if (cta.action === "doctor") {
|
|
17
|
+
setRecoveryReport(null);
|
|
18
|
+
void runAction("doctor", {});
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (cta.action === "dagReport" || cta.id === "report") {
|
|
22
|
+
const runId = dagRunId.trim();
|
|
23
|
+
if (runId) {
|
|
24
|
+
setRecoveryConfirm(null);
|
|
25
|
+
setRecoveryPlanSummary(null);
|
|
26
|
+
const body = await runAction("dagReport", { runId });
|
|
27
|
+
const envelope = (body?.result ?? {});
|
|
28
|
+
const run = (envelope.raw?.runs ?? envelope.runs)?.[0];
|
|
29
|
+
if (body && body.ok !== false && !body.error && run) {
|
|
30
|
+
setRecoveryReport({
|
|
31
|
+
runId: run.runId ?? runId,
|
|
32
|
+
title: run.title,
|
|
33
|
+
status: run.status,
|
|
34
|
+
category: run.productLineFailureCategory,
|
|
35
|
+
recommendation: run.recoveryRecommendation?.summary ??
|
|
36
|
+
run.recommendedFollowUp ??
|
|
37
|
+
undefined,
|
|
38
|
+
});
|
|
39
|
+
setOpMessage(null);
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (taskId.trim()) {
|
|
44
|
+
setRecoveryReport(null);
|
|
45
|
+
void runAction("status", { taskId });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
setOpError("查看报告需要先选择 DAG 运行 ID(或填写任务 ID)");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (cta.action === "observeLink") {
|
|
52
|
+
setRecoveryReport(null);
|
|
53
|
+
void openFullCanvas();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (cta.requiresReason) {
|
|
57
|
+
setRecoveryReport(null);
|
|
58
|
+
setRecoveryConfirm(cta);
|
|
59
|
+
setRecoveryPlanSummary(null);
|
|
60
|
+
// Switching away from an ineligible node-rerun should not keep that error.
|
|
61
|
+
if (cta.id !== "dagRerun") {
|
|
62
|
+
setOpError(null);
|
|
63
|
+
setOpErrorCode(null);
|
|
64
|
+
}
|
|
65
|
+
if (!recoveryReasonTouched || isAutoRecoveryReason(recoveryReason)) {
|
|
66
|
+
setRecoveryReason(buildCurrentRecoveryReason(cta.label));
|
|
67
|
+
setRecoveryReasonTouched(false);
|
|
68
|
+
}
|
|
69
|
+
if (cta.id === "dagRerun" && dagRunId.trim() && fromNodeId.trim()) {
|
|
70
|
+
setBusy(true);
|
|
71
|
+
try {
|
|
72
|
+
const { status, body } = await postOperator("dagRerunPlan", {
|
|
73
|
+
runId: dagRunId.trim(),
|
|
74
|
+
fromNode: fromNodeId.trim(),
|
|
75
|
+
});
|
|
76
|
+
const plan = (body.result ?? {});
|
|
77
|
+
if (plan &&
|
|
78
|
+
typeof plan === "object" &&
|
|
79
|
+
("eligible" in plan || plan.planHash)) {
|
|
80
|
+
setRecoveryPlanSummary(plan);
|
|
81
|
+
}
|
|
82
|
+
if (status < 400 && body.ok !== false && plan.eligible !== false) {
|
|
83
|
+
setOpMessage(null);
|
|
84
|
+
}
|
|
85
|
+
else if (plan.eligible === false) {
|
|
86
|
+
setOpError(body.error?.message ?? formatDagRerunIneligible(plan));
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
setOpMessage(body.error?.message ??
|
|
90
|
+
"无法预检重跑计划;确认后仍将尝试提交(CLI 可能尚未就绪)");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
setBusy(false);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
setOpError(null);
|
|
100
|
+
setOpErrorCode(null);
|
|
101
|
+
setOpMessage(`${cta.label}:请经 CLI — ${cta.commandHint ?? ""}`);
|
|
102
|
+
}, [
|
|
103
|
+
buildCurrentRecoveryReason,
|
|
104
|
+
dagRunId,
|
|
105
|
+
fromNodeId,
|
|
106
|
+
openFullCanvas,
|
|
107
|
+
recoveryReason,
|
|
108
|
+
recoveryReasonTouched,
|
|
109
|
+
runAction,
|
|
110
|
+
setBusy,
|
|
111
|
+
setOpError,
|
|
112
|
+
setOpErrorCode,
|
|
113
|
+
setOpMessage,
|
|
114
|
+
setRecoveryConfirm,
|
|
115
|
+
setRecoveryPlanSummary,
|
|
116
|
+
setRecoveryReport,
|
|
117
|
+
setRecoveryReason,
|
|
118
|
+
setRecoveryReasonTouched,
|
|
119
|
+
taskId,
|
|
120
|
+
]);
|
|
121
|
+
const submitRecoveryConfirm = useCallback(async () => {
|
|
122
|
+
const cta = recoveryConfirm;
|
|
123
|
+
if (!cta)
|
|
124
|
+
return;
|
|
125
|
+
const reason = recoveryReason.trim();
|
|
126
|
+
if (!reason) {
|
|
127
|
+
setOpError("恢复操作需要结构化原因");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const clientRequestId = requestId(`recovery-${cta.id}`);
|
|
131
|
+
setRecoveryConfirm(null);
|
|
132
|
+
if (cta.id === "dagRerun") {
|
|
133
|
+
const runId = dagRunId.trim();
|
|
134
|
+
const fromNode = fromNodeId.trim();
|
|
135
|
+
if (!runId || !fromNode) {
|
|
136
|
+
setOpError("从节点重跑需要 DAG 运行 ID 与起始节点 ID");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
let planHash = recoveryPlanSummary?.planHash?.trim();
|
|
140
|
+
let planEligible = recoveryPlanSummary?.eligible;
|
|
141
|
+
if (!planHash || planEligible === undefined) {
|
|
142
|
+
const planBody = await runAction("dagRerunPlan", {
|
|
143
|
+
runId,
|
|
144
|
+
fromNode,
|
|
145
|
+
});
|
|
146
|
+
const plan = (planBody?.result ?? {});
|
|
147
|
+
if (plan.planHash || plan.eligible !== undefined) {
|
|
148
|
+
setRecoveryPlanSummary(plan);
|
|
149
|
+
}
|
|
150
|
+
planHash = plan.planHash?.trim() || planHash;
|
|
151
|
+
planEligible = plan.eligible;
|
|
152
|
+
if (plan.eligible === false) {
|
|
153
|
+
setOpError(planBody?.error?.message ?? formatDagRerunIneligible(plan));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (planBody && planBody.ok === false && !planHash) {
|
|
157
|
+
setOpError(planBody.error?.message ??
|
|
158
|
+
"无法获取 planHash;请确认 loop-agent dag rerun --plan 可用");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (planEligible === false) {
|
|
163
|
+
setOpError(formatDagRerunIneligible(recoveryPlanSummary ?? {}));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (!planHash) {
|
|
167
|
+
setOpError("无法获取 planHash;请确认 loop-agent dag rerun --plan 可用");
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
await runAction("dagRerun", { runId, fromNode, planHash, reason }, { clientRequestId });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (cta.id === "standaloneTaskRerun") {
|
|
174
|
+
const runId = dagRunId.trim();
|
|
175
|
+
if (!runId) {
|
|
176
|
+
setOpError("完整重跑任务需要 DAG 运行 ID");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const actionParams = {
|
|
180
|
+
runId,
|
|
181
|
+
reason,
|
|
182
|
+
...(taskId.trim() ? { taskId: taskId.trim() } : {}),
|
|
183
|
+
};
|
|
184
|
+
const prep = await runAction("prepareMutationGate", {
|
|
185
|
+
action: "standaloneTaskRerun",
|
|
186
|
+
actionParams,
|
|
187
|
+
});
|
|
188
|
+
if (!isActionSuccess(prep))
|
|
189
|
+
return;
|
|
190
|
+
const receipt = (prep?.result ?? prep);
|
|
191
|
+
if (!receipt.receiptId || !receipt.humanGateToken) {
|
|
192
|
+
setOpError("prepareMutationGate 未返回可消费的 Human Gate receipt");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
await runAction("standaloneTaskRerun", {
|
|
196
|
+
...actionParams,
|
|
197
|
+
confirmationId: receipt.receiptId,
|
|
198
|
+
humanGateToken: receipt.humanGateToken,
|
|
199
|
+
}, { clientRequestId });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (cta.id === "workerTaskRetry") {
|
|
203
|
+
const id = taskId.trim();
|
|
204
|
+
if (!id) {
|
|
205
|
+
setOpError("重新排队 Worker Task 需要任务 ID");
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const actionParams = {
|
|
209
|
+
taskId: id,
|
|
210
|
+
reason,
|
|
211
|
+
...(featureId.trim() ? { featureId: featureId.trim() } : {}),
|
|
212
|
+
};
|
|
213
|
+
const prep = await runAction("prepareMutationGate", {
|
|
214
|
+
action: "workerTaskRetry",
|
|
215
|
+
actionParams,
|
|
216
|
+
});
|
|
217
|
+
if (!isActionSuccess(prep))
|
|
218
|
+
return;
|
|
219
|
+
const receipt = (prep?.result ?? prep);
|
|
220
|
+
if (!receipt.receiptId || !receipt.humanGateToken) {
|
|
221
|
+
setOpError("prepareMutationGate 未返回可消费的 Human Gate receipt");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
await runAction("workerTaskRetry", {
|
|
225
|
+
...actionParams,
|
|
226
|
+
confirmationId: receipt.receiptId,
|
|
227
|
+
humanGateToken: receipt.humanGateToken,
|
|
228
|
+
}, { clientRequestId });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (cta.id === "reconcile") {
|
|
232
|
+
const runId = dagRunId.trim();
|
|
233
|
+
if (!runId) {
|
|
234
|
+
setOpError("对账恢复需要 DAG 运行 ID");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
setOpMessage(`${cta.label}:请经 CLI — ${cta.commandHint ?? ""}`);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
setOpMessage(`${cta.label}:请经 CLI — ${cta.commandHint ?? ""}`);
|
|
241
|
+
}, [
|
|
242
|
+
dagRunId,
|
|
243
|
+
featureId,
|
|
244
|
+
fromNodeId,
|
|
245
|
+
recoveryConfirm,
|
|
246
|
+
recoveryPlanSummary?.planHash,
|
|
247
|
+
recoveryReason,
|
|
248
|
+
runAction,
|
|
249
|
+
isActionSuccess,
|
|
250
|
+
setOpError,
|
|
251
|
+
setOpMessage,
|
|
252
|
+
setRecoveryConfirm,
|
|
253
|
+
setRecoveryPlanSummary,
|
|
254
|
+
taskId,
|
|
255
|
+
]);
|
|
256
|
+
return { handleRecoveryCta, submitRecoveryConfirm };
|
|
257
|
+
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { classifyRecoveryFact, consoleRecoveryCtas, partitionRecoveryCtas, recommendedRecoveryCtaId, } from "../../recovery-cta.js";
|
|
3
|
+
import { buildRecoveryReason, isAutoRecoveryReason, pickDefaultRecoveryNodeId, pickDefaultRecoveryRunId, sortRecoveryNodeCandidates, } from "../../recovery-selection.js";
|
|
4
|
+
import { postOperator, RECOVERY_FACT_LABEL, } from "./console-types.js";
|
|
5
|
+
import { useRecoveryActions } from "./useRecoveryActions.js";
|
|
6
|
+
/** Recovery workspace: DAG-run / node candidate selection, fact-classified
|
|
7
|
+
* CTA lanes, structured reason prefill, plan preflight and gated reruns. */
|
|
8
|
+
export function useRecoveryConsole(params) {
|
|
9
|
+
const { shell, actions, wizard } = params;
|
|
10
|
+
const { nav, opError, opErrorCode, lastOpState, setOpError, setObserveBusy } = shell;
|
|
11
|
+
const { taskId } = wizard;
|
|
12
|
+
const [dagRunId, setDagRunId] = useState("");
|
|
13
|
+
const [fromNodeId, setFromNodeId] = useState("");
|
|
14
|
+
const [featureId, setFeatureId] = useState("");
|
|
15
|
+
const [recoveryReason, setRecoveryReason] = useState("");
|
|
16
|
+
const [recoveryReasonTouched, setRecoveryReasonTouched] = useState(false);
|
|
17
|
+
const [recoveryPlanSummary, setRecoveryPlanSummary] = useState(null);
|
|
18
|
+
const [recoveryConfirm, setRecoveryConfirm] = useState(null);
|
|
19
|
+
const [recoveryRuns, setRecoveryRuns] = useState([]);
|
|
20
|
+
const [recoveryNodes, setRecoveryNodes] = useState([]);
|
|
21
|
+
const [recoveryRunsLoading, setRecoveryRunsLoading] = useState(false);
|
|
22
|
+
const [recoveryNodesLoading, setRecoveryNodesLoading] = useState(false);
|
|
23
|
+
const [recoveryRunStatus, setRecoveryRunStatus] = useState(null);
|
|
24
|
+
const [manualRunId, setManualRunId] = useState(false);
|
|
25
|
+
const [manualNodeId, setManualNodeId] = useState(false);
|
|
26
|
+
const [recoveryReport, setRecoveryReport] = useState(null);
|
|
27
|
+
const [observeLinkStatus, setObserveLinkStatus] = useState("idle");
|
|
28
|
+
const [observeLinkMessage, setObserveLinkMessage] = useState(null);
|
|
29
|
+
const [observeUrl, setObserveUrl] = useState(null);
|
|
30
|
+
const [inspectSplitOpen, setInspectSplitOpen] = useState(false);
|
|
31
|
+
const inspectPath = useCallback(() => {
|
|
32
|
+
if (dagRunId.trim()) {
|
|
33
|
+
return `/inspect/#/dag/${encodeURIComponent(dagRunId.trim())}`;
|
|
34
|
+
}
|
|
35
|
+
return "/inspect/#/";
|
|
36
|
+
}, [dagRunId]);
|
|
37
|
+
const openFullInspect = useCallback(() => {
|
|
38
|
+
window.location.assign(inspectPath());
|
|
39
|
+
}, [inspectPath]);
|
|
40
|
+
/** Resolve DAG run id for inspect (side panel or full page). */
|
|
41
|
+
const resolveDagRunForInspect = useCallback(async () => {
|
|
42
|
+
if (dagRunId.trim())
|
|
43
|
+
return dagRunId.trim();
|
|
44
|
+
const id = taskId.trim();
|
|
45
|
+
if (!id) {
|
|
46
|
+
setOpError("打开 DAG 检视需要任务 ID 或 DAG 运行 ID");
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
setObserveBusy(true);
|
|
50
|
+
setOpError(null);
|
|
51
|
+
try {
|
|
52
|
+
const { status, body } = await postOperator("observeLink", {
|
|
53
|
+
taskId: id,
|
|
54
|
+
});
|
|
55
|
+
const result = (body.result ?? {});
|
|
56
|
+
if (status >= 400 || body.ok === false) {
|
|
57
|
+
setOpError(body.error?.message ?? "无法解析 DAG 画布地址");
|
|
58
|
+
setObserveLinkStatus("unavailable");
|
|
59
|
+
setObserveLinkMessage(body.error?.message ?? null);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const resolvedDag = typeof result.dagRunId === "string" ? result.dagRunId.trim() : "";
|
|
63
|
+
if (resolvedDag) {
|
|
64
|
+
setDagRunId(resolvedDag);
|
|
65
|
+
setObserveLinkStatus("match");
|
|
66
|
+
setObserveLinkMessage(null);
|
|
67
|
+
setObserveUrl(`/inspect/#/dag/${encodeURIComponent(resolvedDag)}`);
|
|
68
|
+
return resolvedDag;
|
|
69
|
+
}
|
|
70
|
+
setOpError("尚未找到该任务的 DAG 运行,请稍后再打开检视");
|
|
71
|
+
setObserveLinkStatus("unavailable");
|
|
72
|
+
setObserveLinkMessage(result.message ?? "未解析到 DAG run");
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
setOpError(error instanceof Error ? error.message : String(error));
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
setObserveBusy(false);
|
|
81
|
+
}
|
|
82
|
+
}, [dagRunId, taskId, setOpError, setObserveBusy]);
|
|
83
|
+
/** Prefer in-page side panel so Operate state is preserved. */
|
|
84
|
+
const openInspectSplit = useCallback(async () => {
|
|
85
|
+
const resolved = await resolveDagRunForInspect();
|
|
86
|
+
if (resolved)
|
|
87
|
+
setInspectSplitOpen(true);
|
|
88
|
+
}, [resolveDagRunForInspect]);
|
|
89
|
+
/** Full DAG canvas — use only when user explicitly leaves Operate. */
|
|
90
|
+
const openFullCanvas = useCallback(async () => {
|
|
91
|
+
const resolved = await resolveDagRunForInspect();
|
|
92
|
+
if (!resolved)
|
|
93
|
+
return;
|
|
94
|
+
window.location.assign(`/inspect/#/dag/${encodeURIComponent(resolved)}`);
|
|
95
|
+
}, [resolveDagRunForInspect]);
|
|
96
|
+
const loadRecoveryRuns = useCallback(async () => {
|
|
97
|
+
setRecoveryRunsLoading(true);
|
|
98
|
+
try {
|
|
99
|
+
const id = taskId.trim();
|
|
100
|
+
const qs = new URLSearchParams();
|
|
101
|
+
if (id)
|
|
102
|
+
qs.set("taskId", id);
|
|
103
|
+
qs.set("limit", "40");
|
|
104
|
+
const res = await fetch(`/api/operator/v1/recovery/candidates?${qs.toString()}`, { credentials: "same-origin" });
|
|
105
|
+
if (!res.ok) {
|
|
106
|
+
setRecoveryRuns([]);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const body = (await res.json());
|
|
110
|
+
setRecoveryRuns(Array.isArray(body.runs) ? body.runs : []);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
setRecoveryRuns([]);
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
setRecoveryRunsLoading(false);
|
|
117
|
+
}
|
|
118
|
+
}, [taskId]);
|
|
119
|
+
const loadRecoveryNodes = useCallback(async (runId) => {
|
|
120
|
+
const id = runId.trim();
|
|
121
|
+
if (!id) {
|
|
122
|
+
setRecoveryNodes([]);
|
|
123
|
+
setRecoveryRunStatus(null);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
setRecoveryNodesLoading(true);
|
|
127
|
+
try {
|
|
128
|
+
const res = await fetch(`/api/dag-runs/${encodeURIComponent(id)}`, {
|
|
129
|
+
credentials: "same-origin",
|
|
130
|
+
});
|
|
131
|
+
if (!res.ok) {
|
|
132
|
+
setRecoveryNodes([]);
|
|
133
|
+
setRecoveryRunStatus(null);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const body = (await res.json());
|
|
137
|
+
setRecoveryRunStatus(typeof body.status === "string" ? body.status : null);
|
|
138
|
+
const rawNodes = Array.isArray(body.nodes) ? body.nodes : [];
|
|
139
|
+
const nodes = sortRecoveryNodeCandidates(rawNodes
|
|
140
|
+
.map((n) => {
|
|
141
|
+
const nodeId = (n.nodeId ?? n.id ?? "").trim();
|
|
142
|
+
if (!nodeId)
|
|
143
|
+
return null;
|
|
144
|
+
return {
|
|
145
|
+
nodeId,
|
|
146
|
+
status: n.status,
|
|
147
|
+
label: n.label ?? n.name,
|
|
148
|
+
errorPreview: n.errorPreview,
|
|
149
|
+
};
|
|
150
|
+
})
|
|
151
|
+
.filter((n) => n !== null));
|
|
152
|
+
setRecoveryNodes(nodes);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
setRecoveryNodes([]);
|
|
156
|
+
setRecoveryRunStatus(null);
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
setRecoveryNodesLoading(false);
|
|
160
|
+
}
|
|
161
|
+
}, []);
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (nav !== "recovery")
|
|
164
|
+
return;
|
|
165
|
+
void loadRecoveryRuns();
|
|
166
|
+
}, [nav, taskId, loadRecoveryRuns]);
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
if (nav !== "recovery")
|
|
169
|
+
return;
|
|
170
|
+
if (manualRunId)
|
|
171
|
+
return;
|
|
172
|
+
if (recoveryRuns.length === 0)
|
|
173
|
+
return;
|
|
174
|
+
const current = dagRunId.trim();
|
|
175
|
+
const inList = current
|
|
176
|
+
? recoveryRuns.some((r) => r.dagRunId === current)
|
|
177
|
+
: false;
|
|
178
|
+
if (!current || !inList) {
|
|
179
|
+
const preferred = pickDefaultRecoveryRunId(recoveryRuns);
|
|
180
|
+
if (preferred && preferred !== current)
|
|
181
|
+
setDagRunId(preferred);
|
|
182
|
+
}
|
|
183
|
+
}, [nav, recoveryRuns, dagRunId, manualRunId]);
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
if (nav !== "recovery")
|
|
186
|
+
return;
|
|
187
|
+
void loadRecoveryNodes(dagRunId);
|
|
188
|
+
}, [nav, dagRunId, loadRecoveryNodes]);
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (nav !== "recovery")
|
|
191
|
+
return;
|
|
192
|
+
if (manualNodeId)
|
|
193
|
+
return;
|
|
194
|
+
if (recoveryNodes.length === 0)
|
|
195
|
+
return;
|
|
196
|
+
const current = fromNodeId.trim();
|
|
197
|
+
const inList = current
|
|
198
|
+
? recoveryNodes.some((n) => n.nodeId === current)
|
|
199
|
+
: false;
|
|
200
|
+
if (!current || !inList) {
|
|
201
|
+
const preferred = pickDefaultRecoveryNodeId(recoveryNodes);
|
|
202
|
+
if (preferred && preferred !== current)
|
|
203
|
+
setFromNodeId(preferred);
|
|
204
|
+
}
|
|
205
|
+
}, [nav, recoveryNodes, fromNodeId, manualNodeId]);
|
|
206
|
+
const recoveryFact = classifyRecoveryFact({
|
|
207
|
+
// Selected DAG run status owns the recovery matrix; do not let read actions
|
|
208
|
+
// (dagReport / doctor) flip CTAs via transient lastOpState=succeeded.
|
|
209
|
+
operationState: recoveryRunStatus ? null : lastOpState,
|
|
210
|
+
errorCode: recoveryRunStatus ? null : opErrorCode,
|
|
211
|
+
dagStatus: recoveryRunStatus,
|
|
212
|
+
});
|
|
213
|
+
const recoveryCtas = useMemo(() => consoleRecoveryCtas(recoveryFact), [recoveryFact]);
|
|
214
|
+
const recoveryLanes = useMemo(() => partitionRecoveryCtas(recoveryCtas), [recoveryCtas]);
|
|
215
|
+
const recommendedCtaId = useMemo(() => recommendedRecoveryCtaId(recoveryFact), [recoveryFact]);
|
|
216
|
+
const selectedRecoveryNode = useMemo(() => recoveryNodes.find((n) => n.nodeId === fromNodeId.trim()), [recoveryNodes, fromNodeId]);
|
|
217
|
+
const recoveryConfirmRef = useRef(null);
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
if (!recoveryConfirm)
|
|
220
|
+
return;
|
|
221
|
+
recoveryConfirmRef.current?.scrollIntoView({
|
|
222
|
+
behavior: "smooth",
|
|
223
|
+
block: "nearest",
|
|
224
|
+
});
|
|
225
|
+
}, [recoveryConfirm?.id]);
|
|
226
|
+
const buildCurrentRecoveryReason = useCallback((actionLabel) => buildRecoveryReason({
|
|
227
|
+
actionLabel,
|
|
228
|
+
dagRunId: dagRunId.trim() || undefined,
|
|
229
|
+
fromNodeId: fromNodeId.trim() || undefined,
|
|
230
|
+
runStatus: recoveryRunStatus,
|
|
231
|
+
nodeStatus: selectedRecoveryNode?.status,
|
|
232
|
+
nodeError: selectedRecoveryNode?.errorPreview,
|
|
233
|
+
factClassLabel: RECOVERY_FACT_LABEL[recoveryFact],
|
|
234
|
+
errorMessage: opError,
|
|
235
|
+
errorCode: opErrorCode,
|
|
236
|
+
}), [
|
|
237
|
+
dagRunId,
|
|
238
|
+
fromNodeId,
|
|
239
|
+
opError,
|
|
240
|
+
opErrorCode,
|
|
241
|
+
recoveryFact,
|
|
242
|
+
recoveryRunStatus,
|
|
243
|
+
selectedRecoveryNode?.errorPreview,
|
|
244
|
+
selectedRecoveryNode?.status,
|
|
245
|
+
]);
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
if (nav !== "recovery")
|
|
248
|
+
return;
|
|
249
|
+
if (recoveryReasonTouched && !isAutoRecoveryReason(recoveryReason))
|
|
250
|
+
return;
|
|
251
|
+
const next = buildCurrentRecoveryReason(recoveryConfirm?.label);
|
|
252
|
+
if (next && next !== recoveryReason)
|
|
253
|
+
setRecoveryReason(next);
|
|
254
|
+
}, [
|
|
255
|
+
nav,
|
|
256
|
+
dagRunId,
|
|
257
|
+
fromNodeId,
|
|
258
|
+
recoveryRunStatus,
|
|
259
|
+
selectedRecoveryNode,
|
|
260
|
+
recoveryFact,
|
|
261
|
+
opError,
|
|
262
|
+
opErrorCode,
|
|
263
|
+
recoveryConfirm?.label,
|
|
264
|
+
recoveryReasonTouched,
|
|
265
|
+
recoveryReason,
|
|
266
|
+
buildCurrentRecoveryReason,
|
|
267
|
+
]);
|
|
268
|
+
const { handleRecoveryCta, submitRecoveryConfirm } = useRecoveryActions({
|
|
269
|
+
shell,
|
|
270
|
+
actions,
|
|
271
|
+
taskId,
|
|
272
|
+
dagRunId,
|
|
273
|
+
fromNodeId,
|
|
274
|
+
featureId,
|
|
275
|
+
recoveryReason,
|
|
276
|
+
recoveryReasonTouched,
|
|
277
|
+
recoveryConfirm,
|
|
278
|
+
recoveryPlanSummary,
|
|
279
|
+
setRecoveryConfirm,
|
|
280
|
+
setRecoveryPlanSummary,
|
|
281
|
+
setRecoveryReport,
|
|
282
|
+
setRecoveryReason,
|
|
283
|
+
setRecoveryReasonTouched,
|
|
284
|
+
buildCurrentRecoveryReason,
|
|
285
|
+
openFullCanvas,
|
|
286
|
+
});
|
|
287
|
+
return {
|
|
288
|
+
dagRunId,
|
|
289
|
+
setDagRunId,
|
|
290
|
+
fromNodeId,
|
|
291
|
+
setFromNodeId,
|
|
292
|
+
featureId,
|
|
293
|
+
setFeatureId,
|
|
294
|
+
recoveryReason,
|
|
295
|
+
setRecoveryReason,
|
|
296
|
+
recoveryReasonTouched,
|
|
297
|
+
setRecoveryReasonTouched,
|
|
298
|
+
recoveryPlanSummary,
|
|
299
|
+
recoveryConfirm,
|
|
300
|
+
setRecoveryConfirm,
|
|
301
|
+
setRecoveryPlanSummary,
|
|
302
|
+
recoveryRuns,
|
|
303
|
+
recoveryNodes,
|
|
304
|
+
recoveryRunsLoading,
|
|
305
|
+
recoveryNodesLoading,
|
|
306
|
+
recoveryRunStatus,
|
|
307
|
+
manualRunId,
|
|
308
|
+
setManualRunId,
|
|
309
|
+
manualNodeId,
|
|
310
|
+
setManualNodeId,
|
|
311
|
+
recoveryReport,
|
|
312
|
+
setRecoveryReport,
|
|
313
|
+
observeLinkStatus,
|
|
314
|
+
observeLinkMessage,
|
|
315
|
+
observeUrl,
|
|
316
|
+
inspectSplitOpen,
|
|
317
|
+
setInspectSplitOpen,
|
|
318
|
+
inspectPath,
|
|
319
|
+
openFullInspect,
|
|
320
|
+
resolveDagRunForInspect,
|
|
321
|
+
openInspectSplit,
|
|
322
|
+
openFullCanvas,
|
|
323
|
+
loadRecoveryRuns,
|
|
324
|
+
loadRecoveryNodes,
|
|
325
|
+
recoveryFact,
|
|
326
|
+
recoveryCtas,
|
|
327
|
+
recoveryLanes,
|
|
328
|
+
recommendedCtaId,
|
|
329
|
+
recoveryConfirmRef,
|
|
330
|
+
buildCurrentRecoveryReason,
|
|
331
|
+
handleRecoveryCta,
|
|
332
|
+
submitRecoveryConfirm,
|
|
333
|
+
};
|
|
334
|
+
}
|