feat(scada): add calibrated monitoring and incident recovery

This commit is contained in:
2026-11-07 19:44:58 +08:00
parent 26fa5d1909
commit ff8aba4964
109 changed files with 9165 additions and 629 deletions
+38 -8
View File
@@ -20,6 +20,7 @@ from app.infra.db.postgresql.gis_attributes import GisAttributeRepository
from app.infra.db.postgresql.network_settings import NetworkSettingsRepository
from app.infra.db.postgresql.operational_tasks import OperationalTaskRepository
from app.services.operational_tasks import OperationalTaskService
from app.services.burst_location_demo import location_demo_result
from app.mcp.models import QueryResult, PageInfo
from app.mcp.scenario_service import SCENARIO_TOOLS
from app.mcp.work_order_skills import read_work_order_skill
@@ -73,9 +74,13 @@ async def authorize_query(
from app.services.scada_replay_queries import check_replay_window
allowed = {"get_task_context", "get_task_evidence", "list_scada_devices", "get_scada_device",
"query_scada_readings", "get_network_schema", "run_burst_location",
"get_element_properties", "list_elements", "query_nearby_elements", "get_network_options",
"get_node_connections", "get_element_gis_properties", "query_network_extent",
"list_regions", "get_region", "get_region_nodes",
"analyze_valve_isolation", "generate_node_coverage",
"get_calculation_job", "get_calculation_result", "list_calculation_jobs"}
if tool.name not in allowed:
raise HTTPException(403, "回放调查当前仅开放冻结 SCADA 证据与模型目录查询;请使用 query_scada_readings")
raise HTTPException(403, "回放调查仅开放冻结 SCADA、资产与拓扑分析;时序观测请使用 query_scada_readings")
if tool.name == "query_scada_readings":
check_replay_window(replay_context, args.start, args.end)
if tool.name == "run_burst_location":
@@ -126,6 +131,11 @@ async def authorize_query(
granted = await task_service(scope).can_read_source(binding.task_id, source)
if not granted:
raise HTTPException(403, "Data source permission is required")
if tool.name == "run_burst_location" and args.burst_leakage is None:
from app.services.burst_location_demo import is_location_demo
if not is_location_demo(getattr(scope.project, "project_code", None), args):
raise HTTPException(422, "水力定位需要有依据的 burst_leakage;仅已开放的候选定位路径可省略漏量")
if (
tool.name == "query_element_history"
and args.mode != "observed"
@@ -179,13 +189,16 @@ async def execute_query(
parameters = await replay_burst_location_inputs(
conn, scope.project.project_id, replay_context, args,
)
result = await location_demo_result(conn, scope.project.project_code, args)
used_demo = result is not None
await conn.commit()
async with scope.simulation_routing():
result = await calculate_replay_burst_location(
parameters,
network=scope.project.project_code,
username=scope.user.username,
)
if result is None:
async with scope.simulation_routing():
result = await calculate_replay_burst_location(
parameters,
network=scope.project.project_code,
username=scope.user.username,
)
data = {
"parameters": args.model_dump(mode="json", exclude={"context_handle"}),
"result": result,
@@ -194,6 +207,15 @@ async def execute_query(
gaps = [
"定位只使用任务知识截止前已固化的回放清洗输出;候选排序依赖漏量假设、模型和测点覆盖,仍需现场核实。"
]
if used_demo:
gaps = [result["notice"]] if result.get("notice") else []
elif name == "run_burst_location" and (
demo_result := await location_demo_result(conn, scope.project.project_code, args)
) is not None:
data = {"parameters": args.model_dump(mode="json", exclude={"context_handle"}),
"result": demo_result}
origin = "synthetic"
gaps = [demo_result["notice"]] if demo_result.get("notice") else []
elif tool.computation:
from app.mcp.analysis_service import TIMESCALE_CALCULATIONS, calculate
@@ -449,7 +471,8 @@ async def execute_query(
data = await reads.task_context_summary(conn, project_id=binding.project_id,
task_id=binding.task_id, report_revision=args.report_revision)
skill = read_work_order_skill(
data.get("task", {}).get("workflow"), include_content=args.section == "skill"
data.get("task", {}).get("workflow"), include_content=args.section == "skill",
replay_context=replay_context,
)
if args.section == "skill":
data = {"section": "skill", "skill": skill}
@@ -461,6 +484,13 @@ async def execute_query(
data["available_sections"] = ["task", "report", "evidence", "actions", "progress", "access_requests", "plan", "run", "skill", "questions"]
truncated = True
gaps.append("Task context is a bounded summary; use section, data_path, limit and offset for detail. Previews are not complete reports.")
if (skill.get("name") == "scada-anomaly" and replay_context
and "run_burst_location" in binding.tool_cap):
from app.services.burst_location_demo import location_demo_context
data["location_demo"] = location_demo_context(
getattr(scope.project, "project_code", None), replay_context,
)
else:
projection = await reads.task_context_page(conn, project_id=binding.project_id, task_id=binding.task_id,
section=args.section, report_revision=args.report_revision, data_path=args.data_path, limit=args.limit, offset=args.offset)