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.
This commit is contained in:
@@ -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:
|
async def _commit(session: AsyncSession, tool: Tool) -> ToolOut:
|
||||||
try:
|
try:
|
||||||
await session.commit()
|
await session.commit()
|
||||||
@@ -99,6 +106,33 @@ async def update_tool(
|
|||||||
return await _commit(session, 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}")
|
@router.delete("/{tool_id}")
|
||||||
async def delete_tool(tool_id: str, session: AsyncSession = Depends(get_session)):
|
async def delete_tool(tool_id: str, session: AsyncSession = Depends(get_session)):
|
||||||
tool = await session.get(Tool, tool_id)
|
tool = await session.get(Tool, tool_id)
|
||||||
|
|||||||
@@ -448,10 +448,11 @@ export function ComponentsModelsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function remove(id: string) {
|
async function remove(resource: ModelResource) {
|
||||||
setDeletingId(id);
|
if (!window.confirm(`确认删除模型“${resource.name}”?`)) return;
|
||||||
|
setDeletingId(resource.id);
|
||||||
try {
|
try {
|
||||||
await modelResourcesApi.remove(id);
|
await modelResourcesApi.remove(resource.id);
|
||||||
await load();
|
await load();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setLoadError(error instanceof Error ? error.message : "删除失败");
|
setLoadError(error instanceof Error ? error.message : "删除失败");
|
||||||
@@ -663,7 +664,7 @@ export function ComponentsModelsPage() {
|
|||||||
disabled={deletingId === resource.id}
|
disabled={deletingId === resource.id}
|
||||||
onSelect={(event) => {
|
onSelect={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
void remove(resource.id);
|
void remove(resource);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{deletingId === resource.id ? (
|
{deletingId === resource.id ? (
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
|
Copy,
|
||||||
Loader2,
|
Loader2,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
Pencil,
|
Pencil,
|
||||||
@@ -243,6 +244,7 @@ export function ComponentsToolsPage() {
|
|||||||
const [formError, setFormError] = useState<string | null>(null);
|
const [formError, setFormError] = useState<string | null>(null);
|
||||||
const [functionNameTouched, setFunctionNameTouched] = useState(false);
|
const [functionNameTouched, setFunctionNameTouched] = useState(false);
|
||||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||||
|
const [duplicatingId, setDuplicatingId] = useState<string | null>(null);
|
||||||
|
|
||||||
const loadTools = useCallback(async () => {
|
const loadTools = useCallback(async () => {
|
||||||
setLoading(true);
|
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<Tool>[] = [
|
const columns: DataListColumn<Tool>[] = [
|
||||||
{
|
{
|
||||||
key: "name",
|
key: "name",
|
||||||
@@ -435,6 +449,21 @@ export function ComponentsToolsPage() {
|
|||||||
align="end"
|
align="end"
|
||||||
className="w-32 min-w-32 rounded-xl border border-hairline bg-popover p-1"
|
className="w-32 min-w-32 rounded-xl border border-hairline bg-popover p-1"
|
||||||
>
|
>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="rounded-lg"
|
||||||
|
disabled={duplicatingId === tool.id}
|
||||||
|
onSelect={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
void duplicateTool(tool.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{duplicatingId === tool.id ? (
|
||||||
|
<Loader2 size={14} className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Copy size={14} />
|
||||||
|
)}
|
||||||
|
复制
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
className="rounded-lg"
|
className="rounded-lg"
|
||||||
|
|||||||
@@ -314,6 +314,8 @@ export const toolsApi = {
|
|||||||
}),
|
}),
|
||||||
remove: (id: string) =>
|
remove: (id: string) =>
|
||||||
request<{ ok: boolean }>(`/api/tools/${id}`, { method: "DELETE" }),
|
request<{ ok: boolean }>(`/api/tools/${id}`, { method: "DELETE" }),
|
||||||
|
duplicate: (id: string) =>
|
||||||
|
request<Tool>(`/api/tools/${id}/duplicate`, { method: "POST" }),
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------- 知识库 ----------
|
// ---------- 知识库 ----------
|
||||||
|
|||||||
Reference in New Issue
Block a user