| | |
| | | <ElCard class="art-table-card"> |
| | | <div class="mb-5 flex flex-wrap items-center justify-between gap-4"> |
| | | <div> |
| | | <h3 class="text-lg font-semibold text-[var(--art-gray-900)]">MCP 挂载</h3> |
| | | <p class="mt-1 text-sm text-[var(--art-gray-500)]" |
| | | >按传输类型管理 MCP 挂载、连通性和工具预览。</p |
| | | > |
| | | <h3 class="text-lg font-semibold text-[var(--art-gray-900)]">{{ t('pages.system.aiMcpMount.title') }}</h3> |
| | | <p class="mt-1 text-sm text-[var(--art-gray-500)]">{{ t('pages.system.aiMcpMount.subtitle') }}</p> |
| | | </div> |
| | | |
| | | <ElSpace wrap> |
| | | <ElButton v-auth="'save'" @click="openCreateDialog" v-ripple>新建挂载</ElButton> |
| | | <ElButton :loading="loading" @click="refreshData" v-ripple>刷新</ElButton> |
| | | <ElButton v-auth="'save'" @click="openCreateDialog" v-ripple>{{ t('pages.system.aiMcpMount.buttons.add') }}</ElButton> |
| | | <ElButton :loading="loading" @click="refreshData" v-ripple>{{ t('common.actions.refresh') }}</ElButton> |
| | | </ElSpace> |
| | | </div> |
| | | |
| | | <div v-loading="loading" class="space-y-6"> |
| | | <ElEmpty v-if="!groupedRecords.length" description="暂无 MCP 挂载数据" :image-size="110" /> |
| | | <ElEmpty v-if="!groupedRecords.length" :description="t('pages.system.aiMcpMount.empty')" :image-size="110" /> |
| | | |
| | | <section v-for="group in groupedRecords" :key="group.key" class="space-y-4"> |
| | | <div> |
| | |
| | | <div |
| | | class="rounded-2xl bg-[var(--art-main-bg-color)]/70 p-3 ring-1 ring-inset ring-[var(--art-border-color)]" |
| | | > |
| | | <p class="text-xs text-[var(--art-gray-500)]">目标地址</p> |
| | | <p class="text-xs text-[var(--art-gray-500)]">{{ t('pages.system.aiMcpMount.fields.target') }}</p> |
| | | <p class="mt-2 break-all text-[var(--art-gray-900)]">{{ |
| | | item.targetLabel || '--' |
| | | }}</p> |
| | |
| | | <div |
| | | class="rounded-2xl bg-[var(--art-main-bg-color)]/70 p-3 ring-1 ring-inset ring-[var(--art-border-color)]" |
| | | > |
| | | <p class="text-xs text-[var(--art-gray-500)]">最近测试</p> |
| | | <p class="text-xs text-[var(--art-gray-500)]">{{ t('pages.system.aiMcpMount.fields.lastTestTime') }}</p> |
| | | <p class="mt-2 text-[var(--art-gray-900)]">{{ |
| | | item['lastTestTime$'] || '未测试' |
| | | item['lastTestTime$'] || t('pages.system.aiMcpMount.health.notTested') |
| | | }}</p> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="mt-4 grid gap-3 text-sm sm:grid-cols-3"> |
| | | <div class="rounded-2xl bg-slate-50 px-3 py-2"> |
| | | <p class="text-xs text-[var(--art-gray-500)]">超时</p> |
| | | <p class="text-xs text-[var(--art-gray-500)]">{{ t('pages.system.aiMcpMount.fields.timeoutMs') }}</p> |
| | | <p class="mt-1 font-medium text-[var(--art-gray-900)]" |
| | | >{{ item.requestTimeoutMs ?? '--' }} ms</p |
| | | > |
| | | </div> |
| | | <div class="rounded-2xl bg-slate-50 px-3 py-2"> |
| | | <p class="text-xs text-[var(--art-gray-500)]">排序</p> |
| | | <p class="text-xs text-[var(--art-gray-500)]">{{ t('table.sort') }}</p> |
| | | <p class="mt-1 font-medium text-[var(--art-gray-900)]">{{ item.sort ?? '--' }}</p> |
| | | </div> |
| | | <div class="rounded-2xl bg-slate-50 px-3 py-2"> |
| | | <p class="text-xs text-[var(--art-gray-500)]">初始化耗时</p> |
| | | <p class="text-xs text-[var(--art-gray-500)]">{{ t('pages.system.aiMcpMount.fields.lastInitElapsedMs') }}</p> |
| | | <p class="mt-1 font-medium text-[var(--art-gray-900)]">{{ |
| | | item.lastInitElapsedMs ?? '--' |
| | | }}</p> |
| | |
| | | </div> |
| | | |
| | | <div class="mt-4 rounded-2xl bg-amber-50/80 px-4 py-3"> |
| | | <p class="text-xs text-[var(--art-gray-500)]">备注</p> |
| | | <p class="text-xs text-[var(--art-gray-500)]">{{ t('table.remark') }}</p> |
| | | <p class="mt-2 line-clamp-3 text-sm leading-6 text-[var(--art-gray-900)]">{{ |
| | | item.memo || '--' |
| | | }}</p> |
| | |
| | | }}</div> |
| | | |
| | | <ElSpace wrap> |
| | | <ElButton text @click="openDetailDialog(item)">详情</ElButton> |
| | | <ElButton v-auth="'update'" text @click="openEditDialog(item)">编辑</ElButton> |
| | | <ElButton text @click="openDetailDialog(item)">{{ t('common.actions.detail') }}</ElButton> |
| | | <ElButton v-auth="'update'" text @click="openEditDialog(item)">{{ t('common.actions.edit') }}</ElButton> |
| | | <ElButton |
| | | v-auth="'update'" |
| | | text |
| | | :loading="connectivityTestingId === item.id" |
| | | @click="handleConnectivityTest(item)" |
| | | > |
| | | 连通性测试 |
| | | {{ t('pages.system.aiMcpMount.actions.connectivityTest') }} |
| | | </ElButton> |
| | | <ElButton v-auth="'list'" text @click="openToolsDrawer(item)">工具预览</ElButton> |
| | | <ElButton v-auth="'remove'" text type="danger" @click="handleDelete(item)" |
| | | >删除</ElButton |
| | | > |
| | | <ElButton v-auth="'list'" text @click="openToolsDrawer(item)">{{ t('pages.system.aiMcpMount.actions.toolsPreview') }}</ElButton> |
| | | <ElButton v-auth="'remove'" text type="danger" @click="handleDelete(item)">{{ t('common.actions.delete') }}</ElButton> |
| | | </ElSpace> |
| | | </div> |
| | | </article> |
| | |
| | | |
| | | <script setup> |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { useI18n } from 'vue-i18n' |
| | | import { useTable } from '@/hooks/core/useTable' |
| | | import { guardRequestWithMessage } from '@/utils/sys/requestGuard' |
| | | import { |
| | |
| | | |
| | | defineOptions({ name: 'AiMcpMount' }) |
| | | |
| | | const { t } = useI18n() |
| | | const searchForm = ref(createAiMcpMountSearchState()) |
| | | const dialogVisible = ref(false) |
| | | const dialogMode = ref('create') |
| | |
| | | |
| | | const searchItems = computed(() => [ |
| | | { |
| | | label: '关键字', |
| | | label: t('pages.system.aiMcpMount.search.condition'), |
| | | key: 'condition', |
| | | type: 'input', |
| | | props: { |
| | | clearable: true, |
| | | placeholder: '请输入名称' |
| | | placeholder: t('pages.system.aiMcpMount.search.conditionPlaceholder') |
| | | } |
| | | }, |
| | | { |
| | | label: '传输类型', |
| | | label: t('pages.system.aiMcpMount.search.transportType'), |
| | | key: 'transportType', |
| | | type: 'select', |
| | | props: { |
| | |
| | | } |
| | | }, |
| | | { |
| | | label: '状态', |
| | | label: t('pages.system.aiMcpMount.search.status'), |
| | | key: 'status', |
| | | type: 'select', |
| | | props: { |
| | | clearable: true, |
| | | options: [ |
| | | { label: '启用', value: 1 }, |
| | | { label: '停用', value: 0 } |
| | | { label: t('common.status.enabled'), value: 1 }, |
| | | { label: t('common.status.disabled'), value: 0 } |
| | | ] |
| | | } |
| | | } |
| | |
| | | |
| | | const groupedRecords = computed(() => { |
| | | const groups = [ |
| | | { key: 'BUILTIN', title: '内置 MCP', description: '系统内置的标准 MCP 挂载。' }, |
| | | { key: 'SSE_HTTP', title: 'SSE / HTTP', description: '通过服务地址和 SSE 端点接入的 MCP。' }, |
| | | { key: 'STDIO', title: 'STDIO', description: '通过本地命令启动的 MCP。' } |
| | | { key: 'BUILTIN', title: t('pages.system.aiMcpMount.groups.builtin.title'), description: t('pages.system.aiMcpMount.groups.builtin.description') }, |
| | | { key: 'SSE_HTTP', title: t('pages.system.aiMcpMount.groups.sse.title'), description: t('pages.system.aiMcpMount.groups.sse.description') }, |
| | | { key: 'STDIO', title: t('pages.system.aiMcpMount.groups.stdio.title'), description: t('pages.system.aiMcpMount.groups.stdio.description') } |
| | | ] |
| | | return groups |
| | | .map((group) => ({ |
| | |
| | | try { |
| | | if (dialogMode.value === 'edit') { |
| | | await fetchUpdateAiMcpMount(payload) |
| | | ElMessage.success('修改成功') |
| | | ElMessage.success(t('crud.messages.updateSuccess')) |
| | | dialogVisible.value = false |
| | | await refreshUpdate() |
| | | return |
| | | } |
| | | await fetchSaveAiMcpMount(payload) |
| | | ElMessage.success('新增成功') |
| | | ElMessage.success(t('crud.messages.createSuccess')) |
| | | dialogVisible.value = false |
| | | await refreshCreate() |
| | | } catch { |
| | |
| | | |
| | | async function handleDelete(record) { |
| | | try { |
| | | await ElMessageBox.confirm(`确定要删除挂载「${record.name || record.id}」吗?`, '删除确认', { |
| | | confirmButtonText: '确定', |
| | | cancelButtonText: '取消', |
| | | await ElMessageBox.confirm( |
| | | t('crud.confirm.deleteMessage', { |
| | | entity: t('pages.system.aiMcpMount.entity'), |
| | | label: record.name || record.id |
| | | }), |
| | | t('crud.confirm.deleteTitle'), |
| | | { |
| | | confirmButtonText: t('common.confirm'), |
| | | cancelButtonText: t('common.cancel'), |
| | | type: 'warning' |
| | | }) |
| | | await fetchDeleteAiMcpMount(record.id) |
| | | ElMessage.success('删除成功') |
| | | ElMessage.success(t('crud.messages.deleteSuccess')) |
| | | await refreshRemove() |
| | | } catch (error) { |
| | | if (error !== 'cancel') { |
| | | ElMessage.error(error?.message || '删除失败') |
| | | ElMessage.error(error?.message || t('crud.messages.deleteFailed')) |
| | | } |
| | | } |
| | | } |
| | |
| | | connectivityTestingId.value = record.id |
| | | try { |
| | | const result = await guardRequestWithMessage(fetchTestAiMcpConnectivity(record.id), null, { |
| | | timeoutMessage: '连通性测试超时,已停止等待' |
| | | timeoutMessage: t('pages.system.aiMcpMount.messages.connectivityTimeout') |
| | | }) |
| | | ElMessage.success(result?.message || '连通性测试成功') |
| | | ElMessage.success(result?.message || t('pages.system.aiMcpMount.messages.connectivitySuccess')) |
| | | await refreshUpdate() |
| | | } catch (error) { |
| | | ElMessage.error(error?.message || '连通性测试失败') |
| | | ElMessage.error(error?.message || t('pages.system.aiMcpMount.messages.connectivityFailed')) |
| | | } finally { |
| | | connectivityTestingId.value = null |
| | | } |