@fieldwangai/agentflow 0.1.154 → 0.1.157

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.
@@ -150,7 +150,7 @@ export function layoutWorkspaceFlowDir(flowDir, { all = false, workspaceRoot = "
150
150
  }
151
151
 
152
152
  /** 没有节点丢失时的空损耗清单,让返回值形状始终一致。 */
153
- const NO_LOSS = { remapped: [], dropped: [], droppedEdges: [], warnings: [] };
153
+ const NO_LOSS = { remapped: [], dropped: [], droppedEdges: [], renamedIds: [], warnings: [] };
154
154
 
155
155
  /**
156
156
  * 把一个流程目录就地迁移成代码形态。两个来源:
@@ -207,6 +207,7 @@ function migrateLegacyYamlDir(dir, { force = false, marketplaceRoot = "" } = {})
207
207
  remapped: converted.remapped,
208
208
  dropped: converted.dropped,
209
209
  droppedEdges: converted.droppedEdges,
210
+ renamedIds: converted.renamedIds,
210
211
  warnings: converted.warnings,
211
212
  };
212
213
  // `control_end` 和指向它的边标了 benign——丢了等于没丢,不该拦住迁移。
@@ -70,6 +70,25 @@ export function generateFlowSource(ir, opts = {}) {
70
70
  return rel;
71
71
  };
72
72
 
73
+ // 节点 id 就是顶层变量名,可能刚好叫 display / flow / file。节点 id 不能为了代码生成
74
+ // 偷偷改掉(那会变成另一张图),所以冲突时给 DSL API import 起别名:
75
+ // `import { display as displayApi } ...; const display = displayApi.markdown(...)`。
76
+ // 所有顶层标识符共用 taken;先给内置 API 占位,后面的输出解构和包 import 也会避开它们。
77
+ const taken = new Set(Object.keys(N));
78
+ const uniqueName = (base, fallback) => {
79
+ let name = isIdentifier(base) ? base : fallback;
80
+ if (taken.has(name)) name = fallback;
81
+ while (taken.has(name)) name += "_";
82
+ taken.add(name);
83
+ return name;
84
+ };
85
+ const flowApiRoots = ["agent", "control", "display", "file", "flow", "provide", "tool", "workspace"];
86
+ const flowApiBinding = new Map(flowApiRoots.map((root) => [root, uniqueName(root, `${root}Api`)]));
87
+ const apiCall = (name) => {
88
+ const [root, ...tail] = String(name || "").split(".");
89
+ return [flowApiBinding.get(root) || root, ...tail].join(".");
90
+ };
91
+
73
92
  // 同一个节点的 body 和某个引脚值可能都超阈值,文件名必须带槽名区分:都叫
74
93
  // `prompts/<id>.md` 的话两段不同的文本会写进同一个文件,往返时后写的覆盖先写的。
75
94
  const externalize = (id, slot, text) => {
@@ -83,7 +102,7 @@ export function generateFlowSource(ir, opts = {}) {
83
102
  };
84
103
  const textArg = (id, slot, text) => {
85
104
  const rel = externalize(id, slot, text);
86
- return rel ? `file(${JSON.stringify(rel)})` : literal(text);
105
+ return rel ? `${apiCall("file")}(${JSON.stringify(rel)})` : literal(text);
87
106
  };
88
107
 
89
108
  const controlNext = new Map();
@@ -114,18 +133,6 @@ export function generateFlowSource(ir, opts = {}) {
114
133
  }),
115
134
  );
116
135
 
117
- // 文件里所有顶层标识符共用一个命名空间:节点 id、解构出来的输出变量、import 绑定名。
118
- // 分开发号就会撞——两个节点各有一个叫 ok 的自定义输出槽,是完全正常的图,但会生成两条
119
- // `const { ok } = ...`,文件根本解析不回来。
120
- const taken = new Set(Object.keys(N));
121
- const uniqueName = (base, fallback) => {
122
- let name = isIdentifier(base) ? base : fallback;
123
- if (taken.has(name)) name = fallback;
124
- while (taken.has(name)) name += "_";
125
- taken.add(name);
126
- return name;
127
- };
128
-
129
136
  // 自定义输出槽通过解构暴露成变量:`const { storyId } = node;`
130
137
  const outVar = new Map();
131
138
  for (const [id, slots] of destructured) {
@@ -143,7 +150,7 @@ export function generateFlowSource(ir, opts = {}) {
143
150
  const shared = [...bindingOf.values()].find((v) => v.spec === pkg.specifier);
144
151
  bindingOf.set(id, shared || { name: uniqueName(base, `${base}Node`), spec: pkg.specifier });
145
152
  }
146
- const callee = (id) => bindingOf.get(id)?.name || apiName(N[id].definitionId);
153
+ const callee = (id) => bindingOf.get(id)?.name || apiCall(apiName(N[id].definitionId));
147
154
 
148
155
  const bodyTextOf = (id) => String(
149
156
  (N[id].definitionId === "tool_nodejs" && N[id].script) ? N[id].script : (N[id].body || ""),
@@ -256,7 +263,7 @@ export function generateFlowSource(ir, opts = {}) {
256
263
  };
257
264
  const printItem = (item) => (
258
265
  Array.isArray(item)
259
- ? `flow.fork(${item.map((s) => `flow(${s.map(printItem).join(", ")})`).join(", ")})`
266
+ ? `${apiCall("flow.fork")}(${item.map((s) => `${apiCall("flow")}(${s.map(printItem).join(", ")})`).join(", ")})`
260
267
  : item
261
268
  );
262
269
 
@@ -286,8 +293,8 @@ export function generateFlowSource(ir, opts = {}) {
286
293
  if (isIf(id)) {
287
294
  const thenIds = (controlNext.get(id) || []).filter((x) => x.slot === "next1").map((x) => x.to);
288
295
  const elseIds = (controlNext.get(id) || []).filter((x) => x.slot === "next2").map((x) => x.to);
289
- args.push(`flow(${chainFrom(thenIds).map(printItem).join(", ")})`);
290
- args.push(`flow(${chainFrom(elseIds).map(printItem).join(", ")})`);
296
+ args.push(`${apiCall("flow")}(${chainFrom(thenIds).map(printItem).join(", ")})`);
297
+ args.push(`${apiCall("flow")}(${chainFrom(elseIds).map(printItem).join(", ")})`);
291
298
  } else {
292
299
  const usesScript = node.definitionId === "tool_nodejs" && node.script;
293
300
  const body = usesScript ? node.script : node.body;
@@ -323,14 +330,14 @@ export function generateFlowSource(ir, opts = {}) {
323
330
  if (N[runId].label) head.push(literal(N[runId].label));
324
331
  // 排程配置存在 run 节点的 body 里,是 JSON 字符串
325
332
  if (definitionId === "workspace_scheduled_run") head.push(N[runId].body ? literal(N[runId].body) : "null");
326
- const fn = definitionId === "workspace_scheduled_run" ? "flow.schedule" : "flow";
333
+ const fn = apiCall(definitionId === "workspace_scheduled_run" ? "flow.schedule" : "flow");
327
334
  out.push(`export const ${runId} = ${fn}(${[...head, ...seq].join(", ")});\n`);
328
335
  }
329
336
 
330
337
  // 一个 run 直接连到另一个 run:接力,不是子图
331
338
  for (const [src, list] of [...controlNext].sort()) {
332
339
  for (const x of list) {
333
- if (RUN_DEFINITIONS.has(N[x.to].definitionId)) out.push(`flow.resume(${src}, ${x.to});\n`);
340
+ if (RUN_DEFINITIONS.has(N[x.to].definitionId)) out.push(`${apiCall("flow.resume")}(${src}, ${x.to});\n`);
334
341
  }
335
342
  }
336
343
 
@@ -339,13 +346,17 @@ export function generateFlowSource(ir, opts = {}) {
339
346
  for (const id of ids) {
340
347
  if (declared.has(id) || RUN_DEFINITIONS.has(N[id].definitionId) || controlTargets.has(id)) continue;
341
348
  if (!(controlNext.get(id) || []).length) continue;
342
- out.push(`flow.detached(${chainFrom([id]).map(printItem).join(", ")});\n`);
349
+ out.push(`${apiCall("flow.detached")}(${chainFrom([id]).map(printItem).join(", ")});\n`);
343
350
  }
344
351
  for (const id of ids) {
345
352
  if (!declared.has(id) && !RUN_DEFINITIONS.has(N[id].definitionId)) declare(id, true);
346
353
  }
347
354
 
348
- const imports = [`import { agent, control, display, file, flow, provide, tool, workspace } from "agentflow/flow";`];
355
+ const flowImports = flowApiRoots.map((root) => {
356
+ const local = flowApiBinding.get(root);
357
+ return local === root ? root : `${root} as ${local}`;
358
+ });
359
+ const imports = [`import { ${flowImports.join(", ")} } from "agentflow/flow";`];
349
360
  const seenBinding = new Set();
350
361
  for (const id of ids) {
351
362
  const binding = bindingOf.get(id);
@@ -67,6 +67,34 @@ function slotsOf(instance, definitionId, kind) {
67
67
 
68
68
  const isNodeSlot = (slot) => String(slot?.type || "") === "node";
69
69
 
70
+ // DSL 里的节点 id 同时也是顶层 `const` 变量名。旧 YAML 允许 `foo-bar`、数字开头,
71
+ // 甚至 JS 关键字;直接交给 codegen 会生成无法解析的 workspace.flow.js,整张图只能退回
72
+ // graph.json。迁移时稳定地改成合法且唯一的绑定名,并把改名清单返回给调用方。
73
+ const JS_RESERVED_WORDS = new Set([
74
+ "arguments", "await", "break", "case", "catch", "class", "const", "continue", "debugger", "default",
75
+ "delete", "do", "else", "enum", "export", "extends", "false", "finally", "for", "function",
76
+ "if", "implements", "import", "in", "instanceof", "interface", "let", "new", "null", "package", "eval",
77
+ "private", "protected", "public", "return", "static", "super", "switch", "this", "throw", "true",
78
+ "try", "typeof", "var", "void", "while", "with", "yield",
79
+ ]);
80
+
81
+ function legacyNodeIdMap(ids) {
82
+ const used = new Set();
83
+ const mapping = new Map();
84
+ for (const raw of ids) {
85
+ let base = String(raw || "").replace(/[^A-Za-z0-9_$]/g, "_");
86
+ if (!/^[A-Za-z_$]/.test(base)) base = `node_${base}`;
87
+ if (!base) base = "node";
88
+ if (JS_RESERVED_WORDS.has(base)) base = `${base}_node`;
89
+ let next = base;
90
+ let suffix = 2;
91
+ while (used.has(next)) next = `${base}_${suffix++}`;
92
+ used.add(next);
93
+ mapping.set(raw, next);
94
+ }
95
+ return mapping;
96
+ }
97
+
70
98
  /**
71
99
  * 把老实例的槽位搬到新定义的槽位表上:先对名字,对不上的按位置兜底。
72
100
  *
@@ -121,6 +149,7 @@ function rebuildSlots(oldSlots, newDefSlots) {
121
149
  * remapped: Array<{ id: string, from: string, to: string, caveat?: string }>,
122
150
  * dropped: Array<{ id: string, definitionId: string, reason: string }>,
123
151
  * droppedEdges: Array<{ source: string, target: string, reason: string }>,
152
+ * renamedIds: Array<{ from: string, to: string }>,
124
153
  * warnings: string[],
125
154
  * }}
126
155
  */
@@ -129,10 +158,14 @@ export function legacyYamlToDesignGraph(yamlText) {
129
158
  if (!parsed || typeof parsed !== "object") throw new Error("flow.yaml 解析不出对象");
130
159
  const srcInstances = parsed.instances && typeof parsed.instances === "object" ? parsed.instances : {};
131
160
  const srcEdges = Array.isArray(parsed.edges) ? parsed.edges : [];
161
+ const idMap = legacyNodeIdMap(Object.keys(srcInstances));
132
162
 
133
163
  const remapped = [];
134
164
  const dropped = [];
135
165
  const droppedEdges = [];
166
+ const renamedIds = [...idMap]
167
+ .filter(([from, to]) => from !== to)
168
+ .map(([from, to]) => ({ from, to }));
136
169
  const warnings = [];
137
170
 
138
171
  /** id -> { input, output, renameIn, renameOut },用来重接边。 */
@@ -141,7 +174,8 @@ export function legacyYamlToDesignGraph(yamlText) {
141
174
  const benignDrops = new Set();
142
175
  const instances = {};
143
176
 
144
- for (const [id, raw] of Object.entries(srcInstances)) {
177
+ for (const [legacyId, raw] of Object.entries(srcInstances)) {
178
+ const id = idMap.get(legacyId);
145
179
  const instance = raw && typeof raw === "object" ? raw : {};
146
180
  const from = String(instance.definitionId || "");
147
181
  const oldIn = slotsOf(instance, from, "input");
@@ -154,17 +188,17 @@ export function legacyYamlToDesignGraph(yamlText) {
154
188
  // 终点节点本来就没有对应物需要表达。所以标 benign——报出来,但不算有损。
155
189
  if (hasRemap && to === null) {
156
190
  dropped.push({
157
- id,
191
+ id: legacyId,
158
192
  definitionId: from,
159
193
  benign: true,
160
194
  reason: "Workspace 没有终点节点,跑到没有后继就结束",
161
195
  });
162
- benignDrops.add(id);
196
+ benignDrops.add(legacyId);
163
197
  continue;
164
198
  }
165
199
  if (!hasRemap && definitionOf(from).runtime === "none") {
166
200
  dropped.push({
167
- id,
201
+ id: legacyId,
168
202
  definitionId: from,
169
203
  benign: false,
170
204
  reason: DEFINITIONS[from]
@@ -180,13 +214,14 @@ export function legacyYamlToDesignGraph(yamlText) {
180
214
  const rebuiltOut = rebuildSlots(oldOut, def.output || []);
181
215
  const next = { ...instance, definitionId: to, input: rebuiltIn.slots, output: rebuiltOut.slots };
182
216
  instances[id] = next;
183
- kept.set(id, {
217
+ kept.set(legacyId, {
218
+ id,
184
219
  input: next.input,
185
220
  output: next.output,
186
221
  renameIn: rebuiltIn.renames,
187
222
  renameOut: rebuiltOut.renames,
188
223
  });
189
- const entry = { id, from, to };
224
+ const entry = { id: legacyId, from, to };
190
225
  const droppedFields = [];
191
226
  for (const field of REMAP_DROPPED_FIELDS[to] || []) {
192
227
  const text = String(next[field] ?? "").trim();
@@ -204,7 +239,7 @@ export function legacyYamlToDesignGraph(yamlText) {
204
239
  // 拿定义去覆盖会把作者加的输入抹掉。
205
240
  const next = { ...instance, input: [...oldIn], output: [...oldOut] };
206
241
  instances[id] = next;
207
- kept.set(id, { input: next.input, output: next.output, renameIn: new Map(), renameOut: new Map() });
242
+ kept.set(legacyId, { id, input: next.input, output: next.output, renameIn: new Map(), renameOut: new Map() });
208
243
  }
209
244
  }
210
245
 
@@ -241,28 +276,28 @@ export function legacyYamlToDesignGraph(yamlText) {
241
276
  }
242
277
  // Workspace 禁止 fan-in:同一个输入槽只能有一条入边。老图里靠 control_anyOne
243
278
  // 汇合的分支,删掉汇合点之后会撞在同一个槽上。
244
- const key = `${target} input-${tgtIdx}`;
279
+ const key = `${to.id} input-${tgtIdx}`;
245
280
  if (takenTargets.has(key)) {
246
281
  droppedEdges.push({ source, target, benign: false, reason: `${target}.${tgtFinal} 已经有入边了,Workspace 不允许 fan-in` });
247
282
  continue;
248
283
  }
249
284
  takenTargets.add(key);
250
- edges.push({ source, target, sourceHandle: `output-${srcIdx}`, targetHandle: `input-${tgtIdx}` });
285
+ edges.push({ source: from.id, target: to.id, sourceHandle: `output-${srcIdx}`, targetHandle: `input-${tgtIdx}` });
251
286
  }
252
287
 
253
288
  // ── ui ────────────────────────────────────────────────────────────────────
254
289
  const srcUi = parsed.ui && typeof parsed.ui === "object" ? parsed.ui : {};
255
290
  const nodePositions = {};
256
291
  for (const [id, pos] of Object.entries(srcUi.nodePositions || {})) {
257
- if (kept.has(id)) nodePositions[id] = pos;
292
+ if (kept.has(id)) nodePositions[kept.get(id).id] = pos;
258
293
  }
259
294
  const ui = { nodePositions, nodeSizes: {} };
260
295
  if (typeof srcUi.description === "string" && srcUi.description.trim()) ui.description = srcUi.description;
261
296
 
262
297
  if (!Object.keys(instances).length) warnings.push("迁移之后一个节点都不剩");
263
- else if (![...kept.keys()].some((id) => instances[id].definitionId === "workspace_run")) {
298
+ else if (![...kept.values()].some(({ id }) => instances[id].definitionId === "workspace_run")) {
264
299
  warnings.push("图里没有运行节点(老流程缺 control_start);补一个 workspace_run 才能跑");
265
300
  }
266
301
 
267
- return { graph: { version: 1, instances, edges, ui }, remapped, dropped, droppedEdges, warnings };
302
+ return { graph: { version: 1, instances, edges, ui }, remapped, dropped, droppedEdges, renamedIds, warnings };
268
303
  }
@@ -59,6 +59,26 @@ export function parseFlowSource(source, opts = {}) {
59
59
  unresolved.push({ line: node?.loc?.start?.line || 0, message });
60
60
  };
61
61
 
62
+ // codegen 在节点 id 与 DSL API 名冲突时会生成
63
+ // `import { display as displayApi } from "agentflow/flow"`。静态解析时先把 local 名还原成
64
+ // canonical API 名,后面的节点类型、flow 控制语句和 file() 才仍走同一套规则。
65
+ const flowApiOf = new Map();
66
+ for (const stmt of ast.body) {
67
+ if (stmt.type !== "ImportDeclaration" || String(stmt.source.value) !== "agentflow/flow") continue;
68
+ for (const specifier of stmt.specifiers || []) {
69
+ if (specifier.type !== "ImportSpecifier") continue;
70
+ const imported = specifier.imported?.name ?? specifier.imported?.value;
71
+ const local = specifier.local?.name;
72
+ if (imported && local) flowApiOf.set(local, String(imported));
73
+ }
74
+ }
75
+ const apiCalleePath = (node) => {
76
+ const raw = calleePath(node);
77
+ if (!raw) return raw;
78
+ const [root, ...tail] = raw.split(".");
79
+ return [flowApiOf.get(root) || root, ...tail].join(".");
80
+ };
81
+
62
82
  function stringOf(node) {
63
83
  if (!node) return null;
64
84
  if (node.type === "Literal" && typeof node.value === "string") return node.value;
@@ -72,7 +92,7 @@ export function parseFlowSource(source, opts = {}) {
72
92
  return String(node.operator === "-" ? -node.argument.value : node.argument.value);
73
93
  }
74
94
  if (node.type === "TemplateLiteral" && node.expressions.length === 0) return node.quasis[0].value.cooked;
75
- if (node.type === "CallExpression" && calleePath(node.callee) === "file") {
95
+ if (node.type === "CallExpression" && apiCalleePath(node.callee) === "file") {
76
96
  const rel = stringOf(node.arguments[0]);
77
97
  if (rel == null) return null;
78
98
  if (!(rel in files)) throw new Error(`file(${JSON.stringify(rel)}) 找不到对应文件`);
@@ -229,7 +249,7 @@ export function parseFlowSource(source, opts = {}) {
229
249
  const out = [];
230
250
  for (const arg of call.arguments) {
231
251
  if (arg.type === "Identifier") out.push(arg.name);
232
- else if (arg.type === "CallExpression" && calleePath(arg.callee) === "flow.fork") {
252
+ else if (arg.type === "CallExpression" && apiCalleePath(arg.callee) === "flow.fork") {
233
253
  out.push({ fork: arg.arguments.map(itemsOf) });
234
254
  } else {
235
255
  unresolvedAt(arg, "控制流参数只能是节点变量名或 flow.fork(...)");
@@ -277,7 +297,7 @@ export function parseFlowSource(source, opts = {}) {
277
297
  unresolvedAt(d, `${id}: 节点声明右边必须是一次节点调用`);
278
298
  continue;
279
299
  }
280
- const path = calleePath(init.callee);
300
+ const path = apiCalleePath(init.callee);
281
301
  const args = [...init.arguments];
282
302
 
283
303
  const first = args[0];
@@ -384,7 +404,7 @@ export function parseFlowSource(source, opts = {}) {
384
404
  }
385
405
 
386
406
  if (decl?.type === "ExpressionStatement" && decl.expression.type === "CallExpression") {
387
- const path = calleePath(decl.expression.callee);
407
+ const path = apiCalleePath(decl.expression.callee);
388
408
  if (path === "flow.resume") {
389
409
  const [a, b] = decl.expression.arguments.map((x) => x.name);
390
410
  if (a && b) edges.push(`${a}|next|${b}|prev`);