feat(workbench): integrate runtime data and map tooling

This commit is contained in:
2026-09-22 10:45:46 +08:00
parent bf44cc0f25
commit bebcf2a06c
163 changed files with 15642 additions and 1312 deletions
@@ -146,7 +146,11 @@ export const runningWorkflowDefinitions: Record<ScheduledConditionTaskId, Runnin
};
export function getRunningWorkflowDefinition(condition: ScheduledConditionRecord) {
return runningWorkflowDefinitions[condition.taskId];
const workflow = runningWorkflowDefinitions[condition.taskId as ScheduledConditionTaskId];
if (!workflow) {
throw new Error("Backend conditions do not use inferred workflow definitions");
}
return workflow;
}
export function getRunningWorkflowState(
@@ -34,14 +34,18 @@ describe("createOperationalScheduledConditions", () => {
const executingWorkOrder = conditions.find(
(condition) =>
condition.kind === "work_order" &&
condition.stages.some(
(stage) => stage.id === "execution" && stage.status === "current"
condition.subtasks.some((subtask) =>
subtask.stages.some(
(stage) => stage.id === "execution" && stage.status === "current"
)
)
);
expect(
executingWorkOrder?.kind === "work_order"
? executingWorkOrder.stages.find((stage) => stage.id === "execution")?.title
? executingWorkOrder.subtasks
.flatMap((subtask) => subtask.stages)
.find((stage) => stage.id === "execution" && stage.status === "current")?.title
: undefined
).toBe("执行中");
});
@@ -51,13 +55,17 @@ describe("createOperationalScheduledConditions", () => {
const awaitingReplyWorkOrder = conditions.find(
(condition) =>
condition.kind === "work_order" &&
condition.stages.some((stage) => stage.id === "reply" && stage.status === "current")
condition.subtasks.some((subtask) =>
subtask.stages.some((stage) => stage.id === "reply" && stage.status === "current")
)
);
expect(awaitingReplyWorkOrder?.status).toBe("awaiting_reply");
expect(
awaitingReplyWorkOrder?.kind === "work_order"
? awaitingReplyWorkOrder.stages.find((stage) => stage.id === "reply")?.title
? awaitingReplyWorkOrder.subtasks
.flatMap((subtask) => subtask.stages)
.find((stage) => stage.id === "reply" && stage.status === "current")?.title
: undefined
).toBe("待复令");
});
@@ -6,6 +6,7 @@ import type {
ScheduledConditionRiskLevel,
ScheduledConditionStatus,
ScheduledConditionTaskId,
ScheduledWorkOrderSubtask,
ScheduledWorkOrderStatus
} from "../types";
@@ -13,7 +14,7 @@ export const SCHEDULED_CONDITION_REFRESH_INTERVAL_MS = 5_000;
const HISTORY_WINDOW_MINUTES = 6 * 60;
const GENERATED_SESSION_ID_PREFIX = "scheduled-condition-";
const MODEL_NAME = "DrainFlow Realtime Simulator";
const MODEL_NAME = "SupplyFlow 前端演示模拟器";
const GUARANTEED_RUNNING_TASK_ID: ConditionTaskId = "scada-diagnosis";
type ConditionTaskId = ScheduledConditionTaskId;
@@ -103,9 +104,9 @@ const CONDITION_TASKS: ConditionTaskDefinition[] = [
const MOCK_WORK_ORDER_TEMPLATES: MockWorkOrderTemplate[] = [
{
id: "mock-incident-valve-dispatch",
title: "系统工单:华山路阀门边界确认",
title: "演示工单:华山路阀门边界确认",
summary: "异常事件分析后生成阀门边界确认任务,派发前需调度员复核影响范围。",
detail: "该工单由异常事件分析流程生成,用于演示人工确认后提交系统工单并通知管网要素操作。",
detail: "该条目是前端演示回退,用于展示人工确认界面,不代表真实提交或岗位通知。",
recommendation: "确认影响用户、上下游压力和应急隔离边界后,再通知执行岗位到场。",
source: "异常事件分析",
location: "华山路沿线阀门组 VG-11",
@@ -121,7 +122,7 @@ const MOCK_WORK_ORDER_TEMPLATES: MockWorkOrderTemplate[] = [
},
{
id: "mock-valve-operation-execution",
title: "系统工单:北辰分区阀门开度调整",
title: "演示工单:北辰分区阀门开度调整",
summary: "根据管网智能调度方案,现场调整北辰分区边界阀门开度并复核压力。",
detail: "该工单模拟系统派发后的执行阶段,班组正在按方案对阀门要素进行操作。",
recommendation: "执行过程中保持调度语音联络,调整后立即复核末端压力和流量变化。",
@@ -139,7 +140,7 @@ const MOCK_WORK_ORDER_TEMPLATES: MockWorkOrderTemplate[] = [
},
{
id: "mock-pump-boundary-reply",
title: "系统工单:泵站边界复核复令",
title: "演示工单:泵站边界复核复令",
summary: "泵站出水边界复核已完成,等待班组复令和调度确认闭环。",
detail: "该工单模拟执行完成后的复令阶段,用于检查处置结果、照片和指标回传。",
recommendation: "核对泵站出水压力、频率曲线和分区边界模拟结果后完成复令。",
@@ -160,9 +161,9 @@ const MOCK_WORK_ORDER_TEMPLATES: MockWorkOrderTemplate[] = [
const MANUAL_WORK_ORDERS: ManualWorkOrderDefinition[] = [
{
id: "huashan-valve-well-service",
title: "人工工单:华山路阀井现场处置",
title: "演示人工工单:华山路阀井现场处置",
summary: "现场班组复核华山路阀井积水与井盖松动问题,完成照片和处置记录回传。",
detail: "该工单由调度员人工创建,来源为现场巡检反馈,不属于周期工况任务。",
detail: "该条目模拟调度员根据现场巡检反馈创建人工工单,不代表真实提交或派发。",
recommendation: "到点后确认现场安全措施、处置照片和回填记录,必要时关联资产缺陷单。",
source: "现场巡检反馈",
location: "华山路阀井",
@@ -178,9 +179,9 @@ const MANUAL_WORK_ORDERS: ManualWorkOrderDefinition[] = [
},
{
id: "renmin-road-meter-change",
title: "人工工单:人民路流量计更换",
title: "演示人工工单:人民路流量计更换",
summary: "更换人民路 DN300 支线流量计通信模块,并复测回传质量。",
detail: "该工单由计量维护计划人工创建,执行前需确认旁通和作业窗口。",
detail: "该条目模拟计量维护计划中的人工工单,执行前需确认旁通和作业窗口。",
recommendation: "作业完成后核对 15 分钟回传稳定性,并记录新模块编号。",
source: "计量维护计划",
location: "人民路 DN300 支线",
@@ -196,9 +197,9 @@ const MANUAL_WORK_ORDERS: ManualWorkOrderDefinition[] = [
},
{
id: "beichen-fire-hydrant-repair",
title: "人工工单:北辰消防栓维修",
title: "演示人工工单:北辰消防栓维修",
summary: "处理北辰片区消防栓渗漏缺陷,现场确认关停影响和恢复时间。",
detail: "该工单来自客服缺陷转派,由调度员人工排入当日维修窗口。",
detail: "该条目模拟客服缺陷转派,由调度员排入当日维修窗口,不代表真实派发。",
recommendation: "维修前通知影响点位,完成后复核周边压力和栓体密封状态。",
source: "客服缺陷转派",
location: "北辰片区消防栓",
@@ -333,7 +334,6 @@ function createUpcomingWorkOrders(now: Date, currentMinutesOfDay: number): Sched
)
.map((workOrder) => {
const scheduledAtDate = setMinutesOfDay(now, workOrder.startMinute);
const replyDeadlineMinute = workOrder.startMinute + workOrder.durationMinutes + workOrder.replyWindowMinutes;
const phase = getWorkOrderPhase(workOrder, currentMinutesOfDay);
return {
@@ -350,16 +350,9 @@ function createUpcomingWorkOrders(now: Date, currentMinutesOfDay: number): Sched
recommendation: workOrder.recommendation,
durationMinutes: workOrder.durationMinutes,
source: workOrder.source,
location: workOrder.location,
dispatcher: workOrder.dispatcher,
assignee: workOrder.assignee,
priority: workOrder.priority,
replyWindowMinutes: workOrder.replyWindowMinutes,
stages: createWorkOrderStages(workOrder, now, scheduledAtDate, phase),
replyRequirements:
currentMinutesOfDay > replyDeadlineMinute
? []
: workOrder.replyRequirements
subtasks: createWorkOrderSubtasks(workOrder, now, scheduledAtDate, phase)
};
})
.sort((a, b) => Date.parse(a.scheduledAt) - Date.parse(b.scheduledAt));
@@ -415,44 +408,112 @@ function getWorkOrderStatus(phase: WorkOrderPhase): ScheduledWorkOrderStatus {
return "awaiting_reply";
}
function createWorkOrderStages(
function createWorkOrderSubtasks(
workOrder: ManualWorkOrderDefinition,
now: Date,
scheduledAtDate: Date,
phase: WorkOrderPhase
): ScheduledWorkOrderSubtask[] {
const taskTitles = workOrder.replyRequirements.length > 0
? workOrder.replyRequirements
: [workOrder.title];
const activeIndex = phase === "dispatch"
? 0
: phase === "execution"
? Math.min(1, taskTitles.length - 1)
: taskTitles.length - 1;
const subtaskDuration = Math.max(5, Math.ceil(workOrder.durationMinutes / taskTitles.length));
return taskTitles.map((title, index) => {
const position = index < activeIndex ? "before" : index > activeIndex ? "after" : "active";
const status = position === "before"
? "completed"
: position === "after" || phase === "dispatch"
? "pending"
: "running";
const subtaskScheduledAt = new Date(scheduledAtDate.getTime() + index * subtaskDuration * 60_000);
return {
id: `${workOrder.id}-subtask-${index + 1}`,
title,
summary: `完成“${title}”并保留可复核的阶段记录。`,
status,
scheduledAt: formatLocalIsoWithOffset(subtaskScheduledAt),
location: workOrder.location,
assignee: workOrder.assignee,
durationMinutes: subtaskDuration,
replyWindowMinutes: workOrder.replyWindowMinutes,
stages: createWorkOrderSubtaskStages(
workOrder,
subtaskScheduledAt,
subtaskDuration,
phase,
position
),
replyRequirements: [`回传“${title}”的执行结果`]
};
});
}
function createWorkOrderSubtaskStages(
workOrder: ManualWorkOrderDefinition,
subtaskScheduledAt: Date,
subtaskDurationMinutes: number,
phase: WorkOrderPhase,
position: "before" | "active" | "after"
) {
const dispatchAt = setMinutesOfDay(now, Math.max(0, workOrder.startMinute - workOrder.dispatchLeadMinutes));
const executionEnd = setMinutesOfDay(now, workOrder.startMinute + workOrder.durationMinutes);
const replyEnd = setMinutesOfDay(
now,
workOrder.startMinute + workOrder.durationMinutes + workOrder.replyWindowMinutes
const dispatchAt = new Date(
subtaskScheduledAt.getTime() - workOrder.dispatchLeadMinutes * 60_000
);
const executionEnd = new Date(
subtaskScheduledAt.getTime() + subtaskDurationMinutes * 60_000
);
const replyEnd = new Date(executionEnd.getTime() + workOrder.replyWindowMinutes * 60_000);
const allDone = position === "before";
const allPending = position === "after";
return [
{
id: "dispatch" as const,
id: "dispatch",
sequence: 1,
title: "派发",
status: phase === "dispatch" ? ("current" as const) : ("done" as const),
status: allDone
? ("done" as const)
: allPending
? ("pending" as const)
: phase === "dispatch"
? ("current" as const)
: ("done" as const),
owner: workOrder.dispatcher,
timeLabel: formatClock(dispatchAt),
description: `确认${workOrder.location}任务范围、优先级和到场窗口,派发给${workOrder.assignee}。`
},
{
id: "execution" as const,
id: "execution",
sequence: 2,
title: "执行中",
status:
phase === "dispatch"
allDone
? ("done" as const)
: allPending || phase === "dispatch"
? ("pending" as const)
: phase === "execution"
? ("current" as const)
: ("done" as const),
owner: workOrder.assignee,
timeLabel: formatClockRange(scheduledAtDate, workOrder.durationMinutes),
timeLabel: formatClockRange(subtaskScheduledAt, subtaskDurationMinutes),
description: workOrder.summary
},
{
id: "reply" as const,
id: "reply",
sequence: 3,
title: "待复令",
status: phase === "reply" ? ("current" as const) : ("pending" as const),
status: allDone
? ("done" as const)
: allPending
? ("pending" as const)
: phase === "reply"
? ("current" as const)
: ("pending" as const),
owner: `${workOrder.assignee} → 调度中心`,
timeLabel: formatClockRange(executionEnd, workOrder.replyWindowMinutes),
description: `提交处置结果、现场照片和影响复核,最迟 ${formatClock(replyEnd)} 前完成复令。`
@@ -795,7 +856,7 @@ function createDispatchInstructions(signal: TaskSignal): DispatchInstruction[] {
},
{
action: "末端压力观察",
target: "华山路末端压力点 P-117",
target: "华山路末端压力点 DEMO-P001",
command: "将该点加入 15 分钟高频复核队列",
verification: "若压力低于 0.18MPa 持续两轮,转人工调度确认"
}
@@ -3,25 +3,25 @@ import type { MapAnnotation } from "../types";
export const mapAnnotations: MapAnnotation[] = [
{
id: "burst",
label: "爆管位置",
label: "演示爆管位置",
coordinate: [121.5215, 30.9084],
kind: "burst"
},
{
id: "dn600",
label: "DN600",
label: "演示 DN600",
coordinate: [121.512, 30.9102],
kind: "pipe-label"
},
{
id: "east-gate",
label: "东直门小区",
label: "演示片区",
coordinate: [121.545, 30.951],
kind: "district"
},
{
id: "impact",
label: "影响范围 约 1.82 km²",
label: "演示影响范围 约 1.82 km²",
coordinate: [121.567, 30.896],
kind: "tooltip"
}
@@ -34,7 +34,7 @@ export const impactAreaPolygon = {
type: "Feature" as const,
properties: {
id: "impact-area",
label: "影响范围",
label: "演示影响范围",
area: "1.82 km²"
},
geometry: {