- Agents, Chat, Settings, Skill, Tools - Account, Plan, Script - useSkills composable Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
314 lines
10 KiB
Vue
314 lines
10 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import {
|
|
FileText,
|
|
Globe,
|
|
Calculator,
|
|
Code,
|
|
Braces,
|
|
Github,
|
|
MessageSquare,
|
|
Mail,
|
|
Database,
|
|
Folder,
|
|
GitBranch,
|
|
Box,
|
|
Wrench,
|
|
Server,
|
|
Terminal,
|
|
Search,
|
|
Plus,
|
|
Pause,
|
|
Play,
|
|
Edit,
|
|
Trash2,
|
|
X,
|
|
} from 'lucide-vue-next'
|
|
import { useTools } from './tools/useTools'
|
|
import '@/views/database/database.css'
|
|
|
|
// 使用工具 composable
|
|
const { tools, toolsLoading, fetchTools, syncTools, deleteTool: deleteToolApi } = useTools()
|
|
|
|
// 图标组件映射
|
|
const iconComponents: Record<string, any> = {
|
|
FileText,
|
|
Globe,
|
|
Calculator,
|
|
Code,
|
|
Braces,
|
|
Github,
|
|
MessageSquare,
|
|
Mail,
|
|
Database,
|
|
Folder,
|
|
GitBranch,
|
|
Box,
|
|
Wrench,
|
|
Server,
|
|
Terminal,
|
|
}
|
|
|
|
const getIconComponent = (iconName?: string) => {
|
|
if (!iconName) return Box
|
|
return iconComponents[iconName] || Box
|
|
}
|
|
|
|
interface Tool {
|
|
id: string
|
|
name: string
|
|
description: string
|
|
category: string
|
|
provider: string
|
|
security_level: string
|
|
require_approval: boolean
|
|
parameters: string
|
|
status: string
|
|
type: 'built-in' | 'mcp'
|
|
createdAt?: string
|
|
}
|
|
|
|
// 页面加载时获取工具列表
|
|
onMounted(async () => {
|
|
await fetchTools()
|
|
})
|
|
|
|
// 按类型分类工具
|
|
const builtInTools = computed(() => {
|
|
return tools.value.filter(t => t.provider === 'system')
|
|
})
|
|
|
|
const mcpTools = computed(() => {
|
|
return tools.value.filter(t => t.provider !== 'system')
|
|
})
|
|
|
|
const activeTab = ref<'built-in' | 'mcp'>('built-in')
|
|
const searchQuery = ref('')
|
|
const filterStatus = ref('all')
|
|
const editingTool = ref<Tool | null>(null)
|
|
const isEditing = ref(false)
|
|
|
|
const editForm = ref({
|
|
name: '',
|
|
description: '',
|
|
provider: '',
|
|
})
|
|
|
|
// Statistics
|
|
const stats = computed(() => ({
|
|
total: tools.value.length,
|
|
active: tools.value.filter(t => t.status === 'active').length,
|
|
builtIn: builtInTools.value.length,
|
|
mcp: mcpTools.value.length
|
|
}))
|
|
|
|
const currentTools = computed(() => {
|
|
let toolsList: Tool[] = []
|
|
switch (activeTab.value) {
|
|
case 'built-in': toolsList = builtInTools.value as Tool[]; break
|
|
case 'mcp': toolsList = mcpTools.value as Tool[]; break
|
|
}
|
|
return toolsList.filter(tool => {
|
|
const matchSearch = tool.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
|
tool.description.toLowerCase().includes(searchQuery.value.toLowerCase())
|
|
const matchStatus = filterStatus.value === 'all' || tool.status === filterStatus.value
|
|
return matchSearch && matchStatus
|
|
})
|
|
})
|
|
|
|
const tabCounts = computed(() => ({
|
|
'built-in': builtInTools.value.length,
|
|
'mcp': mcpTools.value.length,
|
|
}))
|
|
|
|
const openEdit = (tool: any) => {
|
|
editingTool.value = tool
|
|
editForm.value = { name: tool.name, description: tool.description, provider: tool.provider || '' }
|
|
isEditing.value = true
|
|
}
|
|
|
|
const saveEdit = () => {
|
|
isEditing.value = false
|
|
}
|
|
|
|
const cancelEdit = () => { isEditing.value = false; editingTool.value = null }
|
|
|
|
const toggleStatus = async (tool: any) => {
|
|
const newStatus = tool.status === 'active' ? 'inactive' : 'active'
|
|
// TODO: 调用 API 更新状态
|
|
}
|
|
|
|
const handleDeleteTool = async (id: string) => {
|
|
if (confirm('Are you sure you want to delete this tool?')) {
|
|
await deleteToolApi(id)
|
|
}
|
|
}
|
|
|
|
// 同步工具
|
|
const handleSyncTools = async () => {
|
|
await syncTools()
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="p-6 min-h-screen">
|
|
<!-- 顶部导航 -->
|
|
<div class="flex justify-between items-center mb-6 h-10">
|
|
<div class="flex items-center gap-2">
|
|
<Wrench class="w-5 h-5 text-orange-500" />
|
|
<span class="font-medium">Tools</span>
|
|
</div>
|
|
<div class="h-10 flex gap-2">
|
|
<button v-if="activeTab === 'built-in'" @click="handleSyncTools" class="btn-secondary">
|
|
<i class="fa-solid fa-sync mr-1"></i>
|
|
Sync Tools
|
|
</button>
|
|
<button v-if="activeTab === 'mcp'" class="btn-primary">
|
|
<Plus class="w-4 h-4 mr-1" />
|
|
Add MCP
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab 导航 -->
|
|
<div class="flex items-center gap-2 mb-6">
|
|
<button
|
|
v-for="(label, tab) in { 'built-in': 'Built-in', 'mcp': 'MCP Servers' }"
|
|
:key="tab"
|
|
@click="activeTab = tab as 'built-in' | 'mcp'"
|
|
class="px-4 py-2 rounded-lg font-medium flex items-center gap-2 transition-all"
|
|
:class="activeTab === tab
|
|
? 'bg-orange-500 text-white'
|
|
: 'bg-dark-700 text-gray-400 hover:bg-dark-600 hover:text-white'"
|
|
>
|
|
<span>{{ label }}</span>
|
|
<span class="px-2 py-0.5 rounded-full text-xs" :class="activeTab === tab ? 'bg-white/20' : 'bg-dark-600'">
|
|
{{ tabCounts[tab as keyof typeof tabCounts] }}
|
|
</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 搜索和筛选 -->
|
|
<div class="flex gap-4 mb-6">
|
|
<div class="flex-1 relative">
|
|
<Search class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
|
<input
|
|
v-model="searchQuery"
|
|
type="text"
|
|
placeholder="Search tools by name or description..."
|
|
class="search-input w-full pl-10"
|
|
>
|
|
</div>
|
|
<el-select v-model="filterStatus" placeholder="Select" class="w-40" size="large">
|
|
<el-option label="All Status" value="all" />
|
|
<el-option label="Active" value="active" />
|
|
<el-option label="Inactive" value="inactive" />
|
|
</el-select>
|
|
</div>
|
|
|
|
<!-- Tools 列表 -->
|
|
<div class="bg-dark-700 rounded-xl overflow-hidden">
|
|
<!-- Loading -->
|
|
<div v-if="toolsLoading" class="py-12 text-center text-gray-500">
|
|
<i class="fa-solid fa-spinner fa-spin text-2xl"></i>
|
|
</div>
|
|
<table v-else-if="currentTools.length > 0" class="w-full">
|
|
<thead class="bg-dark-600">
|
|
<tr>
|
|
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Tool Name</th>
|
|
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Provider</th>
|
|
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Status</th>
|
|
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Created</th>
|
|
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="tool in currentTools" :key="tool.id" class="table-row">
|
|
<td class="px-5 py-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-10 h-10 rounded-lg flex items-center justify-center" :class="tool.type === 'built-in' ? 'bg-orange-500/20' : 'bg-emerald-500/20'">
|
|
<component :is="getIconComponent(tool.icon)" class="w-5 h-5" :class="tool.type === 'built-in' ? 'text-orange-400' : 'text-emerald-400'" />
|
|
</div>
|
|
<div>
|
|
<div class="font-medium">{{ tool.name }}</div>
|
|
<div class="text-sm text-gray-500">{{ tool.description }}</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="px-5 py-4 text-center">
|
|
<span class="text-gray-400 text-sm">{{ tool.provider || (tool.type === 'built-in' ? 'System' : 'MCP Server') }}</span>
|
|
</td>
|
|
<td class="px-5 py-4 text-center">
|
|
<span class="inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs" :class="tool.status === 'active' ? 'bg-green-500/20 text-green-400' : 'bg-gray-500/20 text-gray-400'">
|
|
<span class="w-1.5 h-1.5 rounded-full" :class="tool.status === 'active' ? 'bg-green-500' : 'bg-gray-400'"></span>
|
|
<span class="capitalize">{{ tool.status }}</span>
|
|
</span>
|
|
</td>
|
|
<td class="px-5 py-4 text-center text-gray-400 text-sm">{{ tool.created_at || '-' }}</td>
|
|
<td class="px-5 py-4">
|
|
<div class="flex items-center justify-center gap-2">
|
|
<button @click="toggleStatus(tool)" class="btn-icon" :title="tool.status === 'active' ? 'Deactivate' : 'Activate'">
|
|
<component :is="tool.status === 'active' ? Pause : Play" class="w-4 h-4 text-gray-400 hover:text-white" />
|
|
</button>
|
|
<button @click="openEdit(tool)" class="btn-icon" title="Edit">
|
|
<Edit class="w-4 h-4 text-gray-400 hover:text-white" />
|
|
</button>
|
|
<button @click="handleDeleteTool(tool.id)" class="btn-icon" title="Delete">
|
|
<Trash2 class="w-4 h-4 text-gray-400 hover:text-red-400" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<!-- 空状态 -->
|
|
<div v-else class="empty-box">
|
|
<div class="empty-icon">
|
|
<Wrench class="w-12 h-12" />
|
|
</div>
|
|
<p class="empty-text">No tools found</p>
|
|
<p class="empty-tip">Click "Add MCP" to add a new MCP server</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 编辑弹窗 -->
|
|
<Teleport to="body">
|
|
<div v-if="isEditing" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50 modal-overlay">
|
|
<div class="bg-dark-700 rounded-2xl w-full max-w-lg border border-dark-500 shadow-2xl modal-content">
|
|
<div class="flex items-center justify-between p-5 border-b border-dark-500">
|
|
<h3 class="text-lg font-semibold">Edit Tool</h3>
|
|
<button @click="cancelEdit" class="btn-icon">
|
|
<X class="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
|
|
<div class="p-5 space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">Tool Name</label>
|
|
<input v-model="editForm.name" type="text" class="input-field">
|
|
</div>
|
|
<div v-if="activeTab !== 'built-in'">
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">Provider</label>
|
|
<input v-model="editForm.provider" type="text" class="input-field">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label>
|
|
<textarea v-model="editForm.description" rows="3" class="input-field resize-none"></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
|
|
<button @click="cancelEdit" class="btn-secondary">Cancel</button>
|
|
<button @click="saveEdit" class="btn-primary">Save Changes</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Teleport>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* 使用全局样式 */
|
|
</style>
|