ForcePilot/backend/server/routers/external_systems/import_router.py
Kris ba798a9bb5 feat(routers): add external systems management module and its sub routers
新增外部系统限界上下文的完整路由体系,包含聚合路由层与23个子领域路由,覆盖外部系统全生命周期管理、健康检查、指标监控、审计日志、Token管理、通知渠道、回收站等功能,并将外部系统路由挂载到全局API前缀下。
2026-06-20 22:16:07 +08:00

467 lines
17 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.

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