@fieldwangai/agentflow 0.1.160 → 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 (111) hide show
  1. package/bin/lib/auth.mjs +231 -14
  2. package/bin/lib/catalog-flows.mjs +2 -0
  3. package/bin/lib/cli-auth-page.mjs +103 -0
  4. package/bin/lib/flow-dsl/codegen.mjs +23 -2
  5. package/bin/lib/flow-dsl/lint.mjs +44 -0
  6. package/bin/lib/flow-dsl/parser.mjs +68 -1
  7. package/bin/lib/marketplace-usage.mjs +218 -0
  8. package/bin/lib/marketplace.mjs +183 -3
  9. package/bin/lib/node-package-manifest.mjs +1 -1
  10. package/bin/lib/skill-runtime.mjs +6 -0
  11. package/bin/lib/spaces.mjs +200 -0
  12. package/bin/lib/table.mjs +2 -3
  13. package/bin/lib/ui-server.mjs +471 -84
  14. package/bin/lib/workspace-draft.mjs +74 -0
  15. package/bin/lib/workspace-routes.mjs +484 -4
  16. package/bin/lib/workspace-server.mjs +373 -30
  17. package/bin/lib/workspace-state.mjs +2 -1
  18. package/builtin/nodes/agent_subAgent.md +5 -1
  19. package/builtin/nodes/context_bundle.md +44 -0
  20. package/builtin/nodes/context_knowledge.md +29 -0
  21. package/builtin/nodes/context_skills.md +29 -0
  22. package/builtin/nodes/context_workspace.md +36 -0
  23. package/builtin/nodes/control_while.md +12 -5
  24. package/builtin/nodes/tool_git_worktree_load.md +4 -3
  25. package/builtin/pipelines/new/workspace.flow.js +1 -1
  26. package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-Bfo9Ythw.js → WorkflowAssistantThread-DmhIgoD7.js} +1 -1
  27. package/builtin/web-ui/dist/assets/index-CI9J6Unt.js +873 -0
  28. package/builtin/web-ui/dist/assets/index-DY5vE7v1.css +1 -0
  29. package/builtin/web-ui/dist/index.html +2 -2
  30. package/package.json +5 -1
  31. package/shared/slot-types.js +1 -0
  32. package/skills/agentflow-author-flow/SKILL.md +81 -8
  33. package/skills/agentflow-author-flow/agents/openai.yaml +3 -3
  34. package/skills/agentflow-cli/SKILL.md +179 -27
  35. package/skills/agentflow-cli/agents/openai.yaml +2 -2
  36. package/skills/agentflow-cli/runtime/bin/lib/skill-runtime.mjs +14879 -0
  37. package/skills/agentflow-cli/runtime/builtin/nodes/agent_subAgent.md +38 -0
  38. package/skills/agentflow-cli/runtime/builtin/nodes/context_bundle.md +44 -0
  39. package/skills/agentflow-cli/runtime/builtin/nodes/context_knowledge.md +29 -0
  40. package/skills/agentflow-cli/runtime/builtin/nodes/context_skills.md +29 -0
  41. package/skills/agentflow-cli/runtime/builtin/nodes/context_workspace.md +36 -0
  42. package/skills/agentflow-cli/runtime/builtin/nodes/control_agent_toBool.md +26 -0
  43. package/skills/agentflow-cli/runtime/builtin/nodes/control_anyOne.md +19 -0
  44. package/skills/agentflow-cli/runtime/builtin/nodes/control_cancelled.md +26 -0
  45. package/skills/agentflow-cli/runtime/builtin/nodes/control_cd_workspace.md +50 -0
  46. package/skills/agentflow-cli/runtime/builtin/nodes/control_delay.md +31 -0
  47. package/skills/agentflow-cli/runtime/builtin/nodes/control_end.md +13 -0
  48. package/skills/agentflow-cli/runtime/builtin/nodes/control_if.md +21 -0
  49. package/skills/agentflow-cli/runtime/builtin/nodes/control_interval_loop.md +55 -0
  50. package/skills/agentflow-cli/runtime/builtin/nodes/control_load_mcp.md +24 -0
  51. package/skills/agentflow-cli/runtime/builtin/nodes/control_load_skills.md +35 -0
  52. package/skills/agentflow-cli/runtime/builtin/nodes/control_parse_json.md +25 -0
  53. package/skills/agentflow-cli/runtime/builtin/nodes/control_start.md +13 -0
  54. package/skills/agentflow-cli/runtime/builtin/nodes/control_subflow_call.md +27 -0
  55. package/skills/agentflow-cli/runtime/builtin/nodes/control_toBool.md +27 -0
  56. package/skills/agentflow-cli/runtime/builtin/nodes/control_user_workspace.md +24 -0
  57. package/skills/agentflow-cli/runtime/builtin/nodes/control_wait_until.md +34 -0
  58. package/skills/agentflow-cli/runtime/builtin/nodes/control_while.md +131 -0
  59. package/skills/agentflow-cli/runtime/builtin/nodes/display_ascii.md +24 -0
  60. package/skills/agentflow-cli/runtime/builtin/nodes/display_chart.md +32 -0
  61. package/skills/agentflow-cli/runtime/builtin/nodes/display_code.md +39 -0
  62. package/skills/agentflow-cli/runtime/builtin/nodes/display_html.md +32 -0
  63. package/skills/agentflow-cli/runtime/builtin/nodes/display_image.md +36 -0
  64. package/skills/agentflow-cli/runtime/builtin/nodes/display_markdown.md +24 -0
  65. package/skills/agentflow-cli/runtime/builtin/nodes/display_mermaid.md +24 -0
  66. package/skills/agentflow-cli/runtime/builtin/nodes/display_react_app.md +53 -0
  67. package/skills/agentflow-cli/runtime/builtin/nodes/display_table.md +32 -0
  68. package/skills/agentflow-cli/runtime/builtin/nodes/provide_bool.md +14 -0
  69. package/skills/agentflow-cli/runtime/builtin/nodes/provide_file.md +14 -0
  70. package/skills/agentflow-cli/runtime/builtin/nodes/provide_json.md +14 -0
  71. package/skills/agentflow-cli/runtime/builtin/nodes/provide_password.md +14 -0
  72. package/skills/agentflow-cli/runtime/builtin/nodes/provide_str.md +14 -0
  73. package/skills/agentflow-cli/runtime/builtin/nodes/tool_display_share_link.md +47 -0
  74. package/skills/agentflow-cli/runtime/builtin/nodes/tool_get_env.md +20 -0
  75. package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_checkout.md +70 -0
  76. package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_worktree_load.md +74 -0
  77. package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_worktree_unload.md +52 -0
  78. package/skills/agentflow-cli/runtime/builtin/nodes/tool_gitlab_create_mr.md +114 -0
  79. package/skills/agentflow-cli/runtime/builtin/nodes/tool_jenkins_build.md +67 -0
  80. package/skills/agentflow-cli/runtime/builtin/nodes/tool_load_key.md +26 -0
  81. package/skills/agentflow-cli/runtime/builtin/nodes/tool_nodejs.md +52 -0
  82. package/skills/agentflow-cli/runtime/builtin/nodes/tool_print.md +21 -0
  83. package/skills/agentflow-cli/runtime/builtin/nodes/tool_save_key.md +26 -0
  84. package/skills/agentflow-cli/runtime/builtin/nodes/tool_set_run_env.md +38 -0
  85. package/skills/agentflow-cli/runtime/builtin/nodes/tool_user_ask.md +27 -0
  86. package/skills/agentflow-cli/runtime/builtin/nodes/tool_user_check.md +26 -0
  87. package/skills/agentflow-cli/runtime/builtin/nodes/tool_wecom_send_app_markdown.md +57 -0
  88. package/skills/agentflow-cli/runtime/builtin/nodes/tool_wecom_send_group_markdown.md +45 -0
  89. package/skills/agentflow-cli/runtime/builtin/nodes/workspace_one_click_task.md +44 -0
  90. package/skills/agentflow-cli/runtime/builtin/nodes/workspace_run.md +16 -0
  91. package/skills/agentflow-cli/runtime/builtin/nodes/workspace_scheduled_run.md +16 -0
  92. package/skills/agentflow-cli/runtime/builtin/nodes/workspace_subflow_input.md +14 -0
  93. package/skills/agentflow-cli/runtime/package.json +6 -0
  94. package/skills/agentflow-cli/scripts/agentflow-auth-store.mjs +113 -0
  95. package/skills/agentflow-cli/scripts/agentflow-cli.mjs +457 -28
  96. package/skills/agentflow-cli/scripts/agentflow-runtime.mjs +11 -4
  97. package/skills/agentflow-flow-add-instances/SKILL.md +2 -2
  98. package/skills/agentflow-flow-dsl/SKILL.md +73 -8
  99. package/skills/agentflow-flow-dsl/agents/openai.yaml +2 -2
  100. package/skills/agentflow-flow-dsl/references/node-calls.md +6 -2
  101. package/skills/agentflow-flow-dsl/references/subflow-authoring.md +34 -7
  102. package/skills/agentflow-flow-edit-node-fields/SKILL.md +3 -3
  103. package/skills/agentflow-flow-recipes/SKILL.md +1 -1
  104. package/skills/agentflow-flow-recipes/references/recipes.md +1 -2
  105. package/skills/agentflow-flow-sync-ui/SKILL.md +1 -1
  106. package/skills/agentflow-node-dsl/SKILL.md +5 -13
  107. package/skills/agentflow-node-reference/references/builtin-nodes.md +37 -5
  108. package/skills/agentflow-workspace-markdown/SKILL.md +1 -1
  109. package/builtin/web-ui/dist/assets/index-XbeI5foV.js +0 -872
  110. package/builtin/web-ui/dist/assets/index-e1omCEau.css +0 -1
  111. package/skills/agentflow-workspace-graph/SKILL.md +0 -85
@@ -0,0 +1,74 @@
1
+ import crypto from "crypto";
2
+ import fs from "fs";
3
+ import path from "path";
4
+
5
+ import { getUserPipelinesRoot } from "./paths.mjs";
6
+
7
+ export const WORKSPACE_DRAFT_METADATA_FILENAME = ".agentflow-workspace-draft.json";
8
+ export const DEFAULT_WORKSPACE_DRAFT_TTL_MS = 2 * 60 * 60 * 1000;
9
+ export const MAX_WORKSPACE_DRAFT_TTL_MS = 24 * 60 * 60 * 1000;
10
+
11
+ export function safeWorkspaceDraftId(value = "") {
12
+ const raw = String(value || "").trim();
13
+ return /^draft_[a-z0-9_-]{8,100}$/i.test(raw) ? raw : "";
14
+ }
15
+
16
+ export function createWorkspaceDraftId() {
17
+ return `draft_${crypto.randomBytes(10).toString("hex")}`;
18
+ }
19
+
20
+ export function workspaceDraftFlowDir(draftId, userId = "") {
21
+ const safeId = safeWorkspaceDraftId(draftId);
22
+ if (!safeId) return "";
23
+ return path.join(getUserPipelinesRoot(userId), safeId);
24
+ }
25
+
26
+ export function workspaceDraftMetadataPath(flowDir) {
27
+ return path.join(flowDir, WORKSPACE_DRAFT_METADATA_FILENAME);
28
+ }
29
+
30
+ export function readWorkspaceDraftMetadata(flowDir) {
31
+ try {
32
+ const filePath = workspaceDraftMetadataPath(flowDir);
33
+ if (!fs.existsSync(filePath)) return null;
34
+ const value = JSON.parse(fs.readFileSync(filePath, "utf8"));
35
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
36
+ const flowId = safeWorkspaceDraftId(value.flowId);
37
+ const ownerId = String(value.ownerId || "").trim();
38
+ const expiresAt = String(value.expiresAt || "").trim();
39
+ if (!flowId || !ownerId || !Number.isFinite(Date.parse(expiresAt))) return null;
40
+ return { ...value, flowId, ownerId, expiresAt };
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ export function isWorkspaceDraftDir(flowDir) {
47
+ return Boolean(readWorkspaceDraftMetadata(flowDir));
48
+ }
49
+
50
+ export function normalizeWorkspaceDraftTtlMs(value) {
51
+ const requested = Number(value);
52
+ if (!Number.isFinite(requested) || requested <= 0) return DEFAULT_WORKSPACE_DRAFT_TTL_MS;
53
+ return Math.min(MAX_WORKSPACE_DRAFT_TTL_MS, Math.max(60_000, Math.round(requested)));
54
+ }
55
+
56
+ export function writeWorkspaceDraftMetadata(flowDir, metadata) {
57
+ const filePath = workspaceDraftMetadataPath(flowDir);
58
+ const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
59
+ fs.writeFileSync(tempPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
60
+ fs.renameSync(tempPath, filePath);
61
+ }
62
+
63
+ export function listExpiredWorkspaceDrafts(userPipelinesRoot, now = Date.now()) {
64
+ if (!fs.existsSync(userPipelinesRoot)) return [];
65
+ const expired = [];
66
+ for (const entry of fs.readdirSync(userPipelinesRoot, { withFileTypes: true })) {
67
+ if (!entry.isDirectory()) continue;
68
+ const flowDir = path.join(userPipelinesRoot, entry.name);
69
+ const metadata = readWorkspaceDraftMetadata(flowDir);
70
+ if (!metadata) continue;
71
+ if (Date.parse(metadata.expiresAt) <= now) expired.push({ flowDir, metadata });
72
+ }
73
+ return expired;
74
+ }
@@ -17,26 +17,38 @@ import { buildSkillCompactInjectionBlock, loadResourcesForSkillKeys } from "./co
17
17
  import { execFileBuffered } from "./exec-buffered.mjs";
18
18
  import { runGit } from "./git-worktree.mjs";
19
19
  import {
20
+ listMarketplaceFlows,
20
21
  listMarketplacePackages,
21
22
  nodePackageArchive,
23
+ publishMarketplaceFlow,
22
24
  publishNodePackage,
23
25
  publishNodePackageArchive,
26
+ readMarketplaceFlow,
24
27
  resolveMarketplaceNodePackage,
28
+ setMarketplaceVisibility,
25
29
  } from "./marketplace.mjs";
30
+ import {
31
+ appendMarketplaceUsageEvent,
32
+ marketplaceResourcesForRun,
33
+ writeMarketplaceFlowOrigin,
34
+ } from "./marketplace-usage.mjs";
26
35
  import { NODE_PACKAGE_ENTRY, nodePackageExportsRun, readNodePackageManifest } from "./node-package-manifest.mjs";
27
36
  import { inspectNodePackageDirectory } from "./node-package-archive.mjs";
28
37
  import { json, readBody } from "./http-util.mjs";
29
38
  import { log } from "./log.mjs";
30
- import { PACKAGE_ROOT, getAgentflowUserDataRoot } from "./paths.mjs";
39
+ import { PACKAGE_ROOT, PIPELINES_DIR, getAgentflowUserDataRoot, getUserPipelinesRoot } from "./paths.mjs";
40
+ import { validateUserPipelineId } from "./flow-write.mjs";
31
41
  import { runLedgerId } from "./run-ledger.mjs";
32
42
  import { getTeamById, getTeamForUser } from "./teams.mjs";
33
43
  import { readMergedEnvObject, runtimeEnvForUser } from "./user-env.mjs";
34
- import { acceptWorkspaceCollaborationInvite, addWorkspaceCollaborationMember, ensureWorkspaceCollaboration, getWorkspaceCollaborationForProject, listWorkspaceCollaborationsForUser, removeWorkspaceCollaborationMember, removeWorkspaceCollaborationTeamShare, setWorkspaceCollaborationTeamShare, workspaceCollaborationAccess } from "./workspace-collaboration.mjs";
44
+ import { acceptWorkspaceCollaborationInvite, addWorkspaceCollaborationMember, deleteWorkspaceCollaborationForFlow, ensureWorkspaceCollaboration, getWorkspaceCollaborationForProject, listWorkspaceCollaborationsForUser, removeWorkspaceCollaborationMember, removeWorkspaceCollaborationTeamShare, setWorkspaceCollaborationTeamShare, workspaceCollaborationAccess } from "./workspace-collaboration.mjs";
35
45
  import { WorkspaceFlowParseError } from "./workspace-flow-store.mjs";
36
46
  import { mergeWorkspaceGraphs, workspaceDesignRevision, workspaceRuntimeRevision } from "./workspace-graph-merge.mjs";
37
47
  import { DEFAULT_WORKSPACE_PREVIEW_TTL_MS, createWorkspacePreviewId, normalizeWorkspacePreviewTtlMs, readWorkspacePreviewMetadata, workspaceSharedPreviewFlowDir, writeWorkspacePreviewMetadata } from "./workspace-preview.mjs";
48
+ import { DEFAULT_WORKSPACE_DRAFT_TTL_MS, createWorkspaceDraftId, normalizeWorkspaceDraftTtlMs, readWorkspaceDraftMetadata, workspaceDraftFlowDir, writeWorkspaceDraftMetadata } from "./workspace-draft.mjs";
38
49
  import { appendWorkspaceRunLogEvent, createWorkspaceRunLogSession, finishWorkspaceRunLogSession, listWorkspaceRunLogs, readWorkspaceRunLogEvents } from "./workspace-run-logs.mjs";
39
- import { activeWorkspaceRuns, appendWorkspaceRunFinished, appendWorkspaceRunStarted, hydrateWorkspaceGraphForRuntime, isReadonlyBuiltinFlowSource, isTransientAgentNetworkError, isValidFlowSourceRead, isWorkspaceRunAbortError, listWorkspaceScheduleStatusesForFlow, mergeWorkspacePersistentNodeRefs, mergeWorkspaceRunGraph, normalizeWorkspaceEntry, readWorkspaceConversations, readWorkspaceFiles, readWorkspaceGraph, removeWorkspaceDeferredRun, resolveWorkspaceFilePath, resolveWorkspaceScopeRoot, runWorkspaceGraph, sleepMs, syncWorkspaceSchedulesForGraph, upsertWorkspaceDeferredRun, workspaceActiveRunsForScope, workspaceCollaborationEventKey, workspaceCollaborationSequences, workspaceCollaborationSubscribers, workspaceCollaborationSummaryWithUsers, workspaceDeferredRunsForScope, workspaceDesignPath, workspaceDownloadContentDisposition, workspaceFindActiveRunConflict, workspaceGraphAsSource, workspaceOptimizeRunImplementations, workspaceRepoUrlWithCredential, workspaceRunControl, workspaceRunEntryKey, workspaceRunKey, workspaceRunPlan, workspaceRunPlanNodeIds, workspaceRunTouchedNodeIds, workspaceRuntimeNodeLabel, workspaceScopedUserContext, workspaceSearchGuardrailsBlock, workspaceUnwrapOutputEnvelopeForDisplay, workspacesPath, writeWorkspaceConversations, writeWorkspaceGraph } from "./workspace-server.mjs";
50
+ import { activeWorkspaceRuns, appendWorkspaceRunFinished, appendWorkspaceRunStarted, cleanupWorkspaceRunResources, hydrateWorkspaceGraphForRuntime, isReadonlyBuiltinFlowSource, isTransientAgentNetworkError, isValidFlowSourceRead, isWorkspaceRunAbortError, listWorkspaceScheduleStatusesForFlow, mergeWorkspacePersistentNodeRefs, mergeWorkspaceRunGraph, normalizeWorkspaceEntry, normalizeWorkspaceScheduledRunConfig, readWorkspaceConversations, readWorkspaceFiles, readWorkspaceGraph, removeWorkspaceDeferredRun, resolveWorkspaceFilePath, resolveWorkspaceScopeRoot, runWorkspaceGraph, sleepMs, syncWorkspaceSchedulesForGraph, upsertWorkspaceDeferredRun, workspaceActiveRunsForScope, workspaceCollaborationEventKey, workspaceCollaborationSequences, workspaceCollaborationSubscribers, workspaceCollaborationSummaryWithUsers, workspaceDeferredRunsForScope, workspaceDesignPath, workspaceDownloadContentDisposition, workspaceFindActiveRunConflict, workspaceGraphAsSource, workspaceOptimizeRunImplementations, workspaceRepoUrlWithCredential, workspaceRunControl, workspaceRunEntryKey, workspaceRunKey, workspaceRunPlan, workspaceRunPlanNodeIds, workspaceRunTouchedNodeIds, workspaceRuntimeNodeLabel, workspaceScheduleNextRunAt, workspaceScopedUserContext, workspaceSearchGuardrailsBlock, workspaceUnwrapOutputEnvelopeForDisplay, workspacesPath, writeWorkspaceConversations, writeWorkspaceGraph } from "./workspace-server.mjs";
51
+ import { splitWorkspaceGraph, WORKSPACE_STATE_FILENAME } from "./workspace-state.mjs";
40
52
  import { getWorkspaceTree } from "./workspace-tree.mjs";
41
53
  import busboy from "busboy";
42
54
  import crypto from "crypto";
@@ -192,6 +204,29 @@ function commitWorkspaceGraph(workspaceRoot, scoped, graph, userCtx) {
192
204
  };
193
205
  }
194
206
 
207
+ export function workspaceGraphWithScheduleMode(graph, mode = "disabled") {
208
+ const normalizedMode = String(mode || "disabled").trim().toLowerCase();
209
+ if (!["enabled", "disabled", "preserve"].includes(normalizedMode)) {
210
+ throw new Error("scheduleMode must be enabled, disabled, or preserve");
211
+ }
212
+ if (normalizedMode === "preserve") return graph;
213
+ const enabled = normalizedMode === "enabled";
214
+ const instances = { ...(graph?.instances || {}) };
215
+ let scheduleCount = 0;
216
+ for (const [nodeId, instance] of Object.entries(instances)) {
217
+ if (String(instance?.definitionId || "") !== "workspace_scheduled_run") continue;
218
+ scheduleCount += 1;
219
+ const config = normalizeWorkspaceScheduledRunConfig(instance.body || "");
220
+ const nextConfig = { ...config, enabled };
221
+ if (enabled) workspaceScheduleNextRunAt(nextConfig, new Date());
222
+ instances[nodeId] = { ...instance, body: JSON.stringify(nextConfig) };
223
+ }
224
+ if (enabled && scheduleCount === 0) {
225
+ throw new Error("Cannot enable scheduling: the draft has no Scheduled Run node");
226
+ }
227
+ return { ...graph, instances };
228
+ }
229
+
195
230
  function missingWorkspaceGraphNodePackages(workspaceRoot, scoped, graph, userCtx) {
196
231
  const missing = new Set();
197
232
  for (const instance of Object.values(graph?.instances || {})) {
@@ -895,6 +930,11 @@ async function workspaceRoutes(req, res, ctx) {
895
930
  inputs: node.inputs,
896
931
  outputs: node.outputs,
897
932
  ownerUserId: node.ownerUserId || node.createdBy || "",
933
+ visibility: node.visibility || "public",
934
+ useCount: Number(node.useCount || 0),
935
+ installCount: Number(node.installCount || 0),
936
+ uniqueUserCount: Number(node.uniqueUserCount || 0),
937
+ lastUsedAt: node.lastUsedAt || "",
898
938
  fileList: node.fileList || [],
899
939
  fileCount: node.fileCount || 0,
900
940
  totalBytes: node.totalBytes || 0,
@@ -954,6 +994,14 @@ async function workspaceRoutes(req, res, ctx) {
954
994
  json(res, 404, { error: result.error || "Node package not found" });
955
995
  return;
956
996
  }
997
+ appendMarketplaceUsageEvent(root, {
998
+ kind: "node",
999
+ id,
1000
+ version,
1001
+ action: "install",
1002
+ actorUserId: userCtx.userId || "",
1003
+ eventId: `install:node:${id}@${version}:${userCtx.userId || "anonymous"}`,
1004
+ });
957
1005
  res.writeHead(200, {
958
1006
  "Content-Type": "application/zip",
959
1007
  "Content-Length": result.archive.length,
@@ -969,6 +1017,158 @@ async function workspaceRoutes(req, res, ctx) {
969
1017
  return;
970
1018
  }
971
1019
 
1020
+ if (req.method === "GET" && url.pathname === "/api/marketplace/resources") {
1021
+ try {
1022
+ const kind = String(url.searchParams.get("kind") || "flow").trim();
1023
+ const marketplaceScope = url.searchParams.get("scope") === "owned" ? "owned" : "all";
1024
+ const queryText = String(url.searchParams.get("q") || "").trim().toLowerCase();
1025
+ const matches = (item) => !queryText || [
1026
+ item.id,
1027
+ item.displayName,
1028
+ item.description,
1029
+ item.ownerUserId,
1030
+ ...(Array.isArray(item.tags) ? item.tags : []),
1031
+ ].filter(Boolean).join("\n").toLowerCase().includes(queryText);
1032
+ if (kind === "node") {
1033
+ const nodes = listMarketplacePackages(root, { ...userCtx, marketplaceScope }).nodes.filter(matches);
1034
+ json(res, 200, { kind, sort: "useCount", order: "desc", items: nodes });
1035
+ return;
1036
+ }
1037
+ if (kind !== "flow") {
1038
+ json(res, 400, { error: "kind must be flow or node" });
1039
+ return;
1040
+ }
1041
+ const flows = listMarketplaceFlows(root, { ...userCtx, marketplaceScope }).flows.filter(matches);
1042
+ json(res, 200, { kind, sort: "useCount", order: "desc", items: flows });
1043
+ } catch (e) {
1044
+ json(res, 500, { error: (e && e.message) || String(e) });
1045
+ }
1046
+ return;
1047
+ }
1048
+
1049
+ if (req.method === "POST" && url.pathname === "/api/marketplace/flows/publish") {
1050
+ let payload;
1051
+ try {
1052
+ payload = JSON.parse(await readBody(req));
1053
+ } catch {
1054
+ json(res, 400, { error: "Invalid JSON body" });
1055
+ return;
1056
+ }
1057
+ try {
1058
+ const flowId = String(payload?.flowId || "").trim();
1059
+ const flowSource = String(payload?.flowSource || "user").trim();
1060
+ const scoped = resolveWorkspaceScopeRoot(root, {
1061
+ flowId,
1062
+ flowSource,
1063
+ archived: payload?.archived === true,
1064
+ }, userCtx);
1065
+ if (scoped.error) {
1066
+ json(res, scoped.status || 400, { error: scoped.error });
1067
+ return;
1068
+ }
1069
+ if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
1070
+ json(res, 400, { error: "Only active editable flows can be published" });
1071
+ return;
1072
+ }
1073
+ const current = readWorkspaceGraph(scoped.root, root).graph;
1074
+ const { design } = splitWorkspaceGraph(current);
1075
+ const graph = workspaceGraphWithScheduleMode(design, "disabled");
1076
+ const result = publishMarketplaceFlow(root, {
1077
+ id: payload?.id || flowId,
1078
+ version: payload?.version || "1.0.0",
1079
+ displayName: payload?.displayName || flowId,
1080
+ description: payload?.description || "",
1081
+ tags: payload?.tags || [],
1082
+ visibility: payload?.visibility || "public",
1083
+ graph,
1084
+ }, userCtx);
1085
+ json(res, result.ok ? (result.alreadyExists ? 200 : 201) : (result.conflict ? 409 : 400), result);
1086
+ } catch (e) {
1087
+ json(res, 500, { error: (e && e.message) || String(e) });
1088
+ }
1089
+ return;
1090
+ }
1091
+
1092
+ if (req.method === "POST" && url.pathname === "/api/marketplace/flows/install") {
1093
+ let payload;
1094
+ try {
1095
+ payload = JSON.parse(await readBody(req));
1096
+ } catch {
1097
+ json(res, 400, { error: "Invalid JSON body" });
1098
+ return;
1099
+ }
1100
+ const id = String(payload?.id || "").trim();
1101
+ const version = String(payload?.version || "").trim();
1102
+ const idCheck = validateUserPipelineId(String(payload?.flowId || id));
1103
+ if (!id || !version || !idCheck.ok) {
1104
+ json(res, 400, { error: idCheck.error || "Missing marketplace flow id or version" });
1105
+ return;
1106
+ }
1107
+ const flowId = idCheck.flowId;
1108
+ const targetDir = path.join(getUserPipelinesRoot(userCtx.userId), flowId);
1109
+ if (fs.existsSync(targetDir)) {
1110
+ json(res, 409, { error: `已存在同名流水线 ${flowId}` });
1111
+ return;
1112
+ }
1113
+ try {
1114
+ const source = readMarketplaceFlow(root, id, version, { ...userCtx, marketplaceScope: "all" });
1115
+ if (!source.ok) {
1116
+ json(res, 404, { error: source.error || "Marketplace flow not found" });
1117
+ return;
1118
+ }
1119
+ fs.mkdirSync(targetDir, { recursive: true });
1120
+ const graph = workspaceGraphWithScheduleMode(source.graph, "disabled");
1121
+ writeWorkspaceGraph(targetDir, graph, root);
1122
+ writeMarketplaceFlowOrigin(targetDir, { id, version });
1123
+ appendMarketplaceUsageEvent(root, {
1124
+ kind: "flow",
1125
+ id,
1126
+ version,
1127
+ action: "install",
1128
+ actorUserId: userCtx.userId || "",
1129
+ eventId: `install:flow:${id}@${version}:${userCtx.userId || "anonymous"}`,
1130
+ });
1131
+ json(res, 201, {
1132
+ ok: true,
1133
+ id,
1134
+ version,
1135
+ flowId,
1136
+ flowSource: "user",
1137
+ url: `/workspace?flowId=${encodeURIComponent(flowId)}&flowSource=user`,
1138
+ });
1139
+ } catch (e) {
1140
+ try {
1141
+ if (fs.existsSync(targetDir)) fs.rmSync(targetDir, { recursive: true, force: true });
1142
+ } catch {}
1143
+ json(res, 500, { error: (e && e.message) || String(e) });
1144
+ }
1145
+ return;
1146
+ }
1147
+
1148
+ if (req.method === "PATCH" && url.pathname === "/api/marketplace/visibility") {
1149
+ let payload;
1150
+ try {
1151
+ payload = JSON.parse(await readBody(req));
1152
+ } catch {
1153
+ json(res, 400, { error: "Invalid JSON body" });
1154
+ return;
1155
+ }
1156
+ try {
1157
+ const result = setMarketplaceVisibility(
1158
+ root,
1159
+ String(payload?.kind || ""),
1160
+ String(payload?.id || ""),
1161
+ String(payload?.version || ""),
1162
+ String(payload?.visibility || "public"),
1163
+ userCtx,
1164
+ );
1165
+ json(res, result.ok ? 200 : 400, result);
1166
+ } catch (e) {
1167
+ json(res, 500, { error: (e && e.message) || String(e) });
1168
+ }
1169
+ return;
1170
+ }
1171
+
972
1172
  if (req.method === "GET" && url.pathname === "/api/workspace-tree") {
973
1173
  try {
974
1174
  json(res, 200, getWorkspaceTree(root));
@@ -1309,6 +1509,185 @@ async function workspaceRoutes(req, res, ctx) {
1309
1509
  return;
1310
1510
  }
1311
1511
 
1512
+ if (req.method === "POST" && url.pathname === "/api/workspace/draft") {
1513
+ if (!authUser?.userId) {
1514
+ json(res, 401, { error: "Authentication required" });
1515
+ return;
1516
+ }
1517
+ let payload;
1518
+ try {
1519
+ payload = JSON.parse(await readBody(req, 4 * 1024 * 1024));
1520
+ } catch {
1521
+ json(res, 400, { error: "Invalid JSON" });
1522
+ return;
1523
+ }
1524
+ const graph = payload?.graph;
1525
+ if (!graph || typeof graph !== "object" || Array.isArray(graph)) {
1526
+ json(res, 400, { error: "graph must be an object" });
1527
+ return;
1528
+ }
1529
+ const rawRequestedId = String(payload.draftId || "").trim();
1530
+ const flowId = rawRequestedId || createWorkspaceDraftId();
1531
+ const flowDir = workspaceDraftFlowDir(flowId, authUser.userId);
1532
+ if (!flowDir) {
1533
+ json(res, 400, { error: "Invalid draftId" });
1534
+ return;
1535
+ }
1536
+ const existing = readWorkspaceDraftMetadata(flowDir);
1537
+ if (existing && existing.ownerId !== authUser.userId) {
1538
+ json(res, 403, { error: "Draft ownership denied" });
1539
+ return;
1540
+ }
1541
+ if (rawRequestedId && !existing && fs.existsSync(flowDir)) {
1542
+ json(res, 409, { error: "Draft project already exists but is not a draft" });
1543
+ return;
1544
+ }
1545
+ if (existing) {
1546
+ const currentGraph = readWorkspaceGraph(flowDir, root).graph;
1547
+ const currentRevision = workspaceDesignRevision(currentGraph);
1548
+ const baseRevision = String(payload.baseRevision || "").trim();
1549
+ if (!baseRevision) {
1550
+ json(res, 428, {
1551
+ error: "Draft update requires baseRevision",
1552
+ conflict: "missing-base-revision",
1553
+ currentRevision,
1554
+ });
1555
+ return;
1556
+ }
1557
+ if (baseRevision !== currentRevision) {
1558
+ json(res, 409, {
1559
+ error: "Draft 已被更新,请先拉取最新版本后再修改",
1560
+ conflict: "revision-mismatch",
1561
+ expectedRevision: baseRevision,
1562
+ currentRevision,
1563
+ });
1564
+ return;
1565
+ }
1566
+ }
1567
+ const now = Date.now();
1568
+ const ttlInput = payload.ttlMs != null
1569
+ ? Number(payload.ttlMs)
1570
+ : payload.ttlSeconds != null
1571
+ ? Number(payload.ttlSeconds) * 1000
1572
+ : DEFAULT_WORKSPACE_DRAFT_TTL_MS;
1573
+ const ttlMs = normalizeWorkspaceDraftTtlMs(ttlInput);
1574
+ const metadata = {
1575
+ version: 1,
1576
+ flowId,
1577
+ ownerId: authUser.userId,
1578
+ title: String(payload.title || "Workspace Draft").trim().slice(0, 200),
1579
+ createdAt: existing?.createdAt || new Date(now).toISOString(),
1580
+ updatedAt: new Date(now).toISOString(),
1581
+ expiresAt: new Date(now + ttlMs).toISOString(),
1582
+ };
1583
+ const created = !fs.existsSync(flowDir);
1584
+ try {
1585
+ fs.mkdirSync(flowDir, { recursive: true });
1586
+ if (payload.resetRuntime !== false) {
1587
+ const statePath = path.join(flowDir, WORKSPACE_STATE_FILENAME);
1588
+ if (fs.existsSync(statePath)) fs.rmSync(statePath);
1589
+ }
1590
+ const written = writeWorkspaceGraph(flowDir, graph, root);
1591
+ writeWorkspaceDraftMetadata(flowDir, metadata);
1592
+ const persisted = readWorkspaceGraph(flowDir, root).graph;
1593
+ const baseUrl = `${url.protocol}//${url.host}`;
1594
+ json(res, 200, {
1595
+ ok: true,
1596
+ flowId,
1597
+ draftId: flowId,
1598
+ flowSource: "user",
1599
+ draft: true,
1600
+ writable: true,
1601
+ runnable: true,
1602
+ schedulesSuppressed: true,
1603
+ revision: workspaceDesignRevision(persisted),
1604
+ format: written.format,
1605
+ expiresAt: metadata.expiresAt,
1606
+ url: `${baseUrl}/workspace?flowId=${encodeURIComponent(flowId)}&flowSource=user`,
1607
+ });
1608
+ } catch (e) {
1609
+ if (created) {
1610
+ try { fs.rmSync(flowDir, { recursive: true, force: true }); } catch (_) {}
1611
+ }
1612
+ json(res, 500, { error: (e && e.message) || String(e) });
1613
+ }
1614
+ return;
1615
+ }
1616
+
1617
+ if (req.method === "POST" && url.pathname === "/api/workspace/draft/publish") {
1618
+ if (!authUser?.userId) {
1619
+ json(res, 401, { error: "Authentication required" });
1620
+ return;
1621
+ }
1622
+ let payload;
1623
+ try {
1624
+ payload = JSON.parse(await readBody(req));
1625
+ } catch {
1626
+ json(res, 400, { error: "Invalid JSON body" });
1627
+ return;
1628
+ }
1629
+ const draftId = String(payload.draftId || "").trim();
1630
+ const draftDir = workspaceDraftFlowDir(draftId, authUser.userId);
1631
+ const draftMetadata = draftDir ? readWorkspaceDraftMetadata(draftDir) : null;
1632
+ if (!draftMetadata || draftMetadata.ownerId !== authUser.userId) {
1633
+ json(res, 404, { error: "Workspace draft not found" });
1634
+ return;
1635
+ }
1636
+ const idCheck = validateUserPipelineId(String(payload.flowId || ""));
1637
+ if (!idCheck.ok) {
1638
+ json(res, 400, { error: idCheck.error });
1639
+ return;
1640
+ }
1641
+ const flowId = idCheck.flowId;
1642
+ const requestedTarget = String(payload.targetSpace || "personal").trim().toLowerCase();
1643
+ const flowSource = requestedTarget === "workspace" || requestedTarget === "team" ? "workspace" : "user";
1644
+ const targetDir = flowSource === "workspace"
1645
+ ? path.join(path.resolve(root), PIPELINES_DIR, flowId)
1646
+ : path.join(getUserPipelinesRoot(authUser.userId), flowId);
1647
+ if (fs.existsSync(targetDir)) {
1648
+ json(res, 409, { error: `已存在同名流水线 ${flowId}` });
1649
+ return;
1650
+ }
1651
+ try {
1652
+ const draftGraph = readWorkspaceGraph(draftDir, root).graph;
1653
+ const graph = workspaceGraphWithScheduleMode(draftGraph, payload.scheduleMode || "disabled");
1654
+ fs.mkdirSync(targetDir, { recursive: true });
1655
+ writeWorkspaceGraph(targetDir, graph, root);
1656
+ let collaborationCreated = false;
1657
+ if (flowSource === "workspace") {
1658
+ ensureWorkspaceCollaboration({ flowId, userId: authUser.userId });
1659
+ collaborationCreated = true;
1660
+ }
1661
+ try {
1662
+ const scoped = resolveWorkspaceScopeRoot(root, { flowId, flowSource }, userCtx);
1663
+ if (scoped.error) throw new Error(scoped.error);
1664
+ const persisted = readWorkspaceGraph(scoped.root, root).graph;
1665
+ const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, persisted, authUser, userCtx);
1666
+ const baseUrl = `${url.protocol}//${url.host}`;
1667
+ json(res, 200, {
1668
+ ok: true,
1669
+ success: true,
1670
+ action: "created",
1671
+ draftId,
1672
+ flowId,
1673
+ flowSource,
1674
+ targetSpace: requestedTarget === "team" ? "team" : flowSource === "user" ? "personal" : "workspace",
1675
+ scheduleMode: String(payload.scheduleMode || "disabled").toLowerCase(),
1676
+ workspaceSchedules,
1677
+ revision: workspaceDesignRevision(persisted),
1678
+ url: `${baseUrl}/workspace?flowId=${encodeURIComponent(flowId)}&flowSource=${encodeURIComponent(flowSource)}`,
1679
+ });
1680
+ } catch (e) {
1681
+ if (collaborationCreated) deleteWorkspaceCollaborationForFlow(flowId, false);
1682
+ throw e;
1683
+ }
1684
+ } catch (e) {
1685
+ try { fs.rmSync(targetDir, { recursive: true, force: true }); } catch (_) {}
1686
+ json(res, 500, { error: (e && e.message) || String(e) });
1687
+ }
1688
+ return;
1689
+ }
1690
+
1312
1691
  if (req.method === "POST" && url.pathname === "/api/workspace/preview") {
1313
1692
  if (!authUser?.userId) {
1314
1693
  json(res, 401, { error: "Authentication required" });
@@ -1475,6 +1854,7 @@ async function workspaceRoutes(req, res, ctx) {
1475
1854
  flowId: scoped.flowId,
1476
1855
  flowSource: scoped.flowSource,
1477
1856
  archived: scoped.archived,
1857
+ draft: scoped.draft === true,
1478
1858
  writable: !(scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource))
1479
1859
  && scoped.adminReadonly !== true
1480
1860
  && collaborationAccess.writable !== false,
@@ -1596,7 +1976,20 @@ async function workspaceRoutes(req, res, ctx) {
1596
1976
  const graph = mergeWorkspacePersistentNodeRefs(nextGraph, currentGraph);
1597
1977
  const committed = commitWorkspaceGraph(root, scoped, graph, userCtx);
1598
1978
  const { path: graphPath, revision, runtimeRevision } = committed;
1599
- const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, committed.graph, authUser, userCtx);
1979
+ let workspaceSchedules;
1980
+ try {
1981
+ workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, committed.graph, authUser, userCtx);
1982
+ } catch (scheduleError) {
1983
+ try {
1984
+ const rolledBack = commitWorkspaceGraph(root, scoped, currentStoredGraph, userCtx);
1985
+ syncWorkspaceSchedulesForGraph(root, scoped, rolledBack.graph, authUser, userCtx);
1986
+ } catch (rollbackError) {
1987
+ throw new Error(
1988
+ `Workspace schedule sync failed and graph rollback also failed: ${(scheduleError && scheduleError.message) || String(scheduleError)}; rollback: ${(rollbackError && rollbackError.message) || String(rollbackError)}`,
1989
+ );
1990
+ }
1991
+ throw new Error(`Workspace graph save rolled back: ${(scheduleError && scheduleError.message) || String(scheduleError)}`);
1992
+ }
1600
1993
  broadcastWorkspaceCollaborationEvent(
1601
1994
  userCtx,
1602
1995
  scoped.flowSource,
@@ -1655,6 +2048,80 @@ async function workspaceRoutes(req, res, ctx) {
1655
2048
  return;
1656
2049
  }
1657
2050
 
2051
+ if (req.method === "POST" && url.pathname === "/api/workspace/schedule/config") {
2052
+ let payload;
2053
+ try {
2054
+ payload = JSON.parse(await readBody(req));
2055
+ } catch {
2056
+ json(res, 400, { error: "Invalid JSON body" });
2057
+ return;
2058
+ }
2059
+ try {
2060
+ const flowId = String(payload.flowId || "").trim();
2061
+ const flowSource = String(payload.flowSource || "user").trim() || "user";
2062
+ const scheduleNodeId = String(payload.scheduleNodeId || "").trim();
2063
+ if (!flowId || !scheduleNodeId) {
2064
+ json(res, 400, { error: "flowId and scheduleNodeId are required" });
2065
+ return;
2066
+ }
2067
+ const scoped = resolveWorkspaceScopeRoot(root, { flowId, flowSource }, userCtx);
2068
+ if (scoped.error) {
2069
+ json(res, scoped.status || 400, { error: scoped.error });
2070
+ return;
2071
+ }
2072
+ if (
2073
+ scoped.archived
2074
+ || isReadonlyBuiltinFlowSource(scoped.flowSource)
2075
+ || scoped.collaborationAccess?.writable === false
2076
+ ) {
2077
+ json(res, 403, { error: "Workspace schedule edit permission denied" });
2078
+ return;
2079
+ }
2080
+ const graph = readWorkspaceGraph(scoped.root, root).graph;
2081
+ const instance = graph.instances?.[scheduleNodeId];
2082
+ if (!instance || String(instance.definitionId || "") !== "workspace_scheduled_run") {
2083
+ json(res, 404, { error: "Workspace schedule node not found" });
2084
+ return;
2085
+ }
2086
+ const current = normalizeWorkspaceScheduledRunConfig(instance.body || "");
2087
+ const config = {
2088
+ ...current,
2089
+ ...(typeof payload.enabled === "boolean" ? { enabled: payload.enabled } : {}),
2090
+ ...(payload.cron != null ? { cron: String(payload.cron).trim() } : {}),
2091
+ ...(payload.timezone != null ? { timezone: String(payload.timezone).trim() } : {}),
2092
+ ...(payload.overlapPolicy != null ? { overlapPolicy: String(payload.overlapPolicy).trim() } : {}),
2093
+ };
2094
+ if (config.overlapPolicy !== "skip") {
2095
+ json(res, 400, { error: "Only overlapPolicy=skip is supported" });
2096
+ return;
2097
+ }
2098
+ if (config.enabled) workspaceScheduleNextRunAt(config, new Date());
2099
+ graph.instances = { ...(graph.instances || {}) };
2100
+ graph.instances[scheduleNodeId] = { ...instance, body: JSON.stringify(config) };
2101
+ const committed = commitWorkspaceGraph(root, scoped, graph, userCtx);
2102
+ const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, committed.graph, authUser, userCtx);
2103
+ const schedule = workspaceSchedules.find((item) => item.scheduleNodeId === scheduleNodeId) || {
2104
+ flowId,
2105
+ flowSource,
2106
+ scheduleNodeId,
2107
+ ...config,
2108
+ suppressed: scoped.draft === true,
2109
+ };
2110
+ json(res, 200, {
2111
+ ok: true,
2112
+ flowId,
2113
+ flowSource,
2114
+ draft: scoped.draft === true,
2115
+ revision: committed.revision,
2116
+ schedule,
2117
+ workspaceSchedules,
2118
+ });
2119
+ } catch (e) {
2120
+ json(res, 400, { error: (e && e.message) || String(e) });
2121
+ }
2122
+ return;
2123
+ }
2124
+
1658
2125
  if (req.method === "POST" && url.pathname === "/api/workspace/run/plan") {
1659
2126
  let payload;
1660
2127
  try {
@@ -1865,6 +2332,8 @@ async function workspaceRoutes(req, res, ctx) {
1865
2332
  flowSource: scoped.flowSource || payload.flowSource || "user",
1866
2333
  plannedNodeIds,
1867
2334
  startedAt: Date.now(),
2335
+ workspaceRoot: root,
2336
+ marketplaceResources: marketplaceResourcesForRun(scoped.root, runtimeGraph, plannedNodeIds),
1868
2337
  };
1869
2338
  const runLog = createWorkspaceRunLogSession({
1870
2339
  runId,
@@ -1962,6 +2431,7 @@ async function workspaceRoutes(req, res, ctx) {
1962
2431
  ...runEntry,
1963
2432
  endedAt,
1964
2433
  durationMs: endedAt - runEntry.startedAt,
2434
+ marketplaceResources: marketplaceResourcesForRun(scoped.root, runtimeGraph, result.order || Array.from(touchedIds)),
1965
2435
  }, "success");
1966
2436
  finishWorkspaceRunLogSession(runLog.runId, "success", {
1967
2437
  endedAt,
@@ -2057,6 +2527,7 @@ async function workspaceRoutes(req, res, ctx) {
2057
2527
  ...runEntry,
2058
2528
  endedAt,
2059
2529
  durationMs: endedAt - runEntry.startedAt,
2530
+ marketplaceResources: marketplaceResourcesForRun(scoped.root, runtimeGraph, result.order || Array.from(touchedIds)),
2060
2531
  }, "success");
2061
2532
  finishWorkspaceRunLogSession(runLog.runId, "success", {
2062
2533
  endedAt,
@@ -2281,6 +2752,11 @@ async function workspaceRoutes(req, res, ctx) {
2281
2752
  return;
2282
2753
  }
2283
2754
  removeWorkspaceDeferredRun(deferred.key);
2755
+ const resourceCleanup = cleanupWorkspaceRunResources(scoped.root, deferred.runId, {
2756
+ force: false,
2757
+ status: "stopped",
2758
+ emit: (event) => appendWorkspaceRunLogEvent(deferred.runId, event),
2759
+ });
2284
2760
  const endedAt = Date.now();
2285
2761
  appendWorkspaceRunLogEvent(deferred.runId, {
2286
2762
  type: "stop-completed",
@@ -2309,6 +2785,10 @@ async function workspaceRoutes(req, res, ctx) {
2309
2785
  ok: true,
2310
2786
  stopped: true,
2311
2787
  monitoringOnly: true,
2788
+ resources: {
2789
+ cleaned: resourceCleanup.cleaned,
2790
+ preserved: resourceCleanup.preserved,
2791
+ },
2312
2792
  });
2313
2793
  return;
2314
2794
  }