@fieldwangai/agentflow 0.1.166 → 0.1.168

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,9 +11,23 @@
11
11
  * 是请求回调的闭包,解构之后路由体里的写法完全不变。
12
12
  */
13
13
 
14
- import { listFlowsJson, listNodesJson, readNodeDetailJson, readNodeFilePreview } from "./catalog-flows.mjs";
14
+ import { listNodesJson, readNodeDetailJson, readNodeFilePreview } from "./catalog-flows.mjs";
15
15
  import { startComposerAgent } from "./composer-agent.mjs";
16
+ import {
17
+ aiMaterializationPrompt,
18
+ aiPlanPrompt,
19
+ appendAiTraceEvents,
20
+ createAiExplorationSession,
21
+ classifyAiToolSideEffect,
22
+ listAiExplorationSessions,
23
+ materializableAiTraceEvents,
24
+ parseAiPlanResult,
25
+ readAiExplorationSession,
26
+ updateAiExplorationSession,
27
+ writeAiExplorationMaterialization,
28
+ } from "./ai-exploration.mjs";
16
29
  import { buildSkillCompactInjectionBlock, loadResourcesForSkillKeys } from "./composer-skill-router.mjs";
30
+ import { builtinNodeReviewSources } from "./builtin-node-review.mjs";
17
31
  import { execFileBuffered } from "./exec-buffered.mjs";
18
32
  import { runGit } from "./git-worktree.mjs";
19
33
  import {
@@ -30,28 +44,36 @@ import {
30
44
  } from "./marketplace.mjs";
31
45
  import {
32
46
  appendMarketplaceUsageEvent,
33
- marketplaceStatsFor,
34
- marketplaceUsageStats,
35
47
  marketplaceResourcesForRun,
36
- readMarketplaceFlowOrigin,
37
48
  writeMarketplaceFlowOrigin,
38
49
  } from "./marketplace-usage.mjs";
50
+ import {
51
+ indexedProjectFlowPreview,
52
+ getRepositoryIndex,
53
+ indexedMarketplaceFlowCopies,
54
+ listIndexedNodes,
55
+ listIndexedProjectFlows,
56
+ markRepositoryIndexDirty,
57
+ updateIndexedProjectFlowVisibility,
58
+ updateIndexedNodeVisibility,
59
+ writeProjectFlowMarketplaceMetadata,
60
+ } from "./repository-index.mjs";
39
61
  import { NODE_PACKAGE_ENTRY, nodePackageExportsRun, readNodePackageManifest } from "./node-package-manifest.mjs";
40
62
  import { inspectNodePackageDirectory } from "./node-package-archive.mjs";
41
63
  import { json, readBody } from "./http-util.mjs";
42
64
  import { log } from "./log.mjs";
43
- import { PACKAGE_ROOT, PIPELINES_DIR, getAgentflowUserDataRoot, getUserPipelinesRoot, listAgentflowUserIds } from "./paths.mjs";
65
+ import { PACKAGE_ROOT, PIPELINES_DIR, getAgentflowUserDataRoot, getUserPipelinesRoot } from "./paths.mjs";
44
66
  import { validateUserPipelineId } from "./flow-write.mjs";
45
67
  import { runLedgerId } from "./run-ledger.mjs";
46
68
  import { getTeamById, getTeamForUser } from "./teams.mjs";
47
69
  import { readMergedEnvObject, runtimeEnvForUser } from "./user-env.mjs";
48
- import { acceptWorkspaceCollaborationInvite, addWorkspaceCollaborationMember, deleteWorkspaceCollaborationForFlow, ensureWorkspaceCollaboration, getWorkspaceCollaborationByFlow, getWorkspaceCollaborationForProject, listWorkspaceCollaborationsForUser, removeWorkspaceCollaborationMember, removeWorkspaceCollaborationTeamShare, setWorkspaceCollaborationTeamShare, workspaceCollaborationAccess } from "./workspace-collaboration.mjs";
70
+ import { acceptWorkspaceCollaborationInvite, addWorkspaceCollaborationMember, deleteWorkspaceCollaborationForFlow, ensureWorkspaceCollaboration, getWorkspaceCollaborationForProject, listWorkspaceCollaborationsForUser, removeWorkspaceCollaborationMember, removeWorkspaceCollaborationTeamShare, setWorkspaceCollaborationTeamShare, workspaceCollaborationAccess } from "./workspace-collaboration.mjs";
49
71
  import { WorkspaceFlowParseError } from "./workspace-flow-store.mjs";
50
72
  import { mergeWorkspaceGraphs, workspaceDesignRevision, workspaceRuntimeRevision } from "./workspace-graph-merge.mjs";
51
73
  import { DEFAULT_WORKSPACE_PREVIEW_TTL_MS, createWorkspacePreviewId, normalizeWorkspacePreviewTtlMs, readWorkspacePreviewMetadata, workspaceSharedPreviewFlowDir, writeWorkspacePreviewMetadata } from "./workspace-preview.mjs";
52
74
  import { DEFAULT_WORKSPACE_DRAFT_TTL_MS, createWorkspaceDraftId, normalizeWorkspaceDraftTtlMs, readWorkspaceDraftMetadata, workspaceDraftFlowDir, writeWorkspaceDraftMetadata } from "./workspace-draft.mjs";
53
75
  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, 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";
76
+ 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";
55
77
  import { splitWorkspaceGraph, WORKSPACE_STATE_FILENAME } from "./workspace-state.mjs";
56
78
  import { getWorkspaceTree } from "./workspace-tree.mjs";
57
79
  import busboy from "busboy";
@@ -372,6 +394,10 @@ function workspaceNodeReviewSnapshot(workspaceRoot, flowRoot, graph, nodeId, use
372
394
  }));
373
395
  }
374
396
 
397
+ if (!marketplaceRef.startsWith("marketplace:")) {
398
+ sources.push(...builtinNodeReviewSources(definitionId).map((source) => workspaceNodeReviewSource(source)));
399
+ }
400
+
375
401
  for (const [ref, kind] of [[instance.scriptRef, "script"], [instance.implementationRef, "implementation"]]) {
376
402
  const source = workspaceNodeReviewFlowFile(flowRoot, ref, kind);
377
403
  if (source?.error) errors.push(source.error);
@@ -1076,200 +1102,12 @@ function workspaceBufferLooksLikeZip(buf) {
1076
1102
  );
1077
1103
  }
1078
1104
 
1079
- function installedMarketplaceFlowCopies(userId) {
1080
- const copies = new Map();
1081
- const pipelinesRoot = getUserPipelinesRoot(userId);
1082
- if (!fs.existsSync(pipelinesRoot)) return copies;
1083
- for (const entry of fs.readdirSync(pipelinesRoot, { withFileTypes: true })) {
1084
- if (!entry.isDirectory()) continue;
1085
- const origin = readMarketplaceFlowOrigin(path.join(pipelinesRoot, entry.name));
1086
- if (!origin) continue;
1087
- const key = `${origin.id}@${origin.version}`;
1088
- const flowIds = copies.get(key) || [];
1089
- flowIds.push(entry.name);
1090
- copies.set(key, flowIds);
1091
- }
1092
- return copies;
1093
- }
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
1105
  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;
1106
+ return listIndexedProjectFlows(workspaceRoot, userCtx, scope);
1269
1107
  }
1270
1108
 
1271
1109
  function publicProjectFlowMarketplaceResource(resource) {
1272
- const { _graph, _flowRoot, ...publicResource } = resource;
1110
+ const { _graph, _flowRoot, flowRoot, ...publicResource } = resource;
1273
1111
  return publicResource;
1274
1112
  }
1275
1113
 
@@ -1295,6 +1133,20 @@ function sortMarketplaceResources(items) {
1295
1133
  ));
1296
1134
  }
1297
1135
 
1136
+ function paginateMarketplaceResources(items, url, defaultLimit = 24) {
1137
+ const requestedLimit = Number(url.searchParams.get("limit") || defaultLimit);
1138
+ const limit = Math.min(100, Math.max(1, Number.isFinite(requestedLimit) ? Math.floor(requestedLimit) : defaultLimit));
1139
+ const requestedCursor = Number(url.searchParams.get("cursor") || 0);
1140
+ const offset = Math.max(0, Number.isFinite(requestedCursor) ? Math.floor(requestedCursor) : 0);
1141
+ const pageItems = items.slice(offset, offset + limit);
1142
+ const nextOffset = offset + pageItems.length;
1143
+ return {
1144
+ items: pageItems,
1145
+ total: items.length,
1146
+ nextCursor: nextOffset < items.length ? String(nextOffset) : "",
1147
+ };
1148
+ }
1149
+
1298
1150
  /**
1299
1151
  * @param {import('http').IncomingMessage} req
1300
1152
  * @param {import('http').ServerResponse} res
@@ -1357,6 +1209,7 @@ async function workspaceRoutes(req, res, ctx) {
1357
1209
  }
1358
1210
  try {
1359
1211
  const result = publishNodePackageArchive(root, parsed.file, { ownerUserId: userCtx.userId });
1212
+ if (result.ok) markRepositoryIndexDirty(root);
1360
1213
  json(res, result.ok ? (result.alreadyExists ? 200 : 201) : (result.conflict ? 409 : 400), result);
1361
1214
  } catch (e) {
1362
1215
  json(res, 500, { ok: false, error: (e && e.message) || String(e) });
@@ -1389,6 +1242,7 @@ async function workspaceRoutes(req, res, ctx) {
1389
1242
  actorUserId: userCtx.userId || "",
1390
1243
  eventId: `install:node:${id}@${version}:${userCtx.userId || "anonymous"}`,
1391
1244
  });
1245
+ markRepositoryIndexDirty(root);
1392
1246
  res.writeHead(200, {
1393
1247
  "Content-Type": "application/zip",
1394
1248
  "Content-Length": result.archive.length,
@@ -1409,10 +1263,12 @@ async function workspaceRoutes(req, res, ctx) {
1409
1263
  const kind = String(url.searchParams.get("kind") || "flow").trim();
1410
1264
  const requestedScope = String(url.searchParams.get("scope") || "all").trim();
1411
1265
  const scope = ["all", "owned", "installed"].includes(requestedScope) ? requestedScope : "all";
1266
+ const requestedReleaseState = String(url.searchParams.get("releaseState") || "all").trim().toLowerCase();
1267
+ const releaseState = ["all", "stable", "draft"].includes(requestedReleaseState) ? requestedReleaseState : "all";
1412
1268
  const marketplaceScope = scope === "owned" ? "owned" : "all";
1413
1269
  const queryText = String(url.searchParams.get("q") || "").trim().toLowerCase();
1414
1270
  if (kind === "node") {
1415
- const marketplaceNodes = listMarketplacePackages(root, { ...userCtx, marketplaceScope }).nodes
1271
+ const marketplaceNodes = listIndexedNodes(root, userCtx, marketplaceScope)
1416
1272
  .map((node) => ({ ...node, resourceType: "node", installed: true }));
1417
1273
  let nodes = marketplaceNodes;
1418
1274
  if (scope === "installed") {
@@ -1443,14 +1299,23 @@ async function workspaceRoutes(req, res, ctx) {
1443
1299
  });
1444
1300
  }
1445
1301
  nodes = sortMarketplaceResources(nodes.filter((item) => marketplaceResourceMatches(item, queryText)));
1446
- json(res, 200, { kind, scope, sort: "useCount", order: "desc", items: nodes });
1302
+ const page = paginateMarketplaceResources(nodes, url);
1303
+ const index = getRepositoryIndex(root);
1304
+ json(res, 200, {
1305
+ kind,
1306
+ scope,
1307
+ sort: "useCount",
1308
+ order: "desc",
1309
+ ...page,
1310
+ index: { status: "ready", generatedAt: index.generatedAt },
1311
+ });
1447
1312
  return;
1448
1313
  }
1449
1314
  if (kind !== "flow") {
1450
1315
  json(res, 400, { error: "kind must be flow or node" });
1451
1316
  return;
1452
1317
  }
1453
- const installedCopies = installedMarketplaceFlowCopies(userCtx.userId);
1318
+ const installedCopies = indexedMarketplaceFlowCopies(root, userCtx.userId);
1454
1319
  const projectFlows = listRunnableProjectMarketplaceFlows(root, userCtx, scope).map((flow) => {
1455
1320
  const installedFlowIds = installedCopies.get(`${flow.id}@${flow.version}`) || [];
1456
1321
  return publicProjectFlowMarketplaceResource({
@@ -1462,9 +1327,20 @@ async function workspaceRoutes(req, res, ctx) {
1462
1327
  const items = sortMarketplaceResources(
1463
1328
  projectFlows
1464
1329
  .filter((item) => scope !== "installed" || item.installed)
1330
+ .filter((item) => releaseState === "all" || item.releaseState === releaseState)
1465
1331
  .filter((item) => marketplaceResourceMatches(item, queryText)),
1466
1332
  );
1467
- json(res, 200, { kind, scope, sort: "useCount", order: "desc", items });
1333
+ const page = paginateMarketplaceResources(items, url);
1334
+ const index = getRepositoryIndex(root);
1335
+ json(res, 200, {
1336
+ kind,
1337
+ scope,
1338
+ releaseState,
1339
+ sort: "useCount",
1340
+ order: "desc",
1341
+ ...page,
1342
+ index: { status: "ready", generatedAt: index.generatedAt },
1343
+ });
1468
1344
  } catch (e) {
1469
1345
  json(res, 500, { error: (e && e.message) || String(e) });
1470
1346
  }
@@ -1517,7 +1393,7 @@ async function workspaceRoutes(req, res, ctx) {
1517
1393
  return;
1518
1394
  }
1519
1395
  try {
1520
- const installedCopies = installedMarketplaceFlowCopies(userCtx.userId);
1396
+ const installedCopies = indexedMarketplaceFlowCopies(root, userCtx.userId);
1521
1397
  if (projectFlow) {
1522
1398
  const source = listRunnableProjectMarketplaceFlows(root, userCtx, "all")
1523
1399
  .find((flow) => flow.id === id);
@@ -1529,13 +1405,19 @@ async function workspaceRoutes(req, res, ctx) {
1529
1405
  json(res, 409, { error: "该流程已有新版本,请刷新流程仓库后重试" });
1530
1406
  return;
1531
1407
  }
1408
+ const preview = indexedProjectFlowPreview(root, source);
1409
+ if (!preview || preview.stale) {
1410
+ markRepositoryIndexDirty(root);
1411
+ json(res, preview?.stale ? 409 : 404, { error: preview?.stale ? "该流程已有新版本,请刷新流程仓库后重试" : "Project flow preview is unavailable" });
1412
+ return;
1413
+ }
1532
1414
  json(res, 200, {
1533
1415
  flow: {
1534
1416
  ...publicProjectFlowMarketplaceResource(source),
1535
1417
  owned: source.ownerUserId === userCtx.userId,
1536
1418
  installedFlowIds: installedCopies.get(`${id}@${version}`) || [],
1537
1419
  },
1538
- graph: source._graph,
1420
+ graph: preview.graph,
1539
1421
  });
1540
1422
  return;
1541
1423
  }
@@ -1580,11 +1462,11 @@ async function workspaceRoutes(req, res, ctx) {
1580
1462
  return;
1581
1463
  }
1582
1464
  try {
1583
- const installedCopies = installedMarketplaceFlowCopies(userCtx.userId);
1465
+ const installedCopies = indexedMarketplaceFlowCopies(root, userCtx.userId);
1584
1466
  let graph;
1585
1467
  let resource;
1586
1468
  if (previewKind === "node") {
1587
- const node = listMarketplacePackages(root, { ...userCtx, marketplaceScope: "all" }).nodes
1469
+ const node = listIndexedNodes(root, userCtx, "all")
1588
1470
  .find((item) => item.id === id && item.version === version);
1589
1471
  if (!node) {
1590
1472
  json(res, 404, { error: "Node package not found or is private" });
@@ -1629,7 +1511,13 @@ async function workspaceRoutes(req, res, ctx) {
1629
1511
  json(res, 409, { error: "该流程已有新版本,请刷新流程仓库后重试" });
1630
1512
  return;
1631
1513
  }
1632
- graph = source._graph;
1514
+ const preview = indexedProjectFlowPreview(root, source);
1515
+ if (!preview || preview.stale) {
1516
+ markRepositoryIndexDirty(root);
1517
+ json(res, preview?.stale ? 409 : 404, { error: preview?.stale ? "该流程已有新版本,请刷新流程仓库后重试" : "Project flow preview is unavailable" });
1518
+ return;
1519
+ }
1520
+ graph = preview.graph;
1633
1521
  resource = {
1634
1522
  ...publicProjectFlowMarketplaceResource(source),
1635
1523
  owned: source.ownerUserId === userCtx.userId,
@@ -1689,7 +1577,11 @@ async function workspaceRoutes(req, res, ctx) {
1689
1577
  marketplaceAction: action,
1690
1578
  marketplaceInstallFlowId: resource.installFlowId || resource.liveFlowId || resource.definitionId || id,
1691
1579
  });
1692
- if (previewKind === "node") previewParams.set("focusNodeId", "node_preview");
1580
+ if (previewKind === "node") {
1581
+ previewParams.set("focusNodeId", "node_preview");
1582
+ } else if (projectFlow && resource.liveEntryId) {
1583
+ previewParams.set("focusNodeId", resource.liveEntryId);
1584
+ }
1693
1585
  if (action === "open-source") {
1694
1586
  previewParams.set("marketplaceTargetFlowId", resource.liveFlowId || resource.definitionId || "");
1695
1587
  previewParams.set("marketplaceTargetFlowSource", resource.liveFlowSource || "user");
@@ -1746,6 +1638,7 @@ async function workspaceRoutes(req, res, ctx) {
1746
1638
  visibility: payload?.visibility || "public",
1747
1639
  graph,
1748
1640
  }, userCtx);
1641
+ if (result.ok) markRepositoryIndexDirty(root);
1749
1642
  json(res, result.ok ? (result.alreadyExists ? 200 : 201) : (result.conflict ? 409 : 400), result);
1750
1643
  } catch (e) {
1751
1644
  json(res, 500, { error: (e && e.message) || String(e) });
@@ -1789,7 +1682,13 @@ async function workspaceRoutes(req, res, ctx) {
1789
1682
  json(res, 409, { error: "该流程已有新版本,请刷新流程仓库后重试" });
1790
1683
  return;
1791
1684
  }
1792
- sourceGraph = source._graph;
1685
+ const preview = indexedProjectFlowPreview(root, source);
1686
+ if (!preview || preview.stale) {
1687
+ markRepositoryIndexDirty(root);
1688
+ json(res, preview?.stale ? 409 : 404, { error: preview?.stale ? "该流程已有新版本,请刷新流程仓库后重试" : "Project flow preview is unavailable" });
1689
+ return;
1690
+ }
1691
+ sourceGraph = preview.graph;
1793
1692
  originKind = "project-flow";
1794
1693
  } else {
1795
1694
  const source = readMarketplaceFlow(root, id, version, { ...userCtx, marketplaceScope: "all" });
@@ -1811,6 +1710,7 @@ async function workspaceRoutes(req, res, ctx) {
1811
1710
  actorUserId: userCtx.userId || "",
1812
1711
  eventId: `install:${originKind}:${id}@${version}:${userCtx.userId || "anonymous"}`,
1813
1712
  });
1713
+ markRepositoryIndexDirty(root);
1814
1714
  json(res, 201, {
1815
1715
  ok: true,
1816
1716
  id,
@@ -1846,17 +1746,27 @@ async function workspaceRoutes(req, res, ctx) {
1846
1746
  return;
1847
1747
  }
1848
1748
  const metadata = writeProjectFlowMarketplaceMetadata(source._flowRoot, payload?.visibility);
1749
+ updateIndexedProjectFlowVisibility(root, id, metadata.visibility, metadata.updatedAt);
1849
1750
  json(res, 200, { ok: true, kind: "project-flow", id, version: source.version, ...metadata });
1850
1751
  return;
1851
1752
  }
1753
+ const resourceKind = String(payload?.kind || "");
1754
+ const resourceId = String(payload?.id || "");
1755
+ const resourceVersion = String(payload?.version || "");
1756
+ const resourceVisibility = String(payload?.visibility || "public");
1852
1757
  const result = setMarketplaceVisibility(
1853
1758
  root,
1854
- String(payload?.kind || ""),
1855
- String(payload?.id || ""),
1856
- String(payload?.version || ""),
1857
- String(payload?.visibility || "public"),
1759
+ resourceKind,
1760
+ resourceId,
1761
+ resourceVersion,
1762
+ resourceVisibility,
1858
1763
  userCtx,
1859
1764
  );
1765
+ if (result.ok && resourceKind === "node") {
1766
+ updateIndexedNodeVisibility(root, resourceId, resourceVersion, resourceVisibility);
1767
+ } else if (result.ok) {
1768
+ markRepositoryIndexDirty(root);
1769
+ }
1860
1770
  json(res, result.ok ? 200 : 400, result);
1861
1771
  } catch (e) {
1862
1772
  json(res, 500, { error: (e && e.message) || String(e) });
@@ -2391,6 +2301,7 @@ async function workspaceRoutes(req, res, ctx) {
2391
2301
  const graph = workspaceGraphWithScheduleMode(draftGraph, payload.scheduleMode || "disabled");
2392
2302
  fs.mkdirSync(targetDir, { recursive: true });
2393
2303
  writeWorkspaceGraph(targetDir, graph, root);
2304
+ markRepositoryIndexDirty(root);
2394
2305
  let collaborationCreated = false;
2395
2306
  if (flowSource === "workspace") {
2396
2307
  ensureWorkspaceCollaboration({ flowId, userId: authUser.userId });
@@ -2674,6 +2585,7 @@ async function workspaceRoutes(req, res, ctx) {
2674
2585
  json(res, result.conflict ? 409 : 400, result);
2675
2586
  return;
2676
2587
  }
2588
+ markRepositoryIndexDirty(root);
2677
2589
  const graph = readWorkspaceGraph(scoped.root, root).graph;
2678
2590
  const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx);
2679
2591
  broadcastWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
@@ -2722,6 +2634,7 @@ async function workspaceRoutes(req, res, ctx) {
2722
2634
  json(res, 404, result);
2723
2635
  return;
2724
2636
  }
2637
+ markRepositoryIndexDirty(root);
2725
2638
  const graph = readWorkspaceGraph(scoped.root, root).graph;
2726
2639
  const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx);
2727
2640
  broadcastWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
@@ -2849,6 +2762,7 @@ async function workspaceRoutes(req, res, ctx) {
2849
2762
  }
2850
2763
  throw new Error(`Workspace graph save rolled back: ${(scheduleError && scheduleError.message) || String(scheduleError)}`);
2851
2764
  }
2765
+ markRepositoryIndexDirty(root);
2852
2766
  broadcastWorkspaceCollaborationEvent(
2853
2767
  userCtx,
2854
2768
  scoped.flowSource,
@@ -2960,6 +2874,7 @@ async function workspaceRoutes(req, res, ctx) {
2960
2874
  graph.instances[scheduleNodeId] = { ...instance, body: JSON.stringify(config) };
2961
2875
  const committed = commitWorkspaceGraph(root, scoped, graph, userCtx);
2962
2876
  const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, committed.graph, authUser, userCtx);
2877
+ markRepositoryIndexDirty(root);
2963
2878
  const schedule = workspaceSchedules.find((item) => item.scheduleNodeId === scheduleNodeId) || {
2964
2879
  flowId,
2965
2880
  flowSource,
@@ -3451,6 +3366,7 @@ async function workspaceRoutes(req, res, ctx) {
3451
3366
  const scoped = resolveWorkspaceScopeRoot(root, {
3452
3367
  flowId,
3453
3368
  flowSource: flowSource || "user",
3369
+ adminOwnerId: url.searchParams.get("adminOwnerId") || "",
3454
3370
  archived: url.searchParams.get("archived") === "1",
3455
3371
  }, userCtx);
3456
3372
  if (scoped.error) {
@@ -3486,6 +3402,7 @@ async function workspaceRoutes(req, res, ctx) {
3486
3402
  const scoped = resolveWorkspaceScopeRoot(root, {
3487
3403
  flowId,
3488
3404
  flowSource,
3405
+ adminOwnerId: url.searchParams.get("adminOwnerId") || "",
3489
3406
  archived: url.searchParams.get("archived") === "1",
3490
3407
  }, userCtx);
3491
3408
  if (scoped.error) {
@@ -4032,6 +3949,436 @@ async function workspaceRoutes(req, res, ctx) {
4032
3949
  return;
4033
3950
  }
4034
3951
 
3952
+ if (req.method === "GET" && url.pathname === "/api/workspace/explorations") {
3953
+ try {
3954
+ const scoped = resolveWorkspaceScopeRoot(root, {
3955
+ flowId: url.searchParams.get("flowId") || "",
3956
+ flowSource: url.searchParams.get("flowSource") || "user",
3957
+ adminOwnerId: url.searchParams.get("adminOwnerId") || "",
3958
+ archived: url.searchParams.get("archived") === "1" || url.searchParams.get("flowArchived") === "1",
3959
+ }, userCtx);
3960
+ if (scoped.error) {
3961
+ json(res, 400, { error: scoped.error });
3962
+ return;
3963
+ }
3964
+ json(res, 200, { ok: true, explorations: listAiExplorationSessions(scoped.root) });
3965
+ } catch (e) {
3966
+ json(res, 500, { error: (e && e.message) || String(e) });
3967
+ }
3968
+ return;
3969
+ }
3970
+
3971
+ if (req.method === "GET" && url.pathname === "/api/workspace/exploration") {
3972
+ try {
3973
+ const scoped = resolveWorkspaceScopeRoot(root, {
3974
+ flowId: url.searchParams.get("flowId") || "",
3975
+ flowSource: url.searchParams.get("flowSource") || "user",
3976
+ adminOwnerId: url.searchParams.get("adminOwnerId") || "",
3977
+ archived: url.searchParams.get("archived") === "1" || url.searchParams.get("flowArchived") === "1",
3978
+ }, userCtx);
3979
+ if (scoped.error) {
3980
+ json(res, 400, { error: scoped.error });
3981
+ return;
3982
+ }
3983
+ json(res, 200, { ok: true, exploration: readAiExplorationSession(scoped.root, url.searchParams.get("id") || "") });
3984
+ } catch (e) {
3985
+ json(res, /Invalid exploration|ENOENT/.test(String(e?.message || e)) ? 404 : 500, { error: (e && e.message) || String(e) });
3986
+ }
3987
+ return;
3988
+ }
3989
+
3990
+ if (req.method === "POST" && url.pathname === "/api/workspace/exploration") {
3991
+ let payload;
3992
+ try { payload = JSON.parse(await readBody(req)); } catch {
3993
+ json(res, 400, { error: "Invalid JSON body" });
3994
+ return;
3995
+ }
3996
+ try {
3997
+ const scoped = resolveWorkspaceScopeRoot(root, {
3998
+ flowId: payload.flowId || "",
3999
+ flowSource: payload.flowSource || "user",
4000
+ adminOwnerId: payload.adminOwnerId || "",
4001
+ archived: payload.archived === true || payload.flowArchived === true,
4002
+ }, userCtx);
4003
+ if (scoped.error) {
4004
+ json(res, 400, { error: scoped.error });
4005
+ return;
4006
+ }
4007
+ if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource) || scoped.collaborationAccess?.writable === false) {
4008
+ json(res, 403, { error: "Exploration write permission denied" });
4009
+ return;
4010
+ }
4011
+ const exploration = createAiExplorationSession(scoped.root, {
4012
+ title: payload.title,
4013
+ goal: payload.goal,
4014
+ mode: payload.mode || "observed",
4015
+ status: payload.status || "running",
4016
+ source: payload.source || { provider: "external", agent: "custom" },
4017
+ });
4018
+ json(res, 201, {
4019
+ ok: true,
4020
+ exploration,
4021
+ ingest: {
4022
+ method: "POST",
4023
+ path: "/api/workspace/exploration/events",
4024
+ body: {
4025
+ flowId: payload.flowId || "",
4026
+ flowSource: payload.flowSource || "user",
4027
+ id: exploration.id,
4028
+ events: [],
4029
+ },
4030
+ },
4031
+ });
4032
+ } catch (e) {
4033
+ json(res, 500, { error: (e && e.message) || String(e) });
4034
+ }
4035
+ return;
4036
+ }
4037
+
4038
+ if (req.method === "POST" && url.pathname === "/api/workspace/exploration/events") {
4039
+ let payload;
4040
+ try { payload = JSON.parse(await readBody(req)); } catch {
4041
+ json(res, 400, { error: "Invalid JSON body" });
4042
+ return;
4043
+ }
4044
+ try {
4045
+ const scoped = resolveWorkspaceScopeRoot(root, {
4046
+ flowId: payload.flowId || "",
4047
+ flowSource: payload.flowSource || "user",
4048
+ adminOwnerId: payload.adminOwnerId || "",
4049
+ archived: payload.archived === true || payload.flowArchived === true,
4050
+ }, userCtx);
4051
+ if (scoped.error) {
4052
+ json(res, 400, { error: scoped.error });
4053
+ return;
4054
+ }
4055
+ if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource) || scoped.collaborationAccess?.writable === false) {
4056
+ json(res, 403, { error: "Exploration write permission denied" });
4057
+ return;
4058
+ }
4059
+ const appended = appendAiTraceEvents(scoped.root, payload.id, payload.events || [], {
4060
+ phase: payload.phase || "observed",
4061
+ });
4062
+ if (payload.status || payload.summary) {
4063
+ appended.session = updateAiExplorationSession(scoped.root, payload.id, {
4064
+ ...(payload.status ? { status: payload.status } : {}),
4065
+ ...(payload.summary ? { summary: payload.summary } : {}),
4066
+ });
4067
+ }
4068
+ json(res, 200, { ok: true, ...appended });
4069
+ } catch (e) {
4070
+ json(res, /Invalid exploration|ENOENT/.test(String(e?.message || e)) ? 404 : 400, { error: (e && e.message) || String(e) });
4071
+ }
4072
+ return;
4073
+ }
4074
+
4075
+ if (req.method === "POST" && url.pathname === "/api/workspace/exploration/plan") {
4076
+ let payload;
4077
+ try { payload = JSON.parse(await readBody(req)); } catch {
4078
+ json(res, 400, { error: "Invalid JSON body" });
4079
+ return;
4080
+ }
4081
+ const goal = String(payload?.goal || "").trim();
4082
+ if (!goal) {
4083
+ json(res, 400, { error: "Missing exploration goal" });
4084
+ return;
4085
+ }
4086
+ let scoped;
4087
+ let exploration;
4088
+ try {
4089
+ scoped = resolveWorkspaceScopeRoot(root, {
4090
+ flowId: payload.flowId || "",
4091
+ flowSource: payload.flowSource || "user",
4092
+ adminOwnerId: payload.adminOwnerId || "",
4093
+ archived: payload.archived === true || payload.flowArchived === true,
4094
+ }, userCtx);
4095
+ if (scoped.error) {
4096
+ json(res, 400, { error: scoped.error });
4097
+ return;
4098
+ }
4099
+ if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource) || scoped.collaborationAccess?.writable === false) {
4100
+ json(res, 403, { error: "Exploration write permission denied" });
4101
+ return;
4102
+ }
4103
+ const graphRead = readWorkspaceGraph(scoped.root);
4104
+ const graph = graphRead?.graph || graphRead;
4105
+ const workspaceSource = workspaceGraphAsSource(graph);
4106
+ exploration = createAiExplorationSession(scoped.root, {
4107
+ title: payload.title || goal.slice(0, 80),
4108
+ goal,
4109
+ mode: "planned",
4110
+ status: "planning",
4111
+ source: { provider: "agentflow", agent: String(payload.model || "default") },
4112
+ });
4113
+ const chunks = [];
4114
+ let result = "";
4115
+ const handle = startComposerAgent({
4116
+ uiWorkspaceRoot: scoped.root,
4117
+ cliWorkspace: scoped.root,
4118
+ prompt: aiPlanPrompt({ goal, workspaceSource }),
4119
+ modelKey: typeof payload.model === "string" ? payload.model.trim() : "",
4120
+ agentflowUserId: userCtx.userId || "",
4121
+ mode: "plan",
4122
+ force: false,
4123
+ sandboxDisabled: false,
4124
+ approveMcps: false,
4125
+ sandboxMode: "read-only",
4126
+ allowDanger: false,
4127
+ includeJsonResult: true,
4128
+ onStreamEvent(ev) {
4129
+ if (ev?.type !== "natural" || typeof ev.text !== "string") return;
4130
+ if (ev.kind === "result") result = ev.text.trim();
4131
+ else if (ev.kind === "assistant" && ev.text.trim()) chunks.push(ev.text.trim());
4132
+ },
4133
+ });
4134
+ await handle.finished;
4135
+ const plan = parseAiPlanResult(result || chunks.at(-1) || chunks.join("\n"), exploration.id);
4136
+ const appended = appendAiTraceEvents(scoped.root, exploration.id, plan.events, { phase: "planned" });
4137
+ const ready = updateAiExplorationSession(scoped.root, exploration.id, {
4138
+ title: plan.title,
4139
+ summary: plan.summary,
4140
+ status: "ready",
4141
+ eventCount: appended.session.eventCount,
4142
+ });
4143
+ json(res, 201, { ok: true, exploration: { ...ready, events: appended.events } });
4144
+ } catch (e) {
4145
+ if (scoped?.root && exploration?.id) {
4146
+ try { updateAiExplorationSession(scoped.root, exploration.id, { status: "failed", summary: String(e?.message || e) }); } catch {}
4147
+ }
4148
+ json(res, 500, { error: (e && e.message) || String(e), ...(exploration?.id ? { explorationId: exploration.id } : {}) });
4149
+ }
4150
+ return;
4151
+ }
4152
+
4153
+ if (req.method === "POST" && url.pathname === "/api/workspace/exploration/dry-run") {
4154
+ let payload;
4155
+ try { payload = JSON.parse(await readBody(req)); } catch {
4156
+ json(res, 400, { error: "Invalid JSON body" });
4157
+ return;
4158
+ }
4159
+ try {
4160
+ const scoped = resolveWorkspaceScopeRoot(root, {
4161
+ flowId: payload.flowId || "",
4162
+ flowSource: payload.flowSource || "user",
4163
+ adminOwnerId: payload.adminOwnerId || "",
4164
+ archived: payload.archived === true || payload.flowArchived === true,
4165
+ }, userCtx);
4166
+ if (scoped.error) {
4167
+ json(res, 400, { error: scoped.error });
4168
+ return;
4169
+ }
4170
+ if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource) || scoped.collaborationAccess?.writable === false) {
4171
+ json(res, 403, { error: "Exploration dry-run permission denied" });
4172
+ return;
4173
+ }
4174
+ const exploration = readAiExplorationSession(scoped.root, payload.id);
4175
+ const planned = exploration.events.filter((event) => event.phase === "planned");
4176
+ if (!planned.length) {
4177
+ json(res, 400, { error: "Exploration has no planned spans" });
4178
+ return;
4179
+ }
4180
+ const runToken = Date.now().toString(36);
4181
+ const simulated = planned.map((event, index) => {
4182
+ const blocked = event.requiresApproval === true || ["write", "external"].includes(event.sideEffect);
4183
+ return {
4184
+ id: `dry_${runToken}_${index + 1}`,
4185
+ spanId: `dry_${runToken}_${event.spanId}`,
4186
+ parentSpanId: event.parentSpanId ? `dry_${runToken}_${event.parentSpanId}` : "",
4187
+ phase: "simulated",
4188
+ type: event.type,
4189
+ name: event.name,
4190
+ summary: blocked
4191
+ ? `策略预检已阻断:${event.summary || event.name}。真实执行前需要明确授权。`
4192
+ : `策略预检通过:${event.summary || event.name}。本次未执行真实工具。`,
4193
+ sideEffect: event.sideEffect,
4194
+ requiresApproval: blocked,
4195
+ status: blocked ? "blocked" : "success",
4196
+ inputPreview: event.inputPreview || "",
4197
+ outputPreview: blocked ? "副作用未执行" : "约束检查通过,未执行真实工具",
4198
+ };
4199
+ });
4200
+ const appended = appendAiTraceEvents(scoped.root, exploration.id, simulated, { phase: "simulated" });
4201
+ const blockedCount = appended.events.filter((event) => event.status === "blocked").length;
4202
+ const updated = updateAiExplorationSession(scoped.root, exploration.id, {
4203
+ mode: "simulated",
4204
+ status: "ready",
4205
+ summary: `Dry-run 策略预检完成:${appended.events.length - blockedCount} 项通过,${blockedCount} 项等待授权。`,
4206
+ eventCount: appended.session.eventCount,
4207
+ });
4208
+ json(res, 200, {
4209
+ ok: true,
4210
+ exploration: { ...updated, events: [...exploration.events, ...appended.events] },
4211
+ dryRun: { kind: "policy-check", executedTools: false, blockedCount },
4212
+ });
4213
+ } catch (e) {
4214
+ json(res, 500, { error: (e && e.message) || String(e) });
4215
+ }
4216
+ return;
4217
+ }
4218
+
4219
+ if (req.method === "POST" && url.pathname === "/api/workspace/exploration/materialize") {
4220
+ let payload;
4221
+ try { payload = JSON.parse(await readBody(req)); } catch {
4222
+ json(res, 400, { error: "Invalid JSON body" });
4223
+ return;
4224
+ }
4225
+ let scoped;
4226
+ let exploration;
4227
+ let materializeRunId = "";
4228
+ try {
4229
+ scoped = resolveWorkspaceScopeRoot(root, {
4230
+ flowId: payload.flowId || "",
4231
+ flowSource: payload.flowSource || "user",
4232
+ adminOwnerId: payload.adminOwnerId || "",
4233
+ archived: payload.archived === true || payload.flowArchived === true,
4234
+ }, userCtx);
4235
+ if (scoped.error) {
4236
+ json(res, 400, { error: scoped.error });
4237
+ return;
4238
+ }
4239
+ if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource) || scoped.collaborationAccess?.writable === false) {
4240
+ json(res, 403, { error: "Exploration materialization permission denied" });
4241
+ return;
4242
+ }
4243
+ exploration = readAiExplorationSession(scoped.root, payload.id);
4244
+ if (!exploration.events.length) {
4245
+ json(res, 400, { error: "Exploration has no trace events" });
4246
+ return;
4247
+ }
4248
+ const materializableEvents = materializableAiTraceEvents(exploration);
4249
+ if (!materializableEvents.length) {
4250
+ json(res, 400, { error: "Exploration has no planned or observed spans to materialize" });
4251
+ return;
4252
+ }
4253
+ const dangerousEvents = materializableEvents.filter((event) => event.requiresApproval || ["write", "external"].includes(event.sideEffect));
4254
+ if (dangerousEvents.length && payload.approveSideEffects !== true) {
4255
+ json(res, 409, {
4256
+ error: "Side-effect review is required before materialization",
4257
+ sideEffects: dangerousEvents.map((event) => ({ spanId: event.spanId, name: event.name, sideEffect: event.sideEffect })),
4258
+ });
4259
+ return;
4260
+ }
4261
+ const beforeGraphRead = readWorkspaceGraph(scoped.root);
4262
+ const beforeGraph = beforeGraphRead?.graph || beforeGraphRead;
4263
+ const beforeIds = new Set([
4264
+ ...(beforeGraph.nodes || []).map((node) => String(node.id || "")),
4265
+ ...Object.keys(beforeGraph.instances || {}),
4266
+ ].filter(Boolean));
4267
+ materializeRunId = `materialize_${Date.now().toString(36)}`;
4268
+ appendAiTraceEvents(scoped.root, exploration.id, [{
4269
+ id: `${materializeRunId}_start`,
4270
+ spanId: materializeRunId,
4271
+ type: "agent",
4272
+ name: "固化 Agent",
4273
+ summary: "开始把已审核 Trace 转换为 Workspace DSL 调整态",
4274
+ status: "running",
4275
+ phase: "observed",
4276
+ sideEffect: "write",
4277
+ }], { phase: "observed" });
4278
+ updateAiExplorationSession(scoped.root, exploration.id, { mode: "observed", status: "running" });
4279
+ let reply = "";
4280
+ let toolIndex = 0;
4281
+ const handle = startComposerAgent({
4282
+ uiWorkspaceRoot: scoped.root,
4283
+ cliWorkspace: scoped.root,
4284
+ prompt: aiMaterializationPrompt(exploration, workspaceGraphAsSource(beforeGraph)),
4285
+ modelKey: typeof payload.model === "string" ? payload.model.trim() : "",
4286
+ agentflowUserId: userCtx.userId || "",
4287
+ force: true,
4288
+ sandboxDisabled: false,
4289
+ approveMcps: false,
4290
+ sandboxMode: "workspace-write",
4291
+ allowDanger: false,
4292
+ onToolCall(subtype, toolName) {
4293
+ toolIndex += 1;
4294
+ try {
4295
+ const toolStatus = /(?:complete|success|done)/i.test(String(subtype || ""))
4296
+ ? "success"
4297
+ : /(?:error|fail)/i.test(String(subtype || "")) ? "error" : "running";
4298
+ const sideEffect = classifyAiToolSideEffect(toolName, subtype);
4299
+ appendAiTraceEvents(scoped.root, exploration.id, [{
4300
+ id: `${materializeRunId}_tool_${toolIndex}`,
4301
+ spanId: `${materializeRunId}_tool_${toolIndex}`,
4302
+ parentSpanId: materializeRunId,
4303
+ type: String(subtype || "").toLowerCase() === "thinking" ? "decision" : "tool",
4304
+ name: String(toolName || subtype || "Agent tool"),
4305
+ summary: subtype ? `Agent 工具事件:${subtype}` : "Agent 工具调用",
4306
+ status: toolStatus,
4307
+ phase: "observed",
4308
+ sideEffect,
4309
+ }], { phase: "observed" });
4310
+ } catch {
4311
+ // Trace persistence must not interrupt an in-flight materialization.
4312
+ }
4313
+ },
4314
+ onStreamEvent(ev) {
4315
+ if (ev?.type === "natural" && ["assistant", "result"].includes(ev.kind) && typeof ev.text === "string" && ev.text.trim()) {
4316
+ reply = ev.text.trim();
4317
+ }
4318
+ },
4319
+ });
4320
+ await handle.finished;
4321
+ appendAiTraceEvents(scoped.root, exploration.id, [{
4322
+ id: `${materializeRunId}_finish`,
4323
+ spanId: materializeRunId,
4324
+ type: "agent",
4325
+ name: "固化 Agent",
4326
+ summary: reply || "Workspace DSL 调整态生成完成",
4327
+ status: "success",
4328
+ phase: "observed",
4329
+ sideEffect: "write",
4330
+ endedAt: new Date().toISOString(),
4331
+ }], { phase: "observed" });
4332
+ const graphRead = readWorkspaceGraph(scoped.root);
4333
+ const graph = graphRead?.graph || graphRead;
4334
+ const graphIds = [
4335
+ ...(graph.nodes || []).map((node) => String(node.id || "")),
4336
+ ...Object.keys(graph.instances || {}),
4337
+ ].filter(Boolean);
4338
+ const nodeIds = [...new Set(graphIds)].filter((id) => !beforeIds.has(id));
4339
+ const materialization = writeAiExplorationMaterialization(scoped.root, exploration.id, {
4340
+ nodeIds,
4341
+ designRevision: workspaceDesignRevision(graph),
4342
+ });
4343
+ appendAiTraceEvents(scoped.root, exploration.id, [{
4344
+ type: "status",
4345
+ name: "固化为 Workspace DSL",
4346
+ summary: reply || `新增 ${nodeIds.length} 个节点`,
4347
+ status: "success",
4348
+ phase: "materialized",
4349
+ sideEffect: "write",
4350
+ artifacts: [{ kind: "dsl", path: "workspace.flow.js", label: "Workspace DSL" }],
4351
+ }], { phase: "materialized" });
4352
+ const updated = updateAiExplorationSession(scoped.root, exploration.id, {
4353
+ mode: "materialized",
4354
+ status: "completed",
4355
+ materializedAt: materialization.materializedAt,
4356
+ });
4357
+ json(res, 200, { ok: true, exploration: updated, materialization, graph, reply });
4358
+ } catch (e) {
4359
+ if (scoped?.root && exploration?.id) {
4360
+ try {
4361
+ if (materializeRunId) {
4362
+ appendAiTraceEvents(scoped.root, exploration.id, [{
4363
+ id: `${materializeRunId}_error`,
4364
+ spanId: materializeRunId,
4365
+ type: "agent",
4366
+ name: "固化 Agent",
4367
+ summary: String(e?.message || e),
4368
+ status: "error",
4369
+ phase: "observed",
4370
+ sideEffect: "write",
4371
+ endedAt: new Date().toISOString(),
4372
+ }], { phase: "observed" });
4373
+ }
4374
+ updateAiExplorationSession(scoped.root, exploration.id, { mode: "observed", status: "failed" });
4375
+ } catch {}
4376
+ }
4377
+ json(res, 500, { error: (e && e.message) || String(e) });
4378
+ }
4379
+ return;
4380
+ }
4381
+
4035
4382
  if (url.pathname === "/api/workspace/conversations") {
4036
4383
  let payload = {};
4037
4384
  if (req.method === "POST") {
@@ -4478,6 +4825,7 @@ async function workspaceRoutes(req, res, ctx) {
4478
4825
  json(res, result.conflict ? 409 : 400, { error: result.error || "发布失败" });
4479
4826
  return;
4480
4827
  }
4828
+ markRepositoryIndexDirty(root);
4481
4829
  json(res, 200, { ok: true, ...result });
4482
4830
  } catch (e) {
4483
4831
  json(res, 500, { error: (e && e.message) || String(e) });