@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,229 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { ensureUniqueDocumentNames, operatorRequestFits, operatorRequestTooLargeMessage, } from "../prd-file-import.js";
|
|
3
|
+
import { requestId, WORKFLOW_OPTIONS, } from "./console-types.js";
|
|
4
|
+
/** Happy Path task wizard: import-prd → interview → contract → dag-confirm →
|
|
5
|
+
* run. Owns task identity, interview projection and contract preflight/apply. */
|
|
6
|
+
export function useTaskWizard(params) {
|
|
7
|
+
const { shell, actions, prd } = params;
|
|
8
|
+
const { step, setStep, setOpMessage } = shell;
|
|
9
|
+
const { runAction, isActionSuccess, summarizeEnvelope } = actions;
|
|
10
|
+
const [taskId, setTaskId] = useState("");
|
|
11
|
+
const [taskTitle, setTaskTitle] = useState("");
|
|
12
|
+
const [taskKind, setTaskKind] = useState("standard");
|
|
13
|
+
const workflowMeta = WORKFLOW_OPTIONS.find((w) => w.kind === taskKind) ?? WORKFLOW_OPTIONS[0];
|
|
14
|
+
const [sessionId, setSessionId] = useState(null);
|
|
15
|
+
const [question, setQuestion] = useState(null);
|
|
16
|
+
const [answerText, setAnswerText] = useState("");
|
|
17
|
+
const [draftSummary, setDraftSummary] = useState(null);
|
|
18
|
+
const [assessmentId, setAssessmentId] = useState(null);
|
|
19
|
+
const [contractValidateOk, setContractValidateOk] = useState(null);
|
|
20
|
+
const [contractValidateSummary, setContractValidateSummary] = useState(null);
|
|
21
|
+
const [contractDiffSummary, setContractDiffSummary] = useState(null);
|
|
22
|
+
const [contractApplied, setContractApplied] = useState(false);
|
|
23
|
+
const [spineOk, setSpineOk] = useState(null);
|
|
24
|
+
const [contractPreflightKey, setContractPreflightKey] = useState(null);
|
|
25
|
+
const contractPreflightRunning = useRef(false);
|
|
26
|
+
const resetContractLanding = useCallback(() => {
|
|
27
|
+
setContractValidateOk(null);
|
|
28
|
+
setContractValidateSummary(null);
|
|
29
|
+
setContractDiffSummary(null);
|
|
30
|
+
setContractApplied(false);
|
|
31
|
+
setSpineOk(null);
|
|
32
|
+
setContractPreflightKey(null);
|
|
33
|
+
contractPreflightRunning.current = false;
|
|
34
|
+
}, []);
|
|
35
|
+
const enterContractStep = useCallback(() => {
|
|
36
|
+
resetContractLanding();
|
|
37
|
+
setStep("contract");
|
|
38
|
+
}, [resetContractLanding, setStep]);
|
|
39
|
+
const applyInterviewPayload = useCallback((result) => {
|
|
40
|
+
const session = result.session;
|
|
41
|
+
if (session?.sessionId)
|
|
42
|
+
setSessionId(session.sessionId);
|
|
43
|
+
const q = result.question;
|
|
44
|
+
setQuestion(q);
|
|
45
|
+
setDraftSummary(result.summary ?? null);
|
|
46
|
+
const aid = result.assessmentId;
|
|
47
|
+
if (typeof aid === "string" && aid)
|
|
48
|
+
setAssessmentId(aid);
|
|
49
|
+
if (result.complete === true || (typeof aid === "string" && aid)) {
|
|
50
|
+
enterContractStep();
|
|
51
|
+
setOpMessage(result.skipped === true
|
|
52
|
+
? "已跳过访谈:已按推荐补齐草稿并签发评估"
|
|
53
|
+
: "访谈已完成:草稿已绑定完整评估结果");
|
|
54
|
+
}
|
|
55
|
+
}, [enterContractStep, setOpMessage]);
|
|
56
|
+
const runContractPreflight = useCallback(async (force = false) => {
|
|
57
|
+
const id = taskId.trim();
|
|
58
|
+
if (!id)
|
|
59
|
+
return;
|
|
60
|
+
const key = `${id}:${assessmentId ?? ""}`;
|
|
61
|
+
if (!force &&
|
|
62
|
+
contractPreflightKey === key &&
|
|
63
|
+
contractValidateOk !== null) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (contractPreflightRunning.current)
|
|
67
|
+
return;
|
|
68
|
+
contractPreflightRunning.current = true;
|
|
69
|
+
setContractValidateOk(null);
|
|
70
|
+
setContractValidateSummary(null);
|
|
71
|
+
setContractDiffSummary(null);
|
|
72
|
+
setSpineOk(null);
|
|
73
|
+
setContractApplied(false);
|
|
74
|
+
try {
|
|
75
|
+
const validateBody = await runAction("contractValidate", {
|
|
76
|
+
taskId: id,
|
|
77
|
+
});
|
|
78
|
+
const validateOk = isActionSuccess(validateBody);
|
|
79
|
+
setContractValidateOk(validateOk);
|
|
80
|
+
setContractValidateSummary(summarizeEnvelope(validateBody));
|
|
81
|
+
if (!validateOk) {
|
|
82
|
+
setContractPreflightKey(key);
|
|
83
|
+
setOpMessage("契约校验未通过:请先修正草稿后再应用");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const diffBody = await runAction("contractDiff", { taskId: id });
|
|
87
|
+
setContractDiffSummary(summarizeEnvelope(diffBody));
|
|
88
|
+
setContractPreflightKey(key);
|
|
89
|
+
setOpMessage(isActionSuccess(diffBody)
|
|
90
|
+
? "校验与差异已就绪:确认后可应用契约"
|
|
91
|
+
: "校验通过,但差异查询失败(仍可查看下方摘要)");
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
contractPreflightRunning.current = false;
|
|
95
|
+
}
|
|
96
|
+
}, [
|
|
97
|
+
assessmentId,
|
|
98
|
+
contractPreflightKey,
|
|
99
|
+
contractValidateOk,
|
|
100
|
+
isActionSuccess,
|
|
101
|
+
runAction,
|
|
102
|
+
setOpMessage,
|
|
103
|
+
summarizeEnvelope,
|
|
104
|
+
taskId,
|
|
105
|
+
]);
|
|
106
|
+
const applyContractThenSpine = useCallback(async () => {
|
|
107
|
+
const id = taskId.trim();
|
|
108
|
+
if (!id || !assessmentId)
|
|
109
|
+
return;
|
|
110
|
+
const applyBody = await runAction("contractApply", { taskId: id, assessmentId }, { clientRequestId: requestId("apply") });
|
|
111
|
+
if (!isActionSuccess(applyBody)) {
|
|
112
|
+
setContractApplied(false);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
setContractApplied(true);
|
|
116
|
+
const spineBody = await runAction("spineCheck", { taskId: id });
|
|
117
|
+
if (isActionSuccess(spineBody)) {
|
|
118
|
+
setSpineOk(true);
|
|
119
|
+
setStep("dag-confirm");
|
|
120
|
+
setOpMessage("契约已应用并通过 Spine 审计,可生成并运行 DAG");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
setSpineOk(false);
|
|
124
|
+
setOpMessage("契约已应用,但 Spine 审计未通过:可重试 Spine");
|
|
125
|
+
}, [assessmentId, isActionSuccess, runAction, setOpMessage, setStep, taskId]);
|
|
126
|
+
const retrySpineOnly = useCallback(async () => {
|
|
127
|
+
const id = taskId.trim();
|
|
128
|
+
if (!id)
|
|
129
|
+
return;
|
|
130
|
+
const spineBody = await runAction("spineCheck", { taskId: id });
|
|
131
|
+
if (isActionSuccess(spineBody)) {
|
|
132
|
+
setSpineOk(true);
|
|
133
|
+
setStep("dag-confirm");
|
|
134
|
+
setOpMessage("Spine 审计已通过,可生成并运行 DAG");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
setSpineOk(false);
|
|
138
|
+
}, [isActionSuccess, runAction, setOpMessage, setStep, taskId]);
|
|
139
|
+
const confirmAndRunDag = useCallback(async () => {
|
|
140
|
+
const id = actions.confirmationId?.trim();
|
|
141
|
+
if (!id)
|
|
142
|
+
return;
|
|
143
|
+
// M0-B: pass the server-signed human-gate token (issued at prepare) instead
|
|
144
|
+
// of the old fixed-string challenge set. The dispatch layer verifies the
|
|
145
|
+
// HMAC signature, so the model cannot self-confirm.
|
|
146
|
+
const humanGateToken = actions.confirmationDetails?.confirmation?.humanGateToken;
|
|
147
|
+
const confirmBody = await runAction("confirmDagConfirmation", {
|
|
148
|
+
confirmationId: id,
|
|
149
|
+
humanGateToken,
|
|
150
|
+
});
|
|
151
|
+
if (!isActionSuccess(confirmBody))
|
|
152
|
+
return;
|
|
153
|
+
setStep("run");
|
|
154
|
+
await runAction("runDag", { confirmationId: id }, { clientRequestId: requestId("run") });
|
|
155
|
+
}, [
|
|
156
|
+
actions.confirmationId,
|
|
157
|
+
actions.confirmationDetails,
|
|
158
|
+
isActionSuccess,
|
|
159
|
+
runAction,
|
|
160
|
+
setStep,
|
|
161
|
+
]);
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (step !== "contract")
|
|
164
|
+
return;
|
|
165
|
+
if (!taskId.trim())
|
|
166
|
+
return;
|
|
167
|
+
void runContractPreflight(false);
|
|
168
|
+
}, [step, taskId, assessmentId, runContractPreflight]);
|
|
169
|
+
const importPrdAndCreate = useCallback(async () => {
|
|
170
|
+
const docsPayload = prd.prdDocuments.length > 0
|
|
171
|
+
? ensureUniqueDocumentNames(prd.prdDocuments)
|
|
172
|
+
: undefined;
|
|
173
|
+
const actionParams = {
|
|
174
|
+
content: prd.prdText,
|
|
175
|
+
taskKind,
|
|
176
|
+
...(docsPayload ? { documents: docsPayload } : {}),
|
|
177
|
+
};
|
|
178
|
+
if (!operatorRequestFits("bootstrapFromPrd", actionParams)) {
|
|
179
|
+
prd.setPrdFileError(operatorRequestTooLargeMessage());
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const body = await runAction("bootstrapFromPrd", actionParams);
|
|
183
|
+
if (!body || body.ok === false)
|
|
184
|
+
return;
|
|
185
|
+
const result = (body.result ?? {});
|
|
186
|
+
if (result.taskId)
|
|
187
|
+
setTaskId(result.taskId);
|
|
188
|
+
if (result.title)
|
|
189
|
+
setTaskTitle(result.title);
|
|
190
|
+
setOpMessage(`已创建任务「${result.title ?? ""}」(${result.taskId ?? ""}),来源:${result.identitySource ?? "prd"}`);
|
|
191
|
+
setStep("interview");
|
|
192
|
+
}, [prd, runAction, setOpMessage, setStep, taskKind]);
|
|
193
|
+
const wizardSteps = [
|
|
194
|
+
{ id: "setup", label: "就绪检查" },
|
|
195
|
+
{ id: "import-prd", label: "导入需求" },
|
|
196
|
+
{ id: "interview", label: "需求访谈(可跳过)" },
|
|
197
|
+
{ id: "contract", label: "契约落地" },
|
|
198
|
+
{ id: "dag-confirm", label: "确认 DAG" },
|
|
199
|
+
{ id: "run", label: "运行" },
|
|
200
|
+
];
|
|
201
|
+
const stepIndex = wizardSteps.findIndex((s) => s.id === step);
|
|
202
|
+
return {
|
|
203
|
+
taskId,
|
|
204
|
+
setTaskId,
|
|
205
|
+
taskTitle,
|
|
206
|
+
taskKind,
|
|
207
|
+
setTaskKind,
|
|
208
|
+
workflowMeta,
|
|
209
|
+
sessionId,
|
|
210
|
+
question,
|
|
211
|
+
answerText,
|
|
212
|
+
setAnswerText,
|
|
213
|
+
draftSummary,
|
|
214
|
+
assessmentId,
|
|
215
|
+
contractValidateOk,
|
|
216
|
+
contractValidateSummary,
|
|
217
|
+
contractDiffSummary,
|
|
218
|
+
contractApplied,
|
|
219
|
+
spineOk,
|
|
220
|
+
applyInterviewPayload,
|
|
221
|
+
runContractPreflight,
|
|
222
|
+
applyContractThenSpine,
|
|
223
|
+
retrySpineOnly,
|
|
224
|
+
confirmAndRunDag,
|
|
225
|
+
importPrdAndCreate,
|
|
226
|
+
wizardSteps,
|
|
227
|
+
stepIndex,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function requestId(prefix) {
|
|
2
|
+
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
3
|
+
}
|
|
4
|
+
export function confirmationToken() {
|
|
5
|
+
return window.__CONSOLE_CONFIRMATION_TOKEN__ ?? "";
|
|
6
|
+
}
|
|
7
|
+
export function statusLabel(status) {
|
|
8
|
+
const map = {
|
|
9
|
+
submitted: "已提交",
|
|
10
|
+
validating: "校验中",
|
|
11
|
+
validated: "待预约确认",
|
|
12
|
+
scheduled: "已预约",
|
|
13
|
+
waiting: "等待触发",
|
|
14
|
+
dispatching: "派发中",
|
|
15
|
+
running: "执行中",
|
|
16
|
+
succeeded: "成功",
|
|
17
|
+
failed: "失败",
|
|
18
|
+
human_required: "需人工",
|
|
19
|
+
cancelled: "已取消",
|
|
20
|
+
rejected: "已拒绝",
|
|
21
|
+
cancel_requested: "取消中",
|
|
22
|
+
};
|
|
23
|
+
return map[status] ?? status;
|
|
24
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { confirmationToken, } from "./night-types.js";
|
|
3
|
+
/** Night board data plane: schedules list + doctor report + task pool +
|
|
4
|
+
* foreground aux-ticker projection, loaded together on mount/refresh. */
|
|
5
|
+
export function useNightBoard(params) {
|
|
6
|
+
const { setOpError } = params;
|
|
7
|
+
const [list, setList] = useState(null);
|
|
8
|
+
const [doctor, setDoctor] = useState(null);
|
|
9
|
+
const [tasks, setTasks] = useState([]);
|
|
10
|
+
const [loadError, setLoadError] = useState(null);
|
|
11
|
+
const [loadingBoard, setLoadingBoard] = useState(false);
|
|
12
|
+
const [advancedOpen, setAdvancedOpen] = useState(false);
|
|
13
|
+
const [morningPreview, setMorningPreview] = useState(null);
|
|
14
|
+
const [auxTicker, setAuxTicker] = useState(null);
|
|
15
|
+
const [auxTickerBusy, setAuxTickerBusy] = useState(false);
|
|
16
|
+
const refreshBoard = useCallback(async () => {
|
|
17
|
+
setLoadingBoard(true);
|
|
18
|
+
setLoadError(null);
|
|
19
|
+
try {
|
|
20
|
+
const [jobsRes, doctorRes, tasksRes, tickerRes] = await Promise.all([
|
|
21
|
+
fetch("/api/night-jobs", { credentials: "same-origin" }),
|
|
22
|
+
fetch("/api/night-jobs/doctor", { credentials: "same-origin" }),
|
|
23
|
+
fetch("/api/tasks", { credentials: "same-origin" }),
|
|
24
|
+
fetch("/api/operator/v1/night-aux-ticker", {
|
|
25
|
+
credentials: "same-origin",
|
|
26
|
+
}),
|
|
27
|
+
]);
|
|
28
|
+
if (!jobsRes.ok) {
|
|
29
|
+
throw new Error(`加载计划列表失败(HTTP ${jobsRes.status})`);
|
|
30
|
+
}
|
|
31
|
+
const jobsBody = (await jobsRes.json());
|
|
32
|
+
setList(jobsBody);
|
|
33
|
+
if (doctorRes.ok) {
|
|
34
|
+
setDoctor((await doctorRes.json()));
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
setDoctor(null);
|
|
38
|
+
}
|
|
39
|
+
if (tasksRes.ok) {
|
|
40
|
+
const taskBody = (await tasksRes.json());
|
|
41
|
+
setTasks(Array.isArray(taskBody) ? taskBody : []);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
setTasks([]);
|
|
45
|
+
}
|
|
46
|
+
if (tickerRes.ok) {
|
|
47
|
+
const tickerBody = (await tickerRes.json());
|
|
48
|
+
if (tickerBody.ticker)
|
|
49
|
+
setAuxTicker(tickerBody.ticker);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
setLoadError(error instanceof Error ? error.message : String(error));
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
setLoadingBoard(false);
|
|
57
|
+
}
|
|
58
|
+
}, []);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
void refreshBoard();
|
|
61
|
+
}, [refreshBoard]);
|
|
62
|
+
const onToggleAuxTicker = useCallback(async (enabled) => {
|
|
63
|
+
setAuxTickerBusy(true);
|
|
64
|
+
try {
|
|
65
|
+
const res = await fetch("/api/operator/v1/night-aux-ticker", {
|
|
66
|
+
method: "POST",
|
|
67
|
+
credentials: "same-origin",
|
|
68
|
+
headers: {
|
|
69
|
+
"Content-Type": "application/json",
|
|
70
|
+
"X-Loop-Console-Confirmation": confirmationToken(),
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify({ enabled }),
|
|
73
|
+
});
|
|
74
|
+
const body = (await res.json());
|
|
75
|
+
if (!res.ok || body.ok === false || !body.ticker) {
|
|
76
|
+
setOpError(body.error?.message ??
|
|
77
|
+
`切换前台辅助 ticker 失败(HTTP ${res.status})`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
setAuxTicker(body.ticker);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
setOpError(error instanceof Error ? error.message : String(error));
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
setAuxTickerBusy(false);
|
|
87
|
+
}
|
|
88
|
+
}, [setOpError]);
|
|
89
|
+
const onMorningReport = useCallback(async () => {
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch("/api/night-jobs/morning", {
|
|
92
|
+
credentials: "same-origin",
|
|
93
|
+
});
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
setOpError(`早晨报告加载失败(HTTP ${res.status})`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const body = await res.json();
|
|
99
|
+
setMorningPreview(JSON.stringify(body, null, 2).slice(0, 4000));
|
|
100
|
+
setAdvancedOpen(true);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
setOpError(error instanceof Error ? error.message : String(error));
|
|
104
|
+
}
|
|
105
|
+
}, [setOpError]);
|
|
106
|
+
const doctorIssues = (doctor?.findings ?? []).filter((f) => f.severity === "error" || f.severity === "warning");
|
|
107
|
+
const clockHint = (doctor?.findings ?? []).find((f) => /clock/i.test(f.code) || /clock/i.test(f.message));
|
|
108
|
+
return {
|
|
109
|
+
list,
|
|
110
|
+
doctor,
|
|
111
|
+
tasks,
|
|
112
|
+
loadError,
|
|
113
|
+
loadingBoard,
|
|
114
|
+
advancedOpen,
|
|
115
|
+
setAdvancedOpen,
|
|
116
|
+
morningPreview,
|
|
117
|
+
auxTicker,
|
|
118
|
+
auxTickerBusy,
|
|
119
|
+
refreshBoard,
|
|
120
|
+
onToggleAuxTicker,
|
|
121
|
+
onMorningReport,
|
|
122
|
+
doctorIssues,
|
|
123
|
+
clockHint,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from "react";
|
|
2
|
+
import { datetimeLocalToAdmissionAt, defaultNightAtLocal, extractNightPrepareReview, featureDirFromFeatureId, nightRowNeedsForceDiscard, } from "../night-prepare-result.js";
|
|
3
|
+
import { requestId, } from "./night-types.js";
|
|
4
|
+
/** Night scheduling wizard + row mutations: every write goes through
|
|
5
|
+
* prepareMutationGate → Human Gate receipt → gated action. */
|
|
6
|
+
export function useNightWizard(params) {
|
|
7
|
+
const { board, runAction, isActionSuccess, setOpError } = params;
|
|
8
|
+
const { tasks, refreshBoard } = board;
|
|
9
|
+
const [phase, setPhase] = useState("pick");
|
|
10
|
+
const [selectedKey, setSelectedKey] = useState("");
|
|
11
|
+
const [nightAt, setNightAt] = useState(defaultNightAtLocal);
|
|
12
|
+
const [taskCard, setTaskCard] = useState("");
|
|
13
|
+
const [review, setReview] = useState(null);
|
|
14
|
+
const [discardReason, setDiscardReason] = useState("");
|
|
15
|
+
const [discardTarget, setDiscardTarget] = useState(null);
|
|
16
|
+
const [forceDiscard, setForceDiscard] = useState(false);
|
|
17
|
+
const selectableTasks = useMemo(() => {
|
|
18
|
+
const rows = tasks.filter((t) => typeof t.featureId === "string" && t.featureId.trim() && t.taskId);
|
|
19
|
+
rows.sort((a, b) => {
|
|
20
|
+
const fa = a.featureId ?? "";
|
|
21
|
+
const fb = b.featureId ?? "";
|
|
22
|
+
if (fa !== fb)
|
|
23
|
+
return fa.localeCompare(fb);
|
|
24
|
+
return a.taskId.localeCompare(b.taskId);
|
|
25
|
+
});
|
|
26
|
+
return rows;
|
|
27
|
+
}, [tasks]);
|
|
28
|
+
const selectedTask = useMemo(() => {
|
|
29
|
+
if (!selectedKey)
|
|
30
|
+
return null;
|
|
31
|
+
return (selectableTasks.find((t) => `${t.featureId}::${t.taskId}` === selectedKey) ?? null);
|
|
32
|
+
}, [selectableTasks, selectedKey]);
|
|
33
|
+
const derivedFeatureDir = selectedTask?.featureId
|
|
34
|
+
? featureDirFromFeatureId(selectedTask.featureId)
|
|
35
|
+
: "";
|
|
36
|
+
const runGatedMutation = useCallback(async (action, actionParams, clientPrefix) => {
|
|
37
|
+
const prep = await runAction("prepareMutationGate", {
|
|
38
|
+
action,
|
|
39
|
+
actionParams,
|
|
40
|
+
});
|
|
41
|
+
if (!isActionSuccess(prep))
|
|
42
|
+
return undefined;
|
|
43
|
+
const receiptSource = (prep?.result ?? prep);
|
|
44
|
+
const receipt = receiptSource.receiptId
|
|
45
|
+
? receiptSource
|
|
46
|
+
: (receiptSource.result ?? receiptSource);
|
|
47
|
+
if (!receipt.receiptId || !receipt.humanGateToken) {
|
|
48
|
+
setOpError("prepareMutationGate 未返回可消费的 Human Gate receipt");
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
return runAction(action, {
|
|
52
|
+
...actionParams,
|
|
53
|
+
confirmationId: receipt.receiptId,
|
|
54
|
+
humanGateToken: receipt.humanGateToken,
|
|
55
|
+
}, { clientRequestId: requestId(clientPrefix) });
|
|
56
|
+
}, [isActionSuccess, runAction, setOpError]);
|
|
57
|
+
const onPrepare = useCallback(async () => {
|
|
58
|
+
if (!selectedTask?.featureId) {
|
|
59
|
+
setOpError("请先点选 Feature / Task");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const at = datetimeLocalToAdmissionAt(nightAt);
|
|
63
|
+
if (!at) {
|
|
64
|
+
setOpError("执行时间格式无效,请使用日期时间选择器");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const actionParams = {
|
|
68
|
+
featureDir: featureDirFromFeatureId(selectedTask.featureId),
|
|
69
|
+
taskId: selectedTask.taskId,
|
|
70
|
+
at,
|
|
71
|
+
tz: "Asia/Shanghai",
|
|
72
|
+
...(taskCard.trim() ? { taskCard: taskCard.trim() } : {}),
|
|
73
|
+
};
|
|
74
|
+
const body = await runGatedMutation("workerAdmissionPrepare", actionParams, "night-prepare");
|
|
75
|
+
if (!isActionSuccess(body))
|
|
76
|
+
return;
|
|
77
|
+
const packet = extractNightPrepareReview(body?.result ?? body);
|
|
78
|
+
if (!packet) {
|
|
79
|
+
setOpError("准备成功但未解析到 scheduleId / gateToken。请刷新后重试,或查看 Inspect。");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
setReview(packet);
|
|
83
|
+
setPhase("review");
|
|
84
|
+
}, [
|
|
85
|
+
isActionSuccess,
|
|
86
|
+
nightAt,
|
|
87
|
+
runGatedMutation,
|
|
88
|
+
selectedTask,
|
|
89
|
+
setOpError,
|
|
90
|
+
taskCard,
|
|
91
|
+
]);
|
|
92
|
+
const onConfirmAdd = useCallback(async () => {
|
|
93
|
+
if (!review)
|
|
94
|
+
return;
|
|
95
|
+
const body = await runGatedMutation("workerSchedulerAdd", {
|
|
96
|
+
scheduleId: review.scheduleId,
|
|
97
|
+
approveGate: review.gateToken,
|
|
98
|
+
}, "night-add");
|
|
99
|
+
if (!isActionSuccess(body))
|
|
100
|
+
return;
|
|
101
|
+
setReview(null);
|
|
102
|
+
setPhase("pick");
|
|
103
|
+
setSelectedKey("");
|
|
104
|
+
setTaskCard("");
|
|
105
|
+
setNightAt(defaultNightAtLocal());
|
|
106
|
+
await refreshBoard();
|
|
107
|
+
}, [isActionSuccess, refreshBoard, review, runGatedMutation]);
|
|
108
|
+
const onCancelWizard = useCallback(() => {
|
|
109
|
+
setPhase("pick");
|
|
110
|
+
setReview(null);
|
|
111
|
+
}, []);
|
|
112
|
+
const onHarvest = useCallback(async (row) => {
|
|
113
|
+
const body = await runGatedMutation("workerSchedulerHarvest", { scheduleId: row.scheduleId }, "night-harvest");
|
|
114
|
+
if (!isActionSuccess(body))
|
|
115
|
+
return;
|
|
116
|
+
await refreshBoard();
|
|
117
|
+
}, [isActionSuccess, refreshBoard, runGatedMutation]);
|
|
118
|
+
const onDiscard = useCallback(async () => {
|
|
119
|
+
if (!discardTarget)
|
|
120
|
+
return;
|
|
121
|
+
const reason = discardReason.trim() || "discard from console night workbench";
|
|
122
|
+
const needsForce = nightRowNeedsForceDiscard(discardTarget.status);
|
|
123
|
+
const body = await runGatedMutation("workerSchedulerDiscard", {
|
|
124
|
+
scheduleId: discardTarget.scheduleId,
|
|
125
|
+
reason,
|
|
126
|
+
...(needsForce || forceDiscard ? { force: true } : {}),
|
|
127
|
+
}, "night-discard");
|
|
128
|
+
if (!isActionSuccess(body))
|
|
129
|
+
return;
|
|
130
|
+
setDiscardTarget(null);
|
|
131
|
+
setDiscardReason("");
|
|
132
|
+
setForceDiscard(false);
|
|
133
|
+
await refreshBoard();
|
|
134
|
+
}, [
|
|
135
|
+
discardReason,
|
|
136
|
+
discardTarget,
|
|
137
|
+
forceDiscard,
|
|
138
|
+
isActionSuccess,
|
|
139
|
+
refreshBoard,
|
|
140
|
+
runGatedMutation,
|
|
141
|
+
]);
|
|
142
|
+
const openDiscard = useCallback((row) => {
|
|
143
|
+
setDiscardTarget(row);
|
|
144
|
+
setForceDiscard(nightRowNeedsForceDiscard(row.status));
|
|
145
|
+
setDiscardReason("");
|
|
146
|
+
}, []);
|
|
147
|
+
return {
|
|
148
|
+
phase,
|
|
149
|
+
selectedKey,
|
|
150
|
+
setSelectedKey,
|
|
151
|
+
nightAt,
|
|
152
|
+
setNightAt,
|
|
153
|
+
taskCard,
|
|
154
|
+
setTaskCard,
|
|
155
|
+
review,
|
|
156
|
+
discardReason,
|
|
157
|
+
setDiscardReason,
|
|
158
|
+
discardTarget,
|
|
159
|
+
setDiscardTarget,
|
|
160
|
+
forceDiscard,
|
|
161
|
+
setForceDiscard,
|
|
162
|
+
selectableTasks,
|
|
163
|
+
derivedFeatureDir,
|
|
164
|
+
onPrepare,
|
|
165
|
+
onConfirmAdd,
|
|
166
|
+
onCancelWizard,
|
|
167
|
+
onHarvest,
|
|
168
|
+
onDiscard,
|
|
169
|
+
openDiscard,
|
|
170
|
+
};
|
|
171
|
+
}
|