diff --git a/server/api/v1/system/sys_auto_code.go b/server/api/v1/system/sys_auto_code.go index 02f458bb3..1283b0208 100644 --- a/server/api/v1/system/sys_auto_code.go +++ b/server/api/v1/system/sys_auto_code.go @@ -1,15 +1,10 @@ package system import ( - "fmt" "github.com/flipped-aurora/gin-vue-admin/server/model/common" - "github.com/goccy/go-json" - "io" - "strings" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" - "github.com/flipped-aurora/gin-vue-admin/server/utils/request" "github.com/gin-gonic/gin" "go.uber.org/zap" ) @@ -108,48 +103,15 @@ func (autoApi *AutoCodeApi) GetColumn(c *gin.Context) { func (autoApi *AutoCodeApi) LLMAuto(c *gin.Context) { var llm common.JSONMap - err := c.ShouldBindJSON(&llm) - if err != nil { + if err := c.ShouldBindJSON(&llm); err != nil { response.FailWithMessage(err.Error(), c) return } - if global.GVA_CONFIG.AutoCode.AiPath == "" { - response.FailWithMessage("请先前往插件市场个人中心获取AiPath并填入config.yaml中", c) - return - } - - path := strings.ReplaceAll(global.GVA_CONFIG.AutoCode.AiPath, "{FUNC}", fmt.Sprintf("api/chat/%s", llm["mode"])) - res, err := request.HttpRequest( - path, - "POST", - nil, - nil, - llm, - ) + data, err := autoCodeService.LLMAuto(c.Request.Context(), llm) if err != nil { global.GVA_LOG.Error("大模型生成失败!", zap.Error(err)) response.FailWithMessage("大模型生成失败"+err.Error(), c) return } - var resStruct response.Response - b, err := io.ReadAll(res.Body) - defer res.Body.Close() - if err != nil { - global.GVA_LOG.Error("大模型生成失败!", zap.Error(err)) - response.FailWithMessage("大模型生成失败"+err.Error(), c) - return - } - err = json.Unmarshal(b, &resStruct) - if err != nil { - global.GVA_LOG.Error("大模型生成失败!", zap.Error(err)) - response.FailWithMessage("大模型生成失败"+err.Error(), c) - return - } - - if resStruct.Code == 7 { - global.GVA_LOG.Error("大模型生成失败!"+resStruct.Msg, zap.Error(err)) - response.FailWithMessage("大模型生成失败"+resStruct.Msg, c) - return - } - response.OkWithData(resStruct.Data, c) + response.OkWithData(data, c) } diff --git a/server/api/v1/system/sys_error.go b/server/api/v1/system/sys_error.go index 0c50aae96..966cd5088 100644 --- a/server/api/v1/system/sys_error.go +++ b/server/api/v1/system/sys_error.go @@ -167,3 +167,33 @@ func (sysErrorApi *SysErrorApi) GetSysErrorList(c *gin.Context) { PageSize: pageInfo.PageSize, }, "获取成功", c) } + +// GetSysErrorSolution 触发错误日志的异步处理 +// @Tags SysError +// @Summary 根据ID触发处理:标记为处理中,1分钟后自动改为处理完成 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param id query string true "错误日志ID" +// @Success 200 {object} response.Response{msg=string} "处理已提交" +// @Router /sysError/getSysErrorSolution [get] +func (sysErrorApi *SysErrorApi) GetSysErrorSolution(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + // 兼容 id 与 ID 两种参数 + ID := c.Query("id") + if ID == "" { + response.FailWithMessage("缺少参数: id", c) + return + } + + err := sysErrorService.GetSysErrorSolution(ctx, ID) + if err != nil { + global.GVA_LOG.Error("处理触发失败!", zap.Error(err)) + response.FailWithMessage("处理触发失败:"+err.Error(), c) + return + } + + response.OkWithMessage("已提交至AI处理", c) +} diff --git a/server/core/internal/zap_core.go b/server/core/internal/zap_core.go index 4648e60cb..2e5455745 100644 --- a/server/core/internal/zap_core.go +++ b/server/core/internal/zap_core.go @@ -1,11 +1,18 @@ package internal import ( - "github.com/flipped-aurora/gin-vue-admin/server/global" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - "os" - "time" + "context" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/service" + astutil "github.com/flipped-aurora/gin-vue-admin/server/utils/ast" + "github.com/flipped-aurora/gin-vue-admin/server/utils/stacktrace" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "os" + "strings" + "time" ) type ZapCore struct { @@ -54,13 +61,75 @@ func (z *ZapCore) Check(entry zapcore.Entry, check *zapcore.CheckedEntry) *zapco } func (z *ZapCore) Write(entry zapcore.Entry, fields []zapcore.Field) error { - for i := 0; i < len(fields); i++ { - if fields[i].Key == "business" || fields[i].Key == "folder" || fields[i].Key == "directory" { - syncer := z.WriteSyncer(fields[i].String) - z.Core = zapcore.NewCore(global.GVA_CONFIG.Zap.Encoder(), syncer, z.level) - } - } - return z.Core.Write(entry, fields) + for i := 0; i < len(fields); i++ { + if fields[i].Key == "business" || fields[i].Key == "folder" || fields[i].Key == "directory" { + syncer := z.WriteSyncer(fields[i].String) + z.Core = zapcore.NewCore(global.GVA_CONFIG.Zap.Encoder(), syncer, z.level) + } + } + // 先写入原日志目标 + err := z.Core.Write(entry, fields) + + // 捕捉 Error 及以上级别日志并入库,且可提取 zap.Error(err) 的错误内容 + if entry.Level >= zapcore.ErrorLevel { + // 避免与 GORM zap 写入互相递归:跳过由 gorm logger writer 触发的日志 + if strings.Contains(entry.Caller.File, "gorm_logger_writer.go") { + return err + } + // 避免重复记录 panic 恢复日志,panic 由 GinRecovery 单独捕捉入库 + if strings.Contains(entry.Message, "[Recovery from panic]") { + return err + } + + form := "后端" + level := entry.Level.String() + // 生成基础信息 + info := entry.Message + + // 提取 zap.Error(err) 内容 + var errStr string + for i := 0; i < len(fields); i++ { + f := fields[i] + if f.Type == zapcore.ErrorType || f.Key == "error" || f.Key == "err" { + if f.Interface != nil { + errStr = fmt.Sprintf("%v", f.Interface) + } else if f.String != "" { + errStr = f.String + } + break + } + } + if errStr != "" { + info = fmt.Sprintf("%s | 错误: %s", info, errStr) + } + + // 附加来源与堆栈信息 + if entry.Caller.File != "" { + info = fmt.Sprintf("%s \n 源文件:%s:%d", info, entry.Caller.File, entry.Caller.Line) + } + stack := entry.Stack + if stack != "" { + info = fmt.Sprintf("%s \n 调用栈:%s", info, stack) + // 解析最终业务调用方,并提取其方法源码 + if frame, ok := stacktrace.FindFinalCaller(stack); ok { + fnName, fnSrc, sLine, eLine, exErr := astutil.ExtractFuncSourceByPosition(frame.File, frame.Line) + if exErr == nil { + info = fmt.Sprintf("%s \n 最终调用方法:%s:%d (%s lines %d-%d)\n----- 产生日志的方法代码如下 -----\n%s", info, frame.File, frame.Line, fnName, sLine, eLine, fnSrc) + } else { + info = fmt.Sprintf("%s \n 最终调用方法:%s:%d (%s) | extract_err=%v", info, frame.File, frame.Line, fnName, exErr) + } + } + } + + // 使用后台上下文,避免依赖 gin.Context + ctx := context.Background() + _ = service.ServiceGroupApp.SystemServiceGroup.SysErrorService.CreateSysError(ctx, &system.SysError{ + Form: &form, + Info: &info, + Level: level, + }) + } + return err } func (z *ZapCore) Sync() error { diff --git a/server/core/zap.go b/server/core/zap.go index 3e8bd47ab..09aa16528 100644 --- a/server/core/zap.go +++ b/server/core/zap.go @@ -1,19 +1,13 @@ package core import ( - "context" - "fmt" - "github.com/flipped-aurora/gin-vue-admin/server/core/internal" - "github.com/flipped-aurora/gin-vue-admin/server/global" - "github.com/flipped-aurora/gin-vue-admin/server/model/system" - "github.com/flipped-aurora/gin-vue-admin/server/service" - "github.com/flipped-aurora/gin-vue-admin/server/utils" - astutil "github.com/flipped-aurora/gin-vue-admin/server/utils/ast" - "github.com/flipped-aurora/gin-vue-admin/server/utils/stacktrace" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - "os" - "strings" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/core/internal" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "os" ) // Zap 获取 zap.Logger @@ -30,53 +24,8 @@ func Zap() (logger *zap.Logger) { core := internal.NewZapCore(levels[i]) cores = append(cores, core) } - // 通过 Hooks 捕捉 Error 及以上级别日志,写入系统错误表 - dbHook := zap.Hooks(func(entry zapcore.Entry) error { - // 仅处理 Error 及以上级别 - if entry.Level < zapcore.ErrorLevel { - return nil - } - // 避免与 GORM zap 写入互相递归:跳过由 gorm logger writer 触发的日志 - if strings.Contains(entry.Caller.File, "gorm_logger_writer.go") { - return nil - } - // 避免重复记录 panic 恢复日志,panic 由 GinRecovery 单独捕捉入库 - if strings.Contains(entry.Message, "[Recovery from panic]") { - return nil - } - - form := "后端" - level := entry.Level.String() - // 尽可能携带来源与堆栈信息(使用 runtime 采集并过滤 zap 内部栈) - info := entry.Message - if entry.Caller.File != "" { - info = fmt.Sprintf("错误信息:%s", info) - } - stack := entry.Stack - if stack != "" { - info = fmt.Sprintf("%s \n 调用栈:%s", info, stack) - // 解析最终业务调用方,并提取其方法源码 - if frame, ok := stacktrace.FindFinalCaller(stack); ok { - fnName, fnSrc, sLine, eLine, exErr := astutil.ExtractFuncSourceByPosition(frame.File, frame.Line) - if exErr == nil { - info = fmt.Sprintf("%s \n 最终调用方法:%s:%d (%s lines %d-%d)\n----- 产生日志的方法代码如下 -----\n%s", info, frame.File, frame.Line, fnName, sLine, eLine, fnSrc) - } else { - info = fmt.Sprintf("%s \n 最终调用方法:%s:%d (%s) | extract_err=%v", info, frame.File, frame.Line, fnName, exErr) - } - } - } - - // 使用后台上下文,避免依赖 gin.Context - ctx := context.Background() - _ = service.ServiceGroupApp.SystemServiceGroup.SysErrorService.CreateSysError(ctx, &system.SysError{ - Form: &form, - Info: &info, - Level: level, - }) - return nil - }) - - logger = zap.New(zapcore.NewTee(cores...), dbHook) + // 构建基础 logger(错误级别的入库逻辑已在自定义 ZapCore 中处理) + logger = zap.New(zapcore.NewTee(cores...)) // 启用 Error 及以上级别的堆栈捕捉,确保 entry.Stack 可用 opts := []zap.Option{zap.AddStacktrace(zapcore.ErrorLevel)} if global.GVA_CONFIG.Zap.ShowLine { diff --git a/server/model/system/sys_error.go b/server/model/system/sys_error.go index 191a3153a..7f5671b04 100644 --- a/server/model/system/sys_error.go +++ b/server/model/system/sys_error.go @@ -11,7 +11,8 @@ type SysError struct { Form *string `json:"form" form:"form" gorm:"comment:错误来源;column:form;type:text;" binding:"required"` //错误来源 Info *string `json:"info" form:"info" gorm:"comment:错误内容;column:info;type:text;"` //错误内容 Level string `json:"level" form:"level" gorm:"comment:日志等级;column:level;"` - Solution *string `json:"solution" form:"solution" gorm:"comment:解决方案;column:solution;"` //解决方案 + Solution *string `json:"solution" form:"solution" gorm:"comment:解决方案;column:solution;type:text"` //解决方案 + Status string `json:"status" form:"status" gorm:"comment:处理状态;column:status;type:varchar(20);default:未处理;"` //处理状态:未处理/处理中/处理完成 } // TableName 错误日志 SysError自定义表名 sys_error diff --git a/server/router/system/sys_error.go b/server/router/system/sys_error.go index c14e16078..ea3860207 100644 --- a/server/router/system/sys_error.go +++ b/server/router/system/sys_error.go @@ -9,19 +9,20 @@ type SysErrorRouter struct{} // InitSysErrorRouter 初始化 错误日志 路由信息 func (s *SysErrorRouter) InitSysErrorRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) { - sysErrorRouter := Router.Group("sysError").Use(middleware.OperationRecord()) - sysErrorRouterWithoutRecord := Router.Group("sysError") - sysErrorRouterWithoutAuth := PublicRouter.Group("sysError") - { - sysErrorRouter.DELETE("deleteSysError", sysErrorApi.DeleteSysError) // 删除错误日志 - sysErrorRouter.DELETE("deleteSysErrorByIds", sysErrorApi.DeleteSysErrorByIds) // 批量删除错误日志 - sysErrorRouter.PUT("updateSysError", sysErrorApi.UpdateSysError) // 更新错误日志 - } - { - sysErrorRouterWithoutRecord.GET("findSysError", sysErrorApi.FindSysError) // 根据ID获取错误日志 - sysErrorRouterWithoutRecord.GET("getSysErrorList", sysErrorApi.GetSysErrorList) // 获取错误日志列表 - } - { - sysErrorRouterWithoutAuth.POST("createSysError", sysErrorApi.CreateSysError) // 新建错误日志 - } + sysErrorRouter := Router.Group("sysError").Use(middleware.OperationRecord()) + sysErrorRouterWithoutRecord := Router.Group("sysError") + sysErrorRouterWithoutAuth := PublicRouter.Group("sysError") + { + sysErrorRouter.DELETE("deleteSysError", sysErrorApi.DeleteSysError) // 删除错误日志 + sysErrorRouter.DELETE("deleteSysErrorByIds", sysErrorApi.DeleteSysErrorByIds) // 批量删除错误日志 + sysErrorRouter.PUT("updateSysError", sysErrorApi.UpdateSysError) // 更新错误日志 + sysErrorRouter.GET("getSysErrorSolution", sysErrorApi.GetSysErrorSolution) // 触发错误日志处理 + } + { + sysErrorRouterWithoutRecord.GET("findSysError", sysErrorApi.FindSysError) // 根据ID获取错误日志 + sysErrorRouterWithoutRecord.GET("getSysErrorList", sysErrorApi.GetSysErrorList) // 获取错误日志列表 + } + { + sysErrorRouterWithoutAuth.POST("createSysError", sysErrorApi.CreateSysError) // 新建错误日志 + } } diff --git a/server/service/system/auto_code_llm.go b/server/service/system/auto_code_llm.go new file mode 100644 index 000000000..76b4fb4b5 --- /dev/null +++ b/server/service/system/auto_code_llm.go @@ -0,0 +1,51 @@ +package system + +import ( + "context" + "errors" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common" + commonResp "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/utils/request" + "github.com/goccy/go-json" + "io" + "strings" +) + +// LLMAuto 调用大模型服务,返回生成结果数据 +// 入参为通用 JSONMap,需包含 mode(例如 ai/butler/eye/painter 等)以及业务 prompt/payload +func (s *AutoCodeService) LLMAuto(ctx context.Context, llm common.JSONMap) (interface{}, error) { + if global.GVA_CONFIG.AutoCode.AiPath == "" { + return nil, errors.New("请先前往插件市场个人中心获取AiPath并填入config.yaml中") + } + + // 构建调用路径:{AiPath} 中的 {FUNC} 由 mode 替换 + mode := fmt.Sprintf("%v", llm["mode"]) // 统一转字符串,避免 nil 造成路径异常 + path := strings.ReplaceAll(global.GVA_CONFIG.AutoCode.AiPath, "{FUNC}", fmt.Sprintf("api/chat/%s", mode)) + + res, err := request.HttpRequest( + path, + "POST", + nil, + nil, + llm, + ) + if err != nil { + return nil, fmt.Errorf("大模型生成失败: %w", err) + } + defer res.Body.Close() + + var resStruct commonResp.Response + b, err := io.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("读取大模型响应失败: %w", err) + } + if err = json.Unmarshal(b, &resStruct); err != nil { + return nil, fmt.Errorf("解析大模型响应失败: %w", err) + } + if resStruct.Code == 7 { // 业务约定:7 表示模型生成失败 + return nil, fmt.Errorf("大模型生成失败: %s", resStruct.Msg) + } + return resStruct.Data, nil +} diff --git a/server/service/system/sys_error.go b/server/service/system/sys_error.go index 0b889e3a1..c9d1b1cb8 100644 --- a/server/service/system/sys_error.go +++ b/server/service/system/sys_error.go @@ -2,7 +2,9 @@ package system import ( "context" + "fmt" "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common" "github.com/flipped-aurora/gin-vue-admin/server/model/system" systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" ) @@ -75,3 +77,48 @@ func (sysErrorService *SysErrorService) GetSysErrorInfoList(ctx context.Context, err = db.Find(&sysErrors).Error return sysErrors, total, err } + +// GetSysErrorSolution 异步处理错误 +// Author [yourname](https://github.com/yourname) +func (sysErrorService *SysErrorService) GetSysErrorSolution(ctx context.Context, ID string) (err error) { + // 立即更新为处理中 + err = global.GVA_DB.WithContext(ctx).Model(&system.SysError{}).Where("id = ?", ID).Update("status", "处理中").Error + if err != nil { + return err + } + + // 异步协程在一分钟后更新为处理完成 + go func(id string) { + // 查询当前错误信息用于生成方案 + var se system.SysError + _ = global.GVA_DB.Model(&system.SysError{}).Where("id = ?", id).First(&se).Error + + // 构造 LLM 请求参数,使用管家模式(butler)根据错误信息生成解决方案 + var form, info string + if se.Form != nil { + form = *se.Form + } + if se.Info != nil { + info = *se.Info + } + + llmReq := common.JSONMap{ + "mode": "solution", + "command": "solution", + "info": info, + "form": form, + } + + // 调用服务层 LLMAuto,忽略错误但尽量写入方案 + var solution string + if data, err := (&AutoCodeService{}).LLMAuto(context.Background(), llmReq); err == nil { + solution = fmt.Sprintf("%v", data) + _ = global.GVA_DB.Model(&system.SysError{}).Where("id = ?", id).Updates(map[string]interface{}{"status": "处理完成", "solution": solution}).Error + } else { + // 即使生成失败也标记为完成,避免任务卡住 + _ = global.GVA_DB.Model(&system.SysError{}).Where("id = ?", id).Update("status", "处理失败").Error + } + }(ID) + + return nil +} diff --git a/server/source/system/api.go b/server/source/system/api.go index 4e40074f8..9500ee865 100644 --- a/server/source/system/api.go +++ b/server/source/system/api.go @@ -184,6 +184,7 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) { {ApiGroup: "错误日志", Method: "PUT", Path: "/sysError/updateSysError", Description: "更新错误日志"}, {ApiGroup: "错误日志", Method: "GET", Path: "/sysError/findSysError", Description: "根据ID获取错误日志"}, {ApiGroup: "错误日志", Method: "GET", Path: "/sysError/getSysErrorList", Description: "获取错误日志列表"}, + {ApiGroup: "错误日志", Method: "GET", Path: "/sysError/getSysErrorSolution", Description: "触发错误处理(异步)"}, {ApiGroup: "公告", Method: "POST", Path: "/info/createInfo", Description: "新建公告"}, {ApiGroup: "公告", Method: "DELETE", Path: "/info/deleteInfo", Description: "删除公告"}, diff --git a/server/source/system/casbin.go b/server/source/system/casbin.go index 0beaf9a40..c63dfa1b7 100644 --- a/server/source/system/casbin.go +++ b/server/source/system/casbin.go @@ -185,6 +185,7 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error {Ptype: "p", V0: "888", V1: "/sysError/updateSysError", V2: "PUT"}, {Ptype: "p", V0: "888", V1: "/sysError/findSysError", V2: "GET"}, {Ptype: "p", V0: "888", V1: "/sysError/getSysErrorList", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysError/getSysErrorSolution", V2: "GET"}, {Ptype: "p", V0: "888", V1: "/info/createInfo", V2: "POST"}, {Ptype: "p", V0: "888", V1: "/info/deleteInfo", V2: "DELETE"}, diff --git a/web/src/api/system/sysError.js b/web/src/api/system/sysError.js index 6452b4527..4b3271bc3 100644 --- a/web/src/api/system/sysError.js +++ b/web/src/api/system/sysError.js @@ -108,3 +108,19 @@ export const getSysErrorPublic = () => { method: 'get', }) } + +// @Tags SysError +// @Summary 触发错误处理(异步) +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param id query string true "错误日志ID" +// @Success 200 {string} string "{\"success\":true,\"data\":{},\"msg\":\"处理已提交\"}" +// @Router /sysError/getSysErrorSolution [get] +export const getSysErrorSolution = (params) => { + return service({ + url: '/sysError/getSysErrorSolution', + method: 'get', + params + }) +} \ No newline at end of file diff --git a/web/src/view/systemTools/sysError/sysError.vue b/web/src/view/systemTools/sysError/sysError.vue index 6284d1c79..fcf0e69ec 100644 --- a/web/src/view/systemTools/sysError/sysError.vue +++ b/web/src/view/systemTools/sysError/sysError.vue @@ -112,6 +112,13 @@ width="120" /> + +