1. 清理测试文件中未使用的导入与冗余代码 2. 修复审计日志与批量操作的空值约束,统一填充"global"作为默认渠道 3. 调整批量消息撤回的响应语义,对齐其他端点的部分成功契约 4. 修复访问规则批量克隆的唯一约束问题,新增后缀自动处理逻辑 5. 替换anyio为asyncio并行调用,修正时间UTC导入路径 6. 优化前端外部系统概览页的刷新状态提示与缓存逻辑 7. 修复测试用例中的断言与请求方式问题,适配httpx删除请求特性 8. 重构后端路由的依赖注入,移除冗余的数据库会话依赖 9. 调整测试用例的权限校验逻辑,修正强制登出的权限判断 10. 修复语义分块测试的numpy依赖问题,清理冗余导入
309 lines
12 KiB
Python
309 lines
12 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, Path, Query
|
||
from sqlalchemy import func, select
|
||
from yuxi.external_systems.integrations.operation_registry import (
|
||
IntegrationOperationRegistry,
|
||
)
|
||
from yuxi.external_systems.integrations.registry import IntegrationRegistry
|
||
from yuxi.storage.postgres.manager import pg_manager
|
||
from yuxi.storage.postgres.models_business import User
|
||
from yuxi.storage.postgres.models_external import ExternalSystem
|
||
|
||
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="模糊匹配 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),
|
||
) -> dict[str, Any]:
|
||
"""查询厂商集成的分组总览(按 adapter_type 分组 + 标签统计 + 使用统计)。
|
||
|
||
通过 filter_integrations 获取全量集成(无分页),按 ``adapter_type``
|
||
分组构造 ``by_adapter_type`` 字典(每组仅含精简字段),并统计每个标签
|
||
出现次数构造 ``by_tag`` 字典(按标签名升序排序)。同时查询
|
||
``ext_systems`` 表,按 ``integration_key`` 聚合已创建系统数量,返回
|
||
``usage_stats``。
|
||
|
||
使用 ``pg_manager.get_async_session_context()`` 开启独立请求级 session,
|
||
与 external_systems 模块其他 router 保持一致(不复用鉴权 session,
|
||
避免 autobegin 残留事务污染连接池)。
|
||
"""
|
||
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)
|
||
)
|
||
async with pg_manager.get_async_session_context() as db:
|
||
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,
|
||
},
|
||
}
|