@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.
- package/bin/lib/ui-server.mjs +633 -172
- package/builtin/web-ui/dist/assets/index-BuRzkSPp.js +262 -0
- package/builtin/web-ui/dist/assets/{index-Db5Z0V8r.css → index-D5OyrRLt.css} +1 -1
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-DdSsdIza.js +0 -238
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -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, "<")
|
|
1213
|
+
.replace(/>/g, ">");
|
|
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
|
|
1495
|
+
kind,
|
|
1358
1496
|
label: String(instance.label || instance.displayName || id),
|
|
1359
|
-
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2009
|
-
const
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
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
|
|
2030
|
-
const
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
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
|
|
2065
|
-
|
|
2066
|
-
:
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
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
|
-
"##
|
|
2073
|
-
"",
|
|
2074
|
-
"最终回复必须是一个 JSON 对象,必须直接以 `{` 开头并以 `}` 结尾;不要使用 Markdown 代码围栏,不要在 JSON 外追加解释文字、进度说明或自然语言前后缀。",
|
|
2075
|
-
"执行过程中不要发送 assistant 进度说明,例如“正在读取/正在生成/准备输出”;需要思考时使用内部 thinking,最终只发送一个 JSON 对象。",
|
|
2076
|
-
"JSON 必须可被 `JSON.parse` 解析;如果 `result` 是多行 Markdown,必须在 JSON 字符串里使用 `\\n` 转义换行,不能把裸 Markdown 直接塞进未转义的字符串。",
|
|
2077
|
-
"固定格式:",
|
|
2405
|
+
"## 输出",
|
|
2078
2406
|
"",
|
|
2079
|
-
|
|
2407
|
+
`请把${resultKindText}结果写入 \`${resultFile}\`。${resultGuidance}`,
|
|
2408
|
+
...(slots.length ? [`额外输出:${slots.map((name) => `\`${name}\``).join("、")}。短值可写在 \`outParams\`,文件值写成 \`outParams.<name>File\`。`] : []),
|
|
2409
|
+
"最终只输出下面的 agentflow envelope,不要输出解释、进度或其它文字:",
|
|
2080
2410
|
"",
|
|
2081
|
-
|
|
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
|
|
2148
|
-
|
|
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
|
|
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 ["##
|
|
2505
|
+
return ["## 输入", "", ...lines].join("\n");
|
|
2224
2506
|
}
|
|
2225
2507
|
|
|
2226
|
-
function
|
|
2227
|
-
const
|
|
2228
|
-
|
|
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
|
-
`-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
"-
|
|
2236
|
-
"-
|
|
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
|
-
|
|
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
|
|
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
|
|
2449
|
-
const
|
|
2450
|
-
const
|
|
2451
|
-
const
|
|
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
|
-
"
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
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##
|
|
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
|
-
|
|
2627
|
-
|
|
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
|
|
2947
|
-
|
|
2948
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|