2026-03-03 11:49:50 +08:00
|
|
|
<template>
|
2026-03-19 18:32:14 +08:00
|
|
|
<div class="extensions-view extension-page-root">
|
2026-04-06 21:04:19 +08:00
|
|
|
<ViewSwitchHeader
|
|
|
|
|
v-model:active-key="activeTab"
|
|
|
|
|
title="扩展管理"
|
|
|
|
|
:items="extensionTabs"
|
|
|
|
|
aria-label="扩展管理视图切换"
|
|
|
|
|
>
|
|
|
|
|
<template #actions>
|
|
|
|
|
<div class="extension-header-actions">
|
|
|
|
|
<!-- Skills Tab 的按钮 -->
|
|
|
|
|
<template v-if="activeTab === 'skills'">
|
|
|
|
|
<a-button
|
|
|
|
|
@click="handleOpenRemoteInstall"
|
|
|
|
|
:disabled="skillsLoading || skillsImporting"
|
|
|
|
|
class="lucide-icon-btn"
|
|
|
|
|
>
|
|
|
|
|
<Computer :size="14" />
|
|
|
|
|
<span>远程安装</span>
|
2026-03-03 11:49:50 +08:00
|
|
|
</a-button>
|
2026-04-06 21:04:19 +08:00
|
|
|
<a-upload
|
|
|
|
|
accept=".zip,.md"
|
|
|
|
|
:show-upload-list="false"
|
|
|
|
|
:custom-request="handleImportUpload"
|
|
|
|
|
:before-upload="beforeSkillUpload"
|
|
|
|
|
:disabled="skillsLoading || skillsImporting"
|
|
|
|
|
>
|
|
|
|
|
<a-button type="primary" :loading="skillsImporting" class="lucide-icon-btn">
|
|
|
|
|
<Upload :size="14" />
|
|
|
|
|
<span>上传 Skill</span>
|
|
|
|
|
</a-button>
|
|
|
|
|
</a-upload>
|
|
|
|
|
</template>
|
|
|
|
|
<!-- MCP Tab 的按钮 -->
|
|
|
|
|
<template v-else-if="activeTab === 'mcp'">
|
|
|
|
|
<a-button type="primary" @click="handleMcpAdd" class="lucide-icon-btn">
|
|
|
|
|
<Plus :size="14" />
|
|
|
|
|
<span>添加 MCP</span>
|
|
|
|
|
</a-button>
|
|
|
|
|
</template>
|
|
|
|
|
<!-- Subagents Tab 的按钮 -->
|
|
|
|
|
<template v-else-if="activeTab === 'subagents'">
|
|
|
|
|
<a-button type="primary" @click="handleSubagentAdd" class="lucide-icon-btn">
|
|
|
|
|
<Plus :size="14" />
|
|
|
|
|
<span>添加</span>
|
|
|
|
|
</a-button>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</ViewSwitchHeader>
|
2026-03-03 11:49:50 +08:00
|
|
|
|
|
|
|
|
<div class="extensions-content">
|
2026-03-05 02:29:01 +08:00
|
|
|
<div v-show="activeTab === 'tools'" class="tab-panel">
|
2026-04-06 21:04:19 +08:00
|
|
|
<ToolsManagerComponent />
|
2026-03-05 02:29:01 +08:00
|
|
|
</div>
|
2026-03-03 11:49:50 +08:00
|
|
|
<div v-show="activeTab === 'skills'" class="tab-panel">
|
2026-04-06 21:04:19 +08:00
|
|
|
<SkillsManagerComponent ref="skillsRef" @import="handleSkillsImport" />
|
2026-03-03 11:49:50 +08:00
|
|
|
</div>
|
|
|
|
|
<div v-show="activeTab === 'mcp'" class="tab-panel">
|
2026-04-06 21:04:19 +08:00
|
|
|
<McpServersComponent ref="mcpRef" @add="handleMcpAdd" />
|
2026-03-03 11:49:50 +08:00
|
|
|
</div>
|
2026-03-19 02:11:52 +08:00
|
|
|
<div v-show="activeTab === 'subagents'" class="tab-panel">
|
2026-04-06 21:04:19 +08:00
|
|
|
<SubAgentsComponent ref="subagentsRef" @add="handleSubagentAdd" />
|
2026-03-19 02:11:52 +08:00
|
|
|
</div>
|
2026-03-03 11:49:50 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2026-03-06 11:46:32 +08:00
|
|
|
import { ref, watch } from 'vue'
|
|
|
|
|
import { useRoute } from 'vue-router'
|
2026-04-02 23:19:33 +08:00
|
|
|
import { message } from 'ant-design-vue'
|
2026-04-06 21:04:19 +08:00
|
|
|
import { Upload, Plus, Computer } from 'lucide-vue-next'
|
2026-03-03 11:49:50 +08:00
|
|
|
import SkillsManagerComponent from '@/components/SkillsManagerComponent.vue'
|
2026-03-03 23:44:57 +08:00
|
|
|
import ToolsManagerComponent from '@/components/ToolsManagerComponent.vue'
|
2026-03-03 11:49:50 +08:00
|
|
|
import McpServersComponent from '@/components/McpServersComponent.vue'
|
2026-03-19 02:11:52 +08:00
|
|
|
import SubAgentsComponent from '@/components/SubAgentsComponent.vue'
|
2026-04-06 21:04:19 +08:00
|
|
|
import ViewSwitchHeader from '@/components/ViewSwitchHeader.vue'
|
2026-03-03 11:49:50 +08:00
|
|
|
|
2026-03-06 11:46:32 +08:00
|
|
|
const route = useRoute()
|
2026-03-05 02:29:01 +08:00
|
|
|
const activeTab = ref('tools')
|
2026-03-03 11:49:50 +08:00
|
|
|
const skillsRef = ref(null)
|
2026-03-06 11:46:32 +08:00
|
|
|
|
2026-04-06 21:04:19 +08:00
|
|
|
const extensionTabs = [
|
|
|
|
|
{ key: 'tools', label: '工具' },
|
|
|
|
|
{ key: 'mcp', label: 'MCP' },
|
|
|
|
|
{ key: 'subagents', label: 'Subagents' },
|
|
|
|
|
{ key: 'skills', label: 'Skills' }
|
|
|
|
|
]
|
|
|
|
|
|
2026-03-06 11:46:32 +08:00
|
|
|
// 监听路由 query 参数变化
|
|
|
|
|
watch(
|
|
|
|
|
() => route.query,
|
|
|
|
|
(query) => {
|
2026-03-19 02:11:52 +08:00
|
|
|
if (query.tab && ['tools', 'skills', 'mcp', 'subagents'].includes(query.tab)) {
|
2026-03-06 11:46:32 +08:00
|
|
|
activeTab.value = query.tab
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{ immediate: true }
|
|
|
|
|
)
|
2026-03-03 11:49:50 +08:00
|
|
|
const mcpRef = ref(null)
|
2026-03-19 02:11:52 +08:00
|
|
|
const subagentsRef = ref(null)
|
2026-03-03 11:49:50 +08:00
|
|
|
|
|
|
|
|
// Skills 相关状态(从子组件透传)
|
|
|
|
|
const skillsLoading = ref(false)
|
|
|
|
|
const skillsImporting = ref(false)
|
|
|
|
|
|
|
|
|
|
// 暴露给子组件的状态更新
|
|
|
|
|
const updateSkillsState = (loading, importing) => {
|
|
|
|
|
skillsLoading.value = loading
|
|
|
|
|
skillsImporting.value = importing
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Skills 事件处理
|
|
|
|
|
const handleSkillsImport = () => {
|
|
|
|
|
// 导入完成后自动刷新
|
|
|
|
|
handleSkillsRefresh()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleSkillsRefresh = () => {
|
|
|
|
|
if (skillsRef.value?.fetchSkills) {
|
|
|
|
|
updateSkillsState(true, skillsImporting.value)
|
|
|
|
|
skillsRef.value.fetchSkills().finally(() => {
|
|
|
|
|
updateSkillsState(false, skillsImporting.value)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 14:44:33 +08:00
|
|
|
const handleOpenRemoteInstall = () => {
|
|
|
|
|
if (skillsRef.value?.openRemoteInstallModal) {
|
|
|
|
|
skillsRef.value.openRemoteInstallModal()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-03 11:49:50 +08:00
|
|
|
// MCP 事件处理
|
|
|
|
|
const handleMcpAdd = () => {
|
|
|
|
|
if (mcpRef.value?.showAddModal) {
|
|
|
|
|
mcpRef.value.showAddModal()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 02:11:52 +08:00
|
|
|
// Subagents 事件处理
|
|
|
|
|
const handleSubagentAdd = () => {
|
|
|
|
|
if (subagentsRef.value?.showAddModal) {
|
|
|
|
|
subagentsRef.value.showAddModal()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 23:19:33 +08:00
|
|
|
// 上传前校验文件名:仅允许 .zip 或 SKILL.md
|
|
|
|
|
const beforeSkillUpload = (file) => {
|
|
|
|
|
const lower = file.name.toLowerCase()
|
|
|
|
|
if (!lower.endsWith('.zip') && lower !== 'skill.md') {
|
|
|
|
|
message.error('仅支持上传 .zip 文件或 SKILL.md 文件')
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-03 11:49:50 +08:00
|
|
|
// 处理导入上传
|
|
|
|
|
const handleImportUpload = async ({ file, onSuccess, onError }) => {
|
|
|
|
|
if (skillsRef.value?.handleImportUpload) {
|
|
|
|
|
updateSkillsState(skillsLoading.value, true)
|
|
|
|
|
try {
|
|
|
|
|
await skillsRef.value.handleImportUpload({ file, onSuccess, onError })
|
|
|
|
|
handleSkillsImport()
|
|
|
|
|
} catch (e) {
|
|
|
|
|
onError?.(e)
|
|
|
|
|
} finally {
|
|
|
|
|
updateSkillsState(skillsLoading.value, false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped lang="less">
|
2026-03-19 18:32:14 +08:00
|
|
|
@import '@/assets/css/extensions.less';
|
2026-03-03 11:49:50 +08:00
|
|
|
|
2026-03-19 18:32:14 +08:00
|
|
|
.extensions-view {
|
2026-04-06 21:04:19 +08:00
|
|
|
.extension-header-actions {
|
2026-03-03 11:49:50 +08:00
|
|
|
display: flex;
|
2026-04-06 21:04:19 +08:00
|
|
|
gap: 8px;
|
2026-03-03 11:49:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.extensions-content {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
|
|
.tab-panel {
|
|
|
|
|
height: 100%;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|