mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-01 23:35:18 +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/execution/:id", monitorHandler.GetExecution)
|
||||
protected.DELETE("/monitor/execution/:id", monitorHandler.DeleteExecution)
|
||||
protected.DELETE("/monitor/executions", monitorHandler.DeleteExecutions)
|
||||
protected.GET("/monitor/stats", monitorHandler.GetStats)
|
||||
|
||||
// 配置管理
|
||||
|
||||
@@ -281,6 +281,117 @@ func (db *DB) DeleteToolExecution(id string) error {
|
||||
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 保存工具统计信息
|
||||
func (db *DB) SaveToolStats(toolName string, stats *mcp.ToolStats) error {
|
||||
var lastCallTime sql.NullTime
|
||||
|
||||
@@ -307,4 +307,79 @@ func (h *MonitorHandler) DeleteExecution(c *gin.Context) {
|
||||
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 {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
@@ -3056,6 +3059,36 @@ header {
|
||||
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 {
|
||||
padding: 6px 12px;
|
||||
font-size: 0.875rem;
|
||||
@@ -3414,6 +3447,50 @@ header {
|
||||
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 {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
|
||||
+182
-2
@@ -1008,7 +1008,11 @@ const monitorState = {
|
||||
lastFetchedAt: null,
|
||||
pagination: {
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
pageSize: (() => {
|
||||
// 从 localStorage 读取保存的每页显示数量,默认为 20
|
||||
const saved = localStorage.getItem('monitorPageSize');
|
||||
return saved ? parseInt(saved, 10) : 20;
|
||||
})(),
|
||||
total: 0,
|
||||
totalPages: 0
|
||||
}
|
||||
@@ -1019,6 +1023,39 @@ function openMonitorPanel() {
|
||||
if (typeof switchPage === 'function') {
|
||||
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() {
|
||||
@@ -1076,6 +1113,9 @@ async function refreshMonitorPanel(page = null) {
|
||||
renderMonitorStats(monitorState.stats, monitorState.lastFetchedAt);
|
||||
renderMonitorExecutions(monitorState.executions, currentStatusFilter);
|
||||
renderMonitorPagination();
|
||||
|
||||
// 初始化每页显示数量选择器
|
||||
initializeMonitorPageSize();
|
||||
} catch (error) {
|
||||
console.error('刷新监控面板失败:', error);
|
||||
if (statsContainer) {
|
||||
@@ -1150,6 +1190,9 @@ async function refreshMonitorPanelWithFilter(statusFilter = 'all', toolFilter =
|
||||
renderMonitorStats(monitorState.stats, monitorState.lastFetchedAt);
|
||||
renderMonitorExecutions(monitorState.executions, statusFilter);
|
||||
renderMonitorPagination();
|
||||
|
||||
// 初始化每页显示数量选择器
|
||||
initializeMonitorPageSize();
|
||||
} catch (error) {
|
||||
console.error('刷新监控面板失败:', error);
|
||||
if (statsContainer) {
|
||||
@@ -1250,6 +1293,11 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
|
||||
} else {
|
||||
container.innerHTML = '<div class="monitor-empty">暂无执行记录</div>';
|
||||
}
|
||||
// 隐藏批量操作栏
|
||||
const batchActions = document.getElementById('monitor-batch-actions');
|
||||
if (batchActions) {
|
||||
batchActions.style.display = 'none';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1266,6 +1314,9 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
|
||||
const executionId = escapeHtml(exec.id || '');
|
||||
return `
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" class="monitor-execution-checkbox" value="${executionId}" onchange="updateBatchActionsState()" />
|
||||
</td>
|
||||
<td>${toolName}</td>
|
||||
<td><span class="${statusClass}">${statusLabel}</span></td>
|
||||
<td>${startTime}</td>
|
||||
@@ -1299,6 +1350,9 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
|
||||
<table class="monitor-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 40px;">
|
||||
<input type="checkbox" id="monitor-select-all" onchange="toggleSelectAll(this)" />
|
||||
</th>
|
||||
<th>工具</th>
|
||||
<th>状态</th>
|
||||
<th>开始时间</th>
|
||||
@@ -1317,6 +1371,9 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
|
||||
} else {
|
||||
container.appendChild(tableContainer);
|
||||
}
|
||||
|
||||
// 更新批量操作状态
|
||||
updateBatchActionsState();
|
||||
}
|
||||
|
||||
// 渲染监控面板分页控件
|
||||
@@ -1342,7 +1399,16 @@ function renderMonitorPagination() {
|
||||
|
||||
pagination.innerHTML = `
|
||||
<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 class="pagination-controls">
|
||||
<button class="btn-secondary" onclick="refreshMonitorPanel(1)" ${page === 1 || total === 0 ? 'disabled' : ''}>首页</button>
|
||||
@@ -1354,6 +1420,9 @@ function renderMonitorPagination() {
|
||||
`;
|
||||
|
||||
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) {
|
||||
if (!start) {
|
||||
return '未知';
|
||||
|
||||
@@ -292,6 +292,16 @@
|
||||
</label>
|
||||
</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 class="monitor-empty">加载中...</div>
|
||||
</div>
|
||||
@@ -1005,7 +1015,7 @@
|
||||
<span class="modal-close" onclick="closeCreateGroupModal()">×</span>
|
||||
</div>
|
||||
<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">
|
||||
<span class="group-icon-input">😊</span>
|
||||
<input type="text" id="create-group-name-input" placeholder="请输入分组名称" />
|
||||
|
||||
Reference in New Issue
Block a user