Compare commits
28 Commits
v2025.2.5
...
v2025.2.9.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d541e81a2 | ||
|
|
6dfe3fd135 | ||
|
|
915e12eab3 | ||
|
|
bcfcbfeef0 | ||
|
|
1dc731de1e | ||
|
|
a580f9254a | ||
|
|
9b080bbb45 | ||
|
|
86183f4585 | ||
|
|
97ab29259a | ||
|
|
91f3e66239 | ||
|
|
713b25d2db | ||
|
|
d0b65e7063 | ||
|
|
f062306158 | ||
|
|
ae7b617e83 | ||
|
|
1035f2a800 | ||
|
|
cb28b18541 | ||
|
|
9d42eb2729 | ||
|
|
7b93d4d8ca | ||
|
|
3e13ef007b | ||
|
|
6ff1b68f1b | ||
|
|
a6547db195 | ||
|
|
567414a136 | ||
|
|
34dc38a95f | ||
|
|
6d2ab3ef41 | ||
|
|
e55506705e | ||
|
|
322e87efbd | ||
|
|
1628381295 | ||
|
|
8afc26badb |
10
.github/workflows/main.yml
vendored
10
.github/workflows/main.yml
vendored
@@ -31,11 +31,19 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Get commit message
|
||||
id: get_commit_message
|
||||
run: |
|
||||
$commit_message = & git log -1 --pretty=format:"%s"
|
||||
echo "::set-output name=commit_message::$commit_message"
|
||||
|
||||
- name: Build wails
|
||||
uses: ArvinLovegood/wails-build-action@v2.3
|
||||
uses: ArvinLovegood/wails-build-action@v2.8
|
||||
id: build
|
||||
with:
|
||||
build-name: ${{ matrix.build.name }}
|
||||
build-platform: ${{ matrix.build.platform }}
|
||||
package: true
|
||||
go-version: '1.23'
|
||||
build-tags: ${{ github.ref_name }}
|
||||
build-commit-message: ${{ steps.get_commit_message.outputs.commit_message }}
|
||||
|
||||
11
README.md
11
README.md
@@ -1,9 +1,11 @@
|
||||

|
||||
## 自选股行情实时监控,基于Wails和NaiveUI构建的AI赋能股票分析工具
|
||||
经测试目前硅基流动(siliconflow)提供的deepSeek api 服务比较稳定,注册即送2000万Tokens,[注册链接](https://cloud.siliconflow.cn/i/foufCerk)
|
||||
- 经测试目前硅基流动(siliconflow)提供的deepSeek api 服务比较稳定,注册即送2000万Tokens,[注册链接](https://cloud.siliconflow.cn/i/foufCerk)
|
||||
- 软件快速迭代开发中,请大家优先测试和使用最新发布的版本。
|
||||
- 欢迎大家提出宝贵的建议,欢迎提issue,PR。当然更欢迎[赞助我](#都划到这了如果我的项目对您有帮助请赞助我吧),谢谢。
|
||||
## BIG NEWS !!! 重大更新!!!
|
||||
- 2025.01.17 新增AI大模型分析股票功能
|
||||

|
||||

|
||||
|
||||
## 简介
|
||||
- 本项目基于Wails和NaiveUI,纯属无聊,仅供娱乐,不喜勿喷。
|
||||
@@ -23,8 +25,9 @@
|
||||
### 钉钉报警通知
|
||||

|
||||
### AI分析股票
|
||||

|
||||
|
||||

|
||||
### 版本信息提示
|
||||

|
||||
## About
|
||||
|
||||
### A China stock data viewer build by [Wails](https://wails.io/) with [NavieUI](https://www.naiveui.com/).
|
||||
|
||||
61
app.go
61
app.go
@@ -4,6 +4,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/coocood/freecache"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -37,6 +39,7 @@ func NewApp() *App {
|
||||
|
||||
// startup is called at application startup
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
logger.SugaredLogger.Infof("Version:%s", Version)
|
||||
// Perform your setup here
|
||||
a.ctx = ctx
|
||||
|
||||
@@ -49,6 +52,36 @@ func (a *App) startup(ctx context.Context) {
|
||||
|
||||
}
|
||||
|
||||
func checkUpdate(a *App) {
|
||||
releaseVersion := &models.GitHubReleaseVersion{}
|
||||
_, err := resty.New().R().
|
||||
SetResult(releaseVersion).
|
||||
Get("https://api.github.com/repos/ArvinLovegood/go-stock/releases/latest")
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("get github release version error:%s", err.Error())
|
||||
return
|
||||
}
|
||||
logger.SugaredLogger.Infof("releaseVersion:%+v", releaseVersion.TagName)
|
||||
if releaseVersion.TagName != Version {
|
||||
tag := &models.Tag{}
|
||||
_, err = resty.New().R().
|
||||
SetResult(tag).
|
||||
Get("https://api.github.com/repos/ArvinLovegood/go-stock/git/ref/tags/" + releaseVersion.TagName)
|
||||
if err == nil {
|
||||
releaseVersion.Tag = *tag
|
||||
}
|
||||
commit := &models.Commit{}
|
||||
_, err = resty.New().R().
|
||||
SetResult(commit).
|
||||
Get(tag.Object.Url)
|
||||
if err == nil {
|
||||
releaseVersion.Commit = *commit
|
||||
}
|
||||
|
||||
go runtime.EventsEmit(a.ctx, "updateVersion", releaseVersion)
|
||||
}
|
||||
}
|
||||
|
||||
// domReady is called after front-end resources have been loaded
|
||||
func (a *App) domReady(ctx context.Context) {
|
||||
// Add your action here
|
||||
@@ -81,6 +114,11 @@ func (a *App) domReady(ctx context.Context) {
|
||||
}()
|
||||
go runtime.EventsEmit(a.ctx, "telegraph", refreshTelegraphList())
|
||||
go MonitorStockPrices(a)
|
||||
|
||||
//检查新版本
|
||||
go func() {
|
||||
checkUpdate(a)
|
||||
}()
|
||||
}
|
||||
|
||||
func refreshTelegraphList() *[]string {
|
||||
@@ -375,10 +413,6 @@ func (a *App) SendDingDingMessageByType(message string, stockCode string, msgTyp
|
||||
return data.NewDingDingAPI().SendDingDingMessage(message)
|
||||
}
|
||||
|
||||
func (a *App) NewChat(stock string) string {
|
||||
return data.NewDeepSeekOpenAi().NewChat(stock)
|
||||
}
|
||||
|
||||
func (a *App) NewChatStream(stock, stockCode string) {
|
||||
msgs := data.NewDeepSeekOpenAi().NewChatStream(stock, stockCode)
|
||||
for msg := range msgs {
|
||||
@@ -387,6 +421,25 @@ func (a *App) NewChatStream(stock, stockCode string) {
|
||||
runtime.EventsEmit(a.ctx, "newChatStream", "DONE")
|
||||
}
|
||||
|
||||
func (a *App) SaveAIResponseResult(stockCode, stockName, result string) {
|
||||
data.NewDeepSeekOpenAi().SaveAIResponseResult(stockCode, stockName, result)
|
||||
}
|
||||
func (a *App) GetAIResponseResult(stock string) *models.AIResponseResult {
|
||||
return data.NewDeepSeekOpenAi().GetAIResponseResult(stock)
|
||||
}
|
||||
|
||||
func (a *App) GetVersionInfo() *models.VersionInfo {
|
||||
return &models.VersionInfo{
|
||||
Version: Version,
|
||||
Icon: GetImageBase(icon),
|
||||
Content: VersionCommit,
|
||||
}
|
||||
}
|
||||
|
||||
func GetImageBase(bytes []byte) string {
|
||||
return "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
func GenNotificationMsg(stockInfo *data.StockInfo) string {
|
||||
Price, err := convertor.ToFloat(stockInfo.Price)
|
||||
if err != nil {
|
||||
|
||||
459
app_darwin.go
Normal file
459
app_darwin.go
Normal file
@@ -0,0 +1,459 @@
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/coocood/freecache"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/mathutil"
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
cache *freecache.Cache
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
func NewApp() *App {
|
||||
cacheSize := 512 * 1024
|
||||
cache := freecache.NewCache(cacheSize)
|
||||
return &App{
|
||||
cache: cache,
|
||||
}
|
||||
}
|
||||
|
||||
// startup is called at application startup
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
logger.SugaredLogger.Infof("Version:%s", Version)
|
||||
// Perform your setup here
|
||||
a.ctx = ctx
|
||||
|
||||
// TODO 创建系统托盘
|
||||
|
||||
}
|
||||
|
||||
func checkUpdate(a *App) {
|
||||
releaseVersion := &models.GitHubReleaseVersion{}
|
||||
_, err := resty.New().R().
|
||||
SetResult(releaseVersion).
|
||||
Get("https://api.github.com/repos/ArvinLovegood/go-stock/releases/latest")
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("get github release version error:%s", err.Error())
|
||||
return
|
||||
}
|
||||
logger.SugaredLogger.Infof("releaseVersion:%+v", releaseVersion.TagName)
|
||||
if releaseVersion.TagName != Version {
|
||||
go runtime.EventsEmit(a.ctx, "updateVersion", releaseVersion)
|
||||
}
|
||||
}
|
||||
|
||||
// domReady is called after front-end resources have been loaded
|
||||
func (a *App) domReady(ctx context.Context) {
|
||||
// Add your action here
|
||||
//定时更新数据
|
||||
go func() {
|
||||
config := data.NewSettingsApi(&data.Settings{}).GetConfig()
|
||||
interval := config.RefreshInterval
|
||||
if interval <= 0 {
|
||||
interval = 1
|
||||
}
|
||||
ticker := time.NewTicker(time.Second * time.Duration(interval))
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
if isTradingTime(time.Now()) {
|
||||
MonitorStockPrices(a)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Second * time.Duration(60))
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
telegraph := refreshTelegraphList()
|
||||
if telegraph != nil {
|
||||
go runtime.EventsEmit(a.ctx, "telegraph", telegraph)
|
||||
}
|
||||
}
|
||||
|
||||
}()
|
||||
go runtime.EventsEmit(a.ctx, "telegraph", refreshTelegraphList())
|
||||
go MonitorStockPrices(a)
|
||||
|
||||
//检查新版本
|
||||
go func() {
|
||||
checkUpdate(a)
|
||||
}()
|
||||
}
|
||||
|
||||
func refreshTelegraphList() *[]string {
|
||||
url := "https://www.cls.cn/telegraph"
|
||||
response, err := resty.New().R().
|
||||
SetHeader("Referer", "https://www.cls.cn/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
|
||||
Get(fmt.Sprintf(url))
|
||||
if err != nil {
|
||||
return &[]string{}
|
||||
}
|
||||
//logger.SugaredLogger.Info(string(response.Body()))
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
|
||||
if err != nil {
|
||||
return &[]string{}
|
||||
}
|
||||
var telegraph []string
|
||||
document.Find("div.telegraph-content-box").Each(func(i int, selection *goquery.Selection) {
|
||||
//logger.SugaredLogger.Info(selection.Text())
|
||||
telegraph = append(telegraph, selection.Text())
|
||||
})
|
||||
return &telegraph
|
||||
}
|
||||
|
||||
// isTradingDay 判断是否是交易日
|
||||
func isTradingDay(date time.Time) bool {
|
||||
weekday := date.Weekday()
|
||||
// 判断是否是周末
|
||||
if weekday == time.Saturday || weekday == time.Sunday {
|
||||
return false
|
||||
}
|
||||
// 这里可以添加具体的节假日判断逻辑
|
||||
// 例如:判断是否是春节、国庆节等
|
||||
return true
|
||||
}
|
||||
|
||||
// isTradingTime 判断是否是交易时间
|
||||
func isTradingTime(date time.Time) bool {
|
||||
if !isTradingDay(date) {
|
||||
return false
|
||||
}
|
||||
|
||||
hour, minute, _ := date.Clock()
|
||||
|
||||
// 判断是否在9:15到11:30之间
|
||||
if (hour == 9 && minute >= 15) || (hour == 10) || (hour == 11 && minute <= 30) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 判断是否在13:00到15:00之间
|
||||
if (hour == 13) || (hour == 14) || (hour == 15 && minute <= 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func MonitorStockPrices(a *App) {
|
||||
dest := &[]data.FollowedStock{}
|
||||
db.Dao.Model(&data.FollowedStock{}).Find(dest)
|
||||
total := float64(0)
|
||||
//for _, follow := range *dest {
|
||||
// stockData := getStockInfo(follow)
|
||||
// total += stockData.ProfitAmountToday
|
||||
// price, _ := convertor.ToFloat(stockData.Price)
|
||||
// if stockData.PrePrice != price {
|
||||
// go runtime.EventsEmit(a.ctx, "stock_price", stockData)
|
||||
// }
|
||||
//}
|
||||
|
||||
stockInfos := GetStockInfos(*dest...)
|
||||
for _, stockInfo := range *stockInfos {
|
||||
total += stockInfo.ProfitAmountToday
|
||||
price, _ := convertor.ToFloat(stockInfo.Price)
|
||||
if stockInfo.PrePrice != price {
|
||||
go runtime.EventsEmit(a.ctx, "stock_price", stockInfo)
|
||||
}
|
||||
}
|
||||
if total != 0 {
|
||||
// title := "go-stock " + time.Now().Format(time.DateTime) + fmt.Sprintf(" %.2f¥", total)
|
||||
// systray.SetTooltip(title)
|
||||
}
|
||||
|
||||
go runtime.EventsEmit(a.ctx, "realtime_profit", fmt.Sprintf(" %.2f", total))
|
||||
//runtime.WindowSetTitle(a.ctx, title)
|
||||
|
||||
}
|
||||
func GetStockInfos(follows ...data.FollowedStock) *[]data.StockInfo {
|
||||
stockCodes := make([]string, 0)
|
||||
for _, follow := range follows {
|
||||
stockCodes = append(stockCodes, follow.StockCode)
|
||||
}
|
||||
stockData, err := data.NewStockDataApi().GetStockCodeRealTimeData(stockCodes...)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("get stock code real time data error:%s", err.Error())
|
||||
return nil
|
||||
}
|
||||
stockInfos := make([]data.StockInfo, 0)
|
||||
for _, info := range *stockData {
|
||||
v, ok := slice.FindBy(follows, func(idx int, follow data.FollowedStock) bool {
|
||||
return follow.StockCode == info.Code
|
||||
})
|
||||
if ok {
|
||||
addStockFollowData(v, &info)
|
||||
stockInfos = append(stockInfos, info)
|
||||
}
|
||||
}
|
||||
return &stockInfos
|
||||
}
|
||||
func getStockInfo(follow data.FollowedStock) *data.StockInfo {
|
||||
stockCode := follow.StockCode
|
||||
stockDatas, err := data.NewStockDataApi().GetStockCodeRealTimeData(stockCode)
|
||||
if err != nil || len(*stockDatas) == 0 {
|
||||
return &data.StockInfo{}
|
||||
}
|
||||
stockData := (*stockDatas)[0]
|
||||
addStockFollowData(follow, &stockData)
|
||||
return &stockData
|
||||
}
|
||||
|
||||
func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) {
|
||||
stockData.PrePrice = follow.Price //上次当前价格
|
||||
stockData.Sort = follow.Sort
|
||||
stockData.CostPrice = follow.CostPrice //成本价
|
||||
stockData.CostVolume = follow.Volume //成本量
|
||||
stockData.AlarmChangePercent = follow.AlarmChangePercent
|
||||
stockData.AlarmPrice = follow.AlarmPrice
|
||||
|
||||
//当前价格
|
||||
price, _ := convertor.ToFloat(stockData.Price)
|
||||
//当前价格为0 时 使用卖一价格作为当前价格
|
||||
if price == 0 {
|
||||
price, _ = convertor.ToFloat(stockData.A1P)
|
||||
}
|
||||
//当前价格依然为0 时 使用买一报价作为当前价格
|
||||
if price == 0 {
|
||||
price, _ = convertor.ToFloat(stockData.B1P)
|
||||
}
|
||||
|
||||
//昨日收盘价
|
||||
preClosePrice, _ := convertor.ToFloat(stockData.PreClose)
|
||||
|
||||
//当前价格依然为0 时 使用昨日收盘价为当前价格
|
||||
if price == 0 {
|
||||
price = preClosePrice
|
||||
}
|
||||
|
||||
//今日最高价
|
||||
highPrice, _ := convertor.ToFloat(stockData.High)
|
||||
if highPrice == 0 {
|
||||
highPrice, _ = convertor.ToFloat(stockData.Open)
|
||||
}
|
||||
|
||||
//今日最低价
|
||||
lowPrice, _ := convertor.ToFloat(stockData.Low)
|
||||
if lowPrice == 0 {
|
||||
lowPrice, _ = convertor.ToFloat(stockData.Open)
|
||||
}
|
||||
//开盘价
|
||||
//openPrice, _ := convertor.ToFloat(stockData.Open)
|
||||
|
||||
if price > 0 {
|
||||
stockData.ChangePrice = mathutil.RoundToFloat(price-preClosePrice, 2)
|
||||
stockData.ChangePercent = mathutil.RoundToFloat(mathutil.Div(price-preClosePrice, preClosePrice)*100, 3)
|
||||
}
|
||||
if highPrice > 0 {
|
||||
stockData.HighRate = mathutil.RoundToFloat(mathutil.Div(highPrice-preClosePrice, preClosePrice)*100, 3)
|
||||
}
|
||||
if lowPrice > 0 {
|
||||
stockData.LowRate = mathutil.RoundToFloat(mathutil.Div(lowPrice-preClosePrice, preClosePrice)*100, 3)
|
||||
}
|
||||
if follow.CostPrice > 0 && follow.Volume > 0 {
|
||||
if price > 0 {
|
||||
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(price-follow.CostPrice, follow.CostPrice)*100, 3)
|
||||
stockData.ProfitAmount = mathutil.RoundToFloat((price-follow.CostPrice)*float64(follow.Volume), 2)
|
||||
stockData.ProfitAmountToday = mathutil.RoundToFloat((price-preClosePrice)*float64(follow.Volume), 2)
|
||||
} else {
|
||||
//未开盘时当前价格为昨日收盘价
|
||||
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(preClosePrice-follow.CostPrice, follow.CostPrice)*100, 3)
|
||||
stockData.ProfitAmount = mathutil.RoundToFloat((preClosePrice-follow.CostPrice)*float64(follow.Volume), 2)
|
||||
stockData.ProfitAmountToday = mathutil.RoundToFloat((preClosePrice-preClosePrice)*float64(follow.Volume), 2)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//logger.SugaredLogger.Debugf("stockData:%+v", stockData)
|
||||
if follow.Price != price && price > 0 {
|
||||
go db.Dao.Model(follow).Where("stock_code = ?", follow.StockCode).Updates(map[string]interface{}{
|
||||
"price": price,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// beforeClose is called when the application is about to 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.
|
||||
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
|
||||
|
||||
dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{
|
||||
Type: runtime.QuestionDialog,
|
||||
Title: "go-stock",
|
||||
Message: "确定关闭吗?",
|
||||
Buttons: []string{"确定"},
|
||||
Icon: icon,
|
||||
CancelButton: "取消",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("dialog error:%s", err.Error())
|
||||
return false
|
||||
}
|
||||
logger.SugaredLogger.Debugf("dialog:%s", dialog)
|
||||
if dialog == "No" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// shutdown is called at application termination
|
||||
func (a *App) shutdown(ctx context.Context) {
|
||||
// Perform your teardown here
|
||||
// systray.Quit()
|
||||
}
|
||||
|
||||
// Greet returns a greeting for the given name
|
||||
func (a *App) Greet(stockCode string) *data.StockInfo {
|
||||
//stockInfo, _ := data.NewStockDataApi().GetStockCodeRealTimeData(stockCode)
|
||||
|
||||
follow := &data.FollowedStock{
|
||||
StockCode: stockCode,
|
||||
}
|
||||
db.Dao.Model(follow).Where("stock_code = ?", stockCode).First(follow)
|
||||
stockInfo := getStockInfo(*follow)
|
||||
return stockInfo
|
||||
}
|
||||
|
||||
func (a *App) Follow(stockCode string) string {
|
||||
return data.NewStockDataApi().Follow(stockCode)
|
||||
}
|
||||
|
||||
func (a *App) UnFollow(stockCode string) string {
|
||||
return data.NewStockDataApi().UnFollow(stockCode)
|
||||
}
|
||||
|
||||
func (a *App) GetFollowList() []data.FollowedStock {
|
||||
return data.NewStockDataApi().GetFollowList()
|
||||
}
|
||||
|
||||
func (a *App) GetStockList(key string) []data.StockBasic {
|
||||
return data.NewStockDataApi().GetStockList(key)
|
||||
}
|
||||
|
||||
func (a *App) SetCostPriceAndVolume(stockCode string, price float64, volume int64) string {
|
||||
return data.NewStockDataApi().SetCostPriceAndVolume(price, volume, stockCode)
|
||||
}
|
||||
|
||||
func (a *App) SetAlarmChangePercent(val, alarmPrice float64, stockCode string) string {
|
||||
return data.NewStockDataApi().SetAlarmChangePercent(val, alarmPrice, stockCode)
|
||||
}
|
||||
func (a *App) SetStockSort(sort int64, stockCode string) {
|
||||
data.NewStockDataApi().SetStockSort(sort, stockCode)
|
||||
}
|
||||
func (a *App) SendDingDingMessage(message string, stockCode string) string {
|
||||
ttl, _ := a.cache.TTL([]byte(stockCode))
|
||||
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
|
||||
if ttl > 0 {
|
||||
return ""
|
||||
}
|
||||
err := a.cache.Set([]byte(stockCode), []byte("1"), 60*5)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
|
||||
return ""
|
||||
}
|
||||
return data.NewDingDingAPI().SendDingDingMessage(message)
|
||||
}
|
||||
|
||||
// SendDingDingMessageByType msgType 报警类型: 1 涨跌报警;2 股价报警 3 成本价报警
|
||||
func (a *App) SendDingDingMessageByType(message string, stockCode string, msgType int) string {
|
||||
ttl, _ := a.cache.TTL([]byte(stockCode))
|
||||
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
|
||||
if ttl > 0 {
|
||||
return ""
|
||||
}
|
||||
err := a.cache.Set([]byte(stockCode), []byte("1"), getMsgTypeTTL(msgType))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
|
||||
return ""
|
||||
}
|
||||
stockInfo := &data.StockInfo{}
|
||||
db.Dao.Model(stockInfo).Where("code = ?", stockCode).First(stockInfo)
|
||||
go data.NewAlertWindowsApi("go-stock消息通知", getMsgTypeName(msgType), GenNotificationMsg(stockInfo), "").SendNotification()
|
||||
return data.NewDingDingAPI().SendDingDingMessage(message)
|
||||
}
|
||||
|
||||
func (a *App) NewChat(stock string) string {
|
||||
return data.NewDeepSeekOpenAi().NewChat(stock)
|
||||
}
|
||||
|
||||
func (a *App) NewChatStream(stock, stockCode string) {
|
||||
msgs := data.NewDeepSeekOpenAi().NewChatStream(stock, stockCode)
|
||||
for msg := range msgs {
|
||||
runtime.EventsEmit(a.ctx, "newChatStream", msg)
|
||||
}
|
||||
runtime.EventsEmit(a.ctx, "newChatStream", "DONE")
|
||||
}
|
||||
|
||||
func GenNotificationMsg(stockInfo *data.StockInfo) string {
|
||||
Price, err := convertor.ToFloat(stockInfo.Price)
|
||||
if err != nil {
|
||||
Price = 0
|
||||
}
|
||||
PreClose, err := convertor.ToFloat(stockInfo.PreClose)
|
||||
if err != nil {
|
||||
PreClose = 0
|
||||
}
|
||||
var RF float64
|
||||
if PreClose > 0 {
|
||||
RF = mathutil.RoundToFloat(((Price-PreClose)/PreClose)*100, 2)
|
||||
}
|
||||
|
||||
return "[" + stockInfo.Name + "] " + stockInfo.Price + " " + convertor.ToString(RF) + "% " + stockInfo.Date + " " + stockInfo.Time
|
||||
}
|
||||
|
||||
// msgType : 1 涨跌报警(5分钟);2 股价报警(30分钟) 3 成本价报警(30分钟)
|
||||
func getMsgTypeTTL(msgType int) int {
|
||||
switch msgType {
|
||||
case 1:
|
||||
return 60 * 5
|
||||
case 2:
|
||||
return 60 * 30
|
||||
case 3:
|
||||
return 60 * 30
|
||||
default:
|
||||
return 60 * 5
|
||||
}
|
||||
}
|
||||
|
||||
func getMsgTypeName(msgType int) string {
|
||||
switch msgType {
|
||||
case 1:
|
||||
return "涨跌报警"
|
||||
case 2:
|
||||
return "股价报警"
|
||||
case 3:
|
||||
return "成本价报警"
|
||||
default:
|
||||
return "未知类型"
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) UpdateConfig(settings *data.Settings) string {
|
||||
logger.SugaredLogger.Infof("UpdateConfig:%+v", settings)
|
||||
return data.NewSettingsApi(settings).UpdateConfig()
|
||||
}
|
||||
|
||||
func (a *App) GetConfig() *data.Settings {
|
||||
return data.NewSettingsApi(&data.Settings{}).GetConfig()
|
||||
}
|
||||
52
backend/data/alert_darwin_api.go
Normal file
52
backend/data/alert_darwin_api.go
Normal file
@@ -0,0 +1,52 @@
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package data
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go-stock/backend/logger"
|
||||
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// AlertWindowsApi @Author 2lovecode
|
||||
// @Date 2025/02/06 17:50
|
||||
// @Desc
|
||||
// -----------------------------------------------------------------------------------
|
||||
type AlertWindowsApi struct {
|
||||
AppID string
|
||||
// 窗口标题
|
||||
Title string
|
||||
// 窗口内容
|
||||
Content string
|
||||
// 窗口图标
|
||||
Icon string
|
||||
}
|
||||
|
||||
func NewAlertWindowsApi(AppID string, Title string, Content string, Icon string) *AlertWindowsApi {
|
||||
return &AlertWindowsApi{
|
||||
AppID: AppID,
|
||||
Title: Title,
|
||||
Content: Content,
|
||||
Icon: Icon,
|
||||
}
|
||||
}
|
||||
|
||||
func (a AlertWindowsApi) SendNotification() bool {
|
||||
if getConfig().LocalPushEnable == false {
|
||||
logger.SugaredLogger.Error("本地推送未开启")
|
||||
return false
|
||||
}
|
||||
|
||||
script := fmt.Sprintf(`display notification "%s" with title "%s"`, a.Content, a.Title)
|
||||
|
||||
cmd := exec.Command("osascript", "-e", script)
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
32
backend/data/alert_darwin_api_test.go
Normal file
32
backend/data/alert_darwin_api_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package data
|
||||
|
||||
import (
|
||||
"go-stock/backend/logger"
|
||||
"testing"
|
||||
|
||||
"github.com/go-toast/toast"
|
||||
)
|
||||
|
||||
// @Author 2lovecode
|
||||
// @Date 2025/02/06 17:50
|
||||
// @Desc
|
||||
// -----------------------------------------------------------------------------------
|
||||
|
||||
func TestAlert(t *testing.T) {
|
||||
notification := toast.Notification{
|
||||
AppID: "go-stock",
|
||||
Title: "Hello, World!",
|
||||
Message: "This is a toast notification.",
|
||||
Icon: "../../build/appicon.png",
|
||||
Duration: "short",
|
||||
Audio: toast.Default,
|
||||
}
|
||||
err := notification.Push()
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,9 @@ import (
|
||||
"github.com/chromedp/chromedp"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -26,6 +28,7 @@ type OpenAi struct {
|
||||
MaxTokens int `json:"max_tokens"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
Prompt string `json:"prompt"`
|
||||
TimeOut int `json:"time_out"`
|
||||
}
|
||||
|
||||
func NewDeepSeekOpenAi() *OpenAi {
|
||||
@@ -37,6 +40,7 @@ func NewDeepSeekOpenAi() *OpenAi {
|
||||
MaxTokens: config.OpenAiMaxTokens,
|
||||
Temperature: config.OpenAiTemperature,
|
||||
Prompt: config.Prompt,
|
||||
TimeOut: config.OpenAiApiTimeOut,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,60 +74,6 @@ type AiResponse struct {
|
||||
SystemFingerprint string `json:"system_fingerprint"`
|
||||
}
|
||||
|
||||
func (o OpenAi) NewChat(stock string) string {
|
||||
client := resty.New()
|
||||
client.SetBaseURL(o.BaseUrl)
|
||||
client.SetHeader("Authorization", "Bearer "+o.ApiKey)
|
||||
client.SetHeader("Content-Type", "application/json")
|
||||
|
||||
res := &AiResponse{}
|
||||
_, err := client.R().
|
||||
SetResult(res).
|
||||
SetBody(map[string]interface{}{
|
||||
"model": o.Model,
|
||||
"max_tokens": o.MaxTokens,
|
||||
"temperature": o.Temperature,
|
||||
"messages": []map[string]interface{}{
|
||||
{
|
||||
"role": "system",
|
||||
"content": "作为一位专业的A股市场分析师和投资顾问,请你根据以下信息提供详细的技术分析和投资策略建议:" +
|
||||
"1. 市场背景:\n" +
|
||||
"- 当前A股市场整体走势(如:牛市、熊市、震荡市)\n " +
|
||||
"- 近期影响市场的主要宏观经济因素\n " +
|
||||
"- 市场情绪指标(如:融资融券余额、成交量变化) " +
|
||||
"2. 技术指标分析: " +
|
||||
"- 当前股价水平" +
|
||||
"- 所在boll区间" +
|
||||
"- 上证指数的MA(移动平均线)、MACD、KDJ指标分析\n " +
|
||||
"- 行业板块轮动情况\n " +
|
||||
"- 近期市场热点和龙头股票的技术形态 " +
|
||||
"3. 风险评估:\n " +
|
||||
"- 当前市场主要风险因素\n " +
|
||||
"- 如何设置止损和止盈位\n " +
|
||||
"- 资金管理建议(如:仓位控制) " +
|
||||
"4. 投资策略:\n " +
|
||||
"- 短期(1-2周)、中期(1-3月)和长期(3-6月)的市场预期\n " +
|
||||
"- 不同风险偏好投资者的策略建议\n " +
|
||||
"- 值得关注的行业板块和个股推荐(请给出2-3个具体例子,包括股票代码和名称) " +
|
||||
"5. 技术面和基本面结合:\n " +
|
||||
"- 如何将技术分析与公司基本面分析相结合\n " +
|
||||
"- 识别高质量股票的关键指标 " +
|
||||
"请提供详细的分析和具体的操作建议,包括入场时机、持仓周期和退出策略。同时,请强调风险控制的重要性,并提醒投资者需要根据自身情况做出决策。 " +
|
||||
"你的分析和建议应当客观、全面,并基于当前可获得的市场数据。如果某些信息无法确定,请明确指出并解释原因。",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "点评一下" + stock,
|
||||
},
|
||||
},
|
||||
}).
|
||||
Post("/chat/completions")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
//logger.SugaredLogger.Infof("%v", res.Choices[0].Message.Content)
|
||||
return res.Choices[0].Message.Content
|
||||
}
|
||||
func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
|
||||
ch := make(chan string, 512)
|
||||
go func() {
|
||||
@@ -136,10 +86,10 @@ func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
|
||||
"content": o.Prompt,
|
||||
},
|
||||
}
|
||||
logger.SugaredLogger.Infof("Prompt:%s", o.Prompt)
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(5)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
messages := SearchStockPriceInfo(stockCode)
|
||||
@@ -206,7 +156,10 @@ func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
|
||||
client.SetHeader("Authorization", "Bearer "+o.ApiKey)
|
||||
client.SetHeader("Content-Type", "application/json")
|
||||
client.SetRetryCount(3)
|
||||
client.SetTimeout(1 * time.Minute)
|
||||
if o.TimeOut <= 0 {
|
||||
o.TimeOut = 300
|
||||
}
|
||||
client.SetTimeout(time.Duration(o.TimeOut) * time.Second)
|
||||
resp, err := client.R().
|
||||
SetDoNotParseResponse(true).
|
||||
SetBody(map[string]interface{}{
|
||||
@@ -444,3 +397,17 @@ func GetTelegraphList() *[]string {
|
||||
})
|
||||
return &telegraph
|
||||
}
|
||||
|
||||
func (o OpenAi) SaveAIResponseResult(stockCode, stockName, result string) {
|
||||
db.Dao.Create(&models.AIResponseResult{
|
||||
StockCode: stockCode,
|
||||
StockName: stockName,
|
||||
Content: result,
|
||||
})
|
||||
}
|
||||
|
||||
func (o OpenAi) GetAIResponseResult(stock string) *models.AIResponseResult {
|
||||
var result models.AIResponseResult
|
||||
db.Dao.Where("stock_code = ?", stock).Order("id desc").First(&result)
|
||||
return &result
|
||||
}
|
||||
|
||||
@@ -21,7 +21,9 @@ type Settings struct {
|
||||
OpenAiModelName string `json:"openAiModelName"`
|
||||
OpenAiMaxTokens int `json:"openAiMaxTokens"`
|
||||
OpenAiTemperature float64 `json:"openAiTemperature"`
|
||||
OpenAiApiTimeOut int `json:"openAiApiTimeOut"`
|
||||
Prompt string `json:"prompt"`
|
||||
CheckUpdate bool `json:"checkUpdate"`
|
||||
}
|
||||
|
||||
func (receiver Settings) TableName() string {
|
||||
@@ -56,6 +58,8 @@ func (s SettingsApi) UpdateConfig() string {
|
||||
"open_ai_temperature": s.Config.OpenAiTemperature,
|
||||
"tushare_token": s.Config.TushareToken,
|
||||
"prompt": s.Config.Prompt,
|
||||
"check_update": s.Config.CheckUpdate,
|
||||
"open_ai_api_time_out": s.Config.OpenAiApiTimeOut,
|
||||
})
|
||||
} else {
|
||||
logger.SugaredLogger.Infof("未找到配置,创建默认配置:%+v", s.Config)
|
||||
@@ -73,6 +77,8 @@ func (s SettingsApi) UpdateConfig() string {
|
||||
OpenAiTemperature: s.Config.OpenAiTemperature,
|
||||
TushareToken: s.Config.TushareToken,
|
||||
Prompt: s.Config.Prompt,
|
||||
CheckUpdate: s.Config.CheckUpdate,
|
||||
OpenAiApiTimeOut: s.Config.OpenAiApiTimeOut,
|
||||
})
|
||||
}
|
||||
return "保存成功!"
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/chromedp/chromedp"
|
||||
@@ -291,6 +292,9 @@ func (receiver StockDataApi) GetStockCodeRealTimeData(StockCodes ...string) (*[]
|
||||
stockInfos := make([]StockInfo, 0)
|
||||
str := GB18030ToUTF8(resp.Body())
|
||||
dataStr := strutil.SplitEx(str, "\n", true)
|
||||
if len(dataStr) == 0 {
|
||||
return &[]StockInfo{}, errors.New("获取股票信息失败,请检查股票代码是否正确")
|
||||
}
|
||||
for _, data := range dataStr {
|
||||
//logger.SugaredLogger.Info(data)
|
||||
stockData, err := ParseFullSingleStockData(data)
|
||||
@@ -318,8 +322,8 @@ func (receiver StockDataApi) GetStockCodeRealTimeData(StockCodes ...string) (*[]
|
||||
func (receiver StockDataApi) Follow(stockCode string) string {
|
||||
logger.SugaredLogger.Infof("Follow %s", stockCode)
|
||||
stockInfos, err := receiver.GetStockCodeRealTimeData(stockCode)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
if err != nil || len(*stockInfos) == 0 {
|
||||
logger.SugaredLogger.Error(err)
|
||||
return "关注失败"
|
||||
}
|
||||
stockInfo := (*stockInfos)[0]
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/chromedp/chromedp"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/go-resty/resty/v2"
|
||||
@@ -31,117 +27,6 @@ func TestGetFinancialReports(t *testing.T) {
|
||||
GetFinancialReports("sz000802")
|
||||
}
|
||||
|
||||
func TestXUEQIU(t *testing.T) {
|
||||
stock := "北京文化"
|
||||
stockCode := "SZ000802"
|
||||
// 创建一个 chromedp 上下文
|
||||
ctx, cancel := chromedp.NewContext(
|
||||
context.Background(),
|
||||
chromedp.WithLogf(logger.SugaredLogger.Infof),
|
||||
chromedp.WithErrorf(logger.SugaredLogger.Errorf),
|
||||
)
|
||||
defer cancel()
|
||||
var htmlContent string
|
||||
url := fmt.Sprintf("https://xueqiu.com/S/%s", stockCode)
|
||||
err := chromedp.Run(ctx,
|
||||
chromedp.Navigate(url),
|
||||
// 等待页面加载完成,可以根据需要调整等待时间
|
||||
//chromedp.Sleep(3*time.Second),
|
||||
chromedp.WaitVisible("table.quote-info", chromedp.ByQuery),
|
||||
chromedp.OuterHTML("html", &htmlContent, chromedp.ByQuery),
|
||||
)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
}
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
table := ""
|
||||
document.Find("table.quote-info tbody td").Each(func(i int, selection *goquery.Selection) {
|
||||
table += selection.Text() + ";"
|
||||
})
|
||||
logger.SugaredLogger.Infof("table: %s", table)
|
||||
client := resty.New()
|
||||
client.SetBaseURL("https://api.siliconflow.cn/v1")
|
||||
client.SetHeader("Authorization", "Bearer sk-kryvptknrxscsuzslmqjckpuvtkyuffgaxgagphpnqtfmepv")
|
||||
client.SetHeader("Content-Type", "application/json")
|
||||
client.SetRetryCount(3)
|
||||
client.SetTimeout(1 * time.Minute)
|
||||
|
||||
msg := []map[string]interface{}{
|
||||
{
|
||||
"role": "system",
|
||||
//"content": "作为一位专业的A股市场分析师和投资顾问,请你根据以下信息提供详细的技术分析和投资策略建议:",
|
||||
"content": "【角色设定】\n你现在是拥有20年实战经验的顶级股票投资大师,精通价值投资、趋势交易、量化分析等多种策略。\n擅长结合宏观经济、行业周期和企业基本面进行多维分析,尤其对A股、港股、美股市场有深刻理解。\n始终秉持\"风险控制第一\"的原则,善于用通俗易懂的方式传授投资智慧。\n\n【核心能力】\n基本面分析专家\n深度解读财报数据(PE/PB/ROE等指标)\n识别企业核心竞争力与护城河\n评估行业前景与政策影响\n技术面分析大师\n精准识别K线形态与量价关系\n运用MACD/RSI/布林线等指标判断买卖点\n绘制关键支撑/阻力位\n风险管理专家\n根据风险偏好制定仓位策略\n设置动态止盈止损方案\n设计投资组合对冲方案\n市场心理学导师\n识别主力资金动向\n预判市场情绪周期\n规避常见认知偏差\n【服务范围】\n个股诊断分析(提供代码/名称)\n行业趋势解读(科技/消费/医疗等)\n投资策略定制(长线价值/波段操作/打新等)\n组合优化建议(股债配置/行业分散)\n投资心理辅导(克服贪婪恐惧)\n【交互风格】\n采用\"先结论后分析\"的表达方式\n重要数据用★标注,风险提示用❗标记\n每次分析至少提供3个可执行建议"},
|
||||
}
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": table,
|
||||
})
|
||||
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": stock + "分析和总结",
|
||||
})
|
||||
|
||||
resp, err := client.R().
|
||||
SetDoNotParseResponse(true).
|
||||
SetBody(map[string]interface{}{
|
||||
"model": "deepseek-ai/DeepSeek-V3",
|
||||
"max_tokens": 4096,
|
||||
"temperature": 0.1,
|
||||
"stream": true,
|
||||
"messages": msg,
|
||||
}).
|
||||
Post("/chat/completions")
|
||||
|
||||
defer resp.RawBody().Close()
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Infof("Stream error : %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(resp.RawBody())
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
logger.SugaredLogger.Infof("Received data: %s", line)
|
||||
if strings.HasPrefix(line, "data: ") {
|
||||
data := strings.TrimPrefix(line, "data: ")
|
||||
if data == "[DONE]" {
|
||||
return
|
||||
}
|
||||
|
||||
var streamResponse struct {
|
||||
Choices []struct {
|
||||
Delta struct {
|
||||
Content string `json:"content"`
|
||||
ReasoningContent string `json:"reasoning_content"`
|
||||
} `json:"delta"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(data), &streamResponse); err == nil {
|
||||
for _, choice := range streamResponse.Choices {
|
||||
if content := choice.Delta.Content; content != "" {
|
||||
logger.SugaredLogger.Infof("Content data: %s", content)
|
||||
}
|
||||
if reasoningContent := choice.Delta.ReasoningContent; reasoningContent != "" {
|
||||
logger.SugaredLogger.Infof("ReasoningContent data: %s", reasoningContent)
|
||||
}
|
||||
if choice.FinishReason == "stop" {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.SugaredLogger.Infof("Stream data error : %s", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTelegraphSearch(t *testing.T) {
|
||||
//url := "https://www.cls.cn/searchPage?keyword=%E9%97%BB%E6%B3%B0%E7%A7%91%E6%8A%80&type=telegram"
|
||||
messages := SearchStockInfo("闻泰科技", "telegram")
|
||||
|
||||
159
backend/models/models.go
Normal file
159
backend/models/models.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/plugin/soft_delete"
|
||||
"time"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/2/6 15:25
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
type GitHubReleaseVersion struct {
|
||||
Url string `json:"url"`
|
||||
AssetsUrl string `json:"assets_url"`
|
||||
UploadUrl string `json:"upload_url"`
|
||||
HtmlUrl string `json:"html_url"`
|
||||
Id int `json:"id"`
|
||||
Author struct {
|
||||
Login string `json:"login"`
|
||||
Id int `json:"id"`
|
||||
NodeId string `json:"node_id"`
|
||||
AvatarUrl string `json:"avatar_url"`
|
||||
GravatarId string `json:"gravatar_id"`
|
||||
Url string `json:"url"`
|
||||
HtmlUrl string `json:"html_url"`
|
||||
FollowersUrl string `json:"followers_url"`
|
||||
FollowingUrl string `json:"following_url"`
|
||||
GistsUrl string `json:"gists_url"`
|
||||
StarredUrl string `json:"starred_url"`
|
||||
SubscriptionsUrl string `json:"subscriptions_url"`
|
||||
OrganizationsUrl string `json:"organizations_url"`
|
||||
ReposUrl string `json:"repos_url"`
|
||||
EventsUrl string `json:"events_url"`
|
||||
ReceivedEventsUrl string `json:"received_events_url"`
|
||||
Type string `json:"type"`
|
||||
UserViewType string `json:"user_view_type"`
|
||||
SiteAdmin bool `json:"site_admin"`
|
||||
} `json:"author"`
|
||||
NodeId string `json:"node_id"`
|
||||
TagName string `json:"tag_name"`
|
||||
TargetCommitish string `json:"target_commitish"`
|
||||
Name string `json:"name"`
|
||||
Draft bool `json:"draft"`
|
||||
Prerelease bool `json:"prerelease"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
PublishedAt time.Time `json:"published_at"`
|
||||
Assets []struct {
|
||||
Url string `json:"url"`
|
||||
Id int `json:"id"`
|
||||
NodeId string `json:"node_id"`
|
||||
Name string `json:"name"`
|
||||
Label string `json:"label"`
|
||||
Uploader struct {
|
||||
Login string `json:"login"`
|
||||
Id int `json:"id"`
|
||||
NodeId string `json:"node_id"`
|
||||
AvatarUrl string `json:"avatar_url"`
|
||||
GravatarId string `json:"gravatar_id"`
|
||||
Url string `json:"url"`
|
||||
HtmlUrl string `json:"html_url"`
|
||||
FollowersUrl string `json:"followers_url"`
|
||||
FollowingUrl string `json:"following_url"`
|
||||
GistsUrl string `json:"gists_url"`
|
||||
StarredUrl string `json:"starred_url"`
|
||||
SubscriptionsUrl string `json:"subscriptions_url"`
|
||||
OrganizationsUrl string `json:"organizations_url"`
|
||||
ReposUrl string `json:"repos_url"`
|
||||
EventsUrl string `json:"events_url"`
|
||||
ReceivedEventsUrl string `json:"received_events_url"`
|
||||
Type string `json:"type"`
|
||||
UserViewType string `json:"user_view_type"`
|
||||
SiteAdmin bool `json:"site_admin"`
|
||||
} `json:"uploader"`
|
||||
ContentType string `json:"content_type"`
|
||||
State string `json:"state"`
|
||||
Size int `json:"size"`
|
||||
DownloadCount int `json:"download_count"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
BrowserDownloadUrl string `json:"browser_download_url"`
|
||||
} `json:"assets"`
|
||||
TarballUrl string `json:"tarball_url"`
|
||||
ZipballUrl string `json:"zipball_url"`
|
||||
Body string `json:"body"`
|
||||
Tag Tag `json:"tag"`
|
||||
Commit Commit `json:"commit"`
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
Ref string `json:"ref"`
|
||||
NodeId string `json:"node_id"`
|
||||
Url string `json:"url"`
|
||||
Object struct {
|
||||
Sha string `json:"sha"`
|
||||
Type string `json:"type"`
|
||||
Url string `json:"url"`
|
||||
} `json:"object"`
|
||||
}
|
||||
|
||||
type Commit struct {
|
||||
Sha string `json:"sha"`
|
||||
NodeId string `json:"node_id"`
|
||||
Url string `json:"url"`
|
||||
HtmlUrl string `json:"html_url"`
|
||||
Author struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Date time.Time `json:"date"`
|
||||
} `json:"author"`
|
||||
Committer struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Date time.Time `json:"date"`
|
||||
} `json:"committer"`
|
||||
Tree struct {
|
||||
Sha string `json:"sha"`
|
||||
Url string `json:"url"`
|
||||
} `json:"tree"`
|
||||
Message string `json:"message"`
|
||||
Parents []struct {
|
||||
Sha string `json:"sha"`
|
||||
Url string `json:"url"`
|
||||
HtmlUrl string `json:"html_url"`
|
||||
} `json:"parents"`
|
||||
Verification struct {
|
||||
Verified bool `json:"verified"`
|
||||
Reason string `json:"reason"`
|
||||
Signature interface{} `json:"signature"`
|
||||
Payload interface{} `json:"payload"`
|
||||
VerifiedAt interface{} `json:"verified_at"`
|
||||
} `json:"verification"`
|
||||
}
|
||||
|
||||
type AIResponseResult struct {
|
||||
gorm.Model
|
||||
StockCode string `json:"stockCode"`
|
||||
StockName string `json:"stockName"`
|
||||
Content string `json:"content"`
|
||||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
|
||||
}
|
||||
|
||||
func (receiver AIResponseResult) TableName() string {
|
||||
return "ai_response_result"
|
||||
}
|
||||
|
||||
type VersionInfo struct {
|
||||
gorm.Model
|
||||
Version string `json:"version"`
|
||||
Content string `json:"content"`
|
||||
Icon string `json:"icon"`
|
||||
BuildTimeStamp int64 `json:"buildTimeStamp"`
|
||||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
|
||||
}
|
||||
|
||||
func (receiver VersionInfo) TableName() string {
|
||||
return "version_info"
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 164 KiB |
BIN
build/screenshot/img_11.png
Normal file
BIN
build/screenshot/img_11.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 122 KiB |
@@ -15,7 +15,7 @@ import {
|
||||
SettingsOutline,
|
||||
ReorderTwoOutline,
|
||||
ExpandOutline,
|
||||
RefreshOutline, PowerOutline, BarChartOutline, MoveOutline, WalletOutline, StarOutline,
|
||||
RefreshOutline, PowerOutline, LogoGithub, MoveOutline, WalletOutline, StarOutline,
|
||||
} from '@vicons/ionicons5'
|
||||
|
||||
const content = ref('数据来源于网络,仅供参考;投资有风险,入市需谨慎')
|
||||
@@ -67,6 +67,23 @@ const menuOptions = ref([
|
||||
key: 'settings',
|
||||
icon: renderIcon(SettingsOutline),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'about',
|
||||
params: {
|
||||
id: 'zh-CN'
|
||||
}
|
||||
}
|
||||
},
|
||||
{ default: () => '关于' }
|
||||
),
|
||||
key: 'about',
|
||||
icon: renderIcon(LogoGithub),
|
||||
},
|
||||
{
|
||||
label: ()=> h("a", {
|
||||
href: '#',
|
||||
|
||||
106
frontend/src/components/about.vue
Normal file
106
frontend/src/components/about.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<script setup>
|
||||
import { MdPreview } from 'md-editor-v3';
|
||||
// preview.css相比style.css少了编辑器那部分样式
|
||||
import 'md-editor-v3/lib/preview.css';
|
||||
import {onMounted, ref} from 'vue';
|
||||
import {GetVersionInfo} from "../../wailsjs/go/main/App";
|
||||
const updateLog = ref('');
|
||||
const versionInfo = ref('');
|
||||
const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/master/build/appicon.png');
|
||||
onMounted(() => {
|
||||
document.title = '关于软件';
|
||||
GetVersionInfo().then((res) => {
|
||||
updateLog.value = res.content;
|
||||
versionInfo.value = res.version;
|
||||
icon.value = res.icon;
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-config-provider>
|
||||
<n-layout>
|
||||
<n-space vertical size="large">
|
||||
<!-- 软件描述 -->
|
||||
<n-card size="large">
|
||||
<n-space vertical >
|
||||
<h1>关于软件</h1>
|
||||
<n-image width="100" :src="icon" />
|
||||
<h1>go-stock <n-tag size="small" round>{{versionInfo}}</n-tag></h1>
|
||||
<div style="justify-self: center;text-align: left" >
|
||||
<p>自选股行情实时监控,基于Wails和NaiveUI构建的AI赋能股票分析工具</p>
|
||||
<p>
|
||||
</p>
|
||||
<p>
|
||||
欢迎点赞GitHub:<a href="https://github.com/ArvinLovegood/go-stock" target="_blank">go-stock</a><n-divider vertical />
|
||||
<a href="https://github.com/ArvinLovegood/go-stock" target="_blank">GitHub</a><n-divider vertical />
|
||||
<a href="https://github.com/ArvinLovegood/go-stock/issues" target="_blank">Issues</a><n-divider vertical />
|
||||
<a href="https://github.com/ArvinLovegood/go-stock/releases" target="_blank">Releases</a><n-divider vertical />
|
||||
</p>
|
||||
<p v-if="updateLog">更新说明:{{updateLog}}</p>
|
||||
</div>
|
||||
</n-space>
|
||||
</n-card>
|
||||
<!-- 关于作者 -->
|
||||
<n-card size="large">
|
||||
<n-space vertical>
|
||||
<h1>关于作者</h1>
|
||||
<n-avatar width="100" src="https://avatars.githubusercontent.com/u/7401917?v=4" />
|
||||
<h2><a href="https://github.com/ArvinLovegood" target="_blank">@ArvinLovegood</a></h2>
|
||||
<p>一个热爱编程的小白,欢迎关注我的Github</p>
|
||||
<p>
|
||||
邮箱:<a href="mailto:sparkmemory@163.com">sparkmemory@163.com</a><n-divider vertical />
|
||||
QQ: 506808970<n-divider vertical />
|
||||
微信:ArvinLovegood</p><n-divider vertical />
|
||||
</n-space>
|
||||
<n-divider title-placement="center">鸣谢</n-divider>
|
||||
<div style="justify-self: center;text-align: left" >
|
||||
<p>
|
||||
感谢以下捐赠者:
|
||||
<n-gradient-text size="small" type="warning">*晨</n-gradient-text><n-divider vertical />
|
||||
</p>
|
||||
<p>
|
||||
感谢以下开发者:
|
||||
<a href="https://github.com/gnim2600" target="_blank">@gnim2600</a><n-divider vertical />
|
||||
<a href="https://github.com/XXXiaohuayanGGG" target="_blank">@XXXiaohuayanGGG</a><n-divider vertical />
|
||||
<a href="https://github.com/2lovecode" target="_blank">@2lovecode</a><n-divider vertical />
|
||||
<a href="https://github.com/JerryLookupU" target="_blank">@JerryLookupU</a><n-divider vertical />
|
||||
</p>
|
||||
<p>
|
||||
感谢以下开源项目:
|
||||
<a href="https://github.com/wailsapp/wails" target="_blank">Wails</a><n-divider vertical />
|
||||
<a href="https://github.com/vuejs" target="_blank">Vue</a><n-divider vertical />
|
||||
<a href="https://github.com/tusen-ai/naive-ui" target="_blank">NaiveUI</a><n-divider vertical />
|
||||
</p>
|
||||
</div>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</n-layout>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 可以在这里添加一些样式 */
|
||||
h1, h2 {
|
||||
margin: 0;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #18a058;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
@@ -27,6 +27,7 @@ const formValue = ref({
|
||||
temperature: 0.1,
|
||||
maxTokens: 1024,
|
||||
prompt:"",
|
||||
timeout: 5
|
||||
},
|
||||
})
|
||||
|
||||
@@ -51,6 +52,7 @@ onMounted(()=>{
|
||||
temperature:res.openAiTemperature,
|
||||
maxTokens:res.openAiMaxTokens,
|
||||
prompt:res.prompt,
|
||||
timeout:res.openAiApiTimeOut
|
||||
}
|
||||
console.log(res)
|
||||
})
|
||||
@@ -73,7 +75,8 @@ function saveConfig(){
|
||||
openAiMaxTokens:formValue.value.openAI.maxTokens,
|
||||
openAiTemperature:formValue.value.openAI.temperature,
|
||||
tushareToken:formValue.value.tushareToken,
|
||||
prompt:formValue.value.openAI.prompt
|
||||
prompt:formValue.value.openAI.prompt,
|
||||
openAiApiTimeOut:formValue.value.openAI.timeout
|
||||
})
|
||||
|
||||
//console.log("Settings",config)
|
||||
@@ -150,22 +153,25 @@ function sendTestNotice(){
|
||||
<n-form-item-gi :span="6" label="是否启用AI诊股:" path="openAI.enable" >
|
||||
<n-switch v-model:value="formValue.openAI.enable" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="22" v-if="formValue.openAI.enable" label="openAI 接口地址:" path="openAI.baseUrl">
|
||||
<n-form-item-gi :span="11" v-if="formValue.openAI.enable" label="openAI 接口地址:" path="openAI.baseUrl">
|
||||
<n-input type="text" placeholder="AI接口地址" v-model:value="formValue.openAI.baseUrl" clearable />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="10" v-if="formValue.openAI.enable" label="openAI apiKey:" path="openAI.apiKey">
|
||||
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" label="请求超时时间(秒):" path="openAI.timeout">
|
||||
<n-input-number min="1" step="1" placeholder="请求超时时间(秒)" v-model:value="formValue.openAI.timeout" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="openAI 令牌(apiKey):" path="openAI.apiKey">
|
||||
<n-input type="text" placeholder="apiKey" v-model:value="formValue.openAI.apiKey" clearable />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="AI模型:" path="openAI.model">
|
||||
<n-input type="text" placeholder="AI模型" v-model:value="formValue.openAI.model" clearable />
|
||||
<n-form-item-gi :span="10" v-if="formValue.openAI.enable" label="AI模型名称:" path="openAI.model">
|
||||
<n-input type="text" placeholder="AI模型名称" v-model:value="formValue.openAI.model" clearable />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="temperature:" path="openAI.temperature" >
|
||||
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="openAI temperature:" path="openAI.temperature" >
|
||||
<n-input-number placeholder="temperature" v-model:value="formValue.openAI.temperature"/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="10" v-if="formValue.openAI.enable" label="maxTokens:" path="openAI.maxTokens">
|
||||
<n-form-item-gi :span="10" v-if="formValue.openAI.enable" label="openAI maxTokens:" path="openAI.maxTokens">
|
||||
<n-input-number placeholder="maxTokens" v-model:value="formValue.openAI.maxTokens"/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="22" v-if="formValue.openAI.enable" label="自定义系统Prompt:" path="openAI.prompt">
|
||||
<n-form-item-gi :span="22" v-if="formValue.openAI.enable" label="模型系统 Prompt:" path="openAI.prompt">
|
||||
<n-input v-model:value="formValue.openAI.prompt"
|
||||
type="textarea"
|
||||
:show-count="true"
|
||||
|
||||
@@ -1,16 +1,27 @@
|
||||
<script setup>
|
||||
import {computed, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref} from 'vue'
|
||||
import {computed, h, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref} from 'vue'
|
||||
import {
|
||||
Follow, GetConfig,
|
||||
GetFollowList,
|
||||
GetStockList,
|
||||
Greet, NewChat, NewChatStream,
|
||||
Greet, SaveAIResponseResult, NewChatStream,
|
||||
SendDingDingMessage, SendDingDingMessageByType,
|
||||
SetAlarmChangePercent,
|
||||
SetCostPriceAndVolume, SetStockSort,
|
||||
UnFollow
|
||||
UnFollow, GetAIResponseResult
|
||||
} from '../../wailsjs/go/main/App'
|
||||
import {NButton, NFlex, NForm, NFormItem, NInputNumber, NText, useMessage, useModal,useNotification} from 'naive-ui'
|
||||
import {
|
||||
NAvatar,
|
||||
NButton,
|
||||
NFlex,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NInputNumber,
|
||||
NText,
|
||||
useMessage,
|
||||
useModal,
|
||||
useNotification
|
||||
} from 'naive-ui'
|
||||
import {EventsOn, WindowFullscreen, WindowReload, WindowUnfullscreen} from '../../wailsjs/runtime'
|
||||
import {Add, Search,StarOutline} from '@vicons/ionicons5'
|
||||
import { MdPreview } from 'md-editor-v3';
|
||||
@@ -141,6 +152,7 @@ EventsOn("newChatStream",async (msg) => {
|
||||
//console.log("newChatStream:->",data.airesult)
|
||||
data.loading = false
|
||||
if (msg === "DONE") {
|
||||
SaveAIResponseResult(data.code, data.name, data.airesult)
|
||||
message.info("AI分析完成!")
|
||||
message.destroyAll()
|
||||
} else {
|
||||
@@ -148,6 +160,54 @@ EventsOn("newChatStream",async (msg) => {
|
||||
}
|
||||
})
|
||||
|
||||
EventsOn("updateVersion",async (msg) => {
|
||||
const githubTimeStr = msg.published_at;
|
||||
// 创建一个 Date 对象
|
||||
const utcDate = new Date(githubTimeStr);
|
||||
// 获取本地时间
|
||||
const date = new Date(utcDate.getTime());
|
||||
const year = date.getFullYear();
|
||||
// getMonth 返回值是 0 - 11,所以要加 1
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
|
||||
const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
|
||||
console.log("GitHub UTC 时间:", utcDate);
|
||||
console.log("转换后的本地时间:", formattedDate);
|
||||
notify.info({
|
||||
avatar: () =>
|
||||
h(NAvatar, {
|
||||
size: 'small',
|
||||
round: false,
|
||||
src: 'https://github.com/ArvinLovegood/go-stock/raw/master/build/appicon.png'
|
||||
}),
|
||||
title: '发现新版本: ' + msg.tag_name,
|
||||
content: () => {
|
||||
//return h(MdPreview, {theme:'dark',modelValue:msg.commit?.message}, null)
|
||||
return h('div', {
|
||||
style: {
|
||||
'text-align': 'left',
|
||||
'font-size': '14px',
|
||||
}
|
||||
}, { default: () => msg.commit?.message })
|
||||
},
|
||||
duration: 0,
|
||||
meta: "发布时间:"+formattedDate,
|
||||
action: () => {
|
||||
return h(NButton, {
|
||||
type: 'primary',
|
||||
size: 'small',
|
||||
onClick: () => {
|
||||
window.open(msg.html_url)
|
||||
}
|
||||
}, { default: () => '查看' })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
//判断是否是A股交易时间
|
||||
@@ -171,12 +231,20 @@ function isTradingTime() {
|
||||
}
|
||||
|
||||
function AddStock(){
|
||||
if (!data?.code) {
|
||||
message.error("请输入有效股票代码");
|
||||
return;
|
||||
}
|
||||
if (!stocks.value.includes(data.code)) {
|
||||
stocks.value.push(data.code)
|
||||
Follow(data.code).then(result => {
|
||||
message.success(result)
|
||||
if(result==="关注成功"){
|
||||
stocks.value.push(data.code)
|
||||
message.success(result)
|
||||
monitor();
|
||||
}else{
|
||||
message.error(result)
|
||||
}
|
||||
})
|
||||
monitor()
|
||||
}else{
|
||||
message.error("已经关注了")
|
||||
}
|
||||
@@ -389,9 +457,9 @@ function SendMessage(result,type){
|
||||
// SendDingDingMessage(msg,result["股票代码"])
|
||||
SendDingDingMessageByType(msg,result["股票代码"],type)
|
||||
}
|
||||
|
||||
function aiCheckStock(stock,stockCode){
|
||||
function aiReCheckStock(stock,stockCode) {
|
||||
data.airesult=""
|
||||
data.time=""
|
||||
data.name=stock
|
||||
data.code=stockCode
|
||||
data.loading=true
|
||||
@@ -402,6 +470,38 @@ function aiCheckStock(stock,stockCode){
|
||||
NewChatStream(stock,stockCode)
|
||||
}
|
||||
|
||||
function aiCheckStock(stock,stockCode){
|
||||
GetAIResponseResult(stockCode).then(result => {
|
||||
if(result.content){
|
||||
data.name=stock
|
||||
data.code=stockCode
|
||||
data.loading=false
|
||||
modalShow4.value=true
|
||||
data.airesult=result.content
|
||||
const date = new Date(result.CreatedAt);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
data.time=formattedDate
|
||||
}else{
|
||||
data.airesult=""
|
||||
data.time=""
|
||||
data.name=stock
|
||||
data.code=stockCode
|
||||
data.loading=true
|
||||
modalShow4.value=true
|
||||
message.loading("ai检测中...",{
|
||||
duration: 0,
|
||||
})
|
||||
NewChatStream(stock,stockCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getTypeName(type){
|
||||
switch (type)
|
||||
{
|
||||
@@ -426,8 +526,8 @@ function getHeight() {
|
||||
|
||||
<template>
|
||||
<n-grid :x-gap="8" :cols="3" :y-gap="8" >
|
||||
<n-gi v-for="result in sortedResults" >
|
||||
<n-card :data-code="result['股票代码']" :bordered="false" :title="result['股票名称']" :closable="false" @close="removeMonitor(result['股票代码'],result['股票名称'],result.key)">
|
||||
<n-gi v-for="result in sortedResults" style="margin-left: 2px" onmouseover="this.style.border='1px solid #3498db' " onmouseout="this.style.border='0px'">
|
||||
<n-card :data-code="result['股票代码']" :bordered="false" :title="result['股票名称']" :closable="false" @close="removeMonitor(result['股票代码'],result['股票名称'],result.key)">
|
||||
<n-grid :cols="1" :y-gap="6">
|
||||
<n-gi>
|
||||
<n-text :type="result.type" >
|
||||
@@ -459,7 +559,9 @@ function getHeight() {
|
||||
<n-button size="tiny" secondary type="primary" @click="removeMonitor(result['股票代码'],result['股票名称'],result.key)">
|
||||
取消关注
|
||||
</n-button>
|
||||
<n-button size="tiny" v-if="data.openAiEnable" secondary type="warning" @click="aiCheckStock(result['股票名称'],result['股票代码'])"> AI分析 </n-button>
|
||||
<n-button size="tiny" v-if="data.openAiEnable" secondary type="warning" @click="aiCheckStock(result['股票名称'],result['股票代码'])">
|
||||
AI分析
|
||||
</n-button>
|
||||
|
||||
</template>
|
||||
<template #footer>
|
||||
@@ -550,13 +652,28 @@ function getHeight() {
|
||||
<n-image :src="data.kURL" />
|
||||
</n-modal>
|
||||
|
||||
<n-modal transform-origin="center" v-model:show="modalShow4" preset="card" style="width: 800px;height: 480px" :title="'['+data.name+']AI分析结果'" >
|
||||
<n-modal transform-origin="center" v-model:show="modalShow4" preset="card" style="width: 800px;height: 500px" :title="'['+data.name+']AI分析结果'" >
|
||||
<n-spin size="small" :show="data.loading">
|
||||
<MdPreview ref="mdPreviewRef" style="height: 380px" :modelValue="data.airesult" :theme="'dark'"/>
|
||||
<MdPreview ref="mdPreviewRef" style="height: 380px;text-align: left" :modelValue="data.airesult" :theme="'dark'"/>
|
||||
</n-spin>
|
||||
<template #header-extra>
|
||||
|
||||
</template>
|
||||
<template #footer>
|
||||
<n-flex justify="space-between">
|
||||
<n-text type="error" v-if="data.time" >分析时间:{{data.time}}</n-text>
|
||||
<n-button size="tiny" type="warning" @click="aiReCheckStock(data.name,data.code)">再次分析</n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.md-editor-preview h3{
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.md-editor-preview p{
|
||||
text-align: left !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,10 +2,12 @@ import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
|
||||
import stockView from '../components/stock.vue'
|
||||
import settingsView from '../components/settings.vue'
|
||||
import about from "../components/about.vue";
|
||||
|
||||
const routes = [
|
||||
{ path: '/', component: stockView,name: 'stock' },
|
||||
{ path: '/settings/:id', component: settingsView,name: 'settings' },
|
||||
{ path: '/about', component: about,name: 'about' },
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
9
frontend/wailsjs/go/main/App.d.ts
vendored
9
frontend/wailsjs/go/main/App.d.ts
vendored
@@ -1,21 +1,26 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {models} from '../models';
|
||||
import {data} from '../models';
|
||||
|
||||
export function Follow(arg1:string):Promise<string>;
|
||||
|
||||
export function GetAIResponseResult(arg1:string):Promise<models.AIResponseResult>;
|
||||
|
||||
export function GetConfig():Promise<data.Settings>;
|
||||
|
||||
export function GetFollowList():Promise<Array<data.FollowedStock>>;
|
||||
|
||||
export function GetStockList(arg1:string):Promise<Array<data.StockBasic>>;
|
||||
|
||||
export function GetVersionInfo():Promise<models.VersionInfo>;
|
||||
|
||||
export function Greet(arg1:string):Promise<data.StockInfo>;
|
||||
|
||||
export function NewChat(arg1:string):Promise<string>;
|
||||
|
||||
export function NewChatStream(arg1:string,arg2:string):Promise<void>;
|
||||
|
||||
export function SaveAIResponseResult(arg1:string,arg2:string,arg3:string):Promise<void>;
|
||||
|
||||
export function SendDingDingMessage(arg1:string,arg2:string):Promise<string>;
|
||||
|
||||
export function SendDingDingMessageByType(arg1:string,arg2:string,arg3:number):Promise<string>;
|
||||
|
||||
@@ -6,6 +6,10 @@ export function Follow(arg1) {
|
||||
return window['go']['main']['App']['Follow'](arg1);
|
||||
}
|
||||
|
||||
export function GetAIResponseResult(arg1) {
|
||||
return window['go']['main']['App']['GetAIResponseResult'](arg1);
|
||||
}
|
||||
|
||||
export function GetConfig() {
|
||||
return window['go']['main']['App']['GetConfig']();
|
||||
}
|
||||
@@ -18,18 +22,22 @@ export function GetStockList(arg1) {
|
||||
return window['go']['main']['App']['GetStockList'](arg1);
|
||||
}
|
||||
|
||||
export function GetVersionInfo() {
|
||||
return window['go']['main']['App']['GetVersionInfo']();
|
||||
}
|
||||
|
||||
export function Greet(arg1) {
|
||||
return window['go']['main']['App']['Greet'](arg1);
|
||||
}
|
||||
|
||||
export function NewChat(arg1) {
|
||||
return window['go']['main']['App']['NewChat'](arg1);
|
||||
}
|
||||
|
||||
export function NewChatStream(arg1, arg2) {
|
||||
return window['go']['main']['App']['NewChatStream'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function SaveAIResponseResult(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['SaveAIResponseResult'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function SendDingDingMessage(arg1, arg2) {
|
||||
return window['go']['main']['App']['SendDingDingMessage'](arg1, arg2);
|
||||
}
|
||||
|
||||
@@ -73,7 +73,9 @@ export namespace data {
|
||||
openAiModelName: string;
|
||||
openAiMaxTokens: number;
|
||||
openAiTemperature: number;
|
||||
openAiApiTimeOut: number;
|
||||
prompt: string;
|
||||
checkUpdate: boolean;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Settings(source);
|
||||
@@ -97,7 +99,9 @@ export namespace data {
|
||||
this.openAiModelName = source["openAiModelName"];
|
||||
this.openAiMaxTokens = source["openAiMaxTokens"];
|
||||
this.openAiTemperature = source["openAiTemperature"];
|
||||
this.openAiApiTimeOut = source["openAiApiTimeOut"];
|
||||
this.prompt = source["prompt"];
|
||||
this.checkUpdate = source["checkUpdate"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
@@ -325,3 +329,104 @@ export namespace data {
|
||||
|
||||
}
|
||||
|
||||
export namespace models {
|
||||
|
||||
export class AIResponseResult {
|
||||
ID: number;
|
||||
// Go type: time
|
||||
CreatedAt: any;
|
||||
// Go type: time
|
||||
UpdatedAt: any;
|
||||
// Go type: gorm
|
||||
DeletedAt: any;
|
||||
stockCode: string;
|
||||
stockName: string;
|
||||
content: string;
|
||||
IsDel: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new AIResponseResult(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.ID = source["ID"];
|
||||
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
|
||||
this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
|
||||
this.DeletedAt = this.convertValues(source["DeletedAt"], null);
|
||||
this.stockCode = source["stockCode"];
|
||||
this.stockName = source["stockName"];
|
||||
this.content = source["content"];
|
||||
this.IsDel = source["IsDel"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class VersionInfo {
|
||||
ID: number;
|
||||
// Go type: time
|
||||
CreatedAt: any;
|
||||
// Go type: time
|
||||
UpdatedAt: any;
|
||||
// Go type: gorm
|
||||
DeletedAt: any;
|
||||
version: string;
|
||||
content: string;
|
||||
icon: string;
|
||||
buildTimeStamp: number;
|
||||
IsDel: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new VersionInfo(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.ID = source["ID"];
|
||||
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
|
||||
this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
|
||||
this.DeletedAt = this.convertValues(source["DeletedAt"], null);
|
||||
this.version = source["version"];
|
||||
this.content = source["content"];
|
||||
this.icon = source["icon"];
|
||||
this.buildTimeStamp = source["buildTimeStamp"];
|
||||
this.IsDel = source["IsDel"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
9
main.go
9
main.go
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/models"
|
||||
"log"
|
||||
"os"
|
||||
goruntime "runtime"
|
||||
@@ -33,6 +34,10 @@ var icon2 []byte
|
||||
var stocksBin []byte
|
||||
|
||||
//go:generate cp -R ./data ./build/bin
|
||||
|
||||
var Version string
|
||||
var VersionCommit string
|
||||
|
||||
func main() {
|
||||
checkDir("data")
|
||||
db.Init("")
|
||||
@@ -41,6 +46,7 @@ func main() {
|
||||
db.Dao.AutoMigrate(&data.FollowedStock{})
|
||||
db.Dao.AutoMigrate(&data.IndexBasic{})
|
||||
db.Dao.AutoMigrate(&data.Settings{})
|
||||
db.Dao.AutoMigrate(&models.AIResponseResult{})
|
||||
|
||||
if stocksBin != nil && len(stocksBin) > 0 {
|
||||
go initStockData()
|
||||
@@ -82,7 +88,8 @@ func main() {
|
||||
//FileMenu.AddText("退出", keys.CmdOrCtrl("q"), func(_ *menu.CallbackData) {
|
||||
// runtime.Quit(app.ctx)
|
||||
//})
|
||||
|
||||
logger.NewDefaultLogger().Info("version: " + Version)
|
||||
logger.NewDefaultLogger().Info("commit: " + VersionCommit)
|
||||
// Create application with options
|
||||
err := wails.Run(&options.App{
|
||||
Title: "go-stock",
|
||||
|
||||
Reference in New Issue
Block a user