"""Import 子域 Router。 外部系统限界上下文的导入/导出 API,覆盖以下能力: 系统级导入(两阶段流程): 1. ``POST /preview``:根据适配器类型与源资产生成系统级导入草稿(系统 + 工具) 2. ``POST /confirm``:基于草稿执行实际持久化 工具级导入(两阶段流程): 3. ``POST /tools/preview``:根据适配器类型与源资产生成工具草稿列表(不落库) 4. ``POST /tools/confirm``:基于草稿执行工具持久化 工具导出: 5. ``GET /tools/export``:导出单个工具配置 6. ``POST /tools/bulk-export``:批量导出工具配置 工具包导入/导出: 7. ``POST /tool-package/export``:导出工具包(含工具 + 系统 + 环境 + 资产) 8. ``POST /tool-package/preview``:工具包导入预览(检查冲突,不落库) 9. ``POST /tool-package/confirm``:工具包导入确认(导入系统/环境/资产/工具) 所有端点通过 ``create_use_cases_from_db`` 装配 use_cases,经 ``import_service`` 端口调用用例。Request Schema 与 Input DTO 不共享类,Router 内显式构造 DTO, 操作人字段(``created_by``)由 ``current_user.uid`` 填充,角色字段 (``requester_role``)由 ``current_user.role`` 填充。 """ from __future__ import annotations from typing import Any from fastapi import APIRouter, Depends, Query from pydantic import BaseModel, ConfigDict, Field 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.environment import EnvironmentOutput from yuxi.external_systems.use_cases.dto.import_draft import ( BulkExportToolsInput, ExportToolInput, ImportConfirmInput, ImportPreviewInput, ToolImportConfirmInput, ToolImportPreviewInput, ToolPackageAssetInput, ToolPackageExportInput, ToolPackageImportInput, ToolPackageImportPreviewInput, ToolPackageInput, ToolPackageItemInput, ) from yuxi.external_systems.use_cases.dto.system import ( ExternalSystemCreateInput, SystemImportDraft, SystemOutput, ) from yuxi.external_systems.use_cases.dto.tool import ( ExternalToolCreateInput, ToolOutput, ) from yuxi.storage.postgres.models_business import User from server.utils.auth_middleware import get_admin_user, get_db import_router = APIRouter(prefix="/imports", tags=["external-systems-import"]) # ---------------- Request Schemas ---------------- class ImportPreviewRequest(BaseModel): """导入预览请求体。字段对齐 ``ImportPreviewInput``。 ``payload`` 类型为 ``Any``,透传给适配器 ``generate_from_asset``, 不同适配器期望不同结构(dict/list/str),与 ``ToolImportPreviewRequest`` 及 DTO ``ImportPreviewInput`` 保持一致。 """ model_config = ConfigDict(frozen=True) adapter_type: str source_type: str payload: Any asset_id: int | None = None class ExternalSystemDraftRequest(BaseModel): """系统级导入草稿中的系统请求体。字段对齐 ``ExternalSystemCreateInput``。""" model_config = ConfigDict(frozen=True) slug: str = Field(..., min_length=1, max_length=128, pattern=r"^[a-zA-Z_][a-zA-Z0-9_-]{0,127}$") name: str = Field(..., min_length=1, max_length=128) description: str = Field(..., min_length=1) category: str = Field(default="default", max_length=64) adapter_type: str = Field(default="http", max_length=32) source_type: str | None = Field(default=None, max_length=64) enabled: bool = Field(default=True) connection_config: dict[str, Any] = Field(default_factory=dict) auth_type: str = Field(default="none", max_length=32) auth_config: dict[str, Any] | None = Field(default=None) secret_refs: dict[str, Any] = Field(default_factory=dict) class ExternalToolDraftRequest(BaseModel): """系统级导入草稿中的工具请求体。字段对齐 ``ExternalToolCreateInput``。""" model_config = ConfigDict(frozen=True) slug: str = Field(..., min_length=1, max_length=128, pattern=r"^[a-zA-Z_][a-zA-Z0-9_-]{0,127}$") name: str = Field(..., min_length=1, max_length=128) description: str = Field(..., min_length=1) category: str = Field(default="default", max_length=64) adapter_type: str = Field(default="http", max_length=32) enabled: bool = Field(default=True) timeout: int = Field(default=30, ge=1, le=300) retry_policy: dict[str, Any] = Field(default_factory=dict) auth_type: str = Field(default="none", max_length=32) auth_config: dict[str, Any] = Field(default_factory=dict) adapter_config: dict[str, Any] = Field(default_factory=dict) class SystemImportDraftRequest(BaseModel): """系统导入草稿请求体。字段对齐 ``SystemImportDraft``(system + tools)。""" model_config = ConfigDict(frozen=True) system: ExternalSystemDraftRequest tools: list[ExternalToolDraftRequest] = Field(default_factory=list) class ImportConfirmRequest(BaseModel): """导入确认请求体。``draft`` 对齐 ``SystemImportDraft`` 结构(system + tools)。 ``created_by`` 由 Router 用 ``current_user.uid`` 填充,不暴露给客户端。 """ model_config = ConfigDict(frozen=True) draft: SystemImportDraftRequest # ---------------- 工具级导入 Request Schemas ---------------- class ToolImportPreviewRequest(BaseModel): """工具级导入预览请求体。字段对齐 ``ToolImportPreviewInput``。""" model_config = ConfigDict(frozen=True) adapter_type: str source_type: str payload: Any class ToolImportConfirmRequest(BaseModel): """工具级导入确认请求体。字段对齐 ``ToolImportConfirmInput``。 ``created_by`` 由 Router 用 ``current_user.uid`` 填充,不暴露给客户端。 """ model_config = ConfigDict(frozen=True) adapter_type: str = "" source_type: str = "" payload: Any = None drafts: list[dict[str, Any]] = Field(default_factory=list) selected_slugs: list[str] = Field(default_factory=list) override_existing: bool = False # ---------------- 工具导出 Request Schemas ---------------- class BulkExportToolsRequest(BaseModel): """批量工具导出请求体。字段对齐 ``BulkExportToolsInput``。 ``requester_role`` 由 Router 用 ``current_user.role`` 填充,不暴露给客户端。 """ model_config = ConfigDict(frozen=True) slugs: list[str] = Field(..., min_length=1) include_secrets: bool = False # ---------------- 工具包 Request Schemas ---------------- class ToolPackageAssetRequest(BaseModel): """工具包资产请求体。字段对齐 ``ToolPackageAssetInput``。""" model_config = ConfigDict(frozen=True) id: int | None = None adapter_type: str asset_type: str name: str content_b64: str = "" size: int = 0 checksum: str | None = None class ToolPackageItemRequest(BaseModel): """工具包条目请求体。字段对齐 ``ToolPackageItemInput``。 ``tool``/``system``/``environments`` 使用 ``dict[str, Any]`` 而非显式 Schema: 其结构对齐 Output DTO(``ToolOutput``/``SystemOutput``/``EnvironmentOutput``), 字段多且包含元数据(id 等),工具包导入为"导出-导入"闭环,前端回传的就是 导出端返回的 dict。定义 Request Schema 副本会造成大量字段重复且无业务价值, 结构校验由 ``_build_tool_package`` 中的 ``ToolOutput(**item.tool)`` 等 Pydantic 构造完成。``assets`` 使用显式 Schema 是因为 ``ToolPackageAssetInput`` 是专为导入定义的 Input DTO(含 ``content_b64``),字段少且独立。 """ model_config = ConfigDict(frozen=True) tool: dict[str, Any] system: dict[str, Any] | None = None environments: list[dict[str, Any]] = Field(default_factory=list) assets: list[ToolPackageAssetRequest] = Field(default_factory=list) class ToolPackageRequest(BaseModel): """工具包请求体。字段对齐 ``ToolPackageInput``。""" model_config = ConfigDict(frozen=True) items: list[ToolPackageItemRequest] = Field(default_factory=list) class ToolPackageExportRequest(BaseModel): """工具包导出请求体。字段对齐 ``ToolPackageExportInput``。 ``requester_role`` 由 Router 用 ``current_user.role`` 填充,不暴露给客户端。 """ model_config = ConfigDict(frozen=True) slugs: list[str] = Field(..., min_length=1) include_system: bool = True include_assets: bool = True include_secrets: bool = False class ToolPackageImportPreviewRequest(BaseModel): """工具包导入预览请求体。字段对齐 ``ToolPackageImportPreviewInput``。""" model_config = ConfigDict(frozen=True) package: ToolPackageRequest class ToolPackageImportConfirmRequest(BaseModel): """工具包导入确认请求体。字段对齐 ``ToolPackageImportInput``。 ``created_by`` 由 Router 用 ``current_user.uid`` 填充,不暴露给客户端。 """ model_config = ConfigDict(frozen=True) package: ToolPackageRequest override_existing: bool = False target_system_slug: str | None = None # ---------------- Endpoints ---------------- def _build_tool_package(package: ToolPackageRequest) -> ToolPackageInput: """将 Request Schema ``ToolPackageRequest`` 转换为 Input DTO ``ToolPackageInput``。 显式构造嵌套 DTO(``ToolPackageItemInput`` / ``ToolOutput`` / ``SystemOutput`` / ``EnvironmentOutput`` / ``ToolPackageAssetInput``),保持 Router 层 Schema ↔ DTO 不共享类的边界。 """ return ToolPackageInput( items=[ ToolPackageItemInput( tool=ToolOutput(**item.tool), system=SystemOutput(**item.system) if item.system else None, environments=[EnvironmentOutput(**env) for env in item.environments], assets=[ ToolPackageAssetInput( id=asset.id, adapter_type=asset.adapter_type, asset_type=asset.asset_type, name=asset.name, content_b64=asset.content_b64, size=asset.size, checksum=asset.checksum, ) for asset in item.assets ], ) for item in package.items ] ) @import_router.post("/preview", response_model=dict) async def preview_import( payload: ImportPreviewRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """生成系统级导入草稿(系统 + 工具),不执行持久化。""" use_cases = create_use_cases_from_db(db) input_dto = ImportPreviewInput( adapter_type=payload.adapter_type, source_type=payload.source_type, payload=payload.payload, asset_id=payload.asset_id, ) output = await use_cases.import_service.preview_import(input_dto) return {"success": True, "data": output.model_dump()} @import_router.post("/confirm", response_model=dict) async def confirm_import( payload: ImportConfirmRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """基于草稿执行系统级导入持久化。``created_by`` 取自当前管理员。""" use_cases = create_use_cases_from_db(db) draft = SystemImportDraft( system=ExternalSystemCreateInput(**payload.draft.system.model_dump()), tools=[ExternalToolCreateInput(**tool.model_dump()) for tool in payload.draft.tools], ) input_dto = ImportConfirmInput(draft=draft, created_by=current_user.uid) output = await use_cases.import_service.confirm_import(input_dto) return {"success": True, "data": output.model_dump()} # ---------------- 工具级导入端点 ---------------- @import_router.post("/tools/preview", response_model=dict) async def preview_tool_import( payload: ToolImportPreviewRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """工具级导入预览:根据适配器类型与源资产生成工具草稿列表(不落库)。""" use_cases = create_use_cases_from_db(db) input_dto = ToolImportPreviewInput( adapter_type=payload.adapter_type, source_type=payload.source_type, payload=payload.payload, ) output = await use_cases.import_service.preview_tool_import(input_dto) return {"success": True, "data": output.model_dump()} @import_router.post("/tools/confirm", response_model=dict) async def confirm_tool_import( payload: ToolImportConfirmRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """工具级导入确认:基于草稿执行工具持久化。``created_by`` 取自当前管理员。""" use_cases = create_use_cases_from_db(db) input_dto = ToolImportConfirmInput( adapter_type=payload.adapter_type, source_type=payload.source_type, payload=payload.payload, drafts=payload.drafts, selected_slugs=payload.selected_slugs, override_existing=payload.override_existing, created_by=current_user.uid, ) output = await use_cases.import_service.confirm_tool_import(input_dto) return {"success": True, "data": output.model_dump()} # ---------------- 工具导出端点 ---------------- @import_router.get("/tools/export", response_model=dict) async def export_tool( slug: str = Query(..., min_length=1, max_length=128), include_secrets: bool = Query(False), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """导出单个工具配置。``requester_role`` 取自当前用户角色。""" use_cases = create_use_cases_from_db(db) input_dto = ExportToolInput( slug=slug, include_secrets=include_secrets, requester_role=current_user.role, ) output = await use_cases.import_service.export_tool(input_dto) return {"success": True, "data": output.model_dump()} @import_router.post("/tools/bulk-export", response_model=dict) async def bulk_export_tools( payload: BulkExportToolsRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """批量导出工具配置。``requester_role`` 取自当前用户角色。""" use_cases = create_use_cases_from_db(db) input_dto = BulkExportToolsInput( slugs=payload.slugs, include_secrets=payload.include_secrets, requester_role=current_user.role, ) output = await use_cases.import_service.bulk_export_tools(input_dto) return {"success": True, "data": output.model_dump()} # ---------------- 工具包导入导出端点 ---------------- @import_router.post("/tool-package/export", response_model=dict) async def export_tool_package( payload: ToolPackageExportRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """导出工具包(含工具 + 系统 + 环境 + 资产)。``requester_role`` 取自当前用户角色。""" use_cases = create_use_cases_from_db(db) input_dto = ToolPackageExportInput( slugs=payload.slugs, include_system=payload.include_system, include_assets=payload.include_assets, include_secrets=payload.include_secrets, requester_role=current_user.role, ) output = await use_cases.import_service.export_tool_package(input_dto) return {"success": True, "data": output.model_dump()} @import_router.post("/tool-package/preview", response_model=dict) async def preview_tool_package_import( payload: ToolPackageImportPreviewRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """工具包导入预览:检查包中每个 tool/system/asset 是否与已有记录冲突(不落库)。""" use_cases = create_use_cases_from_db(db) package = _build_tool_package(payload.package) input_dto = ToolPackageImportPreviewInput(package=package) output = await use_cases.import_service.preview_tool_package_import(input_dto) return {"success": True, "data": output.model_dump()} @import_router.post("/tool-package/confirm", response_model=dict) async def confirm_tool_package_import( payload: ToolPackageImportConfirmRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """工具包导入确认:导入系统、环境、资产、工具,并重映射资产 ID。``created_by`` 取自当前管理员。""" use_cases = create_use_cases_from_db(db) package = _build_tool_package(payload.package) input_dto = ToolPackageImportInput( package=package, override_existing=payload.override_existing, target_system_slug=payload.target_system_slug, created_by=current_user.uid, ) output = await use_cases.import_service.confirm_tool_package_import(input_dto) return {"success": True, "data": output.model_dump()}