ForcePilot/backend/server/routers/external_systems/integration_tool_router.py
Kris 002e6a356b chore: 批量整理代码变更,修复多类细节问题
1.  清理测试文件中未使用的导入与冗余代码
2.  修复审计日志与批量操作的空值约束,统一填充"global"作为默认渠道
3.  调整批量消息撤回的响应语义,对齐其他端点的部分成功契约
4.  修复访问规则批量克隆的唯一约束问题,新增后缀自动处理逻辑
5.  替换anyio为asyncio并行调用,修正时间UTC导入路径
6.  优化前端外部系统概览页的刷新状态提示与缓存逻辑
7.  修复测试用例中的断言与请求方式问题,适配httpx删除请求特性
8.  重构后端路由的依赖注入,移除冗余的数据库会话依赖
9.  调整测试用例的权限校验逻辑,修正强制登出的权限判断
10. 修复语义分块测试的numpy依赖问题,清理冗余导入
2026-07-13 20:48:29 +08:00

214 lines
8.5 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 yuxi.external_systems.infrastructure.container import UseCases
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 import get_use_cases
from server.routers.external_systems.tool_router import ExternalToolItemRequest
from server.utils.auth_middleware import get_admin_user, 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),
use_cases: UseCases = Depends(get_use_cases),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""分页列出已生成的集成工具。
与 ``tool_router.GET /tools`` 功能等价,保持 Router 边界清晰:
``integration_tool_router`` 仅依赖 ``integration_tool_service`` 端口。
"""
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,
use_cases: UseCases = Depends(get_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""发现外部系统可用资源(调用厂商集成的 ``discover`` handler"""
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,
use_cases: UseCases = Depends(get_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""预览将生成的工具列表(调用厂商集成的 ``preview_tools`` handler"""
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,
use_cases: UseCases = Depends(get_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""生成工具草稿(调用厂商集成的 ``create_tools`` handler不持久化
草稿由前端确认后调用 ``POST /integration-tools``persist落库。
"""
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,
use_cases: UseCases = Depends(get_use_cases),
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,
source_type=body.source_type,
)
output = await use_cases.integration_tool_service.persist_generated_tools(input_dto)
return {"success": True, "data": output.model_dump()}