@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.
@@ -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 flows = listMarketplaceFlows(root, { ...userCtx, marketplaceScope }).flows.map((flow) => {
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
- [...flows, ...snippets]
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 idCheck = validateUserPipelineId(String(payload?.flowId || id));
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
- const source = readMarketplaceFlow(root, id, version, { ...userCtx, marketplaceScope: "all" });
1197
- if (!source.ok) {
1198
- json(res, 404, { error: source.error || "Marketplace flow not found" });
1199
- return;
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(source.graph, "disabled");
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: "flow",
1807
+ kind: originKind,
1207
1808
  id,
1208
1809
  version,
1209
1810
  action: "install",
1210
1811
  actorUserId: userCtx.userId || "",
1211
- eventId: `install:flow:${id}@${version}:${userCtx.userId || "anonymous"}`,
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, {