ForcePilot/backend/server/routers/external_systems/adapter_router.py

395 lines
17 KiB
Python
Raw Normal View History

"""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()}