feat(server): integrate MCP queries and investigation messaging

Add project-bound OAuth MCP queries and controlled analysis with scoped evidence,
limits, timeouts and a nearby-model spatial query using EPSG:4326 inputs.
Default nearby searches to 200 meters and five results.

Integrate investigation messaging/presentation, health/readiness and typed history;
remove duplicate web search and fix online simulation and compressed baseline writes.
Include project migrations 023-025; keep local credentials and demo seeding excluded.

Validation: 319 focused pytest checks; real PostGIS temporary-table regression;
read-only Jinqian bridge radius comparison; cross-language MCP discovery/calls.
This commit is contained in:
2026-10-19 18:46:58 +08:00
parent 2b6fc5aa52
commit be6317eb48
117 changed files with 12703 additions and 638 deletions
+74 -2
View File
@@ -11,7 +11,7 @@ from app.native.wndb.model.tanks import get_tank, set_tank
from app.native.wndb.model.times import get_time, set_time
# from get_real_status import *
from datetime import datetime
from datetime import datetime, timedelta
from math import modf
import json
import pytz
@@ -150,6 +150,69 @@ def _apply_valve_control(
set_status(project_name, cs)
def run_online_simulation(
name: str,
start_time: datetime,
duration_seconds: int,
*,
output_node_ids: list[str] | None = None,
output_link_ids: list[str] | None = None,
) -> dict:
"""Recalculate online baseline snapshots without cloning a model database."""
if start_time.tzinfo is None or duration_seconds <= 0:
raise ValueError("Online simulation requires an aware start time and positive duration")
step = parse_clock_duration_seconds(
get_time(name)["HYDRAULIC TIMESTEP"], field_name="HYDRAULIC TIMESTEP"
)
if step <= 0:
raise ValueError("HYDRAULIC TIMESTEP must be greater than 0.")
mappings = query_corresponding_element_id_and_query_id(name)
selected = {
"node": {node: [] for node in output_node_ids or []},
"link": {link: [] for link in output_link_ids or []},
}
sample_times = []
units = {}
current = start_time
end = start_time + timedelta(seconds=duration_seconds)
while current < end:
output = run_simulation(
name=name,
simulation_type="realtime",
modify_pattern_start_time=current.isoformat(timespec="seconds"),
scada_mappings=mappings,
)
if not isinstance(output, dict):
raise RuntimeError("Online simulation did not return calculation results")
units = output.get("units", {})
sample_times.append(current.isoformat(timespec="seconds"))
for kind, elements in selected.items():
if not elements:
continue
rows = {
str(row[kind]): row.get("result", [])
for row in output.get(f"{kind}_results", []) if str(row[kind]) in elements
}
for element, values in elements.items():
samples = rows.get(element, [])
# Each realtime run is a single snapshot. Missing samples remain
# explicit so later values cannot shift to an earlier timestamp.
values.append(samples[0] if samples else None)
current += timedelta(seconds=step)
return {
"units": units,
"times": {
"report_start": 0, "report_step": step,
"sim_duration": duration_seconds, "num_periods": len(sample_times),
"sample_times": sample_times,
},
**{f"{kind}_results": [
{kind: element, "result": values}
for element, values in elements.items() if any(value is not None for value in values)
] for kind, elements in selected.items()},
}
# 2025/01/11
def run_simulation(
name: str,
@@ -170,7 +233,8 @@ def run_simulation(
scheme_username: str = "system",
scheme_detail: dict | None = None,
scada_mappings: ScadaElementMappings | None = None,
) -> UUID | None:
persist_results: bool = True,
) -> UUID | dict | None:
"""
传入需要修改的参数,改变数据库中对应位置的值,然后计算,返回结果
:param name: 模型名称,数据库中对应的名字
@@ -189,6 +253,10 @@ def run_simulation(
:param scheme_name:模拟方案名称
:return:
"""
if not persist_results and (
not name.startswith("tjw_tmp_") or simulation_type.upper() != "EXTENDED"
):
raise ValueError("Non-persistent simulation requires an isolated temporary model")
# 记录开始时间
time_cost_start = time.perf_counter()
@@ -514,6 +582,8 @@ def run_simulation(
num_periods_result = times_info.get("num_periods")
if num_periods_result is None:
raise RuntimeError("run_project output missing times.num_periods")
if not persist_results:
return output_data
print("simulation_type", simulation_type)
print("before store result")
# print(num_periods_result)
@@ -583,4 +653,6 @@ def run_simulation(
# TimescaleInternalQueries.fill_scheme_simulation_result_to_SCADA(scheme_type=scheme_type, scheme_name=scheme_name)
print("after store result")
if simulation_type.upper() == "REALTIME":
return output_data
return run_id if simulation_type.upper() == "EXTENDED" else None