Make server tool http based

This commit is contained in:
Xin Wang
2026-02-11 11:39:45 +08:00
parent 80e1d24443
commit 4c46793169
9 changed files with 281 additions and 17 deletions

View File

@@ -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)

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -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>

View File

@@ -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 });

View File

@@ -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;