@fieldwangai/agentflow 0.1.59 → 0.1.60

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.
@@ -257,6 +257,7 @@ export function buildScriptContentBlockForInstances(flowYamlAbs, instanceIds) {
257
257
  * @param {string} [opts.cliWorkspace]
258
258
  * @param {string} opts.prompt
259
259
  * @param {string} [opts.modelKey]
260
+ * @param {Record<string, string>} [opts.extraEnv]
260
261
  * @param {boolean} [opts.force]
261
262
  * @param {(ev: object) => void} [opts.onStreamEvent]
262
263
  * @param {(subtype: string, toolName: string) => void} [opts.onToolCall]
@@ -273,7 +274,8 @@ export function startComposerAgent(opts) {
273
274
  const cliWs = opts.cliWorkspace ? String(opts.cliWorkspace) : getAgentflowDataRoot();
274
275
  const modelKey = opts.modelKey != null ? String(opts.modelKey).trim() : "";
275
276
  const { cli, model } = resolveCliAndModel(uiRoot, modelKey || null, null);
276
- const env = agentflowUserEnv(opts.agentflowUserId);
277
+ const extraEnv = opts.extraEnv && typeof opts.extraEnv === "object" && !Array.isArray(opts.extraEnv) ? opts.extraEnv : {};
278
+ const env = { ...agentflowUserEnv(opts.agentflowUserId), ...extraEnv };
277
279
 
278
280
  const common = {
279
281
  onStreamEvent: opts.onStreamEvent,
@@ -2072,6 +2072,7 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
2072
2072
  "## Workspace 输出协议",
2073
2073
  "",
2074
2074
  "最终回复必须是一个 JSON 对象,必须直接以 `{` 开头并以 `}` 结尾;不要使用 Markdown 代码围栏,不要在 JSON 外追加解释文字、进度说明或自然语言前后缀。",
2075
+ "执行过程中不要发送 assistant 进度说明,例如“正在读取/正在生成/准备输出”;需要思考时使用内部 thinking,最终只发送一个 JSON 对象。",
2075
2076
  "JSON 必须可被 `JSON.parse` 解析;如果 `result` 是多行 Markdown,必须在 JSON 字符串里使用 `\\n` 转义换行,不能把裸 Markdown 直接塞进未转义的字符串。",
2076
2077
  "固定格式:",
2077
2078
  "",
@@ -2168,6 +2169,74 @@ function isWorkspaceSemanticInputSlot(slot) {
2168
2169
  return type === "node" || name === "prev" || name === "next" || name === "skillsContext" || name === "mcpContext" || name === "workspaceContext" || name === "gitContext";
2169
2170
  }
2170
2171
 
2172
+ function workspaceNodeBrief(graph, nodeId) {
2173
+ const instance = graph?.instances?.[String(nodeId || "")] || {};
2174
+ const label = String(instance?.label || nodeId || "").trim();
2175
+ const defId = String(instance?.definitionId || "").trim();
2176
+ return defId && defId !== label ? `${label || nodeId} (${defId})` : (label || nodeId);
2177
+ }
2178
+
2179
+ function workspaceSlotBrief(slot, fallback) {
2180
+ const name = String(slot?.name || "").trim();
2181
+ const type = String(slot?.type || "").trim();
2182
+ return `${name || fallback}${type ? `:${type}` : ""}`;
2183
+ }
2184
+
2185
+ function workspaceNodeConnectionContextBlock(graph, nodeId) {
2186
+ const edges = Array.isArray(graph?.edges) ? graph.edges : [];
2187
+ const incoming = edges.filter((edge) => String(edge?.target || "") === String(nodeId));
2188
+ const outgoing = edges.filter((edge) => String(edge?.source || "") === String(nodeId));
2189
+ const inputLines = incoming.map((edge) => {
2190
+ const sourceSlot = workspaceSourceSlotForEdge(graph, edge);
2191
+ const targetSlot = workspaceTargetSlotForEdge(graph, edge);
2192
+ const targetName = String(targetSlot?.name || "").trim();
2193
+ const semantic = isWorkspaceSemanticInputSlot(targetSlot) ? "上下文输入" : "业务输入";
2194
+ return `- ${workspaceNodeBrief(graph, edge.source)} \`${workspaceSlotBrief(sourceSlot, edge.sourceHandle || "output")}\` -> 当前 \`${workspaceSlotBrief(targetSlot, edge.targetHandle || "input")}\`(${semantic}${targetName ? `:${targetName}` : ""})`;
2195
+ });
2196
+ const outputLines = outgoing.map((edge) => {
2197
+ const sourceSlot = workspaceSourceSlotForEdge(graph, edge);
2198
+ const targetSlot = workspaceTargetSlotForEdge(graph, edge);
2199
+ const targetKind = workspaceDisplayKind(graph?.instances?.[String(edge?.target || "")]?.definitionId);
2200
+ return `- 当前 \`${workspaceSlotBrief(sourceSlot, edge.sourceHandle || "output")}\` -> ${workspaceNodeBrief(graph, edge.target)} \`${workspaceSlotBrief(targetSlot, edge.targetHandle || "input")}\`${targetKind ? `(${targetKind} 展示)` : ""}`;
2201
+ });
2202
+ if (!inputLines.length && !outputLines.length) return "";
2203
+ return [
2204
+ "## 当前节点连线",
2205
+ "",
2206
+ inputLines.length ? "### 直接上游" : "",
2207
+ ...inputLines,
2208
+ outputLines.length ? "\n### 直接下游" : "",
2209
+ ...outputLines,
2210
+ "",
2211
+ "只把直接上游的业务输入和上下文输入当作本节点依据;直接下游只用于确定输出字段和格式。",
2212
+ ].filter(Boolean).join("\n");
2213
+ }
2214
+
2215
+ function workspaceResolvedInputValuesBlock(inputValues = {}) {
2216
+ const entries = Object.entries(inputValues || {}).filter(([name, value]) => String(name || "").trim() && String(value || "").trim());
2217
+ if (!entries.length) return "";
2218
+ const lines = entries.map(([name, value]) => {
2219
+ const text = String(value || "");
2220
+ const clipped = text.length > 6000 ? `${text.slice(0, 6000)}\n...[已截断 ${text.length - 6000} 字]` : text;
2221
+ return `### ${name}\n\n${clipped}`;
2222
+ });
2223
+ return ["## 已解析业务输入槽", "", ...lines].join("\n");
2224
+ }
2225
+
2226
+ function workspaceNodeTmpDirectoryBlock(nodeTmpDir) {
2227
+ const dir = String(nodeTmpDir || "").trim();
2228
+ if (!dir) return "";
2229
+ return [
2230
+ "## 临时文件目录",
2231
+ "",
2232
+ `- 本节点专用临时目录:\`${dir}\``,
2233
+ "- 如果确实需要创建中间文件,只能写入该目录,路径也可通过环境变量 `AGENTFLOW_NODE_TMP_DIR` 获取。",
2234
+ "- 不要在 workspace 根目录、业务仓库根目录或当前 cwd 下创建 `temp_*`、`_out.json`、`tmp.html` 等临时产物。",
2235
+ "- 不要自行删除该目录或其中的最终待读文件;AgentFlow 会在节点运行结束后统一清理。",
2236
+ "- 纯生成类任务应直接在最终 JSON 中返回结果;不要为了输出而写文件、cat 文件再粘贴。",
2237
+ ].join("\n");
2238
+ }
2239
+
2171
2240
  function workspaceTaskUpstreamText(graph, nodeId, outputs) {
2172
2241
  const edges = Array.isArray(graph?.edges) ? graph.edges : [];
2173
2242
  const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
@@ -2247,8 +2316,7 @@ function workspaceUpstreamSkillBlocks(graph, nodeId, outputs) {
2247
2316
  const slot = workspaceTargetSlotForEdge(graph, edge);
2248
2317
  return String(slot?.name || "") === "skillsContext";
2249
2318
  })
2250
- .map((edge) => String(outputs.get(String(edge.source || "")) || ""))
2251
- .filter((text) => text.includes("Skill") || text.includes("skill"))
2319
+ .map((edge) => workspaceOutputSlotValueForEdge(graph, outputs, edge))
2252
2320
  .flatMap((text) => text.split(/\n\s*---\s*\n/g))
2253
2321
  .map((text) => text.trim())
2254
2322
  .filter(Boolean);
@@ -2263,8 +2331,7 @@ function workspaceUpstreamMcpBlocks(graph, nodeId, outputs) {
2263
2331
  const slot = workspaceTargetSlotForEdge(graph, edge);
2264
2332
  return String(slot?.name || "") === "mcpContext";
2265
2333
  })
2266
- .map((edge) => String(outputs.get(String(edge.source || "")) || ""))
2267
- .filter((text) => text.includes("MCP") || text.includes("mcp"))
2334
+ .map((edge) => workspaceOutputSlotValueForEdge(graph, outputs, edge))
2268
2335
  .flatMap((text) => text.split(/\n\s*---\s*\n/g))
2269
2336
  .map((text) => text.trim())
2270
2337
  .filter(Boolean);
@@ -2291,9 +2358,9 @@ function buildWorkspaceSkillManifestBlock(skills, selectedKeys = []) {
2291
2358
  }).filter(Boolean);
2292
2359
  if (!rows.length && !normalizedKeys.length) return "";
2293
2360
  return [
2294
- "### Workspace Skills Manifest",
2361
+ "### 已加载 Skills",
2295
2362
  "",
2296
- "这些 skills 已在当前 workspace 中可用。不要默认展开或复述其内容;仅当节点任务明确需要时,按路径 Read 对应 SKILL.md。",
2363
+ "这些 skills 来自当前 Workspace 中已连接的 Load Skills 节点。只有节点任务需要对应能力时,才按路径 Read 对应 SKILL.md;不要展开未连接或未加载的 skills。",
2297
2364
  "",
2298
2365
  ...(
2299
2366
  rows.length
@@ -2375,20 +2442,26 @@ function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null)
2375
2442
  return updated;
2376
2443
  }
2377
2444
 
2378
- function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock = "", inputValues = {}) {
2445
+ function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock = "", inputValues = {}, nodeTmpDir = "") {
2379
2446
  const instance = graph.instances[nodeId] || {};
2380
2447
  const body = workspaceResolveBodyPlaceholders(instance.body || "", inputValues).trim();
2381
2448
  const label = String(instance.label || nodeId).trim();
2382
2449
  const scopeGuardrails = workspaceNodeScopeGuardrails(graph, nodeId, inputValues);
2450
+ const connectionContext = workspaceNodeConnectionContextBlock(graph, nodeId);
2451
+ const resolvedInputs = workspaceResolvedInputValuesBlock(inputValues);
2452
+ const tmpDirectory = workspaceNodeTmpDirectoryBlock(nodeTmpDir);
2383
2453
  const downstreamRequirements = workspaceDownstreamDisplayRequirements(graph, nodeId);
2384
2454
  const outputProtocolRequirements = workspaceOutputProtocolRequirements(graph, nodeId);
2385
2455
  return [
2386
2456
  "你正在执行 AgentFlow Workspace 画布中的一个临时节点。",
2387
2457
  "按 Workspace 输出协议返回该节点要传给下游展示/后续节点的数据。",
2388
2458
  scopeGuardrails,
2459
+ connectionContext ? `\n${connectionContext}` : "",
2460
+ tmpDirectory ? `\n${tmpDirectory}` : "",
2389
2461
  workspaceSearchGuardrailsBlock(),
2390
- skillsBlock ? `\n## Available Skills\n\n${skillsBlock}` : "",
2391
- mcpBlock ? `\n## Available MCP\n\n${mcpBlock}` : "",
2462
+ resolvedInputs ? `\n${resolvedInputs}` : "",
2463
+ skillsBlock ? `\n## 上游已加载 Skills\n\n${skillsBlock}` : "",
2464
+ mcpBlock ? `\n## 上游已加载 MCP\n\n${mcpBlock}` : "",
2392
2465
  upstreamText ? `\n## 上游上下文\n\n${upstreamText}` : "",
2393
2466
  downstreamRequirements ? `\n${downstreamRequirements}` : "",
2394
2467
  outputProtocolRequirements ? `\n${outputProtocolRequirements}` : "",
@@ -2463,6 +2536,48 @@ function workspaceCleanupAutoWorktrees(list, graph, emit) {
2463
2536
  list.splice(0, list.length);
2464
2537
  }
2465
2538
 
2539
+ function workspaceSanitizeTmpSegment(value, fallback = "node") {
2540
+ return String(value || fallback)
2541
+ .trim()
2542
+ .replace(/[^a-zA-Z0-9._-]+/g, "_")
2543
+ .replace(/^_+|_+$/g, "")
2544
+ .slice(0, 120) || fallback;
2545
+ }
2546
+
2547
+ function workspaceCreateRunTmpRoot(scopedRoot, runNodeId) {
2548
+ const runPart = workspaceSanitizeTmpSegment(runNodeId || "run", "run");
2549
+ const id = typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(16).slice(2)}`;
2550
+ const dir = path.join(path.resolve(scopedRoot), ".workspace", "agentflow", "tmp", `workspace-run-${Date.now()}-${runPart}-${id}`);
2551
+ fs.mkdirSync(dir, { recursive: true });
2552
+ return dir;
2553
+ }
2554
+
2555
+ function workspaceCreateNodeTmpDir(runTmpRoot, nodeId) {
2556
+ const dir = path.join(path.resolve(runTmpRoot), workspaceSanitizeTmpSegment(nodeId, "node"));
2557
+ fs.mkdirSync(dir, { recursive: true });
2558
+ return dir;
2559
+ }
2560
+
2561
+ function workspaceShouldKeepTmp(userCtx = {}) {
2562
+ const env = { ...process.env, ...readMergedEnvObject(userCtx.userId) };
2563
+ const value = String(env.AGENTFLOW_KEEP_TMP || env.AGENTFLOW_KEEP_WORKSPACE_TMP || "").trim().toLowerCase();
2564
+ return ["1", "true", "yes", "on"].includes(value);
2565
+ }
2566
+
2567
+ function workspaceCleanupTmpRoot(runTmpRoot, userCtx = {}, emit = () => {}) {
2568
+ const dir = String(runTmpRoot || "").trim();
2569
+ if (!dir) return;
2570
+ if (workspaceShouldKeepTmp(userCtx)) {
2571
+ emit({ type: "status", line: `Workspace tmp kept: ${dir}` });
2572
+ return;
2573
+ }
2574
+ try {
2575
+ fs.rmSync(dir, { recursive: true, force: true });
2576
+ } catch (e) {
2577
+ emit({ type: "natural", kind: "warning", text: `Workspace tmp cleanup failed: ${dir}\n原因:${e?.message || String(e)}` });
2578
+ }
2579
+ }
2580
+
2466
2581
  async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts = {}) {
2467
2582
  const graph = normalizeWorkspaceGraphPayload(payload.graph || {});
2468
2583
  const runNodeId = String(payload?.runNodeId || "").trim();
@@ -2475,9 +2590,6 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
2475
2590
  throw err;
2476
2591
  }
2477
2592
  };
2478
- const fallbackSelectedSkillKeys = Array.isArray(payload?.selectedSkills)
2479
- ? payload.selectedSkills.map((x) => String(x || "").trim()).filter(Boolean)
2480
- : [];
2481
2593
  const skillsBlockCache = new Map();
2482
2594
  const loadSkillsBlockForKeys = (keys) => {
2483
2595
  const normalized = Array.from(new Set((keys || []).map((x) => String(x || "").trim()).filter(Boolean)));
@@ -2521,6 +2633,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
2521
2633
  let cwd = scopedRoot;
2522
2634
  const modelKey = typeof payload?.model === "string" ? payload.model.trim() : "";
2523
2635
  const autoCleanupWorktrees = [];
2636
+ const runTmpRoot = workspaceCreateRunTmpRoot(scopedRoot, runNodeId);
2524
2637
 
2525
2638
  try {
2526
2639
  for (const nodeId of order) {
@@ -2537,9 +2650,8 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
2537
2650
  if (defId === "control_load_skills") {
2538
2651
  const skillStartedAt = Date.now();
2539
2652
  const nodeSkillKeys = selectedSkillKeysFromInstance(instance);
2540
- const activeSkillKeys = nodeSkillKeys.length > 0 ? nodeSkillKeys : fallbackSelectedSkillKeys;
2541
- const skillsBlock = loadSkillsBlockForKeys(activeSkillKeys);
2542
- emitTiming(nodeId, "load-skills", skillStartedAt, { skillCount: activeSkillKeys.length, charCount: skillsBlock.length });
2653
+ const skillsBlock = loadSkillsBlockForKeys(nodeSkillKeys);
2654
+ emitTiming(nodeId, "load-skills", skillStartedAt, { skillCount: nodeSkillKeys.length, charCount: skillsBlock.length });
2543
2655
  graph.instances[nodeId] = {
2544
2656
  ...instance,
2545
2657
  output: (Array.isArray(instance.output) ? instance.output : []).map((slot) => (
@@ -2829,9 +2941,10 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
2829
2941
  throw new Error(`Workspace node ${nodeId} has no task. Fill the node body or connect upstream text.`);
2830
2942
  }
2831
2943
  const upstreamSkillBlocks = workspaceUpstreamSkillBlocks(graph, nodeId, outputs);
2832
- const promptSkillsBlock = mergeWorkspaceSkillBlocks(upstreamSkillBlocks, upstreamSkillBlocks ? "" : loadSkillsBlockForKeys(fallbackSelectedSkillKeys));
2944
+ const promptSkillsBlock = mergeWorkspaceSkillBlocks(upstreamSkillBlocks);
2833
2945
  const promptMcpBlock = workspaceUpstreamMcpBlocks(graph, nodeId, outputs);
2834
- const prompt = workspaceNodePrompt(graph, nodeId, upstreamText, promptSkillsBlock, promptMcpBlock, inputValues);
2946
+ const nodeTmpDir = workspaceCreateNodeTmpDir(runTmpRoot, nodeId);
2947
+ const prompt = workspaceNodePrompt(graph, nodeId, upstreamText, promptSkillsBlock, promptMcpBlock, inputValues, nodeTmpDir);
2835
2948
  emitTiming(nodeId, "prepare-agent-prompt", prepareStartedAt, { promptChars: prompt.length, upstreamChars: String(upstreamText || "").length, skillsChars: promptSkillsBlock.length, mcpChars: promptMcpBlock.length });
2836
2949
  emit({ type: "natural", kind: "prompt", nodeId, text: prompt });
2837
2950
  let content = "";
@@ -2847,6 +2960,10 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
2847
2960
  prompt,
2848
2961
  modelKey,
2849
2962
  agentflowUserId: userCtx.userId || "",
2963
+ extraEnv: {
2964
+ AGENTFLOW_WORKSPACE_TMP_ROOT: runTmpRoot,
2965
+ AGENTFLOW_NODE_TMP_DIR: nodeTmpDir,
2966
+ },
2850
2967
  onStreamEvent: (ev) => {
2851
2968
  if (!firstAgentEventSeen) {
2852
2969
  firstAgentEventSeen = true;
@@ -2894,6 +3011,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
2894
3011
  }
2895
3012
  } finally {
2896
3013
  workspaceCleanupAutoWorktrees(autoCleanupWorktrees, graph, emit);
3014
+ workspaceCleanupTmpRoot(runTmpRoot, userCtx, emit);
2897
3015
  }
2898
3016
  if (pauseNodeIds.length > 0) {
2899
3017
  emit({ type: "paused", nodeIds: pauseNodeIds, message: `Workspace run paused at ${pauseNodeIds.join(", ")}` });
@@ -3082,7 +3200,7 @@ function broadcastFlowEditorSync(flowId, flowSource, flowArchived = false, userI
3082
3200
 
3083
3201
  /** 正在执行的 flow run(flowId → { child, runUuid });同一 flow 只允许一个 run */
3084
3202
  const activeFlowRuns = new Map();
3085
- /** 正在执行的 Workspace 临时 run(flowId → { controller, child });同一 flow 只允许一个 run */
3203
+ /** 正在执行的 Workspace 临时 run(flowId → { controller, child, runNodeId, startedAt });同一 flow 只允许一个 run */
3086
3204
  const activeWorkspaceRuns = new Map();
3087
3205
 
3088
3206
  function workspaceRunKey(userCtx, flowSource, flowId) {
@@ -3831,6 +3949,10 @@ export function startUiServer({
3831
3949
  const runEntry = {
3832
3950
  controller,
3833
3951
  child: null,
3952
+ runNodeId: String(payload.runNodeId || "").trim(),
3953
+ flowId,
3954
+ flowSource: scoped.flowSource || payload.flowSource || "user",
3955
+ startedAt: Date.now(),
3834
3956
  stopChild() {
3835
3957
  if (this.child && !this.child.killed) {
3836
3958
  try { this.child.kill("SIGTERM"); } catch (_) {}
@@ -3905,6 +4027,25 @@ export function startUiServer({
3905
4027
  return;
3906
4028
  }
3907
4029
 
4030
+ if (req.method === "GET" && url.pathname === "/api/workspace/run/status") {
4031
+ const flowId = typeof url.searchParams.get("flowId") === "string" ? url.searchParams.get("flowId").trim() : "";
4032
+ if (!flowId) {
4033
+ json(res, 400, { error: "Missing flowId" });
4034
+ return;
4035
+ }
4036
+ const flowSource = url.searchParams.get("flowSource") || "user";
4037
+ const runKey = workspaceRunKey(userCtx, flowSource, flowId);
4038
+ const entry = activeWorkspaceRuns.get(runKey);
4039
+ json(res, 200, {
4040
+ running: Boolean(entry),
4041
+ flowId,
4042
+ flowSource,
4043
+ runNodeId: entry?.runNodeId || "",
4044
+ startedAt: entry?.startedAt || null,
4045
+ });
4046
+ return;
4047
+ }
4048
+
3908
4049
  if (req.method === "POST" && url.pathname === "/api/workspace/run/stop") {
3909
4050
  let payload;
3910
4051
  try {