import type { ScheduledConditionAnalysisInsight, ScheduledConditionItem, ScheduledConditionKpi, ScheduledConditionReport, ScheduledConditionRiskLevel, ScheduledConditionStatus, ScheduledConditionTaskId } from "../types"; 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 GUARANTEED_RUNNING_TASK_ID: ConditionTaskId = "scada-diagnosis"; type ConditionTaskId = ScheduledConditionTaskId; type ConditionTaskDefinition = { id: ConditionTaskId; title: string; summary: string; intervalMinutes: number; durationMinutes: number; activeWindow?: (minutesOfDay: number) => boolean; }; type TaskSignal = { completeness: number; deviation: number; target: string; sampleCount: number; taskId: ConditionTaskId; }; type ManualWorkOrderDefinition = { id: string; title: string; summary: string; detail: string; recommendation: string; source: string; location: string; dispatcher: string; assignee: string; priority: "normal" | "urgent"; startMinute: number; durationMinutes: number; dispatchLeadMinutes: number; replyWindowMinutes: number; riskLevel: ScheduledConditionRiskLevel; replyRequirements: string[]; }; type MockWorkOrderTemplate = Omit & { stage: "dispatch" | "execution" | "reply"; durationMinutes: number; dispatchLeadMinutes: number; replyWindowMinutes: number; }; type DispatchInstruction = { action: string; target: string; command: string; verification: string; }; const CONDITION_TASKS: ConditionTaskDefinition[] = [ { id: "scada-diagnosis", title: "SCADA 数据诊断", summary: "对压力、流量 SCADA 数据进行新鲜度、完整率、突变和一致性诊断,覆盖分区压力巡检范围。", intervalMinutes: 5, durationMinutes: 3 }, { id: "smart-dispatch", title: "管网智能调度", summary: "结合实时工况生成管网要素调整指令,输出泵站、阀门或分区边界的建议动作。", intervalMinutes: 15, durationMinutes: 4 }, { id: "network-simulation", title: "管网定时模拟", summary: "基于最新遥测、泵站启停和阀门开度滚动复核全网水力状态。", intervalMinutes: 15, durationMinutes: 6 }, { id: "pump-energy", title: "泵站能耗检查", summary: "检查泵组负载、单位排水电耗、启停频率和供压稳定性。", intervalMinutes: 60, durationMinutes: 10 } ]; const MOCK_WORK_ORDER_TEMPLATES: MockWorkOrderTemplate[] = [ { id: "mock-incident-valve-dispatch", title: "系统工单:华山路阀门边界确认", summary: "异常事件分析后生成阀门边界确认任务,派发前需调度员复核影响范围。", detail: "该工单由异常事件分析流程生成,用于演示人工确认后提交系统工单并通知管网要素操作。", recommendation: "确认影响用户、上下游压力和应急隔离边界后,再通知执行岗位到场。", source: "异常事件分析", location: "华山路沿线阀门组 VG-11", dispatcher: "调度中心值班长", assignee: "调度执行岗", priority: "urgent", durationMinutes: 40, dispatchLeadMinutes: 30, replyWindowMinutes: 20, riskLevel: "attention", stage: "dispatch", replyRequirements: ["确认阀门当前开度", "记录影响分区和用户范围", "回传派发确认截图"] }, { id: "mock-valve-operation-execution", title: "系统工单:北辰分区阀门开度调整", summary: "根据管网智能调度方案,现场调整北辰分区边界阀门开度并复核压力。", detail: "该工单模拟系统派发后的执行阶段,班组正在按方案对阀门要素进行操作。", recommendation: "执行过程中保持调度语音联络,调整后立即复核末端压力和流量变化。", source: "调度方案确认", location: "北辰分区边界阀门 BV-07", dispatcher: "管网调度岗", assignee: "北辰抢修二组", priority: "urgent", durationMinutes: 55, dispatchLeadMinutes: 25, replyWindowMinutes: 20, riskLevel: "attention", stage: "execution", replyRequirements: ["上传阀门操作前后照片", "记录最终开度", "复核北辰末端压力不低于 0.22MPa"] }, { id: "mock-pump-boundary-reply", title: "系统工单:泵站边界复核复令", summary: "泵站出水边界复核已完成,等待班组复令和调度确认闭环。", detail: "该工单模拟执行完成后的复令阶段,用于检查处置结果、照片和指标回传。", recommendation: "核对泵站出水压力、频率曲线和分区边界模拟结果后完成复令。", source: "模型复核建议", location: "北辰二级泵站 P-02", dispatcher: "模型复核岗", assignee: "泵站运行班组", priority: "normal", durationMinutes: 45, dispatchLeadMinutes: 20, replyWindowMinutes: 35, riskLevel: "normal", stage: "reply", replyRequirements: ["上传泵站运行截图", "填写出水压力复核结果", "确认模型边界已同步更新"] } ]; const MANUAL_WORK_ORDERS: ManualWorkOrderDefinition[] = [ { id: "huashan-valve-well-service", title: "人工工单:华山路阀井现场处置", summary: "现场班组复核华山路阀井积水与井盖松动问题,完成照片和处置记录回传。", detail: "该工单由调度员人工创建,来源为现场巡检反馈,不属于周期工况任务。", recommendation: "到点后确认现场安全措施、处置照片和回填记录,必要时关联资产缺陷单。", source: "现场巡检反馈", location: "华山路阀井", dispatcher: "调度中心值班长", assignee: "北片区抢修一组", priority: "urgent", startMinute: 9 * 60 + 40, durationMinutes: 35, dispatchLeadMinutes: 20, replyWindowMinutes: 15, riskLevel: "attention", replyRequirements: ["上传处置前后照片", "记录阀井积水处理结果", "复核井盖密封和周边安全状态"] }, { id: "renmin-road-meter-change", title: "人工工单:人民路流量计更换", summary: "更换人民路 DN300 支线流量计通信模块,并复测回传质量。", detail: "该工单由计量维护计划人工创建,执行前需确认旁通和作业窗口。", recommendation: "作业完成后核对 15 分钟回传稳定性,并记录新模块编号。", source: "计量维护计划", location: "人民路 DN300 支线", dispatcher: "计量调度岗", assignee: "计量维护二组", priority: "normal", startMinute: 10 * 60 + 30, durationMinutes: 50, dispatchLeadMinutes: 30, replyWindowMinutes: 20, riskLevel: "normal", replyRequirements: ["登记新通信模块编号", "回传 15 分钟稳定性截图", "确认旁通恢复和计量数据连续"] }, { id: "beichen-fire-hydrant-repair", title: "人工工单:北辰消防栓维修", summary: "处理北辰片区消防栓渗漏缺陷,现场确认关停影响和恢复时间。", detail: "该工单来自客服缺陷转派,由调度员人工排入当日维修窗口。", recommendation: "维修前通知影响点位,完成后复核周边压力和栓体密封状态。", source: "客服缺陷转派", location: "北辰片区消防栓", dispatcher: "客服联动调度", assignee: "北辰维修班组", priority: "urgent", startMinute: 14 * 60, durationMinutes: 60, dispatchLeadMinutes: 45, replyWindowMinutes: 20, riskLevel: "attention", replyRequirements: ["回填关停影响范围", "上传维修完成照片", "复测周边压力和消防栓密封状态"] } ]; export function createOperationalScheduledConditions(now = new Date()): ScheduledConditionItem[] { const currentMinutesOfDay = getMinutesOfDay(now); const historyStart = Math.max(0, currentMinutesOfDay - HISTORY_WINDOW_MINUTES); const historicalConditions = CONDITION_TASKS.flatMap((task) => createHistoricalConditionsForTask(task, now, historyStart, currentMinutesOfDay) ); const runningCondition = createGuaranteedRunningCondition(now, currentMinutesOfDay); const operationalConditions = historicalConditions.some((condition) => condition.status === "running") || !runningCondition ? historicalConditions : [runningCondition, ...historicalConditions.filter((condition) => condition.id !== runningCondition.id)]; const futureWorkOrders = createUpcomingWorkOrders(now, currentMinutesOfDay); return [...operationalConditions, ...futureWorkOrders].sort( (a, b) => Date.parse(b.scheduledAt) - Date.parse(a.scheduledAt) ); } export function isGeneratedScheduledConditionSessionId(sessionId: string) { return sessionId.startsWith(GENERATED_SESSION_ID_PREFIX); } function createHistoricalConditionsForTask( task: ConditionTaskDefinition, now: Date, historyStart: number, currentMinutesOfDay: number ) { const firstMinute = ceilToInterval(historyStart, task.intervalMinutes); const lastMinute = floorToInterval(currentMinutesOfDay, task.intervalMinutes); const conditions: ScheduledConditionItem[] = []; for (let minute = firstMinute; minute <= lastMinute; minute += task.intervalMinutes) { if (task.activeWindow && !task.activeWindow(minute)) { continue; } const scheduledAtDate = setMinutesOfDay(now, minute); const scheduledAt = formatLocalIsoWithOffset(scheduledAtDate); const signal = createTaskSignal(task, scheduledAtDate); const status = getConditionStatus(task, scheduledAtDate, now, signal); const riskLevel = getRiskLevel(status, signal); const kpis = createTaskKpis(task, signal); const durationMinutes = getTaskExecutionDuration(task); conditions.push({ id: `${GENERATED_SESSION_ID_PREFIX}${task.id}-${formatCompactLocalTime(scheduledAtDate)}`, kind: "condition", taskId: task.id, scheduledAt, title: task.title, summary: task.summary, status, riskLevel, sessionId: `${GENERATED_SESSION_ID_PREFIX}${task.id}-${formatCompactLocalTime(scheduledAtDate)}`, updatedAt: Math.min(now.getTime(), scheduledAtDate.getTime() + durationMinutes * 60_000), detail: createDetail(task, scheduledAtDate, status, signal), evidence: createEvidence(task, signal, status), recommendation: createRecommendation(status, riskLevel, task, signal), analysisInsight: createAnalysisInsight(task, status, riskLevel, signal), kpis, report: createConditionReport(task, status, riskLevel, signal, kpis), modelName: MODEL_NAME, durationMinutes }); } return conditions; } function createGuaranteedRunningCondition(now: Date, currentMinutesOfDay: number): ScheduledConditionItem | null { const task = CONDITION_TASKS.find((item) => item.id === GUARANTEED_RUNNING_TASK_ID); if (!task) { return null; } const scheduledMinute = floorToInterval(currentMinutesOfDay, task.intervalMinutes); if (task.activeWindow && !task.activeWindow(scheduledMinute)) { return null; } const scheduledAtDate = setMinutesOfDay(now, scheduledMinute); const scheduledAt = formatLocalIsoWithOffset(scheduledAtDate); const signal = createTaskSignal(task, scheduledAtDate); const status: ScheduledConditionStatus = "running"; const riskLevel = getRiskLevel(status, signal); const kpis = createTaskKpis(task, signal); const durationMinutes = getTaskExecutionDuration(task); return { id: `${GENERATED_SESSION_ID_PREFIX}${task.id}-${formatCompactLocalTime(scheduledAtDate)}`, kind: "condition", taskId: task.id, scheduledAt, title: task.title, summary: task.summary, status, riskLevel, sessionId: `${GENERATED_SESSION_ID_PREFIX}${task.id}-${formatCompactLocalTime(scheduledAtDate)}`, updatedAt: now.getTime(), detail: createDetail(task, scheduledAtDate, status, signal), evidence: createEvidence(task, signal, status), recommendation: createRecommendation(status, riskLevel, task, signal), analysisInsight: createAnalysisInsight(task, status, riskLevel, signal), kpis, report: createConditionReport(task, status, riskLevel, signal, kpis), modelName: MODEL_NAME, durationMinutes }; } function createUpcomingWorkOrders(now: Date, currentMinutesOfDay: number): ScheduledConditionItem[] { const workOrders = [...MANUAL_WORK_ORDERS, ...createMockWorkOrders(currentMinutesOfDay)]; return workOrders.filter( (workOrder) => currentMinutesOfDay <= workOrder.startMinute + workOrder.durationMinutes + workOrder.replyWindowMinutes ) .map((workOrder) => { const scheduledAtDate = setMinutesOfDay(now, workOrder.startMinute); const replyDeadlineMinute = workOrder.startMinute + workOrder.durationMinutes + workOrder.replyWindowMinutes; return { id: `manual-work-order-${workOrder.id}-${formatCompactLocalTime(scheduledAtDate)}`, kind: "work_order" as const, code: `WO-${formatCompactLocalTime(scheduledAtDate)}-${workOrder.id.toUpperCase().slice(0, 4)}`, scheduledAt: formatLocalIsoWithOffset(scheduledAtDate), title: workOrder.title, summary: workOrder.summary, status: currentMinutesOfDay < workOrder.startMinute ? ("pending" as const) : ("running" as const), riskLevel: workOrder.riskLevel, updatedAt: now.getTime(), detail: workOrder.detail, 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, currentMinutesOfDay), replyRequirements: currentMinutesOfDay > replyDeadlineMinute ? [] : workOrder.replyRequirements }; }) .sort((a, b) => Date.parse(a.scheduledAt) - Date.parse(b.scheduledAt)); } function createMockWorkOrders(currentMinutesOfDay: number): ManualWorkOrderDefinition[] { return MOCK_WORK_ORDER_TEMPLATES.map((template) => { const startMinute = getMockWorkOrderStartMinute(template, currentMinutesOfDay); return { ...template, startMinute }; }); } function getMockWorkOrderStartMinute(template: MockWorkOrderTemplate, currentMinutesOfDay: number) { if (template.stage === "dispatch") { return Math.min(currentMinutesOfDay + 30, 24 * 60 - 1); } if (template.stage === "execution") { return Math.max(0, currentMinutesOfDay - Math.min(20, template.durationMinutes - 5)); } return currentMinutesOfDay - template.durationMinutes - Math.min(10, template.replyWindowMinutes - 5); } function createWorkOrderStages( workOrder: ManualWorkOrderDefinition, now: Date, scheduledAtDate: Date, currentMinutesOfDay: number ) { 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 executionEndMinute = workOrder.startMinute + workOrder.durationMinutes; return [ { id: "dispatch" as const, title: "派发", status: currentMinutesOfDay >= workOrder.startMinute ? ("done" as const) : ("current" as const), owner: workOrder.dispatcher, timeLabel: formatClock(dispatchAt), description: `确认${workOrder.location}任务范围、优先级和到场窗口,派发给${workOrder.assignee}。` }, { id: "execution" as const, title: "执行", status: currentMinutesOfDay >= executionEndMinute ? ("done" as const) : currentMinutesOfDay >= workOrder.startMinute ? ("current" as const) : ("pending" as const), owner: workOrder.assignee, timeLabel: formatClockRange(scheduledAtDate, workOrder.durationMinutes), description: workOrder.summary }, { id: "reply" as const, title: "复令", status: currentMinutesOfDay >= executionEndMinute ? ("current" as const) : ("pending" as const), owner: `${workOrder.assignee} → 调度中心`, timeLabel: formatClockRange(executionEnd, workOrder.replyWindowMinutes), description: `提交处置结果、现场照片和影响复核,最迟 ${formatClock(replyEnd)} 前完成复令。` } ]; } function getConditionStatus( task: ConditionTaskDefinition, scheduledAt: Date, now: Date, signal: TaskSignal ): ScheduledConditionStatus { const elapsedMinutes = (now.getTime() - scheduledAt.getTime()) / 60_000; const durationMinutes = getTaskExecutionDuration(task); if (elapsedMinutes >= 0 && elapsedMinutes < durationMinutes) { return "running"; } if (task.id === "smart-dispatch") { return "completed"; } if (signal.completeness < 90) { return "error"; } if (signal.deviation >= 0.72) { return "warning"; } return "completed"; } function getTaskExecutionDuration(task: ConditionTaskDefinition) { if (task.id === GUARANTEED_RUNNING_TASK_ID) { return Math.max(task.durationMinutes, task.intervalMinutes); } return task.durationMinutes; } function getRiskLevel(status: ScheduledConditionStatus, signal: TaskSignal): ScheduledConditionRiskLevel { if (signal.taskId === "smart-dispatch" && status === "completed") { return "normal"; } if (status === "error" || signal.deviation >= 0.9) { return "critical"; } if (status === "warning" || status === "running" || signal.deviation >= 0.62) { return "attention"; } return "normal"; } function createTaskSignal(task: ConditionTaskDefinition, scheduledAt: Date): TaskSignal { const seed = hashString(`${task.id}-${formatCompactLocalTime(scheduledAt)}`); const targetIndex = (seed % 7) + 1; const deviation = ((seed >> 3) % 100) / 100; const completenessBase = 96 - ((seed >> 5) % 7); return { completeness: deviation > 0.94 ? 88 : completenessBase, deviation, target: getSignalTarget(task.id, targetIndex), sampleCount: task.id === "scada-diagnosis" ? 96 + (seed % 56) : 24 + (seed % 18), taskId: task.id }; } function getSignalTarget(taskId: ConditionTaskId, index: number) { if (taskId === "scada-diagnosis") { return `SCADA 诊断域-${index}`; } if (taskId === "smart-dispatch") { return `调度指令组-${index}`; } if (taskId === "network-simulation") { return `全网模型边界-${index}`; } return `${index}# 泵站`; } function createDetail( task: ConditionTaskDefinition, scheduledAt: Date, status: ScheduledConditionStatus, signal: TaskSignal ) { const timeLabel = formatClock(scheduledAt); if (status === "running") { return `${timeLabel} 的${task.title}正在执行,正在汇总 ${signal.target} 的实时遥测、模型边界和异常阈值。`; } if (task.id === "smart-dispatch") { const instructions = createDispatchInstructions(signal); return `${timeLabel} 的${task.title}已完成,已形成${instructions.map((item) => item.action).join("、")}指令。`; } if (status === "error") { return `${timeLabel} 的${task.title}未完整闭环,${signal.target} 数据完整率为 ${signal.completeness}%,需要先复核数据源。`; } if (status === "warning") { return `${timeLabel} 的${task.title}发现关注偏差,${signal.target} 偏离运行基线 ${(signal.deviation * 100).toFixed(0)}%。`; } return `${timeLabel} 的${task.title}已完成,${signal.target} 关键指标处于当前调度可解释范围。`; } function createEvidence(task: ConditionTaskDefinition, signal: TaskSignal, status: ScheduledConditionStatus) { if (task.id === "smart-dispatch" && status !== "running") { const instructions = createDispatchInstructions(signal); return [ `${task.title}按 ${task.intervalMinutes} 分钟周期执行,本轮对象为 ${signal.target}。`, `已生成 ${instructions.length} 条管网要素调整指令,并标记调度员复核后下发。`, ...instructions.map((item, index) => `指令 ${index + 1}:${item.target},${item.command}`) ]; } const statusEvidence = status === "error" ? "数据源完整率低于闭环阈值,已保留失败节点供人工复核。" : status === "warning" ? "偏差超过关注阈值,建议下一轮继续观察同一对象趋势。" : "未发现需要立即处置的连锁风险。"; return [ `${task.title}按 ${task.intervalMinutes} 分钟周期执行,本轮对象为 ${signal.target}。`, `已读取 ${signal.sampleCount} 个遥测样本,关键测点完整率 ${signal.completeness}%。`, statusEvidence ]; } function createTaskKpis(task: ConditionTaskDefinition, signal: TaskSignal): ScheduledConditionKpi[] { if (task.id === "scada-diagnosis") { const pressureFreshness = Math.max(82, Math.round(99 - signal.deviation * 12)); const flowFreshness = Math.max(80, Math.round(98 - signal.deviation * 13)); const abnormalSamples = Math.round(signal.deviation * 9); const consistencyScore = Math.max(75, Math.round(99 - signal.deviation * 20)); return [ { id: "pressure-scada-freshness", label: "压力 SCADA 新鲜度", value: pressureFreshness.toString(), unit: "%", threshold: "关注 < 94%,异常 < 90%", status: getInverseMetricRisk(pressureFreshness, 94, 90), description: "压力监测点最近 5 分钟内有效回传占比,覆盖 DMA 分区压力巡检范围。" }, { id: "flow-scada-freshness", label: "流量 SCADA 新鲜度", value: flowFreshness.toString(), unit: "%", threshold: "关注 < 93%,异常 < 88%", status: getInverseMetricRisk(flowFreshness, 93, 88), description: "流量计最近 5 分钟内有效回传占比,用于识别数据滞后和通信异常。" }, { id: "abnormal-scada-samples", label: "异常样本数", value: abnormalSamples.toString(), unit: "个", threshold: "关注 >= 5,异常 >= 8", status: getMetricRisk(abnormalSamples, 5, 8), description: "压力、流量样本中出现突变、越界或跨源不一致的数量。" }, { id: "pressure-flow-consistency", label: "压流一致性评分", value: consistencyScore.toString(), unit: "分", threshold: "关注 < 86,异常 < 80", status: getInverseMetricRisk(consistencyScore, 86, 80), description: "压力变化与流量变化是否符合水力逻辑的综合评分。" } ]; } if (task.id === "smart-dispatch") { const instructions = createDispatchInstructions(signal); const confidence = Math.max(86, Math.round(98 - signal.deviation * 8)); const expectedPressureGain = roundToOneDecimal(0.8 + signal.deviation * 2.6); return [ { id: "dispatch-command-count", label: "调整指令数", value: instructions.length.toString(), unit: "条", threshold: "常规 2-4 条", status: "normal", description: "本轮生成的管网要素调整指令数量。" }, { id: "dispatch-confidence", label: "指令置信度", value: confidence.toString(), unit: "%", threshold: "关注 < 85%,异常 < 75%", status: getInverseMetricRisk(confidence, 85, 75), description: "基于实时工况、SCADA 数据质量和模拟结果的调度指令可信度。" }, { id: "expected-pressure-gain", label: "预计压力改善", value: expectedPressureGain.toString(), unit: "m", threshold: "常规 0.5-3.5m", status: "normal", description: "执行调整指令后,目标片区末端压力的预计改善幅度。" } ]; } if (task.id === "network-simulation") { const pressureDeviation = roundToOneDecimal(1.8 + signal.deviation * 10.6); const flowDeviation = roundToOneDecimal(2.4 + signal.deviation * 12.2); const maxPressureDelta = roundToOneDecimal(0.7 + signal.deviation * 7.8); const balanceScore = Math.max(72, Math.round(98 - signal.deviation * 22)); return [ { id: "pressure-deviation", label: "模拟-实测压力偏差", value: pressureDeviation.toString(), unit: "%", baseline: "近 7 日同窗模型偏差", threshold: "关注 > 8%,异常 > 12%", status: getMetricRisk(pressureDeviation, 8, 12), description: "对比在线水力模拟结果与 SCADA 压力监测值的平均相对偏差。" }, { id: "flow-deviation", label: "模拟-实测流量偏差", value: flowDeviation.toString(), unit: "%", baseline: "近 7 日同窗流量偏差", threshold: "关注 > 10%,异常 > 15%", status: getMetricRisk(flowDeviation, 10, 15), description: "对比模型计算流量与 SCADA 流量计回传值的偏差。" }, { id: "max-pressure-delta", label: "最大节点压差", value: maxPressureDelta.toString(), unit: "m", threshold: "关注 > 5m,异常 > 8m", status: getMetricRisk(maxPressureDelta, 5, 8), description: "模拟节点压力与实测折算压力之间的最大差值。" }, { id: "hydraulic-balance-score", label: "水力平衡评分", value: balanceScore.toString(), unit: "分", threshold: "关注 < 84,异常 < 78", status: getInverseMetricRisk(balanceScore, 84, 78), description: "综合压力、流量、边界条件一致性后的模型可信评分。" } ]; } if (task.id === "pump-energy") { const energyDeviation = roundToOneDecimal(2 + signal.deviation * 13); const unitEnergy = roundToTwoDecimals(0.285 + signal.deviation * 0.095); const starts = 1 + Math.round(signal.deviation * 5); return [ { id: "energy-deviation", label: "单位排水电耗偏差", value: energyDeviation.toString(), unit: "%", threshold: "关注 > 10%,异常 > 15%", status: getMetricRisk(energyDeviation, 10, 15), description: "本轮单位排水电耗相对同窗基线的偏差。" }, { id: "unit-energy", label: "单位排水电耗", value: unitEnergy.toString(), unit: "kWh/m3", baseline: "同窗经济运行区间 0.28-0.34", threshold: "关注 > 0.35,异常 > 0.38", status: getMetricRisk(unitEnergy, 0.35, 0.38), description: "按泵站出水量折算后的实时能耗水平。" }, { id: "pump-starts", label: "泵组启停次数", value: starts.toString(), unit: "次", threshold: "关注 >= 4,异常 >= 6", status: getMetricRisk(starts, 4, 6), description: "当前窗口内泵组启停频率,用于识别低效联动。" } ]; } return [createCompletenessKpi(signal)]; } function createCompletenessKpi(signal: TaskSignal): ScheduledConditionKpi { return { id: "telemetry-completeness", label: "遥测完整率", value: signal.completeness.toString(), unit: "%", threshold: "关注 < 93%,异常 < 90%", status: getInverseMetricRisk(signal.completeness, 93, 90), description: "本轮参与判断的 SCADA/遥测样本完整程度。" }; } function createDispatchInstructions(signal: TaskSignal): DispatchInstruction[] { const templates: DispatchInstruction[][] = [ [ { action: "泵站目标压力微调", target: "2# 泵站出水压力", command: "将目标压力从 0.42MPa 调整至 0.44MPa,保持变频泵优先运行", verification: "15 分钟后复核 DMA-03 末端压力是否提升 1.5m 以上" }, { action: "边界阀开度校正", target: "DMA-03 / DMA-05 边界阀 BV-031", command: "开度由 38% 调整至 42%,限制单次调整幅度不超过 5%", verification: "观察相邻分区压差是否回落至 3m 内" }, { action: "末端压力观察", target: "华山路末端压力点 P-117", command: "将该点加入 15 分钟高频复核队列", verification: "若压力低于 0.18MPa 持续两轮,转人工调度确认" } ], [ { action: "泵组组合优化", target: "1# 泵站 3# / 4# 泵组", command: "保持 3# 泵运行,延后 4# 泵启动 10 分钟,避免低效并联区间", verification: "复核单位排水电耗是否低于 0.34kWh/m3" }, { action: "高区排水边界收敛", target: "高区联络阀 HV-204", command: "开度下调 3%,减少高区向中区回流", verification: "确认高区末端压力不低于 0.22MPa" }, { action: "流量计复核", target: "人民路 DN300 流量计 F-088", command: "将该流量计设为本轮调度结果校验点", verification: "15 分钟内流量波动需低于 6%" } ], [ { action: "DMA 边界切换准备", target: "DMA-07 北侧边界阀 BV-072", command: "保持现状,预置 45% 开度为备选边界方案", verification: "若下一轮 SCADA 诊断正常,可进入调度员确认" }, { action: "低压片区补压", target: "北辰片区支线 PRV-12", command: "出口压力设定值上调 0.01MPa,禁止联动超过 0.03MPa", verification: "复核北辰消防栓维修工单影响范围内压力恢复" }, { action: "模型边界刷新", target: "全网模型边界条件", command: "使用最新泵站出水、阀门开度和 SCADA 流量刷新模拟边界", verification: "模拟-实测压力偏差需回落至 8% 内" } ], [ { action: "阀门开度回退", target: "华山路沿线阀门组 VG-11", command: "将昨日临时开度回退 2%,恢复常规排水边界", verification: "观察华山路和人民路支线压差不超过 2.5m" }, { action: "泵站压力保持", target: "3# 泵站出口压力", command: "维持 0.40MPa 不变,禁止自动升压策略介入", verification: "若末端压力连续两轮低于 0.19MPa 再触发升压建议" }, { action: "异常测点隔离", target: "SCADA 诊断异常测点组", command: "调度计算暂不采用异常测点,使用相邻测点插补边界", verification: "等待下一轮 5 分钟 SCADA 数据诊断恢复后解除隔离" } ] ]; const templateIndex = Math.round(signal.deviation * 100) % templates.length; const selected = templates[templateIndex]; const count = 2 + (Math.round(signal.deviation * 10) % 2); return selected.slice(0, count); } function createConditionReport( task: ConditionTaskDefinition, status: ScheduledConditionStatus, riskLevel: ScheduledConditionRiskLevel, signal: TaskSignal, kpis: ScheduledConditionKpi[] ): ScheduledConditionReport { const attentionKpis = kpis.filter((kpi) => kpi.status !== "normal"); const conclusion = status === "running" ? `本轮${task.title}正在形成报告,已先返回 ${signal.target} 的阶段性关键指标。` : attentionKpis.length > 0 ? `本轮${task.title}发现 ${attentionKpis.length} 项关键指标需要关注,建议结合下一轮趋势复核。` : `本轮${task.title}关键指标均处于可解释范围,可维持当前调度方案。`; return { title: `${task.title}报告`, conclusion, sections: [ { title: "核心判断", items: [ `${signal.target} 本轮样本数 ${signal.sampleCount},遥测完整率 ${signal.completeness}%。`, attentionKpis.length > 0 ? `关注指标:${attentionKpis.map((kpi) => kpi.label).join("、")}。` : "未发现超过关注阈值的关键指标。", riskLevel === "critical" ? "当前风险等级为异常,建议先复核数据源和边界条件。" : "当前风险未达到自动处置阈值。" ] }, { title: "调度指导", items: createGuidanceItems(task, attentionKpis, signal) } ] }; } function createGuidanceItems( task: ConditionTaskDefinition, attentionKpis: ScheduledConditionKpi[], signal: TaskSignal ) { if (task.id === "scada-diagnosis") { return [ "优先处理新鲜度或一致性异常的压力、流量测点,避免后续模拟和调度引用失真数据。", "异常样本集中在同一 DMA 时,可直接替代分区压力巡检形成关注对象。", "若连续两轮 SCADA 诊断异常,应通知数据维护或现场巡检确认通信链路。" ]; } if (task.id === "smart-dispatch") { const instructions = createDispatchInstructions(signal); return [ ...instructions.map((item, index) => `指令 ${index + 1}:${item.target},${item.command};${item.verification}`), attentionKpis.length > 0 ? "执行前需先确认 SCADA 数据诊断关注项,必要时等待下一轮数据质量恢复。" : "本轮指令满足自动建议条件,可由调度员复核后下发执行。" ]; } if (task.id === "network-simulation") { return [ "优先核对压力和流量偏差最大的 SCADA 测点,确认模型边界是否滞后。", "若连续两轮模拟-实测偏差扩大,应重新校准泵站出水边界和关键阀门开度。", "偏差回落前,不建议直接依据模型结果执行大范围阀门或泵站动作。" ]; } if (task.id === "pump-energy") { return [ "关注单位排水电耗和启停频率,判断是否存在泵组组合不经济。", "若电耗偏差持续升高,建议比较相邻泵组效率曲线并优化启停策略。", "调整前需确认供压稳定性,避免节能动作引发末端低压。" ]; } return ["保留本轮诊断结果,等待下一轮定时任务复核。"]; } function getMetricRisk(value: number, attentionThreshold: number, criticalThreshold: number): ScheduledConditionRiskLevel { if (value >= criticalThreshold) { return "critical"; } if (value >= attentionThreshold) { return "attention"; } return "normal"; } function getInverseMetricRisk(value: number, attentionThreshold: number, criticalThreshold: number): ScheduledConditionRiskLevel { if (value <= criticalThreshold) { return "critical"; } if (value <= attentionThreshold) { return "attention"; } return "normal"; } function roundToOneDecimal(value: number) { return Math.round(value * 10) / 10; } function roundToTwoDecimals(value: number) { return Math.round(value * 100) / 100; } function createRecommendation( status: ScheduledConditionStatus, riskLevel: ScheduledConditionRiskLevel, task: ConditionTaskDefinition, signal: TaskSignal ) { if (status === "running") { return "等待本轮工况完成后再确认是否生成处置建议。"; } if (task.id === "smart-dispatch") { const instructions = createDispatchInstructions(signal); return `建议复核并下发本轮管网要素调整指令:${instructions.map((item) => `${item.target} ${item.command}`).join(";")}。`; } if (status === "error") { return "先复查遥测源连通性和模型输入边界,再决定是否转人工调度复核。"; } if (status === "warning" || riskLevel === "attention") { return `保留${task.title}关注项,下一轮继续比对同一对象趋势并复核阈值。`; } return "维持当前调度方案,无需新增人工工单。"; } function createAnalysisInsight( task: ConditionTaskDefinition, status: ScheduledConditionStatus, riskLevel: ScheduledConditionRiskLevel, signal: TaskSignal ): ScheduledConditionAnalysisInsight | undefined { if (status === "error") { return { summary: `Agent 判断本轮${task.title}未完成闭环,优先按数据源异常与局部水力异常并行排查。`, notes: [ `${signal.target} 数据完整率为 ${signal.completeness}%,低于自动闭环阈值。`, "异常证据已保留,可继续打开 Agent 会话补充上下文。", "处置前建议先确认影响分区,避免误触发调度动作。" ], solutionOptions: [ { id: "source-recheck", title: "复核数据源", scenario: "遥测超时或采样完整率不足时优先执行。", action: "重试关键测点拉取,核对 SCADA 新鲜度,并标记缺失源。", tradeoff: "动作最稳妥,但会延后现场处置判断。" }, { id: "hydraulic-compare", title: "水力模型比对", scenario: "压力/流量同时偏离基线,疑似局部水力异常。", action: "调用最近基线工况对比异常分区,输出可能影响范围。", tradeoff: "可快速缩小范围,但依赖模型参数和边界条件质量。" }, { id: "manual-dispatch", title: "人工调度复核", scenario: "异常持续超过一个调度窗口或伴随用户侧反馈。", action: "转入人工复核,准备阀门、泵站和现场巡检联动清单。", tradeoff: "处置闭环更完整,但会占用调度员和巡检资源。" } ] }; } if (status === "warning" || riskLevel === "attention") { return { summary: `Agent 将本轮${task.title}标记为关注项,当前偏差不足以触发自动处置。`, notes: [ `${signal.target} 偏离运行基线 ${(signal.deviation * 100).toFixed(0)}%,建议保留本轮证据。`, "重点观察压力/流量偏差是否持续扩大,避免单点噪声误判。", "可在 Agent 面板继续追问偏差来源和相关历史会话。" ], escalationCriteria: [ "连续两轮工况仍高于关注阈值。", "关键测点完整率继续下降或出现多源超时。", "同一分区出现用户投诉、低压或水质联动线索。" ] }; } return undefined; } function getMinutesOfDay(date: Date) { return date.getHours() * 60 + date.getMinutes(); } function setMinutesOfDay(baseDate: Date, minutesOfDay: number) { const nextDate = new Date(baseDate); nextDate.setHours(Math.floor(minutesOfDay / 60), minutesOfDay % 60, 0, 0); return nextDate; } function ceilToInterval(value: number, interval: number) { return Math.ceil(value / interval) * interval; } function floorToInterval(value: number, interval: number) { return Math.floor(value / interval) * interval; } function formatClock(date: Date) { return `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}`; } function formatClockRange(start: Date, durationMinutes: number) { const end = new Date(start.getTime() + durationMinutes * 60_000); return `${formatClock(start)} - ${formatClock(end)}`; } function formatCompactLocalTime(date: Date) { const yyyy = date.getFullYear().toString(); const mm = (date.getMonth() + 1).toString().padStart(2, "0"); const dd = date.getDate().toString().padStart(2, "0"); const hh = date.getHours().toString().padStart(2, "0"); const min = date.getMinutes().toString().padStart(2, "0"); return `${yyyy}${mm}${dd}${hh}${min}`; } function formatLocalIsoWithOffset(date: Date) { const yyyy = date.getFullYear().toString(); const mm = (date.getMonth() + 1).toString().padStart(2, "0"); const dd = date.getDate().toString().padStart(2, "0"); const hh = date.getHours().toString().padStart(2, "0"); const min = date.getMinutes().toString().padStart(2, "0"); const ss = date.getSeconds().toString().padStart(2, "0"); const offsetMinutes = -date.getTimezoneOffset(); const offsetSign = offsetMinutes >= 0 ? "+" : "-"; const offsetAbs = Math.abs(offsetMinutes); const offsetHours = Math.floor(offsetAbs / 60).toString().padStart(2, "0"); const offsetMins = (offsetAbs % 60).toString().padStart(2, "0"); return `${yyyy}-${mm}-${dd}T${hh}:${min}:${ss}${offsetSign}${offsetHours}:${offsetMins}`; } function hashString(value: string) { let hash = 0; for (let index = 0; index < value.length; index += 1) { hash = (hash * 31 + value.charCodeAt(index)) | 0; } return Math.abs(hash); }