Compare commits

..

1 Commits

Author SHA1 Message Date
SparkMemory
6116e9287a Merge pull request #84 from ArvinLovegood/dev
合并
2025-07-01 08:46:43 +08:00
97 changed files with 2043 additions and 1757549 deletions

View File

@@ -5,13 +5,10 @@ on:
tags:
# Match any new tag
- '*-release'
- '*-dev'
env:
# Necessary for most environments as build failure can occur due to OOM issues
NODE_OPTIONS: "--max-old-space-size=4096"
OFFICIAL_STATEMENT: ${{ vars.OFFICIAL_STATEMENT }}
BUILD_KEY: ${{ vars.BUILD_KEY }}
jobs:
build:
@@ -26,9 +23,6 @@ jobs:
# - name: 'go-stock-linux-amd64'
# platform: 'linux/amd64'
# os: 'ubuntu-latest'
- name: 'go-stock-darwin-universal'
platform: 'darwin/universal'
os: 'macos-latest'
runs-on: ${{ matrix.build.os }}
steps:
@@ -44,15 +38,13 @@ jobs:
echo "::set-output name=commit_message::$commit_message"
- name: Build wails x go-stock
uses: ArvinLovegood/wails-build-action@v3.6
uses: ArvinLovegood/wails-build-action@v3.4
id: build
with:
build-name: ${{ matrix.build.name }}
build-platform: ${{ matrix.build.platform }}
package: true
go-version: '1.25'
go-version: '1.24'
build-tags: ${{ github.ref_name }}
build-commit-message: ${{ steps.get_commit_message.outputs.commit_message }}
build-statement: ${{ env.OFFICIAL_STATEMENT }}
build-key: ${{ env.BUILD_KEY }}
node-version: '20.x'

View File

@@ -10,9 +10,8 @@
![扫码_搜索联合传播样式-白色版.png](build/screenshot/%E6%89%AB%E7%A0%81_%E6%90%9C%E7%B4%A2%E8%81%94%E5%90%88%E4%BC%A0%E6%92%AD%E6%A0%B7%E5%BC%8F-%E7%99%BD%E8%89%B2%E7%89%88.png)
### 📈 交流群
[//]: # (- QQ交流群2[点击链接加入群聊【go-stock交流群2】:892666282](https://qm.qq.com/q/5mYiy6Yxh0))
- QQ交流群[点击链接加入群聊【go-stock交流群】491605333(定期清理,随缘入群)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0YQ8qD3exahsD4YLNhzQTWe5ssstWC89&authKey=usOMMRFtIQDC%2FYcatHYapcxQbJ7PwXPHK9OypTXWzNjAq%2FRVvQu9bj2lRgb%2BSZ3p&noverify=0&group_code=491605333)
- QQ交流群2[点击链接加入群聊【go-stock交流群2】892666282](https://qm.qq.com/q/5mYiy6Yxh0)
- QQ交流群[点击链接加入群聊【go-stock交流群】491605333(已满会定期清理,随缘入群)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0YQ8qD3exahsD4YLNhzQTWe5ssstWC89&authKey=usOMMRFtIQDC%2FYcatHYapcxQbJ7PwXPHK9OypTXWzNjAq%2FRVvQu9bj2lRgb%2BSZ3p&noverify=0&group_code=491605333)
### ✨ 简介
- 本项目基于Wails和NaiveUI开发结合AI大模型构建的股票分析工具。
@@ -22,45 +21,34 @@
- 开发环境主要基于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))
### 💬 支持大模型/平台
| 模型 | 状态 | 备注 |
| --- | --- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [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) |
| 模型 | 状态 | 备注 |
| --- | --- |---------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [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) |
### <span style="color: #568DF4;">各位亲爱的朋友们,如果您对这个项目感兴趣,请先给我一个<i style="color: #EA2626;">star</i>吧,谢谢!</span>💕
[//]: # (- 优云智算by UCloud万卡规模4090免费用10小时新人注册另增50万tokens海量热门源项目镜像一键部署[注册链接]&#40;https://www.compshare.cn/image-community?ytag=GPU_YY-gh_gostock&#41;)
- 火山方舟:新用户每个模型注册即送50万tokens[注册链接](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ)
- 硅基流动(siliconflow)注册即送2000万Tokens[注册链接](https://cloud.siliconflow.cn/i/foufCerk)
- 优云智算by UCloud万卡规模4090免费用10小时新人注册另增50万tokens海量热门源项目镜像一键部署[注册链接](https://www.compshare.cn/image-community?ytag=GPU_YY-gh_gostock)
- 经测试目前硅基流动(siliconflow)提供的deepSeek api 服务比较稳定,注册即送2000万Tokens[注册链接](https://cloud.siliconflow.cn/i/foufCerk)
- 火山方舟:每个模型注册即送50万tokens[注册链接](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ)
- Tushare大数据开放社区,免费提供各类金融数据,助力行业和量化研究(注意Tushare只需要120积分即可注册完成个人资料补充即可得120积分)[注册链接](https://tushare.pro/register?reg=701944)
- 软件快速迭代开发中,请大家优先测试和使用最新发布的版本。
- 欢迎大家提出宝贵的建议欢迎提issue,PR。当然更欢迎[赞助我](#都划到这了如果我的项目对您有帮助请赞助我吧)。💕
### 支持开源💕计划
| 赞助计划 | 赞助等级 | 权益说明 |
|:--------------------------------|----------------|:-------------------------------------------------------|
| 每月 0 RMB | vip0 | 🌟 全部功能,软件自动更新(从GitHub下载),自行解决github平台网络问题。 |
| 每月赞助 18.8 RMB<br>每年赞助 120 RMB | vip1 | 💕 全部功能,软件自动更新(从CDN下载),更新快速便捷。AI配置指导提示词参考等 |
| 每月赞助 28.8 RMB<br>每年赞助 240 RMB | vip2 | 💕 💕 vip1全部功能,赠送硅基流动AI分析服务 |
| 每月赞助 X RMB | vipX | 🧩 更多计划视go-stock开源项目发展情况而定...(承接GitHub项目README广告推广💖) |
## 🧩 重大功能开发计划
| 功能说明 | 状态 | 备注 |
|-----------------|----|----------------------------------------------------------------------------------------------------------|
| 股票分析知识库 | 🚧 | 未来计划 |
| Ai智能选股 | | Ai智能选股功能(市场行情-》AI总结/AI智能体功能) |
| Ai智能选股 | 🚧 | Ai智能选股功能开发中(下半年重点开发计划) |
| ETF支持 | 🚧 | ETF数据支持 (目前可以查看净值和估值) |
| 美股支持 | ✅ | 美股数据支持 |
| 港股支持 | ✅ | 港股数据支持 |
@@ -69,10 +57,6 @@
| 不再强制依赖Chrome浏览器 | ✅ | 默认使用edge浏览器抓取新闻资讯 |
## 👀 更新日志
### 2025.07.08 实现软件自动更新功能
### 2025.07.07 卡片添加迷你分时图
### 2025.07.05 MacOs支持
### 2025.07.01 AI分析集成工具函数AI分析将更加智能
### 2025.06.30 添加指标选股功能
### 2025.06.27 添加财经日历和重大事件时间轴功能
### 2025.06.25 添加热门股票、事件和话题功能

906
app.go

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,8 @@
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
@@ -28,66 +24,38 @@ 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 {
return data.NewSearchStockApi(words).SearchStock(5000)
}
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,
}
func (a App) SearchStock(words string) map[string]any {
return data.NewSearchStockApi(words).SearchStock()
}

View File

@@ -6,168 +6,303 @@ package main
import (
"context"
"fmt"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/strutil"
"github.com/gen2brain/beeep"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go-stock/backend/data"
"go-stock/backend/db"
"go-stock/backend/logger"
"log"
"go-stock/backend/models"
"strings"
"time"
"github.com/PuerkitoBio/goquery"
"github.com/coocood/freecache"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/mathutil"
"github.com/duke-git/lancet/v2/slice"
"github.com/go-resty/resty/v2"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// startup 在应用程序启动时调用
// App struct
type App struct {
ctx context.Context
cache *freecache.Cache
}
// NewApp creates a new App application struct
func NewApp() *App {
cacheSize := 512 * 1024
cache := freecache.NewCache(cacheSize)
return &App{
cache: cache,
}
}
// startup is called at application startup
func (a *App) startup(ctx context.Context) {
defer PanicHandler()
runtime.EventsOn(ctx, "frontendError", func(optionalData ...interface{}) {
logger.SugaredLogger.Errorf("Frontend error: %v\n", optionalData)
})
logger.SugaredLogger.Infof("Version:%s", Version)
// Perform your setup here
a.ctx = ctx
// 监听设置更新事件
runtime.EventsOn(ctx, "updateSettings", func(optionalData ...interface{}) {
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
//}
// TODO 创建系统托盘
logger.SugaredLogger.Infof("updateSettings config:%+v", config)
if config.DarkTheme {
runtime.WindowSetBackgroundColour(ctx, 27, 38, 54, 1)
runtime.WindowSetDarkTheme(ctx)
} else {
runtime.WindowSetBackgroundColour(ctx, 255, 255, 255, 1)
runtime.WindowSetLightTheme(ctx)
}
runtime.WindowReloadApp(ctx)
})
// 创建 macOS 托盘
go func() {
// 使用 Beeep 库替代 Windows 的托盘库
err := beeep.Notify("go-stock", "应用程序已启动", "")
if err != nil {
log.Fatalf("系统通知失败: %v", err)
}
}()
go setUpScreen(a)
logger.SugaredLogger.Infof(" application startup Version:%s", Version)
}
func setUpScreen(a *App) {
screens, _ := runtime.ScreenGetAll(a.ctx)
if len(screens) == 0 {
func checkUpdate(a *App) {
releaseVersion := &models.GitHubReleaseVersion{}
_, err := resty.New().R().
SetResult(releaseVersion).
Get("https://api.github.com/repos/ArvinLovegood/go-stock/releases/latest")
if err != nil {
logger.SugaredLogger.Errorf("get github release version error:%s", err.Error())
return
}
screen := screens[0]
sw, sh := screen.Width, screen.Height
// macOS 菜单栏 + Dock 留出空间
topBarHeight := 22
dockHeight := 56
verticalMargin := topBarHeight + dockHeight
// 设置窗口为屏幕 80% 宽 × 可用高度 90%
w := int(float64(sw) * 0.8)
h := int(float64(sh-verticalMargin) * 0.9)
runtime.WindowSetSize(a.ctx, w, h)
runtime.WindowCenter(a.ctx)
logger.SugaredLogger.Infof("releaseVersion:%+v", releaseVersion.TagName)
if releaseVersion.TagName != Version {
go runtime.EventsEmit(a.ctx, "updateVersion", releaseVersion)
}
}
// OnSecondInstanceLaunch 处理第二实例启动时的通知
func OnSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) {
err := beeep.Notify("go-stock", "程序已经在运行了", "")
// domReady is called after front-end resources have been loaded
func (a *App) domReady(ctx context.Context) {
// Add your action here
//定时更新数据
go func() {
config := data.NewSettingsApi(&data.Settings{}).GetConfig()
interval := config.RefreshInterval
if interval <= 0 {
interval = 1
}
ticker := time.NewTicker(time.Second * time.Duration(interval))
defer ticker.Stop()
for range ticker.C {
if isTradingTime(time.Now()) {
MonitorStockPrices(a)
}
}
}()
go func() {
ticker := time.NewTicker(time.Second * time.Duration(60))
defer ticker.Stop()
for range ticker.C {
telegraph := refreshTelegraphList()
if telegraph != nil {
go runtime.EventsEmit(a.ctx, "telegraph", telegraph)
}
}
}()
go runtime.EventsEmit(a.ctx, "telegraph", refreshTelegraphList())
go MonitorStockPrices(a)
//检查新版本
go func() {
checkUpdate(a)
}()
}
func refreshTelegraphList() *[]string {
url := "https://www.cls.cn/telegraph"
response, err := resty.New().R().
SetHeader("Referer", "https://www.cls.cn/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get(fmt.Sprintf(url))
if err != nil {
logger.SugaredLogger.Error(err)
return &[]string{}
}
time.Sleep(time.Second * 3)
//logger.SugaredLogger.Info(string(response.Body()))
document, err := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
if err != nil {
return &[]string{}
}
var telegraph []string
document.Find("div.telegraph-content-box").Each(func(i int, selection *goquery.Selection) {
//logger.SugaredLogger.Info(selection.Text())
telegraph = append(telegraph, selection.Text())
})
return &telegraph
}
// isTradingDay 判断是否是交易日
func isTradingDay(date time.Time) bool {
weekday := date.Weekday()
// 判断是否是周末
if weekday == time.Saturday || weekday == time.Sunday {
return false
}
// 这里可以添加具体的节假日判断逻辑
// 例如:判断是否是春节、国庆节等
return true
}
// isTradingTime 判断是否是交易时间
func isTradingTime(date time.Time) bool {
if !isTradingDay(date) {
return false
}
hour, minute, _ := date.Clock()
// 判断是否在9:15到11:30之间
if (hour == 9 && minute >= 15) || (hour == 10) || (hour == 11 && minute <= 30) {
return true
}
// 判断是否在13:00到15:00之间
if (hour == 13) || (hour == 14) || (hour == 15 && minute <= 0) {
return true
}
return false
}
func MonitorStockPrices(a *App) {
dest := &[]data.FollowedStock{}
db.Dao.Model(&data.FollowedStock{}).Find(dest)
total := float64(0)
//for _, follow := range *dest {
// stockData := getStockInfo(follow)
// total += stockData.ProfitAmountToday
// price, _ := convertor.ToFloat(stockData.Price)
// if stockData.PrePrice != price {
// go runtime.EventsEmit(a.ctx, "stock_price", stockData)
// }
//}
// 股票信息处理逻辑
stockInfos := GetStockInfos(*dest...)
for _, stockInfo := range *stockInfos {
if strutil.HasPrefixAny(stockInfo.Code, []string{"SZ", "SH", "sh", "sz"}) && (!isTradingTime(time.Now())) {
continue
}
if strutil.HasPrefixAny(stockInfo.Code, []string{"hk", "HK"}) && (!IsHKTradingTime(time.Now())) {
continue
}
if strutil.HasPrefixAny(stockInfo.Code, []string{"us", "US", "gb_"}) && (!IsUSTradingTime(time.Now())) {
continue
}
total += stockInfo.ProfitAmountToday
price, _ := convertor.ToFloat(stockInfo.Price)
if stockInfo.PrePrice != price {
go runtime.EventsEmit(a.ctx, "stock_price", stockInfo)
}
}
// 计算总收益并更新状态
if total != 0 {
// 使用通知替代 systray 更新 Tooltip
title := "go-stock " + time.Now().Format(time.DateTime) + fmt.Sprintf(" %.2f¥", total)
// title := "go-stock " + time.Now().Format(time.DateTime) + fmt.Sprintf(" %.2f¥", total)
// systray.SetTooltip(title)
}
// 发送通知显示实时数据
err := beeep.Notify("go-stock", title, "")
if err != nil {
logger.SugaredLogger.Errorf("发送通知失败: %v", err)
go runtime.EventsEmit(a.ctx, "realtime_profit", fmt.Sprintf(" %.2f", total))
//runtime.WindowSetTitle(a.ctx, title)
}
func GetStockInfos(follows ...data.FollowedStock) *[]data.StockInfo {
stockCodes := make([]string, 0)
for _, follow := range follows {
stockCodes = append(stockCodes, follow.StockCode)
}
stockData, err := data.NewStockDataApi().GetStockCodeRealTimeData(stockCodes...)
if err != nil {
logger.SugaredLogger.Errorf("get stock code real time data error:%s", err.Error())
return nil
}
stockInfos := make([]data.StockInfo, 0)
for _, info := range *stockData {
v, ok := slice.FindBy(follows, func(idx int, follow data.FollowedStock) bool {
return follow.StockCode == info.Code
})
if ok {
addStockFollowData(v, &info)
stockInfos = append(stockInfos, info)
}
}
// 触发实时利润事件
go runtime.EventsEmit(a.ctx, "realtime_profit", fmt.Sprintf(" %.2f", total))
return &stockInfos
}
func getStockInfo(follow data.FollowedStock) *data.StockInfo {
stockCode := follow.StockCode
stockDatas, err := data.NewStockDataApi().GetStockCodeRealTimeData(stockCode)
if err != nil || len(*stockDatas) == 0 {
return &data.StockInfo{}
}
stockData := (*stockDatas)[0]
addStockFollowData(follow, &stockData)
return &stockData
}
// onReady 在应用程序准备好时调用
func onReady(a *App) {
// 初始化操作
logger.SugaredLogger.Infof("onReady")
func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) {
stockData.PrePrice = follow.Price //上次当前价格
stockData.Sort = follow.Sort
stockData.CostPrice = follow.CostPrice //成本价
stockData.CostVolume = follow.Volume //成本量
stockData.AlarmChangePercent = follow.AlarmChangePercent
stockData.AlarmPrice = follow.AlarmPrice
// 使用 Beeep 发送通知
err := beeep.Notify("go-stock", "应用程序已准备就绪", "")
if err != nil {
log.Fatalf("系统通知失败: %v", err)
//当前价格
price, _ := convertor.ToFloat(stockData.Price)
//当前价格为0 时 使用卖一价格作为当前价格
if price == 0 {
price, _ = convertor.ToFloat(stockData.A1P)
}
//当前价格依然为0 时 使用买一报价作为当前价格
if price == 0 {
price, _ = convertor.ToFloat(stockData.B1P)
}
// 显示应用窗口
runtime.WindowShow(a.ctx)
//昨日收盘价
preClosePrice, _ := convertor.ToFloat(stockData.PreClose)
// 在 macOS 上没有系统托盘图标菜单,通常我们通过通知或其他方式提供与用户交互的界面
//当前价格依然为0 时 使用昨日收盘价为当前价格
if price == 0 {
price = preClosePrice
}
//今日最高价
highPrice, _ := convertor.ToFloat(stockData.High)
if highPrice == 0 {
highPrice, _ = convertor.ToFloat(stockData.Open)
}
//今日最低价
lowPrice, _ := convertor.ToFloat(stockData.Low)
if lowPrice == 0 {
lowPrice, _ = convertor.ToFloat(stockData.Open)
}
//开盘价
//openPrice, _ := convertor.ToFloat(stockData.Open)
if price > 0 {
stockData.ChangePrice = mathutil.RoundToFloat(price-preClosePrice, 2)
stockData.ChangePercent = mathutil.RoundToFloat(mathutil.Div(price-preClosePrice, preClosePrice)*100, 3)
}
if highPrice > 0 {
stockData.HighRate = mathutil.RoundToFloat(mathutil.Div(highPrice-preClosePrice, preClosePrice)*100, 3)
}
if lowPrice > 0 {
stockData.LowRate = mathutil.RoundToFloat(mathutil.Div(lowPrice-preClosePrice, preClosePrice)*100, 3)
}
if follow.CostPrice > 0 && follow.Volume > 0 {
if price > 0 {
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(price-follow.CostPrice, follow.CostPrice)*100, 3)
stockData.ProfitAmount = mathutil.RoundToFloat((price-follow.CostPrice)*float64(follow.Volume), 2)
stockData.ProfitAmountToday = mathutil.RoundToFloat((price-preClosePrice)*float64(follow.Volume), 2)
} else {
//未开盘时当前价格为昨日收盘价
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(preClosePrice-follow.CostPrice, follow.CostPrice)*100, 3)
stockData.ProfitAmount = mathutil.RoundToFloat((preClosePrice-follow.CostPrice)*float64(follow.Volume), 2)
// 未开盘时,今日盈亏为 0
stockData.ProfitAmountToday = 0
}
}
//logger.SugaredLogger.Debugf("stockData:%+v", stockData)
if follow.Price != price && price > 0 {
go db.Dao.Model(follow).Where("stock_code = ?", follow.StockCode).Updates(map[string]interface{}{
"price": price,
})
}
}
// beforeClose 在应用程序关闭前调用,显示确认对话框
// beforeClose is called when the application is about to quit,
// either by clicking the window close button or calling runtime.Quit.
// Returning true will cause the application to continue, false will continue shutdown as normal.
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
defer PanicHandler()
// 在 macOS 上使用 MessageDialog 显示确认窗口
dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{
Type: runtime.QuestionDialog,
Title: "go-stock",
Message: "确定关闭吗?",
Buttons: []string{"确定", "取消"},
Buttons: []string{"确定"},
Icon: icon,
CancelButton: "取消",
})
@@ -176,27 +311,150 @@ func (a *App) beforeClose(ctx context.Context) (prevent bool) {
logger.SugaredLogger.Errorf("dialog error:%s", err.Error())
return false
}
logger.SugaredLogger.Debugf("dialog:%s", dialog)
if dialog == "取消" {
return true // 如果选择了取消,不关闭应用
} else {
// 在 macOS 上应用退出时执行清理工作
a.cron.Stop() // 停止定时任务
return false // 如果选择了确定,继续关闭应用
if dialog == "No" {
return true
}
}
func getFrameless() bool {
return false
}
func getScreenResolution() (int, int, int, int, error) {
//user32 := syscall.NewLazyDLL("user32.dll")
//getSystemMetrics := user32.NewProc("GetSystemMetrics")
//
//width, _, _ := getSystemMetrics.Call(0)
//height, _, _ := getSystemMetrics.Call(1)
return int(1200), int(800), 0, 0, nil
// shutdown is called at application termination
func (a *App) shutdown(ctx context.Context) {
// Perform your teardown here
// systray.Quit()
}
// Greet returns a greeting for the given name
func (a *App) Greet(stockCode string) *data.StockInfo {
//stockInfo, _ := data.NewStockDataApi().GetStockCodeRealTimeData(stockCode)
follow := &data.FollowedStock{
StockCode: stockCode,
}
db.Dao.Model(follow).Where("stock_code = ?", stockCode).First(follow)
stockInfo := getStockInfo(*follow)
return stockInfo
}
func (a *App) Follow(stockCode string) string {
return data.NewStockDataApi().Follow(stockCode)
}
func (a *App) UnFollow(stockCode string) string {
return data.NewStockDataApi().UnFollow(stockCode)
}
func (a *App) GetFollowList() []data.FollowedStock {
return data.NewStockDataApi().GetFollowList()
}
func (a *App) GetStockList(key string) []data.StockBasic {
return data.NewStockDataApi().GetStockList(key)
}
func (a *App) SetCostPriceAndVolume(stockCode string, price float64, volume int64) string {
return data.NewStockDataApi().SetCostPriceAndVolume(price, volume, stockCode)
}
func (a *App) SetAlarmChangePercent(val, alarmPrice float64, stockCode string) string {
return data.NewStockDataApi().SetAlarmChangePercent(val, alarmPrice, stockCode)
}
func (a *App) SetStockSort(sort int64, stockCode string) {
data.NewStockDataApi().SetStockSort(sort, stockCode)
}
func (a *App) SendDingDingMessage(message string, stockCode string) string {
ttl, _ := a.cache.TTL([]byte(stockCode))
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
if ttl > 0 {
return ""
}
err := a.cache.Set([]byte(stockCode), []byte("1"), 60*5)
if err != nil {
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
return ""
}
return data.NewDingDingAPI().SendDingDingMessage(message)
}
// SendDingDingMessageByType msgType 报警类型: 1 涨跌报警;2 股价报警 3 成本价报警
func (a *App) SendDingDingMessageByType(message string, stockCode string, msgType int) string {
ttl, _ := a.cache.TTL([]byte(stockCode))
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
if ttl > 0 {
return ""
}
err := a.cache.Set([]byte(stockCode), []byte("1"), getMsgTypeTTL(msgType))
if err != nil {
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
return ""
}
stockInfo := &data.StockInfo{}
db.Dao.Model(stockInfo).Where("code = ?", stockCode).First(stockInfo)
go data.NewAlertWindowsApi("go-stock消息通知", getMsgTypeName(msgType), GenNotificationMsg(stockInfo), "").SendNotification()
return data.NewDingDingAPI().SendDingDingMessage(message)
}
func (a *App) NewChat(stock string) string {
return data.NewDeepSeekOpenAi().NewChat(stock)
}
func (a *App) NewChatStream(stock, stockCode string) {
msgs := data.NewDeepSeekOpenAi().NewChatStream(stock, stockCode)
for msg := range msgs {
runtime.EventsEmit(a.ctx, "newChatStream", msg)
}
runtime.EventsEmit(a.ctx, "newChatStream", "DONE")
}
func GenNotificationMsg(stockInfo *data.StockInfo) string {
Price, err := convertor.ToFloat(stockInfo.Price)
if err != nil {
Price = 0
}
PreClose, err := convertor.ToFloat(stockInfo.PreClose)
if err != nil {
PreClose = 0
}
var RF float64
if PreClose > 0 {
RF = mathutil.RoundToFloat(((Price-PreClose)/PreClose)*100, 2)
}
return "[" + stockInfo.Name + "] " + stockInfo.Price + " " + convertor.ToString(RF) + "% " + stockInfo.Date + " " + stockInfo.Time
}
// msgType : 1 涨跌报警(5分钟);2 股价报警(30分钟) 3 成本价报警(30分钟)
func getMsgTypeTTL(msgType int) int {
switch msgType {
case 1:
return 60 * 5
case 2:
return 60 * 30
case 3:
return 60 * 30
default:
return 60 * 5
}
}
func getMsgTypeName(msgType int) string {
switch msgType {
case 1:
return "涨跌报警"
case 2:
return "股价报警"
case 3:
return "成本价报警"
default:
return "未知类型"
}
}
func (a *App) UpdateConfig(settings *data.Settings) string {
logger.SugaredLogger.Infof("UpdateConfig:%+v", settings)
return data.NewSettingsApi(settings).UpdateConfig()
}
func (a *App) GetConfig() *data.Settings {
return data.NewSettingsApi(&data.Settings{}).GetConfig()
}

View File

@@ -184,10 +184,10 @@ func getMsgTypeName(msgType int) string {
return "未知类型"
}
}
func (a *App) UpdateConfig(settingConfig *data.SettingConfig) string {
return data.UpdateConfig(settingConfig)
func (a *App) UpdateConfig(settings *data.Settings) string {
return data.NewSettingsApi(settings).UpdateConfig()
}
func (a *App) GetConfig() *data.SettingConfig {
return data.GetSettingConfig()
func (a *App) GetConfig() *data.Settings {
return data.NewSettingsApi(&data.Settings{}).GetConfig()
}

View File

@@ -1,15 +1,9 @@
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,36 +23,3 @@ func TestIsUSTradingTime(t *testing.T) {
t.Log(IsUSTradingTime(time.Now()))
}
func TestCheckStockBaseInfo(t *testing.T) {
db.Init("./data/stock.db")
NewApp().CheckStockBaseInfo(context.Background())
}
func TestJson(t *testing.T) {
db.Init("./data/stock.db")
jsonStr := "{\n\t\t\"id\" : 3334,\n\t\t\"created_at\" : \"2025-02-28 16:49:31.8342514+08:00\",\n\t\t\"updated_at\" : \"2025-02-28 16:49:31.8342514+08:00\",\n\t\t\"deleted_at\" : null,\n\t\t\"code\" : \"PUK.US\",\n\t\t\"name\" : \"英国保诚集团\",\n\t\t\"full_name\" : \"\",\n\t\t\"e_name\" : \"\",\n\t\t\"exchange\" : \"NASDAQ\",\n\t\t\"type\" : \"stock\",\n\t\t\"is_del\" : 0,\n\t\t\"bk_name\" : null,\n\t\t\"bk_code\" : null\n\t}"
v := &models.StockInfoUS{}
json.Unmarshal([]byte(jsonStr), v)
logger.SugaredLogger.Infof("v:%+v", v)
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)
}

View File

@@ -1,214 +0,0 @@
//go:build windows
// +build windows
package main
import (
"context"
"fmt"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/strutil"
"github.com/energye/systray"
"github.com/go-toast/toast"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go-stock/backend/data"
"go-stock/backend/db"
"go-stock/backend/logger"
"time"
)
// startup is called at application startup
func (a *App) startup(ctx context.Context) {
defer PanicHandler()
runtime.EventsOn(ctx, "frontendError", func(optionalData ...interface{}) {
logger.SugaredLogger.Errorf("Frontend error: %v\n", optionalData)
})
logger.SugaredLogger.Infof("Version:%s", Version)
// Perform your setup here
a.ctx = ctx
// 创建系统托盘
//systray.RunWithExternalLoop(func() {
// onReady(a)
//}, func() {
// onExit(a)
//})
runtime.EventsOn(ctx, "updateSettings", func(optionalData ...interface{}) {
logger.SugaredLogger.Infof("updateSettings : %v\n", optionalData)
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 {
runtime.WindowSetBackgroundColour(ctx, 27, 38, 54, 1)
runtime.WindowSetDarkTheme(ctx)
} else {
runtime.WindowSetBackgroundColour(ctx, 255, 255, 255, 1)
runtime.WindowSetLightTheme(ctx)
}
runtime.WindowReloadApp(ctx)
})
go systray.Run(func() {
onReady(a)
}, func() {
onExit(a)
})
logger.SugaredLogger.Infof(" application startup Version:%s", Version)
}
func OnSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) {
notification := toast.Notification{
AppID: "go-stock",
Title: "go-stock",
Message: "程序已经在运行了",
Icon: "",
Duration: "short",
Audio: toast.Default,
}
err := notification.Push()
if err != nil {
logger.SugaredLogger.Error(err)
}
time.Sleep(time.Second * 3)
}
func MonitorStockPrices(a *App) {
dest := &[]data.FollowedStock{}
db.Dao.Model(&data.FollowedStock{}).Find(dest)
total := float64(0)
//for _, follow := range *dest {
// stockData := getStockInfo(follow)
// total += stockData.ProfitAmountToday
// price, _ := convertor.ToFloat(stockData.Price)
// if stockData.PrePrice != price {
// go runtime.EventsEmit(a.ctx, "stock_price", stockData)
// }
//}
stockInfos := GetStockInfos(*dest...)
for _, stockInfo := range *stockInfos {
if strutil.HasPrefixAny(stockInfo.Code, []string{"SZ", "SH", "sh", "sz"}) && (!isTradingTime(time.Now())) {
continue
}
if strutil.HasPrefixAny(stockInfo.Code, []string{"hk", "HK"}) && (!IsHKTradingTime(time.Now())) {
continue
}
if strutil.HasPrefixAny(stockInfo.Code, []string{"us", "US", "gb_"}) && (!IsUSTradingTime(time.Now())) {
continue
}
total += stockInfo.ProfitAmountToday
price, _ := convertor.ToFloat(stockInfo.Price)
if stockInfo.PrePrice != price {
//logger.SugaredLogger.Infof("-----------sz------------股票代码: %s, 股票名称: %s, 股票价格: %s,盘前盘后:%s", stockInfo.Code, stockInfo.Name, stockInfo.Price, stockInfo.BA)
go runtime.EventsEmit(a.ctx, "stock_price", stockInfo)
}
}
if total != 0 {
title := "go-stock " + time.Now().Format(time.DateTime) + fmt.Sprintf(" %.2f¥", total)
systray.SetTooltip(title)
}
go runtime.EventsEmit(a.ctx, "realtime_profit", fmt.Sprintf(" %.2f", total))
//runtime.WindowSetTitle(a.ctx, title)
}
func onReady(a *App) {
// 初始化操作
logger.SugaredLogger.Infof("systray onReady")
systray.SetIcon(icon2)
systray.SetTitle("go-stock")
systray.SetTooltip("go-stock 股票行情实时获取")
// 创建菜单项
show := systray.AddMenuItem("显示", "显示应用程序")
show.Click(func() {
//logger.SugaredLogger.Infof("显示应用程序")
runtime.WindowShow(a.ctx)
})
hide := systray.AddMenuItem("隐藏", "隐藏应用程序")
hide.Click(func() {
//logger.SugaredLogger.Infof("隐藏应用程序")
runtime.WindowHide(a.ctx)
})
systray.AddSeparator()
mQuitOrig := systray.AddMenuItem("退出", "退出应用程序")
mQuitOrig.Click(func() {
//logger.SugaredLogger.Infof("退出应用程序")
runtime.Quit(a.ctx)
})
systray.SetOnRClick(func(menu systray.IMenu) {
menu.ShowMenu()
//logger.SugaredLogger.Infof("SetOnRClick")
})
systray.SetOnClick(func(menu systray.IMenu) {
//logger.SugaredLogger.Infof("SetOnClick")
menu.ShowMenu()
})
systray.SetOnDClick(func(menu systray.IMenu) {
menu.ShowMenu()
//logger.SugaredLogger.Infof("SetOnDClick")
})
}
// beforeClose is called when the application is about to quit,
// either by clicking the window close button or calling runtime.Quit.
// Returning true will cause the application to continue, false will continue shutdown as normal.
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
defer PanicHandler()
dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{
Type: runtime.QuestionDialog,
Title: "go-stock",
Message: "确定关闭吗?",
Buttons: []string{"确定"},
Icon: icon,
CancelButton: "取消",
})
if err != nil {
logger.SugaredLogger.Errorf("dialog error:%s", err.Error())
return false
}
logger.SugaredLogger.Debugf("dialog:%s", dialog)
if dialog == "No" {
return true
} else {
systray.Quit()
a.cron.Stop()
return false
}
}
func getFrameless() bool {
return true
}
func getScreenResolution() (int, int, int, int, error) {
//user32 := syscall.NewLazyDLL("user32.dll")
//getSystemMetrics := user32.NewProc("GetSystemMetrics")
//
//width, _, _ := getSystemMetrics.Call(0)
//height, _, _ := getSystemMetrics.Call(1)
return int(1366), int(768), 1456, 768, nil
}

View File

@@ -1,93 +0,0 @@
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
}

View File

@@ -1,91 +0,0 @@
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
}

View File

@@ -1,84 +0,0 @@
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())
}
}

View File

@@ -1,98 +0,0 @@
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
}

View File

@@ -1,34 +0,0 @@
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{}
}

View File

@@ -1,140 +0,0 @@
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
}

View File

@@ -1,35 +0,0 @@
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])
}
//北京证券交易所 883、87、88 等) 创新型中小企业(专精特新为主)
//上海证券交易所 660、688 等) 大盘蓝筹、科创板(高新技术)
//深圳证券交易所 0、3000、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
}

View File

@@ -1,79 +0,0 @@
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
}

View File

@@ -1,50 +0,0 @@
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
}

View File

@@ -1,69 +0,0 @@
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
}

View File

@@ -1,64 +0,0 @@
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
}

View File

@@ -1,80 +0,0 @@
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
}

View File

@@ -1,49 +0,0 @@
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
}

View File

@@ -1,80 +0,0 @@
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)
}
}

View File

@@ -1,42 +0,0 @@
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
}

View File

@@ -1,57 +0,0 @@
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
}

View File

@@ -34,7 +34,7 @@ func NewAlertWindowsApi(AppID string, Title string, Content string, Icon string)
}
func (a AlertWindowsApi) SendNotification() bool {
if GetSettingConfig().LocalPushEnable == false {
if GetConfig().LocalPushEnable == false {
logger.SugaredLogger.Error("本地推送未开启")
return false
}

View File

@@ -1,12 +1,10 @@
//go:build windows
// +build windows
package data
import (
"go-stock/backend/logger"
"github.com/go-toast/toast"
"go-stock/backend/logger"
)
// AlertWindowsApi @Author spark
@@ -33,7 +31,7 @@ func NewAlertWindowsApi(AppID string, Title string, Content string, Icon string)
}
func (a AlertWindowsApi) SendNotification() bool {
if GetSettingConfig().LocalPushEnable == false {
if GetConfig().LocalPushEnable == false {
logger.SugaredLogger.Error("本地推送未开启")
return false
}

View File

@@ -1,13 +1,11 @@
//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

View File

@@ -27,7 +27,7 @@ func (c *CrawlerApi) NewCrawler(ctx context.Context, crawlerBaseInfo CrawlerBase
return CrawlerApi{
crawlerCtx: ctx,
crawlerBaseInfo: crawlerBaseInfo,
pool: NewBrowserPool(GetSettingConfig().BrowserPoolSize),
pool: NewBrowserPool(GetConfig().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 := GetSettingConfig().BrowserPath
path := GetConfig().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 := GetSettingConfig().BrowserPath
path := GetConfig().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 := GetSettingConfig().BrowserPath
path := GetConfig().BrowserPath
//logger.SugaredLogger.Infof("GetHtmlWithActions path:%s", path)
if path != "" {
pctx, pcancel := chromedp.NewExecAllocator(

View File

@@ -1 +0,0 @@
Some dict/zh data is from [github.com/fxsjy/jieba](https://github.com/fxsjy/jieba)

View File

@@ -1,428 +0,0 @@
# 金融股票全场景分词字典(最终去重优化版)
# 格式:单词 权重 词性 | 权重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

View File

@@ -1 +0,0 @@
dict.txt 通过内部工具生成, Copyright 2017 ego authors. 商用和拷贝请注明来源和版权

File diff suppressed because it is too large Load Diff

View File

@@ -1,185 +0,0 @@
# 补充热点概念与板块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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,88 +0,0 @@
,
.
?
!
"
@
 
~
*
<
>
/
\
|
-
_
+
=
&
^
%
#
`
;
$
︿
哎呀
哎哟
俺们
按照
吧哒
罢了
本着
比方
比如
鄙人
彼此
别的
别说

File diff suppressed because it is too large Load Diff

View File

@@ -21,7 +21,7 @@ func NewDingDingAPI() *DingDingAPI {
}
func (DingDingAPI) SendDingDingMessage(message string) string {
if GetSettingConfig().DingPushEnable == false {
if GetConfig().DingPushEnable == false {
//logger.SugaredLogger.Info("钉钉推送未开启")
return "钉钉推送未开启"
}
@@ -37,9 +37,11 @@ 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 GetSettingConfig().DingRobot
return GetConfig().DingRobot
}
func (DingDingAPI) SendToDingDing(title, message string) string {

View File

@@ -20,13 +20,13 @@ import (
type FundApi struct {
client *resty.Client
config *SettingConfig
config *Settings
}
func NewFundApi() *FundApi {
return &FundApi{
client: resty.New(),
config: GetSettingConfig(),
config: GetConfig(),
}
}

View File

@@ -4,15 +4,6 @@ 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"
@@ -21,6 +12,12 @@ 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"
"strconv"
"strings"
"time"
)
// @Author spark
@@ -34,76 +31,12 @@ 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{
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(url)
Get(fmt.Sprintf(url))
var telegraphs []models.Telegraph
//logger.SugaredLogger.Info(string(response.Body()))
document, _ := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
@@ -142,7 +75,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 content=?", telegraph.Time, telegraph.Content).Count(&cnt)
db.Dao.Model(telegraph).Where("time=? and source=?", telegraph.Time, telegraph.Source).Count(&cnt)
if cnt == 0 {
db.Dao.Create(&telegraph)
telegraphs = append(telegraphs, telegraph)
@@ -165,9 +98,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("data_time desc,time desc").Limit(limit).Find(news)
db.Dao.Model(news).Preload("TelegraphTags").Where("source=?", source).Order("id desc").Limit(limit).Find(news)
} else {
db.Dao.Model(news).Preload("TelegraphTags").Order("data_time desc,time desc").Limit(limit).Find(news)
db.Dao.Model(news).Preload("TelegraphTags").Order("id desc").Limit(limit).Find(news)
}
for _, item := range *news {
tags := &[]models.Tags{}
@@ -182,34 +115,12 @@ 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("data_time desc,time desc").Limit(50).Find(news)
db.Dao.Model(news).Preload("TelegraphTags").Where("source=?", source).Order("id desc").Limit(20).Find(news)
} else {
db.Dao.Model(news).Preload("TelegraphTags").Order("data_time desc,time desc").Limit(50).Find(news)
db.Dao.Model(news).Preload("TelegraphTags").Order("id desc").Limit(20).Find(news)
}
for _, item := range *news {
tags := &[]models.Tags{}
@@ -266,10 +177,6 @@ func (m MarketNewsApi) GetSinaNews(crawlTimeOut uint) *[]models.Telegraph {
//logger.SugaredLogger.Infof("%s:%s", data["create_time"], data["rich_text"])
telegraph.Content = 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)
@@ -288,7 +195,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("content=? and source=?", telegraph.Content, telegraph.Source).Count(&cnt)
db.Dao.Model(telegraph).Where("time=? and source=?", telegraph.Time, telegraph.Source).Count(&cnt)
if cnt == 0 {
db.Dao.Create(&telegraph)
telegraphs = append(telegraphs, telegraph)
@@ -642,19 +549,10 @@ func (m MarketNewsApi) EMDictCode(code string, cache *freecache.Cache) []any {
return respMap["data"].([]any)
}
func (m MarketNewsApi) TradingViewNews() *[]models.Telegraph {
client := resty.New()
config := GetSettingConfig()
if config.HttpProxyEnabled && config.HttpProxy != "" {
client.SetProxy(config.HttpProxy)
}
func (m MarketNewsApi) TradingViewNews() *[]models.TVNews {
TVNews := &[]models.TVNews{}
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().
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().
SetHeader("Host", "news-mediator.tradingview.com").
SetHeader("Origin", "https://cn.tradingview.com").
SetHeader("Referer", "https://cn.tradingview.com/").
@@ -662,81 +560,19 @@ func (m MarketNewsApi) TradingViewNews() *[]models.Telegraph {
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("TradingViewNews err:%s", err.Error())
return news
return TVNews
}
respMap := map[string]any{}
err = json.Unmarshal(resp.Body(), &respMap)
if err != nil {
return news
return TVNews
}
items, err := json.Marshal(respMap["items"])
if err != nil {
return news
return TVNews
}
json.Unmarshal(items, 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
return TVNews
}
func (m MarketNewsApi) XUEQIUHotStock(size int, marketType string) *[]models.HotItem {
@@ -763,7 +599,7 @@ func (m MarketNewsApi) XUEQIUHotStock(size int, marketType string) *[]models.Hot
logger.SugaredLogger.Errorf("XUEQIUHotStock err:%s", err.Error())
return &[]models.HotItem{}
}
//logger.SugaredLogger.Infof("XUEQIUHotStock:%+v", res)
logger.SugaredLogger.Infof("XUEQIUHotStock:%+v", res)
return &res.Data.Items
}
@@ -865,264 +701,3 @@ func (m MarketNewsApi) ClsCalendar() []any {
err = json.Unmarshal(resp.Body(), &respMap)
return respMap["data"].([]any)
}
func (m MarketNewsApi) GetGDP() *models.GDPResp {
res := &models.GDPResp{}
url := "https://datacenter-web.eastmoney.com/api/data/v1/get?callback=data&columns=REPORT_DATE%2CTIME%2CDOMESTICL_PRODUCT_BASE%2CFIRST_PRODUCT_BASE%2CSECOND_PRODUCT_BASE%2CTHIRD_PRODUCT_BASE%2CSUM_SAME%2CFIRST_SAME%2CSECOND_SAME%2CTHIRD_SAME&pageNumber=1&pageSize=20&sortColumns=REPORT_DATE&sortTypes=-1&source=WEB&client=WEB&reportName=RPT_ECONOMY_GDP&p=1&pageNo=1&pageNum=1&_=" + strconv.FormatInt(time.Now().Unix(), 10)
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "datacenter-web.eastmoney.com").
SetHeader("Origin", "https://datacenter.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/cjsj/gdp.html").
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())
return res
}
body := resp.Body()
logger.SugaredLogger.Debugf("GDP:%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())
return res
}
data, _ := val.Object().Value().Export()
logger.SugaredLogger.Infof("GDP:%v", data)
marshal, err := json.Marshal(data)
if err != nil {
return res
}
json.Unmarshal(marshal, &res)
logger.SugaredLogger.Infof("GDP:%+v", res)
return res
}
func (m MarketNewsApi) GetCPI() *models.CPIResp {
res := &models.CPIResp{}
url := "https://datacenter-web.eastmoney.com/api/data/v1/get?callback=data&columns=REPORT_DATE%2CTIME%2CNATIONAL_SAME%2CNATIONAL_BASE%2CNATIONAL_SEQUENTIAL%2CNATIONAL_ACCUMULATE%2CCITY_SAME%2CCITY_BASE%2CCITY_SEQUENTIAL%2CCITY_ACCUMULATE%2CRURAL_SAME%2CRURAL_BASE%2CRURAL_SEQUENTIAL%2CRURAL_ACCUMULATE&pageNumber=1&pageSize=20&sortColumns=REPORT_DATE&sortTypes=-1&source=WEB&client=WEB&reportName=RPT_ECONOMY_CPI&p=1&pageNo=1&pageNum=1&_=" + strconv.FormatInt(time.Now().Unix(), 10)
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "datacenter-web.eastmoney.com").
SetHeader("Origin", "https://datacenter.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/cjsj/gdp.html").
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("GetCPI err:%s", err.Error())
return res
}
body := resp.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("GetCPI err:%s", err.Error())
return res
}
data, _ := val.Object().Value().Export()
logger.SugaredLogger.Infof("GetCPI:%v", data)
marshal, err := json.Marshal(data)
if err != nil {
return res
}
json.Unmarshal(marshal, &res)
logger.SugaredLogger.Infof("GetCPI:%+v", res)
return res
}
// GetPPI PPI
func (m MarketNewsApi) GetPPI() *models.PPIResp {
res := &models.PPIResp{}
url := "https://datacenter-web.eastmoney.com/api/data/v1/get?callback=data&columns=REPORT_DATE,TIME,BASE,BASE_SAME,BASE_ACCUMULATE&pageNumber=1&pageSize=20&sortColumns=REPORT_DATE&sortTypes=-1&source=WEB&client=WEB&reportName=RPT_ECONOMY_PPI&p=1&pageNo=1&pageNum=1&_=" + strconv.FormatInt(time.Now().Unix(), 10)
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "datacenter-web.eastmoney.com").
SetHeader("Origin", "https://datacenter.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/cjsj/gdp.html").
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("GetPPI err:%s", err.Error())
return res
}
body := resp.Body()
vm := otto.New()
vm.Run("function data(res){return res};")
val, err := vm.Run(body)
if err != nil {
return res
}
data, _ := val.Object().Value().Export()
marshal, err := json.Marshal(data)
if err != nil {
return res
}
json.Unmarshal(marshal, &res)
return res
}
func (m MarketNewsApi) GetPMI() *models.PMIResp {
res := &models.PMIResp{}
url := "https://datacenter-web.eastmoney.com/api/data/v1/get?callback=data&columns=REPORT_DATE%2CTIME%2CMAKE_INDEX%2CMAKE_SAME%2CNMAKE_INDEX%2CNMAKE_SAME&pageNumber=1&pageSize=20&sortColumns=REPORT_DATE&sortTypes=-1&source=WEB&client=WEB&reportName=RPT_ECONOMY_PMI&p=1&pageNo=1&pageNum=1&_=" + strconv.FormatInt(time.Now().Unix(), 10)
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "datacenter-web.eastmoney.com").
SetHeader("Origin", "https://datacenter.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/cjsj/gdp.html").
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 {
return res
}
body := resp.Body()
vm := otto.New()
vm.Run("function data(res){return res};")
val, err := vm.Run(body)
if err != nil {
return res
}
data, _ := val.Object().Value().Export()
marshal, err := json.Marshal(data)
if err != nil {
return res
}
json.Unmarshal(marshal, &res)
return res
}
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").
SetHeader("Origin", "https://data.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/report/industry.jshtml").
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("GetIndustryReportInfo err:%s", err.Error())
return ""
}
body := resp.Body()
//logger.SugaredLogger.Debugf("GetIndustryReportInfo:%s", body)
doc, err := goquery.NewDocumentFromReader(strings.NewReader(string(body)))
title, _ := doc.Find("div.c-title").Html()
content, _ := doc.Find("div.ctx-content").Html()
//logger.SugaredLogger.Infof("GetIndustryReportInfo:\n%s\n%s", title, content)
markdown, err := util.HTMLToMarkdown(title + content)
if err != nil {
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
}

View File

@@ -2,17 +2,10 @@ package data
import (
"encoding/json"
"github.com/coocood/freecache"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"go-stock/backend/util"
"strings"
"testing"
"github.com/coocood/freecache"
"github.com/duke-git/lancet/v2/random"
"github.com/go-resty/resty/v2"
"github.com/tidwall/gjson"
)
// @Author spark
@@ -22,12 +15,7 @@ import (
func TestGetSinaNews(t *testing.T) {
db.Init("../../data/stock.db")
InitAnalyzeSentiment()
news := NewMarketNewsApi().GetSinaNews(30)
for i, telegraph := range *news {
logger.SugaredLogger.Debugf("key: %+v, value: %+v", i, telegraph)
}
NewMarketNewsApi().GetSinaNews(30)
//NewMarketNewsApi().GetNewTelegraph(30)
}
@@ -45,6 +33,7 @@ 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) {
@@ -79,24 +68,19 @@ func TestLongTiger(t *testing.T) {
func TestStockResearchReport(t *testing.T) {
db.Init("../../data/stock.db")
resp := NewMarketNewsApi().StockResearchReport("688082", 7)
resp := NewMarketNewsApi().StockResearchReport("600584.sh", 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("456", 7)
resp := NewMarketNewsApi().IndustryResearchReport("", 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) {
@@ -114,22 +98,15 @@ 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")
InitAnalyzeSentiment()
NewMarketNewsApi().TradingViewNews()
resp := NewMarketNewsApi().TradingViewNews()
for _, a := range *resp {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestXUEQIUHotStock(t *testing.T) {
@@ -163,126 +140,14 @@ func TestInvestCalendar(t *testing.T) {
db.Init("../../data/stock.db")
res := NewMarketNewsApi().InvestCalendar("2025-06")
for _, a := range res {
bytes, err := json.Marshal(a)
if err != nil {
continue
}
date := gjson.Get(string(bytes), "date")
list := gjson.Get(string(bytes), "list")
logger.SugaredLogger.Debugf("value: %+v,list: %+v", date.String(), list)
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestClsCalendar(t *testing.T) {
db.Init("../../data/stock.db")
res := NewMarketNewsApi().ClsCalendar()
md := strings.Builder{}
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
})
logger.SugaredLogger.Debugf("value: %+v", a)
}
logger.SugaredLogger.Debugf("md:\n %s", md.String())
}
func TestGetGDP(t *testing.T) {
res := NewMarketNewsApi().GetGDP()
md := util.MarkdownTableWithTitle("国内生产总值(GDP)", res.GDPResult.Data)
logger.SugaredLogger.Debugf(md)
}
func TestGetCPI(t *testing.T) {
res := NewMarketNewsApi().GetCPI()
md := util.MarkdownTableWithTitle("居民消费价格指数(CPI)", res.CPIResult.Data)
logger.SugaredLogger.Debugf(md)
}
// PPI
func TestGetPPI(t *testing.T) {
res := NewMarketNewsApi().GetPPI()
md := util.MarkdownTableWithTitle("工业品出厂价格指数(PPI)", res.PPIResult.Data)
logger.SugaredLogger.Debugf(md)
}
// PMI
func TestGetPMI(t *testing.T) {
res := NewMarketNewsApi().GetPMI()
md := util.MarkdownTableWithTitle("采购经理人指数(PMI)", res.PMIResult.Data)
logger.SugaredLogger.Debugf(md)
}
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 := "https://go-stock.sparkmemory.top/%E5%88%86%E6%9E%90%E6%8A%A5%E5%91%8A/2025/12/09/%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-09)AI%E5%88%86%E6%9E%90%E7%BB%93%E6%9E%9C.md"
post, err := resty.New().SetBaseURL("https://go-stock.sparkmemory.top:16667").R().
SetHeader("Content-Type", "text/markdown").
SetHeader("Filename", "投资分析报告.md").
SetHeader("Attach", attach).
SetBody("分析报告").Post("/go-stock")
if err != nil {
logger.SugaredLogger.Error(err)
return
}
logger.SugaredLogger.Debugf("value: %s", post.String())
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,46 +3,17 @@ 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{
Type: "function",
Function: ToolFunction{
Name: "SearchStockByIndicators",
Description: "根据自然语言筛选股票,返回自然语言选股条件要求的股票所有相关数据",
Parameters: FunctionParameters{
Type: "object",
Properties: map[string]any{
"words": map[string]any{
"type": "string",
"description": "选股自然语言,并且条件使用;分隔,或者条件使用,分隔。例如:创新药;PE<30;净利润增长率>50%;",
},
},
Required: []string{"words"},
},
},
})
ai := NewDeepSeekOpenAi(context.TODO(), 0)
//res := ai.NewChatStream("长电科技", "sh600584", "长电科技分析和总结", nil)
res := ai.NewSummaryStockNewsStreamWithTools("总结市场资讯,发掘潜力标的/行业/板块/概念,控制风险。调用工具函数验证", nil, tools)
ai := NewDeepSeekOpenAi(context.TODO())
res := ai.NewChatStream("长电科技", "sh600584", "长电科技分析和总结", nil)
for {
select {
case msg := <-res:
if len(msg) > 0 {
t.Log(msg)
if msg["content"] == "DONE" {
return
}
}
t.Log(msg)
}
}
}
@@ -54,17 +25,8 @@ func TestGetTopNewsList(t *testing.T) {
func TestSearchGuShiTongStockInfo(t *testing.T) {
db.Init("../../data/stock.db")
//SearchGuShiTongStockInfo("hk01810", 60)
msgs := SearchGuShiTongStockInfo("sh600745", 60)
for _, msg := range *msgs {
log.SugaredLogger.Infof("%s", msg)
}
//SearchGuShiTongStockInfo("gb_goog", 60)
SearchGuShiTongStockInfo("hk01810", 60)
SearchGuShiTongStockInfo("sh600745", 60)
SearchGuShiTongStockInfo("gb_goog", 60)
}
func TestGetZSInfo(t *testing.T) {
db.Init("../../data/stock.db")
GetZSInfo("中证银行", "sz399986", 30)
GetZSInfo("上海贝岭", "sh600171", 30)
}

View File

@@ -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 := GetSettingConfig().BrowserPath
crawlTimeOut := GetSettingConfig().CrawlTimeOut
path := GetConfig().BrowserPath
crawlTimeOut := GetConfig().CrawlTimeOut
if crawlTimeOut < 15 {
crawlTimeOut = 30
}

View File

@@ -3,10 +3,9 @@ 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,64 +19,37 @@ type SearchStockApi struct {
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对应的值。保存到设置中的东财唯一标识输入框",
}
}
func (s SearchStockApi) SearchStock() map[string]any {
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:145.0) Gecko/20100101 Firefox/145.0").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetHeader("Content-Type", "application/json").
SetBody(fmt.Sprintf(`{
"keyWord": "%s",
"pageSize": %d,
"pageSize": 50000,
"pageNo": 1,
"fingerprint": "%s",
"fingerprint": "e38b5faabf9378c8238e57219f0ebc9b",
"gids": [],
"matchWord": "",
"timestamp": "%d",
"timestamp": "1751113883290349",
"shareToGuba": false,
"requestId": "",
"requestId": "8xTWgCDAjvQ5lmvz5mDA3Ydk2AE4yoiJ1751113883290",
"needCorrect": true,
"removedConditionIdList": [],
"xcId": "",
"xcId": "xc0af28549ab330013ed",
"ownSelectAll": false,
"dxInfo": [],
"extraCondition": ""
}`, s.words, pageSize, qgqpBId, time.Now().Unix())).Post(url)
}`, s.words)).Post(url)
if err != nil {
logger.SugaredLogger.Errorf("SearchStock-err:%+v", err)
return map[string]any{
"code": -1,
"message": err.Error(),
}
return map[string]any{}
}
respMap := map[string]any{}
json.Unmarshal(resp.Body(), &respMap)
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
return respMap
}
func (s SearchStockApi) HotStrategy() map[string]any {
url := fmt.Sprintf("https://np-ipick.eastmoney.com/recommend/stock/heat/ranking?count=20&trace=%d&client=web&biz=web_smart_tag", time.Now().Unix())
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "np-ipick.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").
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("HotStrategy-err:%+v", err)
return map[string]any{}
}
respMap := map[string]any{}
json.Unmarshal(resp.Body(), &respMap)
return respMap
}

View File

@@ -1,69 +1,25 @@
package data
import (
"encoding/json"
"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")
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)
res := NewSearchStockApi("算力股;净利润连续3年增长").SearchStock()
data := res["data"].(map[string]any)
result := data["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 {
//logger.SugaredLogger.Infof("v:%+v", v)
d := v.(map[string]any)
tmp := map[string]any{}
for key, title := range headers {
//logger.SugaredLogger.Infof("%s:%s", title, convertor.ToString(d[key]))
tmp[title] = convertor.ToString(d[key])
}
*table = append(*table, tmp)
//logger.SugaredLogger.Infof("--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------")
logger.SugaredLogger.Infof("%s:%s", d["INDUSTRY"], d["SECURITY_SHORT_NAME"])
}
jsonData, _ := json.Marshal(*table)
markdownTable, _ := JSONToMarkdownTable(jsonData)
logger.SugaredLogger.Infof("markdownTable=\n%s", markdownTable)
}
//columns := result["columns"].([]any)
//for _, v := range columns {
// logger.SugaredLogger.Infof("v:%+v", v)
//}
func TestSearchStockApi_HotStrategy(t *testing.T) {
db.Init("../../data/stock.db")
res := NewSearchStockApi("").HotStrategy()
logger.SugaredLogger.Infof("res:%+v", res)
dataList := res["data"].([]any)
for _, v := range dataList {
d := v.(map[string]any)
logger.SugaredLogger.Infof("v:%+v", d)
}
}

View File

@@ -2,12 +2,8 @@ package data
import (
"encoding/json"
"errors"
"go-stock/backend/db"
"go-stock/backend/logger"
"time"
"github.com/samber/lo"
"gorm.io/gorm"
)
@@ -19,202 +15,110 @@ type Settings struct {
DingRobot string `json:"dingRobot"`
UpdateBasicInfoOnStart bool `json:"updateBasicInfoOnStart"`
RefreshInterval int64 `json:"refreshInterval"`
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"`
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"`
}
func (receiver Settings) TableName() string {
return "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 (AIConfig) TableName() string {
return "ai_config"
}
type SettingConfig struct {
*Settings
AiConfigs []*AIConfig `json:"aiConfigs"`
}
type SettingsApi struct {
Config *SettingConfig
Config Settings
}
func NewSettingsApi() *SettingsApi {
func NewSettingsApi(settings *Settings) *SettingsApi {
return &SettingsApi{
Config: GetSettingConfig(),
Config: *settings,
}
}
func (s *SettingsApi) Export() string {
d, _ := json.MarshalIndent(s.Config, "", " ")
return string(d)
}
func UpdateConfig(s *SettingConfig) string {
func (s SettingsApi) UpdateConfig() string {
count := int64(0)
db.Dao.Model(&Settings{}).Count(&count)
db.Dao.Model(s.Config).Count(&count)
if count > 0 {
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,
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,
})
//更新AiConfig
err := updateAiConfigs(s.AiConfigs)
if err != nil {
logger.SugaredLogger.Errorf("更新AI模型服务配置失败: %v", err)
return "更新AI模型服务配置失败: " + err.Error()
}
} else {
logger.SugaredLogger.Infof("未找到配置,创建默认配置")
// 创建主配置
result := db.Dao.Model(&Settings{}).Create(&Settings{})
if result.Error != nil {
logger.SugaredLogger.Error("创建配置失败:", result.Error)
return "创建配置失败: " + result.Error.Error()
}
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,
})
}
return "保存成功!"
}
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)
}
func (s SettingsApi) GetConfig() *Settings {
var settings Settings
db.Dao.Model(&Settings{}).First(&settings)
if settings.OpenAiEnable {
// 处理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.OpenAiApiTimeOut <= 0 {
settings.OpenAiApiTimeOut = 60 * 5
}
if settings.CrawlTimeOut <= 0 {
settings.CrawlTimeOut = 60
@@ -224,13 +128,15 @@ func GetSettingConfig() *SettingConfig {
}
}
if settings.BrowserPath == "" {
settings.BrowserPath, _ = CheckBrowser()
settings.BrowserPath, _ = CheckBrowserOnWindows()
}
if settings.BrowserPoolSize <= 0 {
settings.BrowserPoolSize = 1
}
settingConfig.Settings = settings
settingConfig.AiConfigs = aiConfigs
return settingConfig
return &settings
}
func (s SettingsApi) Export() string {
d, _ := json.MarshalIndent(s.GetConfig(), "", " ")
return string(d)
}

View File

@@ -9,14 +9,6 @@ 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"
@@ -25,10 +17,18 @@ 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/sys/windows/registry"
"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"
@@ -38,7 +38,7 @@ const tushareApiUrl = "http://api.tushare.pro"
type StockDataApi struct {
client *resty.Client
config *SettingConfig
config *Settings
}
type StockInfo struct {
gorm.Model
@@ -154,8 +154,6 @@ type StockBasic struct {
IsHs string `json:"is_hs"`
ActName string `json:"act_name"`
ActEntType string `json:"act_ent_type"`
BKName string `json:"bk_name"`
BKCode string `json:"bk_code"`
}
type FollowedStock struct {
@@ -173,7 +171,6 @@ 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 {
@@ -198,7 +195,7 @@ func (receiver StockBasic) TableName() string {
func NewStockDataApi() *StockDataApi {
return &StockDataApi{
client: resty.New(),
config: GetSettingConfig(),
config: GetConfig(),
}
}
@@ -379,9 +376,6 @@ func (receiver StockDataApi) GetStockCodeRealTimeData(StockCodes ...string) (*[]
logger.SugaredLogger.Error(err.Error())
continue
}
if stockData == nil {
continue
}
stockInfos = append(stockInfos, *stockData)
go func() {
@@ -420,15 +414,6 @@ 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)
@@ -1178,7 +1163,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{
@@ -1203,10 +1188,6 @@ 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)
@@ -1317,6 +1298,50 @@ func SearchStockInfoByCode(stock string) *[]string {
return &messages
}
// checkChromeOnWindows 在 Windows 系统上检查谷歌浏览器是否安装
func checkChromeOnWindows() (string, bool) {
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe`, registry.QUERY_VALUE)
if err != nil {
// 尝试在 WOW6432Node 中查找(适用于 64 位系统上的 32 位程序)
key, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe`, registry.QUERY_VALUE)
if err != nil {
return "", false
}
defer key.Close()
}
defer key.Close()
path, _, err := key.GetStringValue("Path")
//logger.SugaredLogger.Infof("Chrome安装路径%s", path)
if err != nil {
return "", false
}
return path + "\\chrome.exe", true
}
// CheckBrowserOnWindows 在 Windows 系统上检查Edge浏览器是否安装并返回安装路径
func CheckBrowserOnWindows() (string, bool) {
if path, ok := checkChromeOnWindows(); ok {
return path, true
}
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe`, registry.QUERY_VALUE)
if err != nil {
// 尝试在 WOW6432Node 中查找(适用于 64 位系统上的 32 位程序)
key, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe`, registry.QUERY_VALUE)
if err != nil {
return "", false
}
defer key.Close()
}
defer key.Close()
path, _, err := key.GetStringValue("Path")
//logger.SugaredLogger.Infof("Edge安装路径%s", path)
if err != nil {
return "", false
}
return path + "\\msedge.exe", true
}
// 分时数据
func (receiver StockDataApi) GetStockMinutePriceData(stockCode string) (*[]MinuteData, string) {
url := fmt.Sprintf("https://web.ifzq.gtimg.cn/appstock/app/minute/query?code=%s", stockCode)
@@ -1474,7 +1499,7 @@ func getSinaStockInfo(receiver StockDataApi, page, pageSize int) *[]models.SinaS
func (receiver StockDataApi) getDCStockInfo(market string, page, pageSize int) {
//m:105,m:106,m:107 //美股
//m:128+t:3,m:128+t:4,m:128+t:1,m:128+t:2 //港股
fs := "m:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23,m:0+t:81+s:2048"
fs := ""
switch market {
case "hk":
fs = "m:128+t:3,m:128+t:4,m:128+t:1,m:128+t:2"
@@ -1482,108 +1507,62 @@ func (receiver StockDataApi) getDCStockInfo(market string, page, pageSize int) {
fs = "m:105,m:106,m:107"
}
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("page:%d url:%s", page, sprintfUrl)
url := "https://push2.eastmoney.com/api/qt/clist/get?cb=jQuery371047843066631541353_1745889398012&fs=%s&fields=f12,f13,f14,f19,f1,f2,f4,f3,f152,f17,f18,f15,f16,f5,f6&fid=f3&pn=%d&pz=%d&po=1&dect=1"
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; rv:146.0) Gecko/20100101 Firefox/146.0").
Get(sprintfUrl)
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").
Get(fmt.Sprintf(url, fs, page, pageSize))
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
return
}
body := string(resp.Body())
logger.SugaredLogger.Infof("resp:%s", body)
body = strutil.ReplaceWithMap(body, map[string]string{
"jQuery371047843066631541353_1745889398012(": "",
");": "",
})
js := "var d=" + body
vm := otto.New()
vm.Run("function data(res){return res};")
val, err := vm.Run(body)
if err != nil {
logger.SugaredLogger.Errorf("vm.Run error:%v", err.Error())
}
value, _ := val.Object().Value().Export()
marshal, err := json.Marshal(value)
_, err = vm.Run(js)
_, err = vm.Run("var data = JSON.stringify(d);")
value, err := vm.Get("data")
data := make(map[string]any)
err = json.Unmarshal(marshal, &data)
err = json.Unmarshal([]byte(value.String()), &data)
if err != nil {
logger.SugaredLogger.Errorf("json:%s", value.String())
logger.SugaredLogger.Errorf("json.Unmarshal error:%v", err.Error())
}
logger.SugaredLogger.Infof("resp:%s", data)
if data["data"] != nil {
datas := data["data"].(map[string]any)
total := datas["total"].(float64)
diff := datas["diff"].([]any)
diff := datas["diff"].(map[string]any)
logger.SugaredLogger.Infof("total:%d", int(total))
for k, item := range diff {
stock := item.(map[string]any)
logger.SugaredLogger.Infof("k:%d,%s:%s:%s %s:%s", k, stock["f14"], stock["f12"], DCToTsCode(stock["f12"].(string)), stock["f100"], stock["f265"])
if market == "" {
stockInfo := &StockBasic{
Symbol: stock["f12"].(string),
TsCode: DCToTsCode(stock["f12"].(string)),
Name: stock["f14"].(string),
BKName: stock["f100"].(string),
BKCode: stock["f265"].(string),
}
db.Dao.Model(&StockBasic{}).Where("symbol = ?", stockInfo.Symbol).First(stockInfo)
logger.SugaredLogger.Infof("stockInfo:%+v", stockInfo)
if stockInfo.ID == 0 {
db.Dao.Model(&StockBasic{}).Create(stockInfo)
} else {
stockInfo = &StockBasic{
Symbol: stock["f12"].(string),
TsCode: DCToTsCode(stock["f12"].(string)),
Name: stock["f14"].(string),
BKName: stock["f100"].(string),
BKCode: stock["f265"].(string),
}
db.Dao.Model(&StockBasic{}).Where("symbol = ?", stockInfo.Symbol).Updates(stockInfo)
}
}
logger.SugaredLogger.Infof("k:%s,%s:%s", k, stock["f14"], stock["f12"])
if market == "hk" {
stockInfo := &models.StockInfoHK{
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".HK",
Name: stock["f14"].(string),
BKName: stock["f100"].(string),
BKCode: stock["f265"].(string),
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".HK",
Name: stock["f14"].(string),
}
db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", stockInfo.Code).First(stockInfo)
logger.SugaredLogger.Infof("stockInfo:%+v", stockInfo)
if stockInfo.ID == 0 {
db.Dao.Model(&models.StockInfoHK{}).Create(stockInfo)
} else {
stockInfo = &models.StockInfoHK{
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".HK",
Name: stock["f14"].(string),
BKName: stock["f100"].(string),
BKCode: stock["f265"].(string),
}
db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", stockInfo.Code).Updates(stockInfo)
}
}
if market == "us" {
stockInfo := &models.StockInfoUS{
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".US",
Name: stock["f14"].(string),
BKName: stock["f100"].(string),
BKCode: stock["f265"].(string),
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".US",
Name: stock["f14"].(string),
}
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", stockInfo.Code).First(stockInfo)
logger.SugaredLogger.Infof("stockInfo:%+v", stockInfo)
if stockInfo.ID == 0 {
db.Dao.Model(&models.StockInfoUS{}).Create(stockInfo)
} else {
stockInfo = &models.StockInfoUS{
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".US",
Name: stock["f14"].(string),
BKName: stock["f100"].(string),
BKCode: stock["f265"].(string),
}
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", stockInfo.Code).Updates(stockInfo)
}
}
@@ -1592,25 +1571,6 @@ func (receiver StockDataApi) getDCStockInfo(market string, page, pageSize int) {
}
}
func DCToTsCode(dcCode string) string {
//北京证券交易所 883、87、88 等) 创新型中小企业(专精特新为主)
//上海证券交易所 660、688 等) 大盘蓝筹、科创板(高新技术)
//深圳证券交易所 0、3000、002、30 等) 中小盘、创业板(成长型创新企业)
switch dcCode[0:1] {
case "8":
return dcCode + ".BJ"
case "9":
return dcCode + ".BJ"
case "6":
return dcCode + ".SH"
case "0":
return dcCode + ".SZ"
case "3":
return dcCode + ".SZ"
}
return ""
}
func (receiver StockDataApi) GetHKStockInfo(pageSize int) {
url := "https://stock.gtimg.cn/data/hk_rank.php?board=main_all&metric=price&pageSize=%d&reqPage=1&order=desc&var_name=list_data"
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
@@ -1745,11 +1705,6 @@ 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{}

View File

@@ -1,37 +0,0 @@
//go:build darwin
// +build darwin
package data
import "os"
// CheckChrome 检查 macOS 是否安装了 Chrome 浏览器
func CheckChrome() (string, bool) {
// 检查 /Applications 目录下是否存在 Chrome
locations := []string{
// Mac
"/Applications/Chromium.app/Contents/MacOS/Chromium",
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
}
path := ""
for _, location := range locations {
_, err := os.Stat(location)
if err != nil {
continue
}
path = location
}
if path == "" {
return "", false
}
return path, true
}
// CheckBrowser 检查 macOS 是否安装了浏览器,并返回安装路径
func CheckBrowser() (string, bool) {
if path, ok := CheckChrome(); ok {
return path, ok
}
return "", false
}

View File

@@ -4,19 +4,17 @@ 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
@@ -50,23 +48,14 @@ 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(searchWords, "telegram", 30)
messages := SearchStockInfo("谷歌", "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")
@@ -74,7 +63,7 @@ func TestSearchStockInfoByCode(t *testing.T) {
func TestSearchStockPriceInfo(t *testing.T) {
db.Init("../../data/stock.db")
SearchStockPriceInfo("博安生物", "hk06955", 30)
//SearchStockPriceInfo("中信证券", "hk06030", 30)
SearchStockPriceInfo("上海贝岭", "sh600171", 30)
//SearchStockPriceInfo("苹果公司", "gb_aapl", 30)
//SearchStockPriceInfo("微创光电", "bj430198", 30)
@@ -121,10 +110,9 @@ 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 //港股
//274 224 605
for i := 197; i <= 274; i++ {
NewStockDataApi().getDCStockInfo("", i, 20)
time.Sleep(time.Duration(random.RandInt(2, 5)) * time.Second)
for i := 1; i <= 592; i++ {
NewStockDataApi().getDCStockInfo("us", i, 20)
time.Sleep(time.Duration(random.RandInt(1, 3)) * time.Second)
}
}
@@ -265,21 +253,3 @@ 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())
//}
}

View File

@@ -1,50 +0,0 @@
//go:build windows
// +build windows
package data
import "golang.org/x/sys/windows/registry"
// CheckChrome 在 Windows 系统上检查谷歌浏览器是否安装
func CheckChrome() (string, bool) {
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe`, registry.QUERY_VALUE)
if err != nil {
// 尝试在 WOW6432Node 中查找(适用于 64 位系统上的 32 位程序)
key, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe`, registry.QUERY_VALUE)
if err != nil {
return "", false
}
defer key.Close()
}
defer key.Close()
path, _, err := key.GetStringValue("Path")
//logger.SugaredLogger.Infof("Chrome安装路径%s", path)
if err != nil {
return "", false
}
return path + "\\chrome.exe", true
}
// CheckBrowser 在 Windows 系统上检查Edge浏览器是否安装并返回安装路径
func CheckBrowser() (string, bool) {
if path, ok := CheckChrome(); ok {
return path, true
}
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe`, registry.QUERY_VALUE)
if err != nil {
// 尝试在 WOW6432Node 中查找(适用于 64 位系统上的 32 位程序)
key, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe`, registry.QUERY_VALUE)
if err != nil {
return "", false
}
defer key.Close()
}
defer key.Close()
path, _, err := key.GetStringValue("Path")
//logger.SugaredLogger.Infof("Edge安装路径%s", path)
if err != nil {
return "", false
}
return path + "\\msedge.exe", true
}

View File

@@ -39,74 +39,17 @@ func NewStockGroupApi(dao *gorm.DB) *StockGroupApi {
}
func (receiver StockGroupApi) AddGroup(group Group) bool {
// 检查是否已存在相同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
err := receiver.dao.Where("name = ?", group.Name).FirstOrCreate(&group).Updates(&Group{
Name: group.Name,
Sort: group.Sort,
}).Error
return err == nil
}
func (receiver StockGroupApi) GetGroupList() []Group {
var groups []Group
receiver.dao.Order("sort ASC").Find(&groups)
receiver.dao.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(&currentGroup, 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)

View File

@@ -2,50 +2,37 @@ package data
import (
"bufio"
_ "embed"
"fmt"
"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"
"go-stock/backend/logger"
"os"
"strings"
)
const basefreq float64 = 100
// 金融情感词典,包含股票市场相关的专业词汇
var (
seg gse.Segmenter
// 正面金融词汇及其权重
positiveFinanceWords = map[string]float64{
"涨": 1.0, "上涨": 2.0, "涨停": 3.0, "牛市": 3.0, "反弹": 2.0, "新高": 2.5,
"上涨": 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, "暴涨": 3.0,
"大涨": 2.5, "飙升": 3.0, "井喷": 3.0, "爆发": 2.5, "暴涨": 3.0,
}
// 负面金融词汇及其权重
negativeFinanceWords = map[string]float64{
"跌": 2.0, "下跌": 2.0, "跌停": 3.0, "熊市": 3.0, "回调": 2.5, "新低": 2.5,
"下跌": 2.0, "跌停": 3.0, "熊市": 3.0, "回调": 1.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, "走弱": 2.5, "下挫": 2.5,
"衰退": 2.0, "跌破": 2.0, "创新低": 3.0, "走弱": 1.5, "下挫": 1.5,
"利空消息": 3.0, "收益下降": 2.5, "利润下滑": 2.5, "业绩不佳": 2.5,
"垃圾股": 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,
"垃圾股": 2.0, "风险股": 2.0, "弱势": 1.5, "走低": 1.5, "缩量": 2.5,
"大跌": 2.5, "暴跌": 3.0, "崩盘": 3.0, "跳水": 3.0, "重挫": 3.0,
}
// 否定词,用于反转情感极性
@@ -57,7 +44,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, "超": 1.8,
"大幅": 1.8, "急剧": 2.0, "轻微": 0.6, "小幅": 0.7,
}
// 转折词,用于识别情感转折
@@ -66,267 +53,12 @@ var (
}
)
//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)
func init() {
// 加载默认词典
err := seg.LoadDict()
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 情感分析结果类型

View File

@@ -2,12 +2,8 @@ package data
import (
"fmt"
"go-stock/backend/db"
"go-stock/backend/logger"
"strings"
"testing"
"github.com/duke-git/lancet/v2/random"
)
// @Author spark
@@ -16,34 +12,25 @@ import (
//-----------------------------------------------------------------------------------
func TestAnalyzeSentiment(t *testing.T) {
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月消费者信心指数降至5110月为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亿美元。"
// 分析情感
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日电据央视新闻报道以军在贝尔谢巴及周边区域拦截了数枚伊朗导弹但仍有导弹或拦截残骸落地。以色列国防军发文表示搜救队伍正在一处“空中物体落地”的所在区域开展工作公众目前可以离开避难场所。伊朗方面对上述说法暂无回应。"
words := splitWords(text)
fmt.Println(strings.Join(words, " "))
result, frequencies := AnalyzeSentimentWithFreqWeight(text)
// 过滤标点符号和分隔符
cleanFrequencies := FilterPunctuationAndSeparators(frequencies)
result := AnalyzeSentiment(text)
// 输出结果
logger.SugaredLogger.Infof("情感分析结果: %s (得分: %.2f, 正面词:%d, 负面词:%d)\n 词频统计结果: %v",
fmt.Printf("情感分析结果: %s (得分: %.2f, 正面词:%d, 负面词:%d)\n",
result.Description,
result.Score,
result.PositiveCount,
result.NegativeCount,
cleanFrequencies,
)
result.NegativeCount)
}

View File

@@ -17,10 +17,10 @@ import (
type TushareApi struct {
client *resty.Client
config *SettingConfig
config *Settings
}
func NewTushareApi(config *SettingConfig) *TushareApi {
func NewTushareApi(config *Settings) *TushareApi {
return &TushareApi{
client: resty.New(),
config: config,

View File

@@ -11,7 +11,7 @@ import (
// -----------------------------------------------------------------------------------
func TestGetDaily(t *testing.T) {
db.Init("../../data/stock.db")
tushareApi := NewTushareApi(GetSettingConfig())
tushareApi := NewTushareApi(GetConfig())
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(GetSettingConfig())
tushareApi := NewTushareApi(GetConfig())
res := tushareApi.GetDaily("gb_AAPL", "20250101", "20250217", 30)
t.Log(res)

View File

@@ -1,14 +1,13 @@
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
@@ -27,7 +26,7 @@ func Init(sqlitePath string) {
var openDb *gorm.DB
var err error
if sqlitePath == "" {
sqlitePath = "data/stock.db?cache_size=-524288&journal_mode=WAL"
sqlitePath = "data/stock.db?cache=shared&mode=rwc&_journal_mode=WAL"
}
openDb, err = gorm.Open(sqlite.Open(sqlitePath), &gorm.Config{
Logger: dbLogger,
@@ -49,8 +48,8 @@ func Init(sqlitePath string) {
if err != nil {
log.Fatalf("openDb.DB error is %s", err.Error())
}
dbCon.SetMaxIdleConns(4)
dbCon.SetMaxOpenConns(10)
dbCon.SetMaxIdleConns(10)
dbCon.SetMaxOpenConns(100)
dbCon.SetConnMaxLifetime(time.Hour)
Dao = openDb
}

View File

@@ -1,10 +1,9 @@
package models
import (
"time"
"gorm.io/gorm"
"gorm.io/plugin/soft_delete"
"time"
)
// @Author spark
@@ -151,15 +150,13 @@ func (receiver AIResponseResult) TableName() string {
type VersionInfo struct {
gorm.Model
Version string `json:"version"`
Content string `json:"content"`
Icon string `json:"icon"`
Alipay string `json:"alipay"`
Wxpay string `json:"wxpay"`
Wxgzh string `json:"wxgzh"`
BuildTimeStamp int64 `json:"buildTimeStamp"`
OfficialStatement string `json:"officialStatement"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
Version string `json:"version"`
Content string `json:"content"`
Icon string `json:"icon"`
Alipay string `json:"alipay"`
Wxpay string `json:"wxpay"`
BuildTimeStamp int64 `json:"buildTimeStamp"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
}
func (receiver VersionInfo) TableName() string {
@@ -173,8 +170,6 @@ type StockInfoHK struct {
FullName string `json:"fullName"`
EName string `json:"eName"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
BKName string `json:"bk_name"`
BKCode string `json:"bk_code"`
}
func (receiver StockInfoHK) TableName() string {
@@ -190,8 +185,6 @@ type StockInfoUS struct {
Exchange string `json:"exchange"`
Type string `json:"type"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
BKName string `json:"bk_name"`
BKCode string `json:"bk_code"`
}
func (receiver StockInfoUS) TableName() string {
@@ -201,12 +194,6 @@ func (receiver StockInfoUS) TableName() string {
type Resp struct {
Code int `json:"code"`
Message string `json:"message"`
Error struct {
Code string `json:"code"`
Message string `json:"message"`
Param string `json:"param"`
Type string `json:"type"`
} `json:"error"`
}
type PromptTemplate struct {
@@ -232,16 +219,14 @@ type Prompt struct {
type Telegraph struct {
gorm.Model
Time string `json:"time"`
DataTime *time.Time `json:"dataTime" gorm:"index"`
Title string `json:"title" gorm:"index"`
Content string `json:"content" gorm:"index"`
Content string `json:"content"`
SubjectTags []string `json:"subjects" gorm:"-:all"`
StocksTags []string `json:"stocks" gorm:"-:all"`
IsRed bool `json:"isRed" gorm:"index"`
IsRed bool `json:"isRed"`
Url string `json:"url"`
Source string `json:"source" gorm:"index"`
Source string `json:"source"`
TelegraphTags []TelegraphTags `json:"tags" gorm:"-:migration;foreignKey:TelegraphId"`
SentimentResult string `json:"sentimentResult" gorm:"index"`
SentimentResult string `json:"sentimentResult" gorm:"-:all"`
}
type TelegraphTags struct {
gorm.Model
@@ -332,22 +317,6 @@ 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 {
@@ -393,329 +362,3 @@ type HotEvent struct {
StatusCount int `json:"status_count"`
Content string `json:"content"`
}
type GDP struct {
REPORTDATE string `json:"REPORT_DATE" md:"报告时间"`
TIME string `json:"TIME" md:"报告期"`
DOMESTICLPRODUCTBASE float64 `json:"DOMESTICL_PRODUCT_BASE" md:"国内生产总值(亿元)"`
SUMSAME float64 `json:"SUM_SAME" md:"国内生产总值同比增长(%)"`
FIRSTPRODUCTBASE float64 `json:"FIRST_PRODUCT_BASE" md:"第一产业(亿元)"`
FIRSTSAME int `json:"FIRST_SAME" md:"第一产业同比增长(%)"`
SECONDPRODUCTBASE float64 `json:"SECOND_PRODUCT_BASE" md:"第二产业(亿元)"`
SECONDSAME float64 `json:"SECOND_SAME" md:"第二产业同比增长(%)"`
THIRDPRODUCTBASE float64 `json:"THIRD_PRODUCT_BASE" md:"第三产业(亿元)"`
THIRDSAME float64 `json:"THIRD_SAME" md:"第三产业同比增长(%)"`
}
type CPI struct {
REPORTDATE string `json:"REPORT_DATE" md:"报告时间"`
TIME string `json:"TIME" md:"报告期"`
NATIONALBASE float64 `json:"NATIONAL_BASE" md:"全国当月"`
NATIONALSAME float64 `json:"NATIONAL_SAME" md:"全国当月同比增长(%)"`
NATIONALSEQUENTIAL float64 `json:"NATIONAL_SEQUENTIAL" md:"全国当月环比增长(%)"`
NATIONALACCUMULATE float64 `json:"NATIONAL_ACCUMULATE" md:"全国当月累计"`
CITYBASE float64 `json:"CITY_BASE" md:"城市当月"`
CITYSAME float64 `json:"CITY_SAME" md:"城市当月同比增长(%)"`
CITYSEQUENTIAL float64 `json:"CITY_SEQUENTIAL" md:"城市当月环比增长(%)"`
CITYACCUMULATE int `json:"CITY_ACCUMULATE" md:"城市当月累计"`
RURALBASE float64 `json:"RURAL_BASE" md:"农村当月"`
RURALSAME float64 `json:"RURAL_SAME" md:"农村当月同比增长(%)"`
RURALSEQUENTIAL int `json:"RURAL_SEQUENTIAL" md:"农村当月环比增长(%)"`
RURALACCUMULATE float64 `json:"RURAL_ACCUMULATE" md:"农村当月累计"`
}
type PPI struct {
REPORTDATE string `json:"REPORT_DATE" md:"报告时间"`
TIME string `json:"TIME" md:"报告期"`
BASE float64 `json:"BASE" md:"当月"`
BASESAME float64 `json:"BASE_SAME" md:"当月同比增长(%)"`
BASEACCUMULATE float64 `json:"BASE_ACCUMULATE" md:"累计"`
}
type PMI struct {
REPORTDATE string `md:"报告时间" json:"REPORT_DATE"`
TIME string `md:"报告期" json:"TIME"`
MAKEINDEX float64 `md:"制造业指数" json:"MAKE_INDEX"`
MAKESAME float64 `md:"制造业指数同比增长(%)" json:"MAKE_SAME"`
NMAKEINDEX float64 `md:"非制造业" json:"NMAKE_INDEX"`
NMAKESAME float64 `md:"非制造业同比增长(%)" json:"NMAKE_SAME"`
}
type DCResp struct {
Version string `json:"version"`
Success bool `json:"success"`
Message string `json:"message"`
Code int `json:"code"`
}
type GDPResult struct {
Pages int `json:"pages"`
Data []GDP `json:"data"`
Count int `json:"count"`
}
type CPIResult struct {
Pages int `json:"pages"`
Data []CPI `json:"data"`
Count int `json:"count"`
}
type PPIResult struct {
Pages int `json:"pages"`
Data []PPI `json:"data"`
Count int `json:"count"`
}
type PMIResult struct {
Pages int `json:"pages"`
Data []PMI `json:"data"`
Count int `json:"count"`
}
type GDPResp struct {
DCResp
GDPResult GDPResult `json:"result"`
}
type CPIResp struct {
DCResp
CPIResult CPIResult `json:"result"`
}
type PPIResp struct {
DCResp
PPIResult PPIResult `json:"result"`
}
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"
}

View File

@@ -1,221 +0,0 @@
package util
// @Author spark
// @Date 2025/7/15 14:08
// @Desc
//-----------------------------------------------------------------------------------
import (
"bytes"
"fmt"
"golang.org/x/net/html"
"strings"
)
// HTMLNode 表示HTML文档中的一个节点
type HTMLNode struct {
Type html.NodeType
Data string
Attr []html.Attribute
Children []*HTMLNode
}
// HTMLToMarkdown 将HTML转换为Markdown
func HTMLToMarkdown(htmlContent string) (string, error) {
doc, err := html.Parse(strings.NewReader(htmlContent))
if err != nil {
return "", err
}
root := parseHTMLNode(doc)
var buf bytes.Buffer
convertNode(&buf, root, 0)
return buf.String(), nil
}
// parseHTMLNode 递归解析HTML节点
func parseHTMLNode(n *html.Node) *HTMLNode {
node := &HTMLNode{
Type: n.Type,
Data: n.Data,
Attr: n.Attr,
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
node.Children = append(node.Children, parseHTMLNode(c))
}
return node
}
// convertNode 递归转换节点为Markdown
func convertNode(buf *bytes.Buffer, node *HTMLNode, depth int) {
switch node.Type {
case html.ElementNode:
convertElementNode(buf, node, depth)
case html.TextNode:
// 处理文本节点,去除多余的空白
text := strings.TrimSpace(node.Data)
if text != "" {
buf.WriteString(text)
}
}
// 递归处理子节点
for _, child := range node.Children {
convertNode(buf, child, depth+1)
}
// 处理需要在结束标签后添加内容的元素
switch node.Data {
case "p", "h1", "h2", "h3", "h4", "h5", "h6", "li":
buf.WriteString("\n\n")
case "blockquote":
buf.WriteString("\n")
}
}
// convertElementNode 转换元素节点为Markdown
func convertElementNode(buf *bytes.Buffer, node *HTMLNode, depth int) {
switch node.Data {
case "h1":
buf.WriteString("# ")
case "h2":
buf.WriteString("## ")
case "h3":
buf.WriteString("### ")
case "h4":
buf.WriteString("#### ")
case "h5":
buf.WriteString("##### ")
case "h6":
buf.WriteString("###### ")
case "p":
// 段落标签不需要特殊标记,直接处理内容
case "strong", "b":
buf.WriteString("**")
case "em", "i":
buf.WriteString("*")
case "u":
buf.WriteString("<u>")
case "s", "del":
buf.WriteString("~~")
case "a":
//href := getAttrValue(node.Attr, "href")
buf.WriteString("[")
case "img":
src := getAttrValue(node.Attr, "src")
alt := getAttrValue(node.Attr, "alt")
buf.WriteString(fmt.Sprintf("![%s](%s)", alt, src))
case "ul":
// 无序列表不需要特殊标记,子项会处理
case "ol":
// 有序列表不需要特殊标记,子项会处理
case "li":
if isParentListType(node, "ul") {
buf.WriteString("- ")
} else {
// 计算当前列表项的序号
index := 1
if parent := findParentList(node); parent != nil {
for i, sibling := range parent.Children {
if sibling == node {
index = i + 1
break
}
}
}
buf.WriteString(fmt.Sprintf("%d. ", index))
}
case "blockquote":
buf.WriteString("> ")
case "code":
if isParentPre(node) {
// 父节点是pre使用代码块
buf.WriteString("\n```\n")
} else {
// 行内代码
buf.WriteString("`")
}
case "pre":
// 前置代码块由子节点code处理
case "br":
buf.WriteString("\n")
case "hr":
buf.WriteString("\n---\n")
}
// 处理闭合标签
if needsClosingTag(node.Data) {
defer func() {
switch node.Data {
case "strong", "b":
buf.WriteString("**")
case "em", "i":
buf.WriteString("*")
case "u":
buf.WriteString("</u>")
case "s", "del":
buf.WriteString("~~")
case "a":
href := getAttrValue(node.Attr, "href")
buf.WriteString(fmt.Sprintf("](%s)", href))
case "code":
if isParentPre(node) {
buf.WriteString("\n```\n")
} else {
buf.WriteString("`")
}
}
}()
}
}
// getAttrValue 获取属性值
func getAttrValue(attrs []html.Attribute, key string) string {
for _, attr := range attrs {
if attr.Key == key {
return attr.Val
}
}
return ""
}
// isParentListType 检查父节点是否为指定类型的列表
func isParentListType(node *HTMLNode, listType string) bool {
parent := findParentList(node)
return parent != nil && parent.Data == listType
}
// findParentList 查找父列表节点
func findParentList(node *HTMLNode) *HTMLNode {
// 简化实现,实际应该递归查找父节点
if node.Type == html.ElementNode && (node.Data == "ul" || node.Data == "ol") {
return node
}
return nil
}
// isParentPre 检查父节点是否为pre
func isParentPre(node *HTMLNode) bool {
if len(node.Children) == 0 {
return false
}
for _, child := range node.Children {
if child.Type == html.ElementNode && child.Data == "pre" {
return true
}
}
return false
}
// needsClosingTag 判断元素是否需要闭合标签
func needsClosingTag(tag string) bool {
switch tag {
case "img", "br", "hr", "input", "meta", "link":
return false
default:
return true
}
}

View File

@@ -1,6 +0,0 @@
package util
// @Author spark
// @Date 2025/7/15 14:08
// @Desc
//-----------------------------------------------------------------------------------

View File

@@ -1,286 +0,0 @@
package util
import (
"fmt"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/strutil"
"reflect"
"strings"
)
// MarkdownTable 生成结构体或结构体切片的Markdown表格表示
func MarkdownTable(v interface{}) string {
value := reflect.ValueOf(v)
if value.Kind() == reflect.Ptr {
value = value.Elem()
}
// 处理单个结构体
if value.Kind() == reflect.Struct {
return markdownSingleStruct(value)
}
// 处理结构体切片/数组
if value.Kind() == reflect.Slice || value.Kind() == reflect.Array {
if value.Len() == 0 {
return "切片/数组为空"
}
return markdownStructSlice(value)
}
return "输入必须是结构体、结构体指针、结构体切片或数组"
}
func MarkdownTableWithTitle(title string, v interface{}) string {
value := reflect.ValueOf(v)
if value.Kind() == reflect.Ptr {
value = value.Elem()
}
// 处理单个结构体
if value.Kind() == reflect.Struct {
return markdownSingleStruct(value)
}
// 处理结构体切片/数组
if value.Kind() == reflect.Slice || value.Kind() == reflect.Array {
if value.Len() == 0 {
return "\n## " + title + "\n" + "无数据" + "\n"
}
return "\n## " + title + "\n" + markdownStructSlice(value) + "\n"
}
return "\n## " + title + "\n" + "无数据" + "\n"
}
// 处理单个结构体
func markdownSingleStruct(value reflect.Value) string {
t := value.Type()
var b strings.Builder
// 表头
b.WriteString("|")
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if shouldSkip(field) {
continue
}
b.WriteString(fmt.Sprintf(" %s |", getFieldName(field)))
}
b.WriteString("\n")
// 分隔线
b.WriteString("|")
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if shouldSkip(field) {
continue
}
b.WriteString(" --- |")
}
b.WriteString("\n")
// 数据行
b.WriteString("|")
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if shouldSkip(field) {
continue
}
fieldValue := value.Field(i)
b.WriteString(fmt.Sprintf(" %s |", formatValue(fieldValue)))
}
b.WriteString("\n")
return b.String()
}
// 处理结构体切片/数组
func markdownStructSlice(value reflect.Value) string {
if value.Len() == 0 {
return "切片/数组为空"
}
firstElem := value.Index(0)
if firstElem.Kind() == reflect.Ptr {
firstElem = firstElem.Elem()
}
if firstElem.Kind() != reflect.Struct {
return "切片/数组元素必须是结构体或结构体指针"
}
t := firstElem.Type()
var b strings.Builder
// 表头
b.WriteString("|")
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if shouldSkip(field) {
continue
}
b.WriteString(fmt.Sprintf(" %s |", getFieldName(field)))
}
b.WriteString("\n")
// 分隔线
b.WriteString("|")
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if shouldSkip(field) {
continue
}
b.WriteString(" --- |")
}
b.WriteString("\n")
// 多行数据
for i := 0; i < value.Len(); i++ {
elem := value.Index(i)
if elem.Kind() == reflect.Ptr {
elem = elem.Elem()
}
b.WriteString("|")
for j := 0; j < t.NumField(); j++ {
field := t.Field(j)
if shouldSkip(field) {
continue
}
fieldValue := elem.Field(j)
b.WriteString(fmt.Sprintf(" %s |", formatValue(fieldValue)))
}
b.WriteString("\n")
}
return b.String()
}
// 判断是否应该跳过该字段
func shouldSkip(field reflect.StructField) bool {
return field.Tag.Get("md") == "-"
}
// 获取字段的Markdown表头名称
func getFieldName(field reflect.StructField) string {
name := field.Tag.Get("md")
if name == "" || name == "-" {
return field.Name
}
return name
}
// 格式化字段值为字符串
func formatValue(value reflect.Value) string {
if !value.IsValid() {
return "n/a"
}
// 处理指针
if value.Kind() == reflect.Ptr {
if value.IsNil() {
return "nil"
}
return formatValue(value.Elem())
}
// 处理结构体
if value.Kind() == reflect.Struct {
var fields []string
for i := 0; i < value.NumField(); i++ {
field := value.Type().Field(i)
if shouldSkip(field) {
continue
}
fieldValue := value.Field(i)
fields = append(fields, fmt.Sprintf("%s: %s", getFieldName(field), formatValue(fieldValue)))
}
return "{" + strings.Join(fields, ", ") + "}"
}
// 处理切片/数组
if value.Kind() == reflect.Slice || value.Kind() == reflect.Array {
var items []string
for i := 0; i < value.Len(); i++ {
items = append(items, formatValue(value.Index(i)))
}
return "[" + strings.Join(items, ", ") + "]"
}
// 处理映射
if value.Kind() == reflect.Map {
var items []string
for _, key := range value.MapKeys() {
keyStr := formatValue(key)
valueStr := formatValue(value.MapIndex(key))
items = append(items, fmt.Sprintf("%s: %s", keyStr, valueStr))
}
return "{" + strings.Join(items, ", ") + "}"
}
// 基本类型
return fmt.Sprintf("%s", strutil.RemoveNonPrintable(convertor.ToString(value.Interface())))
}
// 示例结构体
type Address struct {
City string `md:"城市"`
Country string `md:"国家"`
}
type User struct {
Name string `md:"姓名"`
Age int `md:"年龄"`
Email string `md:"邮箱"`
Address Address `md:"地址"`
Phones []string `md:"电话"`
Active bool `md:"活跃状态"`
}
func main() {
// 示例使用:单个结构体
user := User{
Name: "张三",
Age: 30,
Email: "zhangsan@example.com",
Address: Address{
City: "北京",
Country: "中国",
},
Phones: []string{"13800138000", "13900139000"},
Active: true,
}
fmt.Println("单个结构体转换:")
fmt.Println(MarkdownTable(user))
fmt.Println()
// 示例使用:结构体切片
users := []User{
{
Name: "张三",
Age: 30,
Email: "zhangsan@example.com",
Address: Address{
City: "北京",
Country: "中国",
},
Phones: []string{"13800138000"},
Active: true,
},
{
Name: "李四",
Age: 25,
Email: "lisi@example.com",
Address: Address{
City: "上海",
Country: "中国",
},
Phones: []string{"13900139000", "13700137000"},
Active: false,
},
}
fmt.Println("结构体切片转换:")
fmt.Println(MarkdownTable(users))
}

View File

@@ -1,54 +0,0 @@
package util
import (
"fmt"
"testing"
)
func TestMd(t *testing.T) {
// 示例使用:单个结构体
user := User{
Name: "张三",
Age: 30,
Email: "zhangsan@example.com",
Address: Address{
City: "北京",
Country: "中国",
},
Phones: []string{"13800138000", "13900139000"},
Active: true,
}
fmt.Println("单个结构体转换:")
fmt.Println(MarkdownTable(user))
fmt.Println()
// 示例使用:结构体切片
users := []User{
{
Name: "张三",
Age: 30,
Email: "zhangsan@example.com",
Address: Address{
City: "北京",
Country: "中国",
},
Phones: []string{"13800138000"},
Active: true,
},
{
Name: "李四",
Age: 25,
Email: "lisi@example.com",
Address: Address{
City: "上海",
Country: "中国",
},
Phones: []string{"13900139000", "13700137000"},
Active: false,
},
}
fmt.Println("结构体切片转换:")
fmt.Println(MarkdownTable(users))
}

View File

@@ -1,10 +0,0 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
}

View File

@@ -1,45 +0,0 @@
/* 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']
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,6 @@
"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",
@@ -19,13 +18,11 @@
"html2canvas": "^1.4.1",
"lodash": "^4.17.21",
"md-editor-v3": "^5.2.3",
"tdesign-icons-vue-next": "^0.3.7",
"vue": "^3.5.17",
"vue": "^3.2.25",
"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",
@@ -34,14 +31,11 @@
"@vicons/ionicons5": "^0.13.0",
"@vicons/material": "^0.13.0",
"@vicons/tabler": "^0.13.0",
"@vitejs/plugin-vue": "^6.0.2",
"@vitejs/plugin-vue": "^5.2.1",
"html-docx-js-typescript": "^0.1.5",
"less": "^4.4.0",
"naive-ui": "^2.43.2",
"unplugin-auto-import": "^20.0.0",
"unplugin-vue-components": "^29.0.0",
"naive-ui": "^2.41.0",
"vfonts": "^0.0.3",
"vite": "7.2.4"
"vite": "^6.3.5"
},
"keywords": [
"AI赋能股票分析",

View File

@@ -1 +1 @@
f4fb0059ba6044c039be717fcc2e40bc
2d63c3a999d797889c01d6c96451b197

View File

@@ -6,12 +6,11 @@ import {
Quit,
WindowFullscreen,
WindowHide,
WindowUnfullscreen,
WindowSetTitle
WindowUnfullscreen
} from '../wailsjs/runtime'
import {h, onBeforeMount, onBeforeUnmount, onMounted, ref} from "vue";
import {RouterLink, useRouter} from 'vue-router'
import {createDiscreteApi,darkTheme,lightTheme , NIcon, NText,NButton,dateZhCN,zhCN} from 'naive-ui'
import {createDiscreteApi,darkTheme,lightTheme , NIcon, NText,dateZhCN,zhCN} from 'naive-ui'
import {
AlarmOutline,
AnalyticsOutline,
@@ -28,8 +27,8 @@ import {
StarOutline,
Wallet, WarningOutline,
} from '@vicons/ionicons5'
import {AnalyzeSentiment, GetConfig, GetGroupList,GetVersionInfo} from "../wailsjs/go/main/App";
import {Dragon, Fire, FirefoxBrowser, Gripfire, Robot} from "@vicons/fa";
import {AnalyzeSentiment, GetConfig, GetGroupList} from "../wailsjs/go/main/App";
import {Dragon, Fire, Gripfire} from "@vicons/fa";
import {ReportSearch} from "@vicons/tabler";
import {LocalFireDepartmentRound} from "@vicons/material";
import {BoxSearch20Regular, CommentNote20Filled} from "@vicons/fluent";
@@ -44,16 +43,14 @@ 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 content = ref('数据来源于网络,仅供参考;投资有风险,入市需谨慎\n\n未经授权,禁止商业目的!')
const isFullscreen = ref(false)
const activeKey = ref('stock')
const activeKey = ref('')
const containerRef = ref({})
const realtimeProfit = ref(0)
const telegraph = ref([])
const groupList = ref([])
const officialStatement= ref("")
const menuOptions = ref([
{
label: () =>
@@ -67,10 +64,7 @@ const menuOptions = ref([
groupId: 0,
},
params: {},
},
onClick: () => {
activeKey.value = 'stock'
},
}
},
{default: () => '股票自选',}
),
@@ -85,7 +79,6 @@ const menuOptions = ref([
href: '#',
type: 'info',
onClick: () => {
activeKey.value = 'stock'
//console.log("push",item)
router.push({
name: 'stock',
@@ -121,7 +114,6 @@ const menuOptions = ref([
params: {}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '市场快讯'})
},
},
@@ -143,7 +135,6 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '市场快讯'})
},
},
@@ -165,7 +156,6 @@ const menuOptions = ref([
},
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '全球股指'})
},
},
@@ -183,15 +173,14 @@ const menuOptions = ref([
to: {
name: 'market',
query: {
name: "重大指数",
name: "指标行情",
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '重大指数'})
EventsEmit("changeMarketTab", {ID: 0, name: '指标行情'})
},
},
{default: () => '重大指数',}
{default: () => '指标行情',}
),
key: 'market3',
icon: renderIcon(AnalyticsOutline),
@@ -209,7 +198,6 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '行业排名'})
},
},
@@ -231,7 +219,6 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '个股资金流向'})
},
},
@@ -253,7 +240,6 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '龙虎榜'})
},
},
@@ -275,7 +261,6 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '个股研报'})
},
},
@@ -297,7 +282,6 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '公司公告'})
},
},
@@ -319,7 +303,6 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '行业研究'})
},
},
@@ -341,7 +324,6 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '当前热门'})
},
},
@@ -363,7 +345,6 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '指标选股'})
},
},
@@ -372,28 +353,6 @@ 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),
},
]
},
{
@@ -403,13 +362,8 @@ const menuOptions = ref([
{
to: {
name: 'fund',
query: {
name: '基金自选',
},
},
onClick: () => {
activeKey.value = 'fund'
},
params: {},
}
},
{default: () => '基金自选',}
),
@@ -425,27 +379,6 @@ 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(
@@ -453,12 +386,7 @@ const menuOptions = ref([
{
to: {
name: 'settings',
query: {
name:"设置",
},
onClick: () => {
activeKey.value = 'settings'
},
params: {}
}
},
{default: () => '设置'}
@@ -473,13 +401,8 @@ const menuOptions = ref([
{
to: {
name: 'about',
query: {
name:"关于",
}
},
onClick: () => {
activeKey.value = 'about'
},
params: {}
}
},
{default: () => '关于'}
),
@@ -487,7 +410,6 @@ const menuOptions = ref([
icon: renderIcon(LogoGithub),
},
{
show:false,
label: () => h("a", {
href: '#',
onClick: toggleFullscreen,
@@ -500,7 +422,7 @@ const menuOptions = ref([
label: () => h("a", {
href: '#',
onClick: WindowHide,
title: '隐藏到托盘区 Ctrl+Z',
title: '隐藏到托盘区 Ctrl+H',
}, {default: () => '隐藏到托盘区'}),
key: 'hide',
icon: renderIcon(ReorderTwoOutline),
@@ -529,7 +451,6 @@ function renderIcon(icon) {
}
function toggleFullscreen(e) {
activeKey.value = 'full'
//console.log(e)
if (isFullscreen.value) {
WindowUnfullscreen()
@@ -597,13 +518,6 @@ window.onerror = function (msg, source, lineno, colno, error) {
};
onBeforeMount(() => {
GetVersionInfo().then(result => {
if(result.officialStatement){
content.value = result.officialStatement+"\n\n"+content.value
officialStatement.value = result.officialStatement
}
})
GetGroupList().then(result => {
groupList.value = result
menuOptions.value.map((item) => {
@@ -651,15 +565,11 @@ 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) {
@@ -671,14 +581,12 @@ onBeforeMount(() => {
})
onMounted(() => {
WindowSetTitle("go-stockAI赋能股票分析✨ "+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 ,
@@ -692,24 +600,16 @@ onMounted(() => {
//type:"error",
// avatar: () => h(NIcon,{component:Notifications,color:"red"}),
title: data.time,
content: () => h('div',{type:"error",style:{
"text-align":"left",
"font-size":"14px",
"color":"#f67979"
}}, { default: () => data.content }),
content: () => h(NText,{type:"error"}, { default: () => data.content }),
meta: () => h(NText,{type:"warning"}, { default: () => data.source}),
duration:1000*40,
})
}else{
notification.create({
notification.create({
//type:"info",
//avatar: () => h(NIcon,{component:Notifications}),
title: data.time,
content: () => h('div',{type:"info",style:{
"text-align":"left",
"font-size":"14px",
"color": data.source==="go-stock"?"#F98C24":"#549EC8"
}}, { default: () => data.content }),
content: () => h(NText,{type:"info"}, { default: () => data.content }),
meta: () => h(NText,{type:"warning"}, { default: () => data.source}),
duration:1000*30 ,
})
@@ -725,7 +625,7 @@ onMounted(() => {
<n-modal-provider>
<n-dialog-provider>
<n-watermark
:content="''"
:content="content"
cross
selectable
:font-size="16"
@@ -756,7 +656,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:no-drag">
<n-card size="small" style="--wails-draggable:drag">
<n-menu style="font-size: 18px;"
v-model:value="activeKey"
mode="horizontal"

View File

@@ -1,317 +0,0 @@
<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}}&nbsp;{{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>

View File

@@ -1,133 +0,0 @@
<template>
<div class="embed-container">
<h3 v-if="title">{{ title }}</h3>
<div class="iframe-wrapper">
<iframe
:src="url"
:title="iframeTitle"
frameborder="0"
scrolling="auto"
class="embedded-iframe"
@load="onLoad"
@error="onError"
:style="iframeStyle"
></iframe>
</div>
<div v-if="loading" class="loading-indicator">
<div class="spinner"></div>
<p>加载中...</p>
</div>
<p v-if="error" class="error-message">{{ error }}</p>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
const props = defineProps({
url: {
type: String,
required: true
},
title: {
type: String,
default: ''
},
iframeTitle: {
type: String,
default: '外部内容'
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '100%'
}
})
const loading = ref(true)
const error = ref(null)
const onLoad = () => {
loading.value = false
error.value = null
}
const onError = (event) => {
loading.value = false
error.value = `加载失败: ${event.message || '无法加载该 URL'}`
}
// 监听 URL 变化,重新加载
watch(() => props.url, () => {
loading.value = true
error.value = null
})
// 设置 iframe 样式
const iframeStyle = {
width: props.width,
height: props.height
}
</script>
<style scoped>
.embed-container {
margin: 1rem 0;
border: 0 solid #e5e7eb;
border-radius: 0.5rem;
overflow: hidden;
}
.iframe-wrapper {
position: relative;
width: 100%;
}
.embedded-iframe {
display: block;
width: 100%;
min-height: 400px;
transition: opacity 0.3s ease;
}
.loading-indicator {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
}
.spinner {
width: 2rem;
height: 2rem;
border: 3px solid #f3f4f6;
border-radius: 50%;
border-top-color: #3b82f6;
animation: spin 1s linear infinite;
margin-bottom: 0.5rem;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.error-message {
color: #ef4444;
padding: 1rem;
margin: 0;
background-color: #fee2e2;
text-align: center;
}
</style>

View File

@@ -1,7 +1,6 @@
<script setup lang="ts">
import {onBeforeMount, onUnmounted, ref} from 'vue'
import {HotTopic, OpenURL} from "../../wailsjs/go/main/App";
import {Environment} from "../../wailsjs/runtime";
import {HotTopic} from "../../wailsjs/go/main/App";
const list = ref([])
const task =ref()
@@ -19,20 +18,11 @@ function openCenteredWindow(url, width, height) {
const left = (window.screen.width - width) / 2;
const top = (window.screen.height - height) / 2;
Environment().then(env => {
switch (env.platform) {
case 'windows':
window.open(
url,
'centeredWindow',
`width=${width},height=${height},left=${left},top=${top}`
)
break
default:
OpenURL(url)
break
}
})
return window.open(
url,
'centeredWindow',
`width=${width},height=${height},left=${left},top=${top}`
);
}
function showPage(htid) {
openCenteredWindow(`https://gubatopic.eastmoney.com/topic_v3.html?htid=${htid}`, 1000, 600)

View File

@@ -378,7 +378,7 @@ function calculateMA(dayCount,values) {
</script>
<template>
<div ref="kLineChartRef" style="width: 100%;height: auto;--wails-draggable:no-drag" :style="{height:chartHeight+'px'}" ></div>
<div ref="kLineChartRef" style="width: 100%;height: auto;" :style="{height:chartHeight+'px'}"></div>
</template>
<style scoped>

View File

@@ -1,260 +1,99 @@
<script setup lang="ts">
import {h, onBeforeMount, onMounted, onUnmounted, ref} from 'vue'
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";
import {SearchStock} from "../../wailsjs/go/main/App";
import {useMessage, NText, NTag} from 'naive-ui'
const message = useMessage()
const search = ref('')
const search = ref('科技股;换手率连续3日大于2')
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) {
message.warning('请输入选股指标或者要求')
return
}
const loading = message.loading("正在获取选股数据...", {duration: 0});
SearchStock(search.value).then(res => {
loading.destroy()
// console.log(res)
if (res.code == 100) {
traceInfo.value = res.data.traceInfo.showText
// message.success(res.msg)
columns.value = res.data.result.columns.filter(item => !item.hiddenNeed && (item.title != "市场码" && item.title != "市场简称")).map(item => {
if (item.children) {
//console.log(res)
if(res.code==100){
message.success(res.msg)
columns.value=res.data.result.columns.filter(item=>!item.hiddenNeed&&(item.title!="市场码"&&item.title!="市场简称")).map(item=>{
if(item.children){
return {
title: item.title + (item.unit ? '[' + item.unit + ']' : ''),
key: item.key,
title:item.title+(item.unit?'['+item.unit+']':''),
key:item.key,
resizable: true,
minWidth: 200,
minWidth:200,
ellipsis: {
tooltip: true
},
children: item.children.filter(item => !item.hiddenNeed).map(item => {
children:item.children.filter(item=>!item.hiddenNeed).map(item=>{
return {
title: item.dateMsg,
key: item.key,
minWidth: 100,
title:item.dateMsg,
key:item.key,
minWidth:100,
resizable: true,
ellipsis: {
tooltip: true
},
sorter: (row1, row2) => {
if (isNumeric(row1[item.key]) && isNumeric(row2[item.key])) {
return row1[item.key] - row2[item.key];
} else {
return 'default'
}
},
}
}
})
}
} else {
}else{
return {
title: item.title + (item.unit ? '[' + item.unit + ']' : ''),
key: item.key,
title:item.title+(item.unit?'['+item.unit+']':''),
key:item.key,
resizable: true,
minWidth: 120,
minWidth:100,
ellipsis: {
tooltip: true
},
sorter: (row1, row2) => {
if (isNumeric(row1[item.key]) && isNumeric(row2[item.key])) {
return row1[item.key] - row2[item.key];
} else {
return 'default'
}
},
}
}
}
})
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 {
if(res.msg){
message.error(res.msg)
}
if(res.message){
message.error(res.message)
}
}
dataList.value=res.data.result.dataList
}else {
message.error(res.msg)
}
}).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);
}
onBeforeMount(() => {
GetHotStrategy().then(res => {
console.log(res)
if (res.code == 1) {
hotStrategy.value = res.data
search.value = hotStrategy.value[0].question
Search()
}
}).catch(err => {
message.error(err)
})
Search()
})
function DoSearch(question) {
search.value = question
Search()
}
function openCenteredWindow(url, width, height) {
const left = (window.screen.width - width) / 2;
const top = (window.screen.height - height) / 2;
Environment().then(env => {
switch (env.platform) {
case 'windows':
window.open(
url,
'centeredWindow',
`width=${width},height=${height},left=${left},top=${top},location=no,menubar=no,toolbar=no,display=standalone`
)
break
default:
OpenURL(url)
}
})
}
</script>
<template>
<n-grid :cols="24" style="max-height: calc(100vh - 165px)">
<n-gi :span="4">
<n-list bordered style="text-align: left;" hoverable clickable>
<n-scrollbar style="max-height: calc(100vh - 170px);">
<n-list-item v-for="item in hotStrategy" :key="item.rank" @click="DoSearch(item.question)">
<n-ellipsis line-clamp="1" :tooltip="true">
<n-tag size="small" :bordered="false" type="info">#{{ item.rank }}</n-tag>
<n-text type="warning">{{ item.question }}</n-text>
<template #tooltip>
<div style="text-align: center;max-width: 180px">
<n-text type="warning">{{ item.question }}</n-text>
</div>
</template>
</n-ellipsis>
</n-list-item>
</n-scrollbar>
</n-list>
<!-- <n-virtual-list :items="hotStrategy" :item-size="hotStrategy.length">-->
<!-- <template #default="{ item, index }">-->
<!-- <n-card :title="''" size="small">-->
<!-- <template #header-extra>-->
<!-- {{item.rank}}-->
<!-- </template>-->
<!-- <n-ellipsis expand-trigger="click" line-clamp="3" :tooltip="false" >-->
<!-- <n-text type="warning">{{item.question }}</n-text>-->
<!-- </n-ellipsis>-->
<!-- </n-card>-->
<!-- </template>-->
<!-- </n-virtual-list>-->
</n-gi>
<n-gi :span="20">
<n-flex style="--wails-draggable:no-drag">
<n-input-group style="text-align: left">
<n-input :rows="1" clearable v-model:value="search" placeholder="请输入选股指标或者要求"/>
<n-button type="primary" @click="Search">搜索A股</n-button>
</n-input-group>
</n-flex>
<n-flex justify="start" v-if="traceInfo" style="margin: 5px 0;--wails-draggable:no-drag">
<n-ellipsis line-clamp="1" :tooltip="true">
<n-text type="info" :bordered="false">选股条件</n-text>
<n-text type="warning" :bordered="true">{{ traceInfo }}</n-text>
<template #tooltip>
<div style="text-align: center;max-width: 580px">
<n-text type="warning">{{ traceInfo }}</n-text>
</div>
</template>
</n-ellipsis>
<!-- <n-button type="primary" size="small">保存策略</n-button>-->
</n-flex>
<n-data-table
:striped="true"
:max-height="'calc(100vh - 150px)'"
size="medium"
:columns="columns"
:data="dataList"
:pagination="{pageSize: 10}"
:scroll-x="tableScrollX"
:render-cell="(value, rowData, column) => {
<n-flex>
<n-input-group>
<n-input v-model:value="search" placeholder="请输入选股指标或者要求" />
<n-button type="success" @click="Search">搜索A股</n-button>
</n-input-group>
</n-flex>
<!-- <n-table striped size="small">-->
<!-- <n-thead>-->
<!-- <n-tr>-->
<!-- <n-th v-for="item in columns">{{item.title}}</n-th>-->
<!-- </n-tr>-->
<!-- </n-thead>-->
<!-- <n-tbody>-->
<!-- <n-tr v-for="(item,index) in dataList">-->
<!-- <n-td v-for="d in columns">{{item[d.key]}}</n-td>-->
<!-- </n-tr>-->
<!-- </n-tbody>-->
<!-- </n-table>-->
<n-data-table
:max-height="'calc(100vh - 285px)'"
size="small"
:columns="columns"
:data="dataList"
:pagination="false"
:scroll-x="1800"
:render-cell="(value, rowData, column) => {
if(column.key=='SECURITY_CODE'||column.key=='SERIAL'){
return h(NText, { type: 'info',border: false }, { default: () => `${value}` })
@@ -273,24 +112,13 @@ function openCenteredWindow(url, width, height) {
return h(NText, { type: type }, { default: () => `${value}` })
}else{
if(column.key=='SECURITY_SHORT_NAME'){
return h(NButton, { type: 'info',bordered: false ,size:'small',onClick:()=>{
//https://quote.eastmoney.com/sz300558.html#fullScreenChart
openCenteredWindow(`https://quote.eastmoney.com/${rowData.MARKET_SHORT_NAME}${rowData.SECURITY_CODE}.html#fullScreenChart`,1240,700)
}}, { default: () => `${value}` })
return h(NTag, { type: 'info',bordered: false }, { default: () => `${value}` })
}else{
return h(NText, { type: 'info' }, { default: () => `${value}` })
}
}
}"
/>
<div style="margin-top: -25px">共找到
<n-tag type="info" :bordered="false">{{ dataList.length }}</n-tag>
只股
</div>
</n-gi>
</n-grid>
/>
</template>
<style scoped>

View File

@@ -3,22 +3,15 @@
// preview.css相比style.css少了编辑器那部分样式
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,NText} from "naive-ui";
import { addMonths, format ,parse} from 'date-fns';
import { zhCN } from 'date-fns/locale';
import {CheckUpdate, GetVersionInfo} from "../../wailsjs/go/main/App";
import {EventsOff, EventsOn} from "../../wailsjs/runtime";
import {NAvatar, NButton, useNotification} from "naive-ui";
const updateLog = ref('');
const versionInfo = ref('');
const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/master/build/appicon.png');
const alipay =ref('https://github.com/ArvinLovegood/go-stock/raw/master/build/screenshot/alipay.jpg')
const wxpay =ref('https://github.com/ArvinLovegood/go-stock/raw/master/build/screenshot/wxpay.jpg')
const wxgzh =ref('https://github.com/ArvinLovegood/go-stock/raw/dev/build/screenshot/%E6%89%AB%E7%A0%81_%E6%90%9C%E7%B4%A2%E8%81%94%E5%90%88%E4%BC%A0%E6%92%AD%E6%A0%B7%E5%BC%8F-%E7%99%BD%E8%89%B2%E7%89%88.png')
const notify = useNotification()
const vipLevel=ref("");
const vipStartTime=ref("");
const vipEndTime=ref("");
const expired=ref(false)
onMounted(() => {
document.title = '关于软件';
@@ -28,25 +21,7 @@ onMounted(() => {
icon.value = res.icon;
alipay.value=res.alipay;
wxpay.value=res.wxpay;
wxgzh.value=res.wxgzh;
GetSponsorInfo().then((res) => {
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;
}
}
})
});
})
onBeforeUnmount(() => {
notify.destroyAll()
@@ -95,16 +70,7 @@ EventsOn("updateVersion",async (msg) => {
type: 'primary',
size: 'small',
onClick: () => {
Environment().then(env => {
switch (env.platform) {
case 'windows':
window.open(msg.html_url)
break
default :
OpenURL(msg.html_url)
break
}
})
window.open(msg.html_url)
}
}, { default: () => '查看' })
}
@@ -114,22 +80,21 @@ EventsOn("updateVersion",async (msg) => {
</script>
<template>
<n-space vertical size="large" style="--wails-draggable:no-drag">
<n-space vertical size="large">
<!-- 软件描述 -->
<n-card size="large">
<n-divider title-placement="center">关于软件</n-divider>
<n-space vertical >
<n-image width="100" :src="icon" />
<h1>
<n-badge v-if="!vipLevel" :value="versionInfo" :offset="[50,10]" type="success">
<n-badge :value="versionInfo" :offset="[50,10]" type="success">
<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="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="expired?'error':'warning'" v-if="vipLevel" >vip到期时间{{vipEndTime}}</n-gradient-text>
<n-button size="tiny" @click="CheckUpdate(1)" type="info" tertiary >检查更新</n-button>
<n-button size="tiny" @click="CheckUpdate" type="info" tertiary >检查更新</n-button>
<div style="justify-self: center;text-align: left" >
<p>自选股行情实时监控基于Wails和NaiveUI构建的AI赋能股票分析工具</p>
<p>目前已支持A股港股美股未来计划加入基金ETF等支持</p>
@@ -148,39 +113,14 @@ EventsOn("updateVersion",async (msg) => {
<p>QQ交流群<a href="http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0YQ8qD3exahsD4YLNhzQTWe5ssstWC89&authKey=usOMMRFtIQDC%2FYcatHYapcxQbJ7PwXPHK9OypTXWzNjAq%2FRVvQu9bj2lRgb%2BSZ3p&noverify=0&group_code=491605333" target="_blank">491605333</a></p>
</div>
</n-space>
<n-divider title-placement="center">支持💕开源</n-divider>
<n-flex justify="center">
<n-table size="small" style="width: 820px">
<n-thead>
<n-tr>
<n-th>赞助计划</n-th>
<n-th>赞助等级</n-th>
<n-th>权益说明</n-th>
</n-tr>
</n-thead>
<n-tbody>
<n-tr>
<n-td>每月 0 RMB</n-td><n-td>vip0</n-td><n-td>🌟 全部功能,软件自动更新(从GitHub下载),自行解决github平台网络问题</n-td>
</n-tr>
<n-tr>
<n-td>赞助 18.8 RMB/<br>赞助 120 RMB/</n-td><n-td>vip1</n-td><n-td>💕 全部功能,软件自动更新(从CDN下载),更新快速便捷AI配置指导提示词参考等</n-td>
</n-tr>
<n-tr>
<n-td>赞助 28.8 RMB/<br>赞助 240 RMB/</n-td><n-td>vip2</n-td><n-td>💕 vip1全部功能,赠送硅基流动AI分析服务💕</n-td>
</n-tr>
<n-tr>
<n-td>每月赞助 X RMB</n-td><n-td>vipX</n-td><n-td>🧩 更多计划视go-stock开源项目发展情况而定...(承接GitHub项目README广告推广💖)</n-td>
</n-tr>
</n-tbody>
</n-table>
</n-flex>
<n-divider title-placement="center">关于作者</n-divider>
<n-space vertical>
<!-- <h1>关于作者</h1>-->
<n-avatar width="100" src="https://avatars.githubusercontent.com/u/7401917?v=4" />
<h2><a href="https://github.com/ArvinLovegood" target="_blank">@ArvinLovegood</a></h2>
<p>一个热爱编程的小白欢迎关注我的Github/微信公众号</p>
<n-image width="300" :src="wxgzh" />
<p>一个热爱编程的小白欢迎关注我的Github</p>
<n-image width="300" src="https://go-stock.sparkmemory.top/assets/%E6%89%AB%E7%A0%81_%E6%90%9C%E7%B4%A2%E8%81%94%E5%90%88%E4%BC%A0%E6%92%AD%E6%A0%B7%E5%BC%8F-%E7%99%BD%E8%89%B2%E7%89%88-DEJtWc_y.png" />
<p>开源不易如果觉得好用可以请作者喝杯咖啡</p>
<n-flex justify="center">
<n-image width="200" :src="alipay" />
@@ -195,7 +135,6 @@ EventsOn("updateVersion",async (msg) => {
</p>
<p>
感谢以下开发者
<a href="https://github.com/GiCo001" target="_blank">@Gico</a><n-divider vertical />
<a href="https://github.com/CodeNoobLH" target="_blank">浓睡不消残酒</a><n-divider vertical />
<a href="https://github.com/gnim2600" target="_blank">@gnim2600</a><n-divider vertical />
<a href="https://github.com/XXXiaohuayanGGG" target="_blank">@XXXiaohuayanGGG</a><n-divider vertical />

View File

@@ -1,365 +0,0 @@
<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>

View File

@@ -1,338 +0,0 @@
<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>

View File

@@ -7,7 +7,7 @@ import {
GetConfig,
GetFollowedFund,
GetfundList,
GetVersionInfo, OpenURL,
GetVersionInfo,
UnFollowFund
} from "../../wailsjs/go/main/App";
import vueDanmaku from 'vue3-danmaku'
@@ -147,19 +147,8 @@ function formatterTitle(title){
function search(code,name){
setTimeout(() => {
//window.open("https://fund.eastmoney.com/"+code+".html","_blank","noreferrer,width=1000,top=100,left=100,status=no,toolbar=no,location=no,scrollbars=no")
window.open("https://fund.eastmoney.com/"+code+".html","_blank","noreferrer,width=1000,top=100,left=100,status=no,toolbar=no,location=no,scrollbars=no")
//window.open("https://finance.sina.com.cn/fund/quotes/"+code+"/bc.shtml","_blank","width=1000,height=800,top=100,left=100,toolbar=no,location=no")
Environment().then(env => {
switch (env.platform) {
case 'windows':
window.open("https://fund.eastmoney.com/"+code+".html","_blank","noreferrer,width=1000,top=100,left=100,status=no,toolbar=no,location=no,scrollbars=no")
break
default :
OpenURL("https://fund.eastmoney.com/"+code+".html")
}
})
}, 500)
}

View File

@@ -1,6 +1,5 @@
<script setup>
import * as echarts from "echarts";
import {computed, h, onBeforeMount, onBeforeUnmount, onMounted,onUnmounted, ref} from 'vue'
import {computed, h, onBeforeMount, onBeforeUnmount, ref} from 'vue'
import {
GetAIResponseResult,
GetConfig,
@@ -12,8 +11,7 @@ import {
SaveAIResponseResult,
SaveAsMarkdown,
ShareAnalysis,
SummaryStockNews,
GetAiConfigs,
SummaryStockNews
} from "../../wailsjs/go/main/App";
import {EventsOff, EventsOn} from "../../wailsjs/runtime";
import NewsList from "./newsList.vue";
@@ -34,7 +32,6 @@ import HotTopics from "./HotTopics.vue";
import InvestCalendarTimeLine from "./InvestCalendarTimeLine.vue";
import ClsCalendarTimeLine from "./ClsCalendarTimeLine.vue";
import SelectStock from "./SelectStock.vue";
import Stockhotmap from "./stockhotmap.vue";
const route = useRoute()
const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/master/build/appicon.png');
@@ -45,7 +42,6 @@ const panelHeight = ref(window.innerHeight - 240)
const telegraphList = ref([])
const sinaNewsList = ref([])
const foreignNewsList = ref([])
const common = ref([])
const america = ref([])
const europe = ref([])
@@ -55,7 +51,6 @@ 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'
})
@@ -64,10 +59,8 @@ const aiSummaryTime = ref("")
const modelName = ref("")
const chatId = ref("")
const question = ref(``)
const aiConfigId = ref(null)
const sysPromptId = ref(null)
const sysPromptId = ref(0)
const loading = ref(true)
const aiConfigs = ref([])
const sysPromptOptions = ref([])
const userPromptOptions = ref([])
const promptTemplates = ref([])
@@ -77,9 +70,6 @@ const nowTab = ref("市场快讯")
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) => {
@@ -92,13 +82,14 @@ function getIndex() {
})
}
onBeforeMount(() => {
nowTab.value = route.query.name
stockCode.value = route.query.stockCode
GetConfig().then(result => {
summaryBTN.value = result.openAiEnable
darkTheme.value = result.darkTheme
httpProxyEnabled.value = result.httpProxyEnabled
})
GetPromptTemplates("", "").then(res => {
promptTemplates.value = res
@@ -106,19 +97,12 @@ 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(() => {
@@ -128,12 +112,7 @@ onBeforeMount(() => {
indexIndustryRank.value = setInterval(() => {
industryRank()
}, 1000 * 10)
})
onMounted(() => {
})
onBeforeUnmount(() => {
EventsOff("changeMarketTab")
@@ -144,38 +123,22 @@ onBeforeUnmount(() => {
clearInterval(indexIndustryRank.value)
})
onUnmounted(() => {
});
EventsOn("changeMarketTab", async (msg) => {
//message.info(msg.name)
console.log(msg.name)
updateTab(msg.name)
})
EventsOn("newTelegraph", (data) => {
if (data!=null) {
for (let i = 0; i < data.length; i++) {
telegraphList.value.pop()
}
telegraphList.value.unshift(...data)
for (let i = 0; i < data.length; i++) {
telegraphList.value.pop()
}
telegraphList.value.unshift(...data)
})
EventsOn("newSinaNews", (data) => {
if (data!=null) {
for (let i = 0; i < data.length; i++) {
sinaNewsList.value.pop()
}
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)
}
})
//获取页面高度
@@ -223,14 +186,13 @@ function reAiSummary() {
aiSummary.value = ""
summaryModal.value = true
loading.value = true
SummaryStockNews(question.value,aiConfigId.value, sysPromptId.value,enableTools.value)
SummaryStockNews(question.value, sysPromptId.value)
}
function getAiSummary() {
summaryModal.value = true
loading.value = true
GetAIResponseResult("市场资讯").then(result => {
loading.value = false
if (result.content) {
aiSummary.value = result.content
question.value = result.question
@@ -249,7 +211,7 @@ function getAiSummary() {
aiSummaryTime.value = ""
aiSummary.value = ""
modelName.value = ""
//SummaryStockNews(question.value, sysPromptId.value,enableTools.value)
SummaryStockNews(question.value, sysPromptId.value)
}
})
}
@@ -263,7 +225,7 @@ EventsOn("summaryStockNews", async (msg) => {
loading.value = false
////console.log(msg)
if (msg === "DONE") {
await SaveAIResponseResult("市场资讯", "市场资讯", aiSummary.value, chatId.value, question.value,aiConfigId.value)
SaveAIResponseResult("市场资讯", "市场资讯", aiSummary.value, chatId.value, question.value)
message.info("AI分析完成")
message.destroyAll()
@@ -337,37 +299,22 @@ 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:no-drag">
<n-tabs type="line" animated @update-value="updateTab" :value="nowTab">
<n-tab-pane name="市场快讯" tab="市场快讯">
<n-grid :cols="1" :y-gap="0">
<n-grid :cols="2" :y-gap="0">
<n-gi>
<AnalyzeMartket :dark-theme="darkTheme" :chart-height="300" :kDays="1" :name="'最近24小时热词'" />
<news-list :newsList="telegraphList" :header-title="'财联社电报'" @update:message="ReFlesh"></news-list>
</n-gi>
<n-gi>
<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>
<news-list :newsList="sinaNewsList" :header-title="'新浪财经'" @update:message="ReFlesh"></news-list>
</n-gi>
</n-grid>
</n-tab-pane>
<n-tab-pane name="全球股指" tab="全球股指">
<n-tabs type="segment" animated>
@@ -436,34 +383,10 @@ function ReFlesh(source) {
</n-tab-pane>
</n-tabs>
</n-tab-pane>
<n-tab-pane name="重大指数" tab="重大指数">
<n-tab-pane name="指标行情" tab="指标行情">
<n-tabs type="segment" animated>
<n-tab-pane name="恒生科技指数" tab="恒生科技指数">
<k-line-chart code="hkHSTECH" :chart-height="panelHeight" name="恒生科技指数" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="科创50" tab="科创50" >
<k-line-chart code="sh000688" :chart-height="panelHeight" name="科创50" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="科创芯片" tab="科创芯片" >
<k-line-chart code="sh000685" :chart-height="panelHeight" name="科创芯片" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="证券龙头" tab="证券龙头" >
<k-line-chart code="sz399437" :chart-height="panelHeight" name="证券龙头" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="高端装备" tab="高端装备" >
<k-line-chart code="sz399437" :chart-height="panelHeight" name="高端装备" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="中证银行" tab="中证银行">
<k-line-chart code="sz399986" :chart-height="panelHeight" name="中证银行" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="上证医药" tab="上证医药">
<k-line-chart code="sh000037" :chart-height="panelHeight" name="上证医药" :k-days="20"
<n-tab-pane name="科创50" tab="科创50">
<k-line-chart code="sh000688" :chart-height="panelHeight" name="科创50" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="沪深300" tab="沪深300">
@@ -675,9 +598,6 @@ function ReFlesh(source) {
<n-tab-pane name="指标选股" tab="指标选股">
<select-stock />
</n-tab-pane>
<n-tab-pane name="名站优选" tab="名站优选">
<Stockhotmap />
</n-tab-pane>
</n-tabs>
</n-card>
<n-modal transform-origin="center" v-model:show="summaryModal" preset="card" style="width: 800px;"
@@ -695,23 +615,10 @@ function ReFlesh(source) {
</n-flex>
</template>
<template #action>
<n-flex justify="left" style="margin-bottom: 10px">
<n-switch v-model:value="enableTools" :round="false">
<template #checked>
启用AI函数工具调用
</template>
<template #unchecked>
不启用AI函数工具调用
</template>
</n-switch>
<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: 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"
<n-select style="width: 49%" v-model:value="sysPromptId" label-field="name" value-field="ID"
:options="sysPromptOptions" placeholder="请选择系统提示词"/>
<n-select style="width: 32%" v-model:value="question" label-field="name" value-field="content"
<n-select style="width: 49%" v-model:value="question" label-field="name" value-field="content"
:options="userPromptOptions" placeholder="请选择用户提示词"/>
</n-flex>
<n-flex justify="right">
@@ -744,4 +651,5 @@ function ReFlesh(source) {
</template>
<style scoped>
</style>

View File

@@ -29,19 +29,11 @@ const updateMessage = () => {
</n-flex>
</template>
<n-list-item v-for="item in newsList">
<n-space justify="start" >
<n-space justify="start">
<n-text justify="start" :bordered="false" :type="item.isRed?'error':'info'">
<n-tag size="small" :type="item.isRed?'error':'warning'" :bordered="false"> {{ item.time }}</n-tag>
<n-gradient-text :size="14" :type="'warning'" :bordered="false">{{ item.title }}</n-gradient-text> <n-text :type="item.isRed?'error':'info'">{{ item.content }}</n-text>
{{ item.content }}
</n-text>
<!-- <n-collapse v-if="item.title">-->
<!-- <n-collapse-item :title="item.title" :name="item.title">-->
<!-- <n-text 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>-->
<!-- </n-collapse-item>-->
<!-- </n-collapse>-->
</n-space>
<n-space v-if="item.subjects" style="margin-top: 2px">
<n-tag :bordered="false" type="success" size="small" v-for="sub in item.subjects">
@@ -57,9 +49,6 @@ 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>

View File

@@ -1,204 +1,165 @@
<script setup>
import {h, onBeforeUnmount, onMounted, ref} from "vue";
import {computed, onBeforeUnmount, onMounted, ref} from "vue";
import {
AddPrompt, DelPrompt,
ExportConfig,
GetConfig,
GetPromptTemplates,
SendDingDingMessageByType,
UpdateConfig, CheckSponsorCode
UpdateConfig
} from "../../wailsjs/go/main/App";
import {NTag, useMessage} from "naive-ui";
import {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,
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: '',
updateBasicInfoOnStart:false,
refreshInterval:1,
openAI:{
enable:false,
baseUrl: 'https://api.deepseek.com',
apiKey: '',
modelName: 'deepseek-chat',
model: 'deepseek-chat',
temperature: 0.1,
maxTokens: 1024,
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 => {
prompt:"",
timeout: 5,
questionTemplate: "{{stockName}}分析和总结",
crawlTimeOut:30,
kDays:30,
},
enableDanmu:false,
browserPath: '',
enableNews:false,
darkTheme:true,
enableFund:false,
enablePushNews:false,
})
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,
aiConfigs: res.aiConfigs || [],
prompt: res.prompt,
questionTemplate: res.questionTemplate ? res.questionTemplate : '{{stockName}}分析和总结',
crawlTimeOut: res.crawlTimeOut,
kDays: res.kDays,
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,
}
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 => {
promptTemplates.value = res
GetPromptTemplates("","").then(res=>{
//console.log(res)
promptTemplates.value=res
})
})
onBeforeUnmount(() => {
message.destroyAll()
})
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
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
})
if (config.sponsorCode) {
CheckSponsorCode(config.sponsorCode).then(res => {
if (res.code) {
UpdateConfig(config).then(res => {
message.success(res)
EventsEmit("updateSettings", config);
})
} else {
message.error(res.msg)
}
})
} else {
UpdateConfig(config).then(res => {
message.success(res)
EventsEmit("updateSettings", config);
})
}
//console.log("Settings",config)
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';
@@ -207,25 +168,30 @@ 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,
aiConfigs: config.aiConfigs || [],
prompt: config.prompt,
questionTemplate: config.questionTemplate,
crawlTimeOut: config.crawlTimeOut,
kDays: config.kDays
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
}
formValue.value.enableDanmu = config.enableDanmu
formValue.value.browserPath = config.browserPath
@@ -233,12 +199,7 @@ 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
formValue.value.httpProxy=config.httpProxy
formValue.value.httpProxyEnabled=config.httpProxyEnabled
formValue.value.enableAgent = config.enableAgent
formValue.value.qgqpBId = config.qgqpBId
// formRef.value.resetFields()
};
reader.readAsText(file);
};
@@ -247,6 +208,8 @@ function importConfig() {
window.onerror = function (event, source, lineno, colno, error) {
//console.log(event, source, lineno, colno, error)
// 将错误信息发送给后端
EventsEmit("frontendError", {
page: "settings.vue",
message: event,
@@ -255,259 +218,223 @@ 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 => {
promptTemplates.value = res
GetPromptTemplates("","").then(res=>{
//console.log(res)
promptTemplates.value=res
})
showManagePromptsModal.value = false
showManagePromptsModal.value=false
})
}
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 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 deletePrompt(ID) {
DelPrompt(ID).then(res => {
function deletePrompt(ID){
DelPrompt(ID).then(res=>{
message.success(res)
GetPromptTemplates("", "").then(res => {
promptTemplates.value = res
GetPromptTemplates("","").then(res=>{
//console.log(res)
promptTemplates.value=res
})
})
}
</script>
<template>
<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-flex justify="left" style="margin-top: 12px;padding-left: 12px;">
<n-form ref="formRef" :label-placement="'left'" :label-align="'left'" >
<n-grid :cols="24" :x-gap="24" style="text-align: left" :layout-shift-disabled="true">
<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 &nbsp;&nbsp;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="6" label="指数基金" path="enableFund" >
<n-switch v-model:value="formValue.enableFund" />
</n-form-item-gi>
</n-grid>
<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-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 :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-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-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-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.maxTokens" >
<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: 8
}"
/>
</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: 8
}"
/>
</n-form-item-gi>
</n-grid>
<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-form>
<n-gi :span="24" v-if="promptTemplates.length>0" v-for="prompt in promptTemplates" >
<n-flex justify="start">
<n-tag 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-gi>
</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>
@@ -515,9 +442,5 @@ function deletePrompt(ID) {
</template>
<style scoped>
.cardHeaderClass {
font-size: 16px;
font-weight: bold;
color: red;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,137 +0,0 @@
<script setup>
import {onMounted, onBeforeMount, ref, watchEffect} from "vue";
import * as echarts from 'echarts';
import {GetStockMinutePriceLineData} from "../../wailsjs/go/main/App"; // 如果您使用多个组件,请将此样式导入放在您的主文件中
const {stockCode,stockName,lastPrice,openPrice,darkTheme} = defineProps({
stockCode: {
type: String,
default: ""
},
stockName: {
type: String,
default: ""
},
lastPrice: {
type: Number,
default: 0
},
openPrice: {
type: Number,
default: 0
},
darkTheme: {
type: Boolean,
default: true
},
})
const chartRef=ref();
function setChartData(chart) {
//console.log("setChartData")
GetStockMinutePriceLineData(stockCode, stockName).then(result => {
//console.log("GetStockMinutePriceLineData",result)
const priceData = result.priceData
let category = []
let price = []
let min = 0
let max = 0
for (let i = 0; i < priceData.length; i++) {
category.push(priceData[i].time)
price.push(priceData[i].price)
if (min === 0 || min > priceData[i].price) {
min = priceData[i].price
}
if (max < priceData[i].price) {
max = priceData[i].price
}
}
let option = {
padding: [0, 0, 0, 0],
grid: {
top: 0,
left: 0,
right: 0,
bottom: 0
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
}
},
xAxis: {
show: false,
type: 'category',
data: category
},
yAxis: {
show: false,
type: 'value',
min: (min).toFixed(2),
max: (max).toFixed(2),
minInterval: 0.01,
},
// visualMap: {
// show: false,
// type: 'piecewise',
// pieces: [
// {
// min: Number(min),
// max: Number(openPrice),
// color: 'green'
// },
// {
// min: Number(openPrice),
// max: Number(max),
// color: 'red'
// }
// ]
// },
series: [
{
data: price,
type: 'line',
smooth: false,
stack: '总量',
showSymbol: false,
lineStyle: {
color: lastPrice > openPrice ? 'rgba(245, 0, 0, 1)' : 'rgb(6,251,10)'
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: lastPrice > openPrice ? 'rgba(245, 0, 0, 1)' : 'rgba(6,251,10, 1)'
}, {
offset: 1,
color: lastPrice > openPrice ? 'rgba(245, 0, 0, 0.25)' : 'rgba(6,251,10, 0.25)'
}])
},
}
]
};
chart.setOption(option);
})
}
const chart =ref( null)
onMounted(() => {
chart.value = echarts.init( document.getElementById('sparkLine'+stockCode));
setChartData(chart.value);
})
watchEffect(() => {
console.log(stockName,'lastPrice变化为:', lastPrice,lastPrice > openPrice)
setChartData(chart.value);
})
</script>
<template>
<div style="height: 20px;width: 100%" :id="'sparkLine'+stockCode">
</div>
</template>

View File

@@ -1,43 +0,0 @@
<script setup lang="ts">
import {h} from 'vue'
import {NTag,NImage} from 'naive-ui'
import EmbeddedUrl from "./EmbeddedUrl.vue";
</script>
<template>
<n-tabs type="line" animated>
<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://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://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="欢迎推荐更多有趣的财经网页">
</n-tab-pane>
<!-- <n-tab-pane name="自在量化" tab="自在量化">-->
<!-- <embedded-url url="https://quant.zizizaizai.com/home"/>-->
<!-- </n-tab-pane>-->
</n-tabs>
</template>
<style scoped>
</style>

View File

@@ -2,8 +2,7 @@ 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)

View File

@@ -1,101 +0,0 @@
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值作为延迟时间
});
}
}

View File

@@ -2,23 +2,21 @@ import {createMemoryHistory, createRouter, createWebHashHistory, createWebHistor
import stockView from '../components/stock.vue'
import settingsView from '../components/settings.vue'
import aboutView from "../components/about.vue";
import about from "../components/about.vue";
import fundView from "../components/fund.vue";
import marketView from "../components/market.vue";
import agentChat from "../components/agent-chat.vue"
import market from "../components/market.vue";
const routes = [
{ path: '/', component: stockView,name: 'stock'},
{ path: '/fund', component: fundView,name: 'fund' },
{ path: '/settings', component: settingsView,name: 'settings' },
{ path: '/about', component: aboutView,name: 'about' },
{ path: '/market', component: marketView,name: 'market' },
{ path: '/agent', component: agentChat,name: 'agent' },
{ path: '/about', component: about,name: 'about' },
{ path: '/market', component: market,name: 'market' },
]
const router = createRouter({
//history: createWebHistory(),
history: createWebHashHistory(),
history: createWebHistory(),
routes,
})

View File

@@ -1,22 +1,7 @@
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(),
AutoImport({
resolvers: [TDesignResolver({
library: 'chat'
})],
}),
Components({
resolvers: [TDesignResolver({
library: 'chat'
})],
}),
]
plugins: [vue()]
})

37
frontend/wailsjs/go/main/App.d.ts vendored Executable file → Normal file
View File

@@ -2,7 +2,6 @@
// This file is automatically generated. DO NOT EDIT
import {data} from '../models';
import {models} from '../models';
import {context} from '../models';
export function AddCronTask(arg1:data.FollowedStock):Promise<any>;
@@ -14,15 +13,7 @@ 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>;
export function CheckUpdate(arg1:number):Promise<void>;
export function CheckUpdate():Promise<void>;
export function ClsCalendar():Promise<Array<any>>;
@@ -38,9 +29,7 @@ export function FollowFund(arg1:string):Promise<string>;
export function GetAIResponseResult(arg1:string):Promise<models.AIResponseResult>;
export function GetAiConfigs():Promise<Array<data.AIConfig>>;
export function GetConfig():Promise<data.SettingConfig>;
export function GetConfig():Promise<data.Settings>;
export function GetFollowList(arg1:number):Promise<any>;
@@ -50,8 +39,6 @@ export function GetGroupList():Promise<Array<data.Group>>;
export function GetGroupStockList(arg1:number):Promise<Array<data.GroupStock>>;
export function GetHotStrategy():Promise<Record<string, any>>;
export function GetIndustryMoneyRankSina(arg1:string,arg2:string):Promise<Array<Record<string, any>>>;
export function GetIndustryRank(arg1:string,arg2:number):Promise<Array<any>>;
@@ -60,8 +47,6 @@ export function GetMoneyRankSina(arg1:string):Promise<Array<Record<string, any>>
export function GetPromptTemplates(arg1:string,arg2:string):Promise<any>;
export function GetSponsorInfo():Promise<Record<string, any>>;
export function GetStockCommonKLine(arg1:string,arg2:string,arg3:number):Promise<any>;
export function GetStockKLine(arg1:string,arg2:string,arg3:number):Promise<any>;
@@ -90,32 +75,24 @@ 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:number,arg5:any,arg6:boolean):Promise<void>;
export function NewChatStream(arg1:string,arg2:string,arg3:string,arg4:any):Promise<void>;
export function NewsPush(arg1:any):Promise<void>;
export function OpenURL(arg1:string):Promise<void>;
export function ReFleshTelegraphList(arg1:string):Promise<any>;
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,arg6:number):Promise<void>;
export function SaveAIResponseResult(arg1:string,arg2:string,arg3:string,arg4:string,arg5:string):Promise<void>;
export function SaveAsMarkdown(arg1:string,arg2:string):Promise<string>;
export function SaveImage(arg1:string,arg2:string):Promise<string>;
export function SaveWordFile(arg1:string,arg2:string):Promise<string>;
export function SearchStock(arg1:string):Promise<Record<string, any>>;
export function SendDingDingMessage(arg1:string,arg2:string):Promise<string>;
@@ -136,12 +113,10 @@ export function StockNotice(arg1:string):Promise<Array<any>>;
export function StockResearchReport(arg1:string):Promise<Array<any>>;
export function SummaryStockNews(arg1:string,arg2:number,arg3:any,arg4:boolean):Promise<void>;
export function SummaryStockNews(arg1:string,arg2:any):Promise<void>;
export function UnFollow(arg1:string):Promise<string>;
export function UnFollowFund(arg1:string):Promise<string>;
export function UpdateConfig(arg1:data.SettingConfig):Promise<string>;
export function UpdateGroupSort(arg1:number,arg2:number):Promise<boolean>;
export function UpdateConfig(arg1:data.Settings):Promise<string>;

64
frontend/wailsjs/go/main/App.js Executable file → Normal file
View File

@@ -22,24 +22,8 @@ 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);
}
export function CheckStockBaseInfo(arg1) {
return window['go']['main']['App']['CheckStockBaseInfo'](arg1);
}
export function CheckUpdate(arg1) {
return window['go']['main']['App']['CheckUpdate'](arg1);
export function CheckUpdate() {
return window['go']['main']['App']['CheckUpdate']();
}
export function ClsCalendar() {
@@ -70,10 +54,6 @@ 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']();
}
@@ -94,10 +74,6 @@ export function GetGroupStockList(arg1) {
return window['go']['main']['App']['GetGroupStockList'](arg1);
}
export function GetHotStrategy() {
return window['go']['main']['App']['GetHotStrategy']();
}
export function GetIndustryMoneyRankSina(arg1, arg2) {
return window['go']['main']['App']['GetIndustryMoneyRankSina'](arg1, arg2);
}
@@ -114,10 +90,6 @@ export function GetPromptTemplates(arg1, arg2) {
return window['go']['main']['App']['GetPromptTemplates'](arg1, arg2);
}
export function GetSponsorInfo() {
return window['go']['main']['App']['GetSponsorInfo']();
}
export function GetStockCommonKLine(arg1, arg2, arg3) {
return window['go']['main']['App']['GetStockCommonKLine'](arg1, arg2, arg3);
}
@@ -174,10 +146,6 @@ 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);
}
@@ -186,18 +154,14 @@ export function LongTigerRank(arg1) {
return window['go']['main']['App']['LongTigerRank'](arg1);
}
export function NewChatStream(arg1, arg2, arg3, arg4, arg5, arg6) {
return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3, arg4, arg5, arg6);
export function NewChatStream(arg1, arg2, arg3, arg4) {
return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3, arg4);
}
export function NewsPush(arg1) {
return window['go']['main']['App']['NewsPush'](arg1);
}
export function OpenURL(arg1) {
return window['go']['main']['App']['OpenURL'](arg1);
}
export function ReFleshTelegraphList(arg1) {
return window['go']['main']['App']['ReFleshTelegraphList'](arg1);
}
@@ -210,22 +174,14 @@ export function RemoveStockGroup(arg1, arg2, arg3) {
return window['go']['main']['App']['RemoveStockGroup'](arg1, arg2, arg3);
}
export function SaveAIResponseResult(arg1, arg2, arg3, arg4, arg5, arg6) {
return window['go']['main']['App']['SaveAIResponseResult'](arg1, arg2, arg3, arg4, arg5, arg6);
export function SaveAIResponseResult(arg1, arg2, arg3, arg4, arg5) {
return window['go']['main']['App']['SaveAIResponseResult'](arg1, arg2, arg3, arg4, arg5);
}
export function SaveAsMarkdown(arg1, arg2) {
return window['go']['main']['App']['SaveAsMarkdown'](arg1, arg2);
}
export function SaveImage(arg1, arg2) {
return window['go']['main']['App']['SaveImage'](arg1, arg2);
}
export function SaveWordFile(arg1, arg2) {
return window['go']['main']['App']['SaveWordFile'](arg1, arg2);
}
export function SearchStock(arg1) {
return window['go']['main']['App']['SearchStock'](arg1);
}
@@ -266,8 +222,8 @@ export function StockResearchReport(arg1) {
return window['go']['main']['App']['StockResearchReport'](arg1);
}
export function SummaryStockNews(arg1, arg2, arg3, arg4) {
return window['go']['main']['App']['SummaryStockNews'](arg1, arg2, arg3, arg4);
export function SummaryStockNews(arg1, arg2) {
return window['go']['main']['App']['SummaryStockNews'](arg1, arg2);
}
export function UnFollow(arg1) {
@@ -281,7 +237,3 @@ 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);
}

90
frontend/wailsjs/go/models.ts Executable file → Normal file
View File

@@ -1,55 +1,5 @@
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
@@ -296,7 +246,6 @@ export namespace data {
Cron?: string;
IsDel: number;
Groups: GroupStock[];
AiConfigId: number;
static createFrom(source: any = {}) {
return new FollowedStock(source);
@@ -318,7 +267,6 @@ 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 {
@@ -362,7 +310,7 @@ export namespace data {
this.Description = source["Description"];
}
}
export class SettingConfig {
export class Settings {
ID: number;
// Go type: time
CreatedAt: any;
@@ -377,6 +325,12 @@ 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;
@@ -389,16 +343,9 @@ 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 SettingConfig(source);
return new Settings(source);
}
constructor(source: any = {}) {
@@ -414,6 +361,12 @@ 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"];
@@ -426,13 +379,6 @@ 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 {
@@ -478,8 +424,6 @@ export namespace data {
is_hs: string;
act_name: string;
act_ent_type: string;
bk_name: string;
bk_code: string;
static createFrom(source: any = {}) {
return new StockBasic(source);
@@ -508,8 +452,6 @@ export namespace data {
this.is_hs = source["is_hs"];
this.act_name = source["act_name"];
this.act_ent_type = source["act_ent_type"];
this.bk_name = source["bk_name"];
this.bk_code = source["bk_code"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
@@ -758,9 +700,7 @@ export namespace models {
icon: string;
alipay: string;
wxpay: string;
wxgzh: string;
buildTimeStamp: number;
officialStatement: string;
IsDel: number;
static createFrom(source: any = {}) {
@@ -778,9 +718,7 @@ export namespace models {
this.icon = source["icon"];
this.alipay = source["alipay"];
this.wxpay = source["wxpay"];
this.wxgzh = source["wxgzh"];
this.buildTimeStamp = source["buildTimeStamp"];
this.officialStatement = source["officialStatement"];
this.IsDel = source["IsDel"];
}

View File

@@ -48,10 +48,6 @@ 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);
}

117
go.mod
View File

@@ -1,134 +1,85 @@
module go-stock
go 1.25.0
go 1.23.0
require (
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/PuerkitoBio/goquery v1.10.1
github.com/chromedp/chromedp v0.11.2
github.com/coocood/freecache v1.2.4
github.com/duke-git/lancet/v2 v2.3.8
github.com/duke-git/lancet/v2 v2.3.4
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.17.0
github.com/go-resty/resty/v2 v2.16.2
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.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
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/sys v0.31.0
golang.org/x/text v0.23.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gorm.io/gorm v1.31.1
gorm.io/plugin/dbresolver v1.6.2
gorm.io/gorm v1.25.12
gorm.io/plugin/dbresolver v1.5.3
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/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/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb // 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/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/glebarez/go-sqlite v1.21.2 // 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.2.0 // indirect
github.com/godbus/dbus/v5 v5.1.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-20250406163304-c1995be93bd1 // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // 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/josharian/intern v1.0.0 // indirect
github.com/labstack/echo/v4 v4.13.3 // 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.9.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // 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 // 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.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // 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/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/go-webview2 v1.0.19 // indirect
github.com/wailsapp/mimetype v1.4.1 // 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
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
golang.org/x/net v0.38.0 // 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.67.1 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.40.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
)
// replace github.com/wailsapp/wails/v2 v2.9.2 => C:\Users\spark\go\pkg\mod

1060
go.sum

File diff suppressed because it is too large Load Diff

133
main.go
View File

@@ -5,22 +5,24 @@ import (
"embed"
"encoding/json"
"fmt"
"github.com/duke-git/lancet/v2/slice"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/menu/keys"
"github.com/wailsapp/wails/v2/pkg/options"
"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"
goruntime "runtime"
"runtime/debug"
"strings"
"github.com/duke-git/lancet/v2/slice"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/mac"
"github.com/wailsapp/wails/v2/pkg/options/windows"
"github.com/wailsapp/wails/v2/pkg/runtime"
"time"
)
//go:embed frontend/dist
@@ -38,9 +40,6 @@ var alipay []byte
//go:embed build/screenshot/wxpay.jpg
var wxpay []byte
//go:embed build/screenshot/扫码_搜索联合传播样式-白色版.png
var wxgzh []byte
//go:embed build/stock_basic.json
var stocksBin []byte
@@ -54,13 +53,10 @@ var stocksBinUS []byte
var Version string
var VersionCommit string
var OFFICIAL_STATEMENT string
var BuildKey string
func main() {
checkDir("data")
db.Init("")
data.InitAnalyzeSentiment()
go AutoMigrate()
//db.Dao.Model(&data.Group{}).Where("id = ?", 0).FirstOrCreate(&data.Group{
@@ -68,40 +64,37 @@ 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()
if IsMacOS() {
AppMenu.Append(menu.EditMenu())
}
//FileMenu := AppMenu.AddSubmenu("设置")
//FileMenu.AddText("窗口全屏", keys.CmdOrCtrl("f"), func(callback *menu.CallbackData) {
// runtime.WindowFullscreen(app.ctx)
//})
//FileMenu.AddText("窗口还原", keys.Key("Esc"), func(callback *menu.CallbackData) {
// runtime.WindowUnfullscreen(app.ctx)
//})
//FileMenu.AddText("显示搜索框", keys.CmdOrCtrl("s"), func(callbackData *menu.CallbackData) {
// runtime.EventsEmit(app.ctx, "showSearch", 1)
//})
//FileMenu.AddText("隐藏搜索框", keys.CmdOrCtrl("d"), func(callbackData *menu.CallbackData) {
// runtime.EventsEmit(app.ctx, "showSearch", 0)
//})
//FileMenu.AddText("刷新数据", keys.CmdOrCtrl("r"), func(callbackData *menu.CallbackData) {
// //runtime.EventsEmit(app.ctx, "refresh", "setting-"+time.Now().Format("2006-01-02 15:04:05"))
// runtime.EventsEmit(app.ctx, "refreshFollowList", "refresh-"+time.Now().Format("2006-01-02 15:04:05"))
//})
//FileMenu.AddSeparator()
FileMenu := AppMenu.AddSubmenu("设置")
FileMenu.AddText("显示搜索框", keys.CmdOrCtrl("s"), func(callbackData *menu.CallbackData) {
runtime.EventsEmit(app.ctx, "showSearch", 1)
})
FileMenu.AddText("隐藏搜索框", keys.CmdOrCtrl("d"), func(callbackData *menu.CallbackData) {
runtime.EventsEmit(app.ctx, "showSearch", 0)
})
FileMenu.AddText("刷新数据", keys.CmdOrCtrl("r"), func(callbackData *menu.CallbackData) {
//runtime.EventsEmit(app.ctx, "refresh", "setting-"+time.Now().Format("2006-01-02 15:04:05"))
runtime.EventsEmit(app.ctx, "refreshFollowList", "refresh-"+time.Now().Format("2006-01-02 15:04:05"))
})
FileMenu.AddSeparator()
FileMenu.AddText("窗口全屏", keys.CmdOrCtrl("f"), func(callback *menu.CallbackData) {
runtime.WindowFullscreen(app.ctx)
})
FileMenu.AddText("窗口还原", keys.Key("Esc"), func(callback *menu.CallbackData) {
runtime.WindowUnfullscreen(app.ctx)
})
//if goruntime.GOOS == "windows" {
// FileMenu.AddText("隐藏到托盘区", keys.CmdOrCtrl("z"), func(_ *menu.CallbackData) {
// runtime.WindowHide(app.ctx)
// })
//}
if goruntime.GOOS == "windows" {
FileMenu.AddText("隐藏到托盘区", keys.CmdOrCtrl("h"), func(_ *menu.CallbackData) {
runtime.WindowHide(app.ctx)
})
FileMenu.AddText("显示", keys.CmdOrCtrl("v"), func(_ *menu.CallbackData) {
runtime.WindowShow(app.ctx)
})
}
//FileMenu.AddText("退出", keys.CmdOrCtrl("q"), func(_ *menu.CallbackData) {
// runtime.Quit(app.ctx)
@@ -112,33 +105,31 @@ func main() {
//var width, height int
//var err error
//
width, _, minWidth, minHeight, err := getScreenResolution()
width, _, err := getScreenResolution()
if err != nil {
log.SugaredLogger.Error("get screen resolution error")
width = 1456
//height = 768
}
darkTheme := data.GetSettingConfig().DarkTheme
darkTheme := data.NewSettingsApi(&data.Settings{}).GetConfig().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}
}
//frameless := getFrameless()
// Create application with options
err = wails.Run(&options.App{
Title: "go-stockAI赋能股票分析✨ " + OFFICIAL_STATEMENT,
Title: "go-stock",
Width: width * 4 / 5,
Height: 920,
MinWidth: minWidth,
MinHeight: minHeight,
Height: 900,
MinWidth: 1456,
MinHeight: 768,
//MaxWidth: width,
//MaxHeight: height,
DisableResize: false,
Fullscreen: false,
Frameless: false,
Frameless: true,
StartHidden: false,
HideWindowOnClose: false,
EnableDefaultContextMenu: true,
@@ -171,11 +162,12 @@ func main() {
// Mac platform specific options
Mac: &mac.Options{
TitleBar: &mac.TitleBar{
TitlebarAppearsTransparent: false,
TitlebarAppearsTransparent: true,
HideTitle: false,
HideTitleBar: false,
FullSizeContent: false,
UseToolbar: true,
UseToolbar: false,
HideToolbarSeparator: true,
},
Appearance: mac.NSAppearanceNameDarkAqua,
WebviewIsTransparent: true,
@@ -194,26 +186,6 @@ 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{})
@@ -232,10 +204,6 @@ 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) {
@@ -292,7 +260,7 @@ func initStockDataHK(ctx context.Context) {
}
func updateBasicInfo() {
config := data.GetSettingConfig()
config := data.NewSettingsApi(&data.Settings{}).GetConfig()
if config.UpdateBasicInfoOnStart {
//更新基本信息
go data.NewStockDataApi().GetStockBaseInfo()
@@ -377,9 +345,6 @@ func checkDir(dir string) {
os.Mkdir(dir, os.ModePerm)
log.SugaredLogger.Info("create dir: " + dir)
}
if BuildKey == "" {
BuildKey = "cc1e0d684e32f176c56ff1fcf384dcd9"
}
}
// PanicHandler 捕获 panic 的包装函数

View File

@@ -1,23 +0,0 @@
package main
// @Author spark
// @Date 2025/7/8 18:51
// @Desc
//-----------------------------------------------------------------------------------
import "runtime"
// IsWindows 判断是否为 Windows 系统
func IsWindows() bool {
return runtime.GOOS == "windows"
}
// IsMacOS 判断是否为 macOS 系统
func IsMacOS() bool {
return runtime.GOOS == "darwin"
}
// IsLinux 判断是否为 Linux 系统
func IsLinux() bool {
return runtime.GOOS == "linux"
}