@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.
- package/bin/lib/auth.mjs +231 -14
- package/bin/lib/catalog-flows.mjs +2 -0
- package/bin/lib/cli-auth-page.mjs +103 -0
- package/bin/lib/flow-dsl/codegen.mjs +23 -2
- package/bin/lib/flow-dsl/lint.mjs +44 -0
- package/bin/lib/flow-dsl/parser.mjs +68 -1
- package/bin/lib/marketplace-usage.mjs +218 -0
- package/bin/lib/marketplace.mjs +183 -3
- package/bin/lib/node-package-manifest.mjs +1 -1
- package/bin/lib/skill-runtime.mjs +6 -0
- package/bin/lib/spaces.mjs +200 -0
- package/bin/lib/table.mjs +2 -3
- package/bin/lib/ui-server.mjs +471 -84
- package/bin/lib/workspace-draft.mjs +74 -0
- package/bin/lib/workspace-routes.mjs +484 -4
- package/bin/lib/workspace-server.mjs +373 -30
- package/bin/lib/workspace-state.mjs +2 -1
- package/builtin/nodes/agent_subAgent.md +5 -1
- package/builtin/nodes/context_bundle.md +44 -0
- package/builtin/nodes/context_knowledge.md +29 -0
- package/builtin/nodes/context_skills.md +29 -0
- package/builtin/nodes/context_workspace.md +36 -0
- package/builtin/nodes/control_while.md +12 -5
- package/builtin/nodes/tool_git_worktree_load.md +4 -3
- package/builtin/pipelines/new/workspace.flow.js +1 -1
- package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-Bfo9Ythw.js → WorkflowAssistantThread-DmhIgoD7.js} +1 -1
- package/builtin/web-ui/dist/assets/index-CI9J6Unt.js +873 -0
- package/builtin/web-ui/dist/assets/index-DY5vE7v1.css +1 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +5 -1
- package/shared/slot-types.js +1 -0
- package/skills/agentflow-author-flow/SKILL.md +81 -8
- package/skills/agentflow-author-flow/agents/openai.yaml +3 -3
- package/skills/agentflow-cli/SKILL.md +179 -27
- package/skills/agentflow-cli/agents/openai.yaml +2 -2
- package/skills/agentflow-cli/runtime/bin/lib/skill-runtime.mjs +14879 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/agent_subAgent.md +38 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/context_bundle.md +44 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/context_knowledge.md +29 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/context_skills.md +29 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/context_workspace.md +36 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_agent_toBool.md +26 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_anyOne.md +19 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_cancelled.md +26 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_cd_workspace.md +50 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_delay.md +31 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_end.md +13 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_if.md +21 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_interval_loop.md +55 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_load_mcp.md +24 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_load_skills.md +35 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_parse_json.md +25 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_start.md +13 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_subflow_call.md +27 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_toBool.md +27 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_user_workspace.md +24 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_wait_until.md +34 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_while.md +131 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/display_ascii.md +24 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/display_chart.md +32 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/display_code.md +39 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/display_html.md +32 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/display_image.md +36 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/display_markdown.md +24 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/display_mermaid.md +24 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/display_react_app.md +53 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/display_table.md +32 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/provide_bool.md +14 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/provide_file.md +14 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/provide_json.md +14 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/provide_password.md +14 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/provide_str.md +14 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_display_share_link.md +47 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_get_env.md +20 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_checkout.md +70 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_worktree_load.md +74 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_worktree_unload.md +52 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_gitlab_create_mr.md +114 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_jenkins_build.md +67 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_load_key.md +26 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_nodejs.md +52 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_print.md +21 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_save_key.md +26 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_set_run_env.md +38 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_user_ask.md +27 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_user_check.md +26 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_wecom_send_app_markdown.md +57 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_wecom_send_group_markdown.md +45 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/workspace_one_click_task.md +44 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/workspace_run.md +16 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/workspace_scheduled_run.md +16 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/workspace_subflow_input.md +14 -0
- package/skills/agentflow-cli/runtime/package.json +6 -0
- package/skills/agentflow-cli/scripts/agentflow-auth-store.mjs +113 -0
- package/skills/agentflow-cli/scripts/agentflow-cli.mjs +457 -28
- package/skills/agentflow-cli/scripts/agentflow-runtime.mjs +11 -4
- package/skills/agentflow-flow-add-instances/SKILL.md +2 -2
- package/skills/agentflow-flow-dsl/SKILL.md +73 -8
- package/skills/agentflow-flow-dsl/agents/openai.yaml +2 -2
- package/skills/agentflow-flow-dsl/references/node-calls.md +6 -2
- package/skills/agentflow-flow-dsl/references/subflow-authoring.md +34 -7
- package/skills/agentflow-flow-edit-node-fields/SKILL.md +3 -3
- package/skills/agentflow-flow-recipes/SKILL.md +1 -1
- package/skills/agentflow-flow-recipes/references/recipes.md +1 -2
- package/skills/agentflow-flow-sync-ui/SKILL.md +1 -1
- package/skills/agentflow-node-dsl/SKILL.md +5 -13
- package/skills/agentflow-node-reference/references/builtin-nodes.md +37 -5
- package/skills/agentflow-workspace-markdown/SKILL.md +1 -1
- package/builtin/web-ui/dist/assets/index-XbeI5foV.js +0 -872
- package/builtin/web-ui/dist/assets/index-e1omCEau.css +0 -1
- 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
|
-
|
|
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
|
}
|