@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.
- package/bin/lib/ui-server.mjs +63 -9
- package/bin/lib/workspace-routes.mjs +220 -16
- package/bin/lib/workspace-run-logs.mjs +2 -0
- package/bin/lib/workspace-server.mjs +335 -22
- package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-DmhIgoD7.js → WorkflowAssistantThread-CffIx5BY.js} +1 -1
- package/builtin/web-ui/dist/assets/index-CDItaRfX.css +1 -0
- package/builtin/web-ui/dist/assets/index-CWIcfWHO.js +873 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-CI9J6Unt.js +0 -873
- package/builtin/web-ui/dist/assets/index-DY5vE7v1.css +0 -1
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
1034
|
-
|
|
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
|
|
1042
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 || ""),
|