| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 |
- package services
- import (
- "bytes"
- "ems-backend/models"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "log"
- "net/http"
- "strings"
- "time"
- "github.com/google/uuid"
- )
- // CheckAndAutoHandleAlarms 检查告警数量并执行自动处理
- func CheckAndAutoHandleAlarms() {
- // 1. 获取配置
- var autoAck, thresholdStr string
- models.DB.Model(&models.SysConfig{}).Where("config_key = ?", "ai_auto_ack").Select("config_value").Scan(&autoAck)
- models.DB.Model(&models.SysConfig{}).Where("config_key = ?", "ai_alert_threshold").Select("config_value").Scan(&thresholdStr)
- if autoAck != "true" {
- return
- }
- threshold := 200 // 默认值
- if _, err := fmt.Sscanf(thresholdStr, "%d", &threshold); err != nil {
- // 解析失败使用默认值
- }
- // 2. 检查当前活跃告警数量
- var count int64
- models.DB.Model(&models.AlarmLog{}).Where("status = ?", "ACTIVE").Count(&count)
- if count > int64(threshold) {
- log.Printf("Active alarms (%d) exceeded threshold (%d), triggering auto-confirm...", count, threshold)
- go processAutoConfirmAndReport()
- }
- }
- func processAutoConfirmAndReport() {
- // 1. 获取待确认的300条告警
- var alarms []models.AlarmLog
- err := models.DB.Where("status = ?", "ACTIVE").
- Order("start_time asc"). // 处理最早的告警
- Limit(300).
- Find(&alarms).Error
- if err != nil || len(alarms) == 0 {
- return
- }
- // 2. 批量确认告警 (更新状态为 CONFIRMED)
- var alarmIDs []uuid.UUID
- for _, a := range alarms {
- alarmIDs = append(alarmIDs, a.ID)
- }
- tx := models.DB.Begin()
- // 注意:AlarmLog 的状态字段可能是 Status 或 status,根据 schema.go 是 Status
- if err := tx.Model(&models.AlarmLog{}).
- Where("id IN ?", alarmIDs).
- Update("status", "CONFIRMED").Error; err != nil {
- tx.Rollback()
- log.Printf("Failed to auto-confirm alarms: %v", err)
- return
- }
- tx.Commit()
- // 3. 生成 AI 分析报表
- GenerateAIReportInternal(alarms, fmt.Sprintf("自动告警处理报告 (已确认 %d 条) - %s", len(alarms), time.Now().Format("2006-01-02 15:04:05")), "AUTO_ALARM_ANALYSIS")
- }
- // GenerateAIReportInternal 内部生成报表逻辑
- func GenerateAIReportInternal(alarms []models.AlarmLog, title string, reportType string) (*models.AIAnalysisReport, error) {
- // 获取 AI 配置
- var apiUrl, apiKey, model string
- models.DB.Model(&models.SysConfig{}).Where("config_key = ?", "ai_api_url").Select("config_value").Scan(&apiUrl)
- models.DB.Model(&models.SysConfig{}).Where("config_key = ?", "ai_api_key").Select("config_value").Scan(&apiKey)
- models.DB.Model(&models.SysConfig{}).Where("config_key = ?", "ai_model").Select("config_value").Scan(&model)
- if apiUrl == "" || apiKey == "" {
- return nil, errors.New("AI configuration missing")
- }
- if model == "" {
- model = "gpt-3.5-turbo"
- }
- // 创建报表记录
- report := models.AIAnalysisReport{
- ReportType: reportType,
- Title: title,
- Status: "GENERATING",
- AlertCount: len(alarms),
- }
- if err := models.DB.Create(&report).Error; err != nil {
- return nil, err
- }
- // 异步调用 AI
- go func(rID uuid.UUID, alarms []models.AlarmLog, url, key, model string) {
- // 构建 Prompt
- prompt := fmt.Sprintf("系统包含 %d 条告警信息。以下是这些告警的摘要(前20条):\n", len(alarms))
- limit := 20
- if len(alarms) < 20 {
- limit = len(alarms)
- }
- for i := 0; i < limit; i++ {
- a := alarms[i]
- prompt += fmt.Sprintf("- [%s] 类型: %s, 内容: %s\n", a.StartTime.Format("15:04:05"), a.Type, a.Content)
- }
- prompt += "\n请根据以上信息,生成一份简要的监控分析报表,包含:\n1. 告警爆发原因分析\n2. 系统风险评估\n3. 后续优化建议\n请使用 Markdown 格式。"
- // 调用 AI
- content, err := FetchAIResponse(url, key, model, prompt)
- status := "COMPLETED"
- if err != nil {
- content = "生成失败: " + err.Error()
- status = "FAILED"
- }
- models.DB.Model(&models.AIAnalysisReport{}).Where("id = ?", rID).Updates(map[string]interface{}{
- "status": status,
- "content": content,
- })
- }(report.ID, alarms, NormalizeChatUrl(apiUrl), apiKey, model)
- return &report, nil
- }
- // NormalizeChatUrl 规范化 Chat 接口地址 (Public for controller usage if needed)
- func NormalizeChatUrl(inputUrl string) string {
- inputUrl = strings.TrimSpace(inputUrl)
- if strings.HasSuffix(inputUrl, "/chat/completions") {
- return inputUrl
- }
- // 如果是 /models 结尾,替换
- if strings.HasSuffix(inputUrl, "/models") {
- return strings.Replace(inputUrl, "/models", "/chat/completions", 1)
- }
- // 如果是 /v1 结尾
- if strings.HasSuffix(inputUrl, "/v1") {
- return strings.TrimRight(inputUrl, "/") + "/chat/completions"
- }
- return strings.TrimRight(inputUrl, "/") + "/chat/completions"
- }
- // 全局 HTTP Client 复用,优化连接池
- var aiHttpClient = &http.Client{
- Timeout: 60 * time.Second,
- Transport: &http.Transport{
- MaxIdleConns: 10,
- IdleConnTimeout: 30 * time.Second,
- DisableKeepAlives: false,
- },
- }
- // FetchAIResponse 实际调用 (Public for controller usage)
- func FetchAIResponse(apiUrl, apiKey, model, prompt string) (string, error) {
- requestBody, _ := json.Marshal(map[string]interface{}{
- "model": model,
- "messages": []map[string]string{
- {"role": "user", "content": prompt},
- },
- "temperature": 0.7,
- })
- req, err := http.NewRequest("POST", apiUrl, bytes.NewBuffer(requestBody))
- if err != nil {
- return "", err
- }
- req.Header.Set("Content-Type", "application/json")
- req.Header.Set("Authorization", "Bearer "+apiKey)
- // 使用全局 Client 复用连接
- resp, err := aiHttpClient.Do(req)
- if err != nil {
- return "", err
- }
- defer resp.Body.Close()
- bodyBytes, _ := io.ReadAll(resp.Body)
- if resp.StatusCode != 200 {
- return "", fmt.Errorf("API Error: %d - %s", resp.StatusCode, string(bodyBytes))
- }
- var result struct {
- Choices []struct {
- Message struct {
- Content string `json:"content"`
- } `json:"message"`
- } `json:"choices"`
- Error struct {
- Message string `json:"message"`
- } `json:"error"`
- }
- if err := json.Unmarshal(bodyBytes, &result); err != nil {
- return "", fmt.Errorf("Parse Error: %v", err)
- }
- if len(result.Choices) > 0 {
- return result.Choices[0].Message.Content, nil
- }
- return "", errors.New("Empty response from AI")
- }
|