feat(workbench): add runtime GIS and sample integrations

This commit is contained in:
2026-09-22 10:44:31 +08:00
parent a3b4e75317
commit 7ab9b36a5e
62 changed files with 6685 additions and 51 deletions
+390
View File
@@ -0,0 +1,390 @@
"""Project persisted runtime state into the read-only supply workbench feed."""
from collections.abc import Callable
from datetime import datetime, timezone
from math import isfinite
from typing import Any
from app.domain.schemas.workbench_runtime import (
WorkbenchPersistedAlert,
WorkbenchRuntimeWorkOrder,
)
_ANALYSIS_TYPE_LABELS = {
"burst_analysis": "爆管分析",
"burst_detection": "爆管检测",
"burst_location": "爆管定位",
"contaminant_analysis": "污染物分析",
"contaminant_simulation": "污染物模拟",
"dma_leak_identification": "DMA 漏损识别",
"flushing_analysis": "冲洗分析",
"pressure_regulation": "压力调控",
"sensor_placement": "传感器布点",
"valve_close_analysis": "关阀分析",
}
_ANALYSIS_STATUS_LABELS = {
"queued": "排队中",
"pending": "等待中",
"running": "运行中",
"completed": "已完成",
"succeeded": "已成功",
"failed": "已失败",
"error": "异常",
"cancelled": "已取消",
}
async def build_workbench_runtime_snapshot(
*,
analysis_runs: list[dict[str, Any]] | None = None,
latest_analysis_results: list[dict[str, Any]] | None = None,
latest_analysis_map_payload: dict[str, Any] | None = None,
work_order_storage_configured: bool = False,
work_orders: list[dict[str, Any]] | None = None,
alert_storage_configured: bool = False,
persisted_alerts: list[dict[str, Any]] | None = None,
scenario_availability: str | None = None,
artifact_availability: str | None = None,
schedule_availability: str | None = None,
work_order_availability: str | None = None,
alert_availability: str | None = None,
projection_degraded: bool = False,
clock: Callable[[], datetime] = lambda: datetime.now(timezone.utc),
) -> dict[str, Any]:
persisted_analysis_runs = (analysis_runs or [])[:100]
scenarios = [project_analysis_scenario(run) for run in persisted_analysis_runs[:20]]
latest_analysis = (
project_latest_analysis(
persisted_analysis_runs[0],
latest_analysis_results or [],
referenced_map_payload=latest_analysis_map_payload,
)
if persisted_analysis_runs
else None
)
persisted_work_orders = [
WorkbenchRuntimeWorkOrder.model_validate(item).model_dump()
for item in (
(work_orders or [])[:100]
if work_order_storage_configured
else []
)
]
stored_alerts = [
WorkbenchPersistedAlert.model_validate(item)
for item in (
(persisted_alerts or [])[:100]
if alert_storage_configured
else []
)
]
active_stored_alerts = [
{
"id": f"backend-alert-{alert.alert_id}",
"condition_id": alert.condition_id,
"source": alert.source.model_dump(),
"title": alert.title,
"description": alert.description,
"occurred_at": alert.occurred_at,
"severity": alert.severity,
}
for alert in stored_alerts
if alert.status == "active"
]
projected_alerts = active_stored_alerts
projected_alerts.sort(
key=lambda alert: _timestamp(alert["occurred_at"]),
reverse=True,
)
source = "none"
resolved_scenario_availability = scenario_availability or (
"ready" if scenarios else "not_configured"
)
resolved_artifact_availability = artifact_availability or (
"ready" if latest_analysis else "not_configured"
)
resolved_schedule_availability = schedule_availability or "not_configured"
resolved_work_order_availability = work_order_availability or (
"ready" if work_order_storage_configured else "not_configured"
)
resolved_alert_availability = alert_availability or (
"ready" if alert_storage_configured else "not_configured"
)
section_availabilities = (
resolved_scenario_availability,
resolved_artifact_availability,
resolved_schedule_availability,
resolved_work_order_availability,
resolved_alert_availability,
)
return {
"availability": (
"degraded"
if projection_degraded or "degraded" in section_availabilities
else
"ready"
if (
source != "none"
or work_order_storage_configured
or alert_storage_configured
)
else "not_configured"
),
"source": source,
"scenario_availability": resolved_scenario_availability,
"artifact_availability": resolved_artifact_availability,
"schedule_availability": resolved_schedule_availability,
"work_order_availability": resolved_work_order_availability,
"alert_availability": resolved_alert_availability,
"generated_at": clock(),
"conditions": [],
"alerts": projected_alerts,
"scenarios": scenarios,
"latest_analysis": latest_analysis,
"work_orders": persisted_work_orders,
}
def project_analysis_scenario(run: dict[str, Any]) -> dict[str, Any]:
run_id = str(run["run_id"])
run_type = str(run.get("run_type") or "analysis")
run_status = str(run.get("status") or "unknown").lower()
type_label = _analysis_type_label(run_type)
return {
"id": f"analysis-run-{run_id}",
"run_id": run_id,
"name": str(run.get("name") or type_label),
"description": (
f"{type_label} · {_analysis_status_label(run_status)} · "
f"创建人 {run.get('created_by') or 'unknown'}"
),
"status": _analysis_scenario_state(run_status),
"run_type": run_type,
"run_type_label": type_label,
"run_status": run_status,
"created_at": run["created_at"],
}
def project_analysis_history_item(run: dict[str, Any]) -> dict[str, Any]:
run_type = str(run.get("run_type") or "analysis")
run_status = str(run.get("status") or "unknown").lower()
created_at = run["created_at"]
return {
"run_id": str(run["run_id"]),
"name": str(run.get("name") or _analysis_type_label(run_type)),
"run_type": run_type,
"run_type_label": _analysis_type_label(run_type),
"status": run_status,
"status_label": _analysis_status_label(run_status),
"created_by": str(run.get("created_by") or "unknown"),
"created_at": created_at,
"started_at": run.get("started_at") or created_at,
"has_results": bool(run.get("has_results")),
}
def project_latest_analysis(
run: dict[str, Any],
results: list[dict[str, Any]],
*,
referenced_map_payload: dict[str, Any] | None = None,
) -> dict[str, Any]:
run_type = str(run.get("run_type") or "analysis")
run_status = str(run.get("status") or "unknown").lower()
created_at = run["created_at"]
return {
"run_id": str(run["run_id"]),
"name": str(run.get("name") or _analysis_type_label(run_type)),
"run_type": run_type,
"run_type_label": _analysis_type_label(run_type),
"status": run_status,
"status_label": _analysis_status_label(run_status),
"created_by": str(run.get("created_by") or "unknown"),
"created_at": created_at,
"started_at": run.get("started_at") or created_at,
"results": [
{
"result_type": str(result.get("result_type") or "analysis_result"),
"created_at": result.get("created_at") or created_at,
"facts": _analysis_result_facts(result),
}
for result in results
],
"map_payload": _analysis_map_payload(results) or referenced_map_payload,
}
def sensor_location_ids_for_map_projection(
results: list[dict[str, Any]],
) -> list[str]:
if _analysis_map_payload(results) is not None:
return []
for result in reversed(results):
if result.get("result_type") != "sensor_placement":
continue
payload = result.get("payload")
if not isinstance(payload, dict):
continue
locations = _non_empty_text_list(payload.get("sensor_locations"))
if locations:
return list(dict.fromkeys(locations))[:500]
return []
def project_sensor_location_map_payload(
node_ids: list[str], map_points: list[dict[str, Any]]
) -> dict[str, Any] | None:
points_by_id = {
str(point.get("node_id")): point
for point in map_points
if point.get("node_id") is not None
}
features: list[dict[str, Any]] = []
for position, node_id in enumerate(node_ids, start=1):
point = points_by_id.get(node_id)
if point is None:
continue
longitude = point.get("longitude")
latitude = point.get("latitude")
if (
isinstance(longitude, bool)
or isinstance(latitude, bool)
or not isinstance(longitude, (int, float))
or not isinstance(latitude, (int, float))
or not isfinite(float(longitude))
or not isfinite(float(latitude))
):
continue
features.append({
"type": "Feature",
"id": node_id,
"properties": {
"label": f"监测点 {position} · {node_id}",
"node_id": node_id,
"result_type": "sensor_placement",
"position": position,
},
"geometry": {
"type": "Point",
"coordinates": [float(longitude), float(latitude)],
},
})
if not features:
return None
return {"type": "FeatureCollection", "features": features}
def _analysis_result_facts(result: dict[str, Any]) -> list[dict[str, str]]:
facts: list[dict[str, str]] = []
node_id = _non_empty_text(result.get("node_id"))
link_id = _non_empty_text(result.get("link_id"))
if node_id:
facts.append({"label": "关联节点", "value": node_id})
if link_id:
facts.append({"label": "关联管段", "value": link_id})
payload = result.get("payload")
if not isinstance(payload, dict):
return facts
result_type = str(result.get("result_type") or "")
if result_type == "sensor_placement":
locations = _non_empty_text_list(payload.get("sensor_locations"))
sensor_count = _finite_number_text(payload.get("sensor_number"))
if sensor_count is None and locations:
sensor_count = str(len(locations))
if sensor_count is not None:
facts.append({"label": "测点数量", "value": sensor_count})
minimum_diameter = _finite_number_text(payload.get("minimum_diameter"))
if minimum_diameter is not None:
facts.append({"label": "最小管径参数", "value": minimum_diameter})
if locations:
facts.append({
"label": "测点位置",
"value": _summarize_identifiers(locations),
})
elif result_type == "leakage_identification":
run_status = _non_empty_text(payload.get("run_status"))
if run_status:
facts.append({"label": "结果状态", "value": run_status})
sensor_nodes = _non_empty_text_list(payload.get("sensor_nodes"))
if sensor_nodes:
facts.append({"label": "传感器节点数", "value": str(len(sensor_nodes))})
rows = payload.get("rows")
if isinstance(rows, list):
facts.append({"label": "结果行数", "value": str(len(rows))})
areas = payload.get("areas")
if isinstance(areas, list):
facts.append({"label": "分区数量", "value": str(len(areas))})
return facts
def _non_empty_text(value: object) -> str | None:
if not isinstance(value, str):
return None
normalized = value.strip()
return normalized or None
def _non_empty_text_list(value: object) -> list[str]:
if not isinstance(value, list):
return []
return [text for item in value if (text := _non_empty_text(item)) is not None]
def _finite_number_text(value: object) -> str | None:
if isinstance(value, bool) or not isinstance(value, (int, float)):
return None
if isinstance(value, float) and not isfinite(value):
return None
return str(value)
def _summarize_identifiers(values: list[str], limit: int = 12) -> str:
visible = values[:limit]
suffix = f" 等 {len(values)} 个" if len(values) > limit else ""
return "、".join(visible) + suffix
def _analysis_map_payload(
results: list[dict[str, Any]],
) -> dict[str, Any] | None:
for result in reversed(results):
payload = result.get("payload")
if not isinstance(payload, dict):
continue
candidate = payload.get("drawing_payload")
if not isinstance(candidate, dict):
candidate = payload
if (
candidate.get("type") == "FeatureCollection"
and isinstance(candidate.get("features"), list)
and candidate["features"]
):
return candidate
return None
def _analysis_type_label(run_type: str) -> str:
return _ANALYSIS_TYPE_LABELS.get(run_type, run_type)
def _analysis_status_label(status: str) -> str:
return _ANALYSIS_STATUS_LABELS.get(status, status)
def _analysis_scenario_state(status: str) -> str:
if status == "running":
return "active"
if status in {"completed", "succeeded", "failed", "error"}:
return "review"
return "draft"
def _timestamp(value: datetime | str) -> float:
parsed = (
value
if isinstance(value, datetime)
else datetime.fromisoformat(value.replace("Z", "+00:00"))
)
return parsed.timestamp()