ForcePilot/backend/server/routers/external_systems/import_router.py

467 lines
17 KiB
Python
Raw Normal View History

"""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()}