feat(frontend):增加前端错误捕获和后端panic处理

- 在前端 App.vue、settings.vue 和 stock.vue 中添加 window.onerror 事件处理器,捕获前端错误并发送给后端
- 在后端 app.go 和 openai_api.go 中添加 panic 处理逻辑,捕获并记录 panic错误
- 在 main.go 中添加 PanicHandler 函数,用于捕获和处理全局 panic
This commit is contained in:
spark 2025-02-10 17:46:42 +08:00
parent 22111411c3
commit b4c55ce233
6 changed files with 80 additions and 7 deletions

8
app.go
View File

@ -41,6 +41,10 @@ func NewApp() *App {
// startup is called at application startup // startup is called at application startup
func (a *App) startup(ctx context.Context) { func (a *App) startup(ctx context.Context) {
defer PanicHandler()
runtime.EventsOn(ctx, "frontendError", func(optionalData ...interface{}) {
logger.SugaredLogger.Errorf("Frontend error: %v\n", optionalData)
})
logger.SugaredLogger.Infof("Version:%s", Version) logger.SugaredLogger.Infof("Version:%s", Version)
// Perform your setup here // Perform your setup here
a.ctx = ctx a.ctx = ctx
@ -86,6 +90,8 @@ func checkUpdate(a *App) {
// domReady is called after front-end resources have been loaded // domReady is called after front-end resources have been loaded
func (a *App) domReady(ctx context.Context) { func (a *App) domReady(ctx context.Context) {
defer PanicHandler()
// Add your action here // Add your action here
//定时更新数据 //定时更新数据
go func() { go func() {
@ -321,6 +327,7 @@ func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) {
// either by clicking the window close button or calling runtime.Quit. // either by clicking the window close button or calling runtime.Quit.
// Returning true will cause the application to continue, false will continue shutdown as normal. // Returning true will cause the application to continue, false will continue shutdown as normal.
func (a *App) beforeClose(ctx context.Context) (prevent bool) { func (a *App) beforeClose(ctx context.Context) (prevent bool) {
defer PanicHandler()
dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{ dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{
Type: runtime.QuestionDialog, Type: runtime.QuestionDialog,
@ -344,6 +351,7 @@ func (a *App) beforeClose(ctx context.Context) (prevent bool) {
// shutdown is called at application termination // shutdown is called at application termination
func (a *App) shutdown(ctx context.Context) { func (a *App) shutdown(ctx context.Context) {
defer PanicHandler()
// Perform your teardown here // Perform your teardown here
systray.Quit() systray.Quit()
} }

View File

@ -76,7 +76,19 @@ type AiResponse struct {
func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string { func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
ch := make(chan string, 512) ch := make(chan string, 512)
defer func() {
if err := recover(); err != nil {
logger.SugaredLogger.Error("NewChatStream panic", err)
}
}()
go func() { go func() {
defer func() {
if err := recover(); err != nil {
logger.SugaredLogger.Error("NewChatStream goroutine panic", err)
}
}()
defer close(ch) defer close(ch)
msg := []map[string]interface{}{ msg := []map[string]interface{}{
{ {
@ -174,7 +186,7 @@ func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
client.SetBaseURL(o.BaseUrl) client.SetBaseURL(o.BaseUrl)
client.SetHeader("Authorization", "Bearer "+o.ApiKey) client.SetHeader("Authorization", "Bearer "+o.ApiKey)
client.SetHeader("Content-Type", "application/json") client.SetHeader("Content-Type", "application/json")
client.SetRetryCount(3) //client.SetRetryCount(3)
if o.TimeOut <= 0 { if o.TimeOut <= 0 {
o.TimeOut = 300 o.TimeOut = 300
} }
@ -190,14 +202,15 @@ func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
}). }).
Post("/chat/completions") Post("/chat/completions")
defer resp.RawBody().Close() body := resp.RawBody()
defer body.Close()
if err != nil { if err != nil {
logger.SugaredLogger.Infof("Stream error : %s", err.Error()) logger.SugaredLogger.Infof("Stream error : %s", err.Error())
ch <- err.Error() ch <- err.Error()
return return
} }
scanner := bufio.NewScanner(resp.RawBody()) scanner := bufio.NewScanner(body)
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
logger.SugaredLogger.Infof("Received data: %s", line) logger.SugaredLogger.Infof("Received data: %s", line)
@ -232,8 +245,13 @@ func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
} }
} }
} else { } else {
logger.SugaredLogger.Infof("Stream data error : %s", err.Error()) if err != nil {
ch <- err.Error() logger.SugaredLogger.Infof("Stream data error : %s", err.Error())
ch <- err.Error()
} else {
logger.SugaredLogger.Infof("Stream data error : %s", data)
ch <- data
}
} }
} else { } else {
ch <- line ch <- line

View File

@ -1,6 +1,7 @@
<script setup> <script setup>
import stockInfo from './components/stock.vue' import stockInfo from './components/stock.vue'
import { import {
EventsEmit,
EventsOn, EventsOn,
Quit, Quit,
WindowFullscreen, WindowGetPosition, WindowFullscreen, WindowGetPosition,
@ -157,6 +158,18 @@ EventsOn("realtime_profit",(data)=>{
EventsOn("telegraph",(data)=>{ EventsOn("telegraph",(data)=>{
telegraph.value=data telegraph.value=data
}) })
window.onerror = function (message, source, lineno, colno, error) {
//
EventsEmit("frontendError", {
message: message,
source: source,
lineno: lineno,
colno: colno,
error: error ? error.stack : null
});
return true;
};
</script> </script>
<template> <template>

View File

@ -4,6 +4,7 @@ import {onMounted, ref, watch} from "vue";
import {ExportConfig, GetConfig, SendDingDingMessageByType, UpdateConfig} from "../../wailsjs/go/main/App"; import {ExportConfig, GetConfig, SendDingDingMessageByType, UpdateConfig} from "../../wailsjs/go/main/App";
import {useMessage} from "naive-ui"; import {useMessage} from "naive-ui";
import {data} from "../../wailsjs/go/models"; import {data} from "../../wailsjs/go/models";
import {EventsEmit} from "../../wailsjs/runtime";
const message = useMessage() const message = useMessage()
const formRef = ref(null) const formRef = ref(null)
@ -150,6 +151,19 @@ function importConfig(){
}; };
input.click(); input.click();
} }
window.onerror = function (message, source, lineno, colno, error) {
//
EventsEmit("frontendError", {
message: message,
source: source,
lineno: lineno,
colno: colno,
error: error ? error.stack : null
});
return true;
};
</script> </script>
<template> <template>

View File

@ -22,7 +22,7 @@ import {
useModal, useModal,
useNotification useNotification
} from 'naive-ui' } from 'naive-ui'
import {EventsOn, WindowFullscreen, WindowReload, WindowUnfullscreen} from '../../wailsjs/runtime' import {EventsEmit, EventsOn, WindowFullscreen, WindowReload, WindowUnfullscreen} from '../../wailsjs/runtime'
import {Add, Search,StarOutline} from '@vicons/ionicons5' import {Add, Search,StarOutline} from '@vicons/ionicons5'
import { MdPreview } from 'md-editor-v3'; import { MdPreview } from 'md-editor-v3';
// preview.cssstyle.css // preview.cssstyle.css
@ -546,7 +546,17 @@ function getHeight() {
return document.documentElement.clientHeight return document.documentElement.clientHeight
} }
window.onerror = function (message, source, lineno, colno, error) {
//
EventsEmit("frontendError", {
message: message,
source: source,
lineno: lineno,
colno: colno,
error: error ? error.stack : null
});
return true;
};
</script> </script>
<template> <template>

10
main.go
View File

@ -3,6 +3,7 @@ package main
import ( import (
"embed" "embed"
"encoding/json" "encoding/json"
"fmt"
"github.com/duke-git/lancet/v2/convertor" "github.com/duke-git/lancet/v2/convertor"
"github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/logger" "github.com/wailsapp/wails/v2/pkg/logger"
@ -18,6 +19,7 @@ import (
"log" "log"
"os" "os"
goruntime "runtime" goruntime "runtime"
"runtime/debug"
"time" "time"
) )
@ -206,3 +208,11 @@ func checkDir(dir string) {
logger.NewDefaultLogger().Info("create dir: " + dir) logger.NewDefaultLogger().Info("create dir: " + dir)
} }
} }
// PanicHandler 捕获 panic 的包装函数
func PanicHandler() {
if r := recover(); r != nil {
fmt.Printf("Recovered from panic: %v\n", r)
debug.PrintStack()
}
}