"""外部系统限界上下文 Router 聚合层与共享装配层。 本模块承担两个职责: 1. **共享装配层**:为各子 Router 提供请求级独立 session 的 UseCases / 轻量 service 装配依赖(``get_use_cases`` / ``get_dashboard_service`` / ``get_adapter_stats_service``)。子 Router 通过 ``Depends(get_use_cases)`` 注入已装配的 ``UseCases``,不再各自调用 ``create_use_cases_from_db(db)``。 2. **Router 聚合层**:将 23 个子 router 通过 include_router 注册,统一挂载到 /system/external-systems 前缀下。子 router 只需声明相对前缀(如 /systems、 /tools),根前缀 /system/external-systems 由本聚合 router 统一追加。 独立 session 设计说明(参照 channels 模块): external_systems 模块 **不复用** 鉴权依赖(``get_db``)持有的 ``AsyncSession``, 而是通过 ``pg_manager.get_async_session_context()`` 开启独立的请求级会话。原因: 鉴权阶段执行的 ``SELECT`` 会触发 SQLAlchemy 2.0 autobegin 语义开启隐式事务, 若复用同一 session,控制面 ``TransactionPort.begin()`` 会在已存在的事务上再次 ``begin()``,且异常路径下底层 asyncpg 连接会残留 "manually started transaction", 连接归还连接池后被下个请求复用时触发 ``cannot use Connection.transaction() in a manually started transaction``。 独立 session 使事务边界由 external_systems 应用层独占控制,与鉴权会话隔离, 避免事务状态互相污染。会话生命周期限制在本次请求内,请求结束自动 commit(无异常)或 rollback(有异常)后关闭。 """ from __future__ import annotations from datetime import datetime from fastapi import APIRouter from fastapi.exceptions import RequestValidationError from yuxi.external_systems.infrastructure.container import ( UseCases, create_adapter_stats_service_from_db, create_dashboard_service_from_db, create_use_cases_from_db, ) from yuxi.external_systems.use_cases.ports import ( AdapterStatsServicePort, DashboardServicePort, ) from yuxi.storage.postgres.manager import pg_manager from yuxi.utils.datetime_utils import coerce_any_to_utc_datetime, to_naive_utc async def get_use_cases() -> UseCases: """装配 external_systems UseCases 集合(请求级独立 session)。 通过 ``pg_manager.get_async_session_context()`` 开启独立请求级 session, 调用 ``create_use_cases_from_db(db)`` 装配完整 24-service ``UseCases`` 容器, yield 完成后自动关闭 session。不复用 ``get_db`` session 的原因见模块 docstring。 Yields: 已装配的 ``UseCases`` 实例。 """ async with pg_manager.get_async_session_context() as db: yield create_use_cases_from_db(db) async def get_dashboard_service() -> DashboardServicePort: """装配轻量级 DashboardService(请求级独立 session)。 Dashboard 是纯聚合查询,无需完整 UseCases 容器。使用 ``create_dashboard_service_from_db(db)`` 轻量工厂仅构造 ``DashboardService``(仅注入 Repositories),降低延迟与资源消耗。 不复用 ``get_db`` session 的原因见模块 docstring。 Yields: 已装配的 ``DashboardServicePort`` 实例。 """ async with pg_manager.get_async_session_context() as db: yield create_dashboard_service_from_db(db) async def get_adapter_stats_service() -> AdapterStatsServicePort: """装配轻量级 AdapterStatsService(请求级独立 session)。 AdapterStatsService 仅依赖 Repositories(纯查询用例,无副作用),无需 完整 UseCases 容器。使用 ``create_adapter_stats_service_from_db(db)`` 轻量工厂避免每次统计/健康查询都构造完整 24-service 容器。不复用 ``get_db`` session 的原因见模块 docstring。 Yields: 已装配的 ``AdapterStatsServicePort`` 实例。 """ async with pg_manager.get_async_session_context() as db: yield create_adapter_stats_service_from_db(db) # ------------------------------------------------------------------ # 统一时间参数处理(对齐 channels 模块 parse_datetime 模式) # # 所有子 Router 的时间查询参数统一使用 str 类型(ISO 8601), # 通过 parse_datetime 翻译为 aware UTC datetime,再用 to_naive_utc # 剥离为 naive UTC 以适配 TIMESTAMP WITHOUT TIME ZONE 列。 # ------------------------------------------------------------------ def parse_datetime(field: str, raw: str | None) -> datetime | None: """ISO 8601 string → aware UTC datetime,失败抛 RequestValidationError (422)。 对齐 channels 模块的 parse_datetime 模式:naive 字符串视为 Asia/Shanghai, aware 字符串统一转 UTC。raw 为 None 时返回 None。 """ if raw is None: return None try: return coerce_any_to_utc_datetime(raw) except (ValueError, TypeError) as exc: raise RequestValidationError( errors=[ { "loc": ("query", field), "msg": f"invalid {field} format: {raw} (expected ISO 8601)", "type": "value_error", "input": raw, } ] ) from exc def validate_time_range(start: datetime | None, end: datetime | None) -> None: """校验时间范围语义:start 不晚于 end。失败抛 RequestValidationError (422)。""" if start is not None and end is not None and start > end: raise RequestValidationError( errors=[ { "loc": ("query", "start_time"), "msg": "start_time 不能晚于 end_time", "type": "value_error", "input": {"start_time": start, "end_time": end}, } ] ) def parse_and_naive(field: str, raw: str | None) -> datetime | None: """一步完成 parse_datetime + to_naive_utc,返回 naive UTC datetime 或 None。""" return to_naive_utc(parse_datetime(field, raw)) external_systems_router = APIRouter( prefix="/system/external-systems", tags=["external-systems"], ) # 子 router 导入置于共享依赖定义之后,避免循环导入 # (子 router 通过 ``from server.routers.external_systems import get_use_cases`` # 反向引用本模块依赖,须保证此处已定义完成) from server.routers.external_systems.access_rule_router import access_rule_router # noqa: E402 from server.routers.external_systems.adapter_router import adapter_router # noqa: E402 from server.routers.external_systems.alert_router import alert_router # noqa: E402 from server.routers.external_systems.asset_router import asset_router # noqa: E402 from server.routers.external_systems.audit_log_router import audit_log_router # noqa: E402 from server.routers.external_systems.auth_plugin_router import auth_plugin_router # noqa: E402 from server.routers.external_systems.dashboard_router import dashboard_router # noqa: E402 from server.routers.external_systems.environment_router import environment_router # noqa: E402 from server.routers.external_systems.execution_router import execution_router # noqa: E402 from server.routers.external_systems.health_check_router import health_check_router # noqa: E402 from server.routers.external_systems.import_router import import_router # noqa: E402 from server.routers.external_systems.integration_router import integration_router # noqa: E402 from server.routers.external_systems.integration_tool_router import integration_tool_router # noqa: E402 from server.routers.external_systems.metric_router import metric_router # noqa: E402 from server.routers.external_systems.notification_channel_router import notification_channel_router # noqa: E402 from server.routers.external_systems.quota_router import quota_router # noqa: E402 from server.routers.external_systems.secret_rotation_router import secret_rotation_router # noqa: E402 from server.routers.external_systems.system_router import system_router # noqa: E402 from server.routers.external_systems.test_case_router import test_case_router # noqa: E402 from server.routers.external_systems.token_router import token_router # noqa: E402 from server.routers.external_systems.tool_router import tool_router # noqa: E402 from server.routers.external_systems.trash_router import trash_router # noqa: E402 from server.routers.external_systems.webhook_router import webhook_router # noqa: E402 # 注册 23 个子 router external_systems_router.include_router(system_router) external_systems_router.include_router(tool_router) external_systems_router.include_router(environment_router) external_systems_router.include_router(execution_router) external_systems_router.include_router(token_router) external_systems_router.include_router(audit_log_router) external_systems_router.include_router(asset_router) external_systems_router.include_router(adapter_router) external_systems_router.include_router(import_router) external_systems_router.include_router(integration_router) external_systems_router.include_router(integration_tool_router) external_systems_router.include_router(webhook_router) external_systems_router.include_router(health_check_router) external_systems_router.include_router(metric_router) external_systems_router.include_router(quota_router) external_systems_router.include_router(alert_router) external_systems_router.include_router(secret_rotation_router) external_systems_router.include_router(access_rule_router) external_systems_router.include_router(test_case_router) external_systems_router.include_router(dashboard_router) external_systems_router.include_router(auth_plugin_router) external_systems_router.include_router(notification_channel_router) external_systems_router.include_router(trash_router)