@fieldwangai/agentflow 0.1.62 → 0.1.63

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.
@@ -1948,13 +1948,23 @@ function workspaceExtractLooseResult(raw) {
1948
1948
  return "";
1949
1949
  }
1950
1950
 
1951
+ function workspaceNormalizeAgentflowEnvelopeBody(body) {
1952
+ let text = String(body || "").replace(/\r\n/g, "\n").trim();
1953
+ if (!text.includes("\n")) {
1954
+ text = text
1955
+ .replace(/\s+(resultFile|result|outParams|outParams\.[A-Za-z_][A-Za-z0-9_-]*)\s*:/g, "\n$1:")
1956
+ .replace(/(^|\n)outParams:\s+([A-Za-z_][A-Za-z0-9_-]*\s*:)/g, "$1outParams:\n $2");
1957
+ }
1958
+ return text;
1959
+ }
1960
+
1951
1961
  function workspaceExtractAgentflowEnvelope(raw) {
1952
1962
  const text = String(raw || "");
1953
- const match = text.match(/(?:^|\n)---agentflow\s*\n([\s\S]*?)\n---end(?:\n|$)/i);
1963
+ const match = text.match(/---agentflow\b([\s\S]*?)---end/i);
1954
1964
  if (!match) return null;
1955
- const envelope = match[1] || "";
1965
+ const envelope = workspaceNormalizeAgentflowEnvelopeBody(match[1] || "");
1956
1966
  const outside = `${text.slice(0, match.index || 0)}\n${text.slice((match.index || 0) + match[0].length)}`.trim();
1957
- const lines = envelope.replace(/\r\n/g, "\n").split("\n");
1967
+ const lines = envelope.split("\n");
1958
1968
  const outParams = {};
1959
1969
  let result = "";
1960
1970
  let resultFile = "";
@@ -1986,7 +1996,7 @@ function workspaceExtractAgentflowEnvelope(raw) {
1986
1996
  i += 1;
1987
1997
  continue;
1988
1998
  }
1989
- const top = line.match(/^([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/);
1999
+ const top = line.match(/^([A-Za-z_][A-Za-z0-9_.-]*)\s*:\s*(.*)$/);
1990
2000
  if (!top) {
1991
2001
  i += 1;
1992
2002
  continue;
@@ -2002,7 +2012,7 @@ function workspaceExtractAgentflowEnvelope(raw) {
2002
2012
  continue;
2003
2013
  }
2004
2014
  if (lineIndent(childLine) === 0) break;
2005
- const child = childLine.match(/^\s+([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/);
2015
+ const child = childLine.match(/^\s+([A-Za-z_][A-Za-z0-9_.-]*)\s*:\s*(.*)$/);
2006
2016
  if (!child) {
2007
2017
  i += 1;
2008
2018
  continue;
@@ -2017,6 +2027,7 @@ function workspaceExtractAgentflowEnvelope(raw) {
2017
2027
  const parsed = parseValue(rawValue, i, 0);
2018
2028
  if (key === "result") result = String(parsed.value || "");
2019
2029
  else if (key === "resultFile") resultFile = String(parsed.value || "").trim();
2030
+ else if (key.startsWith("outParams.")) outParams[key.slice("outParams.".length)] = String(parsed.value || "").trim();
2020
2031
  else if (key) outParams[key] = String(parsed.value || "").trim();
2021
2032
  i = parsed.nextIndex;
2022
2033
  }
@@ -2031,8 +2042,8 @@ function workspaceExtractAgentflowEnvelope(raw) {
2031
2042
 
2032
2043
  function workspaceCanonicalAgentOutput(content) {
2033
2044
  const raw = String(content || "").trim();
2034
- const match = raw.match(/(?:^|\n)(---agentflow\s*\n[\s\S]*?\n---end)(?:\n|$)/i);
2035
- if (match?.[1]) return match[1].trim();
2045
+ const match = raw.match(/---agentflow\b[\s\S]*?---end/i);
2046
+ if (match?.[0]) return match[0].trim();
2036
2047
  return raw;
2037
2048
  }
2038
2049
 
@@ -2055,6 +2066,7 @@ function workspaceStructuredAgentOutput(content) {
2055
2066
  return { result: raw, outParams: {}, structured: false, parsed: null };
2056
2067
  }
2057
2068
  const hasEnvelope = Object.prototype.hasOwnProperty.call(parsed, "result") ||
2069
+ Object.prototype.hasOwnProperty.call(parsed, "resultFile") ||
2058
2070
  Object.prototype.hasOwnProperty.call(parsed, "outParams");
2059
2071
  if (!hasEnvelope) return { result: raw, outParams: {}, structured: false, parsed };
2060
2072
  const outParamsRaw = parsed.outParams && typeof parsed.outParams === "object" && !Array.isArray(parsed.outParams)
@@ -2195,6 +2207,57 @@ function workspaceDisplayTextFilePath(value, kind = "") {
2195
2207
  return allowed.has(ext) ? clean : "";
2196
2208
  }
2197
2209
 
2210
+ function workspaceSafeNodeOutputRelPath(value) {
2211
+ const text = String(value || "").trim().replace(/^["']|["']$/g, "");
2212
+ if (!text || text.length > 260) return "";
2213
+ if (/[\r\n<>]/.test(text)) return "";
2214
+ if (/^(?:https?:|data:|blob:|file:|javascript:|mailto:|tel:)/i.test(text)) return "";
2215
+ const clean = text.replace(/^\/+/, "");
2216
+ if (clean.includes("..") || clean.startsWith(".") || path.isAbsolute(clean)) return "";
2217
+ if (!clean.startsWith("outputs/")) return "";
2218
+ return clean;
2219
+ }
2220
+
2221
+ function workspacePublishNodeOutputFile(runPackage, relPath) {
2222
+ if (!String(relPath || "").trim()) return "";
2223
+ const clean = workspaceSafeNodeOutputRelPath(relPath);
2224
+ if (!clean) throw new Error(`Agent returned an invalid output file path: ${String(relPath || "").trim()}`);
2225
+ const nodeRunDir = path.resolve(runPackage?.nodeRunDir || "");
2226
+ const workspaceOutputsDir = path.resolve(runPackage?.workspaceOutputsDir || "");
2227
+ if (!nodeRunDir || !workspaceOutputsDir) return clean;
2228
+ const src = path.resolve(nodeRunDir, clean);
2229
+ const nodeRootWithSep = nodeRunDir.endsWith(path.sep) ? nodeRunDir : `${nodeRunDir}${path.sep}`;
2230
+ if (src !== nodeRunDir && !src.startsWith(nodeRootWithSep)) {
2231
+ throw new Error(`Invalid node output path: ${clean}`);
2232
+ }
2233
+ if (!fs.existsSync(src) || !fs.statSync(src).isFile()) {
2234
+ throw new Error(`Agent returned resultFile but did not create it under node outputs: ${clean}`);
2235
+ }
2236
+ const destRel = clean.slice("outputs/".length);
2237
+ const dest = path.resolve(workspaceOutputsDir, destRel);
2238
+ const workspaceOutputsWithSep = workspaceOutputsDir.endsWith(path.sep) ? workspaceOutputsDir : `${workspaceOutputsDir}${path.sep}`;
2239
+ if (dest !== workspaceOutputsDir && !dest.startsWith(workspaceOutputsWithSep)) {
2240
+ throw new Error(`Invalid workspace output path: ${clean}`);
2241
+ }
2242
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
2243
+ fs.copyFileSync(src, dest);
2244
+ return clean;
2245
+ }
2246
+
2247
+ function workspacePublishAgentOutputFiles(structured, runPackage) {
2248
+ if (!structured?.structured || !runPackage) return structured;
2249
+ const resultFile = workspacePublishNodeOutputFile(runPackage, structured.resultFile);
2250
+ const outParams = { ...(structured.outParams || {}) };
2251
+ for (const [key, value] of Object.entries(outParams)) {
2252
+ if (!String(key || "").endsWith("File")) continue;
2253
+ const published = workspacePublishNodeOutputFile(runPackage, value);
2254
+ if (published) outParams[key] = published;
2255
+ }
2256
+ return resultFile || Object.keys(outParams).length
2257
+ ? { ...structured, result: resultFile || structured.result, resultFile: resultFile || structured.resultFile, outParams }
2258
+ : structured;
2259
+ }
2260
+
2198
2261
  function workspaceOutputFieldForSlot(slot, index = 0) {
2199
2262
  const name = String(slot?.name || "").trim();
2200
2263
  if (!name || name === "result" || name === "content" || index === 0) return "result";
@@ -2212,19 +2275,6 @@ function workspaceDisplayKindExample(kind, field) {
2212
2275
  return `<${field} 的值>`;
2213
2276
  }
2214
2277
 
2215
- function workspaceDisplayFieldRule(kind, field, slotName = "") {
2216
- const slotText = field === "result" ? "`result` 或 `resultFile`" : `\`${field}\` 或 \`${field}File\``;
2217
- const prefix = slotName ? `- 输出引脚 \`${slotName}\` 连接了 ${kind} 展示节点:` : `- 下游连接了 ${kind} 展示节点:`;
2218
- if (kind === "html") return `${prefix}将可直接放入 iframe 渲染的 HTML 放在 ${slotText} 中;内容较长时优先写入 outputs/*.html 并返回文件路径。不要使用 Markdown 代码围栏。`;
2219
- if (kind === "markdown") return `${prefix}将 Markdown 正文放在 ${slotText} 中;内容较长时优先写入 outputs/*.md 并返回文件路径。除非正文确实需要代码块,否则不要额外包裹代码围栏。`;
2220
- if (kind === "mermaid") return `${prefix}将 Mermaid 图表代码放在 ${slotText} 中,例如 flowchart/sequenceDiagram;不要使用 Markdown 代码围栏。`;
2221
- if (kind === "ascii") return `${prefix}将纯文本/ASCII 图或表格放在 ${slotText} 中;不要输出 HTML 或 Markdown 装饰。`;
2222
- if (kind === "image") return `${prefix}将可作为 img src 使用的图片地址、data URL 或 base64 data URL 放在 ${slotText} 中;不要输出 Markdown 图片语法。`;
2223
- if (kind === "chart") return `${prefix}将 ChartSpec JSON 对象放在 ${slotText} 中。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 函数。`;
2224
- if (kind === "table") return `${prefix}将表格数据放在 ${slotText} 中。推荐格式:{"columns":["列名1","列名2"],"rows":[["值1","值2"]]};也可使用对象数组、Markdown 表格、CSV 或 TSV。不要输出 HTML。`;
2225
- return `${prefix}将展示内容放在 ${slotText} 中。`;
2226
- }
2227
-
2228
2278
  function workspaceDownstreamOutputDisplayBindings(graph, nodeId) {
2229
2279
  const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
2230
2280
  const edges = Array.isArray(graph?.edges) ? graph.edges : [];
@@ -2284,45 +2334,24 @@ function normalizeHtmlDisplayContent(content) {
2284
2334
  return text;
2285
2335
  }
2286
2336
 
2287
- function workspaceDownstreamDisplayRequirements(graph, nodeId) {
2288
- const bindings = workspaceDownstreamOutputDisplayBindings(graph, nodeId);
2289
- if (bindings.length === 0) return "";
2290
- const seen = new Set();
2291
- const rules = [];
2292
- for (const binding of bindings) {
2293
- const key = `${binding.field}:${binding.kind}`;
2294
- if (seen.has(key)) continue;
2295
- seen.add(key);
2296
- rules.push(workspaceDisplayFieldRule(binding.kind, binding.field, binding.name));
2297
- }
2298
- return [
2299
- "## 下游输出要求",
2300
- "",
2301
- ...rules,
2302
- "",
2303
- "这些要求按输出引脚分别生效:不要把非 `result` 引脚连接的展示内容误写到 `result`;具名引脚应写入 `outParams.<引脚名>`。",
2304
- "如果用户任务与下游展示格式没有冲突,优先满足上述格式要求;如果用户明确指定了其他格式,以用户任务为准。",
2305
- ].join("\n");
2337
+ function workspaceBodyPlaceholderNames(body) {
2338
+ const names = new Set();
2339
+ const raw = String(body || "");
2340
+ raw.replace(/\$\{([A-Za-z_][A-Za-z0-9_-]*)\}/g, (_match, name) => {
2341
+ if (name) names.add(String(name));
2342
+ return _match;
2343
+ });
2344
+ return names;
2306
2345
  }
2307
2346
 
2308
- function workspaceNodeScopeGuardrails(graph, nodeId, inputValues = {}) {
2309
- const edges = Array.isArray(graph?.edges) ? graph.edges : [];
2310
- const directInputEdges = edges
2311
- .filter((edge) => String(edge?.target || "") === String(nodeId))
2312
- .filter((edge) => !isWorkspaceSemanticInputSlot(workspaceTargetSlotForEdge(graph, edge)));
2313
- const upstreamNodeIds = Array.from(new Set(directInputEdges.map((edge) => String(edge?.source || "").trim()).filter(Boolean)));
2314
- const inputNames = Object.keys(inputValues || {}).filter(Boolean);
2315
- return [
2316
- "## 当前节点上下文边界",
2317
- "",
2318
- "你只负责执行当前节点;`上游上下文`、已解析输入槽和节点任务就是本节点的业务上下文边界。",
2319
- upstreamNodeIds.length ? `当前直接上游节点:${upstreamNodeIds.map((id) => `\`${id}\``).join("、")}。` : "当前没有直接业务上游节点。",
2320
- inputNames.length ? `当前已解析输入槽:${inputNames.map((name) => `\`${name}\``).join("、")}。` : "当前没有已解析的具名业务输入槽。",
2321
- "不要为了理解本节点而读取或搜索整张 workspace、`workspace.graph.json`、正式 `flow.yaml`、历史 run/log 或其它未连接节点。",
2322
- "不要把下游展示节点已有内容、下游错误信息、其它分支节点内容当作本节点输入;下游输出要求只用于决定输出字段和格式。",
2323
- "只有当当前节点任务文本或上游输入明确要求读取/修改 `workspace.graph.json`、`flow.yaml` 或某个具体文件路径时,才可以打开对应文件。",
2324
- "如果完成任务所需信息不在当前节点任务、输入槽或上游上下文中,应明确说明缺少哪个上游输入,而不是扫描整张 workspace 猜测。",
2325
- ].join("\n");
2347
+ function workspaceRelevantInputValues(body, inputValues = {}) {
2348
+ const placeholders = workspaceBodyPlaceholderNames(body);
2349
+ if (!placeholders.size) return { values: inputValues || {}, placeholders };
2350
+ const values = {};
2351
+ for (const [name, value] of Object.entries(inputValues || {})) {
2352
+ if (placeholders.has(name)) values[name] = value;
2353
+ }
2354
+ return { values, placeholders };
2326
2355
  }
2327
2356
 
2328
2357
  function workspaceOutputProtocolRequirements(graph, nodeId) {
@@ -2340,39 +2369,46 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
2340
2369
  return name && type !== "node" && name !== "next" && name !== "result" && name !== "content";
2341
2370
  })
2342
2371
  .map((slot) => String(slot.name).trim());
2343
- const outParamsExample = slots.length
2344
- ? Object.fromEntries(slots.map((name) => [name, workspaceDisplayKindExample(displayByField.get(`outParams.${name}`) || "", name)]))
2345
- : {};
2346
- const resultExample = workspaceDisplayKindExample(displayByField.get("result") || "markdown", "result");
2347
- const slotDisplayRules = displayBindings
2348
- .map((binding) => `- 输出引脚 \`${binding.name}\` -> ${binding.kind} 展示节点:写入 \`${binding.field}\`。`)
2349
- .filter((line, index, arr) => arr.indexOf(line) === index);
2372
+ const resultKind = displayByField.get("result") || "";
2373
+ const resultExtByKind = {
2374
+ html: "html",
2375
+ markdown: "md",
2376
+ mermaid: "mmd",
2377
+ ascii: "txt",
2378
+ chart: "json",
2379
+ table: "json",
2380
+ };
2381
+ const resultExt = resultExtByKind[resultKind] || "txt";
2382
+ const resultFile = `outputs/result.${resultExt}`;
2383
+ const resultKindText = resultKind ? ` ${resultKind}` : "";
2384
+ const resultGuidance = {
2385
+ html: "内容必须是可直接放入 iframe 渲染的 HTML;不要使用 Markdown 代码围栏。",
2386
+ markdown: "内容必须是 Markdown 正文;除非正文确实需要代码块,否则不要额外包裹代码围栏。",
2387
+ mermaid: "内容必须是 Mermaid 图表代码,例如 flowchart/sequenceDiagram;不要使用 Markdown 代码围栏。",
2388
+ ascii: "内容必须是纯文本/ASCII 图或表格;不要输出 HTML 或 Markdown 装饰。",
2389
+ image: "内容必须是可作为 img src 使用的图片地址、data URL 或 base64 data URL;不要输出 Markdown 图片语法。",
2390
+ chart: "内容必须是 ChartSpec JSON 对象,包含 type/version/renderer/option;不要输出 HTML、script、iframe 或 JS 函数。",
2391
+ table: "内容必须是表格数据,推荐 JSON:{\"columns\":[...],\"rows\":[...]};不要输出 HTML。",
2392
+ }[resultKind] || "内容应满足任务要求。";
2350
2393
  const envelopeExample = [
2351
2394
  "---agentflow",
2352
- "resultFile: outputs/result.html",
2395
+ `resultFile: ${resultFile}`,
2353
2396
  slots.length ? "outParams:" : "",
2354
- ...slots.slice(0, 3).map((name) => ` ${name}: <${name} 的短值>`),
2397
+ ...slots.slice(0, 3).map((name) => {
2398
+ const kind = displayByField.get(`outParams.${name}`) || "";
2399
+ const ext = resultExtByKind[kind] || "txt";
2400
+ return kind ? ` ${name}File: outputs/${name}.${ext}` : ` ${name}: <${name} 的短值>`;
2401
+ }),
2355
2402
  "---end",
2356
2403
  ].filter(Boolean).join("\n");
2357
2404
  return [
2358
- "## Workspace 输出协议",
2405
+ "## 输出",
2359
2406
  "",
2360
- "如果只有一个默认输出,直接输出正文即可,系统会写入 `result` / `content` 输出口。",
2361
- "如果需要多个输出,或需要返回文件路径,最后输出一个轻量 envelope;不要把大段 HTML/Markdown/SQL 包进 JSON 字符串。",
2362
- "长内容、HTML、Markdown、SQL、CSV、图片等 artifact 优先写入当前 workspace 的 `outputs/` 目录,然后在 `resultFile` 或 `outParams.<name>File` 返回相对路径。",
2363
- "普通 assistant 文本会被当作本节点输出内容;执行过程中不要发送进度说明、解释或寒暄,例如“正在读取/正在生成/准备输出”。需要思考时只使用内部 thinking,最终只发送正文或 envelope。",
2364
- "envelope 格式:",
2407
+ `请把${resultKindText}结果写入 \`${resultFile}\`。${resultGuidance}`,
2408
+ ...(slots.length ? [`额外输出:${slots.map((name) => `\`${name}\``).join("、")}。短值可写在 \`outParams\`,文件值写成 \`outParams.<name>File\`。`] : []),
2409
+ "最终只输出下面的 agentflow envelope,不要输出解释、进度或其它文字:",
2365
2410
  "",
2366
2411
  envelopeExample,
2367
- "",
2368
- "- `result`:默认输出正文;`resultFile`:默认输出对应的 workspace 相对文件路径。",
2369
- "- `outParams`:具名输出参数;`outParams.<name>File`:具名输出对应的 workspace 相对文件路径。",
2370
- "- 旧格式 `{ \"result\": ..., \"outParams\": ... }` 仍兼容,但新输出优先使用正文或 envelope。",
2371
- ...(slotDisplayRules.length ? ["- 当前输出引脚与展示节点映射:", ...slotDisplayRules] : []),
2372
- ...(slots.length
2373
- ? [`- 当前节点具名输出槽:${slots.map((name) => `\`${name}\``).join("、")}。例如任务要求写入 \`${slots[0]}\` 时,放到 \`outParams.${slots[0]}\`;如果写成文件,放到 \`outParams.${slots[0]}File\`。`]
2374
- : ["- 当前节点没有额外具名输出槽;只有默认输出时不需要 envelope。"]),
2375
- "- 如果 `result` 需要承载表格、ChartSpec 等短结构化内容,可以直接输出对象或正文;系统会转换给下游展示节点。",
2376
2412
  ].join("\n");
2377
2413
  }
2378
2414
 
@@ -2458,79 +2494,44 @@ function isWorkspaceSemanticInputSlot(slot) {
2458
2494
  return type === "node" || name === "prev" || name === "next" || name === "skillsContext" || name === "mcpContext" || name === "workspaceContext" || name === "gitContext";
2459
2495
  }
2460
2496
 
2461
- function workspaceNodeBrief(graph, nodeId) {
2462
- const instance = graph?.instances?.[String(nodeId || "")] || {};
2463
- const label = String(instance?.label || nodeId || "").trim();
2464
- const defId = String(instance?.definitionId || "").trim();
2465
- return defId && defId !== label ? `${label || nodeId} (${defId})` : (label || nodeId);
2466
- }
2467
-
2468
- function workspaceSlotBrief(slot, fallback) {
2469
- const name = String(slot?.name || "").trim();
2470
- const type = String(slot?.type || "").trim();
2471
- return `${name || fallback}${type ? `:${type}` : ""}`;
2472
- }
2473
-
2474
- function workspaceNodeConnectionContextBlock(graph, nodeId) {
2475
- const edges = Array.isArray(graph?.edges) ? graph.edges : [];
2476
- const incoming = edges.filter((edge) => String(edge?.target || "") === String(nodeId));
2477
- const outgoing = edges.filter((edge) => String(edge?.source || "") === String(nodeId));
2478
- const inputLines = incoming.map((edge) => {
2479
- const sourceSlot = workspaceSourceSlotForEdge(graph, edge);
2480
- const targetSlot = workspaceTargetSlotForEdge(graph, edge);
2481
- const targetName = String(targetSlot?.name || "").trim();
2482
- const semantic = isWorkspaceSemanticInputSlot(targetSlot) ? "上下文输入" : "业务输入";
2483
- return `- ${workspaceNodeBrief(graph, edge.source)} \`${workspaceSlotBrief(sourceSlot, edge.sourceHandle || "output")}\` -> 当前 \`${workspaceSlotBrief(targetSlot, edge.targetHandle || "input")}\`(${semantic}${targetName ? `:${targetName}` : ""})`;
2484
- });
2485
- const outputLines = outgoing.map((edge) => {
2486
- const sourceSlot = workspaceSourceSlotForEdge(graph, edge);
2487
- const targetSlot = workspaceTargetSlotForEdge(graph, edge);
2488
- const targetKind = workspaceDisplayKind(graph?.instances?.[String(edge?.target || "")]?.definitionId);
2489
- return `- 当前 \`${workspaceSlotBrief(sourceSlot, edge.sourceHandle || "output")}\` -> ${workspaceNodeBrief(graph, edge.target)} \`${workspaceSlotBrief(targetSlot, edge.targetHandle || "input")}\`${targetKind ? `(${targetKind} 展示)` : ""}`;
2490
- });
2491
- if (!inputLines.length && !outputLines.length) return "";
2492
- return [
2493
- "## 当前节点连线",
2494
- "",
2495
- inputLines.length ? "### 直接上游" : "",
2496
- ...inputLines,
2497
- outputLines.length ? "\n### 直接下游" : "",
2498
- ...outputLines,
2499
- "",
2500
- "只把直接上游的业务输入和上下文输入当作本节点依据;直接下游只用于确定输出字段和格式。",
2501
- ].filter(Boolean).join("\n");
2502
- }
2503
-
2504
- function workspaceResolvedInputValuesBlock(inputValues = {}) {
2497
+ function workspaceAgentInputBlock(inputValues = {}) {
2505
2498
  const entries = Object.entries(inputValues || {}).filter(([name, value]) => String(name || "").trim() && String(value || "").trim());
2506
- if (!entries.length) return "";
2499
+ if (!entries.length) return "## 输入\n\n无。";
2507
2500
  const lines = entries.map(([name, value]) => {
2508
2501
  const text = String(value || "");
2509
2502
  const clipped = text.length > 6000 ? `${text.slice(0, 6000)}\n...[已截断 ${text.length - 6000} 字]` : text;
2510
2503
  return `### ${name}\n\n${clipped}`;
2511
2504
  });
2512
- return ["## 已解析业务输入槽", "", ...lines].join("\n");
2505
+ return ["## 输入", "", ...lines].join("\n");
2513
2506
  }
2514
2507
 
2515
- function workspaceNodeTmpDirectoryBlock(nodeTmpDir) {
2516
- const dir = String(nodeTmpDir || "").trim();
2517
- if (!dir) return "";
2508
+ function workspaceNodeFileBoundaryBlock(runPackage = {}) {
2509
+ const nodeRunDir = String(runPackage?.nodeRunDir || "").trim();
2510
+ const nodeTmpDir = String(runPackage?.nodeTmpDir || "").trim();
2511
+ const outputsRel = String(runPackage?.outputsRel || "outputs").trim() || "outputs";
2512
+ if (!nodeRunDir && !nodeTmpDir) return "";
2518
2513
  return [
2519
- "## 临时文件目录",
2514
+ "## 文件边界",
2520
2515
  "",
2521
- `- 本节点专用临时目录:\`${dir}\``,
2522
- "- 如果确实需要创建中间文件,只能写入该目录,路径也可通过环境变量 `AGENTFLOW_NODE_TMP_DIR` 获取。",
2523
- "- 不要在 workspace 根目录、业务仓库根目录或当前 cwd 下创建 `temp_*`、`_out.json`、`tmp.html` 等临时产物。",
2524
- "- 不要自行删除该目录或其中的最终待读文件;AgentFlow 会在节点运行结束后统一清理。",
2525
- "- 短文本结果可直接输出;长 HTML/Markdown/SQL 等正式 artifact 应写入 workspace 的 `outputs/` 目录并返回相对路径,不要为了输出而写临时文件、cat 文件再粘贴。",
2526
- ].join("\n");
2516
+ nodeRunDir ? `- 当前执行目录:\`${nodeRunDir}\`。` : "",
2517
+ nodeTmpDir ? `- 临时文件只能写入:\`${nodeTmpDir}\`,也可通过环境变量 \`AGENTFLOW_NODE_TMP_DIR\` 获取。` : "",
2518
+ `- 正式产物写入本任务 \`${outputsRel}/\`,例如 \`${outputsRel}/result.html\`、\`${outputsRel}/result.md\`;返回时仍使用 \`${outputsRel}/...\` 相对路径。`,
2519
+ "- 不要在执行目录根部创建 `temp_*`、`_out.json`、`tmp.html` 等临时产物。",
2520
+ "- 不要自行删除 run package;AgentFlow 会在运行结束后统一清理。",
2521
+ ].filter(Boolean).join("\n");
2527
2522
  }
2528
2523
 
2529
- function workspaceTaskUpstreamText(graph, nodeId, outputs) {
2524
+ function workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputNames = null) {
2530
2525
  const edges = Array.isArray(graph?.edges) ? graph.edges : [];
2531
2526
  const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
2532
2527
  const incoming = edges.filter((edge) => String(edge?.target || "") === String(nodeId));
2533
- const contentEdges = incoming.filter((edge) => !isWorkspaceSemanticInputSlot(workspaceTargetSlotForEdge(graph, edge)));
2528
+ let contentEdges = incoming.filter((edge) => !isWorkspaceSemanticInputSlot(workspaceTargetSlotForEdge(graph, edge)));
2529
+ if (relevantInputNames && relevantInputNames.size) {
2530
+ contentEdges = contentEdges.filter((edge) => {
2531
+ const slot = workspaceTargetSlotForEdge(graph, edge);
2532
+ return relevantInputNames.has(String(slot?.name || "").trim());
2533
+ });
2534
+ }
2534
2535
  const contentEdge = contentEdges.find((edge) => String(edge?.targetHandle || "") === "input-1") || contentEdges[0];
2535
2536
  if (!contentEdge) return "";
2536
2537
  return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge);
@@ -2744,28 +2745,21 @@ function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null)
2744
2745
  function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock = "", inputValues = {}, nodeTmpDir = "") {
2745
2746
  const instance = graph.instances[nodeId] || {};
2746
2747
  const body = workspaceResolveBodyPlaceholders(instance.body || "", inputValues).trim();
2747
- const label = String(instance.label || nodeId).trim();
2748
- const scopeGuardrails = workspaceNodeScopeGuardrails(graph, nodeId, inputValues);
2749
- const connectionContext = workspaceNodeConnectionContextBlock(graph, nodeId);
2750
- const resolvedInputs = workspaceResolvedInputValuesBlock(inputValues);
2751
- const tmpDirectory = workspaceNodeTmpDirectoryBlock(nodeTmpDir);
2752
- const downstreamRequirements = workspaceDownstreamDisplayRequirements(graph, nodeId);
2748
+ const { values: relevantInputValues, placeholders } = workspaceRelevantInputValues(instance.body || "", inputValues);
2749
+ const runPackage = typeof nodeTmpDir === "object" && nodeTmpDir ? nodeTmpDir : { nodeTmpDir: String(nodeTmpDir || "") };
2750
+ const inputBlock = workspaceAgentInputBlock(relevantInputValues);
2751
+ const fileBoundary = workspaceNodeFileBoundaryBlock(runPackage);
2753
2752
  const outputProtocolRequirements = workspaceOutputProtocolRequirements(graph, nodeId);
2754
2753
  return [
2755
- "你正在执行 AgentFlow Workspace 画布中的一个临时节点。",
2756
- "按 Workspace 输出协议返回该节点要传给下游展示/后续节点的数据。",
2757
- scopeGuardrails,
2758
- connectionContext ? `\n${connectionContext}` : "",
2759
- tmpDirectory ? `\n${tmpDirectory}` : "",
2760
- workspaceSearchGuardrailsBlock(),
2761
- resolvedInputs ? `\n${resolvedInputs}` : "",
2762
- skillsBlock ? `\n## 上游已加载 Skills\n\n${skillsBlock}` : "",
2763
- mcpBlock ? `\n## 上游已加载 MCP\n\n${mcpBlock}` : "",
2764
- upstreamText ? `\n## 上游上下文\n\n${upstreamText}` : "",
2765
- downstreamRequirements ? `\n${downstreamRequirements}` : "",
2754
+ "你正在执行一个独立任务。只使用本提示中的任务、输入、可用能力和文件边界。",
2755
+ fileBoundary ? `\n${fileBoundary}` : "",
2756
+ inputBlock ? `\n${inputBlock}` : "",
2757
+ placeholders.size ? "\n任务只显式引用了上面的输入槽;其它未被 `${...}` 引用的已连接业务输入不要作为分析依据。" : "",
2758
+ skillsBlock ? `\n## 可用能力\n\n${skillsBlock}` : "",
2759
+ mcpBlock ? `\n## 可用 MCP\n\n${mcpBlock}` : "",
2760
+ upstreamText ? `\n## 上游正文\n\n${upstreamText}` : "",
2766
2761
  outputProtocolRequirements ? `\n${outputProtocolRequirements}` : "",
2767
- `\n## 当前节点\n\n- id: ${nodeId}\n- label: ${label}\n- definitionId: ${instance.definitionId || ""}`,
2768
- `\n## 节点任务\n\n${body || upstreamText}`,
2762
+ `\n## 任务\n\n${body || upstreamText}`,
2769
2763
  ].filter(Boolean).join("\n");
2770
2764
  }
2771
2765
 
@@ -2857,6 +2851,40 @@ function workspaceCreateNodeTmpDir(runTmpRoot, nodeId) {
2857
2851
  return dir;
2858
2852
  }
2859
2853
 
2854
+ function workspaceCreateNodeRunPackage(runTmpRoot, nodeId, { scopedRoot, task = "", inputValues = {}, skillsBlock = "", mcpBlock = "" } = {}) {
2855
+ const nodeRunDir = workspaceCreateNodeTmpDir(runTmpRoot, nodeId);
2856
+ const nodeTmpDir = path.join(nodeRunDir, "tmp");
2857
+ const outputsDir = path.join(nodeRunDir, "outputs");
2858
+ const workspaceOutputsDir = path.join(path.resolve(scopedRoot), "outputs");
2859
+ fs.mkdirSync(nodeTmpDir, { recursive: true });
2860
+ fs.mkdirSync(outputsDir, { recursive: true });
2861
+ fs.mkdirSync(workspaceOutputsDir, { recursive: true });
2862
+ const manifest = {
2863
+ version: 1,
2864
+ nodeId: String(nodeId || ""),
2865
+ nodeRunDir,
2866
+ nodeTmpDir,
2867
+ outputsDir,
2868
+ createdAt: new Date().toISOString(),
2869
+ };
2870
+ try {
2871
+ fs.writeFileSync(path.join(nodeRunDir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n", "utf-8");
2872
+ fs.writeFileSync(path.join(nodeRunDir, "task.md"), String(task || "").trimEnd() + "\n", "utf-8");
2873
+ if (Object.keys(inputValues || {}).length) {
2874
+ fs.writeFileSync(path.join(nodeRunDir, "inputs.json"), JSON.stringify(inputValues, null, 2) + "\n", "utf-8");
2875
+ }
2876
+ if (skillsBlock) fs.writeFileSync(path.join(nodeRunDir, "skills.md"), String(skillsBlock).trimEnd() + "\n", "utf-8");
2877
+ if (mcpBlock) fs.writeFileSync(path.join(nodeRunDir, "mcp.md"), String(mcpBlock).trimEnd() + "\n", "utf-8");
2878
+ } catch {
2879
+ // Runtime metadata is best-effort and should not block node execution.
2880
+ }
2881
+ return {
2882
+ ...manifest,
2883
+ workspaceOutputsDir,
2884
+ outputsRel: "outputs",
2885
+ };
2886
+ }
2887
+
2860
2888
  function workspaceShouldKeepTmp(userCtx = {}) {
2861
2889
  const env = { ...process.env, ...readMergedEnvObject(userCtx.userId) };
2862
2890
  const value = String(env.AGENTFLOW_KEEP_TMP || env.AGENTFLOW_KEEP_WORKSPACE_TMP || "").trim().toLowerCase();
@@ -2921,9 +2949,16 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
2921
2949
  };
2922
2950
  const outputs = new Map();
2923
2951
  const events = [];
2952
+ const runStartedAt = Date.now();
2924
2953
  const emit = (event) => {
2925
- events.push(event);
2926
- if (typeof opts.onEvent === "function") opts.onEvent(event);
2954
+ const now = Date.now();
2955
+ const enriched = {
2956
+ ...event,
2957
+ ts: Number(event?.ts) || now,
2958
+ runElapsedMs: Number.isFinite(event?.runElapsedMs) ? event.runElapsedMs : Math.max(0, now - runStartedAt),
2959
+ };
2960
+ events.push(enriched);
2961
+ if (typeof opts.onEvent === "function") opts.onEvent(enriched);
2927
2962
  };
2928
2963
  const emitTiming = (nodeId, label, startedAt, extra = {}) => {
2929
2964
  const elapsedMs = Math.max(0, Date.now() - startedAt);
@@ -3233,8 +3268,9 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
3233
3268
  }
3234
3269
 
3235
3270
  const prepareStartedAt = Date.now();
3236
- const upstreamText = workspaceTaskUpstreamText(graph, nodeId, outputs);
3237
3271
  const inputValues = workspaceInputValues(graph, nodeId, outputs);
3272
+ const relevantInputs = workspaceRelevantInputValues(instance.body || "", inputValues);
3273
+ const upstreamText = workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputs.placeholders);
3238
3274
  const body = workspaceResolveBodyPlaceholders(instance.body || "", inputValues).trim();
3239
3275
  if (defId === "agent_subAgent" && !body && !String(upstreamText || "").trim()) {
3240
3276
  throw new Error(`Workspace node ${nodeId} has no task. Fill the node body or connect upstream text.`);
@@ -3242,9 +3278,27 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
3242
3278
  const upstreamSkillBlocks = workspaceUpstreamSkillBlocks(graph, nodeId, outputs);
3243
3279
  const promptSkillsBlock = mergeWorkspaceSkillBlocks(upstreamSkillBlocks);
3244
3280
  const promptMcpBlock = workspaceUpstreamMcpBlocks(graph, nodeId, outputs);
3245
- const nodeTmpDir = workspaceCreateNodeTmpDir(runTmpRoot, nodeId);
3246
- const prompt = workspaceNodePrompt(graph, nodeId, upstreamText, promptSkillsBlock, promptMcpBlock, inputValues, nodeTmpDir);
3247
- emitTiming(nodeId, "prepare-agent-prompt", prepareStartedAt, { promptChars: prompt.length, upstreamChars: String(upstreamText || "").length, skillsChars: promptSkillsBlock.length, mcpChars: promptMcpBlock.length });
3281
+ const runPackage = workspaceCreateNodeRunPackage(runTmpRoot, nodeId, {
3282
+ scopedRoot,
3283
+ cwd,
3284
+ task: body || upstreamText,
3285
+ inputValues: relevantInputs.values,
3286
+ skillsBlock: promptSkillsBlock,
3287
+ mcpBlock: promptMcpBlock,
3288
+ });
3289
+ const prompt = workspaceNodePrompt(graph, nodeId, upstreamText, promptSkillsBlock, promptMcpBlock, inputValues, runPackage);
3290
+ try {
3291
+ fs.writeFileSync(path.join(runPackage.nodeRunDir, "prompt.md"), prompt.trimEnd() + "\n", "utf-8");
3292
+ } catch {
3293
+ // Best-effort debug artifact only.
3294
+ }
3295
+ emitTiming(nodeId, "prepare-agent-prompt", prepareStartedAt, {
3296
+ promptChars: prompt.length,
3297
+ upstreamChars: String(upstreamText || "").length,
3298
+ skillsChars: promptSkillsBlock.length,
3299
+ mcpChars: promptMcpBlock.length,
3300
+ nodeRunDir: runPackage.nodeRunDir,
3301
+ });
3248
3302
  emit({ type: "natural", kind: "prompt", nodeId, text: prompt });
3249
3303
  let content = "";
3250
3304
  const maxAttempts = 3;
@@ -3253,15 +3307,19 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
3253
3307
  try {
3254
3308
  const spawnStartedAt = Date.now();
3255
3309
  let firstAgentEventSeen = false;
3310
+ let attemptResultContent = "";
3311
+ let attemptLastAssistantContent = "";
3256
3312
  const handle = startComposerAgent({
3257
3313
  uiWorkspaceRoot: scopedRoot,
3258
- cliWorkspace: cwd,
3314
+ cliWorkspace: runPackage.nodeRunDir,
3259
3315
  prompt,
3260
3316
  modelKey,
3261
3317
  agentflowUserId: userCtx.userId || "",
3262
3318
  extraEnv: {
3263
3319
  AGENTFLOW_WORKSPACE_TMP_ROOT: runTmpRoot,
3264
- AGENTFLOW_NODE_TMP_DIR: nodeTmpDir,
3320
+ AGENTFLOW_NODE_RUN_DIR: runPackage.nodeRunDir,
3321
+ AGENTFLOW_NODE_TMP_DIR: runPackage.nodeTmpDir,
3322
+ AGENTFLOW_OUTPUTS_DIR: runPackage.outputsDir,
3265
3323
  },
3266
3324
  onStreamEvent: (ev) => {
3267
3325
  if (!firstAgentEventSeen) {
@@ -3273,7 +3331,10 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
3273
3331
  : { ...ev, nodeId };
3274
3332
  emit(eventToEmit);
3275
3333
  if (ev?.type === "natural" && ev.kind === "assistant" && typeof ev.text === "string") {
3334
+ attemptLastAssistantContent = ev.text;
3276
3335
  attemptContent += (attemptContent ? "\n" : "") + ev.text;
3336
+ } else if (ev?.type === "natural" && ev.kind === "result" && typeof ev.text === "string") {
3337
+ attemptResultContent = ev.text;
3277
3338
  }
3278
3339
  },
3279
3340
  onToolCall: (subtype, toolName) => {
@@ -3290,7 +3351,11 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
3290
3351
  if (typeof opts.onActiveChild === "function") opts.onActiveChild(null);
3291
3352
  }
3292
3353
  throwIfAborted();
3293
- content = workspaceCanonicalAgentOutput(attemptContent);
3354
+ const resultStructured = attemptResultContent ? workspaceStructuredAgentOutput(attemptResultContent) : null;
3355
+ const assistantStructured = attemptLastAssistantContent ? workspaceStructuredAgentOutput(attemptLastAssistantContent) : null;
3356
+ if (resultStructured?.structured) content = workspaceCanonicalAgentOutput(attemptResultContent);
3357
+ else if (assistantStructured?.structured) content = workspaceCanonicalAgentOutput(attemptLastAssistantContent);
3358
+ else content = workspaceCanonicalAgentOutput(attemptLastAssistantContent || attemptContent);
3294
3359
  break;
3295
3360
  } catch (e) {
3296
3361
  if (signal?.aborted || e?.code === "WORKSPACE_RUN_ABORTED") throwIfAborted();
@@ -3302,7 +3367,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
3302
3367
  throw e;
3303
3368
  }
3304
3369
  }
3305
- const normalizedAgentOutput = workspaceStructuredAgentOutput(content);
3370
+ const normalizedAgentOutput = workspacePublishAgentOutputFiles(workspaceStructuredAgentOutput(content), runPackage);
3306
3371
  const resultContent = normalizedAgentOutput.result || content;
3307
3372
  outputs.set(nodeId, resultContent);
3308
3373
  const slotUpdate = workspaceApplyAgentOutputSlots(instance, content);