ForcePilot/backend/server/routers/external_systems/integration_router.py
Kris 2f6a4c29bb refactor(channel routers): 统一参数校验与常量定义,新增功能端点
1. 为渠道账户ID查询添加最小长度校验,统一分析模块常量引用
2. 新增扫码登录向导端点,完善文档说明
3. 优化配对统计接口,移除无效参数
4. 为出站箱接口添加批量上限与202状态码
5. 新增测试用例、访问规则、配额等模块的查询与校验参数
6. 新增适配器健康批量查询、健康检查触发接口
7. 统一告警、审计日志的错误处理方式
8. 新增插件配置账户ID支持,优化批量操作响应
9. 新增环境健康批量查询、Webhook限流与参数校验
10. 完善会话管理、审计日志的参数与文档说明
11. 修复导入模块的校验错误处理逻辑
2026-07-11 21:39:05 +08:00

305 lines
12 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.

"""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, Path, Query
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
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 yuxi.storage.postgres.models_external import ExternalSystem
from server.utils.auth_middleware import get_db, 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="模糊匹配 key / 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),
db: AsyncSession = Depends(get_db),
) -> dict[str, Any]:
"""查询厂商集成的分组总览(按 adapter_type 分组 + 标签统计 + 使用统计)。
通过 filter_integrations 获取全量集成(无分页),按 ``adapter_type``
分组构造 ``by_adapter_type`` 字典(每组仅含精简字段),并统计每个标签
出现次数构造 ``by_tag`` 字典(按标签名升序排序)。同时查询
``ext_systems`` 表,按 ``integration_key`` 聚合已创建系统数量,返回
``usage_stats``。
"""
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 or ():
by_tag[tag] = by_tag.get(tag, 0) + 1
# 查询数据库统计每个 integration_key 对应的非删除系统数量
stmt = (
select(ExternalSystem.integration_key, func.count(ExternalSystem.id))
.where(
ExternalSystem.integration_key.is_not(None),
ExternalSystem.is_deleted.is_(False),
)
.group_by(ExternalSystem.integration_key)
)
result = await db.execute(stmt)
usage_stats = {key: int(count) for key, count in result.all() if key}
return {
"success": True,
"data": {
"by_adapter_type": by_adapter_type,
"total": len(metadata_list),
"by_tag": dict(sorted(by_tag.items())),
"usage_stats": usage_stats,
},
}
@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 or ():
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 = Path(..., min_length=1, max_length=64),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""查询单个厂商集成的完整元数据详情。
``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 = Path(..., min_length=1, max_length=64),
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 sorted(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 = Path(..., min_length=1, max_length=64),
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 = Path(..., min_length=1, max_length=64),
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,
},
}