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:
@@ -122,7 +122,9 @@ async def sync_once(client: httpx.AsyncClient, settings: WorkerSettings, periphe
|
||||
existing[task["source"].get("external_id")] = task
|
||||
elif task["task_type"] == "event_investigation":
|
||||
event_tasks[task.get("source", {}).get("external_id")] = task
|
||||
if task["status"] in {"queued", "waiting_human", "running"}:
|
||||
if task["status"] in {"queued", "waiting_human", "running"} or (
|
||||
work_order_ids is not None and task["status"] == "completed"
|
||||
):
|
||||
investigations[task["task_id"]] = task
|
||||
if page.get("next_offset") is None:
|
||||
break
|
||||
@@ -188,23 +190,62 @@ async def sync_once(client: httpx.AsyncClient, settings: WorkerSettings, periphe
|
||||
result.raise_for_status()
|
||||
task = result.json()
|
||||
existing[external_id] = task
|
||||
changed = (task["source"].get("external_revision") or 0) < order["revision"]
|
||||
if work_order_ids is None and changed and order["revision"] > 1:
|
||||
# Only changed sources need completed-parent receipt discovery.
|
||||
# Never fetch every historical completed investigation each tick.
|
||||
offset = 0
|
||||
while True:
|
||||
related = await client.get(tasks_url, headers=headers, params={
|
||||
"limit": 100, "offset": offset, "peripheral_work_order_id": order["work_order_id"],
|
||||
})
|
||||
related.raise_for_status()
|
||||
related_page = related.json()
|
||||
for parent in related_page["items"]:
|
||||
if parent["task_type"] != "event_investigation" or parent.get("status") != "completed":
|
||||
continue
|
||||
detail = await client.get(f"{tasks_url}/{parent['task_id']}", headers=headers)
|
||||
detail.raise_for_status()
|
||||
for action in detail.json().get("actions", []):
|
||||
receipt = action.get("receipt")
|
||||
if (action["kind"] == "create_work_order" and receipt
|
||||
and receipt["adapter"] == "peripheral_sample"
|
||||
and receipt["receipt_id"] == order["work_order_id"]):
|
||||
linked.setdefault(order["work_order_id"], []).append(parent["task_id"])
|
||||
if related_page.get("next_offset") is None:
|
||||
break
|
||||
offset = related_page["next_offset"]
|
||||
event_id = f"{external_id}:{order['revision']}"
|
||||
notes = [activity["detail"] for activity in order.get("activity", [])
|
||||
if activity.get("kind") == "feedback" and activity.get("detail")]
|
||||
evidence_summary = order["summary"]
|
||||
if notes:
|
||||
evidence_summary += "\n现场反馈:\n" + "\n".join(notes[-10:])
|
||||
feedback = {
|
||||
"request_id": str(uuid5(NAMESPACE_URL, event_id)), "source_event_id": external_id,
|
||||
"source_revision": order["revision"], "status": STATUS[order["status"]],
|
||||
"occurred_at": order["updated_at"],
|
||||
"detail": f"合成外围工单 {order['work_order_id']}:{order['status']}",
|
||||
"evidence": {"evidence_id": event_id, "kind": "synthetic", "summary": order["summary"],
|
||||
"evidence": {"evidence_id": event_id, "kind": "synthetic", "summary": evidence_summary,
|
||||
"source_label": "外围样板合成工单", "source_ref": external_id,
|
||||
"observed_at": order["updated_at"]},
|
||||
}
|
||||
targets = set(linked.get(order["work_order_id"], []))
|
||||
# A child order's creation is already represented by its dispatch
|
||||
# receipt. Only a subsequent source revision is new field input.
|
||||
targets = set(linked.get(order["work_order_id"], [])) if order["revision"] > 1 else set()
|
||||
if work_order_ids is None and not changed:
|
||||
targets.clear()
|
||||
event_task = event_tasks.get(order["work_order_id"])
|
||||
if event_task and order["revision"] > (event_task["source"].get("external_revision") or 0):
|
||||
if (event_task and event_task.get("status") != "cancelled"
|
||||
and order["revision"] > (event_task["source"].get("external_revision") or 0)):
|
||||
targets.add(event_task["task_id"])
|
||||
if (task["source"].get("external_revision") or 0) < order["revision"]:
|
||||
targets.add(task["task_id"])
|
||||
for target in targets:
|
||||
# The external projection is the reconciliation cursor. Advance it
|
||||
# only after all investigation feedback was accepted; retry remains
|
||||
# safe if a response is lost or a later target fails.
|
||||
ordered_targets = sorted(targets)
|
||||
if changed:
|
||||
ordered_targets.append(task["task_id"])
|
||||
for target in ordered_targets:
|
||||
payload = dict(feedback)
|
||||
if target == task["task_id"]:
|
||||
payload["work_order_snapshot"] = work_order_snapshot(order)
|
||||
|
||||
Reference in New Issue
Block a user