mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-08-28 21:50:43 +02:00
Remove conversation grouping feature
This commit is contained in:
@@ -132,7 +132,7 @@ CyberStrikeAI connects planning, execution, human oversight, evidence, and repla
|
||||
|
||||
### Security operations
|
||||
|
||||
- 📁 **Conversation management** provides grouping, pinning, renaming, and batch organization.
|
||||
- 📁 **Conversation management** provides pinning, renaming, and batch organization.
|
||||
- 📂 **Projects and attack chains** connect cross-session facts, risk scoring, graph views, and step-by-step replay.
|
||||
- 🗂️ **Asset management** normalizes and deduplicates domains, IP addresses, ports, and services; supports XLSX/CSV import and export, advanced filters and saved views, ownership and business metadata, cross-page bulk maintenance, and duplicate merging; and tracks scan coverage, linked vulnerabilities, and risk state. See the [Asset Management guide](docs/en-US/asset-management.md).
|
||||
- 🛡️ **Vulnerability management** provides severity classification, lifecycle tracking, filtering, and statistics.
|
||||
|
||||
@@ -89,7 +89,6 @@ Permissions use `module:action`. Common actions are `read`, `write`, `delete`, a
|
||||
| Attack chain | `attackchain:read`, `attackchain:write` |
|
||||
| Network-space search / Reconnaissance | `fofa:execute` |
|
||||
| OpenAPI | `openapi:read` |
|
||||
| Chat groups | `group:read`, `group:write`, `group:delete` |
|
||||
| Monitor | `monitor:read`, `monitor:write`, `monitor:delete` |
|
||||
|
||||
Important distinctions:
|
||||
|
||||
@@ -96,7 +96,6 @@ AI 测试角色不是安全授权边界。即使选择了“渗透测试”角
|
||||
| 攻击链 | `attackchain:read`、`attackchain:write` |
|
||||
| 网络空间测绘 / 信息收集 | `fofa:execute` |
|
||||
| OpenAPI | `openapi:read` |
|
||||
| 对话分组 | `group:read`、`group:write`、`group:delete` |
|
||||
| 执行监控 | `monitor:read`、`monitor:write`、`monitor:delete` |
|
||||
|
||||
特殊权限说明:
|
||||
|
||||
+1
-17
@@ -391,7 +391,6 @@ func New(cfg *config.Config, log *logger.Logger, configPath string) (*App, error
|
||||
monitorHandler.SetTaskManager(agentHandler.TaskManager())
|
||||
monitorHandler.SetAgentHandler(agentHandler)
|
||||
notificationHandler := handler.NewNotificationHandler(db, agentHandler, log.Logger)
|
||||
groupHandler := handler.NewGroupHandler(db, log.Logger)
|
||||
authHandler := handler.NewAuthHandler(authManager, cfg, configPath, log.Logger)
|
||||
authHandler.SetAudit(auditSvc)
|
||||
attackChainHandler := handler.NewAttackChainHandler(db, &cfg.OpenAI, log.Logger)
|
||||
@@ -567,7 +566,6 @@ func New(cfg *config.Config, log *logger.Logger, configPath string) (*App, error
|
||||
conversationHandler,
|
||||
robotHandler,
|
||||
wechatRobotHandler,
|
||||
groupHandler,
|
||||
configHandler,
|
||||
externalMCPHandler,
|
||||
attackChainHandler,
|
||||
@@ -870,7 +868,6 @@ func setupRoutes(
|
||||
conversationHandler *handler.ConversationHandler,
|
||||
robotHandler *handler.RobotHandler,
|
||||
wechatRobotHandler *handler.WechatRobotHandler,
|
||||
groupHandler *handler.GroupHandler,
|
||||
configHandler *handler.ConfigHandler,
|
||||
externalMCPHandler *handler.ExternalMCPHandler,
|
||||
attackChainHandler *handler.AttackChainHandler,
|
||||
@@ -1041,20 +1038,7 @@ func setupRoutes(
|
||||
protected.PUT("/conversations/:id/project", conversationHandler.SetConversationProject)
|
||||
protected.DELETE("/conversations/:id", conversationHandler.DeleteConversation)
|
||||
protected.POST("/conversations/:id/delete-turn", conversationHandler.DeleteConversationTurn)
|
||||
protected.PUT("/conversations/:id/pinned", groupHandler.UpdateConversationPinned)
|
||||
|
||||
// 对话分组
|
||||
protected.POST("/groups", groupHandler.CreateGroup)
|
||||
protected.GET("/groups", groupHandler.ListGroups)
|
||||
protected.GET("/groups/:id", groupHandler.GetGroup)
|
||||
protected.PUT("/groups/:id", groupHandler.UpdateGroup)
|
||||
protected.DELETE("/groups/:id", groupHandler.DeleteGroup)
|
||||
protected.PUT("/groups/:id/pinned", groupHandler.UpdateGroupPinned)
|
||||
protected.GET("/groups/:id/conversations", groupHandler.GetGroupConversations)
|
||||
protected.GET("/groups/mappings", groupHandler.GetAllMappings)
|
||||
protected.POST("/groups/conversations", groupHandler.AddConversationToGroup)
|
||||
protected.DELETE("/groups/:id/conversations/:conversationId", groupHandler.RemoveConversationFromGroup)
|
||||
protected.PUT("/groups/:id/conversations/:conversationId/pinned", groupHandler.UpdateConversationPinnedInGroup)
|
||||
protected.PUT("/conversations/:id/pinned", conversationHandler.UpdateConversationPinned)
|
||||
|
||||
// 监控
|
||||
protected.GET("/monitor", monitorHandler.Monitor)
|
||||
|
||||
@@ -665,81 +665,6 @@ func scanConversationRows(rows *sql.Rows) ([]*Conversation, error) {
|
||||
return conversations, rows.Err()
|
||||
}
|
||||
|
||||
const ungroupedConversationsSQL = `
|
||||
FROM conversations c
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM conversation_group_mappings cgm WHERE cgm.conversation_id = c.id
|
||||
)`
|
||||
|
||||
// CountUngroupedConversations 统计不在任何分组中的对话数量。
|
||||
func (db *DB) CountUngroupedConversations(projectID string) (int, error) {
|
||||
where := ungroupedConversationsSQL
|
||||
args := []interface{}{}
|
||||
where, args = appendConversationProjectFilter(where, args, projectID, "c")
|
||||
var count int
|
||||
if err := db.QueryRow(`SELECT COUNT(*) `+where, args...).Scan(&count); err != nil {
|
||||
return 0, fmt.Errorf("统计未分组对话失败: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (db *DB) CountUngroupedConversationsForAccess(projectID, userID, scope string) (int, error) {
|
||||
where := ungroupedConversationsSQL
|
||||
args := []interface{}{}
|
||||
where, args = appendConversationProjectFilter(where, args, projectID, "c")
|
||||
where, args = appendConversationAccessFilter(where, args, userID, scope, "c")
|
||||
var count int
|
||||
if err := db.QueryRow(`SELECT COUNT(*) `+where, args...).Scan(&count); err != nil {
|
||||
return 0, fmt.Errorf("统计未分组对话失败: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// ListUngroupedConversations 列出不在任何分组中的对话(最近对话侧栏)。
|
||||
func (db *DB) ListUngroupedConversations(limit, offset int, sortBy, projectID string) ([]*Conversation, error) {
|
||||
orderClause := conversationOrderClause(sortBy, "c")
|
||||
where := ungroupedConversationsSQL
|
||||
args := []interface{}{}
|
||||
where, args = appendConversationProjectFilter(where, args, projectID, "c")
|
||||
args = append(args, limit, offset)
|
||||
rows, err := db.Query(
|
||||
`SELECT c.id, c.title, COALESCE(c.pinned, 0), c.created_at, c.updated_at, c.project_id, c.role_name, c.agent_mode `+
|
||||
where+`
|
||||
`+orderClause+`
|
||||
LIMIT ? OFFSET ?`,
|
||||
args...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询未分组对话失败: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanConversationRows(rows)
|
||||
}
|
||||
|
||||
func (db *DB) ListUngroupedConversationsForAccess(limit, offset int, sortBy, projectID, userID, scope string) ([]*Conversation, error) {
|
||||
if scope == RBACScopeAll || strings.TrimSpace(userID) == "" {
|
||||
return db.ListUngroupedConversations(limit, offset, sortBy, projectID)
|
||||
}
|
||||
orderClause := conversationOrderClause(sortBy, "c")
|
||||
where := ungroupedConversationsSQL
|
||||
args := []interface{}{}
|
||||
where, args = appendConversationProjectFilter(where, args, projectID, "c")
|
||||
where, args = appendConversationAccessFilter(where, args, userID, scope, "c")
|
||||
args = append(args, limit, offset)
|
||||
rows, err := db.Query(
|
||||
`SELECT c.id, c.title, COALESCE(c.pinned, 0), c.created_at, c.updated_at, c.project_id, c.role_name, c.agent_mode `+
|
||||
where+`
|
||||
`+orderClause+`
|
||||
LIMIT ? OFFSET ?`,
|
||||
args...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询未分组对话失败: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanConversationRows(rows)
|
||||
}
|
||||
|
||||
// GetConversationTitle 获取对话标题(轻量查询,不加载消息)
|
||||
func (db *DB) GetConversationTitle(id string) (string, error) {
|
||||
var title string
|
||||
@@ -766,6 +691,22 @@ func (db *DB) UpdateConversationTitle(id, title string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateConversationPinned 更新对话置顶状态
|
||||
func (db *DB) UpdateConversationPinned(id string, pinned bool) error {
|
||||
pinnedValue := 0
|
||||
if pinned {
|
||||
pinnedValue = 1
|
||||
}
|
||||
_, err := db.Exec(
|
||||
"UPDATE conversations SET pinned = ?, updated_at = ? WHERE id = ?",
|
||||
pinnedValue, time.Now(), id,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新对话置顶状态失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateConversationTime 更新对话时间
|
||||
func (db *DB) UpdateConversationTime(id string) error {
|
||||
_, err := db.Exec(
|
||||
@@ -784,7 +725,6 @@ func (db *DB) UpdateConversationTime(id string) error {
|
||||
// - process_details(过程详情)
|
||||
// - attack_chain_nodes(攻击链节点)
|
||||
// - attack_chain_edges(攻击链边)
|
||||
// - conversation_group_mappings(分组映射)
|
||||
// 漏洞记录会保留:vulnerabilities.conversation_id 使用 ON DELETE SET NULL,仅解除与会话的关联。
|
||||
// 注意:knowledge_retrieval_logs 在删除前会被显式清理。
|
||||
func (db *DB) DeleteConversation(id string) error {
|
||||
|
||||
@@ -329,29 +329,6 @@ func (db *DB) initTables() error {
|
||||
FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE SET NULL
|
||||
);`
|
||||
|
||||
// 创建对话分组表
|
||||
createConversationGroupsTable := `
|
||||
CREATE TABLE IF NOT EXISTS conversation_groups (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
icon TEXT,
|
||||
owner_user_id TEXT,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL
|
||||
);`
|
||||
|
||||
// 创建对话分组映射表
|
||||
createConversationGroupMappingsTable := `
|
||||
CREATE TABLE IF NOT EXISTS conversation_group_mappings (
|
||||
id TEXT PRIMARY KEY,
|
||||
conversation_id TEXT NOT NULL,
|
||||
group_id TEXT NOT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (group_id) REFERENCES conversation_groups(id) ON DELETE CASCADE,
|
||||
UNIQUE(conversation_id, group_id)
|
||||
);`
|
||||
|
||||
// 机器人会话绑定表(用于跨重启保持「平台+租户+用户」到 conversation 的映射)
|
||||
createRobotUserSessionsTable := `
|
||||
CREATE TABLE IF NOT EXISTS robot_user_sessions (
|
||||
@@ -759,8 +736,6 @@ func (db *DB) initTables() error {
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledge_retrieval_logs_conversation ON knowledge_retrieval_logs(conversation_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledge_retrieval_logs_message ON knowledge_retrieval_logs(message_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledge_retrieval_logs_created_at ON knowledge_retrieval_logs(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_conversation_group_mappings_conversation ON conversation_group_mappings(conversation_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_conversation_group_mappings_group ON conversation_group_mappings(group_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_robot_user_sessions_updated_at ON robot_user_sessions(updated_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_conversations_pinned ON conversations(pinned);
|
||||
CREATE INDEX IF NOT EXISTS idx_vulnerabilities_conversation_id ON vulnerabilities(conversation_id);
|
||||
@@ -864,13 +839,6 @@ func (db *DB) initTables() error {
|
||||
return fmt.Errorf("创建knowledge_retrieval_logs表失败: %w", err)
|
||||
}
|
||||
|
||||
if _, err := db.Exec(createConversationGroupsTable); err != nil {
|
||||
return fmt.Errorf("创建conversation_groups表失败: %w", err)
|
||||
}
|
||||
|
||||
if _, err := db.Exec(createConversationGroupMappingsTable); err != nil {
|
||||
return fmt.Errorf("创建conversation_group_mappings表失败: %w", err)
|
||||
}
|
||||
if _, err := db.Exec(createRobotUserSessionsTable); err != nil {
|
||||
return fmt.Errorf("创建robot_user_sessions表失败: %w", err)
|
||||
}
|
||||
@@ -966,16 +934,6 @@ func (db *DB) initTables() error {
|
||||
// 不返回错误,允许继续运行
|
||||
}
|
||||
|
||||
if err := db.migrateConversationGroupsTable(); err != nil {
|
||||
db.logger.Warn("迁移conversation_groups表失败", zap.Error(err))
|
||||
// 不返回错误,允许继续运行
|
||||
}
|
||||
|
||||
if err := db.migrateConversationGroupMappingsTable(); err != nil {
|
||||
db.logger.Warn("迁移conversation_group_mappings表失败", zap.Error(err))
|
||||
// 不返回错误,允许继续运行
|
||||
}
|
||||
|
||||
if err := db.migrateBatchTaskQueuesTable(); err != nil {
|
||||
db.logger.Warn("迁移batch_task_queues表失败", zap.Error(err))
|
||||
// 不返回错误,允许继续运行
|
||||
@@ -1237,54 +1195,6 @@ func (db *DB) migrateConversationsTable() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateConversationGroupsTable 迁移conversation_groups表,添加新字段
|
||||
func (db *DB) migrateConversationGroupsTable() error {
|
||||
// 检查pinned字段是否存在
|
||||
var count int
|
||||
err := db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('conversation_groups') WHERE name='pinned'").Scan(&count)
|
||||
if err != nil {
|
||||
// 如果查询失败,尝试添加字段
|
||||
if _, addErr := db.Exec("ALTER TABLE conversation_groups ADD COLUMN pinned INTEGER DEFAULT 0"); addErr != nil {
|
||||
// 如果字段已存在,忽略错误
|
||||
errMsg := strings.ToLower(addErr.Error())
|
||||
if !strings.Contains(errMsg, "duplicate column") && !strings.Contains(errMsg, "already exists") {
|
||||
db.logger.Warn("添加pinned字段失败", zap.Error(addErr))
|
||||
}
|
||||
}
|
||||
} else if count == 0 {
|
||||
// 字段不存在,添加它
|
||||
if _, err := db.Exec("ALTER TABLE conversation_groups ADD COLUMN pinned INTEGER DEFAULT 0"); err != nil {
|
||||
db.logger.Warn("添加pinned字段失败", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateConversationGroupMappingsTable 迁移conversation_group_mappings表,添加新字段
|
||||
func (db *DB) migrateConversationGroupMappingsTable() error {
|
||||
// 检查pinned字段是否存在
|
||||
var count int
|
||||
err := db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('conversation_group_mappings') WHERE name='pinned'").Scan(&count)
|
||||
if err != nil {
|
||||
// 如果查询失败,尝试添加字段
|
||||
if _, addErr := db.Exec("ALTER TABLE conversation_group_mappings ADD COLUMN pinned INTEGER DEFAULT 0"); addErr != nil {
|
||||
// 如果字段已存在,忽略错误
|
||||
errMsg := strings.ToLower(addErr.Error())
|
||||
if !strings.Contains(errMsg, "duplicate column") && !strings.Contains(errMsg, "already exists") {
|
||||
db.logger.Warn("添加pinned字段失败", zap.Error(addErr))
|
||||
}
|
||||
}
|
||||
} else if count == 0 {
|
||||
// 字段不存在,添加它
|
||||
if _, err := db.Exec("ALTER TABLE conversation_group_mappings ADD COLUMN pinned INTEGER DEFAULT 0"); err != nil {
|
||||
db.logger.Warn("添加pinned字段失败", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateBatchTaskQueuesTable 迁移batch_task_queues表,补充新字段
|
||||
func (db *DB) migrateBatchTaskQueuesTable() error {
|
||||
// 检查title字段是否存在
|
||||
|
||||
@@ -1,486 +0,0 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ConversationGroup 对话分组
|
||||
type ConversationGroup struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
Pinned bool `json:"pinned"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
OwnerUserID string `json:"-"`
|
||||
}
|
||||
|
||||
// GroupExistsByName 检查分组名称是否已存在
|
||||
func (db *DB) GroupExistsByName(name string, excludeID string) (bool, error) {
|
||||
return db.groupExistsByNameForOwner(name, excludeID, "")
|
||||
}
|
||||
|
||||
func (db *DB) groupExistsByNameForOwner(name, excludeID, ownerUserID string) (bool, error) {
|
||||
var count int
|
||||
var err error
|
||||
if ownerUserID != "" && excludeID != "" {
|
||||
err = db.QueryRow("SELECT COUNT(*) FROM conversation_groups WHERE name = ? AND owner_user_id = ? AND id != ?", name, ownerUserID, excludeID).Scan(&count)
|
||||
} else if ownerUserID != "" {
|
||||
err = db.QueryRow("SELECT COUNT(*) FROM conversation_groups WHERE name = ? AND owner_user_id = ?", name, ownerUserID).Scan(&count)
|
||||
} else if excludeID != "" {
|
||||
err = db.QueryRow(
|
||||
"SELECT COUNT(*) FROM conversation_groups WHERE name = ? AND id != ?",
|
||||
name, excludeID,
|
||||
).Scan(&count)
|
||||
} else {
|
||||
err = db.QueryRow(
|
||||
"SELECT COUNT(*) FROM conversation_groups WHERE name = ?",
|
||||
name,
|
||||
).Scan(&count)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("检查分组名称失败: %w", err)
|
||||
}
|
||||
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// CreateGroup 创建分组
|
||||
func (db *DB) CreateGroup(name, icon string, owners ...string) (*ConversationGroup, error) {
|
||||
ownerUserID := ""
|
||||
if len(owners) > 0 {
|
||||
ownerUserID = owners[0]
|
||||
}
|
||||
// 检查名称是否已存在
|
||||
exists, err := db.groupExistsByNameForOwner(name, "", ownerUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exists {
|
||||
return nil, fmt.Errorf("分组名称已存在")
|
||||
}
|
||||
|
||||
id := uuid.New().String()
|
||||
now := time.Now()
|
||||
|
||||
if icon == "" {
|
||||
icon = "📁"
|
||||
}
|
||||
|
||||
_, err = db.Exec(
|
||||
"INSERT INTO conversation_groups (id, name, icon, pinned, owner_user_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
id, name, icon, 0, ownerUserID, now, now,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建分组失败: %w", err)
|
||||
}
|
||||
|
||||
return &ConversationGroup{
|
||||
ID: id,
|
||||
Name: name,
|
||||
Icon: icon,
|
||||
Pinned: false,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
OwnerUserID: ownerUserID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListGroups 列出所有分组
|
||||
func (db *DB) ListGroups() ([]*ConversationGroup, error) {
|
||||
return db.ListGroupsForAccess("", RBACScopeAll)
|
||||
}
|
||||
|
||||
func (db *DB) ListGroupsForAccess(userID, scope string) ([]*ConversationGroup, error) {
|
||||
query := "SELECT id, name, icon, COALESCE(pinned, 0), COALESCE(owner_user_id, ''), created_at, updated_at FROM conversation_groups"
|
||||
args := []interface{}{}
|
||||
if scope != RBACScopeAll {
|
||||
query += " WHERE owner_user_id = ?"
|
||||
args = append(args, userID)
|
||||
}
|
||||
query += " ORDER BY COALESCE(pinned, 0) DESC, created_at ASC"
|
||||
rows, err := db.Query(
|
||||
query, args...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询分组列表失败: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var groups []*ConversationGroup
|
||||
for rows.Next() {
|
||||
var group ConversationGroup
|
||||
var createdAt, updatedAt string
|
||||
var pinned int
|
||||
|
||||
if err := rows.Scan(&group.ID, &group.Name, &group.Icon, &pinned, &group.OwnerUserID, &createdAt, &updatedAt); err != nil {
|
||||
return nil, fmt.Errorf("扫描分组失败: %w", err)
|
||||
}
|
||||
|
||||
group.Pinned = pinned != 0
|
||||
|
||||
// 尝试多种时间格式解析
|
||||
var err1, err2 error
|
||||
group.CreatedAt, err1 = time.Parse("2006-01-02 15:04:05.999999999-07:00", createdAt)
|
||||
if err1 != nil {
|
||||
group.CreatedAt, err1 = time.Parse("2006-01-02 15:04:05", createdAt)
|
||||
}
|
||||
if err1 != nil {
|
||||
group.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
||||
}
|
||||
|
||||
group.UpdatedAt, err2 = time.Parse("2006-01-02 15:04:05.999999999-07:00", updatedAt)
|
||||
if err2 != nil {
|
||||
group.UpdatedAt, err2 = time.Parse("2006-01-02 15:04:05", updatedAt)
|
||||
}
|
||||
if err2 != nil {
|
||||
group.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
|
||||
}
|
||||
|
||||
groups = append(groups, &group)
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
// GetGroup 获取分组
|
||||
func (db *DB) GetGroup(id string) (*ConversationGroup, error) {
|
||||
var group ConversationGroup
|
||||
var createdAt, updatedAt string
|
||||
var pinned int
|
||||
|
||||
err := db.QueryRow(
|
||||
"SELECT id, name, icon, COALESCE(pinned, 0), COALESCE(owner_user_id, ''), created_at, updated_at FROM conversation_groups WHERE id = ?",
|
||||
id,
|
||||
).Scan(&group.ID, &group.Name, &group.Icon, &pinned, &group.OwnerUserID, &createdAt, &updatedAt)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, fmt.Errorf("分组不存在")
|
||||
}
|
||||
return nil, fmt.Errorf("查询分组失败: %w", err)
|
||||
}
|
||||
|
||||
// 尝试多种时间格式解析
|
||||
var err1, err2 error
|
||||
group.CreatedAt, err1 = time.Parse("2006-01-02 15:04:05.999999999-07:00", createdAt)
|
||||
if err1 != nil {
|
||||
group.CreatedAt, err1 = time.Parse("2006-01-02 15:04:05", createdAt)
|
||||
}
|
||||
if err1 != nil {
|
||||
group.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
||||
}
|
||||
|
||||
group.UpdatedAt, err2 = time.Parse("2006-01-02 15:04:05.999999999-07:00", updatedAt)
|
||||
if err2 != nil {
|
||||
group.UpdatedAt, err2 = time.Parse("2006-01-02 15:04:05", updatedAt)
|
||||
}
|
||||
if err2 != nil {
|
||||
group.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
|
||||
}
|
||||
|
||||
group.Pinned = pinned != 0
|
||||
|
||||
return &group, nil
|
||||
}
|
||||
|
||||
func (db *DB) UserCanAccessGroup(userID, scope, groupID string) bool {
|
||||
if scope == RBACScopeAll {
|
||||
return true
|
||||
}
|
||||
var count int
|
||||
err := db.QueryRow(`SELECT COUNT(*) FROM conversation_groups WHERE id = ? AND owner_user_id = ?`, groupID, userID).Scan(&count)
|
||||
return err == nil && count > 0
|
||||
}
|
||||
|
||||
// UpdateGroup 更新分组
|
||||
func (db *DB) UpdateGroup(id, name, icon string) error {
|
||||
existing, err := db.GetGroup(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 检查名称是否已存在(排除当前分组)
|
||||
exists, err := db.groupExistsByNameForOwner(name, id, existing.OwnerUserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return fmt.Errorf("分组名称已存在")
|
||||
}
|
||||
|
||||
_, err = db.Exec(
|
||||
"UPDATE conversation_groups SET name = ?, icon = ?, updated_at = ? WHERE id = ?",
|
||||
name, icon, time.Now(), id,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新分组失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteGroup 删除分组
|
||||
func (db *DB) DeleteGroup(id string) error {
|
||||
_, err := db.Exec("DELETE FROM conversation_groups WHERE id = ?", id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除分组失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddConversationToGroup 将对话添加到分组
|
||||
// 注意:一个对话只能属于一个分组,所以在添加新分组之前,会先删除该对话的所有旧分组关联
|
||||
func (db *DB) AddConversationToGroup(conversationID, groupID string) error {
|
||||
// 先删除该对话的所有旧分组关联,确保一个对话只属于一个分组
|
||||
_, err := db.Exec(
|
||||
"DELETE FROM conversation_group_mappings WHERE conversation_id = ?",
|
||||
conversationID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除对话旧分组关联失败: %w", err)
|
||||
}
|
||||
|
||||
// 然后插入新的分组关联
|
||||
id := uuid.New().String()
|
||||
_, err = db.Exec(
|
||||
"INSERT INTO conversation_group_mappings (id, conversation_id, group_id, created_at) VALUES (?, ?, ?, ?)",
|
||||
id, conversationID, groupID, time.Now(),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("添加对话到分组失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveConversationFromGroup 从分组中移除对话
|
||||
func (db *DB) RemoveConversationFromGroup(conversationID, groupID string) error {
|
||||
_, err := db.Exec(
|
||||
"DELETE FROM conversation_group_mappings WHERE conversation_id = ? AND group_id = ?",
|
||||
conversationID, groupID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("从分组中移除对话失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetConversationsByGroup 获取分组中的所有对话
|
||||
func (db *DB) GetConversationsByGroup(groupID string) ([]*Conversation, error) {
|
||||
rows, err := db.Query(
|
||||
`SELECT c.id, c.title, COALESCE(c.pinned, 0), c.created_at, c.updated_at, COALESCE(cgm.pinned, 0) as group_pinned
|
||||
FROM conversations c
|
||||
INNER JOIN conversation_group_mappings cgm ON c.id = cgm.conversation_id
|
||||
WHERE cgm.group_id = ?
|
||||
ORDER BY COALESCE(cgm.pinned, 0) DESC, c.updated_at DESC`,
|
||||
groupID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询分组对话失败: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var conversations []*Conversation
|
||||
for rows.Next() {
|
||||
var conv Conversation
|
||||
var createdAt, updatedAt string
|
||||
var pinned int
|
||||
var groupPinned int
|
||||
|
||||
if err := rows.Scan(&conv.ID, &conv.Title, &pinned, &createdAt, &updatedAt, &groupPinned); err != nil {
|
||||
return nil, fmt.Errorf("扫描对话失败: %w", err)
|
||||
}
|
||||
|
||||
// 尝试多种时间格式解析
|
||||
var err1, err2 error
|
||||
conv.CreatedAt, err1 = time.Parse("2006-01-02 15:04:05.999999999-07:00", createdAt)
|
||||
if err1 != nil {
|
||||
conv.CreatedAt, err1 = time.Parse("2006-01-02 15:04:05", createdAt)
|
||||
}
|
||||
if err1 != nil {
|
||||
conv.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
||||
}
|
||||
|
||||
conv.UpdatedAt, err2 = time.Parse("2006-01-02 15:04:05.999999999-07:00", updatedAt)
|
||||
if err2 != nil {
|
||||
conv.UpdatedAt, err2 = time.Parse("2006-01-02 15:04:05", updatedAt)
|
||||
}
|
||||
if err2 != nil {
|
||||
conv.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
|
||||
}
|
||||
|
||||
conv.Pinned = pinned != 0
|
||||
|
||||
conversations = append(conversations, &conv)
|
||||
}
|
||||
|
||||
return conversations, nil
|
||||
}
|
||||
|
||||
// SearchConversationsByGroup 搜索分组中的对话(按标题和消息内容模糊匹配)
|
||||
func (db *DB) SearchConversationsByGroup(groupID string, searchQuery string) ([]*Conversation, error) {
|
||||
// 构建SQL查询,支持按标题和消息内容搜索
|
||||
// 使用 DISTINCT 避免因为一个对话有多条匹配消息而重复
|
||||
query := `SELECT DISTINCT c.id, c.title, COALESCE(c.pinned, 0), c.created_at, c.updated_at, COALESCE(cgm.pinned, 0) as group_pinned
|
||||
FROM conversations c
|
||||
INNER JOIN conversation_group_mappings cgm ON c.id = cgm.conversation_id
|
||||
WHERE cgm.group_id = ?`
|
||||
|
||||
args := []interface{}{groupID}
|
||||
|
||||
// 如果有搜索关键词,添加标题和消息内容搜索条件
|
||||
if searchQuery != "" {
|
||||
searchPattern := "%" + searchQuery + "%"
|
||||
// 搜索标题或消息内容
|
||||
// 使用 LEFT JOIN 连接消息表,这样即使没有消息的对话也能被搜索到(通过标题)
|
||||
query += ` AND (
|
||||
LOWER(c.title) LIKE LOWER(?)
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM messages m
|
||||
WHERE m.conversation_id = c.id
|
||||
AND LOWER(m.content) LIKE LOWER(?)
|
||||
)
|
||||
)`
|
||||
args = append(args, searchPattern, searchPattern)
|
||||
}
|
||||
|
||||
query += " ORDER BY COALESCE(cgm.pinned, 0) DESC, c.updated_at DESC"
|
||||
|
||||
rows, err := db.Query(query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("搜索分组对话失败: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var conversations []*Conversation
|
||||
for rows.Next() {
|
||||
var conv Conversation
|
||||
var createdAt, updatedAt string
|
||||
var pinned int
|
||||
var groupPinned int
|
||||
|
||||
if err := rows.Scan(&conv.ID, &conv.Title, &pinned, &createdAt, &updatedAt, &groupPinned); err != nil {
|
||||
return nil, fmt.Errorf("扫描对话失败: %w", err)
|
||||
}
|
||||
|
||||
// 尝试多种时间格式解析
|
||||
var err1, err2 error
|
||||
conv.CreatedAt, err1 = time.Parse("2006-01-02 15:04:05.999999999-07:00", createdAt)
|
||||
if err1 != nil {
|
||||
conv.CreatedAt, err1 = time.Parse("2006-01-02 15:04:05", createdAt)
|
||||
}
|
||||
if err1 != nil {
|
||||
conv.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
||||
}
|
||||
|
||||
conv.UpdatedAt, err2 = time.Parse("2006-01-02 15:04:05.999999999-07:00", updatedAt)
|
||||
if err2 != nil {
|
||||
conv.UpdatedAt, err2 = time.Parse("2006-01-02 15:04:05", updatedAt)
|
||||
}
|
||||
if err2 != nil {
|
||||
conv.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
|
||||
}
|
||||
|
||||
conv.Pinned = pinned != 0
|
||||
|
||||
conversations = append(conversations, &conv)
|
||||
}
|
||||
|
||||
return conversations, nil
|
||||
}
|
||||
|
||||
// GetGroupByConversation 获取对话所属的分组
|
||||
func (db *DB) GetGroupByConversation(conversationID string) (string, error) {
|
||||
var groupID string
|
||||
err := db.QueryRow(
|
||||
"SELECT group_id FROM conversation_group_mappings WHERE conversation_id = ? LIMIT 1",
|
||||
conversationID,
|
||||
).Scan(&groupID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return "", nil // 没有分组
|
||||
}
|
||||
return "", fmt.Errorf("查询对话分组失败: %w", err)
|
||||
}
|
||||
return groupID, nil
|
||||
}
|
||||
|
||||
// UpdateConversationPinned 更新对话置顶状态
|
||||
func (db *DB) UpdateConversationPinned(id string, pinned bool) error {
|
||||
pinnedValue := 0
|
||||
if pinned {
|
||||
pinnedValue = 1
|
||||
}
|
||||
// 注意:不更新 updated_at,因为置顶操作不应该改变对话的更新时间
|
||||
_, err := db.Exec(
|
||||
"UPDATE conversations SET pinned = ? WHERE id = ?",
|
||||
pinnedValue, id,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新对话置顶状态失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateGroupPinned 更新分组置顶状态
|
||||
func (db *DB) UpdateGroupPinned(id string, pinned bool) error {
|
||||
pinnedValue := 0
|
||||
if pinned {
|
||||
pinnedValue = 1
|
||||
}
|
||||
_, err := db.Exec(
|
||||
"UPDATE conversation_groups SET pinned = ?, updated_at = ? WHERE id = ?",
|
||||
pinnedValue, time.Now(), id,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新分组置顶状态失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GroupMapping 分组映射关系
|
||||
type GroupMapping struct {
|
||||
ConversationID string `json:"conversationId"`
|
||||
GroupID string `json:"groupId"`
|
||||
}
|
||||
|
||||
// GetAllGroupMappings 批量获取所有分组映射(消除 N+1 查询)
|
||||
func (db *DB) GetAllGroupMappings() ([]GroupMapping, error) {
|
||||
rows, err := db.Query("SELECT conversation_id, group_id FROM conversation_group_mappings")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询分组映射失败: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var mappings []GroupMapping
|
||||
for rows.Next() {
|
||||
var m GroupMapping
|
||||
if err := rows.Scan(&m.ConversationID, &m.GroupID); err != nil {
|
||||
return nil, fmt.Errorf("扫描分组映射失败: %w", err)
|
||||
}
|
||||
mappings = append(mappings, m)
|
||||
}
|
||||
|
||||
if mappings == nil {
|
||||
mappings = []GroupMapping{}
|
||||
}
|
||||
return mappings, nil
|
||||
}
|
||||
|
||||
// UpdateConversationPinnedInGroup 更新对话在分组中的置顶状态
|
||||
func (db *DB) UpdateConversationPinnedInGroup(conversationID, groupID string, pinned bool) error {
|
||||
pinnedValue := 0
|
||||
if pinned {
|
||||
pinnedValue = 1
|
||||
}
|
||||
_, err := db.Exec(
|
||||
"UPDATE conversation_group_mappings SET pinned = ? WHERE conversation_id = ? AND group_id = ?",
|
||||
pinnedValue, conversationID, groupID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新分组对话置顶状态失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -201,7 +201,6 @@ func (db *DB) migrateRBACOwnershipColumns() error {
|
||||
{"webshell_connections", "owner_user_id", "ALTER TABLE webshell_connections ADD COLUMN owner_user_id TEXT"},
|
||||
{"batch_task_queues", "owner_user_id", "ALTER TABLE batch_task_queues ADD COLUMN owner_user_id TEXT"},
|
||||
{"c2_listeners", "owner_user_id", "ALTER TABLE c2_listeners ADD COLUMN owner_user_id TEXT"},
|
||||
{"conversation_groups", "owner_user_id", "ALTER TABLE conversation_groups ADD COLUMN owner_user_id TEXT"},
|
||||
{"tool_executions", "owner_user_id", "ALTER TABLE tool_executions ADD COLUMN owner_user_id TEXT"},
|
||||
{"tool_executions", "conversation_id", "ALTER TABLE tool_executions ADD COLUMN conversation_id TEXT"},
|
||||
} {
|
||||
|
||||
@@ -58,27 +58,8 @@ func TestRBACToolExecutionOwnershipAccess(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRBACGroupAndUploadOwnership(t *testing.T) {
|
||||
func TestRBACUploadOwnership(t *testing.T) {
|
||||
db := newRBACTestDB(t)
|
||||
group1, err := db.CreateGroup("u1 group", "", "u1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
group2, err := db.CreateGroup("u2 group", "", "u2")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
groups, err := db.ListGroupsForAccess("u1", RBACScopeAssigned)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(groups) != 1 || groups[0].ID != group1.ID {
|
||||
t.Fatalf("groups = %#v, want only %s (not %s)", groups, group1.ID, group2.ID)
|
||||
}
|
||||
if db.UserCanAccessGroup("u1", RBACScopeAssigned, group2.ID) {
|
||||
t.Fatal("foreign group was accessible")
|
||||
}
|
||||
|
||||
conversation, err := db.CreateConversation("upload", ConversationCreateMeta{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -160,24 +160,15 @@ func (h *ConversationHandler) ListConversations(c *gin.Context) {
|
||||
limit = 1000
|
||||
}
|
||||
|
||||
excludeGrouped := strings.TrimSpace(search) == "" && projectID == "" &&
|
||||
(c.Query("exclude_grouped") == "true" || c.Query("exclude_grouped") == "1")
|
||||
sortBy := strings.TrimSpace(c.Query("sort_by"))
|
||||
session, _ := security.CurrentSession(c)
|
||||
|
||||
var conversations []*database.Conversation
|
||||
var total int
|
||||
var err error
|
||||
if excludeGrouped {
|
||||
conversations, err = h.db.ListUngroupedConversationsForAccess(limit, offset, sortBy, projectID, session.UserID, session.Scope)
|
||||
if err == nil {
|
||||
total, err = h.db.CountUngroupedConversationsForAccess(projectID, session.UserID, session.Scope)
|
||||
}
|
||||
} else {
|
||||
conversations, err = h.db.ListConversationsForAccess(limit, offset, search, sortBy, projectID, session.UserID, session.Scope)
|
||||
if err == nil {
|
||||
total, err = h.db.CountConversationsForAccess(search, projectID, session.UserID, session.Scope)
|
||||
}
|
||||
conversations, err = h.db.ListConversationsForAccess(limit, offset, search, sortBy, projectID, session.UserID, session.Scope)
|
||||
if err == nil {
|
||||
total, err = h.db.CountConversationsForAccess(search, projectID, session.UserID, session.Scope)
|
||||
}
|
||||
if err != nil {
|
||||
h.logger.Error("获取对话列表失败", zap.Error(err))
|
||||
@@ -195,6 +186,35 @@ func (h *ConversationHandler) ListConversations(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateConversationPinnedRequest 更新对话置顶状态请求
|
||||
type UpdateConversationPinnedRequest struct {
|
||||
Pinned bool `json:"pinned"`
|
||||
}
|
||||
|
||||
// UpdateConversationPinned 更新对话置顶状态
|
||||
func (h *ConversationHandler) UpdateConversationPinned(c *gin.Context) {
|
||||
conversationID := c.Param("id")
|
||||
session, ok := security.CurrentSession(c)
|
||||
if !ok || !h.db.UserCanAccessResource(session.UserID, session.Scope, "conversation", conversationID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权访问该资源"})
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateConversationPinnedRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.UpdateConversationPinned(conversationID, req.Pinned); err != nil {
|
||||
h.logger.Error("更新对话置顶状态失败", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "更新成功"})
|
||||
}
|
||||
|
||||
// GetConversation 获取对话
|
||||
func (h *ConversationHandler) GetConversation(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
@@ -1,438 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"cyberstrike-ai/internal/database"
|
||||
"cyberstrike-ai/internal/security"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// GroupHandler 分组处理器
|
||||
type GroupHandler struct {
|
||||
db *database.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
const (
|
||||
maxGroupNameRunes = 64
|
||||
maxGroupIconRunes = 16
|
||||
)
|
||||
|
||||
// NewGroupHandler 创建新的分组处理器
|
||||
func NewGroupHandler(db *database.DB, logger *zap.Logger) *GroupHandler {
|
||||
return &GroupHandler{
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func validateGroupTextField(field, value string, maxRunes int, required bool) (string, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
if required {
|
||||
return "", errors.New(field + "不能为空")
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
if utf8.RuneCountInString(value) > maxRunes {
|
||||
return "", errors.New(field + "过长")
|
||||
}
|
||||
for _, r := range value {
|
||||
switch r {
|
||||
case '<', '>', '"', '\'', '`':
|
||||
return "", errors.New(field + "包含非法字符")
|
||||
}
|
||||
if r < 0x20 || r == 0x7f {
|
||||
return "", errors.New(field + "包含非法控制字符")
|
||||
}
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func validateGroupFields(name, icon string) (string, string, error) {
|
||||
validName, err := validateGroupTextField("分组名称", name, maxGroupNameRunes, true)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
validIcon, err := validateGroupTextField("分组图标", icon, maxGroupIconRunes, false)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return validName, validIcon, nil
|
||||
}
|
||||
|
||||
// CreateGroupRequest 创建分组请求
|
||||
type CreateGroupRequest struct {
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
}
|
||||
|
||||
// CreateGroup 创建分组
|
||||
func (h *GroupHandler) CreateGroup(c *gin.Context) {
|
||||
var req CreateGroupRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
name, icon, err := validateGroupFields(req.Name, req.Icon)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
session, _ := security.CurrentSession(c)
|
||||
group, err := h.db.CreateGroup(name, icon, session.UserID)
|
||||
if err != nil {
|
||||
h.logger.Error("创建分组失败", zap.Error(err))
|
||||
// 如果是名称重复错误,返回400状态码
|
||||
if err.Error() == "分组名称已存在" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "分组名称已存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, group)
|
||||
}
|
||||
|
||||
// ListGroups 列出所有分组
|
||||
func (h *GroupHandler) ListGroups(c *gin.Context) {
|
||||
session, _ := security.CurrentSession(c)
|
||||
groups, err := h.db.ListGroupsForAccess(session.UserID, session.Scope)
|
||||
if err != nil {
|
||||
h.logger.Error("获取分组列表失败", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, groups)
|
||||
}
|
||||
|
||||
// GetGroup 获取分组
|
||||
func (h *GroupHandler) GetGroup(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !h.groupAllowed(c, id) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权访问该资源"})
|
||||
return
|
||||
}
|
||||
|
||||
group, err := h.db.GetGroup(id)
|
||||
if err != nil {
|
||||
h.logger.Error("获取分组失败", zap.Error(err))
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "分组不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, group)
|
||||
}
|
||||
|
||||
// UpdateGroupRequest 更新分组请求
|
||||
type UpdateGroupRequest struct {
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
}
|
||||
|
||||
// UpdateGroup 更新分组
|
||||
func (h *GroupHandler) UpdateGroup(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !h.groupAllowed(c, id) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权访问该资源"})
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateGroupRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
name, icon, err := validateGroupFields(req.Name, req.Icon)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.UpdateGroup(id, name, icon); err != nil {
|
||||
h.logger.Error("更新分组失败", zap.Error(err))
|
||||
// 如果是名称重复错误,返回400状态码
|
||||
if err.Error() == "分组名称已存在" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "分组名称已存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
group, err := h.db.GetGroup(id)
|
||||
if err != nil {
|
||||
h.logger.Error("获取更新后的分组失败", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, group)
|
||||
}
|
||||
|
||||
// DeleteGroup 删除分组
|
||||
func (h *GroupHandler) DeleteGroup(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !h.groupAllowed(c, id) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权访问该资源"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.DeleteGroup(id); err != nil {
|
||||
h.logger.Error("删除分组失败", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
|
||||
}
|
||||
|
||||
// AddConversationToGroupRequest 添加对话到分组请求
|
||||
type AddConversationToGroupRequest struct {
|
||||
ConversationID string `json:"conversationId"`
|
||||
GroupID string `json:"groupId"`
|
||||
}
|
||||
|
||||
// AddConversationToGroup 将对话添加到分组
|
||||
func (h *GroupHandler) AddConversationToGroup(c *gin.Context) {
|
||||
var req AddConversationToGroupRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if !h.groupConversationAllowed(c, req.ConversationID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权访问该资源"})
|
||||
return
|
||||
}
|
||||
if !h.groupAllowed(c, req.GroupID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权访问该分组"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.AddConversationToGroup(req.ConversationID, req.GroupID); err != nil {
|
||||
h.logger.Error("添加对话到分组失败", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "添加成功"})
|
||||
}
|
||||
|
||||
// RemoveConversationFromGroup 从分组中移除对话
|
||||
func (h *GroupHandler) RemoveConversationFromGroup(c *gin.Context) {
|
||||
conversationID := c.Param("conversationId")
|
||||
groupID := c.Param("id")
|
||||
if !h.groupAllowed(c, groupID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权访问该分组"})
|
||||
return
|
||||
}
|
||||
if !h.groupConversationAllowed(c, conversationID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权访问该资源"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.RemoveConversationFromGroup(conversationID, groupID); err != nil {
|
||||
h.logger.Error("从分组中移除对话失败", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "移除成功"})
|
||||
}
|
||||
|
||||
// GroupConversation 分组对话响应结构
|
||||
type GroupConversation struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Pinned bool `json:"pinned"`
|
||||
GroupPinned bool `json:"groupPinned"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// GetGroupConversations 获取分组中的所有对话
|
||||
func (h *GroupHandler) GetGroupConversations(c *gin.Context) {
|
||||
groupID := c.Param("id")
|
||||
if !h.groupAllowed(c, groupID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权访问该分组"})
|
||||
return
|
||||
}
|
||||
searchQuery := c.Query("search") // 获取搜索参数
|
||||
|
||||
var conversations []*database.Conversation
|
||||
var err error
|
||||
|
||||
// 如果有搜索关键词,使用搜索方法;否则使用普通方法
|
||||
if searchQuery != "" {
|
||||
conversations, err = h.db.SearchConversationsByGroup(groupID, searchQuery)
|
||||
} else {
|
||||
conversations, err = h.db.GetConversationsByGroup(groupID)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
h.logger.Error("获取分组对话失败", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取每个对话在分组中的置顶状态
|
||||
groupConvs := make([]GroupConversation, 0, len(conversations))
|
||||
for _, conv := range conversations {
|
||||
if conv == nil || !h.groupConversationAllowed(c, conv.ID) {
|
||||
continue
|
||||
}
|
||||
// 查询分组内置顶状态
|
||||
var groupPinned int
|
||||
err := h.db.QueryRow(
|
||||
"SELECT COALESCE(pinned, 0) FROM conversation_group_mappings WHERE conversation_id = ? AND group_id = ?",
|
||||
conv.ID, groupID,
|
||||
).Scan(&groupPinned)
|
||||
if err != nil {
|
||||
h.logger.Warn("查询分组内置顶状态失败", zap.String("conversationId", conv.ID), zap.Error(err))
|
||||
groupPinned = 0
|
||||
}
|
||||
|
||||
groupConvs = append(groupConvs, GroupConversation{
|
||||
ID: conv.ID,
|
||||
Title: conv.Title,
|
||||
Pinned: conv.Pinned,
|
||||
GroupPinned: groupPinned != 0,
|
||||
CreatedAt: conv.CreatedAt,
|
||||
UpdatedAt: conv.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, groupConvs)
|
||||
}
|
||||
|
||||
// GetAllMappings 批量获取所有分组映射(消除前端 N+1 请求)
|
||||
func (h *GroupHandler) GetAllMappings(c *gin.Context) {
|
||||
mappings, err := h.db.GetAllGroupMappings()
|
||||
if err != nil {
|
||||
h.logger.Error("获取分组映射失败", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
filtered := mappings[:0]
|
||||
for _, mapping := range mappings {
|
||||
if h.groupConversationAllowed(c, mapping.ConversationID) && h.groupAllowed(c, mapping.GroupID) {
|
||||
filtered = append(filtered, mapping)
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, filtered)
|
||||
}
|
||||
|
||||
// UpdateConversationPinnedRequest 更新对话置顶状态请求
|
||||
type UpdateConversationPinnedRequest struct {
|
||||
Pinned bool `json:"pinned"`
|
||||
}
|
||||
|
||||
// UpdateConversationPinned 更新对话置顶状态
|
||||
func (h *GroupHandler) UpdateConversationPinned(c *gin.Context) {
|
||||
conversationID := c.Param("id")
|
||||
if !h.groupConversationAllowed(c, conversationID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权访问该资源"})
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateConversationPinnedRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.UpdateConversationPinned(conversationID, req.Pinned); err != nil {
|
||||
h.logger.Error("更新对话置顶状态失败", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "更新成功"})
|
||||
}
|
||||
|
||||
// UpdateGroupPinnedRequest 更新分组置顶状态请求
|
||||
type UpdateGroupPinnedRequest struct {
|
||||
Pinned bool `json:"pinned"`
|
||||
}
|
||||
|
||||
// UpdateGroupPinned 更新分组置顶状态
|
||||
func (h *GroupHandler) UpdateGroupPinned(c *gin.Context) {
|
||||
groupID := c.Param("id")
|
||||
if !h.groupAllowed(c, groupID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权访问该分组"})
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateGroupPinnedRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.UpdateGroupPinned(groupID, req.Pinned); err != nil {
|
||||
h.logger.Error("更新分组置顶状态失败", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "更新成功"})
|
||||
}
|
||||
|
||||
// UpdateConversationPinnedInGroupRequest 更新分组对话置顶状态请求
|
||||
type UpdateConversationPinnedInGroupRequest struct {
|
||||
Pinned bool `json:"pinned"`
|
||||
}
|
||||
|
||||
// UpdateConversationPinnedInGroup 更新对话在分组中的置顶状态
|
||||
func (h *GroupHandler) UpdateConversationPinnedInGroup(c *gin.Context) {
|
||||
groupID := c.Param("id")
|
||||
conversationID := c.Param("conversationId")
|
||||
if !h.groupAllowed(c, groupID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权访问该分组"})
|
||||
return
|
||||
}
|
||||
if !h.groupConversationAllowed(c, conversationID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权访问该资源"})
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateConversationPinnedInGroupRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.UpdateConversationPinnedInGroup(conversationID, groupID, req.Pinned); err != nil {
|
||||
h.logger.Error("更新分组对话置顶状态失败", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "更新成功"})
|
||||
}
|
||||
|
||||
func (h *GroupHandler) groupConversationAllowed(c *gin.Context, conversationID string) bool {
|
||||
session, ok := security.CurrentSession(c)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return h.db.UserCanAccessResource(session.UserID, session.Scope, "conversation", conversationID)
|
||||
}
|
||||
|
||||
func (h *GroupHandler) groupAllowed(c *gin.Context, groupID string) bool {
|
||||
session, ok := security.CurrentSession(c)
|
||||
return ok && h.db.UserCanAccessGroup(session.UserID, session.Scope, groupID)
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateGroupFieldsAllowsNormalNamesAndIcons(t *testing.T) {
|
||||
name, icon, err := validateGroupFields(" 日常安全巡检 ", " 📁 ")
|
||||
if err != nil {
|
||||
t.Fatalf("validateGroupFields returned error: %v", err)
|
||||
}
|
||||
if name != "日常安全巡检" {
|
||||
t.Fatalf("name = %q, want trimmed normal name", name)
|
||||
}
|
||||
if icon != "📁" {
|
||||
t.Fatalf("icon = %q, want trimmed icon", icon)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateGroupFieldsRejectsStoredXSSPayloads(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
icon string
|
||||
}{
|
||||
{name: `<img src=x onerror="alert(1)">`, icon: "📁"},
|
||||
{name: "日常安全巡检", icon: `<svg onload=alert(1)>`},
|
||||
{name: "日常安全巡检`onmouseover=alert(1)", icon: "📁"},
|
||||
{name: "日常安全巡检\x00", icon: "📁"},
|
||||
{name: strings.Repeat("分", maxGroupNameRunes+1), icon: "📁"},
|
||||
{name: "日常安全巡检", icon: strings.Repeat("📁", maxGroupIconRunes+1)},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name+"/"+tt.icon, func(t *testing.T) {
|
||||
if _, _, err := validateGroupFields(tt.name, tt.icon); err == nil {
|
||||
t.Fatal("validateGroupFields returned nil error for unsafe input")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -456,75 +456,6 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
|
||||
},
|
||||
},
|
||||
},
|
||||
"Group": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"id": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "分组ID",
|
||||
},
|
||||
"name": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "分组名称",
|
||||
},
|
||||
"icon": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "分组图标",
|
||||
},
|
||||
"createdAt": map[string]interface{}{
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "创建时间",
|
||||
},
|
||||
"updatedAt": map[string]interface{}{
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "更新时间",
|
||||
},
|
||||
},
|
||||
},
|
||||
"CreateGroupRequest": map[string]interface{}{
|
||||
"type": "object",
|
||||
"required": []string{"name"},
|
||||
"properties": map[string]interface{}{
|
||||
"name": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "分组名称",
|
||||
},
|
||||
"icon": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "分组图标(可选)",
|
||||
},
|
||||
},
|
||||
},
|
||||
"UpdateGroupRequest": map[string]interface{}{
|
||||
"type": "object",
|
||||
"required": []string{"name"},
|
||||
"properties": map[string]interface{}{
|
||||
"name": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "分组名称",
|
||||
},
|
||||
"icon": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "分组图标",
|
||||
},
|
||||
},
|
||||
},
|
||||
"AddConversationToGroupRequest": map[string]interface{}{
|
||||
"type": "object",
|
||||
"required": []string{"conversationId", "groupId"},
|
||||
"properties": map[string]interface{}{
|
||||
"conversationId": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "对话ID",
|
||||
},
|
||||
"groupId": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "分组ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
"BatchTaskRequest": map[string]interface{}{
|
||||
"type": "object",
|
||||
"required": []string{"tasks"},
|
||||
@@ -1401,15 +1332,6 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "exclude_grouped",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "为 true 时排除已加入分组的对话(默认在未搜索且未按项目筛选时启用)",
|
||||
"schema": map[string]interface{}{
|
||||
"type": "boolean",
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "sort_by",
|
||||
"in": "query",
|
||||
@@ -2315,290 +2237,6 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/groups": map[string]interface{}{
|
||||
"post": map[string]interface{}{
|
||||
"tags": []string{"对话分组"},
|
||||
"summary": "创建分组",
|
||||
"description": "创建一个新的对话分组",
|
||||
"operationId": "createGroup",
|
||||
"requestBody": map[string]interface{}{
|
||||
"required": true,
|
||||
"content": map[string]interface{}{
|
||||
"application/json": map[string]interface{}{
|
||||
"schema": map[string]interface{}{
|
||||
"$ref": "#/components/schemas/CreateGroupRequest",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"responses": map[string]interface{}{
|
||||
"200": map[string]interface{}{
|
||||
"description": "创建成功",
|
||||
"content": map[string]interface{}{
|
||||
"application/json": map[string]interface{}{
|
||||
"schema": map[string]interface{}{
|
||||
"$ref": "#/components/schemas/Group",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"400": map[string]interface{}{
|
||||
"description": "请求参数错误或分组名称已存在",
|
||||
},
|
||||
"401": map[string]interface{}{
|
||||
"description": "未授权",
|
||||
},
|
||||
},
|
||||
},
|
||||
"get": map[string]interface{}{
|
||||
"tags": []string{"对话分组"},
|
||||
"summary": "列出分组",
|
||||
"description": "获取所有对话分组",
|
||||
"operationId": "listGroups",
|
||||
"responses": map[string]interface{}{
|
||||
"200": map[string]interface{}{
|
||||
"description": "获取成功",
|
||||
"content": map[string]interface{}{
|
||||
"application/json": map[string]interface{}{
|
||||
"schema": map[string]interface{}{
|
||||
"type": "array",
|
||||
"items": map[string]interface{}{
|
||||
"$ref": "#/components/schemas/Group",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"401": map[string]interface{}{
|
||||
"description": "未授权",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/groups/{id}": map[string]interface{}{
|
||||
"get": map[string]interface{}{
|
||||
"tags": []string{"对话分组"},
|
||||
"summary": "获取分组",
|
||||
"description": "获取指定分组的详细信息",
|
||||
"operationId": "getGroup",
|
||||
"parameters": []map[string]interface{}{
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "分组ID",
|
||||
"schema": map[string]interface{}{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"responses": map[string]interface{}{
|
||||
"200": map[string]interface{}{
|
||||
"description": "获取成功",
|
||||
"content": map[string]interface{}{
|
||||
"application/json": map[string]interface{}{
|
||||
"schema": map[string]interface{}{
|
||||
"$ref": "#/components/schemas/Group",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"404": map[string]interface{}{
|
||||
"description": "分组不存在",
|
||||
},
|
||||
"401": map[string]interface{}{
|
||||
"description": "未授权",
|
||||
},
|
||||
},
|
||||
},
|
||||
"put": map[string]interface{}{
|
||||
"tags": []string{"对话分组"},
|
||||
"summary": "更新分组",
|
||||
"description": "更新分组信息",
|
||||
"operationId": "updateGroup",
|
||||
"parameters": []map[string]interface{}{
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "分组ID",
|
||||
"schema": map[string]interface{}{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"requestBody": map[string]interface{}{
|
||||
"required": true,
|
||||
"content": map[string]interface{}{
|
||||
"application/json": map[string]interface{}{
|
||||
"schema": map[string]interface{}{
|
||||
"$ref": "#/components/schemas/UpdateGroupRequest",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"responses": map[string]interface{}{
|
||||
"200": map[string]interface{}{
|
||||
"description": "更新成功",
|
||||
"content": map[string]interface{}{
|
||||
"application/json": map[string]interface{}{
|
||||
"schema": map[string]interface{}{
|
||||
"$ref": "#/components/schemas/Group",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"400": map[string]interface{}{
|
||||
"description": "请求参数错误或分组名称已存在",
|
||||
},
|
||||
"404": map[string]interface{}{
|
||||
"description": "分组不存在",
|
||||
},
|
||||
"401": map[string]interface{}{
|
||||
"description": "未授权",
|
||||
},
|
||||
},
|
||||
},
|
||||
"delete": map[string]interface{}{
|
||||
"tags": []string{"对话分组"},
|
||||
"summary": "删除分组",
|
||||
"description": "删除指定分组",
|
||||
"operationId": "deleteGroup",
|
||||
"parameters": []map[string]interface{}{
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "分组ID",
|
||||
"schema": map[string]interface{}{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"responses": map[string]interface{}{
|
||||
"200": map[string]interface{}{
|
||||
"description": "删除成功",
|
||||
},
|
||||
"404": map[string]interface{}{
|
||||
"description": "分组不存在",
|
||||
},
|
||||
"401": map[string]interface{}{
|
||||
"description": "未授权",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/groups/{id}/conversations": map[string]interface{}{
|
||||
"get": map[string]interface{}{
|
||||
"tags": []string{"对话分组"},
|
||||
"summary": "获取分组中的对话",
|
||||
"description": "获取指定分组中的所有对话",
|
||||
"operationId": "getGroupConversations",
|
||||
"parameters": []map[string]interface{}{
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "分组ID",
|
||||
"schema": map[string]interface{}{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"responses": map[string]interface{}{
|
||||
"200": map[string]interface{}{
|
||||
"description": "获取成功",
|
||||
"content": map[string]interface{}{
|
||||
"application/json": map[string]interface{}{
|
||||
"schema": map[string]interface{}{
|
||||
"type": "array",
|
||||
"items": map[string]interface{}{
|
||||
"$ref": "#/components/schemas/Conversation",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"404": map[string]interface{}{
|
||||
"description": "分组不存在",
|
||||
},
|
||||
"401": map[string]interface{}{
|
||||
"description": "未授权",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/groups/conversations": map[string]interface{}{
|
||||
"post": map[string]interface{}{
|
||||
"tags": []string{"对话分组"},
|
||||
"summary": "添加对话到分组",
|
||||
"description": "将对话添加到指定分组",
|
||||
"operationId": "addConversationToGroup",
|
||||
"requestBody": map[string]interface{}{
|
||||
"required": true,
|
||||
"content": map[string]interface{}{
|
||||
"application/json": map[string]interface{}{
|
||||
"schema": map[string]interface{}{
|
||||
"$ref": "#/components/schemas/AddConversationToGroupRequest",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"responses": map[string]interface{}{
|
||||
"200": map[string]interface{}{
|
||||
"description": "添加成功",
|
||||
},
|
||||
"400": map[string]interface{}{
|
||||
"description": "请求参数错误",
|
||||
},
|
||||
"404": map[string]interface{}{
|
||||
"description": "对话或分组不存在",
|
||||
},
|
||||
"401": map[string]interface{}{
|
||||
"description": "未授权",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/groups/{id}/conversations/{conversationId}": map[string]interface{}{
|
||||
"delete": map[string]interface{}{
|
||||
"tags": []string{"对话分组"},
|
||||
"summary": "从分组移除对话",
|
||||
"description": "从指定分组中移除对话",
|
||||
"operationId": "removeConversationFromGroup",
|
||||
"parameters": []map[string]interface{}{
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "分组ID",
|
||||
"schema": map[string]interface{}{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "conversationId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "对话ID",
|
||||
"schema": map[string]interface{}{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"responses": map[string]interface{}{
|
||||
"200": map[string]interface{}{
|
||||
"description": "移除成功",
|
||||
},
|
||||
"404": map[string]interface{}{
|
||||
"description": "对话或分组不存在",
|
||||
},
|
||||
"401": map[string]interface{}{
|
||||
"description": "未授权",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/assets/import": map[string]interface{}{
|
||||
"post": map[string]interface{}{
|
||||
"tags": []string{"资产管理"},
|
||||
@@ -4266,109 +3904,6 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/groups/{id}/pinned": map[string]interface{}{
|
||||
"put": map[string]interface{}{
|
||||
"tags": []string{"对话分组"},
|
||||
"summary": "设置分组置顶",
|
||||
"description": "设置或取消分组的置顶状态",
|
||||
"operationId": "updateGroupPinned",
|
||||
"parameters": []map[string]interface{}{
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "分组ID",
|
||||
"schema": map[string]interface{}{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"requestBody": map[string]interface{}{
|
||||
"required": true,
|
||||
"content": map[string]interface{}{
|
||||
"application/json": map[string]interface{}{
|
||||
"schema": map[string]interface{}{
|
||||
"type": "object",
|
||||
"required": []string{"pinned"},
|
||||
"properties": map[string]interface{}{
|
||||
"pinned": map[string]interface{}{
|
||||
"type": "boolean",
|
||||
"description": "是否置顶",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"responses": map[string]interface{}{
|
||||
"200": map[string]interface{}{
|
||||
"description": "更新成功",
|
||||
},
|
||||
"404": map[string]interface{}{
|
||||
"description": "分组不存在",
|
||||
},
|
||||
"401": map[string]interface{}{
|
||||
"description": "未授权",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/groups/{id}/conversations/{conversationId}/pinned": map[string]interface{}{
|
||||
"put": map[string]interface{}{
|
||||
"tags": []string{"对话分组"},
|
||||
"summary": "设置分组中对话的置顶",
|
||||
"description": "设置或取消分组中对话的置顶状态",
|
||||
"operationId": "updateConversationPinnedInGroup",
|
||||
"parameters": []map[string]interface{}{
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "分组ID",
|
||||
"schema": map[string]interface{}{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "conversationId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "对话ID",
|
||||
"schema": map[string]interface{}{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"requestBody": map[string]interface{}{
|
||||
"required": true,
|
||||
"content": map[string]interface{}{
|
||||
"application/json": map[string]interface{}{
|
||||
"schema": map[string]interface{}{
|
||||
"type": "object",
|
||||
"required": []string{"pinned"},
|
||||
"properties": map[string]interface{}{
|
||||
"pinned": map[string]interface{}{
|
||||
"type": "boolean",
|
||||
"description": "是否置顶",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"responses": map[string]interface{}{
|
||||
"200": map[string]interface{}{
|
||||
"description": "更新成功",
|
||||
},
|
||||
"404": map[string]interface{}{
|
||||
"description": "对话或分组不存在",
|
||||
},
|
||||
"401": map[string]interface{}{
|
||||
"description": "未授权",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/knowledge/categories": map[string]interface{}{
|
||||
"get": map[string]interface{}{
|
||||
"tags": []string{"知识库"},
|
||||
@@ -5194,38 +4729,6 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// ==================== 对话分组 - 缺失端点 ====================
|
||||
"/api/groups/mappings": map[string]interface{}{
|
||||
"get": map[string]interface{}{
|
||||
"tags": []string{"对话分组"},
|
||||
"summary": "获取所有分组映射",
|
||||
"description": "获取所有对话与分组之间的映射关系列表。",
|
||||
"operationId": "getAllGroupMappings",
|
||||
"responses": map[string]interface{}{
|
||||
"200": map[string]interface{}{
|
||||
"description": "获取成功",
|
||||
"content": map[string]interface{}{
|
||||
"application/json": map[string]interface{}{
|
||||
"schema": map[string]interface{}{
|
||||
"type": "array",
|
||||
"items": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"conversation_id": map[string]interface{}{"type": "string", "description": "对话ID"},
|
||||
"group_id": map[string]interface{}{"type": "string", "description": "分组ID"},
|
||||
"pinned": map[string]interface{}{"type": "boolean", "description": "是否置顶"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"401": map[string]interface{}{"description": "未授权"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// ==================== FOFA信息收集 ====================
|
||||
"/api/fofa/search": map[string]interface{}{
|
||||
"post": map[string]interface{}{
|
||||
|
||||
@@ -5,7 +5,7 @@ package handler
|
||||
|
||||
var apiDocI18nTagToKey = map[string]string{
|
||||
"认证": "auth", "对话管理": "conversationManagement", "对话交互": "conversationInteraction",
|
||||
"批量任务": "batchTasks", "对话分组": "conversationGroups", "漏洞管理": "vulnerabilityManagement",
|
||||
"批量任务": "batchTasks", "漏洞管理": "vulnerabilityManagement",
|
||||
"角色管理": "roleManagement", "Skills管理": "skillsManagement", "监控": "monitoring",
|
||||
"配置管理": "configManagement", "外部MCP管理": "externalMCPManagement", "攻击链": "attackChain",
|
||||
"知识库": "knowledgeBase", "MCP": "mcp",
|
||||
@@ -24,10 +24,7 @@ var apiDocI18nSummaryToKey = map[string]string{
|
||||
"删除批量任务队列": "deleteBatchQueue", "启动批量任务队列": "startBatchQueue", "暂停批量任务队列": "pauseBatchQueue",
|
||||
"添加任务到队列": "addTaskToQueue", "SQL注入扫描": "sqlInjectionScan", "端口扫描": "portScan",
|
||||
"更新批量任务": "updateBatchTask", "删除批量任务": "deleteBatchTask",
|
||||
"创建分组": "createGroup", "列出分组": "listGroups", "获取分组": "getGroup", "更新分组": "updateGroup",
|
||||
"删除分组": "deleteGroup", "获取分组中的对话": "getGroupConversations", "添加对话到分组": "addConversationToGroup",
|
||||
"从分组移除对话": "removeConversationFromGroup",
|
||||
"列出漏洞": "listVulnerabilities", "创建漏洞": "createVulnerability", "获取漏洞统计": "getVulnerabilityStats",
|
||||
"列出漏洞": "listVulnerabilities", "创建漏洞": "createVulnerability", "获取漏洞统计": "getVulnerabilityStats",
|
||||
"获取漏洞": "getVulnerability", "更新漏洞": "updateVulnerability", "删除漏洞": "deleteVulnerability",
|
||||
"列出角色": "listRoles", "创建角色": "createRole", "获取角色": "getRole", "更新角色": "updateRole", "删除角色": "deleteRole",
|
||||
"获取可用Skills列表": "getAvailableSkills", "列出Skills": "listSkills", "创建Skill": "createSkill",
|
||||
@@ -40,8 +37,8 @@ var apiDocI18nSummaryToKey = map[string]string{
|
||||
"添加或更新外部MCP": "addOrUpdateExternalMCP", "stdio模式配置": "stdioModeConfig", "SSE模式配置": "sseModeConfig",
|
||||
"删除外部MCP": "deleteExternalMCP", "启动外部MCP": "startExternalMCP", "停止外部MCP": "stopExternalMCP",
|
||||
"获取攻击链": "getAttackChain", "重新生成攻击链": "regenerateAttackChain",
|
||||
"设置对话置顶": "pinConversation", "设置分组置顶": "pinGroup", "设置分组中对话的置顶": "pinGroupConversation",
|
||||
"获取分类": "getCategories", "列出知识项": "listKnowledgeItems", "创建知识项": "createKnowledgeItem",
|
||||
"设置对话置顶": "pinConversation",
|
||||
"获取分类": "getCategories", "列出知识项": "listKnowledgeItems", "创建知识项": "createKnowledgeItem",
|
||||
"获取知识项": "getKnowledgeItem", "更新知识项": "updateKnowledgeItem", "删除知识项": "deleteKnowledgeItem",
|
||||
"获取索引状态": "getIndexStatus", "构建索引": "startKnowledgeIndex", "扫描知识库": "scanKnowledgeBase",
|
||||
"搜索知识库": "searchKnowledgeBase", "基础搜索": "basicSearch", "按风险类型搜索": "searchByRiskType",
|
||||
@@ -52,8 +49,7 @@ var apiDocI18nSummaryToKey = map[string]string{
|
||||
"删除对话轮次": "deleteConversationTurn", "获取消息过程详情": "getMessageProcessDetails",
|
||||
"重跑批量任务队列": "rerunBatchQueue", "修改队列元数据": "updateBatchQueueMetadata",
|
||||
"修改队列调度配置": "updateBatchQueueSchedule", "开关Cron自动调度": "setBatchQueueScheduleEnabled",
|
||||
"获取所有分组映射": "getAllGroupMappings",
|
||||
"FOFA搜索": "fofaSearch", "自然语言解析为FOFA语法": "fofaParse",
|
||||
"FOFA搜索": "fofaSearch", "自然语言解析为FOFA语法": "fofaParse",
|
||||
"测试OpenAI API连接": "testOpenAI",
|
||||
"执行终端命令": "terminalRun", "流式执行终端命令": "terminalRunStream", "WebSocket终端": "terminalWS",
|
||||
"列出WebShell连接": "listWebshellConnections", "创建WebShell连接": "createWebshellConnection",
|
||||
@@ -84,7 +80,6 @@ var apiDocI18nResponseDescToKey = map[string]string{
|
||||
"获取成功": "getSuccess", "未授权": "unauthorized", "未授权,需要有效的Token": "unauthorizedToken",
|
||||
"创建成功": "createSuccess", "请求参数错误": "badRequest", "对话不存在": "conversationNotFound",
|
||||
"对话不存在或结果不存在": "conversationOrResultNotFound", "请求参数错误(如task为空)": "badRequestTaskEmpty",
|
||||
"请求参数错误或分组名称已存在": "badRequestGroupNameExists", "分组不存在": "groupNotFound",
|
||||
"请求参数错误(如配置格式不正确、缺少必需字段等)": "badRequestConfig",
|
||||
"请求参数错误(如query为空)": "badRequestQueryEmpty", "方法不允许(仅支持POST请求)": "methodNotAllowed",
|
||||
"登录成功": "loginSuccess", "密码错误": "invalidPassword", "登出成功": "logoutSuccess",
|
||||
@@ -92,7 +87,7 @@ var apiDocI18nResponseDescToKey = map[string]string{
|
||||
"对话创建成功": "conversationCreated", "服务器内部错误": "internalError", "更新成功": "updateSuccess",
|
||||
"删除成功": "deleteSuccess", "队列不存在": "queueNotFound", "启动成功": "startSuccess",
|
||||
"暂停成功": "pauseSuccess", "添加成功": "addSuccess",
|
||||
"任务不存在": "taskNotFound", "对话或分组不存在": "conversationOrGroupNotFound",
|
||||
"任务不存在": "taskNotFound",
|
||||
"取消请求已提交": "cancelSubmitted", "未找到正在执行的任务": "noRunningTask",
|
||||
"消息发送成功,返回AI回复": "messageSent", "流式响应(Server-Sent Events)": "streamResponse",
|
||||
// 新增缺失端点响应
|
||||
|
||||
@@ -78,9 +78,6 @@ var PermissionCatalog = map[string]string{
|
||||
"attackchain:write": "Regenerate attack chains",
|
||||
"fofa:execute": "Run FOFA searches and query parsing",
|
||||
"openapi:read": "Read OpenAPI aggregation results",
|
||||
"group:read": "View conversation groups",
|
||||
"group:write": "Create and update conversation groups",
|
||||
"group:delete": "Delete conversation groups",
|
||||
"monitor:read": "View execution monitor",
|
||||
"monitor:write": "Cancel monitor executions",
|
||||
"monitor:delete": "Delete monitor executions",
|
||||
|
||||
@@ -122,8 +122,6 @@ func permissionForRequest(method, fullPath string) string {
|
||||
return "dashboard:read"
|
||||
case strings.HasPrefix(path, "/conversations"), strings.HasPrefix(path, "/messages"), strings.HasPrefix(path, "/process-details"):
|
||||
return crudPermission(method, "chat")
|
||||
case strings.HasPrefix(path, "/groups"):
|
||||
return crudPermission(method, "group")
|
||||
case strings.HasPrefix(path, "/monitor"):
|
||||
return crudPermission(method, "monitor")
|
||||
case strings.HasPrefix(path, "/notifications"):
|
||||
|
||||
+9
-888
File diff suppressed because it is too large
Load Diff
+51
-113
@@ -10,6 +10,7 @@
|
||||
"close": "Close",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"remove": "Remove",
|
||||
"save": "Save",
|
||||
"loading": "Loading…",
|
||||
"search": "Search",
|
||||
@@ -554,10 +555,10 @@
|
||||
"projectPreviewNoDescription": "No project description",
|
||||
"projectPreviewScope": "Test scope: {{scope}}",
|
||||
"projectPreviewEdit": "Edit project",
|
||||
"conversationPreviewJustNow": "Now",
|
||||
"conversationPreviewJustNow": "Just now",
|
||||
"conversationPreviewMinutes": "{{count}} min",
|
||||
"conversationPreviewHours": "{{count}}h",
|
||||
"conversationPreviewDays": "{{count}}d",
|
||||
"conversationPreviewHours": "{{count}} hr",
|
||||
"conversationPreviewDays": "{{count}} days",
|
||||
"conversationPreviewDateTime": "{{year}}-{{month}}-{{day}} {{hour}}:{{minute}}",
|
||||
"conversationPreviewNoProject": "No project",
|
||||
"conversationPreviewDefaultMode": "Default",
|
||||
@@ -579,8 +580,6 @@
|
||||
"renameConversationSubtitle": "The name will update in project folders and recent conversations",
|
||||
"conversationTitleLabel": "Conversation name",
|
||||
"conversationTitlePlaceholder": "Enter a conversation name",
|
||||
"conversationGroups": "Conversation groups",
|
||||
"addGroup": "New group",
|
||||
"recentConversations": "Recent conversations",
|
||||
"toggleRecentConversations": "Expand/collapse recent conversations",
|
||||
"filterByProject": "Filter by project",
|
||||
@@ -622,7 +621,6 @@
|
||||
"attachmentUploadFailed": "Failed",
|
||||
"attachmentUploadAlert": "Upload failed: {{name}}",
|
||||
"send": "Send",
|
||||
"searchInGroup": "Search in group...",
|
||||
"loadingTools": "Loading tools...",
|
||||
"noMatchTools": "No matching tools",
|
||||
"penetrationTestDetail": "Task execution details",
|
||||
@@ -656,11 +654,7 @@
|
||||
"deleteTurnTitle": "Delete this turn",
|
||||
"deleteTurnConfirm": "Delete this entire turn (user message and assistant reply)? This cannot be undone. The next reply will use only the remaining messages; saved context snapshots will be cleared.",
|
||||
"deleteTurnFailed": "Failed to delete turn",
|
||||
"emptyGroupConversations": "This group has no conversations yet.",
|
||||
"noMatchingConversationsInGroup": "No matching conversations found.",
|
||||
"noHistoryConversations": "No conversation history yet",
|
||||
"renameGroupPrompt": "Please enter new name:",
|
||||
"deleteGroupConfirm": "Are you sure you want to delete this group? Conversations in the group will not be deleted, but will be removed from the group.",
|
||||
"deleteConversationConfirm": "Delete this conversation? Chat messages cannot be recovered, but recorded vulnerabilities will remain in the vulnerability library.",
|
||||
"renameFailed": "Rename failed",
|
||||
"downloadConversationFailed": "Failed to download conversation",
|
||||
@@ -675,7 +669,6 @@
|
||||
"projectWelcomeTitleSuffix": "?",
|
||||
"noProjectWelcomeTitle": "What should be tested?",
|
||||
"welcomeSubtitle": "Enter your test requirements and the system will automatically run the corresponding security tests.",
|
||||
"addNewGroup": "+ New group",
|
||||
"callNumber": "Call #{{n}}",
|
||||
"iterationRound": "Iteration {{n}}",
|
||||
"einoOrchestratorRound": "Orchestrator · round {{n}}",
|
||||
@@ -747,10 +740,6 @@
|
||||
"historyGroupToday": "Today",
|
||||
"historyGroupLast7Days": "Past 7 days",
|
||||
"historyGroupEarlier": "Older",
|
||||
"conversationPreviewJustNow": "Just now",
|
||||
"conversationPreviewMinutes": "{{count}} min",
|
||||
"conversationPreviewHours": "{{count}} hr",
|
||||
"conversationPreviewDays": "{{count}} days",
|
||||
"agentModeSelectAria": "Choose conversation execution mode",
|
||||
"agentModePanelTitle": "Conversation mode",
|
||||
"agentModeEinoSingle": "Eino single (ADK)",
|
||||
@@ -829,7 +818,8 @@
|
||||
"hitlTimeoutTenMinutes": "10 minutes",
|
||||
"hitlTimeoutUnlimited": "No limit",
|
||||
"hitlTimeoutHint": "Unanswered requests are rejected automatically when time expires; approval cards show the countdown.",
|
||||
"hitlStatusOff": "Human-in-the-loop: Off"
|
||||
"hitlStatusOff": "Human-in-the-loop: Off",
|
||||
"rolePanelTitle": "Select role"
|
||||
},
|
||||
"hitl": {
|
||||
"pageTitle": "HITL approvals",
|
||||
@@ -870,7 +860,7 @@
|
||||
"viewEditedArgs": "View edited parameters",
|
||||
"reviewArgs": "Review parameters (JSON)",
|
||||
"commentOptional": "Comment (optional)",
|
||||
"commentPlaceholder": "For example: read-only operations only",
|
||||
"commentPlaceholder": "e.g. allow read-only command",
|
||||
"reject": "Reject",
|
||||
"allowOnce": "Allow once",
|
||||
"saveEditedAndAllow": "Save edits and allow",
|
||||
@@ -971,8 +961,6 @@
|
||||
"reviewEditHelp": "Review & edit mode: provide a JSON object to override tool arguments. Example: {\"command\":\"ls -la\"}",
|
||||
"approvalHelp": "Approval mode: only approve/reject, argument editing is disabled.",
|
||||
"commentHelp": "Comment (optional): briefly note the approval reason.",
|
||||
"commentPlaceholder": "e.g. allow read-only command",
|
||||
"reject": "Reject",
|
||||
"approve": "Approve",
|
||||
"loadFailed": "Failed to load",
|
||||
"invalidJson": "Invalid JSON arguments",
|
||||
@@ -2019,7 +2007,6 @@
|
||||
"conversationManagement": "Conversation Management",
|
||||
"conversationInteraction": "Conversation Interaction",
|
||||
"batchTasks": "Batch Tasks",
|
||||
"conversationGroups": "Conversation Groups",
|
||||
"vulnerabilityManagement": "Vulnerability Management",
|
||||
"roleManagement": "Role Management",
|
||||
"skillsManagement": "Skills Management",
|
||||
@@ -2065,14 +2052,6 @@
|
||||
"portScan": "Port scan",
|
||||
"updateBatchTask": "Update batch task",
|
||||
"deleteBatchTask": "Delete batch task",
|
||||
"createGroup": "Create group",
|
||||
"listGroups": "List groups",
|
||||
"getGroup": "Get group",
|
||||
"updateGroup": "Update group",
|
||||
"deleteGroup": "Delete group",
|
||||
"getGroupConversations": "Get conversations in group",
|
||||
"addConversationToGroup": "Add conversation to group",
|
||||
"removeConversationFromGroup": "Remove conversation from group",
|
||||
"listVulnerabilities": "List vulnerabilities",
|
||||
"createVulnerability": "Create vulnerability",
|
||||
"getVulnerabilityStats": "Get vulnerability statistics",
|
||||
@@ -2115,8 +2094,6 @@
|
||||
"getAttackChain": "Get attack chain",
|
||||
"regenerateAttackChain": "Regenerate attack chain",
|
||||
"pinConversation": "Pin conversation",
|
||||
"pinGroup": "Pin group",
|
||||
"pinGroupConversation": "Pin conversation in group",
|
||||
"getCategories": "Get categories",
|
||||
"listKnowledgeItems": "List knowledge items",
|
||||
"createKnowledgeItem": "Create knowledge item",
|
||||
@@ -2143,7 +2120,6 @@
|
||||
"updateBatchQueueMetadata": "Update queue metadata",
|
||||
"updateBatchQueueSchedule": "Update queue schedule",
|
||||
"setBatchQueueScheduleEnabled": "Toggle cron auto-schedule",
|
||||
"getAllGroupMappings": "Get all group mappings",
|
||||
"fofaSearch": "FOFA search",
|
||||
"fofaParse": "Parse natural language to FOFA syntax",
|
||||
"testOpenAI": "Test OpenAI API connection",
|
||||
@@ -2207,8 +2183,6 @@
|
||||
"conversationNotFound": "Conversation not found",
|
||||
"conversationOrResultNotFound": "Conversation or result not found",
|
||||
"badRequestTaskEmpty": "Bad request (e.g. task is empty)",
|
||||
"badRequestGroupNameExists": "Bad request or group name already exists",
|
||||
"groupNotFound": "Group not found",
|
||||
"badRequestConfig": "Bad request (e.g. invalid config or missing required fields)",
|
||||
"badRequestQueryEmpty": "Bad request (e.g. query is empty)",
|
||||
"methodNotAllowed": "Method not allowed (POST only)",
|
||||
@@ -2227,7 +2201,6 @@
|
||||
"pauseSuccess": "Paused successfully",
|
||||
"addSuccess": "Added successfully",
|
||||
"taskNotFound": "Task not found",
|
||||
"conversationOrGroupNotFound": "Conversation or group not found",
|
||||
"cancelSubmitted": "Cancel request submitted",
|
||||
"noRunningTask": "No running task found",
|
||||
"messageSent": "Message sent, AI reply returned",
|
||||
@@ -2268,23 +2241,6 @@
|
||||
"assetImportTransactionFailed": "Import transaction failed"
|
||||
}
|
||||
},
|
||||
"chatGroup": {
|
||||
"search": "Search",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"clearSearch": "Clear search",
|
||||
"searchInGroupPlaceholder": "Search in group...",
|
||||
"attackChain": "Attack chain",
|
||||
"viewAttackChain": "View attack chain",
|
||||
"selectRole": "Select role",
|
||||
"close": "Close",
|
||||
"selectFile": "Select file",
|
||||
"uploadFile": "Upload file (multi-select or drag & drop)",
|
||||
"send": "Send",
|
||||
"rolePanelTitle": "Select role",
|
||||
"copyMessage": "Copy message",
|
||||
"remove": "Remove"
|
||||
},
|
||||
"mcpMonitor": {
|
||||
"deselectAll": "Deselect all",
|
||||
"statusPending": "Pending",
|
||||
@@ -2579,16 +2535,16 @@
|
||||
"errorGeneric": "Something went wrong. Please try again."
|
||||
},
|
||||
"vulnerabilityPage": {
|
||||
"alertTitle": "Robot vulnerability alerts",
|
||||
"alertDescription": "Push newly discovered vulnerabilities to robots bound to this account, filtered by severity.",
|
||||
"alertHint": "Send newly discovered vulnerabilities at or above this severity to your bound robot accounts.",
|
||||
"alertConfiguredNotBound": "{{platforms}} is enabled, but this Web account is not bound to a recipient identity.",
|
||||
"alertNoBinding": "No proactive robot is both enabled and bound. Your settings are still saved.",
|
||||
"alertBindAction": "Bind recipient account",
|
||||
"alertMinimum": "Minimum severity",
|
||||
"alertEnabled": "Enable alerts",
|
||||
"alertSaved": "Vulnerability alert settings saved",
|
||||
"alertSaveFailed": "Failed to save vulnerability alert settings",
|
||||
"alertTitle": "Robot vulnerability alerts",
|
||||
"alertDescription": "Push newly discovered vulnerabilities to robots bound to this account, filtered by severity.",
|
||||
"alertHint": "Send newly discovered vulnerabilities at or above this severity to your bound robot accounts.",
|
||||
"alertConfiguredNotBound": "{{platforms}} is enabled, but this Web account is not bound to a recipient identity.",
|
||||
"alertNoBinding": "No proactive robot is both enabled and bound. Your settings are still saved.",
|
||||
"alertBindAction": "Bind recipient account",
|
||||
"alertMinimum": "Minimum severity",
|
||||
"alertEnabled": "Enable alerts",
|
||||
"alertSaved": "Vulnerability alert settings saved",
|
||||
"alertSaveFailed": "Failed to save vulnerability alert settings",
|
||||
"statTotal": "Total",
|
||||
"statClickAll": "View all (clear severity filter)",
|
||||
"statClickFilter": "Click to filter by this severity; click again to clear",
|
||||
@@ -3376,50 +3332,18 @@
|
||||
"searchPlaceholder": "Search history",
|
||||
"conversationName": "Conversation name",
|
||||
"project": "Project",
|
||||
"group": "Group",
|
||||
"noProject": "No project",
|
||||
"unknownProject": "Unknown project",
|
||||
"noGroup": "Ungrouped",
|
||||
"unknownGroup": "Unknown group",
|
||||
"filterByProject": "Filter by project",
|
||||
"filterByGroup": "Filter by group",
|
||||
"filterAllGroups": "All groups",
|
||||
"filterUngrouped": "Ungrouped",
|
||||
"lastTime": "Last activity",
|
||||
"action": "Action",
|
||||
"selectAll": "Select all",
|
||||
"setGroup": "Set group",
|
||||
"noGroupOption": "Ungrouped",
|
||||
"deleteSelected": "Delete selected",
|
||||
"confirmDeleteNone": "Please select at least one conversation to delete",
|
||||
"confirmDeleteN": "Delete {{count}} selected conversation(s)? Chat messages cannot be recovered, but recorded vulnerabilities will remain in the vulnerability library.",
|
||||
"confirmGroupChangeNone": "Please select at least one conversation",
|
||||
"confirmMoveN": "Move {{count}} selected conversation(s) to \"{{group}}\"?",
|
||||
"confirmRemoveNoGroup": "None of the selected conversations belong to a group",
|
||||
"confirmRemoveN": "Remove {{count}} selected conversation(s) from their group(s)?",
|
||||
"removeFailed": "Remove failed",
|
||||
"moveFailed": "Move failed",
|
||||
"deleteFailed": "Delete failed",
|
||||
"unnamedConversation": "Unnamed conversation"
|
||||
},
|
||||
"createGroupModal": {
|
||||
"title": "Create group",
|
||||
"description": "Group conversations for easier management.",
|
||||
"selectIcon": "Click to choose icon",
|
||||
"groupNamePlaceholder": "Enter group name",
|
||||
"pickIcon": "Pick icon",
|
||||
"customIcon": "Custom",
|
||||
"confirmIcon": "OK",
|
||||
"create": "Create",
|
||||
"cancel": "Cancel",
|
||||
"suggestionPenetrationTest": "Penetration Testing",
|
||||
"suggestionCtf": "CTF",
|
||||
"suggestionRedTeam": "Red Team",
|
||||
"suggestionVulnerabilityMining": "Vulnerability Mining",
|
||||
"nameExists": "Group name already exists, please use another name.",
|
||||
"createFailed": "Create failed",
|
||||
"unknownError": "Unknown error"
|
||||
},
|
||||
"contextMenu": {
|
||||
"viewAttackChain": "View attack chain",
|
||||
"viewVulnerabilities": "View vulnerabilities",
|
||||
@@ -3430,11 +3354,7 @@
|
||||
"pinConversation": "Pin conversation",
|
||||
"unpinConversation": "Unpin",
|
||||
"batchManage": "Batch manage",
|
||||
"moveToGroup": "Move to group",
|
||||
"deleteConversation": "Delete conversation",
|
||||
"pinGroup": "Pin group",
|
||||
"unpinGroup": "Unpin",
|
||||
"deleteGroup": "Delete group"
|
||||
"deleteConversation": "Delete conversation"
|
||||
},
|
||||
"batchImportModal": {
|
||||
"title": "New task",
|
||||
@@ -4565,10 +4485,22 @@
|
||||
}
|
||||
},
|
||||
"systemRoles": {
|
||||
"admin": { "name": "Administrator", "description": "Full platform administration access" },
|
||||
"operator": { "name": "Operator", "description": "Run daily security workflows without account or core configuration management" },
|
||||
"auditor": { "name": "Auditor", "description": "Read-only access to audits, monitoring, and assets" },
|
||||
"viewer": { "name": "Read-only User", "description": "Read-only access to explicitly granted resources" }
|
||||
"admin": {
|
||||
"name": "Administrator",
|
||||
"description": "Full platform administration access"
|
||||
},
|
||||
"operator": {
|
||||
"name": "Operator",
|
||||
"description": "Run daily security workflows without account or core configuration management"
|
||||
},
|
||||
"auditor": {
|
||||
"name": "Auditor",
|
||||
"description": "Read-only access to audits, monitoring, and assets"
|
||||
},
|
||||
"viewer": {
|
||||
"name": "Read-only User",
|
||||
"description": "Read-only access to explicitly granted resources"
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
"noMatchingUsers": "No matching members",
|
||||
@@ -4624,7 +4556,6 @@
|
||||
"dashboard": "Dashboard",
|
||||
"files": "Files",
|
||||
"fofa": "FOFA",
|
||||
"group": "Conversation Groups",
|
||||
"hitl": "Human-in-the-loop",
|
||||
"knowledge": "Knowledge Base",
|
||||
"mcp": "MCP",
|
||||
@@ -4643,14 +4574,20 @@
|
||||
"workflow": "Workflows"
|
||||
},
|
||||
"permissionDescriptions": {
|
||||
"auth": { "self": "Manage own session and password" },
|
||||
"dashboard": { "read": "View dashboard summaries" },
|
||||
"auth": {
|
||||
"self": "Manage own session and password"
|
||||
},
|
||||
"dashboard": {
|
||||
"read": "View dashboard summaries"
|
||||
},
|
||||
"chat": {
|
||||
"read": "View conversations",
|
||||
"write": "Create and update conversations",
|
||||
"delete": "Delete conversations and turns"
|
||||
},
|
||||
"agent": { "execute": "Run AI agents and workflows" },
|
||||
"agent": {
|
||||
"execute": "Run AI agents and workflows"
|
||||
},
|
||||
"hitl": {
|
||||
"read": "View human-in-the-loop queues and logs",
|
||||
"write": "Approve, dismiss, and configure human-in-the-loop requests"
|
||||
@@ -4713,7 +4650,9 @@
|
||||
"read": "View system configuration",
|
||||
"write": "Update and apply system configuration"
|
||||
},
|
||||
"terminal": { "execute": "Execute terminal commands" },
|
||||
"terminal": {
|
||||
"execute": "Execute terminal commands"
|
||||
},
|
||||
"audit": {
|
||||
"read": "View and export audit logs",
|
||||
"delete": "Delete audit logs"
|
||||
@@ -4739,12 +4678,11 @@
|
||||
"read": "View attack chains",
|
||||
"write": "Regenerate attack chains"
|
||||
},
|
||||
"fofa": { "execute": "Run FOFA searches and parse queries" },
|
||||
"openapi": { "read": "Read OpenAPI aggregation results" },
|
||||
"group": {
|
||||
"read": "View conversation groups",
|
||||
"write": "Create and update conversation groups",
|
||||
"delete": "Delete conversation groups"
|
||||
"fofa": {
|
||||
"execute": "Run FOFA searches and parse queries"
|
||||
},
|
||||
"openapi": {
|
||||
"read": "Read OpenAPI aggregation results"
|
||||
},
|
||||
"monitor": {
|
||||
"read": "View the execution monitor",
|
||||
|
||||
+48
-110
@@ -10,6 +10,7 @@
|
||||
"close": "关闭",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"remove": "移除",
|
||||
"save": "保存",
|
||||
"loading": "加载中…",
|
||||
"search": "搜索",
|
||||
@@ -567,8 +568,6 @@
|
||||
"renameConversationSubtitle": "修改后会同步更新项目文件夹和最近对话中的名称",
|
||||
"conversationTitleLabel": "对话名称",
|
||||
"conversationTitlePlaceholder": "请输入对话名称",
|
||||
"conversationGroups": "对话分组",
|
||||
"addGroup": "新建分组",
|
||||
"recentConversations": "最近对话",
|
||||
"toggleRecentConversations": "展开/折叠最近对话",
|
||||
"filterByProject": "按项目筛选",
|
||||
@@ -610,7 +609,6 @@
|
||||
"attachmentUploadFailed": "失败",
|
||||
"attachmentUploadAlert": "上传失败:{{name}}",
|
||||
"send": "发送",
|
||||
"searchInGroup": "搜索分组中的对话...",
|
||||
"loadingTools": "正在加载工具...",
|
||||
"noMatchTools": "没有匹配的工具",
|
||||
"penetrationTestDetail": "任务执行详情",
|
||||
@@ -644,11 +642,7 @@
|
||||
"deleteTurnTitle": "删除本轮对话",
|
||||
"deleteTurnConfirm": "确定删除本轮对话?将同时删除该轮用户消息与助手回复,且无法恢复;下次模型回复将仅基于剩余消息(已保存的上下文快照会清空并按剩余内容重建)。",
|
||||
"deleteTurnFailed": "删除本轮失败",
|
||||
"emptyGroupConversations": "该分组暂无对话",
|
||||
"noMatchingConversationsInGroup": "未找到匹配的对话",
|
||||
"noHistoryConversations": "暂无历史对话",
|
||||
"renameGroupPrompt": "请输入新名称:",
|
||||
"deleteGroupConfirm": "确定要删除此分组吗?分组中的对话不会被删除,但会从分组中移除。",
|
||||
"deleteConversationConfirm": "确定要删除此对话吗?对话消息将不可恢复,但已记录的漏洞会保留在漏洞库中。",
|
||||
"renameFailed": "重命名失败",
|
||||
"downloadConversationFailed": "下载对话失败",
|
||||
@@ -663,7 +657,6 @@
|
||||
"projectWelcomeTitleSuffix": " 项目中测试什么?",
|
||||
"noProjectWelcomeTitle": "要测试什么?",
|
||||
"welcomeSubtitle": "请输入您的测试需求,系统将自动执行相应的安全测试。",
|
||||
"addNewGroup": "+ 新增分组",
|
||||
"callNumber": "调用 #{{n}}",
|
||||
"iterationRound": "第 {{n}} 轮迭代",
|
||||
"einoOrchestratorRound": "主代理 · 第 {{n}} 轮",
|
||||
@@ -735,10 +728,6 @@
|
||||
"historyGroupToday": "今天",
|
||||
"historyGroupLast7Days": "过去七天",
|
||||
"historyGroupEarlier": "更早",
|
||||
"conversationPreviewJustNow": "刚刚",
|
||||
"conversationPreviewMinutes": "{{count}} 分钟",
|
||||
"conversationPreviewHours": "{{count}} 小时",
|
||||
"conversationPreviewDays": "{{count}} 天",
|
||||
"agentModeSelectAria": "选择对话执行模式",
|
||||
"agentModePanelTitle": "对话模式",
|
||||
"agentModeEinoSingle": "Eino 单代理(ADK)",
|
||||
@@ -817,7 +806,8 @@
|
||||
"hitlTimeoutTenMinutes": "10 分钟",
|
||||
"hitlTimeoutUnlimited": "不限制",
|
||||
"hitlTimeoutHint": "到期未处理将自动拒绝;审批卡片会显示倒计时。",
|
||||
"hitlStatusOff": "人机协同:关闭"
|
||||
"hitlStatusOff": "人机协同:关闭",
|
||||
"rolePanelTitle": "选择角色"
|
||||
},
|
||||
"hitl": {
|
||||
"pageTitle": "人机协同审批",
|
||||
@@ -858,7 +848,7 @@
|
||||
"viewEditedArgs": "查看修改后的参数",
|
||||
"reviewArgs": "审查参数(JSON)",
|
||||
"commentOptional": "备注(可选)",
|
||||
"commentPlaceholder": "例如:仅允许只读操作",
|
||||
"commentPlaceholder": "例如:允许只读命令",
|
||||
"reject": "拒绝",
|
||||
"allowOnce": "允许一次",
|
||||
"saveEditedAndAllow": "保存修改并允许",
|
||||
@@ -959,8 +949,6 @@
|
||||
"reviewEditHelp": "审查编辑模式:可填写 JSON 对象覆盖参数。示例:{\"command\":\"ls -la\"}",
|
||||
"approvalHelp": "审批模式:仅通过/拒绝,不支持改参。",
|
||||
"commentHelp": "备注(可选):建议写审批依据。",
|
||||
"commentPlaceholder": "例如:允许只读命令",
|
||||
"reject": "拒绝",
|
||||
"approve": "通过",
|
||||
"loadFailed": "加载失败",
|
||||
"invalidJson": "JSON 参数格式错误",
|
||||
@@ -2007,7 +1995,6 @@
|
||||
"conversationManagement": "对话管理",
|
||||
"conversationInteraction": "对话交互",
|
||||
"batchTasks": "批量任务",
|
||||
"conversationGroups": "对话分组",
|
||||
"vulnerabilityManagement": "漏洞管理",
|
||||
"roleManagement": "角色管理",
|
||||
"skillsManagement": "Skills管理",
|
||||
@@ -2053,14 +2040,6 @@
|
||||
"portScan": "端口扫描",
|
||||
"updateBatchTask": "更新批量任务",
|
||||
"deleteBatchTask": "删除批量任务",
|
||||
"createGroup": "创建分组",
|
||||
"listGroups": "列出分组",
|
||||
"getGroup": "获取分组",
|
||||
"updateGroup": "更新分组",
|
||||
"deleteGroup": "删除分组",
|
||||
"getGroupConversations": "获取分组中的对话",
|
||||
"addConversationToGroup": "添加对话到分组",
|
||||
"removeConversationFromGroup": "从分组移除对话",
|
||||
"listVulnerabilities": "列出漏洞",
|
||||
"createVulnerability": "创建漏洞",
|
||||
"getVulnerabilityStats": "获取漏洞统计",
|
||||
@@ -2103,8 +2082,6 @@
|
||||
"getAttackChain": "获取攻击链",
|
||||
"regenerateAttackChain": "重新生成攻击链",
|
||||
"pinConversation": "设置对话置顶",
|
||||
"pinGroup": "设置分组置顶",
|
||||
"pinGroupConversation": "设置分组中对话的置顶",
|
||||
"getCategories": "获取分类",
|
||||
"listKnowledgeItems": "列出知识项",
|
||||
"createKnowledgeItem": "创建知识项",
|
||||
@@ -2131,7 +2108,6 @@
|
||||
"updateBatchQueueMetadata": "修改队列元数据",
|
||||
"updateBatchQueueSchedule": "修改队列调度配置",
|
||||
"setBatchQueueScheduleEnabled": "开关Cron自动调度",
|
||||
"getAllGroupMappings": "获取所有分组映射",
|
||||
"fofaSearch": "FOFA搜索",
|
||||
"fofaParse": "自然语言解析为FOFA语法",
|
||||
"testOpenAI": "测试OpenAI API连接",
|
||||
@@ -2195,8 +2171,6 @@
|
||||
"conversationNotFound": "对话不存在",
|
||||
"conversationOrResultNotFound": "对话不存在或结果不存在",
|
||||
"badRequestTaskEmpty": "请求参数错误(如task为空)",
|
||||
"badRequestGroupNameExists": "请求参数错误或分组名称已存在",
|
||||
"groupNotFound": "分组不存在",
|
||||
"badRequestConfig": "请求参数错误(如配置格式不正确、缺少必需字段等)",
|
||||
"badRequestQueryEmpty": "请求参数错误(如query为空)",
|
||||
"methodNotAllowed": "方法不允许(仅支持POST请求)",
|
||||
@@ -2215,7 +2189,6 @@
|
||||
"pauseSuccess": "暂停成功",
|
||||
"addSuccess": "添加成功",
|
||||
"taskNotFound": "任务不存在",
|
||||
"conversationOrGroupNotFound": "对话或分组不存在",
|
||||
"cancelSubmitted": "取消请求已提交",
|
||||
"noRunningTask": "未找到正在执行的任务",
|
||||
"messageSent": "消息发送成功,返回AI回复",
|
||||
@@ -2256,23 +2229,6 @@
|
||||
"assetImportTransactionFailed": "导入事务失败"
|
||||
}
|
||||
},
|
||||
"chatGroup": {
|
||||
"search": "搜索",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"clearSearch": "清除搜索",
|
||||
"searchInGroupPlaceholder": "搜索分组中的对话...",
|
||||
"attackChain": "攻击链",
|
||||
"viewAttackChain": "查看攻击链",
|
||||
"selectRole": "选择角色",
|
||||
"close": "关闭",
|
||||
"selectFile": "选择文件",
|
||||
"uploadFile": "上传文件(可多选或拖拽到此处)",
|
||||
"send": "发送",
|
||||
"rolePanelTitle": "选择角色",
|
||||
"copyMessage": "复制消息内容",
|
||||
"remove": "移除"
|
||||
},
|
||||
"mcpMonitor": {
|
||||
"deselectAll": "取消全选",
|
||||
"statusPending": "等待中",
|
||||
@@ -2567,16 +2523,16 @@
|
||||
"errorGeneric": "操作失败,请稍后重试。"
|
||||
},
|
||||
"vulnerabilityPage": {
|
||||
"alertTitle": "机器人漏洞提醒",
|
||||
"alertDescription": "按严重级别将新发现的漏洞推送至当前账号绑定的机器人。",
|
||||
"alertHint": "发现符合级别的新漏洞后,通过已绑定机器人账号推送。",
|
||||
"alertConfiguredNotBound": "{{platforms}}已启用,但当前 Web 账号尚未绑定对应的接收身份。",
|
||||
"alertNoBinding": "尚未启用并绑定支持主动推送的机器人。设置仍会保存。",
|
||||
"alertBindAction": "绑定接收账号",
|
||||
"alertMinimum": "最低级别",
|
||||
"alertEnabled": "启用提醒",
|
||||
"alertSaved": "漏洞提醒设置已保存",
|
||||
"alertSaveFailed": "保存漏洞提醒设置失败",
|
||||
"alertTitle": "机器人漏洞提醒",
|
||||
"alertDescription": "按严重级别将新发现的漏洞推送至当前账号绑定的机器人。",
|
||||
"alertHint": "发现符合级别的新漏洞后,通过已绑定机器人账号推送。",
|
||||
"alertConfiguredNotBound": "{{platforms}}已启用,但当前 Web 账号尚未绑定对应的接收身份。",
|
||||
"alertNoBinding": "尚未启用并绑定支持主动推送的机器人。设置仍会保存。",
|
||||
"alertBindAction": "绑定接收账号",
|
||||
"alertMinimum": "最低级别",
|
||||
"alertEnabled": "启用提醒",
|
||||
"alertSaved": "漏洞提醒设置已保存",
|
||||
"alertSaveFailed": "保存漏洞提醒设置失败",
|
||||
"statTotal": "总漏洞数",
|
||||
"statClickAll": "查看全部(清除严重度筛选)",
|
||||
"statClickFilter": "点击按此严重度筛选;再次点击清除",
|
||||
@@ -3364,50 +3320,18 @@
|
||||
"searchPlaceholder": "搜索历史记录",
|
||||
"conversationName": "对话名称",
|
||||
"project": "项目",
|
||||
"group": "对话分组",
|
||||
"noProject": "无项目",
|
||||
"unknownProject": "未知项目",
|
||||
"noGroup": "无分组",
|
||||
"unknownGroup": "未知分组",
|
||||
"filterByProject": "按项目筛选",
|
||||
"filterByGroup": "按分组筛选",
|
||||
"filterAllGroups": "全部分组",
|
||||
"filterUngrouped": "无分组",
|
||||
"lastTime": "最近一次对话时间",
|
||||
"action": "操作",
|
||||
"selectAll": "全选",
|
||||
"setGroup": "设置分组",
|
||||
"noGroupOption": "无分组",
|
||||
"deleteSelected": "删除所选",
|
||||
"confirmDeleteNone": "请先选择要删除的对话",
|
||||
"confirmDeleteN": "确定要删除选中的 {{count}} 条对话吗?对话消息将不可恢复,但已记录的漏洞会保留在漏洞库中。",
|
||||
"confirmGroupChangeNone": "请先选择要操作的对话",
|
||||
"confirmMoveN": "确定将选中的 {{count}} 条对话移动到「{{group}}」吗?",
|
||||
"confirmRemoveNoGroup": "所选对话均未归属分组",
|
||||
"confirmRemoveN": "确定将选中的 {{count}} 条对话移出分组吗?",
|
||||
"removeFailed": "移出失败",
|
||||
"moveFailed": "移动失败",
|
||||
"deleteFailed": "删除失败",
|
||||
"unnamedConversation": "未命名对话"
|
||||
},
|
||||
"createGroupModal": {
|
||||
"title": "创建分组",
|
||||
"description": "分组功能可将对话集中归类管理,让对话更加井然有序。",
|
||||
"selectIcon": "点击选择图标",
|
||||
"groupNamePlaceholder": "请输入分组名称",
|
||||
"pickIcon": "选择图标",
|
||||
"customIcon": "自定义",
|
||||
"confirmIcon": "确定",
|
||||
"create": "创建",
|
||||
"cancel": "取消",
|
||||
"suggestionPenetrationTest": "渗透测试",
|
||||
"suggestionCtf": "CTF",
|
||||
"suggestionRedTeam": "红队",
|
||||
"suggestionVulnerabilityMining": "漏洞挖掘",
|
||||
"nameExists": "分组名称已存在,请使用其他名称",
|
||||
"createFailed": "创建失败",
|
||||
"unknownError": "未知错误"
|
||||
},
|
||||
"contextMenu": {
|
||||
"viewAttackChain": "查看攻击链",
|
||||
"viewVulnerabilities": "查看漏洞",
|
||||
@@ -3418,11 +3342,7 @@
|
||||
"pinConversation": "置顶此对话",
|
||||
"unpinConversation": "取消置顶",
|
||||
"batchManage": "批量管理",
|
||||
"moveToGroup": "移动到分组",
|
||||
"deleteConversation": "删除此对话",
|
||||
"pinGroup": "置顶此分组",
|
||||
"unpinGroup": "取消置顶",
|
||||
"deleteGroup": "删除此分组"
|
||||
"deleteConversation": "删除此对话"
|
||||
},
|
||||
"batchImportModal": {
|
||||
"title": "新建任务",
|
||||
@@ -4553,10 +4473,22 @@
|
||||
}
|
||||
},
|
||||
"systemRoles": {
|
||||
"admin": { "name": "管理员", "description": "全局管理权限" },
|
||||
"operator": { "name": "操作员", "description": "可执行日常安全工作流,不能管理账号与核心配置" },
|
||||
"auditor": { "name": "审计员", "description": "只读查看审计、监控与资产" },
|
||||
"viewer": { "name": "只读用户", "description": "只读查看被授权资源" }
|
||||
"admin": {
|
||||
"name": "管理员",
|
||||
"description": "全局管理权限"
|
||||
},
|
||||
"operator": {
|
||||
"name": "操作员",
|
||||
"description": "可执行日常安全工作流,不能管理账号与核心配置"
|
||||
},
|
||||
"auditor": {
|
||||
"name": "审计员",
|
||||
"description": "只读查看审计、监控与资产"
|
||||
},
|
||||
"viewer": {
|
||||
"name": "只读用户",
|
||||
"description": "只读查看被授权资源"
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
"noMatchingUsers": "没有匹配的成员",
|
||||
@@ -4612,7 +4544,6 @@
|
||||
"dashboard": "仪表盘",
|
||||
"files": "文件",
|
||||
"fofa": "FOFA",
|
||||
"group": "对话分组",
|
||||
"hitl": "人机协同",
|
||||
"knowledge": "知识库",
|
||||
"mcp": "MCP",
|
||||
@@ -4631,14 +4562,20 @@
|
||||
"workflow": "工作流"
|
||||
},
|
||||
"permissionDescriptions": {
|
||||
"auth": { "self": "管理自己的会话和密码" },
|
||||
"dashboard": { "read": "查看仪表盘汇总" },
|
||||
"auth": {
|
||||
"self": "管理自己的会话和密码"
|
||||
},
|
||||
"dashboard": {
|
||||
"read": "查看仪表盘汇总"
|
||||
},
|
||||
"chat": {
|
||||
"read": "查看对话",
|
||||
"write": "创建和更新对话",
|
||||
"delete": "删除对话和消息轮次"
|
||||
},
|
||||
"agent": { "execute": "运行 AI 智能体和工作流" },
|
||||
"agent": {
|
||||
"execute": "运行 AI 智能体和工作流"
|
||||
},
|
||||
"hitl": {
|
||||
"read": "查看人机协同队列和日志",
|
||||
"write": "审批、驳回和配置人机协同"
|
||||
@@ -4701,7 +4638,9 @@
|
||||
"read": "查看系统配置",
|
||||
"write": "更新并应用系统配置"
|
||||
},
|
||||
"terminal": { "execute": "执行终端命令" },
|
||||
"terminal": {
|
||||
"execute": "执行终端命令"
|
||||
},
|
||||
"audit": {
|
||||
"read": "查看和导出审计日志",
|
||||
"delete": "删除审计日志"
|
||||
@@ -4727,12 +4666,11 @@
|
||||
"read": "查看攻击链",
|
||||
"write": "重新生成攻击链"
|
||||
},
|
||||
"fofa": { "execute": "执行 FOFA 搜索和查询解析" },
|
||||
"openapi": { "read": "读取 OpenAPI 聚合结果" },
|
||||
"group": {
|
||||
"read": "查看对话分组",
|
||||
"write": "创建和更新对话分组",
|
||||
"delete": "删除对话分组"
|
||||
"fofa": {
|
||||
"execute": "执行 FOFA 搜索和查询解析"
|
||||
},
|
||||
"openapi": {
|
||||
"read": "读取 OpenAPI 聚合结果"
|
||||
},
|
||||
"monitor": {
|
||||
"read": "查看执行监控",
|
||||
|
||||
+279
-2286
File diff suppressed because it is too large
Load Diff
@@ -8,18 +8,20 @@ const template = fs.readFileSync('web/templates/index.html', 'utf8');
|
||||
|
||||
function functionSource(source, name, nextName) {
|
||||
const start = source.indexOf(`function ${name}(`);
|
||||
const end = source.indexOf(`function ${nextName}(`, start);
|
||||
const end = nextName ? source.indexOf(`function ${nextName}(`, start) : source.length;
|
||||
assert.notEqual(start, -1, `${name} should exist`);
|
||||
assert.notEqual(end, -1, `${nextName} should follow ${name}`);
|
||||
if (nextName) {
|
||||
assert.notEqual(end, -1, `${nextName} should follow ${name}`);
|
||||
}
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
test('全局置顶检查接口结果并即时通知项目文件夹', () => {
|
||||
const source = functionSource(chat, 'pinConversation', 'showMoveToGroupSubmenu');
|
||||
const source = functionSource(chat, 'pinConversation');
|
||||
|
||||
assert.match(source, /assertConversationActionResponse\(updateResponse, '更新置顶状态失败'\)/);
|
||||
assert.match(source, /notifyConversationPinnedChanged\(convId, newPinned\)/);
|
||||
assert.match(source, /loadConversationsWithGroups\(\)/);
|
||||
assert.match(source, /loadConversations\(\)/);
|
||||
});
|
||||
|
||||
test('项目文件夹内置顶对话优先排序并显示图钉', () => {
|
||||
@@ -58,17 +60,20 @@ test('项目文件夹菜单可以置顶并立即更新排序', () => {
|
||||
assert.match(projects, /\[\.\.\.pinnedProjects, unassignedProject, \.\.\.regularProjects\]/);
|
||||
});
|
||||
|
||||
test('对话侧栏不再显示对话分组区域', () => {
|
||||
test('对话侧栏只保留最近对话区域', () => {
|
||||
assert.doesNotMatch(template, /class="conversation-groups-section"/);
|
||||
assert.doesNotMatch(template, /id="conversation-groups-list"/);
|
||||
});
|
||||
|
||||
test('删除对话分组检查接口结果并先清理本地状态', () => {
|
||||
const deleteSource = functionSource(chat, 'deleteConversationGroupById', 'deleteGroup');
|
||||
const contextSource = functionSource(chat, 'deleteGroupFromContext', 'closeGroupContextMenu');
|
||||
test('对话三点菜单仍绑定打开上下文菜单', () => {
|
||||
const itemSource = functionSource(chat, 'createConversationListItemWithMenu', 'openConversationContextMenuForId');
|
||||
const menuSource = functionSource(chat, 'showConversationContextMenu', 'ensureConversationRenameModal');
|
||||
|
||||
assert.match(deleteSource, /assertConversationActionResponse\(deleteResponse, '删除分组失败'\)/);
|
||||
assert.match(deleteSource, /removeConversationGroupFromLocalState\(groupId\)/);
|
||||
assert.match(deleteSource, /if \(currentGroupId === groupId\) exitGroupDetail\(\)/);
|
||||
assert.match(contextSource, /deleteConversationGroupById\(groupId, \{ closeContextMenu: true \}\)/);
|
||||
assert.match(itemSource, /menuBtn\.onclick = \(e\) => openConversationContextMenuForId\(e, conversation\.id, conversation\.title \|\| ''\)/);
|
||||
assert.match(menuSource, /const menu = document\.getElementById\('conversation-context-menu'\)/);
|
||||
assert.match(menuSource, /menu\.style\.display = 'block'/);
|
||||
assert.match(chat, /function clearDownloadMarkdownSubmenuHideTimeout\(/);
|
||||
assert.match(chat, /function handleDownloadMarkdownSubmenuEnter\(/);
|
||||
assert.match(chat, /function handleDownloadMarkdownSubmenuLeave\(/);
|
||||
assert.match(chat, /function hideDownloadMarkdownSubmenu\(/);
|
||||
});
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
'skill-modal',
|
||||
'agent-md-modal',
|
||||
'batch-manage-modal',
|
||||
'create-group-modal',
|
||||
'workflow-meta-modal',
|
||||
'workflow-dry-run-modal',
|
||||
'login-overlay',
|
||||
|
||||
@@ -2999,11 +2999,9 @@ function handleStreamEvent(event, progressElement, progressId,
|
||||
loadActiveTasks();
|
||||
// 延迟刷新对话列表,确保用户消息已保存,updated_at已更新
|
||||
// 这样新对话才能正确显示在最近对话列表的顶部
|
||||
// 使用loadConversationsWithGroups确保分组映射缓存正确加载,无论是否有分组都能立即显示
|
||||
// 刷新最近对话列表
|
||||
setTimeout(() => {
|
||||
if (typeof loadConversationsWithGroups === 'function') {
|
||||
loadConversationsWithGroups();
|
||||
} else if (typeof loadConversations === 'function') {
|
||||
if (typeof loadConversations === 'function') {
|
||||
loadConversations();
|
||||
}
|
||||
if (typeof window.refreshChatProjectFolders === 'function') {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
'use strict';
|
||||
|
||||
const GLOBAL_WRITE_HANDLER_PERMISSIONS = {
|
||||
// 对话 / 分组
|
||||
// 对话
|
||||
sendMessage: 'chat:write',
|
||||
startNewConversation: 'chat:write',
|
||||
deleteConversation: 'chat:delete',
|
||||
@@ -16,15 +16,6 @@
|
||||
deleteSelectedConversations: 'chat:delete',
|
||||
renameConversation: 'chat:write',
|
||||
pinConversation: 'chat:write',
|
||||
showCreateGroupModal: 'group:write',
|
||||
createGroup: 'group:write',
|
||||
editGroup: 'group:write',
|
||||
deleteGroup: 'group:delete',
|
||||
deleteGroupFromContext: 'group:delete',
|
||||
pinGroupFromContext: 'group:write',
|
||||
renameGroupFromContext: 'group:write',
|
||||
applyBatchGroupChange: 'group:write',
|
||||
applyCustomIcon: 'group:write',
|
||||
|
||||
// 人机协同
|
||||
applyHitlSidebarConfig: 'hitl:write',
|
||||
|
||||
@@ -2557,7 +2557,7 @@ function selectWebshell(id, stateReady) {
|
||||
'<svg class="role-selector-arrow" width="10" height="10" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' +
|
||||
'</button>' +
|
||||
'<div id="ws-role-selection-panel" class="role-selection-panel" style="display:none;">' +
|
||||
'<div class="role-selection-panel-header"><h3 class="role-selection-panel-title">' + (wsT('chatGroup.rolePanelTitle') || '选择角色') + '</h3>' +
|
||||
'<div class="role-selection-panel-header"><h3 class="role-selection-panel-title">' + (wsT('chat.rolePanelTitle') || '选择角色') + '</h3>' +
|
||||
'<button type="button" class="role-selection-panel-close" onclick="wsCloseRolePanel()"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>' +
|
||||
'</div><div id="ws-role-selection-list" class="role-selection-list-main"></div></div>' +
|
||||
'</div>' +
|
||||
|
||||
+2
-154
@@ -1084,51 +1084,6 @@
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 分组详情页面 -->
|
||||
<div id="group-detail-page" class="group-detail-page" style="display: none;">
|
||||
<div class="group-detail-header">
|
||||
<button class="back-btn" onclick="exitGroupDetail()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 12H5M12 19l-7-7 7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<h2 id="group-detail-title" class="group-detail-title"></h2>
|
||||
<div class="group-detail-actions">
|
||||
<button class="group-action-btn" onclick="toggleGroupSearch()" data-i18n="chatGroup.search" data-i18n-attr="title" title="搜索" id="group-search-toggle-btn">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="11" cy="11" r="8" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="m21 21-4.35-4.35" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="group-action-btn" data-require-permission="group:write" onclick="editGroup()" data-i18n="chatGroup.edit" data-i18n-attr="title" title="编辑">
|
||||
<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="group-action-btn delete-btn" data-require-permission="group:delete" onclick="deleteGroup()" data-i18n="chatGroup.delete" data-i18n-attr="title" title="删除">
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="group-search-container" class="group-search-container" style="display: none;">
|
||||
<div class="group-search-input-wrapper">
|
||||
<input type="text" id="group-search-input" class="group-search-input" data-i18n="chat.searchInGroup" data-i18n-attr="placeholder" placeholder="搜索分组中的对话..." onkeyup="handleGroupSearchInput(event)" oninput="handleGroupSearchInput(event)">
|
||||
<button class="group-search-clear-btn" onclick="clearGroupSearch()" data-i18n="common.clearSearch" data-i18n-attr="title" title="清除搜索" id="group-search-clear-btn" style="display: none;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="m8 8 8 8M16 8l-8 8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="group-detail-content">
|
||||
<div id="group-conversations-list" class="group-conversations-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 对话界面 -->
|
||||
<div class="chat-container">
|
||||
<!-- 会话顶部栏(只在有会话选中时显示) -->
|
||||
@@ -1210,7 +1165,7 @@
|
||||
<!-- 角色选择下拉面板 -->
|
||||
<div id="role-selection-panel" class="role-selection-panel" style="display: none;">
|
||||
<div class="role-selection-panel-header">
|
||||
<h3 class="role-selection-panel-title" data-i18n="chatGroup.rolePanelTitle">选择角色</h3>
|
||||
<h3 class="role-selection-panel-title" data-i18n="chat.rolePanelTitle">选择角色</h3>
|
||||
<button class="role-selection-panel-close" onclick="closeRoleSelectionPanel()" data-i18n="common.close" data-i18n-attr="title" title="关闭">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
@@ -5923,10 +5878,6 @@
|
||||
<option value="" data-i18n="chat.filterAllProjects">全部项目</option>
|
||||
<option value="__none__" data-i18n="chat.filterUnboundProjects">未绑定项目</option>
|
||||
</select>
|
||||
<select id="batch-group-filter" class="conversation-project-filter-native" onchange="applyBatchConversationFilters()" data-i18n="batchManageModal.filterByGroup" data-i18n-attr="title" title="按分组筛选">
|
||||
<option value="" data-i18n="batchManageModal.filterAllGroups">全部分组</option>
|
||||
<option value="__none__" data-i18n="batchManageModal.filterUngrouped">无分组</option>
|
||||
</select>
|
||||
<div class="batch-search-box">
|
||||
<input type="text" id="batch-search-input" data-i18n="batchManageModal.searchPlaceholder" data-i18n-attr="placeholder" placeholder="搜索历史记录" oninput="filterBatchConversations(this.value)" />
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -5945,7 +5896,6 @@
|
||||
</div>
|
||||
<div class="batch-table-col-name" data-i18n="batchManageModal.conversationName">对话名称</div>
|
||||
<div class="batch-table-col-project" data-i18n="batchManageModal.project">项目</div>
|
||||
<div class="batch-table-col-group" data-i18n="batchManageModal.group">对话分组</div>
|
||||
<div class="batch-table-col-time" data-i18n="batchManageModal.lastTime">最近一次对话时间</div>
|
||||
<div class="batch-table-col-action" data-i18n="batchManageModal.action">操作</div>
|
||||
</div>
|
||||
@@ -5953,12 +5903,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer batch-manage-footer">
|
||||
<div class="batch-footer-move">
|
||||
<select id="batch-move-group-select" class="batch-move-group-select conversation-project-filter-native" data-i18n="batchManageModal.setGroup" data-i18n-attr="title" title="设置分组">
|
||||
<option value="__none__" data-i18n="batchManageModal.noGroupOption">无分组</option>
|
||||
</select>
|
||||
<button type="button" class="btn-secondary" onclick="applyBatchGroupChange()" data-i18n="batchManageModal.setGroup">设置分组</button>
|
||||
</div>
|
||||
<div class="batch-footer-actions">
|
||||
<button class="btn-secondary" onclick="closeBatchManageModal()" data-i18n="common.cancel">取消</button>
|
||||
<button class="btn-primary" data-require-permission="chat:delete" onclick="deleteSelectedConversations()" data-i18n="batchManageModal.deleteSelected">删除所选</button>
|
||||
@@ -5967,69 +5911,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建分组模态框 -->
|
||||
<div id="create-group-modal" class="modal">
|
||||
<div class="modal-content create-group-modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 data-i18n="createGroupModal.title">创建分组</h2>
|
||||
<span class="modal-close" onclick="closeCreateGroupModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body create-group-body">
|
||||
<p class="create-group-description" data-i18n="createGroupModal.description">分组功能可将对话集中归类管理,让对话更加井然有序。</p>
|
||||
<div class="create-group-input-wrapper">
|
||||
<button type="button" class="group-icon-input" id="create-group-icon-btn" onclick="toggleGroupIconPicker()" data-i18n="createGroupModal.selectIcon" data-i18n-attr="title" title="点击选择图标">📁</button>
|
||||
<input type="text" id="create-group-name-input" data-i18n="createGroupModal.groupNamePlaceholder" data-i18n-attr="placeholder" placeholder="请输入分组名称" />
|
||||
<!-- Emoji选择器面板 -->
|
||||
<div id="group-icon-picker" class="group-icon-picker" style="display: none;">
|
||||
<div class="icon-picker-header">
|
||||
<span data-i18n="createGroupModal.pickIcon">选择图标</span>
|
||||
<div class="icon-picker-custom">
|
||||
<input type="text" id="custom-icon-input" class="custom-icon-input" data-i18n="createGroupModal.customIcon" data-i18n-attr="placeholder" placeholder="自定义" maxlength="2" />
|
||||
<button type="button" class="custom-icon-btn" onclick="applyCustomIcon()" data-i18n="createGroupModal.confirmIcon">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="icon-picker-grid">
|
||||
<span class="icon-option" onclick="selectGroupIcon('📁')">📁</span>
|
||||
<span class="icon-option" onclick="selectGroupIcon('🔒')">🔒</span>
|
||||
<span class="icon-option" onclick="selectGroupIcon('🛡️')">🛡️</span>
|
||||
<span class="icon-option" onclick="selectGroupIcon('⚔️')">⚔️</span>
|
||||
<span class="icon-option" onclick="selectGroupIcon('🎯')">🎯</span>
|
||||
<span class="icon-option" onclick="selectGroupIcon('🔍')">🔍</span>
|
||||
<span class="icon-option" onclick="selectGroupIcon('💻')">💻</span>
|
||||
<span class="icon-option" onclick="selectGroupIcon('🐛')">🐛</span>
|
||||
<span class="icon-option" onclick="selectGroupIcon('🚀')">🚀</span>
|
||||
<span class="icon-option" onclick="selectGroupIcon('⚡')">⚡</span>
|
||||
<span class="icon-option" onclick="selectGroupIcon('🔥')">🔥</span>
|
||||
<span class="icon-option" onclick="selectGroupIcon('💡')">💡</span>
|
||||
<span class="icon-option" onclick="selectGroupIcon('🎮')">🎮</span>
|
||||
<span class="icon-option" onclick="selectGroupIcon('🏴☠️')">🏴☠️</span>
|
||||
<span class="icon-option" onclick="selectGroupIcon('🕵️')">🕵️</span>
|
||||
<span class="icon-option" onclick="selectGroupIcon('🔑')">🔑</span>
|
||||
<span class="icon-option" onclick="selectGroupIcon('📡')">📡</span>
|
||||
<span class="icon-option" onclick="selectGroupIcon('🌐')">🌐</span>
|
||||
<span class="icon-option" onclick="selectGroupIcon('📊')">📊</span>
|
||||
<span class="icon-option" onclick="selectGroupIcon('📝')">📝</span>
|
||||
<span class="icon-option" onclick="selectGroupIcon('🗂️')">🗂️</span>
|
||||
<span class="icon-option" onclick="selectGroupIcon('📌')">📌</span>
|
||||
<span class="icon-option" onclick="selectGroupIcon('⭐')">⭐</span>
|
||||
<span class="icon-option" onclick="selectGroupIcon('💎')">💎</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="create-group-suggestions">
|
||||
<div class="suggestion-tag" data-i18n="createGroupModal.suggestionPenetrationTest" onclick="selectSuggestionByKey('createGroupModal.suggestionPenetrationTest')">渗透测试</div>
|
||||
<div class="suggestion-tag" data-i18n="createGroupModal.suggestionCtf" onclick="selectSuggestionByKey('createGroupModal.suggestionCtf')">CTF</div>
|
||||
<div class="suggestion-tag" data-i18n="createGroupModal.suggestionRedTeam" onclick="selectSuggestionByKey('createGroupModal.suggestionRedTeam')">红队</div>
|
||||
<div class="suggestion-tag" data-i18n="createGroupModal.suggestionVulnerabilityMining" onclick="selectSuggestionByKey('createGroupModal.suggestionVulnerabilityMining')">漏洞挖掘</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" onclick="closeCreateGroupModal()" data-i18n="createGroupModal.cancel">取消</button>
|
||||
<button class="btn-primary" data-require-permission="group:write" onclick="createGroup(event)" data-i18n="createGroupModal.create">创建</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上下文菜单 -->
|
||||
<div id="conversation-context-menu" class="context-menu" style="display: none;">
|
||||
<div id="attack-chain-menu-item" class="context-menu-item" onclick="showAttackChainFromContext()">
|
||||
@@ -6088,16 +5969,6 @@
|
||||
</svg>
|
||||
<span data-i18n="contextMenu.batchManage">批量管理</span>
|
||||
</div>
|
||||
<div class="context-menu-item context-menu-item-has-submenu" onmouseenter="handleMoveToGroupSubmenuEnter()" onmouseleave="handleMoveToGroupSubmenuLeave(event)">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span data-i18n="contextMenu.moveToGroup">移动到分组</span>
|
||||
<svg class="submenu-arrow" width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<div id="move-to-group-submenu" class="context-submenu" style="display: none;" onmouseenter="clearSubmenuHideTimeout()" onmouseleave="hideMoveToGroupSubmenu()"></div>
|
||||
</div>
|
||||
<div class="context-menu-divider"></div>
|
||||
<div class="context-menu-item context-menu-item-danger" data-require-permission="chat:delete" onclick="deleteConversationFromContext()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -6107,29 +5978,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分组上下文菜单 -->
|
||||
<div id="group-context-menu" class="context-menu" style="display: none;">
|
||||
<div class="context-menu-item" onclick="renameGroupFromContext()">
|
||||
<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>
|
||||
<span data-i18n="contextMenu.rename">重命名</span>
|
||||
</div>
|
||||
<div class="context-menu-item" onclick="pinGroupFromContext()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 17v5M5 17h14l-1-7H6l-1 7zM9 10V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span id="pin-group-menu-text" data-i18n="contextMenu.pinGroup">置顶此分组</span>
|
||||
</div>
|
||||
<div class="context-menu-item context-menu-item-danger" data-require-permission="group:delete" onclick="deleteGroupFromContext()">
|
||||
<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>
|
||||
<span data-i18n="contextMenu.deleteGroup">删除此分组</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 项目列表操作菜单 -->
|
||||
<div id="projects-list-action-menu" class="context-menu" style="display: none;" role="menu">
|
||||
<div id="projects-list-menu-edit" class="context-menu-item" data-require-permission="project:write" onclick="editProjectFromListMenu()">
|
||||
@@ -6443,7 +6291,7 @@
|
||||
<div id="role-select-modal" class="modal">
|
||||
<div class="modal-content role-select-modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 data-i18n="chatGroup.rolePanelTitle">选择角色</h2>
|
||||
<h2 data-i18n="chat.rolePanelTitle">选择角色</h2>
|
||||
<span class="modal-close" onclick="closeRoleSelectModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body role-select-body">
|
||||
|
||||
Reference in New Issue
Block a user