|
|
@ -3,7 +3,7 @@ |
|
|
<p class="desc-text">管理 MCP (Model Context Protocol) 服务端配置,支持 SSE 和 Stdio 两种传输方式</p> |
|
|
<p class="desc-text">管理 MCP (Model Context Protocol) 服务端配置,支持 SSE 和 Stdio 两种传输方式</p> |
|
|
<div class="toolbar"> |
|
|
<div class="toolbar"> |
|
|
<t-button theme="primary" size="small" @click="openAddModal">新建配置</t-button> |
|
|
<t-button theme="primary" size="small" @click="openAddModal">新建配置</t-button> |
|
|
<t-button variant="outline" size="small" @click="refreshConnections" :disabled="refreshLoading">{{ refreshLoading?'刷新中...':'刷新连接' }}</t-button> |
|
|
|
|
|
|
|
|
<t-button variant="outline" size="small" @click="refreshConnections" :loading="refreshLoading">刷新连接</t-button> |
|
|
<t-button variant="outline" size="small" @click="load(currentPage)">刷新列表</t-button> |
|
|
<t-button variant="outline" size="small" @click="load(currentPage)">刷新列表</t-button> |
|
|
<div style="flex:1;" /> |
|
|
<div style="flex:1;" /> |
|
|
<span class="total-hint">共 {{ total }} 条配置</span> |
|
|
<span class="total-hint">共 {{ total }} 条配置</span> |
|
|
@ -16,19 +16,13 @@ |
|
|
<template #is_active="{ row }"><t-switch v-model="row._active" size="small" :disabled="!!row._toggling" @change="toggleActive(row)" /></template> |
|
|
<template #is_active="{ row }"><t-switch v-model="row._active" size="small" :disabled="!!row._toggling" @change="toggleActive(row)" /></template> |
|
|
<template #op="{ row }"> |
|
|
<template #op="{ row }"> |
|
|
<t-space :size="4"> |
|
|
<t-space :size="4"> |
|
|
<t-button size="small" variant="text" @click="testConnection(row)" :disabled="testLoading[row.id]">{{ testLoading[row.id]?'测试中...':'测试' }}</t-button> |
|
|
|
|
|
|
|
|
<t-button size="small" variant="text" @click="testConnection(row)" :loading="testLoading[row.id]">测试</t-button> |
|
|
<t-button size="small" variant="text" @click="openEditModal(row)">编辑</t-button> |
|
|
<t-button size="small" variant="text" @click="openEditModal(row)">编辑</t-button> |
|
|
<t-button size="small" variant="text" theme="danger" @click="remove(row.id,row.name)">删除</t-button> |
|
|
<t-button size="small" variant="text" theme="danger" @click="remove(row.id,row.name)">删除</t-button> |
|
|
</t-space> |
|
|
</t-space> |
|
|
</template> |
|
|
</template> |
|
|
</BaseTable> |
|
|
</BaseTable> |
|
|
|
|
|
|
|
|
<!-- 测试结果 --> |
|
|
|
|
|
<div v-if="testResult.visible" class="test-toast" :style="{background:testResult.success?'var(--color-success-text)':'var(--color-error-text)'}"> |
|
|
|
|
|
<strong>{{ testResult.success?'连接成功':'连接失败' }}</strong> |
|
|
|
|
|
<span v-if="testResult.message" class="test-msg">{{ testResult.message }}</span> |
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
<!-- 编辑弹窗 --> |
|
|
<!-- 编辑弹窗 --> |
|
|
<t-dialog v-model:visible="showModal" :header="modalMode==='add'?'新建 MCP 服务配置':'编辑 MCP 服务配置'" width="600px" :footer="false"> |
|
|
<t-dialog v-model:visible="showModal" :header="modalMode==='add'?'新建 MCP 服务配置':'编辑 MCP 服务配置'" width="600px" :footer="false"> |
|
|
<t-form label-align="top"> |
|
|
<t-form label-align="top"> |
|
|
@ -60,7 +54,7 @@ |
|
|
</template> |
|
|
</template> |
|
|
|
|
|
|
|
|
<script setup lang="ts"> |
|
|
<script setup lang="ts"> |
|
|
import { ref, onMounted, onUnmounted } from 'vue' |
|
|
|
|
|
|
|
|
import { ref, onMounted } from 'vue' |
|
|
import * as api from '@/api/mcp-server' |
|
|
import * as api from '@/api/mcp-server' |
|
|
import { toast } from '@/utils/toast' |
|
|
import { toast } from '@/utils/toast' |
|
|
import { formatDate } from '@/utils/format' |
|
|
import { formatDate } from '@/utils/format' |
|
|
@ -71,14 +65,13 @@ const { confirm } = useConfirm() |
|
|
const servers = ref<any[]>([]); const currentPage=ref(1); const pageSize=ref(10); const total=ref(0); const loading=ref(false) |
|
|
const servers = ref<any[]>([]); const currentPage=ref(1); const pageSize=ref(10); const total=ref(0); const loading=ref(false) |
|
|
const showModal=ref(false); const modalMode=ref('add'); const editId=ref<any>(null); const saving=ref(false) |
|
|
const showModal=ref(false); const modalMode=ref('add'); const editId=ref<any>(null); const saving=ref(false) |
|
|
const form=ref({name:'',transportType:'sse',url:'',command:'',argsText:'',envText:'',headersText:'',description:''}) |
|
|
const form=ref({name:'',transportType:'sse',url:'',command:'',argsText:'',envText:'',headersText:'',description:''}) |
|
|
const testLoading=ref<Record<string,boolean>>({}); const testResult=ref({visible:false,success:false,message:'' as any}); let testTimer:any=null |
|
|
|
|
|
|
|
|
const testLoading=ref<Record<string,boolean>>({}) |
|
|
const refreshLoading=ref(false) |
|
|
const refreshLoading=ref(false) |
|
|
|
|
|
|
|
|
const columns=[{colKey:'name',title:'名称',width:130,sorter:true},{colKey:'transport_type',title:'传输类型',width:90,sorter:true},{colKey:'displayUrl',title:'URL/命令',width:200,ellipsis:true},{colKey:'is_active',title:'状态',width:70,sorter:true},{colKey:'description',title:'描述',width:140,ellipsis:true,sorter:true},{colKey:'create_time',title:'创建时间',width:160,sorter:true,cell:(_:any,{row}:any)=>formatDate(row.create_time)},{colKey:'op',title:'操作',width:170,}] |
|
|
const columns=[{colKey:'name',title:'名称',width:130,sorter:true},{colKey:'transport_type',title:'传输类型',width:90,sorter:true},{colKey:'displayUrl',title:'URL/命令',width:200,ellipsis:true},{colKey:'is_active',title:'状态',width:70,sorter:true},{colKey:'description',title:'描述',width:140,ellipsis:true,sorter:true},{colKey:'create_time',title:'创建时间',width:160,sorter:true,cell:(_:any,{row}:any)=>formatDate(row.create_time)},{colKey:'op',title:'操作',width:170,}] |
|
|
const sortInfo = ref<{ sortBy: string; descending: boolean } | null>(null) |
|
|
const sortInfo = ref<{ sortBy: string; descending: boolean } | null>(null) |
|
|
|
|
|
|
|
|
onMounted(()=>load()) |
|
|
onMounted(()=>load()) |
|
|
onUnmounted(()=>{if(testTimer)clearTimeout(testTimer)}) |
|
|
|
|
|
|
|
|
|
|
|
async function load(p=1){currentPage.value=p;loading.value=true |
|
|
async function load(p=1){currentPage.value=p;loading.value=true |
|
|
try{const r=await api.listMcpServers(p,pageSize.value,sortInfo.value?.sortBy||undefined,sortInfo.value?(sortInfo.value.descending?'desc':'asc'):undefined);if(r.success){servers.value=(r.data||[]).map((s:any)=>({...s,_active:s.is_active}));total.value=r.total||0}else toast(r.message||'查询失败','error')}catch(e:any){toast('加载失败:'+e.message,'error')}finally{loading.value=false}} |
|
|
try{const r=await api.listMcpServers(p,pageSize.value,sortInfo.value?.sortBy||undefined,sortInfo.value?(sortInfo.value.descending?'desc':'asc'):undefined);if(r.success){servers.value=(r.data||[]).map((s:any)=>({...s,_active:s.is_active}));total.value=r.total||0}else toast(r.message||'查询失败','error')}catch(e:any){toast('加载失败:'+e.message,'error')}finally{loading.value=false}} |
|
|
@ -117,7 +110,7 @@ async function toggleActive(s:any){ |
|
|
async function testConnection(s:any){testLoading.value={...testLoading.value,[s.id]:true} |
|
|
async function testConnection(s:any){testLoading.value={...testLoading.value,[s.id]:true} |
|
|
try{const r=await api.testMcpServer(s.id);showResult(r)}catch(e:any){showResult({success:false,message:e.message})}finally{testLoading.value={...testLoading.value,[s.id]:false}}} |
|
|
try{const r=await api.testMcpServer(s.id);showResult(r)}catch(e:any){showResult({success:false,message:e.message})}finally{testLoading.value={...testLoading.value,[s.id]:false}}} |
|
|
|
|
|
|
|
|
function showResult(r:any){if(testTimer)clearTimeout(testTimer);testResult.value={visible:true,success:r.success,message:r.message||''};testTimer=setTimeout(()=>{testResult.value.visible=false},5000)} |
|
|
|
|
|
|
|
|
function showResult(r:any){if(r.success){toast('连接成功'+(r.message?':'+r.message:''),'success')}else{toast('连接失败:'+(r.message||'未知错误'),'error')}} |
|
|
|
|
|
|
|
|
async function refreshConnections(){refreshLoading.value=true |
|
|
async function refreshConnections(){refreshLoading.value=true |
|
|
try{const r=await api.refreshMcpServers();if(r.success){toast('连接已刷新','success');load(currentPage.value)}else toast(r.message||'刷新失败','error')}catch(e:any){toast('刷新失败:'+e.message,'error')}finally{refreshLoading.value=false}} |
|
|
try{const r=await api.refreshMcpServers();if(r.success){toast('连接已刷新','success');load(currentPage.value)}else toast(r.message||'刷新失败','error')}catch(e:any){toast('刷新失败:'+e.message,'error')}finally{refreshLoading.value=false}} |
|
|
@ -125,4 +118,4 @@ async function refreshConnections(){refreshLoading.value=true |
|
|
function getDisplayUrl(s:any){if(s.transport_type==='sse')return s.server_url||'-';let d=s.command||'';if(s.args)d+=' '+(Array.isArray(s.args)?s.args.join(' '):s.args);return d||'-'} |
|
|
function getDisplayUrl(s:any){if(s.transport_type==='sse')return s.server_url||'-';let d=s.command||'';if(s.args)d+=' '+(Array.isArray(s.args)?s.args.join(' '):s.args);return d||'-'} |
|
|
</script> |
|
|
</script> |
|
|
<style scoped> |
|
|
<style scoped> |
|
|
.ellipsis{max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;}.test-toast{position:fixed;bottom:20px;right:20px;padding:12px 20px;border-radius:8px;z-index:9999;color:var(--td-text-color-anti);max-width:400px;box-shadow:0 4px 12px rgba(0,0,0,.15);}.test-msg{font-size:12px;margin-top:4px;opacity:.9;display:block;}</style> |
|
|
|
|
|
|
|
|
.ellipsis{max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;}</style> |