Make server tool http based
This commit is contained in:
@@ -92,6 +92,10 @@ class ToolResource(Base):
|
|||||||
description: Mapped[str] = mapped_column(String(512), nullable=False, default="")
|
description: Mapped[str] = mapped_column(String(512), nullable=False, default="")
|
||||||
category: Mapped[str] = mapped_column(String(32), nullable=False, default="system") # system/query
|
category: Mapped[str] = mapped_column(String(32), nullable=False, default="system") # system/query
|
||||||
icon: Mapped[str] = mapped_column(String(64), nullable=False, default="Wrench")
|
icon: Mapped[str] = mapped_column(String(64), nullable=False, default="Wrench")
|
||||||
|
http_method: Mapped[str] = mapped_column(String(16), nullable=False, default="GET")
|
||||||
|
http_url: Mapped[Optional[str]] = mapped_column(String(1024), nullable=True)
|
||||||
|
http_headers: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||||
|
http_timeout_ms: Mapped[int] = mapped_column(Integer, default=10000)
|
||||||
enabled: Mapped[bool] = mapped_column(default=True)
|
enabled: Mapped[bool] = mapped_column(default=True)
|
||||||
is_system: Mapped[bool] = mapped_column(default=False)
|
is_system: Mapped[bool] = mapped_column(default=False)
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||||
|
|||||||
@@ -108,11 +108,37 @@ TOOL_ICON_MAP = {
|
|||||||
"decrease_volume": "Volume2",
|
"decrease_volume": "Volume2",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TOOL_HTTP_DEFAULTS = {
|
||||||
|
"current_time": {
|
||||||
|
"http_method": "GET",
|
||||||
|
"http_url": "https://worldtimeapi.org/api/ip",
|
||||||
|
"http_headers": {},
|
||||||
|
"http_timeout_ms": 10000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_http_method(method: Optional[str]) -> str:
|
||||||
|
normalized = str(method or "GET").strip().upper()
|
||||||
|
return normalized if normalized in {"GET", "POST", "PUT", "PATCH", "DELETE"} else "GET"
|
||||||
|
|
||||||
|
|
||||||
|
def _requires_http_request(category: str, tool_id: Optional[str]) -> bool:
|
||||||
|
if category != "query":
|
||||||
|
return False
|
||||||
|
return str(tool_id or "").strip() not in {"calculator", "code_interpreter"}
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_query_http_config(*, category: str, tool_id: Optional[str], http_url: Optional[str]) -> None:
|
||||||
|
if _requires_http_request(category, tool_id) and not str(http_url or "").strip():
|
||||||
|
raise HTTPException(status_code=400, detail="http_url is required for query tools (except calculator/code_interpreter)")
|
||||||
|
|
||||||
def _seed_default_tools_if_empty(db: Session) -> None:
|
def _seed_default_tools_if_empty(db: Session) -> None:
|
||||||
"""Seed built-in tools only when tool_resources is empty."""
|
"""Seed built-in tools only when tool_resources is empty."""
|
||||||
if db.query(ToolResource).count() > 0:
|
if db.query(ToolResource).count() > 0:
|
||||||
return
|
return
|
||||||
for tool_id, payload in TOOL_REGISTRY.items():
|
for tool_id, payload in TOOL_REGISTRY.items():
|
||||||
|
http_defaults = TOOL_HTTP_DEFAULTS.get(tool_id, {})
|
||||||
db.add(ToolResource(
|
db.add(ToolResource(
|
||||||
id=tool_id,
|
id=tool_id,
|
||||||
user_id=1,
|
user_id=1,
|
||||||
@@ -120,6 +146,10 @@ def _seed_default_tools_if_empty(db: Session) -> None:
|
|||||||
description=payload.get("description", ""),
|
description=payload.get("description", ""),
|
||||||
category=TOOL_CATEGORY_MAP.get(tool_id, "system"),
|
category=TOOL_CATEGORY_MAP.get(tool_id, "system"),
|
||||||
icon=TOOL_ICON_MAP.get(tool_id, "Wrench"),
|
icon=TOOL_ICON_MAP.get(tool_id, "Wrench"),
|
||||||
|
http_method=_normalize_http_method(http_defaults.get("http_method")),
|
||||||
|
http_url=http_defaults.get("http_url"),
|
||||||
|
http_headers=http_defaults.get("http_headers") or {},
|
||||||
|
http_timeout_ms=int(http_defaults.get("http_timeout_ms") or 10000),
|
||||||
enabled=True,
|
enabled=True,
|
||||||
is_system=True,
|
is_system=True,
|
||||||
))
|
))
|
||||||
@@ -128,8 +158,9 @@ def _seed_default_tools_if_empty(db: Session) -> None:
|
|||||||
|
|
||||||
def recreate_tool_resources(db: Session) -> None:
|
def recreate_tool_resources(db: Session) -> None:
|
||||||
"""Recreate tool resources table content with current built-in defaults."""
|
"""Recreate tool resources table content with current built-in defaults."""
|
||||||
db.query(ToolResource).delete()
|
bind = db.get_bind()
|
||||||
db.commit()
|
ToolResource.__table__.drop(bind=bind, checkfirst=True)
|
||||||
|
ToolResource.__table__.create(bind=bind, checkfirst=True)
|
||||||
_seed_default_tools_if_empty(db)
|
_seed_default_tools_if_empty(db)
|
||||||
|
|
||||||
|
|
||||||
@@ -189,6 +220,8 @@ def create_tool_resource(data: ToolResourceCreate, db: Session = Depends(get_db)
|
|||||||
if candidate_id and db.query(ToolResource).filter(ToolResource.id == candidate_id).first():
|
if candidate_id and db.query(ToolResource).filter(ToolResource.id == candidate_id).first():
|
||||||
raise HTTPException(status_code=400, detail="Tool ID already exists")
|
raise HTTPException(status_code=400, detail="Tool ID already exists")
|
||||||
|
|
||||||
|
_validate_query_http_config(category=data.category, tool_id=candidate_id, http_url=data.http_url)
|
||||||
|
|
||||||
item = ToolResource(
|
item = ToolResource(
|
||||||
id=candidate_id or f"tool_{str(uuid.uuid4())[:8]}",
|
id=candidate_id or f"tool_{str(uuid.uuid4())[:8]}",
|
||||||
user_id=1,
|
user_id=1,
|
||||||
@@ -196,6 +229,10 @@ def create_tool_resource(data: ToolResourceCreate, db: Session = Depends(get_db)
|
|||||||
description=data.description,
|
description=data.description,
|
||||||
category=data.category,
|
category=data.category,
|
||||||
icon=data.icon,
|
icon=data.icon,
|
||||||
|
http_method=_normalize_http_method(data.http_method),
|
||||||
|
http_url=(data.http_url or "").strip() or None,
|
||||||
|
http_headers=data.http_headers or {},
|
||||||
|
http_timeout_ms=max(1000, int(data.http_timeout_ms or 10000)),
|
||||||
enabled=data.enabled,
|
enabled=data.enabled,
|
||||||
is_system=False,
|
is_system=False,
|
||||||
)
|
)
|
||||||
@@ -214,6 +251,16 @@ def update_tool_resource(id: str, data: ToolResourceUpdate, db: Session = Depend
|
|||||||
raise HTTPException(status_code=404, detail="Tool resource not found")
|
raise HTTPException(status_code=404, detail="Tool resource not found")
|
||||||
|
|
||||||
update_data = data.model_dump(exclude_unset=True)
|
update_data = data.model_dump(exclude_unset=True)
|
||||||
|
|
||||||
|
new_category = update_data.get("category", item.category)
|
||||||
|
new_http_url = update_data.get("http_url", item.http_url)
|
||||||
|
_validate_query_http_config(category=new_category, tool_id=id, http_url=new_http_url)
|
||||||
|
|
||||||
|
if "http_method" in update_data:
|
||||||
|
update_data["http_method"] = _normalize_http_method(update_data.get("http_method"))
|
||||||
|
if "http_timeout_ms" in update_data and update_data.get("http_timeout_ms") is not None:
|
||||||
|
update_data["http_timeout_ms"] = max(1000, int(update_data["http_timeout_ms"]))
|
||||||
|
|
||||||
for field, value in update_data.items():
|
for field, value in update_data.items():
|
||||||
setattr(item, field, value)
|
setattr(item, field, value)
|
||||||
item.updated_at = datetime.utcnow()
|
item.updated_at = datetime.utcnow()
|
||||||
|
|||||||
@@ -235,6 +235,10 @@ class ToolResourceBase(BaseModel):
|
|||||||
description: str = ""
|
description: str = ""
|
||||||
category: str = "system" # system/query
|
category: str = "system" # system/query
|
||||||
icon: str = "Wrench"
|
icon: str = "Wrench"
|
||||||
|
http_method: str = "GET"
|
||||||
|
http_url: Optional[str] = None
|
||||||
|
http_headers: Dict[str, str] = Field(default_factory=dict)
|
||||||
|
http_timeout_ms: int = 10000
|
||||||
enabled: bool = True
|
enabled: bool = True
|
||||||
|
|
||||||
|
|
||||||
@@ -247,6 +251,10 @@ class ToolResourceUpdate(BaseModel):
|
|||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
category: Optional[str] = None
|
category: Optional[str] = None
|
||||||
icon: Optional[str] = None
|
icon: Optional[str] = None
|
||||||
|
http_method: Optional[str] = None
|
||||||
|
http_url: Optional[str] = None
|
||||||
|
http_headers: Optional[Dict[str, str]] = None
|
||||||
|
http_timeout_ms: Optional[int] = None
|
||||||
enabled: Optional[bool] = None
|
enabled: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -288,6 +288,10 @@ class TestToolResourceCRUD:
|
|||||||
"description": "抓取页面并提取正文",
|
"description": "抓取页面并提取正文",
|
||||||
"category": "query",
|
"category": "query",
|
||||||
"icon": "Globe",
|
"icon": "Globe",
|
||||||
|
"http_method": "GET",
|
||||||
|
"http_url": "https://example.com/search",
|
||||||
|
"http_headers": {},
|
||||||
|
"http_timeout_ms": 10000,
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
})
|
})
|
||||||
assert create_resp.status_code == 200
|
assert create_resp.status_code == 200
|
||||||
@@ -315,6 +319,16 @@ 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_create_query_tool_requires_http_url(self, client):
|
||||||
|
resp = client.post("/api/tools/resources", json={
|
||||||
|
"name": "缺失URL的查询工具",
|
||||||
|
"description": "应当失败",
|
||||||
|
"category": "query",
|
||||||
|
"icon": "Globe",
|
||||||
|
"enabled": True,
|
||||||
|
})
|
||||||
|
assert resp.status_code == 400
|
||||||
|
|
||||||
def test_system_tool_can_be_updated_and_deleted(self, client):
|
def test_system_tool_can_be_updated_and_deleted(self, client):
|
||||||
list_resp = client.get("/api/tools/resources")
|
list_resp = client.get("/api/tools/resources")
|
||||||
assert list_resp.status_code == 200
|
assert list_resp.status_code == 200
|
||||||
|
|||||||
@@ -189,3 +189,23 @@ async def search_knowledge_context(
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(f"Knowledge search failed (kb_id={kb_id}): {exc}")
|
logger.warning(f"Knowledge search failed (kb_id={kb_id}): {exc}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_tool_resource(tool_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Fetch tool resource configuration from backend API."""
|
||||||
|
base_url = _backend_base_url()
|
||||||
|
if not base_url or not tool_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
url = f"{base_url}/api/tools/resources/{tool_id}"
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession(timeout=_timeout()) as session:
|
||||||
|
async with session.get(url) as resp:
|
||||||
|
if resp.status == 404:
|
||||||
|
return None
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = await resp.json()
|
||||||
|
return data if isinstance(data, dict) else None
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(f"Failed to fetch tool resource ({tool_id}): {exc}")
|
||||||
|
return None
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
"""Server-side tool execution helpers."""
|
"""Server-side tool execution helpers."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import ast
|
import ast
|
||||||
import operator
|
import operator
|
||||||
from datetime import datetime
|
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from app.backend_client import fetch_tool_resource
|
||||||
|
|
||||||
_BIN_OPS = {
|
_BIN_OPS = {
|
||||||
ast.Add: operator.add,
|
ast.Add: operator.add,
|
||||||
@@ -206,19 +209,6 @@ async def execute_server_tool(tool_call: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
"status": {"code": 422, "message": "invalid_expression"},
|
"status": {"code": 422, "message": "invalid_expression"},
|
||||||
}
|
}
|
||||||
|
|
||||||
if tool_name == "current_time":
|
|
||||||
now = datetime.now().astimezone()
|
|
||||||
return {
|
|
||||||
"tool_call_id": call_id,
|
|
||||||
"name": tool_name,
|
|
||||||
"output": {
|
|
||||||
"iso": now.isoformat(),
|
|
||||||
"local": now.strftime("%Y-%m-%d %H:%M:%S %Z"),
|
|
||||||
"unix": int(now.timestamp()),
|
|
||||||
},
|
|
||||||
"status": {"code": 200, "message": "ok"},
|
|
||||||
}
|
|
||||||
|
|
||||||
if tool_name == "code_interpreter":
|
if tool_name == "code_interpreter":
|
||||||
code = str(args.get("code") or args.get("expression") or "").strip()
|
code = str(args.get("code") or args.get("expression") or "").strip()
|
||||||
if not code:
|
if not code:
|
||||||
@@ -251,6 +241,82 @@ async def execute_server_tool(tool_call: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
"status": {"code": 422, "message": "invalid_code"},
|
"status": {"code": 422, "message": "invalid_code"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tool_name and tool_name not in {"calculator", "code_interpreter"}:
|
||||||
|
resource = await fetch_tool_resource(tool_name)
|
||||||
|
if resource and str(resource.get("category") or "") == "query":
|
||||||
|
method = str(resource.get("http_method") or "GET").strip().upper()
|
||||||
|
if method not in {"GET", "POST", "PUT", "PATCH", "DELETE"}:
|
||||||
|
method = "GET"
|
||||||
|
url = str(resource.get("http_url") or "").strip()
|
||||||
|
headers = resource.get("http_headers") if isinstance(resource.get("http_headers"), dict) else {}
|
||||||
|
timeout_ms = resource.get("http_timeout_ms")
|
||||||
|
try:
|
||||||
|
timeout_s = max(1.0, float(timeout_ms) / 1000.0)
|
||||||
|
except Exception:
|
||||||
|
timeout_s = 10.0
|
||||||
|
|
||||||
|
if not url:
|
||||||
|
return {
|
||||||
|
"tool_call_id": call_id,
|
||||||
|
"name": tool_name,
|
||||||
|
"output": {"error": "http_url not configured"},
|
||||||
|
"status": {"code": 422, "message": "invalid_tool_config"},
|
||||||
|
}
|
||||||
|
|
||||||
|
request_kwargs: Dict[str, Any] = {}
|
||||||
|
if method in {"GET", "DELETE"}:
|
||||||
|
request_kwargs["params"] = args
|
||||||
|
else:
|
||||||
|
request_kwargs["json"] = args
|
||||||
|
|
||||||
|
try:
|
||||||
|
timeout = aiohttp.ClientTimeout(total=timeout_s)
|
||||||
|
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||||
|
async with session.request(method, url, headers=headers, **request_kwargs) as resp:
|
||||||
|
content_type = str(resp.headers.get("Content-Type") or "").lower()
|
||||||
|
if "application/json" in content_type:
|
||||||
|
body: Any = await resp.json()
|
||||||
|
else:
|
||||||
|
body = await resp.text()
|
||||||
|
status_code = int(resp.status)
|
||||||
|
if 200 <= status_code < 300:
|
||||||
|
return {
|
||||||
|
"tool_call_id": call_id,
|
||||||
|
"name": tool_name,
|
||||||
|
"output": {
|
||||||
|
"method": method,
|
||||||
|
"url": url,
|
||||||
|
"status_code": status_code,
|
||||||
|
"response": _json_safe(body),
|
||||||
|
},
|
||||||
|
"status": {"code": 200, "message": "ok"},
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"tool_call_id": call_id,
|
||||||
|
"name": tool_name,
|
||||||
|
"output": {
|
||||||
|
"method": method,
|
||||||
|
"url": url,
|
||||||
|
"status_code": status_code,
|
||||||
|
"response": _json_safe(body),
|
||||||
|
},
|
||||||
|
"status": {"code": status_code, "message": "http_error"},
|
||||||
|
}
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
return {
|
||||||
|
"tool_call_id": call_id,
|
||||||
|
"name": tool_name,
|
||||||
|
"output": {"method": method, "url": url, "error": "request timeout"},
|
||||||
|
"status": {"code": 504, "message": "http_timeout"},
|
||||||
|
}
|
||||||
|
except Exception as exc:
|
||||||
|
return {
|
||||||
|
"tool_call_id": call_id,
|
||||||
|
"name": tool_name,
|
||||||
|
"output": {"method": method, "url": url, "error": str(exc)},
|
||||||
|
"status": {"code": 502, "message": "http_request_failed"},
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"tool_call_id": call_id,
|
"tool_call_id": call_id,
|
||||||
"name": tool_name or "unknown_tool",
|
"name": tool_name or "unknown_tool",
|
||||||
|
|||||||
@@ -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 } from 'lucide-react';
|
import { Search, Filter, Plus, Wrench, Terminal, Globe, Camera, CameraOff, Image, Images, CloudSun, Calendar, TrendingUp, Coins, Trash2, Edit2, Box, Volume2 } 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';
|
||||||
@@ -17,6 +17,7 @@ const iconMap: Record<string, React.ReactNode> = {
|
|||||||
Globe: <Globe className="w-5 h-5" />,
|
Globe: <Globe className="w-5 h-5" />,
|
||||||
Wrench: <Wrench className="w-5 h-5" />,
|
Wrench: <Wrench className="w-5 h-5" />,
|
||||||
Box: <Box className="w-5 h-5" />,
|
Box: <Box className="w-5 h-5" />,
|
||||||
|
Volume2: <Volume2 className="w-5 h-5" />,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ToolLibraryPage: React.FC = () => {
|
export const ToolLibraryPage: React.FC = () => {
|
||||||
@@ -32,6 +33,10 @@ export const ToolLibraryPage: React.FC = () => {
|
|||||||
const [toolCategory, setToolCategory] = useState<'system' | 'query'>('system');
|
const [toolCategory, setToolCategory] = useState<'system' | 'query'>('system');
|
||||||
const [toolIcon, setToolIcon] = useState('Wrench');
|
const [toolIcon, setToolIcon] = useState('Wrench');
|
||||||
const [toolEnabled, setToolEnabled] = useState(true);
|
const [toolEnabled, setToolEnabled] = useState(true);
|
||||||
|
const [toolHttpMethod, setToolHttpMethod] = useState<'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'>('GET');
|
||||||
|
const [toolHttpUrl, setToolHttpUrl] = useState('');
|
||||||
|
const [toolHttpHeadersText, setToolHttpHeadersText] = useState('{}');
|
||||||
|
const [toolHttpTimeoutMs, setToolHttpTimeoutMs] = useState(10000);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
const loadTools = async () => {
|
const loadTools = async () => {
|
||||||
@@ -57,6 +62,10 @@ export const ToolLibraryPage: React.FC = () => {
|
|||||||
setToolCategory('system');
|
setToolCategory('system');
|
||||||
setToolIcon('Wrench');
|
setToolIcon('Wrench');
|
||||||
setToolEnabled(true);
|
setToolEnabled(true);
|
||||||
|
setToolHttpMethod('GET');
|
||||||
|
setToolHttpUrl('');
|
||||||
|
setToolHttpHeadersText('{}');
|
||||||
|
setToolHttpTimeoutMs(10000);
|
||||||
setIsToolModalOpen(true);
|
setIsToolModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -67,6 +76,10 @@ export const ToolLibraryPage: React.FC = () => {
|
|||||||
setToolCategory(tool.category);
|
setToolCategory(tool.category);
|
||||||
setToolIcon(tool.icon || 'Wrench');
|
setToolIcon(tool.icon || 'Wrench');
|
||||||
setToolEnabled(tool.enabled ?? true);
|
setToolEnabled(tool.enabled ?? true);
|
||||||
|
setToolHttpMethod((tool.httpMethod || 'GET') as 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE');
|
||||||
|
setToolHttpUrl(tool.httpUrl || '');
|
||||||
|
setToolHttpHeadersText(JSON.stringify(tool.httpHeaders || {}, null, 2));
|
||||||
|
setToolHttpTimeoutMs(tool.httpTimeoutMs || 10000);
|
||||||
setIsToolModalOpen(true);
|
setIsToolModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -88,12 +101,37 @@ export const ToolLibraryPage: React.FC = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
|
let parsedHeaders: Record<string, string> = {};
|
||||||
|
if (toolCategory === 'query') {
|
||||||
|
if (!toolHttpUrl.trim() && editingTool?.id !== 'calculator' && editingTool?.id !== 'code_interpreter') {
|
||||||
|
alert('信息查询工具请填写 HTTP URL');
|
||||||
|
setSaving(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(toolHttpHeadersText || '{}');
|
||||||
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||||
|
parsedHeaders = parsed as Record<string, string>;
|
||||||
|
} else {
|
||||||
|
throw new Error('headers must be object');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
alert('HTTP Headers 必须是合法 JSON 对象');
|
||||||
|
setSaving(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (editingTool) {
|
if (editingTool) {
|
||||||
const updated = await updateTool(editingTool.id, {
|
const updated = await updateTool(editingTool.id, {
|
||||||
name: toolName.trim(),
|
name: toolName.trim(),
|
||||||
description: toolDesc,
|
description: toolDesc,
|
||||||
category: toolCategory,
|
category: toolCategory,
|
||||||
icon: toolIcon,
|
icon: toolIcon,
|
||||||
|
httpMethod: toolHttpMethod,
|
||||||
|
httpUrl: toolHttpUrl.trim(),
|
||||||
|
httpHeaders: parsedHeaders,
|
||||||
|
httpTimeoutMs: toolHttpTimeoutMs,
|
||||||
enabled: toolEnabled,
|
enabled: toolEnabled,
|
||||||
});
|
});
|
||||||
setTools((prev) => prev.map((item) => (item.id === updated.id ? updated : item)));
|
setTools((prev) => prev.map((item) => (item.id === updated.id ? updated : item)));
|
||||||
@@ -103,6 +141,10 @@ export const ToolLibraryPage: React.FC = () => {
|
|||||||
description: toolDesc,
|
description: toolDesc,
|
||||||
category: toolCategory,
|
category: toolCategory,
|
||||||
icon: toolIcon,
|
icon: toolIcon,
|
||||||
|
httpMethod: toolHttpMethod,
|
||||||
|
httpUrl: toolHttpUrl.trim(),
|
||||||
|
httpHeaders: parsedHeaders,
|
||||||
|
httpTimeoutMs: toolHttpTimeoutMs,
|
||||||
enabled: toolEnabled,
|
enabled: toolEnabled,
|
||||||
});
|
});
|
||||||
setTools((prev) => [created, ...prev]);
|
setTools((prev) => [created, ...prev]);
|
||||||
@@ -294,6 +336,53 @@ export const ToolLibraryPage: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{toolCategory === 'query' && (
|
||||||
|
<div className="space-y-4 rounded-md border border-blue-500/20 bg-blue-500/5 p-3">
|
||||||
|
<div className="text-[10px] font-black uppercase tracking-widest text-blue-300">HTTP Request Config</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-2">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">Method</label>
|
||||||
|
<select
|
||||||
|
className="flex h-9 w-full rounded-md border-0 bg-white/5 px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card text-foreground"
|
||||||
|
value={toolHttpMethod}
|
||||||
|
onChange={(e) => setToolHttpMethod(e.target.value as 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE')}
|
||||||
|
>
|
||||||
|
<option value="GET">GET</option>
|
||||||
|
<option value="POST">POST</option>
|
||||||
|
<option value="PUT">PUT</option>
|
||||||
|
<option value="PATCH">PATCH</option>
|
||||||
|
<option value="DELETE">DELETE</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5 md:col-span-2">
|
||||||
|
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">URL</label>
|
||||||
|
<Input value={toolHttpUrl} onChange={(e) => setToolHttpUrl(e.target.value)} placeholder="https://api.example.com/endpoint" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">Headers (JSON)</label>
|
||||||
|
<textarea
|
||||||
|
className="flex min-h-[90px] w-full rounded-md border border-white/10 bg-white/5 px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 text-white font-mono"
|
||||||
|
value={toolHttpHeadersText}
|
||||||
|
onChange={(e) => setToolHttpHeadersText(e.target.value)}
|
||||||
|
placeholder='{"Authorization":"Bearer ..."}'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-[10px] font-black text-muted-foreground uppercase tracking-widest block">Timeout (ms)</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1000}
|
||||||
|
value={toolHttpTimeoutMs}
|
||||||
|
onChange={(e) => setToolHttpTimeoutMs(Math.max(1000, Number(e.target.value || 10000)))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] text-muted-foreground">
|
||||||
|
Query tools send model arguments as request params for GET/DELETE, and JSON body for POST/PUT/PATCH.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<label className="flex items-center space-x-2 text-xs text-muted-foreground">
|
<label className="flex items-center space-x-2 text-xs text-muted-foreground">
|
||||||
<input type="checkbox" checked={toolEnabled} onChange={(e) => setToolEnabled(e.target.checked)} />
|
<input type="checkbox" checked={toolEnabled} onChange={(e) => setToolEnabled(e.target.checked)} />
|
||||||
<span>启用该工具</span>
|
<span>启用该工具</span>
|
||||||
|
|||||||
@@ -102,6 +102,10 @@ const mapTool = (raw: AnyRecord): Tool => ({
|
|||||||
description: readField(raw, ['description'], ''),
|
description: readField(raw, ['description'], ''),
|
||||||
category: readField(raw, ['category'], 'system') as 'system' | 'query',
|
category: readField(raw, ['category'], 'system') as 'system' | 'query',
|
||||||
icon: readField(raw, ['icon'], 'Wrench'),
|
icon: readField(raw, ['icon'], 'Wrench'),
|
||||||
|
httpMethod: readField(raw, ['httpMethod', 'http_method'], 'GET') as Tool['httpMethod'],
|
||||||
|
httpUrl: readField(raw, ['httpUrl', 'http_url'], ''),
|
||||||
|
httpHeaders: readField(raw, ['httpHeaders', 'http_headers'], {}),
|
||||||
|
httpTimeoutMs: Number(readField(raw, ['httpTimeoutMs', 'http_timeout_ms'], 10000)),
|
||||||
isSystem: Boolean(readField(raw, ['isSystem', 'is_system'], false)),
|
isSystem: Boolean(readField(raw, ['isSystem', 'is_system'], false)),
|
||||||
enabled: Boolean(readField(raw, ['enabled'], true)),
|
enabled: Boolean(readField(raw, ['enabled'], true)),
|
||||||
isCustom: !Boolean(readField(raw, ['isSystem', 'is_system'], false)),
|
isCustom: !Boolean(readField(raw, ['isSystem', 'is_system'], false)),
|
||||||
@@ -500,6 +504,10 @@ export const createTool = async (data: Partial<Tool>): Promise<Tool> => {
|
|||||||
description: data.description || '',
|
description: data.description || '',
|
||||||
category: data.category || 'system',
|
category: data.category || 'system',
|
||||||
icon: data.icon || (data.category === 'query' ? 'Globe' : 'Terminal'),
|
icon: data.icon || (data.category === 'query' ? 'Globe' : 'Terminal'),
|
||||||
|
http_method: data.httpMethod || 'GET',
|
||||||
|
http_url: data.httpUrl || null,
|
||||||
|
http_headers: data.httpHeaders || {},
|
||||||
|
http_timeout_ms: data.httpTimeoutMs ?? 10000,
|
||||||
enabled: data.enabled ?? true,
|
enabled: data.enabled ?? true,
|
||||||
};
|
};
|
||||||
const response = await apiRequest<AnyRecord>('/tools/resources', { method: 'POST', body: payload });
|
const response = await apiRequest<AnyRecord>('/tools/resources', { method: 'POST', body: payload });
|
||||||
@@ -512,6 +520,10 @@ export const updateTool = async (id: string, data: Partial<Tool>): Promise<Tool>
|
|||||||
description: data.description,
|
description: data.description,
|
||||||
category: data.category,
|
category: data.category,
|
||||||
icon: data.icon,
|
icon: data.icon,
|
||||||
|
http_method: data.httpMethod,
|
||||||
|
http_url: data.httpUrl,
|
||||||
|
http_headers: data.httpHeaders,
|
||||||
|
http_timeout_ms: data.httpTimeoutMs,
|
||||||
enabled: data.enabled,
|
enabled: data.enabled,
|
||||||
};
|
};
|
||||||
const response = await apiRequest<AnyRecord>(`/tools/resources/${id}`, { method: 'PUT', body: payload });
|
const response = await apiRequest<AnyRecord>(`/tools/resources/${id}`, { method: 'PUT', body: payload });
|
||||||
|
|||||||
@@ -186,6 +186,10 @@ export interface Tool {
|
|||||||
description: string;
|
description: string;
|
||||||
category: 'system' | 'query';
|
category: 'system' | 'query';
|
||||||
icon: string;
|
icon: string;
|
||||||
|
httpMethod?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
||||||
|
httpUrl?: string;
|
||||||
|
httpHeaders?: Record<string, string>;
|
||||||
|
httpTimeoutMs?: number;
|
||||||
isCustom?: boolean;
|
isCustom?: boolean;
|
||||||
isSystem?: boolean;
|
isSystem?: boolean;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user