@fieldwangai/agentflow 0.1.61 → 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.
@@ -10,8 +10,10 @@ import http from "http";
10
10
  import os from "os";
11
11
  import path from "path";
12
12
  import crypto from "crypto";
13
+ import { pathToFileURL } from "url";
13
14
  import { execFile, spawn } from "child_process";
14
15
  import busboy from "busboy";
16
+ import sharp from "sharp";
15
17
  import { log } from "./log.mjs";
16
18
  import {
17
19
  getFlowYamlAbs,
@@ -118,6 +120,25 @@ const MIME = {
118
120
  ".ico": "image/x-icon",
119
121
  };
120
122
 
123
+ function execFileBuffered(command, args, options = {}) {
124
+ return new Promise((resolve, reject) => {
125
+ execFile(command, args, {
126
+ timeout: Number(options.timeout || 30000),
127
+ maxBuffer: Number(options.maxBuffer || 2 * 1024 * 1024),
128
+ cwd: options.cwd || process.cwd(),
129
+ env: options.env || process.env,
130
+ }, (error, stdout, stderr) => {
131
+ if (error) {
132
+ error.stdout = stdout;
133
+ error.stderr = stderr;
134
+ reject(error);
135
+ return;
136
+ }
137
+ resolve({ stdout, stderr });
138
+ });
139
+ });
140
+ }
141
+
121
142
  const RUN_CONFIG_FILENAME = "run-config.json";
122
143
  const SKILL_COLLECTIONS_FILENAME = "skill-collections.json";
123
144
  const BUILTIN_SKILL_COLLECTIONS = [
@@ -1184,6 +1205,113 @@ function workspaceDownloadContentDisposition(relPath) {
1184
1205
  return `attachment; filename="${quotedName}"; filename*=UTF-8''${encodeURIComponent(fallbackName)}`;
1185
1206
  }
1186
1207
 
1208
+ function htmlEscapeAttribute(value) {
1209
+ return String(value || "")
1210
+ .replace(/&/g, "&")
1211
+ .replace(/"/g, """)
1212
+ .replace(/</g, "&lt;")
1213
+ .replace(/>/g, "&gt;");
1214
+ }
1215
+
1216
+ function fileUrlFromPath(absPath) {
1217
+ return pathToFileURL(path.resolve(absPath)).href;
1218
+ }
1219
+
1220
+ function injectHtmlBaseHref(html, baseHref) {
1221
+ const raw = String(html || "");
1222
+ const base = `<base href="${htmlEscapeAttribute(baseHref)}">`;
1223
+ if (/<base\b/i.test(raw)) return raw;
1224
+ if (/<head\b[^>]*>/i.test(raw)) return raw.replace(/<head\b([^>]*)>/i, `<head$1>${base}`);
1225
+ if (/<html\b[^>]*>/i.test(raw)) return raw.replace(/<html\b([^>]*)>/i, `<html$1><head>${base}</head>`);
1226
+ return `<!doctype html><html><head>${base}</head><body>${raw}</body></html>`;
1227
+ }
1228
+
1229
+ function chromeScreenshotCandidates() {
1230
+ const candidates = [];
1231
+ if (process.env.AGENTFLOW_CHROME_PATH) candidates.push(process.env.AGENTFLOW_CHROME_PATH);
1232
+ if (process.platform === "darwin") {
1233
+ candidates.push(
1234
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
1235
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
1236
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
1237
+ );
1238
+ } else if (process.platform === "win32") {
1239
+ candidates.push(
1240
+ path.join(process.env.PROGRAMFILES || "C:\\Program Files", "Google", "Chrome", "Application", "chrome.exe"),
1241
+ path.join(process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)", "Google", "Chrome", "Application", "chrome.exe"),
1242
+ path.join(process.env.LOCALAPPDATA || "", "Google", "Chrome", "Application", "chrome.exe"),
1243
+ );
1244
+ }
1245
+ candidates.push("google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "chrome", "msedge");
1246
+ return candidates.filter(Boolean);
1247
+ }
1248
+
1249
+ async function renderHtmlScreenshotWithChrome({ html, workspaceRoot, baseDir, width, height }) {
1250
+ const w = Math.max(240, Math.min(4096, Math.round(Number(width) || 390)));
1251
+ const h = Math.max(240, Math.min(12000, Math.round(Number(height) || 844)));
1252
+ const debug = {
1253
+ requestedWidth: Number(width) || null,
1254
+ requestedHeight: Number(height) || null,
1255
+ viewportWidth: w,
1256
+ viewportHeight: h,
1257
+ htmlChars: String(html || "").length,
1258
+ baseDir: path.resolve(baseDir || workspaceRoot),
1259
+ tried: [],
1260
+ usedCommand: "",
1261
+ pngBytes: 0,
1262
+ pngWidth: null,
1263
+ pngHeight: null,
1264
+ };
1265
+ const tmpDir = path.join(path.resolve(workspaceRoot), ".workspace", "agentflow", "tmp", `html-screenshot-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`);
1266
+ fs.mkdirSync(tmpDir, { recursive: true });
1267
+ const htmlPath = path.join(tmpDir, "snapshot.html");
1268
+ const pngPath = path.join(tmpDir, "snapshot.png");
1269
+ const baseHref = `${fileUrlFromPath(baseDir || workspaceRoot).replace(/\/?$/, "/")}`;
1270
+ fs.writeFileSync(htmlPath, injectHtmlBaseHref(html, baseHref), "utf-8");
1271
+ const args = [
1272
+ "--headless=new",
1273
+ "--disable-gpu",
1274
+ "--no-sandbox",
1275
+ "--disable-dev-shm-usage",
1276
+ "--no-first-run",
1277
+ "--no-default-browser-check",
1278
+ "--allow-file-access-from-files",
1279
+ "--hide-scrollbars",
1280
+ "--force-device-scale-factor=1",
1281
+ `--window-size=${w},${h}`,
1282
+ `--screenshot=${pngPath}`,
1283
+ fileUrlFromPath(htmlPath),
1284
+ ];
1285
+ let lastError = null;
1286
+ try {
1287
+ for (const command of chromeScreenshotCandidates()) {
1288
+ if (path.isAbsolute(command) && !fs.existsSync(command)) continue;
1289
+ debug.tried.push(command);
1290
+ try {
1291
+ await execFileBuffered(command, args, { timeout: 45000, cwd: workspaceRoot });
1292
+ if (fs.existsSync(pngPath) && fs.statSync(pngPath).size > 0) {
1293
+ const png = fs.readFileSync(pngPath);
1294
+ debug.usedCommand = command;
1295
+ debug.pngBytes = png.length;
1296
+ try {
1297
+ const meta = await sharp(png).metadata();
1298
+ debug.pngWidth = meta.width || null;
1299
+ debug.pngHeight = meta.height || null;
1300
+ } catch (_) {}
1301
+ return { png, debug };
1302
+ }
1303
+ lastError = new Error(`${command} did not produce a screenshot`);
1304
+ } catch (error) {
1305
+ lastError = error;
1306
+ debug.lastError = String(error?.message || error);
1307
+ }
1308
+ }
1309
+ throw new Error(`无法使用 Chrome 生成截图${lastError?.message ? `:${lastError.message}` : ""}`);
1310
+ } finally {
1311
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
1312
+ }
1313
+ }
1314
+
1187
1315
  const WORKSPACE_FILE_SKIP_REL_PREFIXES = [
1188
1316
  ".workspace/agentflow/worktrees",
1189
1317
  ".workspace/agentflow/git-repos",
@@ -1351,12 +1479,22 @@ function publicDisplayPayloadFromShare(root, share) {
1351
1479
  const nodes = nodeIds.map((id) => {
1352
1480
  const instance = instances[id] || {};
1353
1481
  const definitionId = String(instance.definitionId || "");
1482
+ const kind = workspaceDisplayKind(definitionId);
1483
+ const rawBody = String(instance.body || "");
1484
+ const filePath = workspaceDisplayTextFilePath(rawBody, kind);
1485
+ let body = rawBody;
1486
+ if (filePath) {
1487
+ const resolved = resolveWorkspaceFilePath(scoped.root, filePath);
1488
+ if (resolved.rel && fs.existsSync(resolved.abs) && fs.statSync(resolved.abs).isFile()) {
1489
+ body = fs.readFileSync(resolved.abs, "utf-8");
1490
+ }
1491
+ }
1354
1492
  return {
1355
1493
  id,
1356
1494
  definitionId,
1357
- kind: workspaceDisplayKind(definitionId),
1495
+ kind,
1358
1496
  label: String(instance.label || instance.displayName || id),
1359
- body: String(instance.body || ""),
1497
+ body,
1360
1498
  inputs: Array.isArray(instance.input) ? instance.input : [],
1361
1499
  outputs: Array.isArray(instance.output) ? instance.output : [],
1362
1500
  size: displayPageSizes[id] || workspaceSizes[id] || null,
@@ -1532,6 +1670,9 @@ function buildWorkspaceNodeChatPrompt(payload) {
1532
1670
  const userMessage = String(payload?.message || "").trim();
1533
1671
  const currentContent = String(payload?.currentContent || "").trim();
1534
1672
  const nodeKind = String(payload?.nodeKind || payload?.kind || "markdown").trim().toLowerCase();
1673
+ const sourceContext = String(payload?.sourceContext || "").trim();
1674
+ const targetFilePath = String(payload?.targetFilePath || "").trim();
1675
+ const directFileEdit = Boolean(targetFilePath);
1535
1676
  const history = Array.isArray(payload?.messages) ? payload.messages : [];
1536
1677
  const historyBlock = history
1537
1678
  .slice(-8)
@@ -1542,8 +1683,14 @@ function buildWorkspaceNodeChatPrompt(payload) {
1542
1683
  })
1543
1684
  .filter(Boolean)
1544
1685
  .join("\n\n");
1545
- const outputRule =
1546
- nodeKind === "html"
1686
+ const outputRule = directFileEdit
1687
+ ? [
1688
+ `直接修改当前 workspace 内的文件:${targetFilePath}`,
1689
+ "必须使用可用的文件编辑工具实际写入该文件;不要只描述改法。",
1690
+ "不要把完整文件内容输出到聊天回复。",
1691
+ "完成后只输出一句简短中文确认;如果无法完成,只输出原因,且说明文件未修改。",
1692
+ ].join("\n")
1693
+ : nodeKind === "html"
1547
1694
  ? "只输出完整或片段 HTML,不要解释,不要包裹 Markdown 代码围栏。"
1548
1695
  : nodeKind === "image"
1549
1696
  ? "只输出新的图片 src,可以是 URL、data URL 或文件路径,不要解释。"
@@ -1554,7 +1701,10 @@ function buildWorkspaceNodeChatPrompt(payload) {
1554
1701
  : "只输出新的 Markdown 正文,不要解释,不要包裹 Markdown 代码围栏。";
1555
1702
  return [
1556
1703
  "你正在微调 AgentFlow Workspace 画布中的单个展示节点。",
1557
- "根据用户 follow-up 和当前节点内容,生成一个可直接替换当前节点展示内容的候选版本。",
1704
+ directFileEdit
1705
+ ? "根据用户 follow-up 直接编辑该展示节点引用的 artifact 文件。"
1706
+ : "根据用户 follow-up 和当前节点内容,生成一个可直接替换当前节点展示内容的候选版本。",
1707
+ "上下文只来自当前展示内容、直接上游节点任务和本节点对话历史;不要引用或复述 thinking、运行日志、下游展示内容。",
1558
1708
  outputRule,
1559
1709
  "",
1560
1710
  "## 当前节点",
@@ -1562,7 +1712,9 @@ function buildWorkspaceNodeChatPrompt(payload) {
1562
1712
  `- label: ${String(node.label || "").trim() || "(unnamed)"}`,
1563
1713
  `- definitionId: ${String(node.definitionId || "").trim() || "(unknown)"}`,
1564
1714
  `- kind: ${nodeKind}`,
1565
- currentContent ? `\n## 当前展示内容\n\n${currentContent}` : "",
1715
+ targetFilePath ? `- artifactFile: ${targetFilePath}` : "",
1716
+ sourceContext ? `\n## 生成该展示的直接上游上下文(不含 thinking/log)\n\n${sourceContext}` : "",
1717
+ !directFileEdit && currentContent ? `\n## 当前展示内容\n\n${currentContent}` : "",
1566
1718
  historyBlock ? `\n## 本节点对话历史\n\n${historyBlock}` : "",
1567
1719
  `\n## 用户 follow-up\n\n${userMessage}`,
1568
1720
  ].filter(Boolean).join("\n");
@@ -1598,9 +1750,16 @@ function workspaceSourceSlotForEdge(graph, edge) {
1598
1750
  return output[workspaceHandleIndex(edge?.sourceHandle, "output")] || null;
1599
1751
  }
1600
1752
 
1753
+ function isWorkspaceSemanticOutputSlot(slot) {
1754
+ const name = String(slot?.name || "");
1755
+ const type = String(slot?.type || "");
1756
+ return type === "node" || name === "prev" || name === "next";
1757
+ }
1758
+
1601
1759
  function workspaceOutputSlotValueForEdge(graph, outputs, edge) {
1602
1760
  const sourceId = String(edge?.source || "");
1603
1761
  const slot = workspaceSourceSlotForEdge(graph, edge);
1762
+ if (isWorkspaceSemanticOutputSlot(slot)) return "";
1604
1763
  const out = outputs.get(sourceId);
1605
1764
  const sourceIndex = workspaceHandleIndex(edge?.sourceHandle, "output");
1606
1765
  const slotName = String(slot?.name || "").trim();
@@ -1789,8 +1948,109 @@ function workspaceExtractLooseResult(raw) {
1789
1948
  return "";
1790
1949
  }
1791
1950
 
1792
- function workspaceStructuredAgentOutput(content) {
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
+
1961
+ function workspaceExtractAgentflowEnvelope(raw) {
1962
+ const text = String(raw || "");
1963
+ const match = text.match(/---agentflow\b([\s\S]*?)---end/i);
1964
+ if (!match) return null;
1965
+ const envelope = workspaceNormalizeAgentflowEnvelopeBody(match[1] || "");
1966
+ const outside = `${text.slice(0, match.index || 0)}\n${text.slice((match.index || 0) + match[0].length)}`.trim();
1967
+ const lines = envelope.split("\n");
1968
+ const outParams = {};
1969
+ let result = "";
1970
+ let resultFile = "";
1971
+
1972
+ const lineIndent = (line) => {
1973
+ const m = String(line || "").match(/^(\s*)/);
1974
+ return m ? m[1].length : 0;
1975
+ };
1976
+ const cleanScalar = (value) => String(value || "").trim().replace(/^["']|["']$/g, "");
1977
+ const collectBlock = (startIndex, baseIndent) => {
1978
+ const collected = [];
1979
+ let i = startIndex;
1980
+ for (; i < lines.length; i += 1) {
1981
+ const line = lines[i] || "";
1982
+ if (line.trim() && lineIndent(line) <= baseIndent) break;
1983
+ collected.push(line.slice(Math.min(line.length, baseIndent + 2)));
1984
+ }
1985
+ return { value: collected.join("\n").replace(/\s+$/g, ""), nextIndex: i };
1986
+ };
1987
+ const parseValue = (rawValue, currentIndex, baseIndent) => {
1988
+ const value = String(rawValue || "").trim();
1989
+ if (value === "|" || value === ">") return collectBlock(currentIndex + 1, baseIndent);
1990
+ return { value: cleanScalar(value), nextIndex: currentIndex + 1 };
1991
+ };
1992
+
1993
+ for (let i = 0; i < lines.length;) {
1994
+ const line = lines[i] || "";
1995
+ if (!line.trim() || /^\s*#/.test(line)) {
1996
+ i += 1;
1997
+ continue;
1998
+ }
1999
+ const top = line.match(/^([A-Za-z_][A-Za-z0-9_.-]*)\s*:\s*(.*)$/);
2000
+ if (!top) {
2001
+ i += 1;
2002
+ continue;
2003
+ }
2004
+ const key = top[1];
2005
+ const rawValue = top[2] || "";
2006
+ if (key === "outParams") {
2007
+ i += 1;
2008
+ while (i < lines.length) {
2009
+ const childLine = lines[i] || "";
2010
+ if (!childLine.trim()) {
2011
+ i += 1;
2012
+ continue;
2013
+ }
2014
+ if (lineIndent(childLine) === 0) break;
2015
+ const child = childLine.match(/^\s+([A-Za-z_][A-Za-z0-9_.-]*)\s*:\s*(.*)$/);
2016
+ if (!child) {
2017
+ i += 1;
2018
+ continue;
2019
+ }
2020
+ const childKey = child[1];
2021
+ const parsed = parseValue(child[2] || "", i, lineIndent(childLine));
2022
+ if (childKey) outParams[childKey] = String(parsed.value || "").trim();
2023
+ i = parsed.nextIndex;
2024
+ }
2025
+ continue;
2026
+ }
2027
+ const parsed = parseValue(rawValue, i, 0);
2028
+ if (key === "result") result = String(parsed.value || "");
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();
2031
+ else if (key) outParams[key] = String(parsed.value || "").trim();
2032
+ i = parsed.nextIndex;
2033
+ }
2034
+ return {
2035
+ result: resultFile || result || outside,
2036
+ resultFile,
2037
+ outParams,
2038
+ structured: true,
2039
+ parsed: { result, resultFile, outParams },
2040
+ };
2041
+ }
2042
+
2043
+ function workspaceCanonicalAgentOutput(content) {
1793
2044
  const raw = String(content || "").trim();
2045
+ const match = raw.match(/---agentflow\b[\s\S]*?---end/i);
2046
+ if (match?.[0]) return match[0].trim();
2047
+ return raw;
2048
+ }
2049
+
2050
+ function workspaceStructuredAgentOutput(content) {
2051
+ const raw = workspaceCanonicalAgentOutput(content);
2052
+ const agentflowEnvelope = workspaceExtractAgentflowEnvelope(raw);
2053
+ if (agentflowEnvelope) return agentflowEnvelope;
1794
2054
  const parsed = workspaceParseJsonObjectFromText(raw);
1795
2055
  if (!parsed) {
1796
2056
  const looseResult = workspaceExtractLooseResult(raw);
@@ -1806,6 +2066,7 @@ function workspaceStructuredAgentOutput(content) {
1806
2066
  return { result: raw, outParams: {}, structured: false, parsed: null };
1807
2067
  }
1808
2068
  const hasEnvelope = Object.prototype.hasOwnProperty.call(parsed, "result") ||
2069
+ Object.prototype.hasOwnProperty.call(parsed, "resultFile") ||
1809
2070
  Object.prototype.hasOwnProperty.call(parsed, "outParams");
1810
2071
  if (!hasEnvelope) return { result: raw, outParams: {}, structured: false, parsed };
1811
2072
  const outParamsRaw = parsed.outParams && typeof parsed.outParams === "object" && !Array.isArray(parsed.outParams)
@@ -1816,8 +2077,10 @@ function workspaceStructuredAgentOutput(content) {
1816
2077
  const name = String(key || "").trim();
1817
2078
  if (name) outParams[name] = workspaceStringifyOutputValue(value);
1818
2079
  }
2080
+ const resultFile = workspaceStringifyOutputValue(parsed.resultFile ?? "").trim();
1819
2081
  return {
1820
- result: workspaceStringifyOutputValue(parsed.result ?? ""),
2082
+ result: resultFile || workspaceStringifyOutputValue(parsed.result ?? ""),
2083
+ resultFile,
1821
2084
  outParams,
1822
2085
  structured: true,
1823
2086
  parsed,
@@ -1831,6 +2094,10 @@ function workspaceExtractNamedOutputValue(content, slotName) {
1831
2094
  if (Object.prototype.hasOwnProperty.call(structured.outParams, name)) {
1832
2095
  return String(structured.outParams[name] ?? "");
1833
2096
  }
2097
+ const fileName = `${name}File`;
2098
+ if (Object.prototype.hasOwnProperty.call(structured.outParams, fileName)) {
2099
+ return String(structured.outParams[fileName] ?? "");
2100
+ }
1834
2101
  const parsed = structured.parsed || workspaceParseJsonObjectFromText(content);
1835
2102
  if (parsed && Object.prototype.hasOwnProperty.call(parsed, name)) {
1836
2103
  const value = parsed[name];
@@ -1869,6 +2136,8 @@ function workspaceApplyAgentOutputSlots(instance, content) {
1869
2136
  value = text;
1870
2137
  } else if (Object.prototype.hasOwnProperty.call(structured.outParams, name)) {
1871
2138
  value = structured.outParams[name];
2139
+ } else if (Object.prototype.hasOwnProperty.call(structured.outParams, `${name}File`)) {
2140
+ value = structured.outParams[`${name}File`];
1872
2141
  } else {
1873
2142
  value = workspaceExtractNamedOutputValue(text, name);
1874
2143
  }
@@ -1918,6 +2187,77 @@ function workspaceDisplayKind(definitionId) {
1918
2187
  return "";
1919
2188
  }
1920
2189
 
2190
+ function workspaceDisplayTextFilePath(value, kind = "") {
2191
+ const text = String(value || "").trim();
2192
+ if (!text || text.length > 260) return "";
2193
+ if (/[\r\n<>]/.test(text)) return "";
2194
+ if (/^(?:https?:|data:|blob:|file:|javascript:|mailto:|tel:)/i.test(text)) return "";
2195
+ const clean = text.replace(/^\/+/, "");
2196
+ if (clean.includes("..") || clean.startsWith(".")) return "";
2197
+ const ext = clean.split("?")[0].split("#")[0].toLowerCase().split(".").pop() || "";
2198
+ const allowedByKind = {
2199
+ html: new Set(["html", "htm"]),
2200
+ markdown: new Set(["md", "markdown", "txt"]),
2201
+ mermaid: new Set(["mmd", "mermaid", "txt"]),
2202
+ ascii: new Set(["txt", "log"]),
2203
+ chart: new Set(["json"]),
2204
+ table: new Set(["json", "csv", "tsv"]),
2205
+ };
2206
+ const allowed = allowedByKind[kind] || new Set(["html", "htm", "md", "markdown", "txt", "json", "csv", "tsv"]);
2207
+ return allowed.has(ext) ? clean : "";
2208
+ }
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
+
1921
2261
  function workspaceOutputFieldForSlot(slot, index = 0) {
1922
2262
  const name = String(slot?.name || "").trim();
1923
2263
  if (!name || name === "result" || name === "content" || index === 0) return "result";
@@ -1935,19 +2275,6 @@ function workspaceDisplayKindExample(kind, field) {
1935
2275
  return `<${field} 的值>`;
1936
2276
  }
1937
2277
 
1938
- function workspaceDisplayFieldRule(kind, field, slotName = "") {
1939
- const slotText = field === "result" ? "`result` 字段" : `\`${field}\``;
1940
- const prefix = slotName ? `- 输出引脚 \`${slotName}\` 连接了 ${kind} 展示节点:` : `- 下游连接了 ${kind} 展示节点:`;
1941
- if (kind === "html") return `${prefix}将可直接放入 iframe 渲染的 HTML 放在 ${slotText} 中。可以是完整 HTML 文档或 HTML fragment;不要使用 Markdown 代码围栏。`;
1942
- if (kind === "markdown") return `${prefix}将 Markdown 正文放在 ${slotText} 中;除非正文确实需要代码块,否则不要额外包裹代码围栏。`;
1943
- if (kind === "mermaid") return `${prefix}将 Mermaid 图表代码放在 ${slotText} 中,例如 flowchart/sequenceDiagram;不要使用 Markdown 代码围栏。`;
1944
- if (kind === "ascii") return `${prefix}将纯文本/ASCII 图或表格放在 ${slotText} 中;不要输出 HTML 或 Markdown 装饰。`;
1945
- if (kind === "image") return `${prefix}将可作为 img src 使用的图片地址、data URL 或 base64 data URL 放在 ${slotText} 中;不要输出 Markdown 图片语法。`;
1946
- 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 函数。`;
1947
- if (kind === "table") return `${prefix}将表格数据放在 ${slotText} 中。推荐格式:{"columns":["列名1","列名2"],"rows":[["值1","值2"]]};也可使用对象数组、Markdown 表格、CSV 或 TSV。不要输出 HTML。`;
1948
- return `${prefix}将展示内容放在 ${slotText} 中。`;
1949
- }
1950
-
1951
2278
  function workspaceDownstreamOutputDisplayBindings(graph, nodeId) {
1952
2279
  const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
1953
2280
  const edges = Array.isArray(graph?.edges) ? graph.edges : [];
@@ -1956,11 +2283,13 @@ function workspaceDownstreamOutputDisplayBindings(graph, nodeId) {
1956
2283
  const bindings = [];
1957
2284
  for (const edge of edges) {
1958
2285
  if (String(edge?.source || "") !== String(nodeId)) continue;
2286
+ if (isWorkspaceSemanticInputSlot(workspaceTargetSlotForEdge(graph, edge))) continue;
1959
2287
  const target = instances[String(edge?.target || "")];
1960
2288
  const kind = workspaceDisplayKind(target?.definitionId);
1961
2289
  if (!kind) continue;
1962
2290
  const index = workspaceHandleIndex(edge?.sourceHandle, "output");
1963
2291
  const slot = output[index] || null;
2292
+ if (isWorkspaceSemanticOutputSlot(slot)) continue;
1964
2293
  const name = String(slot?.name || "").trim() || (index === 0 ? "result" : `output-${index}`);
1965
2294
  bindings.push({
1966
2295
  kind,
@@ -2005,45 +2334,24 @@ function normalizeHtmlDisplayContent(content) {
2005
2334
  return text;
2006
2335
  }
2007
2336
 
2008
- function workspaceDownstreamDisplayRequirements(graph, nodeId) {
2009
- const bindings = workspaceDownstreamOutputDisplayBindings(graph, nodeId);
2010
- if (bindings.length === 0) return "";
2011
- const seen = new Set();
2012
- const rules = [];
2013
- for (const binding of bindings) {
2014
- const key = `${binding.field}:${binding.kind}`;
2015
- if (seen.has(key)) continue;
2016
- seen.add(key);
2017
- rules.push(workspaceDisplayFieldRule(binding.kind, binding.field, binding.name));
2018
- }
2019
- return [
2020
- "## 下游输出要求",
2021
- "",
2022
- ...rules,
2023
- "",
2024
- "这些要求按输出引脚分别生效:不要把非 `result` 引脚连接的展示内容误写到 `result`;具名引脚应写入 `outParams.<引脚名>`。",
2025
- "如果用户任务与下游展示格式没有冲突,优先满足上述格式要求;如果用户明确指定了其他格式,以用户任务为准。",
2026
- ].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;
2027
2345
  }
2028
2346
 
2029
- function workspaceNodeScopeGuardrails(graph, nodeId, inputValues = {}) {
2030
- const edges = Array.isArray(graph?.edges) ? graph.edges : [];
2031
- const directInputEdges = edges
2032
- .filter((edge) => String(edge?.target || "") === String(nodeId))
2033
- .filter((edge) => !isWorkspaceSemanticInputSlot(workspaceTargetSlotForEdge(graph, edge)));
2034
- const upstreamNodeIds = Array.from(new Set(directInputEdges.map((edge) => String(edge?.source || "").trim()).filter(Boolean)));
2035
- const inputNames = Object.keys(inputValues || {}).filter(Boolean);
2036
- return [
2037
- "## 当前节点上下文边界",
2038
- "",
2039
- "你只负责执行当前节点;`上游上下文`、已解析输入槽和节点任务就是本节点的业务上下文边界。",
2040
- upstreamNodeIds.length ? `当前直接上游节点:${upstreamNodeIds.map((id) => `\`${id}\``).join("、")}。` : "当前没有直接业务上游节点。",
2041
- inputNames.length ? `当前已解析输入槽:${inputNames.map((name) => `\`${name}\``).join("、")}。` : "当前没有已解析的具名业务输入槽。",
2042
- "不要为了理解本节点而读取或搜索整张 workspace、`workspace.graph.json`、正式 `flow.yaml`、历史 run/log 或其它未连接节点。",
2043
- "不要把下游展示节点已有内容、下游错误信息、其它分支节点内容当作本节点输入;下游输出要求只用于决定最终 JSON 的字段和格式。",
2044
- "只有当当前节点任务文本或上游输入明确要求读取/修改 `workspace.graph.json`、`flow.yaml` 或某个具体文件路径时,才可以打开对应文件。",
2045
- "如果完成任务所需信息不在当前节点任务、输入槽或上游上下文中,应明确说明缺少哪个上游输入,而不是扫描整张 workspace 猜测。",
2046
- ].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 };
2047
2355
  }
2048
2356
 
2049
2357
  function workspaceOutputProtocolRequirements(graph, nodeId) {
@@ -2061,30 +2369,46 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
2061
2369
  return name && type !== "node" && name !== "next" && name !== "result" && name !== "content";
2062
2370
  })
2063
2371
  .map((slot) => String(slot.name).trim());
2064
- const outParamsExample = slots.length
2065
- ? Object.fromEntries(slots.map((name) => [name, workspaceDisplayKindExample(displayByField.get(`outParams.${name}`) || "", name)]))
2066
- : {};
2067
- const resultExample = workspaceDisplayKindExample(displayByField.get("result") || "markdown", "result");
2068
- const slotDisplayRules = displayBindings
2069
- .map((binding) => `- 输出引脚 \`${binding.name}\` -> ${binding.kind} 展示节点:写入 \`${binding.field}\`。`)
2070
- .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] || "内容应满足任务要求。";
2393
+ const envelopeExample = [
2394
+ "---agentflow",
2395
+ `resultFile: ${resultFile}`,
2396
+ slots.length ? "outParams:" : "",
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
+ }),
2402
+ "---end",
2403
+ ].filter(Boolean).join("\n");
2071
2404
  return [
2072
- "## Workspace 输出协议",
2073
- "",
2074
- "最终回复必须是一个 JSON 对象,必须直接以 `{` 开头并以 `}` 结尾;不要使用 Markdown 代码围栏,不要在 JSON 外追加解释文字、进度说明或自然语言前后缀。",
2075
- "执行过程中不要发送 assistant 进度说明,例如“正在读取/正在生成/准备输出”;需要思考时使用内部 thinking,最终只发送一个 JSON 对象。",
2076
- "JSON 必须可被 `JSON.parse` 解析;如果 `result` 是多行 Markdown,必须在 JSON 字符串里使用 `\\n` 转义换行,不能把裸 Markdown 直接塞进未转义的字符串。",
2077
- "固定格式:",
2405
+ "## 输出",
2078
2406
  "",
2079
- JSON.stringify({ result: resultExample, outParams: outParamsExample }, null, 2),
2407
+ `请把${resultKindText}结果写入 \`${resultFile}\`。${resultGuidance}`,
2408
+ ...(slots.length ? [`额外输出:${slots.map((name) => `\`${name}\``).join("、")}。短值可写在 \`outParams\`,文件值写成 \`outParams.<name>File\`。`] : []),
2409
+ "最终只输出下面的 agentflow envelope,不要输出解释、进度或其它文字:",
2080
2410
  "",
2081
- "- `result`:完整正文,写入 `result` / `content` 输出口,直连默认展示节点时展示它。",
2082
- "- `outParams`:具名输出参数,只写入同名输出引脚。",
2083
- ...(slotDisplayRules.length ? ["- 当前输出引脚与展示节点映射:", ...slotDisplayRules] : []),
2084
- ...(slots.length
2085
- ? [`- 当前节点具名输出槽:${slots.map((name) => `\`${name}\``).join("、")}。例如任务要求写入 \`${slots[0]}\` 时,放到 \`outParams.${slots[0]}\`。`]
2086
- : ["- 当前节点没有额外具名输出槽,`outParams` 返回空对象即可。"]),
2087
- "- 如果 `result` 需要承载表格、ChartSpec、HTML 等结构化内容,可把对象或字符串放入 `result`;系统会把它转换给下游展示节点。",
2411
+ envelopeExample,
2088
2412
  ].join("\n");
2089
2413
  }
2090
2414
 
@@ -2144,8 +2468,9 @@ function workspaceRunPlan(graph, runNodeId) {
2144
2468
 
2145
2469
  function workspaceUpstreamText(graph, nodeId, outputs) {
2146
2470
  const edges = Array.isArray(graph?.edges) ? graph.edges : [];
2147
- const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
2148
- const incoming = edges.filter((edge) => String(edge?.target || "") === String(nodeId));
2471
+ const incoming = edges
2472
+ .filter((edge) => String(edge?.target || "") === String(nodeId))
2473
+ .filter((edge) => !isWorkspaceSemanticInputSlot(workspaceTargetSlotForEdge(graph, edge)));
2149
2474
  const contentEdge = incoming.find((edge) => String(edge?.targetHandle || "") === "input-1") || incoming[0];
2150
2475
  if (!contentEdge) return "";
2151
2476
  return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge);
@@ -2169,79 +2494,44 @@ function isWorkspaceSemanticInputSlot(slot) {
2169
2494
  return type === "node" || name === "prev" || name === "next" || name === "skillsContext" || name === "mcpContext" || name === "workspaceContext" || name === "gitContext";
2170
2495
  }
2171
2496
 
2172
- function workspaceNodeBrief(graph, nodeId) {
2173
- const instance = graph?.instances?.[String(nodeId || "")] || {};
2174
- const label = String(instance?.label || nodeId || "").trim();
2175
- const defId = String(instance?.definitionId || "").trim();
2176
- return defId && defId !== label ? `${label || nodeId} (${defId})` : (label || nodeId);
2177
- }
2178
-
2179
- function workspaceSlotBrief(slot, fallback) {
2180
- const name = String(slot?.name || "").trim();
2181
- const type = String(slot?.type || "").trim();
2182
- return `${name || fallback}${type ? `:${type}` : ""}`;
2183
- }
2184
-
2185
- function workspaceNodeConnectionContextBlock(graph, nodeId) {
2186
- const edges = Array.isArray(graph?.edges) ? graph.edges : [];
2187
- const incoming = edges.filter((edge) => String(edge?.target || "") === String(nodeId));
2188
- const outgoing = edges.filter((edge) => String(edge?.source || "") === String(nodeId));
2189
- const inputLines = incoming.map((edge) => {
2190
- const sourceSlot = workspaceSourceSlotForEdge(graph, edge);
2191
- const targetSlot = workspaceTargetSlotForEdge(graph, edge);
2192
- const targetName = String(targetSlot?.name || "").trim();
2193
- const semantic = isWorkspaceSemanticInputSlot(targetSlot) ? "上下文输入" : "业务输入";
2194
- return `- ${workspaceNodeBrief(graph, edge.source)} \`${workspaceSlotBrief(sourceSlot, edge.sourceHandle || "output")}\` -> 当前 \`${workspaceSlotBrief(targetSlot, edge.targetHandle || "input")}\`(${semantic}${targetName ? `:${targetName}` : ""})`;
2195
- });
2196
- const outputLines = outgoing.map((edge) => {
2197
- const sourceSlot = workspaceSourceSlotForEdge(graph, edge);
2198
- const targetSlot = workspaceTargetSlotForEdge(graph, edge);
2199
- const targetKind = workspaceDisplayKind(graph?.instances?.[String(edge?.target || "")]?.definitionId);
2200
- return `- 当前 \`${workspaceSlotBrief(sourceSlot, edge.sourceHandle || "output")}\` -> ${workspaceNodeBrief(graph, edge.target)} \`${workspaceSlotBrief(targetSlot, edge.targetHandle || "input")}\`${targetKind ? `(${targetKind} 展示)` : ""}`;
2201
- });
2202
- if (!inputLines.length && !outputLines.length) return "";
2203
- return [
2204
- "## 当前节点连线",
2205
- "",
2206
- inputLines.length ? "### 直接上游" : "",
2207
- ...inputLines,
2208
- outputLines.length ? "\n### 直接下游" : "",
2209
- ...outputLines,
2210
- "",
2211
- "只把直接上游的业务输入和上下文输入当作本节点依据;直接下游只用于确定输出字段和格式。",
2212
- ].filter(Boolean).join("\n");
2213
- }
2214
-
2215
- function workspaceResolvedInputValuesBlock(inputValues = {}) {
2497
+ function workspaceAgentInputBlock(inputValues = {}) {
2216
2498
  const entries = Object.entries(inputValues || {}).filter(([name, value]) => String(name || "").trim() && String(value || "").trim());
2217
- if (!entries.length) return "";
2499
+ if (!entries.length) return "## 输入\n\n无。";
2218
2500
  const lines = entries.map(([name, value]) => {
2219
2501
  const text = String(value || "");
2220
2502
  const clipped = text.length > 6000 ? `${text.slice(0, 6000)}\n...[已截断 ${text.length - 6000} 字]` : text;
2221
2503
  return `### ${name}\n\n${clipped}`;
2222
2504
  });
2223
- return ["## 已解析业务输入槽", "", ...lines].join("\n");
2505
+ return ["## 输入", "", ...lines].join("\n");
2224
2506
  }
2225
2507
 
2226
- function workspaceNodeTmpDirectoryBlock(nodeTmpDir) {
2227
- const dir = String(nodeTmpDir || "").trim();
2228
- 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 "";
2229
2513
  return [
2230
- "## 临时文件目录",
2514
+ "## 文件边界",
2231
2515
  "",
2232
- `- 本节点专用临时目录:\`${dir}\``,
2233
- "- 如果确实需要创建中间文件,只能写入该目录,路径也可通过环境变量 `AGENTFLOW_NODE_TMP_DIR` 获取。",
2234
- "- 不要在 workspace 根目录、业务仓库根目录或当前 cwd 下创建 `temp_*`、`_out.json`、`tmp.html` 等临时产物。",
2235
- "- 不要自行删除该目录或其中的最终待读文件;AgentFlow 会在节点运行结束后统一清理。",
2236
- "- 纯生成类任务应直接在最终 JSON 中返回结果;不要为了输出而写文件、cat 文件再粘贴。",
2237
- ].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");
2238
2522
  }
2239
2523
 
2240
- function workspaceTaskUpstreamText(graph, nodeId, outputs) {
2524
+ function workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputNames = null) {
2241
2525
  const edges = Array.isArray(graph?.edges) ? graph.edges : [];
2242
2526
  const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
2243
2527
  const incoming = edges.filter((edge) => String(edge?.target || "") === String(nodeId));
2244
- 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
+ }
2245
2535
  const contentEdge = contentEdges.find((edge) => String(edge?.targetHandle || "") === "input-1") || contentEdges[0];
2246
2536
  if (!contentEdge) return "";
2247
2537
  return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge);
@@ -2410,7 +2700,8 @@ function buildWorkspaceMcpManifestBlock(results, servers = [], selectedNames = [
2410
2700
  function workspaceWriteDisplayContent(instance, content) {
2411
2701
  const next = { ...(instance || {}) };
2412
2702
  const kind = workspaceDisplayKind(next.definitionId);
2413
- const text = kind === "html" ? normalizeHtmlDisplayContent(content) : String(content || "");
2703
+ const unwrapped = workspaceUnwrapOutputEnvelopeForDisplay(content);
2704
+ const text = kind === "html" ? normalizeHtmlDisplayContent(unwrapped) : String(unwrapped || "");
2414
2705
  const primaryName = kind === "image" ? "src" : "content";
2415
2706
  next.body = text;
2416
2707
  next.input = (Array.isArray(next.input) ? next.input : []).map((slot) => (
@@ -2426,12 +2717,21 @@ function workspaceWriteDisplayContent(instance, content) {
2426
2717
  return next;
2427
2718
  }
2428
2719
 
2720
+ function workspaceUnwrapOutputEnvelopeForDisplay(content) {
2721
+ const raw = String(content || "").trim();
2722
+ if (!raw) return "";
2723
+ if (!/---agentflow\b|["']result["']\s*:|["']outParams["']\s*:|["']resultFile["']\s*:/i.test(raw)) return raw;
2724
+ const structured = workspaceStructuredAgentOutput(raw);
2725
+ return structured.structured ? String(structured.result || "") : raw;
2726
+ }
2727
+
2429
2728
  function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null) {
2430
2729
  const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
2431
2730
  const edges = Array.isArray(graph?.edges) ? graph.edges : [];
2432
2731
  const updated = [];
2433
2732
  for (const edge of edges) {
2434
2733
  if (String(edge?.source || "") !== String(sourceId)) continue;
2734
+ if (isWorkspaceSemanticInputSlot(workspaceTargetSlotForEdge(graph, edge))) continue;
2435
2735
  const targetId = String(edge?.target || "");
2436
2736
  const target = instances[targetId];
2437
2737
  if (!target || !workspaceDisplayKind(target.definitionId)) continue;
@@ -2445,28 +2745,21 @@ function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null)
2445
2745
  function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock = "", inputValues = {}, nodeTmpDir = "") {
2446
2746
  const instance = graph.instances[nodeId] || {};
2447
2747
  const body = workspaceResolveBodyPlaceholders(instance.body || "", inputValues).trim();
2448
- const label = String(instance.label || nodeId).trim();
2449
- const scopeGuardrails = workspaceNodeScopeGuardrails(graph, nodeId, inputValues);
2450
- const connectionContext = workspaceNodeConnectionContextBlock(graph, nodeId);
2451
- const resolvedInputs = workspaceResolvedInputValuesBlock(inputValues);
2452
- const tmpDirectory = workspaceNodeTmpDirectoryBlock(nodeTmpDir);
2453
- 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);
2454
2752
  const outputProtocolRequirements = workspaceOutputProtocolRequirements(graph, nodeId);
2455
2753
  return [
2456
- "你正在执行 AgentFlow Workspace 画布中的一个临时节点。",
2457
- "按 Workspace 输出协议返回该节点要传给下游展示/后续节点的数据。",
2458
- scopeGuardrails,
2459
- connectionContext ? `\n${connectionContext}` : "",
2460
- tmpDirectory ? `\n${tmpDirectory}` : "",
2461
- workspaceSearchGuardrailsBlock(),
2462
- resolvedInputs ? `\n${resolvedInputs}` : "",
2463
- skillsBlock ? `\n## 上游已加载 Skills\n\n${skillsBlock}` : "",
2464
- mcpBlock ? `\n## 上游已加载 MCP\n\n${mcpBlock}` : "",
2465
- upstreamText ? `\n## 上游上下文\n\n${upstreamText}` : "",
2466
- 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}` : "",
2467
2761
  outputProtocolRequirements ? `\n${outputProtocolRequirements}` : "",
2468
- `\n## 当前节点\n\n- id: ${nodeId}\n- label: ${label}\n- definitionId: ${instance.definitionId || ""}`,
2469
- `\n## 节点任务\n\n${body || upstreamText}`,
2762
+ `\n## 任务\n\n${body || upstreamText}`,
2470
2763
  ].filter(Boolean).join("\n");
2471
2764
  }
2472
2765
 
@@ -2558,6 +2851,40 @@ function workspaceCreateNodeTmpDir(runTmpRoot, nodeId) {
2558
2851
  return dir;
2559
2852
  }
2560
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
+
2561
2888
  function workspaceShouldKeepTmp(userCtx = {}) {
2562
2889
  const env = { ...process.env, ...readMergedEnvObject(userCtx.userId) };
2563
2890
  const value = String(env.AGENTFLOW_KEEP_TMP || env.AGENTFLOW_KEEP_WORKSPACE_TMP || "").trim().toLowerCase();
@@ -2622,9 +2949,16 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
2622
2949
  };
2623
2950
  const outputs = new Map();
2624
2951
  const events = [];
2952
+ const runStartedAt = Date.now();
2625
2953
  const emit = (event) => {
2626
- events.push(event);
2627
- 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);
2628
2962
  };
2629
2963
  const emitTiming = (nodeId, label, startedAt, extra = {}) => {
2630
2964
  const elapsedMs = Math.max(0, Date.now() - startedAt);
@@ -2934,8 +3268,9 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
2934
3268
  }
2935
3269
 
2936
3270
  const prepareStartedAt = Date.now();
2937
- const upstreamText = workspaceTaskUpstreamText(graph, nodeId, outputs);
2938
3271
  const inputValues = workspaceInputValues(graph, nodeId, outputs);
3272
+ const relevantInputs = workspaceRelevantInputValues(instance.body || "", inputValues);
3273
+ const upstreamText = workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputs.placeholders);
2939
3274
  const body = workspaceResolveBodyPlaceholders(instance.body || "", inputValues).trim();
2940
3275
  if (defId === "agent_subAgent" && !body && !String(upstreamText || "").trim()) {
2941
3276
  throw new Error(`Workspace node ${nodeId} has no task. Fill the node body or connect upstream text.`);
@@ -2943,9 +3278,27 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
2943
3278
  const upstreamSkillBlocks = workspaceUpstreamSkillBlocks(graph, nodeId, outputs);
2944
3279
  const promptSkillsBlock = mergeWorkspaceSkillBlocks(upstreamSkillBlocks);
2945
3280
  const promptMcpBlock = workspaceUpstreamMcpBlocks(graph, nodeId, outputs);
2946
- const nodeTmpDir = workspaceCreateNodeTmpDir(runTmpRoot, nodeId);
2947
- const prompt = workspaceNodePrompt(graph, nodeId, upstreamText, promptSkillsBlock, promptMcpBlock, inputValues, nodeTmpDir);
2948
- 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
+ });
2949
3302
  emit({ type: "natural", kind: "prompt", nodeId, text: prompt });
2950
3303
  let content = "";
2951
3304
  const maxAttempts = 3;
@@ -2954,24 +3307,34 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
2954
3307
  try {
2955
3308
  const spawnStartedAt = Date.now();
2956
3309
  let firstAgentEventSeen = false;
3310
+ let attemptResultContent = "";
3311
+ let attemptLastAssistantContent = "";
2957
3312
  const handle = startComposerAgent({
2958
3313
  uiWorkspaceRoot: scopedRoot,
2959
- cliWorkspace: cwd,
3314
+ cliWorkspace: runPackage.nodeRunDir,
2960
3315
  prompt,
2961
3316
  modelKey,
2962
3317
  agentflowUserId: userCtx.userId || "",
2963
3318
  extraEnv: {
2964
3319
  AGENTFLOW_WORKSPACE_TMP_ROOT: runTmpRoot,
2965
- 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,
2966
3323
  },
2967
3324
  onStreamEvent: (ev) => {
2968
3325
  if (!firstAgentEventSeen) {
2969
3326
  firstAgentEventSeen = true;
2970
3327
  emitTiming(nodeId, "agent-first-event", spawnStartedAt, { attempt, firstType: ev?.type || "" });
2971
3328
  }
2972
- emit({ ...ev, nodeId });
3329
+ const eventToEmit = (ev?.type === "natural" && (ev.kind === "result" || ev.kind === "assistant") && typeof ev.text === "string")
3330
+ ? { ...ev, text: workspaceCanonicalAgentOutput(ev.text), nodeId }
3331
+ : { ...ev, nodeId };
3332
+ emit(eventToEmit);
2973
3333
  if (ev?.type === "natural" && ev.kind === "assistant" && typeof ev.text === "string") {
3334
+ attemptLastAssistantContent = ev.text;
2974
3335
  attemptContent += (attemptContent ? "\n" : "") + ev.text;
3336
+ } else if (ev?.type === "natural" && ev.kind === "result" && typeof ev.text === "string") {
3337
+ attemptResultContent = ev.text;
2975
3338
  }
2976
3339
  },
2977
3340
  onToolCall: (subtype, toolName) => {
@@ -2988,7 +3351,11 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
2988
3351
  if (typeof opts.onActiveChild === "function") opts.onActiveChild(null);
2989
3352
  }
2990
3353
  throwIfAborted();
2991
- content = attemptContent.trim();
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);
2992
3359
  break;
2993
3360
  } catch (e) {
2994
3361
  if (signal?.aborted || e?.code === "WORKSPACE_RUN_ABORTED") throwIfAborted();
@@ -3000,7 +3367,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
3000
3367
  throw e;
3001
3368
  }
3002
3369
  }
3003
- const normalizedAgentOutput = workspaceStructuredAgentOutput(content);
3370
+ const normalizedAgentOutput = workspacePublishAgentOutputFiles(workspaceStructuredAgentOutput(content), runPackage);
3004
3371
  const resultContent = normalizedAgentOutput.result || content;
3005
3372
  outputs.set(nodeId, resultContent);
3006
3373
  const slotUpdate = workspaceApplyAgentOutputSlots(instance, content);
@@ -4140,6 +4507,67 @@ export function startUiServer({
4140
4507
  return;
4141
4508
  }
4142
4509
 
4510
+ if (req.method === "POST" && url.pathname === "/api/workspace/html-screenshot") {
4511
+ let payload;
4512
+ try {
4513
+ payload = JSON.parse(await readBody(req));
4514
+ } catch {
4515
+ json(res, 400, { error: "Invalid JSON body" });
4516
+ return;
4517
+ }
4518
+ try {
4519
+ const scoped = resolveWorkspaceScopeRoot(root, {
4520
+ flowId: payload.flowId || "",
4521
+ flowSource: payload.flowSource || "user",
4522
+ archived: payload.archived === true || payload.flowArchived === true,
4523
+ }, userCtx);
4524
+ if (scoped.error) {
4525
+ json(res, 400, { error: scoped.error });
4526
+ return;
4527
+ }
4528
+ const sourceFilePath = String(payload.sourceFilePath || payload.path || "").trim();
4529
+ let html = String(payload.content || "");
4530
+ let baseDir = scoped.root;
4531
+ if (sourceFilePath) {
4532
+ const { abs } = resolveWorkspaceFilePath(scoped.root, sourceFilePath);
4533
+ if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
4534
+ json(res, 404, { error: "HTML file not found" });
4535
+ return;
4536
+ }
4537
+ const stat = fs.statSync(abs);
4538
+ if (stat.size > 5 * 1024 * 1024) {
4539
+ json(res, 413, { error: "HTML file too large" });
4540
+ return;
4541
+ }
4542
+ if (!html.trim()) html = fs.readFileSync(abs, "utf-8");
4543
+ baseDir = path.dirname(abs);
4544
+ }
4545
+ if (!html.trim()) {
4546
+ json(res, 400, { error: "Missing HTML content" });
4547
+ return;
4548
+ }
4549
+ const screenshot = await renderHtmlScreenshotWithChrome({
4550
+ html,
4551
+ workspaceRoot: scoped.root,
4552
+ baseDir,
4553
+ width: payload.width,
4554
+ height: payload.height,
4555
+ });
4556
+ const png = screenshot.png;
4557
+ const filename = sanitizeWorkspaceUploadName(payload.filename || "html-render.png").replace(/\.[^.]+$/i, ".png");
4558
+ res.writeHead(200, {
4559
+ "Content-Type": "image/png",
4560
+ "Content-Length": png.length,
4561
+ "Cache-Control": "no-store",
4562
+ "Content-Disposition": workspaceDownloadContentDisposition(filename),
4563
+ });
4564
+ res.end(png);
4565
+ } catch (e) {
4566
+ json(res, 500, { error: (e && e.message) || String(e) });
4567
+ }
4568
+ return;
4569
+ }
4570
+
4143
4571
  if (req.method === "POST" && url.pathname === "/api/workspace/file") {
4144
4572
  let payload;
4145
4573
  try {
@@ -4407,6 +4835,22 @@ export function startUiServer({
4407
4835
  json(res, 400, { error: scoped.error });
4408
4836
  return;
4409
4837
  }
4838
+ const targetFilePath = String(payload?.targetFilePath || "").trim();
4839
+ let targetFile = null;
4840
+ if (targetFilePath) {
4841
+ if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
4842
+ json(res, 400, { error: "Cannot edit builtin or archived pipeline workspace" });
4843
+ return;
4844
+ }
4845
+ targetFile = resolveWorkspaceFilePath(scoped.root, targetFilePath);
4846
+ if (!targetFile.rel) {
4847
+ json(res, 400, { error: "Missing artifact file path" });
4848
+ return;
4849
+ }
4850
+ }
4851
+ const beforeTargetContent = targetFile && fs.existsSync(targetFile.abs) && fs.statSync(targetFile.abs).isFile()
4852
+ ? fs.readFileSync(targetFile.abs, "utf-8")
4853
+ : null;
4410
4854
  const promptText = buildWorkspaceNodeChatPrompt(payload);
4411
4855
  const modelKey = typeof payload?.model === "string" ? payload.model.trim() : "";
4412
4856
  let content = "";
@@ -4425,12 +4869,29 @@ export function startUiServer({
4425
4869
  },
4426
4870
  });
4427
4871
  await handle.finished;
4428
- const candidateContent = content.trim();
4872
+ let candidateContent = targetFile
4873
+ ? (fs.existsSync(targetFile.abs) && fs.statSync(targetFile.abs).isFile()
4874
+ ? fs.readFileSync(targetFile.abs, "utf-8")
4875
+ : "")
4876
+ : content.trim();
4877
+ if (targetFile) {
4878
+ const unwrappedTargetContent = workspaceUnwrapOutputEnvelopeForDisplay(candidateContent);
4879
+ if (unwrappedTargetContent && unwrappedTargetContent !== candidateContent) {
4880
+ fs.writeFileSync(targetFile.abs, unwrappedTargetContent, "utf-8");
4881
+ candidateContent = unwrappedTargetContent;
4882
+ }
4883
+ }
4884
+ if (targetFile && beforeTargetContent != null && candidateContent === beforeTargetContent) {
4885
+ json(res, 500, { error: "Agent 未修改目标展示文件,请换一种更明确的描述后重试。" });
4886
+ return;
4887
+ }
4429
4888
  json(res, 200, {
4430
4889
  ok: true,
4431
4890
  sessionId: String(payload?.sessionId || "") || `nodechat_${Date.now()}`,
4432
- reply: candidateContent,
4891
+ reply: targetFile ? content.trim() : candidateContent,
4433
4892
  candidateContent,
4893
+ directFileEdit: Boolean(targetFile),
4894
+ artifactPath: targetFile?.rel || "",
4434
4895
  events,
4435
4896
  });
4436
4897
  } catch (e) {