2026-06-20 22:16:07 +08:00
|
|
|
|
"""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(
|
2026-07-04 00:16:45 +08:00
|
|
|
|
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(大小写不敏感)"
|
|
|
|
|
|
),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
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`` 为满足筛选
|
|
|
|
|
|
条件的全量总数(由 count_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
|
|
|
|
|
|
|
|
|
|
|
|
metadata_list = IntegrationRegistry.list_integrations(
|
|
|
|
|
|
adapter_type=adapter_type,
|
|
|
|
|
|
tags=tags_list,
|
|
|
|
|
|
keyword=keyword,
|
|
|
|
|
|
limit=limit,
|
|
|
|
|
|
offset=offset,
|
|
|
|
|
|
)
|
|
|
|
|
|
total = IntegrationRegistry.count_integrations(
|
|
|
|
|
|
adapter_type=adapter_type,
|
|
|
|
|
|
tags=tags_list,
|
|
|
|
|
|
keyword=keyword,
|
|
|
|
|
|
)
|
|
|
|
|
|
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 sorted(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 个字段)。
|
|
|
|
|
|
|
2026-07-02 03:29:06 +08:00
|
|
|
|
``get_integration_or_raise`` 未找到时抛 ``EntityNotFoundError`` (404),
|
|
|
|
|
|
由全局处理器统一映射。返回 ``model_dump()`` 完整字段,包含
|
2026-06-20 22:16:07 +08:00
|
|
|
|
connection_extra_schema / example_config / dependency_note。
|
|
|
|
|
|
"""
|
2026-07-02 03:29:06 +08:00
|
|
|
|
metadata = IntegrationRegistry.get_integration_or_raise(key)
|
2026-06-20 22:16:07 +08:00
|
|
|
|
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 操作的并集(去重,按字母升序排序)。
|
|
|
|
|
|
|
2026-07-02 03:29:06 +08:00
|
|
|
|
``get_integration_or_raise`` 未找到时抛 ``EntityNotFoundError`` (404)。
|
2026-06-20 22:16:07 +08:00
|
|
|
|
"""
|
2026-07-02 03:29:06 +08:00
|
|
|
|
metadata = IntegrationRegistry.get_integration_or_raise(key)
|
2026-06-20 22:16:07 +08:00
|
|
|
|
|
|
|
|
|
|
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,适合前端表单渲染
|
|
|
|
|
|
时独立获取。
|
|
|
|
|
|
|
2026-07-02 03:29:06 +08:00
|
|
|
|
``get_integration_or_raise`` 未找到时抛 ``EntityNotFoundError`` (404)。
|
2026-06-20 22:16:07 +08:00
|
|
|
|
"""
|
2026-07-02 03:29:06 +08:00
|
|
|
|
metadata = IntegrationRegistry.get_integration_or_raise(key)
|
2026-06-20 22:16:07 +08:00
|
|
|
|
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,表示该集成无推荐配置)。
|
|
|
|
|
|
|
2026-07-02 03:29:06 +08:00
|
|
|
|
``get_integration_or_raise`` 未找到时抛 ``EntityNotFoundError`` (404)。
|
2026-06-20 22:16:07 +08:00
|
|
|
|
"""
|
2026-07-02 03:29:06 +08:00
|
|
|
|
metadata = IntegrationRegistry.get_integration_or_raise(key)
|
2026-06-20 22:16:07 +08:00
|
|
|
|
return {
|
|
|
|
|
|
"success": True,
|
|
|
|
|
|
"data": {
|
|
|
|
|
|
"key": key,
|
|
|
|
|
|
"example_config": metadata.example_config,
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|