@fieldwangai/agentflow 0.1.35 → 0.1.36
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/auth.mjs +58 -0
- package/bin/lib/catalog-flows.mjs +3 -1
- package/bin/lib/composer-agent.mjs +2 -0
- package/bin/lib/git-worktree.mjs +57 -8
- package/bin/lib/locales/en.json +4 -0
- package/bin/lib/locales/zh.json +4 -0
- package/bin/lib/marketplace.mjs +21 -4
- package/bin/lib/runtime-context.mjs +22 -4
- package/bin/lib/skill-registry.mjs +3 -1
- package/bin/lib/ui-server.mjs +300 -23
- package/bin/pipeline/pre-process-node.mjs +4 -0
- package/builtin/nodes/agent_subAgent.md +4 -1
- package/builtin/nodes/display_chart.md +31 -0
- package/builtin/nodes/tool_git_worktree_load.md +10 -0
- package/builtin/nodes/tool_nodejs.md +3 -0
- package/builtin/web-ui/dist/assets/index-7-343AUn.js +214 -0
- package/builtin/web-ui/dist/assets/index-CPsrRISH.css +1 -0
- package/builtin/web-ui/dist/assets/index-DgQRkS4v.js +61 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-BzmhleR9.css +0 -1
- package/builtin/web-ui/dist/assets/index-DEeZI5V6.js +0 -214
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 本地 HTTP:静态 UI + /api/flows(GET/POST/HEAD)、/api/flows/import(POST multipart 导入 .yaml/.zip)、/api/flow/archive(POST)、/api/flow/delete(POST 永久删除)、/api/model-lists、/api/ui-context、/api/pipeline-recent-runs、/api/run-node-statuses(GET 某次 run 各节点磁盘状态)、/api/workspace-tree(GET 工作区目录树)、/api/nodes、/api/flow(GET/POST)、
|
|
3
|
-
* /api/flow-editor-sync(POST 通知画布刷新)、/api/flow-editor-sync-events(GET SSE)、/api/flow/run(POST NDJSON 流式执行 agentflow apply --machine-readable)、/api/flow/run/stop(POST
|
|
3
|
+
* /api/flow-editor-sync(POST 通知画布刷新)、/api/flow-editor-sync-events(GET SSE)、/api/flow/run(POST NDJSON 流式执行 agentflow apply --machine-readable)、/api/flow/run/stop(POST 终止运行)、/api/workspace/run/stop(POST 终止 Workspace 临时运行)、
|
|
4
4
|
* /api/composer-agent(POST NDJSON;有 flow 时结束后 validate-flow,失败则自动 agent 修复至多 5 次)、
|
|
5
5
|
* /api/agentflow-config(GET/POST 读写 ~/agentflow/config.json 的 opencodeProvider;POST 后执行 update-model-lists)、/api/update-model-lists(POST 可选 JSON body.opencodeProvider 覆盖本次拉取用的 Provider,未保存 config 也可用);
|
|
6
6
|
* listen 后后台 updateModelLists
|
|
@@ -89,8 +89,10 @@ import {
|
|
|
89
89
|
buildClearSessionCookie,
|
|
90
90
|
buildSessionCookie,
|
|
91
91
|
getAuthUserFromRequest,
|
|
92
|
+
isAuthUserAllowed,
|
|
92
93
|
loginOrCreateUser,
|
|
93
94
|
logoutRequest,
|
|
95
|
+
readUserAllowlist,
|
|
94
96
|
} from "./auth.mjs";
|
|
95
97
|
import { readUserEnvObject, readUserEnvRows, writeUserEnvRows } from "./user-env.mjs";
|
|
96
98
|
|
|
@@ -415,6 +417,21 @@ function omitObjectKeys(obj, keys) {
|
|
|
415
417
|
return out;
|
|
416
418
|
}
|
|
417
419
|
|
|
420
|
+
function privateKeyMetadataFromConfig(configValue = {}) {
|
|
421
|
+
const meta = configValue?.__agentflowPrivateKeys;
|
|
422
|
+
const env = Array.isArray(meta?.env) ? meta.env.map((key) => String(key || "").trim()).filter(Boolean) : [];
|
|
423
|
+
const headers = Array.isArray(meta?.headers) ? meta.headers.map((key) => String(key || "").trim()).filter(Boolean) : [];
|
|
424
|
+
return { env: Array.from(new Set(env)), headers: Array.from(new Set(headers)) };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function withPrivatePlaceholders(obj, keys) {
|
|
428
|
+
const out = { ...(obj && typeof obj === "object" && !Array.isArray(obj) ? obj : {}) };
|
|
429
|
+
for (const key of keys) {
|
|
430
|
+
if (key && !Object.prototype.hasOwnProperty.call(out, key)) out[key] = "";
|
|
431
|
+
}
|
|
432
|
+
return out;
|
|
433
|
+
}
|
|
434
|
+
|
|
418
435
|
function normalizeMcpServerConfig(value) {
|
|
419
436
|
const raw = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
420
437
|
const next = {};
|
|
@@ -459,10 +476,13 @@ function readCursorMcpServers(userCtx = {}) {
|
|
|
459
476
|
const privateValue = privateConfig.servers?.[name] && typeof privateConfig.servers[name] === "object" ? privateConfig.servers[name] : {};
|
|
460
477
|
const privateEnv = privateValue.env && typeof privateValue.env === "object" && !Array.isArray(privateValue.env) ? privateValue.env : {};
|
|
461
478
|
const privateHeaders = privateValue.headers && typeof privateValue.headers === "object" && !Array.isArray(privateValue.headers) ? privateValue.headers : {};
|
|
479
|
+
const privateMeta = privateKeyMetadataFromConfig(publicValue);
|
|
480
|
+
const privateEnvKeys = Array.from(new Set([...privateMeta.env, ...Object.keys(privateEnv)]));
|
|
481
|
+
const privateHeaderKeys = Array.from(new Set([...privateMeta.headers, ...Object.keys(privateHeaders)]));
|
|
462
482
|
const configValue = {
|
|
463
483
|
...publicValue,
|
|
464
|
-
env: { ...(publicValue.env || {}), ...privateEnv },
|
|
465
|
-
headers: { ...(publicValue.headers || {}), ...privateHeaders },
|
|
484
|
+
env: { ...withPrivatePlaceholders(publicValue.env || {}, privateEnvKeys), ...privateEnv },
|
|
485
|
+
headers: { ...withPrivatePlaceholders(publicValue.headers || {}, privateHeaderKeys), ...privateHeaders },
|
|
466
486
|
};
|
|
467
487
|
return {
|
|
468
488
|
name,
|
|
@@ -474,8 +494,8 @@ function readCursorMcpServers(userCtx = {}) {
|
|
|
474
494
|
headers: configValue.headers && typeof configValue.headers === "object" ? configValue.headers : {},
|
|
475
495
|
description: typeof configValue.description === "string" ? configValue.description : "",
|
|
476
496
|
raw: configValue,
|
|
477
|
-
privateEnvKeys
|
|
478
|
-
privateHeaderKeys
|
|
497
|
+
privateEnvKeys,
|
|
498
|
+
privateHeaderKeys,
|
|
479
499
|
};
|
|
480
500
|
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
481
501
|
return { path: cursorMcpConfigPath(), servers };
|
|
@@ -496,6 +516,16 @@ function writeCursorMcpServer(payload = {}, userCtx = {}) {
|
|
|
496
516
|
env: omitObjectKeys(server.env || {}, privateEnvKeys),
|
|
497
517
|
headers: omitObjectKeys(server.headers || {}, privateHeaderKeys),
|
|
498
518
|
};
|
|
519
|
+
publicServer.env = withPrivatePlaceholders(publicServer.env, privateEnvKeys);
|
|
520
|
+
publicServer.headers = withPrivatePlaceholders(publicServer.headers, privateHeaderKeys);
|
|
521
|
+
if (privateEnvKeys.size || privateHeaderKeys.size) {
|
|
522
|
+
publicServer.__agentflowPrivateKeys = {
|
|
523
|
+
...(privateEnvKeys.size ? { env: Array.from(privateEnvKeys) } : {}),
|
|
524
|
+
...(privateHeaderKeys.size ? { headers: Array.from(privateHeaderKeys) } : {}),
|
|
525
|
+
};
|
|
526
|
+
} else {
|
|
527
|
+
delete publicServer.__agentflowPrivateKeys;
|
|
528
|
+
}
|
|
499
529
|
if (!Object.keys(publicServer.env).length) delete publicServer.env;
|
|
500
530
|
if (!Object.keys(publicServer.headers).length) delete publicServer.headers;
|
|
501
531
|
const p = cursorMcpConfigPath();
|
|
@@ -1125,6 +1155,21 @@ function resolveWorkspaceScopeRoot(workspaceRoot, params = {}, opts = {}) {
|
|
|
1125
1155
|
return { root: path.resolve(result.path), flowId, flowSource, archived };
|
|
1126
1156
|
}
|
|
1127
1157
|
|
|
1158
|
+
function workspaceSearchGuardrailsBlock() {
|
|
1159
|
+
return [
|
|
1160
|
+
"## 检索约束",
|
|
1161
|
+
"",
|
|
1162
|
+
"默认不要读取、搜索或 Glob 历史运行产物;除非用户明确要求分析历史 run/log,否则必须排除:",
|
|
1163
|
+
"- `**/runBuild/**`",
|
|
1164
|
+
"- `**/logs/**`",
|
|
1165
|
+
"- `.workspace/agentflow/**/runBuild/**`",
|
|
1166
|
+
"- `~/agentflow/runBuild/**`",
|
|
1167
|
+
"- `node_modules/**`、`dist/**` 等依赖或构建产物",
|
|
1168
|
+
"",
|
|
1169
|
+
"使用 grep/rg/find/Glob 等工具时,应把上述路径作为 exclude/glob ignore;不要从历史 runBuild/logs 中推断业务事实、指标资产或 skill 文档。",
|
|
1170
|
+
].join("\n");
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1128
1173
|
function buildWorkspaceGeneratePrompt(payload) {
|
|
1129
1174
|
const userPrompt = String(payload?.prompt || "").trim();
|
|
1130
1175
|
const outputKind = String(payload?.outputKind || payload?.kind || "markdown").trim().toLowerCase();
|
|
@@ -1175,6 +1220,7 @@ function buildWorkspaceGeneratePrompt(payload) {
|
|
|
1175
1220
|
allowFlowYaml
|
|
1176
1221
|
? "用户已允许你考虑正式 flow.yaml;如需修改仍必须明确说明影响。"
|
|
1177
1222
|
: "默认不要修改正式 flow.yaml;优先在 workspace 文件、workspace.graph.json 或回复内容中完成任务。",
|
|
1223
|
+
workspaceSearchGuardrailsBlock(),
|
|
1178
1224
|
workspaceGraph ? `\n## 当前 workspace graph\n\n${JSON.stringify(workspaceGraph, null, 2)}` : "",
|
|
1179
1225
|
selectedNodeIds.length > 0 ? `\n## 当前用户选中的 workspace 节点\n\n${selectedNodeIds.map((id) => `- ${id}`).join("\n")}` : "",
|
|
1180
1226
|
skillsBlock ? `\n## Selected Skills\n\n${skillsBlock}` : "",
|
|
@@ -1281,6 +1327,7 @@ function workspaceDisplayKind(definitionId) {
|
|
|
1281
1327
|
if (id === "display_ascii") return "ascii";
|
|
1282
1328
|
if (id === "display_html") return "html";
|
|
1283
1329
|
if (id === "display_image") return "image";
|
|
1330
|
+
if (id === "display_chart") return "chart";
|
|
1284
1331
|
return "";
|
|
1285
1332
|
}
|
|
1286
1333
|
|
|
@@ -1344,6 +1391,9 @@ function workspaceDownstreamDisplayRequirements(graph, nodeId) {
|
|
|
1344
1391
|
if (kinds.has("image")) {
|
|
1345
1392
|
rules.push("- 下游连接了图片展示节点:输出可作为 img src 使用的图片地址、data URL 或 base64 data URL;不要输出 Markdown 图片语法或解释文字。");
|
|
1346
1393
|
}
|
|
1394
|
+
if (kinds.has("chart")) {
|
|
1395
|
+
rules.push('- 下游连接了 Chart 展示节点:只输出 ChartSpec JSON 对象,不要 Markdown 代码围栏,不要解释文字。格式必须包含 `"type":"chart"`、`"version":"1.0"`、`"renderer":"echarts"`、`"option"`;`option.series[].type` 只使用 line/bar/pie/scatter/radar/heatmap/tree/treemap/sunburst/sankey/graph/gauge/funnel;不要输出 HTML、script、iframe 或 JS 函数。');
|
|
1396
|
+
}
|
|
1347
1397
|
return [
|
|
1348
1398
|
"## 下游输出要求",
|
|
1349
1399
|
"",
|
|
@@ -1434,7 +1484,7 @@ function workspaceTargetSlotForEdge(graph, edge) {
|
|
|
1434
1484
|
function isWorkspaceSemanticInputSlot(slot) {
|
|
1435
1485
|
const name = String(slot?.name || "");
|
|
1436
1486
|
const type = String(slot?.type || "");
|
|
1437
|
-
return type === "node" || name === "prev" || name === "next" || name === "skillsContext" || name === "workspaceContext" || name === "gitContext";
|
|
1487
|
+
return type === "node" || name === "prev" || name === "next" || name === "skillsContext" || name === "mcpContext" || name === "workspaceContext" || name === "gitContext";
|
|
1438
1488
|
}
|
|
1439
1489
|
|
|
1440
1490
|
function workspaceTaskUpstreamText(graph, nodeId, outputs) {
|
|
@@ -1470,6 +1520,14 @@ function selectedSkillKeysFromInstance(instance) {
|
|
|
1470
1520
|
return parseWorkspaceSkillKeys(workspaceSlotValue(slot) || "");
|
|
1471
1521
|
}
|
|
1472
1522
|
|
|
1523
|
+
function selectedMcpServerNamesFromInstance(instance) {
|
|
1524
|
+
const bodyNames = parseWorkspaceSkillKeys(instance?.body || "");
|
|
1525
|
+
if (bodyNames.length > 0) return bodyNames;
|
|
1526
|
+
const slots = [...(Array.isArray(instance?.input) ? instance.input : []), ...(Array.isArray(instance?.output) ? instance.output : [])];
|
|
1527
|
+
const slot = slots.find((item) => item?.name === "mcpContext") || slots.find((item) => item?.name === "serverNames");
|
|
1528
|
+
return parseWorkspaceSkillKeys(workspaceSlotValue(slot) || "");
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1473
1531
|
function workspaceUpstreamSkillBlocks(graph, nodeId, outputs) {
|
|
1474
1532
|
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
1475
1533
|
const blocks = edges
|
|
@@ -1486,6 +1544,22 @@ function workspaceUpstreamSkillBlocks(graph, nodeId, outputs) {
|
|
|
1486
1544
|
return Array.from(new Set(blocks)).join("\n\n---\n\n");
|
|
1487
1545
|
}
|
|
1488
1546
|
|
|
1547
|
+
function workspaceUpstreamMcpBlocks(graph, nodeId, outputs) {
|
|
1548
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
1549
|
+
const blocks = edges
|
|
1550
|
+
.filter((edge) => String(edge?.target || "") === String(nodeId))
|
|
1551
|
+
.filter((edge) => {
|
|
1552
|
+
const slot = workspaceTargetSlotForEdge(graph, edge);
|
|
1553
|
+
return String(slot?.name || "") === "mcpContext";
|
|
1554
|
+
})
|
|
1555
|
+
.map((edge) => String(outputs.get(String(edge.source || "")) || ""))
|
|
1556
|
+
.filter((text) => text.includes("MCP") || text.includes("mcp"))
|
|
1557
|
+
.flatMap((text) => text.split(/\n\s*---\s*\n/g))
|
|
1558
|
+
.map((text) => text.trim())
|
|
1559
|
+
.filter(Boolean);
|
|
1560
|
+
return Array.from(new Set(blocks)).join("\n\n---\n\n");
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1489
1563
|
function mergeWorkspaceSkillBlocks(...values) {
|
|
1490
1564
|
const blocks = values
|
|
1491
1565
|
.map((value) => String(value || ""))
|
|
@@ -1518,6 +1592,43 @@ function buildWorkspaceSkillManifestBlock(skills, selectedKeys = []) {
|
|
|
1518
1592
|
].join("\n");
|
|
1519
1593
|
}
|
|
1520
1594
|
|
|
1595
|
+
function buildWorkspaceMcpManifestBlock(results, servers = [], selectedNames = []) {
|
|
1596
|
+
const serverByName = new Map((Array.isArray(servers) ? servers : []).map((server) => [String(server?.name || ""), server]));
|
|
1597
|
+
const normalizedNames = Array.from(new Set((selectedNames || []).map((x) => String(x || "").trim()).filter(Boolean)));
|
|
1598
|
+
const targets = (Array.isArray(results) ? results : []).filter((item) => !normalizedNames.length || normalizedNames.includes(String(item?.name || "")));
|
|
1599
|
+
const rows = [];
|
|
1600
|
+
for (const result of targets) {
|
|
1601
|
+
const name = String(result?.name || "").trim();
|
|
1602
|
+
if (!name) continue;
|
|
1603
|
+
const server = serverByName.get(name) || {};
|
|
1604
|
+
const description = String(server?.description || "").trim();
|
|
1605
|
+
if (!result?.ok) {
|
|
1606
|
+
rows.push(`- MCP server \`${name}\`: unavailable${result?.error ? ` (${String(result.error)})` : ""}`);
|
|
1607
|
+
continue;
|
|
1608
|
+
}
|
|
1609
|
+
rows.push(`- MCP server \`${name}\`${description ? `: ${description}` : ""}`);
|
|
1610
|
+
const tools = Array.isArray(result?.tools) ? result.tools : [];
|
|
1611
|
+
if (!tools.length) {
|
|
1612
|
+
rows.push(" - no tools reported");
|
|
1613
|
+
continue;
|
|
1614
|
+
}
|
|
1615
|
+
for (const tool of tools.slice(0, 80)) {
|
|
1616
|
+
const toolName = String(tool?.name || "").trim();
|
|
1617
|
+
if (!toolName) continue;
|
|
1618
|
+
const toolDescription = String(tool?.description || "").trim();
|
|
1619
|
+
rows.push(` - tool \`${toolName}\`${toolDescription ? `: ${toolDescription}` : ""}`);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
if (!rows.length && !normalizedNames.length) return "";
|
|
1623
|
+
return [
|
|
1624
|
+
"### Workspace MCP Manifest",
|
|
1625
|
+
"",
|
|
1626
|
+
"这些 MCP servers/tools 已在当前 Agent 运行器中可用。需要外部工具能力时,优先使用下列 MCP 工具;不要声称调用了工具,除非实际工具调用成功。",
|
|
1627
|
+
"",
|
|
1628
|
+
...(rows.length ? rows : normalizedNames.map((name) => `- MCP server \`${name}\``)),
|
|
1629
|
+
].join("\n");
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1521
1632
|
function workspaceWriteDisplayContent(instance, content) {
|
|
1522
1633
|
const next = { ...(instance || {}) };
|
|
1523
1634
|
const kind = workspaceDisplayKind(next.definitionId);
|
|
@@ -1552,7 +1663,7 @@ function workspaceUpdateDirectDisplays(graph, sourceId, content) {
|
|
|
1552
1663
|
return updated;
|
|
1553
1664
|
}
|
|
1554
1665
|
|
|
1555
|
-
function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock) {
|
|
1666
|
+
function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock = "") {
|
|
1556
1667
|
const instance = graph.instances[nodeId] || {};
|
|
1557
1668
|
const body = String(instance.body || "").trim();
|
|
1558
1669
|
const label = String(instance.label || nodeId).trim();
|
|
@@ -1560,7 +1671,9 @@ function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock) {
|
|
|
1560
1671
|
return [
|
|
1561
1672
|
"你正在执行 AgentFlow Workspace 画布中的一个临时节点。",
|
|
1562
1673
|
"只输出该节点要传给下游展示/后续节点的正文,不要解释运行过程。",
|
|
1674
|
+
workspaceSearchGuardrailsBlock(),
|
|
1563
1675
|
skillsBlock ? `\n## Available Skills\n\n${skillsBlock}` : "",
|
|
1676
|
+
mcpBlock ? `\n## Available MCP\n\n${mcpBlock}` : "",
|
|
1564
1677
|
upstreamText ? `\n## 上游上下文\n\n${upstreamText}` : "",
|
|
1565
1678
|
downstreamRequirements ? `\n${downstreamRequirements}` : "",
|
|
1566
1679
|
`\n## 当前节点\n\n- id: ${nodeId}\n- label: ${label}\n- definitionId: ${instance.definitionId || ""}`,
|
|
@@ -1572,6 +1685,14 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
1572
1685
|
const graph = normalizeWorkspaceGraphPayload(payload.graph || {});
|
|
1573
1686
|
const runNodeId = String(payload?.runNodeId || "").trim();
|
|
1574
1687
|
const { order, pauseNodeIds } = workspaceRunPlan(graph, runNodeId);
|
|
1688
|
+
const signal = opts.signal || null;
|
|
1689
|
+
const throwIfAborted = () => {
|
|
1690
|
+
if (signal?.aborted) {
|
|
1691
|
+
const err = new Error("Workspace run stopped");
|
|
1692
|
+
err.code = "WORKSPACE_RUN_ABORTED";
|
|
1693
|
+
throw err;
|
|
1694
|
+
}
|
|
1695
|
+
};
|
|
1575
1696
|
const fallbackSelectedSkillKeys = Array.isArray(payload?.selectedSkills)
|
|
1576
1697
|
? payload.selectedSkills.map((x) => String(x || "").trim()).filter(Boolean)
|
|
1577
1698
|
: [];
|
|
@@ -1589,6 +1710,22 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
1589
1710
|
skillsBlockCache.set(cacheKey, block);
|
|
1590
1711
|
return block;
|
|
1591
1712
|
};
|
|
1713
|
+
const mcpBlockCache = new Map();
|
|
1714
|
+
const loadMcpBlockForNames = async (names) => {
|
|
1715
|
+
const normalized = Array.from(new Set((names || []).map((x) => String(x || "").trim()).filter(Boolean)));
|
|
1716
|
+
if (!normalized.length) return "";
|
|
1717
|
+
const cacheKey = normalized.join("\n");
|
|
1718
|
+
if (mcpBlockCache.has(cacheKey)) return mcpBlockCache.get(cacheKey);
|
|
1719
|
+
const { servers } = readCursorMcpServers(userCtx);
|
|
1720
|
+
const results = [];
|
|
1721
|
+
for (const name of normalized) {
|
|
1722
|
+
const checked = await checkCursorMcpServers(name, userCtx);
|
|
1723
|
+
results.push(...(Array.isArray(checked.results) ? checked.results : []));
|
|
1724
|
+
}
|
|
1725
|
+
const block = buildWorkspaceMcpManifestBlock(results, servers, normalized);
|
|
1726
|
+
mcpBlockCache.set(cacheKey, block);
|
|
1727
|
+
return block;
|
|
1728
|
+
};
|
|
1592
1729
|
const outputs = new Map();
|
|
1593
1730
|
const events = [];
|
|
1594
1731
|
const emit = (event) => {
|
|
@@ -1603,6 +1740,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
1603
1740
|
const modelKey = typeof payload?.model === "string" ? payload.model.trim() : "";
|
|
1604
1741
|
|
|
1605
1742
|
for (const nodeId of order) {
|
|
1743
|
+
throwIfAborted();
|
|
1606
1744
|
const instance = graph.instances[nodeId];
|
|
1607
1745
|
if (!instance) continue;
|
|
1608
1746
|
const defId = String(instance.definitionId || "");
|
|
@@ -1633,6 +1771,26 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
1633
1771
|
continue;
|
|
1634
1772
|
}
|
|
1635
1773
|
|
|
1774
|
+
if (defId === "control_load_mcp") {
|
|
1775
|
+
const mcpStartedAt = Date.now();
|
|
1776
|
+
const serverNames = selectedMcpServerNamesFromInstance(instance);
|
|
1777
|
+
const mcpBlock = await loadMcpBlockForNames(serverNames);
|
|
1778
|
+
emitTiming(nodeId, "load-mcp", mcpStartedAt, { serverCount: serverNames.length, charCount: mcpBlock.length });
|
|
1779
|
+
graph.instances[nodeId] = {
|
|
1780
|
+
...instance,
|
|
1781
|
+
output: (Array.isArray(instance.output) ? instance.output : []).map((slot) => (
|
|
1782
|
+
String(slot?.name || "") === "mcpContext" || String(slot?.type || "") === "text"
|
|
1783
|
+
? { ...slot, default: mcpBlock, value: mcpBlock }
|
|
1784
|
+
: slot
|
|
1785
|
+
)),
|
|
1786
|
+
};
|
|
1787
|
+
outputs.set(nodeId, mcpBlock);
|
|
1788
|
+
workspaceUpdateDirectDisplays(graph, nodeId, mcpBlock);
|
|
1789
|
+
emit({ type: "graph", nodeId, graph });
|
|
1790
|
+
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
1791
|
+
continue;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1636
1794
|
if (workspaceDisplayKind(defId)) {
|
|
1637
1795
|
const content = workspaceUpstreamText(graph, nodeId, outputs);
|
|
1638
1796
|
graph.instances[nodeId] = workspaceWriteDisplayContent(instance, content);
|
|
@@ -1768,7 +1926,10 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
1768
1926
|
const rawWorktreePath = workspaceSlotValue(workspaceSlotByName(instance, "worktreePath")).trim();
|
|
1769
1927
|
const worktreePath = rawWorktreePath ? workspaceResolvePath(cwd, rawWorktreePath) : (gitContext?.worktreePath ? path.resolve(gitContext.worktreePath) : "");
|
|
1770
1928
|
const previousCwd = cwd;
|
|
1771
|
-
const
|
|
1929
|
+
const force = ["true", "1", "yes", "on"].includes(workspaceSlotValue(workspaceSlotByName(instance, "force")).trim().toLowerCase());
|
|
1930
|
+
const pruneMissingRaw = workspaceSlotValue(workspaceSlotByName(instance, "pruneMissing")).trim().toLowerCase();
|
|
1931
|
+
const pruneMissing = pruneMissingRaw !== "false";
|
|
1932
|
+
const result = loadGitWorktree({ repoPath, branch, worktreePath, pipelineWorkspace: scopedRoot, force, pruneMissing });
|
|
1772
1933
|
const outGitContext = buildGitContext({
|
|
1773
1934
|
repoPath: result.repoRoot,
|
|
1774
1935
|
worktreePath: result.worktreePath,
|
|
@@ -1873,8 +2034,9 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
1873
2034
|
}
|
|
1874
2035
|
const upstreamSkillBlocks = workspaceUpstreamSkillBlocks(graph, nodeId, outputs);
|
|
1875
2036
|
const promptSkillsBlock = mergeWorkspaceSkillBlocks(upstreamSkillBlocks, upstreamSkillBlocks ? "" : loadSkillsBlockForKeys(fallbackSelectedSkillKeys));
|
|
1876
|
-
const
|
|
1877
|
-
|
|
2037
|
+
const promptMcpBlock = workspaceUpstreamMcpBlocks(graph, nodeId, outputs);
|
|
2038
|
+
const prompt = workspaceNodePrompt(graph, nodeId, upstreamText, promptSkillsBlock, promptMcpBlock);
|
|
2039
|
+
emitTiming(nodeId, "prepare-agent-prompt", prepareStartedAt, { promptChars: prompt.length, upstreamChars: String(upstreamText || "").length, skillsChars: promptSkillsBlock.length, mcpChars: promptMcpBlock.length });
|
|
1878
2040
|
emit({ type: "natural", kind: "prompt", nodeId, text: prompt });
|
|
1879
2041
|
let content = "";
|
|
1880
2042
|
const maxAttempts = 3;
|
|
@@ -1905,14 +2067,21 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
1905
2067
|
emit({ type: "status", nodeId, line: `工具 ${tool || "thinking"}${sub ? ` (${sub})` : ""}` });
|
|
1906
2068
|
},
|
|
1907
2069
|
});
|
|
2070
|
+
if (typeof opts.onActiveChild === "function") opts.onActiveChild(handle.child || null);
|
|
1908
2071
|
emitTiming(nodeId, "spawn-agent", spawnStartedAt, { attempt });
|
|
1909
|
-
|
|
2072
|
+
try {
|
|
2073
|
+
await handle.finished;
|
|
2074
|
+
} finally {
|
|
2075
|
+
if (typeof opts.onActiveChild === "function") opts.onActiveChild(null);
|
|
2076
|
+
}
|
|
2077
|
+
throwIfAborted();
|
|
1910
2078
|
content = attemptContent.trim();
|
|
1911
2079
|
break;
|
|
1912
2080
|
} catch (e) {
|
|
2081
|
+
if (signal?.aborted || e?.code === "WORKSPACE_RUN_ABORTED") throwIfAborted();
|
|
1913
2082
|
if (attempt < maxAttempts && isTransientAgentNetworkError(e)) {
|
|
1914
2083
|
emit({ type: "status", nodeId, line: `Workspace node retry ${attempt + 1}/${maxAttempts} after network error` });
|
|
1915
|
-
await sleepMs(Math.min(1500 * attempt, 5000));
|
|
2084
|
+
await sleepMs(Math.min(1500 * attempt, 5000), signal);
|
|
1916
2085
|
continue;
|
|
1917
2086
|
}
|
|
1918
2087
|
throw e;
|
|
@@ -1930,6 +2099,10 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
1930
2099
|
return { graph, events, order, pauseNodeIds };
|
|
1931
2100
|
}
|
|
1932
2101
|
|
|
2102
|
+
function isWorkspaceRunAbortError(err) {
|
|
2103
|
+
return err?.code === "WORKSPACE_RUN_ABORTED" || /Workspace run stopped/i.test(String(err?.message || ""));
|
|
2104
|
+
}
|
|
2105
|
+
|
|
1933
2106
|
function isTransientAgentNetworkError(err) {
|
|
1934
2107
|
const text = [
|
|
1935
2108
|
err?.message,
|
|
@@ -1946,8 +2119,17 @@ function isTransientAgentNetworkError(err) {
|
|
|
1946
2119
|
/socket hang up/i.test(text);
|
|
1947
2120
|
}
|
|
1948
2121
|
|
|
1949
|
-
function sleepMs(ms) {
|
|
1950
|
-
|
|
2122
|
+
function sleepMs(ms, signal = null) {
|
|
2123
|
+
if (signal?.aborted) return Promise.resolve();
|
|
2124
|
+
return new Promise((resolve) => {
|
|
2125
|
+
const timer = setTimeout(resolve, ms);
|
|
2126
|
+
if (signal) {
|
|
2127
|
+
signal.addEventListener("abort", () => {
|
|
2128
|
+
clearTimeout(timer);
|
|
2129
|
+
resolve();
|
|
2130
|
+
}, { once: true });
|
|
2131
|
+
}
|
|
2132
|
+
});
|
|
1951
2133
|
}
|
|
1952
2134
|
|
|
1953
2135
|
/** ZIP 本地头:PK\x03\x04 / \x05\x06 / \x07\x08 */
|
|
@@ -2052,6 +2234,12 @@ function broadcastFlowEditorSync(flowId, flowSource, flowArchived = false, userI
|
|
|
2052
2234
|
|
|
2053
2235
|
/** 正在执行的 flow run(flowId → { child, runUuid });同一 flow 只允许一个 run */
|
|
2054
2236
|
const activeFlowRuns = new Map();
|
|
2237
|
+
/** 正在执行的 Workspace 临时 run(flowId → { controller, child });同一 flow 只允许一个 run */
|
|
2238
|
+
const activeWorkspaceRuns = new Map();
|
|
2239
|
+
|
|
2240
|
+
function workspaceRunKey(userCtx, flowSource, flowId) {
|
|
2241
|
+
return `${userCtx?.userId || ""}:${flowSource || "user"}:${flowId}`;
|
|
2242
|
+
}
|
|
2055
2243
|
|
|
2056
2244
|
/** Cursor/OpenCode 执行目录统一使用当前 UI 启动 workspace。 */
|
|
2057
2245
|
function composerCliWorkspaceForFlowDir(workspaceRoot, _flowDir) {
|
|
@@ -2186,7 +2374,16 @@ export function startUiServer({
|
|
|
2186
2374
|
|
|
2187
2375
|
if (url.pathname === "/api/auth/me" && req.method === "GET") {
|
|
2188
2376
|
const user = getAuthUserFromRequest(req);
|
|
2189
|
-
|
|
2377
|
+
const allowed = user ? isAuthUserAllowed(user) : true;
|
|
2378
|
+
const allowlist = readUserAllowlist();
|
|
2379
|
+
json(res, 200, {
|
|
2380
|
+
authenticated: Boolean(user && allowed),
|
|
2381
|
+
user: user && allowed ? user : null,
|
|
2382
|
+
setupRequired: authSetupRequired(),
|
|
2383
|
+
allowlistEnabled: allowlist.enabled,
|
|
2384
|
+
forbidden: Boolean(user && !allowed),
|
|
2385
|
+
error: user && !allowed ? "用户不在白名单中,请联系管理员开通访问权限" : "",
|
|
2386
|
+
});
|
|
2190
2387
|
return;
|
|
2191
2388
|
}
|
|
2192
2389
|
|
|
@@ -2200,7 +2397,7 @@ export function startUiServer({
|
|
|
2200
2397
|
}
|
|
2201
2398
|
const result = loginOrCreateUser(payload?.username, payload?.password);
|
|
2202
2399
|
if (!result.ok) {
|
|
2203
|
-
json(res, 401, { error: result.error || "Login failed", setupRequired: authSetupRequired() });
|
|
2400
|
+
json(res, result.forbidden ? 403 : 401, { error: result.error || "Login failed", setupRequired: authSetupRequired() });
|
|
2204
2401
|
return;
|
|
2205
2402
|
}
|
|
2206
2403
|
const body = JSON.stringify({ authenticated: true, user: result.user, setupRequired: false, migration: result.migration || null });
|
|
@@ -2231,6 +2428,10 @@ export function startUiServer({
|
|
|
2231
2428
|
json(res, 401, { error: "Authentication required", setupRequired: authSetupRequired() });
|
|
2232
2429
|
return;
|
|
2233
2430
|
}
|
|
2431
|
+
if (url.pathname.startsWith("/api/") && authUser && !isAuthUserAllowed(authUser)) {
|
|
2432
|
+
json(res, 403, { error: "用户不在白名单中,请联系管理员开通访问权限" });
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2234
2435
|
|
|
2235
2436
|
if (url.pathname === "/api/flows") {
|
|
2236
2437
|
if (req.method === "GET") {
|
|
@@ -2566,6 +2767,34 @@ export function startUiServer({
|
|
|
2566
2767
|
return;
|
|
2567
2768
|
}
|
|
2568
2769
|
const wantsStream = /\bapplication\/x-ndjson\b/i.test(req.headers.accept || "") || payload.stream === true;
|
|
2770
|
+
const flowId = String(payload.flowId || "").trim();
|
|
2771
|
+
if (!flowId) {
|
|
2772
|
+
json(res, 400, { error: "Missing flowId" });
|
|
2773
|
+
return;
|
|
2774
|
+
}
|
|
2775
|
+
const runKey = workspaceRunKey(userCtx, scoped.flowSource || payload.flowSource || "user", flowId);
|
|
2776
|
+
if (activeWorkspaceRuns.has(runKey)) {
|
|
2777
|
+
json(res, 409, { error: "该 Workspace 正在运行" });
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
const controller = new AbortController();
|
|
2781
|
+
const runEntry = {
|
|
2782
|
+
controller,
|
|
2783
|
+
child: null,
|
|
2784
|
+
stopChild() {
|
|
2785
|
+
if (this.child && !this.child.killed) {
|
|
2786
|
+
try { this.child.kill("SIGTERM"); } catch (_) {}
|
|
2787
|
+
}
|
|
2788
|
+
},
|
|
2789
|
+
};
|
|
2790
|
+
activeWorkspaceRuns.set(runKey, runEntry);
|
|
2791
|
+
const setActiveChild = (child) => {
|
|
2792
|
+
runEntry.child = child || null;
|
|
2793
|
+
if (controller.signal.aborted) runEntry.stopChild();
|
|
2794
|
+
};
|
|
2795
|
+
const clearActiveRun = () => {
|
|
2796
|
+
if (activeWorkspaceRuns.get(runKey) === runEntry) activeWorkspaceRuns.delete(runKey);
|
|
2797
|
+
};
|
|
2569
2798
|
if (wantsStream) {
|
|
2570
2799
|
const graphPath = workspaceGraphPath(scoped.root);
|
|
2571
2800
|
res.writeHead(200, {
|
|
@@ -2574,29 +2803,77 @@ export function startUiServer({
|
|
|
2574
2803
|
"X-Accel-Buffering": "no",
|
|
2575
2804
|
});
|
|
2576
2805
|
const writeEvent = (event) => {
|
|
2577
|
-
res.write(JSON.stringify(event) + "\n");
|
|
2806
|
+
try { res.write(JSON.stringify(event) + "\n"); } catch (_) {}
|
|
2578
2807
|
};
|
|
2579
2808
|
try {
|
|
2580
|
-
const result = await runWorkspaceGraph(root, scoped.root, payload, userCtx, {
|
|
2809
|
+
const result = await runWorkspaceGraph(root, scoped.root, payload, userCtx, {
|
|
2810
|
+
onEvent: writeEvent,
|
|
2811
|
+
signal: controller.signal,
|
|
2812
|
+
onActiveChild: setActiveChild,
|
|
2813
|
+
});
|
|
2581
2814
|
fs.writeFileSync(graphPath, JSON.stringify(result.graph, null, 2) + "\n", "utf-8");
|
|
2582
2815
|
writeEvent({ type: "done", ok: true, path: graphPath, graph: result.graph, order: result.order, pauseNodeIds: result.pauseNodeIds || [] });
|
|
2583
2816
|
res.end();
|
|
2584
2817
|
} catch (e) {
|
|
2585
|
-
|
|
2818
|
+
if (isWorkspaceRunAbortError(e) || controller.signal.aborted) {
|
|
2819
|
+
writeEvent({ type: "stopped", ok: false, stopped: true, message: "Workspace run stopped" });
|
|
2820
|
+
} else {
|
|
2821
|
+
writeEvent({ type: "error", error: (e && e.message) || String(e) });
|
|
2822
|
+
}
|
|
2586
2823
|
res.end();
|
|
2824
|
+
} finally {
|
|
2825
|
+
clearActiveRun();
|
|
2587
2826
|
}
|
|
2588
2827
|
return;
|
|
2589
2828
|
}
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2829
|
+
try {
|
|
2830
|
+
const result = await runWorkspaceGraph(root, scoped.root, payload, userCtx, {
|
|
2831
|
+
signal: controller.signal,
|
|
2832
|
+
onActiveChild: setActiveChild,
|
|
2833
|
+
});
|
|
2834
|
+
const graphPath = workspaceGraphPath(scoped.root);
|
|
2835
|
+
fs.writeFileSync(graphPath, JSON.stringify(result.graph, null, 2) + "\n", "utf-8");
|
|
2836
|
+
json(res, 200, { ok: true, path: graphPath, ...result });
|
|
2837
|
+
} catch (e) {
|
|
2838
|
+
if (isWorkspaceRunAbortError(e) || controller.signal.aborted) {
|
|
2839
|
+
json(res, 200, { ok: false, stopped: true, message: "Workspace run stopped" });
|
|
2840
|
+
} else {
|
|
2841
|
+
throw e;
|
|
2842
|
+
}
|
|
2843
|
+
} finally {
|
|
2844
|
+
clearActiveRun();
|
|
2845
|
+
}
|
|
2594
2846
|
} catch (e) {
|
|
2595
2847
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
2596
2848
|
}
|
|
2597
2849
|
return;
|
|
2598
2850
|
}
|
|
2599
2851
|
|
|
2852
|
+
if (req.method === "POST" && url.pathname === "/api/workspace/run/stop") {
|
|
2853
|
+
let payload;
|
|
2854
|
+
try {
|
|
2855
|
+
payload = JSON.parse(await readBody(req));
|
|
2856
|
+
} catch {
|
|
2857
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
2858
|
+
return;
|
|
2859
|
+
}
|
|
2860
|
+
const flowId = typeof payload.flowId === "string" ? payload.flowId.trim() : "";
|
|
2861
|
+
if (!flowId) {
|
|
2862
|
+
json(res, 400, { error: "Missing flowId" });
|
|
2863
|
+
return;
|
|
2864
|
+
}
|
|
2865
|
+
const runKey = workspaceRunKey(userCtx, payload.flowSource || "user", flowId);
|
|
2866
|
+
const entry = activeWorkspaceRuns.get(runKey);
|
|
2867
|
+
if (!entry) {
|
|
2868
|
+
json(res, 404, { error: "该 Workspace 未在运行" });
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2871
|
+
try { entry.controller?.abort(); } catch (_) {}
|
|
2872
|
+
try { entry.stopChild?.(); } catch (_) {}
|
|
2873
|
+
json(res, 200, { ok: true, stopped: true });
|
|
2874
|
+
return;
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2600
2877
|
if (req.method === "GET" && url.pathname === "/api/workspace/file") {
|
|
2601
2878
|
try {
|
|
2602
2879
|
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
@@ -363,11 +363,15 @@ function emitGitWorktreeLoadNode(workspaceRoot, flowName, uuid, instanceId, exec
|
|
|
363
363
|
const branch = String(inputs.branch || "").trim();
|
|
364
364
|
const worktreePath = resolveMaybeWorkspacePath(inputs.worktreePath, workspaceContext, { branch }) ||
|
|
365
365
|
(gitContext?.worktreePath ? path.resolve(gitContext.worktreePath) : "");
|
|
366
|
+
const force = isTruthyInput(inputs.force);
|
|
367
|
+
const pruneMissing = String(inputs.pruneMissing ?? "true").trim().toLowerCase() !== "false";
|
|
366
368
|
const result = loadGitWorktree({
|
|
367
369
|
repoPath,
|
|
368
370
|
branch,
|
|
369
371
|
worktreePath,
|
|
370
372
|
pipelineWorkspace: workspaceContext.pipelineWorkspace || path.resolve(workspaceRoot),
|
|
373
|
+
force,
|
|
374
|
+
pruneMissing,
|
|
371
375
|
});
|
|
372
376
|
const outWorkspaceContext = {
|
|
373
377
|
version: 1,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
# 内置节点:子 Agent
|
|
3
|
-
description: 利用子 Agent 执行任务;可接收 workspaceContext 切换执行工作区,并接收 skillsContext 注入已加载 skills
|
|
3
|
+
description: 利用子 Agent 执行任务;可接收 workspaceContext 切换执行工作区,并接收 skillsContext / mcpContext 注入已加载 skills 与 MCP 工具清单。
|
|
4
4
|
displayName: 子 Agent
|
|
5
5
|
input:
|
|
6
6
|
- type: node
|
|
@@ -12,6 +12,9 @@ input:
|
|
|
12
12
|
- type: text
|
|
13
13
|
name: skillsContext
|
|
14
14
|
default: ""
|
|
15
|
+
- type: text
|
|
16
|
+
name: mcpContext
|
|
17
|
+
default: ""
|
|
15
18
|
output:
|
|
16
19
|
- type: node
|
|
17
20
|
name: next
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Built-in node: Chart Display
|
|
3
|
+
description: Display a JSON ChartSpec with ECharts in workspace canvas; passes the JSON downstream as text
|
|
4
|
+
displayName: Chart Display
|
|
5
|
+
input:
|
|
6
|
+
- type: node
|
|
7
|
+
name: prev
|
|
8
|
+
default: ""
|
|
9
|
+
- type: text
|
|
10
|
+
name: content
|
|
11
|
+
default: ""
|
|
12
|
+
required: true
|
|
13
|
+
showOnNode: true
|
|
14
|
+
- type: file
|
|
15
|
+
name: filePath
|
|
16
|
+
default: ""
|
|
17
|
+
showOnNode: false
|
|
18
|
+
- type: text
|
|
19
|
+
name: workspaceContext
|
|
20
|
+
default: ""
|
|
21
|
+
showOnNode: false
|
|
22
|
+
output:
|
|
23
|
+
- type: text
|
|
24
|
+
name: content
|
|
25
|
+
default: ""
|
|
26
|
+
showOnNode: false
|
|
27
|
+
- type: node
|
|
28
|
+
name: next
|
|
29
|
+
default: ""
|
|
30
|
+
---
|
|
31
|
+
${content}
|
|
@@ -8,6 +8,8 @@ description: |
|
|
|
8
8
|
- `branch` is optional. When empty, AgentFlow creates a detached worktree at the current HEAD.
|
|
9
9
|
- `worktreePath` is optional. When empty, AgentFlow creates a path under `${pipelineWorkspace}/.workspace/agentflow/worktrees`.
|
|
10
10
|
- Existing worktree paths are reused only when they are registered by `git worktree list` for the given repo.
|
|
11
|
+
- `pruneMissing` defaults to true. When Git has a registered worktree whose directory is missing, AgentFlow runs `git worktree prune` before adding it again.
|
|
12
|
+
- `force` defaults to false. When true, AgentFlow passes `--force` to `git worktree add`.
|
|
11
13
|
displayName: Load Worktree
|
|
12
14
|
input:
|
|
13
15
|
- type: node
|
|
@@ -22,6 +24,14 @@ input:
|
|
|
22
24
|
- type: file
|
|
23
25
|
name: worktreePath
|
|
24
26
|
default: ""
|
|
27
|
+
- type: bool
|
|
28
|
+
name: pruneMissing
|
|
29
|
+
default: "true"
|
|
30
|
+
showOnNode: false
|
|
31
|
+
- type: bool
|
|
32
|
+
name: force
|
|
33
|
+
default: "false"
|
|
34
|
+
showOnNode: false
|
|
25
35
|
- type: text
|
|
26
36
|
name: gitContext
|
|
27
37
|
default: ""
|