@fieldwangai/agentflow 0.1.164 → 0.1.166
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/agent-runners.mjs +105 -13
- package/bin/lib/cursor-api-key-pool.mjs +140 -31
- package/bin/lib/cursor-model-catalog.mjs +152 -0
- package/bin/lib/marketplace-usage.mjs +4 -3
- package/bin/lib/workspace-routes.mjs +677 -21
- package/bin/lib/workspace-server.mjs +30 -14
- package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-CffIx5BY.js → WorkflowAssistantThread-DKdBGu3q.js} +1 -1
- package/builtin/web-ui/dist/assets/{index-CDItaRfX.css → index-B7YuvFR2.css} +1 -1
- package/builtin/web-ui/dist/assets/index-BUljbvrW.js +877 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +2 -2
- package/skills/agentflow-cli/runtime/bin/lib/skill-runtime.mjs +242 -137
- package/skills/agentflow-cli/runtime/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-CWIcfWHO.js +0 -873
|
@@ -11,7 +11,7 @@
|
|
|
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";
|
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
} from "./marketplace.mjs";
|
|
31
31
|
import {
|
|
32
32
|
appendMarketplaceUsageEvent,
|
|
33
|
+
marketplaceStatsFor,
|
|
34
|
+
marketplaceUsageStats,
|
|
33
35
|
marketplaceResourcesForRun,
|
|
34
36
|
readMarketplaceFlowOrigin,
|
|
35
37
|
writeMarketplaceFlowOrigin,
|
|
@@ -38,18 +40,18 @@ import { NODE_PACKAGE_ENTRY, nodePackageExportsRun, readNodePackageManifest } fr
|
|
|
38
40
|
import { inspectNodePackageDirectory } from "./node-package-archive.mjs";
|
|
39
41
|
import { json, readBody } from "./http-util.mjs";
|
|
40
42
|
import { log } from "./log.mjs";
|
|
41
|
-
import { PACKAGE_ROOT, PIPELINES_DIR, getAgentflowUserDataRoot, getUserPipelinesRoot } from "./paths.mjs";
|
|
43
|
+
import { PACKAGE_ROOT, PIPELINES_DIR, getAgentflowUserDataRoot, getUserPipelinesRoot, listAgentflowUserIds } from "./paths.mjs";
|
|
42
44
|
import { validateUserPipelineId } from "./flow-write.mjs";
|
|
43
45
|
import { runLedgerId } from "./run-ledger.mjs";
|
|
44
46
|
import { getTeamById, getTeamForUser } from "./teams.mjs";
|
|
45
47
|
import { readMergedEnvObject, runtimeEnvForUser } from "./user-env.mjs";
|
|
46
|
-
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";
|
|
47
49
|
import { WorkspaceFlowParseError } from "./workspace-flow-store.mjs";
|
|
48
50
|
import { mergeWorkspaceGraphs, workspaceDesignRevision, workspaceRuntimeRevision } from "./workspace-graph-merge.mjs";
|
|
49
51
|
import { DEFAULT_WORKSPACE_PREVIEW_TTL_MS, createWorkspacePreviewId, normalizeWorkspacePreviewTtlMs, readWorkspacePreviewMetadata, workspaceSharedPreviewFlowDir, writeWorkspacePreviewMetadata } from "./workspace-preview.mjs";
|
|
50
52
|
import { DEFAULT_WORKSPACE_DRAFT_TTL_MS, createWorkspaceDraftId, normalizeWorkspaceDraftTtlMs, readWorkspaceDraftMetadata, workspaceDraftFlowDir, writeWorkspaceDraftMetadata } from "./workspace-draft.mjs";
|
|
51
53
|
import { appendWorkspaceRunLogEvent, createWorkspaceRunLogSession, finishWorkspaceRunLogSession, listWorkspaceRunLogs, readWorkspaceRunLogEvents } from "./workspace-run-logs.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";
|
|
54
|
+
import { activeWorkspaceRuns, appendWorkspaceRunFinished, appendWorkspaceRunStarted, cleanupWorkspaceRunResources, hydrateWorkspaceGraphForRuntime, isReadonlyBuiltinFlowSource, isTransientAgentNetworkError, isValidFlowSourceRead, isWorkspaceRunAbortError, listWorkspaceScheduleStatusesForFlow, mergeWorkspacePersistentNodeRefs, mergeWorkspaceRunGraph, normalizeWorkspaceEntry, normalizeWorkspaceScheduledRunConfig, publishWorkspaceRelease, readWorkspaceConversations, readWorkspaceFiles, readWorkspaceGraph, readWorkspaceReleaseStatus, readWorkspaceRunUsageRecords, 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";
|
|
53
55
|
import { splitWorkspaceGraph, WORKSPACE_STATE_FILENAME } from "./workspace-state.mjs";
|
|
54
56
|
import { getWorkspaceTree } from "./workspace-tree.mjs";
|
|
55
57
|
import busboy from "busboy";
|
|
@@ -246,6 +248,170 @@ function missingWorkspaceGraphNodePackages(workspaceRoot, scoped, graph, userCtx
|
|
|
246
248
|
return [...missing].sort();
|
|
247
249
|
}
|
|
248
250
|
|
|
251
|
+
const NODE_REVIEW_MAX_FILES = 100;
|
|
252
|
+
const NODE_REVIEW_MAX_FILE_BYTES = 256 * 1024;
|
|
253
|
+
const NODE_REVIEW_MAX_TOTAL_BYTES = 2 * 1024 * 1024;
|
|
254
|
+
const NODE_REVIEW_TEXT_EXTENSIONS = new Set([
|
|
255
|
+
".cjs", ".css", ".env", ".go", ".html", ".ini", ".java", ".js", ".json", ".jsx", ".kt",
|
|
256
|
+
".md", ".mdx", ".mjs", ".py", ".rs", ".scss", ".sh", ".sql", ".toml", ".ts", ".tsx",
|
|
257
|
+
".txt", ".xml", ".yaml", ".yml",
|
|
258
|
+
]);
|
|
259
|
+
|
|
260
|
+
function workspaceNodeReviewLanguage(filePath = "") {
|
|
261
|
+
const extension = path.extname(String(filePath || "")).toLowerCase().replace(/^\./, "");
|
|
262
|
+
const aliases = { cjs: "javascript", js: "javascript", jsx: "javascript", mjs: "javascript", py: "python", sh: "shell", ts: "typescript", tsx: "typescript", yml: "yaml" };
|
|
263
|
+
return aliases[extension] || extension || "text";
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function workspaceNodeReviewSource({ sourcePath = "", title = "", kind = "file", content = "" } = {}) {
|
|
267
|
+
const text = String(content || "").replace(/\r\n/g, "\n");
|
|
268
|
+
const reviewPath = String(sourcePath || title || "source.txt").replace(/\\/g, "/");
|
|
269
|
+
return {
|
|
270
|
+
path: reviewPath,
|
|
271
|
+
title: String(title || path.basename(reviewPath) || reviewPath),
|
|
272
|
+
kind,
|
|
273
|
+
language: workspaceNodeReviewLanguage(reviewPath),
|
|
274
|
+
size: Buffer.byteLength(text),
|
|
275
|
+
sha256: crypto.createHash("sha256").update(text).digest("hex"),
|
|
276
|
+
content: text,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function workspaceNodeReviewFlowFile(flowRoot, fileRef, kind) {
|
|
281
|
+
const ref = String(fileRef || "").trim();
|
|
282
|
+
if (!ref) return null;
|
|
283
|
+
try {
|
|
284
|
+
const { abs, rel } = resolveWorkspaceFilePath(flowRoot, ref);
|
|
285
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
|
|
286
|
+
return { error: `找不到 ${ref}` };
|
|
287
|
+
}
|
|
288
|
+
const stat = fs.statSync(abs);
|
|
289
|
+
if (stat.size > NODE_REVIEW_MAX_FILE_BYTES) {
|
|
290
|
+
return { error: `${ref} 超过可预览大小限制` };
|
|
291
|
+
}
|
|
292
|
+
return workspaceNodeReviewSource({ sourcePath: rel, title: rel, kind, content: fs.readFileSync(abs, "utf-8") });
|
|
293
|
+
} catch (error) {
|
|
294
|
+
return { error: `${ref}: ${(error && error.message) || String(error)}` };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function workspaceNodeReviewPackageFiles(packageDir) {
|
|
299
|
+
const root = path.resolve(packageDir);
|
|
300
|
+
const candidates = [];
|
|
301
|
+
const walk = (dir) => {
|
|
302
|
+
if (candidates.length >= NODE_REVIEW_MAX_FILES) return;
|
|
303
|
+
let entries = [];
|
|
304
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
305
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
306
|
+
for (const entry of entries) {
|
|
307
|
+
if (candidates.length >= NODE_REVIEW_MAX_FILES) break;
|
|
308
|
+
if ([".git", "node_modules"].includes(entry.name)) continue;
|
|
309
|
+
const abs = path.join(dir, entry.name);
|
|
310
|
+
if (entry.isDirectory()) walk(abs);
|
|
311
|
+
else if (entry.isFile() && NODE_REVIEW_TEXT_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) candidates.push(abs);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
walk(root);
|
|
315
|
+
candidates.sort((left, right) => {
|
|
316
|
+
const leftRel = path.relative(root, left).replace(/\\/g, "/");
|
|
317
|
+
const rightRel = path.relative(root, right).replace(/\\/g, "/");
|
|
318
|
+
if (leftRel === NODE_PACKAGE_ENTRY) return -1;
|
|
319
|
+
if (rightRel === NODE_PACKAGE_ENTRY) return 1;
|
|
320
|
+
return leftRel.localeCompare(rightRel);
|
|
321
|
+
});
|
|
322
|
+
const sources = [];
|
|
323
|
+
let totalBytes = 0;
|
|
324
|
+
for (const abs of candidates) {
|
|
325
|
+
let stat;
|
|
326
|
+
try { stat = fs.statSync(abs); } catch { continue; }
|
|
327
|
+
if (!stat.isFile() || stat.size > NODE_REVIEW_MAX_FILE_BYTES || totalBytes + stat.size > NODE_REVIEW_MAX_TOTAL_BYTES) continue;
|
|
328
|
+
const rel = path.relative(root, abs).replace(/\\/g, "/");
|
|
329
|
+
sources.push(workspaceNodeReviewSource({ sourcePath: rel, title: rel, kind: "package", content: fs.readFileSync(abs, "utf-8") }));
|
|
330
|
+
totalBytes += stat.size;
|
|
331
|
+
}
|
|
332
|
+
return sources;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function workspaceNodeReviewSnapshot(workspaceRoot, flowRoot, graph, nodeId, userCtx = {}, label = "Draft") {
|
|
336
|
+
const instance = graph?.instances?.[nodeId];
|
|
337
|
+
if (!instance) return null;
|
|
338
|
+
const definitionId = String(instance.definitionId || nodeId);
|
|
339
|
+
const marketplaceRef = String(instance.marketplaceRef || definitionId).trim();
|
|
340
|
+
const sources = [];
|
|
341
|
+
const errors = [];
|
|
342
|
+
let packageInfo = null;
|
|
343
|
+
|
|
344
|
+
if (marketplaceRef.startsWith("marketplace:")) {
|
|
345
|
+
const resolved = resolveMarketplaceNodePackage(
|
|
346
|
+
workspaceRoot,
|
|
347
|
+
flowRoot,
|
|
348
|
+
marketplaceRef,
|
|
349
|
+
graph,
|
|
350
|
+
{ ...userCtx, marketplaceScope: "all" },
|
|
351
|
+
);
|
|
352
|
+
if (!resolved) {
|
|
353
|
+
errors.push(`无法解析节点包 ${marketplaceRef}`);
|
|
354
|
+
} else {
|
|
355
|
+
const inspected = inspectNodePackageDirectory(resolved.packageDir, { allowLegacyManifest: true });
|
|
356
|
+
sources.push(...workspaceNodeReviewPackageFiles(resolved.packageDir));
|
|
357
|
+
packageInfo = {
|
|
358
|
+
id: resolved.id,
|
|
359
|
+
version: resolved.version,
|
|
360
|
+
definitionId: resolved.resolvedDefinitionId || marketplaceRef,
|
|
361
|
+
source: resolved.source || "marketplace",
|
|
362
|
+
contentSha256: inspected.ok ? inspected.contentSha256 : String(resolved.contentSha256 || ""),
|
|
363
|
+
};
|
|
364
|
+
if (!sources.length) errors.push(`节点包 ${marketplaceRef} 没有可预览的文本文件`);
|
|
365
|
+
}
|
|
366
|
+
} else if (String(instance.script || "").trim()) {
|
|
367
|
+
sources.push(workspaceNodeReviewSource({
|
|
368
|
+
sourcePath: `nodes/${nodeId}/inline-script.mjs`,
|
|
369
|
+
title: "内联脚本",
|
|
370
|
+
kind: "inline",
|
|
371
|
+
content: instance.script,
|
|
372
|
+
}));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
for (const [ref, kind] of [[instance.scriptRef, "script"], [instance.implementationRef, "implementation"]]) {
|
|
376
|
+
const source = workspaceNodeReviewFlowFile(flowRoot, ref, kind);
|
|
377
|
+
if (source?.error) errors.push(source.error);
|
|
378
|
+
else if (source) sources.push(source);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const body = String(instance.body || "");
|
|
382
|
+
if (body.trim()) {
|
|
383
|
+
const looksJson = /^[\[{]/.test(body.trim());
|
|
384
|
+
sources.push(workspaceNodeReviewSource({
|
|
385
|
+
sourcePath: `nodes/${nodeId}/${looksJson ? "configuration.json" : "prompt.md"}`,
|
|
386
|
+
title: looksJson ? "节点配置" : "Prompt / 指令",
|
|
387
|
+
kind: looksJson ? "configuration" : "prompt",
|
|
388
|
+
content: body,
|
|
389
|
+
}));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const uniqueSources = [];
|
|
393
|
+
const seen = new Set();
|
|
394
|
+
for (const source of sources) {
|
|
395
|
+
const key = `${source.kind}\u0000${source.path}\u0000${source.sha256}`;
|
|
396
|
+
if (seen.has(key)) continue;
|
|
397
|
+
seen.add(key);
|
|
398
|
+
uniqueSources.push(source);
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
label,
|
|
402
|
+
nodeId,
|
|
403
|
+
definitionId,
|
|
404
|
+
model: String(instance.model || ""),
|
|
405
|
+
role: String(instance.role || "normal"),
|
|
406
|
+
implementationMode: String(instance.implementationMode || ""),
|
|
407
|
+
marketplaceRef: marketplaceRef.startsWith("marketplace:") ? marketplaceRef : "",
|
|
408
|
+
package: packageInfo,
|
|
409
|
+
sources: uniqueSources,
|
|
410
|
+
errors,
|
|
411
|
+
reviewable: uniqueSources.length > 0,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
249
415
|
const NODE_STUDIO_DRAFTS_DIRNAME = "node-studio/drafts";
|
|
250
416
|
|
|
251
417
|
function writeUserWorkspaces(userCtx = {}, entries = []) {
|
|
@@ -926,6 +1092,187 @@ function installedMarketplaceFlowCopies(userId) {
|
|
|
926
1092
|
return copies;
|
|
927
1093
|
}
|
|
928
1094
|
|
|
1095
|
+
const PROJECT_FLOW_MARKETPLACE_FILENAME = path.join(".workspace", "agentflow", "marketplace.json");
|
|
1096
|
+
|
|
1097
|
+
function projectFlowMarketplaceId(ownerUserId, flowSource, flowId) {
|
|
1098
|
+
return `project-flow:${String(ownerUserId || "").trim()}:${String(flowSource || "user").trim()}:${String(flowId || "").trim()}`;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function projectFlowMarketplaceMetadataPath(flowRoot) {
|
|
1102
|
+
return path.join(path.resolve(flowRoot), PROJECT_FLOW_MARKETPLACE_FILENAME);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function readProjectFlowMarketplaceMetadata(flowRoot) {
|
|
1106
|
+
try {
|
|
1107
|
+
const parsed = JSON.parse(fs.readFileSync(projectFlowMarketplaceMetadataPath(flowRoot), "utf-8"));
|
|
1108
|
+
return {
|
|
1109
|
+
visibility: String(parsed?.visibility || "").trim() === "private" ? "private" : "public",
|
|
1110
|
+
updatedAt: String(parsed?.updatedAt || "").trim(),
|
|
1111
|
+
};
|
|
1112
|
+
} catch {
|
|
1113
|
+
return { visibility: "public", updatedAt: "" };
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function writeProjectFlowMarketplaceMetadata(flowRoot, visibility) {
|
|
1118
|
+
const filePath = projectFlowMarketplaceMetadataPath(flowRoot);
|
|
1119
|
+
const value = {
|
|
1120
|
+
version: 1,
|
|
1121
|
+
visibility: String(visibility || "").trim() === "private" ? "private" : "public",
|
|
1122
|
+
updatedAt: new Date().toISOString(),
|
|
1123
|
+
};
|
|
1124
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
1125
|
+
const tempPath = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
1126
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
|
|
1127
|
+
fs.renameSync(tempPath, filePath);
|
|
1128
|
+
return value;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function runnableProjectFlowEntries(graph, scopedRoot = "") {
|
|
1132
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
1133
|
+
const entries = Object.entries(instances).filter(([, instance]) => (
|
|
1134
|
+
instance?.definitionId === "workspace_run"
|
|
1135
|
+
|| instance?.definitionId === "workspace_scheduled_run"
|
|
1136
|
+
));
|
|
1137
|
+
return entries.flatMap(([entryId, entry]) => {
|
|
1138
|
+
let plan;
|
|
1139
|
+
try {
|
|
1140
|
+
plan = workspaceRunPlan(graph, entryId, scopedRoot, { ignoreCache: true });
|
|
1141
|
+
} catch {
|
|
1142
|
+
return [];
|
|
1143
|
+
}
|
|
1144
|
+
const executedNodeIds = Array.from(new Set((plan.order || []).map((id) => String(id || "").trim()).filter(Boolean)));
|
|
1145
|
+
if (executedNodeIds.length === 0) return [];
|
|
1146
|
+
const includedNodeIds = new Set([entryId, ...executedNodeIds]);
|
|
1147
|
+
const positions = graph?.ui?.nodePositions && typeof graph.ui.nodePositions === "object"
|
|
1148
|
+
? Object.fromEntries(Object.entries(graph.ui.nodePositions).filter(([id]) => includedNodeIds.has(id)))
|
|
1149
|
+
: {};
|
|
1150
|
+
const sizes = graph?.ui?.nodeSizes && typeof graph.ui.nodeSizes === "object"
|
|
1151
|
+
? Object.fromEntries(Object.entries(graph.ui.nodeSizes).filter(([id]) => includedNodeIds.has(id)))
|
|
1152
|
+
: {};
|
|
1153
|
+
return [{
|
|
1154
|
+
entryId,
|
|
1155
|
+
entry,
|
|
1156
|
+
runMode: entry.definitionId === "workspace_scheduled_run" ? "scheduled" : "manual",
|
|
1157
|
+
graph: {
|
|
1158
|
+
...graph,
|
|
1159
|
+
instances: Object.fromEntries(Object.entries(instances).filter(([id]) => includedNodeIds.has(id))),
|
|
1160
|
+
edges: (Array.isArray(graph?.edges) ? graph.edges : []).filter((edge) => (
|
|
1161
|
+
includedNodeIds.has(String(edge?.source || ""))
|
|
1162
|
+
&& includedNodeIds.has(String(edge?.target || ""))
|
|
1163
|
+
)),
|
|
1164
|
+
ui: {
|
|
1165
|
+
...(graph?.ui || {}),
|
|
1166
|
+
nodePositions: positions,
|
|
1167
|
+
nodeSizes: sizes,
|
|
1168
|
+
},
|
|
1169
|
+
},
|
|
1170
|
+
}];
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function projectFlowUpdatedAt(flowRoot, metadata = {}) {
|
|
1175
|
+
if (metadata.updatedAt) return metadata.updatedAt;
|
|
1176
|
+
try {
|
|
1177
|
+
return fs.statSync(path.resolve(flowRoot)).mtime.toISOString();
|
|
1178
|
+
} catch {
|
|
1179
|
+
return "";
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function listRunnableProjectMarketplaceFlows(workspaceRoot, userCtx = {}, scope = "all") {
|
|
1184
|
+
const requestedUserId = String(userCtx.userId || "").trim();
|
|
1185
|
+
const stats = marketplaceUsageStats(workspaceRoot);
|
|
1186
|
+
const runUsage = readWorkspaceRunUsageRecords();
|
|
1187
|
+
const resources = [];
|
|
1188
|
+
const appendFlow = (ownerId, flow, flowSource = "user", workspaceId = "") => {
|
|
1189
|
+
if (!ownerId || (scope === "owned" && ownerId !== requestedUserId)) return;
|
|
1190
|
+
if (flow.archived || !flow.path) return;
|
|
1191
|
+
let stable;
|
|
1192
|
+
let graph;
|
|
1193
|
+
try {
|
|
1194
|
+
stable = readWorkspaceStableRelease(flow.path, workspaceRoot);
|
|
1195
|
+
graph = stable?.graph || readWorkspaceGraph(flow.path, workspaceRoot).graph;
|
|
1196
|
+
} catch {
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
const runnableEntries = runnableProjectFlowEntries(graph, flow.path);
|
|
1200
|
+
if (runnableEntries.length === 0) return;
|
|
1201
|
+
const metadata = readProjectFlowMarketplaceMetadata(flow.path);
|
|
1202
|
+
const owned = ownerId === requestedUserId;
|
|
1203
|
+
if (scope !== "owned" && metadata.visibility === "private" && !owned && userCtx.isAdmin !== true) return;
|
|
1204
|
+
for (const runnable of runnableEntries) {
|
|
1205
|
+
const baseId = projectFlowMarketplaceId(ownerId, flowSource, flow.id);
|
|
1206
|
+
const id = runnableEntries.length === 1 ? baseId : `${baseId}:${runnable.entryId}`;
|
|
1207
|
+
const version = stable?.release?.id || `current-${workspaceDesignRevision(graph).slice(0, 12)}`;
|
|
1208
|
+
const rawEntryLabel = String(runnable.entry?.label || "").trim();
|
|
1209
|
+
const genericLabel = ["", "Run", "Scheduled Run", "运行", "定时运行"].includes(rawEntryLabel);
|
|
1210
|
+
const directRuns = runUsage.filter((run) => (
|
|
1211
|
+
run.status === "success"
|
|
1212
|
+
&& run.flowId === flow.id
|
|
1213
|
+
&& (run.flowSource || "user") === flowSource
|
|
1214
|
+
&& (flowSource !== "user" || run.userId === ownerId)
|
|
1215
|
+
&& (run.runNodeId ? run.runNodeId === runnable.entryId : runnableEntries.length === 1)
|
|
1216
|
+
));
|
|
1217
|
+
const telemetry = marketplaceStatsFor(stats, "project-flow", id, version);
|
|
1218
|
+
const directLastUsedAt = directRuns.reduce((latest, run) => {
|
|
1219
|
+
const value = new Date(Number(run.endedAt || run.at || 0)).toISOString();
|
|
1220
|
+
return !latest || value > latest ? value : latest;
|
|
1221
|
+
}, "");
|
|
1222
|
+
resources.push({
|
|
1223
|
+
resourceType: "flow",
|
|
1224
|
+
projectFlow: true,
|
|
1225
|
+
id,
|
|
1226
|
+
definitionId: `${flow.id}/${runnable.entryId}`,
|
|
1227
|
+
displayName: runnableEntries.length === 1
|
|
1228
|
+
? flow.id
|
|
1229
|
+
: `${flow.id} · ${genericLabel ? runnable.entryId : rawEntryLabel}`,
|
|
1230
|
+
description: flow.description || "",
|
|
1231
|
+
version,
|
|
1232
|
+
versionLabel: stable?.release?.id ? `Stable ${stable.release.id}` : "当前版本",
|
|
1233
|
+
runMode: runnable.runMode,
|
|
1234
|
+
runModeLabel: runnable.runMode === "scheduled" ? "定时运行" : "手动运行",
|
|
1235
|
+
ownerUserId: ownerId,
|
|
1236
|
+
liveOwnerUserId: ownerId,
|
|
1237
|
+
liveFlowId: flow.id,
|
|
1238
|
+
liveFlowSource: flowSource,
|
|
1239
|
+
liveWorkspaceId: workspaceId,
|
|
1240
|
+
liveEntryId: runnable.entryId,
|
|
1241
|
+
installFlowId: runnableEntries.length === 1 ? flow.id : `${flow.id}-${runnable.entryId}`,
|
|
1242
|
+
visibility: metadata.visibility,
|
|
1243
|
+
nodeCount: Object.keys(runnable.graph?.instances || {}).length,
|
|
1244
|
+
edgeCount: Array.isArray(runnable.graph?.edges) ? runnable.graph.edges.length : 0,
|
|
1245
|
+
updatedAt: projectFlowUpdatedAt(flow.path, metadata),
|
|
1246
|
+
...telemetry,
|
|
1247
|
+
useCount: telemetry.useCount + directRuns.length,
|
|
1248
|
+
lastUsedAt: [telemetry.lastUsedAt, directLastUsedAt].filter(Boolean).sort().at(-1) || "",
|
|
1249
|
+
_graph: runnable.graph,
|
|
1250
|
+
_flowRoot: flow.path,
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
for (const ownerUserId of listAgentflowUserIds()) {
|
|
1255
|
+
const ownerId = String(ownerUserId || "").trim();
|
|
1256
|
+
for (const flow of listFlowsJson(workspaceRoot, { userId: ownerId })) {
|
|
1257
|
+
if ((flow.source || "user") !== "user" || flow.archived || !flow.path) continue;
|
|
1258
|
+
appendFlow(ownerId, flow, "user");
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
for (const flow of listFlowsJson(workspaceRoot, { userId: "", includeWorkspaceFlows: true })) {
|
|
1262
|
+
if ((flow.source || "") !== "workspace" || flow.archived || !flow.path) continue;
|
|
1263
|
+
const collaboration = getWorkspaceCollaborationByFlow(flow.id, false);
|
|
1264
|
+
const ownerId = String(collaboration?.ownerId || "").trim();
|
|
1265
|
+
if (!ownerId) continue;
|
|
1266
|
+
appendFlow(ownerId, flow, "workspace", collaboration.id);
|
|
1267
|
+
}
|
|
1268
|
+
return resources;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function publicProjectFlowMarketplaceResource(resource) {
|
|
1272
|
+
const { _graph, _flowRoot, ...publicResource } = resource;
|
|
1273
|
+
return publicResource;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
929
1276
|
function marketplaceResourceMatches(item, queryText) {
|
|
930
1277
|
if (!queryText) return true;
|
|
931
1278
|
return [
|
|
@@ -942,7 +1289,6 @@ function marketplaceResourceMatches(item, queryText) {
|
|
|
942
1289
|
function sortMarketplaceResources(items) {
|
|
943
1290
|
return items.sort((a, b) => (
|
|
944
1291
|
Number(b.useCount || 0) - Number(a.useCount || 0)
|
|
945
|
-
|| Number(b.installCount || 0) - Number(a.installCount || 0)
|
|
946
1292
|
|| String(b.updatedAt || b.createdAt || "").localeCompare(String(a.updatedAt || a.createdAt || ""))
|
|
947
1293
|
|| String(a.displayName || a.id).localeCompare(String(b.displayName || b.id))
|
|
948
1294
|
|| String(b.version || "").localeCompare(String(a.version || ""), undefined, { numeric: true, sensitivity: "base" })
|
|
@@ -1105,19 +1451,16 @@ async function workspaceRoutes(req, res, ctx) {
|
|
|
1105
1451
|
return;
|
|
1106
1452
|
}
|
|
1107
1453
|
const installedCopies = installedMarketplaceFlowCopies(userCtx.userId);
|
|
1108
|
-
const
|
|
1454
|
+
const projectFlows = listRunnableProjectMarketplaceFlows(root, userCtx, scope).map((flow) => {
|
|
1109
1455
|
const installedFlowIds = installedCopies.get(`${flow.id}@${flow.version}`) || [];
|
|
1110
|
-
return {
|
|
1456
|
+
return publicProjectFlowMarketplaceResource({
|
|
1111
1457
|
...flow,
|
|
1112
|
-
resourceType: "flow",
|
|
1113
1458
|
installed: installedFlowIds.length > 0,
|
|
1114
1459
|
installedFlowIds,
|
|
1115
|
-
};
|
|
1460
|
+
});
|
|
1116
1461
|
});
|
|
1117
|
-
const snippets = scope === "installed" ? [] : listMarketplaceFlowSnippets(root, { ...userCtx, marketplaceScope }).snippets
|
|
1118
|
-
.map((snippet) => ({ ...snippet, resourceType: "flow-snippet", installed: false }));
|
|
1119
1462
|
const items = sortMarketplaceResources(
|
|
1120
|
-
|
|
1463
|
+
projectFlows
|
|
1121
1464
|
.filter((item) => scope !== "installed" || item.installed)
|
|
1122
1465
|
.filter((item) => marketplaceResourceMatches(item, queryText)),
|
|
1123
1466
|
);
|
|
@@ -1128,6 +1471,245 @@ async function workspaceRoutes(req, res, ctx) {
|
|
|
1128
1471
|
return;
|
|
1129
1472
|
}
|
|
1130
1473
|
|
|
1474
|
+
if (req.method === "POST" && url.pathname === "/api/marketplace/flow-snippets/use") {
|
|
1475
|
+
let payload;
|
|
1476
|
+
try {
|
|
1477
|
+
payload = JSON.parse(await readBody(req));
|
|
1478
|
+
} catch {
|
|
1479
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
const id = String(payload?.id || "").trim();
|
|
1483
|
+
const version = String(payload?.version || "").trim();
|
|
1484
|
+
if (!id || !version) {
|
|
1485
|
+
json(res, 400, { error: "Missing flow snippet id or version" });
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
try {
|
|
1489
|
+
const accessible = listMarketplaceFlowSnippets(root, { ...userCtx, marketplaceScope: "all" }).snippets
|
|
1490
|
+
.some((snippet) => String(snippet.id) === id && String(snippet.version) === version);
|
|
1491
|
+
if (!accessible) {
|
|
1492
|
+
json(res, 404, { error: "Flow snippet not found" });
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
const eventToken = String(payload?.eventId || crypto.randomUUID()).trim().slice(0, 180);
|
|
1496
|
+
appendMarketplaceUsageEvent(root, {
|
|
1497
|
+
kind: "flow-snippet",
|
|
1498
|
+
id,
|
|
1499
|
+
version,
|
|
1500
|
+
action: "use",
|
|
1501
|
+
actorUserId: userCtx.userId || "",
|
|
1502
|
+
eventId: `use:flow-snippet:${id}@${version}:${userCtx.userId || "anonymous"}:${eventToken}`,
|
|
1503
|
+
});
|
|
1504
|
+
json(res, 200, { ok: true, id, version });
|
|
1505
|
+
} catch (e) {
|
|
1506
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
1507
|
+
}
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
if (req.method === "GET" && url.pathname === "/api/marketplace/flows/preview") {
|
|
1512
|
+
const id = String(url.searchParams.get("id") || "").trim();
|
|
1513
|
+
const version = String(url.searchParams.get("version") || "").trim();
|
|
1514
|
+
const projectFlow = url.searchParams.get("projectFlow") === "1";
|
|
1515
|
+
if (!id || !version) {
|
|
1516
|
+
json(res, 400, { error: "Missing marketplace flow id or version" });
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
try {
|
|
1520
|
+
const installedCopies = installedMarketplaceFlowCopies(userCtx.userId);
|
|
1521
|
+
if (projectFlow) {
|
|
1522
|
+
const source = listRunnableProjectMarketplaceFlows(root, userCtx, "all")
|
|
1523
|
+
.find((flow) => flow.id === id);
|
|
1524
|
+
if (!source) {
|
|
1525
|
+
json(res, 404, { error: "Project flow not found or is private" });
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
if (source.version !== version) {
|
|
1529
|
+
json(res, 409, { error: "该流程已有新版本,请刷新流程仓库后重试" });
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
json(res, 200, {
|
|
1533
|
+
flow: {
|
|
1534
|
+
...publicProjectFlowMarketplaceResource(source),
|
|
1535
|
+
owned: source.ownerUserId === userCtx.userId,
|
|
1536
|
+
installedFlowIds: installedCopies.get(`${id}@${version}`) || [],
|
|
1537
|
+
},
|
|
1538
|
+
graph: source._graph,
|
|
1539
|
+
});
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
const source = readMarketplaceFlow(root, id, version, { ...userCtx, marketplaceScope: "all" });
|
|
1543
|
+
if (!source.ok) {
|
|
1544
|
+
json(res, 404, { error: source.error || "Marketplace flow not found" });
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
const metadata = listMarketplaceFlows(root, { ...userCtx, marketplaceScope: "all" }).flows
|
|
1548
|
+
.find((flow) => flow.id === id && flow.version === version) || {};
|
|
1549
|
+
json(res, 200, {
|
|
1550
|
+
flow: {
|
|
1551
|
+
...metadata,
|
|
1552
|
+
id,
|
|
1553
|
+
version,
|
|
1554
|
+
resourceType: "flow",
|
|
1555
|
+
owned: metadata.ownerUserId === userCtx.userId,
|
|
1556
|
+
installedFlowIds: installedCopies.get(`${id}@${version}`) || [],
|
|
1557
|
+
},
|
|
1558
|
+
graph: source.graph,
|
|
1559
|
+
});
|
|
1560
|
+
} catch (e) {
|
|
1561
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
1562
|
+
}
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
if (req.method === "POST" && url.pathname === "/api/marketplace/flows/workspace-preview") {
|
|
1567
|
+
let payload;
|
|
1568
|
+
try {
|
|
1569
|
+
payload = JSON.parse(await readBody(req));
|
|
1570
|
+
} catch {
|
|
1571
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
const id = String(payload?.id || "").trim();
|
|
1575
|
+
const version = String(payload?.version || "").trim();
|
|
1576
|
+
const previewKind = ["snippet", "node"].includes(payload?.kind) ? payload.kind : "flow";
|
|
1577
|
+
const projectFlow = payload?.projectFlow === true;
|
|
1578
|
+
if (!id || !version) {
|
|
1579
|
+
json(res, 400, { error: "Missing marketplace flow id or version" });
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
try {
|
|
1583
|
+
const installedCopies = installedMarketplaceFlowCopies(userCtx.userId);
|
|
1584
|
+
let graph;
|
|
1585
|
+
let resource;
|
|
1586
|
+
if (previewKind === "node") {
|
|
1587
|
+
const node = listMarketplacePackages(root, { ...userCtx, marketplaceScope: "all" }).nodes
|
|
1588
|
+
.find((item) => item.id === id && item.version === version);
|
|
1589
|
+
if (!node) {
|
|
1590
|
+
json(res, 404, { error: "Node package not found or is private" });
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
graph = {
|
|
1594
|
+
version: 1,
|
|
1595
|
+
instances: {
|
|
1596
|
+
node_preview: {
|
|
1597
|
+
definitionId: node.definitionId,
|
|
1598
|
+
marketplaceRef: node.definitionId,
|
|
1599
|
+
marketplacePackageId: node.id,
|
|
1600
|
+
marketplaceVersion: node.version,
|
|
1601
|
+
label: node.displayName || node.id,
|
|
1602
|
+
role: "normal",
|
|
1603
|
+
body: "",
|
|
1604
|
+
input: Array.isArray(node.inputs) ? node.inputs : [],
|
|
1605
|
+
output: Array.isArray(node.outputs) ? node.outputs : [],
|
|
1606
|
+
},
|
|
1607
|
+
},
|
|
1608
|
+
edges: [],
|
|
1609
|
+
ui: { nodePositions: { node_preview: { x: 320, y: 220 } } },
|
|
1610
|
+
};
|
|
1611
|
+
resource = { ...node, resourceType: "node", owned: node.ownerUserId === userCtx.userId };
|
|
1612
|
+
} else if (previewKind === "snippet") {
|
|
1613
|
+
const snippet = listMarketplaceFlowSnippets(root, { ...userCtx, marketplaceScope: "all" }).snippets
|
|
1614
|
+
.find((item) => item.id === id && item.version === version);
|
|
1615
|
+
if (!snippet) {
|
|
1616
|
+
json(res, 404, { error: "Flow snippet not found or is private" });
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
graph = snippet.snippet;
|
|
1620
|
+
resource = { ...snippet, resourceType: "flow-snippet" };
|
|
1621
|
+
} else if (projectFlow) {
|
|
1622
|
+
const source = listRunnableProjectMarketplaceFlows(root, userCtx, "all")
|
|
1623
|
+
.find((flow) => flow.id === id);
|
|
1624
|
+
if (!source) {
|
|
1625
|
+
json(res, 404, { error: "Project flow not found or is private" });
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
if (source.version !== version) {
|
|
1629
|
+
json(res, 409, { error: "该流程已有新版本,请刷新流程仓库后重试" });
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
graph = source._graph;
|
|
1633
|
+
resource = {
|
|
1634
|
+
...publicProjectFlowMarketplaceResource(source),
|
|
1635
|
+
owned: source.ownerUserId === userCtx.userId,
|
|
1636
|
+
installedFlowIds: installedCopies.get(`${id}@${version}`) || [],
|
|
1637
|
+
};
|
|
1638
|
+
} else {
|
|
1639
|
+
const source = readMarketplaceFlow(root, id, version, { ...userCtx, marketplaceScope: "all" });
|
|
1640
|
+
if (!source.ok) {
|
|
1641
|
+
json(res, 404, { error: source.error || "Marketplace flow not found" });
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
const metadata = listMarketplaceFlows(root, { ...userCtx, marketplaceScope: "all" }).flows
|
|
1645
|
+
.find((flow) => flow.id === id && flow.version === version) || {};
|
|
1646
|
+
graph = source.graph;
|
|
1647
|
+
resource = {
|
|
1648
|
+
...metadata,
|
|
1649
|
+
id,
|
|
1650
|
+
version,
|
|
1651
|
+
resourceType: "flow",
|
|
1652
|
+
owned: metadata.ownerUserId === userCtx.userId,
|
|
1653
|
+
installedFlowIds: installedCopies.get(`${id}@${version}`) || [],
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
const flowId = createWorkspacePreviewId();
|
|
1657
|
+
const flowDir = workspaceSharedPreviewFlowDir(root, flowId);
|
|
1658
|
+
const now = Date.now();
|
|
1659
|
+
const metadata = {
|
|
1660
|
+
version: 1,
|
|
1661
|
+
flowId,
|
|
1662
|
+
ownerId: authUser.userId,
|
|
1663
|
+
title: String(resource.displayName || resource.definitionId || id).trim().slice(0, 200),
|
|
1664
|
+
createdAt: new Date(now).toISOString(),
|
|
1665
|
+
updatedAt: new Date(now).toISOString(),
|
|
1666
|
+
expiresAt: new Date(now + DEFAULT_WORKSPACE_PREVIEW_TTL_MS).toISOString(),
|
|
1667
|
+
marketplace: { id, version, projectFlow },
|
|
1668
|
+
};
|
|
1669
|
+
fs.mkdirSync(flowDir, { recursive: true });
|
|
1670
|
+
writeWorkspaceGraph(flowDir, graph, root);
|
|
1671
|
+
writeWorkspacePreviewMetadata(flowDir, metadata);
|
|
1672
|
+
const installedFlowId = resource.installedFlowIds?.[0] || "";
|
|
1673
|
+
const action = previewKind === "node"
|
|
1674
|
+
? "add-node"
|
|
1675
|
+
: previewKind === "snippet" ? "add-snippet"
|
|
1676
|
+
: resource.projectFlow && resource.owned
|
|
1677
|
+
? "open-source"
|
|
1678
|
+
: installedFlowId ? "open-installed" : "install";
|
|
1679
|
+
const previewParams = new URLSearchParams({
|
|
1680
|
+
flowId,
|
|
1681
|
+
flowSource: "workspace",
|
|
1682
|
+
archived: "1",
|
|
1683
|
+
marketplacePreview: "1",
|
|
1684
|
+
marketplaceKind: previewKind,
|
|
1685
|
+
marketplaceResourceId: id,
|
|
1686
|
+
marketplaceVersion: version,
|
|
1687
|
+
marketplaceProjectFlow: projectFlow ? "1" : "0",
|
|
1688
|
+
marketplaceTitle: resource.displayName || resource.definitionId || id,
|
|
1689
|
+
marketplaceAction: action,
|
|
1690
|
+
marketplaceInstallFlowId: resource.installFlowId || resource.liveFlowId || resource.definitionId || id,
|
|
1691
|
+
});
|
|
1692
|
+
if (previewKind === "node") previewParams.set("focusNodeId", "node_preview");
|
|
1693
|
+
if (action === "open-source") {
|
|
1694
|
+
previewParams.set("marketplaceTargetFlowId", resource.liveFlowId || resource.definitionId || "");
|
|
1695
|
+
previewParams.set("marketplaceTargetFlowSource", resource.liveFlowSource || "user");
|
|
1696
|
+
if (resource.liveWorkspaceId) previewParams.set("marketplaceTargetWorkspaceId", resource.liveWorkspaceId);
|
|
1697
|
+
} else if (action === "open-installed") {
|
|
1698
|
+
previewParams.set("marketplaceTargetFlowId", installedFlowId);
|
|
1699
|
+
previewParams.set("marketplaceTargetFlowSource", "user");
|
|
1700
|
+
}
|
|
1701
|
+
json(res, 200, {
|
|
1702
|
+
ok: true,
|
|
1703
|
+
preview: true,
|
|
1704
|
+
expiresAt: metadata.expiresAt,
|
|
1705
|
+
url: `/workspace?${previewParams}`,
|
|
1706
|
+
});
|
|
1707
|
+
} catch (e) {
|
|
1708
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
1709
|
+
}
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1131
1713
|
if (req.method === "POST" && url.pathname === "/api/marketplace/flows/publish") {
|
|
1132
1714
|
let payload;
|
|
1133
1715
|
try {
|
|
@@ -1181,7 +1763,8 @@ async function workspaceRoutes(req, res, ctx) {
|
|
|
1181
1763
|
}
|
|
1182
1764
|
const id = String(payload?.id || "").trim();
|
|
1183
1765
|
const version = String(payload?.version || "").trim();
|
|
1184
|
-
const
|
|
1766
|
+
const requestedProjectFlow = payload?.projectFlow === true;
|
|
1767
|
+
const idCheck = validateUserPipelineId(String(payload?.flowId || (requestedProjectFlow ? "" : id)));
|
|
1185
1768
|
if (!id || !version || !idCheck.ok) {
|
|
1186
1769
|
json(res, 400, { error: idCheck.error || "Missing marketplace flow id or version" });
|
|
1187
1770
|
return;
|
|
@@ -1193,22 +1776,40 @@ async function workspaceRoutes(req, res, ctx) {
|
|
|
1193
1776
|
return;
|
|
1194
1777
|
}
|
|
1195
1778
|
try {
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1779
|
+
let sourceGraph;
|
|
1780
|
+
let originKind = "flow";
|
|
1781
|
+
if (requestedProjectFlow) {
|
|
1782
|
+
const source = listRunnableProjectMarketplaceFlows(root, userCtx, "all")
|
|
1783
|
+
.find((flow) => flow.id === id);
|
|
1784
|
+
if (!source) {
|
|
1785
|
+
json(res, 404, { error: "Project flow not found or is private" });
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
if (source.version !== version) {
|
|
1789
|
+
json(res, 409, { error: "该流程已有新版本,请刷新流程仓库后重试" });
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
sourceGraph = source._graph;
|
|
1793
|
+
originKind = "project-flow";
|
|
1794
|
+
} else {
|
|
1795
|
+
const source = readMarketplaceFlow(root, id, version, { ...userCtx, marketplaceScope: "all" });
|
|
1796
|
+
if (!source.ok) {
|
|
1797
|
+
json(res, 404, { error: source.error || "Marketplace flow not found" });
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
sourceGraph = source.graph;
|
|
1200
1801
|
}
|
|
1201
1802
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
1202
|
-
const graph = workspaceGraphWithScheduleMode(
|
|
1803
|
+
const graph = workspaceGraphWithScheduleMode(sourceGraph, "disabled");
|
|
1203
1804
|
writeWorkspaceGraph(targetDir, graph, root);
|
|
1204
|
-
writeMarketplaceFlowOrigin(targetDir, { id, version });
|
|
1805
|
+
writeMarketplaceFlowOrigin(targetDir, { kind: originKind, id, version });
|
|
1205
1806
|
appendMarketplaceUsageEvent(root, {
|
|
1206
|
-
kind:
|
|
1807
|
+
kind: originKind,
|
|
1207
1808
|
id,
|
|
1208
1809
|
version,
|
|
1209
1810
|
action: "install",
|
|
1210
1811
|
actorUserId: userCtx.userId || "",
|
|
1211
|
-
eventId: `install
|
|
1812
|
+
eventId: `install:${originKind}:${id}@${version}:${userCtx.userId || "anonymous"}`,
|
|
1212
1813
|
});
|
|
1213
1814
|
json(res, 201, {
|
|
1214
1815
|
ok: true,
|
|
@@ -1236,6 +1837,18 @@ async function workspaceRoutes(req, res, ctx) {
|
|
|
1236
1837
|
return;
|
|
1237
1838
|
}
|
|
1238
1839
|
try {
|
|
1840
|
+
if (String(payload?.kind || "") === "project-flow") {
|
|
1841
|
+
const id = String(payload?.id || "").trim();
|
|
1842
|
+
const source = listRunnableProjectMarketplaceFlows(root, { ...userCtx, isAdmin: false }, "owned")
|
|
1843
|
+
.find((flow) => flow.id === id);
|
|
1844
|
+
if (!source) {
|
|
1845
|
+
json(res, 403, { error: "Only the project owner can change its visibility" });
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
const metadata = writeProjectFlowMarketplaceMetadata(source._flowRoot, payload?.visibility);
|
|
1849
|
+
json(res, 200, { ok: true, kind: "project-flow", id, version: source.version, ...metadata });
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1239
1852
|
const result = setMarketplaceVisibility(
|
|
1240
1853
|
root,
|
|
1241
1854
|
String(payload?.kind || ""),
|
|
@@ -1533,6 +2146,49 @@ async function workspaceRoutes(req, res, ctx) {
|
|
|
1533
2146
|
return;
|
|
1534
2147
|
}
|
|
1535
2148
|
|
|
2149
|
+
if (req.method === "GET" && url.pathname === "/api/workspace/node-review") {
|
|
2150
|
+
try {
|
|
2151
|
+
const nodeId = String(url.searchParams.get("nodeId") || "").trim();
|
|
2152
|
+
if (!nodeId) {
|
|
2153
|
+
json(res, 400, { error: "Missing nodeId" });
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
2157
|
+
flowId: url.searchParams.get("flowId") || "",
|
|
2158
|
+
flowSource: url.searchParams.get("flowSource") || "user",
|
|
2159
|
+
workspaceId: url.searchParams.get("workspaceId") || "",
|
|
2160
|
+
adminOwnerId: url.searchParams.get("adminOwnerId") || "",
|
|
2161
|
+
archived: url.searchParams.get("archived") === "1",
|
|
2162
|
+
}, userCtx);
|
|
2163
|
+
if (scoped.error) {
|
|
2164
|
+
json(res, scoped.status || 400, { error: scoped.error });
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
const scopedUserCtx = workspaceScopedUserContext(scoped, userCtx);
|
|
2168
|
+
const draftGraph = readWorkspaceGraph(scoped.root, root).graph;
|
|
2169
|
+
const stableRelease = readWorkspaceStableRelease(scoped.root, root);
|
|
2170
|
+
const draft = workspaceNodeReviewSnapshot(root, scoped.root, draftGraph, nodeId, scopedUserCtx, "Draft");
|
|
2171
|
+
const stable = stableRelease
|
|
2172
|
+
? workspaceNodeReviewSnapshot(root, stableRelease.root, stableRelease.graph, nodeId, scopedUserCtx, `Stable ${stableRelease.release.id}`)
|
|
2173
|
+
: null;
|
|
2174
|
+
if (!draft && !stable) {
|
|
2175
|
+
json(res, 404, { error: "Node not found" });
|
|
2176
|
+
return;
|
|
2177
|
+
}
|
|
2178
|
+
json(res, 200, {
|
|
2179
|
+
nodeId,
|
|
2180
|
+
draft,
|
|
2181
|
+
stable,
|
|
2182
|
+
stableReleaseId: stableRelease?.release?.id || "",
|
|
2183
|
+
stableRevision: stableRelease?.release?.designRevision || "",
|
|
2184
|
+
draftRevision: workspaceDesignRevision(draftGraph),
|
|
2185
|
+
});
|
|
2186
|
+
} catch (e) {
|
|
2187
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
2188
|
+
}
|
|
2189
|
+
return;
|
|
2190
|
+
}
|
|
2191
|
+
|
|
1536
2192
|
if (req.method === "GET" && url.pathname === "/api/workspaces") {
|
|
1537
2193
|
try {
|
|
1538
2194
|
const scoped = resolveWorkspaceScopeRoot(root, {
|