import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { AccessTokenProvider } from "@/shared/auth/keycloak-auth"; import { acknowledgeOperationalTaskNotification, createOperationalRequestId, createOperationalTask, decideOperationalTaskAction, executeOperationalTaskAction, fetchOperationalTask, fetchOperationalTaskEvidenceSnapshot, fetchOperationalTaskNotifications, reconcileOperationalTaskAction, type OperationalTask, type OperationalTaskDecision, type OperationalTaskNotification } from "../api/operational-task-client"; import type { ScheduledConditionRecord, ScheduledConditionRiskLevel, ScheduledConditionStatus } from "../types"; import type { WorkbenchSyncState } from "./use-workbench-sync"; export type ManualInvestigationKind = "water_quality_complaint" | "burst_report"; export type CreateInvestigationInput = { kind: ManualInvestigationKind; title: string; summary: string; detail: string; occurredAt: string; priority?: OperationalTask["priority"]; source?: OperationalTask["source"]; triggerKind?: "event" | "manual"; }; export function useOperationalTasks( getAccessToken: AccessTokenProvider | undefined, sync: WorkbenchSyncState ) { const syncEvents = sync.events; 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); const refresh = useCallback(() => { forceRefreshRef.current = true; setRefreshGeneration((current) => current + 1); }, []); useEffect(() => { const controller = new AbortController(); const load = async () => { const summaries = sync.snapshot?.tasks ?? []; try { const current = new Map(tasksRef.current.map((task) => [task.task_id, task])); const changed = forceRefreshRef.current ? summaries : summaries.filter((summary) => current.get(summary.task_id)?.revision !== summary.revision); const details = await Promise.all(changed.map((task) => fetchOperationalTask(task.task_id, getAccessToken, controller.signal) )); if (controller.signal.aborted) return; const detailMap = new Map(details.map((task) => [task.task_id, task])); const nextTasks = summaries.flatMap((summary) => { const detail = detailMap.get(summary.task_id) ?? current.get(summary.task_id); return detail ? [detail] : []; }); tasksRef.current = nextTasks; forceRefreshRef.current = false; setTasks(nextTasks); setNotifications((sync.snapshot?.notifications ?? []).filter((item) => item.status === "unread")); 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) { setLoading(false); } } }; if (sync.snapshot) void load(); return () => { controller.abort(); }; }, [getAccessToken, refreshGeneration, sync.snapshot]); useEffect(() => { const events = syncEvents; if (events.length === 0) return; const controller = new AbortController(); const lastSequence = events.at(-1)!.event_seq; const taskIds = new Set(); let notificationsChanged = false; for (const event of events) { if (event.topic === "operational.notification.changed") notificationsChanged = true; if (!event.topic.startsWith("operational.") || event.topic === "operational.notification.changed") continue; const taskId = typeof event.metadata.task_id === "string" ? event.metadata.task_id : event.aggregate_type === "operational_task" ? event.aggregate_id : null; if (taskId) taskIds.add(taskId); } void Promise.all([ notificationsChanged ? fetchOperationalTaskNotifications(getAccessToken, controller.signal) : Promise.resolve(null), Promise.all([...taskIds].map((taskId) => fetchOperationalTask(taskId, getAccessToken, controller.signal) )) ]).then(([nextNotifications, changedTasks]) => { if (controller.signal.aborted) return; if (nextNotifications) setNotifications(nextNotifications); if (changedTasks.length > 0) { const changed = new Map(changedTasks.map((task) => [task.task_id, task])); tasksRef.current = [ ...changedTasks, ...tasksRef.current.filter((task) => !changed.has(task.task_id)) ]; setTasks(tasksRef.current); setLastSyncedAt(new Date().toISOString()); } acknowledgeEventsThrough(lastSequence); }).catch((nextError) => { if (!controller.signal.aborted) { setError(nextError instanceof Error ? nextError.message : "工况任务增量刷新失败。"); } }); return () => controller.abort(); }, [acknowledgeEventsThrough, getAccessToken, syncEvents]); const conditions = useMemo(() => tasks.map(toOperationalTaskCondition), [tasks]); const createInvestigation = useCallback(async (input: CreateInvestigationInput) => { const task = await createOperationalTask({ request_id: createOperationalRequestId(), task_type: "event_investigation", workflow: input.kind, title: input.title, summary: input.summary, priority: input.priority ?? (input.kind === "burst_report" ? "urgent" : "normal"), source: input.source ?? { kind: "operator", external_id: null, external_revision: null, event_kind: input.kind }, trigger: { kind: input.triggerKind ?? "manual", occurred_at: input.occurredAt, detail: input.detail } }, 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]); const decideAction = useCallback(async ( taskId: string, actionId: string, expectedVersion: number, decision: OperationalTaskDecision, comment?: string ) => { const action = await decideOperationalTaskAction(taskId, actionId, { request_id: createOperationalRequestId(), expected_version: expectedVersion, decision, comment }, getAccessToken); setTasks((current) => current.map((item) => item.task_id === taskId ? { ...item, actions: item.actions.map((currentAction) => currentAction.action_id === action.action_id ? action : currentAction) } : item)); return action; }, [getAccessToken]); const executeAction = useCallback(async ( taskId: string, actionId: string, expectedVersion: number ) => { const action = await executeOperationalTaskAction(taskId, actionId, { request_id: createOperationalRequestId(), expected_version: expectedVersion }, getAccessToken); setTasks((current) => current.map((item) => item.task_id === taskId ? { ...item, actions: item.actions.map((currentAction) => currentAction.action_id === action.action_id ? action : currentAction) } : item)); return action; }, [getAccessToken]); const reconcileAction = useCallback(async (taskId: string, actionId: string, expectedVersion: number) => { const action = await reconcileOperationalTaskAction(taskId, actionId, { request_id: createOperationalRequestId(), expected_version: expectedVersion }, getAccessToken); setTasks((current) => current.map((item) => item.task_id === taskId ? { ...item, actions: item.actions.map((currentAction) => currentAction.action_id === action.action_id ? action : currentAction) } : item)); return action; }, [getAccessToken]); const acknowledgeNotification = useCallback(async (notificationId: string, version: number) => { await acknowledgeOperationalTaskNotification( notificationId, version, createOperationalRequestId(), getAccessToken ); setNotifications((current) => current.filter((item) => item.notification_id !== notificationId)); }, [getAccessToken]); const loadEvidenceSnapshot = useCallback((taskId: string, evidenceId: string) => fetchOperationalTaskEvidenceSnapshot(taskId, evidenceId, getAccessToken), [getAccessToken]); return { available, error: error ?? sync.error, syncStatus: sync.status, lastSyncedAt, loading, tasks, conditions, notifications, refresh, createInvestigation, decideAction, executeAction, reconcileAction, acknowledgeNotification, loadEvidenceSnapshot }; } export function toOperationalTaskCondition(task: OperationalTask): ScheduledConditionRecord { return { id: operationalTaskConditionId(task.task_id), kind: "condition", taskId: "operational-task", dataSource: "operational_tasks", sessionId: task.active_run?.agent_session_id ?? task.agent_session_id ?? "", title: task.title, summary: task.summary, status: toConditionStatus(task), riskLevel: toRiskLevel(task), scheduledAt: task.scheduled_for ?? task.trigger.occurred_at, updatedAt: Date.parse(task.updated_at), detail: task.task_type === "external_work_order" ? task.work_order_snapshot?.summary ?? task.summary : task.trigger.detail, recommendation: task.latest_report?.report.current_conclusion || undefined, evidence: task.evidence.map((item) => item.summary), modelName: "TJWater operational task", operationalTask: task }; } export function operationalTaskConditionId(taskId: string) { return `operational-task:${taskId}`; } function toConditionStatus(task: OperationalTask): ScheduledConditionStatus { const status = task.lifecycle_status; if (status === "completed") return "completed"; if (status === "failed") return "error"; if (status === "cancelled" || status === "waiting_human") return "warning"; if (task.task_type === "external_work_order") { const externalStatus = task.work_order_snapshot?.status; if (externalStatus === "completed") return "completed"; if (externalStatus === "cancelled" || externalStatus === "awaiting_reply") return "warning"; if (externalStatus === "in_progress") return "running"; return "pending"; } return task.active_run?.status === "queued" ? "pending" : "running"; } function toRiskLevel(task: OperationalTask): ScheduledConditionRiskLevel { const { priority, lifecycle_status: status } = task; if (status === "failed" || priority === "urgent") return "critical"; if (status === "waiting_human") return "attention"; return "normal"; }