@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.
- package/bin/lib/agent-runners.mjs +98 -32
- package/bin/lib/ai-exploration.mjs +293 -0
- package/bin/lib/builtin-node-review.mjs +101 -0
- package/bin/lib/composer-agent.mjs +11 -0
- package/bin/lib/cursor-api-key-pool.mjs +106 -1
- package/bin/lib/repository-index-events.mjs +17 -0
- package/bin/lib/repository-index.mjs +534 -0
- package/bin/lib/ui-server.mjs +167 -0
- package/bin/lib/workspace-routes.mjs +560 -212
- package/bin/lib/workspace-run-logs.mjs +12 -1
- package/bin/lib/workspace-server.mjs +219 -22
- package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-DKdBGu3q.js → WorkflowAssistantThread-x1JCQpVD.js} +1 -1
- package/builtin/web-ui/dist/assets/index-BPFVqIDM.css +1 -0
- package/builtin/web-ui/dist/assets/index-Y3sCNmlY.js +880 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/agentflow-ai-exploration/SKILL.md +127 -0
- package/skills/agentflow-ai-exploration/agents/openai.yaml +4 -0
- package/skills/agentflow-ai-exploration/references/protocol.md +120 -0
- package/skills/agentflow-ai-exploration/scripts/agentflow-ai-exploration.mjs +308 -0
- package/skills/agentflow-ai-exploration/scripts/auth-store.mjs +102 -0
- package/skills/agentflow-cli/runtime/bin/lib/skill-runtime.mjs +1 -1
- package/skills/agentflow-cli/runtime/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-B7YuvFR2.css +0 -1
- package/builtin/web-ui/dist/assets/index-BUljbvrW.js +0 -877
|
@@ -11,9 +11,23 @@
|
|
|
11
11
|
* 是请求回调的闭包,解构之后路由体里的写法完全不变。
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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 =
|
|
1465
|
+
const installedCopies = indexedMarketplaceFlowCopies(root, userCtx.userId);
|
|
1584
1466
|
let graph;
|
|
1585
1467
|
let resource;
|
|
1586
1468
|
if (previewKind === "node") {
|
|
1587
|
-
const node =
|
|
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
|
-
|
|
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")
|
|
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
|
-
|
|
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
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
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) });
|