@fieldwangai/agentflow 0.1.162 → 0.1.164
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/flow-dsl/codegen.mjs +23 -2
- package/bin/lib/flow-dsl/lint.mjs +44 -0
- package/bin/lib/flow-dsl/parser.mjs +68 -1
- package/bin/lib/marketplace-usage.mjs +218 -0
- package/bin/lib/marketplace.mjs +183 -3
- package/bin/lib/node-package-manifest.mjs +1 -1
- package/bin/lib/spaces.mjs +200 -0
- package/bin/lib/ui-server.mjs +314 -76
- package/bin/lib/workspace-routes.mjs +396 -5
- package/bin/lib/workspace-run-logs.mjs +2 -0
- package/bin/lib/workspace-server.mjs +698 -48
- package/bin/lib/workspace-state.mjs +2 -1
- package/builtin/nodes/agent_subAgent.md +5 -1
- package/builtin/nodes/context_bundle.md +44 -0
- package/builtin/nodes/context_knowledge.md +29 -0
- package/builtin/nodes/context_skills.md +29 -0
- package/builtin/nodes/context_workspace.md +36 -0
- package/builtin/nodes/control_while.md +7 -0
- package/builtin/nodes/tool_git_worktree_load.md +4 -3
- package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-CTrXOZ00.js → WorkflowAssistantThread-CffIx5BY.js} +1 -1
- package/builtin/web-ui/dist/assets/index-CDItaRfX.css +1 -0
- package/builtin/web-ui/dist/assets/index-CWIcfWHO.js +873 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/shared/slot-types.js +1 -0
- package/skills/agentflow-cli/SKILL.md +50 -3
- package/skills/agentflow-cli/runtime/bin/lib/skill-runtime.mjs +120 -6
- package/skills/agentflow-cli/runtime/builtin/nodes/agent_subAgent.md +5 -1
- package/skills/agentflow-cli/runtime/builtin/nodes/context_bundle.md +44 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/context_knowledge.md +29 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/context_skills.md +29 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/context_workspace.md +36 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/control_while.md +7 -0
- package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_worktree_load.md +4 -3
- package/skills/agentflow-cli/runtime/package.json +1 -1
- package/skills/agentflow-cli/scripts/agentflow-cli.mjs +64 -0
- package/skills/agentflow-flow-dsl/SKILL.md +48 -2
- package/skills/agentflow-flow-dsl/references/node-calls.md +6 -2
- package/skills/agentflow-flow-dsl/references/subflow-authoring.md +34 -7
- package/skills/agentflow-node-reference/references/builtin-nodes.md +37 -5
- package/builtin/web-ui/dist/assets/index-5uJFccdX.css +0 -1
- package/builtin/web-ui/dist/assets/index-BeUfNQRL.js +0 -873
|
@@ -84,7 +84,7 @@ export function generateFlowSource(ir, opts = {}) {
|
|
|
84
84
|
taken.add(name);
|
|
85
85
|
return name;
|
|
86
86
|
};
|
|
87
|
-
const flowApiRoots = ["agent", "control", "display", "file", "flow", "provide", "tool", "workspace"];
|
|
87
|
+
const flowApiRoots = ["agent", "context", "control", "display", "file", "flow", "provide", "tool", "workspace"];
|
|
88
88
|
const flowApiBinding = new Map(flowApiRoots.map((root) => [root, uniqueName(root, `${root}Api`)]));
|
|
89
89
|
const apiCall = (name) => {
|
|
90
90
|
const [root, ...tail] = String(name || "").split(".");
|
|
@@ -210,6 +210,13 @@ export function generateFlowSource(ir, opts = {}) {
|
|
|
210
210
|
const pinValue = (id, name, value) => {
|
|
211
211
|
const text = String(value);
|
|
212
212
|
if (slotTypeOf(id, name) === "bool" && (text === "true" || text === "false")) return text;
|
|
213
|
+
if (slotTypeOf(id, name) === "json") {
|
|
214
|
+
try {
|
|
215
|
+
return JSON.stringify(JSON.parse(text), null, 2);
|
|
216
|
+
} catch {
|
|
217
|
+
// Keep invalid draft values round-trippable; lint/runtime will report the bad JSON.
|
|
218
|
+
}
|
|
219
|
+
}
|
|
213
220
|
return textArg(id, name, text);
|
|
214
221
|
};
|
|
215
222
|
|
|
@@ -228,7 +235,21 @@ export function generateFlowSource(ir, opts = {}) {
|
|
|
228
235
|
const key = isIdentifier(name) ? name : JSON.stringify(name);
|
|
229
236
|
if (wired.has(name)) {
|
|
230
237
|
const x = wired.get(name);
|
|
231
|
-
|
|
238
|
+
const sourceDefinitionId = N[x.from]?.definitionId;
|
|
239
|
+
const bundleAlias = node.definitionId === "context_bundle"
|
|
240
|
+
? ({ knowledgeContext: "knowledge", skillsContext: "skills", workspaceContext: "workspace", mcpContext: "mcp" }[name] || "")
|
|
241
|
+
: "";
|
|
242
|
+
const contextResourceOutput = {
|
|
243
|
+
context_knowledge: "knowledgeContext",
|
|
244
|
+
context_skills: "skillsContext",
|
|
245
|
+
context_workspace: "workspaceContext",
|
|
246
|
+
}[sourceDefinitionId] || "";
|
|
247
|
+
const directContextBundle = name === "context" && sourceDefinitionId === "context_bundle" && x.fromSlot === "context";
|
|
248
|
+
const renderedKey = bundleAlias || key;
|
|
249
|
+
const renderedValue = directContextBundle || (bundleAlias && contextResourceOutput === name && x.fromSlot === name)
|
|
250
|
+
? x.from
|
|
251
|
+
: (outVar.get(`${x.from}|${x.fromSlot}`) || `${x.from}.${x.fromSlot}`);
|
|
252
|
+
lines.push(`${renderedKey}: ${renderedValue}`);
|
|
232
253
|
continue;
|
|
233
254
|
}
|
|
234
255
|
if (node.inputs[name] !== undefined) {
|
|
@@ -60,6 +60,16 @@ function declaredSlotType(node, kind, name) {
|
|
|
60
60
|
return "";
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
function parseJsonInput(node, name) {
|
|
64
|
+
const raw = String(node?.inputs?.[name] || "").trim();
|
|
65
|
+
if (!raw) return { value: null, error: "为空" };
|
|
66
|
+
try {
|
|
67
|
+
return { value: JSON.parse(raw), error: "" };
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return { value: null, error: error.message };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
63
73
|
function walk(node, visit) {
|
|
64
74
|
if (!node || typeof node !== "object") return;
|
|
65
75
|
if (node.type) visit(node);
|
|
@@ -179,6 +189,29 @@ export function lintFlowDir(flowDir, opts = {}) {
|
|
|
179
189
|
if (definitionId === "control_subflow_call" && !subflows[node.attrs?.subflowId]) {
|
|
180
190
|
errors.push(`${id}: 引用的子流程 ${node.attrs?.subflowId || "(empty)"} 不存在`);
|
|
181
191
|
}
|
|
192
|
+
if (definitionId === "context_knowledge") {
|
|
193
|
+
const parsed = parseJsonInput(node, "workspaceIds");
|
|
194
|
+
if (parsed.error) errors.push(`${id}: context.knowledge workspaceIds 必须是 JSON 数组(${parsed.error})`);
|
|
195
|
+
else if (!Array.isArray(parsed.value) || parsed.value.length === 0) errors.push(`${id}: context.knowledge 至少选择一个 Workspace ID`);
|
|
196
|
+
else if (parsed.value.some((value) => typeof value !== "string" || !String(value).trim())) errors.push(`${id}: context.knowledge workspaceIds 只能包含非空字符串 ID`);
|
|
197
|
+
}
|
|
198
|
+
if (definitionId === "context_skills") {
|
|
199
|
+
const parsed = parseJsonInput(node, "skills");
|
|
200
|
+
if (parsed.error) errors.push(`${id}: context.skills skills 必须是 JSON 数组(${parsed.error})`);
|
|
201
|
+
else if (!Array.isArray(parsed.value) || parsed.value.length === 0) errors.push(`${id}: context.skills 至少声明一个 skill`);
|
|
202
|
+
}
|
|
203
|
+
if (definitionId === "context_workspace") {
|
|
204
|
+
const workspaceId = String(node.inputs?.workspaceId || "current").trim();
|
|
205
|
+
const access = String(node.inputs?.access || "read-write").trim().toLowerCase();
|
|
206
|
+
if (!workspaceId) errors.push(`${id}: context.workspace workspaceId 不能为空`);
|
|
207
|
+
if (!["read-only", "read-write"].includes(access)) errors.push(`${id}: context.workspace access 只能是 read-only 或 read-write`);
|
|
208
|
+
}
|
|
209
|
+
if (definitionId === "context_bundle") {
|
|
210
|
+
const incoming = ir.edges.filter((edge) => edge.split("|")[2] === id && edge.split("|")[3] !== "prev");
|
|
211
|
+
if (!incoming.length) errors.push(`${id}: context.bundle 至少连接一个 Context 资源`);
|
|
212
|
+
const literal = Object.entries(node.inputs || {}).filter(([, value]) => String(value || "").trim());
|
|
213
|
+
if (literal.length) errors.push(`${id}: context.bundle 只能连接资源节点,不能内嵌 Context 正文`);
|
|
214
|
+
}
|
|
182
215
|
if (definitionId === "control_while") {
|
|
183
216
|
const conditionId = String(node.attrs?.conditionSubflowId || "");
|
|
184
217
|
const bodyId = String(node.attrs?.bodySubflowId || "");
|
|
@@ -206,9 +239,11 @@ export function lintFlowDir(flowDir, opts = {}) {
|
|
|
206
239
|
}
|
|
207
240
|
if (body && !body.outputs?.state) errors.push(`${id}: Body 子流程 ${bodyId} 缺少输出 state`);
|
|
208
241
|
for (const [contract, name, expected] of [
|
|
242
|
+
[condition?.inputs, "context", "context"],
|
|
209
243
|
[condition?.inputs, "state", "json"],
|
|
210
244
|
[condition?.inputs, "iteration", "text"],
|
|
211
245
|
[condition?.outputs, "decision", "text"],
|
|
246
|
+
[body?.inputs, "context", "context"],
|
|
212
247
|
[body?.inputs, "state", "json"],
|
|
213
248
|
[body?.inputs, "iteration", "text"],
|
|
214
249
|
[body?.inputs, "idempotencyKey", "text"],
|
|
@@ -241,6 +276,15 @@ export function lintFlowDir(flowDir, opts = {}) {
|
|
|
241
276
|
for (const slot of node.extraIn) if (!defIn.has(slot)) errors.push(`${id}[${definitionId}]: 不存在的输入槽 "${slot}"`);
|
|
242
277
|
for (const slot of node.extraOut) if (!defOut.has(slot)) errors.push(`${id}[${definitionId}]: 不存在的输出槽 "${slot}"`);
|
|
243
278
|
}
|
|
279
|
+
if (definitionId === "agent_subAgent") {
|
|
280
|
+
const incomingNames = new Set(ir.edges
|
|
281
|
+
.map((edge) => edge.split("|"))
|
|
282
|
+
.filter((parts) => parts[2] === id)
|
|
283
|
+
.map((parts) => parts[3]));
|
|
284
|
+
if (incomingNames.has("context") && ["knowledgeContext", "skillsContext", "workspaceContext", "mcpContext"].some((name) => incomingNames.has(name))) {
|
|
285
|
+
warnings.push(`${id}: 已连接 context Bundle,不要再重复连接旧 Context 文本引脚`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
244
288
|
}
|
|
245
289
|
|
|
246
290
|
const inputSeen = new Map();
|
|
@@ -101,6 +101,39 @@ export function parseFlowSource(source, opts = {}) {
|
|
|
101
101
|
return null;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/** Read a JSON-compatible literal without evaluating user code. */
|
|
105
|
+
function staticJsonOf(node) {
|
|
106
|
+
if (!node) return { ok: false, value: null };
|
|
107
|
+
if (node.type === "Literal") return { ok: true, value: node.value };
|
|
108
|
+
if (node.type === "UnaryExpression" && (node.operator === "-" || node.operator === "+")
|
|
109
|
+
&& node.argument.type === "Literal" && typeof node.argument.value === "number") {
|
|
110
|
+
return { ok: true, value: node.operator === "-" ? -node.argument.value : node.argument.value };
|
|
111
|
+
}
|
|
112
|
+
if (node.type === "TemplateLiteral" && node.expressions.length === 0) {
|
|
113
|
+
return { ok: true, value: node.quasis[0].value.cooked };
|
|
114
|
+
}
|
|
115
|
+
if (node.type === "ArrayExpression") {
|
|
116
|
+
const value = [];
|
|
117
|
+
for (const item of node.elements) {
|
|
118
|
+
const parsed = staticJsonOf(item);
|
|
119
|
+
if (!parsed.ok) return { ok: false, value: null };
|
|
120
|
+
value.push(parsed.value);
|
|
121
|
+
}
|
|
122
|
+
return { ok: true, value };
|
|
123
|
+
}
|
|
124
|
+
if (node.type === "ObjectExpression") {
|
|
125
|
+
const value = {};
|
|
126
|
+
for (const prop of node.properties) {
|
|
127
|
+
if (prop.type !== "Property" || prop.computed || prop.kind !== "init") return { ok: false, value: null };
|
|
128
|
+
const parsed = staticJsonOf(prop.value);
|
|
129
|
+
if (!parsed.ok) return { ok: false, value: null };
|
|
130
|
+
value[String(prop.key.name ?? prop.key.value ?? "")] = parsed.value;
|
|
131
|
+
}
|
|
132
|
+
return { ok: true, value };
|
|
133
|
+
}
|
|
134
|
+
return { ok: false, value: null };
|
|
135
|
+
}
|
|
136
|
+
|
|
104
137
|
// import 绑定 -> 代码节点包
|
|
105
138
|
const packageOf = new Map();
|
|
106
139
|
for (const stmt of ast.body) {
|
|
@@ -196,6 +229,8 @@ export function parseFlowSource(source, opts = {}) {
|
|
|
196
229
|
const def = definitionOf(definitionId);
|
|
197
230
|
const defInputs = new Set(def.input.map((s) => s.name));
|
|
198
231
|
const defOutputs = new Set(def.output.map((s) => s.name));
|
|
232
|
+
const inputType = new Map(def.input.map((s) => [s.name, String(s.type || "text")]));
|
|
233
|
+
const outputType = new Map(def.output.map((s) => [s.name, String(s.type || "text")]));
|
|
199
234
|
const node = nodes[id];
|
|
200
235
|
if (!obj || obj.type !== "ObjectExpression") return;
|
|
201
236
|
for (const prop of obj.properties) {
|
|
@@ -207,9 +242,32 @@ export function parseFlowSource(source, opts = {}) {
|
|
|
207
242
|
unresolvedAt(prop, `${id}: 引脚名不能是动态表达式`);
|
|
208
243
|
continue;
|
|
209
244
|
}
|
|
210
|
-
const
|
|
245
|
+
const rawKey = prop.key.name ?? prop.key.value;
|
|
246
|
+
const key = definitionId === "context_bundle"
|
|
247
|
+
? ({ knowledge: "knowledgeContext", skills: "skillsContext", workspace: "workspaceContext", mcp: "mcpContext" }[rawKey] || rawKey)
|
|
248
|
+
: rawKey;
|
|
211
249
|
const isCustom = !STD_SLOTS.has(key) && !defInputs.has(key);
|
|
212
250
|
|
|
251
|
+
// Context resources are values, not control nodes. A bundle accepts the resource variable
|
|
252
|
+
// directly (`{ knowledge }`), and consumers accept the bundle directly (`{ context: ctx }`).
|
|
253
|
+
if (prop.value.type === "Identifier" && nodes[prop.value.name]) {
|
|
254
|
+
const sourceDefinitionId = nodes[prop.value.name].definitionId;
|
|
255
|
+
const contextOutput = {
|
|
256
|
+
context_knowledge: "knowledgeContext",
|
|
257
|
+
context_skills: "skillsContext",
|
|
258
|
+
context_workspace: "workspaceContext",
|
|
259
|
+
context_bundle: "context",
|
|
260
|
+
}[sourceDefinitionId];
|
|
261
|
+
if (contextOutput && (
|
|
262
|
+
(definitionId === "context_bundle" && key === contextOutput)
|
|
263
|
+
|| (key === "context" && sourceDefinitionId === "context_bundle")
|
|
264
|
+
)) {
|
|
265
|
+
edges.push(`${prop.value.name}|${contextOutput}|${id}|${key}`);
|
|
266
|
+
if (isCustom) node.extraIn.push(key);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
213
271
|
const member = memberPath(prop.value);
|
|
214
272
|
if (member) {
|
|
215
273
|
edges.push(`${member[0]}|${member[1]}|${id}|${key}`);
|
|
@@ -228,6 +286,15 @@ export function parseFlowSource(source, opts = {}) {
|
|
|
228
286
|
continue;
|
|
229
287
|
}
|
|
230
288
|
const text = stringOf(prop.value);
|
|
289
|
+
if (text === null && (inputType.get(key) === "json" || outputType.get(key) === "json")) {
|
|
290
|
+
const parsed = staticJsonOf(prop.value);
|
|
291
|
+
if (parsed.ok) {
|
|
292
|
+
if (defOutputs.has(key) && !defInputs.has(key)) node.outputs[key] = JSON.stringify(parsed.value);
|
|
293
|
+
else node.inputs[key] = JSON.stringify(parsed.value);
|
|
294
|
+
if (isCustom) node.extraIn.push(key);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
231
298
|
if (text === null) {
|
|
232
299
|
unresolvedAt(prop, `${id}.${key}: 引脚值既不是上游引用也不是字面量`);
|
|
233
300
|
continue;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
import { MARKETPLACE_PACKAGES_DIR } from "./paths.mjs";
|
|
5
|
+
|
|
6
|
+
const USAGE_DIRNAME = "usage";
|
|
7
|
+
const FLOW_ORIGIN_FILENAME = ".agentflow-marketplace-origin.json";
|
|
8
|
+
|
|
9
|
+
function usageRoot(workspaceRoot) {
|
|
10
|
+
return path.join(path.resolve(workspaceRoot), path.dirname(MARKETPLACE_PACKAGES_DIR), USAGE_DIRNAME);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function dayKey(timeMs) {
|
|
14
|
+
const date = new Date(Number(timeMs) || Date.now());
|
|
15
|
+
return [
|
|
16
|
+
date.getFullYear(),
|
|
17
|
+
String(date.getMonth() + 1).padStart(2, "0"),
|
|
18
|
+
String(date.getDate()).padStart(2, "0"),
|
|
19
|
+
].join("-");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function safeText(value, max = 240) {
|
|
23
|
+
return String(value || "").trim().slice(0, max);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function normalizeMarketplaceVisibility(value, fallback = "public") {
|
|
27
|
+
const normalized = safeText(value, 20).toLowerCase();
|
|
28
|
+
if (normalized === "private") return "private";
|
|
29
|
+
if (normalized === "public") return "public";
|
|
30
|
+
return fallback === "private" ? "private" : "public";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function marketplaceResourceKey(kind, id, version) {
|
|
34
|
+
return `${safeText(kind, 32)}:${safeText(id)}@${safeText(version, 80)}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function appendMarketplaceUsageEvent(workspaceRoot, event = {}) {
|
|
38
|
+
try {
|
|
39
|
+
const kind = safeText(event.kind, 32);
|
|
40
|
+
const id = safeText(event.id);
|
|
41
|
+
const version = safeText(event.version, 80);
|
|
42
|
+
const action = safeText(event.action, 32);
|
|
43
|
+
const actorUserId = safeText(event.actorUserId || event.userId, 160);
|
|
44
|
+
const at = Number(event.at || Date.now());
|
|
45
|
+
const eventId = safeText(
|
|
46
|
+
event.eventId || `${action}:${kind}:${id}@${version}:${actorUserId}:${at}`,
|
|
47
|
+
500,
|
|
48
|
+
);
|
|
49
|
+
if (!kind || !id || !version || !["install", "use"].includes(action) || !eventId) return false;
|
|
50
|
+
const record = {
|
|
51
|
+
version: 1,
|
|
52
|
+
eventId,
|
|
53
|
+
kind,
|
|
54
|
+
id,
|
|
55
|
+
resourceVersion: version,
|
|
56
|
+
action,
|
|
57
|
+
actorUserId,
|
|
58
|
+
runId: safeText(event.runId, 240),
|
|
59
|
+
at: Number.isFinite(at) && at > 0 ? at : Date.now(),
|
|
60
|
+
};
|
|
61
|
+
const filePath = path.join(usageRoot(workspaceRoot), `${dayKey(record.at)}.jsonl`);
|
|
62
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
63
|
+
fs.appendFileSync(filePath, `${JSON.stringify(record)}\n`, "utf-8");
|
|
64
|
+
return true;
|
|
65
|
+
} catch {
|
|
66
|
+
// Marketplace telemetry must never break publishing, installing, or running.
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readUsageEvents(workspaceRoot) {
|
|
72
|
+
const dir = usageRoot(workspaceRoot);
|
|
73
|
+
if (!fs.existsSync(dir)) return [];
|
|
74
|
+
const events = [];
|
|
75
|
+
try {
|
|
76
|
+
const files = fs.readdirSync(dir, { withFileTypes: true })
|
|
77
|
+
.filter((entry) => entry.isFile() && /^\d{4}-\d{2}-\d{2}\.jsonl$/.test(entry.name))
|
|
78
|
+
.map((entry) => path.join(dir, entry.name))
|
|
79
|
+
.sort();
|
|
80
|
+
for (const filePath of files) {
|
|
81
|
+
for (const line of fs.readFileSync(filePath, "utf-8").split(/\r?\n/)) {
|
|
82
|
+
if (!line.trim()) continue;
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(line);
|
|
85
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) events.push(parsed);
|
|
86
|
+
} catch {
|
|
87
|
+
// Ignore individual malformed telemetry records.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
return events;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function marketplaceUsageStats(workspaceRoot) {
|
|
98
|
+
const byResource = new Map();
|
|
99
|
+
const seenEvents = new Set();
|
|
100
|
+
for (const event of readUsageEvents(workspaceRoot)) {
|
|
101
|
+
const eventId = safeText(event.eventId, 500);
|
|
102
|
+
if (!eventId || seenEvents.has(eventId)) continue;
|
|
103
|
+
seenEvents.add(eventId);
|
|
104
|
+
const key = marketplaceResourceKey(event.kind, event.id, event.resourceVersion);
|
|
105
|
+
if (!byResource.has(key)) {
|
|
106
|
+
byResource.set(key, {
|
|
107
|
+
useCount: 0,
|
|
108
|
+
installCount: 0,
|
|
109
|
+
uniqueUserCount: 0,
|
|
110
|
+
lastUsedAt: "",
|
|
111
|
+
_users: new Set(),
|
|
112
|
+
_actors: new Map(),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const stats = byResource.get(key);
|
|
116
|
+
const actorUserId = safeText(event.actorUserId, 160);
|
|
117
|
+
if (actorUserId) stats._users.add(actorUserId);
|
|
118
|
+
if (actorUserId && !stats._actors.has(actorUserId)) stats._actors.set(actorUserId, { useCount: 0, installCount: 0 });
|
|
119
|
+
if (event.action === "use") {
|
|
120
|
+
stats.useCount += 1;
|
|
121
|
+
if (actorUserId) stats._actors.get(actorUserId).useCount += 1;
|
|
122
|
+
const atIso = new Date(Number(event.at) || 0).toISOString();
|
|
123
|
+
if (!stats.lastUsedAt || atIso > stats.lastUsedAt) stats.lastUsedAt = atIso;
|
|
124
|
+
} else if (event.action === "install") {
|
|
125
|
+
stats.installCount += 1;
|
|
126
|
+
if (actorUserId) stats._actors.get(actorUserId).installCount += 1;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
for (const stats of byResource.values()) {
|
|
130
|
+
stats.uniqueUserCount = stats._users.size;
|
|
131
|
+
}
|
|
132
|
+
return byResource;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function marketplaceStatsFor(statsByResource, kind, id, version, ownerUserId = "") {
|
|
136
|
+
const stats = statsByResource.get(marketplaceResourceKey(kind, id, version));
|
|
137
|
+
if (!stats) return {
|
|
138
|
+
useCount: 0,
|
|
139
|
+
installCount: 0,
|
|
140
|
+
uniqueUserCount: 0,
|
|
141
|
+
lastUsedAt: "",
|
|
142
|
+
};
|
|
143
|
+
const owner = safeText(ownerUserId, 160);
|
|
144
|
+
const ownerStats = owner ? stats._actors.get(owner) : null;
|
|
145
|
+
return {
|
|
146
|
+
useCount: Math.max(0, stats.useCount - Number(ownerStats?.useCount || 0)),
|
|
147
|
+
installCount: Math.max(0, stats.installCount - Number(ownerStats?.installCount || 0)),
|
|
148
|
+
uniqueUserCount: Math.max(0, stats.uniqueUserCount - (owner && stats._users.has(owner) ? 1 : 0)),
|
|
149
|
+
lastUsedAt: stats.lastUsedAt,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function marketplaceFlowOriginPath(flowDir) {
|
|
154
|
+
return path.join(path.resolve(flowDir), FLOW_ORIGIN_FILENAME);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function writeMarketplaceFlowOrigin(flowDir, origin = {}) {
|
|
158
|
+
const value = {
|
|
159
|
+
kind: "flow",
|
|
160
|
+
id: safeText(origin.id),
|
|
161
|
+
version: safeText(origin.version, 80),
|
|
162
|
+
installedAt: safeText(origin.installedAt || new Date().toISOString(), 80),
|
|
163
|
+
};
|
|
164
|
+
if (!value.id || !value.version) throw new Error("Invalid marketplace flow origin");
|
|
165
|
+
fs.writeFileSync(marketplaceFlowOriginPath(flowDir), `${JSON.stringify(value, null, 2)}\n`, "utf-8");
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function readMarketplaceFlowOrigin(flowDir) {
|
|
170
|
+
try {
|
|
171
|
+
const parsed = JSON.parse(fs.readFileSync(marketplaceFlowOriginPath(flowDir), "utf-8"));
|
|
172
|
+
if (parsed?.kind !== "flow" || !parsed.id || !parsed.version) return null;
|
|
173
|
+
return { kind: "flow", id: String(parsed.id), version: String(parsed.version) };
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function parseMarketplaceNodeRef(instance = {}) {
|
|
180
|
+
const ref = safeText(instance.marketplaceRef || instance.definitionId, 500);
|
|
181
|
+
if (!ref.startsWith("marketplace:")) return null;
|
|
182
|
+
const spec = ref.slice("marketplace:".length);
|
|
183
|
+
const at = spec.lastIndexOf("@");
|
|
184
|
+
if (at <= 0 || at === spec.length - 1) return null;
|
|
185
|
+
return { kind: "node", id: spec.slice(0, at), version: spec.slice(at + 1) };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function marketplaceResourcesForRun(flowDir, graph, executedNodeIds = []) {
|
|
189
|
+
const resources = new Map();
|
|
190
|
+
const flowOrigin = readMarketplaceFlowOrigin(flowDir);
|
|
191
|
+
if (flowOrigin) resources.set(marketplaceResourceKey(flowOrigin.kind, flowOrigin.id, flowOrigin.version), flowOrigin);
|
|
192
|
+
const ids = Array.isArray(executedNodeIds) && executedNodeIds.length
|
|
193
|
+
? executedNodeIds.map((id) => String(id))
|
|
194
|
+
: Object.keys(graph?.instances || {});
|
|
195
|
+
for (const nodeId of ids) {
|
|
196
|
+
const ref = parseMarketplaceNodeRef(graph?.instances?.[nodeId]);
|
|
197
|
+
if (ref) resources.set(marketplaceResourceKey(ref.kind, ref.id, ref.version), ref);
|
|
198
|
+
}
|
|
199
|
+
return [...resources.values()];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function recordMarketplaceRunUsage(workspaceRoot, resources = [], run = {}) {
|
|
203
|
+
if (String(run.status || "") !== "success") return;
|
|
204
|
+
if (!String(workspaceRoot || "").trim() || !Array.isArray(resources) || resources.length === 0) return;
|
|
205
|
+
const runId = safeText(run.runId, 240);
|
|
206
|
+
const actorUserId = safeText(run.userId || run.actorUserId, 160);
|
|
207
|
+
if (!runId) return;
|
|
208
|
+
for (const resource of resources) {
|
|
209
|
+
appendMarketplaceUsageEvent(workspaceRoot, {
|
|
210
|
+
...resource,
|
|
211
|
+
action: "use",
|
|
212
|
+
actorUserId,
|
|
213
|
+
runId,
|
|
214
|
+
at: run.endedAt || Date.now(),
|
|
215
|
+
eventId: `use:${resource.kind}:${resource.id}@${resource.version}:${runId}`,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|