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
+70 -5
View File
@@ -241,12 +241,14 @@ async def test_sample_sync_rejects_cross_project_data_before_writing():
@pytest.mark.anyio
@pytest.mark.parametrize("targeted", [False, True])
@pytest.mark.parametrize("order_revision", [1, 2])
@pytest.mark.parametrize(
("investigation_status", "expected_feedback_count", "routes_to_investigation"),
[
("waiting_human", 2, True),
("queued", 2, True),
("completed", 1, False),
("completed", 2, True),
("cancelled", 1, False),
],
)
async def test_created_work_order_receipt_preserves_projection_and_routes_active_feedback(
@@ -254,6 +256,7 @@ async def test_created_work_order_receipt_preserves_projection_and_routes_active
expected_feedback_count,
routes_to_investigation,
targeted,
order_revision,
):
project_id, order_id, investigation_id, projection_id = map(str, (uuid4(), uuid4(), uuid4(), uuid4()))
received = []
@@ -262,10 +265,14 @@ async def test_created_work_order_receipt_preserves_projection_and_routes_active
order = {
"project_id": project_id, "work_order_id": order_id, "origin": "synthetic",
"created_by": str(uuid4()),
"revision": 2, "summary": "采样已完成", "status": "completed", "updated_at": "2026-09-23T09:00:00Z",
"revision": order_revision, "summary": "采样任务", "status": "completed" if order_revision > 1 else "open",
"updated_at": "2026-09-23T09:00:00Z",
"title": "合成采样工单", "priority": "normal",
"work_order_type": "general", "created_at": "2026-09-23T08:00:00Z",
"scheduled_at": None,
"activity": ([{"sequence": 2, "occurred_at": "2026-09-23T09:00:00Z", "actor": "现场人员",
"kind": "feedback", "title": "现场反馈", "detail": "复测余氯0.2mg/L"}]
if order_revision > 1 else []),
}
if targeted:
assert request.url.path.endswith(f"/work-orders/{order_id}")
@@ -277,6 +284,8 @@ async def test_created_work_order_receipt_preserves_projection_and_routes_active
payload = json.loads(request.content)
assert payload["source_event_id"] == f"peripheral_sample:{order_id}"
assert ("work_order_snapshot" in payload) == request.url.path.endswith(f"{projection_id}/external-feedback")
if order_revision > 1:
assert "复测余氯0.2mg/L" in payload["evidence"]["summary"]
return httpx.Response(200, json={})
if request.url.path.endswith(investigation_id):
return httpx.Response(200, json={"actions": [{"kind": "create_work_order", "receipt": {
@@ -287,7 +296,7 @@ async def test_created_work_order_receipt_preserves_projection_and_routes_active
return httpx.Response(200, json={"items": [
{"task_id": investigation_id, "task_type": "event_investigation", "status": investigation_status},
{"task_id": projection_id, "task_type": "external_work_order", "source": {
"external_id": f"peripheral_sample:{order_id}", "external_revision": 1,
"external_id": f"peripheral_sample:{order_id}", "external_revision": 1 if order_revision > 1 else None,
}},
], "next_offset": None})
settings = WorkerSettings("http://server.test", "http://agent.test", project_id, bearer="TEST_ONLY")
@@ -295,9 +304,10 @@ async def test_created_work_order_receipt_preserves_projection_and_routes_active
assert await sync_once(
client, settings, "http://sample.test", CredentialProvider(settings),
**({"work_order_ids": {order_id}} if targeted else {}),
) == expected_feedback_count
assert any(investigation_id in path for path in received) is routes_to_investigation
) == (expected_feedback_count if order_revision > 1 else 1)
assert any(investigation_id in path for path in received) is (routes_to_investigation and order_revision > 1)
assert any(projection_id in path for path in received)
assert received[-1].endswith(f"{projection_id}/external-feedback")
@pytest.mark.anyio
@@ -331,6 +341,61 @@ async def test_external_events_start_investigations_without_browser(event_kind):
assert investigation["trigger"]["occurred_at"] == "2026-09-23T07:30:00Z"
@pytest.mark.anyio
@pytest.mark.parametrize("failure_target", ["parent", "projection"])
async def test_child_feedback_retry_keeps_projection_cursor_behind_all_parent_deliveries(failure_target):
project_id, order_id, parent_id, projection_id = map(str, (uuid4(), uuid4(), uuid4(), uuid4()))
parent = {"task_id": parent_id, "task_type": "event_investigation", "status": "completed", "source": {}}
projection = {"task_id": projection_id, "task_type": "external_work_order", "source": {
"external_id": f"peripheral_sample:{order_id}", "external_revision": 1,
}}
order = {"project_id": project_id, "work_order_id": order_id, "origin": "synthetic",
"revision": 2, "title": "采样", "summary": "复测通过", "status": "completed", "priority": "normal",
"created_at": "2026-10-10T00:00:00Z", "updated_at": "2026-10-10T01:00:00Z", "scheduled_at": None,
"work_order_type": "general"}
failed = False
accepted_parent_requests = set()
detail_reads = []
def handler(request):
nonlocal failed
if request.url.host == "sample.test":
return httpx.Response(200, json={"items": [order], "next_cursor": None})
if request.method == "GET":
if request.url.path.endswith(parent_id):
detail_reads.append(parent_id)
return httpx.Response(200, json={"actions": [{"kind": "create_work_order", "receipt": {
"adapter": "peripheral_sample", "receipt_id": order_id,
}}]})
return httpx.Response(200, json={"items": [parent, projection], "next_offset": None})
payload = json.loads(request.content)
is_parent = request.url.path.endswith(f"{parent_id}/external-feedback")
if not failed and is_parent == (failure_target == "parent"):
failed = True
return httpx.Response(503, json={})
if is_parent:
accepted_parent_requests.add(payload["request_id"])
parent["status"] = "queued"
else:
assert accepted_parent_requests, "Projection must not advance before its associated investigation"
projection["source"]["external_revision"] = payload["source_revision"]
return httpx.Response(200, json={})
settings = WorkerSettings("http://server.test", "http://agent.test", project_id, bearer="TEST_ONLY")
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
credentials = CredentialProvider(settings)
with pytest.raises(httpx.HTTPStatusError):
await sync_once(client, settings, "http://sample.test", credentials)
assert projection["source"]["external_revision"] == 1
assert await sync_once(client, settings, "http://sample.test", credentials) == 2
assert len(accepted_parent_requests) == 1
assert projection["source"]["external_revision"] == 2
parent["status"] = "completed"
detail_reads.clear()
assert await sync_once(client, settings, "http://sample.test", credentials) == 0
assert detail_reads == [], "Unchanged source versions must not scan completed parent details forever"
def test_snapshot_preserves_recorded_activity_without_legacy_subtasks():
order = {
"work_order_id": str(uuid4()), "revision": 4, "title": "合成预约", "summary": "仅测试过程同步",