mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-08-19 09:27:21 +02:00
Delete internal directory
This commit is contained in:
@@ -1,337 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cyberstrike-ai/internal/config"
|
||||
"cyberstrike-ai/internal/security"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func normalizeHitlReviewer(v string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(v)) {
|
||||
case "audit_agent", "agent", "ai":
|
||||
return "audit_agent"
|
||||
default:
|
||||
return "human"
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeHitlDecidedBy(v string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(v)) {
|
||||
case "audit_agent", "agent", "ai":
|
||||
return "audit_agent"
|
||||
case "system", "timeout":
|
||||
return "system"
|
||||
case "manual":
|
||||
return "manual"
|
||||
default:
|
||||
return "human"
|
||||
}
|
||||
}
|
||||
|
||||
func (m *HITLManager) migrateHitlSchemaColumns() {
|
||||
_, _ = m.db.Exec(`ALTER TABLE hitl_interrupts ADD COLUMN decided_by TEXT NOT NULL DEFAULT 'human'`)
|
||||
_, _ = m.db.Exec(`ALTER TABLE hitl_interrupts ADD COLUMN reviewer TEXT NOT NULL DEFAULT 'human'`)
|
||||
_, _ = m.db.Exec(`UPDATE hitl_interrupts SET reviewer='audit_agent'
|
||||
WHERE COALESCE(decided_by, '') IN ('audit_agent', 'agent', 'ai')`)
|
||||
_, _ = m.db.Exec(`ALTER TABLE hitl_conversation_configs ADD COLUMN reviewer TEXT NOT NULL DEFAULT 'human'`)
|
||||
}
|
||||
|
||||
func hitlInterruptRowToMap(
|
||||
id, cid, mode, toolName, toolCallID, payload, rowStatus, reviewer, decidedBy string,
|
||||
messageID sql.NullString,
|
||||
decision, comment sql.NullString,
|
||||
createdAt time.Time,
|
||||
decidedAt sql.NullTime,
|
||||
) map[string]interface{} {
|
||||
msgID := ""
|
||||
if messageID.Valid {
|
||||
msgID = messageID.String
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"id": id,
|
||||
"conversationId": cid,
|
||||
"messageId": msgID,
|
||||
"mode": mode,
|
||||
"toolName": toolName,
|
||||
"toolCallId": toolCallID,
|
||||
"payload": payload,
|
||||
"status": rowStatus,
|
||||
"reviewer": reviewer,
|
||||
"decision": decision.String,
|
||||
"comment": comment.String,
|
||||
"decidedBy": decidedBy,
|
||||
"createdAt": createdAt,
|
||||
"decidedAt": func() interface{} {
|
||||
if decidedAt.Valid {
|
||||
return decidedAt.Time
|
||||
}
|
||||
return nil
|
||||
}(),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AgentHandler) buildHitlListQuery(logs bool) (string, []interface{}) {
|
||||
where, args := h.buildHitlLogsWhere(logs)
|
||||
q := `SELECT id, conversation_id, message_id, mode, tool_name, tool_call_id, payload, status, COALESCE(reviewer,'human'), decision, decision_comment, COALESCE(decided_by,'human'), created_at, decided_at FROM hitl_interrupts` + where
|
||||
return q, args
|
||||
}
|
||||
|
||||
func (h *AgentHandler) buildHitlLogsWhere(logs bool) (string, []interface{}) {
|
||||
q := " WHERE 1=1"
|
||||
args := []interface{}{}
|
||||
if logs {
|
||||
q += " AND status != 'pending'"
|
||||
} else {
|
||||
// 该接口只返回真正等待用户操作的人工审批。Agent 审查即使正在运行,
|
||||
// 也不应触发弹窗、倒计时或项目待审批计数。
|
||||
q += " AND status = 'pending' AND COALESCE(reviewer,'human') = 'human'"
|
||||
}
|
||||
return q, args
|
||||
}
|
||||
|
||||
func (h *AgentHandler) appendHitlListFilters(q string, args []interface{}, c *gin.Context) (string, []interface{}) {
|
||||
conversationID := strings.TrimSpace(c.Query("conversationId"))
|
||||
toolName := strings.TrimSpace(c.Query("toolName"))
|
||||
decision := strings.TrimSpace(c.Query("decision"))
|
||||
decidedBy := strings.TrimSpace(c.Query("decidedBy"))
|
||||
status := strings.TrimSpace(c.Query("status"))
|
||||
search := strings.TrimSpace(c.Query("q"))
|
||||
|
||||
if conversationID != "" {
|
||||
q += " AND conversation_id = ?"
|
||||
args = append(args, conversationID)
|
||||
}
|
||||
if toolName != "" {
|
||||
q += " AND tool_name LIKE ?"
|
||||
args = append(args, "%"+toolName+"%")
|
||||
}
|
||||
if decision != "" && decision != "all" {
|
||||
q += " AND decision = ?"
|
||||
args = append(args, decision)
|
||||
}
|
||||
if decidedBy != "" && decidedBy != "all" {
|
||||
q += " AND COALESCE(decided_by,'human') = ?"
|
||||
args = append(args, normalizeHitlDecidedBy(decidedBy))
|
||||
}
|
||||
if status != "" && status != "all" {
|
||||
q += " AND status = ?"
|
||||
args = append(args, status)
|
||||
}
|
||||
if search != "" {
|
||||
like := "%" + search + "%"
|
||||
q += " AND (id LIKE ? OR conversation_id LIKE ? OR tool_name LIKE ? OR payload LIKE ? OR COALESCE(decision_comment,'') LIKE ?)"
|
||||
args = append(args, like, like, like, like, like)
|
||||
}
|
||||
return q, args
|
||||
}
|
||||
|
||||
func (h *AgentHandler) scanHitlInterruptRows(rows *sql.Rows) ([]map[string]interface{}, error) {
|
||||
items := make([]map[string]interface{}, 0)
|
||||
for rows.Next() {
|
||||
var id, cid, mode, toolName, toolCallID, payload, rowStatus, reviewer, decidedBy string
|
||||
var messageID sql.NullString
|
||||
var decision, comment sql.NullString
|
||||
var createdAt time.Time
|
||||
var decidedAt sql.NullTime
|
||||
if err := rows.Scan(&id, &cid, &messageID, &mode, &toolName, &toolCallID, &payload, &rowStatus, &reviewer, &decision, &comment, &decidedBy, &createdAt, &decidedAt); err != nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, hitlInterruptRowToMap(id, cid, mode, toolName, toolCallID, payload, rowStatus, reviewer, decidedBy, messageID, decision, comment, createdAt, decidedAt))
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (h *AgentHandler) countHitlQuery(baseQ string, args []interface{}) (int, error) {
|
||||
countQ := "SELECT COUNT(*) FROM (" + baseQ + ") AS hitl_cnt"
|
||||
var total int
|
||||
if err := h.db.QueryRow(countQ, args...).Scan(&total); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (h *AgentHandler) ListHITLLogs(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20"))
|
||||
pageSize = int(math.Max(1, math.Min(float64(pageSize), 200)))
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
q, args := h.buildHitlListQuery(true)
|
||||
q, args = h.appendHitlListFilters(q, args, c)
|
||||
q, args = appendConversationAccessSQL(q, args, "conversation_id", notificationAccessFromContext(c))
|
||||
total, err := h.countHitlQuery(q, args)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
q += " ORDER BY COALESCE(decided_at, created_at) DESC LIMIT ? OFFSET ?"
|
||||
args = append(args, pageSize, offset)
|
||||
rows, err := h.db.Query(q, args...)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
items, err := h.scanHitlInterruptRows(rows)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"items": items, "page": page, "pageSize": pageSize, "total": total, "retentionDays": h.hitlRetentionDays()})
|
||||
}
|
||||
|
||||
func (h *AgentHandler) hitlRetentionDays() int {
|
||||
if h.config != nil {
|
||||
return h.config.Hitl.RetentionDaysEffective()
|
||||
}
|
||||
return config.HitlConfig{}.RetentionDaysEffective()
|
||||
}
|
||||
|
||||
// DeleteHITLLogs 批量删除或按筛选清空已决策的人机协同审计日志(不删除 pending)。
|
||||
func (h *AgentHandler) DeleteHITLLogs(c *gin.Context) {
|
||||
var request struct {
|
||||
IDs []string `json:"ids"`
|
||||
All bool `json:"all"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数无效: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var deleted int64
|
||||
var err error
|
||||
if request.All {
|
||||
where, args := h.buildHitlLogsWhere(true)
|
||||
where, args = h.appendHitlListFilters(where, args, c)
|
||||
where, args = appendConversationAccessSQL(where, args, "conversation_id", notificationAccessFromContext(c))
|
||||
deleted, err = h.db.DeleteHitlInterruptLogsMatching(where, args)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if h.audit != nil {
|
||||
h.audit.RecordOK(c, "hitl", "logs_clear", "清空人机协同审计日志", "hitl_interrupt", "", map[string]interface{}{
|
||||
"deleted": deleted,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if len(request.IDs) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "审计日志 ID 列表不能为空"})
|
||||
return
|
||||
}
|
||||
ids, filterErr := h.filterAllowedHitlInterruptIDs(c, request.IDs)
|
||||
if filterErr != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": filterErr.Error()})
|
||||
return
|
||||
}
|
||||
deleted, err = h.db.DeleteHitlInterruptLogsByIDs(ids)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if h.audit != nil {
|
||||
h.audit.RecordOK(c, "hitl", "logs_delete_batch", "批量删除人机协同审计日志", "hitl_interrupt", "", map[string]interface{}{
|
||||
"count": len(request.IDs),
|
||||
"deleted": deleted,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "删除成功", "deleted": deleted})
|
||||
}
|
||||
|
||||
func (h *AgentHandler) GetHITLLog(c *gin.Context) {
|
||||
id := strings.TrimSpace(c.Param("id"))
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "id is required"})
|
||||
return
|
||||
}
|
||||
q := `SELECT id, conversation_id, message_id, mode, tool_name, tool_call_id, payload, status, COALESCE(reviewer,'human'), decision, decision_comment, COALESCE(decided_by,'human'), created_at, decided_at FROM hitl_interrupts WHERE id = ?`
|
||||
var rowID, cid, mode, toolName, toolCallID, payload, rowStatus, reviewer, decidedBy string
|
||||
var messageID sql.NullString
|
||||
var decision, comment sql.NullString
|
||||
var createdAt time.Time
|
||||
var decidedAt sql.NullTime
|
||||
err := h.db.QueryRow(q, id).Scan(&rowID, &cid, &messageID, &mode, &toolName, &toolCallID, &payload, &rowStatus, &reviewer, &decision, &comment, &decidedBy, &createdAt, &decidedAt)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if !h.hitlConversationAllowed(c, cid) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权访问该资源"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, hitlInterruptRowToMap(rowID, cid, mode, toolName, toolCallID, payload, rowStatus, reviewer, decidedBy, messageID, decision, comment, createdAt, decidedAt))
|
||||
}
|
||||
|
||||
func (h *AgentHandler) filterAllowedHitlInterruptIDs(c *gin.Context, ids []string) ([]string, error) {
|
||||
clean := make([]string, 0, len(ids))
|
||||
seen := map[string]struct{}{}
|
||||
for _, id := range ids {
|
||||
id = strings.TrimSpace(id)
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
clean = append(clean, id)
|
||||
}
|
||||
if len(clean) == 0 {
|
||||
return clean, nil
|
||||
}
|
||||
query := `SELECT id, conversation_id FROM hitl_interrupts WHERE id IN (` + buildPlaceholders(len(clean)) + `)`
|
||||
args := make([]interface{}, 0, len(clean))
|
||||
for _, id := range clean {
|
||||
args = append(args, id)
|
||||
}
|
||||
rows, err := h.db.Query(query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
allowed := make([]string, 0, len(clean))
|
||||
for rows.Next() {
|
||||
var id, conversationID string
|
||||
if err := rows.Scan(&id, &conversationID); err != nil {
|
||||
continue
|
||||
}
|
||||
if h.hitlConversationAllowed(c, conversationID) {
|
||||
allowed = append(allowed, id)
|
||||
}
|
||||
}
|
||||
return allowed, rows.Err()
|
||||
}
|
||||
|
||||
func (h *AgentHandler) hitlInterruptAllowed(c *gin.Context, interruptID string) bool {
|
||||
var conversationID string
|
||||
if err := h.db.QueryRow(`SELECT conversation_id FROM hitl_interrupts WHERE id = ?`, strings.TrimSpace(interruptID)).Scan(&conversationID); err != nil {
|
||||
return false
|
||||
}
|
||||
return h.hitlConversationAllowed(c, conversationID)
|
||||
}
|
||||
|
||||
func (h *AgentHandler) hitlConversationAllowed(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)
|
||||
}
|
||||
Reference in New Issue
Block a user