@fieldwangai/agentflow 0.1.30 → 0.1.32

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.
Files changed (34) hide show
  1. package/bin/lib/agent-runners.mjs +26 -2
  2. package/bin/lib/api-runner.mjs +26 -3
  3. package/bin/lib/apply.mjs +6 -5
  4. package/bin/lib/catalog-flows.mjs +30 -5
  5. package/bin/lib/composer-agent.mjs +2 -1
  6. package/bin/lib/locales/en.json +4 -0
  7. package/bin/lib/locales/zh.json +6 -2
  8. package/bin/lib/marketplace.mjs +124 -2
  9. package/bin/lib/node-execute.mjs +1 -1
  10. package/bin/lib/paths.mjs +5 -0
  11. package/bin/lib/scheduler.mjs +3 -2
  12. package/bin/lib/ui-server.mjs +639 -8
  13. package/bin/lib/user-env.mjs +83 -0
  14. package/bin/pipeline/get-env.mjs +5 -29
  15. package/bin/pipeline/pre-process-node.mjs +28 -6
  16. package/bin/pipeline/run-tool-nodejs.mjs +7 -0
  17. package/builtin/nodes/agent_subAgent.md +6 -3
  18. package/builtin/nodes/control_cd_workspace.md +8 -6
  19. package/builtin/nodes/control_load_skills.md +2 -0
  20. package/builtin/nodes/control_user_workspace.md +20 -0
  21. package/builtin/nodes/display_ascii.md +5 -0
  22. package/builtin/nodes/display_markdown.md +5 -0
  23. package/builtin/nodes/display_mermaid.md +5 -0
  24. package/builtin/nodes/tool_git_checkout.md +3 -0
  25. package/builtin/web-ui/dist/assets/index-D0Tkhqr6.css +1 -0
  26. package/builtin/web-ui/dist/assets/index-DyhW5chp.js +197 -0
  27. package/builtin/web-ui/dist/index.html +2 -2
  28. package/package.json +1 -1
  29. package/skills/agentflow-workspace-ascii/SKILL.md +42 -0
  30. package/skills/agentflow-workspace-graph/SKILL.md +67 -0
  31. package/skills/agentflow-workspace-markdown/SKILL.md +44 -0
  32. package/skills/agentflow-workspace-mermaid/SKILL.md +43 -0
  33. package/builtin/web-ui/dist/assets/index-NdVOJLL9.js +0 -196
  34. package/builtin/web-ui/dist/assets/index-naVI6LZj.css +0 -1
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import fs from "fs";
9
9
  import http from "http";
10
+ import os from "os";
10
11
  import path from "path";
11
12
  import { execFile, spawn } from "child_process";
12
13
  import busboy from "busboy";
@@ -40,6 +41,7 @@ import { t } from "./i18n.mjs";
40
41
  import {
41
42
  PACKAGE_ROOT,
42
43
  getAgentflowUserConfigAbs,
44
+ getAgentflowUserDataRoot,
43
45
  getModelListsAbs,
44
46
  getRunDir,
45
47
  } from "./paths.mjs";
@@ -72,7 +74,7 @@ import {
72
74
  import { runNodeScript } from "./pipeline-scripts.mjs";
73
75
  import { readFlowSchedule, writeFlowSchedule } from "./schedule-config.mjs";
74
76
  import { listScheduleStatuses } from "./scheduler.mjs";
75
- import { installFlowDependency, listMarketplacePackages, publishNodeFromInstance } from "./marketplace.mjs";
77
+ import { deleteMarketplaceNodePackage, installFlowDependency, listMarketplacePackages, publishNodeFromInstance } from "./marketplace.mjs";
76
78
  import {
77
79
  authSetupRequired,
78
80
  buildClearSessionCookie,
@@ -81,6 +83,7 @@ import {
81
83
  loginOrCreateUser,
82
84
  logoutRequest,
83
85
  } from "./auth.mjs";
86
+ import { readUserEnvObject, readUserEnvRows, writeUserEnvRows } from "./user-env.mjs";
84
87
 
85
88
  const MIME = {
86
89
  ".html": "text/html; charset=utf-8",
@@ -92,6 +95,49 @@ const MIME = {
92
95
  };
93
96
 
94
97
  const RUN_CONFIG_FILENAME = "run-config.json";
98
+ const SKILL_COLLECTIONS_FILENAME = "skill-collections.json";
99
+ const BUILTIN_SKILL_COLLECTIONS = [
100
+ {
101
+ id: "pipeline",
102
+ name: "Pipeline",
103
+ defaultKeys: [
104
+ "agentflow-flow-add-instances",
105
+ "agentflow-flow-edit-node-fields",
106
+ "agentflow-flow-recipes",
107
+ "agentflow-flow-sync-ui",
108
+ "agentflow-node-reference",
109
+ "agentflow-placeholder-reference",
110
+ "agentflow-runtime-reference",
111
+ ],
112
+ },
113
+ {
114
+ id: "workspace",
115
+ name: "Workspace",
116
+ defaultKeys: [
117
+ "agentflow-workspace-graph",
118
+ "agentflow-workspace-markdown",
119
+ "agentflow-workspace-mermaid",
120
+ "agentflow-workspace-ascii",
121
+ "agentflow-node-reference",
122
+ "agentflow-placeholder-reference",
123
+ "agentflow-runtime-reference",
124
+ ],
125
+ legacyDefaultKeys: [
126
+ [
127
+ "agentflow-flow-add-instances",
128
+ "agentflow-flow-edit-node-fields",
129
+ "agentflow-node-reference",
130
+ "agentflow-placeholder-reference",
131
+ "agentflow-runtime-reference",
132
+ ],
133
+ [
134
+ "agentflow-node-reference",
135
+ "agentflow-placeholder-reference",
136
+ "agentflow-runtime-reference",
137
+ ],
138
+ ],
139
+ },
140
+ ];
95
141
 
96
142
  function json(res, status, obj) {
97
143
  const body = JSON.stringify(obj);
@@ -102,6 +148,155 @@ function json(res, status, obj) {
102
148
  res.end(body);
103
149
  }
104
150
 
151
+ function skillCollectionsAbs(userCtx = {}) {
152
+ return path.join(getAgentflowUserDataRoot(userCtx.userId), SKILL_COLLECTIONS_FILENAME);
153
+ }
154
+
155
+ function slugifySkillCollectionId(name, fallback = "collection") {
156
+ const raw = String(name || "").trim().toLowerCase();
157
+ const id = raw
158
+ .replace(/[^a-z0-9_-]+/g, "-")
159
+ .replace(/^-+|-+$/g, "")
160
+ .slice(0, 48);
161
+ return id || fallback;
162
+ }
163
+
164
+ function isBuiltinSkillCollectionId(id) {
165
+ return BUILTIN_SKILL_COLLECTIONS.some((collection) => collection.id === id);
166
+ }
167
+
168
+ function resolveSkillKeys(skillNamesOrKeys = [], availableSkills = []) {
169
+ const byToken = buildSkillKeyLookup(availableSkills);
170
+ return skillNamesOrKeys.map((key) => byToken.get(key)).filter(Boolean);
171
+ }
172
+
173
+ function buildSkillKeyLookup(availableSkills = []) {
174
+ const byToken = new Map();
175
+ for (const skill of availableSkills) {
176
+ const key = String(skill?.key || "").trim();
177
+ if (!key) continue;
178
+ for (const token of [skill.key, skill.name, skill.id]) {
179
+ const normalized = String(token || "").trim();
180
+ if (normalized && !byToken.has(normalized)) byToken.set(normalized, key);
181
+ }
182
+ }
183
+ return byToken;
184
+ }
185
+
186
+ function defaultSkillKeysForCollection(def, availableSkills = []) {
187
+ const exact = resolveSkillKeys(def.defaultKeys, availableSkills);
188
+ if (exact.length > 0) return exact;
189
+ return availableSkills
190
+ .filter((skill) => String(skill?.name || skill?.id || skill?.key || "").includes("agentflow-"))
191
+ .map((skill) => String(skill.key || "").trim())
192
+ .filter(Boolean);
193
+ }
194
+
195
+ function sameSkillKeySet(a = [], b = []) {
196
+ if (a.length !== b.length) return false;
197
+ const set = new Set(a);
198
+ return b.every((key) => set.has(key));
199
+ }
200
+
201
+ function normalizeSkillCollectionConfig(value) {
202
+ const now = Date.now();
203
+ const seenIds = new Set();
204
+ const collections = [];
205
+ const input = Array.isArray(value?.collections) ? value.collections : [];
206
+ for (const item of input) {
207
+ if (!item || typeof item !== "object") continue;
208
+ const name = String(item.name || item.id || "").trim().slice(0, 80);
209
+ if (!name) continue;
210
+ let id = slugifySkillCollectionId(item.id || name);
211
+ let suffix = 2;
212
+ while (seenIds.has(id)) {
213
+ id = `${slugifySkillCollectionId(item.id || name)}-${suffix++}`;
214
+ }
215
+ seenIds.add(id);
216
+ const skillSeen = new Set();
217
+ const skillKeys = [];
218
+ for (const key of Array.isArray(item.skillKeys) ? item.skillKeys : []) {
219
+ const normalized = String(key || "").trim();
220
+ if (!normalized || skillSeen.has(normalized)) continue;
221
+ skillSeen.add(normalized);
222
+ skillKeys.push(normalized);
223
+ }
224
+ collections.push({
225
+ id,
226
+ name,
227
+ skillKeys,
228
+ builtin: Boolean(item.builtin) || isBuiltinSkillCollectionId(id),
229
+ createdAt: Number.isFinite(item.createdAt) ? Number(item.createdAt) : now,
230
+ updatedAt: Number.isFinite(item.updatedAt) ? Number(item.updatedAt) : now,
231
+ });
232
+ }
233
+ return { version: 1, collections };
234
+ }
235
+
236
+ function withBuiltinSkillCollections(config, availableSkills = []) {
237
+ const normalized = normalizeSkillCollectionConfig(config);
238
+ const byId = new Map(normalized.collections.map((collection) => [collection.id, collection]));
239
+ const out = [];
240
+ const now = Date.now();
241
+ for (const def of BUILTIN_SKILL_COLLECTIONS) {
242
+ const existing = byId.get(def.id);
243
+ if (existing) {
244
+ const nextDefaultKeys = defaultSkillKeysForCollection(def, availableSkills);
245
+ const legacyDefaultSets = (Array.isArray(def.legacyDefaultKeys) ? def.legacyDefaultKeys : [])
246
+ .map((keys) => Array.isArray(keys) ? resolveSkillKeys(keys, availableSkills) : [])
247
+ .filter((keys) => keys.length > 0);
248
+ const shouldMigrateLegacyDefault =
249
+ existing.skillKeys.length > 0 &&
250
+ legacyDefaultSets.some((keys) => sameSkillKeySet(existing.skillKeys, keys));
251
+ out.push({
252
+ ...existing,
253
+ name: def.name,
254
+ builtin: true,
255
+ skillKeys: existing.skillKeys.length > 0 && !shouldMigrateLegacyDefault ? existing.skillKeys : nextDefaultKeys,
256
+ });
257
+ byId.delete(def.id);
258
+ } else {
259
+ out.push({
260
+ id: def.id,
261
+ name: def.name,
262
+ builtin: true,
263
+ skillKeys: defaultSkillKeysForCollection(def, availableSkills),
264
+ createdAt: now,
265
+ updatedAt: now,
266
+ });
267
+ }
268
+ }
269
+ out.push(...Array.from(byId.values()).map((collection) => ({ ...collection, builtin: false })));
270
+ return { version: 1, collections: out };
271
+ }
272
+
273
+ function readSkillCollectionConfig(userCtx = {}, availableSkills = []) {
274
+ const p = skillCollectionsAbs(userCtx);
275
+ try {
276
+ if (!fs.existsSync(p)) return withBuiltinSkillCollections({}, availableSkills);
277
+ return withBuiltinSkillCollections(JSON.parse(fs.readFileSync(p, "utf-8")), availableSkills);
278
+ } catch {
279
+ return withBuiltinSkillCollections({}, availableSkills);
280
+ }
281
+ }
282
+
283
+ function writeSkillCollectionConfig(userCtx = {}, payload = {}, availableSkills = []) {
284
+ const p = skillCollectionsAbs(userCtx);
285
+ const config = withBuiltinSkillCollections(payload, availableSkills);
286
+ fs.mkdirSync(path.dirname(p), { recursive: true });
287
+ fs.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", "utf-8");
288
+ return config;
289
+ }
290
+
291
+ function runtimeEnvForUser(userCtx = {}, extra = {}) {
292
+ return {
293
+ ...process.env,
294
+ ...readUserEnvObject(userCtx.userId),
295
+ ...extra,
296
+ AGENTFLOW_USER_ID: userCtx.userId || "",
297
+ };
298
+ }
299
+
105
300
  function readAgentflowUserConfigObject() {
106
301
  const p = getAgentflowUserConfigAbs();
107
302
  try {
@@ -427,25 +622,344 @@ function buildWorkspaceGeneratePrompt(payload) {
427
622
  "使用 +-|/\\<> 等字符表达结构,尽量保持对齐。",
428
623
  ].join("\n")
429
624
  : [
430
- "你是 workspace Markdown 节点的内容生成器。",
431
- "请根据用户 prompt 和上游节点/文件上下文,生成可直接保存到工作区的 Markdown 正文。",
432
- "只输出最终 Markdown 内容,不要解释你如何执行,也不要包裹代码围栏,除非正文本身需要代码块。",
625
+ "你是 AgentFlow Workspace Composer。",
626
+ "优先根据已选择的 Skills 操作 workspace.graph.json,创建或修改 workspace 画布节点、连线与展示节点。",
627
+ "如果用户请求需要项目分析、加载代码、整理流程或生成展示结果,不要只给泛泛回答;应先让 Skills 驱动画布建模,例如创建 Git/工作目录/Load Skills/Agent/Markdown Display 等合适节点。",
628
+ "只有当用户明确只是询问概念或无需画布变更时,才直接输出 Markdown 回复。",
433
629
  ].join("\n");
434
630
  return [
435
631
  "你正在 AgentFlow 的 Workspace 工作画布中执行任务。",
436
632
  "Workspace 是当前 pipeline 的临时工作区,用于分析、试验、生成中间文件和展示结果。",
633
+ "Workspace 与 Pipeline 各自有独立的 Skill collection;此处只使用当前 Workspace Composer 选择的 collections / skills 作为本次行为规则与编辑依据。",
634
+ "当 Skills 提到修改 flow.yaml / instances / edges / ui 时,在 Workspace 视图下应映射为修改当前工作区的 workspace.graph.json,除非用户显式勾选并要求修改正式 flow.yaml。",
635
+ "workspace.graph.json 使用 JSON:{ version, instances, edges, ui: { nodePositions, nodeSizes } }。instances 的结构与 flow.yaml instances 一致;edges 使用 source/target/sourceHandle/targetHandle;ui.nodePositions 记录节点坐标。",
437
636
  allowFlowYaml
438
637
  ? "用户已允许你考虑正式 flow.yaml;如需修改仍必须明确说明影响。"
439
638
  : "默认不要修改正式 flow.yaml;优先在 workspace 文件、workspace.graph.json 或回复内容中完成任务。",
440
639
  workspaceGraph ? `\n## 当前 workspace graph\n\n${JSON.stringify(workspaceGraph, null, 2)}` : "",
441
640
  selectedNodeIds.length > 0 ? `\n## 当前用户选中的 workspace 节点\n\n${selectedNodeIds.map((id) => `- ${id}`).join("\n")}` : "",
442
- skillsBlock ? `\n## Workspace Skills\n\n${skillsBlock}` : "",
641
+ skillsBlock ? `\n## Selected Skills\n\n${skillsBlock}` : "",
443
642
  kindInstruction,
444
643
  contextBlocks ? `\n## 上下文\n\n${contextBlocks}` : "",
445
644
  `\n## 用户 prompt\n\n${userPrompt}`,
446
645
  ].filter(Boolean).join("\n");
447
646
  }
448
647
 
648
+ function workspaceSlotValue(slot) {
649
+ if (!slot || typeof slot !== "object") return "";
650
+ for (const key of ["value", "default"]) {
651
+ if (slot[key] != null && String(slot[key]).trim()) return String(slot[key]);
652
+ }
653
+ return "";
654
+ }
655
+
656
+ function workspaceInstanceText(instance) {
657
+ const body = String(instance?.body || "").trim();
658
+ if (body) return body;
659
+ const slots = [...(Array.isArray(instance?.input) ? instance.input : []), ...(Array.isArray(instance?.output) ? instance.output : [])];
660
+ const textSlot = slots.find((slot) => String(slot?.type || "") === "text" && workspaceSlotValue(slot).trim());
661
+ return textSlot ? workspaceSlotValue(textSlot) : "";
662
+ }
663
+
664
+ function workspaceDisplayKind(definitionId) {
665
+ const id = String(definitionId || "");
666
+ if (id === "display_markdown") return "markdown";
667
+ if (id === "display_mermaid") return "mermaid";
668
+ if (id === "display_ascii") return "ascii";
669
+ return "";
670
+ }
671
+
672
+ function workspaceRunOrder(graph, runNodeId) {
673
+ const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
674
+ const edges = Array.isArray(graph?.edges) ? graph.edges : [];
675
+ const target = String(runNodeId || "").trim();
676
+ if (!target || !instances[target]) throw new Error("Missing workspace run node");
677
+ const upstream = new Map();
678
+ const downstream = new Map();
679
+ for (const edge of edges) {
680
+ const source = String(edge?.source || "");
681
+ const dest = String(edge?.target || "");
682
+ if (!source || !dest || !instances[source] || !instances[dest]) continue;
683
+ if (!upstream.has(dest)) upstream.set(dest, []);
684
+ upstream.get(dest).push(source);
685
+ if (!downstream.has(source)) downstream.set(source, []);
686
+ downstream.get(source).push(dest);
687
+ }
688
+ const reachable = new Set();
689
+ const visit = (id) => {
690
+ if (!id || reachable.has(id)) return;
691
+ reachable.add(id);
692
+ for (const next of downstream.get(id) || []) visit(next);
693
+ };
694
+ visit(target);
695
+ reachable.delete(target);
696
+ const needed = reachable;
697
+ const indegree = new Map(Array.from(needed).map((id) => [id, 0]));
698
+ for (const id of needed) {
699
+ for (const prev of upstream.get(id) || []) {
700
+ if (needed.has(prev)) indegree.set(id, (indegree.get(id) || 0) + 1);
701
+ }
702
+ }
703
+ const ready = Array.from(needed).filter((id) => (indegree.get(id) || 0) === 0);
704
+ const ordered = [];
705
+ while (ready.length) {
706
+ const id = ready.shift();
707
+ ordered.push(id);
708
+ for (const next of downstream.get(id) || []) {
709
+ if (!needed.has(next)) continue;
710
+ const n = (indegree.get(next) || 0) - 1;
711
+ indegree.set(next, n);
712
+ if (n === 0) ready.push(next);
713
+ }
714
+ }
715
+ if (ordered.length !== needed.size) {
716
+ throw new Error("Workspace run graph contains a cycle");
717
+ }
718
+ return ordered;
719
+ }
720
+
721
+ function workspaceUpstreamText(graph, nodeId, outputs) {
722
+ const edges = Array.isArray(graph?.edges) ? graph.edges : [];
723
+ const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
724
+ const incoming = edges.filter((edge) => String(edge?.target || "") === String(nodeId));
725
+ const contentEdge = incoming.find((edge) => String(edge?.targetHandle || "") === "input-1") || incoming[0];
726
+ if (!contentEdge) return "";
727
+ const sourceId = String(contentEdge.source || "");
728
+ const out = outputs.get(sourceId);
729
+ if (out != null && String(out).trim()) return String(out);
730
+ return workspaceInstanceText(instances[sourceId]);
731
+ }
732
+
733
+ function parseWorkspaceSkillKeys(raw) {
734
+ const text = String(raw || "").trim();
735
+ if (!text) return [];
736
+ try {
737
+ const parsed = JSON.parse(text);
738
+ if (Array.isArray(parsed)) return parsed.map((item) => String(item || "").trim()).filter(Boolean);
739
+ } catch {
740
+ /* plain list fallback */
741
+ }
742
+ return text.split(/[\n,]+/).map((item) => item.trim()).filter(Boolean);
743
+ }
744
+
745
+ function selectedSkillKeysFromInstance(instance) {
746
+ const bodyKeys = parseWorkspaceSkillKeys(instance?.body || "");
747
+ if (bodyKeys.length > 0) return bodyKeys;
748
+ const slots = [...(Array.isArray(instance?.input) ? instance.input : []), ...(Array.isArray(instance?.output) ? instance.output : [])];
749
+ const slot = slots.find((item) => item?.name === "skillsContext") || slots.find((item) => item?.name === "skillKeys");
750
+ return parseWorkspaceSkillKeys(workspaceSlotValue(slot) || "");
751
+ }
752
+
753
+ function workspaceUpstreamSkillBlocks(graph, nodeId, outputs) {
754
+ const edges = Array.isArray(graph?.edges) ? graph.edges : [];
755
+ return edges
756
+ .filter((edge) => String(edge?.target || "") === String(nodeId))
757
+ .map((edge) => String(outputs.get(String(edge.source || "")) || ""))
758
+ .filter((text) => text.includes("##") || text.includes("Skill"))
759
+ .join("\n\n---\n\n");
760
+ }
761
+
762
+ function workspaceWriteDisplayContent(instance, content) {
763
+ const next = { ...(instance || {}) };
764
+ const text = String(content || "");
765
+ next.body = text;
766
+ next.input = (Array.isArray(next.input) ? next.input : []).map((slot) => (
767
+ String(slot?.name || "") === "content" || String(slot?.type || "") === "text"
768
+ ? { ...slot, default: text, value: text }
769
+ : slot
770
+ ));
771
+ next.output = (Array.isArray(next.output) ? next.output : []).map((slot) => (
772
+ String(slot?.name || "") === "content" || String(slot?.type || "") === "text"
773
+ ? { ...slot, default: text, value: text }
774
+ : slot
775
+ ));
776
+ return next;
777
+ }
778
+
779
+ function workspaceUpdateDirectDisplays(graph, sourceId, content) {
780
+ const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
781
+ const edges = Array.isArray(graph?.edges) ? graph.edges : [];
782
+ const updated = [];
783
+ for (const edge of edges) {
784
+ if (String(edge?.source || "") !== String(sourceId)) continue;
785
+ const targetId = String(edge?.target || "");
786
+ const target = instances[targetId];
787
+ if (!target || !workspaceDisplayKind(target.definitionId)) continue;
788
+ instances[targetId] = workspaceWriteDisplayContent(target, content);
789
+ updated.push(targetId);
790
+ }
791
+ return updated;
792
+ }
793
+
794
+ function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock) {
795
+ const instance = graph.instances[nodeId] || {};
796
+ const body = String(instance.body || "").trim();
797
+ const label = String(instance.label || nodeId).trim();
798
+ return [
799
+ "你正在执行 AgentFlow Workspace 画布中的一个临时节点。",
800
+ "只输出该节点要传给下游展示/后续节点的正文,不要解释运行过程。",
801
+ skillsBlock ? `\n## Selected Skills\n\n${skillsBlock}` : "",
802
+ upstreamText ? `\n## 上游上下文\n\n${upstreamText}` : "",
803
+ `\n## 当前节点\n\n- id: ${nodeId}\n- label: ${label}\n- definitionId: ${instance.definitionId || ""}`,
804
+ `\n## 节点任务\n\n${body || upstreamText}`,
805
+ ].filter(Boolean).join("\n");
806
+ }
807
+
808
+ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts = {}) {
809
+ const graph = normalizeWorkspaceGraphPayload(payload.graph || {});
810
+ const runNodeId = String(payload?.runNodeId || "").trim();
811
+ const order = workspaceRunOrder(graph, runNodeId);
812
+ const fallbackSelectedSkillKeys = Array.isArray(payload?.selectedSkills)
813
+ ? payload.selectedSkills.map((x) => String(x || "").trim()).filter(Boolean)
814
+ : [];
815
+ const skillsBlockCache = new Map();
816
+ const loadSkillsBlockForKeys = (keys) => {
817
+ const normalized = Array.from(new Set((keys || []).map((x) => String(x || "").trim()).filter(Boolean)));
818
+ const cacheKey = normalized.join("\n");
819
+ if (skillsBlockCache.has(cacheKey)) return skillsBlockCache.get(cacheKey);
820
+ const selectedSkillResources = normalized.length > 0
821
+ ? loadResourcesForSkillKeys(normalized, PACKAGE_ROOT, scopedRoot)
822
+ : { skills: [], references: [] };
823
+ const block = normalized.length > 0
824
+ ? buildSkillCompactInjectionBlock(selectedSkillResources.skills, selectedSkillResources.references)
825
+ : "";
826
+ skillsBlockCache.set(cacheKey, block);
827
+ return block;
828
+ };
829
+ const outputs = new Map();
830
+ const events = [];
831
+ const emit = (event) => {
832
+ events.push(event);
833
+ if (typeof opts.onEvent === "function") opts.onEvent(event);
834
+ };
835
+ let cwd = scopedRoot;
836
+ const modelKey = typeof payload?.model === "string" ? payload.model.trim() : "";
837
+
838
+ for (const nodeId of order) {
839
+ const instance = graph.instances[nodeId];
840
+ if (!instance) continue;
841
+ const defId = String(instance.definitionId || "");
842
+ emit({ type: "node-start", nodeId, definitionId: defId });
843
+
844
+ if (defId === "workspace_run") {
845
+ continue;
846
+ }
847
+
848
+ if (defId === "control_load_skills") {
849
+ const nodeSkillKeys = selectedSkillKeysFromInstance(instance);
850
+ const activeSkillKeys = nodeSkillKeys.length > 0 ? nodeSkillKeys : fallbackSelectedSkillKeys;
851
+ const skillsBlock = loadSkillsBlockForKeys(activeSkillKeys);
852
+ graph.instances[nodeId] = {
853
+ ...instance,
854
+ output: (Array.isArray(instance.output) ? instance.output : []).map((slot) => (
855
+ String(slot?.name || "") === "skillsContext" || String(slot?.type || "") === "text"
856
+ ? { ...slot, default: skillsBlock, value: skillsBlock }
857
+ : slot
858
+ )),
859
+ };
860
+ outputs.set(nodeId, skillsBlock);
861
+ workspaceUpdateDirectDisplays(graph, nodeId, skillsBlock);
862
+ emit({ type: "graph", nodeId, graph });
863
+ emit({ type: "node-done", nodeId, definitionId: defId });
864
+ continue;
865
+ }
866
+
867
+ if (workspaceDisplayKind(defId)) {
868
+ const content = workspaceUpstreamText(graph, nodeId, outputs);
869
+ graph.instances[nodeId] = workspaceWriteDisplayContent(instance, content);
870
+ outputs.set(nodeId, content);
871
+ emit({ type: "graph", nodeId, graph });
872
+ emit({ type: "node-done", nodeId, definitionId: defId });
873
+ continue;
874
+ }
875
+
876
+ if (defId === "provide_str") {
877
+ const content = workspaceInstanceText(instance);
878
+ outputs.set(nodeId, content);
879
+ emit({ type: "node-done", nodeId, definitionId: defId });
880
+ continue;
881
+ }
882
+
883
+ if (defId === "provide_file") {
884
+ const fileValue = workspaceSlotValue(Array.isArray(instance.output) ? instance.output[0] : null) || workspaceInstanceText(instance);
885
+ const abs = path.resolve(scopedRoot, fileValue);
886
+ if (!abs.startsWith(path.resolve(scopedRoot) + path.sep) && abs !== path.resolve(scopedRoot)) {
887
+ throw new Error(`Workspace file is outside root: ${fileValue}`);
888
+ }
889
+ const content = fs.existsSync(abs) && fs.statSync(abs).isFile() ? fs.readFileSync(abs, "utf-8") : fileValue;
890
+ outputs.set(nodeId, content);
891
+ emit({ type: "node-done", nodeId, definitionId: defId });
892
+ continue;
893
+ }
894
+
895
+ if (defId === "control_cd_workspace") {
896
+ const inputText = workspaceUpstreamText(graph, nodeId, outputs);
897
+ const inputSlots = Array.isArray(instance.input) ? instance.input : [];
898
+ const pathSlot = inputSlots.find((slot) => String(slot?.name || "") === "path") ||
899
+ inputSlots.find((slot) => String(slot?.name || "") === "target");
900
+ const candidate = workspaceSlotValue(pathSlot) || workspaceInstanceText(instance) || inputText;
901
+ const abs = candidate ? path.resolve(scopedRoot, candidate) : scopedRoot;
902
+ if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) cwd = abs;
903
+ outputs.set(nodeId, cwd);
904
+ emit({ type: "node-done", nodeId, definitionId: defId });
905
+ continue;
906
+ }
907
+
908
+ if (defId === "control_user_workspace") {
909
+ cwd = path.resolve(os.homedir());
910
+ outputs.set(nodeId, cwd);
911
+ emit({ type: "node-done", nodeId, definitionId: defId });
912
+ continue;
913
+ }
914
+
915
+ const upstreamText = workspaceUpstreamText(graph, nodeId, outputs);
916
+ const body = String(instance.body || "").trim();
917
+ if (defId === "agent_subAgent" && !body && !String(upstreamText || "").trim()) {
918
+ throw new Error(`Workspace node ${nodeId} has no task. Fill the node body or connect upstream text.`);
919
+ }
920
+ const upstreamSkillBlocks = workspaceUpstreamSkillBlocks(graph, nodeId, outputs);
921
+ const prompt = workspaceNodePrompt(graph, nodeId, upstreamText, upstreamSkillBlocks || loadSkillsBlockForKeys(fallbackSelectedSkillKeys));
922
+ let content = "";
923
+ const maxAttempts = 3;
924
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
925
+ let attemptContent = "";
926
+ try {
927
+ const handle = startComposerAgent({
928
+ uiWorkspaceRoot: scopedRoot,
929
+ cliWorkspace: cwd,
930
+ prompt,
931
+ modelKey,
932
+ agentflowUserId: userCtx.userId || "",
933
+ onStreamEvent: (ev) => {
934
+ emit({ ...ev, nodeId });
935
+ if (ev?.type === "natural" && ev.kind === "assistant" && typeof ev.text === "string") {
936
+ attemptContent += (attemptContent ? "\n" : "") + ev.text;
937
+ const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, attemptContent);
938
+ if (updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
939
+ }
940
+ },
941
+ });
942
+ await handle.finished;
943
+ content = attemptContent.trim();
944
+ break;
945
+ } catch (e) {
946
+ if (attempt < maxAttempts && isTransientAgentNetworkError(e)) {
947
+ emit({ type: "status", nodeId, line: `Workspace node retry ${attempt + 1}/${maxAttempts} after network error` });
948
+ await sleepMs(Math.min(1500 * attempt, 5000));
949
+ continue;
950
+ }
951
+ throw e;
952
+ }
953
+ }
954
+ outputs.set(nodeId, content);
955
+ const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, content);
956
+ if (updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
957
+ emit({ type: "node-done", nodeId, definitionId: defId });
958
+ }
959
+ graph.updatedAt = new Date().toISOString();
960
+ return { graph, events, order };
961
+ }
962
+
449
963
  function isTransientAgentNetworkError(err) {
450
964
  const text = [
451
965
  err?.message,
@@ -1059,6 +1573,60 @@ export function startUiServer({
1059
1573
  return;
1060
1574
  }
1061
1575
 
1576
+ if (req.method === "POST" && url.pathname === "/api/workspace/run") {
1577
+ let payload;
1578
+ try {
1579
+ payload = JSON.parse(await readBody(req));
1580
+ } catch {
1581
+ json(res, 400, { error: "Invalid JSON body" });
1582
+ return;
1583
+ }
1584
+ try {
1585
+ const scoped = resolveWorkspaceScopeRoot(root, {
1586
+ flowId: payload.flowId || "",
1587
+ flowSource: payload.flowSource || "user",
1588
+ archived: payload.archived === true || payload.flowArchived === true,
1589
+ }, userCtx);
1590
+ if (scoped.error) {
1591
+ json(res, 400, { error: scoped.error });
1592
+ return;
1593
+ }
1594
+ if (scoped.archived || scoped.flowSource === "builtin") {
1595
+ json(res, 400, { error: "Cannot run workspace graph for builtin or archived pipeline" });
1596
+ return;
1597
+ }
1598
+ const wantsStream = /\bapplication\/x-ndjson\b/i.test(req.headers.accept || "") || payload.stream === true;
1599
+ if (wantsStream) {
1600
+ const graphPath = workspaceGraphPath(scoped.root);
1601
+ res.writeHead(200, {
1602
+ "Content-Type": "application/x-ndjson; charset=utf-8",
1603
+ "Cache-Control": "no-cache",
1604
+ "X-Accel-Buffering": "no",
1605
+ });
1606
+ const writeEvent = (event) => {
1607
+ res.write(JSON.stringify(event) + "\n");
1608
+ };
1609
+ try {
1610
+ const result = await runWorkspaceGraph(root, scoped.root, payload, userCtx, { onEvent: writeEvent });
1611
+ fs.writeFileSync(graphPath, JSON.stringify(result.graph, null, 2) + "\n", "utf-8");
1612
+ writeEvent({ type: "done", ok: true, path: graphPath, graph: result.graph, order: result.order });
1613
+ res.end();
1614
+ } catch (e) {
1615
+ writeEvent({ type: "error", error: (e && e.message) || String(e) });
1616
+ res.end();
1617
+ }
1618
+ return;
1619
+ }
1620
+ const result = await runWorkspaceGraph(root, scoped.root, payload, userCtx);
1621
+ const graphPath = workspaceGraphPath(scoped.root);
1622
+ fs.writeFileSync(graphPath, JSON.stringify(result.graph, null, 2) + "\n", "utf-8");
1623
+ json(res, 200, { ok: true, path: graphPath, ...result });
1624
+ } catch (e) {
1625
+ json(res, 500, { error: (e && e.message) || String(e) });
1626
+ }
1627
+ return;
1628
+ }
1629
+
1062
1630
  if (req.method === "GET" && url.pathname === "/api/workspace/file") {
1063
1631
  try {
1064
1632
  const scoped = resolveWorkspaceScopeRoot(root, {
@@ -1252,7 +1820,7 @@ export function startUiServer({
1252
1820
  agentflowUserId: userCtx.userId || "",
1253
1821
  onStreamEvent: (ev) => {
1254
1822
  events.push(ev);
1255
- if (ev?.type === "natural" && typeof ev.text === "string") {
1823
+ if (ev?.type === "natural" && ev.kind === "assistant" && typeof ev.text === "string") {
1256
1824
  attemptContent += (attemptContent ? "\n" : "") + ev.text;
1257
1825
  }
1258
1826
  },
@@ -1494,6 +2062,32 @@ export function startUiServer({
1494
2062
  return;
1495
2063
  }
1496
2064
 
2065
+ if (req.method === "GET" && url.pathname === "/api/user-env") {
2066
+ try {
2067
+ json(res, 200, { env: readUserEnvRows(userCtx.userId) });
2068
+ } catch (e) {
2069
+ json(res, 500, { error: (e && e.message) || String(e) });
2070
+ }
2071
+ return;
2072
+ }
2073
+
2074
+ if (req.method === "POST" && url.pathname === "/api/user-env") {
2075
+ let payload;
2076
+ try {
2077
+ payload = JSON.parse(await readBody(req));
2078
+ } catch {
2079
+ json(res, 400, { error: "Invalid JSON body" });
2080
+ return;
2081
+ }
2082
+ try {
2083
+ const envRows = writeUserEnvRows(userCtx.userId, payload?.env || []);
2084
+ json(res, 200, { success: true, env: envRows });
2085
+ } catch (e) {
2086
+ json(res, 500, { error: (e && e.message) || String(e) });
2087
+ }
2088
+ return;
2089
+ }
2090
+
1497
2091
  if (req.method === "POST" && url.pathname === "/api/update-model-lists") {
1498
2092
  try {
1499
2093
  let opencodeProviderOverride = "";
@@ -1686,13 +2280,29 @@ export function startUiServer({
1686
2280
 
1687
2281
  if (req.method === "GET" && url.pathname === "/api/marketplace/nodes") {
1688
2282
  try {
1689
- json(res, 200, listMarketplacePackages(root));
2283
+ json(res, 200, listMarketplacePackages(root, userCtx));
1690
2284
  } catch (e) {
1691
2285
  json(res, 500, { error: (e && e.message) || String(e) });
1692
2286
  }
1693
2287
  return;
1694
2288
  }
1695
2289
 
2290
+ if (req.method === "DELETE" && url.pathname === "/api/marketplace/node") {
2291
+ const id = url.searchParams.get("id") || "";
2292
+ const version = url.searchParams.get("version") || "";
2293
+ if (!id || !version) {
2294
+ json(res, 400, { ok: false, error: "Missing marketplace node id or version" });
2295
+ return;
2296
+ }
2297
+ try {
2298
+ const result = deleteMarketplaceNodePackage(root, id, version, userCtx);
2299
+ json(res, result.ok ? 200 : 400, result);
2300
+ } catch (e) {
2301
+ json(res, 500, { ok: false, error: (e && e.message) || String(e) });
2302
+ }
2303
+ return;
2304
+ }
2305
+
1696
2306
  if (req.method === "POST" && url.pathname === "/api/marketplace/install-node") {
1697
2307
  let payload;
1698
2308
  try {
@@ -2386,7 +2996,7 @@ finishedAt: "${new Date().toISOString()}"
2386
2996
  child = spawn(process.execPath, args, {
2387
2997
  cwd: root,
2388
2998
  stdio: ["ignore", "pipe", "pipe"],
2389
- env: { ...process.env, FORCE_COLOR: "0", AGENTFLOW_USER_ID: userCtx.userId || "" },
2999
+ env: runtimeEnvForUser(userCtx, { FORCE_COLOR: "0" }),
2390
3000
  // detached: true 使 child 成为新进程组 leader,/api/flow/run/stop 时
2391
3001
  // 用 process.kill(-pid) 可以一次性 SIGTERM 整棵进程树(含 cursor-agent 等孙进程)
2392
3002
  detached: true,
@@ -2514,6 +3124,27 @@ finishedAt: "${new Date().toISOString()}"
2514
3124
  return;
2515
3125
  }
2516
3126
 
3127
+ if (req.method === "GET" && url.pathname === "/api/skill-collections") {
3128
+ json(res, 200, readSkillCollectionConfig(userCtx, listComposerSkills(PACKAGE_ROOT, root)));
3129
+ return;
3130
+ }
3131
+
3132
+ if (req.method === "POST" && url.pathname === "/api/skill-collections") {
3133
+ let payload;
3134
+ try {
3135
+ payload = JSON.parse(await readBody(req));
3136
+ } catch {
3137
+ json(res, 400, { error: "Invalid JSON body" });
3138
+ return;
3139
+ }
3140
+ try {
3141
+ json(res, 200, writeSkillCollectionConfig(userCtx, payload, listComposerSkills(PACKAGE_ROOT, root)));
3142
+ } catch (e) {
3143
+ json(res, 500, { error: (e && e.message) || String(e) });
3144
+ }
3145
+ return;
3146
+ }
3147
+
2517
3148
  if (req.method === "GET" && url.pathname === "/api/skills/detail") {
2518
3149
  const key = url.searchParams.get("key") || url.searchParams.get("name") || "";
2519
3150
  const detail = readComposerSkillDetail(PACKAGE_ROOT, root, key);