1091 lines
28 KiB
Vue
1091 lines
28 KiB
Vue
<template>
|
||
<div class="mcp-settings-page">
|
||
<!-- 页面头部 -->
|
||
<div class="page-header">
|
||
<div class="header-info">
|
||
<h1>MCP 设置</h1>
|
||
<p>Model Context Protocol 服务器配置和管理</p>
|
||
</div>
|
||
<div class="header-stats">
|
||
<div class="stat-item">
|
||
<span class="stat-value">{{ servers.length }}</span>
|
||
<span class="stat-label">总计</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span class="stat-value">{{ connectedServers.length }}</span>
|
||
<span class="stat-label">已连接</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span class="stat-value">{{ availableTools.length }}</span>
|
||
<span class="stat-label">可用工具</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 快速操作 -->
|
||
<div class="quick-actions">
|
||
<n-button type="primary" @click="showAddModal = true">
|
||
<template #icon>
|
||
<n-icon :component="PlusIcon" />
|
||
</template>
|
||
添加服务器
|
||
</n-button>
|
||
<n-button @click="refreshAllServers" :loading="refreshing">
|
||
<template #icon>
|
||
<n-icon :component="RefreshIcon" />
|
||
</template>
|
||
刷新全部
|
||
</n-button>
|
||
<n-button @click="testAllConnections" :loading="testing">
|
||
<template #icon>
|
||
<n-icon :component="TestIcon" />
|
||
</template>
|
||
测试连接
|
||
</n-button>
|
||
<n-button @click="checkAllStatus" :loading="checking">
|
||
<template #icon>
|
||
<n-icon :component="CheckIcon" />
|
||
</template>
|
||
检查状态
|
||
</n-button>
|
||
</div>
|
||
|
||
<!-- 服务器列表 -->
|
||
<div class="servers-section">
|
||
<div class="section-header">
|
||
<h2>已配置的服务器</h2>
|
||
<div class="section-controls">
|
||
<n-input
|
||
v-model:value="searchText"
|
||
placeholder="搜索服务器..."
|
||
clearable
|
||
style="width: 200px"
|
||
>
|
||
<template #prefix>
|
||
<n-icon :component="SearchIcon" />
|
||
</template>
|
||
</n-input>
|
||
<n-select
|
||
v-model:value="filterStatus"
|
||
:options="statusFilters"
|
||
placeholder="状态筛选"
|
||
clearable
|
||
style="width: 150px"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="servers-grid">
|
||
<div
|
||
v-for="server in filteredServers"
|
||
:key="server.id"
|
||
class="server-card"
|
||
:class="{
|
||
'connected': server.status === 'connected',
|
||
'error': server.status === 'error',
|
||
'connecting': server.status === 'connecting'
|
||
}"
|
||
>
|
||
<!-- 服务器头部信息 -->
|
||
<div class="server-header">
|
||
<div class="server-info">
|
||
<div class="server-avatar">
|
||
<n-icon :component="getServerIcon(server.type)" size="20" />
|
||
</div>
|
||
<div class="server-details">
|
||
<h3>{{ server.name }}</h3>
|
||
<p>{{ server.url }}</p>
|
||
</div>
|
||
</div>
|
||
<div class="server-status">
|
||
<n-tag
|
||
:type="getStatusType(server.status)"
|
||
size="small"
|
||
:bordered="false"
|
||
>
|
||
{{ getStatusText(server.status) }}
|
||
</n-tag>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 服务器描述 -->
|
||
<div class="server-description" v-if="server.description">
|
||
<p>{{ server.description }}</p>
|
||
</div>
|
||
|
||
<!-- 服务器能力 -->
|
||
<div class="server-capabilities" v-if="server.status === 'connected' && server.capabilities">
|
||
<div class="capabilities-grid">
|
||
<div class="capability-item">
|
||
<n-icon :component="ToolIcon" />
|
||
<span>{{ server.capabilities.tools?.length || 0 }} 工具</span>
|
||
</div>
|
||
<div class="capability-item">
|
||
<n-icon :component="FileIcon" />
|
||
<span>{{ server.capabilities.resources?.length || 0 }} 资源</span>
|
||
</div>
|
||
<div class="capability-item">
|
||
<n-icon :component="TemplateIcon" />
|
||
<span>{{ server.capabilities.prompts?.length || 0 }} 提示</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 工具列表 -->
|
||
<div class="server-tools" v-if="server.status === 'connected' && server.capabilities?.tools?.length">
|
||
<div class="tools-header">
|
||
<span>可用工具</span>
|
||
<n-button text size="tiny" @click="toggleToolsList(server.id)">
|
||
{{ expandedTools.includes(server.id) ? '收起' : '展开' }}
|
||
</n-button>
|
||
</div>
|
||
<div v-if="expandedTools.includes(server.id)" class="tools-list">
|
||
<div
|
||
v-for="tool in server.capabilities.tools.slice(0, 5)"
|
||
:key="tool.name"
|
||
class="tool-item"
|
||
@click="executeToolModal(server.id, tool)"
|
||
>
|
||
<span class="tool-name">{{ tool.name }}</span>
|
||
<span class="tool-desc">{{ tool.description || '无描述' }}</span>
|
||
</div>
|
||
<div v-if="server.capabilities.tools.length > 5" class="tools-more">
|
||
+{{ server.capabilities.tools.length - 5 }} 更多工具
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 服务器操作 -->
|
||
<div class="server-actions">
|
||
<n-button
|
||
v-if="server.status !== 'connected' && server.status !== 'connecting'"
|
||
type="primary"
|
||
size="small"
|
||
@click="connectServer(server.id)"
|
||
:loading="false"
|
||
>
|
||
连接
|
||
</n-button>
|
||
<n-button
|
||
v-else-if="server.status === 'connected'"
|
||
size="small"
|
||
@click="disconnectServer(server.id)"
|
||
>
|
||
断开
|
||
</n-button>
|
||
|
||
<n-button size="small" @click="openServerDetail(server)">
|
||
<template #icon>
|
||
<n-icon :component="EditIcon" />
|
||
</template>
|
||
编辑
|
||
</n-button>
|
||
|
||
<n-button size="small" @click="testServer(server.id)" :loading="server.id === testingServerId">
|
||
<template #icon>
|
||
<n-icon :component="TestIcon" />
|
||
</template>
|
||
</n-button>
|
||
|
||
<n-dropdown :options="getServerMenuOptions(server)" @select="handleServerAction">
|
||
<n-button size="small">
|
||
<template #icon>
|
||
<n-icon :component="DotsIcon" />
|
||
</template>
|
||
</n-button>
|
||
</n-dropdown>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 空状态 -->
|
||
<div v-if="filteredServers.length === 0" class="empty-state">
|
||
<div class="empty-content">
|
||
<n-icon :component="ServerIcon" size="48" />
|
||
<h3>{{ servers.length === 0 ? '暂无服务器' : '无匹配结果' }}</h3>
|
||
<p>{{ servers.length === 0 ? '点击添加按钮配置您的第一个MCP服务器' : '尝试调整搜索条件' }}</p>
|
||
|
||
<!-- 示例服务器快速添加 -->
|
||
<div v-if="servers.length === 0" class="quick-start">
|
||
<n-button type="primary" @click="showAddModal = true">
|
||
添加服务器
|
||
</n-button>
|
||
<n-button @click="addExampleServer" ghost>
|
||
添加示例服务器
|
||
</n-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MCP协议信息 -->
|
||
<div class="protocol-info">
|
||
<n-card title="关于 Model Context Protocol">
|
||
<div class="protocol-content">
|
||
<p>MCP (Model Context Protocol) 是一个开放标准,允许AI应用安全地连接到外部数据源和工具。</p>
|
||
<div class="protocol-features">
|
||
<div class="feature-item">
|
||
<n-icon :component="ShieldIcon" />
|
||
<span>安全可靠</span>
|
||
</div>
|
||
<div class="feature-item">
|
||
<n-icon :component="PlugIcon" />
|
||
<span>易于集成</span>
|
||
</div>
|
||
<div class="feature-item">
|
||
<n-icon :component="CodeIcon" />
|
||
<span>开源开放</span>
|
||
</div>
|
||
</div>
|
||
<div class="protocol-links">
|
||
<n-button text type="primary" tag="a" href="https://modelcontextprotocol.io" target="_blank">
|
||
了解更多
|
||
</n-button>
|
||
<n-button text type="primary" tag="a" href="https://github.com/modelcontextprotocol" target="_blank">
|
||
GitHub
|
||
</n-button>
|
||
</div>
|
||
</div>
|
||
</n-card>
|
||
</div>
|
||
|
||
<!-- 添加/编辑服务器模态框 -->
|
||
<n-modal v-model:show="showAddModal">
|
||
<n-card
|
||
style="width: 600px"
|
||
:title="editingServer ? '编辑MCP服务器' : '添加MCP服务器'"
|
||
:bordered="false"
|
||
size="huge"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
>
|
||
<ServerForm
|
||
:server="editingServer"
|
||
@submit="handleServerSubmit"
|
||
@cancel="handleServerCancel"
|
||
/>
|
||
</n-card>
|
||
</n-modal>
|
||
|
||
<!-- 工具执行模态框 -->
|
||
<n-modal v-model:show="showToolModal">
|
||
<n-card
|
||
style="width: 700px"
|
||
:title="`执行工具: ${selectedTool?.name}`"
|
||
:bordered="false"
|
||
size="huge"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
>
|
||
<ToolExecutor
|
||
v-if="selectedTool"
|
||
:server-id="selectedServerId"
|
||
:tool="selectedTool"
|
||
@close="showToolModal = false"
|
||
/>
|
||
</n-card>
|
||
</n-modal>
|
||
|
||
<!-- 服务器详情页面 -->
|
||
<n-modal
|
||
v-model:show="showServerDetail"
|
||
:style="{ padding: 0 }"
|
||
transform-origin="center"
|
||
>
|
||
<n-card
|
||
style="width: 90vw; max-width: 1200px; max-height: 90vh; overflow: auto;"
|
||
:bordered="false"
|
||
size="huge"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
>
|
||
<MCPServerDetail
|
||
v-if="editingServer"
|
||
:server="editingServer"
|
||
@back="showServerDetail = false"
|
||
@save="handleServerDetailSave"
|
||
@toggle-server="handleToggleServerFromDetail"
|
||
@toggle-tool="handleToggleToolFromDetail"
|
||
@toggle-auto-approve="handleToggleAutoApproveFromDetail"
|
||
/>
|
||
</n-card>
|
||
</n-modal>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import {
|
||
NButton,
|
||
NIcon,
|
||
NInput,
|
||
NSelect,
|
||
NTag,
|
||
NCard,
|
||
NModal,
|
||
NDropdown,
|
||
useMessage
|
||
} from 'naive-ui'
|
||
import {
|
||
Plus as PlusIcon,
|
||
Refresh as RefreshIcon,
|
||
Search as SearchIcon,
|
||
Tool as ToolIcon,
|
||
File as FileIcon,
|
||
Template as TemplateIcon,
|
||
Edit as EditIcon,
|
||
DotsVertical as DotsIcon,
|
||
Server as ServerIcon,
|
||
Shield as ShieldIcon,
|
||
Plug as PlugIcon,
|
||
Code as CodeIcon,
|
||
Activity as TestIcon,
|
||
Check as CheckIcon
|
||
} from '@vicons/tabler'
|
||
import { useServerStore } from '../stores/newServer'
|
||
import ServerForm from './ServerForm.vue'
|
||
import ToolExecutor from './ToolExecutor.vue'
|
||
import MCPServerDetail from './MCPServerDetail.vue'
|
||
|
||
const message = useMessage()
|
||
const serverStore = useServerStore()
|
||
|
||
// 组件挂载时自动重连之前已连接的服务器
|
||
onMounted(async () => {
|
||
console.log('🚀 MCPSettings 组件已挂载,开始自动重连...')
|
||
try {
|
||
await serverStore.autoReconnect()
|
||
} catch (error) {
|
||
console.error('自动重连失败:', error)
|
||
}
|
||
})
|
||
|
||
// 响应式数据
|
||
const showAddModal = ref(false)
|
||
const showToolModal = ref(false)
|
||
const showServerDetail = ref(false)
|
||
const editingServer = ref<any>(null)
|
||
const selectedTool = ref<any>(null)
|
||
const selectedServerId = ref<string>('')
|
||
const searchText = ref('')
|
||
const filterStatus = ref<string>('')
|
||
const expandedTools = ref<string[]>([])
|
||
const refreshing = ref(false)
|
||
const testing = ref(false)
|
||
const checking = ref(false)
|
||
const testingServerId = ref<string>('')
|
||
|
||
// 计算属性
|
||
const servers = computed(() => serverStore.servers)
|
||
const connectedServers = computed(() => serverStore.connectedServers)
|
||
const availableTools = computed(() => serverStore.availableTools)
|
||
|
||
const filteredServers = computed(() => {
|
||
let filtered = servers.value
|
||
|
||
// 文本搜索
|
||
if (searchText.value) {
|
||
const search = searchText.value.toLowerCase()
|
||
filtered = filtered.filter(server =>
|
||
server.name.toLowerCase().includes(search) ||
|
||
server.url.toLowerCase().includes(search) ||
|
||
server.description?.toLowerCase().includes(search)
|
||
)
|
||
}
|
||
|
||
// 状态筛选
|
||
if (filterStatus.value) {
|
||
filtered = filtered.filter(server => server.status === filterStatus.value)
|
||
}
|
||
|
||
return filtered
|
||
})
|
||
|
||
// 状态筛选选项
|
||
const statusFilters = [
|
||
{ label: '已连接', value: 'connected' },
|
||
{ label: '未连接', value: 'disconnected' },
|
||
{ label: '连接中', value: 'connecting' },
|
||
{ label: '错误', value: 'error' }
|
||
]
|
||
|
||
// 方法
|
||
const getServerIcon = (type: string) => {
|
||
const icons = {
|
||
http: ServerIcon,
|
||
sse: ServerIcon,
|
||
websocket: ServerIcon
|
||
}
|
||
return icons[type as keyof typeof icons] || ServerIcon
|
||
}
|
||
|
||
const getStatusType = (status: string): 'success' | 'error' | 'info' | 'default' => {
|
||
const types: Record<string, 'success' | 'error' | 'info' | 'default'> = {
|
||
connected: 'success',
|
||
disconnected: 'default',
|
||
connecting: 'info',
|
||
error: 'error'
|
||
}
|
||
return types[status] || 'default'
|
||
}
|
||
|
||
const getStatusText = (status: string) => {
|
||
const texts = {
|
||
connected: '已连接',
|
||
disconnected: '未连接',
|
||
connecting: '连接中...',
|
||
error: '连接失败'
|
||
}
|
||
return texts[status as keyof typeof texts] || status
|
||
}
|
||
|
||
const connectServer = async (serverId: string) => {
|
||
const server = servers.value.find(s => s.id === serverId)
|
||
if (!server) return
|
||
|
||
console.log(`🔗 尝试连接服务器: ${server.name} (${server.url})`)
|
||
|
||
try {
|
||
await serverStore.connectServer(serverId)
|
||
message.success(`服务器 ${server.name} 连接成功`)
|
||
} catch (error) {
|
||
console.error('连接失败:', error)
|
||
message.error(`服务器 ${server.name} 连接失败: ${error instanceof Error ? error.message : '未知错误'}`)
|
||
}
|
||
}
|
||
|
||
const disconnectServer = async (serverId: string) => {
|
||
try {
|
||
await serverStore.disconnectServer(serverId)
|
||
message.success('服务器已断开连接')
|
||
} catch (error) {
|
||
message.error('断开连接失败')
|
||
}
|
||
}
|
||
|
||
const testServer = async (serverId: string) => {
|
||
testingServerId.value = serverId
|
||
try {
|
||
const server = servers.value.find(s => s.id === serverId)
|
||
if (!server) {
|
||
throw new Error('服务器不存在')
|
||
}
|
||
|
||
// 真实连接测试
|
||
console.log(`🔍 测试服务器连接: ${server.name} (${server.url}), 类型: ${server.type}`)
|
||
|
||
// 根据服务器类型使用不同的测试方法
|
||
if (server.type === 'sse') {
|
||
// SSE服务器:尝试建立SSE连接
|
||
const testUrl = server.url.replace('0.0.0.0', 'localhost').replace('127.0.0.1', 'localhost')
|
||
console.log(`🔍 测试SSE连接: ${testUrl}`)
|
||
|
||
// 简单的健康检查 - 尝试GET请求
|
||
const response = await fetch(testUrl, {
|
||
method: 'GET',
|
||
headers: { 'Accept': 'text/event-stream' }
|
||
})
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||
}
|
||
|
||
// SSE连接成功建立,关闭连接
|
||
response.body?.cancel()
|
||
|
||
} else {
|
||
// HTTP服务器:发送JSON-RPC请求到 /mcp 端点
|
||
let testUrl = server.url.replace('0.0.0.0', 'localhost').replace('127.0.0.1', 'localhost')
|
||
|
||
// 确保URL包含 /mcp 路径
|
||
if (!testUrl.includes('/mcp')) {
|
||
testUrl = testUrl.replace(/\/$/, '') + '/mcp'
|
||
}
|
||
|
||
console.log(`🔍 测试HTTP连接: ${testUrl}`)
|
||
|
||
const response = await fetch(testUrl, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json, text/event-stream'
|
||
},
|
||
body: JSON.stringify({
|
||
jsonrpc: '2.0',
|
||
id: 'test-' + Date.now(),
|
||
method: 'initialize',
|
||
params: {
|
||
protocolVersion: '2024-11-05',
|
||
capabilities: {},
|
||
clientInfo: { name: 'mcp-test-client', version: '1.0.0' }
|
||
}
|
||
})
|
||
})
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||
}
|
||
|
||
const data = await response.json()
|
||
if (data.error) {
|
||
throw new Error(data.error.message || '服务器返回错误')
|
||
}
|
||
}
|
||
|
||
console.log(`✅ 服务器 ${server.name} 测试成功`)
|
||
message.success(`服务器 ${server.name} 连接测试成功`)
|
||
} catch (error) {
|
||
console.error(`❌ 服务器测试失败:`, error)
|
||
message.error(`服务器连接测试失败: ${error instanceof Error ? error.message : '未知错误'}`)
|
||
} finally {
|
||
testingServerId.value = ''
|
||
}
|
||
}
|
||
|
||
const openServerDetail = (server: any) => {
|
||
console.log('🔍 [1] 打开服务器详情被调用')
|
||
console.log('🔍 [2] 服务器数据:', server)
|
||
console.log('🔍 [3] 当前 showServerDetail 值:', showServerDetail.value)
|
||
|
||
try {
|
||
editingServer.value = { ...server } // 创建副本避免直接修改
|
||
console.log('🔍 [4] editingServer 设置完成:', editingServer.value)
|
||
|
||
showServerDetail.value = true
|
||
console.log('✅ [5] showServerDetail 设置为 true')
|
||
console.log('✅ [6] 最终状态检查 - showServerDetail:', showServerDetail.value, 'editingServer:', editingServer.value)
|
||
} catch (error) {
|
||
console.error('❌ openServerDetail 出错:', error)
|
||
}
|
||
}
|
||
|
||
const refreshAllServers = async () => {
|
||
refreshing.value = true
|
||
try {
|
||
// 刷新所有连接的服务器
|
||
const connectedIds = connectedServers.value.map(s => s.id)
|
||
await Promise.all(connectedIds.map(id => serverStore.connectServer(id)))
|
||
message.success('所有服务器已刷新')
|
||
} catch (error) {
|
||
message.error('刷新失败')
|
||
} finally {
|
||
refreshing.value = false
|
||
}
|
||
}
|
||
|
||
const testAllConnections = async () => {
|
||
testing.value = true
|
||
try {
|
||
console.log('🔍 开始测试所有服务器连接...')
|
||
|
||
// 重置所有服务器状态为未连接
|
||
servers.value.forEach(server => {
|
||
if (server.status === 'connected') {
|
||
server.status = 'disconnected'
|
||
server.capabilities = undefined
|
||
}
|
||
})
|
||
|
||
// 测试所有服务器连接
|
||
const results = await Promise.allSettled(
|
||
servers.value.map(server => testServer(server.id))
|
||
)
|
||
|
||
const successful = results.filter(r => r.status === 'fulfilled').length
|
||
const failed = results.filter(r => r.status === 'rejected').length
|
||
|
||
message.success(`连接测试完成: ${successful} 成功, ${failed} 失败`)
|
||
} catch (error) {
|
||
message.error('连接测试失败')
|
||
} finally {
|
||
testing.value = false
|
||
}
|
||
}
|
||
|
||
const checkAllStatus = async () => {
|
||
checking.value = true
|
||
try {
|
||
console.log('🔍 检查所有服务器真实连接状态...')
|
||
await serverStore.refreshAllStatus()
|
||
message.success('所有服务器状态已更新')
|
||
} catch (error) {
|
||
message.error('状态检查失败')
|
||
} finally {
|
||
checking.value = false
|
||
}
|
||
}
|
||
|
||
const toggleToolsList = (serverId: string) => {
|
||
const index = expandedTools.value.indexOf(serverId)
|
||
if (index > -1) {
|
||
expandedTools.value.splice(index, 1)
|
||
} else {
|
||
expandedTools.value.push(serverId)
|
||
}
|
||
}
|
||
|
||
const executeToolModal = (serverId: string, tool: any) => {
|
||
selectedServerId.value = serverId
|
||
selectedTool.value = tool
|
||
showToolModal.value = true
|
||
}
|
||
|
||
const handleServerSubmit = async (serverData: any) => {
|
||
try {
|
||
if (editingServer.value) {
|
||
// 编辑现有服务器 - 这里需要实现更新方法
|
||
message.success('服务器配置已更新')
|
||
} else {
|
||
await serverStore.addServer(serverData)
|
||
message.success('服务器添加成功')
|
||
}
|
||
handleServerCancel()
|
||
} catch (error) {
|
||
message.error('操作失败')
|
||
}
|
||
}
|
||
|
||
const handleServerCancel = () => {
|
||
editingServer.value = null
|
||
showAddModal.value = false
|
||
}
|
||
|
||
const getServerMenuOptions = (server: any) => [
|
||
{
|
||
label: '复制配置',
|
||
key: 'copy',
|
||
props: {
|
||
onClick: () => copyServerConfig(server)
|
||
}
|
||
},
|
||
{
|
||
label: '导出配置',
|
||
key: 'export',
|
||
props: {
|
||
onClick: () => exportServerConfig(server)
|
||
}
|
||
},
|
||
{
|
||
type: 'divider'
|
||
},
|
||
{
|
||
label: '删除',
|
||
key: 'delete',
|
||
props: {
|
||
onClick: () => deleteServer(server.id)
|
||
}
|
||
}
|
||
]
|
||
|
||
const handleServerAction = (_key: string) => {
|
||
// 处理服务器菜单操作 - 目前由各个选项的onClick直接处理
|
||
}
|
||
|
||
const copyServerConfig = (server: any) => {
|
||
navigator.clipboard.writeText(JSON.stringify(server, null, 2))
|
||
message.success('配置已复制到剪贴板')
|
||
}
|
||
|
||
const exportServerConfig = (server: any) => {
|
||
const config = JSON.stringify(server, null, 2)
|
||
const blob = new Blob([config], { type: 'application/json' })
|
||
const url = URL.createObjectURL(blob)
|
||
const a = document.createElement('a')
|
||
a.href = url
|
||
a.download = `${server.name}-config.json`
|
||
a.click()
|
||
URL.revokeObjectURL(url)
|
||
message.success('配置已导出')
|
||
}
|
||
|
||
const deleteServer = async (serverId: string) => {
|
||
try {
|
||
await serverStore.removeServer(serverId)
|
||
message.success('服务器已删除')
|
||
} catch (error) {
|
||
message.error('删除失败')
|
||
}
|
||
}
|
||
|
||
const addExampleServer = async () => {
|
||
const exampleServer = {
|
||
name: '示例 MCP 服务器',
|
||
url: 'http://localhost:3000/mcp',
|
||
type: 'http' as const,
|
||
description: '这是一个示例MCP服务器配置,您可以根据需要修改URL和设置。',
|
||
enabled: true,
|
||
settings: {
|
||
autoConnect: false,
|
||
retryAttempts: 3,
|
||
timeout: 30
|
||
}
|
||
}
|
||
|
||
try {
|
||
await serverStore.addServer(exampleServer)
|
||
message.success('示例服务器已添加,您可以修改配置并测试连接')
|
||
} catch (error) {
|
||
// 即使连接失败也要添加到列表中,方便用户修改配置
|
||
message.info('示例服务器已添加到列表,请修改URL后再进行连接')
|
||
}
|
||
}
|
||
|
||
const handleServerDetailSave = async (serverData: any) => {
|
||
try {
|
||
await serverStore.updateServer(serverData.id, serverData)
|
||
message.success('服务器配置已保存')
|
||
showServerDetail.value = false
|
||
} catch (error) {
|
||
message.error('保存失败')
|
||
}
|
||
}
|
||
|
||
const handleToggleServerFromDetail = async (serverId: string, enabled: boolean) => {
|
||
try {
|
||
await serverStore.updateServer(serverId, { enabled })
|
||
if (enabled) {
|
||
await serverStore.connectServer(serverId)
|
||
} else {
|
||
await serverStore.disconnectServer(serverId)
|
||
}
|
||
} catch (error) {
|
||
message.error('操作失败')
|
||
}
|
||
}
|
||
|
||
const handleToggleToolFromDetail = async (_serverId: string, toolName: string, enabled: boolean) => {
|
||
// TODO: 实现工具启用/禁用逻辑
|
||
message.info(`${enabled ? '启用' : '禁用'}工具: ${toolName}`)
|
||
}
|
||
|
||
const handleToggleAutoApproveFromDetail = async (_serverId: string, toolName: string, autoApprove: boolean) => {
|
||
// TODO: 实现工具自动批准逻辑
|
||
message.info(`${autoApprove ? '启用' : '禁用'}自动批准: ${toolName}`)
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.mcp-settings-page {
|
||
padding: 24px;
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.page-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.header-info h1 {
|
||
margin: 0 0 8px 0;
|
||
font-size: 28px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.header-info p {
|
||
margin: 0;
|
||
color: var(--text-color-2);
|
||
font-size: 16px;
|
||
}
|
||
|
||
.header-stats {
|
||
display: flex;
|
||
gap: 24px;
|
||
}
|
||
|
||
.stat-item {
|
||
text-align: center;
|
||
}
|
||
|
||
.stat-value {
|
||
display: block;
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: var(--primary-color);
|
||
}
|
||
|
||
.stat-label {
|
||
display: block;
|
||
font-size: 12px;
|
||
color: var(--text-color-3);
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.quick-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.servers-section {
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.section-header h2 {
|
||
margin: 0;
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.section-controls {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.servers-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
||
gap: 16px;
|
||
}
|
||
|
||
.server-card {
|
||
background: var(--card-color);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.server-card:hover {
|
||
border-color: var(--primary-color);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.server-card.connected {
|
||
border-color: var(--success-color);
|
||
}
|
||
|
||
.server-card.error {
|
||
border-color: var(--error-color);
|
||
}
|
||
|
||
.server-card.connecting {
|
||
border-color: var(--info-color);
|
||
}
|
||
|
||
.server-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.server-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.server-avatar {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 8px;
|
||
background: var(--primary-color-suppl);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--primary-color);
|
||
}
|
||
|
||
.server-details h3 {
|
||
margin: 0 0 4px 0;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.server-details p {
|
||
margin: 0;
|
||
font-size: 12px;
|
||
color: var(--text-color-3);
|
||
word-break: break-all;
|
||
}
|
||
|
||
.server-description {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.server-description p {
|
||
margin: 0;
|
||
font-size: 14px;
|
||
color: var(--text-color-2);
|
||
}
|
||
|
||
.server-capabilities {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.capabilities-grid {
|
||
display: flex;
|
||
gap: 16px;
|
||
}
|
||
|
||
.capability-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 12px;
|
||
color: var(--text-color-2);
|
||
}
|
||
|
||
.server-tools {
|
||
margin-bottom: 16px;
|
||
padding: 12px;
|
||
background: var(--hover-color);
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.tools-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.tools-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.tool-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 6px 8px;
|
||
background: var(--card-color);
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.tool-item:hover {
|
||
background: var(--primary-color-suppl);
|
||
}
|
||
|
||
.tool-name {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.tool-desc {
|
||
font-size: 11px;
|
||
color: var(--text-color-3);
|
||
}
|
||
|
||
.tools-more {
|
||
text-align: center;
|
||
font-size: 12px;
|
||
color: var(--text-color-3);
|
||
padding: 6px;
|
||
}
|
||
|
||
.server-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.empty-state {
|
||
grid-column: 1 / -1;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
min-height: 300px;
|
||
}
|
||
|
||
.empty-content {
|
||
text-align: center;
|
||
color: var(--text-color-3);
|
||
}
|
||
|
||
.empty-content h3 {
|
||
margin: 16px 0 8px 0;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.empty-content p {
|
||
margin: 0 0 16px 0;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.quick-start {
|
||
display: flex;
|
||
gap: 12px;
|
||
justify-content: center;
|
||
}
|
||
|
||
.protocol-info {
|
||
margin-top: 32px;
|
||
}
|
||
|
||
.protocol-content p {
|
||
margin: 0 0 16px 0;
|
||
color: var(--text-color-2);
|
||
}
|
||
|
||
.protocol-features {
|
||
display: flex;
|
||
gap: 24px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.feature-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 14px;
|
||
color: var(--text-color-2);
|
||
}
|
||
|
||
.protocol-links {
|
||
display: flex;
|
||
gap: 16px;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 768px) {
|
||
.page-header {
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.header-stats {
|
||
align-self: stretch;
|
||
justify-content: space-around;
|
||
}
|
||
|
||
.section-header {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 12px;
|
||
}
|
||
|
||
.section-controls {
|
||
width: 100%;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.servers-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.quick-actions {
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.protocol-features {
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
}
|
||
</style> |