@fieldwangai/agentflow 0.1.163 → 0.1.165

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.
@@ -11,12 +11,13 @@
11
11
  * 是请求回调的闭包,解构之后路由体里的写法完全不变。
12
12
  */
13
13
 
14
- import { listNodesJson, readNodeDetailJson, readNodeFilePreview } from "./catalog-flows.mjs";
14
+ import { listFlowsJson, listNodesJson, readNodeDetailJson, readNodeFilePreview } from "./catalog-flows.mjs";
15
15
  import { startComposerAgent } from "./composer-agent.mjs";
16
16
  import { buildSkillCompactInjectionBlock, loadResourcesForSkillKeys } from "./composer-skill-router.mjs";
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,
@@ -29,25 +30,28 @@ import {
29
30
  } from "./marketplace.mjs";
30
31
  import {
31
32
  appendMarketplaceUsageEvent,
33
+ marketplaceStatsFor,
34
+ marketplaceUsageStats,
32
35
  marketplaceResourcesForRun,
36
+ readMarketplaceFlowOrigin,
33
37
  writeMarketplaceFlowOrigin,
34
38
  } from "./marketplace-usage.mjs";
35
39
  import { NODE_PACKAGE_ENTRY, nodePackageExportsRun, readNodePackageManifest } from "./node-package-manifest.mjs";
36
40
  import { inspectNodePackageDirectory } from "./node-package-archive.mjs";
37
41
  import { json, readBody } from "./http-util.mjs";
38
42
  import { log } from "./log.mjs";
39
- import { PACKAGE_ROOT, PIPELINES_DIR, getAgentflowUserDataRoot, getUserPipelinesRoot } from "./paths.mjs";
43
+ import { PACKAGE_ROOT, PIPELINES_DIR, getAgentflowUserDataRoot, getUserPipelinesRoot, listAgentflowUserIds } from "./paths.mjs";
40
44
  import { validateUserPipelineId } from "./flow-write.mjs";
41
45
  import { runLedgerId } from "./run-ledger.mjs";
42
46
  import { getTeamById, getTeamForUser } from "./teams.mjs";
43
47
  import { readMergedEnvObject, runtimeEnvForUser } from "./user-env.mjs";
44
- import { acceptWorkspaceCollaborationInvite, addWorkspaceCollaborationMember, deleteWorkspaceCollaborationForFlow, ensureWorkspaceCollaboration, getWorkspaceCollaborationForProject, listWorkspaceCollaborationsForUser, removeWorkspaceCollaborationMember, removeWorkspaceCollaborationTeamShare, setWorkspaceCollaborationTeamShare, workspaceCollaborationAccess } from "./workspace-collaboration.mjs";
48
+ import { acceptWorkspaceCollaborationInvite, addWorkspaceCollaborationMember, deleteWorkspaceCollaborationForFlow, ensureWorkspaceCollaboration, getWorkspaceCollaborationByFlow, getWorkspaceCollaborationForProject, listWorkspaceCollaborationsForUser, removeWorkspaceCollaborationMember, removeWorkspaceCollaborationTeamShare, setWorkspaceCollaborationTeamShare, workspaceCollaborationAccess } from "./workspace-collaboration.mjs";
45
49
  import { WorkspaceFlowParseError } from "./workspace-flow-store.mjs";
46
50
  import { mergeWorkspaceGraphs, workspaceDesignRevision, workspaceRuntimeRevision } from "./workspace-graph-merge.mjs";
47
51
  import { DEFAULT_WORKSPACE_PREVIEW_TTL_MS, createWorkspacePreviewId, normalizeWorkspacePreviewTtlMs, readWorkspacePreviewMetadata, workspaceSharedPreviewFlowDir, writeWorkspacePreviewMetadata } from "./workspace-preview.mjs";
48
52
  import { DEFAULT_WORKSPACE_DRAFT_TTL_MS, createWorkspaceDraftId, normalizeWorkspaceDraftTtlMs, readWorkspaceDraftMetadata, workspaceDraftFlowDir, writeWorkspaceDraftMetadata } from "./workspace-draft.mjs";
49
53
  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";
54
+ import { activeWorkspaceRuns, appendWorkspaceRunFinished, appendWorkspaceRunStarted, cleanupWorkspaceRunResources, hydrateWorkspaceGraphForRuntime, isReadonlyBuiltinFlowSource, isTransientAgentNetworkError, isValidFlowSourceRead, isWorkspaceRunAbortError, listWorkspaceScheduleStatusesForFlow, mergeWorkspacePersistentNodeRefs, mergeWorkspaceRunGraph, normalizeWorkspaceEntry, normalizeWorkspaceScheduledRunConfig, publishWorkspaceRelease, readWorkspaceConversations, readWorkspaceFiles, readWorkspaceGraph, readWorkspaceReleaseStatus, readWorkspaceStableRelease, 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
55
  import { splitWorkspaceGraph, WORKSPACE_STATE_FILENAME } from "./workspace-state.mjs";
52
56
  import { getWorkspaceTree } from "./workspace-tree.mjs";
53
57
  import busboy from "busboy";
@@ -908,6 +912,163 @@ function workspaceBufferLooksLikeZip(buf) {
908
912
  );
909
913
  }
910
914
 
915
+ function installedMarketplaceFlowCopies(userId) {
916
+ const copies = new Map();
917
+ const pipelinesRoot = getUserPipelinesRoot(userId);
918
+ if (!fs.existsSync(pipelinesRoot)) return copies;
919
+ for (const entry of fs.readdirSync(pipelinesRoot, { withFileTypes: true })) {
920
+ if (!entry.isDirectory()) continue;
921
+ const origin = readMarketplaceFlowOrigin(path.join(pipelinesRoot, entry.name));
922
+ if (!origin) continue;
923
+ const key = `${origin.id}@${origin.version}`;
924
+ const flowIds = copies.get(key) || [];
925
+ flowIds.push(entry.name);
926
+ copies.set(key, flowIds);
927
+ }
928
+ return copies;
929
+ }
930
+
931
+ const PROJECT_FLOW_MARKETPLACE_FILENAME = path.join(".workspace", "agentflow", "marketplace.json");
932
+
933
+ function projectFlowMarketplaceId(ownerUserId, flowSource, flowId) {
934
+ return `project-flow:${String(ownerUserId || "").trim()}:${String(flowSource || "user").trim()}:${String(flowId || "").trim()}`;
935
+ }
936
+
937
+ function projectFlowMarketplaceMetadataPath(flowRoot) {
938
+ return path.join(path.resolve(flowRoot), PROJECT_FLOW_MARKETPLACE_FILENAME);
939
+ }
940
+
941
+ function readProjectFlowMarketplaceMetadata(flowRoot) {
942
+ try {
943
+ const parsed = JSON.parse(fs.readFileSync(projectFlowMarketplaceMetadataPath(flowRoot), "utf-8"));
944
+ return {
945
+ visibility: String(parsed?.visibility || "").trim() === "private" ? "private" : "public",
946
+ updatedAt: String(parsed?.updatedAt || "").trim(),
947
+ };
948
+ } catch {
949
+ return { visibility: "public", updatedAt: "" };
950
+ }
951
+ }
952
+
953
+ function writeProjectFlowMarketplaceMetadata(flowRoot, visibility) {
954
+ const filePath = projectFlowMarketplaceMetadataPath(flowRoot);
955
+ const value = {
956
+ version: 1,
957
+ visibility: String(visibility || "").trim() === "private" ? "private" : "public",
958
+ updatedAt: new Date().toISOString(),
959
+ };
960
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
961
+ const tempPath = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
962
+ fs.writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
963
+ fs.renameSync(tempPath, filePath);
964
+ return value;
965
+ }
966
+
967
+ function graphHasRunnableEntry(graph) {
968
+ return Object.values(graph?.instances || {}).some((instance) => (
969
+ instance?.definitionId === "workspace_run"
970
+ || instance?.definitionId === "workspace_scheduled_run"
971
+ ));
972
+ }
973
+
974
+ function projectFlowUpdatedAt(flowRoot, metadata = {}) {
975
+ if (metadata.updatedAt) return metadata.updatedAt;
976
+ try {
977
+ return fs.statSync(path.resolve(flowRoot)).mtime.toISOString();
978
+ } catch {
979
+ return "";
980
+ }
981
+ }
982
+
983
+ function listRunnableProjectMarketplaceFlows(workspaceRoot, userCtx = {}, scope = "all") {
984
+ const requestedUserId = String(userCtx.userId || "").trim();
985
+ const stats = marketplaceUsageStats(workspaceRoot);
986
+ const resources = [];
987
+ const appendFlow = (ownerId, flow, flowSource = "user", workspaceId = "") => {
988
+ if (!ownerId || (scope === "owned" && ownerId !== requestedUserId)) return;
989
+ if (flow.archived || !flow.path) return;
990
+ let stable;
991
+ let graph;
992
+ try {
993
+ stable = readWorkspaceStableRelease(flow.path, workspaceRoot);
994
+ graph = stable?.graph || readWorkspaceGraph(flow.path, workspaceRoot).graph;
995
+ } catch {
996
+ return;
997
+ }
998
+ if (!graphHasRunnableEntry(graph)) return;
999
+ const metadata = readProjectFlowMarketplaceMetadata(flow.path);
1000
+ const owned = ownerId === requestedUserId;
1001
+ if (scope !== "owned" && metadata.visibility === "private" && !owned && userCtx.isAdmin !== true) return;
1002
+ const id = projectFlowMarketplaceId(ownerId, flowSource, flow.id);
1003
+ const version = stable?.release?.id || `current-${workspaceDesignRevision(graph).slice(0, 12)}`;
1004
+ resources.push({
1005
+ resourceType: "flow",
1006
+ projectFlow: true,
1007
+ id,
1008
+ definitionId: flow.id,
1009
+ displayName: flow.id,
1010
+ description: flow.description || "",
1011
+ version,
1012
+ versionLabel: stable?.release?.id ? `Stable ${stable.release.id}` : "当前版本",
1013
+ ownerUserId: ownerId,
1014
+ liveOwnerUserId: ownerId,
1015
+ liveFlowId: flow.id,
1016
+ liveFlowSource: flowSource,
1017
+ liveWorkspaceId: workspaceId,
1018
+ visibility: metadata.visibility,
1019
+ nodeCount: Object.keys(graph?.instances || {}).length,
1020
+ edgeCount: Array.isArray(graph?.edges) ? graph.edges.length : 0,
1021
+ updatedAt: projectFlowUpdatedAt(flow.path, metadata),
1022
+ ...marketplaceStatsFor(stats, "project-flow", id, version, ownerId),
1023
+ _graph: graph,
1024
+ _flowRoot: flow.path,
1025
+ });
1026
+ };
1027
+ for (const ownerUserId of listAgentflowUserIds()) {
1028
+ const ownerId = String(ownerUserId || "").trim();
1029
+ for (const flow of listFlowsJson(workspaceRoot, { userId: ownerId })) {
1030
+ if ((flow.source || "user") !== "user" || flow.archived || !flow.path) continue;
1031
+ appendFlow(ownerId, flow, "user");
1032
+ }
1033
+ }
1034
+ for (const flow of listFlowsJson(workspaceRoot, { userId: "", includeWorkspaceFlows: true })) {
1035
+ if ((flow.source || "") !== "workspace" || flow.archived || !flow.path) continue;
1036
+ const collaboration = getWorkspaceCollaborationByFlow(flow.id, false);
1037
+ const ownerId = String(collaboration?.ownerId || "").trim();
1038
+ if (!ownerId) continue;
1039
+ appendFlow(ownerId, flow, "workspace", collaboration.id);
1040
+ }
1041
+ return resources;
1042
+ }
1043
+
1044
+ function publicProjectFlowMarketplaceResource(resource) {
1045
+ const { _graph, _flowRoot, ...publicResource } = resource;
1046
+ return publicResource;
1047
+ }
1048
+
1049
+ function marketplaceResourceMatches(item, queryText) {
1050
+ if (!queryText) return true;
1051
+ return [
1052
+ item.id,
1053
+ item.definitionId,
1054
+ item.displayName,
1055
+ item.description,
1056
+ item.ownerUserId,
1057
+ item.source,
1058
+ ...(Array.isArray(item.tags) ? item.tags : []),
1059
+ ].filter(Boolean).join("\n").toLowerCase().includes(queryText);
1060
+ }
1061
+
1062
+ function sortMarketplaceResources(items) {
1063
+ return items.sort((a, b) => (
1064
+ Number(b.useCount || 0) - Number(a.useCount || 0)
1065
+ || Number(b.installCount || 0) - Number(a.installCount || 0)
1066
+ || String(b.updatedAt || b.createdAt || "").localeCompare(String(a.updatedAt || a.createdAt || ""))
1067
+ || String(a.displayName || a.id).localeCompare(String(b.displayName || b.id))
1068
+ || String(b.version || "").localeCompare(String(a.version || ""), undefined, { numeric: true, sensitivity: "base" })
1069
+ ));
1070
+ }
1071
+
911
1072
  /**
912
1073
  * @param {import('http').IncomingMessage} req
913
1074
  * @param {import('http').ServerResponse} res
@@ -1020,26 +1181,112 @@ async function workspaceRoutes(req, res, ctx) {
1020
1181
  if (req.method === "GET" && url.pathname === "/api/marketplace/resources") {
1021
1182
  try {
1022
1183
  const kind = String(url.searchParams.get("kind") || "flow").trim();
1023
- const marketplaceScope = url.searchParams.get("scope") === "owned" ? "owned" : "all";
1184
+ const requestedScope = String(url.searchParams.get("scope") || "all").trim();
1185
+ const scope = ["all", "owned", "installed"].includes(requestedScope) ? requestedScope : "all";
1186
+ const marketplaceScope = scope === "owned" ? "owned" : "all";
1024
1187
  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
1188
  if (kind === "node") {
1033
- const nodes = listMarketplacePackages(root, { ...userCtx, marketplaceScope }).nodes.filter(matches);
1034
- json(res, 200, { kind, sort: "useCount", order: "desc", items: nodes });
1189
+ const marketplaceNodes = listMarketplacePackages(root, { ...userCtx, marketplaceScope }).nodes
1190
+ .map((node) => ({ ...node, resourceType: "node", installed: true }));
1191
+ let nodes = marketplaceNodes;
1192
+ if (scope === "installed") {
1193
+ const marketplaceByDefinition = new Map(marketplaceNodes.map((node) => [node.definitionId, node]));
1194
+ nodes = listNodesJson(root, "", "", { ...userCtx, marketplaceScope: "all" }).nodes.map((node) => {
1195
+ const marketplaceNode = marketplaceByDefinition.get(node.id);
1196
+ if (marketplaceNode) {
1197
+ return {
1198
+ ...marketplaceNode,
1199
+ definitionId: node.id,
1200
+ source: node.source || "marketplace",
1201
+ installed: true,
1202
+ };
1203
+ }
1204
+ return {
1205
+ id: node.packageId || node.id,
1206
+ version: node.version || "",
1207
+ definitionId: node.id,
1208
+ displayName: node.displayName || node.label || node.id,
1209
+ description: node.description || "",
1210
+ inputs: node.inputs || {},
1211
+ outputs: node.outputs || {},
1212
+ source: node.source || "project",
1213
+ resourceType: "node",
1214
+ localCatalog: true,
1215
+ installed: true,
1216
+ };
1217
+ });
1218
+ }
1219
+ nodes = sortMarketplaceResources(nodes.filter((item) => marketplaceResourceMatches(item, queryText)));
1220
+ json(res, 200, { kind, scope, sort: "useCount", order: "desc", items: nodes });
1035
1221
  return;
1036
1222
  }
1037
1223
  if (kind !== "flow") {
1038
1224
  json(res, 400, { error: "kind must be flow or node" });
1039
1225
  return;
1040
1226
  }
1041
- const flows = listMarketplaceFlows(root, { ...userCtx, marketplaceScope }).flows.filter(matches);
1042
- json(res, 200, { kind, sort: "useCount", order: "desc", items: flows });
1227
+ const installedCopies = installedMarketplaceFlowCopies(userCtx.userId);
1228
+ const flows = listMarketplaceFlows(root, { ...userCtx, marketplaceScope }).flows.map((flow) => {
1229
+ const installedFlowIds = installedCopies.get(`${flow.id}@${flow.version}`) || [];
1230
+ return {
1231
+ ...flow,
1232
+ resourceType: "flow",
1233
+ installed: installedFlowIds.length > 0,
1234
+ installedFlowIds,
1235
+ };
1236
+ });
1237
+ const projectFlows = listRunnableProjectMarketplaceFlows(root, userCtx, scope).map((flow) => {
1238
+ const installedFlowIds = installedCopies.get(`${flow.id}@${flow.version}`) || [];
1239
+ return publicProjectFlowMarketplaceResource({
1240
+ ...flow,
1241
+ installed: installedFlowIds.length > 0,
1242
+ installedFlowIds,
1243
+ });
1244
+ });
1245
+ const snippets = scope === "installed" ? [] : listMarketplaceFlowSnippets(root, { ...userCtx, marketplaceScope }).snippets
1246
+ .map((snippet) => ({ ...snippet, resourceType: "flow-snippet", installed: false }));
1247
+ const items = sortMarketplaceResources(
1248
+ [...projectFlows, ...flows, ...snippets]
1249
+ .filter((item) => scope !== "installed" || item.installed)
1250
+ .filter((item) => marketplaceResourceMatches(item, queryText)),
1251
+ );
1252
+ json(res, 200, { kind, scope, sort: "useCount", order: "desc", items });
1253
+ } catch (e) {
1254
+ json(res, 500, { error: (e && e.message) || String(e) });
1255
+ }
1256
+ return;
1257
+ }
1258
+
1259
+ if (req.method === "POST" && url.pathname === "/api/marketplace/flow-snippets/use") {
1260
+ let payload;
1261
+ try {
1262
+ payload = JSON.parse(await readBody(req));
1263
+ } catch {
1264
+ json(res, 400, { error: "Invalid JSON body" });
1265
+ return;
1266
+ }
1267
+ const id = String(payload?.id || "").trim();
1268
+ const version = String(payload?.version || "").trim();
1269
+ if (!id || !version) {
1270
+ json(res, 400, { error: "Missing flow snippet id or version" });
1271
+ return;
1272
+ }
1273
+ try {
1274
+ const accessible = listMarketplaceFlowSnippets(root, { ...userCtx, marketplaceScope: "all" }).snippets
1275
+ .some((snippet) => String(snippet.id) === id && String(snippet.version) === version);
1276
+ if (!accessible) {
1277
+ json(res, 404, { error: "Flow snippet not found" });
1278
+ return;
1279
+ }
1280
+ const eventToken = String(payload?.eventId || crypto.randomUUID()).trim().slice(0, 180);
1281
+ appendMarketplaceUsageEvent(root, {
1282
+ kind: "flow-snippet",
1283
+ id,
1284
+ version,
1285
+ action: "use",
1286
+ actorUserId: userCtx.userId || "",
1287
+ eventId: `use:flow-snippet:${id}@${version}:${userCtx.userId || "anonymous"}:${eventToken}`,
1288
+ });
1289
+ json(res, 200, { ok: true, id, version });
1043
1290
  } catch (e) {
1044
1291
  json(res, 500, { error: (e && e.message) || String(e) });
1045
1292
  }
@@ -1099,7 +1346,8 @@ async function workspaceRoutes(req, res, ctx) {
1099
1346
  }
1100
1347
  const id = String(payload?.id || "").trim();
1101
1348
  const version = String(payload?.version || "").trim();
1102
- const idCheck = validateUserPipelineId(String(payload?.flowId || id));
1349
+ const requestedProjectFlow = payload?.projectFlow === true;
1350
+ const idCheck = validateUserPipelineId(String(payload?.flowId || (requestedProjectFlow ? "" : id)));
1103
1351
  if (!id || !version || !idCheck.ok) {
1104
1352
  json(res, 400, { error: idCheck.error || "Missing marketplace flow id or version" });
1105
1353
  return;
@@ -1111,22 +1359,40 @@ async function workspaceRoutes(req, res, ctx) {
1111
1359
  return;
1112
1360
  }
1113
1361
  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;
1362
+ let sourceGraph;
1363
+ let originKind = "flow";
1364
+ if (requestedProjectFlow) {
1365
+ const source = listRunnableProjectMarketplaceFlows(root, userCtx, "all")
1366
+ .find((flow) => flow.id === id);
1367
+ if (!source) {
1368
+ json(res, 404, { error: "Project flow not found or is private" });
1369
+ return;
1370
+ }
1371
+ if (source.version !== version) {
1372
+ json(res, 409, { error: "该流程已有新版本,请刷新流程仓库后重试" });
1373
+ return;
1374
+ }
1375
+ sourceGraph = source._graph;
1376
+ originKind = "project-flow";
1377
+ } else {
1378
+ const source = readMarketplaceFlow(root, id, version, { ...userCtx, marketplaceScope: "all" });
1379
+ if (!source.ok) {
1380
+ json(res, 404, { error: source.error || "Marketplace flow not found" });
1381
+ return;
1382
+ }
1383
+ sourceGraph = source.graph;
1118
1384
  }
1119
1385
  fs.mkdirSync(targetDir, { recursive: true });
1120
- const graph = workspaceGraphWithScheduleMode(source.graph, "disabled");
1386
+ const graph = workspaceGraphWithScheduleMode(sourceGraph, "disabled");
1121
1387
  writeWorkspaceGraph(targetDir, graph, root);
1122
- writeMarketplaceFlowOrigin(targetDir, { id, version });
1388
+ writeMarketplaceFlowOrigin(targetDir, { kind: originKind, id, version });
1123
1389
  appendMarketplaceUsageEvent(root, {
1124
- kind: "flow",
1390
+ kind: originKind,
1125
1391
  id,
1126
1392
  version,
1127
1393
  action: "install",
1128
1394
  actorUserId: userCtx.userId || "",
1129
- eventId: `install:flow:${id}@${version}:${userCtx.userId || "anonymous"}`,
1395
+ eventId: `install:${originKind}:${id}@${version}:${userCtx.userId || "anonymous"}`,
1130
1396
  });
1131
1397
  json(res, 201, {
1132
1398
  ok: true,
@@ -1154,6 +1420,18 @@ async function workspaceRoutes(req, res, ctx) {
1154
1420
  return;
1155
1421
  }
1156
1422
  try {
1423
+ if (String(payload?.kind || "") === "project-flow") {
1424
+ const id = String(payload?.id || "").trim();
1425
+ const source = listRunnableProjectMarketplaceFlows(root, { ...userCtx, isAdmin: false }, "owned")
1426
+ .find((flow) => flow.id === id);
1427
+ if (!source) {
1428
+ json(res, 403, { error: "Only the project owner can change its visibility" });
1429
+ return;
1430
+ }
1431
+ const metadata = writeProjectFlowMarketplaceMetadata(source._flowRoot, payload?.visibility);
1432
+ json(res, 200, { ok: true, kind: "project-flow", id, version: source.version, ...metadata });
1433
+ return;
1434
+ }
1157
1435
  const result = setMarketplaceVisibility(
1158
1436
  root,
1159
1437
  String(payload?.kind || ""),
@@ -1590,7 +1868,7 @@ async function workspaceRoutes(req, res, ctx) {
1590
1868
  const written = writeWorkspaceGraph(flowDir, graph, root);
1591
1869
  writeWorkspaceDraftMetadata(flowDir, metadata);
1592
1870
  const persisted = readWorkspaceGraph(flowDir, root).graph;
1593
- const baseUrl = `${url.protocol}//${url.host}`;
1871
+ const baseUrl = requestPublicBaseUrl(req);
1594
1872
  json(res, 200, {
1595
1873
  ok: true,
1596
1874
  flowId,
@@ -1663,7 +1941,7 @@ async function workspaceRoutes(req, res, ctx) {
1663
1941
  if (scoped.error) throw new Error(scoped.error);
1664
1942
  const persisted = readWorkspaceGraph(scoped.root, root).graph;
1665
1943
  const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, persisted, authUser, userCtx);
1666
- const baseUrl = `${url.protocol}//${url.host}`;
1944
+ const baseUrl = requestPublicBaseUrl(req);
1667
1945
  json(res, 200, {
1668
1946
  ok: true,
1669
1947
  success: true,
@@ -1752,7 +2030,7 @@ async function workspaceRoutes(req, res, ctx) {
1752
2030
  json(res, 500, { error: (e && e.message) || String(e) });
1753
2031
  return;
1754
2032
  }
1755
- const baseUrl = `${url.protocol}//${url.host}`;
2033
+ const baseUrl = requestPublicBaseUrl(req);
1756
2034
  const workspaceUrl = `${baseUrl}/workspace?flowId=${encodeURIComponent(flowId)}&flowSource=workspace&archived=1`;
1757
2035
  json(res, 200, { ok: true, flowId, flowSource: "workspace", archived: true, preview: true, expiresAt: metadata.expiresAt, url: workspaceUrl });
1758
2036
  return;
@@ -1864,6 +2142,7 @@ async function workspaceRoutes(req, res, ctx) {
1864
2142
  ownerUserId: scoped.ownerUserId,
1865
2143
  ownerUsername: scoped.ownerUsername,
1866
2144
  } : null,
2145
+ release: readWorkspaceReleaseStatus(scoped.root, root, hydratedGraph),
1867
2146
  workspaceSchedules: listWorkspaceScheduleStatusesForFlow(scopedUserCtx, scoped.flowSource || "user", scoped.flowId || ""),
1868
2147
  });
1869
2148
  } catch (e) {
@@ -1878,6 +2157,126 @@ async function workspaceRoutes(req, res, ctx) {
1878
2157
  return;
1879
2158
  }
1880
2159
 
2160
+ if (req.method === "GET" && url.pathname === "/api/workspace/releases") {
2161
+ try {
2162
+ const scoped = resolveWorkspaceScopeRoot(root, {
2163
+ flowId: url.searchParams.get("flowId") || "",
2164
+ flowSource: url.searchParams.get("flowSource") || "user",
2165
+ workspaceId: url.searchParams.get("workspaceId") || "",
2166
+ adminOwnerId: url.searchParams.get("adminOwnerId") || "",
2167
+ archived: url.searchParams.get("archived") === "1",
2168
+ }, userCtx);
2169
+ if (scoped.error) {
2170
+ json(res, scoped.status || 400, { error: scoped.error });
2171
+ return;
2172
+ }
2173
+ json(res, 200, { ok: true, release: readWorkspaceReleaseStatus(scoped.root, root) });
2174
+ } catch (e) {
2175
+ json(res, 500, { error: (e && e.message) || String(e) });
2176
+ }
2177
+ return;
2178
+ }
2179
+
2180
+ if (req.method === "POST" && url.pathname === "/api/workspace/releases/publish") {
2181
+ let payload;
2182
+ try {
2183
+ payload = JSON.parse(await readBody(req));
2184
+ } catch {
2185
+ json(res, 400, { error: "Invalid JSON body" });
2186
+ return;
2187
+ }
2188
+ try {
2189
+ const scoped = resolveWorkspaceScopeRoot(root, {
2190
+ flowId: payload.flowId || "",
2191
+ flowSource: payload.flowSource || "user",
2192
+ workspaceId: payload.workspaceId || "",
2193
+ archived: payload.archived === true || payload.flowArchived === true,
2194
+ }, userCtx);
2195
+ if (scoped.error) {
2196
+ json(res, scoped.status || 400, { error: scoped.error });
2197
+ return;
2198
+ }
2199
+ if (
2200
+ scoped.archived
2201
+ || scoped.draft === true
2202
+ || isReadonlyBuiltinFlowSource(scoped.flowSource)
2203
+ || scoped.collaborationAccess?.writable === false
2204
+ ) {
2205
+ json(res, 403, { error: "Workspace release publish permission denied" });
2206
+ return;
2207
+ }
2208
+ const result = publishWorkspaceRelease(scoped.root, root, {
2209
+ expectedRevision: payload.expectedRevision || "",
2210
+ createdBy: userCtx.userId || authUser?.userId || "",
2211
+ notes: payload.notes || "",
2212
+ });
2213
+ if (result.error) {
2214
+ json(res, result.conflict ? 409 : 400, result);
2215
+ return;
2216
+ }
2217
+ const graph = readWorkspaceGraph(scoped.root, root).graph;
2218
+ const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx);
2219
+ broadcastWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
2220
+ type: "release.published",
2221
+ releaseId: result.release.id,
2222
+ revision: result.release.designRevision,
2223
+ actorId: userCtx.userId || "",
2224
+ });
2225
+ json(res, 200, { ...result, workspaceSchedules });
2226
+ } catch (e) {
2227
+ json(res, 500, { error: (e && e.message) || String(e) });
2228
+ }
2229
+ return;
2230
+ }
2231
+
2232
+ if (req.method === "POST" && url.pathname === "/api/workspace/releases/rollback") {
2233
+ let payload;
2234
+ try {
2235
+ payload = JSON.parse(await readBody(req));
2236
+ } catch {
2237
+ json(res, 400, { error: "Invalid JSON body" });
2238
+ return;
2239
+ }
2240
+ try {
2241
+ const scoped = resolveWorkspaceScopeRoot(root, {
2242
+ flowId: payload.flowId || "",
2243
+ flowSource: payload.flowSource || "user",
2244
+ workspaceId: payload.workspaceId || "",
2245
+ archived: payload.archived === true || payload.flowArchived === true,
2246
+ }, userCtx);
2247
+ if (scoped.error) {
2248
+ json(res, scoped.status || 400, { error: scoped.error });
2249
+ return;
2250
+ }
2251
+ if (
2252
+ scoped.archived
2253
+ || scoped.draft === true
2254
+ || isReadonlyBuiltinFlowSource(scoped.flowSource)
2255
+ || scoped.collaborationAccess?.writable === false
2256
+ ) {
2257
+ json(res, 403, { error: "Workspace release rollback permission denied" });
2258
+ return;
2259
+ }
2260
+ const result = rollbackWorkspaceRelease(scoped.root, payload.releaseId || "", root);
2261
+ if (result.error) {
2262
+ json(res, 404, result);
2263
+ return;
2264
+ }
2265
+ const graph = readWorkspaceGraph(scoped.root, root).graph;
2266
+ const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx);
2267
+ broadcastWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
2268
+ type: "release.rollback",
2269
+ releaseId: result.release.id,
2270
+ revision: result.release.designRevision,
2271
+ actorId: userCtx.userId || "",
2272
+ });
2273
+ json(res, 200, { ...result, workspaceSchedules });
2274
+ } catch (e) {
2275
+ json(res, 500, { error: (e && e.message) || String(e) });
2276
+ }
2277
+ return;
2278
+ }
2279
+
1881
2280
  if (req.method === "POST" && url.pathname === "/api/workspace/graph") {
1882
2281
  let payload;
1883
2282
  try {
@@ -2010,6 +2409,7 @@ async function workspaceRoutes(req, res, ctx) {
2010
2409
  designRevision: revision,
2011
2410
  runtimeRevision,
2012
2411
  merged,
2412
+ release: readWorkspaceReleaseStatus(scoped.root, root, committed.graph),
2013
2413
  workspaceSchedules,
2014
2414
  });
2015
2415
  } 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 || ""),