ForcePilot/backend/server/routers/external_systems/adapter_router.py
Kris ddafd95ff0 refactor(routers): 整理并新增多组渠道相关路由功能
1.  移除多个导出接口的显式response_model声明
2.  调整access_rule和test_case的创建接口位置,修复静态路径冲突
3.  优化适配器配置校验的异常处理逻辑
4.  重构集成路由的查询逻辑,统一使用get_integration_or_raise
5.  新增channels路由组下的capability、reports、dashboard、webhook、wizard、doctor、directory、session共8个子路由模块
6.  注册channels_router到全局路由列表
2026-07-02 03:29:06 +08:00

331 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Adapter 子域 Router。
外部系统限界上下文的适配器元数据查询 API。元数据查询端点直接读取
``default_registry`` 全局单例;运行时统计端点通过 ``create_use_cases_from_db``
注入 use_cases调用 ``adapter_stats_service`` 聚合系统/工具/资产/指标/健康数据。
设计依据docs/vibe/v1.1/设计方案/RouterAPI扩展设计/03-adapter_router扩展设计方案.md
"""
from __future__ import annotations
from datetime import datetime
from types import SimpleNamespace
from typing import Any
from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel, ValidationError
from sqlalchemy.ext.asyncio import AsyncSession
from yuxi.external_systems.exceptions import CapabilityNotSupportedError
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.registry.adapter_registry import default_registry
from yuxi.external_systems.infrastructure.container import create_use_cases_from_db
from yuxi.external_systems.use_cases.dto.adapter_stats import (
GetAdapterHealthInput,
GetAdapterStatsInput,
)
from yuxi.storage.postgres.models_business import User
from server.utils.auth_middleware import get_admin_user, get_db, 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]:
"""列出所有已注册的适配器类型。"""
adapters = 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]:
"""查询所有已注册适配器的元数据总览。
返回每个适配器的展示信息、能力、认证类型、数据源类型与 category_prefix
不包含 config_schema / connection_schema / auth_schemas避免响应过重
适合前端适配器市场页一次性渲染卡片。
"""
metadata_list = default_registry.list_adapter_metadata()
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_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。
``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,
"config_schema": metadata.to_json_schema(),
"connection_schema": metadata.to_connection_json_schema(),
"capabilities": metadata.to_capabilities(),
"asset_types": metadata.to_asset_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}/capabilities", response_model=dict)
async def get_adapter_capabilities(
adapter_type: str,
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""查询适配器支持的能力(轻量端点,仅返回 5 个能力字段)。
与详情端点的差异:详情端点返回 9 个字段(含 Schema 与资产类型),
本端点仅返回 ``to_capabilities()`` 的 5 个能力字段,响应体减小约 80%
``AdapterNotFoundError`` 由全局处理器映射为 404。
"""
metadata = default_registry.get_adapter_metadata(adapter_type)
return {"success": True, "data": metadata.to_capabilities()}
@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`` + ``get_auth_json_schema`` + ``is_token_based_auth_type``
返回每个插件的 type / display_name / schema / is_token_based。
与详情端点的差异:详情端点通过直接调用 ``get_auth_json_schemas()`` 仅返回 ``{auth_type: schema}`` 字典,
本端点额外返回 display_name 与 is_token_based适合前端认证方式选择下拉框渲染。
``AdapterNotFoundError`` 由全局处理器映射为 404。
"""
# 先校验适配器存在(抛 AdapterNotFoundError → 404
default_registry.get_adapter_metadata(adapter_type)
plugins = list_auth_plugins(adapter_type=adapter_type)
items = [
{
"type": plugin["type"],
"display_name": plugin["display_name"],
"schema": get_auth_json_schema(plugin["type"], adapter_type=adapter_type),
"is_token_based": is_token_based_auth_type(plugin["type"], adapter_type=adapter_type),
}
for plugin in plugins
]
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
``AdapterNotFoundError`` 由全局处理器映射为 404。
"""
metadata = default_registry.get_adapter_metadata(adapter_type)
data = {
"adapter_type": metadata.adapter_type,
"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]:
"""获取适配器资产信息,包含资产类型列表、示例配置与资产生成能力。
``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,
"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。
``AdapterNotFoundError`` 由全局处理器映射为 404。
"""
adapter = default_registry.get_adapter(adapter_type)
# 构造 ExecutableTool 兼容对象SimpleNamespace 严格对齐 Protocol 的 12 个字段)
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": []}}
# =============================================================================
# === 运行时统计端点(引入 use_cases ===
# =============================================================================
@adapter_router.get("/{adapter_type}/stats", response_model=dict)
async def get_adapter_stats(
adapter_type: str,
start_time: datetime | None = Query(None, description="指标聚合起始时间"),
end_time: datetime | None = Query(None, description="指标聚合结束时间"),
current_user: User = Depends(get_required_user),
db: AsyncSession = Depends(get_db),
) -> dict[str, Any]:
"""查询适配器维度的使用统计(系统数 / 工具数 / 资产数 / 指标聚合)。
先通过 ``get_adapter_metadata`` 校验适配器存在(抛 ``AdapterNotFoundError`` → 404
再注入 use_cases 调用 ``adapter_stats_service.get_adapter_stats``。
``start_time`` / ``end_time`` 仅作用于指标聚合,不影响系统/工具/资产计数。
"""
# 校验适配器存在(抛 AdapterNotFoundError → 404
default_registry.get_adapter_metadata(adapter_type)
use_cases = create_use_cases_from_db(db)
input_dto = GetAdapterStatsInput(
adapter_type=adapter_type,
start=start_time,
end=end_time,
)
output = await use_cases.adapter_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),
db: AsyncSession = Depends(get_db),
) -> dict[str, Any]:
"""查询适配器维度的健康状态(基于关联系统最近一次健康检查聚合)。
先通过 ``get_adapter_metadata`` 校验适配器存在(抛 ``AdapterNotFoundError`` → 404
再注入 use_cases 调用 ``adapter_stats_service.get_adapter_health``。
``status`` 取值unknown / healthy / degraded / unhealthy见 DTO docstring
"""
# 校验适配器存在(抛 AdapterNotFoundError → 404
default_registry.get_adapter_metadata(adapter_type)
use_cases = create_use_cases_from_db(db)
input_dto = GetAdapterHealthInput(adapter_type=adapter_type)
output = await use_cases.adapter_stats_service.get_adapter_health(input_dto)
return {"success": True, "data": output.model_dump()}