mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-16 21:23:29 +02:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b3d094dc4 | |||
| 47922c2083 | |||
| dfaf0bc77f | |||
| 3eb7edb1b8 | |||
| f82f6b861e | |||
| 2acf43c454 | |||
| fad6b3c808 | |||
| 0597838217 | |||
| 1532426b4f | |||
| 3aeb8c3474 | |||
| b2b166972a | |||
| 36b669771c |
+1
-1
@@ -10,7 +10,7 @@
|
||||
# ============================================
|
||||
|
||||
# 前端显示的版本号(可选,不填则显示默认版本)
|
||||
version: "v1.5.8"
|
||||
version: "v1.5.10"
|
||||
# 服务器配置
|
||||
server:
|
||||
host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口
|
||||
|
||||
@@ -901,6 +901,8 @@ func setupRoutes(
|
||||
|
||||
// 漏洞管理
|
||||
protected.GET("/vulnerabilities", vulnerabilityHandler.ListVulnerabilities)
|
||||
protected.GET("/vulnerabilities/export", vulnerabilityHandler.ExportVulnerabilities)
|
||||
protected.GET("/vulnerabilities/filter-options", vulnerabilityHandler.GetVulnerabilityFilterOptions)
|
||||
protected.GET("/vulnerabilities/stats", vulnerabilityHandler.GetVulnerabilityStats)
|
||||
protected.GET("/vulnerabilities/:id", vulnerabilityHandler.GetVulnerability)
|
||||
protected.POST("/vulnerabilities", vulnerabilityHandler.CreateVulnerability)
|
||||
|
||||
@@ -320,7 +320,7 @@ func (b *Builder) formatProcessDetailsForAttackChain(details []database.ProcessD
|
||||
}
|
||||
|
||||
// 1) 编排器的工具调用/结果:保留(这是“主 agent 调了什么工具”)
|
||||
if (d.EventType == "tool_call" || d.EventType == "tool_result" || d.EventType == "tool_calls_detected" || d.EventType == "iteration" || d.EventType == "eino_recovery") && einoRole == "orchestrator" {
|
||||
if (d.EventType == "tool_call" || d.EventType == "tool_result" || d.EventType == "tool_calls_detected" || d.EventType == "iteration") && einoRole == "orchestrator" {
|
||||
sb.WriteString("[")
|
||||
sb.WriteString(d.EventType)
|
||||
sb.WriteString("] ")
|
||||
|
||||
@@ -197,6 +197,8 @@ func (db *DB) initTables() error {
|
||||
CREATE TABLE IF NOT EXISTS vulnerabilities (
|
||||
id TEXT PRIMARY KEY,
|
||||
conversation_id TEXT NOT NULL,
|
||||
conversation_tag TEXT,
|
||||
task_tag TEXT,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
severity TEXT NOT NULL,
|
||||
@@ -289,6 +291,8 @@ func (db *DB) initTables() error {
|
||||
CREATE INDEX IF NOT EXISTS idx_conversation_group_mappings_group ON conversation_group_mappings(group_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_conversations_pinned ON conversations(pinned);
|
||||
CREATE INDEX IF NOT EXISTS idx_vulnerabilities_conversation_id ON vulnerabilities(conversation_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_vulnerabilities_conversation_tag ON vulnerabilities(conversation_tag);
|
||||
CREATE INDEX IF NOT EXISTS idx_vulnerabilities_task_tag ON vulnerabilities(task_tag);
|
||||
CREATE INDEX IF NOT EXISTS idx_vulnerabilities_severity ON vulnerabilities(severity);
|
||||
CREATE INDEX IF NOT EXISTS idx_vulnerabilities_status ON vulnerabilities(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_vulnerabilities_created_at ON vulnerabilities(created_at);
|
||||
@@ -383,6 +387,10 @@ func (db *DB) initTables() error {
|
||||
db.logger.Warn("迁移batch_task_queues表失败", zap.Error(err))
|
||||
// 不返回错误,允许继续运行
|
||||
}
|
||||
if err := db.migrateVulnerabilitiesTable(); err != nil {
|
||||
db.logger.Warn("迁移vulnerabilities表失败", zap.Error(err))
|
||||
// 不返回错误,允许继续运行
|
||||
}
|
||||
|
||||
if _, err := db.Exec(createIndexes); err != nil {
|
||||
return fmt.Errorf("创建索引失败: %w", err)
|
||||
@@ -683,6 +691,37 @@ func (db *DB) migrateBatchTaskQueuesTable() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateVulnerabilitiesTable 迁移 vulnerabilities 表,补充标签字段
|
||||
func (db *DB) migrateVulnerabilitiesTable() error {
|
||||
columns := []struct {
|
||||
name string
|
||||
stmt string
|
||||
}{
|
||||
{name: "conversation_tag", stmt: "ALTER TABLE vulnerabilities ADD COLUMN conversation_tag TEXT"},
|
||||
{name: "task_tag", stmt: "ALTER TABLE vulnerabilities ADD COLUMN task_tag TEXT"},
|
||||
}
|
||||
|
||||
for _, col := range columns {
|
||||
var count int
|
||||
err := db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('vulnerabilities') WHERE name=?", col.name).Scan(&count)
|
||||
if err != nil {
|
||||
if _, addErr := db.Exec(col.stmt); addErr != nil {
|
||||
errMsg := strings.ToLower(addErr.Error())
|
||||
if !strings.Contains(errMsg, "duplicate column") && !strings.Contains(errMsg, "already exists") {
|
||||
db.logger.Warn("添加vulnerabilities字段失败", zap.String("field", col.name), zap.Error(addErr))
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if count == 0 {
|
||||
if _, addErr := db.Exec(col.stmt); addErr != nil {
|
||||
db.logger.Warn("添加vulnerabilities字段失败", zap.String("field", col.name), zap.Error(addErr))
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewKnowledgeDB 创建知识库数据库连接(只包含知识库相关的表)
|
||||
func NewKnowledgeDB(dbPath string, logger *zap.Logger) (*DB, error) {
|
||||
sqlDB, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL&_foreign_keys=1&_busy_timeout=5000&_synchronous=NORMAL")
|
||||
|
||||
@@ -13,6 +13,10 @@ import (
|
||||
type Vulnerability struct {
|
||||
ID string `json:"id"`
|
||||
ConversationID string `json:"conversation_id"`
|
||||
ConversationTag string `json:"conversation_tag,omitempty"`
|
||||
TaskTag string `json:"task_tag,omitempty"`
|
||||
TaskID string `json:"task_id,omitempty"`
|
||||
TaskQueueID string `json:"task_queue_id,omitempty"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Severity string `json:"severity"` // critical, high, medium, low, info
|
||||
@@ -42,15 +46,15 @@ func (db *DB) CreateVulnerability(vuln *Vulnerability) (*Vulnerability, error) {
|
||||
|
||||
query := `
|
||||
INSERT INTO vulnerabilities (
|
||||
id, conversation_id, title, description, severity, status,
|
||||
id, conversation_id, conversation_tag, task_tag, title, description, severity, status,
|
||||
vulnerability_type, target, proof, impact, recommendation,
|
||||
created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
|
||||
_, err := db.Exec(
|
||||
query,
|
||||
vuln.ID, vuln.ConversationID, vuln.Title, vuln.Description,
|
||||
vuln.ID, vuln.ConversationID, vuln.ConversationTag, vuln.TaskTag, vuln.Title, vuln.Description,
|
||||
vuln.Severity, vuln.Status, vuln.Type, vuln.Target,
|
||||
vuln.Proof, vuln.Impact, vuln.Recommendation,
|
||||
vuln.CreatedAt, vuln.UpdatedAt,
|
||||
@@ -67,7 +71,9 @@ func (db *DB) GetVulnerability(id string) (*Vulnerability, error) {
|
||||
var vuln Vulnerability
|
||||
query := `
|
||||
SELECT id, conversation_id, title, description, severity, status,
|
||||
vulnerability_type, target, proof, impact, recommendation,
|
||||
conversation_tag, task_tag, vulnerability_type, target, proof, impact, recommendation,
|
||||
COALESCE((SELECT bt.id FROM batch_tasks bt WHERE bt.conversation_id = vulnerabilities.conversation_id LIMIT 1), '') AS task_id,
|
||||
COALESCE((SELECT bt.queue_id FROM batch_tasks bt WHERE bt.conversation_id = vulnerabilities.conversation_id LIMIT 1), '') AS task_queue_id,
|
||||
created_at, updated_at
|
||||
FROM vulnerabilities
|
||||
WHERE id = ?
|
||||
@@ -75,8 +81,9 @@ func (db *DB) GetVulnerability(id string) (*Vulnerability, error) {
|
||||
|
||||
err := db.QueryRow(query, id).Scan(
|
||||
&vuln.ID, &vuln.ConversationID, &vuln.Title, &vuln.Description,
|
||||
&vuln.Severity, &vuln.Status, &vuln.Type, &vuln.Target,
|
||||
&vuln.Severity, &vuln.Status, &vuln.ConversationTag, &vuln.TaskTag, &vuln.Type, &vuln.Target,
|
||||
&vuln.Proof, &vuln.Impact, &vuln.Recommendation,
|
||||
&vuln.TaskID, &vuln.TaskQueueID,
|
||||
&vuln.CreatedAt, &vuln.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -90,10 +97,12 @@ func (db *DB) GetVulnerability(id string) (*Vulnerability, error) {
|
||||
}
|
||||
|
||||
// ListVulnerabilities 列出漏洞
|
||||
func (db *DB) ListVulnerabilities(limit, offset int, id, conversationID, severity, status string) ([]*Vulnerability, error) {
|
||||
func (db *DB) ListVulnerabilities(limit, offset int, id, conversationID, severity, status, taskID, conversationTag, taskTag string) ([]*Vulnerability, error) {
|
||||
query := `
|
||||
SELECT id, conversation_id, title, description, severity, status,
|
||||
SELECT id, conversation_id, title, description, severity, status, conversation_tag, task_tag,
|
||||
vulnerability_type, target, proof, impact, recommendation,
|
||||
COALESCE((SELECT bt.id FROM batch_tasks bt WHERE bt.conversation_id = vulnerabilities.conversation_id LIMIT 1), '') AS task_id,
|
||||
COALESCE((SELECT bt.queue_id FROM batch_tasks bt WHERE bt.conversation_id = vulnerabilities.conversation_id LIMIT 1), '') AS task_queue_id,
|
||||
created_at, updated_at
|
||||
FROM vulnerabilities
|
||||
WHERE 1=1
|
||||
@@ -108,6 +117,18 @@ func (db *DB) ListVulnerabilities(limit, offset int, id, conversationID, severit
|
||||
query += " AND conversation_id = ?"
|
||||
args = append(args, conversationID)
|
||||
}
|
||||
if taskID != "" {
|
||||
query += " AND EXISTS (SELECT 1 FROM batch_tasks bt WHERE bt.conversation_id = vulnerabilities.conversation_id AND (bt.id = ? OR bt.queue_id = ?))"
|
||||
args = append(args, taskID, taskID)
|
||||
}
|
||||
if conversationTag != "" {
|
||||
query += " AND conversation_tag = ?"
|
||||
args = append(args, conversationTag)
|
||||
}
|
||||
if taskTag != "" {
|
||||
query += " AND task_tag = ?"
|
||||
args = append(args, taskTag)
|
||||
}
|
||||
if severity != "" {
|
||||
query += " AND severity = ?"
|
||||
args = append(args, severity)
|
||||
@@ -131,8 +152,9 @@ func (db *DB) ListVulnerabilities(limit, offset int, id, conversationID, severit
|
||||
var vuln Vulnerability
|
||||
err := rows.Scan(
|
||||
&vuln.ID, &vuln.ConversationID, &vuln.Title, &vuln.Description,
|
||||
&vuln.Severity, &vuln.Status, &vuln.Type, &vuln.Target,
|
||||
&vuln.Severity, &vuln.Status, &vuln.ConversationTag, &vuln.TaskTag, &vuln.Type, &vuln.Target,
|
||||
&vuln.Proof, &vuln.Impact, &vuln.Recommendation,
|
||||
&vuln.TaskID, &vuln.TaskQueueID,
|
||||
&vuln.CreatedAt, &vuln.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -146,7 +168,7 @@ func (db *DB) ListVulnerabilities(limit, offset int, id, conversationID, severit
|
||||
}
|
||||
|
||||
// CountVulnerabilities 统计漏洞总数(支持筛选条件)
|
||||
func (db *DB) CountVulnerabilities(id, conversationID, severity, status string) (int, error) {
|
||||
func (db *DB) CountVulnerabilities(id, conversationID, severity, status, taskID, conversationTag, taskTag string) (int, error) {
|
||||
query := "SELECT COUNT(*) FROM vulnerabilities WHERE 1=1"
|
||||
args := []interface{}{}
|
||||
|
||||
@@ -158,6 +180,18 @@ func (db *DB) CountVulnerabilities(id, conversationID, severity, status string)
|
||||
query += " AND conversation_id = ?"
|
||||
args = append(args, conversationID)
|
||||
}
|
||||
if taskID != "" {
|
||||
query += " AND EXISTS (SELECT 1 FROM batch_tasks bt WHERE bt.conversation_id = vulnerabilities.conversation_id AND (bt.id = ? OR bt.queue_id = ?))"
|
||||
args = append(args, taskID, taskID)
|
||||
}
|
||||
if conversationTag != "" {
|
||||
query += " AND conversation_tag = ?"
|
||||
args = append(args, conversationTag)
|
||||
}
|
||||
if taskTag != "" {
|
||||
query += " AND task_tag = ?"
|
||||
args = append(args, taskTag)
|
||||
}
|
||||
if severity != "" {
|
||||
query += " AND severity = ?"
|
||||
args = append(args, severity)
|
||||
@@ -182,7 +216,7 @@ func (db *DB) UpdateVulnerability(id string, vuln *Vulnerability) error {
|
||||
|
||||
query := `
|
||||
UPDATE vulnerabilities
|
||||
SET title = ?, description = ?, severity = ?, status = ?,
|
||||
SET conversation_tag = ?, task_tag = ?, title = ?, description = ?, severity = ?, status = ?,
|
||||
vulnerability_type = ?, target = ?, proof = ?, impact = ?,
|
||||
recommendation = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
@@ -190,7 +224,7 @@ func (db *DB) UpdateVulnerability(id string, vuln *Vulnerability) error {
|
||||
|
||||
_, err := db.Exec(
|
||||
query,
|
||||
vuln.Title, vuln.Description, vuln.Severity, vuln.Status,
|
||||
vuln.ConversationTag, vuln.TaskTag, vuln.Title, vuln.Description, vuln.Severity, vuln.Status,
|
||||
vuln.Type, vuln.Target, vuln.Proof, vuln.Impact,
|
||||
vuln.Recommendation, vuln.UpdatedAt, id,
|
||||
)
|
||||
@@ -210,18 +244,24 @@ func (db *DB) DeleteVulnerability(id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetVulnerabilityStats 获取漏洞统计
|
||||
func (db *DB) GetVulnerabilityStats(conversationID string) (map[string]interface{}, error) {
|
||||
// GetVulnerabilityStats 获取漏洞统计(筛选条件与 ListVulnerabilities / CountVulnerabilities 一致)
|
||||
func (db *DB) GetVulnerabilityStats(conversationID, taskID string) (map[string]interface{}, error) {
|
||||
stats := make(map[string]interface{})
|
||||
|
||||
where := "WHERE 1=1"
|
||||
args := []interface{}{}
|
||||
if conversationID != "" {
|
||||
where += " AND conversation_id = ?"
|
||||
args = append(args, conversationID)
|
||||
}
|
||||
if taskID != "" {
|
||||
where += " AND EXISTS (SELECT 1 FROM batch_tasks bt WHERE bt.conversation_id = vulnerabilities.conversation_id AND (bt.id = ? OR bt.queue_id = ?))"
|
||||
args = append(args, taskID, taskID)
|
||||
}
|
||||
|
||||
// 总漏洞数
|
||||
var totalCount int
|
||||
query := "SELECT COUNT(*) FROM vulnerabilities"
|
||||
args := []interface{}{}
|
||||
if conversationID != "" {
|
||||
query += " WHERE conversation_id = ?"
|
||||
args = append(args, conversationID)
|
||||
}
|
||||
query := "SELECT COUNT(*) FROM vulnerabilities " + where
|
||||
err := db.QueryRow(query, args...).Scan(&totalCount)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取总漏洞数失败: %w", err)
|
||||
@@ -229,11 +269,7 @@ func (db *DB) GetVulnerabilityStats(conversationID string) (map[string]interface
|
||||
stats["total"] = totalCount
|
||||
|
||||
// 按严重程度统计
|
||||
severityQuery := "SELECT severity, COUNT(*) FROM vulnerabilities"
|
||||
if conversationID != "" {
|
||||
severityQuery += " WHERE conversation_id = ?"
|
||||
}
|
||||
severityQuery += " GROUP BY severity"
|
||||
severityQuery := "SELECT severity, COUNT(*) FROM vulnerabilities " + where + " GROUP BY severity"
|
||||
|
||||
rows, err := db.Query(severityQuery, args...)
|
||||
if err != nil {
|
||||
@@ -253,11 +289,7 @@ func (db *DB) GetVulnerabilityStats(conversationID string) (map[string]interface
|
||||
stats["by_severity"] = severityStats
|
||||
|
||||
// 按状态统计
|
||||
statusQuery := "SELECT status, COUNT(*) FROM vulnerabilities"
|
||||
if conversationID != "" {
|
||||
statusQuery += " WHERE conversation_id = ?"
|
||||
}
|
||||
statusQuery += " GROUP BY status"
|
||||
statusQuery := "SELECT status, COUNT(*) FROM vulnerabilities " + where + " GROUP BY status"
|
||||
|
||||
rows, err = db.Query(statusQuery, args...)
|
||||
if err != nil {
|
||||
@@ -279,3 +311,60 @@ func (db *DB) GetVulnerabilityStats(conversationID string) (map[string]interface
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetVulnerabilityFilterOptions 获取漏洞筛选建议项
|
||||
func (db *DB) GetVulnerabilityFilterOptions() (map[string][]string, error) {
|
||||
collect := func(query string, args ...interface{}) ([]string, error) {
|
||||
rows, err := db.Query(query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := make([]string, 0)
|
||||
for rows.Next() {
|
||||
var val string
|
||||
if err := rows.Scan(&val); err != nil {
|
||||
continue
|
||||
}
|
||||
if val == "" {
|
||||
continue
|
||||
}
|
||||
items = append(items, val)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
vulnIDs, err := collect(`SELECT DISTINCT id FROM vulnerabilities ORDER BY created_at DESC LIMIT 500`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询漏洞ID建议失败: %w", err)
|
||||
}
|
||||
conversationIDs, err := collect(`SELECT DISTINCT conversation_id FROM vulnerabilities WHERE conversation_id <> '' ORDER BY created_at DESC LIMIT 500`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询会话ID建议失败: %w", err)
|
||||
}
|
||||
taskIDs, err := collect(`SELECT DISTINCT id FROM batch_tasks WHERE id <> '' ORDER BY rowid DESC LIMIT 500`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询任务ID建议失败: %w", err)
|
||||
}
|
||||
queueIDs, err := collect(`SELECT DISTINCT queue_id FROM batch_tasks WHERE queue_id <> '' ORDER BY rowid DESC LIMIT 500`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询队列ID建议失败: %w", err)
|
||||
}
|
||||
conversationTags, err := collect(`SELECT DISTINCT conversation_tag FROM vulnerabilities WHERE conversation_tag IS NOT NULL AND conversation_tag <> '' ORDER BY conversation_tag LIMIT 500`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询对话标签建议失败: %w", err)
|
||||
}
|
||||
taskTags, err := collect(`SELECT DISTINCT task_tag FROM vulnerabilities WHERE task_tag IS NOT NULL AND task_tag <> '' ORDER BY task_tag LIMIT 500`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询任务标签建议失败: %w", err)
|
||||
}
|
||||
|
||||
return map[string][]string{
|
||||
"vulnerability_ids": vulnIDs,
|
||||
"conversation_ids": conversationIDs,
|
||||
"task_ids": taskIDs,
|
||||
"queue_ids": queueIDs,
|
||||
"conversation_tags": conversationTags,
|
||||
"task_tags": taskTags,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -160,17 +160,17 @@ func runMCPToolInvocation(
|
||||
}
|
||||
|
||||
// UnknownToolReminderHandler 供 compose.ToolsNodeConfig.UnknownToolsHandler 使用:
|
||||
// 模型请求了未注册的工具名时,返回一个「可恢复」的错误,让上层 runner 触发重试与纠错提示,
|
||||
// 同时避免 UI 永远停留在“执行中”(runner 会在 recoverable 分支 flush 掉 pending 的 tool_call)。
|
||||
// 模型请求了未注册的工具名时,返回一个「软错误」工具结果(nil error),
|
||||
// 让模型在同一轮继续自我修正,避免触发 run-loop 级别的 full rerun。
|
||||
// 不进行名称猜测或映射,避免误执行。
|
||||
func UnknownToolReminderHandler() func(ctx context.Context, name, input string) (string, error) {
|
||||
return func(ctx context.Context, name, input string) (string, error) {
|
||||
_ = ctx
|
||||
_ = input
|
||||
requested := strings.TrimSpace(name)
|
||||
// Return a recoverable error that still carries a friendly, bilingual hint.
|
||||
// This will be caught by multiagent runner as "tool not found" and trigger a retry.
|
||||
return "", fmt.Errorf("tool %q not found: %s", requested, unknownToolReminderText(requested))
|
||||
// Return a soft tool-result error so the graph keeps running and the LLM
|
||||
// can correct tool name/arguments within the same run.
|
||||
return ToolErrorPrefix + unknownToolReminderText(requested), nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6197,7 +6197,7 @@ func (h *OpenAPIHandler) GetConversationResults(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 获取漏洞列表
|
||||
vulnList, err := h.db.ListVulnerabilities(1000, 0, "", conversationID, "", "")
|
||||
vulnList, err := h.db.ListVulnerabilities(1000, 0, "", conversationID, "", "", "", "", "")
|
||||
if err != nil {
|
||||
h.logger.Warn("获取漏洞列表失败", zap.Error(err))
|
||||
vulnList = []*database.Vulnerability{}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cyberstrike-ai/internal/database"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -26,6 +29,8 @@ func NewVulnerabilityHandler(db *database.DB, logger *zap.Logger) *Vulnerability
|
||||
// CreateVulnerabilityRequest 创建漏洞请求
|
||||
type CreateVulnerabilityRequest struct {
|
||||
ConversationID string `json:"conversation_id" binding:"required"`
|
||||
ConversationTag string `json:"conversation_tag"`
|
||||
TaskTag string `json:"task_tag"`
|
||||
Title string `json:"title" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
Severity string `json:"severity" binding:"required"`
|
||||
@@ -47,6 +52,8 @@ func (h *VulnerabilityHandler) CreateVulnerability(c *gin.Context) {
|
||||
|
||||
vuln := &database.Vulnerability{
|
||||
ConversationID: req.ConversationID,
|
||||
ConversationTag: req.ConversationTag,
|
||||
TaskTag: req.TaskTag,
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
Severity: req.Severity,
|
||||
@@ -100,6 +107,9 @@ func (h *VulnerabilityHandler) ListVulnerabilities(c *gin.Context) {
|
||||
conversationID := c.Query("conversation_id")
|
||||
severity := c.Query("severity")
|
||||
status := c.Query("status")
|
||||
taskID := c.Query("task_id")
|
||||
conversationTag := c.Query("conversation_tag")
|
||||
taskTag := c.Query("task_tag")
|
||||
|
||||
limit, _ := strconv.Atoi(limitStr)
|
||||
offset, _ := strconv.Atoi(offsetStr)
|
||||
@@ -121,7 +131,7 @@ func (h *VulnerabilityHandler) ListVulnerabilities(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
total, err := h.db.CountVulnerabilities(id, conversationID, severity, status)
|
||||
total, err := h.db.CountVulnerabilities(id, conversationID, severity, status, taskID, conversationTag, taskTag)
|
||||
if err != nil {
|
||||
h.logger.Error("获取漏洞总数失败", zap.Error(err))
|
||||
// 继续执行,使用0作为总数
|
||||
@@ -129,7 +139,7 @@ func (h *VulnerabilityHandler) ListVulnerabilities(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 获取漏洞列表
|
||||
vulnerabilities, err := h.db.ListVulnerabilities(limit, offset, id, conversationID, severity, status)
|
||||
vulnerabilities, err := h.db.ListVulnerabilities(limit, offset, id, conversationID, severity, status, taskID, conversationTag, taskTag)
|
||||
if err != nil {
|
||||
h.logger.Error("获取漏洞列表失败", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
@@ -160,6 +170,8 @@ func (h *VulnerabilityHandler) ListVulnerabilities(c *gin.Context) {
|
||||
|
||||
// UpdateVulnerabilityRequest 更新漏洞请求
|
||||
type UpdateVulnerabilityRequest struct {
|
||||
ConversationTag string `json:"conversation_tag"`
|
||||
TaskTag string `json:"task_tag"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Severity string `json:"severity"`
|
||||
@@ -189,6 +201,12 @@ func (h *VulnerabilityHandler) UpdateVulnerability(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 更新字段
|
||||
if req.ConversationTag != "" {
|
||||
existing.ConversationTag = req.ConversationTag
|
||||
}
|
||||
if req.TaskTag != "" {
|
||||
existing.TaskTag = req.TaskTag
|
||||
}
|
||||
if req.Title != "" {
|
||||
existing.Title = req.Title
|
||||
}
|
||||
@@ -250,8 +268,9 @@ func (h *VulnerabilityHandler) DeleteVulnerability(c *gin.Context) {
|
||||
// GetVulnerabilityStats 获取漏洞统计
|
||||
func (h *VulnerabilityHandler) GetVulnerabilityStats(c *gin.Context) {
|
||||
conversationID := c.Query("conversation_id")
|
||||
taskID := c.Query("task_id")
|
||||
|
||||
stats, err := h.db.GetVulnerabilityStats(conversationID)
|
||||
stats, err := h.db.GetVulnerabilityStats(conversationID, taskID)
|
||||
if err != nil {
|
||||
h.logger.Error("获取漏洞统计失败", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
@@ -261,3 +280,184 @@ func (h *VulnerabilityHandler) GetVulnerabilityStats(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, stats)
|
||||
}
|
||||
|
||||
// GetVulnerabilityFilterOptions 获取漏洞筛选建议项
|
||||
func (h *VulnerabilityHandler) GetVulnerabilityFilterOptions(c *gin.Context) {
|
||||
options, err := h.db.GetVulnerabilityFilterOptions()
|
||||
if err != nil {
|
||||
h.logger.Error("获取漏洞筛选建议失败", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, options)
|
||||
}
|
||||
|
||||
// ExportVulnerabilities 导出漏洞(支持按对话/任务分组,汇总或拆分)
|
||||
func (h *VulnerabilityHandler) ExportVulnerabilities(c *gin.Context) {
|
||||
groupBy := c.DefaultQuery("group_by", "conversation")
|
||||
mode := c.DefaultQuery("mode", "summary")
|
||||
if groupBy != "conversation" && groupBy != "task" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "group_by 仅支持 conversation 或 task"})
|
||||
return
|
||||
}
|
||||
if mode != "summary" && mode != "split" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "mode 仅支持 summary 或 split"})
|
||||
return
|
||||
}
|
||||
|
||||
id := c.Query("id")
|
||||
conversationID := c.Query("conversation_id")
|
||||
severity := c.Query("severity")
|
||||
status := c.Query("status")
|
||||
taskID := c.Query("task_id")
|
||||
conversationTag := c.Query("conversation_tag")
|
||||
taskTag := c.Query("task_tag")
|
||||
|
||||
total, err := h.db.CountVulnerabilities(id, conversationID, severity, status, taskID, conversationTag, taskTag)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if total == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"mode": mode, "group_by": groupBy, "total": 0, "files": []any{}})
|
||||
return
|
||||
}
|
||||
|
||||
items, err := h.db.ListVulnerabilities(total, 0, id, conversationID, severity, status, taskID, conversationTag, taskTag)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
type exportFile struct {
|
||||
FileName string `json:"filename"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
grouped := map[string][]*database.Vulnerability{}
|
||||
for _, v := range items {
|
||||
key := v.ConversationID
|
||||
if groupBy == "conversation" {
|
||||
if strings.TrimSpace(v.ConversationTag) != "" {
|
||||
key = strings.TrimSpace(v.ConversationTag)
|
||||
}
|
||||
} else {
|
||||
key = firstNonEmpty(v.TaskTag, v.TaskID, v.TaskQueueID, "unassigned-task")
|
||||
}
|
||||
grouped[key] = append(grouped[key], v)
|
||||
}
|
||||
|
||||
files := make([]exportFile, 0)
|
||||
nowStr := time.Now().Format("20060102-150405")
|
||||
if mode == "summary" {
|
||||
var b strings.Builder
|
||||
b.WriteString("# 漏洞批量导出报告\n\n")
|
||||
b.WriteString(fmt.Sprintf("- 导出时间: %s\n", time.Now().Format("2006-01-02 15:04:05")))
|
||||
b.WriteString(fmt.Sprintf("- 分组维度: %s\n", groupBy))
|
||||
b.WriteString(fmt.Sprintf("- 漏洞总数: %d\n", len(items)))
|
||||
b.WriteString(fmt.Sprintf("- 分组数: %d\n\n", len(grouped)))
|
||||
for group, list := range grouped {
|
||||
b.WriteString(fmt.Sprintf("## %s (%d)\n\n", group, len(list)))
|
||||
for _, v := range list {
|
||||
appendVulnerabilityMarkdown(&b, v, "###")
|
||||
}
|
||||
}
|
||||
files = append(files, exportFile{
|
||||
FileName: fmt.Sprintf("vulnerability-report-%s-%s.md", groupBy, nowStr),
|
||||
Content: b.String(),
|
||||
})
|
||||
} else {
|
||||
for group, list := range grouped {
|
||||
var b strings.Builder
|
||||
b.WriteString(fmt.Sprintf("# 漏洞报告 - %s\n\n", group))
|
||||
b.WriteString(fmt.Sprintf("- 导出时间: %s\n", time.Now().Format("2006-01-02 15:04:05")))
|
||||
b.WriteString(fmt.Sprintf("- 漏洞数量: %d\n\n", len(list)))
|
||||
for _, v := range list {
|
||||
appendVulnerabilityMarkdown(&b, v, "##")
|
||||
}
|
||||
files = append(files, exportFile{
|
||||
FileName: fmt.Sprintf("vulnerability-%s-%s.md", sanitizeExportName(group), nowStr),
|
||||
Content: b.String(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"mode": mode,
|
||||
"group_by": groupBy,
|
||||
"total": len(items),
|
||||
"files": files,
|
||||
})
|
||||
}
|
||||
|
||||
// appendVulnerabilityMarkdown 单条漏洞的 Markdown 片段(与单文件下载字段对齐,缺省字段不写)
|
||||
func appendVulnerabilityMarkdown(b *strings.Builder, v *database.Vulnerability, titleHeading string) {
|
||||
b.WriteString(fmt.Sprintf("%s %s\n\n", titleHeading, v.Title))
|
||||
b.WriteString(fmt.Sprintf("- 漏洞ID: `%s`\n", v.ID))
|
||||
b.WriteString(fmt.Sprintf("- 严重程度: %s\n", v.Severity))
|
||||
b.WriteString(fmt.Sprintf("- 状态: %s\n", v.Status))
|
||||
if v.Type != "" {
|
||||
b.WriteString(fmt.Sprintf("- 类型: %s\n", v.Type))
|
||||
}
|
||||
if v.Target != "" {
|
||||
b.WriteString(fmt.Sprintf("- 目标: %s\n", v.Target))
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("- 对话ID: `%s`\n", v.ConversationID))
|
||||
if v.ConversationTag != "" {
|
||||
b.WriteString(fmt.Sprintf("- 对话标签: %s\n", v.ConversationTag))
|
||||
}
|
||||
if v.TaskTag != "" {
|
||||
b.WriteString(fmt.Sprintf("- 任务标签: %s\n", v.TaskTag))
|
||||
}
|
||||
if v.TaskID != "" {
|
||||
b.WriteString(fmt.Sprintf("- 任务ID: `%s`\n", v.TaskID))
|
||||
}
|
||||
if v.TaskQueueID != "" {
|
||||
b.WriteString(fmt.Sprintf("- 任务队列ID: `%s`\n", v.TaskQueueID))
|
||||
}
|
||||
if !v.CreatedAt.IsZero() {
|
||||
b.WriteString(fmt.Sprintf("- 创建时间: %s\n", v.CreatedAt.Format("2006-01-02 15:04:05")))
|
||||
}
|
||||
if !v.UpdatedAt.IsZero() {
|
||||
b.WriteString(fmt.Sprintf("- 更新时间: %s\n", v.UpdatedAt.Format("2006-01-02 15:04:05")))
|
||||
}
|
||||
if v.Description != "" {
|
||||
b.WriteString("\n#### 描述\n\n")
|
||||
b.WriteString(v.Description)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
if v.Proof != "" {
|
||||
b.WriteString("\n#### 证明(POC)\n\n```\n")
|
||||
b.WriteString(v.Proof)
|
||||
b.WriteString("\n```\n")
|
||||
}
|
||||
if v.Impact != "" {
|
||||
b.WriteString("\n#### 影响\n\n")
|
||||
b.WriteString(v.Impact)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
if v.Recommendation != "" {
|
||||
b.WriteString("\n#### 修复建议\n\n")
|
||||
b.WriteString(v.Recommendation)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, v := range values {
|
||||
trimmed := strings.TrimSpace(v)
|
||||
if trimmed != "" {
|
||||
return trimmed
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func sanitizeExportName(raw string) string {
|
||||
name := strings.TrimSpace(raw)
|
||||
if name == "" {
|
||||
return "unknown"
|
||||
}
|
||||
replacer := strings.NewReplacer("/", "-", "\\", "-", ":", "-", "*", "-", "?", "-", "\"", "-", "<", "-", ">", "-", "|", "-")
|
||||
return replacer.Replace(name)
|
||||
}
|
||||
|
||||
|
||||
@@ -112,7 +112,8 @@ func runEinoADKAgentLoop(ctx context.Context, args *einoADKRunLoopArgs, baseMsgs
|
||||
var lastRunMsgs []adk.Message
|
||||
var lastAssistant string
|
||||
var lastPlanExecuteExecutor string
|
||||
var retryHints []adk.Message
|
||||
msgs := append([]adk.Message(nil), baseMsgs...)
|
||||
runAccumulatedMsgs := append([]adk.Message(nil), msgs...)
|
||||
|
||||
emptyHint := strings.TrimSpace(args.EmptyResponseMessage)
|
||||
if emptyHint == "" {
|
||||
@@ -120,29 +121,17 @@ func runEinoADKAgentLoop(ctx context.Context, args *einoADKRunLoopArgs, baseMsgs
|
||||
"(Eino 会话已完成,但未捕获到助手文本输出。请查看过程详情或日志。)"
|
||||
}
|
||||
|
||||
attemptLoop:
|
||||
for attempt := 0; attempt < maxToolCallRecoveryAttempts; attempt++ {
|
||||
msgs := make([]adk.Message, 0, len(baseMsgs)+len(retryHints))
|
||||
msgs = append(msgs, baseMsgs...)
|
||||
msgs = append(msgs, retryHints...)
|
||||
|
||||
if attempt > 0 {
|
||||
mcpIDsMu.Lock()
|
||||
*mcpIDs = (*mcpIDs)[:0]
|
||||
mcpIDsMu.Unlock()
|
||||
}
|
||||
|
||||
lastAssistant = ""
|
||||
lastPlanExecuteExecutor = ""
|
||||
var reasoningStreamSeq int64
|
||||
var einoSubReplyStreamSeq int64
|
||||
toolEmitSeen := make(map[string]struct{})
|
||||
var einoMainRound int
|
||||
var einoLastAgent string
|
||||
subAgentToolStep := make(map[string]int)
|
||||
pendingByID := make(map[string]toolCallPendingInfo)
|
||||
pendingQueueByAgent := make(map[string][]string)
|
||||
markPending := func(tc toolCallPendingInfo) {
|
||||
lastAssistant = ""
|
||||
lastPlanExecuteExecutor = ""
|
||||
var reasoningStreamSeq int64
|
||||
var einoSubReplyStreamSeq int64
|
||||
toolEmitSeen := make(map[string]struct{})
|
||||
var einoMainRound int
|
||||
var einoLastAgent string
|
||||
subAgentToolStep := make(map[string]int)
|
||||
pendingByID := make(map[string]toolCallPendingInfo)
|
||||
pendingQueueByAgent := make(map[string][]string)
|
||||
markPending := func(tc toolCallPendingInfo) {
|
||||
if tc.ToolCallID == "" {
|
||||
return
|
||||
}
|
||||
@@ -218,11 +207,11 @@ attemptLoop:
|
||||
}
|
||||
}
|
||||
}
|
||||
runner := adk.NewRunner(ctx, runnerCfg)
|
||||
iter := runner.Run(ctx, msgs)
|
||||
handleRunErr := func(runErr error, attempt int, reasonOverride string) (retry bool, retErr error) {
|
||||
runner := adk.NewRunner(ctx, runnerCfg)
|
||||
iter := runner.Run(ctx, msgs)
|
||||
handleRunErr := func(runErr error) error {
|
||||
if runErr == nil {
|
||||
return false, nil
|
||||
return nil
|
||||
}
|
||||
if errors.Is(runErr, context.DeadlineExceeded) {
|
||||
flushAllPendingAsFailed(runErr)
|
||||
@@ -233,7 +222,7 @@ attemptLoop:
|
||||
"errorKind": "timeout",
|
||||
})
|
||||
}
|
||||
return false, runErr
|
||||
return runErr
|
||||
}
|
||||
// context.Canceled 是唯一应当直接终止编排的错误(用户关闭页面、主动停止等)。
|
||||
if errors.Is(runErr, context.Canceled) {
|
||||
@@ -244,7 +233,7 @@ attemptLoop:
|
||||
"source": "eino",
|
||||
})
|
||||
}
|
||||
return false, runErr
|
||||
return runErr
|
||||
}
|
||||
if isEinoIterationLimitError(runErr) {
|
||||
flushAllPendingAsFailed(runErr)
|
||||
@@ -260,57 +249,16 @@ attemptLoop:
|
||||
"errorKind": "iteration_limit",
|
||||
})
|
||||
}
|
||||
return false, runErr
|
||||
}
|
||||
|
||||
canRetry := attempt+1 < maxToolCallRecoveryAttempts
|
||||
if !canRetry {
|
||||
// 重试次数已耗尽,终止。
|
||||
flushAllPendingAsFailed(runErr)
|
||||
if progress != nil {
|
||||
progress("error", runErr.Error(), map[string]interface{}{
|
||||
"conversationId": conversationID,
|
||||
"source": "eino",
|
||||
})
|
||||
}
|
||||
return false, runErr
|
||||
}
|
||||
|
||||
// 区分错误类型以选择最合适的纠错提示,但无论哪种都执行重试(default-soft)。
|
||||
var hint *schema.Message
|
||||
var reason, timelineMsg string
|
||||
switch {
|
||||
case strings.TrimSpace(reasonOverride) != "":
|
||||
hint = toolExecutionRetryHint()
|
||||
reason = strings.TrimSpace(reasonOverride)
|
||||
timelineMsg = toolExecutionRecoveryTimelineMessage(attempt)
|
||||
case isRecoverableToolCallArgumentsJSONError(runErr):
|
||||
hint = toolCallArgumentsJSONRetryHint()
|
||||
reason = "invalid_tool_arguments_json"
|
||||
timelineMsg = toolCallArgumentsJSONRecoveryTimelineMessage(attempt)
|
||||
default:
|
||||
hint = toolExecutionRetryHint()
|
||||
reason = "tool_execution_error"
|
||||
timelineMsg = toolExecutionRecoveryTimelineMessage(attempt)
|
||||
}
|
||||
|
||||
if logger != nil {
|
||||
logger.Warn("eino: recoverable error, will retry with corrective hint",
|
||||
zap.Error(runErr), zap.Int("attempt", attempt), zap.String("reason", reason))
|
||||
return runErr
|
||||
}
|
||||
flushAllPendingAsFailed(runErr)
|
||||
retryHints = append(retryHints, hint)
|
||||
if progress != nil {
|
||||
progress("eino_recovery", timelineMsg, map[string]interface{}{
|
||||
progress("error", runErr.Error(), map[string]interface{}{
|
||||
"conversationId": conversationID,
|
||||
"source": "eino",
|
||||
"einoRetry": attempt,
|
||||
"runIndex": attempt + 1,
|
||||
"maxRuns": maxToolCallRecoveryAttempts,
|
||||
"reason": reason,
|
||||
})
|
||||
}
|
||||
return true, nil
|
||||
return runErr
|
||||
}
|
||||
|
||||
for {
|
||||
@@ -342,17 +290,15 @@ attemptLoop:
|
||||
})
|
||||
}
|
||||
}
|
||||
lastRunMsgs = msgs
|
||||
break attemptLoop
|
||||
lastRunMsgs = runAccumulatedMsgs
|
||||
break
|
||||
}
|
||||
if ev == nil {
|
||||
continue
|
||||
}
|
||||
if ev.Err != nil {
|
||||
if retry, retErr := handleRunErr(ev.Err, attempt, ""); retErr != nil {
|
||||
if retErr := handleRunErr(ev.Err); retErr != nil {
|
||||
return nil, retErr
|
||||
} else if retry {
|
||||
continue attemptLoop
|
||||
}
|
||||
}
|
||||
if ev.AgentName != "" && progress != nil {
|
||||
@@ -489,6 +435,7 @@ attemptLoop:
|
||||
if streamsMainAssistant(ev.AgentName) {
|
||||
if s := strings.TrimSpace(mainAssistantBuf.String()); s != "" {
|
||||
lastAssistant = s
|
||||
runAccumulatedMsgs = append(runAccumulatedMsgs, schema.AssistantMessage(s, nil))
|
||||
if orchMode == "plan_execute" && strings.EqualFold(strings.TrimSpace(ev.AgentName), "executor") {
|
||||
lastPlanExecuteExecutor = UnwrapPlanExecuteUserText(s)
|
||||
}
|
||||
@@ -528,10 +475,8 @@ attemptLoop:
|
||||
"einoRole": einoRoleTag(ev.AgentName),
|
||||
})
|
||||
}
|
||||
if retry, retErr := handleRunErr(streamRecvErr, attempt, "stream_recv_error"); retErr != nil {
|
||||
if retErr := handleRunErr(streamRecvErr); retErr != nil {
|
||||
return nil, retErr
|
||||
} else if retry {
|
||||
continue attemptLoop
|
||||
}
|
||||
}
|
||||
continue
|
||||
@@ -541,6 +486,7 @@ attemptLoop:
|
||||
if gerr != nil || msg == nil {
|
||||
continue
|
||||
}
|
||||
runAccumulatedMsgs = append(runAccumulatedMsgs, msg)
|
||||
tryEmitToolCallsOnce(mergeMessageToolCalls(msg), ev.AgentName, orchestratorName, conversationID, progress, toolEmitSeen, subAgentToolStep, markPending)
|
||||
|
||||
if mv.Role == schema.Assistant {
|
||||
@@ -640,7 +586,6 @@ attemptLoop:
|
||||
progress("tool_result", fmt.Sprintf("工具结果 (%s)", toolName), data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mcpIDsMu.Lock()
|
||||
ids := append([]string(nil), *mcpIDs...)
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
package multiagent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
// maxToolCallRecoveryAttempts 含首次运行:首次 + 自动重试次数。
|
||||
// 例如为 3 表示最多共 3 次完整 DeepAgent 运行(2 次失败后各追加一条纠错提示)。
|
||||
// 该常量同时用于 JSON 参数错误和工具执行错误(如子代理名称不存在)的恢复重试。
|
||||
const maxToolCallRecoveryAttempts = 5
|
||||
|
||||
// toolCallArgumentsJSONRetryHint 追加在用户消息后,提示模型输出合法 JSON 工具参数(部分云厂商会在流式阶段校验 arguments)。
|
||||
func toolCallArgumentsJSONRetryHint() *schema.Message {
|
||||
return schema.UserMessage(`[系统提示] 上一次输出中,工具调用的 function.arguments 不是合法 JSON,接口已拒绝。请重新生成:每个 tool call 的 arguments 必须是完整、可解析的 JSON 对象字符串(键名用双引号,无多余逗号,括号配对)。不要输出截断或不完整的 JSON。
|
||||
|
||||
[System] Your previous tool call used invalid JSON in function.arguments and was rejected by the API. Regenerate with strictly valid JSON objects only (double-quoted keys, matched braces, no trailing commas).`)
|
||||
}
|
||||
|
||||
// toolCallArgumentsJSONRecoveryTimelineMessage 供 eino_recovery 事件落库与前端时间线展示。
|
||||
func toolCallArgumentsJSONRecoveryTimelineMessage(attempt int) string {
|
||||
return fmt.Sprintf(
|
||||
"接口拒绝了无效的工具参数 JSON。已向对话追加系统提示并要求模型重新生成合法的 function.arguments。"+
|
||||
"当前为第 %d/%d 轮完整运行。\n\n"+
|
||||
"The API rejected invalid JSON in tool arguments. A system hint was appended. This is full run %d of %d.",
|
||||
attempt+1, maxToolCallRecoveryAttempts, attempt+1, maxToolCallRecoveryAttempts,
|
||||
)
|
||||
}
|
||||
|
||||
// isRecoverableToolCallArgumentsJSONError 判断是否为「工具参数非合法 JSON」类流式错误,可通过追加提示后重跑一轮。
|
||||
func isRecoverableToolCallArgumentsJSONError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
s := strings.ToLower(err.Error())
|
||||
if !strings.Contains(s, "json") {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(s, "function.arguments") || strings.Contains(s, "function arguments") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(s, "invalidparameter") && strings.Contains(s, "json") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(s, "must be in json format") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package multiagent
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsRecoverableToolCallArgumentsJSONError(t *testing.T) {
|
||||
yes := errors.New(`failed to receive stream chunk: error, <400> InternalError.Algo.InvalidParameter: The "function.arguments" parameter of the code model must be in JSON format.`)
|
||||
if !isRecoverableToolCallArgumentsJSONError(yes) {
|
||||
t.Fatal("expected recoverable for function.arguments + JSON")
|
||||
}
|
||||
no := errors.New("unrelated network failure")
|
||||
if isRecoverableToolCallArgumentsJSONError(no) {
|
||||
t.Fatal("expected not recoverable")
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package multiagent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
// toolExecutionRetryHint returns a user message appended to the conversation to prompt
|
||||
// the LLM to adjust after a tool execution error (tool not found, binary missing,
|
||||
// runtime failure, network error, etc.).
|
||||
func toolExecutionRetryHint() *schema.Message {
|
||||
return schema.UserMessage(`[System] Your previous tool call failed. Possible causes:
|
||||
- The tool or sub-agent name does not exist (typo or unregistered name).
|
||||
- The tool call arguments were not valid JSON.
|
||||
- The tool's underlying binary is not installed or not in PATH.
|
||||
- The tool encountered a runtime error (timeout, network failure, permission denied, etc.).
|
||||
|
||||
Please review the error message above, check available tools, and either:
|
||||
1. Retry with corrected arguments or a different tool, OR
|
||||
2. Inform the user about the limitation and proceed with an alternative approach.
|
||||
|
||||
[系统提示] 上一次工具调用失败,可能原因:
|
||||
- 工具名或子代理名称不存在(拼写错误或未注册);
|
||||
- 工具调用参数不是合法 JSON;
|
||||
- 工具依赖的底层二进制程序未安装或不在 PATH 中;
|
||||
- 工具运行时遇到错误(超时、网络故障、权限不足等)。
|
||||
|
||||
请根据上述错误信息检查可用工具,然后:
|
||||
1. 修正参数或改用其他工具重试,或者
|
||||
2. 告知用户当前限制并采用替代方案继续。`)
|
||||
}
|
||||
|
||||
// toolExecutionRecoveryTimelineMessage returns a message for the eino_recovery event
|
||||
// displayed in the UI timeline when a tool execution error triggers a retry.
|
||||
func toolExecutionRecoveryTimelineMessage(attempt int) string {
|
||||
return fmt.Sprintf(
|
||||
"工具调用执行失败。已向对话追加纠错提示并要求模型调整策略。"+
|
||||
"当前为第 %d/%d 轮完整运行。\n\n"+
|
||||
"Tool call execution failed. "+
|
||||
"A corrective hint was appended. This is full run %d of %d.",
|
||||
attempt+1, maxToolCallRecoveryAttempts, attempt+1, maxToolCallRecoveryAttempts,
|
||||
)
|
||||
}
|
||||
+81
-11
@@ -8970,7 +8970,7 @@ header {
|
||||
/* 任务管理 · 队列卡片:单行主网格 + 进度列内统计,降低高度 */
|
||||
.batch-queue-item__inner--grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(128px, auto) minmax(88px, 14%) 44px;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(128px, auto) minmax(88px, 14%) minmax(40px, max-content);
|
||||
grid-template-rows: auto;
|
||||
grid-template-areas: "lead cluster progress actions";
|
||||
column-gap: 22px;
|
||||
@@ -9051,6 +9051,12 @@ header {
|
||||
justify-self: end;
|
||||
align-self: center;
|
||||
padding-left: 6px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.batch-queue-item__idline--lead {
|
||||
@@ -9137,6 +9143,12 @@ header {
|
||||
}
|
||||
|
||||
.batch-queue-icon-btn:hover {
|
||||
color: var(--accent-color, #0066ff);
|
||||
border-color: rgba(0, 102, 255, 0.35);
|
||||
background: rgba(0, 102, 255, 0.08);
|
||||
}
|
||||
|
||||
.batch-queue-icon-btn--danger:hover {
|
||||
color: var(--error-color, #dc3545);
|
||||
border-color: rgba(220, 53, 69, 0.35);
|
||||
background: rgba(220, 53, 69, 0.06);
|
||||
@@ -13575,29 +13587,87 @@ header {
|
||||
|
||||
.vulnerability-details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px 16px;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 6px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
/* 元数据条数为奇数时,最后一项占满一行,长 URL/队列 ID 更易读 */
|
||||
.vulnerability-details .vuln-detail-field:last-child:nth-child(odd) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.vulnerability-details {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.vulnerability-details .vuln-detail-field:last-child:nth-child(odd) {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* 漏洞详情字段:标签与值分行,长 ID/URL 可换行、可选中复制 */
|
||||
.vuln-detail-field {
|
||||
min-width: 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.detail-item strong {
|
||||
.vuln-detail-field__label {
|
||||
color: var(--text-secondary);
|
||||
margin-right: 4px;
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
margin-bottom: 6px;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
.detail-item code {
|
||||
.vuln-detail-field__row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.vuln-detail-field-value {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
background: var(--bg-tertiary);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
border: 1px solid var(--border-color);
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.45;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre-wrap;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
color: var(--text-primary);
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.vuln-detail-field__copy {
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
padding: 6px;
|
||||
line-height: 0;
|
||||
border-radius: 6px;
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vuln-detail-field__copy:hover {
|
||||
color: var(--accent-color);
|
||||
background: var(--bg-primary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.vulnerability-proof,
|
||||
|
||||
@@ -178,7 +178,6 @@
|
||||
"taskCancelled": "Task cancelled",
|
||||
"unknownTool": "Unknown tool",
|
||||
"einoAgentReplyTitle": "Sub-agent reply",
|
||||
"einoRecoveryTitle": "🔄 Invalid tool JSON · run {{n}}/{{max}} (hint appended)",
|
||||
"einoStreamErrorTitle": "⚠️ Eino stream interrupted ({{agent}})",
|
||||
"einoStreamErrorMessage": "Streaming read failed; the system will retry or terminate according to policy.",
|
||||
"iterationLimitReachedTitle": "⛔ Iteration limit reached",
|
||||
@@ -304,6 +303,8 @@
|
||||
"clearHistory": "Clear history",
|
||||
"cancelTask": "Cancel task",
|
||||
"viewConversation": "View conversation",
|
||||
"viewVulnerabilities": "View vulnerabilities",
|
||||
"viewVulnerabilitiesQueueTitle": "View vulnerabilities: open management filtered to this queue",
|
||||
"retryTask": "Retry",
|
||||
"conversationIdLabel": "Conversation ID",
|
||||
"statusPending": "Pending",
|
||||
@@ -1313,6 +1314,12 @@
|
||||
"clear": "Clear",
|
||||
"vulnId": "Vuln ID",
|
||||
"conversationId": "Conversation ID",
|
||||
"taskOrQueueId": "Task / queue ID",
|
||||
"filterTaskOrQueue": "Filter by task or queue ID",
|
||||
"conversationTag": "Conversation tag",
|
||||
"filterConversationTag": "Filter by conversation tag",
|
||||
"taskTag": "Task tag",
|
||||
"filterTaskTag": "Filter by task tag",
|
||||
"severity": "Severity",
|
||||
"status": "Status",
|
||||
"statusOpen": "Open",
|
||||
@@ -1322,7 +1329,31 @@
|
||||
"searchVulnId": "Search vuln ID",
|
||||
"filterConversation": "Filter by conversation",
|
||||
"loading": "Loading...",
|
||||
"noRecords": "No vulnerability records"
|
||||
"loadListFailed": "Failed to load",
|
||||
"noRecords": "No vulnerability records",
|
||||
"batchExport": "Batch export",
|
||||
"downloadMarkdownTitle": "Download Markdown",
|
||||
"exportNoResults": "No vulnerabilities match the current filters",
|
||||
"exportStarted": "Started downloading {{count}} file(s)",
|
||||
"exportFailed": "Export failed",
|
||||
"saveRequiredFields": "Please fill in conversation ID, title, and severity",
|
||||
"saveFailed": "Save failed",
|
||||
"fetchFailed": "Failed to fetch vulnerability",
|
||||
"deleteFailed": "Delete failed",
|
||||
"detailVulnId": "Vuln ID",
|
||||
"detailType": "Type",
|
||||
"detailTarget": "Target",
|
||||
"detailConversationId": "Conversation ID",
|
||||
"detailTaskId": "Task ID",
|
||||
"detailTaskQueueId": "Task queue ID",
|
||||
"detailConversationTag": "Conversation tag",
|
||||
"detailTaskTag": "Task tag",
|
||||
"detailProof": "Proof",
|
||||
"detailImpact": "Impact",
|
||||
"detailRecommendation": "Remediation",
|
||||
"downloadOkTitle": "Downloaded",
|
||||
"exportFailedMessage": "Export failed",
|
||||
"downloadFailed": "Download failed"
|
||||
},
|
||||
"tasksPage": {
|
||||
"statusFilter": "Status filter",
|
||||
@@ -1673,6 +1704,7 @@
|
||||
},
|
||||
"contextMenu": {
|
||||
"viewAttackChain": "View attack chain",
|
||||
"viewVulnerabilities": "View vulnerabilities",
|
||||
"downloadMarkdown": "Download Markdown",
|
||||
"downloadMarkdownSummary": "Summary",
|
||||
"downloadMarkdownFull": "Full",
|
||||
@@ -1768,6 +1800,10 @@
|
||||
"vulnerabilityModal": {
|
||||
"conversationId": "Conversation ID",
|
||||
"conversationIdPlaceholder": "Enter conversation ID",
|
||||
"conversationTag": "Conversation tag",
|
||||
"conversationTagPlaceholder": "e.g. engagement A, weekly report",
|
||||
"taskTag": "Task tag",
|
||||
"taskTagPlaceholder": "e.g. batch scan Q2, retest",
|
||||
"title": "Title",
|
||||
"titlePlaceholder": "Vulnerability title",
|
||||
"description": "Description",
|
||||
@@ -1795,6 +1831,25 @@
|
||||
"recommendation": "Recommendation",
|
||||
"recommendationPlaceholder": "Remediation"
|
||||
},
|
||||
"vulnerabilityMd": {
|
||||
"headingBasic": "Basic information",
|
||||
"labelId": "Vulnerability ID",
|
||||
"labelSeverity": "Severity",
|
||||
"labelStatus": "Status",
|
||||
"labelType": "Type",
|
||||
"labelTarget": "Target",
|
||||
"labelConversationId": "Conversation ID",
|
||||
"labelTaskId": "Task ID",
|
||||
"labelTaskQueueId": "Task queue ID",
|
||||
"labelConversationTag": "Conversation tag",
|
||||
"labelTaskTag": "Task tag",
|
||||
"labelCreated": "Created at",
|
||||
"labelUpdated": "Updated at",
|
||||
"headingDescription": "Description",
|
||||
"headingProof": "Proof (POC)",
|
||||
"headingImpact": "Impact",
|
||||
"headingRecommendation": "Remediation"
|
||||
},
|
||||
"roleModal": {
|
||||
"addRole": "Add role",
|
||||
"editRole": "Edit role",
|
||||
|
||||
@@ -178,7 +178,6 @@
|
||||
"taskCancelled": "任务已取消",
|
||||
"unknownTool": "未知工具",
|
||||
"einoAgentReplyTitle": "子代理回复",
|
||||
"einoRecoveryTitle": "🔄 工具参数无效 · 第 {{n}}/{{max}} 轮(已追加提示)",
|
||||
"einoStreamErrorTitle": "⚠️ Eino 流式中断({{agent}})",
|
||||
"einoStreamErrorMessage": "流式读取异常,系统将按策略重试或结束。",
|
||||
"iterationLimitReachedTitle": "⛔ 达到迭代上限",
|
||||
@@ -304,6 +303,8 @@
|
||||
"clearHistory": "清空历史",
|
||||
"cancelTask": "取消任务",
|
||||
"viewConversation": "查看对话",
|
||||
"viewVulnerabilities": "查看漏洞",
|
||||
"viewVulnerabilitiesQueueTitle": "查看漏洞:打开漏洞管理并筛选本队列",
|
||||
"retryTask": "重试",
|
||||
"conversationIdLabel": "对话ID",
|
||||
"statusPending": "待执行",
|
||||
@@ -1313,6 +1314,12 @@
|
||||
"clear": "清除",
|
||||
"vulnId": "漏洞ID",
|
||||
"conversationId": "会话ID",
|
||||
"taskOrQueueId": "任务ID/队列ID",
|
||||
"filterTaskOrQueue": "筛选任务ID或队列ID",
|
||||
"conversationTag": "对话标签",
|
||||
"filterConversationTag": "筛选对话标签",
|
||||
"taskTag": "任务标签",
|
||||
"filterTaskTag": "筛选任务标签",
|
||||
"severity": "严重程度",
|
||||
"status": "状态",
|
||||
"statusOpen": "待处理",
|
||||
@@ -1322,7 +1329,31 @@
|
||||
"searchVulnId": "搜索漏洞ID",
|
||||
"filterConversation": "筛选特定会话",
|
||||
"loading": "加载中...",
|
||||
"noRecords": "暂无漏洞记录"
|
||||
"loadListFailed": "加载失败",
|
||||
"noRecords": "暂无漏洞记录",
|
||||
"batchExport": "批量导出",
|
||||
"downloadMarkdownTitle": "下载 Markdown",
|
||||
"exportNoResults": "当前筛选条件下无可导出漏洞",
|
||||
"exportStarted": "已开始下载 {{count}} 份报告",
|
||||
"exportFailed": "导出失败",
|
||||
"saveRequiredFields": "请填写必填字段:会话ID、标题和严重程度",
|
||||
"saveFailed": "保存失败",
|
||||
"fetchFailed": "获取漏洞失败",
|
||||
"deleteFailed": "删除失败",
|
||||
"detailVulnId": "漏洞ID",
|
||||
"detailType": "类型",
|
||||
"detailTarget": "目标",
|
||||
"detailConversationId": "会话ID",
|
||||
"detailTaskId": "任务ID",
|
||||
"detailTaskQueueId": "任务队列ID",
|
||||
"detailConversationTag": "对话标签",
|
||||
"detailTaskTag": "任务标签",
|
||||
"detailProof": "证明",
|
||||
"detailImpact": "影响",
|
||||
"detailRecommendation": "修复建议",
|
||||
"downloadOkTitle": "下载成功",
|
||||
"exportFailedMessage": "导出失败",
|
||||
"downloadFailed": "下载失败"
|
||||
},
|
||||
"tasksPage": {
|
||||
"statusFilter": "状态筛选",
|
||||
@@ -1673,6 +1704,7 @@
|
||||
},
|
||||
"contextMenu": {
|
||||
"viewAttackChain": "查看攻击链",
|
||||
"viewVulnerabilities": "查看漏洞",
|
||||
"downloadMarkdown": "下载 Markdown",
|
||||
"downloadMarkdownSummary": "简版",
|
||||
"downloadMarkdownFull": "完整版",
|
||||
@@ -1768,6 +1800,10 @@
|
||||
"vulnerabilityModal": {
|
||||
"conversationId": "会话ID",
|
||||
"conversationIdPlaceholder": "输入会话ID",
|
||||
"conversationTag": "对话标签",
|
||||
"conversationTagPlaceholder": "如:红队演练A、客户A周报",
|
||||
"taskTag": "任务标签",
|
||||
"taskTagPlaceholder": "如:批量扫描Q2、专项复测",
|
||||
"title": "标题",
|
||||
"titlePlaceholder": "漏洞标题",
|
||||
"description": "描述",
|
||||
@@ -1795,6 +1831,25 @@
|
||||
"recommendation": "修复建议",
|
||||
"recommendationPlaceholder": "修复建议"
|
||||
},
|
||||
"vulnerabilityMd": {
|
||||
"headingBasic": "基本信息",
|
||||
"labelId": "漏洞ID",
|
||||
"labelSeverity": "严重程度",
|
||||
"labelStatus": "状态",
|
||||
"labelType": "类型",
|
||||
"labelTarget": "目标",
|
||||
"labelConversationId": "会话ID",
|
||||
"labelTaskId": "任务ID",
|
||||
"labelTaskQueueId": "任务队列ID",
|
||||
"labelConversationTag": "对话标签",
|
||||
"labelTaskTag": "任务标签",
|
||||
"labelCreated": "创建时间",
|
||||
"labelUpdated": "更新时间",
|
||||
"headingDescription": "描述",
|
||||
"headingProof": "证明(POC)",
|
||||
"headingImpact": "影响",
|
||||
"headingRecommendation": "修复建议"
|
||||
},
|
||||
"roleModal": {
|
||||
"addRole": "添加角色",
|
||||
"editRole": "编辑角色",
|
||||
|
||||
+11
-4
@@ -2226,10 +2226,6 @@ function renderProcessDetails(messageId, processDetails) {
|
||||
itemTitle = agPx + execLine;
|
||||
} else if (eventType === 'eino_agent_reply') {
|
||||
itemTitle = agPx + '💬 ' + (typeof window.t === 'function' ? window.t('chat.einoAgentReplyTitle') : '子代理回复');
|
||||
} else if (eventType === 'eino_recovery') {
|
||||
const ri = data.runIndex != null ? data.runIndex : (data.einoRetry != null ? data.einoRetry + 1 : 1);
|
||||
const mx = data.maxRuns != null ? data.maxRuns : 3;
|
||||
itemTitle = (typeof window.t === 'function' ? window.t('chat.einoRecoveryTitle', { n: ri, max: mx }) : ('🔄 第 ' + ri + '/' + mx + ' 轮(已追加提示)'));
|
||||
} else if (eventType === 'knowledge_retrieval') {
|
||||
itemTitle = '📚 ' + (typeof window.t === 'function' ? window.t('chat.knowledgeRetrieval') : '知识检索');
|
||||
} else if (eventType === 'error') {
|
||||
@@ -6125,6 +6121,17 @@ async function downloadConversationMarkdownFromContext(includeToolDetails = fals
|
||||
closeContextMenu();
|
||||
}
|
||||
|
||||
// 从上下文菜单跳转到漏洞管理,并按当前对话 ID 筛选
|
||||
function navigateToVulnerabilitiesForContextConversation() {
|
||||
const convId = contextMenuConversationId;
|
||||
if (!convId) {
|
||||
closeContextMenu();
|
||||
return;
|
||||
}
|
||||
closeContextMenu();
|
||||
window.location.hash = 'vulnerabilities?conversation_id=' + encodeURIComponent(convId);
|
||||
}
|
||||
|
||||
// 从上下文菜单删除对话
|
||||
function deleteConversationFromContext() {
|
||||
const convId = contextMenuConversationId;
|
||||
|
||||
@@ -1133,24 +1133,6 @@ function handleStreamEvent(event, progressElement, progressId,
|
||||
});
|
||||
break;
|
||||
|
||||
case 'eino_recovery': {
|
||||
const d = event.data || {};
|
||||
const runIdx = d.runIndex != null ? d.runIndex : (d.einoRetry != null ? d.einoRetry + 1 : 1);
|
||||
const maxRuns = d.maxRuns != null ? d.maxRuns : 3;
|
||||
const title = typeof window.t === 'function'
|
||||
? window.t('chat.einoRecoveryTitle', { n: runIdx, max: maxRuns })
|
||||
: ('🔄 工具参数无效 · 第 ' + runIdx + '/' + maxRuns + ' 轮(已追加提示)');
|
||||
addTimelineItem(timeline, 'eino_recovery', {
|
||||
title: title,
|
||||
message: event.message || '',
|
||||
data: event.data
|
||||
});
|
||||
// If the backend triggers a recovery run, any "running" tool_call items in this progress
|
||||
// should be closed to avoid being stuck forever.
|
||||
finalizeOutstandingToolCallsForProgress(progressId, 'failed');
|
||||
break;
|
||||
}
|
||||
|
||||
case 'eino_stream_error': {
|
||||
const d = event.data || {};
|
||||
const agent = d.einoAgent ? String(d.einoAgent) : '';
|
||||
@@ -2190,15 +2172,6 @@ function addTimelineItem(timeline, type, options) {
|
||||
if (type === 'progress' && options.message) {
|
||||
item.dataset.progressMessage = options.message;
|
||||
}
|
||||
if (type === 'eino_recovery' && options.data) {
|
||||
const d = options.data;
|
||||
if (d.runIndex != null) {
|
||||
item.dataset.recoveryRunIndex = String(d.runIndex);
|
||||
}
|
||||
if (d.maxRuns != null) {
|
||||
item.dataset.recoveryMaxRuns = String(d.maxRuns);
|
||||
}
|
||||
}
|
||||
if (type === 'tool_calls_detected' && options.data && options.data.count != null) {
|
||||
item.dataset.toolCallsCount = String(options.data.count);
|
||||
}
|
||||
@@ -2309,12 +2282,6 @@ function addTimelineItem(timeline, type, options) {
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else if (type === 'eino_recovery' && options.message) {
|
||||
content += `
|
||||
<div class="timeline-item-content timeline-eino-recovery">
|
||||
${escapeHtml(options.message).replace(/\n/g, '<br>')}
|
||||
</div>
|
||||
`;
|
||||
} else if (type === 'cancelled') {
|
||||
const taskCancelledLabel = typeof window.t === 'function' ? window.t('chat.taskCancelled') : '任务已取消';
|
||||
content += `
|
||||
@@ -3197,10 +3164,6 @@ function refreshProgressAndTimelineI18n() {
|
||||
titleSpan.textContent = ap + icon + (success ? _t('chat.toolExecComplete', { name: name }) : _t('chat.toolExecFailed', { name: name }));
|
||||
} else if (type === 'eino_agent_reply') {
|
||||
titleSpan.textContent = ap + '\uD83D\uDCAC ' + _t('chat.einoAgentReplyTitle');
|
||||
} else if (type === 'eino_recovery' && item.dataset.recoveryRunIndex) {
|
||||
const n = parseInt(item.dataset.recoveryRunIndex, 10) || 1;
|
||||
const mx = parseInt(item.dataset.recoveryMaxRuns, 10) || 3;
|
||||
titleSpan.textContent = _t('chat.einoRecoveryTitle', { n: n, max: mx });
|
||||
} else if (type === 'cancelled') {
|
||||
titleSpan.textContent = '\u26D4 ' + _t('chat.taskCancelled');
|
||||
} else if (type === 'progress' && item.dataset.progressMessage !== undefined) {
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
// 页面路由管理
|
||||
let currentPage = 'dashboard';
|
||||
|
||||
/** 仅当停留在 chat 时保留 ?conversation= 等查询串,其它页面只使用 pageId */
|
||||
/** chat、漏洞管理页在切换时保留当前 hash 上的查询串(如 ?conversation= / ?conversation_id=) */
|
||||
function buildHashForPage(pageId) {
|
||||
if (pageId !== 'chat') {
|
||||
if (pageId !== 'chat' && pageId !== 'vulnerabilities') {
|
||||
return pageId;
|
||||
}
|
||||
const full = window.location.hash.slice(1);
|
||||
const parts = full.split('?');
|
||||
const curPage = parts[0];
|
||||
const q = parts.length > 1 ? parts.slice(1).join('?') : '';
|
||||
if (curPage === 'chat' && q) {
|
||||
return 'chat?' + q;
|
||||
if (curPage === pageId && q) {
|
||||
return pageId + '?' + q;
|
||||
}
|
||||
return 'chat';
|
||||
return pageId;
|
||||
}
|
||||
|
||||
let chatConversationFromHashSeq = 0;
|
||||
|
||||
+16
-2
@@ -531,6 +531,7 @@ function renderTaskItem(task, statusMap, isHistory = false) {
|
||||
${isHistory && completedText ? completedText : timeText}
|
||||
</span>
|
||||
${canCancel ? `<button class="btn-secondary btn-small" onclick="cancelTask('${task.conversationId}', this)">` + _t('tasks.cancelTask') + `</button>` : ''}
|
||||
${task.conversationId ? `<button class="btn-secondary btn-small" onclick="navigateToVulnerabilitiesFromTasksPage('conversation', '${task.conversationId}')">` + _t('tasks.viewVulnerabilities') + `</button>` : ''}
|
||||
${task.conversationId ? `<button class="btn-secondary btn-small" onclick="viewConversation('${task.conversationId}')">` + _t('tasks.viewConversation') + `</button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
@@ -708,6 +709,17 @@ function viewConversation(conversationId) {
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转漏洞管理并按对话 ID 或批量队列 ID 筛选(队列 ID 走 task_id,与列表筛选项一致)
|
||||
function navigateToVulnerabilitiesFromTasksPage(kind, id) {
|
||||
if (!id) return;
|
||||
const enc = encodeURIComponent(id);
|
||||
if (kind === 'queue') {
|
||||
window.location.hash = 'vulnerabilities?task_id=' + enc;
|
||||
} else if (kind === 'conversation') {
|
||||
window.location.hash = 'vulnerabilities?conversation_id=' + enc;
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新任务列表
|
||||
async function refreshTasks() {
|
||||
await loadTasks();
|
||||
@@ -1134,6 +1146,8 @@ function renderBatchQueues() {
|
||||
const progress = stats.total > 0 ? Math.round((stats.completed + stats.failed + stats.cancelled) / stats.total * 100) : 0;
|
||||
// 允许删除待执行、已完成或已取消状态的队列
|
||||
const canDelete = queue.status === 'pending' || queue.status === 'completed' || queue.status === 'cancelled';
|
||||
// 操作列常驻「查看漏洞」,不再使用 --no-actions 隐藏整列(否则无法从运行中队列跳转漏洞页)
|
||||
const noActionsClass = '';
|
||||
|
||||
const loadedRoles = batchQueuesState.loadedRoles || [];
|
||||
const roleIcon = getRoleIconForDisplay(queue.role, loadedRoles);
|
||||
@@ -1157,7 +1171,6 @@ function renderBatchQueues() {
|
||||
: `<h4 class="batch-queue-card-title batch-queue-card-title--muted">${escapeHtml(_t('tasks.batchQueueUntitled'))}</h4>`;
|
||||
const doneCount = stats.completed + stats.failed + stats.cancelled;
|
||||
|
||||
const noActionsClass = canDelete ? '' : ' batch-queue-item--no-actions';
|
||||
return `
|
||||
<div class="batch-queue-item batch-queue-item--compact${cardMod}${noActionsClass}" data-queue-id="${queue.id}" onclick="showBatchQueueDetail('${queue.id}')">
|
||||
<div class="batch-queue-item__inner batch-queue-item__inner--grid">
|
||||
@@ -1182,7 +1195,8 @@ function renderBatchQueues() {
|
||||
</div>
|
||||
</div>
|
||||
<div class="batch-queue-item__actions-col" onclick="event.stopPropagation();">
|
||||
${canDelete ? `<button type="button" class="batch-queue-icon-btn" onclick="deleteBatchQueueFromList('${queue.id}')" title="${escapeHtml(_t('tasks.deleteQueue'))}" aria-label="${escapeHtml(_t('tasks.deleteQueue'))}"><svg class="batch-queue-icon-btn__svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M10 11v6"/><path d="M14 11v6"/></svg></button>` : ''}
|
||||
<button type="button" class="batch-queue-icon-btn" onclick="navigateToVulnerabilitiesFromTasksPage('queue', '${queue.id}')" title="${escapeHtml(_t('tasks.viewVulnerabilitiesQueueTitle'))}" aria-label="${escapeHtml(_t('tasks.viewVulnerabilitiesQueueTitle'))}"><svg class="batch-queue-icon-btn__svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M9 12l2 2 4-4"/></svg></button>
|
||||
${canDelete ? `<button type="button" class="batch-queue-icon-btn batch-queue-icon-btn--danger" onclick="deleteBatchQueueFromList('${queue.id}')" title="${escapeHtml(_t('tasks.deleteQueue'))}" aria-label="${escapeHtml(_t('tasks.deleteQueue'))}"><svg class="batch-queue-icon-btn__svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M10 11v6"/><path d="M14 11v6"/></svg></button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+316
-82
@@ -1,5 +1,43 @@
|
||||
// 漏洞管理相关功能
|
||||
|
||||
function vulnT(key, opts) {
|
||||
if (typeof window.t === 'function') {
|
||||
return window.t(key, opts);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
function vulnDateLocale() {
|
||||
try {
|
||||
const lang = (window.__locale || '').toLowerCase();
|
||||
if (lang.indexOf('zh') === 0) {
|
||||
return 'zh-CN';
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
return 'en-US';
|
||||
}
|
||||
|
||||
function vulnSeverityLabel(code) {
|
||||
const m = {
|
||||
critical: 'dashboard.severityCritical',
|
||||
high: 'dashboard.severityHigh',
|
||||
medium: 'dashboard.severityMedium',
|
||||
low: 'dashboard.severityLow',
|
||||
info: 'dashboard.severityInfo'
|
||||
};
|
||||
return m[code] ? vulnT(m[code]) : code;
|
||||
}
|
||||
|
||||
function vulnStatusLabel(code) {
|
||||
const m = {
|
||||
open: 'vulnerabilityPage.statusOpen',
|
||||
confirmed: 'vulnerabilityPage.statusConfirmed',
|
||||
fixed: 'vulnerabilityPage.statusFixed',
|
||||
false_positive: 'vulnerabilityPage.statusFalsePositive'
|
||||
};
|
||||
return m[code] ? vulnT(m[code]) : code;
|
||||
}
|
||||
|
||||
// 从localStorage读取每页显示数量,默认为20
|
||||
const getVulnerabilityPageSize = () => {
|
||||
const saved = localStorage.getItem('vulnerabilityPageSize');
|
||||
@@ -10,6 +48,9 @@ let currentVulnerabilityId = null;
|
||||
let vulnerabilityFilters = {
|
||||
id: '',
|
||||
conversation_id: '',
|
||||
task_id: '',
|
||||
conversation_tag: '',
|
||||
task_tag: '',
|
||||
severity: '',
|
||||
status: ''
|
||||
};
|
||||
@@ -20,10 +61,43 @@ let vulnerabilityPagination = {
|
||||
totalPages: 1
|
||||
};
|
||||
|
||||
// 从地址栏 #vulnerabilities?conversation_id= / ?task_id= 同步筛选(对话菜单、任务管理联动)
|
||||
function syncVulnerabilityFiltersFromLocationHash() {
|
||||
const hash = window.location.hash.slice(1);
|
||||
const hashParts = hash.split('?');
|
||||
if (hashParts[0] !== 'vulnerabilities' || hashParts.length < 2) {
|
||||
return;
|
||||
}
|
||||
const params = new URLSearchParams(hashParts.slice(1).join('?'));
|
||||
const cid = (params.get('conversation_id') || '').trim();
|
||||
const tid = (params.get('task_id') || '').trim();
|
||||
if (!cid && !tid) {
|
||||
return;
|
||||
}
|
||||
|
||||
vulnerabilityFilters.conversation_id = '';
|
||||
vulnerabilityFilters.task_id = '';
|
||||
const convEl = document.getElementById('vulnerability-conversation-filter');
|
||||
const taskEl = document.getElementById('vulnerability-task-filter');
|
||||
if (convEl) convEl.value = '';
|
||||
if (taskEl) taskEl.value = '';
|
||||
|
||||
if (cid) {
|
||||
vulnerabilityFilters.conversation_id = cid;
|
||||
if (convEl) convEl.value = cid;
|
||||
}
|
||||
if (tid) {
|
||||
vulnerabilityFilters.task_id = tid;
|
||||
if (taskEl) taskEl.value = tid;
|
||||
}
|
||||
vulnerabilityPagination.currentPage = 1;
|
||||
}
|
||||
|
||||
// 初始化漏洞管理页面
|
||||
function initVulnerabilityPage() {
|
||||
// 从localStorage加载每页条数设置
|
||||
vulnerabilityPagination.pageSize = getVulnerabilityPageSize();
|
||||
syncVulnerabilityFiltersFromLocationHash();
|
||||
loadVulnerabilityStats();
|
||||
loadVulnerabilities();
|
||||
}
|
||||
@@ -41,6 +115,9 @@ async function loadVulnerabilityStats() {
|
||||
if (vulnerabilityFilters.conversation_id) {
|
||||
params.append('conversation_id', vulnerabilityFilters.conversation_id);
|
||||
}
|
||||
if (vulnerabilityFilters.task_id) {
|
||||
params.append('task_id', vulnerabilityFilters.task_id);
|
||||
}
|
||||
|
||||
const response = await apiFetch(`/api/vulnerabilities/stats?${params.toString()}`);
|
||||
if (!response.ok) {
|
||||
@@ -82,7 +159,7 @@ function updateVulnerabilityStats(stats) {
|
||||
// 加载漏洞列表
|
||||
async function loadVulnerabilities(page = null) {
|
||||
const listContainer = document.getElementById('vulnerabilities-list');
|
||||
listContainer.innerHTML = '<div class="loading-spinner">加载中...</div>';
|
||||
listContainer.innerHTML = `<div class="loading-spinner">${escapeHtml(vulnT('vulnerabilityPage.loading'))}</div>`;
|
||||
|
||||
try {
|
||||
// 检查apiFetch是否可用
|
||||
@@ -106,6 +183,15 @@ async function loadVulnerabilities(page = null) {
|
||||
if (vulnerabilityFilters.conversation_id) {
|
||||
params.append('conversation_id', vulnerabilityFilters.conversation_id);
|
||||
}
|
||||
if (vulnerabilityFilters.task_id) {
|
||||
params.append('task_id', vulnerabilityFilters.task_id);
|
||||
}
|
||||
if (vulnerabilityFilters.conversation_tag) {
|
||||
params.append('conversation_tag', vulnerabilityFilters.conversation_tag);
|
||||
}
|
||||
if (vulnerabilityFilters.task_tag) {
|
||||
params.append('task_tag', vulnerabilityFilters.task_tag);
|
||||
}
|
||||
if (vulnerabilityFilters.severity) {
|
||||
params.append('severity', vulnerabilityFilters.severity);
|
||||
}
|
||||
@@ -148,7 +234,7 @@ async function loadVulnerabilities(page = null) {
|
||||
renderVulnerabilityPagination();
|
||||
} catch (error) {
|
||||
console.error('加载漏洞列表失败:', error);
|
||||
listContainer.innerHTML = `<div class="error-message">加载失败: ${error.message}</div>`;
|
||||
listContainer.innerHTML = `<div class="error-message">${escapeHtml(vulnT('vulnerabilityPage.loadListFailed'))}: ${escapeHtml(error.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,22 +266,12 @@ function renderVulnerabilities(vulnerabilities) {
|
||||
|
||||
const html = vulnerabilities.map(vuln => {
|
||||
const severityClass = `severity-${vuln.severity}`;
|
||||
const severityText = {
|
||||
'critical': '严重',
|
||||
'high': '高危',
|
||||
'medium': '中危',
|
||||
'low': '低危',
|
||||
'info': '信息'
|
||||
}[vuln.severity] || vuln.severity;
|
||||
|
||||
const statusText = {
|
||||
'open': '待处理',
|
||||
'confirmed': '已确认',
|
||||
'fixed': '已修复',
|
||||
'false_positive': '误报'
|
||||
}[vuln.status] || vuln.status;
|
||||
|
||||
const createdDate = new Date(vuln.created_at).toLocaleString('zh-CN');
|
||||
const severityText = vulnSeverityLabel(vuln.severity);
|
||||
const statusText = vulnStatusLabel(vuln.status);
|
||||
const createdDate = new Date(vuln.created_at).toLocaleString(vulnDateLocale());
|
||||
const dlTitle = escapeHtml(vulnT('vulnerabilityPage.downloadMarkdownTitle'));
|
||||
const editTitle = escapeHtml(vulnT('common.edit'));
|
||||
const deleteTitle = escapeHtml(vulnT('common.delete'));
|
||||
|
||||
return `
|
||||
<div class="vulnerability-card ${severityClass}">
|
||||
@@ -214,20 +290,20 @@ function renderVulnerabilities(vulnerabilities) {
|
||||
</div>
|
||||
</div>
|
||||
<div class="vulnerability-actions" onclick="event.stopPropagation();">
|
||||
<button class="btn-ghost" onclick="downloadVulnerabilityAsMarkdown('${vuln.id}', event)" title="下载Markdown">
|
||||
<button class="btn-ghost" onclick="downloadVulnerabilityAsMarkdown('${vuln.id}', event)" title="${dlTitle}">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<polyline points="7 10 12 15 17 10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="12" y1="15" x2="12" y2="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-ghost" onclick="editVulnerability('${vuln.id}')" title="编辑">
|
||||
<button class="btn-ghost" onclick="editVulnerability('${vuln.id}')" title="${editTitle}">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-ghost" onclick="deleteVulnerability('${vuln.id}')" title="删除">
|
||||
<button class="btn-ghost" onclick="deleteVulnerability('${vuln.id}')" title="${deleteTitle}">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6h14z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
@@ -237,20 +313,27 @@ 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>
|
||||
${vulnDetailField(vulnT('vulnerabilityPage.detailVulnId'), vuln.id, true)}
|
||||
${vuln.type ? vulnDetailField(vulnT('vulnerabilityPage.detailType'), vuln.type, false) : ''}
|
||||
${vuln.target ? vulnDetailField(vulnT('vulnerabilityPage.detailTarget'), vuln.target, false) : ''}
|
||||
${vulnDetailField(vulnT('vulnerabilityPage.detailConversationId'), vuln.conversation_id, true)}
|
||||
${vuln.task_id ? vulnDetailField(vulnT('vulnerabilityPage.detailTaskId'), vuln.task_id, true) : ''}
|
||||
${vuln.task_queue_id ? vulnDetailField(vulnT('vulnerabilityPage.detailTaskQueueId'), vuln.task_queue_id, true) : ''}
|
||||
${vuln.conversation_tag ? vulnDetailField(vulnT('vulnerabilityPage.detailConversationTag'), vuln.conversation_tag, false) : ''}
|
||||
${vuln.task_tag ? vulnDetailField(vulnT('vulnerabilityPage.detailTaskTag'), vuln.task_tag, false) : ''}
|
||||
</div>
|
||||
${vuln.proof ? `<div class="vulnerability-proof"><strong>证明:</strong><pre>${escapeHtml(vuln.proof)}</pre></div>` : ''}
|
||||
${vuln.impact ? `<div class="vulnerability-impact"><strong>影响:</strong> ${escapeHtml(vuln.impact)}</div>` : ''}
|
||||
${vuln.recommendation ? `<div class="vulnerability-recommendation"><strong>修复建议:</strong> ${escapeHtml(vuln.recommendation)}</div>` : ''}
|
||||
${vuln.proof ? `<div class="vulnerability-proof"><strong>${escapeHtml(vulnT('vulnerabilityPage.detailProof'))}:</strong><pre>${escapeHtml(vuln.proof)}</pre></div>` : ''}
|
||||
${vuln.impact ? `<div class="vulnerability-impact"><strong>${escapeHtml(vulnT('vulnerabilityPage.detailImpact'))}:</strong> ${escapeHtml(vuln.impact)}</div>` : ''}
|
||||
${vuln.recommendation ? `<div class="vulnerability-recommendation"><strong>${escapeHtml(vulnT('vulnerabilityPage.detailRecommendation'))}:</strong> ${escapeHtml(vuln.recommendation)}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
listContainer.innerHTML = html;
|
||||
if (typeof window.applyTranslations === 'function') {
|
||||
window.applyTranslations(listContainer);
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染分页控件
|
||||
@@ -277,9 +360,9 @@ function renderVulnerabilityPagination() {
|
||||
// 左侧:显示范围信息和每页数量选择器(参考Skills样式)
|
||||
paginationHTML += `
|
||||
<div class="pagination-info">
|
||||
<span>显示 ${start}-${end} / 共 ${total} 条</span>
|
||||
<span>${escapeHtml(vulnT('skillsPage.paginationShow', { start, end, total }))}</span>
|
||||
<label class="pagination-page-size">
|
||||
每页显示
|
||||
${escapeHtml(vulnT('skillsPage.perPageLabel'))}
|
||||
<select id="vulnerability-page-size-pagination" onchange="changeVulnerabilityPageSize()">
|
||||
<option value="10" ${pageSize === 10 ? 'selected' : ''}>10</option>
|
||||
<option value="20" ${pageSize === 20 ? 'selected' : ''}>20</option>
|
||||
@@ -293,17 +376,20 @@ function renderVulnerabilityPagination() {
|
||||
// 右侧:分页按钮(参考Skills样式:首页、上一页、第X/Y页、下一页、末页)
|
||||
paginationHTML += `
|
||||
<div class="pagination-controls">
|
||||
<button class="btn-secondary" onclick="loadVulnerabilities(1)" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>首页</button>
|
||||
<button class="btn-secondary" onclick="loadVulnerabilities(${currentPage - 1})" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>上一页</button>
|
||||
<span class="pagination-page">第 ${currentPage} / ${totalPages || 1} 页</span>
|
||||
<button class="btn-secondary" onclick="loadVulnerabilities(${currentPage + 1})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>下一页</button>
|
||||
<button class="btn-secondary" onclick="loadVulnerabilities(${totalPages || 1})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>末页</button>
|
||||
<button class="btn-secondary" onclick="loadVulnerabilities(1)" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>${escapeHtml(vulnT('skillsPage.firstPage'))}</button>
|
||||
<button class="btn-secondary" onclick="loadVulnerabilities(${currentPage - 1})" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>${escapeHtml(vulnT('skillsPage.prevPage'))}</button>
|
||||
<span class="pagination-page">${escapeHtml(vulnT('skillsPage.pageOf', { current: currentPage, total: totalPages || 1 }))}</span>
|
||||
<button class="btn-secondary" onclick="loadVulnerabilities(${currentPage + 1})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>${escapeHtml(vulnT('skillsPage.nextPage'))}</button>
|
||||
<button class="btn-secondary" onclick="loadVulnerabilities(${totalPages || 1})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>${escapeHtml(vulnT('skillsPage.lastPage'))}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
paginationHTML += '</div>';
|
||||
|
||||
paginationContainer.innerHTML = paginationHTML;
|
||||
if (typeof window.applyTranslations === 'function') {
|
||||
window.applyTranslations(paginationContainer);
|
||||
}
|
||||
}
|
||||
|
||||
// 改变每页显示数量
|
||||
@@ -334,10 +420,12 @@ async function changeVulnerabilityPageSize() {
|
||||
// 显示添加漏洞模态框
|
||||
function showAddVulnerabilityModal() {
|
||||
currentVulnerabilityId = null;
|
||||
document.getElementById('vulnerability-modal-title').textContent = (typeof window.t === 'function' ? window.t('vulnerability.addVuln') : '添加漏洞');
|
||||
document.getElementById('vulnerability-modal-title').textContent = vulnT('vulnerability.addVuln');
|
||||
|
||||
// 清空表单
|
||||
document.getElementById('vulnerability-conversation-id').value = '';
|
||||
document.getElementById('vulnerability-conversation-tag').value = '';
|
||||
document.getElementById('vulnerability-task-tag').value = '';
|
||||
document.getElementById('vulnerability-title').value = '';
|
||||
document.getElementById('vulnerability-description').value = '';
|
||||
document.getElementById('vulnerability-severity').value = '';
|
||||
@@ -355,14 +443,16 @@ function showAddVulnerabilityModal() {
|
||||
async function editVulnerability(id) {
|
||||
try {
|
||||
const response = await apiFetch(`/api/vulnerabilities/${id}`);
|
||||
if (!response.ok) throw new Error('获取漏洞失败');
|
||||
if (!response.ok) throw new Error(vulnT('vulnerabilityPage.fetchFailed'));
|
||||
|
||||
const vuln = await response.json();
|
||||
currentVulnerabilityId = id;
|
||||
document.getElementById('vulnerability-modal-title').textContent = (typeof window.t === 'function' ? window.t('vulnerability.editVuln') : '编辑漏洞');
|
||||
document.getElementById('vulnerability-modal-title').textContent = vulnT('vulnerability.editVuln');
|
||||
|
||||
// 填充表单
|
||||
document.getElementById('vulnerability-conversation-id').value = vuln.conversation_id || '';
|
||||
document.getElementById('vulnerability-conversation-tag').value = vuln.conversation_tag || '';
|
||||
document.getElementById('vulnerability-task-tag').value = vuln.task_tag || '';
|
||||
document.getElementById('vulnerability-title').value = vuln.title || '';
|
||||
document.getElementById('vulnerability-description').value = vuln.description || '';
|
||||
document.getElementById('vulnerability-severity').value = vuln.severity || '';
|
||||
@@ -376,7 +466,7 @@ async function editVulnerability(id) {
|
||||
document.getElementById('vulnerability-modal').style.display = 'block';
|
||||
} catch (error) {
|
||||
console.error('加载漏洞失败:', error);
|
||||
alert('加载漏洞失败: ' + error.message);
|
||||
alert(vulnT('vulnerability.loadFailed') + ': ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,12 +477,14 @@ async function saveVulnerability() {
|
||||
const severity = document.getElementById('vulnerability-severity').value;
|
||||
|
||||
if (!conversationId || !title || !severity) {
|
||||
alert('请填写必填字段:会话ID、标题和严重程度');
|
||||
alert(vulnT('vulnerabilityPage.saveRequiredFields'));
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
conversation_id: conversationId,
|
||||
conversation_tag: document.getElementById('vulnerability-conversation-tag').value.trim(),
|
||||
task_tag: document.getElementById('vulnerability-task-tag').value.trim(),
|
||||
title: title,
|
||||
description: document.getElementById('vulnerability-description').value.trim(),
|
||||
severity: severity,
|
||||
@@ -420,7 +512,7 @@ async function saveVulnerability() {
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || '保存失败');
|
||||
throw new Error(error.error || vulnT('vulnerabilityPage.saveFailed'));
|
||||
}
|
||||
|
||||
closeVulnerabilityModal();
|
||||
@@ -430,13 +522,13 @@ async function saveVulnerability() {
|
||||
loadVulnerabilities();
|
||||
} catch (error) {
|
||||
console.error('保存漏洞失败:', error);
|
||||
alert('保存漏洞失败: ' + error.message);
|
||||
alert(vulnT('vulnerabilityPage.saveFailed') + ': ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除漏洞
|
||||
async function deleteVulnerability(id) {
|
||||
if (!confirm('确定要删除此漏洞吗?')) {
|
||||
if (!confirm(vulnT('vulnerability.deleteConfirm'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -445,7 +537,7 @@ async function deleteVulnerability(id) {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('删除失败');
|
||||
if (!response.ok) throw new Error(vulnT('vulnerabilityPage.deleteFailed'));
|
||||
|
||||
loadVulnerabilityStats();
|
||||
// 删除后,如果当前页没有数据了,回到上一页
|
||||
@@ -458,7 +550,7 @@ async function deleteVulnerability(id) {
|
||||
loadVulnerabilities();
|
||||
} catch (error) {
|
||||
console.error('删除漏洞失败:', error);
|
||||
alert('删除漏洞失败: ' + error.message);
|
||||
alert(vulnT('vulnerabilityPage.deleteFailed') + ': ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -472,6 +564,9 @@ function closeVulnerabilityModal() {
|
||||
function filterVulnerabilities() {
|
||||
vulnerabilityFilters.id = document.getElementById('vulnerability-id-filter').value.trim();
|
||||
vulnerabilityFilters.conversation_id = document.getElementById('vulnerability-conversation-filter').value.trim();
|
||||
vulnerabilityFilters.task_id = document.getElementById('vulnerability-task-filter').value.trim();
|
||||
vulnerabilityFilters.conversation_tag = document.getElementById('vulnerability-conversation-tag-filter').value.trim();
|
||||
vulnerabilityFilters.task_tag = document.getElementById('vulnerability-task-tag-filter').value.trim();
|
||||
vulnerabilityFilters.severity = document.getElementById('vulnerability-severity-filter').value;
|
||||
vulnerabilityFilters.status = document.getElementById('vulnerability-status-filter').value;
|
||||
|
||||
@@ -486,12 +581,18 @@ function filterVulnerabilities() {
|
||||
function clearVulnerabilityFilters() {
|
||||
document.getElementById('vulnerability-id-filter').value = '';
|
||||
document.getElementById('vulnerability-conversation-filter').value = '';
|
||||
document.getElementById('vulnerability-task-filter').value = '';
|
||||
document.getElementById('vulnerability-conversation-tag-filter').value = '';
|
||||
document.getElementById('vulnerability-task-tag-filter').value = '';
|
||||
document.getElementById('vulnerability-severity-filter').value = '';
|
||||
document.getElementById('vulnerability-status-filter').value = '';
|
||||
|
||||
vulnerabilityFilters = {
|
||||
id: '',
|
||||
conversation_id: '',
|
||||
task_id: '',
|
||||
conversation_tag: '',
|
||||
task_tag: '',
|
||||
severity: '',
|
||||
status: ''
|
||||
};
|
||||
@@ -532,67 +633,193 @@ function escapeHtml(text) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// 将漏洞格式化为Markdown
|
||||
/** 复制详情字段(编码由 encodeURIComponent 传入,避免引号截断) */
|
||||
function vulnerabilityCopyEncoded(evt, encoded) {
|
||||
if (evt && evt.stopPropagation) {
|
||||
evt.stopPropagation();
|
||||
}
|
||||
let text = '';
|
||||
try {
|
||||
text = decodeURIComponent(encoded);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
const done = () => {
|
||||
if (evt && evt.target && evt.target.closest) {
|
||||
const btn = evt.target.closest('.vuln-detail-field__copy');
|
||||
if (btn) {
|
||||
const t0 = btn.getAttribute('title') || '';
|
||||
btn.setAttribute('title', vulnT('common.copied'));
|
||||
setTimeout(() => btn.setAttribute('title', t0), 1600);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
||||
navigator.clipboard.writeText(text).then(done).catch(() => {
|
||||
try {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
ta.style.position = 'fixed';
|
||||
ta.style.left = '-9999px';
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
done();
|
||||
} catch (err) {
|
||||
console.error('copy failed', err);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
ta.style.position = 'fixed';
|
||||
ta.style.left = '-9999px';
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
done();
|
||||
} catch (err) {
|
||||
console.error('copy failed', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function vulnDetailField(label, value, asCode) {
|
||||
if (value === undefined || value === null || String(value) === '') {
|
||||
return '';
|
||||
}
|
||||
const s = String(value);
|
||||
const enc = encodeURIComponent(s);
|
||||
const copyTitle = escapeHtml(vulnT('common.copy'));
|
||||
const valueEl = asCode
|
||||
? `<code class="vuln-detail-field-value">${escapeHtml(s)}</code>`
|
||||
: `<span class="vuln-detail-field-value">${escapeHtml(s)}</span>`;
|
||||
const copyBtn = `<button type="button" class="vuln-detail-field__copy" onclick="vulnerabilityCopyEncoded(event, '${enc}')" title="${copyTitle}" aria-label="${copyTitle}">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
||||
</button>`;
|
||||
return `<div class="vuln-detail-field">
|
||||
<div class="vuln-detail-field__label">${escapeHtml(label)}</div>
|
||||
<div class="vuln-detail-field__row">${valueEl}${copyBtn}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// 将漏洞格式化为Markdown(章节标题随界面语言)
|
||||
function formatVulnerabilityAsMarkdown(vuln) {
|
||||
const severityText = {
|
||||
'critical': '严重',
|
||||
'high': '高危',
|
||||
'medium': '中危',
|
||||
'low': '低危',
|
||||
'info': '信息'
|
||||
}[vuln.severity] || vuln.severity;
|
||||
|
||||
const statusText = {
|
||||
'open': '待处理',
|
||||
'confirmed': '已确认',
|
||||
'fixed': '已修复',
|
||||
'false_positive': '误报'
|
||||
}[vuln.status] || vuln.status;
|
||||
|
||||
const createdDate = new Date(vuln.created_at).toLocaleString('zh-CN');
|
||||
const updatedDate = new Date(vuln.updated_at).toLocaleString('zh-CN');
|
||||
const severityText = vulnSeverityLabel(vuln.severity);
|
||||
const statusText = vulnStatusLabel(vuln.status);
|
||||
const loc = vulnDateLocale();
|
||||
const createdDate = new Date(vuln.created_at).toLocaleString(loc);
|
||||
const updatedDate = new Date(vuln.updated_at).toLocaleString(loc);
|
||||
const L = (k) => vulnT('vulnerabilityMd.' + k);
|
||||
|
||||
let markdown = `# ${vuln.title}\n\n`;
|
||||
|
||||
markdown += `## 基本信息\n\n`;
|
||||
markdown += `- **漏洞ID**: \`${vuln.id}\`\n`;
|
||||
markdown += `- **严重程度**: ${severityText}\n`;
|
||||
markdown += `- **状态**: ${statusText}\n`;
|
||||
|
||||
markdown += `## ${L('headingBasic')}\n\n`;
|
||||
markdown += `- **${L('labelId')}**: \`${vuln.id}\`\n`;
|
||||
markdown += `- **${L('labelSeverity')}**: ${severityText}\n`;
|
||||
markdown += `- **${L('labelStatus')}**: ${statusText}\n`;
|
||||
if (vuln.type) {
|
||||
markdown += `- **类型**: ${vuln.type}\n`;
|
||||
markdown += `- **${L('labelType')}**: ${vuln.type}\n`;
|
||||
}
|
||||
if (vuln.target) {
|
||||
markdown += `- **目标**: ${vuln.target}\n`;
|
||||
markdown += `- **${L('labelTarget')}**: ${vuln.target}\n`;
|
||||
}
|
||||
markdown += `- **会话ID**: \`${vuln.conversation_id}\`\n`;
|
||||
markdown += `- **创建时间**: ${createdDate}\n`;
|
||||
markdown += `- **更新时间**: ${updatedDate}\n\n`;
|
||||
markdown += `- **${L('labelConversationId')}**: \`${vuln.conversation_id}\`\n`;
|
||||
if (vuln.task_id) {
|
||||
markdown += `- **${L('labelTaskId')}**: \`${vuln.task_id}\`\n`;
|
||||
}
|
||||
if (vuln.task_queue_id) {
|
||||
markdown += `- **${L('labelTaskQueueId')}**: \`${vuln.task_queue_id}\`\n`;
|
||||
}
|
||||
if (vuln.conversation_tag) {
|
||||
markdown += `- **${L('labelConversationTag')}**: ${vuln.conversation_tag}\n`;
|
||||
}
|
||||
if (vuln.task_tag) {
|
||||
markdown += `- **${L('labelTaskTag')}**: ${vuln.task_tag}\n`;
|
||||
}
|
||||
markdown += `- **${L('labelCreated')}**: ${createdDate}\n`;
|
||||
markdown += `- **${L('labelUpdated')}**: ${updatedDate}\n\n`;
|
||||
|
||||
if (vuln.description) {
|
||||
markdown += `## 描述\n\n${vuln.description}\n\n`;
|
||||
markdown += `## ${L('headingDescription')}\n\n${vuln.description}\n\n`;
|
||||
}
|
||||
|
||||
if (vuln.proof) {
|
||||
markdown += `## 证明(POC)\n\n\`\`\`\n${vuln.proof}\n\`\`\`\n\n`;
|
||||
markdown += `## ${L('headingProof')}\n\n\`\`\`\n${vuln.proof}\n\`\`\`\n\n`;
|
||||
}
|
||||
|
||||
if (vuln.impact) {
|
||||
markdown += `## 影响\n\n${vuln.impact}\n\n`;
|
||||
markdown += `## ${L('headingImpact')}\n\n${vuln.impact}\n\n`;
|
||||
}
|
||||
|
||||
if (vuln.recommendation) {
|
||||
markdown += `## 修复建议\n\n${vuln.recommendation}\n\n`;
|
||||
markdown += `## ${L('headingRecommendation')}\n\n${vuln.recommendation}\n\n`;
|
||||
}
|
||||
|
||||
return markdown;
|
||||
}
|
||||
|
||||
function buildVulnerabilityFilterParams() {
|
||||
const params = new URLSearchParams();
|
||||
const keys = ['id', 'conversation_id', 'task_id', 'conversation_tag', 'task_tag', 'severity', 'status'];
|
||||
keys.forEach((k) => {
|
||||
if (vulnerabilityFilters[k]) {
|
||||
params.append(k, vulnerabilityFilters[k]);
|
||||
}
|
||||
});
|
||||
return params;
|
||||
}
|
||||
|
||||
function triggerTextDownload(fileName, content) {
|
||||
const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async function exportVulnerabilityReports() {
|
||||
try {
|
||||
const params = buildVulnerabilityFilterParams();
|
||||
params.set('mode', 'summary');
|
||||
params.set('group_by', 'conversation');
|
||||
const response = await apiFetch(`/api/vulnerabilities/export?${params.toString()}`);
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: vulnT('vulnerabilityPage.exportFailedMessage') }));
|
||||
throw new Error(error.error || vulnT('vulnerabilityPage.exportFailedMessage'));
|
||||
}
|
||||
const data = await response.json();
|
||||
const files = Array.isArray(data.files) ? data.files : [];
|
||||
if (!files.length) {
|
||||
alert(vulnT('vulnerabilityPage.exportNoResults'));
|
||||
return;
|
||||
}
|
||||
files.forEach((file, idx) => {
|
||||
setTimeout(() => triggerTextDownload(file.filename || `vulnerability-export-${idx + 1}.md`, file.content || ''), idx * 120);
|
||||
});
|
||||
if (files.length > 1) {
|
||||
alert(vulnT('vulnerabilityPage.exportStarted', { count: files.length }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('导出漏洞报告失败:', error);
|
||||
alert(vulnT('vulnerabilityPage.exportFailed') + ': ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 下载漏洞为Markdown格式
|
||||
async function downloadVulnerabilityAsMarkdown(id, event) {
|
||||
try {
|
||||
const response = await apiFetch(`/api/vulnerabilities/${id}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('获取漏洞失败');
|
||||
throw new Error(vulnT('vulnerabilityPage.fetchFailed'));
|
||||
}
|
||||
|
||||
const vuln = await response.json();
|
||||
@@ -626,8 +853,8 @@ async function downloadVulnerabilityAsMarkdown(id, event) {
|
||||
if (event && event.target) {
|
||||
const button = event.target.closest('button');
|
||||
if (button) {
|
||||
const originalTitle = button.title || '下载Markdown';
|
||||
button.title = '下载成功!';
|
||||
const originalTitle = button.title || vulnT('vulnerabilityPage.downloadMarkdownTitle');
|
||||
button.title = vulnT('vulnerabilityPage.downloadOkTitle');
|
||||
setTimeout(() => {
|
||||
button.title = originalTitle;
|
||||
}, 2000);
|
||||
@@ -635,7 +862,7 @@ async function downloadVulnerabilityAsMarkdown(id, event) {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error);
|
||||
alert('下载失败: ' + error.message);
|
||||
alert(vulnT('vulnerabilityPage.downloadFailed') + ': ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -645,5 +872,12 @@ window.onclick = function(event) {
|
||||
if (event.target === modal) {
|
||||
closeVulnerabilityModal();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('languagechange', function () {
|
||||
const page = document.getElementById('page-vulnerabilities');
|
||||
if (page && page.classList.contains('active')) {
|
||||
loadVulnerabilities();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -2881,17 +2881,6 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
|
||||
} else if (_et === 'warning') {
|
||||
appendTimelineItem('warning', '⚠️ ' + (_em || ''), '', _ed);
|
||||
|
||||
// ─── Eino recovery ───
|
||||
} else if (_et === 'eino_recovery') {
|
||||
var runIdx = _ed.runIndex != null ? _ed.runIndex : (_ed.einoRetry != null ? _ed.einoRetry + 1 : 1);
|
||||
var maxRuns = _ed.maxRuns != null ? _ed.maxRuns : 3;
|
||||
var recTitle = wsTOr('chat.einoRecoveryTitle', '') ||
|
||||
('🔄 工具参数无效 · 第 ' + runIdx + '/' + maxRuns + ' 轮(已追加提示)');
|
||||
if (typeof window.t === 'function') {
|
||||
try { recTitle = window.t('chat.einoRecoveryTitle', { n: runIdx, max: maxRuns }); } catch (e) { /* */ }
|
||||
}
|
||||
appendTimelineItem('eino_recovery', recTitle, _em, _ed);
|
||||
|
||||
// ─── Tool calls ───
|
||||
} else if (_et === 'tool_calls_detected' && _ed) {
|
||||
var count = _ed.count || 0;
|
||||
|
||||
@@ -1097,6 +1097,18 @@
|
||||
<span data-i18n="vulnerabilityPage.conversationId">会话ID</span>
|
||||
<input type="text" id="vulnerability-conversation-filter" data-i18n="vulnerabilityPage.filterConversation" data-i18n-attr="placeholder" placeholder="筛选特定会话" />
|
||||
</label>
|
||||
<label>
|
||||
<span data-i18n="vulnerabilityPage.taskOrQueueId">任务ID/队列ID</span>
|
||||
<input type="text" id="vulnerability-task-filter" data-i18n="vulnerabilityPage.filterTaskOrQueue" data-i18n-attr="placeholder" placeholder="筛选任务ID或队列ID" />
|
||||
</label>
|
||||
<label>
|
||||
<span data-i18n="vulnerabilityPage.conversationTag">对话标签</span>
|
||||
<input type="text" id="vulnerability-conversation-tag-filter" data-i18n="vulnerabilityPage.filterConversationTag" data-i18n-attr="placeholder" placeholder="筛选对话标签" />
|
||||
</label>
|
||||
<label>
|
||||
<span data-i18n="vulnerabilityPage.taskTag">任务标签</span>
|
||||
<input type="text" id="vulnerability-task-tag-filter" data-i18n="vulnerabilityPage.filterTaskTag" data-i18n-attr="placeholder" placeholder="筛选任务标签" />
|
||||
</label>
|
||||
<label>
|
||||
<span data-i18n="vulnerabilityPage.severity">严重程度</span>
|
||||
<select id="vulnerability-severity-filter">
|
||||
@@ -1120,6 +1132,7 @@
|
||||
</label>
|
||||
<button class="btn-secondary" onclick="filterVulnerabilities()" data-i18n="vulnerabilityPage.filter">筛选</button>
|
||||
<button class="btn-secondary" onclick="clearVulnerabilityFilters()" data-i18n="vulnerabilityPage.clear">清除</button>
|
||||
<button class="btn-primary" onclick="exportVulnerabilityReports()" data-i18n="vulnerabilityPage.batchExport">批量导出</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2411,6 +2424,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="context-menu-item" onclick="navigateToVulnerabilitiesForContextConversation()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 12l2 2 4-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span data-i18n="contextMenu.viewVulnerabilities">查看漏洞</span>
|
||||
</div>
|
||||
<div class="context-menu-divider"></div>
|
||||
<div class="context-menu-item" onclick="renameConversation()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -2599,6 +2619,14 @@
|
||||
<label for="vulnerability-conversation-id"><span data-i18n="vulnerabilityModal.conversationId">会话ID</span> <span style="color: red;">*</span></label>
|
||||
<input type="text" id="vulnerability-conversation-id" data-i18n="vulnerabilityModal.conversationIdPlaceholder" data-i18n-attr="placeholder" placeholder="输入会话ID" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="vulnerability-conversation-tag" data-i18n="vulnerabilityModal.conversationTag">对话标签</label>
|
||||
<input type="text" id="vulnerability-conversation-tag" data-i18n="vulnerabilityModal.conversationTagPlaceholder" data-i18n-attr="placeholder" placeholder="如:红队演练A、客户A周报" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="vulnerability-task-tag" data-i18n="vulnerabilityModal.taskTag">任务标签</label>
|
||||
<input type="text" id="vulnerability-task-tag" data-i18n="vulnerabilityModal.taskTagPlaceholder" data-i18n-attr="placeholder" placeholder="如:批量扫描Q2、专项复测" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="vulnerability-title"><span data-i18n="vulnerabilityModal.title">标题</span> <span style="color: red;">*</span></label>
|
||||
<input type="text" id="vulnerability-title" data-i18n="vulnerabilityModal.titlePlaceholder" data-i18n-attr="placeholder" placeholder="漏洞标题" required />
|
||||
@@ -2817,7 +2845,7 @@
|
||||
<script src="/static/js/terminal.js"></script>
|
||||
<script src="/static/js/knowledge.js"></script>
|
||||
<script src="/static/js/skills.js"></script>
|
||||
<script src="/static/js/vulnerability.js?v=4"></script>
|
||||
<script src="/static/js/vulnerability.js?v=7"></script>
|
||||
<script src="/static/js/webshell.js"></script>
|
||||
<script src="/static/js/chat-files.js"></script>
|
||||
<script src="/static/js/tasks.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user