2026-06-20 22:16:07 +08:00
|
|
|
|
"""外部系统限界上下文 - IntegrationTool 子域 Router。
|
|
|
|
|
|
|
|
|
|
|
|
挂载到聚合 router 的 /integration-tools 前缀下,覆盖厂商集成(PRD FR-07/08)
|
|
|
|
|
|
的工具生成流程编排(list / discover / preview / generate / persist)。
|
|
|
|
|
|
Request Schema 与 Input DTO 不共享类,Router 内显式构造 DTO,
|
|
|
|
|
|
操作人字段由 current_user.uid 填充。
|
|
|
|
|
|
|
|
|
|
|
|
静态路径优先:/discover / /preview / /generate 必须在 POST ""(persist)之前声明,
|
|
|
|
|
|
避免被空路径匹配。
|
|
|
|
|
|
|
|
|
|
|
|
discover / preview / generate 三个端点请求体字段完全一致,共用
|
|
|
|
|
|
``IntegrationToolFlowRequest``,对应 use_cases 层 ``IntegrationToolFlowInput``。
|
2026-07-11 06:57:24 +08:00
|
|
|
|
操作语义由端点路径与服务方法名区分。三个端点的 DTO 构造与响应包装逻辑完全
|
|
|
|
|
|
一致,通过 ``_execute_flow`` 辅助函数复用,仅传入不同的 service 方法引用。
|
2026-06-20 22:16:07 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2026-07-11 06:57:24 +08:00
|
|
|
|
from collections.abc import Awaitable, Callable
|
2026-06-20 22:16:07 +08:00
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends, Query
|
2026-07-11 21:39:05 +08:00
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
2026-07-13 20:48:29 +08:00
|
|
|
|
from yuxi.external_systems.infrastructure.container import UseCases
|
2026-06-20 22:16:07 +08:00
|
|
|
|
from yuxi.external_systems.use_cases.dto.tool import (
|
|
|
|
|
|
ExternalToolCreateInput,
|
|
|
|
|
|
IntegrationToolFlowInput,
|
|
|
|
|
|
ListToolsInput,
|
|
|
|
|
|
PersistGeneratedToolsInput,
|
|
|
|
|
|
)
|
|
|
|
|
|
from yuxi.storage.postgres.models_business import User
|
|
|
|
|
|
|
2026-07-13 20:48:29 +08:00
|
|
|
|
from server.routers.external_systems import get_use_cases
|
2026-07-11 06:57:24 +08:00
|
|
|
|
from server.routers.external_systems.tool_router import ExternalToolItemRequest
|
2026-07-13 20:48:29 +08:00
|
|
|
|
from server.utils.auth_middleware import get_admin_user, get_required_user
|
2026-06-20 22:16:07 +08:00
|
|
|
|
|
|
|
|
|
|
integration_tool_router = APIRouter(
|
|
|
|
|
|
prefix="/integration-tools",
|
|
|
|
|
|
tags=["external-systems-integration-tool"],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# === Request Schemas(与 Input DTO 不共享类) ===
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PersistIntegrationToolsRequest(BaseModel):
|
|
|
|
|
|
"""持久化集成生成工具请求体。字段对齐 ``PersistGeneratedToolsInput``(不含 created_by)。"""
|
|
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
2026-07-11 06:57:24 +08:00
|
|
|
|
system_id: int = Field(..., ge=1)
|
2026-07-11 21:39:05 +08:00
|
|
|
|
tools: list[ExternalToolItemRequest] = Field(..., min_length=1, max_length=100)
|
|
|
|
|
|
source_type: str | None = Field(default=None, max_length=64)
|
2026-06-20 22:16:07 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class IntegrationToolFlowRequest(BaseModel):
|
|
|
|
|
|
"""集成工具生成流程请求体(discover / preview / generate 共用)。
|
|
|
|
|
|
|
|
|
|
|
|
字段对齐 ``IntegrationToolFlowInput``。三个操作的请求参数完全一致,
|
|
|
|
|
|
共用一个 Schema,操作语义由端点路径区分。
|
|
|
|
|
|
|
|
|
|
|
|
``source_type`` 为空时由 use_cases 层按 ``system.source_type`` 兜底解析。
|
|
|
|
|
|
``env_key`` 为空时由 use_cases 层解析系统默认环境。
|
|
|
|
|
|
``discovery_options`` 为用户选择,透传到 ``system_config``,各厂商字段名不同。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
2026-07-11 06:57:24 +08:00
|
|
|
|
system_id: int = Field(..., ge=1)
|
|
|
|
|
|
source_type: str | None = Field(default=None, max_length=64)
|
|
|
|
|
|
env_key: str | None = Field(default=None, max_length=32)
|
2026-06-20 22:16:07 +08:00
|
|
|
|
discovery_options: dict[str, Any] | None = None
|
|
|
|
|
|
|
2026-07-11 21:39:05 +08:00
|
|
|
|
@field_validator("discovery_options")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _validate_discovery_options(cls, v: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
|
|
|
|
if v is None:
|
|
|
|
|
|
return v
|
|
|
|
|
|
if not isinstance(v, dict):
|
|
|
|
|
|
raise ValueError("discovery_options 必须是对象")
|
|
|
|
|
|
|
|
|
|
|
|
def _check_value(value: Any, path: str = "root") -> None:
|
|
|
|
|
|
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
if isinstance(value, list):
|
|
|
|
|
|
for idx, item in enumerate(value):
|
|
|
|
|
|
_check_value(item, f"{path}[{idx}]")
|
|
|
|
|
|
return
|
|
|
|
|
|
if isinstance(value, dict):
|
|
|
|
|
|
for key, item in value.items():
|
|
|
|
|
|
_check_value(item, f"{path}.{key}")
|
|
|
|
|
|
return
|
|
|
|
|
|
raise ValueError(f"discovery_options.{path} 包含不支持的类型 {type(value).__name__}")
|
|
|
|
|
|
|
|
|
|
|
|
for key, value in v.items():
|
|
|
|
|
|
_check_value(value, key)
|
|
|
|
|
|
return v
|
|
|
|
|
|
|
2026-06-20 22:16:07 +08:00
|
|
|
|
|
2026-07-11 06:57:24 +08:00
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# === 内部辅助 ===
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_FlowMethod = Callable[[IntegrationToolFlowInput], Awaitable[Any]]
|
|
|
|
|
|
"""集成工具生成流程 service 方法签名(discover / preview / generate 共用)。"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _execute_flow(
|
|
|
|
|
|
body: IntegrationToolFlowRequest,
|
|
|
|
|
|
flow_method: _FlowMethod,
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""执行集成工具生成流程(discover / preview / generate 共用)。
|
|
|
|
|
|
|
|
|
|
|
|
构造 ``IntegrationToolFlowInput`` 并调用传入的 service 方法,
|
|
|
|
|
|
返回统一格式的响应。三个操作的流程完全一致,仅 service 方法名不同,
|
|
|
|
|
|
通过传入 bound method 区分,避免重复构造 DTO 的样板代码。
|
|
|
|
|
|
"""
|
|
|
|
|
|
input_dto = IntegrationToolFlowInput(
|
|
|
|
|
|
system_id=body.system_id,
|
|
|
|
|
|
source_type=body.source_type,
|
|
|
|
|
|
env_key=body.env_key,
|
|
|
|
|
|
discovery_options=body.discovery_options,
|
|
|
|
|
|
)
|
|
|
|
|
|
output = await flow_method(input_dto)
|
|
|
|
|
|
return {"success": True, "data": output.model_dump()}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-20 22:16:07 +08:00
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# === Endpoints ===
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@integration_tool_router.get("", response_model=dict)
|
|
|
|
|
|
async def list_generated_tools(
|
2026-07-11 06:57:24 +08:00
|
|
|
|
limit: int = Query(20, ge=1, le=500),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
offset: int = Query(0, ge=0),
|
|
|
|
|
|
system_id: int | None = Query(None),
|
2026-07-04 00:16:45 +08:00
|
|
|
|
category: str | None = Query(None, max_length=64),
|
|
|
|
|
|
adapter_type: str | None = Query(None, max_length=32),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
enabled: bool | None = Query(None),
|
2026-07-04 00:16:45 +08:00
|
|
|
|
keyword: str | None = Query(None, max_length=128),
|
2026-07-13 20:48:29 +08:00
|
|
|
|
use_cases: UseCases = Depends(get_use_cases),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
current_user: User = Depends(get_required_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""分页列出已生成的集成工具。
|
|
|
|
|
|
|
|
|
|
|
|
与 ``tool_router.GET /tools`` 功能等价,保持 Router 边界清晰:
|
|
|
|
|
|
``integration_tool_router`` 仅依赖 ``integration_tool_service`` 端口。
|
|
|
|
|
|
"""
|
|
|
|
|
|
input_dto = ListToolsInput(
|
2026-07-11 06:57:24 +08:00
|
|
|
|
offset=offset,
|
|
|
|
|
|
limit=limit,
|
2026-06-20 22:16:07 +08:00
|
|
|
|
system_id=system_id,
|
|
|
|
|
|
category=category,
|
|
|
|
|
|
adapter_type=adapter_type,
|
|
|
|
|
|
enabled=enabled,
|
|
|
|
|
|
keyword=keyword,
|
|
|
|
|
|
)
|
|
|
|
|
|
output = await use_cases.integration_tool_service.list_generated_tools(input_dto)
|
|
|
|
|
|
return {"success": True, "data": output.model_dump()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@integration_tool_router.post("/discover", response_model=dict)
|
|
|
|
|
|
async def discover_resources(
|
|
|
|
|
|
body: IntegrationToolFlowRequest,
|
2026-07-13 20:48:29 +08:00
|
|
|
|
use_cases: UseCases = Depends(get_use_cases),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""发现外部系统可用资源(调用厂商集成的 ``discover`` handler)。"""
|
2026-07-11 06:57:24 +08:00
|
|
|
|
return await _execute_flow(body, use_cases.integration_tool_service.discover_resources)
|
2026-06-20 22:16:07 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@integration_tool_router.post("/preview", response_model=dict)
|
|
|
|
|
|
async def preview_tools(
|
|
|
|
|
|
body: IntegrationToolFlowRequest,
|
2026-07-13 20:48:29 +08:00
|
|
|
|
use_cases: UseCases = Depends(get_use_cases),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""预览将生成的工具列表(调用厂商集成的 ``preview_tools`` handler)。"""
|
2026-07-11 06:57:24 +08:00
|
|
|
|
return await _execute_flow(body, use_cases.integration_tool_service.preview_tools)
|
2026-06-20 22:16:07 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@integration_tool_router.post("/generate", response_model=dict)
|
|
|
|
|
|
async def generate_tools_draft(
|
|
|
|
|
|
body: IntegrationToolFlowRequest,
|
2026-07-13 20:48:29 +08:00
|
|
|
|
use_cases: UseCases = Depends(get_use_cases),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""生成工具草稿(调用厂商集成的 ``create_tools`` handler,不持久化)。
|
|
|
|
|
|
|
|
|
|
|
|
草稿由前端确认后调用 ``POST /integration-tools``(persist)落库。
|
|
|
|
|
|
"""
|
2026-07-11 06:57:24 +08:00
|
|
|
|
return await _execute_flow(body, use_cases.integration_tool_service.generate_tools_draft)
|
2026-06-20 22:16:07 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@integration_tool_router.post("", response_model=dict)
|
|
|
|
|
|
async def persist_generated_tools(
|
|
|
|
|
|
body: PersistIntegrationToolsRequest,
|
2026-07-13 20:48:29 +08:00
|
|
|
|
use_cases: UseCases = Depends(get_use_cases),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""持久化厂商集成生成的工具草稿。created_by 由服务端以 current_user.uid 填充。"""
|
|
|
|
|
|
input_dto = PersistGeneratedToolsInput(
|
|
|
|
|
|
system_id=body.system_id,
|
|
|
|
|
|
tools=[ExternalToolCreateInput(**item.model_dump()) for item in body.tools],
|
|
|
|
|
|
created_by=current_user.uid,
|
2026-07-11 21:39:05 +08:00
|
|
|
|
source_type=body.source_type,
|
2026-06-20 22:16:07 +08:00
|
|
|
|
)
|
|
|
|
|
|
output = await use_cases.integration_tool_service.persist_generated_tools(input_dto)
|
|
|
|
|
|
return {"success": True, "data": output.model_dump()}
|