@fieldwangai/agentflow 0.1.39 → 0.1.40

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.
@@ -9,7 +9,7 @@ import { normalizeCursorModelForCli } from "./model-config.mjs";
9
9
  import { appendRunLogLine } from "./run-events.mjs";
10
10
  import { writeWithPrefix } from "./terminal.mjs";
11
11
  import { t } from "./i18n.mjs";
12
- import { readUserEnvObject } from "./user-env.mjs";
12
+ import { readMergedEnvObject } from "./user-env.mjs";
13
13
  import { outputNodeBasename } from "../pipeline/get-exec-id.mjs";
14
14
 
15
15
  function shouldPassCursorModelArg(model) {
@@ -20,7 +20,7 @@ function shouldPassCursorModelArg(model) {
20
20
  function childEnv(options = {}, extra = {}) {
21
21
  const optEnv = options && options.env && typeof options.env === "object" ? options.env : {};
22
22
  const userId = optEnv.AGENTFLOW_USER_ID || process.env.AGENTFLOW_USER_ID || "";
23
- return { ...process.env, ...readUserEnvObject(userId), ...optEnv, ...extra };
23
+ return { ...process.env, ...readMergedEnvObject(userId), ...optEnv, ...extra };
24
24
  }
25
25
 
26
26
  function writeAgentTextArtifacts(absResultPath, absRunDir, instanceId, text) {
package/bin/lib/apply.mjs CHANGED
@@ -23,7 +23,7 @@ import { formatDuration } from "./terminal.mjs";
23
23
  import { printEntryAndFlowFiles, printNodeStatusTable, runValidateFlowAndExitIfInvalid } from "./ui-print.mjs";
24
24
  import { clearApplyActiveLock, writeApplyActiveLock } from "./run-apply-active-lock.mjs";
25
25
  import { ensureReference, findFlowNameByUuid, getFlowDir, getRunDir } from "./workspace.mjs";
26
- import { readUserEnvObject } from "./user-env.mjs";
26
+ import { readMergedEnvObject } from "./user-env.mjs";
27
27
 
28
28
  const PARALLEL_PREFIX_COLORS = [
29
29
  (s) => chalk.cyan(s),
@@ -347,7 +347,7 @@ ${currentContent}
347
347
 
348
348
  const result = spawnSync(opencodeCmd, ["--prompt-file", tmpPromptFile, "--print"], {
349
349
  cwd: workspaceRoot,
350
- env: { ...process.env, ...readUserEnvObject(process.env.AGENTFLOW_USER_ID || ""), OPENCODE_NON_INTERACTIVE: "1" },
350
+ env: { ...process.env, ...readMergedEnvObject(process.env.AGENTFLOW_USER_ID || ""), OPENCODE_NON_INTERACTIVE: "1" },
351
351
  stdio: ["ignore", "pipe", "pipe"],
352
352
  });
353
353
 
@@ -5,9 +5,10 @@
5
5
  * 用户 prompt → planner 分解 → [script 直执 | agent 子调用(按复杂度选模型)] → sync UI
6
6
  */
7
7
  import fs from "fs";
8
+ import os from "os";
8
9
  import path from "path";
9
- import { getAgentflowDataRoot, sanitizeAgentflowUserId } from "./paths.mjs";
10
- import { readUserEnvObject } from "./user-env.mjs";
10
+ import { getAgentflowDataRoot, getAgentflowUserDataRoot, sanitizeAgentflowUserId } from "./paths.mjs";
11
+ import { readMergedEnvObject } from "./user-env.mjs";
11
12
  import { resolveCliAndModel } from "./model-config.mjs";
12
13
  import { runClaudeCodeAgentWithPrompt, runCursorAgentWithPrompt, runOpenCodeAgentWithPrompt } from "./agent-runners.mjs";
13
14
  import { planComposerTasks, hasPlannerApiAvailable, shouldUsePhased, classifyComplexity, classifyTaskComplexity, PHASED_DEFINITIONS } from "./composer-planner.mjs";
@@ -24,9 +25,68 @@ const MAX_PROMPT_CHARS = 500_000;
24
25
  const MAX_COMPOSER_VALIDATION_REPAIR = 5;
25
26
  const MAX_SCRIPT_INJECT_BYTES = 30_000;
26
27
 
28
+ function readJsonObject(filePath) {
29
+ try {
30
+ if (!fs.existsSync(filePath)) return {};
31
+ const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
32
+ return data && typeof data === "object" && !Array.isArray(data) ? data : {};
33
+ } catch {
34
+ return {};
35
+ }
36
+ }
37
+
38
+ function readUserMcpPrivateEnvObject(userId) {
39
+ const safe = sanitizeAgentflowUserId(userId);
40
+ const data = readJsonObject(path.join(getAgentflowUserDataRoot(safe), "mcp-private.json"));
41
+ const servers = data?.servers && typeof data.servers === "object" && !Array.isArray(data.servers) ? data.servers : {};
42
+ const env = {};
43
+ for (const server of Object.values(servers)) {
44
+ const serverEnv = server?.env && typeof server.env === "object" && !Array.isArray(server.env) ? server.env : {};
45
+ for (const [key, value] of Object.entries(serverEnv)) {
46
+ const envKey = String(key || "").trim();
47
+ if (envKey) env[envKey] = String(value ?? "");
48
+ }
49
+ }
50
+ return env;
51
+ }
52
+
53
+ function pruneCursorMcpPrivateEnvPlaceholders() {
54
+ const filePath = path.join(os.homedir(), ".cursor", "mcp.json");
55
+ const config = readJsonObject(filePath);
56
+ const servers = config?.mcpServers && typeof config.mcpServers === "object" && !Array.isArray(config.mcpServers)
57
+ ? config.mcpServers
58
+ : null;
59
+ if (!servers) return;
60
+ let changed = false;
61
+ const nextServers = {};
62
+ for (const [name, raw] of Object.entries(servers)) {
63
+ const server = raw && typeof raw === "object" && !Array.isArray(raw) ? { ...raw } : raw;
64
+ const privateEnvKeys = Array.isArray(server?.__agentflowPrivateKeys?.env)
65
+ ? server.__agentflowPrivateKeys.env.map((key) => String(key || "").trim()).filter(Boolean)
66
+ : [];
67
+ if (!privateEnvKeys.length || !server?.env || typeof server.env !== "object" || Array.isArray(server.env)) {
68
+ nextServers[name] = server;
69
+ continue;
70
+ }
71
+ const nextEnv = { ...server.env };
72
+ for (const key of privateEnvKeys) {
73
+ if (Object.prototype.hasOwnProperty.call(nextEnv, key) && String(nextEnv[key] ?? "") === "") {
74
+ delete nextEnv[key];
75
+ changed = true;
76
+ }
77
+ }
78
+ nextServers[name] = { ...server, env: nextEnv };
79
+ if (Object.keys(nextEnv).length === 0) delete nextServers[name].env;
80
+ }
81
+ if (!changed) return;
82
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
83
+ fs.writeFileSync(filePath, JSON.stringify({ ...config, mcpServers: nextServers }, null, 2) + "\n", "utf-8");
84
+ }
85
+
27
86
  function agentflowUserEnv(userId) {
28
87
  const safe = sanitizeAgentflowUserId(userId);
29
- return safe ? { ...readUserEnvObject(safe), AGENTFLOW_USER_ID: safe } : {};
88
+ pruneCursorMcpPrivateEnvPlaceholders();
89
+ return { ...readMergedEnvObject(safe), ...(safe ? readUserMcpPrivateEnvObject(safe) : {}), AGENTFLOW_USER_ID: safe };
30
90
  }
31
91
 
32
92
  // ─── script 内容注入辅助 ─────────────────────────────────────────────────
@@ -12,7 +12,7 @@ import {
12
12
  import { getAgentflowUserContexts, getRunDir, PACKAGE_ROOT } from "./paths.mjs";
13
13
  import { isApplyProcessAlive } from "./run-apply-active-lock.mjs";
14
14
  import { log } from "./log.mjs";
15
- import { readUserEnvObject } from "./user-env.mjs";
15
+ import { readMergedEnvObject } from "./user-env.mjs";
16
16
  import { writeResult } from "../pipeline/write-result.mjs";
17
17
 
18
18
  const DEFAULT_POLL_MS = 30_000;
@@ -258,7 +258,7 @@ function startScheduledRun(workspaceRoot, flow, schedule, state, opts = {}) {
258
258
  const child = spawn(process.execPath, args, {
259
259
  cwd: path.resolve(workspaceRoot),
260
260
  stdio: ["ignore", "pipe", "pipe"],
261
- env: { ...process.env, ...readUserEnvObject(opts.userId), FORCE_COLOR: "0", AGENTFLOW_USER_ID: opts.userId || "" },
261
+ env: { ...process.env, ...readMergedEnvObject(opts.userId), FORCE_COLOR: "0", AGENTFLOW_USER_ID: opts.userId || "" },
262
262
  detached: true,
263
263
  });
264
264
 
@@ -331,7 +331,7 @@ function startWaitingRunResume(workspaceRoot, flow, waitState, opts = {}) {
331
331
  const child = spawn(process.execPath, args, {
332
332
  cwd: path.resolve(workspaceRoot),
333
333
  stdio: ["ignore", "pipe", "pipe"],
334
- env: { ...process.env, ...readUserEnvObject(opts.userId), FORCE_COLOR: "0", AGENTFLOW_USER_ID: opts.userId || "" },
334
+ env: { ...process.env, ...readMergedEnvObject(opts.userId), FORCE_COLOR: "0", AGENTFLOW_USER_ID: opts.userId || "" },
335
335
  detached: true,
336
336
  });
337
337
  child.stdout.on("data", () => {});
@@ -97,7 +97,7 @@ import {
97
97
  logoutRequest,
98
98
  readUserAllowlist,
99
99
  } from "./auth.mjs";
100
- import { readUserEnvObject, readUserEnvRows, writeUserEnvRows } from "./user-env.mjs";
100
+ import { readGlobalEnvRows, readMergedEnvObject, readUserEnvRows, writeGlobalEnvRows, writeUserEnvRows } from "./user-env.mjs";
101
101
  import {
102
102
  readAdminBuiltinPipelineConfig,
103
103
  updateAdminBuiltinPipelineConfig,
@@ -190,7 +190,6 @@ function writeFeedbackItems(items) {
190
190
  function createFeedbackItem(payload, user) {
191
191
  const title = String(payload?.title || "").trim().slice(0, 120);
192
192
  const content = String(payload?.content || "").trim().slice(0, 5000);
193
- const contact = String(payload?.contact || "").trim().slice(0, 160);
194
193
  const pageUrl = String(payload?.pageUrl || "").trim().slice(0, 500);
195
194
  if (!title) return { error: "Missing feedback title" };
196
195
  if (!content) return { error: "Missing feedback content" };
@@ -199,7 +198,6 @@ function createFeedbackItem(payload, user) {
199
198
  id: `fb_${Date.now().toString(36)}_${crypto.randomBytes(5).toString("hex")}`,
200
199
  title,
201
200
  content,
202
- contact,
203
201
  pageUrl,
204
202
  userId: String(user?.userId || ""),
205
203
  username: String(user?.username || user?.userId || ""),
@@ -390,7 +388,7 @@ function removeSkillhubCollectionGroup(userCtx = {}, collectionId = "", root = p
390
388
  function runtimeEnvForUser(userCtx = {}, extra = {}) {
391
389
  return {
392
390
  ...process.env,
393
- ...readUserEnvObject(userCtx.userId),
391
+ ...readMergedEnvObject(userCtx.userId),
394
392
  ...extra,
395
393
  AGENTFLOW_USER_ID: userCtx.userId || "",
396
394
  };
@@ -565,8 +563,6 @@ function writeCursorMcpServer(payload = {}, userCtx = {}) {
565
563
  env: omitObjectKeys(server.env || {}, privateEnvKeys),
566
564
  headers: omitObjectKeys(server.headers || {}, privateHeaderKeys),
567
565
  };
568
- publicServer.env = withPrivatePlaceholders(publicServer.env, privateEnvKeys);
569
- publicServer.headers = withPrivatePlaceholders(publicServer.headers, privateHeaderKeys);
570
566
  if (privateEnvKeys.size || privateHeaderKeys.size) {
571
567
  publicServer.__agentflowPrivateKeys = {
572
568
  ...(privateEnvKeys.size ? { env: Array.from(privateEnvKeys) } : {}),
@@ -1369,6 +1365,125 @@ function workspaceSetOutputSlot(instance, name, value) {
1369
1365
  };
1370
1366
  }
1371
1367
 
1368
+ function workspaceSourceSlotForEdge(graph, edge) {
1369
+ const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
1370
+ const source = instances[String(edge?.source || "")];
1371
+ const output = Array.isArray(source?.output) ? source.output : [];
1372
+ return output[workspaceHandleIndex(edge?.sourceHandle, "output")] || null;
1373
+ }
1374
+
1375
+ function workspaceOutputSlotValueForEdge(graph, outputs, edge) {
1376
+ const sourceId = String(edge?.source || "");
1377
+ const slot = workspaceSourceSlotForEdge(graph, edge);
1378
+ if (slot && String(slot?.type || "") !== "node") {
1379
+ const value = workspaceSlotValue(slot);
1380
+ if (value.trim()) return value;
1381
+ }
1382
+ const out = outputs.get(sourceId);
1383
+ if (out != null && String(out).trim()) return String(out);
1384
+ const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
1385
+ return workspaceInstanceText(instances[sourceId]);
1386
+ }
1387
+
1388
+ function workspaceParseJsonObjectFromText(text) {
1389
+ const raw = String(text || "").trim();
1390
+ if (!raw) return null;
1391
+ const candidates = [raw];
1392
+ const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i);
1393
+ if (fenced?.[1]) candidates.unshift(fenced[1].trim());
1394
+ const first = raw.indexOf("{");
1395
+ const last = raw.lastIndexOf("}");
1396
+ if (first >= 0 && last > first) candidates.unshift(raw.slice(first, last + 1));
1397
+ for (const candidate of candidates) {
1398
+ try {
1399
+ const parsed = JSON.parse(candidate);
1400
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
1401
+ } catch {
1402
+ /* try next */
1403
+ }
1404
+ }
1405
+ return null;
1406
+ }
1407
+
1408
+ function workspaceStringifyOutputValue(value) {
1409
+ if (value == null) return "";
1410
+ return typeof value === "string" ? value : JSON.stringify(value, null, 2);
1411
+ }
1412
+
1413
+ function workspaceStructuredAgentOutput(content) {
1414
+ const raw = String(content || "").trim();
1415
+ const parsed = workspaceParseJsonObjectFromText(raw);
1416
+ if (!parsed) return { result: raw, outParams: {}, structured: false, parsed: null };
1417
+ const hasEnvelope = Object.prototype.hasOwnProperty.call(parsed, "result") ||
1418
+ Object.prototype.hasOwnProperty.call(parsed, "outParams");
1419
+ if (!hasEnvelope) return { result: raw, outParams: {}, structured: false, parsed };
1420
+ const outParamsRaw = parsed.outParams && typeof parsed.outParams === "object" && !Array.isArray(parsed.outParams)
1421
+ ? parsed.outParams
1422
+ : {};
1423
+ const outParams = {};
1424
+ for (const [key, value] of Object.entries(outParamsRaw)) {
1425
+ const name = String(key || "").trim();
1426
+ if (name) outParams[name] = workspaceStringifyOutputValue(value);
1427
+ }
1428
+ return {
1429
+ result: workspaceStringifyOutputValue(parsed.result ?? ""),
1430
+ outParams,
1431
+ structured: true,
1432
+ parsed,
1433
+ };
1434
+ }
1435
+
1436
+ function workspaceExtractNamedOutputValue(content, slotName) {
1437
+ const name = String(slotName || "").trim();
1438
+ if (!name) return "";
1439
+ const structured = workspaceStructuredAgentOutput(content);
1440
+ if (Object.prototype.hasOwnProperty.call(structured.outParams, name)) {
1441
+ return String(structured.outParams[name] ?? "");
1442
+ }
1443
+ const parsed = structured.parsed || workspaceParseJsonObjectFromText(content);
1444
+ if (parsed && Object.prototype.hasOwnProperty.call(parsed, name)) {
1445
+ const value = parsed[name];
1446
+ return workspaceStringifyOutputValue(value);
1447
+ }
1448
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1449
+ const patterns = [
1450
+ new RegExp(`(?:\\$\\{${escaped}\\}|\\$${escaped})\\s*[=::]\\s*([^\\n\\r]+)`, "i"),
1451
+ new RegExp(`(?:^|[\\n\\r])\\s*${escaped}\\s*[=::]\\s*([^\\n\\r]+)`, "i"),
1452
+ ];
1453
+ for (const pattern of patterns) {
1454
+ const match = pattern.exec(String(content || ""));
1455
+ if (!match?.[1]) continue;
1456
+ return match[1].replace(/^["'`]|["'`]$/g, "").trim();
1457
+ }
1458
+ return "";
1459
+ }
1460
+
1461
+ function workspaceApplyAgentOutputSlots(instance, content) {
1462
+ const structured = workspaceStructuredAgentOutput(content);
1463
+ const text = String(structured.result || "").trim();
1464
+ let changed = false;
1465
+ const next = {
1466
+ ...(instance || {}),
1467
+ output: (Array.isArray(instance?.output) ? instance.output : []).map((slot, index) => {
1468
+ const name = String(slot?.name || "").trim();
1469
+ const type = String(slot?.type || "");
1470
+ if (type === "node" || name === "next" || !name) return slot;
1471
+ let value = "";
1472
+ if (name === "result" || name === "content" || index === 0) {
1473
+ value = text;
1474
+ } else if (Object.prototype.hasOwnProperty.call(structured.outParams, name)) {
1475
+ value = structured.outParams[name];
1476
+ } else {
1477
+ value = workspaceExtractNamedOutputValue(text, name);
1478
+ }
1479
+ if (!value) return slot;
1480
+ changed = true;
1481
+ return { ...slot, default: value, value };
1482
+ }),
1483
+ };
1484
+ return { instance: changed ? next : instance, changed };
1485
+ }
1486
+
1372
1487
  function workspaceResolvePath(baseCwd, raw) {
1373
1488
  const text = String(raw || "").trim();
1374
1489
  if (!text) return "";
@@ -1453,25 +1568,25 @@ function workspaceDownstreamDisplayRequirements(graph, nodeId) {
1453
1568
  if (kinds.size === 0) return "";
1454
1569
  const rules = [];
1455
1570
  if (kinds.has("html")) {
1456
- rules.push("- 下游连接了 HTML 展示节点:输出可直接放入 iframe 渲染的 HTML。可以是完整 HTML 文档或 HTML fragment;不要使用 Markdown 代码围栏;不要解释生成过程。");
1571
+ rules.push("- 下游连接了 HTML 展示节点:将可直接放入 iframe 渲染的 HTML 放在输出协议的 `result` 字段中。可以是完整 HTML 文档或 HTML fragment;不要使用 Markdown 代码围栏。");
1457
1572
  }
1458
1573
  if (kinds.has("markdown")) {
1459
- rules.push("- 下游连接了 Markdown 展示节点:输出 Markdown 正文;不要包裹在代码围栏中,除非正文确实需要代码块。");
1574
+ rules.push("- 下游连接了 Markdown 展示节点:将 Markdown 正文放在输出协议的 `result` 字段中;除非正文确实需要代码块,否则不要额外包裹代码围栏。");
1460
1575
  }
1461
1576
  if (kinds.has("mermaid")) {
1462
- rules.push("- 下游连接了 Mermaid 展示节点:只输出 Mermaid 图表代码,例如 flowchart/sequenceDiagram;不要使用 Markdown 代码围栏;不要附加解释。");
1577
+ rules.push("- 下游连接了 Mermaid 展示节点:将 Mermaid 图表代码放在输出协议的 `result` 字段中,例如 flowchart/sequenceDiagram;不要使用 Markdown 代码围栏。");
1463
1578
  }
1464
1579
  if (kinds.has("ascii")) {
1465
- rules.push("- 下游连接了 ASCII 展示节点:输出纯文本/ASCII 图或表格;不要输出 HTML 或 Markdown 装饰。");
1580
+ rules.push("- 下游连接了 ASCII 展示节点:将纯文本/ASCII 图或表格放在输出协议的 `result` 字段中;不要输出 HTML 或 Markdown 装饰。");
1466
1581
  }
1467
1582
  if (kinds.has("image")) {
1468
- rules.push("- 下游连接了图片展示节点:输出可作为 img src 使用的图片地址、data URL 或 base64 data URL;不要输出 Markdown 图片语法或解释文字。");
1583
+ rules.push("- 下游连接了图片展示节点:将可作为 img src 使用的图片地址、data URL 或 base64 data URL 放在输出协议的 `result` 字段中;不要输出 Markdown 图片语法。");
1469
1584
  }
1470
1585
  if (kinds.has("chart")) {
1471
- 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 函数。');
1586
+ rules.push('- 下游连接了 Chart 展示节点:将 ChartSpec JSON 对象放在输出协议的 `result` 字段中。ChartSpec 必须包含 `"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 函数。');
1472
1587
  }
1473
1588
  if (kinds.has("table")) {
1474
- rules.push('- 下游连接了表格展示节点:优先只输出表格 JSON,不要解释文字。推荐格式:`{"columns":["列名1","列名2"],"rows":[["值1","值2"]]}`;也可输出对象数组、Markdown 表格、CSV 或 TSV。不要输出 HTML。');
1589
+ rules.push('- 下游连接了表格展示节点:将表格数据放在输出协议的 `result` 字段中。推荐格式:`{"columns":["列名1","列名2"],"rows":[["值1","值2"]]}`;也可使用对象数组、Markdown 表格、CSV 或 TSV。不要输出 HTML。');
1475
1590
  }
1476
1591
  return [
1477
1592
  "## 下游输出要求",
@@ -1482,6 +1597,35 @@ function workspaceDownstreamDisplayRequirements(graph, nodeId) {
1482
1597
  ].join("\n");
1483
1598
  }
1484
1599
 
1600
+ function workspaceOutputProtocolRequirements(graph, nodeId) {
1601
+ const instance = graph?.instances?.[nodeId] || {};
1602
+ const slots = (Array.isArray(instance.output) ? instance.output : [])
1603
+ .filter((slot) => {
1604
+ const name = String(slot?.name || "").trim();
1605
+ const type = String(slot?.type || "");
1606
+ return name && type !== "node" && name !== "next" && name !== "result" && name !== "content";
1607
+ })
1608
+ .map((slot) => String(slot.name).trim());
1609
+ const outParamsExample = slots.length
1610
+ ? Object.fromEntries(slots.map((name) => [name, `<${name} 的值>`]))
1611
+ : {};
1612
+ return [
1613
+ "## Workspace 输出协议",
1614
+ "",
1615
+ "最终回复必须是一个 JSON 对象,不要使用 Markdown 代码围栏,不要在 JSON 外追加解释文字。",
1616
+ "固定格式:",
1617
+ "",
1618
+ JSON.stringify({ result: "<给用户看的完整正文>", outParams: outParamsExample }, null, 2),
1619
+ "",
1620
+ "- `result`:完整正文,写入 `result` / `content` 输出口,直连默认展示节点时展示它。",
1621
+ "- `outParams`:具名输出参数,只写入同名输出引脚。",
1622
+ ...(slots.length
1623
+ ? [`- 当前节点具名输出槽:${slots.map((name) => `\`${name}\``).join("、")}。例如任务要求写入 \`${slots[0]}\` 时,放到 \`outParams.${slots[0]}\`。`]
1624
+ : ["- 当前节点没有额外具名输出槽,`outParams` 返回空对象即可。"]),
1625
+ "- 如果 `result` 需要承载表格、ChartSpec、HTML 等结构化内容,可把对象或字符串放入 `result`;系统会把它转换给下游展示节点。",
1626
+ ].join("\n");
1627
+ }
1628
+
1485
1629
  function workspaceRunPlan(graph, runNodeId) {
1486
1630
  const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
1487
1631
  const edges = Array.isArray(graph?.edges) ? graph.edges : [];
@@ -1542,10 +1686,7 @@ function workspaceUpstreamText(graph, nodeId, outputs) {
1542
1686
  const incoming = edges.filter((edge) => String(edge?.target || "") === String(nodeId));
1543
1687
  const contentEdge = incoming.find((edge) => String(edge?.targetHandle || "") === "input-1") || incoming[0];
1544
1688
  if (!contentEdge) return "";
1545
- const sourceId = String(contentEdge.source || "");
1546
- const out = outputs.get(sourceId);
1547
- if (out != null && String(out).trim()) return String(out);
1548
- return workspaceInstanceText(instances[sourceId]);
1689
+ return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge);
1549
1690
  }
1550
1691
 
1551
1692
  function workspaceHandleIndex(handle, prefix) {
@@ -1573,10 +1714,7 @@ function workspaceTaskUpstreamText(graph, nodeId, outputs) {
1573
1714
  const contentEdges = incoming.filter((edge) => !isWorkspaceSemanticInputSlot(workspaceTargetSlotForEdge(graph, edge)));
1574
1715
  const contentEdge = contentEdges.find((edge) => String(edge?.targetHandle || "") === "input-1") || contentEdges[0];
1575
1716
  if (!contentEdge) return "";
1576
- const sourceId = String(contentEdge.source || "");
1577
- const out = outputs.get(sourceId);
1578
- if (out != null && String(out).trim()) return String(out);
1579
- return workspaceInstanceText(instances[sourceId]);
1717
+ return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge);
1580
1718
  }
1581
1719
 
1582
1720
  function parseWorkspaceSkillKeys(raw) {
@@ -1727,7 +1865,7 @@ function workspaceWriteDisplayContent(instance, content) {
1727
1865
  return next;
1728
1866
  }
1729
1867
 
1730
- function workspaceUpdateDirectDisplays(graph, sourceId, content) {
1868
+ function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null) {
1731
1869
  const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
1732
1870
  const edges = Array.isArray(graph?.edges) ? graph.edges : [];
1733
1871
  const updated = [];
@@ -1736,7 +1874,8 @@ function workspaceUpdateDirectDisplays(graph, sourceId, content) {
1736
1874
  const targetId = String(edge?.target || "");
1737
1875
  const target = instances[targetId];
1738
1876
  if (!target || !workspaceDisplayKind(target.definitionId)) continue;
1739
- instances[targetId] = workspaceWriteDisplayContent(target, content);
1877
+ const value = outputs ? workspaceOutputSlotValueForEdge(graph, outputs, edge) : String(content || "");
1878
+ instances[targetId] = workspaceWriteDisplayContent(target, value || content);
1740
1879
  updated.push(targetId);
1741
1880
  }
1742
1881
  return updated;
@@ -1747,14 +1886,16 @@ function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock
1747
1886
  const body = String(instance.body || "").trim();
1748
1887
  const label = String(instance.label || nodeId).trim();
1749
1888
  const downstreamRequirements = workspaceDownstreamDisplayRequirements(graph, nodeId);
1889
+ const outputProtocolRequirements = workspaceOutputProtocolRequirements(graph, nodeId);
1750
1890
  return [
1751
1891
  "你正在执行 AgentFlow Workspace 画布中的一个临时节点。",
1752
- "只输出该节点要传给下游展示/后续节点的正文,不要解释运行过程。",
1892
+ "按 Workspace 输出协议返回该节点要传给下游展示/后续节点的数据。",
1753
1893
  workspaceSearchGuardrailsBlock(),
1754
1894
  skillsBlock ? `\n## Available Skills\n\n${skillsBlock}` : "",
1755
1895
  mcpBlock ? `\n## Available MCP\n\n${mcpBlock}` : "",
1756
1896
  upstreamText ? `\n## 上游上下文\n\n${upstreamText}` : "",
1757
1897
  downstreamRequirements ? `\n${downstreamRequirements}` : "",
1898
+ outputProtocolRequirements ? `\n${outputProtocolRequirements}` : "",
1758
1899
  `\n## 当前节点\n\n- id: ${nodeId}\n- label: ${label}\n- definitionId: ${instance.definitionId || ""}`,
1759
1900
  `\n## 节点任务\n\n${body || upstreamText}`,
1760
1901
  ].filter(Boolean).join("\n");
@@ -1844,7 +1985,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
1844
1985
  )),
1845
1986
  };
1846
1987
  outputs.set(nodeId, skillsBlock);
1847
- workspaceUpdateDirectDisplays(graph, nodeId, skillsBlock);
1988
+ workspaceUpdateDirectDisplays(graph, nodeId, skillsBlock, outputs);
1848
1989
  emit({ type: "graph", nodeId, graph });
1849
1990
  emit({ type: "node-done", nodeId, definitionId: defId });
1850
1991
  continue;
@@ -1864,7 +2005,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
1864
2005
  )),
1865
2006
  };
1866
2007
  outputs.set(nodeId, mcpBlock);
1867
- workspaceUpdateDirectDisplays(graph, nodeId, mcpBlock);
2008
+ workspaceUpdateDirectDisplays(graph, nodeId, mcpBlock, outputs);
1868
2009
  emit({ type: "graph", nodeId, graph });
1869
2010
  emit({ type: "node-done", nodeId, definitionId: defId });
1870
2011
  continue;
@@ -2166,9 +2307,13 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
2166
2307
  throw e;
2167
2308
  }
2168
2309
  }
2169
- outputs.set(nodeId, content);
2170
- const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, content);
2171
- if (updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
2310
+ const normalizedAgentOutput = workspaceStructuredAgentOutput(content);
2311
+ const resultContent = normalizedAgentOutput.result || content;
2312
+ outputs.set(nodeId, resultContent);
2313
+ const slotUpdate = workspaceApplyAgentOutputSlots(instance, content);
2314
+ if (slotUpdate.changed) graph.instances[nodeId] = slotUpdate.instance;
2315
+ const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, resultContent, outputs);
2316
+ if (slotUpdate.changed || updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
2172
2317
  emit({ type: "node-done", nodeId, definitionId: defId });
2173
2318
  }
2174
2319
  if (pauseNodeIds.length > 0) {
@@ -3569,7 +3714,11 @@ export function startUiServer({
3569
3714
 
3570
3715
  if (req.method === "GET" && url.pathname === "/api/user-env") {
3571
3716
  try {
3572
- json(res, 200, { env: readUserEnvRows(userCtx.userId) });
3717
+ json(res, 200, {
3718
+ env: readUserEnvRows(userCtx.userId),
3719
+ globalEnv: authUser?.isAdmin ? readGlobalEnvRows() : [],
3720
+ canEditGlobalEnv: Boolean(authUser?.isAdmin),
3721
+ });
3573
3722
  } catch (e) {
3574
3723
  json(res, 500, { error: (e && e.message) || String(e) });
3575
3724
  }
@@ -3585,8 +3734,20 @@ export function startUiServer({
3585
3734
  return;
3586
3735
  }
3587
3736
  try {
3737
+ if (Object.prototype.hasOwnProperty.call(payload || {}, "globalEnv") && !authUser?.isAdmin) {
3738
+ json(res, 403, { error: "Admin permission required" });
3739
+ return;
3740
+ }
3588
3741
  const envRows = writeUserEnvRows(userCtx.userId, payload?.env || []);
3589
- json(res, 200, { success: true, env: envRows });
3742
+ const globalEnvRows = authUser?.isAdmin && Object.prototype.hasOwnProperty.call(payload || {}, "globalEnv")
3743
+ ? writeGlobalEnvRows(payload?.globalEnv || [])
3744
+ : readGlobalEnvRows();
3745
+ json(res, 200, {
3746
+ success: true,
3747
+ env: envRows,
3748
+ globalEnv: authUser?.isAdmin ? globalEnvRows : [],
3749
+ canEditGlobalEnv: Boolean(authUser?.isAdmin),
3750
+ });
3590
3751
  } catch (e) {
3591
3752
  json(res, 500, { error: (e && e.message) || String(e) });
3592
3753
  }
@@ -2,7 +2,7 @@ import fs from "fs";
2
2
  import os from "os";
3
3
  import path from "path";
4
4
 
5
- import { getAgentflowUserEnvAbs, sanitizeAgentflowUserId } from "./paths.mjs";
5
+ import { getAgentflowDataRoot, getAgentflowUserEnvAbs, sanitizeAgentflowUserId } from "./paths.mjs";
6
6
 
7
7
  function normalizeEnvKey(key) {
8
8
  return String(key || "").trim();
@@ -60,6 +60,35 @@ export function readUserEnvObject(userId) {
60
60
  return out;
61
61
  }
62
62
 
63
+ function getGlobalEnvAbs() {
64
+ return path.join(getAgentflowDataRoot(), "admin", "env.json");
65
+ }
66
+
67
+ export function readGlobalEnvRows() {
68
+ const data = readJsonObject(getGlobalEnvAbs());
69
+ return normalizeUserEnvRows(Array.isArray(data.env) ? data.env : []);
70
+ }
71
+
72
+ export function readGlobalEnvObject() {
73
+ const out = {};
74
+ for (const row of readGlobalEnvRows()) {
75
+ out[row.key] = row.value;
76
+ }
77
+ return out;
78
+ }
79
+
80
+ export function writeGlobalEnvRows(rows) {
81
+ const normalized = normalizeUserEnvRows(rows);
82
+ const filePath = getGlobalEnvAbs();
83
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
84
+ fs.writeFileSync(filePath, JSON.stringify({ version: 1, env: normalized }, null, 2) + "\n", "utf-8");
85
+ return normalized;
86
+ }
87
+
88
+ export function readMergedEnvObject(userId) {
89
+ return { ...readGlobalEnvObject(), ...readUserEnvObject(userId) };
90
+ }
91
+
63
92
  export function writeUserEnvRows(userId, rows) {
64
93
  const normalized = normalizeUserEnvRows(rows);
65
94
  const safeUserId = sanitizeAgentflowUserId(userId);
@@ -74,10 +103,11 @@ export function resolveUserEnvValue(key, userId) {
74
103
  if (!keyStr) return "";
75
104
  const userEnv = readUserEnvObject(userId);
76
105
  if (Object.prototype.hasOwnProperty.call(userEnv, keyStr)) return String(userEnv[keyStr] ?? "");
106
+ const globalEnv = readGlobalEnvObject();
107
+ if (Object.prototype.hasOwnProperty.call(globalEnv, keyStr)) return String(globalEnv[keyStr] ?? "");
77
108
  const processValue = process.env[keyStr];
78
109
  if (processValue != null && processValue !== "") return String(processValue);
79
110
  const configPath = path.join(os.homedir(), ".cursor", "config.json");
80
111
  const fromConfig = getFromConfig(readJsonObject(configPath), keyStr);
81
112
  return fromConfig !== undefined ? fromConfig : "";
82
113
  }
83
-
@@ -19,7 +19,7 @@ import path from "path";
19
19
  import { fileURLToPath } from "url";
20
20
 
21
21
  import { getRunDir } from "../lib/paths.mjs";
22
- import { readUserEnvObject } from "../lib/user-env.mjs";
22
+ import { readMergedEnvObject } from "../lib/user-env.mjs";
23
23
  import { validateAndParse } from "./validate-script-output.mjs";
24
24
  import { writeResult } from "./write-result.mjs";
25
25
  import { loadExecId, outputNodeBasename, outputDirForNode } from "./get-exec-id.mjs";
@@ -31,7 +31,7 @@ const MAX_RETRIES = 3;
31
31
  const RETRY_DELAY_MS = 1000;
32
32
 
33
33
  function runtimeEnv() {
34
- return { ...process.env, ...readUserEnvObject(process.env.AGENTFLOW_USER_ID || "") };
34
+ return { ...process.env, ...readMergedEnvObject(process.env.AGENTFLOW_USER_ID || "") };
35
35
  }
36
36
 
37
37
  function runOnce(workspaceRoot, flowName, uuid, instanceId, execId, scriptArgs) {