refactor(ai): 砍掉 /aiops 整页,模型配置迁入系统设置,浮窗助手保留
CI / Backend (go vet + test) (push) Waiting to run
CI / Frontend (lint + build + test) (push) Waiting to run

评估结论落地:整页分析只喂聚合统计(模型看不到日志原文),结论价值低于 /analysis
图表自身;浮窗助手携带真实查询样本,价值成立。
- 删 /aiops 页面与路由(菜单去项);llmProviders 迁 lib 共用
- 模型配置重做为设置页「AI 模型接入」区:状态严谨映射——供应商/模型按已存
  baseUrl/model 正确回显(自定义供应商显示 BaseURL 与自定义模型输入,不再重置),
  API Key 掩码占位、留空或掩码保存均沿用旧值(不回传掩码)
- 后端删 /ai/analyze 与信号采集链路;/ai/chat 保留(上下文由前端日志样本注入)
This commit is contained in:
opsx committed 2026-11-01 00:46:38 +08:00
1 parent c216bdfdd4
commit fbbd3a3c27
14 files changed
+203 -919

No files matched your search

+4 -66
View File
@@ -1,9 +1,7 @@
package controllers
import (
"fmt"
"strconv"
"time"
"lokistack/models"
"lokistack/services"
@@ -81,56 +79,6 @@ func containsMask(s string) bool {
return false
}
// Analyze 执行一次日志分析。
//
// 时间范围在服务端收敛:最大 24 小时。理由不是性能(P0 全走索引,很快),
// 而是"范围越大,信号越笼统,结论越没用",且 token 会线性增长。
func (ctrl *AIController) Analyze(c *gin.Context) {
var req struct {
SourceID uint `json:"sourceId" binding:"required"`
Start int64 `json:"start" binding:"required"`
End int64 `json:"end" binding:"required"`
Question string `json:"question"`
App string `json:"app"`
}
if !utils.BindAndValidate(c, &req) {
return
}
src := services.GetLogSource(req.SourceID)
if src == nil {
utils.BadRequest(c, "数据源不存在")
return
}
from := time.Unix(0, req.Start)
to := time.Unix(0, req.End)
if !to.After(from) {
utils.BadRequest(c, "时间范围无效:结束时间必须晚于开始时间")
return
}
if to.Sub(from) > 24*time.Hour {
utils.BadRequest(c, fmt.Sprintf("时间范围过大(%d 分钟):AI 分析最多支持 24 小时,建议用 15~60 分钟", int(to.Sub(from).Minutes())))
return
}
// 长窗口必须指定应用:全命名空间聚合在 6h/24h 下数据量过大,查询会拖垮 Loki
if to.Sub(from) > time.Hour && req.App == "" {
utils.BadRequest(c, "超过 1 小时的分析必须指定应用(app):全命名空间长窗口聚合过慢,请先在应用排行里选一个 app 缩小范围")
return
}
userID := c.GetUint("userId")
username := c.GetString("username")
admin := c.GetString("role") == "admin"
cfg := services.GetAIConfig()
outcome, err := services.RunAIAnalysis(c.Request.Context(), cfg, src, userID, username, admin, from, to, req.Question, req.App)
if err != nil {
// 未配置模型 / 配额用尽 / 上游报错:都给出具体原因,不包装成 500
utils.Error(c, 400, err.Error())
return
}
utils.Success(c, outcome)
}
// ListRuns 审计历史(分页)。
func (ctrl *AIController) ListRuns(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
@@ -157,25 +105,15 @@ func (ctrl *AIController) GetRun(c *gin.Context) {
// Chat 全站 AI 助手浮窗(多轮对话 + 页面上下文注入;Pro 闸门)
func (ctrl *AIController) Chat(c *gin.Context) {
var req struct {
SourceID uint `json:"sourceId"`
History []services.ChatMessage `json:"history" binding:"required,min=1,max=40"`
Page string `json:"page"`
RangeMinutes int `json:"rangeMinutes"`
Context string `json:"context"`
History []services.ChatMessage `json:"history" binding:"required,min=1,max=40"`
Page string `json:"page"`
Context string `json:"context"`
}
if !utils.BindAndValidate(c, &req) {
return
}
cfg := services.GetAIConfig()
var src *models.LogSource
if req.SourceID > 0 {
src = services.GetLogSource(req.SourceID)
}
rangeMinutes := req.RangeMinutes
if rangeMinutes <= 0 || rangeMinutes > 120 {
rangeMinutes = 15
}
reply, err := services.RunAIChat(c.Request.Context(), cfg, src, c.GetUint("userId"), c.GetString("role") == "admin", req.History, req.Page, rangeMinutes, req.Context)
reply, err := services.RunAIChat(c.Request.Context(), cfg, c.GetUint("userId"), req.History, req.Page, req.Context)
if err != nil {
utils.Error(c, 400, err.Error())
return
-1
View File
@@ -130,7 +130,6 @@ func SetupRouter() *gin.Engine {
ai.GET("/config", middleware.AdminAuth(), aiCtrl.GetConfig)
ai.PUT("/config", middleware.AdminAuth(), aiCtrl.SaveConfig)
ai.POST("/config/test", middleware.AdminAuth(), aiCtrl.TestConfig)
ai.POST("/analyze", middleware.RequirePro(), aiCtrl.Analyze)
ai.POST("/chat", middleware.RequirePro(), aiCtrl.Chat)
ai.GET("/runs", aiCtrl.ListRuns)
ai.GET("/runs/:id", aiCtrl.GetRun)
+1 -21
View File
@@ -155,7 +155,7 @@ type ChatMessage struct {
// RunAIChat 全站 AI 助手浮窗:多轮对话 + 当前页面上下文注入(轻量信号摘要)。
// 与整页分析共享配置/配额/脱敏/Pro 闸门;context 里的 sourceId 可选,带了就注入日志摘要。
func RunAIChat(ctx context.Context, cfg models.AIConfig, src *models.LogSource, userID uint, admin bool, history []ChatMessage, page string, rangeMinutes int, pageContext string) (string, error) {
func RunAIChat(ctx context.Context, cfg models.AIConfig, userID uint, history []ChatMessage, page string, pageContext string) (string, error) {
if !cfg.Enabled {
return "", fmt.Errorf("AI 未启用:请先在「智能运维 → 模型配置」中开启并保存")
}
@@ -170,26 +170,6 @@ func RunAIChat(ctx context.Context, cfg models.AIConfig, src *models.LogSource,
- 被问"你是什么模型/版本"时,只答一句:"我是 LokiStack 内置运维助手,底层模型由管理员在「智能运维 → 模型配置」中指定。"然后引导回日志排查。
- 只讨论日志、Loki、采集、告警与平台使用;无关话题(写代码、闲聊、其它产品)用一句礼貌拒绝并引导回排查。
- 引用数据时只使用下方"日志概况"中的数字,不得编造。`
if src != nil && rangeMinutes > 0 {
end := time.Now()
from := end.Add(-time.Duration(rangeMinutes) * time.Minute)
if bundle, err := CollectAISignals(ctx, src, userID, admin, from, end, ""); err == nil {
system += "\n\n当前用户正在查看 " + page + " 页,近 " + fmt.Sprintf("%d", rangeMinutes) + " 分钟的日志概况:总量 " +
fmt.Sprintf("%d", bundle.TotalEntries) + " 条(估算)"
if len(bundle.Apps) > 0 {
system += ",Top 应用:"
for i, a := range bundle.Apps {
if i >= 3 {
break
}
system += fmt.Sprintf(" %s(%d)", a.App, a.Entries)
}
}
if len(bundle.Alerts) > 0 {
system += ";存在告警命中"
}
}
}
// 页面上下文(如 /logs 刚执行的查询与样本日志):截断并统一脱敏后并入 system
if trimmed := strings.TrimSpace(pageContext); trimmed != "" {
+3 -3
View File
@@ -6,9 +6,9 @@ import (
)
// 脱敏的两条底线:
// 1) 凭据必须挡住(否则把 key 发给外部模型);
// 2) 排查必需的信息必须留着(IP/端口/命名空间/错误码),
// 否则模型给不出有用结论 —— 这比"脱得太干净"更常见。
// 1. 凭据必须挡住(否则把 key 发给外部模型);
// 2. 排查必需的信息必须留着(IP/端口/命名空间/错误码),
// 否则模型给不出有用结论 —— 这比"脱得太干净"更常见。
func TestRedactText(t *testing.T) {
in := `2026-10-30 12:00:01 ERROR redis client: dial tcp 172.19.16.17:6379: connect: connection refused
password=Sup3rS3cret! user-admin
@@ -15,10 +15,10 @@ func TestQueryParallelism(t *testing.T) {
{"4", 4},
{"8", 8},
{"2", 2},
{"1", 2}, // 低于 2 核按 2 兜底
{"16", 12}, // 上限 12
{"", 4}, // 空值走默认
{"abc", 4}, // 非法值走默认
{"1", 2}, // 低于 2 核按 2 兜底
{"16", 12}, // 上限 12
{"", 4}, // 空值走默认
{"abc", 4}, // 非法值走默认
{"4000m", 4}, // 毫核写法
{"500m", 2}, // 0.5 核 → 向上取整 1 → 不低于 2
{"6000m", 6},