@fieldwangai/agentflow 0.1.106 → 0.1.108
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/agent-runners.mjs +5 -0
- package/bin/lib/composer-agent.mjs +3 -0
- package/bin/lib/prd-workflow-collaboration.mjs +69 -0
- package/bin/lib/ui-server.mjs +612 -52
- package/bin/lib/workflow-report.mjs +431 -0
- package/builtin/web-ui/dist/assets/index-BZMWrBdo.css +1 -0
- package/builtin/web-ui/dist/assets/index-ByS_mRkw.js +348 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/agentflow-cli/SKILL.md +72 -1
- package/skills/agentflow-cli/agents/openai.yaml +2 -2
- package/skills/agentflow-cli/scripts/agentflow-cli.mjs +83 -0
- package/builtin/web-ui/dist/assets/index-CC1ceU6X.css +0 -1
- package/builtin/web-ui/dist/assets/index-CkfM-0Xz.js +0 -348
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const WORKFLOW_REPORT_SCHEMA_VERSION = 1;
|
|
4
|
+
const WORKFLOW_ACTION_STATUSES = new Set([
|
|
5
|
+
"pending",
|
|
6
|
+
"running",
|
|
7
|
+
"done",
|
|
8
|
+
"error",
|
|
9
|
+
"conflict",
|
|
10
|
+
"skipped",
|
|
11
|
+
"cancelled",
|
|
12
|
+
"observed",
|
|
13
|
+
]);
|
|
14
|
+
const UNSAFE_OBJECT_KEYS = new Set(["__proto__", "prototype", "constructor"]);
|
|
15
|
+
|
|
16
|
+
function plainObject(value) {
|
|
17
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function cleanString(value, max = 4000) {
|
|
21
|
+
return String(value ?? "").trim().slice(0, max);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function stableValue(value) {
|
|
25
|
+
if (Array.isArray(value)) return value.map((item) => stableValue(item));
|
|
26
|
+
if (value && typeof value === "object") {
|
|
27
|
+
return Object.fromEntries(
|
|
28
|
+
Object.keys(value)
|
|
29
|
+
.filter((key) => !UNSAFE_OBJECT_KEYS.has(key))
|
|
30
|
+
.sort()
|
|
31
|
+
.map((key) => [key, stableValue(value[key])]),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function valueKey(value) {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.stringify(stableValue(value));
|
|
40
|
+
} catch {
|
|
41
|
+
return String(value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function uniqueValues(values = []) {
|
|
46
|
+
const out = [];
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
for (const value of values) {
|
|
49
|
+
if (value == null || value === "") continue;
|
|
50
|
+
const key = valueKey(value);
|
|
51
|
+
if (!key || seen.has(key)) continue;
|
|
52
|
+
seen.add(key);
|
|
53
|
+
out.push(value);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizeActionStatus(value) {
|
|
59
|
+
const raw = cleanString(value, 40).toLowerCase();
|
|
60
|
+
const aliases = {
|
|
61
|
+
success: "done",
|
|
62
|
+
succeeded: "done",
|
|
63
|
+
complete: "done",
|
|
64
|
+
completed: "done",
|
|
65
|
+
failed: "error",
|
|
66
|
+
failure: "error",
|
|
67
|
+
canceled: "cancelled",
|
|
68
|
+
in_progress: "running",
|
|
69
|
+
"in-progress": "running",
|
|
70
|
+
};
|
|
71
|
+
const normalized = aliases[raw] || raw || "pending";
|
|
72
|
+
return WORKFLOW_ACTION_STATUSES.has(normalized) ? normalized : "pending";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeStringList(value, maxItems = 100) {
|
|
76
|
+
const list = Array.isArray(value) ? value : value == null || value === "" ? [] : [value];
|
|
77
|
+
return uniqueValues(list.map((item) => cleanString(item, 240)).filter(Boolean)).slice(0, maxItems);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeWorkflowArtifact(value, index = 0, defaultScope = "action") {
|
|
81
|
+
const raw = plainObject(value);
|
|
82
|
+
const url = cleanString(raw.url || raw.href, 4000);
|
|
83
|
+
const path = cleanString(raw.path, 4000);
|
|
84
|
+
const title = cleanString(raw.title || raw.label || raw.name || url || path || `Artifact ${index + 1}`, 500);
|
|
85
|
+
const type = cleanString(raw.type || raw.kind || "artifact", 120).toLowerCase() || "artifact";
|
|
86
|
+
const key = cleanString(
|
|
87
|
+
raw.key ||
|
|
88
|
+
raw.id ||
|
|
89
|
+
[type, url || path || title].filter(Boolean).join(":") ||
|
|
90
|
+
`artifact-${index + 1}`,
|
|
91
|
+
500,
|
|
92
|
+
);
|
|
93
|
+
const scope = cleanString(raw.scope || defaultScope, 40).toLowerCase() === "global" ? "global" : "action";
|
|
94
|
+
return {
|
|
95
|
+
...raw,
|
|
96
|
+
key,
|
|
97
|
+
type,
|
|
98
|
+
kind: cleanString(raw.kind || type, 120) || type,
|
|
99
|
+
title,
|
|
100
|
+
label: cleanString(raw.label || title, 500) || title,
|
|
101
|
+
...(url ? { url } : {}),
|
|
102
|
+
...(path ? { path } : {}),
|
|
103
|
+
scope,
|
|
104
|
+
status: cleanString(raw.status, 80),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function mergeWorkflowGlobalState(base, patch) {
|
|
109
|
+
if (!patch || typeof patch !== "object" || Array.isArray(patch)) return plainObject(base);
|
|
110
|
+
const out = { ...plainObject(base) };
|
|
111
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
112
|
+
if (UNSAFE_OBJECT_KEYS.has(key)) continue;
|
|
113
|
+
if (value === undefined) continue;
|
|
114
|
+
if (value === null) {
|
|
115
|
+
delete out[key];
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
119
|
+
out[key] = mergeWorkflowGlobalState(out[key], value);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
out[key] = value;
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function removeWorkflowGlobalStatePath(value, rawPath) {
|
|
128
|
+
const root = mergeWorkflowGlobalState({}, value);
|
|
129
|
+
const parts = cleanString(rawPath, 1000).split(".").map((part) => part.trim()).filter(Boolean);
|
|
130
|
+
if (!parts.length) return root;
|
|
131
|
+
if (parts.some((part) => UNSAFE_OBJECT_KEYS.has(part))) return root;
|
|
132
|
+
let cursor = root;
|
|
133
|
+
for (const part of parts.slice(0, -1)) {
|
|
134
|
+
if (!cursor[part] || typeof cursor[part] !== "object" || Array.isArray(cursor[part])) return root;
|
|
135
|
+
cursor = cursor[part];
|
|
136
|
+
}
|
|
137
|
+
delete cursor[parts[parts.length - 1]];
|
|
138
|
+
return root;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function normalizeWorkflowReference(payload = {}) {
|
|
142
|
+
const workflow = plainObject(payload.workflow);
|
|
143
|
+
const explicitKey = cleanString(workflow.key || payload.workflowKey || payload.workflow_key, 400);
|
|
144
|
+
const keySeparator = explicitKey.indexOf(":");
|
|
145
|
+
const keyedNamespace = keySeparator > 0 ? explicitKey.slice(0, keySeparator) : "";
|
|
146
|
+
const keyedId = keySeparator > 0 ? explicitKey.slice(keySeparator + 1) : explicitKey;
|
|
147
|
+
const namespace = cleanString(
|
|
148
|
+
workflow.namespace || workflow.type || payload.workflowNamespace || payload.workflow_namespace || keyedNamespace || "tapd",
|
|
149
|
+
80,
|
|
150
|
+
).toLowerCase() || "tapd";
|
|
151
|
+
const id = cleanString(
|
|
152
|
+
workflow.id ||
|
|
153
|
+
keyedId ||
|
|
154
|
+
payload.workflowId ||
|
|
155
|
+
payload.workflow_id ||
|
|
156
|
+
payload.tapdId ||
|
|
157
|
+
payload.tapd_id,
|
|
158
|
+
240,
|
|
159
|
+
);
|
|
160
|
+
if (!/^[a-z][a-z0-9._-]{0,79}$/.test(namespace)) {
|
|
161
|
+
return { error: "Invalid workflow namespace" };
|
|
162
|
+
}
|
|
163
|
+
if (!id) return { error: "Missing workflow id" };
|
|
164
|
+
return {
|
|
165
|
+
namespace,
|
|
166
|
+
id,
|
|
167
|
+
key: `${namespace}:${id}`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function normalizeWorkflowReport(payload = {}) {
|
|
172
|
+
const schemaVersion = Number(payload.schemaVersion || payload.schema_version || WORKFLOW_REPORT_SCHEMA_VERSION);
|
|
173
|
+
if (schemaVersion !== WORKFLOW_REPORT_SCHEMA_VERSION) {
|
|
174
|
+
return { error: `Unsupported workflow report schemaVersion: ${schemaVersion}` };
|
|
175
|
+
}
|
|
176
|
+
const workflow = normalizeWorkflowReference(payload);
|
|
177
|
+
if (workflow.error) return workflow;
|
|
178
|
+
const rawWorkflow = plainObject(payload.workflow);
|
|
179
|
+
|
|
180
|
+
const rawAction = plainObject(payload.action);
|
|
181
|
+
const hasAction = Object.keys(rawAction).length > 0;
|
|
182
|
+
const actionKey = cleanString(rawAction.key || rawAction.id || rawAction.actionKey || rawAction.action_key, 240);
|
|
183
|
+
if (hasAction && !actionKey) return { error: "Workflow action requires a stable key" };
|
|
184
|
+
const action = hasAction ? {
|
|
185
|
+
...rawAction,
|
|
186
|
+
key: actionKey,
|
|
187
|
+
title: cleanString(rawAction.title || rawAction.label || actionKey, 500),
|
|
188
|
+
detail: cleanString(rawAction.detail || rawAction.description || rawAction.message, 4000),
|
|
189
|
+
status: normalizeActionStatus(rawAction.status),
|
|
190
|
+
group: cleanString(rawAction.group || rawAction.stage || rawAction.category, 120),
|
|
191
|
+
scope: cleanString(rawAction.scope, 80),
|
|
192
|
+
platform: cleanString(rawAction.platform, 80),
|
|
193
|
+
issueKey: cleanString(rawAction.issueKey || rawAction.issue_key, 240),
|
|
194
|
+
tags: normalizeStringList(rawAction.tags),
|
|
195
|
+
occurredAt: cleanString(
|
|
196
|
+
rawAction.occurredAt ||
|
|
197
|
+
rawAction.occurred_at ||
|
|
198
|
+
rawAction.completedAt ||
|
|
199
|
+
rawAction.startedAt,
|
|
200
|
+
80,
|
|
201
|
+
),
|
|
202
|
+
} : null;
|
|
203
|
+
|
|
204
|
+
const rawArtifacts = Array.isArray(payload.artifacts) ? payload.artifacts : [];
|
|
205
|
+
const artifacts = rawArtifacts
|
|
206
|
+
.filter((item) => item && typeof item === "object" && !Array.isArray(item))
|
|
207
|
+
.map((item, index) => normalizeWorkflowArtifact(item, index, action ? "action" : "global"));
|
|
208
|
+
|
|
209
|
+
const rawGlobalState = plainObject(payload.globalState || payload.global_state);
|
|
210
|
+
const hasGlobalState = Object.keys(rawGlobalState).length > 0;
|
|
211
|
+
const globalStatePatch = plainObject(rawGlobalState.patch);
|
|
212
|
+
const globalStateRemove = normalizeStringList(rawGlobalState.remove || rawGlobalState.removePaths || rawGlobalState.remove_paths);
|
|
213
|
+
const mode = cleanString(rawGlobalState.mode || "merge", 40).toLowerCase() || "merge";
|
|
214
|
+
if (hasGlobalState && mode !== "merge") return { error: "globalState.mode must be merge" };
|
|
215
|
+
if (hasGlobalState && !Object.keys(globalStatePatch).length && !globalStateRemove.length) {
|
|
216
|
+
return { error: "globalState requires patch or remove" };
|
|
217
|
+
}
|
|
218
|
+
if (!action && !artifacts.length && !hasGlobalState) {
|
|
219
|
+
return { error: "Workflow report requires action, artifacts, or globalState" };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const idempotencyKey = cleanString(
|
|
223
|
+
payload.idempotencyKey ||
|
|
224
|
+
payload.idempotency_key ||
|
|
225
|
+
action?.idempotencyKey ||
|
|
226
|
+
action?.idempotency_key,
|
|
227
|
+
500,
|
|
228
|
+
);
|
|
229
|
+
const event = {
|
|
230
|
+
schemaVersion,
|
|
231
|
+
type: "workflow-report",
|
|
232
|
+
source: cleanString(payload.source || "agentflow-cli", 120) || "agentflow-cli",
|
|
233
|
+
workflow,
|
|
234
|
+
workflowKey: workflow.key,
|
|
235
|
+
aggregateByStage: Boolean(action),
|
|
236
|
+
conflictOnArtifact: payload.conflictOnArtifact === true || payload.conflict_on_artifact === true,
|
|
237
|
+
artifactScope: action ? "action" : "global",
|
|
238
|
+
...(action ? {
|
|
239
|
+
action: action.key,
|
|
240
|
+
actionId: action.key,
|
|
241
|
+
actionModel: action,
|
|
242
|
+
stageKey: action.key,
|
|
243
|
+
stage: action.group || action.key,
|
|
244
|
+
title: action.title,
|
|
245
|
+
detail: action.detail,
|
|
246
|
+
status: action.status,
|
|
247
|
+
scope: action.scope || action.group || "workflow",
|
|
248
|
+
platform: action.platform,
|
|
249
|
+
issueKey: action.issueKey,
|
|
250
|
+
tags: action.tags,
|
|
251
|
+
...(action.occurredAt ? { occurredAt: action.occurredAt } : {}),
|
|
252
|
+
} : {
|
|
253
|
+
title: cleanString(payload.title || "Workflow 全局状态更新", 500),
|
|
254
|
+
detail: cleanString(payload.detail, 4000),
|
|
255
|
+
status: "done",
|
|
256
|
+
scope: "global",
|
|
257
|
+
}),
|
|
258
|
+
artifacts,
|
|
259
|
+
...(hasGlobalState ? {
|
|
260
|
+
globalStatePatch,
|
|
261
|
+
globalStateRemove,
|
|
262
|
+
} : {}),
|
|
263
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
264
|
+
};
|
|
265
|
+
return {
|
|
266
|
+
schemaVersion,
|
|
267
|
+
workflow,
|
|
268
|
+
action,
|
|
269
|
+
artifacts,
|
|
270
|
+
globalState: hasGlobalState ? {
|
|
271
|
+
mode: "merge",
|
|
272
|
+
patch: globalStatePatch,
|
|
273
|
+
remove: globalStateRemove,
|
|
274
|
+
} : null,
|
|
275
|
+
expectedRevision: cleanString(payload.expectedRevision || payload.expected_revision, 500),
|
|
276
|
+
idempotencyKey,
|
|
277
|
+
flowId: cleanString(payload.flowId || payload.flow_id || rawWorkflow.flowId || rawWorkflow.flow_id, 240),
|
|
278
|
+
flowSource: cleanString(payload.flowSource || payload.flow_source || rawWorkflow.flowSource || rawWorkflow.flow_source || "user", 80) || "user",
|
|
279
|
+
event,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function displayValue(value) {
|
|
284
|
+
if (value == null) return "";
|
|
285
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
286
|
+
const raw = plainObject(value);
|
|
287
|
+
return raw.label || raw.name || raw.value || raw.username || raw.userId || raw.key || raw.code || "";
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function legacyExperimentLabel(value) {
|
|
291
|
+
const raw = plainObject(value);
|
|
292
|
+
if (!Object.keys(raw).length) return displayValue(value);
|
|
293
|
+
const name = displayValue(raw);
|
|
294
|
+
const groups = Array.isArray(raw.groups || raw.variants || raw.buckets)
|
|
295
|
+
? (raw.groups || raw.variants || raw.buckets).map(displayValue).filter(Boolean)
|
|
296
|
+
: [];
|
|
297
|
+
return [name, groups.length ? groups.join(" / ") : ""].filter(Boolean).join(" · ");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function legacySettingLabel(value) {
|
|
301
|
+
const raw = plainObject(value);
|
|
302
|
+
if (!Object.keys(raw).length) return displayValue(value);
|
|
303
|
+
const name = displayValue(raw);
|
|
304
|
+
const defaultValue = raw.defaultValue ?? raw.default_value ?? raw.default;
|
|
305
|
+
return [name, defaultValue != null && defaultValue !== "" ? `默认 ${String(defaultValue)}` : ""].filter(Boolean).join(" · ");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function globalField(label, type, value) {
|
|
309
|
+
const empty = value == null || value === "" || (Array.isArray(value) && value.length === 0);
|
|
310
|
+
return empty ? null : { label, type, value };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function legacyOverallToGlobalState(tapdId, value = {}) {
|
|
314
|
+
const overall = plainObject(value);
|
|
315
|
+
const requirement = plainObject(overall.requirement);
|
|
316
|
+
const id = cleanString(requirement.tapdId || requirement.tapd_id || tapdId, 240);
|
|
317
|
+
const sections = {};
|
|
318
|
+
for (const [rawPlatform, rawValue] of Object.entries(plainObject(overall.platforms))) {
|
|
319
|
+
const platform = cleanString(rawPlatform, 80).toLowerCase();
|
|
320
|
+
if (!platform) continue;
|
|
321
|
+
const platformValue = plainObject(rawValue);
|
|
322
|
+
const filters = plainObject(platformValue.filters);
|
|
323
|
+
const fields = Object.fromEntries([
|
|
324
|
+
["owner", globalField("负责人", "user", platformValue.owner)],
|
|
325
|
+
["tags", globalField("Tag", "chips", platformValue.tags)],
|
|
326
|
+
["experiments", globalField("AB 实验", "chips", (Array.isArray(platformValue.experiments) ? platformValue.experiments : []).map(legacyExperimentLabel).filter(Boolean))],
|
|
327
|
+
["settings", globalField("Settings", "chips", (Array.isArray(platformValue.settings) ? platformValue.settings : []).map(legacySettingLabel).filter(Boolean))],
|
|
328
|
+
["countries", globalField("国家过滤", "chips", filters.countries)],
|
|
329
|
+
["users", globalField("用户过滤", "chips", filters.users)],
|
|
330
|
+
["versions", globalField("版本过滤", "chips", filters.versions)],
|
|
331
|
+
["rules", globalField("实现规则", "list", platformValue.rules)],
|
|
332
|
+
].filter(([, field]) => field));
|
|
333
|
+
if (Object.keys(fields).length) {
|
|
334
|
+
sections[platform] = {
|
|
335
|
+
title: platform === "android" ? "Android" : platform === "ios" ? "iOS" : rawPlatform,
|
|
336
|
+
fields,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const title = cleanString(requirement.title || requirement.name, 500);
|
|
341
|
+
const url = cleanString(requirement.tapdUrl || requirement.tapd_url || requirement.url, 4000);
|
|
342
|
+
const status = requirement.status || requirement.tapdStatus || requirement.tapd_status || "";
|
|
343
|
+
return {
|
|
344
|
+
workflow: { namespace: "tapd", id, key: `tapd:${id}` },
|
|
345
|
+
...(title ? { title } : {}),
|
|
346
|
+
...(url ? { url } : {}),
|
|
347
|
+
...(status ? { status } : {}),
|
|
348
|
+
sections,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function eventTime(event) {
|
|
353
|
+
const value = event?.updatedAt || event?.occurredAt || event?.completedAt || event?.createdAt || event?.observedAt || "";
|
|
354
|
+
const parsed = Date.parse(value);
|
|
355
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function materializeWorkflowGlobalState(tapdId, snapshot = {}, runtimeEvents = [], legacyOverall = {}) {
|
|
359
|
+
const legacy = legacyOverallToGlobalState(tapdId, legacyOverall);
|
|
360
|
+
const raw = plainObject(
|
|
361
|
+
snapshot.globalState ||
|
|
362
|
+
snapshot.global_state ||
|
|
363
|
+
snapshot.raw?.globalState ||
|
|
364
|
+
snapshot.raw?.global_state,
|
|
365
|
+
);
|
|
366
|
+
let state = mergeWorkflowGlobalState(legacy, raw);
|
|
367
|
+
const events = [...(Array.isArray(runtimeEvents) ? runtimeEvents : [])].sort((left, right) => eventTime(left) - eventTime(right));
|
|
368
|
+
for (const event of events) {
|
|
369
|
+
const patch = event?.globalStatePatch || event?.global_state_patch;
|
|
370
|
+
if (patch && typeof patch === "object" && !Array.isArray(patch)) {
|
|
371
|
+
state = mergeWorkflowGlobalState(state, patch);
|
|
372
|
+
}
|
|
373
|
+
const remove = event?.globalStateRemove || event?.global_state_remove;
|
|
374
|
+
for (const path of Array.isArray(remove) ? remove : []) {
|
|
375
|
+
state = removeWorkflowGlobalStatePath(state, path);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const workflow = normalizeWorkflowReference({ workflow: state.workflow, tapdId });
|
|
379
|
+
state.workflow = workflow.error ? { namespace: "tapd", id: String(tapdId || ""), key: `tapd:${String(tapdId || "")}` } : workflow;
|
|
380
|
+
return state;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function mergeWorkflowArtifacts(baseArtifacts = [], runtimeEvents = []) {
|
|
384
|
+
const globalArtifacts = [];
|
|
385
|
+
for (const event of Array.isArray(runtimeEvents) ? runtimeEvents : []) {
|
|
386
|
+
const defaultScope = event?.artifactScope === "global" || !event?.action ? "global" : "action";
|
|
387
|
+
for (const artifact of Array.isArray(event?.artifacts) ? event.artifacts : []) {
|
|
388
|
+
const scope = cleanString(artifact?.scope || defaultScope, 40).toLowerCase();
|
|
389
|
+
if (scope === "global") globalArtifacts.push(artifact);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return mergeWorkflowArtifactLists(baseArtifacts, globalArtifacts, "global");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export function mergeWorkflowArtifactLists(left = [], right = [], defaultScope = "action") {
|
|
396
|
+
const out = [];
|
|
397
|
+
const seen = new Map();
|
|
398
|
+
const add = (artifact) => {
|
|
399
|
+
if (!artifact || typeof artifact !== "object" || Array.isArray(artifact)) return;
|
|
400
|
+
const normalized = normalizeWorkflowArtifact(artifact, out.length, defaultScope);
|
|
401
|
+
const key = normalized.key || normalized.url || `${normalized.type}:${normalized.title}`;
|
|
402
|
+
const existing = seen.get(key);
|
|
403
|
+
if (existing == null) {
|
|
404
|
+
seen.set(key, out.length);
|
|
405
|
+
out.push(normalized);
|
|
406
|
+
} else {
|
|
407
|
+
out[existing] = { ...out[existing], ...normalized };
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
for (const artifact of Array.isArray(left) ? left : []) add(artifact);
|
|
411
|
+
for (const artifact of Array.isArray(right) ? right : []) add(artifact);
|
|
412
|
+
return out;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function workflowRuntimeRevision(globalState = {}, artifacts = [], runtimeEvents = []) {
|
|
416
|
+
const events = (Array.isArray(runtimeEvents) ? runtimeEvents : []).map((event) => ({
|
|
417
|
+
id: event?.id || "",
|
|
418
|
+
action: event?.action || event?.actionId || "",
|
|
419
|
+
stageKey: event?.stageKey || event?.stage || "",
|
|
420
|
+
status: event?.status || "",
|
|
421
|
+
artifacts: event?.artifacts || [],
|
|
422
|
+
globalStatePatch: event?.globalStatePatch || {},
|
|
423
|
+
globalStateRemove: event?.globalStateRemove || [],
|
|
424
|
+
}));
|
|
425
|
+
const hash = crypto
|
|
426
|
+
.createHash("sha256")
|
|
427
|
+
.update(JSON.stringify(stableValue({ globalState, artifacts, events })))
|
|
428
|
+
.digest("hex")
|
|
429
|
+
.slice(0, 24);
|
|
430
|
+
return `runtime:${hash}`;
|
|
431
|
+
}
|