@fieldwangai/agentflow 0.1.35 → 0.1.37

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.
@@ -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: Object.keys(privateEnv),
478
- privateHeaderKeys: Object.keys(privateHeaders),
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();
@@ -1162,9 +1207,10 @@ function buildWorkspaceGeneratePrompt(payload) {
1162
1207
  ].join("\n")
1163
1208
  : [
1164
1209
  "你是 AgentFlow Workspace Composer。",
1165
- "优先根据已选择的 Skills 操作 workspace.graph.json,创建或修改 workspace 画布节点、连线与展示节点。",
1166
- "如果用户请求需要项目分析、加载代码、整理流程或生成展示结果,不要只给泛泛回答;应先让 Skills 驱动画布建模,例如创建 Git/工作目录/Load Skills/Agent/Markdown Display 等合适节点。",
1167
- "只有当用户明确只是询问概念或无需画布变更时,才直接输出 Markdown 回复。",
1210
+ "默认以用户当前选择的 workspace 节点作为上下文范围;选中节点不是让你重建整张画布的授权。",
1211
+ "默认不要修改 workspace.graph.json,不要新增/删除/重连画布节点;只有当用户明确要求“更新画布、加节点、改连线、展示成节点、生成流程”时,才编辑 workspace.graph.json。",
1212
+ "如果用户请求生成或恢复文档/文件,可以直接在 workspace 文件系统中完成,最终只输出简短结果:改了什么、路径在哪里、是否需要下一步。",
1213
+ "不要在最终回答中列出过程性步骤,例如“先查看结构”“继续检索”“正在生成”;这些属于执行过程,不属于最终结果。",
1168
1214
  ].join("\n");
1169
1215
  return [
1170
1216
  "你正在 AgentFlow 的 Workspace 工作画布中执行任务。",
@@ -1175,6 +1221,7 @@ function buildWorkspaceGeneratePrompt(payload) {
1175
1221
  allowFlowYaml
1176
1222
  ? "用户已允许你考虑正式 flow.yaml;如需修改仍必须明确说明影响。"
1177
1223
  : "默认不要修改正式 flow.yaml;优先在 workspace 文件、workspace.graph.json 或回复内容中完成任务。",
1224
+ workspaceSearchGuardrailsBlock(),
1178
1225
  workspaceGraph ? `\n## 当前 workspace graph\n\n${JSON.stringify(workspaceGraph, null, 2)}` : "",
1179
1226
  selectedNodeIds.length > 0 ? `\n## 当前用户选中的 workspace 节点\n\n${selectedNodeIds.map((id) => `- ${id}`).join("\n")}` : "",
1180
1227
  skillsBlock ? `\n## Selected Skills\n\n${skillsBlock}` : "",
@@ -1281,6 +1328,7 @@ function workspaceDisplayKind(definitionId) {
1281
1328
  if (id === "display_ascii") return "ascii";
1282
1329
  if (id === "display_html") return "html";
1283
1330
  if (id === "display_image") return "image";
1331
+ if (id === "display_chart") return "chart";
1284
1332
  return "";
1285
1333
  }
1286
1334
 
@@ -1344,6 +1392,9 @@ function workspaceDownstreamDisplayRequirements(graph, nodeId) {
1344
1392
  if (kinds.has("image")) {
1345
1393
  rules.push("- 下游连接了图片展示节点:输出可作为 img src 使用的图片地址、data URL 或 base64 data URL;不要输出 Markdown 图片语法或解释文字。");
1346
1394
  }
1395
+ if (kinds.has("chart")) {
1396
+ 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 函数。');
1397
+ }
1347
1398
  return [
1348
1399
  "## 下游输出要求",
1349
1400
  "",
@@ -1434,7 +1485,7 @@ function workspaceTargetSlotForEdge(graph, edge) {
1434
1485
  function isWorkspaceSemanticInputSlot(slot) {
1435
1486
  const name = String(slot?.name || "");
1436
1487
  const type = String(slot?.type || "");
1437
- return type === "node" || name === "prev" || name === "next" || name === "skillsContext" || name === "workspaceContext" || name === "gitContext";
1488
+ return type === "node" || name === "prev" || name === "next" || name === "skillsContext" || name === "mcpContext" || name === "workspaceContext" || name === "gitContext";
1438
1489
  }
1439
1490
 
1440
1491
  function workspaceTaskUpstreamText(graph, nodeId, outputs) {
@@ -1470,6 +1521,14 @@ function selectedSkillKeysFromInstance(instance) {
1470
1521
  return parseWorkspaceSkillKeys(workspaceSlotValue(slot) || "");
1471
1522
  }
1472
1523
 
1524
+ function selectedMcpServerNamesFromInstance(instance) {
1525
+ const bodyNames = parseWorkspaceSkillKeys(instance?.body || "");
1526
+ if (bodyNames.length > 0) return bodyNames;
1527
+ const slots = [...(Array.isArray(instance?.input) ? instance.input : []), ...(Array.isArray(instance?.output) ? instance.output : [])];
1528
+ const slot = slots.find((item) => item?.name === "mcpContext") || slots.find((item) => item?.name === "serverNames");
1529
+ return parseWorkspaceSkillKeys(workspaceSlotValue(slot) || "");
1530
+ }
1531
+
1473
1532
  function workspaceUpstreamSkillBlocks(graph, nodeId, outputs) {
1474
1533
  const edges = Array.isArray(graph?.edges) ? graph.edges : [];
1475
1534
  const blocks = edges
@@ -1486,6 +1545,22 @@ function workspaceUpstreamSkillBlocks(graph, nodeId, outputs) {
1486
1545
  return Array.from(new Set(blocks)).join("\n\n---\n\n");
1487
1546
  }
1488
1547
 
1548
+ function workspaceUpstreamMcpBlocks(graph, nodeId, outputs) {
1549
+ const edges = Array.isArray(graph?.edges) ? graph.edges : [];
1550
+ const blocks = edges
1551
+ .filter((edge) => String(edge?.target || "") === String(nodeId))
1552
+ .filter((edge) => {
1553
+ const slot = workspaceTargetSlotForEdge(graph, edge);
1554
+ return String(slot?.name || "") === "mcpContext";
1555
+ })
1556
+ .map((edge) => String(outputs.get(String(edge.source || "")) || ""))
1557
+ .filter((text) => text.includes("MCP") || text.includes("mcp"))
1558
+ .flatMap((text) => text.split(/\n\s*---\s*\n/g))
1559
+ .map((text) => text.trim())
1560
+ .filter(Boolean);
1561
+ return Array.from(new Set(blocks)).join("\n\n---\n\n");
1562
+ }
1563
+
1489
1564
  function mergeWorkspaceSkillBlocks(...values) {
1490
1565
  const blocks = values
1491
1566
  .map((value) => String(value || ""))
@@ -1518,6 +1593,43 @@ function buildWorkspaceSkillManifestBlock(skills, selectedKeys = []) {
1518
1593
  ].join("\n");
1519
1594
  }
1520
1595
 
1596
+ function buildWorkspaceMcpManifestBlock(results, servers = [], selectedNames = []) {
1597
+ const serverByName = new Map((Array.isArray(servers) ? servers : []).map((server) => [String(server?.name || ""), server]));
1598
+ const normalizedNames = Array.from(new Set((selectedNames || []).map((x) => String(x || "").trim()).filter(Boolean)));
1599
+ const targets = (Array.isArray(results) ? results : []).filter((item) => !normalizedNames.length || normalizedNames.includes(String(item?.name || "")));
1600
+ const rows = [];
1601
+ for (const result of targets) {
1602
+ const name = String(result?.name || "").trim();
1603
+ if (!name) continue;
1604
+ const server = serverByName.get(name) || {};
1605
+ const description = String(server?.description || "").trim();
1606
+ if (!result?.ok) {
1607
+ rows.push(`- MCP server \`${name}\`: unavailable${result?.error ? ` (${String(result.error)})` : ""}`);
1608
+ continue;
1609
+ }
1610
+ rows.push(`- MCP server \`${name}\`${description ? `: ${description}` : ""}`);
1611
+ const tools = Array.isArray(result?.tools) ? result.tools : [];
1612
+ if (!tools.length) {
1613
+ rows.push(" - no tools reported");
1614
+ continue;
1615
+ }
1616
+ for (const tool of tools.slice(0, 80)) {
1617
+ const toolName = String(tool?.name || "").trim();
1618
+ if (!toolName) continue;
1619
+ const toolDescription = String(tool?.description || "").trim();
1620
+ rows.push(` - tool \`${toolName}\`${toolDescription ? `: ${toolDescription}` : ""}`);
1621
+ }
1622
+ }
1623
+ if (!rows.length && !normalizedNames.length) return "";
1624
+ return [
1625
+ "### Workspace MCP Manifest",
1626
+ "",
1627
+ "这些 MCP servers/tools 已在当前 Agent 运行器中可用。需要外部工具能力时,优先使用下列 MCP 工具;不要声称调用了工具,除非实际工具调用成功。",
1628
+ "",
1629
+ ...(rows.length ? rows : normalizedNames.map((name) => `- MCP server \`${name}\``)),
1630
+ ].join("\n");
1631
+ }
1632
+
1521
1633
  function workspaceWriteDisplayContent(instance, content) {
1522
1634
  const next = { ...(instance || {}) };
1523
1635
  const kind = workspaceDisplayKind(next.definitionId);
@@ -1552,7 +1664,7 @@ function workspaceUpdateDirectDisplays(graph, sourceId, content) {
1552
1664
  return updated;
1553
1665
  }
1554
1666
 
1555
- function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock) {
1667
+ function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock = "") {
1556
1668
  const instance = graph.instances[nodeId] || {};
1557
1669
  const body = String(instance.body || "").trim();
1558
1670
  const label = String(instance.label || nodeId).trim();
@@ -1560,7 +1672,9 @@ function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock) {
1560
1672
  return [
1561
1673
  "你正在执行 AgentFlow Workspace 画布中的一个临时节点。",
1562
1674
  "只输出该节点要传给下游展示/后续节点的正文,不要解释运行过程。",
1675
+ workspaceSearchGuardrailsBlock(),
1563
1676
  skillsBlock ? `\n## Available Skills\n\n${skillsBlock}` : "",
1677
+ mcpBlock ? `\n## Available MCP\n\n${mcpBlock}` : "",
1564
1678
  upstreamText ? `\n## 上游上下文\n\n${upstreamText}` : "",
1565
1679
  downstreamRequirements ? `\n${downstreamRequirements}` : "",
1566
1680
  `\n## 当前节点\n\n- id: ${nodeId}\n- label: ${label}\n- definitionId: ${instance.definitionId || ""}`,
@@ -1572,6 +1686,14 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
1572
1686
  const graph = normalizeWorkspaceGraphPayload(payload.graph || {});
1573
1687
  const runNodeId = String(payload?.runNodeId || "").trim();
1574
1688
  const { order, pauseNodeIds } = workspaceRunPlan(graph, runNodeId);
1689
+ const signal = opts.signal || null;
1690
+ const throwIfAborted = () => {
1691
+ if (signal?.aborted) {
1692
+ const err = new Error("Workspace run stopped");
1693
+ err.code = "WORKSPACE_RUN_ABORTED";
1694
+ throw err;
1695
+ }
1696
+ };
1575
1697
  const fallbackSelectedSkillKeys = Array.isArray(payload?.selectedSkills)
1576
1698
  ? payload.selectedSkills.map((x) => String(x || "").trim()).filter(Boolean)
1577
1699
  : [];
@@ -1589,6 +1711,22 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
1589
1711
  skillsBlockCache.set(cacheKey, block);
1590
1712
  return block;
1591
1713
  };
1714
+ const mcpBlockCache = new Map();
1715
+ const loadMcpBlockForNames = async (names) => {
1716
+ const normalized = Array.from(new Set((names || []).map((x) => String(x || "").trim()).filter(Boolean)));
1717
+ if (!normalized.length) return "";
1718
+ const cacheKey = normalized.join("\n");
1719
+ if (mcpBlockCache.has(cacheKey)) return mcpBlockCache.get(cacheKey);
1720
+ const { servers } = readCursorMcpServers(userCtx);
1721
+ const results = [];
1722
+ for (const name of normalized) {
1723
+ const checked = await checkCursorMcpServers(name, userCtx);
1724
+ results.push(...(Array.isArray(checked.results) ? checked.results : []));
1725
+ }
1726
+ const block = buildWorkspaceMcpManifestBlock(results, servers, normalized);
1727
+ mcpBlockCache.set(cacheKey, block);
1728
+ return block;
1729
+ };
1592
1730
  const outputs = new Map();
1593
1731
  const events = [];
1594
1732
  const emit = (event) => {
@@ -1603,6 +1741,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
1603
1741
  const modelKey = typeof payload?.model === "string" ? payload.model.trim() : "";
1604
1742
 
1605
1743
  for (const nodeId of order) {
1744
+ throwIfAborted();
1606
1745
  const instance = graph.instances[nodeId];
1607
1746
  if (!instance) continue;
1608
1747
  const defId = String(instance.definitionId || "");
@@ -1633,6 +1772,26 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
1633
1772
  continue;
1634
1773
  }
1635
1774
 
1775
+ if (defId === "control_load_mcp") {
1776
+ const mcpStartedAt = Date.now();
1777
+ const serverNames = selectedMcpServerNamesFromInstance(instance);
1778
+ const mcpBlock = await loadMcpBlockForNames(serverNames);
1779
+ emitTiming(nodeId, "load-mcp", mcpStartedAt, { serverCount: serverNames.length, charCount: mcpBlock.length });
1780
+ graph.instances[nodeId] = {
1781
+ ...instance,
1782
+ output: (Array.isArray(instance.output) ? instance.output : []).map((slot) => (
1783
+ String(slot?.name || "") === "mcpContext" || String(slot?.type || "") === "text"
1784
+ ? { ...slot, default: mcpBlock, value: mcpBlock }
1785
+ : slot
1786
+ )),
1787
+ };
1788
+ outputs.set(nodeId, mcpBlock);
1789
+ workspaceUpdateDirectDisplays(graph, nodeId, mcpBlock);
1790
+ emit({ type: "graph", nodeId, graph });
1791
+ emit({ type: "node-done", nodeId, definitionId: defId });
1792
+ continue;
1793
+ }
1794
+
1636
1795
  if (workspaceDisplayKind(defId)) {
1637
1796
  const content = workspaceUpstreamText(graph, nodeId, outputs);
1638
1797
  graph.instances[nodeId] = workspaceWriteDisplayContent(instance, content);
@@ -1768,7 +1927,10 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
1768
1927
  const rawWorktreePath = workspaceSlotValue(workspaceSlotByName(instance, "worktreePath")).trim();
1769
1928
  const worktreePath = rawWorktreePath ? workspaceResolvePath(cwd, rawWorktreePath) : (gitContext?.worktreePath ? path.resolve(gitContext.worktreePath) : "");
1770
1929
  const previousCwd = cwd;
1771
- const result = loadGitWorktree({ repoPath, branch, worktreePath, pipelineWorkspace: scopedRoot });
1930
+ const force = ["true", "1", "yes", "on"].includes(workspaceSlotValue(workspaceSlotByName(instance, "force")).trim().toLowerCase());
1931
+ const pruneMissingRaw = workspaceSlotValue(workspaceSlotByName(instance, "pruneMissing")).trim().toLowerCase();
1932
+ const pruneMissing = pruneMissingRaw !== "false";
1933
+ const result = loadGitWorktree({ repoPath, branch, worktreePath, pipelineWorkspace: scopedRoot, force, pruneMissing });
1772
1934
  const outGitContext = buildGitContext({
1773
1935
  repoPath: result.repoRoot,
1774
1936
  worktreePath: result.worktreePath,
@@ -1873,8 +2035,9 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
1873
2035
  }
1874
2036
  const upstreamSkillBlocks = workspaceUpstreamSkillBlocks(graph, nodeId, outputs);
1875
2037
  const promptSkillsBlock = mergeWorkspaceSkillBlocks(upstreamSkillBlocks, upstreamSkillBlocks ? "" : loadSkillsBlockForKeys(fallbackSelectedSkillKeys));
1876
- const prompt = workspaceNodePrompt(graph, nodeId, upstreamText, promptSkillsBlock);
1877
- emitTiming(nodeId, "prepare-agent-prompt", prepareStartedAt, { promptChars: prompt.length, upstreamChars: String(upstreamText || "").length, skillsChars: promptSkillsBlock.length });
2038
+ const promptMcpBlock = workspaceUpstreamMcpBlocks(graph, nodeId, outputs);
2039
+ const prompt = workspaceNodePrompt(graph, nodeId, upstreamText, promptSkillsBlock, promptMcpBlock);
2040
+ emitTiming(nodeId, "prepare-agent-prompt", prepareStartedAt, { promptChars: prompt.length, upstreamChars: String(upstreamText || "").length, skillsChars: promptSkillsBlock.length, mcpChars: promptMcpBlock.length });
1878
2041
  emit({ type: "natural", kind: "prompt", nodeId, text: prompt });
1879
2042
  let content = "";
1880
2043
  const maxAttempts = 3;
@@ -1905,14 +2068,21 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
1905
2068
  emit({ type: "status", nodeId, line: `工具 ${tool || "thinking"}${sub ? ` (${sub})` : ""}` });
1906
2069
  },
1907
2070
  });
2071
+ if (typeof opts.onActiveChild === "function") opts.onActiveChild(handle.child || null);
1908
2072
  emitTiming(nodeId, "spawn-agent", spawnStartedAt, { attempt });
1909
- await handle.finished;
2073
+ try {
2074
+ await handle.finished;
2075
+ } finally {
2076
+ if (typeof opts.onActiveChild === "function") opts.onActiveChild(null);
2077
+ }
2078
+ throwIfAborted();
1910
2079
  content = attemptContent.trim();
1911
2080
  break;
1912
2081
  } catch (e) {
2082
+ if (signal?.aborted || e?.code === "WORKSPACE_RUN_ABORTED") throwIfAborted();
1913
2083
  if (attempt < maxAttempts && isTransientAgentNetworkError(e)) {
1914
2084
  emit({ type: "status", nodeId, line: `Workspace node retry ${attempt + 1}/${maxAttempts} after network error` });
1915
- await sleepMs(Math.min(1500 * attempt, 5000));
2085
+ await sleepMs(Math.min(1500 * attempt, 5000), signal);
1916
2086
  continue;
1917
2087
  }
1918
2088
  throw e;
@@ -1930,6 +2100,10 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
1930
2100
  return { graph, events, order, pauseNodeIds };
1931
2101
  }
1932
2102
 
2103
+ function isWorkspaceRunAbortError(err) {
2104
+ return err?.code === "WORKSPACE_RUN_ABORTED" || /Workspace run stopped/i.test(String(err?.message || ""));
2105
+ }
2106
+
1933
2107
  function isTransientAgentNetworkError(err) {
1934
2108
  const text = [
1935
2109
  err?.message,
@@ -1946,8 +2120,17 @@ function isTransientAgentNetworkError(err) {
1946
2120
  /socket hang up/i.test(text);
1947
2121
  }
1948
2122
 
1949
- function sleepMs(ms) {
1950
- return new Promise((resolve) => setTimeout(resolve, ms));
2123
+ function sleepMs(ms, signal = null) {
2124
+ if (signal?.aborted) return Promise.resolve();
2125
+ return new Promise((resolve) => {
2126
+ const timer = setTimeout(resolve, ms);
2127
+ if (signal) {
2128
+ signal.addEventListener("abort", () => {
2129
+ clearTimeout(timer);
2130
+ resolve();
2131
+ }, { once: true });
2132
+ }
2133
+ });
1951
2134
  }
1952
2135
 
1953
2136
  /** ZIP 本地头:PK\x03\x04 / \x05\x06 / \x07\x08 */
@@ -2052,6 +2235,12 @@ function broadcastFlowEditorSync(flowId, flowSource, flowArchived = false, userI
2052
2235
 
2053
2236
  /** 正在执行的 flow run(flowId → { child, runUuid });同一 flow 只允许一个 run */
2054
2237
  const activeFlowRuns = new Map();
2238
+ /** 正在执行的 Workspace 临时 run(flowId → { controller, child });同一 flow 只允许一个 run */
2239
+ const activeWorkspaceRuns = new Map();
2240
+
2241
+ function workspaceRunKey(userCtx, flowSource, flowId) {
2242
+ return `${userCtx?.userId || ""}:${flowSource || "user"}:${flowId}`;
2243
+ }
2055
2244
 
2056
2245
  /** Cursor/OpenCode 执行目录统一使用当前 UI 启动 workspace。 */
2057
2246
  function composerCliWorkspaceForFlowDir(workspaceRoot, _flowDir) {
@@ -2186,7 +2375,16 @@ export function startUiServer({
2186
2375
 
2187
2376
  if (url.pathname === "/api/auth/me" && req.method === "GET") {
2188
2377
  const user = getAuthUserFromRequest(req);
2189
- json(res, 200, { authenticated: Boolean(user), user: user || null, setupRequired: authSetupRequired() });
2378
+ const allowed = user ? isAuthUserAllowed(user) : true;
2379
+ const allowlist = readUserAllowlist();
2380
+ json(res, 200, {
2381
+ authenticated: Boolean(user && allowed),
2382
+ user: user && allowed ? user : null,
2383
+ setupRequired: authSetupRequired(),
2384
+ allowlistEnabled: allowlist.enabled,
2385
+ forbidden: Boolean(user && !allowed),
2386
+ error: user && !allowed ? "用户不在白名单中,请联系管理员开通访问权限" : "",
2387
+ });
2190
2388
  return;
2191
2389
  }
2192
2390
 
@@ -2200,7 +2398,7 @@ export function startUiServer({
2200
2398
  }
2201
2399
  const result = loginOrCreateUser(payload?.username, payload?.password);
2202
2400
  if (!result.ok) {
2203
- json(res, 401, { error: result.error || "Login failed", setupRequired: authSetupRequired() });
2401
+ json(res, result.forbidden ? 403 : 401, { error: result.error || "Login failed", setupRequired: authSetupRequired() });
2204
2402
  return;
2205
2403
  }
2206
2404
  const body = JSON.stringify({ authenticated: true, user: result.user, setupRequired: false, migration: result.migration || null });
@@ -2231,6 +2429,10 @@ export function startUiServer({
2231
2429
  json(res, 401, { error: "Authentication required", setupRequired: authSetupRequired() });
2232
2430
  return;
2233
2431
  }
2432
+ if (url.pathname.startsWith("/api/") && authUser && !isAuthUserAllowed(authUser)) {
2433
+ json(res, 403, { error: "用户不在白名单中,请联系管理员开通访问权限" });
2434
+ return;
2435
+ }
2234
2436
 
2235
2437
  if (url.pathname === "/api/flows") {
2236
2438
  if (req.method === "GET") {
@@ -2566,6 +2768,34 @@ export function startUiServer({
2566
2768
  return;
2567
2769
  }
2568
2770
  const wantsStream = /\bapplication\/x-ndjson\b/i.test(req.headers.accept || "") || payload.stream === true;
2771
+ const flowId = String(payload.flowId || "").trim();
2772
+ if (!flowId) {
2773
+ json(res, 400, { error: "Missing flowId" });
2774
+ return;
2775
+ }
2776
+ const runKey = workspaceRunKey(userCtx, scoped.flowSource || payload.flowSource || "user", flowId);
2777
+ if (activeWorkspaceRuns.has(runKey)) {
2778
+ json(res, 409, { error: "该 Workspace 正在运行" });
2779
+ return;
2780
+ }
2781
+ const controller = new AbortController();
2782
+ const runEntry = {
2783
+ controller,
2784
+ child: null,
2785
+ stopChild() {
2786
+ if (this.child && !this.child.killed) {
2787
+ try { this.child.kill("SIGTERM"); } catch (_) {}
2788
+ }
2789
+ },
2790
+ };
2791
+ activeWorkspaceRuns.set(runKey, runEntry);
2792
+ const setActiveChild = (child) => {
2793
+ runEntry.child = child || null;
2794
+ if (controller.signal.aborted) runEntry.stopChild();
2795
+ };
2796
+ const clearActiveRun = () => {
2797
+ if (activeWorkspaceRuns.get(runKey) === runEntry) activeWorkspaceRuns.delete(runKey);
2798
+ };
2569
2799
  if (wantsStream) {
2570
2800
  const graphPath = workspaceGraphPath(scoped.root);
2571
2801
  res.writeHead(200, {
@@ -2574,29 +2804,77 @@ export function startUiServer({
2574
2804
  "X-Accel-Buffering": "no",
2575
2805
  });
2576
2806
  const writeEvent = (event) => {
2577
- res.write(JSON.stringify(event) + "\n");
2807
+ try { res.write(JSON.stringify(event) + "\n"); } catch (_) {}
2578
2808
  };
2579
2809
  try {
2580
- const result = await runWorkspaceGraph(root, scoped.root, payload, userCtx, { onEvent: writeEvent });
2810
+ const result = await runWorkspaceGraph(root, scoped.root, payload, userCtx, {
2811
+ onEvent: writeEvent,
2812
+ signal: controller.signal,
2813
+ onActiveChild: setActiveChild,
2814
+ });
2581
2815
  fs.writeFileSync(graphPath, JSON.stringify(result.graph, null, 2) + "\n", "utf-8");
2582
2816
  writeEvent({ type: "done", ok: true, path: graphPath, graph: result.graph, order: result.order, pauseNodeIds: result.pauseNodeIds || [] });
2583
2817
  res.end();
2584
2818
  } catch (e) {
2585
- writeEvent({ type: "error", error: (e && e.message) || String(e) });
2819
+ if (isWorkspaceRunAbortError(e) || controller.signal.aborted) {
2820
+ writeEvent({ type: "stopped", ok: false, stopped: true, message: "Workspace run stopped" });
2821
+ } else {
2822
+ writeEvent({ type: "error", error: (e && e.message) || String(e) });
2823
+ }
2586
2824
  res.end();
2825
+ } finally {
2826
+ clearActiveRun();
2587
2827
  }
2588
2828
  return;
2589
2829
  }
2590
- const result = await runWorkspaceGraph(root, scoped.root, payload, userCtx);
2591
- const graphPath = workspaceGraphPath(scoped.root);
2592
- fs.writeFileSync(graphPath, JSON.stringify(result.graph, null, 2) + "\n", "utf-8");
2593
- json(res, 200, { ok: true, path: graphPath, ...result });
2830
+ try {
2831
+ const result = await runWorkspaceGraph(root, scoped.root, payload, userCtx, {
2832
+ signal: controller.signal,
2833
+ onActiveChild: setActiveChild,
2834
+ });
2835
+ const graphPath = workspaceGraphPath(scoped.root);
2836
+ fs.writeFileSync(graphPath, JSON.stringify(result.graph, null, 2) + "\n", "utf-8");
2837
+ json(res, 200, { ok: true, path: graphPath, ...result });
2838
+ } catch (e) {
2839
+ if (isWorkspaceRunAbortError(e) || controller.signal.aborted) {
2840
+ json(res, 200, { ok: false, stopped: true, message: "Workspace run stopped" });
2841
+ } else {
2842
+ throw e;
2843
+ }
2844
+ } finally {
2845
+ clearActiveRun();
2846
+ }
2594
2847
  } catch (e) {
2595
2848
  json(res, 500, { error: (e && e.message) || String(e) });
2596
2849
  }
2597
2850
  return;
2598
2851
  }
2599
2852
 
2853
+ if (req.method === "POST" && url.pathname === "/api/workspace/run/stop") {
2854
+ let payload;
2855
+ try {
2856
+ payload = JSON.parse(await readBody(req));
2857
+ } catch {
2858
+ json(res, 400, { error: "Invalid JSON body" });
2859
+ return;
2860
+ }
2861
+ const flowId = typeof payload.flowId === "string" ? payload.flowId.trim() : "";
2862
+ if (!flowId) {
2863
+ json(res, 400, { error: "Missing flowId" });
2864
+ return;
2865
+ }
2866
+ const runKey = workspaceRunKey(userCtx, payload.flowSource || "user", flowId);
2867
+ const entry = activeWorkspaceRuns.get(runKey);
2868
+ if (!entry) {
2869
+ json(res, 404, { error: "该 Workspace 未在运行" });
2870
+ return;
2871
+ }
2872
+ try { entry.controller?.abort(); } catch (_) {}
2873
+ try { entry.stopChild?.(); } catch (_) {}
2874
+ json(res, 200, { ok: true, stopped: true });
2875
+ return;
2876
+ }
2877
+
2600
2878
  if (req.method === "GET" && url.pathname === "/api/workspace/file") {
2601
2879
  try {
2602
2880
  const scoped = resolveWorkspaceScopeRoot(root, {
@@ -2773,7 +3051,8 @@ export function startUiServer({
2773
3051
  const promptText = buildWorkspaceGeneratePrompt({ ...payload, skillsBlock });
2774
3052
  const modelKey = typeof payload?.model === "string" ? payload.model.trim() : "";
2775
3053
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2776
- let attemptContent = "";
3054
+ let attemptResult = "";
3055
+ const assistantSegments = [];
2777
3056
  try {
2778
3057
  if (attempt > 1) {
2779
3058
  events.push({
@@ -2791,12 +3070,16 @@ export function startUiServer({
2791
3070
  onStreamEvent: (ev) => {
2792
3071
  events.push(ev);
2793
3072
  if (ev?.type === "natural" && ev.kind === "assistant" && typeof ev.text === "string") {
2794
- attemptContent += (attemptContent ? "\n" : "") + ev.text;
3073
+ const text = ev.text.trim();
3074
+ if (text) assistantSegments.push(text);
3075
+ } else if (ev?.type === "natural" && ev.kind === "result" && typeof ev.text === "string") {
3076
+ const text = ev.text.trim();
3077
+ if (text) attemptResult = text;
2795
3078
  }
2796
3079
  },
2797
3080
  });
2798
3081
  await handle.finished;
2799
- content = attemptContent;
3082
+ content = attemptResult || assistantSegments.at(-1) || "";
2800
3083
  break;
2801
3084
  } catch (e) {
2802
3085
  if (attempt < maxAttempts && isTransientAgentNetworkError(e)) {
@@ -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}