吸收数字员工通知跨端同步

This commit is contained in:
baiyanyun
2026-06-11 20:27:10 +08:00
parent 4c862ef4b7
commit 8278fdc81c
15 changed files with 745 additions and 193 deletions

View File

@@ -104,6 +104,8 @@ vi.mock("../services/digitalEmployee/syncService", () => ({
pullDigitalEmployeeRouteDecisionPolicy: vi.fn(async () => ({ ok: true, policy: { enabled: true, auto_create_governance_commands: true, approval_required_intents: [], blocked_intents: [], preferred_route_kinds: [], updated_at: null } })),
pullDigitalEmployeeApprovalUpdates: vi.fn(async () => ({ ok: true, applied: 1, ignored: 0 })),
submitDigitalEmployeeApprovalDecision: vi.fn(async () => ({ ok: true, submitted_at: "2026-06-07T08:00:00.000Z" })),
pullDigitalEmployeeNotificationUpdates: vi.fn(async () => ({ ok: true, applied: 1, ignored: 0 })),
submitDigitalEmployeeNotificationAction: vi.fn(async () => ({ ok: true, submitted_at: "2026-06-07T08:00:00.000Z" })),
}));
vi.mock("../services/digitalEmployee/planTemplateService", () => ({
@@ -381,4 +383,28 @@ describe("digital employee managed service actions", () => {
expect(syncService.pullDigitalEmployeeApprovalUpdates).toHaveBeenCalledWith({ force: true });
expect(syncService.submitDigitalEmployeeApprovalDecision).toHaveBeenCalledWith({ approval_id: "approval-1", decision: "approved" });
});
it("registers notification update pull and action submit through IPC", async () => {
const electron = await import("electron");
const syncService = await import("../services/digitalEmployee/syncService");
const { registerDigitalEmployeeHandlers } = await import("./digitalEmployeeHandlers");
registerDigitalEmployeeHandlers(createCtx());
const calls = vi.mocked(electron.ipcMain.handle).mock.calls;
const pullCall = calls.find(([channel]) => channel === "digitalEmployee:pullNotificationUpdates");
const submitCall = calls.find(([channel]) => channel === "digitalEmployee:submitNotificationAction");
expect(pullCall).toBeTruthy();
expect(submitCall).toBeTruthy();
const pullResult = await (pullCall?.[1] as (_event: unknown) => Promise<unknown>)({});
const submitResult = await (submitCall?.[1] as (_event: unknown, input: unknown) => Promise<unknown>)({}, { notification_id: "task:1", action: "read" });
const fallbackSubmitResult = await (submitCall?.[1] as (_event: unknown, input: unknown) => Promise<unknown>)({}, null);
expect(pullResult).toMatchObject({ ok: true, applied: 1 });
expect(submitResult).toMatchObject({ ok: true });
expect(fallbackSubmitResult).toMatchObject({ ok: true });
expect(syncService.pullDigitalEmployeeNotificationUpdates).toHaveBeenCalledWith({ force: true });
expect(syncService.submitDigitalEmployeeNotificationAction).toHaveBeenCalledWith({ notification_id: "task:1", action: "read" });
expect(syncService.submitDigitalEmployeeNotificationAction).toHaveBeenCalledWith({});
});
});

View File

@@ -68,6 +68,8 @@ import {
pullDigitalEmployeeRiskApprovalPolicy,
pullDigitalEmployeeRouteDecisionPolicy,
submitDigitalEmployeeApprovalDecision,
pullDigitalEmployeeNotificationUpdates,
submitDigitalEmployeeNotificationAction,
} from "../services/digitalEmployee/syncService";
import { listDigitalEmployeePlanTemplates } from "../services/digitalEmployee/planTemplateService";
import {
@@ -223,6 +225,14 @@ export function registerDigitalEmployeeHandlers(ctx: HandlerContext): void {
return submitDigitalEmployeeApprovalDecision(typeof input === "object" && input ? input as Record<string, unknown> : {});
});
ipcMain.handle("digitalEmployee:pullNotificationUpdates", async () => {
return pullDigitalEmployeeNotificationUpdates({ force: true });
});
ipcMain.handle("digitalEmployee:submitNotificationAction", async (_, input: unknown) => {
return submitDigitalEmployeeNotificationAction(typeof input === "object" && input ? input as Record<string, unknown> : {});
});
ipcMain.handle("digitalEmployee:evaluateRiskPolicy", async (_, input: unknown) => {
return evaluateDigitalEmployeeRiskPolicy(typeof input === "object" && input ? input as Parameters<typeof evaluateDigitalEmployeeRiskPolicy>[0] : {});
});

View File

@@ -1209,6 +1209,14 @@ function digitalEmployeeUiActionMessage(action: string, date?: string): string {
return "数字员工审批处理已回写管理端";
case "approval_decision_submit_failed":
return "数字员工审批处理回写失败";
case "notification_updates_pulled":
return "数字员工通知更新已拉取";
case "notification_updates_pull_failed":
return "数字员工通知更新拉取失败";
case "notification_action_submitted":
return "数字员工通知动作已回写管理端";
case "notification_action_submit_failed":
return "数字员工通知动作回写失败";
default:
return `数字员工页面状态已更新${suffix}`;
}

View File

@@ -446,6 +446,157 @@ describe("digital employee sync service", () => {
]));
});
it("pulls remote notification updates into local notification overlays", async () => {
mockState.settings.set("digital_employee_ui_state_v1", {
notificationStateById: {
"task:existing": { status: "read", read_at: "2026-06-07T07:30:00.000Z" },
},
});
mockState.fetch.mockResolvedValue(jsonResponse({
success: true,
data: {
notifications: [
{
notification_id: "task:need-input:1",
status: "read",
read_at: "2026-06-07T08:01:00.000Z",
remote_updated_at: "2026-06-07T08:02:00.000Z",
source: "management-inbox",
},
{
notificationId: "task:reply:1",
status: "resolved",
reply: "已补充客户编号",
repliedAt: "2026-06-07T08:03:00.000Z",
},
],
},
}));
const { pullDigitalEmployeeNotificationUpdates } = await import("./syncService");
const { readDigitalEmployeeUiState } = await import("./stateService");
const result = await pullDigitalEmployeeNotificationUpdates({ force: true });
expect(result).toMatchObject({
ok: true,
endpoint: "https://manage.example.com/api/digital-employee/notifications/updates",
applied: 2,
ignored: 0,
pulled_at: "2026-06-07T08:00:00.000Z",
});
expect(mockState.fetch).toHaveBeenCalledWith(
"https://manage.example.com/api/digital-employee/notifications/updates",
expect.objectContaining({ method: "GET" }),
);
expect(readDigitalEmployeeUiState().notificationStateById).toMatchObject({
"task:existing": { status: "read", read_at: "2026-06-07T07:30:00.000Z" },
"task:need-input:1": {
status: "read",
read_at: "2026-06-07T08:01:00.000Z",
remote_updated_at: "2026-06-07T08:02:00.000Z",
source: "management-inbox",
},
"task:reply:1": {
status: "resolved",
reply: "已补充客户编号",
replied_at: "2026-06-07T08:03:00.000Z",
source: "management-api",
},
});
expect(Array.from(mockState.db?.events.values() ?? [])).toEqual(expect.arrayContaining([
expect.objectContaining({ kind: "digital_workday_notification_updates_pulled" }),
]));
});
it("keeps local notification overlays when remote notification update pull fails", async () => {
mockState.settings.set("digital_employee_ui_state_v1", {
notificationStateById: {
"task:existing": { status: "read", read_at: "2026-06-07T07:30:00.000Z" },
},
});
mockState.fetch.mockResolvedValue({
ok: false,
status: 503,
text: vi.fn().mockResolvedValue(JSON.stringify({ message: "notification service unavailable" })),
} as unknown as Response);
const { pullDigitalEmployeeNotificationUpdates } = await import("./syncService");
const { readDigitalEmployeeUiState } = await import("./stateService");
const result = await pullDigitalEmployeeNotificationUpdates({ force: true });
expect(result).toMatchObject({
ok: false,
endpoint: "https://manage.example.com/api/digital-employee/notifications/updates",
applied: 0,
ignored: 0,
error: "notification service unavailable",
});
expect(readDigitalEmployeeUiState().notificationStateById).toMatchObject({
"task:existing": { status: "read", read_at: "2026-06-07T07:30:00.000Z" },
});
expect(Array.from(mockState.db?.events.values() ?? [])).toEqual(expect.arrayContaining([
expect.objectContaining({ kind: "digital_workday_notification_updates_pull_failed" }),
]));
});
it("submits notification actions to management and records submission events", async () => {
mockState.fetch.mockResolvedValue(jsonResponse({ success: true, data: { accepted: true } }));
const { submitDigitalEmployeeNotificationAction } = await import("./syncService");
const result = await submitDigitalEmployeeNotificationAction({
notification_id: "task:need-input:1",
action: "reply",
status: "read",
reply: "已补充客户编号",
replied_at: "2026-06-07T08:03:00.000Z",
});
expect(result).toMatchObject({
ok: true,
endpoint: "https://manage.example.com/api/digital-employee/notifications/actions",
submitted_at: "2026-06-07T08:00:00.000Z",
});
expect(mockState.fetch).toHaveBeenCalledWith(
"https://manage.example.com/api/digital-employee/notifications/actions",
expect.objectContaining({
method: "POST",
body: expect.stringContaining("task:need-input:1"),
}),
);
expect(lastSyncRequestBody()).toMatchObject({
notification_id: "task:need-input:1",
action: "reply",
status: "read",
reply: "已补充客户编号",
submitted_at: "2026-06-07T08:00:00.000Z",
device_id: "device-001",
});
expect(Array.from(mockState.db?.events.values() ?? [])).toEqual(expect.arrayContaining([
expect.objectContaining({ kind: "digital_workday_notification_action_submitted" }),
]));
});
it("records notification action submit failures without throwing", async () => {
mockState.fetch.mockResolvedValue({
ok: false,
status: 500,
text: vi.fn().mockResolvedValue(JSON.stringify({ message: "notification action rejected" })),
} as unknown as Response);
const { submitDigitalEmployeeNotificationAction } = await import("./syncService");
const result = await submitDigitalEmployeeNotificationAction({ notification_id: "task:need-input:1", action: "read" });
expect(result).toMatchObject({
ok: false,
endpoint: "https://manage.example.com/api/digital-employee/notifications/actions",
submitted_at: "2026-06-07T08:00:00.000Z",
error: "notification action rejected",
});
expect(Array.from(mockState.db?.events.values() ?? [])).toEqual(expect.arrayContaining([
expect.objectContaining({ kind: "digital_workday_notification_action_submit_failed" }),
]));
});
it.each([
["skillPolicies"],
["policies"],

View File

@@ -28,6 +28,8 @@ const DEFAULT_RISK_APPROVAL_POLICY_PATH = "/api/digital-employee/policies/risk-a
const DEFAULT_ROUTE_DECISION_POLICY_PATH = "/api/digital-employee/policies/route-decision";
const DEFAULT_APPROVAL_UPDATES_PATH = "/api/digital-employee/approvals/updates";
const DEFAULT_APPROVAL_DECISIONS_PATH = "/api/digital-employee/approvals/decisions";
const DEFAULT_NOTIFICATION_UPDATES_PATH = "/api/digital-employee/notifications/updates";
const DEFAULT_NOTIFICATION_ACTIONS_PATH = "/api/digital-employee/notifications/actions";
const DEFAULT_PLAN_STEP_DISPATCH_LEASE_PATH = "/api/digital-employee/leases/plan-step-dispatch";
const MANAGEMENT_SYNC_RECORD_PATH =
"/system/content/content-digital-employee-sync";
@@ -54,6 +56,8 @@ let routeDecisionPolicyPulling = false;
let lastRouteDecisionPolicyPullAttemptMs = 0;
let approvalUpdatesPulling = false;
let lastApprovalUpdatesPullAttemptMs = 0;
let notificationUpdatesPulling = false;
let lastNotificationUpdatesPullAttemptMs = 0;
function getDigitalEmployeeDb(): ReturnType<typeof getDb> {
if (!ensureDigitalEmployeeSchema()) return null;
@@ -69,6 +73,8 @@ interface DigitalEmployeeSyncConfig {
routeDecisionPolicyEndpoint: string | null;
approvalUpdatesEndpoint: string | null;
approvalDecisionsEndpoint: string | null;
notificationUpdatesEndpoint: string | null;
notificationActionsEndpoint: string | null;
leaseEndpoint: string | null;
clientKey: string | null;
authToken: string | null;
@@ -128,6 +134,23 @@ export interface DigitalEmployeeApprovalDecisionSubmitResult {
error?: string | null;
}
export interface DigitalEmployeeNotificationUpdatesPullResult {
ok: boolean;
skipped?: boolean;
endpoint: string | null;
pulled_at: string | null;
applied: number;
ignored: number;
error?: string | null;
}
export interface DigitalEmployeeNotificationActionSubmitResult {
ok: boolean;
endpoint: string | null;
submitted_at: string | null;
error?: string | null;
}
export interface DigitalEmployeePlanStepRemoteLease {
lease_id: string;
state: "active" | "released" | "expired";
@@ -652,6 +675,154 @@ export async function submitDigitalEmployeeApprovalDecision(
}
}
export async function pullDigitalEmployeeNotificationUpdates(
options: { force?: boolean } = {},
): Promise<DigitalEmployeeNotificationUpdatesPullResult> {
if (notificationUpdatesPulling) {
return { ok: true, skipped: true, endpoint: resolveSyncConfig().notificationUpdatesEndpoint, pulled_at: null, applied: 0, ignored: 0 };
}
const nowMs = Date.now();
if (!options.force && nowMs - lastNotificationUpdatesPullAttemptMs < POLICY_PULL_THROTTLE_MS) {
return { ok: true, skipped: true, endpoint: resolveSyncConfig().notificationUpdatesEndpoint, pulled_at: null, applied: 0, ignored: 0 };
}
lastNotificationUpdatesPullAttemptMs = nowMs;
const config = resolveSyncConfig();
const missingCredentials = missingSyncCredentialLabels(config);
if (!config.notificationUpdatesEndpoint || missingCredentials.length > 0) {
const pulledAt = new Date().toISOString();
const error = !config.notificationUpdatesEndpoint ? "management notification updates endpoint unavailable" : `missing credentials: ${missingCredentials.join(", ")}`;
recordNotificationSyncEvent("notification_updates_pull_failed", pulledAt, {
action: "pull_notification_updates",
endpoint: config.notificationUpdatesEndpoint,
ok: false,
applied: 0,
ignored: 0,
error,
source: "management-api",
});
return { ok: false, endpoint: config.notificationUpdatesEndpoint, pulled_at: pulledAt, applied: 0, ignored: 0, error };
}
notificationUpdatesPulling = true;
try {
const response = await fetchJsonWithAuth(config, config.notificationUpdatesEndpoint);
const responseError = readSyncResponseError(response as SyncResponse);
if (responseError) throw new Error(responseError);
const updates = readRemoteNotificationUpdates(response);
const pulledAt = new Date().toISOString();
const currentState = readDigitalEmployeeUiState();
const notificationStateById: Record<string, unknown> = { ...(currentState.notificationStateById ?? {}) };
let applied = 0;
let ignored = 0;
for (const update of updates) {
if (!update.notificationId) {
ignored += 1;
continue;
}
const currentOverlay = objectRecord(notificationStateById[update.notificationId]) ?? {};
notificationStateById[update.notificationId] = {
...currentOverlay,
...update.overlay,
};
applied += 1;
}
saveDigitalEmployeeUiState({
action: "notification_updates_pulled",
metadata: {
action: "pull_notification_updates",
endpoint: config.notificationUpdatesEndpoint,
ok: true,
applied,
ignored,
source: "management-api",
},
state: {
...currentState,
notificationStateById,
},
});
return { ok: true, endpoint: config.notificationUpdatesEndpoint, pulled_at: pulledAt, applied, ignored, error: null };
} catch (error) {
const pulledAt = new Date().toISOString();
const message = normalizeError(error);
recordNotificationSyncEvent("notification_updates_pull_failed", pulledAt, {
action: "pull_notification_updates",
endpoint: config.notificationUpdatesEndpoint,
ok: false,
applied: 0,
ignored: 0,
error: message,
source: "management-api",
});
return { ok: false, endpoint: config.notificationUpdatesEndpoint, pulled_at: pulledAt, applied: 0, ignored: 0, error: message };
} finally {
notificationUpdatesPulling = false;
}
}
export async function submitDigitalEmployeeNotificationAction(
input: Record<string, unknown>,
): Promise<DigitalEmployeeNotificationActionSubmitResult> {
const config = resolveSyncConfig();
const missingCredentials = missingSyncCredentialLabels(config);
const submittedAt = new Date().toISOString();
const notificationId = stringField(input.notification_id ?? input.notificationId) || null;
const notificationAction = stringField(input.action) || null;
const status = stringField(input.status) || null;
const reply = stringField(input.reply ?? input.content ?? input.message);
if (!config.notificationActionsEndpoint || missingCredentials.length > 0) {
const error = !config.notificationActionsEndpoint ? "management notification actions endpoint unavailable" : `missing credentials: ${missingCredentials.join(", ")}`;
recordNotificationSyncEvent("notification_action_submit_failed", submittedAt, {
action: "submit_notification_action",
ok: false,
endpoint: config.notificationActionsEndpoint,
error,
notification_id: notificationId,
notification_action: notificationAction,
status,
reply_length: reply ? reply.length : null,
source: "qimingclaw-notification-sync",
});
return { ok: false, endpoint: config.notificationActionsEndpoint, submitted_at: submittedAt, error };
}
try {
const response = await fetchJsonWithAuth(config, config.notificationActionsEndpoint, {
...input,
acted_at: stringField(input.acted_at ?? input.actedAt) || submittedAt,
submitted_at: submittedAt,
device_id: getDeviceId(),
});
const responseError = readSyncResponseError(response as SyncResponse);
if (responseError) throw new Error(responseError);
recordNotificationSyncEvent("notification_action_submitted", submittedAt, {
action: "submit_notification_action",
ok: true,
endpoint: config.notificationActionsEndpoint,
notification_id: notificationId,
notification_action: notificationAction,
status,
reply_length: reply ? reply.length : null,
source: "qimingclaw-notification-sync",
});
return { ok: true, endpoint: config.notificationActionsEndpoint, submitted_at: submittedAt, error: null };
} catch (error) {
const message = normalizeError(error);
recordNotificationSyncEvent("notification_action_submit_failed", submittedAt, {
action: "submit_notification_action",
ok: false,
endpoint: config.notificationActionsEndpoint,
error: message,
notification_id: notificationId,
notification_action: notificationAction,
status,
reply_length: reply ? reply.length : null,
source: "qimingclaw-notification-sync",
});
return { ok: false, endpoint: config.notificationActionsEndpoint, submitted_at: submittedAt, error: message };
}
}
export async function acquireDigitalEmployeePlanStepRemoteLease(
input: DigitalEmployeePlanStepRemoteLeaseAcquireInput,
): Promise<DigitalEmployeePlanStepRemoteLeaseResult> {
@@ -881,6 +1052,16 @@ function resolveSyncConfig(): DigitalEmployeeSyncConfig {
? config.approvalDecisionsEndpoint.trim()
: null;
const approvalDecisionsEndpoint = approvalDecisionsEndpointOverride || buildEndpoint(serverHost, DEFAULT_APPROVAL_DECISIONS_PATH);
const notificationUpdatesEndpointOverride =
typeof config.notificationUpdatesEndpoint === "string" && config.notificationUpdatesEndpoint.trim()
? config.notificationUpdatesEndpoint.trim()
: null;
const notificationUpdatesEndpoint = notificationUpdatesEndpointOverride || buildEndpoint(serverHost, DEFAULT_NOTIFICATION_UPDATES_PATH);
const notificationActionsEndpointOverride =
typeof config.notificationActionsEndpoint === "string" && config.notificationActionsEndpoint.trim()
? config.notificationActionsEndpoint.trim()
: null;
const notificationActionsEndpoint = notificationActionsEndpointOverride || buildEndpoint(serverHost, DEFAULT_NOTIFICATION_ACTIONS_PATH);
const leaseEndpointOverride =
typeof config.planStepDispatchLeaseEndpoint === "string" && config.planStepDispatchLeaseEndpoint.trim()
? config.planStepDispatchLeaseEndpoint.trim()
@@ -901,6 +1082,8 @@ function resolveSyncConfig(): DigitalEmployeeSyncConfig {
routeDecisionPolicyEndpoint,
approvalUpdatesEndpoint,
approvalDecisionsEndpoint,
notificationUpdatesEndpoint,
notificationActionsEndpoint,
leaseEndpoint,
clientKey: readClientKey(serverHost),
authToken: readAuthToken(serverHost),
@@ -1098,6 +1281,41 @@ function readRemoteApprovalUpdates(response: Record<string, unknown>): DigitalEm
.filter((item) => Boolean(item.approvalId || item.remoteId));
}
function readRemoteNotificationUpdates(response: Record<string, unknown>): Array<{ notificationId: string | null; overlay: Record<string, unknown> }> {
const data = objectRecord(response.data) ?? response;
const raw = Array.isArray(data.notifications)
? data.notifications
: Array.isArray(data.updates)
? data.updates
: Array.isArray(data.items)
? data.items
: Array.isArray(response)
? response
: [];
return raw
.map((item) => objectRecord(item))
.filter((item): item is Record<string, unknown> => Boolean(item))
.map((item) => {
const notificationId = stringField(item.notification_id ?? item.notificationId ?? item.id) || null;
const overlay: Record<string, unknown> = {};
const status = stringField(item.status);
const readAt = stringField(item.read_at ?? item.readAt);
const dismissedAt = stringField(item.dismissed_at ?? item.dismissedAt);
const reply = stringField(item.reply ?? item.content ?? item.message);
const repliedAt = stringField(item.replied_at ?? item.repliedAt);
const remoteUpdatedAt = stringField(item.remote_updated_at ?? item.remoteUpdatedAt ?? item.updated_at ?? item.updatedAt);
const source = stringField(item.source) || "management-api";
if (status) overlay.status = status;
if (readAt) overlay.read_at = readAt;
if (dismissedAt) overlay.dismissed_at = dismissedAt;
if (reply) overlay.reply = reply;
if (repliedAt) overlay.replied_at = repliedAt;
if (remoteUpdatedAt) overlay.remote_updated_at = remoteUpdatedAt;
overlay.source = source;
return { notificationId, overlay };
});
}
function readPlanStepLeaseRejectionReason(response: Record<string, unknown>): string | null {
if (response.ok === false || response.success === false || response.accepted === false || response.granted === false) {
return stringField(response.reason ?? response.error ?? response.message) || "remote_lease_rejected";
@@ -1304,6 +1522,15 @@ function recordApprovalSyncEvent(action: string, occurredAt: string, metadata: R
});
}
function recordNotificationSyncEvent(action: string, occurredAt: string, metadata: Record<string, unknown>): void {
const state = readDigitalEmployeeUiState();
saveDigitalEmployeeUiState({
action,
metadata: { ...metadata, occurred_at: occurredAt },
state,
});
}
function buildSyncItem(row: OutboxRow): Record<string, unknown> {
const payload = parsePayload(row.payload);
const summary = readEntitySummary(row.entity_type, row.entity_id);
@@ -1783,11 +2010,18 @@ function notificationBusinessView(payload: Record<string, unknown>): Record<stri
return {
notification_id: notificationId,
action: stringField(metadata.action ?? payload.action) || null,
notification_action: stringField(metadata.notification_action ?? payload.notification_action) || null,
status: stringField(overlay.status ?? payload.status) || null,
read_at: stringField(overlay.read_at ?? payload.read_at ?? payload.readAt) || null,
dismissed_at: stringField(overlay.dismissed_at ?? payload.dismissed_at ?? payload.dismissedAt) || null,
replied_at: stringField(overlay.replied_at ?? payload.replied_at ?? payload.repliedAt) || null,
reply_length: reply ? reply.length : null,
source: stringField(metadata.source) || null,
endpoint: stringField(metadata.endpoint) || null,
ok: typeof metadata.ok === "boolean" ? metadata.ok : null,
applied: numberField(metadata.applied),
ignored: numberField(metadata.ignored),
error: stringField(metadata.error) || null,
reply_length: numberField(metadata.reply_length) ?? (reply ? reply.length : null),
preferences_enabled: typeof preferences.enabled === "boolean" ? preferences.enabled : null,
muted_kinds: stringArrayField(preferences.muted_kinds ?? preferences.mutedKinds),
muted_levels: stringArrayField(preferences.muted_levels ?? preferences.mutedLevels),
@@ -1839,7 +2073,11 @@ function isNotificationEvent(kind: string): boolean {
return kind === "digital_workday_update_notification_preferences"
|| kind === "digital_workday_notification_read"
|| kind === "digital_workday_notification_dismiss"
|| kind === "digital_workday_notification_reply";
|| kind === "digital_workday_notification_reply"
|| kind === "digital_workday_notification_updates_pulled"
|| kind === "digital_workday_notification_updates_pull_failed"
|| kind === "digital_workday_notification_action_submitted"
|| kind === "digital_workday_notification_action_submit_failed";
}
function routeDecisionAttentionLevel(policyResult: string, reasonCodes: string[], createdCommandId: string | null): "info" | "action" | "warn" | "error" {