feat(scada): add project monitoring replay
This commit is contained in:
@@ -161,6 +161,14 @@ class OperationalTaskService:
|
||||
)
|
||||
|
||||
async def create(self, command: OperationalTaskCreate) -> OperationalTaskDetail:
|
||||
if command.source.replay_context:
|
||||
from app.infra.db.postgresql.scada_replays import ScadaReplayRepository
|
||||
replay_context = command.source.replay_context
|
||||
replay_doc = await ScadaReplayRepository(self.repo.connection, self.context.project_id).get(replay_context.replay_id)
|
||||
require(replay_doc.get("delivered_until") is not None
|
||||
and replay_context.knowledge_cutoff <= datetime.fromisoformat(replay_doc["delivered_until"])
|
||||
and replay_context.batch_sequence <= replay_doc["batch_sequence"],
|
||||
"REPLAY_SCOPE_INVALID", "回放调查不能使用尚未读取的批次", status=422)
|
||||
body = _json(command)
|
||||
request_id = str(command.request_id)
|
||||
scope = "task:create"
|
||||
@@ -219,6 +227,27 @@ class OperationalTaskService:
|
||||
await self._task_event(task_document, now)
|
||||
return await self.detail(task_id)
|
||||
|
||||
async def advance_replay_investigation(self, task_id: UUID, replay_context: dict, request_id: UUID) -> None:
|
||||
"""A recovery is new evidence, coalesced through the existing run lifecycle."""
|
||||
scope = f"task:{task_id}:replay-evidence"
|
||||
async with self.repo.transaction():
|
||||
if await self._request_replay(scope, request_id, replay_context):
|
||||
return
|
||||
await self.repo.lock(f"task:{task_id}")
|
||||
task = await self._task(task_id)
|
||||
prior = task["source"].get("replay_context")
|
||||
require(prior and prior["replay_id"] == replay_context["replay_id"], "REPLAY_SCOPE_INVALID",
|
||||
"Replay evidence belongs to another investigation", status=409)
|
||||
require(datetime.fromisoformat(prior["knowledge_cutoff"]) <= datetime.fromisoformat(replay_context["knowledge_cutoff"]),
|
||||
"REPLAY_SCOPE_INVALID", "Replay evidence cannot move backwards", status=409)
|
||||
task["source"]["replay_context"] = replay_context
|
||||
task["source"]["external_revision"] = task["source"].get("external_revision", 1) + 1
|
||||
now = self._now()
|
||||
await self._requeue_task(task, now)
|
||||
await self.repo.update_task(_task_json(OperationalTaskSummary.model_validate(task)))
|
||||
await self._task_event(task, now)
|
||||
await self._save_request(scope, request_id, replay_context, {"task_id": str(task_id)})
|
||||
|
||||
async def list(
|
||||
self, *, task_type: str | None, status: str | None, limit: int, offset: int,
|
||||
peripheral_work_order_id: UUID | None = None,
|
||||
@@ -336,6 +365,9 @@ class OperationalTaskService:
|
||||
selected_report=await self.report(task_id, report_revision) if report_revision is not None else None,
|
||||
)
|
||||
|
||||
async def replay_context(self, task_id: UUID) -> dict | None:
|
||||
return (await self._task(task_id)).get("source", {}).get("replay_context")
|
||||
|
||||
async def can_read_source(self, task_id: UUID, source: TaskDataSource) -> bool:
|
||||
task = await self._task(task_id)
|
||||
permission = SCADA_VIEW if source.startswith("scada_") else SIMULATION_VIEW
|
||||
@@ -1037,6 +1069,8 @@ class OperationalTaskService:
|
||||
await self.repo.lock(f"run:{command.run_id}")
|
||||
task = await self._task(task_id)
|
||||
self._require_actionable_task(task)
|
||||
require(not task.get("source", {}).get("replay_context"), "REPLAY_ACTION_DISABLED",
|
||||
"SCADA 回放调查仅生成分析与报告,不执行真实工单或设备动作", status=409)
|
||||
run = await self._active_run(task)
|
||||
require(run is not None and str(run["run_id"]) == str(command.run_id)
|
||||
and run["generation"] == task["execution_generation"],
|
||||
|
||||
Reference in New Issue
Block a user