@fieldwangai/agentflow 0.1.138 → 0.1.141
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/bin/lib/catalog-flows.mjs +17 -3
- package/bin/lib/composer-node-schema.mjs +4 -0
- package/bin/lib/i18n.mjs +2 -2
- package/bin/lib/jenkins.mjs +380 -0
- package/bin/lib/locales/en.json +22 -0
- package/bin/lib/locales/zh.json +22 -0
- package/bin/lib/paths.mjs +1 -0
- package/bin/lib/prd-workflow-collaboration.mjs +93 -1
- package/bin/lib/recent-runs.mjs +16 -0
- package/bin/lib/run-node-statuses-from-disk.mjs +41 -3
- package/bin/lib/scheduler.mjs +59 -25
- package/bin/lib/ui-server.mjs +363 -63
- package/bin/pipeline/pre-process-node.mjs +122 -11
- package/bin/pipeline/run-log.mjs +2 -2
- package/bin/pipeline/write-result.mjs +4 -4
- package/builtin/nodes/tool_jenkins_build.md +64 -0
- package/builtin/pipelines/jenkins-build-notify/flow.yaml +217 -0
- package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-B3thaqH2.js → WorkflowAssistantThread-ubxHcM7p.js} +1 -1
- package/builtin/web-ui/dist/assets/index-B6TWUomI.css +1 -0
- package/builtin/web-ui/dist/assets/index-DQzcZp7S.js +590 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/agentflow-workflow-report/SKILL.md +3 -0
- package/skills/agentflow-workflow-report/references/protocol.md +8 -5
- package/builtin/web-ui/dist/assets/index-COX1zMwq.css +0 -1
- package/builtin/web-ui/dist/assets/index-YS4XOpXF.js +0 -590
package/bin/lib/recent-runs.mjs
CHANGED
|
@@ -9,6 +9,21 @@ import { isApplyProcessAlive } from "./run-apply-active-lock.mjs";
|
|
|
9
9
|
/** Web UI 调用 /api/flow/run/stop 时写入,用于与「未跑完但未标记」区分 */
|
|
10
10
|
export const RUN_INTERRUPTED_FILENAME = "run-interrupted.json";
|
|
11
11
|
|
|
12
|
+
function hasActiveDurableWait(runDir) {
|
|
13
|
+
const paths = [path.join(runDir, "wait-states.json"), path.join(runDir, "wait-state.json")];
|
|
14
|
+
for (const filePath of paths) {
|
|
15
|
+
if (!fs.existsSync(filePath)) continue;
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
18
|
+
const waits = Array.isArray(parsed?.waits) ? parsed.waits : parsed && typeof parsed === "object" ? [parsed] : [];
|
|
19
|
+
if (waits.some((wait) => wait && (wait.status === "waiting" || wait.status === "resuming"))) return true;
|
|
20
|
+
} catch {
|
|
21
|
+
/* ignore corrupt wait files */
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
12
27
|
/** @param {string} filePath */
|
|
13
28
|
function parseResultStatusFromFile(filePath) {
|
|
14
29
|
try {
|
|
@@ -112,6 +127,7 @@ function inferRunStatusFromRunDir(runDir) {
|
|
|
112
127
|
|
|
113
128
|
if (anyResult || fs.existsSync(flowJsonPath)) {
|
|
114
129
|
if (isApplyProcessAlive(runDir)) return "running";
|
|
130
|
+
if (hasActiveDurableWait(runDir)) return "running";
|
|
115
131
|
return "interrupted";
|
|
116
132
|
}
|
|
117
133
|
return "unknown";
|
|
@@ -17,6 +17,28 @@ function parseResultStatus(filePath) {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function readJenkinsState(runDir, instanceId) {
|
|
21
|
+
const statePath = path.join(runDir, "state", `${instanceId}.jenkins.json`);
|
|
22
|
+
if (!fs.existsSync(statePath)) return null;
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(fs.readFileSync(statePath, "utf-8"));
|
|
25
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function jenkinsUiStatus(executionStatus, state) {
|
|
32
|
+
if (!state) return executionStatus;
|
|
33
|
+
if (executionStatus === "pending" || state.phase === "queued" || state.phase === "running" || state.phase === "triggering") {
|
|
34
|
+
return "waiting";
|
|
35
|
+
}
|
|
36
|
+
if (executionStatus === "success" && state.phase === "complete") {
|
|
37
|
+
return String(state.status || "").toUpperCase() === "SUCCESS" ? "success" : "outcome_failed";
|
|
38
|
+
}
|
|
39
|
+
return executionStatus;
|
|
40
|
+
}
|
|
41
|
+
|
|
20
42
|
/** @param {string} filePath @returns {number | null} */
|
|
21
43
|
function parseElapsedMsLine(filePath) {
|
|
22
44
|
try {
|
|
@@ -34,7 +56,7 @@ function parseElapsedMsLine(filePath) {
|
|
|
34
56
|
* @param {string} workspaceRoot
|
|
35
57
|
* @param {string} flowName
|
|
36
58
|
* @param {string} uuid
|
|
37
|
-
* @returns {Record<string, { status: string, elapsed?: string }>}
|
|
59
|
+
* @returns {Record<string, { status: string, elapsed?: string, executionStatus?: string, phase?: string, jenkinsStatus?: string, message?: string, buildNumber?: string, url?: string, qrUrl?: string, startedAt?: string, wakeAt?: string }>}
|
|
38
60
|
*/
|
|
39
61
|
export function getRunNodeStatusesFromDisk(workspaceRoot, flowName, uuid, opts = {}) {
|
|
40
62
|
const runDir = getRunDir(workspaceRoot, flowName, uuid, opts);
|
|
@@ -70,9 +92,25 @@ export function getRunNodeStatusesFromDisk(workspaceRoot, flowName, uuid, opts =
|
|
|
70
92
|
const low = String(status).toLowerCase();
|
|
71
93
|
if (low === "completed" || low === "done") uiStatus = "success";
|
|
72
94
|
|
|
73
|
-
|
|
95
|
+
const jenkinsState = defId === "tool_jenkins_build" ? readJenkinsState(runDir, instanceId) : null;
|
|
96
|
+
if (jenkinsState) uiStatus = jenkinsUiStatus(uiStatus, jenkinsState);
|
|
97
|
+
|
|
98
|
+
/** @type {{ status: string, elapsed?: string, executionStatus?: string, phase?: string, jenkinsStatus?: string, message?: string, buildNumber?: string, url?: string, qrUrl?: string, startedAt?: string, wakeAt?: string }} */
|
|
74
99
|
const row = { status: uiStatus };
|
|
75
|
-
if (
|
|
100
|
+
if (jenkinsState) {
|
|
101
|
+
row.executionStatus = status;
|
|
102
|
+
row.phase = String(jenkinsState.phase || "");
|
|
103
|
+
row.jenkinsStatus = String(jenkinsState.status || "");
|
|
104
|
+
row.message = String(jenkinsState.message || "");
|
|
105
|
+
row.buildNumber = String(jenkinsState.buildNumber || "");
|
|
106
|
+
row.url = String(jenkinsState.url || jenkinsState.buildUrl || "");
|
|
107
|
+
row.qrUrl = String(jenkinsState.qrUrl || "");
|
|
108
|
+
row.startedAt = String(jenkinsState.startedAt || "");
|
|
109
|
+
row.wakeAt = String(jenkinsState.wakeAt || "");
|
|
110
|
+
const started = Date.parse(jenkinsState.startedAt || "");
|
|
111
|
+
const ended = Date.parse(jenkinsState.completedAt || "");
|
|
112
|
+
if (Number.isFinite(started) && Number.isFinite(ended) && ended >= started) row.elapsed = formatDuration(ended - started);
|
|
113
|
+
} else if (uiStatus === "success" && fs.existsSync(resultPath)) {
|
|
76
114
|
const ms = parseElapsedMsLine(resultPath);
|
|
77
115
|
if (ms != null && ms > 0) {
|
|
78
116
|
row.elapsed = formatDuration(ms);
|
package/bin/lib/scheduler.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
readScheduleState,
|
|
10
10
|
writeScheduleState,
|
|
11
11
|
} from "./schedule-config.mjs";
|
|
12
|
-
import { getAgentflowUserContexts, getRunDir, PACKAGE_ROOT } from "./paths.mjs";
|
|
12
|
+
import { getAgentflowUserContexts, getFlowRuntimeRoot, getRunDir, PACKAGE_ROOT } from "./paths.mjs";
|
|
13
13
|
import { isApplyProcessAlive } from "./run-apply-active-lock.mjs";
|
|
14
14
|
import { log } from "./log.mjs";
|
|
15
15
|
import { readMergedEnvObject } from "./user-env.mjs";
|
|
@@ -112,17 +112,28 @@ function getLatestRunUuidForFlow(workspaceRoot, flowId, opts = {}) {
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
function listRunDirsForFlow(flow) {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
115
|
+
function listRunDirsForFlow(workspaceRoot, flow, opts = {}) {
|
|
116
|
+
const roots = [
|
|
117
|
+
flow.path ? path.join(flow.path, "runBuild") : "",
|
|
118
|
+
path.join(getFlowRuntimeRoot(workspaceRoot, flow.id, opts), "runBuild"),
|
|
119
|
+
].filter(Boolean);
|
|
120
|
+
const seen = new Set();
|
|
121
|
+
const runs = [];
|
|
122
|
+
for (const runRoot of roots) {
|
|
123
|
+
const resolved = path.resolve(runRoot);
|
|
124
|
+
if (seen.has(resolved) || !fs.existsSync(resolved)) continue;
|
|
125
|
+
seen.add(resolved);
|
|
126
|
+
try {
|
|
127
|
+
for (const entry of fs.readdirSync(resolved, { withFileTypes: true })) {
|
|
128
|
+
if (entry.isDirectory() && /^\d{14}$/.test(entry.name)) {
|
|
129
|
+
runs.push({ uuid: entry.name, runDir: path.join(resolved, entry.name) });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
/* ignore unreadable run roots */
|
|
134
|
+
}
|
|
125
135
|
}
|
|
136
|
+
return runs.sort((a, b) => b.uuid.localeCompare(a.uuid));
|
|
126
137
|
}
|
|
127
138
|
|
|
128
139
|
function readJsonObject(filePath) {
|
|
@@ -317,12 +328,23 @@ function startWaitingRunResume(workspaceRoot, flow, waitState, opts = {}) {
|
|
|
317
328
|
const agentflowBin = path.join(PACKAGE_ROOT, "bin", "agentflow.mjs");
|
|
318
329
|
const uuid = String(waitState.uuid || "");
|
|
319
330
|
const instanceId = String(waitState.instanceId || "");
|
|
331
|
+
const rerunCurrentNode = waitState.resumeMode === "rerun";
|
|
332
|
+
if (rerunCurrentNode) {
|
|
333
|
+
writeResult(
|
|
334
|
+
workspaceRoot,
|
|
335
|
+
flow.id,
|
|
336
|
+
uuid,
|
|
337
|
+
instanceId,
|
|
338
|
+
{ status: "cache_not_met", message: "后台任务到期,重新检查外部状态" },
|
|
339
|
+
{ execId: Number(waitState.execId) || undefined, preserveBody: true, runDir: waitState.runDir },
|
|
340
|
+
);
|
|
341
|
+
}
|
|
320
342
|
const args = [
|
|
321
343
|
agentflowBin,
|
|
322
|
-
"resume",
|
|
344
|
+
rerunCurrentNode ? "apply" : "resume",
|
|
323
345
|
flow.id,
|
|
324
346
|
uuid,
|
|
325
|
-
instanceId,
|
|
347
|
+
...(rerunCurrentNode ? [] : [instanceId]),
|
|
326
348
|
"--machine-readable",
|
|
327
349
|
"--workspace-root",
|
|
328
350
|
path.resolve(workspaceRoot),
|
|
@@ -356,9 +378,9 @@ function startWaitingRunResume(workspaceRoot, flow, waitState, opts = {}) {
|
|
|
356
378
|
return child;
|
|
357
379
|
}
|
|
358
380
|
|
|
359
|
-
function countActiveWaitsForFlow(flow) {
|
|
381
|
+
function countActiveWaitsForFlow(workspaceRoot, flow, opts = {}) {
|
|
360
382
|
let count = 0;
|
|
361
|
-
for (const run of listRunDirsForFlow(flow)) {
|
|
383
|
+
for (const run of listRunDirsForFlow(workspaceRoot, flow, opts)) {
|
|
362
384
|
for (const waitState of readWaitStates(run.runDir)) {
|
|
363
385
|
if (waitState && (waitState.status === "waiting" || waitState.status === "resuming")) count += 1;
|
|
364
386
|
}
|
|
@@ -378,10 +400,10 @@ function hasNodeBranchEdge(runDir, instanceId, branchName) {
|
|
|
378
400
|
return flow.edges.some((e) => e && e.source === instanceId && (e.sourceHandle || "output-0") === sourceHandle);
|
|
379
401
|
}
|
|
380
402
|
|
|
381
|
-
export function cancelScheduledRun(workspaceRoot, flowId, uuid) {
|
|
382
|
-
const flow = listFlowsJson(workspaceRoot).find((f) => f.id === flowId && !f.archived
|
|
403
|
+
export function cancelScheduledRun(workspaceRoot, flowId, uuid, opts = {}) {
|
|
404
|
+
const flow = listFlowsJson(workspaceRoot, opts).find((f) => f.id === flowId && !f.archived);
|
|
383
405
|
if (!flow) return { ok: false, error: `flow not found: ${flowId}` };
|
|
384
|
-
const runDir = getRunDir(workspaceRoot, flow.id, uuid);
|
|
406
|
+
const runDir = getRunDir(workspaceRoot, flow.id, uuid, opts);
|
|
385
407
|
if (!fs.existsSync(runDir)) return { ok: false, error: `run not found: ${flowId}/${uuid}` };
|
|
386
408
|
const cancelledAt = new Date().toISOString();
|
|
387
409
|
fs.writeFileSync(path.join(runDir, "cancelled.json"), JSON.stringify({ cancelled: true, cancelledAt }, null, 2) + "\n", "utf-8");
|
|
@@ -396,16 +418,22 @@ export function cancelScheduledRun(workspaceRoot, flowId, uuid) {
|
|
|
396
418
|
instanceId &&
|
|
397
419
|
hasNodeBranchEdge(runDir, instanceId, "cancelled") &&
|
|
398
420
|
!resumePid;
|
|
399
|
-
|
|
421
|
+
const canRerunForCancellation = waitState.resumeMode === "rerun" && instanceId && !resumePid;
|
|
422
|
+
if (canRerunForCancellation) {
|
|
423
|
+
const child = startWaitingRunResume(workspaceRoot, flow, { ...waitState, uuid, runDir, branch: "cancelled" }, opts);
|
|
424
|
+
resumePid = child.pid || null;
|
|
425
|
+
writeWaitState(waitState, { status: "resuming", branch: "cancelled", cancelledAt, resumePid, resumeStartedAt: cancelledAt });
|
|
426
|
+
propagated += 1;
|
|
427
|
+
} else if (canPropagate) {
|
|
400
428
|
writeResult(
|
|
401
429
|
workspaceRoot,
|
|
402
430
|
flow.id,
|
|
403
431
|
uuid,
|
|
404
432
|
instanceId,
|
|
405
433
|
{ status: "success", message: "已取消", branch: "cancelled" },
|
|
406
|
-
{ execId: Number(waitState.execId) || undefined, preserveBody: false },
|
|
434
|
+
{ execId: Number(waitState.execId) || undefined, preserveBody: false, runDir },
|
|
407
435
|
);
|
|
408
|
-
const child = startWaitingRunResume(workspaceRoot, flow, { ...waitState, uuid, runDir, branch: "cancelled" });
|
|
436
|
+
const child = startWaitingRunResume(workspaceRoot, flow, { ...waitState, uuid, runDir, branch: "cancelled" }, opts);
|
|
409
437
|
resumePid = child.pid || null;
|
|
410
438
|
writeWaitState(waitState, { status: "resuming", branch: "cancelled", cancelledAt, resumePid, resumeStartedAt: cancelledAt });
|
|
411
439
|
propagated += 1;
|
|
@@ -443,7 +471,7 @@ export function listScheduleStatuses(workspaceRoot, opts = {}) {
|
|
|
443
471
|
? "workspace flow is shadowed by a user flow with the same id"
|
|
444
472
|
: state.lastError || "",
|
|
445
473
|
running: isFlowCurrentlyRunning(workspaceRoot, flow.id, state, opts),
|
|
446
|
-
waiting: countActiveWaitsForFlow(flow),
|
|
474
|
+
waiting: countActiveWaitsForFlow(workspaceRoot, flow, opts),
|
|
447
475
|
});
|
|
448
476
|
}
|
|
449
477
|
rows.sort((a, b) => {
|
|
@@ -463,17 +491,21 @@ export async function startScheduler(workspaceRoot, opts = {}) {
|
|
|
463
491
|
const contexts = opts.userId ? [{ userId: opts.userId }] : getAgentflowUserContexts();
|
|
464
492
|
for (const scheduleCtx of contexts) {
|
|
465
493
|
for (const flow of listFlowsJson(workspaceRoot, scheduleCtx)) {
|
|
466
|
-
if (flow.archived
|
|
494
|
+
if (flow.archived) continue;
|
|
467
495
|
const flowSource = flow.source || "user";
|
|
468
496
|
let resumedWaitingRun = false;
|
|
469
|
-
for (const run of listRunDirsForFlow(flow)) {
|
|
497
|
+
for (const run of listRunDirsForFlow(workspaceRoot, flow, scheduleCtx)) {
|
|
470
498
|
if (resumedWaitingRun) break;
|
|
471
499
|
for (const waitState of readWaitStates(run.runDir)) {
|
|
472
500
|
if (!waitState || !waitState.wakeAt || !waitState.instanceId) continue;
|
|
473
501
|
if (waitState.status === "resuming" && !isFlowCurrentlyRunning(workspaceRoot, flow.id, { lastRunUuid: run.uuid }, scheduleCtx)) {
|
|
474
502
|
const nodeStatus = readNodeResultStatus(run.runDir, String(waitState.instanceId));
|
|
475
503
|
writeWaitState(waitState, {
|
|
476
|
-
status:
|
|
504
|
+
status:
|
|
505
|
+
nodeStatus === "pending" ||
|
|
506
|
+
(waitState.resumeMode === "rerun" && nodeStatus !== "success" && nodeStatus !== "failed")
|
|
507
|
+
? "waiting"
|
|
508
|
+
: "resumed",
|
|
477
509
|
reconciledAt: new Date().toISOString(),
|
|
478
510
|
});
|
|
479
511
|
continue;
|
|
@@ -505,6 +537,8 @@ export async function startScheduler(workspaceRoot, opts = {}) {
|
|
|
505
537
|
}
|
|
506
538
|
}
|
|
507
539
|
|
|
540
|
+
if (flow.source === "builtin") continue;
|
|
541
|
+
|
|
508
542
|
const scheduleRes = readFlowSchedule(workspaceRoot, flow.id, flowSource, scheduleCtx);
|
|
509
543
|
if (!scheduleRes.success) {
|
|
510
544
|
log.debug(`[scheduler] ${flow.id}: ${scheduleRes.error}`);
|