652 lines
24 KiB
Python
652 lines
24 KiB
Python
"""外部系统限界上下文 - Tool 子域 Router。
|
||
|
||
挂载到聚合 router 的 /tools 前缀下,覆盖工具 CRUD / 批量导入 / 克隆 / 版本 /
|
||
导入导出 / WSDL 导入导出 / 执行入口 / ``get_enabled_tools`` / ``list_tool_options``,
|
||
以及 tool_router 扩展端点(版本对比 / 版本回滚 / 参数模板 / 批量执行 /
|
||
批量克隆 / 执行趋势 / 依赖资产)。Request Schema 与 Input DTO 不共享类,
|
||
Router 内显式构造 DTO,操作人字段由 current_user.uid 填充。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from datetime import UTC, datetime
|
||
from typing import Any, Literal
|
||
|
||
from fastapi import APIRouter, Depends, Query
|
||
from fastapi.responses import StreamingResponse
|
||
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.tool import (
|
||
BatchCloneToolsInput,
|
||
BatchExecuteToolItem,
|
||
BatchExecuteToolsInput,
|
||
BatchImportToolsInput,
|
||
CloneToolInput,
|
||
CompareToolVersionsInput,
|
||
CreateToolInput,
|
||
DeleteToolInput,
|
||
ExecuteToolInput,
|
||
ExportToolsInput,
|
||
ExportWsdlInput,
|
||
ExternalToolCreateInput,
|
||
GetEnabledToolsInput,
|
||
GetToolAssetsInput,
|
||
GetToolExecutionTrendInput,
|
||
GetToolInput,
|
||
GetToolParameterTemplateInput,
|
||
GetToolVersionsInput,
|
||
HealthCheckToolInput,
|
||
ImportWsdlInput,
|
||
ListToolOptionsInput,
|
||
ListToolsInput,
|
||
RollbackToolVersionInput,
|
||
UpdateToolInput,
|
||
)
|
||
from yuxi.storage.postgres.models_business import User
|
||
|
||
from server.utils.auth_middleware import get_admin_user, get_db, get_required_user
|
||
|
||
tool_router = APIRouter(prefix="/tools", tags=["external-systems-tool"])
|
||
|
||
|
||
# =============================================================================
|
||
# === Request Schemas(与 Input DTO 不共享类) ===
|
||
# =============================================================================
|
||
|
||
|
||
class CreateToolRequest(BaseModel):
|
||
"""创建工具请求体。字段对齐 ``CreateToolInput``(不含 created_by)。"""
|
||
|
||
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 = ""
|
||
category: str = Field(default="default", max_length=64)
|
||
adapter_type: str = Field(default="http", max_length=32)
|
||
auth_type: str = Field(default="none", max_length=32)
|
||
adapter_config: dict[str, Any] = Field(default_factory=dict)
|
||
auth_config: dict[str, Any] | None = None
|
||
timeout: int = Field(default=30, ge=1, le=300)
|
||
retry_policy: dict[str, Any] = Field(default_factory=dict)
|
||
enabled: bool = True
|
||
system_id: int | None = None
|
||
|
||
|
||
class UpdateToolRequest(BaseModel):
|
||
"""更新工具请求体。字段对齐 ``UpdateToolInput``(不含 id 与 updated_by)。
|
||
|
||
字段长度约束对齐 ``CreateToolRequest``,仅透传客户端显式设置的字段
|
||
(通过 ``exclude_unset=True``),未设置字段保持 ``None`` 以保留部分更新语义。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
name: str | None = Field(default=None, min_length=1, max_length=128)
|
||
description: str | None = None
|
||
category: str | None = Field(default=None, max_length=64)
|
||
adapter_type: str | None = Field(default=None, max_length=32)
|
||
auth_type: str | None = Field(default=None, max_length=32)
|
||
adapter_config: dict[str, Any] | None = None
|
||
auth_config: dict[str, Any] | None = None
|
||
timeout: int | None = Field(default=None, ge=1, le=300)
|
||
retry_policy: dict[str, Any] | None = None
|
||
enabled: bool | None = None
|
||
system_id: int | None = None
|
||
|
||
|
||
class CloneToolRequest(BaseModel):
|
||
"""克隆工具请求体。字段对齐 ``CloneToolInput``(不含 id 与 created_by)。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
new_slug: str = Field(..., min_length=1, max_length=128, pattern=r"^[a-zA-Z_][a-zA-Z0-9_-]{0,127}$")
|
||
new_name: str | None = Field(default=None, min_length=1, max_length=128)
|
||
system_id: int | None = None
|
||
|
||
|
||
class ExternalToolItemRequest(BaseModel):
|
||
"""外部工具项请求体。字段对齐 ``ExternalToolCreateInput``。
|
||
|
||
用于批量导入工具(``BatchImportToolsRequest.tools``)。
|
||
"""
|
||
|
||
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 = 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 BatchImportToolsRequest(BaseModel):
|
||
"""批量导入工具请求体。字段对齐 ``BatchImportToolsInput``。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
system_id: int | None = None
|
||
tools: list[ExternalToolItemRequest] = Field(default_factory=list)
|
||
|
||
|
||
class ImportWsdlRequest(BaseModel):
|
||
"""导入 WSDL 请求体。字段对齐 ``ImportWsdlInput``(不含 created_by)。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
wsdl_url: str | None = None
|
||
wsdl_content: str | None = None
|
||
system_id: int | None = None
|
||
|
||
|
||
class ExecuteToolRequest(BaseModel):
|
||
"""执行工具请求体。字段对齐 ``ExecuteToolInput``(不含 caller,由服务端填充)。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
tool_slug: str
|
||
system_id: int | None = None
|
||
env_key: str = "default"
|
||
arguments: dict[str, Any] = Field(default_factory=dict)
|
||
caller_id: str | None = None
|
||
correlation_id: str | None = None
|
||
tags: dict[str, Any] = Field(default_factory=dict)
|
||
|
||
|
||
class HealthCheckToolRequest(BaseModel):
|
||
"""触发工具健康检查请求体。字段对齐 ``HealthCheckToolInput``。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
env_key: str | None = Field(default=None, max_length=32)
|
||
|
||
|
||
class BatchExecuteToolItemRequest(BaseModel):
|
||
"""批量执行工具项请求体。字段对齐 ``BatchExecuteToolItem``。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
tool_slug: str
|
||
system_id: int | None = None
|
||
env_key: str = "default"
|
||
arguments: dict[str, Any] = Field(default_factory=dict)
|
||
caller_id: str | None = None
|
||
correlation_id: str | None = None
|
||
tags: dict[str, Any] = Field(default_factory=dict)
|
||
|
||
|
||
class BatchExecuteToolsRequest(BaseModel):
|
||
"""批量执行工具请求体。字段对齐 ``BatchExecuteToolsInput``(不含 caller,由服务端填充)。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
executions: list[BatchExecuteToolItemRequest] = Field(default_factory=list)
|
||
|
||
|
||
class BatchCloneToolsRequest(BaseModel):
|
||
"""批量克隆工具请求体。字段对齐 ``BatchCloneToolsInput``(不含 created_by,由服务端填充)。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
source_tool_ids: list[int]
|
||
slug_suffix: str = Field(..., min_length=1, max_length=64)
|
||
override_config: dict[str, Any] = Field(default_factory=dict)
|
||
|
||
|
||
# =============================================================================
|
||
# === 静态路径端点(必须在 /{tool_id} 之前声明) ===
|
||
# =============================================================================
|
||
|
||
|
||
@tool_router.get("/enabled", response_model=dict)
|
||
async def get_enabled_tools(
|
||
system_id: int | None = Query(None),
|
||
env_key: str | None = Query(None),
|
||
category: str | None = Query(None),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""获取已启用工具列表(供 agents 模块使用)。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = GetEnabledToolsInput(system_id=system_id, env_key=env_key, category=category)
|
||
output = await use_cases.tool_service.get_enabled_tools(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@tool_router.get("/options", response_model=dict)
|
||
async def list_tool_options(
|
||
system_id: int | None = Query(None),
|
||
category: str | None = Query(None),
|
||
enabled_only: bool = Query(True),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""列出工具选项(供下拉选择使用)。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = ListToolOptionsInput(
|
||
system_id=system_id,
|
||
category=category,
|
||
enabled_only=enabled_only,
|
||
)
|
||
output = await use_cases.tool_service.list_tool_options(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@tool_router.get("/export")
|
||
async def export_tools(
|
||
system_id: int | None = Query(None),
|
||
ids: list[int] | None = Query(None),
|
||
category: str | None = Query(None),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> StreamingResponse:
|
||
"""导出工具列表为 JSON 字节流。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = ExportToolsInput(system_id=system_id, ids=ids, category=category)
|
||
output = await use_cases.tool_service.export_tools(input_dto)
|
||
|
||
payload = json.dumps(
|
||
[item.model_dump() for item in output.items],
|
||
ensure_ascii=False,
|
||
default=str,
|
||
).encode("utf-8")
|
||
timestamp = datetime.now(UTC).strftime("%Y%m%d%H%M%S")
|
||
|
||
async def _stream():
|
||
yield payload
|
||
|
||
return StreamingResponse(
|
||
_stream(),
|
||
media_type="application/octet-stream",
|
||
headers={"Content-Disposition": f"attachment; filename=tools_{timestamp}.json"},
|
||
)
|
||
|
||
|
||
@tool_router.get("/export-wsdl", response_model=dict)
|
||
async def export_wsdl(
|
||
system_id: int = Query(..., description="目标外部系统 ID"),
|
||
tool_slug: str | None = Query(None, description="指定工具 slug,未指定则取系统下首个 SOAP 工具"),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""导出 WSDL 内容。
|
||
|
||
从指定系统下的 SOAP 工具提取 ``wsdl_content``;若工具仅存储 ``wsdl_url``,
|
||
则通过 SOAP 适配器加载后返回。``system_id`` 为必填,``tool_slug`` 可选。
|
||
"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = ExportWsdlInput(system_id=system_id, tool_slug=tool_slug)
|
||
output = await use_cases.tool_service.export_wsdl(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@tool_router.post("/import", response_model=dict)
|
||
async def batch_import_tools(
|
||
body: BatchImportToolsRequest,
|
||
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 = BatchImportToolsInput(
|
||
system_id=body.system_id,
|
||
tools=[ExternalToolCreateInput(**item.model_dump()) for item in body.tools],
|
||
created_by=current_user.uid,
|
||
)
|
||
output = await use_cases.tool_service.batch_import_tools(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@tool_router.post("/import-from-wsdl", response_model=dict)
|
||
async def import_wsdl(
|
||
body: ImportWsdlRequest,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""从 WSDL 导入工具。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = ImportWsdlInput(
|
||
wsdl_url=body.wsdl_url,
|
||
wsdl_content=body.wsdl_content,
|
||
system_id=body.system_id,
|
||
created_by=current_user.uid,
|
||
)
|
||
output = await use_cases.tool_service.import_wsdl(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@tool_router.post("/execute", response_model=dict)
|
||
async def execute_external_tool(
|
||
body: ExecuteToolRequest,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""执行外部工具。caller 由服务端以 current_user.uid 填充。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = ExecuteToolInput(
|
||
tool_slug=body.tool_slug,
|
||
system_id=body.system_id,
|
||
env_key=body.env_key,
|
||
arguments=body.arguments,
|
||
caller=current_user.uid,
|
||
caller_id=body.caller_id,
|
||
correlation_id=body.correlation_id,
|
||
tags=body.tags,
|
||
)
|
||
output = await use_cases.tool_service.execute_external_tool(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@tool_router.post("/{tool_slug}/health-check", response_model=dict)
|
||
async def health_check_external_tool(
|
||
tool_slug: str,
|
||
body: HealthCheckToolRequest | None = None,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""触发单个外部工具健康检查。
|
||
|
||
仅对 system_id 不为 None 的系统级工具持久化结果到 ``ext_system_health_checks``;
|
||
工具级工具仅返回探测结果。
|
||
"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = HealthCheckToolInput(
|
||
tool_slug=tool_slug,
|
||
env_key=body.env_key if body is not None else None,
|
||
)
|
||
output = await use_cases.tool_service.health_check_external_tool(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@tool_router.post("/batch-execute", response_model=dict)
|
||
async def batch_execute_tools(
|
||
body: BatchExecuteToolsRequest,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""批量执行工具。caller 由服务端以 current_user.uid 填充。
|
||
|
||
单个执行项失败时不中断批量执行:执行器内部失败返回 ``status="failed"``,
|
||
前置校验失败(如工具不存在)抛出的异常由 service 层 try/except 捕获并记录错误。
|
||
"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = BatchExecuteToolsInput(
|
||
executions=[BatchExecuteToolItem(**item.model_dump()) for item in body.executions],
|
||
caller=current_user.uid,
|
||
)
|
||
output = await use_cases.tool_service.batch_execute_tools(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@tool_router.post("/batch-clone", response_model=dict)
|
||
async def batch_clone_tools(
|
||
body: BatchCloneToolsRequest,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""批量克隆工具。created_by 由服务端以 current_user.uid 填充。
|
||
|
||
单个事务内执行整个循环,单个失败时记录错误并继续
|
||
(参考 ``batch_import_tools`` 模式)。
|
||
"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = BatchCloneToolsInput(
|
||
source_tool_ids=body.source_tool_ids,
|
||
slug_suffix=body.slug_suffix,
|
||
override_config=body.override_config,
|
||
created_by=current_user.uid,
|
||
)
|
||
output = await use_cases.tool_service.batch_clone_tools(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
# =============================================================================
|
||
# === 根路径端点 ===
|
||
# =============================================================================
|
||
|
||
|
||
@tool_router.get("", response_model=dict)
|
||
async def list_tools(
|
||
limit: int = Query(100, ge=1, le=500),
|
||
offset: int = Query(0, ge=0),
|
||
system_id: int | None = Query(None),
|
||
category: str | None = Query(None),
|
||
adapter_type: str | None = Query(None),
|
||
enabled: bool | None = Query(None),
|
||
keyword: str | None = Query(None),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""分页列出外部工具。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = ListToolsInput(
|
||
page=offset // limit + 1,
|
||
page_size=limit,
|
||
system_id=system_id,
|
||
category=category,
|
||
adapter_type=adapter_type,
|
||
enabled=enabled,
|
||
keyword=keyword,
|
||
)
|
||
output = await use_cases.tool_service.list_tools(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@tool_router.post("", response_model=dict)
|
||
async def create_tool(
|
||
body: CreateToolRequest,
|
||
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 = CreateToolInput(**body.model_dump(), created_by=current_user.uid)
|
||
output = await use_cases.tool_service.create_tool(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
# =============================================================================
|
||
# === 动态路径端点(/{tool_id} 与 /{tool_id}/versions) ===
|
||
# =============================================================================
|
||
|
||
|
||
@tool_router.get("/{tool_id}", response_model=dict)
|
||
async def get_tool(
|
||
tool_id: int,
|
||
system_id: int | None = Query(None),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""获取工具详情。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = GetToolInput(id=tool_id, system_id=system_id)
|
||
output = await use_cases.tool_service.get_tool(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@tool_router.put("/{tool_id}", response_model=dict)
|
||
async def update_tool(
|
||
tool_id: int,
|
||
body: UpdateToolRequest,
|
||
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 = UpdateToolInput(
|
||
**body.model_dump(exclude_unset=True),
|
||
id=tool_id,
|
||
updated_by=current_user.uid,
|
||
)
|
||
output = await use_cases.tool_service.update_tool(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@tool_router.delete("/{tool_id}", response_model=dict)
|
||
async def delete_tool(
|
||
tool_id: int,
|
||
system_id: int | None = Query(None),
|
||
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 = DeleteToolInput(id=tool_id, system_id=system_id, deleted_by=current_user.uid)
|
||
output = await use_cases.tool_service.delete_tool(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@tool_router.post("/{tool_id}/clone", response_model=dict)
|
||
async def clone_tool(
|
||
tool_id: int,
|
||
body: CloneToolRequest,
|
||
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 = CloneToolInput(
|
||
id=tool_id,
|
||
new_slug=body.new_slug,
|
||
new_name=body.new_name,
|
||
system_id=body.system_id,
|
||
created_by=current_user.uid,
|
||
)
|
||
output = await use_cases.tool_service.clone_tool(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@tool_router.get("/{tool_id}/versions", response_model=dict)
|
||
async def get_tool_versions(
|
||
tool_id: int,
|
||
limit: int = Query(100, ge=1, le=500),
|
||
offset: int = Query(0, ge=0),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""获取工具版本列表。路径参数为 tool_id,service 层内部解析为 slug 查询。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = GetToolVersionsInput(
|
||
tool_id=tool_id,
|
||
page=offset // limit + 1,
|
||
page_size=limit,
|
||
)
|
||
output = await use_cases.tool_service.get_tool_versions(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
# -----------------------------------------------------------------------------
|
||
# === tool_router 扩展端点(动态路径) ===
|
||
# -----------------------------------------------------------------------------
|
||
|
||
|
||
@tool_router.get("/{tool_id}/versions/{version_id_1}/compare/{version_id_2}", response_model=dict)
|
||
async def compare_tool_versions(
|
||
tool_id: int,
|
||
version_id_1: str,
|
||
version_id_2: str,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""对比两个工具版本的参数差异。
|
||
|
||
snapshot 以明文存储,service 层使用 ``mask_all_string_values``
|
||
脱敏后对比结构差异,不泄露敏感信息。
|
||
"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = CompareToolVersionsInput(
|
||
tool_id=tool_id,
|
||
version_id_1=version_id_1,
|
||
version_id_2=version_id_2,
|
||
)
|
||
output = await use_cases.tool_service.compare_tool_versions(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@tool_router.post("/{tool_id}/versions/{version_id}/rollback", response_model=dict)
|
||
async def rollback_tool_version(
|
||
tool_id: int,
|
||
version_id: str,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""将工具回滚到指定历史版本。updated_by 由服务端以 current_user.uid 填充。
|
||
|
||
回滚前创建当前配置的新版本快照,记录回滚前的状态。
|
||
snapshot 以明文存储,service 层通过 persistence 层的
|
||
``get_decrypted_snapshot`` 方法获取明文后传给 ``ToolRepository.update``。
|
||
"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = RollbackToolVersionInput(
|
||
tool_id=tool_id,
|
||
version_id=version_id,
|
||
updated_by=current_user.uid,
|
||
)
|
||
output = await use_cases.tool_service.rollback_tool_version(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@tool_router.get("/{tool_id}/parameter-template", response_model=dict)
|
||
async def get_tool_parameter_template(
|
||
tool_id: int,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""获取工具参数模板。
|
||
|
||
基于 ``AdapterMetadata.to_json_schema()`` + ``example_config`` 生成,
|
||
供前端动态表单渲染。
|
||
"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = GetToolParameterTemplateInput(tool_id=tool_id)
|
||
output = await use_cases.tool_service.get_tool_parameter_template(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@tool_router.get("/{tool_id}/execution-trend", response_model=dict)
|
||
async def get_tool_execution_trend(
|
||
tool_id: int,
|
||
start_time: datetime | None = Query(None, description="开始时间(ISO 8601)"),
|
||
end_time: datetime | None = Query(None, description="结束时间(ISO 8601)"),
|
||
interval: Literal["hour", "day"] = Query("day", description="时间桶粒度(hour/day)"),
|
||
env_key: str | None = Query(None, description="环境 key 过滤"),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""查询工具执行历史趋势(按天/小时聚合)。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = GetToolExecutionTrendInput(
|
||
tool_id=tool_id,
|
||
start_time=start_time,
|
||
end_time=end_time,
|
||
interval=interval,
|
||
env_key=env_key,
|
||
)
|
||
output = await use_cases.tool_service.get_tool_execution_trend(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@tool_router.get("/{tool_id}/assets", response_model=dict)
|
||
async def get_tool_assets(
|
||
tool_id: int,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""查询工具依赖的适配器资产。
|
||
|
||
工具与资产的关联关系通过 ``adapter_config`` 中的引用建立,
|
||
service 层按 checksum → asset_name → adapter_type 顺序解析。
|
||
"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = GetToolAssetsInput(tool_id=tool_id)
|
||
output = await use_cases.tool_service.get_tool_assets(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|