@fieldwangai/agentflow 0.1.62 → 0.1.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/lib/ui-server.mjs +233 -168
- 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-CyyeTRvo.js +0 -254
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,57 @@ function workspaceDisplayTextFilePath(value, kind = "") {
|
|
|
2195
2207
|
return allowed.has(ext) ? clean : "";
|
|
2196
2208
|
}
|
|
2197
2209
|
|
|
2210
|
+
function workspaceSafeNodeOutputRelPath(value) {
|
|
2211
|
+
const text = String(value || "").trim().replace(/^["']|["']$/g, "");
|
|
2212
|
+
if (!text || text.length > 260) return "";
|
|
2213
|
+
if (/[\r\n<>]/.test(text)) return "";
|
|
2214
|
+
if (/^(?:https?:|data:|blob:|file:|javascript:|mailto:|tel:)/i.test(text)) return "";
|
|
2215
|
+
const clean = text.replace(/^\/+/, "");
|
|
2216
|
+
if (clean.includes("..") || clean.startsWith(".") || path.isAbsolute(clean)) return "";
|
|
2217
|
+
if (!clean.startsWith("outputs/")) return "";
|
|
2218
|
+
return clean;
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
function workspacePublishNodeOutputFile(runPackage, relPath) {
|
|
2222
|
+
if (!String(relPath || "").trim()) return "";
|
|
2223
|
+
const clean = workspaceSafeNodeOutputRelPath(relPath);
|
|
2224
|
+
if (!clean) throw new Error(`Agent returned an invalid output file path: ${String(relPath || "").trim()}`);
|
|
2225
|
+
const nodeRunDir = path.resolve(runPackage?.nodeRunDir || "");
|
|
2226
|
+
const workspaceOutputsDir = path.resolve(runPackage?.workspaceOutputsDir || "");
|
|
2227
|
+
if (!nodeRunDir || !workspaceOutputsDir) return clean;
|
|
2228
|
+
const src = path.resolve(nodeRunDir, clean);
|
|
2229
|
+
const nodeRootWithSep = nodeRunDir.endsWith(path.sep) ? nodeRunDir : `${nodeRunDir}${path.sep}`;
|
|
2230
|
+
if (src !== nodeRunDir && !src.startsWith(nodeRootWithSep)) {
|
|
2231
|
+
throw new Error(`Invalid node output path: ${clean}`);
|
|
2232
|
+
}
|
|
2233
|
+
if (!fs.existsSync(src) || !fs.statSync(src).isFile()) {
|
|
2234
|
+
throw new Error(`Agent returned resultFile but did not create it under node outputs: ${clean}`);
|
|
2235
|
+
}
|
|
2236
|
+
const destRel = clean.slice("outputs/".length);
|
|
2237
|
+
const dest = path.resolve(workspaceOutputsDir, destRel);
|
|
2238
|
+
const workspaceOutputsWithSep = workspaceOutputsDir.endsWith(path.sep) ? workspaceOutputsDir : `${workspaceOutputsDir}${path.sep}`;
|
|
2239
|
+
if (dest !== workspaceOutputsDir && !dest.startsWith(workspaceOutputsWithSep)) {
|
|
2240
|
+
throw new Error(`Invalid workspace output path: ${clean}`);
|
|
2241
|
+
}
|
|
2242
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
2243
|
+
fs.copyFileSync(src, dest);
|
|
2244
|
+
return clean;
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
function workspacePublishAgentOutputFiles(structured, runPackage) {
|
|
2248
|
+
if (!structured?.structured || !runPackage) return structured;
|
|
2249
|
+
const resultFile = workspacePublishNodeOutputFile(runPackage, structured.resultFile);
|
|
2250
|
+
const outParams = { ...(structured.outParams || {}) };
|
|
2251
|
+
for (const [key, value] of Object.entries(outParams)) {
|
|
2252
|
+
if (!String(key || "").endsWith("File")) continue;
|
|
2253
|
+
const published = workspacePublishNodeOutputFile(runPackage, value);
|
|
2254
|
+
if (published) outParams[key] = published;
|
|
2255
|
+
}
|
|
2256
|
+
return resultFile || Object.keys(outParams).length
|
|
2257
|
+
? { ...structured, result: resultFile || structured.result, resultFile: resultFile || structured.resultFile, outParams }
|
|
2258
|
+
: structured;
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2198
2261
|
function workspaceOutputFieldForSlot(slot, index = 0) {
|
|
2199
2262
|
const name = String(slot?.name || "").trim();
|
|
2200
2263
|
if (!name || name === "result" || name === "content" || index === 0) return "result";
|
|
@@ -2212,19 +2275,6 @@ function workspaceDisplayKindExample(kind, field) {
|
|
|
2212
2275
|
return `<${field} 的值>`;
|
|
2213
2276
|
}
|
|
2214
2277
|
|
|
2215
|
-
function workspaceDisplayFieldRule(kind, field, slotName = "") {
|
|
2216
|
-
const slotText = field === "result" ? "`result` 或 `resultFile`" : `\`${field}\` 或 \`${field}File\``;
|
|
2217
|
-
const prefix = slotName ? `- 输出引脚 \`${slotName}\` 连接了 ${kind} 展示节点:` : `- 下游连接了 ${kind} 展示节点:`;
|
|
2218
|
-
if (kind === "html") return `${prefix}将可直接放入 iframe 渲染的 HTML 放在 ${slotText} 中;内容较长时优先写入 outputs/*.html 并返回文件路径。不要使用 Markdown 代码围栏。`;
|
|
2219
|
-
if (kind === "markdown") return `${prefix}将 Markdown 正文放在 ${slotText} 中;内容较长时优先写入 outputs/*.md 并返回文件路径。除非正文确实需要代码块,否则不要额外包裹代码围栏。`;
|
|
2220
|
-
if (kind === "mermaid") return `${prefix}将 Mermaid 图表代码放在 ${slotText} 中,例如 flowchart/sequenceDiagram;不要使用 Markdown 代码围栏。`;
|
|
2221
|
-
if (kind === "ascii") return `${prefix}将纯文本/ASCII 图或表格放在 ${slotText} 中;不要输出 HTML 或 Markdown 装饰。`;
|
|
2222
|
-
if (kind === "image") return `${prefix}将可作为 img src 使用的图片地址、data URL 或 base64 data URL 放在 ${slotText} 中;不要输出 Markdown 图片语法。`;
|
|
2223
|
-
if (kind === "chart") return `${prefix}将 ChartSpec JSON 对象放在 ${slotText} 中。ChartSpec 必须包含 "type":"chart"、"version":"1.0"、"renderer":"echarts"、"option";option.series[].type 只使用 line/bar/pie/scatter/radar/heatmap/tree/treemap/sunburst/sankey/graph/gauge/funnel;不要输出 HTML、script、iframe 或 JS 函数。`;
|
|
2224
|
-
if (kind === "table") return `${prefix}将表格数据放在 ${slotText} 中。推荐格式:{"columns":["列名1","列名2"],"rows":[["值1","值2"]]};也可使用对象数组、Markdown 表格、CSV 或 TSV。不要输出 HTML。`;
|
|
2225
|
-
return `${prefix}将展示内容放在 ${slotText} 中。`;
|
|
2226
|
-
}
|
|
2227
|
-
|
|
2228
2278
|
function workspaceDownstreamOutputDisplayBindings(graph, nodeId) {
|
|
2229
2279
|
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
2230
2280
|
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
@@ -2284,45 +2334,24 @@ function normalizeHtmlDisplayContent(content) {
|
|
|
2284
2334
|
return text;
|
|
2285
2335
|
}
|
|
2286
2336
|
|
|
2287
|
-
function
|
|
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");
|
|
2337
|
+
function workspaceBodyPlaceholderNames(body) {
|
|
2338
|
+
const names = new Set();
|
|
2339
|
+
const raw = String(body || "");
|
|
2340
|
+
raw.replace(/\$\{([A-Za-z_][A-Za-z0-9_-]*)\}/g, (_match, name) => {
|
|
2341
|
+
if (name) names.add(String(name));
|
|
2342
|
+
return _match;
|
|
2343
|
+
});
|
|
2344
|
+
return names;
|
|
2306
2345
|
}
|
|
2307
2346
|
|
|
2308
|
-
function
|
|
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");
|
|
2347
|
+
function workspaceRelevantInputValues(body, inputValues = {}) {
|
|
2348
|
+
const placeholders = workspaceBodyPlaceholderNames(body);
|
|
2349
|
+
if (!placeholders.size) return { values: inputValues || {}, placeholders };
|
|
2350
|
+
const values = {};
|
|
2351
|
+
for (const [name, value] of Object.entries(inputValues || {})) {
|
|
2352
|
+
if (placeholders.has(name)) values[name] = value;
|
|
2353
|
+
}
|
|
2354
|
+
return { values, placeholders };
|
|
2326
2355
|
}
|
|
2327
2356
|
|
|
2328
2357
|
function workspaceOutputProtocolRequirements(graph, nodeId) {
|
|
@@ -2340,39 +2369,46 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
|
|
|
2340
2369
|
return name && type !== "node" && name !== "next" && name !== "result" && name !== "content";
|
|
2341
2370
|
})
|
|
2342
2371
|
.map((slot) => String(slot.name).trim());
|
|
2343
|
-
const
|
|
2344
|
-
|
|
2345
|
-
:
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2372
|
+
const resultKind = displayByField.get("result") || "";
|
|
2373
|
+
const resultExtByKind = {
|
|
2374
|
+
html: "html",
|
|
2375
|
+
markdown: "md",
|
|
2376
|
+
mermaid: "mmd",
|
|
2377
|
+
ascii: "txt",
|
|
2378
|
+
chart: "json",
|
|
2379
|
+
table: "json",
|
|
2380
|
+
};
|
|
2381
|
+
const resultExt = resultExtByKind[resultKind] || "txt";
|
|
2382
|
+
const resultFile = `outputs/result.${resultExt}`;
|
|
2383
|
+
const resultKindText = resultKind ? ` ${resultKind}` : "";
|
|
2384
|
+
const resultGuidance = {
|
|
2385
|
+
html: "内容必须是可直接放入 iframe 渲染的 HTML;不要使用 Markdown 代码围栏。",
|
|
2386
|
+
markdown: "内容必须是 Markdown 正文;除非正文确实需要代码块,否则不要额外包裹代码围栏。",
|
|
2387
|
+
mermaid: "内容必须是 Mermaid 图表代码,例如 flowchart/sequenceDiagram;不要使用 Markdown 代码围栏。",
|
|
2388
|
+
ascii: "内容必须是纯文本/ASCII 图或表格;不要输出 HTML 或 Markdown 装饰。",
|
|
2389
|
+
image: "内容必须是可作为 img src 使用的图片地址、data URL 或 base64 data URL;不要输出 Markdown 图片语法。",
|
|
2390
|
+
chart: "内容必须是 ChartSpec JSON 对象,包含 type/version/renderer/option;不要输出 HTML、script、iframe 或 JS 函数。",
|
|
2391
|
+
table: "内容必须是表格数据,推荐 JSON:{\"columns\":[...],\"rows\":[...]};不要输出 HTML。",
|
|
2392
|
+
}[resultKind] || "内容应满足任务要求。";
|
|
2350
2393
|
const envelopeExample = [
|
|
2351
2394
|
"---agentflow",
|
|
2352
|
-
|
|
2395
|
+
`resultFile: ${resultFile}`,
|
|
2353
2396
|
slots.length ? "outParams:" : "",
|
|
2354
|
-
...slots.slice(0, 3).map((name) =>
|
|
2397
|
+
...slots.slice(0, 3).map((name) => {
|
|
2398
|
+
const kind = displayByField.get(`outParams.${name}`) || "";
|
|
2399
|
+
const ext = resultExtByKind[kind] || "txt";
|
|
2400
|
+
return kind ? ` ${name}File: outputs/${name}.${ext}` : ` ${name}: <${name} 的短值>`;
|
|
2401
|
+
}),
|
|
2355
2402
|
"---end",
|
|
2356
2403
|
].filter(Boolean).join("\n");
|
|
2357
2404
|
return [
|
|
2358
|
-
"##
|
|
2405
|
+
"## 输出",
|
|
2359
2406
|
"",
|
|
2360
|
-
|
|
2361
|
-
"
|
|
2362
|
-
"
|
|
2363
|
-
"普通 assistant 文本会被当作本节点输出内容;执行过程中不要发送进度说明、解释或寒暄,例如“正在读取/正在生成/准备输出”。需要思考时只使用内部 thinking,最终只发送正文或 envelope。",
|
|
2364
|
-
"envelope 格式:",
|
|
2407
|
+
`请把${resultKindText}结果写入 \`${resultFile}\`。${resultGuidance}`,
|
|
2408
|
+
...(slots.length ? [`额外输出:${slots.map((name) => `\`${name}\``).join("、")}。短值可写在 \`outParams\`,文件值写成 \`outParams.<name>File\`。`] : []),
|
|
2409
|
+
"最终只输出下面的 agentflow envelope,不要输出解释、进度或其它文字:",
|
|
2365
2410
|
"",
|
|
2366
2411
|
envelopeExample,
|
|
2367
|
-
"",
|
|
2368
|
-
"- `result`:默认输出正文;`resultFile`:默认输出对应的 workspace 相对文件路径。",
|
|
2369
|
-
"- `outParams`:具名输出参数;`outParams.<name>File`:具名输出对应的 workspace 相对文件路径。",
|
|
2370
|
-
"- 旧格式 `{ \"result\": ..., \"outParams\": ... }` 仍兼容,但新输出优先使用正文或 envelope。",
|
|
2371
|
-
...(slotDisplayRules.length ? ["- 当前输出引脚与展示节点映射:", ...slotDisplayRules] : []),
|
|
2372
|
-
...(slots.length
|
|
2373
|
-
? [`- 当前节点具名输出槽:${slots.map((name) => `\`${name}\``).join("、")}。例如任务要求写入 \`${slots[0]}\` 时,放到 \`outParams.${slots[0]}\`;如果写成文件,放到 \`outParams.${slots[0]}File\`。`]
|
|
2374
|
-
: ["- 当前节点没有额外具名输出槽;只有默认输出时不需要 envelope。"]),
|
|
2375
|
-
"- 如果 `result` 需要承载表格、ChartSpec 等短结构化内容,可以直接输出对象或正文;系统会转换给下游展示节点。",
|
|
2376
2412
|
].join("\n");
|
|
2377
2413
|
}
|
|
2378
2414
|
|
|
@@ -2458,79 +2494,44 @@ function isWorkspaceSemanticInputSlot(slot) {
|
|
|
2458
2494
|
return type === "node" || name === "prev" || name === "next" || name === "skillsContext" || name === "mcpContext" || name === "workspaceContext" || name === "gitContext";
|
|
2459
2495
|
}
|
|
2460
2496
|
|
|
2461
|
-
function
|
|
2462
|
-
const instance = graph?.instances?.[String(nodeId || "")] || {};
|
|
2463
|
-
const label = String(instance?.label || nodeId || "").trim();
|
|
2464
|
-
const defId = String(instance?.definitionId || "").trim();
|
|
2465
|
-
return defId && defId !== label ? `${label || nodeId} (${defId})` : (label || nodeId);
|
|
2466
|
-
}
|
|
2467
|
-
|
|
2468
|
-
function workspaceSlotBrief(slot, fallback) {
|
|
2469
|
-
const name = String(slot?.name || "").trim();
|
|
2470
|
-
const type = String(slot?.type || "").trim();
|
|
2471
|
-
return `${name || fallback}${type ? `:${type}` : ""}`;
|
|
2472
|
-
}
|
|
2473
|
-
|
|
2474
|
-
function workspaceNodeConnectionContextBlock(graph, nodeId) {
|
|
2475
|
-
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
2476
|
-
const incoming = edges.filter((edge) => String(edge?.target || "") === String(nodeId));
|
|
2477
|
-
const outgoing = edges.filter((edge) => String(edge?.source || "") === String(nodeId));
|
|
2478
|
-
const inputLines = incoming.map((edge) => {
|
|
2479
|
-
const sourceSlot = workspaceSourceSlotForEdge(graph, edge);
|
|
2480
|
-
const targetSlot = workspaceTargetSlotForEdge(graph, edge);
|
|
2481
|
-
const targetName = String(targetSlot?.name || "").trim();
|
|
2482
|
-
const semantic = isWorkspaceSemanticInputSlot(targetSlot) ? "上下文输入" : "业务输入";
|
|
2483
|
-
return `- ${workspaceNodeBrief(graph, edge.source)} \`${workspaceSlotBrief(sourceSlot, edge.sourceHandle || "output")}\` -> 当前 \`${workspaceSlotBrief(targetSlot, edge.targetHandle || "input")}\`(${semantic}${targetName ? `:${targetName}` : ""})`;
|
|
2484
|
-
});
|
|
2485
|
-
const outputLines = outgoing.map((edge) => {
|
|
2486
|
-
const sourceSlot = workspaceSourceSlotForEdge(graph, edge);
|
|
2487
|
-
const targetSlot = workspaceTargetSlotForEdge(graph, edge);
|
|
2488
|
-
const targetKind = workspaceDisplayKind(graph?.instances?.[String(edge?.target || "")]?.definitionId);
|
|
2489
|
-
return `- 当前 \`${workspaceSlotBrief(sourceSlot, edge.sourceHandle || "output")}\` -> ${workspaceNodeBrief(graph, edge.target)} \`${workspaceSlotBrief(targetSlot, edge.targetHandle || "input")}\`${targetKind ? `(${targetKind} 展示)` : ""}`;
|
|
2490
|
-
});
|
|
2491
|
-
if (!inputLines.length && !outputLines.length) return "";
|
|
2492
|
-
return [
|
|
2493
|
-
"## 当前节点连线",
|
|
2494
|
-
"",
|
|
2495
|
-
inputLines.length ? "### 直接上游" : "",
|
|
2496
|
-
...inputLines,
|
|
2497
|
-
outputLines.length ? "\n### 直接下游" : "",
|
|
2498
|
-
...outputLines,
|
|
2499
|
-
"",
|
|
2500
|
-
"只把直接上游的业务输入和上下文输入当作本节点依据;直接下游只用于确定输出字段和格式。",
|
|
2501
|
-
].filter(Boolean).join("\n");
|
|
2502
|
-
}
|
|
2503
|
-
|
|
2504
|
-
function workspaceResolvedInputValuesBlock(inputValues = {}) {
|
|
2497
|
+
function workspaceAgentInputBlock(inputValues = {}) {
|
|
2505
2498
|
const entries = Object.entries(inputValues || {}).filter(([name, value]) => String(name || "").trim() && String(value || "").trim());
|
|
2506
|
-
if (!entries.length) return "";
|
|
2499
|
+
if (!entries.length) return "## 输入\n\n无。";
|
|
2507
2500
|
const lines = entries.map(([name, value]) => {
|
|
2508
2501
|
const text = String(value || "");
|
|
2509
2502
|
const clipped = text.length > 6000 ? `${text.slice(0, 6000)}\n...[已截断 ${text.length - 6000} 字]` : text;
|
|
2510
2503
|
return `### ${name}\n\n${clipped}`;
|
|
2511
2504
|
});
|
|
2512
|
-
return ["##
|
|
2505
|
+
return ["## 输入", "", ...lines].join("\n");
|
|
2513
2506
|
}
|
|
2514
2507
|
|
|
2515
|
-
function
|
|
2516
|
-
const
|
|
2517
|
-
|
|
2508
|
+
function workspaceNodeFileBoundaryBlock(runPackage = {}) {
|
|
2509
|
+
const nodeRunDir = String(runPackage?.nodeRunDir || "").trim();
|
|
2510
|
+
const nodeTmpDir = String(runPackage?.nodeTmpDir || "").trim();
|
|
2511
|
+
const outputsRel = String(runPackage?.outputsRel || "outputs").trim() || "outputs";
|
|
2512
|
+
if (!nodeRunDir && !nodeTmpDir) return "";
|
|
2518
2513
|
return [
|
|
2519
|
-
"##
|
|
2514
|
+
"## 文件边界",
|
|
2520
2515
|
"",
|
|
2521
|
-
`-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
"-
|
|
2525
|
-
"-
|
|
2526
|
-
].join("\n");
|
|
2516
|
+
nodeRunDir ? `- 当前执行目录:\`${nodeRunDir}\`。` : "",
|
|
2517
|
+
nodeTmpDir ? `- 临时文件只能写入:\`${nodeTmpDir}\`,也可通过环境变量 \`AGENTFLOW_NODE_TMP_DIR\` 获取。` : "",
|
|
2518
|
+
`- 正式产物写入本任务 \`${outputsRel}/\`,例如 \`${outputsRel}/result.html\`、\`${outputsRel}/result.md\`;返回时仍使用 \`${outputsRel}/...\` 相对路径。`,
|
|
2519
|
+
"- 不要在执行目录根部创建 `temp_*`、`_out.json`、`tmp.html` 等临时产物。",
|
|
2520
|
+
"- 不要自行删除 run package;AgentFlow 会在运行结束后统一清理。",
|
|
2521
|
+
].filter(Boolean).join("\n");
|
|
2527
2522
|
}
|
|
2528
2523
|
|
|
2529
|
-
function workspaceTaskUpstreamText(graph, nodeId, outputs) {
|
|
2524
|
+
function workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputNames = null) {
|
|
2530
2525
|
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
2531
2526
|
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
2532
2527
|
const incoming = edges.filter((edge) => String(edge?.target || "") === String(nodeId));
|
|
2533
|
-
|
|
2528
|
+
let contentEdges = incoming.filter((edge) => !isWorkspaceSemanticInputSlot(workspaceTargetSlotForEdge(graph, edge)));
|
|
2529
|
+
if (relevantInputNames && relevantInputNames.size) {
|
|
2530
|
+
contentEdges = contentEdges.filter((edge) => {
|
|
2531
|
+
const slot = workspaceTargetSlotForEdge(graph, edge);
|
|
2532
|
+
return relevantInputNames.has(String(slot?.name || "").trim());
|
|
2533
|
+
});
|
|
2534
|
+
}
|
|
2534
2535
|
const contentEdge = contentEdges.find((edge) => String(edge?.targetHandle || "") === "input-1") || contentEdges[0];
|
|
2535
2536
|
if (!contentEdge) return "";
|
|
2536
2537
|
return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge);
|
|
@@ -2744,28 +2745,21 @@ function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null)
|
|
|
2744
2745
|
function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock = "", inputValues = {}, nodeTmpDir = "") {
|
|
2745
2746
|
const instance = graph.instances[nodeId] || {};
|
|
2746
2747
|
const body = workspaceResolveBodyPlaceholders(instance.body || "", inputValues).trim();
|
|
2747
|
-
const
|
|
2748
|
-
const
|
|
2749
|
-
const
|
|
2750
|
-
const
|
|
2751
|
-
const tmpDirectory = workspaceNodeTmpDirectoryBlock(nodeTmpDir);
|
|
2752
|
-
const downstreamRequirements = workspaceDownstreamDisplayRequirements(graph, nodeId);
|
|
2748
|
+
const { values: relevantInputValues, placeholders } = workspaceRelevantInputValues(instance.body || "", inputValues);
|
|
2749
|
+
const runPackage = typeof nodeTmpDir === "object" && nodeTmpDir ? nodeTmpDir : { nodeTmpDir: String(nodeTmpDir || "") };
|
|
2750
|
+
const inputBlock = workspaceAgentInputBlock(relevantInputValues);
|
|
2751
|
+
const fileBoundary = workspaceNodeFileBoundaryBlock(runPackage);
|
|
2753
2752
|
const outputProtocolRequirements = workspaceOutputProtocolRequirements(graph, nodeId);
|
|
2754
2753
|
return [
|
|
2755
|
-
"
|
|
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}` : "",
|
|
2754
|
+
"你正在执行一个独立任务。只使用本提示中的任务、输入、可用能力和文件边界。",
|
|
2755
|
+
fileBoundary ? `\n${fileBoundary}` : "",
|
|
2756
|
+
inputBlock ? `\n${inputBlock}` : "",
|
|
2757
|
+
placeholders.size ? "\n任务只显式引用了上面的输入槽;其它未被 `${...}` 引用的已连接业务输入不要作为分析依据。" : "",
|
|
2758
|
+
skillsBlock ? `\n## 可用能力\n\n${skillsBlock}` : "",
|
|
2759
|
+
mcpBlock ? `\n## 可用 MCP\n\n${mcpBlock}` : "",
|
|
2760
|
+
upstreamText ? `\n## 上游正文\n\n${upstreamText}` : "",
|
|
2766
2761
|
outputProtocolRequirements ? `\n${outputProtocolRequirements}` : "",
|
|
2767
|
-
`\n##
|
|
2768
|
-
`\n## 节点任务\n\n${body || upstreamText}`,
|
|
2762
|
+
`\n## 任务\n\n${body || upstreamText}`,
|
|
2769
2763
|
].filter(Boolean).join("\n");
|
|
2770
2764
|
}
|
|
2771
2765
|
|
|
@@ -2857,6 +2851,40 @@ function workspaceCreateNodeTmpDir(runTmpRoot, nodeId) {
|
|
|
2857
2851
|
return dir;
|
|
2858
2852
|
}
|
|
2859
2853
|
|
|
2854
|
+
function workspaceCreateNodeRunPackage(runTmpRoot, nodeId, { scopedRoot, task = "", inputValues = {}, skillsBlock = "", mcpBlock = "" } = {}) {
|
|
2855
|
+
const nodeRunDir = workspaceCreateNodeTmpDir(runTmpRoot, nodeId);
|
|
2856
|
+
const nodeTmpDir = path.join(nodeRunDir, "tmp");
|
|
2857
|
+
const outputsDir = path.join(nodeRunDir, "outputs");
|
|
2858
|
+
const workspaceOutputsDir = path.join(path.resolve(scopedRoot), "outputs");
|
|
2859
|
+
fs.mkdirSync(nodeTmpDir, { recursive: true });
|
|
2860
|
+
fs.mkdirSync(outputsDir, { recursive: true });
|
|
2861
|
+
fs.mkdirSync(workspaceOutputsDir, { recursive: true });
|
|
2862
|
+
const manifest = {
|
|
2863
|
+
version: 1,
|
|
2864
|
+
nodeId: String(nodeId || ""),
|
|
2865
|
+
nodeRunDir,
|
|
2866
|
+
nodeTmpDir,
|
|
2867
|
+
outputsDir,
|
|
2868
|
+
createdAt: new Date().toISOString(),
|
|
2869
|
+
};
|
|
2870
|
+
try {
|
|
2871
|
+
fs.writeFileSync(path.join(nodeRunDir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
2872
|
+
fs.writeFileSync(path.join(nodeRunDir, "task.md"), String(task || "").trimEnd() + "\n", "utf-8");
|
|
2873
|
+
if (Object.keys(inputValues || {}).length) {
|
|
2874
|
+
fs.writeFileSync(path.join(nodeRunDir, "inputs.json"), JSON.stringify(inputValues, null, 2) + "\n", "utf-8");
|
|
2875
|
+
}
|
|
2876
|
+
if (skillsBlock) fs.writeFileSync(path.join(nodeRunDir, "skills.md"), String(skillsBlock).trimEnd() + "\n", "utf-8");
|
|
2877
|
+
if (mcpBlock) fs.writeFileSync(path.join(nodeRunDir, "mcp.md"), String(mcpBlock).trimEnd() + "\n", "utf-8");
|
|
2878
|
+
} catch {
|
|
2879
|
+
// Runtime metadata is best-effort and should not block node execution.
|
|
2880
|
+
}
|
|
2881
|
+
return {
|
|
2882
|
+
...manifest,
|
|
2883
|
+
workspaceOutputsDir,
|
|
2884
|
+
outputsRel: "outputs",
|
|
2885
|
+
};
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2860
2888
|
function workspaceShouldKeepTmp(userCtx = {}) {
|
|
2861
2889
|
const env = { ...process.env, ...readMergedEnvObject(userCtx.userId) };
|
|
2862
2890
|
const value = String(env.AGENTFLOW_KEEP_TMP || env.AGENTFLOW_KEEP_WORKSPACE_TMP || "").trim().toLowerCase();
|
|
@@ -2921,9 +2949,16 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
2921
2949
|
};
|
|
2922
2950
|
const outputs = new Map();
|
|
2923
2951
|
const events = [];
|
|
2952
|
+
const runStartedAt = Date.now();
|
|
2924
2953
|
const emit = (event) => {
|
|
2925
|
-
|
|
2926
|
-
|
|
2954
|
+
const now = Date.now();
|
|
2955
|
+
const enriched = {
|
|
2956
|
+
...event,
|
|
2957
|
+
ts: Number(event?.ts) || now,
|
|
2958
|
+
runElapsedMs: Number.isFinite(event?.runElapsedMs) ? event.runElapsedMs : Math.max(0, now - runStartedAt),
|
|
2959
|
+
};
|
|
2960
|
+
events.push(enriched);
|
|
2961
|
+
if (typeof opts.onEvent === "function") opts.onEvent(enriched);
|
|
2927
2962
|
};
|
|
2928
2963
|
const emitTiming = (nodeId, label, startedAt, extra = {}) => {
|
|
2929
2964
|
const elapsedMs = Math.max(0, Date.now() - startedAt);
|
|
@@ -3233,8 +3268,9 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
3233
3268
|
}
|
|
3234
3269
|
|
|
3235
3270
|
const prepareStartedAt = Date.now();
|
|
3236
|
-
const upstreamText = workspaceTaskUpstreamText(graph, nodeId, outputs);
|
|
3237
3271
|
const inputValues = workspaceInputValues(graph, nodeId, outputs);
|
|
3272
|
+
const relevantInputs = workspaceRelevantInputValues(instance.body || "", inputValues);
|
|
3273
|
+
const upstreamText = workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputs.placeholders);
|
|
3238
3274
|
const body = workspaceResolveBodyPlaceholders(instance.body || "", inputValues).trim();
|
|
3239
3275
|
if (defId === "agent_subAgent" && !body && !String(upstreamText || "").trim()) {
|
|
3240
3276
|
throw new Error(`Workspace node ${nodeId} has no task. Fill the node body or connect upstream text.`);
|
|
@@ -3242,9 +3278,27 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
3242
3278
|
const upstreamSkillBlocks = workspaceUpstreamSkillBlocks(graph, nodeId, outputs);
|
|
3243
3279
|
const promptSkillsBlock = mergeWorkspaceSkillBlocks(upstreamSkillBlocks);
|
|
3244
3280
|
const promptMcpBlock = workspaceUpstreamMcpBlocks(graph, nodeId, outputs);
|
|
3245
|
-
const
|
|
3246
|
-
|
|
3247
|
-
|
|
3281
|
+
const runPackage = workspaceCreateNodeRunPackage(runTmpRoot, nodeId, {
|
|
3282
|
+
scopedRoot,
|
|
3283
|
+
cwd,
|
|
3284
|
+
task: body || upstreamText,
|
|
3285
|
+
inputValues: relevantInputs.values,
|
|
3286
|
+
skillsBlock: promptSkillsBlock,
|
|
3287
|
+
mcpBlock: promptMcpBlock,
|
|
3288
|
+
});
|
|
3289
|
+
const prompt = workspaceNodePrompt(graph, nodeId, upstreamText, promptSkillsBlock, promptMcpBlock, inputValues, runPackage);
|
|
3290
|
+
try {
|
|
3291
|
+
fs.writeFileSync(path.join(runPackage.nodeRunDir, "prompt.md"), prompt.trimEnd() + "\n", "utf-8");
|
|
3292
|
+
} catch {
|
|
3293
|
+
// Best-effort debug artifact only.
|
|
3294
|
+
}
|
|
3295
|
+
emitTiming(nodeId, "prepare-agent-prompt", prepareStartedAt, {
|
|
3296
|
+
promptChars: prompt.length,
|
|
3297
|
+
upstreamChars: String(upstreamText || "").length,
|
|
3298
|
+
skillsChars: promptSkillsBlock.length,
|
|
3299
|
+
mcpChars: promptMcpBlock.length,
|
|
3300
|
+
nodeRunDir: runPackage.nodeRunDir,
|
|
3301
|
+
});
|
|
3248
3302
|
emit({ type: "natural", kind: "prompt", nodeId, text: prompt });
|
|
3249
3303
|
let content = "";
|
|
3250
3304
|
const maxAttempts = 3;
|
|
@@ -3253,15 +3307,19 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
3253
3307
|
try {
|
|
3254
3308
|
const spawnStartedAt = Date.now();
|
|
3255
3309
|
let firstAgentEventSeen = false;
|
|
3310
|
+
let attemptResultContent = "";
|
|
3311
|
+
let attemptLastAssistantContent = "";
|
|
3256
3312
|
const handle = startComposerAgent({
|
|
3257
3313
|
uiWorkspaceRoot: scopedRoot,
|
|
3258
|
-
cliWorkspace:
|
|
3314
|
+
cliWorkspace: runPackage.nodeRunDir,
|
|
3259
3315
|
prompt,
|
|
3260
3316
|
modelKey,
|
|
3261
3317
|
agentflowUserId: userCtx.userId || "",
|
|
3262
3318
|
extraEnv: {
|
|
3263
3319
|
AGENTFLOW_WORKSPACE_TMP_ROOT: runTmpRoot,
|
|
3264
|
-
|
|
3320
|
+
AGENTFLOW_NODE_RUN_DIR: runPackage.nodeRunDir,
|
|
3321
|
+
AGENTFLOW_NODE_TMP_DIR: runPackage.nodeTmpDir,
|
|
3322
|
+
AGENTFLOW_OUTPUTS_DIR: runPackage.outputsDir,
|
|
3265
3323
|
},
|
|
3266
3324
|
onStreamEvent: (ev) => {
|
|
3267
3325
|
if (!firstAgentEventSeen) {
|
|
@@ -3273,7 +3331,10 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
3273
3331
|
: { ...ev, nodeId };
|
|
3274
3332
|
emit(eventToEmit);
|
|
3275
3333
|
if (ev?.type === "natural" && ev.kind === "assistant" && typeof ev.text === "string") {
|
|
3334
|
+
attemptLastAssistantContent = ev.text;
|
|
3276
3335
|
attemptContent += (attemptContent ? "\n" : "") + ev.text;
|
|
3336
|
+
} else if (ev?.type === "natural" && ev.kind === "result" && typeof ev.text === "string") {
|
|
3337
|
+
attemptResultContent = ev.text;
|
|
3277
3338
|
}
|
|
3278
3339
|
},
|
|
3279
3340
|
onToolCall: (subtype, toolName) => {
|
|
@@ -3290,7 +3351,11 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
3290
3351
|
if (typeof opts.onActiveChild === "function") opts.onActiveChild(null);
|
|
3291
3352
|
}
|
|
3292
3353
|
throwIfAborted();
|
|
3293
|
-
|
|
3354
|
+
const resultStructured = attemptResultContent ? workspaceStructuredAgentOutput(attemptResultContent) : null;
|
|
3355
|
+
const assistantStructured = attemptLastAssistantContent ? workspaceStructuredAgentOutput(attemptLastAssistantContent) : null;
|
|
3356
|
+
if (resultStructured?.structured) content = workspaceCanonicalAgentOutput(attemptResultContent);
|
|
3357
|
+
else if (assistantStructured?.structured) content = workspaceCanonicalAgentOutput(attemptLastAssistantContent);
|
|
3358
|
+
else content = workspaceCanonicalAgentOutput(attemptLastAssistantContent || attemptContent);
|
|
3294
3359
|
break;
|
|
3295
3360
|
} catch (e) {
|
|
3296
3361
|
if (signal?.aborted || e?.code === "WORKSPACE_RUN_ABORTED") throwIfAborted();
|
|
@@ -3302,7 +3367,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
3302
3367
|
throw e;
|
|
3303
3368
|
}
|
|
3304
3369
|
}
|
|
3305
|
-
const normalizedAgentOutput = workspaceStructuredAgentOutput(content);
|
|
3370
|
+
const normalizedAgentOutput = workspacePublishAgentOutputFiles(workspaceStructuredAgentOutput(content), runPackage);
|
|
3306
3371
|
const resultContent = normalizedAgentOutput.result || content;
|
|
3307
3372
|
outputs.set(nodeId, resultContent);
|
|
3308
3373
|
const slotUpdate = workspaceApplyAgentOutputSlots(instance, content);
|