@fieldwangai/agentflow 0.1.25

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 (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +201 -0
  3. package/README.zh-CN.md +201 -0
  4. package/agents/agentflow-node-executor-code.md +32 -0
  5. package/agents/agentflow-node-executor-planning.md +32 -0
  6. package/agents/agentflow-node-executor-requirement.md +32 -0
  7. package/agents/agentflow-node-executor-test.md +32 -0
  8. package/agents/agentflow-node-executor-ui.md +32 -0
  9. package/agents/agentflow-node-executor.md +32 -0
  10. package/agents/agents.json +8 -0
  11. package/agents/en/agentflow-node-executor.md +32 -0
  12. package/agents/zh/agentflow-node-executor.md +32 -0
  13. package/bin/agentflow.mjs +52 -0
  14. package/bin/ensure-workspace-reference.mjs +35 -0
  15. package/bin/lib/agent-runners.mjs +1199 -0
  16. package/bin/lib/agents-path.mjs +61 -0
  17. package/bin/lib/api-runner.mjs +361 -0
  18. package/bin/lib/apply.mjs +852 -0
  19. package/bin/lib/catalog-agents.mjs +300 -0
  20. package/bin/lib/catalog-flows.mjs +532 -0
  21. package/bin/lib/composer-agent.mjs +884 -0
  22. package/bin/lib/composer-flow-instances.mjs +68 -0
  23. package/bin/lib/composer-flow-skeleton.mjs +334 -0
  24. package/bin/lib/composer-flow-validate.mjs +47 -0
  25. package/bin/lib/composer-log.mjs +197 -0
  26. package/bin/lib/composer-model-router.mjs +160 -0
  27. package/bin/lib/composer-node-schema.mjs +299 -0
  28. package/bin/lib/composer-planner.mjs +749 -0
  29. package/bin/lib/composer-script-ops.mjs +233 -0
  30. package/bin/lib/composer-skill-router.mjs +384 -0
  31. package/bin/lib/flow-import.mjs +305 -0
  32. package/bin/lib/flow-normalize.mjs +71 -0
  33. package/bin/lib/flow-write.mjs +395 -0
  34. package/bin/lib/help.mjs +139 -0
  35. package/bin/lib/hub-login.mjs +54 -0
  36. package/bin/lib/hub-publish.mjs +159 -0
  37. package/bin/lib/hub-remote.mjs +189 -0
  38. package/bin/lib/hub.mjs +299 -0
  39. package/bin/lib/i18n.mjs +233 -0
  40. package/bin/lib/locales/en.json +344 -0
  41. package/bin/lib/locales/zh.json +344 -0
  42. package/bin/lib/log.mjs +37 -0
  43. package/bin/lib/main.mjs +611 -0
  44. package/bin/lib/model-config.mjs +118 -0
  45. package/bin/lib/model-lists.mjs +188 -0
  46. package/bin/lib/node-exec-context.mjs +336 -0
  47. package/bin/lib/node-execute.mjs +513 -0
  48. package/bin/lib/normalize-node-tool-command.mjs +97 -0
  49. package/bin/lib/paths.mjs +216 -0
  50. package/bin/lib/pipeline-scripts.mjs +41 -0
  51. package/bin/lib/recent-runs.mjs +173 -0
  52. package/bin/lib/run-apply-active-lock.mjs +82 -0
  53. package/bin/lib/run-events.mjs +85 -0
  54. package/bin/lib/run-node-statuses-from-disk.mjs +85 -0
  55. package/bin/lib/schedule-config.mjs +227 -0
  56. package/bin/lib/scheduler.mjs +312 -0
  57. package/bin/lib/table.mjs +4 -0
  58. package/bin/lib/terminal.mjs +42 -0
  59. package/bin/lib/ui-print.mjs +94 -0
  60. package/bin/lib/ui-server.mjs +2113 -0
  61. package/bin/lib/workspace-tree.mjs +266 -0
  62. package/bin/lib/workspace.mjs +180 -0
  63. package/bin/pipeline/build-node-prompt.mjs +179 -0
  64. package/bin/pipeline/check-cache.mjs +191 -0
  65. package/bin/pipeline/check-flow.mjs +543 -0
  66. package/bin/pipeline/collect-nodes.mjs +212 -0
  67. package/bin/pipeline/compute-cache-md5.mjs +177 -0
  68. package/bin/pipeline/ensure-run-dir.mjs +71 -0
  69. package/bin/pipeline/extract-thinking.mjs +308 -0
  70. package/bin/pipeline/gc.mjs +129 -0
  71. package/bin/pipeline/get-env.mjs +83 -0
  72. package/bin/pipeline/get-exec-id.mjs +145 -0
  73. package/bin/pipeline/get-ready-nodes.mjs +435 -0
  74. package/bin/pipeline/get-resolved-values.mjs +337 -0
  75. package/bin/pipeline/load-key.mjs +62 -0
  76. package/bin/pipeline/parse-bool.mjs +33 -0
  77. package/bin/pipeline/parse-flow.mjs +698 -0
  78. package/bin/pipeline/post-process-control-if.mjs +23 -0
  79. package/bin/pipeline/post-process-node.mjs +490 -0
  80. package/bin/pipeline/pre-process-node.mjs +449 -0
  81. package/bin/pipeline/resolve-inputs.mjs +201 -0
  82. package/bin/pipeline/run-log.mjs +34 -0
  83. package/bin/pipeline/run-tool-nodejs.mjs +160 -0
  84. package/bin/pipeline/save-key.mjs +93 -0
  85. package/bin/pipeline/snapshot-prior-round.mjs +70 -0
  86. package/bin/pipeline/validate-flow.mjs +825 -0
  87. package/bin/pipeline/validate-for-ui.mjs +226 -0
  88. package/bin/pipeline/validate-script-output.mjs +130 -0
  89. package/bin/pipeline/write-result.mjs +182 -0
  90. package/builtin/nodes/agent_subAgent.md +14 -0
  91. package/builtin/nodes/control_agent_toBool.md +20 -0
  92. package/builtin/nodes/control_anyOne.md +17 -0
  93. package/builtin/nodes/control_end.md +11 -0
  94. package/builtin/nodes/control_if.md +20 -0
  95. package/builtin/nodes/control_start.md +11 -0
  96. package/builtin/nodes/control_toBool.md +21 -0
  97. package/builtin/nodes/provide_file.md +11 -0
  98. package/builtin/nodes/provide_str.md +11 -0
  99. package/builtin/nodes/tool_get_env.md +14 -0
  100. package/builtin/nodes/tool_load_key.md +20 -0
  101. package/builtin/nodes/tool_nodejs.md +40 -0
  102. package/builtin/nodes/tool_print.md +14 -0
  103. package/builtin/nodes/tool_save_key.md +20 -0
  104. package/builtin/nodes/tool_user_ask.md +23 -0
  105. package/builtin/nodes/tool_user_check.md +22 -0
  106. package/builtin/pipelines/module-migrate/flow.yaml +819 -0
  107. package/builtin/pipelines/module-migrate/scripts/check_imports.mjs +700 -0
  108. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Makefile +362 -0
  109. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/node_modules/node-addon-api/node_addon_api_except.stamp.d +1 -0
  110. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/tree_sitter_kotlin_binding/bindings/node/binding.o.d +17 -0
  111. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/tree_sitter_kotlin_binding/src/parser.o.d +5 -0
  112. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/tree_sitter_kotlin_binding/src/scanner.o.d +8 -0
  113. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/tree_sitter_kotlin_binding.node.d +1 -0
  114. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/node_modules/node-addon-api/node_addon_api_except.stamp +0 -0
  115. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/tree_sitter_kotlin_binding/bindings/node/binding.o +0 -0
  116. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/tree_sitter_kotlin_binding/src/parser.o +0 -0
  117. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/tree_sitter_kotlin_binding/src/scanner.o +0 -0
  118. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/tree_sitter_kotlin_binding.node +0 -0
  119. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/binding.Makefile +6 -0
  120. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/gyp-mac-tool +768 -0
  121. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api.Makefile +6 -0
  122. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api.target.mk +122 -0
  123. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api_except.target.mk +126 -0
  124. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api_maybe.target.mk +122 -0
  125. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/tree_sitter_kotlin_binding.target.mk +203 -0
  126. package/builtin/pipelines/new/flow.yaml +545 -0
  127. package/builtin/pipelines/new/scripts/check-flow.mjs +9 -0
  128. package/builtin/pipelines/new/scripts/collect-nodes.mjs +211 -0
  129. package/builtin/pipelines/scripts/adjust-node-positions.mjs +113 -0
  130. package/builtin/web-ui/dist/agentflow-icon.svg +23 -0
  131. package/builtin/web-ui/dist/assets/index-CZkUPcXE.css +1 -0
  132. package/builtin/web-ui/dist/assets/index-DkkhNESc.js +190 -0
  133. package/builtin/web-ui/dist/index.html +24 -0
  134. package/package.json +67 -0
  135. package/reference/flow-control-capabilities.md +274 -0
  136. package/reference/flow-layout.md +84 -0
  137. package/reference/flow-prompt-handler-check.md +12 -0
  138. package/reference/flow-result-semantics.md +14 -0
@@ -0,0 +1,749 @@
1
+ /**
2
+ * Composer 任务规划器:将用户编辑请求分解为可独立执行的子任务。
3
+ *
4
+ * 三种模式:
5
+ * 1. API 模式 — 使用快速模型(gpt-4o-mini 等)智能分解
6
+ * 2. 启发式模式 — 基于正则/关键词的简单分类(兜底)
7
+ * 3. 分阶段模式 — 大任务按「流转规划 → 节点补充 → 流程完善」三阶段逐轮生成
8
+ */
9
+ import fs from "fs";
10
+ import { parseApiModel } from "./api-runner.mjs";
11
+ import { buildPlannerSkillContext } from "./composer-skill-router.mjs";
12
+ import { buildNodeSchemaPromptSection } from "./composer-node-schema.mjs";
13
+ import { formatInstancePlannerHint } from "./composer-flow-instances.mjs";
14
+ import { resolveCliAndModel } from "./model-config.mjs";
15
+ import { runClaudeCodeAgentWithPrompt, runCursorAgentWithPrompt, runOpenCodeAgentWithPrompt } from "./agent-runners.mjs";
16
+ import { t } from "./i18n.mjs";
17
+
18
+ const PLANNER_MAX_TOKENS = 2048;
19
+ const PLANNER_TIMEOUT_MS = 15_000;
20
+
21
+ // ─── 分阶段定义 ──────────────────────────────────────────────────────────
22
+
23
+ /** 与 flow.yaml 同目录的节点规格书(阶段一产出,阶段二必读) */
24
+ export const COMPOSER_NODE_SPEC_FILENAME = "composer-node-spec.md";
25
+
26
+ const PHASED_DEFINITIONS = [
27
+ {
28
+ name: "flow_plan",
29
+ label: "流转规划",
30
+ description: "选定节点类型与整体框架,撰写规格书,建立实例与布局(主链拓扑;不补全副引脚连线)",
31
+ },
32
+ {
33
+ name: "node_enrich",
34
+ label: "节点补充",
35
+ description: "按规格书逐节点完善 agent 提示、tool 脚本与引脚索引",
36
+ },
37
+ {
38
+ name: "flow_finish",
39
+ label: "流程完善",
40
+ description: "补全连线与布局,校验并自动修复直至通过",
41
+ },
42
+ ];
43
+
44
+ const PHASED_PHASE_COUNT = PHASED_DEFINITIONS.length;
45
+
46
+ const PHASED_TRIGGER_PATTERNS = [
47
+ /(?:新建|创建|新增|生成|搭建|设计).*(?:流程|流水线|pipeline|flow|agentflow)/i,
48
+ /(?:从零|从头|从无到有).*(?:搭|建|写|做)/i,
49
+ /create\s+(?:a\s+)?(?:new\s+)?(?:flow|pipeline)/i,
50
+ /(?:重新设计|重构|重新规划|重新搭建|大改|重做).*(?:流程|流水线|pipeline|flow)/i,
51
+ /(?:流程|流水线|pipeline|flow).*(?:重新设计|重构|重新规划|大改|重做)/i,
52
+ ];
53
+
54
+ function shouldUsePhased(userPrompt, phaseContext) {
55
+ if (phaseContext && typeof phaseContext === "object" && typeof phaseContext.phaseIndex === "number") {
56
+ return true;
57
+ }
58
+ if (!userPrompt || typeof userPrompt !== "string") return false;
59
+ if (!PHASED_TRIGGER_PATTERNS.some((re) => re.test(userPrompt))) return false;
60
+ // 关键词匹配但需求简单(如"新建 agentflow 打印 helloworld")时不走分阶段
61
+ const complexity = classifyComplexity(userPrompt);
62
+ return complexity !== "simple";
63
+ }
64
+
65
+ // ─── 分阶段规划器提示 ─────────────────────────────────────────────────────
66
+
67
+ function buildPhasedSystemPrompt(phaseName, intents) {
68
+ const skillContext = buildPlannerSkillContext(intents || []);
69
+ const skillSection = skillContext ? "\n\n" + skillContext : "";
70
+ const schemaSection = "\n\n" + buildNodeSchemaPromptSection();
71
+
72
+ const phaseInstructions = {
73
+ flow_plan: `当前阶段:**流转规划**(框架与类型优先)。
74
+ 目标:建立**清晰、合理**的整体流转图(含分支 if、循环/回流、provide、getenv、并行汇合等),**节点类型选对、框架选对、单一权责**。
75
+
76
+ **前置已就位**(脚本已写盘):\`flow.yaml\` 含 control_start + control_end + 主链 edge + nodePositions;\`${COMPOSER_NODE_SPEC_FILENAME}\` 含三段 section 占位符。规划 step 时按"插入节点 / 调整 edge / 填充 section"组织,**不要**让 AI 重建 start/end 或重写 spec.md 标题。
77
+
78
+ 你需要生成 agent/script 步骤,指示 AI 完成:
79
+ 1. 在 flow.yaml 中**新增** instances(保留 start/end):\`definitionId\`、\`label\`、\`role\`;每个节点 \`body\` **一句**职责概要即可(也允许直接写得更详细,阶段二还能补强)。
80
+ 2. **input/output(不强制锁死)**:基础控制槽(\`prev\`/\`next\` 等)必须保留 schema 默认结构。**业务数据槽(text/file)能想清楚就一次写到 flow.yaml**,阶段二会跳过已存在的槽(按 name 去重);想不清的留到阶段二。type 必须 \`text\` 或 \`file\`,**禁止 node**。务必在节点规格书的「计划数据槽」section 也同步登记一份(作为完整设计文档与阶段二补漏依据)。
81
+ 3. **edges(不强制锁死)**:主链 \`output-0 → input-0\` 必连;副边(control_if 的 prediction、control_toBool 的 value、回流、并行汇合的 prev2、tool/agent 业务数据边)**能确定就连**,连后阶段三只补漏;不确定就留到阶段三。
82
+ 4. **ui.nodePositions**:为每个 instance 写入坐标(主链从左到右 x 每节点递增 **280**,起始 x:100 y:300;分支路径 y 错开 **200**)。
83
+ 5. **必须**在与 flow.yaml **同目录**创建(或更新)文件 **${COMPOSER_NODE_SPEC_FILENAME}**,结构如下:
84
+ - \`## 整体框架\`:叙述主链、分支、循环、并行、全局数据(SaveKey/LoadKey)等**设计意图**(可用条目列表)。
85
+ - \`## 节点职责\`:按 **instanceId** 分小节,写清该节点**具体要做什么**;对 **tool_nodejs** 须写明**计划脚本文件名**(如 \`scripts/xxx.mjs\`)与输入输出语义,并注明「**可执行代码在「节点补充」阶段写入**」;对 **agent_subAgent** 注明阶段二要写的 body 要点(判定规则、产物路径等)。
86
+ - \`## 计划数据槽\`:仅对**可扩展节点 ★**(agent_subAgent / tool_nodejs / tool_user_check)逐项列出阶段二要追加的数据槽:\`<instanceId>: input += [name:type, ...], output += [name:type, ...]\`,type 用 \`text\` 或 \`file\`;命名应与上下游一致便于连线(例如 \`agent_check: input += [toapp:text]; output += [result:text]\`)。**固定槽位节点**不在此列出。
87
+ 6. 保留且仅保留一个 control_start、一个 control_end(除非用户明确要求改造已有图)。
88
+ 7. **复杂任务循环拆解原则(关键)**
89
+ AgentFlow 的核心优势是通过**循环**分解复杂问题(普通 CLI 因上下文限制无法解决)。规划时须主动判断是否需要环路:
90
+
91
+ **必须使用循环的场景**(用 control_anyOne + control_toBool(确定性)/ control_agent_toBool(AI 判断)+ control_if 构建环路):
92
+ - 校验/检查类:代码检查、编译构建、测试运行、格式校验——不可能一次通过,必须 check → fix → re-check
93
+ - 迭代改进类:UI 还原、文档生成、翻译校对——需反复校对直至质量达标
94
+ - 批量处理类:逐文件/逐组件操作,结果需逐项确认
95
+ - 外部不确定性:API 调用可能失败、用户输入可能不合规
96
+
97
+ **循环模板 A:简单 bool 环**(适用于通过/不通过的二元判定)
98
+ control_anyOne(入口,汇合首次进入与修复回路) → 执行/检查 → control_toBool(判定结果) → control_if(分支)
99
+ control_if.next1(通过) → 出环到后续节点或 End
100
+ control_if.next2(未通过) → 修复节点 → 回到 control_anyOne(成环)
101
+
102
+ **循环模板 B:todolist 拆解+增量环**(推荐用于复杂/批量任务)
103
+ 适用于大任务拆解与逐项推进的场景(如逐文件修复、逐组件还原、逐任务编译、复杂需求分步实现):
104
+ 1. **拆解节点**(agent_subAgent 或 tool_nodejs):分析大任务,产出 todolist 文件(\`- [ ] 子任务1\\n- [ ] 子任务2\\n...\`),作为后续循环的驱动清单——**拆解记录与执行驱动合二为一**
105
+ 2. control_anyOne(入口) → **执行节点**(从清单取未完成项 \`- [ ]\`,执行后打勾 \`- [x]\`)
106
+ 3. control_toBool(判定 todolist 中是否全部 \`- [x]\`) → control_if(分支)
107
+ control_if.next1(全部完成) → 出环
108
+ control_if.next2(仍有 \`- [ ]\`) → 回到 control_anyOne(继续下一轮)
109
+ 优势:增量执行(每轮只处理未完成项)、进度可视化、断点续作。
110
+
111
+ **禁止**将校验/修复设计为线性链(check → fix → end);
112
+ 如果用户需求包含「检查」「验证」「确认」「校验」「测试」「构建」「遍历」「逐个」「批量」等关键词,必须设计成环路。
113
+ **大任务拆解时**,优先使用 todolist 模式:先用一个节点将大任务拆解为 todolist,再用循环逐项执行。`,
114
+
115
+ node_enrich: `当前阶段:**节点补充**(按节点迭代,可多步)。
116
+ 执行前须阅读 **${COMPOSER_NODE_SPEC_FILENAME}**(整体框架 + 各节点职责 + 计划数据槽)与当前 **flow.yaml**。
117
+
118
+ **幂等原则**:阶段一可能已经把部分业务槽 / body / script 写到 flow.yaml 了。本阶段是**补漏**而非重写——逐节点对照「spec.md 计划数据槽」与「flow.yaml 现状」:
119
+ - 槽位**已存在**(同 name)→ 跳过,不修改 type / 顺序
120
+ - 槽位**未追加** → 在 input/output 末尾补齐
121
+ - body / script 已经完整可执行 → 不动;为空或只有占位 → 写齐
122
+
123
+ 为**每个**需要完善的 instance 生成独立 agent 步骤(或确定性 script 步骤):
124
+ - **agent_subAgent**:若 body 已是可执行 prompt 则跳过;否则编写**准确、可执行**的 \`body\`(提示词/规则/输入输出占位 \`\${...}\`)。**复杂度用 "simple"**。
125
+ - **tool_nodejs**:若 \`script\` 字段已是完整命令且 scripts/ 下脚本已存在 → 跳过;否则在 **scripts/** 子目录创建 Node 脚本(\`scripts/<instanceId>.mjs\`),\`script\` 写入完整 \`node ...\` 调用并用 \`\${}\` 引用槽位。**引用 scripts/ 必须用 \`\${flowDir}/scripts/xxx.mjs\`**,不要写 \`\${workspaceRoot}/.workspace/agentflow/pipelines/\${flowName}/scripts/...\`(flow 可能安装到 \`~/agentflow/pipelines\` 或 builtin,硬编码 workspace 路径会找不到脚本)。**不要**给 \`\${workspaceRoot}\`、\`\${runDir}\` 等外包双引号(已自动 shell-quote)。**禁止**仅 body 自然语言无 script。
126
+ - **control_toBool / provide_str / provide_file** 等:按规格书补齐空的 \`body\` 或 output \`value\`。
127
+ - **引脚补漏**:核对 body/script 中每个 \`\${X}\` 是否对应实际槽位 name;缺槽就在末尾追加(type 必为 \`text\` 或 \`file\`,**绝不写 node**),多余的不要动(可能是阶段三连线用)。
128
+ - **不得删改基础控制槽**(\`prev\`/\`next\` 的 type/name 与顺序);**固定槽位节点**(control_* / provide_* / tool_load_key/save_key/get_env / tool_print / tool_user_ask)的 input/output 结构永不修改。
129
+
130
+ **引脚路径约束(tool_nodejs 必读):** 脚本的输入输出文件路径**必须通过引脚传入**,禁止在脚本内部自行拼接。
131
+ - \`script\` 字段中用 \`\${槽位名}\` 引用 input/output 的文件路径(如 \`--figma-tree \${figma_tree} --output \${restore_todolist}\`),流水线会将其解析为绝对路径并自动 shell-quote。
132
+ - 脚本通过命令行参数接收这些路径后直接读写,**禁止**在脚本中用 \`outDirForNode\`、手写 \`node_<instance>_xxx.json/md\` 等方式自行构造路径——否则产物路径与流水线解析器约定不一致,下游节点将找不到文件。
133
+ - output 类型为「文件」的槽位路径由流水线按约定生成(\`output/<instanceId>/node_<instanceId>_<slot>.md\`),脚本只需 \`ensureDir(path.dirname(outPath))\` 后写入即可。
134
+
135
+ 规则:**不要**在此阶段大改拓扑(尽量不增删 instance、不改 definitionId)。**已存在的边不要重复添加**;阶段一可能已连了部分副边,本阶段聚焦节点内容补漏。可适当微调 \`nodePositions\` 仅当妨碍阅读。`,
136
+
137
+ flow_finish: `当前阶段:**流程完善**。
138
+ 依据 **${COMPOSER_NODE_SPEC_FILENAME}** 与 flow.yaml 中已有实例与内容:
139
+
140
+ **幂等原则**:阶段一/二可能已经连了主链和部分副边。本阶段是**补漏 + 审计**而非重连:
141
+ - 边**已存在**(同 source/target/sourceHandle/targetHandle)→ 跳过,不重复添加
142
+ - 边**缺失**(spec.md「节点职责」的「输入 ← 来自 X」「输出 → 给 Y」描述了但 flow.yaml 没连)→ 补连
143
+
144
+ 具体:
145
+ 1. **补齐 edges**:数据流(text/file 槽对槽)、control_if 的 prediction(toBool.output-1 → if.input-1)、多输入汇合(anyOne.input-1 接修复回流)、tool 业务数据边等。**sourceHandle/targetHandle 必须与槽位索引一致**(如 output-1 → input-1)。
146
+ 2. **引脚语义审查 checklist**(每节点过一遍,发现问题修正):
147
+ a. **同 output 多消费者冲突**:一个 output 槽被两条边消费且消费方语义矛盾(如同时供 \`control_toBool.value\`(要 true/false 单行)和 \`agent.input\`(要详细内容)→ 必须**拆成两个 output 槽**(如 \`result:text\` + \`report:file\`)
148
+ b. **text vs file 错配**:内容超过 ~1KB 或为多行报告/日志/源码 → 应是 \`file\`;只是路径串/key/JSON 短串 → 应是 \`text\`
149
+ c. **bool 误用**:\`bool\` 槽只允许出现在 \`control_toBool.prediction\` / \`control_agent_toBool.prediction\`(out) 与 \`control_if.prediction\`(in) 这一对位置,其它任何节点禁用
150
+ d. **节点类型错配**:发现 \`tool_nodejs\` 实际做的是非确定性任务(代码翻译/源码理解/创意生成)→ 改 \`definitionId: agent_subAgent\` + 删 script + 把要求写到 body
151
+ e. **provide_* 类型对齐**:\`provide_str\` 必须 \`output[0].type=text\`;\`provide_file\` 必须 \`output[0].type=file\`
152
+ 3. **ui.nodePositions**:按 \`reference/flow-layout.md\` 优化布局(主链 x 递增、分支 y 错开、避免一条线)。
153
+ 4. 完成后应能通过 validate-flow;可用 add-edge、update-position 等 script 步骤,必要时用 agent 步骤处理复杂拓扑。
154
+
155
+ **不要**在此阶段重写 tool_nodejs 脚本正文(除非为审查 d 类型错配或修复占位符所必需)。`,
156
+ };
157
+
158
+ return `你是 AgentFlow Composer 的分阶段任务规划器。当前正在执行分阶段 flow 生成的特定阶段。
159
+
160
+ ${phaseInstructions[phaseName] || "根据当前阶段完成对应的操作。"}
161
+
162
+ ## script 类型(Node.js 直接执行,毫秒级)
163
+ 确定性操作,用户给了明确目标值时使用:
164
+ - edit-label:改 label(params: instanceId, value)
165
+ - edit-body:改 body(params: instanceId, value)
166
+ - edit-script:改 tool_nodejs 的 script(params: instanceId, value)**tool_nodejs 节点必须用此操作写入可执行命令**
167
+ - edit-role:改 role(params: instanceId, value)
168
+ - edit-input-value:改某个 input 的 value(params: instanceId, inputName, value)
169
+ - edit-output-value:改某个 output 的 value(params: instanceId, outputName, value)
170
+ - add-edge:连线(params: source, target, sourceHandle, targetHandle)
171
+ - remove-edge:断线(params: source, target)
172
+ - update-position:移动节点(params: instanceId, x, y)
173
+
174
+ ## agent 类型(AI 生成,需要选模型)
175
+ - complexity "simple":写简短文案、微调 body 等
176
+ - complexity "medium":添加节点并设计内容
177
+ - complexity "complex":重新设计流程拓扑、多节点重构
178
+
179
+ agent 步骤的 prompt 必须是独立可执行的精确指令,包含必要上下文(文件路径等)。
180
+
181
+ ## 规则
182
+ 1. 用户给了明确值 → script op;需要 AI 创造 → agent 步骤
183
+ 2. **tool_nodejs 节点(definitionId: tool_nodejs)**核心是 \`script\` 字段(可执行 shell/node),\`body\` 仅是说明不会被执行。
184
+ - 若能确定脚本内容 → 用 edit-script 直接写入完整可执行脚本
185
+ - 若需 AI 生成 → 用 agent 步骤,prompt 中**必须**写明:「把可执行脚本写入 \`script\` 字段(YAML \`|\` 块),body 只写一句说明。script 为空或为自然语言将导致节点失败。」
186
+ - \`script\` 中**禁止**写成 \`node "\${workspaceRoot}/..."\`:\`\${}\` 已会被单独转义,外包双引号会破坏路径;应写成 \`node \${workspaceRoot}/...\`。
187
+ - **引用 scripts/ 下文件**必须用 \`\${flowDir}/scripts/xxx.mjs\`(\`\${flowDir}\` 指向 flow 当前所在目录,兼容 user / workspace / builtin 三种安装位置);**禁止**写 \`\${workspaceRoot}/.workspace/agentflow/pipelines/\${flowName}/scripts/...\`(flow 非 workspace 安装时会 Cannot find module)。
188
+ 3. 只做当前阶段的工作,不要超出范围
189
+ 4. agent 步骤 prompt 里要包含 flow.yaml 路径和上下文
190
+ 5. 生成的 agent 步骤 prompt 必须**复述**或**引用**下方「内置节点 schema」中相关 definitionId 的槽位(type/name/顺序),让子 agent 不必再去读 builtin/nodes/${schemaSection}${skillSection}
191
+
192
+ 输出严格 JSON:
193
+ {
194
+ "steps": [
195
+ { "type": "script", "op": "add-edge", "description": "...", "params": { ... } },
196
+ { "type": "agent", "complexity": "medium", "description": "...", "prompt": "...",
197
+ "instanceId": "操作已有实例时必填", "nodeRole": "可选", "executorModel": "可选" }
198
+ ]
199
+ }`;
200
+ }
201
+
202
+ function buildPhasedUserMessage(userPrompt, flowYaml, flowYamlAbs, phaseName, phaseIndex, thread) {
203
+ const parts = [];
204
+ const historyBlock = formatPlannerThreadHistory(thread);
205
+ if (historyBlock) parts.push(historyBlock);
206
+ parts.push(`## 用户原始需求\n${userPrompt.trim()}`);
207
+ parts.push(`\n## 当前阶段\n第 ${phaseIndex + 1}/${PHASED_PHASE_COUNT} 阶段:${phaseName}`);
208
+ if (flowYaml) {
209
+ const trimmed = flowYaml.length > 6000 ? flowYaml.slice(0, 6000) + "\n# ... (truncated)" : flowYaml;
210
+ parts.push(`\n## 当前 flow.yaml(${flowYamlAbs || ""})\n\`\`\`yaml\n${trimmed}\n\`\`\``);
211
+ parts.push(formatInstancePlannerHint(flowYaml));
212
+ }
213
+ parts.push("\n请输出 JSON 任务分解(仅包含当前阶段需要的步骤)。");
214
+ return parts.join("\n");
215
+ }
216
+
217
+ // ─── 规划器系统提示 ────────────────────────────────────────────────────────
218
+
219
+ function buildPlannerSystemPrompt(intents) {
220
+ const skillContext = buildPlannerSkillContext(intents || []);
221
+ const skillSection = skillContext ? "\n\n" + skillContext : "";
222
+ const schemaSection = "\n\n" + buildNodeSchemaPromptSection();
223
+
224
+ return `你是 AgentFlow Composer 的任务规划器。将用户的 flow.yaml 编辑请求分解为独立子任务。
225
+
226
+ ## script 类型(Node.js 直接执行,毫秒级)
227
+ 确定性操作,用户给了明确目标值时使用:
228
+ - edit-label:改 label(params: instanceId, value)
229
+ - edit-body:改 body,用户给了完整文本(params: instanceId, value)
230
+ - edit-script:改 tool_nodejs 的 script(params: instanceId, value)**tool_nodejs 节点必须用此操作写入可执行命令**
231
+ - edit-role:改 role(params: instanceId, value)
232
+ - edit-input-value:改某个 input 的 value(params: instanceId, inputName, value)
233
+ - edit-output-value:改某个 output 的 value(params: instanceId, outputName, value)
234
+ - add-edge:连线(params: source, target, sourceHandle, targetHandle)
235
+ - remove-edge:断线(params: source, target)
236
+ - update-position:移动节点(params: instanceId, x, y)
237
+
238
+ ## agent 类型(AI 生成,需要选模型)
239
+ - complexity "simple":写简短文案、微调 body 等(用快速模型)
240
+ - complexity "medium":添加节点并设计内容(用中等模型)
241
+ - complexity "complex":重新设计流程拓扑、多节点重构(用强力模型)
242
+
243
+ agent 步骤的 prompt 必须是独立可执行的精确指令,包含必要上下文(文件路径等)。
244
+
245
+ ## 规则
246
+ 1. 用户给了明确值 → script op;需要 AI 创造 → agent 步骤
247
+ 2. **tool_nodejs 节点(definitionId: tool_nodejs)**核心是 \`script\` 字段(可执行 shell/node 命令),\`body\` 仅是说明文本不会被执行。
248
+ - 若能确定脚本内容 → 用 edit-script 直接写入
249
+ - 若需 AI 生成 → 用 agent 步骤,prompt 中**必须**写明:「把可执行脚本写入 instance 的 \`script\` 字段(YAML \`|\` 块),body 只写一句说明。script 为空或为自然语言将导致节点运行失败。」
250
+ - \`script\` 中**禁止** \`node "\${workspaceRoot}/..."\`:应 \`node \${workspaceRoot}/...\`(\`\${}\` 已单独 shell 转义,勿再包双引号)。
251
+ - **引用 scripts/ 下文件**必须用 \`\${flowDir}/scripts/xxx.mjs\`(\`\${flowDir}\` 解析到 flow 真实目录,兼容 user / workspace / builtin 安装位置);**禁止**硬编码 \`\${workspaceRoot}/.workspace/agentflow/pipelines/\${flowName}/scripts/...\`。
252
+ - **引脚路径约束**:脚本读写文件的路径**必须通过引脚传入**。\`script\` 中用 \`\${槽位名}\` 引用 input/output 路径(如 \`--input \${figma_tree} --output \${todolist}\`),脚本通过命令行参数接收后直接读写。**禁止**在脚本内自行拼 \`node_<instance>_xxx\` 或用 \`outDirForNode\` 构造路径——产物路径与流水线解析器约定不一致会导致下游节点找不到文件。
253
+ 3. 拆小不合大:每步只做一件事
254
+ 4. 不同节点的内容生成拆为独立 agent 步骤;涉及已有实例时填写 **instanceId**,**nodeRole** 与该实例在 YAML 中的 role 一致;需要指定本步 CLI 模型时可填 **executorModel**
255
+ 5. 先改内容再连线,先加节点再连线
256
+ 6. agent 步骤 prompt 里要包含 flow.yaml 路径和上下文
257
+ 7. **循环拆解**:涉及校验/检查/迭代/批量/遍历的流程,agent 步骤的 prompt 中须指导 AI 用 control_anyOne + control_toBool(确定性)/ control_agent_toBool(AI 判断)+ control_if 设计环路(check → fix → re-check),禁止线性链。批量/大任务推荐 todolist 模式(拆解产出 \`- [ ]\` 清单 → 循环执行打勾 → ToBool 判定全部完成 → If 出环/继续)${schemaSection}${skillSection}
258
+
259
+ 输出严格 JSON:
260
+ {
261
+ "steps": [
262
+ { "type": "script", "op": "edit-label", "description": "...", "params": { ... } },
263
+ { "type": "agent", "complexity": "simple", "description": "...", "prompt": "...",
264
+ "instanceId": "操作已有实例时**必填**:本步主要操作的实例 id。若「关联节点 ID」列出了画布选中的 id,必须填入对应步骤",
265
+ "nodeRole": "可选,与画布角色一致:requirement|planning|code|test|normal(或中文:需求拆解|技术规划|代码执行|测试回归|普通)",
266
+ "executorModel": "可选,本步执行模型(覆盖实例 model;不设则用实例或用户全局模型)" }
267
+ ]
268
+ }`;
269
+ }
270
+
271
+ function formatPlannerThreadHistory(thread) {
272
+ if (!thread || thread.length === 0) return "";
273
+ const MAX_CHARS = 3000;
274
+ const recent = thread.slice(-10);
275
+ const lines = [];
276
+ let chars = 0;
277
+ for (let i = recent.length - 1; i >= 0; i--) {
278
+ const m = recent[i];
279
+ const label = m.role === "user" ? "用户" : "助手";
280
+ const text = m.text.length > 600 ? m.text.slice(0, 600) + "…(截断)" : m.text;
281
+ const line = `${label}:${text}`;
282
+ if (chars + line.length > MAX_CHARS) break;
283
+ lines.unshift(line);
284
+ chars += line.length;
285
+ }
286
+ if (lines.length === 0) return "";
287
+ return "\n## 对话历史\n" + lines.join("\n\n");
288
+ }
289
+
290
+ function buildPlannerUserMessage(userPrompt, flowYaml, instanceIds, flowYamlAbs, thread) {
291
+ const parts = [];
292
+ const historyBlock = formatPlannerThreadHistory(thread);
293
+ if (historyBlock) parts.push(historyBlock);
294
+ parts.push(`## 用户请求\n${userPrompt.trim()}`);
295
+ if (flowYaml) {
296
+ const trimmed = flowYaml.length > 6000 ? flowYaml.slice(0, 6000) + "\n# ... (truncated)" : flowYaml;
297
+ parts.push(`\n## 当前 flow.yaml(${flowYamlAbs || ""})\n\`\`\`yaml\n${trimmed}\n\`\`\``);
298
+ parts.push(formatInstancePlannerHint(flowYaml));
299
+ }
300
+ if (instanceIds?.length) {
301
+ parts.push(`\n## 关联节点 ID(画布选中,agent 步骤必须填入 instanceId)\n${instanceIds.join(", ")}`);
302
+ }
303
+ parts.push("\n请输出 JSON 任务分解。");
304
+ return parts.join("\n");
305
+ }
306
+
307
+ // ─── API 调用 ──────────────────────────────────────────────────────────────
308
+
309
+ async function callPlannerApi(systemPrompt, userMessage, apiProvider) {
310
+ const { provider, apiKey, baseUrl, model } = apiProvider;
311
+
312
+ if (provider === "anthropic") {
313
+ const resp = await fetch("https://api.anthropic.com/v1/messages", {
314
+ method: "POST",
315
+ headers: {
316
+ "Content-Type": "application/json",
317
+ "x-api-key": apiKey,
318
+ "anthropic-version": "2023-06-01",
319
+ },
320
+ body: JSON.stringify({
321
+ model,
322
+ system: systemPrompt,
323
+ messages: [{ role: "user", content: userMessage }],
324
+ max_tokens: PLANNER_MAX_TOKENS,
325
+ }),
326
+ signal: AbortSignal.timeout(PLANNER_TIMEOUT_MS),
327
+ });
328
+ if (!resp.ok) throw new Error(`Anthropic ${resp.status}`);
329
+ const data = await resp.json();
330
+ const text = data.content?.find((b) => b.type === "text")?.text ?? "";
331
+ return text;
332
+ }
333
+
334
+ const url = `${(baseUrl || "https://api.openai.com/v1").replace(/\/$/, "")}/chat/completions`;
335
+ const resp = await fetch(url, {
336
+ method: "POST",
337
+ headers: {
338
+ "Content-Type": "application/json",
339
+ Authorization: `Bearer ${apiKey}`,
340
+ },
341
+ body: JSON.stringify({
342
+ model,
343
+ messages: [
344
+ { role: "system", content: systemPrompt },
345
+ { role: "user", content: userMessage },
346
+ ],
347
+ max_tokens: PLANNER_MAX_TOKENS,
348
+ response_format: { type: "json_object" },
349
+ }),
350
+ signal: AbortSignal.timeout(PLANNER_TIMEOUT_MS),
351
+ });
352
+ if (!resp.ok) throw new Error(`OpenAI ${resp.status}`);
353
+ const data = await resp.json();
354
+ return data.choices?.[0]?.message?.content ?? "";
355
+ }
356
+
357
+ function parseStepsJson(raw) {
358
+ const text = String(raw || "").trim();
359
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
360
+ if (!jsonMatch) return null;
361
+ try {
362
+ const parsed = JSON.parse(jsonMatch[0]);
363
+ if (Array.isArray(parsed.steps) && parsed.steps.length > 0) return parsed.steps;
364
+ } catch { /* ignore */ }
365
+ return null;
366
+ }
367
+
368
+ // ─── 启发式分类(兜底) ────────────────────────────────────────────────────
369
+
370
+ const HEURISTIC_PATTERNS = [
371
+ { re: /(?:改|修改|更新|设置|设为|改为|换成).*(?:label|标签|名称|名字)/i, type: "script", op: "edit-label" },
372
+ { re: /(?:改|修改|更新|设置).*(?:role|角色)/i, type: "script", op: "edit-role" },
373
+ { re: /(?:连接|连线|接到|连到|接入)/i, type: "script", op: "add-edge" },
374
+ { re: /(?:断开|删除连线|删除边|取消连接|断线)/i, type: "script", op: "remove-edge" },
375
+ { re: /(?:移动|拖到|移到|位置)/i, type: "script", op: "update-position" },
376
+ ];
377
+
378
+ function classifyComplexity(prompt) {
379
+ const len = prompt.length;
380
+ const hasMultipleActions = /[,,].*(?:并|且|然后|接着|同时|再)/.test(prompt);
381
+ const hasTopologyChange = /(?:重新设计|重构|重新规划|调整.*流程|拓扑)/.test(prompt);
382
+ if (hasTopologyChange || len > 300) return "complex";
383
+ if (hasMultipleActions || len > 150) return "medium";
384
+ return "simple";
385
+ }
386
+
387
+ function heuristicPlan(userPrompt) {
388
+ for (const { re, type, op } of HEURISTIC_PATTERNS) {
389
+ if (re.test(userPrompt)) {
390
+ return [{ type, op, description: userPrompt.slice(0, 120), params: {} }];
391
+ }
392
+ }
393
+ const complexity = classifyComplexity(userPrompt);
394
+ return [{ type: "agent", complexity, description: userPrompt.slice(0, 120), prompt: userPrompt }];
395
+ }
396
+
397
+ // ─── AI 任务复杂度分类(通过 Cursor/OpenCode CLI 或 API) ─────────────────
398
+
399
+ const CLASSIFY_PROMPT = `你是 AgentFlow 任务分类器。判断用户对流程图的编辑请求属于哪类:
400
+ - "multi":涉及分支/循环/并行等复杂拓扑的新建流程、重构/重新设计流程、涉及多个节点的增删改、复杂拓扑变更
401
+ - "single":新建简单线性流程(如仅需 start→一两个节点→end 的简单任务)、改标签/文案、改一个节点的 body 或 script、连/断一条边、移动节点位置、添加一两个节点的小改动
402
+
403
+ 判断"新建"请求时,重点看需求本身的复杂度:如果需求简单(如打印文本、单步操作),即使是新建也应归为 single;只有需求涉及多节点协作、分支判断、循环检查等才归为 multi。
404
+
405
+ 用户请求:
406
+ ---
407
+ {USER_PROMPT}
408
+ ---
409
+
410
+ 只回复 multi 或 single 一个单词,不要解释。`;
411
+
412
+ const CLASSIFY_CLI_TIMEOUT_MS = 60_000;
413
+
414
+ /**
415
+ * 通过 Cursor/OpenCode CLI 调用 AI 做分类。
416
+ * 收集 stdout 中的 assistant 文本,提取 multi/single。
417
+ */
418
+ async function classifyViaCli(userPrompt, cliWorkspace) {
419
+ const prompt = CLASSIFY_PROMPT.replace("{USER_PROMPT}", userPrompt.slice(0, 2000));
420
+ const { cli, model } = resolveCliAndModel(cliWorkspace, null, null);
421
+
422
+ let collected = "";
423
+ const onStreamEvent = (ev) => {
424
+ if (ev.type === "natural" && typeof ev.text === "string") {
425
+ collected += " " + ev.text;
426
+ }
427
+ };
428
+
429
+ const runner = cli === "opencode"
430
+ ? runOpenCodeAgentWithPrompt(cliWorkspace, prompt, { onStreamEvent, model: model || undefined })
431
+ : cli === "claude-code"
432
+ ? runClaudeCodeAgentWithPrompt(cliWorkspace, prompt, { onStreamEvent, model: model || undefined })
433
+ : runCursorAgentWithPrompt(cliWorkspace, prompt, { onStreamEvent, model: model || undefined, force: true });
434
+
435
+ const timeout = new Promise((_, reject) =>
436
+ setTimeout(() => {
437
+ try { runner.child.kill("SIGTERM"); } catch {}
438
+ reject(new Error(`[CLASSIFY_TIMEOUT] ${cli} CLI 分类超时 (${CLASSIFY_CLI_TIMEOUT_MS}ms), collected: "${collected.trim().slice(0, 100)}"`));
439
+ }, CLASSIFY_CLI_TIMEOUT_MS),
440
+ );
441
+
442
+ await Promise.race([runner.finished, timeout]);
443
+
444
+ const lower = collected.toLowerCase();
445
+ if (lower.includes("multi")) return "multi";
446
+ if (lower.includes("single")) return "single";
447
+ return null;
448
+ }
449
+
450
+ /**
451
+ * AI 判断任务是否需要多步执行。
452
+ * 优先级:分阶段正则快检 → CLI AI 分类 → 正则启发式兜底。
453
+ * @param {string} userPrompt
454
+ * @param {string} [cliWorkspace] Cursor/OpenCode 工作目录
455
+ * @returns {Promise<"multi" | "single">}
456
+ */
457
+ export async function classifyTaskComplexity(userPrompt, cliWorkspace) {
458
+ if (cliWorkspace) {
459
+ try {
460
+ const result = await classifyViaCli(userPrompt, cliWorkspace);
461
+ if (result) return result;
462
+ } catch (e) {
463
+ process.stderr.write(`[classifyTaskComplexity] CLI classify failed, falling back to heuristic: ${e.message}\n`);
464
+ }
465
+ }
466
+
467
+ return classifyComplexity(userPrompt) === "complex" ? "multi" : "single";
468
+ }
469
+
470
+ // ─── 选择规划器使用的快速模型 ──────────────────────────────────────────────
471
+
472
+ function resolvePlannerApiProvider(plannerModel) {
473
+ if (plannerModel) {
474
+ const { provider, model } = parseApiModel(plannerModel);
475
+ if (provider === "anthropic") {
476
+ const key = process.env.ANTHROPIC_API_KEY;
477
+ if (key) return { provider: "anthropic", apiKey: key, model, baseUrl: null };
478
+ } else {
479
+ const key = process.env.OPENAI_API_KEY;
480
+ if (key) {
481
+ const baseUrl = process.env.OPENAI_BASE_URL || "https://api.openai.com/v1";
482
+ return { provider: "openai", apiKey: key, baseUrl, model };
483
+ }
484
+ }
485
+ }
486
+ const openaiKey = process.env.OPENAI_API_KEY;
487
+ if (openaiKey) {
488
+ const baseUrl = process.env.OPENAI_BASE_URL || "https://api.openai.com/v1";
489
+ return { provider: "openai", apiKey: openaiKey, baseUrl, model: "gpt-4o-mini" };
490
+ }
491
+ const anthropicKey = process.env.ANTHROPIC_API_KEY;
492
+ if (anthropicKey) {
493
+ return { provider: "anthropic", apiKey: anthropicKey, model: "claude-3-5-haiku-20241022", baseUrl: null };
494
+ }
495
+ return null;
496
+ }
497
+
498
+ // ─── 公开接口 ──────────────────────────────────────────────────────────────
499
+
500
+ /**
501
+ * Post-planner: patch agent steps missing instanceId using canvas selection.
502
+ * Heuristic: if step prompt mentions a canvas-selected id, fill it.
503
+ * If only one node selected and no match found, assign to all unassigned steps.
504
+ */
505
+ function patchMissingInstanceIds(steps, instanceIds) {
506
+ if (!steps?.length || !instanceIds?.length) return;
507
+ for (const step of steps) {
508
+ if (step.type !== "agent") continue;
509
+ const sid = step.instanceId != null ? String(step.instanceId).trim() : "";
510
+ if (sid) continue;
511
+ // Heuristic: check if step text mentions any canvas-selected id
512
+ const text = (step.prompt || "") + " " + (step.description || "");
513
+ for (const cid of instanceIds) {
514
+ if (text.includes(cid)) {
515
+ step.instanceId = cid;
516
+ break;
517
+ }
518
+ }
519
+ // Single node selected + no match → assign directly
520
+ if (!step.instanceId && instanceIds.length === 1) {
521
+ step.instanceId = instanceIds[0];
522
+ }
523
+ }
524
+ }
525
+
526
+ /**
527
+ * @param {object} opts
528
+ * @param {string} opts.userPrompt
529
+ * @param {string} [opts.flowYaml] 当前 flow.yaml 内容
530
+ * @param {string} [opts.flowYamlAbs] flow.yaml 绝对路径
531
+ * @param {string[]} [opts.instanceIds]
532
+ * @param {Array<{ role: string, text: string }>} [opts.thread] 对话历史
533
+ * @param {string[]} [opts.intents] 检测到的用户意图(来自 composer-skill-router)
534
+ * @param {string} [opts.plannerModel] 指定规划器模型,如 "api:openai/gpt-4o-mini"
535
+ * @param {object} [opts.phaseContext] 分阶段上下文 { phaseIndex, phases, userPromptOriginal }
536
+ * @param {string} [opts.phaseRole] 用户为本阶段指定的默认节点角色
537
+ * @param {(ev: object) => void} [opts.onEvent]
538
+ * @returns {Promise<{ steps: Array<{ type: string, op?: string, complexity?: string, description: string, params?: object, prompt?: string }>, phased?: boolean, phases?: Array, currentPhase?: number }>}
539
+ */
540
+ export async function planComposerTasks(opts) {
541
+ const emit = opts.onEvent || (() => {});
542
+ const apiProvider = resolvePlannerApiProvider(opts.plannerModel);
543
+
544
+ const phaseCtx = opts.phaseContext;
545
+ const isPhased = shouldUsePhased(opts.userPrompt, phaseCtx);
546
+
547
+ if (isPhased) {
548
+ const phaseIndex = (phaseCtx && typeof phaseCtx.phaseIndex === "number") ? phaseCtx.phaseIndex : 0;
549
+ const result = await planSinglePhase({
550
+ ...opts,
551
+ phaseIndex,
552
+ apiProvider,
553
+ emit,
554
+ });
555
+ return result;
556
+ }
557
+
558
+ if (!apiProvider) {
559
+ emit({ type: "status", line: t("planner.heuristic_analysis") });
560
+ return { steps: heuristicPlan(opts.userPrompt) };
561
+ }
562
+
563
+ emit({ type: "status", line: t("planner.planning", { model: apiProvider.model }) });
564
+
565
+ let flowYaml = opts.flowYaml || "";
566
+ if (!flowYaml && opts.flowYamlAbs) {
567
+ try { flowYaml = fs.readFileSync(opts.flowYamlAbs, "utf-8"); } catch { /* ignore */ }
568
+ }
569
+
570
+ const systemPrompt = buildPlannerSystemPrompt(opts.intents);
571
+ const userMessage = buildPlannerUserMessage(opts.userPrompt, flowYaml, opts.instanceIds, opts.flowYamlAbs, opts.thread);
572
+
573
+ emit({ type: "ai-log", tag: "planner-system", text: systemPrompt, meta: { provider: apiProvider.provider, model: apiProvider.model, mode: "regular" } });
574
+ emit({ type: "ai-log", tag: "planner-user", text: userMessage, meta: { flowYamlAbs: opts.flowYamlAbs || null, instanceIds: opts.instanceIds || [] } });
575
+
576
+ try {
577
+ const raw = await callPlannerApi(systemPrompt, userMessage, apiProvider);
578
+ emit({ type: "ai-log", tag: "planner-response", text: String(raw || ""), meta: { provider: apiProvider.provider, model: apiProvider.model } });
579
+ const steps = parseStepsJson(raw);
580
+ if (steps && steps.length > 0) {
581
+ patchMissingInstanceIds(steps, opts.instanceIds);
582
+ return { steps };
583
+ }
584
+ emit({ type: "status", line: t("planner.planner_format_error") });
585
+ } catch (e) {
586
+ emit({ type: "status", line: t("planner.planner_call_failed", { message: e.message }) });
587
+ }
588
+
589
+ const fallbackSteps = heuristicPlan(opts.userPrompt);
590
+ patchMissingInstanceIds(fallbackSteps, opts.instanceIds);
591
+ return { steps: fallbackSteps };
592
+ }
593
+
594
+ // ─── 分阶段 CLI 快捷路径:追加到 agent prompt 的固定指引 ───────────────────
595
+ // 节点类型速查已并入 buildNodeSchemaCompactSection,不在此处重复
596
+
597
+ /** 与 API 规划器 flow_plan 阶段对齐的 CLI 指引 */
598
+ function buildPhaseCliGuide(phaseIndex) {
599
+ if (phaseIndex === 0) {
600
+ return `
601
+
602
+ ## 阶段:流转规划(必读)
603
+ 目标:**框架合理、类型正确、职责清晰**。建立可读的流转图(含 if/循环/并行等时选对节点)。
604
+
605
+ **前置已就位**(脚本已写盘,**无需重建**):
606
+ - \`flow.yaml\` 已含 \`control_start\` + \`control_end\` + 一条主链 edge(output-0 → input-0)+ \`ui.nodePositions\`
607
+ - \`${COMPOSER_NODE_SPEC_FILENAME}\` 已含三段 section 占位符(整体框架 / 节点职责 / 计划数据槽)
608
+
609
+ 你的工作(**鼓励一次写到位,不强行拆阶段**):
610
+ 1. **向 flow.yaml 的 instances 中插入新节点**(保留 start/end,不要删改现有两节点):definitionId、label。body 写一句职责概要即可;如果你已经清楚要做什么,**也允许直接写得详细**——阶段二会按 name 去重不会重复。
611
+ 2. **input/output(不强制锁死)**:基础控制槽(\`prev\`/\`next\` 等)保留 schema 默认结构。**业务数据槽(text/file)想清楚的就直接追加到 flow.yaml** 末尾,阶段二会跳过已存在的 name;想不清的留到阶段二。type 必为 \`text\` 或 \`file\`,**禁止 node**。务必**同时**在 spec.md「计划数据槽」section 登记一份完整设计(即使你已落到 yaml)——让 spec.md 始终是完整设计文档,阶段二/三的补漏依据。
612
+ 3. **edges(不强制锁死)**:
613
+ - 删除 skeleton 的 start→end 占位 edge,改为 Start→N1→N2→…→End 的主路径,均用 **output-0 → input-0**
614
+ - 副边(control_if 的 prediction、control_toBool 的 value、回流到 anyOne.prev2、tool/agent 业务数据边)**能确定就连**——连后阶段三只补漏;不确定就留到阶段三,spec.md 节点职责里写清「输入 ← 来自 X」「输出 → 给 Y」即可
615
+ 4. **ui.nodePositions**:为每个新 instance 加坐标,主链从左到右 x 每节点递增 **280**(起始 x:100 y:300),分支 y 错开 **200**。不要动 start/end 现有坐标。
616
+ 5. **填充 ${COMPOSER_NODE_SPEC_FILENAME} 三段 section**(保留文件结构与标题;删除 HTML 注释占位 \`<!-- ... -->\` 后写入具体内容):
617
+ - \`## 整体框架\`:主链、分支、循环、并行、全局数据等设计说明。
618
+ - \`## 节点职责\`:**每个 instanceId 必须写齐 4 项**:
619
+ - **职责**:一句话
620
+ - **输入**:列出每个 input 槽 → 语义 + 来源(\`<上游id>.<slot>\`)
621
+ - **输出**:列出每个 output 槽 → 语义 + 去向(\`<下游id>.<slot>\`)
622
+ - **实现要点**:tool_nodejs 写脚本路径(\`scripts/<id>.mjs\`)、agent_subAgent 写 body 关键点、control_* 写判定/汇合规则、provide_* 写固定值
623
+ 用途:输入/输出的来源/去向就是阶段三连线 \`sourceHandle/targetHandle\` 的依据,写清此处可避免阶段三反复猜
624
+ - \`## 计划数据槽\`:仍然要写(即使阶段一已落到 yaml)——是阶段二/三的设计权威与对账依据
625
+ 6. **循环拆解原则**:AgentFlow 通过循环分解复杂问题。涉及校验/检查/迭代/批量/遍历的任务,**必须**用 control_anyOne + control_toBool(确定性)/ control_agent_toBool(AI 判断)+ control_if 构建环路(check → fix → re-check),**禁止**设计成线性链。批量/大任务优先使用 **todolist 模式**:拆解节点产出 \`- [ ]\` 清单 → 循环执行并打勾 → ToBool 判定全部完成 → If 出环/继续。
626
+ **节点类型选型判据**:**确定性任务 → \`tool_nodejs\`;非确定性任务 → \`agent_subAgent\`**。
627
+ - **确定性** = 相同输入永远产出相同输出,可用普通代码完整描述(跑 CLI、npm、读写文件、JSON/路径转换、调现成 API 解析固定格式)→ tool_nodejs
628
+ - **非确定性** = 需语义理解或创造(**代码翻译/生成**如 Android→RN、Vue→React;**理解源码/文本**如解析、改写、review;**多步推理决策**;**创意写作**)→ agent_subAgent
629
+ - 醒目输出 → \`tool_print\`;分支 → \`toBool+if\`;固定文本 → \`provide\`;密钥 → \`get_env\`
630
+ **反例**:『Android 页面转 RN/TS』『代码 review』必然非确定性,必须 agent_subAgent,做成 tool_nodejs 必然失败。`;
631
+ }
632
+ if (phaseIndex === 1) {
633
+ return `
634
+
635
+ ## 阶段:节点补充(必读)
636
+ 1. 阅读 **${COMPOSER_NODE_SPEC_FILENAME}**(路径见上下文)与 **flow.yaml**。
637
+ 2. **agent_subAgent**:写准确、完整的 **body**(规则、\`\${...}\` 路径);本阶段宜用**较快/普通模型**完成即可。
638
+ 3. **tool_nodejs**:在流水线 **scripts/** 下创建 **Node 可执行脚本**(路径见上下文),\`script\` 字段写完整 shell/node 调用命令;遵守仓库 Node 与 tool_nodejs 规范。**不要**写成 \`node "\${workspaceRoot}/..."\`,应 \`node \${workspaceRoot}/...\`(占位符已单独转义,外包双引号会坏路径)。**引用 scripts/ 下文件必须用 \`\${flowDir}/scripts/xxx.mjs\`**(\`\${flowDir}\` 兼容 user / workspace / builtin 安装),**禁止** \`\${workspaceRoot}/.workspace/agentflow/pipelines/\${flowName}/scripts/...\`。
639
+ 4. **引脚路径约束**:脚本读写文件的路径**必须从引脚传入**,\`script\` 中用 \`\${槽位名}\` 引用 input/output 路径(如 \`--input \${figma_tree} --output \${todolist}\`)。**禁止**在脚本内自行拼 \`node_<instance>_xxx\` 或调用 \`outDirForNode\` 构造路径——否则产物路径与流水线解析器约定不一致,下游节点找不到文件。
640
+ 5. **引脚**:按节点定义与 reference 核对 **input/output 顺序与索引**(input-0、output-1 等),与规格书一致。
641
+ 6. **不要**大改实例拓扑或补全复杂副引脚连线(留待流程完善)。`;
642
+ }
643
+ if (phaseIndex === 2) {
644
+ return `
645
+
646
+ ## 阶段:流程完善(必读)
647
+ 1. 依据 **${COMPOSER_NODE_SPEC_FILENAME}** 与当前 **flow.yaml**,**补全所有 edges**(含 if 的 prediction、多路 handle 等),handle 与槽位索引严格对应。
648
+ 2. **引脚语义审查 checklist**(每节点过一遍):
649
+ a. **同 output 多消费者冲突**:一个 output 同时供给两个语义矛盾的下游(如 \`toBool.value\` 要单行 true/false 与 \`agent.input\` 要详细内容)→ 拆成两个 output 槽
650
+ b. **text/file 错配**:内容超 ~1KB 或多行报告/源码 → 应是 \`file\`;只是路径串/key/JSON 短串 → 应是 \`text\`
651
+ c. **bool 误用**:\`bool\` 槽只允许 \`control_toBool.prediction\` / \`control_agent_toBool.prediction\`(out) → \`control_if.prediction\`(in),其它禁用
652
+ d. **节点类型错配**:\`tool_nodejs\` 实际做非确定性任务(代码翻译/源码理解/创意生成)→ 改 \`definitionId: agent_subAgent\` + 删 script + 写 body
653
+ e. **provide_* 类型对齐**:\`provide_str.output[0].type\` 必为 \`text\`;\`provide_file.output[0].type\` 必为 \`file\`
654
+ 3. **优化 ui.nodePositions**(参考 flow-layout.md:主链 x 递增、分支 y 错开)。
655
+ 4. 完成后须能通过 **validate-flow**;本轮结束后系统会自动校验并尝试修复。`;
656
+ }
657
+ return "";
658
+ }
659
+
660
+ /**
661
+ * 为分阶段模式规划单个阶段的步骤。
662
+ */
663
+ async function planSinglePhase(opts) {
664
+ const { phaseIndex, apiProvider, emit } = opts;
665
+ const phase = PHASED_DEFINITIONS[phaseIndex];
666
+ // 创建带 status 字段的 phases 副本,避免修改原始常量
667
+ const buildPhasesWithStatus = (currentIdx) => PHASED_DEFINITIONS.map((p, i) => ({
668
+ ...p,
669
+ status: i < currentIdx ? "done" : i === currentIdx ? "running" : "pending",
670
+ }));
671
+
672
+ if (!phase) {
673
+ return { steps: heuristicPlan(opts.userPrompt), phased: true, phases: buildPhasesWithStatus(phaseIndex), currentPhase: phaseIndex };
674
+ }
675
+
676
+ emit({ type: "status", line: t("planner.phased_planning", { label: phase.label, index: phaseIndex + 1, total: PHASED_DEFINITIONS.length }) });
677
+
678
+ let flowYaml = opts.flowYaml || "";
679
+ if (!flowYaml && opts.flowYamlAbs) {
680
+ try { flowYaml = fs.readFileSync(opts.flowYamlAbs, "utf-8"); } catch { /* ignore */ }
681
+ }
682
+
683
+ let userPromptForPlan = opts.phaseContext?.userPromptOriginal || opts.userPrompt;
684
+ if (opts.phaseRole) {
685
+ userPromptForPlan += `\n\n【用户指定本阶段默认节点角色偏好:${opts.phaseRole}】`;
686
+ }
687
+ const phaseCli = buildPhaseCliGuide(phaseIndex);
688
+
689
+ if (phaseIndex === 0 && (!flowYaml || flowYaml.trim().length < 50)) {
690
+ const steps = [{
691
+ type: "agent",
692
+ complexity: "complex",
693
+ description: `${phase.label}:节点类型与框架 + ${COMPOSER_NODE_SPEC_FILENAME}`,
694
+ prompt: userPromptForPlan + phaseCli,
695
+ }];
696
+ return { steps, phased: true, phases: buildPhasesWithStatus(phaseIndex), currentPhase: phaseIndex };
697
+ }
698
+
699
+ if (!apiProvider) {
700
+ emit({ type: "status", line: t("planner.phased_cli_exec", { label: phase.label }) });
701
+ const complexity = phaseIndex === 0 ? "complex" : phaseIndex === 1 ? "simple" : "complex";
702
+ const steps = [{
703
+ type: "agent",
704
+ complexity,
705
+ description: `${phase.label}:${phase.description}`,
706
+ prompt: `${userPromptForPlan}${phaseCli}\n\n当前阶段:${phase.label} — ${phase.description}`,
707
+ }];
708
+ return { steps, phased: true, phases: buildPhasesWithStatus(phaseIndex), currentPhase: phaseIndex };
709
+ }
710
+
711
+ const systemPrompt = buildPhasedSystemPrompt(phase.name, opts.intents);
712
+ const userMessage = buildPhasedUserMessage(userPromptForPlan, flowYaml, opts.flowYamlAbs, phase.name, phaseIndex, opts.thread);
713
+
714
+ emit({ type: "ai-log", tag: "planner-system", text: systemPrompt, meta: { provider: apiProvider.provider, model: apiProvider.model, mode: "phased", phaseName: phase.name, phaseIndex } });
715
+ emit({ type: "ai-log", tag: "planner-user", text: userMessage, meta: { flowYamlAbs: opts.flowYamlAbs || null, phaseName: phase.name, phaseIndex } });
716
+
717
+ try {
718
+ const raw = await callPlannerApi(systemPrompt, userMessage, apiProvider);
719
+ emit({ type: "ai-log", tag: "planner-response", text: String(raw || ""), meta: { provider: apiProvider.provider, model: apiProvider.model, phaseName: phase.name, phaseIndex } });
720
+ const steps = parseStepsJson(raw);
721
+ if (steps && steps.length > 0) {
722
+ return { steps, phased: true, phases: buildPhasesWithStatus(phaseIndex), currentPhase: phaseIndex };
723
+ }
724
+ emit({ type: "status", line: t("planner.phased_format_error") });
725
+ } catch (e) {
726
+ emit({ type: "status", line: t("planner.phased_planning_failed", { message: e.message }) });
727
+ }
728
+
729
+ const complexity = phaseIndex === 0 ? "complex" : phaseIndex === 1 ? "simple" : "complex";
730
+ const steps = [{
731
+ type: "agent",
732
+ complexity,
733
+ description: `${phase.label}:${phase.description}`,
734
+ prompt: `${userPromptForPlan}${phaseCli}\n\n当前阶段:${phase.label} — ${phase.description}`,
735
+ }];
736
+ return { steps, phased: true, phases: buildPhasesWithStatus(phaseIndex), currentPhase: phaseIndex };
737
+ }
738
+
739
+ /**
740
+ * 检查是否有可用的 API key 用于规划。
741
+ */
742
+ export function hasPlannerApiAvailable() {
743
+ return Boolean(process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY);
744
+ }
745
+
746
+ /**
747
+ * 检查用户请求是否适合分阶段生成。
748
+ */
749
+ export { shouldUsePhased, classifyComplexity, PHASED_DEFINITIONS };