Compare commits

...

87 Commits

Author SHA1 Message Date
ArvinLovegood
f5c8f5d0ef refactor(mac):显示windows窗体(显示最大最小化按钮)
- 在 Mac 系统中添加编辑菜单
- 注释掉全屏和还原菜单项
- 移除无边框窗口设置
- 调整搜索框和表格样式
- 优化设置页面布局
2025-07-16 18:01:51 +08:00
ArvinLovegood
23d3566f31 feat(app):添加版本更新提示和自定义通知颜色
- 在版本检查无更新时发送通知
- 根据通知来源调整颜色:go-stock 为橙色,其他为蓝色
2025-07-16 12:57:13 +08:00
ArvinLovegood
052104b43a fix(app):修复初次安装软件时股票基础信息没有立即初始化的问题
- 将 CheckStockBaseInfo 方法的调用移到 CheckUpdate 方法之前
- 修改 cron定时任务,只在特定日期执行版本检查和股票基础信息检查
2025-07-16 12:31:40 +08:00
ArvinLovegood
93e8fb27b5 fix(app):修复初次安装软件时股票基础信息没有立即初始化的问题
- 将 CheckStockBaseInfo 方法的调用移到 CheckUpdate 方法之前
- 修改 cron定时任务,只在特定日期执行版本检查和股票基础信息检查
2025-07-16 12:16:15 +08:00
ArvinLovegood
25623d90d7 docs:隐藏 QQ 交流群 2 的链接
- 注释掉了 README.md 中 QQ 交流群 2 的链接
-保留了 QQ 交流群的链接
2025-07-16 09:27:34 +08:00
ArvinLovegood
8db94da233 feat(stock):更新股票基础信息并优化相关功能
- 添加 CheckStockBaseInfo 方法,用于更新股票基础信息
- 修改 domReady 方法,移除初始化股票数据的逻辑
- 更新 StockBasic、StockInfoHK 和 StockInfoUS 模型,添加行业代码和名称字段
- 修改 getDCStockInfo 方法,支持获取更详细的股票信息
- 添加 DCToTsCode 函数,用于将东财代码转换为 TS 代码
- 优化行业报告信息获取功能
2025-07-15 18:49:37 +08:00
ArvinLovegood
60e7d87918 docs(README): 更新 QQ 交流群描述
- 修改了 QQ交流群的描述,从"已满会定期清理,随缘入群"改为"定期清理,随缘入群"
- 此修改反映了群聊状态的更新,使得描述更加准确
2025-07-15 09:44:22 +08:00
ArvinLovegood
615b4d231a refactor(updater):优化软件更新提示内容和样式
- 修改更新提示内容,仅显示 commit message
- 调整通知窗口样式,增加文本对齐和颜色设置
- 更新 README 中的下载链接描述,区分 MACOS 绿色版和安装版
2025-07-14 14:04:42 +08:00
ArvinLovegood
490a3c0847 feat(app):增加恒生科技指数并优化版本更新提示信息
- 在市场组件中添加恒生科技指数选项
- 更新版本时增加提交信息显示
- 优化新版本下载失败提示信息
2025-07-14 11:34:01 +08:00
ArvinLovegood
38f83674ef feat(data):添加PPI和PMI
- 新增 GetPPI 和 GetPMI 函数,用于获取工业品出厂价格指数和采购经理人指数数据
- 添加相关测试用例,验证 PPI 和 PMI接口的功能
- 更新模型结构,支持 PPI 和 PMI 数据
- 在 OpenAI API 中调用新增的 PPI 和 PMI 接口,丰富市场数据信息
2025-07-12 13:31:38 +08:00
ArvinLovegood
d26c4bc986 feat(data):AI市场资讯总结添加国内宏观经济数据(GDP和CPI,后期陆续会加其他数据)
- 在 openai_api.go 中添加 GDP 和 CPI 数据的获取和格式化输出
- 在 market_news_api_test.go 中更新相关测试函数
- 在 struct_to_markdown.go 中新增 MarkdownTableWithTitle 函数用于添加标题
2025-07-11 18:58:31 +08:00
ArvinLovegood
7e919376b5 feat(data):添加GDP和CPI数据接口
- 实现了 GetGDP 和 GetCPI 方法,获取国内生产总值和居民消费价格指数数据
- 新增 GDP 和 CPI 数据模型
- 更新相关测试用例
2025-07-11 18:39:24 +08:00
ArvinLovegood
1d9ef724e6 feat(frontend):重命名"指标行情"标签为"重大指数"
- 重命名"指标行情"标签为"重大指数"
- 新增多个重大指数的行情图表,包括:
  - 科创芯片(sh000685)
  - 证券龙头(sz399437)
  - 高端装备(sz399437)  - 中证银行(sz399986)
  - 上证医药(sh000037)
- 统一设置为暗黑主题
2025-07-11 18:05:45 +08:00
ArvinLovegood
8e982d4430 refactor(main):注释掉隐藏到托盘区的功能
-移除了对 runtime 包的导入
- 注释掉了相关代码块
2025-07-11 17:53:55 +08:00
ArvinLovegood
a67559831a style(frontend):优化K线图和市场组件的拖拽体验
- 在 KLineChart 组件中添加 --wails-draggable:no-drag 样式,禁止拖拽
- 在 Market 组件中调整拖拽样式应用位置,提高用户体验
- 优化 Market 组件的模板结构,移除冗余样式
2025-07-11 17:51:06 +08:00
ArvinLovegood
9718d3311d feat:修改隐藏窗口快捷键为Ctrl+Z
- 将前端 App.vue 文件中的隐藏窗口快捷键从 Ctrl+H 修改为 Ctrl+Z
- 在后端 main.go 文件中添加了隐藏窗口的功能,快捷键也为 Ctrl+Z
- 删除了 main.go 文件中注释掉的隐藏和显示窗口的代码
2025-07-11 16:42:09 +08:00
SparkMemory
789e7427ce
Merge pull request #92 from GiCo001/dev-darwin
feat(app): 调整darwin版本的窗口,显示toolbar
2025-07-11 16:18:00 +08:00
Gico001
801aa14c7a feat(app): 调整darwin版本的窗口,显示toolbar 2025-07-11 11:07:46 +08:00
ArvinLovegood
f5c621fbcc refactor(frontend): 优化 Windows 平台下窗口打开方式
- 在 stock.vue 中添加了对 Windows 平台下窗口打开方式的特殊处理
- 指定窗口大小和位置,隐藏菜单栏和工具栏,以实现更佳的用户体验
2025-07-10 17:31:42 +08:00
SparkMemory
119f0f8aa7
Merge pull request #90 from GiCo001/dev-darwin
feat(app): 兼容darwin版本浏览跳转,保存图片文件等功能
2025-07-10 16:31:17 +08:00
ArvinLovegood
fe814974fd feat(util): 添加结构体到 Markdown 表格的转换功能
- 实现了 MarkdownTable 函数,可以将结构体或结构体切片转换为 Markdown 表格格式
- 添加了相关辅助函数,如 markdownSingleStruct、markdownStructSlice、shouldSkip 等
- 示例结构体 User 和 Address 用于演示功能
- 新增 struct_to_markdown_test.go 文件进行测试验证
2025-07-10 16:30:25 +08:00
ArvinLovegood
dd3c231637 feat(data):添加获取国内生产总值(GDP)功能
- 实现了从东财数据中心获取GDP数据的功能
- 新增GDP数据结构用于解析获取的数据
- 添加了获取GDP数据的测试用例
2025-07-10 16:29:40 +08:00
ArvinLovegood
e05ff94aba fix(main):修复不能粘贴的大BUG
- 注释掉了显示搜索框、隐藏搜索框和刷新数据的菜单项
- 注释掉了隐藏到托盘区和显示窗口的菜单项(仅限 Windows)
- 添加了编辑菜单
2025-07-10 16:18:44 +08:00
Gico001
bbd4bb5b48 feat(app): 兼容darwin版本浏览跳转,保存图片文件等功能 2025-07-10 14:49:10 +08:00
ArvinLovegood
58f3009902 feat(frontend):添加微信公众号二维码并更新相关页面
- 在 about.vue 中添加微信公众号二维码图片
- 在 AppInfo 结构中添加 Wxgzh 字段用于存储微信公众号二维码链接
- 在 main.go 中嵌入微信公众号二维码图片
- 在 models 和 TypeScript 中添加相应字段支持微信公众号二维码
2025-07-10 10:01:40 +08:00
ArvinLovegood
c6b841fb8f feat(sponsor):添加新的赞助计划并实现赞助码验证功能
- 在 about.vue 和 README.md 中添加新的 VIP2 赞助计划
- 在 App.d.ts 和 App.js 中添加 CheckSponsorCode函数
- 在 app.go 中实现 CheckSponsorCode 方法,用于验证赞助码
- 在 settings.vue 中集成赞助码验证功能,更新配置时进行验证
- 优化赞助码输入界面,添加验证按钮
2025-07-09 18:13:00 +08:00
ArvinLovegood
2b28390414 feat(sponsor):添加新的赞助计划并实现赞助码验证功能
- 在 about.vue 和 README.md 中添加新的 VIP2 赞助计划
- 在 App.d.ts 和 App.js 中添加 CheckSponsorCode函数
- 在 app.go 中实现 CheckSponsorCode 方法,用于验证赞助码
- 在 settings.vue 中集成赞助码验证功能,更新配置时进行验证
- 优化赞助码输入界面,添加验证按钮
2025-07-09 18:11:53 +08:00
ArvinLovegood
7887dfed5e feat(sponsor):添加新的赞助计划并实现赞助码验证功能
- 在 about.vue 和 README.md 中添加新的 VIP2 赞助计划
- 在 App.d.ts 和 App.js 中添加 CheckSponsorCode函数
- 在 app.go 中实现 CheckSponsorCode 方法,用于验证赞助码
- 在 settings.vue 中集成赞助码验证功能,更新配置时进行验证
- 优化赞助码输入界面,添加验证按钮
2025-07-09 18:06:46 +08:00
ArvinLovegood
a4c98933a4 feat(sponsor):添加新的赞助计划并实现赞助码验证功能
- 在 about.vue 和 README.md 中添加新的 VIP2 赞助计划
- 在 App.d.ts 和 App.js 中添加 CheckSponsorCode函数
- 在 app.go 中实现 CheckSponsorCode 方法,用于验证赞助码
- 在 settings.vue 中集成赞助码验证功能,更新配置时进行验证
- 优化赞助码输入界面,添加验证按钮
2025-07-09 17:54:32 +08:00
ArvinLovegood
ad63ffff7f feat(sponsor):添加新的赞助计划并实现赞助码验证功能
- 在 about.vue 和 README.md 中添加新的 VIP2 赞助计划
- 在 App.d.ts 和 App.js 中添加 CheckSponsorCode函数
- 在 app.go 中实现 CheckSponsorCode 方法,用于验证赞助码
- 在 settings.vue 中集成赞助码验证功能,更新配置时进行验证
- 优化赞助码输入界面,添加验证按钮
2025-07-09 17:52:58 +08:00
ArvinLovegood
1ccc2f8b1f feat(frontend):添加支持开源赞助计划
- 在关于页面中增加支持开源赞助计划的表格
- 列出不同赞助等级及其对应的权益说明
- 旨在鼓励用户支持项目发展,提供不同级别的赞助选项
2025-07-09 15:38:52 +08:00
ArvinLovegood
dc5483aa07 feat(frontend):添加支持开源赞助计划
- 在关于页面中增加支持开源赞助计划的表格
- 列出不同赞助等级及其对应的权益说明
- 旨在鼓励用户支持项目发展,提供不同级别的赞助选项
2025-07-09 14:54:26 +08:00
ArvinLovegood
8c82ba4a38 feat(frontend):添加支持开源赞助计划
- 在关于页面中增加支持开源赞助计划的表格
- 列出不同赞助等级及其对应的权益说明
- 旨在鼓励用户支持项目发展,提供不同级别的赞助选项
2025-07-09 14:18:30 +08:00
ArvinLovegood
fd905ff278 feat(frontend):添加支持开源赞助计划
- 在关于页面中增加支持开源赞助计划的表格
- 列出不同赞助等级及其对应的权益说明
- 旨在鼓励用户支持项目发展,提供不同级别的赞助选项
2025-07-09 14:16:31 +08:00
ArvinLovegood
6ec0f5fbe0 feat(frontend):添加支持开源赞助计划
- 在关于页面中增加支持开源赞助计划的表格
- 列出不同赞助等级及其对应的权益说明
- 旨在鼓励用户支持项目发展,提供不同级别的赞助选项
2025-07-09 12:31:53 +08:00
ArvinLovegood
32706fb4dc feat(frontend):添加支持开源赞助计划
- 在关于页面中增加支持开源赞助计划的表格
- 列出不同赞助等级及其对应的权益说明
- 旨在鼓励用户支持项目发展,提供不同级别的赞助选项
2025-07-09 12:30:13 +08:00
ArvinLovegood
2cb661734f feat(frontend):添加支持开源赞助计划
- 在关于页面中增加支持开源赞助计划的表格
- 列出不同赞助等级及其对应的权益说明
- 旨在鼓励用户支持项目发展,提供不同级别的赞助选项
2025-07-09 12:27:26 +08:00
ArvinLovegood
4fab910340 feat(frontend):添加支持开源赞助计划
- 在关于页面中增加支持开源赞助计划的表格
- 列出不同赞助等级及其对应的权益说明
- 旨在鼓励用户支持项目发展,提供不同级别的赞助选项
2025-07-09 12:27:08 +08:00
ArvinLovegood
84e4ba8474 feat(update):支持macOS系统更新
- 修改了更新检查逻辑,排除 macOS 系统
- 为 macOS 系统添加了专门的下载链接
- 优化了版本更新提示信息的显示
2025-07-09 11:19:33 +08:00
ArvinLovegood
76a44fae32 feat(update):支持macOS系统更新
- 修改了更新检查逻辑,排除 macOS 系统
- 为 macOS 系统添加了专门的下载链接
- 优化了版本更新提示信息的显示
2025-07-09 10:13:25 +08:00
ArvinLovegood
7ea974f1a6 style(market):为指标行情标签添加不可拖动样式
- 在指标行情标签上添加 style属性,设置 --wails-dragable 为 no-drag
- 这个修改可以防止用户在该标签页中进行不必要的拖动操作,提升用户体验
2025-07-09 09:52:19 +08:00
ArvinLovegood
7ea160b6b5 feat(update):优化软件更新逻辑
- 增加对操作系统类型的判断,非 Windows 系统不执行更新
- 优化更新版本信息的传递方式
-重构代码,提高可读性和可维护性
2025-07-09 09:03:11 +08:00
ArvinLovegood
c2f260c613 feat(update):优化软件更新逻辑
- 增加对操作系统类型的判断,非 Windows 系统不执行更新
- 优化更新版本信息的传递方式
-重构代码,提高可读性和可维护性
2025-07-08 21:08:49 +08:00
ArvinLovegood
2d224ccfc4 feat(update):优化软件更新逻辑
- 增加对操作系统类型的判断,非 Windows 系统不执行更新
- 优化更新版本信息的传递方式
-重构代码,提高可读性和可维护性
2025-07-08 18:53:36 +08:00
ArvinLovegood
a66f2156f1 feat(update):实现软件自动更新功能
- 新增自动检查和下载最新版本的功能
- 使用 go-update 库进行软件更新
- 增加新版本推送通知和更新结果通知
- 优化错误处理和日志记录
2025-07-08 18:45:49 +08:00
ArvinLovegood
e90727773f refactor(frontend): 调整股市通组件内容
-将百度股市通替换为选股通
- 注释掉百度股市通和摸鱼选项
- 添加 naive-ui 组件导入
2025-07-08 17:49:52 +08:00
SparkMemory
89dcb713be
Potential fix for code scanning alert no. 4: Clear-text logging of sensitive information
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-07-08 12:00:46 +08:00
SparkMemory
6f4b21207d
Potential fix for code scanning alert no. 5: Clear-text logging of sensitive information
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-07-08 11:57:29 +08:00
SparkMemory
f51e3d863a
Potential fix for code scanning alert no. 6: Clear-text logging of sensitive information
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-07-08 11:51:26 +08:00
ArvinLovegood
c180c2a5f8 feat(stock):优化股票迷你图刷新逻辑
- 在 Stock 组件中添加 lastPrice 属性,传递当前价格给 StockSparkLine 组件
- 在 StockSparkLine 组件中接收 lastPrice 属性,并使用它来更新 K 线图数据
- 优化 StockSparkLine 组件的渲染逻辑,使用 onMounted 和 watchEffect
2025-07-08 10:58:01 +08:00
ArvinLovegood
3ba18e8ef2 feat(stock):优化股票迷你图刷新逻辑
- 在 Stock 组件中添加 lastPrice 属性,传递当前价格给 StockSparkLine 组件
- 在 StockSparkLine 组件中接收 lastPrice 属性,并使用它来更新 K 线图数据
- 优化 StockSparkLine 组件的渲染逻辑,使用 onMounted 和 watchEffect
2025-07-08 10:50:04 +08:00
ArvinLovegood
f0314187e5 fix(stock):修正开盘价数据源并优化迷你分时图渲染逻辑
- 将 stock.vue 中的开盘价数据源从"今日开盘价"改为"昨日收盘价"
- 在 stockSparkLine.vue 中修改图表初始化方式,使用 document.getElementById 获取图表容器
-为 stockSparkLine.vue 中的图表容器添加唯一的 id 属性,以确保正确渲染多个图表
2025-07-07 17:57:17 +08:00
ArvinLovegood
6440885688 docs(README): 更新更新日志并添加新功能说明
- 新增卡片添加迷你分时图功能
- 新增MacOs支持- 更新现有功能说明
2025-07-07 17:30:37 +08:00
ArvinLovegood
2dd4f072b2 feat(frontend):添加股票分钟线迷你图表并优化界面
- 新增 StockSparkLine 组件,用于显示股票分钟迷你线图表
- 在股票页面中集成 StockSparkLine 组件
- 为 about、market 和 settings 页面的主体元素添加可拖拽样式
- 优化股票页面布局,调整网格列数和对齐方式
2025-07-07 17:17:49 +08:00
ArvinLovegood
b5843bcdb8 refactor(frontend):重构前端路由并优化设置页面布局
- 修改路由配置,使用 createWebHashHistory 替代 createWebHistory- 重命名部分组件以提高代码一致性
- 优化设置页面布局,使用卡片组件分类显示不同设置项
- 调整提示词模板展示方式,增加警告提示
2025-07-07 10:50:08 +08:00
ArvinLovegood
c65d0b79f4 ci: 更新 GitHub Actions 工作流
- 添加对 '*-dev' 标签的匹配,以支持开发版本的构建
-移除 Windows ARM64 构建配置,简化构建流程
2025-07-06 20:58:24 +08:00
ArvinLovegood
92bb0097cd ci(workflow):添加windows/arm64平台的构建
- 在 GitHub Actions 工作流中增加了 windows/arm64 平台的构建任务
- 新增 go-stock-windows-arm64.exe 可执行文件的生成
- 设置了针对 arm64 架构的构建参数
2025-07-06 18:38:47 +08:00
ArvinLovegood
0c6bd7292e feat(frontend):添加选股通至股票热图组件
- 在 stockhotmap.vue 中新增了一个名为 "选股通" 的标签页
- 嵌入了选股通网站的 URL,提供股票选择功能
2025-07-05 17:35:57 +08:00
ArvinLovegood
eeae1f77f4 docs(frontend):更新开发者列表(感谢Gico的贡献macos支持)
- 在 about.vue 组件中添加了新的开发者 @Gico
- 保持了开发者列表的最新和准确
2025-07-05 08:24:42 +08:00
ArvinLovegood
707e353ea8 docs(frontend):更新开发者列表(感谢Gico的贡献macos支持)
- 在 about.vue 组件中添加了新的开发者 @Gico
- 保持了开发者列表的最新和准确
2025-07-05 08:21:51 +08:00
ArvinLovegood
5ee14b703c build:更新wails构建动作版本,支持macos自动编译发布
- 将 ArvinLovegood/wails-build-action 版本从 v3.5 升级到 v3.6
- 保持其他配置不变,仅更新动作版本
2025-07-05 08:12:51 +08:00
ArvinLovegood
04a46108f3 build(ci):更新GitHubActions工作流
- 将 'App' 任务重命名为 'go-stock-darwin-universal'
- 更新平台配置为 'darwin/universal'- 保持操作系统为 'macos-latest'
2025-07-05 08:08:50 +08:00
ArvinLovegood
48a601f776 feat(app):添加macos平台支持并优化应用
- 导入 Windows 特定的库,如 systray 和 toast
- 更新 go.mod 和 go.sum 文件以包含新库
- 修改 App.d.ts 和 App.js 以支持 Windows 功能
- 更新 GitHub Actions以构建 Windows 版本
- 优化 Windows 平台上的浏览器检查逻辑
2025-07-05 08:01:40 +08:00
ArvinLovegood
e249933f8b feat(app):添加macos平台支持并优化应用
- 导入 Windows 特定的库,如 systray 和 toast
- 更新 go.mod 和 go.sum 文件以包含新库
- 修改 App.d.ts 和 App.js 以支持 Windows 功能
- 更新 GitHub Actions以构建 Windows 版本
- 优化 Windows 平台上的浏览器检查逻辑
2025-07-05 07:54:51 +08:00
ArvinLovegood
a8bb2b5399 Merge branch 'dev-darwin' into dev 2025-07-05 07:37:47 +08:00
SparkMemory
16c89de792
Merge pull request #87 from GiCo001/dev-darwin
feat(app): 兼容darwin版本 #30
2025-07-04 18:00:03 +08:00
ming
71bfed3744 feat(app): 兼容darwin版本 #30 2025-07-04 17:56:28 +08:00
ArvinLovegood
edd1bf94f9 feat(frontend):添加名站优选功能并实现嵌入外部URL的通用组件
- 在 Market 组件中添加名站优选选项卡- 新增 EmbeddedUrl 组件用于嵌入外部网页
- 创建 Stockhotmap 组件,整合多个财经网站的热图和排行榜
2025-07-04 17:56:03 +08:00
ArvinLovegood
cfe1abb07f feat(data):优化数据处理和展示
- 重构市场新闻和事件日历的处理逻辑,使用 gjson 解析 JSON 数据
- 优化股票筛选工具的结果展示,生成 Markdown 表格
- 改进 K 线数据的处理和展示方式
- 调整 OpenAI API 调用逻辑,增加工具函数验证
2025-07-04 14:35:54 +08:00
ArvinLovegood
8b94e14ec9 refactor(frontend):更新股票页面链接格式
- 修改了 SelectStock 组件中股票名称的链接格式
- 从旧的 URL格式改为新的全屏图表 URL 格式
- 提高了用户体验,用户现在可以查看更详细的股票信息
2025-07-03 16:04:50 +08:00
ArvinLovegood
44e1093e8e refactor(app):优化工具调用
- 在 SearchStockByIndicators 和 GetStockKLine 函数的描述中移除了关于并行调用限制的说明
- 优化了函数描述,使其更加简洁和通用
2025-07-03 14:49:16 +08:00
ArvinLovegood
5e7f34652a feat(frontend):增加AI函数工具调用开关
- 在市场和股票组件中添加启用/禁用 AI 函数工具调用的开关
- 修改相关函数以支持 enableTools 参数,控制是否启用工具调用
- 优化 AI 总结新闻和聊天流函数,根据 enableTools 决定是否使用工具
2025-07-03 14:25:30 +08:00
ArvinLovegood
5b9a81d770 refactor(market-news):优化市场新闻API日志输出
- 注释掉 XUEQIUHotStock 的日志输出,减少不必要的日志信息
- 调整前端股票组件中的关注和 AI 分析逻辑
- 优化 AI 分析相关的用户交互和数据处理
- 美化模态框标题和按钮文案
2025-07-03 12:42:30 +08:00
ArvinLovegood
7021a59ee6 refactor(data):使用随机数替代固定参数以提高数据获取效率
- 在获取财经新闻列表时,使用随机数替代固定的数量参数
- 在搜索股票时,使用随机数替代固定的搜索数量
- 更新README
2025-07-03 10:22:22 +08:00
ArvinLovegood
433dea0772 feat(app):更新SearchStockByIndicators函数描述
- 扩展了函数描述,说明可以同时查询多个股票名称
- 调整了示例,使用多个股票名称进行查询
2025-07-02 18:57:16 +08:00
ArvinLovegood
378a5c47ba fix(backend):处理不支持函数调用的模型
- 当收到 "Function call is not supported for this model." 错误消息时
- 移除所有 "tool" 类型的消息和包含 "tool_calls" 的消息
- 使用剩余的消息重新调用 AskAi函数
2025-07-02 18:46:38 +08:00
ArvinLovegood
9a60736739 fix(backend):优化AI工具调用逻辑
- 当模型不支持函数调用时,重新使用 AI 模型询问
- 添加函数调用相关的消息结构
- 优化错误处理逻辑
2025-07-02 18:41:47 +08:00
ArvinLovegood
efe6365ea5 feat(frontend):增加股票名称点击事件打开行情页面
- 在 SelectStock 组件中添加了 openCenteredWindow 函数,用于打开居中窗口
- 点击股票名称时,会打开东方财富网的股票行情页面
- 优化了表格列的排序功能,支持数值类型的列进行排序- 调整了表格列的最小宽度,提高可读性
2025-07-02 17:40:15 +08:00
ArvinLovegood
062df80712 feat(frontend):添加热门策略功能并优化选股组件
- 在 App.d.ts 和 App.js 中添加 GetHotStrategy 函数
- 在 app_common.go 中实现 GetHotStrategy 方法
- 在 search_stock_api.go 中添加 HotStrategy 方法获取热门策略数据
- 更新 SelectStock.vue 组件,集成热门策略功能并优化界面布局
2025-07-02 16:10:02 +08:00
ArvinLovegood
528482db48 refactor(data):调整财联社电报新闻获取数量
- 将获取新闻列表的数量从 500 条调整为 100 条
- 这一改动可以减少接口请求的数据量,提高响应速度
2025-07-02 14:06:29 +08:00
ArvinLovegood
746e5ec98a feat(app):集成AI工具并优化股票数据获取
- 在 App 结构中添加 AiTools 字段,用于存储 AI 工具配置
- 新增 AddTools 函数,定义了两个 AI 工具:SearchStockByIndicators 和 GetStockKLine- 修改 NewApp 函数,初始化时加载 AI 工具配置- 更新相关函数,支持使用 AI 工具进行股票数据查询- 优化股票 K 线数据获取逻辑,增加对不同市场股票代码的支持
2025-07-02 12:29:57 +08:00
ArvinLovegood
6d345ae91d feat(app):集成AI工具并优化股票数据获取
- 在 App 结构中添加 AiTools 字段,用于存储 AI 工具配置
- 新增 AddTools 函数,定义了两个 AI 工具:SearchStockByIndicators 和 GetStockKLine- 修改 NewApp 函数,初始化时加载 AI 工具配置- 更新相关函数,支持使用 AI 工具进行股票数据查询- 优化股票 K 线数据获取逻辑,增加对不同市场股票代码的支持
2025-07-02 12:13:52 +08:00
ArvinLovegood
888a97e4d3 feat(app):更新SearchStockByIndicators工具函数描述并优化错误处理
- 更新 SearchStockByIndicators 函数描述,使其更准确地反映功能
- 在 Resp 结构中添加 Error 字段,用于处理错误信息
- 修改 openai_api.go 和 openai_api_test.go 中的错误处理逻辑
- 优化消息发送格式,提高错误信息的可读性
2025-07-02 10:25:03 +08:00
ArvinLovegood
ebeaf104bb feat(data):新增SummaryStockNewsStreamWithTools功能
- 在 OpenAi 结构中添加了新的方法 NewSummaryStockNewsStreamWithTools,支持使用工具进行股票分析
- 在 app.go 中调用了新方法,集成了股票搜索工具- 修改了 SearchStockApi 的 SearchStock 方法,增加了 pageSize 参数
- 更新了相关测试文件以适应新的功能
2025-07-01 19:27:59 +08:00
ArvinLovegood
b945a0e0e1 feat(frontend):在获取版本信息时,将官方声明添加到内容开头
- 修改了 App.vue 文件中的 onBeforeMount 钩子
- 在获取到官方声明后,将其添加到现有内容的开头
- 通过换行符分隔官方声明和原有内容
2025-07-01 12:43:03 +08:00
ArvinLovegood
111252f8bd feat(frontend):优化选股组件功能和界面
- 添加输入校验,提醒用户输入选股指标或要求
- 增加选股条件展示区域
- 优化按钮样式和布局
- 调整表格高度以适应新内容
2025-07-01 11:52:42 +08:00
ArvinLovegood
2e5ec6ace8 feat:添加官方声明内容
- 在 VersionInfo 结构中增加 OfficialStatement 字段
- 在前端 App.vue 中添加官方声明内容的获取和显示
- 在 main.go 中定义 OFFICIAL_STATEMENT 变量
- 更新 GitHub Actions 构建配置,添加 OFFICIAL_STATEMENT环境变量
2025-07-01 09:47:46 +08:00
43 changed files with 3677 additions and 999 deletions

View File

@ -5,10 +5,13 @@ 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:
@ -23,6 +26,9 @@ 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:
@ -38,7 +44,7 @@ jobs:
echo "::set-output name=commit_message::$commit_message"
- name: Build wails x go-stock
uses: ArvinLovegood/wails-build-action@v3.4
uses: ArvinLovegood/wails-build-action@v3.6
id: build
with:
build-name: ${{ matrix.build.name }}
@ -47,4 +53,6 @@ jobs:
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,8 +10,9 @@
![扫码_搜索联合传播样式-白色版.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]&#40;https://qm.qq.com/q/5mYiy6Yxh0&#41;)
- 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大模型构建的股票分析工具。
@ -23,6 +24,8 @@
### 📦 立即体验
- 安装版:[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)
### 💬 支持大模型/平台
@ -37,13 +40,21 @@
### <span style="color: #568DF4;">各位亲爱的朋友们,如果您对这个项目感兴趣,请先给我一个<i style="color: #EA2626;">star</i>吧,谢谢!</span>💕
- 优云智算by UCloud万卡规模4090免费用10小时新人注册另增50万tokens海量热门源项目镜像一键部署[注册链接](https://www.compshare.cn/image-community?ytag=GPU_YY-gh_gostock)
- 经测试目前硅基流动(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)
- 火山方舟新用户每个模型注册即送50万tokens[注册链接](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ)
- 硅基流动(siliconflow)注册即送2000万Tokens[注册链接](https://cloud.siliconflow.cn/i/foufCerk)
- Tushare大数据开放社区,免费提供各类金融数据,助力行业和量化研究(注意Tushare只需要120积分即可注册完成个人资料补充即可得120积分)[注册链接](https://tushare.pro/register?reg=701944)
- 软件快速迭代开发中,请大家优先测试和使用最新发布的版本。
- 欢迎大家提出宝贵的建议欢迎提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广告推广💖) |
## 🧩 重大功能开发计划
| 功能说明 | 状态 | 备注 |
|-----------------|----|----------------------------------------------------------------------------------------------------------|
@ -57,6 +68,10 @@
| 不再强制依赖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 添加热门股票、事件和话题功能

694
app.go
View File

@ -1,17 +1,20 @@
//go:build windows
package main
import (
"bytes"
"context"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"github.com/duke-git/lancet/v2/cryptor"
"github.com/inconshreveable/go-update"
"go-stock/backend/data"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"os"
"path/filepath"
"strings"
"time"
@ -21,21 +24,19 @@ import (
"github.com/duke-git/lancet/v2/mathutil"
"github.com/duke-git/lancet/v2/slice"
"github.com/duke-git/lancet/v2/strutil"
"github.com/energye/systray"
"github.com/go-resty/resty/v2"
"github.com/go-toast/toast"
"github.com/robfig/cron/v3"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/runtime"
"golang.org/x/sys/windows/registry"
)
// App struct
type App struct {
ctx context.Context
cache *freecache.Cache
cron *cron.Cron
cronEntrys map[string]cron.EntryID
ctx context.Context
cache *freecache.Cache
cron *cron.Cron
cronEntrys map[string]cron.EntryID
AiTools []data.Tool
SponsorInfo map[string]any
}
// NewApp creates a new App application struct
@ -44,68 +45,126 @@ func NewApp() *App {
cache := freecache.NewCache(cacheSize)
c := cron.New(cron.WithSeconds())
c.Start()
var tools []data.Tool
tools = AddTools(tools)
return &App{
cache: cache,
cron: c,
cronEntrys: make(map[string]cron.EntryID),
AiTools: tools,
}
}
// 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)
func AddTools(tools []data.Tool) []data.Tool {
tools = append(tools, data.Tool{
Type: "function",
Function: data.ToolFunction{
Name: "SearchStockByIndicators",
Description: "根据自然语言筛选股票,返回自然语言选股条件要求的股票所有相关数据。输入股票名称可以获取当前股票最新的股价交易数据和基础财务指标信息,多个股票名称使用,分隔。",
Parameters: data.FunctionParameters{
Type: "object",
Properties: map[string]any{
"words": map[string]any{
"type": "string",
"description": "选股自然语言。" +
"例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",
},
},
Required: []string{"words"},
},
},
})
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.Settings{}
setMap := optionalData[0].(map[string]interface{})
tools = append(tools, data.Tool{
Type: "function",
Function: data.ToolFunction{
Name: "GetStockKLine",
Description: "获取股票日K线数据。",
Parameters: data.FunctionParameters{
Type: "object",
Properties: map[string]any{
"days": map[string]any{
"type": "string",
"description": "日K数据条数",
},
"stockCode": map[string]any{
"type": "string",
"description": "股票代码A股sh,sz开头;港股hk开头,美股us开头",
},
},
Required: []string{"days", "stockCode"},
},
},
})
// 将 map 转换为 JSON 字节切片
jsonData, err := json.Marshal(setMap)
return tools
}
func (a *App) GetSponsorInfo() map[string]any {
return a.SponsorInfo
}
func (a *App) CheckSponsorCode(sponsorCode string) map[string]any {
sponsorCode = strutil.Trim(sponsorCode)
if sponsorCode != "" {
encrypted, err := hex.DecodeString(sponsorCode)
if err != nil {
logger.SugaredLogger.Errorf("Marshal error:%s", err.Error())
return
return map[string]any{
"code": 0,
"msg": "赞助码格式错误,请输入正确的赞助码!",
}
}
// 将 JSON 字节切片解析到结构体中
err = json.Unmarshal(jsonData, config)
key, err := hex.DecodeString(BuildKey)
if err != nil {
logger.SugaredLogger.Errorf("Unmarshal error:%s", err.Error())
return
logger.SugaredLogger.Error(err.Error())
return map[string]any{
"code": 0,
"msg": "版本错误,不支持赞助码!",
}
}
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)
decrypt := cryptor.AesEcbDecrypt(encrypted, key)
if decrypt == nil || len(decrypt) == 0 {
return map[string]any{
"code": 0,
"msg": "赞助码错误,请输入正确的赞助码!",
}
}
runtime.WindowReloadApp(ctx)
})
go systray.Run(func() {
onReady(a)
}, func() {
onExit(a)
})
logger.SugaredLogger.Infof(" application startup Version:%s", Version)
return map[string]any{
"code": 1,
"msg": "赞助码校验成功,感谢您的支持!",
}
} else {
return map[string]any{"code": 0, "message": "赞助码不能为空,请输入正确的赞助码!"}
}
}
func (a *App) CheckUpdate() {
sponsorCode := strutil.Trim(a.GetConfig().SponsorCode)
if sponsorCode != "" {
encrypted, err := hex.DecodeString(sponsorCode)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return
}
key, err := hex.DecodeString(BuildKey)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return
}
decrypt := string(cryptor.AesEcbDecrypt(encrypted, key))
err = json.Unmarshal([]byte(decrypt), &a.SponsorInfo)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return
}
}
releaseVersion := &models.GitHubReleaseVersion{}
_, err := resty.New().R().
SetResult(releaseVersion).
@ -116,6 +175,7 @@ func (a *App) CheckUpdate() {
}
logger.SugaredLogger.Infof("releaseVersion:%+v", releaseVersion.TagName)
if releaseVersion.TagName != Version {
tag := &models.Tag{}
_, err = resty.New().R().
SetResult(tag).
@ -123,6 +183,7 @@ func (a *App) CheckUpdate() {
if err == nil {
releaseVersion.Tag = *tag
}
commit := &models.Commit{}
_, err = resty.New().R().
SetResult(commit).
@ -131,28 +192,140 @@ func (a *App) CheckUpdate() {
releaseVersion.Commit = *commit
}
go runtime.EventsEmit(a.ctx, "updateVersion", releaseVersion)
if !(IsWindows() || IsMacOS()) {
go runtime.EventsEmit(a.ctx, "updateVersion", releaseVersion)
return
}
downloadUrl := fmt.Sprintf("https://github.com/ArvinLovegood/go-stock/releases/download/%s/go-stock-windows-amd64.exe", releaseVersion.TagName)
if IsMacOS() {
downloadUrl = fmt.Sprintf("https://github.com/ArvinLovegood/go-stock/releases/download/%s/go-stock-darwin-universal", releaseVersion.TagName)
}
sponsorCode = strutil.Trim(a.GetConfig().SponsorCode)
if sponsorCode != "" {
encrypted, err := hex.DecodeString(sponsorCode)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return
}
key, err := hex.DecodeString(BuildKey)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return
}
decrypt := string(cryptor.AesEcbDecrypt(encrypted, key))
err = json.Unmarshal([]byte(decrypt), &a.SponsorInfo)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return
}
vipStartTime, err := time.ParseInLocation("2006-01-02 15:04:05", a.SponsorInfo["vipStartTime"].(string), time.Local)
vipEndTime, err := time.ParseInLocation("2006-01-02 15:04:05", a.SponsorInfo["vipEndTime"].(string), time.Local)
vipAuthTime, err := time.ParseInLocation("2006-01-02 15:04:05", a.SponsorInfo["vipAuthTime"].(string), time.Local)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return
}
isVip := false
if time.Now().After(vipAuthTime) && time.Now().After(vipStartTime) && time.Now().Before(vipEndTime) {
isVip = true
}
if IsWindows() {
if isVip {
if a.SponsorInfo["winDownUrl"] == nil {
downloadUrl = fmt.Sprintf("https://gitproxy.click/https://github.com/ArvinLovegood/go-stock/releases/download/%s/go-stock-windows-amd64.exe", releaseVersion.TagName)
} else {
downloadUrl = a.SponsorInfo["winDownUrl"].(string)
}
} else {
downloadUrl = fmt.Sprintf("https://github.com/ArvinLovegood/go-stock/releases/download/%s/go-stock-windows-amd64.exe", releaseVersion.TagName)
}
}
if IsMacOS() {
if isVip {
if a.SponsorInfo["macDownUrl"] == nil {
downloadUrl = fmt.Sprintf("https://gitproxy.click/https://github.com/ArvinLovegood/go-stock/releases/download/%s/go-stock-darwin-universal", releaseVersion.TagName)
} else {
downloadUrl = a.SponsorInfo["macDownUrl"].(string)
}
} else {
downloadUrl = fmt.Sprintf("https://github.com/ArvinLovegood/go-stock/releases/download/%s/go-stock-darwin-universal", releaseVersion.TagName)
}
}
}
go runtime.EventsEmit(a.ctx, "newsPush", map[string]any{
"time": "发现新版本:" + releaseVersion.TagName,
"isRed": false,
"source": "go-stock",
"content": fmt.Sprintf("%s", commit.Message),
})
resp, err := resty.New().R().Get(downloadUrl)
if err != nil {
go runtime.EventsEmit(a.ctx, "newsPush", map[string]any{
"time": "新版本:" + releaseVersion.TagName,
"isRed": true,
"source": "go-stock",
"content": commit.Message + "\n新版本下载失败,请稍后重试或请前往 https://github.com/ArvinLovegood/go-stock/releases 手动下载替换文件。",
})
return
}
body := resp.Body()
if len(body) < 1024 {
go runtime.EventsEmit(a.ctx, "newsPush", map[string]any{
"time": "新版本:" + releaseVersion.TagName,
"isRed": true,
"source": "go-stock",
"content": commit.Message + "\n新版本下载失败,请稍后重试或请前往 https://github.com/ArvinLovegood/go-stock/releases 手动下载替换文件。",
})
return
}
err = update.Apply(bytes.NewReader(body), update.Options{})
if err != nil {
logger.SugaredLogger.Error("更新失败: ", err.Error())
go runtime.EventsEmit(a.ctx, "updateVersion", releaseVersion)
return
} else {
go runtime.EventsEmit(a.ctx, "newsPush", map[string]any{
"time": "新版本:" + releaseVersion.TagName,
"isRed": true,
"source": "go-stock",
"content": "版本更新完成,下次重启软件生效.",
})
}
} else {
go runtime.EventsEmit(a.ctx, "newsPush", map[string]any{
"time": "当前版本:" + Version,
"isRed": false,
"source": "go-stock",
"content": "当前版本无更新",
})
}
}
// domReady is called after front-end resources have been loaded
func (a *App) domReady(ctx context.Context) {
defer PanicHandler()
defer func() {
go runtime.EventsEmit(ctx, "loadingMsg", "done")
}()
if stocksBin != nil && len(stocksBin) > 0 {
go runtime.EventsEmit(a.ctx, "loadingMsg", "检查A股基础信息...")
go initStockData(a.ctx)
}
if stocksBinHK != nil && len(stocksBinHK) > 0 {
go runtime.EventsEmit(a.ctx, "loadingMsg", "检查港股基础信息...")
go initStockDataHK(a.ctx)
}
if stocksBinUS != nil && len(stocksBinUS) > 0 {
go runtime.EventsEmit(a.ctx, "loadingMsg", "检查美股基础信息...")
go initStockDataUS(a.ctx)
}
//if stocksBin != nil && len(stocksBin) > 0 {
// go runtime.EventsEmit(a.ctx, "loadingMsg", "检查A股基础信息...")
// go initStockData(a.ctx)
//}
//
//if stocksBinHK != nil && len(stocksBinHK) > 0 {
// go runtime.EventsEmit(a.ctx, "loadingMsg", "检查港股基础信息...")
// go initStockDataHK(a.ctx)
//}
//
//if stocksBinUS != nil && len(stocksBinUS) > 0 {
// go runtime.EventsEmit(a.ctx, "loadingMsg", "检查美股基础信息...")
// go initStockDataUS(a.ctx)
//}
updateBasicInfo()
// Add your action here
@ -258,11 +431,11 @@ func (a *App) domReady(ctx context.Context) {
//检查新版本
go func() {
a.CheckUpdate()
a.CheckStockBaseInfo(a.ctx)
a.cron.AddFunc("30 05 8,12,20 * * *", func() {
logger.SugaredLogger.Errorf("Checking for updates...")
a.CheckUpdate()
})
}()
//检查谷歌浏览器
@ -297,7 +470,74 @@ func (a *App) domReady(ctx context.Context) {
logger.SugaredLogger.Infof("domReady-cronEntrys:%+v", a.cronEntrys)
}
func (a *App) CheckStockBaseInfo(ctx context.Context) {
defer PanicHandler()
defer func() {
go runtime.EventsEmit(ctx, "loadingMsg", "done")
}()
stockBasics := &[]data.StockBasic{}
resty.New().R().
SetHeader("user", "go-stock").
SetResult(stockBasics).
Get("https://go-stock.sparkmemory.top/stock_basic.json")
for _, stock := range *stockBasics {
stockInfo := &data.StockBasic{
TsCode: stock.TsCode,
Name: stock.Name,
Symbol: stock.Symbol,
BKCode: stock.BKCode,
BKName: stock.BKName,
}
db.Dao.Model(&data.StockBasic{}).Where("ts_code = ?", stock.TsCode).First(stockInfo)
if stockInfo.ID == 0 {
db.Dao.Model(&data.StockBasic{}).Create(stockInfo)
} else {
db.Dao.Model(&data.StockBasic{}).Where("ts_code = ?", stock.TsCode).Updates(stockInfo)
}
}
stockHKBasics := &[]models.StockInfoHK{}
resty.New().R().
SetHeader("user", "go-stock").
SetResult(stockHKBasics).
Get("https://go-stock.sparkmemory.top/stock_base_info_hk.json")
for _, stock := range *stockHKBasics {
stockInfo := &models.StockInfoHK{
Code: stock.Code,
Name: stock.Name,
BKName: stock.BKName,
BKCode: stock.BKCode,
}
db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", stock.Code).First(stockInfo)
if stockInfo.ID == 0 {
db.Dao.Model(&models.StockInfoHK{}).Create(stockInfo)
} else {
db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", stock.Code).Updates(stockInfo)
}
}
stockUSBasics := &[]models.StockInfoUS{}
resty.New().R().
SetHeader("user", "go-stock").
SetResult(stockUSBasics).
Get("https://go-stock.sparkmemory.top/stock_base_info_us.json")
for _, stock := range *stockUSBasics {
stockInfo := &models.StockInfoUS{
Code: stock.Code,
Name: stock.Name,
BKName: stock.BKName,
BKCode: stock.BKCode,
}
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", stock.Code).First(stockInfo)
if stockInfo.ID == 0 {
db.Dao.Model(&models.StockInfoUS{}).Create(stockInfo)
} else {
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", stock.Code).Updates(stockInfo)
}
}
}
func (a *App) NewsPush(news *[]models.Telegraph) {
for _, telegraph := range *news {
//if telegraph.IsRed {
@ -311,7 +551,7 @@ func (a *App) AddCronTask(follow data.FollowedStock) func() {
return func() {
go runtime.EventsEmit(a.ctx, "warnMsg", "开始自动分析"+follow.Name+"_"+follow.StockCode)
ai := data.NewDeepSeekOpenAi(a.ctx)
msgs := ai.NewChatStream(follow.Name, follow.StockCode, "", nil)
msgs := ai.NewChatStream(follow.Name, follow.StockCode, "", nil, a.AiTools)
var res strings.Builder
chatId := ""
@ -469,49 +709,6 @@ func MonitorFundPrices(a *App) {
}
}
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 GetStockInfos(follows ...data.FollowedStock) *[]data.StockInfo {
stockInfos := make([]data.StockInfo, 0)
stockCodes := make([]string, 0)
@ -629,35 +826,6 @@ func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) {
}
}
// 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
}
}
// shutdown is called at application termination
func (a *App) shutdown(ctx context.Context) {
defer PanicHandler()
@ -747,8 +915,13 @@ func (a *App) SendDingDingMessageByType(message string, stockCode string, msgTyp
return data.NewDingDingAPI().SendDingDingMessage(message)
}
func (a *App) NewChatStream(stock, stockCode, question string, sysPromptId *int) {
msgs := data.NewDeepSeekOpenAi(a.ctx).NewChatStream(stock, stockCode, question, sysPromptId)
func (a *App) NewChatStream(stock, stockCode, question string, sysPromptId *int, enableTools bool) {
var msgs <-chan map[string]any
if enableTools {
msgs = data.NewDeepSeekOpenAi(a.ctx).NewChatStream(stock, stockCode, question, sysPromptId, a.AiTools)
} else {
msgs = data.NewDeepSeekOpenAi(a.ctx).NewChatStream(stock, stockCode, question, sysPromptId, []data.Tool{})
}
for msg := range msgs {
runtime.EventsEmit(a.ctx, "newChatStream", msg)
}
@ -764,48 +937,50 @@ func (a *App) GetAIResponseResult(stock string) *models.AIResponseResult {
func (a *App) GetVersionInfo() *models.VersionInfo {
return &models.VersionInfo{
Version: Version,
Icon: GetImageBase(icon),
Alipay: GetImageBase(alipay),
Wxpay: GetImageBase(wxpay),
Content: VersionCommit,
Version: Version,
Icon: GetImageBase(icon),
Alipay: GetImageBase(alipay),
Wxpay: GetImageBase(wxpay),
Wxgzh: GetImageBase(wxgzh),
Content: VersionCommit,
OfficialStatement: OFFICIAL_STATEMENT,
}
}
// checkChromeOnWindows 在 Windows 系统上检查谷歌浏览器是否安装
func checkChromeOnWindows() 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()
_, _, err = key.GetValue("Path", nil)
return err == nil
}
// checkEdgeOnWindows 在 Windows 系统上检查Edge浏览器是否安装并返回安装路径
func checkEdgeOnWindows() (string, bool) {
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")
if err != nil {
return "", false
}
return path, true
}
//// checkChromeOnWindows 在 Windows 系统上检查谷歌浏览器是否安装
//func checkChromeOnWindows() 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()
// _, _, err = key.GetValue("Path", nil)
// return err == nil
//}
//
//// checkEdgeOnWindows 在 Windows 系统上检查Edge浏览器是否安装并返回安装路径
//func checkEdgeOnWindows() (string, bool) {
// 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")
// if err != nil {
// return "", false
// }
// return path, true
//}
func GetImageBase(bytes []byte) string {
return "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(bytes)
@ -862,44 +1037,6 @@ func onExit(a *App) {
//runtime.Quit(a.ctx)
}
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")
})
}
func (a *App) UpdateConfig(settings *data.Settings) string {
//logger.SugaredLogger.Infof("UpdateConfig:%+v", settings)
if settings.RefreshInterval > 0 {
@ -938,15 +1075,6 @@ func (a *App) ExportConfig() string {
}
return "导出成功:" + file
}
func getScreenResolution() (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), nil
}
func (a *App) ShareAnalysis(stockCode, stockName string) string {
//http://go-stock.sparkmemory.top:16688/upload
@ -1034,22 +1162,6 @@ func (a *App) SetStockAICron(cronText, stockCode string) {
a.cronEntrys[stockCode] = id
}
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 (a *App) AddGroup(group data.Group) string {
ok := data.NewStockGroupApi(db.Dao).AddGroup(group)
if ok {
@ -1127,8 +1239,14 @@ func (a *App) GlobalStockIndexes() map[string]any {
return data.NewMarketNewsApi().GlobalStockIndexes(30)
}
func (a *App) SummaryStockNews(question string, sysPromptId *int) {
msgs := data.NewDeepSeekOpenAi(a.ctx).NewSummaryStockNewsStream(question, sysPromptId)
func (a *App) SummaryStockNews(question string, sysPromptId *int, enableTools bool) {
var msgs <-chan map[string]any
if enableTools {
msgs = data.NewDeepSeekOpenAi(a.ctx).NewSummaryStockNewsStreamWithTools(question, sysPromptId, a.AiTools)
} else {
msgs = data.NewDeepSeekOpenAi(a.ctx).NewSummaryStockNewsStream(question, sysPromptId)
}
for msg := range msgs {
runtime.EventsEmit(a.ctx, "summaryStockNews", msg)
}
@ -1152,3 +1270,81 @@ func (a *App) GetStockMoneyTrendByDay(stockCode string, days int) []map[string]a
slice.Reverse(res)
return res
}
// OpenURL
//
// @Description: 跨平台打开默认浏览器
// @receiver a
// @param url
func (a *App) OpenURL(url string) {
runtime.BrowserOpenURL(a.ctx, url)
}
// SaveImage
//
// @Description: 跨平台保存图片
// @receiver a
// @param name
// @param base64Data
// @return error
func (a *App) SaveImage(name, base64Data string) string {
// 打开保存文件对话框
filePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
Title: "保存图片",
DefaultFilename: name + "AI分析.png",
Filters: []runtime.FileFilter{
{
DisplayName: "PNG 图片",
Pattern: "*.png",
},
},
})
if err != nil || filePath == "" {
return "文件路径,无法保存。"
}
// 解码并保存
decodeString, err := base64.StdEncoding.DecodeString(base64Data)
if err != nil {
return "文件内容异常,无法保存。"
}
err = os.WriteFile(filepath.Clean(filePath), decodeString, 0777)
if err != nil {
return "保存结果异常,无法保存。"
}
return filePath
}
// SaveWordFile
//
// @Description: // 跨平台保存word
// @receiver a
// @param filename
// @param base64Data
// @return error
func (a *App) SaveWordFile(filename string, base64Data string) string {
// 弹出保存文件对话框
filePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
Title: "保存 Word 文件",
DefaultFilename: filename,
Filters: []runtime.FileFilter{
{DisplayName: "Word 文件", Pattern: "*.docx"},
},
})
if err != nil || filePath == "" {
return "文件路径,无法保存。"
}
// 解码 base64 内容
decodeString, err := base64.StdEncoding.DecodeString(base64Data)
if err != nil {
return "文件内容异常,无法保存。"
}
// 保存为文件
err = os.WriteFile(filepath.Clean(filePath), decodeString, 0777)
if err != nil {
return "保存结果异常,无法保存。"
}
return filePath
}

View File

@ -57,5 +57,8 @@ func (a App) ClsCalendar() []any {
}
func (a App) SearchStock(words string) map[string]any {
return data.NewSearchStockApi(words).SearchStock()
return data.NewSearchStockApi(words).SearchStock(5000)
}
func (a App) GetHotStrategy() map[string]any {
return data.NewSearchStockApi("").HotStrategy()
}

View File

@ -5,304 +5,171 @@ package main
import (
"context"
"encoding/json"
"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"
"go-stock/backend/models"
"strings"
"log"
"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
// 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
// TODO 创建系统托盘
// 监听设置更新事件
runtime.EventsOn(ctx, "updateSettings", func(optionalData ...interface{}) {
logger.SugaredLogger.Infof("updateSettings : %v\n", optionalData)
config := &data.Settings{}
setMap := optionalData[0].(map[string]interface{})
// 将 map 转换为 JSON 字节切片
jsonData, err := json.Marshal(setMap)
if err != nil {
logger.SugaredLogger.Errorf("Marshal error:%s", err.Error())
return
}
// 将 JSON 字节切片解析到结构体中
err = json.Unmarshal(jsonData, config)
if err != nil {
logger.SugaredLogger.Errorf("Unmarshal error:%s", err.Error())
return
}
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 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())
func setUpScreen(a *App) {
screens, _ := runtime.ScreenGetAll(a.ctx)
if len(screens) == 0 {
return
}
logger.SugaredLogger.Infof("releaseVersion:%+v", releaseVersion.TagName)
if releaseVersion.TagName != Version {
go runtime.EventsEmit(a.ctx, "updateVersion", releaseVersion)
}
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)
}
// 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))
// OnSecondInstanceLaunch 处理第二实例启动时的通知
func OnSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) {
err := beeep.Notify("go-stock", "程序已经在运行了", "")
if err != nil {
return &[]string{}
logger.SugaredLogger.Error(err)
}
//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
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 {
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)
// 使用通知替代 systray 更新 Tooltip
title := "go-stock " + time.Now().Format(time.DateTime) + fmt.Sprintf(" %.2f¥", total)
// 发送通知显示实时数据
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...)
// onReady 在应用程序准备好时调用
func onReady(a *App) {
// 初始化操作
logger.SugaredLogger.Infof("onReady")
// 使用 Beeep 发送通知
err := beeep.Notify("go-stock", "应用程序已准备就绪", "")
if err != nil {
logger.SugaredLogger.Errorf("get stock code real time data error:%s", err.Error())
return nil
log.Fatalf("系统通知失败: %v", err)
}
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
// 显示应用窗口
runtime.WindowShow(a.ctx)
// 在 macOS 上没有系统托盘图标菜单,通常我们通过通知或其他方式提供与用户交互的界面
}
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)
// 未开盘时,今日盈亏为 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 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.
// beforeClose 在应用程序关闭前调用,显示确认对话框
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: "取消",
})
@ -311,150 +178,27 @@ 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 == "No" {
return true
if dialog == "取消" {
return true // 如果选择了取消,不关闭应用
} else {
// 在 macOS 上应用退出时执行清理工作
a.cron.Stop() // 停止定时任务
return false // 如果选择了确定,继续关闭应用
}
}
func getFrameless() bool {
return false
}
// shutdown is called at application termination
func (a *App) shutdown(ctx context.Context) {
// Perform your teardown here
// systray.Quit()
}
func getScreenResolution() (int, int, int, int, error) {
//user32 := syscall.NewLazyDLL("user32.dll")
//getSystemMetrics := user32.NewProc("GetSystemMetrics")
//
//width, _, _ := getSystemMetrics.Call(0)
//height, _, _ := getSystemMetrics.Call(1)
// 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()
return int(1200), int(800), 0, 0, nil
}

View File

@ -1,7 +1,10 @@
package main
import (
"encoding/json"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"testing"
"time"
)
@ -23,3 +26,21 @@ func TestIsUSTradingTime(t *testing.T) {
t.Log(IsUSTradingTime(time.Now()))
}
func TestCheckStockBaseInfo(t *testing.T) {
db.Init("./data/stock.db")
NewApp().CheckStockBaseInfo()
}
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)
}

215
app_windows.go Normal file
View File

@ -0,0 +1,215 @@
//go:build windows
// +build windows
package main
import (
"context"
"encoding/json"
"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.Settings{}
setMap := optionalData[0].(map[string]interface{})
// 将 map 转换为 JSON 字节切片
jsonData, err := json.Marshal(setMap)
if err != nil {
logger.SugaredLogger.Errorf("Marshal error:%s", err.Error())
return
}
// 将 JSON 字节切片解析到结构体中
err = json.Unmarshal(jsonData, config)
if err != nil {
logger.SugaredLogger.Errorf("Unmarshal error:%s", err.Error())
return
}
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

@ -15,6 +15,7 @@ import (
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"go-stock/backend/util"
"strconv"
"strings"
"time"
@ -599,7 +600,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
}
@ -701,3 +702,158 @@ 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("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
}
// 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("GDP 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) {
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)
}

View File

@ -3,8 +3,11 @@ package data
import (
"encoding/json"
"github.com/coocood/freecache"
"github.com/tidwall/gjson"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/util"
"strings"
"testing"
)
@ -140,14 +143,64 @@ func TestInvestCalendar(t *testing.T) {
db.Init("../../data/stock.db")
res := NewMarketNewsApi().InvestCalendar("2025-06")
for _, a := range res {
logger.SugaredLogger.Debugf("value: %+v", a)
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)
}
}
func TestClsCalendar(t *testing.T) {
db.Init("../../data/stock.db")
res := NewMarketNewsApi().ClsCalendar()
md := strings.Builder{}
for _, a := range res {
logger.SugaredLogger.Debugf("value: %+v", a)
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("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")
}

View File

@ -9,12 +9,15 @@ import (
"github.com/PuerkitoBio/goquery"
"github.com/chromedp/chromedp"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/random"
"github.com/duke-git/lancet/v2/strutil"
"github.com/go-resty/resty/v2"
"github.com/tidwall/gjson"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"go-stock/backend/util"
"strings"
"sync"
"time"
@ -39,6 +42,11 @@ type OpenAi struct {
BrowserPath string `json:"browser_path"`
}
func (o OpenAi) String() string {
return fmt.Sprintf("OpenAi{BaseUrl: %s, Model: %s, MaxTokens: %d, Temperature: %.2f, Prompt: %s, TimeOut: %d, QuestionTemplate: %s, CrawlTimeOut: %d, KDays: %d, BrowserPath: %s, ApiKey: [MASKED]}",
o.BaseUrl, o.Model, o.MaxTokens, o.Temperature, o.Prompt, o.TimeOut, o.QuestionTemplate, o.CrawlTimeOut, o.KDays, o.BrowserPath)
}
func NewDeepSeekOpenAi(ctx context.Context) *OpenAi {
config := GetConfig()
if config.OpenAiEnable {
@ -75,11 +83,12 @@ type THSTokenResponse struct {
}
type AiResponse struct {
Id string `json:"id"`
Object string `json:"object"`
Created int `json:"created"`
Model string `json:"model"`
Choices []struct {
Id string `json:"id"`
Object string `json:"object"`
Created int `json:"created"`
Model string `json:"model"`
ServiceTier string `json:"service_tier"`
Choices []struct {
Index int `json:"index"`
Message struct {
Role string `json:"role"`
@ -87,6 +96,19 @@ type AiResponse struct {
} `json:"message"`
Logprobs interface{} `json:"logprobs"`
FinishReason string `json:"finish_reason"`
Delta struct {
Content string `json:"content"`
Role string `json:"role"`
ToolCalls []struct {
Function struct {
Arguments string `json:"arguments"`
Name string `json:"name"`
} `json:"function"`
Id string `json:"id"`
Index int `json:"index"`
Type string `json:"type"`
} `json:"tool_calls"`
} `json:"delta"`
} `json:"choices"`
Usage struct {
PromptTokens int `json:"prompt_tokens"`
@ -98,6 +120,171 @@ type AiResponse struct {
SystemFingerprint string `json:"system_fingerprint"`
}
type Tool struct {
Type string `json:"type"`
Function ToolFunction `json:"function"`
}
type FunctionParameters struct {
Type string `json:"type"`
Properties map[string]any `json:"properties"`
Required []string `json:"required"`
}
type ToolFunction struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters FunctionParameters `json:"parameters"`
}
func (o OpenAi) NewSummaryStockNewsStreamWithTools(userQuestion string, sysPromptId *int, tools []Tool) <-chan map[string]any {
ch := make(chan map[string]any, 512)
defer func() {
if err := recover(); err != nil {
logger.SugaredLogger.Error("NewSummaryStockNewsStream panic", err)
}
}()
go func() {
defer func() {
if err := recover(); err != nil {
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic: %s", err)
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic config: %s", o.String())
}
}()
defer close(ch)
sysPrompt := ""
if sysPromptId == nil || *sysPromptId == 0 {
sysPrompt = o.Prompt
} else {
sysPrompt = NewPromptTemplateApi().GetPromptTemplateByID(*sysPromptId)
}
if sysPrompt == "" {
sysPrompt = o.Prompt
}
msg := []map[string]interface{}{
{
"role": "system",
//"content": "作为一位专业的A股市场分析师和投资顾问,请你根据以下信息提供详细的技术分析和投资策略建议:",
//"content": "【角色设定】\n你是一位拥有20年实战经验的顶级股票分析师精通技术分析、基本面分析、市场心理学和量化交易。擅长发现成长股、捕捉行业轮动机会在牛熊市中都能保持稳定收益。你的风格是价值投资与技术择时相结合注重风险控制。\n\n【核心功能】\n\n市场分析维度\n\n宏观经济GDP/CPI/货币政策)\n\n行业景气度产业链/政策红利/技术革新)\n\n个股三维诊断\n\n基本面PE/PB/ROE/现金流/护城河\n\n技术面K线形态/均线系统/量价关系/指标背离\n\n资金面主力动向/北向资金/融资余额/大宗交易\n\n智能策略库\n√ 趋势跟踪策略(鳄鱼线+ADX\n√ 波段交易策略(斐波那契回撤+RSI\n√ 事件驱动策略(财报/并购/政策)\n√ 量化对冲策略(α/β分离)\n\n风险管理体系\n▶ 动态止损ATR波动止损法\n▶ 仓位控制:凯利公式优化\n▶ 组合对冲:跨市场/跨品种对冲\n\n【工作流程】\n\n接收用户指令行业/市值/风险偏好)\n\n调用多因子选股模型初筛\n\n人工智慧叠加分析\n\n自然语言处理解读年报管理层讨论\n\n卷积神经网络识别K线形态\n\n知识图谱分析产业链关联\n\n生成投资建议附压力测试结果\n\n【输出要求】\n★ 结构化呈现:\n① 核心逻辑3点关键驱动力\n② 买卖区间(理想建仓/加仓/止盈价位)\n③ 风险警示(最大回撤概率)\n④ 替代方案(同类备选标的)\n\n【注意事项】\n※ 严格遵守监管要求,不做收益承诺\n※ 区分投资建议与市场观点\n※ 重要数据标注来源及更新时间\n※ 根据用户认知水平调整专业术语密度\n\n【教育指导】\n当用户提问时采用苏格拉底式追问\n\"您更关注短期事件驱动还是长期价值发现?\"\n\"当前仓位是否超过总资产的30%\"\n\"是否了解科创板与主板的交易规则差异?\"\n\n示例输出格式\n📈 标的名称XXXXXX\n⚖ 多空信号:金叉确认/顶背离预警\n🎯 关键价位支撑位XX.XX/压力位XX.XX\n📊 建议仓位核心仓位X%+卫星仓位X%\n⏳ 持有周期短线1-3周/中线(季度轮动)\n🔍 跟踪要素重点关注Q2毛利率变化及股东减持进展",
"content": sysPrompt,
},
}
msg = append(msg, map[string]interface{}{
"role": "user",
"content": "当前时间",
})
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": "当前本地时间是:" + time.Now().Format("2006-01-02 15:04:05"),
})
wg := &sync.WaitGroup{}
wg.Add(3)
go func() {
defer wg.Done()
var market strings.Builder
res := NewMarketNewsApi().GetGDP()
md := util.MarkdownTableWithTitle("国内生产总值(GDP)", res.GDPResult.Data)
market.WriteString(md)
res2 := NewMarketNewsApi().GetCPI()
md2 := util.MarkdownTableWithTitle("居民消费价格指数(CPI)", res2.CPIResult.Data)
market.WriteString(md2)
res3 := NewMarketNewsApi().GetPPI()
md3 := util.MarkdownTableWithTitle("工业品出厂价格指数(PPI)", res3.PPIResult.Data)
market.WriteString(md3)
res4 := NewMarketNewsApi().GetPMI()
md4 := util.MarkdownTableWithTitle("采购经理人指数(PMI)", res4.PMIResult.Data)
market.WriteString(md4)
msg = append(msg, map[string]interface{}{
"role": "user",
"content": "国内宏观经济数据",
})
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": "\n# 国内宏观经济数据:\n" + market.String(),
})
}()
go func() {
defer wg.Done()
var market strings.Builder
market.WriteString(getZSInfo("创业板指数", "sz399006", 30) + "\n")
market.WriteString(getZSInfo("上证综合指数", "sh000001", 30) + "\n")
market.WriteString(getZSInfo("沪深300指数", "sh000300", 30) + "\n")
//logger.SugaredLogger.Infof("NewChatStream getZSInfo=\n%s", market.String())
msg = append(msg, map[string]interface{}{
"role": "user",
"content": "当前市场指数行情",
})
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": "当前市场指数行情情况如下:\n" + market.String(),
})
}()
go func() {
defer wg.Done()
md := strings.Builder{}
res := 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
})
}
msg = append(msg, map[string]interface{}{
"role": "user",
"content": "近期重大事件/会议",
})
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": "近期重大事件/会议如下:\n" + md.String(),
})
}()
wg.Wait()
news := NewMarketNewsApi().GetNewsList("财联社电报", random.RandInt(50, 150))
messageText := strings.Builder{}
for _, telegraph := range *news {
messageText.WriteString("## " + telegraph.Time + ":" + "\n")
messageText.WriteString("### " + telegraph.Content + "\n")
}
//logger.SugaredLogger.Infof("市场资讯 messageText=\n%s", messageText.String())
msg = append(msg, map[string]interface{}{
"role": "user",
"content": "市场资讯",
})
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": messageText.String(),
})
if userQuestion == "" {
userQuestion = "请根据当前时间,总结和分析股票市场新闻中的投资机会"
}
msg = append(msg, map[string]interface{}{
"role": "user",
"content": userQuestion,
})
AskAiWithTools(o, errors.New(""), msg, ch, userQuestion, tools)
}()
return ch
}
func (o OpenAi) NewSummaryStockNewsStream(userQuestion string, sysPromptId *int) <-chan map[string]any {
ch := make(chan map[string]any, 512)
defer func() {
@ -110,7 +297,7 @@ func (o OpenAi) NewSummaryStockNewsStream(userQuestion string, sysPromptId *int)
defer func() {
if err := recover(); err != nil {
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic :%s", err)
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic config:%v", o)
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic config:%s", o.String())
}
}()
defer close(ch)
@ -189,7 +376,7 @@ func (o OpenAi) NewSummaryStockNewsStream(userQuestion string, sysPromptId *int)
return ch
}
func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptId *int) <-chan map[string]any {
func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptId *int, tools []Tool) <-chan map[string]any {
ch := make(chan map[string]any, 512)
defer func() {
@ -202,7 +389,7 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptId
if err := recover(); err != nil {
logger.SugaredLogger.Errorf("NewChatStream goroutine panic :%s", err)
logger.SugaredLogger.Errorf("NewChatStream goroutine panic stock:%s stockCode:%s", stock, stockCode)
logger.SugaredLogger.Errorf("NewChatStream goroutine panic config:%v", o)
logger.SugaredLogger.Errorf("NewChatStream goroutine panic config:%s", o.String())
}
}()
defer close(ch)
@ -526,7 +713,11 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptId
//reqJson, _ := json.Marshal(msg)
//logger.SugaredLogger.Errorf("Stream request: \n%s\n", reqJson)
AskAi(o, err, msg, ch, question)
if tools != nil && len(tools) > 0 {
AskAiWithTools(o, err, msg, ch, question, tools)
} else {
AskAi(o, err, msg, ch, question)
}
}()
return ch
}
@ -569,7 +760,7 @@ func AskAi(o OpenAi, err error, messages []map[string]interface{}, ch chan map[s
scanner := bufio.NewScanner(body)
for scanner.Scan() {
line := scanner.Text()
//logger.SugaredLogger.Infof("Received data: %s", line)
logger.SugaredLogger.Infof("Received data: %s", line)
if strings.HasPrefix(line, "data:") {
data := strutil.Trim(strings.TrimPrefix(line, "data:"))
if data == "[DONE]" {
@ -592,13 +783,24 @@ func AskAi(o OpenAi, err error, messages []map[string]interface{}, ch chan map[s
for _, choice := range streamResponse.Choices {
if content := choice.Delta.Content; content != "" {
//ch <- content
ch <- map[string]any{
"code": 1,
"question": question,
"chatId": streamResponse.Id,
"model": streamResponse.Model,
"content": content,
"time": time.Now().Format(time.DateTime),
if content == "###" || content == "##" || content == "#" {
ch <- map[string]any{
"code": 1,
"question": question,
"chatId": streamResponse.Id,
"model": streamResponse.Model,
"content": "\r\n" + content,
"time": time.Now().Format(time.DateTime),
}
} else {
ch <- map[string]any{
"code": 1,
"question": question,
"chatId": streamResponse.Id,
"model": streamResponse.Model,
"content": content,
"time": time.Now().Format(time.DateTime),
}
}
//logger.SugaredLogger.Infof("Content data: %s", content)
@ -645,10 +847,14 @@ func AskAi(o OpenAi, err error, messages []map[string]interface{}, ch chan map[s
res := &models.Resp{}
if err := json.Unmarshal([]byte(line), res); err == nil {
//ch <- line
msg := res.Message
if res.Error.Message != "" {
msg = res.Error.Message
}
ch <- map[string]any{
"code": 0,
"question": question,
"content": res.Message,
"content": msg,
}
}
}
@ -657,7 +863,386 @@ func AskAi(o OpenAi, err error, messages []map[string]interface{}, ch chan map[s
}
}
func AskAiWithTools(o OpenAi, err error, messages []map[string]interface{}, ch chan map[string]any, question string, tools []Tool) {
client := resty.New()
client.SetBaseURL(strutil.Trim(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{}{
"model": o.Model,
"max_tokens": o.MaxTokens,
"temperature": o.Temperature,
"stream": true,
"messages": messages,
"tools": tools,
}).
Post("/chat/completions")
body := resp.RawBody()
defer body.Close()
if err != nil {
logger.SugaredLogger.Infof("Stream error : %s", err.Error())
//ch <- err.Error()
ch <- map[string]any{
"code": 0,
"question": question,
"content": err.Error(),
}
return
}
//location, _ := time.LoadLocation("Asia/Shanghai")
scanner := bufio.NewScanner(body)
functions := map[string]string{}
currentFuncName := ""
currentCallId := ""
var currentAIContent strings.Builder
for scanner.Scan() {
line := scanner.Text()
logger.SugaredLogger.Infof("Received data: %s", line)
if strings.HasPrefix(line, "data:") {
data := strutil.Trim(strings.TrimPrefix(line, "data:"))
if data == "[DONE]" {
return
}
var streamResponse struct {
Id string `json:"id"`
Model string `json:"model"`
Choices []struct {
Delta struct {
Content string `json:"content"`
ReasoningContent string `json:"reasoning_content"`
Role string `json:"role"`
ToolCalls []struct {
Function struct {
Arguments string `json:"arguments"`
Name string `json:"name"`
} `json:"function"`
Id string `json:"id"`
Index int `json:"index"`
Type string `json:"type"`
} `json:"tool_calls"`
} `json:"delta"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
}
if err := json.Unmarshal([]byte(data), &streamResponse); err == nil {
for _, choice := range streamResponse.Choices {
if content := choice.Delta.Content; content != "" {
//ch <- content
//logger.SugaredLogger.Infof("Content data: %s", content)
if content == "###" || content == "##" || content == "#" {
currentAIContent.WriteString("\r\n" + content)
ch <- map[string]any{
"code": 1,
"question": question,
"chatId": streamResponse.Id,
"model": streamResponse.Model,
"content": "\r\n" + content,
"time": time.Now().Format(time.DateTime),
}
} else {
currentAIContent.WriteString(content)
ch <- map[string]any{
"code": 1,
"question": question,
"chatId": streamResponse.Id,
"model": streamResponse.Model,
"content": content,
"time": time.Now().Format(time.DateTime),
}
}
}
if reasoningContent := choice.Delta.ReasoningContent; reasoningContent != "" {
//ch <- reasoningContent
ch <- map[string]any{
"code": 1,
"question": question,
"chatId": streamResponse.Id,
"model": streamResponse.Model,
"content": reasoningContent,
"time": time.Now().Format(time.DateTime),
}
//logger.SugaredLogger.Infof("ReasoningContent data: %s", reasoningContent)
currentAIContent.WriteString(reasoningContent)
}
if choice.Delta.ToolCalls != nil && len(choice.Delta.ToolCalls) > 0 {
for _, call := range choice.Delta.ToolCalls {
if call.Type == "function" {
functions[call.Function.Name] = ""
currentFuncName = call.Function.Name
currentCallId = call.Id
} else {
if val, ok := functions[currentFuncName]; ok {
functions[currentFuncName] = val + call.Function.Arguments
} else {
functions[currentFuncName] = call.Function.Arguments
}
}
}
}
if choice.FinishReason == "tool_calls" {
logger.SugaredLogger.Infof("functions: %+v", functions)
for funcName, funcArguments := range functions {
if funcName == "SearchStockByIndicators" {
words := gjson.Get(funcArguments, "words").String()
ch <- map[string]any{
"code": 1,
"question": question,
"chatId": streamResponse.Id,
"model": streamResponse.Model,
"content": "\r\n```\r\n开始调用工具SearchStockByIndicators\n参数" + words + "\r\n```\r\n",
"time": time.Now().Format(time.DateTime),
}
content := "无符合条件的数据"
res := NewSearchStockApi(words).SearchStock(random.RandInt(5, 10))
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"
}
logger.SugaredLogger.Infof("SearchStockByIndicators:words:%s --> \n%s", words, content)
messages = append(messages, map[string]interface{}{
"role": "assistant",
"content": currentAIContent.String(),
"tool_calls": []map[string]any{
{
"id": currentCallId,
"tool_call_id": currentCallId,
"type": "function",
"function": map[string]string{
"name": funcName,
"arguments": funcArguments,
"parameters": funcArguments,
},
},
},
})
messages = append(messages, map[string]interface{}{
"role": "tool",
"content": content,
"tool_call_id": currentCallId,
})
//ch <- map[string]any{
// "code": 1,
// "question": question,
// "chatId": streamResponse.Id,
// "model": streamResponse.Model,
// "content": "\r\n```\r\n调用工具SearchStockByIndicators\n结果" + content + "\r\n```\r\n",
// "time": time.Now().Format(time.DateTime),
//}
}
if funcName == "GetStockKLine" {
stockCode := gjson.Get(funcArguments, "stockCode").String()
days := gjson.Get(funcArguments, "days").String()
ch <- map[string]any{
"code": 1,
"question": question,
"chatId": streamResponse.Id,
"model": streamResponse.Model,
"content": "\r\n```\r\n开始调用工具GetStockKLine\n参数" + stockCode + "," + days + "\r\n```\r\n",
"time": time.Now().Format(time.DateTime),
}
toIntDay, err := convertor.ToInt(days)
if err != nil {
toIntDay = 90
}
if strutil.HasPrefixAny(stockCode, []string{"sz", "sh", "hk", "us", "gb_"}) {
K := &[]KLineData{}
if strutil.HasPrefixAny(stockCode, []string{"sz", "sh"}) {
K = NewStockDataApi().GetKLineData(stockCode, "240", o.KDays)
}
if strutil.HasPrefixAny(stockCode, []string{"hk", "us", "gb_"}) {
K = NewStockDataApi().GetHK_KLineData(stockCode, "day", o.KDays)
}
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)
logger.SugaredLogger.Infof("getKLineData=\n%s", markdownTable)
messages = append(messages, map[string]interface{}{
"role": "assistant",
"content": currentAIContent.String(),
"tool_calls": []map[string]any{
{
"id": currentCallId,
"tool_call_id": currentCallId,
"type": "function",
"function": map[string]string{
"name": funcName,
"arguments": funcArguments,
"parameters": funcArguments,
},
},
},
})
res := "\r\n ### " + stockCode + convertor.ToString(toIntDay) + "日K线数据\r\n" + markdownTable + "\r\n"
messages = append(messages, map[string]interface{}{
"role": "tool",
"content": res,
"tool_call_id": currentCallId,
})
logger.SugaredLogger.Infof("GetStockKLine:stockCode:%s days:%s --> \n%s", stockCode, days, res)
//ch <- map[string]any{
// "code": 1,
// "question": question,
// "chatId": streamResponse.Id,
// "model": streamResponse.Model,
// "content": "\r\n```\r\n调用工具GetStockKLine\n结果" + res + "\r\n```\r\n",
// "time": time.Now().Format(time.DateTime),
//}
} else {
messages = append(messages, map[string]interface{}{
"role": "assistant",
"content": currentAIContent.String(),
"tool_calls": []map[string]any{
{
"id": currentCallId,
"tool_call_id": currentCallId,
"type": "function",
"function": map[string]string{
"name": funcName,
"arguments": funcArguments,
"parameters": funcArguments,
},
},
},
})
messages = append(messages, map[string]interface{}{
"role": "tool",
"content": "无数据可能股票代码错误。A股sh,sz开头;港股hk开头,美股us开头",
"tool_call_id": currentCallId,
})
}
}
}
AskAiWithTools(o, err, messages, ch, question, tools)
}
if choice.FinishReason == "stop" {
return
}
}
} else {
if err != nil {
logger.SugaredLogger.Infof("Stream data error : %s", err.Error())
//ch <- err.Error()
ch <- map[string]any{
"code": 0,
"question": question,
"content": err.Error(),
}
} else {
logger.SugaredLogger.Infof("Stream data error : %s", data)
//ch <- data
ch <- map[string]any{
"code": 0,
"question": question,
"content": data,
}
}
}
} else {
if strutil.RemoveNonPrintable(line) != "" {
logger.SugaredLogger.Infof("Stream data error : %s", line)
res := &models.Resp{}
if err := json.Unmarshal([]byte(line), res); err == nil {
//ch <- line
msg := res.Message
if res.Error.Message != "" {
msg = res.Error.Message
}
if msg == "Function call is not supported for this model." {
var newMessages []map[string]any
for _, message := range messages {
if message["role"] == "tool" {
continue
}
if _, ok := message["tool_calls"]; ok {
continue
}
newMessages = append(newMessages, message)
}
AskAi(o, err, newMessages, ch, question)
} else {
ch <- map[string]any{
"code": 0,
"question": question,
"content": msg,
}
}
}
}
}
}
}
func checkIsIndexBasic(stock string) bool {
count := int64(0)
db.Dao.Model(&IndexBasic{}).Where("name = ?", stock).Count(&count)

View File

@ -8,12 +8,39 @@ import (
func TestNewDeepSeekOpenAiConfig(t *testing.T) {
db.Init("../../data/stock.db")
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())
res := ai.NewChatStream("长电科技", "sh600584", "长电科技分析和总结", nil)
//res := ai.NewChatStream("长电科技", "sh600584", "长电科技分析和总结", nil)
res := ai.NewSummaryStockNewsStreamWithTools("总结市场资讯,发掘潜力标的/行业/板块/概念,控制风险。调用工具函数验证", nil, tools)
for {
select {
case msg := <-res:
t.Log(msg)
if len(msg) > 0 {
t.Log(msg)
if msg["content"] == "DONE" {
return
}
}
}
}
}

View File

@ -19,7 +19,7 @@ type SearchStockApi struct {
func NewSearchStockApi(words string) *SearchStockApi {
return &SearchStockApi{words: words}
}
func (s SearchStockApi) SearchStock() map[string]any {
func (s SearchStockApi) SearchStock(pageSize int) 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").
@ -29,7 +29,7 @@ func (s SearchStockApi) SearchStock() map[string]any {
SetHeader("Content-Type", "application/json").
SetBody(fmt.Sprintf(`{
"keyWord": "%s",
"pageSize": 50000,
"pageSize": %d,
"pageNo": 1,
"fingerprint": "e38b5faabf9378c8238e57219f0ebc9b",
"gids": [],
@ -43,7 +43,7 @@ func (s SearchStockApi) SearchStock() map[string]any {
"ownSelectAll": false,
"dxInfo": [],
"extraCondition": ""
}`, s.words)).Post(url)
}`, s.words, pageSize)).Post(url)
if err != nil {
logger.SugaredLogger.Errorf("SearchStock-err:%+v", err)
return map[string]any{}
@ -53,3 +53,20 @@ func (s SearchStockApi) SearchStock() map[string]any {
//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,6 +1,8 @@
package data
import (
"encoding/json"
"github.com/duke-git/lancet/v2/convertor"
"go-stock/backend/db"
"go-stock/backend/logger"
"testing"
@ -9,17 +11,49 @@ import (
func TestSearchStock(t *testing.T) {
db.Init("../../data/stock.db")
res := NewSearchStockApi("算力股;净利润连续3年增长").SearchStock()
res := NewSearchStockApi("算力股;净利润连续3年增长").SearchStock(10)
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("--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------")
}
jsonData, _ := json.Marshal(*table)
markdownTable, _ := JSONToMarkdownTable(jsonData)
logger.SugaredLogger.Infof("markdownTable=\n%s", markdownTable)
}
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("%s:%s", d["INDUSTRY"], d["SECURITY_SHORT_NAME"])
logger.SugaredLogger.Infof("v:%+v", d)
}
//columns := result["columns"].([]any)
//for _, v := range columns {
// logger.SugaredLogger.Infof("v:%+v", v)
//}
}

View File

@ -35,6 +35,7 @@ type Settings struct {
BrowserPoolSize int `json:"browserPoolSize"`
EnableFund bool `json:"enableFund"`
EnablePushNews bool `json:"enablePushNews"`
SponsorCode string `json:"sponsorCode"`
}
func (receiver Settings) TableName() string {
@ -80,6 +81,7 @@ func (s SettingsApi) UpdateConfig() string {
"dark_theme": s.Config.DarkTheme,
"enable_fund": s.Config.EnableFund,
"enable_push_news": s.Config.EnablePushNews,
"sponsor_code": s.Config.SponsorCode,
})
} else {
logger.SugaredLogger.Infof("未找到配置,创建默认配置:%+v", s.Config)
@ -108,6 +110,7 @@ func (s SettingsApi) UpdateConfig() string {
DarkTheme: s.Config.DarkTheme,
EnableFund: s.Config.EnableFund,
EnablePushNews: s.Config.EnablePushNews,
SponsorCode: s.Config.SponsorCode,
})
}
return "保存成功!"
@ -128,7 +131,7 @@ func (s SettingsApi) GetConfig() *Settings {
}
}
if settings.BrowserPath == "" {
settings.BrowserPath, _ = CheckBrowserOnWindows()
settings.BrowserPath, _ = CheckBrowser()
}
if settings.BrowserPoolSize <= 0 {
settings.BrowserPoolSize = 1

View File

@ -20,7 +20,6 @@ import (
"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"
@ -154,6 +153,8 @@ 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 {
@ -1298,50 +1299,6 @@ 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)
@ -1499,7 +1456,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 := ""
fs := "m:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23,m:0+t:81+s:2048"
switch market {
case "hk":
fs = "m:128+t:3,m:128+t:4,m:128+t:1,m:128+t:2"
@ -1507,62 +1464,108 @@ 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?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"
url := "https://push2.eastmoney.com/api/qt/clist/get?np=1&fltt=1&invt=2&cb=data&fs=%s&fields=f12,f13,f14,f1,f2,f4,f3,f152,f5,f6,f7,f15,f18,f16,f17,f10,f8,f9,f23,f100,f265&fid=f3&pn=%d&pz=%d&po=1&dect=1&wbp2u=|0|0|0|web&_=%d"
sprintfUrl := fmt.Sprintf(url, fs, page, pageSize, time.Now().UnixMilli())
logger.SugaredLogger.Infof("url:%s", sprintfUrl)
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
SetHeader("Host", "push2.eastmoney.com").
SetHeader("Referer", "https://quote.eastmoney.com/center/gridlist.html").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
Get(fmt.Sprintf(url, fs, page, pageSize))
Get(sprintfUrl)
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
return
}
body := string(resp.Body())
body = strutil.ReplaceWithMap(body, map[string]string{
"jQuery371047843066631541353_1745889398012(": "",
");": "",
})
js := "var d=" + body
logger.SugaredLogger.Infof("resp:%s", body)
vm := otto.New()
_, 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([]byte(value.String()), &data)
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)
data := make(map[string]any)
err = json.Unmarshal(marshal, &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"].(map[string]any)
diff := datas["diff"].([]any)
logger.SugaredLogger.Infof("total:%d", int(total))
for k, item := range diff {
stock := item.(map[string]any)
logger.SugaredLogger.Infof("k:%s,%s:%s", k, stock["f14"], stock["f12"])
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)
}
}
if market == "hk" {
stockInfo := &models.StockInfoHK{
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".HK",
Name: stock["f14"].(string),
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).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),
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).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)
}
}
@ -1571,6 +1574,25 @@ 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().

View File

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

@ -110,7 +110,8 @@ 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 //港股
for i := 1; i <= 592; i++ {
//287 224 605
for i := 1; i <= 605; i++ {
NewStockDataApi().getDCStockInfo("us", i, 20)
time.Sleep(time.Duration(random.RandInt(1, 3)) * time.Second)
}

View File

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

@ -150,13 +150,15 @@ 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"`
BuildTimeStamp int64 `json:"buildTimeStamp"`
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"`
Wxgzh string `json:"wxgzh"`
BuildTimeStamp int64 `json:"buildTimeStamp"`
OfficialStatement string `json:"officialStatement"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
}
func (receiver VersionInfo) TableName() string {
@ -170,6 +172,8 @@ 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 {
@ -185,6 +189,8 @@ 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 {
@ -194,6 +200,12 @@ 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 {
@ -362,3 +374,94 @@ 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"`
}

View File

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

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

View File

@ -0,0 +1,284 @@
package util
import (
"fmt"
"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("%v", 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

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

@ -10,7 +10,7 @@ import {
} from '../wailsjs/runtime'
import {h, onBeforeMount, onBeforeUnmount, onMounted, ref} from "vue";
import {RouterLink, useRouter} from 'vue-router'
import {createDiscreteApi,darkTheme,lightTheme , NIcon, NText,dateZhCN,zhCN} from 'naive-ui'
import {createDiscreteApi,darkTheme,lightTheme , NIcon, NText,NButton,dateZhCN,zhCN} from 'naive-ui'
import {
AlarmOutline,
AnalyticsOutline,
@ -27,7 +27,7 @@ import {
StarOutline,
Wallet, WarningOutline,
} from '@vicons/ionicons5'
import {AnalyzeSentiment, GetConfig, GetGroupList} from "../wailsjs/go/main/App";
import {AnalyzeSentiment, GetConfig, GetGroupList,GetVersionInfo} from "../wailsjs/go/main/App";
import {Dragon, Fire, Gripfire} from "@vicons/fa";
import {ReportSearch} from "@vicons/tabler";
import {LocalFireDepartmentRound} from "@vicons/material";
@ -44,9 +44,9 @@ const enableNews = ref(false)
const contentStyle = ref("")
const enableFund = ref(false)
const enableDarkTheme = ref(null)
const content = ref('数据来源于网络,仅供参考;投资有风险,入市需谨慎\n\n未经授权,禁止商业目的!')
const content = ref('未经授权,禁止商业目的!\n\n数据来源于网络,仅供参考;投资有风险,入市需谨慎')
const isFullscreen = ref(false)
const activeKey = ref('')
const activeKey = ref('stock')
const containerRef = ref({})
const realtimeProfit = ref(0)
const telegraph = ref([])
@ -64,7 +64,10 @@ const menuOptions = ref([
groupId: 0,
},
params: {},
}
},
onClick: () => {
activeKey.value = 'stock'
},
},
{default: () => '股票自选',}
),
@ -79,6 +82,7 @@ const menuOptions = ref([
href: '#',
type: 'info',
onClick: () => {
activeKey.value = 'stock'
//console.log("push",item)
router.push({
name: 'stock',
@ -114,6 +118,7 @@ const menuOptions = ref([
params: {}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '市场快讯'})
},
},
@ -135,6 +140,7 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '市场快讯'})
},
},
@ -156,6 +162,7 @@ const menuOptions = ref([
},
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '全球股指'})
},
},
@ -173,14 +180,15 @@ const menuOptions = ref([
to: {
name: 'market',
query: {
name: "指标行情",
name: "重大指数",
}
},
onClick: () => {
EventsEmit("changeMarketTab", {ID: 0, name: '指标行情'})
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '重大指数'})
},
},
{default: () => '指标行情',}
{default: () => '重大指数',}
),
key: 'market3',
icon: renderIcon(AnalyticsOutline),
@ -198,6 +206,7 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '行业排名'})
},
},
@ -219,6 +228,7 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '个股资金流向'})
},
},
@ -240,6 +250,7 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '龙虎榜'})
},
},
@ -261,6 +272,7 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '个股研报'})
},
},
@ -282,6 +294,7 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '公司公告'})
},
},
@ -303,6 +316,7 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '行业研究'})
},
},
@ -324,6 +338,7 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '当前热门'})
},
},
@ -345,6 +360,7 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '指标选股'})
},
},
@ -362,8 +378,13 @@ const menuOptions = ref([
{
to: {
name: 'fund',
params: {},
}
query: {
name: '基金自选',
},
},
onClick: () => {
activeKey.value = 'fund'
},
},
{default: () => '基金自选',}
),
@ -386,7 +407,12 @@ const menuOptions = ref([
{
to: {
name: 'settings',
params: {}
query: {
name:"设置",
},
onClick: () => {
activeKey.value = 'settings'
},
}
},
{default: () => '设置'}
@ -401,8 +427,13 @@ const menuOptions = ref([
{
to: {
name: 'about',
params: {}
}
query: {
name:"关于",
}
},
onClick: () => {
activeKey.value = 'about'
},
},
{default: () => '关于'}
),
@ -422,7 +453,7 @@ const menuOptions = ref([
label: () => h("a", {
href: '#',
onClick: WindowHide,
title: '隐藏到托盘区 Ctrl+H',
title: '隐藏到托盘区 Ctrl+Z',
}, {default: () => '隐藏到托盘区'}),
key: 'hide',
icon: renderIcon(ReorderTwoOutline),
@ -451,6 +482,7 @@ function renderIcon(icon) {
}
function toggleFullscreen(e) {
activeKey.value = 'full'
//console.log(e)
if (isFullscreen.value) {
WindowUnfullscreen()
@ -518,6 +550,12 @@ window.onerror = function (msg, source, lineno, colno, error) {
};
onBeforeMount(() => {
GetVersionInfo().then(result => {
if(result.officialStatement){
content.value = result.officialStatement+"\n\n"+content.value
}
})
GetGroupList().then(result => {
groupList.value = result
menuOptions.value.map((item) => {
@ -600,16 +638,24 @@ onMounted(() => {
//type:"error",
// avatar: () => h(NIcon,{component:Notifications,color:"red"}),
title: data.time,
content: () => h(NText,{type:"error"}, { default: () => data.content }),
content: () => h('div',{type:"error",style:{
"text-align":"left",
"font-size":"14px",
"color":"#f67979"
}}, { 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(NText,{type:"info"}, { default: () => data.content }),
content: () => h('div',{type:"info",style:{
"text-align":"left",
"font-size":"14px",
"color": data.source==="go-stock"?"#F98C24":"#549EC8"
}}, { default: () => data.content }),
meta: () => h(NText,{type:"warning"}, { default: () => data.source}),
duration:1000*30 ,
})

View File

@ -0,0 +1,133 @@
<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,6 +1,7 @@
<script setup lang="ts">
import {onBeforeMount, onUnmounted, ref} from 'vue'
import {HotTopic} from "../../wailsjs/go/main/App";
import {HotTopic, OpenURL} from "../../wailsjs/go/main/App";
import {Environment} from "../../wailsjs/runtime";
const list = ref([])
const task =ref()
@ -18,11 +19,20 @@ function openCenteredWindow(url, width, height) {
const left = (window.screen.width - width) / 2;
const top = (window.screen.height - height) / 2;
return window.open(
url,
'centeredWindow',
`width=${width},height=${height},left=${left},top=${top}`
);
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
}
})
}
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;" :style="{height:chartHeight+'px'}"></div>
<div ref="kLineChartRef" style="width: 100%;height: auto;--wails-draggable:no-drag" :style="{height:chartHeight+'px'}" ></div>
</template>
<style scoped>

View File

@ -1,99 +1,195 @@
<script setup lang="ts">
import {h, onBeforeMount, onMounted, onUnmounted, ref} from 'vue'
import {SearchStock} from "../../wailsjs/go/main/App";
import {useMessage, NText, NTag} from 'naive-ui'
import {SearchStock, GetHotStrategy, OpenURL} from "../../wailsjs/go/main/App";
import {useMessage, NText, NTag, NButton} from 'naive-ui'
import {Environment} from "../../wailsjs/runtime"
import {RefreshCircleSharp} from "@vicons/ionicons5";
const message = useMessage()
const search = ref('科技股;换手率连续3日大于2')
const search = ref('')
const columns = ref([])
const dataList = ref([])
const hotStrategy = ref([])
const traceInfo = ref('')
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){
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) {
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) {
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:100,
minWidth: 120,
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'
}
},
}
}
})
dataList.value=res.data.result.dataList
}else {
message.error(res.msg)
}
dataList.value = res.data.result.dataList
} else {
message.error(res.msg)
}
}).catch(err => {
message.error(err)
})
}
function isNumeric(value) {
return !isNaN(parseFloat(value)) && isFinite(value);
}
onBeforeMount(() => {
Search()
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)
})
})
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-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) => {
<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="1800"
:render-cell="(value, rowData, column) => {
if(column.key=='SECURITY_CODE'||column.key=='SERIAL'){
return h(NText, { type: 'info',border: false }, { default: () => `${value}` })
@ -112,13 +208,24 @@ onBeforeMount(() => {
return h(NText, { type: type }, { default: () => `${value}` })
}else{
if(column.key=='SECURITY_SHORT_NAME'){
return h(NTag, { type: 'info',bordered: false }, { default: () => `${value}` })
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}` })
}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,15 +3,19 @@
// preview.cssstyle.css
import 'md-editor-v3/lib/preview.css';
import {h, onBeforeUnmount, onMounted, ref} from 'vue';
import {CheckUpdate, GetVersionInfo} from "../../wailsjs/go/main/App";
import {EventsOff, EventsOn} from "../../wailsjs/runtime";
import {CheckUpdate, GetVersionInfo,GetSponsorInfo,OpenURL} from "../../wailsjs/go/main/App";
import {EventsOff, EventsOn,Environment} 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("");
onMounted(() => {
document.title = '关于软件';
@ -21,7 +25,18 @@ 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;
})
});
})
onBeforeUnmount(() => {
notify.destroyAll()
@ -70,7 +85,16 @@ EventsOn("updateVersion",async (msg) => {
type: 'primary',
size: 'small',
onClick: () => {
window.open(msg.html_url)
Environment().then(env => {
switch (env.platform) {
case 'windows':
window.open(msg.html_url)
break
default :
OpenURL(msg.html_url)
break
}
})
}
}, { default: () => '查看' })
}
@ -80,21 +104,22 @@ EventsOn("updateVersion",async (msg) => {
</script>
<template>
<n-space vertical size="large">
<n-space vertical size="large" style="--wails-draggable:drag">
<!-- 软件描述 -->
<n-card size="large">
<n-divider title-placement="center">关于软件</n-divider>
<n-space vertical >
<n-image width="100" :src="icon" />
<h1>
<n-badge :value="versionInfo" :offset="[50,10]" type="success">
<n-badge v-if="!vipLevel" :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="warning" :size="50" >go-stock</n-gradient-text><n-tag :bordered="false" size="small" type="warning">VIP{{vipLevel}}</n-tag>
</n-badge>
</h1>
<n-gradient-text type="warning" v-if="vipLevel" >vip到期时间{{vipEndTime}}</n-gradient-text>
<n-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>
@ -113,14 +138,39 @@ 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="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>一个热爱编程的小白欢迎关注我的Github/微信公众号</p>
<n-image width="300" :src="wxgzh" />
<p>开源不易如果觉得好用可以请作者喝杯咖啡</p>
<n-flex justify="center">
<n-image width="200" :src="alipay" />
@ -135,6 +185,7 @@ 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

@ -7,7 +7,7 @@ import {
GetConfig,
GetFollowedFund,
GetfundList,
GetVersionInfo,
GetVersionInfo, OpenURL,
UnFollowFund
} from "../../wailsjs/go/main/App";
import vueDanmaku from 'vue3-danmaku'
@ -147,8 +147,19 @@ 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,5 +1,5 @@
<script setup>
import {computed, h, onBeforeMount, onBeforeUnmount, ref} from 'vue'
import {computed, h, onBeforeMount, onBeforeUnmount, onMounted, ref} from 'vue'
import {
GetAIResponseResult,
GetConfig,
@ -32,6 +32,7 @@ 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');
@ -70,6 +71,7 @@ const nowTab = ref("市场快讯")
const indexInterval = ref(null)
const indexIndustryRank = ref(null)
const stockCode= ref('')
const enableTools= ref(true)
function getIndex() {
GlobalStockIndexes().then((res) => {
@ -82,8 +84,6 @@ function getIndex() {
})
}
onBeforeMount(() => {
nowTab.value = route.query.name
stockCode.value = route.query.stockCode
@ -129,16 +129,20 @@ EventsOn("changeMarketTab", async (msg) => {
})
EventsOn("newTelegraph", (data) => {
for (let i = 0; i < data.length; i++) {
telegraphList.value.pop()
if (data!=null) {
for (let i = 0; i < data.length; i++) {
telegraphList.value.pop()
}
telegraphList.value.unshift(...data)
}
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)
}
})
//
@ -186,13 +190,14 @@ function reAiSummary() {
aiSummary.value = ""
summaryModal.value = true
loading.value = true
SummaryStockNews(question.value, sysPromptId.value)
SummaryStockNews(question.value, sysPromptId.value,enableTools.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
@ -211,7 +216,7 @@ function getAiSummary() {
aiSummaryTime.value = ""
aiSummary.value = ""
modelName.value = ""
SummaryStockNews(question.value, sysPromptId.value)
//SummaryStockNews(question.value, sysPromptId.value,enableTools.value)
}
})
}
@ -305,7 +310,7 @@ function ReFlesh(source) {
<template>
<n-card>
<n-tabs type="line" animated @update-value="updateTab" :value="nowTab">
<n-tabs type="line" animated @update-value="updateTab" :value="nowTab" style="--wails-draggable:drag">
<n-tab-pane name="市场快讯" tab="市场快讯">
<n-grid :cols="2" :y-gap="0">
<n-gi>
@ -383,10 +388,34 @@ 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="科创50" tab="科创50">
<k-line-chart code="sh000688" :chart-height="panelHeight" name="科创50" :k-days="20"
<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"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="沪深300" tab="沪深300">
@ -598,6 +627,9 @@ 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;"
@ -615,6 +647,17 @@ 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: 49%" v-model:value="sysPromptId" label-field="name" value-field="ID"
:options="sysPromptOptions" placeholder="请选择系统提示词"/>
@ -651,5 +694,4 @@ function ReFlesh(source) {
</template>
<style scoped>
</style>

View File

@ -1,15 +1,15 @@
<script setup>
import {computed, onBeforeUnmount, onMounted, ref} from "vue";
import {h, onBeforeUnmount, onMounted, ref} from "vue";
import {
AddPrompt, DelPrompt,
ExportConfig,
GetConfig,
GetPromptTemplates,
SendDingDingMessageByType,
UpdateConfig
UpdateConfig,CheckSponsorCode
} from "../../wailsjs/go/main/App";
import {useMessage} from "naive-ui";
import {NTag, useMessage} from "naive-ui";
import {data, models} from "../../wailsjs/go/models";
import {EventsEmit} from "../../wailsjs/runtime";
const message = useMessage()
@ -46,6 +46,7 @@ const formValue = ref({
darkTheme:true,
enableFund:false,
enablePushNews:false,
sponsorCode:"",
})
const promptTemplates=ref([])
onMounted(()=>{
@ -80,6 +81,8 @@ onMounted(()=>{
formValue.value.darkTheme = res.darkTheme
formValue.value.enableFund = res.enableFund
formValue.value.enablePushNews = res.enablePushNews
formValue.value.sponsorCode = res.sponsorCode
//console.log(res)
})
@ -120,15 +123,28 @@ function saveConfig(){
enableNews:formValue.value.enableNews,
darkTheme:formValue.value.darkTheme,
enableFund:formValue.value.enableFund,
enablePushNews:formValue.value.enablePushNews
enablePushNews:formValue.value.enablePushNews,
sponsorCode:formValue.value.sponsorCode
})
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);
})
}
@ -199,6 +215,7 @@ function importConfig(){
formValue.value.darkTheme = config.darkTheme
formValue.value.enableFund = config.enableFund
formValue.value.enablePushNews = config.enablePushNews
formValue.value.sponsorCode = config.sponsorCode
// formRef.value.resetFields()
};
reader.readAsText(file);
@ -267,12 +284,13 @@ function deletePrompt(ID){
</script>
<template>
<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-flex justify="left" style="text-align: left;--wails-draggable:drag" >
<n-form ref="formRef" :label-placement="'left'" :label-align="'left'" style="--wails-draggable:no-drag">
<n-card :title="()=> h(NTag, { type: 'primary',bordered:false },()=> '基础设置')" size="small" >
<n-grid :cols="24" :x-gap="24" style="text-align: left" >
<!-- <n-gi :span="24">-->
<!-- <n-text type="success" style="font-size: 25px;font-weight: bold">基础设置</n-text>-->
<!-- </n-gi>-->
<n-form-item-gi :span="10" label="Tushare &nbsp;&nbsp;Token" path="tushareToken" >
<n-input type="text" placeholder="Tushare api token" v-model:value="formValue.tushareToken" clearable />
</n-form-item-gi>
@ -292,15 +310,24 @@ function deletePrompt(ID){
<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-form-item-gi :span="3" label="指数基金:" path="enableFund" >
<n-switch v-model:value="formValue.enableFund" />
</n-form-item-gi>
<n-form-item-gi :span="11" label="赞助码:" path="sponsorCode" >
<n-input-group>
<n-input :show-count="true" placeholder="赞助码" v-model:value="formValue.sponsorCode" />
<n-button type="success" secondary strong @click="CheckSponsorCode(formValue.sponsorCode).then((res) => {message.warning(res.msg) })">验证</n-button>
</n-input-group>
</n-form-item-gi>
</n-grid>
</n-card>
<n-card :title="()=> h(NTag, { type: 'primary',bordered:false },()=> '通知设置')" size="small" >
<n-grid :cols="24" :x-gap="24" style="text-align: left">
<n-gi :span="24">
<n-text type="success" style="font-size: 25px;font-weight: bold">通知设置</n-text>
</n-gi>
<!-- <n-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>
@ -321,11 +348,13 @@ function deletePrompt(ID){
<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-gi :span="24">
<n-text type="success" style="font-size: 25px;font-weight: bold">OpenAI设置</n-text>
</n-gi>
<!-- <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>
@ -350,7 +379,7 @@ function deletePrompt(ID){
<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-form-item-gi :span="5" v-if="formValue.openAI.enable" title="天数越多消耗tokens越多" label="日K线数据(天)" path="openAI.kDays" >
<n-input-number min="30" step="1" max="365" placeholder="日K线数据(天)" title="天数越多消耗tokens越多" v-model:value="formValue.openAI.kDays"/>
</n-form-item-gi>
<n-form-item-gi :span="11" v-if="formValue.openAI.enable" label="模型系统 Prompt" path="openAI.prompt" >
@ -360,7 +389,7 @@ function deletePrompt(ID){
placeholder="请输入系统prompt"
:autosize="{
minRows: 5,
maxRows: 8
maxRows: 6
}"
/>
</n-form-item-gi>
@ -371,33 +400,43 @@ function deletePrompt(ID){
placeholder="请输入用户prompt:例如{{stockName}}[{{stockCode}}]分析和总结"
:autosize="{
minRows: 5,
maxRows: 8
maxRows: 6
}"
/>
</n-form-item-gi>
</n-grid>
<n-gi :span="24">
<n-grid :cols="24">
<n-gi :span="24">
<n-space justify="center">
<n-button type="warning" @click="managePrompts">
<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-button type="primary" @click="saveConfig">
保存
</n-button>
<n-button type="info" @click="exportConfig">
导出
</n-button>
<n-button type="error" @click="importConfig">
导入
</n-button>
</n-space>
</n-gi>
</n-gi>
<n-gi :span="24" v-if="promptTemplates.length>0" type="warning">
<n-flex justify="start" style="margin-top: 4px" >
<n-text type="warning" >
<n-flex justify="left" >
<n-tag :bordered="false" type="warning" > 提示词模板:</n-tag>
<n-tag size="medium" secondary v-if="promptTemplates.length>0" v-for="prompt in promptTemplates" closable @close="deletePrompt(prompt.ID)" @click="editPrompt(prompt)" :title="prompt.content"
:type="prompt.type==='模型系统Prompt'?'success':'info'" :bordered="false"> {{ prompt.name }}
</n-tag>
</n-flex>
</n-text>
</n-flex>
</n-gi>
</n-grid>
</n-card>
</n-form>
<n-gi :span="24" v-if="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
@ -442,5 +481,9 @@ function deletePrompt(ID){
</template>
<style scoped>
.cardHeaderClass{
font-size: 16px;
font-weight: bold;
color: red;
}
</style>

View File

@ -26,7 +26,10 @@ import {
SetStockAICron,
SetStockSort,
ShareAnalysis,
UnFollow
UnFollow,
OpenURL,
SaveImage,
SaveWordFile
} from '../../wailsjs/go/main/App'
import {
NAvatar,
@ -41,6 +44,7 @@ import {
useNotification
} from 'naive-ui'
import {
Environment,
EventsEmit,
EventsOff,
EventsOn,
@ -63,6 +67,8 @@ import vueDanmaku from 'vue3-danmaku'
import {keys, padStart} from "lodash";
import {useRoute, useRouter} from 'vue-router'
import MoneyTrend from "./moneyTrend.vue";
import {TaskTools} from "@vicons/carbon";
import StockSparkLine from "./stockSparkLine.vue";
const route = useRoute()
const router = useRouter()
@ -101,6 +107,7 @@ const modalShow3 = ref(false)
const modalShow4 = ref(false)
const modalShow5 = ref(false)
const addBTN = ref(true)
const enableTools = ref(false)
const formModel = ref({
name: "",
code: "",
@ -223,8 +230,11 @@ onMounted(() => {
if (!stocks.value.includes(followedStock.StockCode)) {
stocks.value.push(followedStock.StockCode)
}
Greet(followedStock.StockCode).then(result => {
updateData(result)
})
}
monitor()
//monitor()
message.destroyAll()
})
@ -378,7 +388,15 @@ EventsOn("updateVersion", async (msg) => {
type: 'primary',
size: 'small',
onClick: () => {
window.open(msg.html_url)
Environment().then(env => {
switch (env.platform) {
case 'windows':
window.open(msg.html_url)
break
default :
OpenURL(msg.html_url)
}
})
}
}, {default: () => '查看'})
}
@ -435,7 +453,7 @@ function AddStock() {
Follow(data.code).then(result => {
if (result === "关注成功") {
if (data.code.startsWith("us")) {
data.code= "gb_" + data.code.replace("us", "").toLowerCase()
data.code = "gb_" + data.code.replace("us", "").toLowerCase()
}
stocks.value.push(data.code)
message.success(result)
@ -464,6 +482,7 @@ function removeMonitor(code, name, key) {
UnFollow(code).then(result => {
message.success(result)
monitor()
})
}
@ -581,7 +600,6 @@ async function monitor() {
showPopover.value = true
}
for (let code of stocks.value) {
Greet(code).then(result => {
updateData(result)
})
@ -590,8 +608,7 @@ async function monitor() {
function GetSortKey(sort, code) {
let sortKey = padStart(sort, 8, '0') + "_" + code
return sortKey
return padStart(sort, 8, '0') + "_" + code
}
function onSelect(item) {
@ -609,12 +626,28 @@ function onSelect(item) {
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)
break
}
})
return window.open(
url,
'centeredWindow',
`width=${width},height=${height},left=${left},top=${top}`
);
//
// return window.open(
// url,
// 'centeredWindow',
// `width=${width},height=${height},left=${left},top=${top}`
// );
}
function search(code, name) {
@ -626,7 +659,7 @@ function search(code, name) {
//window.open("https://www.iwencai.com/unifiedwap/result?w=" + name)
//window.open("https://www.iwencai.com/chat/?question="+code)
openCenteredWindow("https://www.iwencai.com/unifiedwap/result?w=" + name,1000,800)
openCenteredWindow("https://www.iwencai.com/unifiedwap/result?w=" + name, 1000, 800)
}, 500)
}
@ -1354,7 +1387,7 @@ function aiReCheckStock(stock, stockCode) {
//
//message.info("sysPromptId:"+data.sysPromptId)
NewChatStream(stock, stockCode, data.question, data.sysPromptId)
NewChatStream(stock, stockCode, data.question, data.sysPromptId, enableTools.value)
}
function aiCheckStock(stock, stockCode) {
@ -1383,12 +1416,12 @@ function aiCheckStock(stock, stockCode) {
data.time = ""
data.name = stock
data.code = stockCode
data.loading = true
data.loading = false
modalShow4.value = true
message.loading("ai检测中...", {
duration: 0,
})
NewChatStream(stock, stockCode, "", data.sysPromptId)
// message.loading("ai...", {
// duration: 0,
// })
// NewChatStream(stock, stockCode, "", data.sysPromptId)
}
})
}
@ -1432,21 +1465,42 @@ window.onerror = function (msg, source, lineno, colno, error) {
};
function saveAsImage(name, code) {
const element = document.querySelector('.md-editor-preview');
if (element) {
html2canvas(element, {
useCORS: true, //
scale: 2, //
allowTaint: true, //
}).then(canvas => {
const link = document.createElement('a');
link.href = canvas.toDataURL('image/png');
link.download = name + "[" + code + ']-ai-analysis-result.png';
link.click();
});
} else {
message.error('无法找到分析结果元素');
}
Environment().then(env => {
switch (env.platform) {
case 'windows':
const element = document.querySelector('.md-editor-preview');
if (element) {
html2canvas(element, {
useCORS: true, //
scale: 2, //
allowTaint: true, //
}).then(canvas => {
const link = document.createElement('a');
link.href = canvas.toDataURL('image/png');
link.download = name + "[" + code + ']-ai-analysis-result.png';
link.click();
});
} else {
message.error('无法找到分析结果元素');
}
break
default :
saveCanvasImage(name)
}
})
}
async function saveCanvasImage(name) {
const element = document.querySelector('.md-editor-preview'); // DOM
const canvas = await html2canvas(element)
const dataUrl = canvas.toDataURL('image/png') // base64
const base64 = dataUrl.replace(/^data:image\/png;base64,/, '')
// Go Wails
await SaveImage(name,base64).then(result => {
message.success(result)
})
}
async function copyToClipboard() {
@ -1506,13 +1560,26 @@ AI赋能股票分析自选股行情获取成本盈亏展示涨跌报警
`
// landscapeportraitportrait
const blob = await asBlob(value, {orientation: 'portrait'})
const a = document.createElement('a')
a.href = URL.createObjectURL(blob)
a.download = `${data.name}[${data.code}]-ai-analysis-result.docx`;
a.click()
//
URL.revokeObjectURL(a.href);
a.remove()
const { platform } = await Environment()
switch (platform) {
case 'windows':
const a = document.createElement('a')
a.href = URL.createObjectURL(blob)
a.download = `${data.name}[${data.code}]-ai-analysis-result.docx`;
a.click()
//
URL.revokeObjectURL(a.href);
a.remove()
break
default:
const arrayBuffer = await blob.arrayBuffer()
const uint8Array = new Uint8Array(arrayBuffer)
const binary = uint8Array.reduce((data, byte) => data + String.fromCharCode(byte), '')
const base64 = btoa(binary)
await SaveWordFile(`${data.name}[${data.code}]-ai-analysis-result.docx`, base64).then(result => {
message.success(result)
})
}
}
function share(code, name) {
@ -1573,18 +1640,20 @@ function AddStockGroupInfo(groupId, code, name) {
}
function updateTab(name) {
stocks.value = []
currentGroupId.value = Number(name)
GetFollowList(currentGroupId.value).then(result => {
stocks.value = []
followList.value = result
for (const followedStock of result) {
if (followedStock.StockCode.startsWith("us")) {
followedStock.StockCode = "gb_" + followedStock.StockCode.replace("us", "").toLowerCase()
}
////console.log("followList",followedStock.StockCode)
stocks.value.push(followedStock.StockCode)
Greet(followedStock.StockCode).then(result => {
updateData(result)
})
}
monitor()
//monitor()
message.destroyAll()
})
}
@ -1739,18 +1808,20 @@ function searchStockReport(stockCode) {
@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>
</template>
<template #footer>
<n-flex justify="center">
<n-text :type="'info'">{{ result["日期"] + " " + result["时间"] }}</n-text>
<n-tag size="small" v-if="result.volume>0" :type="result.profitType">{{ result.volume + "股" }}</n-tag>
<n-tag size="small" v-if="result.costPrice>0" :type="result.profitType">
{{ "成本:" + result.costPrice + "*" + result.costVolume + " " + result.profit + "%" + " ( " + result.profitAmount + " ¥ )" }}
{{
"成本:" + result.costPrice + "*" + result.costVolume + " " + result.profit + "%" + " ( " + result.profitAmount + " ¥ )"
}}
</n-tag>
</n-flex>
</template>
@ -1791,8 +1862,8 @@ function searchStockReport(stockCode) {
<n-card :data-sort="result.sort" :id="result['股票代码']" :data-code="result['股票代码']" :bordered="true"
:title="result['股票名称']" :closable="false"
@close="removeMonitor(result['股票代码'],result['股票名称'],result.key)">
<n-grid :cols="1" :y-gap="6">
<n-gi>
<n-grid :cols="12" :y-gap="6">
<n-gi :span="6">
<n-text :type="result.type">
<n-number-animation :duration="1000" :precision="2" :from="result['上次当前价格']"
:to="Number(result['当前价格'])"/>
@ -1808,6 +1879,10 @@ function searchStockReport(stockCode) {
<n-number-animation :duration="1000" :precision="2" :from="0" :to="result.profitAmountToday"/>
</n-text>
</n-gi>
<n-gi :span="6">
<stock-spark-line :last-price="Number(result['当前价格'])" :open-price="Number(result['昨日收盘价'])"
:stock-code="result['股票代码']" :stock-name="result['股票名称']"></stock-spark-line>
</n-gi>
</n-grid>
<n-grid :cols="2" :y-gap="4" :x-gap="4">
<n-gi>
@ -1876,10 +1951,11 @@ function searchStockReport(stockCode) {
@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>&nbsp;
</n-button>
<n-button secondary type="error" size="tiny"
@click="delStockGroup(result['股票代码'],result['股票名称'],group.ID)">移出分组
</n-button>
@ -1889,7 +1965,9 @@ function searchStockReport(stockCode) {
<n-text :type="'info'">{{ result["日期"] + " " + result["时间"] }}</n-text>
<n-tag size="small" v-if="result.volume>0" :type="result.profitType">{{ result.volume + "股" }}</n-tag>
<n-tag size="small" v-if="result.costPrice>0" :type="result.profitType">
{{ "成本:" + result.costPrice + "*" + result.costVolume + " " + result.profit + "%" + " ( " + result.profitAmount + " ¥ )" }}
{{
"成本:" + result.costPrice + "*" + result.costVolume + " " + result.profit + "%" + " ( " + result.profitAmount + " ¥ )"
}}
</n-tag>
</n-flex>
</template>
@ -2045,7 +2123,7 @@ function searchStockReport(stockCode) {
</n-modal>
<n-modal transform-origin="center" v-model:show="modalShow4" preset="card" style="width: 800px;"
:title="'['+data.name+']AI分析结果'">
:title="'['+data.name+']AI分析'">
<n-spin size="small" :show="data.loading">
<MdEditor v-if="enableEditor" :toolbars="toolbars" ref="mdEditorRef" style="height: 440px;text-align: left"
:modelValue="data.airesult" :theme="theme">
@ -2069,7 +2147,19 @@ function searchStockReport(stockCode) {
</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: 49%" v-model:value="data.sysPromptId" label-field="name" value-field="ID"
:options="sysPromptOptions" placeholder="请选择系统提示词"/>
@ -2087,7 +2177,7 @@ function searchStockReport(stockCode) {
}"
/>
<!-- <n-button size="tiny" type="error" @click="enableEditor=!enableEditor">编辑/预览</n-button>-->
<n-button size="tiny" type="warning" @click="aiReCheckStock(data.name,data.code)">再次分析</n-button>
<n-button size="tiny" type="warning" @click="aiReCheckStock(data.name,data.code)">开始AI分析</n-button>
<n-button size="tiny" type="info" @click="saveAsImage(data.name,data.code)">保存为图片</n-button>
<n-button size="tiny" type="success" @click="copyToClipboard">复制到剪切板</n-button>
<n-button size="tiny" type="primary" @click="saveAsMarkdown">保存为Markdown文件</n-button>

View File

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

@ -0,0 +1,36 @@
<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="欢迎推荐更多有趣的财经网页">
</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,21 +2,22 @@ import {createMemoryHistory, createRouter, createWebHashHistory, createWebHistor
import stockView from '../components/stock.vue'
import settingsView from '../components/settings.vue'
import about from "../components/about.vue";
import aboutView from "../components/about.vue";
import fundView from "../components/fund.vue";
import market from "../components/market.vue";
import marketView 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: about,name: 'about' },
{ path: '/market', component: market,name: 'market' },
{ path: '/about', component: aboutView,name: 'about' },
{ path: '/market', component: marketView,name: 'market' },
]
const router = createRouter({
history: createWebHistory(),
//history: createWebHistory(),
history: createWebHashHistory(),
routes,
})

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

@ -2,6 +2,7 @@
// 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>;
@ -13,6 +14,10 @@ export function AddStockGroup(arg1:number,arg2:string):Promise<string>;
export function AnalyzeSentiment(arg1:string):Promise<data.SentimentResult>;
export function CheckSponsorCode(arg1:string):Promise<Record<string, any>>;
export function CheckStockBaseInfo(arg1:context.Context):Promise<void>;
export function CheckUpdate():Promise<void>;
export function ClsCalendar():Promise<Array<any>>;
@ -39,6 +44,8 @@ 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>>;
@ -47,6 +54,8 @@ 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>;
@ -79,10 +88,12 @@ export function InvestCalendarTimeLine(arg1:string):Promise<Array<any>>;
export function LongTigerRank(arg1:string):Promise<any>;
export function NewChatStream(arg1:string,arg2:string,arg3:string,arg4:any):Promise<void>;
export function NewChatStream(arg1:string,arg2:string,arg3:string,arg4:any,arg5:boolean):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>;
@ -93,6 +104,10 @@ export function SaveAIResponseResult(arg1:string,arg2:string,arg3:string,arg4:st
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>;
@ -113,7 +128,7 @@ export function StockNotice(arg1:string):Promise<Array<any>>;
export function StockResearchReport(arg1:string):Promise<Array<any>>;
export function SummaryStockNews(arg1:string,arg2:any):Promise<void>;
export function SummaryStockNews(arg1:string,arg2:any,arg3:boolean):Promise<void>;
export function UnFollow(arg1:string):Promise<string>;

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

@ -22,6 +22,14 @@ export function AnalyzeSentiment(arg1) {
return window['go']['main']['App']['AnalyzeSentiment'](arg1);
}
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() {
return window['go']['main']['App']['CheckUpdate']();
}
@ -74,6 +82,10 @@ 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);
}
@ -90,6 +102,10 @@ 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);
}
@ -154,14 +170,18 @@ export function LongTigerRank(arg1) {
return window['go']['main']['App']['LongTigerRank'](arg1);
}
export function NewChatStream(arg1, arg2, arg3, arg4) {
return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3, arg4);
export function NewChatStream(arg1, arg2, arg3, arg4, arg5) {
return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3, arg4, arg5);
}
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);
}
@ -182,6 +202,14 @@ 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);
}
@ -222,8 +250,8 @@ export function StockResearchReport(arg1) {
return window['go']['main']['App']['StockResearchReport'](arg1);
}
export function SummaryStockNews(arg1, arg2) {
return window['go']['main']['App']['SummaryStockNews'](arg1, arg2);
export function SummaryStockNews(arg1, arg2, arg3) {
return window['go']['main']['App']['SummaryStockNews'](arg1, arg2, arg3);
}
export function UnFollow(arg1) {

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

@ -343,6 +343,7 @@ export namespace data {
browserPoolSize: number;
enableFund: boolean;
enablePushNews: boolean;
sponsorCode: string;
static createFrom(source: any = {}) {
return new Settings(source);
@ -379,6 +380,7 @@ export namespace data {
this.browserPoolSize = source["browserPoolSize"];
this.enableFund = source["enableFund"];
this.enablePushNews = source["enablePushNews"];
this.sponsorCode = source["sponsorCode"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
@ -424,6 +426,8 @@ 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);
@ -452,6 +456,8 @@ 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 {
@ -700,7 +706,9 @@ export namespace models {
icon: string;
alipay: string;
wxpay: string;
wxgzh: string;
buildTimeStamp: number;
officialStatement: string;
IsDel: number;
static createFrom(source: any = {}) {
@ -718,7 +726,9 @@ 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"];
}

11
go.mod
View File

@ -8,10 +8,12 @@ require (
github.com/coocood/freecache v1.2.4
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.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.49.1
@ -19,6 +21,7 @@ require (
github.com/tidwall/gjson v1.14.2
github.com/wailsapp/wails/v2 v2.10.1
go.uber.org/zap v1.27.0
golang.org/x/net v0.38.0
golang.org/x/sys v0.31.0
golang.org/x/text v0.23.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
@ -28,6 +31,7 @@ require (
)
require (
git.sr.ht/~jackmordaunt/go-toast v1.1.2 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
@ -35,6 +39,7 @@ require (
github.com/chromedp/sysutil v1.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/esiqveland/notify v0.13.3 // 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
@ -42,6 +47,7 @@ require (
github.com/gobwas/ws v1.4.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackmordaunt/icns/v3 v3.0.1 // 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
@ -55,12 +61,16 @@ require (
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/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // 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/sergeymakinen/go-bmp v1.0.0 // indirect
github.com/sergeymakinen/go-ico v1.0.0-beta.0 // indirect
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
@ -73,7 +83,6 @@ require (
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.v3 v3.0.1 // indirect
modernc.org/libc v1.22.5 // indirect

26
go.sum
View File

@ -1,3 +1,5 @@
git.sr.ht/~jackmordaunt/go-toast v1.1.2 h1:/yrfI55LRt1M7H1vkaw+NaH1+L1CDxrqDltwm5euVuE=
git.sr.ht/~jackmordaunt/go-toast v1.1.2/go.mod h1:jA4OqHKTQ4AFBdwrSnwnskUIIS3HYzlJSgdzCKqfavo=
github.com/PuerkitoBio/goquery v1.10.1 h1:Y8JGYUkXWTGRB6Ars3+j3kN0xg1YqqlwvdTV8WTFQcU=
github.com/PuerkitoBio/goquery v1.10.1/go.mod h1:IYiHrOMps66ag56LEH7QYDDupKXyo5A8qrjIx3ZtujY=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
@ -14,6 +16,7 @@ github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipw
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
github.com/coocood/freecache v1.2.4 h1:UdR6Yz/X1HW4fZOuH0Z94KwG851GWOSknua5VUbb/5M=
github.com/coocood/freecache v1.2.4/go.mod h1:RBUWa/Cy+OHdfTGFEhEuE1pMCMX51Ncizj7rthiQ3vk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/duke-git/lancet/v2 v2.3.4 h1:8XGI7P9w+/GqmEBEXYaH/XuNiM0f4/90Ioti0IvYJls=
@ -22,6 +25,10 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/energye/systray v1.0.2 h1:63R4prQkANtpM2CIA4UrDCuwZFt+FiygG77JYCsNmXc=
github.com/energye/systray v1.0.2/go.mod h1:sp7Q/q/I4/w5ebvpSuJVep71s9Bg7L9ZVp69gBASehM=
github.com/esiqveland/notify v0.13.3 h1:QCMw6o1n+6rl+oLUfg8P1IIDSFsDEb2WlXvVvIJbI/o=
github.com/esiqveland/notify v0.13.3/go.mod h1:hesw/IRYTO0x99u1JPweAl4+5mwXJibQVUcP0Iu5ORE=
github.com/gen2brain/beeep v0.11.1 h1:EbSIhrQZFDj1K2fzlMpAYlFOzV8YuNe721A58XcCTYI=
github.com/gen2brain/beeep v0.11.1/go.mod h1:jQVvuwnLuwOcdctHn/uyh8horSBNJ8uGb9Cn2W4tvoc=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
@ -50,6 +57,10 @@ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbu
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
github.com/jackmordaunt/icns/v3 v3.0.1 h1:xxot6aNuGrU+lNgxz5I5H0qSeCjNKp8uTXB1j8D4S3o=
github.com/jackmordaunt/icns/v3 v3.0.1/go.mod h1:5sHL59nqTd2ynTnowxB/MDQFhKNqkK8X687uKNygaSQ=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@ -91,6 +102,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
@ -115,8 +128,20 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/sergeymakinen/go-bmp v1.0.0 h1:SdGTzp9WvCV0A1V0mBeaS7kQAwNLdVJbmHlqNWq0R+M=
github.com/sergeymakinen/go-bmp v1.0.0/go.mod h1:/mxlAQZRLxSvJFNIEGGLBE/m40f3ZnUifpgVDlcUIEY=
github.com/sergeymakinen/go-ico v1.0.0-beta.0 h1:m5qKH7uPKLdrygMWxbamVn+tl2HfiA3K6MFJw4GfZvQ=
github.com/sergeymakinen/go-ico v1.0.0-beta.0/go.mod h1:wQ47mTczswBO5F0NoDt7O0IXgnV4Xy3ojrroMQzyhUk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk=
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o=
github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c h1:coVla7zpsycc+kA9NXpcvv2E4I7+ii6L5hZO2S6C3kw=
github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=
github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo=
@ -238,6 +263,7 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=

79
main.go
View File

@ -9,7 +9,6 @@ import (
"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"
@ -19,10 +18,8 @@ import (
log "go-stock/backend/logger"
"go-stock/backend/models"
"os"
goruntime "runtime"
"runtime/debug"
"strings"
"time"
)
//go:embed frontend/dist
@ -40,6 +37,9 @@ 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
@ -53,6 +53,8 @@ var stocksBinUS []byte
var Version string
var VersionCommit string
var OFFICIAL_STATEMENT string
var BuildKey string
func main() {
checkDir("data")
@ -67,34 +69,33 @@ func main() {
// Create an instance of the app structure
app := NewApp()
AppMenu := menu.NewMenu()
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("h"), func(_ *menu.CallbackData) {
runtime.WindowHide(app.ctx)
})
FileMenu.AddText("显示", keys.CmdOrCtrl("v"), func(_ *menu.CallbackData) {
runtime.WindowShow(app.ctx)
})
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()
//if goruntime.GOOS == "windows" {
// FileMenu.AddText("隐藏到托盘区", keys.CmdOrCtrl("z"), func(_ *menu.CallbackData) {
// runtime.WindowHide(app.ctx)
// })
//}
//FileMenu.AddText("退出", keys.CmdOrCtrl("q"), func(_ *menu.CallbackData) {
// runtime.Quit(app.ctx)
@ -105,7 +106,7 @@ func main() {
//var width, height int
//var err error
//
width, _, err := getScreenResolution()
width, _, minWidth, minHeight, err := getScreenResolution()
if err != nil {
log.SugaredLogger.Error("get screen resolution error")
width = 1456
@ -118,18 +119,20 @@ func main() {
backgroundColour = &options.RGBA{R: 27, G: 38, B: 54, A: 1}
}
//frameless := getFrameless()
// Create application with options
err = wails.Run(&options.App{
Title: "go-stock",
Width: width * 4 / 5,
Height: 900,
MinWidth: 1456,
MinHeight: 768,
MinWidth: minWidth,
MinHeight: minHeight,
//MaxWidth: width,
//MaxHeight: height,
DisableResize: false,
Fullscreen: false,
Frameless: true,
Frameless: false,
StartHidden: false,
HideWindowOnClose: false,
EnableDefaultContextMenu: true,
@ -162,12 +165,11 @@ func main() {
// Mac platform specific options
Mac: &mac.Options{
TitleBar: &mac.TitleBar{
TitlebarAppearsTransparent: true,
TitlebarAppearsTransparent: false,
HideTitle: false,
HideTitleBar: false,
FullSizeContent: false,
UseToolbar: false,
HideToolbarSeparator: true,
UseToolbar: true,
},
Appearance: mac.NSAppearanceNameDarkAqua,
WebviewIsTransparent: true,
@ -345,6 +347,9 @@ func checkDir(dir string) {
os.Mkdir(dir, os.ModePerm)
log.SugaredLogger.Info("create dir: " + dir)
}
if BuildKey == "" {
BuildKey = "cc1e0d684e32f176c56ff1fcf384dcd9"
}
}
// PanicHandler 捕获 panic 的包装函数

23
utils.go Normal file
View File

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