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