@fieldwangai/agentflow 0.1.135 → 0.1.137
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/prd-workflow-collaboration.mjs +172 -17
- package/bin/lib/ui-server.mjs +869 -80
- package/bin/lib/workflow-report.mjs +357 -36
- package/builtin/web-ui/dist/assets/index-CQsrSc3u.css +1 -0
- package/builtin/web-ui/dist/assets/index-DQvqqAeQ.js +590 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/agentflow-cli/SKILL.md +3 -1
- package/skills/agentflow-cli/scripts/agentflow-cli.mjs +55 -6
- package/skills/agentflow-cli/scripts/workflow-report-client.mjs +71 -0
- package/skills/agentflow-workflow-report/SKILL.md +40 -17
- package/skills/agentflow-workflow-report/references/protocol.md +529 -235
- package/builtin/web-ui/dist/assets/index-BFQVTav-.css +0 -1
- package/builtin/web-ui/dist/assets/index-CmpbCHAj.js +0 -420
|
@@ -21,6 +21,14 @@ function cleanString(value, max = 4000) {
|
|
|
21
21
|
return String(value ?? "").trim().slice(0, max);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function rawString(value) {
|
|
25
|
+
return String(value ?? "").trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function stringExceeds(value, max) {
|
|
29
|
+
return rawString(value).length > max;
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
function hasOwn(value, key) {
|
|
25
33
|
return Boolean(value && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, key));
|
|
26
34
|
}
|
|
@@ -76,6 +84,26 @@ function normalizeActionStatus(value) {
|
|
|
76
84
|
return WORKFLOW_ACTION_STATUSES.has(normalized) ? normalized : "pending";
|
|
77
85
|
}
|
|
78
86
|
|
|
87
|
+
function isKnownActionStatus(value) {
|
|
88
|
+
const raw = cleanString(value, 40).toLowerCase();
|
|
89
|
+
if (!raw) return true;
|
|
90
|
+
return WORKFLOW_ACTION_STATUSES.has(raw) || [
|
|
91
|
+
"success", "succeeded", "complete", "completed", "failed", "failure", "canceled", "in_progress", "in-progress",
|
|
92
|
+
].includes(raw);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function isSafeWorkflowUrl(value, { allowRelative = true } = {}) {
|
|
96
|
+
const raw = rawString(value);
|
|
97
|
+
if (!raw) return true;
|
|
98
|
+
if (allowRelative && raw.startsWith("/") && !raw.startsWith("//")) return true;
|
|
99
|
+
try {
|
|
100
|
+
const parsed = new URL(raw);
|
|
101
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
79
107
|
function normalizeStringList(value, maxItems = 100) {
|
|
80
108
|
const list = Array.isArray(value) ? value : value == null || value === "" ? [] : [value];
|
|
81
109
|
return uniqueValues(list.map((item) => cleanString(item, 240)).filter(Boolean)).slice(0, maxItems);
|
|
@@ -136,14 +164,39 @@ function normalizeWorkflowProjectionState(value = {}) {
|
|
|
136
164
|
if (Array.isArray(value?.timeline)) {
|
|
137
165
|
raw.timeline = value.timeline
|
|
138
166
|
.map((item, index) => normalizeWorkflowTimelineProjection(item, index))
|
|
139
|
-
.filter(Boolean)
|
|
140
|
-
.slice(0, 100);
|
|
167
|
+
.filter(Boolean);
|
|
141
168
|
} else {
|
|
142
169
|
delete raw.timeline;
|
|
143
170
|
}
|
|
144
171
|
return raw;
|
|
145
172
|
}
|
|
146
173
|
|
|
174
|
+
export function normalizeWorkflowExtensions(value = {}) {
|
|
175
|
+
const raw = plainObject(value);
|
|
176
|
+
const out = {};
|
|
177
|
+
for (const [namespace, extension] of Object.entries(raw)) {
|
|
178
|
+
const key = cleanString(namespace, 120).toLowerCase();
|
|
179
|
+
if (!key || !/^[a-z][a-z0-9._-]{0,119}$/.test(key)) continue;
|
|
180
|
+
if (!extension || typeof extension !== "object" || Array.isArray(extension)) continue;
|
|
181
|
+
out[key] = mergeWorkflowGlobalState({}, extension);
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function materializeWorkflowExtensions(snapshot = {}, runtimeEvents = []) {
|
|
187
|
+
let extensions = normalizeWorkflowExtensions(
|
|
188
|
+
snapshot.extensions || snapshot.workflowExtensions || snapshot.workflow_extensions || {},
|
|
189
|
+
);
|
|
190
|
+
const events = [...(Array.isArray(runtimeEvents) ? runtimeEvents : [])]
|
|
191
|
+
.sort((left, right) => eventTime(left) - eventTime(right));
|
|
192
|
+
for (const event of events) {
|
|
193
|
+
const patch = event?.extensionsPatch || event?.extensions_patch;
|
|
194
|
+
if (!patch || typeof patch !== "object" || Array.isArray(patch)) continue;
|
|
195
|
+
extensions = mergeWorkflowGlobalState(extensions, normalizeWorkflowExtensions(patch));
|
|
196
|
+
}
|
|
197
|
+
return extensions;
|
|
198
|
+
}
|
|
199
|
+
|
|
147
200
|
export function materializeWorkflowProjections(snapshot = {}, runtimeEvents = []) {
|
|
148
201
|
const base =
|
|
149
202
|
snapshot.projections ||
|
|
@@ -163,7 +216,6 @@ export function materializeWorkflowProjections(snapshot = {}, runtimeEvents = []
|
|
|
163
216
|
? next.timeline
|
|
164
217
|
.map((item, index) => normalizeWorkflowTimelineProjection(item, index))
|
|
165
218
|
.filter(Boolean)
|
|
166
|
-
.slice(0, 100)
|
|
167
219
|
: projections.timeline || [],
|
|
168
220
|
};
|
|
169
221
|
}
|
|
@@ -204,11 +256,22 @@ export function removeWorkflowGlobalStatePath(value, rawPath) {
|
|
|
204
256
|
}
|
|
205
257
|
|
|
206
258
|
export function normalizeWorkflowReference(payload = {}) {
|
|
207
|
-
const
|
|
208
|
-
const
|
|
259
|
+
const workflowInput = payload.workflow;
|
|
260
|
+
const workflow = plainObject(workflowInput);
|
|
261
|
+
const rawExplicitKey = rawString(
|
|
262
|
+
(typeof workflowInput === "string" ? workflowInput : "") || workflow.key || payload.workflowKey || payload.workflow_key,
|
|
263
|
+
);
|
|
264
|
+
if (rawExplicitKey.length > 400) return { error: "Workflow key exceeds 400 characters" };
|
|
265
|
+
if (/[\0\r\n]/.test(rawExplicitKey)) return { error: "Workflow key contains control characters" };
|
|
266
|
+
const explicitKey = rawExplicitKey;
|
|
209
267
|
const keySeparator = explicitKey.indexOf(":");
|
|
210
268
|
const keyedNamespace = keySeparator > 0 ? explicitKey.slice(0, keySeparator) : "";
|
|
211
269
|
const keyedId = keySeparator > 0 ? explicitKey.slice(keySeparator + 1) : explicitKey;
|
|
270
|
+
const rawNamespace = rawString(workflow.namespace || workflow.type || payload.workflowNamespace || payload.workflow_namespace || keyedNamespace || "tapd");
|
|
271
|
+
const rawId = rawString(workflow.id || keyedId || payload.workflowId || payload.workflow_id || payload.tapdId || payload.tapd_id);
|
|
272
|
+
if (rawNamespace.length > 80) return { error: "Workflow namespace exceeds 80 characters" };
|
|
273
|
+
if (rawId.length > 240) return { error: "Workflow id exceeds 240 characters" };
|
|
274
|
+
if (/[\0\r\n]/.test(rawId)) return { error: "Workflow id contains control characters" };
|
|
212
275
|
const namespace = cleanString(
|
|
213
276
|
workflow.namespace || workflow.type || payload.workflowNamespace || payload.workflow_namespace || keyedNamespace || "tapd",
|
|
214
277
|
80,
|
|
@@ -234,29 +297,70 @@ export function normalizeWorkflowReference(payload = {}) {
|
|
|
234
297
|
}
|
|
235
298
|
|
|
236
299
|
export function normalizeWorkflowReport(payload = {}) {
|
|
237
|
-
const
|
|
300
|
+
const schemaValue = payload.schemaVersion ?? payload.schema_version ?? WORKFLOW_REPORT_SCHEMA_VERSION;
|
|
301
|
+
const schemaVersion = Number(schemaValue);
|
|
238
302
|
if (schemaVersion !== WORKFLOW_REPORT_SCHEMA_VERSION) {
|
|
239
303
|
return { error: `Unsupported workflow report schemaVersion: ${schemaVersion}` };
|
|
240
304
|
}
|
|
241
305
|
const workflow = normalizeWorkflowReference(payload);
|
|
242
306
|
if (workflow.error) return workflow;
|
|
243
307
|
const rawWorkflow = plainObject(payload.workflow);
|
|
308
|
+
if (!rawString(payload.source)) return { error: "Workflow report requires source" };
|
|
309
|
+
if (stringExceeds(payload.source, 120)) return { error: "Workflow report source exceeds 120 characters" };
|
|
310
|
+
const source = cleanString(payload.source, 120).toLowerCase();
|
|
311
|
+
if (!/^[a-z][a-z0-9._-]{0,119}$/.test(source)) {
|
|
312
|
+
return { error: "Invalid workflow report source" };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const rawObservation = plainObject(payload.observation);
|
|
316
|
+
const hasObservation = Object.keys(rawObservation).length > 0;
|
|
317
|
+
const observationState = plainObject(rawObservation.state || rawObservation.facts || rawObservation.value);
|
|
318
|
+
if (hasObservation && !Object.keys(observationState).length) {
|
|
319
|
+
return { error: "observation requires state" };
|
|
320
|
+
}
|
|
321
|
+
if (stringExceeds(rawObservation.schema || rawObservation.model, 160)) return { error: "observation.schema exceeds 160 characters" };
|
|
322
|
+
if (stringExceeds(rawObservation.clientId || rawObservation.client_id || source, 160)) return { error: "observation.clientId exceeds 160 characters" };
|
|
323
|
+
if (stringExceeds(rawObservation.scope || "client", 80)) return { error: "observation.scope exceeds 80 characters" };
|
|
324
|
+
const rawObservedAt = rawString(rawObservation.observedAt || rawObservation.observed_at);
|
|
325
|
+
if (rawObservedAt && !Number.isFinite(Date.parse(rawObservedAt))) return { error: "observation.observedAt must be an ISO-compatible date" };
|
|
326
|
+
const observation = hasObservation ? {
|
|
327
|
+
schema: cleanString(rawObservation.schema || rawObservation.model || "workflow-observation/v1", 160),
|
|
328
|
+
state: mergeWorkflowGlobalState({}, observationState),
|
|
329
|
+
observedAt: cleanString(rawObservation.observedAt || rawObservation.observed_at, 80),
|
|
330
|
+
clientId: cleanString(rawObservation.clientId || rawObservation.client_id || source, 160),
|
|
331
|
+
scope: cleanString(rawObservation.scope || "client", 80).toLowerCase() || "client",
|
|
332
|
+
} : null;
|
|
244
333
|
|
|
245
334
|
const rawAction = plainObject(payload.action);
|
|
246
335
|
const hasAction = Object.keys(rawAction).length > 0;
|
|
336
|
+
if (stringExceeds(rawAction.key || rawAction.id || rawAction.actionKey || rawAction.action_key, 240)) {
|
|
337
|
+
return { error: "Workflow action key exceeds 240 characters" };
|
|
338
|
+
}
|
|
247
339
|
const actionKey = cleanString(rawAction.key || rawAction.id || rawAction.actionKey || rawAction.action_key, 240);
|
|
248
340
|
if (hasAction && !actionKey) return { error: "Workflow action requires a stable key" };
|
|
341
|
+
if (hasAction && /[\0\r\n]/.test(actionKey)) return { error: "Workflow action key contains control characters" };
|
|
342
|
+
if (stringExceeds(rawAction.title || rawAction.label || actionKey, 500)) return { error: "Workflow action title exceeds 500 characters" };
|
|
343
|
+
if (stringExceeds(rawAction.detail || rawAction.description || rawAction.message, 4000)) return { error: "Workflow action detail exceeds 4000 characters" };
|
|
344
|
+
if (stringExceeds(rawAction.group || rawAction.stage || rawAction.category, 120)) return { error: "Workflow action group exceeds 120 characters" };
|
|
345
|
+
if (stringExceeds(rawAction.scope, 80)) return { error: "Workflow action scope exceeds 80 characters" };
|
|
346
|
+
if (stringExceeds(rawAction.platform, 80)) return { error: "Workflow action platform exceeds 80 characters" };
|
|
347
|
+
if (stringExceeds(rawAction.issueKey || rawAction.issue_key, 240)) return { error: "Workflow action issueKey exceeds 240 characters" };
|
|
348
|
+
const rawTags = Array.isArray(rawAction.tags) ? rawAction.tags : rawAction.tags == null ? [] : [rawAction.tags];
|
|
349
|
+
if (rawTags.length > 100 || rawTags.some((tag) => stringExceeds(tag, 240))) return { error: "Workflow action tags exceed supported limits" };
|
|
350
|
+
if (hasAction && !isKnownActionStatus(rawAction.status)) return { error: `Invalid workflow action status: ${rawAction.status}` };
|
|
351
|
+
const rawOccurredAt = rawString(rawAction.occurredAt || rawAction.occurred_at || rawAction.completedAt || rawAction.startedAt);
|
|
352
|
+
if (rawOccurredAt && !Number.isFinite(Date.parse(rawOccurredAt))) return { error: "action.occurredAt must be an ISO-compatible date" };
|
|
249
353
|
const action = hasAction ? {
|
|
250
354
|
...rawAction,
|
|
251
355
|
key: actionKey,
|
|
252
356
|
title: cleanString(rawAction.title || rawAction.label || actionKey, 500),
|
|
253
|
-
detail: cleanString(rawAction.detail || rawAction.description || rawAction.message, 4000),
|
|
357
|
+
...(rawAction.detail || rawAction.description || rawAction.message ? { detail: cleanString(rawAction.detail || rawAction.description || rawAction.message, 4000) } : {}),
|
|
254
358
|
status: normalizeActionStatus(rawAction.status),
|
|
255
|
-
group: cleanString(rawAction.group || rawAction.stage || rawAction.category, 120),
|
|
256
|
-
scope: cleanString(rawAction.scope, 80),
|
|
257
|
-
platform: cleanString(rawAction.platform, 80),
|
|
258
|
-
issueKey: cleanString(rawAction.issueKey || rawAction.issue_key, 240),
|
|
259
|
-
tags: normalizeStringList(rawAction.tags),
|
|
359
|
+
...(rawAction.group || rawAction.stage || rawAction.category ? { group: cleanString(rawAction.group || rawAction.stage || rawAction.category, 120) } : {}),
|
|
360
|
+
...(rawAction.scope ? { scope: cleanString(rawAction.scope, 80) } : {}),
|
|
361
|
+
...(rawAction.platform ? { platform: cleanString(rawAction.platform, 80) } : {}),
|
|
362
|
+
...(rawAction.issueKey || rawAction.issue_key ? { issueKey: cleanString(rawAction.issueKey || rawAction.issue_key, 240) } : {}),
|
|
363
|
+
...(rawAction.tags != null ? { tags: normalizeStringList(rawAction.tags) } : {}),
|
|
260
364
|
occurredAt: cleanString(
|
|
261
365
|
rawAction.occurredAt ||
|
|
262
366
|
rawAction.occurred_at ||
|
|
@@ -267,9 +371,37 @@ export function normalizeWorkflowReport(payload = {}) {
|
|
|
267
371
|
} : null;
|
|
268
372
|
|
|
269
373
|
const rawArtifacts = Array.isArray(payload.artifacts) ? payload.artifacts : [];
|
|
374
|
+
if (rawArtifacts.length > 100) return { error: "Workflow report supports at most 100 artifacts per request" };
|
|
375
|
+
const invalidArtifactShapeIndex = rawArtifacts.findIndex((item) => !item || typeof item !== "object" || Array.isArray(item));
|
|
376
|
+
if (invalidArtifactShapeIndex >= 0) return { error: `artifacts[${invalidArtifactShapeIndex}] must be an object` };
|
|
377
|
+
const missingArtifactTargetIndex = rawArtifacts.findIndex((item) => !rawString(item?.url || item?.href) && !rawString(item?.path));
|
|
378
|
+
if (missingArtifactTargetIndex >= 0) return { error: `artifacts[${missingArtifactTargetIndex}] requires url or path` };
|
|
379
|
+
const oversizedArtifactIndex = rawArtifacts.findIndex((item) => (
|
|
380
|
+
stringExceeds(item?.url || item?.href, 4000) ||
|
|
381
|
+
stringExceeds(item?.path, 4000) ||
|
|
382
|
+
stringExceeds(item?.title || item?.label || item?.name, 500) ||
|
|
383
|
+
stringExceeds(item?.type || item?.kind, 120) ||
|
|
384
|
+
stringExceeds(item?.label, 500) ||
|
|
385
|
+
stringExceeds(item?.status, 80)
|
|
386
|
+
));
|
|
387
|
+
if (oversizedArtifactIndex >= 0) return { error: `artifacts[${oversizedArtifactIndex}] exceeds supported field limits` };
|
|
388
|
+
const invalidArtifactIndex = rawArtifacts.findIndex((item) => {
|
|
389
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return false;
|
|
390
|
+
const url = item.url || item.href;
|
|
391
|
+
return Boolean(url) && !isSafeWorkflowUrl(url);
|
|
392
|
+
});
|
|
393
|
+
if (invalidArtifactIndex >= 0) return { error: `artifacts[${invalidArtifactIndex}].url must use http, https, or an absolute application path` };
|
|
394
|
+
const invalidArtifactKeyIndex = rawArtifacts.findIndex((item) => {
|
|
395
|
+
const key = rawString(item?.key || item?.id || item?.artifactKey || item?.artifact_key);
|
|
396
|
+
return key.length > 500 || /[\0\r\n]/.test(key);
|
|
397
|
+
});
|
|
398
|
+
if (invalidArtifactKeyIndex >= 0) return { error: `artifacts[${invalidArtifactKeyIndex}].key is invalid` };
|
|
270
399
|
const artifacts = rawArtifacts
|
|
271
400
|
.filter((item) => item && typeof item === "object" && !Array.isArray(item))
|
|
272
|
-
.map((item, index) =>
|
|
401
|
+
.map((item, index) => ({
|
|
402
|
+
...normalizeWorkflowArtifact(item, index, action ? "action" : "global"),
|
|
403
|
+
producer: source,
|
|
404
|
+
}));
|
|
273
405
|
|
|
274
406
|
const rawProjections = plainObject(payload.projections || payload.workflowProjections || payload.workflow_projections);
|
|
275
407
|
const hasProjections = Object.keys(rawProjections).length > 0 || hasOwn(payload, "projections");
|
|
@@ -279,25 +411,88 @@ export function normalizeWorkflowReport(payload = {}) {
|
|
|
279
411
|
if (hasProjections && !Array.isArray(rawProjections.timeline)) {
|
|
280
412
|
return { error: "projections.timeline must be an array" };
|
|
281
413
|
}
|
|
414
|
+
if (hasProjections && rawProjections.timeline.length > 100) {
|
|
415
|
+
return { error: "projections.timeline supports at most 100 entries" };
|
|
416
|
+
}
|
|
282
417
|
const invalidTimelineIndex = hasProjections
|
|
283
418
|
? rawProjections.timeline.findIndex((item, index) => !normalizeWorkflowTimelineProjection(item, index))
|
|
284
419
|
: -1;
|
|
285
420
|
if (invalidTimelineIndex >= 0) {
|
|
286
421
|
return { error: `projections.timeline[${invalidTimelineIndex}] requires kind and id` };
|
|
287
422
|
}
|
|
288
|
-
const
|
|
423
|
+
const oversizedTimelineIndex = hasProjections
|
|
424
|
+
? rawProjections.timeline.findIndex((item) => (
|
|
425
|
+
stringExceeds(item?.kind || item?.type, 80) ||
|
|
426
|
+
stringExceeds(item?.id || item?.key, 240) ||
|
|
427
|
+
stringExceeds(item?.key, 500) ||
|
|
428
|
+
stringExceeds(item?.title || item?.label, 500) ||
|
|
429
|
+
stringExceeds(item?.source || item?.namespace || source, 120) ||
|
|
430
|
+
stringExceeds(item?.date || item?.targetDate || item?.target_date, 80)
|
|
431
|
+
))
|
|
432
|
+
: -1;
|
|
433
|
+
if (oversizedTimelineIndex >= 0) return { error: `projections.timeline[${oversizedTimelineIndex}] exceeds supported field limits` };
|
|
434
|
+
const invalidTimelineSourceIndex = hasProjections
|
|
435
|
+
? rawProjections.timeline.findIndex((item) => {
|
|
436
|
+
const itemSource = cleanString(item?.source || item?.namespace || source, 120).toLowerCase();
|
|
437
|
+
return !/^[a-z][a-z0-9._-]{0,119}$/.test(itemSource);
|
|
438
|
+
})
|
|
439
|
+
: -1;
|
|
440
|
+
if (invalidTimelineSourceIndex >= 0) return { error: `projections.timeline[${invalidTimelineSourceIndex}].source is invalid` };
|
|
441
|
+
const invalidTimelineDateIndex = hasProjections
|
|
442
|
+
? rawProjections.timeline.findIndex((item) => {
|
|
443
|
+
const date = rawString(item?.date || item?.targetDate || item?.target_date);
|
|
444
|
+
return date && !Number.isFinite(Date.parse(date));
|
|
445
|
+
})
|
|
446
|
+
: -1;
|
|
447
|
+
if (invalidTimelineDateIndex >= 0) return { error: `projections.timeline[${invalidTimelineDateIndex}].date must be ISO-compatible` };
|
|
448
|
+
const projections = hasProjections ? normalizeWorkflowProjectionState({
|
|
449
|
+
...rawProjections,
|
|
450
|
+
timeline: rawProjections.timeline.map((item) => ({
|
|
451
|
+
...plainObject(item),
|
|
452
|
+
source: cleanString(item?.source || item?.namespace || source, 120).toLowerCase(),
|
|
453
|
+
})),
|
|
454
|
+
}) : null;
|
|
455
|
+
|
|
456
|
+
const rawExtensions = plainObject(payload.extensions);
|
|
457
|
+
const hasExtensions = Object.keys(rawExtensions).length > 0;
|
|
458
|
+
const invalidExtensionNamespace = Object.keys(rawExtensions).findIndex((namespace) => (
|
|
459
|
+
!/^[a-z][a-z0-9._-]{0,119}$/.test(rawString(namespace).toLowerCase()) ||
|
|
460
|
+
!rawExtensions[namespace] ||
|
|
461
|
+
typeof rawExtensions[namespace] !== "object" ||
|
|
462
|
+
Array.isArray(rawExtensions[namespace])
|
|
463
|
+
));
|
|
464
|
+
if (invalidExtensionNamespace >= 0) {
|
|
465
|
+
const namespace = Object.keys(rawExtensions)[invalidExtensionNamespace];
|
|
466
|
+
return { error: `extensions[${namespace}] must be a valid namespace object` };
|
|
467
|
+
}
|
|
468
|
+
const extensions = hasExtensions ? normalizeWorkflowExtensions(rawExtensions) : null;
|
|
469
|
+
if (hasExtensions && !Object.keys(extensions).length) {
|
|
470
|
+
return { error: "extensions requires at least one valid namespace object" };
|
|
471
|
+
}
|
|
472
|
+
if (hasExtensions && Object.keys(extensions).some((namespace) => namespace !== source)) {
|
|
473
|
+
return { error: "extensions may only update the namespace matching report source" };
|
|
474
|
+
}
|
|
289
475
|
|
|
290
476
|
const rawGlobalState = plainObject(payload.globalState || payload.global_state);
|
|
291
477
|
const hasGlobalState = Object.keys(rawGlobalState).length > 0;
|
|
292
478
|
const globalStatePatch = plainObject(rawGlobalState.patch);
|
|
293
|
-
const
|
|
479
|
+
const rawGlobalStateRemove = rawGlobalState.remove || rawGlobalState.removePaths || rawGlobalState.remove_paths;
|
|
480
|
+
const rawGlobalStateRemoveList = Array.isArray(rawGlobalStateRemove) ? rawGlobalStateRemove : rawGlobalStateRemove == null || rawGlobalStateRemove === "" ? [] : [rawGlobalStateRemove];
|
|
481
|
+
if (rawGlobalStateRemoveList.length > 100 || rawGlobalStateRemoveList.some((item) => stringExceeds(item, 240))) {
|
|
482
|
+
return { error: "globalState.remove exceeds supported limits" };
|
|
483
|
+
}
|
|
484
|
+
const globalStateRemove = normalizeStringList(rawGlobalStateRemove);
|
|
485
|
+
const globalStateOwnerPaths = uniqueValues([
|
|
486
|
+
...patchLeafPaths(globalStatePatch).filter((path) => path.length).map((path) => path.join(".")),
|
|
487
|
+
...globalStateRemove,
|
|
488
|
+
]);
|
|
294
489
|
const mode = cleanString(rawGlobalState.mode || "merge", 40).toLowerCase() || "merge";
|
|
295
490
|
if (hasGlobalState && mode !== "merge") return { error: "globalState.mode must be merge" };
|
|
296
491
|
if (hasGlobalState && !Object.keys(globalStatePatch).length && !globalStateRemove.length) {
|
|
297
492
|
return { error: "globalState requires patch or remove" };
|
|
298
493
|
}
|
|
299
|
-
if (!action && !artifacts.length && !hasGlobalState && !hasProjections) {
|
|
300
|
-
return { error: "Workflow report requires action, artifacts, globalState, or
|
|
494
|
+
if (!action && !artifacts.length && !hasGlobalState && !hasProjections && !hasExtensions && !hasObservation) {
|
|
495
|
+
return { error: "Workflow report requires observation, action, artifacts, globalState, projections, or extensions" };
|
|
301
496
|
}
|
|
302
497
|
|
|
303
498
|
const idempotencyKey = cleanString(
|
|
@@ -307,10 +502,24 @@ export function normalizeWorkflowReport(payload = {}) {
|
|
|
307
502
|
action?.idempotency_key,
|
|
308
503
|
500,
|
|
309
504
|
);
|
|
505
|
+
if (stringExceeds(payload.idempotencyKey || payload.idempotency_key || action?.idempotencyKey || action?.idempotency_key, 500)) {
|
|
506
|
+
return { error: "idempotencyKey exceeds 500 characters" };
|
|
507
|
+
}
|
|
508
|
+
if (stringExceeds(payload.expectedRevision || payload.expected_revision, 500)) return { error: "expectedRevision exceeds 500 characters" };
|
|
509
|
+
const rawExpectedVersions = plainObject(payload.expectedVersions || payload.expected_versions);
|
|
510
|
+
const expectedVersions = {};
|
|
511
|
+
for (const [resourceKey, version] of Object.entries(rawExpectedVersions)) {
|
|
512
|
+
const key = rawString(resourceKey);
|
|
513
|
+
if (!key || key.length > 800 || /[\0\r\n]/.test(key)) return { error: "Invalid expectedVersions resource key" };
|
|
514
|
+
const normalizedVersion = version == null || version === "" ? "absent" : rawString(version);
|
|
515
|
+
if (normalizedVersion.length > 160) return { error: `expectedVersions[${key}] exceeds 160 characters` };
|
|
516
|
+
expectedVersions[key] = normalizedVersion;
|
|
517
|
+
}
|
|
310
518
|
const event = {
|
|
311
519
|
schemaVersion,
|
|
312
520
|
type: "workflow-report",
|
|
313
|
-
|
|
521
|
+
operation: "report",
|
|
522
|
+
source,
|
|
314
523
|
workflow,
|
|
315
524
|
workflowKey: workflow.key,
|
|
316
525
|
aggregateByStage: Boolean(action),
|
|
@@ -327,9 +536,9 @@ export function normalizeWorkflowReport(payload = {}) {
|
|
|
327
536
|
detail: action.detail,
|
|
328
537
|
status: action.status,
|
|
329
538
|
scope: action.scope || action.group || "workflow",
|
|
330
|
-
platform: action.platform,
|
|
331
|
-
issueKey: action.issueKey,
|
|
332
|
-
tags: action.tags,
|
|
539
|
+
...(action.platform ? { platform: action.platform } : {}),
|
|
540
|
+
...(action.issueKey ? { issueKey: action.issueKey } : {}),
|
|
541
|
+
...(action.tags ? { tags: action.tags } : {}),
|
|
333
542
|
...(action.occurredAt ? { occurredAt: action.occurredAt } : {}),
|
|
334
543
|
} : {
|
|
335
544
|
title: cleanString(payload.title || "Workflow 全局状态更新", 500),
|
|
@@ -339,24 +548,43 @@ export function normalizeWorkflowReport(payload = {}) {
|
|
|
339
548
|
}),
|
|
340
549
|
artifacts,
|
|
341
550
|
...(hasProjections ? { projections } : {}),
|
|
551
|
+
...(hasExtensions ? { extensionsPatch: extensions } : {}),
|
|
342
552
|
...(hasGlobalState ? {
|
|
343
553
|
globalStatePatch,
|
|
344
554
|
globalStateRemove,
|
|
555
|
+
globalStateOwnerPaths,
|
|
345
556
|
} : {}),
|
|
346
557
|
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
347
558
|
};
|
|
559
|
+
if (idempotencyKey) {
|
|
560
|
+
event.idempotencyFingerprint = semanticHash({
|
|
561
|
+
workflow,
|
|
562
|
+
source,
|
|
563
|
+
observation,
|
|
564
|
+
action,
|
|
565
|
+
artifacts,
|
|
566
|
+
globalState: hasGlobalState ? { patch: globalStatePatch, remove: globalStateRemove } : null,
|
|
567
|
+
projections,
|
|
568
|
+
extensions,
|
|
569
|
+
});
|
|
570
|
+
event.idempotencyFingerprints = { [idempotencyKey]: event.idempotencyFingerprint };
|
|
571
|
+
}
|
|
348
572
|
return {
|
|
349
573
|
schemaVersion,
|
|
350
574
|
workflow,
|
|
351
575
|
action,
|
|
352
576
|
artifacts,
|
|
353
577
|
projections,
|
|
578
|
+
extensions,
|
|
579
|
+
observation,
|
|
580
|
+
hasRuntimeUpdate: Boolean(action || artifacts.length || hasGlobalState || hasProjections || hasExtensions),
|
|
354
581
|
globalState: hasGlobalState ? {
|
|
355
582
|
mode: "merge",
|
|
356
583
|
patch: globalStatePatch,
|
|
357
584
|
remove: globalStateRemove,
|
|
358
585
|
} : null,
|
|
359
586
|
expectedRevision: cleanString(payload.expectedRevision || payload.expected_revision, 500),
|
|
587
|
+
expectedVersions,
|
|
360
588
|
idempotencyKey,
|
|
361
589
|
flowId: cleanString(payload.flowId || payload.flow_id || rawWorkflow.flowId || rawWorkflow.flow_id, 240),
|
|
362
590
|
flowSource: cleanString(payload.flowSource || payload.flow_source || rawWorkflow.flowSource || rawWorkflow.flow_source || "user", 80) || "user",
|
|
@@ -364,6 +592,102 @@ export function normalizeWorkflowReport(payload = {}) {
|
|
|
364
592
|
};
|
|
365
593
|
}
|
|
366
594
|
|
|
595
|
+
function semanticHash(value) {
|
|
596
|
+
return crypto.createHash("sha256").update(JSON.stringify(stableValue(value))).digest("hex").slice(0, 24);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function resourceVersion(value) {
|
|
600
|
+
return `rv:${semanticHash(value)}`;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function addObjectResourceVersions(out, prefix, value, path = []) {
|
|
604
|
+
if (value === undefined) return;
|
|
605
|
+
if (path.length) out[`${prefix}:${path.join(".")}`] = resourceVersion(value);
|
|
606
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
607
|
+
for (const [key, child] of Object.entries(value)) {
|
|
608
|
+
if (UNSAFE_OBJECT_KEYS.has(key)) continue;
|
|
609
|
+
addObjectResourceVersions(out, prefix, child, [...path, key]);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export function workflowSnapshotResourceVersions(snapshot = {}) {
|
|
614
|
+
const out = {};
|
|
615
|
+
const runtimeEvents = Array.isArray(snapshot.runtimeEvents || snapshot.runtime_events)
|
|
616
|
+
? (snapshot.runtimeEvents || snapshot.runtime_events)
|
|
617
|
+
: [];
|
|
618
|
+
for (const event of runtimeEvents) {
|
|
619
|
+
const source = cleanString(event?.source || event?.producer || "agentflow", 120).toLowerCase() || "agentflow";
|
|
620
|
+
const actionKey = cleanString(event?.actionModel?.key || event?.action || event?.actionId || event?.stageKey, 240);
|
|
621
|
+
if (actionKey && event?.auxiliary !== true) out[`action:${source}:${actionKey}`] = resourceVersion(event);
|
|
622
|
+
for (const artifact of Array.isArray(event?.artifacts) ? event.artifacts : []) {
|
|
623
|
+
const producer = cleanString(artifact?.producer || source, 120).toLowerCase() || source;
|
|
624
|
+
const key = cleanString(artifact?.key || artifact?.artifactKey || artifact?.artifact_key, 500);
|
|
625
|
+
if (key) out[`artifact:${producer}:${key}`] = resourceVersion(artifact);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
for (const artifact of Array.isArray(snapshot.artifacts) ? snapshot.artifacts : []) {
|
|
629
|
+
const producer = cleanString(artifact?.producer || "legacy", 120).toLowerCase() || "legacy";
|
|
630
|
+
const key = cleanString(artifact?.key || artifact?.artifactKey || artifact?.artifact_key, 500);
|
|
631
|
+
if (key) out[`artifact:${producer}:${key}`] = resourceVersion(artifact);
|
|
632
|
+
}
|
|
633
|
+
for (const projection of Array.isArray(snapshot?.projections?.timeline) ? snapshot.projections.timeline : []) {
|
|
634
|
+
const source = cleanString(projection?.source || "legacy", 120).toLowerCase() || "legacy";
|
|
635
|
+
const kind = cleanString(projection?.kind, 80).toLowerCase();
|
|
636
|
+
const id = cleanString(projection?.id, 240);
|
|
637
|
+
if (kind && id) out[`projection:${source}:${kind}:${id}`] = resourceVersion(projection);
|
|
638
|
+
}
|
|
639
|
+
addObjectResourceVersions(out, "global", plainObject(snapshot.globalState));
|
|
640
|
+
for (const [namespace, value] of Object.entries(plainObject(snapshot.extensions))) {
|
|
641
|
+
addObjectResourceVersions(out, `extension:${namespace}`, value);
|
|
642
|
+
}
|
|
643
|
+
for (const observation of Array.isArray(snapshot.clientObservations) ? snapshot.clientObservations : []) {
|
|
644
|
+
const source = cleanString(observation?.source || "legacy", 120).toLowerCase() || "legacy";
|
|
645
|
+
const clientId = cleanString(observation?.clientId, 160);
|
|
646
|
+
if (clientId) out[`observation:${source}:${clientId}`] = resourceVersion(observation);
|
|
647
|
+
}
|
|
648
|
+
return out;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function patchLeafPaths(value, path = []) {
|
|
652
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return [path];
|
|
653
|
+
const entries = Object.entries(value).filter(([key]) => !UNSAFE_OBJECT_KEYS.has(key));
|
|
654
|
+
if (!entries.length) return [path];
|
|
655
|
+
return entries.flatMap(([key, child]) => patchLeafPaths(child, [...path, key]));
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
export function workflowReportResourceKeys(report = {}, currentSnapshot = {}) {
|
|
659
|
+
const keys = new Set();
|
|
660
|
+
const source = cleanString(report?.event?.source || "agentflow-cli", 120).toLowerCase() || "agentflow-cli";
|
|
661
|
+
if (report.action?.key) keys.add(`action:${source}:${report.action.key}`);
|
|
662
|
+
for (const artifact of Array.isArray(report.artifacts) ? report.artifacts : []) {
|
|
663
|
+
if (artifact?.key) keys.add(`artifact:${source}:${artifact.key}`);
|
|
664
|
+
}
|
|
665
|
+
for (const path of patchLeafPaths(report?.globalState?.patch || {})) {
|
|
666
|
+
if (path.length) keys.add(`global:${path.join(".")}`);
|
|
667
|
+
}
|
|
668
|
+
for (const path of Array.isArray(report?.globalState?.remove) ? report.globalState.remove : []) {
|
|
669
|
+
if (path) keys.add(`global:${path}`);
|
|
670
|
+
}
|
|
671
|
+
for (const [namespace, extension] of Object.entries(plainObject(report.extensions))) {
|
|
672
|
+
for (const path of patchLeafPaths(extension)) {
|
|
673
|
+
if (path.length) keys.add(`extension:${namespace}:${path.join(".")}`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
if (report.projections) {
|
|
677
|
+
const current = Array.isArray(currentSnapshot?.projections?.timeline) ? currentSnapshot.projections.timeline : [];
|
|
678
|
+
const incoming = Array.isArray(report.projections.timeline) ? report.projections.timeline : [];
|
|
679
|
+
for (const item of [...current, ...incoming]) {
|
|
680
|
+
const owner = cleanString(item?.source || source, 120).toLowerCase() || source;
|
|
681
|
+
if (owner !== source) continue;
|
|
682
|
+
const kind = cleanString(item?.kind, 80).toLowerCase();
|
|
683
|
+
const id = cleanString(item?.id, 240);
|
|
684
|
+
if (kind && id) keys.add(`projection:${source}:${kind}:${id}`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (report.observation?.clientId) keys.add(`observation:${source}:${report.observation.clientId}`);
|
|
688
|
+
return [...keys].sort();
|
|
689
|
+
}
|
|
690
|
+
|
|
367
691
|
function displayValue(value) {
|
|
368
692
|
if (value == null) return "";
|
|
369
693
|
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
@@ -491,20 +815,22 @@ export function mergeWorkflowArtifactLists(left = [], right = [], defaultScope =
|
|
|
491
815
|
};
|
|
492
816
|
const artifactAliases = (artifact) => {
|
|
493
817
|
const aliases = [];
|
|
818
|
+
const producer = cleanString(artifact?.producer || artifact?.reportSource || artifact?.report_source, 120).toLowerCase();
|
|
819
|
+
const ownedAlias = (alias) => producer ? `producer:${producer}:${alias}` : alias;
|
|
494
820
|
const explicitKey = cleanString(
|
|
495
821
|
artifact?.key || artifact?.artifactKey || artifact?.artifact_key,
|
|
496
822
|
500,
|
|
497
823
|
);
|
|
498
|
-
if (explicitKey) aliases.push(`key:${explicitKey}`);
|
|
824
|
+
if (explicitKey) aliases.push(ownedAlias(`key:${explicitKey}`));
|
|
499
825
|
const url = normalizedUrl(
|
|
500
826
|
artifact?.canonicalUrl
|
|
501
827
|
|| artifact?.canonical_url
|
|
502
828
|
|| artifact?.href
|
|
503
829
|
|| artifact?.url,
|
|
504
830
|
);
|
|
505
|
-
if (url) aliases.push(`url:${url}`);
|
|
831
|
+
if (url) aliases.push(ownedAlias(`url:${url}`));
|
|
506
832
|
const artifactPath = cleanString(artifact?.path, 4000);
|
|
507
|
-
if (artifactPath) aliases.push(`path:${artifactPath}`);
|
|
833
|
+
if (artifactPath) aliases.push(ownedAlias(`path:${artifactPath}`));
|
|
508
834
|
return aliases;
|
|
509
835
|
};
|
|
510
836
|
const add = (artifact) => {
|
|
@@ -531,20 +857,15 @@ export function mergeWorkflowArtifactLists(left = [], right = [], defaultScope =
|
|
|
531
857
|
return out;
|
|
532
858
|
}
|
|
533
859
|
|
|
534
|
-
export function workflowRuntimeRevision(globalState = {}, artifacts = [], runtimeEvents = [], projections = {}) {
|
|
535
|
-
const events = (Array.isArray(runtimeEvents) ? runtimeEvents : []).map((event) =>
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
artifacts: event?.artifacts || [],
|
|
541
|
-
globalStatePatch: event?.globalStatePatch || {},
|
|
542
|
-
globalStateRemove: event?.globalStateRemove || [],
|
|
543
|
-
projections: event?.projections || {},
|
|
544
|
-
}));
|
|
860
|
+
export function workflowRuntimeRevision(globalState = {}, artifacts = [], runtimeEvents = [], projections = {}, extensions = {}) {
|
|
861
|
+
const events = (Array.isArray(runtimeEvents) ? runtimeEvents : []).map((event) => {
|
|
862
|
+
const semantic = { ...plainObject(event) };
|
|
863
|
+
for (const key of ["updatedAt", "createdAt", "actor", "rawOutput", "output", "result"]) delete semantic[key];
|
|
864
|
+
return semantic;
|
|
865
|
+
});
|
|
545
866
|
const hash = crypto
|
|
546
867
|
.createHash("sha256")
|
|
547
|
-
.update(JSON.stringify(stableValue({ globalState, artifacts, projections, events })))
|
|
868
|
+
.update(JSON.stringify(stableValue({ globalState, artifacts, projections, extensions, events })))
|
|
548
869
|
.digest("hex")
|
|
549
870
|
.slice(0, 24);
|
|
550
871
|
return `runtime:${hash}`;
|