@fieldwangai/agentflow 0.1.136 → 0.1.138
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 +218 -17
- package/bin/lib/ui-server.mjs +943 -59
- package/bin/lib/workflow-report.mjs +290 -30
- package/builtin/web-ui/dist/assets/WorkflowAssistantThread-B3thaqH2.js +6 -0
- package/builtin/web-ui/dist/assets/index-COX1zMwq.css +1 -0
- package/builtin/web-ui/dist/assets/index-YS4XOpXF.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 +33 -4
- package/skills/agentflow-cli/scripts/workflow-report-client.mjs +3 -0
- package/skills/agentflow-workflow-report/SKILL.md +17 -17
- package/skills/agentflow-workflow-report/references/protocol.md +118 -45
- package/builtin/web-ui/dist/assets/index-HdswcJWY.js +0 -565
- package/builtin/web-ui/dist/assets/index-KIGufzQf.css +0 -1
|
@@ -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,8 +164,7 @@ 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
|
}
|
|
@@ -189,7 +216,6 @@ export function materializeWorkflowProjections(snapshot = {}, runtimeEvents = []
|
|
|
189
216
|
? next.timeline
|
|
190
217
|
.map((item, index) => normalizeWorkflowTimelineProjection(item, index))
|
|
191
218
|
.filter(Boolean)
|
|
192
|
-
.slice(0, 100)
|
|
193
219
|
: projections.timeline || [],
|
|
194
220
|
};
|
|
195
221
|
}
|
|
@@ -230,11 +256,22 @@ export function removeWorkflowGlobalStatePath(value, rawPath) {
|
|
|
230
256
|
}
|
|
231
257
|
|
|
232
258
|
export function normalizeWorkflowReference(payload = {}) {
|
|
233
|
-
const
|
|
234
|
-
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;
|
|
235
267
|
const keySeparator = explicitKey.indexOf(":");
|
|
236
268
|
const keyedNamespace = keySeparator > 0 ? explicitKey.slice(0, keySeparator) : "";
|
|
237
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" };
|
|
238
275
|
const namespace = cleanString(
|
|
239
276
|
workflow.namespace || workflow.type || payload.workflowNamespace || payload.workflow_namespace || keyedNamespace || "tapd",
|
|
240
277
|
80,
|
|
@@ -260,14 +297,17 @@ export function normalizeWorkflowReference(payload = {}) {
|
|
|
260
297
|
}
|
|
261
298
|
|
|
262
299
|
export function normalizeWorkflowReport(payload = {}) {
|
|
263
|
-
const
|
|
300
|
+
const schemaValue = payload.schemaVersion ?? payload.schema_version ?? WORKFLOW_REPORT_SCHEMA_VERSION;
|
|
301
|
+
const schemaVersion = Number(schemaValue);
|
|
264
302
|
if (schemaVersion !== WORKFLOW_REPORT_SCHEMA_VERSION) {
|
|
265
303
|
return { error: `Unsupported workflow report schemaVersion: ${schemaVersion}` };
|
|
266
304
|
}
|
|
267
305
|
const workflow = normalizeWorkflowReference(payload);
|
|
268
306
|
if (workflow.error) return workflow;
|
|
269
307
|
const rawWorkflow = plainObject(payload.workflow);
|
|
270
|
-
|
|
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();
|
|
271
311
|
if (!/^[a-z][a-z0-9._-]{0,119}$/.test(source)) {
|
|
272
312
|
return { error: "Invalid workflow report source" };
|
|
273
313
|
}
|
|
@@ -278,29 +318,49 @@ export function normalizeWorkflowReport(payload = {}) {
|
|
|
278
318
|
if (hasObservation && !Object.keys(observationState).length) {
|
|
279
319
|
return { error: "observation requires state" };
|
|
280
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" };
|
|
281
326
|
const observation = hasObservation ? {
|
|
282
327
|
schema: cleanString(rawObservation.schema || rawObservation.model || "workflow-observation/v1", 160),
|
|
283
328
|
state: mergeWorkflowGlobalState({}, observationState),
|
|
284
329
|
observedAt: cleanString(rawObservation.observedAt || rawObservation.observed_at, 80),
|
|
285
|
-
clientId: cleanString(rawObservation.clientId || rawObservation.client_id, 160),
|
|
330
|
+
clientId: cleanString(rawObservation.clientId || rawObservation.client_id || source, 160),
|
|
286
331
|
scope: cleanString(rawObservation.scope || "client", 80).toLowerCase() || "client",
|
|
287
332
|
} : null;
|
|
288
333
|
|
|
289
334
|
const rawAction = plainObject(payload.action);
|
|
290
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
|
+
}
|
|
291
339
|
const actionKey = cleanString(rawAction.key || rawAction.id || rawAction.actionKey || rawAction.action_key, 240);
|
|
292
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" };
|
|
293
353
|
const action = hasAction ? {
|
|
294
354
|
...rawAction,
|
|
295
355
|
key: actionKey,
|
|
296
356
|
title: cleanString(rawAction.title || rawAction.label || actionKey, 500),
|
|
297
|
-
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) } : {}),
|
|
298
358
|
status: normalizeActionStatus(rawAction.status),
|
|
299
|
-
group: cleanString(rawAction.group || rawAction.stage || rawAction.category, 120),
|
|
300
|
-
scope: cleanString(rawAction.scope, 80),
|
|
301
|
-
platform: cleanString(rawAction.platform, 80),
|
|
302
|
-
issueKey: cleanString(rawAction.issueKey || rawAction.issue_key, 240),
|
|
303
|
-
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) } : {}),
|
|
304
364
|
occurredAt: cleanString(
|
|
305
365
|
rawAction.occurredAt ||
|
|
306
366
|
rawAction.occurred_at ||
|
|
@@ -311,6 +371,31 @@ export function normalizeWorkflowReport(payload = {}) {
|
|
|
311
371
|
} : null;
|
|
312
372
|
|
|
313
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` };
|
|
314
399
|
const artifacts = rawArtifacts
|
|
315
400
|
.filter((item) => item && typeof item === "object" && !Array.isArray(item))
|
|
316
401
|
.map((item, index) => ({
|
|
@@ -326,25 +411,81 @@ export function normalizeWorkflowReport(payload = {}) {
|
|
|
326
411
|
if (hasProjections && !Array.isArray(rawProjections.timeline)) {
|
|
327
412
|
return { error: "projections.timeline must be an array" };
|
|
328
413
|
}
|
|
414
|
+
if (hasProjections && rawProjections.timeline.length > 100) {
|
|
415
|
+
return { error: "projections.timeline supports at most 100 entries" };
|
|
416
|
+
}
|
|
329
417
|
const invalidTimelineIndex = hasProjections
|
|
330
418
|
? rawProjections.timeline.findIndex((item, index) => !normalizeWorkflowTimelineProjection(item, index))
|
|
331
419
|
: -1;
|
|
332
420
|
if (invalidTimelineIndex >= 0) {
|
|
333
421
|
return { error: `projections.timeline[${invalidTimelineIndex}] requires kind and id` };
|
|
334
422
|
}
|
|
335
|
-
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;
|
|
336
455
|
|
|
337
456
|
const rawExtensions = plainObject(payload.extensions);
|
|
338
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
|
+
}
|
|
339
468
|
const extensions = hasExtensions ? normalizeWorkflowExtensions(rawExtensions) : null;
|
|
340
469
|
if (hasExtensions && !Object.keys(extensions).length) {
|
|
341
470
|
return { error: "extensions requires at least one valid namespace object" };
|
|
342
471
|
}
|
|
472
|
+
if (hasExtensions && Object.keys(extensions).some((namespace) => namespace !== source)) {
|
|
473
|
+
return { error: "extensions may only update the namespace matching report source" };
|
|
474
|
+
}
|
|
343
475
|
|
|
344
476
|
const rawGlobalState = plainObject(payload.globalState || payload.global_state);
|
|
345
477
|
const hasGlobalState = Object.keys(rawGlobalState).length > 0;
|
|
346
478
|
const globalStatePatch = plainObject(rawGlobalState.patch);
|
|
347
|
-
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
|
+
]);
|
|
348
489
|
const mode = cleanString(rawGlobalState.mode || "merge", 40).toLowerCase() || "merge";
|
|
349
490
|
if (hasGlobalState && mode !== "merge") return { error: "globalState.mode must be merge" };
|
|
350
491
|
if (hasGlobalState && !Object.keys(globalStatePatch).length && !globalStateRemove.length) {
|
|
@@ -361,9 +502,23 @@ export function normalizeWorkflowReport(payload = {}) {
|
|
|
361
502
|
action?.idempotency_key,
|
|
362
503
|
500,
|
|
363
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
|
+
}
|
|
364
518
|
const event = {
|
|
365
519
|
schemaVersion,
|
|
366
520
|
type: "workflow-report",
|
|
521
|
+
operation: "report",
|
|
367
522
|
source,
|
|
368
523
|
workflow,
|
|
369
524
|
workflowKey: workflow.key,
|
|
@@ -381,9 +536,9 @@ export function normalizeWorkflowReport(payload = {}) {
|
|
|
381
536
|
detail: action.detail,
|
|
382
537
|
status: action.status,
|
|
383
538
|
scope: action.scope || action.group || "workflow",
|
|
384
|
-
platform: action.platform,
|
|
385
|
-
issueKey: action.issueKey,
|
|
386
|
-
tags: action.tags,
|
|
539
|
+
...(action.platform ? { platform: action.platform } : {}),
|
|
540
|
+
...(action.issueKey ? { issueKey: action.issueKey } : {}),
|
|
541
|
+
...(action.tags ? { tags: action.tags } : {}),
|
|
387
542
|
...(action.occurredAt ? { occurredAt: action.occurredAt } : {}),
|
|
388
543
|
} : {
|
|
389
544
|
title: cleanString(payload.title || "Workflow 全局状态更新", 500),
|
|
@@ -397,9 +552,23 @@ export function normalizeWorkflowReport(payload = {}) {
|
|
|
397
552
|
...(hasGlobalState ? {
|
|
398
553
|
globalStatePatch,
|
|
399
554
|
globalStateRemove,
|
|
555
|
+
globalStateOwnerPaths,
|
|
400
556
|
} : {}),
|
|
401
557
|
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
402
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
|
+
}
|
|
403
572
|
return {
|
|
404
573
|
schemaVersion,
|
|
405
574
|
workflow,
|
|
@@ -415,6 +584,7 @@ export function normalizeWorkflowReport(payload = {}) {
|
|
|
415
584
|
remove: globalStateRemove,
|
|
416
585
|
} : null,
|
|
417
586
|
expectedRevision: cleanString(payload.expectedRevision || payload.expected_revision, 500),
|
|
587
|
+
expectedVersions,
|
|
418
588
|
idempotencyKey,
|
|
419
589
|
flowId: cleanString(payload.flowId || payload.flow_id || rawWorkflow.flowId || rawWorkflow.flow_id, 240),
|
|
420
590
|
flowSource: cleanString(payload.flowSource || payload.flow_source || rawWorkflow.flowSource || rawWorkflow.flow_source || "user", 80) || "user",
|
|
@@ -422,6 +592,102 @@ export function normalizeWorkflowReport(payload = {}) {
|
|
|
422
592
|
};
|
|
423
593
|
}
|
|
424
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
|
+
|
|
425
691
|
function displayValue(value) {
|
|
426
692
|
if (value == null) return "";
|
|
427
693
|
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
@@ -592,17 +858,11 @@ export function mergeWorkflowArtifactLists(left = [], right = [], defaultScope =
|
|
|
592
858
|
}
|
|
593
859
|
|
|
594
860
|
export function workflowRuntimeRevision(globalState = {}, artifacts = [], runtimeEvents = [], projections = {}, extensions = {}) {
|
|
595
|
-
const events = (Array.isArray(runtimeEvents) ? runtimeEvents : []).map((event) =>
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
artifacts: event?.artifacts || [],
|
|
601
|
-
globalStatePatch: event?.globalStatePatch || {},
|
|
602
|
-
globalStateRemove: event?.globalStateRemove || [],
|
|
603
|
-
projections: event?.projections || {},
|
|
604
|
-
extensionsPatch: event?.extensionsPatch || {},
|
|
605
|
-
}));
|
|
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
|
+
});
|
|
606
866
|
const hash = crypto
|
|
607
867
|
.createHash("sha256")
|
|
608
868
|
.update(JSON.stringify(stableValue({ globalState, artifacts, projections, extensions, events })))
|