@fieldwangai/agentflow 0.1.157 → 0.1.160
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 +10 -1
- package/bin/lib/control-while.mjs +336 -0
- package/bin/lib/flow-dsl/codegen.mjs +75 -12
- package/bin/lib/flow-dsl/ir.mjs +33 -2
- package/bin/lib/flow-dsl/lint.mjs +128 -3
- package/bin/lib/flow-dsl/parser.mjs +168 -4
- package/bin/lib/locales/en.json +4 -0
- package/bin/lib/locales/zh.json +4 -0
- package/bin/lib/marketplace.mjs +10 -2
- package/bin/lib/mcp-server.mjs +1 -0
- package/bin/lib/node-package-manifest.mjs +7 -2
- package/bin/lib/node-ui-kit.mjs +156 -0
- package/bin/lib/paths.mjs +2 -0
- package/bin/lib/workspace-flow-store.mjs +11 -2
- package/bin/lib/workspace-graph-merge.mjs +3 -0
- package/bin/lib/workspace-routes.mjs +10 -1
- package/bin/lib/workspace-server.mjs +601 -15
- package/bin/pipeline/validate-flow.mjs +3 -3
- package/builtin/nodes/control_parse_json.md +25 -0
- package/builtin/nodes/control_subflow_call.md +27 -0
- package/builtin/nodes/control_while.md +124 -0
- package/builtin/nodes/display_code.md +39 -0
- package/builtin/nodes/provide_json.md +14 -0
- package/builtin/nodes/workspace_subflow_input.md +14 -0
- package/builtin/pipelines/subflow-preview/workspace.flow.js +29 -0
- package/builtin/pipelines/subflow-preview/workspace.layout.json +38 -0
- package/builtin/pipelines/subflow-preview/workspace.nodes.json +19 -0
- package/builtin/pipelines/subflow-preview/workspace.state.json +93 -0
- package/builtin/pipelines/while-subflow-preview/workspace.flow.js +55 -0
- package/builtin/pipelines/while-subflow-preview/workspace.layout.json +66 -0
- package/builtin/pipelines/while-subflow-preview/workspace.nodes.json +49 -0
- package/builtin/pipelines/while-subflow-preview/workspace.state.json +194 -0
- package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-g_ljSViJ.js → WorkflowAssistantThread-Bfo9Ythw.js} +1 -1
- package/builtin/web-ui/dist/assets/index-XbeI5foV.js +872 -0
- package/builtin/web-ui/dist/assets/index-e1omCEau.css +1 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +2 -1
- package/reference/flow-control-capabilities.md +41 -16
- package/shared/slot-types.js +58 -0
- package/skills/agentflow-flow-dsl/SKILL.md +61 -7
- package/skills/agentflow-flow-dsl/references/node-calls.md +4 -0
- package/skills/agentflow-flow-dsl/references/subflow-authoring.md +230 -0
- package/skills/agentflow-node-dsl/SKILL.md +39 -0
- package/skills/agentflow-node-reference/references/builtin-nodes.md +33 -0
- package/builtin/web-ui/dist/assets/index-C0iq6zHl.js +0 -870
- package/builtin/web-ui/dist/assets/index-mmXs3H9P.css +0 -1
|
@@ -18,6 +18,7 @@ import fs from "fs";
|
|
|
18
18
|
import path from "path";
|
|
19
19
|
import { parse as acornParse } from "acorn";
|
|
20
20
|
|
|
21
|
+
import { slotTypeCompatibility } from "../../../shared/slot-types.js";
|
|
21
22
|
import { CTRL_SLOTS, RUN_DEFINITIONS, DEFINITIONS, definitionOf } from "./defs.mjs";
|
|
22
23
|
import { packageResolverFor, scanAvailableNodePackages } from "./packages.mjs";
|
|
23
24
|
import { FLOW_SOURCE_FILENAME } from "./index.mjs";
|
|
@@ -43,7 +44,21 @@ const BANNED_SYNTAX = {
|
|
|
43
44
|
};
|
|
44
45
|
|
|
45
46
|
/** 这两类节点的槽位由实例自己定义,不受定义表约束。 */
|
|
46
|
-
const CUSTOM_SLOTS_ALLOWED = new Set(["agent_subAgent", "tool_nodejs"]);
|
|
47
|
+
const CUSTOM_SLOTS_ALLOWED = new Set(["agent_subAgent", "tool_nodejs", "control_subflow_call"]);
|
|
48
|
+
|
|
49
|
+
function declaredSlot(node, kind, name) {
|
|
50
|
+
const slots = node?.packageDef?.[kind]
|
|
51
|
+
|| definitionOf(node?.definitionId)?.[kind === "input" ? "input" : "output"]
|
|
52
|
+
|| [];
|
|
53
|
+
return slots.find((slot) => String(slot?.name || "") === String(name || "")) || null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function declaredSlotType(node, kind, name) {
|
|
57
|
+
const slot = declaredSlot(node, kind, name);
|
|
58
|
+
if (slot?.type) return String(slot.type);
|
|
59
|
+
if (kind === "input" && node?.inputTypes?.[name]) return String(node.inputTypes[name]);
|
|
60
|
+
return "";
|
|
61
|
+
}
|
|
47
62
|
|
|
48
63
|
function walk(node, visit) {
|
|
49
64
|
if (!node || typeof node !== "object") return;
|
|
@@ -131,6 +146,28 @@ export function lintFlowDir(flowDir, opts = {}) {
|
|
|
131
146
|
}
|
|
132
147
|
|
|
133
148
|
const N = ir.nodes;
|
|
149
|
+
const subflows = ir?.subflows && typeof ir.subflows === "object" ? ir.subflows : {};
|
|
150
|
+
|
|
151
|
+
const ownerOf = new Map();
|
|
152
|
+
for (const [subflowId, subflow] of Object.entries(subflows)) {
|
|
153
|
+
if (!(subflow.roots || []).length) errors.push(`${subflowId}: 子流程没有执行入口`);
|
|
154
|
+
for (const nodeId of subflow.nodeIds || []) {
|
|
155
|
+
if (!N[nodeId]) errors.push(`${subflowId}: 成员 ${nodeId} 不存在`);
|
|
156
|
+
if (ownerOf.has(nodeId) && ownerOf.get(nodeId) !== subflowId) {
|
|
157
|
+
errors.push(`${nodeId}: 不能同时属于子流程 ${ownerOf.get(nodeId)} 和 ${subflowId}`);
|
|
158
|
+
} else ownerOf.set(nodeId, subflowId);
|
|
159
|
+
}
|
|
160
|
+
for (const [name, binding] of Object.entries(subflow.inputs || {})) {
|
|
161
|
+
if (N[binding.nodeId]?.definitionId !== "workspace_subflow_input") {
|
|
162
|
+
errors.push(`${subflowId}.${name}: 输入代理 ${binding.nodeId} 不是 flow.input`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
for (const [name, binding] of Object.entries(subflow.outputs || {})) {
|
|
166
|
+
if (!ownerOf.has(binding.nodeId) && !(subflow.nodeIds || []).includes(binding.nodeId)) {
|
|
167
|
+
errors.push(`${subflowId}.${name}: 输出来源 ${binding.nodeId} 不在子流程内`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
134
171
|
|
|
135
172
|
for (const [id, node] of Object.entries(N)) {
|
|
136
173
|
const definitionId = node.definitionId;
|
|
@@ -139,6 +176,53 @@ export function lintFlowDir(flowDir, opts = {}) {
|
|
|
139
176
|
errors.push(`${id}: 未知节点类型 ${definitionId}`);
|
|
140
177
|
continue;
|
|
141
178
|
}
|
|
179
|
+
if (definitionId === "control_subflow_call" && !subflows[node.attrs?.subflowId]) {
|
|
180
|
+
errors.push(`${id}: 引用的子流程 ${node.attrs?.subflowId || "(empty)"} 不存在`);
|
|
181
|
+
}
|
|
182
|
+
if (definitionId === "control_while") {
|
|
183
|
+
const conditionId = String(node.attrs?.conditionSubflowId || "");
|
|
184
|
+
const bodyId = String(node.attrs?.bodySubflowId || "");
|
|
185
|
+
const hasSubflowRefs = Boolean(conditionId || bodyId);
|
|
186
|
+
if (hasSubflowRefs && node.script) {
|
|
187
|
+
errors.push(`${id}: control.while 不能同时声明 step 脚本和 Condition/Body 子流程`);
|
|
188
|
+
} else if (hasSubflowRefs) {
|
|
189
|
+
if (!conditionId || !bodyId) {
|
|
190
|
+
errors.push(`${id}: control.while 必须同时声明 Condition 和 Body 子流程`);
|
|
191
|
+
} else if (conditionId === bodyId) {
|
|
192
|
+
errors.push(`${id}: control.while 的 Condition 和 Body 必须是不同子流程`);
|
|
193
|
+
}
|
|
194
|
+
const condition = subflows[conditionId];
|
|
195
|
+
const body = subflows[bodyId];
|
|
196
|
+
if (!condition) errors.push(`${id}: Condition 子流程 ${conditionId || "(empty)"} 不存在`);
|
|
197
|
+
if (!body) errors.push(`${id}: Body 子流程 ${bodyId || "(empty)"} 不存在`);
|
|
198
|
+
for (const name of ["state", "iteration"]) {
|
|
199
|
+
if (condition && !condition.inputs?.[name]) errors.push(`${id}: Condition 子流程 ${conditionId} 缺少输入 ${name}`);
|
|
200
|
+
}
|
|
201
|
+
if (condition && !condition.outputs?.decision) {
|
|
202
|
+
errors.push(`${id}: Condition 子流程 ${conditionId} 缺少输出 decision`);
|
|
203
|
+
}
|
|
204
|
+
for (const name of ["state", "iteration", "idempotencyKey"]) {
|
|
205
|
+
if (body && !body.inputs?.[name]) errors.push(`${id}: Body 子流程 ${bodyId} 缺少输入 ${name}`);
|
|
206
|
+
}
|
|
207
|
+
if (body && !body.outputs?.state) errors.push(`${id}: Body 子流程 ${bodyId} 缺少输出 state`);
|
|
208
|
+
for (const [contract, name, expected] of [
|
|
209
|
+
[condition?.inputs, "state", "json"],
|
|
210
|
+
[condition?.inputs, "iteration", "text"],
|
|
211
|
+
[condition?.outputs, "decision", "text"],
|
|
212
|
+
[body?.inputs, "state", "json"],
|
|
213
|
+
[body?.inputs, "iteration", "text"],
|
|
214
|
+
[body?.inputs, "idempotencyKey", "text"],
|
|
215
|
+
[body?.outputs, "state", "json"],
|
|
216
|
+
]) {
|
|
217
|
+
const actual = String(contract?.[name]?.type || "");
|
|
218
|
+
if (actual && actual !== expected) {
|
|
219
|
+
errors.push(`${id}: While 契约 ${name} 必须是 ${expected},当前是 ${actual}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} else if (!node.script) {
|
|
223
|
+
errors.push(`${id}: control.while 需要 step 脚本,或 Condition/Body 两个子流程`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
142
226
|
// 运行时支持程度来自各节点 .md 的 runtime: 字段,不是这里的第二份清单
|
|
143
227
|
if (def.runtime === "degraded") {
|
|
144
228
|
warnings.push(`${id}: ${definitionId} 无专用 handler,靠通用 agent + 输出契约工作;结果必须恰好是 true/false`);
|
|
@@ -171,6 +255,11 @@ export function lintFlowDir(flowDir, opts = {}) {
|
|
|
171
255
|
errors.push(`边引用了未声明的节点 ${dst}`);
|
|
172
256
|
continue;
|
|
173
257
|
}
|
|
258
|
+
const srcOwner = ownerOf.get(src) || "";
|
|
259
|
+
const dstOwner = ownerOf.get(dst) || "";
|
|
260
|
+
if (srcOwner !== dstOwner) {
|
|
261
|
+
errors.push(`子流程边界禁止直接连线: ${src}.${fromSlot} (${srcOwner || "父流程"}) -> ${dst}.${toSlot} (${dstOwner || "父流程"});请通过 flow.call 契约传值`);
|
|
262
|
+
}
|
|
174
263
|
const def = lookupDef(N[src].definitionId);
|
|
175
264
|
if (def && !CTRL_SLOTS.has(fromSlot)
|
|
176
265
|
&& !def.output.some((s) => s.name === fromSlot)
|
|
@@ -186,6 +275,18 @@ export function lintFlowDir(flowDir, opts = {}) {
|
|
|
186
275
|
if (CTRL_SLOTS.has(fromSlot) && def && !def.output.some((s) => s.name === fromSlot)) {
|
|
187
276
|
errors.push(`${src}[${N[src].definitionId}] 没有 ${fromSlot} 槽,控制流串不下去`);
|
|
188
277
|
}
|
|
278
|
+
const sourceType = declaredSlotType(N[src], "output", fromSlot);
|
|
279
|
+
// 自定义输入槽没有独立声明时会跟随上游类型,因此只对有明确目标类型的边做校验。
|
|
280
|
+
const targetType = declaredSlotType(N[dst], "input", toSlot);
|
|
281
|
+
if (sourceType && targetType) {
|
|
282
|
+
const compatibility = slotTypeCompatibility(sourceType, targetType);
|
|
283
|
+
if (!compatibility.compatible) {
|
|
284
|
+
errors.push(
|
|
285
|
+
`类型不兼容: ${src}.${fromSlot}(${compatibility.source}) -> `
|
|
286
|
+
+ `${dst}.${toSlot}(${compatibility.target});请改用同类型引脚或显式转换节点`,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
189
290
|
const target = `${dst}|${toSlot}`;
|
|
190
291
|
if (inputSeen.has(target)) {
|
|
191
292
|
errors.push(`fan-in 禁止: ${dst}.${toSlot} 被 ${inputSeen.get(target)} 和 ${src} 同时连入`);
|
|
@@ -211,6 +312,32 @@ export function lintFlowDir(flowDir, opts = {}) {
|
|
|
211
312
|
};
|
|
212
313
|
for (const id of Object.keys(N)) if (!color.has(id)) visit(id);
|
|
213
314
|
|
|
315
|
+
// 图本身仍是 DAG,但子流程调用关系也不能递归,否则会形成运行时调用环。
|
|
316
|
+
const callGraph = new Map(Object.keys(subflows).map((id) => [id, []]));
|
|
317
|
+
for (const [nodeId, node] of Object.entries(N)) {
|
|
318
|
+
const owner = ownerOf.get(nodeId);
|
|
319
|
+
if (!owner) continue;
|
|
320
|
+
if (node.definitionId === "control_subflow_call") {
|
|
321
|
+
const target = String(node.attrs?.subflowId || "");
|
|
322
|
+
if (target) callGraph.get(owner)?.push(target);
|
|
323
|
+
}
|
|
324
|
+
if (node.definitionId === "control_while") {
|
|
325
|
+
for (const target of [node.attrs?.conditionSubflowId, node.attrs?.bodySubflowId]) {
|
|
326
|
+
if (target) callGraph.get(owner)?.push(String(target));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const callColor = new Map();
|
|
331
|
+
const visitCall = (id) => {
|
|
332
|
+
callColor.set(id, 1);
|
|
333
|
+
for (const next of callGraph.get(id) || []) {
|
|
334
|
+
if (callColor.get(next) === 1) errors.push(`子流程递归调用禁止: ${id} -> ${next}`);
|
|
335
|
+
else if (!callColor.has(next)) visitCall(next);
|
|
336
|
+
}
|
|
337
|
+
callColor.set(id, 2);
|
|
338
|
+
};
|
|
339
|
+
for (const id of callGraph.keys()) if (!callColor.has(id)) visitCall(id);
|
|
340
|
+
|
|
214
341
|
for (const [id, node] of Object.entries(N)) {
|
|
215
342
|
if (node.definitionId !== "control_if") continue;
|
|
216
343
|
const edge = ir.edges.find((e) => e.endsWith(`|${id}|prediction`));
|
|
@@ -218,8 +345,6 @@ export function lintFlowDir(flowDir, opts = {}) {
|
|
|
218
345
|
errors.push(`${id}: control.if 的 prediction 未接线`);
|
|
219
346
|
} else {
|
|
220
347
|
const [src, slot] = edge.split("|");
|
|
221
|
-
const type = lookupDef(N[src]?.definitionId)?.output.find((s) => s.name === slot)?.type;
|
|
222
|
-
if (type && type !== "bool") errors.push(`${id}: prediction 只能接 bool,${src}.${slot} 是 ${type}`);
|
|
223
348
|
}
|
|
224
349
|
for (const [slot, label] of [["next1", "then"], ["next2", "else"]]) {
|
|
225
350
|
if (!ir.edges.some((e) => e.startsWith(`${id}|${slot}|`))) warnings.push(`${id}: control.if 缺 ${label} 分支`);
|
|
@@ -122,6 +122,8 @@ export function parseFlowSource(source, opts = {}) {
|
|
|
122
122
|
const pendingIf = [];
|
|
123
123
|
const runDecls = [];
|
|
124
124
|
const destructures = [];
|
|
125
|
+
const subflows = {};
|
|
126
|
+
const subflowOf = new Map();
|
|
125
127
|
|
|
126
128
|
// 先扫一遍解构声明:`const { storyId } = node;` 让后面引用 storyId 时能还原成边
|
|
127
129
|
for (const stmt of ast.body) {
|
|
@@ -151,7 +153,7 @@ export function parseFlowSource(source, opts = {}) {
|
|
|
151
153
|
if (tpl?.type !== "TemplateLiteral" || !tpl.expressions.length) return null;
|
|
152
154
|
// 本节点自己就有这个名字 -> `${x}` 是运行时占位符,原样留在正文里,不是 JS 插值
|
|
153
155
|
const def = definitionOf(definitionId);
|
|
154
|
-
const isScript = definitionId === "tool_nodejs";
|
|
156
|
+
const isScript = definitionId === "tool_nodejs" || definitionId === "control_while";
|
|
155
157
|
const ownSlot = (name) => def.input.some((s) => s.name === name)
|
|
156
158
|
|| nodes[id].extraIn.includes(name)
|
|
157
159
|
|| nodes[id].inputs[name] !== undefined
|
|
@@ -258,6 +260,35 @@ export function parseFlowSource(source, opts = {}) {
|
|
|
258
260
|
return out;
|
|
259
261
|
};
|
|
260
262
|
|
|
263
|
+
const flatItemIds = (items, out = []) => {
|
|
264
|
+
for (const item of items || []) {
|
|
265
|
+
if (typeof item === "string") out.push(item);
|
|
266
|
+
else if (item?.fork) for (const branch of item.fork) flatItemIds(branch, out);
|
|
267
|
+
}
|
|
268
|
+
return out;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const rootItemIds = (items) => {
|
|
272
|
+
const first = items?.[0];
|
|
273
|
+
if (typeof first === "string") return [first];
|
|
274
|
+
if (!first?.fork) return [];
|
|
275
|
+
return first.fork.map((branch) => flatItemIds(branch)[0]).filter(Boolean);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const objectEntries = (node, context) => {
|
|
279
|
+
if (!node || node.type !== "ObjectExpression") {
|
|
280
|
+
unresolvedAt(node, `${context} 必须是对象字面量`);
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
return node.properties.map((prop) => {
|
|
284
|
+
if (prop.type !== "Property" || prop.computed || prop.kind !== "init") {
|
|
285
|
+
unresolvedAt(prop, `${context} 只能包含静态字段`);
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
return [String(prop.key.name ?? prop.key.value ?? ""), prop.value];
|
|
289
|
+
}).filter(Boolean);
|
|
290
|
+
};
|
|
291
|
+
|
|
261
292
|
function linkChain(head, items) {
|
|
262
293
|
let prev = head;
|
|
263
294
|
let slot = "next";
|
|
@@ -300,11 +331,98 @@ export function parseFlowSource(source, opts = {}) {
|
|
|
300
331
|
const path = apiCalleePath(init.callee);
|
|
301
332
|
const args = [...init.arguments];
|
|
302
333
|
|
|
334
|
+
if (path === "flow.input") {
|
|
335
|
+
const name = stringOf(args[0]);
|
|
336
|
+
const type = stringOf(args[1]) || "text";
|
|
337
|
+
if (!name) unresolvedAt(init, `${id}: flow.input 的第一个参数必须是输入名`);
|
|
338
|
+
nodes[id] = {
|
|
339
|
+
definitionId: "workspace_subflow_input",
|
|
340
|
+
inputs: {},
|
|
341
|
+
inputTypes: {},
|
|
342
|
+
outputs: {},
|
|
343
|
+
extraIn: [],
|
|
344
|
+
extraOut: [],
|
|
345
|
+
declaredOut: [],
|
|
346
|
+
attrs: { subflowInputName: name || id, subflowInputType: type },
|
|
347
|
+
packageDef: { input: [], output: [{ name: "value", type }] },
|
|
348
|
+
label: name || id,
|
|
349
|
+
};
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
|
|
303
353
|
const first = args[0];
|
|
304
354
|
const hasLabel = first
|
|
305
355
|
&& ((first.type === "Literal" && typeof first.value === "string") || first.type === "TemplateLiteral");
|
|
306
356
|
const label = hasLabel ? stringOf(args.shift()) : null;
|
|
307
357
|
|
|
358
|
+
if (path === "flow.subflow") {
|
|
359
|
+
const inputObject = args.shift();
|
|
360
|
+
const sequence = args.shift();
|
|
361
|
+
const outputObject = args.shift();
|
|
362
|
+
const items = sequence?.type === "CallExpression" && apiCalleePath(sequence.callee) === "flow"
|
|
363
|
+
? itemsOf(sequence)
|
|
364
|
+
: [];
|
|
365
|
+
// The visual editor persists an empty flow() while its START/RETURN
|
|
366
|
+
// contract is still being wired. Keep that draft round-trippable;
|
|
367
|
+
// lint already reports a missing execution root and runtime refuses it.
|
|
368
|
+
const inputs = {};
|
|
369
|
+
for (const [name, value] of objectEntries(inputObject, `${id} inputs`)) {
|
|
370
|
+
if (value.type !== "Identifier" || nodes[value.name]?.definitionId !== "workspace_subflow_input") {
|
|
371
|
+
unresolvedAt(value, `${id}.${name}: 子流程输入必须引用 flow.input 变量`);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const proxy = nodes[value.name];
|
|
375
|
+
inputs[name] = {
|
|
376
|
+
nodeId: value.name,
|
|
377
|
+
slot: "value",
|
|
378
|
+
type: String(proxy.attrs?.subflowInputType || "text"),
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
const outputs = {};
|
|
382
|
+
for (const [name, value] of objectEntries(outputObject, `${id} outputs`)) {
|
|
383
|
+
const member = memberPath(value);
|
|
384
|
+
const ref = member || (value.type === "Identifier" ? varOf.get(value.name) : null);
|
|
385
|
+
if (!ref) {
|
|
386
|
+
unresolvedAt(value, `${id}.${name}: 子流程输出必须引用内部节点输出`);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const sourceNode = nodes[ref[0]];
|
|
390
|
+
const declaredSlot = sourceNode?.packageDef?.output?.find((slot) => slot.name === ref[1])
|
|
391
|
+
|| definitionOf(sourceNode?.definitionId).output.find((slot) => slot.name === ref[1]);
|
|
392
|
+
outputs[name] = { nodeId: ref[0], slot: ref[1], type: String(declaredSlot?.type || "text") };
|
|
393
|
+
}
|
|
394
|
+
const roots = rootItemIds(items);
|
|
395
|
+
subflows[id] = { id, label: label || id, inputs, outputs, roots, nodeIds: [] };
|
|
396
|
+
subflowOf.set(id, subflows[id]);
|
|
397
|
+
linkChain(null, items);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (path === "flow.call") {
|
|
402
|
+
const ref = args.shift();
|
|
403
|
+
const subflow = ref?.type === "Identifier" ? subflowOf.get(ref.name) : null;
|
|
404
|
+
if (!subflow) unresolvedAt(ref, `${id}: flow.call 第二个参数必须引用前面声明的 flow.subflow`);
|
|
405
|
+
const inputSlots = Object.entries(subflow?.inputs || {}).map(([name, binding]) => ({ name, type: binding.type || "text" }));
|
|
406
|
+
const outputSlots = Object.entries(subflow?.outputs || {}).map(([name, binding]) => ({ name, type: binding.type || "text" }));
|
|
407
|
+
nodes[id] = {
|
|
408
|
+
definitionId: "control_subflow_call",
|
|
409
|
+
inputs: {},
|
|
410
|
+
inputTypes: {},
|
|
411
|
+
outputs: {},
|
|
412
|
+
extraIn: inputSlots.map((slot) => slot.name),
|
|
413
|
+
extraOut: outputSlots.map((slot) => slot.name),
|
|
414
|
+
declaredOut: outputSlots.map((slot) => slot.name),
|
|
415
|
+
attrs: { subflowId: ref?.name || "" },
|
|
416
|
+
packageDef: {
|
|
417
|
+
input: [{ name: "prev", type: "node" }, ...inputSlots],
|
|
418
|
+
output: [{ name: "next", type: "node" }, ...outputSlots],
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
if (label) nodes[id].label = label;
|
|
422
|
+
readPins(id, "control_subflow_call", args[0]);
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
|
|
308
426
|
if (path === "flow" || path === "flow.schedule") {
|
|
309
427
|
const body = path === "flow.schedule" ? stringOf(args.shift()) : null;
|
|
310
428
|
runDecls.push({
|
|
@@ -364,7 +482,22 @@ export function parseFlowSource(source, opts = {}) {
|
|
|
364
482
|
}
|
|
365
483
|
readPins(id, definitionId, args[0]);
|
|
366
484
|
|
|
367
|
-
if (definitionId === "
|
|
485
|
+
if (definitionId === "control_while" && args[1]?.type === "Identifier") {
|
|
486
|
+
const conditionRef = args[1];
|
|
487
|
+
const bodyRef = args[2];
|
|
488
|
+
const conditionSubflow = subflowOf.get(conditionRef.name);
|
|
489
|
+
const bodySubflow = bodyRef?.type === "Identifier" ? subflowOf.get(bodyRef.name) : null;
|
|
490
|
+
if (!conditionSubflow) {
|
|
491
|
+
unresolvedAt(conditionRef, `${id}: control.while 第三个参数必须引用前面声明的 Condition 子流程`);
|
|
492
|
+
}
|
|
493
|
+
if (!bodySubflow) {
|
|
494
|
+
unresolvedAt(bodyRef || args[1], `${id}: control.while 第四个参数必须引用前面声明的 Body 子流程`);
|
|
495
|
+
}
|
|
496
|
+
nodes[id].attrs.conditionSubflowId = conditionRef.name || "";
|
|
497
|
+
nodes[id].attrs.bodySubflowId = bodyRef?.type === "Identifier" ? bodyRef.name : "";
|
|
498
|
+
} else if (definitionId === "control_while" && args[2]) {
|
|
499
|
+
unresolvedAt(args[2], `${id}: control.while 只能使用一个 step 脚本,或依次传入 Condition/Body 两个子流程`);
|
|
500
|
+
} else if (definitionId === "control_if") {
|
|
368
501
|
if (args[1]) pendingIf.push({ id, slot: "next1", call: args[1] });
|
|
369
502
|
if (args[2]) pendingIf.push({ id, slot: "next2", call: args[2] });
|
|
370
503
|
} else if (args[1]) {
|
|
@@ -396,7 +529,7 @@ export function parseFlowSource(source, opts = {}) {
|
|
|
396
529
|
}
|
|
397
530
|
}
|
|
398
531
|
if (body !== null) {
|
|
399
|
-
if (definitionId === "tool_nodejs") nodes[id].script = body;
|
|
532
|
+
if (definitionId === "tool_nodejs" || definitionId === "control_while") nodes[id].script = body;
|
|
400
533
|
else nodes[id].body = body;
|
|
401
534
|
}
|
|
402
535
|
}
|
|
@@ -460,5 +593,36 @@ export function parseFlowSource(source, opts = {}) {
|
|
|
460
593
|
node.extraOut = [...new Set(node.extraOut)];
|
|
461
594
|
}
|
|
462
595
|
|
|
463
|
-
|
|
596
|
+
// 子流程成员 = 显式控制链 + 输出来源 + 它们的数据依赖。边仍保存在统一 IR 中,
|
|
597
|
+
// 但成员清单让运行时能为每次 flow.call 建立隔离的调用帧。
|
|
598
|
+
const upstream = new Map();
|
|
599
|
+
for (const edge of edges) {
|
|
600
|
+
const [src, fromSlot, dst, toSlot] = edge.split("|");
|
|
601
|
+
if (toSlot === "prev") continue;
|
|
602
|
+
if (!upstream.has(dst)) upstream.set(dst, []);
|
|
603
|
+
upstream.get(dst).push(src);
|
|
604
|
+
}
|
|
605
|
+
for (const subflow of Object.values(subflows)) {
|
|
606
|
+
const members = new Set([
|
|
607
|
+
...Object.values(subflow.inputs).map((binding) => binding.nodeId),
|
|
608
|
+
...Object.values(subflow.outputs).map((binding) => binding.nodeId),
|
|
609
|
+
]);
|
|
610
|
+
const queue = [...subflow.roots, ...members];
|
|
611
|
+
const visited = new Set();
|
|
612
|
+
while (queue.length) {
|
|
613
|
+
const current = queue.shift();
|
|
614
|
+
if (!current || visited.has(current)) continue;
|
|
615
|
+
visited.add(current);
|
|
616
|
+
members.add(current);
|
|
617
|
+
for (const dep of upstream.get(current) || []) queue.push(dep);
|
|
618
|
+
for (const edge of edges) {
|
|
619
|
+
const [src, fromSlot, dst, toSlot] = edge.split("|");
|
|
620
|
+
if (src === current && toSlot === "prev") queue.push(dst);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
subflow.nodeIds = [...members].filter((nodeId) => nodes[nodeId]).sort();
|
|
624
|
+
for (const nodeId of subflow.nodeIds) nodes[nodeId].attrs.subflowId = subflow.id;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return { nodes, edges: [...new Set(edges)].sort(), subflows, unresolved };
|
|
464
628
|
}
|
package/bin/lib/locales/en.json
CHANGED
|
@@ -351,6 +351,10 @@
|
|
|
351
351
|
"displayName": "Markdown Display",
|
|
352
352
|
"description": "Render Markdown content on the Workspace canvas and pass the text downstream"
|
|
353
353
|
},
|
|
354
|
+
"display_code": {
|
|
355
|
+
"displayName": "Code Display",
|
|
356
|
+
"description": "Render highlighted source code with line numbers, copy, wrap, and download controls, then pass the code downstream"
|
|
357
|
+
},
|
|
354
358
|
"display_mermaid": {
|
|
355
359
|
"displayName": "Mermaid Display",
|
|
356
360
|
"description": "Render Mermaid diagram source on the Workspace canvas and pass the source downstream"
|
package/bin/lib/locales/zh.json
CHANGED
|
@@ -351,6 +351,10 @@
|
|
|
351
351
|
"displayName": "Markdown 展示",
|
|
352
352
|
"description": "在 Workspace 画布中渲染 Markdown 内容,并将文本继续传给下游"
|
|
353
353
|
},
|
|
354
|
+
"display_code": {
|
|
355
|
+
"displayName": "代码展示",
|
|
356
|
+
"description": "在 Workspace 画布中高亮展示代码,支持行号、复制、换行和下载,并将代码继续传给下游"
|
|
357
|
+
},
|
|
354
358
|
"display_mermaid": {
|
|
355
359
|
"displayName": "Mermaid 展示",
|
|
356
360
|
"description": "在 Workspace 画布中渲染 Mermaid 图,并将源码继续传给下游"
|
package/bin/lib/marketplace.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
isFlowDir,
|
|
12
12
|
} from "./paths.mjs";
|
|
13
13
|
import { NODE_PACKAGE_ENTRY, isNodePackageDir, readNodePackageManifest } from "./node-package-manifest.mjs";
|
|
14
|
+
import { normalizeNodeUiForSlots } from "./node-ui-kit.mjs";
|
|
14
15
|
import {
|
|
15
16
|
NODE_PACKAGE_METADATA_FILENAME,
|
|
16
17
|
createNodePackageArchive,
|
|
@@ -118,6 +119,9 @@ function normalizeManifest(raw, packageDir, source = "workspace") {
|
|
|
118
119
|
: runtime.type != null && String(runtime.type).trim() !== ""
|
|
119
120
|
? String(runtime.type).trim()
|
|
120
121
|
: "";
|
|
122
|
+
const input = normalizeSlotList(raw.input || raw.inputs);
|
|
123
|
+
const output = normalizeSlotList(raw.output || raw.outputs);
|
|
124
|
+
const ui = normalizeNodeUiForSlots(raw.ui, input, output);
|
|
121
125
|
return {
|
|
122
126
|
...raw,
|
|
123
127
|
id,
|
|
@@ -127,9 +131,10 @@ function normalizeManifest(raw, packageDir, source = "workspace") {
|
|
|
127
131
|
baseDefinitionId,
|
|
128
132
|
displayName: raw.displayName != null ? String(raw.displayName) : raw.name != null ? String(raw.name) : id,
|
|
129
133
|
description: raw.description != null ? String(raw.description) : "",
|
|
130
|
-
input
|
|
131
|
-
output
|
|
134
|
+
input,
|
|
135
|
+
output,
|
|
132
136
|
runtime,
|
|
137
|
+
...(ui ? { ui } : {}),
|
|
133
138
|
source,
|
|
134
139
|
};
|
|
135
140
|
}
|
|
@@ -474,6 +479,7 @@ export function listMarketplacePackages(workspaceRoot, opts = {}) {
|
|
|
474
479
|
description: n.description,
|
|
475
480
|
inputs: n.input,
|
|
476
481
|
outputs: n.output,
|
|
482
|
+
ui: n.ui,
|
|
477
483
|
packagedFiles: Array.isArray(n.packagedFiles) ? n.packagedFiles : [],
|
|
478
484
|
fileList: Array.isArray(n.fileList) ? n.fileList : [],
|
|
479
485
|
fileCount: Number(n.fileCount) || 0,
|
|
@@ -943,6 +949,7 @@ export function publishNodeFromInstance(workspaceRoot, payload = {}, options = {
|
|
|
943
949
|
const implementationMode = String(payload.implementationMode || "").trim();
|
|
944
950
|
const body = String(payload.body || "").trim();
|
|
945
951
|
const description = String(payload.description || body || `Published from node ${label}`).trim();
|
|
952
|
+
const ui = normalizeNodeUiForSlots(payload.ui, inputs, outputs);
|
|
946
953
|
const dest = path.join(workspacePackageRoot(workspaceRoot), "nodes", id, version);
|
|
947
954
|
const existingManifest = readYamlObject(path.join(dest, NODE_MANIFEST));
|
|
948
955
|
if (existingManifest) {
|
|
@@ -984,6 +991,7 @@ export function publishNodeFromInstance(workspaceRoot, payload = {}, options = {
|
|
|
984
991
|
runtime,
|
|
985
992
|
inputs,
|
|
986
993
|
outputs,
|
|
994
|
+
...(ui ? { ui } : {}),
|
|
987
995
|
ownerUserId,
|
|
988
996
|
createdBy: ownerUserId,
|
|
989
997
|
createdAt: existingManifest?.createdAt || now,
|
package/bin/lib/mcp-server.mjs
CHANGED
|
@@ -70,6 +70,7 @@ function query(params = {}) {
|
|
|
70
70
|
function displayKind(definitionId) {
|
|
71
71
|
const id = String(definitionId || "");
|
|
72
72
|
if (id === "display_markdown") return "markdown";
|
|
73
|
+
if (id === "display_code") return "code";
|
|
73
74
|
if (id === "display_mermaid") return "mermaid";
|
|
74
75
|
if (id === "display_ascii") return "ascii";
|
|
75
76
|
if (id === "display_html") return "html";
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import fs from "fs";
|
|
25
25
|
import path from "path";
|
|
26
26
|
import { parse as acornParse } from "acorn";
|
|
27
|
+
import { normalizeNodeUiForSlots } from "./node-ui-kit.mjs";
|
|
27
28
|
|
|
28
29
|
export const NODE_PACKAGE_ENTRY = "index.mjs";
|
|
29
30
|
|
|
@@ -135,15 +136,19 @@ export function nodePackageDeclarationToManifest(decl, packageDir) {
|
|
|
135
136
|
const id = decl.id != null ? String(decl.id).trim() : path.basename(packageDir);
|
|
136
137
|
const version = decl.version != null ? String(decl.version).trim() : "";
|
|
137
138
|
if (!id || !version) return null;
|
|
139
|
+
const input = slotMapToList(decl.inputs, "input");
|
|
140
|
+
const output = slotMapToList(decl.outputs, "output");
|
|
141
|
+
const ui = normalizeNodeUiForSlots(decl.ui, input, output);
|
|
138
142
|
return {
|
|
139
143
|
id,
|
|
140
144
|
version,
|
|
141
145
|
displayName: decl.name != null ? String(decl.name) : id,
|
|
142
146
|
description: decl.description != null ? String(decl.description) : "",
|
|
143
|
-
input
|
|
144
|
-
output
|
|
147
|
+
input,
|
|
148
|
+
output,
|
|
145
149
|
baseDefinitionId: "tool_nodejs",
|
|
146
150
|
runtime: { type: "tool_nodejs", entry: NODE_PACKAGE_ENTRY, mode: "module" },
|
|
151
|
+
...(ui ? { ui } : {}),
|
|
147
152
|
...(decl.ownerUserId != null ? { ownerUserId: String(decl.ownerUserId) } : {}),
|
|
148
153
|
...(decl.createdBy != null ? { createdBy: String(decl.createdBy) } : {}),
|
|
149
154
|
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node UI Kit v1.
|
|
3
|
+
*
|
|
4
|
+
* 节点包来自 Marketplace,UI 声明必须和节点声明一样是纯数据。这里把可接受的卡片组件收敛成
|
|
5
|
+
* 白名单,避免节点包把任意 HTML / React / 事件处理器带进 Workspace。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const CARD_TEMPLATES = new Set(["details", "state-machine"]);
|
|
9
|
+
const TONES = new Set(["neutral", "blue", "purple", "green", "amber", "red"]);
|
|
10
|
+
const SECTION_TYPES = new Set(["binding", "code", "decision", "metrics", "summary", "history", "subflow", "loop"]);
|
|
11
|
+
const CODE_FIELDS = new Set(["script", "scriptRef", "body", "implementationRef"]);
|
|
12
|
+
|
|
13
|
+
function object(value) {
|
|
14
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function shortText(value, max = 160) {
|
|
18
|
+
return String(value ?? "").trim().slice(0, max);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function token(value, max = 80) {
|
|
22
|
+
const text = shortText(value, max);
|
|
23
|
+
return /^[A-Za-z_][A-Za-z0-9_.:-]*$/.test(text) ? text : "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function tone(value, fallback = "neutral") {
|
|
27
|
+
const valueText = shortText(value, 20).toLowerCase();
|
|
28
|
+
return TONES.has(valueText) ? valueText : fallback;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeDecisionOptions(value) {
|
|
32
|
+
if (!Array.isArray(value)) return [];
|
|
33
|
+
return value.slice(0, 8).map((raw) => {
|
|
34
|
+
const item = object(raw);
|
|
35
|
+
const optionValue = token(item?.value, 40);
|
|
36
|
+
if (!item || !optionValue) return null;
|
|
37
|
+
return {
|
|
38
|
+
value: optionValue,
|
|
39
|
+
label: shortText(item.label || optionValue, 60),
|
|
40
|
+
tone: tone(item.tone),
|
|
41
|
+
...(item.description != null ? { description: shortText(item.description, 180) } : {}),
|
|
42
|
+
};
|
|
43
|
+
}).filter(Boolean);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeMetricItems(value) {
|
|
47
|
+
if (!Array.isArray(value)) return [];
|
|
48
|
+
return value.slice(0, 6).map((raw) => {
|
|
49
|
+
const item = object(raw);
|
|
50
|
+
if (!item) return null;
|
|
51
|
+
const input = token(item.input);
|
|
52
|
+
const output = token(item.output);
|
|
53
|
+
if (!input && !output) return null;
|
|
54
|
+
const maxInput = token(item.maxInput);
|
|
55
|
+
return {
|
|
56
|
+
label: shortText(item.label || output || input, 60),
|
|
57
|
+
...(input ? { input } : {}),
|
|
58
|
+
...(output ? { output } : {}),
|
|
59
|
+
...(maxInput ? { maxInput } : {}),
|
|
60
|
+
...(item.suffix != null ? { suffix: shortText(item.suffix, 24) } : {}),
|
|
61
|
+
};
|
|
62
|
+
}).filter(Boolean);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeSection(raw) {
|
|
66
|
+
const item = object(raw);
|
|
67
|
+
const type = shortText(item?.type, 30).toLowerCase();
|
|
68
|
+
if (!item || !SECTION_TYPES.has(type)) return null;
|
|
69
|
+
const label = shortText(item.label || type, 60);
|
|
70
|
+
if (type === "subflow") return { type, label };
|
|
71
|
+
if (type === "loop") {
|
|
72
|
+
const field = shortText(item.field || "script", 40);
|
|
73
|
+
return CODE_FIELDS.has(field) ? { type, label, field } : null;
|
|
74
|
+
}
|
|
75
|
+
if (type === "binding") {
|
|
76
|
+
const input = token(item.input);
|
|
77
|
+
return input ? { type, label, input } : null;
|
|
78
|
+
}
|
|
79
|
+
if (type === "code") {
|
|
80
|
+
const field = shortText(item.field, 40);
|
|
81
|
+
return CODE_FIELDS.has(field) ? { type, label, field } : null;
|
|
82
|
+
}
|
|
83
|
+
if (type === "decision") {
|
|
84
|
+
const output = token(item.output);
|
|
85
|
+
if (!output) return null;
|
|
86
|
+
return {
|
|
87
|
+
type,
|
|
88
|
+
label,
|
|
89
|
+
output,
|
|
90
|
+
...(item.source != null ? { source: shortText(item.source, 120) } : {}),
|
|
91
|
+
options: normalizeDecisionOptions(item.options),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (type === "metrics") {
|
|
95
|
+
const items = normalizeMetricItems(item.items);
|
|
96
|
+
return items.length ? { type, label, items } : null;
|
|
97
|
+
}
|
|
98
|
+
if (type === "summary" || type === "history") {
|
|
99
|
+
const output = token(item.output);
|
|
100
|
+
if (!output) return null;
|
|
101
|
+
return {
|
|
102
|
+
type,
|
|
103
|
+
label,
|
|
104
|
+
output,
|
|
105
|
+
...(type === "history" ? { limit: Math.min(8, Math.max(1, Number(item.limit) || 3)) } : {}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function normalizeNodeUi(value) {
|
|
112
|
+
const root = object(value);
|
|
113
|
+
const rawCard = object(root?.card);
|
|
114
|
+
if (!root || !rawCard) return undefined;
|
|
115
|
+
const templateText = shortText(rawCard.template, 40).toLowerCase();
|
|
116
|
+
const template = CARD_TEMPLATES.has(templateText) ? templateText : "details";
|
|
117
|
+
const icon = token(rawCard.icon, 48);
|
|
118
|
+
const sections = (Array.isArray(rawCard.sections) ? rawCard.sections : [])
|
|
119
|
+
.slice(0, 12)
|
|
120
|
+
.map(normalizeSection)
|
|
121
|
+
.filter(Boolean);
|
|
122
|
+
if (!icon && sections.length === 0) return undefined;
|
|
123
|
+
return {
|
|
124
|
+
version: 1,
|
|
125
|
+
card: {
|
|
126
|
+
template,
|
|
127
|
+
tone: tone(rawCard.tone),
|
|
128
|
+
...(icon ? { icon } : {}),
|
|
129
|
+
sections,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function normalizeNodeUiForSlots(value, inputs = [], outputs = []) {
|
|
135
|
+
const ui = normalizeNodeUi(value);
|
|
136
|
+
if (!ui) return undefined;
|
|
137
|
+
const inputNames = new Set((Array.isArray(inputs) ? inputs : []).map((slot) => String(slot?.name || "")));
|
|
138
|
+
const outputNames = new Set((Array.isArray(outputs) ? outputs : []).map((slot) => String(slot?.name || "")));
|
|
139
|
+
const sections = ui.card.sections.map((section) => {
|
|
140
|
+
if (section.type === "binding") return inputNames.has(section.input) ? section : null;
|
|
141
|
+
if (section.type === "decision" || section.type === "summary" || section.type === "history") {
|
|
142
|
+
return outputNames.has(section.output) ? section : null;
|
|
143
|
+
}
|
|
144
|
+
if (section.type === "metrics") {
|
|
145
|
+
const items = section.items.filter((item) => (
|
|
146
|
+
(!item.input || inputNames.has(item.input))
|
|
147
|
+
&& (!item.output || outputNames.has(item.output))
|
|
148
|
+
&& (!item.maxInput || inputNames.has(item.maxInput))
|
|
149
|
+
));
|
|
150
|
+
return items.length ? { ...section, items } : null;
|
|
151
|
+
}
|
|
152
|
+
return section;
|
|
153
|
+
}).filter(Boolean);
|
|
154
|
+
if (!ui.card.icon && sections.length === 0) return undefined;
|
|
155
|
+
return { ...ui, card: { ...ui.card, sections } };
|
|
156
|
+
}
|