@fieldwangai/agentflow 0.1.163 → 0.1.164

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.
@@ -1272,6 +1272,7 @@ function buildAdminUsageDashboard(workspaceRoot) {
1272
1272
  username: String(run.username || run.userId || ""),
1273
1273
  flowId: String(run.flowId || ""),
1274
1274
  flowSource: String(run.flowSource || "user"),
1275
+ archived: run.archived === true,
1275
1276
  runId: String(run.runId || ""),
1276
1277
  at: Number(run.at || 0),
1277
1278
  endedAt: run.endedAt == null ? null : Number(run.endedAt),
@@ -1993,6 +1994,50 @@ function setWorkspaceScheduleEnabled(root, payload = {}, authUser = {}, userCtx
1993
1994
  return { success: true, workspaceSchedules };
1994
1995
  }
1995
1996
 
1997
+ function schedulesForRequest(root, authUser = {}, userCtx = {}) {
1998
+ if (!authUser?.isAdmin) {
1999
+ return {
2000
+ adminView: false,
2001
+ users: [],
2002
+ schedules: listWorkspaceScheduleStatuses(root, userCtx),
2003
+ };
2004
+ }
2005
+ const users = listAuthUsers();
2006
+ const usernames = new Map(users.map((user) => [user.userId, user.username || user.userId]));
2007
+ const byIdentity = new Map();
2008
+ for (const user of users) {
2009
+ const rows = listWorkspaceScheduleStatuses(root, { ...userCtx, userId: user.userId });
2010
+ for (const row of rows) {
2011
+ const ownerUserId = String(row.ownerUserId || row.userId || user.userId);
2012
+ const key = String(row.key || `${ownerUserId}:${row.flowSource || "user"}:${row.flowId || ""}:${row.scheduleNodeId || ""}`);
2013
+ const identity = String(row.flowSource || "user") === "workspace"
2014
+ ? `workspace:${row.workspaceId || row.flowId || ""}:${row.scheduleNodeId || ""}`
2015
+ : key;
2016
+ const existing = byIdentity.get(identity);
2017
+ if (existing && (existing.registered || !row.registered)) continue;
2018
+ byIdentity.set(identity, {
2019
+ ...row,
2020
+ ownerUserId,
2021
+ ownerUsername: usernames.get(ownerUserId) || row.ownerUsername || ownerUserId,
2022
+ });
2023
+ }
2024
+ }
2025
+ const schedules = [...byIdentity.values()].sort((a, b) => {
2026
+ const ea = a.enabled ? 0 : 1;
2027
+ const eb = b.enabled ? 0 : 1;
2028
+ return ea - eb
2029
+ || String(a.nextRunAt || "").localeCompare(String(b.nextRunAt || ""))
2030
+ || String(a.ownerUsername || a.ownerUserId || "").localeCompare(String(b.ownerUsername || b.ownerUserId || ""))
2031
+ || String(a.flowId || "").localeCompare(String(b.flowId || ""));
2032
+ });
2033
+ const ownerIds = new Set(schedules.map((schedule) => String(schedule.ownerUserId || "")).filter(Boolean));
2034
+ return {
2035
+ adminView: true,
2036
+ users: users.filter((user) => ownerIds.has(user.userId)),
2037
+ schedules,
2038
+ };
2039
+ }
2040
+
1996
2041
  function pollWorkspaceSchedules(root) {
1997
2042
  const now = Date.now();
1998
2043
  const registry = readWorkspaceScheduleRegistry();
@@ -4578,14 +4623,7 @@ finishedAt: "${new Date().toISOString()}"
4578
4623
  try {
4579
4624
  // 旧 Pipeline schedule 随 Start/End 执行一并下线:它们只会驱动已废弃的
4580
4625
  // `agentflow apply`,列出来只会给用户永远不会触发的条目。
4581
- const workspaceSchedules = listWorkspaceScheduleStatuses(root, userCtx);
4582
- json(res, 200, {
4583
- schedules: [...workspaceSchedules].sort((a, b) => {
4584
- const ea = a.enabled ? 0 : 1;
4585
- const eb = b.enabled ? 0 : 1;
4586
- return ea - eb || String(a.nextRunAt || "").localeCompare(String(b.nextRunAt || "")) || String(a.flowId || "").localeCompare(String(b.flowId || ""));
4587
- }),
4588
- });
4626
+ json(res, 200, schedulesForRequest(root, authUser, userCtx));
4589
4627
  } catch (e) {
4590
4628
  json(res, 500, { error: (e && e.message) || String(e) });
4591
4629
  }
@@ -4603,7 +4641,23 @@ finishedAt: "${new Date().toISOString()}"
4603
4641
  const kind = String(payload.kind || "").trim();
4604
4642
  if (kind === "workspace") {
4605
4643
  try {
4606
- const result = setWorkspaceScheduleEnabled(root, payload, authUser, userCtx);
4644
+ const requestedOwnerId = String(payload.ownerUserId || userCtx.userId || "").trim();
4645
+ if (requestedOwnerId !== String(userCtx.userId || "") && !authUser?.isAdmin) {
4646
+ json(res, 403, { error: "Admin permission required to manage another user's schedule" });
4647
+ return;
4648
+ }
4649
+ const targetRecord = readAuthUsers()[requestedOwnerId];
4650
+ if (!targetRecord) {
4651
+ json(res, 404, { error: "Schedule owner not found" });
4652
+ return;
4653
+ }
4654
+ const targetAuthUser = {
4655
+ userId: requestedOwnerId,
4656
+ username: String(targetRecord.username || requestedOwnerId),
4657
+ isAdmin: Boolean(targetRecord.isAdmin),
4658
+ };
4659
+ const targetUserCtx = { ...userCtx, userId: requestedOwnerId };
4660
+ const result = setWorkspaceScheduleEnabled(root, payload, targetAuthUser, targetUserCtx);
4607
4661
  if (!result.success) {
4608
4662
  json(res, 400, { error: result.error || "Could not update workspace schedule" });
4609
4663
  return;
@@ -17,6 +17,7 @@ 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
+ listMarketplaceFlowSnippets,
20
21
  listMarketplaceFlows,
21
22
  listMarketplacePackages,
22
23
  nodePackageArchive,
@@ -30,6 +31,7 @@ import {
30
31
  import {
31
32
  appendMarketplaceUsageEvent,
32
33
  marketplaceResourcesForRun,
34
+ readMarketplaceFlowOrigin,
33
35
  writeMarketplaceFlowOrigin,
34
36
  } from "./marketplace-usage.mjs";
35
37
  import { NODE_PACKAGE_ENTRY, nodePackageExportsRun, readNodePackageManifest } from "./node-package-manifest.mjs";
@@ -47,7 +49,7 @@ import { mergeWorkspaceGraphs, workspaceDesignRevision, workspaceRuntimeRevision
47
49
  import { DEFAULT_WORKSPACE_PREVIEW_TTL_MS, createWorkspacePreviewId, normalizeWorkspacePreviewTtlMs, readWorkspacePreviewMetadata, workspaceSharedPreviewFlowDir, writeWorkspacePreviewMetadata } from "./workspace-preview.mjs";
48
50
  import { DEFAULT_WORKSPACE_DRAFT_TTL_MS, createWorkspaceDraftId, normalizeWorkspaceDraftTtlMs, readWorkspaceDraftMetadata, workspaceDraftFlowDir, writeWorkspaceDraftMetadata } from "./workspace-draft.mjs";
49
51
  import { appendWorkspaceRunLogEvent, createWorkspaceRunLogSession, finishWorkspaceRunLogSession, listWorkspaceRunLogs, readWorkspaceRunLogEvents } from "./workspace-run-logs.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";
52
+ import { activeWorkspaceRuns, appendWorkspaceRunFinished, appendWorkspaceRunStarted, cleanupWorkspaceRunResources, hydrateWorkspaceGraphForRuntime, isReadonlyBuiltinFlowSource, isTransientAgentNetworkError, isValidFlowSourceRead, isWorkspaceRunAbortError, listWorkspaceScheduleStatusesForFlow, mergeWorkspacePersistentNodeRefs, mergeWorkspaceRunGraph, normalizeWorkspaceEntry, normalizeWorkspaceScheduledRunConfig, publishWorkspaceRelease, readWorkspaceConversations, readWorkspaceFiles, readWorkspaceGraph, readWorkspaceReleaseStatus, removeWorkspaceDeferredRun, resolveWorkspaceFilePath, resolveWorkspaceScopeRoot, rollbackWorkspaceRelease, 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
53
  import { splitWorkspaceGraph, WORKSPACE_STATE_FILENAME } from "./workspace-state.mjs";
52
54
  import { getWorkspaceTree } from "./workspace-tree.mjs";
53
55
  import busboy from "busboy";
@@ -908,6 +910,45 @@ function workspaceBufferLooksLikeZip(buf) {
908
910
  );
909
911
  }
910
912
 
913
+ function installedMarketplaceFlowCopies(userId) {
914
+ const copies = new Map();
915
+ const pipelinesRoot = getUserPipelinesRoot(userId);
916
+ if (!fs.existsSync(pipelinesRoot)) return copies;
917
+ for (const entry of fs.readdirSync(pipelinesRoot, { withFileTypes: true })) {
918
+ if (!entry.isDirectory()) continue;
919
+ const origin = readMarketplaceFlowOrigin(path.join(pipelinesRoot, entry.name));
920
+ if (!origin) continue;
921
+ const key = `${origin.id}@${origin.version}`;
922
+ const flowIds = copies.get(key) || [];
923
+ flowIds.push(entry.name);
924
+ copies.set(key, flowIds);
925
+ }
926
+ return copies;
927
+ }
928
+
929
+ function marketplaceResourceMatches(item, queryText) {
930
+ if (!queryText) return true;
931
+ return [
932
+ item.id,
933
+ item.definitionId,
934
+ item.displayName,
935
+ item.description,
936
+ item.ownerUserId,
937
+ item.source,
938
+ ...(Array.isArray(item.tags) ? item.tags : []),
939
+ ].filter(Boolean).join("\n").toLowerCase().includes(queryText);
940
+ }
941
+
942
+ function sortMarketplaceResources(items) {
943
+ return items.sort((a, b) => (
944
+ Number(b.useCount || 0) - Number(a.useCount || 0)
945
+ || Number(b.installCount || 0) - Number(a.installCount || 0)
946
+ || String(b.updatedAt || b.createdAt || "").localeCompare(String(a.updatedAt || a.createdAt || ""))
947
+ || String(a.displayName || a.id).localeCompare(String(b.displayName || b.id))
948
+ || String(b.version || "").localeCompare(String(a.version || ""), undefined, { numeric: true, sensitivity: "base" })
949
+ ));
950
+ }
951
+
911
952
  /**
912
953
  * @param {import('http').IncomingMessage} req
913
954
  * @param {import('http').ServerResponse} res
@@ -1020,26 +1061,67 @@ async function workspaceRoutes(req, res, ctx) {
1020
1061
  if (req.method === "GET" && url.pathname === "/api/marketplace/resources") {
1021
1062
  try {
1022
1063
  const kind = String(url.searchParams.get("kind") || "flow").trim();
1023
- const marketplaceScope = url.searchParams.get("scope") === "owned" ? "owned" : "all";
1064
+ const requestedScope = String(url.searchParams.get("scope") || "all").trim();
1065
+ const scope = ["all", "owned", "installed"].includes(requestedScope) ? requestedScope : "all";
1066
+ const marketplaceScope = scope === "owned" ? "owned" : "all";
1024
1067
  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
1068
  if (kind === "node") {
1033
- const nodes = listMarketplacePackages(root, { ...userCtx, marketplaceScope }).nodes.filter(matches);
1034
- json(res, 200, { kind, sort: "useCount", order: "desc", items: nodes });
1069
+ const marketplaceNodes = listMarketplacePackages(root, { ...userCtx, marketplaceScope }).nodes
1070
+ .map((node) => ({ ...node, resourceType: "node", installed: true }));
1071
+ let nodes = marketplaceNodes;
1072
+ if (scope === "installed") {
1073
+ const marketplaceByDefinition = new Map(marketplaceNodes.map((node) => [node.definitionId, node]));
1074
+ nodes = listNodesJson(root, "", "", { ...userCtx, marketplaceScope: "all" }).nodes.map((node) => {
1075
+ const marketplaceNode = marketplaceByDefinition.get(node.id);
1076
+ if (marketplaceNode) {
1077
+ return {
1078
+ ...marketplaceNode,
1079
+ definitionId: node.id,
1080
+ source: node.source || "marketplace",
1081
+ installed: true,
1082
+ };
1083
+ }
1084
+ return {
1085
+ id: node.packageId || node.id,
1086
+ version: node.version || "",
1087
+ definitionId: node.id,
1088
+ displayName: node.displayName || node.label || node.id,
1089
+ description: node.description || "",
1090
+ inputs: node.inputs || {},
1091
+ outputs: node.outputs || {},
1092
+ source: node.source || "project",
1093
+ resourceType: "node",
1094
+ localCatalog: true,
1095
+ installed: true,
1096
+ };
1097
+ });
1098
+ }
1099
+ nodes = sortMarketplaceResources(nodes.filter((item) => marketplaceResourceMatches(item, queryText)));
1100
+ json(res, 200, { kind, scope, sort: "useCount", order: "desc", items: nodes });
1035
1101
  return;
1036
1102
  }
1037
1103
  if (kind !== "flow") {
1038
1104
  json(res, 400, { error: "kind must be flow or node" });
1039
1105
  return;
1040
1106
  }
1041
- const flows = listMarketplaceFlows(root, { ...userCtx, marketplaceScope }).flows.filter(matches);
1042
- json(res, 200, { kind, sort: "useCount", order: "desc", items: flows });
1107
+ const installedCopies = installedMarketplaceFlowCopies(userCtx.userId);
1108
+ const flows = listMarketplaceFlows(root, { ...userCtx, marketplaceScope }).flows.map((flow) => {
1109
+ const installedFlowIds = installedCopies.get(`${flow.id}@${flow.version}`) || [];
1110
+ return {
1111
+ ...flow,
1112
+ resourceType: "flow",
1113
+ installed: installedFlowIds.length > 0,
1114
+ installedFlowIds,
1115
+ };
1116
+ });
1117
+ const snippets = scope === "installed" ? [] : listMarketplaceFlowSnippets(root, { ...userCtx, marketplaceScope }).snippets
1118
+ .map((snippet) => ({ ...snippet, resourceType: "flow-snippet", installed: false }));
1119
+ const items = sortMarketplaceResources(
1120
+ [...flows, ...snippets]
1121
+ .filter((item) => scope !== "installed" || item.installed)
1122
+ .filter((item) => marketplaceResourceMatches(item, queryText)),
1123
+ );
1124
+ json(res, 200, { kind, scope, sort: "useCount", order: "desc", items });
1043
1125
  } catch (e) {
1044
1126
  json(res, 500, { error: (e && e.message) || String(e) });
1045
1127
  }
@@ -1590,7 +1672,7 @@ async function workspaceRoutes(req, res, ctx) {
1590
1672
  const written = writeWorkspaceGraph(flowDir, graph, root);
1591
1673
  writeWorkspaceDraftMetadata(flowDir, metadata);
1592
1674
  const persisted = readWorkspaceGraph(flowDir, root).graph;
1593
- const baseUrl = `${url.protocol}//${url.host}`;
1675
+ const baseUrl = requestPublicBaseUrl(req);
1594
1676
  json(res, 200, {
1595
1677
  ok: true,
1596
1678
  flowId,
@@ -1663,7 +1745,7 @@ async function workspaceRoutes(req, res, ctx) {
1663
1745
  if (scoped.error) throw new Error(scoped.error);
1664
1746
  const persisted = readWorkspaceGraph(scoped.root, root).graph;
1665
1747
  const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, persisted, authUser, userCtx);
1666
- const baseUrl = `${url.protocol}//${url.host}`;
1748
+ const baseUrl = requestPublicBaseUrl(req);
1667
1749
  json(res, 200, {
1668
1750
  ok: true,
1669
1751
  success: true,
@@ -1752,7 +1834,7 @@ async function workspaceRoutes(req, res, ctx) {
1752
1834
  json(res, 500, { error: (e && e.message) || String(e) });
1753
1835
  return;
1754
1836
  }
1755
- const baseUrl = `${url.protocol}//${url.host}`;
1837
+ const baseUrl = requestPublicBaseUrl(req);
1756
1838
  const workspaceUrl = `${baseUrl}/workspace?flowId=${encodeURIComponent(flowId)}&flowSource=workspace&archived=1`;
1757
1839
  json(res, 200, { ok: true, flowId, flowSource: "workspace", archived: true, preview: true, expiresAt: metadata.expiresAt, url: workspaceUrl });
1758
1840
  return;
@@ -1864,6 +1946,7 @@ async function workspaceRoutes(req, res, ctx) {
1864
1946
  ownerUserId: scoped.ownerUserId,
1865
1947
  ownerUsername: scoped.ownerUsername,
1866
1948
  } : null,
1949
+ release: readWorkspaceReleaseStatus(scoped.root, root, hydratedGraph),
1867
1950
  workspaceSchedules: listWorkspaceScheduleStatusesForFlow(scopedUserCtx, scoped.flowSource || "user", scoped.flowId || ""),
1868
1951
  });
1869
1952
  } catch (e) {
@@ -1878,6 +1961,126 @@ async function workspaceRoutes(req, res, ctx) {
1878
1961
  return;
1879
1962
  }
1880
1963
 
1964
+ if (req.method === "GET" && url.pathname === "/api/workspace/releases") {
1965
+ try {
1966
+ const scoped = resolveWorkspaceScopeRoot(root, {
1967
+ flowId: url.searchParams.get("flowId") || "",
1968
+ flowSource: url.searchParams.get("flowSource") || "user",
1969
+ workspaceId: url.searchParams.get("workspaceId") || "",
1970
+ adminOwnerId: url.searchParams.get("adminOwnerId") || "",
1971
+ archived: url.searchParams.get("archived") === "1",
1972
+ }, userCtx);
1973
+ if (scoped.error) {
1974
+ json(res, scoped.status || 400, { error: scoped.error });
1975
+ return;
1976
+ }
1977
+ json(res, 200, { ok: true, release: readWorkspaceReleaseStatus(scoped.root, root) });
1978
+ } catch (e) {
1979
+ json(res, 500, { error: (e && e.message) || String(e) });
1980
+ }
1981
+ return;
1982
+ }
1983
+
1984
+ if (req.method === "POST" && url.pathname === "/api/workspace/releases/publish") {
1985
+ let payload;
1986
+ try {
1987
+ payload = JSON.parse(await readBody(req));
1988
+ } catch {
1989
+ json(res, 400, { error: "Invalid JSON body" });
1990
+ return;
1991
+ }
1992
+ try {
1993
+ const scoped = resolveWorkspaceScopeRoot(root, {
1994
+ flowId: payload.flowId || "",
1995
+ flowSource: payload.flowSource || "user",
1996
+ workspaceId: payload.workspaceId || "",
1997
+ archived: payload.archived === true || payload.flowArchived === true,
1998
+ }, userCtx);
1999
+ if (scoped.error) {
2000
+ json(res, scoped.status || 400, { error: scoped.error });
2001
+ return;
2002
+ }
2003
+ if (
2004
+ scoped.archived
2005
+ || scoped.draft === true
2006
+ || isReadonlyBuiltinFlowSource(scoped.flowSource)
2007
+ || scoped.collaborationAccess?.writable === false
2008
+ ) {
2009
+ json(res, 403, { error: "Workspace release publish permission denied" });
2010
+ return;
2011
+ }
2012
+ const result = publishWorkspaceRelease(scoped.root, root, {
2013
+ expectedRevision: payload.expectedRevision || "",
2014
+ createdBy: userCtx.userId || authUser?.userId || "",
2015
+ notes: payload.notes || "",
2016
+ });
2017
+ if (result.error) {
2018
+ json(res, result.conflict ? 409 : 400, result);
2019
+ return;
2020
+ }
2021
+ const graph = readWorkspaceGraph(scoped.root, root).graph;
2022
+ const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx);
2023
+ broadcastWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
2024
+ type: "release.published",
2025
+ releaseId: result.release.id,
2026
+ revision: result.release.designRevision,
2027
+ actorId: userCtx.userId || "",
2028
+ });
2029
+ json(res, 200, { ...result, workspaceSchedules });
2030
+ } catch (e) {
2031
+ json(res, 500, { error: (e && e.message) || String(e) });
2032
+ }
2033
+ return;
2034
+ }
2035
+
2036
+ if (req.method === "POST" && url.pathname === "/api/workspace/releases/rollback") {
2037
+ let payload;
2038
+ try {
2039
+ payload = JSON.parse(await readBody(req));
2040
+ } catch {
2041
+ json(res, 400, { error: "Invalid JSON body" });
2042
+ return;
2043
+ }
2044
+ try {
2045
+ const scoped = resolveWorkspaceScopeRoot(root, {
2046
+ flowId: payload.flowId || "",
2047
+ flowSource: payload.flowSource || "user",
2048
+ workspaceId: payload.workspaceId || "",
2049
+ archived: payload.archived === true || payload.flowArchived === true,
2050
+ }, userCtx);
2051
+ if (scoped.error) {
2052
+ json(res, scoped.status || 400, { error: scoped.error });
2053
+ return;
2054
+ }
2055
+ if (
2056
+ scoped.archived
2057
+ || scoped.draft === true
2058
+ || isReadonlyBuiltinFlowSource(scoped.flowSource)
2059
+ || scoped.collaborationAccess?.writable === false
2060
+ ) {
2061
+ json(res, 403, { error: "Workspace release rollback permission denied" });
2062
+ return;
2063
+ }
2064
+ const result = rollbackWorkspaceRelease(scoped.root, payload.releaseId || "", root);
2065
+ if (result.error) {
2066
+ json(res, 404, result);
2067
+ return;
2068
+ }
2069
+ const graph = readWorkspaceGraph(scoped.root, root).graph;
2070
+ const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx);
2071
+ broadcastWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
2072
+ type: "release.rollback",
2073
+ releaseId: result.release.id,
2074
+ revision: result.release.designRevision,
2075
+ actorId: userCtx.userId || "",
2076
+ });
2077
+ json(res, 200, { ...result, workspaceSchedules });
2078
+ } catch (e) {
2079
+ json(res, 500, { error: (e && e.message) || String(e) });
2080
+ }
2081
+ return;
2082
+ }
2083
+
1881
2084
  if (req.method === "POST" && url.pathname === "/api/workspace/graph") {
1882
2085
  let payload;
1883
2086
  try {
@@ -2010,6 +2213,7 @@ async function workspaceRoutes(req, res, ctx) {
2010
2213
  designRevision: revision,
2011
2214
  runtimeRevision,
2012
2215
  merged,
2216
+ release: readWorkspaceReleaseStatus(scoped.root, root, committed.graph),
2013
2217
  workspaceSchedules,
2014
2218
  });
2015
2219
  } catch (e) {
@@ -90,6 +90,8 @@ function indexRecord(meta = {}, patch = {}) {
90
90
  flowSource: String(meta.flowSource || "user"),
91
91
  scheduleNodeId: String(meta.scheduleNodeId || ""),
92
92
  runNodeId: String(meta.runNodeId || ""),
93
+ releaseId: String(meta.releaseId || ""),
94
+ designRevision: String(meta.designRevision || ""),
93
95
  scheduled: meta.scheduled === true,
94
96
  trigger: String(meta.trigger || (meta.scheduled === true ? "scheduled" : "manual")),
95
97
  label: String(meta.label || ""),