@fieldwangai/agentflow 0.1.137 → 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 +139 -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 +721 -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-ubxHcM7p.js +6 -0
- 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-CQsrSc3u.css +0 -1
- package/builtin/web-ui/dist/assets/index-DQvqqAeQ.js +0 -590
|
@@ -4,7 +4,7 @@ import path from "path";
|
|
|
4
4
|
import { getAgentflowDataRoot } from "./paths.mjs";
|
|
5
5
|
import { getTeamForUser } from "./teams.mjs";
|
|
6
6
|
|
|
7
|
-
const REGISTRY_VERSION =
|
|
7
|
+
const REGISTRY_VERSION = 4;
|
|
8
8
|
|
|
9
9
|
function registryPath() {
|
|
10
10
|
return path.join(getAgentflowDataRoot(), "collaboration", "prd-workflows.json");
|
|
@@ -63,6 +63,48 @@ function normalizedMemberMap(value) {
|
|
|
63
63
|
.filter(([userId, role]) => userId && role));
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
function normalizeKnowledgeBindings(value) {
|
|
67
|
+
if (!Array.isArray(value)) return [];
|
|
68
|
+
const seen = new Set();
|
|
69
|
+
return value.flatMap((entry) => {
|
|
70
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
|
|
71
|
+
const workspaceId = String(entry.workspaceId || entry.id || "").trim();
|
|
72
|
+
if (!workspaceId || seen.has(workspaceId)) return [];
|
|
73
|
+
seen.add(workspaceId);
|
|
74
|
+
return [{
|
|
75
|
+
workspaceId,
|
|
76
|
+
label: String(entry.label || workspaceId).trim() || workspaceId,
|
|
77
|
+
kind: String(entry.kind || "git").trim().toLowerCase(),
|
|
78
|
+
type: String(entry.type || "knowledge").trim().toLowerCase(),
|
|
79
|
+
branch: String(entry.branch || "").trim(),
|
|
80
|
+
boundBy: normalizeUserId(entry.boundBy),
|
|
81
|
+
boundAt: String(entry.boundAt || "").trim(),
|
|
82
|
+
}];
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizeProjectBindings(value) {
|
|
87
|
+
if (!Array.isArray(value)) return [];
|
|
88
|
+
const seen = new Set();
|
|
89
|
+
return value.flatMap((entry) => {
|
|
90
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
|
|
91
|
+
const workspaceId = String(entry.workspaceId || "").trim();
|
|
92
|
+
const flowId = String(entry.flowId || "").trim();
|
|
93
|
+
const flowSource = String(entry.flowSource || "user").trim() || "user";
|
|
94
|
+
if (!workspaceId || !flowId || seen.has(workspaceId)) return [];
|
|
95
|
+
seen.add(workspaceId);
|
|
96
|
+
return [{
|
|
97
|
+
workspaceId,
|
|
98
|
+
flowId,
|
|
99
|
+
flowSource,
|
|
100
|
+
archived: entry.archived === true,
|
|
101
|
+
ownerId: normalizeUserId(entry.ownerId),
|
|
102
|
+
boundBy: normalizeUserId(entry.boundBy),
|
|
103
|
+
boundAt: String(entry.boundAt || "").trim(),
|
|
104
|
+
}];
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
66
108
|
function collaborationMembers(record) {
|
|
67
109
|
const ownerId = normalizeUserId(record?.ownerId);
|
|
68
110
|
const explicit = normalizedMemberMap(record?.members);
|
|
@@ -107,6 +149,8 @@ function publicWorkflow(record, userId = "") {
|
|
|
107
149
|
? record.authority.unresolvedParticipants.map((value) => String(value || "")).filter(Boolean)
|
|
108
150
|
: [],
|
|
109
151
|
} : null,
|
|
152
|
+
knowledgeBindings: normalizeKnowledgeBindings(record.knowledgeBindings),
|
|
153
|
+
projectBindingCount: normalizeProjectBindings(record.projectBindings).length,
|
|
110
154
|
shareActive: Boolean(record.shareToken),
|
|
111
155
|
shareCreatedAt: record.shareCreatedAt || "",
|
|
112
156
|
createdAt: record.createdAt || "",
|
|
@@ -232,6 +276,100 @@ export function prdWorkflowCollaborationSummary(record, userId) {
|
|
|
232
276
|
return publicWorkflow(record, userId);
|
|
233
277
|
}
|
|
234
278
|
|
|
279
|
+
export function setPrdWorkflowKnowledgeBindings({ tapdId, userId, bindings = [] }) {
|
|
280
|
+
const record = getPrdWorkflowCollaborationForUser(tapdId, userId);
|
|
281
|
+
if (!record) return { error: "PRD Workflow collaboration not found", status: 404 };
|
|
282
|
+
const actorId = normalizeUserId(userId);
|
|
283
|
+
if (prdWorkflowCollaborationAccess(record, actorId).role !== "owner") {
|
|
284
|
+
return { error: "Only the Workflow owner can manage knowledge bindings", status: 403 };
|
|
285
|
+
}
|
|
286
|
+
const registry = readRegistry();
|
|
287
|
+
const stored = registry.workflows[record.id];
|
|
288
|
+
if (!stored) return { error: "PRD Workflow collaboration not found", status: 404 };
|
|
289
|
+
const now = new Date().toISOString();
|
|
290
|
+
stored.knowledgeBindings = normalizeKnowledgeBindings(bindings).map((binding) => ({
|
|
291
|
+
...binding,
|
|
292
|
+
boundBy: actorId,
|
|
293
|
+
boundAt: binding.boundAt || now,
|
|
294
|
+
}));
|
|
295
|
+
stored.updatedAt = now;
|
|
296
|
+
writeRegistry(registry);
|
|
297
|
+
return {
|
|
298
|
+
record: stored,
|
|
299
|
+
workflow: publicWorkflow(stored, actorId),
|
|
300
|
+
knowledgeBindings: normalizeKnowledgeBindings(stored.knowledgeBindings),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function listPrdWorkflowProjectBindings({ tapdId, userId }) {
|
|
305
|
+
const record = getPrdWorkflowCollaborationForUser(tapdId, userId);
|
|
306
|
+
if (!record) return { error: "PRD Workflow collaboration not found", status: 404 };
|
|
307
|
+
return {
|
|
308
|
+
record,
|
|
309
|
+
workflow: publicWorkflow(record, userId),
|
|
310
|
+
projectBindings: normalizeProjectBindings(record.projectBindings),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function bindPrdWorkflowProject({ tapdId, userId, project }) {
|
|
315
|
+
const record = getPrdWorkflowCollaborationForUser(tapdId, userId);
|
|
316
|
+
if (!record) return { error: "PRD Workflow collaboration not found", status: 404 };
|
|
317
|
+
const normalized = normalizeProjectBindings([project])[0];
|
|
318
|
+
if (!normalized) return { error: "Project binding requires workspaceId and flowId", status: 400 };
|
|
319
|
+
const registry = readRegistry();
|
|
320
|
+
const stored = registry.workflows[record.id];
|
|
321
|
+
if (!stored) return { error: "PRD Workflow collaboration not found", status: 404 };
|
|
322
|
+
const actorId = normalizeUserId(userId);
|
|
323
|
+
const now = new Date().toISOString();
|
|
324
|
+
const current = normalizeProjectBindings(stored.projectBindings);
|
|
325
|
+
const existing = current.find((binding) => binding.workspaceId === normalized.workspaceId);
|
|
326
|
+
stored.projectBindings = [
|
|
327
|
+
...current.filter((binding) => binding.workspaceId !== normalized.workspaceId),
|
|
328
|
+
{
|
|
329
|
+
...normalized,
|
|
330
|
+
boundBy: existing?.boundBy || actorId,
|
|
331
|
+
boundAt: existing?.boundAt || now,
|
|
332
|
+
},
|
|
333
|
+
];
|
|
334
|
+
stored.updatedAt = now;
|
|
335
|
+
writeRegistry(registry);
|
|
336
|
+
return {
|
|
337
|
+
record: stored,
|
|
338
|
+
workflow: publicWorkflow(stored, actorId),
|
|
339
|
+
projectBindings: normalizeProjectBindings(stored.projectBindings),
|
|
340
|
+
created: !existing,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function unbindPrdWorkflowProject({ tapdId, userId, workspaceId }) {
|
|
345
|
+
const record = getPrdWorkflowCollaborationForUser(tapdId, userId);
|
|
346
|
+
if (!record) return { error: "PRD Workflow collaboration not found", status: 404 };
|
|
347
|
+
const id = String(workspaceId || "").trim();
|
|
348
|
+
if (!id) return { error: "Missing workspaceId", status: 400 };
|
|
349
|
+
const registry = readRegistry();
|
|
350
|
+
const stored = registry.workflows[record.id];
|
|
351
|
+
if (!stored) return { error: "PRD Workflow collaboration not found", status: 404 };
|
|
352
|
+
const current = normalizeProjectBindings(stored.projectBindings);
|
|
353
|
+
const next = current.filter((binding) => binding.workspaceId !== id);
|
|
354
|
+
if (next.length === current.length) {
|
|
355
|
+
return {
|
|
356
|
+
record: stored,
|
|
357
|
+
workflow: publicWorkflow(stored, userId),
|
|
358
|
+
projectBindings: current,
|
|
359
|
+
unchanged: true,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
stored.projectBindings = next;
|
|
363
|
+
stored.updatedAt = new Date().toISOString();
|
|
364
|
+
writeRegistry(registry);
|
|
365
|
+
return {
|
|
366
|
+
record: stored,
|
|
367
|
+
workflow: publicWorkflow(stored, userId),
|
|
368
|
+
projectBindings: next,
|
|
369
|
+
removedWorkspaceId: id,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
235
373
|
export function ensurePrdWorkflowShareLink({ tapdId, userId }) {
|
|
236
374
|
const ensured = ensurePrdWorkflowCollaboration({ tapdId, userId });
|
|
237
375
|
if (ensured.error) return ensured;
|
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}`);
|