diff --git a/api/app/routers/tools.py b/api/app/routers/tools.py index 5c42fc3..3602ab6 100644 --- a/api/app/routers/tools.py +++ b/api/app/routers/tools.py @@ -103,17 +103,23 @@ TOOL_ICON_MAP = { "code_interpreter": "Terminal", } +def _seed_default_tools_if_empty(db: Session) -> None: + """Seed default tools into DB when tool_resources is empty.""" + if db.query(ToolResource).count() > 0: + return -def _builtin_tool_to_resource(tool_id: str, payload: Dict[str, Any]) -> Dict[str, Any]: - return { - "id": tool_id, - "name": payload.get("name", tool_id), - "description": payload.get("description", ""), - "category": TOOL_CATEGORY_MAP.get(tool_id, "system"), - "icon": TOOL_ICON_MAP.get(tool_id, "Wrench"), - "enabled": True, - "is_system": True, - } + for tool_id, payload in TOOL_REGISTRY.items(): + db.add(ToolResource( + id=tool_id, + user_id=1, + name=payload.get("name", tool_id), + description=payload.get("description", ""), + category=TOOL_CATEGORY_MAP.get(tool_id, "system"), + icon=TOOL_ICON_MAP.get(tool_id, "Wrench"), + enabled=True, + is_system=True, + )) + db.commit() @router.get("/list") @@ -140,49 +146,24 @@ def list_tool_resources( limit: int = 100, db: Session = Depends(get_db), ): - """获取工具资源列表(内置工具 + 自定义工具)。""" - merged: List[Dict[str, Any]] = [] - - if include_system: - for tool_id, payload in TOOL_REGISTRY.items(): - merged.append(_builtin_tool_to_resource(tool_id, payload)) - + """获取工具资源列表。system/query 仅表示工具执行类型,不代表权限。""" + _seed_default_tools_if_empty(db) query = db.query(ToolResource) + if not include_system: + query = query.filter(ToolResource.is_system == False) if category: query = query.filter(ToolResource.category == category) if enabled is not None: query = query.filter(ToolResource.enabled == enabled) - custom_tools = query.order_by(ToolResource.created_at.desc()).all() - - for item in custom_tools: - merged.append({ - "id": item.id, - "name": item.name, - "description": item.description, - "category": item.category, - "icon": item.icon, - "enabled": item.enabled, - "is_system": item.is_system, - }) - - if category: - merged = [item for item in merged if item.get("category") == category] - if enabled is not None: - merged = [item for item in merged if item.get("enabled") == enabled] - - total = len(merged) - start = max(page - 1, 0) * limit - end = start + limit - return {"total": total, "page": page, "limit": limit, "list": merged[start:end]} + total = query.count() + rows = query.order_by(ToolResource.created_at.desc()).offset(max(page - 1, 0) * limit).limit(limit).all() + return {"total": total, "page": page, "limit": limit, "list": rows} @router.get("/resources/{id}", response_model=ToolResourceOut) def get_tool_resource(id: str, db: Session = Depends(get_db)): """获取单个工具资源详情。""" - if id in TOOL_REGISTRY: - tool = _builtin_tool_to_resource(id, TOOL_REGISTRY[id]) - return ToolResourceOut(**tool) - + _seed_default_tools_if_empty(db) item = db.query(ToolResource).filter(ToolResource.id == id).first() if not item: raise HTTPException(status_code=404, detail="Tool resource not found") @@ -192,9 +173,10 @@ def get_tool_resource(id: str, db: Session = Depends(get_db)): @router.post("/resources", response_model=ToolResourceOut) def create_tool_resource(data: ToolResourceCreate, db: Session = Depends(get_db)): """创建自定义工具资源。""" + _seed_default_tools_if_empty(db) candidate_id = (data.id or "").strip() - if candidate_id and candidate_id in TOOL_REGISTRY: - raise HTTPException(status_code=400, detail="Tool ID conflicts with system tool") + if candidate_id and db.query(ToolResource).filter(ToolResource.id == candidate_id).first(): + raise HTTPException(status_code=400, detail="Tool ID already exists") item = ToolResource( id=candidate_id or f"tool_{str(uuid.uuid4())[:8]}", @@ -214,10 +196,8 @@ def create_tool_resource(data: ToolResourceCreate, db: Session = Depends(get_db) @router.put("/resources/{id}", response_model=ToolResourceOut) def update_tool_resource(id: str, data: ToolResourceUpdate, db: Session = Depends(get_db)): - """更新自定义工具资源。""" - if id in TOOL_REGISTRY: - raise HTTPException(status_code=400, detail="System tools are read-only") - + """更新工具资源。""" + _seed_default_tools_if_empty(db) item = db.query(ToolResource).filter(ToolResource.id == id).first() if not item: raise HTTPException(status_code=404, detail="Tool resource not found") @@ -234,10 +214,8 @@ def update_tool_resource(id: str, data: ToolResourceUpdate, db: Session = Depend @router.delete("/resources/{id}") def delete_tool_resource(id: str, db: Session = Depends(get_db)): - """删除自定义工具资源。""" - if id in TOOL_REGISTRY: - raise HTTPException(status_code=400, detail="System tools cannot be deleted") - + """删除工具资源。""" + _seed_default_tools_if_empty(db) item = db.query(ToolResource).filter(ToolResource.id == id).first() if not item: raise HTTPException(status_code=404, detail="Tool resource not found") diff --git a/api/tests/test_tools.py b/api/tests/test_tools.py index 85c8c1f..124b7c0 100644 --- a/api/tests/test_tools.py +++ b/api/tests/test_tools.py @@ -311,9 +311,17 @@ class TestToolResourceCRUD: missing_resp = client.get(f"/api/tools/resources/{tool_id}") assert missing_resp.status_code == 404 - def test_system_tool_is_read_only(self, client): - update_resp = client.put("/api/tools/resources/search", json={"name": "new"}) - assert update_resp.status_code == 400 + def test_system_tool_can_be_updated_and_deleted(self, client): + list_resp = client.get("/api/tools/resources") + assert list_resp.status_code == 200 + assert any(item["id"] == "search" for item in list_resp.json()["list"]) + + update_resp = client.put("/api/tools/resources/search", json={"name": "更新后的搜索工具", "category": "query"}) + assert update_resp.status_code == 200 + assert update_resp.json()["name"] == "更新后的搜索工具" delete_resp = client.delete("/api/tools/resources/search") - assert delete_resp.status_code == 400 + assert delete_resp.status_code == 200 + + get_resp = client.get("/api/tools/resources/search") + assert get_resp.status_code == 404 diff --git a/web/pages/ToolLibrary.tsx b/web/pages/ToolLibrary.tsx index 7665f9b..9cbc115 100644 --- a/web/pages/ToolLibrary.tsx +++ b/web/pages/ToolLibrary.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { Search, Filter, Plus, Wrench, Terminal, Globe, Camera, CameraOff, Image, Images, CloudSun, Calendar, TrendingUp, Coins, Trash2, Edit2, Box, Lock } from 'lucide-react'; +import { Search, Filter, Plus, Wrench, Terminal, Globe, Camera, CameraOff, Image, Images, CloudSun, Calendar, TrendingUp, Coins, Trash2, Edit2, Box } from 'lucide-react'; import { Button, Input, Badge, Dialog } from '../components/UI'; import { Tool } from '../types'; import { createTool, deleteTool, fetchTools, updateTool } from '../services/backendApi'; @@ -117,10 +117,6 @@ export const ToolLibraryPage: React.FC = () => { const handleDeleteTool = async (e: React.MouseEvent, tool: Tool) => { e.stopPropagation(); - if (tool.isSystem) { - alert('系统工具不可删除'); - return; - } if (!confirm('确认删除该工具吗?')) return; try { @@ -177,11 +173,7 @@ export const ToolLibraryPage: React.FC = () => {
{tool.name} - {tool.isSystem ? ( - SYSTEM - ) : ( - CUSTOM - )} + {tool.isSystem ? SYSTEM : CUSTOM}
@@ -194,38 +186,22 @@ export const ToolLibraryPage: React.FC = () => {
- {tool.isSystem ? ( - - 系统工具只读 - - ) : ( - 可编辑自定义工具 - )} + system/query 仅表示执行类型