吸收数字员工PlanStep依赖图派发策略
This commit is contained in:
@@ -14,6 +14,12 @@ const mockState = vi.hoisted(() => ({
|
||||
max_catch_up_runs: 5,
|
||||
max_concurrent_schedule_runs: 1,
|
||||
},
|
||||
dispatchPolicy: {
|
||||
enabled: true,
|
||||
max_per_sweep: 3,
|
||||
auto_dispatch_ready_steps: true,
|
||||
updated_at: null as string | null,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../startupPorts", () => ({
|
||||
@@ -26,6 +32,7 @@ vi.mock("../constants", () => ({
|
||||
|
||||
vi.mock("./stateService", () => ({
|
||||
readDigitalEmployeeCronSettings: () => mockState.settings,
|
||||
readDigitalEmployeePlanStepDispatchPolicy: () => mockState.dispatchPolicy,
|
||||
listDigitalEmployeeReadyPlanStepDispatchCandidates: () => mockState.readyCandidates,
|
||||
listDigitalEmployeeSchedules: () => mockState.schedules,
|
||||
listDueDigitalEmployeeSchedules: () => mockState.schedules,
|
||||
@@ -62,6 +69,12 @@ describe("digital employee scheduler service", () => {
|
||||
mockState.finishedRuns = [];
|
||||
mockState.dispatchEvents = [];
|
||||
mockState.governanceCommands = [];
|
||||
mockState.dispatchPolicy = {
|
||||
enabled: true,
|
||||
max_per_sweep: 3,
|
||||
auto_dispatch_ready_steps: true,
|
||||
updated_at: null,
|
||||
};
|
||||
mockState.schedules = [{
|
||||
id: "schedule-1",
|
||||
planId: "plan-1",
|
||||
@@ -165,6 +178,45 @@ describe("digital employee scheduler service", () => {
|
||||
expect(mockState.dispatchEvents.map((event) => event.status)).toEqual(["started", "failed"]);
|
||||
expect(mockState.governanceCommands).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("honors disabled ready step dispatch policy", async () => {
|
||||
mockState.readyCandidates = [readyStepCandidate()];
|
||||
mockState.dispatchPolicy = { ...mockState.dispatchPolicy, enabled: false };
|
||||
const { dispatchReadyPlanSteps } = await import("./schedulerService");
|
||||
|
||||
const result = await dispatchReadyPlanSteps(new Date("2026-06-07T01:00:10.000Z"));
|
||||
|
||||
expect(result).toMatchObject({ candidates: 1, dispatched: 0, skipped: 1, failed: 0 });
|
||||
expect(fetch).not.toHaveBeenCalled();
|
||||
expect(mockState.dispatchEvents).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("honors ready step auto dispatch switch", async () => {
|
||||
mockState.readyCandidates = [readyStepCandidate()];
|
||||
mockState.dispatchPolicy = { ...mockState.dispatchPolicy, auto_dispatch_ready_steps: false };
|
||||
const { dispatchReadyPlanSteps } = await import("./schedulerService");
|
||||
|
||||
const result = await dispatchReadyPlanSteps(new Date("2026-06-07T01:00:10.000Z"));
|
||||
|
||||
expect(result).toMatchObject({ candidates: 1, dispatched: 0, skipped: 1, failed: 0 });
|
||||
expect(fetch).not.toHaveBeenCalled();
|
||||
expect(mockState.dispatchEvents).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("limits ready step dispatches by policy max per sweep", async () => {
|
||||
mockState.readyCandidates = [
|
||||
readyStepCandidate(),
|
||||
{ ...readyStepCandidate(), stepId: "step-ready-2", taskId: "task-2" },
|
||||
];
|
||||
mockState.dispatchPolicy = { ...mockState.dispatchPolicy, max_per_sweep: 1 };
|
||||
const { dispatchReadyPlanSteps } = await import("./schedulerService");
|
||||
|
||||
const result = await dispatchReadyPlanSteps(new Date("2026-06-07T01:00:10.000Z"));
|
||||
|
||||
expect(result).toMatchObject({ candidates: 2, dispatched: 1, skipped: 1, failed: 0 });
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect(mockState.dispatchEvents.map((event) => event.stepId)).toEqual(["step-ready", "step-ready"]);
|
||||
});
|
||||
});
|
||||
|
||||
function readyStepCandidate(): Record<string, unknown> {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
listDigitalEmployeeSchedules,
|
||||
listDueDigitalEmployeeSchedules,
|
||||
readDigitalEmployeeCronSettings,
|
||||
readDigitalEmployeePlanStepDispatchPolicy,
|
||||
readDigitalEmployeeScheduleRuns,
|
||||
recordDigitalEmployeeGovernanceCommand,
|
||||
recordDigitalEmployeePlanStepDispatch,
|
||||
@@ -19,7 +20,6 @@ import {
|
||||
const SWEEP_INTERVAL_MS = 30_000;
|
||||
const MAX_SCAN_MINUTES = 366 * 24 * 60;
|
||||
const RETRY_DELAYS_MS = [60_000, 5 * 60_000, 15 * 60_000];
|
||||
const MAX_READY_STEP_DISPATCH_PER_SWEEP = 3;
|
||||
|
||||
let schedulerTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let schedulerSweeping = false;
|
||||
@@ -126,13 +126,19 @@ export async function dispatchReadyPlanSteps(
|
||||
failed: 0,
|
||||
errors: [],
|
||||
};
|
||||
const policy = readDigitalEmployeePlanStepDispatchPolicy();
|
||||
if (!policy.enabled || !policy.auto_dispatch_ready_steps) {
|
||||
result.skipped = candidates.length;
|
||||
return result;
|
||||
}
|
||||
const maxPerSweep = Math.max(1, Math.min(Math.floor(policy.max_per_sweep), 10));
|
||||
let attempts = 0;
|
||||
for (const candidate of candidates) {
|
||||
if (!candidate.dispatchable || !candidate.taskId) {
|
||||
result.skipped += 1;
|
||||
continue;
|
||||
}
|
||||
if (attempts >= MAX_READY_STEP_DISPATCH_PER_SWEEP) {
|
||||
if (attempts >= maxPerSweep) {
|
||||
result.skipped += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -938,6 +938,70 @@ describe("digital employee state service", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("persists plan step dispatch policy and records runtime events", async () => {
|
||||
const { readDigitalEmployeeUiState, saveDigitalEmployeeUiState } = await import("./stateService");
|
||||
|
||||
saveDigitalEmployeeUiState({
|
||||
action: "update_plan_step_dispatch_policy",
|
||||
metadata: { source: "plan-step-settings" },
|
||||
state: {
|
||||
selectedDutyIdsByDate: {},
|
||||
confirmedDates: {},
|
||||
reportScheduleTime: "18:00",
|
||||
reportGeneratedAtByDate: {},
|
||||
decisionResultsByDate: {},
|
||||
notificationStateById: {},
|
||||
notificationPreferences: {
|
||||
enabled: true,
|
||||
muted_kinds: [],
|
||||
muted_levels: [],
|
||||
updated_at: null,
|
||||
},
|
||||
approvalSubscriptionPreferences: {
|
||||
enabled: true,
|
||||
subscribed_actions: ["comment", "delegate", "mark_timeout", "mark_risk", "clear_risk", "decision"],
|
||||
subscribed_risk_levels: ["high", "critical"],
|
||||
notify_on_timeout: true,
|
||||
updated_at: null,
|
||||
},
|
||||
planStepDispatchPolicy: {
|
||||
enabled: true,
|
||||
max_per_sweep: 11,
|
||||
auto_dispatch_ready_steps: false,
|
||||
updated_at: "2026-06-07T08:07:00.000Z",
|
||||
},
|
||||
consoleRole: "sales",
|
||||
rolePreferences: {},
|
||||
profile: {
|
||||
name: "飞天数字员工",
|
||||
company: "qimingclaw 客户端",
|
||||
position: "客户端任务执行员",
|
||||
currentStatus: "working",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(readDigitalEmployeeUiState()).toMatchObject({
|
||||
planStepDispatchPolicy: {
|
||||
enabled: true,
|
||||
max_per_sweep: 10,
|
||||
auto_dispatch_ready_steps: false,
|
||||
updated_at: "2026-06-07T08:07:00.000Z",
|
||||
},
|
||||
});
|
||||
const policyEvent = Array.from(mockState.db?.events.values() ?? [])
|
||||
.find((event) => event.kind === "digital_workday_update_plan_step_dispatch_policy");
|
||||
expect(policyEvent).toMatchObject({
|
||||
kind: "digital_workday_update_plan_step_dispatch_policy",
|
||||
message: "数字员工PlanStep派发策略已更新",
|
||||
});
|
||||
expect(mockState.db?.outbox.get(`event:insert:${policyEvent?.id}`)).toMatchObject({
|
||||
entity_type: "event",
|
||||
operation: "insert",
|
||||
status: "pending",
|
||||
});
|
||||
});
|
||||
|
||||
it("records governance commands as formal runtime records", async () => {
|
||||
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
|
||||
|
||||
|
||||
@@ -323,6 +323,13 @@ export interface DigitalEmployeePlanStepDispatchRecord {
|
||||
payload: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface DigitalEmployeePlanStepDispatchPolicy {
|
||||
enabled: boolean;
|
||||
max_per_sweep: number;
|
||||
auto_dispatch_ready_steps: boolean;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface DigitalEmployeeRouteDecisionRecord {
|
||||
id: string;
|
||||
route_kind: string;
|
||||
@@ -480,6 +487,7 @@ export interface DigitalEmployeeUiState {
|
||||
notify_on_timeout: boolean;
|
||||
updated_at: string | null;
|
||||
};
|
||||
planStepDispatchPolicy?: DigitalEmployeePlanStepDispatchPolicy;
|
||||
consoleRole?: string;
|
||||
rolePreferences?: Record<string, unknown>;
|
||||
profile: {
|
||||
@@ -657,6 +665,7 @@ function defaultUiState(): DigitalEmployeeUiState {
|
||||
notificationStateById: {},
|
||||
notificationPreferences: defaultNotificationPreferences(),
|
||||
approvalSubscriptionPreferences: defaultApprovalSubscriptionPreferences(),
|
||||
planStepDispatchPolicy: defaultPlanStepDispatchPolicy(),
|
||||
consoleRole: "sales",
|
||||
rolePreferences: {},
|
||||
profile: {
|
||||
@@ -729,6 +738,15 @@ function defaultApprovalSubscriptionPreferences(): NonNullable<DigitalEmployeeUi
|
||||
};
|
||||
}
|
||||
|
||||
function defaultPlanStepDispatchPolicy(): DigitalEmployeePlanStepDispatchPolicy {
|
||||
return {
|
||||
enabled: true,
|
||||
max_per_sweep: 3,
|
||||
auto_dispatch_ready_steps: true,
|
||||
updated_at: null,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeStringArray(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return [...new Set(value.filter((item): item is string => typeof item === "string" && Boolean(item.trim())))];
|
||||
@@ -768,6 +786,25 @@ function normalizeApprovalSubscriptionPreferences(
|
||||
};
|
||||
}
|
||||
|
||||
function boundedInteger(value: unknown, fallback: number, min: number, max: number): number {
|
||||
const raw = typeof value === "number" ? value : Number(value);
|
||||
if (!Number.isFinite(raw)) return fallback;
|
||||
return Math.max(min, Math.min(Math.floor(raw), max));
|
||||
}
|
||||
|
||||
function normalizePlanStepDispatchPolicy(value: unknown): DigitalEmployeePlanStepDispatchPolicy {
|
||||
const fallback = defaultPlanStepDispatchPolicy();
|
||||
const record = value && typeof value === "object" && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: {};
|
||||
return {
|
||||
enabled: record.enabled !== false,
|
||||
max_per_sweep: boundedInteger(record.max_per_sweep, fallback.max_per_sweep, 1, 10),
|
||||
auto_dispatch_ready_steps: record.auto_dispatch_ready_steps !== false,
|
||||
updated_at: typeof record.updated_at === "string" ? record.updated_at : null,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeUiState(
|
||||
value: Partial<DigitalEmployeeUiState>,
|
||||
): DigitalEmployeeUiState {
|
||||
@@ -784,6 +821,7 @@ function normalizeUiState(
|
||||
notificationStateById: normalizeRecord(value.notificationStateById),
|
||||
notificationPreferences: normalizeNotificationPreferences(value.notificationPreferences),
|
||||
approvalSubscriptionPreferences: normalizeApprovalSubscriptionPreferences(value.approvalSubscriptionPreferences),
|
||||
planStepDispatchPolicy: normalizePlanStepDispatchPolicy(value.planStepDispatchPolicy),
|
||||
consoleRole: typeof value.consoleRole === "string" && value.consoleRole.trim()
|
||||
? value.consoleRole
|
||||
: fallback.consoleRole,
|
||||
@@ -826,6 +864,8 @@ function digitalEmployeeUiActionMessage(action: string, date?: string): string {
|
||||
return "数字员工通知订阅已更新";
|
||||
case "update_approval_subscriptions":
|
||||
return "数字员工审批订阅已更新";
|
||||
case "update_plan_step_dispatch_policy":
|
||||
return "数字员工PlanStep派发策略已更新";
|
||||
default:
|
||||
return `数字员工页面状态已更新${suffix}`;
|
||||
}
|
||||
@@ -855,6 +895,10 @@ export function readDigitalEmployeeUiState(): DigitalEmployeeUiState {
|
||||
return normalizeUiState(state as Partial<DigitalEmployeeUiState>);
|
||||
}
|
||||
|
||||
export function readDigitalEmployeePlanStepDispatchPolicy(): DigitalEmployeePlanStepDispatchPolicy {
|
||||
return readDigitalEmployeeUiState().planStepDispatchPolicy ?? defaultPlanStepDispatchPolicy();
|
||||
}
|
||||
|
||||
export function saveDigitalEmployeeUiState(
|
||||
update: DigitalEmployeeUiStateUpdate,
|
||||
): DigitalEmployeeUiState {
|
||||
|
||||
@@ -334,6 +334,15 @@ describe("digital employee sync service", () => {
|
||||
seq: 2,
|
||||
dependsOn: ["step-0"],
|
||||
preferredSkillIds: ["qimingclaw-mcp-tool-crm-crm-search"],
|
||||
payload: {
|
||||
dependencyGraph: {
|
||||
status: "blocked",
|
||||
blockedDependencies: ["step-0"],
|
||||
waitingDependencies: ["step-2"],
|
||||
},
|
||||
lastDispatch: { status: "failed" },
|
||||
skipReason: "approval_pending",
|
||||
},
|
||||
}),
|
||||
);
|
||||
mockState.fetch.mockResolvedValue(
|
||||
@@ -365,7 +374,16 @@ describe("digital employee sync service", () => {
|
||||
status: "pending",
|
||||
seq: 2,
|
||||
depends_on: ["step-0"],
|
||||
dependency_count: 1,
|
||||
dependency_status: "blocked",
|
||||
blocked_dependencies: ["step-0"],
|
||||
blocked_dependency_count: 1,
|
||||
waiting_dependencies: ["step-2"],
|
||||
waiting_dependency_count: 1,
|
||||
preferred_skill_ids: ["qimingclaw-mcp-tool-crm-crm-search"],
|
||||
dispatchable: false,
|
||||
skip_reason: "approval_pending",
|
||||
last_dispatch_status: "failed",
|
||||
}),
|
||||
}),
|
||||
],
|
||||
@@ -541,9 +559,24 @@ describe("digital employee sync service", () => {
|
||||
});
|
||||
insertEventEntity("event-dispatch", "plan_step_dispatch_failed", {
|
||||
step_id: "step-1",
|
||||
plan_id: "plan-1",
|
||||
task_id: "task-1",
|
||||
run_id: "run-1",
|
||||
status: "failed",
|
||||
reason: "computer chat failed",
|
||||
});
|
||||
insertEventEntity("event-plan-step-ready", "plan_step_ready", {
|
||||
stepId: "step-2",
|
||||
planId: "plan-1",
|
||||
status: "ready",
|
||||
dependsOn: ["step-1"],
|
||||
dependencyGraph: {
|
||||
status: "ready",
|
||||
blockedDependencies: [],
|
||||
waitingDependencies: [],
|
||||
satisfiedDependencies: ["step-1"],
|
||||
},
|
||||
});
|
||||
insertEventEntity("event-governance-input", "governance_provide_task_input", {
|
||||
commandKind: "provide_task_input",
|
||||
commandId: "command-provide-input",
|
||||
@@ -648,6 +681,25 @@ describe("digital employee sync service", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
insertEventEntity("event-plan-step-policy", "digital_workday_update_plan_step_dispatch_policy", {
|
||||
metadata: {
|
||||
action: "update_plan_step_dispatch_policy",
|
||||
plan_step_dispatch_policy: {
|
||||
enabled: false,
|
||||
max_per_sweep: 1,
|
||||
auto_dispatch_ready_steps: false,
|
||||
updated_at: "2026-06-07T08:07:00.000Z",
|
||||
},
|
||||
},
|
||||
state: {
|
||||
planStepDispatchPolicy: {
|
||||
enabled: false,
|
||||
max_per_sweep: 1,
|
||||
auto_dispatch_ready_steps: false,
|
||||
updated_at: "2026-06-07T08:07:00.000Z",
|
||||
},
|
||||
},
|
||||
});
|
||||
mockState.fetch.mockResolvedValue(
|
||||
jsonResponse({
|
||||
success: true,
|
||||
@@ -658,12 +710,14 @@ describe("digital employee sync service", () => {
|
||||
{ entity_type: "event", entity_id: "event-artifact" },
|
||||
{ entity_type: "event", entity_id: "event-skill" },
|
||||
{ entity_type: "event", entity_id: "event-dispatch" },
|
||||
{ entity_type: "event", entity_id: "event-plan-step-ready" },
|
||||
{ entity_type: "event", entity_id: "event-governance-input" },
|
||||
{ entity_type: "event", entity_id: "event-notification-read" },
|
||||
{ entity_type: "event", entity_id: "event-notification-dismiss" },
|
||||
{ entity_type: "event", entity_id: "event-notification-reply" },
|
||||
{ entity_type: "event", entity_id: "event-notification-preferences" },
|
||||
{ entity_type: "event", entity_id: "event-approval-subscriptions" },
|
||||
{ entity_type: "event", entity_id: "event-plan-step-policy" },
|
||||
],
|
||||
}),
|
||||
);
|
||||
@@ -736,9 +790,22 @@ describe("digital employee sync service", () => {
|
||||
expect(businessView("event-dispatch")).toMatchObject({
|
||||
category: "dispatch",
|
||||
step_id: "step-1",
|
||||
plan_id: "plan-1",
|
||||
task_id: "task-1",
|
||||
run_id: "run-1",
|
||||
status: "failed",
|
||||
reason: "computer chat failed",
|
||||
});
|
||||
expect(businessView("event-plan-step-ready")).toMatchObject({
|
||||
category: "dispatch",
|
||||
step_id: "step-2",
|
||||
plan_id: "plan-1",
|
||||
status: "ready",
|
||||
dependency_count: 1,
|
||||
blocked_dependencies: [],
|
||||
waiting_dependencies: [],
|
||||
satisfied_dependencies: ["step-1"],
|
||||
});
|
||||
expect(businessView("event-governance-input")).toMatchObject({
|
||||
category: "governance",
|
||||
command_kind: "provide_task_input",
|
||||
@@ -790,6 +857,16 @@ describe("digital employee sync service", () => {
|
||||
});
|
||||
expect(businessView("event-approval-subscriptions")).not.toHaveProperty("state");
|
||||
expect(businessView("event-approval-subscriptions")).not.toHaveProperty("approvalSubscriptionPreferences");
|
||||
expect(businessView("event-plan-step-policy")).toMatchObject({
|
||||
category: "dispatch",
|
||||
action: "update_plan_step_dispatch_policy",
|
||||
policy_enabled: false,
|
||||
max_per_sweep: 1,
|
||||
auto_dispatch_ready_steps: false,
|
||||
updated_at: "2026-06-07T08:07:00.000Z",
|
||||
});
|
||||
expect(businessView("event-plan-step-policy")).not.toHaveProperty("state");
|
||||
expect(businessView("event-plan-step-policy")).not.toHaveProperty("planStepDispatchPolicy");
|
||||
expect(readEntity("event", "event-route")).toMatchObject({
|
||||
sync_status: "synced",
|
||||
sync_error: null,
|
||||
|
||||
@@ -458,18 +458,7 @@ function buildBusinessView(
|
||||
assigned_skill_id: stringField(record.assignedSkillId ?? record.assigned_skill_id) || null,
|
||||
};
|
||||
case "plan_step":
|
||||
return {
|
||||
...base,
|
||||
plan_id: stringField(record.planId ?? record.plan_id) || null,
|
||||
step_id: row.entity_id,
|
||||
seq: typeof record.seq === "number" ? record.seq : null,
|
||||
depends_on: Array.isArray(record.dependsOn ?? record.depends_on)
|
||||
? record.dependsOn ?? record.depends_on
|
||||
: [],
|
||||
preferred_skill_ids: Array.isArray(record.preferredSkillIds ?? record.preferred_skill_ids)
|
||||
? record.preferredSkillIds ?? record.preferred_skill_ids
|
||||
: [],
|
||||
};
|
||||
return planStepBusinessView(base, row, record);
|
||||
case "run":
|
||||
return {
|
||||
...base,
|
||||
@@ -511,6 +500,36 @@ function buildBusinessView(
|
||||
}
|
||||
}
|
||||
|
||||
function planStepBusinessView(
|
||||
base: Record<string, unknown>,
|
||||
row: OutboxRow,
|
||||
record: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const payload = objectRecord(record.payload) ?? {};
|
||||
const dependencyGraph = objectRecord(payload.dependencyGraph ?? payload.dependency_graph) ?? {};
|
||||
const lastDispatch = objectRecord(payload.lastDispatch ?? payload.last_dispatch) ?? {};
|
||||
const dependsOn = stringArrayField(record.dependsOn ?? record.depends_on);
|
||||
const blockedDependencies = stringArrayField(dependencyGraph.blockedDependencies ?? dependencyGraph.blocked_dependencies);
|
||||
const waitingDependencies = stringArrayField(dependencyGraph.waitingDependencies ?? dependencyGraph.waiting_dependencies);
|
||||
return {
|
||||
...base,
|
||||
plan_id: stringField(record.planId ?? record.plan_id) || null,
|
||||
step_id: row.entity_id,
|
||||
seq: numberField(record.seq),
|
||||
depends_on: dependsOn,
|
||||
dependency_count: dependsOn.length,
|
||||
dependency_status: stringField(dependencyGraph.status) || null,
|
||||
blocked_dependencies: blockedDependencies,
|
||||
blocked_dependency_count: blockedDependencies.length,
|
||||
waiting_dependencies: waitingDependencies,
|
||||
waiting_dependency_count: waitingDependencies.length,
|
||||
preferred_skill_ids: stringArrayField(record.preferredSkillIds ?? record.preferred_skill_ids),
|
||||
dispatchable: stringField(record.status) === "ready" && blockedDependencies.length === 0 && waitingDependencies.length === 0,
|
||||
skip_reason: stringField(payload.skipReason ?? payload.skip_reason) || null,
|
||||
last_dispatch_status: stringField(lastDispatch.status) || null,
|
||||
};
|
||||
}
|
||||
|
||||
function buildEventBusinessView(
|
||||
base: Record<string, unknown>,
|
||||
row: OutboxRow,
|
||||
@@ -569,7 +588,9 @@ function eventSpecificBusinessView(
|
||||
return artifactEventBusinessView(payload);
|
||||
}
|
||||
if (kind.startsWith("skill_call_")) return skillCallBusinessView(payload);
|
||||
if (isPlanStepDependencyEvent(kind)) return planStepDependencyBusinessView(payload);
|
||||
if (kind.startsWith("plan_step_dispatch_")) return planStepDispatchBusinessView(payload);
|
||||
if (isPlanStepDispatchPolicyEvent(kind)) return planStepDispatchPolicyBusinessView(payload);
|
||||
if (kind === "governance_provide_task_input") return governanceProvideTaskInputBusinessView(payload);
|
||||
if (isNotificationEvent(kind)) return notificationBusinessView(payload);
|
||||
return {};
|
||||
@@ -667,11 +688,43 @@ function skillCallBusinessView(payload: Record<string, unknown>): Record<string,
|
||||
function planStepDispatchBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
|
||||
return {
|
||||
step_id: stringField(payload.step_id ?? payload.stepId) || null,
|
||||
plan_id: stringField(payload.plan_id ?? payload.planId) || null,
|
||||
task_id: stringField(payload.task_id ?? payload.taskId) || null,
|
||||
run_id: stringField(payload.run_id ?? payload.runId) || null,
|
||||
status: stringField(payload.status) || null,
|
||||
reason: stringField(payload.reason) || null,
|
||||
};
|
||||
}
|
||||
|
||||
function planStepDependencyBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
|
||||
const dependencyGraph = objectRecord(payload.dependencyGraph ?? payload.dependency_graph) ?? {};
|
||||
const dependencies = stringArrayField(payload.dependsOn ?? payload.depends_on);
|
||||
return {
|
||||
step_id: stringField(payload.stepId ?? payload.step_id) || null,
|
||||
plan_id: stringField(payload.planId ?? payload.plan_id) || null,
|
||||
status: stringField(payload.status ?? dependencyGraph.status) || null,
|
||||
dependency_count: dependencies.length,
|
||||
blocked_dependencies: stringArrayField(dependencyGraph.blockedDependencies ?? dependencyGraph.blocked_dependencies),
|
||||
waiting_dependencies: stringArrayField(dependencyGraph.waitingDependencies ?? dependencyGraph.waiting_dependencies),
|
||||
satisfied_dependencies: stringArrayField(dependencyGraph.satisfiedDependencies ?? dependencyGraph.satisfied_dependencies),
|
||||
};
|
||||
}
|
||||
|
||||
function planStepDispatchPolicyBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
|
||||
const metadata = objectRecord(payload.metadata) ?? {};
|
||||
const state = objectRecord(payload.state) ?? {};
|
||||
const policy = objectRecord(metadata.plan_step_dispatch_policy)
|
||||
?? objectRecord(state.planStepDispatchPolicy)
|
||||
?? {};
|
||||
return {
|
||||
action: stringField(metadata.action ?? payload.action) || "update_plan_step_dispatch_policy",
|
||||
policy_enabled: typeof policy.enabled === "boolean" ? policy.enabled : null,
|
||||
max_per_sweep: numberField(policy.max_per_sweep),
|
||||
auto_dispatch_ready_steps: typeof policy.auto_dispatch_ready_steps === "boolean" ? policy.auto_dispatch_ready_steps : null,
|
||||
updated_at: stringField(policy.updated_at) || null,
|
||||
};
|
||||
}
|
||||
|
||||
function governanceProvideTaskInputBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
|
||||
const commandPayload = objectRecord(payload.payload) ?? {};
|
||||
const result = objectRecord(payload.result) ?? {};
|
||||
@@ -714,7 +767,9 @@ function eventBusinessCategory(kind: string): string {
|
||||
if (isApprovalSubscriptionEvent(kind)) return "approval";
|
||||
if (kind.startsWith("artifact_")) return "artifact";
|
||||
if (kind.startsWith("skill_call_")) return "skill";
|
||||
if (isPlanStepDependencyEvent(kind)) return "dispatch";
|
||||
if (kind.startsWith("plan_step_dispatch_")) return "dispatch";
|
||||
if (isPlanStepDispatchPolicyEvent(kind)) return "dispatch";
|
||||
if (isNotificationEvent(kind)) return "notification";
|
||||
if (kind.startsWith("governance_")) return "governance";
|
||||
return "runtime";
|
||||
@@ -724,6 +779,14 @@ function isApprovalSubscriptionEvent(kind: string): boolean {
|
||||
return kind === "digital_workday_update_approval_subscriptions";
|
||||
}
|
||||
|
||||
function isPlanStepDependencyEvent(kind: string): boolean {
|
||||
return kind === "plan_step_ready" || kind === "plan_step_blocked" || kind === "plan_step_waiting";
|
||||
}
|
||||
|
||||
function isPlanStepDispatchPolicyEvent(kind: string): boolean {
|
||||
return kind === "digital_workday_update_plan_step_dispatch_policy";
|
||||
}
|
||||
|
||||
function isNotificationEvent(kind: string): boolean {
|
||||
return kind === "digital_workday_update_notification_preferences"
|
||||
|| kind === "digital_workday_notification_read"
|
||||
|
||||
Reference in New Issue
Block a user