mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-04-21 10:16:32 +02:00
Add files via upload
This commit is contained in:
@@ -195,10 +195,21 @@ func (db *DB) DeleteGroup(id string) error {
|
||||
}
|
||||
|
||||
// AddConversationToGroup 将对话添加到分组
|
||||
// 注意:一个对话只能属于一个分组,所以在添加新分组之前,会先删除该对话的所有旧分组关联
|
||||
func (db *DB) AddConversationToGroup(conversationID, groupID string) error {
|
||||
id := uuid.New().String()
|
||||
// 先删除该对话的所有旧分组关联,确保一个对话只属于一个分组
|
||||
_, err := db.Exec(
|
||||
"INSERT OR REPLACE INTO conversation_group_mappings (id, conversation_id, group_id, created_at) VALUES (?, ?, ?, ?)",
|
||||
"DELETE FROM conversation_group_mappings WHERE conversation_id = ?",
|
||||
conversationID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除对话旧分组关联失败: %w", err)
|
||||
}
|
||||
|
||||
// 然后插入新的分组关联
|
||||
id := uuid.New().String()
|
||||
_, err = db.Exec(
|
||||
"INSERT INTO conversation_group_mappings (id, conversation_id, group_id, created_at) VALUES (?, ?, ?, ?)",
|
||||
id, conversationID, groupID, time.Now(),
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -90,7 +90,7 @@ func (db *DB) GetVulnerability(id string) (*Vulnerability, error) {
|
||||
}
|
||||
|
||||
// ListVulnerabilities 列出漏洞
|
||||
func (db *DB) ListVulnerabilities(limit, offset int, conversationID, severity, status string) ([]*Vulnerability, error) {
|
||||
func (db *DB) ListVulnerabilities(limit, offset int, id, conversationID, severity, status string) ([]*Vulnerability, error) {
|
||||
query := `
|
||||
SELECT id, conversation_id, title, description, severity, status,
|
||||
vulnerability_type, target, proof, impact, recommendation,
|
||||
@@ -100,6 +100,10 @@ func (db *DB) ListVulnerabilities(limit, offset int, conversationID, severity, s
|
||||
`
|
||||
args := []interface{}{}
|
||||
|
||||
if id != "" {
|
||||
query += " AND id = ?"
|
||||
args = append(args, id)
|
||||
}
|
||||
if conversationID != "" {
|
||||
query += " AND conversation_id = ?"
|
||||
args = append(args, conversationID)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
"time"
|
||||
|
||||
"cyberstrike-ai/internal/agent"
|
||||
@@ -16,6 +17,47 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// safeTruncateString 安全截断字符串,避免在 UTF-8 字符中间截断
|
||||
func safeTruncateString(s string, maxLen int) string {
|
||||
if maxLen <= 0 {
|
||||
return ""
|
||||
}
|
||||
if utf8.RuneCountInString(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
|
||||
// 将字符串转换为 rune 切片以正确计算字符数
|
||||
runes := []rune(s)
|
||||
if len(runes) <= maxLen {
|
||||
return s
|
||||
}
|
||||
|
||||
// 截断到最大长度
|
||||
truncated := string(runes[:maxLen])
|
||||
|
||||
// 尝试在标点符号或空格处截断,使截断更自然
|
||||
// 在截断点往前查找合适的断点(不超过20%的长度)
|
||||
searchRange := maxLen / 5
|
||||
if searchRange > maxLen {
|
||||
searchRange = maxLen
|
||||
}
|
||||
breakChars := []rune(",。、 ,.;:!?!?/\\-_")
|
||||
bestBreakPos := len(runes[:maxLen])
|
||||
|
||||
for i := bestBreakPos - 1; i >= bestBreakPos-searchRange && i >= 0; i-- {
|
||||
for _, breakChar := range breakChars {
|
||||
if runes[i] == breakChar {
|
||||
bestBreakPos = i + 1 // 在标点符号后断开
|
||||
goto found
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
found:
|
||||
truncated = string(runes[:bestBreakPos])
|
||||
return truncated + "..."
|
||||
}
|
||||
|
||||
// AgentHandler Agent处理器
|
||||
type AgentHandler struct {
|
||||
agent *agent.Agent
|
||||
@@ -74,10 +116,7 @@ func (h *AgentHandler) AgentLoop(c *gin.Context) {
|
||||
// 如果没有对话ID,创建新对话
|
||||
conversationID := req.ConversationID
|
||||
if conversationID == "" {
|
||||
title := req.Message
|
||||
if len(title) > 50 {
|
||||
title = title[:50] + "..."
|
||||
}
|
||||
title := safeTruncateString(req.Message, 50)
|
||||
conv, err := h.db.CreateConversation(title)
|
||||
if err != nil {
|
||||
h.logger.Error("创建对话失败", zap.Error(err))
|
||||
@@ -237,10 +276,7 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
|
||||
// 如果没有对话ID,创建新对话
|
||||
conversationID := req.ConversationID
|
||||
if conversationID == "" {
|
||||
title := req.Message
|
||||
if len(title) > 50 {
|
||||
title = title[:50] + "..."
|
||||
}
|
||||
title := safeTruncateString(req.Message, 50)
|
||||
conv, err := h.db.CreateConversation(title)
|
||||
if err != nil {
|
||||
h.logger.Error("创建对话失败", zap.Error(err))
|
||||
|
||||
@@ -86,6 +86,7 @@ func (h *VulnerabilityHandler) GetVulnerability(c *gin.Context) {
|
||||
func (h *VulnerabilityHandler) ListVulnerabilities(c *gin.Context) {
|
||||
limitStr := c.DefaultQuery("limit", "50")
|
||||
offsetStr := c.DefaultQuery("offset", "0")
|
||||
id := c.Query("id")
|
||||
conversationID := c.Query("conversation_id")
|
||||
severity := c.Query("severity")
|
||||
status := c.Query("status")
|
||||
@@ -97,7 +98,7 @@ func (h *VulnerabilityHandler) ListVulnerabilities(c *gin.Context) {
|
||||
limit = 50
|
||||
}
|
||||
|
||||
vulnerabilities, err := h.db.ListVulnerabilities(limit, offset, conversationID, severity, status)
|
||||
vulnerabilities, err := h.db.ListVulnerabilities(limit, offset, id, conversationID, severity, status)
|
||||
if err != nil {
|
||||
h.logger.Error("获取漏洞列表失败", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
|
||||
@@ -5427,8 +5427,10 @@ header {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-overflow: clip;
|
||||
white-space: nowrap;
|
||||
word-break: keep-all;
|
||||
/* 完全依赖JavaScript截断,禁用CSS的ellipsis以避免在UTF-8多字节字符中间截断 */
|
||||
}
|
||||
|
||||
.batch-table-col-time {
|
||||
|
||||
+70
-8
@@ -1356,7 +1356,9 @@ function createConversationListItem(conversation) {
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'conversation-title';
|
||||
title.textContent = conversation.title || '未命名对话';
|
||||
const titleText = conversation.title || '未命名对话';
|
||||
title.textContent = safeTruncateText(titleText, 60);
|
||||
title.title = titleText; // 设置完整标题以便悬停查看
|
||||
contentWrapper.appendChild(title);
|
||||
|
||||
const time = document.createElement('div');
|
||||
@@ -3916,7 +3918,9 @@ function createConversationListItemWithMenu(conversation, isPinned) {
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'conversation-title';
|
||||
title.textContent = conversation.title || '未命名对话';
|
||||
const titleText = conversation.title || '未命名对话';
|
||||
title.textContent = safeTruncateText(titleText, 60);
|
||||
title.title = titleText; // 设置完整标题以便悬停查看
|
||||
titleWrapper.appendChild(title);
|
||||
|
||||
if (isPinned) {
|
||||
@@ -4394,8 +4398,13 @@ async function showMoveToGroupSubmenu() {
|
||||
groupsCache = [];
|
||||
}
|
||||
|
||||
// 如果有分组,显示所有分组(排除当前分组)
|
||||
// 如果有分组,显示所有分组(排除对话已所在的分组)
|
||||
if (groupsCache.length > 0) {
|
||||
// 检查对话当前所在的分组ID
|
||||
const conversationCurrentGroupId = contextMenuConversationId
|
||||
? conversationGroupMappingCache[contextMenuConversationId]
|
||||
: null;
|
||||
|
||||
groupsCache.forEach(group => {
|
||||
// 验证分组对象是否有效
|
||||
if (!group || !group.id || !group.name) {
|
||||
@@ -4403,8 +4412,8 @@ async function showMoveToGroupSubmenu() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果当前在分组详情页面,不显示当前分组
|
||||
if (currentGroupId && group.id === currentGroupId) {
|
||||
// 如果对话已经在当前分组中,不显示该分组(因为已经在里面了)
|
||||
if (conversationCurrentGroupId && group.id === conversationCurrentGroupId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4570,16 +4579,23 @@ async function moveConversationToGroup(convId, groupId) {
|
||||
currentConversationGroupId = groupId;
|
||||
}
|
||||
|
||||
// 重新加载分组映射缓存,确保数据同步
|
||||
await loadConversationGroupMapping();
|
||||
|
||||
// 如果当前在分组详情页面,重新加载分组对话
|
||||
if (currentGroupId) {
|
||||
// 如果从当前分组移出,或者移动到当前分组,都需要重新加载
|
||||
if (currentGroupId === oldGroupId || currentGroupId === groupId) {
|
||||
loadGroupConversations(currentGroupId);
|
||||
await loadGroupConversations(currentGroupId);
|
||||
}
|
||||
} else {
|
||||
// 如果不在分组详情页面,刷新最近对话列表
|
||||
loadConversationsWithGroups();
|
||||
}
|
||||
|
||||
// 如果旧分组和新分组不同,且用户正在查看旧分组,也需要刷新旧分组
|
||||
// 但上面的逻辑已经处理了这种情况(currentGroupId === oldGroupId)
|
||||
|
||||
// 刷新分组列表,更新高亮状态
|
||||
await loadGroups();
|
||||
} catch (error) {
|
||||
@@ -4718,6 +4734,45 @@ async function showBatchManageModal() {
|
||||
}
|
||||
}
|
||||
|
||||
// 安全截断中文字符串,避免在汉字中间截断
|
||||
function safeTruncateText(text, maxLength = 50) {
|
||||
if (!text || typeof text !== 'string') {
|
||||
return text || '';
|
||||
}
|
||||
|
||||
// 使用 Array.from 将字符串转换为字符数组(正确处理 Unicode 代理对)
|
||||
const chars = Array.from(text);
|
||||
|
||||
// 如果文本长度未超过限制,直接返回
|
||||
if (chars.length <= maxLength) {
|
||||
return text;
|
||||
}
|
||||
|
||||
// 截断到最大长度(基于字符数,而不是代码单元)
|
||||
let truncatedChars = chars.slice(0, maxLength);
|
||||
|
||||
// 尝试在标点符号或空格处截断,使截断更自然
|
||||
// 在截断点往前查找合适的断点(不超过20%的长度)
|
||||
const searchRange = Math.floor(maxLength * 0.2);
|
||||
const breakChars = [',', '。', '、', ' ', ',', '.', ';', ':', '!', '?', '!', '?', '/', '\\', '-', '_'];
|
||||
let bestBreakPos = truncatedChars.length;
|
||||
|
||||
for (let i = truncatedChars.length - 1; i >= truncatedChars.length - searchRange && i >= 0; i--) {
|
||||
if (breakChars.includes(truncatedChars[i])) {
|
||||
bestBreakPos = i + 1; // 在标点符号后断开
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找到合适的断点,使用它;否则使用原截断位置
|
||||
if (bestBreakPos < truncatedChars.length) {
|
||||
truncatedChars = truncatedChars.slice(0, bestBreakPos);
|
||||
}
|
||||
|
||||
// 将字符数组转换回字符串,并添加省略号
|
||||
return truncatedChars.join('') + '...';
|
||||
}
|
||||
|
||||
// 渲染批量管理对话列表
|
||||
function renderBatchConversations(filtered = null) {
|
||||
const list = document.getElementById('batch-conversations-list');
|
||||
@@ -4738,7 +4793,12 @@ function renderBatchConversations(filtered = null) {
|
||||
|
||||
const name = document.createElement('div');
|
||||
name.className = 'batch-table-col-name';
|
||||
name.textContent = conv.title || '未命名对话';
|
||||
const originalTitle = conv.title || '未命名对话';
|
||||
// 使用安全截断函数,限制最大长度为45个字符(留出空间显示省略号)
|
||||
const truncatedTitle = safeTruncateText(originalTitle, 45);
|
||||
name.textContent = truncatedTitle;
|
||||
// 设置title属性以显示完整文本(鼠标悬停时)
|
||||
name.title = originalTitle;
|
||||
|
||||
const time = document.createElement('div');
|
||||
time.className = 'batch-table-col-time';
|
||||
@@ -5114,7 +5174,9 @@ async function loadGroupConversations(groupId) {
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'group-conversation-title';
|
||||
title.textContent = fullConv.title || conv.title || '未命名对话';
|
||||
const titleText = fullConv.title || conv.title || '未命名对话';
|
||||
title.textContent = safeTruncateText(titleText, 60);
|
||||
title.title = titleText; // 设置完整标题以便悬停查看
|
||||
titleWrapper.appendChild(title);
|
||||
|
||||
// 如果对话在分组中置顶,显示置顶图标
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
let currentVulnerabilityId = null;
|
||||
let vulnerabilityFilters = {
|
||||
id: '',
|
||||
conversation_id: '',
|
||||
severity: '',
|
||||
status: ''
|
||||
@@ -80,6 +81,9 @@ async function loadVulnerabilities() {
|
||||
params.append('limit', '100');
|
||||
params.append('offset', '0');
|
||||
|
||||
if (vulnerabilityFilters.id) {
|
||||
params.append('id', vulnerabilityFilters.id);
|
||||
}
|
||||
if (vulnerabilityFilters.conversation_id) {
|
||||
params.append('conversation_id', vulnerabilityFilters.conversation_id);
|
||||
}
|
||||
@@ -172,6 +176,7 @@ function renderVulnerabilities(vulnerabilities) {
|
||||
<div class="vulnerability-content" id="content-${vuln.id}" style="display: none;">
|
||||
${vuln.description ? `<div class="vulnerability-description">${escapeHtml(vuln.description)}</div>` : ''}
|
||||
<div class="vulnerability-details">
|
||||
<div class="detail-item"><strong>漏洞ID:</strong> <code>${escapeHtml(vuln.id)}</code></div>
|
||||
${vuln.type ? `<div class="detail-item"><strong>类型:</strong> ${escapeHtml(vuln.type)}</div>` : ''}
|
||||
${vuln.target ? `<div class="detail-item"><strong>目标:</strong> ${escapeHtml(vuln.target)}</div>` : ''}
|
||||
<div class="detail-item"><strong>会话ID:</strong> <code>${escapeHtml(vuln.conversation_id)}</code></div>
|
||||
@@ -317,6 +322,7 @@ function closeVulnerabilityModal() {
|
||||
|
||||
// 筛选漏洞
|
||||
function filterVulnerabilities() {
|
||||
vulnerabilityFilters.id = document.getElementById('vulnerability-id-filter').value.trim();
|
||||
vulnerabilityFilters.conversation_id = document.getElementById('vulnerability-conversation-filter').value.trim();
|
||||
vulnerabilityFilters.severity = document.getElementById('vulnerability-severity-filter').value;
|
||||
vulnerabilityFilters.status = document.getElementById('vulnerability-status-filter').value;
|
||||
@@ -327,11 +333,13 @@ function filterVulnerabilities() {
|
||||
|
||||
// 清除筛选
|
||||
function clearVulnerabilityFilters() {
|
||||
document.getElementById('vulnerability-id-filter').value = '';
|
||||
document.getElementById('vulnerability-conversation-filter').value = '';
|
||||
document.getElementById('vulnerability-severity-filter').value = '';
|
||||
document.getElementById('vulnerability-status-filter').value = '';
|
||||
|
||||
vulnerabilityFilters = {
|
||||
id: '',
|
||||
conversation_id: '',
|
||||
severity: '',
|
||||
status: ''
|
||||
|
||||
@@ -482,6 +482,10 @@
|
||||
<!-- 筛选和搜索 -->
|
||||
<div class="vulnerability-controls">
|
||||
<div class="vulnerability-filters">
|
||||
<label>
|
||||
漏洞ID
|
||||
<input type="text" id="vulnerability-id-filter" placeholder="搜索漏洞ID" />
|
||||
</label>
|
||||
<label>
|
||||
会话ID
|
||||
<input type="text" id="vulnerability-conversation-filter" placeholder="筛选特定会话" />
|
||||
|
||||
Reference in New Issue
Block a user