@fieldwangai/agentflow 0.1.162 → 0.1.163

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.
Files changed (41) hide show
  1. package/bin/lib/flow-dsl/codegen.mjs +23 -2
  2. package/bin/lib/flow-dsl/lint.mjs +44 -0
  3. package/bin/lib/flow-dsl/parser.mjs +68 -1
  4. package/bin/lib/marketplace-usage.mjs +218 -0
  5. package/bin/lib/marketplace.mjs +183 -3
  6. package/bin/lib/node-package-manifest.mjs +1 -1
  7. package/bin/lib/spaces.mjs +200 -0
  8. package/bin/lib/ui-server.mjs +251 -67
  9. package/bin/lib/workspace-routes.mjs +189 -2
  10. package/bin/lib/workspace-server.mjs +367 -30
  11. package/bin/lib/workspace-state.mjs +2 -1
  12. package/builtin/nodes/agent_subAgent.md +5 -1
  13. package/builtin/nodes/context_bundle.md +44 -0
  14. package/builtin/nodes/context_knowledge.md +29 -0
  15. package/builtin/nodes/context_skills.md +29 -0
  16. package/builtin/nodes/context_workspace.md +36 -0
  17. package/builtin/nodes/control_while.md +7 -0
  18. package/builtin/nodes/tool_git_worktree_load.md +4 -3
  19. package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-CTrXOZ00.js → WorkflowAssistantThread-DmhIgoD7.js} +1 -1
  20. package/builtin/web-ui/dist/assets/index-CI9J6Unt.js +873 -0
  21. package/builtin/web-ui/dist/assets/index-DY5vE7v1.css +1 -0
  22. package/builtin/web-ui/dist/index.html +2 -2
  23. package/package.json +1 -1
  24. package/shared/slot-types.js +1 -0
  25. package/skills/agentflow-cli/SKILL.md +50 -3
  26. package/skills/agentflow-cli/runtime/bin/lib/skill-runtime.mjs +120 -6
  27. package/skills/agentflow-cli/runtime/builtin/nodes/agent_subAgent.md +5 -1
  28. package/skills/agentflow-cli/runtime/builtin/nodes/context_bundle.md +44 -0
  29. package/skills/agentflow-cli/runtime/builtin/nodes/context_knowledge.md +29 -0
  30. package/skills/agentflow-cli/runtime/builtin/nodes/context_skills.md +29 -0
  31. package/skills/agentflow-cli/runtime/builtin/nodes/context_workspace.md +36 -0
  32. package/skills/agentflow-cli/runtime/builtin/nodes/control_while.md +7 -0
  33. package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_worktree_load.md +4 -3
  34. package/skills/agentflow-cli/runtime/package.json +1 -1
  35. package/skills/agentflow-cli/scripts/agentflow-cli.mjs +64 -0
  36. package/skills/agentflow-flow-dsl/SKILL.md +48 -2
  37. package/skills/agentflow-flow-dsl/references/node-calls.md +6 -2
  38. package/skills/agentflow-flow-dsl/references/subflow-authoring.md +34 -7
  39. package/skills/agentflow-node-reference/references/builtin-nodes.md +37 -5
  40. package/builtin/web-ui/dist/assets/index-5uJFccdX.css +0 -1
  41. package/builtin/web-ui/dist/assets/index-BeUfNQRL.js +0 -873
@@ -130,7 +130,6 @@ import { json, readBody } from "./http-util.mjs";
130
130
 
131
131
  // 从 ui-server 拆出去的 Workspace 子系统;路由仍在下面的 startUiServer 里
132
132
  import {
133
- USER_WORKSPACES_FILENAME,
134
133
  WORKSPACE_DEFERRED_RUN_POLL_MS,
135
134
  WORKSPACE_SCHEDULE_POLL_MS,
136
135
  activeWorkspaceRunUsageRecords,
@@ -141,6 +140,7 @@ import {
141
140
  displayShareOutputUrl,
142
141
  isReadonlyBuiltinFlowSource,
143
142
  isValidFlowSourceRead,
143
+ listConfiguredWorkspaces,
144
144
  listWorkspaceScheduleStatuses,
145
145
  normalizeDisplayShareExpiry,
146
146
  normalizeDisplayShareNodeIds,
@@ -151,11 +151,11 @@ import {
151
151
  readCursorMcpConfig,
152
152
  readCursorMcpServers,
153
153
  readDisplayShares,
154
+ readUserWorkspaces,
154
155
  readUserMcpPrivate,
155
156
  readWorkspaceGraph,
156
157
  readWorkspaceRunUsageRecords,
157
158
  readWorkspaceScheduleRegistry,
158
- readWorkspacesFromPath,
159
159
  resolveWorkspaceFilePath,
160
160
  resolveWorkspaceScopeRoot,
161
161
  runStatusBucket,
@@ -173,11 +173,22 @@ import {
173
173
  workspaceDownloadContentDisposition,
174
174
  workspaceFlowCollaborationGuard,
175
175
  workspaceScheduleNextRunAt,
176
- workspacesPath,
177
176
  writeDisplayShares,
178
177
  writeWorkspaceGraph,
179
178
  } from "./workspace-server.mjs";
180
179
  import { handleWorkspaceRoutes, workspaceGraphWithScheduleMode } from "./workspace-routes.mjs";
180
+ import {
181
+ createSpace,
182
+ deleteSpacePage,
183
+ getSpaceById,
184
+ getSpaceByRoute,
185
+ listSpacesForUser,
186
+ readSpaces,
187
+ normalizeSpacePagePath,
188
+ normalizeSpaceVisibility,
189
+ updateSpace,
190
+ upsertSpacePage,
191
+ } from "./spaces.mjs";
181
192
 
182
193
  const MIME = {
183
194
  ".html": "text/html; charset=utf-8",
@@ -935,70 +946,6 @@ function skillhubInstallArgs(payload, { uninstall = false } = {}) {
935
946
  }
936
947
 
937
948
  const DISPLAY_SHARE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
938
- function legacyUserWorkspacesPath(userCtx = {}) {
939
- return path.join(getAgentflowUserDataRoot(userCtx.userId || ""), USER_WORKSPACES_FILENAME);
940
- }
941
-
942
- function readLegacyAdminWorkspaces(userCtx = {}) {
943
- const users = readAuthUsers();
944
- const candidates = [];
945
- for (const [userId, user] of Object.entries(users || {})) {
946
- if (user?.isAdmin) candidates.push(String(userId || ""));
947
- }
948
- if (userCtx?.isAdmin && userCtx.userId) candidates.unshift(String(userCtx.userId));
949
- const seenPaths = new Set();
950
- const seenEntries = new Set();
951
- const out = [];
952
- for (const userId of candidates) {
953
- const p = legacyUserWorkspacesPath({ userId });
954
- const resolved = path.resolve(p);
955
- if (seenPaths.has(resolved) || resolved === path.resolve(workspacesPath())) continue;
956
- seenPaths.add(resolved);
957
- for (const entry of readWorkspacesFromPath(p, { userId })) {
958
- const key = entry.id || entry.path || entry.repoUrl;
959
- if (seenEntries.has(key)) continue;
960
- seenEntries.add(key);
961
- out.push(entry);
962
- }
963
- }
964
- return out;
965
- }
966
-
967
- function readUserWorkspaces(userCtx = {}) {
968
- const globalPath = workspacesPath();
969
- const globalWorkspaces = fs.existsSync(globalPath) ? readWorkspacesFromPath(globalPath, userCtx) : [];
970
- const adminLegacy = readLegacyAdminWorkspaces(userCtx);
971
- if (globalWorkspaces.length || adminLegacy.length) {
972
- const seen = new Set();
973
- const out = [];
974
- for (const entry of [...globalWorkspaces, ...adminLegacy]) {
975
- const key = entry.id || entry.path || entry.repoUrl;
976
- if (seen.has(key)) continue;
977
- seen.add(key);
978
- out.push(entry);
979
- }
980
- return out;
981
- }
982
- return readWorkspacesFromPath(legacyUserWorkspacesPath(userCtx), userCtx);
983
- }
984
-
985
- function listConfiguredWorkspaces(root, scopedRoot, userCtx = {}) {
986
- const currentRoot = path.resolve(scopedRoot || root);
987
- const homeRoot = path.resolve(os.homedir());
988
- const builtins = [
989
- { id: "current", label: "当前流程工作区", kind: "local", path: currentRoot, builtin: true, exists: fs.existsSync(currentRoot) && fs.statSync(currentRoot).isDirectory(), type: "flow", enabled: true },
990
- { id: "home", label: "用户 Home", kind: "local", path: homeRoot, builtin: true, exists: fs.existsSync(homeRoot) && fs.statSync(homeRoot).isDirectory(), type: "local", enabled: true },
991
- ];
992
- const custom = readUserWorkspaces(userCtx).filter((entry) => entry.enabled !== false).map((entry) => ({ ...entry, builtin: false }));
993
- const seen = new Set();
994
- return [...builtins, ...custom].filter((entry) => {
995
- const key = path.resolve(entry.path);
996
- if (seen.has(key)) return false;
997
- seen.add(key);
998
- return true;
999
- });
1000
- }
1001
-
1002
949
  function workflowBindableWorkspaces(userCtx = {}) {
1003
950
  return readUserWorkspaces(userCtx)
1004
951
  .filter((entry) => entry.enabled !== false && entry.exists)
@@ -1351,6 +1298,12 @@ function isDisplayShareExpired(share) {
1351
1298
  return Number.isFinite(expiresAt) && expiresAt <= Date.now();
1352
1299
  }
1353
1300
 
1301
+ function canReadDisplayShare(share, userCtx = {}) {
1302
+ if (!share) return false;
1303
+ if (String(share.visibility || "public") !== "private") return true;
1304
+ return userCtx?.isAdmin === true || String(share.userId || "") === String(userCtx?.userId || "");
1305
+ }
1306
+
1354
1307
  function getDisplayShareOrExpired(id) {
1355
1308
  const shares = readDisplayShares();
1356
1309
  const share = shares[id];
@@ -1376,10 +1329,78 @@ function displayShareSummary(share, baseUrl = "") {
1376
1329
  expiresAt: String(share?.expiresAt || ""),
1377
1330
  expiresMode: String(share?.expiresMode || (share?.expiresAt ? "days" : "permanent")),
1378
1331
  expiresInDays: share?.expiresInDays == null ? null : Number(share.expiresInDays),
1332
+ visibility: String(share?.visibility || "public") === "private" ? "private" : "public",
1379
1333
  url: displayShareOutputUrl(share?.id || "", baseUrl),
1380
1334
  };
1381
1335
  }
1382
1336
 
1337
+ function spacePublicSummary(space = {}, baseUrl = "") {
1338
+ const ownerId = String(space.ownerId || "");
1339
+ const slug = String(space.slug || "");
1340
+ const rootPath = `/s/${encodeURIComponent(ownerId)}/${encodeURIComponent(slug)}`;
1341
+ const base = normalizePublicBaseUrl(baseUrl);
1342
+ const absoluteRoot = base ? new URL(rootPath, base.endsWith("/") ? base : `${base}/`).href : rootPath;
1343
+ return {
1344
+ id: String(space.id || ""),
1345
+ ownerId,
1346
+ slug,
1347
+ title: String(space.title || slug),
1348
+ description: String(space.description || ""),
1349
+ visibility: normalizeSpaceVisibility(space.visibility),
1350
+ status: String(space.status || "active"),
1351
+ pages: (Array.isArray(space.pages) ? space.pages : []).map((page) => ({
1352
+ id: String(page.id || ""),
1353
+ title: String(page.title || ""),
1354
+ path: normalizeSpacePagePath(page.path || "/"),
1355
+ shareId: String(page.shareId || ""),
1356
+ hidden: page.hidden === true,
1357
+ order: Number(page.order || 0),
1358
+ })),
1359
+ url: absoluteRoot,
1360
+ createdAt: String(space.createdAt || ""),
1361
+ updatedAt: String(space.updatedAt || ""),
1362
+ };
1363
+ }
1364
+
1365
+ function canManageSpace(space, userCtx = {}) {
1366
+ return Boolean(space) && (userCtx?.isAdmin === true || String(space.ownerId || "") === String(userCtx?.userId || ""));
1367
+ }
1368
+
1369
+ function displayShareSpaceReferences(shareId = "") {
1370
+ const wanted = String(shareId || "").trim();
1371
+ if (!wanted) return [];
1372
+ const refs = [];
1373
+ for (const space of readSpaces()) {
1374
+ for (const page of Array.isArray(space.pages) ? space.pages : []) {
1375
+ if (String(page.shareId || "") !== wanted) continue;
1376
+ refs.push({ spaceId: space.id, ownerId: space.ownerId, slug: space.slug, pageId: page.id, path: page.path });
1377
+ }
1378
+ }
1379
+ return refs;
1380
+ }
1381
+
1382
+ function syncSpaceShareVisibility(space) {
1383
+ if (!space) return;
1384
+ const shares = readDisplayShares();
1385
+ let changed = false;
1386
+ for (const page of Array.isArray(space.pages) ? space.pages : []) {
1387
+ const share = shares[String(page.shareId || "")];
1388
+ if (!share || String(share.userId || "") !== String(space.ownerId || "")) continue;
1389
+ const visibility = normalizeSpaceVisibility(space.visibility);
1390
+ if (share.visibility === visibility && !share.expiresAt) continue;
1391
+ shares[page.shareId] = {
1392
+ ...share,
1393
+ visibility,
1394
+ expiresAt: "",
1395
+ expiresMode: "permanent",
1396
+ expiresInDays: null,
1397
+ updatedAt: new Date().toISOString(),
1398
+ };
1399
+ changed = true;
1400
+ }
1401
+ if (changed) writeDisplayShares(shares);
1402
+ }
1403
+
1383
1404
  function listDisplaySharesForUser(userCtx = {}, baseUrl = "") {
1384
1405
  const userId = String(userCtx?.userId || "");
1385
1406
  const isAdmin = userCtx?.isAdmin === true;
@@ -1407,6 +1428,9 @@ function updateDisplayShareExpiryForUser(id, userCtx = {}, patch = {}) {
1407
1428
  const userId = String(userCtx?.userId || "");
1408
1429
  if (userCtx?.isAdmin !== true && String(share.userId || "") !== userId) return { status: 403, error: "Forbidden" };
1409
1430
  const expiry = normalizeDisplayShareExpiry(patch, new Date());
1431
+ if (displayShareSpaceReferences(id).length > 0 && expiry.expiresMode !== "permanent") {
1432
+ return { status: 409, error: "Space 页面使用中的展示内容必须永久有效;请先从空间移除页面" };
1433
+ }
1410
1434
  const updated = {
1411
1435
  ...share,
1412
1436
  expiresAt: expiry.expiresAt,
@@ -1425,6 +1449,9 @@ function deleteDisplayShareForUser(id, userCtx = {}) {
1425
1449
  if (!share) return { status: 404, error: "Display share not found" };
1426
1450
  const userId = String(userCtx?.userId || "");
1427
1451
  if (userCtx?.isAdmin !== true && String(share.userId || "") !== userId) return { status: 403, error: "Forbidden" };
1452
+ if (displayShareSpaceReferences(id).length > 0) {
1453
+ return { status: 409, error: "展示内容仍被 Space 页面引用;请先从空间移除页面" };
1454
+ }
1428
1455
  delete shares[id];
1429
1456
  writeDisplayShares(shares);
1430
1457
  return { status: 200 };
@@ -2339,6 +2366,154 @@ export function startUiServer({
2339
2366
  json(res, 200, { token: getSessionTokenFromRequest(req) || "" });
2340
2367
  return;
2341
2368
  }
2369
+
2370
+ if (req.method === "GET" && url.pathname === "/api/spaces/public") {
2371
+ try {
2372
+ const ownerId = String(url.searchParams.get("owner") || "").trim();
2373
+ const slug = String(url.searchParams.get("slug") || "").trim();
2374
+ const space = getSpaceByRoute(ownerId, slug);
2375
+ if (!space || (!canManageSpace(space, userCtx) && normalizeSpaceVisibility(space.visibility) === "private")) {
2376
+ json(res, 404, { error: "Space not found" });
2377
+ return;
2378
+ }
2379
+ if (space.status === "paused" && !canManageSpace(space, userCtx)) {
2380
+ json(res, 410, { error: "Space is paused" });
2381
+ return;
2382
+ }
2383
+ json(res, 200, {
2384
+ space: spacePublicSummary(space, requestPublicBaseUrl(req)),
2385
+ manageable: canManageSpace(space, userCtx),
2386
+ });
2387
+ } catch (e) {
2388
+ json(res, 500, { error: (e && e.message) || String(e) });
2389
+ }
2390
+ return;
2391
+ }
2392
+
2393
+ if (req.method === "GET" && url.pathname === "/api/spaces") {
2394
+ if (!authUser?.userId) {
2395
+ json(res, 401, { error: "Unauthorized" });
2396
+ return;
2397
+ }
2398
+ const spaces = listSpacesForUser(userCtx.userId, userCtx.isAdmin)
2399
+ .map((space) => spacePublicSummary(space, requestPublicBaseUrl(req)));
2400
+ json(res, 200, { spaces });
2401
+ return;
2402
+ }
2403
+
2404
+ if (req.method === "POST" && url.pathname === "/api/spaces") {
2405
+ if (!authUser?.userId) {
2406
+ json(res, 401, { error: "Unauthorized" });
2407
+ return;
2408
+ }
2409
+ try {
2410
+ const payload = JSON.parse(await readBody(req));
2411
+ const result = createSpace({
2412
+ ownerId: userCtx.userId,
2413
+ slug: payload.slug,
2414
+ title: payload.title,
2415
+ description: payload.description,
2416
+ visibility: payload.visibility,
2417
+ });
2418
+ if (result.error) {
2419
+ json(res, 400, { error: result.error });
2420
+ return;
2421
+ }
2422
+ json(res, 200, { ok: true, space: spacePublicSummary(result.space, requestPublicBaseUrl(req)) });
2423
+ } catch (e) {
2424
+ json(res, 400, { error: (e && e.message) || String(e) });
2425
+ }
2426
+ return;
2427
+ }
2428
+
2429
+ if (req.method === "PATCH" && url.pathname === "/api/spaces") {
2430
+ if (!authUser?.userId) {
2431
+ json(res, 401, { error: "Unauthorized" });
2432
+ return;
2433
+ }
2434
+ try {
2435
+ const payload = JSON.parse(await readBody(req));
2436
+ const current = getSpaceById(payload.id);
2437
+ if (!canManageSpace(current, userCtx)) {
2438
+ json(res, current ? 403 : 404, { error: current ? "Forbidden" : "Space not found" });
2439
+ return;
2440
+ }
2441
+ const result = updateSpace(payload.id, payload);
2442
+ if (result.error) {
2443
+ json(res, 400, { error: result.error });
2444
+ return;
2445
+ }
2446
+ syncSpaceShareVisibility(result.space);
2447
+ json(res, 200, { ok: true, space: spacePublicSummary(result.space, requestPublicBaseUrl(req)) });
2448
+ } catch (e) {
2449
+ json(res, 400, { error: (e && e.message) || String(e) });
2450
+ }
2451
+ return;
2452
+ }
2453
+
2454
+ if (req.method === "POST" && url.pathname === "/api/spaces/page") {
2455
+ if (!authUser?.userId) {
2456
+ json(res, 401, { error: "Unauthorized" });
2457
+ return;
2458
+ }
2459
+ try {
2460
+ const payload = JSON.parse(await readBody(req));
2461
+ const space = getSpaceById(payload.spaceId);
2462
+ if (!canManageSpace(space, userCtx)) {
2463
+ json(res, space ? 403 : 404, { error: space ? "Forbidden" : "Space not found" });
2464
+ return;
2465
+ }
2466
+ const { share } = getDisplayShareOrExpired(String(payload.shareId || "").trim());
2467
+ if (!share || (userCtx.isAdmin !== true && String(share.userId || "") !== String(userCtx.userId || ""))) {
2468
+ json(res, 400, { error: "Display share not found or not owned by current user" });
2469
+ return;
2470
+ }
2471
+ const foreignSpaceRef = displayShareSpaceReferences(share.id).find((ref) => ref.spaceId !== space.id);
2472
+ if (foreignSpaceRef) {
2473
+ json(res, 409, { error: "同一展示内容不能绑定到多个空间;请从 Workspace 重新生成一次展示" });
2474
+ return;
2475
+ }
2476
+ const result = upsertSpacePage(space.id, {
2477
+ title: payload.title,
2478
+ path: payload.path,
2479
+ shareId: share.id,
2480
+ hidden: payload.hidden === true,
2481
+ });
2482
+ if (result.error) {
2483
+ json(res, 400, { error: result.error });
2484
+ return;
2485
+ }
2486
+ syncSpaceShareVisibility(result.space);
2487
+ json(res, 200, {
2488
+ ok: true,
2489
+ page: result.page,
2490
+ space: spacePublicSummary(result.space, requestPublicBaseUrl(req)),
2491
+ });
2492
+ } catch (e) {
2493
+ json(res, 400, { error: (e && e.message) || String(e) });
2494
+ }
2495
+ return;
2496
+ }
2497
+
2498
+ if (req.method === "DELETE" && url.pathname === "/api/spaces/page") {
2499
+ if (!authUser?.userId) {
2500
+ json(res, 401, { error: "Unauthorized" });
2501
+ return;
2502
+ }
2503
+ const space = getSpaceById(url.searchParams.get("spaceId") || "");
2504
+ if (!canManageSpace(space, userCtx)) {
2505
+ json(res, space ? 403 : 404, { error: space ? "Forbidden" : "Space not found" });
2506
+ return;
2507
+ }
2508
+ const result = deleteSpacePage(space.id, url.searchParams.get("pageId") || "");
2509
+ if (result.error) {
2510
+ json(res, 404, { error: result.error });
2511
+ return;
2512
+ }
2513
+ json(res, 200, { ok: true, space: spacePublicSummary(result.space, requestPublicBaseUrl(req)) });
2514
+ return;
2515
+ }
2516
+
2342
2517
  if (req.method === "GET" && url.pathname === "/api/display/shares") {
2343
2518
  try {
2344
2519
  if (!authUser?.userId) {
@@ -2366,6 +2541,10 @@ export function startUiServer({
2366
2541
  json(res, expired ? 410 : 404, { error: expired ? "Display share has expired" : "Display share not found" });
2367
2542
  return;
2368
2543
  }
2544
+ if (!canReadDisplayShare(share, userCtx)) {
2545
+ json(res, 404, { error: "Display share not found" });
2546
+ return;
2547
+ }
2369
2548
  const payload = publicDisplayPayloadFromShare(root, share);
2370
2549
  if (payload.error) {
2371
2550
  json(res, 404, { error: payload.error });
@@ -2446,6 +2625,10 @@ export function startUiServer({
2446
2625
  json(res, expired ? 410 : 404, { error: expired ? "Display share has expired" : "Display share not found" });
2447
2626
  return;
2448
2627
  }
2628
+ if (!canReadDisplayShare(share, userCtx)) {
2629
+ json(res, 404, { error: "Display share not found" });
2630
+ return;
2631
+ }
2449
2632
  const scoped = resolveWorkspaceScopeRoot(root, {
2450
2633
  flowId: share.flowId || "",
2451
2634
  flowSource: share.flowSource || "user",
@@ -3096,6 +3279,7 @@ export function startUiServer({
3096
3279
  expiresInDays: payload.expiresInDays,
3097
3280
  permanent: payload.permanent,
3098
3281
  expiresAt: payload.expiresAt,
3282
+ visibility: payload.visibility,
3099
3283
  });
3100
3284
  json(res, 200, { ok: true, share, url: `/display/${encodeURIComponent(share.id)}` });
3101
3285
  } catch (e) {
@@ -17,12 +17,21 @@ import { buildSkillCompactInjectionBlock, loadResourcesForSkillKeys } from "./co
17
17
  import { execFileBuffered } from "./exec-buffered.mjs";
18
18
  import { runGit } from "./git-worktree.mjs";
19
19
  import {
20
+ listMarketplaceFlows,
20
21
  listMarketplacePackages,
21
22
  nodePackageArchive,
23
+ publishMarketplaceFlow,
22
24
  publishNodePackage,
23
25
  publishNodePackageArchive,
26
+ readMarketplaceFlow,
24
27
  resolveMarketplaceNodePackage,
28
+ setMarketplaceVisibility,
25
29
  } from "./marketplace.mjs";
30
+ import {
31
+ appendMarketplaceUsageEvent,
32
+ marketplaceResourcesForRun,
33
+ writeMarketplaceFlowOrigin,
34
+ } from "./marketplace-usage.mjs";
26
35
  import { NODE_PACKAGE_ENTRY, nodePackageExportsRun, readNodePackageManifest } from "./node-package-manifest.mjs";
27
36
  import { inspectNodePackageDirectory } from "./node-package-archive.mjs";
28
37
  import { json, readBody } from "./http-util.mjs";
@@ -38,8 +47,8 @@ import { mergeWorkspaceGraphs, workspaceDesignRevision, workspaceRuntimeRevision
38
47
  import { DEFAULT_WORKSPACE_PREVIEW_TTL_MS, createWorkspacePreviewId, normalizeWorkspacePreviewTtlMs, readWorkspacePreviewMetadata, workspaceSharedPreviewFlowDir, writeWorkspacePreviewMetadata } from "./workspace-preview.mjs";
39
48
  import { DEFAULT_WORKSPACE_DRAFT_TTL_MS, createWorkspaceDraftId, normalizeWorkspaceDraftTtlMs, readWorkspaceDraftMetadata, workspaceDraftFlowDir, writeWorkspaceDraftMetadata } from "./workspace-draft.mjs";
40
49
  import { appendWorkspaceRunLogEvent, createWorkspaceRunLogSession, finishWorkspaceRunLogSession, listWorkspaceRunLogs, readWorkspaceRunLogEvents } from "./workspace-run-logs.mjs";
41
- import { activeWorkspaceRuns, appendWorkspaceRunFinished, appendWorkspaceRunStarted, hydrateWorkspaceGraphForRuntime, isReadonlyBuiltinFlowSource, isTransientAgentNetworkError, isValidFlowSourceRead, isWorkspaceRunAbortError, listWorkspaceScheduleStatusesForFlow, mergeWorkspacePersistentNodeRefs, mergeWorkspaceRunGraph, normalizeWorkspaceEntry, normalizeWorkspaceScheduledRunConfig, readWorkspaceConversations, readWorkspaceFiles, readWorkspaceGraph, removeWorkspaceDeferredRun, resolveWorkspaceFilePath, resolveWorkspaceScopeRoot, runWorkspaceGraph, sleepMs, syncWorkspaceSchedulesForGraph, upsertWorkspaceDeferredRun, workspaceActiveRunsForScope, workspaceCollaborationEventKey, workspaceCollaborationSequences, workspaceCollaborationSubscribers, workspaceCollaborationSummaryWithUsers, workspaceDeferredRunsForScope, workspaceDesignPath, workspaceDownloadContentDisposition, workspaceFindActiveRunConflict, workspaceGraphAsSource, workspaceOptimizeRunImplementations, workspaceRepoUrlWithCredential, workspaceRunControl, workspaceRunEntryKey, workspaceRunKey, workspaceRunPlan, workspaceRunPlanNodeIds, workspaceRunTouchedNodeIds, workspaceRuntimeNodeLabel, workspaceScheduleNextRunAt, workspaceScopedUserContext, workspaceSearchGuardrailsBlock, workspaceUnwrapOutputEnvelopeForDisplay, workspacesPath, writeWorkspaceConversations, writeWorkspaceGraph } from "./workspace-server.mjs";
42
- import { WORKSPACE_STATE_FILENAME } from "./workspace-state.mjs";
50
+ import { activeWorkspaceRuns, appendWorkspaceRunFinished, appendWorkspaceRunStarted, cleanupWorkspaceRunResources, hydrateWorkspaceGraphForRuntime, isReadonlyBuiltinFlowSource, isTransientAgentNetworkError, isValidFlowSourceRead, isWorkspaceRunAbortError, listWorkspaceScheduleStatusesForFlow, mergeWorkspacePersistentNodeRefs, mergeWorkspaceRunGraph, normalizeWorkspaceEntry, normalizeWorkspaceScheduledRunConfig, readWorkspaceConversations, readWorkspaceFiles, readWorkspaceGraph, removeWorkspaceDeferredRun, resolveWorkspaceFilePath, resolveWorkspaceScopeRoot, runWorkspaceGraph, sleepMs, syncWorkspaceSchedulesForGraph, upsertWorkspaceDeferredRun, workspaceActiveRunsForScope, workspaceCollaborationEventKey, workspaceCollaborationSequences, workspaceCollaborationSubscribers, workspaceCollaborationSummaryWithUsers, workspaceDeferredRunsForScope, workspaceDesignPath, workspaceDownloadContentDisposition, workspaceFindActiveRunConflict, workspaceGraphAsSource, workspaceOptimizeRunImplementations, workspaceRepoUrlWithCredential, workspaceRunControl, workspaceRunEntryKey, workspaceRunKey, workspaceRunPlan, workspaceRunPlanNodeIds, workspaceRunTouchedNodeIds, workspaceRuntimeNodeLabel, workspaceScheduleNextRunAt, workspaceScopedUserContext, workspaceSearchGuardrailsBlock, workspaceUnwrapOutputEnvelopeForDisplay, workspacesPath, writeWorkspaceConversations, writeWorkspaceGraph } from "./workspace-server.mjs";
51
+ import { splitWorkspaceGraph, WORKSPACE_STATE_FILENAME } from "./workspace-state.mjs";
43
52
  import { getWorkspaceTree } from "./workspace-tree.mjs";
44
53
  import busboy from "busboy";
45
54
  import crypto from "crypto";
@@ -921,6 +930,11 @@ async function workspaceRoutes(req, res, ctx) {
921
930
  inputs: node.inputs,
922
931
  outputs: node.outputs,
923
932
  ownerUserId: node.ownerUserId || node.createdBy || "",
933
+ visibility: node.visibility || "public",
934
+ useCount: Number(node.useCount || 0),
935
+ installCount: Number(node.installCount || 0),
936
+ uniqueUserCount: Number(node.uniqueUserCount || 0),
937
+ lastUsedAt: node.lastUsedAt || "",
924
938
  fileList: node.fileList || [],
925
939
  fileCount: node.fileCount || 0,
926
940
  totalBytes: node.totalBytes || 0,
@@ -980,6 +994,14 @@ async function workspaceRoutes(req, res, ctx) {
980
994
  json(res, 404, { error: result.error || "Node package not found" });
981
995
  return;
982
996
  }
997
+ appendMarketplaceUsageEvent(root, {
998
+ kind: "node",
999
+ id,
1000
+ version,
1001
+ action: "install",
1002
+ actorUserId: userCtx.userId || "",
1003
+ eventId: `install:node:${id}@${version}:${userCtx.userId || "anonymous"}`,
1004
+ });
983
1005
  res.writeHead(200, {
984
1006
  "Content-Type": "application/zip",
985
1007
  "Content-Length": result.archive.length,
@@ -995,6 +1017,158 @@ async function workspaceRoutes(req, res, ctx) {
995
1017
  return;
996
1018
  }
997
1019
 
1020
+ if (req.method === "GET" && url.pathname === "/api/marketplace/resources") {
1021
+ try {
1022
+ const kind = String(url.searchParams.get("kind") || "flow").trim();
1023
+ const marketplaceScope = url.searchParams.get("scope") === "owned" ? "owned" : "all";
1024
+ const queryText = String(url.searchParams.get("q") || "").trim().toLowerCase();
1025
+ const matches = (item) => !queryText || [
1026
+ item.id,
1027
+ item.displayName,
1028
+ item.description,
1029
+ item.ownerUserId,
1030
+ ...(Array.isArray(item.tags) ? item.tags : []),
1031
+ ].filter(Boolean).join("\n").toLowerCase().includes(queryText);
1032
+ if (kind === "node") {
1033
+ const nodes = listMarketplacePackages(root, { ...userCtx, marketplaceScope }).nodes.filter(matches);
1034
+ json(res, 200, { kind, sort: "useCount", order: "desc", items: nodes });
1035
+ return;
1036
+ }
1037
+ if (kind !== "flow") {
1038
+ json(res, 400, { error: "kind must be flow or node" });
1039
+ return;
1040
+ }
1041
+ const flows = listMarketplaceFlows(root, { ...userCtx, marketplaceScope }).flows.filter(matches);
1042
+ json(res, 200, { kind, sort: "useCount", order: "desc", items: flows });
1043
+ } catch (e) {
1044
+ json(res, 500, { error: (e && e.message) || String(e) });
1045
+ }
1046
+ return;
1047
+ }
1048
+
1049
+ if (req.method === "POST" && url.pathname === "/api/marketplace/flows/publish") {
1050
+ let payload;
1051
+ try {
1052
+ payload = JSON.parse(await readBody(req));
1053
+ } catch {
1054
+ json(res, 400, { error: "Invalid JSON body" });
1055
+ return;
1056
+ }
1057
+ try {
1058
+ const flowId = String(payload?.flowId || "").trim();
1059
+ const flowSource = String(payload?.flowSource || "user").trim();
1060
+ const scoped = resolveWorkspaceScopeRoot(root, {
1061
+ flowId,
1062
+ flowSource,
1063
+ archived: payload?.archived === true,
1064
+ }, userCtx);
1065
+ if (scoped.error) {
1066
+ json(res, scoped.status || 400, { error: scoped.error });
1067
+ return;
1068
+ }
1069
+ if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
1070
+ json(res, 400, { error: "Only active editable flows can be published" });
1071
+ return;
1072
+ }
1073
+ const current = readWorkspaceGraph(scoped.root, root).graph;
1074
+ const { design } = splitWorkspaceGraph(current);
1075
+ const graph = workspaceGraphWithScheduleMode(design, "disabled");
1076
+ const result = publishMarketplaceFlow(root, {
1077
+ id: payload?.id || flowId,
1078
+ version: payload?.version || "1.0.0",
1079
+ displayName: payload?.displayName || flowId,
1080
+ description: payload?.description || "",
1081
+ tags: payload?.tags || [],
1082
+ visibility: payload?.visibility || "public",
1083
+ graph,
1084
+ }, userCtx);
1085
+ json(res, result.ok ? (result.alreadyExists ? 200 : 201) : (result.conflict ? 409 : 400), result);
1086
+ } catch (e) {
1087
+ json(res, 500, { error: (e && e.message) || String(e) });
1088
+ }
1089
+ return;
1090
+ }
1091
+
1092
+ if (req.method === "POST" && url.pathname === "/api/marketplace/flows/install") {
1093
+ let payload;
1094
+ try {
1095
+ payload = JSON.parse(await readBody(req));
1096
+ } catch {
1097
+ json(res, 400, { error: "Invalid JSON body" });
1098
+ return;
1099
+ }
1100
+ const id = String(payload?.id || "").trim();
1101
+ const version = String(payload?.version || "").trim();
1102
+ const idCheck = validateUserPipelineId(String(payload?.flowId || id));
1103
+ if (!id || !version || !idCheck.ok) {
1104
+ json(res, 400, { error: idCheck.error || "Missing marketplace flow id or version" });
1105
+ return;
1106
+ }
1107
+ const flowId = idCheck.flowId;
1108
+ const targetDir = path.join(getUserPipelinesRoot(userCtx.userId), flowId);
1109
+ if (fs.existsSync(targetDir)) {
1110
+ json(res, 409, { error: `已存在同名流水线 ${flowId}` });
1111
+ return;
1112
+ }
1113
+ try {
1114
+ const source = readMarketplaceFlow(root, id, version, { ...userCtx, marketplaceScope: "all" });
1115
+ if (!source.ok) {
1116
+ json(res, 404, { error: source.error || "Marketplace flow not found" });
1117
+ return;
1118
+ }
1119
+ fs.mkdirSync(targetDir, { recursive: true });
1120
+ const graph = workspaceGraphWithScheduleMode(source.graph, "disabled");
1121
+ writeWorkspaceGraph(targetDir, graph, root);
1122
+ writeMarketplaceFlowOrigin(targetDir, { id, version });
1123
+ appendMarketplaceUsageEvent(root, {
1124
+ kind: "flow",
1125
+ id,
1126
+ version,
1127
+ action: "install",
1128
+ actorUserId: userCtx.userId || "",
1129
+ eventId: `install:flow:${id}@${version}:${userCtx.userId || "anonymous"}`,
1130
+ });
1131
+ json(res, 201, {
1132
+ ok: true,
1133
+ id,
1134
+ version,
1135
+ flowId,
1136
+ flowSource: "user",
1137
+ url: `/workspace?flowId=${encodeURIComponent(flowId)}&flowSource=user`,
1138
+ });
1139
+ } catch (e) {
1140
+ try {
1141
+ if (fs.existsSync(targetDir)) fs.rmSync(targetDir, { recursive: true, force: true });
1142
+ } catch {}
1143
+ json(res, 500, { error: (e && e.message) || String(e) });
1144
+ }
1145
+ return;
1146
+ }
1147
+
1148
+ if (req.method === "PATCH" && url.pathname === "/api/marketplace/visibility") {
1149
+ let payload;
1150
+ try {
1151
+ payload = JSON.parse(await readBody(req));
1152
+ } catch {
1153
+ json(res, 400, { error: "Invalid JSON body" });
1154
+ return;
1155
+ }
1156
+ try {
1157
+ const result = setMarketplaceVisibility(
1158
+ root,
1159
+ String(payload?.kind || ""),
1160
+ String(payload?.id || ""),
1161
+ String(payload?.version || ""),
1162
+ String(payload?.visibility || "public"),
1163
+ userCtx,
1164
+ );
1165
+ json(res, result.ok ? 200 : 400, result);
1166
+ } catch (e) {
1167
+ json(res, 500, { error: (e && e.message) || String(e) });
1168
+ }
1169
+ return;
1170
+ }
1171
+
998
1172
  if (req.method === "GET" && url.pathname === "/api/workspace-tree") {
999
1173
  try {
1000
1174
  json(res, 200, getWorkspaceTree(root));
@@ -2158,6 +2332,8 @@ async function workspaceRoutes(req, res, ctx) {
2158
2332
  flowSource: scoped.flowSource || payload.flowSource || "user",
2159
2333
  plannedNodeIds,
2160
2334
  startedAt: Date.now(),
2335
+ workspaceRoot: root,
2336
+ marketplaceResources: marketplaceResourcesForRun(scoped.root, runtimeGraph, plannedNodeIds),
2161
2337
  };
2162
2338
  const runLog = createWorkspaceRunLogSession({
2163
2339
  runId,
@@ -2255,6 +2431,7 @@ async function workspaceRoutes(req, res, ctx) {
2255
2431
  ...runEntry,
2256
2432
  endedAt,
2257
2433
  durationMs: endedAt - runEntry.startedAt,
2434
+ marketplaceResources: marketplaceResourcesForRun(scoped.root, runtimeGraph, result.order || Array.from(touchedIds)),
2258
2435
  }, "success");
2259
2436
  finishWorkspaceRunLogSession(runLog.runId, "success", {
2260
2437
  endedAt,
@@ -2350,6 +2527,7 @@ async function workspaceRoutes(req, res, ctx) {
2350
2527
  ...runEntry,
2351
2528
  endedAt,
2352
2529
  durationMs: endedAt - runEntry.startedAt,
2530
+ marketplaceResources: marketplaceResourcesForRun(scoped.root, runtimeGraph, result.order || Array.from(touchedIds)),
2353
2531
  }, "success");
2354
2532
  finishWorkspaceRunLogSession(runLog.runId, "success", {
2355
2533
  endedAt,
@@ -2574,6 +2752,11 @@ async function workspaceRoutes(req, res, ctx) {
2574
2752
  return;
2575
2753
  }
2576
2754
  removeWorkspaceDeferredRun(deferred.key);
2755
+ const resourceCleanup = cleanupWorkspaceRunResources(scoped.root, deferred.runId, {
2756
+ force: false,
2757
+ status: "stopped",
2758
+ emit: (event) => appendWorkspaceRunLogEvent(deferred.runId, event),
2759
+ });
2577
2760
  const endedAt = Date.now();
2578
2761
  appendWorkspaceRunLogEvent(deferred.runId, {
2579
2762
  type: "stop-completed",
@@ -2602,6 +2785,10 @@ async function workspaceRoutes(req, res, ctx) {
2602
2785
  ok: true,
2603
2786
  stopped: true,
2604
2787
  monitoringOnly: true,
2788
+ resources: {
2789
+ cleaned: resourceCleanup.cleaned,
2790
+ preserved: resourceCleanup.preserved,
2791
+ },
2605
2792
  });
2606
2793
  return;
2607
2794
  }