feat(scada): add calibrated monitoring and incident recovery
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user