From f2438e46604e23b2327a984238d8a6412204c65a Mon Sep 17 00:00:00 2001 From: Huarch Date: Thu, 24 Sep 2026 17:11:15 +0800 Subject: [PATCH] refactor(workbench): remove redundant notification state --- .../workbench/hooks/use-operational-tasks.ts | 14 ------- src/features/workbench/map-workbench-page.tsx | 31 ++++++++------- .../operational-notification-routing.test.ts | 31 ++++++--------- .../utils/operational-notification-routing.ts | 38 ++++++------------- .../browser/operational-task-approval.e2e.ts | 3 +- 5 files changed, 41 insertions(+), 76 deletions(-) diff --git a/src/features/workbench/hooks/use-operational-tasks.ts b/src/features/workbench/hooks/use-operational-tasks.ts index 6a3c9ad..a6e2662 100644 --- a/src/features/workbench/hooks/use-operational-tasks.ts +++ b/src/features/workbench/hooks/use-operational-tasks.ts @@ -71,11 +71,8 @@ export function useOperationalTasks( const acknowledgeEventsThrough = sync.acknowledgeEventsThrough; const [tasks, setTasks] = useState([]); const [notifications, setNotifications] = useState([]); - const [available, setAvailable] = useState(false); - const activatedRef = useRef(false); const tasksRef = useRef([]); const [error, setError] = useState(null); - const [lastSyncedAt, setLastSyncedAt] = useState(null); const [loading, setLoading] = useState(true); const [refreshGeneration, setRefreshGeneration] = useState(0); const forceRefreshRef = useRef(false); @@ -111,13 +108,9 @@ export function useOperationalTasks( detailResetGenerationRef.current = sync.detailResetGeneration; setTasks(nextTasks); setNotifications(selectHeaderNotifications(sync.snapshot?.notifications ?? [])); - setAvailable(true); - activatedRef.current = true; setError(null); - setLastSyncedAt(new Date().toISOString()); } catch (nextError) { if (controller.signal.aborted) return; - if (!activatedRef.current) setAvailable(false); setError(nextError instanceof Error ? nextError.message : "工况任务服务暂时不可用。"); } finally { if (!controller.signal.aborted) { @@ -159,7 +152,6 @@ export function useOperationalTasks( ...tasksRef.current.filter((task) => !changed.has(task.task_id)) ]; setTasks(tasksRef.current); - setLastSyncedAt(new Date().toISOString()); } acknowledgeEventsThrough(lastSequence); }).catch((nextError) => { @@ -189,11 +181,8 @@ export function useOperationalTasks( }, getAccessToken); tasksRef.current = [task, ...tasksRef.current.filter((item) => item.task_id !== task.task_id)]; setTasks(tasksRef.current); - activatedRef.current = true; - setAvailable(true); setLoading(false); setError(null); - setLastSyncedAt(new Date().toISOString()); return task; }, [getAccessToken]); @@ -300,10 +289,7 @@ export function useOperationalTasks( fetchOperationalTaskEvidenceSnapshot(taskId, evidenceId, getAccessToken), [getAccessToken]); return { - available, error: error ?? sync.error, - syncStatus: sync.status, - lastSyncedAt, loading, tasks, conditions, diff --git a/src/features/workbench/map-workbench-page.tsx b/src/features/workbench/map-workbench-page.tsx index 15155f7..9c86f70 100644 --- a/src/features/workbench/map-workbench-page.tsx +++ b/src/features/workbench/map-workbench-page.tsx @@ -107,10 +107,15 @@ import type { WorkbenchUser } from "./types"; import { getScheduledConditionIdFromAlertId } from "./utils/scheduled-condition-alerts"; -import { getOperationalNotificationRoute } from "./utils/operational-notification-routing"; +import { getOperationalNotificationSeverity } from "./utils/operational-notification-routing"; import { createAlertQueueConversationPrompt } from "./utils/scheduled-condition-prompts"; const WORKBENCH_LAYOUT_CSS_VARIABLES = getWorkbenchLayoutCssVariables(); +const ALERT_TIME_FORMATTER = new Intl.DateTimeFormat("zh-CN", { + hour: "2-digit", + minute: "2-digit", + hour12: false +}); export function MapWorkbenchPage({ user = WORKBENCH_USER, @@ -212,28 +217,26 @@ export function MapWorkbenchPage({ }); const scheduledConditions = useMemo(() => [ ...runtimeScheduledConditions, - ...(operationalTasks.available - ? operationalTasks.conditions.filter((condition) => - condition.operationalTask?.task_type === "event_investigation" - || condition.operationalTask?.task_type === "external_work_order" - ) - : []) - ], [operationalTasks.available, operationalTasks.conditions, runtimeScheduledConditions]); + ...operationalTasks.conditions.filter((condition) => + condition.operationalTask?.task_type === "event_investigation" + || condition.operationalTask?.task_type === "external_work_order" + ) + ], [operationalTasks.conditions, runtimeScheduledConditions]); const workbenchAlerts = useMemo(() => { const taskNotifications = operationalTasks.notifications.flatMap((notification) => { - const route = getOperationalNotificationRoute(notification.kind); - return route ? [{ + const severity = getOperationalNotificationSeverity(notification.kind); + return severity ? [{ id: `operational-task-notification:${notification.notification_id}`, conditionId: operationalTaskConditionId(notification.task_id), title: notification.title, description: notification.description, - severity: route.severity, + 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)) + time: ALERT_TIME_FORMATTER.format(new Date(notification.created_at)) }] : []; }); - return operationalTasks.available ? [...taskNotifications, ...runtimeWorkbenchAlerts] : runtimeWorkbenchAlerts; - }, [operationalTasks.available, operationalTasks.notifications, runtimeWorkbenchAlerts]); + return [...taskNotifications, ...runtimeWorkbenchAlerts]; + }, [operationalTasks.notifications, runtimeWorkbenchAlerts]); const collapseAgentPanel = agent.collapsePanel; const expandAgentPanel = agent.expandPanel; const clearActiveTool = useCallback(() => { diff --git a/src/features/workbench/utils/operational-notification-routing.test.ts b/src/features/workbench/utils/operational-notification-routing.test.ts index 8d9c7d0..3f591d1 100644 --- a/src/features/workbench/utils/operational-notification-routing.test.ts +++ b/src/features/workbench/utils/operational-notification-routing.test.ts @@ -1,31 +1,22 @@ import { describe, expect, it } from "vitest"; -import { getOperationalNotificationRoute } from "./operational-notification-routing"; +import { getOperationalNotificationSeverity } from "./operational-notification-routing"; -describe("getOperationalNotificationRoute", () => { +describe("getOperationalNotificationSeverity", () => { it.each(["task_created", "report_updated", "task_completed"] as const)( "keeps normal lifecycle notification %s out of exception handling", - (kind) => expect(getOperationalNotificationRoute(kind)).toBeNull() + (kind) => expect(getOperationalNotificationSeverity(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("maps actionable notifications to their displayed severity", () => { + expect(getOperationalNotificationSeverity("task_failed")).toBe("critical"); + expect(getOperationalNotificationSeverity("run_delivery_degraded")).toBe("critical"); + expect(getOperationalNotificationSeverity("access_approval_pending")).toBe("warning"); + expect(getOperationalNotificationSeverity("action_approval_pending")).toBe("warning"); + expect(getOperationalNotificationSeverity("human_input_required")).toBe("warning"); + expect(getOperationalNotificationSeverity("external_coordination_failed")).toBe("critical"); }); it("keeps legacy action notifications visible during the database rollout", () => { - expect(getOperationalNotificationRoute("action_pending")?.category) - .toBe("human_approval"); + expect(getOperationalNotificationSeverity("action_pending")).toBe("warning"); }); }); diff --git a/src/features/workbench/utils/operational-notification-routing.ts b/src/features/workbench/utils/operational-notification-routing.ts index d413d69..930fade 100644 --- a/src/features/workbench/utils/operational-notification-routing.ts +++ b/src/features/workbench/utils/operational-notification-routing.ts @@ -1,33 +1,19 @@ 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 +const NOTIFICATION_SEVERITIES: Partial< + Record > = { - 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" - } + task_failed: "critical", + run_delivery_degraded: "critical", + action_pending: "warning", + access_approval_pending: "warning", + action_approval_pending: "warning", + human_input_required: "warning", + external_coordination_failed: "critical" }; -export function getOperationalNotificationRoute( +export function getOperationalNotificationSeverity( kind: OperationalTaskNotification["kind"] -): OperationalNotificationRoute | null { - return NOTIFICATION_ROUTES[kind] ?? null; +): "warning" | "critical" | null { + return NOTIFICATION_SEVERITIES[kind] ?? null; } diff --git a/tests/browser/operational-task-approval.e2e.ts b/tests/browser/operational-task-approval.e2e.ts index 3f7d4c6..e55b89d 100644 --- a/tests/browser/operational-task-approval.e2e.ts +++ b/tests/browser/operational-task-approval.e2e.ts @@ -428,8 +428,7 @@ test("后端没有调查任务时显示空态,分类标签按压不缩放", as await page.goto("/"); await page.getByRole("button", { name: "工况任务", exact: true }).press("Enter"); const conditionPanel = page.getByRole("region", { name: "工况任务", exact: true }); - const expandButton = conditionPanel.getByRole("button", { name: "展开工况任务" }); - if (await expandButton.isVisible()) await expandButton.click(); + await conditionPanel.getByRole("button", { name: "展开工况任务" }).click(); await conditionPanel.getByRole("button", { name: /调查任务/ }).click(); await expect(conditionPanel.getByText("暂无任务", { exact: true })).toBeVisible(); await expect(conditionPanel.getByText("新任务进入当前分类后会出现在这里。", { exact: true })).toBeVisible();