mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-08-09 04:16:10 +02:00
Add files via upload
1、修复删除知识项后总分类数统计错误:将 updateKnowledgeStats 中的 || 改为 != null 检查,并移除会错误更新统计的 updateKnowledgeStatsAfterDelete 调用。 2、为 MCP 状态监控页面添加了批量删除功能(复选框、全选、批量删除按钮)和每页显示数量配置(选择器位于分页控件左侧,设置保存到 localStorage)。
This commit is contained in:
@@ -448,6 +448,7 @@ func setupRoutes(
|
|||||||
protected.GET("/monitor", monitorHandler.Monitor)
|
protected.GET("/monitor", monitorHandler.Monitor)
|
||||||
protected.GET("/monitor/execution/:id", monitorHandler.GetExecution)
|
protected.GET("/monitor/execution/:id", monitorHandler.GetExecution)
|
||||||
protected.DELETE("/monitor/execution/:id", monitorHandler.DeleteExecution)
|
protected.DELETE("/monitor/execution/:id", monitorHandler.DeleteExecution)
|
||||||
|
protected.DELETE("/monitor/executions", monitorHandler.DeleteExecutions)
|
||||||
protected.GET("/monitor/stats", monitorHandler.GetStats)
|
protected.GET("/monitor/stats", monitorHandler.GetStats)
|
||||||
|
|
||||||
// 配置管理
|
// 配置管理
|
||||||
|
|||||||
@@ -281,6 +281,117 @@ func (db *DB) DeleteToolExecution(id string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteToolExecutions 批量删除工具执行记录
|
||||||
|
func (db *DB) DeleteToolExecutions(ids []string) error {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建 IN 查询的占位符
|
||||||
|
placeholders := make([]string, len(ids))
|
||||||
|
args := make([]interface{}, len(ids))
|
||||||
|
for i, id := range ids {
|
||||||
|
placeholders[i] = "?"
|
||||||
|
args[i] = id
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `DELETE FROM tool_executions WHERE id IN (` + strings.Join(placeholders, ",") + `)`
|
||||||
|
_, err := db.Exec(query, args...)
|
||||||
|
if err != nil {
|
||||||
|
db.logger.Error("批量删除工具执行记录失败", zap.Error(err), zap.Int("count", len(ids)))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetToolExecutionsByIds 根据ID列表获取工具执行记录(用于批量删除前获取统计信息)
|
||||||
|
func (db *DB) GetToolExecutionsByIds(ids []string) ([]*mcp.ToolExecution, error) {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return []*mcp.ToolExecution{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建 IN 查询的占位符
|
||||||
|
placeholders := make([]string, len(ids))
|
||||||
|
args := make([]interface{}, len(ids))
|
||||||
|
for i, id := range ids {
|
||||||
|
placeholders[i] = "?"
|
||||||
|
args[i] = id
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `
|
||||||
|
SELECT id, tool_name, arguments, status, result, error, start_time, end_time, duration_ms
|
||||||
|
FROM tool_executions
|
||||||
|
WHERE id IN (` + strings.Join(placeholders, ",") + `)
|
||||||
|
`
|
||||||
|
|
||||||
|
rows, err := db.Query(query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var executions []*mcp.ToolExecution
|
||||||
|
for rows.Next() {
|
||||||
|
var exec mcp.ToolExecution
|
||||||
|
var argsJSON string
|
||||||
|
var resultJSON sql.NullString
|
||||||
|
var errorText sql.NullString
|
||||||
|
var endTime sql.NullTime
|
||||||
|
var durationMs sql.NullInt64
|
||||||
|
|
||||||
|
err := rows.Scan(
|
||||||
|
&exec.ID,
|
||||||
|
&exec.ToolName,
|
||||||
|
&argsJSON,
|
||||||
|
&exec.Status,
|
||||||
|
&resultJSON,
|
||||||
|
&errorText,
|
||||||
|
&exec.StartTime,
|
||||||
|
&endTime,
|
||||||
|
&durationMs,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
db.logger.Warn("加载执行记录失败", zap.Error(err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析参数
|
||||||
|
if err := json.Unmarshal([]byte(argsJSON), &exec.Arguments); err != nil {
|
||||||
|
db.logger.Warn("解析执行参数失败", zap.Error(err))
|
||||||
|
exec.Arguments = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析结果
|
||||||
|
if resultJSON.Valid && resultJSON.String != "" {
|
||||||
|
var result mcp.ToolResult
|
||||||
|
if err := json.Unmarshal([]byte(resultJSON.String), &result); err != nil {
|
||||||
|
db.logger.Warn("解析执行结果失败", zap.Error(err))
|
||||||
|
} else {
|
||||||
|
exec.Result = &result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置错误
|
||||||
|
if errorText.Valid {
|
||||||
|
exec.Error = errorText.String
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置结束时间
|
||||||
|
if endTime.Valid {
|
||||||
|
exec.EndTime = &endTime.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置持续时间
|
||||||
|
if durationMs.Valid {
|
||||||
|
exec.Duration = time.Duration(durationMs.Int64) * time.Millisecond
|
||||||
|
}
|
||||||
|
|
||||||
|
executions = append(executions, &exec)
|
||||||
|
}
|
||||||
|
|
||||||
|
return executions, nil
|
||||||
|
}
|
||||||
|
|
||||||
// SaveToolStats 保存工具统计信息
|
// SaveToolStats 保存工具统计信息
|
||||||
func (db *DB) SaveToolStats(toolName string, stats *mcp.ToolStats) error {
|
func (db *DB) SaveToolStats(toolName string, stats *mcp.ToolStats) error {
|
||||||
var lastCallTime sql.NullTime
|
var lastCallTime sql.NullTime
|
||||||
|
|||||||
@@ -307,4 +307,79 @@ func (h *MonitorHandler) DeleteExecution(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"message": "执行记录已删除(如果存在)"})
|
c.JSON(http.StatusOK, gin.H{"message": "执行记录已删除(如果存在)"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteExecutions 批量删除执行记录
|
||||||
|
func (h *MonitorHandler) DeleteExecutions(c *gin.Context) {
|
||||||
|
var request struct {
|
||||||
|
IDs []string `json:"ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数无效: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(request.IDs) == 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "执行记录ID列表不能为空"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果使用数据库,先获取执行记录信息,然后删除并更新统计
|
||||||
|
if h.db != nil {
|
||||||
|
// 先获取执行记录信息(用于更新统计)
|
||||||
|
executions, err := h.db.GetToolExecutionsByIds(request.IDs)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("获取执行记录失败", zap.Error(err))
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取执行记录失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按工具名称分组统计需要减少的数量
|
||||||
|
toolStats := make(map[string]struct {
|
||||||
|
totalCalls int
|
||||||
|
successCalls int
|
||||||
|
failedCalls int
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, exec := range executions {
|
||||||
|
if exec.ToolName == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := toolStats[exec.ToolName]
|
||||||
|
stats.totalCalls++
|
||||||
|
if exec.Status == "failed" {
|
||||||
|
stats.failedCalls++
|
||||||
|
} else if exec.Status == "completed" {
|
||||||
|
stats.successCalls++
|
||||||
|
}
|
||||||
|
toolStats[exec.ToolName] = stats
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量删除执行记录
|
||||||
|
err = h.db.DeleteToolExecutions(request.IDs)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("批量删除执行记录失败", zap.Error(err), zap.Int("count", len(request.IDs)))
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "批量删除执行记录失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新统计信息(减少相应的计数)
|
||||||
|
for toolName, stats := range toolStats {
|
||||||
|
if err := h.db.DecreaseToolStats(toolName, stats.totalCalls, stats.successCalls, stats.failedCalls); err != nil {
|
||||||
|
h.logger.Warn("更新统计信息失败", zap.Error(err), zap.String("toolName", toolName))
|
||||||
|
// 不返回错误,因为记录已经删除成功
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h.logger.Info("批量删除执行记录成功", zap.Int("count", len(request.IDs)))
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "成功删除执行记录", "deleted": len(executions)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不使用数据库,尝试从内存中删除(内部MCP服务器)
|
||||||
|
// 注意:内存中的记录可能已经被清理,所以这里只记录日志
|
||||||
|
h.logger.Info("尝试批量删除内存中的执行记录", zap.Int("count", len(request.IDs)))
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "执行记录已删除(如果存在)"})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3036,6 +3036,9 @@ header {
|
|||||||
.pagination-info {
|
.pagination-info {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-controls {
|
.pagination-controls {
|
||||||
@@ -3056,6 +3059,36 @@ header {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pagination-page-size {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-page-size select {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-page-size select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 102, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-page-size select:hover {
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
.pagination-btn {
|
.pagination-btn {
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
@@ -3414,6 +3447,50 @@ header {
|
|||||||
color: #c82333;
|
color: #c82333;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.monitor-batch-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-batch-actions .batch-actions-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-batch-actions .batch-actions-buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-batch-actions .batch-actions-buttons button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-execution-checkbox {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
accent-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-table th:first-child,
|
||||||
|
.monitor-table td:first-child {
|
||||||
|
text-align: center;
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
.monitor-vuln-container {
|
.monitor-vuln-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
|||||||
+182
-2
@@ -1008,7 +1008,11 @@ const monitorState = {
|
|||||||
lastFetchedAt: null,
|
lastFetchedAt: null,
|
||||||
pagination: {
|
pagination: {
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 20,
|
pageSize: (() => {
|
||||||
|
// 从 localStorage 读取保存的每页显示数量,默认为 20
|
||||||
|
const saved = localStorage.getItem('monitorPageSize');
|
||||||
|
return saved ? parseInt(saved, 10) : 20;
|
||||||
|
})(),
|
||||||
total: 0,
|
total: 0,
|
||||||
totalPages: 0
|
totalPages: 0
|
||||||
}
|
}
|
||||||
@@ -1019,6 +1023,39 @@ function openMonitorPanel() {
|
|||||||
if (typeof switchPage === 'function') {
|
if (typeof switchPage === 'function') {
|
||||||
switchPage('mcp-monitor');
|
switchPage('mcp-monitor');
|
||||||
}
|
}
|
||||||
|
// 初始化每页显示数量选择器
|
||||||
|
initializeMonitorPageSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化每页显示数量选择器
|
||||||
|
function initializeMonitorPageSize() {
|
||||||
|
const pageSizeSelect = document.getElementById('monitor-page-size');
|
||||||
|
if (pageSizeSelect) {
|
||||||
|
pageSizeSelect.value = monitorState.pagination.pageSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 改变每页显示数量
|
||||||
|
function changeMonitorPageSize() {
|
||||||
|
const pageSizeSelect = document.getElementById('monitor-page-size');
|
||||||
|
if (!pageSizeSelect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPageSize = parseInt(pageSizeSelect.value, 10);
|
||||||
|
if (isNaN(newPageSize) || newPageSize <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到 localStorage
|
||||||
|
localStorage.setItem('monitorPageSize', newPageSize.toString());
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
monitorState.pagination.pageSize = newPageSize;
|
||||||
|
monitorState.pagination.page = 1; // 重置到第一页
|
||||||
|
|
||||||
|
// 刷新数据
|
||||||
|
refreshMonitorPanel(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeMonitorPanel() {
|
function closeMonitorPanel() {
|
||||||
@@ -1076,6 +1113,9 @@ async function refreshMonitorPanel(page = null) {
|
|||||||
renderMonitorStats(monitorState.stats, monitorState.lastFetchedAt);
|
renderMonitorStats(monitorState.stats, monitorState.lastFetchedAt);
|
||||||
renderMonitorExecutions(monitorState.executions, currentStatusFilter);
|
renderMonitorExecutions(monitorState.executions, currentStatusFilter);
|
||||||
renderMonitorPagination();
|
renderMonitorPagination();
|
||||||
|
|
||||||
|
// 初始化每页显示数量选择器
|
||||||
|
initializeMonitorPageSize();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('刷新监控面板失败:', error);
|
console.error('刷新监控面板失败:', error);
|
||||||
if (statsContainer) {
|
if (statsContainer) {
|
||||||
@@ -1150,6 +1190,9 @@ async function refreshMonitorPanelWithFilter(statusFilter = 'all', toolFilter =
|
|||||||
renderMonitorStats(monitorState.stats, monitorState.lastFetchedAt);
|
renderMonitorStats(monitorState.stats, monitorState.lastFetchedAt);
|
||||||
renderMonitorExecutions(monitorState.executions, statusFilter);
|
renderMonitorExecutions(monitorState.executions, statusFilter);
|
||||||
renderMonitorPagination();
|
renderMonitorPagination();
|
||||||
|
|
||||||
|
// 初始化每页显示数量选择器
|
||||||
|
initializeMonitorPageSize();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('刷新监控面板失败:', error);
|
console.error('刷新监控面板失败:', error);
|
||||||
if (statsContainer) {
|
if (statsContainer) {
|
||||||
@@ -1250,6 +1293,11 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
|
|||||||
} else {
|
} else {
|
||||||
container.innerHTML = '<div class="monitor-empty">暂无执行记录</div>';
|
container.innerHTML = '<div class="monitor-empty">暂无执行记录</div>';
|
||||||
}
|
}
|
||||||
|
// 隐藏批量操作栏
|
||||||
|
const batchActions = document.getElementById('monitor-batch-actions');
|
||||||
|
if (batchActions) {
|
||||||
|
batchActions.style.display = 'none';
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1266,6 +1314,9 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
|
|||||||
const executionId = escapeHtml(exec.id || '');
|
const executionId = escapeHtml(exec.id || '');
|
||||||
return `
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" class="monitor-execution-checkbox" value="${executionId}" onchange="updateBatchActionsState()" />
|
||||||
|
</td>
|
||||||
<td>${toolName}</td>
|
<td>${toolName}</td>
|
||||||
<td><span class="${statusClass}">${statusLabel}</span></td>
|
<td><span class="${statusClass}">${statusLabel}</span></td>
|
||||||
<td>${startTime}</td>
|
<td>${startTime}</td>
|
||||||
@@ -1299,6 +1350,9 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
|
|||||||
<table class="monitor-table">
|
<table class="monitor-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th style="width: 40px;">
|
||||||
|
<input type="checkbox" id="monitor-select-all" onchange="toggleSelectAll(this)" />
|
||||||
|
</th>
|
||||||
<th>工具</th>
|
<th>工具</th>
|
||||||
<th>状态</th>
|
<th>状态</th>
|
||||||
<th>开始时间</th>
|
<th>开始时间</th>
|
||||||
@@ -1317,6 +1371,9 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
|
|||||||
} else {
|
} else {
|
||||||
container.appendChild(tableContainer);
|
container.appendChild(tableContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新批量操作状态
|
||||||
|
updateBatchActionsState();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染监控面板分页控件
|
// 渲染监控面板分页控件
|
||||||
@@ -1342,7 +1399,16 @@ function renderMonitorPagination() {
|
|||||||
|
|
||||||
pagination.innerHTML = `
|
pagination.innerHTML = `
|
||||||
<div class="pagination-info">
|
<div class="pagination-info">
|
||||||
显示 ${startItem}-${endItem} / 共 ${total} 条记录
|
<span>显示 ${startItem}-${endItem} / 共 ${total} 条记录</span>
|
||||||
|
<label class="pagination-page-size">
|
||||||
|
每页显示
|
||||||
|
<select id="monitor-page-size" onchange="changeMonitorPageSize()">
|
||||||
|
<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>
|
</div>
|
||||||
<div class="pagination-controls">
|
<div class="pagination-controls">
|
||||||
<button class="btn-secondary" onclick="refreshMonitorPanel(1)" ${page === 1 || total === 0 ? 'disabled' : ''}>首页</button>
|
<button class="btn-secondary" onclick="refreshMonitorPanel(1)" ${page === 1 || total === 0 ? 'disabled' : ''}>首页</button>
|
||||||
@@ -1354,6 +1420,9 @@ function renderMonitorPagination() {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
container.appendChild(pagination);
|
container.appendChild(pagination);
|
||||||
|
|
||||||
|
// 初始化每页显示数量选择器
|
||||||
|
initializeMonitorPageSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除执行记录
|
// 删除执行记录
|
||||||
@@ -1388,6 +1457,117 @@ async function deleteExecution(executionId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新批量操作状态
|
||||||
|
function updateBatchActionsState() {
|
||||||
|
const checkboxes = document.querySelectorAll('.monitor-execution-checkbox:checked');
|
||||||
|
const selectedCount = checkboxes.length;
|
||||||
|
const batchActions = document.getElementById('monitor-batch-actions');
|
||||||
|
const selectedCountSpan = document.getElementById('monitor-selected-count');
|
||||||
|
|
||||||
|
if (selectedCount > 0) {
|
||||||
|
if (batchActions) {
|
||||||
|
batchActions.style.display = 'flex';
|
||||||
|
}
|
||||||
|
if (selectedCountSpan) {
|
||||||
|
selectedCountSpan.textContent = `已选择 ${selectedCount} 项`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (batchActions) {
|
||||||
|
batchActions.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新全选复选框状态
|
||||||
|
const selectAllCheckbox = document.getElementById('monitor-select-all');
|
||||||
|
if (selectAllCheckbox) {
|
||||||
|
const allCheckboxes = document.querySelectorAll('.monitor-execution-checkbox');
|
||||||
|
const allChecked = allCheckboxes.length > 0 && Array.from(allCheckboxes).every(cb => cb.checked);
|
||||||
|
selectAllCheckbox.checked = allChecked;
|
||||||
|
selectAllCheckbox.indeterminate = selectedCount > 0 && selectedCount < allCheckboxes.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换全选
|
||||||
|
function toggleSelectAll(checkbox) {
|
||||||
|
const checkboxes = document.querySelectorAll('.monitor-execution-checkbox');
|
||||||
|
checkboxes.forEach(cb => {
|
||||||
|
cb.checked = checkbox.checked;
|
||||||
|
});
|
||||||
|
updateBatchActionsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全选
|
||||||
|
function selectAllExecutions() {
|
||||||
|
const checkboxes = document.querySelectorAll('.monitor-execution-checkbox');
|
||||||
|
checkboxes.forEach(cb => {
|
||||||
|
cb.checked = true;
|
||||||
|
});
|
||||||
|
const selectAllCheckbox = document.getElementById('monitor-select-all');
|
||||||
|
if (selectAllCheckbox) {
|
||||||
|
selectAllCheckbox.checked = true;
|
||||||
|
selectAllCheckbox.indeterminate = false;
|
||||||
|
}
|
||||||
|
updateBatchActionsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消全选
|
||||||
|
function deselectAllExecutions() {
|
||||||
|
const checkboxes = document.querySelectorAll('.monitor-execution-checkbox');
|
||||||
|
checkboxes.forEach(cb => {
|
||||||
|
cb.checked = false;
|
||||||
|
});
|
||||||
|
const selectAllCheckbox = document.getElementById('monitor-select-all');
|
||||||
|
if (selectAllCheckbox) {
|
||||||
|
selectAllCheckbox.checked = false;
|
||||||
|
selectAllCheckbox.indeterminate = false;
|
||||||
|
}
|
||||||
|
updateBatchActionsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量删除执行记录
|
||||||
|
async function batchDeleteExecutions() {
|
||||||
|
const checkboxes = document.querySelectorAll('.monitor-execution-checkbox:checked');
|
||||||
|
if (checkboxes.length === 0) {
|
||||||
|
alert('请先选择要删除的执行记录');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ids = Array.from(checkboxes).map(cb => cb.value);
|
||||||
|
const count = ids.length;
|
||||||
|
|
||||||
|
// 确认删除
|
||||||
|
if (!confirm(`确定要删除选中的 ${count} 条执行记录吗?此操作不可恢复。`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiFetch('/api/monitor/executions', {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ ids: ids })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(error.error || '批量删除执行记录失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json().catch(() => ({}));
|
||||||
|
const deletedCount = result.deleted || count;
|
||||||
|
|
||||||
|
// 删除成功后刷新当前页面
|
||||||
|
const currentPage = monitorState.pagination.page;
|
||||||
|
await refreshMonitorPanel(currentPage);
|
||||||
|
|
||||||
|
alert(`成功删除 ${deletedCount} 条执行记录`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('批量删除执行记录失败:', error);
|
||||||
|
alert('批量删除执行记录失败: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function formatExecutionDuration(start, end) {
|
function formatExecutionDuration(start, end) {
|
||||||
if (!start) {
|
if (!start) {
|
||||||
return '未知';
|
return '未知';
|
||||||
|
|||||||
@@ -292,6 +292,16 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="monitor-batch-actions" class="monitor-batch-actions" style="display: none;">
|
||||||
|
<div class="batch-actions-info">
|
||||||
|
<span id="monitor-selected-count">已选择 0 项</span>
|
||||||
|
</div>
|
||||||
|
<div class="batch-actions-buttons">
|
||||||
|
<button class="btn-secondary" onclick="selectAllExecutions()">全选</button>
|
||||||
|
<button class="btn-secondary" onclick="deselectAllExecutions()">取消全选</button>
|
||||||
|
<button class="btn-secondary btn-delete" onclick="batchDeleteExecutions()">批量删除</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="monitor-executions" class="monitor-table-container">
|
<div id="monitor-executions" class="monitor-table-container">
|
||||||
<div class="monitor-empty">加载中...</div>
|
<div class="monitor-empty">加载中...</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1005,7 +1015,7 @@
|
|||||||
<span class="modal-close" onclick="closeCreateGroupModal()">×</span>
|
<span class="modal-close" onclick="closeCreateGroupModal()">×</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body create-group-body">
|
<div class="modal-body create-group-body">
|
||||||
<p class="create-group-description">分组功能可将对话集中归类管理,并支持自定义指令,让对话更加井然有序。</p>
|
<p class="create-group-description">分组功能可将对话集中归类管理,让对话更加井然有序。</p>
|
||||||
<div class="create-group-input-wrapper">
|
<div class="create-group-input-wrapper">
|
||||||
<span class="group-icon-input">😊</span>
|
<span class="group-icon-input">😊</span>
|
||||||
<input type="text" id="create-group-name-input" placeholder="请输入分组名称" />
|
<input type="text" id="create-group-name-input" placeholder="请输入分组名称" />
|
||||||
|
|||||||
Reference in New Issue
Block a user