@fieldwangai/agentflow 0.1.114 → 0.1.117

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.
@@ -3133,6 +3133,48 @@ function hydrateWorkspaceGraphForRuntime(workspaceRoot, scoped = {}, graph = {},
3133
3133
  return hydrateWorkspaceSlotMetaFromDefinitions(workspaceRoot, scoped, withMarketplaceRuntime, userCtx);
3134
3134
  }
3135
3135
 
3136
+ function adminWorkspaceOwnerSummary(ownerId = "") {
3137
+ const id = String(ownerId || "").trim();
3138
+ if (!id) return null;
3139
+ const users = readAuthUsers();
3140
+ const knownUserIds = new Set([
3141
+ ...Object.keys(users || {}),
3142
+ ...listAgentflowUserIds(),
3143
+ ].map((value) => String(value || "").trim()).filter(Boolean));
3144
+ if (!knownUserIds.has(id)) return null;
3145
+ return {
3146
+ userId: id,
3147
+ username: String(users?.[id]?.username || id),
3148
+ };
3149
+ }
3150
+
3151
+ function adminWorkspaceRequestedUserContext(userCtx = {}) {
3152
+ const ownerId = String(userCtx.adminOwnerId || "").trim();
3153
+ if (!ownerId) return { userCtx };
3154
+ if (userCtx.isAdmin !== true) {
3155
+ return { error: "Admin permission required", status: 403 };
3156
+ }
3157
+ const owner = adminWorkspaceOwnerSummary(ownerId);
3158
+ if (!owner) return { error: "Workspace owner not found", status: 404 };
3159
+ return {
3160
+ owner,
3161
+ userCtx: {
3162
+ ...userCtx,
3163
+ userId: owner.userId,
3164
+ adminOwnerId: "",
3165
+ },
3166
+ };
3167
+ }
3168
+
3169
+ function workspaceScopedUserContext(scoped = {}, userCtx = {}) {
3170
+ if (!scoped.adminReadonly || !scoped.ownerUserId) return userCtx;
3171
+ return {
3172
+ ...userCtx,
3173
+ userId: scoped.ownerUserId,
3174
+ adminOwnerId: "",
3175
+ };
3176
+ }
3177
+
3136
3178
  function resolveWorkspaceScopeRoot(workspaceRoot, params = {}, opts = {}) {
3137
3179
  const flowId = params.flowId != null ? String(params.flowId).trim() : "";
3138
3180
  if (!flowId) return { root: path.resolve(workspaceRoot), flowId: "", flowSource: "", archived: false };
@@ -3143,6 +3185,46 @@ function resolveWorkspaceScopeRoot(workspaceRoot, params = {}, opts = {}) {
3143
3185
  if (!isValidFlowSourceRead(flowSource)) {
3144
3186
  return { root: "", error: "Invalid flowSource" };
3145
3187
  }
3188
+ const adminOwnerId = String(params.adminOwnerId || opts.adminOwnerId || "").trim();
3189
+ if (adminOwnerId) {
3190
+ if (opts.isAdmin !== true) {
3191
+ return { root: "", error: "Admin permission required", status: 403 };
3192
+ }
3193
+ if (flowSource !== "user") {
3194
+ return { root: "", error: "Admin read-only review only supports user projects", status: 400 };
3195
+ }
3196
+ const owner = adminWorkspaceOwnerSummary(adminOwnerId);
3197
+ if (!owner) {
3198
+ return { root: "", error: "Workspace owner not found", status: 404 };
3199
+ }
3200
+ const targetFlow = listFlowsJson(workspaceRoot, { userId: owner.userId })
3201
+ .find((flow) => (
3202
+ flow.id === flowId
3203
+ && (flow.source || "user") === "user"
3204
+ && Boolean(flow.archived) === archived
3205
+ ));
3206
+ if (!targetFlow?.path) {
3207
+ return { root: "", error: "Pipeline workspace not found", status: 404 };
3208
+ }
3209
+ return {
3210
+ root: path.resolve(targetFlow.path),
3211
+ flowId,
3212
+ flowSource: "user",
3213
+ requestedFlowSource: flowSource,
3214
+ workspaceId: "",
3215
+ archived,
3216
+ collaboration: null,
3217
+ collaborationAccess: {
3218
+ allowed: true,
3219
+ writable: false,
3220
+ runnable: false,
3221
+ role: "admin-viewer",
3222
+ },
3223
+ adminReadonly: true,
3224
+ ownerUserId: owner.userId,
3225
+ ownerUsername: owner.username,
3226
+ };
3227
+ }
3146
3228
  const workspaceId = String(params.workspaceId || "").trim();
3147
3229
  let collaboration = getWorkspaceCollaborationForProject({
3148
3230
  workspaceId,
@@ -7472,6 +7554,17 @@ function resolvePrdWorkflowScope(workspaceRoot, params = {}, userCtx = {}, capab
7472
7554
  const flowId = String(params.flowId || "").trim();
7473
7555
  const flowSource = String(params.flowSource || "user").trim() || "user";
7474
7556
  const archived = params.archived === true || params.archived === "1" || params.flowArchived === true;
7557
+ const adminOwnerId = String(params.adminOwnerId || userCtx.adminOwnerId || "").trim();
7558
+ if (adminOwnerId && userCtx.isAdmin !== true) {
7559
+ return { error: "Admin permission required", status: 403 };
7560
+ }
7561
+ const adminOwner = adminOwnerId ? adminWorkspaceOwnerSummary(adminOwnerId) : null;
7562
+ if (adminOwnerId && !adminOwner) {
7563
+ return { error: "Workspace owner not found", status: 404 };
7564
+ }
7565
+ if (adminOwner && capability !== "read") {
7566
+ return { error: "Admin Workspace review is read-only", status: 403 };
7567
+ }
7475
7568
  const shareToken = String(params.workflowShare || params.workflow_share || "").trim();
7476
7569
  const linkCollaboration = shareToken ? getPrdWorkflowCollaborationByShareToken(shareToken) : null;
7477
7570
  if (shareToken && (!linkCollaboration || linkCollaboration.tapdId !== tapdId)) {
@@ -7480,8 +7573,10 @@ function resolvePrdWorkflowScope(workspaceRoot, params = {}, userCtx = {}, capab
7480
7573
  const memberCollaboration = tapdId
7481
7574
  ? getPrdWorkflowCollaborationForUser(tapdId, userCtx?.userId)
7482
7575
  : null;
7483
- const collaboration = linkCollaboration || memberCollaboration;
7484
- const access = linkCollaboration
7576
+ const collaboration = adminOwner ? null : (linkCollaboration || memberCollaboration);
7577
+ const access = adminOwner
7578
+ ? { allowed: true, writable: false, role: "admin-viewer", via: "admin-review" }
7579
+ : linkCollaboration
7485
7580
  ? { allowed: true, writable: false, role: "viewer", via: "share-link" }
7486
7581
  : prdWorkflowCollaborationAccess(collaboration, userCtx?.userId);
7487
7582
  if (collaboration && !access.allowed) {
@@ -7490,7 +7585,7 @@ function resolvePrdWorkflowScope(workspaceRoot, params = {}, userCtx = {}, capab
7490
7585
  if (capability === "write" && (linkCollaboration || (collaboration && !access.writable))) {
7491
7586
  return { error: "PRD Workflow collaboration edit permission denied", status: 403 };
7492
7587
  }
7493
- const ownerId = String(collaboration?.ownerId || userCtx?.userId || "").trim();
7588
+ const ownerId = String(adminOwner?.userId || collaboration?.ownerId || userCtx?.userId || "").trim();
7494
7589
  const stateRoot = path.resolve(getAgentflowUserDataRoot(ownerId));
7495
7590
  let executionRoot = path.resolve(workspaceRoot);
7496
7591
  if (flowId) {
@@ -7498,6 +7593,7 @@ function resolvePrdWorkflowScope(workspaceRoot, params = {}, userCtx = {}, capab
7498
7593
  flowId,
7499
7594
  flowSource,
7500
7595
  workspaceId: params.workspaceId || "",
7596
+ adminOwnerId,
7501
7597
  archived,
7502
7598
  }, userCtx);
7503
7599
  if (projectScope.error) {
@@ -7516,6 +7612,7 @@ function resolvePrdWorkflowScope(workspaceRoot, params = {}, userCtx = {}, capab
7516
7612
  collaborationAccess: access,
7517
7613
  shareToken,
7518
7614
  sharedByLink: Boolean(linkCollaboration),
7615
+ adminReadonly: Boolean(adminOwner),
7519
7616
  flowId,
7520
7617
  flowSource,
7521
7618
  archived,
@@ -7526,7 +7623,8 @@ function prdWorkflowKey(userCtx = {}, flowSource = "user", flowId = "", tapdId =
7526
7623
  const id = String(tapdId || "").trim();
7527
7624
  const collaboration = getPrdWorkflowCollaborationByShareToken(shareToken)
7528
7625
  || getPrdWorkflowCollaborationForUser(id, userCtx?.userId);
7529
- const actorScope = `user:${String(collaboration?.ownerId || userCtx?.userId || "")}`;
7626
+ const adminOwnerId = userCtx?.isAdmin === true ? String(userCtx?.adminOwnerId || "").trim() : "";
7627
+ const actorScope = `user:${String(collaboration?.ownerId || adminOwnerId || userCtx?.userId || "")}`;
7530
7628
  return [actorScope, id].join("\t");
7531
7629
  }
7532
7630
 
@@ -7678,6 +7776,32 @@ function prdWorkflowReviewIdFromRequest(tapdId, payload = {}, durability = "temp
7678
7776
  return prdWorkflowSafeStateId(["review", tapdId, stageHead, digest].filter(Boolean).join("-")).slice(0, 64);
7679
7777
  }
7680
7778
 
7779
+ function prdWorkflowReviewArtifactKey(tapdId, payload = {}, durability = "temporary") {
7780
+ const explicit = String(payload.artifactKey || payload.artifact_key || "").trim();
7781
+ if (explicit) return explicit.slice(0, 500);
7782
+ const issueKey = prdWorkflowSafeStateId(
7783
+ payload.issueKey || payload.issue_key || payload.issue || "global",
7784
+ ).toLowerCase();
7785
+ const platform = prdWorkflowSafeStateId(payload.platform || "all").toLowerCase();
7786
+ const stage = prdWorkflowSafeStateId(
7787
+ payload.stage
7788
+ || payload.stageKey
7789
+ || payload.stage_key
7790
+ || payload.action
7791
+ || payload.actionId
7792
+ || payload.action_id
7793
+ || "review",
7794
+ ).toLowerCase();
7795
+ return [
7796
+ "prd-review",
7797
+ prdWorkflowSafeStateId(tapdId).toLowerCase(),
7798
+ issueKey,
7799
+ platform,
7800
+ stage,
7801
+ String(durability || "temporary").trim().toLowerCase() || "temporary",
7802
+ ].join(":").slice(0, 500);
7803
+ }
7804
+
7681
7805
  function prdWorkflowStatePath(scopedRoot, tapdId) {
7682
7806
  const rootDir = scopedRoot || process.cwd();
7683
7807
  return path.join(rootDir, ".workspace", "prd-flow", "workflow-state", `${prdWorkflowSafeStateId(tapdId)}.json`);
@@ -7724,6 +7848,78 @@ function prdWorkflowReviewPaths(scopedRoot, tapdId, reviewId) {
7724
7848
  };
7725
7849
  }
7726
7850
 
7851
+ function prdWorkflowReviewIndexPath(tapdId, reviewId) {
7852
+ return path.join(
7853
+ getAgentflowDataRoot(),
7854
+ "prd-workflow-review-index",
7855
+ prdWorkflowSafeStateId(tapdId),
7856
+ `${prdWorkflowSafeStateId(reviewId)}.json`,
7857
+ );
7858
+ }
7859
+
7860
+ function prdWorkflowWriteReviewIndex(ownerId, tapdId, reviewId) {
7861
+ const indexPath = prdWorkflowReviewIndexPath(tapdId, reviewId);
7862
+ const tempPath = `${indexPath}.${process.pid}.${Date.now()}.tmp`;
7863
+ fs.mkdirSync(path.dirname(indexPath), { recursive: true });
7864
+ fs.writeFileSync(tempPath, JSON.stringify({
7865
+ version: 1,
7866
+ tapdId: String(tapdId || ""),
7867
+ reviewId: prdWorkflowSafeStateId(reviewId),
7868
+ ownerId: String(ownerId || "").trim(),
7869
+ updatedAt: new Date().toISOString(),
7870
+ }, null, 2) + "\n", "utf-8");
7871
+ fs.renameSync(tempPath, indexPath);
7872
+ }
7873
+
7874
+ function prdWorkflowReadReviewIndex(tapdId, reviewId) {
7875
+ try {
7876
+ const parsed = JSON.parse(fs.readFileSync(prdWorkflowReviewIndexPath(tapdId, reviewId), "utf-8"));
7877
+ if (
7878
+ String(parsed?.tapdId || "") !== String(tapdId || "")
7879
+ || prdWorkflowSafeStateId(parsed?.reviewId) !== prdWorkflowSafeStateId(reviewId)
7880
+ ) {
7881
+ return null;
7882
+ }
7883
+ return parsed;
7884
+ } catch {
7885
+ return null;
7886
+ }
7887
+ }
7888
+
7889
+ function prdWorkflowReviewFileExists(paths) {
7890
+ try {
7891
+ return fs.existsSync(paths.markdownPath) && fs.statSync(paths.markdownPath).isFile();
7892
+ } catch {
7893
+ return false;
7894
+ }
7895
+ }
7896
+
7897
+ function prdWorkflowResolveReviewPaths(scopedRoot, tapdId, reviewId) {
7898
+ const direct = prdWorkflowReviewPaths(scopedRoot, tapdId, reviewId);
7899
+ if (prdWorkflowReviewFileExists(direct)) return direct;
7900
+
7901
+ const indexed = prdWorkflowReadReviewIndex(tapdId, reviewId);
7902
+ if (indexed) {
7903
+ const indexedPaths = prdWorkflowReviewPaths(
7904
+ getAgentflowUserDataRoot(indexed.ownerId || ""),
7905
+ tapdId,
7906
+ reviewId,
7907
+ );
7908
+ if (prdWorkflowReviewFileExists(indexedPaths)) return indexedPaths;
7909
+ }
7910
+
7911
+ const candidateOwners = ["", ...listAgentflowUserIds()];
7912
+ for (const ownerId of candidateOwners) {
7913
+ const candidate = prdWorkflowReviewPaths(getAgentflowUserDataRoot(ownerId), tapdId, reviewId);
7914
+ if (!prdWorkflowReviewFileExists(candidate)) continue;
7915
+ try {
7916
+ prdWorkflowWriteReviewIndex(ownerId, tapdId, reviewId);
7917
+ } catch (_) {}
7918
+ return candidate;
7919
+ }
7920
+ return direct;
7921
+ }
7922
+
7727
7923
  function prdWorkflowMigrateLegacyState(legacyRoot, stateRoot, tapdId) {
7728
7924
  const sourceRoot = path.resolve(legacyRoot || "");
7729
7925
  const destinationRoot = path.resolve(stateRoot || "");
@@ -7753,6 +7949,75 @@ function prdWorkflowMigrateLegacyState(legacyRoot, stateRoot, tapdId) {
7753
7949
  } catch (_) {}
7754
7950
  }
7755
7951
 
7952
+ function prdWorkflowReviewShortLinkDir(root) {
7953
+ return path.join(path.resolve(root || process.cwd()), ".workspace", "prd-flow", "review-short-links");
7954
+ }
7955
+
7956
+ function prdWorkflowReviewShortLinkPath(root, shortCode) {
7957
+ return path.join(
7958
+ prdWorkflowReviewShortLinkDir(root),
7959
+ `${String(shortCode || "").trim()}.json`,
7960
+ );
7961
+ }
7962
+
7963
+ function prdWorkflowReadReviewShortLink(root, shortCode) {
7964
+ const code = String(shortCode || "").trim();
7965
+ if (!/^[A-Za-z0-9_-]{8,32}$/.test(code)) return null;
7966
+ try {
7967
+ const filePath = prdWorkflowReviewShortLinkPath(root, code);
7968
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) return null;
7969
+ const link = JSON.parse(fs.readFileSync(filePath, "utf-8"));
7970
+ const targetPath = String(link?.targetPath || "").trim();
7971
+ if (!targetPath.startsWith("/api/prd-workflow/review/") || /[\r\n]/.test(targetPath)) return null;
7972
+ return {
7973
+ ...link,
7974
+ shortCode: code,
7975
+ targetPath,
7976
+ filePath,
7977
+ };
7978
+ } catch {
7979
+ return null;
7980
+ }
7981
+ }
7982
+
7983
+ function prdWorkflowCreateReviewShortLink(root, reviewUrl, review = {}) {
7984
+ let parsed;
7985
+ try {
7986
+ parsed = new URL(String(reviewUrl || ""));
7987
+ } catch {
7988
+ return null;
7989
+ }
7990
+ const targetPath = `${parsed.pathname}${parsed.search}`;
7991
+ if (!targetPath.startsWith("/api/prd-workflow/review/")) return null;
7992
+ const digest = crypto.createHash("sha256").update(targetPath).digest("base64url");
7993
+ const dir = prdWorkflowReviewShortLinkDir(root);
7994
+ fs.mkdirSync(dir, { recursive: true });
7995
+ for (let length = 8; length <= 24; length += 2) {
7996
+ const shortCode = digest.slice(0, length);
7997
+ const existing = prdWorkflowReadReviewShortLink(root, shortCode);
7998
+ if (existing && existing.targetPath !== targetPath) continue;
7999
+ const link = {
8000
+ shortCode,
8001
+ targetPath,
8002
+ tapdId: String(review?.tapdId || ""),
8003
+ reviewId: String(review?.id || ""),
8004
+ durability: String(review?.durability || "temporary"),
8005
+ expiresAt: String(review?.expiresAt || ""),
8006
+ createdAt: String(review?.createdAt || new Date().toISOString()),
8007
+ };
8008
+ fs.writeFileSync(
8009
+ prdWorkflowReviewShortLinkPath(root, shortCode),
8010
+ JSON.stringify(link, null, 2) + "\n",
8011
+ "utf-8",
8012
+ );
8013
+ return {
8014
+ ...link,
8015
+ shortUrl: `${parsed.origin}/r/${shortCode}`,
8016
+ };
8017
+ }
8018
+ throw new Error("Unable to allocate a unique review short code");
8019
+ }
8020
+
7756
8021
  function prdWorkflowPruneReviews(scopedRoot, tapdId, maxReviews = 200) {
7757
8022
  try {
7758
8023
  const dir = prdWorkflowReviewDir(scopedRoot, tapdId);
@@ -7773,10 +8038,12 @@ function prdWorkflowPruneReviews(scopedRoot, tapdId, maxReviews = 200) {
7773
8038
  for (const entry of entries.filter((item) => Number.isFinite(item.expiresMs) && item.expiresMs < now)) {
7774
8039
  try { fs.unlinkSync(entry.abs); } catch (_) {}
7775
8040
  try { fs.unlinkSync(path.join(dir, `${entry.id}.md`)); } catch (_) {}
8041
+ try { fs.unlinkSync(prdWorkflowReviewIndexPath(tapdId, entry.id)); } catch (_) {}
7776
8042
  }
7777
8043
  for (const entry of entries.filter((item) => !(Number.isFinite(item.expiresMs) && item.expiresMs < now)).slice(maxReviews)) {
7778
8044
  try { fs.unlinkSync(entry.abs); } catch (_) {}
7779
8045
  try { fs.unlinkSync(path.join(dir, `${entry.id}.md`)); } catch (_) {}
8046
+ try { fs.unlinkSync(prdWorkflowReviewIndexPath(tapdId, entry.id)); } catch (_) {}
7780
8047
  }
7781
8048
  } catch (_) {}
7782
8049
  }
@@ -7805,6 +8072,30 @@ function prdWorkflowReviewSplitFrontmatter(markdown) {
7805
8072
  return { frontmatter: match[1].trim(), body: text.slice(match[0].length) };
7806
8073
  }
7807
8074
 
8075
+ function prdWorkflowReviewStripLegacyMetadata(body) {
8076
+ const lines = String(body || "").replace(/\r\n/g, "\n").split("\n");
8077
+ const visible = [];
8078
+ let fenced = false;
8079
+ let hidden = false;
8080
+ for (const line of lines) {
8081
+ if (!hidden && /^\s*```/.test(line)) {
8082
+ fenced = !fenced;
8083
+ visible.push(line);
8084
+ continue;
8085
+ }
8086
+ if (!fenced && !hidden && /<!--\s*prd-flow-start\b/.test(line)) {
8087
+ hidden = !/prd-flow-end\s*-->/.test(line);
8088
+ continue;
8089
+ }
8090
+ if (hidden) {
8091
+ if (/prd-flow-end\s*-->/.test(line)) hidden = false;
8092
+ continue;
8093
+ }
8094
+ visible.push(line);
8095
+ }
8096
+ return visible.join("\n");
8097
+ }
8098
+
7808
8099
  function prdWorkflowReviewRenderFrontmatter(frontmatter) {
7809
8100
  if (!String(frontmatter || "").trim()) return "";
7810
8101
  const rows = [];
@@ -8051,20 +8342,24 @@ export function prdWorkflowReviewMarkdownToHtml(markdown) {
8051
8342
  const { frontmatter, body } = prdWorkflowReviewSplitFrontmatter(markdown);
8052
8343
  return [
8053
8344
  prdWorkflowReviewRenderFrontmatter(frontmatter),
8054
- prdWorkflowReviewBodyToHtml(body),
8345
+ prdWorkflowReviewBodyToHtml(prdWorkflowReviewStripLegacyMetadata(body)),
8055
8346
  ].filter(Boolean).join("\n");
8056
8347
  }
8057
8348
 
8058
8349
  function prdWorkflowReviewExtractPageTitle(markdown, fallbackTitle) {
8059
8350
  const { frontmatter, body } = prdWorkflowReviewSplitFrontmatter(markdown);
8060
- const lines = String(body || "").replace(/\r\n/g, "\n").split("\n");
8351
+ const visibleBody = prdWorkflowReviewStripLegacyMetadata(body);
8352
+ const lines = String(visibleBody || "").replace(/\r\n/g, "\n").split("\n");
8061
8353
  const firstContentIndex = lines.findIndex((line) => String(line || "").trim());
8062
8354
  const heading = firstContentIndex >= 0
8063
8355
  ? String(lines[firstContentIndex] || "").trim().match(/^#\s+(.+)$/)
8064
8356
  : null;
8065
8357
  if (!heading) {
8066
8358
  return {
8067
- markdown: String(markdown || ""),
8359
+ markdown: [
8360
+ frontmatter ? `---\n${frontmatter}\n---` : "",
8361
+ visibleBody,
8362
+ ].filter(Boolean).join("\n\n"),
8068
8363
  title: String(fallbackTitle || "PRD Workflow Review"),
8069
8364
  };
8070
8365
  }
@@ -8276,7 +8571,7 @@ export function prdWorkflowReviewHtml(title, markdown, meta = {}) {
8276
8571
  </html>`;
8277
8572
  }
8278
8573
 
8279
- function prdWorkflowCreateReview(scopedRoot, tapdId, payload = {}, urlBase = "") {
8574
+ function prdWorkflowCreateReview(scopedRoot, tapdId, payload = {}, urlBase = "", ownerId = "") {
8280
8575
  const content = String(payload.markdown || payload.content || payload.rawOutput || "").slice(0, 500000);
8281
8576
  if (!content.trim()) throw new Error("Missing review markdown");
8282
8577
  const title = String(payload.title || payload.label || "PRD Workflow Review").trim().slice(0, 160) || "PRD Workflow Review";
@@ -8312,6 +8607,7 @@ function prdWorkflowCreateReview(scopedRoot, tapdId, payload = {}, urlBase = "")
8312
8607
  fs.mkdirSync(paths.dir, { recursive: true });
8313
8608
  fs.writeFileSync(paths.markdownPath, content.trimEnd() + "\n", "utf-8");
8314
8609
  fs.writeFileSync(paths.metaPath, JSON.stringify(meta, null, 2) + "\n", "utf-8");
8610
+ prdWorkflowWriteReviewIndex(ownerId, tapdId, paths.id);
8315
8611
  prdWorkflowPruneReviews(scopedRoot, tapdId);
8316
8612
  prdWorkflowAppendAudit(scopedRoot, tapdId, {
8317
8613
  type: "review-created",
@@ -8959,18 +9255,27 @@ function prdWorkflowNormalizeRuntimeEvent(tapdId, event = {}) {
8959
9255
 
8960
9256
  function prdWorkflowMergeRuntimeEventArrays(left, right) {
8961
9257
  const out = [];
8962
- const seen = new Set();
9258
+ const seen = new Map();
8963
9259
  for (const value of [...(Array.isArray(left) ? left : []), ...(Array.isArray(right) ? right : [])]) {
8964
9260
  if (value == null) continue;
8965
- let key = "";
9261
+ const explicitKey = value && typeof value === "object" && !Array.isArray(value)
9262
+ ? String(value.key || value.artifactKey || value.artifact_key || "").trim()
9263
+ : "";
9264
+ let key = explicitKey ? `key:${explicitKey}` : "";
8966
9265
  try {
8967
- key = typeof value === "string" ? value : JSON.stringify(value);
9266
+ if (!key) key = typeof value === "string" ? value : JSON.stringify(value);
8968
9267
  } catch {
8969
9268
  key = String(value);
8970
9269
  }
8971
- if (seen.has(key)) continue;
8972
- seen.add(key);
8973
- out.push(value);
9270
+ const index = seen.get(key);
9271
+ if (index == null) {
9272
+ seen.set(key, out.length);
9273
+ out.push(value);
9274
+ continue;
9275
+ }
9276
+ if (explicitKey) {
9277
+ out[index] = value;
9278
+ }
8974
9279
  }
8975
9280
  return out;
8976
9281
  }
@@ -10699,6 +11004,7 @@ function normalizeContextInstanceIds(raw) {
10699
11004
  * @param {string} opts.workspaceRoot
10700
11005
  * @param {number} opts.port
10701
11006
  * @param {boolean} [opts.hideCommunityLinks]
11007
+ * @param {boolean} [opts.enableWorkspaceScheduler]
10702
11008
  * @param {string} [opts.staticDir] 默认 PACKAGE_ROOT/builtin/web-ui/dist(npm run build 产出)
10703
11009
  * @returns {Promise<import('http').Server>}
10704
11010
  */
@@ -10707,6 +11013,7 @@ export function startUiServer({
10707
11013
  port,
10708
11014
  host = "127.0.0.1",
10709
11015
  hideCommunityLinks = false,
11016
+ enableWorkspaceScheduler = true,
10710
11017
  staticDir = path.join(PACKAGE_ROOT, "builtin", "web-ui", "dist"),
10711
11018
  }) {
10712
11019
  const root = path.resolve(workspaceRoot);
@@ -10784,7 +11091,11 @@ export function startUiServer({
10784
11091
  }
10785
11092
 
10786
11093
  const authUser = getAuthUserFromRequest(req);
10787
- const userCtx = authUser ? { userId: authUser.userId, isAdmin: Boolean(authUser.isAdmin) } : {};
11094
+ const userCtx = authUser ? {
11095
+ userId: authUser.userId,
11096
+ isAdmin: Boolean(authUser.isAdmin),
11097
+ adminOwnerId: String(url.searchParams.get("adminOwnerId") || "").trim(),
11098
+ } : {};
10788
11099
  if (req.method === "GET" && url.pathname === "/api/auth/session-token") {
10789
11100
  if (!authUser?.userId) {
10790
11101
  json(res, 401, { error: "Unauthorized" });
@@ -11096,7 +11407,19 @@ export function startUiServer({
11096
11407
  baseSnapshot,
11097
11408
  getSessionTokenFromRequest(req) || "",
11098
11409
  );
11099
- json(res, 200, { ok: true, snapshot });
11410
+ const workflowShare = workflowScope.collaboration?.shareToken
11411
+ ? prdWorkflowShareLinkSummary(
11412
+ workflowScope.collaboration,
11413
+ workflowScope.collaboration.shareToken,
11414
+ serverPublicBaseUrl(req, host, uiPort),
11415
+ userCtx.userId,
11416
+ )
11417
+ : null;
11418
+ json(res, 200, {
11419
+ ok: true,
11420
+ snapshot,
11421
+ ...(workflowShare ? { workflowShare, shareUrl: workflowShare.url } : {}),
11422
+ });
11100
11423
  } catch (e) {
11101
11424
  json(res, 500, { error: (e && e.message) || String(e) });
11102
11425
  }
@@ -11121,6 +11444,22 @@ export function startUiServer({
11121
11444
  json(res, 400, { error: "Missing tapdId" });
11122
11445
  return;
11123
11446
  }
11447
+ const rawSnapshot = payload.snapshot && typeof payload.snapshot === "object" && !Array.isArray(payload.snapshot)
11448
+ ? payload.snapshot
11449
+ : payload.prd || payload.next ? payload : null;
11450
+ if (!rawSnapshot) {
11451
+ json(res, 400, { error: "Missing snapshot" });
11452
+ return;
11453
+ }
11454
+ const existingCollaboration = getPrdWorkflowCollaborationForUser(tapdId, userCtx.userId);
11455
+ const existingAccess = prdWorkflowCollaborationAccess(existingCollaboration, userCtx.userId);
11456
+ const shareResult = existingCollaboration && existingAccess.role !== "owner"
11457
+ ? { record: existingCollaboration, created: false }
11458
+ : ensurePrdWorkflowShareLink({ tapdId, userId: userCtx.userId });
11459
+ if (shareResult.error) {
11460
+ json(res, shareResult.status || 400, { error: shareResult.error });
11461
+ return;
11462
+ }
11124
11463
  const flowId = String(payload.flowId || "").trim();
11125
11464
  const flowSource = String(payload.flowSource || "user").trim() || "user";
11126
11465
  const archived = payload.archived === true || payload.flowArchived === true;
@@ -11137,13 +11476,6 @@ export function startUiServer({
11137
11476
  }
11138
11477
  const scopedRoot = workflowScope.stateRoot;
11139
11478
  prdWorkflowMigrateLegacyState(workflowScope.executionRoot, scopedRoot, tapdId);
11140
- const rawSnapshot = payload.snapshot && typeof payload.snapshot === "object" && !Array.isArray(payload.snapshot)
11141
- ? payload.snapshot
11142
- : payload.prd || payload.next ? payload : null;
11143
- if (!rawSnapshot) {
11144
- json(res, 400, { error: "Missing snapshot" });
11145
- return;
11146
- }
11147
11479
  const normalizedSnapshot = {
11148
11480
  ...prdWorkflowSnapshotFromParsed(scopedRoot, tapdId, rawSnapshot, userCtx, { flowSource, flowId }),
11149
11481
  clientReportedAt: new Date().toISOString(),
@@ -11263,8 +11595,20 @@ export function startUiServer({
11263
11595
  }
11264
11596
  const materialized = prdWorkflowMaterializeSnapshot(workflowScope.executionRoot, scopedRoot, tapdId, userCtx, { flowSource, flowId });
11265
11597
  const withDiagnostic = prdWorkflowWithAgentflowTokenDiagnostic(materialized, getSessionTokenFromRequest(req) || "");
11598
+ const workflowShare = shareResult.record?.shareToken
11599
+ ? prdWorkflowShareLinkSummary(
11600
+ shareResult.record,
11601
+ shareResult.record.shareToken,
11602
+ serverPublicBaseUrl(req, host, uiPort, payload),
11603
+ userCtx.userId,
11604
+ )
11605
+ : null;
11266
11606
  prdWorkflowBroadcast(prdWorkflowKey(userCtx, flowSource, flowId, tapdId), { type: "snapshot-report", tapdId, snapshot: withDiagnostic });
11267
- json(res, 200, { ok: true, snapshot: withDiagnostic });
11607
+ json(res, 200, {
11608
+ ok: true,
11609
+ snapshot: withDiagnostic,
11610
+ ...(workflowShare ? { workflowShare, shareUrl: workflowShare.url } : {}),
11611
+ });
11268
11612
  } catch (e) {
11269
11613
  json(res, 500, { error: (e && e.message) || String(e) });
11270
11614
  }
@@ -11967,20 +12311,45 @@ export function startUiServer({
11967
12311
  }
11968
12312
  const scopedRoot = workflowScope.stateRoot;
11969
12313
  prdWorkflowMigrateLegacyState(workflowScope.executionRoot, scopedRoot, tapdId);
11970
- const review = prdWorkflowCreateReview(scopedRoot, tapdId, payload, serverPublicBaseUrl(req, host, uiPort, payload));
11971
- const reviewUrl = review.url;
12314
+ const review = prdWorkflowCreateReview(
12315
+ scopedRoot,
12316
+ tapdId,
12317
+ payload,
12318
+ serverPublicBaseUrl(req, host, uiPort, payload),
12319
+ workflowScope.ownerId,
12320
+ );
12321
+ const query = new URLSearchParams();
12322
+ if (flowId) query.set("flowId", flowId);
12323
+ if (flowId && flowSource && flowSource !== "user") query.set("flowSource", flowSource);
12324
+ if (archived) query.set("archived", "1");
12325
+ const reviewUrl = query.toString() ? `${review.url}?${query.toString()}` : review.url;
12326
+ let shortLink = null;
12327
+ try {
12328
+ shortLink = prdWorkflowCreateReviewShortLink(root, reviewUrl, review);
12329
+ } catch (e) {
12330
+ log.debug(`[prd-workflow] review short link failed: ${(e && e.message) || String(e)}`);
12331
+ }
12332
+ const shortUrl = shortLink?.shortUrl || "";
12333
+ const displayUrl = shortUrl || reviewUrl;
11972
12334
  const durability = review.durability || "temporary";
12335
+ const artifactKey = prdWorkflowReviewArtifactKey(tapdId, payload, durability);
11973
12336
  const reviewSource = review.source && typeof review.source === "object" && !Array.isArray(review.source)
11974
12337
  ? review.source
11975
12338
  : { kind: durability === "durable" ? "ai-doc" : "local-draft", durability };
12339
+ const idempotencyKey = String(
12340
+ payload.idempotencyKey || payload.idempotency_key || "",
12341
+ ).trim();
11976
12342
  const artifact = {
12343
+ key: artifactKey,
11977
12344
  label: payload.artifactLabel || "Markdown Review",
11978
12345
  kind: durability === "temporary" ? "temporary-review" : "review",
11979
12346
  persistence: "runtime",
11980
12347
  durability,
11981
12348
  source: reviewSource,
11982
12349
  confirmed: payload.confirmed === true || payload.confirmed === "1",
11983
- url: reviewUrl,
12350
+ url: displayUrl,
12351
+ canonicalUrl: reviewUrl,
12352
+ shortUrl,
11984
12353
  expiresAt: review.expiresAt || "",
11985
12354
  };
11986
12355
  const event = prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, {
@@ -11995,19 +12364,42 @@ export function startUiServer({
11995
12364
  detail: "已生成临时 Markdown review 链接",
11996
12365
  status: "current",
11997
12366
  issueKey: payload.issueKey || payload.issue_key || "",
12367
+ idempotencyKey,
11998
12368
  durability,
11999
12369
  sourceArtifact: reviewSource,
12000
12370
  expiresAt: review.expiresAt || "",
12001
12371
  artifacts: [artifact],
12002
- links: [{ label: "Markdown Review", url: reviewUrl, persistence: "runtime", durability, source: reviewSource, expiresAt: review.expiresAt || "" }],
12372
+ links: [{
12373
+ key: artifactKey,
12374
+ label: "Markdown Review",
12375
+ kind: artifact.kind,
12376
+ url: displayUrl,
12377
+ canonicalUrl: reviewUrl,
12378
+ shortUrl,
12379
+ persistence: "runtime",
12380
+ durability,
12381
+ source: reviewSource,
12382
+ expiresAt: review.expiresAt || "",
12383
+ }],
12003
12384
  reviewId: review.id,
12385
+ reviewShortCode: shortLink?.shortCode || "",
12004
12386
  });
12005
12387
  const snapshot = prdWorkflowWithAgentflowTokenDiagnostic(
12006
12388
  await prdWorkflowSnapshot(workflowScope.executionRoot, scopedRoot, tapdId, userCtx, { flowSource, flowId }),
12007
12389
  getSessionTokenFromRequest(req) || "",
12008
12390
  );
12009
12391
  prdWorkflowBroadcast(prdWorkflowKey(userCtx, flowSource, flowId, tapdId), { type: "review-link", tapdId, event, snapshot });
12010
- json(res, 200, { ok: true, review: { ...review, url: reviewUrl }, event, snapshot });
12392
+ json(res, 200, {
12393
+ ok: true,
12394
+ review: {
12395
+ ...review,
12396
+ url: reviewUrl,
12397
+ shortUrl,
12398
+ shortCode: shortLink?.shortCode || "",
12399
+ },
12400
+ event,
12401
+ snapshot,
12402
+ });
12011
12403
  } catch (e) {
12012
12404
  json(res, 500, { error: (e && e.message) || String(e) });
12013
12405
  }
@@ -12054,6 +12446,41 @@ export function startUiServer({
12054
12446
  return;
12055
12447
  }
12056
12448
 
12449
+ if (req.method === "GET" && url.pathname.startsWith("/r/")) {
12450
+ try {
12451
+ const parts = url.pathname.split("/").filter(Boolean);
12452
+ const shortCode = decodeURIComponent(parts[1] || "");
12453
+ if (parts.length !== 2) {
12454
+ res.writeHead(404);
12455
+ res.end("Not found");
12456
+ return;
12457
+ }
12458
+ const link = prdWorkflowReadReviewShortLink(root, shortCode);
12459
+ if (!link) {
12460
+ res.writeHead(404);
12461
+ res.end("Not found");
12462
+ return;
12463
+ }
12464
+ const expiresMs = Date.parse(link.expiresAt || "");
12465
+ if (Number.isFinite(expiresMs) && expiresMs < Date.now()) {
12466
+ try { fs.unlinkSync(link.filePath); } catch (_) {}
12467
+ res.writeHead(410, { "Content-Type": "text/plain; charset=utf-8" });
12468
+ res.end("Review link expired");
12469
+ return;
12470
+ }
12471
+ res.writeHead(302, {
12472
+ Location: link.targetPath,
12473
+ "Cache-Control": "no-store",
12474
+ "Referrer-Policy": "no-referrer",
12475
+ });
12476
+ res.end();
12477
+ } catch {
12478
+ res.writeHead(404);
12479
+ res.end("Not found");
12480
+ }
12481
+ return;
12482
+ }
12483
+
12057
12484
  if (req.method === "GET" && url.pathname.startsWith("/api/prd-workflow/review/")) {
12058
12485
  try {
12059
12486
  const parts = url.pathname.split("/").filter(Boolean);
@@ -12082,8 +12509,8 @@ export function startUiServer({
12082
12509
  }
12083
12510
  const scopedRoot = workflowScope.stateRoot;
12084
12511
  prdWorkflowMigrateLegacyState(workflowScope.executionRoot, scopedRoot, tapdId);
12085
- const paths = prdWorkflowReviewPaths(scopedRoot, tapdId, reviewId);
12086
- if (!fs.existsSync(paths.markdownPath) || !fs.statSync(paths.markdownPath).isFile()) {
12512
+ const paths = prdWorkflowResolveReviewPaths(scopedRoot, tapdId, reviewId);
12513
+ if (!prdWorkflowReviewFileExists(paths)) {
12087
12514
  res.writeHead(404);
12088
12515
  res.end("Not found");
12089
12516
  return;
@@ -12365,6 +12792,36 @@ export function startUiServer({
12365
12792
  return;
12366
12793
  }
12367
12794
 
12795
+ if (req.method === "GET" && url.pathname === "/api/admin/user-workspaces") {
12796
+ if (!authUser?.isAdmin) {
12797
+ json(res, 403, { error: "Admin permission required" });
12798
+ return;
12799
+ }
12800
+ const owner = adminWorkspaceOwnerSummary(url.searchParams.get("userId") || "");
12801
+ if (!owner) {
12802
+ json(res, 404, { error: "Workspace owner not found" });
12803
+ return;
12804
+ }
12805
+ try {
12806
+ const workspaces = listFlowsJson(root, { userId: owner.userId })
12807
+ .filter((flow) => (flow.source || "user") === "user")
12808
+ .map((flow) => ({
12809
+ id: String(flow.id || ""),
12810
+ source: "user",
12811
+ archived: flow.archived === true,
12812
+ description: String(flow.description || ""),
12813
+ ownerUserId: owner.userId,
12814
+ ownerUsername: owner.username,
12815
+ adminReadonly: true,
12816
+ }))
12817
+ .sort((a, b) => Number(a.archived) - Number(b.archived) || a.id.localeCompare(b.id));
12818
+ json(res, 200, { owner, workspaces });
12819
+ } catch (e) {
12820
+ json(res, 500, { error: (e && e.message) || String(e) });
12821
+ }
12822
+ return;
12823
+ }
12824
+
12368
12825
  if (req.method === "GET" && url.pathname === "/api/admin/run-detail") {
12369
12826
  if (!authUser?.isAdmin) {
12370
12827
  json(res, 403, { error: "Admin permission required" });
@@ -12740,12 +13197,17 @@ export function startUiServer({
12740
13197
  flowId,
12741
13198
  flowSource,
12742
13199
  workspaceId: payload.workspaceId || "",
13200
+ adminOwnerId: payload.adminOwnerId || "",
12743
13201
  archived,
12744
13202
  }, userCtx);
12745
13203
  if (scoped.error) {
12746
13204
  json(res, scoped.status || 400, { error: scoped.error });
12747
13205
  return;
12748
13206
  }
13207
+ if (scoped.adminReadonly) {
13208
+ json(res, 403, { error: "Admin Workspace review is read-only" });
13209
+ return;
13210
+ }
12749
13211
  const ensured = ensureWorkspaceCollaboration({
12750
13212
  flowId,
12751
13213
  flowSource,
@@ -12857,7 +13319,12 @@ export function startUiServer({
12857
13319
  json(res, scoped.status || 400, { error: scoped.error });
12858
13320
  return;
12859
13321
  }
12860
- const key = workspaceCollaborationEventKey(userCtx, scoped.flowSource, scoped.flowId, scoped.archived);
13322
+ const key = workspaceCollaborationEventKey(
13323
+ workspaceScopedUserContext(scoped, userCtx),
13324
+ scoped.flowSource,
13325
+ scoped.flowId,
13326
+ scoped.archived,
13327
+ );
12861
13328
  let subscribers = workspaceCollaborationSubscribers.get(key);
12862
13329
  if (!subscribers) {
12863
13330
  subscribers = new Set();
@@ -12974,7 +13441,8 @@ export function startUiServer({
12974
13441
  return;
12975
13442
  }
12976
13443
  const { path: graphPath, graph } = readWorkspaceGraph(scoped.root);
12977
- const hydratedGraph = hydrateWorkspaceGraphForRuntime(root, scoped, graph, userCtx);
13444
+ const scopedUserCtx = workspaceScopedUserContext(scoped, userCtx);
13445
+ const hydratedGraph = hydrateWorkspaceGraphForRuntime(root, scoped, graph, scopedUserCtx);
12978
13446
  const collaborationAccess = scoped.collaborationAccess || workspaceCollaborationAccess(null, userCtx.userId);
12979
13447
  json(res, 200, {
12980
13448
  ok: true,
@@ -12988,9 +13456,15 @@ export function startUiServer({
12988
13456
  flowSource: scoped.flowSource,
12989
13457
  archived: scoped.archived,
12990
13458
  writable: !(scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource))
13459
+ && scoped.adminReadonly !== true
12991
13460
  && collaborationAccess.writable !== false,
12992
13461
  collaboration: workspaceCollaborationSummaryWithUsers(scoped.collaboration, userCtx.userId),
12993
- workspaceSchedules: listWorkspaceScheduleStatusesForFlow(userCtx, scoped.flowSource || "user", scoped.flowId || ""),
13462
+ adminReview: scoped.adminReadonly ? {
13463
+ readonly: true,
13464
+ ownerUserId: scoped.ownerUserId,
13465
+ ownerUsername: scoped.ownerUsername,
13466
+ } : null,
13467
+ workspaceSchedules: listWorkspaceScheduleStatusesForFlow(scopedUserCtx, scoped.flowSource || "user", scoped.flowId || ""),
12994
13468
  });
12995
13469
  } catch (e) {
12996
13470
  json(res, 500, { error: (e && e.message) || String(e) });
@@ -13011,12 +13485,17 @@ export function startUiServer({
13011
13485
  flowId: payload.flowId || "",
13012
13486
  flowSource: payload.flowSource || "user",
13013
13487
  workspaceId: payload.workspaceId || "",
13488
+ adminOwnerId: payload.adminOwnerId || "",
13014
13489
  archived: payload.archived === true || payload.flowArchived === true,
13015
13490
  }, userCtx);
13016
13491
  if (scoped.error) {
13017
13492
  json(res, 400, { error: scoped.error });
13018
13493
  return;
13019
13494
  }
13495
+ if (scoped.adminReadonly) {
13496
+ json(res, 403, { error: "Admin Workspace review is read-only" });
13497
+ return;
13498
+ }
13020
13499
  const { graph } = readWorkspaceGraph(scoped.root);
13021
13500
  const nodeIds = normalizeDisplayShareNodeIds(payload.nodeIds, graph);
13022
13501
  if (nodeIds.length === 0) {
@@ -13056,6 +13535,7 @@ export function startUiServer({
13056
13535
  flowId: payload.flowId || "",
13057
13536
  flowSource: payload.flowSource || "user",
13058
13537
  workspaceId: payload.workspaceId || "",
13538
+ adminOwnerId: payload.adminOwnerId || "",
13059
13539
  archived: payload.archived === true || payload.flowArchived === true,
13060
13540
  }, userCtx);
13061
13541
  if (scoped.error) {
@@ -13169,8 +13649,21 @@ export function startUiServer({
13169
13649
  json(res, 400, { error: "Missing flowId" });
13170
13650
  return;
13171
13651
  }
13652
+ const scoped = resolveWorkspaceScopeRoot(root, {
13653
+ flowId,
13654
+ flowSource,
13655
+ archived: url.searchParams.get("archived") === "1",
13656
+ }, userCtx);
13657
+ if (scoped.error) {
13658
+ json(res, scoped.status || 400, { error: scoped.error });
13659
+ return;
13660
+ }
13172
13661
  json(res, 200, {
13173
- schedules: listWorkspaceScheduleStatusesForFlow(userCtx, flowSource, flowId),
13662
+ schedules: listWorkspaceScheduleStatusesForFlow(
13663
+ workspaceScopedUserContext(scoped, userCtx),
13664
+ flowSource,
13665
+ flowId,
13666
+ ),
13174
13667
  });
13175
13668
  } catch (e) {
13176
13669
  json(res, 500, { error: (e && e.message) || String(e) });
@@ -13191,12 +13684,17 @@ export function startUiServer({
13191
13684
  flowId: payload.flowId || "",
13192
13685
  flowSource: payload.flowSource || "user",
13193
13686
  workspaceId: payload.workspaceId || "",
13687
+ adminOwnerId: payload.adminOwnerId || "",
13194
13688
  archived: payload.archived === true || payload.flowArchived === true,
13195
13689
  }, userCtx);
13196
13690
  if (scoped.error) {
13197
13691
  json(res, 400, { error: scoped.error });
13198
13692
  return;
13199
13693
  }
13694
+ if (scoped.collaborationAccess?.runnable === false) {
13695
+ json(res, 403, { error: "Workspace run permission denied" });
13696
+ return;
13697
+ }
13200
13698
  const flowId = String(payload.flowId || "").trim();
13201
13699
  if (!flowId) {
13202
13700
  json(res, 400, { error: "Missing flowId" });
@@ -13239,6 +13737,7 @@ export function startUiServer({
13239
13737
  flowId: payload.flowId || "",
13240
13738
  flowSource: payload.flowSource || "user",
13241
13739
  workspaceId: payload.workspaceId || "",
13740
+ adminOwnerId: payload.adminOwnerId || "",
13242
13741
  archived: payload.archived === true || payload.flowArchived === true,
13243
13742
  }, userCtx);
13244
13743
  if (scoped.error) {
@@ -13303,6 +13802,7 @@ export function startUiServer({
13303
13802
  flowId: payload.flowId || "",
13304
13803
  flowSource: payload.flowSource || "user",
13305
13804
  workspaceId: payload.workspaceId || "",
13805
+ adminOwnerId: payload.adminOwnerId || "",
13306
13806
  archived: payload.archived === true || payload.flowArchived === true,
13307
13807
  }, userCtx);
13308
13808
  if (scoped.error) {
@@ -13586,9 +14086,10 @@ export function startUiServer({
13586
14086
  json(res, scoped.status || 400, { error: scoped.error });
13587
14087
  return;
13588
14088
  }
14089
+ const scopedUserCtx = workspaceScopedUserContext(scoped, userCtx);
13589
14090
  json(res, 200, {
13590
14091
  runs: listWorkspaceRunLogs({
13591
- userId: flowSource === "workspace" ? "" : userCtx.userId || "",
14092
+ userId: flowSource === "workspace" ? "" : scopedUserCtx.userId || "",
13592
14093
  flowId,
13593
14094
  flowSource,
13594
14095
  scheduleNodeId,
@@ -13620,8 +14121,9 @@ export function startUiServer({
13620
14121
  json(res, scoped.status || 400, { error: scoped.error });
13621
14122
  return;
13622
14123
  }
14124
+ const scopedUserCtx = workspaceScopedUserContext(scoped, userCtx);
13623
14125
  const run = listWorkspaceRunLogs({
13624
- userId: flowSource === "workspace" ? "" : userCtx.userId || "",
14126
+ userId: flowSource === "workspace" ? "" : scopedUserCtx.userId || "",
13625
14127
  flowId,
13626
14128
  flowSource,
13627
14129
  limit: 200,
@@ -13657,7 +14159,7 @@ export function startUiServer({
13657
14159
  json(res, scoped.status || 400, { error: scoped.error });
13658
14160
  return;
13659
14161
  }
13660
- const scopeKey = workspaceRunKey(userCtx, flowSource, flowId);
14162
+ const scopeKey = workspaceRunKey(workspaceScopedUserContext(scoped, userCtx), flowSource, flowId);
13661
14163
  const entries = workspaceActiveRunsForScope(scopeKey).map(([, entry]) => entry);
13662
14164
  const entry = entries[0] || null;
13663
14165
  json(res, 200, {
@@ -13698,6 +14200,7 @@ export function startUiServer({
13698
14200
  const scoped = resolveWorkspaceScopeRoot(root, {
13699
14201
  flowId,
13700
14202
  flowSource,
14203
+ adminOwnerId: payload.adminOwnerId || "",
13701
14204
  archived: payload.archived === true || payload.flowArchived === true,
13702
14205
  }, userCtx);
13703
14206
  if (scoped.error) {
@@ -13842,6 +14345,7 @@ export function startUiServer({
13842
14345
  const scoped = resolveWorkspaceScopeRoot(root, {
13843
14346
  flowId: payload.flowId || "",
13844
14347
  flowSource: payload.flowSource || "user",
14348
+ adminOwnerId: payload.adminOwnerId || "",
13845
14349
  archived: payload.archived === true || payload.flowArchived === true,
13846
14350
  }, userCtx);
13847
14351
  if (scoped.error) {
@@ -13903,6 +14407,7 @@ export function startUiServer({
13903
14407
  const scoped = resolveWorkspaceScopeRoot(root, {
13904
14408
  flowId: payload.flowId || "",
13905
14409
  flowSource: payload.flowSource || "user",
14410
+ adminOwnerId: payload.adminOwnerId || "",
13906
14411
  archived: payload.archived === true || payload.flowArchived === true,
13907
14412
  }, userCtx);
13908
14413
  if (scoped.error) {
@@ -13977,6 +14482,7 @@ export function startUiServer({
13977
14482
  const scoped = resolveWorkspaceScopeRoot(root, {
13978
14483
  flowId: parsed.fields.flowId || "",
13979
14484
  flowSource: parsed.fields.flowSource || "user",
14485
+ adminOwnerId: parsed.fields.adminOwnerId || "",
13980
14486
  archived: parsed.fields.archived === "1" || parsed.fields.archived === "true" || parsed.fields.flowArchived === "true",
13981
14487
  }, userCtx);
13982
14488
  if (scoped.error) {
@@ -14022,6 +14528,7 @@ export function startUiServer({
14022
14528
  const scoped = resolveWorkspaceScopeRoot(root, {
14023
14529
  flowId: payload.flowId || "",
14024
14530
  flowSource: payload.flowSource || "user",
14531
+ adminOwnerId: payload.adminOwnerId || "",
14025
14532
  archived: payload.archived === true || payload.flowArchived === true,
14026
14533
  }, userCtx);
14027
14534
  if (scoped.error) {
@@ -14062,6 +14569,7 @@ export function startUiServer({
14062
14569
  const scoped = resolveWorkspaceScopeRoot(root, {
14063
14570
  flowId: payload.flowId || "",
14064
14571
  flowSource: payload.flowSource || "user",
14572
+ adminOwnerId: payload.adminOwnerId || "",
14065
14573
  archived: payload.archived === true || payload.flowArchived === true,
14066
14574
  }, userCtx);
14067
14575
  if (scoped.error) {
@@ -14112,6 +14620,7 @@ export function startUiServer({
14112
14620
  const scoped = resolveWorkspaceScopeRoot(root, {
14113
14621
  flowId: req.method === "POST" ? (payload.flowId || "") : (url.searchParams.get("flowId") || ""),
14114
14622
  flowSource: req.method === "POST" ? (payload.flowSource || "user") : (url.searchParams.get("flowSource") || "user"),
14623
+ adminOwnerId: req.method === "POST" ? (payload.adminOwnerId || "") : "",
14115
14624
  archived: req.method === "POST"
14116
14625
  ? (payload.archived === true || payload.flowArchived === true)
14117
14626
  : (url.searchParams.get("archived") === "1" || url.searchParams.get("flowArchived") === "1"),
@@ -14153,6 +14662,7 @@ export function startUiServer({
14153
14662
  const scoped = resolveWorkspaceScopeRoot(root, {
14154
14663
  flowId: payload.flowId || "",
14155
14664
  flowSource: payload.flowSource || "user",
14665
+ adminOwnerId: payload.adminOwnerId || "",
14156
14666
  archived: payload.archived === true || payload.flowArchived === true,
14157
14667
  }, userCtx);
14158
14668
  if (scoped.error) {
@@ -14243,6 +14753,7 @@ export function startUiServer({
14243
14753
  const scoped = resolveWorkspaceScopeRoot(root, {
14244
14754
  flowId: payload.flowId || "",
14245
14755
  flowSource: payload.flowSource || "user",
14756
+ adminOwnerId: payload.adminOwnerId || "",
14246
14757
  archived: payload.archived === true || payload.flowArchived === true,
14247
14758
  }, userCtx);
14248
14759
  if (scoped.error) {
@@ -14886,9 +15397,18 @@ export function startUiServer({
14886
15397
  }
14887
15398
  const nodesArchived = url.searchParams.get("archived") === "1";
14888
15399
  try {
15400
+ const requestedContext = adminWorkspaceRequestedUserContext(userCtx);
15401
+ if (requestedContext.error) {
15402
+ json(res, requestedContext.status || 403, { error: requestedContext.error });
15403
+ return;
15404
+ }
14889
15405
  const { setLanguage } = await import("./i18n.mjs");
14890
15406
  setLanguage(lang);
14891
- json(res, 200, listNodesJson(root, flowId || "", flowId ? flowSource : "", { archived: nodesArchived, ...userCtx, marketplaceScope }));
15407
+ json(res, 200, listNodesJson(root, flowId || "", flowId ? flowSource : "", {
15408
+ archived: nodesArchived,
15409
+ ...requestedContext.userCtx,
15410
+ marketplaceScope,
15411
+ }));
14892
15412
  } catch (e) {
14893
15413
  json(res, 500, { error: (e && e.message) || String(e) });
14894
15414
  }
@@ -14909,7 +15429,15 @@ export function startUiServer({
14909
15429
  }
14910
15430
  const archived = url.searchParams.get("archived") === "1";
14911
15431
  try {
14912
- const detail = readNodeDetailJson(root, nodeId, flowId, flowId ? (flowSource || "user") : "", { archived, ...userCtx });
15432
+ const requestedContext = adminWorkspaceRequestedUserContext(userCtx);
15433
+ if (requestedContext.error) {
15434
+ json(res, requestedContext.status || 403, { error: requestedContext.error });
15435
+ return;
15436
+ }
15437
+ const detail = readNodeDetailJson(root, nodeId, flowId, flowId ? (flowSource || "user") : "", {
15438
+ archived,
15439
+ ...requestedContext.userCtx,
15440
+ });
14913
15441
  if (detail.error) {
14914
15442
  json(res, 404, { error: detail.error });
14915
15443
  return;
@@ -15156,7 +15684,12 @@ export function startUiServer({
15156
15684
  json(res, collaborationDenied.status, { error: collaborationDenied.error });
15157
15685
  return;
15158
15686
  }
15159
- const result = readFlowJson(root, flowId, flowSource, { archived: flowArchived, ...userCtx });
15687
+ const requestedContext = adminWorkspaceRequestedUserContext(userCtx);
15688
+ if (requestedContext.error) {
15689
+ json(res, requestedContext.status || 403, { error: requestedContext.error });
15690
+ return;
15691
+ }
15692
+ const result = readFlowJson(root, flowId, flowSource, { archived: flowArchived, ...requestedContext.userCtx });
15160
15693
  if (result.error) {
15161
15694
  json(res, 404, result);
15162
15695
  return;
@@ -16669,24 +17202,26 @@ finishedAt: "${new Date().toISOString()}"
16669
17202
  res.end(data);
16670
17203
  });
16671
17204
 
16672
- const workspaceScheduleTimer = setInterval(() => {
16673
- try {
16674
- pollWorkspaceSchedules(root);
16675
- } catch (e) {
16676
- log.debug(`[workspace-scheduler] poll failed: ${(e && e.message) || String(e)}`);
16677
- }
16678
- }, WORKSPACE_SCHEDULE_POLL_MS);
16679
- try {
16680
- workspaceScheduleTimer.unref?.();
16681
- } catch (_) {}
16682
- server.on("close", () => clearInterval(workspaceScheduleTimer));
16683
- setTimeout(() => {
17205
+ if (enableWorkspaceScheduler) {
17206
+ const workspaceScheduleTimer = setInterval(() => {
17207
+ try {
17208
+ pollWorkspaceSchedules(root);
17209
+ } catch (e) {
17210
+ log.debug(`[workspace-scheduler] poll failed: ${(e && e.message) || String(e)}`);
17211
+ }
17212
+ }, WORKSPACE_SCHEDULE_POLL_MS);
16684
17213
  try {
16685
- pollWorkspaceSchedules(root);
16686
- } catch (e) {
16687
- log.debug(`[workspace-scheduler] initial poll failed: ${(e && e.message) || String(e)}`);
16688
- }
16689
- }, 1000).unref?.();
17214
+ workspaceScheduleTimer.unref?.();
17215
+ } catch (_) {}
17216
+ server.on("close", () => clearInterval(workspaceScheduleTimer));
17217
+ setTimeout(() => {
17218
+ try {
17219
+ pollWorkspaceSchedules(root);
17220
+ } catch (e) {
17221
+ log.debug(`[workspace-scheduler] initial poll failed: ${(e && e.message) || String(e)}`);
17222
+ }
17223
+ }, 1000).unref?.();
17224
+ }
16690
17225
 
16691
17226
  return new Promise((resolve, reject) => {
16692
17227
  server.once("error", reject);