Add files via upload

This commit is contained in:
公明
2025-12-18 22:38:33 +08:00
committed by GitHub
parent 79f3ff5453
commit 7be01e95ef
5 changed files with 1075 additions and 244 deletions

View File

@@ -53,12 +53,411 @@ body {
min-height: 0;
}
header {
background: var(--primary-color);
color: white;
padding: 16px 24px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
/* 主侧边栏样式 - Ant Design Pro 风格 */
.main-sidebar {
width: 256px;
background: linear-gradient(180deg, #fafbfc 0%, #f5f7fa 100%);
color: var(--text-primary);
display: flex;
flex-direction: column;
flex-shrink: 0;
border-right: 1px solid var(--border-color);
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.04);
position: relative;
transition: width 0.2s ease;
}
.main-sidebar.collapsed {
width: 64px;
}
.sidebar-collapse-btn {
position: absolute;
top: 16px;
right: 8px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 6px;
background: var(--bg-primary);
border: 1px solid var(--border-color);
color: var(--text-secondary);
transition: all 0.2s ease;
z-index: 10;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
}
.sidebar-collapse-btn:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
border-color: var(--accent-color);
}
.main-sidebar.collapsed .sidebar-collapse-btn {
right: 16px;
transform: rotate(180deg);
}
.sidebar-collapse-btn svg {
width: 16px;
height: 16px;
stroke: currentColor;
transition: transform 0.2s ease;
}
.main-sidebar-header {
padding: 16px 20px;
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
background: var(--bg-primary);
}
.main-sidebar-header .logo {
display: flex;
align-items: center;
gap: 12px;
color: var(--text-primary);
}
.main-sidebar-header .logo span {
font-size: 1.25rem;
font-weight: 600;
letter-spacing: -0.3px;
color: var(--text-primary);
}
.main-sidebar-nav {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 16px 0;
background: transparent;
padding-top: 56px;
}
/* 侧边栏滚动条样式 */
.main-sidebar-nav::-webkit-scrollbar {
width: 6px;
}
.main-sidebar-nav::-webkit-scrollbar-track {
background: transparent;
}
.main-sidebar-nav::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 3px;
}
.main-sidebar-nav::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.3);
}
.nav-item {
margin-bottom: 0;
}
.nav-item-content {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 20px;
cursor: pointer;
transition: all 0.2s ease;
color: var(--text-primary);
position: relative;
font-size: 0.9375rem;
border-left: 3px solid transparent;
justify-content: flex-start;
}
.main-sidebar.collapsed .nav-item-content {
padding: 12px;
justify-content: center;
position: relative;
}
.main-sidebar.collapsed .nav-item-content:hover::after {
content: attr(data-title);
position: absolute;
left: 100%;
top: 50%;
transform: translateY(-50%);
margin-left: 12px;
padding: 6px 12px;
background: rgba(0, 0, 0, 0.85);
color: white;
border-radius: 4px;
font-size: 0.8125rem;
white-space: nowrap;
z-index: 1000;
pointer-events: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.main-sidebar.collapsed .nav-item-content:hover::before {
content: '';
position: absolute;
left: 100%;
top: 50%;
transform: translateY(-50%);
margin-left: 6px;
border: 6px solid transparent;
border-right-color: rgba(0, 0, 0, 0.85);
z-index: 1001;
pointer-events: none;
}
.nav-item-content:hover {
background: rgba(0, 102, 255, 0.06);
color: var(--text-primary);
}
.nav-item.active > .nav-item-content {
background: linear-gradient(90deg, rgba(0, 102, 255, 0.12) 0%, rgba(0, 102, 255, 0.06) 100%);
color: var(--accent-color);
border-left-color: var(--accent-color);
font-weight: 500;
}
.nav-item.active > .nav-item-content svg {
stroke: var(--accent-color);
}
.nav-item-content svg {
flex-shrink: 0;
stroke: currentColor;
width: 18px;
height: 18px;
}
.nav-item-content span {
flex: 1;
font-size: 0.9375rem;
font-weight: 400;
white-space: nowrap;
opacity: 1;
transition: opacity 0.2s ease;
}
.main-sidebar.collapsed .nav-item-content span {
opacity: 0;
width: 0;
overflow: hidden;
}
.nav-item-has-submenu .nav-item-content {
justify-content: space-between;
}
.main-sidebar.collapsed .nav-item-has-submenu .nav-item-content {
justify-content: center;
}
.main-sidebar.collapsed .submenu-arrow {
display: none;
}
.submenu-arrow {
transition: transform 0.2s ease;
flex-shrink: 0;
width: 14px;
height: 14px;
stroke: var(--text-secondary);
}
.nav-item.expanded .submenu-arrow {
transform: rotate(90deg);
}
.nav-item.expanded > .nav-item-content {
color: var(--text-primary);
font-weight: 500;
}
.nav-submenu {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
background: rgba(255, 255, 255, 0.5);
}
.nav-item.expanded .nav-submenu {
max-height: 300px;
}
.nav-submenu-item {
padding: 10px 20px 10px 56px;
cursor: pointer;
transition: all 0.2s ease;
color: var(--text-secondary);
font-size: 0.875rem;
position: relative;
border-left: 3px solid transparent;
}
.main-sidebar.collapsed .nav-submenu {
display: none;
}
.main-sidebar.collapsed .nav-item.expanded .nav-submenu {
display: none;
}
/* 子菜单弹出框样式 */
.submenu-popup {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 4px 0;
min-width: 160px;
margin-left: 8px;
animation: popupFadeIn 0.2s ease;
}
@keyframes popupFadeIn {
from {
opacity: 0;
transform: translateX(-8px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.submenu-popup-item {
padding: 10px 16px;
cursor: pointer;
transition: all 0.2s ease;
color: var(--text-primary);
font-size: 0.875rem;
white-space: nowrap;
}
.submenu-popup-item:hover {
background: rgba(0, 102, 255, 0.08);
color: var(--accent-color);
}
.submenu-popup-item:active {
background: rgba(0, 102, 255, 0.12);
}
.submenu-popup-item.active {
background: rgba(0, 102, 255, 0.1);
color: var(--accent-color);
font-weight: 500;
}
.nav-submenu-item:hover {
background: rgba(0, 102, 255, 0.06);
color: var(--text-primary);
}
.nav-submenu-item.active {
background: linear-gradient(90deg, rgba(0, 102, 255, 0.12) 0%, rgba(0, 102, 255, 0.06) 100%);
color: var(--accent-color);
border-left-color: var(--accent-color);
font-weight: 500;
}
/* 内容区域 */
.content-area {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 0;
background: #f5f7fa;
}
/* 对话页面不需要page-header */
#page-chat .page-header {
display: none;
}
#page-chat .page-content {
padding: 0;
overflow: hidden;
}
.page {
display: none;
flex: 1;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
.page.active {
display: flex;
}
.page-header {
padding: 20px 24px;
background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%);
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
}
.page-header h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
color: var(--text-primary);
}
.page-header-actions {
display: flex;
gap: 12px;
align-items: center;
}
.page-content {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 24px;
min-height: 0;
}
/* 对话页面布局 */
.chat-page-layout {
display: flex;
flex: 1;
overflow: hidden;
min-height: 0;
width: 100%;
height: 100%;
}
.conversation-sidebar {
width: 280px;
background: linear-gradient(180deg, #ffffff 0%, #fafbfc 100%);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
flex-shrink: 0;
height: 100%;
overflow: hidden;
}
header {
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
color: var(--text-primary);
padding: 10px 24px;
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.header-content {
@@ -78,10 +477,11 @@ header {
}
.logo h1 {
font-size: 1.75rem;
font-size: 1.5rem;
font-weight: 600;
letter-spacing: -0.5px;
margin: 0;
color: var(--text-primary);
}
.header-right {
@@ -92,7 +492,7 @@ header {
.header-subtitle {
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.7);
color: var(--text-secondary);
margin: 0;
font-weight: 400;
}
@@ -108,13 +508,13 @@ header {
align-items: center;
justify-content: center;
gap: 6px;
padding: 8px 14px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
color: white;
padding: 6px 12px;
border-radius: 6px;
border: 1px solid var(--border-color);
background: var(--bg-primary);
color: var(--text-primary);
cursor: pointer;
font-size: 0.85rem;
font-size: 0.8125rem;
font-weight: 500;
transition: all 0.2s ease;
}
@@ -124,41 +524,42 @@ header {
}
.header-actions button:hover {
background: rgba(255, 255, 255, 0.12);
border-color: rgba(255, 255, 255, 0.35);
background: var(--bg-tertiary);
border-color: var(--accent-color);
color: var(--accent-color);
transform: translateY(-1px);
}
.monitor-btn {
color: #8cc4ff;
border-color: rgba(0, 102, 255, 0.35);
background: rgba(0, 102, 255, 0.15);
color: var(--accent-color);
border-color: var(--border-color);
background: var(--bg-primary);
}
.monitor-btn:hover {
background: rgba(0, 102, 255, 0.25);
border-color: rgba(0, 102, 255, 0.45);
color: #cfe4ff;
background: rgba(0, 102, 255, 0.08);
border-color: var(--accent-color);
color: var(--accent-color);
}
.attack-chain-btn {
color: #ffe08a;
border-color: rgba(255, 255, 255, 0.3);
background: rgba(255, 255, 255, 0.08);
color: var(--text-primary);
border-color: var(--border-color);
background: var(--bg-primary);
}
.attack-chain-btn:not(:disabled):hover {
background: rgba(255, 255, 255, 0.18);
border-color: rgba(255, 255, 255, 0.45);
color: #fff5cc;
background: var(--bg-tertiary);
border-color: var(--accent-color);
color: var(--accent-color);
}
.attack-chain-btn:disabled {
opacity: 0.55;
opacity: 0.5;
cursor: not-allowed;
border-color: rgba(255, 255, 255, 0.15);
background: rgba(255, 255, 255, 0.04);
color: rgba(255, 255, 255, 0.6);
border-color: var(--border-color);
background: var(--bg-secondary);
color: var(--text-muted);
}
.settings-btn {
@@ -166,7 +567,17 @@ header {
min-width: 44px;
}
/* 侧边栏样式 */
/* 设置页面样式 */
.settings-actions {
margin-top: 32px;
padding-top: 24px;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: flex-end;
gap: 12px;
}
/* 旧侧边栏样式(保留用于对话页面内的历史对话侧边栏) */
.sidebar {
width: 280px;
background: var(--bg-secondary);
@@ -355,7 +766,7 @@ header {
flex-direction: column;
flex: 1;
min-width: 0;
background: var(--bg-secondary);
background: #f5f7fa;
overflow: hidden;
height: 100%;
}
@@ -365,7 +776,7 @@ header {
overflow-y: auto;
overflow-x: hidden;
padding: 24px;
background: var(--bg-secondary);
background: #f5f7fa;
display: flex;
flex-direction: column;
min-height: 0;
@@ -1365,12 +1776,12 @@ header {
}
header {
padding: 16px 20px;
padding: 10px 20px;
flex-shrink: 0;
}
.logo h1 {
font-size: 1.5rem;
font-size: 1.25rem;
}
.header-subtitle {
@@ -1382,7 +1793,36 @@ header {
min-height: 0;
}
/* 主侧边栏在移动设备上可以折叠或调整 */
.main-sidebar {
width: 200px;
}
.main-sidebar.collapsed {
width: 64px;
}
.sidebar-collapse-btn {
width: 28px;
height: 28px;
}
.main-sidebar-header .logo span {
font-size: 1rem;
}
.nav-item-content {
padding: 10px 12px;
font-size: 0.875rem;
}
.nav-item-content span {
font-size: 0.875rem;
}
.conversation-sidebar,
.sidebar {
width: 240px;
height: 100%;
overflow: hidden;
}
@@ -1410,6 +1850,19 @@ header {
flex-shrink: 0;
}
.page-header {
padding: 16px;
flex-wrap: wrap;
}
.page-header h2 {
font-size: 1.25rem;
}
.page-content {
padding: 16px;
}
.modal-content {
width: 95%;
margin: 10% auto;

View File

@@ -911,42 +911,17 @@ const monitorState = {
};
function openMonitorPanel() {
const modal = document.getElementById('monitor-modal');
if (!modal) {
return;
// 切换到MCP监控页面
if (typeof switchPage === 'function') {
switchPage('mcp-monitor');
}
modal.style.display = 'block';
// 重置显示状态
const statsContainer = document.getElementById('monitor-stats');
const execContainer = document.getElementById('monitor-executions');
if (statsContainer) {
statsContainer.innerHTML = '<div class="monitor-empty">加载中...</div>';
}
if (execContainer) {
execContainer.innerHTML = '<div class="monitor-empty">加载中...</div>';
}
const statusFilter = document.getElementById('monitor-status-filter');
if (statusFilter) {
statusFilter.value = 'all';
}
// 重置分页状态
monitorState.pagination = {
page: 1,
pageSize: 20,
total: 0,
totalPages: 0
};
refreshMonitorPanel(1);
}
function closeMonitorPanel() {
const modal = document.getElementById('monitor-modal');
if (modal) {
modal.style.display = 'none';
// 不再需要关闭功能,因为现在是页面而不是模态框
// 如果需要,可以切换回对话页面
if (typeof switchPage === 'function') {
switchPage('chat');
}
}

238
web/static/js/router.js Normal file
View File

@@ -0,0 +1,238 @@
// 页面路由管理
let currentPage = 'chat';
// 初始化路由
function initRouter() {
// 默认显示对话页面
switchPage('chat');
// 从URL hash读取页面如果有
const hash = window.location.hash.slice(1);
if (hash && ['chat', 'mcp-monitor', 'mcp-management', 'settings'].includes(hash)) {
switchPage(hash);
}
}
// 切换页面
function switchPage(pageId) {
// 隐藏所有页面
document.querySelectorAll('.page').forEach(page => {
page.classList.remove('active');
});
// 显示目标页面
const targetPage = document.getElementById(`page-${pageId}`);
if (targetPage) {
targetPage.classList.add('active');
currentPage = pageId;
// 更新URL hash
window.location.hash = pageId;
// 更新导航状态
updateNavState(pageId);
// 页面特定的初始化
initPage(pageId);
}
}
// 更新导航状态
function updateNavState(pageId) {
// 移除所有活动状态
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.remove('active');
});
document.querySelectorAll('.nav-submenu-item').forEach(item => {
item.classList.remove('active');
});
// 设置活动状态
if (pageId === 'mcp-monitor' || pageId === 'mcp-management') {
// MCP子菜单项
const mcpItem = document.querySelector('.nav-item[data-page="mcp"]');
if (mcpItem) {
mcpItem.classList.add('active');
// 展开MCP子菜单
mcpItem.classList.add('expanded');
}
const submenuItem = document.querySelector(`.nav-submenu-item[data-page="${pageId}"]`);
if (submenuItem) {
submenuItem.classList.add('active');
}
} else {
// 主菜单项
const navItem = document.querySelector(`.nav-item[data-page="${pageId}"]`);
if (navItem) {
navItem.classList.add('active');
}
}
}
// 切换子菜单
function toggleSubmenu(menuId) {
const sidebar = document.getElementById('main-sidebar');
const navItem = document.querySelector(`.nav-item[data-page="${menuId}"]`);
if (!navItem) return;
// 检查侧边栏是否折叠
if (sidebar && sidebar.classList.contains('collapsed')) {
// 折叠状态下显示弹出菜单
showSubmenuPopup(navItem, menuId);
} else {
// 展开状态下正常切换子菜单
navItem.classList.toggle('expanded');
}
}
// 显示子菜单弹出框
function showSubmenuPopup(navItem, menuId) {
// 移除其他已打开的弹出菜单
const existingPopup = document.querySelector('.submenu-popup');
if (existingPopup) {
existingPopup.remove();
return; // 如果已经打开,点击时关闭
}
const navItemContent = navItem.querySelector('.nav-item-content');
const submenu = navItem.querySelector('.nav-submenu');
if (!submenu) return;
// 获取菜单位置
const rect = navItemContent.getBoundingClientRect();
// 创建弹出菜单
const popup = document.createElement('div');
popup.className = 'submenu-popup';
popup.style.position = 'fixed';
popup.style.left = (rect.right + 8) + 'px';
popup.style.top = rect.top + 'px';
popup.style.zIndex = '1000';
// 复制子菜单项到弹出菜单
const submenuItems = submenu.querySelectorAll('.nav-submenu-item');
submenuItems.forEach(item => {
const popupItem = document.createElement('div');
popupItem.className = 'submenu-popup-item';
popupItem.textContent = item.textContent.trim();
// 检查是否是当前激活的页面
const pageId = item.getAttribute('data-page');
if (pageId && document.querySelector(`.nav-submenu-item[data-page="${pageId}"].active`)) {
popupItem.classList.add('active');
}
popupItem.onclick = function(e) {
e.stopPropagation();
e.preventDefault();
// 获取页面ID并切换
const pageId = item.getAttribute('data-page');
if (pageId) {
switchPage(pageId);
}
// 关闭弹出菜单
popup.remove();
document.removeEventListener('click', closePopup);
};
popup.appendChild(popupItem);
});
document.body.appendChild(popup);
// 点击外部关闭弹出菜单
const closePopup = function(e) {
if (!popup.contains(e.target) && !navItem.contains(e.target)) {
popup.remove();
document.removeEventListener('click', closePopup);
}
};
// 延迟添加事件监听,避免立即触发
setTimeout(() => {
document.addEventListener('click', closePopup);
}, 0);
}
// 初始化页面
function initPage(pageId) {
switch(pageId) {
case 'chat':
// 对话页面已由chat.js初始化
break;
case 'mcp-monitor':
// 初始化监控面板
if (typeof refreshMonitorPanel === 'function') {
refreshMonitorPanel();
}
break;
case 'mcp-management':
// 初始化MCP管理
if (typeof loadExternalMCPs === 'function') {
loadExternalMCPs();
}
// 加载工具列表MCP工具配置已移到MCP管理页面
if (typeof loadToolsList === 'function') {
// 确保工具分页设置已初始化
if (typeof getToolsPageSize === 'function' && typeof toolsPagination !== 'undefined') {
toolsPagination.pageSize = getToolsPageSize();
}
loadToolsList(1, '');
}
break;
case 'settings':
// 初始化设置页面
if (typeof loadConfig === 'function') {
loadConfig();
}
break;
}
}
// 页面加载完成后初始化路由
document.addEventListener('DOMContentLoaded', function() {
initRouter();
initSidebarState();
// 监听hash变化
window.addEventListener('hashchange', function() {
const hash = window.location.hash.slice(1);
if (hash && ['chat', 'mcp-monitor', 'mcp-management', 'settings'].includes(hash)) {
switchPage(hash);
}
});
});
// 切换侧边栏折叠/展开
function toggleSidebar() {
const sidebar = document.getElementById('main-sidebar');
if (sidebar) {
sidebar.classList.toggle('collapsed');
// 保存折叠状态到localStorage
const isCollapsed = sidebar.classList.contains('collapsed');
localStorage.setItem('sidebarCollapsed', isCollapsed ? 'true' : 'false');
}
}
// 初始化侧边栏状态
function initSidebarState() {
const sidebar = document.getElementById('main-sidebar');
if (sidebar) {
const savedState = localStorage.getItem('sidebarCollapsed');
if (savedState === 'true') {
sidebar.classList.add('collapsed');
}
}
}
// 导出函数供其他脚本使用
window.switchPage = switchPage;
window.toggleSubmenu = toggleSubmenu;
window.toggleSidebar = toggleSidebar;
window.currentPage = function() { return currentPage; };

View File

@@ -19,8 +19,10 @@ let toolsPagination = {
// 打开设置
async function openSettings() {
const modal = document.getElementById('settings-modal');
modal.style.display = 'block';
// 切换到设置页面
if (typeof switchPage === 'function') {
switchPage('settings');
}
// 每次打开时清空全局状态映射,重新加载最新配置
toolStateMap.clear();
@@ -34,27 +36,22 @@ async function openSettings() {
});
}
// 关闭设置
// 关闭设置(保留函数以兼容旧代码,但现在不需要关闭功能)
function closeSettings() {
const modal = document.getElementById('settings-modal');
modal.style.display = 'none';
// 不再需要关闭功能,因为现在是页面而不是模态框
// 如果需要,可以切换回对话页面
if (typeof switchPage === 'function') {
switchPage('chat');
}
}
// 点击模态框外部关闭
// 点击模态框外部关闭只保留MCP详情模态框
window.onclick = function(event) {
const settingsModal = document.getElementById('settings-modal');
const mcpModal = document.getElementById('mcp-detail-modal');
const monitorModal = document.getElementById('monitor-modal');
if (event.target === settingsModal) {
closeSettings();
}
if (event.target === mcpModal) {
closeMCPDetail();
}
if (event.target === monitorModal) {
closeMonitorPanel();
}
}
// 加载配置
@@ -623,6 +620,122 @@ async function applySettings() {
}
}
// 保存工具配置独立函数用于MCP管理页面
async function saveToolsConfig() {
try {
// 先保存当前页的状态到全局映射
saveCurrentPageToolStates();
// 获取当前配置(只获取工具部分)
const response = await apiFetch('/api/config');
if (!response.ok) {
throw new Error('获取配置失败');
}
const currentConfig = await response.json();
// 构建只包含工具配置的配置对象
const config = {
openai: currentConfig.openai || {},
agent: currentConfig.agent || {},
tools: []
};
// 收集工具启用状态与applySettings中的逻辑相同
try {
const allToolsMap = new Map();
let page = 1;
let hasMore = true;
const pageSize = 100;
// 遍历所有页面获取所有工具
while (hasMore) {
const url = `/api/config/tools?page=${page}&page_size=${pageSize}`;
const pageResponse = await apiFetch(url);
if (!pageResponse.ok) {
throw new Error('获取工具列表失败');
}
const pageResult = await pageResponse.json();
// 将工具添加到映射中
pageResult.tools.forEach(tool => {
const savedState = toolStateMap.get(tool.name);
allToolsMap.set(tool.name, {
name: tool.name,
enabled: savedState ? savedState.enabled : tool.enabled,
is_external: savedState ? savedState.is_external : (tool.is_external || false),
external_mcp: savedState ? savedState.external_mcp : (tool.external_mcp || '')
});
});
// 检查是否还有更多页面
if (page >= pageResult.total_pages) {
hasMore = false;
} else {
page++;
}
}
// 将所有工具添加到配置中
allToolsMap.forEach(tool => {
config.tools.push({
name: tool.name,
enabled: tool.enabled,
is_external: tool.is_external,
external_mcp: tool.external_mcp
});
});
} catch (error) {
console.warn('获取所有工具列表失败,仅使用全局状态映射', error);
// 如果获取失败,使用全局状态映射
toolStateMap.forEach((toolData, toolName) => {
config.tools.push({
name: toolName,
enabled: toolData.enabled,
is_external: toolData.is_external,
external_mcp: toolData.external_mcp
});
});
}
// 更新配置
const updateResponse = await apiFetch('/api/config', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(config)
});
if (!updateResponse.ok) {
const error = await updateResponse.json();
throw new Error(error.error || '更新配置失败');
}
// 应用配置
const applyResponse = await apiFetch('/api/config/apply', {
method: 'POST'
});
if (!applyResponse.ok) {
const error = await applyResponse.json();
throw new Error(error.error || '应用配置失败');
}
alert('工具配置已成功保存!');
// 重新加载工具列表以反映最新状态
if (typeof loadToolsList === 'function') {
await loadToolsList(toolsPagination.page, toolsSearchKeyword);
}
} catch (error) {
console.error('保存工具配置失败:', error);
alert('保存工具配置失败: ' + error.message);
}
}
function resetPasswordForm() {
const currentInput = document.getElementById('auth-current-password');
const newInput = document.getElementById('auth-new-password');

View File

@@ -36,199 +36,250 @@
<div class="header-right">
<p class="header-subtitle">AI 驱动的自动化安全测试平台</p>
<div class="header-actions">
<button class="monitor-btn" onclick="openMonitorPanel()" title="MCP 监控面板">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 12h4l3 8 4-16 3 8h4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>MCP监控</span>
</button>
<button id="attack-chain-btn" class="attack-chain-btn" title="请选择一个对话以查看攻击链" disabled>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5 13.5l3-3M8 8H5a4 4 0 1 0 0 8h3m8-8h3a4 4 0 0 1 0 8h-3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>攻击链</span>
</button>
<button class="settings-btn" onclick="openSettings()" title="设置">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
</div>
</div>
</header>
<div class="main-layout">
<!-- 历史对话侧边栏 -->
<aside class="sidebar">
<div class="sidebar-header">
<button class="new-chat-btn" onclick="startNewConversation()">
<span>+</span> 新对话
</button>
</div>
<div class="sidebar-content">
<div class="sidebar-title">历史对话</div>
<div id="conversations-list" class="conversations-list"></div>
<!-- 侧边栏 -->
<aside class="main-sidebar" id="main-sidebar">
<div class="sidebar-collapse-btn" onclick="toggleSidebar()" title="折叠/展开侧边栏">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<nav class="main-sidebar-nav">
<div class="nav-item" data-page="chat">
<div class="nav-item-content" data-title="对话" onclick="switchPage('chat')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>对话</span>
</div>
</div>
<div class="nav-item nav-item-has-submenu" data-page="mcp">
<div class="nav-item-content" data-title="MCP" onclick="toggleSubmenu('mcp')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 12h4l3 8 4-16 3 8h4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>MCP</span>
<svg class="submenu-arrow" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="nav-submenu">
<div class="nav-submenu-item" data-page="mcp-monitor" onclick="switchPage('mcp-monitor')">
<span>MCP状态监控</span>
</div>
<div class="nav-submenu-item" data-page="mcp-management" onclick="switchPage('mcp-management')">
<span>MCP管理</span>
</div>
</div>
</div>
<div class="nav-item" data-page="settings">
<div class="nav-item-content" data-title="系统设置" onclick="switchPage('settings')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>系统设置</span>
</div>
</div>
</nav>
</aside>
<!-- 对话界面 -->
<div class="chat-container">
<div id="active-tasks-bar" class="active-tasks-bar"></div>
<div id="chat-messages" class="chat-messages"></div>
<div class="chat-input-container">
<div class="chat-input-field">
<textarea id="chat-input" placeholder="输入测试目标或命令... (Shift+Enter 换行Enter 发送)" rows="1"></textarea>
<div id="mention-suggestions" class="mention-suggestions" role="listbox" aria-label="工具提及候选"></div>
</div>
<button onclick="sendMessage()">发送</button>
</div>
</div>
</div>
</div>
<!-- 设置模态框 -->
<div id="settings-modal" class="modal">
<div class="modal-content settings-modal-content">
<div class="modal-header">
<h2>系统设置</h2>
<span class="modal-close" onclick="closeSettings()">&times;</span>
</div>
<div class="modal-body settings-body">
<!-- OpenAI配置 -->
<div class="settings-section">
<h3>OpenAI 配置</h3>
<div class="settings-form">
<div class="form-group">
<label for="openai-base-url">Base URL <span style="color: red;">*</span></label>
<input type="text" id="openai-base-url" placeholder="https://api.openai.com/v1" required />
</div>
<div class="form-group">
<label for="openai-api-key">API Key <span style="color: red;">*</span></label>
<input type="password" id="openai-api-key" placeholder="输入OpenAI API Key" required />
</div>
<div class="form-group">
<label for="openai-model">模型 <span style="color: red;">*</span></label>
<input type="text" id="openai-model" placeholder="gpt-4" required />
</div>
</div>
</div>
<!-- MCP工具配置 -->
<div class="settings-section">
<h3>MCP 工具配置</h3>
<div class="tools-controls">
<div class="tools-actions">
<button class="btn-secondary" onclick="selectAllTools()">全选</button>
<button class="btn-secondary" onclick="deselectAllTools()">全不选</button>
<div class="search-box">
<input type="text" id="tools-search" placeholder="搜索工具..." onkeypress="handleSearchKeyPress(event)" oninput="if(this.value.trim() === '') clearSearch()" />
<button class="btn-search" onclick="searchTools()" title="搜索">🔍</button>
<!-- 内容区域 -->
<div class="content-area">
<!-- 对话页面 -->
<div id="page-chat" class="page active">
<div class="chat-page-layout">
<!-- 历史对话侧边栏 -->
<aside class="conversation-sidebar">
<div class="sidebar-header">
<button class="new-chat-btn" onclick="startNewConversation()">
<span>+</span> 新对话
</button>
</div>
<div class="sidebar-content">
<div class="sidebar-title">历史对话</div>
<div id="conversations-list" class="conversations-list"></div>
</div>
</aside>
<!-- 对话界面 -->
<div class="chat-container">
<div id="active-tasks-bar" class="active-tasks-bar"></div>
<div id="chat-messages" class="chat-messages"></div>
<div class="chat-input-container">
<div class="chat-input-field">
<textarea id="chat-input" placeholder="输入测试目标或命令... (Shift+Enter 换行Enter 发送)" rows="1"></textarea>
<div id="mention-suggestions" class="mention-suggestions" role="listbox" aria-label="工具提及候选"></div>
</div>
<button onclick="sendMessage()">发送</button>
</div>
<div class="tools-stats" id="tools-stats"></div>
</div>
<div id="tools-list" class="tools-list"></div>
</div>
</div>
<!-- 外部MCP配置 -->
<div class="settings-section">
<h3>外部 MCP 配置</h3>
<div class="external-mcp-controls">
<div class="external-mcp-actions">
<button class="btn-primary" onclick="showAddExternalMCPModal()">添加外部MCP</button>
<!-- MCP状态监控页面 -->
<div id="page-mcp-monitor" class="page">
<div class="page-header">
<h2>MCP 状态监控</h2>
<button class="btn-secondary" onclick="refreshMonitorPanel()">刷新</button>
</div>
<div class="page-content">
<div class="monitor-sections">
<section class="monitor-section monitor-overview">
<div class="section-header">
<h3>执行统计</h3>
</div>
<div id="monitor-stats" class="monitor-stats-grid">
<div class="monitor-empty">加载中...</div>
</div>
</section>
<section class="monitor-section monitor-executions">
<div class="section-header">
<h3>最新执行记录</h3>
<div class="section-actions">
<label>
状态筛选
<select id="monitor-status-filter" onchange="applyMonitorFilters()">
<option value="all">全部</option>
<option value="completed">已完成</option>
<option value="running">执行中</option>
<option value="failed">失败</option>
</select>
</label>
</div>
</div>
<div id="monitor-executions" class="monitor-table-container">
<div class="monitor-empty">加载中...</div>
</div>
</section>
</div>
</div>
</div>
<!-- MCP管理页面 -->
<div id="page-mcp-management" class="page">
<div class="page-header">
<h2>MCP 管理</h2>
<div class="page-header-actions">
<button class="btn-secondary" onclick="loadExternalMCPs()">刷新</button>
<div class="external-mcp-stats" id="external-mcp-stats"></div>
</div>
<div id="external-mcp-list" class="external-mcp-list"></div>
</div>
</div>
<!-- Agent配置 -->
<div class="settings-section">
<h3>Agent 配置</h3>
<div class="settings-form">
<div class="form-group">
<label for="agent-max-iterations">最大迭代次数</label>
<input type="number" id="agent-max-iterations" min="1" max="100" placeholder="30" />
<button class="btn-primary" onclick="showAddExternalMCPModal()">添加外部MCP</button>
</div>
</div>
</div>
<!-- 安全设置 -->
<div class="settings-section">
<h3>安全设</h3>
<div class="settings-form">
<div class="form-group">
<label for="auth-current-password">当前密码</label>
<input type="password" id="auth-current-password" placeholder="输入当前登录密码" autocomplete="current-password" />
</div>
<div class="form-group">
<label for="auth-new-password">新密码</label>
<input type="password" id="auth-new-password" placeholder="设置新密码(至少 8 位)" autocomplete="new-password" />
</div>
<div class="form-group">
<label for="auth-confirm-password">确认新密码</label>
<input type="password" id="auth-confirm-password" placeholder="再次输入新密码" autocomplete="new-password" />
</div>
<div class="form-actions">
<button class="btn-secondary" type="button" onclick="resetPasswordForm()">清空</button>
<button class="btn-primary change-password-submit" type="button" onclick="changePassword()">修改密码</button>
</div>
<p class="password-hint">修改密码后,需要使用新密码重新登录。</p>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeSettings()">取消</button>
<button class="btn-primary" onclick="applySettings()">应用配置</button>
</div>
</div>
</div>
<!-- 监控面板模态框 -->
<div id="monitor-modal" class="modal">
<div class="modal-content monitor-modal-content">
<div class="modal-header">
<h2>MCP 监控面板</h2>
<span class="modal-close" onclick="closeMonitorPanel()">&times;</span>
</div>
<div class="monitor-modal-body">
<div class="monitor-sections">
<section class="monitor-section monitor-overview">
<div class="section-header">
<h3>执行统计</h3>
<button class="btn-secondary" onclick="refreshMonitorPanel()">刷新</button>
</div>
<div id="monitor-stats" class="monitor-stats-grid">
<div class="monitor-empty">加载中...</div>
</div>
</section>
<section class="monitor-section monitor-executions">
<div class="section-header">
<h3>最新执行记录</h3>
<div class="section-actions">
<label>
状态筛选
<select id="monitor-status-filter" onchange="applyMonitorFilters()">
<option value="all">全部</option>
<option value="completed">已完成</option>
<option value="running">执行中</option>
<option value="failed">失败</option>
</select>
</label>
<div class="page-content">
<!-- MCP工具配置 -->
<div class="settings-section" style="margin-bottom: 32px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<h3 style="margin: 0;">MCP 工具配</h3>
<button class="btn-primary" onclick="saveToolsConfig()">保存工具配置</button>
</div>
<div class="tools-controls">
<div class="tools-actions">
<button class="btn-secondary" onclick="selectAllTools()">全选</button>
<button class="btn-secondary" onclick="deselectAllTools()">全不选</button>
<div class="search-box">
<input type="text" id="tools-search" placeholder="搜索工具..." onkeypress="handleSearchKeyPress(event)" oninput="if(this.value.trim() === '') clearSearch()" />
<button class="btn-search" onclick="searchTools()" title="搜索">🔍</button>
</div>
<div class="tools-stats" id="tools-stats"></div>
</div>
<div id="tools-list" class="tools-list"></div>
</div>
</div>
<div id="monitor-executions" class="monitor-table-container">
<div class="monitor-empty">加载中...</div>
<!-- 外部MCP配置 -->
<div class="settings-section">
<h3>外部 MCP 配置</h3>
<div class="external-mcp-controls">
<div class="external-mcp-actions">
<div class="external-mcp-stats" id="external-mcp-stats"></div>
</div>
<div id="external-mcp-list" class="external-mcp-list"></div>
</div>
</div>
</section>
</div>
</div>
<!-- 系统设置页面 -->
<div id="page-settings" class="page">
<div class="page-header">
<h2>系统设置</h2>
</div>
<div class="page-content settings-body">
<!-- OpenAI配置 -->
<div class="settings-section">
<h3>OpenAI 配置</h3>
<div class="settings-form">
<div class="form-group">
<label for="openai-base-url">Base URL <span style="color: red;">*</span></label>
<input type="text" id="openai-base-url" placeholder="https://api.openai.com/v1" required />
</div>
<div class="form-group">
<label for="openai-api-key">API Key <span style="color: red;">*</span></label>
<input type="password" id="openai-api-key" placeholder="输入OpenAI API Key" required />
</div>
<div class="form-group">
<label for="openai-model">模型 <span style="color: red;">*</span></label>
<input type="text" id="openai-model" placeholder="gpt-4" required />
</div>
</div>
</div>
<!-- Agent配置 -->
<div class="settings-section">
<h3>Agent 配置</h3>
<div class="settings-form">
<div class="form-group">
<label for="agent-max-iterations">最大迭代次数</label>
<input type="number" id="agent-max-iterations" min="1" max="100" placeholder="30" />
</div>
</div>
</div>
<!-- 安全设置 -->
<div class="settings-section">
<h3>安全设置</h3>
<div class="settings-form">
<div class="form-group">
<label for="auth-current-password">当前密码</label>
<input type="password" id="auth-current-password" placeholder="输入当前登录密码" autocomplete="current-password" />
</div>
<div class="form-group">
<label for="auth-new-password">新密码</label>
<input type="password" id="auth-new-password" placeholder="设置新密码(至少 8 位)" autocomplete="new-password" />
</div>
<div class="form-group">
<label for="auth-confirm-password">确认新密码</label>
<input type="password" id="auth-confirm-password" placeholder="再次输入新密码" autocomplete="new-password" />
</div>
<div class="form-actions">
<button class="btn-secondary" type="button" onclick="resetPasswordForm()">清空</button>
<button class="btn-primary change-password-submit" type="button" onclick="changePassword()">修改密码</button>
</div>
<p class="password-hint">修改密码后,需要使用新密码重新登录。</p>
</div>
</div>
<div class="settings-actions">
<button class="btn-primary" onclick="applySettings()">应用配置</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- MCP调用详情模态框 -->
<div id="mcp-detail-modal" class="modal">
<div class="modal-content">
@@ -413,6 +464,7 @@
<!-- dagre layout for hierarchical layout -->
<script src="https://cdn.jsdelivr.net/npm/cytoscape-dagre@2.5.0/cytoscape-dagre.min.js"></script>
<script src="/static/js/auth.js"></script>
<script src="/static/js/router.js"></script>
<script src="/static/js/monitor.js"></script>
<script src="/static/js/chat.js"></script>
<script src="/static/js/settings.js"></script>