diff --git a/internal/app/app.go b/internal/app/app.go
index 459f98f9..d1444859 100644
--- a/internal/app/app.go
+++ b/internal/app/app.go
@@ -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)
diff --git a/internal/database/monitor.go b/internal/database/monitor.go
index 8140c3b9..06807fb5 100644
--- a/internal/database/monitor.go
+++ b/internal/database/monitor.go
@@ -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)
diff --git a/internal/handler/config.go b/internal/handler/config.go
index a2245cca..4abf8112 100644
--- a/internal/handler/config.go
+++ b/internal/handler/config.go
@@ -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"`
diff --git a/internal/handler/monitor.go b/internal/handler/monitor.go
index c5004208..8a47268f 100644
--- a/internal/handler/monitor.go
+++ b/internal/handler/monitor.go
@@ -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 {
diff --git a/web/static/css/style.css b/web/static/css/style.css
index 3bfd1551..a845744b 100644
--- a/web/static/css/style.css
+++ b/web/static/css/style.css
@@ -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;
diff --git a/web/static/js/app.js b/web/static/js/app.js
index a9c1508c..3eba9f9a 100644
--- a/web/static/js/app.js
+++ b/web/static/js/app.js
@@ -923,7 +923,7 @@ function addTimelineItem(timeline, type, options) {
let content = `
`;
@@ -938,7 +938,7 @@ function addTimelineItem(timeline, type, options) {
@@ -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 += `
`;
@@ -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, '
');
}
} else {
- // 如果没有 marked.js,使用简单处理
+ // 都没有,简单转义
formattedContent = escapeHtml(content).replace(/\n/g, '
');
}
@@ -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 = `加载工具列表失败: ${escapeHtml(error.message)}
`;
+ }
+ }
+}
+
+// 搜索工具
+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 = '暂无工具
';
+ if (!toolsList.contains(listContainer)) {
+ toolsList.appendChild(listContainer);
+ }
+ return;
+ }
allTools.forEach(tool => {
const toolItem = document.createElement('div');
@@ -1710,8 +1851,51 @@ function renderToolsList() {
${escapeHtml(tool.description || '无描述')}
`;
- 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 = `
+
+
+ `;
+
+ 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 = `
-
-
-
-
- | 工具 |
- 状态 |
- 开始时间 |
- 耗时 |
- 操作 |
-
-
- ${rows}
-
+ // 创建表格容器
+ const tableContainer = document.createElement('div');
+ tableContainer.className = 'monitor-table-container';
+ tableContainer.innerHTML = `
+
+
+
+ | 工具 |
+ 状态 |
+ 开始时间 |
+ 耗时 |
+ 操作 |
+
+
+ ${rows}
+
+ `;
+
+ // 清空容器并添加表格
+ 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 = `
+
+
`;
+
+ container.appendChild(pagination);
}
diff --git a/web/templates/index.html b/web/templates/index.html
index cda42cba..b1f1cb16 100644
--- a/web/templates/index.html
+++ b/web/templates/index.html
@@ -115,7 +115,10 @@
@@ -246,6 +249,8 @@
+
+