@fieldwangai/agentflow 0.1.166 → 0.1.167
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/lib/agent-runners.mjs +27 -9
- package/bin/lib/ai-exploration.mjs +293 -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 +522 -0
- package/bin/lib/ui-server.mjs +167 -0
- package/bin/lib/workspace-routes.mjs +543 -210
- package/bin/lib/workspace-server.mjs +2 -0
- package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-DKdBGu3q.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 +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
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
import { collectPipelineNamesFromDir, listFlowsJson, readPipelineListDescription } from "./catalog-flows.mjs";
|
|
6
|
+
import { listMarketplaceNodes } from "./marketplace.mjs";
|
|
7
|
+
import { marketplaceStatsFor, marketplaceUsageStats, normalizeMarketplaceVisibility, readMarketplaceFlowOrigin } from "./marketplace-usage.mjs";
|
|
8
|
+
import {
|
|
9
|
+
getAgentflowDataRoot,
|
|
10
|
+
getUserPipelinesRoot,
|
|
11
|
+
listAgentflowUserIds,
|
|
12
|
+
} from "./paths.mjs";
|
|
13
|
+
import { getWorkspaceCollaborationByFlow } from "./workspace-collaboration.mjs";
|
|
14
|
+
import { onRepositoryRunFinished } from "./repository-index-events.mjs";
|
|
15
|
+
import { workspaceDesignRevision } from "./workspace-graph-merge.mjs";
|
|
16
|
+
import {
|
|
17
|
+
readWorkspaceGraph,
|
|
18
|
+
readWorkspaceRunUsageRecords,
|
|
19
|
+
readWorkspaceStableRelease,
|
|
20
|
+
workspaceRunPlan,
|
|
21
|
+
} from "./workspace-server.mjs";
|
|
22
|
+
|
|
23
|
+
const REPOSITORY_INDEX_VERSION = 1;
|
|
24
|
+
const REPOSITORY_INDEX_FILENAME = "repository-index.json";
|
|
25
|
+
const REPOSITORY_INDEX_MAX_AGE_MS = 5 * 60 * 1000;
|
|
26
|
+
const memoryIndexes = new Map();
|
|
27
|
+
const pendingRebuilds = new Set();
|
|
28
|
+
const pendingWrites = new Map();
|
|
29
|
+
|
|
30
|
+
function indexKey(workspaceRoot) {
|
|
31
|
+
return path.resolve(workspaceRoot);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function repositoryIndexPath(workspaceRoot) {
|
|
35
|
+
const root = indexKey(workspaceRoot);
|
|
36
|
+
const rootKey = crypto.createHash("sha256").update(root).digest("hex").slice(0, 20);
|
|
37
|
+
return path.join(getAgentflowDataRoot(), "admin", "repository-index", `${rootKey}-${REPOSITORY_INDEX_FILENAME}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function projectFlowRepositoryId(ownerUserId, flowSource, flowId) {
|
|
41
|
+
return `project-flow:${String(ownerUserId || "").trim()}:${String(flowSource || "user").trim()}:${String(flowId || "").trim()}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const PROJECT_FLOW_MARKETPLACE_FILENAME = path.join(".workspace", "agentflow", "marketplace.json");
|
|
45
|
+
|
|
46
|
+
export function projectFlowMarketplaceMetadataPath(flowRoot) {
|
|
47
|
+
return path.join(path.resolve(flowRoot), PROJECT_FLOW_MARKETPLACE_FILENAME);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function readProjectFlowMarketplaceMetadata(flowRoot) {
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(fs.readFileSync(projectFlowMarketplaceMetadataPath(flowRoot), "utf-8"));
|
|
53
|
+
return {
|
|
54
|
+
visibility: String(parsed?.visibility || "").trim() === "private" ? "private" : "public",
|
|
55
|
+
updatedAt: String(parsed?.updatedAt || "").trim(),
|
|
56
|
+
};
|
|
57
|
+
} catch {
|
|
58
|
+
return { visibility: "public", updatedAt: "" };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function writeProjectFlowMarketplaceMetadata(flowRoot, visibility) {
|
|
63
|
+
const filePath = projectFlowMarketplaceMetadataPath(flowRoot);
|
|
64
|
+
const value = {
|
|
65
|
+
version: 1,
|
|
66
|
+
visibility: String(visibility || "").trim() === "private" ? "private" : "public",
|
|
67
|
+
updatedAt: new Date().toISOString(),
|
|
68
|
+
};
|
|
69
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
70
|
+
const tempPath = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
71
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
|
|
72
|
+
fs.renameSync(tempPath, filePath);
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function projectFlowUpdatedAt(flowRoot, metadata = {}) {
|
|
77
|
+
if (metadata.updatedAt) return metadata.updatedAt;
|
|
78
|
+
try {
|
|
79
|
+
return fs.statSync(path.resolve(flowRoot)).mtime.toISOString();
|
|
80
|
+
} catch {
|
|
81
|
+
return "";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function validIsoFromMs(value) {
|
|
86
|
+
const time = Number(value || 0);
|
|
87
|
+
if (!Number.isFinite(time) || time <= 0) return "";
|
|
88
|
+
try {
|
|
89
|
+
return new Date(time).toISOString();
|
|
90
|
+
} catch {
|
|
91
|
+
return "";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function runUsageKey(ownerId, flowSource, flowId, runNodeId = "") {
|
|
96
|
+
return [ownerId, flowSource || "user", flowId, runNodeId].map((item) => String(item || "").trim()).join("\u0000");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function aggregateSuccessfulRuns() {
|
|
100
|
+
const aggregate = new Map();
|
|
101
|
+
for (const run of readWorkspaceRunUsageRecords()) {
|
|
102
|
+
if (run?.status !== "success") continue;
|
|
103
|
+
const flowSource = String(run.flowSource || "user");
|
|
104
|
+
const ownerId = flowSource === "user" ? String(run.userId || "") : "";
|
|
105
|
+
const key = runUsageKey(ownerId, flowSource, run.flowId, run.runNodeId || "");
|
|
106
|
+
const current = aggregate.get(key) || { count: 0, lastUsedAt: "" };
|
|
107
|
+
current.count += 1;
|
|
108
|
+
const at = validIsoFromMs(run.endedAt || run.at);
|
|
109
|
+
if (at && (!current.lastUsedAt || at > current.lastUsedAt)) current.lastUsedAt = at;
|
|
110
|
+
aggregate.set(key, current);
|
|
111
|
+
}
|
|
112
|
+
return aggregate;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function runnableProjectFlowEntries(graph, scopedRoot = "", options = {}) {
|
|
116
|
+
const includeGraph = options.includeGraph === true;
|
|
117
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
118
|
+
const entries = Object.entries(instances).filter(([, instance]) => (
|
|
119
|
+
instance?.definitionId === "workspace_run"
|
|
120
|
+
|| instance?.definitionId === "workspace_scheduled_run"
|
|
121
|
+
));
|
|
122
|
+
return entries.flatMap(([entryId, entry]) => {
|
|
123
|
+
let plan;
|
|
124
|
+
try {
|
|
125
|
+
plan = workspaceRunPlan(graph, entryId, scopedRoot, { ignoreCache: true });
|
|
126
|
+
} catch {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
const executedNodeIds = Array.from(new Set((plan.order || []).map((id) => String(id || "").trim()).filter(Boolean)));
|
|
130
|
+
if (executedNodeIds.length === 0) return [];
|
|
131
|
+
const includedNodeIds = new Set([entryId, ...executedNodeIds]);
|
|
132
|
+
const base = {
|
|
133
|
+
entryId,
|
|
134
|
+
entry,
|
|
135
|
+
runMode: entry.definitionId === "workspace_scheduled_run" ? "scheduled" : "manual",
|
|
136
|
+
nodeCount: includedNodeIds.size,
|
|
137
|
+
edgeCount: (Array.isArray(graph?.edges) ? graph.edges : []).filter((edge) => (
|
|
138
|
+
includedNodeIds.has(String(edge?.source || ""))
|
|
139
|
+
&& includedNodeIds.has(String(edge?.target || ""))
|
|
140
|
+
)).length,
|
|
141
|
+
};
|
|
142
|
+
if (!includeGraph) return [base];
|
|
143
|
+
const positions = graph?.ui?.nodePositions && typeof graph.ui.nodePositions === "object"
|
|
144
|
+
? Object.fromEntries(Object.entries(graph.ui.nodePositions).filter(([id]) => includedNodeIds.has(id)))
|
|
145
|
+
: {};
|
|
146
|
+
const sizes = graph?.ui?.nodeSizes && typeof graph.ui.nodeSizes === "object"
|
|
147
|
+
? Object.fromEntries(Object.entries(graph.ui.nodeSizes).filter(([id]) => includedNodeIds.has(id)))
|
|
148
|
+
: {};
|
|
149
|
+
return [{
|
|
150
|
+
...base,
|
|
151
|
+
graph: {
|
|
152
|
+
...graph,
|
|
153
|
+
instances: Object.fromEntries(Object.entries(instances).filter(([id]) => includedNodeIds.has(id))),
|
|
154
|
+
edges: (Array.isArray(graph?.edges) ? graph.edges : []).filter((edge) => (
|
|
155
|
+
includedNodeIds.has(String(edge?.source || ""))
|
|
156
|
+
&& includedNodeIds.has(String(edge?.target || ""))
|
|
157
|
+
)),
|
|
158
|
+
ui: { ...(graph?.ui || {}), nodePositions: positions, nodeSizes: sizes },
|
|
159
|
+
},
|
|
160
|
+
}];
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function scanProjectFlows(workspaceRoot, usageStats) {
|
|
165
|
+
const directRuns = aggregateSuccessfulRuns();
|
|
166
|
+
const resources = [];
|
|
167
|
+
const appendFlow = (ownerId, flow, flowSource = "user", workspaceId = "") => {
|
|
168
|
+
if (!ownerId || flow.archived || !flow.path) return;
|
|
169
|
+
let stable;
|
|
170
|
+
let graph;
|
|
171
|
+
try {
|
|
172
|
+
stable = readWorkspaceStableRelease(flow.path, workspaceRoot);
|
|
173
|
+
graph = stable?.graph || readWorkspaceGraph(flow.path, workspaceRoot).graph;
|
|
174
|
+
} catch {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const runnableEntries = runnableProjectFlowEntries(graph, flow.path);
|
|
178
|
+
if (runnableEntries.length === 0) return;
|
|
179
|
+
const metadata = readProjectFlowMarketplaceMetadata(flow.path);
|
|
180
|
+
for (const runnable of runnableEntries) {
|
|
181
|
+
const baseId = projectFlowRepositoryId(ownerId, flowSource, flow.id);
|
|
182
|
+
const id = runnableEntries.length === 1 ? baseId : `${baseId}:${runnable.entryId}`;
|
|
183
|
+
const version = stable?.release?.id || `current-${workspaceDesignRevision(graph).slice(0, 12)}`;
|
|
184
|
+
const rawEntryLabel = String(runnable.entry?.label || "").trim();
|
|
185
|
+
const genericLabel = ["", "Run", "Scheduled Run", "运行", "定时运行"].includes(rawEntryLabel);
|
|
186
|
+
const exactOwner = flowSource === "user" ? ownerId : "";
|
|
187
|
+
const exactRuns = directRuns.get(runUsageKey(exactOwner, flowSource, flow.id, runnable.entryId));
|
|
188
|
+
const unscopedRuns = runnableEntries.length === 1
|
|
189
|
+
? directRuns.get(runUsageKey(exactOwner, flowSource, flow.id, ""))
|
|
190
|
+
: null;
|
|
191
|
+
const telemetry = marketplaceStatsFor(usageStats, "project-flow", id, version);
|
|
192
|
+
const directCount = Number(exactRuns?.count || 0) + Number(unscopedRuns?.count || 0);
|
|
193
|
+
const directLastUsedAt = [exactRuns?.lastUsedAt, unscopedRuns?.lastUsedAt].filter(Boolean).sort().at(-1) || "";
|
|
194
|
+
resources.push({
|
|
195
|
+
resourceType: "flow",
|
|
196
|
+
projectFlow: true,
|
|
197
|
+
id,
|
|
198
|
+
definitionId: `${flow.id}/${runnable.entryId}`,
|
|
199
|
+
displayName: runnableEntries.length === 1 ? flow.id : `${flow.id} · ${genericLabel ? runnable.entryId : rawEntryLabel}`,
|
|
200
|
+
description: flow.description || "",
|
|
201
|
+
version,
|
|
202
|
+
versionLabel: stable?.release?.id ? `Stable ${stable.release.id}` : "当前版本",
|
|
203
|
+
runMode: runnable.runMode,
|
|
204
|
+
runModeLabel: runnable.runMode === "scheduled" ? "定时运行" : "手动运行",
|
|
205
|
+
ownerUserId: ownerId,
|
|
206
|
+
liveOwnerUserId: ownerId,
|
|
207
|
+
liveFlowId: flow.id,
|
|
208
|
+
liveFlowSource: flowSource,
|
|
209
|
+
liveWorkspaceId: workspaceId,
|
|
210
|
+
liveEntryId: runnable.entryId,
|
|
211
|
+
installFlowId: runnableEntries.length === 1 ? flow.id : `${flow.id}-${runnable.entryId}`,
|
|
212
|
+
visibility: metadata.visibility,
|
|
213
|
+
nodeCount: runnable.nodeCount,
|
|
214
|
+
edgeCount: runnable.edgeCount,
|
|
215
|
+
updatedAt: projectFlowUpdatedAt(flow.path, metadata),
|
|
216
|
+
...telemetry,
|
|
217
|
+
useCount: telemetry.useCount + directCount,
|
|
218
|
+
lastUsedAt: [telemetry.lastUsedAt, directLastUsedAt].filter(Boolean).sort().at(-1) || "",
|
|
219
|
+
flowRoot: flow.path,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
for (const ownerUserId of listAgentflowUserIds()) {
|
|
225
|
+
const ownerId = String(ownerUserId || "").trim();
|
|
226
|
+
const pipelinesRoot = getUserPipelinesRoot(ownerId);
|
|
227
|
+
for (const flowId of collectPipelineNamesFromDir(pipelinesRoot)) {
|
|
228
|
+
const flowRoot = path.join(pipelinesRoot, flowId);
|
|
229
|
+
const description = readPipelineListDescription(flowRoot);
|
|
230
|
+
appendFlow(ownerId, { id: flowId, path: flowRoot, ...(description ? { description } : {}) }, "user");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
for (const flow of listFlowsJson(workspaceRoot, { userId: "", includeWorkspaceFlows: true })) {
|
|
234
|
+
if ((flow.source || "") !== "workspace" || flow.archived || !flow.path) continue;
|
|
235
|
+
const collaboration = getWorkspaceCollaborationByFlow(flow.id, false);
|
|
236
|
+
const ownerId = String(collaboration?.ownerId || "").trim();
|
|
237
|
+
if (ownerId) appendFlow(ownerId, flow, "workspace", collaboration.id);
|
|
238
|
+
}
|
|
239
|
+
return resources;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function scanNodes(workspaceRoot, usageStats) {
|
|
243
|
+
return listMarketplaceNodes(workspaceRoot, null, { userId: "", isAdmin: true, marketplaceScope: "all" }).map((node) => ({
|
|
244
|
+
id: node.id,
|
|
245
|
+
version: node.version,
|
|
246
|
+
definitionId: node.definitionId,
|
|
247
|
+
baseDefinitionId: node.baseDefinitionId,
|
|
248
|
+
displayName: node.displayName,
|
|
249
|
+
description: node.description,
|
|
250
|
+
inputs: node.input,
|
|
251
|
+
outputs: node.output,
|
|
252
|
+
ui: node.ui,
|
|
253
|
+
packagedFiles: Array.isArray(node.packagedFiles) ? node.packagedFiles : [],
|
|
254
|
+
fileList: Array.isArray(node.fileList) ? node.fileList : [],
|
|
255
|
+
fileCount: Number(node.fileCount) || 0,
|
|
256
|
+
totalBytes: Number(node.totalBytes) || 0,
|
|
257
|
+
contentSha256: String(node.contentSha256 || ""),
|
|
258
|
+
archiveSha256: String(node.archiveSha256 || ""),
|
|
259
|
+
installedFrom: String(node.installedFrom || ""),
|
|
260
|
+
installedAt: String(node.installedAt || ""),
|
|
261
|
+
ownerUserId: node.ownerUserId || node.createdBy || "",
|
|
262
|
+
createdBy: node.createdBy || node.ownerUserId || "",
|
|
263
|
+
visibility: normalizeMarketplaceVisibility(node.visibility),
|
|
264
|
+
packageDir: node.packageDir,
|
|
265
|
+
source: node.source || "marketplace",
|
|
266
|
+
resourceType: "node",
|
|
267
|
+
installed: true,
|
|
268
|
+
...marketplaceStatsFor(usageStats, "node", node.id, node.version, node.ownerUserId || node.createdBy || ""),
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function scanFlowInstallations() {
|
|
273
|
+
const installations = {};
|
|
274
|
+
for (const rawUserId of listAgentflowUserIds()) {
|
|
275
|
+
const userId = String(rawUserId || "").trim();
|
|
276
|
+
if (!userId) continue;
|
|
277
|
+
const pipelinesRoot = getUserPipelinesRoot(userId);
|
|
278
|
+
const entries = [];
|
|
279
|
+
for (const flowId of collectPipelineNamesFromDir(pipelinesRoot)) {
|
|
280
|
+
const origin = readMarketplaceFlowOrigin(path.join(pipelinesRoot, flowId));
|
|
281
|
+
if (!origin) continue;
|
|
282
|
+
entries.push({ key: `${origin.id}@${origin.version}`, flowId });
|
|
283
|
+
}
|
|
284
|
+
if (entries.length > 0) installations[userId] = entries;
|
|
285
|
+
}
|
|
286
|
+
return installations;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function writeIndex(workspaceRoot, index) {
|
|
290
|
+
const filePath = repositoryIndexPath(workspaceRoot);
|
|
291
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
292
|
+
const tempPath = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
293
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(index)}\n`, "utf-8");
|
|
294
|
+
fs.renameSync(tempPath, filePath);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function scheduleIndexWrite(workspaceRoot) {
|
|
298
|
+
const root = indexKey(workspaceRoot);
|
|
299
|
+
if (pendingWrites.has(root)) return;
|
|
300
|
+
const timer = setTimeout(() => {
|
|
301
|
+
pendingWrites.delete(root);
|
|
302
|
+
const index = memoryIndexes.get(root);
|
|
303
|
+
if (!index) return;
|
|
304
|
+
try { writeIndex(root, index); } catch {}
|
|
305
|
+
}, 250);
|
|
306
|
+
timer.unref?.();
|
|
307
|
+
pendingWrites.set(root, timer);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function readIndex(workspaceRoot) {
|
|
311
|
+
try {
|
|
312
|
+
const parsed = JSON.parse(fs.readFileSync(repositoryIndexPath(workspaceRoot), "utf-8"));
|
|
313
|
+
if (
|
|
314
|
+
parsed?.version !== REPOSITORY_INDEX_VERSION
|
|
315
|
+
|| parsed.workspaceRoot !== indexKey(workspaceRoot)
|
|
316
|
+
|| !Array.isArray(parsed.flows)
|
|
317
|
+
|| !Array.isArray(parsed.nodes)
|
|
318
|
+
) return null;
|
|
319
|
+
if (!parsed.installations || typeof parsed.installations !== "object" || Array.isArray(parsed.installations)) parsed.installations = {};
|
|
320
|
+
return parsed;
|
|
321
|
+
} catch {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function rebuildRepositoryIndex(workspaceRoot) {
|
|
327
|
+
const root = indexKey(workspaceRoot);
|
|
328
|
+
const usageStats = marketplaceUsageStats(root);
|
|
329
|
+
const index = {
|
|
330
|
+
version: REPOSITORY_INDEX_VERSION,
|
|
331
|
+
workspaceRoot: root,
|
|
332
|
+
generatedAt: new Date().toISOString(),
|
|
333
|
+
flows: scanProjectFlows(root, usageStats),
|
|
334
|
+
nodes: scanNodes(root, usageStats),
|
|
335
|
+
installations: scanFlowInstallations(),
|
|
336
|
+
};
|
|
337
|
+
memoryIndexes.set(root, index);
|
|
338
|
+
writeIndex(root, index);
|
|
339
|
+
return index;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function scheduleRebuild(workspaceRoot) {
|
|
343
|
+
const root = indexKey(workspaceRoot);
|
|
344
|
+
if (pendingRebuilds.has(root)) return;
|
|
345
|
+
pendingRebuilds.add(root);
|
|
346
|
+
setImmediate(() => {
|
|
347
|
+
try {
|
|
348
|
+
rebuildRepositoryIndex(root);
|
|
349
|
+
} catch {
|
|
350
|
+
// The last valid index remains available; the next request retries reconciliation.
|
|
351
|
+
} finally {
|
|
352
|
+
pendingRebuilds.delete(root);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function getRepositoryIndex(workspaceRoot, options = {}) {
|
|
358
|
+
const root = indexKey(workspaceRoot);
|
|
359
|
+
if (options.force === true) return rebuildRepositoryIndex(root);
|
|
360
|
+
let index = memoryIndexes.get(root);
|
|
361
|
+
if (!index) {
|
|
362
|
+
index = readIndex(root);
|
|
363
|
+
if (index) memoryIndexes.set(root, index);
|
|
364
|
+
}
|
|
365
|
+
if (!index) return rebuildRepositoryIndex(root);
|
|
366
|
+
const generatedAt = Date.parse(index.generatedAt || "");
|
|
367
|
+
if (!Number.isFinite(generatedAt) || Date.now() - generatedAt > REPOSITORY_INDEX_MAX_AGE_MS) scheduleRebuild(root);
|
|
368
|
+
return index;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function markRepositoryIndexDirty(workspaceRoot) {
|
|
372
|
+
scheduleRebuild(workspaceRoot);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function updateIndexedProjectFlowVisibility(workspaceRoot, id, visibility, updatedAt = "") {
|
|
376
|
+
const root = indexKey(workspaceRoot);
|
|
377
|
+
const index = getRepositoryIndex(root);
|
|
378
|
+
let changed = false;
|
|
379
|
+
const flows = index.flows.map((flow) => {
|
|
380
|
+
if (flow.id !== id) return flow;
|
|
381
|
+
changed = true;
|
|
382
|
+
return { ...flow, visibility: visibility === "private" ? "private" : "public", updatedAt: updatedAt || flow.updatedAt };
|
|
383
|
+
});
|
|
384
|
+
if (!changed) return false;
|
|
385
|
+
const next = { ...index, generatedAt: new Date().toISOString(), flows };
|
|
386
|
+
memoryIndexes.set(root, next);
|
|
387
|
+
writeIndex(root, next);
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function updateIndexedNodeVisibility(workspaceRoot, id, version, visibility) {
|
|
392
|
+
const root = indexKey(workspaceRoot);
|
|
393
|
+
const index = getRepositoryIndex(root);
|
|
394
|
+
let changed = false;
|
|
395
|
+
const nodes = index.nodes.map((node) => {
|
|
396
|
+
if (node.id !== id || node.version !== version) return node;
|
|
397
|
+
changed = true;
|
|
398
|
+
return { ...node, visibility: visibility === "private" ? "private" : "public" };
|
|
399
|
+
});
|
|
400
|
+
if (!changed) return false;
|
|
401
|
+
const next = { ...index, generatedAt: new Date().toISOString(), nodes };
|
|
402
|
+
memoryIndexes.set(root, next);
|
|
403
|
+
writeIndex(root, next);
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function recordIndexedProjectFlowUse(workspaceRoot, run = {}) {
|
|
408
|
+
const root = indexKey(workspaceRoot);
|
|
409
|
+
let index = memoryIndexes.get(root);
|
|
410
|
+
if (!index) {
|
|
411
|
+
index = readIndex(root);
|
|
412
|
+
if (index) memoryIndexes.set(root, index);
|
|
413
|
+
}
|
|
414
|
+
if (!index) return false;
|
|
415
|
+
const flowId = String(run.flowId || "").trim();
|
|
416
|
+
const flowSource = String(run.flowSource || "user").trim() || "user";
|
|
417
|
+
const runNodeId = String(run.runNodeId || "").trim();
|
|
418
|
+
const userId = String(run.userId || "").trim();
|
|
419
|
+
const candidates = flowId ? index.flows.filter((flow) => (
|
|
420
|
+
flow.liveFlowId === flowId
|
|
421
|
+
&& flow.liveFlowSource === flowSource
|
|
422
|
+
&& (flowSource !== "user" || flow.ownerUserId === userId)
|
|
423
|
+
)) : [];
|
|
424
|
+
const matchingIds = new Set(
|
|
425
|
+
candidates
|
|
426
|
+
.filter((flow) => !runNodeId || flow.liveEntryId === runNodeId || candidates.length === 1)
|
|
427
|
+
.map((flow) => flow.id),
|
|
428
|
+
);
|
|
429
|
+
for (const resource of Array.isArray(run.marketplaceResources) ? run.marketplaceResources : []) {
|
|
430
|
+
if (resource?.kind !== "project-flow") continue;
|
|
431
|
+
const resourceId = String(resource.id || "");
|
|
432
|
+
const resourceVersion = String(resource.version || "");
|
|
433
|
+
for (const flow of index.flows) {
|
|
434
|
+
if (flow.id === resourceId && flow.version === resourceVersion) matchingIds.add(flow.id);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const usedNodes = new Set((Array.isArray(run.marketplaceResources) ? run.marketplaceResources : [])
|
|
438
|
+
.filter((resource) => resource?.kind === "node")
|
|
439
|
+
.map((resource) => `${String(resource.id || "")}@${String(resource.version || "")}`));
|
|
440
|
+
if (matchingIds.size === 0 && usedNodes.size === 0) return false;
|
|
441
|
+
const at = validIsoFromMs(run.endedAt || run.at || Date.now()) || new Date().toISOString();
|
|
442
|
+
const flows = index.flows.map((flow) => matchingIds.has(flow.id) ? {
|
|
443
|
+
...flow,
|
|
444
|
+
useCount: Number(flow.useCount || 0) + 1,
|
|
445
|
+
lastUsedAt: !flow.lastUsedAt || at > flow.lastUsedAt ? at : flow.lastUsedAt,
|
|
446
|
+
} : flow);
|
|
447
|
+
const nodes = index.nodes.map((node) => usedNodes.has(`${node.id}@${node.version}`) ? {
|
|
448
|
+
...node,
|
|
449
|
+
useCount: Number(node.useCount || 0) + 1,
|
|
450
|
+
lastUsedAt: !node.lastUsedAt || at > node.lastUsedAt ? at : node.lastUsedAt,
|
|
451
|
+
} : node);
|
|
452
|
+
const next = { ...index, flows, nodes };
|
|
453
|
+
memoryIndexes.set(root, next);
|
|
454
|
+
scheduleIndexWrite(root);
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
onRepositoryRunFinished((workspaceRoot, run, status) => {
|
|
459
|
+
if (status === "success") recordIndexedProjectFlowUse(workspaceRoot, run);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
function canAccessResource(item, userCtx = {}, scope = "all") {
|
|
463
|
+
const userId = String(userCtx.userId || "").trim();
|
|
464
|
+
const owned = String(item.ownerUserId || "").trim() === userId;
|
|
465
|
+
if (scope === "owned" && !owned && userCtx.isAdmin !== true) return false;
|
|
466
|
+
if (scope !== "owned" && item.visibility === "private" && !owned && userCtx.isAdmin !== true) return false;
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export function listIndexedProjectFlows(workspaceRoot, userCtx = {}, scope = "all") {
|
|
471
|
+
return getRepositoryIndex(workspaceRoot).flows
|
|
472
|
+
.filter((item) => canAccessResource(item, userCtx, scope))
|
|
473
|
+
.map((item) => ({ ...item, _flowRoot: item.flowRoot }));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export function listIndexedNodes(workspaceRoot, userCtx = {}, scope = "all") {
|
|
477
|
+
return getRepositoryIndex(workspaceRoot).nodes
|
|
478
|
+
.filter((item) => canAccessResource(item, userCtx, scope));
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export function indexedMarketplaceFlowCopies(workspaceRoot, userId) {
|
|
482
|
+
const entries = getRepositoryIndex(workspaceRoot).installations?.[String(userId || "").trim()] || [];
|
|
483
|
+
const copies = new Map();
|
|
484
|
+
for (const entry of entries) {
|
|
485
|
+
const flowIds = copies.get(entry.key) || [];
|
|
486
|
+
flowIds.push(entry.flowId);
|
|
487
|
+
copies.set(entry.key, flowIds);
|
|
488
|
+
}
|
|
489
|
+
return copies;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export function indexedProjectFlowPreview(workspaceRoot, indexedFlow) {
|
|
493
|
+
if (!indexedFlow?.flowRoot) return null;
|
|
494
|
+
let stable;
|
|
495
|
+
let graph;
|
|
496
|
+
try {
|
|
497
|
+
stable = readWorkspaceStableRelease(indexedFlow.flowRoot, workspaceRoot);
|
|
498
|
+
graph = stable?.graph || readWorkspaceGraph(indexedFlow.flowRoot, workspaceRoot).graph;
|
|
499
|
+
} catch {
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
const version = stable?.release?.id || `current-${workspaceDesignRevision(graph).slice(0, 12)}`;
|
|
503
|
+
if (version !== indexedFlow.version) return { stale: true, version };
|
|
504
|
+
const runnable = runnableProjectFlowEntries(graph, indexedFlow.flowRoot, { includeGraph: true })
|
|
505
|
+
.find((item) => item.entryId === indexedFlow.liveEntryId);
|
|
506
|
+
if (!runnable) return null;
|
|
507
|
+
return { graph: runnable.graph, version };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export function clearRepositoryIndexMemoryForTest(workspaceRoot = "") {
|
|
511
|
+
if (workspaceRoot) {
|
|
512
|
+
const root = indexKey(workspaceRoot);
|
|
513
|
+
memoryIndexes.delete(root);
|
|
514
|
+
const timer = pendingWrites.get(root);
|
|
515
|
+
if (timer) clearTimeout(timer);
|
|
516
|
+
pendingWrites.delete(root);
|
|
517
|
+
} else {
|
|
518
|
+
memoryIndexes.clear();
|
|
519
|
+
for (const timer of pendingWrites.values()) clearTimeout(timer);
|
|
520
|
+
pendingWrites.clear();
|
|
521
|
+
}
|
|
522
|
+
}
|