Compare commits

...

44 Commits

Author SHA1 Message Date
spark
0006501cc8 fix(data):优化数据获取流程并添加错误日志
- 在获取股票价格、财报、市场资讯等数据时,增加了空值判断并记录错误日志
-优化了数据获取流程,提高了代码的健壮性和可维护性- 在 chromedp 上下文中添加了日志记录,便于调试和排查问题
2025-02-09 20:58:45 +08:00
spark
c5fbe5fdae Merge branch 'master' of https://github.com/ArvinLovegood/go-stock 2025-02-09 20:18:42 +08:00
spark
66d85cf0a2 fix(backend):修复chromedp未取消导致的资源泄漏问题
- 在 openai_api.go 和 stock_data_api.go 中添加了对 chromedp.Cancel 的调用
- 确保在请求完成后正确取消 chromedp 的执行上下文,释放资源
2025-02-09 20:18:05 +08:00
spark
24145894b6 refactor(app):优化GetStockInfos函数,避免闪退
- 移除错误处理,因为调用方可能不需要错误信息
- 调整变量初始化顺序,提高代码可读性
- 简化错误处理逻辑,忽略错误并返回空值
2025-02-09 19:20:46 +08:00
Lovegood
75f680a298 Update issue templates 2025-02-09 17:16:59 +08:00
spark
2658f207dc feat(frontend):使用内嵌应用图标
- 使用内嵌应用图标替换URL图标
- 添加 GetVersionInfo 函数调用,用于获取版本信息
2025-02-09 16:26:20 +08:00
spark
626f99f0d1 refactor(frontend):重构关于页面布局
- 使用 n-divider组件替代 h1 标题,提高页面美观度
- 移除多余的 n-card 嵌套,简化页面结构
- 注释掉多余的 h1 标题,优化代码可读性
2025-02-09 16:17:12 +08:00
spark
4d541e81a2 feat(frontend):添加鸣谢部分
- 增加了捐赠者、开发者和开源项目的鸣谢列表
- 优化了关于页面的布局,使鸣谢内容更加突出
- 添加了外部链接,方便用户访问相关开源项目
2025-02-09 16:04:45 +08:00
spark
6dfe3fd135 feat(frontend):添加鸣谢部分
- 增加了捐赠者、开发者和开源项目的鸣谢列表
- 优化了关于页面的布局,使鸣谢内容更加突出
- 添加了外部链接,方便用户访问相关开源项目
2025-02-09 16:01:47 +08:00
spark
915e12eab3 feat(frontend):丰富关于页面内容并优化布局
- 在 GitHub 链接旁边添加 Issues 和 Releases 链接
- 在邮箱下方添加 QQ 和微信联系方式- 使用 n-divider 组件进行垂直分割,提高可读性
2025-02-09 15:40:04 +08:00
spark
bcfcbfeef0 fix(stock):优化股票关注功能
- 增加股票代码有效性验证
- 改进关注失败时的错误处理和用户提示
- 修复可能的 nil pointer dereference 问题
2025-02-09 15:29:04 +08:00
spark
1dc731de1e refactor(frontend):重构关于页面并添加作者信息
- 更新了 about.vue 页面布局和内容- 添加了作者信息和邮箱链接
- 移除了更新说明部分
- 调整了软件描述的样式和内容
2025-02-08 17:50:13 +08:00
spark
a580f9254a refactor(frontend):重构关于页面并添加作者信息
- 更新了 about.vue 页面布局和内容- 添加了作者信息和邮箱链接
- 移除了更新说明部分
- 调整了软件描述的样式和内容
2025-02-08 17:44:39 +08:00
spark
9b080bbb45 refactor(frontend):重构关于页面并添加作者信息
- 更新了 about.vue 页面布局和内容- 添加了作者信息和邮箱链接
- 移除了更新说明部分
- 调整了软件描述的样式和内容
2025-02-08 17:26:10 +08:00
spark
86183f4585 build:更新Wails构建动作版本 2025-02-08 16:48:12 +08:00
spark
97ab29259a ci:精简 commit message 输出
- 修改了获取 commit message 的 git 命令,仅保留 commit 主题行
-移除了不必要的信息,如作者和日期
- 优化了输出格式,提高了可读性
2025-02-08 16:30:54 +08:00
spark
91f3e66239 ci: 更新获取 commit message 的命令
- 修复了获取 commit message 时的语法错误
- 使用 PowerShell 兼容的命令格式
2025-02-08 16:06:59 +08:00
Lovegood
713b25d2db Update main.yml 2025-02-08 15:46:58 +08:00
spark
d0b65e7063 ci: 更新获取 commit message 的命令
- 修复了获取 commit message 时的语法错误
- 使用 PowerShell 兼容的命令格式
2025-02-08 15:34:40 +08:00
spark
f062306158 build: 更新 Wails 构建动作版本
- 将 ArvinLovegood/wails-build-action 版本从 v2.5 升级到 v2.6
- 保持其他配置不变,仅更新动作版本
2025-02-08 15:25:35 +08:00
spark
ae7b617e83 feat(frontend): 添加关于软件页面并实现版本信息动态获取
- 新增 about.vue 组件,包含软件介绍、更新说明和作者信息
- 添加 GetVersionInfo 函数,用于获取版本信息
- 在 App.vue 中添加关于软件的菜单项
- 在 router.js 中添加关于软件的路由
- 优化页面布局和样式
2025-02-08 15:05:52 +08:00
spark
1035f2a800 feat(frontend): 添加关于软件页面
- 在 App.vue 中添加关于软件的菜单项
- 在 router.js 中添加关于软件的路由- 新增 about.vue 组件,包含软件介绍和作者信息
2025-02-08 12:20:40 +08:00
spark
cb28b18541 docs(README): 更新 AI 分析股票功能截图
- 将 AI 分析股票功能的截图从 img_10.png 修改为 img.png
- 更新 README.md 中的相关图片引用
2025-02-08 11:39:39 +08:00
spark
9d42eb2729 docs(README): 更新项目介绍和赞助信息 2025-02-08 11:24:56 +08:00
spark
7b93d4d8ca feat(data): 添加 AIResponseResult模型并实现相关功能
感谢 @gnim2600 的建议!

- 新增 AIResponseResult 模型用于保存 AI 分析结果
- 实现 SaveAIResponseResult 和 GetAIResponseResult 函数
- 在前端添加 AI 分析功能,包括保存和获取分析结果
-优化 AI 分析界面,增加分析时间显示和再次分析按钮
2025-02-08 11:13:17 +08:00
spark
3e13ef007b feat(openai): 添加 OpenAI API 超时设置并调整相关功能
感谢@gnim2600 @XXXiaohuayanGGG 两位提供的帮助和建议
- 在前端和后端添加 OpenAI API 超时设置选项
- 更新 AI 诊断股票功能,支持自定义超时时间
- 优化设置界面布局,提高用户体验
- 为 AI 分析结果添加居中显示样式
2025-02-08 09:14:09 +08:00
spark
6ff1b68f1b fix:修复 GitHub 时间转换错误
- 移除了 getTimezoneOffset() * 60 * 1000 的计算
-现在直接使用 utcDate.getTime() 获取时间戳
2025-02-07 11:21:05 +08:00
spark
a6547db195 docs(README): 添加版本信息提示截图
- 在 README.md 中新增了版本信息提示部分
- 添加了对应的截图 img_11.png
2025-02-07 11:03:01 +08:00
spark
567414a136 feat(update): 增加新版本详细信息和发布时间
- 获取并显示新版本的 Tag 和 Commit 信息
- 将 UTC 时间转换为本地时间并显示
- 在通知中添加新版本详细信息和发布时间
- 优化股票卡片样式,增加鼠标悬停效果
2025-02-07 10:49:55 +08:00
Lovegood
34dc38a95f Merge pull request #3 from 2lovecode/feature-support-macos
feat(macos):support macos
2025-02-06 20:12:38 +08:00
2lovecode
6d2ab3ef41 feat(macos):support macos 2025-02-06 18:03:06 +08:00
spark
e55506705e feat(update): 添加软件更新检查功能
- 在应用启动时检查 GitHub 上的最新版本
- 如果发现新版本,通过通知提示用户更新
- 新增 GitHubReleaseVersion模型用于解析版本信息
- 在前端添加更新通知的展示逻辑
2025-02-06 16:19:11 +08:00
spark
322e87efbd ci:为 GitHub Actions 添加 build-tags 参数
在 GitHub Actions 的构建配置中添加了 build-tags 参数,使其等于当前的引用名称(github.ref_name)。这允许我们在构建过程中使用特定的标记。

- 修改了 .github/workflows/main.yml 文件- 在 build-platform 部分添加了 build-tags 参数
- 参数值设置为当前引用名称,增加了构建的灵活性和可追溯性
2025-02-06 15:01:36 +08:00
spark
1628381295 feat(app): 添加版本信息,为更新推送做准备
- 在应用启动时打印版本号
2025-02-06 14:53:07 +08:00
spark
8afc26badb test:移除雪球和硅流 API 调用相关代码 2025-02-05 16:44:00 +08:00
spark
d5db2ef879 feat(backend): 添加获取财务报告功能并优化聊天流
- 新增 GetFinancialReports 函数,用于抓取股票财务报告信息
- 优化 NewChatStream 函数,增加财务报告信息到聊天流中
- 更新测试用例,使用北京文化(sz000802)作为示例股票- 添加 TestGetFinancialReports 和 TestXUEQIU 测试函数
2025-02-05 16:25:24 +08:00
spark
509cd2dbca refactor(backend): 调整 openai_api.go 中的资源关闭逻辑
- 将 resp.RawBody().Close() 调用移动到 if err != nil块之后
- 确保在发生错误时也能正确关闭网络连接
- 优化了代码结构,提高了资源管理的可靠性
2025-02-04 20:02:21 +08:00
spark
3de2ad3cdc refactor(backend): 重构 OpenAI 和股票数据 API
-优化了 OpenAI API 的调用逻辑,提高了错误处理和数据处理的能力
- 改进了股票数据 API 的数据抓取和处理方式
- 移除了测试代码中冗余的部分,提高了代码可读性和维护性
2025-02-04 19:45:22 +08:00
spark
b00bddcdec refactor(stock-data): 重构股票数据获取逻辑
- 移除了不必要的并发请求,简化了代码结构
- 新增 FetchPrice 函数,用于获取股票价格信息
- 优化 SearchStockInfo 函数,提高了搜索效率和准确性
- 新增 SearchStockInfoByCode 函数,用于根据股票代码获取相关信息- 修复了一些潜在的错误和性能问题
2025-02-04 18:12:08 +08:00
spark
64b37b687c refactor(data): 优化 OpenAI API 客户端配置并改进流数据处理
- 将请求超时时间从 30秒增加到 60 秒
- 修正流数据的前缀检查,从 "chat data: " 改为 "data: "- 增加对 reasoning_content 的处理逻辑
- 优化数据处理流程,提高错误处理能力
2025-02-04 15:12:15 +08:00
spark
e81319bb4f docs(README): 添加赞助信息在 README.md 中添加了赞助信息部分,提供了支付宝和微信支付的二维码图片链接,鼓励对项目有帮助的用户进行赞助。 2025-02-04 07:31:44 +08:00
spark
7bc219d1a5 refactor(frontend): 优化 OpenAI 设置界面文案
- 将"自定义Prompt"标签修改为"自定义系统Prompt"
- 更新输入框占位符为"请输入系统prompt"
2025-02-03 22:03:16 +08:00
spark
0f2f58e6b8 docs: 更新 README 中的设置截图
- 将 README.md 中的 img_11.png 替换为 img_12.png
- 优化设置界面的视觉效果
2025-02-03 21:53:31 +08:00
spark
2dc0b95b45 docs: 更新 README 中的设置截图
- 将 README.md 中的 img_11.png 替换为 img_12.png
- 优化设置界面的视觉效果
2025-02-03 14:01:20 +08:00
30 changed files with 1487 additions and 152 deletions

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -31,11 +31,19 @@ jobs:
with:
submodules: recursive
- name: Get commit message
id: get_commit_message
run: |
$commit_message = & git log -1 --pretty=format:"%s"
echo "::set-output name=commit_message::$commit_message"
- name: Build wails
uses: ArvinLovegood/wails-build-action@v2.3
uses: ArvinLovegood/wails-build-action@v2.8
id: build
with:
build-name: ${{ matrix.build.name }}
build-platform: ${{ matrix.build.platform }}
package: true
go-version: '1.23'
build-tags: ${{ github.ref_name }}
build-commit-message: ${{ steps.get_commit_message.outputs.commit_message }}

View File

@@ -1,9 +1,11 @@
![Wails and NaiveUI](./build/appicon.png)
## 自选股行情实时监控基于Wails和NaiveUI构建的AI赋能股票分析工具
- 经测试目前硅基流动(siliconflow)提供的deepSeek api 服务比较稳定注册即送2000万Tokens[注册链接](https://cloud.siliconflow.cn/i/foufCerk)
- 软件快速迭代开发中,请大家优先测试和使用最新发布的版本。
- 欢迎大家提出宝贵的建议欢迎提issue,PR。当然更欢迎[赞助我](#都划到这了如果我的项目对您有帮助请赞助我吧),谢谢。
## BIG NEWS !!! 重大更新!!!
- 2025.01.17 新增AI大模型分析股票功能
![img_5.png](build/screenshot/img_10.png)
![img_5.png](build/screenshot/img.png)
## 简介
- 本项目基于Wails和NaiveUI纯属无聊仅供娱乐不喜勿喷。
@@ -13,7 +15,7 @@
## Snapshot
![img_1.png](build/screenshot/img_6.png)
### 设置
![img.png](build/screenshot/img_11.png)
![img_12.png](build/screenshot/img_12.png)
### 成本设置
![img.png](build/screenshot/img_7.png)
### 日K
@@ -23,8 +25,9 @@
### 钉钉报警通知
![img_4.png](build/screenshot/img_5.png)
### AI分析股票
![img_5.png](build/screenshot/img_10.png)
![img_5.png](build/screenshot/img.png)
### 版本信息提示
![img_11.png](build/screenshot/img_11.png)
## About
### A China stock data viewer build by [Wails](https://wails.io/) with [NavieUI](https://www.naiveui.com/).
@@ -51,3 +54,8 @@ You can build you Application with: `wails build`
[Wails](https://wails.io/)
[Vue](https://vuejs.org/)
[Vite](https://vitejs.dev/)
## 都划到这了,如果我的项目对您有帮助,请赞助我吧!😊😊😊
| 支付宝 | 微信 |
|-----|-----|
| ![alipay.jpg](build/screenshot/alipay.jpg) | ![wxpay.jpg](build/screenshot/wxpay.jpg) |

69
app.go
View File

@@ -4,6 +4,7 @@ package main
import (
"context"
"encoding/base64"
"fmt"
"github.com/PuerkitoBio/goquery"
"github.com/coocood/freecache"
@@ -16,6 +17,7 @@ import (
"go-stock/backend/data"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"strings"
"time"
)
@@ -37,6 +39,7 @@ func NewApp() *App {
// startup is called at application startup
func (a *App) startup(ctx context.Context) {
logger.SugaredLogger.Infof("Version:%s", Version)
// Perform your setup here
a.ctx = ctx
@@ -49,6 +52,36 @@ func (a *App) startup(ctx context.Context) {
}
func checkUpdate(a *App) {
releaseVersion := &models.GitHubReleaseVersion{}
_, err := resty.New().R().
SetResult(releaseVersion).
Get("https://api.github.com/repos/ArvinLovegood/go-stock/releases/latest")
if err != nil {
logger.SugaredLogger.Errorf("get github release version error:%s", err.Error())
return
}
logger.SugaredLogger.Infof("releaseVersion:%+v", releaseVersion.TagName)
if releaseVersion.TagName != Version {
tag := &models.Tag{}
_, err = resty.New().R().
SetResult(tag).
Get("https://api.github.com/repos/ArvinLovegood/go-stock/git/ref/tags/" + releaseVersion.TagName)
if err == nil {
releaseVersion.Tag = *tag
}
commit := &models.Commit{}
_, err = resty.New().R().
SetResult(commit).
Get(tag.Object.Url)
if err == nil {
releaseVersion.Commit = *commit
}
go runtime.EventsEmit(a.ctx, "updateVersion", releaseVersion)
}
}
// domReady is called after front-end resources have been loaded
func (a *App) domReady(ctx context.Context) {
// Add your action here
@@ -81,6 +114,11 @@ func (a *App) domReady(ctx context.Context) {
}()
go runtime.EventsEmit(a.ctx, "telegraph", refreshTelegraphList())
go MonitorStockPrices(a)
//检查新版本
go func() {
checkUpdate(a)
}()
}
func refreshTelegraphList() *[]string {
@@ -169,16 +207,12 @@ func MonitorStockPrices(a *App) {
}
func GetStockInfos(follows ...data.FollowedStock) *[]data.StockInfo {
stockInfos := make([]data.StockInfo, 0)
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)
stockData, _ := data.NewStockDataApi().GetStockCodeRealTimeData(stockCodes...)
for _, info := range *stockData {
v, ok := slice.FindBy(follows, func(idx int, follow data.FollowedStock) bool {
return follow.StockCode == info.Code
@@ -375,10 +409,6 @@ func (a *App) SendDingDingMessageByType(message string, stockCode string, msgTyp
return data.NewDingDingAPI().SendDingDingMessage(message)
}
func (a *App) NewChat(stock string) string {
return data.NewDeepSeekOpenAi().NewChat(stock)
}
func (a *App) NewChatStream(stock, stockCode string) {
msgs := data.NewDeepSeekOpenAi().NewChatStream(stock, stockCode)
for msg := range msgs {
@@ -387,6 +417,25 @@ func (a *App) NewChatStream(stock, stockCode string) {
runtime.EventsEmit(a.ctx, "newChatStream", "DONE")
}
func (a *App) SaveAIResponseResult(stockCode, stockName, result string) {
data.NewDeepSeekOpenAi().SaveAIResponseResult(stockCode, stockName, result)
}
func (a *App) GetAIResponseResult(stock string) *models.AIResponseResult {
return data.NewDeepSeekOpenAi().GetAIResponseResult(stock)
}
func (a *App) GetVersionInfo() *models.VersionInfo {
return &models.VersionInfo{
Version: Version,
Icon: GetImageBase(icon),
Content: VersionCommit,
}
}
func GetImageBase(bytes []byte) string {
return "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(bytes)
}
func GenNotificationMsg(stockInfo *data.StockInfo) string {
Price, err := convertor.ToFloat(stockInfo.Price)
if err != nil {

459
app_darwin.go Normal file
View File

@@ -0,0 +1,459 @@
//go:build darwin
// +build darwin
package main
import (
"context"
"fmt"
"go-stock/backend/data"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"strings"
"time"
"github.com/PuerkitoBio/goquery"
"github.com/coocood/freecache"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/mathutil"
"github.com/duke-git/lancet/v2/slice"
"github.com/go-resty/resty/v2"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// App struct
type App struct {
ctx context.Context
cache *freecache.Cache
}
// NewApp creates a new App application struct
func NewApp() *App {
cacheSize := 512 * 1024
cache := freecache.NewCache(cacheSize)
return &App{
cache: cache,
}
}
// startup is called at application startup
func (a *App) startup(ctx context.Context) {
logger.SugaredLogger.Infof("Version:%s", Version)
// Perform your setup here
a.ctx = ctx
// TODO 创建系统托盘
}
func checkUpdate(a *App) {
releaseVersion := &models.GitHubReleaseVersion{}
_, err := resty.New().R().
SetResult(releaseVersion).
Get("https://api.github.com/repos/ArvinLovegood/go-stock/releases/latest")
if err != nil {
logger.SugaredLogger.Errorf("get github release version error:%s", err.Error())
return
}
logger.SugaredLogger.Infof("releaseVersion:%+v", releaseVersion.TagName)
if releaseVersion.TagName != Version {
go runtime.EventsEmit(a.ctx, "updateVersion", releaseVersion)
}
}
// domReady is called after front-end resources have been loaded
func (a *App) domReady(ctx context.Context) {
// Add your action here
//定时更新数据
go func() {
config := data.NewSettingsApi(&data.Settings{}).GetConfig()
interval := config.RefreshInterval
if interval <= 0 {
interval = 1
}
ticker := time.NewTicker(time.Second * time.Duration(interval))
defer ticker.Stop()
for range ticker.C {
if isTradingTime(time.Now()) {
MonitorStockPrices(a)
}
}
}()
go func() {
ticker := time.NewTicker(time.Second * time.Duration(60))
defer ticker.Stop()
for range ticker.C {
telegraph := refreshTelegraphList()
if telegraph != nil {
go runtime.EventsEmit(a.ctx, "telegraph", telegraph)
}
}
}()
go runtime.EventsEmit(a.ctx, "telegraph", refreshTelegraphList())
go MonitorStockPrices(a)
//检查新版本
go func() {
checkUpdate(a)
}()
}
func refreshTelegraphList() *[]string {
url := "https://www.cls.cn/telegraph"
response, err := resty.New().R().
SetHeader("Referer", "https://www.cls.cn/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get(fmt.Sprintf(url))
if err != nil {
return &[]string{}
}
//logger.SugaredLogger.Info(string(response.Body()))
document, err := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
if err != nil {
return &[]string{}
}
var telegraph []string
document.Find("div.telegraph-content-box").Each(func(i int, selection *goquery.Selection) {
//logger.SugaredLogger.Info(selection.Text())
telegraph = append(telegraph, selection.Text())
})
return &telegraph
}
// isTradingDay 判断是否是交易日
func isTradingDay(date time.Time) bool {
weekday := date.Weekday()
// 判断是否是周末
if weekday == time.Saturday || weekday == time.Sunday {
return false
}
// 这里可以添加具体的节假日判断逻辑
// 例如:判断是否是春节、国庆节等
return true
}
// isTradingTime 判断是否是交易时间
func isTradingTime(date time.Time) bool {
if !isTradingDay(date) {
return false
}
hour, minute, _ := date.Clock()
// 判断是否在9:15到11:30之间
if (hour == 9 && minute >= 15) || (hour == 10) || (hour == 11 && minute <= 30) {
return true
}
// 判断是否在13:00到15:00之间
if (hour == 13) || (hour == 14) || (hour == 15 && minute <= 0) {
return true
}
return false
}
func MonitorStockPrices(a *App) {
dest := &[]data.FollowedStock{}
db.Dao.Model(&data.FollowedStock{}).Find(dest)
total := float64(0)
//for _, follow := range *dest {
// stockData := getStockInfo(follow)
// total += stockData.ProfitAmountToday
// price, _ := convertor.ToFloat(stockData.Price)
// if stockData.PrePrice != price {
// go runtime.EventsEmit(a.ctx, "stock_price", stockData)
// }
//}
stockInfos := GetStockInfos(*dest...)
for _, stockInfo := range *stockInfos {
total += stockInfo.ProfitAmountToday
price, _ := convertor.ToFloat(stockInfo.Price)
if stockInfo.PrePrice != price {
go runtime.EventsEmit(a.ctx, "stock_price", stockInfo)
}
}
if total != 0 {
// title := "go-stock " + time.Now().Format(time.DateTime) + fmt.Sprintf(" %.2f¥", total)
// systray.SetTooltip(title)
}
go runtime.EventsEmit(a.ctx, "realtime_profit", fmt.Sprintf(" %.2f", total))
//runtime.WindowSetTitle(a.ctx, title)
}
func GetStockInfos(follows ...data.FollowedStock) *[]data.StockInfo {
stockCodes := make([]string, 0)
for _, follow := range follows {
stockCodes = append(stockCodes, follow.StockCode)
}
stockData, err := data.NewStockDataApi().GetStockCodeRealTimeData(stockCodes...)
if err != nil {
logger.SugaredLogger.Errorf("get stock code real time data error:%s", err.Error())
return nil
}
stockInfos := make([]data.StockInfo, 0)
for _, info := range *stockData {
v, ok := slice.FindBy(follows, func(idx int, follow data.FollowedStock) bool {
return follow.StockCode == info.Code
})
if ok {
addStockFollowData(v, &info)
stockInfos = append(stockInfos, info)
}
}
return &stockInfos
}
func getStockInfo(follow data.FollowedStock) *data.StockInfo {
stockCode := follow.StockCode
stockDatas, err := data.NewStockDataApi().GetStockCodeRealTimeData(stockCode)
if err != nil || len(*stockDatas) == 0 {
return &data.StockInfo{}
}
stockData := (*stockDatas)[0]
addStockFollowData(follow, &stockData)
return &stockData
}
func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) {
stockData.PrePrice = follow.Price //上次当前价格
stockData.Sort = follow.Sort
stockData.CostPrice = follow.CostPrice //成本价
stockData.CostVolume = follow.Volume //成本量
stockData.AlarmChangePercent = follow.AlarmChangePercent
stockData.AlarmPrice = follow.AlarmPrice
//当前价格
price, _ := convertor.ToFloat(stockData.Price)
//当前价格为0 时 使用卖一价格作为当前价格
if price == 0 {
price, _ = convertor.ToFloat(stockData.A1P)
}
//当前价格依然为0 时 使用买一报价作为当前价格
if price == 0 {
price, _ = convertor.ToFloat(stockData.B1P)
}
//昨日收盘价
preClosePrice, _ := convertor.ToFloat(stockData.PreClose)
//当前价格依然为0 时 使用昨日收盘价为当前价格
if price == 0 {
price = preClosePrice
}
//今日最高价
highPrice, _ := convertor.ToFloat(stockData.High)
if highPrice == 0 {
highPrice, _ = convertor.ToFloat(stockData.Open)
}
//今日最低价
lowPrice, _ := convertor.ToFloat(stockData.Low)
if lowPrice == 0 {
lowPrice, _ = convertor.ToFloat(stockData.Open)
}
//开盘价
//openPrice, _ := convertor.ToFloat(stockData.Open)
if price > 0 {
stockData.ChangePrice = mathutil.RoundToFloat(price-preClosePrice, 2)
stockData.ChangePercent = mathutil.RoundToFloat(mathutil.Div(price-preClosePrice, preClosePrice)*100, 3)
}
if highPrice > 0 {
stockData.HighRate = mathutil.RoundToFloat(mathutil.Div(highPrice-preClosePrice, preClosePrice)*100, 3)
}
if lowPrice > 0 {
stockData.LowRate = mathutil.RoundToFloat(mathutil.Div(lowPrice-preClosePrice, preClosePrice)*100, 3)
}
if follow.CostPrice > 0 && follow.Volume > 0 {
if price > 0 {
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(price-follow.CostPrice, follow.CostPrice)*100, 3)
stockData.ProfitAmount = mathutil.RoundToFloat((price-follow.CostPrice)*float64(follow.Volume), 2)
stockData.ProfitAmountToday = mathutil.RoundToFloat((price-preClosePrice)*float64(follow.Volume), 2)
} else {
//未开盘时当前价格为昨日收盘价
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(preClosePrice-follow.CostPrice, follow.CostPrice)*100, 3)
stockData.ProfitAmount = mathutil.RoundToFloat((preClosePrice-follow.CostPrice)*float64(follow.Volume), 2)
stockData.ProfitAmountToday = mathutil.RoundToFloat((preClosePrice-preClosePrice)*float64(follow.Volume), 2)
}
}
//logger.SugaredLogger.Debugf("stockData:%+v", stockData)
if follow.Price != price && price > 0 {
go db.Dao.Model(follow).Where("stock_code = ?", follow.StockCode).Updates(map[string]interface{}{
"price": price,
})
}
}
// beforeClose is called when the application is about to quit,
// either by clicking the window close button or calling runtime.Quit.
// Returning true will cause the application to continue, false will continue shutdown as normal.
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{
Type: runtime.QuestionDialog,
Title: "go-stock",
Message: "确定关闭吗?",
Buttons: []string{"确定"},
Icon: icon,
CancelButton: "取消",
})
if err != nil {
logger.SugaredLogger.Errorf("dialog error:%s", err.Error())
return false
}
logger.SugaredLogger.Debugf("dialog:%s", dialog)
if dialog == "No" {
return true
}
return false
}
// shutdown is called at application termination
func (a *App) shutdown(ctx context.Context) {
// Perform your teardown here
// systray.Quit()
}
// Greet returns a greeting for the given name
func (a *App) Greet(stockCode string) *data.StockInfo {
//stockInfo, _ := data.NewStockDataApi().GetStockCodeRealTimeData(stockCode)
follow := &data.FollowedStock{
StockCode: stockCode,
}
db.Dao.Model(follow).Where("stock_code = ?", stockCode).First(follow)
stockInfo := getStockInfo(*follow)
return stockInfo
}
func (a *App) Follow(stockCode string) string {
return data.NewStockDataApi().Follow(stockCode)
}
func (a *App) UnFollow(stockCode string) string {
return data.NewStockDataApi().UnFollow(stockCode)
}
func (a *App) GetFollowList() []data.FollowedStock {
return data.NewStockDataApi().GetFollowList()
}
func (a *App) GetStockList(key string) []data.StockBasic {
return data.NewStockDataApi().GetStockList(key)
}
func (a *App) SetCostPriceAndVolume(stockCode string, price float64, volume int64) string {
return data.NewStockDataApi().SetCostPriceAndVolume(price, volume, stockCode)
}
func (a *App) SetAlarmChangePercent(val, alarmPrice float64, stockCode string) string {
return data.NewStockDataApi().SetAlarmChangePercent(val, alarmPrice, stockCode)
}
func (a *App) SetStockSort(sort int64, stockCode string) {
data.NewStockDataApi().SetStockSort(sort, stockCode)
}
func (a *App) SendDingDingMessage(message string, stockCode string) string {
ttl, _ := a.cache.TTL([]byte(stockCode))
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
if ttl > 0 {
return ""
}
err := a.cache.Set([]byte(stockCode), []byte("1"), 60*5)
if err != nil {
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
return ""
}
return data.NewDingDingAPI().SendDingDingMessage(message)
}
// SendDingDingMessageByType msgType 报警类型: 1 涨跌报警;2 股价报警 3 成本价报警
func (a *App) SendDingDingMessageByType(message string, stockCode string, msgType int) string {
ttl, _ := a.cache.TTL([]byte(stockCode))
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
if ttl > 0 {
return ""
}
err := a.cache.Set([]byte(stockCode), []byte("1"), getMsgTypeTTL(msgType))
if err != nil {
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
return ""
}
stockInfo := &data.StockInfo{}
db.Dao.Model(stockInfo).Where("code = ?", stockCode).First(stockInfo)
go data.NewAlertWindowsApi("go-stock消息通知", getMsgTypeName(msgType), GenNotificationMsg(stockInfo), "").SendNotification()
return data.NewDingDingAPI().SendDingDingMessage(message)
}
func (a *App) NewChat(stock string) string {
return data.NewDeepSeekOpenAi().NewChat(stock)
}
func (a *App) NewChatStream(stock, stockCode string) {
msgs := data.NewDeepSeekOpenAi().NewChatStream(stock, stockCode)
for msg := range msgs {
runtime.EventsEmit(a.ctx, "newChatStream", msg)
}
runtime.EventsEmit(a.ctx, "newChatStream", "DONE")
}
func GenNotificationMsg(stockInfo *data.StockInfo) string {
Price, err := convertor.ToFloat(stockInfo.Price)
if err != nil {
Price = 0
}
PreClose, err := convertor.ToFloat(stockInfo.PreClose)
if err != nil {
PreClose = 0
}
var RF float64
if PreClose > 0 {
RF = mathutil.RoundToFloat(((Price-PreClose)/PreClose)*100, 2)
}
return "[" + stockInfo.Name + "] " + stockInfo.Price + " " + convertor.ToString(RF) + "% " + stockInfo.Date + " " + stockInfo.Time
}
// msgType : 1 涨跌报警(5分钟);2 股价报警(30分钟) 3 成本价报警(30分钟)
func getMsgTypeTTL(msgType int) int {
switch msgType {
case 1:
return 60 * 5
case 2:
return 60 * 30
case 3:
return 60 * 30
default:
return 60 * 5
}
}
func getMsgTypeName(msgType int) string {
switch msgType {
case 1:
return "涨跌报警"
case 2:
return "股价报警"
case 3:
return "成本价报警"
default:
return "未知类型"
}
}
func (a *App) UpdateConfig(settings *data.Settings) string {
logger.SugaredLogger.Infof("UpdateConfig:%+v", settings)
return data.NewSettingsApi(settings).UpdateConfig()
}
func (a *App) GetConfig() *data.Settings {
return data.NewSettingsApi(&data.Settings{}).GetConfig()
}

View File

@@ -0,0 +1,52 @@
//go:build darwin
// +build darwin
package data
import (
"fmt"
"go-stock/backend/logger"
"os/exec"
)
// AlertWindowsApi @Author 2lovecode
// @Date 2025/02/06 17:50
// @Desc
// -----------------------------------------------------------------------------------
type AlertWindowsApi struct {
AppID string
// 窗口标题
Title string
// 窗口内容
Content string
// 窗口图标
Icon string
}
func NewAlertWindowsApi(AppID string, Title string, Content string, Icon string) *AlertWindowsApi {
return &AlertWindowsApi{
AppID: AppID,
Title: Title,
Content: Content,
Icon: Icon,
}
}
func (a AlertWindowsApi) SendNotification() bool {
if getConfig().LocalPushEnable == false {
logger.SugaredLogger.Error("本地推送未开启")
return false
}
script := fmt.Sprintf(`display notification "%s" with title "%s"`, a.Content, a.Title)
cmd := exec.Command("osascript", "-e", script)
err := cmd.Run()
if err != nil {
logger.SugaredLogger.Error(err)
return false
}
return true
}

View File

@@ -0,0 +1,32 @@
//go:build darwin
// +build darwin
package data
import (
"go-stock/backend/logger"
"testing"
"github.com/go-toast/toast"
)
// @Author 2lovecode
// @Date 2025/02/06 17:50
// @Desc
// -----------------------------------------------------------------------------------
func TestAlert(t *testing.T) {
notification := toast.Notification{
AppID: "go-stock",
Title: "Hello, World!",
Message: "This is a toast notification.",
Icon: "../../build/appicon.png",
Duration: "short",
Audio: toast.Default,
}
err := notification.Push()
if err != nil {
logger.SugaredLogger.Error(err)
return
}
}

View File

@@ -2,11 +2,16 @@ package data
import (
"bufio"
"context"
"encoding/json"
"fmt"
"github.com/PuerkitoBio/goquery"
"github.com/chromedp/chromedp"
"github.com/duke-git/lancet/v2/strutil"
"github.com/go-resty/resty/v2"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"strings"
"sync"
"time"
@@ -23,6 +28,7 @@ type OpenAi struct {
MaxTokens int `json:"max_tokens"`
Temperature float64 `json:"temperature"`
Prompt string `json:"prompt"`
TimeOut int `json:"time_out"`
}
func NewDeepSeekOpenAi() *OpenAi {
@@ -34,6 +40,7 @@ func NewDeepSeekOpenAi() *OpenAi {
MaxTokens: config.OpenAiMaxTokens,
Temperature: config.OpenAiTemperature,
Prompt: config.Prompt,
TimeOut: config.OpenAiApiTimeOut,
}
}
@@ -67,71 +74,10 @@ type AiResponse struct {
SystemFingerprint string `json:"system_fingerprint"`
}
func (o OpenAi) NewChat(stock string) string {
client := resty.New()
client.SetBaseURL(o.BaseUrl)
client.SetHeader("Authorization", "Bearer "+o.ApiKey)
client.SetHeader("Content-Type", "application/json")
res := &AiResponse{}
_, err := client.R().
SetResult(res).
SetBody(map[string]interface{}{
"model": o.Model,
"max_tokens": o.MaxTokens,
"temperature": o.Temperature,
"messages": []map[string]interface{}{
{
"role": "system",
"content": "作为一位专业的A股市场分析师和投资顾问,请你根据以下信息提供详细的技术分析和投资策略建议:" +
"1. 市场背景:\n" +
"- 当前A股市场整体走势(如:牛市、熊市、震荡市)\n " +
"- 近期影响市场的主要宏观经济因素\n " +
"- 市场情绪指标(如:融资融券余额、成交量变化) " +
"2. 技术指标分析: " +
"- 当前股价水平" +
"- 所在boll区间" +
"- 上证指数的MA(移动平均线)、MACD、KDJ指标分析\n " +
"- 行业板块轮动情况\n " +
"- 近期市场热点和龙头股票的技术形态 " +
"3. 风险评估:\n " +
"- 当前市场主要风险因素\n " +
"- 如何设置止损和止盈位\n " +
"- 资金管理建议(如:仓位控制) " +
"4. 投资策略:\n " +
"- 短期(1-2周)、中期(1-3月)和长期(3-6月)的市场预期\n " +
"- 不同风险偏好投资者的策略建议\n " +
"- 值得关注的行业板块和个股推荐(请给出2-3个具体例子,包括股票代码和名称) " +
"5. 技术面和基本面结合:\n " +
"- 如何将技术分析与公司基本面分析相结合\n " +
"- 识别高质量股票的关键指标 " +
"请提供详细的分析和具体的操作建议,包括入场时机、持仓周期和退出策略。同时,请强调风险控制的重要性,并提醒投资者需要根据自身情况做出决策。 " +
"你的分析和建议应当客观、全面,并基于当前可获得的市场数据。如果某些信息无法确定,请明确指出并解释原因。",
},
{
"role": "user",
"content": "点评一下" + stock,
},
},
}).
Post("/chat/completions")
if err != nil {
return ""
}
//logger.SugaredLogger.Infof("%v", res.Choices[0].Message.Content)
return res.Choices[0].Message.Content
}
func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
ch := make(chan string)
ch := make(chan string, 512)
go func() {
defer close(ch)
client := resty.New()
client.SetBaseURL(o.BaseUrl)
client.SetHeader("Authorization", "Bearer "+o.ApiKey)
client.SetHeader("Content-Type", "application/json")
client.SetRetryCount(3)
client.SetTimeout(time.Second * 30)
msg := []map[string]interface{}{
{
"role": "system",
@@ -140,13 +86,17 @@ func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
"content": o.Prompt,
},
}
logger.SugaredLogger.Infof("Prompt%s", o.Prompt)
wg := &sync.WaitGroup{}
wg.Add(4)
wg.Add(5)
go func() {
defer wg.Done()
messages := SearchStockPriceInfo(stockCode)
if messages == nil || len(*messages) == 0 {
logger.SugaredLogger.Error("获取股票价格失败")
return
}
price := ""
for _, message := range *messages {
price += message + ";"
@@ -157,9 +107,28 @@ func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
})
}()
go func() {
defer wg.Done()
messages := GetFinancialReports(stockCode)
if messages == nil || len(*messages) == 0 {
logger.SugaredLogger.Error("获取股票财报失败")
return
}
for _, message := range *messages {
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": stock + message,
})
}
}()
go func() {
defer wg.Done()
messages := GetTelegraphList()
if messages == nil || len(*messages) == 0 {
logger.SugaredLogger.Error("获取市场资讯失败")
return
}
for _, message := range *messages {
msg = append(msg, map[string]interface{}{
"role": "assistant",
@@ -171,6 +140,10 @@ func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
go func() {
defer wg.Done()
messages := SearchStockInfo(stock, "depth")
if messages == nil || len(*messages) == 0 {
logger.SugaredLogger.Error("获取股票资讯失败")
return
}
for _, message := range *messages {
msg = append(msg, map[string]interface{}{
"role": "assistant",
@@ -181,6 +154,10 @@ func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
go func() {
defer wg.Done()
messages := SearchStockInfo(stock, "telegram")
if messages == nil || len(*messages) == 0 {
logger.SugaredLogger.Error("获取股票电报资讯失败")
return
}
for _, message := range *messages {
msg = append(msg, map[string]interface{}{
"role": "assistant",
@@ -189,12 +166,19 @@ func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
}
}()
wg.Wait()
msg = append(msg, map[string]interface{}{
"role": "user",
"content": stock + "分析和总结",
})
client := resty.New()
client.SetBaseURL(o.BaseUrl)
client.SetHeader("Authorization", "Bearer "+o.ApiKey)
client.SetHeader("Content-Type", "application/json")
client.SetRetryCount(3)
if o.TimeOut <= 0 {
o.TimeOut = 300
}
client.SetTimeout(time.Duration(o.TimeOut) * time.Second)
resp, err := client.R().
SetDoNotParseResponse(true).
SetBody(map[string]interface{}{
@@ -206,17 +190,18 @@ func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
}).
Post("/chat/completions")
defer resp.RawBody().Close()
if err != nil {
logger.SugaredLogger.Infof("Stream error : %s", err.Error())
ch <- err.Error()
return
}
defer resp.RawBody().Close()
scanner := bufio.NewScanner(resp.RawBody())
for scanner.Scan() {
line := scanner.Text()
logger.SugaredLogger.Infof("Received data: %s", line)
if strings.HasPrefix(line, "chat data: ") {
if strings.HasPrefix(line, "data: ") {
data := strings.TrimPrefix(line, "data: ")
if data == "[DONE]" {
return
@@ -225,8 +210,10 @@ func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
var streamResponse struct {
Choices []struct {
Delta struct {
Content string `json:"content"`
Content string `json:"content"`
ReasoningContent string `json:"reasoning_content"`
} `json:"delta"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
}
@@ -234,15 +221,77 @@ func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
for _, choice := range streamResponse.Choices {
if content := choice.Delta.Content; content != "" {
ch <- content
logger.SugaredLogger.Infof("Content data: %s", content)
}
if reasoningContent := choice.Delta.ReasoningContent; reasoningContent != "" {
ch <- reasoningContent
logger.SugaredLogger.Infof("ReasoningContent data: %s", reasoningContent)
}
if choice.FinishReason == "stop" {
return
}
}
} else {
logger.SugaredLogger.Infof("Stream data error : %s", err.Error())
ch <- err.Error()
}
} else {
ch <- line
}
}
}()
return ch
}
func GetFinancialReports(stockCode string) *[]string {
// 创建一个 chromedp 上下文
ctx, cancel := chromedp.NewContext(
context.Background(),
chromedp.WithLogf(logger.SugaredLogger.Infof),
chromedp.WithErrorf(logger.SugaredLogger.Errorf),
)
defer cancel()
defer func(ctx context.Context) {
err := chromedp.Cancel(ctx)
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
}(ctx)
var htmlContent string
url := fmt.Sprintf("https://xueqiu.com/snowman/S/%s/detail#/ZYCWZB", stockCode)
err := chromedp.Run(ctx,
chromedp.Navigate(url),
// 等待页面加载完成,可以根据需要调整等待时间
chromedp.WaitVisible("table.table", chromedp.ByQuery),
chromedp.OuterHTML("html", &htmlContent, chromedp.ByQuery),
)
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
logger.SugaredLogger.Error(err.Error())
return &[]string{}
}
var messages []string
document.Find("table tr").Each(func(i int, selection *goquery.Selection) {
tr := ""
selection.Find("th,td").Each(func(i int, selection *goquery.Selection) {
ret := selection.Find("p").First().Text()
if ret == "" {
ret = selection.Text()
}
text := strutil.RemoveNonPrintable(ret)
tr += text + " "
})
logger.SugaredLogger.Infof("%s", tr+" \n")
messages = append(messages, tr+" \n")
})
return &messages
}
func (o OpenAi) NewCommonChatStream(stock, stockCode, apiURL, apiKey, Model string) <-chan string {
ch := make(chan string)
go func() {
@@ -369,8 +418,22 @@ func GetTelegraphList() *[]string {
}
var telegraph []string
document.Find("div.telegraph-content-box").Each(func(i int, selection *goquery.Selection) {
//logger.SugaredLogger.Info(selection.Text())
logger.SugaredLogger.Info(selection.Text())
telegraph = append(telegraph, selection.Text())
})
return &telegraph
}
func (o OpenAi) SaveAIResponseResult(stockCode, stockName, result string) {
db.Dao.Create(&models.AIResponseResult{
StockCode: stockCode,
StockName: stockName,
Content: result,
})
}
func (o OpenAi) GetAIResponseResult(stock string) *models.AIResponseResult {
var result models.AIResponseResult
db.Dao.Where("stock_code = ?", stock).Order("id desc").First(&result)
return &result
}

View File

@@ -8,12 +8,12 @@ import (
func TestNewDeepSeekOpenAiConfig(t *testing.T) {
db.Init("../../data/stock.db")
ai := NewDeepSeekOpenAi()
res := ai.NewChatStream("闻泰科技", "sh600745")
res := ai.NewChatStream("北京文化", "sz000802")
for {
select {
case msg := <-res:
if msg == "" {
return
continue
}
t.Log(msg)
}

View File

@@ -21,7 +21,9 @@ type Settings struct {
OpenAiModelName string `json:"openAiModelName"`
OpenAiMaxTokens int `json:"openAiMaxTokens"`
OpenAiTemperature float64 `json:"openAiTemperature"`
OpenAiApiTimeOut int `json:"openAiApiTimeOut"`
Prompt string `json:"prompt"`
CheckUpdate bool `json:"checkUpdate"`
}
func (receiver Settings) TableName() string {
@@ -56,6 +58,8 @@ func (s SettingsApi) UpdateConfig() string {
"open_ai_temperature": s.Config.OpenAiTemperature,
"tushare_token": s.Config.TushareToken,
"prompt": s.Config.Prompt,
"check_update": s.Config.CheckUpdate,
"open_ai_api_time_out": s.Config.OpenAiApiTimeOut,
})
} else {
logger.SugaredLogger.Infof("未找到配置,创建默认配置:%+v", s.Config)
@@ -73,6 +77,8 @@ func (s SettingsApi) UpdateConfig() string {
OpenAiTemperature: s.Config.OpenAiTemperature,
TushareToken: s.Config.TushareToken,
Prompt: s.Config.Prompt,
CheckUpdate: s.Config.CheckUpdate,
OpenAiApiTimeOut: s.Config.OpenAiApiTimeOut,
})
}
return "保存成功!"

View File

@@ -8,6 +8,7 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/PuerkitoBio/goquery"
"github.com/chromedp/chromedp"
@@ -291,6 +292,9 @@ func (receiver StockDataApi) GetStockCodeRealTimeData(StockCodes ...string) (*[]
stockInfos := make([]StockInfo, 0)
str := GB18030ToUTF8(resp.Body())
dataStr := strutil.SplitEx(str, "\n", true)
if len(dataStr) == 0 {
return &[]StockInfo{}, errors.New("获取股票信息失败,请检查股票代码是否正确")
}
for _, data := range dataStr {
//logger.SugaredLogger.Info(data)
stockData, err := ParseFullSingleStockData(data)
@@ -318,8 +322,8 @@ func (receiver StockDataApi) GetStockCodeRealTimeData(StockCodes ...string) (*[]
func (receiver StockDataApi) Follow(stockCode string) string {
logger.SugaredLogger.Infof("Follow %s", stockCode)
stockInfos, err := receiver.GetStockCodeRealTimeData(stockCode)
if err != nil {
logger.SugaredLogger.Error(err.Error())
if err != nil || len(*stockInfos) == 0 {
logger.SugaredLogger.Error(err)
return "关注失败"
}
stockInfo := (*stockInfos)[0]
@@ -516,24 +520,26 @@ func SearchStockPriceInfo(stockCode string) *[]string {
// 创建一个 chromedp 上下文
ctx, cancel := chromedp.NewContext(
context.Background(),
chromedp.WithLogf(logger.SugaredLogger.Infof),
chromedp.WithErrorf(logger.SugaredLogger.Errorf),
)
defer cancel()
defer func(ctx context.Context) {
err := chromedp.Cancel(ctx)
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
}(ctx)
var htmlContent string
var tasks chromedp.Tasks
tasks = append(tasks, chromedp.Navigate(url))
tasks = append(tasks, chromedp.WaitVisible("div.quote-change-box", chromedp.ByQuery))
tasks = append(tasks, chromedp.ActionFunc(func(ctx context.Context) error {
chromedp.WaitVisible("span.quote-price", chromedp.ByQuery)
price := ""
for {
chromedp.Text("span.quote-price", &price, chromedp.BySearch).Do(ctx)
logger.SugaredLogger.Infof("price:%s", price)
if price != "" && validator.IsNumberStr(price) {
break
}
}
price, _ := FetchPrice(ctx)
logger.SugaredLogger.Infof("price:%s", price)
return nil
}))
tasks = append(tasks, chromedp.OuterHTML("html", &htmlContent, chromedp.ByQuery))
@@ -557,6 +563,29 @@ func SearchStockPriceInfo(stockCode string) *[]string {
})
return &messages
}
func FetchPrice(ctx context.Context) (string, error) {
var price string
timeout := time.After(10 * time.Second) // 设置超时时间为10秒
ticker := time.NewTicker(1 * time.Second) // 每秒尝试一次
defer ticker.Stop()
for {
select {
case <-timeout:
return "", fmt.Errorf("timeout reached while fetching price")
case <-ticker.C:
err := chromedp.Run(ctx, chromedp.Text("span.quote-price", &price, chromedp.BySearch))
if err != nil {
logger.SugaredLogger.Errorf("failed to fetch price: %v", err)
continue
}
logger.SugaredLogger.Infof("price:%s", price)
if price != "" && validator.IsNumberStr(price) {
return price, nil
}
}
}
}
func SearchStockInfo(stock, msgType string) *[]string {
// 创建一个 chromedp 上下文
ctx, cancel := chromedp.NewContext(
@@ -565,13 +594,19 @@ func SearchStockInfo(stock, msgType string) *[]string {
chromedp.WithErrorf(logger.SugaredLogger.Errorf),
)
defer cancel()
defer func(ctx context.Context) {
err := chromedp.Cancel(ctx)
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
}(ctx)
var htmlContent string
url := fmt.Sprintf("https://www.cls.cn/searchPage?keyword=%s&type=%s", stock, msgType)
err := chromedp.Run(ctx,
chromedp.Navigate(url),
// 等待页面加载完成,可以根据需要调整等待时间
//chromedp.Sleep(3*time.Second),
chromedp.WaitVisible("div.search-content,a.search-content", chromedp.ByQuery),
chromedp.WaitVisible(".search-content", chromedp.ByQuery),
chromedp.OuterHTML("html", &htmlContent, chromedp.ByQuery),
)
if err != nil {
@@ -584,7 +619,46 @@ func SearchStockInfo(stock, msgType string) *[]string {
return &[]string{}
}
var messages []string
document.Find("div.search-telegraph-list,a.search-content").Each(func(i int, selection *goquery.Selection) {
document.Find(".search-content").Each(func(i int, selection *goquery.Selection) {
text := strutil.RemoveNonPrintable(selection.Text())
if strings.Contains(text, stock) {
messages = append(messages, text)
logger.SugaredLogger.Infof("搜索到消息-%s: %s", msgType, text)
}
})
return &messages
}
func SearchStockInfoByCode(stock string) *[]string {
// 创建一个 chromedp 上下文
ctx, cancel := chromedp.NewContext(
context.Background(),
chromedp.WithLogf(logger.SugaredLogger.Infof),
chromedp.WithErrorf(logger.SugaredLogger.Errorf),
)
defer cancel()
var htmlContent string
stock = strings.ReplaceAll(stock, "sh", "")
stock = strings.ReplaceAll(stock, "sz", "")
url := fmt.Sprintf("https://gushitong.baidu.com/stock/ab-%s", stock)
err := chromedp.Run(ctx,
chromedp.Navigate(url),
// 等待页面加载完成,可以根据需要调整等待时间
//chromedp.Sleep(3*time.Second),
chromedp.WaitVisible("a.news-item-link", chromedp.ByQuery),
chromedp.OuterHTML("html", &htmlContent, chromedp.ByQuery),
)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return &[]string{}
}
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
logger.SugaredLogger.Error(err.Error())
return &[]string{}
}
var messages []string
document.Find("a.news-item-link").Each(func(i int, selection *goquery.Selection) {
text := strutil.RemoveNonPrintable(selection.Text())
if strings.Contains(text, stock) {
messages = append(messages, text)

View File

@@ -3,7 +3,6 @@ package data
import (
"encoding/json"
"fmt"
"github.com/PuerkitoBio/goquery"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/strutil"
"github.com/go-resty/resty/v2"
@@ -21,35 +20,26 @@ import (
//-----------------------------------------------------------------------------------
func TestGetTelegraph(t *testing.T) {
url := "https://www.cls.cn/telegraph"
response, err := resty.New().R().
SetHeader("Referer", "https://www.cls.cn/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get(fmt.Sprintf(url))
if err != nil {
return
}
logger.SugaredLogger.Info(string(response.Body()))
document, err := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
if err != nil {
return
}
document.Find("div.telegraph-content-box").Each(func(i int, selection *goquery.Selection) {
text := selection.Text()
logger.SugaredLogger.Info(text)
GetTelegraphList()
}
})
func TestGetFinancialReports(t *testing.T) {
GetFinancialReports("sz000802")
}
func TestGetTelegraphSearch(t *testing.T) {
//url := "https://www.cls.cn/searchPage?keyword=%E9%97%BB%E6%B3%B0%E7%A7%91%E6%8A%80&type=telegram"
messages := SearchStockInfo("闻泰科技", "depth")
messages := SearchStockInfo("闻泰科技", "telegram")
for _, message := range *messages {
logger.SugaredLogger.Info(message)
}
//https://www.cls.cn/stock?code=sh600745
}
func TestSearchStockInfoByCode(t *testing.T) {
SearchStockInfoByCode("sh600745")
}
func TestSearchStockPriceInfo(t *testing.T) {
SearchStockPriceInfo("sh600745")
}

159
backend/models/models.go Normal file
View File

@@ -0,0 +1,159 @@
package models
import (
"gorm.io/gorm"
"gorm.io/plugin/soft_delete"
"time"
)
// @Author spark
// @Date 2025/2/6 15:25
// @Desc
//-----------------------------------------------------------------------------------
type GitHubReleaseVersion struct {
Url string `json:"url"`
AssetsUrl string `json:"assets_url"`
UploadUrl string `json:"upload_url"`
HtmlUrl string `json:"html_url"`
Id int `json:"id"`
Author struct {
Login string `json:"login"`
Id int `json:"id"`
NodeId string `json:"node_id"`
AvatarUrl string `json:"avatar_url"`
GravatarId string `json:"gravatar_id"`
Url string `json:"url"`
HtmlUrl string `json:"html_url"`
FollowersUrl string `json:"followers_url"`
FollowingUrl string `json:"following_url"`
GistsUrl string `json:"gists_url"`
StarredUrl string `json:"starred_url"`
SubscriptionsUrl string `json:"subscriptions_url"`
OrganizationsUrl string `json:"organizations_url"`
ReposUrl string `json:"repos_url"`
EventsUrl string `json:"events_url"`
ReceivedEventsUrl string `json:"received_events_url"`
Type string `json:"type"`
UserViewType string `json:"user_view_type"`
SiteAdmin bool `json:"site_admin"`
} `json:"author"`
NodeId string `json:"node_id"`
TagName string `json:"tag_name"`
TargetCommitish string `json:"target_commitish"`
Name string `json:"name"`
Draft bool `json:"draft"`
Prerelease bool `json:"prerelease"`
CreatedAt time.Time `json:"created_at"`
PublishedAt time.Time `json:"published_at"`
Assets []struct {
Url string `json:"url"`
Id int `json:"id"`
NodeId string `json:"node_id"`
Name string `json:"name"`
Label string `json:"label"`
Uploader struct {
Login string `json:"login"`
Id int `json:"id"`
NodeId string `json:"node_id"`
AvatarUrl string `json:"avatar_url"`
GravatarId string `json:"gravatar_id"`
Url string `json:"url"`
HtmlUrl string `json:"html_url"`
FollowersUrl string `json:"followers_url"`
FollowingUrl string `json:"following_url"`
GistsUrl string `json:"gists_url"`
StarredUrl string `json:"starred_url"`
SubscriptionsUrl string `json:"subscriptions_url"`
OrganizationsUrl string `json:"organizations_url"`
ReposUrl string `json:"repos_url"`
EventsUrl string `json:"events_url"`
ReceivedEventsUrl string `json:"received_events_url"`
Type string `json:"type"`
UserViewType string `json:"user_view_type"`
SiteAdmin bool `json:"site_admin"`
} `json:"uploader"`
ContentType string `json:"content_type"`
State string `json:"state"`
Size int `json:"size"`
DownloadCount int `json:"download_count"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
BrowserDownloadUrl string `json:"browser_download_url"`
} `json:"assets"`
TarballUrl string `json:"tarball_url"`
ZipballUrl string `json:"zipball_url"`
Body string `json:"body"`
Tag Tag `json:"tag"`
Commit Commit `json:"commit"`
}
type Tag struct {
Ref string `json:"ref"`
NodeId string `json:"node_id"`
Url string `json:"url"`
Object struct {
Sha string `json:"sha"`
Type string `json:"type"`
Url string `json:"url"`
} `json:"object"`
}
type Commit struct {
Sha string `json:"sha"`
NodeId string `json:"node_id"`
Url string `json:"url"`
HtmlUrl string `json:"html_url"`
Author struct {
Name string `json:"name"`
Email string `json:"email"`
Date time.Time `json:"date"`
} `json:"author"`
Committer struct {
Name string `json:"name"`
Email string `json:"email"`
Date time.Time `json:"date"`
} `json:"committer"`
Tree struct {
Sha string `json:"sha"`
Url string `json:"url"`
} `json:"tree"`
Message string `json:"message"`
Parents []struct {
Sha string `json:"sha"`
Url string `json:"url"`
HtmlUrl string `json:"html_url"`
} `json:"parents"`
Verification struct {
Verified bool `json:"verified"`
Reason string `json:"reason"`
Signature interface{} `json:"signature"`
Payload interface{} `json:"payload"`
VerifiedAt interface{} `json:"verified_at"`
} `json:"verification"`
}
type AIResponseResult struct {
gorm.Model
StockCode string `json:"stockCode"`
StockName string `json:"stockName"`
Content string `json:"content"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
}
func (receiver AIResponseResult) TableName() string {
return "ai_response_result"
}
type VersionInfo struct {
gorm.Model
Version string `json:"version"`
Content string `json:"content"`
Icon string `json:"icon"`
BuildTimeStamp int64 `json:"buildTimeStamp"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
}
func (receiver VersionInfo) TableName() string {
return "version_info"
}

BIN
build/screenshot/alipay.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 173 KiB

BIN
build/screenshot/img_12.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

BIN
build/screenshot/wxpay.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

View File

@@ -15,7 +15,7 @@ import {
SettingsOutline,
ReorderTwoOutline,
ExpandOutline,
RefreshOutline, PowerOutline, BarChartOutline, MoveOutline, WalletOutline, StarOutline,
RefreshOutline, PowerOutline, LogoGithub, MoveOutline, WalletOutline, StarOutline,
} from '@vicons/ionicons5'
const content = ref('数据来源于网络,仅供参考;投资有风险,入市需谨慎')
@@ -67,6 +67,23 @@ const menuOptions = ref([
key: 'settings',
icon: renderIcon(SettingsOutline),
},
{
label: () =>
h(
RouterLink,
{
to: {
name: 'about',
params: {
id: 'zh-CN'
}
}
},
{ default: () => '关于' }
),
key: 'about',
icon: renderIcon(LogoGithub),
},
{
label: ()=> h("a", {
href: '#',

View File

@@ -0,0 +1,104 @@
<script setup>
import { MdPreview } from 'md-editor-v3';
// preview.css相比style.css少了编辑器那部分样式
import 'md-editor-v3/lib/preview.css';
import {onMounted, ref} from 'vue';
import {GetVersionInfo} from "../../wailsjs/go/main/App";
const updateLog = ref('');
const versionInfo = ref('');
const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/master/build/appicon.png');
onMounted(() => {
document.title = '关于软件';
GetVersionInfo().then((res) => {
updateLog.value = res.content;
versionInfo.value = res.version;
icon.value = res.icon;
});
})
</script>
<template>
<n-config-provider>
<n-layout>
<n-space vertical size="large">
<!-- 软件描述 -->
<n-card size="large">
<n-divider title-placement="center">关于软件</n-divider>
<n-space vertical >
<n-image width="100" :src="icon" />
<h1>go-stock <n-tag size="small" round>{{versionInfo}}</n-tag></h1>
<div style="justify-self: center;text-align: left" >
<p>自选股行情实时监控基于Wails和NaiveUI构建的AI赋能股票分析工具</p>
<p>
</p>
<p>
欢迎点赞GitHub<a href="https://github.com/ArvinLovegood/go-stock" target="_blank">go-stock</a><n-divider vertical />
<a href="https://github.com/ArvinLovegood/go-stock" target="_blank">GitHub</a><n-divider vertical />
<a href="https://github.com/ArvinLovegood/go-stock/issues" target="_blank">Issues</a><n-divider vertical />
<a href="https://github.com/ArvinLovegood/go-stock/releases" target="_blank">Releases</a><n-divider vertical />
</p>
<p v-if="updateLog">更新说明{{updateLog}}</p>
</div>
</n-space>
<n-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>
<p>
邮箱<a href="mailto:sparkmemory@163.com">sparkmemory@163.com</a><n-divider vertical />
QQ 506808970<n-divider vertical />
微信ArvinLovegood</p><n-divider vertical />
</n-space>
<n-divider title-placement="center">鸣谢</n-divider>
<div style="justify-self: center;text-align: left" >
<p>
感谢以下捐赠者
<n-gradient-text size="small" type="warning">*</n-gradient-text><n-divider vertical />
</p>
<p>
感谢以下开发者
<a href="https://github.com/gnim2600" target="_blank">@gnim2600</a><n-divider vertical />
<a href="https://github.com/XXXiaohuayanGGG" target="_blank">@XXXiaohuayanGGG</a><n-divider vertical />
<a href="https://github.com/2lovecode" target="_blank">@2lovecode</a><n-divider vertical />
<a href="https://github.com/JerryLookupU" target="_blank">@JerryLookupU</a><n-divider vertical />
</p>
<p>
感谢以下开源项目
<a href="https://github.com/wailsapp/wails" target="_blank">Wails</a><n-divider vertical />
<a href="https://github.com/vuejs" target="_blank">Vue</a><n-divider vertical />
<a href="https://github.com/tusen-ai/naive-ui" target="_blank">NaiveUI</a><n-divider vertical />
</p>
</div>
</n-card>
</n-space>
</n-layout>
</n-config-provider>
</template>
<style scoped>
/* 可以在这里添加一些样式 */
h1, h2 {
margin: 0;
padding: 6px 0;
}
p {
margin: 2px 0;
}
ul {
list-style-type: disc;
padding-left: 20px;
}
a {
color: #18a058;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
</style>

View File

@@ -27,6 +27,7 @@ const formValue = ref({
temperature: 0.1,
maxTokens: 1024,
prompt:"",
timeout: 5
},
})
@@ -51,6 +52,7 @@ onMounted(()=>{
temperature:res.openAiTemperature,
maxTokens:res.openAiMaxTokens,
prompt:res.prompt,
timeout:res.openAiApiTimeOut
}
console.log(res)
})
@@ -73,7 +75,8 @@ function saveConfig(){
openAiMaxTokens:formValue.value.openAI.maxTokens,
openAiTemperature:formValue.value.openAI.temperature,
tushareToken:formValue.value.tushareToken,
prompt:formValue.value.openAI.prompt
prompt:formValue.value.openAI.prompt,
openAiApiTimeOut:formValue.value.openAI.timeout
})
//console.log("Settings",config)
@@ -150,26 +153,29 @@ function sendTestNotice(){
<n-form-item-gi :span="6" label="是否启用AI诊股" path="openAI.enable" >
<n-switch v-model:value="formValue.openAI.enable" />
</n-form-item-gi>
<n-form-item-gi :span="22" v-if="formValue.openAI.enable" label="openAI 接口地址:" path="openAI.baseUrl">
<n-form-item-gi :span="11" v-if="formValue.openAI.enable" label="openAI 接口地址:" path="openAI.baseUrl">
<n-input type="text" placeholder="AI接口地址" v-model:value="formValue.openAI.baseUrl" clearable />
</n-form-item-gi>
<n-form-item-gi :span="10" v-if="formValue.openAI.enable" label="openAI apiKey" path="openAI.apiKey">
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" label="请求超时时间(秒)" path="openAI.timeout">
<n-input-number min="1" step="1" placeholder="请求超时时间(秒)" v-model:value="formValue.openAI.timeout" />
</n-form-item-gi>
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="openAI 令牌(apiKey)" path="openAI.apiKey">
<n-input type="text" placeholder="apiKey" v-model:value="formValue.openAI.apiKey" clearable />
</n-form-item-gi>
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="AI模型" path="openAI.model">
<n-input type="text" placeholder="AI模型" v-model:value="formValue.openAI.model" clearable />
<n-form-item-gi :span="10" v-if="formValue.openAI.enable" label="AI模型名称" path="openAI.model">
<n-input type="text" placeholder="AI模型名称" v-model:value="formValue.openAI.model" clearable />
</n-form-item-gi>
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="temperature" path="openAI.temperature" >
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="openAI temperature" path="openAI.temperature" >
<n-input-number placeholder="temperature" v-model:value="formValue.openAI.temperature"/>
</n-form-item-gi>
<n-form-item-gi :span="10" v-if="formValue.openAI.enable" label="maxTokens" path="openAI.maxTokens">
<n-form-item-gi :span="10" v-if="formValue.openAI.enable" label="openAI maxTokens" path="openAI.maxTokens">
<n-input-number placeholder="maxTokens" v-model:value="formValue.openAI.maxTokens"/>
</n-form-item-gi>
<n-form-item-gi :span="22" v-if="formValue.openAI.enable" label="自定义Prompt" path="openAI.prompt">
<n-form-item-gi :span="22" v-if="formValue.openAI.enable" label="模型系统 Prompt" path="openAI.prompt">
<n-input v-model:value="formValue.openAI.prompt"
type="textarea"
:show-count="true"
placeholder="请输入prompt"
placeholder="请输入系统prompt"
:autosize="{
minRows: 5,
maxRows: 8

View File

@@ -1,16 +1,27 @@
<script setup>
import {computed, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref} from 'vue'
import {computed, h, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref} from 'vue'
import {
Follow, GetConfig,
GetFollowList,
GetStockList,
Greet, NewChat, NewChatStream,
Greet, SaveAIResponseResult, NewChatStream,
SendDingDingMessage, SendDingDingMessageByType,
SetAlarmChangePercent,
SetCostPriceAndVolume, SetStockSort,
UnFollow
UnFollow, GetAIResponseResult, GetVersionInfo
} from '../../wailsjs/go/main/App'
import {NButton, NFlex, NForm, NFormItem, NInputNumber, NText, useMessage, useModal,useNotification} from 'naive-ui'
import {
NAvatar,
NButton,
NFlex,
NForm,
NFormItem,
NInputNumber,
NText,
useMessage,
useModal,
useNotification
} from 'naive-ui'
import {EventsOn, WindowFullscreen, WindowReload, WindowUnfullscreen} from '../../wailsjs/runtime'
import {Add, Search,StarOutline} from '@vicons/ionicons5'
import { MdPreview } from 'md-editor-v3';
@@ -53,6 +64,7 @@ const data = reactive({
openAiEnable: false,
loading: true,
})
const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/master/build/appicon.png');
const sortedResults = computed(() => {
//console.log("computed",sortedResults.value)
@@ -101,6 +113,11 @@ onMounted(() => {
data.fenshiURL='http://image.sinajs.cn/newchart/min/n/'+data.code+'.gif'+"?t="+Date.now()
}
}, 3500)
GetVersionInfo().then((res) => {
icon.value = res.icon;
});
})
onBeforeUnmount(() => {
@@ -141,6 +158,7 @@ EventsOn("newChatStream",async (msg) => {
//console.log("newChatStream:->",data.airesult)
data.loading = false
if (msg === "DONE") {
SaveAIResponseResult(data.code, data.name, data.airesult)
message.info("AI分析完成")
message.destroyAll()
} else {
@@ -148,6 +166,54 @@ EventsOn("newChatStream",async (msg) => {
}
})
EventsOn("updateVersion",async (msg) => {
const githubTimeStr = msg.published_at;
// 创建一个 Date 对象
const utcDate = new Date(githubTimeStr);
// 获取本地时间
const date = new Date(utcDate.getTime());
const year = date.getFullYear();
// getMonth 返回值是 0 - 11所以要加 1
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
console.log("GitHub UTC 时间:", utcDate);
console.log("转换后的本地时间:", formattedDate);
notify.info({
avatar: () =>
h(NAvatar, {
size: 'small',
round: false,
src: icon.value
}),
title: '发现新版本: ' + msg.tag_name,
content: () => {
//return h(MdPreview, {theme:'dark',modelValue:msg.commit?.message}, null)
return h('div', {
style: {
'text-align': 'left',
'font-size': '14px',
}
}, { default: () => msg.commit?.message })
},
duration: 0,
meta: "发布时间:"+formattedDate,
action: () => {
return h(NButton, {
type: 'primary',
size: 'small',
onClick: () => {
window.open(msg.html_url)
}
}, { default: () => '查看' })
}
})
})
//判断是否是A股交易时间
@@ -171,12 +237,20 @@ function isTradingTime() {
}
function AddStock(){
if (!data?.code) {
message.error("请输入有效股票代码");
return;
}
if (!stocks.value.includes(data.code)) {
stocks.value.push(data.code)
Follow(data.code).then(result => {
message.success(result)
if(result==="关注成功"){
stocks.value.push(data.code)
message.success(result)
monitor();
}else{
message.error(result)
}
})
monitor()
}else{
message.error("已经关注了")
}
@@ -389,9 +463,9 @@ function SendMessage(result,type){
// SendDingDingMessage(msg,result["股票代码"])
SendDingDingMessageByType(msg,result["股票代码"],type)
}
function aiCheckStock(stock,stockCode){
function aiReCheckStock(stock,stockCode) {
data.airesult=""
data.time=""
data.name=stock
data.code=stockCode
data.loading=true
@@ -402,6 +476,38 @@ function aiCheckStock(stock,stockCode){
NewChatStream(stock,stockCode)
}
function aiCheckStock(stock,stockCode){
GetAIResponseResult(stockCode).then(result => {
if(result.content){
data.name=stock
data.code=stockCode
data.loading=false
modalShow4.value=true
data.airesult=result.content
const date = new Date(result.CreatedAt);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
data.time=formattedDate
}else{
data.airesult=""
data.time=""
data.name=stock
data.code=stockCode
data.loading=true
modalShow4.value=true
message.loading("ai检测中...",{
duration: 0,
})
NewChatStream(stock,stockCode)
}
})
}
function getTypeName(type){
switch (type)
{
@@ -426,8 +532,8 @@ function getHeight() {
<template>
<n-grid :x-gap="8" :cols="3" :y-gap="8" >
<n-gi v-for="result in sortedResults" >
<n-card :data-code="result['股票代码']" :bordered="false" :title="result['股票名称']" :closable="false" @close="removeMonitor(result['股票代码'],result['股票名称'],result.key)">
<n-gi v-for="result in sortedResults" style="margin-left: 2px" onmouseover="this.style.border='1px solid #3498db' " onmouseout="this.style.border='0px'">
<n-card :data-code="result['股票代码']" :bordered="false" :title="result['股票名称']" :closable="false" @close="removeMonitor(result['股票代码'],result['股票名称'],result.key)">
<n-grid :cols="1" :y-gap="6">
<n-gi>
<n-text :type="result.type" >
@@ -459,7 +565,9 @@ function getHeight() {
<n-button size="tiny" secondary type="primary" @click="removeMonitor(result['股票代码'],result['股票名称'],result.key)">
取消关注
</n-button>&nbsp;
<n-button size="tiny" v-if="data.openAiEnable" secondary type="warning" @click="aiCheckStock(result['股票名称'],result['股票代码'])"> AI分析 </n-button>
<n-button size="tiny" v-if="data.openAiEnable" secondary type="warning" @click="aiCheckStock(result['股票名称'],result['股票代码'])">
AI分析
</n-button>
</template>
<template #footer>
@@ -550,13 +658,28 @@ function getHeight() {
<n-image :src="data.kURL" />
</n-modal>
<n-modal transform-origin="center" v-model:show="modalShow4" preset="card" style="width: 800px;height: 480px" :title="'['+data.name+']AI分析结果'" >
<n-modal transform-origin="center" v-model:show="modalShow4" preset="card" style="width: 800px;height: 500px" :title="'['+data.name+']AI分析结果'" >
<n-spin size="small" :show="data.loading">
<MdPreview ref="mdPreviewRef" style="height: 380px" :modelValue="data.airesult" :theme="'dark'"/>
<MdPreview ref="mdPreviewRef" style="height: 380px;text-align: left" :modelValue="data.airesult" :theme="'dark'"/>
</n-spin>
<template #header-extra>
</template>
<template #footer>
<n-flex justify="space-between">
<n-text type="error" v-if="data.time" >分析时间:{{data.time}}</n-text>
<n-button size="tiny" type="warning" @click="aiReCheckStock(data.name,data.code)">再次分析</n-button>
</n-flex>
</template>
</n-modal>
</template>
<style scoped>
.md-editor-preview h3{
text-align: center !important;
}
.md-editor-preview p{
text-align: left !important;
}
</style>

View File

@@ -2,10 +2,12 @@ import { createMemoryHistory, createRouter } from 'vue-router'
import stockView from '../components/stock.vue'
import settingsView from '../components/settings.vue'
import about from "../components/about.vue";
const routes = [
{ path: '/', component: stockView,name: 'stock' },
{ path: '/settings/:id', component: settingsView,name: 'settings' },
{ path: '/about', component: about,name: 'about' },
]
const router = createRouter({

View File

@@ -1,21 +1,26 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import {models} from '../models';
import {data} from '../models';
export function Follow(arg1:string):Promise<string>;
export function GetAIResponseResult(arg1:string):Promise<models.AIResponseResult>;
export function GetConfig():Promise<data.Settings>;
export function GetFollowList():Promise<Array<data.FollowedStock>>;
export function GetStockList(arg1:string):Promise<Array<data.StockBasic>>;
export function GetVersionInfo():Promise<models.VersionInfo>;
export function Greet(arg1:string):Promise<data.StockInfo>;
export function NewChat(arg1:string):Promise<string>;
export function NewChatStream(arg1:string,arg2:string):Promise<void>;
export function SaveAIResponseResult(arg1:string,arg2:string,arg3:string):Promise<void>;
export function SendDingDingMessage(arg1:string,arg2:string):Promise<string>;
export function SendDingDingMessageByType(arg1:string,arg2:string,arg3:number):Promise<string>;

View File

@@ -6,6 +6,10 @@ export function Follow(arg1) {
return window['go']['main']['App']['Follow'](arg1);
}
export function GetAIResponseResult(arg1) {
return window['go']['main']['App']['GetAIResponseResult'](arg1);
}
export function GetConfig() {
return window['go']['main']['App']['GetConfig']();
}
@@ -18,18 +22,22 @@ export function GetStockList(arg1) {
return window['go']['main']['App']['GetStockList'](arg1);
}
export function GetVersionInfo() {
return window['go']['main']['App']['GetVersionInfo']();
}
export function Greet(arg1) {
return window['go']['main']['App']['Greet'](arg1);
}
export function NewChat(arg1) {
return window['go']['main']['App']['NewChat'](arg1);
}
export function NewChatStream(arg1, arg2) {
return window['go']['main']['App']['NewChatStream'](arg1, arg2);
}
export function SaveAIResponseResult(arg1, arg2, arg3) {
return window['go']['main']['App']['SaveAIResponseResult'](arg1, arg2, arg3);
}
export function SendDingDingMessage(arg1, arg2) {
return window['go']['main']['App']['SendDingDingMessage'](arg1, arg2);
}

View File

@@ -73,7 +73,9 @@ export namespace data {
openAiModelName: string;
openAiMaxTokens: number;
openAiTemperature: number;
openAiApiTimeOut: number;
prompt: string;
checkUpdate: boolean;
static createFrom(source: any = {}) {
return new Settings(source);
@@ -97,7 +99,9 @@ export namespace data {
this.openAiModelName = source["openAiModelName"];
this.openAiMaxTokens = source["openAiMaxTokens"];
this.openAiTemperature = source["openAiTemperature"];
this.openAiApiTimeOut = source["openAiApiTimeOut"];
this.prompt = source["prompt"];
this.checkUpdate = source["checkUpdate"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
@@ -325,3 +329,104 @@ export namespace data {
}
export namespace models {
export class AIResponseResult {
ID: number;
// Go type: time
CreatedAt: any;
// Go type: time
UpdatedAt: any;
// Go type: gorm
DeletedAt: any;
stockCode: string;
stockName: string;
content: string;
IsDel: number;
static createFrom(source: any = {}) {
return new AIResponseResult(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.ID = source["ID"];
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
this.DeletedAt = this.convertValues(source["DeletedAt"], null);
this.stockCode = source["stockCode"];
this.stockName = source["stockName"];
this.content = source["content"];
this.IsDel = source["IsDel"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class VersionInfo {
ID: number;
// Go type: time
CreatedAt: any;
// Go type: time
UpdatedAt: any;
// Go type: gorm
DeletedAt: any;
version: string;
content: string;
icon: string;
buildTimeStamp: number;
IsDel: number;
static createFrom(source: any = {}) {
return new VersionInfo(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.ID = source["ID"];
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
this.DeletedAt = this.convertValues(source["DeletedAt"], null);
this.version = source["version"];
this.content = source["content"];
this.icon = source["icon"];
this.buildTimeStamp = source["buildTimeStamp"];
this.IsDel = source["IsDel"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
}

BIN
img.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -14,6 +14,7 @@ import (
"github.com/wailsapp/wails/v2/pkg/runtime"
"go-stock/backend/data"
"go-stock/backend/db"
"go-stock/backend/models"
"log"
"os"
goruntime "runtime"
@@ -33,6 +34,10 @@ var icon2 []byte
var stocksBin []byte
//go:generate cp -R ./data ./build/bin
var Version string
var VersionCommit string
func main() {
checkDir("data")
db.Init("")
@@ -41,6 +46,7 @@ func main() {
db.Dao.AutoMigrate(&data.FollowedStock{})
db.Dao.AutoMigrate(&data.IndexBasic{})
db.Dao.AutoMigrate(&data.Settings{})
db.Dao.AutoMigrate(&models.AIResponseResult{})
if stocksBin != nil && len(stocksBin) > 0 {
go initStockData()
@@ -82,7 +88,8 @@ func main() {
//FileMenu.AddText("退出", keys.CmdOrCtrl("q"), func(_ *menu.CallbackData) {
// runtime.Quit(app.ctx)
//})
logger.NewDefaultLogger().Info("version: " + Version)
logger.NewDefaultLogger().Info("commit: " + VersionCommit)
// Create application with options
err := wails.Run(&options.App{
Title: "go-stock",