diff --git a/backend/app/api/tools.py b/backend/app/api/tools.py index 4481ba4c..eeed4278 100644 --- a/backend/app/api/tools.py +++ b/backend/app/api/tools.py @@ -1,6 +1,10 @@ """Tool management API — CRUD for tools and per-agent assignments.""" import uuid +import logging +from datetime import datetime, timezone + +logger = logging.getLogger(__name__) from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel @@ -555,6 +559,98 @@ async def delete_agent_tool( return {"ok": True} +class BulkDeleteAgentToolsRequest(BaseModel): + agent_tool_ids: list[str] + + # Validation: limit to 50 items per request to prevent abuse + model_config = {"json_schema_extra": {"maxItems": 50}} + + +class BulkDeleteResult(BaseModel): + deleted: int + errors: list[dict] = [] + + +@router.delete("/agent-tools/bulk", response_model=BulkDeleteResult) +async def delete_agent_tools_bulk( + request: BulkDeleteAgentToolsRequest, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """Admin: bulk delete agent-tool assignments. Also deletes tool records if no other agents use them.""" + # Admin check + if current_user.role not in ("org_admin", "platform_admin"): + from fastapi import HTTPException + raise HTTPException(status_code=403, detail="Admin permission required") + + deleted_count = 0 + errors: list[dict] = [] + tools_to_check: set[uuid.UUID] = set() + + # Parse and validate IDs + valid_ids: list[uuid.UUID] = [] + for agent_tool_id_str in request.agent_tool_ids: + try: + agent_tool_id = uuid.UUID(agent_tool_id_str) + valid_ids.append(agent_tool_id) + except ValueError: + errors.append({"id": agent_tool_id_str, "error": "Invalid ID"}) + + # Batch-fetch AgentTool with Agent using JOIN (avoids N+1 and MissingGreenlet) + from app.models.agent import Agent + if valid_ids: + results = await db.execute( + select(AgentTool, Agent) + .join(Agent, AgentTool.agent_id == Agent.id) + .where(AgentTool.id.in_(valid_ids)) + ) + # Map agent_tool_id -> (AgentTool, Agent) for tenant check + agent_tool_map = {str(at.id): (at, ag) for at, ag in results.all()} + + # Process each requested ID + for agent_tool_id_str in request.agent_tool_ids: + # Skip invalid IDs (already added to errors above) + try: + uuid.UUID(agent_tool_id_str) + except ValueError: + continue + + pair = agent_tool_map.get(agent_tool_id_str) + if not pair: + errors.append({"id": agent_tool_id_str, "error": "Not found"}) + continue + + at, ag = pair + # Check ownership (admin can only delete within their tenant) + if current_user.role != "platform_admin" and ag.tenant_id != current_user.tenant_id: + errors.append({"id": agent_tool_id_str, "error": "Access denied"}) + continue + + tools_to_check.add(at.tool_id) + await db.delete(at) + deleted_count += 1 + + await db.flush() + + # Log the bulk delete operation for audit trail + logger.info( + f"[{datetime.now(timezone.utc).isoformat()}] Bulk delete agent-tools: user={current_user.id}, tenant={current_user.tenant_id}, " + f"deleted={deleted_count}, errors={len(errors)}, tools_cleaned={len(tools_to_check)}" + ) + + # Check if any tools should be deleted (no remaining agents use them) + for tool_id in tools_to_check: + remaining_r = await db.execute(select(AgentTool).where(AgentTool.tool_id == tool_id).limit(1)) + if not remaining_r.scalar_one_or_none(): + tool_r = await db.execute(select(Tool).where(Tool.id == tool_id)) + tool = tool_r.scalar_one_or_none() + if tool and tool.type == "mcp": + await db.delete(tool) + + await db.commit() + return BulkDeleteResult(deleted=deleted_count, errors=errors) + + # ─── Per-Agent Tool Config ─────────────────────────────────── class AgentToolConfigUpdate(BaseModel): diff --git a/backend/app/api/wecom.py b/backend/app/api/wecom.py index 41bb14b6..8d07fd72 100644 --- a/backend/app/api/wecom.py +++ b/backend/app/api/wecom.py @@ -117,11 +117,19 @@ async def configure_wecom_channel( detail="Either bot_id+bot_secret (WebSocket) or corp_id+secret+token+encoding_aes_key (Webhook) required" ) + # Priority: use explicit connection_mode from frontend, then auto-detect + requested_mode = data.get("connection_mode", "").strip() + if requested_mode in ("websocket", "webhook"): + connection_mode = requested_mode + else: + # Auto-detect based on available fields (WebSocket takes priority if both present) + connection_mode = "websocket" if has_ws_mode else "webhook" + extra_config = { "wecom_agent_id": wecom_agent_id, "bot_id": bot_id, "bot_secret": bot_secret, - "connection_mode": "websocket" if has_ws_mode else "webhook", + "connection_mode": connection_mode, } result = await db.execute( @@ -580,7 +588,7 @@ async def _process_wecom_text( logger.info(f"[WeCom KF] send_msg result: {res_send.json()}") else: # Default legacy Send as text - await client.post( + res_send = await client.post( f"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}", json={ "touser": from_user, @@ -589,6 +597,7 @@ async def _process_wecom_text( "text": {"content": reply_text}, }, ) + logger.info(f"[WeCom] message/send result: {res_send.json()}") except Exception as e: logger.error(f"[WeCom] Failed to send reply: {e}") diff --git a/frontend/src/components/ChannelConfig.tsx b/frontend/src/components/ChannelConfig.tsx index 525f2ed8..853d92f1 100644 --- a/frontend/src/components/ChannelConfig.tsx +++ b/frontend/src/components/ChannelConfig.tsx @@ -386,7 +386,7 @@ export default function ChannelConfig({ mode, agentId, canManage = true, values, }); const { data: wecomWebhook } = useQuery({ queryKey: ['wecom-webhook-url', agentId], - queryFn: () => fetchAuth(`/agents/${agentId}/wecom-channel/webhook-url`), + queryFn: () => fetchAuth(`/agents/${agentId}/wecom-channel/webhook-url`).catch(() => null), enabled: enabled, }); const { data: atlassianConfig } = useQuery({ @@ -435,11 +435,12 @@ export default function ChannelConfig({ mode, agentId, canManage = true, values, } return fetchAuth(`/agents/${agentId}/${ch.apiSlug}`, { method: 'POST', body: JSON.stringify(data) }); }, - onSuccess: (_d, { ch }) => { + onSuccess: async (_d, { ch }) => { const keys = ch.useChannelApi ? [['channel', agentId]] : [[`${ch.apiSlug}`, agentId], [`${ch.id}-webhook-url`, agentId]]; - keys.forEach(k => queryClient.invalidateQueries({ queryKey: k })); + // Invalidate and wait for refetch to complete + await Promise.all(keys.map(k => queryClient.invalidateQueries({ queryKey: k }))); // Reset form setForms(prev => ({ ...prev, [ch.id]: {} })); setEditing(ch.id, false); @@ -794,6 +795,13 @@ export default function ChannelConfig({ mode, agentId, canManage = true, values, )} + {/* WeCom webhook status */} + {ch.id === 'wecom' && configConnMode === 'webhook' && ( +
+
Mode: Webhook
+
CorpID: {config.app_id}
+
+ )} {/* Webhook URL (non-websocket channels) */} {ch.webhookLabel && !(ch.connectionMode && configConnMode === 'websocket') && ch.id !== 'dingtalk' && ch.id !== 'atlassian' && ( diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index ef9bcc6e..41ec99de 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -598,7 +598,15 @@ "reloadPage": "Reload Page", "selectAgent": "— Select Agent —", "failedToCreateSession": "Failed to create session", - "failed": "Failed" + "failed": "Failed", + "selectAll": "Select All", + "bulkDelete": "Bulk Delete", + "selectedCount": "{{count}} selected", + "bulkDeleteConfirm": "Are you sure you want to delete {{count}} selected tools? This action cannot be undone.", + "bulkDeleteSuccess": "Successfully deleted {{count}} tools", + "deleteSuccess": "Delete successful", + "toolsCount": "tools", + "partialFailed": "Some operations failed" }, "toolCategories": { "file": "File Operations", @@ -1035,6 +1043,14 @@ "noAgentInstalledTools": "No agent-installed tools yet.", "removeFromAgent": "Remove \"{{name}}\" from agent?", "delete": "Delete", + "selectAll": "Select All", + "bulkDelete": "Bulk Delete", + "selectedCount": "{{count}} selected", + "bulkDeleteConfirm": "Are you sure you want to delete {{count}} selected tools? This action cannot be undone.", + "bulkDeleteSuccess": "Successfully deleted {{count}} tools", + "deleteFailed": "Delete failed", + "deleteSuccess": "Delete successful", + "toolsCount": "tools", "addMcpServer": "Add MCP Server", "mcpServer": "MCP Server", "jsonConfig": "JSON Config", diff --git a/frontend/src/i18n/zh.json b/frontend/src/i18n/zh.json index 3de777d1..b2c2b778 100644 --- a/frontend/src/i18n/zh.json +++ b/frontend/src/i18n/zh.json @@ -679,7 +679,15 @@ "reloadPage": "重新加载页面", "selectAgent": "— 选择数字员工 —", "failedToCreateSession": "创建会话失败", - "failed": "失败" + "failed": "失败", + "selectAll": "全选", + "bulkDelete": "批量删除", + "selectedCount": "已选择 {{count}} 项", + "bulkDeleteConfirm": "确定要删除选中的 {{count}} 个工具吗?此操作无法撤销。", + "bulkDeleteSuccess": "成功删除 {{count}} 个工具", + "deleteSuccess": "删除成功", + "toolsCount": "个工具", + "partialFailed": "部分失败" }, "toolCategories": { "file": "文件操作", @@ -1148,6 +1156,14 @@ "noAgentInstalledTools": "暂无 Agent 安装的工具。", "removeFromAgent": "从 Agent 中移除 \"{{name}}\"?", "delete": "删除", + "selectAll": "全选", + "bulkDelete": "批量删除", + "selectedCount": "已选择 {{count}} 项", + "bulkDeleteConfirm": "确定要删除选中的 {{count}} 个工具吗?此操作无法撤销。", + "bulkDeleteSuccess": "成功删除 {{count}} 个工具", + "deleteFailed": "删除失败", + "deleteSuccess": "删除成功", + "toolsCount": "个工具", "addMcpServer": "添加 MCP 服务器", "mcpServer": "MCP 服务器", "jsonConfig": "JSON 配置", diff --git a/frontend/src/pages/AgentDetail.tsx b/frontend/src/pages/AgentDetail.tsx index a1d40390..52bb86a2 100644 --- a/frontend/src/pages/AgentDetail.tsx +++ b/frontend/src/pages/AgentDetail.tsx @@ -17,7 +17,7 @@ import { useAppStore } from '../stores'; import { useAuthStore } from '../stores'; import { copyToClipboard } from '../utils/clipboard'; import { formatFileSize } from '../utils/formatFileSize'; -import { IconPaperclip, IconSend } from '@tabler/icons-react'; +import { IconPaperclip, IconSend, IconTrash, IconRobot, IconPlug, IconMessage, IconBolt, IconSend2, IconFileText, IconAlertCircle, IconClock, IconHeart, IconBuildingArch } from '@tabler/icons-react'; const TABS = ['status', 'aware', 'mind', 'tools', 'skills', 'relationships', 'workspace', 'chat', 'activityLog', 'approvals', 'settings'] as const; @@ -55,12 +55,21 @@ function ToolsManager({ agentId, canManage = false }: { agentId: string; canMana const [configSaving, setConfigSaving] = useState(false); const [toolTab, setToolTab] = useState<'company' | 'installed'>('company'); const [deletingToolId, setDeletingToolId] = useState(null); + const [selectedToolIds, setSelectedToolIds] = useState>(new Set()); + const [bulkDeleteConfirm, setBulkDeleteConfirm] = useState(null); + const [singleDeleteConfirm, setSingleDeleteConfirm] = useState<{ id: string; agentToolId: string; name: string } | null>(null); + const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null); const [configCategory, setConfigCategory] = useState(null); const [focusedField, setFocusedField] = useState(null); // Global (company-level) config for the currently open modal — used to show // lock hints and prevent agent from overriding company-set fields. const [configGlobalData, setConfigGlobalData] = useState>({}); + const showToast = (message: string, type: 'success' | 'error' = 'success') => { + setToast({ message, type }); + setTimeout(() => setToast(null), 3000); + }; + const CATEGORY_CONFIG_SCHEMAS: Record = { agentbay: { title: 'AgentBay Settings', @@ -284,6 +293,37 @@ function ToolsManager({ agentId, canManage = false }: { agentId: string; canMana return (
+ {/* Checkbox for agent-installed tools when in installed tab */} + {toolTab === 'installed' && tool.source === 'agent' && tool.agent_tool_id && canManage && ( + + )} {tool.icon}
@@ -314,22 +354,10 @@ function ToolsManager({ agentId, canManage = false }: { agentId: string; canMana )} {canManage && tool.source === 'agent' && tool.agent_tool_id && ( )} @@ -385,7 +413,7 @@ function ToolsManager({ agentId, canManage = false }: { agentId: string; canMana {t('agent.tools.companyTools', 'Company Tools')} ({companyTools.length})
+ {/* Bulk Delete Bar for Agent-Installed Tools */} + {toolTab === 'installed' && agentInstalledTools.length > 0 && canManage && ( +
+ + {selectedToolIds.size > 0 ? ( +
+ + {t('agent.tools.selectedCount', { count: selectedToolIds.size, defaultValue: `已选择 ${selectedToolIds.size} 项` })} + + +
+ ) : ( + + {agentInstalledTools.length} {t('agent.tools.toolsCount', '个工具')} + + )} +
+ )} + + {/* Bulk Delete Confirm Modal */} + setBulkDeleteConfirm(null)} + onConfirm={async () => { + const count = bulkDeleteConfirm; + setBulkDeleteConfirm(null); + try { + const token = localStorage.getItem('token'); + const res = await fetch('/api/tools/agent-tools/bulk', { + method: 'DELETE', + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, + body: JSON.stringify({ agent_tool_ids: Array.from(selectedToolIds) }), + }); + if (res.ok) { + const result = await res.json(); + if (result.errors && result.errors.length > 0) { + // 有部分错误 + const errorMsg = result.errors.map((e: any) => e.error).join(', '); + showToast(`${t('agent.tools.bulkDeleteSuccess', { count: result.deleted })} (${result.errors.length} ${t('agent.tools.deleteFailed', '部分失败')})`, result.deleted > 0 ? 'success' : 'error'); + } else { + showToast(t('agent.tools.bulkDeleteSuccess', { count: result.deleted, defaultValue: `成功删除 ${result.deleted} 个工具` })); + } + setSelectedToolIds(new Set()); + loadTools(); + } else { + showToast(t('agent.tools.deleteFailed', '删除失败'), 'error'); + } + } catch (e) { + showToast(t('agent.tools.deleteFailed', '删除失败') + ': ' + e, 'error'); + } + }} + /> + + {/* Single Delete Confirm Modal */} + setSingleDeleteConfirm(null)} + onConfirm={async () => { + const target = singleDeleteConfirm; + setSingleDeleteConfirm(null); + if (!target) return; + setDeletingToolId(target.id); + try { + const token = localStorage.getItem('token'); + const res = await fetch(`/api/tools/agent-tool/${target.agentToolId}`, { + method: 'DELETE', + headers: { Authorization: `Bearer ${token}` }, + }); + if (res.ok) { + showToast(t('agent.tools.deleteSuccess', '删除成功')); + loadTools(); + } else { + showToast(t('agent.tools.deleteFailed', '删除失败'), 'error'); + } + } catch (e) { + showToast(t('agent.tools.deleteFailed', '删除失败') + ': ' + e, 'error'); + } + setDeletingToolId(null); + }} + /> + + {/* Toast */} + {toast && ( +
+ {toast.message} +
+ )} + {/* Tool List */} {activeTools.length > 0 ? ( renderToolGroup(groupByCategory(activeTools)) @@ -2159,9 +2320,10 @@ function AgentDetailInner() { const fe = msg.fileName?.split('.').pop()?.toLowerCase() ?? ''; const fi = fe === 'pdf' ? '📄' : (fe === 'csv' || fe === 'xlsx' || fe === 'xls') ? '📊' : (fe === 'docx' || fe === 'doc') ? '📝' : '📎'; const isImage = msg.imageUrl && ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'].includes(fe); - const resolvedSenderLabel = msg.sender_name || senderLabel; - const resolvedAvatarText = avatarText || (resolvedSenderLabel ? resolvedSenderLabel[0] : (isLeft ? 'A' : 'U')); - const showSenderLabel = !!resolvedSenderLabel && (forceSenderLabel || !!msg.sender_name); + + const resolvedAvatarText = avatarText ?? (isLeft ? (msg.sender_name ? msg.sender_name[0] : 'A') : 'U'); + const showSenderLabel = forceSenderLabel || (isLeft && msg.sender_name); + const resolvedSenderLabel = senderLabel ?? msg.sender_name; const timestampHtml = msg.timestamp ? (() => { const d = new Date(msg.timestamp); @@ -2184,7 +2346,7 @@ function AgentDetailInner() {
{resolvedAvatarText}
- {showSenderLabel &&
{resolvedSenderLabel}
} + {showSenderLabel &&
{resolvedSenderLabel}
} {isImage ? (
{msg.fileName} @@ -3471,7 +3633,7 @@ function AgentDetailInner() { }} />
- {(session.title || 'Trigger execution').replace(/^🤖\s*/, '')} + {(session.title || 'Trigger execution').replace(/^.?\s*/, '')}
{new Date(session.created_at).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })} @@ -4308,7 +4470,7 @@ function AgentDetailInner() { pointerEvents: 'none', }} > - {activeSession.source_channel === 'agent' ? `🤖 Agent Conversation · ${activeSession.username || 'Agents'}` : `Read-only · ${activeSession.username || 'User'}`} + {activeSession.source_channel === 'agent' ? <> Agent Conversation · {activeSession.username || 'Agents'} : `Read-only · ${activeSession.username || 'User'}`}
{(() => { @@ -4380,9 +4542,9 @@ function AgentDetailInner() { i={i} isLeft={isLeft} t={t} - senderLabel={isHumanReadonly ? (isLeft ? ((agent as any)?.name || 'Agent') : (activeSession.username || 'User')) : undefined} - avatarText={isHumanReadonly ? (isLeft ? (((agent as any)?.name || 'Agent')[0]) : ((activeSession.username || 'User')[0])) : undefined} - forceSenderLabel={isHumanReadonly} + senderLabel={isHumanReadonly ? (isLeft ? ((agent as any)?.name || 'Agent') : (activeSession.username || 'User')) : (isA2A && m.sender_name && m.sender_name !== thisAgentName ? m.sender_name : undefined)} + avatarText={isHumanReadonly ? (isLeft ? (((agent as any)?.name || 'Agent')[0]) : ((activeSession.username || 'User')[0])) : (isLeft ? (m.sender_name ? m.sender_name[0] : 'A') : 'U')} + forceSenderLabel={isHumanReadonly || (isA2A && !!m.sender_name && m.sender_name !== thisAgentName)} /> ); }); @@ -4721,11 +4883,19 @@ function AgentDetailInner() { {filteredLogs.length > 0 ? (
{filteredLogs.map((log: any) => { - const icons: Record = { - chat_reply: '💬', tool_call: '⚡', feishu_msg_sent: '📤', - agent_msg_sent: '🤖', web_msg_sent: '🌐', task_created: '📋', - task_updated: '✅', file_written: '📝', error: '❌', - schedule_run: '⏰', heartbeat: '💓', plaza_post: '🏛️', + const iconComponents: Record = { + chat_reply: , + tool_call: , + feishu_msg_sent: , + agent_msg_sent: , + web_msg_sent: , + task_created: , + task_updated: , + file_written: , + error: , + schedule_run: , + heartbeat: , + plaza_post: , }; const time = log.created_at ? new Date(log.created_at).toLocaleString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', @@ -4742,8 +4912,8 @@ function AgentDetailInner() { }} >
- - {icons[log.action_type] || '·'} + + {iconComponents[log.action_type] || '·'}
{log.summary}
diff --git a/frontend/src/pages/EnterpriseSettings.tsx b/frontend/src/pages/EnterpriseSettings.tsx index 666b829d..126b7c41 100644 --- a/frontend/src/pages/EnterpriseSettings.tsx +++ b/frontend/src/pages/EnterpriseSettings.tsx @@ -1,8 +1,10 @@ import { useState, useEffect, useMemo } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; +import { IconTrash, IconPlug, IconRobot } from '@tabler/icons-react'; import { enterpriseApi, skillApi } from '../services/api'; import PromptModal from '../components/PromptModal'; +import ConfirmModal from '../components/ConfirmModal'; import FileBrowser from '../components/FileBrowser'; import type { FileBrowserApi } from '../components/FileBrowser'; import { saveAccentColor, getSavedAccentColor, resetAccentColor, PRESET_COLORS } from '../utils/theme'; @@ -1788,6 +1790,15 @@ export default function EnterpriseSettings() { }; const [toolsView, setToolsView] = useState<'global' | 'agent-installed'>('global'); const [agentInstalledTools, setAgentInstalledTools] = useState([]); + const [selectedToolIds, setSelectedToolIds] = useState>(new Set()); + const [bulkDeleteConfirm, setBulkDeleteConfirm] = useState(null); + const [singleDeleteConfirm, setSingleDeleteConfirm] = useState<{ agentToolId: string; name: string } | null>(null); + const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null); + + const showToast = (message: string, type: 'success' | 'error' = 'success') => { + setToast({ message, type }); + setTimeout(() => setToast(null), 3000); + }; const loadAllTools = async () => { const tid = selectedTenantId; const data = await fetchJson(`/tools${tid ? `?tenant_id=${tid}` : ''}`); @@ -1798,6 +1809,7 @@ export default function EnterpriseSettings() { const tid = selectedTenantId; const data = await fetchJson(`/tools/agent-installed${tid ? `?tenant_id=${tid}` : ''}`); setAgentInstalledTools(data); + setSelectedToolIds(new Set()); // Reset selection on load } catch { } }; useEffect(() => { if (activeTab === 'tools') { loadAllTools(); loadAgentInstalledTools(); } }, [activeTab, selectedTenantId]); @@ -2552,31 +2564,165 @@ export default function EnterpriseSettings() { {agentInstalledTools.length === 0 ? (
{t('enterprise.tools.noAgentInstalledTools')}
) : ( -
- {agentInstalledTools.map((row: any) => ( -
-
-
- 🔌 {row.tool_display_name} - {row.mcp_server_name && MCP} -
-
- 🤖 {row.installed_by_agent_name || 'Unknown Agent'} - {row.installed_at && · {new Date(row.installed_at).toLocaleString()}} + <> + {/* Bulk actions bar */} +
+ + {selectedToolIds.size > 0 ? ( +
+ + {t('enterprise.tools.selectedCount', { count: selectedToolIds.size })} + + +
+ ) : ( + + {agentInstalledTools.length} {t('enterprise.tools.toolsCount', '个工具')} + + )} +
+ {/* Tool list with checkboxes */} +
+ {agentInstalledTools.map((row: any) => ( +
+
+ +
+
+ {row.tool_display_name} + {row.mcp_server_name && MCP} +
+
+ {row.installed_by_agent_name || 'Unknown Agent'} + {row.installed_at && · {new Date(row.installed_at).toLocaleString()}} +
+
+
-
+ + {/* Bulk Delete Confirm Modal */} + setBulkDeleteConfirm(null)} + onConfirm={async () => { + setBulkDeleteConfirm(null); + try { + const result = await fetchJson<{ deleted: number; errors?: Array<{ id: string; error: string }> }>('/tools/agent-tools/bulk', { + method: 'DELETE', + body: JSON.stringify({ agent_tool_ids: Array.from(selectedToolIds) }), + }); + if (result.errors && result.errors.length > 0) { + showToast(`${t('enterprise.tools.bulkDeleteSuccess', { count: result.deleted })} (${result.errors.length} ${t('enterprise.tools.deleteFailed', '部分失败')})`, result.deleted > 0 ? 'success' : 'error'); + } else { + showToast(t('enterprise.tools.bulkDeleteSuccess', { count: result.deleted })); } - loadAgentInstalledTools(); - }}>🗑️ {t('enterprise.tools.delete')} + } catch (e: any) { + showToast(e.message || t('enterprise.tools.deleteFailed', '删除失败'), 'error'); + } + loadAgentInstalledTools(); + }} + /> + + {/* Single Delete Confirm Modal */} + setSingleDeleteConfirm(null)} + onConfirm={async () => { + const target = singleDeleteConfirm; + setSingleDeleteConfirm(null); + if (!target) return; + try { + await fetchJson(`/tools/agent-tool/${target.agentToolId}`, { method: 'DELETE' }); + showToast(t('enterprise.tools.deleteSuccess', '删除成功')); + } catch { + showToast(t('enterprise.tools.deleteFailed', '删除失败'), 'error'); + } + loadAgentInstalledTools(); + }} + /> + + {/* Toast */} + {toast && ( +
+ {toast.message}
- ))} -
+ )} + )}
)}