ForcePilot/backend/server/routers/external_systems/integration_tool_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

219 lines
8.7 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.

"""外部系统限界上下文 - 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()}