"""Adapter 子域 Router。 外部系统限界上下文的适配器元数据查询 API。元数据查询端点直接读取 ``default_registry`` 全局单例;运行时统计端点通过 ``create_adapter_stats_service_from_db`` 轻量工厂注入 ``AdapterStatsService``(仅需 Repositories,无需构造完整 24-service UseCases 容器),调用 ``adapter_stats_service`` 聚合系统/工具/资产/指标/健康数据。 设计依据:docs/vibe/v1.1/设计方案/RouterAPI扩展设计/03-adapter_router扩展设计方案.md """ from __future__ import annotations from types import SimpleNamespace from typing import Any from fastapi import APIRouter, Depends, Query from pydantic import BaseModel, ValidationError from yuxi.external_systems.exceptions import ( CapabilityNotSupportedError, DomainValidationError, ) from yuxi.external_systems.framework.auth_plugins import ( get_auth_json_schema, get_auth_json_schemas, is_token_based_auth_type, list_auth_plugins, ) from yuxi.external_systems.framework.auth_plugins.protocol_registry import ( list_protocol_auth_plugins, ) from yuxi.external_systems.framework.registry.adapter_registry import default_registry from yuxi.external_systems.use_cases.dto.adapter_stats import ( GetAdapterHealthInput, GetAdapterStatsInput, ) from yuxi.external_systems.use_cases.ports import AdapterStatsServicePort from yuxi.storage.postgres.models_business import User from server.routers.external_systems import get_adapter_stats_service, parse_and_naive, validate_time_range from server.utils.auth_middleware import get_admin_user, get_required_user # ============================================================================= # === Request Schemas === # ============================================================================= class ValidateConfigRequest(BaseModel): """适配器配置校验请求体。""" config: dict[str, Any] adapter_router = APIRouter( prefix="/adapters", tags=["external-systems-adapter"], ) # ============================================================================= # === 静态路径端点(必须在 /{adapter_type} 之前声明) === # ============================================================================= @adapter_router.get("", response_model=dict) async def list_adapters( current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """列出所有已注册的适配器类型(按字母序排序,保证 API 响应稳定性)。""" adapters = sorted(default_registry.list_adapter_types()) return {"success": True, "data": {"items": adapters, "total": len(adapters)}} @adapter_router.get("/overview", response_model=dict) async def get_adapters_overview( current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """查询所有已注册适配器的元数据总览(按 adapter_type 排序)。 返回每个适配器的展示信息、能力、认证类型、数据源类型与 category_prefix, 不包含 config_schema / connection_schema / auth_schemas(避免响应过重)。 适合前端适配器市场页一次性渲染卡片。 """ metadata_list = sorted( default_registry.list_adapter_metadata(), key=lambda m: m.adapter_type, ) items = [ { "adapter_type": metadata.adapter_type, "display_name": metadata.display_name, "description": metadata.description, "capabilities": metadata.to_capabilities(), "supported_auth_types": metadata.supported_auth_types, "supported_source_types": metadata.supported_source_types, "category_prefix": metadata.category_prefix, } for metadata in metadata_list ] return {"success": True, "data": {"items": items, "total": len(items)}} @adapter_router.get("/health-batch", response_model=dict) async def get_adapters_health_batch( current_user: User = Depends(get_required_user), stats_service: AdapterStatsServicePort = Depends(get_adapter_stats_service), ) -> dict[str, Any]: """批量查询所有已注册适配器的健康状态。 遍历 ``default_registry`` 中所有适配器,通过 ``AdapterStatsService`` 逐个聚合健康状态,返回 ``{ adapter_type: health_data }`` 列表。 避免前端逐个发起 ``GET /{adapter_type}/health`` 请求(N+1 HTTP 问题)。 """ metadata_list = sorted( default_registry.list_adapter_metadata(), key=lambda m: m.adapter_type, ) items = [] for metadata in metadata_list: input_dto = GetAdapterHealthInput(adapter_type=metadata.adapter_type) output = await stats_service.get_adapter_health(input_dto) items.append(output.model_dump()) return {"success": True, "data": {"items": items, "total": len(items)}} # ============================================================================= # === 路径参数端点(/{adapter_type}) === # ============================================================================= @adapter_router.get("/{adapter_type}", response_model=dict) async def get_adapter( adapter_type: str, current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """获取适配器详情,包含展示信息、配置/连接 Schema、能力声明、资产类型与认证 Schema。 作为 overview 的超集,额外返回 config_schema / connection_schema / asset_types / auth_schemas,并补全 category_prefix 与 supported_auth_types, 确保 detail 端点字段覆盖度 >= overview 端点。 ``AdapterMetadata.config_schema`` / ``connection_schema`` 是 Pydantic 模型类 (非实例),无法直接序列化,故通过 ``to_json_schema()`` / ``to_connection_json_schema()`` 单独提供其 JSON Schema。``AdapterNotFoundError`` 由全局处理器映射为 404。 """ metadata = default_registry.get_adapter_metadata(adapter_type) data = { "adapter_type": metadata.adapter_type, "display_name": metadata.display_name, "description": metadata.description, "category_prefix": metadata.category_prefix, "config_schema": metadata.to_json_schema(), "connection_schema": metadata.to_connection_json_schema(), "capabilities": metadata.to_capabilities(), "asset_types": metadata.to_asset_types(), "supported_auth_types": metadata.supported_auth_types, "auth_schemas": get_auth_json_schemas( metadata.supported_auth_types, adapter_type=metadata.adapter_type, ), "supported_source_types": metadata.supported_source_types, } return {"success": True, "data": data} @adapter_router.get("/{adapter_type}/config-schema", response_model=dict) async def get_adapter_config_schema( adapter_type: str, current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """获取适配器配置与连接配置的 JSON Schema,用于前端动态表单渲染。 与详情端点的差异:详情端点的 config_schema / connection_schema 与其他元数据混合, 本端点仅返回 Schema,适合前端表单渲染时独立获取。 ``AdapterNotFoundError`` 由全局处理器映射为 404。 """ metadata = default_registry.get_adapter_metadata(adapter_type) data = { "config_schema": metadata.to_json_schema(), "connection_schema": metadata.to_connection_json_schema(), } return {"success": True, "data": data} @adapter_router.get("/{adapter_type}/auth-plugins", response_model=dict) async def get_adapter_auth_plugins( adapter_type: str, current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """查询适配器支持的认证插件清单(含框架级 + 协议专属)。 过滤逻辑:``list_auth_plugins`` 返回全部框架级插件 + 协议专属插件, 但适配器仅声明支持 ``supported_auth_types`` 中的框架级认证类型。此处 按 ``metadata.supported_auth_types`` 过滤框架级插件,同时保留全部协议 专属插件(协议专属插件由适配器层自注册,天然属于该适配器)。 Schema 获取:``get_auth_json_schema`` 对 ``credential_type=None`` 的插件 抛 ``DomainValidationError``,此处捕获并返回 ``None``,避免单个无凭证类型 的插件导致整个端点 500。 与详情端点的差异:详情端点通过 ``get_auth_json_schemas()`` 仅返回 ``{auth_type: schema}`` 字典,本端点额外返回 display_name 与 is_token_based, 适合前端认证方式选择下拉框渲染。 ``AdapterNotFoundError`` 由全局处理器映射为 404。 """ metadata = default_registry.get_adapter_metadata(adapter_type) # 协议专属插件类型集合(天然属于该适配器,无需过滤) protocol_types = {p["type"] for p in list_protocol_auth_plugins(adapter_type)} # 框架级插件按 supported_auth_types 过滤 supported_set = set(metadata.supported_auth_types) all_plugins = list_auth_plugins(adapter_type=adapter_type) filtered_plugins = [p for p in all_plugins if p["type"] in supported_set or p["type"] in protocol_types] items = [] for plugin in filtered_plugins: try: schema = get_auth_json_schema(plugin["type"], adapter_type=adapter_type) except DomainValidationError: # 插件 credential_type=None(如纯协议标记插件),无 JSON Schema schema = None items.append( { "type": plugin["type"], "display_name": plugin["display_name"], "schema": schema, "is_token_based": is_token_based_auth_type( plugin["type"], adapter_type=adapter_type, ), } ) return {"success": True, "data": {"items": items, "total": len(items)}} @adapter_router.get("/{adapter_type}/source-types", response_model=dict) async def get_adapter_source_types( adapter_type: str, current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """查询适配器支持的数据源类型(如 http 适配器的 openapi / postman / curl)。 响应字段名 ``supported_source_types`` 与 ``AdapterMetadata`` 字段名及 overview / detail 端点保持一致,避免命名碎片化。 ``AdapterNotFoundError`` 由全局处理器映射为 404。 """ metadata = default_registry.get_adapter_metadata(adapter_type) data = { "adapter_type": metadata.adapter_type, "supported_source_types": metadata.supported_source_types, } return {"success": True, "data": data} @adapter_router.get("/{adapter_type}/assets", response_model=dict) async def get_adapter_assets( adapter_type: str, current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """获取适配器资产信息,包含资产类型列表、示例配置与资产生成能力。 ``asset_type_descriptions`` / ``example_config`` 为 None 时默认返回空值 (``{}``),避免前端需额外处理 None 分支。 ``AdapterNotFoundError`` 由全局处理器映射为 404。 """ metadata = default_registry.get_adapter_metadata(adapter_type) data = { "adapter_type": metadata.adapter_type, "asset_types": metadata.to_asset_types(), "asset_type_descriptions": metadata.asset_type_descriptions or {}, "example_config": metadata.example_config or {}, "supports_asset_generation": metadata.supports_asset_generation, } return {"success": True, "data": data} @adapter_router.post("/{adapter_type}/validate-config", response_model=dict) async def validate_adapter_config( adapter_type: str, body: ValidateConfigRequest, current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """校验适配器配置是否符合 Schema(不实际执行,仅校验)。 调用适配器实例的 ``validate_config`` 可选能力。适配器未实现该能力时 抛 Python 内置 ``NotImplementedError``,Router 内翻译为 ``CapabilityNotSupportedError`` 交由 ``unified_error_handler`` 统一映射 为 HTTP 501,避免原生异常被兜底为 500。 ``SimpleNamespace`` 构造说明:``validate_config`` 签名仅接受 ``ExecutableTool``,其中 ``adapter_config`` 是校验目标(来自请求体), 其余字段(slug / name / auth_type 等)对配置校验无语义影响,使用 合理默认值填充以满足 Protocol 结构契约。 ``AdapterNotFoundError`` 由全局处理器映射为 404。 """ adapter = default_registry.get_adapter(adapter_type) # 构造 ExecutableTool 兼容对象(SimpleNamespace 严格对齐 Protocol 的 12 个字段) # validate_config 仅消费 tool.adapter_config,其余字段用安全默认值填充 tool = SimpleNamespace( slug="", name="", description="", adapter_type=adapter_type, auth_type="none", adapter_config=body.config, auth_config={}, timeout=30, retry_policy={}, enabled=True, system_id=None, id=None, ) try: await adapter.validate_config(tool) # type: ignore[attr-defined] except NotImplementedError as exc: # 适配器未实现配置校验能力,翻译为 CapabilityNotSupportedError, # 交由 unified_error_handler 统一映射为 HTTP 501,保留原始 traceback。 raise CapabilityNotSupportedError( capability="validate_config", adapter_type=adapter_type, ) from exc except ValidationError as exc: # Pydantic 校验失败,返回 200 + valid=False(业务校验结果,非请求体格式错误) # errors 格式化为 list[str],与 auth_plugin_router / system_router 校验端点对齐 return { "success": True, "data": { "valid": False, "errors": [f"{'.'.join(str(p) for p in e['loc'])}: {e['msg']}" for e in exc.errors()], }, } return {"success": True, "data": {"valid": True, "errors": []}} # ============================================================================= # === 运行时统计端点(轻量工厂注入 adapter_stats_service) === # ============================================================================= @adapter_router.get("/{adapter_type}/stats", response_model=dict) async def get_adapter_stats( adapter_type: str, start_time: str | None = Query(None, description="指标聚合起始时间(ISO 8601)"), end_time: str | None = Query(None, description="指标聚合结束时间(ISO 8601)"), current_user: User = Depends(get_required_user), stats_service: AdapterStatsServicePort = Depends(get_adapter_stats_service), ) -> dict[str, Any]: """查询适配器维度的使用统计(系统数 / 工具数 / 资产数 / 指标聚合)。 先通过 ``get_adapter_metadata`` 校验适配器存在(抛 ``AdapterNotFoundError`` → 404), 再通过轻量工厂 ``create_adapter_stats_service_from_db`` 创建 ``AdapterStatsService``(仅注入 Repositories,不构造完整 UseCases 容器)。 ``start_time`` / ``end_time`` 仅作用于指标聚合,不影响系统/工具/资产计数。 """ # 校验适配器存在(抛 AdapterNotFoundError → 404) default_registry.get_adapter_metadata(adapter_type) start_dt = parse_and_naive("start_time", start_time) end_dt = parse_and_naive("end_time", end_time) validate_time_range(start_dt, end_dt) input_dto = GetAdapterStatsInput( adapter_type=adapter_type, start=start_dt, end=end_dt, ) output = await stats_service.get_adapter_stats(input_dto) return {"success": True, "data": output.model_dump()} @adapter_router.get("/{adapter_type}/health", response_model=dict) async def get_adapter_health( adapter_type: str, current_user: User = Depends(get_required_user), stats_service: AdapterStatsServicePort = Depends(get_adapter_stats_service), ) -> dict[str, Any]: """查询适配器维度的健康状态(基于关联系统最近一次健康检查聚合)。 先通过 ``get_adapter_metadata`` 校验适配器存在(抛 ``AdapterNotFoundError`` → 404), 再通过轻量工厂 ``create_adapter_stats_service_from_db`` 创建 ``AdapterStatsService``(仅注入 Repositories,不构造完整 UseCases 容器)。 ``status`` 取值:unknown / healthy / degraded / unhealthy(见 DTO docstring)。 """ # 校验适配器存在(抛 AdapterNotFoundError → 404) default_registry.get_adapter_metadata(adapter_type) input_dto = GetAdapterHealthInput(adapter_type=adapter_type) output = await stats_service.get_adapter_health(input_dto) return {"success": True, "data": output.model_dump()}