mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-07-02 02:35:38 +02:00
Add files via upload
This commit is contained in:
+531
-3
@@ -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
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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参数,加载对应对话
|
||||
|
||||
@@ -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()">×</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;
|
||||
}
|
||||
@@ -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()">×</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>
|
||||
|
||||
Reference in New Issue
Block a user