diff --git a/web/src/assets/css/extensions.less b/web/src/assets/css/extensions.less new file mode 100644 index 00000000..a0aa3351 --- /dev/null +++ b/web/src/assets/css/extensions.less @@ -0,0 +1,218 @@ +@border-color: var(--gray-150); +@bg-secondary: var(--gray-10); +@radius: 8px; + +// Root container for extension pages to ensure 100% height +.extension-page-root { + height: 100%; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; + background-color: var(--gray-0); + position: relative; +} + +.layout-wrapper { + display: flex; + flex: 1; + min-height: 0; + height: 100%; +} + +/* Sidebar List */ +.sidebar-list { + width: 280px; + border-right: 1px solid @border-color; + background-color: @bg-secondary; + display: flex; + flex-direction: column; + flex-shrink: 0; + + .search-box { + padding: 12px 12px 0; + .search-input :deep(.ant-input) { + height: 28px; + } + } + + .list-container { + flex: 1; + overflow-y: auto; + padding: 12px; + display: flex; + flex-direction: column; + gap: 4px; + } +} + +.list-item { + padding: 10px 12px; + border-radius: @radius; + cursor: pointer; + transition: all 0.2s; + border: 1px solid transparent; + + &:hover { background-color: var(--gray-100); } + + &.active { + background-color: var(--gray-0); + border-color: @border-color; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + } + + .item-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 4px; + + .item-icon, .server-icon { + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: var(--gray-400); + font-size: 14px; // For emoji icons + } + + .item-name { + flex: 1; + font-size: 14px; + font-weight: 500; + color: var(--gray-700); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + &.active { + .item-icon, .server-icon { color: var(--main-color); } + .item-name { color: var(--gray-900); } + } +} + +/* Main Panel */ +.main-panel { + flex: 1; + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; + background-color: var(--gray-0); + + .unselected-state { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + background-color: @bg-secondary; + .hint-box { + text-align: center; + p { margin-top: 12px; color: var(--gray-400); } + } + } + + .panel-top-bar { + padding: 10px 16px 0 16px; + display: flex; + justify-content: space-between; + align-items: center; + flex-shrink: 0; + + h2 { + margin: 0; + font-size: 18px; + font-weight: 600; + display: flex; + align-items: center; + gap: 10px; + } + } +} + +/* Tabs inside main panel */ +.detail-tabs, .minimal-tabs { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; + + :deep(.ant-tabs-nav) { + margin: 0; + padding: 0 16px; + flex-shrink: 0; + border-bottom: 1px solid @border-color; + &::before { border-bottom: none; } + } + + :deep(.ant-tabs-content) { + flex: 1; + min-height: 0; + height: 100%; + overflow: hidden; + } + + :deep(.ant-tabs-tabpane) { + height: 100%; + min-height: 0; + overflow: hidden; + } +} + +.tab-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; +} + +/* Common helpers */ +.text-muted { color: var(--gray-400); } +.empty-text { padding: 40px 0; text-align: center; color: var(--gray-400); } + +// Spin wrapper for full height +.full-height-spin { + height: 100%; + :deep(.ant-spin-nested-loading) { height: 100%; } + :deep(.ant-spin-container) { + height: 100%; + display: flex; + flex-direction: column; + } +} + +/* Loading States */ +.loading-bar-wrapper { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + z-index: 100; + overflow: hidden; + background: transparent; + + .loading-bar { + height: 100%; + background: var(--main-color); + width: 30%; + position: absolute; + animation: loading-bar-anim 1.5s infinite linear; + } +} + +@keyframes loading-bar-anim { + 0% { left: -30%; } + 100% { left: 100%; } +} + +.content-loading { + opacity: 0.6; + pointer-events: none; + transition: opacity 0.2s; +} diff --git a/web/src/components/McpServerDetailModal.vue b/web/src/components/McpServerDetailModal.vue deleted file mode 100644 index fb9fead6..00000000 --- a/web/src/components/McpServerDetailModal.vue +++ /dev/null @@ -1,660 +0,0 @@ - - - - - - - {{ server.icon || '🔌' }} - - {{ server.name }} - - {{ server.enabled ? '已启用' : '已禁用' }} - - - - - - - 测试连接 - - - - - - - - - - - 传输类型 - - - {{ server.transport }} - - - - - - - - 服务器 URL - {{ server.url || '-' }} - - - 请求头 - {{ JSON.stringify(server.headers, null, 2) }} - - - HTTP 超时 - {{ server.timeout }} 秒 - - - SSE 读取超时 - {{ server.sse_read_timeout }} 秒 - - - - - - - 命令 - {{ server.command || '-' }} - - - 参数 - - - {{ arg }} - - - - - 环境变量 - {{ JSON.stringify(server.env, null, 2) }} - - - - - 描述 - {{ server.description }} - - - 标签 - - {{ tag }} - - - - 创建时间 - {{ formatTime(server.created_at) }} - - - 更新时间 - {{ formatTime(server.updated_at) }} - - - 创建人 - {{ server.created_by }} - - - - - - - - - - - - - 刷新工具 - - - - - - - - - - - - - {{ tool.name }} - - - - - - - - - - - - - - - {{ tool.description }} - - - - - - - {{ paramName }} - 必填 - {{ param.type || 'any' }} - - - {{ param.description }} - - - - - - - - - - - - - - - - 📝 - - - - - - - - - - 📦 - - - - - - - - - - - - diff --git a/web/src/components/McpServersComponent.vue b/web/src/components/McpServersComponent.vue index ff237394..95812a2e 100644 --- a/web/src/components/McpServersComponent.vue +++ b/web/src/components/McpServersComponent.vue @@ -1,128 +1,288 @@ - - - - - MCP 服务器管理 - - 管理 MCP(Model Context Protocol)服务器配置。添加、编辑或删除 MCP 服务器以扩展 AI - 的能力。 - - - - - 添加服务器 - + + + - - - - - 已配置 {{ servers.length }} 个 MCP 服务器: HTTP: {{ httpCount }} · SSE: {{ sseCount }} · - StdIO: {{ stdioCount }} - - - - - - - - + + + + + + + + - - - - 添加服务器 - + + + + + + + - - - - - {{ server.icon || '🔌' }} - - {{ server.name }} - - - {{ server.transport }} - - - - - - - - - - {{ server.description || '暂无描述' }} - - - - - - - - 详情 - - - - - - 测试 - - - - - - 编辑 - - - - - - 删除 - - - + + + {{ server.icon || '🔌' }} + {{ server.name }} + + + + {{ server.transport }} + {{ server.description || '暂无描述' }} - + + + + + + + + 请在左侧选择服务器进行操作 + + + + + + + {{ currentServer.icon || '🔌' }} + {{ currentServer.name }} + + + + + + 测试 + + + + 编辑 + + + + 删除 + + + + + + + + + + 通用 + + + + + 传输类型 + + + {{ currentServer.transport }} + + + + + + + + 服务器 URL + {{ currentServer.url }} + + + 请求头 + {{ JSON.stringify(currentServer.headers, null, 2) }} + + + HTTP 超时 + {{ currentServer.timeout }} 秒 + + + SSE 读取超时 + {{ currentServer.sse_read_timeout }} 秒 + + + + + + + 命令 + {{ currentServer.command }} + + + 参数 + + + {{ arg }} + + + + + 环境变量 + {{ JSON.stringify(currentServer.env, null, 2) }} + + + + + 描述 + {{ currentServer.description }} + + + 标签 + + {{ tag }} + + + + 创建时间 + {{ formatTime(currentServer.created_at) }} + + + 更新时间 + {{ formatTime(currentServer.updated_at) }} + + + 创建人 + {{ currentServer.created_by }} + + + + + + + + 工具 ({{ tools.length }}) + + + + + + + 刷新 + + + + + + + + + + + {{ tool.name }} + + + + + + + + + + + + + + + {{ tool.description }} + + + + + + + {{ paramName }} + 必填 + {{ param.type || 'any' }} + + + {{ param.description }} + + + + + + + + + + + + + + 提示 + + + + + 📝 + + + + + + + + 资源 + + + + + 📦 + + + + + + + @@ -266,12 +426,6 @@ - - @@ -279,14 +433,21 @@ import { ref, reactive, computed, onMounted } from 'vue' import { notification, Modal } from 'ant-design-vue' import { - PlusOutlined, - EditOutlined, - DeleteOutlined, - EyeOutlined, - ApiOutlined -} from '@ant-design/icons-vue' + Search, + Plug, + Zap, + Pencil, + Trash2, + RotateCw, + Info, + Copy, + Settings2, + Wrench, + MessageSquare, + Box +} from 'lucide-vue-next' import { mcpApi } from '@/apis/mcp_api' -import McpServerDetailModal from './McpServerDetailModal.vue' +import { formatFullDateTime } from '@/utils/time' import McpEnvEditor from './McpEnvEditor.vue' // 状态 @@ -295,6 +456,16 @@ const error = ref(null) const servers = ref([]) const toggleLoading = ref(null) const testLoading = ref(null) +const searchQuery = ref('') +const currentServer = ref(null) +const detailTab = ref('general') + +// 工具相关状态 +const tools = ref([]) +const toolsLoading = ref(false) +const toolsError = ref(null) +const toolSearchText = ref('') +const toggleToolLoading = ref(null) // 表单相关 const formModalVisible = ref(false) @@ -317,21 +488,31 @@ const form = reactive({ icon: '' }) -// 详情模态框 -const detailModalVisible = ref(false) -const selectedServer = ref(null) // 计算属性 -const httpCount = computed( - () => servers.value.filter((s) => s.transport === 'streamable_http').length -) -const sseCount = computed(() => servers.value.filter((s) => s.transport === 'sse').length) -const stdioCount = computed(() => servers.value.filter((s) => s.transport === 'stdio').length) -const envEditorKey = computed(() => `${form.name}-${form.transport}`) +const filteredServers = computed(() => { + if (!searchQuery.value) return servers.value + const q = searchQuery.value.toLowerCase() + return servers.value.filter( + (s) => s.name.toLowerCase().includes(q) || (s.description || '').toLowerCase().includes(q) + ) +}) + const isStdioTransport = computed( () => String(form.transport || '').trim().toLowerCase() === 'stdio' ) +// 工具相关计算属性 +const filteredTools = computed(() => { + if (!toolSearchText.value) return tools.value + const search = toolSearchText.value.toLowerCase() + return tools.value.filter( + (t) => + t.name.toLowerCase().includes(search) || + (t.description && t.description.toLowerCase().includes(search)) + ) +}) + // 获取服务器列表 const fetchServers = async () => { try { @@ -340,6 +521,15 @@ const fetchServers = async () => { const result = await mcpApi.getMcpServers() if (result.success) { servers.value = result.data || [] + // 默认选中第一个服务器 + if (!currentServer.value && servers.value.length > 0) { + selectServer(servers.value[0]) + } else if (currentServer.value) { + const latest = servers.value.find((s) => s.name === currentServer.value.name) + if (latest) { + currentServer.value = latest + } + } } else { error.value = result.message || '获取服务器列表失败' } @@ -351,6 +541,83 @@ const fetchServers = async () => { } } +// 获取工具列表 +const fetchTools = async () => { + if (!currentServer.value) return + + try { + toolsLoading.value = true + toolsError.value = null + const result = await mcpApi.getMcpServerTools(currentServer.value.name) + if (result.success) { + tools.value = result.data || [] + } else { + toolsError.value = result.message || '获取工具列表失败' + tools.value = [] + } + } catch (err) { + console.error('获取工具列表失败:', err) + toolsError.value = err.message || '获取工具列表失败' + tools.value = [] + } finally { + toolsLoading.value = false + } +} + +// 切换工具启用状态 +const handleToggleTool = async (tool) => { + if (!currentServer.value) return + + try { + toggleToolLoading.value = tool.name + const result = await mcpApi.toggleMcpServerTool(currentServer.value.name, tool.name) + if (result.success) { + notification.success({ message: result.message }) + const targetTool = tools.value.find((t) => t.name === tool.name) + if (targetTool) { + targetTool.enabled = result.enabled + } + } else { + notification.error({ message: result.message || '操作失败' }) + } + } catch (err) { + console.error('切换工具状态失败:', err) + notification.error({ message: err.message || '操作失败' }) + } finally { + toggleToolLoading.value = null + } +} + +// 复制工具名称 +const copyToolName = async (name) => { + try { + await navigator.clipboard.writeText(name) + notification.success({ message: '已复制到剪贴板' }) + } catch { + notification.error({ message: '复制失败' }) + } +} + +// 格式化时间 +const formatTime = (timeStr) => formatFullDateTime(timeStr) + +// 获取传输类型颜色 +const getTransportColor = (transport) => { + const colors = { + sse: 'orange', + stdio: 'green', + streamable_http: 'blue' + } + return colors[transport] || 'blue' +} + +// 选择服务器 +const selectServer = (server) => { + currentServer.value = server + detailTab.value = 'general' + fetchTools() +} + // 显示添加模态框 const showAddModal = () => { editMode.value = false @@ -407,12 +674,6 @@ const showEditModal = async (server) => { applyServerToForm(server) } -// 显示详情模态框 -const showDetailModal = (server) => { - selectedServer.value = server - detailModalVisible.value = true -} - // 处理表单提交 const handleFormSubmit = async () => { try { @@ -577,11 +838,6 @@ const confirmDeleteServer = (server) => { }) } -// 处理服务器更新(来自详情模态框) -const handleServerUpdate = () => { - fetchServers() -} - // 格式化 JSON const formatJson = () => { try { @@ -621,193 +877,207 @@ const parseJsonToForm = () => { onMounted(() => { fetchServers() }) + +// 暴露方法给父组件 +defineExpose({ + fetchServers, + showAddModal +}) diff --git a/web/src/components/SettingsModal.vue b/web/src/components/SettingsModal.vue index f82c1dda..64405bf5 100644 --- a/web/src/components/SettingsModal.vue +++ b/web/src/components/SettingsModal.vue @@ -49,15 +49,6 @@ 部门管理 - - - MCP 管理 - @@ -86,14 +77,6 @@ > 用户管理 - - MCP 管理 - - - - - @@ -139,13 +118,11 @@ import { SettingOutlined, CodeOutlined, UserOutlined, - ApiOutlined, TeamOutlined } from '@ant-design/icons-vue' import BasicSettingsSection from '@/components/BasicSettingsSection.vue' import ModelProvidersComponent from '@/components/ModelProvidersComponent.vue' import UserManagementComponent from '@/components/UserManagementComponent.vue' -import McpServersComponent from '@/components/McpServersComponent.vue' import DepartmentManagementComponent from '@/components/DepartmentManagementComponent.vue' const props = defineProps({ diff --git a/web/src/components/SkillsManagerComponent.vue b/web/src/components/SkillsManagerComponent.vue index 84e6786b..8d11c46d 100644 --- a/web/src/components/SkillsManagerComponent.vue +++ b/web/src/components/SkillsManagerComponent.vue @@ -1,186 +1,163 @@ - - - - - + + + + + + + + + + + + + + + + + - - - 导入 ZIP - - - - - 刷新 - - - - - - - - - - - - - + + + {{ skill.name }} - - - - + + {{ skill.slug }} + + + - - - - {{ skill.name }} + + + + + + + + + + + 请在左侧选择技能包进行编辑 + + + + + + + {{ currentSkill.name }} + + + + + + + 导出 + + + + 删除 + + + + + + + + + 代码管理 + + + + + 项目结构 + + + + + + + + + - - {{ skill.slug }} - - - + + + + + + {{ selectedPath || '未选择文件' }} + ● + + + + 保存 + + + + + - - + - - - - - - 请在左侧选择技能包进行编辑 - - - - - - - {{ currentSkill.name }} - + + + 依赖管理 + + + + + 依赖声明 + 配置此 Skill 所需的工具、MCP 服务器及其他 Skill 依赖。 + + + + 更新依赖 + - - - - - 导出 - - - - 删除 - - + + + + + + + + + + + + + - - - - - - 代码管理 - - - - - 项目结构 - - - - - - - - - - - - - - - - {{ selectedPath || '未选择文件' }} - ● - - - - 保存 - - - - - - - - - - - - - 依赖管理 - - - - - 依赖声明 - 配置此 Skill 所需的工具、MCP 服务器及其他 Skill 依赖。 - - - - 更新依赖 - - - - - - - - - - - - - - - - - - - - - - - + + + + @@ -214,7 +191,6 @@ import { FilePlus, FolderPlus, File, Search, Box, FileCode } from 'lucide-vue-next' import { skillApi } from '@/apis/skill_api' -import HeaderComponent from '@/components/HeaderComponent.vue' import FileTreeComponent from '@/components/FileTreeComponent.vue' dayjs.extend(relativeTime) @@ -274,18 +250,9 @@ const resetFileState = () => { originalFileContent.value = '' } -const expandAllKeys = (nodes) => { - let keys = [] - nodes.forEach(node => { - if (node.is_dir) { - keys.push(node.key) - if (node.children) { - keys = keys.concat(expandAllKeys(node.children)) - } - } - }) - return keys -} +const expandAllKeys = (nodes) => nodes.flatMap(node => + node.is_dir ? [node.key, ...expandAllKeys(node.children || [])] : [] +) const fetchSkills = async () => { loading.value = true @@ -366,7 +333,7 @@ const selectSkill = async (record) => { resetFileState() // 并行执行:加载树结构和获取 SKILL.md - const [treeResult] = await Promise.all([ + await Promise.all([ reloadTree(), loadSkillFile(record.slug) ]) @@ -507,121 +474,18 @@ const saveDependencies = async () => { } onMounted(fetchSkills) + +// 暴露方法给父组件 +defineExpose({ + fetchSkills, + handleImportUpload +})
{{ JSON.stringify(server.headers, null, 2) }}
{{ JSON.stringify(server.env, null, 2) }}
- 管理 MCP(Model Context Protocol)服务器配置。添加、编辑或删除 MCP 服务器以扩展 AI - 的能力。 -
请在左侧选择服务器进行操作
{{ JSON.stringify(currentServer.headers, null, 2) }}
{{ JSON.stringify(currentServer.env, null, 2) }}
请在左侧选择技能包进行编辑
配置此 Skill 所需的工具、MCP 服务器及其他 Skill 依赖。