diff --git a/internal/database/audit.go b/internal/database/audit.go index 062d3f36..7853ddf5 100644 --- a/internal/database/audit.go +++ b/internal/database/audit.go @@ -21,6 +21,7 @@ type AuditLog struct { UserAgent string `json:"userAgent,omitempty"` ResourceType string `json:"resourceType,omitempty"` ResourceID string `json:"resourceId,omitempty"` + ResourceAvailable *bool `json:"resourceAvailable,omitempty"` // API-only: whether linked resource still exists Message string `json:"message"` Detail map[string]interface{} `json:"detail,omitempty"` } diff --git a/internal/database/conversation.go b/internal/database/conversation.go index 7c3ffff0..e51b9a6e 100644 --- a/internal/database/conversation.go +++ b/internal/database/conversation.go @@ -37,12 +37,12 @@ type Message struct { } // CreateConversation 创建新对话 -func (db *DB) CreateConversation(title string) (*Conversation, error) { - return db.CreateConversationWithWebshell("", title) +func (db *DB) CreateConversation(title string, meta ConversationCreateMeta) (*Conversation, error) { + return db.CreateConversationWithWebshell("", title, meta) } // CreateConversationWithWebshell 创建新对话,可选绑定 WebShell 连接 ID(为空则普通对话) -func (db *DB) CreateConversationWithWebshell(webshellConnectionID, title string) (*Conversation, error) { +func (db *DB) CreateConversationWithWebshell(webshellConnectionID, title string, meta ConversationCreateMeta) (*Conversation, error) { id := uuid.New().String() now := time.Now() @@ -62,12 +62,17 @@ func (db *DB) CreateConversationWithWebshell(webshellConnectionID, title string) return nil, fmt.Errorf("创建对话失败: %w", err) } - return &Conversation{ + conv := &Conversation{ ID: id, Title: title, CreatedAt: now, UpdatedAt: now, - }, nil + } + if webshellConnectionID != "" { + meta.WebShellConnectionID = webshellConnectionID + } + notifyConversationCreated(conv, meta) + return conv, nil } // GetConversationByWebshellConnectionID 根据 WebShell 连接 ID 获取该连接下最近一条对话(用于 AI 助手持久化) @@ -182,6 +187,23 @@ func (db *DB) ListConversationsByWebshellConnectionID(connectionID string) ([]We return list, rows.Err() } +// ConversationExists reports whether a conversation row exists (lightweight check for audit links). +func (db *DB) ConversationExists(id string) (bool, error) { + id = strings.TrimSpace(id) + if id == "" { + return false, nil + } + var one int + err := db.QueryRow("SELECT 1 FROM conversations WHERE id = ? LIMIT 1", id).Scan(&one) + if err == sql.ErrNoRows { + return false, nil + } + if err != nil { + return false, err + } + return true, nil +} + // GetConversation 获取对话 func (db *DB) GetConversation(id string) (*Conversation, error) { var conv Conversation diff --git a/internal/database/conversation_create_meta.go b/internal/database/conversation_create_meta.go new file mode 100644 index 00000000..4ba96530 --- /dev/null +++ b/internal/database/conversation_create_meta.go @@ -0,0 +1,29 @@ +package database + +// ConversationCreateMeta describes how a conversation was created (for audit hooks). +type ConversationCreateMeta struct { + Source string + WebShellConnectionID string + ClientIP string + SessionHint string +} + +// ConversationCreateHook is invoked after a conversation row is inserted. +type ConversationCreateHook func(conv *Conversation, meta ConversationCreateMeta) + +var conversationCreateHook ConversationCreateHook + +// SetConversationCreateHook registers a global hook (e.g. platform audit). +func SetConversationCreateHook(h ConversationCreateHook) { + conversationCreateHook = h +} + +func notifyConversationCreated(conv *Conversation, meta ConversationCreateMeta) { + if conversationCreateHook == nil || conv == nil { + return + } + if meta.Source == "" { + meta.Source = "unknown" + } + conversationCreateHook(conv, meta) +}