Compare commits

..

119 Commits

Author SHA1 Message Date
ArvinLovegood
6dff9d95c4 build:更新Go依赖并升级到Go1.25
- 更新 golang.org/x/sys从 v0.33.0 到 v0.35.0
- 更新 sonic、sonic/loader、base64x、cpuid、arch 等多个依赖库
- 将 Go 版本从 1.24升级到 1.25
2025-08-26 11:28:49 +08:00
ArvinLovegood
06967420f8 feat(app):添加股票基础信息自动检查功能
- 在应用启动时检查股票基础信息是否为空
- 如果为空,则自动调用 CheckStockBaseInfo 方法获取数据
-每天凌晨 2 点定时检查股票基础信息
2025-08-26 10:45:17 +08:00
ArvinLovegood
6f4eb0ac86 refactor(backend): 移除未使用的导入语句
- 删除了 "C" 的导入语句,该语句在代码中未被使用
- 优化代码结构,提高代码的可读性和维护性
2025-08-24 20:11:57 +08:00
ArvinLovegood
f59255cc6c style:调整 Windows特定代码的构建标签
- 在 alert_windows_api.go 和 alert_windows_api_test.go 文件中添加额外的 +build windows 标签
- 优化代码结构,提高代码的可读性和可维护性
2025-08-24 16:29:57 +08:00
ArvinLovegood
4f4fa46338 refactor(app):优化代码结构和功能
- 调整导入顺序,提高代码可读性
- 使用 cron 定时任务替代直接调用检查更新方法
- 在前端 App.vue 中添加名站优选市场选项
-调整主窗口高度
-优化股票情感分析数据处理
2025-08-24 15:46:55 +08:00
SparkMemory
05bf35fdf4 Merge pull request #99 from CodeNoobLH/dev
feat(frontend): 添加股票分组拖拽排序功能
2025-08-24 14:35:21 +08:00
浓睡不消残酒
567c81ae7c feat(frontend): 添加股票分组拖拽排序功能
- 在前端实现股票分组的拖拽排序功能
- 新增后端接口支持分组排序的更新
- 优化数据库操作,确保分组顺序的正确性和唯一性
- 修复了一些与分组列表相关的小问题
2025-08-22 15:10:47 +08:00
SparkMemory
86f4e54d13 Merge pull request #97 from CodeNoobLH/dev
feat(frontend): 增加关注股票功能并优化表格显示- 在指标选股组件中添加关注股票功能
2025-08-16 21:36:50 +08:00
浓睡不消残酒
71e6ff4233 feat(frontend): 增加关注股票功能并优化表格显示- 在 SelectStock 组件中添加关注股票功能
- 实现股票关注逻辑,包括检查是否已关注
- 优化表格宽度计算,确保适配不同屏幕
- 在表格中添加操作列,用于关注股票
2025-08-14 18:13:24 +08:00
ArvinLovegood
e844e2cff9 feat(frontend):添加AI智能体聊天功能
- 在前端 App.vue 中添加 AI智能体聊天入口
- 在后端 App.d.ts 和 App.js 中添加 ChatWithAgent 函数- 在 app_common.go 中实现 ChatWithAgent 方法,使用 agent.NewStockAiAgentApi().Chat 进行聊天
- 更新 go.mod,添加与 AI 聊天相关的依赖
2025-08-09 19:54:49 +08:00
ArvinLovegood
27af39ff61 docs(README): 更新大模型支持列表并调整推广链接
-移除 MACOS 安装版下载链接
- 删除 优云智算 注册链接
-优化大模型支持列表格式
2025-08-08 11:56:27 +08:00
ArvinLovegood
5537ebb87a docs(README):更新支持大模型/平台列表
- 在支持大模型/平台列表中添加了302.AI
- 302.AI 提供新用户注册赠送 $1 测试额度
- 更新了README中的注册链接信息
2025-08-05 18:38:14 +08:00
ArvinLovegood
b906140dd5 docs(README):更新支持大模型/平台列表
- 在支持大模型/平台列表中添加了302.AI
- 302.AI 提供新用户注册赠送 $1 测试额度
- 更新了README中的注册链接信息
2025-08-05 18:32:33 +08:00
ArvinLovegood
087b953ed8 refactor(backend):优化投资互动数据处理
- 修改搜索关键词描述,移除多余信息
- 更新搜索类型参数,仅使用代码11
- 优化结构体转换为 Markdown 的处理逻辑
2025-08-01 17:31:57 +08:00
ArvinLovegood
3c5205738f feat(data):添加投资者互动问答数据
- 在 app.go 中添加了 InteractiveAnswer 工具函数
- 在 market_news_api.go 中实现了 InteractiveAnswer 方法
- 在 market_news_api_test.go 中添加了 InteractiveAnswer 测试用例
- 在 models.go 中定义了 InteractiveAnswer 相关的结构体
- 在 openai_api.go 中集成了 InteractiveAnswer 功能
2025-07-31 18:48:28 +08:00
ArvinLovegood
b1b34d950b feat(notify):新增只提醒红字或关注个股新闻的设置
- 在 App 中实现只推送红字或关注个股新闻的逻辑
- 在前端添加 enableOnlyPushRedNews 配置项
- 更新后端设置 API 以支持新功能
2025-07-28 18:22:37 +08:00
ArvinLovegood
83aa4331ad feat(notify):新增只提醒红字或关注个股新闻的设置
- 在 App 中实现只推送红字或关注个股新闻的逻辑
- 在前端添加 enableOnlyPushRedNews 配置项
- 更新后端设置 API 以支持新功能
2025-07-28 18:02:33 +08:00
ArvinLovegood
d4d3c44cf4 refactor(data):优化市场行情信息获取和展示
- 调整市场指数行情的展示格式和内容,增加更多指数信息
- 修改财联社电报的新闻列表获取参数,增加随机性
- 更新测试用例,增加对新功能的测试
2025-07-25 16:52:12 +08:00
ArvinLovegood
81a9cc5927 style(frontend):禁止界面拖拽
- 在多个组件中将 --wails-draggable 属性从 drag 改为 no-drag
- 这包括 about、App、market、settings 和 stock 组件
2025-07-24 17:17:50 +08:00
ArvinLovegood
3fc89a85da feat(ai):AI分析默认使用配置的第一个AI
- 在 market.vue 和 stock.vue 组件中,获取 AI 配置后设置了第一个配置的 ID
- 这个改动确保了在组件初始化时有一个默认的 AI 配置被选中
2025-07-23 12:35:05 +08:00
ArvinLovegood
0605c8442d feat(backend):添加Reuters新闻接口并整合到OpenAI消息中
- 新增 ReutersNew 方法获取 Reuters 新闻数据
- 创建 ReutersNews 模型用于解析新闻响应
- 在 OpenAI消息中添加 Reuters 新闻资讯
- 优化 MarketNewsApi 和 OpenAI 相关代码结构
2025-07-22 16:51:25 +08:00
ArvinLovegood
cf8591c208 refactor(data):调整ReutersAPI请求超时时间并集成TradingView 新闻
- 将 Reuters API 请求超时时间从30 秒调整为 5 秒
- 在 OpenAI API 中添加 TradingView 新闻获取功能
- 优化新闻文本处理和日志输出
2025-07-22 16:38:38 +08:00
ArvinLovegood
7607c4356f refactor(data):调整ReutersAPI请求超时时间并集成TradingView 新闻
- 将 Reuters API 请求超时时间从30 秒调整为 5 秒
- 在 OpenAI API 中添加 TradingView 新闻获取功能
- 优化新闻文本处理和日志输出
2025-07-22 16:24:38 +08:00
ArvinLovegood
4aae2ece00 feat(proxy):添加http代理支持来获取外媒新闻功能
- 在设置中添加 http 代理相关配置
- 优化 TradingView 新闻获取逻辑
- 添加 Reuters 新闻获取功能
- 调整行业报告信息获取方法
- 更新前端设置组件以支持 http 代理配置
2025-07-22 15:56:06 +08:00
ArvinLovegood
369d14025c refactor(settings):更新旧设置模型并迁移数据到新的多模型版本
- 新增 OldSettings 结构体,用于表示旧的设置模型
- 实现 updateMultipleModel 函数,将旧设置中的 AI 配置数据迁移到新模型
- 在 AutoMigrate 函数中调用 updateMultipleModel,确保数据迁移在数据库自动迁移过程中完成
2025-07-21 18:20:18 +08:00
ArvinLovegood
1e7387f3fa refactor(go-stock):优化配置文件写入权限并调整窗口大小
- 将配置文件写入权限改为 os.ModePerm,提高安全性
- 调整主窗口高度,优化用户界面布局
- 修正 AI 模型服务配置选择框宽度
2025-07-19 21:55:30 +08:00
SparkMemory
cfd218f181 Merge pull request #94 from GiCo001/dev-darwin
feat:新增多AI模型服务配置
2025-07-19 17:27:19 +08:00
Gico001
b8e1f38a32 feat:新增多AI模型服务配置 2025-07-19 16:52:15 +08:00
ArvinLovegood
b1a9a8d4d8 refactor(update):优化更新检查逻辑
- 修改 CheckUpdate 函数签名,添加 flag 参数
- 根据 flag 参数控制是否显示"当前版本无更新"的通知
- 调整前端按钮点击事件,传递参数 1 给 CheckUpdate 函数
- 优化后端更新检查流程,减少不必要的通知推送
2025-07-17 17:39:59 +08:00
ArvinLovegood
b98f829286 refactor(update):优化更新检查逻辑
- 修改 CheckUpdate 函数签名,添加 flag 参数
-根据 flag 参数控制是否显示"当前版本无更新"的通知
- 调整前端按钮点击事件,传递参数 1 给 CheckUpdate 函数
- 优化后端更新检查流程,减少不必要的通知推送
2025-07-17 17:31:40 +08:00
ArvinLovegood
dda160069a refactor(app):更新股票数据接口地址
- 将股票数据接口的 HTTPS 地址替换为 HTTP 地址
- 更新接口服务器 IP 和端口
- 此修改影响 A 股、港股和美股的股票数据获取
2025-07-17 14:40:59 +08:00
ArvinLovegood
f80ea181be feat:更新应用标题添加“AI赋能股票分析
- 在 main.go 文件中更新了应用的标题
- 添加了 AI赋能股票分析 和 星星图标,提升应用吸引力
2025-07-16 18:16:04 +08:00
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
80 changed files with 8945 additions and 1792 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,13 +44,15 @@ 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 }}
build-platform: ${{ matrix.build.platform }}
package: true
go-version: '1.24'
go-version: '1.25'
build-tags: ${{ github.ref_name }}
build-commit-message: ${{ steps.get_commit_message.outputs.commit_message }}
build-statement: ${{ env.OFFICIAL_STATEMENT }}
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,27 +24,40 @@
### 📦 立即体验
- 安装版:[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]&#40;https://github.com/ArvinLovegood/go-stock/releases&#41;)
### 💬 支持大模型/平台
| 模型 | 状态 | 备注 |
| --- | --- |---------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [OpenAI](https://platform.openai.com/) | ✅ | 可接入任何 OpenAI 接口格式模型 |
| [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 |
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型运行平台 |
| [AnythingLLM](https://anythingllm.com/) | ✅ | 本地知识库 |
| [DeepSeek](https://www.deepseek.com/) | ✅ | deepseek-reasoner,deepseek-chat |
| [大模型聚合平台](https://cloud.siliconflow.cn/i/foufCerk) | ✅ | 如:[硅基流动](https://cloud.siliconflow.cn/i/foufCerk)[火山方舟](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ) ,[优云智算](https://www.compshare.cn/image-community?ytag=GPU_YY-gh_gostock) |
| 模型 | 状态 | 备注 |
| --- | --- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [OpenAI](https://platform.openai.com/) | ✅ | 可接入任何 OpenAI 接口格式模型 |
| [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 |
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型运行平台 |
| [AnythingLLM](https://anythingllm.com/) | ✅ | 本地知识库 |
| [DeepSeek](https://www.deepseek.com/) | ✅ | deepseek-reasoner,deepseek-chat |
| [大模型聚合平台](https://cloud.siliconflow.cn/i/foufCerk) | ✅ | 如:[302.AI](https://share.302.ai/1KUpfG)[硅基流动](https://cloud.siliconflow.cn/i/foufCerk)[火山方舟](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ) |
### <span style="color: #568DF4;">各位亲爱的朋友们,如果您对这个项目感兴趣,请先给我一个<i style="color: #EA2626;">star</i>吧,谢谢!</span>💕
- 优云智算by UCloud万卡规模4090免费用10小时新人注册另增50万tokens海量热门源项目镜像一键部署[注册链接](https://www.compshare.cn/image-community?ytag=GPU_YY-gh_gostock)
- 经测试目前硅基流动(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)
- 302.AI新用户使用邀请码注册即可领取 $1 测试额度![注册链接](https://share.302.ai/1KUpfG)
[//]: # (- 优云智算by UCloud万卡规模4090免费用10小时新人注册另增50万tokens海量热门源项目镜像一键部署[注册链接]&#40;https://www.compshare.cn/image-community?ytag=GPU_YY-gh_gostock&#41;)
- 火山方舟新用户每个模型注册即送50万tokens[注册链接](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ)
- 硅基流动(siliconflow)注册即送2000万Tokens[注册链接](https://cloud.siliconflow.cn/i/foufCerk)
- 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 +71,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 添加热门股票、事件和话题功能

824
app.go

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,8 @@
package main
import (
"github.com/wailsapp/wails/v2/pkg/runtime"
"go-stock/backend/agent"
"go-stock/backend/data"
"go-stock/backend/models"
)
@@ -57,5 +59,15 @@ 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()
}
func (a App) ChatWithAgent(question string, aiConfigId int, sysPromptId *int) {
ch := agent.NewStockAiAgentApi().Chat(question, aiConfigId, sysPromptId)
for msg := range ch {
runtime.EventsEmit(a.ctx, "agent-message", msg)
}
}

View File

@@ -6,303 +6,168 @@ package main
import (
"context"
"fmt"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/strutil"
"github.com/gen2brain/beeep"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go-stock/backend/data"
"go-stock/backend/db"
"go-stock/backend/logger"
"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{}) {
config := data.GetSettingConfig()
//setMap := optionalData[0].(map[string]interface{})
//
//// 将 map 转换为 JSON 字节切片
//jsonData, err := json.Marshal(setMap)
//if err != nil {
// logger.SugaredLogger.Errorf("Marshal error:%s", err.Error())
// return
//}
//// 将 JSON 字节切片解析到结构体中
//err = json.Unmarshal(jsonData, config)
//if err != nil {
// logger.SugaredLogger.Errorf("Unmarshal error:%s", err.Error())
// return
//}
logger.SugaredLogger.Infof("updateSettings config:%+v", config)
if config.DarkTheme {
runtime.WindowSetBackgroundColour(ctx, 27, 38, 54, 1)
runtime.WindowSetDarkTheme(ctx)
} else {
runtime.WindowSetBackgroundColour(ctx, 255, 255, 255, 1)
runtime.WindowSetLightTheme(ctx)
}
runtime.WindowReloadApp(ctx)
})
// 创建 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 +176,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

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

View File

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

214
app_windows.go Normal file
View File

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

92
backend/agent/agent.go Normal file
View File

@@ -0,0 +1,92 @@
package agent
import (
"context"
"go-stock/backend/agent/tools"
"go-stock/backend/data"
"go-stock/backend/logger"
"time"
"github.com/cloudwego/eino-ext/components/model/ark"
"github.com/cloudwego/eino-ext/components/model/deepseek"
"github.com/cloudwego/eino-ext/components/model/openai"
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/flow/agent/react"
"github.com/cloudwego/eino/schema"
)
// GetStockAiAgent @Author spark
// @Date 2025/8/4 16:17
// @Desc
// -----------------------------------------------------------------------------------
func GetStockAiAgent(ctx *context.Context, aiConfig data.AIConfig) *react.Agent {
logger.SugaredLogger.Infof("GetStockAiAgent aiConfig: %v", aiConfig)
temperature := float32(aiConfig.Temperature)
var toolableChatModel model.ToolCallingChatModel
var err error
if aiConfig.BaseUrl == "https://ark.cn-beijing.volces.com/api/v3" {
toolableChatModel, err = ark.NewChatModel(context.Background(), &ark.ChatModelConfig{
BaseURL: aiConfig.BaseUrl,
Model: aiConfig.ModelName,
APIKey: aiConfig.ApiKey,
MaxTokens: &aiConfig.MaxTokens,
Temperature: &temperature,
})
} else if aiConfig.BaseUrl == "https://api.deepseek.com" {
toolableChatModel, err = deepseek.NewChatModel(*ctx, &deepseek.ChatModelConfig{
BaseURL: aiConfig.BaseUrl,
Model: aiConfig.ModelName,
APIKey: aiConfig.ApiKey,
Timeout: time.Duration(aiConfig.TimeOut) * time.Second,
MaxTokens: aiConfig.MaxTokens,
Temperature: temperature,
})
} else {
toolableChatModel, err = openai.NewChatModel(*ctx, &openai.ChatModelConfig{
BaseURL: aiConfig.BaseUrl,
Model: aiConfig.ModelName,
APIKey: aiConfig.ApiKey,
Timeout: time.Duration(aiConfig.TimeOut) * time.Second,
MaxTokens: &aiConfig.MaxTokens,
Temperature: &temperature,
})
}
if err != nil {
logger.SugaredLogger.Error(err.Error())
return nil
}
// 初始化所需的 tools
aiTools := compose.ToolsNodeConfig{
Tools: []tool.BaseTool{
tools.GetQueryEconomicDataTool(),
tools.GetQueryStockPriceInfoTool(),
tools.GetQueryStockCodeInfoTool(),
tools.GetQueryMarketNewsTool(),
tools.GetChoiceStockByIndicatorsTool(),
tools.GetStockKLineTool(),
tools.GetInteractiveAnswerDataTool(),
tools.GetFinancialReportTool(),
tools.GetQueryStockNewsTool(),
tools.GetIndustryResearchReportTool(),
},
}
// 创建 agent
agent, err := react.NewAgent(*ctx, &react.AgentConfig{
ToolCallingModel: toolableChatModel,
ToolsConfig: aiTools,
MaxStep: len(aiTools.Tools)*3 + 2,
MessageModifier: func(ctx context.Context, input []*schema.Message) []*schema.Message {
return input
},
})
if err != nil {
logger.SugaredLogger.Error(err.Error())
return nil
}
return agent
}

View File

@@ -0,0 +1,91 @@
package agent
import (
"context"
"errors"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/flow/agent"
"github.com/cloudwego/eino/flow/agent/react"
"github.com/cloudwego/eino/schema"
"github.com/samber/lo"
"go-stock/backend/agent/tool_logger"
"go-stock/backend/data"
"go-stock/backend/logger"
"io"
)
// @Author spark
// @Date 2025/8/7 9:07
// @Desc
// -----------------------------------------------------------------------------------
type StockAiAgent struct {
*react.Agent
}
func NewStockAiAgentApi() *StockAiAgent {
return &StockAiAgent{}
}
func (receiver StockAiAgent) newStockAiAgent(ctx *context.Context, aiConfigId int) *StockAiAgent {
settingConfig := data.GetSettingConfig()
aiConfig, ok := lo.Find(settingConfig.AiConfigs, func(item *data.AIConfig) bool {
return uint(aiConfigId) == item.ID
})
if !ok {
return nil
}
return &StockAiAgent{
Agent: GetStockAiAgent(ctx, *aiConfig),
}
}
func (receiver StockAiAgent) Chat(question string, aiConfigId int, sysPromptId *int) chan *schema.Message {
ch := make(chan *schema.Message, 512)
ctx := context.Background()
stockAiAgent := receiver.newStockAiAgent(&ctx, aiConfigId)
sysPrompt := ""
if sysPromptId == nil || *sysPromptId == 0 {
sysPrompt = "你现在扮演一位拥有20年实战经验的顶级股票投资大师精通价值投资、趋势交易、量化分析等多种策略。你擅长结合宏观经济、行业周期和企业基本面进行全方位、精准的多维分析尤其对A股、港股、美股市场有深刻理解始终秉持“风险控制第一”的原则善于用通俗易懂的方式传授投资智慧。"
} else {
sysPrompt = data.NewPromptTemplateApi().GetPromptTemplateByID(*sysPromptId)
}
agentOption := []agent.AgentOption{
agent.WithComposeOptions(compose.WithCallbacks(&tool_logger.LoggerCallback{MessageChanel: ch})),
//react.WithChatModelOptions(ark.WithCache(cacheOption)),
}
go func() {
defer close(ch)
sr, err := stockAiAgent.Stream(ctx, []*schema.Message{
{
Role: schema.System,
Content: sysPrompt,
},
{
Role: schema.User,
Content: question,
},
}, agentOption...)
if err != nil {
logger.SugaredLogger.Errorf("stream error: %v", err)
return
}
defer sr.Close()
for {
msg, err := sr.Recv()
if err != nil {
if errors.Is(err, io.EOF) {
// finish
break
}
// error
logger.SugaredLogger.Errorf("failed to recv: %v", err)
break
}
logger.SugaredLogger.Infof("stream: %s", msg.String())
ch <- msg
}
}()
return ch
}

View File

@@ -0,0 +1,84 @@
package agent
import (
"context"
"errors"
"go-stock/backend/agent/tool_logger"
"go-stock/backend/data"
"go-stock/backend/db"
"go-stock/backend/logger"
"io"
"strings"
"testing"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/flow/agent"
"github.com/cloudwego/eino/schema"
)
// @Author spark
// @Date 2025/8/4 17:32
// @Desc
//-----------------------------------------------------------------------------------
func TestGetStockAiAgent(t *testing.T) {
ctx := context.Background()
db.Init("../../data/stock.db")
config := data.GetSettingConfig()
aiAgent := GetStockAiAgent(&ctx, *config.AiConfigs[0])
opt := []agent.AgentOption{
agent.WithComposeOptions(compose.WithCallbacks(&tool_logger.LoggerCallback{})),
//react.WithChatModelOptions(ark.WithCache(cacheOption)),
}
sr, err := aiAgent.Stream(ctx, []*schema.Message{
{
Role: schema.System,
Content: config.Settings.Prompt + "",
},
{
Role: schema.User,
Content: "结合以上提供的宏观经济数据/市场指数行情/国内外市场资讯/电报/会议/事件/投资者关注的问题,\n结合宏观经济事件驱动政策支持投资者关注的问题分析当前市场情绪和热点 找出有潜力/优质的板块/行业/概念/标的/主题,\n多因子深度分析计算上涨或下跌的逻辑和概率\n最后按风险和投资周期给出具体推荐标的操作建议",
},
}, opt...)
if err != nil {
logger.SugaredLogger.Errorf("stream error: %v", err)
return
}
defer sr.Close() // remember to close the stream
md := strings.Builder{}
for {
msg, err := sr.Recv()
if err != nil {
if errors.Is(err, io.EOF) {
// finish
break
}
// error
logger.SugaredLogger.Errorf("failed to recv: %v", err)
return
}
//logger.SugaredLogger.Infof("stream recv: %v", msg)
if msg.ReasoningContent != "" {
md.WriteString(msg.ReasoningContent)
}
if msg.Content != "" {
md.WriteString(msg.Content)
}
}
logger.SugaredLogger.Info(md.String())
//logger.SugaredLogger.Infof("stream done:\n%s", md.String())
}
func TestAgent(t *testing.T) {
db.Init("../../data/stock.db")
ch := NewStockAiAgentApi().Chat("分析一下海立股份,使用工具", 1, nil)
for message := range ch {
logger.SugaredLogger.Infof("res:%s", message.String())
}
}

View File

@@ -0,0 +1,98 @@
package tool_logger
import (
"context"
"encoding/json"
"errors"
"go-stock/backend/logger"
"io"
"github.com/cloudwego/eino/callbacks"
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/flow/agent/react"
"github.com/cloudwego/eino/schema"
)
// @Author spark
// @Date 2025/8/5 10:21
// @Desc
//-----------------------------------------------------------------------------------
type LoggerCallback struct {
MessageChanel chan *schema.Message
callbacks.HandlerBuilder // 可以用 callbacks.HandlerBuilder 来辅助实现 callback
}
func (cb *LoggerCallback) OnStart(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context {
logger.SugaredLogger.Infof("==================")
inputStr, _ := json.MarshalIndent(input, "", " ") // nolint: byted_s_returned_err_check
logger.SugaredLogger.Infof("[OnStart] %s\n", string(inputStr))
modelCallbackInput := model.ConvCallbackInput(input)
if modelCallbackInput != nil {
for _, message := range modelCallbackInput.Messages {
cb.MessageChanel <- message
}
}
return ctx
}
func (cb *LoggerCallback) OnEnd(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context {
logger.SugaredLogger.Infof("=========[OnEnd]=========")
outputStr, _ := json.MarshalIndent(output, "", " ") // nolint: byted_s_returned_err_check
logger.SugaredLogger.Infof(string(outputStr))
return ctx
}
func (cb *LoggerCallback) OnError(ctx context.Context, info *callbacks.RunInfo, err error) context.Context {
logger.SugaredLogger.Infof("=========[OnError]=========")
logger.SugaredLogger.Infof("%s", err.Error())
return ctx
}
func (cb *LoggerCallback) OnEndWithStreamOutput(ctx context.Context, info *callbacks.RunInfo,
output *schema.StreamReader[callbacks.CallbackOutput]) context.Context {
var graphInfoName = react.GraphName
go func() {
defer func() {
if err := recover(); err != nil {
logger.SugaredLogger.Infof("[OnEndStream] panic err:", err)
}
}()
defer output.Close() // remember to close the stream in defer
logger.SugaredLogger.Infof("=========[OnEndStream]=========")
for {
frame, err := output.Recv()
if errors.Is(err, io.EOF) {
// finish
break
}
if err != nil {
logger.SugaredLogger.Infof("internal error: %s\n", err)
return
}
s, err := json.Marshal(frame)
if err != nil {
logger.SugaredLogger.Infof("internal error: %s\n", err)
return
}
if info.Name == graphInfoName { // 仅打印 graph 的输出, 否则每个 stream 节点的输出都会打印一遍
logger.SugaredLogger.Infof("%s: %s\n", info.Name, string(s))
}
}
}()
return ctx
}
func (cb *LoggerCallback) OnStartWithStreamInput(ctx context.Context, info *callbacks.RunInfo,
input *schema.StreamReader[callbacks.CallbackInput]) context.Context {
defer input.Close()
return ctx
}

View File

@@ -0,0 +1,139 @@
package tools
import (
"context"
"encoding/json"
"fmt"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/schema"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/random"
"go-stock/backend/data"
)
// @Author spark
// @Date 2025/8/5 11:17
// @Desc
//-----------------------------------------------------------------------------------
func GetChoiceStockByIndicatorsTool() tool.InvokableTool {
return &ChoiceStockByIndicators{}
}
type ChoiceStockByIndicators struct {
}
func (c ChoiceStockByIndicators) Info(ctx context.Context) (*schema.ToolInfo, error) {
return &schema.ToolInfo{
Name: "ChoiceStockByIndicators",
Desc: "根据自然语言筛选股票,返回自然语言选股条件要求的股票所有相关数据。输入股票名称可以获取当前股票最新的股价交易数据和基础财务指标信息,多个股票名称使用,分隔。",
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
"words": {
Type: "string",
Desc: "选股自然语言。" +
"例:上海贝岭,macd,rsi,kdj,boll,5日均线,14日均线,30日均线,60日均线,成交量,OBV,EMA" +
"例1创新药,半导体;PE<30;净利润增长率>50%。 " +
"例2上证指数,科创50。 " +
"例3长电科技,上海贝岭。" +
"例4长电科技,上海贝岭;KDJ,MACD,RSI,BOLL,主力净流入/流出" +
"例5换手率大于3%小于25%.量比1以上. 10日内有过涨停.股价处于峰值的二分之一以下.流通股本<100亿.当日和连续四日净流入;股价在20日均线以上.分时图股价在均线之上.热门板块下涨幅领先的A股. 当日量能20000手以上.沪深个股.近一年市盈率波动小于150%.MACD金叉;不要ST股及不要退市股非北交所每股收益>0。" +
"例6沪深主板.流通市值小于100亿.市值大于10亿.60分钟dif大于dea.60分钟skdj指标k值大于d值.skdj指标k值小于90.换手率大于3%.成交额大于1亿元.量比大于2.涨幅大于2%小于7%.股价大于5小于50.创业板.10日均线大于20日均线;不要ST股及不要退市股;不要北交所;不要科创板;不要创业板。" +
"例7股价在20日线上一月之内涨停次数>=1量比大于1换手率大于3%,流通市值大于 50亿小于200亿。" +
"例8基本条件前期有爆量回调到 10 日线,当日是缩量阴线,均线趋势向上。;优选条件:一月之内涨停次数>=1" +
"例9今日涨幅大于等于2%小于等于9%;量比大于等于1.1小于等于5;换手率大于等于5%小于等于20%;市值大于等于30小于等于300亿;5日、10日、30日、60日均线、5周、10周、30周、60周均线多头排列",
Required: true,
},
}),
}, nil
}
func (c ChoiceStockByIndicators) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
parms := map[string]any{}
err := json.Unmarshal([]byte(argumentsInJSON), &parms)
if err != nil {
return "", err
}
content := "无符合条件的数据"
words := parms["words"].(string)
res := data.NewSearchStockApi(words).SearchStock(random.RandInt(5, 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"
}
return content, nil
}
// JSONToMarkdownTable 将JSON数据转换为Markdown表格
func JSONToMarkdownTable(jsonData []byte) (string, error) {
var data []map[string]interface{}
err := json.Unmarshal(jsonData, &data)
if err != nil {
return "", err
}
if len(data) == 0 {
return "", nil
}
// 获取表头
headers := []string{}
for key := range data[0] {
headers = append(headers, key)
}
// 构建表头行
headerRow := "|"
for _, header := range headers {
headerRow += fmt.Sprintf(" %s |", header)
}
headerRow += "\n"
// 构建分隔行
separatorRow := "|"
for range headers {
separatorRow += " --- |"
}
separatorRow += "\n"
// 构建数据行
bodyRows := ""
for _, rowData := range data {
bodyRow := "|"
for _, header := range headers {
value := rowData[header]
bodyRow += fmt.Sprintf(" %v |", value)
}
bodyRows += bodyRow + "\n"
}
return headerRow + separatorRow + bodyRows, nil
}

View File

@@ -0,0 +1,35 @@
package tools
import (
"github.com/duke-git/lancet/v2/strutil"
"strings"
)
// @Author spark
// @Date 2025/8/5 17:20
// @Desc
//-----------------------------------------------------------------------------------
func GetStockCode(dcCode string) string {
if strutil.ContainsAny(dcCode, []string{"."}) {
sp := strings.Split(dcCode, ".")
return strings.ToLower(sp[1] + sp[0])
}
//北京证券交易所 883、87、88 等) 创新型中小企业(专精特新为主)
//上海证券交易所 660、688 等) 大盘蓝筹、科创板(高新技术)
//深圳证券交易所 0、3000、002、30 等) 中小盘、创业板(成长型创新企业)
switch dcCode[0:1] {
case "8":
return "bj" + dcCode
case "9":
return "bj" + dcCode
case "6":
return "sh" + dcCode
case "0":
return "sz" + dcCode
case "3":
return "sz" + dcCode
}
return dcCode
}

View File

@@ -0,0 +1,79 @@
package tools
import (
"context"
"encoding/json"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/schema"
"go-stock/backend/data"
"go-stock/backend/util"
"strings"
)
// @Author spark
// @Date 2025/8/4 16:38
// @Desc
//-----------------------------------------------------------------------------------
func GetQueryEconomicDataTool() tool.InvokableTool {
return &ToolQueryEconomicData{}
}
type ToolQueryEconomicData struct {
}
func (t ToolQueryEconomicData) Info(ctx context.Context) (*schema.ToolInfo, error) {
return &schema.ToolInfo{
Name: "QueryEconomicData",
Desc: "查询宏观经济数据(GDP,CPI,PPI,PMI)",
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
"flag": {
Type: "string",
Desc: "all:宏观经济数据(GDP,CPI,PPI,PMI);GDP:国内生产总值;CPI:居民消费价格指数;PPI:工业品出厂价格指数;PMI:采购经理人指数",
Required: false,
},
}),
}, nil
}
func (t ToolQueryEconomicData) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
parms := map[string]any{}
err := json.Unmarshal([]byte(argumentsInJSON), &parms)
if err != nil {
return "", err
}
var market strings.Builder
switch parms["flag"].(string) {
case "GDP":
res := data.NewMarketNewsApi().GetGDP()
md := util.MarkdownTableWithTitle("国内生产总值(GDP)", res.GDPResult.Data)
market.WriteString(md)
case "CPI":
res2 := data.NewMarketNewsApi().GetCPI()
md2 := util.MarkdownTableWithTitle("居民消费价格指数(CPI)", res2.CPIResult.Data)
market.WriteString(md2)
case "PPI":
res3 := data.NewMarketNewsApi().GetPPI()
md3 := util.MarkdownTableWithTitle("工业品出厂价格指数(PPI)", res3.PPIResult.Data)
market.WriteString(md3)
case "PMI":
res4 := data.NewMarketNewsApi().GetPMI()
md4 := util.MarkdownTableWithTitle("商品价格指数(PMI)", res4.PMIResult.Data)
market.WriteString(md4)
default:
res := data.NewMarketNewsApi().GetGDP()
md := util.MarkdownTableWithTitle("国内生产总值(GDP)", res.GDPResult.Data)
market.WriteString(md)
res2 := data.NewMarketNewsApi().GetCPI()
md2 := util.MarkdownTableWithTitle("居民消费价格指数(CPI)", res2.CPIResult.Data)
market.WriteString(md2)
res3 := data.NewMarketNewsApi().GetPPI()
md3 := util.MarkdownTableWithTitle("工业品出厂价格指数(PPI)", res3.PPIResult.Data)
market.WriteString(md3)
res4 := data.NewMarketNewsApi().GetPMI()
md4 := util.MarkdownTableWithTitle("采购经理人指数(PMI)", res4.PMIResult.Data)
market.WriteString(md4)
}
return market.String(), nil
}

View File

@@ -0,0 +1,50 @@
package tools
import (
"context"
"fmt"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/schema"
"github.com/tidwall/gjson"
"go-stock/backend/data"
"strings"
)
// @Author spark
// @Date 2025/8/5 15:49
// @Desc
//-----------------------------------------------------------------------------------
func GetFinancialReportTool() tool.InvokableTool {
return &FinancialReportTool{}
}
type FinancialReportTool struct {
}
func (f FinancialReportTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
return &schema.ToolInfo{
Name: "GetFinancialReport",
Desc: "查询股票财务报表数据",
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
"stockCode": {
Type: "string",
Desc: "股票代码A股sh,sz开头;港股hk开头,美股us开头不能批量查询",
Required: true,
},
}),
}, nil
}
func (f FinancialReportTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
stockCode := gjson.Get(argumentsInJSON, "stockCode").String()
messages := data.GetFinancialReportsByXUEQIU(GetStockCode(stockCode), 30)
if messages == nil || len(*messages) == 0 {
return "", fmt.Errorf("没有找到%s的财务报告", stockCode)
}
md := strings.Builder{}
for _, s := range *messages {
md.WriteString(s)
}
return md.String(), nil
}

View File

@@ -0,0 +1,69 @@
package tools
import (
"context"
"go-stock/backend/data"
log "go-stock/backend/logger"
"strings"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/schema"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/strutil"
"github.com/tidwall/gjson"
)
// @Author spark
// @Date 2025/8/9 18:48
// @Desc
//-----------------------------------------------------------------------------------
func GetIndustryResearchReportTool() tool.InvokableTool {
return &IndustryResearchReportTool{api: data.NewMarketNewsApi()}
}
type IndustryResearchReportTool struct {
api *data.MarketNewsApi
}
func (i IndustryResearchReportTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
return &schema.ToolInfo{
Name: "GetIndustryResearchReport",
Desc: "获取行业/板块研究报告",
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
"name": {
Type: "string",
Desc: "行业/板块行业名称",
Required: false,
},
"code": {
Type: "string",
Desc: "行业/板块代码",
Required: true,
},
}),
}, nil
}
func (i IndustryResearchReportTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
code := gjson.Get(argumentsInJSON, "code").String()
code = strutil.ReplaceWithMap(code, map[string]string{
"-": "",
"_": "",
"bk": "",
"BK": "",
"bk0": "",
"BK0": "",
})
log.SugaredLogger.Debugf("code:%s", code)
codeStr := convertor.ToString(code)
resp := i.api.IndustryResearchReport(codeStr, 7)
md := strings.Builder{}
for _, a := range resp {
data := a.(map[string]any)
md.WriteString(i.api.GetIndustryReportInfo(data["infoCode"].(string)))
}
log.SugaredLogger.Debugf("codeNum:%s IndustryResearchReport:\n %s", code, md.String())
return md.String(), nil
}

View File

@@ -0,0 +1,64 @@
package tools
import (
"context"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/schema"
"github.com/duke-git/lancet/v2/convertor"
"github.com/tidwall/gjson"
"go-stock/backend/data"
"go-stock/backend/util"
)
// @Author spark
// @Date 2025/8/5 12:46
// @Desc
//-----------------------------------------------------------------------------------
func GetInteractiveAnswerDataTool() tool.InvokableTool {
return &InteractiveAnswerDataTool{}
}
type InteractiveAnswerDataTool struct {
}
func (i InteractiveAnswerDataTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
return &schema.ToolInfo{
Name: "QueryInteractiveAnswerData",
Desc: "获取投资者与上市公司互动问答的数据,反映当前投资者关注的热点问题。",
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
"page": {
Type: "string",
Desc: "分页号",
Required: true,
},
"pageSize": {
Type: "string",
Desc: "分页大小",
Required: true,
},
"keyWord": {
Type: "string",
Desc: "搜索关键词,多个关键词空格隔开(可输入股票名称或者当前热门板块/行业/概念/标的/事件等)",
Required: false,
},
}),
}, nil
}
func (i InteractiveAnswerDataTool) InvokableRun(ctx context.Context, funcArguments string, opts ...tool.Option) (string, error) {
page := gjson.Get(funcArguments, "page").String()
pageSize := gjson.Get(funcArguments, "pageSize").String()
keyWord := gjson.Get(funcArguments, "keyWord").String()
pageNo, err := convertor.ToInt(page)
if err != nil {
pageNo = 1
}
pageSizeNum, err := convertor.ToInt(pageSize)
if err != nil {
pageSizeNum = 50
}
datas := data.NewMarketNewsApi().InteractiveAnswer(int(pageNo), int(pageSizeNum), keyWord)
content := util.MarkdownTableWithTitle("投资互动数据", datas.Results)
return content, nil
}

View File

@@ -0,0 +1,79 @@
package tools
import (
"context"
"encoding/json"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/schema"
"github.com/duke-git/lancet/v2/random"
"github.com/tidwall/gjson"
"go-stock/backend/data"
"go-stock/backend/logger"
"strings"
)
// @Author spark
// @Date 2025/8/4 16:38
// @Desc
//-----------------------------------------------------------------------------------
func GetQueryMarketNewsTool() tool.InvokableTool {
return &QueryMarketNews{}
}
type QueryMarketNews struct {
}
func (q QueryMarketNews) Info(ctx context.Context) (*schema.ToolInfo, error) {
return &schema.ToolInfo{
Name: "QueryMarketNews",
Desc: "国内外市场资讯/电报/会议/事件",
}, nil
}
func (q QueryMarketNews) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
md := strings.Builder{}
res := data.NewMarketNewsApi().ClsCalendar()
for _, a := range res {
bytes, err := json.Marshal(a)
if err != nil {
continue
}
//logger.SugaredLogger.Debugf("value: %+v", string(bytes))
date := gjson.Get(string(bytes), "calendar_day")
md.WriteString("\n### 事件/会议日期:" + date.String())
list := gjson.Get(string(bytes), "items")
//logger.SugaredLogger.Debugf("value: %+v,list: %+v", date.String(), list)
list.ForEach(func(key, value gjson.Result) bool {
logger.SugaredLogger.Debugf("key: %+v,value: %+v", key.String(), gjson.Get(value.String(), "title"))
md.WriteString("\n- " + gjson.Get(value.String(), "title").String())
return true
})
}
news := data.NewMarketNewsApi().GetNewsList("财联社电报", random.RandInt(100, 500))
messageText := strings.Builder{}
for _, telegraph := range *news {
messageText.WriteString("## " + telegraph.Time + ":" + "\n")
messageText.WriteString("### " + telegraph.Content + "\n")
}
md.WriteString("\n### 市场资讯:\n" + messageText.String())
resp := data.NewMarketNewsApi().TradingViewNews()
var newsText strings.Builder
for _, a := range *resp {
logger.SugaredLogger.Debugf("TradingViewNews: %s", a.Title)
newsText.WriteString(a.Title + "\n")
}
md.WriteString("\n### 全球新闻资讯:\n" + newsText.String())
reutersNew := data.NewMarketNewsApi().ReutersNew()
reutersNewMessageText := strings.Builder{}
for _, article := range reutersNew.Result.Articles {
reutersNewMessageText.WriteString("## " + article.Title + "\n")
reutersNewMessageText.WriteString("### " + article.Description + "\n")
}
md.WriteString("\n### 外媒全球新闻资讯:\n" + reutersNewMessageText.String())
return md.String(), nil
}

View File

@@ -0,0 +1,49 @@
package tools
import (
"context"
"encoding/json"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/schema"
"go-stock/backend/data"
)
// @Author spark
// @Date 2025/8/4 18:25
// @Desc
//-----------------------------------------------------------------------------------
func GetQueryStockCodeInfoTool() tool.InvokableTool {
return &QueryStockCodeInfo{}
}
type QueryStockCodeInfo struct {
}
func (q QueryStockCodeInfo) Info(ctx context.Context) (*schema.ToolInfo, error) {
return &schema.ToolInfo{
Name: "QueryStockCodeInfo",
Desc: "查询股票/指数信息(股票/指数名称,股票/指数代码,股票/指数拼音,股票/指数拼音首字母,股票/指数交易所等",
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
"searchWord": {
Type: "string",
Desc: "股票搜索关键词",
Required: true,
},
}),
}, nil
}
func (q QueryStockCodeInfo) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
parms := map[string]any{}
err := json.Unmarshal([]byte(argumentsInJSON), &parms)
if err != nil {
return "", err
}
stockList := data.NewStockDataApi().GetStockList(parms["searchWord"].(string))
marshal, err := json.Marshal(stockList)
if err != nil {
return "", err
}
return string(marshal), nil
}

View File

@@ -0,0 +1,80 @@
package tools
import (
"context"
"encoding/json"
"fmt"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/schema"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/strutil"
"github.com/tidwall/gjson"
"go-stock/backend/data"
)
// @Author spark
// @Date 2025/8/5 11:31
// @Desc
//-----------------------------------------------------------------------------------
func GetStockKLineTool() tool.InvokableTool {
return &QueryStockKLine{}
}
type QueryStockKLine struct {
}
func (q QueryStockKLine) Info(ctx context.Context) (*schema.ToolInfo, error) {
return &schema.ToolInfo{
Name: "QueryStockKLine",
Desc: "获取股票K线数据。输入股票名称和K线周期返回股票K线数据。",
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
"days": {
Type: "string",
Desc: "日K数据条数。",
Required: true,
},
"stockCode": {
Type: "string",
Desc: "股票代码A股sh,sz开头;港股hk开头,美股us开头",
Required: true,
},
}),
}, nil
}
func (q QueryStockKLine) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
stockCode := GetStockCode(gjson.Get(argumentsInJSON, "stockCode").String())
days := gjson.Get(argumentsInJSON, "days").String()
toIntDay, err := convertor.ToInt(days)
if err != nil {
toIntDay = 90
}
if strutil.HasPrefixAny(stockCode, []string{"sz", "sh", "hk", "us", "gb_"}) {
K := &[]data.KLineData{}
if strutil.HasPrefixAny(stockCode, []string{"sz", "sh"}) {
K = data.NewStockDataApi().GetKLineData(stockCode, "240", toIntDay)
}
if strutil.HasPrefixAny(stockCode, []string{"hk", "us", "gb_"}) {
K = data.NewStockDataApi().GetHK_KLineData(stockCode, "day", toIntDay)
}
Kmap := &[]map[string]any{}
for _, kline := range *K {
mapk := make(map[string]any, 6)
mapk["日期"] = kline.Day
mapk["开盘价"] = kline.Open
mapk["最高价"] = kline.High
mapk["最低价"] = kline.Low
mapk["收盘价"] = kline.Close
Volume, _ := convertor.ToFloat(kline.Volume)
mapk["成交量(万手)"] = Volume / 10000.00 / 100.00
*Kmap = append(*Kmap, mapk)
}
jsonData, _ := json.Marshal(Kmap)
markdownTable, _ := JSONToMarkdownTable(jsonData)
res := "\r\n ### " + stockCode + " " + convertor.ToString(toIntDay) + "日K线数据\r\n" + markdownTable + "\r\n"
return res, nil
} else {
return "无数据可能股票代码错误。A股sh,sz开头;港股hk开头,美股us开头", fmt.Errorf("不支持的股票代码:%s", stockCode)
}
}

View File

@@ -0,0 +1,42 @@
package tools
import (
"context"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/schema"
"github.com/tidwall/gjson"
"go-stock/backend/data"
"go-stock/backend/util"
)
// @Author spark
// @Date 2025/8/5 16:27
// @Desc
//-----------------------------------------------------------------------------------
func GetQueryStockNewsTool() tool.InvokableTool {
return &QueryStockNewsTool{}
}
type QueryStockNewsTool struct {
}
func (q QueryStockNewsTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
return &schema.ToolInfo{
Name: "QueryStockNewsTool",
Desc: "按关键词搜索相关市场资讯/新闻",
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
"searchWords": {
Type: "string",
Desc: "搜索关键词(多个关键词使用空格分隔)",
Required: true,
},
}),
}, nil
}
func (q QueryStockNewsTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
searchWords := gjson.Get(argumentsInJSON, "searchWords").String()
res := data.NewMarketNewsApi().CailianpressWeb(searchWords)
return util.MarkdownTableWithTitle(searchWords+"市场资讯/新闻", res.List), nil
}

View File

@@ -0,0 +1,57 @@
package tools
import (
"context"
"encoding/json"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/schema"
"go-stock/backend/data"
"strings"
)
// @Author spark
// @Date 2025/8/4 17:58
// @Desc
//-----------------------------------------------------------------------------------
func GetQueryStockPriceInfoTool() tool.InvokableTool {
return &ToolQueryStockPriceInfo{}
}
type ToolQueryStockPriceInfo struct{}
func (t ToolQueryStockPriceInfo) Info(ctx context.Context) (*schema.ToolInfo, error) {
return &schema.ToolInfo{
Name: "QueryStockPriceInfo",
Desc: "批量获取实时股价数据",
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
"stockCodes": {
Type: "string",
Desc: "股票代码,多个,隔开,股票代码必须转化为sh或者sz或者hk开头的形式例如sz399001,sh600859",
Required: true,
},
}),
}, nil
}
func (t ToolQueryStockPriceInfo) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
parms := map[string]any{}
err := json.Unmarshal([]byte(argumentsInJSON), &parms)
if err != nil {
return "", err
}
stockCodes := strings.Split(parms["stockCodes"].(string), ",")
var codes []string
for _, code := range stockCodes {
codes = append(codes, GetStockCode(code))
}
realTimeData, err := data.NewStockDataApi().GetStockCodeRealTimeData(codes...)
if err != nil {
return "", err
}
marshal, err := json.Marshal(realTimeData)
if err != nil {
return "", err
}
return string(marshal), nil
}

View File

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

View File

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

View File

@@ -1,11 +1,13 @@
//go:build windows
// +build windows
package data
import (
"github.com/go-toast/toast"
"go-stock/backend/logger"
"testing"
"github.com/go-toast/toast"
)
// @Author spark

View File

@@ -27,7 +27,7 @@ func (c *CrawlerApi) NewCrawler(ctx context.Context, crawlerBaseInfo CrawlerBase
return CrawlerApi{
crawlerCtx: ctx,
crawlerBaseInfo: crawlerBaseInfo,
pool: NewBrowserPool(GetConfig().BrowserPoolSize),
pool: NewBrowserPool(GetSettingConfig().BrowserPoolSize),
}
}
func (c *CrawlerApi) GetHtml(url, waitVisible string, headless bool) (string, bool) {
@@ -39,7 +39,7 @@ func (c *CrawlerApi) GetHtml(url, waitVisible string, headless bool) (string, bo
}
func (c *CrawlerApi) GetHtml_old(url, waitVisible string, headless bool) (string, bool) {
htmlContent := ""
path := GetConfig().BrowserPath
path := GetSettingConfig().BrowserPath
//logger.SugaredLogger.Infof("Browser path:%s", path)
if path != "" {
pctx, pcancel := chromedp.NewExecAllocator(
@@ -102,7 +102,7 @@ func (c *CrawlerApi) GetHtml_old(url, waitVisible string, headless bool) (string
func (c *CrawlerApi) GetHtmlWithNoCancel(url, waitVisible string, headless bool) (html string, ok bool, parent context.CancelFunc, child context.CancelFunc) {
htmlContent := ""
path := GetConfig().BrowserPath
path := GetSettingConfig().BrowserPath
//logger.SugaredLogger.Infof("BrowserPath :%s", path)
var parentCancel context.CancelFunc
var childCancel context.CancelFunc
@@ -170,7 +170,7 @@ func (c *CrawlerApi) GetHtmlWithActions(actions *[]chromedp.Action, headless boo
htmlContent := ""
*actions = append(*actions, chromedp.InnerHTML("body", &htmlContent))
path := GetConfig().BrowserPath
path := GetSettingConfig().BrowserPath
//logger.SugaredLogger.Infof("GetHtmlWithActions path:%s", path)
if path != "" {
pctx, pcancel := chromedp.NewExecAllocator(

View File

@@ -21,7 +21,7 @@ func NewDingDingAPI() *DingDingAPI {
}
func (DingDingAPI) SendDingDingMessage(message string) string {
if GetConfig().DingPushEnable == false {
if GetSettingConfig().DingPushEnable == false {
//logger.SugaredLogger.Info("钉钉推送未开启")
return "钉钉推送未开启"
}
@@ -37,11 +37,9 @@ func (DingDingAPI) SendDingDingMessage(message string) string {
logger.SugaredLogger.Infof("send dingding message: %s", resp.String())
return "发送钉钉消息成功"
}
func GetConfig() *Settings {
return NewSettingsApi(&Settings{}).GetConfig()
}
func getApiURL() string {
return GetConfig().DingRobot
return GetSettingConfig().DingRobot
}
func (DingDingAPI) SendToDingDing(title, message string) string {

View File

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

View File

@@ -4,6 +4,14 @@ import (
"bytes"
"encoding/json"
"fmt"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"go-stock/backend/util"
"strconv"
"strings"
"time"
"github.com/PuerkitoBio/goquery"
"github.com/coocood/freecache"
"github.com/duke-git/lancet/v2/convertor"
@@ -12,12 +20,6 @@ import (
"github.com/robertkrimen/otto"
"github.com/samber/lo"
"github.com/tidwall/gjson"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"strconv"
"strings"
"time"
)
// @Author spark
@@ -36,7 +38,7 @@ func (m MarketNewsApi) GetNewTelegraph(crawlTimeOut int64) *[]models.Telegraph {
response, _ := resty.New().SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
SetHeader("Referer", "https://www.cls.cn/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get(fmt.Sprintf(url))
Get(url)
var telegraphs []models.Telegraph
//logger.SugaredLogger.Info(string(response.Body()))
document, _ := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
@@ -550,9 +552,14 @@ func (m MarketNewsApi) EMDictCode(code string, cache *freecache.Cache) []any {
}
func (m MarketNewsApi) TradingViewNews() *[]models.TVNews {
client := resty.New()
config := GetSettingConfig()
if config.HttpProxyEnabled && config.HttpProxy != "" {
client.SetProxy(config.HttpProxy)
}
TVNews := &[]models.TVNews{}
url := "https://news-mediator.tradingview.com/news-flow/v2/news?filter=lang:zh-Hans&filter=provider:panews,reuters&client=screener&streaming=false"
resp, err := resty.New().SetProxy("http://127.0.0.1:10809").SetTimeout(time.Duration(30)*time.Second).R().
resp, err := client.SetTimeout(time.Duration(5)*time.Second).R().
SetHeader("Host", "news-mediator.tradingview.com").
SetHeader("Origin", "https://cn.tradingview.com").
SetHeader("Referer", "https://cn.tradingview.com/").
@@ -599,7 +606,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 +708,233 @@ func (m MarketNewsApi) ClsCalendar() []any {
err = json.Unmarshal(resp.Body(), &respMap)
return respMap["data"].([]any)
}
func (m MarketNewsApi) GetGDP() *models.GDPResp {
res := &models.GDPResp{}
url := "https://datacenter-web.eastmoney.com/api/data/v1/get?callback=data&columns=REPORT_DATE%2CTIME%2CDOMESTICL_PRODUCT_BASE%2CFIRST_PRODUCT_BASE%2CSECOND_PRODUCT_BASE%2CTHIRD_PRODUCT_BASE%2CSUM_SAME%2CFIRST_SAME%2CSECOND_SAME%2CTHIRD_SAME&pageNumber=1&pageSize=20&sortColumns=REPORT_DATE&sortTypes=-1&source=WEB&client=WEB&reportName=RPT_ECONOMY_GDP&p=1&pageNo=1&pageNum=1&_=" + strconv.FormatInt(time.Now().Unix(), 10)
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "datacenter-web.eastmoney.com").
SetHeader("Origin", "https://datacenter.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/cjsj/gdp.html").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("GDP err:%s", err.Error())
return res
}
body := resp.Body()
logger.SugaredLogger.Debugf("GDP:%s", body)
vm := otto.New()
vm.Run("function data(res){return res};")
val, err := vm.Run(body)
if err != nil {
logger.SugaredLogger.Errorf("GDP err:%s", err.Error())
return res
}
data, _ := val.Object().Value().Export()
logger.SugaredLogger.Infof("GDP:%v", data)
marshal, err := json.Marshal(data)
if err != nil {
return res
}
json.Unmarshal(marshal, &res)
logger.SugaredLogger.Infof("GDP:%+v", res)
return res
}
func (m MarketNewsApi) GetCPI() *models.CPIResp {
res := &models.CPIResp{}
url := "https://datacenter-web.eastmoney.com/api/data/v1/get?callback=data&columns=REPORT_DATE%2CTIME%2CNATIONAL_SAME%2CNATIONAL_BASE%2CNATIONAL_SEQUENTIAL%2CNATIONAL_ACCUMULATE%2CCITY_SAME%2CCITY_BASE%2CCITY_SEQUENTIAL%2CCITY_ACCUMULATE%2CRURAL_SAME%2CRURAL_BASE%2CRURAL_SEQUENTIAL%2CRURAL_ACCUMULATE&pageNumber=1&pageSize=20&sortColumns=REPORT_DATE&sortTypes=-1&source=WEB&client=WEB&reportName=RPT_ECONOMY_CPI&p=1&pageNo=1&pageNum=1&_=" + strconv.FormatInt(time.Now().Unix(), 10)
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "datacenter-web.eastmoney.com").
SetHeader("Origin", "https://datacenter.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/cjsj/gdp.html").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("GetCPI err:%s", err.Error())
return res
}
body := resp.Body()
logger.SugaredLogger.Debugf("GetCPI:%s", body)
vm := otto.New()
vm.Run("function data(res){return res};")
val, err := vm.Run(body)
if err != nil {
logger.SugaredLogger.Errorf("GetCPI err:%s", err.Error())
return res
}
data, _ := val.Object().Value().Export()
logger.SugaredLogger.Infof("GetCPI:%v", data)
marshal, err := json.Marshal(data)
if err != nil {
return res
}
json.Unmarshal(marshal, &res)
logger.SugaredLogger.Infof("GetCPI:%+v", res)
return res
}
// GetPPI PPI
func (m MarketNewsApi) GetPPI() *models.PPIResp {
res := &models.PPIResp{}
url := "https://datacenter-web.eastmoney.com/api/data/v1/get?callback=data&columns=REPORT_DATE,TIME,BASE,BASE_SAME,BASE_ACCUMULATE&pageNumber=1&pageSize=20&sortColumns=REPORT_DATE&sortTypes=-1&source=WEB&client=WEB&reportName=RPT_ECONOMY_PPI&p=1&pageNo=1&pageNum=1&_=" + strconv.FormatInt(time.Now().Unix(), 10)
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "datacenter-web.eastmoney.com").
SetHeader("Origin", "https://datacenter.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/cjsj/gdp.html").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("GetPPI err:%s", err.Error())
return res
}
body := resp.Body()
vm := otto.New()
vm.Run("function data(res){return res};")
val, err := vm.Run(body)
if err != nil {
return res
}
data, _ := val.Object().Value().Export()
marshal, err := json.Marshal(data)
if err != nil {
return res
}
json.Unmarshal(marshal, &res)
return res
}
func (m MarketNewsApi) GetPMI() *models.PMIResp {
res := &models.PMIResp{}
url := "https://datacenter-web.eastmoney.com/api/data/v1/get?callback=data&columns=REPORT_DATE%2CTIME%2CMAKE_INDEX%2CMAKE_SAME%2CNMAKE_INDEX%2CNMAKE_SAME&pageNumber=1&pageSize=20&sortColumns=REPORT_DATE&sortTypes=-1&source=WEB&client=WEB&reportName=RPT_ECONOMY_PMI&p=1&pageNo=1&pageNum=1&_=" + strconv.FormatInt(time.Now().Unix(), 10)
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "datacenter-web.eastmoney.com").
SetHeader("Origin", "https://datacenter.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/cjsj/gdp.html").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
Get(url)
if err != nil {
return res
}
body := resp.Body()
vm := otto.New()
vm.Run("function data(res){return res};")
val, err := vm.Run(body)
if err != nil {
return res
}
data, _ := val.Object().Value().Export()
marshal, err := json.Marshal(data)
if err != nil {
return res
}
json.Unmarshal(marshal, &res)
return res
}
func (m MarketNewsApi) GetIndustryReportInfo(infoCode string) string {
url := "https://data.eastmoney.com/report/zw_industry.jshtml?infocode=" + infoCode
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "data.eastmoney.com").
SetHeader("Origin", "https://data.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/report/industry.jshtml").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("GetIndustryReportInfo err:%s", err.Error())
return ""
}
body := resp.Body()
//logger.SugaredLogger.Debugf("GetIndustryReportInfo:%s", body)
doc, err := goquery.NewDocumentFromReader(strings.NewReader(string(body)))
title, _ := doc.Find("div.c-title").Html()
content, _ := doc.Find("div.ctx-content").Html()
//logger.SugaredLogger.Infof("GetIndustryReportInfo:\n%s\n%s", title, content)
markdown, err := util.HTMLToMarkdown(title + content)
if err != nil {
return ""
}
logger.SugaredLogger.Infof("GetIndustryReportInfo markdown:\n%s", markdown)
return markdown
}
func (m MarketNewsApi) ReutersNew() *models.ReutersNews {
client := resty.New()
config := GetSettingConfig()
if config.HttpProxyEnabled && config.HttpProxy != "" {
client.SetProxy(config.HttpProxy)
}
news := &models.ReutersNews{}
url := "https://www.reuters.com/pf/api/v3/content/fetch/articles-by-section-alias-or-id-v1?query={\"arc-site\":\"reuters\",\"fetch_type\":\"collection\",\"offset\":0,\"section_id\":\"/world/\",\"size\":9,\"uri\":\"/world/\",\"website\":\"reuters\"}&d=300&mxId=00000000&_website=reuters"
_, err := client.SetTimeout(time.Duration(5)*time.Second).R().
SetHeader("Host", "www.reuters.com").
SetHeader("Origin", "https://www.reuters.com").
SetHeader("Referer", "https://www.reuters.com/world/china/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetResult(news).
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("ReutersNew err:%s", err.Error())
return news
}
logger.SugaredLogger.Infof("Articles:%+v", news.Result.Articles)
return news
}
func (m MarketNewsApi) InteractiveAnswer(page int, pageSize int, keyWord string) *models.InteractiveAnswer {
client := resty.New()
config := GetSettingConfig()
if config.HttpProxyEnabled && config.HttpProxy != "" {
client.SetProxy(config.HttpProxy)
}
url := fmt.Sprintf("https://irm.cninfo.com.cn/newircs/index/search?_t=%d", time.Now().Unix())
answers := &models.InteractiveAnswer{}
logger.SugaredLogger.Infof("请求url:%s", url)
resp, err := client.SetTimeout(time.Duration(5)*time.Second).R().
SetHeader("Host", "irm.cninfo.com.cn").
SetHeader("Origin", "https://irm.cninfo.com.cn").
SetHeader("Referer", "https://irm.cninfo.com.cn/views/interactiveAnswer").
SetHeader("handleError", "true").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0").
SetFormData(map[string]string{
"pageNo": convertor.ToString(page),
"pageSize": convertor.ToString(pageSize),
"searchTypes": "11",
"highLight": "true",
"keyWord": keyWord,
}).
SetResult(answers).
Post(url)
if err != nil {
logger.SugaredLogger.Errorf("InteractiveAnswer-err:%+v", err)
}
logger.SugaredLogger.Debugf("InteractiveAnswer-resp:%s", resp.Body())
return answers
}
func (m MarketNewsApi) CailianpressWeb(searchWords string) *models.CailianpressWeb {
res := &models.CailianpressWeb{}
_, err := resty.New().SetTimeout(time.Second*10).R().
SetHeader("Content-Type", "application/json").
SetHeader("Host", "www.cls.cn").
SetHeader("Origin", "https://www.cls.cn").
SetHeader("Referer", "https://www.cls.cn/telegraph").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
SetBody(fmt.Sprintf(`{"app":"CailianpressWeb","os":"web","sv":"8.4.6","category":"","keyword":"%s"}`, searchWords)).
SetResult(res).
Post("https://www.cls.cn/api/csw?app=CailianpressWeb&os=web&sv=8.4.6&sign=9f8797a1f4de66c2370f7a03990d2737")
if err != nil {
return nil
}
logger.SugaredLogger.Debug(res)
return res
}

View File

@@ -2,10 +2,14 @@ package data
import (
"encoding/json"
"github.com/coocood/freecache"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/util"
"strings"
"testing"
"github.com/coocood/freecache"
"github.com/tidwall/gjson"
)
// @Author spark
@@ -76,11 +80,13 @@ func TestStockResearchReport(t *testing.T) {
func TestIndustryResearchReport(t *testing.T) {
db.Init("../../data/stock.db")
resp := NewMarketNewsApi().IndustryResearchReport("", 7)
resp := NewMarketNewsApi().IndustryResearchReport("456", 7)
for _, a := range resp {
logger.SugaredLogger.Debugf("value: %+v", a)
data := a.(map[string]any)
logger.SugaredLogger.Debugf("value: %s infoCode:%s", data["title"], data["infoCode"])
NewMarketNewsApi().GetIndustryReportInfo(data["infoCode"].(string))
}
}
func TestStockNotice(t *testing.T) {
@@ -98,6 +104,11 @@ func TestEMDictCode(t *testing.T) {
for _, a := range resp {
logger.SugaredLogger.Debugf("value: %+v", a)
}
bytes, err := json.Marshal(resp)
if err != nil {
return
}
logger.SugaredLogger.Debugf("value: %s", string(bytes))
}
@@ -140,14 +151,78 @@ 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")
}
func TestReutersNew(t *testing.T) {
db.Init("../../data/stock.db")
NewMarketNewsApi().ReutersNew()
}
func TestInteractiveAnswer(t *testing.T) {
db.Init("../../data/stock.db")
datas := NewMarketNewsApi().InteractiveAnswer(1, 100, "")
logger.SugaredLogger.Debugf("PageSize:%d", datas.PageSize)
md := util.MarkdownTableWithTitle("投资互动", datas.Results)
logger.SugaredLogger.Debugf(md)
}

File diff suppressed because it is too large Load Diff

View File

@@ -8,12 +8,39 @@ import (
func TestNewDeepSeekOpenAiConfig(t *testing.T) {
db.Init("../../data/stock.db")
ai := NewDeepSeekOpenAi(context.TODO())
res := ai.NewChatStream("长电科技", "sh600584", "长电科技分析和总结", nil)
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(), 1)
//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
}
}
}
}
}
@@ -30,3 +57,9 @@ func TestSearchGuShiTongStockInfo(t *testing.T) {
SearchGuShiTongStockInfo("gb_goog", 60)
}
func TestGetZSInfo(t *testing.T) {
db.Init("../../data/stock.db")
GetZSInfo("中证银行", "sz399986", 30)
GetZSInfo("上海贝岭", "sh600171", 30)
}

View File

@@ -20,8 +20,8 @@ type BrowserPool struct {
func NewBrowserPool(size int) *BrowserPool {
pool := make(chan *context.Context, size)
for i := 0; i < size; i++ {
path := GetConfig().BrowserPath
crawlTimeOut := GetConfig().CrawlTimeOut
path := GetSettingConfig().BrowserPath
crawlTimeOut := GetSettingConfig().CrawlTimeOut
if crawlTimeOut < 15 {
crawlTimeOut = 30
}

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

@@ -2,9 +2,12 @@ package data
import (
"encoding/json"
"errors"
"github.com/samber/lo"
"go-stock/backend/db"
"go-stock/backend/logger"
"gorm.io/gorm"
"time"
)
type Settings struct {
@@ -15,110 +18,198 @@ type Settings struct {
DingRobot string `json:"dingRobot"`
UpdateBasicInfoOnStart bool `json:"updateBasicInfoOnStart"`
RefreshInterval int64 `json:"refreshInterval"`
OpenAiEnable bool `json:"openAiEnable"`
OpenAiBaseUrl string `json:"openAiBaseUrl"`
OpenAiApiKey string `json:"openAiApiKey"`
OpenAiModelName string `json:"openAiModelName"`
OpenAiMaxTokens int `json:"openAiMaxTokens"`
OpenAiTemperature float64 `json:"openAiTemperature"`
OpenAiApiTimeOut int `json:"openAiApiTimeOut"`
Prompt string `json:"prompt"`
CheckUpdate bool `json:"checkUpdate"`
QuestionTemplate string `json:"questionTemplate"`
CrawlTimeOut int64 `json:"crawlTimeOut"`
KDays int64 `json:"kDays"`
EnableDanmu bool `json:"enableDanmu"`
BrowserPath string `json:"browserPath"`
EnableNews bool `json:"enableNews"`
DarkTheme bool `json:"darkTheme"`
BrowserPoolSize int `json:"browserPoolSize"`
EnableFund bool `json:"enableFund"`
EnablePushNews bool `json:"enablePushNews"`
OpenAiEnable bool `json:"openAiEnable"`
Prompt string `json:"prompt"`
CheckUpdate bool `json:"checkUpdate"`
QuestionTemplate string `json:"questionTemplate"`
CrawlTimeOut int64 `json:"crawlTimeOut"`
KDays int64 `json:"kDays"`
EnableDanmu bool `json:"enableDanmu"`
BrowserPath string `json:"browserPath"`
EnableNews bool `json:"enableNews"`
DarkTheme bool `json:"darkTheme"`
BrowserPoolSize int `json:"browserPoolSize"`
EnableFund bool `json:"enableFund"`
EnablePushNews bool `json:"enablePushNews"`
EnableOnlyPushRedNews bool `json:"enableOnlyPushRedNews"`
SponsorCode string `json:"sponsorCode"`
HttpProxy string `json:"httpProxy"`
HttpProxyEnabled bool `json:"httpProxyEnabled"`
}
func (receiver Settings) TableName() string {
return "settings"
}
type SettingsApi struct {
Config Settings
type AIConfig struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
Name string `json:"name"`
BaseUrl string `json:"baseUrl"`
ApiKey string `json:"apiKey" `
ModelName string `json:"modelName"`
MaxTokens int `json:"maxTokens"`
Temperature float64 `json:"temperature"`
TimeOut int `json:"timeOut"`
}
func NewSettingsApi(settings *Settings) *SettingsApi {
func (AIConfig) TableName() string {
return "ai_config"
}
type SettingConfig struct {
*Settings
AiConfigs []*AIConfig `json:"aiConfigs"`
}
type SettingsApi struct {
Config *SettingConfig
}
func NewSettingsApi() *SettingsApi {
return &SettingsApi{
Config: *settings,
Config: GetSettingConfig(),
}
}
func (s SettingsApi) UpdateConfig() string {
func (s *SettingsApi) Export() string {
d, _ := json.MarshalIndent(s.Config, "", " ")
return string(d)
}
func UpdateConfig(s *SettingConfig) string {
count := int64(0)
db.Dao.Model(s.Config).Count(&count)
db.Dao.Model(&Settings{}).Count(&count)
if count > 0 {
db.Dao.Model(s.Config).Where("id=?", s.Config.ID).Updates(map[string]any{
"local_push_enable": s.Config.LocalPushEnable,
"ding_push_enable": s.Config.DingPushEnable,
"ding_robot": s.Config.DingRobot,
"update_basic_info_on_start": s.Config.UpdateBasicInfoOnStart,
"refresh_interval": s.Config.RefreshInterval,
"open_ai_enable": s.Config.OpenAiEnable,
"open_ai_base_url": s.Config.OpenAiBaseUrl,
"open_ai_api_key": s.Config.OpenAiApiKey,
"open_ai_model_name": s.Config.OpenAiModelName,
"open_ai_max_tokens": s.Config.OpenAiMaxTokens,
"open_ai_temperature": s.Config.OpenAiTemperature,
"tushare_token": s.Config.TushareToken,
"prompt": s.Config.Prompt,
"check_update": s.Config.CheckUpdate,
"open_ai_api_time_out": s.Config.OpenAiApiTimeOut,
"question_template": s.Config.QuestionTemplate,
"crawl_time_out": s.Config.CrawlTimeOut,
"k_days": s.Config.KDays,
"enable_danmu": s.Config.EnableDanmu,
"browser_path": s.Config.BrowserPath,
"enable_news": s.Config.EnableNews,
"dark_theme": s.Config.DarkTheme,
"enable_fund": s.Config.EnableFund,
"enable_push_news": s.Config.EnablePushNews,
db.Dao.Model(&Settings{}).Where("id=?", s.ID).Updates(map[string]any{
"local_push_enable": s.LocalPushEnable,
"ding_push_enable": s.DingPushEnable,
"ding_robot": s.DingRobot,
"update_basic_info_on_start": s.UpdateBasicInfoOnStart,
"refresh_interval": s.RefreshInterval,
"open_ai_enable": s.OpenAiEnable,
"tushare_token": s.TushareToken,
"prompt": s.Prompt,
"check_update": s.CheckUpdate,
"question_template": s.QuestionTemplate,
"crawl_time_out": s.CrawlTimeOut,
"k_days": s.KDays,
"enable_danmu": s.EnableDanmu,
"browser_path": s.BrowserPath,
"enable_news": s.EnableNews,
"dark_theme": s.DarkTheme,
"enable_fund": s.EnableFund,
"enable_push_news": s.EnablePushNews,
"enable_only_push_red_news": s.EnableOnlyPushRedNews,
"sponsor_code": s.SponsorCode,
"http_proxy": s.HttpProxy,
"http_proxy_enabled": s.HttpProxyEnabled,
})
//更新AiConfig
err := updateAiConfigs(s.AiConfigs)
if err != nil {
logger.SugaredLogger.Errorf("更新AI模型服务配置失败: %v", err)
return "更新AI模型服务配置失败: " + err.Error()
}
} else {
logger.SugaredLogger.Infof("未找到配置,创建默认配置:%+v", s.Config)
db.Dao.Model(s.Config).Create(&Settings{
LocalPushEnable: s.Config.LocalPushEnable,
DingPushEnable: s.Config.DingPushEnable,
DingRobot: s.Config.DingRobot,
UpdateBasicInfoOnStart: s.Config.UpdateBasicInfoOnStart,
RefreshInterval: s.Config.RefreshInterval,
OpenAiEnable: s.Config.OpenAiEnable,
OpenAiBaseUrl: s.Config.OpenAiBaseUrl,
OpenAiApiKey: s.Config.OpenAiApiKey,
OpenAiModelName: s.Config.OpenAiModelName,
OpenAiMaxTokens: s.Config.OpenAiMaxTokens,
OpenAiTemperature: s.Config.OpenAiTemperature,
TushareToken: s.Config.TushareToken,
Prompt: s.Config.Prompt,
CheckUpdate: s.Config.CheckUpdate,
OpenAiApiTimeOut: s.Config.OpenAiApiTimeOut,
QuestionTemplate: s.Config.QuestionTemplate,
CrawlTimeOut: s.Config.CrawlTimeOut,
KDays: s.Config.KDays,
EnableDanmu: s.Config.EnableDanmu,
BrowserPath: s.Config.BrowserPath,
EnableNews: s.Config.EnableNews,
DarkTheme: s.Config.DarkTheme,
EnableFund: s.Config.EnableFund,
EnablePushNews: s.Config.EnablePushNews,
})
logger.SugaredLogger.Infof("未找到配置,创建默认配置")
// 创建主配置
result := db.Dao.Model(&Settings{}).Create(&Settings{})
if result.Error != nil {
logger.SugaredLogger.Error("创建配置失败:", result.Error)
return "创建配置失败: " + result.Error.Error()
}
}
return "保存成功!"
}
func (s SettingsApi) GetConfig() *Settings {
var settings Settings
db.Dao.Model(&Settings{}).First(&settings)
func updateAiConfigs(aiConfigs []*AIConfig) error {
if len(aiConfigs) == 0 {
err := db.Dao.Exec("DELETE FROM ai_config").Error
if err != nil {
return err
}
return db.Dao.Exec("DELETE FROM sqlite_sequence WHERE name='ai_config'").Error
}
var ids []uint
lo.ForEach(aiConfigs, func(item *AIConfig, index int) {
ids = append(ids, item.ID)
})
var existAiConfigs []*AIConfig
err := db.Dao.Model(&AIConfig{}).Select("id").Where("id in (?) ", ids).Find(&existAiConfigs).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
idMap := make(map[uint]bool)
lo.ForEach(existAiConfigs, func(item *AIConfig, index int) {
idMap[item.ID] = true
})
var addAiConfigs []*AIConfig
var notDeleteIds []uint
var e error
lo.ForEach(aiConfigs, func(item *AIConfig, index int) {
if e != nil {
return
}
if !idMap[item.ID] {
addAiConfigs = append(addAiConfigs, item)
} else {
notDeleteIds = append(notDeleteIds, item.ID)
e = db.Dao.Model(&AIConfig{}).Where("id=?", item.ID).Updates(map[string]interface{}{
"name": item.Name,
"base_url": item.BaseUrl,
"api_key": item.ApiKey,
"model_name": item.ModelName,
"max_tokens": item.MaxTokens,
"temperature": item.Temperature,
"time_out": item.TimeOut,
}).Error
if e != nil {
return
}
}
})
if e != nil {
return e
}
//删除旧的配置
if len(notDeleteIds) > 0 {
err = db.Dao.Exec("DELETE FROM ai_config WHERE id NOT IN ?", notDeleteIds).Error
if err != nil {
return err
}
}
logger.SugaredLogger.Infof("更新aiConfigs +%d", len(addAiConfigs))
//批量新增的配置
err = db.Dao.CreateInBatches(addAiConfigs, len(addAiConfigs)).Error
return err
}
func GetSettingConfig() *SettingConfig {
settingConfig := &SettingConfig{}
settings := &Settings{}
aiConfigs := make([]*AIConfig, 0)
// 处理数据库查询可能返回的空结果
result := db.Dao.Model(&Settings{}).First(settings)
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
// 初始化默认设置并保存到数据库
settings = &Settings{OpenAiEnable: false, CrawlTimeOut: 60}
db.Dao.Create(settings)
}
if settings.OpenAiEnable {
if settings.OpenAiApiTimeOut <= 0 {
settings.OpenAiApiTimeOut = 60 * 5
// 处理AI配置查询可能出现的错误
result = db.Dao.Model(&AIConfig{}).Find(&aiConfigs)
if result.Error != nil {
logger.SugaredLogger.Error("查询AI配置失败:", result.Error)
} else if len(aiConfigs) > 0 {
lo.ForEach(aiConfigs, func(item *AIConfig, index int) {
if item.TimeOut <= 0 {
item.TimeOut = 60 * 5
}
})
}
if settings.CrawlTimeOut <= 0 {
settings.CrawlTimeOut = 60
@@ -128,15 +219,13 @@ func (s SettingsApi) GetConfig() *Settings {
}
}
if settings.BrowserPath == "" {
settings.BrowserPath, _ = CheckBrowserOnWindows()
settings.BrowserPath, _ = CheckBrowser()
}
if settings.BrowserPoolSize <= 0 {
settings.BrowserPoolSize = 1
}
return &settings
}
settingConfig.Settings = settings
settingConfig.AiConfigs = aiConfigs
func (s SettingsApi) Export() string {
d, _ := json.MarshalIndent(s.GetConfig(), "", " ")
return string(d)
return settingConfig
}

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"
@@ -38,7 +37,7 @@ const tushareApiUrl = "http://api.tushare.pro"
type StockDataApi struct {
client *resty.Client
config *Settings
config *SettingConfig
}
type StockInfo struct {
gorm.Model
@@ -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 {
@@ -171,6 +172,7 @@ type FollowedStock struct {
Cron *string
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
Groups []GroupStock `gorm:"foreignKey:StockCode;references:StockCode"`
AiConfigId int
}
func (receiver FollowedStock) TableName() string {
@@ -195,7 +197,7 @@ func (receiver StockBasic) TableName() string {
func NewStockDataApi() *StockDataApi {
return &StockDataApi{
client: resty.New(),
config: GetConfig(),
config: GetSettingConfig(),
}
}
@@ -376,6 +378,9 @@ func (receiver StockDataApi) GetStockCodeRealTimeData(StockCodes ...string) (*[]
logger.SugaredLogger.Error(err.Error())
continue
}
if stockData == nil {
continue
}
stockInfos = append(stockInfos, *stockData)
go func() {
@@ -414,6 +419,15 @@ func (receiver StockDataApi) Follow(stockCode string) string {
}
stockCode = strings.ToLower(stockCode)
// 检查是否已经关注过该股票
var existingStock FollowedStock
result := db.Dao.Model(&FollowedStock{}).Where("stock_code = ? AND is_del = ?", stockCode, 0).First(&existingStock)
if result.Error == nil {
// 股票已经关注过
return "已经关注了"
}
maxSort := int64(0)
db.Dao.Model(&FollowedStock{}).Raw("select max(sort) as sort from followed_stock").Scan(&maxSort)
@@ -1163,7 +1177,7 @@ func getHKStockPriceInfo(stockCode string, crawlTimeOut int64) *[]string {
return &messages
}
func getZSInfo(name, stockCode string, crawlTimeOut int64) string {
func GetZSInfo(name, stockCode string, crawlTimeOut int64) string {
url := "https://finance.sina.com.cn/realstock/company/" + stockCode + "/nc.shtml"
crawlerAPI := CrawlerApi{}
crawlerBaseInfo := CrawlerBaseInfo{
@@ -1188,6 +1202,10 @@ func getZSInfo(name, stockCode string, crawlTimeOut int64) string {
price := strutil.RemoveWhiteSpace(document.Find("div#price").First().Text(), false)
hqTime := strutil.RemoveWhiteSpace(document.Find("div#hqTime").First().Text(), false)
if strutil.ContainsAny(price, []string{"-", "--", ""}) {
return "暂无数据"
}
var markdown strings.Builder
markdown.WriteString(fmt.Sprintf("### 时间:%s %s%s \n", hqTime, name, price))
GetTableMarkdown(document, "div#hqDetails table", &markdown)
@@ -1298,50 +1316,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 +1473,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 +1481,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 +1591,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

@@ -4,17 +4,19 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/random"
"github.com/duke-git/lancet/v2/strutil"
"github.com/go-resty/resty/v2"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/util"
"io/ioutil"
"regexp"
"strings"
"testing"
"time"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/random"
"github.com/duke-git/lancet/v2/strutil"
"github.com/go-resty/resty/v2"
)
// @Author spark
@@ -48,14 +50,23 @@ func TestGetFinancialReports(t *testing.T) {
func TestGetTelegraphSearch(t *testing.T) {
db.Init("../../data/stock.db")
searchWords := "半导体 新能源汽车 机器人"
//url := "https://www.cls.cn/searchPage?keyword=%E9%97%BB%E6%B3%B0%E7%A7%91%E6%8A%80&type=telegram"
messages := SearchStockInfo("谷歌", "telegram", 30)
messages := SearchStockInfo(searchWords, "telegram", 30)
for _, message := range *messages {
logger.SugaredLogger.Info(message)
}
//https://www.cls.cn/stock?code=sh600745
}
func TestCailianpressWeb(t *testing.T) {
db.Init("../../data/stock.db")
searchWords := "半导体 新能源汽车 机器人"
res := NewMarketNewsApi().CailianpressWeb(searchWords)
md := util.MarkdownTableWithTitle(searchWords+"财联社新闻", res.List)
logger.SugaredLogger.Info(md)
}
func TestSearchStockInfoByCode(t *testing.T) {
db.Init("../../data/stock.db")
SearchStockInfoByCode("sh600745")
@@ -63,7 +74,7 @@ func TestSearchStockInfoByCode(t *testing.T) {
func TestSearchStockPriceInfo(t *testing.T) {
db.Init("../../data/stock.db")
//SearchStockPriceInfo("中信证券", "hk06030", 30)
SearchStockPriceInfo("博安生物", "hk06955", 30)
SearchStockPriceInfo("上海贝岭", "sh600171", 30)
//SearchStockPriceInfo("苹果公司", "gb_aapl", 30)
//SearchStockPriceInfo("微创光电", "bj430198", 30)
@@ -110,7 +121,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

@@ -39,17 +39,74 @@ func NewStockGroupApi(dao *gorm.DB) *StockGroupApi {
}
func (receiver StockGroupApi) AddGroup(group Group) bool {
err := receiver.dao.Where("name = ?", group.Name).FirstOrCreate(&group).Updates(&Group{
Name: group.Name,
Sort: group.Sort,
}).Error
// 检查是否已存在相同sort的组
var existingGroup Group
err := receiver.dao.Where("sort = ?", group.Sort).First(&existingGroup).Error
// 如果存在相同sort的组则将该组及之后的所有组向后移动一位
if err == nil {
// 处理sort冲突将相同sort值及之后的所有组向后移动一位
receiver.dao.Model(&Group{}).Where("sort >= ?", group.Sort).Update("sort", gorm.Expr("sort + ?", 1))
}
// 创建新组
err = receiver.dao.Create(&group).Error
return err == nil
}
func (receiver StockGroupApi) GetGroupList() []Group {
var groups []Group
receiver.dao.Find(&groups)
receiver.dao.Order("sort ASC").Find(&groups)
return groups
}
func (receiver StockGroupApi) UpdateGroupSort(id int, newSort int) bool {
// First, get the current group to check if it exists
var currentGroup Group
if err := receiver.dao.First(&currentGroup, id).Error; err != nil {
return false
}
// If the new sort is the same as current, no need to update
if currentGroup.Sort == newSort {
return true
}
// Get all groups ordered by sort
var allGroups []Group
receiver.dao.Order("sort ASC").Find(&allGroups)
// Adjust sort numbers to make space for the new sort value
if newSort > currentGroup.Sort {
// Moving down: decrease sort of groups between old and new position
receiver.dao.Model(&Group{}).Where("sort > ? AND sort <= ? AND id != ?", currentGroup.Sort, newSort, id).Update("sort", gorm.Expr("sort - ?", 1))
} else {
// Moving up: increase sort of groups between new and old position
receiver.dao.Model(&Group{}).Where("sort >= ? AND sort < ? AND id != ?", newSort, currentGroup.Sort, id).Update("sort", gorm.Expr("sort + ?", 1))
}
// Update the target group's sort
err := receiver.dao.Model(&Group{}).Where("id = ?", id).Update("sort", newSort).Error
return err == nil
}
// InitializeGroupSort initializes sort order for all groups based on created time
func (receiver StockGroupApi) InitializeGroupSort() bool {
// Get all groups ordered by created time
var groups []Group
err := receiver.dao.Order("created_at ASC").Find(&groups).Error
if err != nil {
return false
}
// Update each group with new sort value based on their position
for i, group := range groups {
newSort := i + 1
err := receiver.dao.Model(&Group{}).Where("id = ?", group.ID).Update("sort", newSort).Error
if err != nil {
return false
}
}
return true
}
func (receiver StockGroupApi) GetGroupStockByGroupId(groupId int) []GroupStock {
var stockGroup []GroupStock
receiver.dao.Preload("GroupInfo").Where("group_id = ?", groupId).Find(&stockGroup)

View File

@@ -3,10 +3,11 @@ package data
import (
"bufio"
"fmt"
"github.com/go-ego/gse"
"go-stock/backend/logger"
"os"
"strings"
"github.com/go-ego/gse"
)
// 金融情感词典,包含股票市场相关的专业词汇

View File

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

View File

@@ -11,7 +11,7 @@ import (
// -----------------------------------------------------------------------------------
func TestGetDaily(t *testing.T) {
db.Init("../../data/stock.db")
tushareApi := NewTushareApi(GetConfig())
tushareApi := NewTushareApi(GetSettingConfig())
res := tushareApi.GetDaily("00927.HK", "20250101", "20250217", 30)
t.Log(res)
@@ -19,7 +19,7 @@ func TestGetDaily(t *testing.T) {
func TestGetUSDaily(t *testing.T) {
db.Init("../../data/stock.db")
tushareApi := NewTushareApi(GetConfig())
tushareApi := NewTushareApi(GetSettingConfig())
res := tushareApi.GetDaily("gb_AAPL", "20250101", "20250217", 30)
t.Log(res)

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,316 @@ type HotEvent struct {
StatusCount int `json:"status_count"`
Content string `json:"content"`
}
type GDP struct {
REPORTDATE string `json:"REPORT_DATE" md:"报告时间"`
TIME string `json:"TIME" md:"报告期"`
DOMESTICLPRODUCTBASE float64 `json:"DOMESTICL_PRODUCT_BASE" md:"国内生产总值(亿元)"`
SUMSAME float64 `json:"SUM_SAME" md:"国内生产总值同比增长(%)"`
FIRSTPRODUCTBASE float64 `json:"FIRST_PRODUCT_BASE" md:"第一产业(亿元)"`
FIRSTSAME int `json:"FIRST_SAME" md:"第一产业同比增长(%)"`
SECONDPRODUCTBASE float64 `json:"SECOND_PRODUCT_BASE" md:"第二产业(亿元)"`
SECONDSAME float64 `json:"SECOND_SAME" md:"第二产业同比增长(%)"`
THIRDPRODUCTBASE float64 `json:"THIRD_PRODUCT_BASE" md:"第三产业(亿元)"`
THIRDSAME float64 `json:"THIRD_SAME" md:"第三产业同比增长(%)"`
}
type CPI struct {
REPORTDATE string `json:"REPORT_DATE" md:"报告时间"`
TIME string `json:"TIME" md:"报告期"`
NATIONALBASE float64 `json:"NATIONAL_BASE" md:"全国当月"`
NATIONALSAME float64 `json:"NATIONAL_SAME" md:"全国当月同比增长(%)"`
NATIONALSEQUENTIAL float64 `json:"NATIONAL_SEQUENTIAL" md:"全国当月环比增长(%)"`
NATIONALACCUMULATE float64 `json:"NATIONAL_ACCUMULATE" md:"全国当月累计"`
CITYBASE float64 `json:"CITY_BASE" md:"城市当月"`
CITYSAME float64 `json:"CITY_SAME" md:"城市当月同比增长(%)"`
CITYSEQUENTIAL float64 `json:"CITY_SEQUENTIAL" md:"城市当月环比增长(%)"`
CITYACCUMULATE int `json:"CITY_ACCUMULATE" md:"城市当月累计"`
RURALBASE float64 `json:"RURAL_BASE" md:"农村当月"`
RURALSAME float64 `json:"RURAL_SAME" md:"农村当月同比增长(%)"`
RURALSEQUENTIAL int `json:"RURAL_SEQUENTIAL" md:"农村当月环比增长(%)"`
RURALACCUMULATE float64 `json:"RURAL_ACCUMULATE" md:"农村当月累计"`
}
type PPI struct {
REPORTDATE string `json:"REPORT_DATE" md:"报告时间"`
TIME string `json:"TIME" md:"报告期"`
BASE float64 `json:"BASE" md:"当月"`
BASESAME float64 `json:"BASE_SAME" md:"当月同比增长(%)"`
BASEACCUMULATE float64 `json:"BASE_ACCUMULATE" md:"累计"`
}
type PMI struct {
REPORTDATE string `md:"报告时间" json:"REPORT_DATE"`
TIME string `md:"报告期" json:"TIME"`
MAKEINDEX float64 `md:"制造业指数" json:"MAKE_INDEX"`
MAKESAME float64 `md:"制造业指数同比增长(%)" json:"MAKE_SAME"`
NMAKEINDEX float64 `md:"非制造业" json:"NMAKE_INDEX"`
NMAKESAME float64 `md:"非制造业同比增长(%)" json:"NMAKE_SAME"`
}
type DCResp struct {
Version string `json:"version"`
Success bool `json:"success"`
Message string `json:"message"`
Code int `json:"code"`
}
type GDPResult struct {
Pages int `json:"pages"`
Data []GDP `json:"data"`
Count int `json:"count"`
}
type CPIResult struct {
Pages int `json:"pages"`
Data []CPI `json:"data"`
Count int `json:"count"`
}
type PPIResult struct {
Pages int `json:"pages"`
Data []PPI `json:"data"`
Count int `json:"count"`
}
type PMIResult struct {
Pages int `json:"pages"`
Data []PMI `json:"data"`
Count int `json:"count"`
}
type GDPResp struct {
DCResp
GDPResult GDPResult `json:"result"`
}
type CPIResp struct {
DCResp
CPIResult CPIResult `json:"result"`
}
type PPIResp struct {
DCResp
PPIResult PPIResult `json:"result"`
}
type PMIResp struct {
DCResp
PMIResult PMIResult `json:"result"`
}
type OldSettings struct {
gorm.Model
TushareToken string `json:"tushareToken"`
LocalPushEnable bool `json:"localPushEnable"`
DingPushEnable bool `json:"dingPushEnable"`
DingRobot string `json:"dingRobot"`
UpdateBasicInfoOnStart bool `json:"updateBasicInfoOnStart"`
RefreshInterval int64 `json:"refreshInterval"`
OpenAiEnable bool `json:"openAiEnable"`
OpenAiBaseUrl string `json:"openAiBaseUrl"`
OpenAiApiKey string `json:"openAiApiKey"`
OpenAiModelName string `json:"openAiModelName"`
OpenAiMaxTokens int `json:"openAiMaxTokens"`
OpenAiTemperature float64 `json:"openAiTemperature"`
OpenAiApiTimeOut int `json:"openAiApiTimeOut"`
Prompt string `json:"prompt"`
CheckUpdate bool `json:"checkUpdate"`
QuestionTemplate string `json:"questionTemplate"`
CrawlTimeOut int64 `json:"crawlTimeOut"`
KDays int64 `json:"kDays"`
EnableDanmu bool `json:"enableDanmu"`
BrowserPath string `json:"browserPath"`
EnableNews bool `json:"enableNews"`
DarkTheme bool `json:"darkTheme"`
BrowserPoolSize int `json:"browserPoolSize"`
EnableFund bool `json:"enableFund"`
EnablePushNews bool `json:"enablePushNews"`
SponsorCode string `json:"sponsorCode"`
}
func (receiver OldSettings) TableName() string {
return "settings"
}
type ReutersNews struct {
StatusCode int `json:"statusCode"`
Message string `json:"message"`
Result struct {
ParentSectionName string `json:"parent_section_name"`
Pagination struct {
Size int `json:"size"`
ExpectedSize int `json:"expected_size"`
TotalSize int `json:"total_size"`
Orderby string `json:"orderby"`
} `json:"pagination"`
DateModified time.Time `json:"date_modified"`
FetchType string `json:"fetch_type"`
Articles []struct {
Id string `json:"id"`
CanonicalUrl string `json:"canonical_url"`
Website string `json:"website"`
Web string `json:"web"`
Native string `json:"native"`
UpdatedTime time.Time `json:"updated_time"`
PublishedTime time.Time `json:"published_time"`
ArticleType string `json:"article_type"`
DisplayMyNews bool `json:"display_my_news"`
DisplayNewsletterSignup bool `json:"display_newsletter_signup"`
DisplayNotifications bool `json:"display_notifications"`
DisplayRelatedMedia bool `json:"display_related_media"`
DisplayRelatedOrganizations bool `json:"display_related_organizations"`
ContentCode string `json:"content_code"`
Source struct {
Name string `json:"name"`
OriginalName string `json:"original_name"`
} `json:"source"`
Title string `json:"title"`
BasicHeadline string `json:"basic_headline"`
Distributor string `json:"distributor"`
Description string `json:"description"`
PrimaryMediaType string `json:"primary_media_type,omitempty"`
PrimaryTag struct {
ShortBio string `json:"short_bio"`
Description string `json:"description"`
Slug string `json:"slug"`
Text string `json:"text"`
TopicUrl string `json:"topic_url"`
CanFollow bool `json:"can_follow,omitempty"`
IsTopic bool `json:"is_topic,omitempty"`
} `json:"primary_tag"`
WordCount int `json:"word_count"`
ReadMinutes int `json:"read_minutes"`
Kicker struct {
Path string `json:"path"`
Names []string `json:"names"`
Name string `json:"name,omitempty"`
} `json:"kicker"`
AdTopics []string `json:"ad_topics"`
Thumbnail struct {
Url string `json:"url"`
Caption string `json:"caption,omitempty"`
Type string `json:"type"`
ResizerUrl string `json:"resizer_url"`
Location string `json:"location,omitempty"`
Id string `json:"id"`
Authors string `json:"authors,omitempty"`
AltText string `json:"alt_text"`
Width int `json:"width"`
Height int `json:"height"`
Subtitle string `json:"subtitle"`
Slug string `json:"slug,omitempty"`
UpdatedAt time.Time `json:"updated_at"`
Company string `json:"company,omitempty"`
PurchaseLicensingPath string `json:"purchase_licensing_path,omitempty"`
} `json:"thumbnail"`
Authors []struct {
Id string `json:"id,omitempty"`
Name string `json:"name"`
FirstName string `json:"first_name,omitempty"`
LastName string `json:"last_name,omitempty"`
Company string `json:"company"`
Thumbnail struct {
Url string `json:"url"`
Type string `json:"type"`
ResizerUrl string `json:"resizer_url"`
} `json:"thumbnail"`
SocialLinks []struct {
Site string `json:"site"`
Url string `json:"url"`
} `json:"social_links,omitempty"`
Byline string `json:"byline"`
Description string `json:"description,omitempty"`
TopicUrl string `json:"topic_url,omitempty"`
Role string `json:"role,omitempty"`
} `json:"authors"`
DisplayTime time.Time `json:"display_time"`
ThumbnailDark struct {
Url string `json:"url"`
Type string `json:"type"`
ResizerUrl string `json:"resizer_url"`
Id string `json:"id"`
AltText string `json:"alt_text"`
Width int `json:"width"`
Height int `json:"height"`
Subtitle string `json:"subtitle"`
UpdatedAt time.Time `json:"updated_at"`
} `json:"thumbnail_dark,omitempty"`
} `json:"articles"`
Section struct {
Id string `json:"id"`
AdUnitCode string `json:"ad_unit_code"`
Website string `json:"website"`
Name string `json:"name"`
PageTitle string `json:"page_title"`
CanFollow bool `json:"can_follow"`
Language string `json:"language"`
Type string `json:"type"`
Advertising struct {
Sponsored string `json:"sponsored"`
} `json:"advertising"`
VideoPlaylistId string `json:"video_playlistId"`
MobileAdUnitPath string `json:"mobile_ad_unit_path"`
AdUnitPath string `json:"ad_unit_path"`
CollectionAlias string `json:"collection_alias"`
SectionAbout string `json:"section_about"`
Title string `json:"title"`
Personalization struct {
Id string `json:"id"`
Type string `json:"type"`
ShowTags bool `json:"show_tags"`
CanFollow bool `json:"can_follow"`
} `json:"personalization"`
} `json:"section"`
AdUnitPath string `json:"ad_unit_path"`
ResponseTime int64 `json:"response_time"`
} `json:"result"`
Id string `json:"_id"`
}
type InteractiveAnswer struct {
PageNo int `json:"pageNo"`
PageSize int `json:"pageSize"`
TotalRecord int `json:"totalRecord"`
TotalPage int `json:"totalPage"`
Results []InteractiveAnswerResults `json:"results"`
Count bool `json:"count"`
}
type InteractiveAnswerResults struct {
EsId string `json:"esId" md:"-"`
IndexId string `json:"indexId" md:"-"`
ContentType int `json:"contentType" md:"-"`
Trade []string `json:"trade" md:"行业名称"`
MainContent string `json:"mainContent" md:"投资者提问"`
StockCode string `json:"stockCode" md:"股票代码"`
Secid string `json:"secid" md:"-"`
CompanyShortName string `json:"companyShortName" md:"股票名称"`
CompanyLogo string `json:"companyLogo,omitempty" md:"-"`
BoardType []string `json:"boardType" md:"-"`
PubDate string `json:"pubDate" md:"发布时间"`
UpdateDate string `json:"updateDate" md:"-"`
Author string `json:"author" md:"-"`
AuthorName string `json:"authorName" md:"-"`
PubClient string `json:"pubClient" md:"-"`
AttachedId string `json:"attachedId" md:"-"`
AttachedContent string `json:"attachedContent" md:"上市公司回复"`
AttachedAuthor string `json:"attachedAuthor" md:"-"`
AttachedPubDate string `json:"attachedPubDate" md:"回复时间"`
Score float64 `json:"score" md:"-"`
TopStatus int `json:"topStatus" md:"-"`
PraiseCount int `json:"praiseCount" md:"-"`
PraiseStatus bool `json:"praiseStatus" md:"-"`
FavoriteStatus bool `json:"favoriteStatus" md:"-"`
AttentionCompany bool `json:"attentionCompany" md:"-"`
IsCheck string `json:"isCheck" md:"-"`
QaStatus int `json:"qaStatus" md:"-"`
PackageDate string `json:"packageDate" md:"-"`
RemindStatus bool `json:"remindStatus" md:"-"`
InterviewLive bool `json:"interviewLive" md:"-"`
}
type CailianpressWeb struct {
Total int `json:"total"`
List []struct {
Title string `json:"title" md:"资讯标题"`
Ctime int `json:"ctime" md:"资讯时间"`
Content string `json:"content" md:"资讯内容"`
Author string `json:"author" md:"资讯发布者"`
} `json:"list"`
}

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

View File

@@ -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))
}

10
frontend/auto-imports.d.ts vendored Normal file
View File

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

44
frontend/components.d.ts vendored Normal file
View File

@@ -0,0 +1,44 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
About: typeof import('./src/components/about.vue')['default']
AgentChat: typeof import('./src/components/agent-chat.vue')['default']
AgentChat_bk: typeof import('./src/components/agent-chat_bk.vue')['default']
ClsCalendarTimeLine: typeof import('./src/components/ClsCalendarTimeLine.vue')['default']
EmbeddedUrl: typeof import('./src/components/EmbeddedUrl.vue')['default']
Fund: typeof import('./src/components/fund.vue')['default']
HotEvents: typeof import('./src/components/HotEvents.vue')['default']
HotStockList: typeof import('./src/components/HotStockList.vue')['default']
HotTopics: typeof import('./src/components/HotTopics.vue')['default']
IndustryMoneyRank: typeof import('./src/components/industryMoneyRank.vue')['default']
IndustryResearchReportList: typeof import('./src/components/IndustryResearchReportList.vue')['default']
InvestCalendarTimeLine: typeof import('./src/components/InvestCalendarTimeLine.vue')['default']
KLineChart: typeof import('./src/components/KLineChart.vue')['default']
LongTigerRankList: typeof import('./src/components/LongTigerRankList.vue')['default']
Market: typeof import('./src/components/market.vue')['default']
MoneyTrend: typeof import('./src/components/moneyTrend.vue')['default']
NewsList: typeof import('./src/components/newsList.vue')['default']
RankTable: typeof import('./src/components/rankTable.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SelectStock: typeof import('./src/components/SelectStock.vue')['default']
Settings: typeof import('./src/components/settings.vue')['default']
Stock: typeof import('./src/components/stock.vue')['default']
Stockhotmap: typeof import('./src/components/stockhotmap.vue')['default']
StockNoticeList: typeof import('./src/components/StockNoticeList.vue')['default']
StockResearchReportList: typeof import('./src/components/StockResearchReportList.vue')['default']
StockSparkLine: typeof import('./src/components/stockSparkLine.vue')['default']
TChat: typeof import('@tdesign-vue-next/chat')['Chat']
TChatAction: typeof import('@tdesign-vue-next/chat')['ChatAction']
TChatContent: typeof import('@tdesign-vue-next/chat')['ChatContent']
TChatLoading: typeof import('@tdesign-vue-next/chat')['ChatLoading']
TChatSender: typeof import('@tdesign-vue-next/chat')['ChatSender']
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
"@tdesign-vue-next/chat": "^0.4.5",
"@types/file-saver": "^2.0.7",
"@vavt/cm-extension": "^1.8.0",
"@vavt/v3-extension": "^3.0.0",
@@ -18,11 +19,13 @@
"html2canvas": "^1.4.1",
"lodash": "^4.17.21",
"md-editor-v3": "^5.2.3",
"vue": "^3.2.25",
"tdesign-icons-vue-next": "^0.3.7",
"vue": "^3.5.17",
"vue-router": "^4.5.0",
"vue3-danmaku": "^1.6.1"
},
"devDependencies": {
"@tdesign-vue-next/auto-import-resolver": "^0.1.1",
"@vicons/antd": "^0.13.0",
"@vicons/carbon": "^0.13.0",
"@vicons/fa": "^0.13.0",
@@ -33,7 +36,10 @@
"@vicons/tabler": "^0.13.0",
"@vitejs/plugin-vue": "^5.2.1",
"html-docx-js-typescript": "^0.1.5",
"less": "^4.4.0",
"naive-ui": "^2.41.0",
"unplugin-auto-import": "^20.0.0",
"unplugin-vue-components": "^29.0.0",
"vfonts": "^0.0.3",
"vite": "^6.3.5"
},

View File

@@ -1 +1 @@
2d63c3a999d797889c01d6c96451b197
b0b9f944d9af9c00b6d48234793db58c

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,8 +27,8 @@ import {
StarOutline,
Wallet, WarningOutline,
} from '@vicons/ionicons5'
import {AnalyzeSentiment, GetConfig, GetGroupList} from "../wailsjs/go/main/App";
import {Dragon, Fire, Gripfire} from "@vicons/fa";
import {AnalyzeSentiment, GetConfig, GetGroupList,GetVersionInfo} from "../wailsjs/go/main/App";
import {Dragon, Fire, FirefoxBrowser, Gripfire, Robot} from "@vicons/fa";
import {ReportSearch} from "@vicons/tabler";
import {LocalFireDepartmentRound} from "@vicons/material";
import {BoxSearch20Regular, CommentNote20Filled} from "@vicons/fluent";
@@ -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: '指标选股'})
},
},
@@ -353,6 +369,28 @@ const menuOptions = ref([
key: 'market11',
icon: renderIcon(BoxSearch20Regular),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "名站优选",
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '名站优选'})
},
},
{default: () => '名站优选',}
),
key: 'market12',
icon: renderIcon(FirefoxBrowser),
},
]
},
{
@@ -362,8 +400,13 @@ const menuOptions = ref([
{
to: {
name: 'fund',
params: {},
}
query: {
name: '基金自选',
},
},
onClick: () => {
activeKey.value = 'fund'
},
},
{default: () => '基金自选',}
),
@@ -379,6 +422,26 @@ const menuOptions = ref([
},
]
},
{
label: () =>
h(
RouterLink,
{
to: {
name: 'agent',
query: {
name:"Ai智能体",
},
onClick: () => {
activeKey.value = 'agent'
},
}
},
{default: () => 'Ai智能体'}
),
key: 'agent',
icon: renderIcon(Robot),
},
{
label: () =>
h(
@@ -386,7 +449,12 @@ const menuOptions = ref([
{
to: {
name: 'settings',
params: {}
query: {
name:"设置",
},
onClick: () => {
activeKey.value = 'settings'
},
}
},
{default: () => '设置'}
@@ -401,8 +469,13 @@ const menuOptions = ref([
{
to: {
name: 'about',
params: {}
}
query: {
name:"关于",
}
},
onClick: () => {
activeKey.value = 'about'
},
},
{default: () => '关于'}
),
@@ -410,6 +483,7 @@ const menuOptions = ref([
icon: renderIcon(LogoGithub),
},
{
show:false,
label: () => h("a", {
href: '#',
onClick: toggleFullscreen,
@@ -422,7 +496,7 @@ const menuOptions = ref([
label: () => h("a", {
href: '#',
onClick: WindowHide,
title: '隐藏到托盘区 Ctrl+H',
title: '隐藏到托盘区 Ctrl+Z',
}, {default: () => '隐藏到托盘区'}),
key: 'hide',
icon: renderIcon(ReorderTwoOutline),
@@ -451,6 +525,7 @@ function renderIcon(icon) {
}
function toggleFullscreen(e) {
activeKey.value = 'full'
//console.log(e)
if (isFullscreen.value) {
WindowUnfullscreen()
@@ -518,6 +593,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 +681,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 ,
})
@@ -656,7 +745,7 @@ onMounted(() => {
</n-spin>
</n-gi>
<n-gi style="position: fixed;bottom:0;z-index: 9;width: 100%;">
<n-card size="small" style="--wails-draggable:drag">
<n-card size="small" style="--wails-draggable:no-drag">
<n-menu style="font-size: 18px;"
v-model:value="activeKey"
mode="horizontal"

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,255 @@
<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, Follow, GetFollowList} from "../../wailsjs/go/main/App";
import {useMessage, NText, NTag, NButton} from 'naive-ui'
import {Environment} from "../../wailsjs/runtime"
import {RefreshCircleSharp} from "@vicons/ionicons5";
import {EventsEmit} from "../../wailsjs/runtime";
const message = useMessage()
const search = ref('科技股;换手率连续3日大于2')
const search = ref('')
const columns = ref([])
const dataList = ref([])
const hotStrategy = ref([])
const traceInfo = ref('')
const tableScrollX = ref(2800) // 默认滚动宽度
// 计算表格总宽度
function calculateTableWidth(cols) {
let totalWidth = 0;
cols.forEach(col => {
if (col.children && col.children.length > 0) {
// 有子列的情况
let childrenWidth = 0;
col.children.forEach(child => {
childrenWidth += child.width || child.minWidth || 100;
});
// 取标题列宽度和子列总宽度的较大值
totalWidth += Math.max(col.width || col.minWidth || 200, childrenWidth);
} else {
// 没有子列的情况
totalWidth += col.width || col.minWidth || 120;
}
});
// 加上操作列的宽度
totalWidth += 100;
return Math.max(totalWidth, 1200); // 最小宽度1200
}
function Search() {
if (!search.value) {
message.warning('请输入选股指标或者要求')
return
}
const loading = message.loading("正在获取选股数据...", {duration: 0});
SearchStock(search.value).then(res => {
loading.destroy()
//console.log(res)
if(res.code==100){
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)
}
columns.value.push({
title: '操作',
key: 'actions',
width: 80,
fixed: 'right', // 固定在右侧
render: (row) => {
return h(
NButton,
{
strong: true,
tertiary: true,
size: 'small',
type: 'warning', // 橙色按钮
style: 'font-size: 14px; padding: 0 10px;', // 稍微大一点的按钮
onClick: () => handleFollow(row)
},
{ default: () => '关注' }
)
}
});
dataList.value = res.data.result.dataList
console.log("sss"+columns.value. length)
// 计算并设置表格宽度
tableScrollX.value = calculateTableWidth(columns.value);
} else {
message.error(res.msg)
}
}).catch(err => {
message.error(err)
})
}
// 修改handleFollow方法使用stock.vue的AddStock逻辑
function handleFollow(row) {
let code=row.MARKET_SHORT_NAME.toLowerCase()+row.SECURITY_CODE
Follow(code).then(result => {
if (result === "关注成功") {
message.success(result)
} else {
message.error(result)
}
});
}
function isNumeric(value) {
return !isNaN(parseFloat(value)) && isFinite(value);
}
onBeforeMount(() => {
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="tableScrollX"
:render-cell="(value, rowData, column) => {
if(column.key=='SECURITY_CODE'||column.key=='SERIAL'){
return h(NText, { type: 'info',border: false }, { default: () => `${value}` })
@@ -112,13 +268,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.css相比style.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:no-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-button size="tiny" @click="CheckUpdate" type="info" tertiary >检查更新</n-button>
<n-gradient-text type="warning" v-if="vipLevel" >vip到期时间{{vipEndTime}}</n-gradient-text>
<n-button size="tiny" @click="CheckUpdate(1)" type="info" tertiary >检查更新</n-button>
<div style="justify-self: center;text-align: left" >
<p>自选股行情实时监控基于Wails和NaiveUI构建的AI赋能股票分析工具</p>
<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

@@ -0,0 +1,365 @@
<template>
<div class="chat-box">
<t-chat
ref="chatRef"
:clear-history="chatList.length > 0 && !isStreamLoad"
:data="chatList"
:text-loading="loading"
:is-stream-load="isStreamLoad"
style="height: 100%"
@scroll="handleChatScroll"
@clear="clearConfirm"
>
<!-- eslint-disable vue/no-unused-vars -->
<template #content="{ item, index }">
<t-chat-reasoning v-if="item.role === 'assistant'" expand-icon-placement="right">
<t-chat-loading v-if="isStreamLoad" text="思考中..." />
<!-- <t-chat-content v-if="item.reasoning.length > 0" :content="item.reasoning" />-->
</t-chat-reasoning>
<t-chat-content v-if="item.content.length > 0" :content="item.content" />
</template>
<template #actions="{ item, index }">
<t-chat-action
:content="item.content"
:operation-btn="['copy']"
@operation="handleOperation"
/>
</template>
<template #footer>
<!-- <t-chat-input :stop-disabled="isStreamLoad" @send="inputEnter" @stop="onStop"> </t-chat-input>-->
<t-chat-sender
ref="chatSenderRef"
v-model="inputValue"
class="chat-sender"
:textarea-props="{
placeholder: '请输入消息...',
}"
:loading="loading"
:stop-disabled="isStreamLoad"
@send="inputEnter"
@stop="onStop"
>
<template #suffix>
<!-- 监听键盘回车发送事件需要在sender组件监听 -->
<t-button theme="default" variant="text" size="large" class="btn" @click="inputEnter"> 发送 </t-button>
</template>
<template #prefix>
<NFlex>
<NSelect
v-model:value="selectValue"
:options="selectOptions"
label-field="name" value-field="ID"
size="tiny"
style="width: 200px;"
/>
</NFlex>
</template>
</t-chat-sender>
</template>
</t-chat>
<t-button v-show="isShowToBottom" variant="text" class="bottomBtn" @click="backBottom">
<div class="to-bottom">
<ArrowDownIcon />
</div>
</t-button>
</div>
</template>
<script setup lang="ts">
import {ref, onMounted, h, onBeforeUnmount, onBeforeMount} from 'vue';
import {ArrowDownIcon, CheckCircleIcon, SystemSumIcon} from 'tdesign-icons-vue-next';
const fetchCancel = ref(null);
const loading = ref(false);
const inputValue = ref('');
// 流式数据加载中
const isStreamLoad = ref(false);
const chatRef = ref(null);
const isShowToBottom = ref(false);
const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/master/build/appicon.png');
import {darkTheme, NFlex, NImage,NSelect} from "naive-ui";
import {ChatWithAgent, GetAiConfigs, GetConfig, GetSponsorInfo, GetVersionInfo} from "../../wailsjs/go/main/App";
import {EventsOff, EventsOn} from '../../wailsjs/runtime'
import 'tdesign-vue-next/es/style/index.css';
const allowToolTip = ref(true);
const chatSenderRef = ref(null);
const selectOptions = ref([]);
const selectValue = ref("default");
onBeforeUnmount(() => {
EventsOff("agent-message")
})
EventsOn("agent-message", (data) => {
console.log(data)
if(data['role']==="assistant"){
loading.value = false;
const lastItem = chatList.value[0];
// if (data['reasoning_content']){
// lastItem.reasoning = data['reasoning_content'];
// }
if (data['content']){
lastItem.content +=data['content'];
}
if(data['tool_calls']){
for (const tool of data['tool_calls']) {
console.log(tool.id, tool.type, tool.function.name, tool.function.arguments);
lastItem.reasoning += "\n```"+tool.function.name+"\n" +
"参数:"+ (tool.function.arguments?tool.function.arguments:"无")+
"\n```\n";
}
}
}
if(data['response_meta']&&data['response_meta'].finish_reason==="stop"){
isStreamLoad.value = false;
loading.value = false;
}
})
onBeforeMount(() => {
GetAiConfigs().then(res=>{
console.log(res)
selectOptions.value = res
selectValue.value = res[0].ID
})
})
onMounted(() => {
//chatRef.value.scrollToBottom();
GetConfig().then((res) => {
if (res.darkTheme) {
document.documentElement.setAttribute("theme-mode", "dark");
} else {
document.documentElement.removeAttribute("theme-mode"); }
})
GetVersionInfo().then((res) => {
icon.value = res.icon;
});
});
// 滚动到底部
const backBottom = () => {
chatRef.value.scrollToBottom({
behavior: 'smooth',
});
};
// 是否显示回到底部按钮
const handleChatScroll = function ({ e }) {
const scrollTop = e.target.scrollTop;
isShowToBottom.value = scrollTop < 0;
};
// 清空消息
const clearConfirm = function () {
chatList.value = [];
};
const handleOperation = function (type, options) {
console.log('handleOperation', type, options);
};
// 倒序渲染
const chatList = ref([
// {
// content: `模型由<span>hunyuan</span>变为<span>GPT4</span>`,
// role: 'model-change',
// reasoning: '',
// },
{
avatar: h(NImage, { src: icon.value, height: '48px', width: '48px'}),
name: 'Go-Stock AI',
datetime: '',
reasoning: '',
content: '我是您的AI赋能股票分析助手,您可以问我任何关于股票投资方面的问题。',
role: 'assistant',
duration: 10,
},
{
avatar: 'https://tdesign.gtimg.com/site/avatar.jpg',
name: '宇宙无敌大韭菜',
datetime: '',
content: '介绍下自己?',
role: 'user',
reasoning: '',
},
]);
const onStop = function () {
if (fetchCancel.value) {
fetchCancel.value.controller.close();
loading.value = false;
isStreamLoad.value = false;
}
};
const inputEnter = function () {
if (isStreamLoad.value) {
return;
}
if (!inputValue.value) return;
const params = {
avatar: 'https://tdesign.gtimg.com/site/avatar.jpg',
name: '宇宙无敌大韭菜',
datetime: new Date().toDateString(),
content: inputValue.value,
role: 'user',
};
chatList.value.unshift(params);
// 空消息占位
const params2 = {
avatar: h(NImage, { src: icon.value, height: '48px', width: '48px'}),
name: 'Go-Stock AI',
datetime: new Date().toDateString(),
content: '',
reasoning: '',
role: 'assistant',
};
chatList.value.unshift(params2);
loading.value = true;
isStreamLoad.value = true;
ChatWithAgent(inputValue.value,selectValue.value,0)
};
</script>
<style lang="less">
/* 应用滚动条样式 */
::-webkit-scrollbar-thumb {
background-color: var(--td-scrollbar-color);
}
::-webkit-scrollbar-thumb:horizontal:hover {
background-color: var(--td-scrollbar-hover-color);
}
::-webkit-scrollbar-track {
background-color: var(--td-scroll-track-color);
}
.chat-box {
position: relative;
height: 100%;
margin: 5px 10px 5px 10px;
text-align: left;
.bottomBtn {
position: absolute;
left: 50%;
margin-left: -20px;
bottom: 210px;
padding: 0;
border: 0;
width: 40px;
height: 40px;
border-radius: 50%;
box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, 0.08), 0px 16px 24px 2px rgba(0, 0, 0, 0.04),
0px 6px 30px 5px rgba(0, 0, 0, 0.05);
}
.to-bottom {
width: 40px;
height: 40px;
border: 1px solid #dcdcdc;
box-sizing: border-box;
background: var(--td-bg-color-container);
border-radius: 50%;
font-size: 24px;
line-height: 40px;
display: flex;
align-items: center;
justify-content: center;
.t-icon {
font-size: 24px;
}
}
}
.model-select {
display: flex;
align-items: center;
.t-select {
width: 112px;
height: 32px;
margin-right: 8px;
.t-input {
border-radius: 32px;
padding: 0 15px;
}
}
.check-box {
width: 112px;
height: 32px;
border-radius: 32px;
border: 0;
background: #e7e7e7;
color: rgba(0, 0, 0, 0.9);
box-sizing: border-box;
flex: 0 0 auto;
.t-button__text {
display: flex;
align-items: center;
justify-content: center;
span {
margin-left: 4px;
}
}
}
.check-box.is-active {
border: 1px solid #d9e1ff;
background: #f2f3ff;
color: var(--td-brand-color);
}
}
.chat-sender {
.btn {
color: var(--td-text-color-disabled);
border: none;
&:hover {
color: var(--td-brand-color-hover);
border: none;
background: none;
}
}
.btn.t-button {
height: var(--td-comp-size-m);
padding: 0;
}
.model-select {
display: flex;
align-items: center;
.t-select {
width: 112px;
height: var(--td-comp-size-m);
margin-right: var(--td-comp-margin-s);
.t-input {
border-radius: 32px;
padding: 0 15px;
}
.t-input.t-is-focused {
box-shadow: none;
}
}
.check-box {
width: 112px;
height: var(--td-comp-size-m);
border-radius: 32px;
border: 0;
background: var(--td-bg-color-component);
color: var(--td-text-color-primary);
box-sizing: border-box;
flex: 0 0 auto;
.t-button__text {
display: flex;
align-items: center;
justify-content: center;
span {
margin-left: var(--td-comp-margin-xs);
}
}
}
.check-box.is-active {
border: 1px solid var(--td-brand-color-focus);
background: var(--td-brand-color-light);
color: var(--td-text-color-brand);
}
}
}
</style>

View File

@@ -0,0 +1,338 @@
<template>
<div class="chat-box">
<t-chat
ref="chatRef"
:clear-history="chatList.length > 0 && !isStreamLoad"
:data="chatList"
:text-loading="loading"
:is-stream-load="isStreamLoad"
style="height: 100%"
@scroll="handleChatScroll"
@clear="clearConfirm"
>
<!-- eslint-disable vue/no-unused-vars -->
<template #content="{ item, index }">
<t-chat-reasoning v-if="item.reasoning?.length > 0" expand-icon-placement="right">
<template #header>
<t-chat-loading v-if="isStreamLoad && item.content.length === 0" text="思考中..." />
<div v-else style="display: flex; align-items: center">
<CheckCircleIcon style="color: var(--td-success-color-5); font-size: 20px; margin-right: 8px" />
<span>已深度思考</span>
</div>
</template>
<t-chat-content v-if="item.reasoning.length > 0" :content="item.reasoning" />
</t-chat-reasoning>
<t-chat-content v-if="item.content.length > 0" :content="item.content" />
</template>
<template #actions="{ item, index }">
<t-chat-action
:content="item.content"
:operation-btn="['good', 'bad', 'replay', 'copy']"
@operation="handleOperation"
/>
</template>
<template #footer>
<t-chat-input :stop-disabled="isStreamLoad" @send="inputEnter" @stop="onStop"> </t-chat-input>
</template>
</t-chat>
<t-button v-show="isShowToBottom" variant="text" class="bottomBtn" @click="backBottom">
<div class="to-bottom">
<ArrowDownIcon />
</div>
</t-button>
</div>
</template>
<script setup lang="jsx">
import {ref, onMounted, h, onBeforeUnmount} from 'vue';
import { MockSSEResponse } from '../mock-data/index';
import { ArrowDownIcon, CheckCircleIcon } from 'tdesign-icons-vue-next';
const fetchCancel = ref(null);
const loading = ref(false);
// 流式数据加载中
const isStreamLoad = ref(false);
const chatRef = ref(null);
const isShowToBottom = ref(false);
const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/master/build/appicon.png');
import {darkTheme, NAvatar, NImage} from "naive-ui";
import {ChatWithAgent, GetConfig, GetSponsorInfo, GetVersionInfo} from "../../wailsjs/go/main/App";
import {EventsOff, EventsOn} from '../../wailsjs/runtime'
import 'tdesign-vue-next/es/style/index.css';
onBeforeUnmount(() => {
EventsOff("agent-message")
})
EventsOn("agent-message", (data) => {
console.log(data)
if(data['role']==="assistant"){
loading.value = false;
isStreamLoad.value = true;
const lastItem = chatList.value[0];
if (data['reasoning_content']){
lastItem.reasoning += data['reasoning_content'];
}
if (data['content']){
lastItem.content +=data['content'];
}
if(data['response_meta'].finish_reason==="stop"){
isStreamLoad.value = false;
}
if(data['tool_calls']){
lastItem.tool_calls = data['tool_calls'];
}
}
})
onMounted(() => {
//chatRef.value.scrollToBottom();
GetConfig().then((res) => {
if (res.darkTheme) {
document.documentElement.setAttribute("theme-mode", "dark");
} else {
document.documentElement.removeAttribute("theme-mode"); }
})
GetVersionInfo().then((res) => {
icon.value = res.icon;
});
});
// 滚动到底部
const backBottom = () => {
chatRef.value.scrollToBottom({
behavior: 'smooth',
});
};
// 是否显示回到底部按钮
const handleChatScroll = function ({ e }) {
const scrollTop = e.target.scrollTop;
isShowToBottom.value = scrollTop < 0;
};
// 清空消息
const clearConfirm = function () {
chatList.value = [];
};
const handleOperation = function (type, options) {
console.log('handleOperation', type, options);
};
// 倒序渲染
const chatList = ref([
// {
// content: `模型由<span>hunyuan</span>变为<span>GPT4</span>`,
// role: 'model-change',
// reasoning: '',
// },
{
avatar: h(NImage, { src: icon.value, height: '48px', width: '48px'}),
name: 'Go-Stock AI',
datetime: '',
reasoning: '',
content: '我是您的AI赋能股票分析助手,您可以问我任何关于股票投资方面的问题。',
role: 'assistant',
duration: 10,
},
{
avatar: 'https://tdesign.gtimg.com/site/avatar.jpg',
name: '宇宙无敌大韭菜',
datetime: '',
content: '介绍下自己?',
role: 'user',
reasoning: '',
},
]);
const onStop = function () {
if (fetchCancel.value) {
fetchCancel.value.controller.close();
loading.value = false;
isStreamLoad.value = false;
}
};
const inputEnter = function (inputValue) {
if (isStreamLoad.value) {
return;
}
if (!inputValue) return;
const params = {
avatar: 'https://tdesign.gtimg.com/site/avatar.jpg',
name: '宇宙无敌大韭菜',
datetime: new Date().toDateString(),
content: inputValue,
role: 'user',
};
chatList.value.unshift(params);
// 空消息占位
const params2 = {
avatar: h(NImage, { src: icon.value, height: '48px', width: '48px'}),
name: 'Go-Stock AI',
datetime: new Date().toDateString(),
content: '',
reasoning: '',
role: 'assistant',
};
chatList.value.unshift(params2);
handleData(inputValue);
ChatWithAgent(inputValue,1,0)
};
const fetchSSE = async (fetchFn, options) => {
const response = await fetchFn();
const { success, fail, complete } = options;
// 如果不 ok 说明有请求错误
if (!response.ok) {
complete?.(false, response.statusText);
fail?.();
return;
}
const reader = response?.body?.getReader();
const decoder = new TextDecoder();
if (!reader) return;
reader.read().then(function processText({ done, value }) {
if (done) {
// 正常的返回
complete?.(true);
return;
}
const chunk = decoder.decode(value, { stream: true });
const buffers = chunk.toString().split(/\r?\n/);
const jsonData = JSON.parse(buffers);
success(jsonData);
reader.read().then(processText);
});
};
const handleData = async () => {
loading.value = true;
isStreamLoad.value = true;
const lastItem = chatList.value[0];
const mockedData = {
reasoning: `嗯,用户问牛顿第一定律是不是适用于所有参考系。首先,我得先回忆一下牛顿第一定律的内容。牛顿第一定律,也就是惯性定律,说物体在没有外力作用时会保持静止或匀速直线运动。也就是说,保持原来的运动状态。
那问题来了,这个定律是否适用于所有参考系呢?记得以前学过的参考系分惯性系和非惯性系。惯性系里,牛顿定律成立;非惯性系里,可能需要引入惯性力之类的修正。所以牛顿第一定律应该只在惯性参考系中成立,而在非惯性系中不适用,比如加速的电梯或者旋转的参考系,这时候物体会有看似无外力下的加速度,所以必须引入假想的力来解释。`,
content: `牛顿第一定律(惯性定律)**并不适用于所有参考系**,它只在**惯性参考系**中成立。以下是关键点:
---
### **1. 牛顿第一定律的核心**
- **内容**:物体在不受外力(或合力为零)时,将保持静止或匀速直线运动状态。
- **本质**:定义了惯性系的存在——即存在一类参考系,在其中惯性定律成立。`,
};
const mockResponse = new MockSSEResponse(mockedData);
fetchCancel.value = mockResponse;
await fetchSSE(
() => {
return mockResponse.getResponse();
},
{
success(result) {
console.log('success', result);
loading.value = false;
lastItem.reasoning += result.delta.reasoning_content;
lastItem.content += result.delta.content;
},
complete(isOk, msg) {
if (!isOk) {
lastItem.role = 'error';
lastItem.content = msg;
lastItem.reasoning = msg;
}
// 显示用时xx秒业务侧需要自行处理
lastItem.duration = 20;
// 控制终止按钮
isStreamLoad.value = false;
loading.value = false;
},
},
);
};
</script>
<style lang="less">
/* 应用滚动条样式 */
::-webkit-scrollbar-thumb {
background-color: var(--td-scrollbar-color);
}
::-webkit-scrollbar-thumb:horizontal:hover {
background-color: var(--td-scrollbar-hover-color);
}
::-webkit-scrollbar-track {
background-color: var(--td-scroll-track-color);
}
.chat-box {
position: relative;
height: 100%;
margin: 5px 10px 5px 10px;
.bottomBtn {
position: absolute;
left: 50%;
margin-left: -20px;
bottom: 210px;
padding: 0;
border: 0;
width: 40px;
height: 40px;
border-radius: 50%;
box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, 0.08), 0px 16px 24px 2px rgba(0, 0, 0, 0.04),
0px 6px 30px 5px rgba(0, 0, 0, 0.05);
}
.to-bottom {
width: 40px;
height: 40px;
border: 1px solid #dcdcdc;
box-sizing: border-box;
background: var(--td-bg-color-container);
border-radius: 50%;
font-size: 24px;
line-height: 40px;
display: flex;
align-items: center;
justify-content: center;
.t-icon {
font-size: 24px;
}
}
}
.model-select {
display: flex;
align-items: center;
.t-select {
width: 112px;
height: 32px;
margin-right: 8px;
.t-input {
border-radius: 32px;
padding: 0 15px;
}
}
.check-box {
width: 112px;
height: 32px;
border-radius: 32px;
border: 0;
background: #e7e7e7;
color: rgba(0, 0, 0, 0.9);
box-sizing: border-box;
flex: 0 0 auto;
.t-button__text {
display: flex;
align-items: center;
justify-content: center;
span {
margin-left: 4px;
}
}
}
.check-box.is-active {
border: 1px solid #d9e1ff;
background: #f2f3ff;
color: var(--td-brand-color);
}
}
</style>

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,
@@ -11,7 +11,8 @@ import {
SaveAIResponseResult,
SaveAsMarkdown,
ShareAnalysis,
SummaryStockNews
SummaryStockNews,
GetAiConfigs
} from "../../wailsjs/go/main/App";
import {EventsOff, EventsOn} from "../../wailsjs/runtime";
import NewsList from "./newsList.vue";
@@ -32,6 +33,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');
@@ -59,8 +61,10 @@ const aiSummaryTime = ref("")
const modelName = ref("")
const chatId = ref("")
const question = ref(``)
const sysPromptId = ref(0)
const aiConfigId = ref(null)
const sysPromptId = ref(null)
const loading = ref(true)
const aiConfigs = ref([])
const sysPromptOptions = ref([])
const userPromptOptions = ref([])
const promptTemplates = ref([])
@@ -70,6 +74,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 +87,6 @@ function getIndex() {
})
}
onBeforeMount(() => {
nowTab.value = route.query.name
stockCode.value = route.query.stockCode
@@ -97,6 +100,11 @@ onBeforeMount(() => {
userPromptOptions.value = promptTemplates.value.filter(item => item.type === '模型用户Prompt')
})
GetAiConfigs().then(res=>{
aiConfigs.value = res
aiConfigId.value = res[0].ID
})
GetTelegraphList("财联社电报").then((res) => {
telegraphList.value = res
})
@@ -129,16 +137,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 +198,14 @@ function reAiSummary() {
aiSummary.value = ""
summaryModal.value = true
loading.value = true
SummaryStockNews(question.value, sysPromptId.value)
SummaryStockNews(question.value,aiConfigId.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 +224,7 @@ function getAiSummary() {
aiSummaryTime.value = ""
aiSummary.value = ""
modelName.value = ""
SummaryStockNews(question.value, sysPromptId.value)
//SummaryStockNews(question.value, sysPromptId.value,enableTools.value)
}
})
}
@@ -225,7 +238,7 @@ EventsOn("summaryStockNews", async (msg) => {
loading.value = false
////console.log(msg)
if (msg === "DONE") {
SaveAIResponseResult("市场资讯", "市场资讯", aiSummary.value, chatId.value, question.value)
await SaveAIResponseResult("市场资讯", "市场资讯", aiSummary.value, chatId.value, question.value,aiConfigId.value)
message.info("AI分析完成")
message.destroyAll()
@@ -305,7 +318,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:no-drag">
<n-tab-pane name="市场快讯" tab="市场快讯">
<n-grid :cols="2" :y-gap="0">
<n-gi>
@@ -383,10 +396,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 +635,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,10 +655,23 @@ 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"
<n-select style="width: 32%" v-model:value="aiConfigId" label-field="name" value-field="ID"
:options="aiConfigs" placeholder="请选择AI模型服务配置"/>
<n-select style="width: 32%" v-model:value="sysPromptId" label-field="name" value-field="ID"
:options="sysPromptOptions" placeholder="请选择系统提示词"/>
<n-select style="width: 49%" v-model:value="question" label-field="name" value-field="content"
<n-select style="width: 32%" v-model:value="question" label-field="name" value-field="content"
:options="userPromptOptions" placeholder="请选择用户提示词"/>
</n-flex>
<n-flex justify="right">
@@ -651,5 +704,4 @@ function ReFlesh(source) {
</template>
<style scoped>
</style>

View File

@@ -1,165 +1,198 @@
<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()
const formRef = ref(null)
const formValue = ref({
ID:1,
tushareToken:'',
dingPush:{
enable:false,
ID: 1,
tushareToken: '',
dingPush: {
enable: false,
dingRobot: ''
},
localPush:{
enable:true,
localPush: {
enable: true,
},
updateBasicInfoOnStart:false,
refreshInterval:1,
openAI:{
enable:false,
updateBasicInfoOnStart: false,
refreshInterval: 1,
openAI: {
enable: false,
aiConfigs: [], // AI配置列表
prompt: "",
questionTemplate: "{{stockName}}分析和总结",
crawlTimeOut: 30,
kDays: 30,
},
enableDanmu: false,
browserPath: '',
enableNews: false,
darkTheme: true,
enableFund: false,
enablePushNews: false,
enableOnlyPushRedNews: false,
sponsorCode: "",
httpProxy:"",
httpProxyEnabled:false,
})
// 添加一个新的AI配置到列表
function addAiConfig() {
formValue.value.openAI.aiConfigs.push(new data.AIConfig({
name: '',
baseUrl: 'https://api.deepseek.com',
apiKey: '',
model: 'deepseek-chat',
modelName: 'deepseek-chat',
temperature: 0.1,
maxTokens: 1024,
prompt:"",
timeout: 5,
questionTemplate: "{{stockName}}分析和总结",
crawlTimeOut:30,
kDays:30,
},
enableDanmu:false,
browserPath: '',
enableNews:false,
darkTheme:true,
enableFund:false,
enablePushNews:false,
})
const promptTemplates=ref([])
onMounted(()=>{
GetConfig().then(res=>{
timeOut: 60,
}));
}
// 从列表中移除一个AI配置
function removeAiConfig(index) {
const originalCount = formValue.value.openAI.aiConfigs.length;
// 使用filter创建新数组确保响应式更新
formValue.value.openAI.aiConfigs = formValue.value.openAI.aiConfigs.filter((_, i) => i !== index);
}
const promptTemplates = ref([])
onMounted(() => {
GetConfig().then(res => {
formValue.value.ID = res.ID
formValue.value.tushareToken = res.tushareToken
formValue.value.dingPush = {
enable:res.dingPushEnable,
dingRobot:res.dingRobot
enable: res.dingPushEnable,
dingRobot: res.dingRobot
}
formValue.value.localPush = {
enable:res.localPushEnable,
enable: res.localPushEnable,
}
formValue.value.updateBasicInfoOnStart = res.updateBasicInfoOnStart
formValue.value.refreshInterval = res.refreshInterval
// 加载AI配置
formValue.value.openAI = {
enable:res.openAiEnable,
baseUrl: res.openAiBaseUrl,
apiKey:res.openAiApiKey,
model:res.openAiModelName,
temperature:res.openAiTemperature,
maxTokens:res.openAiMaxTokens,
prompt:res.prompt,
timeout:res.openAiApiTimeOut,
questionTemplate:res.questionTemplate?res.questionTemplate:'{{stockName}}分析和总结',
crawlTimeOut:res.crawlTimeOut,
kDays:res.kDays,
enable: res.openAiEnable,
aiConfigs: res.aiConfigs || [],
prompt: res.prompt,
questionTemplate: res.questionTemplate ? res.questionTemplate : '{{stockName}}分析和总结',
crawlTimeOut: res.crawlTimeOut,
kDays: res.kDays,
}
formValue.value.enableDanmu = res.enableDanmu
formValue.value.browserPath = res.browserPath
formValue.value.enableNews = res.enableNews
formValue.value.darkTheme = res.darkTheme
formValue.value.enableFund = res.enableFund
formValue.value.enablePushNews = res.enablePushNews
formValue.value.enableOnlyPushRedNews = res.enableOnlyPushRedNews
formValue.value.sponsorCode = res.sponsorCode
formValue.value.httpProxy=res.httpProxy;
formValue.value.httpProxyEnabled=res.httpProxyEnabled;
//console.log(res)
})
//message.info("加载完成")
GetPromptTemplates("","").then(res=>{
//console.log(res)
promptTemplates.value=res
GetPromptTemplates("", "").then(res => {
promptTemplates.value = res
})
})
onBeforeUnmount(() => {
message.destroyAll()
})
function saveConfig(){
let config= new data.Settings({
ID:formValue.value.ID,
dingPushEnable:formValue.value.dingPush.enable,
dingRobot:formValue.value.dingPush.dingRobot,
localPushEnable:formValue.value.localPush.enable,
updateBasicInfoOnStart:formValue.value.updateBasicInfoOnStart,
refreshInterval:formValue.value.refreshInterval,
openAiEnable:formValue.value.openAI.enable,
openAiBaseUrl:formValue.value.openAI.baseUrl,
openAiApiKey:formValue.value.openAI.apiKey,
openAiModelName:formValue.value.openAI.model,
openAiMaxTokens:formValue.value.openAI.maxTokens,
openAiTemperature:formValue.value.openAI.temperature,
tushareToken:formValue.value.tushareToken,
prompt:formValue.value.openAI.prompt,
openAiApiTimeOut:formValue.value.openAI.timeout,
questionTemplate:formValue.value.openAI.questionTemplate,
crawlTimeOut:formValue.value.openAI.crawlTimeOut,
kDays:formValue.value.openAI.kDays,
enableDanmu:formValue.value.enableDanmu,
browserPath:formValue.value.browserPath,
enableNews:formValue.value.enableNews,
darkTheme:formValue.value.darkTheme,
enableFund:formValue.value.enableFund,
enablePushNews:formValue.value.enablePushNews
function saveConfig() {
console.log('开始保存设置', formValue.value);
// 构建配置时包含aiConfigs列表
let config = new data.SettingConfig({
ID: formValue.value.ID,
dingPushEnable: formValue.value.dingPush.enable,
dingRobot: formValue.value.dingPush.dingRobot,
localPushEnable: formValue.value.localPush.enable,
updateBasicInfoOnStart: formValue.value.updateBasicInfoOnStart,
refreshInterval: formValue.value.refreshInterval,
openAiEnable: formValue.value.openAI.enable,
aiConfigs: formValue.value.openAI.aiConfigs,
// 序列化aiConfigs列表以传递给后端
tushareToken: formValue.value.tushareToken,
prompt: formValue.value.openAI.prompt,
questionTemplate: formValue.value.openAI.questionTemplate,
crawlTimeOut: formValue.value.openAI.crawlTimeOut,
kDays: formValue.value.openAI.kDays,
enableDanmu: formValue.value.enableDanmu,
browserPath: formValue.value.browserPath,
enableNews: formValue.value.enableNews,
darkTheme: formValue.value.darkTheme,
enableFund: formValue.value.enableFund,
enablePushNews: formValue.value.enablePushNews,
enableOnlyPushRedNews: formValue.value.enableOnlyPushRedNews,
sponsorCode: formValue.value.sponsorCode,
httpProxy:formValue.value.httpProxy,
httpProxyEnabled:formValue.value.httpProxyEnabled,
})
//console.log("Settings",config)
UpdateConfig(config).then(res=>{
message.success(res)
EventsEmit("updateSettings", config);
})
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);
})
}
}
function getHeight() {
return document.documentElement.clientHeight
}
function sendTestNotice(){
let markdown="### go-stock test\n"+new Date()
let msg='{' +
function sendTestNotice() {
let markdown = "### go-stock test\n" + new Date()
let msg = '{' +
' "msgtype": "markdown",' +
' "markdown": {' +
' "title":"go-stock'+new Date()+'",' +
' "text": "'+markdown+'"' +
' "title":"go-stock' + new Date() + '",' +
' "text": "' + markdown + '"' +
' },' +
' "at": {' +
' "isAtAll": true' +
' }' +
' }'
SendDingDingMessageByType(msg, "test-"+new Date().getTime(),1).then(res=>{
SendDingDingMessageByType(msg, "test-" + new Date().getTime(), 1).then(res => {
message.info(res)
})
}
function exportConfig(){
ExportConfig().then(res=>{
function exportConfig() {
ExportConfig().then(res => {
message.info(res)
})
}
function importConfig(){
function importConfig() {
let input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
@@ -168,30 +201,25 @@ function importConfig(){
let reader = new FileReader();
reader.onload = (e) => {
let config = JSON.parse(e.target.result);
//console.log(config)
formValue.value.ID = config.ID
formValue.value.tushareToken = config.tushareToken
formValue.value.dingPush = {
enable:config.dingPushEnable,
dingRobot:config.dingRobot
enable: config.dingPushEnable,
dingRobot: config.dingRobot
}
formValue.value.localPush = {
enable:config.localPushEnable,
enable: config.localPushEnable,
}
formValue.value.updateBasicInfoOnStart = config.updateBasicInfoOnStart
formValue.value.refreshInterval = config.refreshInterval
// 导入AI配置
formValue.value.openAI = {
enable:config.openAiEnable,
baseUrl: config.openAiBaseUrl,
apiKey:config.openAiApiKey,
model:config.openAiModelName,
temperature:config.openAiTemperature,
maxTokens:config.openAiMaxTokens,
prompt:config.prompt,
timeout:config.openAiApiTimeOut,
questionTemplate:config.questionTemplate,
crawlTimeOut:config.crawlTimeOut,
kDays:config.kDays
enable: config.openAiEnable,
aiConfigs: config.aiConfigs || [],
prompt: config.prompt,
questionTemplate: config.questionTemplate,
crawlTimeOut: config.crawlTimeOut,
kDays: config.kDays
}
formValue.value.enableDanmu = config.enableDanmu
formValue.value.browserPath = config.browserPath
@@ -199,7 +227,10 @@ function importConfig(){
formValue.value.darkTheme = config.darkTheme
formValue.value.enableFund = config.enableFund
formValue.value.enablePushNews = config.enablePushNews
// formRef.value.resetFields()
formValue.value.enableOnlyPushRedNews = config.enableOnlyPushRedNews
formValue.value.sponsorCode = config.sponsorCode
formValue.value.httpProxy=config.httpProxy
formValue.value.httpProxyEnabled=config.httpProxyEnabled
};
reader.readAsText(file);
};
@@ -208,8 +239,6 @@ function importConfig(){
window.onerror = function (event, source, lineno, colno, error) {
//console.log(event, source, lineno, colno, error)
// 将错误信息发送给后端
EventsEmit("frontendError", {
page: "settings.vue",
message: event,
@@ -218,223 +247,252 @@ window.onerror = function (event, source, lineno, colno, error) {
colno: colno,
error: error ? error.stack : null
});
//message.error("发生错误:"+event)
return true;
};
const showManagePromptsModal=ref(false)
const promptTypeOptions=[
{label:"模型系统Prompt",value:'模型系统Prompt'},
{label:"模型用户Prompt",value:'模型用户Prompt'},]
const formPromptRef=ref(null)
const formPrompt=ref({
ID:0,
Name:'',
Content:'',
Type:'',
const showManagePromptsModal = ref(false)
const promptTypeOptions = [
{label: "模型系统Prompt", value: '模型系统Prompt'},
{label: "模型用户Prompt", value: '模型用户Prompt'},]
const formPromptRef = ref(null)
const formPrompt = ref({
ID: 0,
Name: '',
Content: '',
Type: '',
})
function managePrompts(){
formPrompt.value.ID=0
showManagePromptsModal.value=true
function managePrompts() {
formPrompt.value.ID = 0
showManagePromptsModal.value = true
}
function savePrompt(){
AddPrompt(formPrompt.value).then(res=>{
function savePrompt() {
AddPrompt(formPrompt.value).then(res => {
message.success(res)
GetPromptTemplates("","").then(res=>{
//console.log(res)
promptTemplates.value=res
GetPromptTemplates("", "").then(res => {
promptTemplates.value = res
})
showManagePromptsModal.value=false
showManagePromptsModal.value = false
})
}
function editPrompt(prompt){
//console.log(prompt)
formPrompt.value.ID=prompt.ID
formPrompt.value.Name=prompt.name
formPrompt.value.Content=prompt.content
formPrompt.value.Type=prompt.type
showManagePromptsModal.value=true
function editPrompt(prompt) {
formPrompt.value.ID = prompt.ID
formPrompt.value.Name = prompt.name
formPrompt.value.Content = prompt.content
formPrompt.value.Type = prompt.type
showManagePromptsModal.value = true
}
function deletePrompt(ID){
DelPrompt(ID).then(res=>{
function deletePrompt(ID) {
DelPrompt(ID).then(res => {
message.success(res)
GetPromptTemplates("","").then(res=>{
//console.log(res)
promptTemplates.value=res
GetPromptTemplates("", "").then(res => {
promptTemplates.value = res
})
})
}
</script>
<template>
<n-flex justify="left" style="margin-top: 12px;padding-left: 12px;">
<n-form ref="formRef" :label-placement="'left'" :label-align="'left'" >
<n-grid :cols="24" :x-gap="24" style="text-align: left" :layout-shift-disabled="true">
<n-gi :span="24">
<n-text type="success" style="font-size: 25px;font-weight: bold">基础设置</n-text>
</n-gi>
<n-form-item-gi :span="10" label="Tushare &nbsp;&nbsp;Token" path="tushareToken" >
<n-input type="text" placeholder="Tushare api token" v-model:value="formValue.tushareToken" clearable />
</n-form-item-gi>
<n-form-item-gi :span="4" label="启动时更新A股/指数信息:" path="updateBasicInfoOnStart" >
<n-switch v-model:value="formValue.updateBasicInfoOnStart" />
</n-form-item-gi>
<n-form-item-gi :span="4" label="数据刷新间隔" path="refreshInterval" >
<n-input-number v-model:value="formValue.refreshInterval" placeholder="请输入数据刷新间隔(秒)">
<template #suffix>
</template>
</n-input-number>
</n-form-item-gi>
<n-form-item-gi :span="6" label="暗黑主题" path="darkTheme" >
<n-switch v-model:value="formValue.darkTheme" />
</n-form-item-gi>
<n-form-item-gi :span="10" label="浏览器安装路径" path="browserPath" >
<n-input type="text" placeholder="浏览器安装路径" v-model:value="formValue.browserPath" clearable />
</n-form-item-gi>
<n-form-item-gi :span="6" label="指数基金" path="enableFund" >
<n-switch v-model:value="formValue.enableFund" />
</n-form-item-gi>
</n-grid>
<n-flex justify="left" style="text-align: left; --wails-draggable:no-drag">
<n-form ref="formRef" :label-placement="'left'" :label-align="'left'">
<n-space vertical size="large">
<n-card :title="() => h(NTag, { type: 'primary', bordered: false }, () => '基础设置')" size="small">
<n-grid :cols="24" :x-gap="24" style="text-align: left">
<n-form-item-gi :span="10" label="Tushare Token" path="tushareToken">
<n-input type="text" placeholder="Tushare api token" v-model:value="formValue.tushareToken" clearable/>
</n-form-item-gi>
<n-form-item-gi :span="4" label="启动时更新基础信息:" path="updateBasicInfoOnStart">
<n-switch v-model:value="formValue.updateBasicInfoOnStart"/>
</n-form-item-gi>
<n-form-item-gi :span="4" label="数据刷新间隔:" path="refreshInterval">
<n-input-number v-model:value="formValue.refreshInterval" placeholder="请输入数据刷新间隔(秒)">
<template #suffix></template>
</n-input-number>
</n-form-item-gi>
<n-form-item-gi :span="6" label="暗黑主题:" path="darkTheme">
<n-switch v-model:value="formValue.darkTheme"/>
</n-form-item-gi>
<n-form-item-gi :span="10" label="浏览器安装路径" path="browserPath">
<n-input type="text" placeholder="浏览器安装路径" v-model:value="formValue.browserPath" clearable/>
</n-form-item-gi>
<n-form-item-gi :span="3" label="指数基金" path="enableFund">
<n-switch v-model:value="formValue.enableFund"/>
</n-form-item-gi>
<n-form-item-gi :span="11" label="赞助码" path="sponsorCode">
<n-input-group>
<n-input :show-count="true" placeholder="赞助码" v-model:value="formValue.sponsorCode"/>
<n-button type="success" secondary strong
@click="CheckSponsorCode(formValue.sponsorCode).then((res) => {message.warning(res.msg)})">验证
</n-button>
</n-input-group>
</n-form-item-gi>
</n-grid>
</n-card>
<n-grid :cols="24" :x-gap="24" style="text-align: left">
<n-gi :span="24">
<n-text type="success" style="font-size: 25px;font-weight: bold">通知设置</n-text>
</n-gi>
<n-form-item-gi :span="4" label="钉钉推送:" path="dingPush.enable" >
<n-switch v-model:value="formValue.dingPush.enable" />
</n-form-item-gi>
<n-form-item-gi :span="4" label="本地推送:" path="localPush.enable" >
<n-switch v-model:value="formValue.localPush.enable" />
</n-form-item-gi>
<n-form-item-gi :span="4" label="弹幕功能:" path="enableDanmu" >
<n-switch v-model:value="formValue.enableDanmu" />
</n-form-item-gi>
<n-form-item-gi :span="4" label="显示滚动快讯:" path="enableNews" >
<n-switch v-model:value="formValue.enableNews" />
</n-form-item-gi>
<n-form-item-gi :span="4" label="市场资讯提醒:" path="enablePushNews" >
<n-switch v-model:value="formValue.enablePushNews" />
</n-form-item-gi>
<n-form-item-gi :span="22" v-if="formValue.dingPush.enable" label="钉钉机器人接口地址:" path="dingPush.dingRobot" >
<n-input placeholder="请输入钉钉机器人接口地址" v-model:value="formValue.dingPush.dingRobot"/>
<n-button type="primary" @click="sendTestNotice">发送测试通知</n-button>
</n-form-item-gi>
</n-grid>
<n-card :title="() => h(NTag, { type: 'primary', bordered: false }, () => '通知设置')" size="small">
<n-grid :cols="24" :x-gap="24" style="text-align: left">
<n-form-item-gi :span="3" label="钉钉推送:" path="dingPush.enable">
<n-switch v-model:value="formValue.dingPush.enable"/>
</n-form-item-gi>
<n-form-item-gi :span="3" label="本地推送:" path="localPush.enable">
<n-switch v-model:value="formValue.localPush.enable"/>
</n-form-item-gi>
<n-form-item-gi :span="3" label="弹幕功能:" path="enableDanmu">
<n-switch v-model:value="formValue.enableDanmu"/>
</n-form-item-gi>
<n-form-item-gi :span="3" label="显示滚动快讯:" path="enableNews">
<n-switch v-model:value="formValue.enableNews"/>
</n-form-item-gi>
<n-form-item-gi :span="3" label="市场资讯提醒:" path="enablePushNews">
<n-switch v-model:value="formValue.enablePushNews"/>
</n-form-item-gi>
<n-form-item-gi v-if="formValue.enablePushNews" :span="4" label="只提醒红字或关注个股的新闻:" path="enableOnlyPushRedNews">
<n-switch v-model:value="formValue.enableOnlyPushRedNews"/>
</n-form-item-gi>
<n-grid :cols="24" :x-gap="24" style="text-align: left;">
<n-gi :span="24">
<n-text type="success" style="font-size: 25px;font-weight: bold">OpenAI设置</n-text>
</n-gi>
<n-form-item-gi :span="3" label="AI诊股" path="openAI.enable" >
<n-switch v-model:value="formValue.openAI.enable" />
</n-form-item-gi>
<n-form-item-gi :span="9" v-if="formValue.openAI.enable" label="openAI 接口地址:" path="openAI.baseUrl" >
<n-input type="text" placeholder="AI接口地址" v-model:value="formValue.openAI.baseUrl" clearable />
</n-form-item-gi>
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" label="AI Timeout(秒)" title="AI请求超时时间(秒)" path="openAI.timeout" >
<n-input-number min="60" step="1" placeholder="AI请求超时时间(秒)" v-model:value="formValue.openAI.timeout" />
</n-form-item-gi>
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" label="Crawler Timeout(秒)" title="资讯采集超时时间(秒)" path="openAI.crawlTimeOut" >
<n-input-number min="30" step="1" placeholder="资讯采集超时时间(秒)" v-model:value="formValue.openAI.crawlTimeOut" />
</n-form-item-gi>
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="openAI 令牌(apiKey)" path="openAI.apiKey" >
<n-input type="text" placeholder="apiKey" v-model:value="formValue.openAI.apiKey" clearable />
</n-form-item-gi>
<n-form-item-gi :span="10" v-if="formValue.openAI.enable" label="AI模型名称" path="openAI.model" >
<n-input type="text" placeholder="AI模型名称" v-model:value="formValue.openAI.model" clearable />
</n-form-item-gi>
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="openAI temperature" path="openAI.temperature" >
<n-input-number placeholder="temperature" v-model:value="formValue.openAI.temperature"/>
</n-form-item-gi>
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" label="openAI maxTokens" path="openAI.maxTokens" >
<n-input-number placeholder="maxTokens" v-model:value="formValue.openAI.maxTokens"/>
</n-form-item-gi>
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" title="天数越多消耗tokens越多" label="日K线数据(天)" path="openAI.maxTokens" >
<n-input-number min="30" step="1" max="365" placeholder="日K线数据(天)" title="天数越多消耗tokens越多" v-model:value="formValue.openAI.kDays"/>
</n-form-item-gi>
<n-form-item-gi :span="11" v-if="formValue.openAI.enable" label="模型系统 Prompt" path="openAI.prompt" >
<n-input v-model:value="formValue.openAI.prompt"
type="textarea"
:show-count="true"
placeholder="请输入系统prompt"
:autosize="{
minRows: 5,
maxRows: 8
}"
/>
</n-form-item-gi>
<n-form-item-gi :span="11" v-if="formValue.openAI.enable" label="模型用户 Prompt" path="openAI.questionTemplate" >
<n-input v-model:value="formValue.openAI.questionTemplate"
type="textarea"
:show-count="true"
placeholder="请输入用户prompt:例如{{stockName}}[{{stockCode}}]分析和总结"
:autosize="{
minRows: 5,
maxRows: 8
}"
/>
</n-form-item-gi>
</n-grid>
<n-gi :span="24">
<n-space justify="center">
<n-button type="warning" @click="managePrompts">
添加提示词模板
</n-button>
<n-button type="primary" @click="saveConfig">
保存
</n-button>
<n-button type="info" @click="exportConfig">
导出
</n-button>
<n-button type="error" @click="importConfig">
导入
</n-button>
</n-space>
</n-gi>
</n-form>
<n-gi :span="24" v-if="promptTemplates.length>0" v-for="prompt in promptTemplates" >
<n-flex justify="start">
<n-tag closable @close="deletePrompt(prompt.ID)" @click="editPrompt(prompt)" :title="prompt.content" :type="prompt.type==='模型系统Prompt'?'success':'info'" :bordered="false"> {{prompt.name}} </n-tag>
</n-flex>
</n-gi>
<n-form-item-gi :span="22" v-if="formValue.dingPush.enable" label="钉钉机器人接口地址:"
path="dingPush.dingRobot">
<n-input placeholder="请输入钉钉机器人接口地址" v-model:value="formValue.dingPush.dingRobot"/>
<n-button type="primary" @click="sendTestNotice">发送测试通知</n-button>
</n-form-item-gi>
</n-grid>
</n-card>
<n-card :title="() => h(NTag, { type: 'primary', bordered: false }, () => 'AI设置')" size="small">
<n-grid :cols="24" :x-gap="24" style="text-align: left;">
<n-form-item-gi :span="24" label="AI诊股" path="openAI.enable">
<n-switch v-model:value="formValue.openAI.enable"/>
</n-form-item-gi>
<n-form-item-gi :span="6" v-if="formValue.openAI.enable" label="Crawler Timeout(秒)"
title="资讯采集超时时间(秒)" path="openAI.crawlTimeOut">
<n-input-number min="30" step="1" v-model:value="formValue.openAI.crawlTimeOut"/>
</n-form-item-gi>
<n-form-item-gi :span="4" v-if="formValue.openAI.enable" title="天数越多消耗tokens越多"
label="日K线数据(天)" path="openAI.kDays">
<n-input-number min="30" step="1" max="365" v-model:value="formValue.openAI.kDays"/>
</n-form-item-gi>
<n-form-item-gi :span="2" label="http代理" path="httpProxyEnabled">
<n-switch v-model:value="formValue.httpProxyEnabled"/>
</n-form-item-gi>
<n-form-item-gi :span="10" v-if="formValue.httpProxyEnabled" title="http代理地址"
label="http代理地址" path="httpProxy">
<n-input type="text" placeholder="http代理地址" v-model:value="formValue.httpProxy" clearable/>
</n-form-item-gi>
<n-gi :span="24" v-if="formValue.openAI.enable">
<n-divider title-placement="left">Prompt 内容设置</n-divider>
</n-gi>
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="模型系统 Prompt" path="openAI.prompt">
<n-input v-model:value="formValue.openAI.prompt" type="textarea" :show-count="true"
placeholder="请输入系统prompt" :autosize="{ minRows: 4, maxRows: 8 }"/>
</n-form-item-gi>
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="模型用户 Prompt"
path="openAI.questionTemplate">
<n-input v-model:value="formValue.openAI.questionTemplate" type="textarea" :show-count="true"
placeholder="请输入用户prompt:例如{{stockName}}[{{stockCode}}]分析和总结"
:autosize="{ minRows: 4, maxRows: 8 }"/>
</n-form-item-gi>
<n-gi :span="24" v-if="formValue.openAI.enable">
<n-divider title-placement="left">AI模型服务配置</n-divider>
</n-gi>
<n-gi :span="24" v-if="formValue.openAI.enable">
<n-space vertical>
<n-card v-for="(aiConfig, index) in formValue.openAI.aiConfigs" :key="index" :bordered="true"
size="small">
<template #header>
<n-flex justify="space-between" align="center">
<n-text depth="3">AI 配置 #{{ index + 1 }}</n-text>
<n-button type="error" size="tiny" ghost @click="removeAiConfig(index)">删除</n-button>
</n-flex>
</template>
<n-grid :cols="24" :x-gap="24">
<n-form-item-gi :span="24" hidden label="配置ID" :path="`openAI.aiConfigs[${index}].ID`">
<n-input type="text" placeholder="配置ID" v-model:value="aiConfig.ID" clearable/>
</n-form-item-gi>
<n-form-item-gi :span="12" label="配置名称" :path="`openAI.aiConfigs[${index}].name`">
<n-input type="text" placeholder="配置名称" v-model:value="aiConfig.name" clearable/>
</n-form-item-gi>
<n-form-item-gi :span="12" label="接口地址" :path="`openAI.aiConfigs[${index}].baseUrl`">
<n-input type="text" placeholder="AI接口地址" v-model:value="aiConfig.baseUrl" clearable/>
</n-form-item-gi>
<n-form-item-gi :span="12" label="令牌(apiKey)" :path="`openAI.aiConfigs[${index}].apiKey`">
<n-input type="password" placeholder="apiKey" v-model:value="aiConfig.apiKey" clearable
show-password-on="click"/>
</n-form-item-gi>
<n-form-item-gi :span="8" label="模型名称" :path="`openAI.aiConfigs[${index}].modelName`">
<n-input type="text" placeholder="AI模型名称" v-model:value="aiConfig.modelName" clearable/>
</n-form-item-gi>
<n-form-item-gi :span="5" label="Temperature" :path="`openAI.aiConfigs[${index}].temperature`">
<n-input-number placeholder="temperature" v-model:value="aiConfig.temperature" :step="0.1"/>
</n-form-item-gi>
<n-form-item-gi :span="5" label="MaxTokens" :path="`openAI.aiConfigs[${index}].maxTokens`">
<n-input-number placeholder="maxTokens" v-model:value="aiConfig.maxTokens"/>
</n-form-item-gi>
<n-form-item-gi :span="5" label="Timeout(秒)" :path="`openAI.aiConfigs[${index}].timeOut`">
<n-input-number min="60" step="1" placeholder="超时(秒)" v-model:value="aiConfig.timeOut"/>
</n-form-item-gi>
</n-grid>
</n-card>
<n-button type="primary" dashed @click="addAiConfig" style="width: 100%;">+ 添加AI配置</n-button>
</n-space>
</n-gi>
<n-gi :span="24">
<n-divider/>
</n-gi>
<n-gi :span="24">
<n-space vertical>
<n-space justify="center">
<n-button type="warning" @click="managePrompts">管理提示词模板</n-button>
<n-button type="primary" strong @click="saveConfig">保存设置</n-button>
<n-button type="info" @click="exportConfig">导出配置</n-button>
<n-button type="error" @click="importConfig">导入配置</n-button>
</n-space>
<n-flex justify="start" style="margin-top: 10px" v-if="promptTemplates.length > 0">
<n-tag :bordered="false" type="warning">提示词模板:</n-tag>
<n-tag size="medium" secondary v-for="prompt in promptTemplates" closable
@close="deletePrompt(prompt.ID)" @click="editPrompt(prompt)" :title="prompt.content"
:type="prompt.type === '模型系统Prompt' ? 'success' : 'info'" :bordered="false">{{
prompt.name
}}
</n-tag>
</n-flex>
</n-space>
</n-gi>
</n-grid>
</n-card>
</n-space>
</n-form>
</n-flex>
<n-modal v-model:show="showManagePromptsModal" closable :mask-closable="false">
<n-card
style="width: 800px;height: 600px;text-align: left"
:bordered="false"
:title="(formPrompt.ID>0?'修改':'添加')+'提示词'"
size="huge"
role="dialog"
aria-modal="true"
>
<n-form ref="formPromptRef" :label-placement="'left'" :label-align="'left'" >
<n-form-item label="名称">
<n-input v-model:value="formPrompt.Name" placeholder="请输入提示词名称" />
<n-modal v-model:show="showManagePromptsModal" closable :mask-closable="false">
<n-card style="width: 800px; height: 600px; text-align: left" :bordered="false"
:title="(formPrompt.ID > 0 ? '修改' : '添加') + '提示词'" size="huge" role="dialog" aria-modal="true">
<n-form ref="formPromptRef" :label-placement="'left'" :label-align="'left'">
<n-form-item label="名称">
<n-input v-model:value="formPrompt.Name" placeholder="请输入提示词名称"/>
</n-form-item>
<n-form-item label="类型">
<n-select v-model:value="formPrompt.Type" :options="promptTypeOptions" placeholder="请选择提示词类型" />
<n-form-item label="类型">
<n-select v-model:value="formPrompt.Type" :options="promptTypeOptions" placeholder="请选择提示词类型"/>
</n-form-item>
<n-form-item label="内容">
<n-input v-model:value="formPrompt.Content"
type="textarea"
:show-count="true"
placeholder="请输入prompt"
:autosize="{
minRows: 12,
maxRows: 12,
}"
/>
<n-form-item label="内容">
<n-input v-model:value="formPrompt.Content" type="textarea" :show-count="true" placeholder="请输入prompt"
:autosize="{ minRows: 12, maxRows: 12, }"/>
</n-form-item>
</n-form>
<template #footer>
<n-flex justify="end">
<n-button type="primary" @click="savePrompt">
保存
</n-button>
<n-button type="warning" @click="showManagePromptsModal=false">
取消
</n-button>
<n-button type="primary" @click="savePrompt">保存</n-button>
<n-button type="warning" @click="showManagePromptsModal = false">取消</n-button>
</n-flex>
</template>
</n-card>
@@ -442,5 +500,9 @@ function deletePrompt(ID){
</template>
<style scoped>
.cardHeaderClass {
font-size: 16px;
font-weight: bold;
color: red;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -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,7 +2,8 @@ import {createApp} from 'vue'
import naive from 'naive-ui'
import App from './App.vue'
import router from './router/router'
// 引入组件库的少量全局样式变量
import 'tdesign-vue-next/es/style/index.css';
const app = createApp(App)
app.use(router)

View File

@@ -0,0 +1,101 @@
export class MockSSEResponse {
private controller!: ReadableStreamDefaultController<Uint8Array>;
private encoder = new TextEncoder();
private stream: ReadableStream<Uint8Array>;
private error: boolean;
private currentPhase: 'reasoning' | 'content' = 'reasoning';
constructor(
private data: {
reasoning: string; // 推理内容
content: string; // 正式内容
},
private delay: number = 100,
error = false,
) {
this.error = error;
this.stream = new ReadableStream({
start: (controller) => {
this.controller = controller;
if (!this.error) {
// 如果不是错误情况,则开始推送数据
setTimeout(() => this.pushData(), this.delay); // 延迟开始推送数据
}
},
cancel() {},
});
}
private pushData() {
try {
if (this.currentPhase === 'reasoning') {
// 推送推理内容
if (this.data.reasoning.length > 0) {
const chunk = JSON.stringify({
delta: {
reasoning_content: this.data.reasoning.slice(0, 1),
content: '',
},
finished: false,
});
this.controller.enqueue(this.encoder.encode(chunk));
this.data.reasoning = this.data.reasoning.slice(1);
// 设置下次推送
setTimeout(() => this.pushData(), this.delay);
} else {
// 推理内容推送完成,切换到正式内容
this.currentPhase = 'content';
setTimeout(() => this.pushData(), this.delay); // 立即开始推送正式内容
return;
}
}
if (this.currentPhase === 'content') {
// 推送正式内容
if (this.data.content.length > 0) {
const chunk = JSON.stringify({
delta: {
reasoning_content: '',
content: this.data.content.slice(0, 1),
},
finished: this.data.content.length === 1, // 最后一个字符时标记完成
});
this.controller.enqueue(this.encoder.encode(chunk));
this.data.content = this.data.content.slice(1);
// 设置下次推送
setTimeout(() => this.pushData(), this.delay);
} else {
// const finalPayload = JSON.stringify({
// delta: {
// reasoning_content: '',
// content: '',
// },
// finished: true,
// });
// this.controller.enqueue(this.encoder.encode(`${finalPayload}`));
// 全部内容推送完成
setTimeout(() => this.controller.close(), this.delay);
return;
}
}
} catch {}
}
getResponse(): Promise<Response> {
return new Promise((resolve) => {
// 使用setTimeout来模拟网络延迟
setTimeout(() => {
if (this.error) {
const errorResponseOptions = { status: 500, statusText: 'Internal Server Error' };
// 返回模拟的网络错误响应这里我们使用500状态码作为示例
resolve(new Response(null, errorResponseOptions));
} else {
resolve(new Response(this.stream));
}
}, this.delay); // 使用构造函数中设置的delay值作为延迟时间
});
}
}

View File

@@ -2,21 +2,23 @@ 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";
import agentChat from "../components/agent-chat.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' },
{ path: '/agent', component: agentChat,name: 'agent' },
]
const router = createRouter({
history: createWebHistory(),
//history: createWebHistory(),
history: createWebHashHistory(),
routes,
})

View File

@@ -1,7 +1,22 @@
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { TDesignResolver } from '@tdesign-vue-next/auto-import-resolver';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()]
plugins: [
vue(),
AutoImport({
resolvers: [TDesignResolver({
library: 'chat'
})],
}),
Components({
resolvers: [TDesignResolver({
library: 'chat'
})],
}),
]
})

35
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,7 +14,13 @@ export function AddStockGroup(arg1:number,arg2:string):Promise<string>;
export function AnalyzeSentiment(arg1:string):Promise<data.SentimentResult>;
export function CheckUpdate():Promise<void>;
export function ChatWithAgent(arg1:string,arg2:number,arg3:any):Promise<void>;
export function CheckSponsorCode(arg1:string):Promise<Record<string, any>>;
export function CheckStockBaseInfo(arg1:context.Context):Promise<void>;
export function CheckUpdate(arg1:number):Promise<void>;
export function ClsCalendar():Promise<Array<any>>;
@@ -29,7 +36,9 @@ export function FollowFund(arg1:string):Promise<string>;
export function GetAIResponseResult(arg1:string):Promise<models.AIResponseResult>;
export function GetConfig():Promise<data.Settings>;
export function GetAiConfigs():Promise<Array<data.AIConfig>>;
export function GetConfig():Promise<data.SettingConfig>;
export function GetFollowList(arg1:number):Promise<any>;
@@ -39,6 +48,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 +58,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>;
@@ -75,24 +88,32 @@ export function HotTopic(arg1:number):Promise<Array<any>>;
export function IndustryResearchReport(arg1:string):Promise<Array<any>>;
export function InitializeGroupSort():Promise<boolean>;
export function InvestCalendarTimeLine(arg1:string):Promise<Array<any>>;
export function LongTigerRank(arg1:string):Promise<any>;
export function NewChatStream(arg1:string,arg2:string,arg3:string,arg4:any):Promise<void>;
export function NewChatStream(arg1:string,arg2:string,arg3:string,arg4:number,arg5:any,arg6:boolean):Promise<void>;
export function NewsPush(arg1:any):Promise<void>;
export function OpenURL(arg1:string):Promise<void>;
export function ReFleshTelegraphList(arg1:string):Promise<any>;
export function RemoveGroup(arg1:number):Promise<string>;
export function RemoveStockGroup(arg1:string,arg2:string,arg3:number):Promise<string>;
export function SaveAIResponseResult(arg1:string,arg2:string,arg3:string,arg4:string,arg5:string):Promise<void>;
export function SaveAIResponseResult(arg1:string,arg2:string,arg3:string,arg4:string,arg5:string,arg6:number):Promise<void>;
export function SaveAsMarkdown(arg1:string,arg2:string):Promise<string>;
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,10 +134,12 @@ 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:number,arg3:any,arg4:boolean):Promise<void>;
export function UnFollow(arg1:string):Promise<string>;
export function UnFollowFund(arg1:string):Promise<string>;
export function UpdateConfig(arg1:data.Settings):Promise<string>;
export function UpdateConfig(arg1:data.SettingConfig):Promise<string>;
export function UpdateGroupSort(arg1:number,arg2:number):Promise<boolean>;

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

@@ -22,8 +22,20 @@ export function AnalyzeSentiment(arg1) {
return window['go']['main']['App']['AnalyzeSentiment'](arg1);
}
export function CheckUpdate() {
return window['go']['main']['App']['CheckUpdate']();
export function ChatWithAgent(arg1, arg2, arg3) {
return window['go']['main']['App']['ChatWithAgent'](arg1, arg2, arg3);
}
export function CheckSponsorCode(arg1) {
return window['go']['main']['App']['CheckSponsorCode'](arg1);
}
export function CheckStockBaseInfo(arg1) {
return window['go']['main']['App']['CheckStockBaseInfo'](arg1);
}
export function CheckUpdate(arg1) {
return window['go']['main']['App']['CheckUpdate'](arg1);
}
export function ClsCalendar() {
@@ -54,6 +66,10 @@ export function GetAIResponseResult(arg1) {
return window['go']['main']['App']['GetAIResponseResult'](arg1);
}
export function GetAiConfigs() {
return window['go']['main']['App']['GetAiConfigs']();
}
export function GetConfig() {
return window['go']['main']['App']['GetConfig']();
}
@@ -74,6 +90,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 +110,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);
}
@@ -146,6 +170,10 @@ export function IndustryResearchReport(arg1) {
return window['go']['main']['App']['IndustryResearchReport'](arg1);
}
export function InitializeGroupSort() {
return window['go']['main']['App']['InitializeGroupSort']();
}
export function InvestCalendarTimeLine(arg1) {
return window['go']['main']['App']['InvestCalendarTimeLine'](arg1);
}
@@ -154,14 +182,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, arg6) {
return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3, arg4, arg5, arg6);
}
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);
}
@@ -174,14 +206,22 @@ export function RemoveStockGroup(arg1, arg2, arg3) {
return window['go']['main']['App']['RemoveStockGroup'](arg1, arg2, arg3);
}
export function SaveAIResponseResult(arg1, arg2, arg3, arg4, arg5) {
return window['go']['main']['App']['SaveAIResponseResult'](arg1, arg2, arg3, arg4, arg5);
export function SaveAIResponseResult(arg1, arg2, arg3, arg4, arg5, arg6) {
return window['go']['main']['App']['SaveAIResponseResult'](arg1, arg2, arg3, arg4, arg5, arg6);
}
export function SaveAsMarkdown(arg1, arg2) {
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 +262,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, arg4) {
return window['go']['main']['App']['SummaryStockNews'](arg1, arg2, arg3, arg4);
}
export function UnFollow(arg1) {
@@ -237,3 +277,7 @@ export function UnFollowFund(arg1) {
export function UpdateConfig(arg1) {
return window['go']['main']['App']['UpdateConfig'](arg1);
}
export function UpdateGroupSort(arg1, arg2) {
return window['go']['main']['App']['UpdateGroupSort'](arg1, arg2);
}

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

@@ -1,5 +1,55 @@
export namespace data {
export class AIConfig {
ID: number;
// Go type: time
CreatedAt: any;
// Go type: time
UpdatedAt: any;
name: string;
baseUrl: string;
apiKey: string;
modelName: string;
maxTokens: number;
temperature: number;
timeOut: number;
static createFrom(source: any = {}) {
return new AIConfig(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.ID = source["ID"];
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
this.name = source["name"];
this.baseUrl = source["baseUrl"];
this.apiKey = source["apiKey"];
this.modelName = source["modelName"];
this.maxTokens = source["maxTokens"];
this.temperature = source["temperature"];
this.timeOut = source["timeOut"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class FundBasic {
ID: number;
// Go type: time
@@ -246,6 +296,7 @@ export namespace data {
Cron?: string;
IsDel: number;
Groups: GroupStock[];
AiConfigId: number;
static createFrom(source: any = {}) {
return new FollowedStock(source);
@@ -267,6 +318,7 @@ export namespace data {
this.Cron = source["Cron"];
this.IsDel = source["IsDel"];
this.Groups = this.convertValues(source["Groups"], GroupStock);
this.AiConfigId = source["AiConfigId"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
@@ -310,7 +362,7 @@ export namespace data {
this.Description = source["Description"];
}
}
export class Settings {
export class SettingConfig {
ID: number;
// Go type: time
CreatedAt: any;
@@ -325,12 +377,6 @@ export namespace data {
updateBasicInfoOnStart: boolean;
refreshInterval: number;
openAiEnable: boolean;
openAiBaseUrl: string;
openAiApiKey: string;
openAiModelName: string;
openAiMaxTokens: number;
openAiTemperature: number;
openAiApiTimeOut: number;
prompt: string;
checkUpdate: boolean;
questionTemplate: string;
@@ -343,9 +389,14 @@ export namespace data {
browserPoolSize: number;
enableFund: boolean;
enablePushNews: boolean;
enableOnlyPushRedNews: boolean;
sponsorCode: string;
httpProxy: string;
httpProxyEnabled: boolean;
aiConfigs: AIConfig[];
static createFrom(source: any = {}) {
return new Settings(source);
return new SettingConfig(source);
}
constructor(source: any = {}) {
@@ -361,12 +412,6 @@ export namespace data {
this.updateBasicInfoOnStart = source["updateBasicInfoOnStart"];
this.refreshInterval = source["refreshInterval"];
this.openAiEnable = source["openAiEnable"];
this.openAiBaseUrl = source["openAiBaseUrl"];
this.openAiApiKey = source["openAiApiKey"];
this.openAiModelName = source["openAiModelName"];
this.openAiMaxTokens = source["openAiMaxTokens"];
this.openAiTemperature = source["openAiTemperature"];
this.openAiApiTimeOut = source["openAiApiTimeOut"];
this.prompt = source["prompt"];
this.checkUpdate = source["checkUpdate"];
this.questionTemplate = source["questionTemplate"];
@@ -379,6 +424,11 @@ export namespace data {
this.browserPoolSize = source["browserPoolSize"];
this.enableFund = source["enableFund"];
this.enablePushNews = source["enablePushNews"];
this.enableOnlyPushRedNews = source["enableOnlyPushRedNews"];
this.sponsorCode = source["sponsorCode"];
this.httpProxy = source["httpProxy"];
this.httpProxyEnabled = source["httpProxyEnabled"];
this.aiConfigs = this.convertValues(source["aiConfigs"], AIConfig);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
@@ -424,6 +474,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 +504,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 +754,9 @@ export namespace models {
icon: string;
alipay: string;
wxpay: string;
wxgzh: string;
buildTimeStamp: number;
officialStatement: string;
IsDel: number;
static createFrom(source: any = {}) {
@@ -718,7 +774,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"];
}

65
go.mod
View File

@@ -1,26 +1,35 @@
module go-stock
go 1.23.0
go 1.24.0
toolchain go1.24.5
require (
github.com/PuerkitoBio/goquery v1.10.1
github.com/chromedp/chromedp v0.11.2
github.com/cloudwego/eino v0.4.1
github.com/cloudwego/eino-ext/components/model/ark v0.1.19
github.com/cloudwego/eino-ext/components/model/deepseek v0.0.0-20250804092122-8845979a2228
github.com/cloudwego/eino-ext/components/model/openai v0.0.0-20250804092122-8845979a2228
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
github.com/stretchr/testify v1.10.0
github.com/tidwall/gjson v1.14.2
github.com/tidwall/gjson v1.14.4
github.com/wailsapp/wails/v2 v2.10.1
go.uber.org/zap v1.27.0
golang.org/x/sys v0.31.0
golang.org/x/text v0.23.0
golang.org/x/net v0.38.0
golang.org/x/sys v0.35.0
golang.org/x/text v0.26.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gorm.io/gorm v1.25.12
gorm.io/plugin/dbresolver v1.5.3
@@ -28,24 +37,42 @@ 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/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb // indirect
github.com/chromedp/sysutil v1.1.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/cloudwego/eino-ext/libs/acl/openai v0.0.0-20250804062529-6e67726a4b3f // indirect
github.com/cohesion-org/deepseek-go v1.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/esiqveland/notify v0.13.3 // indirect
github.com/evanphx/json-patch v0.5.2 // indirect
github.com/getkin/kin-openapi v0.118.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/goph/emperror v0.17.2 // indirect
github.com/invopop/yaml v0.1.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
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/labstack/echo/v4 v4.13.3 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
@@ -55,26 +82,48 @@ 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/meguminnnnnnnnn/go-openai v0.0.0-20250723112853-3bce976e5ccc // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/nikolalohinski/gonja v1.5.3 // indirect
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/ollama/ollama v0.6.5 // indirect
github.com/openai/openai-go v1.10.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sergeymakinen/go-bmp v1.0.0 // indirect
github.com/sergeymakinen/go-ico v1.0.0-beta.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f // indirect
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/tkrajina/go-reflector v0.5.8 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/vcaesar/cedar v0.20.2 // indirect
github.com/volcengine/volc-sdk-golang v1.0.23 // indirect
github.com/volcengine/volcengine-go-sdk v1.1.21 // indirect
github.com/wailsapp/go-webview2 v1.0.19 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/yargevad/filepathx v1.0.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect
gopkg.in/sourcemap.v1 v1.0.5 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect

275
go.sum
View File

@@ -1,9 +1,27 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
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/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/PuerkitoBio/goquery v1.10.1 h1:Y8JGYUkXWTGRB6Ars3+j3kN0xg1YqqlwvdTV8WTFQcU=
github.com/PuerkitoBio/goquery v1.10.1/go.mod h1:IYiHrOMps66ag56LEH7QYDDupKXyo5A8qrjIx3ZtujY=
github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/bugsnag/bugsnag-go v1.4.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/bytedance/mockey v1.2.14 h1:KZaFgPdiUwW+jOWFieo3Lr7INM1P+6adO3hxZhDswY8=
github.com/bytedance/mockey v1.2.14/go.mod h1:1BPHF9sol5R1ud/+0VEHGQq/+i2lN+GTsr3O2Q9IENY=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb h1:noKVm2SsG4v0Yd0lHNtFYc9EUxIVvrr4kJ6hM8wvIYU=
@@ -12,8 +30,24 @@ github.com/chromedp/chromedp v0.11.2 h1:ZRHTh7DjbNTlfIv3NFTbB7eVeu5XCNkgrpcGSpn2
github.com/chromedp/chromedp v0.11.2/go.mod h1:lr8dFRLKsdTTWb75C/Ttol2vnBKOSnt0BW8R9Xaupi8=
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cloudwego/eino v0.4.1 h1:Jy9KWpCvd+Z75oIynhHsT9dEECUuCW8IPZlVjHgVu9s=
github.com/cloudwego/eino v0.4.1/go.mod h1:wUjz990apdsaOraOXdh6CdhVXq8DJsOvLsVlxNTcNfY=
github.com/cloudwego/eino-ext/components/model/ark v0.1.19 h1:XYnOeszXA28T1gxYOpTIjOjLCPO2gjexK+ShSan9u/8=
github.com/cloudwego/eino-ext/components/model/ark v0.1.19/go.mod h1:VZ7Sa1ocNiSZFiNgg1PQXYdnCJAzPy4Dxt/Ctuwlfp8=
github.com/cloudwego/eino-ext/components/model/deepseek v0.0.0-20250804092122-8845979a2228 h1:YIX5vk2Yx2cOiZHsU3xihVnriOMwNX5NP8g4q18yxf4=
github.com/cloudwego/eino-ext/components/model/deepseek v0.0.0-20250804092122-8845979a2228/go.mod h1:3XV+kHvG6IrVj4WXlquihx8i7a8fUKa09PzuS7IvF2k=
github.com/cloudwego/eino-ext/components/model/openai v0.0.0-20250804092122-8845979a2228 h1:uyWZnjeUxlu4KfCKHKW5Ml22SiD+DvkbgWTBDds4ziE=
github.com/cloudwego/eino-ext/components/model/openai v0.0.0-20250804092122-8845979a2228/go.mod h1:3uBZ/GzJzh1izfY2w62282FZrQG3ISs6T/jTmmPffvE=
github.com/cloudwego/eino-ext/libs/acl/openai v0.0.0-20250804062529-6e67726a4b3f h1:J1tQBg6RDftrtm3vsv+ozlupdlNV+WGslpXiTDr/2xI=
github.com/cloudwego/eino-ext/libs/acl/openai v0.0.0-20250804062529-6e67726a4b3f/go.mod h1:4EBgz8+68n1iuKyWC37Tu9NG1WJkPm+yLxvyLik28Us=
github.com/cohesion-org/deepseek-go v1.3.2 h1:WTZ/2346KFYca+n+DL5p+Ar1RQxF2w/wGkU4jDvyXaQ=
github.com/cohesion-org/deepseek-go v1.3.2/go.mod h1:bOVyKj38r90UEYZFrmJOzJKPxuAh8sIzHOCnLOpiXeI=
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,18 +56,40 @@ 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/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
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/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gen2brain/beeep v0.11.1 h1:EbSIhrQZFDj1K2fzlMpAYlFOzV8YuNe721A58XcCTYI=
github.com/gen2brain/beeep v0.11.1/go.mod h1:jQVvuwnLuwOcdctHn/uyh8horSBNJ8uGb9Cn2W4tvoc=
github.com/getkin/kin-openapi v0.118.0 h1:z43njxPmJ7TaPpMSCQb7PN0dEYno4tyBPQcrFdHoLuM=
github.com/getkin/kin-openapi v0.118.0/go.mod h1:l5e9PaFUo9fyLJCPGQeXI2ML8c3P8BHOEV2VaAVf/pc=
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
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=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
github.com/go-ego/gse v0.80.3 h1:YNFkjMhlhQnUeuoFcUEd1ivh6SOB764rT8GDsEbDiEg=
github.com/go-ego/gse v0.80.3/go.mod h1:Gt3A9Ry1Eso2Kza4MRaiZ7f2DTAvActmETY46Lxg0gU=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg=
github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
@@ -45,21 +101,74 @@ github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakr
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/goph/emperror v0.17.2 h1:yLapQcmEsO0ipe9p5TaN22djm3OFV/TfM/fcYP0/J18=
github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
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/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc=
github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=
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/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
@@ -78,6 +187,8 @@ github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
@@ -91,18 +202,45 @@ 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/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/meguminnnnnnnnn/go-openai v0.0.0-20250723112853-3bce976e5ccc h1:vdRbmKDHZMGb5SSUVAT9u+559Vr2gScV5ie/kcOvfeE=
github.com/meguminnnnnnnnn/go-openai v0.0.0-20250723112853-3bce976e5ccc/go.mod h1:CqSFsV6AkkL2fixd25WYjRAolns+gQrY1x/Cz9c30v8=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
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/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c=
github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/ollama/ollama v0.6.5 h1:vXKkVX57ql/1ZzMw4SVK866Qfd6pjwEcITVyEpF0QXQ=
github.com/ollama/ollama v0.6.5/go.mod h1:pGgtoNyc9DdM6oZI6yMfI6jTk2Eh4c36c2GpfQCH7PY=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/openai/openai-go v1.10.1 h1:7VR8z1foqJDjlaFZsNH5zZIYTWKYz97tdsVSzXDHQck=
github.com/openai/openai-go v1.10.1/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
@@ -113,20 +251,63 @@ github.com/robertkrimen/otto v0.5.1 h1:avDI4ToRk8k1hppLdYFTuuzND41n37vPGJU7547dG
github.com/robertkrimen/otto v0.5.1/go.mod h1:bS433I4Q9p+E5pZLu7r17vP6FkE6/wLxBdmKjoqJXF8=
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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ=
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/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f h1:Z2cODYsUxQPofhpYRMQVwWz4yUVpHF+vPi+eUdruUYI=
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f/go.mod h1:JqzWyvTuI2X4+9wOHmKSQCYxybB/8j6Ko43qVmXDuZg=
github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
@@ -135,34 +316,56 @@ github.com/vcaesar/cedar v0.20.2 h1:TDx7AdZhilKcfE1WvdToTJf5VrC/FXcUOW+KY1upLZ4=
github.com/vcaesar/cedar v0.20.2/go.mod h1:lyuGvALuZZDPNXwpzv/9LyxW+8Y6faN7zauFezNsnik=
github.com/vcaesar/tt v0.20.1 h1:D/jUeeVCNbq3ad8M7hhtB3J9x5RZ6I1n1eZ0BJp7M+4=
github.com/vcaesar/tt v0.20.1/go.mod h1:cH2+AwGAJm19Wa6xvEa+0r+sXDJBT0QgNQey6mwqLeU=
github.com/volcengine/volc-sdk-golang v1.0.23 h1:anOslb2Qp6ywnsbyq9jqR0ljuO63kg9PY+4OehIk5R8=
github.com/volcengine/volc-sdk-golang v1.0.23/go.mod h1:AfG/PZRUkHJ9inETvbjNifTDgut25Wbkm2QoYBTbvyU=
github.com/volcengine/volcengine-go-sdk v1.1.21 h1:HxEaSsT+SRx0J5z5hDi+MVeYK6VRljdTjSjUnBg2Aso=
github.com/volcengine/volcengine-go-sdk v1.1.21/go.mod h1:EyKoi6t6eZxoPNGr2GdFCZti2Skd7MO3eUzx7TtSvNo=
github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU=
github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v2 v2.10.1 h1:QWHvWMXII2nI/nXz77gpPG8P3ehl6zKe+u4su5BWIns=
github.com/wailsapp/wails/v2 v2.10.1/go.mod h1:zrebnFV6MQf9kx8HI4iAv63vsR5v67oS7GTEZ7Pz1TY=
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc=
github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4=
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@@ -175,6 +378,9 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -182,6 +388,9 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -189,6 +398,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -199,8 +409,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -210,6 +420,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -220,24 +432,57 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/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=
@@ -253,6 +498,8 @@ gorm.io/plugin/dbresolver v1.5.3 h1:wFwINGZZmttuu9h7XpvbDHd8Lf9bb8GNzp/NpAMV2wU=
gorm.io/plugin/dbresolver v1.5.3/go.mod h1:TSrVhaUg2DZAWP3PrHlDlITEJmNOkL0tFTjvTEsQ4XE=
gorm.io/plugin/soft_delete v1.2.1 h1:qx9D/c4Xu6w5KT8LviX8DgLcB9hkKl6JC9f44Tj7cGU=
gorm.io/plugin/soft_delete v1.2.1/go.mod h1:Zv7vQctOJTGOsJ/bWgrN1n3od0GBAZgnLjEx+cApLGk=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=

129
main.go
View File

@@ -5,24 +5,22 @@ import (
"embed"
"encoding/json"
"fmt"
"github.com/duke-git/lancet/v2/slice"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/menu/keys"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/mac"
"github.com/wailsapp/wails/v2/pkg/options/windows"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go-stock/backend/data"
"go-stock/backend/db"
log "go-stock/backend/logger"
"go-stock/backend/models"
"os"
goruntime "runtime"
"runtime/debug"
"strings"
"time"
"github.com/duke-git/lancet/v2/slice"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/mac"
"github.com/wailsapp/wails/v2/pkg/options/windows"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
//go:embed frontend/dist
@@ -40,6 +38,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 +54,8 @@ var stocksBinUS []byte
var Version string
var VersionCommit string
var OFFICIAL_STATEMENT string
var BuildKey string
func main() {
checkDir("data")
@@ -67,34 +70,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,31 +107,33 @@ 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
//height = 768
}
darkTheme := data.NewSettingsApi(&data.Settings{}).GetConfig().DarkTheme
darkTheme := data.GetSettingConfig().DarkTheme
backgroundColour := &options.RGBA{R: 255, G: 255, B: 255, A: 1}
if darkTheme {
backgroundColour = &options.RGBA{R: 27, G: 38, B: 54, A: 1}
}
//frameless := getFrameless()
// Create application with options
err = wails.Run(&options.App{
Title: "go-stock",
Title: "go-stockAI赋能股票分析✨",
Width: width * 4 / 5,
Height: 900,
MinWidth: 1456,
MinHeight: 768,
Height: 920,
MinWidth: minWidth,
MinHeight: minHeight,
//MaxWidth: width,
//MaxHeight: height,
DisableResize: false,
Fullscreen: false,
Frameless: true,
Frameless: false,
StartHidden: false,
HideWindowOnClose: false,
EnableDefaultContextMenu: true,
@@ -145,7 +149,7 @@ func main() {
OnShutdown: app.shutdown,
WindowStartState: options.Normal,
SingleInstanceLock: &options.SingleInstanceLock{
UniqueId: "go-stock",
UniqueId: "go-stock-dev",
OnSecondInstanceLaunch: OnSecondInstanceLaunch,
},
Bind: []interface{}{
@@ -162,12 +166,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,
@@ -186,6 +189,26 @@ func main() {
}
func updateMultipleModel() {
oldSettings := &models.OldSettings{}
db.Dao.Model(oldSettings).First(oldSettings)
aiConfig := &data.AIConfig{}
db.Dao.Model(aiConfig).First(aiConfig)
if oldSettings.OpenAiEnable && oldSettings.OpenAiApiKey != "" && aiConfig.ID == 0 {
aiConfig.Name = oldSettings.OpenAiModelName
aiConfig.ApiKey = oldSettings.OpenAiApiKey
aiConfig.BaseUrl = oldSettings.OpenAiBaseUrl
aiConfig.ModelName = oldSettings.OpenAiModelName
aiConfig.Temperature = oldSettings.OpenAiTemperature
aiConfig.MaxTokens = oldSettings.OpenAiMaxTokens
aiConfig.TimeOut = oldSettings.OpenAiApiTimeOut
err := db.Dao.Model(aiConfig).Create(aiConfig).Error
if err != nil {
log.SugaredLogger.Error(err.Error())
}
}
}
func AutoMigrate() {
db.Dao.AutoMigrate(&data.StockInfo{})
db.Dao.AutoMigrate(&data.StockBasic{})
@@ -204,6 +227,9 @@ func AutoMigrate() {
db.Dao.AutoMigrate(&models.Telegraph{})
db.Dao.AutoMigrate(&models.TelegraphTags{})
db.Dao.AutoMigrate(&models.LongTigerRankData{})
db.Dao.AutoMigrate(&data.AIConfig{})
updateMultipleModel()
}
func initStockDataUS(ctx context.Context) {
@@ -260,7 +286,7 @@ func initStockDataHK(ctx context.Context) {
}
func updateBasicInfo() {
config := data.NewSettingsApi(&data.Settings{}).GetConfig()
config := data.GetSettingConfig()
if config.UpdateBasicInfoOnStart {
//更新基本信息
go data.NewStockDataApi().GetStockBaseInfo()
@@ -345,6 +371,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"
}