feat(workbench): integrate operational workflows and map state

This commit is contained in:
2026-09-24 04:34:46 +08:00
parent 1752b9ef84
commit 436d5500f1
76 changed files with 5081 additions and 1937 deletions
+145 -30
View File
@@ -36,15 +36,13 @@ import { AgentTaskTicker } from "./components/agent-task-ticker";
import { AnalysisResultsPanel } from "./components/analysis-results-panel";
import { PeripheralSamplePanel } from "./peripheral-sample/peripheral-sample-panel";
import type { PeripheralWorkOrder } from "./peripheral-sample/client";
import {
toWaterQualityConditionTask,
toWaterQualityAgentTrigger,
waterQualityTriggerPrompt
} from "./peripheral-sample/work-order-agent-trigger";
import { MapDevPanel } from "./components/map-dev-panel";
import { MobileWorkbenchSheet } from "./components/mobile-workbench-sheet";
import { FeaturePopover } from "./components/feature-popover";
import { ScheduledConditionFeed } from "./components/scheduled-condition-feed";
import {
ScheduledConditionFeed,
type ScheduleCreateRequest
} from "./components/scheduled-condition-feed";
import { WorkbenchAgentPanels } from "./components/workbench-agent-panels";
import { OperationalTimeline } from "./operational-timeline/operational-timeline";
import {
@@ -70,6 +68,8 @@ import {
} from "./workspace/workspace-context";
import { WORKBENCH_USER } from "./data/workbench-session";
import { useWorkbenchAgent } from "./hooks/use-workbench-agent";
import { operationalTaskConditionId, useOperationalTasks } from "./hooks/use-operational-tasks";
import { useWorkbenchSync } from "./hooks/use-workbench-sync";
import { useWorkbenchDrawing, type WorkbenchDrawMode } from "./hooks/use-workbench-drawing";
import { useWorkbenchMap } from "./hooks/use-workbench-map";
import { useWorkbenchMapController } from "./hooks/use-workbench-map-controller";
@@ -104,6 +104,7 @@ import type {
DetailFeature,
ScheduledConditionItem,
ScheduledConditionRecord,
ScheduledConditionTaskId,
WorkbenchAlert,
WorkbenchUser
} from "./types";
@@ -112,6 +113,17 @@ import { createAlertQueueConversationPrompt } from "./utils/scheduled-condition-
const WORKBENCH_LAYOUT_CSS_VARIABLES = getWorkbenchLayoutCssVariables();
const SCHEDULED_CONDITION_TASK_IDS = new Set<ScheduledConditionTaskId>([
"scada-diagnosis",
"smart-dispatch",
"pump-energy",
"network-simulation"
]);
function isScheduledConditionTaskId(value: string): value is ScheduledConditionTaskId {
return SCHEDULED_CONDITION_TASK_IDS.has(value as ScheduledConditionTaskId);
}
export function MapWorkbenchPage({
user = WORKBENCH_USER,
onLogout,
@@ -150,11 +162,15 @@ export function MapWorkbenchPage({
const [layerVisibility, setLayerVisibility] =
useState<Record<string, boolean>>(INITIAL_LAYER_VISIBILITY);
const [agentUiResults, setAgentUiResults] = useState<AgentUiResult[]>([]);
const [createdMockScheduledTasks, setCreatedMockScheduledTasks] = useState<
ScheduledConditionRecord[]
>([]);
const workbenchSync = useWorkbenchSync(getAccessToken);
const {
headerDataTime,
scheduledConditions: runtimeScheduledConditions,
scheduledConditionsLoading,
workbenchAlerts,
workbenchAlerts: runtimeWorkbenchAlerts,
runtimeDataSource,
alertDataSource,
latestAnalysisArtifact,
@@ -163,12 +179,8 @@ export function MapWorkbenchPage({
flowDataSource,
operationalDate,
operationalCursorTime
} = useWorkbenchRuntimeData(getAccessToken);
const [eventConditions, setEventConditions] = useState<ScheduledConditionRecord[]>([]);
const scheduledConditions = useMemo(
() => [...eventConditions, ...runtimeScheduledConditions],
[eventConditions, runtimeScheduledConditions]
);
} = useWorkbenchRuntimeData(getAccessToken, workbenchSync);
const operationalTasks = useOperationalTasks(getAccessToken, workbenchSync);
const baseLayerOptions = useMemo(
() =>
BASE_LAYER_OPTIONS.map((option) => ({
@@ -194,6 +206,51 @@ export function MapWorkbenchPage({
onFrontendAction: handleFrontendAction,
getAccessToken
});
const scheduledConditions = useMemo(() => [
...createdMockScheduledTasks,
...runtimeScheduledConditions,
...(operationalTasks.available
? operationalTasks.conditions.filter((condition) =>
condition.operationalTask?.task_type === "event_investigation"
|| condition.operationalTask?.task_type === "external_work_order"
)
: [])
], [createdMockScheduledTasks, operationalTasks.available, operationalTasks.conditions, runtimeScheduledConditions]);
const handleCreateMockScheduledTask = useCallback(async (input: ScheduleCreateRequest) => {
const taskId = isScheduledConditionTaskId(input.scheduleKey)
? input.scheduleKey
: "scada-diagnosis";
const condition: ScheduledConditionRecord = {
id: `local-scheduled-task-${crypto.randomUUID()}`,
kind: "condition",
taskId,
dataSource: "mock",
sessionId: `scheduled-condition-${taskId}-${Date.now()}`,
title: input.title,
summary: input.summary,
status: "pending",
riskLevel: "normal",
scheduledAt: input.nextRunAt,
updatedAt: Date.now(),
detail: `按 ${input.intervalSeconds / 60} 分钟周期执行,仅用于当前前端演示。`,
recommendation: "等待模拟执行窗口。",
modelName: "SupplyFlow 前端演示模拟器"
};
setCreatedMockScheduledTasks((current) => [condition, ...current]);
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))
}));
return operationalTasks.available ? [...taskNotifications, ...runtimeWorkbenchAlerts] : runtimeWorkbenchAlerts;
}, [operationalTasks.available, operationalTasks.notifications, runtimeWorkbenchAlerts]);
const collapseAgentPanel = agent.collapsePanel;
const expandAgentPanel = agent.expandPanel;
const clearActiveTool = useCallback(() => {
@@ -301,44 +358,56 @@ export function MapWorkbenchPage({
}
async function handlePeripheralWorkOrderAgentTrigger(workOrder: PeripheralWorkOrder) {
const trigger = toWaterQualityAgentTrigger(workOrder);
if (!trigger) {
throw new Error("工单不符合水质投诉事件触发条件。");
const burstReport = workOrder.work_order_type === "burst_report";
if (workOrder.work_order_type !== "water_quality_complaint" && workOrder.work_order_type !== "burst_report") {
throw new Error("工单不符合事件调查触发条件。");
}
const investigationTitle = burstReport ? "爆管事件调查" : "水质投诉调查";
showMapNotice({
id: "water-quality-agent-trigger",
id: "peripheral-event-agent-trigger",
tone: "loading",
title: "水质投诉事件已接收",
title: `${burstReport ? "爆管上报" : "水质投诉"}已接收`,
message: "正在建立结构化工况任务。",
duration: Infinity
});
try {
const result = await agent.triggerEvent(trigger, waterQualityTriggerPrompt(workOrder));
const condition = toWaterQualityConditionTask(workOrder, result.sessionId);
setEventConditions((current) => [
condition,
...current.filter((item) => item.id !== condition.id)
]);
const task = await operationalTasks.createInvestigation({
kind: burstReport ? "burst_report" : "water_quality_complaint",
title: investigationTitle,
summary: workOrder.summary,
detail: `${workOrder.location ?? "位置待确认"} · 由外围工单事件触发`,
occurredAt: workOrder.scheduled_at ?? workOrder.created_at,
priority: burstReport || workOrder.priority === "urgent" ? "urgent" : "normal",
source: {
kind: "external_system",
external_id: workOrder.work_order_id,
external_revision: workOrder.revision,
event_kind: burstReport ? "burst_report" : "water_quality_complaint"
},
triggerKind: "event"
});
setPeripheralSamplePanelOpen(false);
openConditionWorkspaceForViewport();
setSelectedConditionId(condition.id);
const conditionId = operationalTaskConditionId(task.task_id);
setSelectedConditionId(conditionId);
setConditionFocusRequest((current) => ({
conditionId: condition.id,
conditionId,
requestId: (current?.requestId ?? 0) + 1
}));
showMapNotice({
id: "water-quality-agent-trigger",
id: "peripheral-event-agent-trigger",
tone: "success",
title: result.reused ? "已恢复水质工况任务" : "已建立水质工况任务",
title: `已建立${investigationTitle}`,
message: `关联模拟工单 ${workOrder.work_order_id.slice(0, 8)},可在任务中查看处理流程。`
});
} catch (error) {
showMapNotice({
id: "water-quality-agent-trigger",
id: "peripheral-event-agent-trigger",
tone: "error",
title: "工单已创建,Agent 待触发",
title: "工况任务创建失败",
message: error instanceof Error ? error.message : "Agent 事件入口当前不可用。"
});
throw error;
@@ -711,6 +780,11 @@ export function MapWorkbenchPage({
}
function handleSelectAlert(alert: WorkbenchAlert) {
if (alert.id.startsWith("operational-task-notification:")) {
const notificationId = alert.id.slice("operational-task-notification:".length);
const notification = operationalTasks.notifications.find((item) => item.notification_id === notificationId);
if (notification) void operationalTasks.acknowledgeNotification(notificationId, notification.version);
}
const conditionId = alert.conditionId ?? getScheduledConditionIdFromAlertId(alert.id);
if (conditionId) {
const condition = scheduledConditions.find((item) => item.id === conditionId);
@@ -809,6 +883,11 @@ export function MapWorkbenchPage({
await agent.loadHistorySession(sessionId);
}
async function handleOpenTaskCollaboration(taskId: string) {
mapController.clearScadaAnalysis();
await agent.openTaskCollaboration(taskId);
}
async function handleDeleteAgentHistorySession(sessionId: string) {
const clearsActiveAnalysis = sessionId === agent.sessionId;
const deleted = await agent.deleteHistorySession(sessionId);
@@ -1003,6 +1082,7 @@ export function MapWorkbenchPage({
sessionHistory: agent.sessionHistory,
sessionHistoryLoading: agent.sessionHistoryLoading,
activeSessionId: agent.sessionId,
qaOnly: agent.sessionPurpose === "task_collaboration",
statusLabel: agent.statusLabel,
connectionStatus: agent.connectionStatus,
streaming: agent.streaming,
@@ -1086,6 +1166,23 @@ export function MapWorkbenchPage({
onLogout={onLogout}
/>
{workbenchSync.status === "reconnecting" && workbenchSync.snapshot ? (
<div
role="status"
className="absolute left-1/2 top-[3.7rem] z-[70] -translate-x-1/2 rounded-md border border-amber-300/70 bg-amber-50/95 px-3 py-1.5 text-xs font-medium text-amber-900 shadow-xs backdrop-blur"
>
正在重连,业务数据可能陈旧
</div>
) : null}
{analysisResultsPanelOpen || peripheralSamplePanelOpen ? (
<div
aria-hidden="true"
data-map-interaction-shield
className="pointer-events-auto absolute inset-x-0 bottom-0 top-14 z-[59] touch-none overscroll-none"
/>
) : null}
<AnalysisResultsPanel
open={analysisResultsPanelOpen}
onClose={() => setAnalysisResultsPanelOpen(false)}
@@ -1223,6 +1320,8 @@ export function MapWorkbenchPage({
expanded={conditionFeedExpanded}
focusRequest={conditionFocusRequest}
loading={scheduledConditionsLoading}
backendLoading={operationalTasks.loading}
backendError={operationalTasks.error}
visible={shouldShowConditionFeed}
selectedConditionId={selectedConditionId}
onExpandedChange={handleConditionExpandedChange}
@@ -1231,6 +1330,13 @@ export function MapWorkbenchPage({
onExpandAgent={openAgentWorkspaceForViewport}
onLoadHistorySession={handleLoadAgentHistorySession}
onSubmitPrompt={agent.submitPrompt}
onOpenTaskCollaboration={handleOpenTaskCollaboration}
onTaskDecision={operationalTasks.decideAction}
onTaskExecute={operationalTasks.executeAction}
onTaskReconcile={operationalTasks.reconcileAction}
onLoadTaskEvidenceSnapshot={operationalTasks.loadEvidenceSnapshot}
onTaskRefresh={operationalTasks.refresh}
onCreateSchedule={handleCreateMockScheduledTask}
/>
</div>
) : null}
@@ -1360,6 +1466,8 @@ export function MapWorkbenchPage({
expanded={conditionFeedExpanded}
focusRequest={conditionFocusRequest}
loading={scheduledConditionsLoading}
backendLoading={operationalTasks.loading}
backendError={operationalTasks.error}
visible
selectedConditionId={selectedConditionId}
onExpandedChange={handleConditionExpandedChange}
@@ -1369,6 +1477,13 @@ export function MapWorkbenchPage({
onClose={closeMobileSheet}
onLoadHistorySession={handleLoadAgentHistorySession}
onSubmitPrompt={agent.submitPrompt}
onOpenTaskCollaboration={handleOpenTaskCollaboration}
onTaskDecision={operationalTasks.decideAction}
onTaskExecute={operationalTasks.executeAction}
onTaskReconcile={operationalTasks.reconcileAction}
onLoadTaskEvidenceSnapshot={operationalTasks.loadEvidenceSnapshot}
onTaskRefresh={operationalTasks.refresh}
onCreateSchedule={handleCreateMockScheduledTask}
/>
)}
</MobileWorkbenchSheet>