@fieldwangai/agentflow 0.1.26 → 0.1.28
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/apply.mjs +18 -1
- package/bin/lib/catalog-flows.mjs +65 -0
- package/bin/lib/composer-skill-router.mjs +117 -0
- package/bin/lib/help.mjs +8 -0
- package/bin/lib/locales/en.json +21 -1
- package/bin/lib/locales/zh.json +21 -1
- package/bin/lib/main.mjs +56 -3
- package/bin/lib/marketplace.mjs +542 -0
- package/bin/lib/paths.mjs +7 -0
- package/bin/lib/scheduler.mjs +246 -0
- package/bin/lib/ui-server.mjs +117 -55
- package/bin/pipeline/build-node-prompt.mjs +43 -2
- package/bin/pipeline/get-ready-nodes.mjs +2 -2
- package/bin/pipeline/get-resolved-values.mjs +6 -0
- package/bin/pipeline/parse-flow.mjs +15 -5
- package/bin/pipeline/pre-process-node.mjs +336 -1
- package/builtin/nodes/control_cancelled.md +20 -0
- package/builtin/nodes/control_deadline.md +32 -0
- package/builtin/nodes/control_delay.md +20 -0
- package/builtin/nodes/control_interval_loop.md +53 -0
- package/builtin/nodes/control_wait_until.md +23 -0
- package/builtin/web-ui/dist/assets/index-BeUBxIj1.js +190 -0
- package/builtin/web-ui/dist/assets/index-BzhdjOzb.css +1 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-CZkUPcXE.css +0 -1
- package/builtin/web-ui/dist/assets/index-D4949pHe.js +0 -190
|
@@ -10,6 +10,7 @@ import fs from "fs";
|
|
|
10
10
|
import path from "path";
|
|
11
11
|
|
|
12
12
|
import { getRunDir, LEGACY_NODES_DIR, PIPELINES_DIR, PROJECT_NODES_DIR } from "../lib/paths.mjs";
|
|
13
|
+
import { isMarketplaceDefinitionId, resolveMarketplaceNodePackage } from "../lib/marketplace.mjs";
|
|
13
14
|
import { getFlowDir } from "../lib/workspace.mjs";
|
|
14
15
|
import { fileURLToPath } from "url";
|
|
15
16
|
|
|
@@ -102,6 +103,11 @@ function extractDescriptionFromFrontmatter(frontmatter) {
|
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
function readNodeDescription(workspaceRoot, flowDir, definitionId) {
|
|
106
|
+
if (isMarketplaceDefinitionId(definitionId)) {
|
|
107
|
+
const flowData = loadFlowDefinition(flowDir);
|
|
108
|
+
const resolved = resolveMarketplaceNodePackage(workspaceRoot, flowDir, definitionId, flowData);
|
|
109
|
+
if (resolved?.description) return resolved.description;
|
|
110
|
+
}
|
|
105
111
|
const fileName = definitionId.endsWith(".md") ? definitionId : `${definitionId}.md`;
|
|
106
112
|
const flowNodesPath = path.join(flowDir, "nodes", fileName);
|
|
107
113
|
const projectNodesNew = path.join(workspaceRoot, PROJECT_NODES_DIR, fileName);
|
|
@@ -14,6 +14,7 @@ import yaml from "js-yaml";
|
|
|
14
14
|
|
|
15
15
|
import { getRunDir, LEGACY_NODES_DIR, PIPELINES_DIR, PROJECT_NODES_DIR } from "../lib/paths.mjs";
|
|
16
16
|
import { getFlowDir } from "../lib/workspace.mjs";
|
|
17
|
+
import { isMarketplaceDefinitionId, resolveMarketplaceNodePackage, writeFlowMarketplaceLock } from "../lib/marketplace.mjs";
|
|
17
18
|
import { loadAllExecIds, latestResultExecId, intermediateResultBasename, intermediateDirForNode, outputDirForNode } from "./get-exec-id.mjs";
|
|
18
19
|
|
|
19
20
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -39,6 +40,7 @@ function loadFlowDefinition(flowDir) {
|
|
|
39
40
|
instances: data.instances && typeof data.instances === "object" ? data.instances : {},
|
|
40
41
|
edges,
|
|
41
42
|
ui: data.ui && typeof data.ui === "object" ? data.ui : {},
|
|
43
|
+
dependencies: data.dependencies && typeof data.dependencies === "object" ? data.dependencies : {},
|
|
42
44
|
};
|
|
43
45
|
} catch {
|
|
44
46
|
return null;
|
|
@@ -48,6 +50,7 @@ function loadFlowDefinition(flowDir) {
|
|
|
48
50
|
/** 由 definitionId 前缀推导 type */
|
|
49
51
|
function definitionIdToType(definitionId) {
|
|
50
52
|
const id = (definitionId || "").toLowerCase();
|
|
53
|
+
if (id.startsWith("marketplace:")) return "agent";
|
|
51
54
|
if (id.startsWith("control_")) return "control";
|
|
52
55
|
if (id.startsWith("agent_")) return "agent";
|
|
53
56
|
if (id.startsWith("provide_")) return "provide";
|
|
@@ -62,8 +65,14 @@ function definitionIdToType(definitionId) {
|
|
|
62
65
|
* @param {string} definitionName - 实例中引用的定义名(如 user_confirm_scope)
|
|
63
66
|
* @returns {{ definitionId: string, definitionName: string }}
|
|
64
67
|
*/
|
|
65
|
-
function resolveDefinitionIdFromNodeClass(flowDir, definitionName) {
|
|
66
|
-
|
|
68
|
+
function resolveDefinitionIdFromNodeClass(flowDir, definitionName, workspaceRoot, flowData) {
|
|
69
|
+
if (isMarketplaceDefinitionId(definitionName)) {
|
|
70
|
+
const resolved = resolveMarketplaceNodePackage(workspaceRoot, flowDir, definitionName, flowData);
|
|
71
|
+
return {
|
|
72
|
+
definitionId: resolved?.resolvedDefinitionId || definitionName,
|
|
73
|
+
definitionName,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
67
76
|
const fileName = definitionName.endsWith(".md") ? definitionName : `${definitionName}.md`;
|
|
68
77
|
const flowNodesPath = path.join(flowDir, "nodes", fileName);
|
|
69
78
|
const projectNodesNew = path.join(workspaceRoot, PROJECT_NODES_DIR, fileName);
|
|
@@ -85,7 +94,7 @@ function resolveDefinitionIdFromNodeClass(flowDir, definitionName) {
|
|
|
85
94
|
}
|
|
86
95
|
|
|
87
96
|
/** 从 loadFlowDefinition 结果得到 nodes 和 edges(与 readFlowMd 输出形状一致) */
|
|
88
|
-
function readFlowFromYaml(flowDir) {
|
|
97
|
+
function readFlowFromYaml(flowDir, workspaceRoot = path.resolve(flowDir, "..", "..", "..", "..")) {
|
|
89
98
|
const def = loadFlowDefinition(flowDir);
|
|
90
99
|
if (!def) return { nodes: [], edges: [] };
|
|
91
100
|
const instances = def.instances;
|
|
@@ -98,7 +107,7 @@ function readFlowFromYaml(flowDir) {
|
|
|
98
107
|
const nodes = Array.from(nodeIds).map((id) => {
|
|
99
108
|
const inst = instances[id] || {};
|
|
100
109
|
const definitionName = inst.definitionId ?? id;
|
|
101
|
-
const { definitionId } = resolveDefinitionIdFromNodeClass(flowDir, definitionName);
|
|
110
|
+
const { definitionId } = resolveDefinitionIdFromNodeClass(flowDir, definitionName, workspaceRoot, def);
|
|
102
111
|
const type = definitionIdToType(definitionId);
|
|
103
112
|
const label = inst.label != null ? String(inst.label) : id;
|
|
104
113
|
const role =
|
|
@@ -613,7 +622,8 @@ function main() {
|
|
|
613
622
|
process.exit(1);
|
|
614
623
|
}
|
|
615
624
|
try {
|
|
616
|
-
|
|
625
|
+
writeFlowMarketplaceLock(workspaceRoot || path.resolve(flowDir, "..", "..", "..", ".."), flowDir, flowData);
|
|
626
|
+
const { nodes, edges } = readFlowFromYaml(flowDir, workspaceRoot || path.resolve(flowDir, "..", "..", "..", ".."));
|
|
617
627
|
const { order: topoOrder, hasCycle } = topoSort(nodes, edges);
|
|
618
628
|
const order = hasCycle ? nodes.map((n) => n.id) : topoOrder;
|
|
619
629
|
const cycleNodes = hasCycle ? Array.from(findCycleNodes(nodes, edges)) : [];
|
|
@@ -124,6 +124,153 @@ function bashSingleQuote(s) {
|
|
|
124
124
|
return "'" + String(s).replace(/'/g, "'\\''") + "'";
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
function parseDurationMs(raw) {
|
|
128
|
+
const text = String(raw || "").trim();
|
|
129
|
+
if (!text) throw new Error("duration is required");
|
|
130
|
+
const m = text.match(/^(\d+(?:\.\d+)?)\s*(ms|millisecond|milliseconds|s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hour|hours|d|day|days)$/i);
|
|
131
|
+
if (!m) throw new Error(`Invalid duration: ${text}`);
|
|
132
|
+
const n = Number(m[1]);
|
|
133
|
+
if (!Number.isFinite(n) || n < 0) throw new Error(`Invalid duration: ${text}`);
|
|
134
|
+
const unit = m[2].toLowerCase();
|
|
135
|
+
const mult =
|
|
136
|
+
unit === "ms" || unit.startsWith("millisecond") ? 1 :
|
|
137
|
+
unit === "s" || unit === "sec" || unit === "secs" || unit.startsWith("second") ? 1000 :
|
|
138
|
+
unit === "m" || unit === "min" || unit === "mins" || unit.startsWith("minute") ? 60_000 :
|
|
139
|
+
unit === "h" || unit === "hr" || unit.startsWith("hour") ? 3_600_000 :
|
|
140
|
+
86_400_000;
|
|
141
|
+
return Math.round(n * mult);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function zonedDateTimeToUtc(year, month, day, hour, minute, second, timezone) {
|
|
145
|
+
const guess = Date.UTC(year, month - 1, day, hour, minute, second);
|
|
146
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
147
|
+
timeZone: timezone,
|
|
148
|
+
year: "numeric",
|
|
149
|
+
month: "2-digit",
|
|
150
|
+
day: "2-digit",
|
|
151
|
+
hour: "2-digit",
|
|
152
|
+
minute: "2-digit",
|
|
153
|
+
second: "2-digit",
|
|
154
|
+
hour12: false,
|
|
155
|
+
}).formatToParts(new Date(guess)).reduce((acc, p) => {
|
|
156
|
+
if (p.type !== "literal") acc[p.type] = Number(p.value);
|
|
157
|
+
return acc;
|
|
158
|
+
}, {});
|
|
159
|
+
if (parts.hour === 24) parts.hour = 0;
|
|
160
|
+
const asIfUtc = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
|
|
161
|
+
return new Date(guess - (asIfUtc - guess));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function parseDateTime(raw, timezone = "Asia/Shanghai") {
|
|
165
|
+
const text = String(raw || "").trim();
|
|
166
|
+
if (!text) throw new Error("until/deadlineAt is required");
|
|
167
|
+
const ymd = text.match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[ T](\d{1,2}):(\d{2})(?::(\d{2}))?)?$/);
|
|
168
|
+
if (ymd) {
|
|
169
|
+
return zonedDateTimeToUtc(
|
|
170
|
+
Number(ymd[1]),
|
|
171
|
+
Number(ymd[2]),
|
|
172
|
+
Number(ymd[3]),
|
|
173
|
+
Number(ymd[4] || 0),
|
|
174
|
+
Number(ymd[5] || 0),
|
|
175
|
+
Number(ymd[6] || 0),
|
|
176
|
+
timezone,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const direct = Date.parse(text);
|
|
180
|
+
if (Number.isFinite(direct)) return new Date(direct);
|
|
181
|
+
|
|
182
|
+
const hm = text.match(/^(tomorrow\s+)?(\d{1,2}):(\d{2})(?::(\d{2}))?$/i);
|
|
183
|
+
if (hm) {
|
|
184
|
+
const now = new Date();
|
|
185
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
186
|
+
timeZone: timezone,
|
|
187
|
+
year: "numeric",
|
|
188
|
+
month: "2-digit",
|
|
189
|
+
day: "2-digit",
|
|
190
|
+
}).formatToParts(now).reduce((acc, p) => {
|
|
191
|
+
if (p.type !== "literal") acc[p.type] = Number(p.value);
|
|
192
|
+
return acc;
|
|
193
|
+
}, {});
|
|
194
|
+
const date = zonedDateTimeToUtc(parts.year, parts.month, parts.day, Number(hm[2]), Number(hm[3]), Number(hm[4] || 0), timezone);
|
|
195
|
+
if (hm[1]) date.setUTCDate(date.getUTCDate() + 1);
|
|
196
|
+
return date;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
throw new Error(`Invalid datetime: ${text}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function outputPathAbs(runDir, instanceId, execId, slotName) {
|
|
203
|
+
return path.join(runDir, outputDirForNode(instanceId), outputNodeBasename(instanceId, execId, slotName));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function writeOutputSlot(runDir, instanceId, execId, slotName, value) {
|
|
207
|
+
const p = outputPathAbs(runDir, instanceId, execId, slotName);
|
|
208
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
209
|
+
fs.writeFileSync(p, String(value ?? "") + "\n", "utf-8");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function writeWaitState(runDir, state) {
|
|
213
|
+
const legacyPath = path.join(runDir, "wait-state.json");
|
|
214
|
+
const registryPath = path.join(runDir, "wait-states.json");
|
|
215
|
+
let registry = { version: 1, flowName: state.flowName, uuid: state.uuid, waits: [] };
|
|
216
|
+
if (fs.existsSync(registryPath)) {
|
|
217
|
+
try {
|
|
218
|
+
const parsed = JSON.parse(fs.readFileSync(registryPath, "utf-8"));
|
|
219
|
+
if (parsed && typeof parsed === "object" && Array.isArray(parsed.waits)) registry = parsed;
|
|
220
|
+
} catch (_) {}
|
|
221
|
+
}
|
|
222
|
+
const waits = registry.waits.filter((w) => w && w.instanceId !== state.instanceId);
|
|
223
|
+
const wait = { ...state, id: state.id || state.instanceId };
|
|
224
|
+
waits.push(wait);
|
|
225
|
+
registry = {
|
|
226
|
+
...registry,
|
|
227
|
+
version: 1,
|
|
228
|
+
flowName: state.flowName,
|
|
229
|
+
uuid: state.uuid,
|
|
230
|
+
updatedAt: new Date().toISOString(),
|
|
231
|
+
waits,
|
|
232
|
+
};
|
|
233
|
+
fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2) + "\n", "utf-8");
|
|
234
|
+
fs.writeFileSync(legacyPath, JSON.stringify(wait, null, 2) + "\n", "utf-8");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function readCancelFlag(runDir) {
|
|
238
|
+
for (const name of ["cancelled", "cancelled.json", "wait-cancelled.json"]) {
|
|
239
|
+
const p = path.join(runDir, name);
|
|
240
|
+
if (!fs.existsSync(p)) continue;
|
|
241
|
+
if (name.endsWith(".json")) {
|
|
242
|
+
try {
|
|
243
|
+
const data = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
244
|
+
if (data && (data.cancelled === true || data.status === "cancelled")) return true;
|
|
245
|
+
} catch (_) {}
|
|
246
|
+
} else {
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function boolish(v) {
|
|
254
|
+
if (v == null || String(v).trim() === "") return false;
|
|
255
|
+
const s = String(v).trim();
|
|
256
|
+
if (fs.existsSync(s)) {
|
|
257
|
+
try {
|
|
258
|
+
return parseBool(fs.readFileSync(s, "utf-8").trim());
|
|
259
|
+
} catch (_) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return parseBool(s);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function emitLocalNoopPrompt(workspaceRoot, runDir, instanceId, suffix, content) {
|
|
267
|
+
const nodeIntermediateDir = path.join(runDir, intermediateDirForNode(instanceId));
|
|
268
|
+
fs.mkdirSync(nodeIntermediateDir, { recursive: true });
|
|
269
|
+
const promptPath = path.join(nodeIntermediateDir, `${instanceId}.${suffix}.prompt.md`);
|
|
270
|
+
fs.writeFileSync(promptPath, content, "utf-8");
|
|
271
|
+
return path.relative(workspaceRoot, promptPath).replace(/\\/g, "/");
|
|
272
|
+
}
|
|
273
|
+
|
|
127
274
|
/**
|
|
128
275
|
* 若为 tool_load_key / tool_save_key / tool_get_env,写入「直接执行 agentflow apply -ai run-tool-nodejs + 对应脚本」的 prompt,
|
|
129
276
|
* key/value 从 getResolvedValues 的 resolvedInputs 读取并拼入命令。
|
|
@@ -380,6 +527,194 @@ function main() {
|
|
|
380
527
|
return;
|
|
381
528
|
}
|
|
382
529
|
|
|
530
|
+
if (definitionId === "control_delay" || definitionId === "control_wait_until") {
|
|
531
|
+
const data = getResolvedValues(workspaceRoot, flowName, uuid, instanceId);
|
|
532
|
+
if (!data.ok) {
|
|
533
|
+
console.error(JSON.stringify({ ok: false, error: `${definitionId}: getResolvedValues failed` }));
|
|
534
|
+
process.exit(1);
|
|
535
|
+
}
|
|
536
|
+
const inputs = data.resolvedInputs || {};
|
|
537
|
+
let wakeAt;
|
|
538
|
+
try {
|
|
539
|
+
if (definitionId === "control_delay") {
|
|
540
|
+
const duration = inputs.duration ?? inputs.value ?? "";
|
|
541
|
+
wakeAt = new Date(Date.now() + parseDurationMs(duration)).toISOString();
|
|
542
|
+
} else {
|
|
543
|
+
const timezone = inputs.timezone || "Asia/Shanghai";
|
|
544
|
+
wakeAt = parseDateTime(inputs.until ?? inputs.wakeAt ?? "", timezone).toISOString();
|
|
545
|
+
}
|
|
546
|
+
} catch (e) {
|
|
547
|
+
console.error(JSON.stringify({ ok: false, error: `${definitionId}: ${e.message}` }));
|
|
548
|
+
process.exit(1);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
writeOutputSlot(runDir, instanceId, execId, "wakeAt", wakeAt);
|
|
552
|
+
writeWaitState(runDir, {
|
|
553
|
+
status: "waiting",
|
|
554
|
+
reason: definitionId,
|
|
555
|
+
flowName,
|
|
556
|
+
uuid,
|
|
557
|
+
instanceId,
|
|
558
|
+
execId,
|
|
559
|
+
wakeAt,
|
|
560
|
+
createdAt: new Date().toISOString(),
|
|
561
|
+
});
|
|
562
|
+
writeResult(
|
|
563
|
+
workspaceRoot,
|
|
564
|
+
flowName,
|
|
565
|
+
uuid,
|
|
566
|
+
instanceId,
|
|
567
|
+
{ status: "pending", message: `等待至 ${wakeAt}` },
|
|
568
|
+
{ execId, preserveBody: false },
|
|
569
|
+
);
|
|
570
|
+
const promptPath = emitLocalNoopPrompt(
|
|
571
|
+
workspaceRoot,
|
|
572
|
+
runDir,
|
|
573
|
+
instanceId,
|
|
574
|
+
"waiting",
|
|
575
|
+
`此节点为 ${definitionId},已写入 wait-state.json,等待 scheduler 在 ${wakeAt} 唤醒。\n`,
|
|
576
|
+
);
|
|
577
|
+
writeCacheJsonForNode(workspaceRoot, flowName, uuid, instanceId, execId);
|
|
578
|
+
logToRunTag(workspaceRoot, flowName, uuid, "pre-process", { event: "waiting", instanceId, wakeAt, definitionId });
|
|
579
|
+
console.log(JSON.stringify({
|
|
580
|
+
ok: true,
|
|
581
|
+
promptPath,
|
|
582
|
+
resultPath: resultPathRel,
|
|
583
|
+
execId,
|
|
584
|
+
subagent: "agentflow-node-executor",
|
|
585
|
+
optionalPromptPath: promptPath,
|
|
586
|
+
definitionId,
|
|
587
|
+
}));
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (definitionId === "control_interval_loop") {
|
|
592
|
+
const data = getResolvedValues(workspaceRoot, flowName, uuid, instanceId);
|
|
593
|
+
if (!data.ok) {
|
|
594
|
+
console.error(JSON.stringify({ ok: false, error: "control_interval_loop: getResolvedValues failed" }));
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
const inputs = data.resolvedInputs || {};
|
|
598
|
+
let branch = "continue";
|
|
599
|
+
let message = "";
|
|
600
|
+
let wakeAt = "";
|
|
601
|
+
let expired = false;
|
|
602
|
+
try {
|
|
603
|
+
const timezone = inputs.timezone || "Asia/Shanghai";
|
|
604
|
+
const cancelled = boolish(inputs.cancelled) || readCancelFlag(runDir);
|
|
605
|
+
const done = boolish(inputs.done);
|
|
606
|
+
if (cancelled) {
|
|
607
|
+
branch = "cancelled";
|
|
608
|
+
message = "已取消";
|
|
609
|
+
} else if (done) {
|
|
610
|
+
branch = "done";
|
|
611
|
+
message = "已完成";
|
|
612
|
+
} else {
|
|
613
|
+
let deadline = null;
|
|
614
|
+
if (inputs.deadlineAt) {
|
|
615
|
+
deadline = parseDateTime(inputs.deadlineAt, timezone);
|
|
616
|
+
} else if (inputs.duration) {
|
|
617
|
+
const start = inputs.startAt ? parseDateTime(inputs.startAt, timezone) : new Date();
|
|
618
|
+
deadline = new Date(start.getTime() + parseDurationMs(inputs.duration));
|
|
619
|
+
}
|
|
620
|
+
expired = deadline ? Date.now() >= deadline.getTime() : false;
|
|
621
|
+
if (deadline) writeOutputSlot(runDir, instanceId, execId, "deadlineAt", deadline.toISOString());
|
|
622
|
+
if (expired) {
|
|
623
|
+
branch = "timeout";
|
|
624
|
+
message = `已超过截止时间 ${deadline.toISOString()}`;
|
|
625
|
+
} else {
|
|
626
|
+
const interval = inputs.interval || "10m";
|
|
627
|
+
wakeAt = new Date(Date.now() + parseDurationMs(interval)).toISOString();
|
|
628
|
+
branch = "continue";
|
|
629
|
+
message = `等待至 ${wakeAt}`;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
} catch (e) {
|
|
633
|
+
console.error(JSON.stringify({ ok: false, error: `control_interval_loop: ${e.message}` }));
|
|
634
|
+
process.exit(1);
|
|
635
|
+
}
|
|
636
|
+
writeOutputSlot(runDir, instanceId, execId, "expired", expired ? "true" : "false");
|
|
637
|
+
if (wakeAt) writeOutputSlot(runDir, instanceId, execId, "wakeAt", wakeAt);
|
|
638
|
+
const promptPath = emitLocalNoopPrompt(workspaceRoot, runDir, instanceId, "interval-loop", `此节点为 control_interval_loop,分支:${branch}。\n`);
|
|
639
|
+
writeCacheJsonForNode(workspaceRoot, flowName, uuid, instanceId, execId);
|
|
640
|
+
if (wakeAt) {
|
|
641
|
+
writeWaitState(runDir, {
|
|
642
|
+
status: "waiting",
|
|
643
|
+
reason: definitionId,
|
|
644
|
+
flowName,
|
|
645
|
+
uuid,
|
|
646
|
+
instanceId,
|
|
647
|
+
execId,
|
|
648
|
+
branch,
|
|
649
|
+
wakeAt,
|
|
650
|
+
createdAt: new Date().toISOString(),
|
|
651
|
+
});
|
|
652
|
+
writeResult(workspaceRoot, flowName, uuid, instanceId, { status: "pending", message, branch }, { execId, preserveBody: false });
|
|
653
|
+
} else {
|
|
654
|
+
writeResult(workspaceRoot, flowName, uuid, instanceId, { status: "success", message, branch }, { execId, preserveBody: false });
|
|
655
|
+
}
|
|
656
|
+
logToRunTag(workspaceRoot, flowName, uuid, "pre-process", { event: "interval-loop", instanceId, branch, wakeAt: wakeAt || undefined });
|
|
657
|
+
console.log(JSON.stringify({
|
|
658
|
+
ok: true,
|
|
659
|
+
promptPath,
|
|
660
|
+
resultPath: resultPathRel,
|
|
661
|
+
execId,
|
|
662
|
+
subagent: "agentflow-node-executor",
|
|
663
|
+
optionalPromptPath: promptPath,
|
|
664
|
+
definitionId,
|
|
665
|
+
}));
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (definitionId === "control_deadline" || definitionId === "control_cancelled") {
|
|
670
|
+
const data = getResolvedValues(workspaceRoot, flowName, uuid, instanceId);
|
|
671
|
+
if (!data.ok) {
|
|
672
|
+
console.error(JSON.stringify({ ok: false, error: `${definitionId}: getResolvedValues failed` }));
|
|
673
|
+
process.exit(1);
|
|
674
|
+
}
|
|
675
|
+
const inputs = data.resolvedInputs || {};
|
|
676
|
+
let boolValue = false;
|
|
677
|
+
let message = "";
|
|
678
|
+
try {
|
|
679
|
+
if (definitionId === "control_deadline") {
|
|
680
|
+
const timezone = inputs.timezone || "Asia/Shanghai";
|
|
681
|
+
let deadline;
|
|
682
|
+
if (inputs.deadlineAt) {
|
|
683
|
+
deadline = parseDateTime(inputs.deadlineAt, timezone);
|
|
684
|
+
} else {
|
|
685
|
+
const start = inputs.startAt ? parseDateTime(inputs.startAt, timezone) : new Date();
|
|
686
|
+
deadline = new Date(start.getTime() + parseDurationMs(inputs.duration || ""));
|
|
687
|
+
}
|
|
688
|
+
const deadlineAt = deadline.toISOString();
|
|
689
|
+
boolValue = Date.now() >= deadline.getTime();
|
|
690
|
+
writeOutputSlot(runDir, instanceId, execId, "deadlineAt", deadlineAt);
|
|
691
|
+
writeOutputSlot(runDir, instanceId, execId, "expired", boolValue ? "true" : "false");
|
|
692
|
+
message = boolValue ? `已超过截止时间 ${deadlineAt}` : `未超过截止时间 ${deadlineAt}`;
|
|
693
|
+
} else {
|
|
694
|
+
boolValue = readCancelFlag(runDir);
|
|
695
|
+
writeOutputSlot(runDir, instanceId, execId, "cancelled", boolValue ? "true" : "false");
|
|
696
|
+
message = boolValue ? "已取消" : "未取消";
|
|
697
|
+
}
|
|
698
|
+
} catch (e) {
|
|
699
|
+
console.error(JSON.stringify({ ok: false, error: `${definitionId}: ${e.message}` }));
|
|
700
|
+
process.exit(1);
|
|
701
|
+
}
|
|
702
|
+
writeResult(workspaceRoot, flowName, uuid, instanceId, { status: "success", message }, { execId, preserveBody: false });
|
|
703
|
+
const promptPath = emitLocalNoopPrompt(workspaceRoot, runDir, instanceId, "local", `此节点为 ${definitionId},已本地计算完成。\n`);
|
|
704
|
+
writeCacheJsonForNode(workspaceRoot, flowName, uuid, instanceId, execId);
|
|
705
|
+
logToRunTag(workspaceRoot, flowName, uuid, "pre-process", { event: "local-control", instanceId, definitionId, value: boolValue });
|
|
706
|
+
console.log(JSON.stringify({
|
|
707
|
+
ok: true,
|
|
708
|
+
promptPath,
|
|
709
|
+
resultPath: resultPathRel,
|
|
710
|
+
execId,
|
|
711
|
+
subagent: "agentflow-node-executor",
|
|
712
|
+
optionalPromptPath: promptPath,
|
|
713
|
+
definitionId,
|
|
714
|
+
}));
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
|
|
383
718
|
const data = buildNodePrompt(workspaceRoot, flowName, uuid, instanceId, execId);
|
|
384
719
|
if (!data.ok) {
|
|
385
720
|
console.error(JSON.stringify({ ok: false, error: data.error || "build-node-prompt failed" }));
|
|
@@ -425,7 +760,7 @@ function main() {
|
|
|
425
760
|
output.optionalPromptPath = anyOneResult.optionalPromptPath;
|
|
426
761
|
output.directCommand = anyOneResult.directCommand;
|
|
427
762
|
}
|
|
428
|
-
} else if ((definitionId === "tool_nodejs" || definitionId === "control_toBool") && data.script) {
|
|
763
|
+
} else if ((definitionId === "tool_nodejs" || definitionId === "control_toBool" || String(definitionId || "").startsWith("marketplace:")) && data.script) {
|
|
429
764
|
const toolNodejsResult = emitToolNodejsDirectCommand(workspaceRoot, flowName, uuid, instanceId, data.script, execId);
|
|
430
765
|
if (toolNodejsResult) {
|
|
431
766
|
output.optionalPromptPath = toolNodejsResult.optionalPromptPath;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
# 内置节点:取消状态判断
|
|
3
|
+
description: Check whether the current run/watch has been cancelled. Use cancelled output with control_if.
|
|
4
|
+
displayName: Cancelled
|
|
5
|
+
input:
|
|
6
|
+
- type: node
|
|
7
|
+
name: prev
|
|
8
|
+
default: ""
|
|
9
|
+
- type: text
|
|
10
|
+
name: watchId
|
|
11
|
+
default: ""
|
|
12
|
+
output:
|
|
13
|
+
- type: node
|
|
14
|
+
name: next
|
|
15
|
+
default: ""
|
|
16
|
+
- type: bool
|
|
17
|
+
name: cancelled
|
|
18
|
+
default: ""
|
|
19
|
+
---
|
|
20
|
+
${USER_PROMPT}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
# 内置节点:截止时间判断
|
|
3
|
+
description: Compute whether a deadline has expired. Use expired output with control_if.
|
|
4
|
+
displayName: Deadline
|
|
5
|
+
input:
|
|
6
|
+
- type: node
|
|
7
|
+
name: prev
|
|
8
|
+
default: ""
|
|
9
|
+
- type: text
|
|
10
|
+
name: startAt
|
|
11
|
+
default: ""
|
|
12
|
+
- type: text
|
|
13
|
+
name: duration
|
|
14
|
+
default: ""
|
|
15
|
+
- type: text
|
|
16
|
+
name: deadlineAt
|
|
17
|
+
default: ""
|
|
18
|
+
- type: text
|
|
19
|
+
name: timezone
|
|
20
|
+
default: "Asia/Shanghai"
|
|
21
|
+
output:
|
|
22
|
+
- type: node
|
|
23
|
+
name: next
|
|
24
|
+
default: ""
|
|
25
|
+
- type: bool
|
|
26
|
+
name: expired
|
|
27
|
+
default: ""
|
|
28
|
+
- type: text
|
|
29
|
+
name: deadlineAt
|
|
30
|
+
default: ""
|
|
31
|
+
---
|
|
32
|
+
${USER_PROMPT}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
# 内置节点:延迟等待
|
|
3
|
+
description: Persistently wait for a relative duration, then continue when scheduler resumes this run.
|
|
4
|
+
displayName: Delay
|
|
5
|
+
input:
|
|
6
|
+
- type: node
|
|
7
|
+
name: prev
|
|
8
|
+
default: ""
|
|
9
|
+
- type: text
|
|
10
|
+
name: duration
|
|
11
|
+
default: "10m"
|
|
12
|
+
output:
|
|
13
|
+
- type: node
|
|
14
|
+
name: next
|
|
15
|
+
default: ""
|
|
16
|
+
- type: text
|
|
17
|
+
name: wakeAt
|
|
18
|
+
default: ""
|
|
19
|
+
---
|
|
20
|
+
${USER_PROMPT}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
# 内置节点:间隔循环
|
|
3
|
+
description: Wait by interval and branch to continue, done, timeout, or cancelled for watch-style flows.
|
|
4
|
+
displayName: IntervalLoop
|
|
5
|
+
input:
|
|
6
|
+
- type: node
|
|
7
|
+
name: prev
|
|
8
|
+
default: ""
|
|
9
|
+
- type: bool
|
|
10
|
+
name: done
|
|
11
|
+
default: ""
|
|
12
|
+
- type: bool
|
|
13
|
+
name: cancelled
|
|
14
|
+
default: ""
|
|
15
|
+
- type: text
|
|
16
|
+
name: interval
|
|
17
|
+
default: "10m"
|
|
18
|
+
- type: text
|
|
19
|
+
name: startAt
|
|
20
|
+
default: ""
|
|
21
|
+
- type: text
|
|
22
|
+
name: duration
|
|
23
|
+
default: ""
|
|
24
|
+
- type: text
|
|
25
|
+
name: deadlineAt
|
|
26
|
+
default: ""
|
|
27
|
+
- type: text
|
|
28
|
+
name: timezone
|
|
29
|
+
default: "Asia/Shanghai"
|
|
30
|
+
output:
|
|
31
|
+
- type: node
|
|
32
|
+
name: continue
|
|
33
|
+
default: ""
|
|
34
|
+
- type: node
|
|
35
|
+
name: done
|
|
36
|
+
default: ""
|
|
37
|
+
- type: node
|
|
38
|
+
name: timeout
|
|
39
|
+
default: ""
|
|
40
|
+
- type: node
|
|
41
|
+
name: cancelled
|
|
42
|
+
default: ""
|
|
43
|
+
- type: text
|
|
44
|
+
name: wakeAt
|
|
45
|
+
default: ""
|
|
46
|
+
- type: bool
|
|
47
|
+
name: expired
|
|
48
|
+
default: ""
|
|
49
|
+
- type: text
|
|
50
|
+
name: deadlineAt
|
|
51
|
+
default: ""
|
|
52
|
+
---
|
|
53
|
+
${USER_PROMPT}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
# 内置节点:等待到指定时间
|
|
3
|
+
description: Persistently wait until an absolute time, then continue when scheduler resumes this run.
|
|
4
|
+
displayName: WaitUntil
|
|
5
|
+
input:
|
|
6
|
+
- type: node
|
|
7
|
+
name: prev
|
|
8
|
+
default: ""
|
|
9
|
+
- type: text
|
|
10
|
+
name: until
|
|
11
|
+
default: ""
|
|
12
|
+
- type: text
|
|
13
|
+
name: timezone
|
|
14
|
+
default: "Asia/Shanghai"
|
|
15
|
+
output:
|
|
16
|
+
- type: node
|
|
17
|
+
name: next
|
|
18
|
+
default: ""
|
|
19
|
+
- type: text
|
|
20
|
+
name: wakeAt
|
|
21
|
+
default: ""
|
|
22
|
+
---
|
|
23
|
+
${USER_PROMPT}
|