Compare commits
96 Commits
dev-single
...
v2025.12.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c75c8533a | ||
|
|
1ad02d4b0c | ||
|
|
a354ab8925 | ||
|
|
6d50be8541 | ||
|
|
6303d535bc | ||
|
|
b464d8f563 | ||
|
|
eea0856c1c | ||
|
|
1dd77d5c08 | ||
|
|
9c1c0382ca | ||
|
|
46065f448b | ||
|
|
a7cee69e68 | ||
|
|
459441f838 | ||
|
|
b4b3b61e8c | ||
|
|
b6a99940ab | ||
|
|
5b0f34a3bd | ||
|
|
e34ebf9895 | ||
|
|
c3521c6d7f | ||
|
|
93b37ca621 | ||
|
|
7069af869b | ||
|
|
dbb6789c05 | ||
|
|
8aed4d2753 | ||
|
|
6bd1bdae02 | ||
|
|
9a40d343aa | ||
|
|
e4cdad6ffe | ||
|
|
a0005dab96 | ||
|
|
c945ca9322 | ||
|
|
7bbc6831f4 | ||
|
|
ab0ccc4fe0 | ||
|
|
fa658357c9 | ||
|
|
746589a972 | ||
|
|
401dd17fa8 | ||
|
|
c365bd9534 | ||
|
|
104ee51e13 | ||
|
|
00f3e5f0e0 | ||
|
|
483ffa2244 | ||
|
|
63d278b9aa | ||
|
|
5621d40c71 | ||
|
|
26e9753b94 | ||
|
|
b7f6dbd2da | ||
|
|
18dd01b613 | ||
|
|
81bb33a135 | ||
|
|
9926b61fac | ||
|
|
5e975b060c | ||
|
|
e8f063fd9b | ||
|
|
8b0b53fae7 | ||
|
|
b29c380055 | ||
|
|
cf58a707c7 | ||
|
|
1ae1bb0116 | ||
|
|
d8971935ee | ||
|
|
9c68458b81 | ||
|
|
b367d1eb40 | ||
|
|
8fe79adbb1 | ||
|
|
1d81fdba87 | ||
|
|
6aca0e15cc | ||
|
|
173ce6f243 | ||
|
|
e7875e73d3 | ||
|
|
ca4727db80 | ||
|
|
84ffe7c5fd | ||
|
|
da02d1bd1c | ||
|
|
bae2bf9c5c | ||
|
|
6568b5949a | ||
|
|
c4287f9b78 | ||
|
|
87441d8923 | ||
|
|
ebd166e72b | ||
|
|
494a60debe | ||
|
|
b3e2565a02 | ||
|
|
c0a87d5d2e | ||
|
|
d74ad3c03d | ||
|
|
6dff9d95c4 | ||
|
|
06967420f8 | ||
|
|
6f4eb0ac86 | ||
|
|
f59255cc6c | ||
|
|
4f4fa46338 | ||
|
|
05bf35fdf4 | ||
|
|
567c81ae7c | ||
|
|
86f4e54d13 | ||
|
|
71e6ff4233 | ||
|
|
e844e2cff9 | ||
|
|
27af39ff61 | ||
|
|
5537ebb87a | ||
|
|
b906140dd5 | ||
|
|
087b953ed8 | ||
|
|
3c5205738f | ||
|
|
b1b34d950b | ||
|
|
83aa4331ad | ||
|
|
d4d3c44cf4 | ||
|
|
81a9cc5927 | ||
|
|
3fc89a85da | ||
|
|
0605c8442d | ||
|
|
cf8591c208 | ||
|
|
7607c4356f | ||
|
|
4aae2ece00 | ||
|
|
369d14025c | ||
|
|
1e7387f3fa | ||
|
|
cfd218f181 | ||
|
|
b8e1f38a32 |
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@@ -50,7 +50,7 @@ jobs:
|
||||
build-name: ${{ matrix.build.name }}
|
||||
build-platform: ${{ matrix.build.platform }}
|
||||
package: true
|
||||
go-version: '1.24'
|
||||
go-version: '1.25'
|
||||
build-tags: ${{ github.ref_name }}
|
||||
build-commit-message: ${{ steps.get_commit_message.outputs.commit_message }}
|
||||
build-statement: ${{ env.OFFICIAL_STATEMENT }}
|
||||
|
||||
25
README.md
25
README.md
@@ -22,24 +22,25 @@
|
||||
- 开发环境主要基于Windows10+,其他平台未测试或功能受限。
|
||||
|
||||
### 📦 立即体验
|
||||
- 安装版:[go-stock-amd64-installer.exe](https://github.com/ArvinLovegood/go-stock/releases)
|
||||
[//]: # (- 安装版:[go-stock-amd64-installer.exe](https://github.com/ArvinLovegood/go-stock/releases))
|
||||
- 绿色版:[go-stock-windows-amd64.exe](https://github.com/ArvinLovegood/go-stock/releases)
|
||||
- MACOS绿色版:[go-stock-darwin-universal](https://github.com/ArvinLovegood/go-stock/releases)
|
||||
- MACOS安装版:[go-stock-darwin-universal.pkg](https://github.com/ArvinLovegood/go-stock/releases)
|
||||
|
||||
[//]: # (- MACOS安装版:[go-stock-darwin-universal.pkg](https://github.com/ArvinLovegood/go-stock/releases))
|
||||
|
||||
|
||||
### 💬 支持大模型/平台
|
||||
| 模型 | 状态 | 备注 |
|
||||
| --- | --- |---------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [OpenAI](https://platform.openai.com/) | ✅ | 可接入任何 OpenAI 接口格式模型 |
|
||||
| [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 |
|
||||
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型运行平台 |
|
||||
| [AnythingLLM](https://anythingllm.com/) | ✅ | 本地知识库 |
|
||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | deepseek-reasoner,deepseek-chat |
|
||||
| [大模型聚合平台](https://cloud.siliconflow.cn/i/foufCerk) | ✅ | 如:[硅基流动](https://cloud.siliconflow.cn/i/foufCerk),[火山方舟](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ) ,[优云智算](https://www.compshare.cn/image-community?ytag=GPU_YY-gh_gostock) |
|
||||
| 模型 | 状态 | 备注 |
|
||||
| --- | --- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [OpenAI](https://platform.openai.com/) | ✅ | 可接入任何 OpenAI 接口格式模型 |
|
||||
| [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 |
|
||||
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型运行平台 |
|
||||
| [AnythingLLM](https://anythingllm.com/) | ✅ | 本地知识库 |
|
||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | deepseek-reasoner,deepseek-chat |
|
||||
| [大模型聚合平台](https://cloud.siliconflow.cn/i/foufCerk) | ✅ | 如:[硅基流动](https://cloud.siliconflow.cn/i/foufCerk),[火山方舟](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ) |
|
||||
|
||||
### <span style="color: #568DF4;">各位亲爱的朋友们,如果您对这个项目感兴趣,请先给我一个<i style="color: #EA2626;">star</i>吧,谢谢!</span>💕
|
||||
- 优云智算(by UCloud):万卡规模4090免费用10小时,新人注册另增50万tokens,海量热门源项目镜像一键部署,[注册链接](https://www.compshare.cn/image-community?ytag=GPU_YY-gh_gostock)
|
||||
[//]: # (- 优云智算(by UCloud):万卡规模4090免费用10小时,新人注册另增50万tokens,海量热门源项目镜像一键部署,[注册链接](https://www.compshare.cn/image-community?ytag=GPU_YY-gh_gostock))
|
||||
- 火山方舟:新用户每个模型注册即送50万tokens,[注册链接](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ)
|
||||
- 硅基流动(siliconflow),注册即送2000万Tokens,[注册链接](https://cloud.siliconflow.cn/i/foufCerk)
|
||||
- Tushare大数据开放社区,免费提供各类金融数据,助力行业和量化研究(注意:Tushare只需要120积分即可,注册完成个人资料补充即可得120积分!!!),[注册链接](https://tushare.pro/register?reg=701944)
|
||||
@@ -59,7 +60,7 @@
|
||||
| 功能说明 | 状态 | 备注 |
|
||||
|-----------------|----|----------------------------------------------------------------------------------------------------------|
|
||||
| 股票分析知识库 | 🚧 | 未来计划 |
|
||||
| Ai智能选股 | 🚧 | Ai智能选股功能开发中(下半年重点开发计划) |
|
||||
| Ai智能选股 | ✅ | Ai智能选股功能(市场行情-》AI总结/AI智能体功能) |
|
||||
| ETF支持 | 🚧 | ETF数据支持 (目前可以查看净值和估值) |
|
||||
| 美股支持 | ✅ | 美股数据支持 |
|
||||
| 港股支持 | ✅ | 港股数据支持 |
|
||||
|
||||
310
app.go
310
app.go
@@ -7,8 +7,6 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/duke-git/lancet/v2/cryptor"
|
||||
"github.com/inconshreveable/go-update"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
@@ -18,6 +16,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/duke-git/lancet/v2/cryptor"
|
||||
"github.com/inconshreveable/go-update"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/coocood/freecache"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
@@ -104,6 +105,76 @@ func AddTools(tools []data.Tool) []data.Tool {
|
||||
},
|
||||
})
|
||||
|
||||
tools = append(tools, data.Tool{
|
||||
Type: "function",
|
||||
Function: data.ToolFunction{
|
||||
Name: "InteractiveAnswer",
|
||||
Description: "获取投资者与上市公司互动问答的数据,反映当前投资者关注的热点问题",
|
||||
Parameters: data.FunctionParameters{
|
||||
Type: "object",
|
||||
Properties: map[string]any{
|
||||
"page": map[string]any{
|
||||
"type": "string",
|
||||
"description": "分页号",
|
||||
},
|
||||
"pageSize": map[string]any{
|
||||
"type": "string",
|
||||
"description": "分页大小",
|
||||
},
|
||||
"keyWord": map[string]any{
|
||||
"type": "string",
|
||||
"description": "搜索关键词(可输入股票名称或者当前热门板块/行业/概念/标的/事件等)",
|
||||
},
|
||||
},
|
||||
Required: []string{"page", "pageSize"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
//tools = append(tools, data.Tool{
|
||||
// Type: "function",
|
||||
// Function: data.ToolFunction{
|
||||
// Name: "QueryBKDictInfo",
|
||||
// Description: "获取所有板块/行业名称或者代码(bkCode,bkName)",
|
||||
// },
|
||||
//})
|
||||
|
||||
//tools = append(tools, data.Tool{
|
||||
// Type: "function",
|
||||
// Function: data.ToolFunction{
|
||||
// Name: "GetIndustryResearchReport",
|
||||
// Description: "获取行业/板块研究报告,请先使用QueryBKDictInfo工具获取行业代码,然后输入行业代码调用",
|
||||
// Parameters: data.FunctionParameters{
|
||||
// Type: "object",
|
||||
// Properties: map[string]any{
|
||||
// "bkCode": map[string]any{
|
||||
// "type": "string",
|
||||
// "description": "板块/行业代码",
|
||||
// },
|
||||
// },
|
||||
// Required: []string{"bkCode"},
|
||||
// },
|
||||
// },
|
||||
//})
|
||||
|
||||
tools = append(tools, data.Tool{
|
||||
Type: "function",
|
||||
Function: data.ToolFunction{
|
||||
Name: "GetStockResearchReport",
|
||||
Description: "获取市场分析师的股票研究报告",
|
||||
Parameters: data.FunctionParameters{
|
||||
Type: "object",
|
||||
Properties: map[string]any{
|
||||
"stockCode": map[string]any{
|
||||
"type": "string",
|
||||
"description": "股票代码",
|
||||
},
|
||||
},
|
||||
Required: []string{"stockCode"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return tools
|
||||
}
|
||||
|
||||
@@ -256,7 +327,7 @@ func (a *App) CheckUpdate(flag int) {
|
||||
}
|
||||
go runtime.EventsEmit(a.ctx, "newsPush", map[string]any{
|
||||
"time": "发现新版本:" + releaseVersion.TagName,
|
||||
"isRed": false,
|
||||
"isRed": true,
|
||||
"source": "go-stock",
|
||||
"content": fmt.Sprintf("%s", commit.Message),
|
||||
})
|
||||
@@ -272,7 +343,7 @@ func (a *App) CheckUpdate(flag int) {
|
||||
}
|
||||
body := resp.Body()
|
||||
|
||||
if len(body) < 1024 {
|
||||
if len(body) < 1024*500 {
|
||||
go runtime.EventsEmit(a.ctx, "newsPush", map[string]any{
|
||||
"time": "新版本:" + releaseVersion.TagName,
|
||||
"isRed": true,
|
||||
@@ -299,7 +370,7 @@ func (a *App) CheckUpdate(flag int) {
|
||||
if flag == 1 {
|
||||
go runtime.EventsEmit(a.ctx, "newsPush", map[string]any{
|
||||
"time": "当前版本:" + Version,
|
||||
"isRed": false,
|
||||
"isRed": true,
|
||||
"source": "go-stock",
|
||||
"content": "当前版本无更新",
|
||||
})
|
||||
@@ -312,7 +383,11 @@ func (a *App) CheckUpdate(flag int) {
|
||||
func (a *App) domReady(ctx context.Context) {
|
||||
defer PanicHandler()
|
||||
defer func() {
|
||||
go runtime.EventsEmit(ctx, "loadingMsg", "done")
|
||||
// 增加延迟确保前端已准备好接收事件
|
||||
go func() {
|
||||
time.Sleep(2 * time.Second)
|
||||
runtime.EventsEmit(a.ctx, "loadingMsg", "done")
|
||||
}()
|
||||
}()
|
||||
|
||||
//if stocksBin != nil && len(stocksBin) > 0 {
|
||||
@@ -333,8 +408,12 @@ func (a *App) domReady(ctx context.Context) {
|
||||
|
||||
// Add your action here
|
||||
//定时更新数据
|
||||
config := data.NewSettingsApi(&data.Settings{}).GetConfig()
|
||||
config := data.GetSettingConfig()
|
||||
go func() {
|
||||
go data.NewMarketNewsApi().TelegraphList(30)
|
||||
go data.NewMarketNewsApi().GetSinaNews(30)
|
||||
go data.NewMarketNewsApi().TradingViewNews()
|
||||
|
||||
interval := config.RefreshInterval
|
||||
if interval <= 0 {
|
||||
interval = 1
|
||||
@@ -353,7 +432,8 @@ func (a *App) domReady(ctx context.Context) {
|
||||
a.cronEntrys["MonitorStockPrices"] = id
|
||||
}
|
||||
entryID, err := a.cron.AddFunc(fmt.Sprintf("@every %ds", interval+10), func() {
|
||||
news := data.NewMarketNewsApi().GetNewTelegraph(30)
|
||||
//news := data.NewMarketNewsApi().GetNewTelegraph(30)
|
||||
news := data.NewMarketNewsApi().TelegraphList(30)
|
||||
if config.EnablePushNews {
|
||||
go a.NewsPush(news)
|
||||
}
|
||||
@@ -377,6 +457,19 @@ func (a *App) domReady(ctx context.Context) {
|
||||
} else {
|
||||
a.cronEntrys["newSinaNews"] = entryIDSina
|
||||
}
|
||||
|
||||
entryIDTradingViewNews, err := a.cron.AddFunc(fmt.Sprintf("@every %ds", interval+10), func() {
|
||||
news := data.NewMarketNewsApi().TradingViewNews()
|
||||
if config.EnablePushNews {
|
||||
go a.NewsPush(news)
|
||||
}
|
||||
go runtime.EventsEmit(a.ctx, "tradingViewNews", news)
|
||||
})
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("AddFunc error:%s", err.Error())
|
||||
} else {
|
||||
a.cronEntrys["tradingViewNews"] = entryIDTradingViewNews
|
||||
}
|
||||
}()
|
||||
|
||||
//刷新基金净值信息
|
||||
@@ -434,7 +527,12 @@ func (a *App) domReady(ctx context.Context) {
|
||||
//检查新版本
|
||||
go func() {
|
||||
a.CheckUpdate(0)
|
||||
a.CheckStockBaseInfo(a.ctx)
|
||||
go a.CheckStockBaseInfo(a.ctx)
|
||||
|
||||
a.cron.AddFunc("0 0 2 * * *", func() {
|
||||
logger.SugaredLogger.Errorf("Checking for updates...")
|
||||
a.CheckStockBaseInfo(a.ctx)
|
||||
})
|
||||
a.cron.AddFunc("30 05 8,12,20 * * *", func() {
|
||||
logger.SugaredLogger.Errorf("Checking for updates...")
|
||||
a.CheckUpdate(0)
|
||||
@@ -484,67 +582,102 @@ func (a *App) CheckStockBaseInfo(ctx context.Context) {
|
||||
SetResult(stockBasics).
|
||||
Get("http://8.134.249.145:18080/go-stock/stock_basic.json")
|
||||
|
||||
for _, stock := range *stockBasics {
|
||||
stockInfo := &data.StockBasic{
|
||||
TsCode: stock.TsCode,
|
||||
Name: stock.Name,
|
||||
Symbol: stock.Symbol,
|
||||
BKCode: stock.BKCode,
|
||||
BKName: stock.BKName,
|
||||
}
|
||||
db.Dao.Model(&data.StockBasic{}).Where("ts_code = ?", stock.TsCode).First(stockInfo)
|
||||
if stockInfo.ID == 0 {
|
||||
db.Dao.Model(&data.StockBasic{}).Create(stockInfo)
|
||||
} else {
|
||||
db.Dao.Model(&data.StockBasic{}).Where("ts_code = ?", stock.TsCode).Updates(stockInfo)
|
||||
}
|
||||
db.Dao.Unscoped().Model(&data.StockBasic{}).Where("1=1").Delete(&data.StockBasic{})
|
||||
err := db.Dao.CreateInBatches(stockBasics, 400).Error
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("保存StockBasic股票基础信息失败:%s", err.Error())
|
||||
}
|
||||
|
||||
//count := int64(0)
|
||||
//db.Dao.Model(&data.StockBasic{}).Count(&count)
|
||||
//if count == int64(len(*stockBasics)) {
|
||||
// return
|
||||
//}
|
||||
//for _, stock := range *stockBasics {
|
||||
// stockInfo := &data.StockBasic{
|
||||
// TsCode: stock.TsCode,
|
||||
// Name: stock.Name,
|
||||
// Symbol: stock.Symbol,
|
||||
// BKCode: stock.BKCode,
|
||||
// BKName: stock.BKName,
|
||||
// }
|
||||
// db.Dao.Model(&data.StockBasic{}).Where("ts_code = ?", stock.TsCode).First(stockInfo)
|
||||
// if stockInfo.ID == 0 {
|
||||
// db.Dao.Model(&data.StockBasic{}).Create(stockInfo)
|
||||
// } else {
|
||||
// db.Dao.Model(&data.StockBasic{}).Where("ts_code = ?", stock.TsCode).Updates(stockInfo)
|
||||
// }
|
||||
//}
|
||||
|
||||
stockHKBasics := &[]models.StockInfoHK{}
|
||||
resty.New().R().
|
||||
SetHeader("user", "go-stock").
|
||||
SetResult(stockHKBasics).
|
||||
Get("http://8.134.249.145:18080/go-stock/stock_base_info_hk.json")
|
||||
for _, stock := range *stockHKBasics {
|
||||
stockInfo := &models.StockInfoHK{
|
||||
Code: stock.Code,
|
||||
Name: stock.Name,
|
||||
BKName: stock.BKName,
|
||||
BKCode: stock.BKCode,
|
||||
}
|
||||
db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", stock.Code).First(stockInfo)
|
||||
if stockInfo.ID == 0 {
|
||||
db.Dao.Model(&models.StockInfoHK{}).Create(stockInfo)
|
||||
} else {
|
||||
db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", stock.Code).Updates(stockInfo)
|
||||
}
|
||||
|
||||
db.Dao.Unscoped().Model(&models.StockInfoHK{}).Where("1=1").Delete(&models.StockInfoHK{})
|
||||
err = db.Dao.CreateInBatches(stockHKBasics, 400).Error
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("保存StockInfoHK股票基础信息失败:%s", err.Error())
|
||||
}
|
||||
|
||||
//for _, stock := range *stockHKBasics {
|
||||
// stockInfo := &models.StockInfoHK{
|
||||
// Code: stock.Code,
|
||||
// Name: stock.Name,
|
||||
// BKName: stock.BKName,
|
||||
// BKCode: stock.BKCode,
|
||||
// }
|
||||
// db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", stock.Code).First(stockInfo)
|
||||
// if stockInfo.ID == 0 {
|
||||
// db.Dao.Model(&models.StockInfoHK{}).Create(stockInfo)
|
||||
// } else {
|
||||
// db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", stock.Code).Updates(stockInfo)
|
||||
// }
|
||||
//}
|
||||
stockUSBasics := &[]models.StockInfoUS{}
|
||||
resty.New().R().
|
||||
SetHeader("user", "go-stock").
|
||||
SetResult(stockUSBasics).
|
||||
Get("http://8.134.249.145:18080/go-stock/stock_base_info_us.json")
|
||||
for _, stock := range *stockUSBasics {
|
||||
stockInfo := &models.StockInfoUS{
|
||||
Code: stock.Code,
|
||||
Name: stock.Name,
|
||||
BKName: stock.BKName,
|
||||
BKCode: stock.BKCode,
|
||||
}
|
||||
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", stock.Code).First(stockInfo)
|
||||
if stockInfo.ID == 0 {
|
||||
db.Dao.Model(&models.StockInfoUS{}).Create(stockInfo)
|
||||
} else {
|
||||
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", stock.Code).Updates(stockInfo)
|
||||
}
|
||||
|
||||
db.Dao.Unscoped().Model(&models.StockInfoUS{}).Where("1=1").Delete(&models.StockInfoUS{})
|
||||
err = db.Dao.CreateInBatches(stockUSBasics, 400).Error
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("保存StockInfoUS股票基础信息失败:%s", err.Error())
|
||||
}
|
||||
//for _, stock := range *stockUSBasics {
|
||||
// stockInfo := &models.StockInfoUS{
|
||||
// Code: stock.Code,
|
||||
// Name: stock.Name,
|
||||
// BKName: stock.BKName,
|
||||
// BKCode: stock.BKCode,
|
||||
// }
|
||||
// db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", stock.Code).First(stockInfo)
|
||||
// if stockInfo.ID == 0 {
|
||||
// db.Dao.Model(&models.StockInfoUS{}).Create(stockInfo)
|
||||
// } else {
|
||||
// db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", stock.Code).Updates(stockInfo)
|
||||
// }
|
||||
//}
|
||||
|
||||
}
|
||||
func (a *App) NewsPush(news *[]models.Telegraph) {
|
||||
|
||||
follows := data.NewStockDataApi().GetFollowList(0)
|
||||
stockNames := slice.Map(*follows, func(index int, item data.FollowedStock) string {
|
||||
return item.Name
|
||||
})
|
||||
|
||||
for _, telegraph := range *news {
|
||||
//if telegraph.IsRed {
|
||||
go runtime.EventsEmit(a.ctx, "newsPush", telegraph)
|
||||
go data.NewAlertWindowsApi("go-stock", telegraph.Source+" "+telegraph.Time, telegraph.Content, string(icon)).SendNotification()
|
||||
if a.GetConfig().EnableOnlyPushRedNews {
|
||||
if telegraph.IsRed || strutil.ContainsAny(telegraph.Content, stockNames) {
|
||||
go runtime.EventsEmit(a.ctx, "newsPush", telegraph)
|
||||
}
|
||||
} else {
|
||||
go runtime.EventsEmit(a.ctx, "newsPush", telegraph)
|
||||
}
|
||||
//go data.NewAlertWindowsApi("go-stock", telegraph.Source+" "+telegraph.Time, telegraph.Content, string(icon)).SendNotification()
|
||||
//}
|
||||
}
|
||||
}
|
||||
@@ -552,7 +685,7 @@ func (a *App) NewsPush(news *[]models.Telegraph) {
|
||||
func (a *App) AddCronTask(follow data.FollowedStock) func() {
|
||||
return func() {
|
||||
go runtime.EventsEmit(a.ctx, "warnMsg", "开始自动分析"+follow.Name+"_"+follow.StockCode)
|
||||
ai := data.NewDeepSeekOpenAi(a.ctx)
|
||||
ai := data.NewDeepSeekOpenAi(a.ctx, follow.AiConfigId)
|
||||
msgs := ai.NewChatStream(follow.Name, follow.StockCode, "", nil, a.AiTools)
|
||||
var res strings.Builder
|
||||
|
||||
@@ -572,7 +705,8 @@ func (a *App) AddCronTask(follow data.FollowedStock) func() {
|
||||
question = msg["question"].(string)
|
||||
}
|
||||
}
|
||||
data.NewDeepSeekOpenAi(a.ctx).SaveAIResponseResult(follow.StockCode, follow.Name, res.String(), chatId, question)
|
||||
|
||||
data.NewDeepSeekOpenAi(a.ctx, follow.AiConfigId).SaveAIResponseResult(follow.StockCode, follow.Name, res.String(), chatId, question)
|
||||
go runtime.EventsEmit(a.ctx, "warnMsg", "AI分析完成:"+follow.Name+"_"+follow.StockCode)
|
||||
|
||||
}
|
||||
@@ -583,7 +717,7 @@ func refreshTelegraphList() *[]string {
|
||||
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))
|
||||
Get(url)
|
||||
if err != nil {
|
||||
return &[]string{}
|
||||
}
|
||||
@@ -917,12 +1051,12 @@ func (a *App) SendDingDingMessageByType(message string, stockCode string, msgTyp
|
||||
return data.NewDingDingAPI().SendDingDingMessage(message)
|
||||
}
|
||||
|
||||
func (a *App) NewChatStream(stock, stockCode, question string, sysPromptId *int, enableTools bool) {
|
||||
func (a *App) NewChatStream(stock, stockCode, question string, aiConfigId int, sysPromptId *int, enableTools bool) {
|
||||
var msgs <-chan map[string]any
|
||||
if enableTools {
|
||||
msgs = data.NewDeepSeekOpenAi(a.ctx).NewChatStream(stock, stockCode, question, sysPromptId, a.AiTools)
|
||||
msgs = data.NewDeepSeekOpenAi(a.ctx, aiConfigId).NewChatStream(stock, stockCode, question, sysPromptId, a.AiTools)
|
||||
} else {
|
||||
msgs = data.NewDeepSeekOpenAi(a.ctx).NewChatStream(stock, stockCode, question, sysPromptId, []data.Tool{})
|
||||
msgs = data.NewDeepSeekOpenAi(a.ctx, aiConfigId).NewChatStream(stock, stockCode, question, sysPromptId, []data.Tool{})
|
||||
}
|
||||
for msg := range msgs {
|
||||
runtime.EventsEmit(a.ctx, "newChatStream", msg)
|
||||
@@ -930,11 +1064,11 @@ func (a *App) NewChatStream(stock, stockCode, question string, sysPromptId *int,
|
||||
runtime.EventsEmit(a.ctx, "newChatStream", "DONE")
|
||||
}
|
||||
|
||||
func (a *App) SaveAIResponseResult(stockCode, stockName, result, chatId, question string) {
|
||||
data.NewDeepSeekOpenAi(a.ctx).SaveAIResponseResult(stockCode, stockName, result, chatId, question)
|
||||
func (a *App) SaveAIResponseResult(stockCode, stockName, result, chatId, question string, aiConfigId int) {
|
||||
data.NewDeepSeekOpenAi(a.ctx, aiConfigId).SaveAIResponseResult(stockCode, stockName, result, chatId, question)
|
||||
}
|
||||
func (a *App) GetAIResponseResult(stock string) *models.AIResponseResult {
|
||||
return data.NewDeepSeekOpenAi(a.ctx).GetAIResponseResult(stock)
|
||||
return data.NewDeepSeekOpenAi(a.ctx, 0).GetAIResponseResult(stock)
|
||||
}
|
||||
|
||||
func (a *App) GetVersionInfo() *models.VersionInfo {
|
||||
@@ -1039,28 +1173,29 @@ func onExit(a *App) {
|
||||
//runtime.Quit(a.ctx)
|
||||
}
|
||||
|
||||
func (a *App) UpdateConfig(settings *data.Settings) string {
|
||||
//logger.SugaredLogger.Infof("UpdateConfig:%+v", settings)
|
||||
if settings.RefreshInterval > 0 {
|
||||
func (a *App) UpdateConfig(settingConfig *data.SettingConfig) string {
|
||||
s1, _ := json.Marshal(settingConfig)
|
||||
logger.SugaredLogger.Infof("UpdateConfig:%s", s1)
|
||||
if settingConfig.RefreshInterval > 0 {
|
||||
if entryID, exists := a.cronEntrys["MonitorStockPrices"]; exists {
|
||||
a.cron.Remove(entryID)
|
||||
}
|
||||
id, _ := a.cron.AddFunc(fmt.Sprintf("@every %ds", settings.RefreshInterval), func() {
|
||||
id, _ := a.cron.AddFunc(fmt.Sprintf("@every %ds", settingConfig.RefreshInterval), func() {
|
||||
//logger.SugaredLogger.Infof("MonitorStockPrices:%s", time.Now())
|
||||
MonitorStockPrices(a)
|
||||
})
|
||||
a.cronEntrys["MonitorStockPrices"] = id
|
||||
}
|
||||
|
||||
return data.NewSettingsApi(settings).UpdateConfig()
|
||||
return data.UpdateConfig(settingConfig)
|
||||
}
|
||||
|
||||
func (a *App) GetConfig() *data.Settings {
|
||||
return data.NewSettingsApi(&data.Settings{}).GetConfig()
|
||||
func (a *App) GetConfig() *data.SettingConfig {
|
||||
return data.GetSettingConfig()
|
||||
}
|
||||
|
||||
func (a *App) ExportConfig() string {
|
||||
config := data.NewSettingsApi(&data.Settings{}).Export()
|
||||
config := data.NewSettingsApi().Export()
|
||||
file, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
|
||||
Title: "导出配置文件",
|
||||
CanCreateDirectories: true,
|
||||
@@ -1070,7 +1205,7 @@ func (a *App) ExportConfig() string {
|
||||
logger.SugaredLogger.Errorf("导出配置文件失败:%s", err.Error())
|
||||
return err.Error()
|
||||
}
|
||||
err = os.WriteFile(file, []byte(config), 0644)
|
||||
err = os.WriteFile(file, []byte(config), os.ModePerm)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("导出配置文件失败:%s", err.Error())
|
||||
return err.Error()
|
||||
@@ -1080,7 +1215,7 @@ func (a *App) ExportConfig() string {
|
||||
|
||||
func (a *App) ShareAnalysis(stockCode, stockName string) string {
|
||||
//http://go-stock.sparkmemory.top:16688/upload
|
||||
res := data.NewDeepSeekOpenAi(a.ctx).GetAIResponseResult(stockCode)
|
||||
res := data.NewDeepSeekOpenAi(a.ctx, 0).GetAIResponseResult(stockCode)
|
||||
if res != nil && len(res.Content) > 100 {
|
||||
analysisTime := res.CreatedAt.Format("2006/01/02")
|
||||
logger.SugaredLogger.Infof("%s analysisTime:%s", res.CreatedAt, analysisTime)
|
||||
@@ -1112,7 +1247,7 @@ func (a *App) UnFollowFund(fundCode string) string {
|
||||
return data.NewFundApi().UnFollowFund(fundCode)
|
||||
}
|
||||
func (a *App) SaveAsMarkdown(stockCode, stockName string) string {
|
||||
res := data.NewDeepSeekOpenAi(a.ctx).GetAIResponseResult(stockCode)
|
||||
res := data.NewDeepSeekOpenAi(a.ctx, 0).GetAIResponseResult(stockCode)
|
||||
if res != nil && len(res.Content) > 100 {
|
||||
analysisTime := res.CreatedAt.Format("2006-01-02_15_04_05")
|
||||
file, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
|
||||
@@ -1176,6 +1311,14 @@ func (a *App) GetGroupList() []data.Group {
|
||||
return data.NewStockGroupApi(db.Dao).GetGroupList()
|
||||
}
|
||||
|
||||
func (a *App) UpdateGroupSort(id int, newSort int) bool {
|
||||
return data.NewStockGroupApi(db.Dao).UpdateGroupSort(id, newSort)
|
||||
}
|
||||
|
||||
func (a *App) InitializeGroupSort() bool {
|
||||
return data.NewStockGroupApi(db.Dao).InitializeGroupSort()
|
||||
}
|
||||
|
||||
func (a *App) GetGroupStockList(groupId int) []data.GroupStock {
|
||||
return data.NewStockGroupApi(db.Dao).GetGroupStockByGroupId(groupId)
|
||||
}
|
||||
@@ -1231,8 +1374,10 @@ func (a *App) GetTelegraphList(source string) *[]*models.Telegraph {
|
||||
}
|
||||
|
||||
func (a *App) ReFleshTelegraphList(source string) *[]*models.Telegraph {
|
||||
data.NewMarketNewsApi().GetNewTelegraph(30)
|
||||
data.NewMarketNewsApi().GetSinaNews(30)
|
||||
//data.NewMarketNewsApi().GetNewTelegraph(30)
|
||||
go data.NewMarketNewsApi().TelegraphList(30)
|
||||
go data.NewMarketNewsApi().GetSinaNews(30)
|
||||
go data.NewMarketNewsApi().TradingViewNews()
|
||||
telegraphs := data.NewMarketNewsApi().GetTelegraphList(source)
|
||||
return telegraphs
|
||||
}
|
||||
@@ -1241,12 +1386,12 @@ func (a *App) GlobalStockIndexes() map[string]any {
|
||||
return data.NewMarketNewsApi().GlobalStockIndexes(30)
|
||||
}
|
||||
|
||||
func (a *App) SummaryStockNews(question string, sysPromptId *int, enableTools bool) {
|
||||
func (a *App) SummaryStockNews(question string, aiConfigId int, sysPromptId *int, enableTools bool) {
|
||||
var msgs <-chan map[string]any
|
||||
if enableTools {
|
||||
msgs = data.NewDeepSeekOpenAi(a.ctx).NewSummaryStockNewsStreamWithTools(question, sysPromptId, a.AiTools)
|
||||
msgs = data.NewDeepSeekOpenAi(a.ctx, aiConfigId).NewSummaryStockNewsStreamWithTools(question, sysPromptId, a.AiTools)
|
||||
} else {
|
||||
msgs = data.NewDeepSeekOpenAi(a.ctx).NewSummaryStockNewsStream(question, sysPromptId)
|
||||
msgs = data.NewDeepSeekOpenAi(a.ctx, aiConfigId).NewSummaryStockNewsStream(question, sysPromptId)
|
||||
}
|
||||
|
||||
for msg := range msgs {
|
||||
@@ -1311,7 +1456,7 @@ func (a *App) SaveImage(name, base64Data string) string {
|
||||
return "文件内容异常,无法保存。"
|
||||
}
|
||||
|
||||
err = os.WriteFile(filepath.Clean(filePath), decodeString, 0777)
|
||||
err = os.WriteFile(filepath.Clean(filePath), decodeString, os.ModePerm)
|
||||
if err != nil {
|
||||
return "保存结果异常,无法保存。"
|
||||
}
|
||||
@@ -1350,3 +1495,12 @@ func (a *App) SaveWordFile(filename string, base64Data string) string {
|
||||
}
|
||||
return filePath
|
||||
}
|
||||
|
||||
// GetAiConfigs
|
||||
//
|
||||
// @Description: // 获取AiConfig列表
|
||||
// @receiver a
|
||||
// @return error
|
||||
func (a *App) GetAiConfigs() []*data.AIConfig {
|
||||
return data.GetSettingConfig().AiConfigs
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"go-stock/backend/agent"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/models"
|
||||
"strings"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
@@ -24,41 +28,66 @@ func (a *App) StockNotice(stockCode string) []any {
|
||||
func (a *App) IndustryResearchReport(industryCode string) []any {
|
||||
return data.NewMarketNewsApi().IndustryResearchReport(industryCode, 7)
|
||||
}
|
||||
func (a App) EMDictCode(code string) []any {
|
||||
func (a *App) EMDictCode(code string) []any {
|
||||
return data.NewMarketNewsApi().EMDictCode(code, a.cache)
|
||||
}
|
||||
|
||||
func (a App) AnalyzeSentiment(text string) data.SentimentResult {
|
||||
func (a *App) AnalyzeSentiment(text string) data.SentimentResult {
|
||||
return data.AnalyzeSentiment(text)
|
||||
}
|
||||
|
||||
func (a App) HotStock(marketType string) *[]models.HotItem {
|
||||
func (a *App) HotStock(marketType string) *[]models.HotItem {
|
||||
return data.NewMarketNewsApi().XUEQIUHotStock(100, marketType)
|
||||
}
|
||||
|
||||
func (a App) HotEvent(size int) *[]models.HotEvent {
|
||||
func (a *App) HotEvent(size int) *[]models.HotEvent {
|
||||
if size <= 0 {
|
||||
size = 10
|
||||
}
|
||||
return data.NewMarketNewsApi().HotEvent(size)
|
||||
}
|
||||
func (a App) HotTopic(size int) []any {
|
||||
func (a *App) HotTopic(size int) []any {
|
||||
if size <= 0 {
|
||||
size = 10
|
||||
}
|
||||
return data.NewMarketNewsApi().HotTopic(size)
|
||||
}
|
||||
|
||||
func (a App) InvestCalendarTimeLine(yearMonth string) []any {
|
||||
func (a *App) InvestCalendarTimeLine(yearMonth string) []any {
|
||||
return data.NewMarketNewsApi().InvestCalendar(yearMonth)
|
||||
}
|
||||
func (a App) ClsCalendar() []any {
|
||||
func (a *App) ClsCalendar() []any {
|
||||
return data.NewMarketNewsApi().ClsCalendar()
|
||||
}
|
||||
|
||||
func (a App) SearchStock(words string) map[string]any {
|
||||
func (a *App) SearchStock(words string) map[string]any {
|
||||
return data.NewSearchStockApi(words).SearchStock(5000)
|
||||
}
|
||||
func (a App) GetHotStrategy() map[string]any {
|
||||
func (a *App) GetHotStrategy() map[string]any {
|
||||
return data.NewSearchStockApi("").HotStrategy()
|
||||
}
|
||||
|
||||
func (a *App) ChatWithAgent(question string, aiConfigId int, sysPromptId *int) {
|
||||
ch := agent.NewStockAiAgentApi().Chat(question, aiConfigId, sysPromptId)
|
||||
for msg := range ch {
|
||||
runtime.EventsEmit(a.ctx, "agent-message", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) AnalyzeSentimentWithFreqWeight(text string) map[string]any {
|
||||
if text == "" {
|
||||
telegraphs := data.NewMarketNewsApi().GetNews24HoursList("", 1000*10)
|
||||
messageText := strings.Builder{}
|
||||
for _, telegraph := range *telegraphs {
|
||||
messageText.WriteString(telegraph.Content + "\n")
|
||||
}
|
||||
text = messageText.String()
|
||||
}
|
||||
result, frequencies := data.AnalyzeSentimentWithFreqWeight(text)
|
||||
// 过滤标点符号和分隔符
|
||||
cleanFrequencies := data.FilterAndSortWords(frequencies)
|
||||
return map[string]any{
|
||||
"result": result,
|
||||
"frequencies": cleanFrequencies,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
@@ -31,22 +30,21 @@ func (a *App) startup(ctx context.Context) {
|
||||
|
||||
// 监听设置更新事件
|
||||
runtime.EventsOn(ctx, "updateSettings", func(optionalData ...interface{}) {
|
||||
logger.SugaredLogger.Infof("updateSettings : %v\n", optionalData)
|
||||
config := &data.Settings{}
|
||||
setMap := optionalData[0].(map[string]interface{})
|
||||
|
||||
// 将 map 转换为 JSON 字节切片
|
||||
jsonData, err := json.Marshal(setMap)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("Marshal error:%s", err.Error())
|
||||
return
|
||||
}
|
||||
// 将 JSON 字节切片解析到结构体中
|
||||
err = json.Unmarshal(jsonData, config)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("Unmarshal error:%s", err.Error())
|
||||
return
|
||||
}
|
||||
config := data.GetSettingConfig()
|
||||
//setMap := optionalData[0].(map[string]interface{})
|
||||
//
|
||||
//// 将 map 转换为 JSON 字节切片
|
||||
//jsonData, err := json.Marshal(setMap)
|
||||
//if err != nil {
|
||||
// logger.SugaredLogger.Errorf("Marshal error:%s", err.Error())
|
||||
// return
|
||||
//}
|
||||
//// 将 JSON 字节切片解析到结构体中
|
||||
//err = json.Unmarshal(jsonData, config)
|
||||
//if err != nil {
|
||||
// logger.SugaredLogger.Errorf("Unmarshal error:%s", err.Error())
|
||||
// return
|
||||
//}
|
||||
|
||||
logger.SugaredLogger.Infof("updateSettings config:%+v", config)
|
||||
if config.DarkTheme {
|
||||
|
||||
@@ -184,10 +184,10 @@ func getMsgTypeName(msgType int) string {
|
||||
return "未知类型"
|
||||
}
|
||||
}
|
||||
func (a *App) UpdateConfig(settings *data.Settings) string {
|
||||
return data.NewSettingsApi(settings).UpdateConfig()
|
||||
func (a *App) UpdateConfig(settingConfig *data.SettingConfig) string {
|
||||
return data.UpdateConfig(settingConfig)
|
||||
}
|
||||
|
||||
func (a *App) GetConfig() *data.Settings {
|
||||
return data.NewSettingsApi(&data.Settings{}).GetConfig()
|
||||
func (a *App) GetConfig() *data.SettingConfig {
|
||||
return data.GetSettingConfig()
|
||||
}
|
||||
|
||||
20
app_test.go
20
app_test.go
@@ -1,12 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
@@ -29,7 +32,7 @@ func TestIsUSTradingTime(t *testing.T) {
|
||||
|
||||
func TestCheckStockBaseInfo(t *testing.T) {
|
||||
db.Init("./data/stock.db")
|
||||
NewApp().CheckStockBaseInfo()
|
||||
NewApp().CheckStockBaseInfo(context.Background())
|
||||
}
|
||||
|
||||
func TestJson(t *testing.T) {
|
||||
@@ -44,3 +47,18 @@ func TestJson(t *testing.T) {
|
||||
db.Dao.Model(v).Updates(v)
|
||||
|
||||
}
|
||||
|
||||
func TestUpdateCheck(t *testing.T) {
|
||||
releaseVersion := &models.GitHubReleaseVersion{}
|
||||
_, err := resty.New().R().
|
||||
SetResult(releaseVersion).
|
||||
SetHeader("Accept", "application/vnd.github+json").
|
||||
SetHeader("X-GitHub-Api-Version", "2022-11-28").
|
||||
Get("https://api.github.com/repos/ArvinLovegood/go-stock/releases/latest")
|
||||
// https://api.github.com/repos/OWNER/REPO/releases/latest
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("get github release version error:%s", err.Error())
|
||||
return
|
||||
}
|
||||
logger.SugaredLogger.Infof("releaseVersion:%+v", releaseVersion)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
@@ -37,21 +36,21 @@ func (a *App) startup(ctx context.Context) {
|
||||
//})
|
||||
runtime.EventsOn(ctx, "updateSettings", func(optionalData ...interface{}) {
|
||||
logger.SugaredLogger.Infof("updateSettings : %v\n", optionalData)
|
||||
config := &data.Settings{}
|
||||
setMap := optionalData[0].(map[string]interface{})
|
||||
|
||||
// 将 map 转换为 JSON 字节切片
|
||||
jsonData, err := json.Marshal(setMap)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("Marshal error:%s", err.Error())
|
||||
return
|
||||
}
|
||||
// 将 JSON 字节切片解析到结构体中
|
||||
err = json.Unmarshal(jsonData, config)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("Unmarshal error:%s", err.Error())
|
||||
return
|
||||
}
|
||||
config := data.GetSettingConfig()
|
||||
//setMap := optionalData[0].(map[string]interface{})
|
||||
//
|
||||
//// 将 map 转换为 JSON 字节切片
|
||||
//jsonData, err := json.Marshal(setMap)
|
||||
//if err != nil {
|
||||
// logger.SugaredLogger.Errorf("Marshal error:%s", err.Error())
|
||||
// return
|
||||
//}
|
||||
//// 将 JSON 字节切片解析到结构体中
|
||||
//err = json.Unmarshal(jsonData, config)
|
||||
//if err != nil {
|
||||
// logger.SugaredLogger.Errorf("Unmarshal error:%s", err.Error())
|
||||
// return
|
||||
//}
|
||||
|
||||
logger.SugaredLogger.Infof("updateSettings config:%+v", config)
|
||||
if config.DarkTheme {
|
||||
|
||||
93
backend/agent/agent.go
Normal file
93
backend/agent/agent.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"go-stock/backend/agent/tools"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/logger"
|
||||
"time"
|
||||
|
||||
"github.com/cloudwego/eino-ext/components/model/ark"
|
||||
"github.com/cloudwego/eino-ext/components/model/deepseek"
|
||||
"github.com/cloudwego/eino-ext/components/model/openai"
|
||||
"github.com/cloudwego/eino/components/model"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/compose"
|
||||
"github.com/cloudwego/eino/flow/agent/react"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
// GetStockAiAgent @Author spark
|
||||
// @Date 2025/8/4 16:17
|
||||
// @Desc
|
||||
// -----------------------------------------------------------------------------------
|
||||
func GetStockAiAgent(ctx *context.Context, aiConfig data.AIConfig) *react.Agent {
|
||||
logger.SugaredLogger.Infof("GetStockAiAgent aiConfig: %v", aiConfig)
|
||||
temperature := float32(aiConfig.Temperature)
|
||||
var toolableChatModel model.ToolCallingChatModel
|
||||
var err error
|
||||
if aiConfig.BaseUrl == "https://ark.cn-beijing.volces.com/api/v3" {
|
||||
toolableChatModel, err = ark.NewChatModel(context.Background(), &ark.ChatModelConfig{
|
||||
BaseURL: aiConfig.BaseUrl,
|
||||
Model: aiConfig.ModelName,
|
||||
APIKey: aiConfig.ApiKey,
|
||||
MaxTokens: &aiConfig.MaxTokens,
|
||||
Temperature: &temperature,
|
||||
})
|
||||
|
||||
} else if aiConfig.BaseUrl == "https://api.deepseek.com" {
|
||||
toolableChatModel, err = deepseek.NewChatModel(*ctx, &deepseek.ChatModelConfig{
|
||||
BaseURL: aiConfig.BaseUrl,
|
||||
Model: aiConfig.ModelName,
|
||||
APIKey: aiConfig.ApiKey,
|
||||
Timeout: time.Duration(aiConfig.TimeOut) * time.Second,
|
||||
MaxTokens: aiConfig.MaxTokens,
|
||||
Temperature: temperature,
|
||||
})
|
||||
|
||||
} else {
|
||||
toolableChatModel, err = openai.NewChatModel(*ctx, &openai.ChatModelConfig{
|
||||
BaseURL: aiConfig.BaseUrl,
|
||||
Model: aiConfig.ModelName,
|
||||
APIKey: aiConfig.ApiKey,
|
||||
Timeout: time.Duration(aiConfig.TimeOut) * time.Second,
|
||||
MaxTokens: &aiConfig.MaxTokens,
|
||||
Temperature: &temperature,
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return nil
|
||||
}
|
||||
// 初始化所需的 tools
|
||||
aiTools := compose.ToolsNodeConfig{
|
||||
Tools: []tool.BaseTool{
|
||||
tools.GetQueryEconomicDataTool(),
|
||||
tools.GetQueryStockPriceInfoTool(),
|
||||
tools.GetQueryStockCodeInfoTool(),
|
||||
tools.GetQueryMarketNewsTool(),
|
||||
tools.GetChoiceStockByIndicatorsTool(),
|
||||
tools.GetStockKLineTool(),
|
||||
tools.GetInteractiveAnswerDataTool(),
|
||||
tools.GetFinancialReportTool(),
|
||||
tools.GetQueryStockNewsTool(),
|
||||
tools.GetIndustryResearchReportTool(),
|
||||
tools.GetQueryBKDictTool(),
|
||||
},
|
||||
}
|
||||
// 创建 agent
|
||||
agent, err := react.NewAgent(*ctx, &react.AgentConfig{
|
||||
ToolCallingModel: toolableChatModel,
|
||||
ToolsConfig: aiTools,
|
||||
MaxStep: len(aiTools.Tools)*1 + 3,
|
||||
MessageModifier: func(ctx context.Context, input []*schema.Message) []*schema.Message {
|
||||
return input
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return nil
|
||||
}
|
||||
return agent
|
||||
}
|
||||
91
backend/agent/agent_api.go
Normal file
91
backend/agent/agent_api.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/cloudwego/eino/compose"
|
||||
"github.com/cloudwego/eino/flow/agent"
|
||||
"github.com/cloudwego/eino/flow/agent/react"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/samber/lo"
|
||||
"go-stock/backend/agent/tool_logger"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/logger"
|
||||
"io"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/8/7 9:07
|
||||
// @Desc
|
||||
// -----------------------------------------------------------------------------------
|
||||
type StockAiAgent struct {
|
||||
*react.Agent
|
||||
}
|
||||
|
||||
func NewStockAiAgentApi() *StockAiAgent {
|
||||
return &StockAiAgent{}
|
||||
}
|
||||
|
||||
func (receiver StockAiAgent) newStockAiAgent(ctx *context.Context, aiConfigId int) *StockAiAgent {
|
||||
settingConfig := data.GetSettingConfig()
|
||||
aiConfig, ok := lo.Find(settingConfig.AiConfigs, func(item *data.AIConfig) bool {
|
||||
return uint(aiConfigId) == item.ID
|
||||
})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &StockAiAgent{
|
||||
Agent: GetStockAiAgent(ctx, *aiConfig),
|
||||
}
|
||||
}
|
||||
|
||||
func (receiver StockAiAgent) Chat(question string, aiConfigId int, sysPromptId *int) chan *schema.Message {
|
||||
ch := make(chan *schema.Message, 512)
|
||||
ctx := context.Background()
|
||||
stockAiAgent := receiver.newStockAiAgent(&ctx, aiConfigId)
|
||||
|
||||
sysPrompt := ""
|
||||
if sysPromptId == nil || *sysPromptId == 0 {
|
||||
sysPrompt = "你现在扮演一位拥有20年实战经验的顶级股票投资大师,精通价值投资、趋势交易、量化分析等多种策略。你擅长结合宏观经济、行业周期和企业基本面进行全方位、精准的多维分析,尤其对A股、港股、美股市场有深刻理解,始终秉持“风险控制第一”的原则,善于用通俗易懂的方式传授投资智慧。"
|
||||
} else {
|
||||
sysPrompt = data.NewPromptTemplateApi().GetPromptTemplateByID(*sysPromptId)
|
||||
}
|
||||
agentOption := []agent.AgentOption{
|
||||
agent.WithComposeOptions(compose.WithCallbacks(&tool_logger.LoggerCallback{MessageChanel: ch})),
|
||||
//react.WithChatModelOptions(ark.WithCache(cacheOption)),
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
sr, err := stockAiAgent.Stream(ctx, []*schema.Message{
|
||||
{
|
||||
Role: schema.System,
|
||||
Content: sysPrompt,
|
||||
},
|
||||
{
|
||||
Role: schema.User,
|
||||
Content: question,
|
||||
},
|
||||
}, agentOption...)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("stream error: %v", err)
|
||||
return
|
||||
}
|
||||
defer sr.Close()
|
||||
for {
|
||||
msg, err := sr.Recv()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
// finish
|
||||
break
|
||||
}
|
||||
// error
|
||||
logger.SugaredLogger.Errorf("failed to recv: %v", err)
|
||||
break
|
||||
}
|
||||
logger.SugaredLogger.Infof("stream: %s", msg.String())
|
||||
ch <- msg
|
||||
}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
84
backend/agent/agent_test.go
Normal file
84
backend/agent/agent_test.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"go-stock/backend/agent/tool_logger"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudwego/eino/compose"
|
||||
"github.com/cloudwego/eino/flow/agent"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/8/4 17:32
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func TestGetStockAiAgent(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db.Init("../../data/stock.db")
|
||||
config := data.GetSettingConfig()
|
||||
aiAgent := GetStockAiAgent(&ctx, *config.AiConfigs[0])
|
||||
|
||||
opt := []agent.AgentOption{
|
||||
agent.WithComposeOptions(compose.WithCallbacks(&tool_logger.LoggerCallback{})),
|
||||
//react.WithChatModelOptions(ark.WithCache(cacheOption)),
|
||||
}
|
||||
|
||||
sr, err := aiAgent.Stream(ctx, []*schema.Message{
|
||||
{
|
||||
Role: schema.System,
|
||||
Content: config.Settings.Prompt + "",
|
||||
},
|
||||
{
|
||||
Role: schema.User,
|
||||
Content: "结合以上提供的宏观经济数据/市场指数行情/国内外市场资讯/电报/会议/事件/投资者关注的问题,\n结合宏观经济,事件驱动,政策支持,投资者关注的问题,分析当前市场情绪和热点 找出有潜力/优质的板块/行业/概念/标的/主题,\n多因子深度分析计算上涨或下跌的逻辑和概率,\n最后按风险和投资周期给出具体推荐标的操作建议",
|
||||
},
|
||||
}, opt...)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("stream error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer sr.Close() // remember to close the stream
|
||||
|
||||
md := strings.Builder{}
|
||||
for {
|
||||
msg, err := sr.Recv()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
// finish
|
||||
break
|
||||
}
|
||||
// error
|
||||
logger.SugaredLogger.Errorf("failed to recv: %v", err)
|
||||
return
|
||||
}
|
||||
//logger.SugaredLogger.Infof("stream recv: %v", msg)
|
||||
if msg.ReasoningContent != "" {
|
||||
md.WriteString(msg.ReasoningContent)
|
||||
}
|
||||
if msg.Content != "" {
|
||||
md.WriteString(msg.Content)
|
||||
}
|
||||
}
|
||||
logger.SugaredLogger.Info(md.String())
|
||||
//logger.SugaredLogger.Infof("stream done:\n%s", md.String())
|
||||
}
|
||||
|
||||
func TestAgent(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
|
||||
ch := NewStockAiAgentApi().Chat("分析一下海立股份,使用工具", 1, nil)
|
||||
for message := range ch {
|
||||
logger.SugaredLogger.Infof("res:%s", message.String())
|
||||
}
|
||||
|
||||
}
|
||||
98
backend/agent/tool_logger/tool_logger.go
Normal file
98
backend/agent/tool_logger/tool_logger.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package tool_logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"go-stock/backend/logger"
|
||||
"io"
|
||||
|
||||
"github.com/cloudwego/eino/callbacks"
|
||||
"github.com/cloudwego/eino/components/model"
|
||||
"github.com/cloudwego/eino/flow/agent/react"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/8/5 10:21
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
type LoggerCallback struct {
|
||||
MessageChanel chan *schema.Message
|
||||
callbacks.HandlerBuilder // 可以用 callbacks.HandlerBuilder 来辅助实现 callback
|
||||
}
|
||||
|
||||
func (cb *LoggerCallback) OnStart(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context {
|
||||
logger.SugaredLogger.Infof("==================")
|
||||
inputStr, _ := json.MarshalIndent(input, "", " ") // nolint: byted_s_returned_err_check
|
||||
logger.SugaredLogger.Infof("[OnStart] %s\n", string(inputStr))
|
||||
|
||||
modelCallbackInput := model.ConvCallbackInput(input)
|
||||
if modelCallbackInput != nil {
|
||||
for _, message := range modelCallbackInput.Messages {
|
||||
cb.MessageChanel <- message
|
||||
}
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (cb *LoggerCallback) OnEnd(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context {
|
||||
logger.SugaredLogger.Infof("=========[OnEnd]=========")
|
||||
outputStr, _ := json.MarshalIndent(output, "", " ") // nolint: byted_s_returned_err_check
|
||||
logger.SugaredLogger.Infof(string(outputStr))
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (cb *LoggerCallback) OnError(ctx context.Context, info *callbacks.RunInfo, err error) context.Context {
|
||||
logger.SugaredLogger.Infof("=========[OnError]=========")
|
||||
logger.SugaredLogger.Infof("%s", err.Error())
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (cb *LoggerCallback) OnEndWithStreamOutput(ctx context.Context, info *callbacks.RunInfo,
|
||||
output *schema.StreamReader[callbacks.CallbackOutput]) context.Context {
|
||||
|
||||
var graphInfoName = react.GraphName
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.SugaredLogger.Infof("[OnEndStream] panic err:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
defer output.Close() // remember to close the stream in defer
|
||||
|
||||
logger.SugaredLogger.Infof("=========[OnEndStream]=========")
|
||||
for {
|
||||
frame, err := output.Recv()
|
||||
if errors.Is(err, io.EOF) {
|
||||
// finish
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Infof("internal error: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
s, err := json.Marshal(frame)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Infof("internal error: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if info.Name == graphInfoName { // 仅打印 graph 的输出, 否则每个 stream 节点的输出都会打印一遍
|
||||
logger.SugaredLogger.Infof("%s: %s\n", info.Name, string(s))
|
||||
}
|
||||
}
|
||||
|
||||
}()
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (cb *LoggerCallback) OnStartWithStreamInput(ctx context.Context, info *callbacks.RunInfo,
|
||||
input *schema.StreamReader[callbacks.CallbackInput]) context.Context {
|
||||
defer input.Close()
|
||||
return ctx
|
||||
}
|
||||
34
backend/agent/tools/bk_dict_tool.go
Normal file
34
backend/agent/tools/bk_dict_tool.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"go-stock/backend/data"
|
||||
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/coocood/freecache"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/9/27 14:09
|
||||
// @Desc
|
||||
// -----------------------------------------------------------------------------------
|
||||
type ToolQueryBKDict struct{}
|
||||
|
||||
func (t ToolQueryBKDict) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: "QueryBKDictInfo",
|
||||
Desc: "获取所有板块/行业名称或者代码(bkCode,bkName)",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t ToolQueryBKDict) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||
resp := data.NewMarketNewsApi().EMDictCode("016", freecache.NewCache(100))
|
||||
bytes, err := json.Marshal(resp)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
func GetQueryBKDictTool() tool.InvokableTool {
|
||||
return &ToolQueryBKDict{}
|
||||
}
|
||||
140
backend/agent/tools/choice_stock_by_indicators_tool.go
Normal file
140
backend/agent/tools/choice_stock_by_indicators_tool.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go-stock/backend/data"
|
||||
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/random"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/8/5 11:17
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func GetChoiceStockByIndicatorsTool() tool.InvokableTool {
|
||||
return &ChoiceStockByIndicators{}
|
||||
}
|
||||
|
||||
type ChoiceStockByIndicators struct {
|
||||
}
|
||||
|
||||
func (c ChoiceStockByIndicators) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: "ChoiceStockByIndicators",
|
||||
Desc: "根据自然语言筛选股票,返回自然语言选股条件要求的股票所有相关数据。输入股票名称可以获取当前股票最新的股价交易数据和基础财务指标信息,多个股票名称使用,分隔。",
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
||||
"words": {
|
||||
Type: "string",
|
||||
Desc: "选股自然语言。" +
|
||||
"例:上海贝岭,macd,rsi,kdj,boll,5日均线,14日均线,30日均线,60日均线,成交量,OBV,EMA" +
|
||||
"例1:创新药,半导体;PE<30;净利润增长率>50%。 " +
|
||||
"例2:上证指数,科创50。 " +
|
||||
"例3:长电科技,上海贝岭。" +
|
||||
"例4:长电科技,上海贝岭;KDJ,MACD,RSI,BOLL,主力净流入/流出" +
|
||||
"例5:换手率大于3%小于25%.量比1以上. 10日内有过涨停.股价处于峰值的二分之一以下.流通股本<100亿.当日和连续四日净流入;股价在20日均线以上.分时图股价在均线之上.热门板块下涨幅领先的A股. 当日量能20000手以上.沪深个股.近一年市盈率波动小于150%.MACD金叉;不要ST股及不要退市股,非北交所,每股收益>0。" +
|
||||
"例6:沪深主板.流通市值小于100亿.市值大于10亿.60分钟dif大于dea.60分钟skdj指标k值大于d值.skdj指标k值小于90.换手率大于3%.成交额大于1亿元.量比大于2.涨幅大于2%小于7%.股价大于5小于50.创业板.10日均线大于20日均线;不要ST股及不要退市股;不要北交所;不要科创板;不要创业板。" +
|
||||
"例7:股价在20日线上,一月之内涨停次数>=1,量比大于1,换手率大于3%,流通市值大于 50亿小于200亿。" +
|
||||
"例8:基本条件:前期有爆量,回调到 10 日线,当日是缩量阴线,均线趋势向上。;优选条件:一月之内涨停次数>=1" +
|
||||
"例9:今日涨幅大于等于2%小于等于9%;量比大于等于1.1小于等于5;换手率大于等于5%小于等于20%;市值大于等于30小于等于300亿;5日、10日、30日、60日均线、5周、10周、30周、60周均线多头排列",
|
||||
Required: true,
|
||||
},
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c ChoiceStockByIndicators) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||
parms := map[string]any{}
|
||||
err := json.Unmarshal([]byte(argumentsInJSON), &parms)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
content := "无符合条件的数据"
|
||||
words := parms["words"].(string)
|
||||
res := data.NewSearchStockApi(words).SearchStock(random.RandInt(5, 20))
|
||||
if convertor.ToString(res["code"]) == "100" {
|
||||
resData := res["data"].(map[string]any)
|
||||
result := resData["result"].(map[string]any)
|
||||
dataList := result["dataList"].([]any)
|
||||
columns := result["columns"].([]any)
|
||||
headers := map[string]string{}
|
||||
for _, v := range columns {
|
||||
//logger.SugaredLogger.Infof("v:%+v", v)
|
||||
d := v.(map[string]any)
|
||||
//logger.SugaredLogger.Infof("key:%s title:%s dateMsg:%s unit:%s", d["key"], d["title"], d["dateMsg"], d["unit"])
|
||||
title := convertor.ToString(d["title"])
|
||||
if convertor.ToString(d["dateMsg"]) != "" {
|
||||
title = title + "[" + convertor.ToString(d["dateMsg"]) + "]"
|
||||
}
|
||||
if convertor.ToString(d["unit"]) != "" {
|
||||
title = title + "(" + convertor.ToString(d["unit"]) + ")"
|
||||
}
|
||||
headers[d["key"].(string)] = title
|
||||
}
|
||||
table := &[]map[string]any{}
|
||||
for _, v := range dataList {
|
||||
d := v.(map[string]any)
|
||||
tmp := map[string]any{}
|
||||
for key, title := range headers {
|
||||
tmp[title] = convertor.ToString(d[key])
|
||||
}
|
||||
*table = append(*table, tmp)
|
||||
}
|
||||
jsonData, _ := json.Marshal(*table)
|
||||
markdownTable, _ := JSONToMarkdownTable(jsonData)
|
||||
//logger.SugaredLogger.Infof("markdownTable=\n%s", markdownTable)
|
||||
content = "\r\n### 工具筛选出的股票数据:\r\n" + markdownTable + "\r\n"
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// JSONToMarkdownTable 将JSON数据转换为Markdown表格
|
||||
func JSONToMarkdownTable(jsonData []byte) (string, error) {
|
||||
var data []map[string]interface{}
|
||||
err := json.Unmarshal(jsonData, &data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// 获取表头
|
||||
headers := []string{}
|
||||
for key := range data[0] {
|
||||
headers = append(headers, key)
|
||||
}
|
||||
|
||||
// 构建表头行
|
||||
headerRow := "|"
|
||||
for _, header := range headers {
|
||||
headerRow += fmt.Sprintf(" %s |", header)
|
||||
}
|
||||
headerRow += "\n"
|
||||
|
||||
// 构建分隔行
|
||||
separatorRow := "|"
|
||||
for range headers {
|
||||
separatorRow += " --- |"
|
||||
}
|
||||
separatorRow += "\n"
|
||||
|
||||
// 构建数据行
|
||||
bodyRows := ""
|
||||
for _, rowData := range data {
|
||||
bodyRow := "|"
|
||||
for _, header := range headers {
|
||||
value := rowData[header]
|
||||
bodyRow += fmt.Sprintf(" %v |", value)
|
||||
}
|
||||
bodyRows += bodyRow + "\n"
|
||||
}
|
||||
|
||||
return headerRow + separatorRow + bodyRows, nil
|
||||
}
|
||||
35
backend/agent/tools/common.go
Normal file
35
backend/agent/tools/common.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/8/5 17:20
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func GetStockCode(dcCode string) string {
|
||||
if strutil.ContainsAny(dcCode, []string{"."}) {
|
||||
sp := strings.Split(dcCode, ".")
|
||||
return strings.ToLower(sp[1] + sp[0])
|
||||
}
|
||||
|
||||
//北京证券交易所 8(83、87、88 等) 创新型中小企业(专精特新为主)
|
||||
//上海证券交易所 6(60、688 等) 大盘蓝筹、科创板(高新技术)
|
||||
//深圳证券交易所 0、3(000、002、30 等) 中小盘、创业板(成长型创新企业)
|
||||
switch dcCode[0:1] {
|
||||
case "8":
|
||||
return "bj" + dcCode
|
||||
case "9":
|
||||
return "bj" + dcCode
|
||||
case "6":
|
||||
return "sh" + dcCode
|
||||
case "0":
|
||||
return "sz" + dcCode
|
||||
case "3":
|
||||
return "sz" + dcCode
|
||||
}
|
||||
return dcCode
|
||||
}
|
||||
79
backend/agent/tools/economic_data_tool.go
Normal file
79
backend/agent/tools/economic_data_tool.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/util"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/8/4 16:38
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func GetQueryEconomicDataTool() tool.InvokableTool {
|
||||
return &ToolQueryEconomicData{}
|
||||
}
|
||||
|
||||
type ToolQueryEconomicData struct {
|
||||
}
|
||||
|
||||
func (t ToolQueryEconomicData) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: "QueryEconomicData",
|
||||
Desc: "查询宏观经济数据(GDP,CPI,PPI,PMI)",
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
||||
"flag": {
|
||||
Type: "string",
|
||||
Desc: "all:宏观经济数据(GDP,CPI,PPI,PMI);GDP:国内生产总值;CPI:居民消费价格指数;PPI:工业品出厂价格指数;PMI:采购经理人指数",
|
||||
Required: false,
|
||||
},
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t ToolQueryEconomicData) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||
parms := map[string]any{}
|
||||
err := json.Unmarshal([]byte(argumentsInJSON), &parms)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var market strings.Builder
|
||||
|
||||
switch parms["flag"].(string) {
|
||||
case "GDP":
|
||||
res := data.NewMarketNewsApi().GetGDP()
|
||||
md := util.MarkdownTableWithTitle("国内生产总值(GDP)", res.GDPResult.Data)
|
||||
market.WriteString(md)
|
||||
case "CPI":
|
||||
res2 := data.NewMarketNewsApi().GetCPI()
|
||||
md2 := util.MarkdownTableWithTitle("居民消费价格指数(CPI)", res2.CPIResult.Data)
|
||||
market.WriteString(md2)
|
||||
case "PPI":
|
||||
res3 := data.NewMarketNewsApi().GetPPI()
|
||||
md3 := util.MarkdownTableWithTitle("工业品出厂价格指数(PPI)", res3.PPIResult.Data)
|
||||
market.WriteString(md3)
|
||||
case "PMI":
|
||||
res4 := data.NewMarketNewsApi().GetPMI()
|
||||
md4 := util.MarkdownTableWithTitle("商品价格指数(PMI)", res4.PMIResult.Data)
|
||||
market.WriteString(md4)
|
||||
default:
|
||||
res := data.NewMarketNewsApi().GetGDP()
|
||||
md := util.MarkdownTableWithTitle("国内生产总值(GDP)", res.GDPResult.Data)
|
||||
market.WriteString(md)
|
||||
res2 := data.NewMarketNewsApi().GetCPI()
|
||||
md2 := util.MarkdownTableWithTitle("居民消费价格指数(CPI)", res2.CPIResult.Data)
|
||||
market.WriteString(md2)
|
||||
res3 := data.NewMarketNewsApi().GetPPI()
|
||||
md3 := util.MarkdownTableWithTitle("工业品出厂价格指数(PPI)", res3.PPIResult.Data)
|
||||
market.WriteString(md3)
|
||||
res4 := data.NewMarketNewsApi().GetPMI()
|
||||
md4 := util.MarkdownTableWithTitle("采购经理人指数(PMI)", res4.PMIResult.Data)
|
||||
market.WriteString(md4)
|
||||
}
|
||||
return market.String(), nil
|
||||
}
|
||||
50
backend/agent/tools/financial_reports_tool.go
Normal file
50
backend/agent/tools/financial_reports_tool.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/tidwall/gjson"
|
||||
"go-stock/backend/data"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/8/5 15:49
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func GetFinancialReportTool() tool.InvokableTool {
|
||||
return &FinancialReportTool{}
|
||||
}
|
||||
|
||||
type FinancialReportTool struct {
|
||||
}
|
||||
|
||||
func (f FinancialReportTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: "GetFinancialReport",
|
||||
Desc: "查询股票财务报表数据",
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
||||
"stockCode": {
|
||||
Type: "string",
|
||||
Desc: "股票代码(A股:sh,sz开头;港股hk开头,美股:us开头)不能批量查询",
|
||||
Required: true,
|
||||
},
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f FinancialReportTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||
stockCode := gjson.Get(argumentsInJSON, "stockCode").String()
|
||||
messages := data.GetFinancialReportsByXUEQIU(GetStockCode(stockCode), 30)
|
||||
if messages == nil || len(*messages) == 0 {
|
||||
return "", fmt.Errorf("没有找到%s的财务报告", stockCode)
|
||||
}
|
||||
md := strings.Builder{}
|
||||
for _, s := range *messages {
|
||||
md.WriteString(s)
|
||||
}
|
||||
return md.String(), nil
|
||||
}
|
||||
69
backend/agent/tools/industry_research_report_tool.go
Normal file
69
backend/agent/tools/industry_research_report_tool.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"go-stock/backend/data"
|
||||
log "go-stock/backend/logger"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/8/9 18:48
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func GetIndustryResearchReportTool() tool.InvokableTool {
|
||||
return &IndustryResearchReportTool{api: data.NewMarketNewsApi()}
|
||||
}
|
||||
|
||||
type IndustryResearchReportTool struct {
|
||||
api *data.MarketNewsApi
|
||||
}
|
||||
|
||||
func (i IndustryResearchReportTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: "GetIndustryResearchReport",
|
||||
Desc: "获取行业/板块研究报告",
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
||||
"name": {
|
||||
Type: "string",
|
||||
Desc: "行业/板块行业名称",
|
||||
Required: false,
|
||||
},
|
||||
"code": {
|
||||
Type: "string",
|
||||
Desc: "行业/板块代码",
|
||||
Required: true,
|
||||
},
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (i IndustryResearchReportTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||
code := gjson.Get(argumentsInJSON, "code").String()
|
||||
code = strutil.ReplaceWithMap(code, map[string]string{
|
||||
"-": "",
|
||||
"_": "",
|
||||
"bk": "",
|
||||
"BK": "",
|
||||
"bk0": "",
|
||||
"BK0": "",
|
||||
})
|
||||
|
||||
log.SugaredLogger.Debugf("code:%s", code)
|
||||
codeStr := convertor.ToString(code)
|
||||
resp := i.api.IndustryResearchReport(codeStr, 7)
|
||||
md := strings.Builder{}
|
||||
for _, a := range resp {
|
||||
data := a.(map[string]any)
|
||||
md.WriteString(i.api.GetIndustryReportInfo(data["infoCode"].(string)))
|
||||
}
|
||||
log.SugaredLogger.Debugf("codeNum:%s IndustryResearchReport:\n %s", code, md.String())
|
||||
return md.String(), nil
|
||||
}
|
||||
64
backend/agent/tools/interactive_answer_data_tool.go
Normal file
64
backend/agent/tools/interactive_answer_data_tool.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/tidwall/gjson"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/util"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/8/5 12:46
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func GetInteractiveAnswerDataTool() tool.InvokableTool {
|
||||
return &InteractiveAnswerDataTool{}
|
||||
}
|
||||
|
||||
type InteractiveAnswerDataTool struct {
|
||||
}
|
||||
|
||||
func (i InteractiveAnswerDataTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: "QueryInteractiveAnswerData",
|
||||
Desc: "获取投资者与上市公司互动问答的数据,反映当前投资者关注的热点问题。",
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
||||
"page": {
|
||||
Type: "string",
|
||||
Desc: "分页号",
|
||||
Required: true,
|
||||
},
|
||||
"pageSize": {
|
||||
Type: "string",
|
||||
Desc: "分页大小",
|
||||
Required: true,
|
||||
},
|
||||
"keyWord": {
|
||||
Type: "string",
|
||||
Desc: "搜索关键词,多个关键词空格隔开(可输入股票名称或者当前热门板块/行业/概念/标的/事件等)",
|
||||
Required: false,
|
||||
},
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (i InteractiveAnswerDataTool) InvokableRun(ctx context.Context, funcArguments string, opts ...tool.Option) (string, error) {
|
||||
page := gjson.Get(funcArguments, "page").String()
|
||||
pageSize := gjson.Get(funcArguments, "pageSize").String()
|
||||
keyWord := gjson.Get(funcArguments, "keyWord").String()
|
||||
pageNo, err := convertor.ToInt(page)
|
||||
if err != nil {
|
||||
pageNo = 1
|
||||
}
|
||||
pageSizeNum, err := convertor.ToInt(pageSize)
|
||||
if err != nil {
|
||||
pageSizeNum = 50
|
||||
}
|
||||
datas := data.NewMarketNewsApi().InteractiveAnswer(int(pageNo), int(pageSizeNum), keyWord)
|
||||
content := util.MarkdownTableWithTitle("投资互动数据", datas.Results)
|
||||
return content, nil
|
||||
}
|
||||
80
backend/agent/tools/market_news_tool.go
Normal file
80
backend/agent/tools/market_news_tool.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/logger"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/duke-git/lancet/v2/random"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/8/4 16:38
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func GetQueryMarketNewsTool() tool.InvokableTool {
|
||||
return &QueryMarketNews{}
|
||||
}
|
||||
|
||||
type QueryMarketNews struct {
|
||||
}
|
||||
|
||||
func (q QueryMarketNews) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: "QueryMarketNews",
|
||||
Desc: "国内外市场资讯/电报/会议/事件",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (q QueryMarketNews) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||
md := strings.Builder{}
|
||||
res := data.NewMarketNewsApi().ClsCalendar()
|
||||
for _, a := range res {
|
||||
bytes, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
//logger.SugaredLogger.Debugf("value: %+v", string(bytes))
|
||||
date := gjson.Get(string(bytes), "calendar_day")
|
||||
md.WriteString("\n### 事件/会议日期:" + date.String())
|
||||
list := gjson.Get(string(bytes), "items")
|
||||
//logger.SugaredLogger.Debugf("value: %+v,list: %+v", date.String(), list)
|
||||
list.ForEach(func(key, value gjson.Result) bool {
|
||||
logger.SugaredLogger.Debugf("key: %+v,value: %+v", key.String(), gjson.Get(value.String(), "title"))
|
||||
md.WriteString("\n- " + gjson.Get(value.String(), "title").String())
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
news := data.NewMarketNewsApi().GetNewsList("", random.RandInt(100, 500))
|
||||
messageText := strings.Builder{}
|
||||
for _, telegraph := range *news {
|
||||
messageText.WriteString("## " + telegraph.Time + ":" + "\n")
|
||||
messageText.WriteString("### " + telegraph.Content + "\n")
|
||||
}
|
||||
md.WriteString("\n### 市场资讯:\n" + messageText.String())
|
||||
|
||||
resp := data.NewMarketNewsApi().TradingViewNews()
|
||||
var newsText strings.Builder
|
||||
for _, a := range *resp {
|
||||
logger.SugaredLogger.Debugf("TradingViewNews: %s", a.Title)
|
||||
newsText.WriteString(a.Title + "\n")
|
||||
}
|
||||
md.WriteString("\n### 全球新闻资讯:\n" + newsText.String())
|
||||
|
||||
reutersNew := data.NewMarketNewsApi().ReutersNew()
|
||||
reutersNewMessageText := strings.Builder{}
|
||||
for _, article := range reutersNew.Result.Articles {
|
||||
reutersNewMessageText.WriteString("## " + article.Title + "\n")
|
||||
reutersNewMessageText.WriteString("### " + article.Description + "\n")
|
||||
}
|
||||
md.WriteString("\n### 外媒全球新闻资讯:\n" + reutersNewMessageText.String())
|
||||
|
||||
return md.String(), nil
|
||||
}
|
||||
49
backend/agent/tools/stock_code_tool.go
Normal file
49
backend/agent/tools/stock_code_tool.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"go-stock/backend/data"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/8/4 18:25
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func GetQueryStockCodeInfoTool() tool.InvokableTool {
|
||||
return &QueryStockCodeInfo{}
|
||||
}
|
||||
|
||||
type QueryStockCodeInfo struct {
|
||||
}
|
||||
|
||||
func (q QueryStockCodeInfo) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: "QueryStockCodeInfo",
|
||||
Desc: "查询股票/指数信息(股票/指数名称,股票/指数代码,股票/指数拼音,股票/指数拼音首字母,股票/指数交易所等",
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
||||
"searchWord": {
|
||||
Type: "string",
|
||||
Desc: "股票搜索关键词",
|
||||
Required: true,
|
||||
},
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (q QueryStockCodeInfo) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||
parms := map[string]any{}
|
||||
err := json.Unmarshal([]byte(argumentsInJSON), &parms)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
stockList := data.NewStockDataApi().GetStockList(parms["searchWord"].(string))
|
||||
marshal, err := json.Marshal(stockList)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(marshal), nil
|
||||
}
|
||||
80
backend/agent/tools/stock_k_line_data_tool.go
Normal file
80
backend/agent/tools/stock_k_line_data_tool.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/tidwall/gjson"
|
||||
"go-stock/backend/data"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/8/5 11:31
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func GetStockKLineTool() tool.InvokableTool {
|
||||
return &QueryStockKLine{}
|
||||
}
|
||||
|
||||
type QueryStockKLine struct {
|
||||
}
|
||||
|
||||
func (q QueryStockKLine) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: "QueryStockKLine",
|
||||
Desc: "获取股票K线数据。输入股票名称和K线周期,返回股票K线数据。",
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
||||
"days": {
|
||||
Type: "string",
|
||||
Desc: "日K数据条数。",
|
||||
Required: true,
|
||||
},
|
||||
"stockCode": {
|
||||
Type: "string",
|
||||
Desc: "股票代码(A股:sh,sz开头;港股hk开头,美股:us开头)",
|
||||
Required: true,
|
||||
},
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (q QueryStockKLine) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||
stockCode := GetStockCode(gjson.Get(argumentsInJSON, "stockCode").String())
|
||||
days := gjson.Get(argumentsInJSON, "days").String()
|
||||
toIntDay, err := convertor.ToInt(days)
|
||||
if err != nil {
|
||||
toIntDay = 90
|
||||
}
|
||||
if strutil.HasPrefixAny(stockCode, []string{"sz", "sh", "hk", "us", "gb_"}) {
|
||||
K := &[]data.KLineData{}
|
||||
if strutil.HasPrefixAny(stockCode, []string{"sz", "sh"}) {
|
||||
K = data.NewStockDataApi().GetKLineData(stockCode, "240", toIntDay)
|
||||
}
|
||||
if strutil.HasPrefixAny(stockCode, []string{"hk", "us", "gb_"}) {
|
||||
K = data.NewStockDataApi().GetHK_KLineData(stockCode, "day", toIntDay)
|
||||
}
|
||||
Kmap := &[]map[string]any{}
|
||||
for _, kline := range *K {
|
||||
mapk := make(map[string]any, 6)
|
||||
mapk["日期"] = kline.Day
|
||||
mapk["开盘价"] = kline.Open
|
||||
mapk["最高价"] = kline.High
|
||||
mapk["最低价"] = kline.Low
|
||||
mapk["收盘价"] = kline.Close
|
||||
Volume, _ := convertor.ToFloat(kline.Volume)
|
||||
mapk["成交量(万手)"] = Volume / 10000.00 / 100.00
|
||||
*Kmap = append(*Kmap, mapk)
|
||||
}
|
||||
jsonData, _ := json.Marshal(Kmap)
|
||||
markdownTable, _ := JSONToMarkdownTable(jsonData)
|
||||
res := "\r\n ### " + stockCode + " " + convertor.ToString(toIntDay) + "日K线数据:\r\n" + markdownTable + "\r\n"
|
||||
return res, nil
|
||||
} else {
|
||||
return "无数据,可能股票代码错误。(A股:sh,sz开头;港股hk开头,美股:us开头)", fmt.Errorf("不支持的股票代码:%s", stockCode)
|
||||
}
|
||||
}
|
||||
42
backend/agent/tools/stock_news_tool.go
Normal file
42
backend/agent/tools/stock_news_tool.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/tidwall/gjson"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/util"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/8/5 16:27
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func GetQueryStockNewsTool() tool.InvokableTool {
|
||||
return &QueryStockNewsTool{}
|
||||
}
|
||||
|
||||
type QueryStockNewsTool struct {
|
||||
}
|
||||
|
||||
func (q QueryStockNewsTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: "QueryStockNewsTool",
|
||||
Desc: "按关键词搜索相关市场资讯/新闻",
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
||||
"searchWords": {
|
||||
Type: "string",
|
||||
Desc: "搜索关键词(多个关键词使用空格分隔)",
|
||||
Required: true,
|
||||
},
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (q QueryStockNewsTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||
searchWords := gjson.Get(argumentsInJSON, "searchWords").String()
|
||||
res := data.NewMarketNewsApi().CailianpressWeb(searchWords)
|
||||
return util.MarkdownTableWithTitle(searchWords+"市场资讯/新闻", res.List), nil
|
||||
}
|
||||
57
backend/agent/tools/stock_price_info_tool.go
Normal file
57
backend/agent/tools/stock_price_info_tool.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"go-stock/backend/data"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/8/4 17:58
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func GetQueryStockPriceInfoTool() tool.InvokableTool {
|
||||
return &ToolQueryStockPriceInfo{}
|
||||
}
|
||||
|
||||
type ToolQueryStockPriceInfo struct{}
|
||||
|
||||
func (t ToolQueryStockPriceInfo) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: "QueryStockPriceInfo",
|
||||
Desc: "批量获取实时股价数据",
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
||||
"stockCodes": {
|
||||
Type: "string",
|
||||
Desc: "股票代码,多个,隔开,股票代码必须转化为sh或者sz或者hk开头的形式,例如:sz399001,sh600859",
|
||||
Required: true,
|
||||
},
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t ToolQueryStockPriceInfo) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||
parms := map[string]any{}
|
||||
err := json.Unmarshal([]byte(argumentsInJSON), &parms)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
stockCodes := strings.Split(parms["stockCodes"].(string), ",")
|
||||
var codes []string
|
||||
for _, code := range stockCodes {
|
||||
codes = append(codes, GetStockCode(code))
|
||||
}
|
||||
realTimeData, err := data.NewStockDataApi().GetStockCodeRealTimeData(codes...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
marshal, err := json.Marshal(realTimeData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(marshal), nil
|
||||
}
|
||||
@@ -34,7 +34,7 @@ func NewAlertWindowsApi(AppID string, Title string, Content string, Icon string)
|
||||
}
|
||||
|
||||
func (a AlertWindowsApi) SendNotification() bool {
|
||||
if GetConfig().LocalPushEnable == false {
|
||||
if GetSettingConfig().LocalPushEnable == false {
|
||||
logger.SugaredLogger.Error("本地推送未开启")
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/go-toast/toast"
|
||||
"go-stock/backend/logger"
|
||||
|
||||
"github.com/go-toast/toast"
|
||||
)
|
||||
|
||||
// AlertWindowsApi @Author spark
|
||||
@@ -31,7 +33,7 @@ func NewAlertWindowsApi(AppID string, Title string, Content string, Icon string)
|
||||
}
|
||||
|
||||
func (a AlertWindowsApi) SendNotification() bool {
|
||||
if GetConfig().LocalPushEnable == false {
|
||||
if GetSettingConfig().LocalPushEnable == false {
|
||||
logger.SugaredLogger.Error("本地推送未开启")
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/go-toast/toast"
|
||||
"go-stock/backend/logger"
|
||||
"testing"
|
||||
|
||||
"github.com/go-toast/toast"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
|
||||
@@ -27,7 +27,7 @@ func (c *CrawlerApi) NewCrawler(ctx context.Context, crawlerBaseInfo CrawlerBase
|
||||
return CrawlerApi{
|
||||
crawlerCtx: ctx,
|
||||
crawlerBaseInfo: crawlerBaseInfo,
|
||||
pool: NewBrowserPool(GetConfig().BrowserPoolSize),
|
||||
pool: NewBrowserPool(GetSettingConfig().BrowserPoolSize),
|
||||
}
|
||||
}
|
||||
func (c *CrawlerApi) GetHtml(url, waitVisible string, headless bool) (string, bool) {
|
||||
@@ -39,7 +39,7 @@ func (c *CrawlerApi) GetHtml(url, waitVisible string, headless bool) (string, bo
|
||||
}
|
||||
func (c *CrawlerApi) GetHtml_old(url, waitVisible string, headless bool) (string, bool) {
|
||||
htmlContent := ""
|
||||
path := GetConfig().BrowserPath
|
||||
path := GetSettingConfig().BrowserPath
|
||||
//logger.SugaredLogger.Infof("Browser path:%s", path)
|
||||
if path != "" {
|
||||
pctx, pcancel := chromedp.NewExecAllocator(
|
||||
@@ -102,7 +102,7 @@ func (c *CrawlerApi) GetHtml_old(url, waitVisible string, headless bool) (string
|
||||
|
||||
func (c *CrawlerApi) GetHtmlWithNoCancel(url, waitVisible string, headless bool) (html string, ok bool, parent context.CancelFunc, child context.CancelFunc) {
|
||||
htmlContent := ""
|
||||
path := GetConfig().BrowserPath
|
||||
path := GetSettingConfig().BrowserPath
|
||||
//logger.SugaredLogger.Infof("BrowserPath :%s", path)
|
||||
var parentCancel context.CancelFunc
|
||||
var childCancel context.CancelFunc
|
||||
@@ -170,7 +170,7 @@ func (c *CrawlerApi) GetHtmlWithActions(actions *[]chromedp.Action, headless boo
|
||||
htmlContent := ""
|
||||
*actions = append(*actions, chromedp.InnerHTML("body", &htmlContent))
|
||||
|
||||
path := GetConfig().BrowserPath
|
||||
path := GetSettingConfig().BrowserPath
|
||||
//logger.SugaredLogger.Infof("GetHtmlWithActions path:%s", path)
|
||||
if path != "" {
|
||||
pctx, pcancel := chromedp.NewExecAllocator(
|
||||
|
||||
1
backend/data/data/dict/README.md
Normal file
1
backend/data/data/dict/README.md
Normal file
@@ -0,0 +1 @@
|
||||
Some dict/zh data is from [github.com/fxsjy/jieba](https://github.com/fxsjy/jieba)
|
||||
428
backend/data/data/dict/base.txt
Normal file
428
backend/data/data/dict/base.txt
Normal file
@@ -0,0 +1,428 @@
|
||||
# 金融股票全场景分词字典(最终去重优化版)
|
||||
# 格式:单词 权重 词性 | 权重280-350分,核心术语优先匹配,无重复词汇
|
||||
|
||||
# 一、净买卖与资金流向(核心交易表述)
|
||||
净卖出 340 v
|
||||
净买入 340 v
|
||||
净卖出额 330 n
|
||||
净买入额 330 n
|
||||
净卖出量 330 n
|
||||
净买入量 330 n
|
||||
资金净流出 340 n
|
||||
资金净流入 340 n
|
||||
净额 330 n
|
||||
买卖净额 330 n
|
||||
资金净额 330 n
|
||||
北向资金净买入 330 n
|
||||
北向资金净卖出 330 n
|
||||
南向资金净买入 320 n
|
||||
南向资金净卖出 320 n
|
||||
主力资金净买入 330 n
|
||||
主力资金净卖出 330 n
|
||||
散户资金净买入 320 n
|
||||
散户资金净卖出 320 n
|
||||
机构资金净买入 330 n
|
||||
机构资金净卖出 330 n
|
||||
游资净买入 320 n
|
||||
游资净卖出 320 n
|
||||
大单净买入 320 n
|
||||
大单净卖出 320 n
|
||||
中单净买入 320 n
|
||||
中单净卖出 320 n
|
||||
小单净买入 320 n
|
||||
小单净卖出 320 n
|
||||
净买入占比 320 n
|
||||
净卖出占比 320 n
|
||||
净买入率 320 n
|
||||
净卖出率 320 n
|
||||
连续净买入 320 v
|
||||
连续净卖出 320 v
|
||||
单日净买入 320 n
|
||||
单日净卖出 320 n
|
||||
累计净买入 320 n
|
||||
累计净卖出 320 n
|
||||
净买入创纪录 310 adj
|
||||
净卖出创纪录 310 adj
|
||||
净买入放量 310 v
|
||||
净卖出放量 310 v
|
||||
净买入缩量 310 v
|
||||
净卖出缩量 310 v
|
||||
净多 310 n
|
||||
净空 310 n
|
||||
净多头 310 n
|
||||
净空头 310 n
|
||||
净多头头寸 310 n
|
||||
净空头头寸 310 n
|
||||
跌超 310 n
|
||||
跌逾 310 n
|
||||
|
||||
# 二、金融资讯与市场分析
|
||||
金融资讯 350 n
|
||||
市场快讯 340 n
|
||||
财经新闻 340 n
|
||||
政策解读 330 n
|
||||
市场分析 330 n
|
||||
行业研报 320 n
|
||||
宏观经济 330 n
|
||||
微观层面 310 n
|
||||
基本面 320 n
|
||||
技术面 320 n
|
||||
资金面 320 n
|
||||
政策面 320 n
|
||||
市场情绪 320 n
|
||||
风险偏好 310 n
|
||||
流动性 320 n
|
||||
估值修复 310 n
|
||||
价值投资 310 n
|
||||
趋势投资 310 n
|
||||
波段操作 310 n
|
||||
左侧交易 290 n
|
||||
右侧交易 290 n
|
||||
止损止盈 300 n
|
||||
仓位管理 300 n
|
||||
资产配置 310 n
|
||||
分散投资 290 n
|
||||
集中投资 290 n
|
||||
风险控制 310 n
|
||||
系统性风险 300 n
|
||||
非系统性风险 290 n
|
||||
黑天鹅事件 310 n
|
||||
灰犀牛事件 300 n
|
||||
熔断机制 300 n
|
||||
市场监管 310 n
|
||||
信息披露 310 n
|
||||
内幕交易 300 n
|
||||
操纵市场 300 n
|
||||
亏损 100 n
|
||||
加工 100 n
|
||||
|
||||
# 三、全球主要股指(含中英文缩写)
|
||||
# 中国市场
|
||||
A股 350 n
|
||||
港股 350 n
|
||||
上证指数 350 n
|
||||
深证成指 350 n
|
||||
创业板指 340 n
|
||||
科创板指 330 n
|
||||
北证50 330 n
|
||||
沪深300 350 n
|
||||
沪深300指数 350 n
|
||||
中证500 340 n
|
||||
中证500指数 340 n
|
||||
中证1000 330 n
|
||||
中证1000指数 330 n
|
||||
上证50 340 n
|
||||
上证50指数 340 n
|
||||
科创50 330 n
|
||||
科创50指数 330 n
|
||||
上证综指 350 n
|
||||
富时中国A50指数 340 n
|
||||
恒生指数 340 n
|
||||
恒生科技指数 340 n
|
||||
恒生国企指数 330 n
|
||||
H股指数 330 n
|
||||
# 美洲市场
|
||||
道琼斯工业平均指数 350 n
|
||||
标普500指数 350 n
|
||||
纳斯达克综合指数 340 n
|
||||
纳斯达克100指数 340 n
|
||||
罗素2000指数 320 n
|
||||
标普400中型股指数 310 n
|
||||
标普600小型股指数 310 n
|
||||
纽约证交所综合指数 310 n
|
||||
纳斯达克中国金龙指数 310 n
|
||||
# 欧洲市场
|
||||
德国DAX指数 330 n
|
||||
法国CAC40指数 330 n
|
||||
富时100指数 330 n
|
||||
欧元斯托克50指数 320 n
|
||||
英国富时250指数 310 n
|
||||
意大利富时MIB指数 310 n
|
||||
西班牙IBEX 35指数 310 n
|
||||
# 亚太其他市场
|
||||
日经225指数 330 n
|
||||
日经500指数 310 n
|
||||
韩国综合股价指数 320 n
|
||||
韩国kospi指数 320 n
|
||||
KOSPI 310 n
|
||||
澳洲标普200指数 310 n
|
||||
印度孟买敏感指数 310 n
|
||||
Sensex 300 n
|
||||
印度Nifty 50指数 310 n
|
||||
# 全球综合指数
|
||||
MSCI指数 320 n
|
||||
MSCI全球指数 330 n
|
||||
MSCI新兴市场指数 330 n
|
||||
富时罗素全球指数 320 n
|
||||
摩根大通全球债券指数 310 n
|
||||
全球股指 300 n
|
||||
发达市场指数 300 n
|
||||
新兴市场指数 300 n
|
||||
金砖国家指数 300 n
|
||||
G20国家指数 300 n
|
||||
# 股指衍生工具
|
||||
指数期货 320 n
|
||||
股指期货 320 n
|
||||
富时中国A50指数期货 320 n
|
||||
沪深300股指期货 320 n
|
||||
标普500股指期货 320 n
|
||||
纳斯达克100股指期货 310 n
|
||||
指数成分股 320 n
|
||||
指数权重股 320 n
|
||||
指数涨幅 320 n
|
||||
指数跌幅 320 n
|
||||
指数反弹 310 n
|
||||
指数回调 310 n
|
||||
指数创新高 310 v
|
||||
指数创新低 310 v
|
||||
指数估值 310 n
|
||||
指数市盈率 310 n
|
||||
|
||||
# 四、财务与估值核心指标
|
||||
市盈率 350 n
|
||||
PE 350 n
|
||||
动态市盈率 340 n
|
||||
静态市盈率 340 n
|
||||
滚动市盈率 340 n
|
||||
市净率 350 n
|
||||
PB 350 n
|
||||
市销率 330 n
|
||||
PS 330 n
|
||||
市现率 320 n
|
||||
PCF 320 n
|
||||
净资产收益率 350 n
|
||||
ROE 350 n
|
||||
总资产收益率 330 n
|
||||
ROA 330 n
|
||||
毛利率 340 n
|
||||
净利率 340 n
|
||||
销售净利率 330 n
|
||||
资产负债率 340 n
|
||||
营收 340 n
|
||||
营业收入 340 n
|
||||
净利润 350 n
|
||||
归母净利润 340 n
|
||||
扣非净利润 340 n
|
||||
EPS 330 n
|
||||
每股收益 330 n
|
||||
现金流 340 n
|
||||
经营活动现金流 330 n
|
||||
自由现金流 330 n
|
||||
营收增长率 330 n
|
||||
净利润增长率 330 n
|
||||
股息率 320 n
|
||||
分红率 320 n
|
||||
换手率 330 n
|
||||
成交量 340 n
|
||||
成交额 340 n
|
||||
量比 320 n
|
||||
振幅 320 n
|
||||
|
||||
# 五、政策与宏观经济
|
||||
货币政策 330 n
|
||||
财政政策 330 n
|
||||
稳健货币政策 320 n
|
||||
积极财政政策 320 n
|
||||
宽松政策 320 n
|
||||
紧缩政策 320 n
|
||||
利率 330 n
|
||||
基准利率 320 n
|
||||
LPR 330 n
|
||||
贷款市场报价利率 320 n
|
||||
存款准备金率 320 n
|
||||
MLF 320 n
|
||||
中期借贷便利 310 n
|
||||
逆回购 320 n
|
||||
正回购 310 n
|
||||
汇率 330 n
|
||||
人民币汇率 330 n
|
||||
美元汇率 320 n
|
||||
通胀 320 n
|
||||
CPI 330 n
|
||||
PPI 330 n
|
||||
GDP 330 n
|
||||
国内生产总值 320 n
|
||||
PMI 330 n
|
||||
采购经理人指数 320 n
|
||||
行业政策 320 n
|
||||
产业政策 320 n
|
||||
税收政策 310 n
|
||||
补贴政策 310 n
|
||||
关税 310 n
|
||||
贸易政策 310 n
|
||||
地缘政治 310 n
|
||||
大宗商品 320 n
|
||||
原油价格 310 n
|
||||
黄金价格 310 n
|
||||
有色金属价格 300 n
|
||||
|
||||
# 六、金融产品与机构
|
||||
股票 320 n
|
||||
基金 320 n
|
||||
公募基金 310 n
|
||||
私募基金 310 n
|
||||
ETF 320 n
|
||||
指数基金 310 n
|
||||
混合型基金 300 n
|
||||
股票型基金 310 n
|
||||
债券型基金 300 n
|
||||
货币基金 290 n
|
||||
REITs 310 n
|
||||
可转债 310 n
|
||||
可交换债 300 n
|
||||
期货 310 n
|
||||
股指期货 310 n
|
||||
国债期货 300 n
|
||||
商品期货 300 n
|
||||
期权 300 n
|
||||
融资融券 310 n
|
||||
两融余额 300 n
|
||||
北向资金 320 n
|
||||
南向资金 310 n
|
||||
沪股通 310 n
|
||||
深股通 310 n
|
||||
陆股通 310 n
|
||||
证券公司 310 n
|
||||
券商 320 n
|
||||
基金公司 300 n
|
||||
保险公司 300 n
|
||||
银行 310 n
|
||||
监管机构 310 n
|
||||
证监会 320 n
|
||||
交易所 320 n
|
||||
上交所 320 n
|
||||
深交所 320 n
|
||||
北交所 310 n
|
||||
港交所 310 n
|
||||
社保基金 310 n
|
||||
养老金 300 n
|
||||
QFII 300 n
|
||||
RQFII 290 n
|
||||
北向资金机构 300 n
|
||||
|
||||
# 七、热点概念与行业
|
||||
AI 330 n
|
||||
人工智能 350 n
|
||||
算力 330 n
|
||||
大数据 320 n
|
||||
云计算 320 n
|
||||
半导体 350 n
|
||||
芯片 350 n
|
||||
集成电路 340 n
|
||||
新能源 350 n
|
||||
光伏 340 n
|
||||
锂电 320 n
|
||||
储能 340 n
|
||||
充电桩 310 n
|
||||
新能源车 320 n
|
||||
智能汽车 310 n
|
||||
自动驾驶 330 n
|
||||
军工 310 n
|
||||
国防军工 300 n
|
||||
医药 310 n
|
||||
创新药 310 n
|
||||
医疗器械 300 n
|
||||
CXO 300 n
|
||||
白酒 310 n
|
||||
消费 320 n
|
||||
可选消费 300 n
|
||||
必选消费 300 n
|
||||
食品饮料 310 n
|
||||
家电 300 n
|
||||
地产 300 n
|
||||
房地产 300 n
|
||||
基建 300 n
|
||||
新基建 310 n
|
||||
数字经济 350 n
|
||||
数字货币 310 n
|
||||
区块链 300 n
|
||||
元宇宙 300 n
|
||||
低空经济 340 n
|
||||
人形机器人 330 n
|
||||
工业互联网 330 n
|
||||
物联网 300 n
|
||||
5G 300 n
|
||||
6G 340 n
|
||||
|
||||
# 八、交易操作与行情
|
||||
上涨 310 v
|
||||
下跌 310 v
|
||||
涨停 310 v
|
||||
跌停 310 v
|
||||
反弹 300 v
|
||||
反转 300 v
|
||||
回调 300 v
|
||||
横盘 290 v
|
||||
震荡 290 v
|
||||
跳水 300 v
|
||||
拉升 300 v
|
||||
砸盘 300 v
|
||||
护盘 290 v
|
||||
建仓 300 v
|
||||
加仓 300 v
|
||||
减仓 300 v
|
||||
清仓 300 v
|
||||
平仓 300 v
|
||||
抄底 300 v
|
||||
逃顶 300 v
|
||||
追涨 290 v
|
||||
杀跌 290 v
|
||||
套牢 280 v
|
||||
解套 280 v
|
||||
净流入 300 n
|
||||
净流出 300 n
|
||||
主力资金 300 n
|
||||
资金流入 290 v
|
||||
资金流出 290 v
|
||||
放量 290 v
|
||||
缩量 290 v
|
||||
高换手 290 n
|
||||
低换手 280 n
|
||||
高估值 290 n
|
||||
低估值 290 n
|
||||
超预期 300 v
|
||||
不及预期 300 v
|
||||
符合预期 290 v
|
||||
利好 310 n
|
||||
利空 310 n
|
||||
政策利好 310 n
|
||||
业绩利好 310 n
|
||||
风险警示 300 n
|
||||
涨停板 300 n
|
||||
跌停板 300 n
|
||||
一字涨停 290 n
|
||||
一字跌停 290 n
|
||||
打开涨停 320 v
|
||||
打开跌停 320 v
|
||||
集合竞价 290 n
|
||||
连续竞价 280 n
|
||||
开盘价 340 n
|
||||
收盘价 340 n
|
||||
最高价 330 n
|
||||
最低价 330 n
|
||||
均价 330 n
|
||||
昨日收盘价 320 n
|
||||
涨跌额 330 n
|
||||
涨跌幅 340 n
|
||||
涨幅 340 n
|
||||
跌幅 340 n
|
||||
涨停价 330 n
|
||||
跌停价 330 n
|
||||
熔断 330 n
|
||||
临时停牌 320 n
|
||||
复牌 320 v
|
||||
停牌 320 n
|
||||
量价齐升 320 n
|
||||
量价背离 320 n
|
||||
高开 320 n
|
||||
低开 320 n
|
||||
平开 320 n
|
||||
高走 320 v
|
||||
低走 320 v
|
||||
震荡上行 320 v
|
||||
震荡下行 320 v
|
||||
|
||||
# 九、委托交易与规则
|
||||
限价委托 340 n
|
||||
市价委托 340 n
|
||||
止损委托 330 n
|
||||
0
backend/data/data/dict/en/dict.txt
Normal file
0
backend/data/data/dict/en/dict.txt
Normal file
1
backend/data/data/dict/jp/README.md
Normal file
1
backend/data/data/dict/jp/README.md
Normal file
@@ -0,0 +1 @@
|
||||
dict.txt 通过内部工具生成, Copyright 2017 ego authors. 商用和拷贝请注明来源和版权
|
||||
885298
backend/data/data/dict/jp/dict.txt
Normal file
885298
backend/data/data/dict/jp/dict.txt
Normal file
File diff suppressed because it is too large
Load Diff
185
backend/data/data/dict/user.txt
Normal file
185
backend/data/data/dict/user.txt
Normal file
@@ -0,0 +1,185 @@
|
||||
# 补充:热点概念与板块(Jieba/gse兼容格式)
|
||||
# 权重说明:核心热点500-700分,事件类400分,负权重词汇按需求保留
|
||||
|
||||
# 一、负权重低优先级词汇(减少无差别匹配干扰)
|
||||
公司 -0.1 n
|
||||
国家 -0.1 n
|
||||
国际 -0.1 n
|
||||
会议 -0.1 n
|
||||
市场 -0.1 n
|
||||
经济 -0.1 n
|
||||
技术 -0.1 n
|
||||
记者 -0.1 n
|
||||
时间 -0.1 n
|
||||
项目 -0.1 n
|
||||
问题 -0.1 n
|
||||
企业 -0.1 n
|
||||
财联社 -0.1 n
|
||||
上涨 -0.1 v
|
||||
下跌 -0.1 v
|
||||
期货 -0.1 n
|
||||
跌幅 -0.1 n
|
||||
跌超 -0.1 adj
|
||||
股票 -0.1 n
|
||||
基金 -0.1 n
|
||||
电讯 -0.1 n
|
||||
建筑 -0.1 n
|
||||
平开 -0.1 n
|
||||
保险 -0.1 n
|
||||
行业 -0.1 n
|
||||
其他 -0.1 n
|
||||
|
||||
# 二、核心热点概念(700分,最高优先级)
|
||||
比特币 700 n
|
||||
摩尔线程 700 n
|
||||
摩尔线程概念 700 n
|
||||
AI算力 700 n
|
||||
生成式AI 700 n
|
||||
量子计算 700 n
|
||||
脑机接口 700 n
|
||||
6G通信 700 n
|
||||
人形机器人 700 n
|
||||
固态电池 700 n
|
||||
ChatGPT概念 700 n
|
||||
Web3.0 700 n
|
||||
元宇宙 700 n
|
||||
数字孪生 700 n
|
||||
量子通信 700 n
|
||||
|
||||
# 三、重点赛道板块(500分,高优先级)
|
||||
冰雪旅游 500 n
|
||||
特高压 500 n
|
||||
跨境电商 500 n
|
||||
新能源汽车 500 n
|
||||
机器人 500 n
|
||||
具身智能 500 n
|
||||
油气 500 n
|
||||
商业航天 500 n
|
||||
光伏储能 500 n
|
||||
锂电材料 500 n
|
||||
半导体设备 500 n
|
||||
集成电路 500 n
|
||||
创新药 500 n
|
||||
CXO 500 n
|
||||
医疗器械 500 n
|
||||
数字经济 500 n
|
||||
数字货币 500 n
|
||||
区块链 500 n
|
||||
低空经济 500 n
|
||||
工业互联网 500 n
|
||||
物联网 500 n
|
||||
5G应用 500 n
|
||||
充电桩 500 n
|
||||
氢能源 500 n
|
||||
核聚变 500 n
|
||||
工业母机 500 n
|
||||
新材料 500 n
|
||||
生物制造 500 n
|
||||
智能网联汽车 500 n
|
||||
乡村振兴 500 n
|
||||
国企改革 500 n
|
||||
央企重组 500 n
|
||||
跨境金融 500 n
|
||||
自贸港 500 n
|
||||
一带一路 500 n
|
||||
绿色低碳 500 n
|
||||
碳交易 500 n
|
||||
数据要素 500 n
|
||||
数字基建 500 n
|
||||
东数西算 500 n
|
||||
国产替代 500 n
|
||||
信创 500 n
|
||||
网络安全 500 n
|
||||
算力网络 500 n
|
||||
边缘计算 500 n
|
||||
虚拟现实 500 n
|
||||
增强现实 500 n
|
||||
智能穿戴 500 n
|
||||
智能家居 500 n
|
||||
车联网 500 n
|
||||
激光雷达 500 n
|
||||
氮化镓 500 n
|
||||
碳化硅 500 n
|
||||
第三代半导体 500 n
|
||||
EDA工具 500 n
|
||||
光刻胶 500 n
|
||||
芯片设计 500 n
|
||||
封装测试 500 n
|
||||
储能电池 500 n
|
||||
钠离子电池 500 n
|
||||
氢燃料电池 500 n
|
||||
光伏组件 500 n
|
||||
风电设备 500 n
|
||||
特高压设备 500 n
|
||||
电力物联网 500 n
|
||||
智能电网 500 n
|
||||
轨道交通 500 n
|
||||
航空航天 500 n
|
||||
海洋工程 500 n
|
||||
高端装备 500 n
|
||||
军工电子 500 n
|
||||
卫星互联网 500 n
|
||||
北斗导航 500 n
|
||||
国产大飞机 500 n
|
||||
生物医药 500 n
|
||||
基因测序 500 n
|
||||
疫苗 500 n
|
||||
医疗美容 500 n
|
||||
养老产业 500 n
|
||||
教育信息化 500 n
|
||||
体育产业 500 n
|
||||
文化创意 500 n
|
||||
旅游复苏 500 n
|
||||
预制菜 500 n
|
||||
白酒 500 n
|
||||
食品饮料 500 n
|
||||
家电下乡 500 n
|
||||
房地产复苏 500 n
|
||||
基建投资 500 n
|
||||
新型城镇化 500 n
|
||||
冷链物流 500 n
|
||||
快递物流 500 n
|
||||
跨境支付 500 n
|
||||
金融科技 500 n
|
||||
消费电子 500 n
|
||||
元宇宙基建 500 n
|
||||
数字藏品 500 n
|
||||
NFT 500 n
|
||||
绿色电力 500 n
|
||||
节能降碳 500 n
|
||||
抽水蓄能 500 n
|
||||
生物质能 500 n
|
||||
地热能 500 n
|
||||
潮汐能 500 n
|
||||
|
||||
# 四、事件驱动型概念(400分,中优先级)
|
||||
俄乌冲突 400 n
|
||||
中东局势 400 n
|
||||
美联储加息 400 n
|
||||
降息预期 400 n
|
||||
贸易摩擦 400 n
|
||||
供应链重构 400 n
|
||||
能源危机 400 n
|
||||
粮食安全 400 n
|
||||
疫情复苏 400 n
|
||||
政策利好 400 n
|
||||
产业扶持 400 n
|
||||
技术突破 400 n
|
||||
并购重组 400 n
|
||||
IPO提速 400 n
|
||||
解禁潮 400 n
|
||||
北向资金流入 400 n
|
||||
南向资金流入 400 n
|
||||
主力资金异动 400 n
|
||||
行业景气度 400 n
|
||||
业绩预增 400 n
|
||||
商誉减值 400 n
|
||||
退市风险 400 n
|
||||
监管新规 400 n
|
||||
税收优惠 400 n
|
||||
补贴政策 400 n
|
||||
基建刺激 400 n
|
||||
消费刺激 400 n
|
||||
新能源补贴 400 n
|
||||
碳达峰政策 400 n
|
||||
碳中和目标 400 n
|
||||
270132
backend/data/data/dict/zh/idf.txt
Normal file
270132
backend/data/data/dict/zh/idf.txt
Normal file
File diff suppressed because it is too large
Load Diff
352279
backend/data/data/dict/zh/s_1.txt
Normal file
352279
backend/data/data/dict/zh/s_1.txt
Normal file
File diff suppressed because it is too large
Load Diff
1161
backend/data/data/dict/zh/stop_tokens.txt
Normal file
1161
backend/data/data/dict/zh/stop_tokens.txt
Normal file
File diff suppressed because it is too large
Load Diff
88
backend/data/data/dict/zh/stop_word.txt
Normal file
88
backend/data/data/dict/zh/stop_word.txt
Normal file
@@ -0,0 +1,88 @@
|
||||
,
|
||||
.
|
||||
?
|
||||
!
|
||||
"
|
||||
@
|
||||
,
|
||||
。
|
||||
、
|
||||
?
|
||||
!
|
||||
:
|
||||
“
|
||||
”
|
||||
;
|
||||
|
||||
(
|
||||
)
|
||||
《
|
||||
》
|
||||
~
|
||||
*
|
||||
<
|
||||
>
|
||||
/
|
||||
\
|
||||
|
|
||||
-
|
||||
_
|
||||
+
|
||||
=
|
||||
&
|
||||
^
|
||||
%
|
||||
#
|
||||
`
|
||||
;
|
||||
$
|
||||
¥
|
||||
‘
|
||||
’
|
||||
〉
|
||||
〈
|
||||
…
|
||||
>
|
||||
<
|
||||
@
|
||||
#
|
||||
$
|
||||
%
|
||||
︿
|
||||
&
|
||||
*
|
||||
+
|
||||
~
|
||||
|
|
||||
[
|
||||
]
|
||||
{
|
||||
}
|
||||
啊
|
||||
阿
|
||||
哎
|
||||
哎呀
|
||||
哎哟
|
||||
唉
|
||||
俺
|
||||
俺们
|
||||
按
|
||||
按照
|
||||
吧
|
||||
吧哒
|
||||
把
|
||||
罢了
|
||||
被
|
||||
本
|
||||
本着
|
||||
比
|
||||
比方
|
||||
比如
|
||||
鄙人
|
||||
彼
|
||||
彼此
|
||||
边
|
||||
别
|
||||
别的
|
||||
别说
|
||||
并
|
||||
236754
backend/data/data/dict/zh/t_1.txt
Normal file
236754
backend/data/data/dict/zh/t_1.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,7 @@ func NewDingDingAPI() *DingDingAPI {
|
||||
}
|
||||
|
||||
func (DingDingAPI) SendDingDingMessage(message string) string {
|
||||
if GetConfig().DingPushEnable == false {
|
||||
if GetSettingConfig().DingPushEnable == false {
|
||||
//logger.SugaredLogger.Info("钉钉推送未开启")
|
||||
return "钉钉推送未开启"
|
||||
}
|
||||
@@ -37,11 +37,9 @@ func (DingDingAPI) SendDingDingMessage(message string) string {
|
||||
logger.SugaredLogger.Infof("send dingding message: %s", resp.String())
|
||||
return "发送钉钉消息成功"
|
||||
}
|
||||
func GetConfig() *Settings {
|
||||
return NewSettingsApi(&Settings{}).GetConfig()
|
||||
}
|
||||
|
||||
func getApiURL() string {
|
||||
return GetConfig().DingRobot
|
||||
return GetSettingConfig().DingRobot
|
||||
}
|
||||
|
||||
func (DingDingAPI) SendToDingDing(title, message string) string {
|
||||
|
||||
@@ -20,13 +20,13 @@ import (
|
||||
|
||||
type FundApi struct {
|
||||
client *resty.Client
|
||||
config *Settings
|
||||
config *SettingConfig
|
||||
}
|
||||
|
||||
func NewFundApi() *FundApi {
|
||||
return &FundApi{
|
||||
client: resty.New(),
|
||||
config: GetConfig(),
|
||||
config: GetSettingConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,15 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"go-stock/backend/util"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/coocood/freecache"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
@@ -12,13 +21,6 @@ import (
|
||||
"github.com/robertkrimen/otto"
|
||||
"github.com/samber/lo"
|
||||
"github.com/tidwall/gjson"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"go-stock/backend/util"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
@@ -32,12 +34,77 @@ func NewMarketNewsApi() *MarketNewsApi {
|
||||
return &MarketNewsApi{}
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) TelegraphList(crawlTimeOut int64) *[]models.Telegraph {
|
||||
//https://www.cls.cn/nodeapi/telegraphList
|
||||
url := "https://www.cls.cn/nodeapi/telegraphList"
|
||||
res := map[string]any{}
|
||||
_, _ = resty.New().SetTimeout(time.Duration(crawlTimeOut)*time.Second).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").
|
||||
SetResult(&res).
|
||||
Get(url)
|
||||
var telegraphs []models.Telegraph
|
||||
|
||||
if v, _ := convertor.ToInt(res["error"]); v == 0 {
|
||||
if res["data"] == nil {
|
||||
return m.GetNewTelegraph(30)
|
||||
}
|
||||
data := res["data"].(map[string]any)
|
||||
rollData := data["roll_data"].([]any)
|
||||
for _, v := range rollData {
|
||||
news := v.(map[string]any)
|
||||
ctime, _ := convertor.ToInt(news["ctime"])
|
||||
dataTime := time.Unix(ctime, 0).Local()
|
||||
logger.SugaredLogger.Debugf("dataTime: %s", dataTime)
|
||||
telegraph := models.Telegraph{
|
||||
Title: news["title"].(string),
|
||||
Content: news["content"].(string),
|
||||
Time: dataTime.Format("15:04:05"),
|
||||
DataTime: &dataTime,
|
||||
Url: news["shareurl"].(string),
|
||||
Source: "财联社电报",
|
||||
IsRed: (news["level"].(string)) != "C",
|
||||
SentimentResult: AnalyzeSentiment(news["content"].(string)).Description,
|
||||
}
|
||||
cnt := int64(0)
|
||||
db.Dao.Model(telegraph).Where("time=? and content=?", telegraph.Time, telegraph.Content).Count(&cnt)
|
||||
if cnt > 0 {
|
||||
continue
|
||||
}
|
||||
telegraphs = append(telegraphs, telegraph)
|
||||
db.Dao.Model(&models.Telegraph{}).Create(&telegraph)
|
||||
logger.SugaredLogger.Debugf("telegraph: %+v", &telegraph)
|
||||
if news["subjects"] == nil {
|
||||
continue
|
||||
}
|
||||
subjects := news["subjects"].([]any)
|
||||
for _, subject := range subjects {
|
||||
name := subject.(map[string]any)["subject_name"].(string)
|
||||
tag := &models.Tags{
|
||||
Name: name,
|
||||
Type: "subject",
|
||||
}
|
||||
db.Dao.Model(tag).Where("name=? and type=?", name, "subject").FirstOrCreate(&tag)
|
||||
db.Dao.Model(models.TelegraphTags{}).Where("telegraph_id=? and tag_id=?", telegraph.ID, tag.ID).FirstOrCreate(&models.TelegraphTags{
|
||||
TelegraphId: telegraph.ID,
|
||||
TagId: tag.ID,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
//db.Dao.Model(&models.Telegraph{}).Create(&telegraphs)
|
||||
//logger.SugaredLogger.Debugf("telegraphs: %+v", &telegraphs)
|
||||
}
|
||||
|
||||
return &telegraphs
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) GetNewTelegraph(crawlTimeOut int64) *[]models.Telegraph {
|
||||
url := "https://www.cls.cn/telegraph"
|
||||
response, _ := resty.New().SetTimeout(time.Duration(crawlTimeOut)*time.Second).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))
|
||||
Get(url)
|
||||
var telegraphs []models.Telegraph
|
||||
//logger.SugaredLogger.Info(string(response.Body()))
|
||||
document, _ := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
|
||||
@@ -76,7 +143,7 @@ func (m MarketNewsApi) GetNewTelegraph(crawlTimeOut int64) *[]models.Telegraph {
|
||||
if telegraph.Content != "" {
|
||||
telegraph.SentimentResult = AnalyzeSentiment(telegraph.Content).Description
|
||||
cnt := int64(0)
|
||||
db.Dao.Model(telegraph).Where("time=? and source=?", telegraph.Time, telegraph.Source).Count(&cnt)
|
||||
db.Dao.Model(telegraph).Where("time=? and content=?", telegraph.Time, telegraph.Content).Count(&cnt)
|
||||
if cnt == 0 {
|
||||
db.Dao.Create(&telegraph)
|
||||
telegraphs = append(telegraphs, telegraph)
|
||||
@@ -99,9 +166,9 @@ func (m MarketNewsApi) GetNewTelegraph(crawlTimeOut int64) *[]models.Telegraph {
|
||||
func (m MarketNewsApi) GetNewsList(source string, limit int) *[]*models.Telegraph {
|
||||
news := &[]*models.Telegraph{}
|
||||
if source != "" {
|
||||
db.Dao.Model(news).Preload("TelegraphTags").Where("source=?", source).Order("id desc").Limit(limit).Find(news)
|
||||
db.Dao.Model(news).Preload("TelegraphTags").Where("source=?", source).Order("data_time desc,time desc").Limit(limit).Find(news)
|
||||
} else {
|
||||
db.Dao.Model(news).Preload("TelegraphTags").Order("id desc").Limit(limit).Find(news)
|
||||
db.Dao.Model(news).Preload("TelegraphTags").Order("data_time desc,time desc").Limit(limit).Find(news)
|
||||
}
|
||||
for _, item := range *news {
|
||||
tags := &[]models.Tags{}
|
||||
@@ -116,12 +183,34 @@ func (m MarketNewsApi) GetNewsList(source string, limit int) *[]*models.Telegrap
|
||||
}
|
||||
return news
|
||||
}
|
||||
func (m MarketNewsApi) GetNewsList2(source string, limit int) *[]*models.Telegraph {
|
||||
NewMarketNewsApi().TelegraphList(30)
|
||||
news := &[]*models.Telegraph{}
|
||||
if source != "" {
|
||||
db.Dao.Model(news).Preload("TelegraphTags").Where("source=?", source).Order("data_time desc,is_red desc").Limit(limit).Find(news)
|
||||
} else {
|
||||
db.Dao.Model(news).Preload("TelegraphTags").Order("data_time desc,is_red desc").Limit(limit).Find(news)
|
||||
}
|
||||
for _, item := range *news {
|
||||
tags := &[]models.Tags{}
|
||||
db.Dao.Model(&models.Tags{}).Where("id in ?", lo.Map(item.TelegraphTags, func(item models.TelegraphTags, index int) uint {
|
||||
return item.TagId
|
||||
})).Find(&tags)
|
||||
tagNames := lo.Map(*tags, func(item models.Tags, index int) string {
|
||||
return item.Name
|
||||
})
|
||||
item.SubjectTags = tagNames
|
||||
logger.SugaredLogger.Infof("tagNames %v ,SubjectTags:%s", tagNames, item.SubjectTags)
|
||||
}
|
||||
return news
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) GetTelegraphList(source string) *[]*models.Telegraph {
|
||||
news := &[]*models.Telegraph{}
|
||||
if source != "" {
|
||||
db.Dao.Model(news).Preload("TelegraphTags").Where("source=?", source).Order("id desc").Limit(20).Find(news)
|
||||
db.Dao.Model(news).Preload("TelegraphTags").Where("source=?", source).Order("data_time desc,time desc").Limit(50).Find(news)
|
||||
} else {
|
||||
db.Dao.Model(news).Preload("TelegraphTags").Order("id desc").Limit(20).Find(news)
|
||||
db.Dao.Model(news).Preload("TelegraphTags").Order("data_time desc,time desc").Limit(50).Find(news)
|
||||
}
|
||||
for _, item := range *news {
|
||||
tags := &[]models.Tags{}
|
||||
@@ -177,7 +266,12 @@ func (m MarketNewsApi) GetSinaNews(crawlTimeOut uint) *[]models.Telegraph {
|
||||
data := item.(map[string]any)
|
||||
//logger.SugaredLogger.Infof("%s:%s", data["create_time"], data["rich_text"])
|
||||
telegraph.Content = data["rich_text"].(string)
|
||||
telegraph.Title = strutil.SubInBetween(data["rich_text"].(string), "【", "】")
|
||||
telegraph.Time = strings.Split(data["create_time"].(string), " ")[1]
|
||||
dataTime, _ := time.ParseInLocation("2006-01-02 15:04:05", data["create_time"].(string), time.Local)
|
||||
if &dataTime != nil {
|
||||
telegraph.DataTime = &dataTime
|
||||
}
|
||||
tags := data["tag"].([]any)
|
||||
telegraph.SubjectTags = lo.Map(tags, func(tagItem any, index int) string {
|
||||
name := tagItem.(map[string]any)["name"].(string)
|
||||
@@ -196,7 +290,7 @@ func (m MarketNewsApi) GetSinaNews(crawlTimeOut uint) *[]models.Telegraph {
|
||||
if telegraph.Content != "" {
|
||||
telegraph.SentimentResult = AnalyzeSentiment(telegraph.Content).Description
|
||||
cnt := int64(0)
|
||||
db.Dao.Model(telegraph).Where("time=? and source=?", telegraph.Time, telegraph.Source).Count(&cnt)
|
||||
db.Dao.Model(telegraph).Where("content=? and source=?", telegraph.Content, telegraph.Source).Count(&cnt)
|
||||
if cnt == 0 {
|
||||
db.Dao.Create(&telegraph)
|
||||
telegraphs = append(telegraphs, telegraph)
|
||||
@@ -550,10 +644,19 @@ func (m MarketNewsApi) EMDictCode(code string, cache *freecache.Cache) []any {
|
||||
return respMap["data"].([]any)
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) TradingViewNews() *[]models.TVNews {
|
||||
func (m MarketNewsApi) TradingViewNews() *[]models.Telegraph {
|
||||
client := resty.New()
|
||||
config := GetSettingConfig()
|
||||
if config.HttpProxyEnabled && config.HttpProxy != "" {
|
||||
client.SetProxy(config.HttpProxy)
|
||||
}
|
||||
TVNews := &[]models.TVNews{}
|
||||
url := "https://news-mediator.tradingview.com/news-flow/v2/news?filter=lang:zh-Hans&filter=provider:panews,reuters&client=screener&streaming=false"
|
||||
resp, err := resty.New().SetProxy("http://127.0.0.1:10809").SetTimeout(time.Duration(30)*time.Second).R().
|
||||
news := &[]models.Telegraph{}
|
||||
// url := "https://news-mediator.tradingview.com/news-flow/v2/news?filter=lang:zh-Hans&filter=area:WLD&client=screener&streaming=false"
|
||||
//url := "https://news-mediator.tradingview.com/news-flow/v2/news?filter=area%3AWLD&filter=lang%3Azh-Hans&client=screener&streaming=false"
|
||||
url := "https://news-mediator.tradingview.com/news-flow/v2/news?filter=lang%3Azh-Hans&client=screener&streaming=false"
|
||||
|
||||
resp, err := client.SetTimeout(time.Duration(15)*time.Second).R().
|
||||
SetHeader("Host", "news-mediator.tradingview.com").
|
||||
SetHeader("Origin", "https://cn.tradingview.com").
|
||||
SetHeader("Referer", "https://cn.tradingview.com/").
|
||||
@@ -561,19 +664,81 @@ func (m MarketNewsApi) TradingViewNews() *[]models.TVNews {
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("TradingViewNews err:%s", err.Error())
|
||||
return TVNews
|
||||
return news
|
||||
}
|
||||
respMap := map[string]any{}
|
||||
err = json.Unmarshal(resp.Body(), &respMap)
|
||||
if err != nil {
|
||||
return TVNews
|
||||
return news
|
||||
}
|
||||
items, err := json.Marshal(respMap["items"])
|
||||
if err != nil {
|
||||
return TVNews
|
||||
return news
|
||||
}
|
||||
json.Unmarshal(items, TVNews)
|
||||
return TVNews
|
||||
|
||||
for i, a := range *TVNews {
|
||||
if i > 10 {
|
||||
break
|
||||
}
|
||||
detail := NewMarketNewsApi().TradingViewNewsDetail(a.Id)
|
||||
dataTime := time.Unix(int64(a.Published), 0).Local()
|
||||
description := ""
|
||||
sentimentResult := ""
|
||||
if detail != nil {
|
||||
description = detail.ShortDescription
|
||||
sentimentResult = AnalyzeSentiment(description).Description
|
||||
}
|
||||
if a.Title == "" {
|
||||
continue
|
||||
}
|
||||
telegraph := &models.Telegraph{
|
||||
Title: a.Title,
|
||||
Content: description,
|
||||
DataTime: &dataTime,
|
||||
IsRed: false,
|
||||
Time: dataTime.Format("15:04:05"),
|
||||
Source: "外媒",
|
||||
Url: fmt.Sprintf("https://cn.tradingview.com/news/%s", a.Id),
|
||||
SentimentResult: sentimentResult,
|
||||
}
|
||||
cnt := int64(0)
|
||||
db.Dao.Model(telegraph).Where("time=? and title=? and source=?", telegraph.Time, telegraph.Title, "外媒").Count(&cnt)
|
||||
if cnt > 0 {
|
||||
continue
|
||||
}
|
||||
db.Dao.Model(&models.Telegraph{}).Where("time=? and title=? and source=?", telegraph.Time, telegraph.Title, "外媒").FirstOrCreate(&telegraph)
|
||||
*news = append(*news, *telegraph)
|
||||
}
|
||||
return news
|
||||
}
|
||||
func (m MarketNewsApi) TradingViewNewsDetail(id string) *models.TVNewsDetail {
|
||||
//https://news-headlines.tradingview.com/v3/story?id=panews%3A9be7cf057e3f9%3A0&lang=zh-Hans
|
||||
newsDetail := &models.TVNewsDetail{}
|
||||
newsUrl := fmt.Sprintf("https://news-headlines.tradingview.com/v3/story?id=%s&lang=zh-Hans", url.QueryEscape(id))
|
||||
|
||||
client := resty.New()
|
||||
config := GetSettingConfig()
|
||||
if config.HttpProxyEnabled && config.HttpProxy != "" {
|
||||
client.SetProxy(config.HttpProxy)
|
||||
}
|
||||
request := client.SetTimeout(time.Duration(3) * time.Second).R()
|
||||
_, err := request.
|
||||
SetHeader("Host", "news-headlines.tradingview.com").
|
||||
SetHeader("Origin", "https://cn.tradingview.com").
|
||||
SetHeader("Referer", "https://cn.tradingview.com/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0) Gecko/20100101 Firefox/146.0").
|
||||
//SetHeader("TE", "trailers").
|
||||
//SetHeader("Priority", "u=4").
|
||||
//SetHeader("Connection", "keep-alive").
|
||||
SetResult(newsDetail).
|
||||
Get(newsUrl)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("TradingViewNewsDetail err:%s", err.Error())
|
||||
return newsDetail
|
||||
}
|
||||
logger.SugaredLogger.Infof("resp:%+v", newsDetail)
|
||||
return newsDetail
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) XUEQIUHotStock(size int, marketType string) *[]models.HotItem {
|
||||
@@ -749,27 +914,27 @@ func (m MarketNewsApi) GetCPI() *models.CPIResp {
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("GDP err:%s", err.Error())
|
||||
logger.SugaredLogger.Errorf("GetCPI err:%s", err.Error())
|
||||
return res
|
||||
}
|
||||
body := resp.Body()
|
||||
logger.SugaredLogger.Debugf("GDP:%s", body)
|
||||
logger.SugaredLogger.Debugf("GetCPI:%s", body)
|
||||
vm := otto.New()
|
||||
vm.Run("function data(res){return res};")
|
||||
|
||||
val, err := vm.Run(body)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("GDP err:%s", err.Error())
|
||||
logger.SugaredLogger.Errorf("GetCPI err:%s", err.Error())
|
||||
return res
|
||||
}
|
||||
data, _ := val.Object().Value().Export()
|
||||
logger.SugaredLogger.Infof("GDP:%v", data)
|
||||
logger.SugaredLogger.Infof("GetCPI:%v", data)
|
||||
marshal, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return res
|
||||
}
|
||||
json.Unmarshal(marshal, &res)
|
||||
logger.SugaredLogger.Infof("GDP:%+v", res)
|
||||
logger.SugaredLogger.Infof("GetCPI:%+v", res)
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -784,7 +949,7 @@ func (m MarketNewsApi) GetPPI() *models.PPIResp {
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("GDP err:%s", err.Error())
|
||||
logger.SugaredLogger.Errorf("GetPPI err:%s", err.Error())
|
||||
return res
|
||||
}
|
||||
body := resp.Body()
|
||||
@@ -833,7 +998,7 @@ func (m MarketNewsApi) GetPMI() *models.PMIResp {
|
||||
return res
|
||||
|
||||
}
|
||||
func (m MarketNewsApi) GetIndustryReportInfo(infoCode string) {
|
||||
func (m MarketNewsApi) GetIndustryReportInfo(infoCode string) string {
|
||||
url := "https://data.eastmoney.com/report/zw_industry.jshtml?infocode=" + infoCode
|
||||
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
|
||||
SetHeader("Host", "data.eastmoney.com").
|
||||
@@ -843,7 +1008,7 @@ func (m MarketNewsApi) GetIndustryReportInfo(infoCode string) {
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("GetIndustryReportInfo err:%s", err.Error())
|
||||
return
|
||||
return ""
|
||||
}
|
||||
body := resp.Body()
|
||||
//logger.SugaredLogger.Debugf("GetIndustryReportInfo:%s", body)
|
||||
@@ -853,7 +1018,113 @@ func (m MarketNewsApi) GetIndustryReportInfo(infoCode string) {
|
||||
//logger.SugaredLogger.Infof("GetIndustryReportInfo:\n%s\n%s", title, content)
|
||||
markdown, err := util.HTMLToMarkdown(title + content)
|
||||
if err != nil {
|
||||
return
|
||||
return ""
|
||||
}
|
||||
logger.SugaredLogger.Infof("GetIndustryReportInfo markdown:\n%s", markdown)
|
||||
return markdown
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) ReutersNew() *models.ReutersNews {
|
||||
client := resty.New()
|
||||
config := GetSettingConfig()
|
||||
if config.HttpProxyEnabled && config.HttpProxy != "" {
|
||||
client.SetProxy(config.HttpProxy)
|
||||
}
|
||||
news := &models.ReutersNews{}
|
||||
//url := "https://www.reuters.com/pf/api/v3/content/fetch/articles-by-section-alias-or-id-v1?query={\"arc-site\":\"reuters\",\"fetch_type\":\"collection\",\"offset\":0,\"section_id\":\"/world/\",\"size\":9,\"uri\":\"/world/\",\"website\":\"reuters\"}&d=300&mxId=00000000&_website=reuters"
|
||||
url := "https://www.reuters.com/pf/api/v3/content/fetch/recent-stories-by-sections-v1?query=%7B%22section_ids%22%3A%22%2Fworld%2F%22%2C%22size%22%3A4%2C%22website%22%3A%22reuters%22%7D&d=334&mxId=00000000&_website=reuters"
|
||||
_, err := client.SetTimeout(time.Duration(5)*time.Second).R().
|
||||
SetHeader("Host", "www.reuters.com").
|
||||
SetHeader("Origin", "https://www.reuters.com").
|
||||
SetHeader("Referer", "https://www.reuters.com/world/china/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
SetResult(news).
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("ReutersNew err:%s", err.Error())
|
||||
return news
|
||||
}
|
||||
logger.SugaredLogger.Infof("Articles:%+v", news.Result.Articles)
|
||||
return news
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) InteractiveAnswer(page int, pageSize int, keyWord string) *models.InteractiveAnswer {
|
||||
client := resty.New()
|
||||
config := GetSettingConfig()
|
||||
if config.HttpProxyEnabled && config.HttpProxy != "" {
|
||||
client.SetProxy(config.HttpProxy)
|
||||
}
|
||||
url := fmt.Sprintf("https://irm.cninfo.com.cn/newircs/index/search?_t=%d", time.Now().Unix())
|
||||
answers := &models.InteractiveAnswer{}
|
||||
logger.SugaredLogger.Infof("请求url:%s", url)
|
||||
resp, err := client.SetTimeout(time.Duration(5)*time.Second).R().
|
||||
SetHeader("Host", "irm.cninfo.com.cn").
|
||||
SetHeader("Origin", "https://irm.cninfo.com.cn").
|
||||
SetHeader("Referer", "https://irm.cninfo.com.cn/views/interactiveAnswer").
|
||||
SetHeader("handleError", "true").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0").
|
||||
SetFormData(map[string]string{
|
||||
"pageNo": convertor.ToString(page),
|
||||
"pageSize": convertor.ToString(pageSize),
|
||||
"searchTypes": "11",
|
||||
"highLight": "true",
|
||||
"keyWord": keyWord,
|
||||
}).
|
||||
SetResult(answers).
|
||||
Post(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("InteractiveAnswer-err:%+v", err)
|
||||
}
|
||||
logger.SugaredLogger.Debugf("InteractiveAnswer-resp:%s", resp.Body())
|
||||
return answers
|
||||
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) CailianpressWeb(searchWords string) *models.CailianpressWeb {
|
||||
res := &models.CailianpressWeb{}
|
||||
_, err := resty.New().SetTimeout(time.Second*10).R().
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetHeader("Host", "www.cls.cn").
|
||||
SetHeader("Origin", "https://www.cls.cn").
|
||||
SetHeader("Referer", "https://www.cls.cn/telegraph").
|
||||
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").
|
||||
SetBody(fmt.Sprintf(`{"app":"CailianpressWeb","os":"web","sv":"8.4.6","category":"","keyword":"%s"}`, searchWords)).
|
||||
SetResult(res).
|
||||
Post("https://www.cls.cn/api/csw?app=CailianpressWeb&os=web&sv=8.4.6&sign=9f8797a1f4de66c2370f7a03990d2737")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
logger.SugaredLogger.Debug(res)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) GetNews24HoursList(source string, limit int) *[]*models.Telegraph {
|
||||
news := &[]*models.Telegraph{}
|
||||
if source != "" {
|
||||
db.Dao.Model(news).Preload("TelegraphTags").Where("source=? and created_at>?", source, time.Now().Add(-24*time.Hour)).Order("data_time desc,is_red desc").Limit(limit).Find(news)
|
||||
} else {
|
||||
db.Dao.Model(news).Preload("TelegraphTags").Where("created_at>?", time.Now().Add(-24*time.Hour)).Order("data_time desc,is_red desc").Limit(limit).Find(news)
|
||||
}
|
||||
// 内容去重
|
||||
uniqueNews := make([]*models.Telegraph, 0)
|
||||
seenContent := make(map[string]bool)
|
||||
for _, item := range *news {
|
||||
tags := &[]models.Tags{}
|
||||
db.Dao.Model(&models.Tags{}).Where("id in ?", lo.Map(item.TelegraphTags, func(item models.TelegraphTags, index int) uint {
|
||||
return item.TagId
|
||||
})).Find(&tags)
|
||||
tagNames := lo.Map(*tags, func(item models.Tags, index int) string {
|
||||
return item.Name
|
||||
})
|
||||
item.SubjectTags = tagNames
|
||||
//logger.SugaredLogger.Infof("tagNames %v ,SubjectTags:%s", tagNames, item.SubjectTags)
|
||||
// 使用内容作为去重键值,可以考虑只使用内容的前几个字符或哈希值
|
||||
contentKey := strings.TrimSpace(item.Content)
|
||||
if contentKey != "" && !seenContent[contentKey] {
|
||||
seenContent[contentKey] = true
|
||||
uniqueNews = append(uniqueNews, item)
|
||||
}
|
||||
}
|
||||
return &uniqueNews
|
||||
}
|
||||
|
||||
@@ -2,13 +2,19 @@ package data
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/coocood/freecache"
|
||||
"github.com/tidwall/gjson"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"go-stock/backend/util"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/coocood/freecache"
|
||||
"github.com/duke-git/lancet/v2/random"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
@@ -18,7 +24,12 @@ import (
|
||||
|
||||
func TestGetSinaNews(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
NewMarketNewsApi().GetSinaNews(30)
|
||||
InitAnalyzeSentiment()
|
||||
news := NewMarketNewsApi().GetSinaNews(30)
|
||||
for i, telegraph := range *news {
|
||||
logger.SugaredLogger.Debugf("key: %+v, value: %+v", i, telegraph)
|
||||
|
||||
}
|
||||
//NewMarketNewsApi().GetNewTelegraph(30)
|
||||
|
||||
}
|
||||
@@ -36,7 +47,6 @@ func TestGetIndustryRank(t *testing.T) {
|
||||
res := NewMarketNewsApi().GetIndustryRank("0", 10)
|
||||
for s, a := range res["data"].([]any) {
|
||||
logger.SugaredLogger.Debugf("key: %+v, value: %+v", s, a)
|
||||
|
||||
}
|
||||
}
|
||||
func TestGetIndustryMoneyRankSina(t *testing.T) {
|
||||
@@ -71,19 +81,24 @@ func TestLongTiger(t *testing.T) {
|
||||
|
||||
func TestStockResearchReport(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
resp := NewMarketNewsApi().StockResearchReport("600584.sh", 7)
|
||||
resp := NewMarketNewsApi().StockResearchReport("688082", 7)
|
||||
for _, a := range resp {
|
||||
logger.SugaredLogger.Debugf("value: %+v", a)
|
||||
data := a.(map[string]any)
|
||||
logger.SugaredLogger.Debugf("value: %s infoCode:%s", data["title"], data["infoCode"])
|
||||
NewMarketNewsApi().GetIndustryReportInfo(data["infoCode"].(string))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndustryResearchReport(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
resp := NewMarketNewsApi().IndustryResearchReport("", 7)
|
||||
resp := NewMarketNewsApi().IndustryResearchReport("456", 7)
|
||||
for _, a := range resp {
|
||||
logger.SugaredLogger.Debugf("value: %+v", a)
|
||||
data := a.(map[string]any)
|
||||
logger.SugaredLogger.Debugf("value: %s infoCode:%s", data["title"], data["infoCode"])
|
||||
NewMarketNewsApi().GetIndustryReportInfo(data["infoCode"].(string))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestStockNotice(t *testing.T) {
|
||||
@@ -101,15 +116,22 @@ func TestEMDictCode(t *testing.T) {
|
||||
for _, a := range resp {
|
||||
logger.SugaredLogger.Debugf("value: %+v", a)
|
||||
}
|
||||
bytes, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
dict := &[]models.BKDict{}
|
||||
json.Unmarshal(bytes, dict)
|
||||
logger.SugaredLogger.Debugf("value: %s", string(bytes))
|
||||
md := util.MarkdownTableWithTitle("行业/板块代码", dict)
|
||||
logger.SugaredLogger.Debugf(md)
|
||||
|
||||
}
|
||||
|
||||
func TestTradingViewNews(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
resp := NewMarketNewsApi().TradingViewNews()
|
||||
for _, a := range *resp {
|
||||
logger.SugaredLogger.Debugf("value: %+v", a)
|
||||
}
|
||||
InitAnalyzeSentiment()
|
||||
NewMarketNewsApi().TradingViewNews()
|
||||
}
|
||||
|
||||
func TestXUEQIUHotStock(t *testing.T) {
|
||||
@@ -204,3 +226,68 @@ func TestGetPMI(t *testing.T) {
|
||||
func TestGetIndustryReportInfo(t *testing.T) {
|
||||
NewMarketNewsApi().GetIndustryReportInfo("AP202507151709216483")
|
||||
}
|
||||
|
||||
func TestReutersNew(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
NewMarketNewsApi().ReutersNew()
|
||||
}
|
||||
|
||||
func TestInteractiveAnswer(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
datas := NewMarketNewsApi().InteractiveAnswer(1, 100, "")
|
||||
logger.SugaredLogger.Debugf("PageSize:%d", datas.PageSize)
|
||||
md := util.MarkdownTableWithTitle("投资互动", datas.Results)
|
||||
logger.SugaredLogger.Debugf(md)
|
||||
|
||||
}
|
||||
func TestGetNewsList2(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
news := NewMarketNewsApi().GetNewsList2("财联社电报", random.RandInt(100, 500))
|
||||
messageText := strings.Builder{}
|
||||
for _, telegraph := range *news {
|
||||
messageText.WriteString("## " + telegraph.Time + ":" + "\n")
|
||||
messageText.WriteString("### " + telegraph.Content + "\n")
|
||||
}
|
||||
logger.SugaredLogger.Debugf("value: %s", messageText.String())
|
||||
}
|
||||
|
||||
func TestTelegraphList(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
InitAnalyzeSentiment()
|
||||
NewMarketNewsApi().TelegraphList(30)
|
||||
}
|
||||
|
||||
func TestProxy(t *testing.T) {
|
||||
response, err := resty.New().
|
||||
SetProxy("http://go-stock:778d4ff2-73f3-4d56-b3c3-d9a730a06ae3@stock.sparkmemory.top:8888").
|
||||
R().
|
||||
SetHeader("Host", "news-mediator.tradingview.com").
|
||||
SetHeader("Origin", "https://cn.tradingview.com").
|
||||
SetHeader("Referer", "https://cn.tradingview.com/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
//Get("https://api.ipify.org")
|
||||
Get("https://news-mediator.tradingview.com/news-flow/v2/news?filter=lang%3Azh-Hans&client=screener&streaming=false&user_prostatus=non_pro")
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err)
|
||||
return
|
||||
}
|
||||
logger.SugaredLogger.Debugf("value: %s", response.String())
|
||||
|
||||
}
|
||||
|
||||
func TestNtfy(t *testing.T) {
|
||||
|
||||
//attach := "http://go-stock.sparkmemory.top/%E5%88%86%E6%9E%90%E6%8A%A5%E5%91%8A/%E8%B5%84%E9%87%91%E6%B5%81%E5%90%91/2025-12/AI%EF%BC%9A%E5%B8%82%E5%9C%BA%E5%88%86%E6%9E%90%E6%8A%A5%E5%91%8A-[2025.12.11_12.02.01].html"
|
||||
//post, err := resty.New().SetBaseURL("https://go-stock.sparkmemory.top:16667").R().
|
||||
// SetHeader("Filename", "AI:市场分析报告-[2025.12.11_12.02.01].html").
|
||||
// SetHeader("Icon", "https://go-stock.sparkmemory.top/appicon.png").
|
||||
// SetHeader("Attach", attach).
|
||||
// SetBody("AI:市场分析报告-[2025.12.11_12.02.01]").Post("/go-stock")
|
||||
//if err != nil {
|
||||
// logger.SugaredLogger.Error(err)
|
||||
// return
|
||||
//}
|
||||
//logger.SugaredLogger.Debugf("value: %s", post.String())
|
||||
logger.SugaredLogger.Debugf("value: %s", filepath.Base("https://go-stock.sparkmemory.top/%E5%88%86%E6%9E%90%E6%8A%A5%E5%91%8A/2025/12/11/%E5%B8%82%E5%9C%BA%E8%B5%84%E8%AE%AF[%E5%B8%82%E5%9C%BA%E8%B5%84%E8%AE%AF]-(2025-12-11)AI%E5%88%86%E6%9E%90%E7%BB%93%E6%9E%9C_20251211131509.html"))
|
||||
logger.SugaredLogger.Debugf("value: %s", strutil.After("/data/go-stock-site/docs/分析报告/2025/12/09/市场资讯[市场资讯]-(2025-12-09)AI分析结果.md", "/data/go-stock-site/docs/"))
|
||||
}
|
||||
|
||||
@@ -6,14 +6,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/chromedp/chromedp"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/random"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
@@ -21,6 +13,16 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/chromedp/chromedp"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/random"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/samber/lo"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
@@ -47,33 +49,41 @@ func (o OpenAi) String() string {
|
||||
o.BaseUrl, o.Model, o.MaxTokens, o.Temperature, o.Prompt, o.TimeOut, o.QuestionTemplate, o.CrawlTimeOut, o.KDays, o.BrowserPath)
|
||||
}
|
||||
|
||||
func NewDeepSeekOpenAi(ctx context.Context) *OpenAi {
|
||||
config := GetConfig()
|
||||
if config.OpenAiEnable {
|
||||
if config.OpenAiApiTimeOut <= 0 {
|
||||
config.OpenAiApiTimeOut = 60 * 5
|
||||
func NewDeepSeekOpenAi(ctx context.Context, aiConfigId int) *OpenAi {
|
||||
settingConfig := GetSettingConfig()
|
||||
aiConfig, find := lo.Find(settingConfig.AiConfigs, func(item *AIConfig) bool {
|
||||
return uint(aiConfigId) == item.ID
|
||||
})
|
||||
if !find {
|
||||
aiConfig = &AIConfig{}
|
||||
}
|
||||
|
||||
if settingConfig.OpenAiEnable {
|
||||
if aiConfig.TimeOut <= 0 {
|
||||
aiConfig.TimeOut = 60 * 5
|
||||
}
|
||||
if config.CrawlTimeOut <= 0 {
|
||||
config.CrawlTimeOut = 60
|
||||
if settingConfig.CrawlTimeOut <= 0 {
|
||||
settingConfig.CrawlTimeOut = 60
|
||||
}
|
||||
if config.KDays < 30 {
|
||||
config.KDays = 120
|
||||
if settingConfig.KDays < 30 {
|
||||
settingConfig.KDays = 120
|
||||
}
|
||||
}
|
||||
return &OpenAi{
|
||||
o := &OpenAi{
|
||||
ctx: ctx,
|
||||
BaseUrl: config.OpenAiBaseUrl,
|
||||
ApiKey: config.OpenAiApiKey,
|
||||
Model: config.OpenAiModelName,
|
||||
MaxTokens: config.OpenAiMaxTokens,
|
||||
Temperature: config.OpenAiTemperature,
|
||||
Prompt: config.Prompt,
|
||||
TimeOut: config.OpenAiApiTimeOut,
|
||||
QuestionTemplate: config.QuestionTemplate,
|
||||
CrawlTimeOut: config.CrawlTimeOut,
|
||||
KDays: config.KDays,
|
||||
BrowserPath: config.BrowserPath,
|
||||
BaseUrl: aiConfig.BaseUrl,
|
||||
ApiKey: aiConfig.ApiKey,
|
||||
Model: aiConfig.ModelName,
|
||||
MaxTokens: aiConfig.MaxTokens,
|
||||
Temperature: aiConfig.Temperature,
|
||||
TimeOut: aiConfig.TimeOut,
|
||||
Prompt: settingConfig.Prompt,
|
||||
QuestionTemplate: settingConfig.QuestionTemplate,
|
||||
CrawlTimeOut: settingConfig.CrawlTimeOut,
|
||||
KDays: settingConfig.KDays,
|
||||
BrowserPath: settingConfig.BrowserPath,
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
type THSTokenResponse struct {
|
||||
@@ -135,7 +145,7 @@ type ToolFunction struct {
|
||||
Parameters FunctionParameters `json:"parameters"`
|
||||
}
|
||||
|
||||
func (o OpenAi) NewSummaryStockNewsStreamWithTools(userQuestion string, sysPromptId *int, tools []Tool) <-chan map[string]any {
|
||||
func (o *OpenAi) NewSummaryStockNewsStreamWithTools(userQuestion string, sysPromptId *int, tools []Tool) <-chan map[string]any {
|
||||
ch := make(chan map[string]any, 512)
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
@@ -179,7 +189,21 @@ func (o OpenAi) NewSummaryStockNewsStreamWithTools(userQuestion string, sysPromp
|
||||
"content": "当前本地时间是:" + time.Now().Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(3)
|
||||
wg.Add(6)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
datas := NewMarketNewsApi().InteractiveAnswer(1, 100, "")
|
||||
content := util.MarkdownTableWithTitle("当前最新投资者互动数据", datas.Results)
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": "投资者互动数据",
|
||||
})
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": content,
|
||||
})
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
@@ -210,17 +234,24 @@ func (o OpenAi) NewSummaryStockNewsStreamWithTools(userQuestion string, sysPromp
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
var market strings.Builder
|
||||
market.WriteString(getZSInfo("创业板指数", "sz399006", 30) + "\n")
|
||||
market.WriteString(getZSInfo("上证综合指数", "sh000001", 30) + "\n")
|
||||
market.WriteString(getZSInfo("沪深300指数", "sh000300", 30) + "\n")
|
||||
market.WriteString(GetZSInfo("上证指数", "sh000001", 30) + "\n")
|
||||
market.WriteString(GetZSInfo("深证成指", "sz399001", 30) + "\n")
|
||||
market.WriteString(GetZSInfo("创业板指数", "sz399006", 30) + "\n")
|
||||
market.WriteString(GetZSInfo("科创50", "sh000688", 30) + "\n")
|
||||
market.WriteString(GetZSInfo("沪深300指数", "sh000300", 30) + "\n")
|
||||
market.WriteString(GetZSInfo("中证银行", "sz399986", 30) + "\n")
|
||||
market.WriteString(GetZSInfo("科创芯片", "sh000685", 30) + "\n")
|
||||
market.WriteString(GetZSInfo("上证医药", "sh000037", 30) + "\n")
|
||||
market.WriteString(GetZSInfo("证券龙头", "sz399437", 30) + "\n")
|
||||
market.WriteString(GetZSInfo("中证白酒", "sz399997", 30) + "\n")
|
||||
//logger.SugaredLogger.Infof("NewChatStream getZSInfo=\n%s", market.String())
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": "当前市场指数行情",
|
||||
"content": "当前市场/大盘/行业/指数行情",
|
||||
})
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": "当前市场指数行情情况如下:\n" + market.String(),
|
||||
"content": "当前市场/大盘/行业/指数行情如下:\n" + market.String(),
|
||||
})
|
||||
}()
|
||||
|
||||
@@ -255,9 +286,44 @@ func (o OpenAi) NewSummaryStockNewsStreamWithTools(userQuestion string, sysPromp
|
||||
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
resp := NewMarketNewsApi().TradingViewNews()
|
||||
var newsText strings.Builder
|
||||
|
||||
news := NewMarketNewsApi().GetNewsList("财联社电报", random.RandInt(50, 150))
|
||||
for _, a := range *resp {
|
||||
logger.SugaredLogger.Debugf("TradingViewNews: %s", a.Title)
|
||||
newsText.WriteString(a.Title + "\n")
|
||||
}
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": "全球新闻资讯",
|
||||
})
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": newsText.String(),
|
||||
})
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
news := NewMarketNewsApi().ReutersNew()
|
||||
messageText := strings.Builder{}
|
||||
for _, article := range news.Result.Articles {
|
||||
messageText.WriteString("## " + article.Title + "\n")
|
||||
messageText.WriteString("### " + article.Description + "\n")
|
||||
}
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": "外媒全球新闻资讯",
|
||||
})
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": messageText.String(),
|
||||
})
|
||||
}()
|
||||
wg.Wait()
|
||||
news := NewMarketNewsApi().GetNewsList2("财联社电报", random.RandInt(100, 500))
|
||||
messageText := strings.Builder{}
|
||||
for _, telegraph := range *news {
|
||||
messageText.WriteString("## " + telegraph.Time + ":" + "\n")
|
||||
@@ -285,7 +351,7 @@ func (o OpenAi) NewSummaryStockNewsStreamWithTools(userQuestion string, sysPromp
|
||||
return ch
|
||||
}
|
||||
|
||||
func (o OpenAi) NewSummaryStockNewsStream(userQuestion string, sysPromptId *int) <-chan map[string]any {
|
||||
func (o *OpenAi) NewSummaryStockNewsStream(userQuestion string, sysPromptId *int) <-chan map[string]any {
|
||||
ch := make(chan map[string]any, 512)
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
@@ -329,13 +395,20 @@ func (o OpenAi) NewSummaryStockNewsStream(userQuestion string, sysPromptId *int)
|
||||
"content": "当前本地时间是:" + time.Now().Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
wg.Add(4)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
var market strings.Builder
|
||||
market.WriteString(getZSInfo("创业板指数", "sz399006", 30) + "\n")
|
||||
market.WriteString(getZSInfo("上证综合指数", "sh000001", 30) + "\n")
|
||||
market.WriteString(getZSInfo("沪深300指数", "sh000300", 30) + "\n")
|
||||
market.WriteString(GetZSInfo("上证指数", "sh000001", 30) + "\n")
|
||||
market.WriteString(GetZSInfo("深证成指", "sz399001", 30) + "\n")
|
||||
market.WriteString(GetZSInfo("创业板指数", "sz399006", 30) + "\n")
|
||||
market.WriteString(GetZSInfo("科创50", "sh000688", 30) + "\n")
|
||||
market.WriteString(GetZSInfo("沪深300指数", "sh000300", 30) + "\n")
|
||||
market.WriteString(GetZSInfo("中证银行", "sz399986", 30) + "\n")
|
||||
market.WriteString(GetZSInfo("科创芯片", "sh000685", 30) + "\n")
|
||||
market.WriteString(GetZSInfo("上证医药", "sh000037", 30) + "\n")
|
||||
market.WriteString(GetZSInfo("证券龙头", "sz399437", 30) + "\n")
|
||||
market.WriteString(GetZSInfo("中证白酒", "sz399997", 30) + "\n")
|
||||
//logger.SugaredLogger.Infof("NewChatStream getZSInfo=\n%s", market.String())
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
@@ -346,9 +419,60 @@ func (o OpenAi) NewSummaryStockNewsStream(userQuestion string, sysPromptId *int)
|
||||
"content": "当前市场指数行情情况如下:\n" + market.String(),
|
||||
})
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
resp := NewMarketNewsApi().TradingViewNews()
|
||||
var newsText strings.Builder
|
||||
|
||||
for _, a := range *resp {
|
||||
logger.SugaredLogger.Debugf("TradingViewNews: %s", a.Title)
|
||||
newsText.WriteString(a.Title + "\n")
|
||||
}
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": "外媒全球新闻资讯",
|
||||
})
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": newsText.String(),
|
||||
})
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
news := NewMarketNewsApi().ReutersNew()
|
||||
messageText := strings.Builder{}
|
||||
for _, article := range news.Result.Articles {
|
||||
messageText.WriteString("## " + article.Title + "\n")
|
||||
messageText.WriteString("### " + article.Description + "\n")
|
||||
}
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": "外媒全球新闻资讯",
|
||||
})
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": messageText.String(),
|
||||
})
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
datas := NewMarketNewsApi().InteractiveAnswer(1, 100, "")
|
||||
content := util.MarkdownTableWithTitle("当前最新投资者互动数据", datas.Results)
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": "投资者互动数据",
|
||||
})
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": content,
|
||||
})
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
news := NewMarketNewsApi().GetNewsList("", 100)
|
||||
news := NewMarketNewsApi().GetNewsList2("财联社电报", random.RandInt(100, 500))
|
||||
messageText := strings.Builder{}
|
||||
for _, telegraph := range *news {
|
||||
messageText.WriteString("## " + telegraph.Time + ":" + "\n")
|
||||
@@ -376,7 +500,7 @@ func (o OpenAi) NewSummaryStockNewsStream(userQuestion string, sysPromptId *int)
|
||||
return ch
|
||||
}
|
||||
|
||||
func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptId *int, tools []Tool) <-chan map[string]any {
|
||||
func (o *OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptId *int, tools []Tool) <-chan map[string]any {
|
||||
ch := make(chan map[string]any, 512)
|
||||
|
||||
defer func() {
|
||||
@@ -459,16 +583,15 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptId
|
||||
logger.SugaredLogger.Infof("NewChatStream stock:%s stockCode:%s", stock, stockCode)
|
||||
logger.SugaredLogger.Infof("Prompt:%s", sysPrompt)
|
||||
logger.SugaredLogger.Infof("final question:%s", question)
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(7)
|
||||
wg.Add(8)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
var market strings.Builder
|
||||
market.WriteString(getZSInfo("创业板指数", "sz399006", 30) + "\n")
|
||||
market.WriteString(getZSInfo("上证综合指数", "sh000001", 30) + "\n")
|
||||
market.WriteString(getZSInfo("沪深300指数", "sh000300", 30) + "\n")
|
||||
market.WriteString(GetZSInfo("创业板指数", "sz399006", 30) + "\n")
|
||||
market.WriteString(GetZSInfo("上证综合指数", "sh000001", 30) + "\n")
|
||||
market.WriteString(GetZSInfo("沪深300指数", "sh000300", 30) + "\n")
|
||||
//logger.SugaredLogger.Infof("NewChatStream getZSInfo=\n%s", market.String())
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
@@ -684,20 +807,39 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptId
|
||||
return
|
||||
}
|
||||
|
||||
messages := SearchGuShiTongStockInfo(stockCode, o.CrawlTimeOut)
|
||||
if messages == nil || len(*messages) == 0 {
|
||||
logger.SugaredLogger.Error("获取股势通资讯失败")
|
||||
//ch <- "***❗获取股势通资讯失败,分析结果可能不准确***<hr>"
|
||||
//go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取股势通资讯失败,分析结果可能不准确")
|
||||
return
|
||||
}
|
||||
//messages := SearchGuShiTongStockInfo(stockCode, o.CrawlTimeOut)
|
||||
//if messages == nil || len(*messages) == 0 {
|
||||
// logger.SugaredLogger.Error("获取股势通资讯失败")
|
||||
// //ch <- "***❗获取股势通资讯失败,分析结果可能不准确***<hr>"
|
||||
// //go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取股势通资讯失败,分析结果可能不准确")
|
||||
// return
|
||||
//}
|
||||
//var newsText strings.Builder
|
||||
//for _, message := range *messages {
|
||||
// newsText.WriteString(message + "\n")
|
||||
//}
|
||||
//msg = append(msg, map[string]interface{}{
|
||||
// "role": "user",
|
||||
// "content": stock + "相关新闻资讯",
|
||||
//})
|
||||
//msg = append(msg, map[string]interface{}{
|
||||
// "role": "assistant",
|
||||
// "content": newsText.String(),
|
||||
//})
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
resp := NewMarketNewsApi().TradingViewNews()
|
||||
var newsText strings.Builder
|
||||
for _, message := range *messages {
|
||||
newsText.WriteString(message + "\n")
|
||||
|
||||
for _, a := range *resp {
|
||||
logger.SugaredLogger.Debugf("value: %s", a.Title)
|
||||
newsText.WriteString(a.Title + "\n")
|
||||
}
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": stock + "相关新闻资讯",
|
||||
"content": "外媒全球新闻资讯",
|
||||
})
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
@@ -722,7 +864,7 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptId
|
||||
return ch
|
||||
}
|
||||
|
||||
func AskAi(o OpenAi, err error, messages []map[string]interface{}, ch chan map[string]any, question string) {
|
||||
func AskAi(o *OpenAi, err error, messages []map[string]interface{}, ch chan map[string]any, question string) {
|
||||
client := resty.New()
|
||||
client.SetBaseURL(strutil.Trim(o.BaseUrl))
|
||||
client.SetHeader("Authorization", "Bearer "+o.ApiKey)
|
||||
@@ -735,7 +877,10 @@ func AskAi(o OpenAi, err error, messages []map[string]interface{}, ch chan map[s
|
||||
resp, err := client.R().
|
||||
SetDoNotParseResponse(true).
|
||||
SetBody(map[string]interface{}{
|
||||
"model": o.Model,
|
||||
"model": o.Model,
|
||||
"thinking": map[string]any{
|
||||
"type": "disabled",
|
||||
},
|
||||
"max_tokens": o.MaxTokens,
|
||||
"temperature": o.Temperature,
|
||||
"stream": true,
|
||||
@@ -863,7 +1008,7 @@ func AskAi(o OpenAi, err error, messages []map[string]interface{}, ch chan map[s
|
||||
|
||||
}
|
||||
}
|
||||
func AskAiWithTools(o OpenAi, err error, messages []map[string]interface{}, ch chan map[string]any, question string, tools []Tool) {
|
||||
func AskAiWithTools(o *OpenAi, err error, messages []map[string]interface{}, ch chan map[string]any, question string, tools []Tool) {
|
||||
client := resty.New()
|
||||
client.SetBaseURL(strutil.Trim(o.BaseUrl))
|
||||
client.SetHeader("Authorization", "Bearer "+o.ApiKey)
|
||||
@@ -876,7 +1021,11 @@ func AskAiWithTools(o OpenAi, err error, messages []map[string]interface{}, ch c
|
||||
resp, err := client.R().
|
||||
SetDoNotParseResponse(true).
|
||||
SetBody(map[string]interface{}{
|
||||
"model": o.Model,
|
||||
"model": o.Model,
|
||||
"thinking": map[string]any{
|
||||
"type": "disabled",
|
||||
//"type": "enabled",
|
||||
},
|
||||
"max_tokens": o.MaxTokens,
|
||||
"temperature": o.Temperature,
|
||||
"stream": true,
|
||||
@@ -904,6 +1053,8 @@ func AskAiWithTools(o OpenAi, err error, messages []map[string]interface{}, ch c
|
||||
currentFuncName := ""
|
||||
currentCallId := ""
|
||||
var currentAIContent strings.Builder
|
||||
var reasoningContentText strings.Builder
|
||||
var contentText strings.Builder
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
@@ -939,6 +1090,7 @@ func AskAiWithTools(o OpenAi, err error, messages []map[string]interface{}, ch c
|
||||
if err := json.Unmarshal([]byte(data), &streamResponse); err == nil {
|
||||
for _, choice := range streamResponse.Choices {
|
||||
if content := choice.Delta.Content; content != "" {
|
||||
contentText.WriteString(content)
|
||||
//ch <- content
|
||||
//logger.SugaredLogger.Infof("Content data: %s", content)
|
||||
|
||||
@@ -966,6 +1118,7 @@ func AskAiWithTools(o OpenAi, err error, messages []map[string]interface{}, ch c
|
||||
|
||||
}
|
||||
if reasoningContent := choice.Delta.ReasoningContent; reasoningContent != "" {
|
||||
reasoningContentText.WriteString(reasoningContent)
|
||||
//ch <- reasoningContent
|
||||
ch <- map[string]any{
|
||||
"code": 1,
|
||||
@@ -1012,7 +1165,7 @@ func AskAiWithTools(o OpenAi, err error, messages []map[string]interface{}, ch c
|
||||
}
|
||||
|
||||
content := "无符合条件的数据"
|
||||
res := NewSearchStockApi(words).SearchStock(random.RandInt(5, 10))
|
||||
res := NewSearchStockApi(words).SearchStock(random.RandInt(50, 120))
|
||||
if convertor.ToString(res["code"]) == "100" {
|
||||
resData := res["data"].(map[string]any)
|
||||
result := resData["result"].(map[string]any)
|
||||
@@ -1178,6 +1331,186 @@ func AskAiWithTools(o OpenAi, err error, messages []map[string]interface{}, ch c
|
||||
}
|
||||
}
|
||||
|
||||
if funcName == "InteractiveAnswer" {
|
||||
page := gjson.Get(funcArguments, "page").String()
|
||||
pageSize := gjson.Get(funcArguments, "pageSize").String()
|
||||
keyWord := gjson.Get(funcArguments, "keyWord").String()
|
||||
ch <- map[string]any{
|
||||
"code": 1,
|
||||
"question": question,
|
||||
"chatId": streamResponse.Id,
|
||||
"model": streamResponse.Model,
|
||||
"content": "\r\n```\r\n开始调用工具:InteractiveAnswer,\n参数:" + page + "," + pageSize + "," + keyWord + "\r\n```\r\n",
|
||||
"time": time.Now().Format(time.DateTime),
|
||||
}
|
||||
pageNo, err := convertor.ToInt(page)
|
||||
if err != nil {
|
||||
pageNo = 1
|
||||
}
|
||||
pageSizeNum, err := convertor.ToInt(pageSize)
|
||||
if err != nil {
|
||||
pageSizeNum = 50
|
||||
}
|
||||
datas := NewMarketNewsApi().InteractiveAnswer(int(pageNo), int(pageSizeNum), keyWord)
|
||||
content := util.MarkdownTableWithTitle("投资互动数据", datas.Results)
|
||||
logger.SugaredLogger.Infof("InteractiveAnswer=\n%s", content)
|
||||
messages = append(messages, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": currentAIContent.String(),
|
||||
"tool_calls": []map[string]any{
|
||||
{
|
||||
"id": currentCallId,
|
||||
"tool_call_id": currentCallId,
|
||||
"type": "function",
|
||||
"function": map[string]string{
|
||||
"name": funcName,
|
||||
"arguments": funcArguments,
|
||||
"parameters": funcArguments,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
messages = append(messages, map[string]interface{}{
|
||||
"role": "tool",
|
||||
"content": content,
|
||||
"tool_call_id": currentCallId,
|
||||
})
|
||||
}
|
||||
//
|
||||
//if funcName == "QueryBKDictInfo" {
|
||||
// ch <- map[string]any{
|
||||
// "code": 1,
|
||||
// "question": question,
|
||||
// "chatId": streamResponse.Id,
|
||||
// "model": streamResponse.Model,
|
||||
// "content": "\r\n```\r\n开始调用工具:QueryBKDictInfo,\n参数:" + funcArguments + "\r\n```\r\n",
|
||||
// "time": time.Now().Format(time.DateTime),
|
||||
// }
|
||||
// res := NewMarketNewsApi().EMDictCode("016", freecache.NewCache(100))
|
||||
// bytes, err := json.Marshal(res)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// dict := &[]models.BKDict{}
|
||||
// json.Unmarshal(bytes, dict)
|
||||
// md := util.MarkdownTableWithTitle("行业/板块代码", dict)
|
||||
// logger.SugaredLogger.Infof("行业/板块代码=\n%s", md)
|
||||
// messages = append(messages, map[string]interface{}{
|
||||
// "role": "assistant",
|
||||
// "content": currentAIContent.String(),
|
||||
// "tool_calls": []map[string]any{
|
||||
// {
|
||||
// "id": currentCallId,
|
||||
// "tool_call_id": currentCallId,
|
||||
// "type": "function",
|
||||
// "function": map[string]string{
|
||||
// "name": funcName,
|
||||
// "arguments": funcArguments,
|
||||
// "parameters": funcArguments,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
// messages = append(messages, map[string]interface{}{
|
||||
// "role": "tool",
|
||||
// "content": md,
|
||||
// "tool_call_id": currentCallId,
|
||||
// })
|
||||
//}
|
||||
|
||||
//if funcName == "GetIndustryResearchReport" {
|
||||
// bkCode := gjson.Get(funcArguments, "bkCode").String()
|
||||
// ch <- map[string]any{
|
||||
// "code": 1,
|
||||
// "question": question,
|
||||
// "chatId": streamResponse.Id,
|
||||
// "model": streamResponse.Model,
|
||||
// "content": "\r\n```\r\n开始调用工具:GetIndustryResearchReport,\n参数:" + bkCode + "\r\n```\r\n",
|
||||
// "time": time.Now().Format(time.DateTime),
|
||||
// }
|
||||
// bkCode = strutil.ReplaceWithMap(bkCode, map[string]string{
|
||||
// "-": "",
|
||||
// "_": "",
|
||||
// "bk": "",
|
||||
// "BK": "",
|
||||
// "bk0": "",
|
||||
// "BK0": "",
|
||||
// })
|
||||
//
|
||||
// logger.SugaredLogger.Debugf("code:%s", bkCode)
|
||||
// codeStr := convertor.ToString(bkCode)
|
||||
// res := NewMarketNewsApi().IndustryResearchReport(codeStr, 7)
|
||||
// md := strings.Builder{}
|
||||
// for _, a := range res {
|
||||
// d := a.(map[string]any)
|
||||
// md.WriteString(NewMarketNewsApi().GetIndustryReportInfo(d["infoCode"].(string)))
|
||||
// }
|
||||
// logger.SugaredLogger.Infof("bkCode:%s IndustryResearchReport:\n %s", bkCode, md.String())
|
||||
// messages = append(messages, map[string]interface{}{
|
||||
// "role": "assistant",
|
||||
// "content": currentAIContent.String(),
|
||||
// "tool_calls": []map[string]any{
|
||||
// {
|
||||
// "id": currentCallId,
|
||||
// "tool_call_id": currentCallId,
|
||||
// "type": "function",
|
||||
// "function": map[string]string{
|
||||
// "name": funcName,
|
||||
// "arguments": funcArguments,
|
||||
// "parameters": funcArguments,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
// messages = append(messages, map[string]interface{}{
|
||||
// "role": "tool",
|
||||
// "content": md.String(),
|
||||
// "tool_call_id": currentCallId,
|
||||
// })
|
||||
//}
|
||||
|
||||
if funcName == "GetStockResearchReport" {
|
||||
stockCode := gjson.Get(funcArguments, "stockCode").String()
|
||||
ch <- map[string]any{
|
||||
"code": 1,
|
||||
"question": question,
|
||||
"chatId": streamResponse.Id,
|
||||
"model": streamResponse.Model,
|
||||
"content": "\r\n```\r\n开始调用工具:GetStockResearchReport,\n参数:" + stockCode + "\r\n```\r\n",
|
||||
"time": time.Now().Format(time.DateTime),
|
||||
}
|
||||
res := NewMarketNewsApi().StockResearchReport(stockCode, 7)
|
||||
md := strings.Builder{}
|
||||
for _, a := range res {
|
||||
logger.SugaredLogger.Debugf("value: %+v", a)
|
||||
d := a.(map[string]any)
|
||||
logger.SugaredLogger.Debugf("value: %s infoCode:%s", d["title"], d["infoCode"])
|
||||
md.WriteString(NewMarketNewsApi().GetIndustryReportInfo(d["infoCode"].(string)))
|
||||
}
|
||||
logger.SugaredLogger.Infof("stockCode:%s StockResearchReport:\n %s", stockCode, md.String())
|
||||
messages = append(messages, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": currentAIContent.String(),
|
||||
"tool_calls": []map[string]any{
|
||||
{
|
||||
"id": currentCallId,
|
||||
"tool_call_id": currentCallId,
|
||||
"type": "function",
|
||||
"function": map[string]string{
|
||||
"name": funcName,
|
||||
"arguments": funcArguments,
|
||||
"parameters": funcArguments,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
messages = append(messages, map[string]interface{}{
|
||||
"role": "tool",
|
||||
"content": md.String(),
|
||||
"tool_call_id": currentCallId,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
AskAiWithTools(o, err, messages, ch, question, tools)
|
||||
}
|
||||
@@ -1382,7 +1715,7 @@ func GetTelegraphList(crawlTimeOut int64) *[]string {
|
||||
response, err := resty.New().SetTimeout(time.Duration(crawlTimeOut)*time.Second).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))
|
||||
Get(url)
|
||||
if err != nil {
|
||||
return &[]string{}
|
||||
}
|
||||
@@ -1404,7 +1737,7 @@ func GetTopNewsList(crawlTimeOut int64) *[]string {
|
||||
response, err := resty.New().SetTimeout(time.Duration(crawlTimeOut)*time.Second).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))
|
||||
Get(url)
|
||||
if err != nil {
|
||||
return &[]string{}
|
||||
}
|
||||
@@ -1421,7 +1754,7 @@ func GetTopNewsList(crawlTimeOut int64) *[]string {
|
||||
return &telegraph
|
||||
}
|
||||
|
||||
func (o OpenAi) SaveAIResponseResult(stockCode, stockName, result, chatId, question string) {
|
||||
func (o *OpenAi) SaveAIResponseResult(stockCode, stockName, result, chatId, question string) {
|
||||
db.Dao.Create(&models.AIResponseResult{
|
||||
StockCode: stockCode,
|
||||
StockName: stockName,
|
||||
@@ -1432,7 +1765,7 @@ func (o OpenAi) SaveAIResponseResult(stockCode, stockName, result, chatId, quest
|
||||
})
|
||||
}
|
||||
|
||||
func (o OpenAi) GetAIResponseResult(stock string) *models.AIResponseResult {
|
||||
func (o *OpenAi) GetAIResponseResult(stock string) *models.AIResponseResult {
|
||||
var result models.AIResponseResult
|
||||
db.Dao.Where("stock_code = ?", stock).Order("id desc").Limit(1).Find(&result)
|
||||
return &result
|
||||
|
||||
@@ -3,11 +3,13 @@ package data
|
||||
import (
|
||||
"context"
|
||||
"go-stock/backend/db"
|
||||
log "go-stock/backend/logger"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewDeepSeekOpenAiConfig(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
InitAnalyzeSentiment()
|
||||
|
||||
var tools []Tool
|
||||
tools = append(tools, Tool{
|
||||
@@ -28,7 +30,7 @@ func TestNewDeepSeekOpenAiConfig(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
ai := NewDeepSeekOpenAi(context.TODO())
|
||||
ai := NewDeepSeekOpenAi(context.TODO(), 0)
|
||||
//res := ai.NewChatStream("长电科技", "sh600584", "长电科技分析和总结", nil)
|
||||
res := ai.NewSummaryStockNewsStreamWithTools("总结市场资讯,发掘潜力标的/行业/板块/概念,控制风险。调用工具函数验证", nil, tools)
|
||||
|
||||
@@ -52,8 +54,17 @@ func TestGetTopNewsList(t *testing.T) {
|
||||
|
||||
func TestSearchGuShiTongStockInfo(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
SearchGuShiTongStockInfo("hk01810", 60)
|
||||
SearchGuShiTongStockInfo("sh600745", 60)
|
||||
SearchGuShiTongStockInfo("gb_goog", 60)
|
||||
//SearchGuShiTongStockInfo("hk01810", 60)
|
||||
msgs := SearchGuShiTongStockInfo("sh600745", 60)
|
||||
for _, msg := range *msgs {
|
||||
log.SugaredLogger.Infof("%s", msg)
|
||||
}
|
||||
//SearchGuShiTongStockInfo("gb_goog", 60)
|
||||
|
||||
}
|
||||
|
||||
func TestGetZSInfo(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
GetZSInfo("中证银行", "sz399986", 30)
|
||||
GetZSInfo("上海贝岭", "sh600171", 30)
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ type BrowserPool struct {
|
||||
func NewBrowserPool(size int) *BrowserPool {
|
||||
pool := make(chan *context.Context, size)
|
||||
for i := 0; i < size; i++ {
|
||||
path := GetConfig().BrowserPath
|
||||
crawlTimeOut := GetConfig().CrawlTimeOut
|
||||
path := GetSettingConfig().BrowserPath
|
||||
crawlTimeOut := GetSettingConfig().CrawlTimeOut
|
||||
if crawlTimeOut < 15 {
|
||||
crawlTimeOut = 30
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@ package data
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"go-stock/backend/logger"
|
||||
"time"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
@@ -20,33 +21,43 @@ func NewSearchStockApi(words string) *SearchStockApi {
|
||||
return &SearchStockApi{words: words}
|
||||
}
|
||||
func (s SearchStockApi) SearchStock(pageSize int) map[string]any {
|
||||
qgqpBId := NewSettingsApi().Config.QgqpBId
|
||||
if qgqpBId == "" {
|
||||
return map[string]any{
|
||||
"code": -1,
|
||||
"message": "请先获取东财用户标识(qgqp_b_id):打开浏览器,访问东财网站,按F12打开开发人员工具-》网络面板,随便点开一个请求,复制请求cookie中qgqp_b_id对应的值。保存到设置中的东财唯一标识输入框",
|
||||
}
|
||||
}
|
||||
url := "https://np-tjxg-g.eastmoney.com/api/smart-tag/stock/v3/pw/search-code"
|
||||
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
|
||||
SetHeader("Host", "np-tjxg-g.eastmoney.com").
|
||||
SetHeader("Origin", "https://xuangu.eastmoney.com").
|
||||
SetHeader("Referer", "https://xuangu.eastmoney.com/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:145.0) Gecko/20100101 Firefox/145.0").
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(fmt.Sprintf(`{
|
||||
"keyWord": "%s",
|
||||
"pageSize": %d,
|
||||
"pageNo": 1,
|
||||
"fingerprint": "e38b5faabf9378c8238e57219f0ebc9b",
|
||||
"fingerprint": "%s",
|
||||
"gids": [],
|
||||
"matchWord": "",
|
||||
"timestamp": "1751113883290349",
|
||||
"timestamp": "%d",
|
||||
"shareToGuba": false,
|
||||
"requestId": "8xTWgCDAjvQ5lmvz5mDA3Ydk2AE4yoiJ1751113883290",
|
||||
"requestId": "",
|
||||
"needCorrect": true,
|
||||
"removedConditionIdList": [],
|
||||
"xcId": "xc0af28549ab330013ed",
|
||||
"xcId": "",
|
||||
"ownSelectAll": false,
|
||||
"dxInfo": [],
|
||||
"extraCondition": ""
|
||||
}`, s.words, pageSize)).Post(url)
|
||||
}`, s.words, pageSize, qgqpBId, time.Now().Unix())).Post(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("SearchStock-err:%+v", err)
|
||||
return map[string]any{}
|
||||
return map[string]any{
|
||||
"code": -1,
|
||||
"message": err.Error(),
|
||||
}
|
||||
}
|
||||
respMap := map[string]any{}
|
||||
json.Unmarshal(resp.Body(), &respMap)
|
||||
|
||||
@@ -2,16 +2,26 @@ package data
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/random"
|
||||
)
|
||||
|
||||
func TestSearchStock(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
|
||||
res := NewSearchStockApi("算力股;净利润连续3年增长").SearchStock(10)
|
||||
e := convertor.ToString(math.Floor(float64(9*random.RandFloat(0, 1, 12) + 1)))
|
||||
for i := 0; i < 19; i++ {
|
||||
e += convertor.ToString(math.Floor(float64(9 * random.RandFloat(0, 1, 12))))
|
||||
}
|
||||
logger.SugaredLogger.Infof("e:%s", e)
|
||||
|
||||
res := NewSearchStockApi("量比大于2,基本面优秀,2025年三季报已披露,主力连续3日净流入,非创业板非科创板非ST").SearchStock(20)
|
||||
logger.SugaredLogger.Infof("res:%+v", res)
|
||||
data := res["data"].(map[string]any)
|
||||
result := data["result"].(map[string]any)
|
||||
dataList := result["dataList"].([]any)
|
||||
|
||||
@@ -2,8 +2,12 @@ package data
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"time"
|
||||
|
||||
"github.com/samber/lo"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -15,113 +19,202 @@ type Settings struct {
|
||||
DingRobot string `json:"dingRobot"`
|
||||
UpdateBasicInfoOnStart bool `json:"updateBasicInfoOnStart"`
|
||||
RefreshInterval int64 `json:"refreshInterval"`
|
||||
|
||||
OpenAiEnable bool `json:"openAiEnable"`
|
||||
OpenAiBaseUrl string `json:"openAiBaseUrl"`
|
||||
OpenAiApiKey string `json:"openAiApiKey"`
|
||||
OpenAiModelName string `json:"openAiModelName"`
|
||||
OpenAiMaxTokens int `json:"openAiMaxTokens"`
|
||||
OpenAiTemperature float64 `json:"openAiTemperature"`
|
||||
OpenAiApiTimeOut int `json:"openAiApiTimeOut"`
|
||||
Prompt string `json:"prompt"`
|
||||
CheckUpdate bool `json:"checkUpdate"`
|
||||
QuestionTemplate string `json:"questionTemplate"`
|
||||
CrawlTimeOut int64 `json:"crawlTimeOut"`
|
||||
KDays int64 `json:"kDays"`
|
||||
EnableDanmu bool `json:"enableDanmu"`
|
||||
BrowserPath string `json:"browserPath"`
|
||||
EnableNews bool `json:"enableNews"`
|
||||
DarkTheme bool `json:"darkTheme"`
|
||||
BrowserPoolSize int `json:"browserPoolSize"`
|
||||
EnableFund bool `json:"enableFund"`
|
||||
EnablePushNews bool `json:"enablePushNews"`
|
||||
SponsorCode string `json:"sponsorCode"`
|
||||
OpenAiEnable bool `json:"openAiEnable"`
|
||||
Prompt string `json:"prompt"`
|
||||
CheckUpdate bool `json:"checkUpdate"`
|
||||
QuestionTemplate string `json:"questionTemplate"`
|
||||
CrawlTimeOut int64 `json:"crawlTimeOut"`
|
||||
KDays int64 `json:"kDays"`
|
||||
EnableDanmu bool `json:"enableDanmu"`
|
||||
BrowserPath string `json:"browserPath"`
|
||||
EnableNews bool `json:"enableNews"`
|
||||
DarkTheme bool `json:"darkTheme"`
|
||||
BrowserPoolSize int `json:"browserPoolSize"`
|
||||
EnableFund bool `json:"enableFund"`
|
||||
EnablePushNews bool `json:"enablePushNews"`
|
||||
EnableOnlyPushRedNews bool `json:"enableOnlyPushRedNews"`
|
||||
SponsorCode string `json:"sponsorCode"`
|
||||
HttpProxy string `json:"httpProxy"`
|
||||
HttpProxyEnabled bool `json:"httpProxyEnabled"`
|
||||
EnableAgent bool `json:"enableAgent"`
|
||||
QgqpBId string `json:"qgqpBId" gorm:"column:qgqp_b_id"`
|
||||
}
|
||||
|
||||
func (receiver Settings) TableName() string {
|
||||
return "settings"
|
||||
}
|
||||
|
||||
type SettingsApi struct {
|
||||
Config Settings
|
||||
type AIConfig struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Name string `json:"name"`
|
||||
BaseUrl string `json:"baseUrl"`
|
||||
ApiKey string `json:"apiKey" `
|
||||
ModelName string `json:"modelName"`
|
||||
MaxTokens int `json:"maxTokens"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
TimeOut int `json:"timeOut"`
|
||||
}
|
||||
|
||||
func NewSettingsApi(settings *Settings) *SettingsApi {
|
||||
func (AIConfig) TableName() string {
|
||||
return "ai_config"
|
||||
}
|
||||
|
||||
type SettingConfig struct {
|
||||
*Settings
|
||||
AiConfigs []*AIConfig `json:"aiConfigs"`
|
||||
}
|
||||
|
||||
type SettingsApi struct {
|
||||
Config *SettingConfig
|
||||
}
|
||||
|
||||
func NewSettingsApi() *SettingsApi {
|
||||
return &SettingsApi{
|
||||
Config: *settings,
|
||||
Config: GetSettingConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s SettingsApi) UpdateConfig() string {
|
||||
func (s *SettingsApi) Export() string {
|
||||
d, _ := json.MarshalIndent(s.Config, "", " ")
|
||||
return string(d)
|
||||
}
|
||||
|
||||
func UpdateConfig(s *SettingConfig) string {
|
||||
count := int64(0)
|
||||
db.Dao.Model(s.Config).Count(&count)
|
||||
db.Dao.Model(&Settings{}).Count(&count)
|
||||
if count > 0 {
|
||||
db.Dao.Model(s.Config).Where("id=?", s.Config.ID).Updates(map[string]any{
|
||||
"local_push_enable": s.Config.LocalPushEnable,
|
||||
"ding_push_enable": s.Config.DingPushEnable,
|
||||
"ding_robot": s.Config.DingRobot,
|
||||
"update_basic_info_on_start": s.Config.UpdateBasicInfoOnStart,
|
||||
"refresh_interval": s.Config.RefreshInterval,
|
||||
"open_ai_enable": s.Config.OpenAiEnable,
|
||||
"open_ai_base_url": s.Config.OpenAiBaseUrl,
|
||||
"open_ai_api_key": s.Config.OpenAiApiKey,
|
||||
"open_ai_model_name": s.Config.OpenAiModelName,
|
||||
"open_ai_max_tokens": s.Config.OpenAiMaxTokens,
|
||||
"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,
|
||||
"question_template": s.Config.QuestionTemplate,
|
||||
"crawl_time_out": s.Config.CrawlTimeOut,
|
||||
"k_days": s.Config.KDays,
|
||||
"enable_danmu": s.Config.EnableDanmu,
|
||||
"browser_path": s.Config.BrowserPath,
|
||||
"enable_news": s.Config.EnableNews,
|
||||
"dark_theme": s.Config.DarkTheme,
|
||||
"enable_fund": s.Config.EnableFund,
|
||||
"enable_push_news": s.Config.EnablePushNews,
|
||||
"sponsor_code": s.Config.SponsorCode,
|
||||
db.Dao.Model(&Settings{}).Where("id=?", s.ID).Updates(map[string]any{
|
||||
"local_push_enable": s.LocalPushEnable,
|
||||
"ding_push_enable": s.DingPushEnable,
|
||||
"ding_robot": s.DingRobot,
|
||||
"update_basic_info_on_start": s.UpdateBasicInfoOnStart,
|
||||
"refresh_interval": s.RefreshInterval,
|
||||
"open_ai_enable": s.OpenAiEnable,
|
||||
"tushare_token": s.TushareToken,
|
||||
"prompt": s.Prompt,
|
||||
"check_update": s.CheckUpdate,
|
||||
"question_template": s.QuestionTemplate,
|
||||
"crawl_time_out": s.CrawlTimeOut,
|
||||
"k_days": s.KDays,
|
||||
"enable_danmu": s.EnableDanmu,
|
||||
"browser_path": s.BrowserPath,
|
||||
"enable_news": s.EnableNews,
|
||||
"dark_theme": s.DarkTheme,
|
||||
"enable_fund": s.EnableFund,
|
||||
"enable_push_news": s.EnablePushNews,
|
||||
"enable_only_push_red_news": s.EnableOnlyPushRedNews,
|
||||
"sponsor_code": s.SponsorCode,
|
||||
"http_proxy": s.HttpProxy,
|
||||
"http_proxy_enabled": s.HttpProxyEnabled,
|
||||
"enable_agent": s.EnableAgent,
|
||||
"qgqp_b_id": s.QgqpBId,
|
||||
})
|
||||
|
||||
//更新AiConfig
|
||||
err := updateAiConfigs(s.AiConfigs)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("更新AI模型服务配置失败: %v", err)
|
||||
return "更新AI模型服务配置失败: " + err.Error()
|
||||
}
|
||||
} else {
|
||||
logger.SugaredLogger.Infof("未找到配置,创建默认配置:%+v", s.Config)
|
||||
db.Dao.Model(s.Config).Create(&Settings{
|
||||
LocalPushEnable: s.Config.LocalPushEnable,
|
||||
DingPushEnable: s.Config.DingPushEnable,
|
||||
DingRobot: s.Config.DingRobot,
|
||||
UpdateBasicInfoOnStart: s.Config.UpdateBasicInfoOnStart,
|
||||
RefreshInterval: s.Config.RefreshInterval,
|
||||
OpenAiEnable: s.Config.OpenAiEnable,
|
||||
OpenAiBaseUrl: s.Config.OpenAiBaseUrl,
|
||||
OpenAiApiKey: s.Config.OpenAiApiKey,
|
||||
OpenAiModelName: s.Config.OpenAiModelName,
|
||||
OpenAiMaxTokens: s.Config.OpenAiMaxTokens,
|
||||
OpenAiTemperature: s.Config.OpenAiTemperature,
|
||||
TushareToken: s.Config.TushareToken,
|
||||
Prompt: s.Config.Prompt,
|
||||
CheckUpdate: s.Config.CheckUpdate,
|
||||
OpenAiApiTimeOut: s.Config.OpenAiApiTimeOut,
|
||||
QuestionTemplate: s.Config.QuestionTemplate,
|
||||
CrawlTimeOut: s.Config.CrawlTimeOut,
|
||||
KDays: s.Config.KDays,
|
||||
EnableDanmu: s.Config.EnableDanmu,
|
||||
BrowserPath: s.Config.BrowserPath,
|
||||
EnableNews: s.Config.EnableNews,
|
||||
DarkTheme: s.Config.DarkTheme,
|
||||
EnableFund: s.Config.EnableFund,
|
||||
EnablePushNews: s.Config.EnablePushNews,
|
||||
SponsorCode: s.Config.SponsorCode,
|
||||
})
|
||||
logger.SugaredLogger.Infof("未找到配置,创建默认配置")
|
||||
// 创建主配置
|
||||
result := db.Dao.Model(&Settings{}).Create(&Settings{})
|
||||
if result.Error != nil {
|
||||
logger.SugaredLogger.Error("创建配置失败:", result.Error)
|
||||
return "创建配置失败: " + result.Error.Error()
|
||||
}
|
||||
}
|
||||
return "保存成功!"
|
||||
}
|
||||
func (s SettingsApi) GetConfig() *Settings {
|
||||
var settings Settings
|
||||
db.Dao.Model(&Settings{}).First(&settings)
|
||||
|
||||
func updateAiConfigs(aiConfigs []*AIConfig) error {
|
||||
if len(aiConfigs) == 0 {
|
||||
err := db.Dao.Exec("DELETE FROM ai_config").Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Dao.Exec("DELETE FROM sqlite_sequence WHERE name='ai_config'").Error
|
||||
}
|
||||
var ids []uint
|
||||
lo.ForEach(aiConfigs, func(item *AIConfig, index int) {
|
||||
ids = append(ids, item.ID)
|
||||
})
|
||||
var existAiConfigs []*AIConfig
|
||||
err := db.Dao.Model(&AIConfig{}).Select("id").Where("id in (?) ", ids).Find(&existAiConfigs).Error
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
idMap := make(map[uint]bool)
|
||||
lo.ForEach(existAiConfigs, func(item *AIConfig, index int) {
|
||||
idMap[item.ID] = true
|
||||
})
|
||||
var addAiConfigs []*AIConfig
|
||||
var notDeleteIds []uint
|
||||
var e error
|
||||
lo.ForEach(aiConfigs, func(item *AIConfig, index int) {
|
||||
if e != nil {
|
||||
return
|
||||
}
|
||||
if !idMap[item.ID] {
|
||||
addAiConfigs = append(addAiConfigs, item)
|
||||
} else {
|
||||
notDeleteIds = append(notDeleteIds, item.ID)
|
||||
e = db.Dao.Model(&AIConfig{}).Where("id=?", item.ID).Updates(map[string]interface{}{
|
||||
"name": item.Name,
|
||||
"base_url": item.BaseUrl,
|
||||
"api_key": item.ApiKey,
|
||||
"model_name": item.ModelName,
|
||||
"max_tokens": item.MaxTokens,
|
||||
"temperature": item.Temperature,
|
||||
"time_out": item.TimeOut,
|
||||
}).Error
|
||||
if e != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
//删除旧的配置
|
||||
if len(notDeleteIds) > 0 {
|
||||
err = db.Dao.Exec("DELETE FROM ai_config WHERE id NOT IN ?", notDeleteIds).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
logger.SugaredLogger.Infof("更新aiConfigs +%d", len(addAiConfigs))
|
||||
//批量新增的配置
|
||||
err = db.Dao.CreateInBatches(addAiConfigs, len(addAiConfigs)).Error
|
||||
return err
|
||||
}
|
||||
|
||||
func GetSettingConfig() *SettingConfig {
|
||||
settingConfig := &SettingConfig{}
|
||||
settings := &Settings{}
|
||||
aiConfigs := make([]*AIConfig, 0)
|
||||
// 处理数据库查询可能返回的空结果
|
||||
result := db.Dao.Model(&Settings{}).First(settings)
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
// 初始化默认设置并保存到数据库
|
||||
settings = &Settings{OpenAiEnable: false, CrawlTimeOut: 60}
|
||||
db.Dao.Create(settings)
|
||||
}
|
||||
|
||||
if settings.OpenAiEnable {
|
||||
if settings.OpenAiApiTimeOut <= 0 {
|
||||
settings.OpenAiApiTimeOut = 60 * 5
|
||||
// 处理AI配置查询可能出现的错误
|
||||
result = db.Dao.Model(&AIConfig{}).Find(&aiConfigs)
|
||||
if result.Error != nil {
|
||||
logger.SugaredLogger.Error("查询AI配置失败:", result.Error)
|
||||
} else if len(aiConfigs) > 0 {
|
||||
lo.ForEach(aiConfigs, func(item *AIConfig, index int) {
|
||||
if item.TimeOut <= 0 {
|
||||
item.TimeOut = 60 * 5
|
||||
}
|
||||
})
|
||||
}
|
||||
if settings.CrawlTimeOut <= 0 {
|
||||
settings.CrawlTimeOut = 60
|
||||
@@ -136,10 +229,8 @@ func (s SettingsApi) GetConfig() *Settings {
|
||||
if settings.BrowserPoolSize <= 0 {
|
||||
settings.BrowserPoolSize = 1
|
||||
}
|
||||
return &settings
|
||||
}
|
||||
settingConfig.Settings = settings
|
||||
settingConfig.AiConfigs = aiConfigs
|
||||
|
||||
func (s SettingsApi) Export() string {
|
||||
d, _ := json.MarshalIndent(s.GetConfig(), "", " ")
|
||||
return string(d)
|
||||
return settingConfig
|
||||
}
|
||||
|
||||
@@ -9,6 +9,14 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/chromedp/chromedp"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
@@ -17,17 +25,10 @@ import (
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/robertkrimen/otto"
|
||||
"github.com/samber/lo"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
"golang.org/x/text/transform"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/plugin/soft_delete"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const sinaStockUrl = "http://hq.sinajs.cn/rn=%d&list=%s"
|
||||
@@ -37,7 +38,7 @@ const tushareApiUrl = "http://api.tushare.pro"
|
||||
|
||||
type StockDataApi struct {
|
||||
client *resty.Client
|
||||
config *Settings
|
||||
config *SettingConfig
|
||||
}
|
||||
type StockInfo struct {
|
||||
gorm.Model
|
||||
@@ -172,6 +173,7 @@ type FollowedStock struct {
|
||||
Cron *string
|
||||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
|
||||
Groups []GroupStock `gorm:"foreignKey:StockCode;references:StockCode"`
|
||||
AiConfigId int
|
||||
}
|
||||
|
||||
func (receiver FollowedStock) TableName() string {
|
||||
@@ -196,7 +198,7 @@ func (receiver StockBasic) TableName() string {
|
||||
func NewStockDataApi() *StockDataApi {
|
||||
return &StockDataApi{
|
||||
client: resty.New(),
|
||||
config: GetConfig(),
|
||||
config: GetSettingConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,6 +379,9 @@ func (receiver StockDataApi) GetStockCodeRealTimeData(StockCodes ...string) (*[]
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
continue
|
||||
}
|
||||
if stockData == nil {
|
||||
continue
|
||||
}
|
||||
stockInfos = append(stockInfos, *stockData)
|
||||
|
||||
go func() {
|
||||
@@ -415,6 +420,15 @@ func (receiver StockDataApi) Follow(stockCode string) string {
|
||||
}
|
||||
|
||||
stockCode = strings.ToLower(stockCode)
|
||||
|
||||
// 检查是否已经关注过该股票
|
||||
var existingStock FollowedStock
|
||||
result := db.Dao.Model(&FollowedStock{}).Where("stock_code = ? AND is_del = ?", stockCode, 0).First(&existingStock)
|
||||
if result.Error == nil {
|
||||
// 股票已经关注过
|
||||
return "已经关注了"
|
||||
}
|
||||
|
||||
maxSort := int64(0)
|
||||
db.Dao.Model(&FollowedStock{}).Raw("select max(sort) as sort from followed_stock").Scan(&maxSort)
|
||||
|
||||
@@ -1164,7 +1178,7 @@ func getHKStockPriceInfo(stockCode string, crawlTimeOut int64) *[]string {
|
||||
return &messages
|
||||
}
|
||||
|
||||
func getZSInfo(name, stockCode string, crawlTimeOut int64) string {
|
||||
func GetZSInfo(name, stockCode string, crawlTimeOut int64) string {
|
||||
url := "https://finance.sina.com.cn/realstock/company/" + stockCode + "/nc.shtml"
|
||||
crawlerAPI := CrawlerApi{}
|
||||
crawlerBaseInfo := CrawlerBaseInfo{
|
||||
@@ -1189,6 +1203,10 @@ func getZSInfo(name, stockCode string, crawlTimeOut int64) string {
|
||||
price := strutil.RemoveWhiteSpace(document.Find("div#price").First().Text(), false)
|
||||
hqTime := strutil.RemoveWhiteSpace(document.Find("div#hqTime").First().Text(), false)
|
||||
|
||||
if strutil.ContainsAny(price, []string{"-", "--", ""}) {
|
||||
return "暂无数据"
|
||||
}
|
||||
|
||||
var markdown strings.Builder
|
||||
markdown.WriteString(fmt.Sprintf("### 时间:%s %s:%s \n", hqTime, name, price))
|
||||
GetTableMarkdown(document, "div#hqDetails table", &markdown)
|
||||
@@ -1466,11 +1484,11 @@ func (receiver StockDataApi) getDCStockInfo(market string, page, pageSize int) {
|
||||
|
||||
url := "https://push2.eastmoney.com/api/qt/clist/get?np=1&fltt=1&invt=2&cb=data&fs=%s&fields=f12,f13,f14,f1,f2,f4,f3,f152,f5,f6,f7,f15,f18,f16,f17,f10,f8,f9,f23,f100,f265&fid=f3&pn=%d&pz=%d&po=1&dect=1&wbp2u=|0|0|0|web&_=%d"
|
||||
sprintfUrl := fmt.Sprintf(url, fs, page, pageSize, time.Now().UnixMilli())
|
||||
logger.SugaredLogger.Infof("url:%s", sprintfUrl)
|
||||
logger.SugaredLogger.Infof("page:%d url:%s", page, sprintfUrl)
|
||||
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
|
||||
SetHeader("Host", "push2.eastmoney.com").
|
||||
SetHeader("Referer", "https://quote.eastmoney.com/center/gridlist.html").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0) Gecko/20100101 Firefox/146.0").
|
||||
Get(sprintfUrl)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
@@ -1727,6 +1745,11 @@ func (receiver StockDataApi) GetCommonKLineData(stockCode string, kLineType stri
|
||||
return K
|
||||
}
|
||||
|
||||
// GetStockHistoryMoneyData 获取股票历史资金流向数据
|
||||
func (receiver StockDataApi) GetStockHistoryMoneyData() {
|
||||
|
||||
}
|
||||
|
||||
// JSONToMarkdownTable 将JSON数据转换为Markdown表格
|
||||
func JSONToMarkdownTable(jsonData []byte) (string, error) {
|
||||
var data []map[string]interface{}
|
||||
|
||||
@@ -4,17 +4,19 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/random"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/util"
|
||||
"io/ioutil"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/random"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
@@ -48,14 +50,23 @@ func TestGetFinancialReports(t *testing.T) {
|
||||
|
||||
func TestGetTelegraphSearch(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
searchWords := "半导体 新能源汽车 机器人"
|
||||
//url := "https://www.cls.cn/searchPage?keyword=%E9%97%BB%E6%B3%B0%E7%A7%91%E6%8A%80&type=telegram"
|
||||
messages := SearchStockInfo("谷歌", "telegram", 30)
|
||||
messages := SearchStockInfo(searchWords, "telegram", 30)
|
||||
for _, message := range *messages {
|
||||
logger.SugaredLogger.Info(message)
|
||||
}
|
||||
|
||||
//https://www.cls.cn/stock?code=sh600745
|
||||
}
|
||||
func TestCailianpressWeb(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
searchWords := "半导体 新能源汽车 机器人"
|
||||
res := NewMarketNewsApi().CailianpressWeb(searchWords)
|
||||
md := util.MarkdownTableWithTitle(searchWords+"财联社新闻", res.List)
|
||||
logger.SugaredLogger.Info(md)
|
||||
}
|
||||
|
||||
func TestSearchStockInfoByCode(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
SearchStockInfoByCode("sh600745")
|
||||
@@ -63,7 +74,7 @@ func TestSearchStockInfoByCode(t *testing.T) {
|
||||
|
||||
func TestSearchStockPriceInfo(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
//SearchStockPriceInfo("中信证券", "hk06030", 30)
|
||||
SearchStockPriceInfo("博安生物", "hk06955", 30)
|
||||
SearchStockPriceInfo("上海贝岭", "sh600171", 30)
|
||||
//SearchStockPriceInfo("苹果公司", "gb_aapl", 30)
|
||||
//SearchStockPriceInfo("微创光电", "bj430198", 30)
|
||||
@@ -110,10 +121,10 @@ func TestGetHKStockInfo(t *testing.T) {
|
||||
//NewStockDataApi().GetSinaHKStockInfo()
|
||||
//m:105,m:106,m:107 //美股
|
||||
//m:128+t:3,m:128+t:4,m:128+t:1,m:128+t:2 //港股
|
||||
//287 224 605
|
||||
for i := 1; i <= 605; i++ {
|
||||
NewStockDataApi().getDCStockInfo("us", i, 20)
|
||||
time.Sleep(time.Duration(random.RandInt(1, 3)) * time.Second)
|
||||
//274 224 605
|
||||
for i := 197; i <= 274; i++ {
|
||||
NewStockDataApi().getDCStockInfo("", i, 20)
|
||||
time.Sleep(time.Duration(random.RandInt(2, 5)) * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,3 +265,21 @@ func TestStockDataApi_GetIndexBasic(t *testing.T) {
|
||||
stockDataApi := NewStockDataApi()
|
||||
stockDataApi.GetIndexBasic()
|
||||
}
|
||||
|
||||
func TestName(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
|
||||
stockBasics := &[]StockBasic{}
|
||||
resty.New().SetProxy("").R().
|
||||
SetHeader("user", "go-stock").
|
||||
SetResult(stockBasics).
|
||||
Get("http://8.134.249.145:18080/go-stock/stock_basic.json")
|
||||
|
||||
logger.SugaredLogger.Infof("%+v", stockBasics)
|
||||
//db.Dao.Unscoped().Model(&StockBasic{}).Where("1=1").Delete(&StockBasic{})
|
||||
//err := db.Dao.CreateInBatches(stockBasics, 400).Error
|
||||
//if err != nil {
|
||||
// t.Log(err.Error())
|
||||
//}
|
||||
|
||||
}
|
||||
|
||||
@@ -39,17 +39,74 @@ func NewStockGroupApi(dao *gorm.DB) *StockGroupApi {
|
||||
}
|
||||
|
||||
func (receiver StockGroupApi) AddGroup(group Group) bool {
|
||||
err := receiver.dao.Where("name = ?", group.Name).FirstOrCreate(&group).Updates(&Group{
|
||||
Name: group.Name,
|
||||
Sort: group.Sort,
|
||||
}).Error
|
||||
// 检查是否已存在相同sort的组
|
||||
var existingGroup Group
|
||||
err := receiver.dao.Where("sort = ?", group.Sort).First(&existingGroup).Error
|
||||
|
||||
// 如果存在相同sort的组,则将该组及之后的所有组向后移动一位
|
||||
if err == nil {
|
||||
// 处理sort冲突:将相同sort值及之后的所有组向后移动一位
|
||||
receiver.dao.Model(&Group{}).Where("sort >= ?", group.Sort).Update("sort", gorm.Expr("sort + ?", 1))
|
||||
}
|
||||
|
||||
// 创建新组
|
||||
err = receiver.dao.Create(&group).Error
|
||||
return err == nil
|
||||
}
|
||||
func (receiver StockGroupApi) GetGroupList() []Group {
|
||||
var groups []Group
|
||||
receiver.dao.Find(&groups)
|
||||
receiver.dao.Order("sort ASC").Find(&groups)
|
||||
return groups
|
||||
}
|
||||
func (receiver StockGroupApi) UpdateGroupSort(id int, newSort int) bool {
|
||||
// First, get the current group to check if it exists
|
||||
var currentGroup Group
|
||||
if err := receiver.dao.First(¤tGroup, id).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// If the new sort is the same as current, no need to update
|
||||
if currentGroup.Sort == newSort {
|
||||
return true
|
||||
}
|
||||
|
||||
// Get all groups ordered by sort
|
||||
var allGroups []Group
|
||||
receiver.dao.Order("sort ASC").Find(&allGroups)
|
||||
|
||||
// Adjust sort numbers to make space for the new sort value
|
||||
if newSort > currentGroup.Sort {
|
||||
// Moving down: decrease sort of groups between old and new position
|
||||
receiver.dao.Model(&Group{}).Where("sort > ? AND sort <= ? AND id != ?", currentGroup.Sort, newSort, id).Update("sort", gorm.Expr("sort - ?", 1))
|
||||
} else {
|
||||
// Moving up: increase sort of groups between new and old position
|
||||
receiver.dao.Model(&Group{}).Where("sort >= ? AND sort < ? AND id != ?", newSort, currentGroup.Sort, id).Update("sort", gorm.Expr("sort + ?", 1))
|
||||
}
|
||||
|
||||
// Update the target group's sort
|
||||
err := receiver.dao.Model(&Group{}).Where("id = ?", id).Update("sort", newSort).Error
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// InitializeGroupSort initializes sort order for all groups based on created time
|
||||
func (receiver StockGroupApi) InitializeGroupSort() bool {
|
||||
// Get all groups ordered by created time
|
||||
var groups []Group
|
||||
err := receiver.dao.Order("created_at ASC").Find(&groups).Error
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Update each group with new sort value based on their position
|
||||
for i, group := range groups {
|
||||
newSort := i + 1
|
||||
err := receiver.dao.Model(&Group{}).Where("id = ?", group.ID).Update("sort", newSort).Error
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
func (receiver StockGroupApi) GetGroupStockByGroupId(groupId int) []GroupStock {
|
||||
var stockGroup []GroupStock
|
||||
receiver.dao.Preload("GroupInfo").Where("group_id = ?", groupId).Find(&stockGroup)
|
||||
|
||||
@@ -2,37 +2,50 @@ package data
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"github.com/go-ego/gse"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/fileutil"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/go-ego/gse"
|
||||
)
|
||||
|
||||
const basefreq float64 = 100
|
||||
|
||||
// 金融情感词典,包含股票市场相关的专业词汇
|
||||
var (
|
||||
seg gse.Segmenter
|
||||
|
||||
// 正面金融词汇及其权重
|
||||
positiveFinanceWords = map[string]float64{
|
||||
"上涨": 2.0, "涨停": 3.0, "牛市": 3.0, "反弹": 2.0, "新高": 2.5,
|
||||
"涨": 1.0, "上涨": 2.0, "涨停": 3.0, "牛市": 3.0, "反弹": 2.0, "新高": 2.5,
|
||||
"利好": 2.5, "增持": 2.0, "买入": 2.0, "推荐": 1.5, "看多": 2.0,
|
||||
"盈利": 2.0, "增长": 2.0, "超预期": 2.5, "强劲": 1.5, "回升": 1.5,
|
||||
"复苏": 2.0, "突破": 2.0, "创新高": 3.0, "回暖": 1.5, "上扬": 1.5,
|
||||
"利好消息": 3.0, "收益增长": 2.5, "利润增长": 2.5, "业绩优异": 2.5,
|
||||
"潜力股": 2.0, "绩优股": 2.0, "强势": 1.5, "走高": 1.5, "攀升": 1.5,
|
||||
"大涨": 2.5, "飙升": 3.0, "井喷": 3.0, "爆发": 2.5, "暴涨": 3.0,
|
||||
"大涨": 2.5, "飙升": 3.0, "井喷": 3.0, "暴涨": 3.0,
|
||||
}
|
||||
|
||||
// 负面金融词汇及其权重
|
||||
negativeFinanceWords = map[string]float64{
|
||||
"下跌": 2.0, "跌停": 3.0, "熊市": 3.0, "回调": 1.5, "新低": 2.5,
|
||||
"跌": 2.0, "下跌": 2.0, "跌停": 3.0, "熊市": 3.0, "回调": 2.5, "新低": 2.5,
|
||||
"利空": 2.5, "减持": 2.0, "卖出": 2.0, "看空": 2.0, "亏损": 2.5,
|
||||
"下滑": 2.0, "萎缩": 2.0, "不及预期": 2.5, "疲软": 1.5, "恶化": 2.0,
|
||||
"衰退": 2.0, "跌破": 2.0, "创新低": 3.0, "走弱": 1.5, "下挫": 1.5,
|
||||
"衰退": 2.0, "跌破": 2.0, "创新低": 3.0, "走弱": 2.5, "下挫": 2.5,
|
||||
"利空消息": 3.0, "收益下降": 2.5, "利润下滑": 2.5, "业绩不佳": 2.5,
|
||||
"垃圾股": 2.0, "风险股": 2.0, "弱势": 1.5, "走低": 1.5, "缩量": 2.5,
|
||||
"大跌": 2.5, "暴跌": 3.0, "崩盘": 3.0, "跳水": 3.0, "重挫": 3.0,
|
||||
"垃圾股": 2.0, "风险股": 2.0, "弱势": 2.5, "走低": 2.5, "缩量": 2.5,
|
||||
"大跌": 2.5, "暴跌": 3.0, "崩盘": 3.0, "跳水": 3.0, "重挫": 3.0, "跌超": 2.5, "跌逾": 2.5, "跌近": 3.0,
|
||||
"被抓": 3.0, "被抓捕": 3.0, "回吐": 3.0, "转跌": 3.0,
|
||||
}
|
||||
|
||||
// 否定词,用于反转情感极性
|
||||
@@ -44,7 +57,7 @@ var (
|
||||
degreeWords = map[string]float64{
|
||||
"非常": 1.8, "极其": 2.2, "太": 1.8, "很": 1.5,
|
||||
"比较": 0.8, "稍微": 0.6, "有点": 0.7, "显著": 1.5,
|
||||
"大幅": 1.8, "急剧": 2.0, "轻微": 0.6, "小幅": 0.7,
|
||||
"大幅": 1.8, "急剧": 2.0, "轻微": 0.6, "小幅": 0.7, "逾": 1.8, "超": 1.8,
|
||||
}
|
||||
|
||||
// 转折词,用于识别情感转折
|
||||
@@ -53,12 +66,267 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
// 加载默认词典
|
||||
err := seg.LoadDict()
|
||||
//go:embed data/dict/base.txt
|
||||
var baseDict string
|
||||
|
||||
//go:embed data/dict/zh/s_1.txt
|
||||
var zhDict string
|
||||
|
||||
func InitAnalyzeSentiment() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.SugaredLogger.Error(fmt.Sprintf("panic: %v", r))
|
||||
}
|
||||
}()
|
||||
// 加载简体中文词典
|
||||
//err := seg.LoadDict("zh_s")
|
||||
//if err != nil {
|
||||
// logger.SugaredLogger.Error(err.Error())
|
||||
//}
|
||||
|
||||
err := seg.LoadDictEmbed(baseDict)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
} else {
|
||||
logger.SugaredLogger.Info("加载默认词典成功")
|
||||
}
|
||||
seg.CalcToken()
|
||||
|
||||
stocks := &[]StockBasic{}
|
||||
db.Dao.Model(&StockBasic{}).Find(stocks)
|
||||
for _, stock := range *stocks {
|
||||
if strutil.Trim(stock.Name) == "" {
|
||||
continue
|
||||
}
|
||||
err := seg.AddToken(stock.Name, basefreq+100, "n")
|
||||
if strutil.Trim(stock.BKName) != "" {
|
||||
err = seg.AddToken(stock.BKName, basefreq+100, "n")
|
||||
}
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("添加%s失败:%s", stock.Name, err.Error())
|
||||
}
|
||||
}
|
||||
logger.SugaredLogger.Info("加载股票名称词典成功")
|
||||
|
||||
stockhks := &[]models.StockInfoHK{}
|
||||
db.Dao.Model(&models.StockInfoHK{}).Find(stockhks)
|
||||
for _, stock := range *stockhks {
|
||||
if strutil.Trim(stock.Name) == "" {
|
||||
continue
|
||||
}
|
||||
err := seg.AddToken(stock.Name, basefreq+100, "n")
|
||||
if strutil.Trim(stock.BKName) != "" {
|
||||
err = seg.AddToken(stock.BKName, basefreq+100, "n")
|
||||
}
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("添加%s失败:%s", stock.Name, err.Error())
|
||||
}
|
||||
}
|
||||
logger.SugaredLogger.Info("加载港股名称词典成功")
|
||||
//stockus := &[]models.StockInfoUS{}
|
||||
//db.Dao.Model(&models.StockInfoUS{}).Where("trim(name) != ?", "").Find(stockus)
|
||||
//for _, stock := range *stockus {
|
||||
// err := seg.AddToken(stock.Name, 500)
|
||||
// if err != nil {
|
||||
// logger.SugaredLogger.Errorf("添加%s失败:%s", stock.Name, err.Error())
|
||||
// }
|
||||
//}
|
||||
tags := &[]models.Tags{}
|
||||
db.Dao.Model(&models.Tags{}).Where("type = ?", "subject").Find(tags)
|
||||
for _, tag := range *tags {
|
||||
if tag.Name == "" {
|
||||
continue
|
||||
}
|
||||
err := seg.AddToken(tag.Name, basefreq+100, "n")
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("添加%s失败:%s", tag.Name, err.Error())
|
||||
} else {
|
||||
logger.SugaredLogger.Infof("添加tags词典[%s]成功", tag.Name)
|
||||
}
|
||||
}
|
||||
logger.SugaredLogger.Info("加载tags词典成功")
|
||||
seg.CalcToken()
|
||||
//加载用户自定义词典 先判断用户词典是否存在
|
||||
if fileutil.IsExist("data/dict/user.txt") {
|
||||
lines, err := fileutil.ReadFileByLine("data/dict/user.txt")
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
for _, line := range lines {
|
||||
if len(line) == 0 || line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
k := strutil.SplitAndTrim(line, " ")
|
||||
if len(k) == 0 {
|
||||
continue
|
||||
}
|
||||
_, _, ok := seg.Find(k[0])
|
||||
switch len(k) {
|
||||
case 1:
|
||||
if ok {
|
||||
err = seg.ReAddToken(k[0], basefreq)
|
||||
} else {
|
||||
err = seg.AddToken(k[0], basefreq)
|
||||
}
|
||||
case 2:
|
||||
freq, _ := convertor.ToFloat(k[1])
|
||||
if ok {
|
||||
err = seg.ReAddToken(k[0], freq)
|
||||
} else {
|
||||
err = seg.AddToken(k[0], freq)
|
||||
}
|
||||
case 3:
|
||||
freq, _ := convertor.ToFloat(k[1])
|
||||
if ok {
|
||||
err = seg.ReAddToken(k[0], freq, k[2])
|
||||
} else {
|
||||
err = seg.AddToken(k[0], freq, k[2])
|
||||
}
|
||||
default:
|
||||
logger.SugaredLogger.Errorf("用户词典格式错误:%s", line)
|
||||
}
|
||||
logger.SugaredLogger.Infof("添加用户词典[%s]成功", line)
|
||||
}
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
} else {
|
||||
logger.SugaredLogger.Infof("加载用户词典成功")
|
||||
}
|
||||
} else {
|
||||
logger.SugaredLogger.Info("用户词典不存在")
|
||||
}
|
||||
seg.CalcToken()
|
||||
}
|
||||
|
||||
// WordFreqWithWeight 词频统计结果,包含权重信息
|
||||
type WordFreqWithWeight struct {
|
||||
Word string
|
||||
Frequency int
|
||||
Weight float64
|
||||
Score float64
|
||||
}
|
||||
|
||||
// getWordWeight 获取词汇权重
|
||||
func getWordWeight(word string) float64 {
|
||||
// 从分词器获取词汇权重
|
||||
|
||||
freq, pos, ok := seg.Dictionary().Find([]byte(word))
|
||||
if ok {
|
||||
logger.SugaredLogger.Infof("获取%s的权重:%f,pos:%s,ok:%v", word, freq, pos, ok)
|
||||
return freq
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// SortByWeightAndFrequency 按权重和频次排序词频结果
|
||||
func SortByWeightAndFrequency(frequencies map[string]WordFreqWithWeight) []WordFreqWithWeight {
|
||||
// 将map转换为slice以便排序
|
||||
freqSlice := make([]WordFreqWithWeight, 0, len(frequencies))
|
||||
for _, freq := range frequencies {
|
||||
freqSlice = append(freqSlice, freq)
|
||||
}
|
||||
|
||||
// 按权重*频次降序排列
|
||||
sort.Slice(freqSlice, func(i, j int) bool {
|
||||
return freqSlice[i].Weight*float64(freqSlice[i].Frequency) > freqSlice[j].Weight*float64(freqSlice[j].Frequency)
|
||||
})
|
||||
logger.SugaredLogger.Infof("排序后的结果:%v", freqSlice)
|
||||
|
||||
return freqSlice
|
||||
}
|
||||
|
||||
// FilterAndSortWords 过滤标点符号并按权重频次排序
|
||||
func FilterAndSortWords(frequencies map[string]WordFreqWithWeight) []WordFreqWithWeight {
|
||||
// 先过滤标点符号和分隔符
|
||||
cleanFrequencies := FilterPunctuationAndSeparators(frequencies)
|
||||
|
||||
// 再按权重和频次排序
|
||||
sortedFrequencies := SortByWeightAndFrequency(cleanFrequencies)
|
||||
|
||||
return sortedFrequencies
|
||||
}
|
||||
func FilterPunctuationAndSeparators(frequencies map[string]WordFreqWithWeight) map[string]WordFreqWithWeight {
|
||||
filteredWords := make(map[string]WordFreqWithWeight)
|
||||
|
||||
for word, freqInfo := range frequencies {
|
||||
// 过滤纯标点符号和分隔符
|
||||
if !isPunctuationOrSeparator(word) {
|
||||
filteredWords[word] = freqInfo
|
||||
}
|
||||
}
|
||||
return filteredWords
|
||||
}
|
||||
|
||||
// isPunctuationOrSeparator 判断是否为标点符号或分隔符
|
||||
func isPunctuationOrSeparator(word string) bool {
|
||||
// 空字符串
|
||||
if strings.TrimSpace(word) == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查是否全部由标点符号组成
|
||||
for _, r := range word {
|
||||
if !unicode.IsPunct(r) && !unicode.IsSymbol(r) && !unicode.IsSpace(r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// FilterWithRegex 使用正则表达式过滤标点和特殊字符
|
||||
func FilterWithRegex(frequencies map[string]WordFreqWithWeight) map[string]WordFreqWithWeight {
|
||||
filteredWords := make(map[string]WordFreqWithWeight)
|
||||
|
||||
// 匹配标点符号、特殊字符的正则表达式
|
||||
punctuationRegex := regexp.MustCompile(`^[[:punct:][:space:]]+$`)
|
||||
|
||||
for word, freqInfo := range frequencies {
|
||||
// 过滤纯标点符号
|
||||
if !punctuationRegex.MatchString(word) && strings.TrimSpace(word) != "" {
|
||||
filteredWords[word] = freqInfo
|
||||
}
|
||||
}
|
||||
return filteredWords
|
||||
}
|
||||
|
||||
// countWordFrequencyWithWeight 统计词频并包含权重信息
|
||||
func countWordFrequencyWithWeight(text string) map[string]WordFreqWithWeight {
|
||||
words := splitWords(text)
|
||||
freqMap := make(map[string]WordFreqWithWeight)
|
||||
|
||||
// 统计词频
|
||||
wordCount := make(map[string]int)
|
||||
for _, word := range words {
|
||||
wordCount[word]++
|
||||
}
|
||||
|
||||
// 构建包含权重的结果
|
||||
for word, frequency := range wordCount {
|
||||
weight := getWordWeight(word)
|
||||
if weight >= basefreq {
|
||||
freqMap[word] = WordFreqWithWeight{
|
||||
Word: word,
|
||||
Frequency: frequency,
|
||||
Weight: weight,
|
||||
Score: float64(frequency) * weight,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return freqMap
|
||||
}
|
||||
|
||||
// AnalyzeSentimentWithFreqWeight 带权重词频统计的情感分析
|
||||
func AnalyzeSentimentWithFreqWeight(text string) (SentimentResult, map[string]WordFreqWithWeight) {
|
||||
// 原有情感分析逻辑
|
||||
result := AnalyzeSentiment(text)
|
||||
|
||||
// 带权重的词频统计
|
||||
frequencies := countWordFrequencyWithWeight(text)
|
||||
|
||||
return result, frequencies
|
||||
}
|
||||
|
||||
// SentimentResult 情感分析结果类型
|
||||
|
||||
@@ -2,8 +2,12 @@ package data
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/duke-git/lancet/v2/random"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
@@ -12,25 +16,34 @@ import (
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func TestAnalyzeSentiment(t *testing.T) {
|
||||
// 分析情感
|
||||
text := " 【调查:韩国近两成中小学生过度使用智能手机或互联网】财联社6月19日电,韩国女性家族部18日公布的一项年度调查结果显示,接受调查的韩国中小学生中,共计约17.3%、即超过21万人使用智能手机或互联网的程度达到了“危险水平”,这意味着他们因过度依赖智能手机或互联网而需要关注或干预,这一比例引人担忧。 (新华社)\n"
|
||||
text = "消息人士称,联合利华(Unilever)正在为Graze零食品牌寻找买家。\n"
|
||||
text = "【韩国未来5年将投入51万亿韩元发展文化产业】 据韩联社,韩国文化体育观光部(文体部)今后5年将投入51万亿韩元(约合人民币2667亿元)预算,落实总统李在明在竞选时期提出的“将韩国打造成全球五大文化强国之一”的承诺。\n"
|
||||
//text = "【油气股持续拉升 国际实业午后涨停】财联社6月19日电,油气股午后持续拉升,国际实业、宝莫股份午后涨停,准油股份、山东墨龙。茂化实华此前涨停,通源石油、海默科技、贝肯能源、中曼石油、科力股份等多股涨超5%。\n"
|
||||
//text = " 【三大指数均跌逾1% 下跌个股近4800只】财联社6月19日电,指数持续走弱,沪指下挫跌逾1.00%,深成指跌1.25%,创业板指跌1.39%。核聚变、风电、军工、食品消费等板块指数跌幅居前,沪深京三市下跌个股近4800只。\n"
|
||||
text = "【银行理财首单网下打新落地】财联社6月20日电,记者从多渠道获悉,光大理财以申报价格17元参与信通电子网下打新,并成功入围有效报价,成为行业内首家参与网下打新的银行理财公司。光大理财工作人员向证券时报记者表示,本次光大理财是以其管理的混合类产品“阳光橙增盈绝对收益策略”参与了此次网下打新,该产品为光大理财“固收+”银行理财产品。资料显示,信通电子成立于1996年,核心产品包括输电线路智能巡检系统、变电站智能辅控系统、移动智能终端及其他产品。根据其招股说明书,信通电子2023、2024年营业收入分别较上年增长19.08%和7.97%,净利润分别较上年增长5.6%和15.11%。 (证券时报)"
|
||||
text = " 【以军称拦截数枚伊朗导弹】财联社6月20日电,据央视新闻报道,以军在贝尔谢巴及周边区域拦截了数枚伊朗导弹,但仍有导弹或拦截残骸落地。以色列国防军发文表示,搜救队伍正在一处“空中物体落地”的所在区域开展工作,公众目前可以离开避难场所。伊朗方面对上述说法暂无回应。"
|
||||
|
||||
db.Init("../../data/stock.db")
|
||||
InitAnalyzeSentiment()
|
||||
|
||||
messageText := strings.Builder{}
|
||||
news := NewMarketNewsApi().GetNewsList2("", random.RandInt(500, 1000))
|
||||
for _, telegraph := range *news {
|
||||
messageText.WriteString(telegraph.Content + "\n")
|
||||
}
|
||||
|
||||
text := messageText.String()
|
||||
//text = " 【周六你需要知道的隔夜全球要闻:美联储鸽声重振 美股走势回稳】 1、纽约联储行长威廉姆斯表示,随着劳动力市场走软,美联储近期内仍有再次降息的空间。 2、美联储理事斯蒂芬·米兰表示,自上次联邦公开市场委员会(FOMC)会议以来的经济数据应“促使人们偏向鸽派立场”。 3、波士顿联邦储备银行行长柯林斯表示,由于通胀可能在一段时间内保持高位,维持利率不变“目前合适”。 4、据CME“美联储观察”,截至北京时间11月22日6时30分,美联储12月降息25个基点的概率为69.4%,维持利率不变的概率为30.6%。 5、美国劳工统计局表示,11月CPI报告将于12月18日发布,同时取消了10月CPI报告发布,表示无法追溯采集政府停摆期间未能收集的部分数据。 6、俄罗斯总统普京表示,已收到美提出解决俄乌冲突的计划,俄罗斯愿意进行和平谈判。美国总统特朗普表示,他认为27日是乌克兰接受美国支持的和平计划的最后期限。 7、美联储高官鸽派言论提振市场情绪,美股三大指数收盘集体上涨,道琼斯指数涨1.08%,标普500指数涨0.98%,纳斯达克综合指数涨0.88%。甲骨文跌超5%,英伟达跌超1%。纳指本周累计跌2.74%,标普500指数累跌1.95%,道指累跌1.91%。英伟达本周累跌5.9%。 8、热门中概股多数上涨,纳斯达克中国金龙指数收涨1.23%。蔚来涨超3%,哔哩哔哩、理想汽车涨超2%,京东、小鹏汽车涨超1%。 9、国际油价下跌,交易员评估乌克兰与俄罗斯可能达成和平协议的前景。WTI 1月期货下跌1.6%,结算价报每桶58.06美元,为过去五个交易日中第四次下跌。布伦特1月期货下跌1.3%,结算价报每桶62.56美元。 10、美联储延长压力测试改进方案征询期,为银行反馈提供更多时间。 11、由于美国人对个人财务状况的看法恶化,美国消费者信心在11月跌至接近纪录最低水平;密歇根大学数据显示,11月消费者信心指数降至51,10月为53.6。 12、日本央行政策委员会委员Kazuyuki Masu表示,日本央行接近作出加息决定。 13、穆迪将意大利信用评级从BAA3上调至BAA2,展望稳定。\n"
|
||||
//text = "财联社电:英伟达周五冲高回落,股价涨幅收于1%,市场普遍认为其走势疲软"
|
||||
//text = "【本轮巴以冲突已致加沙地带69733人死亡】财联社11月22日电,当地时间22日下午,以军对加沙城西部一辆汽车发动空袭,已造成5人死亡,多人受伤。自2023年10月7日巴以新一轮大规模冲突爆发以来,以色列对加沙地带的袭击已造成69733人死亡、170863人受伤。"
|
||||
//text = "【牛肉加工亏损 美国泰森公司关停缩减相关业务】财联社11月22日电,受牛肉加工业务亏损影响,当地时间21日,美国泰森食品公司发布公告称,将关闭位于内布拉斯加州的一家大型牛肉加工厂,还计划缩小得克萨斯州一家牛肉加工厂的生产规模。根据泰森食品公司的公告,被关闭的这家工厂位于内布拉斯加州列克星敦,日均可宰杀并处理大约5000头牛,约占全美日均牛肉屠宰数量的4.8%。与此同时,公司还计划缩小得克萨斯州一家牛肉加工厂的生产规模,这家工厂每天大约可屠宰6000头牛。据悉,泰森此次业务调整影响两个工厂大约5000个工作岗位。《华尔街日报》报道称,泰森是美国四大肉类加工公司中首家关闭主要牛肉加工厂的公司,其最新财报显示,2025财年牛肉加工是唯一亏损的业务部门,调整后的营业亏损为4.26亿美元。"
|
||||
// 分析情感
|
||||
words := splitWords(text)
|
||||
fmt.Println(strings.Join(words, " "))
|
||||
|
||||
result := AnalyzeSentiment(text)
|
||||
|
||||
result, frequencies := AnalyzeSentimentWithFreqWeight(text)
|
||||
// 过滤标点符号和分隔符
|
||||
cleanFrequencies := FilterPunctuationAndSeparators(frequencies)
|
||||
// 输出结果
|
||||
fmt.Printf("情感分析结果: %s (得分: %.2f, 正面词:%d, 负面词:%d)\n",
|
||||
logger.SugaredLogger.Infof("情感分析结果: %s (得分: %.2f, 正面词:%d, 负面词:%d)\n 词频统计结果: %v",
|
||||
result.Description,
|
||||
result.Score,
|
||||
result.PositiveCount,
|
||||
result.NegativeCount)
|
||||
result.NegativeCount,
|
||||
cleanFrequencies,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@@ -17,10 +17,10 @@ import (
|
||||
|
||||
type TushareApi struct {
|
||||
client *resty.Client
|
||||
config *Settings
|
||||
config *SettingConfig
|
||||
}
|
||||
|
||||
func NewTushareApi(config *Settings) *TushareApi {
|
||||
func NewTushareApi(config *SettingConfig) *TushareApi {
|
||||
return &TushareApi{
|
||||
client: resty.New(),
|
||||
config: config,
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
// -----------------------------------------------------------------------------------
|
||||
func TestGetDaily(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
tushareApi := NewTushareApi(GetConfig())
|
||||
tushareApi := NewTushareApi(GetSettingConfig())
|
||||
res := tushareApi.GetDaily("00927.HK", "20250101", "20250217", 30)
|
||||
t.Log(res)
|
||||
|
||||
@@ -19,7 +19,7 @@ func TestGetDaily(t *testing.T) {
|
||||
|
||||
func TestGetUSDaily(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
tushareApi := NewTushareApi(GetConfig())
|
||||
tushareApi := NewTushareApi(GetSettingConfig())
|
||||
|
||||
res := tushareApi.GetDaily("gb_AAPL", "20250101", "20250217", 30)
|
||||
t.Log(res)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"gorm.io/plugin/dbresolver"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
var Dao *gorm.DB
|
||||
@@ -26,7 +27,7 @@ func Init(sqlitePath string) {
|
||||
var openDb *gorm.DB
|
||||
var err error
|
||||
if sqlitePath == "" {
|
||||
sqlitePath = "data/stock.db?cache=shared&mode=rwc&_journal_mode=WAL"
|
||||
sqlitePath = "data/stock.db?cache_size=-524288&journal_mode=WAL"
|
||||
}
|
||||
openDb, err = gorm.Open(sqlite.Open(sqlitePath), &gorm.Config{
|
||||
Logger: dbLogger,
|
||||
@@ -48,8 +49,8 @@ func Init(sqlitePath string) {
|
||||
if err != nil {
|
||||
log.Fatalf("openDb.DB error is %s", err.Error())
|
||||
}
|
||||
dbCon.SetMaxIdleConns(10)
|
||||
dbCon.SetMaxOpenConns(100)
|
||||
dbCon.SetMaxIdleConns(4)
|
||||
dbCon.SetMaxOpenConns(10)
|
||||
dbCon.SetConnMaxLifetime(time.Hour)
|
||||
Dao = openDb
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/plugin/soft_delete"
|
||||
"time"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
@@ -231,14 +232,16 @@ type Prompt struct {
|
||||
type Telegraph struct {
|
||||
gorm.Model
|
||||
Time string `json:"time"`
|
||||
Content string `json:"content"`
|
||||
DataTime *time.Time `json:"dataTime" gorm:"index"`
|
||||
Title string `json:"title" gorm:"index"`
|
||||
Content string `json:"content" gorm:"index"`
|
||||
SubjectTags []string `json:"subjects" gorm:"-:all"`
|
||||
StocksTags []string `json:"stocks" gorm:"-:all"`
|
||||
IsRed bool `json:"isRed"`
|
||||
IsRed bool `json:"isRed" gorm:"index"`
|
||||
Url string `json:"url"`
|
||||
Source string `json:"source"`
|
||||
Source string `json:"source" gorm:"index"`
|
||||
TelegraphTags []TelegraphTags `json:"tags" gorm:"-:migration;foreignKey:TelegraphId"`
|
||||
SentimentResult string `json:"sentimentResult" gorm:"-:all"`
|
||||
SentimentResult string `json:"sentimentResult" gorm:"index"`
|
||||
}
|
||||
type TelegraphTags struct {
|
||||
gorm.Model
|
||||
@@ -329,6 +332,22 @@ type TVNews struct {
|
||||
LogoId string `json:"logo_id"`
|
||||
} `json:"provider"`
|
||||
}
|
||||
type TVNewsDetail struct {
|
||||
ShortDescription string `json:"shortDescription"`
|
||||
Tags []struct {
|
||||
Title string `json:"title"`
|
||||
Args []struct {
|
||||
Id string `json:"id"`
|
||||
Value string `json:"value"`
|
||||
} `json:"args"`
|
||||
} `json:"tags"`
|
||||
Copyright string `json:"copyright"`
|
||||
Id string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Published int `json:"published"`
|
||||
Urgency int `json:"urgency"`
|
||||
StoryPath string `json:"storyPath"`
|
||||
}
|
||||
|
||||
type XUEQIUHot struct {
|
||||
Data struct {
|
||||
@@ -465,3 +484,238 @@ type PMIResp struct {
|
||||
DCResp
|
||||
PMIResult PMIResult `json:"result"`
|
||||
}
|
||||
|
||||
type OldSettings struct {
|
||||
gorm.Model
|
||||
TushareToken string `json:"tushareToken"`
|
||||
LocalPushEnable bool `json:"localPushEnable"`
|
||||
DingPushEnable bool `json:"dingPushEnable"`
|
||||
DingRobot string `json:"dingRobot"`
|
||||
UpdateBasicInfoOnStart bool `json:"updateBasicInfoOnStart"`
|
||||
RefreshInterval int64 `json:"refreshInterval"`
|
||||
|
||||
OpenAiEnable bool `json:"openAiEnable"`
|
||||
OpenAiBaseUrl string `json:"openAiBaseUrl"`
|
||||
OpenAiApiKey string `json:"openAiApiKey"`
|
||||
OpenAiModelName string `json:"openAiModelName"`
|
||||
OpenAiMaxTokens int `json:"openAiMaxTokens"`
|
||||
OpenAiTemperature float64 `json:"openAiTemperature"`
|
||||
OpenAiApiTimeOut int `json:"openAiApiTimeOut"`
|
||||
Prompt string `json:"prompt"`
|
||||
CheckUpdate bool `json:"checkUpdate"`
|
||||
QuestionTemplate string `json:"questionTemplate"`
|
||||
CrawlTimeOut int64 `json:"crawlTimeOut"`
|
||||
KDays int64 `json:"kDays"`
|
||||
EnableDanmu bool `json:"enableDanmu"`
|
||||
BrowserPath string `json:"browserPath"`
|
||||
EnableNews bool `json:"enableNews"`
|
||||
DarkTheme bool `json:"darkTheme"`
|
||||
BrowserPoolSize int `json:"browserPoolSize"`
|
||||
EnableFund bool `json:"enableFund"`
|
||||
EnablePushNews bool `json:"enablePushNews"`
|
||||
SponsorCode string `json:"sponsorCode"`
|
||||
}
|
||||
|
||||
func (receiver OldSettings) TableName() string {
|
||||
return "settings"
|
||||
}
|
||||
|
||||
type ReutersNews struct {
|
||||
StatusCode int `json:"statusCode"`
|
||||
Message string `json:"message"`
|
||||
Result struct {
|
||||
ParentSectionName string `json:"parent_section_name"`
|
||||
Pagination struct {
|
||||
Size int `json:"size"`
|
||||
ExpectedSize int `json:"expected_size"`
|
||||
TotalSize int `json:"total_size"`
|
||||
Orderby string `json:"orderby"`
|
||||
} `json:"pagination"`
|
||||
DateModified time.Time `json:"date_modified"`
|
||||
FetchType string `json:"fetch_type"`
|
||||
Articles []struct {
|
||||
Id string `json:"id"`
|
||||
CanonicalUrl string `json:"canonical_url"`
|
||||
Website string `json:"website"`
|
||||
Web string `json:"web"`
|
||||
Native string `json:"native"`
|
||||
UpdatedTime time.Time `json:"updated_time"`
|
||||
PublishedTime time.Time `json:"published_time"`
|
||||
ArticleType string `json:"article_type"`
|
||||
DisplayMyNews bool `json:"display_my_news"`
|
||||
DisplayNewsletterSignup bool `json:"display_newsletter_signup"`
|
||||
DisplayNotifications bool `json:"display_notifications"`
|
||||
DisplayRelatedMedia bool `json:"display_related_media"`
|
||||
DisplayRelatedOrganizations bool `json:"display_related_organizations"`
|
||||
ContentCode string `json:"content_code"`
|
||||
Source struct {
|
||||
Name string `json:"name"`
|
||||
OriginalName string `json:"original_name"`
|
||||
} `json:"source"`
|
||||
Title string `json:"title"`
|
||||
BasicHeadline string `json:"basic_headline"`
|
||||
Distributor string `json:"distributor"`
|
||||
Description string `json:"description"`
|
||||
PrimaryMediaType string `json:"primary_media_type,omitempty"`
|
||||
PrimaryTag struct {
|
||||
ShortBio string `json:"short_bio"`
|
||||
Description string `json:"description"`
|
||||
Slug string `json:"slug"`
|
||||
Text string `json:"text"`
|
||||
TopicUrl string `json:"topic_url"`
|
||||
CanFollow bool `json:"can_follow,omitempty"`
|
||||
IsTopic bool `json:"is_topic,omitempty"`
|
||||
} `json:"primary_tag"`
|
||||
WordCount int `json:"word_count"`
|
||||
ReadMinutes int `json:"read_minutes"`
|
||||
Kicker struct {
|
||||
Path string `json:"path"`
|
||||
Names []string `json:"names"`
|
||||
Name string `json:"name,omitempty"`
|
||||
} `json:"kicker"`
|
||||
AdTopics []string `json:"ad_topics"`
|
||||
Thumbnail struct {
|
||||
Url string `json:"url"`
|
||||
Caption string `json:"caption,omitempty"`
|
||||
Type string `json:"type"`
|
||||
ResizerUrl string `json:"resizer_url"`
|
||||
Location string `json:"location,omitempty"`
|
||||
Id string `json:"id"`
|
||||
Authors string `json:"authors,omitempty"`
|
||||
AltText string `json:"alt_text"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Subtitle string `json:"subtitle"`
|
||||
Slug string `json:"slug,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Company string `json:"company,omitempty"`
|
||||
PurchaseLicensingPath string `json:"purchase_licensing_path,omitempty"`
|
||||
} `json:"thumbnail"`
|
||||
Authors []struct {
|
||||
Id string `json:"id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
FirstName string `json:"first_name,omitempty"`
|
||||
LastName string `json:"last_name,omitempty"`
|
||||
Company string `json:"company"`
|
||||
Thumbnail struct {
|
||||
Url string `json:"url"`
|
||||
Type string `json:"type"`
|
||||
ResizerUrl string `json:"resizer_url"`
|
||||
} `json:"thumbnail"`
|
||||
SocialLinks []struct {
|
||||
Site string `json:"site"`
|
||||
Url string `json:"url"`
|
||||
} `json:"social_links,omitempty"`
|
||||
Byline string `json:"byline"`
|
||||
Description string `json:"description,omitempty"`
|
||||
TopicUrl string `json:"topic_url,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
} `json:"authors"`
|
||||
DisplayTime time.Time `json:"display_time"`
|
||||
ThumbnailDark struct {
|
||||
Url string `json:"url"`
|
||||
Type string `json:"type"`
|
||||
ResizerUrl string `json:"resizer_url"`
|
||||
Id string `json:"id"`
|
||||
AltText string `json:"alt_text"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Subtitle string `json:"subtitle"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
} `json:"thumbnail_dark,omitempty"`
|
||||
} `json:"articles"`
|
||||
Section struct {
|
||||
Id string `json:"id"`
|
||||
AdUnitCode string `json:"ad_unit_code"`
|
||||
Website string `json:"website"`
|
||||
Name string `json:"name"`
|
||||
PageTitle string `json:"page_title"`
|
||||
CanFollow bool `json:"can_follow"`
|
||||
Language string `json:"language"`
|
||||
Type string `json:"type"`
|
||||
Advertising struct {
|
||||
Sponsored string `json:"sponsored"`
|
||||
} `json:"advertising"`
|
||||
VideoPlaylistId string `json:"video_playlistId"`
|
||||
MobileAdUnitPath string `json:"mobile_ad_unit_path"`
|
||||
AdUnitPath string `json:"ad_unit_path"`
|
||||
CollectionAlias string `json:"collection_alias"`
|
||||
SectionAbout string `json:"section_about"`
|
||||
Title string `json:"title"`
|
||||
Personalization struct {
|
||||
Id string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
ShowTags bool `json:"show_tags"`
|
||||
CanFollow bool `json:"can_follow"`
|
||||
} `json:"personalization"`
|
||||
} `json:"section"`
|
||||
AdUnitPath string `json:"ad_unit_path"`
|
||||
ResponseTime int64 `json:"response_time"`
|
||||
} `json:"result"`
|
||||
Id string `json:"_id"`
|
||||
}
|
||||
|
||||
type InteractiveAnswer struct {
|
||||
PageNo int `json:"pageNo"`
|
||||
PageSize int `json:"pageSize"`
|
||||
TotalRecord int `json:"totalRecord"`
|
||||
TotalPage int `json:"totalPage"`
|
||||
Results []InteractiveAnswerResults `json:"results"`
|
||||
Count bool `json:"count"`
|
||||
}
|
||||
|
||||
type InteractiveAnswerResults struct {
|
||||
EsId string `json:"esId" md:"-"`
|
||||
IndexId string `json:"indexId" md:"-"`
|
||||
ContentType int `json:"contentType" md:"-"`
|
||||
Trade []string `json:"trade" md:"行业名称"`
|
||||
MainContent string `json:"mainContent" md:"投资者提问"`
|
||||
StockCode string `json:"stockCode" md:"股票代码"`
|
||||
Secid string `json:"secid" md:"-"`
|
||||
CompanyShortName string `json:"companyShortName" md:"股票名称"`
|
||||
CompanyLogo string `json:"companyLogo,omitempty" md:"-"`
|
||||
BoardType []string `json:"boardType" md:"-"`
|
||||
PubDate string `json:"pubDate" md:"发布时间"`
|
||||
UpdateDate string `json:"updateDate" md:"-"`
|
||||
Author string `json:"author" md:"-"`
|
||||
AuthorName string `json:"authorName" md:"-"`
|
||||
PubClient string `json:"pubClient" md:"-"`
|
||||
AttachedId string `json:"attachedId" md:"-"`
|
||||
AttachedContent string `json:"attachedContent" md:"上市公司回复"`
|
||||
AttachedAuthor string `json:"attachedAuthor" md:"-"`
|
||||
AttachedPubDate string `json:"attachedPubDate" md:"回复时间"`
|
||||
Score float64 `json:"score" md:"-"`
|
||||
TopStatus int `json:"topStatus" md:"-"`
|
||||
PraiseCount int `json:"praiseCount" md:"-"`
|
||||
PraiseStatus bool `json:"praiseStatus" md:"-"`
|
||||
FavoriteStatus bool `json:"favoriteStatus" md:"-"`
|
||||
AttentionCompany bool `json:"attentionCompany" md:"-"`
|
||||
IsCheck string `json:"isCheck" md:"-"`
|
||||
QaStatus int `json:"qaStatus" md:"-"`
|
||||
PackageDate string `json:"packageDate" md:"-"`
|
||||
RemindStatus bool `json:"remindStatus" md:"-"`
|
||||
InterviewLive bool `json:"interviewLive" md:"-"`
|
||||
}
|
||||
|
||||
type CailianpressWeb struct {
|
||||
Total int `json:"total"`
|
||||
List []struct {
|
||||
Title string `json:"title" md:"资讯标题"`
|
||||
Ctime int `json:"ctime" md:"资讯时间"`
|
||||
Content string `json:"content" md:"资讯内容"`
|
||||
Author string `json:"author" md:"资讯发布者"`
|
||||
} `json:"list"`
|
||||
}
|
||||
|
||||
type BKDict struct {
|
||||
gorm.Model `md:"-"`
|
||||
BkCode string `json:"bkCode" md:"行业/板块代码"`
|
||||
BkName string `json:"bkName" md:"行业/板块名称"`
|
||||
FirstLetter string `json:"firstLetter" md:"first_letter"`
|
||||
FubkCode string `json:"fubkCode" md:"fubk_code"`
|
||||
PublishCode string `json:"publishCode" md:"publish_code"`
|
||||
}
|
||||
|
||||
func (b BKDict) TableName() string {
|
||||
return "bk_dict"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
@@ -217,7 +219,7 @@ func formatValue(value reflect.Value) string {
|
||||
}
|
||||
|
||||
// 基本类型
|
||||
return fmt.Sprintf("%v", value.Interface())
|
||||
return fmt.Sprintf("%s", strutil.RemoveNonPrintable(convertor.ToString(value.Interface())))
|
||||
}
|
||||
|
||||
// 示例结构体
|
||||
|
||||
10
frontend/auto-imports.d.ts
vendored
Normal file
10
frontend/auto-imports.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
|
||||
}
|
||||
45
frontend/components.d.ts
vendored
Normal file
45
frontend/components.d.ts
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
About: typeof import('./src/components/about.vue')['default']
|
||||
AgentChat: typeof import('./src/components/agent-chat.vue')['default']
|
||||
AgentChat_bk: typeof import('./src/components/agent-chat_bk.vue')['default']
|
||||
AnalyzeMartket: typeof import('./src/components/AnalyzeMartket.vue')['default']
|
||||
ClsCalendarTimeLine: typeof import('./src/components/ClsCalendarTimeLine.vue')['default']
|
||||
EmbeddedUrl: typeof import('./src/components/EmbeddedUrl.vue')['default']
|
||||
Fund: typeof import('./src/components/fund.vue')['default']
|
||||
HotEvents: typeof import('./src/components/HotEvents.vue')['default']
|
||||
HotStockList: typeof import('./src/components/HotStockList.vue')['default']
|
||||
HotTopics: typeof import('./src/components/HotTopics.vue')['default']
|
||||
IndustryMoneyRank: typeof import('./src/components/industryMoneyRank.vue')['default']
|
||||
IndustryResearchReportList: typeof import('./src/components/IndustryResearchReportList.vue')['default']
|
||||
InvestCalendarTimeLine: typeof import('./src/components/InvestCalendarTimeLine.vue')['default']
|
||||
KLineChart: typeof import('./src/components/KLineChart.vue')['default']
|
||||
LongTigerRankList: typeof import('./src/components/LongTigerRankList.vue')['default']
|
||||
Market: typeof import('./src/components/market.vue')['default']
|
||||
MoneyTrend: typeof import('./src/components/moneyTrend.vue')['default']
|
||||
NewsList: typeof import('./src/components/newsList.vue')['default']
|
||||
RankTable: typeof import('./src/components/rankTable.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SelectStock: typeof import('./src/components/SelectStock.vue')['default']
|
||||
Settings: typeof import('./src/components/settings.vue')['default']
|
||||
Stock: typeof import('./src/components/stock.vue')['default']
|
||||
Stockhotmap: typeof import('./src/components/stockhotmap.vue')['default']
|
||||
StockNoticeList: typeof import('./src/components/StockNoticeList.vue')['default']
|
||||
StockResearchReportList: typeof import('./src/components/StockResearchReportList.vue')['default']
|
||||
StockSparkLine: typeof import('./src/components/stockSparkLine.vue')['default']
|
||||
TChat: typeof import('@tdesign-vue-next/chat')['Chat']
|
||||
TChatAction: typeof import('@tdesign-vue-next/chat')['ChatAction']
|
||||
TChatContent: typeof import('@tdesign-vue-next/chat')['ChatContent']
|
||||
TChatLoading: typeof import('@tdesign-vue-next/chat')['ChatLoading']
|
||||
TChatSender: typeof import('@tdesign-vue-next/chat')['ChatSender']
|
||||
}
|
||||
}
|
||||
1302
frontend/package-lock.json
generated
1302
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tdesign-vue-next/chat": "^0.4.5",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@vavt/cm-extension": "^1.8.0",
|
||||
"@vavt/v3-extension": "^3.0.0",
|
||||
@@ -18,11 +19,13 @@
|
||||
"html2canvas": "^1.4.1",
|
||||
"lodash": "^4.17.21",
|
||||
"md-editor-v3": "^5.2.3",
|
||||
"vue": "^3.2.25",
|
||||
"tdesign-icons-vue-next": "^0.3.7",
|
||||
"vue": "^3.5.17",
|
||||
"vue-router": "^4.5.0",
|
||||
"vue3-danmaku": "^1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tdesign-vue-next/auto-import-resolver": "^0.1.1",
|
||||
"@vicons/antd": "^0.13.0",
|
||||
"@vicons/carbon": "^0.13.0",
|
||||
"@vicons/fa": "^0.13.0",
|
||||
@@ -31,11 +34,14 @@
|
||||
"@vicons/ionicons5": "^0.13.0",
|
||||
"@vicons/material": "^0.13.0",
|
||||
"@vicons/tabler": "^0.13.0",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"html-docx-js-typescript": "^0.1.5",
|
||||
"naive-ui": "^2.41.0",
|
||||
"less": "^4.4.0",
|
||||
"naive-ui": "^2.43.2",
|
||||
"unplugin-auto-import": "^20.0.0",
|
||||
"unplugin-vue-components": "^29.0.0",
|
||||
"vfonts": "^0.0.3",
|
||||
"vite": "^6.3.5"
|
||||
"vite": "7.2.4"
|
||||
},
|
||||
"keywords": [
|
||||
"AI赋能股票分析",
|
||||
|
||||
@@ -1 +1 @@
|
||||
2d63c3a999d797889c01d6c96451b197
|
||||
f4fb0059ba6044c039be717fcc2e40bc
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
Quit,
|
||||
WindowFullscreen,
|
||||
WindowHide,
|
||||
WindowUnfullscreen
|
||||
WindowUnfullscreen,
|
||||
WindowSetTitle
|
||||
} from '../wailsjs/runtime'
|
||||
import {h, onBeforeMount, onBeforeUnmount, onMounted, ref} from "vue";
|
||||
import {RouterLink, useRouter} from 'vue-router'
|
||||
@@ -28,7 +29,7 @@ import {
|
||||
Wallet, WarningOutline,
|
||||
} from '@vicons/ionicons5'
|
||||
import {AnalyzeSentiment, GetConfig, GetGroupList,GetVersionInfo} from "../wailsjs/go/main/App";
|
||||
import {Dragon, Fire, Gripfire} from "@vicons/fa";
|
||||
import {Dragon, Fire, FirefoxBrowser, Gripfire, Robot} from "@vicons/fa";
|
||||
import {ReportSearch} from "@vicons/tabler";
|
||||
import {LocalFireDepartmentRound} from "@vicons/material";
|
||||
import {BoxSearch20Regular, CommentNote20Filled} from "@vicons/fluent";
|
||||
@@ -43,6 +44,7 @@ const loadingMsg = ref("加载数据中...")
|
||||
const enableNews = ref(false)
|
||||
const contentStyle = ref("")
|
||||
const enableFund = ref(false)
|
||||
const enableAgent = ref(false)
|
||||
const enableDarkTheme = ref(null)
|
||||
const content = ref('未经授权,禁止商业目的!\n\n数据来源于网络,仅供参考;投资有风险,入市需谨慎')
|
||||
const isFullscreen = ref(false)
|
||||
@@ -51,6 +53,7 @@ const containerRef = ref({})
|
||||
const realtimeProfit = ref(0)
|
||||
const telegraph = ref([])
|
||||
const groupList = ref([])
|
||||
const officialStatement= ref("")
|
||||
const menuOptions = ref([
|
||||
{
|
||||
label: () =>
|
||||
@@ -369,6 +372,28 @@ const menuOptions = ref([
|
||||
key: 'market11',
|
||||
icon: renderIcon(BoxSearch20Regular),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "名站优选",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'market'
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '名站优选'})
|
||||
},
|
||||
},
|
||||
{default: () => '名站优选',}
|
||||
),
|
||||
key: 'market12',
|
||||
icon: renderIcon(FirefoxBrowser),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -400,6 +425,27 @@ const menuOptions = ref([
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'agent',
|
||||
query: {
|
||||
name:"Ai智能体",
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'agent'
|
||||
},
|
||||
}
|
||||
},
|
||||
{default: () => 'Ai智能体'}
|
||||
),
|
||||
key: 'agent',
|
||||
show:enableAgent.value,
|
||||
icon: renderIcon(Robot),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
@@ -441,6 +487,7 @@ const menuOptions = ref([
|
||||
icon: renderIcon(LogoGithub),
|
||||
},
|
||||
{
|
||||
show:false,
|
||||
label: () => h("a", {
|
||||
href: '#',
|
||||
onClick: toggleFullscreen,
|
||||
@@ -553,6 +600,7 @@ onBeforeMount(() => {
|
||||
GetVersionInfo().then(result => {
|
||||
if(result.officialStatement){
|
||||
content.value = result.officialStatement+"\n\n"+content.value
|
||||
officialStatement.value = result.officialStatement
|
||||
}
|
||||
})
|
||||
|
||||
@@ -603,11 +651,15 @@ onBeforeMount(() => {
|
||||
GetConfig().then((res) => {
|
||||
//console.log(res)
|
||||
enableFund.value = res.enableFund
|
||||
enableAgent.value = res.enableAgent
|
||||
|
||||
menuOptions.value.filter((item) => {
|
||||
if (item.key === 'fund') {
|
||||
item.show = res.enableFund
|
||||
}
|
||||
if (item.key === 'agent') {
|
||||
item.show = res.enableAgent
|
||||
}
|
||||
})
|
||||
|
||||
if (res.darkTheme) {
|
||||
@@ -619,12 +671,14 @@ onBeforeMount(() => {
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
WindowSetTitle("go-stock:AI赋能股票分析✨ "+officialStatement.value+" 未经授权,禁止商业目的! [数据来源于网络,仅供参考;投资有风险,入市需谨慎]")
|
||||
contentStyle.value = "max-height: calc(92vh);overflow: hidden"
|
||||
GetConfig().then((res) => {
|
||||
if (res.enableNews) {
|
||||
enableNews.value = true
|
||||
}
|
||||
enableFund.value = res.enableFund
|
||||
enableAgent.value = res.enableAgent
|
||||
const {notification } =createDiscreteApi(["notification"], {
|
||||
configProviderProps: {
|
||||
theme: enableDarkTheme.value ? darkTheme : lightTheme ,
|
||||
@@ -671,7 +725,7 @@ onMounted(() => {
|
||||
<n-modal-provider>
|
||||
<n-dialog-provider>
|
||||
<n-watermark
|
||||
:content="content"
|
||||
:content="''"
|
||||
cross
|
||||
selectable
|
||||
:font-size="16"
|
||||
@@ -702,7 +756,7 @@ onMounted(() => {
|
||||
</n-spin>
|
||||
</n-gi>
|
||||
<n-gi style="position: fixed;bottom:0;z-index: 9;width: 100%;">
|
||||
<n-card size="small" style="--wails-draggable:drag">
|
||||
<n-card size="small" style="--wails-draggable:no-drag">
|
||||
<n-menu style="font-size: 18px;"
|
||||
v-model:value="activeKey"
|
||||
mode="horizontal"
|
||||
|
||||
317
frontend/src/components/AnalyzeMartket.vue
Normal file
317
frontend/src/components/AnalyzeMartket.vue
Normal file
@@ -0,0 +1,317 @@
|
||||
<script setup>
|
||||
|
||||
import {AnalyzeSentimentWithFreqWeight,GlobalStockIndexes} from "../../wailsjs/go/main/App";
|
||||
import * as echarts from "echarts";
|
||||
import {onMounted,onUnmounted, ref} from "vue";
|
||||
import _ from "lodash";
|
||||
const { name,darkTheme,kDays ,chartHeight} = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
kDays: {
|
||||
type: Number,
|
||||
default: 14
|
||||
},
|
||||
chartHeight: {
|
||||
type: Number,
|
||||
default: 500
|
||||
},
|
||||
darkTheme: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
const common = ref([])
|
||||
const america = ref([])
|
||||
const europe = ref([])
|
||||
const asia = ref([])
|
||||
const mainIndex = ref([])
|
||||
const chinaIndex = ref([])
|
||||
const other = ref([])
|
||||
const globalStockIndexes = ref(null)
|
||||
const chartRef = ref(null);
|
||||
const gaugeChartRef = ref(null);
|
||||
const triggerAreas=ref(["main","extra","arrow"])
|
||||
let handleChartInterval=null
|
||||
let handleIndexInterval=null
|
||||
onMounted(() => {
|
||||
handleChart()
|
||||
getIndex()
|
||||
handleChartInterval=setInterval(function () {
|
||||
handleChart()
|
||||
}, 1000 * 60)
|
||||
|
||||
handleIndexInterval=setInterval(function () {
|
||||
getIndex()
|
||||
}, 1000 * 2)
|
||||
})
|
||||
|
||||
onUnmounted(()=>{
|
||||
clearInterval(handleChartInterval)
|
||||
clearInterval(handleIndexInterval)
|
||||
})
|
||||
|
||||
function getIndex() {
|
||||
GlobalStockIndexes().then((res) => {
|
||||
globalStockIndexes.value = res
|
||||
common.value = res["common"]
|
||||
america.value = res["america"]
|
||||
europe.value = res["europe"]
|
||||
asia.value = res["asia"]
|
||||
other.value = res["other"]
|
||||
mainIndex.value=asia.value.filter(function (item) {
|
||||
return ['上海',"深圳","香港","台湾","北京","东京","首尔","纽约","纳斯达克"].includes(item.location)
|
||||
}).concat(america.value.filter(function (item) {
|
||||
return ['上海',"深圳","香港","台湾","北京","东京","首尔","纽约","纳斯达克"].includes(item.location)
|
||||
}))
|
||||
|
||||
chinaIndex.value=asia.value.filter(function (item) {
|
||||
return ['上海',"深圳","香港","台湾","北京"].includes(item.location)
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
function handleChart(){
|
||||
const formatUtil = echarts.format;
|
||||
AnalyzeSentimentWithFreqWeight("").then((res) => {
|
||||
//console.log(res)
|
||||
const treemapchart = echarts.init(chartRef.value);
|
||||
const gaugeChart=echarts.init(gaugeChartRef.value);
|
||||
let data = res['frequencies'].map(item => ({
|
||||
name: item.Word,
|
||||
// value: item.Frequency,
|
||||
// value: item.Weight,
|
||||
frequency: item.Frequency,
|
||||
weight: item.Weight,
|
||||
value: item.Score,
|
||||
}));
|
||||
|
||||
let data2 = res['frequencies'].map(item => ({
|
||||
name: item.Word,
|
||||
value: item.Frequency,
|
||||
// value: item.Weight,
|
||||
frequency: item.Frequency,
|
||||
weight: item.Weight,
|
||||
//value: item.Score,
|
||||
}));
|
||||
|
||||
let data3 = res['frequencies'].map(item => ({
|
||||
name: item.Word,
|
||||
//value: item.Frequency,
|
||||
value: item.Weight,
|
||||
frequency: item.Frequency,
|
||||
weight: item.Weight,
|
||||
//value: item.Score,
|
||||
}));
|
||||
|
||||
let option = {
|
||||
darkMode: darkTheme,
|
||||
title: {
|
||||
text:name,
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
color: darkTheme?'#ccc':'#456'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
show: false
|
||||
},
|
||||
toolbox: {
|
||||
left: '20px',
|
||||
tooltip:{
|
||||
textStyle: {
|
||||
color: darkTheme?'#ccc':'#456'
|
||||
}
|
||||
},
|
||||
feature: {
|
||||
saveAsImage: {title: '保存图片'},
|
||||
restore: {
|
||||
title: '默认',
|
||||
},
|
||||
myTool2: {
|
||||
show: true,
|
||||
title: '按权重',
|
||||
icon:"path://M393.8816 148.1216a29.3376 29.3376 0 0 1-15.2576 38.0928c-43.776 17.152-81.92 43.8272-114.2784 76.2368A345.7536 345.7536 0 0 0 159.5392 512 352.8704 352.8704 0 0 0 512 864.4608a351.744 351.744 0 0 0 249.5488-102.912 353.536 353.536 0 0 0 76.2368-114.2784c5.6832-15.2576 22.8352-20.992 38.0928-15.2576 15.2576 5.7344 20.992 22.8864 15.2576 38.0928a421.2224 421.2224 0 0 1-89.6 133.376A412.6208 412.6208 0 0 1 512 921.6c-226.7136 0-409.6-182.8864-409.6-409.6 0-108.544 41.9328-211.456 120.0128-289.5872A421.2224 421.2224 0 0 1 355.84 132.864a29.3376 29.3376 0 0 1 38.0928 15.2576zM512 102.4c226.7136 0 409.6 182.8864 409.6 409.6 0 15.2576-13.312 28.5696-28.5696 28.5696H512A29.2864 29.2864 0 0 1 483.4304 512V130.9696c0-15.2576 13.312-28.5696 28.5696-28.5696z m28.5696 59.0336v321.9968h321.9968a350.976 350.976 0 0 0-321.9968-321.9968z",
|
||||
onclick: function (){
|
||||
treemapchart.setOption( {series:{
|
||||
data: data3
|
||||
}})
|
||||
}
|
||||
},
|
||||
myTool1: {
|
||||
show: true,
|
||||
title: '按频次',
|
||||
icon:"path://M895.466667 476.8l-87.424-87.424v-123.626667a49.770667 49.770667 0 0 0-49.770667-49.770666h-123.626667L547.2 128.533333a49.792 49.792 0 0 0-70.4 0l-87.424 87.424h-123.626667a49.770667 49.770667 0 0 0-49.770666 49.770667v123.626667L128.533333 476.8a49.792 49.792 0 0 0 0 70.4l87.424 87.424v123.626667a49.770667 49.770667 0 0 0 49.770667 49.770666h123.626667l87.424 87.424a49.792 49.792 0 0 0 70.4 0l87.424-87.424h123.626666a49.770667 49.770667 0 0 0 49.770667-49.770666v-123.626667l87.424-87.424a49.749333 49.749333 0 0 0 0.042667-70.4z m-137.216 137.194667v144.256h-144.256L512 860.266667l-101.994667-101.994667h-144.256v-144.256L163.733333 512l101.994667-101.994667v-144.256h144.256L512 163.733333l101.994667 101.994667h144.256v144.256L860.266667 512l-102.016 101.994667z M414.378667 514.730667l28.672 10.922666c-18.090667 47.445333-38.229333 92.16-60.757334 133.802667l-30.037333-13.653333a1042.133333 1042.133333 0 0 0 62.122667-131.072zM381.952 367.616L355.669333 384c25.258667 26.282667 45.056 50.176 60.074667 72.021333l25.6-17.749333c-13.994667-20.48-33.792-44.032-59.392-70.656zM537.258667 455.338667c-0.682667 43.690667-6.144 79.189333-16.725334 106.837333-14.336 32.768-44.373333 60.416-89.429333 82.944l21.162667 25.941333c52.224-26.624 85.333333-60.074667 99.328-100.693333 1.706667-5.12 3.413333-10.24 4.778666-15.36 21.504 45.738667 52.906667 83.968 93.866667 115.370667l21.504-24.917334c-51.2-34.474667-86.357333-81.237333-105.813333-140.288 1.706667-15.701333 2.730667-32.085333 2.730666-49.834666h-31.402666z M508.586667 434.858667h115.712c-6.826667 25.258667-15.018667 47.786667-24.917334 66.901333l31.744 8.874667a627.008 627.008 0 0 0 27.989334-85.674667v-21.162667H517.12c3.413333-14.336 6.144-29.354667 8.874667-45.738666l-32.426667-5.12c-7.850667 59.392-25.6 105.813333-52.906667 139.264l26.965334 19.114666c16.725333-19.114667 30.378667-44.373333 40.96-76.458666z",
|
||||
onclick: function (){
|
||||
treemapchart.setOption( {series:{
|
||||
data: data2
|
||||
}})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
formatter: function (info) {
|
||||
var value = info.value.toFixed(2);
|
||||
var frequency = info.data.frequency;
|
||||
var weight = info.data.weight;
|
||||
return [
|
||||
'<div class="tooltip-title">' + info.name+ '</div>',
|
||||
'热度: ' + formatUtil.addCommas(value) + '',
|
||||
'<div class="tooltip-title">频次: ' + formatUtil.addCommas(frequency)+ '</div>',
|
||||
'<div class="tooltip-title">权重: ' + formatUtil.addCommas(weight)+ '</div>',
|
||||
].join('');
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'treemap',
|
||||
breadcrumb:{show: false},
|
||||
left: '0',
|
||||
top: '40',
|
||||
right: '0',
|
||||
bottom: '0',
|
||||
tooltip: {
|
||||
show: true
|
||||
},
|
||||
data: data
|
||||
}
|
||||
]
|
||||
};
|
||||
treemapchart.setOption(option);
|
||||
|
||||
|
||||
|
||||
let option2 = {
|
||||
darkMode: darkTheme,
|
||||
series: [
|
||||
{
|
||||
type: 'gauge',
|
||||
startAngle: 180,
|
||||
endAngle: 0,
|
||||
center: ['50%', '75%'],
|
||||
radius: '90%',
|
||||
min: -100,
|
||||
max: 100,
|
||||
splitNumber: 8,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
width: 6,
|
||||
color: [
|
||||
// [0.25, '#FF6E76'],
|
||||
// [0.5, '#FDDD60'],
|
||||
// [0.75, '#58D9F9'],
|
||||
// [1, '#7CFFB2'],
|
||||
|
||||
[0.25, '#03fb6a'],
|
||||
[0.5, '#58e1f9'],
|
||||
[0.75, '#ef5922'],
|
||||
[1, '#f11d29'],
|
||||
|
||||
]
|
||||
}
|
||||
},
|
||||
pointer: {
|
||||
icon: 'path://M12.8,0.7l12,40.1H0.7L12.8,0.7z',
|
||||
length: '12%',
|
||||
width: 20,
|
||||
offsetCenter: [0, '-60%'],
|
||||
itemStyle: {
|
||||
color: 'auto'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
length: 12,
|
||||
lineStyle: {
|
||||
color: 'auto',
|
||||
width: 2
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
length: 20,
|
||||
lineStyle: {
|
||||
color: 'auto',
|
||||
width: 5
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: darkTheme?'#ccc':'#456',
|
||||
fontSize: 20,
|
||||
distance: -45,
|
||||
rotate: 'tangential',
|
||||
formatter: function (value) {
|
||||
if (value ===100) {
|
||||
return '极热';
|
||||
} else if (value === 50) {
|
||||
return '乐观';
|
||||
} else if (value === 0) {
|
||||
return '中性';
|
||||
}else if (value === -50) {
|
||||
return '谨慎';
|
||||
} else if (value === -100) {
|
||||
return '冰点';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
},
|
||||
title: {
|
||||
offsetCenter: [0, '-10%'],
|
||||
fontSize: 20
|
||||
},
|
||||
detail: {
|
||||
fontSize: 30,
|
||||
offsetCenter: [0, '-35%'],
|
||||
valueAnimation: true,
|
||||
formatter: function (value) {
|
||||
return value.toFixed(2) + '';
|
||||
},
|
||||
color: 'inherit'
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: res.result.Score*0.2,
|
||||
name: '市场情绪强弱'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
gaugeChart.setOption(option2);
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-collapse :trigger-areas="triggerAreas" :default-expanded-names="['1']" display-directive="show">
|
||||
<n-collapse-item name="1" >
|
||||
<template #header>
|
||||
<n-flex>
|
||||
<n-tag size="small" :bordered="false" v-for="(item, index) in mainIndex" :type="item.zdf>0?'error':'success'">
|
||||
<n-flex>
|
||||
<n-image :width="20" :src="item.img" />
|
||||
<n-text style="font-size: 14px" :type="item.zdf>0?'error':'success'">{{item.name}} {{item.zxj}}</n-text>
|
||||
<n-number-animation :precision="2" :from="0" :to="item.zdf" style="font-size: 14px"/>
|
||||
<n-text style="margin-left: -12px;font-size: 14px" :type="item.zdf>0?'error':'success'">%</n-text>
|
||||
</n-flex>
|
||||
</n-tag>
|
||||
</n-flex>
|
||||
</template>
|
||||
<template #header-extra>
|
||||
主要股指
|
||||
</template>
|
||||
<n-grid :cols="24" :y-gap="0">
|
||||
<n-gi span="6">
|
||||
<div ref="gaugeChartRef" style="width: 100%;height: auto;--wails-draggable:no-drag" :style="{height:chartHeight+'px'}" ></div>
|
||||
</n-gi>
|
||||
<n-gi span="18">
|
||||
<div ref="chartRef" style="width: 100%;height: auto;--wails-draggable:no-drag" :style="{height:chartHeight+'px'}" ></div>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import {h, onBeforeMount, onMounted, onUnmounted, ref} from 'vue'
|
||||
import {SearchStock, GetHotStrategy, OpenURL} from "../../wailsjs/go/main/App";
|
||||
import {SearchStock, GetHotStrategy, OpenURL, Follow, GetFollowList} from "../../wailsjs/go/main/App";
|
||||
import {useMessage, NText, NTag, NButton} from 'naive-ui'
|
||||
import {Environment} from "../../wailsjs/runtime"
|
||||
import {RefreshCircleSharp} from "@vicons/ionicons5";
|
||||
import {EventsEmit} from "../../wailsjs/runtime";
|
||||
|
||||
const message = useMessage()
|
||||
const search = ref('')
|
||||
@@ -11,6 +12,32 @@ const columns = ref([])
|
||||
const dataList = ref([])
|
||||
const hotStrategy = ref([])
|
||||
const traceInfo = ref('')
|
||||
const tableScrollX = ref(2800) // 默认滚动宽度
|
||||
|
||||
// 计算表格总宽度
|
||||
function calculateTableWidth(cols) {
|
||||
let totalWidth = 0;
|
||||
|
||||
cols.forEach(col => {
|
||||
if (col.children && col.children.length > 0) {
|
||||
// 有子列的情况
|
||||
let childrenWidth = 0;
|
||||
col.children.forEach(child => {
|
||||
childrenWidth += child.width || child.minWidth || 100;
|
||||
});
|
||||
// 取标题列宽度和子列总宽度的较大值
|
||||
totalWidth += Math.max(col.width || col.minWidth || 200, childrenWidth);
|
||||
} else {
|
||||
// 没有子列的情况
|
||||
totalWidth += col.width || col.minWidth || 120;
|
||||
}
|
||||
});
|
||||
|
||||
// 加上操作列的宽度
|
||||
totalWidth += 100;
|
||||
|
||||
return Math.max(totalWidth, 1200); // 最小宽度1200
|
||||
}
|
||||
|
||||
function Search() {
|
||||
if (!search.value) {
|
||||
@@ -71,19 +98,57 @@ function Search() {
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
columns.value.push({
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 80,
|
||||
fixed: 'right', // 固定在右侧
|
||||
render: (row) => {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
strong: true,
|
||||
tertiary: true,
|
||||
size: 'small',
|
||||
type: 'warning', // 橙色按钮
|
||||
style: 'font-size: 14px; padding: 0 10px;', // 稍微大一点的按钮
|
||||
onClick: () => handleFollow(row)
|
||||
},
|
||||
{ default: () => '关注' }
|
||||
)
|
||||
}
|
||||
});
|
||||
dataList.value = res.data.result.dataList
|
||||
console.log("sss"+columns.value. length)
|
||||
// 计算并设置表格宽度
|
||||
tableScrollX.value = calculateTableWidth(columns.value);
|
||||
} else {
|
||||
message.error(res.msg)
|
||||
if(res.msg){
|
||||
message.error(res.msg)
|
||||
}
|
||||
if(res.message){
|
||||
message.error(res.message)
|
||||
}
|
||||
}
|
||||
}).catch(err => {
|
||||
message.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
// 修改handleFollow方法,使用stock.vue的AddStock逻辑
|
||||
function handleFollow(row) {
|
||||
let code=row.MARKET_SHORT_NAME.toLowerCase()+row.SECURITY_CODE
|
||||
Follow(code).then(result => {
|
||||
if (result === "关注成功") {
|
||||
message.success(result)
|
||||
} else {
|
||||
message.error(result)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isNumeric(value) {
|
||||
return !isNaN(parseFloat(value)) && isFinite(value);
|
||||
}
|
||||
@@ -188,7 +253,7 @@ function openCenteredWindow(url, width, height) {
|
||||
:columns="columns"
|
||||
:data="dataList"
|
||||
:pagination="{pageSize: 10}"
|
||||
:scroll-x="1800"
|
||||
:scroll-x="tableScrollX"
|
||||
:render-cell="(value, rowData, column) => {
|
||||
|
||||
if(column.key=='SECURITY_CODE'||column.key=='SERIAL'){
|
||||
|
||||
@@ -5,7 +5,9 @@ import 'md-editor-v3/lib/preview.css';
|
||||
import {h, onBeforeUnmount, onMounted, ref} from 'vue';
|
||||
import {CheckUpdate, GetVersionInfo,GetSponsorInfo,OpenURL} from "../../wailsjs/go/main/App";
|
||||
import {EventsOff, EventsOn,Environment} from "../../wailsjs/runtime";
|
||||
import {NAvatar, NButton, useNotification} from "naive-ui";
|
||||
import {NAvatar, NButton, useNotification,NText} from "naive-ui";
|
||||
import { addMonths, format ,parse} from 'date-fns';
|
||||
import { zhCN } from 'date-fns/locale';
|
||||
const updateLog = ref('');
|
||||
const versionInfo = ref('');
|
||||
const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/master/build/appicon.png');
|
||||
@@ -16,6 +18,7 @@ const notify = useNotification()
|
||||
const vipLevel=ref("");
|
||||
const vipStartTime=ref("");
|
||||
const vipEndTime=ref("");
|
||||
const expired=ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
document.title = '关于软件';
|
||||
@@ -31,6 +34,13 @@ onMounted(() => {
|
||||
vipLevel.value = res.vipLevel;
|
||||
vipStartTime.value = res.vipStartTime;
|
||||
vipEndTime.value = res.vipEndTime;
|
||||
//判断时间是否到期
|
||||
if (res.vipLevel) {
|
||||
if (res.vipEndTime < format(new Date(), 'yyyy-MM-dd HH:mm:ss')) {
|
||||
notify.warning({content: 'VIP已到期'})
|
||||
expired.value = true;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
});
|
||||
@@ -104,7 +114,7 @@ EventsOn("updateVersion",async (msg) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-space vertical size="large" style="--wails-draggable:drag">
|
||||
<n-space vertical size="large" style="--wails-draggable:no-drag">
|
||||
<!-- 软件描述 -->
|
||||
<n-card size="large">
|
||||
<n-divider title-placement="center">关于软件</n-divider>
|
||||
@@ -115,10 +125,10 @@ EventsOn("updateVersion",async (msg) => {
|
||||
<n-gradient-text type="info" :size="50" >go-stock</n-gradient-text>
|
||||
</n-badge>
|
||||
<n-badge v-if="vipLevel" :value="versionInfo" :offset="[50,10]" type="success">
|
||||
<n-gradient-text type="warning" :size="50" >go-stock</n-gradient-text><n-tag :bordered="false" size="small" type="warning">VIP{{vipLevel}}</n-tag>
|
||||
<n-gradient-text :type="expired?'error':'warning'" :size="50" >go-stock</n-gradient-text><n-tag :bordered="false" size="small" type="warning">VIP{{vipLevel}}</n-tag>
|
||||
</n-badge>
|
||||
</h1>
|
||||
<n-gradient-text type="warning" v-if="vipLevel" >vip到期时间:{{vipEndTime}}</n-gradient-text>
|
||||
<n-gradient-text :type="expired?'error':'warning'" v-if="vipLevel" >vip到期时间:{{vipEndTime}}</n-gradient-text>
|
||||
<n-button size="tiny" @click="CheckUpdate(1)" type="info" tertiary >检查更新</n-button>
|
||||
<div style="justify-self: center;text-align: left" >
|
||||
<p>自选股行情实时监控,基于Wails和NaiveUI构建的AI赋能股票分析工具</p>
|
||||
|
||||
365
frontend/src/components/agent-chat.vue
Normal file
365
frontend/src/components/agent-chat.vue
Normal file
@@ -0,0 +1,365 @@
|
||||
<template>
|
||||
<div class="chat-box">
|
||||
<t-chat
|
||||
ref="chatRef"
|
||||
:clear-history="chatList.length > 0 && !isStreamLoad"
|
||||
:data="chatList"
|
||||
:text-loading="loading"
|
||||
:is-stream-load="isStreamLoad"
|
||||
style="height: 100%"
|
||||
@scroll="handleChatScroll"
|
||||
@clear="clearConfirm"
|
||||
>
|
||||
<!-- eslint-disable vue/no-unused-vars -->
|
||||
<template #content="{ item, index }">
|
||||
<t-chat-reasoning v-if="item.role === 'assistant'" expand-icon-placement="right">
|
||||
<t-chat-loading v-if="isStreamLoad" text="思考中..." />
|
||||
<t-chat-content v-if="item.reasoning.length > 0" :content="item.reasoning" />
|
||||
</t-chat-reasoning>
|
||||
<t-chat-content v-if="item.content.length > 0" :content="item.content" />
|
||||
</template>
|
||||
<template #actions="{ item, index }">
|
||||
<t-chat-action
|
||||
:content="item.content"
|
||||
:operation-btn="['copy']"
|
||||
@operation="handleOperation"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<!-- <t-chat-input :stop-disabled="isStreamLoad" @send="inputEnter" @stop="onStop"> </t-chat-input>-->
|
||||
<t-chat-sender
|
||||
ref="chatSenderRef"
|
||||
v-model="inputValue"
|
||||
class="chat-sender"
|
||||
:textarea-props="{
|
||||
placeholder: '请输入消息...',
|
||||
}"
|
||||
:loading="loading"
|
||||
:stop-disabled="isStreamLoad"
|
||||
@send="inputEnter"
|
||||
@stop="onStop"
|
||||
>
|
||||
<template #suffix>
|
||||
<!-- 监听键盘回车发送事件需要在sender组件监听 -->
|
||||
<t-button theme="default" variant="text" size="large" class="btn" @click="inputEnter"> 发送 </t-button>
|
||||
</template>
|
||||
<template #prefix>
|
||||
<NFlex>
|
||||
<NSelect
|
||||
v-model:value="selectValue"
|
||||
:options="selectOptions"
|
||||
label-field="name" value-field="ID"
|
||||
size="tiny"
|
||||
style="width: 200px;"
|
||||
/>
|
||||
</NFlex>
|
||||
</template>
|
||||
</t-chat-sender>
|
||||
|
||||
</template>
|
||||
</t-chat>
|
||||
<t-button v-show="isShowToBottom" variant="text" class="bottomBtn" @click="backBottom">
|
||||
<div class="to-bottom">
|
||||
<ArrowDownIcon />
|
||||
</div>
|
||||
</t-button>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {ref, onMounted, h, onBeforeUnmount, onBeforeMount} from 'vue';
|
||||
import {ArrowDownIcon, CheckCircleIcon, SystemSumIcon} from 'tdesign-icons-vue-next';
|
||||
const fetchCancel = ref(null);
|
||||
const loading = ref(false);
|
||||
|
||||
const inputValue = ref('');
|
||||
// 流式数据加载中
|
||||
const isStreamLoad = ref(false);
|
||||
|
||||
const chatRef = ref(null);
|
||||
const isShowToBottom = ref(false);
|
||||
|
||||
const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/master/build/appicon.png');
|
||||
import {darkTheme, NFlex, NImage,NSelect} from "naive-ui";
|
||||
import {ChatWithAgent, GetAiConfigs, GetConfig, GetSponsorInfo, GetVersionInfo} from "../../wailsjs/go/main/App";
|
||||
import {EventsOff, EventsOn} from '../../wailsjs/runtime'
|
||||
import 'tdesign-vue-next/es/style/index.css';
|
||||
|
||||
|
||||
const allowToolTip = ref(true);
|
||||
const chatSenderRef = ref(null);
|
||||
const selectOptions = ref([]);
|
||||
const selectValue = ref("default");
|
||||
onBeforeUnmount(() => {
|
||||
EventsOff("agent-message")
|
||||
})
|
||||
EventsOn("agent-message", (data) => {
|
||||
console.log(data)
|
||||
if(data['role']==="assistant"){
|
||||
loading.value = false;
|
||||
const lastItem = chatList.value[0];
|
||||
if (data['reasoning_content']){
|
||||
lastItem.reasoning += data['reasoning_content'];
|
||||
}
|
||||
if (data['content']){
|
||||
lastItem.content +=data['content'];
|
||||
}
|
||||
if(data['tool_calls']){
|
||||
for (const tool of data['tool_calls']) {
|
||||
console.log(tool.id, tool.type, tool.function.name, tool.function.arguments);
|
||||
lastItem.reasoning += "\n```"+tool.function.name+"\n" +
|
||||
"参数:"+ (tool.function.arguments?tool.function.arguments:"无")+
|
||||
"\n```\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
if(data['response_meta']&&data['response_meta'].finish_reason==="stop"){
|
||||
isStreamLoad.value = false;
|
||||
loading.value = false;
|
||||
}
|
||||
})
|
||||
onBeforeMount(() => {
|
||||
GetAiConfigs().then(res=>{
|
||||
console.log(res)
|
||||
selectOptions.value = res
|
||||
selectValue.value = res[0].ID
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
//chatRef.value.scrollToBottom();
|
||||
|
||||
GetConfig().then((res) => {
|
||||
if (res.darkTheme) {
|
||||
document.documentElement.setAttribute("theme-mode", "dark");
|
||||
} else {
|
||||
document.documentElement.removeAttribute("theme-mode"); }
|
||||
})
|
||||
|
||||
|
||||
GetVersionInfo().then((res) => {
|
||||
icon.value = res.icon;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// 滚动到底部
|
||||
const backBottom = () => {
|
||||
chatRef.value.scrollToBottom({
|
||||
behavior: 'smooth',
|
||||
});
|
||||
};
|
||||
// 是否显示回到底部按钮
|
||||
const handleChatScroll = function ({ e }) {
|
||||
const scrollTop = e.target.scrollTop;
|
||||
isShowToBottom.value = scrollTop < 0;
|
||||
};
|
||||
// 清空消息
|
||||
const clearConfirm = function () {
|
||||
chatList.value = [];
|
||||
};
|
||||
const handleOperation = function (type, options) {
|
||||
console.log('handleOperation', type, options);
|
||||
};
|
||||
// 倒序渲染
|
||||
const chatList = ref([
|
||||
// {
|
||||
// content: `模型由<span>hunyuan</span>变为<span>GPT4</span>`,
|
||||
// role: 'model-change',
|
||||
// reasoning: '',
|
||||
// },
|
||||
{
|
||||
avatar: h(NImage, { src: icon.value, height: '48px', width: '48px'}),
|
||||
name: 'Go-Stock AI',
|
||||
datetime: '',
|
||||
reasoning: '',
|
||||
content: '我是您的AI赋能股票分析助手,您可以问我任何关于股票投资方面的问题。',
|
||||
role: 'assistant',
|
||||
duration: 10,
|
||||
},
|
||||
{
|
||||
avatar: 'https://tdesign.gtimg.com/site/avatar.jpg',
|
||||
name: '宇宙无敌大韭菜',
|
||||
datetime: '',
|
||||
content: '介绍下自己?',
|
||||
role: 'user',
|
||||
reasoning: '',
|
||||
},
|
||||
]);
|
||||
|
||||
const onStop = function () {
|
||||
if (fetchCancel.value) {
|
||||
fetchCancel.value.controller.close();
|
||||
loading.value = false;
|
||||
isStreamLoad.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const inputEnter = function () {
|
||||
if (isStreamLoad.value) {
|
||||
return;
|
||||
}
|
||||
if (!inputValue.value) return;
|
||||
const params = {
|
||||
avatar: 'https://tdesign.gtimg.com/site/avatar.jpg',
|
||||
name: '宇宙无敌大韭菜',
|
||||
datetime: new Date().toDateString(),
|
||||
content: inputValue.value,
|
||||
role: 'user',
|
||||
};
|
||||
chatList.value.unshift(params);
|
||||
// 空消息占位
|
||||
const params2 = {
|
||||
avatar: h(NImage, { src: icon.value, height: '48px', width: '48px'}),
|
||||
name: 'Go-Stock AI',
|
||||
datetime: new Date().toDateString(),
|
||||
content: '',
|
||||
reasoning: '',
|
||||
role: 'assistant',
|
||||
};
|
||||
chatList.value.unshift(params2);
|
||||
loading.value = true;
|
||||
isStreamLoad.value = true;
|
||||
ChatWithAgent(inputValue.value,selectValue.value,0)
|
||||
};
|
||||
</script>
|
||||
<style lang="less">
|
||||
/* 应用滚动条样式 */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: var(--td-scrollbar-color);
|
||||
}
|
||||
::-webkit-scrollbar-thumb:horizontal:hover {
|
||||
background-color: var(--td-scrollbar-hover-color);
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: var(--td-scroll-track-color);
|
||||
}
|
||||
.chat-box {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
margin: 5px 10px 5px 10px;
|
||||
text-align: left;
|
||||
.bottomBtn {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
margin-left: -20px;
|
||||
bottom: 210px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, 0.08), 0px 16px 24px 2px rgba(0, 0, 0, 0.04),
|
||||
0px 6px 30px 5px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.to-bottom {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 1px solid #dcdcdc;
|
||||
box-sizing: border-box;
|
||||
background: var(--td-bg-color-container);
|
||||
border-radius: 50%;
|
||||
font-size: 24px;
|
||||
line-height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.t-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.model-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.t-select {
|
||||
width: 112px;
|
||||
height: 32px;
|
||||
margin-right: 8px;
|
||||
.t-input {
|
||||
border-radius: 32px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
.check-box {
|
||||
width: 112px;
|
||||
height: 32px;
|
||||
border-radius: 32px;
|
||||
border: 0;
|
||||
background: #e7e7e7;
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
box-sizing: border-box;
|
||||
flex: 0 0 auto;
|
||||
.t-button__text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
span {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.check-box.is-active {
|
||||
border: 1px solid #d9e1ff;
|
||||
background: #f2f3ff;
|
||||
color: var(--td-brand-color);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.chat-sender {
|
||||
.btn {
|
||||
color: var(--td-text-color-disabled);
|
||||
border: none;
|
||||
&:hover {
|
||||
color: var(--td-brand-color-hover);
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
.btn.t-button {
|
||||
height: var(--td-comp-size-m);
|
||||
padding: 0;
|
||||
}
|
||||
.model-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.t-select {
|
||||
width: 112px;
|
||||
height: var(--td-comp-size-m);
|
||||
margin-right: var(--td-comp-margin-s);
|
||||
.t-input {
|
||||
border-radius: 32px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
.t-input.t-is-focused {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
.check-box {
|
||||
width: 112px;
|
||||
height: var(--td-comp-size-m);
|
||||
border-radius: 32px;
|
||||
border: 0;
|
||||
background: var(--td-bg-color-component);
|
||||
color: var(--td-text-color-primary);
|
||||
box-sizing: border-box;
|
||||
flex: 0 0 auto;
|
||||
.t-button__text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
span {
|
||||
margin-left: var(--td-comp-margin-xs);
|
||||
}
|
||||
}
|
||||
}
|
||||
.check-box.is-active {
|
||||
border: 1px solid var(--td-brand-color-focus);
|
||||
background: var(--td-brand-color-light);
|
||||
color: var(--td-text-color-brand);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
338
frontend/src/components/agent-chat_bk.vue
Normal file
338
frontend/src/components/agent-chat_bk.vue
Normal file
@@ -0,0 +1,338 @@
|
||||
<template>
|
||||
<div class="chat-box">
|
||||
<t-chat
|
||||
ref="chatRef"
|
||||
:clear-history="chatList.length > 0 && !isStreamLoad"
|
||||
:data="chatList"
|
||||
:text-loading="loading"
|
||||
:is-stream-load="isStreamLoad"
|
||||
style="height: 100%"
|
||||
@scroll="handleChatScroll"
|
||||
@clear="clearConfirm"
|
||||
>
|
||||
<!-- eslint-disable vue/no-unused-vars -->
|
||||
<template #content="{ item, index }">
|
||||
<t-chat-reasoning v-if="item.reasoning?.length > 0" expand-icon-placement="right">
|
||||
<template #header>
|
||||
<t-chat-loading v-if="isStreamLoad && item.content.length === 0" text="思考中..." />
|
||||
<div v-else style="display: flex; align-items: center">
|
||||
<CheckCircleIcon style="color: var(--td-success-color-5); font-size: 20px; margin-right: 8px" />
|
||||
<span>已深度思考</span>
|
||||
</div>
|
||||
</template>
|
||||
<t-chat-content v-if="item.reasoning.length > 0" :content="item.reasoning" />
|
||||
</t-chat-reasoning>
|
||||
<t-chat-content v-if="item.content.length > 0" :content="item.content" />
|
||||
</template>
|
||||
<template #actions="{ item, index }">
|
||||
<t-chat-action
|
||||
:content="item.content"
|
||||
:operation-btn="['good', 'bad', 'replay', 'copy']"
|
||||
@operation="handleOperation"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<t-chat-input :stop-disabled="isStreamLoad" @send="inputEnter" @stop="onStop"> </t-chat-input>
|
||||
</template>
|
||||
</t-chat>
|
||||
<t-button v-show="isShowToBottom" variant="text" class="bottomBtn" @click="backBottom">
|
||||
<div class="to-bottom">
|
||||
<ArrowDownIcon />
|
||||
</div>
|
||||
</t-button>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="jsx">
|
||||
import {ref, onMounted, h, onBeforeUnmount} from 'vue';
|
||||
import { MockSSEResponse } from '../mock-data/index';
|
||||
import { ArrowDownIcon, CheckCircleIcon } from 'tdesign-icons-vue-next';
|
||||
const fetchCancel = ref(null);
|
||||
const loading = ref(false);
|
||||
// 流式数据加载中
|
||||
const isStreamLoad = ref(false);
|
||||
|
||||
const chatRef = ref(null);
|
||||
const isShowToBottom = ref(false);
|
||||
|
||||
const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/master/build/appicon.png');
|
||||
import {darkTheme, NAvatar, NImage} from "naive-ui";
|
||||
import {ChatWithAgent, GetConfig, GetSponsorInfo, GetVersionInfo} from "../../wailsjs/go/main/App";
|
||||
import {EventsOff, EventsOn} from '../../wailsjs/runtime'
|
||||
import 'tdesign-vue-next/es/style/index.css';
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
EventsOff("agent-message")
|
||||
})
|
||||
EventsOn("agent-message", (data) => {
|
||||
console.log(data)
|
||||
if(data['role']==="assistant"){
|
||||
loading.value = false;
|
||||
isStreamLoad.value = true;
|
||||
const lastItem = chatList.value[0];
|
||||
if (data['reasoning_content']){
|
||||
lastItem.reasoning += data['reasoning_content'];
|
||||
}
|
||||
if (data['content']){
|
||||
lastItem.content +=data['content'];
|
||||
}
|
||||
if(data['response_meta'].finish_reason==="stop"){
|
||||
isStreamLoad.value = false;
|
||||
}
|
||||
if(data['tool_calls']){
|
||||
lastItem.tool_calls = data['tool_calls'];
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
//chatRef.value.scrollToBottom();
|
||||
|
||||
GetConfig().then((res) => {
|
||||
if (res.darkTheme) {
|
||||
document.documentElement.setAttribute("theme-mode", "dark");
|
||||
} else {
|
||||
document.documentElement.removeAttribute("theme-mode"); }
|
||||
})
|
||||
|
||||
|
||||
GetVersionInfo().then((res) => {
|
||||
icon.value = res.icon;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// 滚动到底部
|
||||
const backBottom = () => {
|
||||
chatRef.value.scrollToBottom({
|
||||
behavior: 'smooth',
|
||||
});
|
||||
};
|
||||
// 是否显示回到底部按钮
|
||||
const handleChatScroll = function ({ e }) {
|
||||
const scrollTop = e.target.scrollTop;
|
||||
isShowToBottom.value = scrollTop < 0;
|
||||
};
|
||||
// 清空消息
|
||||
const clearConfirm = function () {
|
||||
chatList.value = [];
|
||||
};
|
||||
const handleOperation = function (type, options) {
|
||||
console.log('handleOperation', type, options);
|
||||
};
|
||||
// 倒序渲染
|
||||
const chatList = ref([
|
||||
// {
|
||||
// content: `模型由<span>hunyuan</span>变为<span>GPT4</span>`,
|
||||
// role: 'model-change',
|
||||
// reasoning: '',
|
||||
// },
|
||||
{
|
||||
avatar: h(NImage, { src: icon.value, height: '48px', width: '48px'}),
|
||||
name: 'Go-Stock AI',
|
||||
datetime: '',
|
||||
reasoning: '',
|
||||
content: '我是您的AI赋能股票分析助手,您可以问我任何关于股票投资方面的问题。',
|
||||
role: 'assistant',
|
||||
duration: 10,
|
||||
},
|
||||
{
|
||||
avatar: 'https://tdesign.gtimg.com/site/avatar.jpg',
|
||||
name: '宇宙无敌大韭菜',
|
||||
datetime: '',
|
||||
content: '介绍下自己?',
|
||||
role: 'user',
|
||||
reasoning: '',
|
||||
},
|
||||
]);
|
||||
|
||||
const onStop = function () {
|
||||
if (fetchCancel.value) {
|
||||
fetchCancel.value.controller.close();
|
||||
loading.value = false;
|
||||
isStreamLoad.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const inputEnter = function (inputValue) {
|
||||
if (isStreamLoad.value) {
|
||||
return;
|
||||
}
|
||||
if (!inputValue) return;
|
||||
const params = {
|
||||
avatar: 'https://tdesign.gtimg.com/site/avatar.jpg',
|
||||
name: '宇宙无敌大韭菜',
|
||||
datetime: new Date().toDateString(),
|
||||
content: inputValue,
|
||||
role: 'user',
|
||||
};
|
||||
chatList.value.unshift(params);
|
||||
// 空消息占位
|
||||
const params2 = {
|
||||
avatar: h(NImage, { src: icon.value, height: '48px', width: '48px'}),
|
||||
name: 'Go-Stock AI',
|
||||
datetime: new Date().toDateString(),
|
||||
content: '',
|
||||
reasoning: '',
|
||||
role: 'assistant',
|
||||
};
|
||||
chatList.value.unshift(params2);
|
||||
handleData(inputValue);
|
||||
ChatWithAgent(inputValue,1,0)
|
||||
};
|
||||
|
||||
|
||||
|
||||
const fetchSSE = async (fetchFn, options) => {
|
||||
const response = await fetchFn();
|
||||
const { success, fail, complete } = options;
|
||||
// 如果不 ok 说明有请求错误
|
||||
if (!response.ok) {
|
||||
complete?.(false, response.statusText);
|
||||
fail?.();
|
||||
return;
|
||||
}
|
||||
const reader = response?.body?.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
if (!reader) return;
|
||||
|
||||
reader.read().then(function processText({ done, value }) {
|
||||
if (done) {
|
||||
// 正常的返回
|
||||
complete?.(true);
|
||||
return;
|
||||
}
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
const buffers = chunk.toString().split(/\r?\n/);
|
||||
const jsonData = JSON.parse(buffers);
|
||||
success(jsonData);
|
||||
reader.read().then(processText);
|
||||
});
|
||||
};
|
||||
const handleData = async () => {
|
||||
loading.value = true;
|
||||
isStreamLoad.value = true;
|
||||
const lastItem = chatList.value[0];
|
||||
const mockedData = {
|
||||
reasoning: `嗯,用户问牛顿第一定律是不是适用于所有参考系。首先,我得先回忆一下牛顿第一定律的内容。牛顿第一定律,也就是惯性定律,说物体在没有外力作用时会保持静止或匀速直线运动。也就是说,保持原来的运动状态。
|
||||
|
||||
那问题来了,这个定律是否适用于所有参考系呢?记得以前学过的参考系分惯性系和非惯性系。惯性系里,牛顿定律成立;非惯性系里,可能需要引入惯性力之类的修正。所以牛顿第一定律应该只在惯性参考系中成立,而在非惯性系中不适用,比如加速的电梯或者旋转的参考系,这时候物体会有看似无外力下的加速度,所以必须引入假想的力来解释。`,
|
||||
content: `牛顿第一定律(惯性定律)**并不适用于所有参考系**,它只在**惯性参考系**中成立。以下是关键点:
|
||||
|
||||
---
|
||||
|
||||
### **1. 牛顿第一定律的核心**
|
||||
- **内容**:物体在不受外力(或合力为零)时,将保持静止或匀速直线运动状态。
|
||||
- **本质**:定义了惯性系的存在——即存在一类参考系,在其中惯性定律成立。`,
|
||||
};
|
||||
const mockResponse = new MockSSEResponse(mockedData);
|
||||
fetchCancel.value = mockResponse;
|
||||
await fetchSSE(
|
||||
() => {
|
||||
return mockResponse.getResponse();
|
||||
},
|
||||
{
|
||||
success(result) {
|
||||
console.log('success', result);
|
||||
loading.value = false;
|
||||
lastItem.reasoning += result.delta.reasoning_content;
|
||||
lastItem.content += result.delta.content;
|
||||
},
|
||||
complete(isOk, msg) {
|
||||
if (!isOk) {
|
||||
lastItem.role = 'error';
|
||||
lastItem.content = msg;
|
||||
lastItem.reasoning = msg;
|
||||
}
|
||||
// 显示用时xx秒,业务侧需要自行处理
|
||||
lastItem.duration = 20;
|
||||
// 控制终止按钮
|
||||
isStreamLoad.value = false;
|
||||
loading.value = false;
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
</script>
|
||||
<style lang="less">
|
||||
/* 应用滚动条样式 */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: var(--td-scrollbar-color);
|
||||
}
|
||||
::-webkit-scrollbar-thumb:horizontal:hover {
|
||||
background-color: var(--td-scrollbar-hover-color);
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: var(--td-scroll-track-color);
|
||||
}
|
||||
.chat-box {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
margin: 5px 10px 5px 10px;
|
||||
.bottomBtn {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
margin-left: -20px;
|
||||
bottom: 210px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, 0.08), 0px 16px 24px 2px rgba(0, 0, 0, 0.04),
|
||||
0px 6px 30px 5px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.to-bottom {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 1px solid #dcdcdc;
|
||||
box-sizing: border-box;
|
||||
background: var(--td-bg-color-container);
|
||||
border-radius: 50%;
|
||||
font-size: 24px;
|
||||
line-height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.t-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.model-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.t-select {
|
||||
width: 112px;
|
||||
height: 32px;
|
||||
margin-right: 8px;
|
||||
.t-input {
|
||||
border-radius: 32px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
.check-box {
|
||||
width: 112px;
|
||||
height: 32px;
|
||||
border-radius: 32px;
|
||||
border: 0;
|
||||
background: #e7e7e7;
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
box-sizing: border-box;
|
||||
flex: 0 0 auto;
|
||||
.t-button__text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
span {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.check-box.is-active {
|
||||
border: 1px solid #d9e1ff;
|
||||
background: #f2f3ff;
|
||||
color: var(--td-brand-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import {computed, h, onBeforeMount, onBeforeUnmount, onMounted, ref} from 'vue'
|
||||
import * as echarts from "echarts";
|
||||
import {computed, h, onBeforeMount, onBeforeUnmount, onMounted,onUnmounted, ref} from 'vue'
|
||||
import {
|
||||
GetAIResponseResult,
|
||||
GetConfig,
|
||||
@@ -11,7 +12,8 @@ import {
|
||||
SaveAIResponseResult,
|
||||
SaveAsMarkdown,
|
||||
ShareAnalysis,
|
||||
SummaryStockNews
|
||||
SummaryStockNews,
|
||||
GetAiConfigs,
|
||||
} from "../../wailsjs/go/main/App";
|
||||
import {EventsOff, EventsOn} from "../../wailsjs/runtime";
|
||||
import NewsList from "./newsList.vue";
|
||||
@@ -43,6 +45,7 @@ const panelHeight = ref(window.innerHeight - 240)
|
||||
|
||||
const telegraphList = ref([])
|
||||
const sinaNewsList = ref([])
|
||||
const foreignNewsList = ref([])
|
||||
const common = ref([])
|
||||
const america = ref([])
|
||||
const europe = ref([])
|
||||
@@ -52,6 +55,7 @@ const globalStockIndexes = ref(null)
|
||||
const summaryModal = ref(false)
|
||||
const summaryBTN = ref(true)
|
||||
const darkTheme = ref(false)
|
||||
const httpProxyEnabled = ref(false)
|
||||
const theme = computed(() => {
|
||||
return darkTheme ? 'dark' : 'light'
|
||||
})
|
||||
@@ -60,8 +64,10 @@ const aiSummaryTime = ref("")
|
||||
const modelName = ref("")
|
||||
const chatId = ref("")
|
||||
const question = ref(``)
|
||||
const sysPromptId = ref(0)
|
||||
const aiConfigId = ref(null)
|
||||
const sysPromptId = ref(null)
|
||||
const loading = ref(true)
|
||||
const aiConfigs = ref([])
|
||||
const sysPromptOptions = ref([])
|
||||
const userPromptOptions = ref([])
|
||||
const promptTemplates = ref([])
|
||||
@@ -72,6 +78,8 @@ const indexInterval = ref(null)
|
||||
const indexIndustryRank = ref(null)
|
||||
const stockCode= ref('')
|
||||
const enableTools= ref(true)
|
||||
const treemapRef = ref(null);
|
||||
let treemapchart =null;
|
||||
|
||||
function getIndex() {
|
||||
GlobalStockIndexes().then((res) => {
|
||||
@@ -90,6 +98,7 @@ onBeforeMount(() => {
|
||||
GetConfig().then(result => {
|
||||
summaryBTN.value = result.openAiEnable
|
||||
darkTheme.value = result.darkTheme
|
||||
httpProxyEnabled.value = result.httpProxyEnabled
|
||||
})
|
||||
GetPromptTemplates("", "").then(res => {
|
||||
promptTemplates.value = res
|
||||
@@ -97,12 +106,19 @@ onBeforeMount(() => {
|
||||
userPromptOptions.value = promptTemplates.value.filter(item => item.type === '模型用户Prompt')
|
||||
})
|
||||
|
||||
GetAiConfigs().then(res=>{
|
||||
aiConfigs.value = res
|
||||
aiConfigId.value = res[0].ID
|
||||
})
|
||||
GetTelegraphList("财联社电报").then((res) => {
|
||||
telegraphList.value = res
|
||||
})
|
||||
GetTelegraphList("新浪财经").then((res) => {
|
||||
sinaNewsList.value = res
|
||||
})
|
||||
GetTelegraphList("外媒").then((res) => {
|
||||
foreignNewsList.value = res
|
||||
})
|
||||
getIndex();
|
||||
industryRank();
|
||||
indexInterval.value = setInterval(() => {
|
||||
@@ -112,7 +128,12 @@ onBeforeMount(() => {
|
||||
indexIndustryRank.value = setInterval(() => {
|
||||
industryRank()
|
||||
}, 1000 * 10)
|
||||
|
||||
|
||||
})
|
||||
onMounted(() => {
|
||||
})
|
||||
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
EventsOff("changeMarketTab")
|
||||
@@ -123,8 +144,12 @@ onBeforeUnmount(() => {
|
||||
clearInterval(indexIndustryRank.value)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
});
|
||||
EventsOn("changeMarketTab", async (msg) => {
|
||||
//message.info(msg.name)
|
||||
console.log(msg.name)
|
||||
updateTab(msg.name)
|
||||
})
|
||||
|
||||
@@ -144,6 +169,14 @@ EventsOn("newSinaNews", (data) => {
|
||||
sinaNewsList.value.unshift(...data)
|
||||
}
|
||||
})
|
||||
EventsOn("tradingViewNews", (data) => {
|
||||
if (data!=null) {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
foreignNewsList.value.pop()
|
||||
}
|
||||
foreignNewsList.value.unshift(...data)
|
||||
}
|
||||
})
|
||||
|
||||
//获取页面高度
|
||||
window.onresize = () => {
|
||||
@@ -190,7 +223,7 @@ function reAiSummary() {
|
||||
aiSummary.value = ""
|
||||
summaryModal.value = true
|
||||
loading.value = true
|
||||
SummaryStockNews(question.value, sysPromptId.value,enableTools.value)
|
||||
SummaryStockNews(question.value,aiConfigId.value, sysPromptId.value,enableTools.value)
|
||||
}
|
||||
|
||||
function getAiSummary() {
|
||||
@@ -230,7 +263,7 @@ EventsOn("summaryStockNews", async (msg) => {
|
||||
loading.value = false
|
||||
////console.log(msg)
|
||||
if (msg === "DONE") {
|
||||
SaveAIResponseResult("市场资讯", "市场资讯", aiSummary.value, chatId.value, question.value)
|
||||
await SaveAIResponseResult("市场资讯", "市场资讯", aiSummary.value, chatId.value, question.value,aiConfigId.value)
|
||||
message.info("AI分析完成!")
|
||||
message.destroyAll()
|
||||
|
||||
@@ -304,22 +337,37 @@ function ReFlesh(source) {
|
||||
if (source === "新浪财经") {
|
||||
sinaNewsList.value = res
|
||||
}
|
||||
if (source === "外媒") {
|
||||
foreignNewsList.value = res
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card>
|
||||
<n-tabs type="line" animated @update-value="updateTab" :value="nowTab" style="--wails-draggable:drag">
|
||||
<n-tabs type="line" animated @update-value="updateTab" :value="nowTab" style="--wails-draggable:no-drag">
|
||||
<n-tab-pane name="市场快讯" tab="市场快讯">
|
||||
<n-grid :cols="2" :y-gap="0">
|
||||
<n-grid :cols="1" :y-gap="0">
|
||||
<n-gi>
|
||||
<news-list :newsList="telegraphList" :header-title="'财联社电报'" @update:message="ReFlesh"></news-list>
|
||||
<AnalyzeMartket :dark-theme="darkTheme" :chart-height="300" :kDays="1" :name="'最近24小时热词'" />
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<news-list :newsList="sinaNewsList" :header-title="'新浪财经'" @update:message="ReFlesh"></news-list>
|
||||
<n-grid :cols="httpProxyEnabled?3:2" :y-gap="0">
|
||||
<n-gi>
|
||||
<news-list :newsList="telegraphList" :header-title="'财联社电报'" @update:message="ReFlesh"></news-list>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<news-list :newsList="sinaNewsList" :header-title="'新浪财经'" @update:message="ReFlesh"></news-list>
|
||||
</n-gi>
|
||||
<n-gi v-if="httpProxyEnabled">
|
||||
<news-list :newsList="foreignNewsList" :header-title="'外媒'" @update:message="ReFlesh"></news-list>
|
||||
</n-gi>
|
||||
|
||||
</n-grid>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="全球股指" tab="全球股指">
|
||||
<n-tabs type="segment" animated>
|
||||
@@ -659,9 +707,11 @@ function ReFlesh(source) {
|
||||
<n-gradient-text type="error" style="margin-left: 10px">*AI函数工具调用可以增强AI获取数据的能力,但会消耗更多tokens。</n-gradient-text>
|
||||
</n-flex>
|
||||
<n-flex justify="space-between" style="margin-bottom: 10px">
|
||||
<n-select style="width: 49%" v-model:value="sysPromptId" label-field="name" value-field="ID"
|
||||
<n-select style="width: 32%" v-model:value="aiConfigId" label-field="name" value-field="ID"
|
||||
:options="aiConfigs" placeholder="请选择AI模型服务配置"/>
|
||||
<n-select style="width: 32%" v-model:value="sysPromptId" label-field="name" value-field="ID"
|
||||
:options="sysPromptOptions" placeholder="请选择系统提示词"/>
|
||||
<n-select style="width: 49%" v-model:value="question" label-field="name" value-field="content"
|
||||
<n-select style="width: 32%" v-model:value="question" label-field="name" value-field="content"
|
||||
:options="userPromptOptions" placeholder="请选择用户提示词"/>
|
||||
</n-flex>
|
||||
<n-flex justify="right">
|
||||
|
||||
@@ -29,8 +29,24 @@ const updateMessage = () => {
|
||||
</n-flex>
|
||||
</template>
|
||||
<n-list-item v-for="item in newsList">
|
||||
<n-space justify="start">
|
||||
<n-text justify="start" :bordered="false" :type="item.isRed?'error':'info'">
|
||||
<n-space justify="start" >
|
||||
<!-- <n-text justify="start" :bordered="false" :type="item.isRed?'error':'info'" style="overflow-wrap: break-word;">-->
|
||||
<!-- <n-tag size="small" :type="item.isRed?'error':'warning'" :bordered="false"> {{ item.time }}</n-tag>-->
|
||||
<!-- <n-text size="small" v-if="item.title" type="warning" :bordered="false">{{ item.title }} </n-text>-->
|
||||
<!-- <n-text style="overflow-wrap: break-word;word-break: break-all; word-wrap: break-word;" :type="item.isRed?'error':'info'">{{ item.content }}</n-text>-->
|
||||
<!-- </n-text>-->
|
||||
<n-collapse v-if="item.title" arrow-placement="right">
|
||||
<n-collapse-item :name="item.title">
|
||||
<template #header>
|
||||
<n-tag size="small" :type="item.isRed?'error':'warning'" :bordered="false"> {{ item.time }}</n-tag>
|
||||
<n-text size="small" :type="item.isRed?'error':'info'" :bordered="false">{{ item.title }}</n-text>
|
||||
</template>
|
||||
<n-text justify="start" :bordered="false" :type="item.isRed?'error':'info'">
|
||||
{{ item.content }}
|
||||
</n-text>
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
<n-text v-if="!item.title" justify="start" :bordered="false" :type="item.isRed?'error':'info'">
|
||||
<n-tag size="small" :type="item.isRed?'error':'warning'" :bordered="false"> {{ item.time }}</n-tag>
|
||||
{{ item.content }}
|
||||
</n-text>
|
||||
@@ -49,6 +65,9 @@ const updateMessage = () => {
|
||||
<n-text type="warning">查看原文</n-text>
|
||||
</a>
|
||||
</n-tag>
|
||||
<n-tag v-if="item.sentimentResult" :bordered="false" :type="item.sentimentResult==='看涨'?'error':item.sentimentResult==='看跌'?'success':'info'" size="small">
|
||||
{{ item.sentimentResult }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup>
|
||||
|
||||
import {h, onBeforeUnmount, onMounted, ref} from "vue";
|
||||
import {
|
||||
AddPrompt, DelPrompt,
|
||||
@@ -7,175 +6,199 @@ import {
|
||||
GetConfig,
|
||||
GetPromptTemplates,
|
||||
SendDingDingMessageByType,
|
||||
UpdateConfig,CheckSponsorCode
|
||||
UpdateConfig, CheckSponsorCode
|
||||
} from "../../wailsjs/go/main/App";
|
||||
import {NTag, useMessage} from "naive-ui";
|
||||
import {data, models} from "../../wailsjs/go/models";
|
||||
import {EventsEmit} from "../../wailsjs/runtime";
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
const formRef = ref(null)
|
||||
const formValue = ref({
|
||||
ID:1,
|
||||
tushareToken:'',
|
||||
dingPush:{
|
||||
enable:false,
|
||||
ID: 1,
|
||||
tushareToken: '',
|
||||
dingPush: {
|
||||
enable: false,
|
||||
dingRobot: ''
|
||||
},
|
||||
localPush:{
|
||||
enable:true,
|
||||
localPush: {
|
||||
enable: true,
|
||||
},
|
||||
updateBasicInfoOnStart:false,
|
||||
refreshInterval:1,
|
||||
openAI:{
|
||||
enable:false,
|
||||
updateBasicInfoOnStart: false,
|
||||
refreshInterval: 1,
|
||||
openAI: {
|
||||
enable: false,
|
||||
aiConfigs: [], // AI配置列表
|
||||
prompt: "",
|
||||
questionTemplate: "{{stockName}}分析和总结",
|
||||
crawlTimeOut: 30,
|
||||
kDays: 30,
|
||||
},
|
||||
enableDanmu: false,
|
||||
browserPath: '',
|
||||
enableNews: false,
|
||||
darkTheme: true,
|
||||
enableFund: false,
|
||||
enablePushNews: false,
|
||||
enableOnlyPushRedNews: false,
|
||||
sponsorCode: "",
|
||||
httpProxy:"",
|
||||
httpProxyEnabled:false,
|
||||
enableAgent: false,
|
||||
qgqpBId: '',
|
||||
})
|
||||
|
||||
// 添加一个新的AI配置到列表
|
||||
function addAiConfig() {
|
||||
formValue.value.openAI.aiConfigs.push(new data.AIConfig({
|
||||
name: '',
|
||||
baseUrl: 'https://api.deepseek.com',
|
||||
apiKey: '',
|
||||
model: 'deepseek-chat',
|
||||
modelName: 'deepseek-chat',
|
||||
temperature: 0.1,
|
||||
maxTokens: 1024,
|
||||
prompt:"",
|
||||
timeout: 5,
|
||||
questionTemplate: "{{stockName}}分析和总结",
|
||||
crawlTimeOut:30,
|
||||
kDays:30,
|
||||
},
|
||||
enableDanmu:false,
|
||||
browserPath: '',
|
||||
enableNews:false,
|
||||
darkTheme:true,
|
||||
enableFund:false,
|
||||
enablePushNews:false,
|
||||
sponsorCode:"",
|
||||
})
|
||||
const promptTemplates=ref([])
|
||||
onMounted(()=>{
|
||||
GetConfig().then(res=>{
|
||||
timeOut: 60,
|
||||
}));
|
||||
}
|
||||
|
||||
// 从列表中移除一个AI配置
|
||||
function removeAiConfig(index) {
|
||||
const originalCount = formValue.value.openAI.aiConfigs.length;
|
||||
// 使用filter创建新数组确保响应式更新
|
||||
formValue.value.openAI.aiConfigs = formValue.value.openAI.aiConfigs.filter((_, i) => i !== index);
|
||||
}
|
||||
|
||||
|
||||
const promptTemplates = ref([])
|
||||
onMounted(() => {
|
||||
GetConfig().then(res => {
|
||||
formValue.value.ID = res.ID
|
||||
formValue.value.tushareToken = res.tushareToken
|
||||
formValue.value.dingPush = {
|
||||
enable:res.dingPushEnable,
|
||||
dingRobot:res.dingRobot
|
||||
enable: res.dingPushEnable,
|
||||
dingRobot: res.dingRobot
|
||||
}
|
||||
formValue.value.localPush = {
|
||||
enable:res.localPushEnable,
|
||||
enable: res.localPushEnable,
|
||||
}
|
||||
formValue.value.updateBasicInfoOnStart = res.updateBasicInfoOnStart
|
||||
formValue.value.refreshInterval = res.refreshInterval
|
||||
// 加载AI配置
|
||||
formValue.value.openAI = {
|
||||
enable:res.openAiEnable,
|
||||
baseUrl: res.openAiBaseUrl,
|
||||
apiKey:res.openAiApiKey,
|
||||
model:res.openAiModelName,
|
||||
temperature:res.openAiTemperature,
|
||||
maxTokens:res.openAiMaxTokens,
|
||||
prompt:res.prompt,
|
||||
timeout:res.openAiApiTimeOut,
|
||||
questionTemplate:res.questionTemplate?res.questionTemplate:'{{stockName}}分析和总结',
|
||||
crawlTimeOut:res.crawlTimeOut,
|
||||
kDays:res.kDays,
|
||||
enable: res.openAiEnable,
|
||||
aiConfigs: res.aiConfigs || [],
|
||||
prompt: res.prompt,
|
||||
questionTemplate: res.questionTemplate ? res.questionTemplate : '{{stockName}}分析和总结',
|
||||
crawlTimeOut: res.crawlTimeOut,
|
||||
kDays: res.kDays,
|
||||
}
|
||||
|
||||
|
||||
formValue.value.enableDanmu = res.enableDanmu
|
||||
formValue.value.browserPath = res.browserPath
|
||||
formValue.value.enableNews = res.enableNews
|
||||
formValue.value.darkTheme = res.darkTheme
|
||||
formValue.value.enableFund = res.enableFund
|
||||
formValue.value.enablePushNews = res.enablePushNews
|
||||
formValue.value.enableOnlyPushRedNews = res.enableOnlyPushRedNews
|
||||
formValue.value.sponsorCode = res.sponsorCode
|
||||
formValue.value.httpProxy=res.httpProxy;
|
||||
formValue.value.httpProxyEnabled=res.httpProxyEnabled;
|
||||
formValue.value.enableAgent = res.enableAgent;
|
||||
formValue.value.qgqpBId = res.qgqpBId;
|
||||
|
||||
|
||||
//console.log(res)
|
||||
})
|
||||
//message.info("加载完成")
|
||||
|
||||
GetPromptTemplates("","").then(res=>{
|
||||
//console.log(res)
|
||||
promptTemplates.value=res
|
||||
GetPromptTemplates("", "").then(res => {
|
||||
promptTemplates.value = res
|
||||
})
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
message.destroyAll()
|
||||
})
|
||||
|
||||
function saveConfig(){
|
||||
|
||||
let config= new data.Settings({
|
||||
ID:formValue.value.ID,
|
||||
dingPushEnable:formValue.value.dingPush.enable,
|
||||
dingRobot:formValue.value.dingPush.dingRobot,
|
||||
localPushEnable:formValue.value.localPush.enable,
|
||||
updateBasicInfoOnStart:formValue.value.updateBasicInfoOnStart,
|
||||
refreshInterval:formValue.value.refreshInterval,
|
||||
openAiEnable:formValue.value.openAI.enable,
|
||||
openAiBaseUrl:formValue.value.openAI.baseUrl,
|
||||
openAiApiKey:formValue.value.openAI.apiKey,
|
||||
openAiModelName:formValue.value.openAI.model,
|
||||
openAiMaxTokens:formValue.value.openAI.maxTokens,
|
||||
openAiTemperature:formValue.value.openAI.temperature,
|
||||
tushareToken:formValue.value.tushareToken,
|
||||
prompt:formValue.value.openAI.prompt,
|
||||
openAiApiTimeOut:formValue.value.openAI.timeout,
|
||||
questionTemplate:formValue.value.openAI.questionTemplate,
|
||||
crawlTimeOut:formValue.value.openAI.crawlTimeOut,
|
||||
kDays:formValue.value.openAI.kDays,
|
||||
enableDanmu:formValue.value.enableDanmu,
|
||||
browserPath:formValue.value.browserPath,
|
||||
enableNews:formValue.value.enableNews,
|
||||
darkTheme:formValue.value.darkTheme,
|
||||
enableFund:formValue.value.enableFund,
|
||||
enablePushNews:formValue.value.enablePushNews,
|
||||
sponsorCode:formValue.value.sponsorCode
|
||||
function saveConfig() {
|
||||
console.log('开始保存设置', formValue.value);
|
||||
// 构建配置时,包含aiConfigs列表
|
||||
let config = new data.SettingConfig({
|
||||
ID: formValue.value.ID,
|
||||
dingPushEnable: formValue.value.dingPush.enable,
|
||||
dingRobot: formValue.value.dingPush.dingRobot,
|
||||
localPushEnable: formValue.value.localPush.enable,
|
||||
updateBasicInfoOnStart: formValue.value.updateBasicInfoOnStart,
|
||||
refreshInterval: formValue.value.refreshInterval,
|
||||
openAiEnable: formValue.value.openAI.enable,
|
||||
aiConfigs: formValue.value.openAI.aiConfigs,
|
||||
// 序列化aiConfigs列表以传递给后端
|
||||
tushareToken: formValue.value.tushareToken,
|
||||
prompt: formValue.value.openAI.prompt,
|
||||
questionTemplate: formValue.value.openAI.questionTemplate,
|
||||
crawlTimeOut: formValue.value.openAI.crawlTimeOut,
|
||||
kDays: formValue.value.openAI.kDays,
|
||||
enableDanmu: formValue.value.enableDanmu,
|
||||
browserPath: formValue.value.browserPath,
|
||||
enableNews: formValue.value.enableNews,
|
||||
darkTheme: formValue.value.darkTheme,
|
||||
enableFund: formValue.value.enableFund,
|
||||
enablePushNews: formValue.value.enablePushNews,
|
||||
enableOnlyPushRedNews: formValue.value.enableOnlyPushRedNews,
|
||||
sponsorCode: formValue.value.sponsorCode,
|
||||
httpProxy:formValue.value.httpProxy,
|
||||
httpProxyEnabled:formValue.value.httpProxyEnabled,
|
||||
enableAgent: formValue.value.enableAgent,
|
||||
qgqpBId: formValue.value.qgqpBId
|
||||
})
|
||||
|
||||
if (config.sponsorCode){
|
||||
CheckSponsorCode(config.sponsorCode).then(res=>{
|
||||
if (res.code){
|
||||
UpdateConfig(config).then(res=>{
|
||||
if (config.sponsorCode) {
|
||||
CheckSponsorCode(config.sponsorCode).then(res => {
|
||||
if (res.code) {
|
||||
UpdateConfig(config).then(res => {
|
||||
message.success(res)
|
||||
EventsEmit("updateSettings", config);
|
||||
})
|
||||
}else{
|
||||
} else {
|
||||
message.error(res.msg)
|
||||
}
|
||||
})
|
||||
}else{
|
||||
UpdateConfig(config).then(res=>{
|
||||
} else {
|
||||
UpdateConfig(config).then(res => {
|
||||
message.success(res)
|
||||
EventsEmit("updateSettings", config);
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
function getHeight() {
|
||||
return document.documentElement.clientHeight
|
||||
}
|
||||
function sendTestNotice(){
|
||||
let markdown="### go-stock test\n"+new Date()
|
||||
let msg='{' +
|
||||
|
||||
function sendTestNotice() {
|
||||
let markdown = "### go-stock test\n" + new Date()
|
||||
let msg = '{' +
|
||||
' "msgtype": "markdown",' +
|
||||
' "markdown": {' +
|
||||
' "title":"go-stock'+new Date()+'",' +
|
||||
' "text": "'+markdown+'"' +
|
||||
' "title":"go-stock' + new Date() + '",' +
|
||||
' "text": "' + markdown + '"' +
|
||||
' },' +
|
||||
' "at": {' +
|
||||
' "isAtAll": true' +
|
||||
' }' +
|
||||
' }'
|
||||
|
||||
SendDingDingMessageByType(msg, "test-"+new Date().getTime(),1).then(res=>{
|
||||
SendDingDingMessageByType(msg, "test-" + new Date().getTime(), 1).then(res => {
|
||||
message.info(res)
|
||||
})
|
||||
}
|
||||
|
||||
function exportConfig(){
|
||||
ExportConfig().then(res=>{
|
||||
function exportConfig() {
|
||||
ExportConfig().then(res => {
|
||||
message.info(res)
|
||||
})
|
||||
}
|
||||
|
||||
function importConfig(){
|
||||
function importConfig() {
|
||||
let input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
@@ -184,30 +207,25 @@ function importConfig(){
|
||||
let reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
let config = JSON.parse(e.target.result);
|
||||
//console.log(config)
|
||||
formValue.value.ID = config.ID
|
||||
formValue.value.tushareToken = config.tushareToken
|
||||
formValue.value.dingPush = {
|
||||
enable:config.dingPushEnable,
|
||||
dingRobot:config.dingRobot
|
||||
enable: config.dingPushEnable,
|
||||
dingRobot: config.dingRobot
|
||||
}
|
||||
formValue.value.localPush = {
|
||||
enable:config.localPushEnable,
|
||||
enable: config.localPushEnable,
|
||||
}
|
||||
formValue.value.updateBasicInfoOnStart = config.updateBasicInfoOnStart
|
||||
formValue.value.refreshInterval = config.refreshInterval
|
||||
// 导入AI配置
|
||||
formValue.value.openAI = {
|
||||
enable:config.openAiEnable,
|
||||
baseUrl: config.openAiBaseUrl,
|
||||
apiKey:config.openAiApiKey,
|
||||
model:config.openAiModelName,
|
||||
temperature:config.openAiTemperature,
|
||||
maxTokens:config.openAiMaxTokens,
|
||||
prompt:config.prompt,
|
||||
timeout:config.openAiApiTimeOut,
|
||||
questionTemplate:config.questionTemplate,
|
||||
crawlTimeOut:config.crawlTimeOut,
|
||||
kDays:config.kDays
|
||||
enable: config.openAiEnable,
|
||||
aiConfigs: config.aiConfigs || [],
|
||||
prompt: config.prompt,
|
||||
questionTemplate: config.questionTemplate,
|
||||
crawlTimeOut: config.crawlTimeOut,
|
||||
kDays: config.kDays
|
||||
}
|
||||
formValue.value.enableDanmu = config.enableDanmu
|
||||
formValue.value.browserPath = config.browserPath
|
||||
@@ -215,8 +233,12 @@ function importConfig(){
|
||||
formValue.value.darkTheme = config.darkTheme
|
||||
formValue.value.enableFund = config.enableFund
|
||||
formValue.value.enablePushNews = config.enablePushNews
|
||||
formValue.value.enableOnlyPushRedNews = config.enableOnlyPushRedNews
|
||||
formValue.value.sponsorCode = config.sponsorCode
|
||||
// formRef.value.resetFields()
|
||||
formValue.value.httpProxy=config.httpProxy
|
||||
formValue.value.httpProxyEnabled=config.httpProxyEnabled
|
||||
formValue.value.enableAgent = config.enableAgent
|
||||
formValue.value.qgqpBId = config.qgqpBId
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
@@ -225,8 +247,6 @@ function importConfig(){
|
||||
|
||||
|
||||
window.onerror = function (event, source, lineno, colno, error) {
|
||||
//console.log(event, source, lineno, colno, error)
|
||||
// 将错误信息发送给后端
|
||||
EventsEmit("frontendError", {
|
||||
page: "settings.vue",
|
||||
message: event,
|
||||
@@ -235,245 +255,259 @@ window.onerror = function (event, source, lineno, colno, error) {
|
||||
colno: colno,
|
||||
error: error ? error.stack : null
|
||||
});
|
||||
//message.error("发生错误:"+event)
|
||||
return true;
|
||||
};
|
||||
|
||||
const showManagePromptsModal=ref(false)
|
||||
const promptTypeOptions=[
|
||||
{label:"模型系统Prompt",value:'模型系统Prompt'},
|
||||
{label:"模型用户Prompt",value:'模型用户Prompt'},]
|
||||
const formPromptRef=ref(null)
|
||||
const formPrompt=ref({
|
||||
ID:0,
|
||||
Name:'',
|
||||
Content:'',
|
||||
Type:'',
|
||||
const showManagePromptsModal = ref(false)
|
||||
const promptTypeOptions = [
|
||||
{label: "模型系统Prompt", value: '模型系统Prompt'},
|
||||
{label: "模型用户Prompt", value: '模型用户Prompt'},]
|
||||
const formPromptRef = ref(null)
|
||||
const formPrompt = ref({
|
||||
ID: 0,
|
||||
Name: '',
|
||||
Content: '',
|
||||
Type: '',
|
||||
})
|
||||
function managePrompts(){
|
||||
formPrompt.value.ID=0
|
||||
showManagePromptsModal.value=true
|
||||
|
||||
function managePrompts() {
|
||||
formPrompt.value.ID = 0
|
||||
showManagePromptsModal.value = true
|
||||
}
|
||||
function savePrompt(){
|
||||
AddPrompt(formPrompt.value).then(res=>{
|
||||
|
||||
function savePrompt() {
|
||||
AddPrompt(formPrompt.value).then(res => {
|
||||
message.success(res)
|
||||
GetPromptTemplates("","").then(res=>{
|
||||
//console.log(res)
|
||||
promptTemplates.value=res
|
||||
GetPromptTemplates("", "").then(res => {
|
||||
promptTemplates.value = res
|
||||
})
|
||||
showManagePromptsModal.value=false
|
||||
showManagePromptsModal.value = false
|
||||
})
|
||||
}
|
||||
function editPrompt(prompt){
|
||||
//console.log(prompt)
|
||||
formPrompt.value.ID=prompt.ID
|
||||
formPrompt.value.Name=prompt.name
|
||||
formPrompt.value.Content=prompt.content
|
||||
formPrompt.value.Type=prompt.type
|
||||
showManagePromptsModal.value=true
|
||||
|
||||
function editPrompt(prompt) {
|
||||
formPrompt.value.ID = prompt.ID
|
||||
formPrompt.value.Name = prompt.name
|
||||
formPrompt.value.Content = prompt.content
|
||||
formPrompt.value.Type = prompt.type
|
||||
showManagePromptsModal.value = true
|
||||
}
|
||||
function deletePrompt(ID){
|
||||
DelPrompt(ID).then(res=>{
|
||||
|
||||
function deletePrompt(ID) {
|
||||
DelPrompt(ID).then(res => {
|
||||
message.success(res)
|
||||
GetPromptTemplates("","").then(res=>{
|
||||
//console.log(res)
|
||||
promptTemplates.value=res
|
||||
GetPromptTemplates("", "").then(res => {
|
||||
promptTemplates.value = res
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex justify="left" style="text-align: left;--wails-draggable:drag" >
|
||||
<n-form ref="formRef" :label-placement="'left'" :label-align="'left'" style="--wails-draggable:no-drag">
|
||||
<n-card :title="()=> h(NTag, { type: 'primary',bordered:false },()=> '基础设置')" size="small" >
|
||||
<n-grid :cols="24" :x-gap="24" style="text-align: left" >
|
||||
<!-- <n-gi :span="24">-->
|
||||
<!-- <n-text type="success" style="font-size: 25px;font-weight: bold">基础设置</n-text>-->
|
||||
<!-- </n-gi>-->
|
||||
<n-form-item-gi :span="10" label="Tushare Token:" path="tushareToken" >
|
||||
<n-input type="text" placeholder="Tushare api token" v-model:value="formValue.tushareToken" clearable />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="4" label="启动时更新A股/指数信息:" path="updateBasicInfoOnStart" >
|
||||
<n-switch v-model:value="formValue.updateBasicInfoOnStart" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="4" label="数据刷新间隔:" path="refreshInterval" >
|
||||
<n-input-number v-model:value="formValue.refreshInterval" placeholder="请输入数据刷新间隔(秒)">
|
||||
<template #suffix>
|
||||
秒
|
||||
</template>
|
||||
</n-input-number>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="6" label="暗黑主题:" path="darkTheme" >
|
||||
<n-switch v-model:value="formValue.darkTheme" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="10" label="浏览器安装路径:" path="browserPath" >
|
||||
<n-input type="text" placeholder="浏览器安装路径" v-model:value="formValue.browserPath" clearable />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="3" label="指数基金:" path="enableFund" >
|
||||
<n-switch v-model:value="formValue.enableFund" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="11" label="赞助码:" path="sponsorCode" >
|
||||
<n-input-group>
|
||||
<n-input :show-count="true" placeholder="赞助码" v-model:value="formValue.sponsorCode" />
|
||||
<n-button type="success" secondary strong @click="CheckSponsorCode(formValue.sponsorCode).then((res) => {message.warning(res.msg) })">验证</n-button>
|
||||
</n-input-group>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
<n-flex justify="left" style="text-align: left; --wails-draggable:no-drag">
|
||||
<n-form ref="formRef" :label-placement="'left'" :label-align="'left'">
|
||||
<n-space vertical size="large">
|
||||
<n-card :title="() => h(NTag, { type: 'primary', bordered: false }, () => '基础设置')" size="small">
|
||||
<n-grid :cols="24" :x-gap="24" style="text-align: left">
|
||||
<n-form-item-gi :span="10" label="Tushare Token:" path="tushareToken">
|
||||
<n-input type="text" placeholder="Tushare api token" v-model:value="formValue.tushareToken" clearable/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="4" label="启动时更新基础信息:" path="updateBasicInfoOnStart">
|
||||
<n-switch v-model:value="formValue.updateBasicInfoOnStart"/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="4" label="数据刷新间隔:" path="refreshInterval">
|
||||
<n-input-number v-model:value="formValue.refreshInterval" placeholder="请输入数据刷新间隔(秒)">
|
||||
<template #suffix>秒</template>
|
||||
</n-input-number>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="6" label="暗黑主题:" path="darkTheme">
|
||||
<n-switch v-model:value="formValue.darkTheme"/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="10" label="浏览器安装路径:" path="browserPath">
|
||||
<n-input type="text" placeholder="浏览器安装路径" v-model:value="formValue.browserPath" clearable/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="3" label="指数基金:" path="enableFund">
|
||||
<n-switch v-model:value="formValue.enableFund"/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="3" label="AI智能体:" path="enableAgent">
|
||||
<n-switch v-model:value="formValue.enableAgent"/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="11" label="东财唯一标识:" path="qgqpBId">
|
||||
<n-input type="text" placeholder="东财唯一标识" v-model:value="formValue.qgqpBId" clearable/>
|
||||
</n-form-item-gi>
|
||||
|
||||
<n-form-item-gi :span="11" label="赞助码:" path="sponsorCode">
|
||||
<n-input-group>
|
||||
<n-input :show-count="true" placeholder="赞助码" v-model:value="formValue.sponsorCode"/>
|
||||
<n-button type="success" secondary strong
|
||||
@click="CheckSponsorCode(formValue.sponsorCode).then((res) => {message.warning(res.msg)})">验证
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
|
||||
<n-card :title="() => h(NTag, { type: 'primary', bordered: false }, () => '通知设置')" size="small">
|
||||
<n-grid :cols="24" :x-gap="24" style="text-align: left">
|
||||
<n-form-item-gi :span="3" label="钉钉推送:" path="dingPush.enable">
|
||||
<n-switch v-model:value="formValue.dingPush.enable"/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="3" label="本地推送:" path="localPush.enable">
|
||||
<n-switch v-model:value="formValue.localPush.enable"/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="3" label="弹幕功能:" path="enableDanmu">
|
||||
<n-switch v-model:value="formValue.enableDanmu"/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="3" label="显示滚动快讯:" path="enableNews">
|
||||
<n-switch v-model:value="formValue.enableNews"/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="3" label="市场资讯提醒:" path="enablePushNews">
|
||||
<n-switch v-model:value="formValue.enablePushNews"/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi v-if="formValue.enablePushNews" :span="4" label="只提醒红字或关注个股的新闻:" path="enableOnlyPushRedNews">
|
||||
<n-switch v-model:value="formValue.enableOnlyPushRedNews"/>
|
||||
</n-form-item-gi>
|
||||
|
||||
<n-form-item-gi :span="22" v-if="formValue.dingPush.enable" label="钉钉机器人接口地址:"
|
||||
path="dingPush.dingRobot">
|
||||
<n-input placeholder="请输入钉钉机器人接口地址" v-model:value="formValue.dingPush.dingRobot"/>
|
||||
<n-button type="primary" @click="sendTestNotice">发送测试通知</n-button>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
|
||||
<n-card :title="() => h(NTag, { type: 'primary', bordered: false }, () => 'AI设置')" size="small">
|
||||
<n-grid :cols="24" :x-gap="24" style="text-align: left;">
|
||||
<n-form-item-gi :span="24" label="AI诊股:" path="openAI.enable">
|
||||
<n-switch v-model:value="formValue.openAI.enable"/>
|
||||
</n-form-item-gi>
|
||||
|
||||
<n-form-item-gi :span="6" v-if="formValue.openAI.enable" label="Crawler Timeout(秒)"
|
||||
title="资讯采集超时时间(秒)" path="openAI.crawlTimeOut">
|
||||
<n-input-number min="30" step="1" v-model:value="formValue.openAI.crawlTimeOut"/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="4" v-if="formValue.openAI.enable" title="天数越多消耗tokens越多"
|
||||
label="日K线数据(天)" path="openAI.kDays">
|
||||
<n-input-number min="30" step="1" max="365" v-model:value="formValue.openAI.kDays"/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="2" label="http代理" path="httpProxyEnabled">
|
||||
<n-switch v-model:value="formValue.httpProxyEnabled"/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="10" v-if="formValue.httpProxyEnabled" title="http代理地址"
|
||||
label="http代理地址" path="httpProxy">
|
||||
<n-input type="text" placeholder="http代理地址" v-model:value="formValue.httpProxy" clearable/>
|
||||
</n-form-item-gi>
|
||||
|
||||
|
||||
<n-card :title="()=> h(NTag, { type: 'primary',bordered:false },()=> '通知设置')" size="small" >
|
||||
<n-grid :cols="24" :x-gap="24" style="text-align: left">
|
||||
<!-- <n-gi :span="24">-->
|
||||
<!-- <n-text type="success" style="font-size: 25px;font-weight: bold">通知设置</n-text>-->
|
||||
<!-- </n-gi>-->
|
||||
<n-form-item-gi :span="4" label="钉钉推送:" path="dingPush.enable" >
|
||||
<n-switch v-model:value="formValue.dingPush.enable" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="4" label="本地推送:" path="localPush.enable" >
|
||||
<n-switch v-model:value="formValue.localPush.enable" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="4" label="弹幕功能:" path="enableDanmu" >
|
||||
<n-switch v-model:value="formValue.enableDanmu" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="4" label="显示滚动快讯:" path="enableNews" >
|
||||
<n-switch v-model:value="formValue.enableNews" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="4" label="市场资讯提醒:" path="enablePushNews" >
|
||||
<n-switch v-model:value="formValue.enablePushNews" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="22" v-if="formValue.dingPush.enable" label="钉钉机器人接口地址:" path="dingPush.dingRobot" >
|
||||
<n-input placeholder="请输入钉钉机器人接口地址" v-model:value="formValue.dingPush.dingRobot"/>
|
||||
<n-button type="primary" @click="sendTestNotice">发送测试通知</n-button>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
<n-gi :span="24" v-if="formValue.openAI.enable">
|
||||
<n-divider title-placement="left">Prompt 内容设置</n-divider>
|
||||
</n-gi>
|
||||
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="模型系统 Prompt" path="openAI.prompt">
|
||||
<n-input v-model:value="formValue.openAI.prompt" type="textarea" :show-count="true"
|
||||
placeholder="请输入系统prompt" :autosize="{ minRows: 4, maxRows: 8 }"/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="模型用户 Prompt"
|
||||
path="openAI.questionTemplate">
|
||||
<n-input v-model:value="formValue.openAI.questionTemplate" type="textarea" :show-count="true"
|
||||
placeholder="请输入用户prompt:例如{{stockName}}[{{stockCode}}]分析和总结"
|
||||
:autosize="{ minRows: 4, maxRows: 8 }"/>
|
||||
</n-form-item-gi>
|
||||
|
||||
<n-card :title="()=> h(NTag, { type: 'primary',bordered:false },()=> 'AI设置')" size="small" >
|
||||
<n-grid :cols="24" :x-gap="24" style="text-align: left;">
|
||||
<!-- <n-gi :span="24">-->
|
||||
<!-- <n-text type="success" style="font-size: 25px;font-weight: bold">OpenAI设置</n-text>-->
|
||||
<!-- </n-gi>-->
|
||||
<n-form-item-gi :span="3" label="AI诊股:" path="openAI.enable" >
|
||||
<n-switch v-model:value="formValue.openAI.enable" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="9" 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="5" v-if="formValue.openAI.enable" label="AI Timeout(秒):" title="AI请求超时时间(秒)" path="openAI.timeout" >
|
||||
<n-input-number min="60" step="1" placeholder="AI请求超时时间(秒)" v-model:value="formValue.openAI.timeout" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" label="Crawler Timeout(秒):" title="资讯采集超时时间(秒)" path="openAI.crawlTimeOut" >
|
||||
<n-input-number min="30" step="1" placeholder="资讯采集超时时间(秒)" v-model:value="formValue.openAI.crawlTimeOut" />
|
||||
</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="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="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="5" 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="5" v-if="formValue.openAI.enable" title="天数越多消耗tokens越多" label="日K线数据(天):" path="openAI.kDays" >
|
||||
<n-input-number min="30" step="1" max="365" placeholder="日K线数据(天)" title="天数越多消耗tokens越多" v-model:value="formValue.openAI.kDays"/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="11" v-if="formValue.openAI.enable" label="模型系统 Prompt:" path="openAI.prompt" >
|
||||
<n-input v-model:value="formValue.openAI.prompt"
|
||||
type="textarea"
|
||||
:show-count="true"
|
||||
placeholder="请输入系统prompt"
|
||||
:autosize="{
|
||||
minRows: 5,
|
||||
maxRows: 6
|
||||
}"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="11" v-if="formValue.openAI.enable" label="模型用户 Prompt:" path="openAI.questionTemplate" >
|
||||
<n-input v-model:value="formValue.openAI.questionTemplate"
|
||||
type="textarea"
|
||||
:show-count="true"
|
||||
placeholder="请输入用户prompt:例如{{stockName}}[{{stockCode}}]分析和总结"
|
||||
:autosize="{
|
||||
minRows: 5,
|
||||
maxRows: 6
|
||||
}"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
<n-grid :cols="24">
|
||||
<n-gi :span="24">
|
||||
<n-space justify="center">
|
||||
<n-button type="warning" @click="managePrompts">
|
||||
添加提示词模板
|
||||
</n-button>
|
||||
<n-button type="primary" @click="saveConfig">
|
||||
保存
|
||||
</n-button>
|
||||
<n-button type="info" @click="exportConfig">
|
||||
导出
|
||||
</n-button>
|
||||
<n-button type="error" @click="importConfig">
|
||||
导入
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-gi>
|
||||
<n-gi :span="24" v-if="promptTemplates.length>0" type="warning">
|
||||
<n-flex justify="start" style="margin-top: 4px" >
|
||||
<n-text type="warning" >
|
||||
<n-flex justify="left" >
|
||||
<n-tag :bordered="false" type="warning" > 提示词模板:</n-tag>
|
||||
<n-tag size="medium" secondary v-if="promptTemplates.length>0" v-for="prompt in promptTemplates" closable @close="deletePrompt(prompt.ID)" @click="editPrompt(prompt)" :title="prompt.content"
|
||||
:type="prompt.type==='模型系统Prompt'?'success':'info'" :bordered="false"> {{ prompt.name }}
|
||||
</n-tag>
|
||||
</n-flex>
|
||||
</n-text>
|
||||
</n-flex>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
</n-form>
|
||||
<n-gi :span="24" v-if="formValue.openAI.enable">
|
||||
<n-divider title-placement="left">AI模型服务配置</n-divider>
|
||||
</n-gi>
|
||||
<n-gi :span="24" v-if="formValue.openAI.enable">
|
||||
<n-space vertical>
|
||||
<n-card v-for="(aiConfig, index) in formValue.openAI.aiConfigs" :key="index" :bordered="true"
|
||||
size="small">
|
||||
<template #header>
|
||||
<n-flex justify="space-between" align="center">
|
||||
<n-text depth="3">AI 配置 #{{ index + 1 }}</n-text>
|
||||
<n-button type="error" size="tiny" ghost @click="removeAiConfig(index)">删除</n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
<n-grid :cols="24" :x-gap="24">
|
||||
<n-form-item-gi :span="24" hidden label="配置ID" :path="`openAI.aiConfigs[${index}].ID`">
|
||||
<n-input type="text" placeholder="配置ID" v-model:value="aiConfig.ID" clearable/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" label="配置名称" :path="`openAI.aiConfigs[${index}].name`">
|
||||
<n-input type="text" placeholder="配置名称" v-model:value="aiConfig.name" clearable/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" label="接口地址" :path="`openAI.aiConfigs[${index}].baseUrl`">
|
||||
<n-input type="text" placeholder="AI接口地址" v-model:value="aiConfig.baseUrl" clearable/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" label="令牌(apiKey)" :path="`openAI.aiConfigs[${index}].apiKey`">
|
||||
<n-input type="password" placeholder="apiKey" v-model:value="aiConfig.apiKey" clearable
|
||||
show-password-on="click"/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="8" label="模型名称" :path="`openAI.aiConfigs[${index}].modelName`">
|
||||
<n-input type="text" placeholder="AI模型名称" v-model:value="aiConfig.modelName" clearable/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="5" label="Temperature" :path="`openAI.aiConfigs[${index}].temperature`">
|
||||
<n-input-number placeholder="temperature" v-model:value="aiConfig.temperature" :step="0.1"/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="5" label="MaxTokens" :path="`openAI.aiConfigs[${index}].maxTokens`">
|
||||
<n-input-number placeholder="maxTokens" v-model:value="aiConfig.maxTokens"/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="5" label="Timeout(秒)" :path="`openAI.aiConfigs[${index}].timeOut`">
|
||||
<n-input-number min="60" step="1" placeholder="超时(秒)" v-model:value="aiConfig.timeOut"/>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
<n-button type="primary" dashed @click="addAiConfig" style="width: 100%;">+ 添加AI配置</n-button>
|
||||
</n-space>
|
||||
</n-gi>
|
||||
|
||||
<n-gi :span="24">
|
||||
<n-divider/>
|
||||
</n-gi>
|
||||
|
||||
<n-gi :span="24">
|
||||
<n-space vertical>
|
||||
<n-space justify="center">
|
||||
<n-button type="warning" @click="managePrompts">管理提示词模板</n-button>
|
||||
<n-button type="primary" strong @click="saveConfig">保存设置</n-button>
|
||||
<n-button type="info" @click="exportConfig">导出配置</n-button>
|
||||
<n-button type="error" @click="importConfig">导入配置</n-button>
|
||||
</n-space>
|
||||
|
||||
<n-flex justify="start" style="margin-top: 10px" v-if="promptTemplates.length > 0">
|
||||
<n-tag :bordered="false" type="warning">提示词模板:</n-tag>
|
||||
<n-tag size="medium" secondary v-for="prompt in promptTemplates" closable
|
||||
@close="deletePrompt(prompt.ID)" @click="editPrompt(prompt)" :title="prompt.content"
|
||||
:type="prompt.type === '模型系统Prompt' ? 'success' : 'info'" :bordered="false">{{
|
||||
prompt.name
|
||||
}}
|
||||
</n-tag>
|
||||
</n-flex>
|
||||
</n-space>
|
||||
</n-gi>
|
||||
|
||||
</n-grid>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</n-form>
|
||||
</n-flex>
|
||||
<n-modal v-model:show="showManagePromptsModal" closable :mask-closable="false">
|
||||
<n-card
|
||||
style="width: 800px;height: 600px;text-align: left"
|
||||
:bordered="false"
|
||||
:title="(formPrompt.ID>0?'修改':'添加')+'提示词'"
|
||||
size="huge"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<n-form ref="formPromptRef" :label-placement="'left'" :label-align="'left'" >
|
||||
<n-form-item label="名称">
|
||||
<n-input v-model:value="formPrompt.Name" placeholder="请输入提示词名称" />
|
||||
|
||||
<n-modal v-model:show="showManagePromptsModal" closable :mask-closable="false">
|
||||
<n-card style="width: 800px; height: 600px; text-align: left" :bordered="false"
|
||||
:title="(formPrompt.ID > 0 ? '修改' : '添加') + '提示词'" size="huge" role="dialog" aria-modal="true">
|
||||
<n-form ref="formPromptRef" :label-placement="'left'" :label-align="'left'">
|
||||
<n-form-item label="名称">
|
||||
<n-input v-model:value="formPrompt.Name" placeholder="请输入提示词名称"/>
|
||||
</n-form-item>
|
||||
<n-form-item label="类型">
|
||||
<n-select v-model:value="formPrompt.Type" :options="promptTypeOptions" placeholder="请选择提示词类型" />
|
||||
<n-form-item label="类型">
|
||||
<n-select v-model:value="formPrompt.Type" :options="promptTypeOptions" placeholder="请选择提示词类型"/>
|
||||
</n-form-item>
|
||||
<n-form-item label="内容">
|
||||
<n-input v-model:value="formPrompt.Content"
|
||||
type="textarea"
|
||||
:show-count="true"
|
||||
placeholder="请输入prompt"
|
||||
:autosize="{
|
||||
minRows: 12,
|
||||
maxRows: 12,
|
||||
}"
|
||||
/>
|
||||
<n-form-item label="内容">
|
||||
<n-input v-model:value="formPrompt.Content" type="textarea" :show-count="true" placeholder="请输入prompt"
|
||||
:autosize="{ minRows: 12, maxRows: 12, }"/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #footer>
|
||||
<n-flex justify="end">
|
||||
<n-button type="primary" @click="savePrompt">
|
||||
保存
|
||||
</n-button>
|
||||
<n-button type="warning" @click="showManagePromptsModal=false">
|
||||
取消
|
||||
</n-button>
|
||||
<n-button type="primary" @click="savePrompt">保存</n-button>
|
||||
<n-button type="warning" @click="showManagePromptsModal = false">取消</n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-card>
|
||||
@@ -481,7 +515,7 @@ function deletePrompt(ID){
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cardHeaderClass{
|
||||
.cardHeaderClass {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: red;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script setup>
|
||||
import {computed, h, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref} from 'vue'
|
||||
import {computed, h, nextTick, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref, watch} from 'vue'
|
||||
import * as echarts from 'echarts';
|
||||
import {
|
||||
AddGroup,
|
||||
AddStockGroup,
|
||||
Follow,
|
||||
GetAiConfigs,
|
||||
GetAIResponseResult,
|
||||
GetConfig,
|
||||
GetFollowList,
|
||||
@@ -15,11 +16,15 @@ import {
|
||||
GetStockMinutePriceLineData,
|
||||
GetVersionInfo,
|
||||
Greet,
|
||||
InitializeGroupSort,
|
||||
NewChatStream,
|
||||
OpenURL,
|
||||
RemoveGroup,
|
||||
RemoveStockGroup,
|
||||
SaveAIResponseResult,
|
||||
SaveAsMarkdown,
|
||||
SaveImage,
|
||||
SaveWordFile,
|
||||
SendDingDingMessageByType,
|
||||
SetAlarmChangePercent,
|
||||
SetCostPriceAndVolume,
|
||||
@@ -27,9 +32,7 @@ import {
|
||||
SetStockSort,
|
||||
ShareAnalysis,
|
||||
UnFollow,
|
||||
OpenURL,
|
||||
SaveImage,
|
||||
SaveWordFile
|
||||
UpdateGroupSort
|
||||
} from '../../wailsjs/go/main/App'
|
||||
import {
|
||||
NAvatar,
|
||||
@@ -67,7 +70,6 @@ import vueDanmaku from 'vue3-danmaku'
|
||||
import {keys, padStart} from "lodash";
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import MoneyTrend from "./moneyTrend.vue";
|
||||
import {TaskTools} from "@vicons/carbon";
|
||||
import StockSparkLine from "./stockSparkLine.vue";
|
||||
|
||||
const route = useRoute()
|
||||
@@ -120,6 +122,7 @@ const formModel = ref({
|
||||
})
|
||||
|
||||
const promptTemplates = ref([])
|
||||
const aiConfigs = ref([])
|
||||
const sysPromptOptions = ref([])
|
||||
const userPromptOptions = ref([])
|
||||
const data = reactive({
|
||||
@@ -127,6 +130,7 @@ const data = reactive({
|
||||
chatId: "",
|
||||
question: "",
|
||||
sysPromptId: null,
|
||||
aiConfigId: null,
|
||||
name: "",
|
||||
code: "",
|
||||
fenshiURL: "",
|
||||
@@ -167,22 +171,137 @@ const sortedResults = computed(() => {
|
||||
|
||||
const groupResults = computed(() => {
|
||||
const group = {}
|
||||
for (const key in sortedResults.value) {
|
||||
if (stocks.value.includes(sortedResults.value[key]['股票代码'])) {
|
||||
group[key] = sortedResults.value[key]
|
||||
if (currentGroupId.value === 0) {
|
||||
return sortedResults.value
|
||||
} else {
|
||||
for (const key in sortedResults.value) {
|
||||
if (stocks.value.includes(sortedResults.value[key]['股票代码'])) {
|
||||
group[key] = sortedResults.value[key]
|
||||
}
|
||||
}
|
||||
return group
|
||||
}
|
||||
return group
|
||||
})
|
||||
const showPopover = ref(false)
|
||||
// 拖拽相关变量
|
||||
const dragSourceIndex = ref(null)
|
||||
const dragTargetIndex = ref(null)
|
||||
|
||||
// 拖拽处理函数
|
||||
function handleTabDragStart(event, name) {
|
||||
// "全部"标签(name=0)不应该触发拖拽
|
||||
if (name === 0) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
dragSourceIndex.value = name;
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.target.classList.add('tab-dragging');
|
||||
}
|
||||
|
||||
|
||||
function handleTabDragOver(event) {
|
||||
event.preventDefault()
|
||||
event.dataTransfer.dropEffect = 'move'
|
||||
}
|
||||
|
||||
function handleTabDragEnter(event, name) {
|
||||
event.preventDefault();
|
||||
// "全部"标签(name=0)不应该作为拖拽目标
|
||||
if (name > 0) {
|
||||
dragTargetIndex.value = name;
|
||||
if (event.target.classList) {
|
||||
// 查找最近的标签元素并添加高亮样式
|
||||
let tabElement = event.target.closest('.n-tabs-tab');
|
||||
if (tabElement) {
|
||||
tabElement.classList.add('tab-drag-over');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleTabDragLeave(event) {
|
||||
// 查找最近的标签元素并移除高亮样式
|
||||
let tabElement = event.target.closest('.n-tabs-tab')
|
||||
if (tabElement && tabElement.classList) {
|
||||
tabElement.classList.remove('tab-drag-over')
|
||||
}
|
||||
// 不要重置 dragTargetIndex,因为可能会在元素间快速移动
|
||||
}
|
||||
|
||||
function handleTabDrop(event) {
|
||||
event.preventDefault();
|
||||
|
||||
// 移除所有高亮样式
|
||||
const tabs = document.querySelectorAll('.n-tabs-tab');
|
||||
tabs.forEach(tab => {
|
||||
tab.classList.remove('tab-drag-over');
|
||||
});
|
||||
|
||||
if (dragSourceIndex.value !== null && dragTargetIndex.value !== null &&
|
||||
dragSourceIndex.value !== dragTargetIndex.value) {
|
||||
|
||||
// 确保索引有效(排除"全部"选项卡)
|
||||
if (dragSourceIndex.value > 0 && dragTargetIndex.value > 0) {
|
||||
// 查找源分组和目标分组
|
||||
const sourceGroup = groupList.value.find(g => g.ID === dragSourceIndex.value);
|
||||
const targetGroup = groupList.value.find(g => g.ID === dragTargetIndex.value);
|
||||
|
||||
if (sourceGroup && targetGroup) {
|
||||
// 计算新的位置序号(使用目标分组的sort值)
|
||||
const newSortPosition = targetGroup.sort;
|
||||
|
||||
// 调用后端API更新组排序
|
||||
UpdateGroupSort(sourceGroup.ID, newSortPosition).then(result => {
|
||||
if (result) {
|
||||
message.success('分组排序更新成功');
|
||||
// 重新获取分组列表以更新界面
|
||||
GetGroupList().then(result => {
|
||||
groupList.value = result;
|
||||
});
|
||||
} else {
|
||||
message.error('分组排序更新失败');
|
||||
}
|
||||
}).catch(error => {
|
||||
message.error('分组排序更新失败: ' + error.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
dragSourceIndex.value = null;
|
||||
dragTargetIndex.value = null;
|
||||
}
|
||||
|
||||
function handleTabDragEnd(event) {
|
||||
// 移除所有高亮样式
|
||||
const tabs = document.querySelectorAll('.n-tabs-tab')
|
||||
tabs.forEach(tab => {
|
||||
tab.classList.remove('tab-drag-over', 'tab-dragging')
|
||||
})
|
||||
|
||||
dragSourceIndex.value = null
|
||||
dragTargetIndex.value = null
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
GetGroupList().then(result => {
|
||||
groupList.value = result
|
||||
if (route.query.groupId) {
|
||||
message.success("切换分组:" + route.query.groupName)
|
||||
currentGroupId.value = Number(route.query.groupId)
|
||||
//console.log("route.params",route.query)
|
||||
// 检查是否存在相同的序号
|
||||
const sorts = result.map(item => item.sort);
|
||||
const uniqueSorts = new Set(sorts);
|
||||
// 如果存在重复的序号,则重新初始化序号
|
||||
if (sorts.length !== uniqueSorts.size) {
|
||||
// 调用InitializeGroupSort重新初始化序号
|
||||
// 然后重新获取分组列表
|
||||
fetchGroupList();
|
||||
} else {
|
||||
// 没有重复序号,继续正常流程
|
||||
if (route.query.groupId) {
|
||||
message.success("切换分组:" + route.query.groupName)
|
||||
currentGroupId.value = Number(route.query.groupId)
|
||||
}
|
||||
}
|
||||
})
|
||||
GetStockList("").then(result => {
|
||||
@@ -211,15 +330,168 @@ onBeforeMount(() => {
|
||||
sysPromptOptions.value = promptTemplates.value.filter(item => item.type === '模型系统Prompt')
|
||||
userPromptOptions.value = promptTemplates.value.filter(item => item.type === '模型用户Prompt')
|
||||
|
||||
//console.log("userPromptOptions",userPromptOptions.value)
|
||||
//console.log("sysPromptOptions",sysPromptOptions.value)
|
||||
})
|
||||
|
||||
GetAiConfigs().then(res => {
|
||||
aiConfigs.value = res
|
||||
data.aiConfigId = res[0].ID
|
||||
})
|
||||
|
||||
EventsOn("loadingDone", (data) => {
|
||||
message.loading("刷新股票基础数据...")
|
||||
GetStockList("").then(result => {
|
||||
stockList.value = result
|
||||
options.value = result.map(item => {
|
||||
return {
|
||||
label: item.name + " - " + item.ts_code,
|
||||
value: item.ts_code
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
EventsOn("refresh", (data) => {
|
||||
message.success(data)
|
||||
})
|
||||
|
||||
EventsOn("showSearch", (data) => {
|
||||
addBTN.value = data === 1;
|
||||
})
|
||||
|
||||
EventsOn("stock_price", (data) => {
|
||||
updateData(data)
|
||||
})
|
||||
|
||||
EventsOn("refreshFollowList", (data) => {
|
||||
|
||||
WindowReload()
|
||||
})
|
||||
|
||||
EventsOn("newChatStream", async (msg) => {
|
||||
data.loading = false
|
||||
if (msg === "DONE") {
|
||||
SaveAIResponseResult(data.code, data.name, data.airesult, data.chatId, data.question, data.aiConfigId)
|
||||
message.info("AI分析完成!")
|
||||
message.destroyAll()
|
||||
} else {
|
||||
if (msg.chatId) {
|
||||
data.chatId = msg.chatId
|
||||
}
|
||||
if (msg.question) {
|
||||
data.question = msg.question
|
||||
}
|
||||
if (msg.content) {
|
||||
data.airesult = data.airesult + msg.content
|
||||
}
|
||||
if (msg.extraContent) {
|
||||
data.airesult = data.airesult + msg.extraContent
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
EventsOn("changeTab", async (msg) => {
|
||||
currentGroupId.value = Number(msg.ID)
|
||||
nextTick(() => {
|
||||
updateTab(currentGroupId.value);
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
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}`;
|
||||
|
||||
notify.info({
|
||||
avatar: () =>
|
||||
h(NAvatar, {
|
||||
size: 'small',
|
||||
round: false,
|
||||
src: icon.value
|
||||
}),
|
||||
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: 5000,
|
||||
meta: "发布时间:" + formattedDate,
|
||||
action: () => {
|
||||
return h(NButton, {
|
||||
type: 'primary',
|
||||
size: 'small',
|
||||
onClick: () => {
|
||||
Environment().then(env => {
|
||||
switch (env.platform) {
|
||||
case 'windows':
|
||||
window.open(msg.html_url)
|
||||
break
|
||||
default :
|
||||
OpenURL(msg.html_url)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, {default: () => '查看'})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
EventsOn("warnMsg", async (msg) => {
|
||||
notify.error({
|
||||
avatar: () =>
|
||||
h(NAvatar, {
|
||||
size: 'small',
|
||||
round: false,
|
||||
src: icon.value
|
||||
}),
|
||||
title: '警告',
|
||||
duration: 5000,
|
||||
content: () => {
|
||||
return h('div', {
|
||||
style: {
|
||||
'text-align': 'left',
|
||||
'font-size': '14px',
|
||||
}
|
||||
}, {default: () => msg})
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
message.loading("Loading...")
|
||||
nextTick(() => {
|
||||
initDraggableTabs();
|
||||
});
|
||||
|
||||
// 监听分组列表变化,重新初始化拖拽
|
||||
const unwatch = watch(groupList, () => {
|
||||
nextTick(() => {
|
||||
initDraggableTabs();
|
||||
});
|
||||
});
|
||||
|
||||
// 在组件卸载时清理监听器
|
||||
onBeforeUnmount(() => {
|
||||
unwatch();
|
||||
});
|
||||
message.loading("Loading...")
|
||||
GetFollowList(currentGroupId.value).then(result => {
|
||||
|
||||
followList.value = result
|
||||
@@ -238,7 +510,6 @@ onMounted(() => {
|
||||
message.destroyAll()
|
||||
})
|
||||
|
||||
|
||||
GetVersionInfo().then((res) => {
|
||||
icon.value = res.icon;
|
||||
});
|
||||
@@ -264,6 +535,48 @@ onMounted(() => {
|
||||
//console.log('WebSocket 连接已关闭');
|
||||
};
|
||||
})
|
||||
// 清理拖拽事件监听器
|
||||
// 清理拖拽事件监听器
|
||||
function cleanupDraggableTabs() {
|
||||
const tabs = document.querySelectorAll('.n-tabs-tab');
|
||||
tabs.forEach((tab) => {
|
||||
// 移除所有可能的拖拽事件监听器
|
||||
tab.removeEventListener('dragstart', handleTabDragStart);
|
||||
tab.removeEventListener('dragover', handleTabDragOver);
|
||||
tab.removeEventListener('dragenter', handleTabDragEnter);
|
||||
tab.removeEventListener('dragleave', handleTabDragLeave);
|
||||
tab.removeEventListener('drop', handleTabDrop);
|
||||
tab.removeEventListener('dragend', handleTabDragEnd);
|
||||
// 移除draggable属性
|
||||
tab.removeAttribute('draggable');
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化可拖拽选项卡
|
||||
function initDraggableTabs() {
|
||||
// 移除之前可能添加的事件监听器
|
||||
cleanupDraggableTabs();
|
||||
|
||||
// 添加拖拽事件监听器到选项卡元素
|
||||
setTimeout(() => {
|
||||
const tabs = document.querySelectorAll('.n-tabs-tab');
|
||||
tabs.forEach((tab, index) => {
|
||||
const dataIndex = tab.getAttribute('data-name');
|
||||
const name = parseInt(dataIndex);
|
||||
|
||||
// 只为分组标签(name > 0)添加拖拽功能
|
||||
if (name > 0) {
|
||||
tab.setAttribute('draggable', 'true');
|
||||
tab.addEventListener('dragstart', (e) => handleTabDragStart(e, name));
|
||||
tab.addEventListener('dragover', handleTabDragOver);
|
||||
tab.addEventListener('dragenter', (e) => handleTabDragEnter(e, name));
|
||||
tab.addEventListener('dragleave', handleTabDragLeave);
|
||||
tab.addEventListener('drop', handleTabDrop);
|
||||
tab.addEventListener('dragend', handleTabDragEnd);
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// //console.log(`the component is now unmounted.`)
|
||||
@@ -282,146 +595,9 @@ onBeforeUnmount(() => {
|
||||
EventsOff("updateVersion")
|
||||
EventsOff("warnMsg")
|
||||
EventsOff("loadingDone")
|
||||
})
|
||||
|
||||
EventsOn("loadingDone", (data) => {
|
||||
message.loading("刷新股票基础数据...")
|
||||
GetStockList("").then(result => {
|
||||
stockList.value = result
|
||||
options.value = result.map(item => {
|
||||
return {
|
||||
label: item.name + " - " + item.ts_code,
|
||||
value: item.ts_code
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
cleanupDraggableTabs()
|
||||
|
||||
EventsOn("refresh", (data) => {
|
||||
message.success(data)
|
||||
})
|
||||
|
||||
EventsOn("showSearch", (data) => {
|
||||
addBTN.value = data === 1;
|
||||
})
|
||||
|
||||
EventsOn("stock_price", (data) => {
|
||||
updateData(data)
|
||||
})
|
||||
|
||||
EventsOn("refreshFollowList", (data) => {
|
||||
|
||||
WindowReload()
|
||||
})
|
||||
|
||||
EventsOn("newChatStream", async (msg) => {
|
||||
////console.log("newChatStream:->",data.airesult)
|
||||
data.loading = false
|
||||
////console.log(msg)
|
||||
if (msg === "DONE") {
|
||||
SaveAIResponseResult(data.code, data.name, data.airesult, data.chatId, data.question)
|
||||
message.info("AI分析完成!")
|
||||
message.destroyAll()
|
||||
} else {
|
||||
if (msg.chatId) {
|
||||
data.chatId = msg.chatId
|
||||
}
|
||||
if (msg.question) {
|
||||
data.question = msg.question
|
||||
}
|
||||
if (msg.content) {
|
||||
data.airesult = data.airesult + msg.content
|
||||
}
|
||||
if (msg.extraContent) {
|
||||
data.airesult = data.airesult + msg.extraContent
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
EventsOn("changeTab", async (msg) => {
|
||||
//console.log("changeTab",msg)
|
||||
currentGroupId.value = msg.ID
|
||||
updateTab(currentGroupId.value)
|
||||
})
|
||||
|
||||
|
||||
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: icon.value
|
||||
}),
|
||||
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: 5000,
|
||||
meta: "发布时间:" + formattedDate,
|
||||
action: () => {
|
||||
return h(NButton, {
|
||||
type: 'primary',
|
||||
size: 'small',
|
||||
onClick: () => {
|
||||
Environment().then(env => {
|
||||
switch (env.platform) {
|
||||
case 'windows':
|
||||
window.open(msg.html_url)
|
||||
break
|
||||
default :
|
||||
OpenURL(msg.html_url)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, {default: () => '查看'})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
EventsOn("warnMsg", async (msg) => {
|
||||
notify.error({
|
||||
avatar: () =>
|
||||
h(NAvatar, {
|
||||
size: 'small',
|
||||
round: false,
|
||||
src: icon.value
|
||||
}),
|
||||
title: '警告',
|
||||
duration: 5000,
|
||||
content: () => {
|
||||
return h('div', {
|
||||
style: {
|
||||
'text-align': 'left',
|
||||
'font-size': '14px',
|
||||
}
|
||||
}, {default: () => msg})
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
//判断是否是A股交易时间
|
||||
@@ -444,6 +620,23 @@ function isTradingTime() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 添加一个获取分组列表的函数,用于处理初始化逻辑
|
||||
function fetchGroupList() {
|
||||
InitializeGroupSort().then(initResult => {
|
||||
if (initResult) {
|
||||
GetGroupList().then(result => {
|
||||
groupList.value = result
|
||||
if (route.query.groupId) {
|
||||
message.success("切换分组:" + route.query.groupName)
|
||||
currentGroupId.value = Number(route.query.groupId)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
message.error("初始化分组序号失败")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function AddStock() {
|
||||
if (!data?.code) {
|
||||
message.error("请输入有效股票代码");
|
||||
@@ -1387,7 +1580,7 @@ function aiReCheckStock(stock, stockCode) {
|
||||
//
|
||||
|
||||
//message.info("sysPromptId:"+data.sysPromptId)
|
||||
NewChatStream(stock, stockCode, data.question, data.sysPromptId, enableTools.value)
|
||||
NewChatStream(stock, stockCode, data.question, data.aiConfigId, data.sysPromptId, enableTools.value)
|
||||
}
|
||||
|
||||
function aiCheckStock(stock, stockCode) {
|
||||
@@ -1491,14 +1684,14 @@ function saveAsImage(name, code) {
|
||||
}
|
||||
|
||||
async function saveCanvasImage(name) {
|
||||
const element = document.querySelector('.md-editor-preview'); // 要截图的 DOM 节点
|
||||
const element = document.querySelector('.md-editor-preview'); // 要截图的 DOM 节点
|
||||
const canvas = await html2canvas(element)
|
||||
|
||||
const dataUrl = canvas.toDataURL('image/png') // base64 格式
|
||||
const base64 = dataUrl.replace(/^data:image\/png;base64,/, '')
|
||||
|
||||
// 调用 Go 后端保存文件(Wails 绑定方法)
|
||||
await SaveImage(name,base64).then(result => {
|
||||
await SaveImage(name, base64).then(result => {
|
||||
message.success(result)
|
||||
})
|
||||
}
|
||||
@@ -1560,7 +1753,7 @@ AI赋能股票分析:自选股行情获取,成本盈亏展示,涨跌报警
|
||||
`
|
||||
// landscape就是横着的,portrait是竖着的,默认是竖屏portrait。
|
||||
const blob = await asBlob(value, {orientation: 'portrait'})
|
||||
const { platform } = await Environment()
|
||||
const {platform} = await Environment()
|
||||
switch (platform) {
|
||||
case 'windows':
|
||||
const a = document.createElement('a')
|
||||
@@ -1572,13 +1765,13 @@ AI赋能股票分析:自选股行情获取,成本盈亏展示,涨跌报警
|
||||
a.remove()
|
||||
break
|
||||
default:
|
||||
const arrayBuffer = await blob.arrayBuffer()
|
||||
const uint8Array = new Uint8Array(arrayBuffer)
|
||||
const binary = uint8Array.reduce((data, byte) => data + String.fromCharCode(byte), '')
|
||||
const base64 = btoa(binary)
|
||||
await SaveWordFile(`${data.name}[${data.code}]-ai-analysis-result.docx`, base64).then(result => {
|
||||
message.success(result)
|
||||
})
|
||||
const arrayBuffer = await blob.arrayBuffer()
|
||||
const uint8Array = new Uint8Array(arrayBuffer)
|
||||
const binary = uint8Array.reduce((data, byte) => data + String.fromCharCode(byte), '')
|
||||
const base64 = btoa(binary)
|
||||
await SaveWordFile(`${data.name}[${data.code}]-ai-analysis-result.docx`, base64).then(result => {
|
||||
message.success(result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1641,9 +1834,11 @@ function AddStockGroupInfo(groupId, code, name) {
|
||||
|
||||
function updateTab(name) {
|
||||
stocks.value = []
|
||||
currentGroupId.value = Number(name)
|
||||
GetFollowList(currentGroupId.value).then(result => {
|
||||
const tabId= Number(name)
|
||||
currentGroupId.value = tabId;
|
||||
GetFollowList(tabId).then(result => {
|
||||
followList.value = result
|
||||
|
||||
for (const followedStock of result) {
|
||||
if (followedStock.StockCode.startsWith("us")) {
|
||||
followedStock.StockCode = "gb_" + followedStock.StockCode.replace("us", "").toLowerCase()
|
||||
@@ -1658,8 +1853,8 @@ function updateTab(name) {
|
||||
})
|
||||
}
|
||||
|
||||
function delTab(name) {
|
||||
let infos = groupList.value = groupList.value.filter(item => item.ID === Number(name))
|
||||
function delTab(groupId) {
|
||||
let infos = groupList.value = groupList.value.filter(item => item.ID === Number(groupId))
|
||||
dialog.create({
|
||||
title: '删除分组',
|
||||
type: 'warning',
|
||||
@@ -1667,7 +1862,7 @@ function delTab(name) {
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
RemoveGroup(name).then(result => {
|
||||
RemoveGroup(Number(groupId)).then(result => {
|
||||
message.info(result)
|
||||
GetGroupList().then(result => {
|
||||
groupList.value = result
|
||||
@@ -1715,9 +1910,10 @@ function searchStockReport(stockCode) {
|
||||
</n-gradient-text>
|
||||
</template>
|
||||
</vue-danmaku>
|
||||
<n-tabs type="card" style="--wails-draggable:drag" animated addable :data-currentGroupId="currentGroupId"
|
||||
:value="currentGroupId" @add="addTab" @update-value="updateTab" placement="top" @close="(key)=>{delTab(key)}">
|
||||
<n-tab-pane :name="0" :tab="'全部'">
|
||||
<n-tabs type="card" style="--wails-draggable:no-drag" animated addable :data-currentGroupId="currentGroupId"
|
||||
:value="String(currentGroupId)" @add="addTab" @update:value="updateTab" placement="top" @close="(key)=>{delTab(key)}">
|
||||
|
||||
<n-tab-pane closable name="0" :tab="'全部'">
|
||||
<n-grid :x-gap="8" :cols="3" :y-gap="8">
|
||||
<n-gi :id="result['股票代码']+'_gi'" v-for="result in sortedResults" style="margin-left: 2px;">
|
||||
<n-card :data-sort="result.sort" :id="result['股票代码']" :data-code="result['股票代码']" :bordered="true"
|
||||
@@ -1856,7 +2052,7 @@ function searchStockReport(stockCode) {
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane closable v-for="group in groupList" :group-id="group.ID" :name="group.ID" :tab="group.name">
|
||||
<n-tab-pane closable v-for="group in groupList" :group-id="group.ID" :name="String(group.ID)" :tab="group.name">
|
||||
<n-grid :x-gap="8" :cols="3" :y-gap="8">
|
||||
<n-gi :id="result['股票代码']+'_gi'" v-for="result in groupResults" style="margin-left: 2px;">
|
||||
<n-card :data-sort="result.sort" :id="result['股票代码']" :data-code="result['股票代码']" :bordered="true"
|
||||
@@ -2003,6 +2199,7 @@ function searchStockReport(stockCode) {
|
||||
</n-grid>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
|
||||
<div style="position: fixed;bottom: 18px;right:5px;z-index: 10;width: 400px">
|
||||
<!-- <n-card :bordered="false">-->
|
||||
<n-input-group>
|
||||
@@ -2161,9 +2358,11 @@ function searchStockReport(stockCode) {
|
||||
</n-gradient-text>
|
||||
</n-flex>
|
||||
<n-flex justify="space-between" style="margin-bottom: 10px">
|
||||
<n-select style="width: 49%" v-model:value="data.sysPromptId" label-field="name" value-field="ID"
|
||||
<n-select style="width: 31%" v-model:value="data.aiConfigId" label-field="name" value-field="ID"
|
||||
:options="aiConfigs" placeholder="请选择AI模型服务配置"/>
|
||||
<n-select style="width: 31%" v-model:value="data.sysPromptId" label-field="name" value-field="ID"
|
||||
:options="sysPromptOptions" placeholder="请选择系统提示词"/>
|
||||
<n-select style="width: 49%" v-model:value="data.question" label-field="name" value-field="content"
|
||||
<n-select style="width: 31%" v-model:value="data.question" label-field="name" value-field="content"
|
||||
:options="userPromptOptions" placeholder="请选择用户提示词"/>
|
||||
</n-flex>
|
||||
<n-flex justify="right">
|
||||
@@ -2218,4 +2417,38 @@ function searchStockReport(stockCode) {
|
||||
border-color: red;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
/* 所有标签的通用样式 */
|
||||
:deep(.n-tabs-nav .n-tabs-tab) {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 可拖拽标签的样式 */
|
||||
:deep(.n-tabs-nav .n-tabs-tab[draggable="true"]) {
|
||||
user-select: none;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.tab-drag-over {
|
||||
background-color: #e6f7ff !important;
|
||||
border: 2px dashed #1890ff !important;
|
||||
transform: scale(1.02);
|
||||
transition: all 0.2s ease;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.tab-drag-over::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.tab-dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
@@ -9,18 +9,25 @@ import EmbeddedUrl from "./EmbeddedUrl.vue";
|
||||
<n-tab-pane name="选股通" tab="选股通">
|
||||
<embedded-url url="https://xuangutong.com.cn" :height="'calc(100vh - 252px)'"/>
|
||||
</n-tab-pane>
|
||||
<!-- <n-tab-pane name="百度股市通" tab="百度股市通">-->
|
||||
<!-- <embedded-url url="https://gushitong.baidu.com" :height="'calc(100vh - 252px)'"/>-->
|
||||
<!-- </n-tab-pane>-->
|
||||
<n-tab-pane name="百度股市通" tab="百度股市通">
|
||||
<embedded-url url="https://gushitong.baidu.com" :height="'calc(100vh - 252px)'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="东财大盘星图" tab="东财大盘星图">
|
||||
<embedded-url url="https://quote.eastmoney.com/stockhotmap/" :height="'calc(100vh - 252px)'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="TopHub" tab="TopHub(今日热榜)">
|
||||
<embedded-url url="https://tophub.today/c/finance" :height="'calc(100vh - 252px)'"/>
|
||||
</n-tab-pane>
|
||||
<!-- <n-tab-pane name="摸鱼" tab="摸鱼">-->
|
||||
<!-- <embedded-url url="https://996.ninja/" :height="'calc(100vh - 252px)'"/>-->
|
||||
<!-- </n-tab-pane>-->
|
||||
<n-tab-pane name="摸鱼" tab="摸鱼">
|
||||
<embedded-url url="https://996.ninja/" :height="'calc(100vh - 252px)'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="财联社-行情数据" tab="财联社-行情数据">
|
||||
<embedded-url url="https://www.cls.cn/quotation" :height="'calc(100vh - 252px)'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="消息墙" tab="消息墙">
|
||||
<embedded-url url="https://go-stock.sparkmemory.top:16667/go-stock" :height="'calc(100vh - 252px)'"/>
|
||||
</n-tab-pane>
|
||||
|
||||
|
||||
|
||||
<n-tab-pane name="欢迎推荐更多有趣的财经网页" tab="欢迎推荐更多有趣的财经网页">
|
||||
|
||||
@@ -2,7 +2,8 @@ import {createApp} from 'vue'
|
||||
import naive from 'naive-ui'
|
||||
import App from './App.vue'
|
||||
import router from './router/router'
|
||||
|
||||
// 引入组件库的少量全局样式变量
|
||||
import 'tdesign-vue-next/es/style/index.css';
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(router)
|
||||
|
||||
101
frontend/src/mock-data/index.ts
Normal file
101
frontend/src/mock-data/index.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
export class MockSSEResponse {
|
||||
private controller!: ReadableStreamDefaultController<Uint8Array>;
|
||||
private encoder = new TextEncoder();
|
||||
private stream: ReadableStream<Uint8Array>;
|
||||
private error: boolean;
|
||||
private currentPhase: 'reasoning' | 'content' = 'reasoning';
|
||||
|
||||
constructor(
|
||||
private data: {
|
||||
reasoning: string; // 推理内容
|
||||
content: string; // 正式内容
|
||||
},
|
||||
private delay: number = 100,
|
||||
error = false,
|
||||
) {
|
||||
this.error = error;
|
||||
|
||||
this.stream = new ReadableStream({
|
||||
start: (controller) => {
|
||||
this.controller = controller;
|
||||
if (!this.error) {
|
||||
// 如果不是错误情况,则开始推送数据
|
||||
setTimeout(() => this.pushData(), this.delay); // 延迟开始推送数据
|
||||
}
|
||||
},
|
||||
cancel() {},
|
||||
});
|
||||
}
|
||||
|
||||
private pushData() {
|
||||
try {
|
||||
if (this.currentPhase === 'reasoning') {
|
||||
// 推送推理内容
|
||||
if (this.data.reasoning.length > 0) {
|
||||
const chunk = JSON.stringify({
|
||||
delta: {
|
||||
reasoning_content: this.data.reasoning.slice(0, 1),
|
||||
content: '',
|
||||
},
|
||||
finished: false,
|
||||
});
|
||||
this.controller.enqueue(this.encoder.encode(chunk));
|
||||
this.data.reasoning = this.data.reasoning.slice(1);
|
||||
// 设置下次推送
|
||||
setTimeout(() => this.pushData(), this.delay);
|
||||
} else {
|
||||
// 推理内容推送完成,切换到正式内容
|
||||
this.currentPhase = 'content';
|
||||
setTimeout(() => this.pushData(), this.delay); // 立即开始推送正式内容
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.currentPhase === 'content') {
|
||||
// 推送正式内容
|
||||
if (this.data.content.length > 0) {
|
||||
const chunk = JSON.stringify({
|
||||
delta: {
|
||||
reasoning_content: '',
|
||||
content: this.data.content.slice(0, 1),
|
||||
},
|
||||
finished: this.data.content.length === 1, // 最后一个字符时标记完成
|
||||
});
|
||||
this.controller.enqueue(this.encoder.encode(chunk));
|
||||
this.data.content = this.data.content.slice(1);
|
||||
|
||||
// 设置下次推送
|
||||
setTimeout(() => this.pushData(), this.delay);
|
||||
} else {
|
||||
// const finalPayload = JSON.stringify({
|
||||
// delta: {
|
||||
// reasoning_content: '',
|
||||
// content: '',
|
||||
// },
|
||||
// finished: true,
|
||||
// });
|
||||
// this.controller.enqueue(this.encoder.encode(`${finalPayload}`));
|
||||
// 全部内容推送完成
|
||||
setTimeout(() => this.controller.close(), this.delay);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
getResponse(): Promise<Response> {
|
||||
return new Promise((resolve) => {
|
||||
// 使用setTimeout来模拟网络延迟
|
||||
setTimeout(() => {
|
||||
if (this.error) {
|
||||
const errorResponseOptions = { status: 500, statusText: 'Internal Server Error' };
|
||||
|
||||
// 返回模拟的网络错误响应,这里我们使用500状态码作为示例
|
||||
resolve(new Response(null, errorResponseOptions));
|
||||
} else {
|
||||
resolve(new Response(this.stream));
|
||||
}
|
||||
}, this.delay); // 使用构造函数中设置的delay值作为延迟时间
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import settingsView from '../components/settings.vue'
|
||||
import aboutView from "../components/about.vue";
|
||||
import fundView from "../components/fund.vue";
|
||||
import marketView from "../components/market.vue";
|
||||
import agentChat from "../components/agent-chat.vue"
|
||||
|
||||
const routes = [
|
||||
{ path: '/', component: stockView,name: 'stock'},
|
||||
@@ -12,7 +13,7 @@ const routes = [
|
||||
{ path: '/settings', component: settingsView,name: 'settings' },
|
||||
{ path: '/about', component: aboutView,name: 'about' },
|
||||
{ path: '/market', component: marketView,name: 'market' },
|
||||
|
||||
{ path: '/agent', component: agentChat,name: 'agent' },
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
import {defineConfig} from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import AutoImport from 'unplugin-auto-import/vite';
|
||||
import Components from 'unplugin-vue-components/vite';
|
||||
import { TDesignResolver } from '@tdesign-vue-next/auto-import-resolver';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()]
|
||||
plugins: [
|
||||
vue(),
|
||||
AutoImport({
|
||||
resolvers: [TDesignResolver({
|
||||
library: 'chat'
|
||||
})],
|
||||
}),
|
||||
Components({
|
||||
resolvers: [TDesignResolver({
|
||||
library: 'chat'
|
||||
})],
|
||||
}),
|
||||
]
|
||||
})
|
||||
|
||||
20
frontend/wailsjs/go/main/App.d.ts
vendored
20
frontend/wailsjs/go/main/App.d.ts
vendored
@@ -14,6 +14,10 @@ export function AddStockGroup(arg1:number,arg2:string):Promise<string>;
|
||||
|
||||
export function AnalyzeSentiment(arg1:string):Promise<data.SentimentResult>;
|
||||
|
||||
export function AnalyzeSentimentWithFreqWeight(arg1:string):Promise<Record<string, any>>;
|
||||
|
||||
export function ChatWithAgent(arg1:string,arg2:number,arg3:any):Promise<void>;
|
||||
|
||||
export function CheckSponsorCode(arg1:string):Promise<Record<string, any>>;
|
||||
|
||||
export function CheckStockBaseInfo(arg1:context.Context):Promise<void>;
|
||||
@@ -34,7 +38,9 @@ export function FollowFund(arg1:string):Promise<string>;
|
||||
|
||||
export function GetAIResponseResult(arg1:string):Promise<models.AIResponseResult>;
|
||||
|
||||
export function GetConfig():Promise<data.Settings>;
|
||||
export function GetAiConfigs():Promise<Array<data.AIConfig>>;
|
||||
|
||||
export function GetConfig():Promise<data.SettingConfig>;
|
||||
|
||||
export function GetFollowList(arg1:number):Promise<any>;
|
||||
|
||||
@@ -84,11 +90,13 @@ export function HotTopic(arg1:number):Promise<Array<any>>;
|
||||
|
||||
export function IndustryResearchReport(arg1:string):Promise<Array<any>>;
|
||||
|
||||
export function InitializeGroupSort():Promise<boolean>;
|
||||
|
||||
export function InvestCalendarTimeLine(arg1:string):Promise<Array<any>>;
|
||||
|
||||
export function LongTigerRank(arg1:string):Promise<any>;
|
||||
|
||||
export function NewChatStream(arg1:string,arg2:string,arg3:string,arg4:any,arg5:boolean):Promise<void>;
|
||||
export function NewChatStream(arg1:string,arg2:string,arg3:string,arg4:number,arg5:any,arg6:boolean):Promise<void>;
|
||||
|
||||
export function NewsPush(arg1:any):Promise<void>;
|
||||
|
||||
@@ -100,7 +108,7 @@ export function RemoveGroup(arg1:number):Promise<string>;
|
||||
|
||||
export function RemoveStockGroup(arg1:string,arg2:string,arg3:number):Promise<string>;
|
||||
|
||||
export function SaveAIResponseResult(arg1:string,arg2:string,arg3:string,arg4:string,arg5:string):Promise<void>;
|
||||
export function SaveAIResponseResult(arg1:string,arg2:string,arg3:string,arg4:string,arg5:string,arg6:number):Promise<void>;
|
||||
|
||||
export function SaveAsMarkdown(arg1:string,arg2:string):Promise<string>;
|
||||
|
||||
@@ -128,10 +136,12 @@ export function StockNotice(arg1:string):Promise<Array<any>>;
|
||||
|
||||
export function StockResearchReport(arg1:string):Promise<Array<any>>;
|
||||
|
||||
export function SummaryStockNews(arg1:string,arg2:any,arg3:boolean):Promise<void>;
|
||||
export function SummaryStockNews(arg1:string,arg2:number,arg3:any,arg4:boolean):Promise<void>;
|
||||
|
||||
export function UnFollow(arg1:string):Promise<string>;
|
||||
|
||||
export function UnFollowFund(arg1:string):Promise<string>;
|
||||
|
||||
export function UpdateConfig(arg1:data.Settings):Promise<string>;
|
||||
export function UpdateConfig(arg1:data.SettingConfig):Promise<string>;
|
||||
|
||||
export function UpdateGroupSort(arg1:number,arg2:number):Promise<boolean>;
|
||||
|
||||
@@ -22,6 +22,14 @@ export function AnalyzeSentiment(arg1) {
|
||||
return window['go']['main']['App']['AnalyzeSentiment'](arg1);
|
||||
}
|
||||
|
||||
export function AnalyzeSentimentWithFreqWeight(arg1) {
|
||||
return window['go']['main']['App']['AnalyzeSentimentWithFreqWeight'](arg1);
|
||||
}
|
||||
|
||||
export function ChatWithAgent(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['ChatWithAgent'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function CheckSponsorCode(arg1) {
|
||||
return window['go']['main']['App']['CheckSponsorCode'](arg1);
|
||||
}
|
||||
@@ -62,6 +70,10 @@ export function GetAIResponseResult(arg1) {
|
||||
return window['go']['main']['App']['GetAIResponseResult'](arg1);
|
||||
}
|
||||
|
||||
export function GetAiConfigs() {
|
||||
return window['go']['main']['App']['GetAiConfigs']();
|
||||
}
|
||||
|
||||
export function GetConfig() {
|
||||
return window['go']['main']['App']['GetConfig']();
|
||||
}
|
||||
@@ -162,6 +174,10 @@ export function IndustryResearchReport(arg1) {
|
||||
return window['go']['main']['App']['IndustryResearchReport'](arg1);
|
||||
}
|
||||
|
||||
export function InitializeGroupSort() {
|
||||
return window['go']['main']['App']['InitializeGroupSort']();
|
||||
}
|
||||
|
||||
export function InvestCalendarTimeLine(arg1) {
|
||||
return window['go']['main']['App']['InvestCalendarTimeLine'](arg1);
|
||||
}
|
||||
@@ -170,8 +186,8 @@ export function LongTigerRank(arg1) {
|
||||
return window['go']['main']['App']['LongTigerRank'](arg1);
|
||||
}
|
||||
|
||||
export function NewChatStream(arg1, arg2, arg3, arg4, arg5) {
|
||||
return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3, arg4, arg5);
|
||||
export function NewChatStream(arg1, arg2, arg3, arg4, arg5, arg6) {
|
||||
return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3, arg4, arg5, arg6);
|
||||
}
|
||||
|
||||
export function NewsPush(arg1) {
|
||||
@@ -194,8 +210,8 @@ export function RemoveStockGroup(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['RemoveStockGroup'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function SaveAIResponseResult(arg1, arg2, arg3, arg4, arg5) {
|
||||
return window['go']['main']['App']['SaveAIResponseResult'](arg1, arg2, arg3, arg4, arg5);
|
||||
export function SaveAIResponseResult(arg1, arg2, arg3, arg4, arg5, arg6) {
|
||||
return window['go']['main']['App']['SaveAIResponseResult'](arg1, arg2, arg3, arg4, arg5, arg6);
|
||||
}
|
||||
|
||||
export function SaveAsMarkdown(arg1, arg2) {
|
||||
@@ -250,8 +266,8 @@ export function StockResearchReport(arg1) {
|
||||
return window['go']['main']['App']['StockResearchReport'](arg1);
|
||||
}
|
||||
|
||||
export function SummaryStockNews(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['SummaryStockNews'](arg1, arg2, arg3);
|
||||
export function SummaryStockNews(arg1, arg2, arg3, arg4) {
|
||||
return window['go']['main']['App']['SummaryStockNews'](arg1, arg2, arg3, arg4);
|
||||
}
|
||||
|
||||
export function UnFollow(arg1) {
|
||||
@@ -265,3 +281,7 @@ export function UnFollowFund(arg1) {
|
||||
export function UpdateConfig(arg1) {
|
||||
return window['go']['main']['App']['UpdateConfig'](arg1);
|
||||
}
|
||||
|
||||
export function UpdateGroupSort(arg1, arg2) {
|
||||
return window['go']['main']['App']['UpdateGroupSort'](arg1, arg2);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,55 @@
|
||||
export namespace data {
|
||||
|
||||
export class AIConfig {
|
||||
ID: number;
|
||||
// Go type: time
|
||||
CreatedAt: any;
|
||||
// Go type: time
|
||||
UpdatedAt: any;
|
||||
name: string;
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
modelName: string;
|
||||
maxTokens: number;
|
||||
temperature: number;
|
||||
timeOut: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new AIConfig(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.name = source["name"];
|
||||
this.baseUrl = source["baseUrl"];
|
||||
this.apiKey = source["apiKey"];
|
||||
this.modelName = source["modelName"];
|
||||
this.maxTokens = source["maxTokens"];
|
||||
this.temperature = source["temperature"];
|
||||
this.timeOut = source["timeOut"];
|
||||
}
|
||||
|
||||
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 FundBasic {
|
||||
ID: number;
|
||||
// Go type: time
|
||||
@@ -246,6 +296,7 @@ export namespace data {
|
||||
Cron?: string;
|
||||
IsDel: number;
|
||||
Groups: GroupStock[];
|
||||
AiConfigId: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new FollowedStock(source);
|
||||
@@ -267,6 +318,7 @@ export namespace data {
|
||||
this.Cron = source["Cron"];
|
||||
this.IsDel = source["IsDel"];
|
||||
this.Groups = this.convertValues(source["Groups"], GroupStock);
|
||||
this.AiConfigId = source["AiConfigId"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
@@ -310,7 +362,7 @@ export namespace data {
|
||||
this.Description = source["Description"];
|
||||
}
|
||||
}
|
||||
export class Settings {
|
||||
export class SettingConfig {
|
||||
ID: number;
|
||||
// Go type: time
|
||||
CreatedAt: any;
|
||||
@@ -325,12 +377,6 @@ export namespace data {
|
||||
updateBasicInfoOnStart: boolean;
|
||||
refreshInterval: number;
|
||||
openAiEnable: boolean;
|
||||
openAiBaseUrl: string;
|
||||
openAiApiKey: string;
|
||||
openAiModelName: string;
|
||||
openAiMaxTokens: number;
|
||||
openAiTemperature: number;
|
||||
openAiApiTimeOut: number;
|
||||
prompt: string;
|
||||
checkUpdate: boolean;
|
||||
questionTemplate: string;
|
||||
@@ -343,10 +389,16 @@ export namespace data {
|
||||
browserPoolSize: number;
|
||||
enableFund: boolean;
|
||||
enablePushNews: boolean;
|
||||
enableOnlyPushRedNews: boolean;
|
||||
sponsorCode: string;
|
||||
httpProxy: string;
|
||||
httpProxyEnabled: boolean;
|
||||
enableAgent: boolean;
|
||||
qgqpBId: string;
|
||||
aiConfigs: AIConfig[];
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Settings(source);
|
||||
return new SettingConfig(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
@@ -362,12 +414,6 @@ export namespace data {
|
||||
this.updateBasicInfoOnStart = source["updateBasicInfoOnStart"];
|
||||
this.refreshInterval = source["refreshInterval"];
|
||||
this.openAiEnable = source["openAiEnable"];
|
||||
this.openAiBaseUrl = source["openAiBaseUrl"];
|
||||
this.openAiApiKey = source["openAiApiKey"];
|
||||
this.openAiModelName = source["openAiModelName"];
|
||||
this.openAiMaxTokens = source["openAiMaxTokens"];
|
||||
this.openAiTemperature = source["openAiTemperature"];
|
||||
this.openAiApiTimeOut = source["openAiApiTimeOut"];
|
||||
this.prompt = source["prompt"];
|
||||
this.checkUpdate = source["checkUpdate"];
|
||||
this.questionTemplate = source["questionTemplate"];
|
||||
@@ -380,7 +426,13 @@ export namespace data {
|
||||
this.browserPoolSize = source["browserPoolSize"];
|
||||
this.enableFund = source["enableFund"];
|
||||
this.enablePushNews = source["enablePushNews"];
|
||||
this.enableOnlyPushRedNews = source["enableOnlyPushRedNews"];
|
||||
this.sponsorCode = source["sponsorCode"];
|
||||
this.httpProxy = source["httpProxy"];
|
||||
this.httpProxyEnabled = source["httpProxyEnabled"];
|
||||
this.enableAgent = source["enableAgent"];
|
||||
this.qgqpBId = source["qgqpBId"];
|
||||
this.aiConfigs = this.convertValues(source["aiConfigs"], AIConfig);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
|
||||
@@ -48,6 +48,10 @@ export function EventsOff(eventName, ...additionalEventNames) {
|
||||
return window.runtime.EventsOff(eventName, ...additionalEventNames);
|
||||
}
|
||||
|
||||
export function EventsOffAll() {
|
||||
return window.runtime.EventsOffAll();
|
||||
}
|
||||
|
||||
export function EventsOnce(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, 1);
|
||||
}
|
||||
|
||||
110
go.mod
110
go.mod
@@ -1,94 +1,134 @@
|
||||
module go-stock
|
||||
|
||||
go 1.23.0
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.10.1
|
||||
github.com/chromedp/chromedp v0.11.2
|
||||
github.com/PuerkitoBio/goquery v1.11.0
|
||||
github.com/chromedp/chromedp v0.14.2
|
||||
github.com/cloudwego/eino v0.7.3
|
||||
github.com/cloudwego/eino-ext/components/model/ark v0.1.52
|
||||
github.com/cloudwego/eino-ext/components/model/deepseek v0.1.0
|
||||
github.com/cloudwego/eino-ext/components/model/openai v0.1.5
|
||||
github.com/coocood/freecache v1.2.4
|
||||
github.com/duke-git/lancet/v2 v2.3.4
|
||||
github.com/duke-git/lancet/v2 v2.3.8
|
||||
github.com/energye/systray v1.0.2
|
||||
github.com/gen2brain/beeep v0.11.1
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-ego/gse v0.80.3
|
||||
github.com/go-resty/resty/v2 v2.16.2
|
||||
github.com/go-resty/resty/v2 v2.17.0
|
||||
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
|
||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf
|
||||
github.com/robertkrimen/otto v0.5.1
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/samber/lo v1.49.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/tidwall/gjson v1.14.2
|
||||
github.com/wailsapp/wails/v2 v2.10.1
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/net v0.38.0
|
||||
golang.org/x/sys v0.31.0
|
||||
golang.org/x/text v0.23.0
|
||||
github.com/samber/lo v1.52.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/wailsapp/wails/v2 v2.11.0
|
||||
go.uber.org/zap v1.27.1
|
||||
golang.org/x/net v0.47.0
|
||||
golang.org/x/sys v0.38.0
|
||||
golang.org/x/text v0.31.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gorm.io/gorm v1.25.12
|
||||
gorm.io/plugin/dbresolver v1.5.3
|
||||
gorm.io/gorm v1.31.1
|
||||
gorm.io/plugin/dbresolver v1.6.2
|
||||
gorm.io/plugin/soft_delete v1.2.1
|
||||
)
|
||||
|
||||
require (
|
||||
git.sr.ht/~jackmordaunt/go-toast v1.1.2 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.14.2 // indirect
|
||||
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d // indirect
|
||||
github.com/chromedp/sysutil v1.1.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.6 // indirect
|
||||
github.com/cohesion-org/deepseek-go v1.3.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/eino-contrib/jsonschema v1.0.3 // indirect
|
||||
github.com/esiqveland/notify v0.13.3 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/evanphx/json-patch v0.5.2 // indirect
|
||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.4.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.2.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/goph/emperror v0.17.2 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/jackmordaunt/icns/v3 v3.0.1 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/labstack/echo/v4 v4.13.4 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||
github.com/leaanthony/gosod v1.0.4 // indirect
|
||||
github.com/leaanthony/slicer v1.6.0 // indirect
|
||||
github.com/leaanthony/u v1.1.1 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mailru/easyjson v0.9.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/meguminnnnnnnnn/go-openai v0.1.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
||||
github.com/nikolalohinski/gonja v1.5.3 // indirect
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
|
||||
github.com/ollama/ollama v0.13.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/sergeymakinen/go-bmp v1.0.0 // indirect
|
||||
github.com/sergeymakinen/go-ico v1.0.0-beta.0 // indirect
|
||||
github.com/sergeymakinen/go-ico v1.0.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f // indirect
|
||||
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
|
||||
github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/match v1.2.0 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/vcaesar/cedar v0.20.2 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.19 // indirect
|
||||
github.com/volcengine/volc-sdk-golang v1.0.229 // indirect
|
||||
github.com/volcengine/volcengine-go-sdk v1.1.50 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.23 // indirect
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/yargevad/filepathx v1.0.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/arch v0.23.0 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/sqlite v1.23.1 // indirect
|
||||
modernc.org/libc v1.67.1 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.40.1 // indirect
|
||||
)
|
||||
|
||||
// replace github.com/wailsapp/wails/v2 v2.9.2 => C:\Users\spark\go\pkg\mod
|
||||
|
||||
59
main.go
59
main.go
@@ -5,6 +5,14 @@ import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/db"
|
||||
log "go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/logger"
|
||||
@@ -13,13 +21,6 @@ import (
|
||||
"github.com/wailsapp/wails/v2/pkg/options/mac"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/windows"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/db"
|
||||
log "go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed frontend/dist
|
||||
@@ -57,8 +58,16 @@ var OFFICIAL_STATEMENT string
|
||||
var BuildKey string
|
||||
|
||||
func main() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.SugaredLogger.Error("panic: ", r)
|
||||
log.SugaredLogger.Error("stack: ", string(debug.Stack()))
|
||||
}
|
||||
}()
|
||||
|
||||
checkDir("data")
|
||||
db.Init("")
|
||||
data.InitAnalyzeSentiment()
|
||||
go AutoMigrate()
|
||||
|
||||
//db.Dao.Model(&data.Group{}).Where("id = ?", 0).FirstOrCreate(&data.Group{
|
||||
@@ -66,6 +75,10 @@ func main() {
|
||||
// Sort: 0,
|
||||
//})
|
||||
|
||||
log.SugaredLogger.Info("starting...")
|
||||
log.SugaredLogger.Infof("version: %s commit: %s", Version, VersionCommit)
|
||||
//log.SugaredLogger.Infof("build key: %s", BuildKey)
|
||||
|
||||
// Create an instance of the app structure
|
||||
app := NewApp()
|
||||
AppMenu := menu.NewMenu()
|
||||
@@ -113,7 +126,7 @@ func main() {
|
||||
//height = 768
|
||||
}
|
||||
|
||||
darkTheme := data.NewSettingsApi(&data.Settings{}).GetConfig().DarkTheme
|
||||
darkTheme := data.GetSettingConfig().DarkTheme
|
||||
backgroundColour := &options.RGBA{R: 255, G: 255, B: 255, A: 1}
|
||||
if darkTheme {
|
||||
backgroundColour = &options.RGBA{R: 27, G: 38, B: 54, A: 1}
|
||||
@@ -123,9 +136,9 @@ func main() {
|
||||
|
||||
// Create application with options
|
||||
err = wails.Run(&options.App{
|
||||
Title: "go-stock:AI赋能股票分析✨",
|
||||
Title: "go-stock:AI赋能股票分析✨ " + OFFICIAL_STATEMENT,
|
||||
Width: width * 4 / 5,
|
||||
Height: 900,
|
||||
Height: 920,
|
||||
MinWidth: minWidth,
|
||||
MinHeight: minHeight,
|
||||
//MaxWidth: width,
|
||||
@@ -188,6 +201,26 @@ func main() {
|
||||
|
||||
}
|
||||
|
||||
func updateMultipleModel() {
|
||||
oldSettings := &models.OldSettings{}
|
||||
db.Dao.Model(oldSettings).First(oldSettings)
|
||||
aiConfig := &data.AIConfig{}
|
||||
db.Dao.Model(aiConfig).First(aiConfig)
|
||||
if oldSettings.OpenAiEnable && oldSettings.OpenAiApiKey != "" && aiConfig.ID == 0 {
|
||||
aiConfig.Name = oldSettings.OpenAiModelName
|
||||
aiConfig.ApiKey = oldSettings.OpenAiApiKey
|
||||
aiConfig.BaseUrl = oldSettings.OpenAiBaseUrl
|
||||
aiConfig.ModelName = oldSettings.OpenAiModelName
|
||||
aiConfig.Temperature = oldSettings.OpenAiTemperature
|
||||
aiConfig.MaxTokens = oldSettings.OpenAiMaxTokens
|
||||
aiConfig.TimeOut = oldSettings.OpenAiApiTimeOut
|
||||
err := db.Dao.Model(aiConfig).Create(aiConfig).Error
|
||||
if err != nil {
|
||||
log.SugaredLogger.Error(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func AutoMigrate() {
|
||||
db.Dao.AutoMigrate(&data.StockInfo{})
|
||||
db.Dao.AutoMigrate(&data.StockBasic{})
|
||||
@@ -206,6 +239,10 @@ func AutoMigrate() {
|
||||
db.Dao.AutoMigrate(&models.Telegraph{})
|
||||
db.Dao.AutoMigrate(&models.TelegraphTags{})
|
||||
db.Dao.AutoMigrate(&models.LongTigerRankData{})
|
||||
db.Dao.AutoMigrate(&data.AIConfig{})
|
||||
db.Dao.AutoMigrate(&models.BKDict{})
|
||||
|
||||
updateMultipleModel()
|
||||
}
|
||||
|
||||
func initStockDataUS(ctx context.Context) {
|
||||
@@ -262,7 +299,7 @@ func initStockDataHK(ctx context.Context) {
|
||||
}
|
||||
|
||||
func updateBasicInfo() {
|
||||
config := data.NewSettingsApi(&data.Settings{}).GetConfig()
|
||||
config := data.GetSettingConfig()
|
||||
if config.UpdateBasicInfoOnStart {
|
||||
//更新基本信息
|
||||
go data.NewStockDataApi().GetStockBaseInfo()
|
||||
|
||||
Reference in New Issue
Block a user