mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-17 21:44:43 +02:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d4c1dfb11 | |||
| 747c4a4c01 | |||
| 3d9f600e73 | |||
| 81757948eb | |||
| 98d36f750b | |||
| d598c40570 | |||
| 2064e89356 | |||
| 4a7422cbc4 | |||
| c5fc0fa2c1 | |||
| a98bfa35fd | |||
| bb05f6677f | |||
| 231ef57642 | |||
| 12eecfe5d2 | |||
| 5fa25eacb5 | |||
| 885203358c | |||
| 6fdd2c88da | |||
| 8581027bbe | |||
| 6084d2d84f | |||
| 9e7ef85510 | |||
| 89b4517a83 | |||
| ae528843ff | |||
| fc40b42d35 |
@@ -30,6 +30,9 @@ CyberStrikeAI is an **AI-native security testing platform** built in Go. It inte
|
||||
### Task Management
|
||||
<img src="./img/任务.png" alt="Task Management" width="560">
|
||||
|
||||
### Role Management
|
||||
<img src="./img/角色管理.png" alt="Role Management" width="560">
|
||||
|
||||
## Highlights
|
||||
|
||||
- 🤖 AI decision engine with OpenAI-compatible models (GPT, Claude, DeepSeek, etc.)
|
||||
|
||||
@@ -29,6 +29,9 @@ CyberStrikeAI 是一款 **AI 原生安全测试平台**,基于 Go 构建,集
|
||||
### 任务管理
|
||||
<img src="./img/任务.png" alt="任务管理" width="560">
|
||||
|
||||
### 角色管理
|
||||
<img src="./img/角色管理.png" alt="角色管理" width="560">
|
||||
|
||||
## 特性速览
|
||||
|
||||
- 🤖 兼容 OpenAI/DeepSeek/Claude 等模型的智能决策引擎
|
||||
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 331 KiB After Width: | Height: | Size: 543 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 506 KiB |
@@ -12,6 +12,7 @@ import (
|
||||
type BatchTaskQueueRow struct {
|
||||
ID string
|
||||
Title sql.NullString
|
||||
Role sql.NullString
|
||||
Status string
|
||||
CreatedAt time.Time
|
||||
StartedAt sql.NullTime
|
||||
@@ -33,7 +34,7 @@ type BatchTaskRow struct {
|
||||
}
|
||||
|
||||
// CreateBatchQueue 创建批量任务队列
|
||||
func (db *DB) CreateBatchQueue(queueID string, title string, tasks []map[string]interface{}) error {
|
||||
func (db *DB) CreateBatchQueue(queueID string, title string, role string, tasks []map[string]interface{}) error {
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("开始事务失败: %w", err)
|
||||
@@ -42,8 +43,8 @@ func (db *DB) CreateBatchQueue(queueID string, title string, tasks []map[string]
|
||||
|
||||
now := time.Now()
|
||||
_, err = tx.Exec(
|
||||
"INSERT INTO batch_task_queues (id, title, status, created_at, current_index) VALUES (?, ?, ?, ?, ?)",
|
||||
queueID, title, "pending", now, 0,
|
||||
"INSERT INTO batch_task_queues (id, title, role, status, created_at, current_index) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
queueID, title, role, "pending", now, 0,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建批量任务队列失败: %w", err)
|
||||
@@ -77,9 +78,9 @@ func (db *DB) GetBatchQueue(queueID string) (*BatchTaskQueueRow, error) {
|
||||
var row BatchTaskQueueRow
|
||||
var createdAt string
|
||||
err := db.QueryRow(
|
||||
"SELECT id, title, status, created_at, started_at, completed_at, current_index FROM batch_task_queues WHERE id = ?",
|
||||
"SELECT id, title, role, status, created_at, started_at, completed_at, current_index FROM batch_task_queues WHERE id = ?",
|
||||
queueID,
|
||||
).Scan(&row.ID, &row.Title, &row.Status, &createdAt, &row.StartedAt, &row.CompletedAt, &row.CurrentIndex)
|
||||
).Scan(&row.ID, &row.Title, &row.Role, &row.Status, &createdAt, &row.StartedAt, &row.CompletedAt, &row.CurrentIndex)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -103,7 +104,7 @@ func (db *DB) GetBatchQueue(queueID string) (*BatchTaskQueueRow, error) {
|
||||
// GetAllBatchQueues 获取所有批量任务队列
|
||||
func (db *DB) GetAllBatchQueues() ([]*BatchTaskQueueRow, error) {
|
||||
rows, err := db.Query(
|
||||
"SELECT id, title, status, created_at, started_at, completed_at, current_index FROM batch_task_queues ORDER BY created_at DESC",
|
||||
"SELECT id, title, role, status, created_at, started_at, completed_at, current_index FROM batch_task_queues ORDER BY created_at DESC",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询批量任务队列列表失败: %w", err)
|
||||
@@ -114,7 +115,7 @@ func (db *DB) GetAllBatchQueues() ([]*BatchTaskQueueRow, error) {
|
||||
for rows.Next() {
|
||||
var row BatchTaskQueueRow
|
||||
var createdAt string
|
||||
if err := rows.Scan(&row.ID, &row.Title, &row.Status, &createdAt, &row.StartedAt, &row.CompletedAt, &row.CurrentIndex); err != nil {
|
||||
if err := rows.Scan(&row.ID, &row.Title, &row.Role, &row.Status, &createdAt, &row.StartedAt, &row.CompletedAt, &row.CurrentIndex); err != nil {
|
||||
return nil, fmt.Errorf("扫描批量任务队列失败: %w", err)
|
||||
}
|
||||
parsedTime, parseErr := time.Parse("2006-01-02 15:04:05", createdAt)
|
||||
@@ -134,7 +135,7 @@ func (db *DB) GetAllBatchQueues() ([]*BatchTaskQueueRow, error) {
|
||||
|
||||
// ListBatchQueues 列出批量任务队列(支持筛选和分页)
|
||||
func (db *DB) ListBatchQueues(limit, offset int, status, keyword string) ([]*BatchTaskQueueRow, error) {
|
||||
query := "SELECT id, title, status, created_at, started_at, completed_at, current_index FROM batch_task_queues WHERE 1=1"
|
||||
query := "SELECT id, title, role, status, created_at, started_at, completed_at, current_index FROM batch_task_queues WHERE 1=1"
|
||||
args := []interface{}{}
|
||||
|
||||
// 状态筛选
|
||||
@@ -162,7 +163,7 @@ func (db *DB) ListBatchQueues(limit, offset int, status, keyword string) ([]*Bat
|
||||
for rows.Next() {
|
||||
var row BatchTaskQueueRow
|
||||
var createdAt string
|
||||
if err := rows.Scan(&row.ID, &row.Title, &row.Status, &createdAt, &row.StartedAt, &row.CompletedAt, &row.CurrentIndex); err != nil {
|
||||
if err := rows.Scan(&row.ID, &row.Title, &row.Role, &row.Status, &createdAt, &row.StartedAt, &row.CompletedAt, &row.CurrentIndex); err != nil {
|
||||
return nil, fmt.Errorf("扫描批量任务队列失败: %w", err)
|
||||
}
|
||||
parsedTime, parseErr := time.Parse("2006-01-02 15:04:05", createdAt)
|
||||
|
||||
@@ -433,7 +433,7 @@ func (db *DB) migrateConversationGroupMappingsTable() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateBatchTaskQueuesTable 迁移batch_task_queues表,添加title字段
|
||||
// migrateBatchTaskQueuesTable 迁移batch_task_queues表,添加title和role字段
|
||||
func (db *DB) migrateBatchTaskQueuesTable() error {
|
||||
// 检查title字段是否存在
|
||||
var count int
|
||||
@@ -454,6 +454,25 @@ func (db *DB) migrateBatchTaskQueuesTable() error {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查role字段是否存在
|
||||
var roleCount int
|
||||
err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('batch_task_queues') WHERE name='role'").Scan(&roleCount)
|
||||
if err != nil {
|
||||
// 如果查询失败,尝试添加字段
|
||||
if _, addErr := db.Exec("ALTER TABLE batch_task_queues ADD COLUMN role TEXT"); addErr != nil {
|
||||
// 如果字段已存在,忽略错误
|
||||
errMsg := strings.ToLower(addErr.Error())
|
||||
if !strings.Contains(errMsg, "duplicate column") && !strings.Contains(errMsg, "already exists") {
|
||||
db.logger.Warn("添加role字段失败", zap.Error(addErr))
|
||||
}
|
||||
}
|
||||
} else if roleCount == 0 {
|
||||
// 字段不存在,添加它
|
||||
if _, err := db.Exec("ALTER TABLE batch_task_queues ADD COLUMN role TEXT"); err != nil {
|
||||
db.logger.Warn("添加role字段失败", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -811,6 +811,7 @@ func (h *AgentHandler) ListCompletedTasks(c *gin.Context) {
|
||||
type BatchTaskRequest struct {
|
||||
Title string `json:"title"` // 任务标题(可选)
|
||||
Tasks []string `json:"tasks" binding:"required"` // 任务列表,每行一个任务
|
||||
Role string `json:"role,omitempty"` // 角色名称(可选,空字符串表示默认角色)
|
||||
}
|
||||
|
||||
// CreateBatchQueue 创建批量任务队列
|
||||
@@ -839,7 +840,7 @@ func (h *AgentHandler) CreateBatchQueue(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
queue := h.batchTaskManager.CreateBatchQueue(req.Title, validTasks)
|
||||
queue := h.batchTaskManager.CreateBatchQueue(req.Title, req.Role, validTasks)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"queueId": queue.ID,
|
||||
"queue": queue,
|
||||
@@ -1095,7 +1096,27 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
|
||||
// 保存conversationId到任务中(即使是运行中状态也要保存,以便查看对话)
|
||||
h.batchTaskManager.UpdateTaskStatusWithConversationID(queueID, task.ID, "running", "", "", conversationID)
|
||||
|
||||
// 保存用户消息
|
||||
// 应用角色用户提示词和工具配置
|
||||
finalMessage := task.Message
|
||||
var roleTools []string // 角色配置的工具列表
|
||||
if queue.Role != "" && queue.Role != "默认" {
|
||||
if h.config.Roles != nil {
|
||||
if role, exists := h.config.Roles[queue.Role]; exists && role.Enabled {
|
||||
// 应用用户提示词
|
||||
if role.UserPrompt != "" {
|
||||
finalMessage = role.UserPrompt + "\n\n" + task.Message
|
||||
h.logger.Info("应用角色用户提示词", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.String("role", queue.Role))
|
||||
}
|
||||
// 获取角色配置的工具列表(优先使用tools字段,向后兼容mcps字段)
|
||||
if len(role.Tools) > 0 {
|
||||
roleTools = role.Tools
|
||||
h.logger.Info("使用角色配置的工具列表", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.String("role", queue.Role), zap.Int("toolCount", len(roleTools)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 保存用户消息(保存原始消息,不包含角色提示词)
|
||||
_, err = h.db.AddMessage(conversationID, "user", task.Message, nil)
|
||||
if err != nil {
|
||||
h.logger.Error("保存用户消息失败", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.String("conversationId", conversationID), zap.Error(err))
|
||||
@@ -1116,14 +1137,14 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
|
||||
}
|
||||
progressCallback := h.createProgressCallback(conversationID, assistantMessageID, nil)
|
||||
|
||||
// 执行任务
|
||||
h.logger.Info("执行批量任务", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.String("message", task.Message), zap.String("conversationId", conversationID))
|
||||
// 执行任务(使用包含角色提示词的finalMessage和角色工具列表)
|
||||
h.logger.Info("执行批量任务", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.String("message", task.Message), zap.String("role", queue.Role), zap.String("conversationId", conversationID))
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
|
||||
// 存储取消函数,以便在取消队列时能够取消当前任务
|
||||
h.batchTaskManager.SetTaskCancel(queueID, cancel)
|
||||
// 批量任务暂时不支持角色工具过滤,使用所有工具(传入nil)
|
||||
result, err := h.agent.AgentLoopWithProgress(ctx, task.Message, []agent.ChatMessage{}, conversationID, progressCallback, nil)
|
||||
// 使用队列配置的角色工具列表(如果为空,表示使用所有工具)
|
||||
result, err := h.agent.AgentLoopWithProgress(ctx, finalMessage, []agent.ChatMessage{}, conversationID, progressCallback, roleTools)
|
||||
// 任务执行完成,清理取消函数
|
||||
h.batchTaskManager.SetTaskCancel(queueID, nil)
|
||||
cancel()
|
||||
|
||||
@@ -29,6 +29,7 @@ type BatchTask struct {
|
||||
type BatchTaskQueue struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Role string `json:"role,omitempty"` // 角色名称(空字符串表示默认角色)
|
||||
Tasks []*BatchTask `json:"tasks"`
|
||||
Status string `json:"status"` // pending, running, paused, completed, cancelled
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
@@ -62,7 +63,7 @@ func (m *BatchTaskManager) SetDB(db *database.DB) {
|
||||
}
|
||||
|
||||
// CreateBatchQueue 创建批量任务队列
|
||||
func (m *BatchTaskManager) CreateBatchQueue(title string, tasks []string) *BatchTaskQueue {
|
||||
func (m *BatchTaskManager) CreateBatchQueue(title, role string, tasks []string) *BatchTaskQueue {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
@@ -70,6 +71,7 @@ func (m *BatchTaskManager) CreateBatchQueue(title string, tasks []string) *Batch
|
||||
queue := &BatchTaskQueue{
|
||||
ID: queueID,
|
||||
Title: title,
|
||||
Role: role,
|
||||
Tasks: make([]*BatchTask, 0, len(tasks)),
|
||||
Status: "pending",
|
||||
CreatedAt: time.Now(),
|
||||
@@ -98,7 +100,7 @@ func (m *BatchTaskManager) CreateBatchQueue(title string, tasks []string) *Batch
|
||||
|
||||
// 保存到数据库
|
||||
if m.db != nil {
|
||||
if err := m.db.CreateBatchQueue(queueID, title, dbTasks); err != nil {
|
||||
if err := m.db.CreateBatchQueue(queueID, title, role, dbTasks); err != nil {
|
||||
// 如果数据库保存失败,记录错误但继续(使用内存缓存)
|
||||
// 这里可以添加日志记录
|
||||
}
|
||||
@@ -158,6 +160,9 @@ func (m *BatchTaskManager) loadQueueFromDB(queueID string) *BatchTaskQueue {
|
||||
if queueRow.Title.Valid {
|
||||
queue.Title = queueRow.Title.String
|
||||
}
|
||||
if queueRow.Role.Valid {
|
||||
queue.Role = queueRow.Role.String
|
||||
}
|
||||
if queueRow.StartedAt.Valid {
|
||||
queue.StartedAt = &queueRow.StartedAt.Time
|
||||
}
|
||||
@@ -351,6 +356,9 @@ func (m *BatchTaskManager) LoadFromDB() error {
|
||||
if queueRow.Title.Valid {
|
||||
queue.Title = queueRow.Title.String
|
||||
}
|
||||
if queueRow.Role.Valid {
|
||||
queue.Role = queueRow.Role.String
|
||||
}
|
||||
if queueRow.StartedAt.Valid {
|
||||
queue.StartedAt = &queueRow.StartedAt.Time
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package security
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
@@ -616,6 +617,13 @@ func (e *Executor) formatParamValue(param config.ParameterConfig, value interfac
|
||||
return strings.Join(strs, ",")
|
||||
}
|
||||
return fmt.Sprintf("%v", value)
|
||||
case "object":
|
||||
// 对象/字典:序列化为 JSON 字符串
|
||||
if jsonBytes, err := json.Marshal(value); err == nil {
|
||||
return string(jsonBytes)
|
||||
}
|
||||
// 如果 JSON 序列化失败,回退到默认格式化
|
||||
return fmt.Sprintf("%v", value)
|
||||
default:
|
||||
formattedValue := fmt.Sprintf("%v", value)
|
||||
// 特殊处理:对于 ports 参数(通常是 nmap 等工具的端口参数),清理空格
|
||||
|
||||
@@ -445,7 +445,7 @@ args:
|
||||
parser.add_argument("--url", required=True)
|
||||
parser.add_argument("--method", default="GET")
|
||||
parser.add_argument("--data", default="")
|
||||
parser.add_argument("--headers", default="")
|
||||
parser.add_argument("--headers", default="", type=str)
|
||||
parser.add_argument("--cookies", default="")
|
||||
parser.add_argument("--user-agent", dest="user_agent", default="")
|
||||
parser.add_argument("--proxy", default="")
|
||||
@@ -489,7 +489,30 @@ args:
|
||||
prepared_url = smart_encode_url(args.url) if args.auto_encode_url else args.url
|
||||
method = (args.method or "GET").upper()
|
||||
|
||||
headers = httpx.Headers(parse_headers(args.headers))
|
||||
# 处理 headers:支持字典(JSON字符串)和字符串格式
|
||||
# 框架会将 object 类型序列化为 JSON 字符串传递
|
||||
headers_list = []
|
||||
if args.headers:
|
||||
headers_str = args.headers.strip()
|
||||
# 优先尝试解析为 JSON(框架传递的字典会被序列化为 JSON)
|
||||
if headers_str.startswith("{") or headers_str.startswith("["):
|
||||
try:
|
||||
parsed = json.loads(headers_str)
|
||||
if isinstance(parsed, dict):
|
||||
# 字典格式:直接转换为 (key, value) 元组列表
|
||||
headers_list = [(str(k).strip(), str(v).strip()) for k, v in parsed.items()]
|
||||
elif isinstance(parsed, list):
|
||||
# 数组格式:使用原有的 parse_headers 函数处理
|
||||
headers_list = parse_headers(headers_str)
|
||||
else:
|
||||
headers_list = parse_headers(headers_str)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
# JSON 解析失败,回退到原有的字符串解析逻辑
|
||||
headers_list = parse_headers(headers_str)
|
||||
else:
|
||||
# 非 JSON 格式,使用原有的字符串解析逻辑(向后兼容)
|
||||
headers_list = parse_headers(headers_str)
|
||||
headers = httpx.Headers(headers_list)
|
||||
if args.user_agent:
|
||||
headers["User-Agent"] = args.user_agent
|
||||
|
||||
@@ -724,8 +747,8 @@ parameters:
|
||||
required: false
|
||||
flag: "--data"
|
||||
- name: "headers"
|
||||
type: "string"
|
||||
description: "自定义请求头(JSON字典、行分隔或分号分隔的 Header: Value 格式)"
|
||||
type: "object"
|
||||
description: "自定义请求头(字典格式,如 {\"X-Custom\": \"value\"})"
|
||||
required: false
|
||||
flag: "--headers"
|
||||
- name: "cookies"
|
||||
|
||||
+36
-10
@@ -17,20 +17,46 @@ args:
|
||||
url = sys.argv[1]
|
||||
method = (sys.argv[2] or "GET").upper()
|
||||
location = (sys.argv[3] or "query").lower()
|
||||
params_json = sys.argv[4] if len(sys.argv) > 4 else "{}"
|
||||
params_input = sys.argv[4] if len(sys.argv) > 4 else "{}"
|
||||
payloads_json = sys.argv[5] if len(sys.argv) > 5 else "[]"
|
||||
max_requests = int(sys.argv[6]) if len(sys.argv) > 6 and sys.argv[6] else 0
|
||||
|
||||
try:
|
||||
params_template = json.loads(params_json) if params_json else {}
|
||||
# 框架会将 object 类型序列化为 JSON 字符串传递
|
||||
# sys.argv 中的参数都是字符串,需要解析 JSON
|
||||
if params_input and params_input.strip():
|
||||
params_template = json.loads(params_input)
|
||||
if not isinstance(params_template, dict):
|
||||
sys.stderr.write("参数模板必须是字典格式\n")
|
||||
sys.exit(1)
|
||||
else:
|
||||
params_template = {}
|
||||
except json.JSONDecodeError as exc:
|
||||
sys.stderr.write(f"参数模板解析失败: {exc}\n")
|
||||
sys.stderr.write(f"参数模板解析失败(需要 JSON 字典格式): {exc}\n")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
payloads = json.loads(payloads_json)
|
||||
except json.JSONDecodeError as exc:
|
||||
sys.stderr.write(f"载荷解析失败: {exc}\n")
|
||||
# 框架会将 array 类型转换为逗号分隔的字符串(见 formatParamValue)
|
||||
# 但为了兼容性,也支持 JSON 数组格式
|
||||
if payloads_json and payloads_json.strip():
|
||||
payloads_str = payloads_json.strip()
|
||||
# 优先尝试解析为 JSON 数组
|
||||
if payloads_str.startswith("["):
|
||||
try:
|
||||
payloads = json.loads(payloads_str)
|
||||
except json.JSONDecodeError:
|
||||
# JSON 解析失败,尝试逗号分隔格式
|
||||
payloads = [item.strip() for item in payloads_str.split(",") if item.strip()]
|
||||
else:
|
||||
# 逗号分隔的字符串(框架的 array 类型默认格式)
|
||||
payloads = [item.strip() for item in payloads_str.split(",") if item.strip()]
|
||||
if not isinstance(payloads, list):
|
||||
sys.stderr.write("载荷必须是数组格式\n")
|
||||
sys.exit(1)
|
||||
else:
|
||||
payloads = []
|
||||
except (json.JSONDecodeError, ValueError) as exc:
|
||||
sys.stderr.write(f"载荷解析失败(需要 JSON 数组或逗号分隔格式): {exc}\n")
|
||||
sys.exit(1)
|
||||
|
||||
if not isinstance(payloads, list) or not payloads:
|
||||
@@ -110,14 +136,14 @@ parameters:
|
||||
position: 2
|
||||
format: "positional"
|
||||
- name: "params"
|
||||
type: "string"
|
||||
description: "参数模板(JSON字典),指定要模糊的键及默认值"
|
||||
type: "object"
|
||||
description: "参数模板(字典格式),指定要模糊的键及默认值,如 {\"id\": \"1\", \"name\": \"test\"}"
|
||||
required: true
|
||||
position: 3
|
||||
format: "positional"
|
||||
- name: "payloads"
|
||||
type: "string"
|
||||
description: "载荷列表(JSON数组)"
|
||||
type: "array"
|
||||
description: "载荷列表(数组格式),如 [\"test1\", \"test2\", \"test3\"]"
|
||||
required: true
|
||||
position: 4
|
||||
format: "positional"
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
name: "httpx"
|
||||
command: "httpx"
|
||||
enabled: true
|
||||
short_description: "基于Python httpx库的HTTP客户端"
|
||||
description: |
|
||||
该工具包装的是 Python 社区版 httpx CLI(`pip install httpx` 提供),可用于快速向 Web 目标发起请求、调试接口。
|
||||
|
||||
**提示:**
|
||||
- 官方 CLI 的调用方式为 `httpx <URL> [OPTIONS]`
|
||||
- 不支持 ProjectDiscovery 版本的 `-u/-l/-td` 等参数,请使用下方列出的原生选项或 additional_args 自行扩展
|
||||
parameters:
|
||||
- name: "url"
|
||||
type: "string"
|
||||
description: "目标URL(必填,作为位置参数传入)"
|
||||
required: true
|
||||
format: "positional"
|
||||
- name: "method"
|
||||
type: "string"
|
||||
description: "HTTP方法,默认GET"
|
||||
required: false
|
||||
flag: "-m"
|
||||
format: "flag"
|
||||
- name: "content"
|
||||
type: "string"
|
||||
description: "原始请求体内容(对应 httpx CLI 的 --content)"
|
||||
required: false
|
||||
flag: "-c"
|
||||
format: "flag"
|
||||
- name: "json"
|
||||
type: "string"
|
||||
description: "JSON 请求体(字符串形式)"
|
||||
required: false
|
||||
flag: "-j"
|
||||
format: "flag"
|
||||
- name: "proxy"
|
||||
type: "string"
|
||||
description: "代理地址(http(s):// 或 socks5://)"
|
||||
required: false
|
||||
flag: "--proxy"
|
||||
format: "flag"
|
||||
- name: "timeout"
|
||||
type: "string"
|
||||
description: "网络超时时间(秒,可为小数)"
|
||||
required: false
|
||||
flag: "--timeout"
|
||||
format: "flag"
|
||||
- name: "follow_redirects"
|
||||
type: "bool"
|
||||
description: "是否自动跟随重定向"
|
||||
required: false
|
||||
flag: "--follow-redirects"
|
||||
format: "flag"
|
||||
default: false
|
||||
- name: "no_verify"
|
||||
type: "bool"
|
||||
description: "关闭TLS证书校验(对应 --no-verify)"
|
||||
required: false
|
||||
flag: "--no-verify"
|
||||
format: "flag"
|
||||
default: false
|
||||
- name: "http2"
|
||||
type: "bool"
|
||||
description: "启用HTTP/2"
|
||||
required: false
|
||||
flag: "--http2"
|
||||
format: "flag"
|
||||
default: false
|
||||
- name: "download"
|
||||
type: "string"
|
||||
description: "将响应内容保存至文件"
|
||||
required: false
|
||||
flag: "--download"
|
||||
format: "flag"
|
||||
- name: "verbose"
|
||||
type: "bool"
|
||||
description: "显示请求与响应的详细信息"
|
||||
required: false
|
||||
flag: "-v"
|
||||
format: "flag"
|
||||
default: false
|
||||
- name: "additional_args"
|
||||
type: "string"
|
||||
description: |
|
||||
额外 httpx CLI 选项,格式直接与官方命令保持一致。
|
||||
|
||||
**示例:**
|
||||
- "--headers 'X-Test 1' 'X-Token secret'"
|
||||
- "--cookies 'session abc123'"
|
||||
- "--auth user pass"
|
||||
required: false
|
||||
format: "positional"
|
||||
+251
-83
@@ -1063,7 +1063,7 @@ header {
|
||||
}
|
||||
|
||||
.message.system .message-content {
|
||||
max-width: 90%;
|
||||
max-width: 75%;
|
||||
align-items: stretch;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -1208,12 +1208,42 @@ header {
|
||||
}
|
||||
|
||||
/* Markdown 表格样式 */
|
||||
/* 表格包装容器,为每个表格提供独立的横向滚动 */
|
||||
.message-bubble .table-wrapper {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
margin: 1em 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
/* 自定义滚动条样式 */
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
|
||||
}
|
||||
|
||||
.message-bubble .table-wrapper::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.message-bubble .table-wrapper::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.message-bubble .table-wrapper::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.message-bubble .table-wrapper::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 表格本身,允许根据内容自动扩展宽度 */
|
||||
.message-bubble table {
|
||||
width: auto;
|
||||
min-width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1em 0;
|
||||
margin: 0;
|
||||
font-size: 0.9em;
|
||||
display: table;
|
||||
table-layout: auto;
|
||||
@@ -1249,13 +1279,16 @@ header {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
width: auto;
|
||||
max-width: 200px;
|
||||
min-width: 120px;
|
||||
max-width: none;
|
||||
vertical-align: top;
|
||||
/* 强制不换行,内容会保持在一行,超出部分会溢出(由表格横向滚动处理) */
|
||||
white-space: nowrap !important;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
/* 允许文字换行,避免重叠和溢出 */
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
hyphens: auto;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
|
||||
@@ -1332,6 +1365,18 @@ header {
|
||||
}
|
||||
|
||||
/* 用户消息中的表格样式 */
|
||||
.message.user .message-bubble .table-wrapper {
|
||||
scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
|
||||
}
|
||||
|
||||
.message.user .message-bubble .table-wrapper::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.message.user .message-bubble .table-wrapper::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.message.user .message-bubble table {
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
@@ -2593,6 +2638,9 @@ header {
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
color: var(--text-primary);
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.active-task-item {
|
||||
@@ -2604,8 +2652,8 @@ header {
|
||||
border: 1px solid rgba(0, 102, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
flex-shrink: 0;
|
||||
min-width: 280px;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
@@ -3429,6 +3477,9 @@ header {
|
||||
.monitor-sections {
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.monitor-section {
|
||||
@@ -3440,6 +3491,9 @@ header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.monitor-section .section-header {
|
||||
@@ -3447,12 +3501,16 @@ header {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.monitor-section .section-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-primary);
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.monitor-section .section-actions {
|
||||
@@ -3461,6 +3519,9 @@ header {
|
||||
gap: 12px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
flex-wrap: wrap;
|
||||
min-width: 0;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.monitor-section .section-actions select {
|
||||
@@ -3518,6 +3579,8 @@ header {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.monitor-stat-card {
|
||||
@@ -3529,35 +3592,55 @@ header {
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
box-shadow: var(--shadow-xs);
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.monitor-stat-card h4 {
|
||||
margin: 0;
|
||||
font-size: 0.95rem;
|
||||
color: var(--text-secondary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
word-break: break-word;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.monitor-stat-value {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.monitor-stat-meta {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
word-break: break-word;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.monitor-table-container {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.monitor-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.875rem;
|
||||
table-layout: auto;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.monitor-table th,
|
||||
@@ -3566,6 +3649,15 @@ header {
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
vertical-align: middle;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.monitor-table td:nth-child(2) {
|
||||
max-width: 250px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.monitor-table thead {
|
||||
@@ -6112,6 +6204,9 @@ header {
|
||||
.batch-manage-modal-content {
|
||||
max-width: 800px;
|
||||
width: 90vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 85vh;
|
||||
}
|
||||
|
||||
.batch-manage-header-actions {
|
||||
@@ -6146,8 +6241,12 @@ header {
|
||||
}
|
||||
|
||||
.batch-manage-body {
|
||||
max-height: 60vh;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.batch-conversations-table {
|
||||
@@ -6241,6 +6340,7 @@ header {
|
||||
justify-content: space-between;
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.select-all-checkbox {
|
||||
@@ -7742,29 +7842,30 @@ header {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
left: 0;
|
||||
width: 320px;
|
||||
width: 340px;
|
||||
max-width: calc(100vw - 32px);
|
||||
max-height: 60vh;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 16px;
|
||||
padding: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12), 0 4px 16px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(0, 0, 0, 0.04);
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: slideUp 0.3s ease-out;
|
||||
animation: slideUp 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
transform: translateY(8px) scale(0.98);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7772,55 +7873,61 @@ header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px 4px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
margin-bottom: 10px;
|
||||
padding: 10px 4px 12px 4px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--bg-primary);
|
||||
background: #ffffff;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.role-selection-panel-title {
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
color: #1a1a1a;
|
||||
margin: 0;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.role-selection-panel-close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.2s;
|
||||
color: #666666;
|
||||
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.role-selection-panel-close:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
color: #1a1a1a;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.role-selection-panel-close:active {
|
||||
transform: rotate(90deg) scale(0.95);
|
||||
}
|
||||
|
||||
.role-selection-list-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
/* 限制显示8个角色:每个角色约70px高度 + gap,8个角色约580px */
|
||||
max-height: 580px;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
padding-right: 6px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.role-selection-list-main::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.role-selection-list-main::-webkit-scrollbar-track {
|
||||
@@ -7828,81 +7935,102 @@ header {
|
||||
}
|
||||
|
||||
.role-selection-list-main::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
background: rgba(0, 0, 0, 0.16);
|
||||
border-radius: 4px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.role-selection-list-main::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
background: rgba(0, 0, 0, 0.24);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.role-selection-item-main {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-primary);
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: 1.5px solid rgba(0, 0, 0, 0.06);
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.role-selection-item-main:hover {
|
||||
background: var(--bg-secondary);
|
||||
border-color: rgba(138, 43, 226, 0.3);
|
||||
background: linear-gradient(135deg, rgba(138, 43, 226, 0.04) 0%, rgba(138, 43, 226, 0.02) 100%);
|
||||
border-color: rgba(138, 43, 226, 0.2);
|
||||
box-shadow: 0 2px 8px rgba(138, 43, 226, 0.08);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.role-selection-item-main.selected {
|
||||
background: rgba(138, 43, 226, 0.1);
|
||||
border-color: #8a2be2;
|
||||
background: linear-gradient(135deg, rgba(138, 43, 226, 0.12) 0%, rgba(138, 43, 226, 0.06) 100%);
|
||||
border-color: rgba(138, 43, 226, 0.4);
|
||||
box-shadow: 0 4px 12px rgba(138, 43, 226, 0.15), 0 0 0 1px rgba(138, 43, 226, 0.1);
|
||||
}
|
||||
|
||||
.role-selection-item-main:active {
|
||||
transform: translateY(0) scale(0.98);
|
||||
}
|
||||
|
||||
.role-selection-item-icon-main {
|
||||
font-size: 1.125rem;
|
||||
font-size: 1.25rem;
|
||||
flex-shrink: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
background: linear-gradient(135deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.02) 100%);
|
||||
border-radius: 10px;
|
||||
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.role-selection-item-main:hover .role-selection-item-icon-main {
|
||||
background: linear-gradient(135deg, rgba(138, 43, 226, 0.1) 0%, rgba(138, 43, 226, 0.05) 100%);
|
||||
border-color: rgba(138, 43, 226, 0.15);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.role-selection-item-main.selected .role-selection-item-icon-main {
|
||||
background: rgba(138, 43, 226, 0.15);
|
||||
background: linear-gradient(135deg, rgba(138, 43, 226, 0.2) 0%, rgba(138, 43, 226, 0.12) 100%);
|
||||
border-color: rgba(138, 43, 226, 0.3);
|
||||
box-shadow: 0 2px 6px rgba(138, 43, 226, 0.2);
|
||||
}
|
||||
|
||||
.role-selection-item-content-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.role-selection-item-name-main {
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.3;
|
||||
color: #1a1a1a;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
transition: color 0.2s;
|
||||
transition: color 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.role-selection-item-main.selected .role-selection-item-name-main {
|
||||
color: #8a2be2;
|
||||
color: #6a1bb2;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.role-selection-item-description-main {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.3;
|
||||
color: #666666;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
@@ -7910,23 +8038,44 @@ header {
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.role-selection-item-main.selected .role-selection-item-description-main {
|
||||
color: #8a5ab8;
|
||||
}
|
||||
|
||||
.role-selection-checkmark-main {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #8a2be2;
|
||||
background: linear-gradient(135deg, #8a2be2 0%, #6a1bb2 100%);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
font-size: 0.625rem;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2px 8px rgba(138, 43, 226, 0.4);
|
||||
animation: checkmarkPop 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
@keyframes checkmarkPop {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes highlight-flash {
|
||||
@@ -8557,42 +8706,43 @@ header {
|
||||
width: calc(100vw - 16px);
|
||||
max-width: calc(100vw - 16px);
|
||||
left: -8px;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
border-radius: 14px;
|
||||
max-height: 60vh;
|
||||
}
|
||||
|
||||
.role-selection-panel-header {
|
||||
margin-bottom: 8px;
|
||||
padding: 6px 4px;
|
||||
margin-bottom: 10px;
|
||||
padding: 8px 4px 10px 4px;
|
||||
}
|
||||
|
||||
.role-selection-panel-title {
|
||||
font-size: 0.8125rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.role-selection-list-main {
|
||||
/* 在移动设备上限制显示8个角色,每个约60px,总计约480px */
|
||||
max-height: 480px;
|
||||
gap: 4px;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.role-selection-item-main {
|
||||
padding: 8px 10px;
|
||||
padding: 10px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.role-selection-item-icon-main {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 1rem;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.role-selection-item-name-main {
|
||||
font-size: 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.role-selection-item-description-main {
|
||||
font-size: 0.6875rem;
|
||||
font-size: 0.71875rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8601,16 +8751,34 @@ header {
|
||||
width: calc(100vw - 8px);
|
||||
max-width: calc(100vw - 8px);
|
||||
left: -4px;
|
||||
padding: 6px;
|
||||
padding: 8px;
|
||||
border-radius: 12px;
|
||||
max-height: 50vh;
|
||||
}
|
||||
|
||||
.role-selection-list-main {
|
||||
/* 在小屏幕上限制显示8个角色,每个约55px,总计约450px */
|
||||
max-height: 450px;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.role-selection-item-main {
|
||||
padding: 6px 8px;
|
||||
padding: 10px;
|
||||
gap: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.role-selection-item-icon-main {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
font-size: 1.0625rem;
|
||||
}
|
||||
|
||||
.role-selection-checkmark-main {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
font-size: 0.625rem;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
+113
-4
@@ -803,6 +803,25 @@ function initializeChatUI() {
|
||||
// 消息计数器,确保ID唯一
|
||||
let messageCounter = 0;
|
||||
|
||||
// 为消息气泡中的表格添加独立的滚动容器
|
||||
function wrapTablesInBubble(bubble) {
|
||||
const tables = bubble.querySelectorAll('table');
|
||||
tables.forEach(table => {
|
||||
// 检查表格是否已经有包装容器
|
||||
if (table.parentElement && table.parentElement.classList.contains('table-wrapper')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建表格包装容器
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'table-wrapper';
|
||||
|
||||
// 将表格移动到包装容器中
|
||||
table.parentNode.insertBefore(wrapper, table);
|
||||
wrapper.appendChild(table);
|
||||
});
|
||||
}
|
||||
|
||||
// 添加消息
|
||||
function addMessage(role, content, mcpExecutionIds = null, progressId = null, createdAt = null) {
|
||||
const messagesDiv = document.getElementById('chat-messages');
|
||||
@@ -878,6 +897,10 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
|
||||
}
|
||||
|
||||
bubble.innerHTML = formattedContent;
|
||||
|
||||
// 为每个表格添加独立的滚动容器
|
||||
wrapTablesInBubble(bubble);
|
||||
|
||||
contentWrapper.appendChild(bubble);
|
||||
|
||||
// 保存原始内容到消息元素,用于复制功能
|
||||
@@ -943,6 +966,8 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
|
||||
detailBtn.innerHTML = `<span>调用 #${index + 1}</span>`;
|
||||
detailBtn.onclick = () => showMCPDetail(execId);
|
||||
buttonsContainer.appendChild(detailBtn);
|
||||
// 异步获取工具名称并更新按钮文本
|
||||
updateButtonWithToolName(detailBtn, execId, index + 1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1231,6 +1256,23 @@ window.addEventListener('beforeunload', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// 异步获取工具名称并更新按钮文本
|
||||
async function updateButtonWithToolName(button, executionId, index) {
|
||||
try {
|
||||
const response = await apiFetch(`/api/monitor/execution/${executionId}`);
|
||||
if (response.ok) {
|
||||
const exec = await response.json();
|
||||
const toolName = exec.toolName || '未知工具';
|
||||
// 格式化工具名称(如果是 name::toolName 格式,只显示 toolName 部分)
|
||||
const displayToolName = toolName.includes('::') ? toolName.split('::')[1] : toolName;
|
||||
button.querySelector('span').textContent = `${displayToolName} #${index}`;
|
||||
}
|
||||
} catch (error) {
|
||||
// 如果获取失败,保持原有文本不变
|
||||
console.error('获取工具名称失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示MCP调用详情
|
||||
async function showMCPDetail(executionId) {
|
||||
try {
|
||||
@@ -1449,16 +1491,27 @@ async function loadConversations(searchQuery = '') {
|
||||
url += '&search=' + encodeURIComponent(searchQuery.trim());
|
||||
}
|
||||
const response = await apiFetch(url);
|
||||
const conversations = await response.json();
|
||||
|
||||
const listContainer = document.getElementById('conversations-list');
|
||||
if (!listContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存滚动位置
|
||||
const sidebarContent = listContainer.closest('.sidebar-content');
|
||||
const savedScrollTop = sidebarContent ? sidebarContent.scrollTop : 0;
|
||||
|
||||
const emptyStateHtml = '<div style="padding: 20px; text-align: center; color: var(--text-muted); font-size: 0.875rem;">暂无历史对话</div>';
|
||||
listContainer.innerHTML = '';
|
||||
|
||||
// 如果响应不是200,显示空状态(友好处理,不显示错误)
|
||||
if (!response.ok) {
|
||||
listContainer.innerHTML = emptyStateHtml;
|
||||
return;
|
||||
}
|
||||
|
||||
const conversations = await response.json();
|
||||
|
||||
if (!Array.isArray(conversations) || conversations.length === 0) {
|
||||
listContainer.innerHTML = emptyStateHtml;
|
||||
return;
|
||||
@@ -1531,8 +1584,22 @@ async function loadConversations(searchQuery = '') {
|
||||
|
||||
listContainer.appendChild(fragment);
|
||||
updateActiveConversation();
|
||||
|
||||
// 恢复滚动位置
|
||||
if (sidebarContent) {
|
||||
// 使用 requestAnimationFrame 确保 DOM 已经更新
|
||||
requestAnimationFrame(() => {
|
||||
sidebarContent.scrollTop = savedScrollTop;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载对话列表失败:', error);
|
||||
// 错误时显示空状态,而不是错误提示(更友好的用户体验)
|
||||
const listContainer = document.getElementById('conversations-list');
|
||||
if (listContainer) {
|
||||
const emptyStateHtml = '<div style="padding: 20px; text-align: center; color: var(--text-muted); font-size: 0.875rem;">暂无历史对话</div>';
|
||||
listContainer.innerHTML = emptyStateHtml;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4056,16 +4123,27 @@ async function loadConversationsWithGroups(searchQuery = '') {
|
||||
url += '&search=' + encodeURIComponent(searchQuery.trim());
|
||||
}
|
||||
const response = await apiFetch(url);
|
||||
const conversations = await response.json();
|
||||
|
||||
const listContainer = document.getElementById('conversations-list');
|
||||
if (!listContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存滚动位置
|
||||
const sidebarContent = listContainer.closest('.sidebar-content');
|
||||
const savedScrollTop = sidebarContent ? sidebarContent.scrollTop : 0;
|
||||
|
||||
const emptyStateHtml = '<div style="padding: 20px; text-align: center; color: var(--text-muted); font-size: 0.875rem;">暂无历史对话</div>';
|
||||
listContainer.innerHTML = '';
|
||||
|
||||
// 如果响应不是200,显示空状态(友好处理,不显示错误)
|
||||
if (!response.ok) {
|
||||
listContainer.innerHTML = emptyStateHtml;
|
||||
return;
|
||||
}
|
||||
|
||||
const conversations = await response.json();
|
||||
|
||||
if (!Array.isArray(conversations) || conversations.length === 0) {
|
||||
listContainer.innerHTML = emptyStateHtml;
|
||||
return;
|
||||
@@ -4134,8 +4212,22 @@ async function loadConversationsWithGroups(searchQuery = '') {
|
||||
|
||||
listContainer.appendChild(fragment);
|
||||
updateActiveConversation();
|
||||
|
||||
// 恢复滚动位置
|
||||
if (sidebarContent) {
|
||||
// 使用 requestAnimationFrame 确保 DOM 已经更新
|
||||
requestAnimationFrame(() => {
|
||||
sidebarContent.scrollTop = savedScrollTop;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载对话列表失败:', error);
|
||||
// 错误时显示空状态,而不是错误提示(更友好的用户体验)
|
||||
const listContainer = document.getElementById('conversations-list');
|
||||
if (listContainer) {
|
||||
const emptyStateHtml = '<div style="padding: 20px; text-align: center; color: var(--text-muted); font-size: 0.875rem;">暂无历史对话</div>';
|
||||
listContainer.innerHTML = emptyStateHtml;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4988,7 +5080,14 @@ let allConversationsForBatch = [];
|
||||
async function showBatchManageModal() {
|
||||
try {
|
||||
const response = await apiFetch('/api/conversations?limit=1000');
|
||||
allConversationsForBatch = await response.json();
|
||||
|
||||
// 如果响应不是200,使用空数组(友好处理,不显示错误)
|
||||
if (!response.ok) {
|
||||
allConversationsForBatch = [];
|
||||
} else {
|
||||
const data = await response.json();
|
||||
allConversationsForBatch = Array.isArray(data) ? data : [];
|
||||
}
|
||||
|
||||
const modal = document.getElementById('batch-manage-modal');
|
||||
const countEl = document.getElementById('batch-manage-count');
|
||||
@@ -5002,7 +5101,17 @@ async function showBatchManageModal() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载对话列表失败:', error);
|
||||
alert('加载对话列表失败');
|
||||
// 错误时使用空数组,不显示错误提示(更友好的用户体验)
|
||||
allConversationsForBatch = [];
|
||||
const modal = document.getElementById('batch-manage-modal');
|
||||
const countEl = document.getElementById('batch-manage-count');
|
||||
if (countEl) {
|
||||
countEl.textContent = 0;
|
||||
}
|
||||
if (modal) {
|
||||
renderBatchConversations();
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+162
-18
@@ -428,8 +428,9 @@ async function loadRoleTools(page = 1, searchKeyword = '') {
|
||||
});
|
||||
} else {
|
||||
// 工具已在映射中(可能是预先设置的选中工具或用户手动选择的),保留映射中的状态
|
||||
// 但如果使用所有工具,且工具在MCP管理中已启用,确保标记为选中
|
||||
// 注意:即使使用所有工具,也不要强制覆盖用户已取消的工具选择
|
||||
const state = roleToolStateMap.get(toolKey);
|
||||
// 如果使用所有工具,且工具在MCP管理中已启用,确保标记为选中
|
||||
if (roleUsesAllTools && tool.enabled) {
|
||||
// 使用所有工具时,确保所有已启用的工具都被选中
|
||||
state.enabled = true;
|
||||
@@ -670,11 +671,13 @@ function updateRoleToolsStats() {
|
||||
if (roleUsesAllTools) {
|
||||
// 使用从API响应中获取的已启用工具总数
|
||||
const totalEnabled = totalEnabledToolsInMCP || 0;
|
||||
// 当前页分母应该是当前页已启用的工具数,而不是所有工具数
|
||||
const currentPageDenominator = currentPageEnabledInMCP > 0 ? currentPageEnabledInMCP : document.querySelectorAll('#role-tools-list input[type="checkbox"]').length;
|
||||
// 当前页分母应该是当前页的总工具数(每页20个),而不是当前页已启用的工具数
|
||||
const currentPageTotal = document.querySelectorAll('#role-tools-list input[type="checkbox"]').length;
|
||||
// 总工具数(所有工具,包括已启用和未启用的)
|
||||
const totalTools = roleToolsPagination.total || 0;
|
||||
statsEl.innerHTML = `
|
||||
<span title="当前页选中的工具数">✅ 当前页已选中: <strong>${currentPageEnabled}</strong> / ${currentPageDenominator}</span>
|
||||
<span title="所有已启用工具中选中的工具总数(基于MCP管理)">📊 总计已选中: <strong>${totalEnabled}</strong> / ${totalEnabled} <em>(使用所有已启用工具)</em></span>
|
||||
<span title="当前页选中的工具数">✅ 当前页已选中: <strong>${currentPageEnabled}</strong> / ${currentPageTotal}</span>
|
||||
<span title="所有已启用工具中选中的工具总数(基于MCP管理)">📊 总计已选中: <strong>${totalEnabled}</strong> / ${totalTools} <em>(使用所有已启用工具)</em></span>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
@@ -719,12 +722,14 @@ function updateRoleToolsStats() {
|
||||
});
|
||||
}
|
||||
|
||||
// 当前页分母应该是当前页已启用的工具数,而不是所有工具数
|
||||
const currentPageDenominator = currentPageEnabledInMCP > 0 ? currentPageEnabledInMCP : document.querySelectorAll('#role-tools-list input[type="checkbox"]').length;
|
||||
// 当前页分母应该是当前页的总工具数(每页20个),而不是当前页已启用的工具数
|
||||
const currentPageTotal = document.querySelectorAll('#role-tools-list input[type="checkbox"]').length;
|
||||
// 总工具数(所有工具,包括已启用和未启用的)
|
||||
const totalTools = roleToolsPagination.total || 0;
|
||||
|
||||
statsEl.innerHTML = `
|
||||
<span title="当前页选中的工具数(只统计已启用的工具)">✅ 当前页已选中: <strong>${currentPageEnabled}</strong> / ${currentPageDenominator}</span>
|
||||
<span title="角色已关联的工具总数(基于角色实际配置)">📊 总计已选中: <strong>${totalSelected}</strong> / ${totalEnabledForRole}</span>
|
||||
<span title="当前页选中的工具数(只统计已启用的工具)">✅ 当前页已选中: <strong>${currentPageEnabled}</strong> / ${currentPageTotal}</span>
|
||||
<span title="角色已关联的工具总数(基于角色实际配置)">📊 总计已选中: <strong>${totalSelected}</strong> / ${totalTools}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -823,6 +828,11 @@ async function showAddRoleModal() {
|
||||
if (clearBtn) {
|
||||
clearBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
// 清空工具列表 DOM,避免 loadRoleTools 中的 saveCurrentRolePageToolStates 读取旧状态
|
||||
if (toolsList) {
|
||||
toolsList.innerHTML = '';
|
||||
}
|
||||
|
||||
// 加载并渲染工具列表
|
||||
await loadRoleTools(1, '');
|
||||
@@ -831,6 +841,9 @@ async function showAddRoleModal() {
|
||||
if (toolsList) {
|
||||
toolsList.style.display = 'block';
|
||||
}
|
||||
|
||||
// 确保统计信息正确更新(显示0/108)
|
||||
updateRoleToolsStats();
|
||||
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
@@ -965,11 +978,13 @@ async function editRole(roleName) {
|
||||
if (toolKey) {
|
||||
const state = roleToolStateMap.get(toolKey);
|
||||
// 只选中在MCP管理中已启用的工具
|
||||
const shouldEnable = state && state.mcpEnabled !== false;
|
||||
// 如果状态存在,使用状态中的 mcpEnabled;否则假设已启用(因为 loadRoleTools 应该已经初始化了所有工具)
|
||||
const shouldEnable = state ? (state.mcpEnabled !== false) : true;
|
||||
checkbox.checked = shouldEnable;
|
||||
if (state) {
|
||||
state.enabled = shouldEnable;
|
||||
} else {
|
||||
// 如果状态不存在,创建新状态(这种情况不应该发生,因为 loadRoleTools 应该已经初始化了)
|
||||
roleToolStateMap.set(toolKey, {
|
||||
enabled: shouldEnable,
|
||||
is_external: isExternal,
|
||||
@@ -981,6 +996,8 @@ async function editRole(roleName) {
|
||||
}
|
||||
}
|
||||
});
|
||||
// 更新统计信息,确保显示正确的选中数量
|
||||
updateRoleToolsStats();
|
||||
} else if (selectedTools.length > 0) {
|
||||
// 加载完成后,再次设置选中状态(确保当前页的工具也被正确设置)
|
||||
setSelectedRoleTools(selectedTools);
|
||||
@@ -1027,6 +1044,68 @@ function getDisabledTools(selectedTools) {
|
||||
});
|
||||
}
|
||||
|
||||
// 加载所有工具到状态映射中(用于从使用全部工具切换到部分工具时)
|
||||
async function loadAllToolsToStateMap() {
|
||||
try {
|
||||
const pageSize = 100; // 使用较大的页面大小以减少请求次数
|
||||
let page = 1;
|
||||
let hasMore = true;
|
||||
|
||||
// 遍历所有页面获取所有工具
|
||||
while (hasMore) {
|
||||
const url = `/api/config/tools?page=${page}&page_size=${pageSize}`;
|
||||
const response = await apiFetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error('获取工具列表失败');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// 将所有工具添加到状态映射中
|
||||
result.tools.forEach(tool => {
|
||||
const toolKey = getToolKey(tool);
|
||||
if (!roleToolStateMap.has(toolKey)) {
|
||||
// 工具不在映射中,根据当前模式初始化
|
||||
let enabled = false;
|
||||
if (roleUsesAllTools) {
|
||||
// 如果使用所有工具,且工具在MCP管理中已启用,则标记为选中
|
||||
enabled = tool.enabled ? true : false;
|
||||
} else {
|
||||
// 如果不使用所有工具,只有工具在角色配置的工具列表中才标记为选中
|
||||
enabled = roleConfiguredTools.has(toolKey);
|
||||
}
|
||||
roleToolStateMap.set(toolKey, {
|
||||
enabled: enabled,
|
||||
is_external: tool.is_external || false,
|
||||
external_mcp: tool.external_mcp || '',
|
||||
name: tool.name,
|
||||
mcpEnabled: tool.enabled // 保存MCP管理中的原始启用状态
|
||||
});
|
||||
} else {
|
||||
// 工具已在映射中,更新其他属性但保留enabled状态
|
||||
const state = roleToolStateMap.get(toolKey);
|
||||
state.is_external = tool.is_external || false;
|
||||
state.external_mcp = tool.external_mcp || '';
|
||||
state.mcpEnabled = tool.enabled; // 更新MCP管理中的原始启用状态
|
||||
if (!state.name || state.name === toolKey.split('::').pop()) {
|
||||
state.name = tool.name; // 更新工具名称
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 检查是否还有更多页面
|
||||
if (page >= result.total_pages) {
|
||||
hasMore = false;
|
||||
} else {
|
||||
page++;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载所有工具到状态映射失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存角色
|
||||
async function saveRole() {
|
||||
const name = document.getElementById('role-name').value.trim();
|
||||
@@ -1049,22 +1128,86 @@ async function saveRole() {
|
||||
const userPrompt = document.getElementById('role-user-prompt').value.trim();
|
||||
const enabled = document.getElementById('role-enabled').checked;
|
||||
|
||||
const isEdit = document.getElementById('role-name').disabled;
|
||||
|
||||
// 检查是否为默认角色
|
||||
const isDefaultRole = name === '默认';
|
||||
|
||||
// 检查是否是首次添加角色(排除默认角色后,没有任何用户创建的角色)
|
||||
const isFirstUserRole = !isEdit && !isDefaultRole && roles.filter(r => r.name !== '默认').length === 0;
|
||||
|
||||
// 默认角色不保存tools字段(使用所有工具)
|
||||
// 非默认角色:如果使用所有工具(roleUsesAllTools为true),也不保存tools字段
|
||||
let tools = [];
|
||||
let disabledTools = []; // 存储未在MCP管理中启用的工具
|
||||
|
||||
if (!isDefaultRole && !roleUsesAllTools) {
|
||||
if (!isDefaultRole) {
|
||||
// 保存当前页的状态
|
||||
saveCurrentRolePageToolStates();
|
||||
|
||||
// 收集所有选中的工具(包括未在MCP管理中启用的)
|
||||
const allSelectedTools = getAllSelectedRoleTools();
|
||||
let allSelectedTools = getAllSelectedRoleTools();
|
||||
|
||||
// 检查哪些工具未在MCP管理中启用
|
||||
// 如果是首次添加角色且没有选择工具,默认使用全部工具
|
||||
if (isFirstUserRole && allSelectedTools.length === 0) {
|
||||
roleUsesAllTools = true;
|
||||
showNotification('检测到这是首次添加角色且未选择工具,将默认使用全部工具', 'info');
|
||||
} else if (roleUsesAllTools) {
|
||||
// 如果当前使用所有工具,需要检查用户是否取消了一些工具
|
||||
// 检查状态映射中是否有未选中的已启用工具
|
||||
let hasUnselectedTools = false;
|
||||
roleToolStateMap.forEach((state) => {
|
||||
// 如果工具在MCP管理中已启用但未选中,说明用户取消了该工具
|
||||
if (state.mcpEnabled !== false && !state.enabled) {
|
||||
hasUnselectedTools = true;
|
||||
}
|
||||
});
|
||||
|
||||
// 如果用户取消了一些已启用的工具,切换到部分工具模式
|
||||
if (hasUnselectedTools) {
|
||||
// 在切换之前,需要加载所有工具到状态映射中
|
||||
// 这样我们可以正确保存所有工具的状态(除了用户取消的那些)
|
||||
await loadAllToolsToStateMap();
|
||||
|
||||
// 将所有已启用的工具标记为选中(除了用户已取消的那些)
|
||||
// 用户已取消的工具在状态映射中enabled为false,保持不变
|
||||
roleToolStateMap.forEach((state, toolKey) => {
|
||||
// 如果工具在MCP管理中已启用,且状态映射中没有明确标记为未选中(即enabled不是false)
|
||||
// 则标记为选中
|
||||
if (state.mcpEnabled !== false && state.enabled !== false) {
|
||||
state.enabled = true;
|
||||
}
|
||||
});
|
||||
|
||||
roleUsesAllTools = false;
|
||||
} else {
|
||||
// 即使使用所有工具,也需要加载所有工具到状态映射中,以便检查是否有未启用的工具被选中
|
||||
// 这样可以检测用户是否手动选择了一些未启用的工具
|
||||
await loadAllToolsToStateMap();
|
||||
|
||||
// 检查是否有未启用的工具被手动选中(enabled为true但mcpEnabled为false)
|
||||
let hasDisabledToolsSelected = false;
|
||||
roleToolStateMap.forEach((state) => {
|
||||
if (state.enabled && state.mcpEnabled === false) {
|
||||
hasDisabledToolsSelected = true;
|
||||
}
|
||||
});
|
||||
|
||||
// 如果没有未启用的工具被选中,将所有已启用的工具标记为选中(这是使用所有工具的默认行为)
|
||||
if (!hasDisabledToolsSelected) {
|
||||
roleToolStateMap.forEach((state) => {
|
||||
if (state.mcpEnabled !== false) {
|
||||
state.enabled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新 allSelectedTools,因为现在状态映射中包含了所有工具
|
||||
allSelectedTools = getAllSelectedRoleTools();
|
||||
}
|
||||
}
|
||||
|
||||
// 检查哪些工具未在MCP管理中启用(无论是否使用所有工具都要检查)
|
||||
disabledTools = getDisabledTools(allSelectedTools);
|
||||
|
||||
// 如果有未启用的工具,提示用户
|
||||
@@ -1077,8 +1220,11 @@ async function saveRole() {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取选中的工具列表(只包含在MCP管理中已启用的工具)
|
||||
tools = await getSelectedRoleTools();
|
||||
// 如果使用所有工具,不需要获取工具列表
|
||||
if (!roleUsesAllTools) {
|
||||
// 获取选中的工具列表(只包含在MCP管理中已启用的工具)
|
||||
tools = await getSelectedRoleTools();
|
||||
}
|
||||
}
|
||||
|
||||
const roleData = {
|
||||
@@ -1089,8 +1235,6 @@ async function saveRole() {
|
||||
tools: tools, // 默认角色为空数组,表示使用所有工具
|
||||
enabled: enabled
|
||||
};
|
||||
|
||||
const isEdit = document.getElementById('role-name').disabled;
|
||||
const url = isEdit ? `/api/roles/${encodeURIComponent(name)}` : '/api/roles';
|
||||
const method = isEdit ? 'PUT' : 'POST';
|
||||
|
||||
@@ -1116,7 +1260,7 @@ async function saveRole() {
|
||||
toolNames = toolNames.substring(0, 100) + '...';
|
||||
}
|
||||
showNotification(
|
||||
`${isEdit ? '角色已更新' : '角色已创建'},但已过滤 ${disabledTools.length} 个未在MCP管理中启用的工具。请先在"MCP管理"中启用这些工具,然后再在角色中配置。`,
|
||||
`${isEdit ? '角色已更新' : '角色已创建'},但已过滤 ${disabledTools.length} 个未在MCP管理中启用的工具:${toolNames}。请先在"MCP管理"中启用这些工具,然后再在角色中配置。`,
|
||||
'warning'
|
||||
);
|
||||
} else {
|
||||
|
||||
+134
-5
@@ -716,23 +716,56 @@ const batchQueuesState = {
|
||||
totalPages: 1
|
||||
};
|
||||
|
||||
// 显示批量导入模态框
|
||||
function showBatchImportModal() {
|
||||
// 显示新建任务模态框
|
||||
async function showBatchImportModal() {
|
||||
const modal = document.getElementById('batch-import-modal');
|
||||
const input = document.getElementById('batch-tasks-input');
|
||||
const titleInput = document.getElementById('batch-queue-title');
|
||||
const roleSelect = document.getElementById('batch-queue-role');
|
||||
if (modal && input) {
|
||||
input.value = '';
|
||||
if (titleInput) {
|
||||
titleInput.value = '';
|
||||
}
|
||||
// 重置角色选择为默认
|
||||
if (roleSelect) {
|
||||
roleSelect.value = '';
|
||||
}
|
||||
updateBatchImportStats('');
|
||||
|
||||
// 加载并填充角色列表
|
||||
if (roleSelect && typeof loadRoles === 'function') {
|
||||
try {
|
||||
const loadedRoles = await loadRoles();
|
||||
// 清空现有选项(除了默认选项)
|
||||
roleSelect.innerHTML = '<option value="">默认</option>';
|
||||
|
||||
// 添加已启用的角色
|
||||
const sortedRoles = loadedRoles.sort((a, b) => {
|
||||
if (a.name === '默认') return -1;
|
||||
if (b.name === '默认') return 1;
|
||||
return (a.name || '').localeCompare(b.name || '', 'zh-CN');
|
||||
});
|
||||
|
||||
sortedRoles.forEach(role => {
|
||||
if (role.name !== '默认' && role.enabled !== false) {
|
||||
const option = document.createElement('option');
|
||||
option.value = role.name;
|
||||
option.textContent = role.name;
|
||||
roleSelect.appendChild(option);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('加载角色列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
modal.style.display = 'block';
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭批量导入模态框
|
||||
// 关闭新建任务模态框
|
||||
function closeBatchImportModal() {
|
||||
const modal = document.getElementById('batch-import-modal');
|
||||
if (modal) {
|
||||
@@ -740,7 +773,7 @@ function closeBatchImportModal() {
|
||||
}
|
||||
}
|
||||
|
||||
// 更新批量导入统计
|
||||
// 更新新建任务统计
|
||||
function updateBatchImportStats(text) {
|
||||
const statsEl = document.getElementById('batch-import-stats');
|
||||
if (!statsEl) return;
|
||||
@@ -770,6 +803,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
async function createBatchQueue() {
|
||||
const input = document.getElementById('batch-tasks-input');
|
||||
const titleInput = document.getElementById('batch-queue-title');
|
||||
const roleSelect = document.getElementById('batch-queue-role');
|
||||
if (!input) return;
|
||||
|
||||
const text = input.value.trim();
|
||||
@@ -788,13 +822,16 @@ async function createBatchQueue() {
|
||||
// 获取标题(可选)
|
||||
const title = titleInput ? titleInput.value.trim() : '';
|
||||
|
||||
// 获取角色(可选,空字符串表示默认角色)
|
||||
const role = roleSelect ? roleSelect.value || '' : '';
|
||||
|
||||
try {
|
||||
const response = await apiFetch('/api/batch-tasks', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ title, tasks }),
|
||||
body: JSON.stringify({ title, tasks, role }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -816,6 +853,34 @@ async function createBatchQueue() {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取角色图标(辅助函数)
|
||||
function getRoleIconForDisplay(roleName, rolesList) {
|
||||
if (!roleName || roleName === '') {
|
||||
return '🔵'; // 默认角色图标
|
||||
}
|
||||
|
||||
if (Array.isArray(rolesList) && rolesList.length > 0) {
|
||||
const role = rolesList.find(r => r.name === roleName);
|
||||
if (role && role.icon) {
|
||||
let icon = role.icon;
|
||||
// 检查是否是 Unicode 转义格式(可能包含引号)
|
||||
const unicodeMatch = icon.match(/^"?\\U([0-9A-F]{8})"?$/i);
|
||||
if (unicodeMatch) {
|
||||
try {
|
||||
const codePoint = parseInt(unicodeMatch[1], 16);
|
||||
icon = String.fromCodePoint(codePoint);
|
||||
} catch (e) {
|
||||
// 转换失败,使用默认图标
|
||||
console.warn('转换 icon Unicode 转义失败:', icon, e);
|
||||
return '👤';
|
||||
}
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
return '👤'; // 默认图标
|
||||
}
|
||||
|
||||
// 加载批量任务队列列表
|
||||
async function loadBatchQueues(page) {
|
||||
const section = document.getElementById('batch-queues-section');
|
||||
@@ -826,6 +891,17 @@ async function loadBatchQueues(page) {
|
||||
batchQueuesState.currentPage = page;
|
||||
}
|
||||
|
||||
// 加载角色列表(用于显示正确的角色图标)
|
||||
let loadedRoles = [];
|
||||
if (typeof loadRoles === 'function') {
|
||||
try {
|
||||
loadedRoles = await loadRoles();
|
||||
} catch (error) {
|
||||
console.warn('加载角色列表失败,将使用默认图标:', error);
|
||||
}
|
||||
}
|
||||
batchQueuesState.loadedRoles = loadedRoles; // 保存到状态中供渲染使用
|
||||
|
||||
// 构建查询参数
|
||||
const params = new URLSearchParams();
|
||||
params.append('page', batchQueuesState.currentPage.toString());
|
||||
@@ -933,11 +1009,18 @@ function renderBatchQueues() {
|
||||
|
||||
const titleDisplay = queue.title ? `<span class="batch-queue-title" style="font-weight: 600; color: var(--text-primary); margin-right: 8px;">${escapeHtml(queue.title)}</span>` : '';
|
||||
|
||||
// 显示角色信息(使用正确的角色图标)
|
||||
const loadedRoles = batchQueuesState.loadedRoles || [];
|
||||
const roleIcon = getRoleIconForDisplay(queue.role, loadedRoles);
|
||||
const roleName = queue.role && queue.role !== '' ? queue.role : '默认';
|
||||
const roleDisplay = `<span class="batch-queue-role" style="margin-right: 8px;" title="角色: ${escapeHtml(roleName)}">${roleIcon} ${escapeHtml(roleName)}</span>`;
|
||||
|
||||
return `
|
||||
<div class="batch-queue-item" data-queue-id="${queue.id}" onclick="showBatchQueueDetail('${queue.id}')">
|
||||
<div class="batch-queue-header">
|
||||
<div class="batch-queue-info" style="flex: 1;">
|
||||
${titleDisplay}
|
||||
${roleDisplay}
|
||||
<span class="batch-queue-status ${status.class}">${status.text}</span>
|
||||
<span class="batch-queue-id">队列ID: ${escapeHtml(queue.id)}</span>
|
||||
<span class="batch-queue-time">创建时间: ${new Date(queue.createdAt).toLocaleString('zh-CN')}</span>
|
||||
@@ -1110,6 +1193,16 @@ async function showBatchQueueDetail(queueId) {
|
||||
if (!modal || !content) return;
|
||||
|
||||
try {
|
||||
// 加载角色列表(如果还未加载)
|
||||
let loadedRoles = [];
|
||||
if (typeof loadRoles === 'function') {
|
||||
try {
|
||||
loadedRoles = await loadRoles();
|
||||
} catch (error) {
|
||||
console.warn('加载角色列表失败,将使用默认图标:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await apiFetch(`/api/batch-tasks/${queueId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('获取队列详情失败');
|
||||
@@ -1164,12 +1257,48 @@ async function showBatchQueueDetail(queueId) {
|
||||
'cancelled': { text: '已取消', class: 'batch-task-status-cancelled' }
|
||||
};
|
||||
|
||||
// 获取角色信息(如果队列有角色配置)
|
||||
let roleDisplay = '';
|
||||
if (queue.role && queue.role !== '') {
|
||||
// 如果有角色配置,尝试获取角色详细信息
|
||||
let roleName = queue.role;
|
||||
let roleIcon = '👤';
|
||||
// 从已加载的角色列表中查找角色图标
|
||||
if (Array.isArray(loadedRoles) && loadedRoles.length > 0) {
|
||||
const role = loadedRoles.find(r => r.name === roleName);
|
||||
if (role && role.icon) {
|
||||
let icon = role.icon;
|
||||
const unicodeMatch = icon.match(/^"?\\U([0-9A-F]{8})"?$/i);
|
||||
if (unicodeMatch) {
|
||||
try {
|
||||
const codePoint = parseInt(unicodeMatch[1], 16);
|
||||
icon = String.fromCodePoint(codePoint);
|
||||
} catch (e) {
|
||||
// 转换失败,使用默认图标
|
||||
}
|
||||
}
|
||||
roleIcon = icon;
|
||||
}
|
||||
}
|
||||
roleDisplay = `<div class="detail-item">
|
||||
<span class="detail-label">角色</span>
|
||||
<span class="detail-value">${roleIcon} ${escapeHtml(roleName)}</span>
|
||||
</div>`;
|
||||
} else {
|
||||
// 默认角色
|
||||
roleDisplay = `<div class="detail-item">
|
||||
<span class="detail-label">角色</span>
|
||||
<span class="detail-value">🔵 默认</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="batch-queue-detail-info">
|
||||
${queue.title ? `<div class="detail-item">
|
||||
<span class="detail-label">任务标题</span>
|
||||
<span class="detail-value">${escapeHtml(queue.title)}</span>
|
||||
</div>` : ''}
|
||||
${roleDisplay}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">队列ID</span>
|
||||
<span class="detail-value"><code>${escapeHtml(queue.id)}</code></span>
|
||||
|
||||
+33
-25
@@ -70,36 +70,34 @@
|
||||
<nav class="main-sidebar-nav">
|
||||
<div class="nav-item" data-page="chat">
|
||||
<div class="nav-item-content" data-title="对话" onclick="switchPage('chat')">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
|
||||
</svg>
|
||||
<span>对话</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-item" data-page="tasks">
|
||||
<div class="nav-item-content" data-title="任务管理" onclick="switchPage('tasks')">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<polyline points="13 2 13 9 20 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="9" y1="13" x2="15" y2="13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="9" y1="17" x2="15" y2="17" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M9 11l3 3L22 4"></path>
|
||||
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
|
||||
</svg>
|
||||
<span>任务管理</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-item" data-page="vulnerabilities">
|
||||
<div class="nav-item-content" data-title="漏洞管理" onclick="switchPage('vulnerabilities')">
|
||||
<svg width="20" height="20" 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 width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
|
||||
<path d="M9 12l2 2 4-4"></path>
|
||||
</svg>
|
||||
<span>漏洞管理</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-item nav-item-has-submenu" data-page="mcp">
|
||||
<div class="nav-item-content" data-title="MCP" onclick="toggleSubmenu('mcp')">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 12h4l3 8 4-16 3 8h4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"></path>
|
||||
</svg>
|
||||
<span>MCP</span>
|
||||
<svg class="submenu-arrow" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -117,10 +115,9 @@
|
||||
</div>
|
||||
<div class="nav-item nav-item-has-submenu" data-page="knowledge">
|
||||
<div class="nav-item-content" data-title="知识" onclick="toggleSubmenu('knowledge')">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10 7h6M10 11h6M10 15h4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path>
|
||||
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>
|
||||
</svg>
|
||||
<span>知识</span>
|
||||
<svg class="submenu-arrow" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -138,9 +135,11 @@
|
||||
</div>
|
||||
<div class="nav-item nav-item-has-submenu" data-page="roles">
|
||||
<div class="nav-item-content" data-title="角色" onclick="toggleSubmenu('roles')">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="12" cy="7" r="4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="9" cy="7" r="4"></circle>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
||||
</svg>
|
||||
<span>角色</span>
|
||||
<svg class="submenu-arrow" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -155,9 +154,9 @@
|
||||
</div>
|
||||
<div class="nav-item" data-page="settings">
|
||||
<div class="nav-item-content" data-title="系统设置" onclick="switchPage('settings')">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
||||
</svg>
|
||||
<span>系统设置</span>
|
||||
</div>
|
||||
@@ -611,7 +610,7 @@
|
||||
<div class="page-header">
|
||||
<h2>任务管理</h2>
|
||||
<div class="page-header-actions">
|
||||
<button class="btn-primary" onclick="showBatchImportModal()">批量导入任务</button>
|
||||
<button class="btn-primary" onclick="showBatchImportModal()">新建任务</button>
|
||||
<label class="auto-refresh-toggle">
|
||||
<input type="checkbox" id="tasks-auto-refresh" checked onchange="toggleTasksAutoRefresh(this.checked)">
|
||||
<span>自动刷新</span>
|
||||
@@ -1238,11 +1237,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 批量导入任务模态框 -->
|
||||
<!-- 新建任务模态框 -->
|
||||
<div id="batch-import-modal" class="modal">
|
||||
<div class="modal-content" style="max-width: 800px;">
|
||||
<div class="modal-header">
|
||||
<h2>批量导入任务</h2>
|
||||
<h2>新建任务</h2>
|
||||
<span class="modal-close" onclick="closeBatchImportModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@@ -1253,6 +1252,15 @@
|
||||
为批量任务队列设置一个标题,方便后续查找和管理。
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="batch-queue-role">角色</label>
|
||||
<select id="batch-queue-role" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 0.875rem;">
|
||||
<option value="">默认</option>
|
||||
</select>
|
||||
<div class="form-hint" style="margin-top: 4px;">
|
||||
选择一个角色,所有任务将使用该角色的配置(提示词和工具)执行。
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="batch-tasks-input">任务列表(每行一个任务)<span style="color: red;">*</span></label>
|
||||
<textarea id="batch-tasks-input" rows="15" placeholder="请输入任务列表,每行一个任务,例如: 扫描 192.168.1.1 的开放端口 检查 https://example.com 是否存在SQL注入 枚举 example.com 的子域名" style="font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.875rem; line-height: 1.5;"></textarea>
|
||||
|
||||
Reference in New Issue
Block a user