mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-16 21:23:29 +02:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b3d094dc4 | |||
| 47922c2083 | |||
| dfaf0bc77f | |||
| 3eb7edb1b8 | |||
| f82f6b861e | |||
| 2acf43c454 | |||
| fad6b3c808 | |||
| 0597838217 | |||
| 1532426b4f | |||
| 3aeb8c3474 | |||
| b2b166972a | |||
| 36b669771c | |||
| 96564d4d89 | |||
| d85afa2d39 | |||
| 55b6bceb21 | |||
| 65d73b3d66 | |||
| 913115d1fb | |||
| e1b967d781 | |||
| 9d9efa886f | |||
| cae45e9dc5 | |||
| c788b59f25 | |||
| 5edf3a70f9 | |||
| 3dfb3b4e82 | |||
| a517fe0931 | |||
| 0ab5e31a64 | |||
| ea6e027b25 |
@@ -1,5 +1,5 @@
|
||||
<div align="center">
|
||||
<img src="web/static/logo.png" alt="CyberStrikeAI Logo" width="200">
|
||||
<img src="images/logo.png" alt="CyberStrikeAI Logo" width="200">
|
||||
</div>
|
||||
|
||||
# CyberStrikeAI
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<div align="center">
|
||||
<img src="web/static/logo.png" alt="CyberStrikeAI Logo" width="200">
|
||||
<img src="images/logo.png" alt="CyberStrikeAI Logo" width="200">
|
||||
</div>
|
||||
|
||||
# CyberStrikeAI
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
# ============================================
|
||||
|
||||
# 前端显示的版本号(可选,不填则显示默认版本)
|
||||
version: "v1.5.6"
|
||||
version: "v1.5.10"
|
||||
# 服务器配置
|
||||
server:
|
||||
host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
@@ -91,6 +91,20 @@ func DefaultSingleAgentSystemPrompt() string {
|
||||
|
||||
当工具返回错误时,错误信息会包含在工具响应中,请仔细阅读并做出合理的决策。
|
||||
|
||||
## 结束条件与停止约束
|
||||
|
||||
- 在「未完成用户目标」前,不得输出纯计划/纯建议式结论并结束本轮;必须继续给出可执行下一步,并优先通过工具验证。
|
||||
- 若你准备结束回答,先执行一次自检:
|
||||
1) 是否已有可验证证据支撑“任务完成/无法继续”的结论;
|
||||
2) 是否至少尝试过当前路径的合理替代(参数、路径、方法、入口);
|
||||
3) 是否仍存在可执行且低成本的下一步验证动作。
|
||||
- 仅当满足以下任一条件时,才允许输出最终收尾:
|
||||
1) 已达到用户目标并给出证据;
|
||||
2) 达到明确边界(超时、权限、目标不可达、工具不可用且无替代),并清楚说明阻断点与已尝试项;
|
||||
3) 用户明确要求停止。
|
||||
- 若最近一步得到 404/空结果/无效响应,不得直接结束;至少再进行一次“同目标不同策略”的验证(如变更路径、参数、请求方法、上下文来源)。
|
||||
- 避免无效空转:同一工具+同类参数连续失败 3 次后,必须切换策略(改工具、改入口、改假设)并说明切换原因。
|
||||
|
||||
## 漏洞记录
|
||||
|
||||
发现有效漏洞时,必须使用 ` + builtin.ToolRecordVulnerability + ` 记录:标题、描述、严重程度、类型、目标、证明(POC)、影响、修复建议。
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -603,11 +603,13 @@ func (h *AgentHandler) AgentLoop(c *gin.Context) {
|
||||
|
||||
baseCtx, cancelWithCause := context.WithCancelCause(c.Request.Context())
|
||||
defer cancelWithCause(nil)
|
||||
progressCallback := h.createProgressCallback(baseCtx, cancelWithCause, conversationID, "", nil)
|
||||
baseCtx = h.injectReactHITLInterceptor(baseCtx, cancelWithCause, conversationID, "", nil)
|
||||
taskCtx, timeoutCancel := context.WithTimeout(baseCtx, 600*time.Minute)
|
||||
defer timeoutCancel()
|
||||
progressCallback := h.createProgressCallback(taskCtx, cancelWithCause, conversationID, "", nil)
|
||||
taskCtx = h.injectReactHITLInterceptor(taskCtx, cancelWithCause, conversationID, "", nil)
|
||||
|
||||
// 执行Agent Loop,传入历史消息和对话ID(使用包含角色提示词的finalMessage和角色工具列表)
|
||||
result, err := h.agent.AgentLoopWithProgress(baseCtx, finalMessage, agentHistoryMessages, conversationID, progressCallback, roleTools)
|
||||
result, err := h.agent.AgentLoopWithProgress(taskCtx, finalMessage, agentHistoryMessages, conversationID, progressCallback, roleTools)
|
||||
if err != nil {
|
||||
h.logger.Error("Agent Loop执行失败", zap.Error(err))
|
||||
|
||||
@@ -1209,6 +1211,9 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
|
||||
}
|
||||
eventJSON, _ := json.Marshal(event)
|
||||
fmt.Fprintf(c.Writer, "data: %s\n\n", eventJSON)
|
||||
done := StreamEvent{Type: "done", Message: ""}
|
||||
doneJSON, _ := json.Marshal(done)
|
||||
fmt.Fprintf(c.Writer, "data: %s\n\n", doneJSON)
|
||||
c.Writer.Flush()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -281,8 +281,10 @@ func (h *AgentHandler) EinoSingleAgentLoop(c *gin.Context) {
|
||||
}
|
||||
baseCtx, cancelWithCause := context.WithCancelCause(c.Request.Context())
|
||||
defer cancelWithCause(nil)
|
||||
progressCallback := h.createProgressCallback(baseCtx, cancelWithCause, prep.ConversationID, prep.AssistantMessageID, progressCallbackRaw)
|
||||
baseCtx = multiagent.WithHITLToolInterceptor(baseCtx, func(ctx context.Context, toolName, arguments string) (string, error) {
|
||||
taskCtx, timeoutCancel := context.WithTimeout(baseCtx, 600*time.Minute)
|
||||
defer timeoutCancel()
|
||||
progressCallback := h.createProgressCallback(taskCtx, cancelWithCause, prep.ConversationID, prep.AssistantMessageID, progressCallbackRaw)
|
||||
taskCtx = multiagent.WithHITLToolInterceptor(taskCtx, func(ctx context.Context, toolName, arguments string) (string, error) {
|
||||
return h.interceptHITLForEinoTool(ctx, cancelWithCause, prep.ConversationID, prep.AssistantMessageID, nil, toolName, arguments)
|
||||
})
|
||||
|
||||
@@ -292,7 +294,7 @@ func (h *AgentHandler) EinoSingleAgentLoop(c *gin.Context) {
|
||||
}
|
||||
|
||||
result, runErr := multiagent.RunEinoSingleChatModelAgent(
|
||||
baseCtx,
|
||||
taskCtx,
|
||||
h.config,
|
||||
&h.config.MultiAgent,
|
||||
h.agent,
|
||||
|
||||
@@ -40,6 +40,9 @@ func (h *AgentHandler) MultiAgentLoopStream(c *gin.Context) {
|
||||
event := StreamEvent{Type: "error", Message: "请求参数错误: " + err.Error()}
|
||||
b, _ := json.Marshal(event)
|
||||
fmt.Fprintf(c.Writer, "data: %s\n\n", b)
|
||||
done := StreamEvent{Type: "done", Message: ""}
|
||||
db, _ := json.Marshal(done)
|
||||
fmt.Fprintf(c.Writer, "data: %s\n\n", db)
|
||||
c.Writer.Flush()
|
||||
return
|
||||
}
|
||||
@@ -293,13 +296,15 @@ func (h *AgentHandler) MultiAgentLoop(c *gin.Context) {
|
||||
|
||||
baseCtx, cancelWithCause := context.WithCancelCause(c.Request.Context())
|
||||
defer cancelWithCause(nil)
|
||||
progressCallback := h.createProgressCallback(baseCtx, cancelWithCause, prep.ConversationID, prep.AssistantMessageID, nil)
|
||||
baseCtx = multiagent.WithHITLToolInterceptor(baseCtx, func(ctx context.Context, toolName, arguments string) (string, error) {
|
||||
taskCtx, timeoutCancel := context.WithTimeout(baseCtx, 600*time.Minute)
|
||||
defer timeoutCancel()
|
||||
progressCallback := h.createProgressCallback(taskCtx, cancelWithCause, prep.ConversationID, prep.AssistantMessageID, nil)
|
||||
taskCtx = multiagent.WithHITLToolInterceptor(taskCtx, func(ctx context.Context, toolName, arguments string) (string, error) {
|
||||
return h.interceptHITLForEinoTool(ctx, cancelWithCause, prep.ConversationID, prep.AssistantMessageID, nil, toolName, arguments)
|
||||
})
|
||||
|
||||
result, runErr := multiagent.RunDeepAgent(
|
||||
baseCtx,
|
||||
taskCtx,
|
||||
h.config,
|
||||
&h.config.MultiAgent,
|
||||
h.agent,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,21 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func isEinoIterationLimitError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
msg := strings.ToLower(strings.TrimSpace(err.Error()))
|
||||
if msg == "" {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(msg, "max iteration") ||
|
||||
strings.Contains(msg, "maximum iteration") ||
|
||||
strings.Contains(msg, "maximum iterations") ||
|
||||
strings.Contains(msg, "iteration limit") ||
|
||||
strings.Contains(msg, "达到最大迭代")
|
||||
}
|
||||
|
||||
// einoADKRunLoopArgs 将 Eino adk.Runner 事件循环从 RunDeepAgent / RunEinoSingleChatModelAgent 中抽出复用。
|
||||
type einoADKRunLoopArgs struct {
|
||||
OrchMode string
|
||||
@@ -97,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 == "" {
|
||||
@@ -105,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
|
||||
}
|
||||
@@ -203,8 +207,59 @@ attemptLoop:
|
||||
}
|
||||
}
|
||||
}
|
||||
runner := adk.NewRunner(ctx, runnerCfg)
|
||||
iter := runner.Run(ctx, msgs)
|
||||
runner := adk.NewRunner(ctx, runnerCfg)
|
||||
iter := runner.Run(ctx, msgs)
|
||||
handleRunErr := func(runErr error) error {
|
||||
if runErr == nil {
|
||||
return nil
|
||||
}
|
||||
if errors.Is(runErr, context.DeadlineExceeded) {
|
||||
flushAllPendingAsFailed(runErr)
|
||||
if progress != nil {
|
||||
progress("error", runErr.Error(), map[string]interface{}{
|
||||
"conversationId": conversationID,
|
||||
"source": "eino",
|
||||
"errorKind": "timeout",
|
||||
})
|
||||
}
|
||||
return runErr
|
||||
}
|
||||
// context.Canceled 是唯一应当直接终止编排的错误(用户关闭页面、主动停止等)。
|
||||
if errors.Is(runErr, context.Canceled) {
|
||||
flushAllPendingAsFailed(runErr)
|
||||
if progress != nil {
|
||||
progress("error", runErr.Error(), map[string]interface{}{
|
||||
"conversationId": conversationID,
|
||||
"source": "eino",
|
||||
})
|
||||
}
|
||||
return runErr
|
||||
}
|
||||
if isEinoIterationLimitError(runErr) {
|
||||
flushAllPendingAsFailed(runErr)
|
||||
if progress != nil {
|
||||
progress("iteration_limit_reached", runErr.Error(), map[string]interface{}{
|
||||
"conversationId": conversationID,
|
||||
"source": "eino",
|
||||
"orchestration": orchMode,
|
||||
})
|
||||
progress("error", runErr.Error(), map[string]interface{}{
|
||||
"conversationId": conversationID,
|
||||
"source": "eino",
|
||||
"errorKind": "iteration_limit",
|
||||
})
|
||||
}
|
||||
return runErr
|
||||
}
|
||||
flushAllPendingAsFailed(runErr)
|
||||
if progress != nil {
|
||||
progress("error", runErr.Error(), map[string]interface{}{
|
||||
"conversationId": conversationID,
|
||||
"source": "eino",
|
||||
})
|
||||
}
|
||||
return runErr
|
||||
}
|
||||
|
||||
for {
|
||||
// 检测 context 取消(用户关闭浏览器、请求超时等),flush pending 工具状态避免 UI 卡在 "执行中"。
|
||||
@@ -223,79 +278,28 @@ attemptLoop:
|
||||
|
||||
ev, ok := iter.Next()
|
||||
if !ok {
|
||||
lastRunMsgs = msgs
|
||||
break attemptLoop
|
||||
if len(pendingByID) > 0 {
|
||||
orphanCount := len(pendingByID)
|
||||
flushAllPendingAsFailed(errors.New("pending tool call missing result before run completion"))
|
||||
if progress != nil {
|
||||
progress("eino_pending_orphaned", "pending tool calls were force-closed at run end", map[string]interface{}{
|
||||
"conversationId": conversationID,
|
||||
"source": "eino",
|
||||
"orchestration": orchMode,
|
||||
"pendingCount": orphanCount,
|
||||
})
|
||||
}
|
||||
}
|
||||
lastRunMsgs = runAccumulatedMsgs
|
||||
break
|
||||
}
|
||||
if ev == nil {
|
||||
continue
|
||||
}
|
||||
if ev.Err != nil {
|
||||
if errors.Is(ev.Err, context.DeadlineExceeded) {
|
||||
flushAllPendingAsFailed(ev.Err)
|
||||
if progress != nil {
|
||||
progress("error", ev.Err.Error(), map[string]interface{}{
|
||||
"conversationId": conversationID,
|
||||
"source": "eino",
|
||||
"errorKind": "timeout",
|
||||
})
|
||||
}
|
||||
return nil, ev.Err
|
||||
if retErr := handleRunErr(ev.Err); retErr != nil {
|
||||
return nil, retErr
|
||||
}
|
||||
// context.Canceled 是唯一应当直接终止编排的错误(用户关闭页面、主动停止等)。
|
||||
if errors.Is(ev.Err, context.Canceled) {
|
||||
flushAllPendingAsFailed(ev.Err)
|
||||
if progress != nil {
|
||||
progress("error", ev.Err.Error(), map[string]interface{}{
|
||||
"conversationId": conversationID,
|
||||
"source": "eino",
|
||||
})
|
||||
}
|
||||
return nil, ev.Err
|
||||
}
|
||||
|
||||
canRetry := attempt+1 < maxToolCallRecoveryAttempts
|
||||
if !canRetry {
|
||||
// 重试次数已耗尽,终止。
|
||||
flushAllPendingAsFailed(ev.Err)
|
||||
if progress != nil {
|
||||
progress("error", ev.Err.Error(), map[string]interface{}{
|
||||
"conversationId": conversationID,
|
||||
"source": "eino",
|
||||
})
|
||||
}
|
||||
return nil, ev.Err
|
||||
}
|
||||
|
||||
// 区分错误类型以选择最合适的纠错提示,但无论哪种都执行重试(default-soft)。
|
||||
var hint *schema.Message
|
||||
var reason, timelineMsg string
|
||||
if isRecoverableToolCallArgumentsJSONError(ev.Err) {
|
||||
hint = toolCallArgumentsJSONRetryHint()
|
||||
reason = "invalid_tool_arguments_json"
|
||||
timelineMsg = toolCallArgumentsJSONRecoveryTimelineMessage(attempt)
|
||||
} else {
|
||||
hint = toolExecutionRetryHint()
|
||||
reason = "tool_execution_error"
|
||||
timelineMsg = toolExecutionRecoveryTimelineMessage(attempt)
|
||||
}
|
||||
|
||||
if logger != nil {
|
||||
logger.Warn("eino: recoverable error, will retry with corrective hint",
|
||||
zap.Error(ev.Err), zap.Int("attempt", attempt), zap.String("reason", reason))
|
||||
}
|
||||
flushAllPendingAsFailed(ev.Err)
|
||||
retryHints = append(retryHints, hint)
|
||||
if progress != nil {
|
||||
progress("eino_recovery", timelineMsg, map[string]interface{}{
|
||||
"conversationId": conversationID,
|
||||
"source": "eino",
|
||||
"einoRetry": attempt,
|
||||
"runIndex": attempt + 1,
|
||||
"maxRuns": maxToolCallRecoveryAttempts,
|
||||
"reason": reason,
|
||||
})
|
||||
}
|
||||
continue attemptLoop
|
||||
}
|
||||
if ev.AgentName != "" && progress != nil {
|
||||
iterEinoAgent := orchestratorName
|
||||
@@ -349,6 +353,7 @@ attemptLoop:
|
||||
var subAssistantBuf strings.Builder
|
||||
var subReplyStreamID string
|
||||
var mainAssistantBuf strings.Builder
|
||||
var streamRecvErr error
|
||||
for {
|
||||
chunk, rerr := mv.MessageStream.Recv()
|
||||
if rerr != nil {
|
||||
@@ -361,6 +366,7 @@ attemptLoop:
|
||||
zap.String("agent", ev.AgentName),
|
||||
zap.Int("toolFragments", len(toolStreamFragments)))
|
||||
}
|
||||
streamRecvErr = rerr
|
||||
break
|
||||
}
|
||||
if chunk == nil {
|
||||
@@ -429,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)
|
||||
}
|
||||
@@ -459,6 +466,19 @@ attemptLoop:
|
||||
lastToolChunk = &schema.Message{ToolCalls: merged}
|
||||
}
|
||||
tryEmitToolCallsOnce(lastToolChunk, ev.AgentName, orchestratorName, conversationID, progress, toolEmitSeen, subAgentToolStep, markPending)
|
||||
if streamRecvErr != nil {
|
||||
if progress != nil {
|
||||
progress("eino_stream_error", streamRecvErr.Error(), map[string]interface{}{
|
||||
"conversationId": conversationID,
|
||||
"source": "eino",
|
||||
"einoAgent": ev.AgentName,
|
||||
"einoRole": einoRoleTag(ev.AgentName),
|
||||
})
|
||||
}
|
||||
if retErr := handleRunErr(streamRecvErr); retErr != nil {
|
||||
return nil, retErr
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -466,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 {
|
||||
@@ -565,7 +586,6 @@ attemptLoop:
|
||||
progress("tool_result", fmt.Sprintf("工具结果 (%s)", toolName), data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mcpIDsMu.Lock()
|
||||
ids := append([]string(nil), *mcpIDs...)
|
||||
|
||||
@@ -57,19 +57,30 @@ func newEinoSummarizationMiddleware(
|
||||
if modelName == "" {
|
||||
modelName = "gpt-4o"
|
||||
}
|
||||
tokenCounter := einoSummarizationTokenCounter(modelName)
|
||||
recentTrailMax := trigger / 4
|
||||
if recentTrailMax < 2048 {
|
||||
recentTrailMax = 2048
|
||||
}
|
||||
if recentTrailMax > trigger/2 {
|
||||
recentTrailMax = trigger / 2
|
||||
}
|
||||
|
||||
mw, err := summarization.New(ctx, &summarization.Config{
|
||||
Model: summaryModel,
|
||||
Trigger: &summarization.TriggerCondition{
|
||||
ContextTokens: trigger,
|
||||
},
|
||||
TokenCounter: einoSummarizationTokenCounter(modelName),
|
||||
TokenCounter: tokenCounter,
|
||||
UserInstruction: einoSummarizeUserInstruction,
|
||||
EmitInternalEvents: false,
|
||||
PreserveUserMessages: &summarization.PreserveUserMessages{
|
||||
Enabled: true,
|
||||
MaxTokens: preserveMax,
|
||||
},
|
||||
Finalize: func(ctx context.Context, originalMessages []adk.Message, summary adk.Message) ([]adk.Message, error) {
|
||||
return summarizeFinalizeWithRecentAssistantToolTrail(ctx, originalMessages, summary, tokenCounter, recentTrailMax)
|
||||
},
|
||||
Callback: func(ctx context.Context, before, after adk.ChatModelAgentState) error {
|
||||
if logger == nil {
|
||||
return nil
|
||||
@@ -89,6 +100,108 @@ func newEinoSummarizationMiddleware(
|
||||
return mw, nil
|
||||
}
|
||||
|
||||
// summarizeFinalizeWithRecentAssistantToolTrail 在摘要消息后保留最近 assistant/tool 轨迹,避免压缩后执行链断裂。
|
||||
func summarizeFinalizeWithRecentAssistantToolTrail(
|
||||
ctx context.Context,
|
||||
originalMessages []adk.Message,
|
||||
summary adk.Message,
|
||||
tokenCounter summarization.TokenCounterFunc,
|
||||
recentTrailTokenBudget int,
|
||||
) ([]adk.Message, error) {
|
||||
systemMsgs := make([]adk.Message, 0, len(originalMessages))
|
||||
nonSystem := make([]adk.Message, 0, len(originalMessages))
|
||||
for _, msg := range originalMessages {
|
||||
if msg == nil {
|
||||
continue
|
||||
}
|
||||
if msg.Role == schema.System {
|
||||
systemMsgs = append(systemMsgs, msg)
|
||||
continue
|
||||
}
|
||||
nonSystem = append(nonSystem, msg)
|
||||
}
|
||||
|
||||
if recentTrailTokenBudget <= 0 || len(nonSystem) == 0 {
|
||||
out := make([]adk.Message, 0, len(systemMsgs)+1)
|
||||
out = append(out, systemMsgs...)
|
||||
out = append(out, summary)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
selectedReverse := make([]adk.Message, 0, 8)
|
||||
seen := make(map[adk.Message]struct{})
|
||||
totalTokens := 0
|
||||
assistantToolKept := 0
|
||||
const minAssistantToolTrail = 4
|
||||
|
||||
tryKeep := func(msg adk.Message) (bool, error) {
|
||||
if msg == nil {
|
||||
return false, nil
|
||||
}
|
||||
if _, ok := seen[msg]; ok {
|
||||
return false, nil
|
||||
}
|
||||
n, err := tokenCounter(ctx, &summarization.TokenCounterInput{Messages: []adk.Message{msg}})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if n <= 0 {
|
||||
n = 1
|
||||
}
|
||||
if totalTokens+n > recentTrailTokenBudget {
|
||||
return false, nil
|
||||
}
|
||||
totalTokens += n
|
||||
selectedReverse = append(selectedReverse, msg)
|
||||
seen[msg] = struct{}{}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 优先保留最近 assistant/tool,确保执行轨迹可续跑。
|
||||
for i := len(nonSystem) - 1; i >= 0; i-- {
|
||||
msg := nonSystem[i]
|
||||
if msg.Role != schema.Assistant && msg.Role != schema.Tool {
|
||||
continue
|
||||
}
|
||||
ok, err := tryKeep(msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ok {
|
||||
assistantToolKept++
|
||||
}
|
||||
if assistantToolKept >= minAssistantToolTrail {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 在预算内回填更多最近消息,保持短链路上下文。
|
||||
for i := len(nonSystem) - 1; i >= 0; i-- {
|
||||
_, exists := seen[nonSystem[i]]
|
||||
if exists {
|
||||
continue
|
||||
}
|
||||
ok, err := tryKeep(nonSystem[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
selected := make([]adk.Message, 0, len(selectedReverse))
|
||||
for i := len(selectedReverse) - 1; i >= 0; i-- {
|
||||
selected = append(selected, selectedReverse[i])
|
||||
}
|
||||
|
||||
out := make([]adk.Message, 0, len(systemMsgs)+1+len(selected))
|
||||
out = append(out, systemMsgs...)
|
||||
out = append(out, summary)
|
||||
out = append(out, selected...)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func einoSummarizationTokenCounter(openAIModel string) summarization.TokenCounterFunc {
|
||||
tc := agent.NewTikTokenCounter()
|
||||
return func(ctx context.Context, input *summarization.TokenCounterInput) (int, error) {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -64,7 +64,7 @@ public class BurpExtender implements IBurpExtender, IContextMenuFactory {
|
||||
|
||||
String prompt = HttpMessageFormatter.toPrompt(helpers, msg, instruction);
|
||||
String title = HttpMessageFormatter.getRequestTitle(helpers, msg);
|
||||
String agentModeStr = (cfg.agentMode == CyberStrikeAIClient.AgentMode.MULTI) ? "Multi Agent" : "Single Agent";
|
||||
String agentModeStr = cfg.agentMode.displayName;
|
||||
String runId = tab.startNewRun(title, agentModeStr, msg);
|
||||
tab.appendProgressToRun(runId, "\n[server] " + cfg.baseUrl + "\n\n");
|
||||
|
||||
|
||||
+29
-9
@@ -26,8 +26,21 @@ final class CyberStrikeAIClient {
|
||||
}
|
||||
|
||||
enum AgentMode {
|
||||
SINGLE,
|
||||
MULTI
|
||||
NATIVE_REACT("Native ReAct", "/api/agent-loop/stream", null),
|
||||
EINO_SINGLE("Eino Single (ADK)", "/api/eino-agent/stream", null),
|
||||
DEEP("Deep (DeepAgent)", "/api/multi-agent/stream", "deep"),
|
||||
PLAN_EXECUTE("Plan-Execute", "/api/multi-agent/stream", "plan_execute"),
|
||||
SUPERVISOR("Supervisor", "/api/multi-agent/stream", "supervisor");
|
||||
|
||||
final String displayName;
|
||||
final String streamPath;
|
||||
final String orchestration;
|
||||
|
||||
AgentMode(String displayName, String streamPath, String orchestration) {
|
||||
this.displayName = displayName;
|
||||
this.streamPath = streamPath;
|
||||
this.orchestration = orchestration;
|
||||
}
|
||||
}
|
||||
|
||||
interface StreamListener {
|
||||
@@ -94,13 +107,15 @@ final class CyberStrikeAIClient {
|
||||
}
|
||||
|
||||
void streamTest(Config cfg, String token, String message, StreamListener listener) {
|
||||
String path = (cfg.agentMode == AgentMode.MULTI) ? "/api/multi-agent/stream" : "/api/agent-loop/stream";
|
||||
String urlStr = cfg.baseUrl + path;
|
||||
String urlStr = cfg.baseUrl + cfg.agentMode.streamPath;
|
||||
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put("message", message);
|
||||
payload.put("conversationId", "");
|
||||
payload.put("role", "");
|
||||
if (cfg.agentMode.orchestration != null) {
|
||||
payload.put("orchestration", cfg.agentMode.orchestration);
|
||||
}
|
||||
|
||||
new Thread(() -> {
|
||||
HttpURLConnection conn = null;
|
||||
@@ -184,11 +199,16 @@ final class CyberStrikeAIClient {
|
||||
String message = payload.get("message") != null ? String.valueOf(payload.get("message")) : "";
|
||||
String conversationId = payload.get("conversationId") != null ? String.valueOf(payload.get("conversationId")) : "";
|
||||
String role = payload.get("role") != null ? String.valueOf(payload.get("role")) : "";
|
||||
return "{"
|
||||
+ "\"message\":\"" + escapeJson(message) + "\","
|
||||
+ "\"conversationId\":\"" + escapeJson(conversationId) + "\","
|
||||
+ "\"role\":\"" + escapeJson(role) + "\""
|
||||
+ "}";
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("{");
|
||||
sb.append("\"message\":\"").append(escapeJson(message)).append("\",");
|
||||
sb.append("\"conversationId\":\"").append(escapeJson(conversationId)).append("\",");
|
||||
sb.append("\"role\":\"").append(escapeJson(role)).append("\"");
|
||||
if (payload.containsKey("orchestration") && payload.get("orchestration") != null) {
|
||||
sb.append(",\"orchestration\":\"").append(escapeJson(String.valueOf(payload.get("orchestration")))).append("\"");
|
||||
}
|
||||
sb.append("}");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String escapeJson(String s) {
|
||||
|
||||
+10
-5
@@ -15,7 +15,9 @@ final class CyberStrikeAITab implements ITab {
|
||||
private final JTextField hostField = new JTextField("127.0.0.1");
|
||||
private final JTextField portField = new JTextField("8080");
|
||||
private final JPasswordField passwordField = new JPasswordField();
|
||||
private final JComboBox<String> agentModeBox = new JComboBox<>(new String[]{"Single Agent", "Multi Agent"});
|
||||
private final JComboBox<String> agentModeBox = new JComboBox<>(new String[]{
|
||||
"Native ReAct", "Eino Single (ADK)", "Deep (DeepAgent)", "Plan-Execute", "Supervisor"
|
||||
});
|
||||
private final JButton validateButton = new JButton("Validate");
|
||||
private final JButton clearButton = new JButton("Clear Output");
|
||||
private final JButton stopButton = new JButton("Stop");
|
||||
@@ -98,7 +100,7 @@ final class CyberStrikeAITab implements ITab {
|
||||
hostField.setColumns(14);
|
||||
portField.setColumns(6);
|
||||
passwordField.setColumns(12);
|
||||
agentModeBox.setPreferredSize(new Dimension(160, agentModeBox.getPreferredSize().height));
|
||||
agentModeBox.setPreferredSize(new Dimension(200, agentModeBox.getPreferredSize().height));
|
||||
|
||||
JPanel row1 = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 2));
|
||||
row1.add(new JLabel("Host"));
|
||||
@@ -475,14 +477,17 @@ final class CyberStrikeAITab implements ITab {
|
||||
renderMarkdownBox.addActionListener(e -> refreshOutputView());
|
||||
}
|
||||
|
||||
private static final CyberStrikeAIClient.AgentMode[] AGENT_MODES = CyberStrikeAIClient.AgentMode.values();
|
||||
|
||||
CyberStrikeAIClient.Config currentConfig() {
|
||||
String host = hostField.getText().trim();
|
||||
String port = portField.getText().trim();
|
||||
String password = new String(passwordField.getPassword());
|
||||
String baseUrl = "http://" + host + ":" + port;
|
||||
CyberStrikeAIClient.AgentMode mode = agentModeBox.getSelectedIndex() == 1
|
||||
? CyberStrikeAIClient.AgentMode.MULTI
|
||||
: CyberStrikeAIClient.AgentMode.SINGLE;
|
||||
int idx = agentModeBox.getSelectedIndex();
|
||||
CyberStrikeAIClient.AgentMode mode = (idx >= 0 && idx < AGENT_MODES.length)
|
||||
? AGENT_MODES[idx]
|
||||
: CyberStrikeAIClient.AgentMode.NATIVE_REACT;
|
||||
return new CyberStrikeAIClient.Config(baseUrl, password, mode);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,19 +2,4 @@ name: API安全测试
|
||||
description: API安全测试专家,专注于API接口安全检测
|
||||
user_prompt: 你是一个专业的API安全测试专家。请使用专业的API测试工具对目标API接口进行全面的安全检测,包括GraphQL安全、API参数fuzzing、JWT分析、API架构分析等工作。
|
||||
icon: "\U0001F4E1"
|
||||
tools:
|
||||
- api-fuzzer
|
||||
- api-schema-analyzer
|
||||
- graphql-scanner
|
||||
- arjun
|
||||
- jwt-analyzer
|
||||
- http-intruder
|
||||
- http-framework-test
|
||||
- burpsuite
|
||||
- httpx
|
||||
- execute-python-script
|
||||
- install-python-package
|
||||
- record_vulnerability
|
||||
- list_knowledge_risk_types
|
||||
- search_knowledge_base
|
||||
enabled: true
|
||||
|
||||
@@ -2,32 +2,4 @@ name: CTF
|
||||
description: CTF竞赛专家,擅长解题和漏洞利用
|
||||
user_prompt: 你是一个CTF竞赛专家。请使用CTF解题思维和方法,快速定位和利用漏洞,解决各类CTF题目。
|
||||
icon: "\U0001F3C6"
|
||||
tools:
|
||||
- amass
|
||||
- anew
|
||||
- angr
|
||||
- api-fuzzer
|
||||
- api-schema-analyzer
|
||||
- arjun
|
||||
- arp-scan
|
||||
- autorecon
|
||||
- binwalk
|
||||
- bloodhound
|
||||
- burpsuite
|
||||
- cat
|
||||
- checkov
|
||||
- checksec
|
||||
- cloudmapper
|
||||
- create-file
|
||||
- cyberchef
|
||||
- dalfox
|
||||
- delete-file
|
||||
- httpx
|
||||
- http-framework-test
|
||||
- exec
|
||||
- execute-python-script
|
||||
- install-python-package
|
||||
- record_vulnerability
|
||||
- list_knowledge_risk_types
|
||||
- search_knowledge_base
|
||||
enabled: true
|
||||
|
||||
@@ -2,24 +2,4 @@ name: Web应用扫描
|
||||
description: Web应用漏洞扫描专家,全面的Web安全检测
|
||||
user_prompt: 你是一个专业的Web应用漏洞扫描专家。请使用各种Web扫描工具对目标Web应用进行全面的安全检测,包括目录枚举、文件扫描、漏洞识别等工作。
|
||||
icon: "\U0001F310"
|
||||
tools:
|
||||
- dirsearch
|
||||
- dirb
|
||||
- gobuster
|
||||
- feroxbuster
|
||||
- ffuf
|
||||
- wfuzz
|
||||
- sqlmap
|
||||
- dalfox
|
||||
- xsser
|
||||
- nikto
|
||||
- nuclei
|
||||
- wpscan
|
||||
- httpx
|
||||
- http-framework-test
|
||||
- execute-python-script
|
||||
- install-python-package
|
||||
- record_vulnerability
|
||||
- list_knowledge_risk_types
|
||||
- search_knowledge_base
|
||||
enabled: true
|
||||
|
||||
@@ -2,18 +2,4 @@ name: Web框架测试
|
||||
description: Web框架安全测试专家,专注于Web应用框架漏洞检测
|
||||
user_prompt: 你是一个专业的Web框架安全测试专家。请使用专业的工具对Web应用框架进行安全测试,识别框架相关的安全漏洞和配置问题。
|
||||
icon: "\U0001F310"
|
||||
tools:
|
||||
- http-framework-test
|
||||
- nikto
|
||||
- nuclei
|
||||
- wafw00f
|
||||
- wpscan
|
||||
- httpx
|
||||
- burpsuite
|
||||
- zap
|
||||
- execute-python-script
|
||||
- install-python-package
|
||||
- record_vulnerability
|
||||
- list_knowledge_risk_types
|
||||
- search_knowledge_base
|
||||
enabled: true
|
||||
|
||||
@@ -2,30 +2,4 @@ name: 二进制分析
|
||||
description: 二进制分析与利用专家,擅长逆向工程和密码破解
|
||||
user_prompt: 你是一个专业的二进制分析与利用专家。请使用逆向工程工具分析二进制文件,识别漏洞,进行利用开发。同时擅长密码破解、哈希分析等技术。
|
||||
icon: "\U0001F52C"
|
||||
tools:
|
||||
- dirsearch
|
||||
- docker-bench-security
|
||||
- exec
|
||||
- execute-python-script
|
||||
- install-python-package
|
||||
- ghidra
|
||||
- graphql-scanner
|
||||
- hakrawler
|
||||
- hash-identifier
|
||||
- hashcat
|
||||
- hashpump
|
||||
- http-framework-test
|
||||
- httpx
|
||||
- gdb
|
||||
- radare2
|
||||
- objdump
|
||||
- strings
|
||||
- binwalk
|
||||
- ropper
|
||||
- ropgadget
|
||||
- john
|
||||
- cyberchef
|
||||
- record_vulnerability
|
||||
- list_knowledge_risk_types
|
||||
- search_knowledge_base
|
||||
enabled: true
|
||||
|
||||
@@ -2,16 +2,4 @@ name: 云安全审计
|
||||
description: 云安全审计专家,多云环境安全检测
|
||||
user_prompt: 你是一个专业的云安全审计专家。请使用专业的云安全工具对AWS、Azure、GCP等云环境进行全面的安全审计,包括配置检查、合规性评估、权限审计、安全最佳实践验证等工作。
|
||||
icon: ☁
|
||||
tools:
|
||||
- prowler
|
||||
- scout-suite
|
||||
- cloudmapper
|
||||
- pacu
|
||||
- terrascan
|
||||
- checkov
|
||||
- execute-python-script
|
||||
- install-python-package
|
||||
- record_vulnerability
|
||||
- list_knowledge_risk_types
|
||||
- search_knowledge_base
|
||||
enabled: true
|
||||
|
||||
@@ -2,30 +2,4 @@ name: 信息收集
|
||||
description: 资产发现与信息搜集专家
|
||||
user_prompt: 你是一个专业的信息收集专家。请使用各种信息收集技术和工具,对目标进行全面的资产发现、子域名枚举、端口扫描、服务识别等信息收集工作。
|
||||
icon: "\U0001F50D"
|
||||
tools:
|
||||
- amass
|
||||
- subfinder
|
||||
- dnsenum
|
||||
- fierce
|
||||
- fofa_search
|
||||
- zoomeye_search
|
||||
- nmap
|
||||
- masscan
|
||||
- rustscan
|
||||
- arp-scan
|
||||
- nbtscan
|
||||
- httpx
|
||||
- http-framework-test
|
||||
- katana
|
||||
- hakrawler
|
||||
- waybackurls
|
||||
- paramspider
|
||||
- gau
|
||||
- uro
|
||||
- qsreplace
|
||||
- execute-python-script
|
||||
- install-python-package
|
||||
- record_vulnerability
|
||||
- list_knowledge_risk_types
|
||||
- search_knowledge_base
|
||||
enabled: true
|
||||
|
||||
@@ -2,22 +2,4 @@ name: 后渗透测试
|
||||
description: 后渗透测试专家,权限维持与横向移动
|
||||
user_prompt: 你是一个专业的后渗透测试专家。请使用专业的后渗透工具在获得初始访问权限后进行权限提升、横向移动、权限维持、数据收集等后渗透测试工作。
|
||||
icon: "\U0001F575"
|
||||
tools:
|
||||
- linpeas
|
||||
- winpeas
|
||||
- mimikatz
|
||||
- bloodhound
|
||||
- impacket
|
||||
- responder
|
||||
- netexec
|
||||
- rpcclient
|
||||
- smbmap
|
||||
- enum4linux
|
||||
- enum4linux-ng
|
||||
- exec
|
||||
- execute-python-script
|
||||
- install-python-package
|
||||
- record_vulnerability
|
||||
- list_knowledge_risk_types
|
||||
- search_knowledge_base
|
||||
enabled: true
|
||||
|
||||
@@ -2,17 +2,4 @@ name: 容器安全
|
||||
description: 容器与Kubernetes安全专家,容器环境安全检测
|
||||
user_prompt: 你是一个专业的容器与Kubernetes安全专家。请使用专业的容器安全工具对Docker容器和Kubernetes集群进行全面的安全检测,包括镜像漏洞扫描、配置检查、运行时安全等工作。
|
||||
icon: "\U0001F6E1"
|
||||
tools:
|
||||
- trivy
|
||||
- clair
|
||||
- docker-bench-security
|
||||
- kube-bench
|
||||
- kube-hunter
|
||||
- falco
|
||||
- exec
|
||||
- execute-python-script
|
||||
- install-python-package
|
||||
- record_vulnerability
|
||||
- list_knowledge_risk_types
|
||||
- search_knowledge_base
|
||||
enabled: true
|
||||
|
||||
@@ -2,23 +2,4 @@ name: 数字取证
|
||||
description: 数字取证与隐写分析专家,文件与内存取证
|
||||
user_prompt: 你是一个专业的数字取证与隐写分析专家。请使用专业的取证工具对文件、磁盘镜像、内存转储进行分析,提取证据信息。同时擅长隐写分析、数据恢复、元数据提取等技术。
|
||||
icon: "\U0001F50E"
|
||||
tools:
|
||||
- volatility
|
||||
- volatility3
|
||||
- foremost
|
||||
- steghide
|
||||
- stegsolve
|
||||
- zsteg
|
||||
- exiftool
|
||||
- binwalk
|
||||
- strings
|
||||
- xxd
|
||||
- fcrackzip
|
||||
- pdfcrack
|
||||
- exec
|
||||
- execute-python-script
|
||||
- install-python-package
|
||||
- record_vulnerability
|
||||
- list_knowledge_risk_types
|
||||
- search_knowledge_base
|
||||
enabled: true
|
||||
|
||||
@@ -2,32 +2,4 @@ name: 渗透测试
|
||||
description: 专业渗透测试专家,全面深入的漏洞检测
|
||||
user_prompt: 你是一个专业的网络安全渗透测试专家。请使用专业的渗透测试方法和工具,对目标进行全面的安全测试,包括但不限于SQL注入、XSS、CSRF、文件包含、命令执行等常见漏洞。
|
||||
icon: "\U0001F3AF"
|
||||
tools:
|
||||
- http-framework-test
|
||||
- httpx
|
||||
- amass
|
||||
- anew
|
||||
- angr
|
||||
- api-fuzzer
|
||||
- api-schema-analyzer
|
||||
- arjun
|
||||
- arp-scan
|
||||
- autorecon
|
||||
- binwalk
|
||||
- bloodhound
|
||||
- burpsuite
|
||||
- cat
|
||||
- checkov
|
||||
- checksec
|
||||
- cloudmapper
|
||||
- create-file
|
||||
- cyberchef
|
||||
- dalfox
|
||||
- delete-file
|
||||
- exec
|
||||
- execute-python-script
|
||||
- install-python-package
|
||||
- record_vulnerability
|
||||
- list_knowledge_risk_types
|
||||
- search_knowledge_base
|
||||
enabled: true
|
||||
|
||||
@@ -2,22 +2,4 @@ name: 综合漏洞扫描
|
||||
description: 综合漏洞扫描专家,多类型漏洞检测
|
||||
user_prompt: 你是一个专业的综合漏洞扫描专家。请使用各种漏洞扫描工具对目标进行全面的安全检测,包括Web漏洞、网络服务漏洞、配置缺陷等多种类型的漏洞识别和分析。
|
||||
icon: ⚠
|
||||
tools:
|
||||
- nuclei
|
||||
- nikto
|
||||
- sqlmap
|
||||
- nmap
|
||||
- masscan
|
||||
- rustscan
|
||||
- wafw00f
|
||||
- dalfox
|
||||
- xsser
|
||||
- jaeles
|
||||
- httpx
|
||||
- http-framework-test
|
||||
- execute-python-script
|
||||
- install-python-package
|
||||
- record_vulnerability
|
||||
- list_knowledge_risk_types
|
||||
- search_knowledge_base
|
||||
enabled: true
|
||||
|
||||
+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,
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 85 KiB |
@@ -178,7 +178,12 @@
|
||||
"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",
|
||||
"iterationLimitReachedMessage": "Maximum iteration count reached; automatic iteration has stopped.",
|
||||
"einoPendingOrphanedTitle": "🧹 Tool call reconciliation",
|
||||
"einoPendingOrphanedMessage": "Detected {{count}} unclosed tool call(s); marked as failed and finalized automatically.",
|
||||
"noDescription": "No description",
|
||||
"noResponseData": "No response data",
|
||||
"loading": "Loading...",
|
||||
@@ -234,7 +239,7 @@
|
||||
},
|
||||
"hitl": {
|
||||
"pageTitle": "HITL approvals",
|
||||
"pendingTitle": "Pending interrupts"
|
||||
"pendingTitle": "Pending approvals"
|
||||
},
|
||||
"progress": {
|
||||
"callingAI": "Calling AI model...",
|
||||
@@ -298,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",
|
||||
@@ -1307,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",
|
||||
@@ -1316,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",
|
||||
@@ -1667,6 +1704,7 @@
|
||||
},
|
||||
"contextMenu": {
|
||||
"viewAttackChain": "View attack chain",
|
||||
"viewVulnerabilities": "View vulnerabilities",
|
||||
"downloadMarkdown": "Download Markdown",
|
||||
"downloadMarkdownSummary": "Summary",
|
||||
"downloadMarkdownFull": "Full",
|
||||
@@ -1762,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",
|
||||
@@ -1789,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,12 @@
|
||||
"taskCancelled": "任务已取消",
|
||||
"unknownTool": "未知工具",
|
||||
"einoAgentReplyTitle": "子代理回复",
|
||||
"einoRecoveryTitle": "🔄 工具参数无效 · 第 {{n}}/{{max}} 轮(已追加提示)",
|
||||
"einoStreamErrorTitle": "⚠️ Eino 流式中断({{agent}})",
|
||||
"einoStreamErrorMessage": "流式读取异常,系统将按策略重试或结束。",
|
||||
"iterationLimitReachedTitle": "⛔ 达到迭代上限",
|
||||
"iterationLimitReachedMessage": "已达到最大迭代次数,任务已停止继续自动迭代。",
|
||||
"einoPendingOrphanedTitle": "🧹 工具调用收尾补偿",
|
||||
"einoPendingOrphanedMessage": "检测到 {{count}} 个未闭合工具调用,已自动标记为失败并收尾。",
|
||||
"noDescription": "暂无描述",
|
||||
"noResponseData": "暂无响应数据",
|
||||
"loading": "加载中...",
|
||||
@@ -234,7 +239,7 @@
|
||||
},
|
||||
"hitl": {
|
||||
"pageTitle": "人机协同审批",
|
||||
"pendingTitle": "待处理中断"
|
||||
"pendingTitle": "待处理审批"
|
||||
},
|
||||
"progress": {
|
||||
"callingAI": "正在调用AI模型...",
|
||||
@@ -298,6 +303,8 @@
|
||||
"clearHistory": "清空历史",
|
||||
"cancelTask": "取消任务",
|
||||
"viewConversation": "查看对话",
|
||||
"viewVulnerabilities": "查看漏洞",
|
||||
"viewVulnerabilitiesQueueTitle": "查看漏洞:打开漏洞管理并筛选本队列",
|
||||
"retryTask": "重试",
|
||||
"conversationIdLabel": "对话ID",
|
||||
"statusPending": "待执行",
|
||||
@@ -1307,6 +1314,12 @@
|
||||
"clear": "清除",
|
||||
"vulnId": "漏洞ID",
|
||||
"conversationId": "会话ID",
|
||||
"taskOrQueueId": "任务ID/队列ID",
|
||||
"filterTaskOrQueue": "筛选任务ID或队列ID",
|
||||
"conversationTag": "对话标签",
|
||||
"filterConversationTag": "筛选对话标签",
|
||||
"taskTag": "任务标签",
|
||||
"filterTaskTag": "筛选任务标签",
|
||||
"severity": "严重程度",
|
||||
"status": "状态",
|
||||
"statusOpen": "待处理",
|
||||
@@ -1316,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": "状态筛选",
|
||||
@@ -1667,6 +1704,7 @@
|
||||
},
|
||||
"contextMenu": {
|
||||
"viewAttackChain": "查看攻击链",
|
||||
"viewVulnerabilities": "查看漏洞",
|
||||
"downloadMarkdown": "下载 Markdown",
|
||||
"downloadMarkdownSummary": "简版",
|
||||
"downloadMarkdownFull": "完整版",
|
||||
@@ -1762,6 +1800,10 @@
|
||||
"vulnerabilityModal": {
|
||||
"conversationId": "会话ID",
|
||||
"conversationIdPlaceholder": "输入会话ID",
|
||||
"conversationTag": "对话标签",
|
||||
"conversationTagPlaceholder": "如:红队演练A、客户A周报",
|
||||
"taskTag": "任务标签",
|
||||
"taskTagPlaceholder": "如:批量扫描Q2、专项复测",
|
||||
"title": "标题",
|
||||
"titlePlaceholder": "漏洞标题",
|
||||
"description": "描述",
|
||||
@@ -1789,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": "编辑角色",
|
||||
|
||||
+73
-41
@@ -765,50 +765,59 @@ async function sendMessage() {
|
||||
if (!response.ok) {
|
||||
throw new Error('请求失败: ' + response.status);
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop(); // 保留最后一个不完整的行
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
try {
|
||||
const eventData = JSON.parse(line.slice(6));
|
||||
handleStreamEvent(eventData, progressElement, progressId,
|
||||
() => assistantMessageId, (id) => { assistantMessageId = id; },
|
||||
() => mcpExecutionIds, (ids) => { mcpExecutionIds = ids; });
|
||||
} catch (e) {
|
||||
console.error('解析事件数据失败:', e, line);
|
||||
|
||||
window.__csAgentLiveStream = {
|
||||
active: true,
|
||||
conversationId: currentConversationId || null,
|
||||
progressId: progressId
|
||||
};
|
||||
try {
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop(); // 保留最后一个不完整的行
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
try {
|
||||
const eventData = JSON.parse(line.slice(6));
|
||||
handleStreamEvent(eventData, progressElement, progressId,
|
||||
() => assistantMessageId, (id) => { assistantMessageId = id; },
|
||||
() => mcpExecutionIds, (ids) => { mcpExecutionIds = ids; });
|
||||
} catch (e) {
|
||||
console.error('解析事件数据失败:', e, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理剩余的buffer
|
||||
if (buffer.trim()) {
|
||||
const lines = buffer.split('\n');
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
try {
|
||||
const eventData = JSON.parse(line.slice(6));
|
||||
handleStreamEvent(eventData, progressElement, progressId,
|
||||
() => assistantMessageId, (id) => { assistantMessageId = id; },
|
||||
() => mcpExecutionIds, (ids) => { mcpExecutionIds = ids; });
|
||||
} catch (e) {
|
||||
console.error('解析事件数据失败:', e, line);
|
||||
|
||||
// 处理剩余的buffer
|
||||
if (buffer.trim()) {
|
||||
const lines = buffer.split('\n');
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
try {
|
||||
const eventData = JSON.parse(line.slice(6));
|
||||
handleStreamEvent(eventData, progressElement, progressId,
|
||||
() => assistantMessageId, (id) => { assistantMessageId = id; },
|
||||
() => mcpExecutionIds, (ids) => { mcpExecutionIds = ids; });
|
||||
} catch (e) {
|
||||
console.error('解析事件数据失败:', e, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
window.__csAgentLiveStream = { active: false, conversationId: null, progressId: null };
|
||||
}
|
||||
|
||||
|
||||
// 消息发送成功后,再次确保草稿被清除
|
||||
clearChatDraft();
|
||||
try {
|
||||
@@ -2217,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') {
|
||||
@@ -2922,6 +2927,22 @@ async function loadConversation(conversationId) {
|
||||
await window.restoreHitlInlineForConversation(conversationId);
|
||||
}
|
||||
}
|
||||
|
||||
// 页面刷新后主流式连接会中断;若该会话仍在后端运行,自动挂载 task-events 补流继续更新前端迭代进度。
|
||||
const skipReplay = typeof window.shouldSkipTaskEventReplayAttach === 'function'
|
||||
&& window.shouldSkipTaskEventReplayAttach(conversationId);
|
||||
if (
|
||||
seq === loadConversationRequestSeq &&
|
||||
currentConversationId === conversationId &&
|
||||
typeof window.attachRunningTaskEventStream === 'function' &&
|
||||
!skipReplay
|
||||
) {
|
||||
Promise.resolve()
|
||||
.then(() => window.attachRunningTaskEventStream(conversationId))
|
||||
.catch((e) => {
|
||||
console.warn('attachRunningTaskEventStream on loadConversation failed', e);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载对话失败:', error);
|
||||
alert('加载对话失败: ' + error.message);
|
||||
@@ -6100,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;
|
||||
|
||||
+170
-100
@@ -3,6 +3,36 @@ let activeTaskInterval = null;
|
||||
const ACTIVE_TASK_REFRESH_INTERVAL = 10000; // 10秒检查一次
|
||||
const TASK_FINAL_STATUSES = new Set(['failed', 'timeout', 'cancelled', 'completed']);
|
||||
|
||||
/**
|
||||
* 主对话 POST 流仍在读取时,禁止再挂 task-events 补流,否则同一事件会画两遍(与 HITL 是否开启无关)。
|
||||
* window.__csAgentLiveStream 由 chat.js sendMessage 在读到 body 后设置,在 finally 中清除。
|
||||
*/
|
||||
function syncAgentLiveStreamConversationId(cid) {
|
||||
if (!cid) return;
|
||||
try {
|
||||
const live = window.__csAgentLiveStream;
|
||||
if (live && live.active) {
|
||||
live.conversationId = cid;
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
function shouldSkipTaskEventReplayAttach(conversationId) {
|
||||
try {
|
||||
const live = window.__csAgentLiveStream;
|
||||
if (!live || !live.active || !live.progressId) return false;
|
||||
if (!document.getElementById(live.progressId)) return false;
|
||||
// 新会话:conversation 事件尚未到达前 conversationId 可能仍为 null,一律不补挂
|
||||
if (live.conversationId == null) return true;
|
||||
return live.conversationId === conversationId;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
window.shouldSkipTaskEventReplayAttach = shouldSkipTaskEventReplayAttach;
|
||||
}
|
||||
|
||||
// 当前界面语言对应的 BCP 47 标签(与时间格式化一致)
|
||||
function getCurrentTimeLocale() {
|
||||
if (typeof window.__locale === 'string' && window.__locale.length) {
|
||||
@@ -934,6 +964,7 @@ function handleStreamEvent(event, progressElement, progressId,
|
||||
|
||||
// 更新当前对话ID
|
||||
currentConversationId = event.data.conversationId;
|
||||
syncAgentLiveStreamConversationId(event.data.conversationId);
|
||||
updateActiveConversation();
|
||||
addAttackChainButton(currentConversationId);
|
||||
loadActiveTasks();
|
||||
@@ -1102,20 +1133,45 @@ function handleStreamEvent(event, progressElement, progressId,
|
||||
});
|
||||
break;
|
||||
|
||||
case 'eino_recovery': {
|
||||
case 'eino_stream_error': {
|
||||
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 agent = d.einoAgent ? String(d.einoAgent) : '';
|
||||
const title = typeof window.t === 'function'
|
||||
? window.t('chat.einoRecoveryTitle', { n: runIdx, max: maxRuns })
|
||||
: ('🔄 工具参数无效 · 第 ' + runIdx + '/' + maxRuns + ' 轮(已追加提示)');
|
||||
addTimelineItem(timeline, 'eino_recovery', {
|
||||
? window.t('chat.einoStreamErrorTitle', { agent: agent || '-' })
|
||||
: (agent ? ('⚠️ Eino 流式中断(' + agent + ')') : '⚠️ Eino 流式中断');
|
||||
addTimelineItem(timeline, 'warning', {
|
||||
title: title,
|
||||
message: event.message || '',
|
||||
message: event.message || (typeof window.t === 'function'
|
||||
? window.t('chat.einoStreamErrorMessage')
|
||||
: '流式读取异常,系统将按策略重试或结束。'),
|
||||
data: d
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'iteration_limit_reached': {
|
||||
addTimelineItem(timeline, 'warning', {
|
||||
title: typeof window.t === 'function' ? window.t('chat.iterationLimitReachedTitle') : '⛔ 达到迭代上限',
|
||||
message: event.message || (typeof window.t === 'function'
|
||||
? window.t('chat.iterationLimitReachedMessage')
|
||||
: '已达到最大迭代次数,任务已停止继续自动迭代。'),
|
||||
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_pending_orphaned': {
|
||||
const d = event.data || {};
|
||||
const count = Number(d.pendingCount || 0);
|
||||
const countText = Number.isFinite(count) && count > 0 ? String(count) : '?';
|
||||
addTimelineItem(timeline, 'warning', {
|
||||
title: typeof window.t === 'function' ? window.t('chat.einoPendingOrphanedTitle') : '🧹 工具调用收尾补偿',
|
||||
message: event.message || (typeof window.t === 'function'
|
||||
? window.t('chat.einoPendingOrphanedMessage', { count: countText })
|
||||
: ('检测到 ' + countText + ' 个未闭合工具调用,已自动标记为失败并收尾。')),
|
||||
data: d
|
||||
});
|
||||
finalizeOutstandingToolCallsForProgress(progressId, 'failed');
|
||||
break;
|
||||
}
|
||||
@@ -1429,6 +1485,7 @@ function handleStreamEvent(event, progressElement, progressId,
|
||||
break;
|
||||
}
|
||||
currentConversationId = responseData.conversationId;
|
||||
syncAgentLiveStreamConversationId(responseData.conversationId);
|
||||
updateActiveConversation();
|
||||
addAttackChainButton(currentConversationId);
|
||||
updateProgressConversation(progressId, responseData.conversationId);
|
||||
@@ -1509,6 +1566,7 @@ function handleStreamEvent(event, progressElement, progressId,
|
||||
}
|
||||
|
||||
currentConversationId = responseData.conversationId;
|
||||
syncAgentLiveStreamConversationId(responseData.conversationId);
|
||||
updateActiveConversation();
|
||||
addAttackChainButton(currentConversationId);
|
||||
updateProgressConversation(progressId, responseData.conversationId);
|
||||
@@ -1639,6 +1697,7 @@ function handleStreamEvent(event, progressElement, progressId,
|
||||
// 更新对话ID
|
||||
if (event.data && event.data.conversationId) {
|
||||
currentConversationId = event.data.conversationId;
|
||||
syncAgentLiveStreamConversationId(event.data.conversationId);
|
||||
updateActiveConversation();
|
||||
addAttackChainButton(currentConversationId);
|
||||
updateProgressConversation(progressId, event.data.conversationId);
|
||||
@@ -1939,90 +1998,120 @@ async function refreshLastAssistantProcessDetails(conversationId) {
|
||||
|
||||
window.refreshLastAssistantProcessDetails = refreshLastAssistantProcessDetails;
|
||||
|
||||
const taskEventReplayAttachState = {
|
||||
conversationId: null,
|
||||
inFlightPromise: null
|
||||
};
|
||||
|
||||
/**
|
||||
* 订阅运行中任务的 SSE 镜像(GET /api/agent-loop/task-events),用于 HITL 通过后主连接已断开时接续 UI。
|
||||
*/
|
||||
async function attachRunningTaskEventStream(conversationId) {
|
||||
if (!conversationId || typeof apiFetch !== 'function') return false;
|
||||
try {
|
||||
const check = await apiFetch('/api/agent-loop/tasks');
|
||||
if (!check.ok) return false;
|
||||
const j = await check.json().catch(function () { return {}; });
|
||||
const active = (j.tasks || []).some(function (t) {
|
||||
return t && t.conversationId === conversationId && (t.status === 'running' || t.status === 'cancelling');
|
||||
});
|
||||
if (!active) return false;
|
||||
if (
|
||||
taskEventReplayAttachState.inFlightPromise &&
|
||||
taskEventReplayAttachState.conversationId === conversationId
|
||||
) {
|
||||
return taskEventReplayAttachState.inFlightPromise;
|
||||
}
|
||||
if (shouldSkipTaskEventReplayAttach(conversationId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const asEl = findLastAssistantMessageElInChat();
|
||||
if (!asEl || !asEl.id) return false;
|
||||
const backendId = asEl.dataset && asEl.dataset.backendMessageId;
|
||||
if (backendId && typeof renderProcessDetails === 'function') {
|
||||
const res = await apiFetch('/api/messages/' + encodeURIComponent(String(backendId)) + '/process-details');
|
||||
const jd = await res.json().catch(function () { return {}; });
|
||||
if (res.ok && Array.isArray(jd.processDetails)) {
|
||||
renderProcessDetails(asEl.id, jd.processDetails);
|
||||
}
|
||||
}
|
||||
expandProcessDetailsTimeline(asEl.id);
|
||||
const attachPromise = (async function () {
|
||||
try {
|
||||
const check = await apiFetch('/api/agent-loop/tasks');
|
||||
if (!check.ok) return false;
|
||||
const j = await check.json().catch(function () { return {}; });
|
||||
const active = (j.tasks || []).some(function (t) {
|
||||
return t && t.conversationId === conversationId && (t.status === 'running' || t.status === 'cancelling');
|
||||
});
|
||||
if (!active) return false;
|
||||
|
||||
const progressId = taskReplayProgressId(conversationId);
|
||||
beginCsTaskReplay(progressId, asEl.id, conversationId);
|
||||
|
||||
const url = '/api/agent-loop/task-events?conversationId=' + encodeURIComponent(conversationId);
|
||||
const response = await apiFetch(url, {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'text/event-stream' }
|
||||
});
|
||||
if (!response.ok) {
|
||||
clearCsTaskReplay();
|
||||
if (progressTaskState.has(progressId)) {
|
||||
progressTaskState.delete(progressId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
let mcpIds = [];
|
||||
const assistantDomId = asEl.id;
|
||||
const getAssistantIdFn = function () { return assistantDomId; };
|
||||
const setAssistantIdFn = function () {};
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
while (true) {
|
||||
const chunk = await reader.read();
|
||||
if (chunk.done) break;
|
||||
buffer += decoder.decode(chunk.value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
for (let li = 0; li < lines.length; li++) {
|
||||
const line = lines[li];
|
||||
if (line.indexOf('data: ') === 0) {
|
||||
try {
|
||||
const eventData = JSON.parse(line.slice(6));
|
||||
handleStreamEvent(eventData, null, progressId, getAssistantIdFn, setAssistantIdFn, function () { return mcpIds; }, function (ids) { mcpIds = ids; });
|
||||
} catch (e) {
|
||||
console.error('task-events parse', e);
|
||||
const asEl = findLastAssistantMessageElInChat();
|
||||
if (!asEl || !asEl.id) return false;
|
||||
const backendId = asEl.dataset && asEl.dataset.backendMessageId;
|
||||
if (backendId && typeof renderProcessDetails === 'function') {
|
||||
const res = await apiFetch('/api/messages/' + encodeURIComponent(String(backendId)) + '/process-details');
|
||||
const jd = await res.json().catch(function () { return {}; });
|
||||
if (res.ok && Array.isArray(jd.processDetails)) {
|
||||
renderProcessDetails(asEl.id, jd.processDetails);
|
||||
// renderProcessDetails 会重建时间线节点,需重新挂载 HITL 审批入口
|
||||
if (typeof window.restoreHitlInlineForConversation === 'function') {
|
||||
await window.restoreHitlInlineForConversation(conversationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (window.csTaskReplay && window.csTaskReplay.progressId === progressId) {
|
||||
expandProcessDetailsTimeline(asEl.id);
|
||||
|
||||
const progressId = taskReplayProgressId(conversationId);
|
||||
beginCsTaskReplay(progressId, asEl.id, conversationId);
|
||||
|
||||
const url = '/api/agent-loop/task-events?conversationId=' + encodeURIComponent(conversationId);
|
||||
const response = await apiFetch(url, {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'text/event-stream' }
|
||||
});
|
||||
if (!response.ok) {
|
||||
clearCsTaskReplay();
|
||||
if (progressTaskState.has(progressId)) {
|
||||
progressTaskState.delete(progressId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
let mcpIds = [];
|
||||
const assistantDomId = asEl.id;
|
||||
const getAssistantIdFn = function () { return assistantDomId; };
|
||||
const setAssistantIdFn = function () {};
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
while (true) {
|
||||
const chunk = await reader.read();
|
||||
if (chunk.done) break;
|
||||
buffer += decoder.decode(chunk.value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
for (let li = 0; li < lines.length; li++) {
|
||||
const line = lines[li];
|
||||
if (line.indexOf('data: ') === 0) {
|
||||
try {
|
||||
const eventData = JSON.parse(line.slice(6));
|
||||
handleStreamEvent(eventData, null, progressId, getAssistantIdFn, setAssistantIdFn, function () { return mcpIds; }, function (ids) { mcpIds = ids; });
|
||||
} catch (e) {
|
||||
console.error('task-events parse', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (window.csTaskReplay && window.csTaskReplay.progressId === progressId) {
|
||||
clearCsTaskReplay();
|
||||
}
|
||||
if (progressTaskState.has(progressId)) {
|
||||
finalizeProgressTask(progressId, typeof window.t === 'function' ? window.t('tasks.statusCompleted') : '已完成');
|
||||
}
|
||||
if (typeof loadActiveTasks === 'function') loadActiveTasks();
|
||||
if (typeof window.loadConversation === 'function' && window.currentConversationId === conversationId) {
|
||||
await window.loadConversation(conversationId);
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.warn('attachRunningTaskEventStream', e);
|
||||
clearCsTaskReplay();
|
||||
return false;
|
||||
} finally {
|
||||
if (taskEventReplayAttachState.inFlightPromise === attachPromise) {
|
||||
taskEventReplayAttachState.inFlightPromise = null;
|
||||
taskEventReplayAttachState.conversationId = null;
|
||||
}
|
||||
}
|
||||
if (progressTaskState.has(progressId)) {
|
||||
finalizeProgressTask(progressId, typeof window.t === 'function' ? window.t('tasks.statusCompleted') : '已完成');
|
||||
}
|
||||
if (typeof loadActiveTasks === 'function') loadActiveTasks();
|
||||
if (typeof window.loadConversation === 'function' && window.currentConversationId === conversationId) {
|
||||
await window.loadConversation(conversationId);
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.warn('attachRunningTaskEventStream', e);
|
||||
clearCsTaskReplay();
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
taskEventReplayAttachState.conversationId = conversationId;
|
||||
taskEventReplayAttachState.inFlightPromise = attachPromise;
|
||||
return attachPromise;
|
||||
}
|
||||
|
||||
window.attachRunningTaskEventStream = attachRunningTaskEventStream;
|
||||
@@ -2083,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);
|
||||
}
|
||||
@@ -2202,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 += `
|
||||
@@ -3090,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;
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 442 KiB After Width: | Height: | Size: 85 KiB |
@@ -734,7 +734,7 @@
|
||||
</div>
|
||||
<div class="page-content">
|
||||
<div class="settings-section">
|
||||
<h3 data-i18n="hitl.pendingTitle">待处理中断</h3>
|
||||
<h3 data-i18n="hitl.pendingTitle">待处理审批</h3>
|
||||
<div id="hitl-pending-list" class="hitl-pending-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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