Skip to main content
Glama

SOAR MCP Server

by wuzhi-dev
admin.js39.7 kB
// 检查认证状态 function checkAuth() { const jwt = localStorage.getItem('jwt_token'); if (!jwt) { // 如果没有token,跳转到登录页 window.location.href = '/login'; return false; } return jwt; } // 注销函数 function logout() { // 确认注销 if (confirm('确定要注销吗?')) { // 清除本地存储的JWT token localStorage.removeItem('jwt_token'); // 跳转到登录页面 window.location.href = '/login'; } } // 通用API调用函数,自动添加JWT认证头 async function apiCall(url, options = {}) { const jwt = checkAuth(); if (!jwt) return null; const defaultOptions = { headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${jwt}` } }; const mergedOptions = { ...defaultOptions, ...options, headers: { ...defaultOptions.headers, ...(options.headers || {}) } }; try { const response = await fetch(url, mergedOptions); // 如果返回401,清除token并跳转到登录页 if (response.status === 401) { localStorage.removeItem('jwt_token'); window.location.href = '/login'; return null; } return response; } catch (error) { console.error('API调用失败:', error); throw error; } } // 全局状态 let playbooks = []; let currentPage = 1; const itemsPerPage = 20; // DOM 元素 const alertContainer = document.getElementById('alert-container'); const playbooksTable = document.getElementById('playbooks-tbody'); const totalCount = document.getElementById('total-count'); const enabledCount = document.getElementById('enabled-count'); const disabledCount = document.getElementById('disabled-count'); const paginationInfo = document.getElementById('pagination-info'); const prevPageBtn = document.getElementById('prev-page'); const nextPageBtn = document.getElementById('next-page'); const pageNumbers = document.getElementById('page-numbers'); // 显示通知 function showAlert(message, type = 'success') { const alert = document.createElement('div'); alert.className = type === 'error' ? 'error' : 'success'; alert.textContent = message; alertContainer.innerHTML = ''; alertContainer.appendChild(alert); setTimeout(() => { alert.remove(); }, 3000); } // 格式化日期 function formatDate(dateStr) { if (!dateStr) return '-'; try { const date = new Date(dateStr); return date.toLocaleString('zh-CN'); } catch { return '-'; } } // 切换剧本状态 async function togglePlaybook(playbookId, enabled) { try { const response = await apiCall(`/api/admin/playbooks/${playbookId}/toggle`, { method: 'POST', body: JSON.stringify({ enabled: enabled }) }); if (!response) return; const result = await response.json(); if (result.success) { showAlert(`剧本 ${playbookId} 已${enabled ? '启用' : '禁用'}`, 'success'); // 更新本地状态 (注意:比较时统一转换为字符串) const playbook = playbooks.find(p => String(p.id) === String(playbookId)); if (playbook) { playbook.enabled = enabled; updateStats(); } } else { showAlert(result.error || '操作失败', 'error'); // 回滚开关状态 const toggle = document.querySelector(`input[data-id="${playbookId}"]`); if (toggle) { toggle.checked = !enabled; } } } catch (error) { showAlert('网络错误: ' + error.message, 'error'); // 回滚开关状态 const toggle = document.querySelector(`input[data-id="${playbookId}"]`); if (toggle) { toggle.checked = !enabled; } } } // 更新统计 function updateStats() { const total = playbooks.length; const enabled = playbooks.filter(p => p.enabled).length; const disabled = total - enabled; totalCount.textContent = total; enabledCount.textContent = enabled; disabledCount.textContent = disabled; } // 渲染剧本表格 function renderPlaybooks() { if (playbooks.length === 0) { playbooksTable.innerHTML = ` <tr> <td colspan="4" class="loading"> <div>暂无剧本数据</div> </td> </tr> `; updatePagination(); return; } // 计算分页数据 const totalPages = Math.ceil(playbooks.length / itemsPerPage); const startIndex = (currentPage - 1) * itemsPerPage; const endIndex = startIndex + itemsPerPage; const currentPlaybooks = playbooks.slice(startIndex, endIndex); playbooksTable.innerHTML = currentPlaybooks.map(playbook => ` <tr> <td> <span class="playbook-id" data-id="${playbook.id}">${String(playbook.id)}</span> </td> <td> <div class="playbook-display-name">${playbook.displayName || playbook.name}</div> </td> <td>${formatDate(playbook.syncTime)}</td> <td> <label class="switch"> <input type="checkbox" data-id="${playbook.id}" ${playbook.enabled ? 'checked' : ''}> <span class="slider"></span> </label> </td> </tr> `).join(''); // 绑定开关事件 playbooksTable.querySelectorAll('input[type="checkbox"]').forEach(toggle => { toggle.addEventListener('change', (e) => { const playbookId = e.target.dataset.id; // 保持字符串格式避免精度丢失 const enabled = e.target.checked; togglePlaybook(playbookId, enabled); }); }); // 绑定剧本ID点击事件 playbooksTable.querySelectorAll('.playbook-id').forEach(playbookIdElement => { playbookIdElement.addEventListener('click', (e) => { const playbookId = e.target.dataset.id; // 保持字符串格式避免精度丢失 showPlaybookDetail(playbookId); }); }); updateStats(); updatePagination(); } // 加载剧本数据 async function loadPlaybooks() { try { const response = await apiCall('/api/admin/playbooks'); if (!response) return; const result = await response.json(); if (result.success) { playbooks = result.data; renderPlaybooks(); } else { showAlert(result.error || '加载剧本数据失败', 'error'); playbooksTable.innerHTML = ` <tr> <td colspan="4" class="loading"> <div>加载失败,请刷新页面重试</div> </td> </tr> `; } } catch (error) { showAlert('网络错误: ' + error.message, 'error'); playbooksTable.innerHTML = ` <tr> <td colspan="4" class="loading"> <div>网络错误,请检查连接</div> </td> </tr> `; } } // 显示剧本详情 async function showPlaybookDetail(playbookId) { const modal = document.getElementById('playbook-modal'); const modalBody = document.getElementById('modal-body'); modal.style.display = 'block'; modalBody.innerHTML = '<div class="loading">加载中...</div>'; try { const response = await apiCall(`/api/admin/playbooks/${playbookId}`); if (!response) { modal.style.display = 'none'; return; } const result = await response.json(); if (result.success) { const playbook = result.data; let params = []; // 解析参数 if (playbook.playbookParams) { try { params = JSON.parse(playbook.playbookParams); } catch (e) { console.warn('参数解析失败:', e); } } modalBody.innerHTML = ` <div class="detail-section"> <div class="detail-label">剧本ID</div> <div class="detail-value">${String(playbook.id)}</div> </div> <div class="detail-section"> <div class="detail-label">剧本名称</div> <div class="detail-value">${playbook.name}</div> </div> <div class="detail-section"> <div class="detail-label">显示名称</div> <div class="detail-value">${playbook.displayName || playbook.name}</div> </div> <div class="detail-section"> <div class="detail-label">分类</div> <div class="detail-value">${playbook.playbookCategory}</div> </div> <div class="detail-section"> <div class="detail-label">描述</div> <div class="detail-value">${playbook.description || '无描述'}</div> </div> <div class="detail-section"> <div class="detail-label">创建时间</div> <div class="detail-value">${formatDate(playbook.createTime)}</div> </div> <div class="detail-section"> <div class="detail-label">更新时间</div> <div class="detail-value">${formatDate(playbook.updateTime)}</div> </div> <div class="detail-section"> <div class="detail-label">最后同步</div> <div class="detail-value">${formatDate(playbook.syncTime)}</div> </div> <div class="detail-section"> <div class="detail-label">状态</div> <div class="detail-value">${playbook.enabled ? '已启用' : '已禁用'}</div> </div> <div class="detail-section"> <div class="detail-label">参数信息 (${params.length} 个)</div> <div class="detail-value"> ${params.length === 0 ? '无参数' : params.map(param => ` <div class="param-item"> <div class="param-name"> ${param.cefColumn || param.key || '未知参数'} <span class="param-type">${param.valueType || param.type || 'string'}</span> ${param.required ? '<span class="param-required">必填</span>' : ''} </div> ${param.cefDesc ? `<div class="param-description">${param.cefDesc}</div>` : ''} ${param.description ? `<div class="param-description">${param.description}</div>` : ''} ${param.defaultValue ? `<div class="param-description">默认值: ${param.defaultValue}</div>` : ''} </div> `).join('')} </div> </div> `; } else { modalBody.innerHTML = `<div class="error">加载失败: ${result.error || '未知错误'}</div>`; } } catch (error) { modalBody.innerHTML = `<div class="error">网络错误: ${error.message}</div>`; } } // 关闭Modal function closeModal() { document.getElementById('playbook-modal').style.display = 'none'; } // 更新分页信息和控件 function updatePagination() { const totalPages = Math.ceil(playbooks.length / itemsPerPage); const startIndex = (currentPage - 1) * itemsPerPage; const endIndex = Math.min(startIndex + itemsPerPage, playbooks.length); // 更新分页信息 if (playbooks.length === 0) { paginationInfo.textContent = '显示第 0-0 条,共 0 条'; } else { paginationInfo.textContent = `显示第 ${startIndex + 1}-${endIndex} 条,共 ${playbooks.length} 条`; } // 更新按钮状态 prevPageBtn.disabled = currentPage === 1; nextPageBtn.disabled = currentPage === totalPages || totalPages === 0; // 生成页码 renderPageNumbers(totalPages); } // 渲染页码 function renderPageNumbers(totalPages) { pageNumbers.innerHTML = ''; if (totalPages <= 1) { return; } const maxVisiblePages = 5; let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2)); let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1); if (endPage - startPage < maxVisiblePages - 1) { startPage = Math.max(1, endPage - maxVisiblePages + 1); } // 显示第一页和省略号 if (startPage > 1) { pageNumbers.appendChild(createPageButton(1)); if (startPage > 2) { pageNumbers.appendChild(createEllipsis()); } } // 显示页码 for (let page = startPage; page <= endPage; page++) { pageNumbers.appendChild(createPageButton(page)); } // 显示省略号和最后一页 if (endPage < totalPages) { if (endPage < totalPages - 1) { pageNumbers.appendChild(createEllipsis()); } pageNumbers.appendChild(createPageButton(totalPages)); } } // 创建页码按钮 function createPageButton(page) { const button = document.createElement('div'); button.className = `page-number ${page === currentPage ? 'active' : ''}`; button.textContent = page; button.onclick = () => goToPage(page); return button; } // 创建省略号 function createEllipsis() { const ellipsis = document.createElement('div'); ellipsis.className = 'page-number ellipsis'; ellipsis.textContent = '...'; return ellipsis; } // 跳转到指定页 function goToPage(page) { if (page >= 1 && page <= Math.ceil(playbooks.length / itemsPerPage)) { currentPage = page; renderPlaybooks(); } } // 点击Modal背景关闭 function setupModalEvents() { const modal = document.getElementById('playbook-modal'); const closeBtn = document.getElementById('close-modal-btn'); // 关闭按钮事件 closeBtn.addEventListener('click', closeModal); // 点击背景关闭 modal.addEventListener('click', function(event) { if (event.target === modal) { closeModal(); } }); } // 密码显示/隐藏功能 function togglePasswordVisibility(inputId) { const input = document.getElementById(inputId); const eyeIcon = document.getElementById(inputId + '-eye'); if (input.type === 'password') { input.type = 'text'; eyeIcon.textContent = '🙈'; } else { input.type = 'password'; eyeIcon.textContent = '👁️'; } } // 测试连接功能 async function testConnection() { const testBtn = document.getElementById('test-connection-btn'); const connectionStatus = document.getElementById('connection-status'); testBtn.disabled = true; testBtn.textContent = '测试中...'; connectionStatus.textContent = '测试中...'; connectionStatus.className = 'status-warning'; try { const response = await apiCall('/api/admin/config/test', { method: 'POST', body: JSON.stringify({}) // 发送空对象,使用当前数据库配置 }); if (!response) return; const data = await response.json(); if (data.success) { connectionStatus.textContent = '连接成功'; connectionStatus.className = 'status-success'; showAlert('API连接测试成功!', 'success'); } else { connectionStatus.textContent = '连接失败'; connectionStatus.className = 'status-error'; showAlert(`连接测试失败: ${data.message}`, 'error'); } } catch (error) { connectionStatus.textContent = '连接失败'; connectionStatus.className = 'status-error'; showAlert(`连接测试失败: ${error.message}`, 'error'); } testBtn.disabled = false; testBtn.textContent = '测试连接'; } // 保存配置功能 async function saveConfiguration() { const saveBtn = document.getElementById('save-config-btn'); const configValid = document.getElementById('config-valid'); const configUpdated = document.getElementById('config-updated'); saveBtn.disabled = true; saveBtn.textContent = '保存中...'; // 收集配置数据 const apiUrl = document.getElementById('soar-api-url').value.trim(); const apiToken = document.getElementById('soar-api-token').value.trim(); const timeout = parseInt(document.getElementById('soar-timeout').value); const syncInterval = parseInt(document.getElementById('sync-interval').value); // 收集标签 const labelElements = document.querySelectorAll('.label-tag .label-name'); const labels = Array.from(labelElements).map(el => el.textContent.trim()).filter(label => label); // 构建配置数据 const configData = { soar_api_url: apiUrl, soar_timeout: timeout, sync_interval: syncInterval, soar_labels: labels }; // 检查Token是否被修改(如果不是打码格式,说明用户输入了新Token) if (!apiToken.includes('***')) { configData.soar_api_token = apiToken; } // 如果是打码格式,则不包含Token字段,后端会保持原有Token // 验证必填项 if (!apiUrl || !apiToken || !timeout || labels.length === 0) { showAlert('请填写所有必填项并至少添加一个标签', 'error'); saveBtn.disabled = false; saveBtn.textContent = '保存配置'; return; } try { const response = await apiCall('/api/admin/config', { method: 'POST', body: JSON.stringify(configData) }); if (!response) return; const data = await response.json(); if (data.success) { configValid.textContent = '配置有效'; configValid.className = 'status-success'; configUpdated.textContent = new Date().toLocaleString('zh-CN'); showAlert('配置保存成功!', 'success'); } else { configValid.textContent = '配置无效'; configValid.className = 'status-error'; showAlert(`配置保存失败: ${data.message}`, 'error'); } } catch (error) { configValid.textContent = '配置无效'; configValid.className = 'status-error'; showAlert(`配置保存失败: ${error.message}`, 'error'); } saveBtn.disabled = false; saveBtn.textContent = '保存配置'; } // 标签管理功能 function addLabel() { const labelInput = document.getElementById('label-input'); const labelsList = document.getElementById('labels-list'); const labelName = labelInput.value.trim(); if (!labelName) { showAlert('请输入标签名称', 'warning'); return; } // 检查重复 const existingLabels = Array.from(document.querySelectorAll('.label-tag .label-name')) .map(el => el.textContent.trim()); if (existingLabels.includes(labelName)) { showAlert('标签已存在', 'warning'); return; } // 创建标签元素 const labelElement = document.createElement('div'); labelElement.className = 'label-tag'; labelElement.innerHTML = ` <span class="label-name">${labelName}</span> <button type="button" class="label-remove" onclick="this.parentElement.remove()">×</button> `; labelsList.appendChild(labelElement); labelInput.value = ''; } // 初始化 // 加载系统配置 async function loadSystemConfig() { try { const response = await apiCall('/api/admin/config'); if (!response) return; const data = await response.json(); if (data.success && data.data) { // 填充表单字段 document.getElementById('soar-api-url').value = data.data.soar_api_url || ''; document.getElementById('soar-api-token').value = data.data.soar_api_token || ''; document.getElementById('soar-timeout').value = data.data.soar_timeout || 30; document.getElementById('sync-interval').value = data.data.sync_interval || 14400; // 填充标签列表 const labelsList = document.getElementById('labels-list'); labelsList.innerHTML = ''; const labels = data.data.soar_labels && Array.isArray(data.data.soar_labels) ? data.data.soar_labels : ['MCP']; // 默认显示MCP标签 labels.forEach(label => { const labelElement = document.createElement('div'); labelElement.className = 'label-tag'; labelElement.innerHTML = ` <span class="label-name">${label}</span> <button type="button" class="label-remove" onclick="this.parentElement.remove()">×</button> `; labelsList.appendChild(labelElement); }); } } catch (error) { console.error('加载系统配置失败:', error); showAlert('加载系统配置失败', 'error'); // 即使加载失败,也显示默认的MCP标签 const labelsList = document.getElementById('labels-list'); labelsList.innerHTML = ''; const labelElement = document.createElement('div'); labelElement.className = 'label-tag'; labelElement.innerHTML = ` <span class="label-name">MCP</span> <button type="button" class="label-remove" onclick="this.parentElement.remove()">×</button> `; labelsList.appendChild(labelElement); } } // Token管理功能 let tokens = []; // 加载Token列表 async function loadTokens() { try { const response = await apiCall('/api/admin/tokens'); if (!response) return; const result = await response.json(); if (result.success) { tokens = result.data; renderTokens(); } else { showAlert(result.error || '加载Token数据失败', 'error'); document.getElementById('tokens-tbody').innerHTML = ` <tr> <td colspan="7" class="loading"> <div>加载失败,请刷新页面重试</div> </td> </tr> `; } } catch (error) { showAlert('网络错误: ' + error.message, 'error'); document.getElementById('tokens-tbody').innerHTML = ` <tr> <td colspan="7" class="loading"> <div>网络错误,请检查连接</div> </td> </tr> `; } } // 渲染Token表格 function renderTokens() { const tokensTable = document.getElementById('tokens-tbody'); if (tokens.length === 0) { tokensTable.innerHTML = ` <tr> <td colspan="7" class="loading"> <div>暂无Token数据</div> </td> </tr> `; return; } tokensTable.innerHTML = tokens.map(token => { const isExpired = token.expires_at && new Date(token.expires_at) < new Date(); const maskedToken = token.token.substring(0, 8) + '***' + token.token.substring(token.token.length - 4); return ` <tr> <td>${token.name}</td> <td> <span class="playbook-id" style="font-size: 0.8rem; cursor: default;">${maskedToken}</span> </td> <td style="white-space: nowrap; font-size: 0.85rem;">${formatDate(token.created_at)}</td> <td style="white-space: nowrap; font-size: 0.85rem;">${token.expires_at ? formatDate(token.expires_at) : '永不过期'}</td> <td style="white-space: nowrap; font-size: 0.85rem;">${formatDate(token.last_used_at)}</td> <td> <span style="color: ${isExpired ? '#e53e3e' : (token.is_active ? '#48bb78' : '#cbd5e0')}"> ${isExpired ? '已过期' : (token.is_active ? '活跃' : '禁用')} </span> </td> <td> <button class="btn btn-secondary" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;" onclick="deleteToken(${token.id})">删除</button> </td> </tr> `; }).join(''); } // 显示创建Token模态框 function showCreateTokenModal() { document.getElementById('create-token-modal').style.display = 'block'; document.getElementById('token-name').value = ''; document.getElementById('token-expires').value = ''; } // 关闭创建Token模态框 function closeCreateTokenModal() { document.getElementById('create-token-modal').style.display = 'none'; } // 创建Token async function createToken() { const name = document.getElementById('token-name').value.trim(); const expiresInDays = document.getElementById('token-expires').value; if (!name) { showAlert('请输入Token名称', 'error'); return; } try { const response = await apiCall('/api/admin/tokens', { method: 'POST', body: JSON.stringify({ name: name, expires_in_days: expiresInDays ? parseInt(expiresInDays) : null }) }); if (!response) return; const result = await response.json(); if (result.success) { closeCreateTokenModal(); // 显示新创建的Token值 document.getElementById('new-token-value').value = result.token; document.getElementById('show-token-modal').style.display = 'block'; // 重新加载Token列表 loadTokens(); showAlert('Token创建成功!', 'success'); } else { showAlert(result.error || 'Token创建失败', 'error'); } } catch (error) { showAlert('网络错误: ' + error.message, 'error'); } } // 关闭显示Token值模态框 function closeShowTokenModal() { document.getElementById('show-token-modal').style.display = 'none'; document.getElementById('new-token-value').value = ''; } // 复制Token到剪贴板 async function copyTokenToClipboard() { const tokenInput = document.getElementById('new-token-value'); try { await navigator.clipboard.writeText(tokenInput.value); showAlert('Token已复制到剪贴板', 'success'); } catch (error) { // 兼容旧浏览器 tokenInput.select(); document.execCommand('copy'); showAlert('Token已复制到剪贴板', 'success'); } } // 删除Token async function deleteToken(tokenId) { if (!confirm('确定要删除这个Token吗?删除后无法恢复!')) { return; } try { const response = await apiCall(`/api/admin/tokens/${tokenId}`, { method: 'DELETE' }); if (!response) return; const result = await response.json(); if (result.success) { showAlert('Token删除成功', 'success'); loadTokens(); // 重新加载Token列表 } else { showAlert(result.error || 'Token删除失败', 'error'); } } catch (error) { showAlert('网络错误: ' + error.message, 'error'); } } // 将refreshStats定义为全局函数 function refreshStats() { // 获取所有统计信息(包括剧本和应用统计) apiCall('/api/admin/stats') .then(response => { if (!response) return; return response.json(); }) .then(data => { if (!data) return; if (data.success && data.stats) { // 更新剧本统计 document.getElementById('total-playbooks').textContent = data.stats.total_playbooks || '-'; document.getElementById('enabled-playbooks').textContent = data.stats.enabled_playbooks || '-'; document.getElementById('disabled-playbooks').textContent = data.stats.disabled_playbooks || '-'; // 更新应用统计 document.getElementById('total-apps').textContent = data.stats.total_apps || '-'; // 更新最后同步时间 if (data.stats.last_sync_time) { const lastSync = new Date(data.stats.last_sync_time).toLocaleString('zh-CN'); document.getElementById('last-sync').textContent = lastSync; } } }) .catch(error => console.error('Error fetching stats:', error)); } // Tab切换功能 function showTab(tabName) { // 隐藏所有section document.getElementById('playbooks-section').style.display = 'none'; document.getElementById('tokens-section').style.display = 'none'; document.getElementById('config-section').style.display = 'none'; document.getElementById('stats-section').style.display = 'none'; // 移除所有导航项的active类 document.querySelectorAll('.nav-item').forEach(item => { item.classList.remove('active'); }); // 显示对应的section if (tabName === 'playbooks') { document.getElementById('playbooks-section').style.display = 'block'; document.getElementById('nav-playbooks').classList.add('active'); } else if (tabName === 'tokens') { document.getElementById('tokens-section').style.display = 'block'; document.getElementById('nav-tokens').classList.add('active'); loadTokens(); // 加载Token列表 } else if (tabName === 'config') { document.getElementById('config-section').style.display = 'block'; document.getElementById('nav-config').classList.add('active'); loadSystemConfig(); // 加载系统配置 } else if (tabName === 'stats') { document.getElementById('stats-section').style.display = 'block'; document.getElementById('nav-stats').classList.add('active'); refreshStats(); // 刷新统计信息 } } document.addEventListener('DOMContentLoaded', function() { // 页面加载时检查认证 if (!checkAuth()) { return; // checkAuth会处理跳转 } // 添加导航事件监听器 document.getElementById('nav-playbooks').addEventListener('click', (e) => { e.preventDefault(); showTab('playbooks'); }); document.getElementById('nav-tokens').addEventListener('click', (e) => { e.preventDefault(); showTab('tokens'); }); document.getElementById('nav-config').addEventListener('click', (e) => { e.preventDefault(); showTab('config'); }); document.getElementById('nav-stats').addEventListener('click', (e) => { e.preventDefault(); showTab('stats'); }); // 注销按钮事件监听器 document.getElementById('logout-btn').addEventListener('click', (e) => { e.preventDefault(); logout(); }); setupModalEvents(); // 设置分页按钮事件 prevPageBtn.addEventListener('click', () => { if (currentPage > 1) { goToPage(currentPage - 1); } }); nextPageBtn.addEventListener('click', () => { const totalPages = Math.ceil(playbooks.length / itemsPerPage); if (currentPage < totalPages) { goToPage(currentPage + 1); } }); loadPlaybooks(); // 定期刷新(每30秒) setInterval(loadPlaybooks, 30000); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/wuzhi-dev/soar-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server