feat(workbench): route actionable notifications
This commit is contained in:
@@ -198,7 +198,18 @@ const operationalTaskSchema = taskSummarySchema.extend({
|
||||
const notificationSchema = z.object({
|
||||
notification_id: z.string().uuid(),
|
||||
task_id: z.string().uuid(),
|
||||
kind: z.enum(["task_created", "action_pending", "report_updated", "task_completed", "task_failed", "run_delivery_degraded"]),
|
||||
kind: z.enum([
|
||||
"task_created",
|
||||
"action_pending",
|
||||
"report_updated",
|
||||
"task_completed",
|
||||
"task_failed",
|
||||
"run_delivery_degraded",
|
||||
"access_approval_pending",
|
||||
"action_approval_pending",
|
||||
"human_input_required",
|
||||
"external_coordination_failed"
|
||||
]),
|
||||
title: z.string().min(1),
|
||||
description: z.string().min(1),
|
||||
status: z.enum(["unread", "read"]),
|
||||
|
||||
@@ -110,6 +110,7 @@ import type {
|
||||
WorkbenchUser
|
||||
} from "./types";
|
||||
import { getScheduledConditionIdFromAlertId } from "./utils/scheduled-condition-alerts";
|
||||
import { getOperationalNotificationRoute } from "./utils/operational-notification-routing";
|
||||
import { createAlertQueueConversationPrompt } from "./utils/scheduled-condition-prompts";
|
||||
|
||||
const WORKBENCH_LAYOUT_CSS_VARIABLES = getWorkbenchLayoutCssVariables();
|
||||
@@ -260,15 +261,18 @@ export function MapWorkbenchPage({
|
||||
return condition;
|
||||
}, []);
|
||||
const workbenchAlerts = useMemo<WorkbenchAlert[]>(() => {
|
||||
const taskNotifications = operationalTasks.notifications.map((notification) => ({
|
||||
id: `operational-task-notification:${notification.notification_id}`,
|
||||
conditionId: operationalTaskConditionId(notification.task_id),
|
||||
title: notification.title,
|
||||
description: notification.description,
|
||||
severity: notification.kind === "task_failed" ? "critical" as const : "warning" as const,
|
||||
occurredAt: notification.created_at,
|
||||
time: new Intl.DateTimeFormat("zh-CN", { hour: "2-digit", minute: "2-digit", hour12: false }).format(new Date(notification.created_at))
|
||||
}));
|
||||
const taskNotifications = operationalTasks.notifications.flatMap((notification) => {
|
||||
const route = getOperationalNotificationRoute(notification.kind);
|
||||
return route ? [{
|
||||
id: `operational-task-notification:${notification.notification_id}`,
|
||||
conditionId: operationalTaskConditionId(notification.task_id),
|
||||
title: notification.title,
|
||||
description: notification.description,
|
||||
severity: route.severity,
|
||||
occurredAt: notification.created_at,
|
||||
time: new Intl.DateTimeFormat("zh-CN", { hour: "2-digit", minute: "2-digit", hour12: false }).format(new Date(notification.created_at))
|
||||
}] : [];
|
||||
});
|
||||
return operationalTasks.available ? [...taskNotifications, ...runtimeWorkbenchAlerts] : runtimeWorkbenchAlerts;
|
||||
}, [operationalTasks.available, operationalTasks.notifications, runtimeWorkbenchAlerts]);
|
||||
const collapseAgentPanel = agent.collapsePanel;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getOperationalNotificationRoute } from "./operational-notification-routing";
|
||||
|
||||
describe("getOperationalNotificationRoute", () => {
|
||||
it.each(["task_created", "report_updated", "task_completed"] as const)(
|
||||
"keeps normal lifecycle notification %s out of exception handling",
|
||||
(kind) => expect(getOperationalNotificationRoute(kind)).toBeNull()
|
||||
);
|
||||
|
||||
it("routes failures, approvals, human input, and external coordination", () => {
|
||||
expect(getOperationalNotificationRoute("task_failed")).toEqual({
|
||||
category: "task_exception",
|
||||
severity: "critical"
|
||||
});
|
||||
expect(getOperationalNotificationRoute("access_approval_pending")?.category)
|
||||
.toBe("human_approval");
|
||||
expect(getOperationalNotificationRoute("action_approval_pending")?.category)
|
||||
.toBe("human_approval");
|
||||
expect(getOperationalNotificationRoute("human_input_required")?.category)
|
||||
.toBe("human_input");
|
||||
expect(getOperationalNotificationRoute("external_coordination_failed")).toEqual({
|
||||
category: "external_coordination",
|
||||
severity: "critical"
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps legacy action notifications visible during the database rollout", () => {
|
||||
expect(getOperationalNotificationRoute("action_pending")?.category)
|
||||
.toBe("human_approval");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { OperationalTaskNotification } from "../api/operational-task-client";
|
||||
|
||||
export type OperationalNotificationCategory =
|
||||
| "task_exception"
|
||||
| "human_approval"
|
||||
| "human_input"
|
||||
| "external_coordination";
|
||||
|
||||
export type OperationalNotificationRoute = {
|
||||
category: OperationalNotificationCategory;
|
||||
severity: "warning" | "critical";
|
||||
};
|
||||
|
||||
const NOTIFICATION_ROUTES: Partial<
|
||||
Record<OperationalTaskNotification["kind"], OperationalNotificationRoute>
|
||||
> = {
|
||||
task_failed: { category: "task_exception", severity: "critical" },
|
||||
run_delivery_degraded: { category: "task_exception", severity: "critical" },
|
||||
action_pending: { category: "human_approval", severity: "warning" },
|
||||
access_approval_pending: { category: "human_approval", severity: "warning" },
|
||||
action_approval_pending: { category: "human_approval", severity: "warning" },
|
||||
human_input_required: { category: "human_input", severity: "warning" },
|
||||
external_coordination_failed: {
|
||||
category: "external_coordination",
|
||||
severity: "critical"
|
||||
}
|
||||
};
|
||||
|
||||
export function getOperationalNotificationRoute(
|
||||
kind: OperationalTaskNotification["kind"]
|
||||
): OperationalNotificationRoute | null {
|
||||
return NOTIFICATION_ROUTES[kind] ?? null;
|
||||
}
|
||||
Reference in New Issue
Block a user