@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.
Files changed (42) 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 +314 -76
  9. package/bin/lib/workspace-routes.mjs +396 -5
  10. package/bin/lib/workspace-run-logs.mjs +2 -0
  11. package/bin/lib/workspace-server.mjs +698 -48
  12. package/bin/lib/workspace-state.mjs +2 -1
  13. package/builtin/nodes/agent_subAgent.md +5 -1
  14. package/builtin/nodes/context_bundle.md +44 -0
  15. package/builtin/nodes/context_knowledge.md +29 -0
  16. package/builtin/nodes/context_skills.md +29 -0
  17. package/builtin/nodes/context_workspace.md +36 -0
  18. package/builtin/nodes/control_while.md +7 -0
  19. package/builtin/nodes/tool_git_worktree_load.md +4 -3
  20. package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-CTrXOZ00.js → WorkflowAssistantThread-CffIx5BY.js} +1 -1
  21. package/builtin/web-ui/dist/assets/index-CDItaRfX.css +1 -0
  22. package/builtin/web-ui/dist/assets/index-CWIcfWHO.js +873 -0
  23. package/builtin/web-ui/dist/index.html +2 -2
  24. package/package.json +1 -1
  25. package/shared/slot-types.js +1 -0
  26. package/skills/agentflow-cli/SKILL.md +50 -3
  27. package/skills/agentflow-cli/runtime/bin/lib/skill-runtime.mjs +120 -6
  28. package/skills/agentflow-cli/runtime/builtin/nodes/agent_subAgent.md +5 -1
  29. package/skills/agentflow-cli/runtime/builtin/nodes/context_bundle.md +44 -0
  30. package/skills/agentflow-cli/runtime/builtin/nodes/context_knowledge.md +29 -0
  31. package/skills/agentflow-cli/runtime/builtin/nodes/context_skills.md +29 -0
  32. package/skills/agentflow-cli/runtime/builtin/nodes/context_workspace.md +36 -0
  33. package/skills/agentflow-cli/runtime/builtin/nodes/control_while.md +7 -0
  34. package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_worktree_load.md +4 -3
  35. package/skills/agentflow-cli/runtime/package.json +1 -1
  36. package/skills/agentflow-cli/scripts/agentflow-cli.mjs +64 -0
  37. package/skills/agentflow-flow-dsl/SKILL.md +48 -2
  38. package/skills/agentflow-flow-dsl/references/node-calls.md +6 -2
  39. package/skills/agentflow-flow-dsl/references/subflow-authoring.md +34 -7
  40. package/skills/agentflow-node-reference/references/builtin-nodes.md +37 -5
  41. package/builtin/web-ui/dist/assets/index-5uJFccdX.css +0 -1
  42. package/builtin/web-ui/dist/assets/index-BeUfNQRL.js +0 -873
@@ -19,10 +19,17 @@ import {
19
19
  inspectNodePackageDirectory,
20
20
  writeNodePackageFiles,
21
21
  } from "./node-package-archive.mjs";
22
+ import {
23
+ marketplaceStatsFor,
24
+ marketplaceUsageStats,
25
+ normalizeMarketplaceVisibility,
26
+ } from "./marketplace-usage.mjs";
22
27
 
23
28
  const NODE_MANIFEST = "node.yaml";
24
29
  const COLLECTION_MANIFEST = "collection.yaml";
25
30
  const FLOW_SNIPPET_MANIFEST = "flow-snippet.yaml";
31
+ const FLOW_PACKAGE_MANIFEST = "flow-package.yaml";
32
+ const FLOW_PACKAGE_GRAPH = "workspace.graph.json";
26
33
  const LOCK_FILENAME = "agentflow.lock.json";
27
34
 
28
35
  function workspacePackageRoot(workspaceRoot) {
@@ -131,6 +138,7 @@ function normalizeManifest(raw, packageDir, source = "workspace") {
131
138
  baseDefinitionId,
132
139
  displayName: raw.displayName != null ? String(raw.displayName) : raw.name != null ? String(raw.name) : id,
133
140
  description: raw.description != null ? String(raw.description) : "",
141
+ visibility: normalizeMarketplaceVisibility(raw.visibility),
134
142
  input,
135
143
  output,
136
144
  runtime,
@@ -168,7 +176,12 @@ function canManageMarketplaceOwner(ownerUserId, opts = {}) {
168
176
 
169
177
  function canAccessMarketplaceNode(manifest, opts = {}) {
170
178
  if ((manifest?.source || "marketplace") !== "marketplace") return true;
171
- return canAccessMarketplaceOwner(manifestOwnerUserId(manifest), opts);
179
+ const ownerUserId = manifestOwnerUserId(manifest);
180
+ if (!canAccessMarketplaceOwner(ownerUserId, opts)) return false;
181
+ if (normalizeMarketplaceVisibility(manifest?.visibility) === "public") return true;
182
+ const requestedUserId = String(opts.userId || "").trim();
183
+ if (!requestedUserId) return true;
184
+ return isAdminRequest(opts) || Boolean(ownerUserId) && ownerUserId === requestedUserId;
172
185
  }
173
186
 
174
187
  function sortVersionsDesc(versions) {
@@ -204,6 +217,14 @@ function resolveWorkspaceFlowSnippetPackageDir(workspaceRoot, id, version) {
204
217
  return target;
205
218
  }
206
219
 
220
+ function resolveWorkspaceFlowPackageDir(workspaceRoot, id, version) {
221
+ if (!isSafePathSegment(id) || !isSafePathSegment(version)) return null;
222
+ const base = path.resolve(workspacePackageRoot(workspaceRoot), "flows");
223
+ const target = path.resolve(base, id, version);
224
+ if (target !== base && !target.startsWith(base + path.sep)) return null;
225
+ return target;
226
+ }
227
+
207
228
  function collectFlowDirs(rootDir, source, archived = false) {
208
229
  const out = [];
209
230
  if (!fs.existsSync(rootDir)) return out;
@@ -470,6 +491,7 @@ export function listMarketplaceNodes(workspaceRoot, flowData = null, opts = {})
470
491
 
471
492
  export function listMarketplacePackages(workspaceRoot, opts = {}) {
472
493
  const root = workspacePackageRoot(workspaceRoot);
494
+ const stats = marketplaceUsageStats(workspaceRoot);
473
495
  const nodes = listMarketplaceNodes(workspaceRoot, null, opts).map((n) => ({
474
496
  id: n.id,
475
497
  version: n.version,
@@ -490,9 +512,16 @@ export function listMarketplacePackages(workspaceRoot, opts = {}) {
490
512
  installedAt: String(n.installedAt || ""),
491
513
  ownerUserId: n.ownerUserId || n.createdBy || "",
492
514
  createdBy: n.createdBy || n.ownerUserId || "",
515
+ visibility: normalizeMarketplaceVisibility(n.visibility),
493
516
  packageDir: n.packageDir,
494
517
  usage: listMarketplaceNodeUsages(workspaceRoot, n.id, n.version, opts),
495
- }));
518
+ ...marketplaceStatsFor(stats, "node", n.id, n.version, n.ownerUserId || n.createdBy || ""),
519
+ })).sort((a, b) => (
520
+ Number(b.useCount || 0) - Number(a.useCount || 0)
521
+ || Number(b.installCount || 0) - Number(a.installCount || 0)
522
+ || String(a.id).localeCompare(String(b.id))
523
+ || String(b.version).localeCompare(String(a.version), undefined, { numeric: true, sensitivity: "base" })
524
+ ));
496
525
  const collections = [];
497
526
  const collectionsRoot = path.join(root, "collections");
498
527
  if (fs.existsSync(collectionsRoot)) {
@@ -519,6 +548,7 @@ export function listMarketplaceFlowSnippets(workspaceRoot, opts = {}) {
519
548
  const root = workspacePackageRoot(workspaceRoot);
520
549
  const snippetsRoot = path.join(root, "flow-snippets");
521
550
  const snippets = [];
551
+ const stats = marketplaceUsageStats(workspaceRoot);
522
552
  const requestedUserId = String(opts.userId || "").trim();
523
553
  if (!fs.existsSync(snippetsRoot)) return { snippets };
524
554
  for (const entry of fs.readdirSync(snippetsRoot, { withFileTypes: true })) {
@@ -531,6 +561,8 @@ export function listMarketplaceFlowSnippets(workspaceRoot, opts = {}) {
531
561
  const snippet = manifest.snippet && typeof manifest.snippet === "object" ? manifest.snippet : {};
532
562
  const ownerUserId = String(manifest.ownerUserId || manifest.createdBy || "").trim();
533
563
  if (requestedUserId && !canAccessMarketplaceOwner(ownerUserId, opts)) continue;
564
+ const visibility = normalizeMarketplaceVisibility(manifest.visibility);
565
+ if (visibility === "private" && requestedUserId && !isAdminRequest(opts) && ownerUserId !== requestedUserId) continue;
534
566
  snippets.push({
535
567
  id: manifest.id || entry.name,
536
568
  version: manifest.version || version,
@@ -542,18 +574,162 @@ export function listMarketplaceFlowSnippets(workspaceRoot, opts = {}) {
542
574
  createdAt: manifest.createdAt || "",
543
575
  updatedAt: manifest.updatedAt || "",
544
576
  ownerUserId,
577
+ visibility,
545
578
  packageDir: dir,
546
579
  snippet,
580
+ ...marketplaceStatsFor(stats, "flow-snippet", manifest.id || entry.name, manifest.version || version, ownerUserId),
547
581
  });
548
582
  }
549
583
  }
550
584
  snippets.sort((a, b) => {
585
+ const byUsage = Number(b.useCount || 0) - Number(a.useCount || 0);
586
+ const byInstall = Number(b.installCount || 0) - Number(a.installCount || 0);
551
587
  const byTime = String(b.updatedAt || b.createdAt || "").localeCompare(String(a.updatedAt || a.createdAt || ""));
552
- return byTime || a.id.localeCompare(b.id) || a.version.localeCompare(b.version);
588
+ return byUsage || byInstall || byTime || a.id.localeCompare(b.id) || a.version.localeCompare(b.version);
553
589
  });
554
590
  return { snippets };
555
591
  }
556
592
 
593
+ function marketplaceRecordAccessible(manifest, opts = {}) {
594
+ const ownerUserId = String(manifest?.ownerUserId || manifest?.createdBy || "").trim();
595
+ if (!canAccessMarketplaceOwner(ownerUserId, opts)) return false;
596
+ if (normalizeMarketplaceVisibility(manifest?.visibility) === "public") return true;
597
+ const requestedUserId = String(opts.userId || "").trim();
598
+ if (!requestedUserId) return true;
599
+ return isAdminRequest(opts) || ownerUserId === requestedUserId;
600
+ }
601
+
602
+ export function listMarketplaceFlows(workspaceRoot, opts = {}) {
603
+ const base = path.join(workspacePackageRoot(workspaceRoot), "flows");
604
+ const stats = marketplaceUsageStats(workspaceRoot);
605
+ const flows = [];
606
+ if (!fs.existsSync(base)) return { flows };
607
+ for (const entry of fs.readdirSync(base, { withFileTypes: true })) {
608
+ if (!entry.isDirectory()) continue;
609
+ for (const version of listVersionDirs(path.join(base, entry.name))) {
610
+ const packageDir = resolveWorkspaceFlowPackageDir(workspaceRoot, entry.name, version);
611
+ const manifest = packageDir ? readYamlObject(path.join(packageDir, FLOW_PACKAGE_MANIFEST)) : null;
612
+ if (!manifest || !marketplaceRecordAccessible(manifest, opts)) continue;
613
+ const id = String(manifest.id || entry.name);
614
+ const resourceVersion = String(manifest.version || version);
615
+ flows.push({
616
+ id,
617
+ version: resourceVersion,
618
+ displayName: String(manifest.displayName || manifest.name || id),
619
+ description: String(manifest.description || ""),
620
+ tags: Array.isArray(manifest.tags) ? manifest.tags.map((tag) => String(tag)) : [],
621
+ ownerUserId: String(manifest.ownerUserId || manifest.createdBy || ""),
622
+ visibility: normalizeMarketplaceVisibility(manifest.visibility),
623
+ nodeCount: Number(manifest.nodeCount || 0),
624
+ edgeCount: Number(manifest.edgeCount || 0),
625
+ createdAt: String(manifest.createdAt || ""),
626
+ updatedAt: String(manifest.updatedAt || manifest.createdAt || ""),
627
+ packageDir,
628
+ ...marketplaceStatsFor(stats, "flow", id, resourceVersion, String(manifest.ownerUserId || manifest.createdBy || "")),
629
+ });
630
+ }
631
+ }
632
+ flows.sort((a, b) => (
633
+ Number(b.useCount || 0) - Number(a.useCount || 0)
634
+ || Number(b.installCount || 0) - Number(a.installCount || 0)
635
+ || String(b.updatedAt || "").localeCompare(String(a.updatedAt || ""))
636
+ || a.id.localeCompare(b.id)
637
+ || b.version.localeCompare(a.version, undefined, { numeric: true, sensitivity: "base" })
638
+ ));
639
+ return { flows };
640
+ }
641
+
642
+ export function publishMarketplaceFlow(workspaceRoot, payload = {}, opts = {}) {
643
+ const ownerUserId = String(opts.userId || "").trim();
644
+ if (!ownerUserId) return { ok: false, error: "Authentication required" };
645
+ const label = String(payload.displayName || payload.name || payload.id || "flow").trim();
646
+ const id = safePackageId(payload.id || payload.packageId || label);
647
+ const version = normalizeVersion(payload.version || "1.0.0");
648
+ const graph = payload.graph && typeof payload.graph === "object" && !Array.isArray(payload.graph) ? payload.graph : null;
649
+ if (!id || !graph) return { ok: false, error: "Invalid marketplace flow" };
650
+ const packageDir = resolveWorkspaceFlowPackageDir(workspaceRoot, id, version);
651
+ if (!packageDir) return { ok: false, error: "Invalid marketplace flow id or version" };
652
+ const existing = readYamlObject(path.join(packageDir, FLOW_PACKAGE_MANIFEST));
653
+ if (existing && !canManageMarketplaceOwner(String(existing.ownerUserId || existing.createdBy || ""), opts)) {
654
+ return { ok: false, error: "Marketplace flow permission denied" };
655
+ }
656
+ if (existing) {
657
+ const existingGraph = readJsonObject(path.join(packageDir, FLOW_PACKAGE_GRAPH));
658
+ if (JSON.stringify(existingGraph) !== JSON.stringify(graph)) {
659
+ return { ok: false, conflict: true, error: `${id}@${version} 已存在且内容不同,请提升版本号` };
660
+ }
661
+ return {
662
+ ok: true,
663
+ alreadyExists: true,
664
+ ...existing,
665
+ visibility: normalizeMarketplaceVisibility(existing.visibility),
666
+ packageDir,
667
+ };
668
+ }
669
+ const now = new Date().toISOString();
670
+ const manifest = {
671
+ id,
672
+ version,
673
+ displayName: label,
674
+ description: String(payload.description || "").trim(),
675
+ tags: Array.isArray(payload.tags) ? payload.tags.map((tag) => String(tag).trim()).filter(Boolean) : [],
676
+ visibility: normalizeMarketplaceVisibility(payload.visibility),
677
+ ownerUserId,
678
+ createdBy: ownerUserId,
679
+ nodeCount: Object.keys(graph.instances || {}).length,
680
+ edgeCount: Array.isArray(graph.edges) ? graph.edges.length : 0,
681
+ createdAt: String(existing?.createdAt || now),
682
+ updatedAt: now,
683
+ };
684
+ fs.mkdirSync(path.dirname(packageDir), { recursive: true });
685
+ fs.rmSync(packageDir, { recursive: true, force: true });
686
+ fs.mkdirSync(packageDir, { recursive: true });
687
+ fs.writeFileSync(path.join(packageDir, FLOW_PACKAGE_MANIFEST), yaml.dump(manifest, { lineWidth: -1 }), "utf-8");
688
+ fs.writeFileSync(path.join(packageDir, FLOW_PACKAGE_GRAPH), `${JSON.stringify(graph, null, 2)}\n`, "utf-8");
689
+ fs.writeFileSync(path.join(packageDir, "README.md"), `# ${label}\n\n${manifest.description || "Published AgentFlow template."}\n`, "utf-8");
690
+ return { ok: true, ...manifest, packageDir };
691
+ }
692
+
693
+ export function readMarketplaceFlow(workspaceRoot, id, version, opts = {}) {
694
+ const packageDir = resolveWorkspaceFlowPackageDir(workspaceRoot, id, version);
695
+ const manifest = packageDir ? readYamlObject(path.join(packageDir, FLOW_PACKAGE_MANIFEST)) : null;
696
+ if (!manifest || !marketplaceRecordAccessible(manifest, opts)) return { ok: false, error: "Marketplace flow not found" };
697
+ const graph = readJsonObject(path.join(packageDir, FLOW_PACKAGE_GRAPH));
698
+ if (!graph) return { ok: false, error: "Marketplace flow graph is missing" };
699
+ return { ok: true, manifest, graph, packageDir };
700
+ }
701
+
702
+ export function setMarketplaceVisibility(workspaceRoot, kind, id, version, visibility, opts = {}) {
703
+ const normalized = normalizeMarketplaceVisibility(visibility);
704
+ let manifestPath = "";
705
+ if (kind === "flow") {
706
+ const dir = resolveWorkspaceFlowPackageDir(workspaceRoot, id, version);
707
+ manifestPath = dir ? path.join(dir, FLOW_PACKAGE_MANIFEST) : "";
708
+ } else if (kind === "flow-snippet") {
709
+ const dir = resolveWorkspaceFlowSnippetPackageDir(workspaceRoot, id, version);
710
+ manifestPath = dir ? path.join(dir, FLOW_SNIPPET_MANIFEST) : "";
711
+ } else if (kind === "node") {
712
+ const dir = resolveWorkspaceNodePackageDir(workspaceRoot, id, version);
713
+ if (!dir) return { ok: false, error: "Invalid marketplace node" };
714
+ const manifest = normalizeManifest(readNodeManifestRaw(dir), dir, "marketplace");
715
+ if (!manifest || !canManageMarketplaceOwner(manifestOwnerUserId(manifest), opts)) {
716
+ return { ok: false, error: "Marketplace node permission denied" };
717
+ }
718
+ const metadataPath = path.join(dir, NODE_PACKAGE_METADATA_FILENAME);
719
+ const metadata = readJsonObject(metadataPath) || {};
720
+ fs.writeFileSync(metadataPath, `${JSON.stringify({ ...metadata, visibility: normalized }, null, 2)}\n`, "utf-8");
721
+ return { ok: true, kind, id, version, visibility: normalized };
722
+ } else {
723
+ return { ok: false, error: "Unsupported marketplace resource kind" };
724
+ }
725
+ const manifest = manifestPath ? readYamlObject(manifestPath) : null;
726
+ if (!manifest || !canManageMarketplaceOwner(String(manifest.ownerUserId || manifest.createdBy || ""), opts)) {
727
+ return { ok: false, error: "Marketplace resource permission denied" };
728
+ }
729
+ fs.writeFileSync(manifestPath, yaml.dump({ ...manifest, visibility: normalized, updatedAt: new Date().toISOString() }, { lineWidth: -1 }), "utf-8");
730
+ return { ok: true, kind, id, version, visibility: normalized };
731
+ }
732
+
557
733
  export function deleteMarketplaceNodePackage(workspaceRoot, id, version, opts = {}) {
558
734
  const packageDir = resolveWorkspaceNodePackageDir(workspaceRoot, id, version);
559
735
  if (!packageDir) return { ok: false, error: "Invalid marketplace node id or version" };
@@ -638,6 +814,7 @@ export function publishNodePackage(workspaceRoot, sourceDir, opts = {}) {
638
814
  const ownerUserId = String(opts.ownerUserId || opts.userId || manifest.ownerUserId || manifest.createdBy || "").trim();
639
815
  fs.writeFileSync(path.join(dest, NODE_PACKAGE_METADATA_FILENAME), `${JSON.stringify({
640
816
  ...(ownerUserId ? { ownerUserId, createdBy: ownerUserId } : {}),
817
+ visibility: normalizeMarketplaceVisibility(opts.visibility),
641
818
  contentSha256: inspected.contentSha256,
642
819
  fileList: inspected.fileList,
643
820
  fileCount: inspected.fileCount,
@@ -692,6 +869,7 @@ export function publishNodePackageArchive(workspaceRoot, archiveInput, opts = {}
692
869
  if (!written.ok) return written;
693
870
  fs.writeFileSync(path.join(staging, NODE_PACKAGE_METADATA_FILENAME), `${JSON.stringify({
694
871
  ...(ownerUserId ? { ownerUserId, createdBy: ownerUserId } : {}),
872
+ visibility: normalizeMarketplaceVisibility(opts.visibility),
695
873
  contentSha256: inspected.contentSha256,
696
874
  archiveSha256: inspected.archiveSha256,
697
875
  fileList: inspected.fileList,
@@ -994,6 +1172,7 @@ export function publishNodeFromInstance(workspaceRoot, payload = {}, options = {
994
1172
  ...(ui ? { ui } : {}),
995
1173
  ownerUserId,
996
1174
  createdBy: ownerUserId,
1175
+ visibility: normalizeMarketplaceVisibility(payload.visibility),
997
1176
  createdAt: existingManifest?.createdAt || now,
998
1177
  updatedAt: now,
999
1178
  };
@@ -1056,6 +1235,7 @@ export function publishFlowSnippet(workspaceRoot, payload = {}, opts = {}) {
1056
1235
  tags: Array.isArray(payload.tags) ? payload.tags.map((x) => String(x).trim()).filter(Boolean) : [],
1057
1236
  ownerUserId,
1058
1237
  createdBy: ownerUserId,
1238
+ visibility: normalizeMarketplaceVisibility(payload.visibility),
1059
1239
  nodeCount,
1060
1240
  edgeCount: edges.length,
1061
1241
  createdAt: now,
@@ -29,7 +29,7 @@ import { normalizeNodeUiForSlots } from "./node-ui-kit.mjs";
29
29
  export const NODE_PACKAGE_ENTRY = "index.mjs";
30
30
 
31
31
  /** 槽位类型;与 builtin/nodes/*.md 的 `type:` 取值一致。 */
32
- const SLOT_TYPES = new Set(["text", "file", "bool", "node", "image", "json"]);
32
+ const SLOT_TYPES = new Set(["text", "file", "bool", "node", "image", "json", "context"]);
33
33
 
34
34
  function staticEval(node, where) {
35
35
  if (!node) throw new Error(`${where}: 空表达式`);
@@ -0,0 +1,200 @@
1
+ import crypto from "crypto";
2
+ import fs from "fs";
3
+ import path from "path";
4
+
5
+ import { getAgentflowDataRoot } from "./paths.mjs";
6
+
7
+ export const SPACES_FILENAME = "spaces.json";
8
+
9
+ function spacesPath() {
10
+ return path.join(getAgentflowDataRoot(), SPACES_FILENAME);
11
+ }
12
+
13
+ function cleanText(value, max = 200) {
14
+ return String(value || "").trim().slice(0, max);
15
+ }
16
+
17
+ export function normalizeSpaceSlug(value = "") {
18
+ return String(value || "")
19
+ .trim()
20
+ .toLowerCase()
21
+ .replace(/[^a-z0-9_-]+/g, "-")
22
+ .replace(/^-+|-+$/g, "")
23
+ .slice(0, 64);
24
+ }
25
+
26
+ export function normalizeSpaceVisibility(value = "public") {
27
+ return String(value || "").trim().toLowerCase() === "private" ? "private" : "public";
28
+ }
29
+
30
+ export function normalizeSpacePagePath(value = "/") {
31
+ const raw = String(value || "").trim();
32
+ if (!raw || raw === "/") return "/";
33
+ const parts = raw.split("/").map((part) => normalizeSpaceSlug(part)).filter(Boolean);
34
+ return parts.length ? `/${parts.join("/")}` : "/";
35
+ }
36
+
37
+ function normalizePage(page = {}, index = 0) {
38
+ const shareId = cleanText(page.shareId, 160);
39
+ if (!shareId) return null;
40
+ const pagePath = normalizeSpacePagePath(page.path || page.route || "/");
41
+ return {
42
+ id: cleanText(page.id, 100) || `page_${crypto.randomBytes(8).toString("hex")}`,
43
+ title: cleanText(page.title, 160) || (pagePath === "/" ? "首页" : pagePath.split("/").filter(Boolean).pop()),
44
+ path: pagePath,
45
+ shareId,
46
+ hidden: page.hidden === true,
47
+ order: Number.isFinite(Number(page.order)) ? Number(page.order) : index,
48
+ createdAt: cleanText(page.createdAt, 80) || new Date().toISOString(),
49
+ updatedAt: cleanText(page.updatedAt, 80) || new Date().toISOString(),
50
+ };
51
+ }
52
+
53
+ function normalizeSpace(space = {}) {
54
+ const ownerId = cleanText(space.ownerId, 100);
55
+ const slug = normalizeSpaceSlug(space.slug);
56
+ if (!ownerId || !slug) return null;
57
+ const pages = (Array.isArray(space.pages) ? space.pages : [])
58
+ .map(normalizePage)
59
+ .filter(Boolean)
60
+ .sort((a, b) => a.order - b.order || a.createdAt.localeCompare(b.createdAt));
61
+ return {
62
+ id: cleanText(space.id, 100) || `space_${crypto.randomBytes(8).toString("hex")}`,
63
+ ownerId,
64
+ slug,
65
+ title: cleanText(space.title, 160) || slug,
66
+ description: cleanText(space.description, 1000),
67
+ visibility: normalizeSpaceVisibility(space.visibility),
68
+ status: String(space.status || "active").trim().toLowerCase() === "paused" ? "paused" : "active",
69
+ pages,
70
+ createdAt: cleanText(space.createdAt, 80) || new Date().toISOString(),
71
+ updatedAt: cleanText(space.updatedAt, 80) || new Date().toISOString(),
72
+ };
73
+ }
74
+
75
+ export function readSpaces() {
76
+ const file = spacesPath();
77
+ if (!fs.existsSync(file)) return [];
78
+ try {
79
+ const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
80
+ const input = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.spaces) ? parsed.spaces : [];
81
+ return input.map(normalizeSpace).filter(Boolean);
82
+ } catch {
83
+ return [];
84
+ }
85
+ }
86
+
87
+ export function writeSpaces(spaces = []) {
88
+ const file = spacesPath();
89
+ const normalized = (Array.isArray(spaces) ? spaces : []).map(normalizeSpace).filter(Boolean);
90
+ fs.mkdirSync(path.dirname(file), { recursive: true });
91
+ const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
92
+ fs.writeFileSync(temp, `${JSON.stringify({ version: 1, spaces: normalized }, null, 2)}\n`, "utf-8");
93
+ fs.renameSync(temp, file);
94
+ return normalized;
95
+ }
96
+
97
+ export function listSpacesForUser(userId = "", isAdmin = false) {
98
+ const actor = cleanText(userId, 100);
99
+ return readSpaces().filter((space) => isAdmin || space.ownerId === actor);
100
+ }
101
+
102
+ export function getSpaceById(id = "") {
103
+ const wanted = cleanText(id, 100);
104
+ return readSpaces().find((space) => space.id === wanted) || null;
105
+ }
106
+
107
+ export function getSpaceByRoute(ownerId = "", slug = "") {
108
+ const owner = cleanText(ownerId, 100);
109
+ const safeSlug = normalizeSpaceSlug(slug);
110
+ return readSpaces().find((space) => space.ownerId === owner && space.slug === safeSlug) || null;
111
+ }
112
+
113
+ export function createSpace({ ownerId, slug, title, description = "", visibility = "public" } = {}) {
114
+ const owner = cleanText(ownerId, 100);
115
+ const safeSlug = normalizeSpaceSlug(slug || title);
116
+ if (!owner) return { error: "Missing space owner" };
117
+ if (!safeSlug) return { error: "Space slug must contain letters or numbers" };
118
+ const spaces = readSpaces();
119
+ if (spaces.some((space) => space.ownerId === owner && space.slug === safeSlug)) {
120
+ return { error: "Space slug already exists" };
121
+ }
122
+ const now = new Date().toISOString();
123
+ const space = normalizeSpace({
124
+ id: `space_${crypto.randomBytes(8).toString("hex")}`,
125
+ ownerId: owner,
126
+ slug: safeSlug,
127
+ title: title || safeSlug,
128
+ description,
129
+ visibility,
130
+ pages: [],
131
+ createdAt: now,
132
+ updatedAt: now,
133
+ });
134
+ spaces.push(space);
135
+ writeSpaces(spaces);
136
+ return { space };
137
+ }
138
+
139
+ export function updateSpace(id = "", patch = {}) {
140
+ const spaces = readSpaces();
141
+ const index = spaces.findIndex((space) => space.id === cleanText(id, 100));
142
+ if (index < 0) return { error: "Space not found" };
143
+ const current = spaces[index];
144
+ const nextSlug = patch.slug == null ? current.slug : normalizeSpaceSlug(patch.slug);
145
+ if (!nextSlug) return { error: "Space slug must contain letters or numbers" };
146
+ if (spaces.some((space, itemIndex) => itemIndex !== index && space.ownerId === current.ownerId && space.slug === nextSlug)) {
147
+ return { error: "Space slug already exists" };
148
+ }
149
+ const next = normalizeSpace({
150
+ ...current,
151
+ ...(patch.title == null ? {} : { title: patch.title }),
152
+ ...(patch.description == null ? {} : { description: patch.description }),
153
+ ...(patch.visibility == null ? {} : { visibility: patch.visibility }),
154
+ ...(patch.status == null ? {} : { status: patch.status }),
155
+ slug: nextSlug,
156
+ updatedAt: new Date().toISOString(),
157
+ });
158
+ spaces[index] = next;
159
+ writeSpaces(spaces);
160
+ return { space: next };
161
+ }
162
+
163
+ export function upsertSpacePage(spaceId = "", input = {}) {
164
+ const spaces = readSpaces();
165
+ const spaceIndex = spaces.findIndex((space) => space.id === cleanText(spaceId, 100));
166
+ if (spaceIndex < 0) return { error: "Space not found" };
167
+ const space = spaces[spaceIndex];
168
+ const pagePath = normalizeSpacePagePath(input.path || "/");
169
+ const existingIndex = space.pages.findIndex((page) => page.path === pagePath);
170
+ const previous = existingIndex >= 0 ? space.pages[existingIndex] : {};
171
+ const page = normalizePage({
172
+ ...previous,
173
+ ...input,
174
+ id: previous.id || input.id,
175
+ path: pagePath,
176
+ order: existingIndex >= 0 ? previous.order : space.pages.length,
177
+ createdAt: previous.createdAt || input.createdAt,
178
+ updatedAt: new Date().toISOString(),
179
+ }, existingIndex >= 0 ? existingIndex : space.pages.length);
180
+ if (!page) return { error: "Missing display share" };
181
+ if (existingIndex >= 0) space.pages[existingIndex] = page;
182
+ else space.pages.push(page);
183
+ space.updatedAt = new Date().toISOString();
184
+ spaces[spaceIndex] = normalizeSpace(space);
185
+ writeSpaces(spaces);
186
+ return { space: spaces[spaceIndex], page };
187
+ }
188
+
189
+ export function deleteSpacePage(spaceId = "", pageId = "") {
190
+ const spaces = readSpaces();
191
+ const spaceIndex = spaces.findIndex((space) => space.id === cleanText(spaceId, 100));
192
+ if (spaceIndex < 0) return { error: "Space not found" };
193
+ const before = spaces[spaceIndex].pages.length;
194
+ spaces[spaceIndex].pages = spaces[spaceIndex].pages.filter((page) => page.id !== cleanText(pageId, 100));
195
+ if (spaces[spaceIndex].pages.length === before) return { error: "Space page not found" };
196
+ spaces[spaceIndex].pages = spaces[spaceIndex].pages.map((page, index) => ({ ...page, order: index }));
197
+ spaces[spaceIndex].updatedAt = new Date().toISOString();
198
+ writeSpaces(spaces);
199
+ return { space: spaces[spaceIndex] };
200
+ }