Files
aiops/backend/controller/server.go
T
LoganandClaude 0b2e43ea9e feat(llm): 大模型双协议(OpenAI+Anthropic)接入 + SSH凭证修复 + 容量预测AI解读
后端双协议: 新增 anthropic.go(Complete/Stream/工具转换) + client.go 工厂按 Protocol 路由; llm.Client 加 Stream; llm_chat 三方法收敛复用 llm.Client; resolveProtocol 按 provider 路由; Agent CLI 注入协议。编码套餐(GLM-5.2/Kimi/MiMo/Anthropic)走 Anthropic /v1/messages。

SSH凭证修复: applyCredential 正确解析凭证 JSON 的 private_key(旧实现把整个 JSON 当私钥 → ssh.ParsePrivateKey 失败 → ssh-key 登录不通); AssignCredential/批量导入/单台 CRUD 三处统一; 单台补齐 credential_id 字段(原被忽略)。

容量预测: 新增 AI 深度解读(buildCapacityInsightPrompt + PredictiveCapacityTab 按钮, 复用 aiApi.chat)。

其他: DeepSeek 模型名更新为 deepseek-v4-flash/pro; 智能助手去掉重复模型配置按钮; LLMConfig 精简生成参数(Claude Code 式)。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-28 22:37:46 +08:00

452 lines
12 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 (
"context"
"encoding/json"
"net/http"
"strconv"
"time"
"ezops/model"
"ezops/pkg/crypto"
ezredis "ezops/pkg/redis"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// ServerHandler 服务器处理器
type ServerHandler struct {
db *gorm.DB
cache ezredis.Cache
crypt *crypto.SecretBox
}
// NewServerHandler 创建服务器处理器
func NewServerHandler(db *gorm.DB, masterKey string, cache ezredis.Cache) *ServerHandler {
return &ServerHandler{db: db, crypt: crypto.NewSecretBox(masterKey), cache: cache}
}
// recordServerAudit 记录服务器操作审计日志 // Logan.Li
func (h *ServerHandler) recordServerAudit(c *gin.Context, action, resource string, resourceID string, status int, errMsg string) {
userID, _ := c.Get("userID")
username, _ := c.Get("username")
uid, _ := userID.(uint)
uname, _ := username.(string)
h.db.Create(&model.AuditLog{
UserID: &uid,
Username: uname,
IP: c.ClientIP(),
UserAgent: c.Request.UserAgent(),
Module: "server",
Action: action,
Resource: resource,
ResourceID: resourceID,
Status: status,
Error: errMsg,
})
}
// ListServers 获取服务器列表
// @Summary 获取服务器列表
// @Description 获取所有服务器列表
// @Tags 服务器
// @Produce json
// @Security Bearer
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Param keyword query string false "搜索关键词"
// @Param status query string false "状态过滤"
// @Param region_id query int false "区域ID"
// @Success 200 {object} model.Response{data=model.ListResponse}
// @Router /servers [get]
func (h *ServerHandler) ListServers(c *gin.Context) {
var query model.PageQuery
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, model.Response{
Code: 400,
Message: "请求参数错误",
})
return
}
status := c.Query("status")
regionID := c.Query("region_id")
// 构建缓存键(按查询参数区分)
cacheKey := "servers:list:" + query.Keyword + ":" + status + ":" + regionID +
":" + strconv.Itoa(query.Page) + ":" + strconv.Itoa(query.PageSize)
const cacheTTL = 10 * time.Second
// 尝试从 Redis 缓存读取
if h.cache != nil {
if cached, err := h.cache.Get(c.Request.Context(), cacheKey); err == nil && cached != "" {
var resp model.Response
if json.Unmarshal([]byte(cached), &resp) == nil {
c.JSON(http.StatusOK, resp)
return
}
}
}
db := h.db.Model(&model.Server{})
// 关键词搜索
if query.Keyword != "" {
db = db.Where("hostname LIKE ? OR ip LIKE ? OR public_ip LIKE ?",
"%"+query.Keyword+"%", "%"+query.Keyword+"%", "%"+query.Keyword+"%")
}
// 状态过滤
if status := c.Query("status"); status != "" {
db = db.Where("status = ?", status)
}
// 区域过滤
if regionID := c.Query("region_id"); regionID != "" {
db = db.Where("region_id = ?", regionID)
}
var total int64
db.Count(&total)
var servers []model.Server
db.Preload("Region").Preload("Cloud").
Offset(query.GetOffset()).Limit(query.GetLimit()).
Order("created_at DESC").
Find(&servers)
// 填充 has_auth 字段(密码和SSH密钥在JSON中隐藏,但前端需知道是否有凭据) // Logan.Li
for i := range servers {
servers[i].HasAuth = servers[i].Password != "" || servers[i].SSHKey != ""
}
resp := model.Response{
Code: 200,
Message: "success",
Data: model.ListResponse{
List: servers,
Total: total,
},
}
// 写入 Redis 缓存(异步,缓存失败不影响响应)
if h.cache != nil {
if data, err := json.Marshal(resp); err == nil {
_ = h.cache.Set(context.Background(), cacheKey, string(data), cacheTTL)
}
}
c.JSON(http.StatusOK, resp)
}
// GetServer 获取服务器详情
// @Summary 获取服务器详情
// @Description 获取指定服务器的详细信息
// @Tags 服务器
// @Produce json
// @Security Bearer
// @Param id path int true "服务器ID"
// @Success 200 {object} model.Response{data=model.Server}
// @Router /servers/{id} [get]
func (h *ServerHandler) GetServer(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var server model.Server
if err := h.db.Preload("Region").Preload("Cloud").First(&server, id).Error; err != nil {
c.JSON(http.StatusNotFound, model.Response{
Code: 404,
Message: "服务器不存在",
})
return
}
c.JSON(http.StatusOK, model.Response{
Code: 200,
Message: "success",
Data: server,
})
}
// CreateServer 创建服务器
// @Summary 创建服务器
// @Description 创建新服务器
// @Tags 服务器
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body model.CreateServerRequest true "创建请求"
// @Success 200 {object} model.Response{data=model.Server}
// @Router /servers [post]
func (h *ServerHandler) CreateServer(c *gin.Context) {
var req model.CreateServerRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
server := model.Server{
Hostname: req.Hostname,
IP: req.IP,
PublicIP: req.PublicIP,
Port: req.Port,
Username: req.Username,
Password: req.Password,
SSHKey: req.SSHKey,
AuthType: req.AuthType,
OS: req.OS,
CPU: req.CPU,
Memory: req.Memory,
Disk: req.Disk,
RegionID: req.RegionID,
CloudID: req.CloudID,
Tags: req.Tags,
Description: req.Description,
Status: "stopped",
}
if req.Status != "" {
server.Status = req.Status
}
if server.Port == 0 {
server.Port = 22
}
if server.Username == "" {
server.Username = "root"
}
if server.AuthType == "" {
server.AuthType = "password"
}
// 关联 SSH 凭证:解密后覆盖认证字段(同 AssignCredential,避免单台 CRUD 忽略 credential_id) // Logan.Li
if req.CredentialID != nil && *req.CredentialID > 0 {
var cred model.Credential
if err := h.db.First(&cred, *req.CredentialID).Error; err != nil {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "凭据不存在"})
return
}
plain, err := h.crypt.Decrypt(cred.Content)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "凭据解密失败"})
return
}
applyCredential(cred, plain, &server)
}
if err := h.db.Create(&server).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.Response{
Code: 500,
Message: "创建服务器失败",
})
return
}
h.recordServerAudit(c, "create", server.Hostname+" ("+server.IP+")", strconv.Itoa(int(server.ID)), 1, "")
c.JSON(http.StatusOK, model.Response{
Code: 200,
Message: "创建成功",
Data: server,
})
}
// UpdateServer 更新服务器
// @Summary 更新服务器
// @Description 更新指定服务器信息
// @Tags 服务器
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "服务器ID"
// @Param request body model.UpdateServerRequest true "更新请求"
// @Success 200 {object} model.Response{data=model.Server}
// @Router /servers/{id} [put]
func (h *ServerHandler) UpdateServer(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req model.UpdateServerRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
var server model.Server
if err := h.db.First(&server, id).Error; err != nil {
c.JSON(http.StatusNotFound, model.Response{
Code: 404,
Message: "服务器不存在",
})
return
}
updates := map[string]interface{}{}
if req.Hostname != "" {
updates["hostname"] = req.Hostname
}
if req.IP != "" {
updates["ip"] = req.IP
}
if req.PublicIP != "" {
updates["public_ip"] = req.PublicIP
}
if req.Port > 0 {
updates["port"] = req.Port
}
if req.Username != "" {
updates["username"] = req.Username
}
if req.Password != "" {
updates["password"] = req.Password
}
if req.SSHKey != "" {
updates["ssh_key"] = req.SSHKey
}
if req.AuthType != "" {
updates["auth_type"] = req.AuthType
}
// 关联 SSH 凭证:解密后覆盖认证字段(applyCredential 填充已 load 的 server,再同步到 updates) // Logan.Li
if req.CredentialID != nil && *req.CredentialID > 0 {
var cred model.Credential
if err := h.db.First(&cred, *req.CredentialID).Error; err != nil {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "凭据不存在"})
return
}
plain, err := h.crypt.Decrypt(cred.Content)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "凭据解密失败"})
return
}
applyCredential(cred, plain, &server)
updates["ssh_key"] = server.SSHKey
updates["password"] = server.Password
updates["auth_type"] = server.AuthType
updates["username"] = server.Username
updates["port"] = server.Port
}
if req.OS != "" {
updates["os"] = req.OS
}
if req.CPU > 0 {
updates["cpu"] = req.CPU
}
if req.Memory > 0 {
updates["memory"] = req.Memory
}
if req.Disk > 0 {
updates["disk"] = req.Disk
}
if req.Status != "" {
updates["status"] = req.Status
}
if req.RegionID != nil {
updates["region_id"] = *req.RegionID
}
if req.CloudID != nil {
updates["cloud_id"] = *req.CloudID
}
if req.Tags != "" {
updates["tags"] = req.Tags
}
if req.Description != "" {
updates["description"] = req.Description
}
if err := h.db.Model(&server).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.Response{
Code: 500,
Message: "更新失败",
})
return
}
h.db.Preload("Region").Preload("Cloud").First(&server, id)
h.recordServerAudit(c, "update", server.Hostname+" ("+server.IP+")", strconv.Itoa(int(server.ID)), 1, "")
c.JSON(http.StatusOK, model.Response{
Code: 200,
Message: "更新成功",
Data: server,
})
}
// DeleteServer 删除服务器
// @Summary 删除服务器
// @Description 删除指定服务器
// @Tags 服务器
// @Produce json
// @Security Bearer
// @Param id path int true "服务器ID"
// @Success 200 {object} model.Response
// @Router /servers/{id} [delete]
func (h *ServerHandler) DeleteServer(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var server model.Server
if err := h.db.First(&server, id).Error; err != nil {
c.JSON(http.StatusNotFound, model.Response{
Code: 404,
Message: "服务器不存在",
})
return
}
// 硬删除:先清理关联数据,再物理删除服务器记录
h.db.Unscoped().Where("hostname = ?", server.Hostname).Delete(&model.AgentReport{})
h.db.Unscoped().Where("hostname = ?", server.Hostname).Delete(&model.MetricsAggregate{})
if err := h.db.Unscoped().Delete(&model.Server{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.Response{
Code: 500,
Message: "删除失败",
})
return
}
h.recordServerAudit(c, "delete", server.Hostname+" ("+server.IP+")", strconv.Itoa(int(server.ID)), 1, "")
c.JSON(http.StatusOK, model.Response{
Code: 200,
Message: "服务器已彻底删除",
})
}
// GetServerStats 获取服务器统计
// @Summary 获取服务器统计
// @Description 获取服务器统计数据
// @Tags 服务器
// @Produce json
// @Security Bearer
// @Success 200 {object} model.Response{data=model.ServerStats}
// @Router /servers/stats [get]
func (h *ServerHandler) GetServerStats(c *gin.Context) {
var stats model.ServerStats
h.db.Model(&model.Server{}).Count(&stats.Total)
h.db.Model(&model.Server{}).Where("status = ?", "running").Count(&stats.Running)
h.db.Model(&model.Server{}).Where("status = ?", "stopped").Count(&stats.Stopped)
h.db.Model(&model.Server{}).Where("status = ?", "maintenance").Count(&stats.Maintenance)
h.db.Model(&model.Server{}).Where("status = ?", "error").Count(&stats.Error)
var avgCPU, avgMemory, avgDisk float64
h.db.Model(&model.Server{}).Select("AVG(cpu_usage)").Scan(&avgCPU)
h.db.Model(&model.Server{}).Select("AVG(memory_usage)").Scan(&avgMemory)
h.db.Model(&model.Server{}).Select("AVG(disk_usage)").Scan(&avgDisk)
stats.AvgCPU = avgCPU
stats.AvgMemory = avgMemory
stats.AvgDisk = avgDisk
c.JSON(http.StatusOK, model.Response{
Code: 200,
Message: "success",
Data: stats,
})
}