@fieldwangai/agentflow 0.1.27 → 0.1.28

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.
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import fs from "fs";
15
15
  import path from "path";
16
+ import yaml from "js-yaml";
16
17
 
17
18
  // ─── 意图模式定义 ─────────────────────────────────────────────────────────
18
19
 
@@ -124,6 +125,112 @@ function readFileCached(absPath) {
124
125
  }
125
126
  }
126
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
+ }
162
+ }
163
+
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
+ });
203
+ }
204
+
205
+ export function loadResourcesForSkillKeys(skillKeys, packageRoot, workspaceRoot) {
206
+ if (!Array.isArray(skillKeys) || skillKeys.length === 0) {
207
+ return { skills: [], references: [], skillsHint: "", hasContext: false };
208
+ }
209
+ const wanted = new Set(skillKeys.map((x) => String(x || "").trim()).filter(Boolean));
210
+ if (wanted.size === 0) return { skills: [], references: [], skillsHint: "", hasContext: false };
211
+
212
+ const skills = [];
213
+ for (const item of listComposerSkills(packageRoot, workspaceRoot)) {
214
+ if (!wanted.has(item.key) && !wanted.has(item.name)) continue;
215
+ const parsed = parseSkillFile(item.path);
216
+ if (!parsed) continue;
217
+ skills.push({
218
+ id: item.name,
219
+ content: parsed.body,
220
+ absPath: item.path,
221
+ source: item.source,
222
+ sourceLabel: item.sourceLabel,
223
+ });
224
+ }
225
+
226
+ return {
227
+ skills,
228
+ references: [],
229
+ skillsHint: buildSelectedSkillsHint(skills),
230
+ hasContext: skills.length > 0,
231
+ };
232
+ }
233
+
127
234
  // ─── 意图检测 ─────────────────────────────────────────────────────────────
128
235
 
129
236
  /**
@@ -307,6 +414,16 @@ function buildSkillsHint(intents, skills, references) {
307
414
  return lines.join("\n");
308
415
  }
309
416
 
417
+ function buildSelectedSkillsHint(skills) {
418
+ if (!Array.isArray(skills) || skills.length === 0) return "";
419
+ const lines = ["## 用户选择的 skills"];
420
+ for (const s of skills) {
421
+ lines.push(`- 使用 skill \`${s.id}\`:${s.absPath}`);
422
+ }
423
+ lines.push("如任务与所选 skill 匹配,请先读取对应 SKILL.md 并遵循其说明;如果只是问答,按问题直接回答。");
424
+ return lines.join("\n");
425
+ }
426
+
310
427
  // ─── 辅助 ─────────────────────────────────────────────────────────────────
311
428
 
312
429
  function stripFrontmatter(content) {
@@ -26,11 +26,9 @@ import { updateModelLists } from "./model-lists.mjs";
26
26
  import {
27
27
  startComposerAgent,
28
28
  startComposerMultiStep,
29
- shouldUseMultiStep,
30
29
  runComposerPostFlowValidationAndRepair,
31
30
  buildScriptContentBlockForInstances,
32
31
  } from "./composer-agent.mjs";
33
- import { buildNodeSchemaCompactSection } from "./composer-node-schema.mjs";
34
32
  import { t } from "./i18n.mjs";
35
33
  import {
36
34
  PACKAGE_ROOT,
@@ -42,6 +40,8 @@ import { RUN_INTERRUPTED_FILENAME } from "./recent-runs.mjs";
42
40
  import {
43
41
  detectIntents,
44
42
  loadResourcesForIntents,
43
+ loadResourcesForSkillKeys,
44
+ listComposerSkills,
45
45
  buildSkillInjectionBlock,
46
46
  buildSkillCompactInjectionBlock,
47
47
  } from "./composer-skill-router.mjs";
@@ -277,10 +277,6 @@ function buildComposerPromptWithFlowContext(p) {
277
277
  const flowDirAbs = path.dirname(p.flowYamlAbs);
278
278
  const idsLine =
279
279
  p.instanceIds.length > 0 ? p.instanceIds.map(String).join(", ") : "(无,可能为全局修改或新增节点)";
280
- const syncFs = p.editorSyncFlowSource ?? p.flowSource;
281
- const syncBody = { flowId: p.flowId, flowSource: syncFs };
282
- if (p.flowArchived) syncBody.flowArchived = true;
283
- const syncJsonArg = JSON.stringify(JSON.stringify(syncBody));
284
280
  const builtinExtra =
285
281
  p.flowSource === "builtin" && p.workspaceWriteDirAbs
286
282
  ? [
@@ -289,23 +285,6 @@ function buildComposerPromptWithFlowContext(p) {
289
285
  ]
290
286
  : [];
291
287
 
292
- // 基于用户意图动态注入 skill 和 reference 内容
293
- const intents = detectIntents(p.userPrompt);
294
- const resources = loadResourcesForIntents(intents, PACKAGE_ROOT);
295
- const skillBlock = resources.hasContext
296
- ? buildSkillInjectionBlock(resources.skills, resources.references)
297
- : "";
298
-
299
- // 无意图匹配时使用通用 skill 路径引用作为兜底
300
- const skillPathHints = resources.hasContext
301
- ? []
302
- : [
303
- "- 新增实例与边:遵循 skill `skills/agentflow-flow-add-instances/SKILL.md`(或 `.cursor/skills/.../SKILL.md`)。",
304
- "- 仅改已有实例文案/占位等:遵循 `skills/agentflow-flow-edit-node-fields/SKILL.md`,勿改 definitionId、instanceId、IO 结构与边拓扑。",
305
- ];
306
-
307
- const nodeSchemaSection = buildNodeSchemaCompactSection();
308
-
309
288
  const prefix = [
310
289
  "## AgentFlow Composer 上下文",
311
290
  `- 流水线目录(flowId=${p.flowId}):${flowDirAbs}`,
@@ -314,33 +293,15 @@ function buildComposerPromptWithFlowContext(p) {
314
293
  `- flowSource:${p.flowSource}`,
315
294
  ...builtinExtra,
316
295
  `- 当前关联的节点实例 ID(顺序:画布选中优先,再输入框 @提及):${idsLine}`,
317
- "- 请根据用户需求自行判断:如果是在问问题,只回答;如果是在要求新增、修改、完善或修复流程,请直接修改对应文件。",
318
- "- 一旦修改 flow.yaml、脚本或相关文件,必须按下方方式刷新 Web 画布。",
319
- ...skillPathHints,
320
- "",
321
- "### 节点能力选择",
322
- "**判据**:确定性任务优先 `tool_nodejs`;需要语义理解、生成、判断或多步推理时使用 `agent_subAgent`。",
323
- "- **确定性**:相同输入永远产出相同输出,可用普通代码完整描述(CLI/npm 调用、读写文件、JSON/路径转换、调现成 API 解析固定格式、跑脚手架等)。",
324
- "- **非确定性**:需要语义理解或创造(代码翻译/生成、源码/文本解析改写、多步推理决策、创意写作)。",
325
- "- 分支/循环使用 `control_toBool` / `control_agent_toBool` + `control_if` + `control_anyOne` 组合。",
326
- "- 常量输入使用 `provide_str` / `provide_file`;读取环境变量使用 `tool_get_env`;终端展示使用 `tool_print`。",
327
- "",
328
- nodeSchemaSection,
329
- "",
330
- "### tool_nodejs 的 script 与 body 关键区分",
331
- "- **`script` 字段**:实际执行的命令代码,流水线直接 spawn 执行;**tool_nodejs 必须写 script**",
332
- "- **`body` 字段**:纯文档注释,有 script 时完全不执行;**禁止在 body 写期望执行的逻辑**",
333
- "- 如果无法写出完整可执行的 script(需要 AI 理解/判断),**必须改用 agent_subAgent**,不要用 tool_nodejs",
334
- "- script 支持多行(YAML `|`)和管道,可写复杂的 curl + node 组合",
335
- "- **禁止**:tool_nodejs 只有 body 没有 script(body 中的自然语言不会被执行,节点会失败)",
336
- "",
337
- // 动态注入的 skill 和 reference 内容
338
- ...(skillBlock ? [skillBlock, ""] : []),
339
- "- **保存 flow.yaml 后必须刷新 Web 画布**:遵循 `skills/agentflow-flow-sync-ui/SKILL.md`;在终端执行(将 JSON 与上方 flowId、flowSource" +
340
- (p.flowArchived ? "、flowArchived" : "") +
341
- " 保持一致):",
342
- ` curl -sS -X POST http://127.0.0.1:${p.uiPort}/api/flow-editor-sync -H 'Content-Type: application/json' -d ${syncJsonArg}`,
296
+ "- 像普通 agent 请求一样处理用户说明:可能只是问问题,也可能要求编辑文件。不要因为存在 flowId 就默认修改 flow.yaml。",
297
+ "- 按需使用当前环境可用的 skills;如果用户点名某个 skill,遵循该 skill 的 SKILL.md。",
298
+ "- 如果你判断需要编辑 AgentFlow 流程,可按需读取这些本地 skills:",
299
+ " - `skills/agentflow-flow-add-instances/SKILL.md`:新增实例、边和布局",
300
+ " - `skills/agentflow-flow-edit-node-fields/SKILL.md`:只改已有节点字段",
301
+ " - `skills/agentflow-flow-sync-ui/SKILL.md`:保存 flow.yaml 后刷新画布",
302
+ "- 如果只是回答问题,不要修改文件。",
343
303
  "",
304
+ ...(p.selectedSkillBlock ? [p.selectedSkillBlock, ""] : []),
344
305
  ...(p.thread && p.thread.length > 0
345
306
  ? [formatThreadHistory(p.thread), ""]
346
307
  : []),
@@ -352,6 +313,15 @@ function buildComposerPromptWithFlowContext(p) {
352
313
  return prefix;
353
314
  }
354
315
 
316
+ function flowYamlChangedSince(flowYamlAbs, beforeText) {
317
+ if (!flowYamlAbs || beforeText == null) return false;
318
+ try {
319
+ return fs.readFileSync(flowYamlAbs, "utf-8") !== beforeText;
320
+ } catch {
321
+ return false;
322
+ }
323
+ }
324
+
355
325
  function normalizeContextInstanceIds(raw) {
356
326
  if (raw == null) return [];
357
327
  if (!Array.isArray(raw)) return [];
@@ -1709,6 +1679,11 @@ finishedAt: "${new Date().toISOString()}"
1709
1679
  return;
1710
1680
  }
1711
1681
 
1682
+ if (req.method === "GET" && url.pathname === "/api/skills") {
1683
+ json(res, 200, { skills: listComposerSkills(PACKAGE_ROOT, root) });
1684
+ return;
1685
+ }
1686
+
1712
1687
  if (req.method === "POST" && url.pathname === "/api/composer-agent") {
1713
1688
  let payload;
1714
1689
  try {
@@ -1728,6 +1703,9 @@ finishedAt: "${new Date().toISOString()}"
1728
1703
  json(res, 400, { error: "Invalid model" });
1729
1704
  return;
1730
1705
  }
1706
+ const selectedSkillKeys = Array.isArray(payload.selectedSkills)
1707
+ ? payload.selectedSkills.map((x) => String(x || "").trim()).filter(Boolean).slice(0, 20)
1708
+ : [];
1731
1709
 
1732
1710
  const flowIdRaw = payload.flowId;
1733
1711
  const flowSourceRaw = payload.flowSource;
@@ -1750,6 +1728,8 @@ finishedAt: "${new Date().toISOString()}"
1750
1728
  let flowSource = null;
1751
1729
  let instanceIds = [];
1752
1730
  let flowContextForMultiStep = null;
1731
+ let flowYamlBefore = null;
1732
+ const hasPhaseContext = payload.phaseContext && typeof payload.phaseContext === "object" && typeof payload.phaseContext.phaseIndex === "number";
1753
1733
 
1754
1734
  if (hasFlowId) {
1755
1735
  flowId = String(flowIdRaw).trim();
@@ -1765,6 +1745,7 @@ finishedAt: "${new Date().toISOString()}"
1765
1745
  return;
1766
1746
  }
1767
1747
  flowYamlAbs = yamlRes.path;
1748
+ try { flowYamlBefore = fs.readFileSync(flowYamlAbs, "utf-8"); } catch { flowYamlBefore = null; }
1768
1749
  let workspaceWriteDirAbs;
1769
1750
  let editorSyncFlowSource = flowSource;
1770
1751
  let flowDirForCli = path.dirname(flowYamlAbs);
@@ -1785,10 +1766,18 @@ finishedAt: "${new Date().toISOString()}"
1785
1766
  if (flowArchived) syncBody.flowArchived = true;
1786
1767
  const syncJsonArg = JSON.stringify(JSON.stringify(syncBody));
1787
1768
 
1788
- // 基于用户意图动态加载 skill 上下文
1769
+ // 多步分阶段仍需要技能上下文;普通 Composer 请求直接交给 agent + skills 自行判断。
1789
1770
  const multiStepIntents = detectIntents(prompt);
1790
- const multiStepResources = loadResourcesForIntents(multiStepIntents, PACKAGE_ROOT);
1771
+ const selectedSkillResources = selectedSkillKeys.length > 0
1772
+ ? loadResourcesForSkillKeys(selectedSkillKeys, PACKAGE_ROOT, root)
1773
+ : { skills: [], references: [], skillsHint: "", hasContext: false };
1774
+ const multiStepResources = selectedSkillResources.hasContext
1775
+ ? selectedSkillResources
1776
+ : loadResourcesForIntents(multiStepIntents, PACKAGE_ROOT);
1791
1777
  const flowPipelineDir = flowYamlAbs ? path.dirname(flowYamlAbs) : "";
1778
+ const selectedSkillBlock = selectedSkillResources.hasContext
1779
+ ? buildSkillInjectionBlock(selectedSkillResources.skills, selectedSkillResources.references)
1780
+ : "";
1792
1781
 
1793
1782
  flowContextForMultiStep = {
1794
1783
  flowYamlAbs,
@@ -1818,6 +1807,7 @@ finishedAt: "${new Date().toISOString()}"
1818
1807
  flowArchived,
1819
1808
  thread,
1820
1809
  scriptContentBlock,
1810
+ selectedSkillBlock,
1821
1811
  });
1822
1812
  cliWorkspace = composerCliWorkspaceForFlowDir(root, flowDirForCli);
1823
1813
  }
@@ -1909,10 +1899,9 @@ finishedAt: "${new Date().toISOString()}"
1909
1899
  onStreamEvent({ type: "status", line: t("composer.analyzing_task") });
1910
1900
  log.debug(`[ui] composer-agent: flowId=${flowId || "(none)"} model=${model || "default"} promptLen=${finalPrompt.length}`);
1911
1901
 
1912
- const hasPhaseContext = payload.phaseContext && typeof payload.phaseContext === "object" && typeof payload.phaseContext.phaseIndex === "number";
1913
1902
  let useMultiStep;
1914
1903
  try {
1915
- useMultiStep = hasPhaseContext || ((await shouldUseMultiStep({ flowYamlAbs, userPrompt: prompt.trim(), cliWorkspace })) && !payload.singleStep);
1904
+ useMultiStep = hasPhaseContext && !payload.singleStep;
1916
1905
  } catch (classifyErr) {
1917
1906
  log.debug(`[ui] composer classify error: ${classifyErr.message}`);
1918
1907
  logComposerEvent(composerLogPath, "composer-done", {
@@ -2009,7 +1998,8 @@ finishedAt: "${new Date().toISOString()}"
2009
1998
  endSafe();
2010
1999
  return;
2011
2000
  }
2012
- if (flowYamlAbs && flowContextForMultiStep) {
2001
+ const flowYamlChanged = flowYamlChangedSince(flowYamlAbs, flowYamlBefore);
2002
+ if (flowYamlChanged && flowYamlAbs && flowContextForMultiStep) {
2013
2003
  try {
2014
2004
  await runComposerPostFlowValidationAndRepair({
2015
2005
  uiWorkspaceRoot: root,
@@ -2038,7 +2028,7 @@ finishedAt: "${new Date().toISOString()}"
2038
2028
  flowId: flowId || null,
2039
2029
  flowSource: flowSource || null,
2040
2030
  });
2041
- if (flowId && flowSource) {
2031
+ if (flowYamlChanged && flowId && flowSource) {
2042
2032
  broadcastFlowEditorSync(flowId, flowSource, Boolean(payload.flowArchived));
2043
2033
  }
2044
2034
  try { res.write(JSON.stringify({ type: "done" }) + "\n"); } catch (_) {}