"""外部系统限界上下文 - 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``。 操作语义由端点路径与服务方法名区分。三个端点的 DTO 构造与响应包装逻辑完全 一致,通过 ``_execute_flow`` 辅助函数复用,仅传入不同的 service 方法引用。 """ from __future__ import annotations from collections.abc import Awaitable, Callable from typing import Any from fastapi import APIRouter, Depends, Query from pydantic import BaseModel, ConfigDict, Field, field_validator from sqlalchemy.ext.asyncio import AsyncSession from yuxi.external_systems.infrastructure.container import create_use_cases_from_db from yuxi.external_systems.use_cases.dto.tool import ( ExternalToolCreateInput, IntegrationToolFlowInput, ListToolsInput, PersistGeneratedToolsInput, ) from yuxi.storage.postgres.models_business import User from server.routers.external_systems.tool_router import ExternalToolItemRequest from server.utils.auth_middleware import get_admin_user, get_db, get_required_user 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) system_id: int = Field(..., ge=1) tools: list[ExternalToolItemRequest] = Field(..., min_length=1, max_length=100) source_type: str | None = Field(default=None, max_length=64) 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) 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) discovery_options: dict[str, Any] | None = None @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 # ============================================================================= # === 内部辅助 === # ============================================================================= _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()} # ============================================================================= # === Endpoints === # ============================================================================= @integration_tool_router.get("", response_model=dict) async def list_generated_tools( limit: int = Query(20, ge=1, le=500), offset: int = Query(0, ge=0), system_id: int | None = Query(None), category: str | None = Query(None, max_length=64), adapter_type: str | None = Query(None, max_length=32), enabled: bool | None = Query(None), keyword: str | None = Query(None, max_length=128), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """分页列出已生成的集成工具。 与 ``tool_router.GET /tools`` 功能等价,保持 Router 边界清晰: ``integration_tool_router`` 仅依赖 ``integration_tool_service`` 端口。 """ use_cases = create_use_cases_from_db(db) input_dto = ListToolsInput( offset=offset, limit=limit, 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, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """发现外部系统可用资源(调用厂商集成的 ``discover`` handler)。""" use_cases = create_use_cases_from_db(db) return await _execute_flow(body, use_cases.integration_tool_service.discover_resources) @integration_tool_router.post("/preview", response_model=dict) async def preview_tools( body: IntegrationToolFlowRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """预览将生成的工具列表(调用厂商集成的 ``preview_tools`` handler)。""" use_cases = create_use_cases_from_db(db) return await _execute_flow(body, use_cases.integration_tool_service.preview_tools) @integration_tool_router.post("/generate", response_model=dict) async def generate_tools_draft( body: IntegrationToolFlowRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """生成工具草稿(调用厂商集成的 ``create_tools`` handler,不持久化)。 草稿由前端确认后调用 ``POST /integration-tools``(persist)落库。 """ use_cases = create_use_cases_from_db(db) return await _execute_flow(body, use_cases.integration_tool_service.generate_tools_draft) @integration_tool_router.post("", response_model=dict) async def persist_generated_tools( body: PersistIntegrationToolsRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """持久化厂商集成生成的工具草稿。created_by 由服务端以 current_user.uid 填充。""" use_cases = create_use_cases_from_db(db) input_dto = PersistGeneratedToolsInput( system_id=body.system_id, tools=[ExternalToolCreateInput(**item.model_dump()) for item in body.tools], created_by=current_user.uid, source_type=body.source_type, ) output = await use_cases.integration_tool_service.persist_generated_tools(input_dto) return {"success": True, "data": output.model_dump()}