|
|
@@ -3,12 +3,14 @@
|
|
|
import asyncio
|
|
|
import json
|
|
|
import re
|
|
|
-from collections.abc import Mapping
|
|
|
-from dataclasses import asdict
|
|
|
+from collections.abc import Awaitable, Callable, Mapping
|
|
|
+from dataclasses import asdict, replace
|
|
|
from typing import Annotated, Any, Protocol, cast
|
|
|
from urllib.parse import urlsplit, urlunsplit
|
|
|
+from uuid import uuid4
|
|
|
|
|
|
-from fastapi import APIRouter, Depends, Query, Request, WebSocket, WebSocketDisconnect
|
|
|
+from fastapi import APIRouter, Depends, Header, Query, Request, WebSocket, WebSocketDisconnect
|
|
|
+from fastapi.responses import HTMLResponse
|
|
|
|
|
|
from script_build_host.agents.prompt_catalog import script_prompt_requests
|
|
|
from script_build_host.application.mission_service import (
|
|
|
@@ -19,6 +21,7 @@ from script_build_host.application.observation_views import (
|
|
|
mission_snapshot_view,
|
|
|
task_detail_view,
|
|
|
)
|
|
|
+from script_build_host.domain.errors import BuildNotFound
|
|
|
from script_build_host.domain.ports import (
|
|
|
MissionBindingRepository,
|
|
|
PublicationRepository,
|
|
|
@@ -58,6 +61,12 @@ def create_script_build_router(
|
|
|
outbound_policy: OutboundPolicy | None = None,
|
|
|
default_model_manifest: Mapping[str, object] | None = None,
|
|
|
default_datasource_manifest: Mapping[str, object] | None = None,
|
|
|
+ phase_three: Any | None = None,
|
|
|
+ finalization: Any | None = None,
|
|
|
+ recovery: Any | None = None,
|
|
|
+ legacy_projection: Any | None = None,
|
|
|
+ legacy_api: Any | None = None,
|
|
|
+ http_command_journal: Any | None = None,
|
|
|
) -> APIRouter:
|
|
|
router = APIRouter(tags=["script-build"])
|
|
|
|
|
|
@@ -70,10 +79,33 @@ def create_script_build_router(
|
|
|
await security.authorize(script_build_id, actor)
|
|
|
return await bindings.get_by_build(script_build_id)
|
|
|
|
|
|
+ async def execute_mutation(
|
|
|
+ *,
|
|
|
+ actor: Principal,
|
|
|
+ route_family: str,
|
|
|
+ idempotency_key: str | None,
|
|
|
+ request_payload: Mapping[str, Any],
|
|
|
+ command: Callable[[], Awaitable[tuple[int, dict[str, Any]]]],
|
|
|
+ resource_id: int | None = None,
|
|
|
+ ) -> dict[str, Any]:
|
|
|
+ if idempotency_key and http_command_journal is not None:
|
|
|
+ _, payload = await http_command_journal.execute(
|
|
|
+ principal=actor,
|
|
|
+ route_family=route_family,
|
|
|
+ idempotency_key=idempotency_key,
|
|
|
+ request_payload=request_payload,
|
|
|
+ command=command,
|
|
|
+ resource_id=resource_id,
|
|
|
+ )
|
|
|
+ return cast(dict[str, Any], payload)
|
|
|
+ _, payload = await command()
|
|
|
+ return payload
|
|
|
+
|
|
|
@router.post("/api/pattern/script_builds", response_model=ScriptBuildStartResponse)
|
|
|
async def start_script_build(
|
|
|
body: ScriptBuildRequest,
|
|
|
actor: PrincipalDependency,
|
|
|
+ idempotency_key: Annotated[str | None, Header(alias="Idempotency-Key")] = None,
|
|
|
) -> ScriptBuildStartResponse:
|
|
|
if body.agent_type != "AigcAgent":
|
|
|
raise problem(400, "UNSUPPORTED_AGENT_TYPE", "script build supports AigcAgent only")
|
|
|
@@ -90,11 +122,84 @@ def create_script_build_router(
|
|
|
default_model_manifest=default_model_manifest,
|
|
|
default_datasource_manifest=default_datasource_manifest,
|
|
|
)
|
|
|
- result = await mission_service.start(command)
|
|
|
- return ScriptBuildStartResponse(
|
|
|
- script_build_id=result.script_build_id,
|
|
|
- status=result.status.value,
|
|
|
- root_trace_id=result.root_trace_id,
|
|
|
+ preallocated_root_trace_id = str(uuid4()) if idempotency_key else None
|
|
|
+ if preallocated_root_trace_id is not None:
|
|
|
+ command = replace(command, root_trace_id=preallocated_root_trace_id)
|
|
|
+
|
|
|
+ async def execute_with_root(root_trace_id: str | None) -> tuple[int, dict[str, Any]]:
|
|
|
+ result = await mission_service.start(
|
|
|
+ replace(command, root_trace_id=root_trace_id or command.root_trace_id)
|
|
|
+ )
|
|
|
+ return (
|
|
|
+ 200,
|
|
|
+ {
|
|
|
+ "script_build_id": result.script_build_id,
|
|
|
+ "status": result.status.value,
|
|
|
+ "root_trace_id": result.root_trace_id,
|
|
|
+ },
|
|
|
+ )
|
|
|
+
|
|
|
+ async def execute() -> tuple[int, dict[str, Any]]:
|
|
|
+ return await execute_with_root(preallocated_root_trace_id)
|
|
|
+
|
|
|
+ async def resume_reserved(
|
|
|
+ root_trace_id: str | None, _resource_id: int | None
|
|
|
+ ) -> tuple[int, dict[str, Any]]:
|
|
|
+ if root_trace_id is None:
|
|
|
+ raise problem(409, "IDEMPOTENCY_RECOVERY_REQUIRED", "reserved start has no Root")
|
|
|
+ try:
|
|
|
+ result = await mission_service.start_result_by_root(root_trace_id)
|
|
|
+ except BuildNotFound:
|
|
|
+ return await execute_with_root(root_trace_id)
|
|
|
+ return 200, {
|
|
|
+ "script_build_id": result.script_build_id,
|
|
|
+ "status": result.status.value,
|
|
|
+ "root_trace_id": result.root_trace_id,
|
|
|
+ }
|
|
|
+
|
|
|
+ if idempotency_key and http_command_journal is not None:
|
|
|
+ _, payload = await http_command_journal.execute(
|
|
|
+ principal=actor,
|
|
|
+ route_family="script-build:start",
|
|
|
+ idempotency_key=idempotency_key,
|
|
|
+ request_payload=body.model_dump(mode="json"),
|
|
|
+ command=execute,
|
|
|
+ resume_reserved=resume_reserved,
|
|
|
+ preallocated_root_trace_id=preallocated_root_trace_id,
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ _, payload = await execute()
|
|
|
+ return ScriptBuildStartResponse(**payload)
|
|
|
+
|
|
|
+ @router.get("/api/pattern/script_builds")
|
|
|
+ async def list_script_builds(
|
|
|
+ actor: PrincipalDependency,
|
|
|
+ page: int = Query(1, ge=1),
|
|
|
+ page_size: int = Query(20, ge=1, le=100),
|
|
|
+ status: str | None = None,
|
|
|
+ ) -> dict[str, Any]:
|
|
|
+ if legacy_api is None:
|
|
|
+ raise problem(503, "LEGACY_API_NOT_CONFIGURED", "legacy list is unavailable")
|
|
|
+ return cast(
|
|
|
+ dict[str, Any],
|
|
|
+ await legacy_api.list_authorized(actor, page=page, page_size=page_size, status=status),
|
|
|
+ )
|
|
|
+
|
|
|
+ @router.get("/api/pattern/script_builds/overview")
|
|
|
+ async def script_build_overview(actor: PrincipalDependency) -> dict[str, Any]:
|
|
|
+ if legacy_api is None:
|
|
|
+ raise problem(503, "LEGACY_API_NOT_CONFIGURED", "overview is unavailable")
|
|
|
+ return cast(dict[str, Any], await legacy_api.overview(actor))
|
|
|
+
|
|
|
+ @router.get("/api/pattern/script_builds/overview/{topic_build_id}")
|
|
|
+ async def script_build_topic_overview(
|
|
|
+ topic_build_id: int, actor: PrincipalDependency
|
|
|
+ ) -> dict[str, Any]:
|
|
|
+ if legacy_api is None:
|
|
|
+ raise problem(503, "LEGACY_API_NOT_CONFIGURED", "overview is unavailable")
|
|
|
+ return cast(
|
|
|
+ dict[str, Any],
|
|
|
+ await legacy_api.overview(actor, topic_build_id=topic_build_id),
|
|
|
)
|
|
|
|
|
|
@router.post("/api/pattern/script_builds/parse_topic_json")
|
|
|
@@ -132,6 +237,7 @@ def create_script_build_router(
|
|
|
async def start_from_topic_json(
|
|
|
body: ScriptBuildFromTopicJsonRequest,
|
|
|
actor: PrincipalDependency,
|
|
|
+ idempotency_key: Annotated[str | None, Header(alias="Idempotency-Key")] = None,
|
|
|
) -> dict[str, Any]:
|
|
|
if uploaded_topics is None:
|
|
|
raise problem(
|
|
|
@@ -143,68 +249,331 @@ def create_script_build_router(
|
|
|
topic_build_id=0,
|
|
|
topic_id=0,
|
|
|
)
|
|
|
- parsed = await uploaded_topics.parse(body.json_data)
|
|
|
- created = await uploaded_topics.create(parsed, account_name=body.account_name)
|
|
|
- request = ScriptBuildRequest(
|
|
|
- execution_id=0,
|
|
|
- topic_build_id=int(created["topic_build_id"]),
|
|
|
- topic_id=int(created["topic_id"]),
|
|
|
- agent_config=body.agent_config,
|
|
|
- data_source_url=body.data_source_url,
|
|
|
- strategies_always_on=body.strategies_always_on,
|
|
|
- strategies_on_demand=body.strategies_on_demand,
|
|
|
- )
|
|
|
- await security.authorize_source(
|
|
|
- actor,
|
|
|
- execution_id=request.execution_id,
|
|
|
- topic_build_id=request.topic_build_id,
|
|
|
- topic_id=request.topic_id,
|
|
|
- )
|
|
|
- result = await mission_service.start(
|
|
|
- await _start_command(
|
|
|
- request,
|
|
|
+
|
|
|
+ async def command() -> tuple[int, dict[str, Any]]:
|
|
|
+ parsed = await uploaded_topics.parse(body.json_data)
|
|
|
+ created = await uploaded_topics.create(parsed, account_name=body.account_name)
|
|
|
+ request = ScriptBuildRequest(
|
|
|
+ execution_id=0,
|
|
|
+ topic_build_id=int(created["topic_build_id"]),
|
|
|
+ topic_id=int(created["topic_id"]),
|
|
|
+ agent_config=body.agent_config,
|
|
|
+ data_source_url=body.data_source_url,
|
|
|
+ strategies_always_on=body.strategies_always_on,
|
|
|
+ strategies_on_demand=body.strategies_on_demand,
|
|
|
+ )
|
|
|
+ await security.authorize_source(
|
|
|
actor,
|
|
|
- outbound_policy,
|
|
|
- default_model_manifest=default_model_manifest,
|
|
|
- default_datasource_manifest=default_datasource_manifest,
|
|
|
+ execution_id=request.execution_id,
|
|
|
+ topic_build_id=request.topic_build_id,
|
|
|
+ topic_id=request.topic_id,
|
|
|
)
|
|
|
+ result = await mission_service.start(
|
|
|
+ await _start_command(
|
|
|
+ request,
|
|
|
+ actor,
|
|
|
+ outbound_policy,
|
|
|
+ default_model_manifest=default_model_manifest,
|
|
|
+ default_datasource_manifest=default_datasource_manifest,
|
|
|
+ )
|
|
|
+ )
|
|
|
+ return 200, {
|
|
|
+ "success": True,
|
|
|
+ "message": "脚本构建任务已提交",
|
|
|
+ "script_build_id": result.script_build_id,
|
|
|
+ "topic_id": created["topic_id"],
|
|
|
+ "topic_build_id": created["topic_build_id"],
|
|
|
+ "account_name": created.get("account_name"),
|
|
|
+ "item_count": created.get("item_count", 0),
|
|
|
+ "point_count": created.get("point_count", 0),
|
|
|
+ "status": result.status.value,
|
|
|
+ "root_trace_id": result.root_trace_id,
|
|
|
+ }
|
|
|
+
|
|
|
+ return await execute_mutation(
|
|
|
+ actor=actor,
|
|
|
+ route_family="script-build:from-topic-json",
|
|
|
+ idempotency_key=idempotency_key,
|
|
|
+ request_payload=body.model_dump(mode="json"),
|
|
|
+ command=command,
|
|
|
)
|
|
|
- return {
|
|
|
- "success": True,
|
|
|
- "message": "脚本构建任务已提交",
|
|
|
- "script_build_id": result.script_build_id,
|
|
|
- "topic_id": created["topic_id"],
|
|
|
- "topic_build_id": created["topic_build_id"],
|
|
|
- "account_name": created.get("account_name"),
|
|
|
- "item_count": created.get("item_count", 0),
|
|
|
- "point_count": created.get("point_count", 0),
|
|
|
- "status": result.status.value,
|
|
|
- "root_trace_id": result.root_trace_id,
|
|
|
- }
|
|
|
|
|
|
@router.post("/api/pattern/script_builds/{script_build_id}/stop")
|
|
|
async def stop_script_build(
|
|
|
script_build_id: int,
|
|
|
actor: PrincipalDependency,
|
|
|
+ idempotency_key: Annotated[str | None, Header(alias="Idempotency-Key")] = None,
|
|
|
) -> dict[str, Any]:
|
|
|
await security.authorize(script_build_id, actor)
|
|
|
- result = await mission_service.stop(script_build_id, actor)
|
|
|
- return {"success": True, **asdict(result), "status": result.status.value}
|
|
|
+
|
|
|
+ async def command() -> tuple[int, dict[str, Any]]:
|
|
|
+ result = await mission_service.stop(script_build_id, actor)
|
|
|
+ return 200, {"success": True, **asdict(result), "status": result.status.value}
|
|
|
+
|
|
|
+ return await execute_mutation(
|
|
|
+ actor=actor,
|
|
|
+ route_family="script-build:stop",
|
|
|
+ idempotency_key=idempotency_key,
|
|
|
+ request_payload={"script_build_id": script_build_id},
|
|
|
+ command=command,
|
|
|
+ resource_id=script_build_id,
|
|
|
+ )
|
|
|
+
|
|
|
+ @router.get("/api/pattern/script_builds/{script_build_id}/prefill")
|
|
|
+ async def script_build_prefill(
|
|
|
+ script_build_id: int, actor: PrincipalDependency
|
|
|
+ ) -> dict[str, Any]:
|
|
|
+ if legacy_api is None:
|
|
|
+ raise problem(503, "LEGACY_API_NOT_CONFIGURED", "prefill is unavailable")
|
|
|
+ await security.authorize(script_build_id, actor)
|
|
|
+ return cast(dict[str, Any], await legacy_api.prefill(script_build_id, actor))
|
|
|
+
|
|
|
+ @router.get("/api/pattern/script_builds/{script_build_id}/log")
|
|
|
+ async def script_build_log(script_build_id: int, actor: PrincipalDependency) -> dict[str, Any]:
|
|
|
+ if legacy_api is None:
|
|
|
+ raise problem(503, "LEGACY_API_NOT_CONFIGURED", "build log is unavailable")
|
|
|
+ await security.authorize(script_build_id, actor)
|
|
|
+ return cast(dict[str, Any], await legacy_api.log(script_build_id, actor))
|
|
|
+
|
|
|
+ @router.post("/api/pattern/script_builds/{script_build_id}/favorite")
|
|
|
+ @router.patch("/api/pattern/script_builds/{script_build_id}/favorite")
|
|
|
+ async def favorite_script_build(
|
|
|
+ script_build_id: int,
|
|
|
+ actor: PrincipalDependency,
|
|
|
+ is_favorited: bool = True,
|
|
|
+ idempotency_key: Annotated[str | None, Header(alias="Idempotency-Key")] = None,
|
|
|
+ ) -> dict[str, Any]:
|
|
|
+ if legacy_api is None:
|
|
|
+ raise problem(503, "LEGACY_API_NOT_CONFIGURED", "favorite is unavailable")
|
|
|
+ await security.authorize(script_build_id, actor)
|
|
|
+
|
|
|
+ async def command() -> tuple[int, dict[str, Any]]:
|
|
|
+ return 200, cast(
|
|
|
+ dict[str, Any],
|
|
|
+ await legacy_api.set_favorite(script_build_id, actor, value=is_favorited),
|
|
|
+ )
|
|
|
+
|
|
|
+ return await execute_mutation(
|
|
|
+ actor=actor,
|
|
|
+ route_family="script-build:favorite",
|
|
|
+ idempotency_key=idempotency_key,
|
|
|
+ request_payload={"script_build_id": script_build_id, "is_favorited": is_favorited},
|
|
|
+ command=command,
|
|
|
+ resource_id=script_build_id,
|
|
|
+ )
|
|
|
+
|
|
|
+ @router.delete("/api/pattern/script_builds/{script_build_id}")
|
|
|
+ async def delete_script_build(
|
|
|
+ script_build_id: int,
|
|
|
+ actor: PrincipalDependency,
|
|
|
+ idempotency_key: Annotated[str | None, Header(alias="Idempotency-Key")] = None,
|
|
|
+ ) -> dict[str, Any]:
|
|
|
+ if legacy_api is None:
|
|
|
+ raise problem(503, "LEGACY_API_NOT_CONFIGURED", "delete is unavailable")
|
|
|
+ await security.authorize(script_build_id, actor)
|
|
|
+
|
|
|
+ async def command() -> tuple[int, dict[str, Any]]:
|
|
|
+ return 200, cast(dict[str, Any], await legacy_api.delete(script_build_id, actor))
|
|
|
+
|
|
|
+ return await execute_mutation(
|
|
|
+ actor=actor,
|
|
|
+ route_family="script-build:delete",
|
|
|
+ idempotency_key=idempotency_key,
|
|
|
+ request_payload={"script_build_id": script_build_id},
|
|
|
+ command=command,
|
|
|
+ resource_id=script_build_id,
|
|
|
+ )
|
|
|
+
|
|
|
+ @router.post("/api/pattern/script_builds/{script_build_id}/retry")
|
|
|
+ async def retry_script_build(
|
|
|
+ script_build_id: int,
|
|
|
+ actor: PrincipalDependency,
|
|
|
+ idempotency_key: Annotated[str | None, Header(alias="Idempotency-Key")] = None,
|
|
|
+ ) -> dict[str, Any]:
|
|
|
+ if legacy_api is None:
|
|
|
+ raise problem(503, "LEGACY_API_NOT_CONFIGURED", "retry is unavailable")
|
|
|
+ await security.authorize(script_build_id, actor)
|
|
|
+ prefill = await legacy_api.prefill(script_build_id, actor)
|
|
|
+ strategies = prefill.get("strategies_config") or {}
|
|
|
+ request = ScriptBuildRequest(
|
|
|
+ execution_id=int(prefill["execution_id"]),
|
|
|
+ topic_build_id=int(prefill["topic_build_id"]),
|
|
|
+ topic_id=int(prefill["topic_id"]),
|
|
|
+ agent_type=str(prefill.get("agent_type") or "AigcAgent"),
|
|
|
+ agent_config=prefill.get("agent_config"),
|
|
|
+ data_source_url=prefill.get("data_source_url"),
|
|
|
+ strategies_always_on=list(strategies.get("always_on", [])),
|
|
|
+ strategies_on_demand=list(strategies.get("on_demand", [])),
|
|
|
+ )
|
|
|
+
|
|
|
+ async def command() -> tuple[int, dict[str, Any]]:
|
|
|
+ result = await mission_service.start(
|
|
|
+ await _start_command(
|
|
|
+ request,
|
|
|
+ actor,
|
|
|
+ outbound_policy,
|
|
|
+ default_model_manifest=default_model_manifest,
|
|
|
+ default_datasource_manifest=default_datasource_manifest,
|
|
|
+ )
|
|
|
+ )
|
|
|
+ return 200, {
|
|
|
+ "success": True,
|
|
|
+ "script_build_id": result.script_build_id,
|
|
|
+ "status": result.status.value,
|
|
|
+ "root_trace_id": result.root_trace_id,
|
|
|
+ "retried_from": script_build_id,
|
|
|
+ }
|
|
|
+
|
|
|
+ return await execute_mutation(
|
|
|
+ actor=actor,
|
|
|
+ route_family="script-build:retry",
|
|
|
+ idempotency_key=idempotency_key,
|
|
|
+ request_payload={"script_build_id": script_build_id},
|
|
|
+ command=command,
|
|
|
+ resource_id=script_build_id,
|
|
|
+ )
|
|
|
|
|
|
@router.post("/api/pattern/script_builds/{script_build_id}/phase-two/advance")
|
|
|
async def advance_to_phase_two(
|
|
|
script_build_id: int,
|
|
|
actor: PrincipalDependency,
|
|
|
+ idempotency_key: Annotated[str | None, Header(alias="Idempotency-Key")] = None,
|
|
|
) -> dict[str, Any]:
|
|
|
await security.authorize(script_build_id, actor)
|
|
|
- result = await mission_service.advance_to_phase_two(script_build_id, actor)
|
|
|
- return {
|
|
|
- "success": True,
|
|
|
- "script_build_id": result.script_build_id,
|
|
|
- "status": result.status.value,
|
|
|
- "root_trace_id": result.root_trace_id,
|
|
|
- "input_snapshot_id": result.input_snapshot_id,
|
|
|
- }
|
|
|
+
|
|
|
+ async def command() -> tuple[int, dict[str, Any]]:
|
|
|
+ result = await mission_service.advance_to_phase_two(script_build_id, actor)
|
|
|
+ return 200, {
|
|
|
+ "success": True,
|
|
|
+ "script_build_id": result.script_build_id,
|
|
|
+ "status": result.status.value,
|
|
|
+ "root_trace_id": result.root_trace_id,
|
|
|
+ "input_snapshot_id": result.input_snapshot_id,
|
|
|
+ }
|
|
|
+
|
|
|
+ return await execute_mutation(
|
|
|
+ actor=actor,
|
|
|
+ route_family="script-build:phase-two-advance",
|
|
|
+ idempotency_key=idempotency_key,
|
|
|
+ request_payload={"script_build_id": script_build_id},
|
|
|
+ command=command,
|
|
|
+ resource_id=script_build_id,
|
|
|
+ )
|
|
|
+
|
|
|
+ @router.post("/api/pattern/script_builds/{script_build_id}/phase-three/advance")
|
|
|
+ async def advance_to_phase_three(
|
|
|
+ script_build_id: int,
|
|
|
+ actor: PrincipalDependency,
|
|
|
+ idempotency_key: Annotated[str | None, Header(alias="Idempotency-Key")] = None,
|
|
|
+ ) -> dict[str, Any]:
|
|
|
+ if phase_three is None:
|
|
|
+ raise problem(503, "PHASE_THREE_NOT_CONFIGURED", "phase three is unavailable")
|
|
|
+ await security.authorize(script_build_id, actor)
|
|
|
+
|
|
|
+ async def command() -> tuple[int, dict[str, Any]]:
|
|
|
+ result = await phase_three.advance(script_build_id, actor)
|
|
|
+ return 200, {
|
|
|
+ "success": True,
|
|
|
+ "script_build_id": result.script_build_id,
|
|
|
+ "status": result.status.value,
|
|
|
+ "root_trace_id": result.root_trace_id,
|
|
|
+ "input_snapshot_id": result.input_snapshot_id,
|
|
|
+ }
|
|
|
+
|
|
|
+ return await execute_mutation(
|
|
|
+ actor=actor,
|
|
|
+ route_family="script-build:phase-three-advance",
|
|
|
+ idempotency_key=idempotency_key,
|
|
|
+ request_payload={"script_build_id": script_build_id},
|
|
|
+ command=command,
|
|
|
+ resource_id=script_build_id,
|
|
|
+ )
|
|
|
+
|
|
|
+ @router.post("/api/pattern/script_builds/{script_build_id}/resume")
|
|
|
+ async def resume_script_build(
|
|
|
+ script_build_id: int,
|
|
|
+ actor: PrincipalDependency,
|
|
|
+ idempotency_key: Annotated[str | None, Header(alias="Idempotency-Key")] = None,
|
|
|
+ ) -> dict[str, Any]:
|
|
|
+ if recovery is None:
|
|
|
+ raise problem(503, "RECOVERY_NOT_CONFIGURED", "recovery is unavailable")
|
|
|
+ await security.authorize(script_build_id, actor)
|
|
|
+
|
|
|
+ async def command() -> tuple[int, dict[str, Any]]:
|
|
|
+ result = await recovery.resume(script_build_id, actor)
|
|
|
+ return 200, {"success": True, **_json_values(asdict(result))}
|
|
|
+
|
|
|
+ return await execute_mutation(
|
|
|
+ actor=actor,
|
|
|
+ route_family="script-build:resume",
|
|
|
+ idempotency_key=idempotency_key,
|
|
|
+ request_payload={"script_build_id": script_build_id},
|
|
|
+ command=command,
|
|
|
+ resource_id=script_build_id,
|
|
|
+ )
|
|
|
+
|
|
|
+ @router.post("/api/pattern/script_builds/{script_build_id}/finalize")
|
|
|
+ async def finalize_script_build(
|
|
|
+ script_build_id: int,
|
|
|
+ actor: PrincipalDependency,
|
|
|
+ idempotency_key: Annotated[str | None, Header(alias="Idempotency-Key")] = None,
|
|
|
+ ) -> dict[str, Any]:
|
|
|
+ if finalization is None:
|
|
|
+ raise problem(503, "FINAL_PUBLISHER_NOT_CONFIGURED", "publisher is unavailable")
|
|
|
+ await security.authorize(script_build_id, actor)
|
|
|
+
|
|
|
+ async def command() -> tuple[int, dict[str, Any]]:
|
|
|
+ result = await finalization.finalize(script_build_id, actor)
|
|
|
+ return 200, {"success": True, **_json_values(asdict(result))}
|
|
|
+
|
|
|
+ return await execute_mutation(
|
|
|
+ actor=actor,
|
|
|
+ route_family="script-build:finalize",
|
|
|
+ idempotency_key=idempotency_key,
|
|
|
+ request_payload={"script_build_id": script_build_id},
|
|
|
+ command=command,
|
|
|
+ resource_id=script_build_id,
|
|
|
+ )
|
|
|
+
|
|
|
+ @router.post("/api/pattern/script_builds/{script_build_id}/reconcile")
|
|
|
+ async def reconcile_script_build(
|
|
|
+ script_build_id: int,
|
|
|
+ actor: PrincipalDependency,
|
|
|
+ retry_finalize: bool = False,
|
|
|
+ idempotency_key: Annotated[str | None, Header(alias="Idempotency-Key")] = None,
|
|
|
+ ) -> dict[str, Any]:
|
|
|
+ if recovery is None:
|
|
|
+ raise problem(503, "RECOVERY_NOT_CONFIGURED", "recovery is unavailable")
|
|
|
+ await security.authorize(script_build_id, actor)
|
|
|
+
|
|
|
+ async def command() -> tuple[int, dict[str, Any]]:
|
|
|
+ result = await recovery.reconcile(script_build_id, actor, retry_finalize=retry_finalize)
|
|
|
+ return 200, {"success": True, **_json_values(asdict(result))}
|
|
|
+
|
|
|
+ return await execute_mutation(
|
|
|
+ actor=actor,
|
|
|
+ route_family="script-build:reconcile",
|
|
|
+ idempotency_key=idempotency_key,
|
|
|
+ request_payload={
|
|
|
+ "script_build_id": script_build_id,
|
|
|
+ "retry_finalize": retry_finalize,
|
|
|
+ },
|
|
|
+ command=command,
|
|
|
+ resource_id=script_build_id,
|
|
|
+ )
|
|
|
+
|
|
|
+ @router.get("/api/pattern/script_builds/{script_build_id}")
|
|
|
+ async def legacy_detail(
|
|
|
+ script_build_id: int,
|
|
|
+ actor: PrincipalDependency,
|
|
|
+ ) -> dict[str, Any]:
|
|
|
+ if legacy_projection is None:
|
|
|
+ raise problem(503, "LEGACY_PROJECTION_NOT_CONFIGURED", "detail is unavailable")
|
|
|
+ await security.authorize(script_build_id, actor)
|
|
|
+ return cast(
|
|
|
+ dict[str, Any],
|
|
|
+ await legacy_projection.project_authorized(script_build_id, principal=actor),
|
|
|
+ )
|
|
|
|
|
|
@router.get("/api/pattern/script_builds/{script_build_id}/mission")
|
|
|
async def mission_snapshot(
|
|
|
@@ -260,11 +629,18 @@ def create_script_build_router(
|
|
|
async def publication_detail(
|
|
|
script_build_id: int,
|
|
|
actor: PrincipalDependency,
|
|
|
+ publication_type: str = Query("direction", alias="type"),
|
|
|
) -> dict[str, Any]:
|
|
|
await scope(script_build_id, actor)
|
|
|
- value = await publications.get_by_build(
|
|
|
- script_build_id, publication_type=PublicationType.DIRECTION
|
|
|
- )
|
|
|
+ try:
|
|
|
+ selected_type = PublicationType(publication_type)
|
|
|
+ except ValueError as exc:
|
|
|
+ raise problem(
|
|
|
+ 422,
|
|
|
+ "INVALID_PUBLICATION_TYPE",
|
|
|
+ "publication type must be direction or final",
|
|
|
+ ) from exc
|
|
|
+ value = await publications.get_by_build(script_build_id, publication_type=selected_type)
|
|
|
if value is None:
|
|
|
raise problem(404, "PUBLICATION_NOT_FOUND", "publication does not exist")
|
|
|
return _json_values(asdict(value))
|
|
|
@@ -347,6 +723,30 @@ def create_script_build_router(
|
|
|
messages = await trace_store.get_trace_messages(trace_id)
|
|
|
return {"messages": [item.to_dict() for item in messages]}
|
|
|
|
|
|
+ @router.get("/api/pattern/script_builds/{script_build_id}/trace_messages")
|
|
|
+ async def legacy_trace_messages(
|
|
|
+ script_build_id: int, actor: PrincipalDependency
|
|
|
+ ) -> dict[str, Any]:
|
|
|
+ if legacy_api is None:
|
|
|
+ raise problem(503, "LEGACY_API_NOT_CONFIGURED", "trace messages are unavailable")
|
|
|
+ await security.authorize(script_build_id, actor)
|
|
|
+ trace_id = await legacy_api.root_trace_id(script_build_id, actor)
|
|
|
+ messages = await trace_store.get_trace_messages(trace_id) if trace_id else []
|
|
|
+ return {
|
|
|
+ "success": True,
|
|
|
+ "script_build_id": script_build_id,
|
|
|
+ "trace_id": trace_id,
|
|
|
+ "messages": [item.to_dict() for item in messages],
|
|
|
+ }
|
|
|
+
|
|
|
+ @router.get("/script_build_detail/{script_build_id}", response_class=HTMLResponse)
|
|
|
+ async def legacy_detail_entry(script_build_id: int, actor: PrincipalDependency) -> HTMLResponse:
|
|
|
+ await security.authorize(script_build_id, actor)
|
|
|
+ return HTMLResponse(
|
|
|
+ '<!doctype html><html><body><div id="script-build-detail" '
|
|
|
+ f'data-script-build-id="{script_build_id}"></div></body></html>'
|
|
|
+ )
|
|
|
+
|
|
|
@router.websocket("/api/pattern/script_builds/{script_build_id}/traces/{trace_id}/watch")
|
|
|
async def watch_trace(
|
|
|
websocket: WebSocket,
|
|
|
@@ -365,9 +765,16 @@ def create_script_build_router(
|
|
|
since = 0
|
|
|
try:
|
|
|
while True:
|
|
|
- events = await trace_store.get_events(trace_id, since)
|
|
|
+ try:
|
|
|
+ actor = await security.principal(websocket)
|
|
|
+ binding = await scope(script_build_id, actor)
|
|
|
+ await _owned_trace(trace_store, binding.root_trace_id, trace_id)
|
|
|
+ except Exception:
|
|
|
+ await websocket.close(code=4404)
|
|
|
+ return
|
|
|
+ events = (await trace_store.get_events(trace_id, since))[:100]
|
|
|
for event in events:
|
|
|
- await websocket.send_json(event)
|
|
|
+ await asyncio.wait_for(websocket.send_json(event), timeout=2.0)
|
|
|
if isinstance(event.get("event_id"), int):
|
|
|
since = max(since, event["event_id"])
|
|
|
try:
|