@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.
- package/bin/lib/composer-skill-router.mjs +117 -0
- package/bin/lib/ui-server.mjs +45 -55
- package/builtin/web-ui/dist/assets/index-BeUBxIj1.js +190 -0
- package/builtin/web-ui/dist/assets/{index-CFuFD_86.css → index-BzhdjOzb.css} +1 -1
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-C3PT7ICx.js +0 -190
|
@@ -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) {
|
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -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
|
-
"-
|
|
319
|
-
|
|
320
|
-
"",
|
|
321
|
-
"
|
|
322
|
-
"
|
|
323
|
-
"-
|
|
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
|
-
//
|
|
1769
|
+
// 多步分阶段仍需要技能上下文;普通 Composer 请求直接交给 agent + skills 自行判断。
|
|
1789
1770
|
const multiStepIntents = detectIntents(prompt);
|
|
1790
|
-
const
|
|
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
|
|
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
|
-
|
|
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 (_) {}
|