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