Update tool panel and db
This commit is contained in:
@@ -103,17 +103,23 @@ TOOL_ICON_MAP = {
|
|||||||
"code_interpreter": "Terminal",
|
"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]:
|
for tool_id, payload in TOOL_REGISTRY.items():
|
||||||
return {
|
db.add(ToolResource(
|
||||||
"id": tool_id,
|
id=tool_id,
|
||||||
"name": payload.get("name", tool_id),
|
user_id=1,
|
||||||
"description": payload.get("description", ""),
|
name=payload.get("name", tool_id),
|
||||||
"category": TOOL_CATEGORY_MAP.get(tool_id, "system"),
|
description=payload.get("description", ""),
|
||||||
"icon": TOOL_ICON_MAP.get(tool_id, "Wrench"),
|
category=TOOL_CATEGORY_MAP.get(tool_id, "system"),
|
||||||
"enabled": True,
|
icon=TOOL_ICON_MAP.get(tool_id, "Wrench"),
|
||||||
"is_system": True,
|
enabled=True,
|
||||||
}
|
is_system=True,
|
||||||
|
))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/list")
|
@router.get("/list")
|
||||||
@@ -140,49 +146,24 @@ def list_tool_resources(
|
|||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""获取工具资源列表(内置工具 + 自定义工具)。"""
|
"""获取工具资源列表。system/query 仅表示工具执行类型,不代表权限。"""
|
||||||
merged: List[Dict[str, Any]] = []
|
_seed_default_tools_if_empty(db)
|
||||||
|
|
||||||
if include_system:
|
|
||||||
for tool_id, payload in TOOL_REGISTRY.items():
|
|
||||||
merged.append(_builtin_tool_to_resource(tool_id, payload))
|
|
||||||
|
|
||||||
query = db.query(ToolResource)
|
query = db.query(ToolResource)
|
||||||
|
if not include_system:
|
||||||
|
query = query.filter(ToolResource.is_system == False)
|
||||||
if category:
|
if category:
|
||||||
query = query.filter(ToolResource.category == category)
|
query = query.filter(ToolResource.category == category)
|
||||||
if enabled is not None:
|
if enabled is not None:
|
||||||
query = query.filter(ToolResource.enabled == enabled)
|
query = query.filter(ToolResource.enabled == enabled)
|
||||||
custom_tools = query.order_by(ToolResource.created_at.desc()).all()
|
total = query.count()
|
||||||
|
rows = query.order_by(ToolResource.created_at.desc()).offset(max(page - 1, 0) * limit).limit(limit).all()
|
||||||
for item in custom_tools:
|
return {"total": total, "page": page, "limit": limit, "list": rows}
|
||||||
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]}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/resources/{id}", response_model=ToolResourceOut)
|
@router.get("/resources/{id}", response_model=ToolResourceOut)
|
||||||
def get_tool_resource(id: str, db: Session = Depends(get_db)):
|
def get_tool_resource(id: str, db: Session = Depends(get_db)):
|
||||||
"""获取单个工具资源详情。"""
|
"""获取单个工具资源详情。"""
|
||||||
if id in TOOL_REGISTRY:
|
_seed_default_tools_if_empty(db)
|
||||||
tool = _builtin_tool_to_resource(id, TOOL_REGISTRY[id])
|
|
||||||
return ToolResourceOut(**tool)
|
|
||||||
|
|
||||||
item = db.query(ToolResource).filter(ToolResource.id == id).first()
|
item = db.query(ToolResource).filter(ToolResource.id == id).first()
|
||||||
if not item:
|
if not item:
|
||||||
raise HTTPException(status_code=404, detail="Tool resource not found")
|
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)
|
@router.post("/resources", response_model=ToolResourceOut)
|
||||||
def create_tool_resource(data: ToolResourceCreate, db: Session = Depends(get_db)):
|
def create_tool_resource(data: ToolResourceCreate, db: Session = Depends(get_db)):
|
||||||
"""创建自定义工具资源。"""
|
"""创建自定义工具资源。"""
|
||||||
|
_seed_default_tools_if_empty(db)
|
||||||
candidate_id = (data.id or "").strip()
|
candidate_id = (data.id or "").strip()
|
||||||
if candidate_id and candidate_id in TOOL_REGISTRY:
|
if candidate_id and db.query(ToolResource).filter(ToolResource.id == candidate_id).first():
|
||||||
raise HTTPException(status_code=400, detail="Tool ID conflicts with system tool")
|
raise HTTPException(status_code=400, detail="Tool ID already exists")
|
||||||
|
|
||||||
item = ToolResource(
|
item = ToolResource(
|
||||||
id=candidate_id or f"tool_{str(uuid.uuid4())[:8]}",
|
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)
|
@router.put("/resources/{id}", response_model=ToolResourceOut)
|
||||||
def update_tool_resource(id: str, data: ToolResourceUpdate, db: Session = Depends(get_db)):
|
def update_tool_resource(id: str, data: ToolResourceUpdate, db: Session = Depends(get_db)):
|
||||||
"""更新自定义工具资源。"""
|
"""更新工具资源。"""
|
||||||
if id in TOOL_REGISTRY:
|
_seed_default_tools_if_empty(db)
|
||||||
raise HTTPException(status_code=400, detail="System tools are read-only")
|
|
||||||
|
|
||||||
item = db.query(ToolResource).filter(ToolResource.id == id).first()
|
item = db.query(ToolResource).filter(ToolResource.id == id).first()
|
||||||
if not item:
|
if not item:
|
||||||
raise HTTPException(status_code=404, detail="Tool resource not found")
|
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}")
|
@router.delete("/resources/{id}")
|
||||||
def delete_tool_resource(id: str, db: Session = Depends(get_db)):
|
def delete_tool_resource(id: str, db: Session = Depends(get_db)):
|
||||||
"""删除自定义工具资源。"""
|
"""删除工具资源。"""
|
||||||
if id in TOOL_REGISTRY:
|
_seed_default_tools_if_empty(db)
|
||||||
raise HTTPException(status_code=400, detail="System tools cannot be deleted")
|
|
||||||
|
|
||||||
item = db.query(ToolResource).filter(ToolResource.id == id).first()
|
item = db.query(ToolResource).filter(ToolResource.id == id).first()
|
||||||
if not item:
|
if not item:
|
||||||
raise HTTPException(status_code=404, detail="Tool resource not found")
|
raise HTTPException(status_code=404, detail="Tool resource not found")
|
||||||
|
|||||||
@@ -311,9 +311,17 @@ class TestToolResourceCRUD:
|
|||||||
missing_resp = client.get(f"/api/tools/resources/{tool_id}")
|
missing_resp = client.get(f"/api/tools/resources/{tool_id}")
|
||||||
assert missing_resp.status_code == 404
|
assert missing_resp.status_code == 404
|
||||||
|
|
||||||
def test_system_tool_is_read_only(self, client):
|
def test_system_tool_can_be_updated_and_deleted(self, client):
|
||||||
update_resp = client.put("/api/tools/resources/search", json={"name": "new"})
|
list_resp = client.get("/api/tools/resources")
|
||||||
assert update_resp.status_code == 400
|
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")
|
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
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
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 { Button, Input, Badge, Dialog } from '../components/UI';
|
||||||
import { Tool } from '../types';
|
import { Tool } from '../types';
|
||||||
import { createTool, deleteTool, fetchTools, updateTool } from '../services/backendApi';
|
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) => {
|
const handleDeleteTool = async (e: React.MouseEvent, tool: Tool) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (tool.isSystem) {
|
|
||||||
alert('系统工具不可删除');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!confirm('确认删除该工具吗?')) return;
|
if (!confirm('确认删除该工具吗?')) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -177,11 +173,7 @@ export const ToolLibraryPage: React.FC = () => {
|
|||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center justify-between mb-1 gap-2">
|
<div className="flex items-center justify-between mb-1 gap-2">
|
||||||
<span className="text-base font-bold text-white truncate">{tool.name}</span>
|
<span className="text-base font-bold text-white truncate">{tool.name}</span>
|
||||||
{tool.isSystem ? (
|
{tool.isSystem ? <Badge variant="outline" className="text-[9px] h-4 px-1">SYSTEM</Badge> : <Badge variant="outline" className="text-[9px] h-4 px-1">CUSTOM</Badge>}
|
||||||
<Badge variant="outline" className="text-[9px] h-4 px-1">SYSTEM</Badge>
|
|
||||||
) : (
|
|
||||||
<Badge variant="outline" className="text-[9px] h-4 px-1">CUSTOM</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<Badge variant="outline" className={`text-[10px] border-0 px-0 ${tool.category === 'system' ? 'text-primary' : 'text-blue-400'}`}>
|
<Badge variant="outline" className={`text-[10px] border-0 px-0 ${tool.category === 'system' ? 'text-primary' : 'text-blue-400'}`}>
|
||||||
@@ -194,38 +186,22 @@ export const ToolLibraryPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 pt-3 border-t border-white/10 flex items-center justify-between">
|
<div className="mt-4 pt-3 border-t border-white/10 flex items-center justify-between">
|
||||||
{tool.isSystem ? (
|
<span className="text-[11px] text-muted-foreground">system/query 仅表示执行类型</span>
|
||||||
<span className="inline-flex items-center text-[11px] text-muted-foreground">
|
|
||||||
<Lock className="w-3 h-3 mr-1.5" /> 系统工具只读
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-[11px] text-muted-foreground">可编辑自定义工具</span>
|
|
||||||
)}
|
|
||||||
<div className="flex space-x-1">
|
<div className="flex space-x-1">
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (!tool.isSystem) openEdit(tool);
|
openEdit(tool);
|
||||||
}}
|
}}
|
||||||
disabled={tool.isSystem}
|
title="编辑工具"
|
||||||
title={tool.isSystem ? '系统工具不可编辑' : '编辑工具'}
|
className="p-1.5 rounded-md transition-colors hover:bg-primary/20 text-muted-foreground hover:text-primary"
|
||||||
className={`p-1.5 rounded-md transition-colors ${
|
|
||||||
tool.isSystem
|
|
||||||
? 'text-muted-foreground/40 cursor-not-allowed'
|
|
||||||
: 'hover:bg-primary/20 text-muted-foreground hover:text-primary'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<Edit2 className="w-4 h-4" />
|
<Edit2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => handleDeleteTool(e, tool)}
|
onClick={(e) => handleDeleteTool(e, tool)}
|
||||||
disabled={tool.isSystem}
|
title="删除工具"
|
||||||
title={tool.isSystem ? '系统工具不可删除' : '删除工具'}
|
className="p-1.5 rounded-md transition-colors hover:bg-destructive/20 text-muted-foreground hover:text-destructive"
|
||||||
className={`p-1.5 rounded-md transition-colors ${
|
|
||||||
tool.isSystem
|
|
||||||
? 'text-muted-foreground/40 cursor-not-allowed'
|
|
||||||
: 'hover:bg-destructive/20 text-muted-foreground hover:text-destructive'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user