1. 为渠道账户ID查询添加最小长度校验,统一分析模块常量引用 2. 新增扫码登录向导端点,完善文档说明 3. 优化配对统计接口,移除无效参数 4. 为出站箱接口添加批量上限与202状态码 5. 新增测试用例、访问规则、配额等模块的查询与校验参数 6. 新增适配器健康批量查询、健康检查触发接口 7. 统一告警、审计日志的错误处理方式 8. 新增插件配置账户ID支持,优化批量操作响应 9. 新增环境健康批量查询、Webhook限流与参数校验 10. 完善会话管理、审计日志的参数与文档说明 11. 修复导入模块的校验错误处理逻辑
398 lines
17 KiB
Python
398 lines
17 KiB
Python
"""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 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,
|
||
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.infrastructure.container import (
|
||
create_adapter_stats_service_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]:
|
||
"""列出所有已注册的适配器类型(按字母序排序,保证 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),
|
||
db: AsyncSession = Depends(get_db),
|
||
) -> 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,
|
||
)
|
||
stats_service = create_adapter_stats_service_from_db(db)
|
||
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: 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),
|
||
再通过轻量工厂 ``create_adapter_stats_service_from_db`` 创建
|
||
``AdapterStatsService``(仅注入 Repositories,不构造完整 UseCases 容器)。
|
||
``start_time`` / ``end_time`` 仅作用于指标聚合,不影响系统/工具/资产计数。
|
||
"""
|
||
# 校验适配器存在(抛 AdapterNotFoundError → 404)
|
||
default_registry.get_adapter_metadata(adapter_type)
|
||
|
||
stats_service = create_adapter_stats_service_from_db(db)
|
||
input_dto = GetAdapterStatsInput(
|
||
adapter_type=adapter_type,
|
||
start=start_time,
|
||
end=end_time,
|
||
)
|
||
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),
|
||
db: AsyncSession = Depends(get_db),
|
||
) -> 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)
|
||
|
||
stats_service = create_adapter_stats_service_from_db(db)
|
||
input_dto = GetAdapterHealthInput(adapter_type=adapter_type)
|
||
output = await stats_service.get_adapter_health(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|