@fieldwangai/agentflow 0.1.31 → 0.1.33

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.
@@ -281,6 +281,18 @@
281
281
  "displayName": "Git 拉取",
282
282
  "description": "克隆或更新 Git 仓库,并输出可供下游使用的工作区上下文"
283
283
  },
284
+ "tool_git_worktree_load": {
285
+ "displayName": "加载 Worktree",
286
+ "description": "创建或复用 Git worktree,并输出 workspaceContext 与 gitContext"
287
+ },
288
+ "tool_git_worktree_unload": {
289
+ "displayName": "卸载 Worktree",
290
+ "description": "移除 Git worktree,并恢复下游工作区上下文"
291
+ },
292
+ "tool_gitlab_create_mr": {
293
+ "displayName": "提交 GitLab MR",
294
+ "description": "为当前分支创建或复用 GitLab Merge Request,并输出 MR 链接"
295
+ },
284
296
  "tool_nodejs": {
285
297
  "displayName": "Node.js 脚本",
286
298
  "description": "执行 Node.js 脚本,以 exit code 判断成败,stdout 作为结果"
@@ -313,6 +325,14 @@
313
325
  "displayName": "ASCII 图展示",
314
326
  "description": "在 Workspace 画布中渲染等宽 ASCII 图,并将文本继续传给下游"
315
327
  },
328
+ "display_html": {
329
+ "displayName": "HTML 展示",
330
+ "description": "在 Workspace 画布中用 sandbox iframe 渲染 HTML,并将源码继续传给下游"
331
+ },
332
+ "display_image": {
333
+ "displayName": "图片展示",
334
+ "description": "在 Workspace 画布中预览图片 URL、data URL 或图片路径,并将来源继续传给下游"
335
+ },
316
336
  "provide_str": {
317
337
  "displayName": "文本",
318
338
  "description": "直接提供一段文本,value 会原样供下游引用"
@@ -320,6 +340,10 @@
320
340
  "provide_file": {
321
341
  "displayName": "文件",
322
342
  "description": "直接提供文件路径或内容,value 会原样供下游引用"
343
+ },
344
+ "provide_bool": {
345
+ "displayName": "布尔",
346
+ "description": "直接提供 true/false 布尔值,value 会供下游 bool 引脚引用"
323
347
  }
324
348
  },
325
349
  "pipeline": {
@@ -2,10 +2,17 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import yaml from "js-yaml";
4
4
 
5
- import { MARKETPLACE_PACKAGES_DIR } from "./paths.mjs";
5
+ import {
6
+ ARCHIVED_PIPELINES_DIR_NAME,
7
+ LEGACY_PIPELINES_DIR,
8
+ MARKETPLACE_PACKAGES_DIR,
9
+ PIPELINES_DIR,
10
+ getUserPipelinesRoot,
11
+ } from "./paths.mjs";
6
12
 
7
13
  const NODE_MANIFEST = "node.yaml";
8
14
  const COLLECTION_MANIFEST = "collection.yaml";
15
+ const FLOW_SNIPPET_MANIFEST = "flow-snippet.yaml";
9
16
  const LOCK_FILENAME = "agentflow.lock.json";
10
17
 
11
18
  function workspacePackageRoot(workspaceRoot) {
@@ -55,15 +62,20 @@ function readJsonObject(filePath) {
55
62
  function normalizeSlotList(value) {
56
63
  if (!Array.isArray(value)) return [];
57
64
  return value.map((slot) => {
58
- if (!slot || typeof slot !== "object") return { type: "text", name: "", default: "" };
65
+ if (!slot || typeof slot !== "object") return { type: "text", name: "", default: "", showOnNode: false };
59
66
  const type = slot.type != null ? String(slot.type).trim() : "text";
60
67
  const name = slot.name != null ? String(slot.name).trim() : "";
61
68
  const def = slot.default !== undefined ? slot.default : slot.value;
62
- return {
69
+ const normalized = {
63
70
  type,
64
71
  name,
65
72
  default: def == null ? "" : String(def),
66
73
  };
74
+ if (slot.required != null) normalized.required = Boolean(slot.required);
75
+ normalized.showOnNode = slot.showOnNode != null
76
+ ? Boolean(slot.showOnNode)
77
+ : Boolean(normalized.required) || type.toLowerCase() === "node";
78
+ return normalized;
67
79
  });
68
80
  }
69
81
 
@@ -100,6 +112,107 @@ function listVersionDirs(baseDir) {
100
112
  .filter(Boolean);
101
113
  }
102
114
 
115
+ function isSafePathSegment(value) {
116
+ const text = String(value || "").trim();
117
+ return Boolean(text) && !text.includes("\0") && !path.isAbsolute(text) && !text.split(/[\\/]+/).includes("..");
118
+ }
119
+
120
+ function resolveWorkspaceNodePackageDir(workspaceRoot, id, version) {
121
+ if (!isSafePathSegment(id) || !isSafePathSegment(version)) return null;
122
+ const base = path.resolve(workspacePackageRoot(workspaceRoot), "nodes");
123
+ const target = path.resolve(base, id, version);
124
+ if (target !== base && !target.startsWith(base + path.sep)) return null;
125
+ return target;
126
+ }
127
+
128
+ function resolveWorkspaceFlowSnippetPackageDir(workspaceRoot, id, version) {
129
+ if (!isSafePathSegment(id) || !isSafePathSegment(version)) return null;
130
+ const base = path.resolve(workspacePackageRoot(workspaceRoot), "flow-snippets");
131
+ const target = path.resolve(base, id, version);
132
+ if (target !== base && !target.startsWith(base + path.sep)) return null;
133
+ return target;
134
+ }
135
+
136
+ function collectFlowDirs(rootDir, source, archived = false) {
137
+ const out = [];
138
+ if (!fs.existsSync(rootDir)) return out;
139
+ let entries = [];
140
+ try {
141
+ entries = fs.readdirSync(rootDir, { withFileTypes: true });
142
+ } catch {
143
+ return out;
144
+ }
145
+ for (const entry of entries) {
146
+ if (!entry.isDirectory() || entry.name === ARCHIVED_PIPELINES_DIR_NAME) continue;
147
+ const dir = path.join(rootDir, entry.name);
148
+ if (!fs.existsSync(path.join(dir, "flow.yaml"))) continue;
149
+ out.push({ flowId: entry.name, flowSource: source, archived, flowDir: dir });
150
+ }
151
+ return out;
152
+ }
153
+
154
+ function listWritableFlowDirs(workspaceRoot, opts = {}) {
155
+ const root = path.resolve(workspaceRoot);
156
+ const userRoot = getUserPipelinesRoot(opts.userId);
157
+ const wsRoot = path.join(root, PIPELINES_DIR);
158
+ const legacyRoot = path.join(root, LEGACY_PIPELINES_DIR);
159
+ return [
160
+ ...collectFlowDirs(userRoot, "user", false),
161
+ ...collectFlowDirs(path.join(userRoot, ARCHIVED_PIPELINES_DIR_NAME), "user", true),
162
+ ...collectFlowDirs(wsRoot, "workspace", false),
163
+ ...collectFlowDirs(path.join(wsRoot, ARCHIVED_PIPELINES_DIR_NAME), "workspace", true),
164
+ ...collectFlowDirs(legacyRoot, "workspace", false),
165
+ ...collectFlowDirs(path.join(legacyRoot, ARCHIVED_PIPELINES_DIR_NAME), "workspace", true),
166
+ ];
167
+ }
168
+
169
+ function depMatchesNode(dep, id, version) {
170
+ if (typeof dep === "string") {
171
+ const parsed = parseMarketplaceDefinitionId(dep.startsWith("marketplace:") ? dep : `marketplace:${dep}`);
172
+ return Boolean(parsed && parsed.id === id && (!parsed.version || parsed.version === version));
173
+ }
174
+ if (!dep || typeof dep !== "object") return false;
175
+ return dep.id === id && (dep.version == null || String(dep.version) === version);
176
+ }
177
+
178
+ function instanceMatchesNode(inst, id, version) {
179
+ const parsed = parseMarketplaceDefinitionId(inst?.definitionId);
180
+ return Boolean(parsed && parsed.id === id && (!parsed.version || parsed.version === version));
181
+ }
182
+
183
+ export function listMarketplaceNodeUsages(workspaceRoot, id, version, opts = {}) {
184
+ const usages = [];
185
+ if (!id || !version) return usages;
186
+ for (const flow of listWritableFlowDirs(workspaceRoot, opts)) {
187
+ const flowYamlPath = path.join(flow.flowDir, "flow.yaml");
188
+ const data = readYamlObject(flowYamlPath);
189
+ if (!data) continue;
190
+ const hits = [];
191
+ const deps = data.dependencies && typeof data.dependencies === "object" ? data.dependencies : {};
192
+ const nodeDeps = Array.isArray(deps.nodes) ? deps.nodes : [];
193
+ for (const dep of nodeDeps) {
194
+ if (depMatchesNode(dep, id, version)) {
195
+ hits.push({ instanceId: "dependencies.nodes", label: "dependency" });
196
+ }
197
+ }
198
+ const instances = data.instances && typeof data.instances === "object" ? data.instances : {};
199
+ for (const [instanceId, inst] of Object.entries(instances)) {
200
+ if (instanceMatchesNode(inst, id, version)) {
201
+ hits.push({ instanceId, label: inst?.label || instanceId });
202
+ }
203
+ }
204
+ if (hits.length > 0) {
205
+ usages.push({
206
+ flowId: flow.flowId,
207
+ flowSource: flow.flowSource,
208
+ archived: flow.archived,
209
+ instances: hits,
210
+ });
211
+ }
212
+ }
213
+ return usages;
214
+ }
215
+
103
216
  function findNodePackageDir(workspaceRoot, id, version) {
104
217
  const root = workspacePackageRoot(workspaceRoot);
105
218
  const nodeBase = path.join(root, "nodes", id);
@@ -230,7 +343,7 @@ export function listMarketplaceNodes(workspaceRoot, flowData = null) {
230
343
  return out.sort((a, b) => a.id.localeCompare(b.id) || a.version.localeCompare(b.version));
231
344
  }
232
345
 
233
- export function listMarketplacePackages(workspaceRoot) {
346
+ export function listMarketplacePackages(workspaceRoot, opts = {}) {
234
347
  const root = workspacePackageRoot(workspaceRoot);
235
348
  const nodes = listMarketplaceNodes(workspaceRoot).map((n) => ({
236
349
  id: n.id,
@@ -242,6 +355,7 @@ export function listMarketplacePackages(workspaceRoot) {
242
355
  outputs: n.output,
243
356
  packagedFiles: Array.isArray(n.packagedFiles) ? n.packagedFiles : [],
244
357
  packageDir: n.packageDir,
358
+ usage: listMarketplaceNodeUsages(workspaceRoot, n.id, n.version, opts),
245
359
  }));
246
360
  const collections = [];
247
361
  const collectionsRoot = path.join(root, "collections");
@@ -265,6 +379,63 @@ export function listMarketplacePackages(workspaceRoot) {
265
379
  return { nodes, collections };
266
380
  }
267
381
 
382
+ export function listMarketplaceFlowSnippets(workspaceRoot) {
383
+ const root = workspacePackageRoot(workspaceRoot);
384
+ const snippetsRoot = path.join(root, "flow-snippets");
385
+ const snippets = [];
386
+ if (!fs.existsSync(snippetsRoot)) return { snippets };
387
+ for (const entry of fs.readdirSync(snippetsRoot, { withFileTypes: true })) {
388
+ if (!entry.isDirectory()) continue;
389
+ const base = path.join(snippetsRoot, entry.name);
390
+ for (const version of listVersionDirs(base)) {
391
+ const dir = path.join(base, version);
392
+ const manifest = readYamlObject(path.join(dir, FLOW_SNIPPET_MANIFEST));
393
+ if (!manifest) continue;
394
+ const snippet = manifest.snippet && typeof manifest.snippet === "object" ? manifest.snippet : {};
395
+ snippets.push({
396
+ id: manifest.id || entry.name,
397
+ version: manifest.version || version,
398
+ displayName: manifest.displayName || manifest.name || entry.name,
399
+ description: manifest.description || "",
400
+ tags: Array.isArray(manifest.tags) ? manifest.tags.map((x) => String(x)) : [],
401
+ nodeCount: Number(manifest.nodeCount) || Object.keys(snippet.instances || {}).length || 0,
402
+ edgeCount: Number(manifest.edgeCount) || (Array.isArray(snippet.edges) ? snippet.edges.length : 0),
403
+ createdAt: manifest.createdAt || "",
404
+ updatedAt: manifest.updatedAt || "",
405
+ packageDir: dir,
406
+ snippet,
407
+ });
408
+ }
409
+ }
410
+ snippets.sort((a, b) => {
411
+ const byTime = String(b.updatedAt || b.createdAt || "").localeCompare(String(a.updatedAt || a.createdAt || ""));
412
+ return byTime || a.id.localeCompare(b.id) || a.version.localeCompare(b.version);
413
+ });
414
+ return { snippets };
415
+ }
416
+
417
+ export function deleteMarketplaceNodePackage(workspaceRoot, id, version, opts = {}) {
418
+ const packageDir = resolveWorkspaceNodePackageDir(workspaceRoot, id, version);
419
+ if (!packageDir) return { ok: false, error: "Invalid marketplace node id or version" };
420
+ if (!fs.existsSync(path.join(packageDir, NODE_MANIFEST))) {
421
+ return { ok: false, error: `Marketplace node package not found: ${id}@${version}` };
422
+ }
423
+ const usage = listMarketplaceNodeUsages(workspaceRoot, id, version, opts);
424
+ if (usage.length > 0) {
425
+ return { ok: false, error: "Marketplace node is still used by flows", usage };
426
+ }
427
+ fs.rmSync(packageDir, { recursive: true, force: true });
428
+ const versionRoot = path.dirname(packageDir);
429
+ try {
430
+ if (fs.existsSync(versionRoot) && fs.readdirSync(versionRoot).length === 0) {
431
+ fs.rmdirSync(versionRoot);
432
+ }
433
+ } catch {
434
+ /* keep non-empty or unreadable parent */
435
+ }
436
+ return { ok: true, id, version, packageDir };
437
+ }
438
+
268
439
  export function writeFlowMarketplaceLock(workspaceRoot, flowDir, flowData) {
269
440
  if (!flowData || !flowData.instances || typeof flowData.instances !== "object") return null;
270
441
  const nodes = {};
@@ -465,11 +636,15 @@ export function publishNodeFromInstance(workspaceRoot, payload = {}, options = {
465
636
  type: slot.type,
466
637
  name: slot.name,
467
638
  default: slot.default,
639
+ ...(slot.required != null ? { required: Boolean(slot.required) } : {}),
640
+ ...(slot.showOnNode != null ? { showOnNode: Boolean(slot.showOnNode) } : {}),
468
641
  }));
469
642
  const outputs = normalizeSlotList(payload.outputs || payload.output).map((slot) => ({
470
643
  type: slot.type,
471
644
  name: slot.name,
472
645
  default: slot.default,
646
+ ...(slot.required != null ? { required: Boolean(slot.required) } : {}),
647
+ ...(slot.showOnNode != null ? { showOnNode: Boolean(slot.showOnNode) } : {}),
473
648
  }));
474
649
  const script = String(payload.script || "").trim();
475
650
  const body = String(payload.body || "").trim();
@@ -522,6 +697,57 @@ export function publishNodeFromInstance(workspaceRoot, payload = {}, options = {
522
697
  };
523
698
  }
524
699
 
700
+ export function publishFlowSnippet(workspaceRoot, payload = {}) {
701
+ const label = String(payload.displayName || payload.name || payload.id || "flow snippet").trim();
702
+ const id = safePackageId(payload.id || payload.packageId || label);
703
+ const version = normalizeVersion(payload.version || "1.0.0");
704
+ if (!id) return { ok: false, error: "Invalid snippet id" };
705
+
706
+ const rawSnippet = payload.snippet && typeof payload.snippet === "object" ? payload.snippet : {};
707
+ const instances = rawSnippet.instances && typeof rawSnippet.instances === "object" ? rawSnippet.instances : {};
708
+ const edges = Array.isArray(rawSnippet.edges) ? rawSnippet.edges : [];
709
+ const ui = rawSnippet.ui && typeof rawSnippet.ui === "object" ? rawSnippet.ui : {};
710
+ const nodeCount = Object.keys(instances).length;
711
+ if (nodeCount < 2) return { ok: false, error: "A flow snippet needs at least two nodes" };
712
+
713
+ const now = new Date().toISOString();
714
+ const dest = resolveWorkspaceFlowSnippetPackageDir(workspaceRoot, id, version);
715
+ if (!dest) return { ok: false, error: "Invalid snippet id or version" };
716
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
717
+ fs.rmSync(dest, { recursive: true, force: true });
718
+ fs.mkdirSync(dest, { recursive: true });
719
+
720
+ const manifest = {
721
+ id,
722
+ version,
723
+ name: label,
724
+ displayName: label,
725
+ description: String(payload.description || "").trim(),
726
+ tags: Array.isArray(payload.tags) ? payload.tags.map((x) => String(x).trim()).filter(Boolean) : [],
727
+ nodeCount,
728
+ edgeCount: edges.length,
729
+ createdAt: now,
730
+ updatedAt: now,
731
+ snippet: {
732
+ instances,
733
+ edges: edges.map((edge) => ({
734
+ source: String(edge.source || ""),
735
+ target: String(edge.target || ""),
736
+ sourceHandle: edge.sourceHandle ?? null,
737
+ targetHandle: edge.targetHandle ?? null,
738
+ })).filter((edge) => edge.source && edge.target),
739
+ ui,
740
+ },
741
+ };
742
+ fs.writeFileSync(path.join(dest, FLOW_SNIPPET_MANIFEST), yaml.dump(manifest, { lineWidth: -1 }), "utf-8");
743
+ fs.writeFileSync(
744
+ path.join(dest, "README.md"),
745
+ `# ${label}\n\n${manifest.description || "Published from an AgentFlow canvas selection."}\n`,
746
+ "utf-8",
747
+ );
748
+ return { ok: true, id, version, packageDir: dest, snippet: manifest.snippet };
749
+ }
750
+
525
751
  export function installFlowDependency(workspaceRoot, flowDir, spec) {
526
752
  const parsed = parseMarketplaceDefinitionId(spec.startsWith("marketplace:") ? spec : `marketplace:${spec}`);
527
753
  if (!parsed) return { ok: false, error: `Invalid marketplace node spec: ${spec}` };
package/bin/lib/paths.mjs CHANGED
@@ -262,11 +262,15 @@ export const LOCAL_ONLY_DEFINITION_IDS = new Set([
262
262
  "control_start",
263
263
  "control_end",
264
264
  "tool_git_checkout",
265
+ "tool_git_worktree_load",
266
+ "tool_git_worktree_unload",
267
+ "tool_gitlab_create_mr",
265
268
  "tool_print",
266
269
  "tool_user_check",
267
270
  "tool_user_ask",
268
271
  "provide_str",
269
272
  "provide_file",
273
+ "provide_bool",
270
274
  ]);
271
275
 
272
276
  /** 仅 pre+post 且由 CLI 负责写终态的节点 */
@@ -276,4 +280,5 @@ export const LOCAL_ONLY_TERMINAL_SUCCESS_IDS = new Set([
276
280
  "tool_print",
277
281
  "provide_str",
278
282
  "provide_file",
283
+ "provide_bool",
279
284
  ]);