@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.
- package/bin/lib/marketplace-usage.mjs +4 -3
- package/bin/lib/ui-server.mjs +63 -9
- package/bin/lib/workspace-routes.mjs +428 -28
- package/bin/lib/workspace-run-logs.mjs +2 -0
- package/bin/lib/workspace-server.mjs +364 -36
- package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-DmhIgoD7.js → WorkflowAssistantThread-pVdrZ-Rl.js} +1 -1
- package/builtin/web-ui/dist/assets/index-BQeq5tdj.css +1 -0
- package/builtin/web-ui/dist/assets/index-Czutb6ai.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
|
@@ -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
|
|
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
|
|
1034
|
-
|
|
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
|
|
1042
|
-
|
|
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
|
|
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
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
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(
|
|
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:
|
|
1390
|
+
kind: originKind,
|
|
1125
1391
|
id,
|
|
1126
1392
|
version,
|
|
1127
1393
|
action: "install",
|
|
1128
1394
|
actorUserId: userCtx.userId || "",
|
|
1129
|
-
eventId: `install
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 || ""),
|