@fieldwangai/agentflow 0.1.70 → 0.1.72

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.
@@ -45,6 +45,7 @@ import {
45
45
  PACKAGE_ROOT,
46
46
  ARCHIVED_PIPELINES_DIR_NAME,
47
47
  getAgentflowDataRoot,
48
+ getAgentflowSkillsRoot,
48
49
  getAgentflowUserConfigAbs,
49
50
  getAgentflowUserDataRoot,
50
51
  getUserPipelinesRoot,
@@ -62,6 +63,7 @@ import {
62
63
  buildSkillInjectionBlock,
63
64
  buildSkillCompactInjectionBlock,
64
65
  } from "./composer-skill-router.mjs";
66
+ import { clearSkillRegistryCache } from "./skill-registry.mjs";
65
67
  import { COMPOSER_NODE_SPEC_FILENAME } from "./composer-planner.mjs";
66
68
  import { listRecentRunsFromDisk } from "./recent-runs.mjs";
67
69
  import {
@@ -92,6 +94,7 @@ import {
92
94
  } from "./marketplace.mjs";
93
95
  import { buildGitContext, inferGitRepoRootFromWorktree, loadGitWorktree, normalizeGitContext, runGit, sanitizeWorktreeName, unloadGitWorktree } from "./git-worktree.mjs";
94
96
  import { createGitLabMergeRequest } from "./gitlab-mr.mjs";
97
+ import { sendWecomAppMarkdown, sendWecomGroupMarkdown } from "./wecom.mjs";
95
98
  import {
96
99
  authSetupRequired,
97
100
  buildClearSessionCookie,
@@ -1133,6 +1136,22 @@ function normalizeSkillhubListPayload(raw) {
1133
1136
  }).filter((x) => x.name);
1134
1137
  }
1135
1138
 
1139
+ function skillhubManagedSkillsDir() {
1140
+ const dir = getAgentflowSkillsRoot();
1141
+ fs.mkdirSync(dir, { recursive: true });
1142
+ return dir;
1143
+ }
1144
+
1145
+ function skillhubListArgs(target, agent) {
1146
+ const normalizedTarget = String(target || "agentflow").trim();
1147
+ const normalizedAgent = String(agent || "codex").trim();
1148
+ const args = ["list", "--json"];
1149
+ if (normalizedTarget === "all") args.push("--all");
1150
+ else if (normalizedTarget === "legacy-global") args.push("--global", "--agent", normalizedAgent);
1151
+ else args.push("--dir", skillhubManagedSkillsDir());
1152
+ return args;
1153
+ }
1154
+
1136
1155
  function skillhubInstallArgs(payload, { uninstall = false } = {}) {
1137
1156
  const slug = String(payload?.slug || payload?.name || "").trim();
1138
1157
  if (!slug && !payload?.collection) return null;
@@ -1143,10 +1162,12 @@ function skillhubInstallArgs(payload, { uninstall = false } = {}) {
1143
1162
  args.push(slug);
1144
1163
  }
1145
1164
  if (payload?.skillId) args.push("--skill-id", String(payload.skillId).trim());
1146
- const target = String(payload?.target || "project").trim();
1165
+ const target = String(payload?.target || "agentflow").trim();
1147
1166
  const agent = String(payload?.agent || "codex").trim();
1148
- if (target === "global") {
1167
+ if (target === "global" || target === "legacy-global") {
1149
1168
  args.push("--global", "--agent", agent);
1169
+ } else if (target === "agentflow") {
1170
+ args.push("--dir", skillhubManagedSkillsDir());
1150
1171
  } else if (payload?.dir) {
1151
1172
  args.push("--dir", String(payload.dir).trim());
1152
1173
  }
@@ -2106,6 +2127,57 @@ function hydrateWorkspaceNodeRefsFromFiles(scopedRoot, graph) {
2106
2127
  return changed ? { ...next, instances } : next;
2107
2128
  }
2108
2129
 
2130
+ function workspaceMergeSlotsWithDefinitionMeta(slots, definitionSlots) {
2131
+ const current = Array.isArray(slots) ? slots : [];
2132
+ const defs = Array.isArray(definitionSlots) ? definitionSlots : [];
2133
+ const byName = new Map(defs.map((slot) => [String(slot?.name || ""), slot]));
2134
+ return current.map((slot, index) => {
2135
+ const def = byName.get(String(slot?.name || "")) || defs[index] || null;
2136
+ if (!def) return slot;
2137
+ const next = { ...slot };
2138
+ for (const key of ["type", "name", "description", "required", "showOnNode"]) {
2139
+ if (next[key] == null || String(next[key]).trim?.() === "") {
2140
+ if (def[key] != null) next[key] = def[key];
2141
+ }
2142
+ }
2143
+ return next;
2144
+ });
2145
+ }
2146
+
2147
+ function hydrateWorkspaceSlotMetaFromDefinitions(workspaceRoot, scoped = {}, graph = {}, userCtx = {}) {
2148
+ const next = normalizeWorkspaceGraphPayload(graph || {});
2149
+ let definitions = [];
2150
+ try {
2151
+ definitions = listNodesJson(workspaceRoot, scoped.flowId || "", scoped.flowSource || "user", {
2152
+ archived: scoped.archived === true,
2153
+ userId: userCtx?.userId || "",
2154
+ }).nodes || [];
2155
+ } catch {
2156
+ definitions = [];
2157
+ }
2158
+ if (!definitions.length) return next;
2159
+ const defById = new Map(definitions.map((def) => [String(def?.id || ""), def]));
2160
+ const instances = { ...(next.instances || {}) };
2161
+ let changed = false;
2162
+ for (const [nodeId, instance] of Object.entries(instances)) {
2163
+ if (!instance || typeof instance !== "object") continue;
2164
+ const def = defById.get(String(instance.definitionId || ""));
2165
+ if (!def) continue;
2166
+ const input = workspaceMergeSlotsWithDefinitionMeta(instance.input, def.inputs);
2167
+ const output = workspaceMergeSlotsWithDefinitionMeta(instance.output, def.outputs);
2168
+ if (JSON.stringify(input) !== JSON.stringify(instance.input || []) || JSON.stringify(output) !== JSON.stringify(instance.output || [])) {
2169
+ instances[nodeId] = { ...instance, input, output };
2170
+ changed = true;
2171
+ }
2172
+ }
2173
+ return changed ? { ...next, instances } : next;
2174
+ }
2175
+
2176
+ function hydrateWorkspaceGraphForRuntime(workspaceRoot, scoped = {}, graph = {}, userCtx = {}) {
2177
+ const withRefs = hydrateWorkspaceNodeRefsFromFiles(scoped.root || scoped.scopedRoot || workspaceRoot, graph);
2178
+ return hydrateWorkspaceSlotMetaFromDefinitions(workspaceRoot, scoped, withRefs, userCtx);
2179
+ }
2180
+
2109
2181
  function resolveWorkspaceScopeRoot(workspaceRoot, params = {}, opts = {}) {
2110
2182
  const flowId = params.flowId != null ? String(params.flowId).trim() : "";
2111
2183
  if (!flowId) return { root: path.resolve(workspaceRoot), flowId: "", flowSource: "", archived: false };
@@ -2290,22 +2362,47 @@ function isWorkspaceSemanticOutputSlot(slot) {
2290
2362
  return type === "node" || name === "prev" || name === "next";
2291
2363
  }
2292
2364
 
2293
- function workspaceOutputSlotValueForEdge(graph, outputs, edge) {
2365
+ function workspaceLinkedOutputShouldStayPath(slot) {
2366
+ const rawName = String(slot?.name || "").trim();
2367
+ const name = rawName.toLowerCase();
2368
+ const type = String(slot?.type || "").trim().toLowerCase();
2369
+ if (["file", "image", "audio", "video", "binary", "directory", "dir"].includes(type)) return true;
2370
+ if (["file", "filepath", "file_path", "path", "url", "uri", "ref"].includes(name)) return true;
2371
+ return /(?:^|[_-])(file|path|url|uri|ref)$/i.test(rawName) || /(?:File|Path|Url|URL|Uri|URI|Ref)$/.test(rawName);
2372
+ }
2373
+
2374
+ function workspaceResolveLinkedOutputForTarget(value, targetSlot, scopedRoot = "") {
2375
+ const text = String(value ?? "");
2376
+ if (!text.trim() || workspaceLinkedOutputShouldStayPath(targetSlot)) return text;
2377
+ const outputRel = workspaceSafeNodeOutputRelPath(text);
2378
+ if (!outputRel || !String(scopedRoot || "").trim()) return text;
2379
+ try {
2380
+ const abs = workspaceResolveFlowFile(scopedRoot, outputRel, "linked output");
2381
+ const content = workspaceReadTextFileIfExists(abs, 120000);
2382
+ return content || text;
2383
+ } catch {
2384
+ return text;
2385
+ }
2386
+ }
2387
+
2388
+ function workspaceOutputSlotValueForEdge(graph, outputs, edge, scopedRoot = "") {
2294
2389
  const sourceId = String(edge?.source || "");
2295
2390
  const slot = workspaceSourceSlotForEdge(graph, edge);
2296
2391
  if (isWorkspaceSemanticOutputSlot(slot)) return "";
2392
+ const targetSlot = workspaceTargetSlotForEdge(graph, edge);
2393
+ const resolveValue = (value) => workspaceResolveLinkedOutputForTarget(value, targetSlot, scopedRoot);
2297
2394
  const out = outputs.get(sourceId);
2298
2395
  const sourceIndex = workspaceHandleIndex(edge?.sourceHandle, "output");
2299
2396
  const slotName = String(slot?.name || "").trim();
2300
2397
  const isPrimaryOutput = !slot || slotName === "result" || slotName === "content" || sourceIndex === 0;
2301
- if (isPrimaryOutput && out != null && String(out).trim()) return String(out);
2398
+ if (isPrimaryOutput && out != null && String(out).trim()) return resolveValue(out);
2302
2399
  if (slot && String(slot?.type || "") !== "node") {
2303
2400
  const value = workspaceSlotValue(slot);
2304
- if (value.trim()) return value;
2401
+ if (value.trim()) return resolveValue(value);
2305
2402
  }
2306
- if (out != null && String(out).trim()) return String(out);
2403
+ if (out != null && String(out).trim()) return resolveValue(out);
2307
2404
  const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
2308
- return workspaceInstanceText(instances[sourceId]);
2405
+ return resolveValue(workspaceInstanceText(instances[sourceId]));
2309
2406
  }
2310
2407
 
2311
2408
  function workspaceParseJsonObjectFromText(text) {
@@ -2931,10 +3028,47 @@ function workspaceRelevantInputValues(body, inputValues = {}) {
2931
3028
  return { values, placeholders };
2932
3029
  }
2933
3030
 
3031
+ function workspaceDownstreamSlotKind(slot) {
3032
+ const name = String(slot?.name || "").trim().toLowerCase();
3033
+ const type = String(slot?.type || "").trim().toLowerCase();
3034
+ if (type === "markdown" || name === "markdown" || name.endsWith("markdown")) return "markdown";
3035
+ if (type === "html" || name === "html" || name.endsWith("html")) return "html";
3036
+ if (type === "mermaid" || name === "mermaid" || name.endsWith("mermaid")) return "mermaid";
3037
+ if (type === "ascii" || name === "ascii") return "ascii";
3038
+ if (type === "chart" || name === "chart" || name.endsWith("chart")) return "chart";
3039
+ if (type === "table" || name === "table" || name.endsWith("table")) return "table";
3040
+ return "";
3041
+ }
3042
+
3043
+ function workspaceDownstreamOutputKindForField(graph, nodeId, field) {
3044
+ const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
3045
+ const edges = Array.isArray(graph?.edges) ? graph.edges : [];
3046
+ const source = instances[String(nodeId || "")] || {};
3047
+ const outputSlots = Array.isArray(source.output) ? source.output : [];
3048
+ for (const edge of edges) {
3049
+ if (String(edge?.source || "") !== String(nodeId)) continue;
3050
+ const sourceIndex = workspaceHandleIndex(edge?.sourceHandle, "output");
3051
+ const sourceSlot = outputSlots[sourceIndex] || null;
3052
+ if (workspaceOutputFieldForSlot(sourceSlot, sourceIndex) !== field) continue;
3053
+ const targetSlot = workspaceTargetSlotForEdge(graph, edge);
3054
+ if (!targetSlot || isWorkspaceSemanticInputSlot(targetSlot)) continue;
3055
+ const kind = workspaceDownstreamSlotKind(targetSlot);
3056
+ if (kind) return kind;
3057
+ }
3058
+ return "";
3059
+ }
3060
+
3061
+ function workspaceDownstreamInputDescription(target, slot) {
3062
+ const description = String(slot?.description || "").trim();
3063
+ if (description) return description;
3064
+ return "";
3065
+ }
3066
+
2934
3067
  function workspaceOutputProtocolRequirements(graph, nodeId) {
2935
3068
  const instance = graph?.instances?.[nodeId] || {};
2936
3069
  const outputSlots = Array.isArray(instance.output) ? instance.output : [];
2937
3070
  const displayBindings = workspaceDownstreamOutputDisplayBindings(graph, nodeId);
3071
+ const downstreamInputRequirements = workspaceDownstreamInputRequirements(graph, nodeId);
2938
3072
  const displayByField = new Map();
2939
3073
  for (const binding of displayBindings) {
2940
3074
  if (!displayByField.has(binding.field)) displayByField.set(binding.field, binding.kind);
@@ -2946,7 +3080,7 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
2946
3080
  return name && type !== "node" && name !== "next" && name !== "result" && name !== "content";
2947
3081
  })
2948
3082
  .map((slot) => String(slot.name).trim());
2949
- const resultKind = displayByField.get("result") || "";
3083
+ const resultKind = displayByField.get("result") || workspaceDownstreamOutputKindForField(graph, nodeId, "result") || "";
2950
3084
  const resultExtByKind = {
2951
3085
  html: "html",
2952
3086
  markdown: "md",
@@ -2982,6 +3116,7 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
2982
3116
  "## 输出",
2983
3117
  "",
2984
3118
  `请把${resultKindText}结果写入 \`${resultFile}\`。${resultGuidance}`,
3119
+ downstreamInputRequirements ? `\n${downstreamInputRequirements}` : "",
2985
3120
  ...(slots.length ? [`额外输出:${slots.map((name) => `\`${name}\``).join("、")}。短值可写在 \`outParams\`,文件值写成 \`outParams.<name>File\`。`] : []),
2986
3121
  "最终只输出下面的 agentflow envelope,不要输出解释、进度或其它文字:",
2987
3122
  "",
@@ -2989,6 +3124,42 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
2989
3124
  ].join("\n");
2990
3125
  }
2991
3126
 
3127
+ function workspaceDownstreamInputRequirements(graph, nodeId) {
3128
+ const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
3129
+ const edges = Array.isArray(graph?.edges) ? graph.edges : [];
3130
+ const source = instances[String(nodeId || "")] || {};
3131
+ const outputSlots = Array.isArray(source.output) ? source.output : [];
3132
+ const rows = [];
3133
+ const seen = new Set();
3134
+ for (const edge of edges) {
3135
+ if (String(edge?.source || "") !== String(nodeId)) continue;
3136
+ const target = instances[String(edge?.target || "")];
3137
+ if (!target) continue;
3138
+ const targetSlot = workspaceTargetSlotForEdge(graph, edge);
3139
+ if (!targetSlot || isWorkspaceSemanticInputSlot(targetSlot)) continue;
3140
+ const targetName = String(targetSlot.name || "").trim();
3141
+ const targetType = String(targetSlot.type || "text").trim();
3142
+ const description = workspaceDownstreamInputDescription(target, targetSlot);
3143
+ if (!targetName && !description) continue;
3144
+ const sourceIndex = workspaceHandleIndex(edge?.sourceHandle, "output");
3145
+ const sourceSlot = outputSlots[sourceIndex] || null;
3146
+ const sourceField = workspaceOutputFieldForSlot(sourceSlot, sourceIndex);
3147
+ const targetLabel = String(target.label || target.definitionId || edge.target || "").trim();
3148
+ const key = `${sourceField}->${edge.target}:${targetName}:${description}`;
3149
+ if (seen.has(key)) continue;
3150
+ seen.add(key);
3151
+ rows.push([
3152
+ `- 输出 \`${sourceField}\` 会连接到下游 \`${targetLabel}\` 的输入 \`${targetName || "input"}\`(type=${targetType})。`,
3153
+ description ? ` 要求:${description}` : "",
3154
+ ].filter(Boolean).join("\n"));
3155
+ }
3156
+ if (!rows.length) return "";
3157
+ return [
3158
+ "下游输入格式要求:",
3159
+ ...rows,
3160
+ ].join("\n");
3161
+ }
3162
+
2992
3163
  function workspaceRunPlan(graph, runNodeId, scopedRoot = "") {
2993
3164
  const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
2994
3165
  const edges = Array.isArray(graph?.edges) ? graph.edges : [];
@@ -3121,14 +3292,14 @@ function workspaceCachedOutputValueExists(value, scopedRoot = "") {
3121
3292
  return fs.existsSync(abs) && fs.statSync(abs).isFile();
3122
3293
  }
3123
3294
 
3124
- function workspaceUpstreamText(graph, nodeId, outputs) {
3295
+ function workspaceUpstreamText(graph, nodeId, outputs, scopedRoot = "") {
3125
3296
  const edges = Array.isArray(graph?.edges) ? graph.edges : [];
3126
3297
  const incoming = edges
3127
3298
  .filter((edge) => String(edge?.target || "") === String(nodeId))
3128
3299
  .filter((edge) => !isWorkspaceSemanticInputSlot(workspaceTargetSlotForEdge(graph, edge)));
3129
3300
  const contentEdge = incoming.find((edge) => String(edge?.targetHandle || "") === "input-1") || incoming[0];
3130
3301
  if (!contentEdge) return "";
3131
- return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge);
3302
+ return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge, scopedRoot);
3132
3303
  }
3133
3304
 
3134
3305
  function workspaceHandleIndex(handle, prefix) {
@@ -3181,7 +3352,7 @@ function workspaceNodeFileBoundaryBlock(runPackage = {}) {
3181
3352
  ].filter(Boolean).join("\n");
3182
3353
  }
3183
3354
 
3184
- function workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputNames = null) {
3355
+ function workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputNames = null, scopedRoot = "") {
3185
3356
  const edges = Array.isArray(graph?.edges) ? graph.edges : [];
3186
3357
  const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
3187
3358
  const incoming = edges.filter((edge) => String(edge?.target || "") === String(nodeId));
@@ -3194,10 +3365,10 @@ function workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputNames =
3194
3365
  }
3195
3366
  const contentEdge = contentEdges.find((edge) => String(edge?.targetHandle || "") === "input-1") || contentEdges[0];
3196
3367
  if (!contentEdge) return "";
3197
- return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge);
3368
+ return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge, scopedRoot);
3198
3369
  }
3199
3370
 
3200
- function workspaceInputValues(graph, nodeId, outputs) {
3371
+ function workspaceInputValues(graph, nodeId, outputs, scopedRoot = "") {
3201
3372
  const values = {};
3202
3373
  const edges = Array.isArray(graph?.edges) ? graph.edges : [];
3203
3374
  const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
@@ -3209,7 +3380,7 @@ function workspaceInputValues(graph, nodeId, outputs) {
3209
3380
  const slot = inputSlots[index] || null;
3210
3381
  const name = String(slot?.name || "").trim();
3211
3382
  if (!name || isWorkspaceSemanticInputSlot(slot)) continue;
3212
- const value = workspaceOutputSlotValueForEdge(graph, outputs, edge);
3383
+ const value = workspaceOutputSlotValueForEdge(graph, outputs, edge, scopedRoot);
3213
3384
  if (String(value || "").trim()) values[name] = String(value);
3214
3385
  }
3215
3386
  for (const slot of inputSlots) {
@@ -3706,7 +3877,7 @@ function workspaceUnwrapOutputEnvelopeForDisplay(content) {
3706
3877
  return structured.structured ? String(structured.result || "") : raw;
3707
3878
  }
3708
3879
 
3709
- function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null) {
3880
+ function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null, scopedRoot = "") {
3710
3881
  const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
3711
3882
  const edges = Array.isArray(graph?.edges) ? graph.edges : [];
3712
3883
  const updated = [];
@@ -3716,7 +3887,7 @@ function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null)
3716
3887
  const targetId = String(edge?.target || "");
3717
3888
  const target = instances[targetId];
3718
3889
  if (!target || !workspaceDisplayKind(target.definitionId)) continue;
3719
- const value = outputs ? workspaceOutputSlotValueForEdge(graph, outputs, edge) : String(content || "");
3890
+ const value = outputs ? workspaceOutputSlotValueForEdge(graph, outputs, edge, scopedRoot) : String(content || "");
3720
3891
  instances[targetId] = workspaceWriteDisplayContent(target, value || content);
3721
3892
  updated.push(targetId);
3722
3893
  }
@@ -4110,7 +4281,12 @@ async function workspaceRunToolNodejsScript({
4110
4281
  }
4111
4282
 
4112
4283
  async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts = {}) {
4113
- const graph = normalizeWorkspaceGraphPayload(payload.graph || {});
4284
+ const graph = hydrateWorkspaceGraphForRuntime(root, {
4285
+ root: scopedRoot,
4286
+ flowId: payload.flowId || "",
4287
+ flowSource: payload.flowSource || "user",
4288
+ archived: payload.archived === true || payload.flowArchived === true,
4289
+ }, payload.graph || {}, userCtx);
4114
4290
  const runNodeId = String(payload?.runNodeId || "").trim();
4115
4291
  const { order, pauseNodeIds } = workspaceRunPlan(graph, runNodeId, scopedRoot);
4116
4292
  const signal = opts.signal || null;
@@ -4199,7 +4375,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
4199
4375
  )),
4200
4376
  };
4201
4377
  outputs.set(nodeId, skillsBlock);
4202
- workspaceUpdateDirectDisplays(graph, nodeId, skillsBlock, outputs);
4378
+ workspaceUpdateDirectDisplays(graph, nodeId, skillsBlock, outputs, scopedRoot);
4203
4379
  emit({ type: "graph", nodeId, graph });
4204
4380
  emit({ type: "node-done", nodeId, definitionId: defId });
4205
4381
  continue;
@@ -4219,14 +4395,14 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
4219
4395
  )),
4220
4396
  };
4221
4397
  outputs.set(nodeId, mcpBlock);
4222
- workspaceUpdateDirectDisplays(graph, nodeId, mcpBlock, outputs);
4398
+ workspaceUpdateDirectDisplays(graph, nodeId, mcpBlock, outputs, scopedRoot);
4223
4399
  emit({ type: "graph", nodeId, graph });
4224
4400
  emit({ type: "node-done", nodeId, definitionId: defId });
4225
4401
  continue;
4226
4402
  }
4227
4403
 
4228
4404
  if (workspaceDisplayKind(defId)) {
4229
- const content = workspaceUpstreamText(graph, nodeId, outputs);
4405
+ const content = workspaceUpstreamText(graph, nodeId, outputs, scopedRoot);
4230
4406
  graph.instances[nodeId] = workspaceWriteDisplayContent(instance, content);
4231
4407
  outputs.set(nodeId, content);
4232
4408
  emit({ type: "graph", nodeId, graph });
@@ -4262,7 +4438,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
4262
4438
  }
4263
4439
 
4264
4440
  if (defId === "control_cd_workspace") {
4265
- const inputText = workspaceUpstreamText(graph, nodeId, outputs);
4441
+ const inputText = workspaceUpstreamText(graph, nodeId, outputs, scopedRoot);
4266
4442
  const inputSlots = Array.isArray(instance.input) ? instance.input : [];
4267
4443
  const pathSlot = inputSlots.find((slot) => String(slot?.name || "") === "path") ||
4268
4444
  inputSlots.find((slot) => String(slot?.name || "") === "target");
@@ -4473,9 +4649,36 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
4473
4649
  continue;
4474
4650
  }
4475
4651
 
4652
+ if (defId === "tool_wecom_send_group_markdown" || defId === "tool_wecom_send_app_markdown") {
4653
+ const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
4654
+ const markdown = String(inputValues.markdown || inputValues.content || workspaceSlotValue(workspaceSlotByName(instance, "markdown")) || workspaceUpstreamText(graph, nodeId, outputs, scopedRoot) || "");
4655
+ const result = defId === "tool_wecom_send_app_markdown"
4656
+ ? await sendWecomAppMarkdown({
4657
+ markdown,
4658
+ toUser: inputValues.toUser || workspaceSlotValue(workspaceSlotByName(instance, "toUser")),
4659
+ corpId: inputValues.corpId || workspaceSlotValue(workspaceSlotByName(instance, "corpId")),
4660
+ corpSecret: inputValues.corpSecret || workspaceSlotValue(workspaceSlotByName(instance, "corpSecret")),
4661
+ agentId: inputValues.agentId || workspaceSlotValue(workspaceSlotByName(instance, "agentId")),
4662
+ accessToken: inputValues.accessToken || workspaceSlotValue(workspaceSlotByName(instance, "accessToken")),
4663
+ }, runtimeEnvForUser(userCtx))
4664
+ : await sendWecomGroupMarkdown({
4665
+ markdown,
4666
+ webhookUrl: inputValues.webhookUrl || workspaceSlotValue(workspaceSlotByName(instance, "webhookUrl")),
4667
+ webhookKey: inputValues.webhookKey || workspaceSlotValue(workspaceSlotByName(instance, "webhookKey")),
4668
+ }, runtimeEnvForUser(userCtx));
4669
+ let nextInstance = workspaceSetOutputSlot(instance, "sent", "true");
4670
+ nextInstance = workspaceSetOutputSlot(nextInstance, "message", result.message);
4671
+ nextInstance = workspaceSetOutputSlot(nextInstance, "response", JSON.stringify(result.response || {}));
4672
+ graph.instances[nodeId] = nextInstance;
4673
+ outputs.set(nodeId, result.message);
4674
+ emit({ type: "graph", nodeId, graph });
4675
+ emit({ type: "node-done", nodeId, definitionId: defId });
4676
+ continue;
4677
+ }
4678
+
4476
4679
  if (defId === "tool_nodejs") {
4477
4680
  const prepareStartedAt = Date.now();
4478
- const inputValues = workspaceInputValues(graph, nodeId, outputs);
4681
+ const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
4479
4682
  const runPackage = workspaceCreateNodeRunPackage(runTmpRoot, nodeId, {
4480
4683
  scopedRoot,
4481
4684
  cwd,
@@ -4512,16 +4715,16 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
4512
4715
  onActiveChild: opts.onActiveChild,
4513
4716
  });
4514
4717
  if (implementationUpdate.changed) graph.instances[nodeId] = implementationUpdate.instance;
4515
- const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, resultContent, outputs);
4718
+ const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, resultContent, outputs, scopedRoot);
4516
4719
  if (slotUpdate.changed || implementationUpdate.changed || updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
4517
4720
  emit({ type: "node-done", nodeId, definitionId: defId });
4518
4721
  continue;
4519
4722
  }
4520
4723
 
4521
4724
  const prepareStartedAt = Date.now();
4522
- const inputValues = workspaceInputValues(graph, nodeId, outputs);
4725
+ const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
4523
4726
  const relevantInputs = workspaceRelevantInputValues(instance.body || "", inputValues);
4524
- const upstreamText = workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputs.placeholders);
4727
+ const upstreamText = workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputs.placeholders, scopedRoot);
4525
4728
  const upstreamSkillBlocks = workspaceUpstreamSkillBlocks(graph, nodeId, outputs);
4526
4729
  const promptSkillsBlock = mergeWorkspaceSkillBlocks(upstreamSkillBlocks);
4527
4730
  const promptMcpBlock = workspaceUpstreamMcpBlocks(graph, nodeId, outputs);
@@ -4642,7 +4845,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
4642
4845
  onActiveChild: opts.onActiveChild,
4643
4846
  });
4644
4847
  if (implementationUpdate.changed) graph.instances[nodeId] = implementationUpdate.instance;
4645
- const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, resultContent, outputs);
4848
+ const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, resultContent, outputs, scopedRoot);
4646
4849
  if (slotUpdate.changed || implementationUpdate.changed || updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
4647
4850
  emit({ type: "node-done", nodeId, definitionId: defId });
4648
4851
  }
@@ -4839,11 +5042,267 @@ function broadcastFlowEditorSync(flowId, flowSource, flowArchived = false, userI
4839
5042
  const activeFlowRuns = new Map();
4840
5043
  /** 正在执行的 Workspace 临时 run(flowId → { controller, child, runNodeId, startedAt });同一 flow 只允许一个 run */
4841
5044
  const activeWorkspaceRuns = new Map();
5045
+ const WORKSPACE_SCHEDULES_FILENAME = "workspace-schedules.json";
5046
+ const WORKSPACE_SCHEDULE_POLL_MS = 30_000;
4842
5047
 
4843
5048
  function workspaceRunKey(userCtx, flowSource, flowId) {
4844
5049
  return `${userCtx?.userId || ""}:${flowSource || "user"}:${flowId}`;
4845
5050
  }
4846
5051
 
5052
+ function normalizeWorkspaceScheduledRunConfig(raw) {
5053
+ let parsed = {};
5054
+ const text = String(raw || "").trim();
5055
+ if (text) {
5056
+ try {
5057
+ parsed = JSON.parse(text);
5058
+ } catch {
5059
+ parsed = {};
5060
+ }
5061
+ }
5062
+ const intervalMinutes = Number(parsed.intervalMinutes);
5063
+ return {
5064
+ enabled: parsed.enabled === true,
5065
+ intervalMinutes: Number.isFinite(intervalMinutes) && intervalMinutes > 0
5066
+ ? Math.min(Math.max(Math.round(intervalMinutes), 1), 1440)
5067
+ : 60,
5068
+ };
5069
+ }
5070
+
5071
+ function workspaceSchedulesPath() {
5072
+ return path.join(getAgentflowDataRoot(), WORKSPACE_SCHEDULES_FILENAME);
5073
+ }
5074
+
5075
+ function readWorkspaceScheduleRegistry() {
5076
+ const filePath = workspaceSchedulesPath();
5077
+ if (!fs.existsSync(filePath)) return { version: 1, schedules: {} };
5078
+ try {
5079
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
5080
+ return {
5081
+ version: 1,
5082
+ schedules: parsed?.schedules && typeof parsed.schedules === "object" && !Array.isArray(parsed.schedules)
5083
+ ? parsed.schedules
5084
+ : {},
5085
+ };
5086
+ } catch {
5087
+ return { version: 1, schedules: {} };
5088
+ }
5089
+ }
5090
+
5091
+ function writeWorkspaceScheduleRegistry(registry) {
5092
+ const filePath = workspaceSchedulesPath();
5093
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
5094
+ fs.writeFileSync(filePath, JSON.stringify({
5095
+ version: 1,
5096
+ updatedAt: new Date().toISOString(),
5097
+ schedules: registry?.schedules && typeof registry.schedules === "object" ? registry.schedules : {},
5098
+ }, null, 2) + "\n", "utf-8");
5099
+ }
5100
+
5101
+ function workspaceScheduleKey(userId, flowSource, flowId, runNodeId) {
5102
+ return [
5103
+ String(userId || ""),
5104
+ String(flowSource || "user"),
5105
+ String(flowId || ""),
5106
+ String(runNodeId || ""),
5107
+ ].join(":");
5108
+ }
5109
+
5110
+ function listWorkspaceScheduleStatusesForFlow(userCtx = {}, flowSource = "user", flowId = "") {
5111
+ const registry = readWorkspaceScheduleRegistry();
5112
+ const userId = String(userCtx.userId || "");
5113
+ return Object.values(registry.schedules || {})
5114
+ .filter((entry) => (
5115
+ String(entry?.userId || "") === userId &&
5116
+ String(entry?.flowSource || "user") === String(flowSource || "user") &&
5117
+ String(entry?.flowId || "") === String(flowId || "")
5118
+ ))
5119
+ .sort((a, b) => String(a.runNodeId || "").localeCompare(String(b.runNodeId || "")));
5120
+ }
5121
+
5122
+ function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx = {}) {
5123
+ const flowId = String(scoped?.flowId || "").trim();
5124
+ const flowSource = String(scoped?.flowSource || "user");
5125
+ const userId = String(userCtx.userId || authUser?.userId || "");
5126
+ if (!flowId || !userId) return [];
5127
+ const now = Date.now();
5128
+ const nowIso = new Date(now).toISOString();
5129
+ const registry = readWorkspaceScheduleRegistry();
5130
+ const schedules = { ...(registry.schedules || {}) };
5131
+ const prefix = `${userId}:${flowSource}:${flowId}:`;
5132
+ for (const key of Object.keys(schedules)) {
5133
+ if (key.startsWith(prefix)) delete schedules[key];
5134
+ }
5135
+ const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
5136
+ for (const [runNodeId, instance] of Object.entries(instances)) {
5137
+ if (String(instance?.definitionId || "") !== "workspace_scheduled_run") continue;
5138
+ const config = normalizeWorkspaceScheduledRunConfig(instance.body || "");
5139
+ if (!config.enabled) continue;
5140
+ const key = workspaceScheduleKey(userId, flowSource, flowId, runNodeId);
5141
+ const intervalMs = config.intervalMinutes * 60 * 1000;
5142
+ const previous = registry.schedules?.[key] && typeof registry.schedules[key] === "object" ? registry.schedules[key] : {};
5143
+ const previousNext = Number(previous.nextRunAt || 0);
5144
+ schedules[key] = {
5145
+ ...previous,
5146
+ key,
5147
+ enabled: true,
5148
+ userId,
5149
+ username: String(authUser?.username || previous.username || userId),
5150
+ flowId,
5151
+ flowSource,
5152
+ runNodeId,
5153
+ label: String(instance.label || "Scheduled Run"),
5154
+ intervalMinutes: config.intervalMinutes,
5155
+ nextRunAt: Number.isFinite(previousNext) && previousNext > now ? previousNext : now + intervalMs,
5156
+ lastStatus: previous.lastStatus || "armed",
5157
+ updatedAt: nowIso,
5158
+ };
5159
+ }
5160
+ const nextRegistry = { version: 1, schedules };
5161
+ writeWorkspaceScheduleRegistry(nextRegistry);
5162
+ return listWorkspaceScheduleStatusesForFlow(userCtx, flowSource, flowId);
5163
+ }
5164
+
5165
+ function updateWorkspaceScheduleEntry(key, patch) {
5166
+ const registry = readWorkspaceScheduleRegistry();
5167
+ const current = registry.schedules?.[key];
5168
+ if (!current) return null;
5169
+ const next = {
5170
+ ...current,
5171
+ ...(patch && typeof patch === "object" ? patch : {}),
5172
+ updatedAt: new Date().toISOString(),
5173
+ };
5174
+ registry.schedules[key] = next;
5175
+ writeWorkspaceScheduleRegistry(registry);
5176
+ return next;
5177
+ }
5178
+
5179
+ async function runWorkspaceScheduledEntry(root, entry) {
5180
+ const userCtx = { userId: String(entry.userId || "") };
5181
+ const runKey = workspaceRunKey(userCtx, entry.flowSource || "user", entry.flowId || "");
5182
+ const intervalMs = Math.max(1, Number(entry.intervalMinutes || 60)) * 60 * 1000;
5183
+ const nextRunAt = Date.now() + intervalMs;
5184
+ if (activeWorkspaceRuns.has(runKey)) {
5185
+ updateWorkspaceScheduleEntry(entry.key, {
5186
+ nextRunAt,
5187
+ lastSkippedAt: Date.now(),
5188
+ lastStatus: "skipped: busy",
5189
+ });
5190
+ return;
5191
+ }
5192
+ const scoped = resolveWorkspaceScopeRoot(root, {
5193
+ flowId: entry.flowId || "",
5194
+ flowSource: entry.flowSource || "user",
5195
+ }, userCtx);
5196
+ if (scoped.error || scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
5197
+ updateWorkspaceScheduleEntry(entry.key, {
5198
+ nextRunAt,
5199
+ lastStatus: "error",
5200
+ lastError: scoped.error || "Workspace schedule target is not writable",
5201
+ lastErrorAt: Date.now(),
5202
+ });
5203
+ return;
5204
+ }
5205
+ const graphPath = workspaceGraphPath(scoped.root);
5206
+ const graph = hydrateWorkspaceGraphForRuntime(root, scoped, readWorkspaceGraph(scoped.root).graph, userCtx);
5207
+ const instance = graph.instances?.[entry.runNodeId];
5208
+ const config = normalizeWorkspaceScheduledRunConfig(instance?.body || "");
5209
+ if (!instance || String(instance.definitionId || "") !== "workspace_scheduled_run" || !config.enabled) {
5210
+ updateWorkspaceScheduleEntry(entry.key, {
5211
+ enabled: false,
5212
+ nextRunAt: null,
5213
+ lastStatus: "disabled",
5214
+ });
5215
+ return;
5216
+ }
5217
+
5218
+ const controller = new AbortController();
5219
+ const authUsers = readAuthUsers();
5220
+ const authUser = authUsers[userCtx.userId] || {};
5221
+ const runEntry = {
5222
+ controller,
5223
+ child: null,
5224
+ runId: runLedgerId("workspace"),
5225
+ userId: userCtx.userId,
5226
+ username: String(authUser.username || entry.username || userCtx.userId),
5227
+ runNodeId: String(entry.runNodeId || ""),
5228
+ flowId: String(entry.flowId || ""),
5229
+ flowSource: String(entry.flowSource || "user"),
5230
+ startedAt: Date.now(),
5231
+ scheduled: true,
5232
+ stopChild() {
5233
+ if (this.child && !this.child.killed) {
5234
+ try { this.child.kill("SIGTERM"); } catch (_) {}
5235
+ }
5236
+ },
5237
+ };
5238
+ activeWorkspaceRuns.set(runKey, runEntry);
5239
+ appendWorkspaceRunStarted(runEntry);
5240
+ updateWorkspaceScheduleEntry(entry.key, {
5241
+ lastStatus: "running",
5242
+ lastTriggeredAt: runEntry.startedAt,
5243
+ lastRunId: runEntry.runId,
5244
+ lastError: "",
5245
+ });
5246
+ const setActiveChild = (child) => {
5247
+ runEntry.child = child || null;
5248
+ if (controller.signal.aborted) runEntry.stopChild();
5249
+ };
5250
+ try {
5251
+ const result = await runWorkspaceGraph(root, scoped.root, {
5252
+ flowId: entry.flowId,
5253
+ flowSource: entry.flowSource || "user",
5254
+ runNodeId: entry.runNodeId,
5255
+ graph,
5256
+ }, userCtx, {
5257
+ signal: controller.signal,
5258
+ onActiveChild: setActiveChild,
5259
+ });
5260
+ const currentGraph = readWorkspaceGraph(scoped.root).graph;
5261
+ const touchedIds = workspaceRunTouchedNodeIds(result);
5262
+ const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
5263
+ fs.writeFileSync(graphPath, JSON.stringify(mergedGraph, null, 2) + "\n", "utf-8");
5264
+ const endedAt = Date.now();
5265
+ appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "success");
5266
+ updateWorkspaceScheduleEntry(entry.key, {
5267
+ nextRunAt: Date.now() + intervalMs,
5268
+ lastFinishedAt: endedAt,
5269
+ lastStatus: "success",
5270
+ lastError: "",
5271
+ });
5272
+ } catch (e) {
5273
+ const endedAt = Date.now();
5274
+ appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "failed");
5275
+ updateWorkspaceScheduleEntry(entry.key, {
5276
+ nextRunAt: Date.now() + intervalMs,
5277
+ lastFinishedAt: endedAt,
5278
+ lastStatus: "failed",
5279
+ lastError: (e && e.message) || String(e),
5280
+ lastErrorAt: endedAt,
5281
+ });
5282
+ log.info(`[workspace-scheduler] failed ${entry.flowId}/${entry.runNodeId}: ${(e && e.message) || String(e)}`);
5283
+ } finally {
5284
+ if (activeWorkspaceRuns.get(runKey) === runEntry) activeWorkspaceRuns.delete(runKey);
5285
+ }
5286
+ }
5287
+
5288
+ function pollWorkspaceSchedules(root) {
5289
+ const now = Date.now();
5290
+ const registry = readWorkspaceScheduleRegistry();
5291
+ for (const entry of Object.values(registry.schedules || {})) {
5292
+ if (!entry || entry.enabled !== true) continue;
5293
+ const nextRunAt = Number(entry.nextRunAt || 0);
5294
+ if (!Number.isFinite(nextRunAt) || nextRunAt <= 0) {
5295
+ updateWorkspaceScheduleEntry(entry.key, {
5296
+ nextRunAt: now + Math.max(1, Number(entry.intervalMinutes || 60)) * 60 * 1000,
5297
+ lastStatus: entry.lastStatus || "armed",
5298
+ });
5299
+ continue;
5300
+ }
5301
+ if (nextRunAt > now) continue;
5302
+ void runWorkspaceScheduledEntry(root, entry);
5303
+ }
5304
+ }
5305
+
4847
5306
  /** Cursor/OpenCode 执行目录统一使用当前 UI 启动 workspace。 */
4848
5307
  function composerCliWorkspaceForFlowDir(workspaceRoot, _flowDir) {
4849
5308
  return path.resolve(workspaceRoot);
@@ -5181,6 +5640,7 @@ export function startUiServer({
5181
5640
  }
5182
5641
  try {
5183
5642
  const config = writeAdminStorageConfig(payload?.config || payload || {});
5643
+ clearSkillRegistryCache();
5184
5644
  json(res, 200, { ok: true, config });
5185
5645
  } catch (e) {
5186
5646
  json(res, 400, { error: (e && e.message) || String(e) });
@@ -5498,7 +5958,7 @@ export function startUiServer({
5498
5958
  return;
5499
5959
  }
5500
5960
  const { path: graphPath, graph } = readWorkspaceGraph(scoped.root);
5501
- const hydratedGraph = hydrateWorkspaceNodeRefsFromFiles(scoped.root, graph);
5961
+ const hydratedGraph = hydrateWorkspaceGraphForRuntime(root, scoped, graph, userCtx);
5502
5962
  json(res, 200, {
5503
5963
  ok: true,
5504
5964
  graph: hydratedGraph,
@@ -5508,6 +5968,7 @@ export function startUiServer({
5508
5968
  flowSource: scoped.flowSource,
5509
5969
  archived: scoped.archived,
5510
5970
  writable: !(scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)),
5971
+ workspaceSchedules: listWorkspaceScheduleStatusesForFlow(userCtx, scoped.flowSource || "user", scoped.flowId || ""),
5511
5972
  });
5512
5973
  } catch (e) {
5513
5974
  json(res, 500, { error: (e && e.message) || String(e) });
@@ -5588,12 +6049,30 @@ export function startUiServer({
5588
6049
  json(res, 400, { error: "Cannot write workspace graph for builtin or archived pipeline" });
5589
6050
  return;
5590
6051
  }
5591
- const submittedGraph = normalizeWorkspaceGraphPayload(payload.graph || payload);
6052
+ const submittedGraph = hydrateWorkspaceGraphForRuntime(root, scoped, payload.graph || payload, userCtx);
5592
6053
  const graphPath = workspaceGraphPath(scoped.root);
5593
- const currentGraph = hydrateWorkspaceNodeRefsFromFiles(scoped.root, readWorkspaceGraph(scoped.root).graph);
6054
+ const currentGraph = hydrateWorkspaceGraphForRuntime(root, scoped, readWorkspaceGraph(scoped.root).graph, userCtx);
5594
6055
  const graph = mergeWorkspacePersistentNodeRefs(submittedGraph, currentGraph);
5595
6056
  fs.writeFileSync(graphPath, JSON.stringify(graph, null, 2) + "\n", "utf-8");
5596
- json(res, 200, { ok: true, path: graphPath, graph });
6057
+ const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx);
6058
+ json(res, 200, { ok: true, path: graphPath, graph, workspaceSchedules });
6059
+ } catch (e) {
6060
+ json(res, 500, { error: (e && e.message) || String(e) });
6061
+ }
6062
+ return;
6063
+ }
6064
+
6065
+ if (req.method === "GET" && url.pathname === "/api/workspace/schedules") {
6066
+ try {
6067
+ const flowId = url.searchParams.get("flowId") || "";
6068
+ const flowSource = url.searchParams.get("flowSource") || "user";
6069
+ if (!flowId) {
6070
+ json(res, 400, { error: "Missing flowId" });
6071
+ return;
6072
+ }
6073
+ json(res, 200, {
6074
+ schedules: listWorkspaceScheduleStatusesForFlow(userCtx, flowSource, flowId),
6075
+ });
5597
6076
  } catch (e) {
5598
6077
  json(res, 500, { error: (e && e.message) || String(e) });
5599
6078
  }
@@ -6600,17 +7079,19 @@ export function startUiServer({
6600
7079
  }
6601
7080
 
6602
7081
  if (req.method === "GET" && url.pathname === "/api/skillhub/list") {
6603
- const target = url.searchParams.get("target") || "global";
7082
+ const target = url.searchParams.get("target") || "agentflow";
6604
7083
  const agent = url.searchParams.get("agent") || "codex";
6605
- const args = ["list", "--json"];
6606
- if (target === "all") args.push("--all");
6607
- else if (target === "global") args.push("--global", "--agent", agent);
7084
+ const args = skillhubListArgs(target, agent);
6608
7085
  const result = await runSkillhub(args, { cwd: root });
6609
7086
  if (!result.ok) {
6610
7087
  json(res, 500, { error: result.error, stdout: result.stdout });
6611
7088
  return;
6612
7089
  }
6613
- json(res, 200, { skills: normalizeSkillhubListPayload(parseJsonText(result.stdout, [])) });
7090
+ json(res, 200, {
7091
+ skills: normalizeSkillhubListPayload(parseJsonText(result.stdout, [])),
7092
+ target,
7093
+ skillsRoot: target === "agentflow" ? getAgentflowSkillsRoot() : "",
7094
+ });
6614
7095
  return;
6615
7096
  }
6616
7097
 
@@ -6655,6 +7136,10 @@ export function startUiServer({
6655
7136
  }
6656
7137
 
6657
7138
  if (req.method === "POST" && url.pathname === "/api/skillhub/install") {
7139
+ if (!authUser?.isAdmin) {
7140
+ json(res, 403, { error: "Admin required" });
7141
+ return;
7142
+ }
6658
7143
  let payload;
6659
7144
  try {
6660
7145
  payload = JSON.parse(await readBody(req));
@@ -6667,16 +7152,13 @@ export function startUiServer({
6667
7152
  json(res, 400, { error: "Missing skill slug or collection" });
6668
7153
  return;
6669
7154
  }
6670
- if (payload?.collection && !authUser?.isAdmin) {
6671
- json(res, 403, { error: "Admin required" });
6672
- return;
6673
- }
6674
7155
  const beforeSkills = payload?.collection ? listComposerSkills(PACKAGE_ROOT, root) : [];
6675
7156
  const result = await runSkillhub(args, { cwd: root, timeoutMs: 180_000, maxBuffer: 4 * 1024 * 1024 });
6676
7157
  if (!result.ok) {
6677
7158
  json(res, 500, { error: result.error, stdout: result.stdout });
6678
7159
  return;
6679
7160
  }
7161
+ clearSkillRegistryCache();
6680
7162
  let skillCollections = null;
6681
7163
  if (payload?.collection) {
6682
7164
  const afterSkills = listComposerSkills(PACKAGE_ROOT, root);
@@ -6688,6 +7170,10 @@ export function startUiServer({
6688
7170
  }
6689
7171
 
6690
7172
  if (req.method === "POST" && url.pathname === "/api/skillhub/uninstall") {
7173
+ if (!authUser?.isAdmin) {
7174
+ json(res, 403, { error: "Admin required" });
7175
+ return;
7176
+ }
6691
7177
  let payload;
6692
7178
  try {
6693
7179
  payload = JSON.parse(await readBody(req));
@@ -6700,21 +7186,22 @@ export function startUiServer({
6700
7186
  json(res, 400, { error: "Missing skill slug or collection" });
6701
7187
  return;
6702
7188
  }
6703
- if (payload?.collection && !authUser?.isAdmin) {
6704
- json(res, 403, { error: "Admin required" });
6705
- return;
6706
- }
6707
7189
  const result = await runSkillhub(args, { cwd: root, timeoutMs: 120_000, maxBuffer: 4 * 1024 * 1024 });
6708
7190
  if (!result.ok) {
6709
7191
  json(res, 500, { error: result.error, stdout: result.stdout });
6710
7192
  return;
6711
7193
  }
7194
+ clearSkillRegistryCache();
6712
7195
  const skillCollections = payload?.collection ? removeSkillhubCollectionGroup(userCtx, payload.collection, root) : null;
6713
7196
  json(res, 200, { ok: true, stdout: result.stdout, skillCollections });
6714
7197
  return;
6715
7198
  }
6716
7199
 
6717
7200
  if (req.method === "POST" && url.pathname === "/api/skillhub/update") {
7201
+ if (!authUser?.isAdmin) {
7202
+ json(res, 403, { error: "Admin required" });
7203
+ return;
7204
+ }
6718
7205
  const result = await runSkillhub(["update"], { cwd: root, timeoutMs: 180_000, maxBuffer: 4 * 1024 * 1024 });
6719
7206
  if (!result.ok) {
6720
7207
  json(res, 500, { error: result.error, stdout: result.stdout });
@@ -8154,6 +8641,25 @@ finishedAt: "${new Date().toISOString()}"
8154
8641
  res.end(data);
8155
8642
  });
8156
8643
 
8644
+ const workspaceScheduleTimer = setInterval(() => {
8645
+ try {
8646
+ pollWorkspaceSchedules(root);
8647
+ } catch (e) {
8648
+ log.debug(`[workspace-scheduler] poll failed: ${(e && e.message) || String(e)}`);
8649
+ }
8650
+ }, WORKSPACE_SCHEDULE_POLL_MS);
8651
+ try {
8652
+ workspaceScheduleTimer.unref?.();
8653
+ } catch (_) {}
8654
+ server.on("close", () => clearInterval(workspaceScheduleTimer));
8655
+ setTimeout(() => {
8656
+ try {
8657
+ pollWorkspaceSchedules(root);
8658
+ } catch (e) {
8659
+ log.debug(`[workspace-scheduler] initial poll failed: ${(e && e.message) || String(e)}`);
8660
+ }
8661
+ }, 1000).unref?.();
8662
+
8157
8663
  return new Promise((resolve, reject) => {
8158
8664
  server.once("error", reject);
8159
8665
  server.listen(port, host, () => {