ai_service.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. package services
  2. import (
  3. "bytes"
  4. "ems-backend/models"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "log"
  10. "net/http"
  11. "strings"
  12. "time"
  13. "github.com/google/uuid"
  14. )
  15. // CheckAndAutoHandleAlarms 检查告警数量并执行自动处理
  16. func CheckAndAutoHandleAlarms() {
  17. // 1. 获取配置
  18. var autoAck, thresholdStr string
  19. models.DB.Model(&models.SysConfig{}).Where("config_key = ?", "ai_auto_ack").Select("config_value").Scan(&autoAck)
  20. models.DB.Model(&models.SysConfig{}).Where("config_key = ?", "ai_alert_threshold").Select("config_value").Scan(&thresholdStr)
  21. if autoAck != "true" {
  22. return
  23. }
  24. threshold := 200 // 默认值
  25. if _, err := fmt.Sscanf(thresholdStr, "%d", &threshold); err != nil {
  26. // 解析失败使用默认值
  27. }
  28. // 2. 检查当前活跃告警数量
  29. var count int64
  30. models.DB.Model(&models.AlarmLog{}).Where("status = ?", "ACTIVE").Count(&count)
  31. if count > int64(threshold) {
  32. log.Printf("Active alarms (%d) exceeded threshold (%d), triggering auto-confirm...", count, threshold)
  33. go processAutoConfirmAndReport()
  34. }
  35. }
  36. func processAutoConfirmAndReport() {
  37. // 1. 获取待确认的300条告警
  38. var alarms []models.AlarmLog
  39. err := models.DB.Where("status = ?", "ACTIVE").
  40. Order("start_time asc"). // 处理最早的告警
  41. Limit(300).
  42. Find(&alarms).Error
  43. if err != nil || len(alarms) == 0 {
  44. return
  45. }
  46. // 2. 批量确认告警 (更新状态为 CONFIRMED)
  47. var alarmIDs []uuid.UUID
  48. for _, a := range alarms {
  49. alarmIDs = append(alarmIDs, a.ID)
  50. }
  51. tx := models.DB.Begin()
  52. // 注意:AlarmLog 的状态字段可能是 Status 或 status,根据 schema.go 是 Status
  53. if err := tx.Model(&models.AlarmLog{}).
  54. Where("id IN ?", alarmIDs).
  55. Update("status", "CONFIRMED").Error; err != nil {
  56. tx.Rollback()
  57. log.Printf("Failed to auto-confirm alarms: %v", err)
  58. return
  59. }
  60. tx.Commit()
  61. // 3. 生成 AI 分析报表
  62. GenerateAIReportInternal(alarms, fmt.Sprintf("自动告警处理报告 (已确认 %d 条) - %s", len(alarms), time.Now().Format("2006-01-02 15:04:05")), "AUTO_ALARM_ANALYSIS")
  63. }
  64. // GenerateAIReportInternal 内部生成报表逻辑
  65. func GenerateAIReportInternal(alarms []models.AlarmLog, title string, reportType string) (*models.AIAnalysisReport, error) {
  66. // 获取 AI 配置
  67. var apiUrl, apiKey, model string
  68. models.DB.Model(&models.SysConfig{}).Where("config_key = ?", "ai_api_url").Select("config_value").Scan(&apiUrl)
  69. models.DB.Model(&models.SysConfig{}).Where("config_key = ?", "ai_api_key").Select("config_value").Scan(&apiKey)
  70. models.DB.Model(&models.SysConfig{}).Where("config_key = ?", "ai_model").Select("config_value").Scan(&model)
  71. if apiUrl == "" || apiKey == "" {
  72. return nil, errors.New("AI configuration missing")
  73. }
  74. if model == "" {
  75. model = "gpt-3.5-turbo"
  76. }
  77. // 创建报表记录
  78. report := models.AIAnalysisReport{
  79. ReportType: reportType,
  80. Title: title,
  81. Status: "GENERATING",
  82. AlertCount: len(alarms),
  83. }
  84. if err := models.DB.Create(&report).Error; err != nil {
  85. return nil, err
  86. }
  87. // 异步调用 AI
  88. go func(rID uuid.UUID, alarms []models.AlarmLog, url, key, model string) {
  89. // 构建 Prompt
  90. prompt := fmt.Sprintf("系统包含 %d 条告警信息。以下是这些告警的摘要(前20条):\n", len(alarms))
  91. limit := 20
  92. if len(alarms) < 20 {
  93. limit = len(alarms)
  94. }
  95. for i := 0; i < limit; i++ {
  96. a := alarms[i]
  97. prompt += fmt.Sprintf("- [%s] 类型: %s, 内容: %s\n", a.StartTime.Format("15:04:05"), a.Type, a.Content)
  98. }
  99. prompt += "\n请根据以上信息,生成一份简要的监控分析报表,包含:\n1. 告警爆发原因分析\n2. 系统风险评估\n3. 后续优化建议\n请使用 Markdown 格式。"
  100. // 调用 AI
  101. content, err := FetchAIResponse(url, key, model, prompt)
  102. status := "COMPLETED"
  103. if err != nil {
  104. content = "生成失败: " + err.Error()
  105. status = "FAILED"
  106. }
  107. models.DB.Model(&models.AIAnalysisReport{}).Where("id = ?", rID).Updates(map[string]interface{}{
  108. "status": status,
  109. "content": content,
  110. })
  111. }(report.ID, alarms, NormalizeChatUrl(apiUrl), apiKey, model)
  112. return &report, nil
  113. }
  114. // NormalizeChatUrl 规范化 Chat 接口地址 (Public for controller usage if needed)
  115. func NormalizeChatUrl(inputUrl string) string {
  116. inputUrl = strings.TrimSpace(inputUrl)
  117. if strings.HasSuffix(inputUrl, "/chat/completions") {
  118. return inputUrl
  119. }
  120. // 如果是 /models 结尾,替换
  121. if strings.HasSuffix(inputUrl, "/models") {
  122. return strings.Replace(inputUrl, "/models", "/chat/completions", 1)
  123. }
  124. // 如果是 /v1 结尾
  125. if strings.HasSuffix(inputUrl, "/v1") {
  126. return strings.TrimRight(inputUrl, "/") + "/chat/completions"
  127. }
  128. return strings.TrimRight(inputUrl, "/") + "/chat/completions"
  129. }
  130. // 全局 HTTP Client 复用,优化连接池
  131. var aiHttpClient = &http.Client{
  132. Timeout: 60 * time.Second,
  133. Transport: &http.Transport{
  134. MaxIdleConns: 10,
  135. IdleConnTimeout: 30 * time.Second,
  136. DisableKeepAlives: false,
  137. },
  138. }
  139. // FetchAIResponse 实际调用 (Public for controller usage)
  140. func FetchAIResponse(apiUrl, apiKey, model, prompt string) (string, error) {
  141. requestBody, _ := json.Marshal(map[string]interface{}{
  142. "model": model,
  143. "messages": []map[string]string{
  144. {"role": "user", "content": prompt},
  145. },
  146. "temperature": 0.7,
  147. })
  148. req, err := http.NewRequest("POST", apiUrl, bytes.NewBuffer(requestBody))
  149. if err != nil {
  150. return "", err
  151. }
  152. req.Header.Set("Content-Type", "application/json")
  153. req.Header.Set("Authorization", "Bearer "+apiKey)
  154. // 使用全局 Client 复用连接
  155. resp, err := aiHttpClient.Do(req)
  156. if err != nil {
  157. return "", err
  158. }
  159. defer resp.Body.Close()
  160. bodyBytes, _ := io.ReadAll(resp.Body)
  161. if resp.StatusCode != 200 {
  162. return "", fmt.Errorf("API Error: %d - %s", resp.StatusCode, string(bodyBytes))
  163. }
  164. var result struct {
  165. Choices []struct {
  166. Message struct {
  167. Content string `json:"content"`
  168. } `json:"message"`
  169. } `json:"choices"`
  170. Error struct {
  171. Message string `json:"message"`
  172. } `json:"error"`
  173. }
  174. if err := json.Unmarshal(bodyBytes, &result); err != nil {
  175. return "", fmt.Errorf("Parse Error: %v", err)
  176. }
  177. if len(result.Choices) > 0 {
  178. return result.Choices[0].Message.Content, nil
  179. }
  180. return "", errors.New("Empty response from AI")
  181. }