@fieldwangai/agentflow 0.1.138 → 0.1.142
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/catalog-flows.mjs +17 -3
- package/bin/lib/composer-node-schema.mjs +4 -0
- package/bin/lib/i18n.mjs +2 -2
- package/bin/lib/jenkins.mjs +380 -0
- package/bin/lib/locales/en.json +22 -0
- package/bin/lib/locales/zh.json +22 -0
- package/bin/lib/paths.mjs +1 -0
- package/bin/lib/prd-workflow-collaboration.mjs +93 -1
- package/bin/lib/recent-runs.mjs +16 -0
- package/bin/lib/run-node-statuses-from-disk.mjs +41 -3
- package/bin/lib/scheduler.mjs +59 -25
- package/bin/lib/ui-server.mjs +996 -73
- package/bin/lib/workflow-report.mjs +127 -1
- package/bin/pipeline/pre-process-node.mjs +122 -11
- package/bin/pipeline/run-log.mjs +2 -2
- package/bin/pipeline/write-result.mjs +4 -4
- package/builtin/nodes/tool_jenkins_build.md +64 -0
- package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-B3thaqH2.js → WorkflowAssistantThread-DGaXikJ9.js} +1 -1
- package/builtin/web-ui/dist/assets/index-CtG65J3e.css +1 -0
- package/builtin/web-ui/dist/assets/index-pLsH0Uyj.js +835 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/agentflow-workflow-report/SKILL.md +9 -1
- package/skills/agentflow-workflow-report/references/protocol.md +137 -12
- package/builtin/web-ui/dist/assets/index-COX1zMwq.css +0 -1
- package/builtin/web-ui/dist/assets/index-YS4XOpXF.js +0 -590
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -83,7 +83,7 @@ import {
|
|
|
83
83
|
} from "./composer-log.mjs";
|
|
84
84
|
import { runNodeScript } from "./pipeline-scripts.mjs";
|
|
85
85
|
import { computeNextRunAt, readFlowSchedule, writeFlowSchedule } from "./schedule-config.mjs";
|
|
86
|
-
import { listScheduleStatuses } from "./scheduler.mjs";
|
|
86
|
+
import { cancelScheduledRun, listScheduleStatuses } from "./scheduler.mjs";
|
|
87
87
|
import {
|
|
88
88
|
mergeWorkspaceGraphs,
|
|
89
89
|
workspaceDesignRevision,
|
|
@@ -153,6 +153,7 @@ import {
|
|
|
153
153
|
} from "./workspace-collaboration.mjs";
|
|
154
154
|
import {
|
|
155
155
|
addPrdWorkflowCollaborationMember,
|
|
156
|
+
bindPrdWorkflowProject,
|
|
156
157
|
ensurePrdWorkflowCollaboration,
|
|
157
158
|
getPrdWorkflowCollaborationById,
|
|
158
159
|
getPrdWorkflowCollaborationByShareToken,
|
|
@@ -161,12 +162,14 @@ import {
|
|
|
161
162
|
ensurePrdWorkflowShareLink,
|
|
162
163
|
listPrdWorkflowCollaborationsForUser,
|
|
163
164
|
listPrdWorkflowCollaborationsForTeam,
|
|
165
|
+
listPrdWorkflowProjectBindings,
|
|
164
166
|
prdWorkflowCollaborationAccess,
|
|
165
167
|
prdWorkflowCollaborationSummary,
|
|
166
168
|
removePrdWorkflowCollaborationMember,
|
|
167
169
|
revokePrdWorkflowShareLink,
|
|
168
170
|
setPrdWorkflowKnowledgeBindings,
|
|
169
171
|
syncPrdWorkflowAuthority,
|
|
172
|
+
unbindPrdWorkflowProject,
|
|
170
173
|
} from "./prd-workflow-collaboration.mjs";
|
|
171
174
|
import {
|
|
172
175
|
createTeam,
|
|
@@ -185,6 +188,8 @@ import {
|
|
|
185
188
|
mergeWorkflowArtifactLists,
|
|
186
189
|
mergeWorkflowArtifacts,
|
|
187
190
|
mergeWorkflowGlobalState,
|
|
191
|
+
isSafeWorkflowUrl,
|
|
192
|
+
normalizeWorkflowChecklistItemStatus,
|
|
188
193
|
normalizeWorkflowReference,
|
|
189
194
|
normalizeWorkflowReport,
|
|
190
195
|
removeWorkflowGlobalStatePath,
|
|
@@ -3592,6 +3597,95 @@ function prdWorkflowShareLinkSummary(record, shareToken, publicBaseUrl, userId =
|
|
|
3592
3597
|
};
|
|
3593
3598
|
}
|
|
3594
3599
|
|
|
3600
|
+
function listAccessibleProjectFlows(root, userCtx = {}) {
|
|
3601
|
+
const flows = listFlowsJson(root, { ...userCtx, includeWorkspaceFlows: true })
|
|
3602
|
+
.filter((flow) => (
|
|
3603
|
+
!workspaceFlowCollaborationGuard(
|
|
3604
|
+
flow.id,
|
|
3605
|
+
flow.source || "user",
|
|
3606
|
+
flow.archived === true,
|
|
3607
|
+
userCtx,
|
|
3608
|
+
"read",
|
|
3609
|
+
)
|
|
3610
|
+
))
|
|
3611
|
+
.map((flow) => {
|
|
3612
|
+
const source = flow.source || "user";
|
|
3613
|
+
const collaboration = source === "workspace"
|
|
3614
|
+
? getWorkspaceCollaborationByFlow(flow.id, flow.archived === true)
|
|
3615
|
+
: getWorkspaceCollaborationForProject({
|
|
3616
|
+
flowId: flow.id,
|
|
3617
|
+
flowSource: source,
|
|
3618
|
+
archived: flow.archived === true,
|
|
3619
|
+
ownerId: userCtx.userId,
|
|
3620
|
+
});
|
|
3621
|
+
return collaboration
|
|
3622
|
+
? { ...flow, collaboration: workspaceCollaborationSummaryWithUsers(collaboration, userCtx.userId) }
|
|
3623
|
+
: flow;
|
|
3624
|
+
});
|
|
3625
|
+
const existingCollaborationIds = new Set(flows.map((flow) => flow.collaboration?.id).filter(Boolean));
|
|
3626
|
+
for (const record of listWorkspaceCollaborationsForUser(userCtx.userId)) {
|
|
3627
|
+
const source = record.projectSource || record.flowSource || "workspace";
|
|
3628
|
+
if (source !== "user" || record.ownerId === userCtx.userId) continue;
|
|
3629
|
+
if (existingCollaborationIds.has(record.id)) continue;
|
|
3630
|
+
const ownerFlow = listFlowsJson(root, { userId: record.ownerId })
|
|
3631
|
+
.find((flow) => (
|
|
3632
|
+
flow.id === record.flowId
|
|
3633
|
+
&& (flow.source || "user") === "user"
|
|
3634
|
+
&& Boolean(flow.archived) === Boolean(record.archived)
|
|
3635
|
+
));
|
|
3636
|
+
if (!ownerFlow) continue;
|
|
3637
|
+
flows.push({
|
|
3638
|
+
...ownerFlow,
|
|
3639
|
+
collaboration: workspaceCollaborationSummaryWithUsers(record, userCtx.userId),
|
|
3640
|
+
});
|
|
3641
|
+
existingCollaborationIds.add(record.id);
|
|
3642
|
+
}
|
|
3643
|
+
return flows;
|
|
3644
|
+
}
|
|
3645
|
+
|
|
3646
|
+
function workflowProjectBindingRows(bindings = [], accessibleProjects = [], userCtx = {}) {
|
|
3647
|
+
return (Array.isArray(bindings) ? bindings : []).flatMap((binding) => {
|
|
3648
|
+
const workspaceId = String(binding?.workspaceId || "").trim();
|
|
3649
|
+
if (!workspaceId) return [];
|
|
3650
|
+
const project = accessibleProjects.find((flow) => String(flow?.collaboration?.id || "") === workspaceId);
|
|
3651
|
+
if (!project) return [];
|
|
3652
|
+
const role = String(project.collaboration?.role || "");
|
|
3653
|
+
return [{
|
|
3654
|
+
workspaceId,
|
|
3655
|
+
flowId: String(project.id || binding.flowId || ""),
|
|
3656
|
+
flowSource: String(project.source || binding.flowSource || "user"),
|
|
3657
|
+
archived: project.archived === true,
|
|
3658
|
+
label: String(project.id || binding.flowId || "Project"),
|
|
3659
|
+
description: String(project.description || ""),
|
|
3660
|
+
role: role || ((project.source || "user") === "user" ? "owner" : "editor"),
|
|
3661
|
+
canManage: role === "owner" || role === "editor" || (!role && (project.source || "user") === "user"),
|
|
3662
|
+
boundBy: String(binding.boundBy || ""),
|
|
3663
|
+
boundAt: String(binding.boundAt || ""),
|
|
3664
|
+
}];
|
|
3665
|
+
});
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3668
|
+
function availableWorkflowBindingProjects(accessibleProjects = [], bindings = []) {
|
|
3669
|
+
const bound = new Set((Array.isArray(bindings) ? bindings : []).map((item) => String(item?.workspaceId || "")).filter(Boolean));
|
|
3670
|
+
return accessibleProjects
|
|
3671
|
+
.filter((project) => {
|
|
3672
|
+
const source = String(project?.source || "user");
|
|
3673
|
+
const role = String(project?.collaboration?.role || "");
|
|
3674
|
+
const workspaceId = String(project?.collaboration?.id || "");
|
|
3675
|
+
return !project?.archived
|
|
3676
|
+
&& (source === "user" || source === "workspace")
|
|
3677
|
+
&& !bound.has(workspaceId)
|
|
3678
|
+
&& (!role || role === "owner" || role === "editor");
|
|
3679
|
+
})
|
|
3680
|
+
.map((project) => ({
|
|
3681
|
+
flowId: String(project.id || ""),
|
|
3682
|
+
flowSource: String(project.source || "user"),
|
|
3683
|
+
workspaceId: String(project.collaboration?.id || ""),
|
|
3684
|
+
label: String(project.id || "Project"),
|
|
3685
|
+
description: String(project.description || ""),
|
|
3686
|
+
}));
|
|
3687
|
+
}
|
|
3688
|
+
|
|
3595
3689
|
function prdWorkflowDashboardActions(snapshot = {}) {
|
|
3596
3690
|
const rows = new Map();
|
|
3597
3691
|
for (const field of ["actions", "workflowActions", "workflow_actions", "timeline", "history"]) {
|
|
@@ -3639,7 +3733,7 @@ function prdWorkflowDashboardTimestamp(item = {}) {
|
|
|
3639
3733
|
return 0;
|
|
3640
3734
|
}
|
|
3641
3735
|
|
|
3642
|
-
function prdWorkflowDashboardSummary(record, snapshot = {}, userCtx = {}) {
|
|
3736
|
+
function prdWorkflowDashboardSummary(record, snapshot = {}, userCtx = {}, projectBindings = []) {
|
|
3643
3737
|
const tapdId = String(record?.tapdId || snapshot?.tapdId || snapshot?.tapd_id || "").trim();
|
|
3644
3738
|
const collaboration = prdWorkflowCollaborationSummaryWithUsers(record, userCtx?.userId) || {};
|
|
3645
3739
|
const actions = prdWorkflowDashboardActions(snapshot);
|
|
@@ -3669,11 +3763,11 @@ function prdWorkflowDashboardSummary(record, snapshot = {}, userCtx = {}) {
|
|
|
3669
3763
|
? snapshot.overall.requirement
|
|
3670
3764
|
: {};
|
|
3671
3765
|
const title = String(
|
|
3672
|
-
|
|
3766
|
+
snapshot?.globalState?.title
|
|
3767
|
+
|| requirement.title
|
|
3673
3768
|
|| requirement.name
|
|
3674
3769
|
|| snapshot?.prd?.title
|
|
3675
3770
|
|| snapshot?.raw?.prd?.title
|
|
3676
|
-
|| snapshot?.title
|
|
3677
3771
|
|| "",
|
|
3678
3772
|
).trim();
|
|
3679
3773
|
const timeline = Array.isArray(snapshot?.projections?.timeline)
|
|
@@ -3685,6 +3779,8 @@ function prdWorkflowDashboardSummary(record, snapshot = {}, userCtx = {}) {
|
|
|
3685
3779
|
id: String(entry.id || ""),
|
|
3686
3780
|
title: String(entry.title || entry.label || entry.id || ""),
|
|
3687
3781
|
date: String(entry.date || ""),
|
|
3782
|
+
startDate: String(entry.startDate || entry.start_date || entry.start || ""),
|
|
3783
|
+
endDate: String(entry.endDate || entry.end_date || entry.end || entry.date || ""),
|
|
3688
3784
|
source: String(entry.source || ""),
|
|
3689
3785
|
dimensions: entry.dimensions && typeof entry.dimensions === "object" && !Array.isArray(entry.dimensions)
|
|
3690
3786
|
? entry.dimensions
|
|
@@ -3727,10 +3823,73 @@ function prdWorkflowDashboardSummary(record, snapshot = {}, userCtx = {}) {
|
|
|
3727
3823
|
teamName: String(getTeamById(collaboration.teamId)?.name || ""),
|
|
3728
3824
|
shareActive: collaboration.shareActive === true,
|
|
3729
3825
|
updatedAt: updatedAtTimestamp ? new Date(updatedAtTimestamp).toISOString() : String(record?.updatedAt || ""),
|
|
3826
|
+
projectBindings,
|
|
3730
3827
|
};
|
|
3731
3828
|
}
|
|
3732
3829
|
|
|
3733
|
-
function
|
|
3830
|
+
function prdWorkflowDashboardTimelineDimensionValues(value) {
|
|
3831
|
+
return (Array.isArray(value) ? value : [value])
|
|
3832
|
+
.flatMap((item) => Array.isArray(item) ? item : [item])
|
|
3833
|
+
.map((item) => String(item ?? "").trim())
|
|
3834
|
+
.filter(Boolean);
|
|
3835
|
+
}
|
|
3836
|
+
|
|
3837
|
+
function prdWorkflowDashboardTimelineIdentity(entry = {}) {
|
|
3838
|
+
const id = String(entry?.id || "").trim();
|
|
3839
|
+
const source = String(entry?.source || "").trim().toLowerCase();
|
|
3840
|
+
const kind = String(entry?.kind || "").trim().toLowerCase();
|
|
3841
|
+
const dimensions = entry?.dimensions && typeof entry.dimensions === "object" && !Array.isArray(entry.dimensions)
|
|
3842
|
+
? entry.dimensions
|
|
3843
|
+
: {};
|
|
3844
|
+
const legacyPlatformIdentity = id.match(/^(android|ios|all)[:_-](.+)$/i);
|
|
3845
|
+
const declaredPlatforms = [
|
|
3846
|
+
...prdWorkflowDashboardTimelineDimensionValues(dimensions.platform),
|
|
3847
|
+
...prdWorkflowDashboardTimelineDimensionValues(dimensions.platforms),
|
|
3848
|
+
].map((value) => value.toLowerCase());
|
|
3849
|
+
if (
|
|
3850
|
+
source === "prd-flow"
|
|
3851
|
+
&& ["version", "iteration"].includes(kind)
|
|
3852
|
+
&& legacyPlatformIdentity
|
|
3853
|
+
&& declaredPlatforms.includes(legacyPlatformIdentity[1].toLowerCase())
|
|
3854
|
+
) {
|
|
3855
|
+
return legacyPlatformIdentity[2].trim().toLowerCase();
|
|
3856
|
+
}
|
|
3857
|
+
return id.toLowerCase();
|
|
3858
|
+
}
|
|
3859
|
+
|
|
3860
|
+
function prdWorkflowDashboardTimelineGroupKey(entry = {}) {
|
|
3861
|
+
const source = String(entry?.source || "").trim().toLowerCase();
|
|
3862
|
+
const kind = String(entry?.kind || "").trim().toLowerCase();
|
|
3863
|
+
const identity = prdWorkflowDashboardTimelineIdentity(entry);
|
|
3864
|
+
const dimensions = entry?.dimensions && typeof entry.dimensions === "object" && !Array.isArray(entry.dimensions)
|
|
3865
|
+
? entry.dimensions
|
|
3866
|
+
: {};
|
|
3867
|
+
const nonPlatformDimensions = Object.entries(dimensions)
|
|
3868
|
+
.filter(([key]) => !["platform", "platforms", "client", "clients", "os"].includes(String(key).trim().toLowerCase()))
|
|
3869
|
+
.map(([key, value]) => [
|
|
3870
|
+
String(key).trim().toLowerCase(),
|
|
3871
|
+
prdWorkflowDashboardTimelineDimensionValues(value).map((item) => item.toLowerCase()).sort(),
|
|
3872
|
+
])
|
|
3873
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
3874
|
+
if (identity) return JSON.stringify([source, kind, identity, nonPlatformDimensions]);
|
|
3875
|
+
return String(entry?.key || [entry?.source, entry?.kind, entry?.id].filter(Boolean).join(":"));
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3878
|
+
function prdWorkflowDashboardMergeTimelineDimensions(current = {}, incoming = {}) {
|
|
3879
|
+
const out = {};
|
|
3880
|
+
for (const key of new Set([...Object.keys(current || {}), ...Object.keys(incoming || {})])) {
|
|
3881
|
+
const values = [
|
|
3882
|
+
...prdWorkflowDashboardTimelineDimensionValues(current?.[key]),
|
|
3883
|
+
...prdWorkflowDashboardTimelineDimensionValues(incoming?.[key]),
|
|
3884
|
+
];
|
|
3885
|
+
const unique = Array.from(new Map(values.map((value) => [value.toLowerCase(), value])).values());
|
|
3886
|
+
if (unique.length === 1) out[key] = unique[0];
|
|
3887
|
+
else if (unique.length > 1) out[key] = unique;
|
|
3888
|
+
}
|
|
3889
|
+
return out;
|
|
3890
|
+
}
|
|
3891
|
+
|
|
3892
|
+
export function prdWorkflowDashboardTimeline(workflows = []) {
|
|
3734
3893
|
const buckets = new Map();
|
|
3735
3894
|
const assignedWorkflowIds = new Set();
|
|
3736
3895
|
const rows = [...(Array.isArray(workflows) ? workflows : [])].sort((left, right) => {
|
|
@@ -3744,16 +3903,19 @@ function prdWorkflowDashboardTimeline(workflows = []) {
|
|
|
3744
3903
|
const workflowId = String(workflow?.id || workflow?.tapdId || "");
|
|
3745
3904
|
const seen = new Set();
|
|
3746
3905
|
for (const entry of Array.isArray(workflow?.timeline) ? workflow.timeline : []) {
|
|
3747
|
-
const
|
|
3748
|
-
|
|
3749
|
-
|
|
3906
|
+
const memberKey = String(entry?.key || [entry?.source, entry?.kind, entry?.id].filter(Boolean).join(":"));
|
|
3907
|
+
const identity = prdWorkflowDashboardTimelineIdentity(entry);
|
|
3908
|
+
const groupKey = prdWorkflowDashboardTimelineGroupKey(entry);
|
|
3909
|
+
if (!memberKey || !groupKey) continue;
|
|
3750
3910
|
assignedWorkflowIds.add(workflowId);
|
|
3751
|
-
const current = buckets.get(
|
|
3752
|
-
key,
|
|
3911
|
+
const current = buckets.get(groupKey) || {
|
|
3912
|
+
key: memberKey,
|
|
3753
3913
|
kind: String(entry.kind || ""),
|
|
3754
|
-
id: String(entry.id || ""),
|
|
3914
|
+
id: identity || String(entry.id || ""),
|
|
3755
3915
|
title: String(entry.title || entry.id || ""),
|
|
3756
3916
|
date: String(entry.date || ""),
|
|
3917
|
+
startDate: String(entry.startDate || ""),
|
|
3918
|
+
endDate: String(entry.endDate || entry.date || ""),
|
|
3757
3919
|
source: String(entry.source || ""),
|
|
3758
3920
|
dimensions: entry.dimensions && typeof entry.dimensions === "object" && !Array.isArray(entry.dimensions)
|
|
3759
3921
|
? entry.dimensions
|
|
@@ -3763,21 +3925,27 @@ function prdWorkflowDashboardTimeline(workflows = []) {
|
|
|
3763
3925
|
completedCount: 0,
|
|
3764
3926
|
blockedCount: 0,
|
|
3765
3927
|
workflowIds: [],
|
|
3928
|
+
memberKeys: [],
|
|
3766
3929
|
};
|
|
3767
3930
|
current.kind = String(entry.kind || current.kind);
|
|
3768
|
-
current.id = String(entry.id || current.id);
|
|
3769
3931
|
current.title = String(entry.title || current.title);
|
|
3770
3932
|
current.date = String(entry.date || current.date);
|
|
3933
|
+
current.startDate = String(entry.startDate || current.startDate);
|
|
3934
|
+
current.endDate = String(entry.endDate || entry.date || current.endDate);
|
|
3771
3935
|
current.source = String(entry.source || current.source);
|
|
3772
|
-
current.dimensions =
|
|
3773
|
-
? entry.dimensions
|
|
3774
|
-
: current.dimensions;
|
|
3936
|
+
current.dimensions = prdWorkflowDashboardMergeTimelineDimensions(current.dimensions, entry.dimensions);
|
|
3775
3937
|
current.order = Number.isFinite(Number(entry.order)) ? Number(entry.order) : current.order;
|
|
3776
|
-
current.
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3938
|
+
if (!current.memberKeys.includes(memberKey)) current.memberKeys.push(memberKey);
|
|
3939
|
+
current.memberKeys.sort();
|
|
3940
|
+
current.key = current.memberKeys[0] || memberKey;
|
|
3941
|
+
if (!seen.has(groupKey)) {
|
|
3942
|
+
seen.add(groupKey);
|
|
3943
|
+
current.workflowCount += 1;
|
|
3944
|
+
if (workflow?.state === "completed") current.completedCount += 1;
|
|
3945
|
+
if (workflow?.state === "blocked") current.blockedCount += 1;
|
|
3946
|
+
if (!current.workflowIds.includes(workflowId)) current.workflowIds.push(workflowId);
|
|
3947
|
+
}
|
|
3948
|
+
buckets.set(groupKey, current);
|
|
3781
3949
|
}
|
|
3782
3950
|
}
|
|
3783
3951
|
const timeline = Array.from(buckets.values()).sort((left, right) => {
|
|
@@ -3796,6 +3964,112 @@ function prdWorkflowDashboardTimeline(workflows = []) {
|
|
|
3796
3964
|
};
|
|
3797
3965
|
}
|
|
3798
3966
|
|
|
3967
|
+
function prdWorkflowTimelineTimestamp(value, endOfDay = false) {
|
|
3968
|
+
const text = String(value || "").trim();
|
|
3969
|
+
if (!text) return Number.NaN;
|
|
3970
|
+
const timestamp = Date.parse(text);
|
|
3971
|
+
if (!Number.isFinite(timestamp)) return Number.NaN;
|
|
3972
|
+
return endOfDay && /^\d{4}-\d{2}-\d{2}$/.test(text)
|
|
3973
|
+
? timestamp + (24 * 60 * 60 * 1000) - 1
|
|
3974
|
+
: timestamp;
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
export function prdWorkflowDefaultTimelineKey(timeline = [], now = Date.now()) {
|
|
3978
|
+
const rows = (Array.isArray(timeline) ? timeline : [])
|
|
3979
|
+
.map((entry) => ({
|
|
3980
|
+
entry,
|
|
3981
|
+
startAt: prdWorkflowTimelineTimestamp(entry?.startDate || entry?.start),
|
|
3982
|
+
endAt: prdWorkflowTimelineTimestamp(entry?.endDate || entry?.end || entry?.date, true),
|
|
3983
|
+
}))
|
|
3984
|
+
.filter(({ entry, endAt }) => String(entry?.key || "").trim() && Number.isFinite(endAt));
|
|
3985
|
+
const active = rows
|
|
3986
|
+
.filter(({ startAt, endAt }) => Number.isFinite(startAt) && startAt <= now && now <= endAt)
|
|
3987
|
+
.sort((left, right) => left.endAt - right.endAt);
|
|
3988
|
+
if (active.length > 0) return String(active[0].entry.key);
|
|
3989
|
+
const upcoming = rows
|
|
3990
|
+
.filter(({ endAt }) => endAt >= now)
|
|
3991
|
+
.sort((left, right) => left.endAt - right.endAt);
|
|
3992
|
+
if (upcoming.length > 0) return String(upcoming[0].entry.key);
|
|
3993
|
+
const latestPast = rows.sort((left, right) => right.endAt - left.endAt)[0];
|
|
3994
|
+
return latestPast ? String(latestPast.entry.key) : "all";
|
|
3995
|
+
}
|
|
3996
|
+
|
|
3997
|
+
function prdWorkflowDashboardSearchValues(workflow = {}) {
|
|
3998
|
+
return [
|
|
3999
|
+
workflow.tapdId,
|
|
4000
|
+
workflow.title,
|
|
4001
|
+
workflow.pointer,
|
|
4002
|
+
workflow.phase,
|
|
4003
|
+
workflow.ownerUsername,
|
|
4004
|
+
workflow.latestAction?.title,
|
|
4005
|
+
...(Array.isArray(workflow.timeline) ? workflow.timeline : []).flatMap((entry) => [
|
|
4006
|
+
entry?.title,
|
|
4007
|
+
entry?.kind,
|
|
4008
|
+
entry?.date,
|
|
4009
|
+
...Object.values(entry?.dimensions || {}).flatMap((value) => Array.isArray(value) ? value : [value]),
|
|
4010
|
+
]),
|
|
4011
|
+
];
|
|
4012
|
+
}
|
|
4013
|
+
|
|
4014
|
+
export function prdWorkflowDashboardPage(workflows = [], dashboardTimeline = {}, options = {}) {
|
|
4015
|
+
const allWorkflows = Array.isArray(workflows) ? workflows : [];
|
|
4016
|
+
const timeline = Array.isArray(dashboardTimeline?.timeline) ? dashboardTimeline.timeline : [];
|
|
4017
|
+
const unassignedCount = Number(dashboardTimeline?.unassignedCount || 0);
|
|
4018
|
+
const defaultTimelineKey = prdWorkflowDefaultTimelineKey(timeline, options.now);
|
|
4019
|
+
const requestedTimelineKey = String(options.timelineKey || "").trim();
|
|
4020
|
+
const matchedTimeline = timeline.find((entry) => (
|
|
4021
|
+
entry.key === requestedTimelineKey
|
|
4022
|
+
|| (Array.isArray(entry.memberKeys) && entry.memberKeys.includes(requestedTimelineKey))
|
|
4023
|
+
));
|
|
4024
|
+
const selectedTimelineKey = requestedTimelineKey === "all"
|
|
4025
|
+
? "all"
|
|
4026
|
+
: requestedTimelineKey === "unassigned" && unassignedCount > 0
|
|
4027
|
+
? "unassigned"
|
|
4028
|
+
: matchedTimeline?.key || defaultTimelineKey;
|
|
4029
|
+
const selectedTimeline = timeline.find((entry) => entry.key === selectedTimelineKey) || null;
|
|
4030
|
+
const selectedWorkflowIds = new Set(
|
|
4031
|
+
Array.isArray(selectedTimeline?.workflowIds)
|
|
4032
|
+
? selectedTimeline.workflowIds.map((value) => String(value))
|
|
4033
|
+
: [],
|
|
4034
|
+
);
|
|
4035
|
+
const query = String(options.query || "").trim().toLowerCase();
|
|
4036
|
+
const scope = ["owned", "collaborating"].includes(options.scope) ? options.scope : "all";
|
|
4037
|
+
const state = ["active", "completed", "blocked"].includes(options.state) ? options.state : "all";
|
|
4038
|
+
const filtered = allWorkflows.filter((workflow) => {
|
|
4039
|
+
const workflowId = String(workflow?.id || workflow?.tapdId || "");
|
|
4040
|
+
if (selectedTimelineKey === "unassigned" && (workflow.timeline || []).length > 0) return false;
|
|
4041
|
+
if (selectedTimeline && !selectedWorkflowIds.has(workflowId)) return false;
|
|
4042
|
+
if (scope === "owned" && workflow.role !== "owner") return false;
|
|
4043
|
+
if (scope === "collaborating" && workflow.role === "owner") return false;
|
|
4044
|
+
if (state !== "all" && workflow.state !== state) return false;
|
|
4045
|
+
if (query && !prdWorkflowDashboardSearchValues(workflow).some(
|
|
4046
|
+
(value) => String(value || "").toLowerCase().includes(query),
|
|
4047
|
+
)) return false;
|
|
4048
|
+
return true;
|
|
4049
|
+
});
|
|
4050
|
+
const requestedPageSize = Number.parseInt(String(options.pageSize || "20"), 10);
|
|
4051
|
+
const pageSize = [20, 50, 100].includes(requestedPageSize) ? requestedPageSize : 20;
|
|
4052
|
+
const total = filtered.length;
|
|
4053
|
+
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
|
4054
|
+
const requestedPage = Number.parseInt(String(options.page || "1"), 10);
|
|
4055
|
+
const page = Math.min(totalPages, Math.max(1, Number.isFinite(requestedPage) ? requestedPage : 1));
|
|
4056
|
+
const start = (page - 1) * pageSize;
|
|
4057
|
+
return {
|
|
4058
|
+
workflows: filtered.slice(start, start + pageSize),
|
|
4059
|
+
availableCount: allWorkflows.length,
|
|
4060
|
+
selectedTimelineKey,
|
|
4061
|
+
defaultTimelineKey,
|
|
4062
|
+
pagination: {
|
|
4063
|
+
page,
|
|
4064
|
+
pageSize,
|
|
4065
|
+
total,
|
|
4066
|
+
totalPages,
|
|
4067
|
+
hasPrevious: page > 1,
|
|
4068
|
+
hasNext: page < totalPages,
|
|
4069
|
+
},
|
|
4070
|
+
};
|
|
4071
|
+
}
|
|
4072
|
+
|
|
3799
4073
|
function workspaceConversationsPath(scopedRoot) {
|
|
3800
4074
|
return path.join(path.resolve(scopedRoot), ".workspace", "agentflow", "conversations.json");
|
|
3801
4075
|
}
|
|
@@ -8025,6 +8299,10 @@ function resolvePrdWorkflowScope(workspaceRoot, params = {}, userCtx = {}, capab
|
|
|
8025
8299
|
return { error: "Admin permission required", status: 403 };
|
|
8026
8300
|
}
|
|
8027
8301
|
const adminOwner = adminOwnerId ? adminWorkspaceOwnerSummary(adminOwnerId) : null;
|
|
8302
|
+
const adminVersionRepair = capability === "admin-version-repair";
|
|
8303
|
+
if (adminVersionRepair && userCtx.isAdmin !== true) {
|
|
8304
|
+
return { error: "Admin permission required", status: 403 };
|
|
8305
|
+
}
|
|
8028
8306
|
if (adminOwnerId && !adminOwner) {
|
|
8029
8307
|
return { error: "Workspace owner not found", status: 404 };
|
|
8030
8308
|
}
|
|
@@ -8040,19 +8318,24 @@ function resolvePrdWorkflowScope(workspaceRoot, params = {}, userCtx = {}, capab
|
|
|
8040
8318
|
? getPrdWorkflowCollaborationForUser(tapdId, userCtx?.userId)
|
|
8041
8319
|
: null;
|
|
8042
8320
|
const existingCollaboration = tapdId ? getPrdWorkflowCollaborationByTapdId(tapdId) : null;
|
|
8043
|
-
if (!adminOwner && !linkCollaboration && existingCollaboration && !memberCollaboration) {
|
|
8321
|
+
if (!adminOwner && !linkCollaboration && existingCollaboration && !memberCollaboration && !adminVersionRepair) {
|
|
8044
8322
|
return { error: "PRD Workflow collaboration permission denied", status: 403 };
|
|
8045
8323
|
}
|
|
8046
|
-
|
|
8324
|
+
if (adminVersionRepair && !existingCollaboration) {
|
|
8325
|
+
return { error: "PRD Workflow collaboration not found", status: 404 };
|
|
8326
|
+
}
|
|
8327
|
+
const collaboration = adminOwner ? null : (adminVersionRepair ? existingCollaboration : (linkCollaboration || memberCollaboration));
|
|
8047
8328
|
const access = adminOwner
|
|
8048
8329
|
? { allowed: true, writable: false, role: "admin-viewer", via: "admin-review" }
|
|
8330
|
+
: adminVersionRepair
|
|
8331
|
+
? { allowed: true, writable: true, role: "admin-version-repair", via: "admin-version-repair" }
|
|
8049
8332
|
: linkCollaboration
|
|
8050
8333
|
? { allowed: true, writable: false, role: "viewer", via: "share-link" }
|
|
8051
8334
|
: prdWorkflowCollaborationAccess(collaboration, userCtx?.userId);
|
|
8052
8335
|
if (collaboration && !access.allowed) {
|
|
8053
8336
|
return { error: "PRD Workflow collaboration permission denied", status: 403 };
|
|
8054
8337
|
}
|
|
8055
|
-
if (capability === "write" && (linkCollaboration || (collaboration && !access.writable))) {
|
|
8338
|
+
if ((capability === "write" || adminVersionRepair) && (linkCollaboration || (collaboration && !access.writable))) {
|
|
8056
8339
|
return { error: "PRD Workflow collaboration edit permission denied", status: 403 };
|
|
8057
8340
|
}
|
|
8058
8341
|
const ownerId = String(adminOwner?.userId || collaboration?.ownerId || userCtx?.userId || "").trim();
|
|
@@ -8085,6 +8368,7 @@ function resolvePrdWorkflowScope(workspaceRoot, params = {}, userCtx = {}, capab
|
|
|
8085
8368
|
shareToken,
|
|
8086
8369
|
sharedByLink: Boolean(linkCollaboration),
|
|
8087
8370
|
adminReadonly: Boolean(adminOwner),
|
|
8371
|
+
adminVersionRepair,
|
|
8088
8372
|
flowId,
|
|
8089
8373
|
flowSource,
|
|
8090
8374
|
archived,
|
|
@@ -11392,6 +11676,72 @@ function prdWorkflowMergeProducerTimeline(report, currentSnapshot = {}) {
|
|
|
11392
11676
|
};
|
|
11393
11677
|
}
|
|
11394
11678
|
|
|
11679
|
+
function prdWorkflowAdminVersionRepairIntent(payload = {}, report = {}, userCtx = {}) {
|
|
11680
|
+
const operation = String(
|
|
11681
|
+
payload.adminOperation || payload.admin_operation || payload.administrativeOperation || payload.administrative_operation || "",
|
|
11682
|
+
).trim().toLowerCase();
|
|
11683
|
+
if (!operation) return { requested: false };
|
|
11684
|
+
if (operation !== "repair-version-membership") {
|
|
11685
|
+
return { requested: true, status: 400, error: `Unsupported admin Workflow operation: ${operation}` };
|
|
11686
|
+
}
|
|
11687
|
+
if (userCtx.isAdmin !== true) {
|
|
11688
|
+
return { requested: true, status: 403, error: "Admin permission required" };
|
|
11689
|
+
}
|
|
11690
|
+
const forbiddenKeys = ["action", "artifacts", "observation", "globalState", "global_state", "extensions", "extension"]
|
|
11691
|
+
.filter((key) => Object.prototype.hasOwnProperty.call(payload, key));
|
|
11692
|
+
if (forbiddenKeys.length) {
|
|
11693
|
+
return {
|
|
11694
|
+
requested: true,
|
|
11695
|
+
status: 400,
|
|
11696
|
+
error: `Admin version repair may only update projections.timeline; remove: ${forbiddenKeys.join(", ")}`,
|
|
11697
|
+
};
|
|
11698
|
+
}
|
|
11699
|
+
const projections = payload.projections;
|
|
11700
|
+
if (!projections || typeof projections !== "object" || Array.isArray(projections) || !Array.isArray(projections.timeline)) {
|
|
11701
|
+
return { requested: true, status: 400, error: "Admin version repair requires projections.timeline" };
|
|
11702
|
+
}
|
|
11703
|
+
const extraProjectionKeys = Object.keys(projections).filter((key) => key !== "timeline");
|
|
11704
|
+
if (extraProjectionKeys.length) {
|
|
11705
|
+
return { requested: true, status: 400, error: "Admin version repair may only update projections.timeline" };
|
|
11706
|
+
}
|
|
11707
|
+
const nonVersionEntry = report?.projections?.timeline?.find((item) => String(item?.kind || "").trim().toLowerCase() !== "version");
|
|
11708
|
+
if (nonVersionEntry) {
|
|
11709
|
+
return { requested: true, status: 400, error: "Admin version repair only accepts timeline entries with kind=version" };
|
|
11710
|
+
}
|
|
11711
|
+
if (!report.idempotencyKey) {
|
|
11712
|
+
return { requested: true, status: 400, error: "Admin version repair requires idempotencyKey" };
|
|
11713
|
+
}
|
|
11714
|
+
if (!report.expectedRevision) {
|
|
11715
|
+
return { requested: true, status: 400, error: "Admin version repair requires expectedRevision" };
|
|
11716
|
+
}
|
|
11717
|
+
return { requested: true, operation };
|
|
11718
|
+
}
|
|
11719
|
+
|
|
11720
|
+
function prdWorkflowMergeAdminVersionTimeline(report, currentSnapshot = {}) {
|
|
11721
|
+
const source = prdWorkflowRuntimeEventProducer(report?.event || {});
|
|
11722
|
+
const current = Array.isArray(currentSnapshot?.projections?.timeline) ? currentSnapshot.projections.timeline : [];
|
|
11723
|
+
const incoming = Array.isArray(report?.projections?.timeline) ? report.projections.timeline : [];
|
|
11724
|
+
const retained = current.filter((item) => (
|
|
11725
|
+
prdWorkflowRuntimeEventProducer(item) !== source
|
|
11726
|
+
|| String(item?.kind || "").trim().toLowerCase() !== "version"
|
|
11727
|
+
));
|
|
11728
|
+
const timeline = [...retained, ...incoming];
|
|
11729
|
+
const administrativeRepair = {
|
|
11730
|
+
kind: "version-attribution",
|
|
11731
|
+
operation: "repair-version-membership",
|
|
11732
|
+
};
|
|
11733
|
+
return {
|
|
11734
|
+
...report,
|
|
11735
|
+
projections: { ...report.projections, timeline },
|
|
11736
|
+
event: {
|
|
11737
|
+
...report.event,
|
|
11738
|
+
projections: { ...report.event.projections, timeline },
|
|
11739
|
+
administrativeRepair,
|
|
11740
|
+
administrative_repair: administrativeRepair,
|
|
11741
|
+
},
|
|
11742
|
+
};
|
|
11743
|
+
}
|
|
11744
|
+
|
|
11395
11745
|
function prdWorkflowRuntimeEventDedupeKey(event = {}, index = 0) {
|
|
11396
11746
|
const producer = prdWorkflowRuntimeEventProducer(event);
|
|
11397
11747
|
const operation = prdWorkflowRuntimeEventOperation(event);
|
|
@@ -11678,6 +12028,136 @@ function prdWorkflowGlobalStateFromEvents(tapdId, snapshot = {}, runtimeEvents =
|
|
|
11678
12028
|
return state;
|
|
11679
12029
|
}
|
|
11680
12030
|
|
|
12031
|
+
function prdWorkflowChecklistActionKey(action = {}) {
|
|
12032
|
+
return String(
|
|
12033
|
+
action?.actionModel?.key ||
|
|
12034
|
+
action?.key ||
|
|
12035
|
+
action?.actionKey ||
|
|
12036
|
+
action?.action_key ||
|
|
12037
|
+
action?.action ||
|
|
12038
|
+
action?.actionId ||
|
|
12039
|
+
action?.action_id ||
|
|
12040
|
+
action?.stageKey ||
|
|
12041
|
+
action?.stage_key ||
|
|
12042
|
+
"",
|
|
12043
|
+
).trim();
|
|
12044
|
+
}
|
|
12045
|
+
|
|
12046
|
+
function prdWorkflowChecklistResourceKey(producer, actionKey, itemKey) {
|
|
12047
|
+
return `checklist:${String(producer || "").trim().toLowerCase()}:${String(actionKey || "").trim()}:${String(itemKey || "").trim()}`;
|
|
12048
|
+
}
|
|
12049
|
+
|
|
12050
|
+
function prdWorkflowChecklistStateEntries(runtimeEvents = []) {
|
|
12051
|
+
const states = new Map();
|
|
12052
|
+
for (const event of Array.isArray(runtimeEvents) ? runtimeEvents : []) {
|
|
12053
|
+
const state = event?.checklistState || event?.checklist_state;
|
|
12054
|
+
if (!state || typeof state !== "object" || Array.isArray(state)) continue;
|
|
12055
|
+
const producer = String(state.producer || state.source || "").trim().toLowerCase();
|
|
12056
|
+
const actionKey = String(state.actionKey || state.action_key || "").trim();
|
|
12057
|
+
const itemKey = String(state.itemKey || state.item_key || "").trim();
|
|
12058
|
+
if (!producer || !actionKey || !itemKey) continue;
|
|
12059
|
+
const resourceKey = prdWorkflowChecklistResourceKey(producer, actionKey, itemKey);
|
|
12060
|
+
const version = workflowSnapshotResourceVersions({ runtimeEvents: [event] })[resourceKey] || "absent";
|
|
12061
|
+
states.set(resourceKey, { ...state, producer, actionKey, itemKey, resourceKey, version });
|
|
12062
|
+
}
|
|
12063
|
+
return states;
|
|
12064
|
+
}
|
|
12065
|
+
|
|
12066
|
+
function prdWorkflowMaterializeChecklists(snapshot = {}, runtimeEvents = []) {
|
|
12067
|
+
const actionSources = new Map();
|
|
12068
|
+
for (const event of Array.isArray(runtimeEvents) ? runtimeEvents : []) {
|
|
12069
|
+
const actionKey = prdWorkflowChecklistActionKey(event);
|
|
12070
|
+
const producer = String(event?.source || event?.producer || "").trim().toLowerCase();
|
|
12071
|
+
if (!actionKey || !producer || (!event?.checklist && !event?.actionModel?.checklist)) continue;
|
|
12072
|
+
const existing = actionSources.get(actionKey);
|
|
12073
|
+
actionSources.set(actionKey, existing && existing !== producer ? "" : producer);
|
|
12074
|
+
}
|
|
12075
|
+
const stateEntries = prdWorkflowChecklistStateEntries(runtimeEvents);
|
|
12076
|
+
const terminalStatuses = new Set(["passed", "skipped"]);
|
|
12077
|
+
const decorate = (action) => {
|
|
12078
|
+
if (!action || typeof action !== "object" || Array.isArray(action)) return action;
|
|
12079
|
+
const definition = action.checklist || action.actionModel?.checklist;
|
|
12080
|
+
if (!definition || typeof definition !== "object" || Array.isArray(definition)) return action;
|
|
12081
|
+
const actionKey = prdWorkflowChecklistActionKey(action);
|
|
12082
|
+
const producer = String(
|
|
12083
|
+
definition.source || action.source || action.producer || action.actionModel?.source || actionSources.get(actionKey) || "",
|
|
12084
|
+
).trim().toLowerCase();
|
|
12085
|
+
const items = (Array.isArray(definition.items) ? definition.items : []).map((item) => {
|
|
12086
|
+
const itemKey = String(item?.key || item?.id || "").trim();
|
|
12087
|
+
const resourceKey = producer && actionKey && itemKey
|
|
12088
|
+
? prdWorkflowChecklistResourceKey(producer, actionKey, itemKey)
|
|
12089
|
+
: "";
|
|
12090
|
+
const stored = resourceKey ? stateEntries.get(resourceKey) : null;
|
|
12091
|
+
const state = stored || {
|
|
12092
|
+
producer,
|
|
12093
|
+
actionKey,
|
|
12094
|
+
itemKey,
|
|
12095
|
+
status: "pending",
|
|
12096
|
+
note: "",
|
|
12097
|
+
evidence: [],
|
|
12098
|
+
resourceKey,
|
|
12099
|
+
version: "absent",
|
|
12100
|
+
};
|
|
12101
|
+
return { ...item, state };
|
|
12102
|
+
});
|
|
12103
|
+
const required = items.filter((item) => item.required !== false);
|
|
12104
|
+
const completed = items.filter((item) => terminalStatuses.has(String(item?.state?.status || "pending"))).length;
|
|
12105
|
+
const requiredCompleted = required.filter((item) => terminalStatuses.has(String(item?.state?.status || "pending"))).length;
|
|
12106
|
+
const completionPolicy = String(definition.completionPolicy || definition.completion_policy || "all_required").trim().toLowerCase();
|
|
12107
|
+
const completionCandidates = required.length ? required : items;
|
|
12108
|
+
const ready = completionPolicy === "manual"
|
|
12109
|
+
? false
|
|
12110
|
+
: completionPolicy === "any_required"
|
|
12111
|
+
? completionCandidates.some((item) => terminalStatuses.has(String(item?.state?.status || "pending")))
|
|
12112
|
+
: completionCandidates.length > 0 && completionCandidates.every((item) => terminalStatuses.has(String(item?.state?.status || "pending")));
|
|
12113
|
+
const checklist = {
|
|
12114
|
+
...definition,
|
|
12115
|
+
source: producer,
|
|
12116
|
+
items,
|
|
12117
|
+
progress: {
|
|
12118
|
+
total: items.length,
|
|
12119
|
+
completed,
|
|
12120
|
+
required: required.length,
|
|
12121
|
+
requiredCompleted,
|
|
12122
|
+
percent: items.length ? Math.round((completed / items.length) * 100) : 0,
|
|
12123
|
+
ready,
|
|
12124
|
+
},
|
|
12125
|
+
};
|
|
12126
|
+
return {
|
|
12127
|
+
...action,
|
|
12128
|
+
checklist,
|
|
12129
|
+
...(action.actionModel && typeof action.actionModel === "object" && !Array.isArray(action.actionModel)
|
|
12130
|
+
? { actionModel: { ...action.actionModel, checklist } }
|
|
12131
|
+
: {}),
|
|
12132
|
+
};
|
|
12133
|
+
};
|
|
12134
|
+
const out = { ...snapshot };
|
|
12135
|
+
for (const key of ["actions", "workflowActions", "workflow_actions", "timeline", "history", "events", "runtimeEvents", "runtime_events"]) {
|
|
12136
|
+
if (Array.isArray(snapshot?.[key])) out[key] = snapshot[key].map(decorate);
|
|
12137
|
+
}
|
|
12138
|
+
out.checklistStates = [...stateEntries.values()];
|
|
12139
|
+
return out;
|
|
12140
|
+
}
|
|
12141
|
+
|
|
12142
|
+
function prdWorkflowFindChecklistAction(snapshot = {}, producer = "", actionKey = "") {
|
|
12143
|
+
const wantedProducer = String(producer || "").trim().toLowerCase();
|
|
12144
|
+
const wantedActionKey = String(actionKey || "").trim();
|
|
12145
|
+
const matches = [];
|
|
12146
|
+
for (const key of ["actions", "workflowActions", "workflow_actions", "timeline", "history", "events", "runtimeEvents", "runtime_events"]) {
|
|
12147
|
+
for (const action of Array.isArray(snapshot?.[key]) ? snapshot[key] : []) {
|
|
12148
|
+
if (!action || typeof action !== "object" || Array.isArray(action)) continue;
|
|
12149
|
+
const checklist = action.checklist || action.actionModel?.checklist;
|
|
12150
|
+
if (!checklist || typeof checklist !== "object" || Array.isArray(checklist)) continue;
|
|
12151
|
+
const resolvedActionKey = prdWorkflowChecklistActionKey(action);
|
|
12152
|
+
const resolvedProducer = String(checklist.source || action.source || action.producer || "").trim().toLowerCase();
|
|
12153
|
+
if (resolvedActionKey !== wantedActionKey) continue;
|
|
12154
|
+
if (wantedProducer && resolvedProducer !== wantedProducer) continue;
|
|
12155
|
+
matches.push({ ...action, checklist, source: resolvedProducer || wantedProducer });
|
|
12156
|
+
}
|
|
12157
|
+
}
|
|
12158
|
+
return matches.at(-1) || null;
|
|
12159
|
+
}
|
|
12160
|
+
|
|
11681
12161
|
function prdWorkflowMergeRuntimeEvents(scopedRoot, tapdId, snapshot) {
|
|
11682
12162
|
const runtime = prdWorkflowReadRuntimeEvents(scopedRoot, tapdId);
|
|
11683
12163
|
const runtimeEvents = runtime.events;
|
|
@@ -11694,7 +12174,7 @@ function prdWorkflowMergeRuntimeEvents(scopedRoot, tapdId, snapshot) {
|
|
|
11694
12174
|
for (const key of ["issues", "issueGroups", "issue_groups", "epics", "epicGroups", "epic_groups", "aiDocs", "ai_docs"]) {
|
|
11695
12175
|
if (Object.prototype.hasOwnProperty.call(prdFlowExtension, key)) prdFlowExtensionView[key] = prdFlowExtension[key];
|
|
11696
12176
|
}
|
|
11697
|
-
|
|
12177
|
+
let materialized = {
|
|
11698
12178
|
...snapshot,
|
|
11699
12179
|
...prdFlowExtensionView,
|
|
11700
12180
|
workflow: globalState.workflow,
|
|
@@ -11711,6 +12191,7 @@ function prdWorkflowMergeRuntimeEvents(scopedRoot, tapdId, snapshot) {
|
|
|
11711
12191
|
runtimeEventsUpdatedAt: runtime.updatedAt || "",
|
|
11712
12192
|
},
|
|
11713
12193
|
};
|
|
12194
|
+
materialized = prdWorkflowMaterializeChecklists(materialized, runtimeEvents);
|
|
11714
12195
|
materialized.resourceVersions = workflowSnapshotResourceVersions(materialized);
|
|
11715
12196
|
return materialized;
|
|
11716
12197
|
}
|
|
@@ -13220,13 +13701,25 @@ export function startUiServer({
|
|
|
13220
13701
|
? getTeamById(requestedTeamId)
|
|
13221
13702
|
: getTeamForUser(userCtx.userId);
|
|
13222
13703
|
if (!team || team.status !== "active") {
|
|
13223
|
-
json(res, 200, {
|
|
13704
|
+
json(res, 200, {
|
|
13705
|
+
ok: true,
|
|
13706
|
+
view: "team",
|
|
13707
|
+
team: null,
|
|
13708
|
+
workflows: [],
|
|
13709
|
+
timeline: [],
|
|
13710
|
+
unassignedCount: 0,
|
|
13711
|
+
availableCount: 0,
|
|
13712
|
+
selectedTimelineKey: "all",
|
|
13713
|
+
defaultTimelineKey: "all",
|
|
13714
|
+
pagination: { page: 1, pageSize: 20, total: 0, totalPages: 1, hasPrevious: false, hasNext: false },
|
|
13715
|
+
});
|
|
13224
13716
|
return;
|
|
13225
13717
|
}
|
|
13226
13718
|
records = listPrdWorkflowCollaborationsForTeam(team.id);
|
|
13227
13719
|
} else {
|
|
13228
13720
|
records = listPrdWorkflowCollaborationsForUser(userCtx.userId);
|
|
13229
13721
|
}
|
|
13722
|
+
const accessibleProjects = listAccessibleProjectFlows(root, userCtx);
|
|
13230
13723
|
const workflows = records.map((record) => {
|
|
13231
13724
|
const stateRoot = path.resolve(getAgentflowUserDataRoot(record.stateOwnerId || record.ownerId));
|
|
13232
13725
|
const tapdId = String(record.tapdId || "").trim();
|
|
@@ -13235,14 +13728,23 @@ export function startUiServer({
|
|
|
13235
13728
|
const legacy = prdWorkflowReadCachedSnapshot(stateRoot, tapdId);
|
|
13236
13729
|
const snapshot = project?.snapshot || latestClient || legacy?.snapshot || {};
|
|
13237
13730
|
const materialized = prdWorkflowMergeRuntimeEvents(stateRoot, tapdId, snapshot);
|
|
13238
|
-
|
|
13731
|
+
const projectBindings = workflowProjectBindingRows(record.projectBindings, accessibleProjects, userCtx);
|
|
13732
|
+
return prdWorkflowDashboardSummary(record, materialized, userCtx, projectBindings);
|
|
13239
13733
|
});
|
|
13240
13734
|
const dashboardTimeline = prdWorkflowDashboardTimeline(workflows);
|
|
13735
|
+
const dashboardPage = prdWorkflowDashboardPage(workflows, dashboardTimeline, {
|
|
13736
|
+
timelineKey: url.searchParams.has("timelineKey") ? url.searchParams.get("timelineKey") : "",
|
|
13737
|
+
query: url.searchParams.get("q"),
|
|
13738
|
+
scope: url.searchParams.get("scope"),
|
|
13739
|
+
state: url.searchParams.get("state"),
|
|
13740
|
+
page: url.searchParams.get("page"),
|
|
13741
|
+
pageSize: url.searchParams.get("pageSize"),
|
|
13742
|
+
});
|
|
13241
13743
|
json(res, 200, {
|
|
13242
13744
|
ok: true,
|
|
13243
13745
|
view: view === "team" ? "team" : "personal",
|
|
13244
13746
|
team: teamSummaryWithUsers(team),
|
|
13245
|
-
|
|
13747
|
+
...dashboardPage,
|
|
13246
13748
|
...dashboardTimeline,
|
|
13247
13749
|
});
|
|
13248
13750
|
} catch (error) {
|
|
@@ -13373,6 +13875,177 @@ export function startUiServer({
|
|
|
13373
13875
|
});
|
|
13374
13876
|
return;
|
|
13375
13877
|
}
|
|
13878
|
+
if (req.method === "GET" && url.pathname === "/api/workflows/project-bindings") {
|
|
13879
|
+
if (!authUser?.userId) {
|
|
13880
|
+
json(res, 401, { error: "Authentication required" });
|
|
13881
|
+
return;
|
|
13882
|
+
}
|
|
13883
|
+
const tapdId = String(url.searchParams.get("tapdId") || url.searchParams.get("id") || "").trim();
|
|
13884
|
+
if (!tapdId) {
|
|
13885
|
+
json(res, 400, { error: "Missing tapdId" });
|
|
13886
|
+
return;
|
|
13887
|
+
}
|
|
13888
|
+
const existing = getPrdWorkflowCollaborationByTapdId(tapdId);
|
|
13889
|
+
const result = listPrdWorkflowProjectBindings({ tapdId, userId: userCtx.userId });
|
|
13890
|
+
if (existing && result.error) {
|
|
13891
|
+
json(res, 403, { error: "PRD Workflow collaboration permission denied" });
|
|
13892
|
+
return;
|
|
13893
|
+
}
|
|
13894
|
+
const accessibleProjects = listAccessibleProjectFlows(root, userCtx);
|
|
13895
|
+
const bindings = workflowProjectBindingRows(result.projectBindings || [], accessibleProjects, userCtx);
|
|
13896
|
+
json(res, 200, {
|
|
13897
|
+
ok: true,
|
|
13898
|
+
bindings,
|
|
13899
|
+
availableProjects: availableWorkflowBindingProjects(accessibleProjects, bindings),
|
|
13900
|
+
});
|
|
13901
|
+
return;
|
|
13902
|
+
}
|
|
13903
|
+
if (req.method === "POST" && url.pathname === "/api/workflows/project-bindings") {
|
|
13904
|
+
if (!authUser?.userId) {
|
|
13905
|
+
json(res, 401, { error: "Authentication required" });
|
|
13906
|
+
return;
|
|
13907
|
+
}
|
|
13908
|
+
let payload;
|
|
13909
|
+
try {
|
|
13910
|
+
payload = JSON.parse(await readBody(req, 128 * 1024));
|
|
13911
|
+
} catch {
|
|
13912
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
13913
|
+
return;
|
|
13914
|
+
}
|
|
13915
|
+
const tapdId = String(payload?.tapdId || payload?.tapd_id || "").trim();
|
|
13916
|
+
const flowId = String(payload?.flowId || "").trim();
|
|
13917
|
+
const flowSource = String(payload?.flowSource || "user").trim() || "user";
|
|
13918
|
+
const workspaceId = String(payload?.workspaceId || "").trim();
|
|
13919
|
+
if (!tapdId || !flowId) {
|
|
13920
|
+
json(res, 400, { error: "Project binding requires tapdId and flowId" });
|
|
13921
|
+
return;
|
|
13922
|
+
}
|
|
13923
|
+
if (flowSource !== "user" && flowSource !== "workspace") {
|
|
13924
|
+
json(res, 400, { error: "Only editable Projects can be bound" });
|
|
13925
|
+
return;
|
|
13926
|
+
}
|
|
13927
|
+
const existingWorkflow = getPrdWorkflowCollaborationByTapdId(tapdId);
|
|
13928
|
+
if (existingWorkflow && !getPrdWorkflowCollaborationForUser(tapdId, userCtx.userId)) {
|
|
13929
|
+
json(res, 403, { error: "PRD Workflow collaboration permission denied" });
|
|
13930
|
+
return;
|
|
13931
|
+
}
|
|
13932
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
13933
|
+
flowId,
|
|
13934
|
+
flowSource,
|
|
13935
|
+
workspaceId,
|
|
13936
|
+
archived: false,
|
|
13937
|
+
}, userCtx);
|
|
13938
|
+
if (scoped.error) {
|
|
13939
|
+
json(res, scoped.status || 400, { error: scoped.error });
|
|
13940
|
+
return;
|
|
13941
|
+
}
|
|
13942
|
+
if (scoped.archived || (scoped.collaboration && !scoped.collaborationAccess?.writable)) {
|
|
13943
|
+
json(res, 403, { error: "Only Project owners and editors can bind an iteration" });
|
|
13944
|
+
return;
|
|
13945
|
+
}
|
|
13946
|
+
const projectCollaboration = scoped.collaboration
|
|
13947
|
+
? { record: scoped.collaboration, workspace: workspaceCollaborationSummary(scoped.collaboration, userCtx.userId) }
|
|
13948
|
+
: ensureWorkspaceCollaboration({
|
|
13949
|
+
flowId: scoped.flowId,
|
|
13950
|
+
flowSource: scoped.flowSource,
|
|
13951
|
+
archived: false,
|
|
13952
|
+
userId: userCtx.userId,
|
|
13953
|
+
});
|
|
13954
|
+
if (projectCollaboration.error || !projectCollaboration.record?.id) {
|
|
13955
|
+
json(res, projectCollaboration.status || 400, { error: projectCollaboration.error || "Project collaboration is unavailable" });
|
|
13956
|
+
return;
|
|
13957
|
+
}
|
|
13958
|
+
const projectAccess = workspaceCollaborationAccess(projectCollaboration.record, userCtx.userId);
|
|
13959
|
+
if (!projectAccess.writable) {
|
|
13960
|
+
json(res, 403, { error: "Only Project owners and editors can bind an iteration" });
|
|
13961
|
+
return;
|
|
13962
|
+
}
|
|
13963
|
+
const ensuredWorkflow = ensurePrdWorkflowCollaboration({ tapdId, userId: userCtx.userId });
|
|
13964
|
+
if (ensuredWorkflow.error) {
|
|
13965
|
+
json(res, ensuredWorkflow.status || 400, { error: ensuredWorkflow.error });
|
|
13966
|
+
return;
|
|
13967
|
+
}
|
|
13968
|
+
const result = bindPrdWorkflowProject({
|
|
13969
|
+
tapdId,
|
|
13970
|
+
userId: userCtx.userId,
|
|
13971
|
+
project: {
|
|
13972
|
+
workspaceId: projectCollaboration.record.id,
|
|
13973
|
+
flowId: scoped.flowId,
|
|
13974
|
+
flowSource: scoped.flowSource,
|
|
13975
|
+
archived: false,
|
|
13976
|
+
ownerId: projectCollaboration.record.ownerId,
|
|
13977
|
+
},
|
|
13978
|
+
});
|
|
13979
|
+
if (result.error) {
|
|
13980
|
+
json(res, result.status || 400, { error: result.error });
|
|
13981
|
+
return;
|
|
13982
|
+
}
|
|
13983
|
+
const accessibleProjects = listAccessibleProjectFlows(root, userCtx);
|
|
13984
|
+
const bindings = workflowProjectBindingRows(result.projectBindings, accessibleProjects, userCtx);
|
|
13985
|
+
json(res, 200, {
|
|
13986
|
+
ok: true,
|
|
13987
|
+
created: result.created === true,
|
|
13988
|
+
bindings,
|
|
13989
|
+
availableProjects: availableWorkflowBindingProjects(accessibleProjects, bindings),
|
|
13990
|
+
});
|
|
13991
|
+
return;
|
|
13992
|
+
}
|
|
13993
|
+
if (req.method === "DELETE" && url.pathname === "/api/workflows/project-bindings") {
|
|
13994
|
+
if (!authUser?.userId) {
|
|
13995
|
+
json(res, 401, { error: "Authentication required" });
|
|
13996
|
+
return;
|
|
13997
|
+
}
|
|
13998
|
+
let payload;
|
|
13999
|
+
try {
|
|
14000
|
+
payload = JSON.parse(await readBody(req, 128 * 1024));
|
|
14001
|
+
} catch {
|
|
14002
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
14003
|
+
return;
|
|
14004
|
+
}
|
|
14005
|
+
const tapdId = String(payload?.tapdId || payload?.tapd_id || "").trim();
|
|
14006
|
+
const workspaceId = String(payload?.workspaceId || "").trim();
|
|
14007
|
+
if (!tapdId || !workspaceId) {
|
|
14008
|
+
json(res, 400, { error: "Unbinding requires tapdId and workspaceId" });
|
|
14009
|
+
return;
|
|
14010
|
+
}
|
|
14011
|
+
const listed = listPrdWorkflowProjectBindings({ tapdId, userId: userCtx.userId });
|
|
14012
|
+
if (listed.error) {
|
|
14013
|
+
json(res, listed.status || 400, { error: listed.error });
|
|
14014
|
+
return;
|
|
14015
|
+
}
|
|
14016
|
+
const binding = listed.projectBindings.find((item) => item.workspaceId === workspaceId);
|
|
14017
|
+
if (!binding) {
|
|
14018
|
+
json(res, 404, { error: "Project binding not found" });
|
|
14019
|
+
return;
|
|
14020
|
+
}
|
|
14021
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
14022
|
+
flowId: binding.flowId,
|
|
14023
|
+
flowSource: binding.flowSource,
|
|
14024
|
+
workspaceId,
|
|
14025
|
+
archived: binding.archived === true,
|
|
14026
|
+
}, userCtx);
|
|
14027
|
+
if (scoped.error) {
|
|
14028
|
+
json(res, scoped.status || 400, { error: scoped.error });
|
|
14029
|
+
return;
|
|
14030
|
+
}
|
|
14031
|
+
if (!scoped.collaborationAccess?.writable) {
|
|
14032
|
+
json(res, 403, { error: "Only Project owners and editors can unbind an iteration" });
|
|
14033
|
+
return;
|
|
14034
|
+
}
|
|
14035
|
+
const result = unbindPrdWorkflowProject({ tapdId, userId: userCtx.userId, workspaceId });
|
|
14036
|
+
if (result.error) {
|
|
14037
|
+
json(res, result.status || 400, { error: result.error });
|
|
14038
|
+
return;
|
|
14039
|
+
}
|
|
14040
|
+
const accessibleProjects = listAccessibleProjectFlows(root, userCtx);
|
|
14041
|
+
const bindings = workflowProjectBindingRows(result.projectBindings, accessibleProjects, userCtx);
|
|
14042
|
+
json(res, 200, {
|
|
14043
|
+
ok: true,
|
|
14044
|
+
bindings,
|
|
14045
|
+
availableProjects: availableWorkflowBindingProjects(accessibleProjects, bindings),
|
|
14046
|
+
});
|
|
14047
|
+
return;
|
|
14048
|
+
}
|
|
13376
14049
|
if (req.method === "GET" && url.pathname === "/api/workflows/knowledge-bindings") {
|
|
13377
14050
|
if (!authUser?.userId) {
|
|
13378
14051
|
json(res, 401, { error: "Authentication required" });
|
|
@@ -13713,6 +14386,276 @@ export function startUiServer({
|
|
|
13713
14386
|
}
|
|
13714
14387
|
return;
|
|
13715
14388
|
}
|
|
14389
|
+
if (req.method === "GET" && url.pathname === "/api/workflows/checklist") {
|
|
14390
|
+
try {
|
|
14391
|
+
const workflow = normalizeWorkflowReference({ workflow: url.searchParams.get("workflow") || "" });
|
|
14392
|
+
if (workflow.error) {
|
|
14393
|
+
json(res, 400, { error: workflow.error });
|
|
14394
|
+
return;
|
|
14395
|
+
}
|
|
14396
|
+
if (workflow.namespace !== "tapd") {
|
|
14397
|
+
json(res, 400, { error: `Unsupported workflow namespace: ${workflow.namespace}` });
|
|
14398
|
+
return;
|
|
14399
|
+
}
|
|
14400
|
+
const source = String(url.searchParams.get("source") || "").trim().toLowerCase();
|
|
14401
|
+
const actionKey = String(url.searchParams.get("actionKey") || url.searchParams.get("action_key") || "").trim();
|
|
14402
|
+
if (!/^[a-z][a-z0-9._-]{0,119}$/.test(source)) {
|
|
14403
|
+
json(res, 400, { error: "Invalid checklist source" });
|
|
14404
|
+
return;
|
|
14405
|
+
}
|
|
14406
|
+
if (!actionKey || actionKey.length > 240 || /[\0\r\n]/.test(actionKey)) {
|
|
14407
|
+
json(res, 400, { error: "Invalid checklist actionKey" });
|
|
14408
|
+
return;
|
|
14409
|
+
}
|
|
14410
|
+
const flowId = String(url.searchParams.get("flowId") || "").trim();
|
|
14411
|
+
const flowSource = String(url.searchParams.get("flowSource") || "user").trim() || "user";
|
|
14412
|
+
const workflowScope = resolvePrdWorkflowScope(root, {
|
|
14413
|
+
tapdId: workflow.id,
|
|
14414
|
+
flowId,
|
|
14415
|
+
flowSource,
|
|
14416
|
+
archived: url.searchParams.get("archived") === "1",
|
|
14417
|
+
workspaceId: url.searchParams.get("workspaceId") || "",
|
|
14418
|
+
workflowShare: url.searchParams.get("workflowShare") || "",
|
|
14419
|
+
}, userCtx, "read");
|
|
14420
|
+
if (workflowScope.error) {
|
|
14421
|
+
json(res, workflowScope.status || 400, { error: workflowScope.error });
|
|
14422
|
+
return;
|
|
14423
|
+
}
|
|
14424
|
+
prdWorkflowMigrateLegacyState(workflowScope.executionRoot, workflowScope.stateRoot, workflow.id);
|
|
14425
|
+
const snapshot = prdWorkflowMaterializeSnapshot(
|
|
14426
|
+
workflowScope.executionRoot,
|
|
14427
|
+
workflowScope.stateRoot,
|
|
14428
|
+
workflow.id,
|
|
14429
|
+
userCtx,
|
|
14430
|
+
{ flowSource, flowId },
|
|
14431
|
+
);
|
|
14432
|
+
const action = prdWorkflowFindChecklistAction(snapshot, source, actionKey);
|
|
14433
|
+
if (!action) {
|
|
14434
|
+
json(res, 404, { error: "Workflow Action checklist not found" });
|
|
14435
|
+
return;
|
|
14436
|
+
}
|
|
14437
|
+
const access = workflowScope.collaborationAccess || {};
|
|
14438
|
+
const canWrite = Boolean(authUser?.userId) && !workflowScope.sharedByLink && !workflowScope.adminReadonly && (
|
|
14439
|
+
workflowScope.collaboration ? access.writable === true : true
|
|
14440
|
+
);
|
|
14441
|
+
json(res, 200, {
|
|
14442
|
+
ok: true,
|
|
14443
|
+
workflow,
|
|
14444
|
+
action: {
|
|
14445
|
+
key: actionKey,
|
|
14446
|
+
source,
|
|
14447
|
+
title: String(action.title || action.label || actionKey),
|
|
14448
|
+
status: String(action.status || "pending"),
|
|
14449
|
+
checklist: action.checklist,
|
|
14450
|
+
},
|
|
14451
|
+
canWrite,
|
|
14452
|
+
});
|
|
14453
|
+
} catch (error) {
|
|
14454
|
+
json(res, 500, { error: (error && error.message) || String(error) });
|
|
14455
|
+
}
|
|
14456
|
+
return;
|
|
14457
|
+
}
|
|
14458
|
+
if (req.method === "PATCH" && url.pathname === "/api/workflows/checklist") {
|
|
14459
|
+
if (!authUser?.userId) {
|
|
14460
|
+
json(res, 401, { error: "Authentication required" });
|
|
14461
|
+
return;
|
|
14462
|
+
}
|
|
14463
|
+
let payload;
|
|
14464
|
+
try {
|
|
14465
|
+
payload = JSON.parse(await readBody(req, 1024 * 1024));
|
|
14466
|
+
} catch (error) {
|
|
14467
|
+
json(res, error?.status === 413 ? 413 : 400, { error: error?.status === 413 ? error.message : "Invalid JSON body" });
|
|
14468
|
+
return;
|
|
14469
|
+
}
|
|
14470
|
+
let releaseWorkflowWriteLock = null;
|
|
14471
|
+
try {
|
|
14472
|
+
const workflow = normalizeWorkflowReference(payload);
|
|
14473
|
+
if (workflow.error) {
|
|
14474
|
+
json(res, 400, { error: workflow.error });
|
|
14475
|
+
return;
|
|
14476
|
+
}
|
|
14477
|
+
if (workflow.namespace !== "tapd") {
|
|
14478
|
+
json(res, 400, { error: `Unsupported workflow namespace: ${workflow.namespace}` });
|
|
14479
|
+
return;
|
|
14480
|
+
}
|
|
14481
|
+
const source = String(payload.source || "").trim().toLowerCase();
|
|
14482
|
+
const actionKey = String(payload.actionKey || payload.action_key || "").trim();
|
|
14483
|
+
const itemKey = String(payload.itemKey || payload.item_key || "").trim();
|
|
14484
|
+
if (!/^[a-z][a-z0-9._-]{0,119}$/.test(source)) {
|
|
14485
|
+
json(res, 400, { error: "Invalid checklist source" });
|
|
14486
|
+
return;
|
|
14487
|
+
}
|
|
14488
|
+
if (!actionKey || actionKey.length > 240 || /[\0\r\n]/.test(actionKey)) {
|
|
14489
|
+
json(res, 400, { error: "Invalid checklist actionKey" });
|
|
14490
|
+
return;
|
|
14491
|
+
}
|
|
14492
|
+
if (!itemKey || itemKey.length > 240 || /[\0\r\n]/.test(itemKey)) {
|
|
14493
|
+
json(res, 400, { error: "Invalid checklist itemKey" });
|
|
14494
|
+
return;
|
|
14495
|
+
}
|
|
14496
|
+
const rawStatus = String(payload.status || "pending").trim().toLowerCase();
|
|
14497
|
+
if (!["pending", "passed", "failed", "blocked", "skipped", "done", "complete", "completed", "success", "error", "cancelled", "canceled"].includes(rawStatus)) {
|
|
14498
|
+
json(res, 400, { error: `Invalid checklist item status: ${rawStatus}` });
|
|
14499
|
+
return;
|
|
14500
|
+
}
|
|
14501
|
+
const status = normalizeWorkflowChecklistItemStatus(rawStatus);
|
|
14502
|
+
const note = String(payload.note || "").trim();
|
|
14503
|
+
if (note.length > 4000) {
|
|
14504
|
+
json(res, 400, { error: "Checklist note exceeds 4000 characters" });
|
|
14505
|
+
return;
|
|
14506
|
+
}
|
|
14507
|
+
const rawEvidence = Array.isArray(payload.evidence) ? payload.evidence : [];
|
|
14508
|
+
if (rawEvidence.length > 20) {
|
|
14509
|
+
json(res, 400, { error: "Checklist evidence supports at most 20 entries" });
|
|
14510
|
+
return;
|
|
14511
|
+
}
|
|
14512
|
+
const evidence = [];
|
|
14513
|
+
for (let index = 0; index < rawEvidence.length; index += 1) {
|
|
14514
|
+
const item = rawEvidence[index];
|
|
14515
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
14516
|
+
json(res, 400, { error: `evidence[${index}] must be an object` });
|
|
14517
|
+
return;
|
|
14518
|
+
}
|
|
14519
|
+
const evidenceUrl = String(item.url || item.href || "").trim();
|
|
14520
|
+
if (!evidenceUrl || evidenceUrl.length > 4000 || !isSafeWorkflowUrl(evidenceUrl)) {
|
|
14521
|
+
json(res, 400, { error: `evidence[${index}].url must use http, https, or an absolute application path` });
|
|
14522
|
+
return;
|
|
14523
|
+
}
|
|
14524
|
+
evidence.push({
|
|
14525
|
+
title: String(item.title || item.label || `证据 ${index + 1}`).trim().slice(0, 500),
|
|
14526
|
+
url: evidenceUrl,
|
|
14527
|
+
});
|
|
14528
|
+
}
|
|
14529
|
+
const expectedVersion = String(payload.expectedVersion || payload.expected_version || "").trim();
|
|
14530
|
+
if (!expectedVersion) {
|
|
14531
|
+
json(res, 400, { error: "Checklist update requires expectedVersion" });
|
|
14532
|
+
return;
|
|
14533
|
+
}
|
|
14534
|
+
const flowId = String(payload.flowId || payload.flow_id || "").trim();
|
|
14535
|
+
const flowSource = String(payload.flowSource || payload.flow_source || "user").trim() || "user";
|
|
14536
|
+
const workflowScope = resolvePrdWorkflowScope(root, {
|
|
14537
|
+
...payload,
|
|
14538
|
+
tapdId: workflow.id,
|
|
14539
|
+
flowId,
|
|
14540
|
+
flowSource,
|
|
14541
|
+
}, userCtx, "write");
|
|
14542
|
+
if (workflowScope.error) {
|
|
14543
|
+
json(res, workflowScope.status || 400, { error: workflowScope.error });
|
|
14544
|
+
return;
|
|
14545
|
+
}
|
|
14546
|
+
if (!workflowScope.collaboration) {
|
|
14547
|
+
const ensured = ensurePrdWorkflowCollaboration({ tapdId: workflow.id, userId: userCtx.userId });
|
|
14548
|
+
if (ensured.error) {
|
|
14549
|
+
json(res, ensured.status || 400, { error: ensured.error });
|
|
14550
|
+
return;
|
|
14551
|
+
}
|
|
14552
|
+
}
|
|
14553
|
+
const scopedRoot = workflowScope.stateRoot;
|
|
14554
|
+
prdWorkflowMigrateLegacyState(workflowScope.executionRoot, scopedRoot, workflow.id);
|
|
14555
|
+
releaseWorkflowWriteLock = await prdWorkflowAcquireWriteLock(`${scopedRoot}\t${workflow.id}`);
|
|
14556
|
+
let snapshot = prdWorkflowMaterializeSnapshot(
|
|
14557
|
+
workflowScope.executionRoot,
|
|
14558
|
+
scopedRoot,
|
|
14559
|
+
workflow.id,
|
|
14560
|
+
userCtx,
|
|
14561
|
+
{ flowSource, flowId },
|
|
14562
|
+
);
|
|
14563
|
+
const idempotencyKey = String(payload.idempotencyKey || payload.idempotency_key || "").trim().slice(0, 500);
|
|
14564
|
+
if (idempotencyKey) {
|
|
14565
|
+
const existing = prdWorkflowFindIdempotencyEvent(scopedRoot, workflow.id, idempotencyKey, "agentflow-checklist", false, "checklist.update");
|
|
14566
|
+
if (existing) {
|
|
14567
|
+
json(res, 200, { ok: true, alreadyApplied: true, workflow, checklistState: existing.checklistState, snapshot });
|
|
14568
|
+
return;
|
|
14569
|
+
}
|
|
14570
|
+
}
|
|
14571
|
+
const action = prdWorkflowFindChecklistAction(snapshot, source, actionKey);
|
|
14572
|
+
const checklistItem = action?.checklist?.items?.find((item) => String(item?.key || "") === itemKey);
|
|
14573
|
+
if (!action || !checklistItem) {
|
|
14574
|
+
json(res, 404, { error: "Workflow Action checklist item not found" });
|
|
14575
|
+
return;
|
|
14576
|
+
}
|
|
14577
|
+
if (status === "passed" && checklistItem.evidenceRequired === true && evidence.length === 0) {
|
|
14578
|
+
json(res, 400, { error: "Checklist item requires evidence before it can pass" });
|
|
14579
|
+
return;
|
|
14580
|
+
}
|
|
14581
|
+
const resourceKey = prdWorkflowChecklistResourceKey(source, actionKey, itemKey);
|
|
14582
|
+
const currentVersion = String(snapshot.resourceVersions?.[resourceKey] || "absent");
|
|
14583
|
+
if (expectedVersion !== currentVersion) {
|
|
14584
|
+
json(res, 409, {
|
|
14585
|
+
error: "Checklist item changed; refresh it before saving",
|
|
14586
|
+
conflict: { type: "workflow-resource-conflict", conflicts: [{ resourceKey, expectedVersion, currentVersion }], workflow },
|
|
14587
|
+
snapshot,
|
|
14588
|
+
});
|
|
14589
|
+
return;
|
|
14590
|
+
}
|
|
14591
|
+
const now = new Date().toISOString();
|
|
14592
|
+
const checklistState = {
|
|
14593
|
+
producer: source,
|
|
14594
|
+
actionKey,
|
|
14595
|
+
itemKey,
|
|
14596
|
+
status,
|
|
14597
|
+
note,
|
|
14598
|
+
evidence,
|
|
14599
|
+
updatedAt: now,
|
|
14600
|
+
updatedBy: {
|
|
14601
|
+
userId: String(userCtx.userId || ""),
|
|
14602
|
+
username: String(authUser.username || userCtx.userId || ""),
|
|
14603
|
+
},
|
|
14604
|
+
};
|
|
14605
|
+
const event = prdWorkflowAppendRuntimeEvent(scopedRoot, workflow.id, {
|
|
14606
|
+
id: `checklist_state_${prdWorkflowSafeStateId([source, actionKey, itemKey].join(":"))}`,
|
|
14607
|
+
type: "workflow-checklist-update",
|
|
14608
|
+
operation: "checklist.update",
|
|
14609
|
+
source: "agentflow-checklist",
|
|
14610
|
+
auxiliary: true,
|
|
14611
|
+
aggregateByStage: false,
|
|
14612
|
+
status: "done",
|
|
14613
|
+
checklistState,
|
|
14614
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
14615
|
+
});
|
|
14616
|
+
if (!event) throw new Error("Failed to store checklist state");
|
|
14617
|
+
snapshot = prdWorkflowMaterializeSnapshot(
|
|
14618
|
+
workflowScope.executionRoot,
|
|
14619
|
+
scopedRoot,
|
|
14620
|
+
workflow.id,
|
|
14621
|
+
userCtx,
|
|
14622
|
+
{ flowSource, flowId },
|
|
14623
|
+
);
|
|
14624
|
+
const updatedAction = prdWorkflowFindChecklistAction(snapshot, source, actionKey);
|
|
14625
|
+
const updatedItem = updatedAction?.checklist?.items?.find((item) => String(item?.key || "") === itemKey);
|
|
14626
|
+
prdWorkflowAppendAudit(scopedRoot, workflow.id, {
|
|
14627
|
+
type: "checklist-item-updated",
|
|
14628
|
+
source,
|
|
14629
|
+
actionKey,
|
|
14630
|
+
itemKey,
|
|
14631
|
+
status,
|
|
14632
|
+
resourceKey,
|
|
14633
|
+
actorUserId: String(userCtx.userId || ""),
|
|
14634
|
+
});
|
|
14635
|
+
prdWorkflowBroadcast(prdWorkflowKey(userCtx, flowSource, flowId, workflow.id), {
|
|
14636
|
+
type: "workflow-checklist-updated",
|
|
14637
|
+
tapdId: workflow.id,
|
|
14638
|
+
source,
|
|
14639
|
+
actionKey,
|
|
14640
|
+
itemKey,
|
|
14641
|
+
checklistState: updatedItem?.state || checklistState,
|
|
14642
|
+
snapshot,
|
|
14643
|
+
});
|
|
14644
|
+
json(res, 200, {
|
|
14645
|
+
ok: true,
|
|
14646
|
+
alreadyApplied: false,
|
|
14647
|
+
workflow,
|
|
14648
|
+
checklistState: updatedItem?.state || checklistState,
|
|
14649
|
+
checklist: updatedAction?.checklist || null,
|
|
14650
|
+
snapshot,
|
|
14651
|
+
});
|
|
14652
|
+
} catch (error) {
|
|
14653
|
+
json(res, 500, { error: (error && error.message) || String(error) });
|
|
14654
|
+
} finally {
|
|
14655
|
+
releaseWorkflowWriteLock?.();
|
|
14656
|
+
}
|
|
14657
|
+
return;
|
|
14658
|
+
}
|
|
13716
14659
|
if (req.method === "GET" && url.pathname === "/api/prd-workflow/snapshot") {
|
|
13717
14660
|
try {
|
|
13718
14661
|
const tapdId = String(url.searchParams.get("tapdId") || "").trim();
|
|
@@ -14534,6 +15477,11 @@ export function startUiServer({
|
|
|
14534
15477
|
json(res, 400, { error: `Unsupported workflow namespace: ${report.workflow.namespace}` });
|
|
14535
15478
|
return;
|
|
14536
15479
|
}
|
|
15480
|
+
const adminVersionRepair = prdWorkflowAdminVersionRepairIntent(payload, report, userCtx);
|
|
15481
|
+
if (adminVersionRepair.error) {
|
|
15482
|
+
json(res, adminVersionRepair.status || 400, { error: adminVersionRepair.error });
|
|
15483
|
+
return;
|
|
15484
|
+
}
|
|
14537
15485
|
const tapdId = report.workflow.id;
|
|
14538
15486
|
const flowId = report.flowId;
|
|
14539
15487
|
const flowSource = report.flowSource || "user";
|
|
@@ -14544,12 +15492,12 @@ export function startUiServer({
|
|
|
14544
15492
|
flowId,
|
|
14545
15493
|
flowSource,
|
|
14546
15494
|
archived,
|
|
14547
|
-
}, userCtx, "write");
|
|
15495
|
+
}, userCtx, adminVersionRepair.requested ? "admin-version-repair" : "write");
|
|
14548
15496
|
if (workflowScope.error) {
|
|
14549
15497
|
json(res, workflowScope.status || 400, { error: workflowScope.error });
|
|
14550
15498
|
return;
|
|
14551
15499
|
}
|
|
14552
|
-
if (!workflowScope.collaboration) {
|
|
15500
|
+
if (!workflowScope.collaboration && !adminVersionRepair.requested) {
|
|
14553
15501
|
const ensured = ensurePrdWorkflowCollaboration({ tapdId, userId: userCtx.userId });
|
|
14554
15502
|
if (ensured.error) {
|
|
14555
15503
|
json(res, ensured.status || 400, { error: ensured.error });
|
|
@@ -14657,7 +15605,9 @@ export function startUiServer({
|
|
|
14657
15605
|
});
|
|
14658
15606
|
return;
|
|
14659
15607
|
}
|
|
14660
|
-
report =
|
|
15608
|
+
report = adminVersionRepair.requested
|
|
15609
|
+
? prdWorkflowMergeAdminVersionTimeline(report, currentSnapshot)
|
|
15610
|
+
: prdWorkflowMergeProducerTimeline(report, currentSnapshot);
|
|
14661
15611
|
if (report.error) {
|
|
14662
15612
|
json(res, 400, { error: report.error });
|
|
14663
15613
|
return;
|
|
@@ -14704,11 +15654,17 @@ export function startUiServer({
|
|
|
14704
15654
|
getSessionTokenFromRequest(req) || "",
|
|
14705
15655
|
);
|
|
14706
15656
|
prdWorkflowBroadcast(
|
|
14707
|
-
prdWorkflowKey(
|
|
15657
|
+
prdWorkflowKey(
|
|
15658
|
+
adminVersionRepair.requested ? { userId: workflowScope.stateOwnerId } : userCtx,
|
|
15659
|
+
flowSource,
|
|
15660
|
+
flowId,
|
|
15661
|
+
tapdId,
|
|
15662
|
+
),
|
|
14708
15663
|
{ type: "workflow-report", tapdId, workflow: report.workflow, event, observation: Boolean(observation), snapshot },
|
|
14709
15664
|
);
|
|
14710
15665
|
json(res, 200, {
|
|
14711
15666
|
ok: true,
|
|
15667
|
+
...(adminVersionRepair.requested ? { administrativeRepair: report.event.administrativeRepair } : {}),
|
|
14712
15668
|
report,
|
|
14713
15669
|
resourceKeys,
|
|
14714
15670
|
event,
|
|
@@ -15701,48 +16657,7 @@ export function startUiServer({
|
|
|
15701
16657
|
try {
|
|
15702
16658
|
const projectView = String(url.searchParams.get("view") || "all").trim().toLowerCase();
|
|
15703
16659
|
const currentTeam = getTeamForUser(userCtx.userId);
|
|
15704
|
-
const flows =
|
|
15705
|
-
.filter((flow) => (
|
|
15706
|
-
!workspaceFlowCollaborationGuard(
|
|
15707
|
-
flow.id,
|
|
15708
|
-
flow.source || "user",
|
|
15709
|
-
flow.archived === true,
|
|
15710
|
-
userCtx,
|
|
15711
|
-
"read",
|
|
15712
|
-
)
|
|
15713
|
-
))
|
|
15714
|
-
.map((flow) => {
|
|
15715
|
-
const source = flow.source || "user";
|
|
15716
|
-
const collaboration = source === "workspace"
|
|
15717
|
-
? getWorkspaceCollaborationByFlow(flow.id, flow.archived === true)
|
|
15718
|
-
: getWorkspaceCollaborationForProject({
|
|
15719
|
-
flowId: flow.id,
|
|
15720
|
-
flowSource: source,
|
|
15721
|
-
archived: flow.archived === true,
|
|
15722
|
-
ownerId: userCtx.userId,
|
|
15723
|
-
});
|
|
15724
|
-
return collaboration
|
|
15725
|
-
? { ...flow, collaboration: workspaceCollaborationSummaryWithUsers(collaboration, userCtx.userId) }
|
|
15726
|
-
: flow;
|
|
15727
|
-
});
|
|
15728
|
-
const existingCollaborationIds = new Set(flows.map((flow) => flow.collaboration?.id).filter(Boolean));
|
|
15729
|
-
for (const record of listWorkspaceCollaborationsForUser(userCtx.userId)) {
|
|
15730
|
-
const source = record.projectSource || record.flowSource || "workspace";
|
|
15731
|
-
if (source !== "user" || record.ownerId === userCtx.userId) continue;
|
|
15732
|
-
if (existingCollaborationIds.has(record.id)) continue;
|
|
15733
|
-
const ownerFlow = listFlowsJson(root, { userId: record.ownerId })
|
|
15734
|
-
.find((flow) => (
|
|
15735
|
-
flow.id === record.flowId
|
|
15736
|
-
&& (flow.source || "user") === "user"
|
|
15737
|
-
&& Boolean(flow.archived) === Boolean(record.archived)
|
|
15738
|
-
));
|
|
15739
|
-
if (!ownerFlow) continue;
|
|
15740
|
-
flows.push({
|
|
15741
|
-
...ownerFlow,
|
|
15742
|
-
collaboration: workspaceCollaborationSummaryWithUsers(record, userCtx.userId),
|
|
15743
|
-
});
|
|
15744
|
-
existingCollaborationIds.add(record.id);
|
|
15745
|
-
}
|
|
16660
|
+
const flows = listAccessibleProjectFlows(root, userCtx);
|
|
15746
16661
|
const visibleFlows = projectView === "team"
|
|
15747
16662
|
? flows.filter((flow) => (
|
|
15748
16663
|
currentTeam
|
|
@@ -19585,6 +20500,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
19585
20500
|
return;
|
|
19586
20501
|
}
|
|
19587
20502
|
const flowSource = payload.flowSource || "user";
|
|
20503
|
+
const requestedRunId = typeof payload.runId === "string" ? payload.runId.trim() : "";
|
|
19588
20504
|
const collaborationDenied = workspaceFlowCollaborationGuard(
|
|
19589
20505
|
flowId,
|
|
19590
20506
|
flowSource,
|
|
@@ -19599,6 +20515,13 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
19599
20515
|
const runKey = workspaceRunKey(userCtx, flowSource, flowId);
|
|
19600
20516
|
const entry = activeFlowRuns.get(runKey);
|
|
19601
20517
|
if (!entry || !entry.child) {
|
|
20518
|
+
if (requestedRunId) {
|
|
20519
|
+
const cancelled = cancelScheduledRun(root, flowId, requestedRunId, userCtx);
|
|
20520
|
+
if (cancelled.ok && cancelled.updatedWaits > 0) {
|
|
20521
|
+
json(res, 200, { ok: true, cancelledWaitingRun: true, ...cancelled });
|
|
20522
|
+
return;
|
|
20523
|
+
}
|
|
20524
|
+
}
|
|
19602
20525
|
json(res, 404, { error: "该流水线未在运行" });
|
|
19603
20526
|
return;
|
|
19604
20527
|
}
|