From 8c0358ad91805d9294419b20eb9a7b5670bdeec5 Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Fri, 10 Jul 2026 16:45:51 +0800 Subject: [PATCH] Add tool duplication functionality and enhance ComponentsToolsPage - Implement a new API endpoint for duplicating tools in the backend. - Add a duplicate_tool function in ComponentsToolsPage to handle tool duplication requests. - Update the UI to include a dropdown menu option for duplicating tools, improving user experience. - Refactor the remove function to accept a resource object instead of an ID for better clarity and maintainability. --- backend/routes/tools.py | 34 +++++++++++++++++++ .../components/pages/ComponentsModelsPage.tsx | 9 ++--- .../components/pages/ComponentsToolsPage.tsx | 29 ++++++++++++++++ frontend/src/lib/api.ts | 2 ++ 4 files changed, 70 insertions(+), 4 deletions(-) diff --git a/backend/routes/tools.py b/backend/routes/tools.py index 6e90585..4af1cfd 100644 --- a/backend/routes/tools.py +++ b/backend/routes/tools.py @@ -52,6 +52,13 @@ def _payload(body: ToolUpsert, stored_secrets: dict | None = None) -> dict: } +def _duplicate_function_name(source: str) -> str: + candidate = f"{source}_copy" + if len(candidate) <= 64: + return candidate + return f"{source[:57]}_copy" + + async def _commit(session: AsyncSession, tool: Tool) -> ToolOut: try: await session.commit() @@ -99,6 +106,33 @@ async def update_tool( return await _commit(session, tool) +@router.post("/{tool_id}/duplicate", response_model=ToolOut) +async def duplicate_tool(tool_id: str, session: AsyncSession = Depends(get_session)): + source = await session.get(Tool, tool_id) + if not source: + raise HTTPException(404, "工具不存在") + function_name = _duplicate_function_name(source.function_name) + existing = ( + await session.execute(select(Tool).where(Tool.function_name == function_name)) + ).scalar_one_or_none() + if existing: + suffix = uuid.uuid4().hex[:6] + max_base = 64 - len(suffix) - 1 + function_name = f"{source.function_name[:max_base]}_{suffix}" + tool = Tool( + id=f"tool_{uuid.uuid4().hex[:12]}", + name=f"{source.name} 副本", + function_name=function_name, + type=source.type, + description=source.description, + definition=dict(source.definition or {}), + secrets=dict(source.secrets or {}), + status=source.status, + ) + session.add(tool) + return await _commit(session, tool) + + @router.delete("/{tool_id}") async def delete_tool(tool_id: str, session: AsyncSession = Depends(get_session)): tool = await session.get(Tool, tool_id) diff --git a/frontend/src/components/pages/ComponentsModelsPage.tsx b/frontend/src/components/pages/ComponentsModelsPage.tsx index 4c5c3a9..dfa6ece 100644 --- a/frontend/src/components/pages/ComponentsModelsPage.tsx +++ b/frontend/src/components/pages/ComponentsModelsPage.tsx @@ -448,10 +448,11 @@ export function ComponentsModelsPage() { } } - async function remove(id: string) { - setDeletingId(id); + async function remove(resource: ModelResource) { + if (!window.confirm(`确认删除模型“${resource.name}”?`)) return; + setDeletingId(resource.id); try { - await modelResourcesApi.remove(id); + await modelResourcesApi.remove(resource.id); await load(); } catch (error) { setLoadError(error instanceof Error ? error.message : "删除失败"); @@ -663,7 +664,7 @@ export function ComponentsModelsPage() { disabled={deletingId === resource.id} onSelect={(event) => { event.preventDefault(); - void remove(resource.id); + void remove(resource); }} > {deletingId === resource.id ? ( diff --git a/frontend/src/components/pages/ComponentsToolsPage.tsx b/frontend/src/components/pages/ComponentsToolsPage.tsx index d535ca3..d6b2123 100644 --- a/frontend/src/components/pages/ComponentsToolsPage.tsx +++ b/frontend/src/components/pages/ComponentsToolsPage.tsx @@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { ChevronDown, ChevronUp, + Copy, Loader2, MoreHorizontal, Pencil, @@ -243,6 +244,7 @@ export function ComponentsToolsPage() { const [formError, setFormError] = useState(null); const [functionNameTouched, setFunctionNameTouched] = useState(false); const [deletingId, setDeletingId] = useState(null); + const [duplicatingId, setDuplicatingId] = useState(null); const loadTools = useCallback(async () => { setLoading(true); @@ -345,6 +347,18 @@ export function ComponentsToolsPage() { } } + async function duplicateTool(id: string) { + setDuplicatingId(id); + try { + await toolsApi.duplicate(id); + await loadTools(); + } catch (duplicateError) { + setError(duplicateError instanceof Error ? duplicateError.message : "复制失败"); + } finally { + setDuplicatingId(null); + } + } + const columns: DataListColumn[] = [ { key: "name", @@ -435,6 +449,21 @@ export function ComponentsToolsPage() { align="end" className="w-32 min-w-32 rounded-xl border border-hairline bg-popover p-1" > + { + event.preventDefault(); + void duplicateTool(tool.id); + }} + > + {duplicatingId === tool.id ? ( + + ) : ( + + )} + 复制 + request<{ ok: boolean }>(`/api/tools/${id}`, { method: "DELETE" }), + duplicate: (id: string) => + request(`/api/tools/${id}/duplicate`, { method: "POST" }), }; // ---------- 知识库 ----------