mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-15 21:08:01 +02:00
Add files via upload
This commit is contained in:
@@ -191,6 +191,7 @@ func setupRoutes(
|
||||
|
||||
// 配置管理
|
||||
protected.GET("/config", configHandler.GetConfig)
|
||||
protected.GET("/config/tools", configHandler.GetTools)
|
||||
protected.PUT("/config", configHandler.UpdateConfig)
|
||||
protected.POST("/config/apply", configHandler.ApplyConfig)
|
||||
|
||||
|
||||
@@ -69,6 +69,17 @@ func (db *DB) SaveToolExecution(exec *mcp.ToolExecution) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CountToolExecutions 统计工具执行记录总数
|
||||
func (db *DB) CountToolExecutions() (int, error) {
|
||||
query := `SELECT COUNT(*) FROM tool_executions`
|
||||
var count int
|
||||
err := db.QueryRow(query).Scan(&count)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// LoadToolExecutions 加载所有工具执行记录(支持分页)
|
||||
func (db *DB) LoadToolExecutions() ([]*mcp.ToolExecution, error) {
|
||||
return db.LoadToolExecutionsWithPagination(0, 1000)
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"cyberstrike-ai/internal/config"
|
||||
@@ -91,6 +93,99 @@ func (h *ConfigHandler) GetConfig(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// GetToolsResponse 获取工具列表响应(分页)
|
||||
type GetToolsResponse struct {
|
||||
Tools []ToolConfigInfo `json:"tools"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
|
||||
// GetTools 获取工具列表(支持分页和搜索)
|
||||
func (h *ConfigHandler) GetTools(c *gin.Context) {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
|
||||
// 解析分页参数
|
||||
page := 1
|
||||
pageSize := 20
|
||||
if pageStr := c.Query("page"); pageStr != "" {
|
||||
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
|
||||
page = p
|
||||
}
|
||||
}
|
||||
if pageSizeStr := c.Query("page_size"); pageSizeStr != "" {
|
||||
if ps, err := strconv.Atoi(pageSizeStr); err == nil && ps > 0 && ps <= 100 {
|
||||
pageSize = ps
|
||||
}
|
||||
}
|
||||
|
||||
// 解析搜索参数
|
||||
searchTerm := c.Query("search")
|
||||
searchTermLower := ""
|
||||
if searchTerm != "" {
|
||||
searchTermLower = strings.ToLower(searchTerm)
|
||||
}
|
||||
|
||||
// 获取所有工具并应用搜索过滤
|
||||
allTools := make([]ToolConfigInfo, 0, len(h.config.Security.Tools))
|
||||
for _, tool := range h.config.Security.Tools {
|
||||
toolInfo := ToolConfigInfo{
|
||||
Name: tool.Name,
|
||||
Description: tool.ShortDescription,
|
||||
Enabled: tool.Enabled,
|
||||
}
|
||||
// 如果没有简短描述,使用详细描述的前100个字符
|
||||
if toolInfo.Description == "" {
|
||||
desc := tool.Description
|
||||
if len(desc) > 100 {
|
||||
desc = desc[:100] + "..."
|
||||
}
|
||||
toolInfo.Description = desc
|
||||
}
|
||||
|
||||
// 如果有关键词,进行搜索过滤
|
||||
if searchTermLower != "" {
|
||||
nameLower := strings.ToLower(toolInfo.Name)
|
||||
descLower := strings.ToLower(toolInfo.Description)
|
||||
if !strings.Contains(nameLower, searchTermLower) && !strings.Contains(descLower, searchTermLower) {
|
||||
continue // 不匹配,跳过
|
||||
}
|
||||
}
|
||||
|
||||
allTools = append(allTools, toolInfo)
|
||||
}
|
||||
|
||||
total := len(allTools)
|
||||
totalPages := (total + pageSize - 1) / pageSize
|
||||
if totalPages == 0 {
|
||||
totalPages = 1
|
||||
}
|
||||
|
||||
// 计算分页范围
|
||||
offset := (page - 1) * pageSize
|
||||
end := offset + pageSize
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
|
||||
var tools []ToolConfigInfo
|
||||
if offset < total {
|
||||
tools = allTools[offset:end]
|
||||
} else {
|
||||
tools = []ToolConfigInfo{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, GetToolsResponse{
|
||||
Tools: tools,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
TotalPages: totalPages,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateConfigRequest 更新配置请求
|
||||
type UpdateConfigRequest struct {
|
||||
OpenAI *config.OpenAIConfig `json:"openai,omitempty"`
|
||||
|
||||
@@ -2,6 +2,7 @@ package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"cyberstrike-ai/internal/database"
|
||||
@@ -34,31 +35,96 @@ type MonitorResponse struct {
|
||||
Executions []*mcp.ToolExecution `json:"executions"`
|
||||
Stats map[string]*mcp.ToolStats `json:"stats"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Total int `json:"total,omitempty"`
|
||||
Page int `json:"page,omitempty"`
|
||||
PageSize int `json:"page_size,omitempty"`
|
||||
TotalPages int `json:"total_pages,omitempty"`
|
||||
}
|
||||
|
||||
// Monitor 获取监控信息
|
||||
func (h *MonitorHandler) Monitor(c *gin.Context) {
|
||||
executions := h.loadExecutions()
|
||||
// 解析分页参数
|
||||
page := 1
|
||||
pageSize := 20
|
||||
if pageStr := c.Query("page"); pageStr != "" {
|
||||
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
|
||||
page = p
|
||||
}
|
||||
}
|
||||
if pageSizeStr := c.Query("page_size"); pageSizeStr != "" {
|
||||
if ps, err := strconv.Atoi(pageSizeStr); err == nil && ps > 0 && ps <= 100 {
|
||||
pageSize = ps
|
||||
}
|
||||
}
|
||||
|
||||
executions, total := h.loadExecutionsWithPagination(page, pageSize)
|
||||
stats := h.loadStats()
|
||||
|
||||
totalPages := (total + pageSize - 1) / pageSize
|
||||
if totalPages == 0 {
|
||||
totalPages = 1
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, MonitorResponse{
|
||||
Executions: executions,
|
||||
Stats: stats,
|
||||
Timestamp: time.Now(),
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
TotalPages: totalPages,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *MonitorHandler) loadExecutions() []*mcp.ToolExecution {
|
||||
executions, _ := h.loadExecutionsWithPagination(1, 1000)
|
||||
return executions
|
||||
}
|
||||
|
||||
func (h *MonitorHandler) loadExecutionsWithPagination(page, pageSize int) ([]*mcp.ToolExecution, int) {
|
||||
if h.db == nil {
|
||||
return h.mcpServer.GetAllExecutions()
|
||||
allExecutions := h.mcpServer.GetAllExecutions()
|
||||
total := len(allExecutions)
|
||||
offset := (page - 1) * pageSize
|
||||
end := offset + pageSize
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
if offset >= total {
|
||||
return []*mcp.ToolExecution{}, total
|
||||
}
|
||||
return allExecutions[offset:end], total
|
||||
}
|
||||
|
||||
executions, err := h.db.LoadToolExecutions()
|
||||
offset := (page - 1) * pageSize
|
||||
executions, err := h.db.LoadToolExecutionsWithPagination(offset, pageSize)
|
||||
if err != nil {
|
||||
h.logger.Warn("从数据库加载执行记录失败,回退到内存数据", zap.Error(err))
|
||||
return h.mcpServer.GetAllExecutions()
|
||||
allExecutions := h.mcpServer.GetAllExecutions()
|
||||
total := len(allExecutions)
|
||||
offset := (page - 1) * pageSize
|
||||
end := offset + pageSize
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
if offset >= total {
|
||||
return []*mcp.ToolExecution{}, total
|
||||
}
|
||||
return allExecutions[offset:end], total
|
||||
}
|
||||
return executions
|
||||
|
||||
// 获取总数
|
||||
total, err := h.db.CountToolExecutions()
|
||||
if err != nil {
|
||||
h.logger.Warn("获取执行记录总数失败", zap.Error(err))
|
||||
// 回退:使用已加载的记录数估算
|
||||
total = offset + len(executions)
|
||||
if len(executions) == pageSize {
|
||||
total = offset + len(executions) + 1
|
||||
}
|
||||
}
|
||||
|
||||
return executions, total
|
||||
}
|
||||
|
||||
func (h *MonitorHandler) loadStats() map[string]*mcp.ToolStats {
|
||||
|
||||
@@ -1553,6 +1553,52 @@ header {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.search-box input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 0 2px rgba(0, 102, 255, 0.1);
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
min-width: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-search:hover {
|
||||
background: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
color: white;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.tools-list {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
@@ -1607,6 +1653,50 @@ header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tools-list-items {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.tools-pagination,
|
||||
.monitor-pagination {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
margin-top: 8px;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pagination-controls button {
|
||||
padding: 6px 12px;
|
||||
font-size: 0.875rem;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.pagination-controls button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.pagination-page {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
+334
-53
@@ -923,7 +923,7 @@ function addTimelineItem(timeline, type, options) {
|
||||
let content = `
|
||||
<div class="timeline-item-header">
|
||||
<span class="timeline-item-time">${time}</span>
|
||||
<span class="timeline-item-title">${options.title}</span>
|
||||
<span class="timeline-item-title">${escapeHtml(options.title || '')}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -938,7 +938,7 @@ function addTimelineItem(timeline, type, options) {
|
||||
<div class="tool-details">
|
||||
<div class="tool-arg-section">
|
||||
<strong>参数:</strong>
|
||||
<pre class="tool-args">${JSON.stringify(args, null, 2)}</pre>
|
||||
<pre class="tool-args">${escapeHtml(JSON.stringify(args, null, 2))}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -947,12 +947,14 @@ function addTimelineItem(timeline, type, options) {
|
||||
const data = options.data;
|
||||
const isError = data.isError || !data.success;
|
||||
const result = data.result || data.error || '无结果';
|
||||
// 确保 result 是字符串
|
||||
const resultStr = typeof result === 'string' ? result : JSON.stringify(result);
|
||||
content += `
|
||||
<div class="timeline-item-content">
|
||||
<div class="tool-result-section ${isError ? 'error' : 'success'}">
|
||||
<strong>执行结果:</strong>
|
||||
<pre class="tool-result">${escapeHtml(result)}</pre>
|
||||
${data.executionId ? `<div class="tool-execution-id">执行ID: <code>${data.executionId}</code></div>` : ''}
|
||||
<pre class="tool-result">${escapeHtml(resultStr)}</pre>
|
||||
${data.executionId ? `<div class="tool-execution-id">执行ID: <code>${escapeHtml(data.executionId)}</code></div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1006,24 +1008,53 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null) {
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'message-bubble';
|
||||
|
||||
// 解析 Markdown 格式
|
||||
// 解析 Markdown 或 HTML 格式
|
||||
let formattedContent;
|
||||
if (typeof marked !== 'undefined') {
|
||||
// 使用 marked.js 解析 Markdown
|
||||
|
||||
// 先使用 DOMPurify 清理(如果可用),这样可以处理已经是 HTML 的内容
|
||||
if (typeof DOMPurify !== 'undefined') {
|
||||
// 配置 DOMPurify 允许的标签和属性
|
||||
const sanitizeConfig = {
|
||||
// 允许基本的 Markdown 格式化标签
|
||||
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 's', 'code', 'pre', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'a', 'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr'],
|
||||
ALLOWED_ATTR: ['href', 'title', 'alt', 'src', 'class'],
|
||||
ALLOW_DATA_ATTR: false,
|
||||
};
|
||||
|
||||
// 如果内容看起来已经是 HTML(包含 HTML 标签),直接清理
|
||||
// 否则先用 marked.js 解析 Markdown,再清理
|
||||
if (typeof marked !== 'undefined' && !/<[a-z][\s\S]*>/i.test(content)) {
|
||||
// 内容不包含 HTML 标签,可能是 Markdown,使用 marked.js 解析
|
||||
try {
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
});
|
||||
let parsedContent = marked.parse(content);
|
||||
formattedContent = DOMPurify.sanitize(parsedContent, sanitizeConfig);
|
||||
} catch (e) {
|
||||
console.error('Markdown 解析失败:', e);
|
||||
// 降级处理:直接清理原始内容
|
||||
formattedContent = DOMPurify.sanitize(content, sanitizeConfig);
|
||||
}
|
||||
} else {
|
||||
// 内容包含 HTML 标签或 marked.js 不可用,直接清理
|
||||
formattedContent = DOMPurify.sanitize(content, sanitizeConfig);
|
||||
}
|
||||
} else if (typeof marked !== 'undefined') {
|
||||
// 没有 DOMPurify,但有 marked.js
|
||||
try {
|
||||
// 配置 marked 选项
|
||||
marked.setOptions({
|
||||
breaks: true, // 支持换行
|
||||
gfm: true, // 支持 GitHub Flavored Markdown
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
});
|
||||
formattedContent = marked.parse(content);
|
||||
} catch (e) {
|
||||
console.error('Markdown 解析失败:', e);
|
||||
// 降级处理:转义 HTML 并保留换行
|
||||
formattedContent = escapeHtml(content).replace(/\n/g, '<br>');
|
||||
}
|
||||
} else {
|
||||
// 如果没有 marked.js,使用简单处理
|
||||
// 都没有,简单转义
|
||||
formattedContent = escapeHtml(content).replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
@@ -1315,7 +1346,35 @@ function escapeHtml(text) {
|
||||
}
|
||||
|
||||
function formatMarkdown(text) {
|
||||
if (typeof marked !== 'undefined') {
|
||||
// 配置 DOMPurify 允许的标签和属性
|
||||
const sanitizeConfig = {
|
||||
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 's', 'code', 'pre', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'a', 'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr'],
|
||||
ALLOWED_ATTR: ['href', 'title', 'alt', 'src', 'class'],
|
||||
ALLOW_DATA_ATTR: false,
|
||||
};
|
||||
|
||||
if (typeof DOMPurify !== 'undefined') {
|
||||
// 如果内容看起来已经是 HTML(包含 HTML 标签),直接清理
|
||||
// 否则先用 marked.js 解析 Markdown,再清理
|
||||
if (typeof marked !== 'undefined' && !/<[a-z][\s\S]*>/i.test(text)) {
|
||||
// 内容不包含 HTML 标签,可能是 Markdown,使用 marked.js 解析
|
||||
try {
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
});
|
||||
let parsedContent = marked.parse(text);
|
||||
return DOMPurify.sanitize(parsedContent, sanitizeConfig);
|
||||
} catch (e) {
|
||||
console.error('Markdown 解析失败:', e);
|
||||
return DOMPurify.sanitize(text, sanitizeConfig);
|
||||
}
|
||||
} else {
|
||||
// 内容包含 HTML 标签或 marked.js 不可用,直接清理
|
||||
return DOMPurify.sanitize(text, sanitizeConfig);
|
||||
}
|
||||
} else if (typeof marked !== 'undefined') {
|
||||
// 没有 DOMPurify,但有 marked.js
|
||||
try {
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
@@ -1629,6 +1688,12 @@ async function cancelActiveTask(conversationId, button) {
|
||||
// 设置相关功能
|
||||
let currentConfig = null;
|
||||
let allTools = [];
|
||||
let toolsPagination = {
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
totalPages: 0
|
||||
};
|
||||
|
||||
// 打开设置
|
||||
async function openSettings() {
|
||||
@@ -1685,19 +1750,95 @@ async function loadConfig() {
|
||||
// 填充Agent配置
|
||||
document.getElementById('agent-max-iterations').value = currentConfig.agent.max_iterations || 30;
|
||||
|
||||
// 填充工具列表
|
||||
allTools = currentConfig.tools || [];
|
||||
renderToolsList();
|
||||
// 加载工具列表(使用分页)
|
||||
toolsSearchKeyword = '';
|
||||
await loadToolsList(1, '');
|
||||
} catch (error) {
|
||||
console.error('加载配置失败:', error);
|
||||
alert('加载配置失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 工具搜索关键词
|
||||
let toolsSearchKeyword = '';
|
||||
|
||||
// 加载工具列表(分页)
|
||||
async function loadToolsList(page = 1, searchKeyword = '') {
|
||||
try {
|
||||
const pageSize = toolsPagination.pageSize;
|
||||
let url = `/api/config/tools?page=${page}&page_size=${pageSize}`;
|
||||
if (searchKeyword) {
|
||||
url += `&search=${encodeURIComponent(searchKeyword)}`;
|
||||
}
|
||||
|
||||
const response = await apiFetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error('获取工具列表失败');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
allTools = result.tools || [];
|
||||
toolsPagination = {
|
||||
page: result.page || page,
|
||||
pageSize: result.page_size || pageSize,
|
||||
total: result.total || 0,
|
||||
totalPages: result.total_pages || 1
|
||||
};
|
||||
|
||||
renderToolsList();
|
||||
renderToolsPagination();
|
||||
} catch (error) {
|
||||
console.error('加载工具列表失败:', error);
|
||||
const toolsList = document.getElementById('tools-list');
|
||||
if (toolsList) {
|
||||
toolsList.innerHTML = `<div class="error">加载工具列表失败: ${escapeHtml(error.message)}</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索工具
|
||||
function searchTools() {
|
||||
const searchInput = document.getElementById('tools-search');
|
||||
const keyword = searchInput ? searchInput.value.trim() : '';
|
||||
toolsSearchKeyword = keyword;
|
||||
// 搜索时重置到第一页
|
||||
loadToolsList(1, keyword);
|
||||
}
|
||||
|
||||
// 清除搜索
|
||||
function clearSearch() {
|
||||
const searchInput = document.getElementById('tools-search');
|
||||
if (searchInput) {
|
||||
searchInput.value = '';
|
||||
}
|
||||
toolsSearchKeyword = '';
|
||||
loadToolsList(1, '');
|
||||
}
|
||||
|
||||
// 处理搜索框回车事件
|
||||
function handleSearchKeyPress(event) {
|
||||
if (event.key === 'Enter') {
|
||||
searchTools();
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染工具列表
|
||||
function renderToolsList() {
|
||||
const toolsList = document.getElementById('tools-list');
|
||||
toolsList.innerHTML = '';
|
||||
if (!toolsList) return;
|
||||
|
||||
// 只渲染列表部分,分页控件单独渲染
|
||||
const listContainer = toolsList.querySelector('.tools-list-items') || document.createElement('div');
|
||||
listContainer.className = 'tools-list-items';
|
||||
listContainer.innerHTML = '';
|
||||
|
||||
if (allTools.length === 0) {
|
||||
listContainer.innerHTML = '<div class="empty">暂无工具</div>';
|
||||
if (!toolsList.contains(listContainer)) {
|
||||
toolsList.appendChild(listContainer);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
allTools.forEach(tool => {
|
||||
const toolItem = document.createElement('div');
|
||||
@@ -1710,8 +1851,51 @@ function renderToolsList() {
|
||||
<div class="tool-item-desc">${escapeHtml(tool.description || '无描述')}</div>
|
||||
</div>
|
||||
`;
|
||||
toolsList.appendChild(toolItem);
|
||||
listContainer.appendChild(toolItem);
|
||||
});
|
||||
|
||||
if (!toolsList.contains(listContainer)) {
|
||||
toolsList.appendChild(listContainer);
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染工具列表分页控件
|
||||
function renderToolsPagination() {
|
||||
const toolsList = document.getElementById('tools-list');
|
||||
if (!toolsList) return;
|
||||
|
||||
// 移除旧的分页控件
|
||||
const oldPagination = toolsList.querySelector('.tools-pagination');
|
||||
if (oldPagination) {
|
||||
oldPagination.remove();
|
||||
}
|
||||
|
||||
// 如果只有一页或没有数据,不显示分页
|
||||
if (toolsPagination.totalPages <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pagination = document.createElement('div');
|
||||
pagination.className = 'tools-pagination';
|
||||
|
||||
const { page, totalPages, total } = toolsPagination;
|
||||
const startItem = (page - 1) * toolsPagination.pageSize + 1;
|
||||
const endItem = Math.min(page * toolsPagination.pageSize, total);
|
||||
|
||||
pagination.innerHTML = `
|
||||
<div class="pagination-info">
|
||||
显示 ${startItem}-${endItem} / 共 ${total} 个工具${toolsSearchKeyword ? ` (搜索: "${escapeHtml(toolsSearchKeyword)}")` : ''}
|
||||
</div>
|
||||
<div class="pagination-controls">
|
||||
<button class="btn-secondary" onclick="loadToolsList(1, '${escapeHtml(toolsSearchKeyword)}')" ${page === 1 ? 'disabled' : ''}>首页</button>
|
||||
<button class="btn-secondary" onclick="loadToolsList(${page - 1}, '${escapeHtml(toolsSearchKeyword)}')" ${page === 1 ? 'disabled' : ''}>上一页</button>
|
||||
<span class="pagination-page">第 ${page} / ${totalPages} 页</span>
|
||||
<button class="btn-secondary" onclick="loadToolsList(${page + 1}, '${escapeHtml(toolsSearchKeyword)}')" ${page === totalPages ? 'disabled' : ''}>下一页</button>
|
||||
<button class="btn-secondary" onclick="loadToolsList(${totalPages}, '${escapeHtml(toolsSearchKeyword)}')" ${page === totalPages ? 'disabled' : ''}>末页</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
toolsList.appendChild(pagination);
|
||||
}
|
||||
|
||||
// 全选工具
|
||||
@@ -1728,18 +1912,11 @@ function deselectAllTools() {
|
||||
});
|
||||
}
|
||||
|
||||
// 过滤工具
|
||||
// 过滤工具(已废弃,现在使用服务端搜索)
|
||||
// 保留此函数以防其他地方调用,但实际功能已由searchTools()替代
|
||||
function filterTools() {
|
||||
const searchTerm = document.getElementById('tools-search').value.toLowerCase();
|
||||
document.querySelectorAll('.tool-item').forEach(item => {
|
||||
const toolName = (item.dataset.toolName || '').toLowerCase();
|
||||
const toolDesc = item.querySelector('.tool-item-desc').textContent.toLowerCase();
|
||||
if (toolName.includes(searchTerm) || toolDesc.includes(searchTerm)) {
|
||||
item.classList.remove('hidden');
|
||||
} else {
|
||||
item.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
// 不再使用客户端过滤,改为触发服务端搜索
|
||||
// 可以保留为空函数或移除oninput事件
|
||||
}
|
||||
|
||||
// 应用设置
|
||||
@@ -1791,18 +1968,49 @@ async function applySettings() {
|
||||
};
|
||||
|
||||
// 收集工具启用状态
|
||||
// 由于使用分页,需要先获取所有工具的状态
|
||||
// 先获取当前页的工具状态
|
||||
const currentPageTools = new Map();
|
||||
document.querySelectorAll('#tools-list .tool-item').forEach(item => {
|
||||
const checkbox = item.querySelector('input[type="checkbox"]');
|
||||
const toolName = item.dataset.toolName;
|
||||
if (toolName) {
|
||||
// 直接使用工具名称
|
||||
config.tools.push({
|
||||
name: toolName,
|
||||
enabled: checkbox.checked
|
||||
});
|
||||
currentPageTools.set(toolName, checkbox.checked);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取所有工具列表以获取完整状态
|
||||
try {
|
||||
const allToolsResponse = await apiFetch(`/api/config/tools?page=1&page_size=1000`);
|
||||
if (allToolsResponse.ok) {
|
||||
const allToolsResult = await allToolsResponse.json();
|
||||
// 使用所有工具,但用当前页的修改覆盖
|
||||
allToolsResult.tools.forEach(tool => {
|
||||
config.tools.push({
|
||||
name: tool.name,
|
||||
enabled: currentPageTools.has(tool.name) ? currentPageTools.get(tool.name) : tool.enabled
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// 如果获取失败,只使用当前页的工具
|
||||
currentPageTools.forEach((enabled, toolName) => {
|
||||
config.tools.push({
|
||||
name: toolName,
|
||||
enabled: enabled
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('获取所有工具列表失败,仅使用当前页工具状态', error);
|
||||
// 如果获取失败,只使用当前页的工具
|
||||
currentPageTools.forEach((enabled, toolName) => {
|
||||
config.tools.push({
|
||||
name: toolName,
|
||||
enabled: enabled
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 更新配置
|
||||
const updateResponse = await apiFetch('/api/config', {
|
||||
method: 'PUT',
|
||||
@@ -1922,7 +2130,13 @@ async function changePassword() {
|
||||
const monitorState = {
|
||||
executions: [],
|
||||
stats: {},
|
||||
lastFetchedAt: null
|
||||
lastFetchedAt: null,
|
||||
pagination: {
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
totalPages: 0
|
||||
}
|
||||
};
|
||||
|
||||
function openMonitorPanel() {
|
||||
@@ -1947,7 +2161,15 @@ function openMonitorPanel() {
|
||||
statusFilter.value = 'all';
|
||||
}
|
||||
|
||||
refreshMonitorPanel();
|
||||
// 重置分页状态
|
||||
monitorState.pagination = {
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
totalPages: 0
|
||||
};
|
||||
|
||||
refreshMonitorPanel(1);
|
||||
}
|
||||
|
||||
function closeMonitorPanel() {
|
||||
@@ -1957,12 +2179,16 @@ function closeMonitorPanel() {
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshMonitorPanel() {
|
||||
async function refreshMonitorPanel(page = null) {
|
||||
const statsContainer = document.getElementById('monitor-stats');
|
||||
const execContainer = document.getElementById('monitor-executions');
|
||||
|
||||
try {
|
||||
const response = await apiFetch('/api/monitor', { method: 'GET' });
|
||||
// 如果指定了页码,使用指定页码,否则使用当前页码
|
||||
const currentPage = page !== null ? page : monitorState.pagination.page;
|
||||
const pageSize = monitorState.pagination.pageSize;
|
||||
|
||||
const response = await apiFetch(`/api/monitor?page=${currentPage}&page_size=${pageSize}`, { method: 'GET' });
|
||||
const result = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
throw new Error(result.error || '获取监控数据失败');
|
||||
@@ -1971,9 +2197,20 @@ async function refreshMonitorPanel() {
|
||||
monitorState.executions = Array.isArray(result.executions) ? result.executions : [];
|
||||
monitorState.stats = result.stats || {};
|
||||
monitorState.lastFetchedAt = new Date();
|
||||
|
||||
// 更新分页信息
|
||||
if (result.total !== undefined) {
|
||||
monitorState.pagination = {
|
||||
page: result.page || currentPage,
|
||||
pageSize: result.page_size || pageSize,
|
||||
total: result.total || 0,
|
||||
totalPages: result.total_pages || 1
|
||||
};
|
||||
}
|
||||
|
||||
renderMonitorStats(monitorState.stats, monitorState.lastFetchedAt);
|
||||
renderMonitorExecutions(monitorState.executions);
|
||||
renderMonitorPagination();
|
||||
} catch (error) {
|
||||
console.error('刷新监控面板失败:', error);
|
||||
if (statsContainer) {
|
||||
@@ -2084,7 +2321,6 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
|
||||
}
|
||||
|
||||
const rows = filtered
|
||||
.slice(0, 25)
|
||||
.map(exec => {
|
||||
const status = (exec.status || 'unknown').toLowerCase();
|
||||
const statusClass = `monitor-status-chip ${status}`;
|
||||
@@ -2109,22 +2345,67 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
|
||||
})
|
||||
.join('');
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="monitor-table-container">
|
||||
<table class="monitor-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>工具</th>
|
||||
<th>状态</th>
|
||||
<th>开始时间</th>
|
||||
<th>耗时</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
// 创建表格容器
|
||||
const tableContainer = document.createElement('div');
|
||||
tableContainer.className = 'monitor-table-container';
|
||||
tableContainer.innerHTML = `
|
||||
<table class="monitor-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>工具</th>
|
||||
<th>状态</th>
|
||||
<th>开始时间</th>
|
||||
<th>耗时</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
// 清空容器并添加表格
|
||||
container.innerHTML = '';
|
||||
container.appendChild(tableContainer);
|
||||
}
|
||||
|
||||
// 渲染监控面板分页控件
|
||||
function renderMonitorPagination() {
|
||||
const container = document.getElementById('monitor-executions');
|
||||
if (!container) return;
|
||||
|
||||
// 移除旧的分页控件
|
||||
const oldPagination = container.querySelector('.monitor-pagination');
|
||||
if (oldPagination) {
|
||||
oldPagination.remove();
|
||||
}
|
||||
|
||||
const { page, totalPages, total, pageSize } = monitorState.pagination;
|
||||
|
||||
// 如果只有一页或没有数据,不显示分页
|
||||
if (totalPages <= 1 || total === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pagination = document.createElement('div');
|
||||
pagination.className = 'monitor-pagination';
|
||||
|
||||
const startItem = (page - 1) * pageSize + 1;
|
||||
const endItem = Math.min(page * pageSize, total);
|
||||
|
||||
pagination.innerHTML = `
|
||||
<div class="pagination-info">
|
||||
显示 ${startItem}-${endItem} / 共 ${total} 条记录
|
||||
</div>
|
||||
<div class="pagination-controls">
|
||||
<button class="btn-secondary" onclick="refreshMonitorPanel(1)" ${page === 1 ? 'disabled' : ''}>首页</button>
|
||||
<button class="btn-secondary" onclick="refreshMonitorPanel(${page - 1})" ${page === 1 ? 'disabled' : ''}>上一页</button>
|
||||
<span class="pagination-page">第 ${page} / ${totalPages} 页</span>
|
||||
<button class="btn-secondary" onclick="refreshMonitorPanel(${page + 1})" ${page === totalPages ? 'disabled' : ''}>下一页</button>
|
||||
<button class="btn-secondary" onclick="refreshMonitorPanel(${totalPages})" ${page === totalPages ? 'disabled' : ''}>末页</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(pagination);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -115,7 +115,10 @@
|
||||
<div class="tools-actions">
|
||||
<button class="btn-secondary" onclick="selectAllTools()">全选</button>
|
||||
<button class="btn-secondary" onclick="deselectAllTools()">全不选</button>
|
||||
<input type="text" id="tools-search" placeholder="搜索工具..." oninput="filterTools()" />
|
||||
<div class="search-box">
|
||||
<input type="text" id="tools-search" placeholder="搜索工具..." onkeypress="handleSearchKeyPress(event)" oninput="if(this.value.trim() === '') clearSearch()" />
|
||||
<button class="btn-search" onclick="searchTools()" title="搜索">🔍</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tools-list" class="tools-list"></div>
|
||||
</div>
|
||||
@@ -246,6 +249,8 @@
|
||||
|
||||
<!-- Marked.js for Markdown parsing -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
|
||||
<!-- DOMPurify for HTML sanitization to prevent XSS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.8/dist/purify.min.js"></script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user