mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-08-29 06:00:52 +02:00
Remove conversation grouping feature
This commit is contained in:
+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"):
|
||||
|
||||
Reference in New Issue
Block a user