@fieldwangai/agentflow 0.1.166 → 0.1.167

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,8 +11,21 @@
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";
17
30
  import { execFileBuffered } from "./exec-buffered.mjs";
18
31
  import { runGit } from "./git-worktree.mjs";
@@ -30,28 +43,36 @@ import {
30
43
  } from "./marketplace.mjs";
31
44
  import {
32
45
  appendMarketplaceUsageEvent,
33
- marketplaceStatsFor,
34
- marketplaceUsageStats,
35
46
  marketplaceResourcesForRun,
36
- readMarketplaceFlowOrigin,
37
47
  writeMarketplaceFlowOrigin,
38
48
  } from "./marketplace-usage.mjs";
49
+ import {
50
+ indexedProjectFlowPreview,
51
+ getRepositoryIndex,
52
+ indexedMarketplaceFlowCopies,
53
+ listIndexedNodes,
54
+ listIndexedProjectFlows,
55
+ markRepositoryIndexDirty,
56
+ updateIndexedProjectFlowVisibility,
57
+ updateIndexedNodeVisibility,
58
+ writeProjectFlowMarketplaceMetadata,
59
+ } from "./repository-index.mjs";
39
60
  import { NODE_PACKAGE_ENTRY, nodePackageExportsRun, readNodePackageManifest } from "./node-package-manifest.mjs";
40
61
  import { inspectNodePackageDirectory } from "./node-package-archive.mjs";
41
62
  import { json, readBody } from "./http-util.mjs";
42
63
  import { log } from "./log.mjs";
43
- import { PACKAGE_ROOT, PIPELINES_DIR, getAgentflowUserDataRoot, getUserPipelinesRoot, listAgentflowUserIds } from "./paths.mjs";
64
+ import { PACKAGE_ROOT, PIPELINES_DIR, getAgentflowUserDataRoot, getUserPipelinesRoot } from "./paths.mjs";
44
65
  import { validateUserPipelineId } from "./flow-write.mjs";
45
66
  import { runLedgerId } from "./run-ledger.mjs";
46
67
  import { getTeamById, getTeamForUser } from "./teams.mjs";
47
68
  import { readMergedEnvObject, runtimeEnvForUser } from "./user-env.mjs";
48
- import { acceptWorkspaceCollaborationInvite, addWorkspaceCollaborationMember, deleteWorkspaceCollaborationForFlow, ensureWorkspaceCollaboration, getWorkspaceCollaborationByFlow, getWorkspaceCollaborationForProject, listWorkspaceCollaborationsForUser, removeWorkspaceCollaborationMember, removeWorkspaceCollaborationTeamShare, setWorkspaceCollaborationTeamShare, workspaceCollaborationAccess } from "./workspace-collaboration.mjs";
69
+ import { acceptWorkspaceCollaborationInvite, addWorkspaceCollaborationMember, deleteWorkspaceCollaborationForFlow, ensureWorkspaceCollaboration, getWorkspaceCollaborationForProject, listWorkspaceCollaborationsForUser, removeWorkspaceCollaborationMember, removeWorkspaceCollaborationTeamShare, setWorkspaceCollaborationTeamShare, workspaceCollaborationAccess } from "./workspace-collaboration.mjs";
49
70
  import { WorkspaceFlowParseError } from "./workspace-flow-store.mjs";
50
71
  import { mergeWorkspaceGraphs, workspaceDesignRevision, workspaceRuntimeRevision } from "./workspace-graph-merge.mjs";
51
72
  import { DEFAULT_WORKSPACE_PREVIEW_TTL_MS, createWorkspacePreviewId, normalizeWorkspacePreviewTtlMs, readWorkspacePreviewMetadata, workspaceSharedPreviewFlowDir, writeWorkspacePreviewMetadata } from "./workspace-preview.mjs";
52
73
  import { DEFAULT_WORKSPACE_DRAFT_TTL_MS, createWorkspaceDraftId, normalizeWorkspaceDraftTtlMs, readWorkspaceDraftMetadata, workspaceDraftFlowDir, writeWorkspaceDraftMetadata } from "./workspace-draft.mjs";
53
74
  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";
75
+ 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
76
  import { splitWorkspaceGraph, WORKSPACE_STATE_FILENAME } from "./workspace-state.mjs";
56
77
  import { getWorkspaceTree } from "./workspace-tree.mjs";
57
78
  import busboy from "busboy";
@@ -1076,196 +1097,8 @@ function workspaceBufferLooksLikeZip(buf) {
1076
1097
  );
1077
1098
  }
1078
1099
 
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
1100
  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;
1101
+ return listIndexedProjectFlows(workspaceRoot, userCtx, scope);
1269
1102
  }
1270
1103
 
1271
1104
  function publicProjectFlowMarketplaceResource(resource) {
@@ -1295,6 +1128,20 @@ function sortMarketplaceResources(items) {
1295
1128
  ));
1296
1129
  }
1297
1130
 
1131
+ function paginateMarketplaceResources(items, url, defaultLimit = 24) {
1132
+ const requestedLimit = Number(url.searchParams.get("limit") || defaultLimit);
1133
+ const limit = Math.min(100, Math.max(1, Number.isFinite(requestedLimit) ? Math.floor(requestedLimit) : defaultLimit));
1134
+ const requestedCursor = Number(url.searchParams.get("cursor") || 0);
1135
+ const offset = Math.max(0, Number.isFinite(requestedCursor) ? Math.floor(requestedCursor) : 0);
1136
+ const pageItems = items.slice(offset, offset + limit);
1137
+ const nextOffset = offset + pageItems.length;
1138
+ return {
1139
+ items: pageItems,
1140
+ total: items.length,
1141
+ nextCursor: nextOffset < items.length ? String(nextOffset) : "",
1142
+ };
1143
+ }
1144
+
1298
1145
  /**
1299
1146
  * @param {import('http').IncomingMessage} req
1300
1147
  * @param {import('http').ServerResponse} res
@@ -1357,6 +1204,7 @@ async function workspaceRoutes(req, res, ctx) {
1357
1204
  }
1358
1205
  try {
1359
1206
  const result = publishNodePackageArchive(root, parsed.file, { ownerUserId: userCtx.userId });
1207
+ if (result.ok) markRepositoryIndexDirty(root);
1360
1208
  json(res, result.ok ? (result.alreadyExists ? 200 : 201) : (result.conflict ? 409 : 400), result);
1361
1209
  } catch (e) {
1362
1210
  json(res, 500, { ok: false, error: (e && e.message) || String(e) });
@@ -1389,6 +1237,7 @@ async function workspaceRoutes(req, res, ctx) {
1389
1237
  actorUserId: userCtx.userId || "",
1390
1238
  eventId: `install:node:${id}@${version}:${userCtx.userId || "anonymous"}`,
1391
1239
  });
1240
+ markRepositoryIndexDirty(root);
1392
1241
  res.writeHead(200, {
1393
1242
  "Content-Type": "application/zip",
1394
1243
  "Content-Length": result.archive.length,
@@ -1412,7 +1261,7 @@ async function workspaceRoutes(req, res, ctx) {
1412
1261
  const marketplaceScope = scope === "owned" ? "owned" : "all";
1413
1262
  const queryText = String(url.searchParams.get("q") || "").trim().toLowerCase();
1414
1263
  if (kind === "node") {
1415
- const marketplaceNodes = listMarketplacePackages(root, { ...userCtx, marketplaceScope }).nodes
1264
+ const marketplaceNodes = listIndexedNodes(root, userCtx, marketplaceScope)
1416
1265
  .map((node) => ({ ...node, resourceType: "node", installed: true }));
1417
1266
  let nodes = marketplaceNodes;
1418
1267
  if (scope === "installed") {
@@ -1443,14 +1292,23 @@ async function workspaceRoutes(req, res, ctx) {
1443
1292
  });
1444
1293
  }
1445
1294
  nodes = sortMarketplaceResources(nodes.filter((item) => marketplaceResourceMatches(item, queryText)));
1446
- json(res, 200, { kind, scope, sort: "useCount", order: "desc", items: nodes });
1295
+ const page = paginateMarketplaceResources(nodes, url);
1296
+ const index = getRepositoryIndex(root);
1297
+ json(res, 200, {
1298
+ kind,
1299
+ scope,
1300
+ sort: "useCount",
1301
+ order: "desc",
1302
+ ...page,
1303
+ index: { status: "ready", generatedAt: index.generatedAt },
1304
+ });
1447
1305
  return;
1448
1306
  }
1449
1307
  if (kind !== "flow") {
1450
1308
  json(res, 400, { error: "kind must be flow or node" });
1451
1309
  return;
1452
1310
  }
1453
- const installedCopies = installedMarketplaceFlowCopies(userCtx.userId);
1311
+ const installedCopies = indexedMarketplaceFlowCopies(root, userCtx.userId);
1454
1312
  const projectFlows = listRunnableProjectMarketplaceFlows(root, userCtx, scope).map((flow) => {
1455
1313
  const installedFlowIds = installedCopies.get(`${flow.id}@${flow.version}`) || [];
1456
1314
  return publicProjectFlowMarketplaceResource({
@@ -1464,7 +1322,16 @@ async function workspaceRoutes(req, res, ctx) {
1464
1322
  .filter((item) => scope !== "installed" || item.installed)
1465
1323
  .filter((item) => marketplaceResourceMatches(item, queryText)),
1466
1324
  );
1467
- json(res, 200, { kind, scope, sort: "useCount", order: "desc", items });
1325
+ const page = paginateMarketplaceResources(items, url);
1326
+ const index = getRepositoryIndex(root);
1327
+ json(res, 200, {
1328
+ kind,
1329
+ scope,
1330
+ sort: "useCount",
1331
+ order: "desc",
1332
+ ...page,
1333
+ index: { status: "ready", generatedAt: index.generatedAt },
1334
+ });
1468
1335
  } catch (e) {
1469
1336
  json(res, 500, { error: (e && e.message) || String(e) });
1470
1337
  }
@@ -1517,7 +1384,7 @@ async function workspaceRoutes(req, res, ctx) {
1517
1384
  return;
1518
1385
  }
1519
1386
  try {
1520
- const installedCopies = installedMarketplaceFlowCopies(userCtx.userId);
1387
+ const installedCopies = indexedMarketplaceFlowCopies(root, userCtx.userId);
1521
1388
  if (projectFlow) {
1522
1389
  const source = listRunnableProjectMarketplaceFlows(root, userCtx, "all")
1523
1390
  .find((flow) => flow.id === id);
@@ -1529,13 +1396,19 @@ async function workspaceRoutes(req, res, ctx) {
1529
1396
  json(res, 409, { error: "该流程已有新版本,请刷新流程仓库后重试" });
1530
1397
  return;
1531
1398
  }
1399
+ const preview = indexedProjectFlowPreview(root, source);
1400
+ if (!preview || preview.stale) {
1401
+ markRepositoryIndexDirty(root);
1402
+ json(res, preview?.stale ? 409 : 404, { error: preview?.stale ? "该流程已有新版本,请刷新流程仓库后重试" : "Project flow preview is unavailable" });
1403
+ return;
1404
+ }
1532
1405
  json(res, 200, {
1533
1406
  flow: {
1534
1407
  ...publicProjectFlowMarketplaceResource(source),
1535
1408
  owned: source.ownerUserId === userCtx.userId,
1536
1409
  installedFlowIds: installedCopies.get(`${id}@${version}`) || [],
1537
1410
  },
1538
- graph: source._graph,
1411
+ graph: preview.graph,
1539
1412
  });
1540
1413
  return;
1541
1414
  }
@@ -1580,11 +1453,11 @@ async function workspaceRoutes(req, res, ctx) {
1580
1453
  return;
1581
1454
  }
1582
1455
  try {
1583
- const installedCopies = installedMarketplaceFlowCopies(userCtx.userId);
1456
+ const installedCopies = indexedMarketplaceFlowCopies(root, userCtx.userId);
1584
1457
  let graph;
1585
1458
  let resource;
1586
1459
  if (previewKind === "node") {
1587
- const node = listMarketplacePackages(root, { ...userCtx, marketplaceScope: "all" }).nodes
1460
+ const node = listIndexedNodes(root, userCtx, "all")
1588
1461
  .find((item) => item.id === id && item.version === version);
1589
1462
  if (!node) {
1590
1463
  json(res, 404, { error: "Node package not found or is private" });
@@ -1629,7 +1502,13 @@ async function workspaceRoutes(req, res, ctx) {
1629
1502
  json(res, 409, { error: "该流程已有新版本,请刷新流程仓库后重试" });
1630
1503
  return;
1631
1504
  }
1632
- graph = source._graph;
1505
+ const preview = indexedProjectFlowPreview(root, source);
1506
+ if (!preview || preview.stale) {
1507
+ markRepositoryIndexDirty(root);
1508
+ json(res, preview?.stale ? 409 : 404, { error: preview?.stale ? "该流程已有新版本,请刷新流程仓库后重试" : "Project flow preview is unavailable" });
1509
+ return;
1510
+ }
1511
+ graph = preview.graph;
1633
1512
  resource = {
1634
1513
  ...publicProjectFlowMarketplaceResource(source),
1635
1514
  owned: source.ownerUserId === userCtx.userId,
@@ -1746,6 +1625,7 @@ async function workspaceRoutes(req, res, ctx) {
1746
1625
  visibility: payload?.visibility || "public",
1747
1626
  graph,
1748
1627
  }, userCtx);
1628
+ if (result.ok) markRepositoryIndexDirty(root);
1749
1629
  json(res, result.ok ? (result.alreadyExists ? 200 : 201) : (result.conflict ? 409 : 400), result);
1750
1630
  } catch (e) {
1751
1631
  json(res, 500, { error: (e && e.message) || String(e) });
@@ -1789,7 +1669,13 @@ async function workspaceRoutes(req, res, ctx) {
1789
1669
  json(res, 409, { error: "该流程已有新版本,请刷新流程仓库后重试" });
1790
1670
  return;
1791
1671
  }
1792
- sourceGraph = source._graph;
1672
+ const preview = indexedProjectFlowPreview(root, source);
1673
+ if (!preview || preview.stale) {
1674
+ markRepositoryIndexDirty(root);
1675
+ json(res, preview?.stale ? 409 : 404, { error: preview?.stale ? "该流程已有新版本,请刷新流程仓库后重试" : "Project flow preview is unavailable" });
1676
+ return;
1677
+ }
1678
+ sourceGraph = preview.graph;
1793
1679
  originKind = "project-flow";
1794
1680
  } else {
1795
1681
  const source = readMarketplaceFlow(root, id, version, { ...userCtx, marketplaceScope: "all" });
@@ -1811,6 +1697,7 @@ async function workspaceRoutes(req, res, ctx) {
1811
1697
  actorUserId: userCtx.userId || "",
1812
1698
  eventId: `install:${originKind}:${id}@${version}:${userCtx.userId || "anonymous"}`,
1813
1699
  });
1700
+ markRepositoryIndexDirty(root);
1814
1701
  json(res, 201, {
1815
1702
  ok: true,
1816
1703
  id,
@@ -1846,17 +1733,27 @@ async function workspaceRoutes(req, res, ctx) {
1846
1733
  return;
1847
1734
  }
1848
1735
  const metadata = writeProjectFlowMarketplaceMetadata(source._flowRoot, payload?.visibility);
1736
+ updateIndexedProjectFlowVisibility(root, id, metadata.visibility, metadata.updatedAt);
1849
1737
  json(res, 200, { ok: true, kind: "project-flow", id, version: source.version, ...metadata });
1850
1738
  return;
1851
1739
  }
1740
+ const resourceKind = String(payload?.kind || "");
1741
+ const resourceId = String(payload?.id || "");
1742
+ const resourceVersion = String(payload?.version || "");
1743
+ const resourceVisibility = String(payload?.visibility || "public");
1852
1744
  const result = setMarketplaceVisibility(
1853
1745
  root,
1854
- String(payload?.kind || ""),
1855
- String(payload?.id || ""),
1856
- String(payload?.version || ""),
1857
- String(payload?.visibility || "public"),
1746
+ resourceKind,
1747
+ resourceId,
1748
+ resourceVersion,
1749
+ resourceVisibility,
1858
1750
  userCtx,
1859
1751
  );
1752
+ if (result.ok && resourceKind === "node") {
1753
+ updateIndexedNodeVisibility(root, resourceId, resourceVersion, resourceVisibility);
1754
+ } else if (result.ok) {
1755
+ markRepositoryIndexDirty(root);
1756
+ }
1860
1757
  json(res, result.ok ? 200 : 400, result);
1861
1758
  } catch (e) {
1862
1759
  json(res, 500, { error: (e && e.message) || String(e) });
@@ -2391,6 +2288,7 @@ async function workspaceRoutes(req, res, ctx) {
2391
2288
  const graph = workspaceGraphWithScheduleMode(draftGraph, payload.scheduleMode || "disabled");
2392
2289
  fs.mkdirSync(targetDir, { recursive: true });
2393
2290
  writeWorkspaceGraph(targetDir, graph, root);
2291
+ markRepositoryIndexDirty(root);
2394
2292
  let collaborationCreated = false;
2395
2293
  if (flowSource === "workspace") {
2396
2294
  ensureWorkspaceCollaboration({ flowId, userId: authUser.userId });
@@ -2674,6 +2572,7 @@ async function workspaceRoutes(req, res, ctx) {
2674
2572
  json(res, result.conflict ? 409 : 400, result);
2675
2573
  return;
2676
2574
  }
2575
+ markRepositoryIndexDirty(root);
2677
2576
  const graph = readWorkspaceGraph(scoped.root, root).graph;
2678
2577
  const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx);
2679
2578
  broadcastWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
@@ -2722,6 +2621,7 @@ async function workspaceRoutes(req, res, ctx) {
2722
2621
  json(res, 404, result);
2723
2622
  return;
2724
2623
  }
2624
+ markRepositoryIndexDirty(root);
2725
2625
  const graph = readWorkspaceGraph(scoped.root, root).graph;
2726
2626
  const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx);
2727
2627
  broadcastWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
@@ -2849,6 +2749,7 @@ async function workspaceRoutes(req, res, ctx) {
2849
2749
  }
2850
2750
  throw new Error(`Workspace graph save rolled back: ${(scheduleError && scheduleError.message) || String(scheduleError)}`);
2851
2751
  }
2752
+ markRepositoryIndexDirty(root);
2852
2753
  broadcastWorkspaceCollaborationEvent(
2853
2754
  userCtx,
2854
2755
  scoped.flowSource,
@@ -2960,6 +2861,7 @@ async function workspaceRoutes(req, res, ctx) {
2960
2861
  graph.instances[scheduleNodeId] = { ...instance, body: JSON.stringify(config) };
2961
2862
  const committed = commitWorkspaceGraph(root, scoped, graph, userCtx);
2962
2863
  const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, committed.graph, authUser, userCtx);
2864
+ markRepositoryIndexDirty(root);
2963
2865
  const schedule = workspaceSchedules.find((item) => item.scheduleNodeId === scheduleNodeId) || {
2964
2866
  flowId,
2965
2867
  flowSource,
@@ -4032,6 +3934,436 @@ async function workspaceRoutes(req, res, ctx) {
4032
3934
  return;
4033
3935
  }
4034
3936
 
3937
+ if (req.method === "GET" && url.pathname === "/api/workspace/explorations") {
3938
+ try {
3939
+ const scoped = resolveWorkspaceScopeRoot(root, {
3940
+ flowId: url.searchParams.get("flowId") || "",
3941
+ flowSource: url.searchParams.get("flowSource") || "user",
3942
+ adminOwnerId: url.searchParams.get("adminOwnerId") || "",
3943
+ archived: url.searchParams.get("archived") === "1" || url.searchParams.get("flowArchived") === "1",
3944
+ }, userCtx);
3945
+ if (scoped.error) {
3946
+ json(res, 400, { error: scoped.error });
3947
+ return;
3948
+ }
3949
+ json(res, 200, { ok: true, explorations: listAiExplorationSessions(scoped.root) });
3950
+ } catch (e) {
3951
+ json(res, 500, { error: (e && e.message) || String(e) });
3952
+ }
3953
+ return;
3954
+ }
3955
+
3956
+ if (req.method === "GET" && url.pathname === "/api/workspace/exploration") {
3957
+ try {
3958
+ const scoped = resolveWorkspaceScopeRoot(root, {
3959
+ flowId: url.searchParams.get("flowId") || "",
3960
+ flowSource: url.searchParams.get("flowSource") || "user",
3961
+ adminOwnerId: url.searchParams.get("adminOwnerId") || "",
3962
+ archived: url.searchParams.get("archived") === "1" || url.searchParams.get("flowArchived") === "1",
3963
+ }, userCtx);
3964
+ if (scoped.error) {
3965
+ json(res, 400, { error: scoped.error });
3966
+ return;
3967
+ }
3968
+ json(res, 200, { ok: true, exploration: readAiExplorationSession(scoped.root, url.searchParams.get("id") || "") });
3969
+ } catch (e) {
3970
+ json(res, /Invalid exploration|ENOENT/.test(String(e?.message || e)) ? 404 : 500, { error: (e && e.message) || String(e) });
3971
+ }
3972
+ return;
3973
+ }
3974
+
3975
+ if (req.method === "POST" && url.pathname === "/api/workspace/exploration") {
3976
+ let payload;
3977
+ try { payload = JSON.parse(await readBody(req)); } catch {
3978
+ json(res, 400, { error: "Invalid JSON body" });
3979
+ return;
3980
+ }
3981
+ try {
3982
+ const scoped = resolveWorkspaceScopeRoot(root, {
3983
+ flowId: payload.flowId || "",
3984
+ flowSource: payload.flowSource || "user",
3985
+ adminOwnerId: payload.adminOwnerId || "",
3986
+ archived: payload.archived === true || payload.flowArchived === true,
3987
+ }, userCtx);
3988
+ if (scoped.error) {
3989
+ json(res, 400, { error: scoped.error });
3990
+ return;
3991
+ }
3992
+ if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource) || scoped.collaborationAccess?.writable === false) {
3993
+ json(res, 403, { error: "Exploration write permission denied" });
3994
+ return;
3995
+ }
3996
+ const exploration = createAiExplorationSession(scoped.root, {
3997
+ title: payload.title,
3998
+ goal: payload.goal,
3999
+ mode: payload.mode || "observed",
4000
+ status: payload.status || "running",
4001
+ source: payload.source || { provider: "external", agent: "custom" },
4002
+ });
4003
+ json(res, 201, {
4004
+ ok: true,
4005
+ exploration,
4006
+ ingest: {
4007
+ method: "POST",
4008
+ path: "/api/workspace/exploration/events",
4009
+ body: {
4010
+ flowId: payload.flowId || "",
4011
+ flowSource: payload.flowSource || "user",
4012
+ id: exploration.id,
4013
+ events: [],
4014
+ },
4015
+ },
4016
+ });
4017
+ } catch (e) {
4018
+ json(res, 500, { error: (e && e.message) || String(e) });
4019
+ }
4020
+ return;
4021
+ }
4022
+
4023
+ if (req.method === "POST" && url.pathname === "/api/workspace/exploration/events") {
4024
+ let payload;
4025
+ try { payload = JSON.parse(await readBody(req)); } catch {
4026
+ json(res, 400, { error: "Invalid JSON body" });
4027
+ return;
4028
+ }
4029
+ try {
4030
+ const scoped = resolveWorkspaceScopeRoot(root, {
4031
+ flowId: payload.flowId || "",
4032
+ flowSource: payload.flowSource || "user",
4033
+ adminOwnerId: payload.adminOwnerId || "",
4034
+ archived: payload.archived === true || payload.flowArchived === true,
4035
+ }, userCtx);
4036
+ if (scoped.error) {
4037
+ json(res, 400, { error: scoped.error });
4038
+ return;
4039
+ }
4040
+ if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource) || scoped.collaborationAccess?.writable === false) {
4041
+ json(res, 403, { error: "Exploration write permission denied" });
4042
+ return;
4043
+ }
4044
+ const appended = appendAiTraceEvents(scoped.root, payload.id, payload.events || [], {
4045
+ phase: payload.phase || "observed",
4046
+ });
4047
+ if (payload.status || payload.summary) {
4048
+ appended.session = updateAiExplorationSession(scoped.root, payload.id, {
4049
+ ...(payload.status ? { status: payload.status } : {}),
4050
+ ...(payload.summary ? { summary: payload.summary } : {}),
4051
+ });
4052
+ }
4053
+ json(res, 200, { ok: true, ...appended });
4054
+ } catch (e) {
4055
+ json(res, /Invalid exploration|ENOENT/.test(String(e?.message || e)) ? 404 : 400, { error: (e && e.message) || String(e) });
4056
+ }
4057
+ return;
4058
+ }
4059
+
4060
+ if (req.method === "POST" && url.pathname === "/api/workspace/exploration/plan") {
4061
+ let payload;
4062
+ try { payload = JSON.parse(await readBody(req)); } catch {
4063
+ json(res, 400, { error: "Invalid JSON body" });
4064
+ return;
4065
+ }
4066
+ const goal = String(payload?.goal || "").trim();
4067
+ if (!goal) {
4068
+ json(res, 400, { error: "Missing exploration goal" });
4069
+ return;
4070
+ }
4071
+ let scoped;
4072
+ let exploration;
4073
+ try {
4074
+ scoped = resolveWorkspaceScopeRoot(root, {
4075
+ flowId: payload.flowId || "",
4076
+ flowSource: payload.flowSource || "user",
4077
+ adminOwnerId: payload.adminOwnerId || "",
4078
+ archived: payload.archived === true || payload.flowArchived === true,
4079
+ }, userCtx);
4080
+ if (scoped.error) {
4081
+ json(res, 400, { error: scoped.error });
4082
+ return;
4083
+ }
4084
+ if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource) || scoped.collaborationAccess?.writable === false) {
4085
+ json(res, 403, { error: "Exploration write permission denied" });
4086
+ return;
4087
+ }
4088
+ const graphRead = readWorkspaceGraph(scoped.root);
4089
+ const graph = graphRead?.graph || graphRead;
4090
+ const workspaceSource = workspaceGraphAsSource(graph);
4091
+ exploration = createAiExplorationSession(scoped.root, {
4092
+ title: payload.title || goal.slice(0, 80),
4093
+ goal,
4094
+ mode: "planned",
4095
+ status: "planning",
4096
+ source: { provider: "agentflow", agent: String(payload.model || "default") },
4097
+ });
4098
+ const chunks = [];
4099
+ let result = "";
4100
+ const handle = startComposerAgent({
4101
+ uiWorkspaceRoot: scoped.root,
4102
+ cliWorkspace: scoped.root,
4103
+ prompt: aiPlanPrompt({ goal, workspaceSource }),
4104
+ modelKey: typeof payload.model === "string" ? payload.model.trim() : "",
4105
+ agentflowUserId: userCtx.userId || "",
4106
+ mode: "plan",
4107
+ force: false,
4108
+ sandboxDisabled: false,
4109
+ approveMcps: false,
4110
+ sandboxMode: "read-only",
4111
+ allowDanger: false,
4112
+ includeJsonResult: true,
4113
+ onStreamEvent(ev) {
4114
+ if (ev?.type !== "natural" || typeof ev.text !== "string") return;
4115
+ if (ev.kind === "result") result = ev.text.trim();
4116
+ else if (ev.kind === "assistant" && ev.text.trim()) chunks.push(ev.text.trim());
4117
+ },
4118
+ });
4119
+ await handle.finished;
4120
+ const plan = parseAiPlanResult(result || chunks.at(-1) || chunks.join("\n"), exploration.id);
4121
+ const appended = appendAiTraceEvents(scoped.root, exploration.id, plan.events, { phase: "planned" });
4122
+ const ready = updateAiExplorationSession(scoped.root, exploration.id, {
4123
+ title: plan.title,
4124
+ summary: plan.summary,
4125
+ status: "ready",
4126
+ eventCount: appended.session.eventCount,
4127
+ });
4128
+ json(res, 201, { ok: true, exploration: { ...ready, events: appended.events } });
4129
+ } catch (e) {
4130
+ if (scoped?.root && exploration?.id) {
4131
+ try { updateAiExplorationSession(scoped.root, exploration.id, { status: "failed", summary: String(e?.message || e) }); } catch {}
4132
+ }
4133
+ json(res, 500, { error: (e && e.message) || String(e), ...(exploration?.id ? { explorationId: exploration.id } : {}) });
4134
+ }
4135
+ return;
4136
+ }
4137
+
4138
+ if (req.method === "POST" && url.pathname === "/api/workspace/exploration/dry-run") {
4139
+ let payload;
4140
+ try { payload = JSON.parse(await readBody(req)); } catch {
4141
+ json(res, 400, { error: "Invalid JSON body" });
4142
+ return;
4143
+ }
4144
+ try {
4145
+ const scoped = resolveWorkspaceScopeRoot(root, {
4146
+ flowId: payload.flowId || "",
4147
+ flowSource: payload.flowSource || "user",
4148
+ adminOwnerId: payload.adminOwnerId || "",
4149
+ archived: payload.archived === true || payload.flowArchived === true,
4150
+ }, userCtx);
4151
+ if (scoped.error) {
4152
+ json(res, 400, { error: scoped.error });
4153
+ return;
4154
+ }
4155
+ if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource) || scoped.collaborationAccess?.writable === false) {
4156
+ json(res, 403, { error: "Exploration dry-run permission denied" });
4157
+ return;
4158
+ }
4159
+ const exploration = readAiExplorationSession(scoped.root, payload.id);
4160
+ const planned = exploration.events.filter((event) => event.phase === "planned");
4161
+ if (!planned.length) {
4162
+ json(res, 400, { error: "Exploration has no planned spans" });
4163
+ return;
4164
+ }
4165
+ const runToken = Date.now().toString(36);
4166
+ const simulated = planned.map((event, index) => {
4167
+ const blocked = event.requiresApproval === true || ["write", "external"].includes(event.sideEffect);
4168
+ return {
4169
+ id: `dry_${runToken}_${index + 1}`,
4170
+ spanId: `dry_${runToken}_${event.spanId}`,
4171
+ parentSpanId: event.parentSpanId ? `dry_${runToken}_${event.parentSpanId}` : "",
4172
+ phase: "simulated",
4173
+ type: event.type,
4174
+ name: event.name,
4175
+ summary: blocked
4176
+ ? `策略预检已阻断:${event.summary || event.name}。真实执行前需要明确授权。`
4177
+ : `策略预检通过:${event.summary || event.name}。本次未执行真实工具。`,
4178
+ sideEffect: event.sideEffect,
4179
+ requiresApproval: blocked,
4180
+ status: blocked ? "blocked" : "success",
4181
+ inputPreview: event.inputPreview || "",
4182
+ outputPreview: blocked ? "副作用未执行" : "约束检查通过,未执行真实工具",
4183
+ };
4184
+ });
4185
+ const appended = appendAiTraceEvents(scoped.root, exploration.id, simulated, { phase: "simulated" });
4186
+ const blockedCount = appended.events.filter((event) => event.status === "blocked").length;
4187
+ const updated = updateAiExplorationSession(scoped.root, exploration.id, {
4188
+ mode: "simulated",
4189
+ status: "ready",
4190
+ summary: `Dry-run 策略预检完成:${appended.events.length - blockedCount} 项通过,${blockedCount} 项等待授权。`,
4191
+ eventCount: appended.session.eventCount,
4192
+ });
4193
+ json(res, 200, {
4194
+ ok: true,
4195
+ exploration: { ...updated, events: [...exploration.events, ...appended.events] },
4196
+ dryRun: { kind: "policy-check", executedTools: false, blockedCount },
4197
+ });
4198
+ } catch (e) {
4199
+ json(res, 500, { error: (e && e.message) || String(e) });
4200
+ }
4201
+ return;
4202
+ }
4203
+
4204
+ if (req.method === "POST" && url.pathname === "/api/workspace/exploration/materialize") {
4205
+ let payload;
4206
+ try { payload = JSON.parse(await readBody(req)); } catch {
4207
+ json(res, 400, { error: "Invalid JSON body" });
4208
+ return;
4209
+ }
4210
+ let scoped;
4211
+ let exploration;
4212
+ let materializeRunId = "";
4213
+ try {
4214
+ scoped = resolveWorkspaceScopeRoot(root, {
4215
+ flowId: payload.flowId || "",
4216
+ flowSource: payload.flowSource || "user",
4217
+ adminOwnerId: payload.adminOwnerId || "",
4218
+ archived: payload.archived === true || payload.flowArchived === true,
4219
+ }, userCtx);
4220
+ if (scoped.error) {
4221
+ json(res, 400, { error: scoped.error });
4222
+ return;
4223
+ }
4224
+ if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource) || scoped.collaborationAccess?.writable === false) {
4225
+ json(res, 403, { error: "Exploration materialization permission denied" });
4226
+ return;
4227
+ }
4228
+ exploration = readAiExplorationSession(scoped.root, payload.id);
4229
+ if (!exploration.events.length) {
4230
+ json(res, 400, { error: "Exploration has no trace events" });
4231
+ return;
4232
+ }
4233
+ const materializableEvents = materializableAiTraceEvents(exploration);
4234
+ if (!materializableEvents.length) {
4235
+ json(res, 400, { error: "Exploration has no planned or observed spans to materialize" });
4236
+ return;
4237
+ }
4238
+ const dangerousEvents = materializableEvents.filter((event) => event.requiresApproval || ["write", "external"].includes(event.sideEffect));
4239
+ if (dangerousEvents.length && payload.approveSideEffects !== true) {
4240
+ json(res, 409, {
4241
+ error: "Side-effect review is required before materialization",
4242
+ sideEffects: dangerousEvents.map((event) => ({ spanId: event.spanId, name: event.name, sideEffect: event.sideEffect })),
4243
+ });
4244
+ return;
4245
+ }
4246
+ const beforeGraphRead = readWorkspaceGraph(scoped.root);
4247
+ const beforeGraph = beforeGraphRead?.graph || beforeGraphRead;
4248
+ const beforeIds = new Set([
4249
+ ...(beforeGraph.nodes || []).map((node) => String(node.id || "")),
4250
+ ...Object.keys(beforeGraph.instances || {}),
4251
+ ].filter(Boolean));
4252
+ materializeRunId = `materialize_${Date.now().toString(36)}`;
4253
+ appendAiTraceEvents(scoped.root, exploration.id, [{
4254
+ id: `${materializeRunId}_start`,
4255
+ spanId: materializeRunId,
4256
+ type: "agent",
4257
+ name: "固化 Agent",
4258
+ summary: "开始把已审核 Trace 转换为 Workspace DSL 调整态",
4259
+ status: "running",
4260
+ phase: "observed",
4261
+ sideEffect: "write",
4262
+ }], { phase: "observed" });
4263
+ updateAiExplorationSession(scoped.root, exploration.id, { mode: "observed", status: "running" });
4264
+ let reply = "";
4265
+ let toolIndex = 0;
4266
+ const handle = startComposerAgent({
4267
+ uiWorkspaceRoot: scoped.root,
4268
+ cliWorkspace: scoped.root,
4269
+ prompt: aiMaterializationPrompt(exploration, workspaceGraphAsSource(beforeGraph)),
4270
+ modelKey: typeof payload.model === "string" ? payload.model.trim() : "",
4271
+ agentflowUserId: userCtx.userId || "",
4272
+ force: true,
4273
+ sandboxDisabled: false,
4274
+ approveMcps: false,
4275
+ sandboxMode: "workspace-write",
4276
+ allowDanger: false,
4277
+ onToolCall(subtype, toolName) {
4278
+ toolIndex += 1;
4279
+ try {
4280
+ const toolStatus = /(?:complete|success|done)/i.test(String(subtype || ""))
4281
+ ? "success"
4282
+ : /(?:error|fail)/i.test(String(subtype || "")) ? "error" : "running";
4283
+ const sideEffect = classifyAiToolSideEffect(toolName, subtype);
4284
+ appendAiTraceEvents(scoped.root, exploration.id, [{
4285
+ id: `${materializeRunId}_tool_${toolIndex}`,
4286
+ spanId: `${materializeRunId}_tool_${toolIndex}`,
4287
+ parentSpanId: materializeRunId,
4288
+ type: String(subtype || "").toLowerCase() === "thinking" ? "decision" : "tool",
4289
+ name: String(toolName || subtype || "Agent tool"),
4290
+ summary: subtype ? `Agent 工具事件:${subtype}` : "Agent 工具调用",
4291
+ status: toolStatus,
4292
+ phase: "observed",
4293
+ sideEffect,
4294
+ }], { phase: "observed" });
4295
+ } catch {
4296
+ // Trace persistence must not interrupt an in-flight materialization.
4297
+ }
4298
+ },
4299
+ onStreamEvent(ev) {
4300
+ if (ev?.type === "natural" && ["assistant", "result"].includes(ev.kind) && typeof ev.text === "string" && ev.text.trim()) {
4301
+ reply = ev.text.trim();
4302
+ }
4303
+ },
4304
+ });
4305
+ await handle.finished;
4306
+ appendAiTraceEvents(scoped.root, exploration.id, [{
4307
+ id: `${materializeRunId}_finish`,
4308
+ spanId: materializeRunId,
4309
+ type: "agent",
4310
+ name: "固化 Agent",
4311
+ summary: reply || "Workspace DSL 调整态生成完成",
4312
+ status: "success",
4313
+ phase: "observed",
4314
+ sideEffect: "write",
4315
+ endedAt: new Date().toISOString(),
4316
+ }], { phase: "observed" });
4317
+ const graphRead = readWorkspaceGraph(scoped.root);
4318
+ const graph = graphRead?.graph || graphRead;
4319
+ const graphIds = [
4320
+ ...(graph.nodes || []).map((node) => String(node.id || "")),
4321
+ ...Object.keys(graph.instances || {}),
4322
+ ].filter(Boolean);
4323
+ const nodeIds = [...new Set(graphIds)].filter((id) => !beforeIds.has(id));
4324
+ const materialization = writeAiExplorationMaterialization(scoped.root, exploration.id, {
4325
+ nodeIds,
4326
+ designRevision: workspaceDesignRevision(graph),
4327
+ });
4328
+ appendAiTraceEvents(scoped.root, exploration.id, [{
4329
+ type: "status",
4330
+ name: "固化为 Workspace DSL",
4331
+ summary: reply || `新增 ${nodeIds.length} 个节点`,
4332
+ status: "success",
4333
+ phase: "materialized",
4334
+ sideEffect: "write",
4335
+ artifacts: [{ kind: "dsl", path: "workspace.flow.js", label: "Workspace DSL" }],
4336
+ }], { phase: "materialized" });
4337
+ const updated = updateAiExplorationSession(scoped.root, exploration.id, {
4338
+ mode: "materialized",
4339
+ status: "completed",
4340
+ materializedAt: materialization.materializedAt,
4341
+ });
4342
+ json(res, 200, { ok: true, exploration: updated, materialization, graph, reply });
4343
+ } catch (e) {
4344
+ if (scoped?.root && exploration?.id) {
4345
+ try {
4346
+ if (materializeRunId) {
4347
+ appendAiTraceEvents(scoped.root, exploration.id, [{
4348
+ id: `${materializeRunId}_error`,
4349
+ spanId: materializeRunId,
4350
+ type: "agent",
4351
+ name: "固化 Agent",
4352
+ summary: String(e?.message || e),
4353
+ status: "error",
4354
+ phase: "observed",
4355
+ sideEffect: "write",
4356
+ endedAt: new Date().toISOString(),
4357
+ }], { phase: "observed" });
4358
+ }
4359
+ updateAiExplorationSession(scoped.root, exploration.id, { mode: "observed", status: "failed" });
4360
+ } catch {}
4361
+ }
4362
+ json(res, 500, { error: (e && e.message) || String(e) });
4363
+ }
4364
+ return;
4365
+ }
4366
+
4035
4367
  if (url.pathname === "/api/workspace/conversations") {
4036
4368
  let payload = {};
4037
4369
  if (req.method === "POST") {
@@ -4478,6 +4810,7 @@ async function workspaceRoutes(req, res, ctx) {
4478
4810
  json(res, result.conflict ? 409 : 400, { error: result.error || "发布失败" });
4479
4811
  return;
4480
4812
  }
4813
+ markRepositoryIndexDirty(root);
4481
4814
  json(res, 200, { ok: true, ...result });
4482
4815
  } catch (e) {
4483
4816
  json(res, 500, { error: (e && e.message) || String(e) });