mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-17 21:44:43 +02:00
Add files via upload
This commit is contained in:
@@ -3,9 +3,11 @@ package database
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cyberstrike-ai/internal/mcp"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -70,13 +72,25 @@ func (db *DB) SaveToolExecution(exec *mcp.ToolExecution) error {
|
||||
}
|
||||
|
||||
// CountToolExecutions 统计工具执行记录总数
|
||||
func (db *DB) CountToolExecutions(status string) (int, error) {
|
||||
func (db *DB) CountToolExecutions(status, toolName string) (int, error) {
|
||||
query := `SELECT COUNT(*) FROM tool_executions`
|
||||
args := []interface{}{}
|
||||
conditions := []string{}
|
||||
if status != "" {
|
||||
query += ` WHERE status = ?`
|
||||
conditions = append(conditions, "status = ?")
|
||||
args = append(args, status)
|
||||
}
|
||||
if toolName != "" {
|
||||
// 支持部分匹配(模糊搜索),不区分大小写
|
||||
conditions = append(conditions, "LOWER(tool_name) LIKE ?")
|
||||
args = append(args, "%"+strings.ToLower(toolName)+"%")
|
||||
}
|
||||
if len(conditions) > 0 {
|
||||
query += ` WHERE ` + conditions[0]
|
||||
for i := 1; i < len(conditions); i++ {
|
||||
query += ` AND ` + conditions[i]
|
||||
}
|
||||
}
|
||||
var count int
|
||||
err := db.QueryRow(query, args...).Scan(&count)
|
||||
if err != nil {
|
||||
@@ -87,30 +101,43 @@ func (db *DB) CountToolExecutions(status string) (int, error) {
|
||||
|
||||
// LoadToolExecutions 加载所有工具执行记录(支持分页)
|
||||
func (db *DB) LoadToolExecutions() ([]*mcp.ToolExecution, error) {
|
||||
return db.LoadToolExecutionsWithPagination(0, 1000, "")
|
||||
return db.LoadToolExecutionsWithPagination(0, 1000, "", "")
|
||||
}
|
||||
|
||||
// LoadToolExecutionsWithPagination 分页加载工具执行记录
|
||||
// limit: 最大返回记录数,0 表示使用默认值 1000
|
||||
// offset: 跳过的记录数,用于分页
|
||||
// status: 状态筛选,空字符串表示不过滤
|
||||
func (db *DB) LoadToolExecutionsWithPagination(offset, limit int, status string) ([]*mcp.ToolExecution, error) {
|
||||
// toolName: 工具名称筛选,空字符串表示不过滤
|
||||
func (db *DB) LoadToolExecutionsWithPagination(offset, limit int, status, toolName string) ([]*mcp.ToolExecution, error) {
|
||||
if limit <= 0 {
|
||||
limit = 1000 // 默认限制
|
||||
}
|
||||
if limit > 10000 {
|
||||
limit = 10000 // 最大限制,防止一次性加载过多数据
|
||||
}
|
||||
|
||||
|
||||
query := `
|
||||
SELECT id, tool_name, arguments, status, result, error, start_time, end_time, duration_ms
|
||||
FROM tool_executions
|
||||
`
|
||||
args := []interface{}{}
|
||||
conditions := []string{}
|
||||
if status != "" {
|
||||
query += ` WHERE status = ?`
|
||||
conditions = append(conditions, "status = ?")
|
||||
args = append(args, status)
|
||||
}
|
||||
if toolName != "" {
|
||||
// 支持部分匹配(模糊搜索),不区分大小写
|
||||
conditions = append(conditions, "LOWER(tool_name) LIKE ?")
|
||||
args = append(args, "%"+strings.ToLower(toolName)+"%")
|
||||
}
|
||||
if len(conditions) > 0 {
|
||||
query += ` WHERE ` + conditions[0]
|
||||
for i := 1; i < len(conditions); i++ {
|
||||
query += ` AND ` + conditions[i]
|
||||
}
|
||||
}
|
||||
query += ` ORDER BY start_time DESC LIMIT ? OFFSET ?`
|
||||
args = append(args, limit, offset)
|
||||
|
||||
|
||||
+21
-12
@@ -3,6 +3,7 @@ package handler
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cyberstrike-ai/internal/database"
|
||||
@@ -66,8 +67,10 @@ func (h *MonitorHandler) Monitor(c *gin.Context) {
|
||||
|
||||
// 解析状态筛选参数
|
||||
status := c.Query("status")
|
||||
// 解析工具筛选参数
|
||||
toolName := c.Query("tool")
|
||||
|
||||
executions, total := h.loadExecutionsWithPagination(page, pageSize, status)
|
||||
executions, total := h.loadExecutionsWithPagination(page, pageSize, status, toolName)
|
||||
stats := h.loadStats()
|
||||
|
||||
totalPages := (total + pageSize - 1) / pageSize
|
||||
@@ -87,18 +90,21 @@ func (h *MonitorHandler) Monitor(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *MonitorHandler) loadExecutions() []*mcp.ToolExecution {
|
||||
executions, _ := h.loadExecutionsWithPagination(1, 1000, "")
|
||||
executions, _ := h.loadExecutionsWithPagination(1, 1000, "", "")
|
||||
return executions
|
||||
}
|
||||
|
||||
func (h *MonitorHandler) loadExecutionsWithPagination(page, pageSize int, status string) ([]*mcp.ToolExecution, int) {
|
||||
func (h *MonitorHandler) loadExecutionsWithPagination(page, pageSize int, status, toolName string) ([]*mcp.ToolExecution, int) {
|
||||
if h.db == nil {
|
||||
allExecutions := h.mcpServer.GetAllExecutions()
|
||||
// 如果指定了状态筛选,先进行筛选
|
||||
if status != "" {
|
||||
// 如果指定了状态筛选或工具筛选,先进行筛选
|
||||
if status != "" || toolName != "" {
|
||||
filtered := make([]*mcp.ToolExecution, 0)
|
||||
for _, exec := range allExecutions {
|
||||
if exec.Status == status {
|
||||
matchStatus := status == "" || exec.Status == status
|
||||
// 支持部分匹配(模糊搜索)
|
||||
matchTool := toolName == "" || strings.Contains(strings.ToLower(exec.ToolName), strings.ToLower(toolName))
|
||||
if matchStatus && matchTool {
|
||||
filtered = append(filtered, exec)
|
||||
}
|
||||
}
|
||||
@@ -117,15 +123,18 @@ func (h *MonitorHandler) loadExecutionsWithPagination(page, pageSize int, status
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
executions, err := h.db.LoadToolExecutionsWithPagination(offset, pageSize, status)
|
||||
executions, err := h.db.LoadToolExecutionsWithPagination(offset, pageSize, status, toolName)
|
||||
if err != nil {
|
||||
h.logger.Warn("从数据库加载执行记录失败,回退到内存数据", zap.Error(err))
|
||||
allExecutions := h.mcpServer.GetAllExecutions()
|
||||
// 如果指定了状态筛选,先进行筛选
|
||||
if status != "" {
|
||||
// 如果指定了状态筛选或工具筛选,先进行筛选
|
||||
if status != "" || toolName != "" {
|
||||
filtered := make([]*mcp.ToolExecution, 0)
|
||||
for _, exec := range allExecutions {
|
||||
if exec.Status == status {
|
||||
matchStatus := status == "" || exec.Status == status
|
||||
// 支持部分匹配(模糊搜索)
|
||||
matchTool := toolName == "" || strings.Contains(strings.ToLower(exec.ToolName), strings.ToLower(toolName))
|
||||
if matchStatus && matchTool {
|
||||
filtered = append(filtered, exec)
|
||||
}
|
||||
}
|
||||
@@ -143,8 +152,8 @@ func (h *MonitorHandler) loadExecutionsWithPagination(page, pageSize int, status
|
||||
return allExecutions[offset:end], total
|
||||
}
|
||||
|
||||
// 获取总数(考虑状态筛选)
|
||||
total, err := h.db.CountToolExecutions(status)
|
||||
// 获取总数(考虑状态筛选和工具筛选)
|
||||
total, err := h.db.CountToolExecutions(status, toolName)
|
||||
if err != nil {
|
||||
h.logger.Warn("获取执行记录总数失败", zap.Error(err))
|
||||
// 回退:使用已加载的记录数估算
|
||||
|
||||
@@ -639,7 +639,12 @@ func (m *Manager) UpdateItem(id, category, title, content string) (*KnowledgeIte
|
||||
// 删除旧目录(如果为空)
|
||||
oldDir := filepath.Dir(item.FilePath)
|
||||
if entries, err := os.ReadDir(oldDir); err == nil && len(entries) == 0 {
|
||||
os.Remove(oldDir)
|
||||
// 只有当目录不是知识库根目录时才删除(避免删除根目录)
|
||||
if oldDir != m.basePath {
|
||||
if err := os.Remove(oldDir); err != nil {
|
||||
m.logger.Warn("删除空目录失败", zap.String("dir", oldDir), zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -686,6 +691,17 @@ func (m *Manager) DeleteItem(id string) error {
|
||||
return fmt.Errorf("删除知识项失败: %w", err)
|
||||
}
|
||||
|
||||
// 删除空目录(如果为空)
|
||||
dir := filepath.Dir(filePath)
|
||||
if entries, err := os.ReadDir(dir); err == nil && len(entries) == 0 {
|
||||
// 只有当目录不是知识库根目录时才删除(避免删除根目录)
|
||||
if dir != m.basePath {
|
||||
if err := os.Remove(dir); err != nil {
|
||||
m.logger.Warn("删除空目录失败", zap.String("dir", dir), zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -3192,6 +3192,47 @@ header {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
min-width: 140px;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.monitor-section .section-actions select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 0 2px rgba(0, 102, 255, 0.1);
|
||||
}
|
||||
|
||||
.monitor-section .section-actions select:hover {
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.monitor-section .section-actions input[type="text"] {
|
||||
margin-left: 6px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s;
|
||||
min-width: 180px;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.monitor-section .section-actions input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 0 2px rgba(0, 102, 255, 0.1);
|
||||
}
|
||||
|
||||
.monitor-section .section-actions input[type="text"]:hover {
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.monitor-section .section-actions input[type="text"]::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.monitor-stats-grid {
|
||||
|
||||
+44
-21
@@ -3772,6 +3772,7 @@ let contextMenuConversationId = null;
|
||||
let contextMenuGroupId = null;
|
||||
let groupsCache = [];
|
||||
let conversationGroupMappingCache = {};
|
||||
let pendingGroupMappings = {}; // 待保留的分组映射(用于处理后端API延迟的情况)
|
||||
|
||||
// 加载分组列表
|
||||
async function loadGroups() {
|
||||
@@ -3901,13 +3902,10 @@ async function loadConversationsWithGroups(searchQuery = '') {
|
||||
}
|
||||
|
||||
// 如果没有搜索关键词,使用原有逻辑
|
||||
// 如果对话在某个分组中,且当前不在分组详情页,则跳过
|
||||
if (currentGroupId === null && conversationGroupMappingCache[conv.id]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果当前在分组详情页,只显示该分组的对话
|
||||
if (currentGroupId !== null && conversationGroupMappingCache[conv.id] !== currentGroupId) {
|
||||
// "最近对话"列表应该只显示不在任何分组中的对话
|
||||
// 无论是否在分组详情页,都不应该在"最近对话"中显示分组中的对话
|
||||
if (conversationGroupMappingCache[conv.id]) {
|
||||
// 对话在某个分组中,不应该显示在"最近对话"列表中
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4050,8 +4048,12 @@ async function showConversationContextMenu(event) {
|
||||
if (convId) {
|
||||
try {
|
||||
let isPinned = false;
|
||||
if (currentGroupId) {
|
||||
// 如果在分组详情页面,获取分组内置顶状态
|
||||
// 检查对话是否真的在当前分组中
|
||||
const conversationGroupId = conversationGroupMappingCache[convId];
|
||||
const isInCurrentGroup = currentGroupId && conversationGroupId === currentGroupId;
|
||||
|
||||
if (isInCurrentGroup) {
|
||||
// 对话在当前分组中,获取分组内置顶状态
|
||||
const response = await apiFetch(`/api/groups/${currentGroupId}/conversations`);
|
||||
if (response.ok) {
|
||||
const groupConvs = await response.json();
|
||||
@@ -4061,7 +4063,7 @@ async function showConversationContextMenu(event) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 不在分组详情页面,获取全局置顶状态
|
||||
// 不在分组详情页面,或者对话不在当前分组中,获取全局置顶状态
|
||||
const response = await apiFetch(`/api/conversations/${convId}`);
|
||||
if (response.ok) {
|
||||
const conv = await response.json();
|
||||
@@ -4316,8 +4318,14 @@ async function pinConversation() {
|
||||
if (!convId) return;
|
||||
|
||||
try {
|
||||
// 如果当前在分组详情页面,使用分组内置顶
|
||||
if (currentGroupId) {
|
||||
// 检查对话是否真的在当前分组中
|
||||
// 如果对话已经从分组移出,conversationGroupMappingCache 中不会有该对话的映射
|
||||
// 或者映射的分组ID不等于当前分组ID
|
||||
const conversationGroupId = conversationGroupMappingCache[convId];
|
||||
const isInCurrentGroup = currentGroupId && conversationGroupId === currentGroupId;
|
||||
|
||||
// 如果当前在分组详情页面,且对话确实在当前分组中,使用分组内置顶
|
||||
if (isInCurrentGroup) {
|
||||
// 获取当前对话在分组中的置顶状态
|
||||
const response = await apiFetch(`/api/groups/${currentGroupId}/conversations`);
|
||||
const groupConvs = await response.json();
|
||||
@@ -4339,7 +4347,7 @@ async function pinConversation() {
|
||||
// 重新加载分组对话
|
||||
loadGroupConversations(currentGroupId);
|
||||
} else {
|
||||
// 不在分组详情页面,使用全局置顶
|
||||
// 不在分组详情页面,或者对话不在当前分组中,使用全局置顶
|
||||
const response = await apiFetch(`/api/conversations/${convId}`);
|
||||
const conv = await response.json();
|
||||
const newPinned = !conv.pinned;
|
||||
@@ -4629,27 +4637,30 @@ async function moveConversationToGroup(convId, groupId) {
|
||||
const oldGroupId = conversationGroupMappingCache[convId];
|
||||
conversationGroupMappingCache[convId] = groupId;
|
||||
|
||||
// 将新移动的对话添加到待保留映射中,防止后端API延迟导致映射丢失
|
||||
pendingGroupMappings[convId] = groupId;
|
||||
|
||||
// 如果移动的是当前对话,更新 currentConversationGroupId
|
||||
if (currentConversationId === convId) {
|
||||
currentConversationGroupId = groupId;
|
||||
}
|
||||
|
||||
// 重新加载分组映射缓存,确保数据同步
|
||||
await loadConversationGroupMapping();
|
||||
|
||||
// 如果当前在分组详情页面,重新加载分组对话
|
||||
if (currentGroupId) {
|
||||
// 如果从当前分组移出,或者移动到当前分组,都需要重新加载
|
||||
if (currentGroupId === oldGroupId || currentGroupId === groupId) {
|
||||
await loadGroupConversations(currentGroupId);
|
||||
}
|
||||
} else {
|
||||
// 如果不在分组详情页面,刷新最近对话列表
|
||||
loadConversationsWithGroups();
|
||||
}
|
||||
|
||||
// 如果旧分组和新分组不同,且用户正在查看旧分组,也需要刷新旧分组
|
||||
// 但上面的逻辑已经处理了这种情况(currentGroupId === oldGroupId)
|
||||
// 无论是否在分组详情页面,都需要刷新最近对话列表
|
||||
// 因为最近对话列表会根据分组映射缓存来过滤显示,需要立即更新
|
||||
// loadConversationsWithGroups 内部会调用 loadConversationGroupMapping,
|
||||
// loadConversationGroupMapping 会保留 pendingGroupMappings 中的映射
|
||||
await loadConversationsWithGroups();
|
||||
|
||||
// 注意:pendingGroupMappings 中的映射会在下次 loadConversationGroupMapping
|
||||
// 成功从后端加载时自动清理(在 loadConversationGroupMapping 中处理)
|
||||
|
||||
// 刷新分组列表,更新高亮状态
|
||||
await loadGroups();
|
||||
@@ -4670,6 +4681,8 @@ async function removeConversationFromGroup(convId, groupId) {
|
||||
|
||||
// 更新缓存 - 立即删除,确保后续加载时能正确识别
|
||||
delete conversationGroupMappingCache[convId];
|
||||
// 同时从待保留映射中移除
|
||||
delete pendingGroupMappings[convId];
|
||||
|
||||
// 如果移除的是当前对话,清除 currentConversationGroupId
|
||||
if (currentConversationId === convId) {
|
||||
@@ -4719,6 +4732,9 @@ async function loadConversationGroupMapping() {
|
||||
groups = [];
|
||||
}
|
||||
|
||||
// 保存待保留的映射
|
||||
const preservedMappings = { ...pendingGroupMappings };
|
||||
|
||||
conversationGroupMappingCache = {};
|
||||
|
||||
for (const group of groups) {
|
||||
@@ -4728,9 +4744,16 @@ async function loadConversationGroupMapping() {
|
||||
if (Array.isArray(conversations)) {
|
||||
conversations.forEach(conv => {
|
||||
conversationGroupMappingCache[conv.id] = group.id;
|
||||
// 如果这个对话在待保留映射中,从待保留映射中移除(因为已经从后端加载了)
|
||||
if (preservedMappings[conv.id] === group.id) {
|
||||
delete pendingGroupMappings[conv.id];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复待保留的映射(这些是后端API尚未同步的映射)
|
||||
Object.assign(conversationGroupMappingCache, preservedMappings);
|
||||
} catch (error) {
|
||||
console.error('加载对话分组映射失败:', error);
|
||||
}
|
||||
|
||||
+39
-11
@@ -974,12 +974,17 @@ async function refreshMonitorPanel(page = null) {
|
||||
|
||||
// 获取当前的筛选条件
|
||||
const statusFilter = document.getElementById('monitor-status-filter');
|
||||
const currentFilter = statusFilter ? statusFilter.value : 'all';
|
||||
const toolFilter = document.getElementById('monitor-tool-filter');
|
||||
const currentStatusFilter = statusFilter ? statusFilter.value : 'all';
|
||||
const currentToolFilter = toolFilter ? (toolFilter.value.trim() || 'all') : 'all';
|
||||
|
||||
// 构建请求 URL
|
||||
let url = `/api/monitor?page=${currentPage}&page_size=${pageSize}`;
|
||||
if (currentFilter && currentFilter !== 'all') {
|
||||
url += `&status=${encodeURIComponent(currentFilter)}`;
|
||||
if (currentStatusFilter && currentStatusFilter !== 'all') {
|
||||
url += `&status=${encodeURIComponent(currentStatusFilter)}`;
|
||||
}
|
||||
if (currentToolFilter && currentToolFilter !== 'all') {
|
||||
url += `&tool=${encodeURIComponent(currentToolFilter)}`;
|
||||
}
|
||||
|
||||
const response = await apiFetch(url, { method: 'GET' });
|
||||
@@ -1003,7 +1008,7 @@ async function refreshMonitorPanel(page = null) {
|
||||
}
|
||||
|
||||
renderMonitorStats(monitorState.stats, monitorState.lastFetchedAt);
|
||||
renderMonitorExecutions(monitorState.executions, currentFilter);
|
||||
renderMonitorExecutions(monitorState.executions, currentStatusFilter);
|
||||
renderMonitorPagination();
|
||||
} catch (error) {
|
||||
console.error('刷新监控面板失败:', error);
|
||||
@@ -1016,14 +1021,30 @@ async function refreshMonitorPanel(page = null) {
|
||||
}
|
||||
}
|
||||
|
||||
async function applyMonitorFilters() {
|
||||
const statusFilter = document.getElementById('monitor-status-filter');
|
||||
const status = statusFilter ? statusFilter.value : 'all';
|
||||
// 当筛选条件改变时,从后端重新获取数据
|
||||
await refreshMonitorPanelWithFilter(status);
|
||||
// 处理工具搜索输入(防抖)
|
||||
let toolFilterDebounceTimer = null;
|
||||
function handleToolFilterInput() {
|
||||
// 清除之前的定时器
|
||||
if (toolFilterDebounceTimer) {
|
||||
clearTimeout(toolFilterDebounceTimer);
|
||||
}
|
||||
|
||||
// 设置新的定时器,500ms后执行筛选
|
||||
toolFilterDebounceTimer = setTimeout(() => {
|
||||
applyMonitorFilters();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function refreshMonitorPanelWithFilter(statusFilter = 'all') {
|
||||
async function applyMonitorFilters() {
|
||||
const statusFilter = document.getElementById('monitor-status-filter');
|
||||
const toolFilter = document.getElementById('monitor-tool-filter');
|
||||
const status = statusFilter ? statusFilter.value : 'all';
|
||||
const tool = toolFilter ? (toolFilter.value.trim() || 'all') : 'all';
|
||||
// 当筛选条件改变时,从后端重新获取数据
|
||||
await refreshMonitorPanelWithFilter(status, tool);
|
||||
}
|
||||
|
||||
async function refreshMonitorPanelWithFilter(statusFilter = 'all', toolFilter = 'all') {
|
||||
const statsContainer = document.getElementById('monitor-stats');
|
||||
const execContainer = document.getElementById('monitor-executions');
|
||||
|
||||
@@ -1036,6 +1057,9 @@ async function refreshMonitorPanelWithFilter(statusFilter = 'all') {
|
||||
if (statusFilter && statusFilter !== 'all') {
|
||||
url += `&status=${encodeURIComponent(statusFilter)}`;
|
||||
}
|
||||
if (toolFilter && toolFilter !== 'all') {
|
||||
url += `&tool=${encodeURIComponent(toolFilter)}`;
|
||||
}
|
||||
|
||||
const response = await apiFetch(url, { method: 'GET' });
|
||||
const result = await response.json().catch(() => ({}));
|
||||
@@ -1071,6 +1095,7 @@ async function refreshMonitorPanelWithFilter(statusFilter = 'all') {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function renderMonitorStats(statsMap = {}, lastFetchedAt = null) {
|
||||
const container = document.getElementById('monitor-stats');
|
||||
if (!container) {
|
||||
@@ -1151,7 +1176,10 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
|
||||
|
||||
if (!Array.isArray(executions) || executions.length === 0) {
|
||||
// 根据是否有筛选条件显示不同的提示
|
||||
if (statusFilter && statusFilter !== 'all') {
|
||||
const toolFilter = document.getElementById('monitor-tool-filter');
|
||||
const currentToolFilter = toolFilter ? toolFilter.value : 'all';
|
||||
const hasFilter = (statusFilter && statusFilter !== 'all') || (currentToolFilter && currentToolFilter !== 'all');
|
||||
if (hasFilter) {
|
||||
container.innerHTML = '<div class="monitor-empty">当前筛选条件下暂无记录</div>';
|
||||
} else {
|
||||
container.innerHTML = '<div class="monitor-empty">暂无执行记录</div>';
|
||||
|
||||
@@ -248,7 +248,7 @@
|
||||
<div id="chat-messages" class="chat-messages"></div>
|
||||
<div class="chat-input-container">
|
||||
<div class="chat-input-field">
|
||||
<textarea id="chat-input" placeholder="输入测试目标或命令... (Shift+Enter 换行,Enter 发送)" rows="1"></textarea>
|
||||
<textarea id="chat-input" placeholder="输入测试目标或命令... (输入 @ 选择工具 | Shift+Enter 换行,Enter 发送)" rows="1"></textarea>
|
||||
<div id="mention-suggestions" class="mention-suggestions" role="listbox" aria-label="工具提及候选"></div>
|
||||
</div>
|
||||
<button onclick="sendMessage()">发送</button>
|
||||
@@ -277,6 +277,10 @@
|
||||
<div class="section-header">
|
||||
<h3>最新执行记录</h3>
|
||||
<div class="section-actions">
|
||||
<label>
|
||||
工具搜索
|
||||
<input type="text" id="monitor-tool-filter" placeholder="输入工具名称..." oninput="handleToolFilterInput()" onkeydown="if(event.key==='Enter') applyMonitorFilters()" />
|
||||
</label>
|
||||
<label>
|
||||
状态筛选
|
||||
<select id="monitor-status-filter" onchange="applyMonitorFilters()">
|
||||
|
||||
Reference in New Issue
Block a user