@fieldwangai/agentflow 0.1.61 → 0.1.62
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
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,98 @@ function workspaceExtractLooseResult(raw) {
|
|
|
1789
1948
|
return "";
|
|
1790
1949
|
}
|
|
1791
1950
|
|
|
1792
|
-
function
|
|
1951
|
+
function workspaceExtractAgentflowEnvelope(raw) {
|
|
1952
|
+
const text = String(raw || "");
|
|
1953
|
+
const match = text.match(/(?:^|\n)---agentflow\s*\n([\s\S]*?)\n---end(?:\n|$)/i);
|
|
1954
|
+
if (!match) return null;
|
|
1955
|
+
const envelope = match[1] || "";
|
|
1956
|
+
const outside = `${text.slice(0, match.index || 0)}\n${text.slice((match.index || 0) + match[0].length)}`.trim();
|
|
1957
|
+
const lines = envelope.replace(/\r\n/g, "\n").split("\n");
|
|
1958
|
+
const outParams = {};
|
|
1959
|
+
let result = "";
|
|
1960
|
+
let resultFile = "";
|
|
1961
|
+
|
|
1962
|
+
const lineIndent = (line) => {
|
|
1963
|
+
const m = String(line || "").match(/^(\s*)/);
|
|
1964
|
+
return m ? m[1].length : 0;
|
|
1965
|
+
};
|
|
1966
|
+
const cleanScalar = (value) => String(value || "").trim().replace(/^["']|["']$/g, "");
|
|
1967
|
+
const collectBlock = (startIndex, baseIndent) => {
|
|
1968
|
+
const collected = [];
|
|
1969
|
+
let i = startIndex;
|
|
1970
|
+
for (; i < lines.length; i += 1) {
|
|
1971
|
+
const line = lines[i] || "";
|
|
1972
|
+
if (line.trim() && lineIndent(line) <= baseIndent) break;
|
|
1973
|
+
collected.push(line.slice(Math.min(line.length, baseIndent + 2)));
|
|
1974
|
+
}
|
|
1975
|
+
return { value: collected.join("\n").replace(/\s+$/g, ""), nextIndex: i };
|
|
1976
|
+
};
|
|
1977
|
+
const parseValue = (rawValue, currentIndex, baseIndent) => {
|
|
1978
|
+
const value = String(rawValue || "").trim();
|
|
1979
|
+
if (value === "|" || value === ">") return collectBlock(currentIndex + 1, baseIndent);
|
|
1980
|
+
return { value: cleanScalar(value), nextIndex: currentIndex + 1 };
|
|
1981
|
+
};
|
|
1982
|
+
|
|
1983
|
+
for (let i = 0; i < lines.length;) {
|
|
1984
|
+
const line = lines[i] || "";
|
|
1985
|
+
if (!line.trim() || /^\s*#/.test(line)) {
|
|
1986
|
+
i += 1;
|
|
1987
|
+
continue;
|
|
1988
|
+
}
|
|
1989
|
+
const top = line.match(/^([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/);
|
|
1990
|
+
if (!top) {
|
|
1991
|
+
i += 1;
|
|
1992
|
+
continue;
|
|
1993
|
+
}
|
|
1994
|
+
const key = top[1];
|
|
1995
|
+
const rawValue = top[2] || "";
|
|
1996
|
+
if (key === "outParams") {
|
|
1997
|
+
i += 1;
|
|
1998
|
+
while (i < lines.length) {
|
|
1999
|
+
const childLine = lines[i] || "";
|
|
2000
|
+
if (!childLine.trim()) {
|
|
2001
|
+
i += 1;
|
|
2002
|
+
continue;
|
|
2003
|
+
}
|
|
2004
|
+
if (lineIndent(childLine) === 0) break;
|
|
2005
|
+
const child = childLine.match(/^\s+([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/);
|
|
2006
|
+
if (!child) {
|
|
2007
|
+
i += 1;
|
|
2008
|
+
continue;
|
|
2009
|
+
}
|
|
2010
|
+
const childKey = child[1];
|
|
2011
|
+
const parsed = parseValue(child[2] || "", i, lineIndent(childLine));
|
|
2012
|
+
if (childKey) outParams[childKey] = String(parsed.value || "").trim();
|
|
2013
|
+
i = parsed.nextIndex;
|
|
2014
|
+
}
|
|
2015
|
+
continue;
|
|
2016
|
+
}
|
|
2017
|
+
const parsed = parseValue(rawValue, i, 0);
|
|
2018
|
+
if (key === "result") result = String(parsed.value || "");
|
|
2019
|
+
else if (key === "resultFile") resultFile = String(parsed.value || "").trim();
|
|
2020
|
+
else if (key) outParams[key] = String(parsed.value || "").trim();
|
|
2021
|
+
i = parsed.nextIndex;
|
|
2022
|
+
}
|
|
2023
|
+
return {
|
|
2024
|
+
result: resultFile || result || outside,
|
|
2025
|
+
resultFile,
|
|
2026
|
+
outParams,
|
|
2027
|
+
structured: true,
|
|
2028
|
+
parsed: { result, resultFile, outParams },
|
|
2029
|
+
};
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
function workspaceCanonicalAgentOutput(content) {
|
|
1793
2033
|
const raw = String(content || "").trim();
|
|
2034
|
+
const match = raw.match(/(?:^|\n)(---agentflow\s*\n[\s\S]*?\n---end)(?:\n|$)/i);
|
|
2035
|
+
if (match?.[1]) return match[1].trim();
|
|
2036
|
+
return raw;
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
function workspaceStructuredAgentOutput(content) {
|
|
2040
|
+
const raw = workspaceCanonicalAgentOutput(content);
|
|
2041
|
+
const agentflowEnvelope = workspaceExtractAgentflowEnvelope(raw);
|
|
2042
|
+
if (agentflowEnvelope) return agentflowEnvelope;
|
|
1794
2043
|
const parsed = workspaceParseJsonObjectFromText(raw);
|
|
1795
2044
|
if (!parsed) {
|
|
1796
2045
|
const looseResult = workspaceExtractLooseResult(raw);
|
|
@@ -1816,8 +2065,10 @@ function workspaceStructuredAgentOutput(content) {
|
|
|
1816
2065
|
const name = String(key || "").trim();
|
|
1817
2066
|
if (name) outParams[name] = workspaceStringifyOutputValue(value);
|
|
1818
2067
|
}
|
|
2068
|
+
const resultFile = workspaceStringifyOutputValue(parsed.resultFile ?? "").trim();
|
|
1819
2069
|
return {
|
|
1820
|
-
result: workspaceStringifyOutputValue(parsed.result ?? ""),
|
|
2070
|
+
result: resultFile || workspaceStringifyOutputValue(parsed.result ?? ""),
|
|
2071
|
+
resultFile,
|
|
1821
2072
|
outParams,
|
|
1822
2073
|
structured: true,
|
|
1823
2074
|
parsed,
|
|
@@ -1831,6 +2082,10 @@ function workspaceExtractNamedOutputValue(content, slotName) {
|
|
|
1831
2082
|
if (Object.prototype.hasOwnProperty.call(structured.outParams, name)) {
|
|
1832
2083
|
return String(structured.outParams[name] ?? "");
|
|
1833
2084
|
}
|
|
2085
|
+
const fileName = `${name}File`;
|
|
2086
|
+
if (Object.prototype.hasOwnProperty.call(structured.outParams, fileName)) {
|
|
2087
|
+
return String(structured.outParams[fileName] ?? "");
|
|
2088
|
+
}
|
|
1834
2089
|
const parsed = structured.parsed || workspaceParseJsonObjectFromText(content);
|
|
1835
2090
|
if (parsed && Object.prototype.hasOwnProperty.call(parsed, name)) {
|
|
1836
2091
|
const value = parsed[name];
|
|
@@ -1869,6 +2124,8 @@ function workspaceApplyAgentOutputSlots(instance, content) {
|
|
|
1869
2124
|
value = text;
|
|
1870
2125
|
} else if (Object.prototype.hasOwnProperty.call(structured.outParams, name)) {
|
|
1871
2126
|
value = structured.outParams[name];
|
|
2127
|
+
} else if (Object.prototype.hasOwnProperty.call(structured.outParams, `${name}File`)) {
|
|
2128
|
+
value = structured.outParams[`${name}File`];
|
|
1872
2129
|
} else {
|
|
1873
2130
|
value = workspaceExtractNamedOutputValue(text, name);
|
|
1874
2131
|
}
|
|
@@ -1918,6 +2175,26 @@ function workspaceDisplayKind(definitionId) {
|
|
|
1918
2175
|
return "";
|
|
1919
2176
|
}
|
|
1920
2177
|
|
|
2178
|
+
function workspaceDisplayTextFilePath(value, kind = "") {
|
|
2179
|
+
const text = String(value || "").trim();
|
|
2180
|
+
if (!text || text.length > 260) return "";
|
|
2181
|
+
if (/[\r\n<>]/.test(text)) return "";
|
|
2182
|
+
if (/^(?:https?:|data:|blob:|file:|javascript:|mailto:|tel:)/i.test(text)) return "";
|
|
2183
|
+
const clean = text.replace(/^\/+/, "");
|
|
2184
|
+
if (clean.includes("..") || clean.startsWith(".")) return "";
|
|
2185
|
+
const ext = clean.split("?")[0].split("#")[0].toLowerCase().split(".").pop() || "";
|
|
2186
|
+
const allowedByKind = {
|
|
2187
|
+
html: new Set(["html", "htm"]),
|
|
2188
|
+
markdown: new Set(["md", "markdown", "txt"]),
|
|
2189
|
+
mermaid: new Set(["mmd", "mermaid", "txt"]),
|
|
2190
|
+
ascii: new Set(["txt", "log"]),
|
|
2191
|
+
chart: new Set(["json"]),
|
|
2192
|
+
table: new Set(["json", "csv", "tsv"]),
|
|
2193
|
+
};
|
|
2194
|
+
const allowed = allowedByKind[kind] || new Set(["html", "htm", "md", "markdown", "txt", "json", "csv", "tsv"]);
|
|
2195
|
+
return allowed.has(ext) ? clean : "";
|
|
2196
|
+
}
|
|
2197
|
+
|
|
1921
2198
|
function workspaceOutputFieldForSlot(slot, index = 0) {
|
|
1922
2199
|
const name = String(slot?.name || "").trim();
|
|
1923
2200
|
if (!name || name === "result" || name === "content" || index === 0) return "result";
|
|
@@ -1936,10 +2213,10 @@ function workspaceDisplayKindExample(kind, field) {
|
|
|
1936
2213
|
}
|
|
1937
2214
|
|
|
1938
2215
|
function workspaceDisplayFieldRule(kind, field, slotName = "") {
|
|
1939
|
-
const slotText = field === "result" ? "`result`
|
|
2216
|
+
const slotText = field === "result" ? "`result` 或 `resultFile`" : `\`${field}\` 或 \`${field}File\``;
|
|
1940
2217
|
const prefix = slotName ? `- 输出引脚 \`${slotName}\` 连接了 ${kind} 展示节点:` : `- 下游连接了 ${kind} 展示节点:`;
|
|
1941
|
-
if (kind === "html") return `${prefix}将可直接放入 iframe 渲染的 HTML 放在 ${slotText}
|
|
1942
|
-
if (kind === "markdown") return `${prefix}将 Markdown 正文放在 ${slotText}
|
|
2218
|
+
if (kind === "html") return `${prefix}将可直接放入 iframe 渲染的 HTML 放在 ${slotText} 中;内容较长时优先写入 outputs/*.html 并返回文件路径。不要使用 Markdown 代码围栏。`;
|
|
2219
|
+
if (kind === "markdown") return `${prefix}将 Markdown 正文放在 ${slotText} 中;内容较长时优先写入 outputs/*.md 并返回文件路径。除非正文确实需要代码块,否则不要额外包裹代码围栏。`;
|
|
1943
2220
|
if (kind === "mermaid") return `${prefix}将 Mermaid 图表代码放在 ${slotText} 中,例如 flowchart/sequenceDiagram;不要使用 Markdown 代码围栏。`;
|
|
1944
2221
|
if (kind === "ascii") return `${prefix}将纯文本/ASCII 图或表格放在 ${slotText} 中;不要输出 HTML 或 Markdown 装饰。`;
|
|
1945
2222
|
if (kind === "image") return `${prefix}将可作为 img src 使用的图片地址、data URL 或 base64 data URL 放在 ${slotText} 中;不要输出 Markdown 图片语法。`;
|
|
@@ -1956,11 +2233,13 @@ function workspaceDownstreamOutputDisplayBindings(graph, nodeId) {
|
|
|
1956
2233
|
const bindings = [];
|
|
1957
2234
|
for (const edge of edges) {
|
|
1958
2235
|
if (String(edge?.source || "") !== String(nodeId)) continue;
|
|
2236
|
+
if (isWorkspaceSemanticInputSlot(workspaceTargetSlotForEdge(graph, edge))) continue;
|
|
1959
2237
|
const target = instances[String(edge?.target || "")];
|
|
1960
2238
|
const kind = workspaceDisplayKind(target?.definitionId);
|
|
1961
2239
|
if (!kind) continue;
|
|
1962
2240
|
const index = workspaceHandleIndex(edge?.sourceHandle, "output");
|
|
1963
2241
|
const slot = output[index] || null;
|
|
2242
|
+
if (isWorkspaceSemanticOutputSlot(slot)) continue;
|
|
1964
2243
|
const name = String(slot?.name || "").trim() || (index === 0 ? "result" : `output-${index}`);
|
|
1965
2244
|
bindings.push({
|
|
1966
2245
|
kind,
|
|
@@ -2040,7 +2319,7 @@ function workspaceNodeScopeGuardrails(graph, nodeId, inputValues = {}) {
|
|
|
2040
2319
|
upstreamNodeIds.length ? `当前直接上游节点:${upstreamNodeIds.map((id) => `\`${id}\``).join("、")}。` : "当前没有直接业务上游节点。",
|
|
2041
2320
|
inputNames.length ? `当前已解析输入槽:${inputNames.map((name) => `\`${name}\``).join("、")}。` : "当前没有已解析的具名业务输入槽。",
|
|
2042
2321
|
"不要为了理解本节点而读取或搜索整张 workspace、`workspace.graph.json`、正式 `flow.yaml`、历史 run/log 或其它未连接节点。",
|
|
2043
|
-
"
|
|
2322
|
+
"不要把下游展示节点已有内容、下游错误信息、其它分支节点内容当作本节点输入;下游输出要求只用于决定输出字段和格式。",
|
|
2044
2323
|
"只有当当前节点任务文本或上游输入明确要求读取/修改 `workspace.graph.json`、`flow.yaml` 或某个具体文件路径时,才可以打开对应文件。",
|
|
2045
2324
|
"如果完成任务所需信息不在当前节点任务、输入槽或上游上下文中,应明确说明缺少哪个上游输入,而不是扫描整张 workspace 猜测。",
|
|
2046
2325
|
].join("\n");
|
|
@@ -2068,23 +2347,32 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
|
|
|
2068
2347
|
const slotDisplayRules = displayBindings
|
|
2069
2348
|
.map((binding) => `- 输出引脚 \`${binding.name}\` -> ${binding.kind} 展示节点:写入 \`${binding.field}\`。`)
|
|
2070
2349
|
.filter((line, index, arr) => arr.indexOf(line) === index);
|
|
2350
|
+
const envelopeExample = [
|
|
2351
|
+
"---agentflow",
|
|
2352
|
+
"resultFile: outputs/result.html",
|
|
2353
|
+
slots.length ? "outParams:" : "",
|
|
2354
|
+
...slots.slice(0, 3).map((name) => ` ${name}: <${name} 的短值>`),
|
|
2355
|
+
"---end",
|
|
2356
|
+
].filter(Boolean).join("\n");
|
|
2071
2357
|
return [
|
|
2072
2358
|
"## Workspace 输出协议",
|
|
2073
2359
|
"",
|
|
2074
|
-
"
|
|
2075
|
-
"
|
|
2076
|
-
"
|
|
2077
|
-
"
|
|
2360
|
+
"如果只有一个默认输出,直接输出正文即可,系统会写入 `result` / `content` 输出口。",
|
|
2361
|
+
"如果需要多个输出,或需要返回文件路径,最后输出一个轻量 envelope;不要把大段 HTML/Markdown/SQL 包进 JSON 字符串。",
|
|
2362
|
+
"长内容、HTML、Markdown、SQL、CSV、图片等 artifact 优先写入当前 workspace 的 `outputs/` 目录,然后在 `resultFile` 或 `outParams.<name>File` 返回相对路径。",
|
|
2363
|
+
"普通 assistant 文本会被当作本节点输出内容;执行过程中不要发送进度说明、解释或寒暄,例如“正在读取/正在生成/准备输出”。需要思考时只使用内部 thinking,最终只发送正文或 envelope。",
|
|
2364
|
+
"envelope 格式:",
|
|
2078
2365
|
"",
|
|
2079
|
-
|
|
2366
|
+
envelopeExample,
|
|
2080
2367
|
"",
|
|
2081
|
-
"- `result
|
|
2082
|
-
"- `outParams
|
|
2368
|
+
"- `result`:默认输出正文;`resultFile`:默认输出对应的 workspace 相对文件路径。",
|
|
2369
|
+
"- `outParams`:具名输出参数;`outParams.<name>File`:具名输出对应的 workspace 相对文件路径。",
|
|
2370
|
+
"- 旧格式 `{ \"result\": ..., \"outParams\": ... }` 仍兼容,但新输出优先使用正文或 envelope。",
|
|
2083
2371
|
...(slotDisplayRules.length ? ["- 当前输出引脚与展示节点映射:", ...slotDisplayRules] : []),
|
|
2084
2372
|
...(slots.length
|
|
2085
|
-
? [`- 当前节点具名输出槽:${slots.map((name) => `\`${name}\``).join("、")}。例如任务要求写入 \`${slots[0]}\` 时,放到 \`outParams.${slots[0]}\`。`]
|
|
2086
|
-
: ["-
|
|
2087
|
-
"- 如果 `result` 需要承载表格、ChartSpec
|
|
2373
|
+
? [`- 当前节点具名输出槽:${slots.map((name) => `\`${name}\``).join("、")}。例如任务要求写入 \`${slots[0]}\` 时,放到 \`outParams.${slots[0]}\`;如果写成文件,放到 \`outParams.${slots[0]}File\`。`]
|
|
2374
|
+
: ["- 当前节点没有额外具名输出槽;只有默认输出时不需要 envelope。"]),
|
|
2375
|
+
"- 如果 `result` 需要承载表格、ChartSpec 等短结构化内容,可以直接输出对象或正文;系统会转换给下游展示节点。",
|
|
2088
2376
|
].join("\n");
|
|
2089
2377
|
}
|
|
2090
2378
|
|
|
@@ -2144,8 +2432,9 @@ function workspaceRunPlan(graph, runNodeId) {
|
|
|
2144
2432
|
|
|
2145
2433
|
function workspaceUpstreamText(graph, nodeId, outputs) {
|
|
2146
2434
|
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
2147
|
-
const
|
|
2148
|
-
|
|
2435
|
+
const incoming = edges
|
|
2436
|
+
.filter((edge) => String(edge?.target || "") === String(nodeId))
|
|
2437
|
+
.filter((edge) => !isWorkspaceSemanticInputSlot(workspaceTargetSlotForEdge(graph, edge)));
|
|
2149
2438
|
const contentEdge = incoming.find((edge) => String(edge?.targetHandle || "") === "input-1") || incoming[0];
|
|
2150
2439
|
if (!contentEdge) return "";
|
|
2151
2440
|
return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge);
|
|
@@ -2233,7 +2522,7 @@ function workspaceNodeTmpDirectoryBlock(nodeTmpDir) {
|
|
|
2233
2522
|
"- 如果确实需要创建中间文件,只能写入该目录,路径也可通过环境变量 `AGENTFLOW_NODE_TMP_DIR` 获取。",
|
|
2234
2523
|
"- 不要在 workspace 根目录、业务仓库根目录或当前 cwd 下创建 `temp_*`、`_out.json`、`tmp.html` 等临时产物。",
|
|
2235
2524
|
"- 不要自行删除该目录或其中的最终待读文件;AgentFlow 会在节点运行结束后统一清理。",
|
|
2236
|
-
"-
|
|
2525
|
+
"- 短文本结果可直接输出;长 HTML/Markdown/SQL 等正式 artifact 应写入 workspace 的 `outputs/` 目录并返回相对路径,不要为了输出而写临时文件、cat 文件再粘贴。",
|
|
2237
2526
|
].join("\n");
|
|
2238
2527
|
}
|
|
2239
2528
|
|
|
@@ -2410,7 +2699,8 @@ function buildWorkspaceMcpManifestBlock(results, servers = [], selectedNames = [
|
|
|
2410
2699
|
function workspaceWriteDisplayContent(instance, content) {
|
|
2411
2700
|
const next = { ...(instance || {}) };
|
|
2412
2701
|
const kind = workspaceDisplayKind(next.definitionId);
|
|
2413
|
-
const
|
|
2702
|
+
const unwrapped = workspaceUnwrapOutputEnvelopeForDisplay(content);
|
|
2703
|
+
const text = kind === "html" ? normalizeHtmlDisplayContent(unwrapped) : String(unwrapped || "");
|
|
2414
2704
|
const primaryName = kind === "image" ? "src" : "content";
|
|
2415
2705
|
next.body = text;
|
|
2416
2706
|
next.input = (Array.isArray(next.input) ? next.input : []).map((slot) => (
|
|
@@ -2426,12 +2716,21 @@ function workspaceWriteDisplayContent(instance, content) {
|
|
|
2426
2716
|
return next;
|
|
2427
2717
|
}
|
|
2428
2718
|
|
|
2719
|
+
function workspaceUnwrapOutputEnvelopeForDisplay(content) {
|
|
2720
|
+
const raw = String(content || "").trim();
|
|
2721
|
+
if (!raw) return "";
|
|
2722
|
+
if (!/---agentflow\b|["']result["']\s*:|["']outParams["']\s*:|["']resultFile["']\s*:/i.test(raw)) return raw;
|
|
2723
|
+
const structured = workspaceStructuredAgentOutput(raw);
|
|
2724
|
+
return structured.structured ? String(structured.result || "") : raw;
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2429
2727
|
function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null) {
|
|
2430
2728
|
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
2431
2729
|
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
2432
2730
|
const updated = [];
|
|
2433
2731
|
for (const edge of edges) {
|
|
2434
2732
|
if (String(edge?.source || "") !== String(sourceId)) continue;
|
|
2733
|
+
if (isWorkspaceSemanticInputSlot(workspaceTargetSlotForEdge(graph, edge))) continue;
|
|
2435
2734
|
const targetId = String(edge?.target || "");
|
|
2436
2735
|
const target = instances[targetId];
|
|
2437
2736
|
if (!target || !workspaceDisplayKind(target.definitionId)) continue;
|
|
@@ -2969,7 +3268,10 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
2969
3268
|
firstAgentEventSeen = true;
|
|
2970
3269
|
emitTiming(nodeId, "agent-first-event", spawnStartedAt, { attempt, firstType: ev?.type || "" });
|
|
2971
3270
|
}
|
|
2972
|
-
|
|
3271
|
+
const eventToEmit = (ev?.type === "natural" && (ev.kind === "result" || ev.kind === "assistant") && typeof ev.text === "string")
|
|
3272
|
+
? { ...ev, text: workspaceCanonicalAgentOutput(ev.text), nodeId }
|
|
3273
|
+
: { ...ev, nodeId };
|
|
3274
|
+
emit(eventToEmit);
|
|
2973
3275
|
if (ev?.type === "natural" && ev.kind === "assistant" && typeof ev.text === "string") {
|
|
2974
3276
|
attemptContent += (attemptContent ? "\n" : "") + ev.text;
|
|
2975
3277
|
}
|
|
@@ -2988,7 +3290,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
2988
3290
|
if (typeof opts.onActiveChild === "function") opts.onActiveChild(null);
|
|
2989
3291
|
}
|
|
2990
3292
|
throwIfAborted();
|
|
2991
|
-
content = attemptContent
|
|
3293
|
+
content = workspaceCanonicalAgentOutput(attemptContent);
|
|
2992
3294
|
break;
|
|
2993
3295
|
} catch (e) {
|
|
2994
3296
|
if (signal?.aborted || e?.code === "WORKSPACE_RUN_ABORTED") throwIfAborted();
|
|
@@ -4140,6 +4442,67 @@ export function startUiServer({
|
|
|
4140
4442
|
return;
|
|
4141
4443
|
}
|
|
4142
4444
|
|
|
4445
|
+
if (req.method === "POST" && url.pathname === "/api/workspace/html-screenshot") {
|
|
4446
|
+
let payload;
|
|
4447
|
+
try {
|
|
4448
|
+
payload = JSON.parse(await readBody(req));
|
|
4449
|
+
} catch {
|
|
4450
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
4451
|
+
return;
|
|
4452
|
+
}
|
|
4453
|
+
try {
|
|
4454
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
4455
|
+
flowId: payload.flowId || "",
|
|
4456
|
+
flowSource: payload.flowSource || "user",
|
|
4457
|
+
archived: payload.archived === true || payload.flowArchived === true,
|
|
4458
|
+
}, userCtx);
|
|
4459
|
+
if (scoped.error) {
|
|
4460
|
+
json(res, 400, { error: scoped.error });
|
|
4461
|
+
return;
|
|
4462
|
+
}
|
|
4463
|
+
const sourceFilePath = String(payload.sourceFilePath || payload.path || "").trim();
|
|
4464
|
+
let html = String(payload.content || "");
|
|
4465
|
+
let baseDir = scoped.root;
|
|
4466
|
+
if (sourceFilePath) {
|
|
4467
|
+
const { abs } = resolveWorkspaceFilePath(scoped.root, sourceFilePath);
|
|
4468
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
|
|
4469
|
+
json(res, 404, { error: "HTML file not found" });
|
|
4470
|
+
return;
|
|
4471
|
+
}
|
|
4472
|
+
const stat = fs.statSync(abs);
|
|
4473
|
+
if (stat.size > 5 * 1024 * 1024) {
|
|
4474
|
+
json(res, 413, { error: "HTML file too large" });
|
|
4475
|
+
return;
|
|
4476
|
+
}
|
|
4477
|
+
if (!html.trim()) html = fs.readFileSync(abs, "utf-8");
|
|
4478
|
+
baseDir = path.dirname(abs);
|
|
4479
|
+
}
|
|
4480
|
+
if (!html.trim()) {
|
|
4481
|
+
json(res, 400, { error: "Missing HTML content" });
|
|
4482
|
+
return;
|
|
4483
|
+
}
|
|
4484
|
+
const screenshot = await renderHtmlScreenshotWithChrome({
|
|
4485
|
+
html,
|
|
4486
|
+
workspaceRoot: scoped.root,
|
|
4487
|
+
baseDir,
|
|
4488
|
+
width: payload.width,
|
|
4489
|
+
height: payload.height,
|
|
4490
|
+
});
|
|
4491
|
+
const png = screenshot.png;
|
|
4492
|
+
const filename = sanitizeWorkspaceUploadName(payload.filename || "html-render.png").replace(/\.[^.]+$/i, ".png");
|
|
4493
|
+
res.writeHead(200, {
|
|
4494
|
+
"Content-Type": "image/png",
|
|
4495
|
+
"Content-Length": png.length,
|
|
4496
|
+
"Cache-Control": "no-store",
|
|
4497
|
+
"Content-Disposition": workspaceDownloadContentDisposition(filename),
|
|
4498
|
+
});
|
|
4499
|
+
res.end(png);
|
|
4500
|
+
} catch (e) {
|
|
4501
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
4502
|
+
}
|
|
4503
|
+
return;
|
|
4504
|
+
}
|
|
4505
|
+
|
|
4143
4506
|
if (req.method === "POST" && url.pathname === "/api/workspace/file") {
|
|
4144
4507
|
let payload;
|
|
4145
4508
|
try {
|
|
@@ -4407,6 +4770,22 @@ export function startUiServer({
|
|
|
4407
4770
|
json(res, 400, { error: scoped.error });
|
|
4408
4771
|
return;
|
|
4409
4772
|
}
|
|
4773
|
+
const targetFilePath = String(payload?.targetFilePath || "").trim();
|
|
4774
|
+
let targetFile = null;
|
|
4775
|
+
if (targetFilePath) {
|
|
4776
|
+
if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
|
|
4777
|
+
json(res, 400, { error: "Cannot edit builtin or archived pipeline workspace" });
|
|
4778
|
+
return;
|
|
4779
|
+
}
|
|
4780
|
+
targetFile = resolveWorkspaceFilePath(scoped.root, targetFilePath);
|
|
4781
|
+
if (!targetFile.rel) {
|
|
4782
|
+
json(res, 400, { error: "Missing artifact file path" });
|
|
4783
|
+
return;
|
|
4784
|
+
}
|
|
4785
|
+
}
|
|
4786
|
+
const beforeTargetContent = targetFile && fs.existsSync(targetFile.abs) && fs.statSync(targetFile.abs).isFile()
|
|
4787
|
+
? fs.readFileSync(targetFile.abs, "utf-8")
|
|
4788
|
+
: null;
|
|
4410
4789
|
const promptText = buildWorkspaceNodeChatPrompt(payload);
|
|
4411
4790
|
const modelKey = typeof payload?.model === "string" ? payload.model.trim() : "";
|
|
4412
4791
|
let content = "";
|
|
@@ -4425,12 +4804,29 @@ export function startUiServer({
|
|
|
4425
4804
|
},
|
|
4426
4805
|
});
|
|
4427
4806
|
await handle.finished;
|
|
4428
|
-
|
|
4807
|
+
let candidateContent = targetFile
|
|
4808
|
+
? (fs.existsSync(targetFile.abs) && fs.statSync(targetFile.abs).isFile()
|
|
4809
|
+
? fs.readFileSync(targetFile.abs, "utf-8")
|
|
4810
|
+
: "")
|
|
4811
|
+
: content.trim();
|
|
4812
|
+
if (targetFile) {
|
|
4813
|
+
const unwrappedTargetContent = workspaceUnwrapOutputEnvelopeForDisplay(candidateContent);
|
|
4814
|
+
if (unwrappedTargetContent && unwrappedTargetContent !== candidateContent) {
|
|
4815
|
+
fs.writeFileSync(targetFile.abs, unwrappedTargetContent, "utf-8");
|
|
4816
|
+
candidateContent = unwrappedTargetContent;
|
|
4817
|
+
}
|
|
4818
|
+
}
|
|
4819
|
+
if (targetFile && beforeTargetContent != null && candidateContent === beforeTargetContent) {
|
|
4820
|
+
json(res, 500, { error: "Agent 未修改目标展示文件,请换一种更明确的描述后重试。" });
|
|
4821
|
+
return;
|
|
4822
|
+
}
|
|
4429
4823
|
json(res, 200, {
|
|
4430
4824
|
ok: true,
|
|
4431
4825
|
sessionId: String(payload?.sessionId || "") || `nodechat_${Date.now()}`,
|
|
4432
|
-
reply: candidateContent,
|
|
4826
|
+
reply: targetFile ? content.trim() : candidateContent,
|
|
4433
4827
|
candidateContent,
|
|
4828
|
+
directFileEdit: Boolean(targetFile),
|
|
4829
|
+
artifactPath: targetFile?.rel || "",
|
|
4434
4830
|
events,
|
|
4435
4831
|
});
|
|
4436
4832
|
} catch (e) {
|