feat: expand intelligent investigation workspace
Generic Container CI/CD / test-build-publish (push) Successful in 1m52s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 1m52s

This commit is contained in:
2026-10-30 17:30:19 +08:00
parent ddd03dbff6
commit c0e4231b83
223 changed files with 13155 additions and 5304 deletions
+62 -90
View File
@@ -15,13 +15,11 @@ import { ReadingWorkspaceProvider } from "@/shared/ui/reading-workspace";
import {
AgentCommandPanel,
AgentPersona,
parseFrontendChart,
toTrustedMapAction,
type FrontendActionRequest,
type UIEnvelopePayload,
type AgentUiResult,
type AgentWorkspaceContextItem,
type AnswerUiSelection
type AgentWorkspaceContextItem
} from "@/features/agent";
import {
MapErrorNotice,
@@ -35,7 +33,6 @@ import {
type MapSourceStatus
} from "@/features/map/core";
import { env } from "@/shared/config/env";
import { waitForAgentPresentation } from "./map/agent-presentation";
import { getWorkbenchProject, type WorkbenchAccess } from "./api/request-context";
import type { WorkbenchProjectSelection } from "./components/workbench-top-bar-menus";
import { AgentTaskTicker } from "./components/agent-task-ticker";
@@ -48,7 +45,6 @@ import { InvestigationPresentationPanel } from "./components/investigation-prese
import { MobileWorkbenchSheet } from "./components/mobile-workbench-sheet";
import { FeaturePopover } from "./components/feature-popover";
import { ScheduledConditionFeed } from "./components/scheduled-condition-feed";
import { TaskConversationReview } from "./components/task-conversation-review";
import type { InvestigationDiscussion } from "./api/investigation-report";
import { fetchOperationalTaskRuns, type TaskExecutionHistoryTarget } from "./api/operational-task-client";
import { WorkbenchAgentPanels } from "./components/workbench-agent-panels";
@@ -104,7 +100,6 @@ import {
import { getResponsivePadding, getResponsiveWorkbenchPadding } from "./map/camera";
import { exportMapViewImage } from "./map/export-view";
import { setMapLayersVisibility } from "./map/layer-visibility";
import { parseScadaAnalysisItems } from "./map/scada-analysis";
import {
BASE_LAYER_OPTIONS,
INITIAL_LAYER_VISIBILITY,
@@ -116,7 +111,6 @@ import {
import { waterNetworkToolbarItems } from "./map/toolbar-config";
import type {
DetailFeature,
ScheduledConditionItem,
ScheduledConditionRecord,
WorkbenchAlert,
WorkbenchUser
@@ -520,6 +514,16 @@ function MapWorkbenchContent({
onClearLayers: restorePresentationLayers,
getPadding: () => mapRef.current ? getResponsiveWorkbenchPadding(mapRef.current, leftPanelOpen, rightPanelOpen, rightPanelExpanded, agentPanelWidth) : { top: 48, right: 48, bottom: 48, left: 48 }
});
const previewPresentation = useInvestigationPresentation({
scope: "preview", enabled: false, task: selectedInvestigationTask, projectId: projectPresentation.projectId,
sync: workbenchSync, getAccessToken, mapRef, mapReady, controller: mapController,
onClearLayers: () => undefined, getPadding: () => mapRef.current ? getResponsiveWorkbenchPadding(mapRef.current, leftPanelOpen, rightPanelOpen, rightPanelExpanded, agentPanelWidth) : { top: 48, right: 48, bottom: 48, left: 48 }
});
const chatPresentation = useInvestigationPresentation({
scope: "chat", scopeId: agent.sessionId, enabled: false, task: null, projectId: projectPresentation.projectId,
sync: workbenchSync, getAccessToken, mapRef, mapReady, controller: mapController,
onClearLayers: () => undefined, getPadding: () => mapRef.current ? getResponsiveWorkbenchPadding(mapRef.current, leftPanelOpen, rightPanelOpen, rightPanelExpanded, agentPanelWidth) : { top: 48, right: 48, bottom: 48, left: 48 }
});
const geocoding = useWorkbenchGeocoding({
picking: geocodingPickActive,
onPickingChange: (picking) => {
@@ -739,7 +743,7 @@ function MapWorkbenchContent({
}
const presentationLayerIds = new Set([
...presentation.highlightLayers,
...(selectedInvestigationTask?.latest_progress?.progress.diagram?.node_refs.flatMap((node) => node.map_targets?.map((target) => target.source_id) ?? []) ?? [])
...(presentation.history.selectedId === null ? selectedInvestigationTask?.latest_progress?.progress.diagram?.node_refs.flatMap((node) => node.map_targets?.map((target) => target.source_id) ?? []) ?? [] : [])
].flatMap((id) => id === "scada" ? ["scada-pressure", "scada-flow"] : [id]));
const presentationRelatedLayers = layerControlItems.filter((item) => presentationLayerIds.has(item.id)).map((item) => ({ id: item.id, label: item.label, visible: item.visible }));
@@ -942,56 +946,7 @@ function MapWorkbenchContent({
}
async function handleFrontendAction(request: FrontendActionRequest, signal: AbortSignal) {
signal.throwIfAborted();
if (request.name === "locate_features") {
const action = toTrustedMapAction("locate_features", request.params);
if (!action || action.type !== "locate_features" || !action.layer) throw new Error("INVALID_MAP_TARGET");
const result = await mapController.locateFeatures(action.layer as Parameters<typeof mapController.locateFeatures>[0], action.featureIds, signal);
return result;
}
if (request.name === "zoom_to_map") {
const action = toTrustedMapAction("zoom_to_map", request.params);
if (!action || action.type !== "zoom_to_map") throw new Error("INVALID_MAP_CAMERA");
const map = mapRef.current;
if (!map || !mapReady) throw new Error("MAP_NOT_READY");
map.jumpTo({ center: action.center, zoom: action.zoom });
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
signal.throwIfAborted();
return { fitted: true };
}
if (request.name === "show_chart" || request.name === "view_history") {
const chart = parseFrontendChart(request.name === "view_history" ? request.params.chart : request.params);
if (!chart) throw new Error("INVALID_CHART_DATA");
const data = { x_data: chart.x_data, series: chart.series };
const envelope: AgentUiResult["envelope"] = request.name === "view_history"
? { kind: "registered_component", schemaVersion: "agent-ui@1", component: "HistoryPanel", surface: "side_panel", props: { feature_id: request.params.element_id, metric: chart.title, start_time: request.params.start, end_time: request.params.end }, data }
: { kind: "chart", schemaVersion: "agent-ui@1", grammar: "echarts-safe-subset", surface: "side_panel", spec: { title: chart.title, chart_type: chart.chart_type }, data };
setAgentUiResults((current) => [...current.slice(-5), { id: request.actionId, sessionId: request.sessionId, createdAt: Date.now(), envelope }]);
await waitForAgentPresentation(request.actionId, signal);
return { displayed: true, series_count: chart.series.length };
}
if (request.name === "render_scada_analysis") {
const items = parseScadaAnalysisItems(request.params.items);
if (!items) throw new Error("INVALID_SCADA_ANALYSIS");
const result = await mapController.renderScadaAnalysis(items, signal);
const counts = result.level_counts;
showMapNotice({
tone: "success",
title: "Agent SCADA 分析已渲染",
message: `已渲染 ${result.rendered_ids.length} 个,高 ${counts.high}、中 ${counts.medium}、低 ${counts.low}${result.missing_ids.length ? `,缺失 ${result.missing_ids.length} 个` : ""}。`
});
return result;
}
if (request.name === "clear_scada_analysis") {
const result = mapController.clearScadaAnalysis();
showMapNotice({
tone: "info",
title: "Agent SCADA 分析已清除",
message: "地图已移除当前会话的 SCADA 分析结果。"
});
return result;
}
throw new Error("UNSUPPORTED_FRONTEND_ACTION");
return chatPresentation.executeCommand(request.command, signal, "chat");
}
async function handleStartNewAgentSession() {
@@ -1135,6 +1090,8 @@ function MapWorkbenchContent({
function handleClearMapOverlays() {
mapController.clearOverlays();
presentation.clearMapOverlays();
chatPresentation.clearMapOverlays();
previewPresentation.clearMapOverlays();
handleClearSelection();
dispatchWorkspaceContext({ type: "clear-spatial-feature" });
measurement.clear();
@@ -1215,7 +1172,7 @@ function MapWorkbenchContent({
}
return items;
}, [artifactWorkspace.activeArtifact, selectedOperationalEvent?.lane, workspaceContext]);
const collaborationReviewTask = agent.sessionPurpose !== "ordinary_chat" && agent.sessionTaskId
const collaborationTask = agent.sessionPurpose !== "ordinary_chat" && agent.sessionTaskId
? operationalTasks.tasks.find((item) => item.task_id === agent.sessionTaskId)
: undefined;
const promptSuggestions = useAgentPromptSuggestions({
@@ -1225,6 +1182,7 @@ function MapWorkbenchContent({
model: agent.selectedModel,
sessionId: agent.sessionId,
request: agent.generatePromptSuggestions,
recoveryVersion: agent.connectionRecoveryVersion,
alerts: workbenchAlerts,
alertDataSource,
conditions: scheduledConditions,
@@ -1263,7 +1221,9 @@ function MapWorkbenchContent({
promptSuggestions: promptSuggestions.suggestions,
promptSuggestionsLoading: promptSuggestions.loading,
promptSuggestionsUnavailable: promptSuggestions.unavailable,
onRetryPromptSuggestions: promptSuggestions.retry,
activeSessionId: agent.sessionId,
answerStateScope: agent.answerStateScope,
qaOnly: agent.sessionPurpose === "task_collaboration" || agent.sessionPurpose === "task_execution",
executionHistory: agent.sessionPurpose === "task_execution",
statusLabel: agent.statusLabel,
@@ -1289,14 +1249,6 @@ function MapWorkbenchContent({
currentProjectName: getWorkbenchProject(getAccessToken)?.name,
approvalMode: agent.approvalMode,
uiResults: activeAgentUiResults,
reviewPanel: collaborationReviewTask ? <TaskConversationReview task={collaborationReviewTask}
onAccessDecision={(requestId, version, decision, scopes) => operationalTasks.decideAccess(collaborationReviewTask.task_id, requestId, version, decision, scopes)}
onActionDecision={(actionId, version, decision, comment) => operationalTasks.decideAction(collaborationReviewTask.task_id, actionId, version, decision, comment)}
onActionExecute={(actionId, version) => operationalTasks.executeAction(collaborationReviewTask.task_id, actionId, version)}
onDraftUpdate={(actionId, version, payload) => operationalTasks.updateWorkOrderDraft(collaborationReviewTask.task_id, actionId, version, payload)}
onConfirmDispatch={(actionId, version) => operationalTasks.confirmWorkOrderDispatch(collaborationReviewTask.task_id, actionId, version)}
onActionReconcile={(actionId, version) => operationalTasks.reconcileAction(collaborationReviewTask.task_id, actionId, version)}
onRefresh={operationalTasks.refresh} /> : undefined,
workspaceContext: agentWorkspaceContext,
prompt: agent.promptDraft,
contentReference: agent.contentReference,
@@ -1304,16 +1256,20 @@ function MapWorkbenchContent({
agent.setContentReference(value);
if (value) readingWorkspace?.close();
},
promptReference: collaborationReviewTask ? {
title: collaborationReviewTask.title,
placeholder: "可询问调查进展、讨论报告或补充信息",
detail: [
agent.taskDiscussionContext?.report_revision && agent.taskDiscussionReportGeneration
? `第 ${agent.taskDiscussionReportGeneration} 轮` : null,
agent.taskDiscussionContext?.report_revision ? `报告第 ${agent.taskDiscussionContext.report_revision} 版` : null,
agent.taskDiscussionContext?.evidence_id ? "已引用证据" : null,
agent.taskDiscussionContext?.gap_id ? "已引用数据缺口" : null
].filter(Boolean).join(" · ")
promptReference: collaborationTask ? {
title: collaborationTask.title,
placeholder: agent.sessionPurpose === "task_execution"
? "追加调试问题,发送后进入独立调试会话"
: "可询问调查进展、讨论报告或补充信息",
detail: agent.sessionPurpose === "task_execution"
? "发送后进入调试分支 · 原始记录保持不变"
: [
agent.taskDiscussionContext?.report_revision && agent.taskDiscussionReportGeneration
? `第 ${agent.taskDiscussionReportGeneration} 轮` : null,
agent.taskDiscussionContext?.report_revision ? `报告第 ${agent.taskDiscussionContext.report_revision} 版` : null,
agent.taskDiscussionContext?.evidence_id ? "已引用证据" : null,
agent.taskDiscussionContext?.gap_id ? "已引用数据缺口" : null
].filter(Boolean).join(" · ")
} : undefined,
onRefreshHistory: agent.refreshSessionHistory,
onStartNewSession: handleStartNewAgentSession,
@@ -1332,12 +1288,12 @@ function MapWorkbenchContent({
onSubmitPrompt: agent.submitPrompt,
onLocateAnswerFeatures: async (target) => {
if (readingWorkspace?.active) readingWorkspace.setView(businessWorkspaceWidth >= 960 ? "map_split" : "map_only");
return mapController.locateFeatures(target.layer, target.feature_ids, new AbortController().signal);
return chatPresentation.executeCommand({ action: "locate_features", layer: target.layer, feature_ids: target.feature_ids }, new AbortController().signal, "chat");
},
onStopPrompt: agent.stopPrompt,
onReplyPermission: agent.replyPermission,
onReplyQuestion: agent.replyQuestion,
onRejectQuestion: agent.rejectQuestion
onStopPrompt: agent.sessionPurpose === "task_execution" ? undefined : agent.stopPrompt,
onReplyPermission: agent.sessionPurpose === "task_execution" ? undefined : agent.replyPermission,
onReplyQuestion: agent.sessionPurpose === "task_execution" ? undefined : agent.replyQuestion,
onRejectQuestion: agent.sessionPurpose === "task_execution" ? undefined : agent.rejectQuestion
};
return (
@@ -1397,7 +1353,7 @@ function MapWorkbenchContent({
onLogout={onLogout}
/>
{analysisResultsPanelOpen || peripheralSamplePanelOpen ? (
{analysisResultsPanelOpen ? (
<div
aria-hidden="true"
data-map-interaction-shield
@@ -1501,13 +1457,21 @@ function MapWorkbenchContent({
}
mapOverlay={
<div className="pointer-events-none absolute inset-x-0 bottom-0 top-14">
{investigationFollow && selectedInvestigationTask && !devPanelOpen ? <InvestigationPresentationPanel
task={selectedInvestigationTask} charts={presentation.charts} overlay={presentation.overlay} cameraPaused={presentation.cameraPaused}
status={presentation.status} error={presentation.error}
width={Math.min(380, Math.max(180, businessWorkspaceWidth - (rightPanelOpen ? (rightPanelExpanded ? (viewportWidth >= 1536 ? 960 : 880) : 432) + 64 : 64) - 24))}
relatedLayers={presentationRelatedLayers} onToggleLayer={handleTogglePresentationLayer}
onResumeCamera={presentation.resumeCamera} onCloseChart={presentation.closeChart} onToggleOverlay={presentation.toggleOverlay} onFitOverlay={presentation.fitOverlay}
/> : null}
<div className="pointer-events-none absolute bottom-[calc(var(--workbench-timeline-height)+1.25rem)] left-3 z-40 flex max-h-[calc(100dvh-9rem-var(--workbench-timeline-height))] max-w-[calc(100%-24px)] flex-col gap-2 overflow-y-auto">
<AnimatePresence initial={false}>
{[
{ scope: "investigation", surface: presentation, task: selectedInvestigationTask, visible: Boolean(selectedInvestigationTask) || presentation.hasHistory },
{ scope: "chat", surface: chatPresentation, task: null, visible: chatPresentation.hasHistory || chatPresentation.charts.length > 0 || Boolean(chatPresentation.overlay) || chatPresentation.highlightLayers.length > 0 },
{ scope: "preview", surface: previewPresentation, task: null, visible: previewPresentation.hasHistory || previewPresentation.localPreview }
].filter((item) => item.visible).map(({ scope, surface, task }) => <InvestigationPresentationPanel
key={scope} scope={scope} embedded blocks={surface.blocks} records={surface.records} evidence={surface.evidence} history={surface.history} onLocateTargets={surface.locateTargets} task={task} localPreview={scope === "preview"} charts={surface.charts} overlay={surface.overlay} cameraPaused={surface.cameraPaused}
status={scope === "chat" ? "live" : surface.status} error={surface.error}
width={Math.min(380, Math.max(180, businessWorkspaceWidth - (rightPanelOpen ? (rightPanelExpanded ? (viewportWidth >= 1536 ? 960 : 880) : 432) + 64 : 64) - 24))}
relatedLayers={scope === "investigation" ? presentationRelatedLayers : []} onToggleLayer={handleTogglePresentationLayer}
onResumeCamera={surface.resumeCamera} onCloseChart={surface.closeChart} onToggleOverlay={surface.toggleOverlay} onFitOverlay={surface.fitOverlay}
/>)}
</AnimatePresence>
</div>
{!devPanelOpen ? (
<>
<div className="pointer-events-auto absolute right-3 top-10 hidden lg:block">
@@ -1577,6 +1541,7 @@ function MapWorkbenchContent({
onLoadTaskEvidenceSnapshot={operationalTasks.loadEvidenceSnapshot}
onTaskRefresh={operationalTasks.refresh}
onLoadTaskReports={operationalTasks.loadReports}
onAnswerTaskQuestion={operationalTasks.answerQuestion}
onReportViewed={operationalTasks.markReportRead}
onRefresh={workbenchSync.refresh}
/>
@@ -1591,6 +1556,12 @@ function MapWorkbenchContent({
flowDataSource={flowDataSource}
investigationFollow={investigationFollow}
onInvestigationFollowChange={setInvestigationFollow}
investigationTools={{
task: selectedInvestigationTask, mapReady,
charts: previewPresentation.charts.length, overlay: previewPresentation.overlay?.title ?? null,
activity: [...presentation.activity, ...previewPresentation.activity], onSample: previewPresentation.createSample,
onPreview: previewPresentation.runPreview, onClear: previewPresentation.clearPreview
}}
onClose={() => setDevPanelOpen(false)}
/>
) : null}
@@ -1735,6 +1706,7 @@ function MapWorkbenchContent({
onLoadTaskEvidenceSnapshot={operationalTasks.loadEvidenceSnapshot}
onTaskRefresh={operationalTasks.refresh}
onLoadTaskReports={operationalTasks.loadReports}
onAnswerTaskQuestion={operationalTasks.answerQuestion}
onReportViewed={operationalTasks.markReportRead}
onRefresh={workbenchSync.refresh}
/>