@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.
- package/bin/lib/ui-server.mjs +593 -58
- package/builtin/web-ui/dist/assets/index-BKLuP3ZS.css +1 -0
- package/builtin/web-ui/dist/assets/{index-By6mIwoZ.js → index-RzWv0xOo.js} +81 -80
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-CuRtjhm3.css +0 -1
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
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 ? {
|
|
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
|
-
|
|
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, {
|
|
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(
|
|
11971
|
-
|
|
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:
|
|
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: [{
|
|
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, {
|
|
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 =
|
|
12086
|
-
if (!
|
|
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
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" ? "" :
|
|
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" ? "" :
|
|
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 : "", {
|
|
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
|
|
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
|
|
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
|
-
|
|
16673
|
-
|
|
16674
|
-
|
|
16675
|
-
|
|
16676
|
-
|
|
16677
|
-
|
|
16678
|
-
|
|
16679
|
-
|
|
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
|
-
|
|
16686
|
-
} catch (
|
|
16687
|
-
|
|
16688
|
-
|
|
16689
|
-
|
|
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);
|