"""Execution 子域 Router。 提供执行记录的查询 / 详情 / trace / 重试 / 清理 / 统计聚合 / 慢执行 / 重试链 / 执行上下文 / 按 trace 查询 API。挂载到 ``/system/external-systems/executions`` 前缀下(根前缀由聚合 router 追加)。 路径顺序约束:静态路径 ``/cleanup`` / ``/stats`` / ``/stats/grouped`` / ``/stats/error-analysis`` / ``/stats/trend`` / ``/slow`` / ``/by-trace/{trace_id}`` 必须在 ``/{execution_id}`` 前声明,避免被路径参数捕获。 设计依据:docs/vibe/v1.1/设计方案/2026-06-17-Router-API-设计方案.md 扩展依据:docs/vibe/v1.1/设计方案/RouterAPI扩展设计/10-execution_router扩展设计方案.md """ from __future__ import annotations from datetime import datetime from typing import Any, Literal from fastapi import APIRouter, Depends, Query from pydantic import BaseModel, ConfigDict, Field from sqlalchemy.ext.asyncio import AsyncSession from yuxi.external_systems.infrastructure.container import create_use_cases_from_db from yuxi.external_systems.use_cases.dto.execution import ( CleanupInput, GetExecutionContextInput, GetExecutionDetailInput, GetExecutionStatsInput, GetExecutionTraceInput, GetExecutionTrendInput, GetSlowExecutionsInput, GetTraceExecutionsInput, ListRetriesInput, PaginateExecutionsInput, RetryExecutionInput, ) from yuxi.storage.postgres.models_business import User from server.utils.auth_middleware import get_admin_user, get_db, get_required_user execution_router = APIRouter(prefix="/executions", tags=["external-systems-execution"]) # ============================================================================= # === Request Schemas === # ============================================================================= class CleanupExecutionsRequest(BaseModel): """清理历史执行记录请求体。字段对齐 ``CleanupInput``。""" model_config = ConfigDict(frozen=True) system_id: int | None = None before_at: datetime | None = None status: Literal["pending", "running", "success", "failed", "timeout", "throttled", "cancelled"] | None = None batch_size: int = Field(default=1000, ge=1, le=10000) # ============================================================================= # === 静态路径端点(必须在 /{execution_id} 之前声明) === # ============================================================================= @execution_router.post("/cleanup", response_model=dict) async def cleanup_old_executions( payload: CleanupExecutionsRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """清理历史执行记录(管理员)。""" use_cases = create_use_cases_from_db(db) input_dto = CleanupInput( system_id=payload.system_id, before_at=payload.before_at, status=payload.status, batch_size=payload.batch_size, ) output = await use_cases.execution_service.cleanup_old_executions(input_dto) return {"success": True, "data": output.model_dump()} @execution_router.get("", response_model=dict) async def paginate_executions( limit: int = Query(100, ge=1, le=500), offset: int = Query(0, ge=0), system_id: int | None = Query(None, ge=1), env_key: str | None = Query(None, max_length=32), tool_slug: str | None = Query(None, max_length=128), status: Literal["pending", "running", "success", "failed", "timeout", "throttled", "cancelled"] | None = Query( None, description="执行状态:pending/running/success/failed/timeout/throttled/cancelled" ), caller: str | None = Query(None, max_length=32), caller_id: str | None = Query(None, max_length=64, description="按调用方实体过滤"), operation: str | None = Query(None, max_length=32, description="按操作类型过滤"), tag_key: str | None = Query(None, max_length=64, description="按业务标签键过滤"), tag_value: str | None = Query(None, max_length=256, description="按业务标签值过滤"), trace_id: str | None = Query(None, max_length=64), correlation_id: str | None = Query(None, max_length=64), start_time: datetime | None = Query(None, description="ISO8601 开始时间"), end_time: datetime | None = Query(None, description="ISO8601 结束时间"), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """分页查询执行记录。""" use_cases = create_use_cases_from_db(db) input_dto = PaginateExecutionsInput( page=offset // limit + 1, page_size=limit, system_id=system_id, env_key=env_key, tool_slug=tool_slug, status=status, caller=caller, caller_id=caller_id, operation=operation, tag_key=tag_key, tag_value=tag_value, trace_id=trace_id, correlation_id=correlation_id, start_at=start_time, end_at=end_time, ) output = await use_cases.execution_service.paginate_executions(input_dto) return {"success": True, "data": output.model_dump()} # ============================================================================= # === 扩展:统计聚合 / 慢执行 / 按 trace 查询(静态路径,必须在 /{execution_id} 之前) === # ============================================================================= @execution_router.get("/stats", response_model=dict) async def get_execution_stats( system_id: int | None = Query(None, ge=1, description="系统过滤"), env_key: str | None = Query(None, max_length=32, description="环境过滤"), adapter_type: str | None = Query(None, max_length=32, description="适配器类型过滤"), start_time: datetime | None = Query(None, description="起始时间"), end_time: datetime | None = Query(None, description="结束时间"), tool_limit: int = Query(20, ge=1, le=100, description="Top 工具数量"), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """执行统计聚合。含按状态/工具分组。""" use_cases = create_use_cases_from_db(db) input_dto = GetExecutionStatsInput( system_id=system_id, env_key=env_key, adapter_type=adapter_type, start=start_time, end=end_time, limit=tool_limit, ) output = await use_cases.execution_service.get_execution_stats(input_dto) return {"success": True, "data": output.model_dump()} @execution_router.get("/stats/grouped", response_model=dict) async def get_grouped_stats( group_by: Literal["tool"] = Query(..., description="分组维度,仅支持 tool"), system_id: int | None = Query(None, ge=1, description="系统过滤"), env_key: str | None = Query(None, max_length=32, description="环境过滤"), adapter_type: str | None = Query(None, max_length=32, description="适配器类型过滤"), start_time: datetime | None = Query(None, description="起始时间"), end_time: datetime | None = Query(None, description="结束时间"), limit: int = Query(20, ge=1, le=100, description="返回数量"), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """分组统计。仅支持 group_by=tool(仓储仅 aggregate_by_tool 支持分组)。""" use_cases = create_use_cases_from_db(db) input_dto = GetExecutionStatsInput( system_id=system_id, env_key=env_key, adapter_type=adapter_type, start=start_time, end=end_time, limit=limit, ) output = await use_cases.execution_service.get_grouped_stats(input_dto) return {"success": True, "data": output.model_dump()} @execution_router.get("/stats/error-analysis", response_model=dict) async def get_error_analysis( system_id: int = Query(..., ge=1, description="系统 ID(必填)"), start_time: datetime | None = Query(None, description="起始时间"), end_time: datetime | None = Query(None, description="结束时间"), limit: int = Query(5, ge=1, le=50, description="返回数量"), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """错误分析。按错误码分组统计失败执行数量。""" use_cases = create_use_cases_from_db(db) input_dto = GetExecutionStatsInput( system_id=system_id, start=start_time, end=end_time, limit=limit, ) output = await use_cases.execution_service.get_error_analysis(input_dto) return {"success": True, "data": output.model_dump()} @execution_router.get("/stats/trend", response_model=dict) async def get_execution_trend( tool_slug: str = Query(..., min_length=1, max_length=128, description="工具标识(必填)"), system_id: int | None = Query(None, ge=1, description="系统过滤"), env_key: str | None = Query(None, max_length=32, description="环境过滤"), start_time: datetime | None = Query(None, description="起始时间"), end_time: datetime | None = Query(None, description="结束时间"), interval: Literal["hour", "day"] = Query("day", description="时间桶粒度:hour / day"), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """执行趋势。按时间桶 + 工具聚合。""" use_cases = create_use_cases_from_db(db) input_dto = GetExecutionTrendInput( tool_slug=tool_slug, system_id=system_id, env_key=env_key, start=start_time, end=end_time, interval=interval, ) output = await use_cases.execution_service.get_execution_trend(input_dto) return {"success": True, "data": output.model_dump()} @execution_router.get("/slow", response_model=dict) async def list_slow_executions( system_id: int = Query(..., ge=1, description="系统 ID(必填)"), min_duration_ms: int = Query(..., ge=0, description="耗时阈值(毫秒,必填)"), limit: int = Query(50, ge=1, le=200, description="返回数量"), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """慢执行查询。按耗时降序。""" use_cases = create_use_cases_from_db(db) input_dto = GetSlowExecutionsInput( system_id=system_id, min_duration_ms=min_duration_ms, limit=limit, ) output = await use_cases.execution_service.list_slow_executions(input_dto) return {"success": True, "data": output.model_dump()} @execution_router.get("/by-trace/{trace_id}", response_model=dict) async def list_by_trace( trace_id: str, limit: int = Query(50, ge=1, le=200, description="返回数量"), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """按 trace 查询。返回 execution + trace 组合。""" use_cases = create_use_cases_from_db(db) input_dto = GetTraceExecutionsInput( trace_id=trace_id, limit=limit, ) output = await use_cases.execution_service.list_by_trace(input_dto) return {"success": True, "data": output.model_dump()} # ============================================================================= # === 动态路径端点 /{execution_id} === # ============================================================================= @execution_router.get("/{execution_id}", response_model=dict) async def get_execution_detail( execution_id: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """获取执行详情。""" use_cases = create_use_cases_from_db(db) input_dto = GetExecutionDetailInput(execution_id=execution_id) output = await use_cases.execution_service.get_execution_detail(input_dto) return {"success": True, "data": output.model_dump()} @execution_router.get("/{execution_id}/trace", response_model=dict) async def get_execution_trace( execution_id: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """获取执行 trace。""" use_cases = create_use_cases_from_db(db) input_dto = GetExecutionTraceInput(execution_id=execution_id) output = await use_cases.execution_service.get_execution_trace(input_dto) return {"success": True, "data": output.model_dump()} @execution_router.get("/{execution_id}/retries", response_model=dict) async def list_retries( execution_id: str, limit: int = Query(50, ge=1, le=200, description="返回数量"), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """重试链查询。基于 retry_of 字段,按 started_at 升序。""" use_cases = create_use_cases_from_db(db) input_dto = ListRetriesInput( execution_id=execution_id, limit=limit, ) output = await use_cases.execution_service.list_retries(input_dto) return {"success": True, "data": output.model_dump()} @execution_router.get("/{execution_id}/context", response_model=dict) async def get_execution_context( execution_id: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """执行上下文。含 trace + 关联告警。""" use_cases = create_use_cases_from_db(db) input_dto = GetExecutionContextInput(execution_id=execution_id) output = await use_cases.execution_service.get_execution_context(input_dto) return {"success": True, "data": output.model_dump()} @execution_router.post("/{execution_id}/retry", response_model=dict) async def retry_execution( execution_id: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """重试执行(管理员)。 ``caller`` / ``caller_id`` 均使用当前管理员 uid,用于审计日志。 执行记录的 ``caller`` 由 service 层固定为 ``retry``,确保重试语义准确。 """ use_cases = create_use_cases_from_db(db) input_dto = RetryExecutionInput( execution_id=execution_id, caller=current_user.uid, caller_id=current_user.uid, ) output = await use_cases.execution_service.retry_execution(input_dto) return {"success": True, "data": output.model_dump()}