本次提交对多个外部系统路由进行了多维度优化: 1. 统一分页参数:将所有路由的`page = offset//limit +1`、`page_size=limit`替换为标准的`limit`+`offset`分页格式 2. 完善接口文档:补充多个端点的功能说明、参数含义与返回字段解释 3. 增强参数校验:新增字段长度限制、正则校验、枚举类型约束与业务逻辑校验 4. 优化代码复用:提取重复逻辑为辅助函数,减少样板代码 5. 修复接口问题:修正工具健康检查端点路径参数类型,优化导出接口响应格式 6. 补充异常处理:为批量操作添加异常捕获与日志记录,避免流程中断
285 lines
11 KiB
Python
285 lines
11 KiB
Python
"""Integration 子域 Router.
|
||
|
||
外部系统限界上下文的厂商集成目录查询 API。所有端点直接读取
|
||
``IntegrationRegistry`` 与 ``IntegrationOperationRegistry`` 注册表全局单例
|
||
(代码注册的内存态对象),不引入 use_cases 层——数据源无持久化需求,与
|
||
adapter_router / auth_plugin_router 元数据端点保持一致。
|
||
|
||
设计依据:docs/vibe/v1.1/设计方案/RouterAPI扩展设计/15-integration_router扩展设计方案.md
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Any
|
||
|
||
from fastapi import APIRouter, Depends, Query
|
||
from yuxi.external_systems.integrations.operation_registry import (
|
||
IntegrationOperationRegistry,
|
||
)
|
||
from yuxi.external_systems.integrations.registry import IntegrationRegistry
|
||
from yuxi.storage.postgres.models_business import User
|
||
|
||
from server.utils.auth_middleware import get_required_user
|
||
|
||
integration_router = APIRouter(
|
||
prefix="/integrations",
|
||
tags=["external-systems-integration"],
|
||
)
|
||
|
||
|
||
# 列表端点剔除的字段:Schema/配置/依赖说明,避免列表响应过重
|
||
_SUMMARY_EXCLUDED_FIELDS = ("connection_extra_schema", "example_config", "dependency_note")
|
||
|
||
|
||
# =============================================================================
|
||
# === 静态路径端点(必须在 /{key} 之前声明) ===
|
||
# =============================================================================
|
||
|
||
|
||
@integration_router.get("", response_model=dict)
|
||
async def list_integrations(
|
||
adapter_type: str | None = Query(None, max_length=32, description="按适配器类型精确过滤"),
|
||
tags: str | None = Query(None, max_length=256, description="按标签过滤,多个标签以逗号分隔(OR 关系)"),
|
||
keyword: str | None = Query(
|
||
None, max_length=128, description="模糊匹配 display_name / description(大小写不敏感)"
|
||
),
|
||
limit: int = Query(50, ge=1, le=200, description="分页大小,最大 200"),
|
||
offset: int = Query(0, ge=0, description="分页偏移"),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""查询厂商集成目录列表,支持按适配器类型、标签、关键词过滤与分页。
|
||
|
||
列表项返回精简字段(剔除 connection_extra_schema / example_config /
|
||
dependency_note),适合前端集成市场页表格展示。``total`` 为满足筛选
|
||
条件的全量总数(非当前分页条数)。
|
||
|
||
通过 ``filter_integrations`` 一次调用获取全量过滤结果,再切片分页、
|
||
``len()`` 取总数,避免 ``list_integrations`` + ``count_integrations``
|
||
各自独立调用 ``filter_integrations`` 导致双重过滤+排序。
|
||
"""
|
||
tags_list: list[str] | None = None
|
||
if tags is not None:
|
||
tags_list = [t.strip() for t in tags.split(",") if t.strip()]
|
||
if not tags_list:
|
||
tags_list = None
|
||
|
||
filtered = IntegrationRegistry.filter_integrations(
|
||
adapter_type=adapter_type,
|
||
tags=tags_list,
|
||
keyword=keyword,
|
||
)
|
||
total = len(filtered)
|
||
metadata_list = filtered[offset : offset + limit]
|
||
items = [
|
||
{k: v for k, v in metadata.model_dump().items() if k not in _SUMMARY_EXCLUDED_FIELDS}
|
||
for metadata in metadata_list
|
||
]
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"items": items,
|
||
"total": total,
|
||
"limit": limit,
|
||
"offset": offset,
|
||
},
|
||
}
|
||
|
||
|
||
@integration_router.get("/overview", response_model=dict)
|
||
async def get_integrations_overview(
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""查询厂商集成的分组总览(按 adapter_type 分组 + 标签统计)。
|
||
|
||
通过 filter_integrations 获取全量集成(无分页),按 ``adapter_type``
|
||
分组构造 ``by_adapter_type`` 字典(每组仅含精简字段),并统计每个标签
|
||
出现次数构造 ``by_tag`` 字典。适合前端管理页树形渲染与统计卡片。
|
||
"""
|
||
metadata_list = IntegrationRegistry.filter_integrations()
|
||
|
||
by_adapter_type: dict[str, list[dict[str, Any]]] = {}
|
||
by_tag: dict[str, int] = {}
|
||
for metadata in metadata_list:
|
||
by_adapter_type.setdefault(metadata.adapter_type, []).append(
|
||
{
|
||
"key": metadata.key,
|
||
"display_name": metadata.display_name,
|
||
"adapter_type": metadata.adapter_type,
|
||
"tags": metadata.tags,
|
||
}
|
||
)
|
||
for tag in metadata.tags:
|
||
by_tag[tag] = by_tag.get(tag, 0) + 1
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"by_adapter_type": by_adapter_type,
|
||
"total": len(metadata_list),
|
||
"by_tag": by_tag,
|
||
},
|
||
}
|
||
|
||
|
||
@integration_router.get("/categories", response_model=dict)
|
||
async def get_integration_categories(
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""查询厂商集成的分类标签树(基于 tags 字段聚合)。
|
||
|
||
通过 filter_integrations 获取全量集成(无分页),每个标签返回
|
||
``count`` 与 ``integrations``(key 列表),按 ``count`` 降序、
|
||
``tag`` 升序排序。适合前端集成市场页分类筛选侧边栏。
|
||
"""
|
||
metadata_list = IntegrationRegistry.filter_integrations()
|
||
|
||
tag_to_integrations: dict[str, list[str]] = {}
|
||
for metadata in metadata_list:
|
||
for tag in metadata.tags:
|
||
tag_to_integrations.setdefault(tag, []).append(metadata.key)
|
||
|
||
items = [{"tag": tag, "count": len(keys), "integrations": keys} for tag, keys in tag_to_integrations.items()]
|
||
items.sort(key=lambda it: (-it["count"], it["tag"]))
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {"items": items, "total": len(items)},
|
||
}
|
||
|
||
|
||
@integration_router.get("/adapter-types", response_model=dict)
|
||
async def get_integration_adapter_types(
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""查询所有厂商集成涉及的适配器类型(去重),用于前端过滤选项。
|
||
|
||
仅返回有厂商集成的适配器类型(当前全部为 http),与 adapter_router 的
|
||
``GET /adapters``(所有注册协议适配器)语义不同。
|
||
"""
|
||
adapter_types = IntegrationRegistry.list_adapter_types()
|
||
return {
|
||
"success": True,
|
||
"data": {"items": adapter_types, "total": len(adapter_types)},
|
||
}
|
||
|
||
|
||
@integration_router.get("/source-types", response_model=dict)
|
||
async def get_integration_source_types(
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""查询所有已注册操作 handler 的数据源类型(含操作支持矩阵)。
|
||
|
||
对每个 source_type 调用 ``list_operations`` 获取已注册操作,按
|
||
``source_type`` 升序排序。适合前端"工具生成"页的 source_type 选择。
|
||
仅返回厂商 source_type,不含 openapi / postman 等通用 source_type
|
||
(通用 source_type 走资产导入流程,未注册操作 handler)。
|
||
"""
|
||
source_types = IntegrationOperationRegistry.list_source_types()
|
||
items = [
|
||
{
|
||
"source_type": source_type,
|
||
"operations": IntegrationOperationRegistry.list_operations(source_type),
|
||
}
|
||
for source_type in source_types
|
||
]
|
||
return {
|
||
"success": True,
|
||
"data": {"items": items, "total": len(items)},
|
||
}
|
||
|
||
|
||
# =============================================================================
|
||
# === 路径参数端点(/{key}) ===
|
||
# =============================================================================
|
||
|
||
|
||
@integration_router.get("/{key}", response_model=dict)
|
||
async def get_integration_detail(
|
||
key: str,
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""查询指定厂商集成的完整详情(含所有 12 个字段)。
|
||
|
||
``get_integration_or_raise`` 未找到时抛 ``EntityNotFoundError`` (404),
|
||
由全局处理器统一映射。返回 ``model_dump()`` 完整字段,包含
|
||
connection_extra_schema / example_config / dependency_note。
|
||
"""
|
||
metadata = IntegrationRegistry.get_integration_or_raise(key)
|
||
return {"success": True, "data": metadata.model_dump()}
|
||
|
||
|
||
@integration_router.get("/{key}/operations", response_model=dict)
|
||
async def get_integration_operations(
|
||
key: str,
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""查询指定厂商集成已注册的操作 handler 列表(按 source_type 分组)。
|
||
|
||
遍历 ``metadata.source_types``,对每个 source_type 调用
|
||
``list_operations`` 获取已注册操作。``operations_by_source_type`` 为
|
||
``{source_type: [operations]}`` 字典;``supported_operations`` 为所有
|
||
source_type 操作的并集(去重,按字母升序排序)。
|
||
|
||
``get_integration_or_raise`` 未找到时抛 ``EntityNotFoundError`` (404)。
|
||
"""
|
||
metadata = IntegrationRegistry.get_integration_or_raise(key)
|
||
|
||
operations_by_source_type: dict[str, list[str]] = {}
|
||
for source_type in metadata.source_types:
|
||
operations_by_source_type[source_type] = IntegrationOperationRegistry.list_operations(source_type)
|
||
|
||
supported_operations = sorted(set(op for ops in operations_by_source_type.values() for op in ops))
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"key": key,
|
||
"operations_by_source_type": operations_by_source_type,
|
||
"supported_operations": supported_operations,
|
||
},
|
||
}
|
||
|
||
|
||
@integration_router.get("/{key}/connection-schema", response_model=dict)
|
||
async def get_integration_connection_schema(
|
||
key: str,
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""获取厂商集成的连接配置 JSON Schema,用于前端动态表单渲染。
|
||
|
||
返回 ``connection_extra_schema`` 字段(可能为 None,表示该集成无额外
|
||
连接配置)。与详情端点的差异:本端点仅返回 Schema,适合前端表单渲染
|
||
时独立获取。
|
||
|
||
``get_integration_or_raise`` 未找到时抛 ``EntityNotFoundError`` (404)。
|
||
"""
|
||
metadata = IntegrationRegistry.get_integration_or_raise(key)
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"key": key,
|
||
"connection_extra_schema": metadata.connection_extra_schema,
|
||
},
|
||
}
|
||
|
||
|
||
@integration_router.get("/{key}/example-config", response_model=dict)
|
||
async def get_integration_example_config(
|
||
key: str,
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""获取厂商集成的推荐配置示例,用于前端表单预填与配置引导。
|
||
|
||
返回 ``example_config`` 字段(可能为 None,表示该集成无推荐配置)。
|
||
|
||
``get_integration_or_raise`` 未找到时抛 ``EntityNotFoundError`` (404)。
|
||
"""
|
||
metadata = IntegrationRegistry.get_integration_or_raise(key)
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"key": key,
|
||
"example_config": metadata.example_config,
|
||
},
|
||
}
|