@fieldwangai/agentflow 0.1.162 → 0.1.163

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.
Files changed (41) hide show
  1. package/bin/lib/flow-dsl/codegen.mjs +23 -2
  2. package/bin/lib/flow-dsl/lint.mjs +44 -0
  3. package/bin/lib/flow-dsl/parser.mjs +68 -1
  4. package/bin/lib/marketplace-usage.mjs +218 -0
  5. package/bin/lib/marketplace.mjs +183 -3
  6. package/bin/lib/node-package-manifest.mjs +1 -1
  7. package/bin/lib/spaces.mjs +200 -0
  8. package/bin/lib/ui-server.mjs +251 -67
  9. package/bin/lib/workspace-routes.mjs +189 -2
  10. package/bin/lib/workspace-server.mjs +367 -30
  11. package/bin/lib/workspace-state.mjs +2 -1
  12. package/builtin/nodes/agent_subAgent.md +5 -1
  13. package/builtin/nodes/context_bundle.md +44 -0
  14. package/builtin/nodes/context_knowledge.md +29 -0
  15. package/builtin/nodes/context_skills.md +29 -0
  16. package/builtin/nodes/context_workspace.md +36 -0
  17. package/builtin/nodes/control_while.md +7 -0
  18. package/builtin/nodes/tool_git_worktree_load.md +4 -3
  19. package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-CTrXOZ00.js → WorkflowAssistantThread-DmhIgoD7.js} +1 -1
  20. package/builtin/web-ui/dist/assets/index-CI9J6Unt.js +873 -0
  21. package/builtin/web-ui/dist/assets/index-DY5vE7v1.css +1 -0
  22. package/builtin/web-ui/dist/index.html +2 -2
  23. package/package.json +1 -1
  24. package/shared/slot-types.js +1 -0
  25. package/skills/agentflow-cli/SKILL.md +50 -3
  26. package/skills/agentflow-cli/runtime/bin/lib/skill-runtime.mjs +120 -6
  27. package/skills/agentflow-cli/runtime/builtin/nodes/agent_subAgent.md +5 -1
  28. package/skills/agentflow-cli/runtime/builtin/nodes/context_bundle.md +44 -0
  29. package/skills/agentflow-cli/runtime/builtin/nodes/context_knowledge.md +29 -0
  30. package/skills/agentflow-cli/runtime/builtin/nodes/context_skills.md +29 -0
  31. package/skills/agentflow-cli/runtime/builtin/nodes/context_workspace.md +36 -0
  32. package/skills/agentflow-cli/runtime/builtin/nodes/control_while.md +7 -0
  33. package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_worktree_load.md +4 -3
  34. package/skills/agentflow-cli/runtime/package.json +1 -1
  35. package/skills/agentflow-cli/scripts/agentflow-cli.mjs +64 -0
  36. package/skills/agentflow-flow-dsl/SKILL.md +48 -2
  37. package/skills/agentflow-flow-dsl/references/node-calls.md +6 -2
  38. package/skills/agentflow-flow-dsl/references/subflow-authoring.md +34 -7
  39. package/skills/agentflow-node-reference/references/builtin-nodes.md +37 -5
  40. package/builtin/web-ui/dist/assets/index-5uJFccdX.css +0 -1
  41. package/builtin/web-ui/dist/assets/index-BeUfNQRL.js +0 -873
@@ -33,6 +33,7 @@ import { t } from "./i18n.mjs";
33
33
  import { advanceJenkinsBuild, createJenkinsHttpInvoker, jenkinsBuildStatePath, normalizeJenkinsBuildConfig, readJenkinsBuildState, writeJenkinsBuildState } from "./jenkins.mjs";
34
34
  import { log } from "./log.mjs";
35
35
  import { resolveMarketplaceNodePackage } from "./marketplace.mjs";
36
+ import { marketplaceResourcesForRun, recordMarketplaceRunUsage } from "./marketplace-usage.mjs";
36
37
  import { PACKAGE_ROOT, getAgentflowDataRoot, getAgentflowUserDataRoot, listAgentflowUserIds } from "./paths.mjs";
37
38
  import { appendRunLedgerEvent, readRunLedgerEvents, runLedgerId } from "./run-ledger.mjs";
38
39
  import { computeNextRunAt } from "./schedule-config.mjs";
@@ -796,6 +797,71 @@ export function readWorkspacesFromPath(p, userCtx = {}) {
796
797
  }
797
798
  }
798
799
 
800
+ function legacyUserWorkspacesPath(userCtx = {}) {
801
+ return path.join(getAgentflowUserDataRoot(userCtx.userId || ""), USER_WORKSPACES_FILENAME);
802
+ }
803
+
804
+ function readLegacyAdminWorkspaces(userCtx = {}) {
805
+ const users = readAuthUsers();
806
+ const candidates = [];
807
+ for (const [userId, user] of Object.entries(users || {})) {
808
+ if (user?.isAdmin) candidates.push(String(userId || ""));
809
+ }
810
+ if (userCtx?.isAdmin && userCtx.userId) candidates.unshift(String(userCtx.userId));
811
+ const seenPaths = new Set();
812
+ const seenEntries = new Set();
813
+ const out = [];
814
+ for (const userId of candidates) {
815
+ const p = legacyUserWorkspacesPath({ userId });
816
+ const resolved = path.resolve(p);
817
+ if (seenPaths.has(resolved) || resolved === path.resolve(workspacesPath())) continue;
818
+ seenPaths.add(resolved);
819
+ for (const entry of readWorkspacesFromPath(p, { userId })) {
820
+ const key = entry.id || entry.path || entry.repoUrl;
821
+ if (seenEntries.has(key)) continue;
822
+ seenEntries.add(key);
823
+ out.push(entry);
824
+ }
825
+ }
826
+ return out;
827
+ }
828
+
829
+ /** The same authenticated Workspace catalog used by GET /api/workspaces. */
830
+ export function readUserWorkspaces(userCtx = {}) {
831
+ const globalPath = workspacesPath();
832
+ const globalWorkspaces = fs.existsSync(globalPath) ? readWorkspacesFromPath(globalPath, userCtx) : [];
833
+ const adminLegacy = readLegacyAdminWorkspaces(userCtx);
834
+ if (globalWorkspaces.length || adminLegacy.length) {
835
+ const seen = new Set();
836
+ const out = [];
837
+ for (const entry of [...globalWorkspaces, ...adminLegacy]) {
838
+ const key = entry.id || entry.path || entry.repoUrl;
839
+ if (seen.has(key)) continue;
840
+ seen.add(key);
841
+ out.push(entry);
842
+ }
843
+ return out;
844
+ }
845
+ return readWorkspacesFromPath(legacyUserWorkspacesPath(userCtx), userCtx);
846
+ }
847
+
848
+ export function listConfiguredWorkspaces(root, scopedRoot, userCtx = {}) {
849
+ const currentRoot = path.resolve(scopedRoot || root);
850
+ const homeRoot = path.resolve(os.homedir());
851
+ const builtins = [
852
+ { id: "current", label: "当前流程工作区", kind: "local", path: currentRoot, builtin: true, exists: fs.existsSync(currentRoot) && fs.statSync(currentRoot).isDirectory(), type: "flow", enabled: true },
853
+ { id: "home", label: "用户 Home", kind: "local", path: homeRoot, builtin: true, exists: fs.existsSync(homeRoot) && fs.statSync(homeRoot).isDirectory(), type: "local", enabled: true },
854
+ ];
855
+ const custom = readUserWorkspaces(userCtx).filter((entry) => entry.enabled !== false).map((entry) => ({ ...entry, builtin: false }));
856
+ const seen = new Set();
857
+ return [...builtins, ...custom].filter((entry) => {
858
+ const key = path.resolve(entry.path);
859
+ if (seen.has(key)) return false;
860
+ seen.add(key);
861
+ return true;
862
+ });
863
+ }
864
+
799
865
  export function workspaceRepoUrlWithCredential(repoUrl = "", credential = "") {
800
866
  const token = String(credential || "").trim();
801
867
  if (!token) return String(repoUrl || "").trim();
@@ -856,6 +922,10 @@ export function appendWorkspaceRunFinished(record, status) {
856
922
  durationMs: Math.max(0, Number(record.durationMs || (Number(record.endedAt || Date.now()) - Number(record.startedAt || record.at || Date.now())))),
857
923
  status,
858
924
  });
925
+ recordMarketplaceRunUsage(record.workspaceRoot || record.root || "", record.marketplaceResources || [], {
926
+ ...record,
927
+ status,
928
+ });
859
929
  }
860
930
 
861
931
  function normalizeWorkspaceUsageRecord(parsed, source = "workspace-run") {
@@ -1034,7 +1104,7 @@ function normalizeDisplayShareLayout(layout, fallback = "canvas") {
1034
1104
  return ["canvas", "gallery", "slides", "document", "single"].includes(text) ? text : fallback;
1035
1105
  }
1036
1106
 
1037
- export function createDisplayShareRecord({ userId, flowId, flowSource, archived, title, layout, nodeIds, expiresMode, expiresInDays, permanent, expiresAt }) {
1107
+ export function createDisplayShareRecord({ userId, flowId, flowSource, archived, title, layout, nodeIds, expiresMode, expiresInDays, permanent, expiresAt, visibility = "public" }) {
1038
1108
  const shares = readDisplayShares();
1039
1109
  let id = createDisplayShareId();
1040
1110
  while (shares[id]) id = createDisplayShareId();
@@ -1050,6 +1120,7 @@ export function createDisplayShareRecord({ userId, flowId, flowSource, archived,
1050
1120
  title: String(title || "").trim() || "AgentFlow Display",
1051
1121
  layout: normalizeDisplayShareLayout(layout, "canvas"),
1052
1122
  nodeIds: Array.isArray(nodeIds) ? nodeIds : [],
1123
+ visibility: String(visibility || "").trim().toLowerCase() === "private" ? "private" : "public",
1053
1124
  createdAt: now,
1054
1125
  updatedAt: now,
1055
1126
  expiresAt: expiry.expiresAt,
@@ -3175,7 +3246,7 @@ function workspaceTargetSlotForEdge(graph, edge) {
3175
3246
  function isWorkspaceSemanticInputSlot(slot) {
3176
3247
  const name = String(slot?.name || "");
3177
3248
  const type = String(slot?.type || "");
3178
- return type === "node" || name === "prev" || name === "next" || name === "skillsContext" || name === "mcpContext" || name === "knowledgeContext" || name === "workspaceContext" || name === "gitContext";
3249
+ return type === "node" || type === "context" || name === "prev" || name === "next" || name === "context" || name === "skillsContext" || name === "mcpContext" || name === "knowledgeContext" || name === "workspaceContext" || name === "gitContext";
3179
3250
  }
3180
3251
 
3181
3252
  function workspaceAgentInputBlock(inputValues = {}, inputMounts = {}) {
@@ -3230,8 +3301,9 @@ function workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputNames =
3230
3301
  return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge, scopedRoot);
3231
3302
  }
3232
3303
 
3233
- function workspaceInputValues(graph, nodeId, outputs, scopedRoot = "") {
3304
+ function workspaceInputValues(graph, nodeId, outputs, scopedRoot = "", options = {}) {
3234
3305
  const values = {};
3306
+ const includeContext = options?.includeContext === true;
3235
3307
  const edges = Array.isArray(graph?.edges) ? graph.edges : [];
3236
3308
  const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
3237
3309
  const target = instances[String(nodeId || "")] || {};
@@ -3241,13 +3313,13 @@ function workspaceInputValues(graph, nodeId, outputs, scopedRoot = "") {
3241
3313
  const index = workspaceHandleIndex(edge?.targetHandle, "input");
3242
3314
  const slot = inputSlots[index] || null;
3243
3315
  const name = String(slot?.name || "").trim();
3244
- if (!name || isWorkspaceSemanticInputSlot(slot)) continue;
3316
+ if (!name || (isWorkspaceSemanticInputSlot(slot) && !(includeContext && name === "context"))) continue;
3245
3317
  const value = workspaceOutputSlotValueForEdge(graph, outputs, edge, scopedRoot);
3246
3318
  if (String(value || "").trim()) values[name] = String(value);
3247
3319
  }
3248
3320
  for (const slot of inputSlots) {
3249
3321
  const name = String(slot?.name || "").trim();
3250
- if (!name || isWorkspaceSemanticInputSlot(slot) || Object.prototype.hasOwnProperty.call(values, name)) continue;
3322
+ if (!name || (isWorkspaceSemanticInputSlot(slot) && !(includeContext && name === "context")) || Object.prototype.hasOwnProperty.call(values, name)) continue;
3251
3323
  const value = workspaceSlotValue(slot);
3252
3324
  if (String(value || "").trim()) values[name] = String(value);
3253
3325
  }
@@ -3944,7 +4016,9 @@ function selectedSkillKeysFromInstance(instance) {
3944
4016
 
3945
4017
  function selectedSkillKeysFromConfigSlots(instance) {
3946
4018
  const slots = [...(Array.isArray(instance?.input) ? instance.input : []), ...(Array.isArray(instance?.output) ? instance.output : [])];
3947
- const slot = slots.find((item) => item?.name === "skillsContext") || slots.find((item) => item?.name === "skillKeys");
4019
+ const slot = slots.find((item) => item?.name === "skills") ||
4020
+ slots.find((item) => item?.name === "skillsContext") ||
4021
+ slots.find((item) => item?.name === "skillKeys");
3948
4022
  return parseWorkspaceSkillKeys(workspaceSlotValue(slot) || "");
3949
4023
  }
3950
4024
 
@@ -3998,6 +4072,25 @@ function workspaceSemanticInputText(graph, nodeId, outputs, name, scopedRoot = "
3998
4072
  return workspaceSlotValue(workspaceSlotByName(instance, targetName));
3999
4073
  }
4000
4074
 
4075
+ function workspaceContextBundleFromText(text) {
4076
+ const parsed = parseJsonText(String(text || "").trim(), null);
4077
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
4078
+ if (Number(parsed.version || 1) !== 1) return {};
4079
+ const out = {};
4080
+ for (const name of ["knowledgeContext", "skillsContext", "workspaceContext", "mcpContext"]) {
4081
+ if (parsed[name] !== undefined && parsed[name] !== null) {
4082
+ out[name] = typeof parsed[name] === "string" ? parsed[name] : JSON.stringify(parsed[name]);
4083
+ }
4084
+ }
4085
+ return out;
4086
+ }
4087
+
4088
+ function workspaceNodeContextBundle(graph, nodeId, outputs, scopedRoot = "") {
4089
+ return workspaceContextBundleFromText(
4090
+ workspaceSemanticInputText(graph, nodeId, outputs, "context", scopedRoot),
4091
+ );
4092
+ }
4093
+
4001
4094
  function workspaceContextObjectFromText(text, baseCwd, scopedRoot) {
4002
4095
  const raw = String(text || "").trim();
4003
4096
  if (!raw) return null;
@@ -4024,20 +4117,30 @@ function workspaceLooksLikeKnowledgePath(value) {
4024
4117
  }
4025
4118
 
4026
4119
  function workspaceKnowledgeSourceFromObject(source = {}, baseCwd = "", scopedRoot = "") {
4120
+ if (typeof source === "string") {
4121
+ const ref = source.trim();
4122
+ if (!ref) return null;
4123
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(ref)) {
4124
+ return { id: ref, label: ref, kind: "binding", type: "", path: "", repoPath: "", mountPath: "", repoUrl: "", branch: "", ref, readonly: true };
4125
+ }
4126
+ source = { path: ref };
4127
+ }
4027
4128
  if (!source || typeof source !== "object" || Array.isArray(source)) return null;
4129
+ const ref = String(source.ref || source.binding || "").trim();
4028
4130
  const rawPath = String(source.path || source.repoPath || source.cwd || source.workspaceRoot || "").trim();
4029
- if (!workspaceLooksLikeKnowledgePath(rawPath)) return null;
4030
- const resolvedPath = workspaceResolvePath(baseCwd || scopedRoot, rawPath) || rawPath;
4131
+ if (!workspaceLooksLikeKnowledgePath(rawPath) && !ref) return null;
4132
+ const resolvedPath = rawPath ? (workspaceResolvePath(baseCwd || scopedRoot, rawPath) || rawPath) : "";
4031
4133
  return {
4032
- id: String(source.id || source.mountPath || source.label || path.basename(resolvedPath) || "").trim(),
4033
- label: String(source.label || source.id || source.mountPath || path.basename(resolvedPath) || "知识库").trim(),
4034
- kind: String(source.kind || (source.repoUrl ? "git" : "local")).trim() || "local",
4134
+ id: String(source.id || source.mountPath || source.label || path.basename(resolvedPath) || ref || "").trim(),
4135
+ label: String(source.label || source.id || source.mountPath || path.basename(resolvedPath) || ref || "知识库").trim(),
4136
+ kind: String(source.kind || (source.repoUrl ? "git" : (ref ? "binding" : "local"))).trim() || "local",
4035
4137
  type: String(source.type || "").trim(),
4036
4138
  path: resolvedPath,
4037
4139
  repoPath: resolvedPath,
4038
4140
  mountPath: String(source.mountPath || "").trim(),
4039
4141
  repoUrl: String(source.repoUrl || "").trim(),
4040
4142
  branch: String(source.branch || "").trim(),
4143
+ ref,
4041
4144
  readonly: source.readonly !== false,
4042
4145
  };
4043
4146
  }
@@ -4073,7 +4176,7 @@ export function workspaceKnowledgeSourcesFromText(text, baseCwd = "", scopedRoot
4073
4176
  }
4074
4177
 
4075
4178
  function workspaceKnowledgeContextBlockFromSources(sources = []) {
4076
- const valid = Array.isArray(sources) ? sources.filter((source) => source?.path || source?.repoPath) : [];
4179
+ const valid = Array.isArray(sources) ? sources.filter((source) => source?.path || source?.repoPath || source?.ref) : [];
4077
4180
  if (!valid.length) return "";
4078
4181
  const lines = [
4079
4182
  "## 知识库上下文",
@@ -4087,6 +4190,7 @@ function workspaceKnowledgeContextBlockFromSources(sources = []) {
4087
4190
  lines.push(`${index + 1}. ${label}`);
4088
4191
  if (source.kind) lines.push(` - 类型:${source.kind}${source.type ? `/${source.type}` : ""}`);
4089
4192
  if (sourcePath) lines.push(` - 路径:\`${sourcePath}\``);
4193
+ if (source.ref) lines.push(` - 绑定:\`${source.ref}\``);
4090
4194
  if (source.mountPath) lines.push(` - 挂载目录:${source.mountPath}`);
4091
4195
  if (source.repoUrl) lines.push(` - Git URL:${source.repoUrl}`);
4092
4196
  if (source.branch) lines.push(` - 分支:${source.branch}`);
@@ -4100,10 +4204,11 @@ function workspaceDedupeKnowledgeSources(sources = []) {
4100
4204
  for (const source of Array.isArray(sources) ? sources : []) {
4101
4205
  if (!source || typeof source !== "object") continue;
4102
4206
  const key = [
4103
- path.resolve(String(source.path || source.repoPath || "")),
4207
+ source.path || source.repoPath ? path.resolve(String(source.path || source.repoPath)) : "",
4104
4208
  String(source.mountPath || ""),
4105
4209
  String(source.repoUrl || ""),
4106
4210
  String(source.branch || ""),
4211
+ String(source.ref || ""),
4107
4212
  ].join("\n");
4108
4213
  if (seen.has(key)) continue;
4109
4214
  seen.add(key);
@@ -4149,16 +4254,18 @@ function workspaceGlobalKnowledgeSources(graph, scopedRoot = "", logicalCwd = ""
4149
4254
  );
4150
4255
  }
4151
4256
 
4152
- function workspaceNodeWorkspaceContextBlock(graph, nodeId, outputs, scopedRoot = "", logicalCwd = "") {
4257
+ function workspaceNodeWorkspaceContextBlock(graph, nodeId, outputs, scopedRoot = "", logicalCwd = "", contextBundle = {}) {
4153
4258
  const root = scopedRoot ? path.resolve(scopedRoot) : "";
4154
4259
  const cwd = logicalCwd ? path.resolve(logicalCwd) : root;
4155
- const knowledgeText = workspaceSemanticInputText(graph, nodeId, outputs, "knowledgeContext", scopedRoot);
4260
+ const knowledgeText = workspaceSemanticInputText(graph, nodeId, outputs, "knowledgeContext", scopedRoot)
4261
+ || String(contextBundle?.knowledgeContext || "");
4156
4262
  let knowledgeSources = workspaceKnowledgeSourcesFromText(knowledgeText, cwd || root, scopedRoot);
4157
4263
  knowledgeSources = workspaceDedupeKnowledgeSources([
4158
4264
  ...workspaceGlobalKnowledgeSources(graph, scopedRoot, logicalCwd, nodeId),
4159
4265
  ...knowledgeSources,
4160
4266
  ]);
4161
- const workspaceText = workspaceSemanticInputText(graph, nodeId, outputs, "workspaceContext", scopedRoot);
4267
+ const workspaceText = workspaceSemanticInputText(graph, nodeId, outputs, "workspaceContext", scopedRoot)
4268
+ || String(contextBundle?.workspaceContext || "");
4162
4269
  let workspaceContext = workspaceContextObjectFromText(workspaceText, cwd || root, scopedRoot);
4163
4270
  if (!knowledgeSources.length && workspaceContext?.cwd) {
4164
4271
  knowledgeSources = workspaceKnowledgeSourcesFromText(JSON.stringify([workspaceContext]), cwd || root, scopedRoot);
@@ -4384,7 +4491,7 @@ function workspaceDefaultGitRepoRoot(scopedRoot, _userCtx = {}) {
4384
4491
  return path.join(path.resolve(scopedRoot), ".workspace", "agentflow", "git-repos");
4385
4492
  }
4386
4493
 
4387
- function workspaceDefaultWorktreePath(runTmpRoot, nodeId, repoPath, branch = "") {
4494
+ function workspaceDefaultWorktreePath(scopedRoot, runId, nodeId, repoPath, branch = "") {
4388
4495
  const repoRoot = path.resolve(repoPath);
4389
4496
  const repoName = sanitizeWorktreeName(path.basename(repoRoot));
4390
4497
  const branchName = String(branch || "").trim();
@@ -4402,9 +4509,12 @@ function workspaceDefaultWorktreePath(runTmpRoot, nodeId, repoPath, branch = "")
4402
4509
  : "HEAD";
4403
4510
  }
4404
4511
  return path.join(
4405
- path.resolve(runTmpRoot),
4406
- "worktrees",
4512
+ path.resolve(scopedRoot),
4513
+ ".workspace",
4514
+ "agentflow",
4515
+ "run-workspaces",
4407
4516
  workspaceSanitizeTmpSegment(nodeId, "node"),
4517
+ workspaceSanitizeTmpSegment(runId, "run"),
4408
4518
  repoName,
4409
4519
  sanitizeWorktreeName(refLabel),
4410
4520
  );
@@ -4444,15 +4554,18 @@ function workspaceMarkAutoWorktreeCleaned(graph, entry) {
4444
4554
  return true;
4445
4555
  }
4446
4556
 
4447
- function workspaceCleanupAutoWorktrees(list, graph, emit) {
4557
+ function workspaceCleanupAutoWorktrees(list, graph, emit, { force = false } = {}) {
4558
+ const cleaned = [];
4559
+ const preserved = [];
4448
4560
  for (const entry of [...list].reverse()) {
4449
4561
  try {
4450
4562
  const result = unloadGitWorktree({
4451
4563
  repoPath: entry.repoPath,
4452
4564
  worktreePath: entry.worktreePath,
4453
- force: true,
4565
+ force,
4454
4566
  prune: true,
4455
4567
  });
4568
+ cleaned.push(result.worktreePath);
4456
4569
  emit({
4457
4570
  type: "natural",
4458
4571
  kind: "status",
@@ -4463,15 +4576,74 @@ function workspaceCleanupAutoWorktrees(list, graph, emit) {
4463
4576
  emit({ type: "graph", nodeId: entry.nodeId, graph });
4464
4577
  }
4465
4578
  } catch (e) {
4579
+ preserved.push({ ...entry, reason: e?.message || String(e) });
4466
4580
  emit({
4467
4581
  type: "natural",
4468
4582
  kind: "warning",
4469
4583
  nodeId: entry.nodeId,
4470
- text: `临时 worktree 未自动清理:${entry.worktreePath}\n原因:${e?.message || String(e)}`,
4584
+ text: `运行 worktree 已保留:${entry.worktreePath}\n原因:${e?.message || String(e)}`,
4471
4585
  });
4472
4586
  }
4473
4587
  }
4474
4588
  list.splice(0, list.length);
4589
+ return { cleaned, preserved };
4590
+ }
4591
+
4592
+ function workspaceRunManifestPath(scopedRoot, runId) {
4593
+ const id = workspaceSanitizeTmpSegment(runId, "run");
4594
+ return path.join(path.resolve(scopedRoot), ".workspace", "agentflow", "run-manifests", `${id}.json`);
4595
+ }
4596
+
4597
+ function workspaceWriteRunManifest(scopedRoot, runId, value = {}) {
4598
+ const filePath = workspaceRunManifestPath(scopedRoot, runId);
4599
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
4600
+ const previous = (() => {
4601
+ try {
4602
+ return fs.existsSync(filePath) ? JSON.parse(fs.readFileSync(filePath, "utf-8")) : {};
4603
+ } catch {
4604
+ return {};
4605
+ }
4606
+ })();
4607
+ const next = {
4608
+ version: 1,
4609
+ ...previous,
4610
+ ...value,
4611
+ runId: String(runId || previous.runId || ""),
4612
+ updatedAt: new Date().toISOString(),
4613
+ };
4614
+ const tempPath = `${filePath}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
4615
+ fs.writeFileSync(tempPath, JSON.stringify(next, null, 2) + "\n", { encoding: "utf-8", mode: 0o600 });
4616
+ fs.renameSync(tempPath, filePath);
4617
+ return next;
4618
+ }
4619
+
4620
+ export function cleanupWorkspaceRunResources(scopedRoot, runId, { force = false, status = "stopped", emit = () => {} } = {}) {
4621
+ const filePath = workspaceRunManifestPath(scopedRoot, runId);
4622
+ if (!fs.existsSync(filePath)) return { cleaned: [], preserved: [], manifestPath: filePath };
4623
+ let manifest = {};
4624
+ try {
4625
+ manifest = JSON.parse(fs.readFileSync(filePath, "utf-8"));
4626
+ } catch {
4627
+ return { cleaned: [], preserved: [], manifestPath: filePath };
4628
+ }
4629
+ const resources = Array.isArray(manifest.worktrees) ? manifest.worktrees : [];
4630
+ const pending = resources.filter((entry) => entry?.repoPath && entry?.worktreePath && entry.removed !== true);
4631
+ const result = workspaceCleanupAutoWorktrees(pending.map((entry) => ({ ...entry })), null, emit, { force });
4632
+ const cleanedSet = new Set(result.cleaned.map((item) => path.resolve(item)));
4633
+ const preservedByPath = new Map(result.preserved.map((item) => [path.resolve(item.worktreePath), item]));
4634
+ const worktrees = resources.map((entry) => {
4635
+ const target = entry?.worktreePath ? path.resolve(entry.worktreePath) : "";
4636
+ if (target && cleanedSet.has(target)) return { ...entry, removed: true, removedAt: new Date().toISOString(), reason: "" };
4637
+ if (target && preservedByPath.has(target)) return { ...entry, removed: false, reason: preservedByPath.get(target).reason };
4638
+ return entry;
4639
+ });
4640
+ workspaceWriteRunManifest(scopedRoot, runId, {
4641
+ ...manifest,
4642
+ status: result.preserved.length ? `${status}:resources-preserved` : status,
4643
+ worktrees,
4644
+ finishedAt: new Date().toISOString(),
4645
+ });
4646
+ return { ...result, manifestPath: filePath };
4475
4647
  }
4476
4648
 
4477
4649
  function workspaceSanitizeTmpSegment(value, fallback = "node") {
@@ -5110,10 +5282,27 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5110
5282
  const runtimeEnv = (extra = {}) => runtimeEnvForUser(userCtx, { ...runEnv, ...(extra || {}) });
5111
5283
  const autoCleanupWorktrees = [];
5112
5284
  const runTmpRoot = workspaceCreateRunTmpRoot(scopedRoot, runNodeId);
5285
+ const runtimeRunId = String(opts.runId || payload?.runId || "").trim() || runLedgerId("workspace-execution");
5286
+ const ownsRunManifest = !(Array.isArray(opts?.subflowCallStack) && opts.subflowCallStack.length);
5287
+ const persistRunManifest = (status, extra = {}) => {
5288
+ if (!ownsRunManifest) return null;
5289
+ return workspaceWriteRunManifest(scopedRoot, runtimeRunId, {
5290
+ flowId: String(payload?.flowId || ""),
5291
+ flowSource: String(payload?.flowSource || "user"),
5292
+ runNodeId,
5293
+ status,
5294
+ runtimeRoot: runTmpRoot,
5295
+ artifactRoot: path.join(path.resolve(scopedRoot), "outputs"),
5296
+ worktrees: autoCleanupWorktrees.map((entry) => ({ ...entry, removed: false })),
5297
+ ...extra,
5298
+ });
5299
+ };
5300
+ persistRunManifest("running", { startedAt: new Date().toISOString() });
5113
5301
  const controlBranches = new Map();
5114
5302
  const skippedNodes = new Set();
5115
5303
  const runtimePauseNodeIds = [];
5116
5304
  let deferred = null;
5305
+ let runFailure = null;
5117
5306
  const incomingControlEdgesByTarget = new Map();
5118
5307
  for (const edge of Array.isArray(graph?.edges) ? graph.edges : []) {
5119
5308
  const target = String(edge?.target || "");
@@ -5177,11 +5366,87 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5177
5366
  continue;
5178
5367
  }
5179
5368
 
5369
+ if (defId === "context_knowledge") {
5370
+ const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
5371
+ const workspaceIds = parseWorkspaceSkillKeys(inputValues.workspaceIds || "[]");
5372
+ if (!workspaceIds.length) throw new Error(`context.knowledge ${nodeId} requires at least one Workspace ID`);
5373
+ const catalog = new Map(listConfiguredWorkspaces(root, scopedRoot, userCtx).map((entry) => [String(entry.id || ""), entry]));
5374
+ const missing = workspaceIds.filter((id) => !catalog.has(id));
5375
+ if (missing.length) throw new Error(`context.knowledge ${nodeId} cannot resolve Workspace IDs: ${missing.join(", ")}`);
5376
+ const unavailable = workspaceIds.filter((id) => catalog.get(id)?.exists === false);
5377
+ if (unavailable.length) throw new Error(`context.knowledge ${nodeId} Workspace paths are not ready: ${unavailable.join(", ")}`);
5378
+ const sources = workspaceIds
5379
+ .map((id, index) => workspaceKnowledgeSourceFromObject({ ...catalog.get(id), role: index === 0 ? "primary" : "context" }, cwd || scopedRoot, scopedRoot))
5380
+ .filter(Boolean);
5381
+ const value = JSON.stringify({ version: 1, sources });
5382
+ graph.instances[nodeId] = workspaceSetOutputSlot(instance, "knowledgeContext", value);
5383
+ publishNodeOutput(nodeId, value, { emitGraph: true });
5384
+ emit({ type: "node-done", nodeId, definitionId: defId, sourceCount: sources.length, workspaceIds });
5385
+ continue;
5386
+ }
5387
+
5388
+ if (defId === "context_skills") {
5389
+ const keys = selectedSkillKeysFromConfigSlots(instance);
5390
+ if (!keys.length) throw new Error(`context.skills ${nodeId} requires at least one skill`);
5391
+ const value = loadSkillsBlockForKeys(keys);
5392
+ graph.instances[nodeId] = workspaceSetOutputSlot(instance, "skillsContext", value);
5393
+ publishNodeOutput(nodeId, value, { emitGraph: true });
5394
+ emit({ type: "node-done", nodeId, definitionId: defId, skillCount: keys.length });
5395
+ continue;
5396
+ }
5397
+
5398
+ if (defId === "context_workspace") {
5399
+ const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
5400
+ const workspaceId = String(inputValues.workspaceId || "current").trim();
5401
+ const access = String(inputValues.access || "read-write").trim().toLowerCase();
5402
+ if (!["read-only", "read-write"].includes(access)) {
5403
+ throw new Error(`context.workspace ${nodeId} access must be read-only or read-write`);
5404
+ }
5405
+ const catalog = new Map(listConfiguredWorkspaces(root, scopedRoot, userCtx).map((entry) => [String(entry.id || ""), entry]));
5406
+ const selected = catalog.get(workspaceId);
5407
+ if (!selected) throw new Error(`context.workspace ${nodeId} cannot resolve Workspace ID ${workspaceId || "(empty)"}`);
5408
+ if (selected.exists === false) throw new Error(`context.workspace ${nodeId} Workspace path is not ready: ${workspaceId}`);
5409
+ const workspaceRoot = path.resolve(selected.path);
5410
+ const value = JSON.stringify({
5411
+ version: 1,
5412
+ workspaceId,
5413
+ access,
5414
+ label: String(selected.label || selected.id || path.basename(workspaceRoot)),
5415
+ cwd: workspaceRoot,
5416
+ workspaceRoot,
5417
+ pipelineWorkspace: path.resolve(scopedRoot),
5418
+ previous: null,
5419
+ });
5420
+ let nextInstance = workspaceSetOutputSlot(instance, "workspaceContext", value);
5421
+ graph.instances[nodeId] = nextInstance;
5422
+ publishNodeOutput(nodeId, value, { emitGraph: true });
5423
+ emit({ type: "node-done", nodeId, definitionId: defId, workspaceId, access });
5424
+ continue;
5425
+ }
5426
+
5427
+ if (defId === "context_bundle") {
5428
+ const bundle = {
5429
+ version: 1,
5430
+ knowledgeContext: workspaceSemanticInputText(graph, nodeId, outputs, "knowledgeContext", scopedRoot),
5431
+ skillsContext: workspaceSemanticInputText(graph, nodeId, outputs, "skillsContext", scopedRoot),
5432
+ workspaceContext: workspaceSemanticInputText(graph, nodeId, outputs, "workspaceContext", scopedRoot),
5433
+ mcpContext: workspaceSemanticInputText(graph, nodeId, outputs, "mcpContext", scopedRoot),
5434
+ };
5435
+ if (![bundle.knowledgeContext, bundle.skillsContext, bundle.workspaceContext, bundle.mcpContext].some((item) => String(item || "").trim())) {
5436
+ throw new Error(`context.bundle ${nodeId} requires at least one connected Context resource`);
5437
+ }
5438
+ const value = JSON.stringify(bundle);
5439
+ graph.instances[nodeId] = workspaceSetOutputSlot(instance, "context", value);
5440
+ publishNodeOutput(nodeId, value, { emitGraph: true });
5441
+ emit({ type: "node-done", nodeId, definitionId: defId });
5442
+ continue;
5443
+ }
5444
+
5180
5445
  if (defId === "control_subflow_call") {
5181
5446
  const subflowId = String(instance.subflowId || "").trim();
5182
5447
  const subflow = graph?.subflows?.[subflowId];
5183
5448
  if (!subflow) throw new Error(`flow.call ${nodeId} references missing subflow ${subflowId || "(empty)"}`);
5184
- const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
5449
+ const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot, { includeContext: true });
5185
5450
  const { resultValues, callFrameId } = await workspaceRunSubflowFrame({
5186
5451
  root,
5187
5452
  scopedRoot,
@@ -5297,6 +5562,7 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5297
5562
 
5298
5563
  if (defId === "control_while") {
5299
5564
  const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
5565
+ const loopContext = workspaceSemanticInputText(graph, nodeId, outputs, "context", scopedRoot);
5300
5566
  const config = normalizeControlWhileConfig(inputValues);
5301
5567
  const stepScript = String(instance.script || instance.body || "").trim();
5302
5568
  const stepScriptRef = String(instance.scriptRef || "").trim();
@@ -5392,6 +5658,7 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5392
5658
  parentDefinitionId: defId,
5393
5659
  subflowId: whileSubflows.conditionId,
5394
5660
  inputValues: {
5661
+ ...(loopContext && whileSubflows.condition.inputs?.context ? { context: loopContext } : {}),
5395
5662
  state: stateText,
5396
5663
  iteration: String(iteration),
5397
5664
  idempotencyKey,
@@ -5417,6 +5684,7 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5417
5684
  parentDefinitionId: defId,
5418
5685
  subflowId: whileSubflows.bodyId,
5419
5686
  inputValues: {
5687
+ ...(loopContext && whileSubflows.body.inputs?.context ? { context: loopContext } : {}),
5420
5688
  state: stateText,
5421
5689
  iteration: String(iteration),
5422
5690
  idempotencyKey,
@@ -5811,9 +6079,17 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5811
6079
  const worktreeInputSlot = (Array.isArray(instance.input) ? instance.input : [])
5812
6080
  .find((slot) => String(slot?.name || "") === "worktreePath") || null;
5813
6081
  const rawWorktreePath = workspaceSlotValue(worktreeInputSlot || workspaceSlotByName(instance, "worktreePath")).trim();
6082
+ const retainedWorktreePath = workspaceSlotValue(
6083
+ (Array.isArray(instance.output) ? instance.output : [])
6084
+ .find((slot) => String(slot?.name || "") === "worktreePath"),
6085
+ ).trim();
5814
6086
  const worktreePath = rawWorktreePath
5815
6087
  ? workspaceResolvePath(cwd, rawWorktreePath)
5816
- : (gitContext?.worktreePath ? path.resolve(gitContext.worktreePath) : workspaceDefaultWorktreePath(runTmpRoot, nodeId, repoPath, branch));
6088
+ : (gitContext?.worktreePath
6089
+ ? path.resolve(gitContext.worktreePath)
6090
+ : (retainedWorktreePath
6091
+ ? path.resolve(retainedWorktreePath)
6092
+ : workspaceDefaultWorktreePath(scopedRoot, runtimeRunId, nodeId, repoPath, branch)));
5817
6093
  const previousCwd = cwd;
5818
6094
  const force = ["true", "1", "yes", "on"].includes(workspaceSlotValue(workspaceSlotByName(instance, "force")).trim().toLowerCase());
5819
6095
  const pruneMissingRaw = workspaceSlotValue(workspaceSlotByName(instance, "pruneMissing")).trim().toLowerCase();
@@ -5825,6 +6101,7 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5825
6101
  repoPath: result.repoRoot,
5826
6102
  worktreePath: result.worktreePath,
5827
6103
  });
6104
+ persistRunManifest("running");
5828
6105
  }
5829
6106
  const outGitContext = buildGitContext({
5830
6107
  repoPath: result.repoRoot,
@@ -6067,10 +6344,17 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
6067
6344
  const relevantInputs = workspaceRelevantInputValues(instance.body || "", inputValues);
6068
6345
  workspaceAssertRequiredInputs(instance.body || "", inputValues, nodeId);
6069
6346
  const upstreamText = workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputs.placeholders, scopedRoot);
6070
- const upstreamSkillBlocks = workspaceUpstreamSkillBlocks(graph, nodeId, outputs);
6347
+ const contextBundle = workspaceNodeContextBundle(graph, nodeId, outputs, scopedRoot);
6348
+ const upstreamSkillBlocks = mergeWorkspaceSkillBlocks(
6349
+ workspaceUpstreamSkillBlocks(graph, nodeId, outputs),
6350
+ String(contextBundle.skillsContext || ""),
6351
+ );
6071
6352
  const ownSkillBlock = isContextRunNode ? loadSkillsBlockForKeys(selectedSkillKeysFromConfigSlots(instance)) : "";
6072
6353
  const promptSkillsBlock = mergeWorkspaceSkillBlocks(ownSkillBlock, upstreamSkillBlocks);
6073
- const promptMcpBlock = workspaceUpstreamMcpBlocks(graph, nodeId, outputs);
6354
+ const promptMcpBlock = mergeWorkspaceSkillBlocks(
6355
+ workspaceUpstreamMcpBlocks(graph, nodeId, outputs),
6356
+ String(contextBundle.mcpContext || ""),
6357
+ );
6074
6358
  const resultOutputSpec = workspaceResultOutputSpec(graph, nodeId);
6075
6359
  const runPackage = workspaceCreateNodeRunPackage(runTmpRoot, nodeId, {
6076
6360
  scopedRoot,
@@ -6095,7 +6379,7 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
6095
6379
  // Best-effort debug artifact only.
6096
6380
  }
6097
6381
  const historyBlock = workspaceNodeHistoryBlock(nodeId, scopedRoot, runPackage);
6098
- let workspaceContextBlock = workspaceNodeWorkspaceContextBlock(graph, nodeId, outputs, scopedRoot, cwd);
6382
+ let workspaceContextBlock = workspaceNodeWorkspaceContextBlock(graph, nodeId, outputs, scopedRoot, cwd, contextBundle);
6099
6383
  if (!isContextRunNode && workspaceBoolSlot(instance, "includeWorkspaceContext", true)) {
6100
6384
  const defaultWorkspaceBlock = workspaceDefaultWorkspaceContextBlock(scopedRoot, cwd);
6101
6385
  if (!workspaceContextBlock) {
@@ -6229,9 +6513,47 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
6229
6513
  outputFiles: normalizedAgentOutput.outputFiles || [],
6230
6514
  });
6231
6515
  }
6516
+ } catch (error) {
6517
+ runFailure = error;
6518
+ throw error;
6232
6519
  } finally {
6233
- workspaceCleanupAutoWorktrees(autoCleanupWorktrees, graph, emit);
6234
- workspaceCleanupTmpRoot(runTmpRoot, userCtx, emit);
6520
+ const retainForResume = Boolean(deferred || runtimePauseNodeIds.length);
6521
+ const trackedWorktrees = autoCleanupWorktrees.map((entry) => ({ ...entry }));
6522
+ let cleanup = { cleaned: [], preserved: [] };
6523
+ if (retainForResume) {
6524
+ emit({
6525
+ type: "status",
6526
+ line: `Run workspace retained for resume${trackedWorktrees.length ? ` (${trackedWorktrees.length} worktree)` : ""}`,
6527
+ runId: runtimeRunId,
6528
+ });
6529
+ persistRunManifest("waiting", {
6530
+ worktrees: trackedWorktrees.map((entry) => ({ ...entry, removed: false })),
6531
+ waitingAt: new Date().toISOString(),
6532
+ });
6533
+ autoCleanupWorktrees.splice(0, autoCleanupWorktrees.length);
6534
+ } else {
6535
+ cleanup = workspaceCleanupAutoWorktrees(autoCleanupWorktrees, graph, emit, { force: false });
6536
+ const cleanedSet = new Set(cleanup.cleaned.map((item) => path.resolve(item)));
6537
+ const preservedByPath = new Map(cleanup.preserved.map((item) => [path.resolve(item.worktreePath), item]));
6538
+ persistRunManifest(runFailure ? "failed" : "completed", {
6539
+ worktrees: trackedWorktrees.map((entry) => {
6540
+ const target = path.resolve(entry.worktreePath);
6541
+ if (cleanedSet.has(target)) return { ...entry, removed: true, removedAt: new Date().toISOString(), reason: "" };
6542
+ if (preservedByPath.has(target)) return { ...entry, removed: false, reason: preservedByPath.get(target).reason };
6543
+ return entry;
6544
+ }),
6545
+ finishedAt: new Date().toISOString(),
6546
+ error: runFailure ? (runFailure?.message || String(runFailure)) : "",
6547
+ resourcesPreserved: cleanup.preserved.length > 0,
6548
+ });
6549
+ }
6550
+ const protectedWorktrees = retainForResume ? trackedWorktrees : cleanup.preserved;
6551
+ const protectsRunTmpRoot = protectedWorktrees.some((entry) => workspacePathInside(runTmpRoot, entry.worktreePath));
6552
+ if (protectsRunTmpRoot) {
6553
+ emit({ type: "status", line: `Workspace tmp kept because it contains a retained worktree: ${runTmpRoot}` });
6554
+ } else {
6555
+ workspaceCleanupTmpRoot(runTmpRoot, userCtx, emit);
6556
+ }
6235
6557
  }
6236
6558
  const finalPauseNodeIds = Array.from(new Set([...pauseNodeIds, ...runtimePauseNodeIds]));
6237
6559
  if (!deferred && finalPauseNodeIds.length > 0) {
@@ -6444,6 +6766,14 @@ export function upsertWorkspaceDeferredRun(meta = {}, deferred = {}) {
6444
6766
  nodeId: String(deferred.nodeId || meta.nodeId || previous.nodeId || ""),
6445
6767
  label: String(meta.label || previous.label || "Workspace Run"),
6446
6768
  plannedNodeIds: Array.isArray(meta.plannedNodeIds) ? meta.plannedNodeIds.map(String) : (previous.plannedNodeIds || []),
6769
+ workspaceRoot: String(meta.workspaceRoot || previous.workspaceRoot || ""),
6770
+ marketplaceResources: Array.isArray(meta.marketplaceResources)
6771
+ ? meta.marketplaceResources.map((item) => ({
6772
+ kind: String(item?.kind || ""),
6773
+ id: String(item?.id || ""),
6774
+ version: String(item?.version || ""),
6775
+ })).filter((item) => item.kind && item.id && item.version)
6776
+ : (previous.marketplaceResources || []),
6447
6777
  startedAt: Number(meta.startedAt || previous.startedAt || now),
6448
6778
  scheduled: meta.scheduled === true || previous.scheduled === true,
6449
6779
  scheduleKey: String(meta.scheduleKey || previous.scheduleKey || ""),
@@ -6943,6 +7273,8 @@ export async function runWorkspaceScheduledEntry(root, entry) {
6943
7273
  plannedNodeIds,
6944
7274
  startedAt: Date.now(),
6945
7275
  scheduled: true,
7276
+ workspaceRoot: root,
7277
+ marketplaceResources: marketplaceResourcesForRun(scoped.root, graph, plannedNodeIds),
6946
7278
  };
6947
7279
  activeWorkspaceRuns.set(runKey, runEntry);
6948
7280
  appendWorkspaceRunStarted(runEntry);
@@ -6997,7 +7329,12 @@ export async function runWorkspaceScheduledEntry(root, entry) {
6997
7329
  return;
6998
7330
  }
6999
7331
  const endedAt = Date.now();
7000
- appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "success");
7332
+ appendWorkspaceRunFinished({
7333
+ ...runEntry,
7334
+ endedAt,
7335
+ durationMs: endedAt - runEntry.startedAt,
7336
+ marketplaceResources: marketplaceResourcesForRun(scoped.root, graph, result.order || Array.from(touchedIds)),
7337
+ }, "success");
7001
7338
  finishWorkspaceRunLogSession(runLog.runId, "success", {
7002
7339
  endedAt,
7003
7340
  durationMs: endedAt - runEntry.startedAt,