@fieldwangai/agentflow 0.1.28 → 0.1.30

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 (63) hide show
  1. package/agents/agentflow-node-executor-code.md +3 -2
  2. package/agents/agentflow-node-executor-planning.md +3 -2
  3. package/agents/agentflow-node-executor-requirement.md +3 -2
  4. package/agents/agentflow-node-executor-test.md +3 -2
  5. package/agents/agentflow-node-executor-ui.md +3 -2
  6. package/agents/agentflow-node-executor.md +3 -2
  7. package/agents/en/agentflow-node-executor.md +3 -2
  8. package/agents/zh/agentflow-node-executor.md +3 -2
  9. package/bin/lib/agent-runners.mjs +38 -13
  10. package/bin/lib/api-runner.mjs +6 -3
  11. package/bin/lib/auth.mjs +240 -0
  12. package/bin/lib/catalog-agents.mjs +2 -2
  13. package/bin/lib/catalog-flows.mjs +192 -16
  14. package/bin/lib/composer-agent.mjs +21 -1
  15. package/bin/lib/composer-skill-router.mjs +10 -78
  16. package/bin/lib/flow-import.mjs +2 -2
  17. package/bin/lib/flow-write.mjs +20 -20
  18. package/bin/lib/help.mjs +2 -2
  19. package/bin/lib/locales/en.json +25 -1
  20. package/bin/lib/locales/zh.json +25 -1
  21. package/bin/lib/main.mjs +6 -1
  22. package/bin/lib/node-exec-context.mjs +5 -5
  23. package/bin/lib/node-execute.mjs +14 -9
  24. package/bin/lib/paths.mjs +64 -13
  25. package/bin/lib/recent-runs.mjs +2 -2
  26. package/bin/lib/run-node-statuses-from-disk.mjs +3 -3
  27. package/bin/lib/runtime-context.mjs +225 -0
  28. package/bin/lib/scheduler.mjs +41 -38
  29. package/bin/lib/skill-registry.mjs +145 -0
  30. package/bin/lib/ui-server.mjs +902 -57
  31. package/bin/lib/workspace-tree.mjs +4 -3
  32. package/bin/lib/workspace.mjs +9 -11
  33. package/bin/pipeline/build-node-prompt.mjs +29 -4
  34. package/bin/pipeline/get-exec-id.mjs +2 -2
  35. package/bin/pipeline/get-resolved-values.mjs +1 -0
  36. package/bin/pipeline/pre-process-node.mjs +306 -6
  37. package/bin/pipeline/validate-flow.mjs +2 -0
  38. package/builtin/nodes/agent_subAgent.md +7 -1
  39. package/builtin/nodes/control_cd_workspace.md +43 -0
  40. package/builtin/nodes/control_load_skills.md +48 -0
  41. package/builtin/nodes/display_ascii.md +17 -0
  42. package/builtin/nodes/display_markdown.md +17 -0
  43. package/builtin/nodes/display_mermaid.md +17 -0
  44. package/builtin/nodes/tool_git_checkout.md +54 -0
  45. package/builtin/nodes/tool_nodejs.md +8 -1
  46. package/builtin/nodes/tool_print.md +4 -1
  47. package/builtin/web-ui/dist/assets/index-NdVOJLL9.js +196 -0
  48. package/builtin/web-ui/dist/assets/index-naVI6LZj.css +1 -0
  49. package/builtin/web-ui/dist/index.html +2 -2
  50. package/package.json +2 -1
  51. package/skills/agentflow-flow-add-instances/SKILL.md +257 -0
  52. package/skills/agentflow-flow-edit-node-fields/SKILL.md +79 -0
  53. package/skills/agentflow-flow-recipes/SKILL.md +24 -0
  54. package/skills/agentflow-flow-recipes/references/recipes.md +63 -0
  55. package/skills/agentflow-flow-sync-ui/SKILL.md +59 -0
  56. package/skills/agentflow-node-reference/SKILL.md +25 -0
  57. package/skills/agentflow-node-reference/references/builtin-nodes.md +210 -0
  58. package/skills/agentflow-placeholder-reference/SKILL.md +24 -0
  59. package/skills/agentflow-placeholder-reference/references/placeholders.md +20 -0
  60. package/skills/agentflow-runtime-reference/SKILL.md +25 -0
  61. package/skills/agentflow-runtime-reference/references/runtime.md +64 -0
  62. package/builtin/web-ui/dist/assets/index-BeUBxIj1.js +0 -190
  63. package/builtin/web-ui/dist/assets/index-BzhdjOzb.css +0 -1
@@ -50,7 +50,7 @@ export function readPipelineListDescription(flowDir) {
50
50
  }
51
51
  }
52
52
 
53
- export function listFlowsJson(workspaceRoot) {
53
+ export function listFlowsJson(workspaceRoot, opts = {}) {
54
54
  const root = path.resolve(workspaceRoot);
55
55
  const out = [];
56
56
  const fromBuiltin = collectPipelineNamesFromDir(PACKAGE_BUILTIN_PIPELINES_DIR);
@@ -59,7 +59,7 @@ export function listFlowsJson(workspaceRoot) {
59
59
  const description = readPipelineListDescription(dir);
60
60
  out.push({ id: name, path: dir, source: "builtin", ...(description ? { description } : {}) });
61
61
  }
62
- const userPipelinesRoot = getUserPipelinesRoot();
62
+ const userPipelinesRoot = getUserPipelinesRoot(opts.userId);
63
63
  const fromUserData = collectPipelineNamesFromDir(userPipelinesRoot);
64
64
  for (const name of fromUserData) {
65
65
  if (name === ARCHIVED_PIPELINES_DIR_NAME) continue;
@@ -200,6 +200,7 @@ export function parseNodeFrontmatter(raw) {
200
200
  export function listNodesJson(workspaceRoot, flowId, flowSource, opts = {}) {
201
201
  const root = path.resolve(workspaceRoot);
202
202
  const archived = Boolean(opts.archived);
203
+ const userPipelinesRoot = getUserPipelinesRoot(opts.userId);
203
204
  const byId = new Map();
204
205
  const pipelineTranslations = {};
205
206
  let marketplaceFlowData = null;
@@ -293,13 +294,9 @@ export function listNodesJson(workspaceRoot, flowId, flowSource, opts = {}) {
293
294
  } catch (_) {}
294
295
  } else if (flowSource === "user") {
295
296
  if (archived) {
296
- addFromDir(
297
- path.join(getUserPipelinesRoot(), ARCHIVED_PIPELINES_DIR_NAME, flowId, "nodes"),
298
- "flow",
299
- flowId,
300
- );
297
+ addFromDir(path.join(userPipelinesRoot, ARCHIVED_PIPELINES_DIR_NAME, flowId, "nodes"), "flow", flowId);
301
298
  } else {
302
- addFromDir(path.join(getUserPipelinesRoot(), flowId, "nodes"), "flow", flowId);
299
+ addFromDir(path.join(userPipelinesRoot, flowId, "nodes"), "flow", flowId);
303
300
  addFromDir(path.join(root, PIPELINES_DIR, flowId, "nodes"), "flow", flowId);
304
301
  addFromDir(path.join(root, LEGACY_PIPELINES_DIR, flowId, "nodes"), "flow", flowId);
305
302
  }
@@ -342,13 +339,14 @@ export function printNodesTable(list) {
342
339
  export function readFlowJson(workspaceRoot, flowId, flowSource, options = {}) {
343
340
  const root = path.resolve(workspaceRoot);
344
341
  const archived = Boolean(options.archived);
342
+ const userPipelinesRoot = getUserPipelinesRoot(options.userId);
345
343
  let flowDir;
346
344
  if (archived) {
347
345
  if (flowSource === "builtin") {
348
346
  return { error: t("catalog.builtin_flow_archive_not_supported") };
349
347
  }
350
348
  if (flowSource === "user") {
351
- flowDir = path.join(getUserPipelinesRoot(), ARCHIVED_PIPELINES_DIR_NAME, flowId);
349
+ flowDir = path.join(userPipelinesRoot, ARCHIVED_PIPELINES_DIR_NAME, flowId);
352
350
  } else if (flowSource === "workspace") {
353
351
  flowDir = path.join(root, PIPELINES_DIR, ARCHIVED_PIPELINES_DIR_NAME, flowId);
354
352
  } else {
@@ -377,7 +375,7 @@ export function readFlowJson(workspaceRoot, flowId, flowSource, options = {}) {
377
375
  if (flowSource === "builtin") {
378
376
  flowDir = path.join(PACKAGE_BUILTIN_PIPELINES_DIR, flowId);
379
377
  } else if (flowSource === "user") {
380
- flowDir = path.join(getUserPipelinesRoot(), flowId);
378
+ flowDir = path.join(userPipelinesRoot, flowId);
381
379
  } else if (flowSource === "workspace") {
382
380
  flowDir = path.join(root, PIPELINES_DIR, flowId);
383
381
  } else {
@@ -422,13 +420,14 @@ export function readFlowJson(workspaceRoot, flowId, flowSource, options = {}) {
422
420
  export function getFlowYamlAbs(workspaceRoot, flowId, flowSource, options = {}) {
423
421
  const root = path.resolve(workspaceRoot);
424
422
  const archived = Boolean(options.archived);
423
+ const userPipelinesRoot = getUserPipelinesRoot(options.userId);
425
424
  let yamlPath;
426
425
  if (archived) {
427
426
  if (flowSource === "builtin") {
428
427
  return { error: t("catalog.builtin_flow_archive_path_not_supported") };
429
428
  }
430
429
  if (flowSource === "user") {
431
- yamlPath = path.join(getUserPipelinesRoot(), ARCHIVED_PIPELINES_DIR_NAME, flowId, "flow.yaml");
430
+ yamlPath = path.join(userPipelinesRoot, ARCHIVED_PIPELINES_DIR_NAME, flowId, "flow.yaml");
432
431
  } else if (flowSource === "workspace") {
433
432
  yamlPath = path.join(root, PIPELINES_DIR, ARCHIVED_PIPELINES_DIR_NAME, flowId, "flow.yaml");
434
433
  if (!fs.existsSync(yamlPath)) {
@@ -447,7 +446,7 @@ export function getFlowYamlAbs(workspaceRoot, flowId, flowSource, options = {})
447
446
  if (flowSource === "builtin") {
448
447
  yamlPath = path.join(PACKAGE_BUILTIN_PIPELINES_DIR, flowId, "flow.yaml");
449
448
  } else if (flowSource === "user") {
450
- yamlPath = path.join(getUserPipelinesRoot(), flowId, "flow.yaml");
449
+ yamlPath = path.join(userPipelinesRoot, flowId, "flow.yaml");
451
450
  if (!fs.existsSync(yamlPath)) {
452
451
  const alt = path.join(root, PIPELINES_DIR, flowId, "flow.yaml");
453
452
  if (fs.existsSync(alt)) yamlPath = alt;
@@ -474,6 +473,7 @@ export function getFlowYamlAbs(workspaceRoot, flowId, flowSource, options = {})
474
473
  export function readNodeJson(workspaceRoot, nodeId, flowId, flowSource, opts = {}) {
475
474
  const root = path.resolve(workspaceRoot);
476
475
  const archived = Boolean(opts.archived);
476
+ const userPipelinesRoot = getUserPipelinesRoot(opts.userId);
477
477
  const marketSpec = parseMarketplaceDefinitionId(nodeId);
478
478
  if (marketSpec) {
479
479
  let flowDir = root;
@@ -515,11 +515,9 @@ export function readNodeJson(workspaceRoot, nodeId, flowId, flowSource, opts = {
515
515
  pathsToTry.push(path.join(PACKAGE_BUILTIN_PIPELINES_DIR, flowId, "nodes", fileName));
516
516
  } else if (flowSource === "user") {
517
517
  if (archived) {
518
- pathsToTry.push(
519
- path.join(getUserPipelinesRoot(), ARCHIVED_PIPELINES_DIR_NAME, flowId, "nodes", fileName),
520
- );
518
+ pathsToTry.push(path.join(userPipelinesRoot, ARCHIVED_PIPELINES_DIR_NAME, flowId, "nodes", fileName));
521
519
  } else {
522
- pathsToTry.push(path.join(getUserPipelinesRoot(), flowId, "nodes", fileName));
520
+ pathsToTry.push(path.join(userPipelinesRoot, flowId, "nodes", fileName));
523
521
  pathsToTry.push(path.join(root, PIPELINES_DIR, flowId, "nodes", fileName));
524
522
  pathsToTry.push(path.join(root, LEGACY_PIPELINES_DIR, flowId, "nodes", fileName));
525
523
  }
@@ -568,6 +566,184 @@ export function readNodeJson(workspaceRoot, nodeId, flowId, flowSource, opts = {
568
566
  return { error: "Node not found: " + nodeId };
569
567
  }
570
568
 
569
+ const NODE_DETAIL_MAX_FILES = 300;
570
+ const NODE_FILE_MAX_BYTES = 256 * 1024;
571
+
572
+ function isTextPreviewPath(filePath) {
573
+ const ext = path.extname(filePath).toLowerCase();
574
+ return [
575
+ ".md", ".mdx", ".txt", ".json", ".yaml", ".yml", ".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx",
576
+ ".py", ".sh", ".css", ".html", ".xml", ".toml", ".ini", ".env", ".sql",
577
+ ].includes(ext);
578
+ }
579
+
580
+ function listNodeFiles(baseDir, allowAllFiles, primaryFilePath = "") {
581
+ const root = path.resolve(baseDir);
582
+ const out = [];
583
+ const addFile = (filePath) => {
584
+ if (out.length >= NODE_DETAIL_MAX_FILES) return;
585
+ try {
586
+ const stat = fs.statSync(filePath);
587
+ if (!stat.isFile()) return;
588
+ const rel = path.relative(root, filePath).replace(/\\/g, "/");
589
+ out.push({
590
+ path: rel,
591
+ size: stat.size,
592
+ previewable: isTextPreviewPath(filePath),
593
+ });
594
+ } catch (_) {}
595
+ };
596
+ if (!allowAllFiles) {
597
+ if (primaryFilePath) addFile(primaryFilePath);
598
+ return out;
599
+ }
600
+ const walk = (dir) => {
601
+ if (out.length >= NODE_DETAIL_MAX_FILES) return;
602
+ let entries = [];
603
+ try {
604
+ entries = fs.readdirSync(dir, { withFileTypes: true });
605
+ } catch {
606
+ return;
607
+ }
608
+ entries.sort((a, b) => a.name.localeCompare(b.name));
609
+ for (const entry of entries) {
610
+ if (out.length >= NODE_DETAIL_MAX_FILES) return;
611
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
612
+ const p = path.join(dir, entry.name);
613
+ if (entry.isDirectory()) walk(p);
614
+ else if (entry.isFile()) addFile(p);
615
+ }
616
+ };
617
+ walk(root);
618
+ return out;
619
+ }
620
+
621
+ function resolveMarkdownNodeFile(workspaceRoot, nodeId, flowId, flowSource, opts = {}) {
622
+ const root = path.resolve(workspaceRoot);
623
+ const archived = Boolean(opts.archived);
624
+ const userPipelinesRoot = getUserPipelinesRoot(opts.userId);
625
+ const fileName = nodeId.endsWith(".md") ? nodeId : `${nodeId}.md`;
626
+ const pathsToTry = [];
627
+ if (flowId && flowSource) {
628
+ if (flowSource === "builtin") {
629
+ pathsToTry.push(path.join(PACKAGE_BUILTIN_PIPELINES_DIR, flowId, "nodes", fileName));
630
+ } else if (flowSource === "user") {
631
+ if (archived) {
632
+ pathsToTry.push(path.join(userPipelinesRoot, ARCHIVED_PIPELINES_DIR_NAME, flowId, "nodes", fileName));
633
+ } else {
634
+ pathsToTry.push(path.join(userPipelinesRoot, flowId, "nodes", fileName));
635
+ pathsToTry.push(path.join(root, PIPELINES_DIR, flowId, "nodes", fileName));
636
+ pathsToTry.push(path.join(root, LEGACY_PIPELINES_DIR, flowId, "nodes", fileName));
637
+ }
638
+ } else if (flowSource === "workspace") {
639
+ if (archived) {
640
+ pathsToTry.push(path.join(root, PIPELINES_DIR, ARCHIVED_PIPELINES_DIR_NAME, flowId, "nodes", fileName));
641
+ pathsToTry.push(path.join(root, LEGACY_PIPELINES_DIR, ARCHIVED_PIPELINES_DIR_NAME, flowId, "nodes", fileName));
642
+ } else {
643
+ pathsToTry.push(path.join(root, PIPELINES_DIR, flowId, "nodes", fileName));
644
+ pathsToTry.push(path.join(root, LEGACY_PIPELINES_DIR, flowId, "nodes", fileName));
645
+ }
646
+ }
647
+ }
648
+ pathsToTry.push(path.join(root, PROJECT_NODES_DIR, fileName));
649
+ pathsToTry.push(path.join(root, LEGACY_NODES_DIR, fileName));
650
+ pathsToTry.push(path.join(PACKAGE_BUILTIN_NODES_DIR, fileName));
651
+ return pathsToTry.find((p) => fs.existsSync(p) && fs.statSync(p).isFile()) || "";
652
+ }
653
+
654
+ function readNodeUsage(workspaceRoot, nodeId, opts = {}) {
655
+ const usage = [];
656
+ for (const flow of listFlowsJson(workspaceRoot, opts)) {
657
+ const flowPath = getFlowYamlAbs(workspaceRoot, flow.id, flow.source || "user", { archived: Boolean(flow.archived), userId: opts.userId });
658
+ if (!flowPath.path) continue;
659
+ try {
660
+ const data = yaml.load(fs.readFileSync(flowPath.path, "utf-8"));
661
+ const instances = data && typeof data === "object" ? data.instances : null;
662
+ if (!instances || typeof instances !== "object") continue;
663
+ const hits = Object.entries(instances)
664
+ .filter(([, inst]) => inst && inst.definitionId === nodeId)
665
+ .map(([instanceId, inst]) => ({ instanceId, label: inst.label || instanceId }));
666
+ if (hits.length > 0) {
667
+ usage.push({ flowId: flow.id, flowSource: flow.source || "user", archived: Boolean(flow.archived), instances: hits });
668
+ }
669
+ } catch (_) {}
670
+ }
671
+ return usage;
672
+ }
673
+
674
+ function resolveNodeFileScope(workspaceRoot, nodeId, flowId, flowSource, opts = {}) {
675
+ const marketSpec = parseMarketplaceDefinitionId(nodeId);
676
+ if (marketSpec) {
677
+ let flowDir = path.resolve(workspaceRoot);
678
+ let flowData = null;
679
+ if (flowId && flowSource) {
680
+ const flowPath = getFlowYamlAbs(workspaceRoot, flowId, flowSource, opts);
681
+ if (flowPath.path) {
682
+ flowDir = path.dirname(flowPath.path);
683
+ try {
684
+ const parsed = yaml.load(fs.readFileSync(flowPath.path, "utf-8"));
685
+ if (parsed && typeof parsed === "object") flowData = parsed;
686
+ } catch (_) {}
687
+ }
688
+ }
689
+ const resolved = resolveMarketplaceNodePackage(workspaceRoot, flowDir, nodeId, flowData);
690
+ if (!resolved) return null;
691
+ return { baseDir: resolved.packageDir, allowAllFiles: true, primaryFilePath: path.join(resolved.packageDir, "node.yaml"), manifest: resolved };
692
+ }
693
+ const filePath = resolveMarkdownNodeFile(workspaceRoot, nodeId, flowId, flowSource, opts);
694
+ if (!filePath) return null;
695
+ return { baseDir: path.dirname(filePath), allowAllFiles: false, primaryFilePath: filePath, manifest: null };
696
+ }
697
+
698
+ export function readNodeDetailJson(workspaceRoot, nodeId, flowId = "", flowSource = "", opts = {}) {
699
+ if (!nodeId) return { error: "Missing node id" };
700
+ const node = readNodeJson(workspaceRoot, nodeId, flowId, flowSource, opts);
701
+ if (node.error) return node;
702
+ const scope = resolveNodeFileScope(workspaceRoot, nodeId, flowId, flowSource, opts);
703
+ const files = scope ? listNodeFiles(scope.baseDir, scope.allowAllFiles, scope.primaryFilePath) : [];
704
+ return {
705
+ node: { id: nodeId, ...node },
706
+ readOnly: true,
707
+ manifest: scope?.manifest || null,
708
+ runtime: node.runtime || scope?.manifest?.runtime || null,
709
+ body: node.executionLogic || "",
710
+ baseDir: scope?.baseDir || "",
711
+ files,
712
+ usage: readNodeUsage(workspaceRoot, nodeId, opts),
713
+ };
714
+ }
715
+
716
+ export function readNodeFilePreview(workspaceRoot, nodeId, relPath, flowId = "", flowSource = "", opts = {}) {
717
+ if (!nodeId) return { error: "Missing node id" };
718
+ const rel = String(relPath || "").trim();
719
+ if (!rel || rel.includes("\0") || path.isAbsolute(rel) || rel.split(/[\\/]+/).includes("..")) {
720
+ return { error: "Invalid file path" };
721
+ }
722
+ const scope = resolveNodeFileScope(workspaceRoot, nodeId, flowId, flowSource, opts);
723
+ if (!scope) return { error: "Node files not found" };
724
+ if (!scope.allowAllFiles) {
725
+ const primaryRel = path.relative(scope.baseDir, scope.primaryFilePath).replace(/\\/g, "/");
726
+ if (rel !== primaryRel) return { error: "File is outside node preview scope" };
727
+ }
728
+ const base = path.resolve(scope.baseDir);
729
+ const abs = path.resolve(base, rel);
730
+ if (abs !== base && !abs.startsWith(base + path.sep)) return { error: "File is outside node package" };
731
+ if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) return { error: "File not found" };
732
+ const stat = fs.statSync(abs);
733
+ if (!isTextPreviewPath(abs)) {
734
+ return { path: rel, size: stat.size, binary: true, content: "", truncated: false };
735
+ }
736
+ const fd = fs.openSync(abs, "r");
737
+ try {
738
+ const len = Math.min(stat.size, NODE_FILE_MAX_BYTES);
739
+ const buf = Buffer.alloc(len);
740
+ fs.readSync(fd, buf, 0, len, 0);
741
+ return { path: rel, size: stat.size, binary: false, content: buf.toString("utf-8"), truncated: stat.size > len };
742
+ } finally {
743
+ fs.closeSync(fd);
744
+ }
745
+ }
746
+
571
747
  /** 列出所有 pipeline(包内 builtin + ~/agentflow/pipelines + 项目内 .workspace/.cursor agentflow/pipelines);nodes 见 PROJECT_NODES_DIR / LEGACY_NODES_DIR */
572
748
  export function listPipelines(workspaceRoot) {
573
749
  const rows = listFlowsJson(workspaceRoot);
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import fs from "fs";
8
8
  import path from "path";
9
- import { getAgentflowDataRoot } from "./paths.mjs";
9
+ import { getAgentflowDataRoot, sanitizeAgentflowUserId } from "./paths.mjs";
10
10
  import { resolveCliAndModel } from "./model-config.mjs";
11
11
  import { runClaudeCodeAgentWithPrompt, runCursorAgentWithPrompt, runOpenCodeAgentWithPrompt } from "./agent-runners.mjs";
12
12
  import { planComposerTasks, hasPlannerApiAvailable, shouldUsePhased, classifyComplexity, classifyTaskComplexity, PHASED_DEFINITIONS } from "./composer-planner.mjs";
@@ -23,6 +23,11 @@ const MAX_PROMPT_CHARS = 500_000;
23
23
  const MAX_COMPOSER_VALIDATION_REPAIR = 5;
24
24
  const MAX_SCRIPT_INJECT_BYTES = 30_000;
25
25
 
26
+ function agentflowUserEnv(userId) {
27
+ const safe = sanitizeAgentflowUserId(userId);
28
+ return safe ? { AGENTFLOW_USER_ID: safe } : {};
29
+ }
30
+
26
31
  // ─── script 内容注入辅助 ─────────────────────────────────────────────────
27
32
 
28
33
  /**
@@ -113,10 +118,12 @@ export function startComposerAgent(opts) {
113
118
  const cliWs = opts.cliWorkspace ? String(opts.cliWorkspace) : getAgentflowDataRoot();
114
119
  const modelKey = opts.modelKey != null ? String(opts.modelKey).trim() : "";
115
120
  const { cli, model } = resolveCliAndModel(uiRoot, modelKey || null, null);
121
+ const env = agentflowUserEnv(opts.agentflowUserId);
116
122
 
117
123
  const common = {
118
124
  onStreamEvent: opts.onStreamEvent,
119
125
  force: Boolean(opts.force),
126
+ env,
120
127
  };
121
128
 
122
129
  if (cli === "opencode") {
@@ -185,6 +192,10 @@ function buildAgentStepPrompt(step, flowContext) {
185
192
  parts.push(`- flowId:${flowContext.flowId}`);
186
193
  parts.push(`- flowSource:${flowContext.flowSource || "user"}`);
187
194
  }
195
+ if (flowContext.userId) {
196
+ parts.push(`- agentflow 用户:${flowContext.userId}`);
197
+ parts.push(`- 执行 agentflow 命令时保留当前环境中的 AGENTFLOW_USER_ID,必要时显式前置 AGENTFLOW_USER_ID='${flowContext.userId}'。`);
198
+ }
188
199
  if (flowContext.skillsHint) {
189
200
  parts.push(flowContext.skillsHint);
190
201
  }
@@ -344,6 +355,7 @@ export async function runComposerPostFlowValidationAndRepair(opts) {
344
355
  const flowYamlAbs = String(opts.flowYamlAbs || "").trim();
345
356
  const cliWs = opts.cliWorkspace ? String(opts.cliWorkspace) : getAgentflowDataRoot();
346
357
  const maxRepair = Math.max(1, Math.min(10, Number(opts.maxRepairAttempts) || MAX_COMPOSER_VALIDATION_REPAIR));
358
+ const env = agentflowUserEnv(opts.agentflowUserId || opts.flowContext?.userId);
347
359
 
348
360
  if (!uiRoot || !flowYamlAbs) {
349
361
  return { ok: true, result: { skipped: true } };
@@ -405,6 +417,7 @@ export async function runComposerPostFlowValidationAndRepair(opts) {
405
417
  onStreamEvent: stepEmit,
406
418
  model: model || undefined,
407
419
  force: Boolean(opts.force),
420
+ env,
408
421
  });
409
422
  setChild(handle.child);
410
423
  await handle.finished;
@@ -413,6 +426,7 @@ export async function runComposerPostFlowValidationAndRepair(opts) {
413
426
  onStreamEvent: stepEmit,
414
427
  model: model || undefined,
415
428
  force: Boolean(opts.force),
429
+ env,
416
430
  });
417
431
  setChild(handle.child);
418
432
  await handle.finished;
@@ -421,6 +435,7 @@ export async function runComposerPostFlowValidationAndRepair(opts) {
421
435
  onStreamEvent: stepEmit,
422
436
  model: model || undefined,
423
437
  force: Boolean(opts.force),
438
+ env,
424
439
  });
425
440
  setChild(handle.child);
426
441
  await handle.finished;
@@ -490,6 +505,7 @@ export function startComposerMultiStep(opts) {
490
505
  const emit = typeof opts.onStreamEvent === "function" ? opts.onStreamEvent : () => {};
491
506
  let aborted = false;
492
507
  let currentChild = null;
508
+ const env = agentflowUserEnv(opts.agentflowUserId || opts.flowContext?.userId);
493
509
 
494
510
  const abort = () => {
495
511
  aborted = true;
@@ -674,6 +690,7 @@ export function startComposerMultiStep(opts) {
674
690
  onStreamEvent: stepEmit,
675
691
  model: model || undefined,
676
692
  force: Boolean(opts.force),
693
+ env,
677
694
  });
678
695
  currentChild = handle.child;
679
696
  await handle.finished;
@@ -682,6 +699,7 @@ export function startComposerMultiStep(opts) {
682
699
  onStreamEvent: stepEmit,
683
700
  model: model || undefined,
684
701
  force: Boolean(opts.force),
702
+ env,
685
703
  });
686
704
  currentChild = handle.child;
687
705
  await handle.finished;
@@ -690,6 +708,7 @@ export function startComposerMultiStep(opts) {
690
708
  onStreamEvent: stepEmit,
691
709
  model: model || undefined,
692
710
  force: Boolean(opts.force),
711
+ env,
693
712
  });
694
713
  currentChild = handle.child;
695
714
  await handle.finished;
@@ -757,6 +776,7 @@ export function startComposerMultiStep(opts) {
757
776
  flowContext: opts.flowContext,
758
777
  modelKey: opts.modelKey,
759
778
  force: Boolean(opts.force),
779
+ agentflowUserId: opts.agentflowUserId || opts.flowContext?.userId,
760
780
  onStreamEvent: emit,
761
781
  getAborted: () => aborted,
762
782
  setCurrentChild: (c) => {
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import fs from "fs";
15
15
  import path from "path";
16
- import yaml from "js-yaml";
16
+ import { listSkills as registryListSkills, readSkillDetail as registryReadSkillDetail } from "./skill-registry.mjs";
17
17
 
18
18
  // ─── 意图模式定义 ─────────────────────────────────────────────────────────
19
19
 
@@ -125,81 +125,12 @@ function readFileCached(absPath) {
125
125
  }
126
126
  }
127
127
 
128
- function parseSkillFile(absPath) {
129
- const content = readFileCached(absPath);
130
- if (!content) return null;
131
- const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?/);
132
- let meta = {};
133
- if (fmMatch) {
134
- try {
135
- meta = yaml.load(fmMatch[1]) || {};
136
- } catch {
137
- meta = {};
138
- }
139
- }
140
- const dirName = path.basename(path.dirname(absPath));
141
- const name = String(meta.name || dirName).trim();
142
- if (!name) return null;
143
- const description = String(meta.description || "").trim();
144
- return {
145
- name,
146
- description,
147
- content,
148
- body: stripFrontmatter(content),
149
- absPath,
150
- };
151
- }
152
-
153
- function listSkillDirs(rootDir) {
154
- try {
155
- return fs.readdirSync(rootDir, { withFileTypes: true })
156
- .filter((e) => e.isDirectory())
157
- .map((e) => path.join(rootDir, e.name, "SKILL.md"))
158
- .filter((p) => fs.existsSync(p));
159
- } catch {
160
- return [];
161
- }
128
+ export function listComposerSkills(packageRoot, workspaceRoot) {
129
+ return registryListSkills(packageRoot, workspaceRoot).map(({ body, content, ...skill }) => skill);
162
130
  }
163
131
 
164
- function skillSources(packageRoot, workspaceRoot) {
165
- const sources = [
166
- { source: "builtin", label: "AgentFlow", dir: path.join(packageRoot, "skills") },
167
- ];
168
- if (workspaceRoot) {
169
- sources.push(
170
- { source: "workspace-agents", label: ".agents", dir: path.join(workspaceRoot, ".agents", "skills") },
171
- { source: "workspace-cursor", label: ".cursor", dir: path.join(workspaceRoot, ".cursor", "skills") },
172
- );
173
- }
174
- return sources;
175
- }
176
-
177
- export function listComposerSkills(packageRoot, workspaceRoot) {
178
- const out = [];
179
- const seenKeys = new Set();
180
- for (const src of skillSources(packageRoot, workspaceRoot)) {
181
- for (const skillPath of listSkillDirs(src.dir)) {
182
- const skill = parseSkillFile(skillPath);
183
- if (!skill) continue;
184
- const key = `${src.source}:${skill.name}`;
185
- if (seenKeys.has(key)) continue;
186
- seenKeys.add(key);
187
- out.push({
188
- key,
189
- id: skill.name,
190
- name: skill.name,
191
- description: skill.description,
192
- source: src.source,
193
- sourceLabel: src.label,
194
- path: skill.absPath,
195
- });
196
- }
197
- }
198
- return out.sort((a, b) => {
199
- const bySource = a.sourceLabel.localeCompare(b.sourceLabel);
200
- if (bySource !== 0) return bySource;
201
- return a.name.localeCompare(b.name);
202
- });
132
+ export function readComposerSkillDetail(packageRoot, workspaceRoot, keyOrName) {
133
+ return registryReadSkillDetail(packageRoot, workspaceRoot, keyOrName);
203
134
  }
204
135
 
205
136
  export function loadResourcesForSkillKeys(skillKeys, packageRoot, workspaceRoot) {
@@ -210,13 +141,11 @@ export function loadResourcesForSkillKeys(skillKeys, packageRoot, workspaceRoot)
210
141
  if (wanted.size === 0) return { skills: [], references: [], skillsHint: "", hasContext: false };
211
142
 
212
143
  const skills = [];
213
- for (const item of listComposerSkills(packageRoot, workspaceRoot)) {
144
+ for (const item of registryListSkills(packageRoot, workspaceRoot)) {
214
145
  if (!wanted.has(item.key) && !wanted.has(item.name)) continue;
215
- const parsed = parseSkillFile(item.path);
216
- if (!parsed) continue;
217
146
  skills.push({
218
147
  id: item.name,
219
- content: parsed.body,
148
+ content: item.body,
220
149
  absPath: item.path,
221
150
  source: item.source,
222
151
  sourceLabel: item.sourceLabel,
@@ -356,6 +285,9 @@ export function buildSkillCompactInjectionBlock(skills, references) {
356
285
  */
357
286
  export function buildSkillInjectionBlock(skills, references) {
358
287
  const parts = [];
288
+ if ((skills?.length || 0) + (references?.length || 0) > 8) {
289
+ return buildSkillCompactInjectionBlock(skills || [], references || []);
290
+ }
359
291
 
360
292
  if (skills.length > 0) {
361
293
  parts.push("### 相关编辑技能(请严格遵循)");
@@ -263,8 +263,8 @@ export function unzipAndNormalizePipelineZip(zipBuffer) {
263
263
  * @param {Map<string, Buffer>} filesRelative 相对流水线根,须含 flow.yaml
264
264
  * @returns {{ success: true } | { success: false, error: string }}
265
265
  */
266
- export function writePipelineTree(workspaceRoot, flowId, flowSource, filesRelative) {
267
- const { flowDir, error } = resolveFlowDirForWrite(workspaceRoot, flowId, flowSource);
266
+ export function writePipelineTree(workspaceRoot, flowId, flowSource, filesRelative, opts = {}) {
267
+ const { flowDir, error } = resolveFlowDirForWrite(workspaceRoot, flowId, flowSource, opts);
268
268
  if (error) return { success: false, error };
269
269
  if (fs.existsSync(flowDir)) {
270
270
  return { success: false, error: "目标目录已存在" };