mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-17 21:44:43 +02:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c95ed03c2 | |||
| 2772c4d9e7 | |||
| 1eb5133492 | |||
| 60fa266af6 | |||
| b75b5be1f7 | |||
| 1e4b846be5 | |||
| 335be9ab03 |
@@ -149,7 +149,7 @@ CyberStrikeAI ships with 100+ curated tools covering the whole kill chain:
|
|||||||
**One-Command Deployment:**
|
**One-Command Deployment:**
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/Ed1s0nZ/CyberStrikeAI.git
|
git clone https://github.com/Ed1s0nZ/CyberStrikeAI.git
|
||||||
cd CyberStrikeAI-main
|
cd CyberStrikeAI
|
||||||
chmod +x run.sh && ./run.sh
|
chmod +x run.sh && ./run.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -148,7 +148,7 @@ CyberStrikeAI 是一款 **AI 原生安全测试平台**,基于 Go 构建,集
|
|||||||
**一条命令部署:**
|
**一条命令部署:**
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/Ed1s0nZ/CyberStrikeAI.git
|
git clone https://github.com/Ed1s0nZ/CyberStrikeAI.git
|
||||||
cd CyberStrikeAI-main
|
cd CyberStrikeAI
|
||||||
chmod +x run.sh && ./run.sh
|
chmod +x run.sh && ./run.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
# 前端显示的版本号(可选,不填则显示默认版本)
|
# 前端显示的版本号(可选,不填则显示默认版本)
|
||||||
version: "v1.4.5"
|
version: "v1.4.6"
|
||||||
# 服务器配置
|
# 服务器配置
|
||||||
server:
|
server:
|
||||||
host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口
|
host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口
|
||||||
|
|||||||
@@ -661,6 +661,7 @@ func setupRoutes(
|
|||||||
protected.POST("/conversations", conversationHandler.CreateConversation)
|
protected.POST("/conversations", conversationHandler.CreateConversation)
|
||||||
protected.GET("/conversations", conversationHandler.ListConversations)
|
protected.GET("/conversations", conversationHandler.ListConversations)
|
||||||
protected.GET("/conversations/:id", conversationHandler.GetConversation)
|
protected.GET("/conversations/:id", conversationHandler.GetConversation)
|
||||||
|
protected.GET("/messages/:id/process-details", conversationHandler.GetMessageProcessDetails)
|
||||||
protected.PUT("/conversations/:id", conversationHandler.UpdateConversation)
|
protected.PUT("/conversations/:id", conversationHandler.UpdateConversation)
|
||||||
protected.DELETE("/conversations/:id", conversationHandler.DeleteConversation)
|
protected.DELETE("/conversations/:id", conversationHandler.DeleteConversation)
|
||||||
protected.PUT("/conversations/:id/pinned", groupHandler.UpdateConversationPinned)
|
protected.PUT("/conversations/:id/pinned", groupHandler.UpdateConversationPinned)
|
||||||
|
|||||||
@@ -256,6 +256,53 @@ func (db *DB) GetConversation(id string) (*Conversation, error) {
|
|||||||
return &conv, nil
|
return &conv, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetConversationLite 获取对话(轻量版):包含 messages,但不加载 process_details。
|
||||||
|
// 用于历史会话快速切换,避免一次性把大体量过程详情灌到前端导致卡顿。
|
||||||
|
func (db *DB) GetConversationLite(id string) (*Conversation, error) {
|
||||||
|
var conv Conversation
|
||||||
|
var createdAt, updatedAt string
|
||||||
|
var pinned int
|
||||||
|
|
||||||
|
err := db.QueryRow(
|
||||||
|
"SELECT id, title, pinned, created_at, updated_at FROM conversations WHERE id = ?",
|
||||||
|
id,
|
||||||
|
).Scan(&conv.ID, &conv.Title, &pinned, &createdAt, &updatedAt)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, fmt.Errorf("对话不存在")
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("查询对话失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试多种时间格式解析
|
||||||
|
var err1, err2 error
|
||||||
|
conv.CreatedAt, err1 = time.Parse("2006-01-02 15:04:05.999999999-07:00", createdAt)
|
||||||
|
if err1 != nil {
|
||||||
|
conv.CreatedAt, err1 = time.Parse("2006-01-02 15:04:05", createdAt)
|
||||||
|
}
|
||||||
|
if err1 != nil {
|
||||||
|
conv.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
conv.UpdatedAt, err2 = time.Parse("2006-01-02 15:04:05.999999999-07:00", updatedAt)
|
||||||
|
if err2 != nil {
|
||||||
|
conv.UpdatedAt, err2 = time.Parse("2006-01-02 15:04:05", updatedAt)
|
||||||
|
}
|
||||||
|
if err2 != nil {
|
||||||
|
conv.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
conv.Pinned = pinned != 0
|
||||||
|
|
||||||
|
// 加载消息(不加载 process_details)
|
||||||
|
messages, err := db.GetMessages(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("加载消息失败: %w", err)
|
||||||
|
}
|
||||||
|
conv.Messages = messages
|
||||||
|
return &conv, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ListConversations 列出所有对话
|
// ListConversations 列出所有对话
|
||||||
func (db *DB) ListConversations(limit, offset int, search string) ([]*Conversation, error) {
|
func (db *DB) ListConversations(limit, offset int, search string) ([]*Conversation, error) {
|
||||||
var rows *sql.Rows
|
var rows *sql.Rows
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
@@ -78,7 +79,20 @@ func (h *ConversationHandler) ListConversations(c *gin.Context) {
|
|||||||
func (h *ConversationHandler) GetConversation(c *gin.Context) {
|
func (h *ConversationHandler) GetConversation(c *gin.Context) {
|
||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
|
|
||||||
conv, err := h.db.GetConversation(id)
|
// 默认轻量加载,只有用户需要展开详情时再按需拉取
|
||||||
|
// include_process_details=1/true 时返回全量 processDetails(兼容旧行为)
|
||||||
|
includeStr := c.DefaultQuery("include_process_details", "0")
|
||||||
|
include := includeStr == "1" || includeStr == "true" || includeStr == "yes"
|
||||||
|
|
||||||
|
var (
|
||||||
|
conv *database.Conversation
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if include {
|
||||||
|
conv, err = h.db.GetConversation(id)
|
||||||
|
} else {
|
||||||
|
conv, err = h.db.GetConversationLite(id)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("获取对话失败", zap.Error(err))
|
h.logger.Error("获取对话失败", zap.Error(err))
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "对话不存在"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "对话不存在"})
|
||||||
@@ -88,6 +102,44 @@ func (h *ConversationHandler) GetConversation(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, conv)
|
c.JSON(http.StatusOK, conv)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetMessageProcessDetails 获取指定消息的过程详情(按需加载)
|
||||||
|
func (h *ConversationHandler) GetMessageProcessDetails(c *gin.Context) {
|
||||||
|
messageID := c.Param("id")
|
||||||
|
if messageID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "message id required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
details, err := h.db.GetProcessDetails(messageID)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("获取过程详情失败", zap.Error(err))
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为前端期望的 JSON 结构(与 GetConversation 中 processDetails 结构一致)
|
||||||
|
out := make([]map[string]interface{}, 0, len(details))
|
||||||
|
for _, d := range details {
|
||||||
|
var data interface{}
|
||||||
|
if d.Data != "" {
|
||||||
|
if err := json.Unmarshal([]byte(d.Data), &data); err != nil {
|
||||||
|
h.logger.Warn("解析过程详情数据失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = append(out, map[string]interface{}{
|
||||||
|
"id": d.ID,
|
||||||
|
"messageId": d.MessageID,
|
||||||
|
"conversationId": d.ConversationID,
|
||||||
|
"eventType": d.EventType,
|
||||||
|
"message": d.Message,
|
||||||
|
"data": data,
|
||||||
|
"createdAt": d.CreatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"processDetails": out})
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateConversationRequest 更新对话请求
|
// UpdateConversationRequest 更新对话请求
|
||||||
type UpdateConversationRequest struct {
|
type UpdateConversationRequest struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
|
|||||||
+73
-17
@@ -1519,7 +1519,50 @@ function copyMessageToClipboard(messageDiv, button) {
|
|||||||
try {
|
try {
|
||||||
// 获取保存的原始Markdown内容
|
// 获取保存的原始Markdown内容
|
||||||
const originalContent = messageDiv.dataset.originalContent;
|
const originalContent = messageDiv.dataset.originalContent;
|
||||||
|
|
||||||
|
// 统一的复制处理函数
|
||||||
|
const doCopy = (text) => {
|
||||||
|
// 优先使用现代 Clipboard API(需要 HTTPS 或 localhost)
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
return navigator.clipboard.writeText(text).then(() => {
|
||||||
|
showCopySuccess(button);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Clipboard API 复制失败:', err);
|
||||||
|
fallbackCopy(text);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 降级方案:使用传统的 execCommand 方法(适用于 HTTP 环境)
|
||||||
|
return fallbackCopy(text);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 降级复制函数(使用 document.execCommand)
|
||||||
|
const fallbackCopy = (text) => {
|
||||||
|
try {
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.value = text;
|
||||||
|
textArea.style.position = 'fixed';
|
||||||
|
textArea.style.left = '-999999px';
|
||||||
|
textArea.style.top = '-999999px';
|
||||||
|
textArea.style.opacity = '0';
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.focus();
|
||||||
|
textArea.select();
|
||||||
|
|
||||||
|
const successful = document.execCommand('copy');
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
|
||||||
|
if (successful) {
|
||||||
|
showCopySuccess(button);
|
||||||
|
} else {
|
||||||
|
throw new Error('execCommand copy failed');
|
||||||
|
}
|
||||||
|
} catch (execErr) {
|
||||||
|
console.error('降级复制失败:', execErr);
|
||||||
|
alert(typeof window.t === 'function' ? window.t('chat.copyFailedManual') : '复制失败,请手动选择内容复制');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!originalContent) {
|
if (!originalContent) {
|
||||||
// 如果没有保存原始内容,尝试从渲染后的HTML提取(降级方案)
|
// 如果没有保存原始内容,尝试从渲染后的HTML提取(降级方案)
|
||||||
const bubble = messageDiv.querySelector('.message-bubble');
|
const bubble = messageDiv.querySelector('.message-bubble');
|
||||||
@@ -1536,24 +1579,14 @@ function copyMessageToClipboard(messageDiv, button) {
|
|||||||
// 提取纯文本内容
|
// 提取纯文本内容
|
||||||
let textContent = tempDiv.textContent || tempDiv.innerText || '';
|
let textContent = tempDiv.textContent || tempDiv.innerText || '';
|
||||||
textContent = textContent.replace(/\n{3,}/g, '\n\n').trim();
|
textContent = textContent.replace(/\n{3,}/g, '\n\n').trim();
|
||||||
|
|
||||||
navigator.clipboard.writeText(textContent).then(() => {
|
doCopy(textContent);
|
||||||
showCopySuccess(button);
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('复制失败:', err);
|
|
||||||
alert(typeof window.t === 'function' ? window.t('chat.copyFailedManual') : '复制失败,请手动选择内容复制');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用原始Markdown内容
|
// 使用原始Markdown内容
|
||||||
navigator.clipboard.writeText(originalContent).then(() => {
|
doCopy(originalContent);
|
||||||
showCopySuccess(button);
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('复制失败:', err);
|
|
||||||
alert(typeof window.t === 'function' ? window.t('chat.copyFailedManual') : '复制失败,请手动选择内容复制');
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('复制消息时出错:', error);
|
console.error('复制消息时出错:', error);
|
||||||
alert(typeof window.t === 'function' ? window.t('chat.copyFailedManual') : '复制失败,请手动选择内容复制');
|
alert(typeof window.t === 'function' ? window.t('chat.copyFailedManual') : '复制失败,请手动选择内容复制');
|
||||||
@@ -1662,6 +1695,20 @@ function renderProcessDetails(messageId, processDetails) {
|
|||||||
detailsContainer.appendChild(contentDiv);
|
detailsContainer.appendChild(contentDiv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// processDetails === null 表示“尚未加载(懒加载)”
|
||||||
|
const isLazyNotLoaded = (processDetails === null);
|
||||||
|
if (isLazyNotLoaded) {
|
||||||
|
detailsContainer.dataset.lazyNotLoaded = '1';
|
||||||
|
detailsContainer.dataset.loaded = '0';
|
||||||
|
timeline.innerHTML = '<div class="progress-timeline-empty">' +
|
||||||
|
(typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') +
|
||||||
|
'(点击后加载)</div>';
|
||||||
|
// 默认折叠
|
||||||
|
timeline.classList.remove('expanded');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
detailsContainer.dataset.lazyNotLoaded = '0';
|
||||||
|
detailsContainer.dataset.loaded = '1';
|
||||||
// 如果没有processDetails或为空,显示空状态
|
// 如果没有processDetails或为空,显示空状态
|
||||||
if (!processDetails || processDetails.length === 0) {
|
if (!processDetails || processDetails.length === 0) {
|
||||||
// 显示空状态提示
|
// 显示空状态提示
|
||||||
@@ -2294,7 +2341,8 @@ function getConversationGroup(dateObj, todayStart, startOfWeek, yesterdayStart)
|
|||||||
// 加载对话
|
// 加载对话
|
||||||
async function loadConversation(conversationId) {
|
async function loadConversation(conversationId) {
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(`/api/conversations/${conversationId}`);
|
// 轻量加载:不带 processDetails,避免历史会话切换卡顿;展开详情时再按需拉取
|
||||||
|
const response = await apiFetch(`/api/conversations/${conversationId}?include_process_details=0`);
|
||||||
const conversation = await response.json();
|
const conversation = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -2393,11 +2441,18 @@ async function loadConversation(conversationId) {
|
|||||||
|
|
||||||
// 传递消息的创建时间
|
// 传递消息的创建时间
|
||||||
const messageId = addMessage(msg.role, displayContent, msg.mcpExecutionIds || [], null, msg.createdAt);
|
const messageId = addMessage(msg.role, displayContent, msg.mcpExecutionIds || [], null, msg.createdAt);
|
||||||
|
// 绑定后端 messageId,供按需加载过程详情使用
|
||||||
|
const messageEl = document.getElementById(messageId);
|
||||||
|
if (messageEl && msg && msg.id) {
|
||||||
|
messageEl.dataset.backendMessageId = String(msg.id);
|
||||||
|
}
|
||||||
// 对于助手消息,总是渲染过程详情(即使没有processDetails也要显示展开详情按钮)
|
// 对于助手消息,总是渲染过程详情(即使没有processDetails也要显示展开详情按钮)
|
||||||
if (msg.role === 'assistant') {
|
if (msg.role === 'assistant') {
|
||||||
// 延迟一下,确保消息已经渲染
|
// 延迟一下,确保消息已经渲染
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
renderProcessDetails(messageId, msg.processDetails || []);
|
// 如果后端未返回 processDetails 字段,传 null 表示“尚未加载,点击展开时再请求”
|
||||||
|
const hasField = msg && Object.prototype.hasOwnProperty.call(msg, 'processDetails');
|
||||||
|
renderProcessDetails(messageId, hasField ? (msg.processDetails || []) : null);
|
||||||
// 如果有过程详情,检查是否有错误或取消事件,如果有,确保详情默认折叠
|
// 如果有过程详情,检查是否有错误或取消事件,如果有,确保详情默认折叠
|
||||||
if (msg.processDetails && msg.processDetails.length > 0) {
|
if (msg.processDetails && msg.processDetails.length > 0) {
|
||||||
const hasErrorOrCancelled = msg.processDetails.some(d =>
|
const hasErrorOrCancelled = msg.processDetails.some(d =>
|
||||||
@@ -5474,7 +5529,8 @@ async function downloadConversationMarkdownFromContext(includeToolDetails = fals
|
|||||||
if (!convId) return;
|
if (!convId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(`/api/conversations/${convId}`);
|
// 下载不影响页面性能:直接从后端一次性拉取全量过程详情
|
||||||
|
const response = await apiFetch(`/api/conversations/${convId}?include_process_details=1`);
|
||||||
let conversation = null;
|
let conversation = null;
|
||||||
try {
|
try {
|
||||||
conversation = await response.json();
|
conversation = await response.json();
|
||||||
|
|||||||
@@ -506,6 +506,46 @@ function toggleProcessDetails(progressId, assistantMessageId) {
|
|||||||
const detailsId = 'process-details-' + assistantMessageId;
|
const detailsId = 'process-details-' + assistantMessageId;
|
||||||
const detailsContainer = document.getElementById(detailsId);
|
const detailsContainer = document.getElementById(detailsId);
|
||||||
if (!detailsContainer) return;
|
if (!detailsContainer) return;
|
||||||
|
|
||||||
|
// 懒加载:首次展开时才从后端拉取该条消息的过程详情
|
||||||
|
const maybeLazy = detailsContainer.dataset && detailsContainer.dataset.lazyNotLoaded === '1' && detailsContainer.dataset.loaded !== '1';
|
||||||
|
if (maybeLazy) {
|
||||||
|
const messageEl = document.getElementById(assistantMessageId);
|
||||||
|
const backendMessageId = messageEl && messageEl.dataset ? messageEl.dataset.backendMessageId : '';
|
||||||
|
if (backendMessageId && typeof apiFetch === 'function' && typeof renderProcessDetails === 'function') {
|
||||||
|
if (detailsContainer.dataset.loading === '1') {
|
||||||
|
// 正在加载中,避免重复请求
|
||||||
|
} else {
|
||||||
|
detailsContainer.dataset.loading = '1';
|
||||||
|
// 先展开容器,显示加载态
|
||||||
|
const timeline = detailsContainer.querySelector('.progress-timeline');
|
||||||
|
if (timeline) {
|
||||||
|
timeline.innerHTML = '<div class="progress-timeline-empty">' + ((typeof window.t === 'function') ? window.t('common.loading') : '加载中…') + '</div>';
|
||||||
|
}
|
||||||
|
apiFetch(`/api/messages/${encodeURIComponent(String(backendMessageId))}/process-details`)
|
||||||
|
.then(async (res) => {
|
||||||
|
const j = await res.json().catch(() => ({}));
|
||||||
|
if (!res.ok) throw new Error((j && j.error) ? j.error : res.status);
|
||||||
|
const details = (j && Array.isArray(j.processDetails)) ? j.processDetails : [];
|
||||||
|
// 重新渲染详情(renderProcessDetails 会清掉 lazy 标记并写入 loaded)
|
||||||
|
renderProcessDetails(assistantMessageId, details);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error('加载过程详情失败:', e);
|
||||||
|
const tl = detailsContainer.querySelector('.progress-timeline');
|
||||||
|
if (tl) {
|
||||||
|
tl.innerHTML = '<div class="progress-timeline-empty">' + ((typeof window.t === 'function') ? window.t('chat.noProcessDetail') : '暂无过程详情(加载失败)') + '</div>';
|
||||||
|
}
|
||||||
|
// 失败时保留 lazy 状态,允许用户重试
|
||||||
|
detailsContainer.dataset.lazyNotLoaded = '1';
|
||||||
|
detailsContainer.dataset.loaded = '0';
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
detailsContainer.dataset.loading = '0';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const content = detailsContainer.querySelector('.process-details-content');
|
const content = detailsContainer.querySelector('.process-details-content');
|
||||||
const timeline = detailsContainer.querySelector('.progress-timeline');
|
const timeline = detailsContainer.querySelector('.progress-timeline');
|
||||||
|
|||||||
Reference in New Issue
Block a user