@fieldwangai/agentflow 0.1.70 → 0.1.71

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.
@@ -92,6 +92,7 @@ import {
92
92
  } from "./marketplace.mjs";
93
93
  import { buildGitContext, inferGitRepoRootFromWorktree, loadGitWorktree, normalizeGitContext, runGit, sanitizeWorktreeName, unloadGitWorktree } from "./git-worktree.mjs";
94
94
  import { createGitLabMergeRequest } from "./gitlab-mr.mjs";
95
+ import { sendWecomAppMarkdown, sendWecomGroupMarkdown } from "./wecom.mjs";
95
96
  import {
96
97
  authSetupRequired,
97
98
  buildClearSessionCookie,
@@ -2106,6 +2107,57 @@ function hydrateWorkspaceNodeRefsFromFiles(scopedRoot, graph) {
2106
2107
  return changed ? { ...next, instances } : next;
2107
2108
  }
2108
2109
 
2110
+ function workspaceMergeSlotsWithDefinitionMeta(slots, definitionSlots) {
2111
+ const current = Array.isArray(slots) ? slots : [];
2112
+ const defs = Array.isArray(definitionSlots) ? definitionSlots : [];
2113
+ const byName = new Map(defs.map((slot) => [String(slot?.name || ""), slot]));
2114
+ return current.map((slot, index) => {
2115
+ const def = byName.get(String(slot?.name || "")) || defs[index] || null;
2116
+ if (!def) return slot;
2117
+ const next = { ...slot };
2118
+ for (const key of ["type", "name", "description", "required", "showOnNode"]) {
2119
+ if (next[key] == null || String(next[key]).trim?.() === "") {
2120
+ if (def[key] != null) next[key] = def[key];
2121
+ }
2122
+ }
2123
+ return next;
2124
+ });
2125
+ }
2126
+
2127
+ function hydrateWorkspaceSlotMetaFromDefinitions(workspaceRoot, scoped = {}, graph = {}, userCtx = {}) {
2128
+ const next = normalizeWorkspaceGraphPayload(graph || {});
2129
+ let definitions = [];
2130
+ try {
2131
+ definitions = listNodesJson(workspaceRoot, scoped.flowId || "", scoped.flowSource || "user", {
2132
+ archived: scoped.archived === true,
2133
+ userId: userCtx?.userId || "",
2134
+ }).nodes || [];
2135
+ } catch {
2136
+ definitions = [];
2137
+ }
2138
+ if (!definitions.length) return next;
2139
+ const defById = new Map(definitions.map((def) => [String(def?.id || ""), def]));
2140
+ const instances = { ...(next.instances || {}) };
2141
+ let changed = false;
2142
+ for (const [nodeId, instance] of Object.entries(instances)) {
2143
+ if (!instance || typeof instance !== "object") continue;
2144
+ const def = defById.get(String(instance.definitionId || ""));
2145
+ if (!def) continue;
2146
+ const input = workspaceMergeSlotsWithDefinitionMeta(instance.input, def.inputs);
2147
+ const output = workspaceMergeSlotsWithDefinitionMeta(instance.output, def.outputs);
2148
+ if (JSON.stringify(input) !== JSON.stringify(instance.input || []) || JSON.stringify(output) !== JSON.stringify(instance.output || [])) {
2149
+ instances[nodeId] = { ...instance, input, output };
2150
+ changed = true;
2151
+ }
2152
+ }
2153
+ return changed ? { ...next, instances } : next;
2154
+ }
2155
+
2156
+ function hydrateWorkspaceGraphForRuntime(workspaceRoot, scoped = {}, graph = {}, userCtx = {}) {
2157
+ const withRefs = hydrateWorkspaceNodeRefsFromFiles(scoped.root || scoped.scopedRoot || workspaceRoot, graph);
2158
+ return hydrateWorkspaceSlotMetaFromDefinitions(workspaceRoot, scoped, withRefs, userCtx);
2159
+ }
2160
+
2109
2161
  function resolveWorkspaceScopeRoot(workspaceRoot, params = {}, opts = {}) {
2110
2162
  const flowId = params.flowId != null ? String(params.flowId).trim() : "";
2111
2163
  if (!flowId) return { root: path.resolve(workspaceRoot), flowId: "", flowSource: "", archived: false };
@@ -2290,22 +2342,47 @@ function isWorkspaceSemanticOutputSlot(slot) {
2290
2342
  return type === "node" || name === "prev" || name === "next";
2291
2343
  }
2292
2344
 
2293
- function workspaceOutputSlotValueForEdge(graph, outputs, edge) {
2345
+ function workspaceLinkedOutputShouldStayPath(slot) {
2346
+ const rawName = String(slot?.name || "").trim();
2347
+ const name = rawName.toLowerCase();
2348
+ const type = String(slot?.type || "").trim().toLowerCase();
2349
+ if (["file", "image", "audio", "video", "binary", "directory", "dir"].includes(type)) return true;
2350
+ if (["file", "filepath", "file_path", "path", "url", "uri", "ref"].includes(name)) return true;
2351
+ return /(?:^|[_-])(file|path|url|uri|ref)$/i.test(rawName) || /(?:File|Path|Url|URL|Uri|URI|Ref)$/.test(rawName);
2352
+ }
2353
+
2354
+ function workspaceResolveLinkedOutputForTarget(value, targetSlot, scopedRoot = "") {
2355
+ const text = String(value ?? "");
2356
+ if (!text.trim() || workspaceLinkedOutputShouldStayPath(targetSlot)) return text;
2357
+ const outputRel = workspaceSafeNodeOutputRelPath(text);
2358
+ if (!outputRel || !String(scopedRoot || "").trim()) return text;
2359
+ try {
2360
+ const abs = workspaceResolveFlowFile(scopedRoot, outputRel, "linked output");
2361
+ const content = workspaceReadTextFileIfExists(abs, 120000);
2362
+ return content || text;
2363
+ } catch {
2364
+ return text;
2365
+ }
2366
+ }
2367
+
2368
+ function workspaceOutputSlotValueForEdge(graph, outputs, edge, scopedRoot = "") {
2294
2369
  const sourceId = String(edge?.source || "");
2295
2370
  const slot = workspaceSourceSlotForEdge(graph, edge);
2296
2371
  if (isWorkspaceSemanticOutputSlot(slot)) return "";
2372
+ const targetSlot = workspaceTargetSlotForEdge(graph, edge);
2373
+ const resolveValue = (value) => workspaceResolveLinkedOutputForTarget(value, targetSlot, scopedRoot);
2297
2374
  const out = outputs.get(sourceId);
2298
2375
  const sourceIndex = workspaceHandleIndex(edge?.sourceHandle, "output");
2299
2376
  const slotName = String(slot?.name || "").trim();
2300
2377
  const isPrimaryOutput = !slot || slotName === "result" || slotName === "content" || sourceIndex === 0;
2301
- if (isPrimaryOutput && out != null && String(out).trim()) return String(out);
2378
+ if (isPrimaryOutput && out != null && String(out).trim()) return resolveValue(out);
2302
2379
  if (slot && String(slot?.type || "") !== "node") {
2303
2380
  const value = workspaceSlotValue(slot);
2304
- if (value.trim()) return value;
2381
+ if (value.trim()) return resolveValue(value);
2305
2382
  }
2306
- if (out != null && String(out).trim()) return String(out);
2383
+ if (out != null && String(out).trim()) return resolveValue(out);
2307
2384
  const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
2308
- return workspaceInstanceText(instances[sourceId]);
2385
+ return resolveValue(workspaceInstanceText(instances[sourceId]));
2309
2386
  }
2310
2387
 
2311
2388
  function workspaceParseJsonObjectFromText(text) {
@@ -2931,10 +3008,47 @@ function workspaceRelevantInputValues(body, inputValues = {}) {
2931
3008
  return { values, placeholders };
2932
3009
  }
2933
3010
 
3011
+ function workspaceDownstreamSlotKind(slot) {
3012
+ const name = String(slot?.name || "").trim().toLowerCase();
3013
+ const type = String(slot?.type || "").trim().toLowerCase();
3014
+ if (type === "markdown" || name === "markdown" || name.endsWith("markdown")) return "markdown";
3015
+ if (type === "html" || name === "html" || name.endsWith("html")) return "html";
3016
+ if (type === "mermaid" || name === "mermaid" || name.endsWith("mermaid")) return "mermaid";
3017
+ if (type === "ascii" || name === "ascii") return "ascii";
3018
+ if (type === "chart" || name === "chart" || name.endsWith("chart")) return "chart";
3019
+ if (type === "table" || name === "table" || name.endsWith("table")) return "table";
3020
+ return "";
3021
+ }
3022
+
3023
+ function workspaceDownstreamOutputKindForField(graph, nodeId, field) {
3024
+ const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
3025
+ const edges = Array.isArray(graph?.edges) ? graph.edges : [];
3026
+ const source = instances[String(nodeId || "")] || {};
3027
+ const outputSlots = Array.isArray(source.output) ? source.output : [];
3028
+ for (const edge of edges) {
3029
+ if (String(edge?.source || "") !== String(nodeId)) continue;
3030
+ const sourceIndex = workspaceHandleIndex(edge?.sourceHandle, "output");
3031
+ const sourceSlot = outputSlots[sourceIndex] || null;
3032
+ if (workspaceOutputFieldForSlot(sourceSlot, sourceIndex) !== field) continue;
3033
+ const targetSlot = workspaceTargetSlotForEdge(graph, edge);
3034
+ if (!targetSlot || isWorkspaceSemanticInputSlot(targetSlot)) continue;
3035
+ const kind = workspaceDownstreamSlotKind(targetSlot);
3036
+ if (kind) return kind;
3037
+ }
3038
+ return "";
3039
+ }
3040
+
3041
+ function workspaceDownstreamInputDescription(target, slot) {
3042
+ const description = String(slot?.description || "").trim();
3043
+ if (description) return description;
3044
+ return "";
3045
+ }
3046
+
2934
3047
  function workspaceOutputProtocolRequirements(graph, nodeId) {
2935
3048
  const instance = graph?.instances?.[nodeId] || {};
2936
3049
  const outputSlots = Array.isArray(instance.output) ? instance.output : [];
2937
3050
  const displayBindings = workspaceDownstreamOutputDisplayBindings(graph, nodeId);
3051
+ const downstreamInputRequirements = workspaceDownstreamInputRequirements(graph, nodeId);
2938
3052
  const displayByField = new Map();
2939
3053
  for (const binding of displayBindings) {
2940
3054
  if (!displayByField.has(binding.field)) displayByField.set(binding.field, binding.kind);
@@ -2946,7 +3060,7 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
2946
3060
  return name && type !== "node" && name !== "next" && name !== "result" && name !== "content";
2947
3061
  })
2948
3062
  .map((slot) => String(slot.name).trim());
2949
- const resultKind = displayByField.get("result") || "";
3063
+ const resultKind = displayByField.get("result") || workspaceDownstreamOutputKindForField(graph, nodeId, "result") || "";
2950
3064
  const resultExtByKind = {
2951
3065
  html: "html",
2952
3066
  markdown: "md",
@@ -2982,6 +3096,7 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
2982
3096
  "## 输出",
2983
3097
  "",
2984
3098
  `请把${resultKindText}结果写入 \`${resultFile}\`。${resultGuidance}`,
3099
+ downstreamInputRequirements ? `\n${downstreamInputRequirements}` : "",
2985
3100
  ...(slots.length ? [`额外输出:${slots.map((name) => `\`${name}\``).join("、")}。短值可写在 \`outParams\`,文件值写成 \`outParams.<name>File\`。`] : []),
2986
3101
  "最终只输出下面的 agentflow envelope,不要输出解释、进度或其它文字:",
2987
3102
  "",
@@ -2989,6 +3104,42 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
2989
3104
  ].join("\n");
2990
3105
  }
2991
3106
 
3107
+ function workspaceDownstreamInputRequirements(graph, nodeId) {
3108
+ const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
3109
+ const edges = Array.isArray(graph?.edges) ? graph.edges : [];
3110
+ const source = instances[String(nodeId || "")] || {};
3111
+ const outputSlots = Array.isArray(source.output) ? source.output : [];
3112
+ const rows = [];
3113
+ const seen = new Set();
3114
+ for (const edge of edges) {
3115
+ if (String(edge?.source || "") !== String(nodeId)) continue;
3116
+ const target = instances[String(edge?.target || "")];
3117
+ if (!target) continue;
3118
+ const targetSlot = workspaceTargetSlotForEdge(graph, edge);
3119
+ if (!targetSlot || isWorkspaceSemanticInputSlot(targetSlot)) continue;
3120
+ const targetName = String(targetSlot.name || "").trim();
3121
+ const targetType = String(targetSlot.type || "text").trim();
3122
+ const description = workspaceDownstreamInputDescription(target, targetSlot);
3123
+ if (!targetName && !description) continue;
3124
+ const sourceIndex = workspaceHandleIndex(edge?.sourceHandle, "output");
3125
+ const sourceSlot = outputSlots[sourceIndex] || null;
3126
+ const sourceField = workspaceOutputFieldForSlot(sourceSlot, sourceIndex);
3127
+ const targetLabel = String(target.label || target.definitionId || edge.target || "").trim();
3128
+ const key = `${sourceField}->${edge.target}:${targetName}:${description}`;
3129
+ if (seen.has(key)) continue;
3130
+ seen.add(key);
3131
+ rows.push([
3132
+ `- 输出 \`${sourceField}\` 会连接到下游 \`${targetLabel}\` 的输入 \`${targetName || "input"}\`(type=${targetType})。`,
3133
+ description ? ` 要求:${description}` : "",
3134
+ ].filter(Boolean).join("\n"));
3135
+ }
3136
+ if (!rows.length) return "";
3137
+ return [
3138
+ "下游输入格式要求:",
3139
+ ...rows,
3140
+ ].join("\n");
3141
+ }
3142
+
2992
3143
  function workspaceRunPlan(graph, runNodeId, scopedRoot = "") {
2993
3144
  const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
2994
3145
  const edges = Array.isArray(graph?.edges) ? graph.edges : [];
@@ -3121,14 +3272,14 @@ function workspaceCachedOutputValueExists(value, scopedRoot = "") {
3121
3272
  return fs.existsSync(abs) && fs.statSync(abs).isFile();
3122
3273
  }
3123
3274
 
3124
- function workspaceUpstreamText(graph, nodeId, outputs) {
3275
+ function workspaceUpstreamText(graph, nodeId, outputs, scopedRoot = "") {
3125
3276
  const edges = Array.isArray(graph?.edges) ? graph.edges : [];
3126
3277
  const incoming = edges
3127
3278
  .filter((edge) => String(edge?.target || "") === String(nodeId))
3128
3279
  .filter((edge) => !isWorkspaceSemanticInputSlot(workspaceTargetSlotForEdge(graph, edge)));
3129
3280
  const contentEdge = incoming.find((edge) => String(edge?.targetHandle || "") === "input-1") || incoming[0];
3130
3281
  if (!contentEdge) return "";
3131
- return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge);
3282
+ return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge, scopedRoot);
3132
3283
  }
3133
3284
 
3134
3285
  function workspaceHandleIndex(handle, prefix) {
@@ -3181,7 +3332,7 @@ function workspaceNodeFileBoundaryBlock(runPackage = {}) {
3181
3332
  ].filter(Boolean).join("\n");
3182
3333
  }
3183
3334
 
3184
- function workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputNames = null) {
3335
+ function workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputNames = null, scopedRoot = "") {
3185
3336
  const edges = Array.isArray(graph?.edges) ? graph.edges : [];
3186
3337
  const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
3187
3338
  const incoming = edges.filter((edge) => String(edge?.target || "") === String(nodeId));
@@ -3194,10 +3345,10 @@ function workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputNames =
3194
3345
  }
3195
3346
  const contentEdge = contentEdges.find((edge) => String(edge?.targetHandle || "") === "input-1") || contentEdges[0];
3196
3347
  if (!contentEdge) return "";
3197
- return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge);
3348
+ return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge, scopedRoot);
3198
3349
  }
3199
3350
 
3200
- function workspaceInputValues(graph, nodeId, outputs) {
3351
+ function workspaceInputValues(graph, nodeId, outputs, scopedRoot = "") {
3201
3352
  const values = {};
3202
3353
  const edges = Array.isArray(graph?.edges) ? graph.edges : [];
3203
3354
  const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
@@ -3209,7 +3360,7 @@ function workspaceInputValues(graph, nodeId, outputs) {
3209
3360
  const slot = inputSlots[index] || null;
3210
3361
  const name = String(slot?.name || "").trim();
3211
3362
  if (!name || isWorkspaceSemanticInputSlot(slot)) continue;
3212
- const value = workspaceOutputSlotValueForEdge(graph, outputs, edge);
3363
+ const value = workspaceOutputSlotValueForEdge(graph, outputs, edge, scopedRoot);
3213
3364
  if (String(value || "").trim()) values[name] = String(value);
3214
3365
  }
3215
3366
  for (const slot of inputSlots) {
@@ -3706,7 +3857,7 @@ function workspaceUnwrapOutputEnvelopeForDisplay(content) {
3706
3857
  return structured.structured ? String(structured.result || "") : raw;
3707
3858
  }
3708
3859
 
3709
- function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null) {
3860
+ function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null, scopedRoot = "") {
3710
3861
  const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
3711
3862
  const edges = Array.isArray(graph?.edges) ? graph.edges : [];
3712
3863
  const updated = [];
@@ -3716,7 +3867,7 @@ function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null)
3716
3867
  const targetId = String(edge?.target || "");
3717
3868
  const target = instances[targetId];
3718
3869
  if (!target || !workspaceDisplayKind(target.definitionId)) continue;
3719
- const value = outputs ? workspaceOutputSlotValueForEdge(graph, outputs, edge) : String(content || "");
3870
+ const value = outputs ? workspaceOutputSlotValueForEdge(graph, outputs, edge, scopedRoot) : String(content || "");
3720
3871
  instances[targetId] = workspaceWriteDisplayContent(target, value || content);
3721
3872
  updated.push(targetId);
3722
3873
  }
@@ -4110,7 +4261,12 @@ async function workspaceRunToolNodejsScript({
4110
4261
  }
4111
4262
 
4112
4263
  async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts = {}) {
4113
- const graph = normalizeWorkspaceGraphPayload(payload.graph || {});
4264
+ const graph = hydrateWorkspaceGraphForRuntime(root, {
4265
+ root: scopedRoot,
4266
+ flowId: payload.flowId || "",
4267
+ flowSource: payload.flowSource || "user",
4268
+ archived: payload.archived === true || payload.flowArchived === true,
4269
+ }, payload.graph || {}, userCtx);
4114
4270
  const runNodeId = String(payload?.runNodeId || "").trim();
4115
4271
  const { order, pauseNodeIds } = workspaceRunPlan(graph, runNodeId, scopedRoot);
4116
4272
  const signal = opts.signal || null;
@@ -4199,7 +4355,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
4199
4355
  )),
4200
4356
  };
4201
4357
  outputs.set(nodeId, skillsBlock);
4202
- workspaceUpdateDirectDisplays(graph, nodeId, skillsBlock, outputs);
4358
+ workspaceUpdateDirectDisplays(graph, nodeId, skillsBlock, outputs, scopedRoot);
4203
4359
  emit({ type: "graph", nodeId, graph });
4204
4360
  emit({ type: "node-done", nodeId, definitionId: defId });
4205
4361
  continue;
@@ -4219,14 +4375,14 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
4219
4375
  )),
4220
4376
  };
4221
4377
  outputs.set(nodeId, mcpBlock);
4222
- workspaceUpdateDirectDisplays(graph, nodeId, mcpBlock, outputs);
4378
+ workspaceUpdateDirectDisplays(graph, nodeId, mcpBlock, outputs, scopedRoot);
4223
4379
  emit({ type: "graph", nodeId, graph });
4224
4380
  emit({ type: "node-done", nodeId, definitionId: defId });
4225
4381
  continue;
4226
4382
  }
4227
4383
 
4228
4384
  if (workspaceDisplayKind(defId)) {
4229
- const content = workspaceUpstreamText(graph, nodeId, outputs);
4385
+ const content = workspaceUpstreamText(graph, nodeId, outputs, scopedRoot);
4230
4386
  graph.instances[nodeId] = workspaceWriteDisplayContent(instance, content);
4231
4387
  outputs.set(nodeId, content);
4232
4388
  emit({ type: "graph", nodeId, graph });
@@ -4262,7 +4418,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
4262
4418
  }
4263
4419
 
4264
4420
  if (defId === "control_cd_workspace") {
4265
- const inputText = workspaceUpstreamText(graph, nodeId, outputs);
4421
+ const inputText = workspaceUpstreamText(graph, nodeId, outputs, scopedRoot);
4266
4422
  const inputSlots = Array.isArray(instance.input) ? instance.input : [];
4267
4423
  const pathSlot = inputSlots.find((slot) => String(slot?.name || "") === "path") ||
4268
4424
  inputSlots.find((slot) => String(slot?.name || "") === "target");
@@ -4473,9 +4629,36 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
4473
4629
  continue;
4474
4630
  }
4475
4631
 
4632
+ if (defId === "tool_wecom_send_group_markdown" || defId === "tool_wecom_send_app_markdown") {
4633
+ const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
4634
+ const markdown = String(inputValues.markdown || inputValues.content || workspaceSlotValue(workspaceSlotByName(instance, "markdown")) || workspaceUpstreamText(graph, nodeId, outputs, scopedRoot) || "");
4635
+ const result = defId === "tool_wecom_send_app_markdown"
4636
+ ? await sendWecomAppMarkdown({
4637
+ markdown,
4638
+ toUser: inputValues.toUser || workspaceSlotValue(workspaceSlotByName(instance, "toUser")),
4639
+ corpId: inputValues.corpId || workspaceSlotValue(workspaceSlotByName(instance, "corpId")),
4640
+ corpSecret: inputValues.corpSecret || workspaceSlotValue(workspaceSlotByName(instance, "corpSecret")),
4641
+ agentId: inputValues.agentId || workspaceSlotValue(workspaceSlotByName(instance, "agentId")),
4642
+ accessToken: inputValues.accessToken || workspaceSlotValue(workspaceSlotByName(instance, "accessToken")),
4643
+ }, runtimeEnvForUser(userCtx))
4644
+ : await sendWecomGroupMarkdown({
4645
+ markdown,
4646
+ webhookUrl: inputValues.webhookUrl || workspaceSlotValue(workspaceSlotByName(instance, "webhookUrl")),
4647
+ webhookKey: inputValues.webhookKey || workspaceSlotValue(workspaceSlotByName(instance, "webhookKey")),
4648
+ }, runtimeEnvForUser(userCtx));
4649
+ let nextInstance = workspaceSetOutputSlot(instance, "sent", "true");
4650
+ nextInstance = workspaceSetOutputSlot(nextInstance, "message", result.message);
4651
+ nextInstance = workspaceSetOutputSlot(nextInstance, "response", JSON.stringify(result.response || {}));
4652
+ graph.instances[nodeId] = nextInstance;
4653
+ outputs.set(nodeId, result.message);
4654
+ emit({ type: "graph", nodeId, graph });
4655
+ emit({ type: "node-done", nodeId, definitionId: defId });
4656
+ continue;
4657
+ }
4658
+
4476
4659
  if (defId === "tool_nodejs") {
4477
4660
  const prepareStartedAt = Date.now();
4478
- const inputValues = workspaceInputValues(graph, nodeId, outputs);
4661
+ const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
4479
4662
  const runPackage = workspaceCreateNodeRunPackage(runTmpRoot, nodeId, {
4480
4663
  scopedRoot,
4481
4664
  cwd,
@@ -4512,16 +4695,16 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
4512
4695
  onActiveChild: opts.onActiveChild,
4513
4696
  });
4514
4697
  if (implementationUpdate.changed) graph.instances[nodeId] = implementationUpdate.instance;
4515
- const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, resultContent, outputs);
4698
+ const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, resultContent, outputs, scopedRoot);
4516
4699
  if (slotUpdate.changed || implementationUpdate.changed || updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
4517
4700
  emit({ type: "node-done", nodeId, definitionId: defId });
4518
4701
  continue;
4519
4702
  }
4520
4703
 
4521
4704
  const prepareStartedAt = Date.now();
4522
- const inputValues = workspaceInputValues(graph, nodeId, outputs);
4705
+ const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
4523
4706
  const relevantInputs = workspaceRelevantInputValues(instance.body || "", inputValues);
4524
- const upstreamText = workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputs.placeholders);
4707
+ const upstreamText = workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputs.placeholders, scopedRoot);
4525
4708
  const upstreamSkillBlocks = workspaceUpstreamSkillBlocks(graph, nodeId, outputs);
4526
4709
  const promptSkillsBlock = mergeWorkspaceSkillBlocks(upstreamSkillBlocks);
4527
4710
  const promptMcpBlock = workspaceUpstreamMcpBlocks(graph, nodeId, outputs);
@@ -4642,7 +4825,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
4642
4825
  onActiveChild: opts.onActiveChild,
4643
4826
  });
4644
4827
  if (implementationUpdate.changed) graph.instances[nodeId] = implementationUpdate.instance;
4645
- const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, resultContent, outputs);
4828
+ const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, resultContent, outputs, scopedRoot);
4646
4829
  if (slotUpdate.changed || implementationUpdate.changed || updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
4647
4830
  emit({ type: "node-done", nodeId, definitionId: defId });
4648
4831
  }
@@ -4839,11 +5022,267 @@ function broadcastFlowEditorSync(flowId, flowSource, flowArchived = false, userI
4839
5022
  const activeFlowRuns = new Map();
4840
5023
  /** 正在执行的 Workspace 临时 run(flowId → { controller, child, runNodeId, startedAt });同一 flow 只允许一个 run */
4841
5024
  const activeWorkspaceRuns = new Map();
5025
+ const WORKSPACE_SCHEDULES_FILENAME = "workspace-schedules.json";
5026
+ const WORKSPACE_SCHEDULE_POLL_MS = 30_000;
4842
5027
 
4843
5028
  function workspaceRunKey(userCtx, flowSource, flowId) {
4844
5029
  return `${userCtx?.userId || ""}:${flowSource || "user"}:${flowId}`;
4845
5030
  }
4846
5031
 
5032
+ function normalizeWorkspaceScheduledRunConfig(raw) {
5033
+ let parsed = {};
5034
+ const text = String(raw || "").trim();
5035
+ if (text) {
5036
+ try {
5037
+ parsed = JSON.parse(text);
5038
+ } catch {
5039
+ parsed = {};
5040
+ }
5041
+ }
5042
+ const intervalMinutes = Number(parsed.intervalMinutes);
5043
+ return {
5044
+ enabled: parsed.enabled === true,
5045
+ intervalMinutes: Number.isFinite(intervalMinutes) && intervalMinutes > 0
5046
+ ? Math.min(Math.max(Math.round(intervalMinutes), 1), 1440)
5047
+ : 60,
5048
+ };
5049
+ }
5050
+
5051
+ function workspaceSchedulesPath() {
5052
+ return path.join(getAgentflowDataRoot(), WORKSPACE_SCHEDULES_FILENAME);
5053
+ }
5054
+
5055
+ function readWorkspaceScheduleRegistry() {
5056
+ const filePath = workspaceSchedulesPath();
5057
+ if (!fs.existsSync(filePath)) return { version: 1, schedules: {} };
5058
+ try {
5059
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
5060
+ return {
5061
+ version: 1,
5062
+ schedules: parsed?.schedules && typeof parsed.schedules === "object" && !Array.isArray(parsed.schedules)
5063
+ ? parsed.schedules
5064
+ : {},
5065
+ };
5066
+ } catch {
5067
+ return { version: 1, schedules: {} };
5068
+ }
5069
+ }
5070
+
5071
+ function writeWorkspaceScheduleRegistry(registry) {
5072
+ const filePath = workspaceSchedulesPath();
5073
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
5074
+ fs.writeFileSync(filePath, JSON.stringify({
5075
+ version: 1,
5076
+ updatedAt: new Date().toISOString(),
5077
+ schedules: registry?.schedules && typeof registry.schedules === "object" ? registry.schedules : {},
5078
+ }, null, 2) + "\n", "utf-8");
5079
+ }
5080
+
5081
+ function workspaceScheduleKey(userId, flowSource, flowId, runNodeId) {
5082
+ return [
5083
+ String(userId || ""),
5084
+ String(flowSource || "user"),
5085
+ String(flowId || ""),
5086
+ String(runNodeId || ""),
5087
+ ].join(":");
5088
+ }
5089
+
5090
+ function listWorkspaceScheduleStatusesForFlow(userCtx = {}, flowSource = "user", flowId = "") {
5091
+ const registry = readWorkspaceScheduleRegistry();
5092
+ const userId = String(userCtx.userId || "");
5093
+ return Object.values(registry.schedules || {})
5094
+ .filter((entry) => (
5095
+ String(entry?.userId || "") === userId &&
5096
+ String(entry?.flowSource || "user") === String(flowSource || "user") &&
5097
+ String(entry?.flowId || "") === String(flowId || "")
5098
+ ))
5099
+ .sort((a, b) => String(a.runNodeId || "").localeCompare(String(b.runNodeId || "")));
5100
+ }
5101
+
5102
+ function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx = {}) {
5103
+ const flowId = String(scoped?.flowId || "").trim();
5104
+ const flowSource = String(scoped?.flowSource || "user");
5105
+ const userId = String(userCtx.userId || authUser?.userId || "");
5106
+ if (!flowId || !userId) return [];
5107
+ const now = Date.now();
5108
+ const nowIso = new Date(now).toISOString();
5109
+ const registry = readWorkspaceScheduleRegistry();
5110
+ const schedules = { ...(registry.schedules || {}) };
5111
+ const prefix = `${userId}:${flowSource}:${flowId}:`;
5112
+ for (const key of Object.keys(schedules)) {
5113
+ if (key.startsWith(prefix)) delete schedules[key];
5114
+ }
5115
+ const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
5116
+ for (const [runNodeId, instance] of Object.entries(instances)) {
5117
+ if (String(instance?.definitionId || "") !== "workspace_scheduled_run") continue;
5118
+ const config = normalizeWorkspaceScheduledRunConfig(instance.body || "");
5119
+ if (!config.enabled) continue;
5120
+ const key = workspaceScheduleKey(userId, flowSource, flowId, runNodeId);
5121
+ const intervalMs = config.intervalMinutes * 60 * 1000;
5122
+ const previous = registry.schedules?.[key] && typeof registry.schedules[key] === "object" ? registry.schedules[key] : {};
5123
+ const previousNext = Number(previous.nextRunAt || 0);
5124
+ schedules[key] = {
5125
+ ...previous,
5126
+ key,
5127
+ enabled: true,
5128
+ userId,
5129
+ username: String(authUser?.username || previous.username || userId),
5130
+ flowId,
5131
+ flowSource,
5132
+ runNodeId,
5133
+ label: String(instance.label || "Scheduled Run"),
5134
+ intervalMinutes: config.intervalMinutes,
5135
+ nextRunAt: Number.isFinite(previousNext) && previousNext > now ? previousNext : now + intervalMs,
5136
+ lastStatus: previous.lastStatus || "armed",
5137
+ updatedAt: nowIso,
5138
+ };
5139
+ }
5140
+ const nextRegistry = { version: 1, schedules };
5141
+ writeWorkspaceScheduleRegistry(nextRegistry);
5142
+ return listWorkspaceScheduleStatusesForFlow(userCtx, flowSource, flowId);
5143
+ }
5144
+
5145
+ function updateWorkspaceScheduleEntry(key, patch) {
5146
+ const registry = readWorkspaceScheduleRegistry();
5147
+ const current = registry.schedules?.[key];
5148
+ if (!current) return null;
5149
+ const next = {
5150
+ ...current,
5151
+ ...(patch && typeof patch === "object" ? patch : {}),
5152
+ updatedAt: new Date().toISOString(),
5153
+ };
5154
+ registry.schedules[key] = next;
5155
+ writeWorkspaceScheduleRegistry(registry);
5156
+ return next;
5157
+ }
5158
+
5159
+ async function runWorkspaceScheduledEntry(root, entry) {
5160
+ const userCtx = { userId: String(entry.userId || "") };
5161
+ const runKey = workspaceRunKey(userCtx, entry.flowSource || "user", entry.flowId || "");
5162
+ const intervalMs = Math.max(1, Number(entry.intervalMinutes || 60)) * 60 * 1000;
5163
+ const nextRunAt = Date.now() + intervalMs;
5164
+ if (activeWorkspaceRuns.has(runKey)) {
5165
+ updateWorkspaceScheduleEntry(entry.key, {
5166
+ nextRunAt,
5167
+ lastSkippedAt: Date.now(),
5168
+ lastStatus: "skipped: busy",
5169
+ });
5170
+ return;
5171
+ }
5172
+ const scoped = resolveWorkspaceScopeRoot(root, {
5173
+ flowId: entry.flowId || "",
5174
+ flowSource: entry.flowSource || "user",
5175
+ }, userCtx);
5176
+ if (scoped.error || scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
5177
+ updateWorkspaceScheduleEntry(entry.key, {
5178
+ nextRunAt,
5179
+ lastStatus: "error",
5180
+ lastError: scoped.error || "Workspace schedule target is not writable",
5181
+ lastErrorAt: Date.now(),
5182
+ });
5183
+ return;
5184
+ }
5185
+ const graphPath = workspaceGraphPath(scoped.root);
5186
+ const graph = hydrateWorkspaceGraphForRuntime(root, scoped, readWorkspaceGraph(scoped.root).graph, userCtx);
5187
+ const instance = graph.instances?.[entry.runNodeId];
5188
+ const config = normalizeWorkspaceScheduledRunConfig(instance?.body || "");
5189
+ if (!instance || String(instance.definitionId || "") !== "workspace_scheduled_run" || !config.enabled) {
5190
+ updateWorkspaceScheduleEntry(entry.key, {
5191
+ enabled: false,
5192
+ nextRunAt: null,
5193
+ lastStatus: "disabled",
5194
+ });
5195
+ return;
5196
+ }
5197
+
5198
+ const controller = new AbortController();
5199
+ const authUsers = readAuthUsers();
5200
+ const authUser = authUsers[userCtx.userId] || {};
5201
+ const runEntry = {
5202
+ controller,
5203
+ child: null,
5204
+ runId: runLedgerId("workspace"),
5205
+ userId: userCtx.userId,
5206
+ username: String(authUser.username || entry.username || userCtx.userId),
5207
+ runNodeId: String(entry.runNodeId || ""),
5208
+ flowId: String(entry.flowId || ""),
5209
+ flowSource: String(entry.flowSource || "user"),
5210
+ startedAt: Date.now(),
5211
+ scheduled: true,
5212
+ stopChild() {
5213
+ if (this.child && !this.child.killed) {
5214
+ try { this.child.kill("SIGTERM"); } catch (_) {}
5215
+ }
5216
+ },
5217
+ };
5218
+ activeWorkspaceRuns.set(runKey, runEntry);
5219
+ appendWorkspaceRunStarted(runEntry);
5220
+ updateWorkspaceScheduleEntry(entry.key, {
5221
+ lastStatus: "running",
5222
+ lastTriggeredAt: runEntry.startedAt,
5223
+ lastRunId: runEntry.runId,
5224
+ lastError: "",
5225
+ });
5226
+ const setActiveChild = (child) => {
5227
+ runEntry.child = child || null;
5228
+ if (controller.signal.aborted) runEntry.stopChild();
5229
+ };
5230
+ try {
5231
+ const result = await runWorkspaceGraph(root, scoped.root, {
5232
+ flowId: entry.flowId,
5233
+ flowSource: entry.flowSource || "user",
5234
+ runNodeId: entry.runNodeId,
5235
+ graph,
5236
+ }, userCtx, {
5237
+ signal: controller.signal,
5238
+ onActiveChild: setActiveChild,
5239
+ });
5240
+ const currentGraph = readWorkspaceGraph(scoped.root).graph;
5241
+ const touchedIds = workspaceRunTouchedNodeIds(result);
5242
+ const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
5243
+ fs.writeFileSync(graphPath, JSON.stringify(mergedGraph, null, 2) + "\n", "utf-8");
5244
+ const endedAt = Date.now();
5245
+ appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "success");
5246
+ updateWorkspaceScheduleEntry(entry.key, {
5247
+ nextRunAt: Date.now() + intervalMs,
5248
+ lastFinishedAt: endedAt,
5249
+ lastStatus: "success",
5250
+ lastError: "",
5251
+ });
5252
+ } catch (e) {
5253
+ const endedAt = Date.now();
5254
+ appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "failed");
5255
+ updateWorkspaceScheduleEntry(entry.key, {
5256
+ nextRunAt: Date.now() + intervalMs,
5257
+ lastFinishedAt: endedAt,
5258
+ lastStatus: "failed",
5259
+ lastError: (e && e.message) || String(e),
5260
+ lastErrorAt: endedAt,
5261
+ });
5262
+ log.info(`[workspace-scheduler] failed ${entry.flowId}/${entry.runNodeId}: ${(e && e.message) || String(e)}`);
5263
+ } finally {
5264
+ if (activeWorkspaceRuns.get(runKey) === runEntry) activeWorkspaceRuns.delete(runKey);
5265
+ }
5266
+ }
5267
+
5268
+ function pollWorkspaceSchedules(root) {
5269
+ const now = Date.now();
5270
+ const registry = readWorkspaceScheduleRegistry();
5271
+ for (const entry of Object.values(registry.schedules || {})) {
5272
+ if (!entry || entry.enabled !== true) continue;
5273
+ const nextRunAt = Number(entry.nextRunAt || 0);
5274
+ if (!Number.isFinite(nextRunAt) || nextRunAt <= 0) {
5275
+ updateWorkspaceScheduleEntry(entry.key, {
5276
+ nextRunAt: now + Math.max(1, Number(entry.intervalMinutes || 60)) * 60 * 1000,
5277
+ lastStatus: entry.lastStatus || "armed",
5278
+ });
5279
+ continue;
5280
+ }
5281
+ if (nextRunAt > now) continue;
5282
+ void runWorkspaceScheduledEntry(root, entry);
5283
+ }
5284
+ }
5285
+
4847
5286
  /** Cursor/OpenCode 执行目录统一使用当前 UI 启动 workspace。 */
4848
5287
  function composerCliWorkspaceForFlowDir(workspaceRoot, _flowDir) {
4849
5288
  return path.resolve(workspaceRoot);
@@ -5498,7 +5937,7 @@ export function startUiServer({
5498
5937
  return;
5499
5938
  }
5500
5939
  const { path: graphPath, graph } = readWorkspaceGraph(scoped.root);
5501
- const hydratedGraph = hydrateWorkspaceNodeRefsFromFiles(scoped.root, graph);
5940
+ const hydratedGraph = hydrateWorkspaceGraphForRuntime(root, scoped, graph, userCtx);
5502
5941
  json(res, 200, {
5503
5942
  ok: true,
5504
5943
  graph: hydratedGraph,
@@ -5508,6 +5947,7 @@ export function startUiServer({
5508
5947
  flowSource: scoped.flowSource,
5509
5948
  archived: scoped.archived,
5510
5949
  writable: !(scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)),
5950
+ workspaceSchedules: listWorkspaceScheduleStatusesForFlow(userCtx, scoped.flowSource || "user", scoped.flowId || ""),
5511
5951
  });
5512
5952
  } catch (e) {
5513
5953
  json(res, 500, { error: (e && e.message) || String(e) });
@@ -5588,12 +6028,30 @@ export function startUiServer({
5588
6028
  json(res, 400, { error: "Cannot write workspace graph for builtin or archived pipeline" });
5589
6029
  return;
5590
6030
  }
5591
- const submittedGraph = normalizeWorkspaceGraphPayload(payload.graph || payload);
6031
+ const submittedGraph = hydrateWorkspaceGraphForRuntime(root, scoped, payload.graph || payload, userCtx);
5592
6032
  const graphPath = workspaceGraphPath(scoped.root);
5593
- const currentGraph = hydrateWorkspaceNodeRefsFromFiles(scoped.root, readWorkspaceGraph(scoped.root).graph);
6033
+ const currentGraph = hydrateWorkspaceGraphForRuntime(root, scoped, readWorkspaceGraph(scoped.root).graph, userCtx);
5594
6034
  const graph = mergeWorkspacePersistentNodeRefs(submittedGraph, currentGraph);
5595
6035
  fs.writeFileSync(graphPath, JSON.stringify(graph, null, 2) + "\n", "utf-8");
5596
- json(res, 200, { ok: true, path: graphPath, graph });
6036
+ const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx);
6037
+ json(res, 200, { ok: true, path: graphPath, graph, workspaceSchedules });
6038
+ } catch (e) {
6039
+ json(res, 500, { error: (e && e.message) || String(e) });
6040
+ }
6041
+ return;
6042
+ }
6043
+
6044
+ if (req.method === "GET" && url.pathname === "/api/workspace/schedules") {
6045
+ try {
6046
+ const flowId = url.searchParams.get("flowId") || "";
6047
+ const flowSource = url.searchParams.get("flowSource") || "user";
6048
+ if (!flowId) {
6049
+ json(res, 400, { error: "Missing flowId" });
6050
+ return;
6051
+ }
6052
+ json(res, 200, {
6053
+ schedules: listWorkspaceScheduleStatusesForFlow(userCtx, flowSource, flowId),
6054
+ });
5597
6055
  } catch (e) {
5598
6056
  json(res, 500, { error: (e && e.message) || String(e) });
5599
6057
  }
@@ -8154,6 +8612,25 @@ finishedAt: "${new Date().toISOString()}"
8154
8612
  res.end(data);
8155
8613
  });
8156
8614
 
8615
+ const workspaceScheduleTimer = setInterval(() => {
8616
+ try {
8617
+ pollWorkspaceSchedules(root);
8618
+ } catch (e) {
8619
+ log.debug(`[workspace-scheduler] poll failed: ${(e && e.message) || String(e)}`);
8620
+ }
8621
+ }, WORKSPACE_SCHEDULE_POLL_MS);
8622
+ try {
8623
+ workspaceScheduleTimer.unref?.();
8624
+ } catch (_) {}
8625
+ server.on("close", () => clearInterval(workspaceScheduleTimer));
8626
+ setTimeout(() => {
8627
+ try {
8628
+ pollWorkspaceSchedules(root);
8629
+ } catch (e) {
8630
+ log.debug(`[workspace-scheduler] initial poll failed: ${(e && e.message) || String(e)}`);
8631
+ }
8632
+ }, 1000).unref?.();
8633
+
8157
8634
  return new Promise((resolve, reject) => {
8158
8635
  server.once("error", reject);
8159
8636
  server.listen(port, host, () => {