Files
aiops/backend/controller/llm.go
T
2026-07-09 14:16:56 +08:00

188 lines
6.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package controller
import (
"net/http"
"strings"
"time"
"ezops/model"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// LLMHandler 大语言模型配置 + 对话代理 // Logan.Li
type LLMHandler struct {
db *gorm.DB
client *http.Client
}
// NewLLMHandler 创建 LLM 处理器 // Logan.Li
func NewLLMHandler(db *gorm.DB) *LLMHandler {
return &LLMHandler{
db: db,
// 超时控制:LLM 推理可能较慢,给 120s
client: &http.Client{Timeout: 120 * time.Second},
}
}
// maskAPIKey 脱敏 API Key(仅保留后 4 位) // Logan.Li
func maskAPIKey(key string) string {
if len(key) <= 4 {
return strings.Repeat("*", len(key))
}
return strings.Repeat("*", len(key)-4) + key[len(key)-4:]
}
// getActiveConfig 读取启用的配置(脱敏后) // Logan.Li
func (h *LLMHandler) getActiveConfig(maskKey bool) (model.LLMConfig, error) {
var cfg model.LLMConfig
if err := h.db.First(&cfg).Error; err != nil {
return cfg, err
}
if maskKey {
cfg.APIKey = maskAPIKey(cfg.APIKey)
}
return cfg, nil
}
// GetLLMConfig 获取 LLM 配置(API Key 脱敏) // Logan.Li
// GET /api/v1/aiops/llm-config
func (h *LLMHandler) GetLLMConfig(c *gin.Context) {
cfg, err := h.getActiveConfig(true)
if err != nil {
// 未配置返回空对象(前端渲染空表单)
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
"provider": "", "model": "", "api_key": "",
"endpoint": "", "temperature": 0.7, "max_tokens": 2000,
"system_prompt": "", "enabled": false, "reasoning_enabled": false, "configured": false,
}})
return
}
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
"id": cfg.ID,
"provider": cfg.Provider,
"model": cfg.Model,
"api_key": cfg.APIKey,
"endpoint": cfg.Endpoint,
"temperature": cfg.Temperature,
"max_tokens": cfg.MaxTokens,
"system_prompt": cfg.SystemPrompt,
"enabled": cfg.Enabled,
"reasoning_enabled": cfg.ReasoningEnabled,
"configured": true,
}})
}
// SaveLLMConfig 保存 LLM 配置(upsert 单行) // Logan.Li
// PUT /api/v1/aiops/llm-config
func (h *LLMHandler) SaveLLMConfig(c *gin.Context) {
var req model.SaveLLMConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
return
}
var cfg model.LLMConfig
h.db.First(&cfg)
// 若前端回传脱敏值(含 ****),保留原 Key
if strings.Contains(req.APIKey, "****") && cfg.ID != 0 {
req.APIKey = cfg.APIKey
}
cfg.Provider = req.Provider
cfg.Model = req.Model
cfg.APIKey = req.APIKey
cfg.Endpoint = req.Endpoint
cfg.Temperature = req.Temperature
cfg.MaxTokens = req.MaxTokens
cfg.SystemPrompt = req.SystemPrompt
cfg.Enabled = req.Enabled
cfg.ReasoningEnabled = req.ReasoningEnabled
if cfg.ID == 0 {
if err := h.db.Create(&cfg).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "保存失败: " + err.Error()})
return
}
} else {
if err := h.db.Save(&cfg).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "保存失败: " + err.Error()})
return
}
}
// 返回脱敏后的配置
cfg.APIKey = maskAPIKey(cfg.APIKey)
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "保存成功", Data: gin.H{
"id": cfg.ID,
"provider": cfg.Provider,
"model": cfg.Model,
"api_key": cfg.APIKey,
"endpoint": cfg.Endpoint,
"temperature": cfg.Temperature,
"max_tokens": cfg.MaxTokens,
"system_prompt": cfg.SystemPrompt,
"enabled": cfg.Enabled,
"reasoning_enabled": cfg.ReasoningEnabled,
"configured": true,
}})
}
// resolveEndpoint 根据提供商补全默认 endpoint // Logan.Li
func resolveEndpoint(provider, endpoint string) string {
if endpoint != "" {
return strings.TrimRight(endpoint, "/")
}
switch provider {
case "deepseek":
return "https://api.deepseek.com/v1"
case "glm": // 智谱 GLM 通用 OpenAI 兼容端点(编码套餐改用 https://open.bigmodel.cn/api/coding/paas/v4) // Logan.Li
return "https://open.bigmodel.cn/api/paas/v4"
case "qwen":
return "https://dashscope.aliyuncs.com/compatible-mode/v1"
case "moonshot":
return "https://api.moonshot.cn/v1"
case "kimi": // Kimi 编码套餐 OpenAI 兼容端点(注意:该端点有客户端白名单,通用调用可能被拒) // Logan.Li
return "https://api.kimi.com/coding/v1"
case "mimo": // 小米 MiMo OpenAI 兼容端点 // Logan.Li
return "https://api.mimo.mi.com/v1"
case "anthropic": // Anthropic 官方 Messages 协议(anthropic.go 客户端拼 /v1/messages) // Logan.Li
return "https://api.anthropic.com"
case "glm-coding": // 智谱 GLM 编码套餐 Anthropic 协议端点(模型 GLM-5.2) // Logan.Li
return "https://open.bigmodel.cn/api/anthropic"
case "minimax": // MiniMax 按量 OpenAI 兼容端点 // Logan.Li
return "https://api.minimaxi.com/v1"
case "minimax-coding": // MiniMax 编码套餐 Anthropic 协议端点 // Logan.Li
return "https://api.minimaxi.com/anthropic"
case "openai": // OpenAI 官方端点 // Logan.Li
return "https://api.openai.com/v1"
case "siliconflow": // 硅基流动 OpenAI 兼容端点 // Logan.Li
return "https://api.siliconflow.cn/v1"
case "ollama":
return "http://localhost:11434/v1"
case "vllm": // vLLM OpenAI 兼容端点 // Logan.Li
return "http://localhost:8000/v1"
default: // openai / custom
return "https://api.openai.com/v1"
}
}
// isLocalLLMProvider 本地/自建模型,API Key 可选 // Logan.Li
func isLocalLLMProvider(provider string) bool {
return provider == "ollama" || provider == "vllm" || provider == "custom"
}
// resolveProtocol 根据提供商推断接入协议(决定走 OpenAI 还是 Anthropic 客户端) // Logan.Li
// 编码套餐(智谱 / MiniMax)与 Anthropic 官方走 Messages 协议;其余走 OpenAI 兼容协议。 // Logan.Li
func resolveProtocol(provider string) string {
if provider == "anthropic" || provider == "glm-coding" || provider == "minimax-coding" {
return "anthropic"
}
return "openai"
}
// defaultSystemPrompt AIOps 运维助手默认系统提示词 // Logan.Li
const defaultSystemPrompt = `你是 AIOps 智能运维助手,专注于企业级运维场景。你可以:
1. 分析服务器、容器、K8s 集群的性能瓶颈与异常
2. 解读告警事件并给出根因分析与处置建议
3. 生成 Shell / Ansible / Python 运维脚本
4. 提供容量规划、成本优化、高可用架构建议
回答请简洁、专业、可执行,优先给出具体命令或步骤。使用中文回答。`