Add files via upload

This commit is contained in:
公明
2026-01-15 22:00:10 +08:00
committed by GitHub
parent 67e2e56bd2
commit 68ad2bf67a
44 changed files with 10016 additions and 72 deletions
+531 -3
View File
@@ -840,6 +840,8 @@ header {
justify-content: space-between;
gap: 8px;
position: relative;
user-select: none;
-webkit-user-select: none;
}
.conversation-item:hover {
@@ -3260,6 +3262,189 @@ header {
flex-wrap: wrap;
}
.pagination-container {
margin-top: 16px;
}
/* Skills管理页面分页优化 */
.page-content-with-pagination {
display: flex;
flex-direction: column;
height: 100%;
position: relative;
overflow: hidden;
}
.skills-list-with-pagination {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
min-height: 0;
/* 为分页组件预留空间,确保视觉连接自然 */
padding-bottom: 0;
}
.pagination-fixed {
background: var(--bg-primary);
margin-top: 0;
padding: 0;
/* 确保分页组件宽度与内容区域一致,不包括滚动条 */
width: 100%;
box-sizing: border-box;
/* 确保分页组件不延伸到滚动条区域 */
overflow: hidden;
/* 当列表有滚动条时,分页组件应该与内容区域对齐 */
position: relative;
}
.pagination-fixed .pagination {
margin-top: 0;
border-top: 1px solid var(--border-color);
padding: 16px 20px;
background: var(--bg-primary);
justify-content: space-between;
align-items: center;
gap: 20px;
flex-wrap: wrap;
/* 确保分页内容与列表内容对齐 */
width: 100%;
box-sizing: border-box;
/* 柔和的顶部边框,与列表自然分离 */
border-top-color: rgba(233, 236, 239, 0.6);
}
/* 左侧:信息显示和每页数量选择器 - 更自然的设计 */
.pagination-fixed .pagination-info {
font-size: 0.875rem;
color: var(--text-secondary);
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
/* 去掉背景色和边框,更自然 */
padding: 0;
background: transparent;
border-radius: 0;
}
.pagination-fixed .pagination-info span {
color: var(--text-primary);
white-space: nowrap;
font-weight: 400;
}
.pagination-fixed .pagination-info .pagination-page-size {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.875rem;
color: var(--text-secondary);
font-weight: 400;
}
.pagination-fixed .pagination-info .pagination-page-size select {
padding: 6px 10px;
border-radius: 6px;
border: 1px solid var(--border-color);
background: var(--bg-primary);
color: var(--text-primary);
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s ease;
min-width: 70px;
font-weight: 500;
/* 更柔和的边框 */
border-color: rgba(233, 236, 239, 0.8);
}
.pagination-fixed .pagination-info .pagination-page-size select:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1);
background: var(--bg-primary);
}
.pagination-fixed .pagination-info .pagination-page-size select:hover {
border-color: var(--accent-color);
background: var(--bg-secondary);
}
/* 右侧:分页按钮组 - 更统一的设计 */
.pagination-fixed .pagination-controls {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.pagination-fixed .pagination-controls .btn-secondary {
padding: 7px 14px;
font-size: 0.875rem;
min-width: auto;
transition: all 0.2s ease;
font-weight: 500;
/* 更柔和的边框 */
border-color: rgba(233, 236, 239, 0.8);
}
.pagination-fixed .pagination-controls .btn-secondary:hover:not(:disabled) {
background: var(--bg-tertiary);
border-color: var(--accent-color);
color: var(--accent-color);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 102, 255, 0.1);
}
.pagination-fixed .pagination-controls .btn-secondary:active:not(:disabled) {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 102, 255, 0.08);
}
.pagination-fixed .pagination-controls .btn-secondary:disabled {
opacity: 0.4;
cursor: not-allowed;
background: var(--bg-secondary);
border-color: var(--border-color);
color: var(--text-muted);
}
.pagination-fixed .pagination-page {
font-size: 0.875rem;
color: var(--text-secondary);
padding: 0 12px;
white-space: nowrap;
font-weight: 400;
}
/* 响应式优化 */
@media (max-width: 768px) {
.pagination-fixed .pagination {
flex-direction: column;
gap: 16px;
align-items: stretch;
padding: 16px;
}
.pagination-fixed .pagination-info {
width: 100%;
justify-content: center;
gap: 16px;
}
.pagination-fixed .pagination-controls {
width: 100%;
justify-content: center;
flex-wrap: wrap;
gap: 6px;
}
.pagination-fixed .pagination-controls .btn-secondary {
flex: 1;
min-width: 60px;
max-width: 120px;
}
}
.pagination-info {
font-size: 0.875rem;
color: var(--text-secondary);
@@ -3655,6 +3840,14 @@ header {
overflow-wrap: break-word;
}
.monitor-table th {
white-space: nowrap;
}
.monitor-table td {
white-space: nowrap;
}
.monitor-table td:nth-child(2) {
max-width: 250px;
overflow: hidden;
@@ -3754,14 +3947,31 @@ header {
width: 18px;
height: 18px;
accent-color: var(--accent-color);
margin: 0;
vertical-align: middle;
display: inline-block;
}
.monitor-table th:first-child,
.monitor-table td:first-child {
/* 确保复选框单元格垂直居中 */
.monitor-table td:first-child,
.monitor-table th:first-child {
text-align: center;
width: 40px;
vertical-align: middle;
}
/* 统一表头复选框大小 */
#monitor-select-all {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: var(--accent-color);
margin: 0;
vertical-align: middle;
display: inline-block;
}
/* 移除第一列的特殊样式,让表格更灵活 */
.monitor-vuln-container {
display: grid;
gap: 16px;
@@ -8784,3 +8994,321 @@ header {
right: 8px;
}
}
/* Skills选择相关样式 */
.role-skills-controls {
margin-bottom: 12px;
}
.role-skills-actions {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.role-skills-search-box {
position: relative;
flex: 1;
min-width: 200px;
max-width: 400px;
}
.role-skills-search-box input {
width: 100%;
padding: 8px 32px 8px 12px;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 0.875rem;
background: var(--bg-primary);
color: var(--text-primary);
transition: all 0.2s;
}
.role-skills-search-box input:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1);
}
.role-skills-search-clear {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
color: var(--text-secondary);
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s;
}
.role-skills-search-clear:hover {
color: var(--text-primary);
}
.role-skills-stats {
font-size: 0.8125rem;
color: var(--text-secondary);
margin-top: 8px;
}
.role-skills-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 8px;
background: var(--bg-primary);
}
.role-skill-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 10px;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--bg-primary);
transition: all 0.2s ease;
margin-bottom: 6px;
}
.role-skill-item:last-child {
margin-bottom: 0;
}
.role-skill-item:hover {
background: var(--bg-secondary);
border-color: var(--accent-color);
box-shadow: 0 2px 4px rgba(0, 102, 255, 0.1);
}
.role-skill-item .checkbox-text {
font-size: 0.9375rem;
color: var(--text-primary);
font-weight: 500;
}
.skills-loading,
.skills-empty,
.skills-error {
padding: 20px;
text-align: center;
color: var(--text-secondary);
font-size: 0.875rem;
}
.skills-error {
color: var(--error-color);
}
/* Skills管理页面样式 */
.skills-controls {
margin-bottom: 24px;
}
.skills-stats-bar {
display: flex;
gap: 16px;
padding: 16px;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 8px;
margin-bottom: 16px;
}
.skill-stat-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.skill-stat-label {
font-size: 0.8125rem;
color: var(--text-secondary);
}
.skill-stat-value {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
}
.skills-filters {
display: flex;
gap: 12px;
align-items: center;
}
.skills-filters input {
flex: 1;
padding: 8px 12px;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 0.875rem;
}
.skills-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.skill-item {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 16px;
transition: all 0.2s;
}
.skill-item:hover {
border-color: var(--accent-color);
box-shadow: 0 2px 8px rgba(0, 102, 255, 0.1);
}
.skill-item-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.skill-item-info {
flex: 1;
}
.skill-item-name {
font-size: 1.125rem;
font-weight: 600;
color: var(--text-primary);
margin: 0 0 8px 0;
}
.skill-item-desc {
font-size: 0.875rem;
color: var(--text-secondary);
margin: 0;
}
.skill-item-actions {
display: flex;
gap: 8px;
}
.btn-icon {
background: none;
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
transition: all 0.2s;
}
.btn-icon:hover {
background: var(--bg-secondary);
border-color: var(--accent-color);
color: var(--accent-color);
}
.btn-icon.btn-danger:hover {
background: rgba(220, 53, 69, 0.1);
border-color: #dc3545;
color: #dc3545;
}
.skill-item-meta {
display: flex;
gap: 16px;
flex-wrap: wrap;
font-size: 0.8125rem;
color: var(--text-secondary);
}
.skill-meta-item {
display: flex;
align-items: center;
}
/* Skills监控页面样式 */
.skills-monitor-controls {
margin-bottom: 24px;
}
.skills-monitor-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.skill-monitor-item {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 16px;
transition: all 0.2s;
}
.skill-monitor-item:hover {
border-color: var(--accent-color);
box-shadow: 0 2px 8px rgba(0, 102, 255, 0.1);
}
.skill-monitor-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.skill-monitor-info {
flex: 1;
}
.skill-monitor-name {
font-size: 1.125rem;
font-weight: 600;
color: var(--text-primary);
margin: 0 0 8px 0;
}
.skill-monitor-desc {
font-size: 0.875rem;
color: var(--text-secondary);
margin: 0;
}
.skill-monitor-status {
display: flex;
align-items: center;
}
.status-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
}
.status-success {
background: rgba(40, 167, 69, 0.1);
color: #28a745;
border: 1px solid rgba(40, 167, 69, 0.3);
}
.skill-monitor-meta {
display: flex;
gap: 16px;
flex-wrap: wrap;
font-size: 0.8125rem;
color: var(--text-secondary);
}
+78 -4
View File
@@ -860,6 +860,20 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
ALLOW_DATA_ATTR: false,
};
// HTML实体编码函数
const escapeHtml = (text) => {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
};
// 注意:代码块内容不需要转义,因为:
// 1. Markdown解析后,代码块会被包裹在<code>或<pre>标签中
// 2. 浏览器不会执行<code>和<pre>标签内的HTML(它们是文本节点)
// 3. DOMPurify会保留这些标签内的文本内容
// 这样既能防止XSS,又能正常显示代码
const parseMarkdown = (raw) => {
if (typeof marked === 'undefined') {
return null;
@@ -880,11 +894,47 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
if (role === 'user') {
formattedContent = escapeHtml(content).replace(/\n/g, '<br>');
} else if (typeof DOMPurify !== 'undefined') {
// 直接解析Markdown(代码块会被包裹在<code>/<pre>中,DOMPurify会保留其文本内容)
let parsedContent = parseMarkdown(content);
if (!parsedContent) {
// 如果 Markdown 解析失败或 marked 不可用,则退回原始内容
parsedContent = content;
}
// 使用DOMPurify清理,只添加必要的URL验证钩子(DOMPurify默认会处理事件处理器等)
if (DOMPurify.addHook) {
// 移除之前可能存在的钩子
try {
DOMPurify.removeHook('uponSanitizeAttribute');
} catch (e) {
// 钩子不存在,忽略
}
// 只验证URL属性,防止危险协议(DOMPurify默认会处理事件处理器、style等)
DOMPurify.addHook('uponSanitizeAttribute', (node, data) => {
const attrName = data.attrName.toLowerCase();
// 只验证URL属性(src, href
if ((attrName === 'src' || attrName === 'href') && data.attrValue) {
const value = data.attrValue.trim().toLowerCase();
// 禁止危险协议
if (value.startsWith('javascript:') ||
value.startsWith('vbscript:') ||
value.startsWith('data:text/html') ||
value.startsWith('data:text/javascript')) {
data.keepAttr = false;
return;
}
// 对于img的src,禁止可疑的短URL(防止404和XSS)
if (attrName === 'src' && node.tagName && node.tagName.toLowerCase() === 'img') {
if (value.length <= 2 || /^[a-z]$/i.test(value)) {
data.keepAttr = false;
return;
}
}
}
});
}
formattedContent = DOMPurify.sanitize(parsedContent, defaultSanitizeConfig);
} else if (typeof marked !== 'undefined') {
const parsedContent = parseMarkdown(content);
@@ -899,6 +949,22 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
bubble.innerHTML = formattedContent;
// 最后的安全检查:只处理明显的可疑图片(防止404和XSS)
// DOMPurify已经处理了大部分XSS向量,这里只做必要的补充
const images = bubble.querySelectorAll('img');
images.forEach(img => {
const src = img.getAttribute('src');
if (src) {
const trimmedSrc = src.trim();
// 只检查明显的可疑URL(短字符串、单个字符)
if (trimmedSrc.length <= 2 || /^[a-z]$/i.test(trimmedSrc)) {
img.remove();
}
} else {
img.remove();
}
});
// 为每个表格添加独立的滚动容器
wrapTablesInBubble(bubble);
@@ -1644,7 +1710,11 @@ function createConversationListItem(conversation) {
};
item.appendChild(deleteBtn);
item.onclick = () => loadConversation(conversation.id);
item.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
loadConversation(conversation.id);
};
return item;
}
@@ -3894,7 +3964,9 @@ function createConversationListItemWithMenu(conversation, isPinned) {
};
item.appendChild(menuBtn);
item.onclick = () => {
item.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
if (currentGroupId) {
exitGroupDetail();
}
@@ -5269,7 +5341,9 @@ async function loadGroupConversations(groupId, searchQuery = '') {
};
item.appendChild(menuBtn);
item.onclick = () => {
item.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
// 切换到对话界面,但保持分组详情状态
const groupDetailPage = document.getElementById('group-detail-page');
const chatContainer = document.querySelector('.chat-container');
+4 -4
View File
@@ -10,7 +10,7 @@ let knowledgePagination = {
total: 0,
currentCategory: ''
};
let searchTimeout = null; // 搜索防抖定时器
let knowledgeSearchTimeout = null; // 搜索防抖定时器
// 加载知识分类
async function loadKnowledgeCategories() {
@@ -639,8 +639,8 @@ function handleKnowledgeSearchInput() {
const searchTerm = searchInput?.value.trim() || '';
// 清除之前的定时器
if (searchTimeout) {
clearTimeout(searchTimeout);
if (knowledgeSearchTimeout) {
clearTimeout(knowledgeSearchTimeout);
}
// 如果搜索框为空,立即恢复列表
@@ -656,7 +656,7 @@ function handleKnowledgeSearchInput() {
}
// 有搜索词时,延迟500ms后执行搜索(防抖)
searchTimeout = setTimeout(() => {
knowledgeSearchTimeout = setTimeout(() => {
searchKnowledgeItems();
}, 500);
}
+187
View File
@@ -14,6 +14,11 @@ let roleUsesAllTools = false; // 标记角色是否使用所有工具(当没
let totalEnabledToolsInMCP = 0; // 已启用的工具总数(从MCP管理中获取,从API响应中获取)
let roleConfiguredTools = new Set(); // 角色配置的工具列表(用于确定哪些工具应该被选中)
// Skills相关
let allRoleSkills = []; // 存储所有skills列表
let roleSkillsSearchKeyword = ''; // Skills搜索关键词
let roleSelectedSkills = new Set(); // 选中的skills集合
// 对角色列表进行排序:默认角色排在第一个,其他按名称排序
function sortRoles(rolesArray) {
const sortedRoles = [...rolesArray];
@@ -834,6 +839,18 @@ async function showAddRoleModal() {
toolsList.innerHTML = '';
}
// 重置skills状态
roleSelectedSkills.clear();
roleSkillsSearchKeyword = '';
const skillsSearchInput = document.getElementById('role-skills-search');
if (skillsSearchInput) {
skillsSearchInput.value = '';
}
const skillsClearBtn = document.getElementById('role-skills-search-clear');
if (skillsClearBtn) {
skillsClearBtn.style.display = 'none';
}
// 加载并渲染工具列表
await loadRoleTools(1, '');
@@ -845,6 +862,9 @@ async function showAddRoleModal() {
// 确保统计信息正确更新(显示0/108)
updateRoleToolsStats();
// 加载并渲染skills列表
await loadRoleSkills();
modal.style.display = 'flex';
}
@@ -1004,6 +1024,16 @@ async function editRole(roleName) {
}
}
// 加载并设置skills
await loadRoleSkills();
// 设置角色配置的skills
const selectedSkills = role.skills || [];
roleSelectedSkills.clear();
selectedSkills.forEach(skill => {
roleSelectedSkills.add(skill);
});
renderRoleSkills();
modal.style.display = 'flex';
}
@@ -1227,12 +1257,16 @@ async function saveRole() {
}
}
// 获取选中的skills
const skills = Array.from(roleSelectedSkills);
const roleData = {
name: name,
description: description,
icon: icon || undefined, // 如果为空字符串,则不发送该字段
user_prompt: userPrompt,
tools: tools, // 默认角色为空数组,表示使用所有工具
skills: skills, // Skills列表
enabled: enabled
};
const url = isEdit ? `/api/roles/${encodeURIComponent(name)}` : '/api/roles';
@@ -1372,3 +1406,156 @@ if (typeof window !== 'undefined') {
};
}
// ==================== Skills相关函数 ====================
// 加载skills列表
async function loadRoleSkills() {
try {
const response = await apiFetch('/api/roles/skills/list');
if (!response.ok) {
throw new Error('加载skills列表失败');
}
const data = await response.json();
allRoleSkills = data.skills || [];
renderRoleSkills();
} catch (error) {
console.error('加载skills列表失败:', error);
allRoleSkills = [];
const skillsList = document.getElementById('role-skills-list');
if (skillsList) {
skillsList.innerHTML = '<div class="skills-error">加载skills列表失败: ' + error.message + '</div>';
}
}
}
// 渲染skills列表
function renderRoleSkills() {
const skillsList = document.getElementById('role-skills-list');
if (!skillsList) return;
// 过滤skills
let filteredSkills = allRoleSkills;
if (roleSkillsSearchKeyword) {
const keyword = roleSkillsSearchKeyword.toLowerCase();
filteredSkills = allRoleSkills.filter(skill =>
skill.toLowerCase().includes(keyword)
);
}
if (filteredSkills.length === 0) {
skillsList.innerHTML = '<div class="skills-empty">' +
(roleSkillsSearchKeyword ? '没有找到匹配的skills' : '暂无可用skills') +
'</div>';
updateRoleSkillsStats();
return;
}
// 渲染skills列表
skillsList.innerHTML = filteredSkills.map(skill => {
const isSelected = roleSelectedSkills.has(skill);
return `
<div class="role-skill-item" data-skill="${skill}">
<label class="checkbox-label">
<input type="checkbox" class="modern-checkbox"
${isSelected ? 'checked' : ''}
onchange="toggleRoleSkill('${skill}', this.checked)" />
<span class="checkbox-custom"></span>
<span class="checkbox-text">${escapeHtml(skill)}</span>
</label>
</div>
`;
}).join('');
updateRoleSkillsStats();
}
// 切换skill选中状态
function toggleRoleSkill(skill, checked) {
if (checked) {
roleSelectedSkills.add(skill);
} else {
roleSelectedSkills.delete(skill);
}
updateRoleSkillsStats();
}
// 全选skills
function selectAllRoleSkills() {
let filteredSkills = allRoleSkills;
if (roleSkillsSearchKeyword) {
const keyword = roleSkillsSearchKeyword.toLowerCase();
filteredSkills = allRoleSkills.filter(skill =>
skill.toLowerCase().includes(keyword)
);
}
filteredSkills.forEach(skill => {
roleSelectedSkills.add(skill);
});
renderRoleSkills();
}
// 全不选skills
function deselectAllRoleSkills() {
let filteredSkills = allRoleSkills;
if (roleSkillsSearchKeyword) {
const keyword = roleSkillsSearchKeyword.toLowerCase();
filteredSkills = allRoleSkills.filter(skill =>
skill.toLowerCase().includes(keyword)
);
}
filteredSkills.forEach(skill => {
roleSelectedSkills.delete(skill);
});
renderRoleSkills();
}
// 搜索skills
function searchRoleSkills(keyword) {
roleSkillsSearchKeyword = keyword;
const clearBtn = document.getElementById('role-skills-search-clear');
if (clearBtn) {
clearBtn.style.display = keyword ? 'block' : 'none';
}
renderRoleSkills();
}
// 清除skills搜索
function clearRoleSkillsSearch() {
const searchInput = document.getElementById('role-skills-search');
if (searchInput) {
searchInput.value = '';
}
roleSkillsSearchKeyword = '';
const clearBtn = document.getElementById('role-skills-search-clear');
if (clearBtn) {
clearBtn.style.display = 'none';
}
renderRoleSkills();
}
// 更新skills统计信息
function updateRoleSkillsStats() {
const statsEl = document.getElementById('role-skills-stats');
if (!statsEl) return;
let filteredSkills = allRoleSkills;
if (roleSkillsSearchKeyword) {
const keyword = roleSkillsSearchKeyword.toLowerCase();
filteredSkills = allRoleSkills.filter(skill =>
skill.toLowerCase().includes(keyword)
);
}
const selectedCount = Array.from(roleSelectedSkills).filter(skill =>
filteredSkills.includes(skill)
).length;
statsEl.textContent = `已选择 ${selectedCount} / ${filteredSkills.length}`;
}
// HTML转义函数
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
+43 -2
View File
@@ -8,7 +8,7 @@ function initRouter() {
if (hash) {
const hashParts = hash.split('?');
const pageId = hashParts[0];
if (pageId && ['chat', 'vulnerabilities', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'settings', 'tasks'].includes(pageId)) {
if (pageId && ['chat', 'vulnerabilities', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'settings', 'tasks'].includes(pageId)) {
switchPage(pageId);
// 如果是chat页面且带有conversation参数,加载对应对话
@@ -94,6 +94,19 @@ function updateNavState(pageId) {
knowledgeItem.classList.add('expanded');
}
const submenuItem = document.querySelector(`.nav-submenu-item[data-page="${pageId}"]`);
if (submenuItem) {
submenuItem.classList.add('active');
}
} else if (pageId === 'skills-monitor' || pageId === 'skills-management') {
// Skills子菜单项
const skillsItem = document.querySelector('.nav-item[data-page="skills"]');
if (skillsItem) {
skillsItem.classList.add('active');
// 展开Skills子菜单
skillsItem.classList.add('expanded');
}
const submenuItem = document.querySelector(`.nav-submenu-item[data-page="${pageId}"]`);
if (submenuItem) {
submenuItem.classList.add('active');
@@ -107,6 +120,19 @@ function updateNavState(pageId) {
rolesItem.classList.add('expanded');
}
const submenuItem = document.querySelector(`.nav-submenu-item[data-page="${pageId}"]`);
if (submenuItem) {
submenuItem.classList.add('active');
}
} else if (pageId === 'skills-monitor' || pageId === 'skills-management') {
// Skills子菜单项
const skillsItem = document.querySelector('.nav-item[data-page="skills"]');
if (skillsItem) {
skillsItem.classList.add('active');
// 展开Skills子菜单
skillsItem.classList.add('expanded');
}
const submenuItem = document.querySelector(`.nav-submenu-item[data-page="${pageId}"]`);
if (submenuItem) {
submenuItem.classList.add('active');
@@ -262,6 +288,21 @@ function initPage(pageId) {
});
}
break;
case 'skills-monitor':
// 初始化Skills状态监控页面
if (typeof loadSkillsMonitor === 'function') {
loadSkillsMonitor();
}
break;
case 'skills-management':
// 初始化Skills管理页面
if (typeof initSkillsPagination === 'function') {
initSkillsPagination();
}
if (typeof loadSkills === 'function') {
loadSkills();
}
break;
}
// 清理其他页面的定时器
@@ -282,7 +323,7 @@ document.addEventListener('DOMContentLoaded', function() {
const hashParts = hash.split('?');
const pageId = hashParts[0];
if (pageId && ['chat', 'tasks', 'vulnerabilities', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'settings'].includes(pageId)) {
if (pageId && ['chat', 'tasks', 'vulnerabilities', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'settings'].includes(pageId)) {
switchPage(pageId);
// 如果是chat页面且带有conversation参数,加载对应对话
+716
View File
@@ -0,0 +1,716 @@
// Skills管理相关功能
let skillsList = [];
let currentEditingSkillName = null;
let isSavingSkill = false; // 防止重复提交
let skillsSearchKeyword = '';
let skillsSearchTimeout = null; // 搜索防抖定时器
let skillsPagination = {
currentPage: 1,
pageSize: 20, // 每页20条(默认值,实际从localStorage读取)
total: 0
};
let skillsStats = {
total: 0,
totalCalls: 0,
totalSuccess: 0,
totalFailed: 0,
skillsDir: '',
stats: []
};
// 获取保存的每页显示数量
function getSkillsPageSize() {
try {
const saved = localStorage.getItem('skillsPageSize');
if (saved) {
const size = parseInt(saved);
if ([10, 20, 50, 100].includes(size)) {
return size;
}
}
} catch (e) {
console.warn('无法从localStorage读取分页设置:', e);
}
return 20; // 默认20
}
// 初始化分页设置
function initSkillsPagination() {
const savedPageSize = getSkillsPageSize();
skillsPagination.pageSize = savedPageSize;
}
// 加载skills列表(支持分页)
async function loadSkills(page = 1, pageSize = null) {
try {
// 如果没有指定pageSize,使用保存的值或默认值
if (pageSize === null) {
pageSize = getSkillsPageSize();
}
// 更新分页状态(确保使用正确的pageSize)
skillsPagination.currentPage = page;
skillsPagination.pageSize = pageSize;
// 清空搜索关键词(正常分页加载时)
skillsSearchKeyword = '';
const searchInput = document.getElementById('skills-search');
if (searchInput) {
searchInput.value = '';
}
// 构建URL(支持分页)
const offset = (page - 1) * pageSize;
const url = `/api/skills?limit=${pageSize}&offset=${offset}`;
const response = await apiFetch(url);
if (!response.ok) {
throw new Error('获取skills列表失败');
}
const data = await response.json();
skillsList = data.skills || [];
skillsPagination.total = data.total || 0;
renderSkillsList();
renderSkillsPagination();
updateSkillsManagementStats();
} catch (error) {
console.error('加载skills列表失败:', error);
showNotification('加载skills列表失败: ' + error.message, 'error');
const skillsListEl = document.getElementById('skills-list');
if (skillsListEl) {
skillsListEl.innerHTML = '<div class="empty-state">加载失败: ' + error.message + '</div>';
}
}
}
// 渲染skills列表
function renderSkillsList() {
const skillsListEl = document.getElementById('skills-list');
if (!skillsListEl) return;
// 后端已经完成搜索过滤,直接使用skillsList
const filteredSkills = skillsList;
if (filteredSkills.length === 0) {
skillsListEl.innerHTML = '<div class="empty-state">' +
(skillsSearchKeyword ? '没有找到匹配的skills' : '暂无skills,点击"添加Skill"创建第一个skill') +
'</div>';
// 搜索时隐藏分页
const paginationContainer = document.getElementById('skills-pagination');
if (paginationContainer) {
paginationContainer.innerHTML = '';
}
return;
}
skillsListEl.innerHTML = filteredSkills.map(skill => {
const fileSize = skill.file_size || 0;
const fileSizeStr = fileSize < 1024 ? fileSize + ' B' :
fileSize < 1024 * 1024 ? (fileSize / 1024).toFixed(2) + ' KB' :
(fileSize / (1024 * 1024)).toFixed(2) + ' MB';
return `
<div class="skill-item">
<div class="skill-item-header">
<div class="skill-item-info">
<h3 class="skill-item-name">${escapeHtml(skill.name || '')}</h3>
${skill.description ? `<p class="skill-item-desc">${escapeHtml(skill.description)}</p>` : ''}
</div>
<div class="skill-item-actions">
<button class="btn-icon" onclick="viewSkill('${escapeHtml(skill.name)}')" title="查看">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</button>
<button class="btn-icon" onclick="editSkill('${escapeHtml(skill.name)}')" title="编辑">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
</button>
<button class="btn-icon btn-danger" onclick="deleteSkill('${escapeHtml(skill.name)}')" title="删除">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
</div>
</div>
<div class="skill-item-meta">
<span class="skill-meta-item">路径: ${escapeHtml(skill.path || '')}</span>
<span class="skill-meta-item">大小: ${fileSizeStr}</span>
${skill.mod_time ? `<span class="skill-meta-item">修改时间: ${escapeHtml(skill.mod_time)}</span>` : ''}
</div>
</div>
`;
}).join('');
}
// 渲染分页组件(参考MCP管理页面样式)
function renderSkillsPagination() {
const paginationContainer = document.getElementById('skills-pagination');
if (!paginationContainer) return;
const total = skillsPagination.total;
const pageSize = skillsPagination.pageSize;
const currentPage = skillsPagination.currentPage;
const totalPages = Math.ceil(total / pageSize);
// 即使只有一页也显示分页信息(参考MCP样式)
if (total === 0) {
paginationContainer.innerHTML = '';
return;
}
// 计算显示范围
const start = total === 0 ? 0 : (currentPage - 1) * pageSize + 1;
const end = total === 0 ? 0 : Math.min(currentPage * pageSize, total);
let paginationHTML = '<div class="pagination">';
// 左侧:显示范围信息和每页数量选择器(参考MCP样式)
paginationHTML += `
<div class="pagination-info">
<span>显示 ${start}-${end} / ${total} </span>
<label class="pagination-page-size">
每页显示
<select id="skills-page-size-pagination" onchange="changeSkillsPageSize()">
<option value="10" ${pageSize === 10 ? 'selected' : ''}>10</option>
<option value="20" ${pageSize === 20 ? 'selected' : ''}>20</option>
<option value="50" ${pageSize === 50 ? 'selected' : ''}>50</option>
<option value="100" ${pageSize === 100 ? 'selected' : ''}>100</option>
</select>
</label>
</div>
`;
// 右侧:分页按钮(参考MCP样式:首页、上一页、第X/Y页、下一页、末页)
paginationHTML += `
<div class="pagination-controls">
<button class="btn-secondary" onclick="loadSkills(1, ${pageSize})" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>首页</button>
<button class="btn-secondary" onclick="loadSkills(${currentPage - 1}, ${pageSize})" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>上一页</button>
<span class="pagination-page"> ${currentPage} / ${totalPages || 1} </span>
<button class="btn-secondary" onclick="loadSkills(${currentPage + 1}, ${pageSize})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>下一页</button>
<button class="btn-secondary" onclick="loadSkills(${totalPages || 1}, ${pageSize})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>末页</button>
</div>
`;
paginationHTML += '</div>';
paginationContainer.innerHTML = paginationHTML;
// 确保分页组件与列表内容区域对齐(不包括滚动条)
function alignPaginationWidth() {
const skillsList = document.getElementById('skills-list');
if (skillsList && paginationContainer) {
// 获取列表的实际内容宽度(不包括滚动条)
const listClientWidth = skillsList.clientWidth; // 可视区域宽度(不包括滚动条)
const listScrollHeight = skillsList.scrollHeight; // 内容总高度
const listClientHeight = skillsList.clientHeight; // 可视区域高度
const hasScrollbar = listScrollHeight > listClientHeight;
// 如果列表有垂直滚动条,分页组件应该与列表内容区域对齐(clientWidth
// 如果没有滚动条,使用100%宽度
if (hasScrollbar) {
// 分页组件应该与列表内容区域对齐,不包括滚动条
paginationContainer.style.width = `${listClientWidth}px`;
} else {
// 如果没有滚动条,使用100%宽度
paginationContainer.style.width = '100%';
}
}
}
// 立即执行一次
alignPaginationWidth();
// 监听窗口大小变化和列表内容变化
const resizeObserver = new ResizeObserver(() => {
alignPaginationWidth();
});
const skillsList = document.getElementById('skills-list');
if (skillsList) {
resizeObserver.observe(skillsList);
}
}
// 改变每页显示数量
async function changeSkillsPageSize() {
const pageSizeSelect = document.getElementById('skills-page-size-pagination');
if (!pageSizeSelect) return;
const newPageSize = parseInt(pageSizeSelect.value);
if (isNaN(newPageSize) || newPageSize <= 0) return;
// 保存到localStorage
try {
localStorage.setItem('skillsPageSize', newPageSize.toString());
} catch (e) {
console.warn('无法保存分页设置到localStorage:', e);
}
// 更新分页状态
skillsPagination.pageSize = newPageSize;
// 重新计算当前页(确保不超出范围)
const totalPages = Math.ceil(skillsPagination.total / newPageSize);
const currentPage = Math.min(skillsPagination.currentPage, totalPages || 1);
skillsPagination.currentPage = currentPage;
// 重新加载数据
await loadSkills(currentPage, newPageSize);
}
// 更新skills管理统计信息
function updateSkillsManagementStats() {
const statsEl = document.getElementById('skills-management-stats');
if (!statsEl) return;
const totalEl = statsEl.querySelector('.skill-stat-value');
if (totalEl) {
totalEl.textContent = skillsPagination.total;
}
}
// 搜索skills
function handleSkillsSearchInput() {
clearTimeout(skillsSearchTimeout);
skillsSearchTimeout = setTimeout(() => {
searchSkills();
}, 300);
}
async function searchSkills() {
const searchInput = document.getElementById('skills-search');
if (!searchInput) return;
skillsSearchKeyword = searchInput.value.trim();
if (skillsSearchKeyword) {
// 有搜索关键词时,使用后端搜索API(加载所有匹配结果,不分页)
try {
const response = await apiFetch(`/api/skills?search=${encodeURIComponent(skillsSearchKeyword)}&limit=10000&offset=0`);
if (!response.ok) {
throw new Error('获取skills列表失败');
}
const data = await response.json();
skillsList = data.skills || [];
skillsPagination.total = data.total || 0;
renderSkillsList();
// 搜索时隐藏分页
const paginationContainer = document.getElementById('skills-pagination');
if (paginationContainer) {
paginationContainer.innerHTML = '';
}
// 更新统计信息(显示搜索结果数量)
updateSkillsManagementStats();
} catch (error) {
console.error('搜索skills失败:', error);
showNotification('搜索失败: ' + error.message, 'error');
}
} else {
// 没有搜索关键词时,恢复分页加载
await loadSkills(1, skillsPagination.pageSize);
}
}
// 刷新skills
async function refreshSkills() {
await loadSkills(skillsPagination.currentPage, skillsPagination.pageSize);
showNotification('已刷新', 'success');
}
// 显示添加skill模态框
function showAddSkillModal() {
const modal = document.getElementById('skill-modal');
if (!modal) return;
document.getElementById('skill-modal-title').textContent = '添加Skill';
document.getElementById('skill-name').value = '';
document.getElementById('skill-name').disabled = false;
document.getElementById('skill-description').value = '';
document.getElementById('skill-content').value = '';
modal.style.display = 'flex';
}
// 编辑skill
async function editSkill(skillName) {
try {
const response = await apiFetch(`/api/skills/${encodeURIComponent(skillName)}`);
if (!response.ok) {
throw new Error('获取skill详情失败');
}
const data = await response.json();
const skill = data.skill;
const modal = document.getElementById('skill-modal');
if (!modal) return;
document.getElementById('skill-modal-title').textContent = '编辑Skill';
document.getElementById('skill-name').value = skill.name;
document.getElementById('skill-name').disabled = true; // 编辑时不允许修改名称
document.getElementById('skill-description').value = skill.description || '';
document.getElementById('skill-content').value = skill.content || '';
currentEditingSkillName = skillName;
modal.style.display = 'flex';
} catch (error) {
console.error('加载skill详情失败:', error);
showNotification('加载skill详情失败: ' + error.message, 'error');
}
}
// 查看skill
async function viewSkill(skillName) {
try {
const response = await apiFetch(`/api/skills/${encodeURIComponent(skillName)}`);
if (!response.ok) {
throw new Error('获取skill详情失败');
}
const data = await response.json();
const skill = data.skill;
// 创建查看模态框
const modal = document.createElement('div');
modal.className = 'modal';
modal.id = 'skill-view-modal';
modal.innerHTML = `
<div class="modal-content" style="max-width: 900px; max-height: 90vh;">
<div class="modal-header">
<h2>查看Skill: ${escapeHtml(skill.name)}</h2>
<span class="modal-close" onclick="closeSkillViewModal()">&times;</span>
</div>
<div class="modal-body" style="overflow-y: auto; max-height: calc(90vh - 120px);">
${skill.description ? `<div style="margin-bottom: 16px;"><strong>描述:</strong> ${escapeHtml(skill.description)}</div>` : ''}
<div style="margin-bottom: 8px;"><strong>路径:</strong> ${escapeHtml(skill.path || '')}</div>
<div style="margin-bottom: 16px;"><strong>修改时间:</strong> ${escapeHtml(skill.mod_time || '')}</div>
<div style="margin-bottom: 8px;"><strong>内容:</strong></div>
<pre style="background: #f5f5f5; padding: 16px; border-radius: 4px; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;">${escapeHtml(skill.content || '')}</pre>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeSkillViewModal()">关闭</button>
<button class="btn-primary" onclick="editSkill('${escapeHtml(skill.name)}'); closeSkillViewModal();">编辑</button>
</div>
</div>
`;
document.body.appendChild(modal);
modal.style.display = 'flex';
} catch (error) {
console.error('查看skill失败:', error);
showNotification('查看skill失败: ' + error.message, 'error');
}
}
// 关闭查看模态框
function closeSkillViewModal() {
const modal = document.getElementById('skill-view-modal');
if (modal) {
modal.remove();
}
}
// 关闭skill模态框
function closeSkillModal() {
const modal = document.getElementById('skill-modal');
if (modal) {
modal.style.display = 'none';
currentEditingSkillName = null;
}
}
// 保存skill
async function saveSkill() {
if (isSavingSkill) return;
const name = document.getElementById('skill-name').value.trim();
const description = document.getElementById('skill-description').value.trim();
const content = document.getElementById('skill-content').value.trim();
if (!name) {
showNotification('skill名称不能为空', 'error');
return;
}
if (!content) {
showNotification('skill内容不能为空', 'error');
return;
}
// 验证skill名称
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
showNotification('skill名称只能包含字母、数字、连字符和下划线', 'error');
return;
}
isSavingSkill = true;
const saveBtn = document.querySelector('#skill-modal .btn-primary');
if (saveBtn) {
saveBtn.disabled = true;
saveBtn.textContent = '保存中...';
}
try {
const isEdit = !!currentEditingSkillName;
const url = isEdit ? `/api/skills/${encodeURIComponent(currentEditingSkillName)}` : '/api/skills';
const method = isEdit ? 'PUT' : 'POST';
const response = await apiFetch(url, {
method: method,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: name,
description: description,
content: content
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || '保存skill失败');
}
showNotification(isEdit ? 'skill已更新' : 'skill已创建', 'success');
closeSkillModal();
await loadSkills(skillsPagination.currentPage, skillsPagination.pageSize);
} catch (error) {
console.error('保存skill失败:', error);
showNotification('保存skill失败: ' + error.message, 'error');
} finally {
isSavingSkill = false;
if (saveBtn) {
saveBtn.disabled = false;
saveBtn.textContent = '保存';
}
}
}
// 删除skill
async function deleteSkill(skillName) {
// 先检查是否有角色绑定了该skill
let boundRoles = [];
try {
const checkResponse = await apiFetch(`/api/skills/${encodeURIComponent(skillName)}/bound-roles`);
if (checkResponse.ok) {
const checkData = await checkResponse.json();
boundRoles = checkData.bound_roles || [];
}
} catch (error) {
console.warn('检查skill绑定失败:', error);
// 如果检查失败,继续执行删除流程
}
// 构建确认消息
let confirmMessage = `确定要删除skill "${skillName}" 吗?此操作不可恢复。`;
if (boundRoles.length > 0) {
const rolesList = boundRoles.join('、');
confirmMessage = `确定要删除skill "${skillName}" 吗?\n\n⚠️ 该skill当前已被以下 ${boundRoles.length} 个角色绑定:\n${rolesList}\n\n删除后,系统将自动从这些角色中移除该skill的绑定。\n\n此操作不可恢复,是否继续?`;
}
if (!confirm(confirmMessage)) {
return;
}
try {
const response = await apiFetch(`/api/skills/${encodeURIComponent(skillName)}`, {
method: 'DELETE'
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || '删除skill失败');
}
const data = await response.json();
let successMessage = 'skill已删除';
if (data.affected_roles && data.affected_roles.length > 0) {
const rolesList = data.affected_roles.join('、');
successMessage = `skill已删除,已自动从 ${data.affected_roles.length} 个角色中移除绑定:${rolesList}`;
}
showNotification(successMessage, 'success');
// 如果当前页没有数据了,回到上一页
const currentPage = skillsPagination.currentPage;
const totalAfterDelete = skillsPagination.total - 1;
const totalPages = Math.ceil(totalAfterDelete / skillsPagination.pageSize);
const pageToLoad = currentPage > totalPages && totalPages > 0 ? totalPages : currentPage;
await loadSkills(pageToLoad, skillsPagination.pageSize);
} catch (error) {
console.error('删除skill失败:', error);
showNotification('删除skill失败: ' + error.message, 'error');
}
}
// ==================== Skills状态监控相关函数 ====================
// 加载skills监控数据
async function loadSkillsMonitor() {
try {
const response = await apiFetch('/api/skills/stats');
if (!response.ok) {
throw new Error('获取skills统计信息失败');
}
const data = await response.json();
skillsStats = {
total: data.total_skills || 0,
totalCalls: data.total_calls || 0,
totalSuccess: data.total_success || 0,
totalFailed: data.total_failed || 0,
skillsDir: data.skills_dir || '',
stats: data.stats || []
};
renderSkillsMonitor();
} catch (error) {
console.error('加载skills监控数据失败:', error);
showNotification('加载skills监控数据失败: ' + error.message, 'error');
const statsEl = document.getElementById('skills-stats');
if (statsEl) {
statsEl.innerHTML = '<div class="monitor-error">无法加载统计信息:' + escapeHtml(error.message) + '</div>';
}
const monitorListEl = document.getElementById('skills-monitor-list');
if (monitorListEl) {
monitorListEl.innerHTML = '<div class="monitor-error">无法加载调用统计:' + escapeHtml(error.message) + '</div>';
}
}
}
// 渲染skills监控页面
function renderSkillsMonitor() {
// 渲染总体统计
const statsEl = document.getElementById('skills-stats');
if (statsEl) {
const successRate = skillsStats.totalCalls > 0
? ((skillsStats.totalSuccess / skillsStats.totalCalls) * 100).toFixed(1)
: '0.0';
statsEl.innerHTML = `
<div class="monitor-stat-card">
<div class="monitor-stat-label">总Skills数</div>
<div class="monitor-stat-value">${skillsStats.total}</div>
</div>
<div class="monitor-stat-card">
<div class="monitor-stat-label">总调用次数</div>
<div class="monitor-stat-value">${skillsStats.totalCalls}</div>
</div>
<div class="monitor-stat-card">
<div class="monitor-stat-label">成功调用</div>
<div class="monitor-stat-value" style="color: #28a745;">${skillsStats.totalSuccess}</div>
</div>
<div class="monitor-stat-card">
<div class="monitor-stat-label">失败调用</div>
<div class="monitor-stat-value" style="color: #dc3545;">${skillsStats.totalFailed}</div>
</div>
<div class="monitor-stat-card">
<div class="monitor-stat-label">成功率</div>
<div class="monitor-stat-value">${successRate}%</div>
</div>
<div class="monitor-stat-card">
<div class="monitor-stat-label">Skills目录</div>
<div class="monitor-stat-value" style="font-size: 0.875rem;">${escapeHtml(skillsStats.skillsDir || '-')}</div>
</div>
`;
}
// 渲染调用统计表格
const monitorListEl = document.getElementById('skills-monitor-list');
if (!monitorListEl) return;
const stats = skillsStats.stats || [];
// 如果没有统计数据,显示空状态
if (stats.length === 0) {
monitorListEl.innerHTML = '<div class="monitor-empty">暂无Skills调用记录</div>';
return;
}
// 按调用次数排序(降序),如果调用次数相同,按名称排序
const sortedStats = [...stats].sort((a, b) => {
const callsA = b.total_calls || 0;
const callsB = a.total_calls || 0;
if (callsA !== callsB) {
return callsA - callsB;
}
return (a.skill_name || '').localeCompare(b.skill_name || '');
});
monitorListEl.innerHTML = `
<table class="monitor-table">
<thead>
<tr>
<th style="text-align: left !important;">Skill名称</th>
<th style="text-align: center;">总调用</th>
<th style="text-align: center;">成功</th>
<th style="text-align: center;">失败</th>
<th style="text-align: center;">成功率</th>
<th style="text-align: left;">最后调用时间</th>
</tr>
</thead>
<tbody>
${sortedStats.map(stat => {
const totalCalls = stat.total_calls || 0;
const successCalls = stat.success_calls || 0;
const failedCalls = stat.failed_calls || 0;
const successRate = totalCalls > 0 ? ((successCalls / totalCalls) * 100).toFixed(1) : '0.0';
const lastCallTime = stat.last_call_time && stat.last_call_time !== '-' ? stat.last_call_time : '-';
return `
<tr>
<td style="text-align: left !important;"><strong>${escapeHtml(stat.skill_name || '')}</strong></td>
<td style="text-align: center;">${totalCalls}</td>
<td style="text-align: center; color: #28a745; font-weight: 500;">${successCalls}</td>
<td style="text-align: center; color: #dc3545; font-weight: 500;">${failedCalls}</td>
<td style="text-align: center;">${successRate}%</td>
<td style="color: var(--text-secondary);">${escapeHtml(lastCallTime)}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
`;
}
// 刷新skills监控
async function refreshSkillsMonitor() {
await loadSkillsMonitor();
showNotification('已刷新', 'success');
}
// 清空skills统计数据
async function clearSkillsStats() {
if (!confirm('确定要清空所有Skills统计数据吗?此操作不可恢复。')) {
return;
}
try {
const response = await apiFetch('/api/skills/stats', {
method: 'DELETE'
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || '清空统计数据失败');
}
showNotification('已清空所有Skills统计数据', 'success');
// 重新加载统计数据
await loadSkillsMonitor();
} catch (error) {
console.error('清空统计数据失败:', error);
showNotification('清空统计数据失败: ' + error.message, 'error');
}
}
// HTML转义函数
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
+149
View File
@@ -133,6 +133,29 @@
</div>
</div>
</div>
<div class="nav-item nav-item-has-submenu" data-page="skills">
<div class="nav-item-content" data-title="Skills" onclick="toggleSubmenu('skills')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
</svg>
<span>Skills</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="skills-monitor" onclick="switchPage('skills-monitor')">
<span>Skills状态监控</span>
</div>
<div class="nav-submenu-item" data-page="skills-management" onclick="switchPage('skills-management')">
<span>Skills管理</span>
</div>
</div>
</div>
<div class="nav-item nav-item-has-submenu" data-page="roles">
<div class="nav-item-content" data-title="角色" onclick="toggleSubmenu('roles')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -675,6 +698,68 @@
</div>
</div>
<!-- Skills状态监控页面 -->
<div id="page-skills-monitor" class="page">
<div class="page-header">
<h2>Skills状态监控</h2>
<div class="page-header-actions">
<button class="btn-secondary" onclick="refreshSkillsMonitor()">刷新</button>
</div>
</div>
<div class="page-content">
<div class="monitor-sections">
<section class="monitor-section monitor-overview">
<div class="section-header">
<h3>调用统计</h3>
</div>
<div id="skills-stats" class="monitor-stats-grid">
<div class="monitor-empty">加载中...</div>
</div>
</section>
<section class="monitor-section monitor-executions">
<div class="section-header">
<h3>Skills调用统计</h3>
<div class="section-actions">
<button class="btn-secondary btn-small" onclick="clearSkillsStats()" title="清空所有统计数据">清空统计</button>
</div>
</div>
<div id="skills-monitor-list" class="monitor-table-container">
<div class="monitor-empty">加载中...</div>
</div>
</section>
</div>
</div>
</div>
<!-- Skills管理页面 -->
<div id="page-skills-management" class="page">
<div class="page-header">
<h2>Skills管理</h2>
<div class="page-header-actions">
<button class="btn-secondary" onclick="refreshSkills()">刷新</button>
<button class="btn-primary" onclick="showAddSkillModal()">添加Skill</button>
</div>
</div>
<div class="page-content page-content-with-pagination">
<div class="skills-controls">
<div class="skills-stats-bar" id="skills-management-stats">
<div class="skill-stat-item">
<span class="skill-stat-label">总Skills数</span>
<span class="skill-stat-value">-</span>
</div>
</div>
<div class="skills-filters">
<input type="text" id="skills-search" placeholder="搜索skill..." oninput="handleSkillsSearchInput()" onkeydown="if(event.key==='Enter') searchSkills()" />
<button class="btn-search" onclick="searchSkills()" title="搜索">🔍</button>
</div>
</div>
<div id="skills-list" class="skills-list skills-list-with-pagination">
<div class="loading-spinner">加载中...</div>
</div>
<div id="skills-pagination" class="pagination-container pagination-fixed"></div>
</div>
</div>
<!-- 系统设置页面 -->
<div id="page-settings" class="page">
<div class="page-header">
@@ -1084,6 +1169,43 @@
}
</script>
<!-- 知识项编辑模态框 -->
<!-- Skill模态框 -->
<div id="skill-modal" class="modal">
<div class="modal-content" style="max-width: 900px;">
<div class="modal-header">
<h2 id="skill-modal-title">添加Skill</h2>
<span class="modal-close" onclick="closeSkillModal()">&times;</span>
</div>
<div class="modal-body">
<div class="form-group">
<label for="skill-name">Skill名称 <span style="color: red;">*</span></label>
<input type="text" id="skill-name" placeholder="例如: sql-injection-testing" required />
<small class="form-hint">只能包含字母、数字、连字符和下划线</small>
</div>
<div class="form-group">
<label for="skill-description">描述</label>
<input type="text" id="skill-description" placeholder="Skill的简短描述" />
</div>
<div class="form-group">
<label for="skill-content">内容(Markdown格式) <span style="color: red;">*</span></label>
<textarea id="skill-content" rows="20" placeholder="输入skill内容,支持Markdown格式..." style="font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.875rem; line-height: 1.5;" required></textarea>
<small class="form-hint">支持YAML front matter格式(可选),例如:<br>
---<br>
name: skill-name<br>
description: Skill描述<br>
version: 1.0.0<br>
---<br><br>
# Skill标题<br>
这里是skill内容...</small>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeSkillModal()">取消</button>
<button class="btn-primary" onclick="saveSkill()">保存</button>
</div>
</div>
</div>
<div id="knowledge-item-modal" class="modal">
<div class="modal-content" style="max-width: 900px;">
<div class="modal-header">
@@ -1492,6 +1614,32 @@
</div>
<small class="form-hint">勾选要关联的工具,留空则使用MCP管理中的全部工具配置。</small>
</div>
<div class="form-group" id="role-skills-section">
<label>关联的Skills(可选)</label>
<div class="role-skills-controls">
<div class="role-skills-actions">
<button type="button" class="btn-secondary" onclick="selectAllRoleSkills()">全选</button>
<button type="button" class="btn-secondary" onclick="deselectAllRoleSkills()">全不选</button>
<div class="role-skills-search-box">
<input type="text" id="role-skills-search" placeholder="搜索skill..."
oninput="searchRoleSkills(this.value)"
onkeypress="if(event.key === 'Enter') searchRoleSkills(this.value)" />
<button class="role-skills-search-clear" id="role-skills-search-clear"
onclick="clearRoleSkillsSearch()" style="display: none;" title="清除搜索">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
<path d="M15 9l-6 6M9 9l6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
</div>
</div>
<div id="role-skills-stats" class="role-skills-stats"></div>
</div>
<div id="role-skills-list" class="role-skills-list">
<div class="skills-loading">正在加载skills列表...</div>
</div>
<small class="form-hint">勾选要关联的skills,这些skills的内容会在执行任务前注入到系统提示词中,帮助AI更好地理解相关专业知识。</small>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="role-enabled" class="modern-checkbox" checked />
@@ -1514,6 +1662,7 @@
<script src="/static/js/chat.js"></script>
<script src="/static/js/settings.js"></script>
<script src="/static/js/knowledge.js"></script>
<script src="/static/js/skills.js"></script>
<script src="/static/js/vulnerability.js?v=4"></script>
<script src="/static/js/tasks.js"></script>
<script src="/static/js/roles.js"></script>