feat(workbench): integrate operational workflows and map state
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user