taskmeld 0.1.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/LICENSE +18 -0
- package/README.md +172 -0
- package/README.zh-CN.md +172 -0
- package/dist/src/app/app-context-env.js +51 -0
- package/dist/src/app/create-app-context.js +127 -0
- package/dist/src/app/data-dir.js +29 -0
- package/dist/src/app/pipeline-config.js +105 -0
- package/dist/src/app/pipeline-plugin-config.js +2 -0
- package/dist/src/app/pipeline-registry.js +502 -0
- package/dist/src/app/pipeline-runtime.js +202 -0
- package/dist/src/app/runtime-store.js +151 -0
- package/dist/src/app/user-config.js +37 -0
- package/dist/src/artifacts/artifact-cleanup.js +192 -0
- package/dist/src/artifacts/artifact-index.js +262 -0
- package/dist/src/artifacts/artifact-rebuilder.js +120 -0
- package/dist/src/artifacts/storage-service.js +371 -0
- package/dist/src/cli/bootstrap.js +226 -0
- package/dist/src/cli/commands/agent.js +126 -0
- package/dist/src/cli/commands/artifact.js +175 -0
- package/dist/src/cli/commands/init.js +150 -0
- package/dist/src/cli/commands/pipeline/errors.js +37 -0
- package/dist/src/cli/commands/pipeline/result.js +179 -0
- package/dist/src/cli/commands/pipeline/selector.js +51 -0
- package/dist/src/cli/commands/pipeline/types.js +2 -0
- package/dist/src/cli/commands/pipeline/watch.js +67 -0
- package/dist/src/cli/commands/pipeline.js +339 -0
- package/dist/src/cli/commands/scheduler.js +81 -0
- package/dist/src/cli/commands/server.js +70 -0
- package/dist/src/cli/commands/system.js +21 -0
- package/dist/src/cli/errors.js +71 -0
- package/dist/src/cli/help.js +184 -0
- package/dist/src/cli/index.js +65 -0
- package/dist/src/cli/output.js +19 -0
- package/dist/src/cli/renderers/engine/json.js +67 -0
- package/dist/src/cli/renderers/engine/markdown.js +95 -0
- package/dist/src/cli/renderers/engine/types.js +2 -0
- package/dist/src/cli/renderers/engine/utils.js +32 -0
- package/dist/src/cli/renderers/index.js +27 -0
- package/dist/src/cli/renderers/specs/agent.js +78 -0
- package/dist/src/cli/renderers/specs/artifact.js +32 -0
- package/dist/src/cli/renderers/specs/index.js +36 -0
- package/dist/src/cli/renderers/specs/init.js +25 -0
- package/dist/src/cli/renderers/specs/pipeline.js +561 -0
- package/dist/src/cli/renderers/specs/scheduler.js +46 -0
- package/dist/src/cli/renderers/specs/server.js +38 -0
- package/dist/src/cli/renderers/specs/system.js +36 -0
- package/dist/src/cli/router.js +199 -0
- package/dist/src/cli/server-runtime-client.js +780 -0
- package/dist/src/cli/types.js +2 -0
- package/dist/src/gateway/frame-sanitizer.js +78 -0
- package/dist/src/gateway/gateway-client.js +462 -0
- package/dist/src/gateway/index.js +18 -0
- package/dist/src/gateway/types.js +2 -0
- package/dist/src/index.js +123 -0
- package/dist/src/logs/run-log-reader.js +141 -0
- package/dist/src/logs/run-log-service.js +42 -0
- package/dist/src/logs/run-log-types.js +2 -0
- package/dist/src/pipeline/agent-activity.js +191 -0
- package/dist/src/pipeline/artifact-storage.js +208 -0
- package/dist/src/pipeline/diagnostics/dependency-diagnostic.js +105 -0
- package/dist/src/pipeline/diagnostics/index.js +6 -0
- package/dist/src/pipeline/dispatch/pipeline-inbound-queue.js +215 -0
- package/dist/src/pipeline/dispatch/pipeline-link-dispatcher.js +66 -0
- package/dist/src/pipeline/dispatch/pipeline-link-store.js +94 -0
- package/dist/src/pipeline/dispatch/pipeline-queue-drainer.js +71 -0
- package/dist/src/pipeline/execution/dependency-check.js +52 -0
- package/dist/src/pipeline/execution/execution-result.js +2 -0
- package/dist/src/pipeline/execution/group-item-executor.js +128 -0
- package/dist/src/pipeline/execution/index.js +5 -0
- package/dist/src/pipeline/execution/node-item-executor.js +58 -0
- package/dist/src/pipeline/execution/node-runner.js +159 -0
- package/dist/src/pipeline/execution/readiness-state.js +10 -0
- package/dist/src/pipeline/execution/reject-handler.js +94 -0
- package/dist/src/pipeline/execution/rejected-artifact-archiver.js +45 -0
- package/dist/src/pipeline/execution/route-item-manager.js +253 -0
- package/dist/src/pipeline/execution/run-abort-controller.js +66 -0
- package/dist/src/pipeline/execution/run-state-helpers.js +257 -0
- package/dist/src/pipeline/execution/service.js +165 -0
- package/dist/src/pipeline/execution/session-registry.js +96 -0
- package/dist/src/pipeline/execution/structured-node-runner.js +411 -0
- package/dist/src/pipeline/execution-status.js +96 -0
- package/dist/src/pipeline/execution-timeout.js +21 -0
- package/dist/src/pipeline/identity/index.js +32 -0
- package/dist/src/pipeline/identity/types.js +2 -0
- package/dist/src/pipeline/item-batch-controller.js +227 -0
- package/dist/src/pipeline/output/pipeline-output-resolver.js +91 -0
- package/dist/src/pipeline/output/pipeline-output-store.js +60 -0
- package/dist/src/pipeline/runtime-model.js +173 -0
- package/dist/src/pipeline/scheduler/dependency-state.js +144 -0
- package/dist/src/pipeline/scheduler-service.js +314 -0
- package/dist/src/pipeline/state/group-item-state.js +50 -0
- package/dist/src/pipeline/state/group-run-state.js +41 -0
- package/dist/src/pipeline/state/index.js +20 -0
- package/dist/src/pipeline/state/node-item-state.js +67 -0
- package/dist/src/pipeline/state/node-run-state.js +51 -0
- package/dist/src/pipeline/state/types.js +2 -0
- package/dist/src/pipeline/state-machine.js +101 -0
- package/dist/src/pipeline/structured-output/contract.js +133 -0
- package/dist/src/pipeline/structured-output/index.js +22 -0
- package/dist/src/pipeline/structured-output/parser.js +214 -0
- package/dist/src/pipeline/structured-output/prompt.js +290 -0
- package/dist/src/pipeline/structured-output/waiter.js +139 -0
- package/dist/src/pipeline/template.js +135 -0
- package/dist/src/pipeline/timeline-log-store.js +57 -0
- package/dist/src/pipeline/tool-activity.js +94 -0
- package/dist/src/pipeline/types/pipeline-link.js +7 -0
- package/dist/src/pipeline/types/pipeline-output.js +11 -0
- package/dist/src/pipeline/types/workflow.js +2 -0
- package/dist/src/pipeline/workflow/branch-rules.js +74 -0
- package/dist/src/pipeline/workflow/defaults.js +48 -0
- package/dist/src/pipeline/workflow/io.js +89 -0
- package/dist/src/pipeline/workflow/normalize.js +347 -0
- package/dist/src/pipeline/workflow/routes.js +16 -0
- package/dist/src/pipeline/workflow/template-mapper.js +113 -0
- package/dist/src/pipeline/workflow/validate.js +312 -0
- package/dist/src/pipeline/workflow-graph.js +165 -0
- package/dist/src/server/api-handler.js +163 -0
- package/dist/src/server/http-utils.js +34 -0
- package/dist/src/server/middleware.js +61 -0
- package/dist/src/server/router.js +105 -0
- package/dist/src/server/routes/agents.js +189 -0
- package/dist/src/server/routes/artifacts.js +163 -0
- package/dist/src/server/routes/gateway.js +18 -0
- package/dist/src/server/routes/health.js +16 -0
- package/dist/src/server/routes/logs.js +73 -0
- package/dist/src/server/routes/pipeline-batch.js +163 -0
- package/dist/src/server/routes/pipeline-diagnostics.js +33 -0
- package/dist/src/server/routes/pipeline-identity.js +24 -0
- package/dist/src/server/routes/pipeline-links.js +117 -0
- package/dist/src/server/routes/pipeline-outputs.js +27 -0
- package/dist/src/server/routes/pipeline-queue.js +62 -0
- package/dist/src/server/routes/pipeline-runtime.js +162 -0
- package/dist/src/server/routes/pipeline-scheduler.js +69 -0
- package/dist/src/server/routes/pipeline-workflow.js +180 -0
- package/dist/src/server/routes/pipelines.js +96 -0
- package/dist/src/server/routes/sessions.js +244 -0
- package/dist/src/server/routes/timeline.js +14 -0
- package/dist/src/server/serve-static.js +42 -0
- package/dist/src/server/types.js +2 -0
- package/dist/src/services/agent-service.js +79 -0
- package/dist/src/services/artifact-service.js +74 -0
- package/dist/src/services/gateway-read-helpers.js +10 -0
- package/dist/src/services/index.js +23 -0
- package/dist/src/services/pipeline-service.js +529 -0
- package/dist/src/services/pipeline-status.js +93 -0
- package/dist/src/services/read-services.js +60 -0
- package/dist/src/services/scheduler-service.js +37 -0
- package/dist/src/services/session-service.js +227 -0
- package/dist/src/services/system-service.js +26 -0
- package/dist/src/transport/ws-broker.js +48 -0
- package/dist/src/utils/array.js +17 -0
- package/dist/src/utils/guards.js +5 -0
- package/dist/src/utils/session.js +60 -0
- package/dist/src/version.js +5 -0
- package/package.json +61 -0
- package/web/dist/assets/index-CWnfhkn-.js +65 -0
- package/web/dist/assets/index-gZ0xOfSO.css +1 -0
- package/web/dist/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/web/dist/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
- package/web/dist/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-cyrillic-700-normal-BWTpRfYl.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-cyrillic-700-normal-CEoEElIJ.woff +0 -0
- package/web/dist/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/web/dist/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
- package/web/dist/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-greek-700-normal-C6CZE3T8.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-greek-700-normal-DEigVDxa.woff +0 -0
- package/web/dist/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/web/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
- package/web/dist/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
- package/web/dist/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/web/dist/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
- package/web/dist/assets/jetbrains-mono-latin-ext-700-normal-CZipNAKV.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-ext-700-normal-CxPITLHs.woff +0 -0
- package/web/dist/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/web/dist/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
- package/web/dist/assets/jetbrains-mono-vietnamese-700-normal-BDLVIk2r.woff +0 -0
- package/web/dist/assets/space-grotesk-latin-400-normal-BnQMeOim.woff +0 -0
- package/web/dist/assets/space-grotesk-latin-400-normal-CJ-V5oYT.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-500-normal-CNSSEhBt.woff +0 -0
- package/web/dist/assets/space-grotesk-latin-500-normal-lFbtlQH6.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-700-normal-CwsQ-cCU.woff +0 -0
- package/web/dist/assets/space-grotesk-latin-700-normal-RjhwGPKo.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-ext-400-normal-CfP_5XZW.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-ext-400-normal-DRPE3kg4.woff +0 -0
- package/web/dist/assets/space-grotesk-latin-ext-500-normal-3dgZTiw9.woff +0 -0
- package/web/dist/assets/space-grotesk-latin-ext-500-normal-DUe3BAxM.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-ext-700-normal-BQnZhY3m.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-ext-700-normal-HVCqSBdx.woff +0 -0
- package/web/dist/assets/space-grotesk-vietnamese-400-normal-B7xT_GF5.woff2 +0 -0
- package/web/dist/assets/space-grotesk-vietnamese-400-normal-BIWiOVfw.woff +0 -0
- package/web/dist/assets/space-grotesk-vietnamese-500-normal-BTqKIpxg.woff +0 -0
- package/web/dist/assets/space-grotesk-vietnamese-500-normal-BmEvtly_.woff2 +0 -0
- package/web/dist/assets/space-grotesk-vietnamese-700-normal-DMty7AZE.woff2 +0 -0
- package/web/dist/assets/space-grotesk-vietnamese-700-normal-Duxec5Rn.woff +0 -0
- package/web/dist/favicon.svg +10 -0
- package/web/dist/index.html +14 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createToolActivityLogger = void 0;
|
|
4
|
+
const SESSION_AGENT_PATTERN = /^agent:([^:]+):/i;
|
|
5
|
+
const MAX_SEEN_KEYS = 500;
|
|
6
|
+
const findStringByKeys = (value, keys, depth = 0) => {
|
|
7
|
+
if (depth > 5 || value === null || value === undefined)
|
|
8
|
+
return null;
|
|
9
|
+
if (typeof value === "string")
|
|
10
|
+
return value.trim() || null;
|
|
11
|
+
if (Array.isArray(value)) {
|
|
12
|
+
for (const item of value) {
|
|
13
|
+
const found = findStringByKeys(item, keys, depth + 1);
|
|
14
|
+
if (found)
|
|
15
|
+
return found;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
if (typeof value !== "object")
|
|
20
|
+
return null;
|
|
21
|
+
const record = value;
|
|
22
|
+
for (const key of keys) {
|
|
23
|
+
const raw = record[key];
|
|
24
|
+
if (typeof raw === "string" && raw.trim())
|
|
25
|
+
return raw.trim();
|
|
26
|
+
}
|
|
27
|
+
for (const item of Object.values(record)) {
|
|
28
|
+
const found = findStringByKeys(item, keys, depth + 1);
|
|
29
|
+
if (found)
|
|
30
|
+
return found;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
};
|
|
34
|
+
const createToolActivityLogger = (deps) => {
|
|
35
|
+
const seenOrder = [];
|
|
36
|
+
const seenSet = new Set();
|
|
37
|
+
const markSeen = (key) => {
|
|
38
|
+
if (seenSet.has(key))
|
|
39
|
+
return false;
|
|
40
|
+
seenSet.add(key);
|
|
41
|
+
seenOrder.push(key);
|
|
42
|
+
if (seenOrder.length > MAX_SEEN_KEYS) {
|
|
43
|
+
const oldest = seenOrder.shift();
|
|
44
|
+
if (oldest)
|
|
45
|
+
seenSet.delete(oldest);
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
};
|
|
49
|
+
const inferAgentId = (sessionKey) => {
|
|
50
|
+
if (!sessionKey)
|
|
51
|
+
return null;
|
|
52
|
+
const mapped = deps.resolveAgentBySessionId(sessionKey);
|
|
53
|
+
if (mapped && mapped.trim())
|
|
54
|
+
return mapped.trim();
|
|
55
|
+
const match = sessionKey.match(SESSION_AGENT_PATTERN);
|
|
56
|
+
return match?.[1] ?? null;
|
|
57
|
+
};
|
|
58
|
+
const handleFrame = (frame) => {
|
|
59
|
+
if (frame.type !== "event")
|
|
60
|
+
return;
|
|
61
|
+
if (frame.event !== "agent" && frame.event !== "session.tool")
|
|
62
|
+
return;
|
|
63
|
+
const payload = frame.payload;
|
|
64
|
+
if (!payload)
|
|
65
|
+
return;
|
|
66
|
+
if (payload.stream !== "tool")
|
|
67
|
+
return;
|
|
68
|
+
const data = (payload.data ?? {});
|
|
69
|
+
const phase = typeof data.phase === "string" ? data.phase.trim() : "";
|
|
70
|
+
const toolName = typeof data.name === "string" && data.name.trim() ? data.name.trim() : "unknown_tool";
|
|
71
|
+
const toolCallId = typeof data.toolCallId === "string" ? data.toolCallId.trim() : "";
|
|
72
|
+
const runId = typeof payload.runId === "string" && payload.runId.trim() ? payload.runId.trim() : "unknown";
|
|
73
|
+
const seq = typeof payload.seq === "number" ? String(payload.seq) : "n/a";
|
|
74
|
+
const sessionKey = findStringByKeys(payload, ["sessionKey", "sessionId", "key", "session"]);
|
|
75
|
+
const agentId = inferAgentId(sessionKey) ?? "unknown";
|
|
76
|
+
const dedupeKey = `${runId}|${toolCallId || toolName}|${phase}|${seq}`;
|
|
77
|
+
if (!markSeen(dedupeKey))
|
|
78
|
+
return;
|
|
79
|
+
if (phase === "start") {
|
|
80
|
+
deps.pushTimeline(`Agent ${agentId} 工具开始: ${toolName} (run:${runId})`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (phase === "result" || phase === "end") {
|
|
84
|
+
const isError = data.isError === true;
|
|
85
|
+
deps.pushTimeline(`Agent ${agentId} 工具结束: ${toolName} (run:${runId}${isError ? ", error" : ""})`, isError ? "warn" : "info");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
deps.pushTimeline(`Agent ${agentId} 工具事件: ${toolName}/${phase || "unknown"} (run:${runId})`);
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
handleFrame,
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
exports.createToolActivityLogger = createToolActivityLogger;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isValidLinkId = exports.buildJobId = void 0;
|
|
4
|
+
const buildJobId = (linkId, outputId) => `job:${linkId}:${outputId}`;
|
|
5
|
+
exports.buildJobId = buildJobId;
|
|
6
|
+
const isValidLinkId = (id) => /^link:[a-zA-Z0-9._-]+$/.test(id);
|
|
7
|
+
exports.isValidLinkId = isValidLinkId;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildOutputId = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
/** 用去重键计算短哈希,稳定去重且长度可控。 */
|
|
6
|
+
const buildOutputId = (pipelineId, runId, batchRunId, itemKey, outputNodeId, artifactId, hash) => {
|
|
7
|
+
const key = `${pipelineId}|${runId}|${batchRunId ?? ""}|${itemKey ?? ""}|${outputNodeId}|${artifactId}|${hash}`;
|
|
8
|
+
const digest = (0, node_crypto_1.createHash)("sha256").update(key).digest("hex").slice(0, 16);
|
|
9
|
+
return `output:${digest}`;
|
|
10
|
+
};
|
|
11
|
+
exports.buildOutputId = buildOutputId;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 支线判定纯规则函数。
|
|
4
|
+
* 无外部依赖,只接受数据参数,供 workflow/validate 和 execution/dependency-check 共同使用。
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.getBranchScope = exports.isCrossBranchEdgeByScope = exports.computeNodeScopes = void 0;
|
|
8
|
+
// ====== Phase 2: 基于显式 branchScopeId 的支线规则 ======
|
|
9
|
+
/**
|
|
10
|
+
* 从 workflow 的 route 边推导每个节点的 branch scope。
|
|
11
|
+
* scope 标识 = routerNodeId:routeValue,如 "router:a"。
|
|
12
|
+
* 主线节点 scope 为 null。
|
|
13
|
+
*/
|
|
14
|
+
const computeNodeScopes = (nodes, edges, explicitScopes) => {
|
|
15
|
+
const scopes = new Map();
|
|
16
|
+
// 初始化:有显式 scope 的用显式,否则为 null
|
|
17
|
+
for (const node of nodes) {
|
|
18
|
+
scopes.set(node.id, explicitScopes?.get(node.id) ?? null);
|
|
19
|
+
}
|
|
20
|
+
// 从 route 边推导 scope:路由边 from 的 scope 已知时,to 的 scope = fromScope != null ? fromScope : "from:when"
|
|
21
|
+
for (const edge of edges) {
|
|
22
|
+
if (edge.when === null)
|
|
23
|
+
continue;
|
|
24
|
+
const fromScope = scopes.get(edge.from);
|
|
25
|
+
const targetScope = fromScope != null ? fromScope : `${edge.from}:${edge.when}`;
|
|
26
|
+
const existing = scopes.get(edge.to);
|
|
27
|
+
if (existing == null) {
|
|
28
|
+
scopes.set(edge.to, targetScope);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// 传播 scope 沿普通依赖边:BFS 确保同一分支内的下游节点继承上游 scope。
|
|
32
|
+
// 多条路径到达节点时,若 scope 一致则合并;若冲突则保留首次设置的 scope。
|
|
33
|
+
let changed = true;
|
|
34
|
+
while (changed) {
|
|
35
|
+
changed = false;
|
|
36
|
+
for (const edge of edges) {
|
|
37
|
+
if (edge.when !== null)
|
|
38
|
+
continue; // 只传播普通依赖边
|
|
39
|
+
const fromScope = scopes.get(edge.from);
|
|
40
|
+
if (fromScope == null)
|
|
41
|
+
continue; // 上游无 scope 则不传播
|
|
42
|
+
const toScope = scopes.get(edge.to);
|
|
43
|
+
if (toScope == null) {
|
|
44
|
+
scopes.set(edge.to, fromScope);
|
|
45
|
+
changed = true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return scopes;
|
|
50
|
+
};
|
|
51
|
+
exports.computeNodeScopes = computeNodeScopes;
|
|
52
|
+
/**
|
|
53
|
+
* 基于 scope 判断边是否为跨支线普通边。
|
|
54
|
+
* 跨支线:when 为 null 且 from 和 to 在不同的 scope 中。
|
|
55
|
+
*/
|
|
56
|
+
const isCrossBranchEdgeByScope = (edge, nodeScopes) => {
|
|
57
|
+
if (edge.when !== null)
|
|
58
|
+
return false;
|
|
59
|
+
const fromScope = nodeScopes.get(edge.from) ?? null;
|
|
60
|
+
const toScope = nodeScopes.get(edge.to) ?? null;
|
|
61
|
+
// 同 scope → 同一分支内,允许
|
|
62
|
+
if (fromScope === toScope)
|
|
63
|
+
return false;
|
|
64
|
+
// from 有 scope,to 无 scope → 支线结点向主线传播,不阻断(scope 沿依赖边继承)
|
|
65
|
+
if (fromScope != null && toScope == null)
|
|
66
|
+
return false;
|
|
67
|
+
// from 无 scope(主线),to 有 scope(支线)→ 禁止:主线不能无条件依赖支线内部节点
|
|
68
|
+
// from 和 to 都有 scope 但不同 → 禁止:不同支线之间的跨分支依赖
|
|
69
|
+
return fromScope !== toScope;
|
|
70
|
+
};
|
|
71
|
+
exports.isCrossBranchEdgeByScope = isCrossBranchEdgeByScope;
|
|
72
|
+
/** 基于 scope 获取节点的支线身份。null scope = 主线。 */
|
|
73
|
+
const getBranchScope = (nodeId, nodeScopes) => nodeScopes.get(nodeId) ?? null;
|
|
74
|
+
exports.getBranchScope = getBranchScope;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.defaultWorkflowDefinition = exports.defaultTemplateNodes = void 0;
|
|
4
|
+
const normalize_1 = require("./normalize");
|
|
5
|
+
// ====== Default template nodes ======
|
|
6
|
+
const defaultTemplateNodes = () => [];
|
|
7
|
+
exports.defaultTemplateNodes = defaultTemplateNodes;
|
|
8
|
+
const mapTemplateNodesToWorkflow = (nodes) => {
|
|
9
|
+
const workflowNodes = nodes.map((node) => ({
|
|
10
|
+
id: node.id,
|
|
11
|
+
name: node.title,
|
|
12
|
+
type: "task",
|
|
13
|
+
enabled: true,
|
|
14
|
+
isMainline: true,
|
|
15
|
+
lane: "main",
|
|
16
|
+
parallelGroupId: null,
|
|
17
|
+
executor: node.executor,
|
|
18
|
+
inputMode: "single",
|
|
19
|
+
outputMode: "single",
|
|
20
|
+
dependencyPolicy: "all",
|
|
21
|
+
routePolicy: null,
|
|
22
|
+
retryPolicy: {
|
|
23
|
+
maxAttempts: 2,
|
|
24
|
+
backoffMs: 0,
|
|
25
|
+
},
|
|
26
|
+
outputSpec: node.outputSpec,
|
|
27
|
+
instruction: node.instruction,
|
|
28
|
+
allowReject: node.allowReject,
|
|
29
|
+
maxRejectCount: node.maxRejectCount,
|
|
30
|
+
}));
|
|
31
|
+
const edges = [];
|
|
32
|
+
for (const node of nodes) {
|
|
33
|
+
for (const dep of node.dependsOn) {
|
|
34
|
+
edges.push({ from: dep, to: node.id, when: null });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
version: "3.0",
|
|
39
|
+
scheduler: (0, normalize_1.normalizeWorkflowScheduler)(undefined),
|
|
40
|
+
plugins: (0, normalize_1.normalizeWorkflowPlugins)(undefined),
|
|
41
|
+
output: { mode: "mainline_last", nodeId: null },
|
|
42
|
+
nodes: workflowNodes,
|
|
43
|
+
edges,
|
|
44
|
+
groups: [],
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
const defaultWorkflowDefinition = () => mapTemplateNodesToWorkflow((0, exports.defaultTemplateNodes)());
|
|
48
|
+
exports.defaultWorkflowDefinition = defaultWorkflowDefinition;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.savePipelineTemplateWithStorage = exports.savePipelineTemplate = exports.loadPipelineTemplateWithStorage = exports.loadPipelineTemplate = exports.saveWorkflowDefinitionWithStorage = exports.saveWorkflowDefinition = exports.loadWorkflowDefinitionWithStorage = exports.loadWorkflowDefinition = void 0;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const data_dir_1 = require("../../app/data-dir");
|
|
7
|
+
const defaults_1 = require("./defaults");
|
|
8
|
+
const template_mapper_1 = require("./template-mapper");
|
|
9
|
+
const normalize_1 = require("./normalize");
|
|
10
|
+
const validate_1 = require("./validate");
|
|
11
|
+
const TEMPLATE_FILE = (0, data_dir_1.resolveTaskMeldDataPath)("pipeline-template.json");
|
|
12
|
+
const loadWorkflowDefinition = () => {
|
|
13
|
+
return (0, exports.loadWorkflowDefinitionWithStorage)({});
|
|
14
|
+
};
|
|
15
|
+
exports.loadWorkflowDefinition = loadWorkflowDefinition;
|
|
16
|
+
const loadWorkflowDefinitionWithStorage = (options) => {
|
|
17
|
+
const workflowFilePath = options.workflowFilePath ?? TEMPLATE_FILE;
|
|
18
|
+
if (!(0, node_fs_1.existsSync)(workflowFilePath)) {
|
|
19
|
+
return (0, defaults_1.defaultWorkflowDefinition)();
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const raw = (0, node_fs_1.readFileSync)(workflowFilePath, "utf8");
|
|
23
|
+
const parsed = JSON.parse(raw);
|
|
24
|
+
const readResult = (0, normalize_1.readWorkflowDefinitionFromRawDetailed)(parsed);
|
|
25
|
+
if (readResult.ok)
|
|
26
|
+
return readResult.workflow;
|
|
27
|
+
const error = new Error("invalid_persisted_workflow_definition");
|
|
28
|
+
error.detail = readResult.detail;
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error instanceof Error && error.message === "invalid_persisted_workflow_definition") {
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
const wrapped = new Error("invalid_persisted_workflow_definition");
|
|
36
|
+
wrapped.detail = "workflow 文件解析失败,请检查 JSON 格式";
|
|
37
|
+
throw wrapped;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
exports.loadWorkflowDefinitionWithStorage = loadWorkflowDefinitionWithStorage;
|
|
41
|
+
const saveWorkflowDefinition = (workflow) => {
|
|
42
|
+
(0, exports.saveWorkflowDefinitionWithStorage)(workflow, {});
|
|
43
|
+
};
|
|
44
|
+
exports.saveWorkflowDefinition = saveWorkflowDefinition;
|
|
45
|
+
const saveWorkflowDefinitionWithStorage = (workflow, options) => {
|
|
46
|
+
const validation = (0, validate_1.validateWorkflowDefinition)(workflow);
|
|
47
|
+
if (!validation.ok) {
|
|
48
|
+
const error = new Error(validation.error);
|
|
49
|
+
error.detail = validation.detail;
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
const outputValidation = (0, validate_1.validateWorkflowOutputConfig)(workflow);
|
|
53
|
+
if (!outputValidation.ok) {
|
|
54
|
+
const error = new Error(outputValidation.error);
|
|
55
|
+
error.detail = outputValidation.detail;
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
const workflowFilePath = options.workflowFilePath ?? TEMPLATE_FILE;
|
|
59
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(workflowFilePath), { recursive: true });
|
|
60
|
+
const persisted = {
|
|
61
|
+
...workflow,
|
|
62
|
+
version: "3.0",
|
|
63
|
+
edges: workflow.edges.map((edge) => edge.when === null
|
|
64
|
+
? { from: edge.from, to: edge.to, kind: "dependency" }
|
|
65
|
+
: { from: edge.from, to: edge.to, kind: "route", route: edge.when }),
|
|
66
|
+
};
|
|
67
|
+
(0, node_fs_1.writeFileSync)(workflowFilePath, JSON.stringify(persisted, null, 2), "utf8");
|
|
68
|
+
};
|
|
69
|
+
exports.saveWorkflowDefinitionWithStorage = saveWorkflowDefinitionWithStorage;
|
|
70
|
+
const loadPipelineTemplate = () => {
|
|
71
|
+
return (0, exports.loadPipelineTemplateWithStorage)({});
|
|
72
|
+
};
|
|
73
|
+
exports.loadPipelineTemplate = loadPipelineTemplate;
|
|
74
|
+
const loadPipelineTemplateWithStorage = (options) => {
|
|
75
|
+
const workflow = (0, exports.loadWorkflowDefinitionWithStorage)(options);
|
|
76
|
+
const legacy = (0, template_mapper_1.workflowToTemplateNodes)(workflow);
|
|
77
|
+
return legacy;
|
|
78
|
+
};
|
|
79
|
+
exports.loadPipelineTemplateWithStorage = loadPipelineTemplateWithStorage;
|
|
80
|
+
const savePipelineTemplate = (nodes) => {
|
|
81
|
+
(0, exports.savePipelineTemplateWithStorage)(nodes, {});
|
|
82
|
+
};
|
|
83
|
+
exports.savePipelineTemplate = savePipelineTemplate;
|
|
84
|
+
const savePipelineTemplateWithStorage = (nodes, options) => {
|
|
85
|
+
const current = (0, exports.loadWorkflowDefinitionWithStorage)(options);
|
|
86
|
+
const merged = (0, template_mapper_1.mergeTemplateNodesIntoWorkflow)(current, nodes);
|
|
87
|
+
(0, exports.saveWorkflowDefinitionWithStorage)(merged, options);
|
|
88
|
+
};
|
|
89
|
+
exports.savePipelineTemplateWithStorage = savePipelineTemplateWithStorage;
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readWorkflowDefinitionFromRawDetailed = exports.readWorkflowDefinitionFromRaw = exports.normalizeWorkflowOutputConfig = exports.normalizeWorkflowGroup = exports.normalizeWorkflowEdge = exports.normalizeWorkflowNode = exports.normalizeTemplateNode = exports.normalizeWorkflowPlugins = exports.normalizeWorkflowScheduler = exports.isRecord = void 0;
|
|
4
|
+
const validate_1 = require("./validate");
|
|
5
|
+
const routes_1 = require("./routes");
|
|
6
|
+
// ====== Internal constants / helpers ======
|
|
7
|
+
const DEFAULT_REMOTE_BATCH_URL = String(process.env.OPENCLAW_PIPELINE_POOL_URL ?? "").trim();
|
|
8
|
+
const guards_1 = require("../../utils/guards");
|
|
9
|
+
Object.defineProperty(exports, "isRecord", { enumerable: true, get: function () { return guards_1.isRecord; } });
|
|
10
|
+
const isExecutorRole = (value) => value === "planner" || value === "coder" || value === "tester" || value === "reviewer" || value === "operator";
|
|
11
|
+
const normalizeNonEmptyString = (value) => {
|
|
12
|
+
if (typeof value !== "string")
|
|
13
|
+
return null;
|
|
14
|
+
const trimmed = value.trim();
|
|
15
|
+
return trimmed || null;
|
|
16
|
+
};
|
|
17
|
+
const normalizeStringArray = (value) => {
|
|
18
|
+
if (!Array.isArray(value))
|
|
19
|
+
return [];
|
|
20
|
+
const out = [];
|
|
21
|
+
for (const item of value) {
|
|
22
|
+
if (typeof item !== "string")
|
|
23
|
+
continue;
|
|
24
|
+
const trimmed = item.trim();
|
|
25
|
+
if (!trimmed)
|
|
26
|
+
continue;
|
|
27
|
+
out.push(trimmed);
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
};
|
|
31
|
+
const normalizeIntegerInRange = (value, fallback, min, max) => {
|
|
32
|
+
const num = Number(value);
|
|
33
|
+
if (!Number.isFinite(num))
|
|
34
|
+
return fallback;
|
|
35
|
+
return Math.min(max, Math.max(min, Math.trunc(num)));
|
|
36
|
+
};
|
|
37
|
+
const normalizeWorkflowScheduler = (value) => {
|
|
38
|
+
const record = (0, guards_1.isRecord)(value) ? value : {};
|
|
39
|
+
const loopGuard = (0, guards_1.isRecord)(record.loopGuard) ? record.loopGuard : {};
|
|
40
|
+
const mode = record.mode === "manual" ? "manual" : "auto";
|
|
41
|
+
const dispatchBy = record.dispatchBy === "node" ? "node" : "item";
|
|
42
|
+
return {
|
|
43
|
+
enabled: record.enabled !== false,
|
|
44
|
+
mode,
|
|
45
|
+
dispatchBy,
|
|
46
|
+
maxConcurrency: normalizeIntegerInRange(record.maxConcurrency, 3, 1, 20),
|
|
47
|
+
loopGuard: {
|
|
48
|
+
maxGlobalIterations: normalizeIntegerInRange(loopGuard.maxGlobalIterations, 200, 10, 100_000),
|
|
49
|
+
maxPerItemLoop: normalizeIntegerInRange(loopGuard.maxPerItemLoop, 8, 1, 100),
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
exports.normalizeWorkflowScheduler = normalizeWorkflowScheduler;
|
|
54
|
+
const normalizeRemoteBatchPlugin = (value) => {
|
|
55
|
+
const record = (0, guards_1.isRecord)(value) ? value : {};
|
|
56
|
+
return {
|
|
57
|
+
enabled: record.enabled === true,
|
|
58
|
+
url: normalizeNonEmptyString(record.url) ?? DEFAULT_REMOTE_BATCH_URL,
|
|
59
|
+
startBatch: normalizeIntegerInRange(record.startBatch, 1, 1, 1_000_000),
|
|
60
|
+
// 批大小与取数来源都属于低频配置,统一并入插件配置,避免主面板堆叠过多参数。
|
|
61
|
+
batchSize: normalizeIntegerInRange(record.batchSize, 5, 1, 1_000),
|
|
62
|
+
sourceField: normalizeNonEmptyString(record.sourceField) ?? "list30",
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
const normalizeSchedulerPlugin = (value) => {
|
|
66
|
+
const record = (0, guards_1.isRecord)(value) ? value : {};
|
|
67
|
+
return {
|
|
68
|
+
// 调度器历史上默认对所有流水线开启;插件化后继续保持默认开启,避免升级后界面和行为突然消失。
|
|
69
|
+
enabled: record.enabled !== false,
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
const normalizeWorkflowPlugins = (value) => {
|
|
73
|
+
const record = (0, guards_1.isRecord)(value) ? value : {};
|
|
74
|
+
return {
|
|
75
|
+
// 插件能力对所有流水线一致,这里只记录每条流水线自己的启用与参数配置。
|
|
76
|
+
remoteBatch: normalizeRemoteBatchPlugin(record.remoteBatch),
|
|
77
|
+
scheduler: normalizeSchedulerPlugin(record.scheduler),
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
exports.normalizeWorkflowPlugins = normalizeWorkflowPlugins;
|
|
81
|
+
const normalizeRoutePolicy = (value) => {
|
|
82
|
+
if (!(0, guards_1.isRecord)(value))
|
|
83
|
+
return null;
|
|
84
|
+
const allowed = (0, routes_1.normalizeRouteListWithDefaults)(normalizeStringArray(value.allowed));
|
|
85
|
+
if (allowed.length < 2 || allowed.length > 5)
|
|
86
|
+
return null;
|
|
87
|
+
return { allowed };
|
|
88
|
+
};
|
|
89
|
+
const normalizeRetryPolicy = (value) => {
|
|
90
|
+
const record = (0, guards_1.isRecord)(value) ? value : {};
|
|
91
|
+
return {
|
|
92
|
+
maxAttempts: normalizeIntegerInRange(record.maxAttempts, 2, 1, 10),
|
|
93
|
+
backoffMs: normalizeIntegerInRange(record.backoffMs, 0, 0, 600_000),
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
const normalizeExecutor = (value) => {
|
|
97
|
+
if (!(0, guards_1.isRecord)(value))
|
|
98
|
+
return null;
|
|
99
|
+
const role = value.role;
|
|
100
|
+
if (!isExecutorRole(role))
|
|
101
|
+
return null;
|
|
102
|
+
const agentId = normalizeNonEmptyString(value.agentId);
|
|
103
|
+
if (!agentId)
|
|
104
|
+
return null;
|
|
105
|
+
const fallbackAgentId = normalizeNonEmptyString(value.fallbackAgentId);
|
|
106
|
+
const sessionId = normalizeNonEmptyString(value.sessionId);
|
|
107
|
+
return {
|
|
108
|
+
agentId,
|
|
109
|
+
role,
|
|
110
|
+
fallbackAgentId,
|
|
111
|
+
sessionId,
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
const normalizeOutputSpec = (value) => {
|
|
115
|
+
if (!(0, guards_1.isRecord)(value))
|
|
116
|
+
return null;
|
|
117
|
+
const type = normalizeNonEmptyString(value.type);
|
|
118
|
+
if (!type)
|
|
119
|
+
return null;
|
|
120
|
+
const schemaVersion = normalizeIntegerInRange(value.schemaVersion, 0, 1, Number.MAX_SAFE_INTEGER);
|
|
121
|
+
if (schemaVersion <= 0)
|
|
122
|
+
return null;
|
|
123
|
+
return { type, schemaVersion };
|
|
124
|
+
};
|
|
125
|
+
const normalizeTemplateNode = (value) => {
|
|
126
|
+
if (!(0, guards_1.isRecord)(value))
|
|
127
|
+
return null;
|
|
128
|
+
const id = normalizeNonEmptyString(value.id);
|
|
129
|
+
const title = normalizeNonEmptyString(value.title);
|
|
130
|
+
if (!id || !title)
|
|
131
|
+
return null;
|
|
132
|
+
const executor = normalizeExecutor(value.executor);
|
|
133
|
+
if (!executor)
|
|
134
|
+
return null;
|
|
135
|
+
const outputSpec = normalizeOutputSpec(value.outputSpec);
|
|
136
|
+
if (!outputSpec)
|
|
137
|
+
return null;
|
|
138
|
+
const instruction = typeof value.instruction === "string" ? value.instruction.trim() : "";
|
|
139
|
+
const dependsOn = normalizeStringArray(value.dependsOn);
|
|
140
|
+
const allowReject = value.allowReject === true;
|
|
141
|
+
const maxRejectCount = normalizeIntegerInRange(value.maxRejectCount, 3, 0, 10);
|
|
142
|
+
return {
|
|
143
|
+
id,
|
|
144
|
+
title,
|
|
145
|
+
dependsOn,
|
|
146
|
+
executor,
|
|
147
|
+
instruction,
|
|
148
|
+
outputSpec,
|
|
149
|
+
allowReject,
|
|
150
|
+
maxRejectCount,
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
exports.normalizeTemplateNode = normalizeTemplateNode;
|
|
154
|
+
const normalizeWorkflowNode = (value) => {
|
|
155
|
+
if (!(0, guards_1.isRecord)(value))
|
|
156
|
+
return null;
|
|
157
|
+
const id = normalizeNonEmptyString(value.id);
|
|
158
|
+
const name = normalizeNonEmptyString(value.name);
|
|
159
|
+
if (!id || !name)
|
|
160
|
+
return null;
|
|
161
|
+
const executor = normalizeExecutor(value.executor);
|
|
162
|
+
if (!executor)
|
|
163
|
+
return null;
|
|
164
|
+
const outputSpec = normalizeOutputSpec(value.outputSpec);
|
|
165
|
+
if (!outputSpec)
|
|
166
|
+
return null;
|
|
167
|
+
const lane = value.lane === "branch" ? "branch" : "main";
|
|
168
|
+
const inputMode = value.inputMode === "batch" ? "batch" : "single";
|
|
169
|
+
const outputMode = value.outputMode === "array" ? "array" : "single";
|
|
170
|
+
const routePolicy = value.routePolicy === null ? null : normalizeRoutePolicy(value.routePolicy);
|
|
171
|
+
if (value.routePolicy !== undefined && value.routePolicy !== null && !routePolicy)
|
|
172
|
+
return null;
|
|
173
|
+
const dependencyPolicy = value.dependencyPolicy === "any" ? "any" : "all";
|
|
174
|
+
const branchScopeId = normalizeNonEmptyString(value.branchScopeId);
|
|
175
|
+
const routeSourceNodeId = normalizeNonEmptyString(value.routeSourceNodeId);
|
|
176
|
+
const routeValue = normalizeNonEmptyString(value.routeValue);
|
|
177
|
+
const node = {
|
|
178
|
+
id,
|
|
179
|
+
name,
|
|
180
|
+
type: normalizeNonEmptyString(value.type) ?? "task",
|
|
181
|
+
enabled: value.enabled !== false,
|
|
182
|
+
isMainline: value.isMainline !== false,
|
|
183
|
+
lane,
|
|
184
|
+
parallelGroupId: normalizeNonEmptyString(value.parallelGroupId),
|
|
185
|
+
executor,
|
|
186
|
+
inputMode,
|
|
187
|
+
outputMode,
|
|
188
|
+
dependencyPolicy,
|
|
189
|
+
routePolicy,
|
|
190
|
+
retryPolicy: normalizeRetryPolicy(value.retryPolicy),
|
|
191
|
+
outputSpec,
|
|
192
|
+
instruction: typeof value.instruction === "string" ? value.instruction.trim() : "",
|
|
193
|
+
allowReject: value.allowReject === true,
|
|
194
|
+
maxRejectCount: normalizeIntegerInRange(value.maxRejectCount, 3, 0, 10),
|
|
195
|
+
};
|
|
196
|
+
// 仅在显式提供时写入 branch scope 字段,避免 null 值污染 JSON 输出
|
|
197
|
+
if (branchScopeId)
|
|
198
|
+
node.branchScopeId = branchScopeId;
|
|
199
|
+
if (routeSourceNodeId)
|
|
200
|
+
node.routeSourceNodeId = routeSourceNodeId;
|
|
201
|
+
if (routeValue)
|
|
202
|
+
node.routeValue = routeValue;
|
|
203
|
+
return node;
|
|
204
|
+
};
|
|
205
|
+
exports.normalizeWorkflowNode = normalizeWorkflowNode;
|
|
206
|
+
const normalizeWorkflowEdge = (value) => {
|
|
207
|
+
if (!(0, guards_1.isRecord)(value))
|
|
208
|
+
return null;
|
|
209
|
+
const from = normalizeNonEmptyString(value.from);
|
|
210
|
+
const to = normalizeNonEmptyString(value.to);
|
|
211
|
+
if (!from || !to)
|
|
212
|
+
return null;
|
|
213
|
+
return {
|
|
214
|
+
from,
|
|
215
|
+
to,
|
|
216
|
+
when: normalizeNonEmptyString(value.when),
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
exports.normalizeWorkflowEdge = normalizeWorkflowEdge;
|
|
220
|
+
const normalizeWorkflowEdgeV3 = (value) => {
|
|
221
|
+
if (!(0, guards_1.isRecord)(value))
|
|
222
|
+
return null;
|
|
223
|
+
const from = normalizeNonEmptyString(value.from);
|
|
224
|
+
const to = normalizeNonEmptyString(value.to);
|
|
225
|
+
if (!from || !to)
|
|
226
|
+
return null;
|
|
227
|
+
// v3 API 闭环要求:读取接口返回运行时 when 形状时,写回 /workflow 也必须可直接接受。
|
|
228
|
+
// 这里仅在 version=3.0 契约下做"同版本双形状"归一化,不允许 version=2.0 旁路写入。
|
|
229
|
+
if ("when" in value) {
|
|
230
|
+
return {
|
|
231
|
+
from,
|
|
232
|
+
to,
|
|
233
|
+
when: normalizeNonEmptyString(value.when),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
const kind = value.kind;
|
|
237
|
+
if (kind === "dependency") {
|
|
238
|
+
return { from, to, when: null };
|
|
239
|
+
}
|
|
240
|
+
if (kind === "route") {
|
|
241
|
+
const route = normalizeNonEmptyString(value.route);
|
|
242
|
+
if (!route)
|
|
243
|
+
return null;
|
|
244
|
+
return { from, to, when: route };
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
};
|
|
248
|
+
const normalizeWorkflowGroup = (value) => {
|
|
249
|
+
if (!(0, guards_1.isRecord)(value))
|
|
250
|
+
return null;
|
|
251
|
+
const id = normalizeNonEmptyString(value.id);
|
|
252
|
+
if (!id)
|
|
253
|
+
return null;
|
|
254
|
+
const members = [...new Set(normalizeStringArray(value.members))];
|
|
255
|
+
if (members.length < 2)
|
|
256
|
+
return null;
|
|
257
|
+
const type = value.type === "parallel" ? "parallel" : null;
|
|
258
|
+
if (!type)
|
|
259
|
+
return null;
|
|
260
|
+
const joinPolicy = "all";
|
|
261
|
+
// 历史 any/quorum 降级:运行时仅支持 all,读取历史数据时静默降级为 all。
|
|
262
|
+
// 保存新 workflow 时 validate 会显式拒绝 any/quorum。
|
|
263
|
+
if (value.joinPolicy === "any" || value.joinPolicy === "quorum") {
|
|
264
|
+
// 静默降级 — 历史数据兼容,新保存会被 validate 拦截
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
id,
|
|
268
|
+
type,
|
|
269
|
+
members,
|
|
270
|
+
joinPolicy,
|
|
271
|
+
};
|
|
272
|
+
};
|
|
273
|
+
exports.normalizeWorkflowGroup = normalizeWorkflowGroup;
|
|
274
|
+
const normalizeWorkflowOutputConfig = (value) => {
|
|
275
|
+
const record = (0, guards_1.isRecord)(value) ? value : {};
|
|
276
|
+
const mode = record.mode === "explicit" ? "explicit" : "mainline_last";
|
|
277
|
+
const nodeId = mode === "explicit" ? normalizeNonEmptyString(record.nodeId) : null;
|
|
278
|
+
return { mode, nodeId };
|
|
279
|
+
};
|
|
280
|
+
exports.normalizeWorkflowOutputConfig = normalizeWorkflowOutputConfig;
|
|
281
|
+
// ====== Read raw / parse ======
|
|
282
|
+
const readWorkflowDefinitionFromRaw = (value) => {
|
|
283
|
+
const parsed = (0, exports.readWorkflowDefinitionFromRawDetailed)(value);
|
|
284
|
+
return parsed.ok ? parsed.workflow : null;
|
|
285
|
+
};
|
|
286
|
+
exports.readWorkflowDefinitionFromRaw = readWorkflowDefinitionFromRaw;
|
|
287
|
+
const readWorkflowDefinitionFromRawDetailed = (value) => {
|
|
288
|
+
if (!(0, guards_1.isRecord)(value)) {
|
|
289
|
+
return { ok: false, error: "invalid_workflow_definition", detail: "workflow 根对象格式非法" };
|
|
290
|
+
}
|
|
291
|
+
if (value.version === "2.0") {
|
|
292
|
+
return {
|
|
293
|
+
ok: false,
|
|
294
|
+
error: "workflow_migration_required",
|
|
295
|
+
detail: "检测到 workflow v2.0,请先执行迁移脚本再写入",
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
if (value.version !== "3.0") {
|
|
299
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `workflow.version 非法: ${String(value.version ?? "")}` };
|
|
300
|
+
}
|
|
301
|
+
if (!Array.isArray(value.nodes) || !Array.isArray(value.edges) || !Array.isArray(value.groups)) {
|
|
302
|
+
return { ok: false, error: "invalid_workflow_definition", detail: "workflow.nodes/edges/groups 必须为数组" };
|
|
303
|
+
}
|
|
304
|
+
const nodes = [];
|
|
305
|
+
for (const item of value.nodes) {
|
|
306
|
+
const normalized = (0, exports.normalizeWorkflowNode)(item);
|
|
307
|
+
if (!normalized)
|
|
308
|
+
return { ok: false, error: "invalid_workflow_definition", detail: "workflow.nodes 存在非法节点结构" };
|
|
309
|
+
nodes.push(normalized);
|
|
310
|
+
}
|
|
311
|
+
const edges = [];
|
|
312
|
+
for (const item of value.edges) {
|
|
313
|
+
const normalized = normalizeWorkflowEdgeV3(item);
|
|
314
|
+
if (!normalized)
|
|
315
|
+
return { ok: false, error: "invalid_workflow_definition", detail: "workflow.edges 存在非法边结构" };
|
|
316
|
+
edges.push(normalized);
|
|
317
|
+
}
|
|
318
|
+
const groups = [];
|
|
319
|
+
for (const item of value.groups) {
|
|
320
|
+
// 预检:在 normalize 静默降级之前显式拒绝不支持的 joinPolicy
|
|
321
|
+
if ((0, guards_1.isRecord)(item) && (item.joinPolicy === "any" || item.joinPolicy === "quorum")) {
|
|
322
|
+
return {
|
|
323
|
+
ok: false,
|
|
324
|
+
error: "join_policy_not_supported",
|
|
325
|
+
detail: `并行组 "${String(item.id ?? "?")}" 的 joinPolicy "${String(item.joinPolicy)}" 未支持,当前仅支持 "all"`,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
const normalized = (0, exports.normalizeWorkflowGroup)(item);
|
|
329
|
+
if (!normalized)
|
|
330
|
+
return { ok: false, error: "invalid_workflow_definition", detail: "workflow.groups 存在非法并行组结构" };
|
|
331
|
+
groups.push(normalized);
|
|
332
|
+
}
|
|
333
|
+
const workflow = {
|
|
334
|
+
version: "3.0",
|
|
335
|
+
scheduler: (0, exports.normalizeWorkflowScheduler)(value.scheduler),
|
|
336
|
+
plugins: (0, exports.normalizeWorkflowPlugins)(value.plugins),
|
|
337
|
+
output: (0, exports.normalizeWorkflowOutputConfig)(value.output),
|
|
338
|
+
nodes,
|
|
339
|
+
edges,
|
|
340
|
+
groups,
|
|
341
|
+
};
|
|
342
|
+
const validation = (0, validate_1.validateWorkflowGraph)(workflow);
|
|
343
|
+
if (!validation.ok)
|
|
344
|
+
return { ok: false, error: validation.error, detail: validation.detail };
|
|
345
|
+
return { ok: true, workflow };
|
|
346
|
+
};
|
|
347
|
+
exports.readWorkflowDefinitionFromRawDetailed = readWorkflowDefinitionFromRawDetailed;
|