@fieldwangai/agentflow 0.1.165 → 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.
@@ -51,7 +51,7 @@ import { mergeWorkspaceGraphs, workspaceDesignRevision, workspaceRuntimeRevision
51
51
  import { DEFAULT_WORKSPACE_PREVIEW_TTL_MS, createWorkspacePreviewId, normalizeWorkspacePreviewTtlMs, readWorkspacePreviewMetadata, workspaceSharedPreviewFlowDir, writeWorkspacePreviewMetadata } from "./workspace-preview.mjs";
52
52
  import { DEFAULT_WORKSPACE_DRAFT_TTL_MS, createWorkspaceDraftId, normalizeWorkspaceDraftTtlMs, readWorkspaceDraftMetadata, workspaceDraftFlowDir, writeWorkspaceDraftMetadata } from "./workspace-draft.mjs";
53
53
  import { appendWorkspaceRunLogEvent, createWorkspaceRunLogSession, finishWorkspaceRunLogSession, listWorkspaceRunLogs, readWorkspaceRunLogEvents } from "./workspace-run-logs.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";
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";
55
55
  import { splitWorkspaceGraph, WORKSPACE_STATE_FILENAME } from "./workspace-state.mjs";
56
56
  import { getWorkspaceTree } from "./workspace-tree.mjs";
57
57
  import busboy from "busboy";
@@ -248,6 +248,170 @@ function missingWorkspaceGraphNodePackages(workspaceRoot, scoped, graph, userCtx
248
248
  return [...missing].sort();
249
249
  }
250
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
+
251
415
  const NODE_STUDIO_DRAFTS_DIRNAME = "node-studio/drafts";
252
416
 
253
417
  function writeUserWorkspaces(userCtx = {}, entries = []) {
@@ -964,11 +1128,47 @@ function writeProjectFlowMarketplaceMetadata(flowRoot, visibility) {
964
1128
  return value;
965
1129
  }
966
1130
 
967
- function graphHasRunnableEntry(graph) {
968
- return Object.values(graph?.instances || {}).some((instance) => (
1131
+ function runnableProjectFlowEntries(graph, scopedRoot = "") {
1132
+ const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
1133
+ const entries = Object.entries(instances).filter(([, instance]) => (
969
1134
  instance?.definitionId === "workspace_run"
970
1135
  || instance?.definitionId === "workspace_scheduled_run"
971
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
+ });
972
1172
  }
973
1173
 
974
1174
  function projectFlowUpdatedAt(flowRoot, metadata = {}) {
@@ -983,6 +1183,7 @@ function projectFlowUpdatedAt(flowRoot, metadata = {}) {
983
1183
  function listRunnableProjectMarketplaceFlows(workspaceRoot, userCtx = {}, scope = "all") {
984
1184
  const requestedUserId = String(userCtx.userId || "").trim();
985
1185
  const stats = marketplaceUsageStats(workspaceRoot);
1186
+ const runUsage = readWorkspaceRunUsageRecords();
986
1187
  const resources = [];
987
1188
  const appendFlow = (ownerId, flow, flowSource = "user", workspaceId = "") => {
988
1189
  if (!ownerId || (scope === "owned" && ownerId !== requestedUserId)) return;
@@ -995,34 +1196,60 @@ function listRunnableProjectMarketplaceFlows(workspaceRoot, userCtx = {}, scope
995
1196
  } catch {
996
1197
  return;
997
1198
  }
998
- if (!graphHasRunnableEntry(graph)) return;
1199
+ const runnableEntries = runnableProjectFlowEntries(graph, flow.path);
1200
+ if (runnableEntries.length === 0) return;
999
1201
  const metadata = readProjectFlowMarketplaceMetadata(flow.path);
1000
1202
  const owned = ownerId === requestedUserId;
1001
1203
  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
- });
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
+ }
1026
1253
  };
1027
1254
  for (const ownerUserId of listAgentflowUserIds()) {
1028
1255
  const ownerId = String(ownerUserId || "").trim();
@@ -1062,7 +1289,6 @@ function marketplaceResourceMatches(item, queryText) {
1062
1289
  function sortMarketplaceResources(items) {
1063
1290
  return items.sort((a, b) => (
1064
1291
  Number(b.useCount || 0) - Number(a.useCount || 0)
1065
- || Number(b.installCount || 0) - Number(a.installCount || 0)
1066
1292
  || String(b.updatedAt || b.createdAt || "").localeCompare(String(a.updatedAt || a.createdAt || ""))
1067
1293
  || String(a.displayName || a.id).localeCompare(String(b.displayName || b.id))
1068
1294
  || String(b.version || "").localeCompare(String(a.version || ""), undefined, { numeric: true, sensitivity: "base" })
@@ -1225,15 +1451,6 @@ async function workspaceRoutes(req, res, ctx) {
1225
1451
  return;
1226
1452
  }
1227
1453
  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
1454
  const projectFlows = listRunnableProjectMarketplaceFlows(root, userCtx, scope).map((flow) => {
1238
1455
  const installedFlowIds = installedCopies.get(`${flow.id}@${flow.version}`) || [];
1239
1456
  return publicProjectFlowMarketplaceResource({
@@ -1242,10 +1459,8 @@ async function workspaceRoutes(req, res, ctx) {
1242
1459
  installedFlowIds,
1243
1460
  });
1244
1461
  });
1245
- const snippets = scope === "installed" ? [] : listMarketplaceFlowSnippets(root, { ...userCtx, marketplaceScope }).snippets
1246
- .map((snippet) => ({ ...snippet, resourceType: "flow-snippet", installed: false }));
1247
1462
  const items = sortMarketplaceResources(
1248
- [...projectFlows, ...flows, ...snippets]
1463
+ projectFlows
1249
1464
  .filter((item) => scope !== "installed" || item.installed)
1250
1465
  .filter((item) => marketplaceResourceMatches(item, queryText)),
1251
1466
  );
@@ -1293,6 +1508,208 @@ async function workspaceRoutes(req, res, ctx) {
1293
1508
  return;
1294
1509
  }
1295
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
+
1296
1713
  if (req.method === "POST" && url.pathname === "/api/marketplace/flows/publish") {
1297
1714
  let payload;
1298
1715
  try {
@@ -1729,6 +2146,49 @@ async function workspaceRoutes(req, res, ctx) {
1729
2146
  return;
1730
2147
  }
1731
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
+
1732
2192
  if (req.method === "GET" && url.pathname === "/api/workspaces") {
1733
2193
  try {
1734
2194
  const scoped = resolveWorkspaceScopeRoot(root, {
@@ -1182,6 +1182,7 @@ function normalizeWorkspaceUsageRecord(parsed, source = "workspace-run") {
1182
1182
  username: String(parsed?.username || userId),
1183
1183
  flowId,
1184
1184
  flowSource: String(parsed?.flowSource || "user"),
1185
+ runNodeId: String(parsed?.runNodeId || ""),
1185
1186
  runId: String(parsed?.runId || ""),
1186
1187
  at,
1187
1188
  endedAt: parsed?.endedAt == null ? null : Number(parsed.endedAt),