feat(workbench): add dynamic task approval lifecycle

This commit is contained in:
2026-09-24 16:36:12 +08:00
parent db092b0f5e
commit ad813a7c16
35 changed files with 2956 additions and 564 deletions
+48 -2
View File
@@ -6,10 +6,14 @@ import argparse
import asyncio
import logging
import os
from uuid import NAMESPACE_URL, uuid5
import sys
from pathlib import Path
from uuid import NAMESPACE_URL, UUID, uuid5
import httpx
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from samples.peripheral.seed_identity import SEED_ACTOR_ID
try:
@@ -24,6 +28,16 @@ STATUS = {"draft": "scheduled", "open": "assigned", "in_progress": "in_progress"
EVENT_WORK_ORDER_TYPES = frozenset({"water_quality_complaint", "burst_report"})
def peripheral_order_id(external_id: str | None) -> str | None:
"""Return only canonical IDs owned by this synchronizer's namespace."""
if not external_id or not external_id.startswith("peripheral_sample:"):
return None
try:
return str(UUID(external_id.removeprefix("peripheral_sample:")))
except ValueError:
return None
def should_project_work_order(order: dict, linked_order_ids: set[str]) -> bool:
"""Exclude retired seed checks while preserving user and workflow orders."""
work_order_id = order["work_order_id"]
@@ -87,6 +101,7 @@ async def sync_once(client: httpx.AsyncClient, settings: WorkerSettings, periphe
linked_order_ids = set(linked)
cursor, synchronized = None, 0
seen_order_ids: set[str] = set()
while True:
params = {"limit": 100}
if cursor:
@@ -98,6 +113,7 @@ async def sync_once(client: httpx.AsyncClient, settings: WorkerSettings, periphe
for order in page["items"]:
if order["project_id"] != settings.project_id or order["origin"] != "synthetic":
raise ValueError("peripheral work order scope or origin mismatch")
seen_order_ids.add(order["work_order_id"])
if not should_project_work_order(order, linked_order_ids):
continue
external_id = f"peripheral_sample:{order['work_order_id']}"
@@ -162,7 +178,37 @@ async def sync_once(client: httpx.AsyncClient, settings: WorkerSettings, periphe
synchronized += 1
cursor = page.get("next_cursor")
if not cursor:
return synchronized
break
for external_id, task in existing.items():
order_id = peripheral_order_id(external_id)
if order_id is None:
continue
if order_id in seen_order_ids:
continue
# The list may change during pagination. Only a scoped 404 proves that
# the source order is now absent; transient errors must stop this tick.
response = await client.get(
f"{peripheral_url.rstrip('/')}/api/sample/v1/work-orders/{order_id}",
headers=headers,
)
if response.status_code == 200:
continue
if response.status_code != 404:
response.raise_for_status()
if response.json().get("code") != "WORK_ORDER_NOT_FOUND":
raise ValueError("peripheral work order lookup did not confirm deletion")
result = await client.post(
f"{tasks_url}/{task['task_id']}/withdraw-external-source",
headers=headers,
json={
"request_id": str(uuid5(NAMESPACE_URL, f"{settings.project_id}:{external_id}:withdraw")),
"source_event_id": external_id,
},
)
result.raise_for_status()
synchronized += 1
return synchronized
async def run(once: bool):