@fieldwangai/agentflow 0.1.165 → 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.
- package/bin/lib/agent-runners.mjs +131 -21
- package/bin/lib/ai-exploration.mjs +293 -0
- package/bin/lib/composer-agent.mjs +11 -0
- package/bin/lib/cursor-api-key-pool.mjs +246 -32
- package/bin/lib/cursor-model-catalog.mjs +152 -0
- package/bin/lib/repository-index-events.mjs +17 -0
- package/bin/lib/repository-index.mjs +522 -0
- package/bin/lib/ui-server.mjs +167 -0
- package/bin/lib/workspace-routes.mjs +947 -154
- package/bin/lib/workspace-server.mjs +3 -0
- package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-pVdrZ-Rl.js → WorkflowAssistantThread-B0i4F0Ab.js} +1 -1
- package/builtin/web-ui/dist/assets/index-BLTi7FF5.js +877 -0
- package/builtin/web-ui/dist/assets/index-yplDmRpj.css +1 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +2 -2
- 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 +242 -137
- package/skills/agentflow-cli/runtime/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-BQeq5tdj.css +0 -1
- package/builtin/web-ui/dist/assets/index-Czutb6ai.js +0 -873
|
@@ -11,8 +11,21 @@
|
|
|
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";
|
|
17
30
|
import { execFileBuffered } from "./exec-buffered.mjs";
|
|
18
31
|
import { runGit } from "./git-worktree.mjs";
|
|
@@ -30,22 +43,30 @@ 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
|
|
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,
|
|
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";
|
|
@@ -248,6 +269,170 @@ function missingWorkspaceGraphNodePackages(workspaceRoot, scoped, graph, userCtx
|
|
|
248
269
|
return [...missing].sort();
|
|
249
270
|
}
|
|
250
271
|
|
|
272
|
+
const NODE_REVIEW_MAX_FILES = 100;
|
|
273
|
+
const NODE_REVIEW_MAX_FILE_BYTES = 256 * 1024;
|
|
274
|
+
const NODE_REVIEW_MAX_TOTAL_BYTES = 2 * 1024 * 1024;
|
|
275
|
+
const NODE_REVIEW_TEXT_EXTENSIONS = new Set([
|
|
276
|
+
".cjs", ".css", ".env", ".go", ".html", ".ini", ".java", ".js", ".json", ".jsx", ".kt",
|
|
277
|
+
".md", ".mdx", ".mjs", ".py", ".rs", ".scss", ".sh", ".sql", ".toml", ".ts", ".tsx",
|
|
278
|
+
".txt", ".xml", ".yaml", ".yml",
|
|
279
|
+
]);
|
|
280
|
+
|
|
281
|
+
function workspaceNodeReviewLanguage(filePath = "") {
|
|
282
|
+
const extension = path.extname(String(filePath || "")).toLowerCase().replace(/^\./, "");
|
|
283
|
+
const aliases = { cjs: "javascript", js: "javascript", jsx: "javascript", mjs: "javascript", py: "python", sh: "shell", ts: "typescript", tsx: "typescript", yml: "yaml" };
|
|
284
|
+
return aliases[extension] || extension || "text";
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function workspaceNodeReviewSource({ sourcePath = "", title = "", kind = "file", content = "" } = {}) {
|
|
288
|
+
const text = String(content || "").replace(/\r\n/g, "\n");
|
|
289
|
+
const reviewPath = String(sourcePath || title || "source.txt").replace(/\\/g, "/");
|
|
290
|
+
return {
|
|
291
|
+
path: reviewPath,
|
|
292
|
+
title: String(title || path.basename(reviewPath) || reviewPath),
|
|
293
|
+
kind,
|
|
294
|
+
language: workspaceNodeReviewLanguage(reviewPath),
|
|
295
|
+
size: Buffer.byteLength(text),
|
|
296
|
+
sha256: crypto.createHash("sha256").update(text).digest("hex"),
|
|
297
|
+
content: text,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function workspaceNodeReviewFlowFile(flowRoot, fileRef, kind) {
|
|
302
|
+
const ref = String(fileRef || "").trim();
|
|
303
|
+
if (!ref) return null;
|
|
304
|
+
try {
|
|
305
|
+
const { abs, rel } = resolveWorkspaceFilePath(flowRoot, ref);
|
|
306
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
|
|
307
|
+
return { error: `找不到 ${ref}` };
|
|
308
|
+
}
|
|
309
|
+
const stat = fs.statSync(abs);
|
|
310
|
+
if (stat.size > NODE_REVIEW_MAX_FILE_BYTES) {
|
|
311
|
+
return { error: `${ref} 超过可预览大小限制` };
|
|
312
|
+
}
|
|
313
|
+
return workspaceNodeReviewSource({ sourcePath: rel, title: rel, kind, content: fs.readFileSync(abs, "utf-8") });
|
|
314
|
+
} catch (error) {
|
|
315
|
+
return { error: `${ref}: ${(error && error.message) || String(error)}` };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function workspaceNodeReviewPackageFiles(packageDir) {
|
|
320
|
+
const root = path.resolve(packageDir);
|
|
321
|
+
const candidates = [];
|
|
322
|
+
const walk = (dir) => {
|
|
323
|
+
if (candidates.length >= NODE_REVIEW_MAX_FILES) return;
|
|
324
|
+
let entries = [];
|
|
325
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
326
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
327
|
+
for (const entry of entries) {
|
|
328
|
+
if (candidates.length >= NODE_REVIEW_MAX_FILES) break;
|
|
329
|
+
if ([".git", "node_modules"].includes(entry.name)) continue;
|
|
330
|
+
const abs = path.join(dir, entry.name);
|
|
331
|
+
if (entry.isDirectory()) walk(abs);
|
|
332
|
+
else if (entry.isFile() && NODE_REVIEW_TEXT_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) candidates.push(abs);
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
walk(root);
|
|
336
|
+
candidates.sort((left, right) => {
|
|
337
|
+
const leftRel = path.relative(root, left).replace(/\\/g, "/");
|
|
338
|
+
const rightRel = path.relative(root, right).replace(/\\/g, "/");
|
|
339
|
+
if (leftRel === NODE_PACKAGE_ENTRY) return -1;
|
|
340
|
+
if (rightRel === NODE_PACKAGE_ENTRY) return 1;
|
|
341
|
+
return leftRel.localeCompare(rightRel);
|
|
342
|
+
});
|
|
343
|
+
const sources = [];
|
|
344
|
+
let totalBytes = 0;
|
|
345
|
+
for (const abs of candidates) {
|
|
346
|
+
let stat;
|
|
347
|
+
try { stat = fs.statSync(abs); } catch { continue; }
|
|
348
|
+
if (!stat.isFile() || stat.size > NODE_REVIEW_MAX_FILE_BYTES || totalBytes + stat.size > NODE_REVIEW_MAX_TOTAL_BYTES) continue;
|
|
349
|
+
const rel = path.relative(root, abs).replace(/\\/g, "/");
|
|
350
|
+
sources.push(workspaceNodeReviewSource({ sourcePath: rel, title: rel, kind: "package", content: fs.readFileSync(abs, "utf-8") }));
|
|
351
|
+
totalBytes += stat.size;
|
|
352
|
+
}
|
|
353
|
+
return sources;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function workspaceNodeReviewSnapshot(workspaceRoot, flowRoot, graph, nodeId, userCtx = {}, label = "Draft") {
|
|
357
|
+
const instance = graph?.instances?.[nodeId];
|
|
358
|
+
if (!instance) return null;
|
|
359
|
+
const definitionId = String(instance.definitionId || nodeId);
|
|
360
|
+
const marketplaceRef = String(instance.marketplaceRef || definitionId).trim();
|
|
361
|
+
const sources = [];
|
|
362
|
+
const errors = [];
|
|
363
|
+
let packageInfo = null;
|
|
364
|
+
|
|
365
|
+
if (marketplaceRef.startsWith("marketplace:")) {
|
|
366
|
+
const resolved = resolveMarketplaceNodePackage(
|
|
367
|
+
workspaceRoot,
|
|
368
|
+
flowRoot,
|
|
369
|
+
marketplaceRef,
|
|
370
|
+
graph,
|
|
371
|
+
{ ...userCtx, marketplaceScope: "all" },
|
|
372
|
+
);
|
|
373
|
+
if (!resolved) {
|
|
374
|
+
errors.push(`无法解析节点包 ${marketplaceRef}`);
|
|
375
|
+
} else {
|
|
376
|
+
const inspected = inspectNodePackageDirectory(resolved.packageDir, { allowLegacyManifest: true });
|
|
377
|
+
sources.push(...workspaceNodeReviewPackageFiles(resolved.packageDir));
|
|
378
|
+
packageInfo = {
|
|
379
|
+
id: resolved.id,
|
|
380
|
+
version: resolved.version,
|
|
381
|
+
definitionId: resolved.resolvedDefinitionId || marketplaceRef,
|
|
382
|
+
source: resolved.source || "marketplace",
|
|
383
|
+
contentSha256: inspected.ok ? inspected.contentSha256 : String(resolved.contentSha256 || ""),
|
|
384
|
+
};
|
|
385
|
+
if (!sources.length) errors.push(`节点包 ${marketplaceRef} 没有可预览的文本文件`);
|
|
386
|
+
}
|
|
387
|
+
} else if (String(instance.script || "").trim()) {
|
|
388
|
+
sources.push(workspaceNodeReviewSource({
|
|
389
|
+
sourcePath: `nodes/${nodeId}/inline-script.mjs`,
|
|
390
|
+
title: "内联脚本",
|
|
391
|
+
kind: "inline",
|
|
392
|
+
content: instance.script,
|
|
393
|
+
}));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
for (const [ref, kind] of [[instance.scriptRef, "script"], [instance.implementationRef, "implementation"]]) {
|
|
397
|
+
const source = workspaceNodeReviewFlowFile(flowRoot, ref, kind);
|
|
398
|
+
if (source?.error) errors.push(source.error);
|
|
399
|
+
else if (source) sources.push(source);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const body = String(instance.body || "");
|
|
403
|
+
if (body.trim()) {
|
|
404
|
+
const looksJson = /^[\[{]/.test(body.trim());
|
|
405
|
+
sources.push(workspaceNodeReviewSource({
|
|
406
|
+
sourcePath: `nodes/${nodeId}/${looksJson ? "configuration.json" : "prompt.md"}`,
|
|
407
|
+
title: looksJson ? "节点配置" : "Prompt / 指令",
|
|
408
|
+
kind: looksJson ? "configuration" : "prompt",
|
|
409
|
+
content: body,
|
|
410
|
+
}));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const uniqueSources = [];
|
|
414
|
+
const seen = new Set();
|
|
415
|
+
for (const source of sources) {
|
|
416
|
+
const key = `${source.kind}\u0000${source.path}\u0000${source.sha256}`;
|
|
417
|
+
if (seen.has(key)) continue;
|
|
418
|
+
seen.add(key);
|
|
419
|
+
uniqueSources.push(source);
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
label,
|
|
423
|
+
nodeId,
|
|
424
|
+
definitionId,
|
|
425
|
+
model: String(instance.model || ""),
|
|
426
|
+
role: String(instance.role || "normal"),
|
|
427
|
+
implementationMode: String(instance.implementationMode || ""),
|
|
428
|
+
marketplaceRef: marketplaceRef.startsWith("marketplace:") ? marketplaceRef : "",
|
|
429
|
+
package: packageInfo,
|
|
430
|
+
sources: uniqueSources,
|
|
431
|
+
errors,
|
|
432
|
+
reviewable: uniqueSources.length > 0,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
251
436
|
const NODE_STUDIO_DRAFTS_DIRNAME = "node-studio/drafts";
|
|
252
437
|
|
|
253
438
|
function writeUserWorkspaces(userCtx = {}, entries = []) {
|
|
@@ -912,133 +1097,8 @@ function workspaceBufferLooksLikeZip(buf) {
|
|
|
912
1097
|
);
|
|
913
1098
|
}
|
|
914
1099
|
|
|
915
|
-
function installedMarketplaceFlowCopies(userId) {
|
|
916
|
-
const copies = new Map();
|
|
917
|
-
const pipelinesRoot = getUserPipelinesRoot(userId);
|
|
918
|
-
if (!fs.existsSync(pipelinesRoot)) return copies;
|
|
919
|
-
for (const entry of fs.readdirSync(pipelinesRoot, { withFileTypes: true })) {
|
|
920
|
-
if (!entry.isDirectory()) continue;
|
|
921
|
-
const origin = readMarketplaceFlowOrigin(path.join(pipelinesRoot, entry.name));
|
|
922
|
-
if (!origin) continue;
|
|
923
|
-
const key = `${origin.id}@${origin.version}`;
|
|
924
|
-
const flowIds = copies.get(key) || [];
|
|
925
|
-
flowIds.push(entry.name);
|
|
926
|
-
copies.set(key, flowIds);
|
|
927
|
-
}
|
|
928
|
-
return copies;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
const PROJECT_FLOW_MARKETPLACE_FILENAME = path.join(".workspace", "agentflow", "marketplace.json");
|
|
932
|
-
|
|
933
|
-
function projectFlowMarketplaceId(ownerUserId, flowSource, flowId) {
|
|
934
|
-
return `project-flow:${String(ownerUserId || "").trim()}:${String(flowSource || "user").trim()}:${String(flowId || "").trim()}`;
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
function projectFlowMarketplaceMetadataPath(flowRoot) {
|
|
938
|
-
return path.join(path.resolve(flowRoot), PROJECT_FLOW_MARKETPLACE_FILENAME);
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
function readProjectFlowMarketplaceMetadata(flowRoot) {
|
|
942
|
-
try {
|
|
943
|
-
const parsed = JSON.parse(fs.readFileSync(projectFlowMarketplaceMetadataPath(flowRoot), "utf-8"));
|
|
944
|
-
return {
|
|
945
|
-
visibility: String(parsed?.visibility || "").trim() === "private" ? "private" : "public",
|
|
946
|
-
updatedAt: String(parsed?.updatedAt || "").trim(),
|
|
947
|
-
};
|
|
948
|
-
} catch {
|
|
949
|
-
return { visibility: "public", updatedAt: "" };
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
function writeProjectFlowMarketplaceMetadata(flowRoot, visibility) {
|
|
954
|
-
const filePath = projectFlowMarketplaceMetadataPath(flowRoot);
|
|
955
|
-
const value = {
|
|
956
|
-
version: 1,
|
|
957
|
-
visibility: String(visibility || "").trim() === "private" ? "private" : "public",
|
|
958
|
-
updatedAt: new Date().toISOString(),
|
|
959
|
-
};
|
|
960
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
961
|
-
const tempPath = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
962
|
-
fs.writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
|
|
963
|
-
fs.renameSync(tempPath, filePath);
|
|
964
|
-
return value;
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
function graphHasRunnableEntry(graph) {
|
|
968
|
-
return Object.values(graph?.instances || {}).some((instance) => (
|
|
969
|
-
instance?.definitionId === "workspace_run"
|
|
970
|
-
|| instance?.definitionId === "workspace_scheduled_run"
|
|
971
|
-
));
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
function projectFlowUpdatedAt(flowRoot, metadata = {}) {
|
|
975
|
-
if (metadata.updatedAt) return metadata.updatedAt;
|
|
976
|
-
try {
|
|
977
|
-
return fs.statSync(path.resolve(flowRoot)).mtime.toISOString();
|
|
978
|
-
} catch {
|
|
979
|
-
return "";
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
|
|
983
1100
|
function listRunnableProjectMarketplaceFlows(workspaceRoot, userCtx = {}, scope = "all") {
|
|
984
|
-
|
|
985
|
-
const stats = marketplaceUsageStats(workspaceRoot);
|
|
986
|
-
const resources = [];
|
|
987
|
-
const appendFlow = (ownerId, flow, flowSource = "user", workspaceId = "") => {
|
|
988
|
-
if (!ownerId || (scope === "owned" && ownerId !== requestedUserId)) return;
|
|
989
|
-
if (flow.archived || !flow.path) return;
|
|
990
|
-
let stable;
|
|
991
|
-
let graph;
|
|
992
|
-
try {
|
|
993
|
-
stable = readWorkspaceStableRelease(flow.path, workspaceRoot);
|
|
994
|
-
graph = stable?.graph || readWorkspaceGraph(flow.path, workspaceRoot).graph;
|
|
995
|
-
} catch {
|
|
996
|
-
return;
|
|
997
|
-
}
|
|
998
|
-
if (!graphHasRunnableEntry(graph)) return;
|
|
999
|
-
const metadata = readProjectFlowMarketplaceMetadata(flow.path);
|
|
1000
|
-
const owned = ownerId === requestedUserId;
|
|
1001
|
-
if (scope !== "owned" && metadata.visibility === "private" && !owned && userCtx.isAdmin !== true) return;
|
|
1002
|
-
const id = projectFlowMarketplaceId(ownerId, flowSource, flow.id);
|
|
1003
|
-
const version = stable?.release?.id || `current-${workspaceDesignRevision(graph).slice(0, 12)}`;
|
|
1004
|
-
resources.push({
|
|
1005
|
-
resourceType: "flow",
|
|
1006
|
-
projectFlow: true,
|
|
1007
|
-
id,
|
|
1008
|
-
definitionId: flow.id,
|
|
1009
|
-
displayName: flow.id,
|
|
1010
|
-
description: flow.description || "",
|
|
1011
|
-
version,
|
|
1012
|
-
versionLabel: stable?.release?.id ? `Stable ${stable.release.id}` : "当前版本",
|
|
1013
|
-
ownerUserId: ownerId,
|
|
1014
|
-
liveOwnerUserId: ownerId,
|
|
1015
|
-
liveFlowId: flow.id,
|
|
1016
|
-
liveFlowSource: flowSource,
|
|
1017
|
-
liveWorkspaceId: workspaceId,
|
|
1018
|
-
visibility: metadata.visibility,
|
|
1019
|
-
nodeCount: Object.keys(graph?.instances || {}).length,
|
|
1020
|
-
edgeCount: Array.isArray(graph?.edges) ? graph.edges.length : 0,
|
|
1021
|
-
updatedAt: projectFlowUpdatedAt(flow.path, metadata),
|
|
1022
|
-
...marketplaceStatsFor(stats, "project-flow", id, version, ownerId),
|
|
1023
|
-
_graph: graph,
|
|
1024
|
-
_flowRoot: flow.path,
|
|
1025
|
-
});
|
|
1026
|
-
};
|
|
1027
|
-
for (const ownerUserId of listAgentflowUserIds()) {
|
|
1028
|
-
const ownerId = String(ownerUserId || "").trim();
|
|
1029
|
-
for (const flow of listFlowsJson(workspaceRoot, { userId: ownerId })) {
|
|
1030
|
-
if ((flow.source || "user") !== "user" || flow.archived || !flow.path) continue;
|
|
1031
|
-
appendFlow(ownerId, flow, "user");
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
for (const flow of listFlowsJson(workspaceRoot, { userId: "", includeWorkspaceFlows: true })) {
|
|
1035
|
-
if ((flow.source || "") !== "workspace" || flow.archived || !flow.path) continue;
|
|
1036
|
-
const collaboration = getWorkspaceCollaborationByFlow(flow.id, false);
|
|
1037
|
-
const ownerId = String(collaboration?.ownerId || "").trim();
|
|
1038
|
-
if (!ownerId) continue;
|
|
1039
|
-
appendFlow(ownerId, flow, "workspace", collaboration.id);
|
|
1040
|
-
}
|
|
1041
|
-
return resources;
|
|
1101
|
+
return listIndexedProjectFlows(workspaceRoot, userCtx, scope);
|
|
1042
1102
|
}
|
|
1043
1103
|
|
|
1044
1104
|
function publicProjectFlowMarketplaceResource(resource) {
|
|
@@ -1062,13 +1122,26 @@ function marketplaceResourceMatches(item, queryText) {
|
|
|
1062
1122
|
function sortMarketplaceResources(items) {
|
|
1063
1123
|
return items.sort((a, b) => (
|
|
1064
1124
|
Number(b.useCount || 0) - Number(a.useCount || 0)
|
|
1065
|
-
|| Number(b.installCount || 0) - Number(a.installCount || 0)
|
|
1066
1125
|
|| String(b.updatedAt || b.createdAt || "").localeCompare(String(a.updatedAt || a.createdAt || ""))
|
|
1067
1126
|
|| String(a.displayName || a.id).localeCompare(String(b.displayName || b.id))
|
|
1068
1127
|
|| String(b.version || "").localeCompare(String(a.version || ""), undefined, { numeric: true, sensitivity: "base" })
|
|
1069
1128
|
));
|
|
1070
1129
|
}
|
|
1071
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
|
+
|
|
1072
1145
|
/**
|
|
1073
1146
|
* @param {import('http').IncomingMessage} req
|
|
1074
1147
|
* @param {import('http').ServerResponse} res
|
|
@@ -1131,6 +1204,7 @@ async function workspaceRoutes(req, res, ctx) {
|
|
|
1131
1204
|
}
|
|
1132
1205
|
try {
|
|
1133
1206
|
const result = publishNodePackageArchive(root, parsed.file, { ownerUserId: userCtx.userId });
|
|
1207
|
+
if (result.ok) markRepositoryIndexDirty(root);
|
|
1134
1208
|
json(res, result.ok ? (result.alreadyExists ? 200 : 201) : (result.conflict ? 409 : 400), result);
|
|
1135
1209
|
} catch (e) {
|
|
1136
1210
|
json(res, 500, { ok: false, error: (e && e.message) || String(e) });
|
|
@@ -1163,6 +1237,7 @@ async function workspaceRoutes(req, res, ctx) {
|
|
|
1163
1237
|
actorUserId: userCtx.userId || "",
|
|
1164
1238
|
eventId: `install:node:${id}@${version}:${userCtx.userId || "anonymous"}`,
|
|
1165
1239
|
});
|
|
1240
|
+
markRepositoryIndexDirty(root);
|
|
1166
1241
|
res.writeHead(200, {
|
|
1167
1242
|
"Content-Type": "application/zip",
|
|
1168
1243
|
"Content-Length": result.archive.length,
|
|
@@ -1186,7 +1261,7 @@ async function workspaceRoutes(req, res, ctx) {
|
|
|
1186
1261
|
const marketplaceScope = scope === "owned" ? "owned" : "all";
|
|
1187
1262
|
const queryText = String(url.searchParams.get("q") || "").trim().toLowerCase();
|
|
1188
1263
|
if (kind === "node") {
|
|
1189
|
-
const marketplaceNodes =
|
|
1264
|
+
const marketplaceNodes = listIndexedNodes(root, userCtx, marketplaceScope)
|
|
1190
1265
|
.map((node) => ({ ...node, resourceType: "node", installed: true }));
|
|
1191
1266
|
let nodes = marketplaceNodes;
|
|
1192
1267
|
if (scope === "installed") {
|
|
@@ -1217,23 +1292,23 @@ async function workspaceRoutes(req, res, ctx) {
|
|
|
1217
1292
|
});
|
|
1218
1293
|
}
|
|
1219
1294
|
nodes = sortMarketplaceResources(nodes.filter((item) => marketplaceResourceMatches(item, queryText)));
|
|
1220
|
-
|
|
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
|
+
});
|
|
1221
1305
|
return;
|
|
1222
1306
|
}
|
|
1223
1307
|
if (kind !== "flow") {
|
|
1224
1308
|
json(res, 400, { error: "kind must be flow or node" });
|
|
1225
1309
|
return;
|
|
1226
1310
|
}
|
|
1227
|
-
const installedCopies =
|
|
1228
|
-
const flows = listMarketplaceFlows(root, { ...userCtx, marketplaceScope }).flows.map((flow) => {
|
|
1229
|
-
const installedFlowIds = installedCopies.get(`${flow.id}@${flow.version}`) || [];
|
|
1230
|
-
return {
|
|
1231
|
-
...flow,
|
|
1232
|
-
resourceType: "flow",
|
|
1233
|
-
installed: installedFlowIds.length > 0,
|
|
1234
|
-
installedFlowIds,
|
|
1235
|
-
};
|
|
1236
|
-
});
|
|
1311
|
+
const installedCopies = indexedMarketplaceFlowCopies(root, userCtx.userId);
|
|
1237
1312
|
const projectFlows = listRunnableProjectMarketplaceFlows(root, userCtx, scope).map((flow) => {
|
|
1238
1313
|
const installedFlowIds = installedCopies.get(`${flow.id}@${flow.version}`) || [];
|
|
1239
1314
|
return publicProjectFlowMarketplaceResource({
|
|
@@ -1242,14 +1317,21 @@ async function workspaceRoutes(req, res, ctx) {
|
|
|
1242
1317
|
installedFlowIds,
|
|
1243
1318
|
});
|
|
1244
1319
|
});
|
|
1245
|
-
const snippets = scope === "installed" ? [] : listMarketplaceFlowSnippets(root, { ...userCtx, marketplaceScope }).snippets
|
|
1246
|
-
.map((snippet) => ({ ...snippet, resourceType: "flow-snippet", installed: false }));
|
|
1247
1320
|
const items = sortMarketplaceResources(
|
|
1248
|
-
|
|
1321
|
+
projectFlows
|
|
1249
1322
|
.filter((item) => scope !== "installed" || item.installed)
|
|
1250
1323
|
.filter((item) => marketplaceResourceMatches(item, queryText)),
|
|
1251
1324
|
);
|
|
1252
|
-
|
|
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
|
+
});
|
|
1253
1335
|
} catch (e) {
|
|
1254
1336
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
1255
1337
|
}
|
|
@@ -1293,6 +1375,220 @@ async function workspaceRoutes(req, res, ctx) {
|
|
|
1293
1375
|
return;
|
|
1294
1376
|
}
|
|
1295
1377
|
|
|
1378
|
+
if (req.method === "GET" && url.pathname === "/api/marketplace/flows/preview") {
|
|
1379
|
+
const id = String(url.searchParams.get("id") || "").trim();
|
|
1380
|
+
const version = String(url.searchParams.get("version") || "").trim();
|
|
1381
|
+
const projectFlow = url.searchParams.get("projectFlow") === "1";
|
|
1382
|
+
if (!id || !version) {
|
|
1383
|
+
json(res, 400, { error: "Missing marketplace flow id or version" });
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
try {
|
|
1387
|
+
const installedCopies = indexedMarketplaceFlowCopies(root, userCtx.userId);
|
|
1388
|
+
if (projectFlow) {
|
|
1389
|
+
const source = listRunnableProjectMarketplaceFlows(root, userCtx, "all")
|
|
1390
|
+
.find((flow) => flow.id === id);
|
|
1391
|
+
if (!source) {
|
|
1392
|
+
json(res, 404, { error: "Project flow not found or is private" });
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
if (source.version !== version) {
|
|
1396
|
+
json(res, 409, { error: "该流程已有新版本,请刷新流程仓库后重试" });
|
|
1397
|
+
return;
|
|
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
|
+
}
|
|
1405
|
+
json(res, 200, {
|
|
1406
|
+
flow: {
|
|
1407
|
+
...publicProjectFlowMarketplaceResource(source),
|
|
1408
|
+
owned: source.ownerUserId === userCtx.userId,
|
|
1409
|
+
installedFlowIds: installedCopies.get(`${id}@${version}`) || [],
|
|
1410
|
+
},
|
|
1411
|
+
graph: preview.graph,
|
|
1412
|
+
});
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
const source = readMarketplaceFlow(root, id, version, { ...userCtx, marketplaceScope: "all" });
|
|
1416
|
+
if (!source.ok) {
|
|
1417
|
+
json(res, 404, { error: source.error || "Marketplace flow not found" });
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
const metadata = listMarketplaceFlows(root, { ...userCtx, marketplaceScope: "all" }).flows
|
|
1421
|
+
.find((flow) => flow.id === id && flow.version === version) || {};
|
|
1422
|
+
json(res, 200, {
|
|
1423
|
+
flow: {
|
|
1424
|
+
...metadata,
|
|
1425
|
+
id,
|
|
1426
|
+
version,
|
|
1427
|
+
resourceType: "flow",
|
|
1428
|
+
owned: metadata.ownerUserId === userCtx.userId,
|
|
1429
|
+
installedFlowIds: installedCopies.get(`${id}@${version}`) || [],
|
|
1430
|
+
},
|
|
1431
|
+
graph: source.graph,
|
|
1432
|
+
});
|
|
1433
|
+
} catch (e) {
|
|
1434
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
1435
|
+
}
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
if (req.method === "POST" && url.pathname === "/api/marketplace/flows/workspace-preview") {
|
|
1440
|
+
let payload;
|
|
1441
|
+
try {
|
|
1442
|
+
payload = JSON.parse(await readBody(req));
|
|
1443
|
+
} catch {
|
|
1444
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
const id = String(payload?.id || "").trim();
|
|
1448
|
+
const version = String(payload?.version || "").trim();
|
|
1449
|
+
const previewKind = ["snippet", "node"].includes(payload?.kind) ? payload.kind : "flow";
|
|
1450
|
+
const projectFlow = payload?.projectFlow === true;
|
|
1451
|
+
if (!id || !version) {
|
|
1452
|
+
json(res, 400, { error: "Missing marketplace flow id or version" });
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
try {
|
|
1456
|
+
const installedCopies = indexedMarketplaceFlowCopies(root, userCtx.userId);
|
|
1457
|
+
let graph;
|
|
1458
|
+
let resource;
|
|
1459
|
+
if (previewKind === "node") {
|
|
1460
|
+
const node = listIndexedNodes(root, userCtx, "all")
|
|
1461
|
+
.find((item) => item.id === id && item.version === version);
|
|
1462
|
+
if (!node) {
|
|
1463
|
+
json(res, 404, { error: "Node package not found or is private" });
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
graph = {
|
|
1467
|
+
version: 1,
|
|
1468
|
+
instances: {
|
|
1469
|
+
node_preview: {
|
|
1470
|
+
definitionId: node.definitionId,
|
|
1471
|
+
marketplaceRef: node.definitionId,
|
|
1472
|
+
marketplacePackageId: node.id,
|
|
1473
|
+
marketplaceVersion: node.version,
|
|
1474
|
+
label: node.displayName || node.id,
|
|
1475
|
+
role: "normal",
|
|
1476
|
+
body: "",
|
|
1477
|
+
input: Array.isArray(node.inputs) ? node.inputs : [],
|
|
1478
|
+
output: Array.isArray(node.outputs) ? node.outputs : [],
|
|
1479
|
+
},
|
|
1480
|
+
},
|
|
1481
|
+
edges: [],
|
|
1482
|
+
ui: { nodePositions: { node_preview: { x: 320, y: 220 } } },
|
|
1483
|
+
};
|
|
1484
|
+
resource = { ...node, resourceType: "node", owned: node.ownerUserId === userCtx.userId };
|
|
1485
|
+
} else if (previewKind === "snippet") {
|
|
1486
|
+
const snippet = listMarketplaceFlowSnippets(root, { ...userCtx, marketplaceScope: "all" }).snippets
|
|
1487
|
+
.find((item) => item.id === id && item.version === version);
|
|
1488
|
+
if (!snippet) {
|
|
1489
|
+
json(res, 404, { error: "Flow snippet not found or is private" });
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
graph = snippet.snippet;
|
|
1493
|
+
resource = { ...snippet, resourceType: "flow-snippet" };
|
|
1494
|
+
} else if (projectFlow) {
|
|
1495
|
+
const source = listRunnableProjectMarketplaceFlows(root, userCtx, "all")
|
|
1496
|
+
.find((flow) => flow.id === id);
|
|
1497
|
+
if (!source) {
|
|
1498
|
+
json(res, 404, { error: "Project flow not found or is private" });
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
if (source.version !== version) {
|
|
1502
|
+
json(res, 409, { error: "该流程已有新版本,请刷新流程仓库后重试" });
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
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;
|
|
1512
|
+
resource = {
|
|
1513
|
+
...publicProjectFlowMarketplaceResource(source),
|
|
1514
|
+
owned: source.ownerUserId === userCtx.userId,
|
|
1515
|
+
installedFlowIds: installedCopies.get(`${id}@${version}`) || [],
|
|
1516
|
+
};
|
|
1517
|
+
} else {
|
|
1518
|
+
const source = readMarketplaceFlow(root, id, version, { ...userCtx, marketplaceScope: "all" });
|
|
1519
|
+
if (!source.ok) {
|
|
1520
|
+
json(res, 404, { error: source.error || "Marketplace flow not found" });
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
const metadata = listMarketplaceFlows(root, { ...userCtx, marketplaceScope: "all" }).flows
|
|
1524
|
+
.find((flow) => flow.id === id && flow.version === version) || {};
|
|
1525
|
+
graph = source.graph;
|
|
1526
|
+
resource = {
|
|
1527
|
+
...metadata,
|
|
1528
|
+
id,
|
|
1529
|
+
version,
|
|
1530
|
+
resourceType: "flow",
|
|
1531
|
+
owned: metadata.ownerUserId === userCtx.userId,
|
|
1532
|
+
installedFlowIds: installedCopies.get(`${id}@${version}`) || [],
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
const flowId = createWorkspacePreviewId();
|
|
1536
|
+
const flowDir = workspaceSharedPreviewFlowDir(root, flowId);
|
|
1537
|
+
const now = Date.now();
|
|
1538
|
+
const metadata = {
|
|
1539
|
+
version: 1,
|
|
1540
|
+
flowId,
|
|
1541
|
+
ownerId: authUser.userId,
|
|
1542
|
+
title: String(resource.displayName || resource.definitionId || id).trim().slice(0, 200),
|
|
1543
|
+
createdAt: new Date(now).toISOString(),
|
|
1544
|
+
updatedAt: new Date(now).toISOString(),
|
|
1545
|
+
expiresAt: new Date(now + DEFAULT_WORKSPACE_PREVIEW_TTL_MS).toISOString(),
|
|
1546
|
+
marketplace: { id, version, projectFlow },
|
|
1547
|
+
};
|
|
1548
|
+
fs.mkdirSync(flowDir, { recursive: true });
|
|
1549
|
+
writeWorkspaceGraph(flowDir, graph, root);
|
|
1550
|
+
writeWorkspacePreviewMetadata(flowDir, metadata);
|
|
1551
|
+
const installedFlowId = resource.installedFlowIds?.[0] || "";
|
|
1552
|
+
const action = previewKind === "node"
|
|
1553
|
+
? "add-node"
|
|
1554
|
+
: previewKind === "snippet" ? "add-snippet"
|
|
1555
|
+
: resource.projectFlow && resource.owned
|
|
1556
|
+
? "open-source"
|
|
1557
|
+
: installedFlowId ? "open-installed" : "install";
|
|
1558
|
+
const previewParams = new URLSearchParams({
|
|
1559
|
+
flowId,
|
|
1560
|
+
flowSource: "workspace",
|
|
1561
|
+
archived: "1",
|
|
1562
|
+
marketplacePreview: "1",
|
|
1563
|
+
marketplaceKind: previewKind,
|
|
1564
|
+
marketplaceResourceId: id,
|
|
1565
|
+
marketplaceVersion: version,
|
|
1566
|
+
marketplaceProjectFlow: projectFlow ? "1" : "0",
|
|
1567
|
+
marketplaceTitle: resource.displayName || resource.definitionId || id,
|
|
1568
|
+
marketplaceAction: action,
|
|
1569
|
+
marketplaceInstallFlowId: resource.installFlowId || resource.liveFlowId || resource.definitionId || id,
|
|
1570
|
+
});
|
|
1571
|
+
if (previewKind === "node") previewParams.set("focusNodeId", "node_preview");
|
|
1572
|
+
if (action === "open-source") {
|
|
1573
|
+
previewParams.set("marketplaceTargetFlowId", resource.liveFlowId || resource.definitionId || "");
|
|
1574
|
+
previewParams.set("marketplaceTargetFlowSource", resource.liveFlowSource || "user");
|
|
1575
|
+
if (resource.liveWorkspaceId) previewParams.set("marketplaceTargetWorkspaceId", resource.liveWorkspaceId);
|
|
1576
|
+
} else if (action === "open-installed") {
|
|
1577
|
+
previewParams.set("marketplaceTargetFlowId", installedFlowId);
|
|
1578
|
+
previewParams.set("marketplaceTargetFlowSource", "user");
|
|
1579
|
+
}
|
|
1580
|
+
json(res, 200, {
|
|
1581
|
+
ok: true,
|
|
1582
|
+
preview: true,
|
|
1583
|
+
expiresAt: metadata.expiresAt,
|
|
1584
|
+
url: `/workspace?${previewParams}`,
|
|
1585
|
+
});
|
|
1586
|
+
} catch (e) {
|
|
1587
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
1588
|
+
}
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1296
1592
|
if (req.method === "POST" && url.pathname === "/api/marketplace/flows/publish") {
|
|
1297
1593
|
let payload;
|
|
1298
1594
|
try {
|
|
@@ -1329,6 +1625,7 @@ async function workspaceRoutes(req, res, ctx) {
|
|
|
1329
1625
|
visibility: payload?.visibility || "public",
|
|
1330
1626
|
graph,
|
|
1331
1627
|
}, userCtx);
|
|
1628
|
+
if (result.ok) markRepositoryIndexDirty(root);
|
|
1332
1629
|
json(res, result.ok ? (result.alreadyExists ? 200 : 201) : (result.conflict ? 409 : 400), result);
|
|
1333
1630
|
} catch (e) {
|
|
1334
1631
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
@@ -1372,7 +1669,13 @@ async function workspaceRoutes(req, res, ctx) {
|
|
|
1372
1669
|
json(res, 409, { error: "该流程已有新版本,请刷新流程仓库后重试" });
|
|
1373
1670
|
return;
|
|
1374
1671
|
}
|
|
1375
|
-
|
|
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;
|
|
1376
1679
|
originKind = "project-flow";
|
|
1377
1680
|
} else {
|
|
1378
1681
|
const source = readMarketplaceFlow(root, id, version, { ...userCtx, marketplaceScope: "all" });
|
|
@@ -1394,6 +1697,7 @@ async function workspaceRoutes(req, res, ctx) {
|
|
|
1394
1697
|
actorUserId: userCtx.userId || "",
|
|
1395
1698
|
eventId: `install:${originKind}:${id}@${version}:${userCtx.userId || "anonymous"}`,
|
|
1396
1699
|
});
|
|
1700
|
+
markRepositoryIndexDirty(root);
|
|
1397
1701
|
json(res, 201, {
|
|
1398
1702
|
ok: true,
|
|
1399
1703
|
id,
|
|
@@ -1429,17 +1733,27 @@ async function workspaceRoutes(req, res, ctx) {
|
|
|
1429
1733
|
return;
|
|
1430
1734
|
}
|
|
1431
1735
|
const metadata = writeProjectFlowMarketplaceMetadata(source._flowRoot, payload?.visibility);
|
|
1736
|
+
updateIndexedProjectFlowVisibility(root, id, metadata.visibility, metadata.updatedAt);
|
|
1432
1737
|
json(res, 200, { ok: true, kind: "project-flow", id, version: source.version, ...metadata });
|
|
1433
1738
|
return;
|
|
1434
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");
|
|
1435
1744
|
const result = setMarketplaceVisibility(
|
|
1436
1745
|
root,
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1746
|
+
resourceKind,
|
|
1747
|
+
resourceId,
|
|
1748
|
+
resourceVersion,
|
|
1749
|
+
resourceVisibility,
|
|
1441
1750
|
userCtx,
|
|
1442
1751
|
);
|
|
1752
|
+
if (result.ok && resourceKind === "node") {
|
|
1753
|
+
updateIndexedNodeVisibility(root, resourceId, resourceVersion, resourceVisibility);
|
|
1754
|
+
} else if (result.ok) {
|
|
1755
|
+
markRepositoryIndexDirty(root);
|
|
1756
|
+
}
|
|
1443
1757
|
json(res, result.ok ? 200 : 400, result);
|
|
1444
1758
|
} catch (e) {
|
|
1445
1759
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
@@ -1729,6 +2043,49 @@ async function workspaceRoutes(req, res, ctx) {
|
|
|
1729
2043
|
return;
|
|
1730
2044
|
}
|
|
1731
2045
|
|
|
2046
|
+
if (req.method === "GET" && url.pathname === "/api/workspace/node-review") {
|
|
2047
|
+
try {
|
|
2048
|
+
const nodeId = String(url.searchParams.get("nodeId") || "").trim();
|
|
2049
|
+
if (!nodeId) {
|
|
2050
|
+
json(res, 400, { error: "Missing nodeId" });
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
2054
|
+
flowId: url.searchParams.get("flowId") || "",
|
|
2055
|
+
flowSource: url.searchParams.get("flowSource") || "user",
|
|
2056
|
+
workspaceId: url.searchParams.get("workspaceId") || "",
|
|
2057
|
+
adminOwnerId: url.searchParams.get("adminOwnerId") || "",
|
|
2058
|
+
archived: url.searchParams.get("archived") === "1",
|
|
2059
|
+
}, userCtx);
|
|
2060
|
+
if (scoped.error) {
|
|
2061
|
+
json(res, scoped.status || 400, { error: scoped.error });
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
const scopedUserCtx = workspaceScopedUserContext(scoped, userCtx);
|
|
2065
|
+
const draftGraph = readWorkspaceGraph(scoped.root, root).graph;
|
|
2066
|
+
const stableRelease = readWorkspaceStableRelease(scoped.root, root);
|
|
2067
|
+
const draft = workspaceNodeReviewSnapshot(root, scoped.root, draftGraph, nodeId, scopedUserCtx, "Draft");
|
|
2068
|
+
const stable = stableRelease
|
|
2069
|
+
? workspaceNodeReviewSnapshot(root, stableRelease.root, stableRelease.graph, nodeId, scopedUserCtx, `Stable ${stableRelease.release.id}`)
|
|
2070
|
+
: null;
|
|
2071
|
+
if (!draft && !stable) {
|
|
2072
|
+
json(res, 404, { error: "Node not found" });
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
json(res, 200, {
|
|
2076
|
+
nodeId,
|
|
2077
|
+
draft,
|
|
2078
|
+
stable,
|
|
2079
|
+
stableReleaseId: stableRelease?.release?.id || "",
|
|
2080
|
+
stableRevision: stableRelease?.release?.designRevision || "",
|
|
2081
|
+
draftRevision: workspaceDesignRevision(draftGraph),
|
|
2082
|
+
});
|
|
2083
|
+
} catch (e) {
|
|
2084
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
2085
|
+
}
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
|
|
1732
2089
|
if (req.method === "GET" && url.pathname === "/api/workspaces") {
|
|
1733
2090
|
try {
|
|
1734
2091
|
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
@@ -1931,6 +2288,7 @@ async function workspaceRoutes(req, res, ctx) {
|
|
|
1931
2288
|
const graph = workspaceGraphWithScheduleMode(draftGraph, payload.scheduleMode || "disabled");
|
|
1932
2289
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
1933
2290
|
writeWorkspaceGraph(targetDir, graph, root);
|
|
2291
|
+
markRepositoryIndexDirty(root);
|
|
1934
2292
|
let collaborationCreated = false;
|
|
1935
2293
|
if (flowSource === "workspace") {
|
|
1936
2294
|
ensureWorkspaceCollaboration({ flowId, userId: authUser.userId });
|
|
@@ -2214,6 +2572,7 @@ async function workspaceRoutes(req, res, ctx) {
|
|
|
2214
2572
|
json(res, result.conflict ? 409 : 400, result);
|
|
2215
2573
|
return;
|
|
2216
2574
|
}
|
|
2575
|
+
markRepositoryIndexDirty(root);
|
|
2217
2576
|
const graph = readWorkspaceGraph(scoped.root, root).graph;
|
|
2218
2577
|
const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx);
|
|
2219
2578
|
broadcastWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
|
|
@@ -2262,6 +2621,7 @@ async function workspaceRoutes(req, res, ctx) {
|
|
|
2262
2621
|
json(res, 404, result);
|
|
2263
2622
|
return;
|
|
2264
2623
|
}
|
|
2624
|
+
markRepositoryIndexDirty(root);
|
|
2265
2625
|
const graph = readWorkspaceGraph(scoped.root, root).graph;
|
|
2266
2626
|
const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, userCtx);
|
|
2267
2627
|
broadcastWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
|
|
@@ -2389,6 +2749,7 @@ async function workspaceRoutes(req, res, ctx) {
|
|
|
2389
2749
|
}
|
|
2390
2750
|
throw new Error(`Workspace graph save rolled back: ${(scheduleError && scheduleError.message) || String(scheduleError)}`);
|
|
2391
2751
|
}
|
|
2752
|
+
markRepositoryIndexDirty(root);
|
|
2392
2753
|
broadcastWorkspaceCollaborationEvent(
|
|
2393
2754
|
userCtx,
|
|
2394
2755
|
scoped.flowSource,
|
|
@@ -2500,6 +2861,7 @@ async function workspaceRoutes(req, res, ctx) {
|
|
|
2500
2861
|
graph.instances[scheduleNodeId] = { ...instance, body: JSON.stringify(config) };
|
|
2501
2862
|
const committed = commitWorkspaceGraph(root, scoped, graph, userCtx);
|
|
2502
2863
|
const workspaceSchedules = syncWorkspaceSchedulesForGraph(root, scoped, committed.graph, authUser, userCtx);
|
|
2864
|
+
markRepositoryIndexDirty(root);
|
|
2503
2865
|
const schedule = workspaceSchedules.find((item) => item.scheduleNodeId === scheduleNodeId) || {
|
|
2504
2866
|
flowId,
|
|
2505
2867
|
flowSource,
|
|
@@ -3572,6 +3934,436 @@ async function workspaceRoutes(req, res, ctx) {
|
|
|
3572
3934
|
return;
|
|
3573
3935
|
}
|
|
3574
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
|
+
|
|
3575
4367
|
if (url.pathname === "/api/workspace/conversations") {
|
|
3576
4368
|
let payload = {};
|
|
3577
4369
|
if (req.method === "POST") {
|
|
@@ -4018,6 +4810,7 @@ async function workspaceRoutes(req, res, ctx) {
|
|
|
4018
4810
|
json(res, result.conflict ? 409 : 400, { error: result.error || "发布失败" });
|
|
4019
4811
|
return;
|
|
4020
4812
|
}
|
|
4813
|
+
markRepositoryIndexDirty(root);
|
|
4021
4814
|
json(res, 200, { ok: true, ...result });
|
|
4022
4815
|
} catch (e) {
|
|
4023
4816
|
json(res, 500, { error: (e && e.message) || String(e) });
|