@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.
- 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 +312 -29
- 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-B0FRh8GY.js +214 -0
- package/builtin/web-ui/dist/assets/index-DgQRkS4v.js +61 -0
- package/builtin/web-ui/dist/assets/index-xdncUuHG.css +1 -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();
|
|
@@ -1162,9 +1207,10 @@ function buildWorkspaceGeneratePrompt(payload) {
|
|
|
1162
1207
|
].join("\n")
|
|
1163
1208
|
: [
|
|
1164
1209
|
"你是 AgentFlow Workspace Composer。",
|
|
1165
|
-
"
|
|
1166
|
-
"
|
|
1167
|
-
"
|
|
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
|
|
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
|
|
1877
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
-
|
|
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
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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}
|