Compare commits

...

250 Commits

Author SHA1 Message Date
ArvinLovegood
d74ad3c03d refactor(frontend):升级go到1.25版本(性能更强劲!)
- 从 components.d.ts 文件中删除了未使用的 TChat、TChatAction、TChatContent、TChatLoading 和 TChatSender导入
- 更新 go.mod 文件,移除 toolchain go1.24.5 并将 Go 版本升级到 1.25.0
2025-08-26 11:45:03 +08:00
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
ArvinLovegood
3e16574faa docs(README): 更新重大功能开发计划
- 新增股票分析知识库功能,状态为施工中
- 新增Ai智能选股功能,状态为施工中,计划在下半年重点开发
2025-06-30 16:51:07 +08:00
ArvinLovegood
482472af4e feat(frontend):添加指标选股功能
- 在 App.vue 中添加指标选股相关路由和菜单项
- 新增 SelectStock 组件实现选股功能
- 在 backend 中调整搜索股票接口的分页参数
2025-06-30 16:27:15 +08:00
ArvinLovegood
bdc3689ac8 fix(backend):修复雪球热门股票接口
- 新增请求获取 cookies
- 使用 cookies 进行后续请求
- 优化请求头设置
2025-06-30 11:07:14 +08:00
ArvinLovegood
e8ebb577b2 test: 更新测试代码并优化日志输出
- 在 market_news_api.go 中更新了 XUEQIUHotStock 的日志输出
- 在 search_stock_api.go 中注释掉了日志输出语句
- 修改了 search_stock_api_test.go 中的测试用例和日志输出格式
2025-06-30 10:45:41 +08:00
sparkmemory
71f8265bc2 feat(app): 添加股票搜索功能并优化测试用例
- 在 App 结构中添加 SearchStock 方法,用于股票搜索
- 更新测试用例,增加对搜索结果 columns 的打印
- 使用分号分隔多个搜索条件,提高搜索灵活性
2025-06-29 18:11:52 +08:00
ArvinLovegood
43063fa7fb feat(data): 添加搜索股票 API功能
- 实现了搜索股票 API 的请求和解析功能
- 添加了搜索股票的测试用例
2025-06-29 17:31:29 +08:00
ArvinLovegood
86f041b4d6 feat(frontend):添加财经日历和重大事件时间轴功能
- 在 App.d.ts 和 App.js 中添加了 ClsCalendar 和 InvestCalendarTimeLine 函数
- 在 app_common.go 中实现了对应的后端逻辑
- 新增了 InvestCalendarTimeLine 和 ClsCalendarTimeLine组件用于展示数据
- 更新了 market.vue 中的 tabs,添加了新功能的页面
2025-06-27 17:46:50 +08:00
ArvinLovegood
0ce7e8e7a7 docs(README):添加优云智算平台信息
- 在 README.md 中添加优云智算平台信息,提供免费 GPU 资源和海量源项目镜像
2025-06-26 13:25:15 +08:00
ArvinLovegood
bbab60e2ad docs(README):添加优云智算平台信息
- 在 README.md 中添加优云智算平台信息,提供免费 GPU 资源和海量源项目镜像
- 在 stock_data_api.go 中增加关注股票数量的限制,最多只能关注 63 只股票
2025-06-26 13:19:07 +08:00
ArvinLovegood
1fbd564bff build(frontend):更新Node.js版本并迁移图标库
- 将 Node.js 版本从 18.x 升级到 20.x
- 从 package.json 中移除 @vicons/ionicons5 依赖
- 在 devDependencies 中添加多个 @vicons 开头的图标库
- 更新 package-lock.json 和相关文件以反映这些更改
2025-06-25 14:10:26 +08:00
ArvinLovegood
f0ad50303e feat(frontend):优化热门股票和话题组件
- 更新热门股票列表,增加更多图标和数据字段
- 改进热门话题组件,添加点击事件和额外信息展示
- 调整股票搜索功能,使用居中弹窗打开链接
- 更新 App.vue 中的图标和菜单项
- 修改后端 HotStock 函数,增加返回数据量
2025-06-25 13:52:31 +08:00
ArvinLovegood
55839d3329 feat(frontend):优化热门股票和话题组件
- 更新热门股票列表,增加更多图标和数据字段
- 改进热门话题组件,添加点击事件和额外信息展示
- 调整股票搜索功能,使用居中弹窗打开链接
- 更新 App.vue 中的图标和菜单项
- 修改后端 HotStock 函数,增加返回数据量
2025-06-25 13:37:55 +08:00
ArvinLovegood
3f4cbca4a7 docs(README):添加热门股票、事件和话题功能,更新开发者列表
- 在 README.md 文件的更新日志中添加了 2025.06.25 的更新内容- 新增了热门股票、事件和话题功能
2025-06-25 10:34:57 +08:00
SparkMemory
6e3b9ff1f9 fix(stock): 修复昨天因为美股逻辑导致A股关注错误(CodeNoobLH/dev)
fix(stock): 修复昨天因为美股逻辑导致A股关注错误
2025-06-25 10:25:47 +08:00
浓睡不消残酒
0e45866421 fix(stock): 优化股票代码处理逻辑
- 在关注股票时,仅当股票代码以 "us" 开头时,才将其转换为 "gb_" 前缀的格式
2025-06-25 10:21:40 +08:00
ArvinLovegood
e0225c4158 docs(README): 添加热门股票、事件和话题功能
- 在 README.md 文件的更新日志中添加了 2025.06.25 的更新内容- 新增了热门股票、事件和话题功能
2025-06-25 09:43:04 +08:00
ArvinLovegood
2f6c17fb2a feat(frontend):添加热门股票、事件和话题功能
- 在 App.d.ts 和 App.js 中添加了 HotEvent、HotStock 和 HotTopic 函数
- 在 app_common.go 中实现了相关功能的后端逻辑
- 新增 HotEvents、HotStockList 和 HotTopics 组件用于前端展示
- 更新 market.vue以包含新的热门股票和话题功能
- 在 KLineChart.vue 中添加了代码和名称的显示
2025-06-25 09:41:16 +08:00
ArvinLovegood
22b4fcdffb Merge remote-tracking branch 'origin/dev' into dev 2025-06-25 09:40:35 +08:00
ArvinLovegood
7dd10d443e feat(frontend):添加热门股票、事件和话题功能
- 在 App.d.ts 和 App.js 中添加了 HotEvent、HotStock 和 HotTopic 函数
- 在 app_common.go 中实现了相关功能的后端逻辑
- 新增 HotEvents、HotStockList 和 HotTopics 组件用于前端展示
- 更新 market.vue以包含新的热门股票和话题功能
- 在 KLineChart.vue 中添加了代码和名称的显示
2025-06-25 09:40:04 +08:00
SparkMemory
5b5590ebd7 Merge pull request #81 from CodeNoobLH/master
修复美股展示和排序问题
2025-06-24 18:26:08 +08:00
浓睡不消残酒
be02343d68 修复前端关注美股后不会展示的问题
修复前端美股默认排序靠前问题
修复后端美股无法排序问题
2025-06-24 18:11:28 +08:00
SparkMemory
942d249671 Merge pull request #80 from CodeNoobLH/master
修复股票排序后,前端股票数量异常问题
2025-06-23 18:26:12 +08:00
浓睡不消残酒
9f2719cdbc 修改排序前端代码 2025-06-23 18:09:43 +08:00
浓睡不消残酒
0343a95a21 Merge branch 'master' of https://github.com/CodeNoobLH/go-stock 2025-06-23 16:42:22 +08:00
浓睡不消残酒
9337084ebf 修改排序后端代码 2025-06-23 16:37:20 +08:00
浓睡不消残酒
18834d9281 Merge branch 'ArvinLovegood:master' into master 2025-06-23 16:19:48 +08:00
SparkMemory
9e06136983 Merge pull request #79 from ArvinLovegood/dev
移除jieba依赖
2025-06-21 15:27:56 +08:00
ArvinLovegood
30a3d1d9ef Merge remote-tracking branch 'origin/dev' into dev 2025-06-21 15:21:54 +08:00
ArvinLovegood
5b6de9f9f6 build(deps): 从 go.mod 中移除 github.com/yanyiwu/gojieba
移除了 go.mod 和 go.sum 文件中不再使用的 github.com/yanyiwu/gojieba 依赖。
2025-06-21 15:21:35 +08:00
SparkMemory
65d737c695 Merge pull request #78 from ArvinLovegood/dev
合并
2025-06-21 15:19:30 +08:00
SparkMemory
af73691b22 Merge pull request #77 from ArvinLovegood/master
合并稳定版
2025-06-21 15:17:38 +08:00
ArvinLovegood
b2c12cffbb feat(backend):使用gse替代gojieba进行分词
- 移除 gojieba 依赖,减少二进制文件大小
- 添加 gse 依赖,支持更高效的分词处理
- 更新 splitWords 函数,使用 gse 进行中英文分词
- 在包初始化时加载 gse 默认词典
2025-06-21 13:49:30 +08:00
ArvinLovegood
a936dc6371 feat(frontend):个股卡片中添加按钮,可以直接跳转到个股研报和公司公告页面,查询对应个股的研报或公告
- 在 market.vue 中添加个股研报和公司公告组件
- 在 stock.vue 中增加研报和公告的搜索功能
- 修改 StockNoticeList 和 StockResearchReportList 组件,支持接收 stockCode 参数
- 在 backend 中添加 TradingView 新闻 API 接口
2025-06-20 18:43:11 +08:00
ArvinLovegood
f6d217e4fd feat(analyze): 添加情感分析功能并优化新闻推送通知
- 在 App.vue 中添加情感分析相关的导入和使用
- 在 app_common.go 中实现 AnalyzeSentiment 方法- 在 market_news_api.go 和 models.go 中集成情感分析结果
- 更新前端通知显示,根据情感分析结果调整通知类型和样式
- 在 go.mod 中添加 gojieba 依赖用于情感分析
2025-06-20 11:33:38 +08:00
ArvinLovegood
378b669827 feat(market):添加行业研究功能
- 在 App.vue 中添加行业研究选项
- 在 market.vue 中实现行业研究页面布局
- 新增 IndustryResearchReportList 组件用于显示行业研究列表
- 在 app_common.go 中添加相关 API 接口
- 在 market_news_api.go 中实现行业研究数据获取逻辑
- 更新 README.md,添加行业研究功能说明
2025-06-18 18:34:15 +08:00
ArvinLovegood
0d3fd47552 feat(market):添加行业研究功能
- 在 App.vue 中添加行业研究选项
- 在 market.vue 中实现行业研究页面布局
- 新增 IndustryResearchReportList 组件用于显示行业研究列表
- 在 app_common.go 中添加相关 API 接口
- 在 market_news_api.go 中实现行业研究数据获取逻辑
- 更新 README.md,添加行业研究功能说明
2025-06-18 18:33:20 +08:00
ArvinLovegood
a2fee361e7 feat(frontend):实时市场资讯信息提醒功能
- 新增 NewsPush 函数用于推送市场资讯
- 在 App.vue 中添加新闻推送的事件监听
- 在 settings 中增加启用新闻推送的选项
- 修改 README.md,添加实时市场资讯信息提醒的更新说明
2025-06-18 14:23:32 +08:00
ArvinLovegood
1ef950b961 docs: 注释掉 README 中的 GitCode 星星徽章
- 在 README.md 文件中,将 GitCode 的星星徽章图片链接用注释标记包围
-这样做可能是为了暂时移除或隐藏该徽章,而不直接删除代码行
2025-06-18 10:29:39 +08:00
ArvinLovegood
934b4608b7 docs(README): 更新内置股票基础数据
- 在 README.md 文件中的更新日志部分添加了新的更新记录
- 新增了"2025.06.18 更新内置股票基础数据"的更新记录条目
2025-06-18 10:11:54 +08:00
ArvinLovegood
68e7b6a68c refactor(data):更新内置股票基础数据
- 更新内置股票基础数据
2025-06-18 09:54:15 +08:00
ArvinLovegood
700572567e refactor(backend):移除市场新闻 API 的来源参数
- 将 NewMarketNewsApi().GetNewsList("新浪财经", 100) 调用中的来源参数修改为空字符串
- 此修改可能会影响市场新闻的获取结果,但具体影响需要进一步测试
2025-06-17 15:54:49 +08:00
ArvinLovegood
c9ade36844 refactor(frontend):重构龙虎榜功能
- 将龙虎榜相关代码从 market.vue 中抽离,创建独立的 LongTigerRankList 组件
-优化龙虎榜数据获取逻辑,增加对历史数据的递归查询
- 改进用户界面,保留原有的筛选和排序功能
- 删除 market.vue 中的冗余代码,提高代码可读性和维护性
2025-06-17 14:08:17 +08:00
ArvinLovegood
0a2491d725 feat(frontend):为股票公告列表添加走势图和资金趋势图
- 在 StockNoticeList 组件中添加 KLineChart 和 MoneyTrend 组件
- 实现股票代码和名称的悬停显示功能- 添加资金趋势图和 K线图的渲染
- 优化股票代码显示格式
2025-06-17 09:49:43 +08:00
ArvinLovegood
9d8af191c5 docs(README):更新公司公告信息搜索/查看功能
- 在更新日志中添加了"2025.06.15 添加公司公告信息搜索/查看功能"的记录
- 此更新增加了对公司公告信息进行搜索和查看的功能,进一步丰富了应用的资讯获取渠道
2025-06-16 18:16:20 +08:00
ArvinLovegood
6382be6b19 ci: 更新 GitHub Actions 触发条件
- 移除对 master 分支的监听
- 取消注释并启用对 '*-release' 标签的监听
2025-06-16 17:54:25 +08:00
SparkMemory
0cafcb9cd4 feat(market):添加公司公告功能
feat(market):添加公司公告功能

- 在市场页面添加公司公告选项卡
- 实现公司公告数据接口和组件
- 优化市场页面布局和功能
2025-06-16 17:42:34 +08:00
ArvinLovegood
21c7f5390c feat(market):添加公司公告功能
- 在市场页面添加公司公告选项卡
- 实现公司公告数据接口和组件
- 优化市场页面布局和功能
2025-06-16 17:40:53 +08:00
ArvinLovegood
02db6c2e87 feat(market):添加公司公告功能
- 在市场页面添加公司公告选项卡
- 实现公司公告数据接口和组件
- 优化市场页面布局和功能
2025-06-16 17:40:35 +08:00
ArvinLovegood
2811786bfd ci: 注释掉 tag 触发条件
- 注释掉了 GitHub Actions 工作流中的 tags配置
- 这将阻止任何新标签触发该工作流- 可能是为了控制工作流的触发条件,避免不必要的自动构建
2025-06-16 15:02:26 +08:00
SparkMemory
9aa2c4095a feat(个股研报):增加个股研报搜索功能
feat(研报):增加个股研报搜索功能
2025-06-16 15:01:21 +08:00
ArvinLovegood
ad9bea4c24 feat(研报):增加个股研报搜索功能
- 修改 App.d.ts 和 App.js,为 StockResearchReport 函数添加股票代码参数
- 更新 app_common.go,将 StockResearchReport 方法改为接收股票代码参数
- 修改 market_news_api.go,实现根据股票代码查询研报的逻辑
- 更新 market_news_api_test.go,添加针对具体股票代码的测试用例
- 在前端 StockResearchReportList 组件中增加股票代码搜索功能
2025-06-16 14:45:59 +08:00
SparkMemory
4f8d84b8a0 Merge pull request #72 from ArvinLovegood/dev
ci:优化 GitHub Actions 工作流触发条件
2025-06-16 13:20:50 +08:00
ArvinLovegood
e238700333 ci:优化 GitHub Actions 工作流触发条件
- 添加 master 分支的 push 事件触发
- 保留标签触发条件
2025-06-16 13:18:59 +08:00
浓睡不消残酒
6bdff0a0f3 Merge remote-tracking branch 'origin/master' 2025-06-16 10:24:37 +08:00
浓睡不消残酒
c7655d2adf refactor(backend): 优化股票排序功能
- 重构了 SetStockSort 函数,增加了事务处理和错误处理
- 添加了对新排序位置是否被占用的检查
- 实现了向前和向后移动排序时对其他记录的影响
- 优化了数据库查询和更新操作,提高了代码的健壮性和性能
2025-06-16 10:17:17 +08:00
ArvinLovegood
8996ddf986 build:更新前端依赖
- 更新了 frontend/package.json.md5 文件- 可能涉及前端项目的依赖更新或调整
2025-06-15 17:57:03 +08:00
SparkMemory
329936568f Merge pull request #71 from ArvinLovegood/dependabot/npm_and_yarn/frontend/multi-a91bf2f4f6
build(deps): bump esbuild and vite in /frontend
2025-06-15 17:42:34 +08:00
dependabot[bot]
0d85e24595 build(deps): bump esbuild and vite in /frontend
Bumps [esbuild](https://github.com/evanw/esbuild) to 0.25.5 and updates ancestor dependency [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite). These dependencies need to be updated together.


Updates `esbuild` from 0.21.5 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2024.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.21.5...v0.25.5)

Updates `vite` from 5.4.19 to 6.3.5
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.5/packages/vite)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.25.5
  dependency-type: indirect
- dependency-name: vite
  dependency-version: 6.3.5
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-15 09:41:49 +00:00
SparkMemory
b266281bbd Merge pull request #70 from ArvinLovegood/dependabot/npm_and_yarn/frontend/vite-5.4.19
build(deps-dev): bump vite from 5.4.14 to 5.4.19 in /frontend
2025-06-15 17:40:07 +08:00
dependabot[bot]
ace3ff7302 build(deps-dev): bump vite from 5.4.14 to 5.4.19 in /frontend
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.14 to 5.4.19.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.19/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.19/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 5.4.19
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-15 09:37:17 +00:00
SparkMemory
60b7cdc761 Merge pull request #69 from ArvinLovegood/dependabot/go_modules/golang.org/x/net-0.38.0
build(deps): bump golang.org/x/net from 0.35.0 to 0.38.0
2025-06-15 17:34:19 +08:00
dependabot[bot]
3cc597d361 build(deps): bump golang.org/x/net from 0.35.0 to 0.38.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.35.0 to 0.38.0.
- [Commits](https://github.com/golang/net/compare/v0.35.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.38.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-15 09:33:55 +00:00
SparkMemory
68bcfc679a Merge pull request #68 from ArvinLovegood/dependabot/go_modules/golang.org/x/crypto-0.35.0
build(deps): bump golang.org/x/crypto from 0.33.0 to 0.35.0
2025-06-15 17:32:23 +08:00
dependabot[bot]
78f7808f1b build(deps): bump golang.org/x/crypto from 0.33.0 to 0.35.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.33.0 to 0.35.0.
- [Commits](https://github.com/golang/crypto/compare/v0.33.0...v0.35.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.35.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-15 09:13:25 +00:00
ArvinLovegood
d6c3a6b98b feat(frontend):添加个股研报到弹出菜单
- 在市场标签列表中新增了个股研报标签
- 设置了个股研报的路由和点击事件处理
- 使用了 RouterLink 组件实现导航
2025-06-15 17:06:03 +08:00
SparkMemory
3b25aa79bb Merge pull request #67 from CodeNoobLH/master
修复了关注股票后点击成本没有效果的bug
2025-06-13 21:46:07 +08:00
浓睡不消残酒
e49545a581 Merge branch 'ArvinLovegood:master' into master 2025-06-13 18:29:30 +08:00
浓睡不消残酒
1185af5a87 feat(frontend): 更新关注列表并优化关注功能
- 修复了关注后不能点击成本的bug
2025-06-13 17:00:54 +08:00
ArvinLovegood
152a6335d8 feat(frontend):添加个股研报到弹出菜单
- 在市场标签列表中新增了个股研报标签
- 设置了个股研报的路由和点击事件处理
- 使用了 RouterLink 组件实现导航
2025-06-13 17:00:04 +08:00
ArvinLovegood
338e371190 feat(frontend):添加个股研报功能
- 在前端新增 StockResearchReportList 组件,用于显示个股研报列表
- 在后端新增 StockResearchReport 接口和实现,获取个股研报数据- 在 App.d.ts 和 App.js 中添加相关函数声明和实现- 在 market.vue 中集成新增的个股研报功能
2025-06-13 15:48:01 +08:00
ArvinLovegood
3ffcaa0374 feat(frontend):添加个股研报功能
- 在前端新增 StockResearchReportList 组件,用于显示个股研报列表
- 在后端新增 StockResearchReport 接口和实现,获取个股研报数据- 在 App.d.ts 和 App.js 中添加相关函数声明和实现- 在 market.vue 中集成新增的个股研报功能
2025-06-13 15:37:41 +08:00
ArvinLovegood
ed9d9cde77 docs(README): 更新 QQ 交流群描述
- 在 README.md 文件中更新了 QQ 交流群的描述信息
- 说明群聊已满,但会定期清理,随缘入群
2025-06-13 13:36:44 +08:00
ArvinLovegood
673d446b05 feat(market):增加龙虎榜上榜原因筛选功能并优化数据处理
- 在前端市场组件中添加龙虎榜上榜原因筛选功能
- 实现后台数据存储优化,避免重复插入相同数据
- 为 LongTigerRankData 模型添加索引,提高查询效率
2025-06-12 17:32:58 +08:00
ArvinLovegood
e2e0ef2aad docs(README): 更新龙虎榜功能和近期优化
- 添加龙虎榜功能,新增行业排名分类
- 优化股票分时图显示
- 修复财联社电报获取问题
- 优化资金趋势图表组件
- 重构应用加载和数据初始化逻辑
- 添加股票资金趋势功能,增加主力当日净流入数据并优化展示效果- 添加个股资金流向功能
- 排行榜增加股票行情K线图弹窗
- 添加行业排名功能
- 优化分时图的展示
- 补全港股/美股基础数据,优化港股股价延迟问题和初始化逻辑
2025-06-12 15:47:29 +08:00
ArvinLovegood
a8ecbf9329 feat(frontend):添加龙虎榜功能
- 在前端 App.vue 中添加龙虎榜相关路由和图标
- 实现龙虎榜数据获取和展示功能
- 添加龙虎榜数据模型和 API 接口
- 更新后端 MarketNewsApi 类,增加 LongTiger 方法获取龙虎榜数据
2025-06-12 15:38:42 +08:00
ArvinLovegood
9eded54d8d refactor(frontend):优化股票分时图显示
- 调整股票价格显示范围,增加百分比浮动
- 在模态框标题中添加股票涨跌百分比
2025-05-30 11:31:51 +08:00
ArvinLovegood
c1d458e5cf refactor(frontend):优化股票分时图显示
- 调整股票价格显示范围,增加百分比浮动
- 在模态框标题中添加股票涨跌百分比
2025-05-30 11:25:17 +08:00
ArvinLovegood
7158e405a6 refactor(frontend):优化股票分时图显示
- 调整股票价格显示范围,增加百分比浮动
- 在模态框标题中添加股票涨跌百分比
2025-05-30 10:56:42 +08:00
ArvinLovegood
d993a5525f feat(market): 添加证监会行业资金排名(净流入)板块
- 在市场组件中增加了一个新的标签页"证监会行业资金排名(净流入)"
- 使用 industryMoneyRank组件来展示该排名,传入不同的分类参数 fenlei='2'- 保持与其他行业排名相同的展示逻辑和样式
2025-05-27 14:45:38 +08:00
ArvinLovegood
6af6d989ba feat(frontend):新增行业资金排名功能
- 在市场页面添加行业资金排名和概念板块资金排名两个新标签页
- 实现行业和概念板块的资金流向数据展示
- 新增 industryMoneyRank组件用于显示资金排名数据
- 更新后端 API 接口,支持按不同排序方式获取行业资金排名数据
2025-05-27 14:39:26 +08:00
ArvinLovegood
0b3acd9adc ci: 更新 GitHub Actions 工作流触发条件
- 将标签匹配模式从 '*' 改为 '*-release'
- 仅匹配以 '-release' 结尾的标签,限制发布次数
2025-05-21 10:15:17 +08:00
ArvinLovegood
013de869f4 feat(backend):新增 top stocks 排行榜功能并更新相关模块
- 在 MarketNewsApi 中添加 TopStocksRankingList 方法,实现 top stocks 排行榜数据的获取和解析
- 更新 App.vue 中的 content 文本,增加未经授权禁止商业用途的声明- 在 market_news_api_test.go 中添加 TopStocksRankingList 的测试用例
2025-05-21 09:59:38 +08:00
ArvinLovegood
1b67e20932 refactor(backend/data):修复财联社电报获取问题
- 修改 market_news_api.go 中的 GoQuery 选择器,从 ".telegraph-list"改为 ".telegraph-content-box"- 更新 openai_api_test.go 中的 TestGetTopNewsList 函数,增加测试日志输出
2025-05-20 10:46:30 +08:00
ArvinLovegood
8b510bce94 refactor(frontend):优化资金趋势图表组件
- 优化图表展示效果,增加累计净流入和股价的显示
- 调整图表样式,增加暗黑主题支持
- 优化数据处理逻辑,提高图表准确性
-调整模态框样式,移除不必要的属性
2025-05-16 17:26:34 +08:00
ArvinLovegood
71676eead4 feat(moneyTrend):资金趋势图表增加主力当日净流入数据并优化展示效果
- 在资金趋势图表中添加主力当日净流入数据
- 优化图表颜色和样式,增加最大值和最小值标记
- 添加平均值参考线
- 调整轴线样式,提高可读性
- 后端接口增加数据天数至360天
2025-05-15 21:28:59 +08:00
ArvinLovegood
2a274db7ae feat(frontend):添加股票资金趋势功能
- 在前端添加了股票资金趋势页面组件
- 在后端实现了获取股票资金趋势数据的接口
- 优化了前端界面布局,增加了资金趋势按钮
2025-05-15 18:36:53 +08:00
ArvinLovegood
4fd5cbf8e6 refactor(app):重构应用加载和数据初始化逻辑(小白福音)
- 在 domReady 函数中添加股票数据初始化逻辑
- 更新前端 App.vue以显示加载信息
- 修改后端 initStockData 函数,添加上下文和加载消息
- 优化市场数据定时刷新逻辑
- 修复 AI 响应结果获取方式
2025-05-15 14:29:08 +08:00
ArvinLovegood
d7b17b2561 refactor(app):重构应用加载和数据初始化逻辑(小白福音)
- 在 domReady 函数中添加股票数据初始化逻辑
- 更新前端 App.vue以显示加载信息
- 修改后端 initStockData 函数,添加上下文和加载消息
- 优化市场数据定时刷新逻辑
- 修复 AI 响应结果获取方式
2025-05-15 14:13:42 +08:00
ArvinLovegood
ad92c41d08 feat(rankTable):排行榜增加股票行情K线图弹窗
- 在排名表格中,将股票名称单元格改为可触发 popover 的按钮
- 在 popover 中显示股票的 K 线图
- 引入 KLineChart 组件用于渲染 K线图
- 优化表格展示效果,调整涨跌幅和成交额的显示方式
2025-05-14 15:29:06 +08:00
ArvinLovegood
47dbbb8813 feat(frontend):添加个股资金流向功能
- 在 App.vue 中添加个股资金流向相关路由和菜单项
- 新增 RankTable 组件用于展示排名数据
- 在 market.vue 中集成 RankTable 组件,实现资金流向排名展示
- 在后端添加 GetIndustryMoneyRankSina 和 GetMoneyRankSina接口
- 更新前端 App.d.ts、App.js 和后端 app.go 以支持新功能
2025-05-14 12:04:32 +08:00
ArvinLovegood
ae9f4073dc feat(market):添加行业排名功能
- 在市场行情模块中增加行业排名标签页
- 实现行业排名数据的获取和展示- 添加行业排名相关的图标和交互
- 优化市场行情模块的结构和样式
2025-05-13 23:11:36 +08:00
ArvinLovegood
c7e37e039e feat(frontend):添加股票分组菜单功能并优化路由
- 在 App.vue 中添加股票分组列表,动态生成分组选项
- 更新路由配置,使用 createWebHistory替代 createWebHashHistory
- 在 stock.vue 中添加分组切换逻辑,支持通过路由和事件切换分组
2025-05-09 23:43:51 +08:00
ArvinLovegood
99b6586c77 feat(stock):添加A股盘口数据解析和展示功能
- 在 stock.vue 中添加盘口数据展示组件
- 在 stock_data_api.go 中增加 A 股盘口数据解析逻辑
- 优化数据库自动迁移逻辑,提取到单独的函数中
- 更新测试用例以覆盖新的盘口数据解析功能
2025-05-09 11:52:52 +08:00
ArvinLovegood
7e24424ea0 feat(stock):添加A股盘口数据解析和展示功能
- 在 stock.vue 中添加盘口数据展示组件
- 在 stock_data_api.go 中增加 A 股盘口数据解析逻辑
- 优化数据库自动迁移逻辑,提取到单独的函数中
- 更新测试用例以覆盖新的盘口数据解析功能
2025-05-09 11:44:13 +08:00
ArvinLovegood
58d93c76f6 feat(stock):优化分时图展示效果
- 重新设计分时图布局和样式,增加更多图表元素
- 添加开盘价、收盘价等关键信息显示
- 实现分时图自动刷新功能
- 优化模态框样式,调整图表尺寸
- 重构相关函数,提高代码可维护性
2025-05-08 18:31:20 +08:00
ArvinLovegood
df989b706b docs(README): 更新分时图展示优化及版本日志
- 优化分时图的展示效果
- 在更新日志中添加 2025.05.07 版本的改动说明
2025-05-07 16:42:43 +08:00
ArvinLovegood
cf537ca695 feat(stock):优化股票分时图表展示
- 新增 GetStockMinutePriceLineData 函数获取股票分时数据
- 在前端实现分时数据图表展示
- 后端增加 GetStockMinutePriceData接口获取分时数据
- 更新数据库模型,添加 MinuteData 结构体
2025-05-07 16:17:33 +08:00
ArvinLovegood
11a1a47eca feat(data):补全港股/美股基础数据,优化初始化逻辑
- 美股和港股数据初始化时增加总数检查,避免重复插入
- 优化数据插入逻辑,减少不必要的查询操作
-港股数据初始化逻辑调整,解决数据延迟问题
2025-04-29 15:34:06 +08:00
ArvinLovegood
338064e536 feat(backend/data):添加腾讯股票数据接口支持
- 新增腾讯股票数据接口 URL
- 实现腾讯股票数据解析逻辑,支持港股和 A 股
- 更新 GetStockCodeRealTimeData 方法,支持腾讯股票数据
- 添加腾讯股票数据解析单元测试
2025-04-29 15:06:21 +08:00
ArvinLovegood
8ba26b6250 feat(data):补全港股和美股基础信息
- 新增了从多个数据源获取港股和美股信息的方法
- 实现了对获取数据的解析和存储
- 添加了相关的测试函数
2025-04-29 10:52:00 +08:00
ArvinLovegood
54138ff61e feat(frontend):添加市场资讯手动刷新功能
- 在 App.d.ts 中添加 ReFleshTelegraphList 函数声明
- 在 app.go 中实现 ReFleshTelegraphList 方法,用于刷新电报列表- 在 App.js 中添加 ReFleshTelegraphList 函数的前端调用接口
- 在 market.vue 中添加 ReFlesh 函数,用于调用刷新接口并更新数据
- 在 newsList.vue 中添加刷新按钮和相关事件处理,支持手动刷新功能
2025-04-28 12:17:27 +08:00
ArvinLovegood
d8d5091709 docs: 更新项目名称描述
- 将"基于大预言模型的AI赋能股票分析工具"修改为"基于大语言模型的AI赋能股票分析工具"
- 此更新更准确地描述了项目的功能和目标
2025-04-27 14:59:59 +08:00
ArvinLovegood
7f204ee80d docs: 更新项目 README
- 修改项目标题为"基于大预言模型的AI赋能股票分析工具"
- 移除了对 Wails 和 NaiveUI 的提及
2025-04-27 14:58:38 +08:00
ArvinLovegood
4a367b6027 docs(README): 更新 QQ 交流群链接
- 添加新的 QQ交流群 2 链接
- 保留原 QQ 交流群链接,并注明已满
- 优化群链接格式,提高可读性
2025-04-27 14:33:36 +08:00
ArvinLovegood
e615fc4108 docs(README): 更新 QQ 交流群链接
- 添加新的 QQ交流群 2 链接
- 保留原 QQ 交流群链接,并注明已满
- 优化群链接格式,提高可读性
2025-04-27 14:33:06 +08:00
ArvinLovegood
2b982f924e feat(market):添加VIX恐慌指数
- 在市场组件中新增 VIX 恐慌指数选项卡
- 使用代码 usUVXY.AM 获取 VIX 恐慌指数数据
- 设置图表显示参数,包括面板高度、名称、K 线天数和暗黑主题
2025-04-27 14:21:20 +08:00
ArvinLovegood
24e24f8236 refactor(frontend):修正美洲地区名称翻译
- 将美国地区的翻译从 "美国" 修改为 "美洲"
- 此修改提高了地区名称的准确性和一致性
2025-04-27 10:49:12 +08:00
ArvinLovegood
e77c23e42a docs(README): 更新市场资讯支持AI分析和总结
- 新增AI分析和总结功能,让AI帮助用户读取市场信息
- 添加相关截图展示新功能
- 调整现有内容格式,优化列表符号使用
2025-04-25 18:14:21 +08:00
ArvinLovegood
ef6228922e ci:更新Go版本并修复市场资讯保存功能
-将 Go 版本从 1.23 升级到1.24
- 修复市场资讯保存功能,更新 SaveAsMarkdown 调用参数
2025-04-25 17:55:24 +08:00
ArvinLovegood
c4caea5be8 feat(frontend):添加AI市场资讯总结功能
- 在市场组件中增加 AI 总结按钮和模态框
- 实现 SummaryStockNews 函数用于获取 AI 总结
- 添加 GetNewsList 方法获取市场新闻列表
- 优化市场资讯的展示和交互
2025-04-25 17:03:52 +08:00
ArvinLovegood
3535ba57ab feat(frontend):添加AI市场资讯总结功能
- 在市场组件中增加 AI 总结按钮和模态框
- 实现 SummaryStockNews 函数用于获取 AI 总结
- 添加 GetNewsList 方法获取市场新闻列表
- 优化市场资讯的展示和交互
2025-04-25 16:33:14 +08:00
ArvinLovegood
cedff896bb refactor(frontend):调整K线图标记点样式
- 注释掉 KLineChart.vue 中的 markPoint.symbol属性,以修改标记点的显示方式
- 在 main.go 中:
- 移除初始化股票数据的冗余注释代码 - 保留获取屏幕分辨率的代码并修复缩进
  -调整应用程序窗口的最小高度
2025-04-24 22:19:58 +08:00
ArvinLovegood
ffc212abc3 feat(README): 新增市场行情模块说明和截图
- 在更新日志和重大更新部分添加市场行情模块的相关信息
- 插入市场行情模块的截图链接
- 优化README内容,增强项目展示效果
2025-04-24 17:50:55 +08:00
ArvinLovegood
7bacbe0d89 feat(frontend):重构市场资讯页面并添加全球股指功能
- 重构市场资讯页面布局,增加多个新闻源和全球股指信息- 新增 GetStockCommonKLine、GetTelegraphList 和 GlobalStockIndexes 等接口
- 实现全球股指数据的获取和展示
- 优化市场资讯的获取和更新逻辑
- 调整 K 线图组件的参数和样式
2025-04-24 17:30:54 +08:00
ArvinLovegood
6be23d6abc feat(frontend):添加市场资讯功能
- 新增市场资讯页面,用于展示财经新闻
- 实现电报列表获取和实时更新功能
- 添加新闻标签和股票标签显示
- 优化新闻列表展示样式
2025-04-23 16:39:54 +08:00
ArvinLovegood
3a74e0ed98 refactor(frontend):优化股票K线组件的样式和功能
-移除了多余的 console.log 语句
- 调整了图表的样式,包括颜色、背景等
- 优化了鼠标悬停时的提示信息显示
-调整了均线的透明度
- 优化了分组列表的加载逻辑
2025-04-22 18:03:20 +08:00
ArvinLovegood
4b0b3c0491 refactor(frontend):调整K线成交量颜色
- 调整了 stock.vue 文件中 dimension 属性的 pieces 数组顺序
- 将下降颜色(downColor)对应的值改为 -1,上升颜色(upColor)对应的值改为 1
2025-04-22 13:04:45 +08:00
ArvinLovegood
2bd63cf2f4 feat(frontend):优化股票K线图功能
- 在 App.d.ts 中添加 GetStockKLine 函数声明
- 在 app.go 中实现 GetStockKLine 方法- 在 App.js 中添加 GetStockKLine 函数导出
- 在 package.json 中添加 echarts依赖
- 在 stock.vue 中实现 K 线图展示功能- 优化 K 线图数据处理和图表配置
2025-04-22 11:56:35 +08:00
ArvinLovegood
71d8822d15 feat(frontend):优化股票K线图功能
- 在 App.d.ts 中添加 GetStockKLine 函数声明
- 在 app.go 中实现 GetStockKLine 方法- 在 App.js 中添加 GetStockKLine 函数导出
- 在 package.json 中添加 echarts依赖
- 在 stock.vue 中实现 K 线图展示功能- 优化 K 线图数据处理和图表配置
2025-04-22 11:43:31 +08:00
ArvinLovegood
db3594af77 feat(stock):支持港股和美股的K线数据获取
- 修改了 openai_api.go 中的股票代码处理逻辑,增加了对港股和美股的支持
- 新增了 StockDataApi 类中的 GetHK_KLineData 方法,用于获取港股和美股的 K 线数据
- 更新了前端 stock.vue 组件的样式
-增加了 GetHK_KLineData 方法的单元测试
2025-04-21 18:39:20 +08:00
ArvinLovegood
c7d728e613 refactor(backend):修正K线数据接口参数
- 将 URL 中的 `days` 参数改为 `datalen` 参数- 适应 Sina API 的变更,确保正确获取 K 线数据
2025-04-18 18:22:45 +08:00
Lovegood
3ca2eed575 Merge pull request #65 from Exisfar/exisfar
更正了 未开盘时今日盈亏 计算存在的问题
2025-04-13 18:30:09 +08:00
Exisfar
8cd55034c3 更正了 未开盘时今日盈亏 计算存在的问题 2025-04-12 23:07:08 +08:00
ArvinLovegood
344c43cbf1 ci:移除 windows/arm64 平台的构建
- 删除了 GitHub Actions 工作流中针对 windows/arm64 平台的构建任务
- 保留了 windows/amd64 平台的构建
- 注释掉了 linux/amd64 平台的构建
2025-04-10 18:03:21 +08:00
ArvinLovegood
8c49b00057 ci:添加windows/arm64平台
- 在 GitHub Actions 工作流程中增加了 windows/arm64 平台的生成任务
- 新增 go-stock-windows-arm64.exe 可执行文件
2025-04-10 17:56:04 +08:00
ArvinLovegood
51cc21107a feat(data):从雪球接口获取财务数据并优化表格解析
- 新增 GetFinancialReportsByXUEQIU 函数,用于从雪球获取财务报告
- 优化 GetTableMarkdown 函数,改进表格解析逻辑
- 更新测试用例,验证新接口的正确性- 重构原有 GetFinancialReports函数,提高代码可维护性
2025-04-08 17:09:48 +08:00
ArvinLovegood
ece40d1fc0 feat(data): 添加雪球接口获取财务数据并优化表格解析
- 新增 GetFinancialReportsByXUEQIU 函数,用于从雪球获取财务报告
- 优化 GetTableMarkdown 函数,改进表格解析逻辑
- 更新测试用例,验证新接口的正确性- 重构原有 GetFinancialReports函数,提高代码可维护性
2025-04-08 17:06:10 +08:00
ArvinLovegood
1a3c8b4fae feat(config):添加基金功能启用配置
- 在配置文件中增加 enableFund 字段,用于控制是否启用基金功能
- 根据配置决定是否启动基金监控和数据获取任务
- 更新前端界面,在设置页面添加基金功能启用开关
- 优化代码结构,提高可维护性和可读性
2025-04-07 14:03:17 +08:00
ArvinLovegood
09d3a16841 feat(config):添加基金功能启用配置
- 在配置文件中增加 enableFund 字段,用于控制是否启用基金功能
- 根据配置决定是否启动基金监控和数据获取任务
- 更新前端界面,在设置页面添加基金功能启用开关
- 优化代码结构,提高可维护性和可读性
2025-04-07 13:44:47 +08:00
ArvinLovegood
65bc8cde47 fix(stock):修复股票代码错误导致的重复问题
- 在前端 stock 组件中增加了对股票代码是否已存在于 stocks.value 中的检查
- 如果股票代码不存在,则删除对应的 result.key
- 在后端 app.go 中增加了对股票价格变化的判断,只有在价格变化时才发送事件
2025-04-07 12:25:40 +08:00
103 changed files with 217265 additions and 28876 deletions

View File

@@ -4,11 +4,14 @@ on:
push:
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.23'
go-version: '1.25'
build-tags: ${{ github.ref_name }}
build-commit-message: ${{ steps.get_commit_message.outputs.commit_message }}
node-version: '18.x'
build-statement: ${{ env.OFFICIAL_STATEMENT }}
build-key: ${{ env.BUILD_KEY }}
node-version: '20.x'

View File

@@ -1,16 +1,18 @@
# go-stock : 基于Wails和NaiveUI构建的AI赋能股票分析工具
# go-stock : 基于大语言模型的AI赋能股票分析工具
## ![go-stock](./build/appicon.png)
![GitHub Release](https://img.shields.io/github/v/release/ArvinLovegood/go-stock?link=https%3A%2F%2Fgithub.com%2FArvinLovegood%2Fgo-stock%2Freleases&link=https%3A%2F%2Fgithub.com%2FArvinLovegood%2Fgo-stock%2Freleases)
[![GitHub Repo stars](https://img.shields.io/github/stars/ArvinLovegood/go-stock?link=https%3A%2F%2Fgithub.com%2FArvinLovegood%2Fgo-stock)](https://github.com/ArvinLovegood/go-stock)
[![star](https://gitee.com/arvinlovegood_admin/go-stock/badge/star.svg?theme=dark)](https://gitee.com/arvinlovegood_admin/go-stock)
[![star](https://gitcode.com/ArvinLovegood/go-stock/star/badge.svg)](https://gitcode.com/ArvinLovegood/go-stock)
[//]: # ([![star]&#40;https://gitcode.com/ArvinLovegood/go-stock/star/badge.svg&#41;]&#40;https://gitcode.com/ArvinLovegood/go-stock&#41;)
### 🌟公众号
![扫码_搜索联合传播样式-白色版.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交流群[点击链接加入群聊【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大模型构建的股票分析工具。
@@ -22,37 +24,78 @@ QQ交流群[点击链接加入群聊【go-stock交流群】491605333](http
### 📦 立即体验
- 安装版:[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) |
| 模型 | 状态 | 备注 |
| --- | --- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [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>💕
- 经测试目前硅基流动(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广告推广💖) |
## 🧩 重大功能开发计划
| 功能说明 | 状态 | 备注 |
|-----------------|----|----------------------------------------------------------------------------------------------------------|
| 股票分析知识库 | 🚧 | 未来计划 |
| Ai智能选股 | 🚧 | Ai智能选股功能开发中(下半年重点开发计划) |
| ETF支持 | 🚧 | ETF数据支持 (目前可以查看净值和估值) |
| 美股支持 | ✅ | 美股数据支持 |
| 港股支持 | ✅ | 港股数据支持 (目前有延迟) |
| 美股支持 | ✅ | 美股数据支持 |
| 港股支持 | ✅ | 港股数据支持 |
| 多轮对话 | ✅ | AI分析后可继续对话提问 |
| 自定义AI分析提问模板 | ✅ | 可配置的提问模板 [v2025.2.12.7-alpha](https://github.com/ArvinLovegood/go-stock/releases/tag/v2025.2.12.7-alpha) |
| 不再强制依赖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 添加热门股票、事件和话题功能
### 2025.06.18 更新内置股票基础数据,软件内实时市场资讯信息提醒,添加行业研究功能
### 2025.06.15 添加公司公告信息搜索/查看功能
### 2025.06.15 添加个股研报到弹出菜单
### 2025.06.13 添加个股研报功能
### 2025.06.12 添加龙虎榜功能,新增行业排名分类
### 2025.05.30 优化股票分时图显示
### 2025.05.20 修复财联社电报获取问题
### 2025.05.16 优化资金趋势图表组件
### 2025.05.15 重构应用加载和数据初始化逻辑,添加股票资金趋势功能,资金趋势图表增加主力当日净流入数据并优化展示效果
### 2025.05.14 添加个股资金流向功能排行榜增加股票行情K线图弹窗
### 2025.05.13 添加行业排名功能
### 2025.05.09 添加A股盘口数据解析和展示功能
### 2025.05.07 优化分时图的展示
### 2025.04.29 补全港股/美股基础数据,优化港股股价延迟问题,优化初始化逻辑
### 2025.04.25 市场资讯支持AI分析和总结让AI帮你读市场
### 2025.04.24 新增市场行情模块:即时掌握全球市场行情资讯/动态从此再也不用偷摸去各大财经网站啦。go-stock一键帮你搞定
### 2025.04.22 优化K线图展示支持拉伸放大看得更舒服啦
### 2025.04.21 港股美股K线数据获取优化
### 2025.04.01 优化部分设置选项,避免重启软件
### 2025.03.31 优化数据爬取
### 2025.03.30 AI自动定时分析功能
@@ -75,6 +118,12 @@ QQ交流群[点击链接加入群聊【go-stock交流群】491605333](http
## 🦄 重大更新
### BIG NEWS !!! 重大更新!!!
- 2025.04.25 市场资讯支持AI分析和总结让AI帮你读市场
![img.png](img.png)
- 2025.04.24 新增市场行情模块:即时掌握全球市场行情资讯/动态从此再也不用偷摸去各大财经网站啦。go-stock一键帮你搞定
![img.png](build/screenshot/img13.png)
![img_13.png](build/screenshot/img_13.png)
- ![img_14.png](build/screenshot/img_14.png)
- 2025.01.17 新增AI大模型分析股票功能
![img_5.png](build/screenshot/img.png)
## 📸 功能截图
@@ -84,7 +133,7 @@ QQ交流群[点击链接加入群聊【go-stock交流群】491605333](http
### 成本设置
![img.png](build/screenshot/img_7.png)
### 日K
![img_2.png](build/screenshot/img_8.png)
![img_12.png](build/screenshot/img_12.png)
### 分时
![img_3.png](build/screenshot/img_9.png)
### 钉钉报警通知

934
app.go

File diff suppressed because it is too large Load Diff

73
app_common.go Normal file
View File

@@ -0,0 +1,73 @@
package main
import (
"github.com/wailsapp/wails/v2/pkg/runtime"
"go-stock/backend/agent"
"go-stock/backend/data"
"go-stock/backend/models"
)
// @Author spark
// @Date 2025/6/8 20:45
// @Desc
//-----------------------------------------------------------------------------------
func (a *App) LongTigerRank(date string) *[]models.LongTigerRankData {
return data.NewMarketNewsApi().LongTiger(date)
}
func (a *App) StockResearchReport(stockCode string) []any {
return data.NewMarketNewsApi().StockResearchReport(stockCode, 7)
}
func (a *App) StockNotice(stockCode string) []any {
return data.NewMarketNewsApi().StockNotice(stockCode)
}
func (a *App) IndustryResearchReport(industryCode string) []any {
return data.NewMarketNewsApi().IndustryResearchReport(industryCode, 7)
}
func (a App) EMDictCode(code string) []any {
return data.NewMarketNewsApi().EMDictCode(code, a.cache)
}
func (a App) AnalyzeSentiment(text string) data.SentimentResult {
return data.AnalyzeSentiment(text)
}
func (a App) HotStock(marketType string) *[]models.HotItem {
return data.NewMarketNewsApi().XUEQIUHotStock(100, marketType)
}
func (a App) HotEvent(size int) *[]models.HotEvent {
if size <= 0 {
size = 10
}
return data.NewMarketNewsApi().HotEvent(size)
}
func (a App) HotTopic(size int) []any {
if size <= 0 {
size = 10
}
return data.NewMarketNewsApi().HotTopic(size)
}
func (a App) InvestCalendarTimeLine(yearMonth string) []any {
return data.NewMarketNewsApi().InvestCalendar(yearMonth)
}
func (a App) ClsCalendar() []any {
return data.NewMarketNewsApi().ClsCalendar()
}
func (a App) SearchStock(words string) map[string]any {
return data.NewSearchStockApi(words).SearchStock(5000)
}
func (a App) GetHotStrategy() map[string]any {
return data.NewSearchStockApi("").HotStrategy()
}
func (a App) ChatWithAgent(question string, aiConfigId int, sysPromptId *int) {
ch := agent.NewStockAiAgentApi().Chat(question, aiConfigId, sysPromptId)
for msg := range ch {
runtime.EventsEmit(a.ctx, "agent-message", msg)
}
}

View File

@@ -6,302 +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)
stockData.ProfitAmountToday = mathutil.RoundToFloat((preClosePrice-preClosePrice)*float64(follow.Volume), 2)
}
}
//logger.SugaredLogger.Debugf("stockData:%+v", stockData)
if follow.Price != price && price > 0 {
go db.Dao.Model(follow).Where("stock_code = ?", follow.StockCode).Updates(map[string]interface{}{
"price": price,
})
}
}
// beforeClose is called when the application is about to quit,
// either by clicking the window close button or calling runtime.Quit.
// Returning true will cause the application to continue, false will continue shutdown as normal.
// 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: "取消",
})
@@ -310,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

@@ -0,0 +1,940 @@
package data
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"
"github.com/duke-git/lancet/v2/strutil"
"github.com/go-resty/resty/v2"
"github.com/robertkrimen/otto"
"github.com/samber/lo"
"github.com/tidwall/gjson"
)
// @Author spark
// @Date 2025/4/23 14:54
// @Desc
// -----------------------------------------------------------------------------------
type MarketNewsApi struct {
}
func NewMarketNewsApi() *MarketNewsApi {
return &MarketNewsApi{}
}
func (m MarketNewsApi) GetNewTelegraph(crawlTimeOut int64) *[]models.Telegraph {
url := "https://www.cls.cn/telegraph"
response, _ := resty.New().SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
SetHeader("Referer", "https://www.cls.cn/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get(url)
var telegraphs []models.Telegraph
//logger.SugaredLogger.Info(string(response.Body()))
document, _ := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
document.Find(".telegraph-content-box").Each(func(i int, selection *goquery.Selection) {
//logger.SugaredLogger.Info(selection.Text())
telegraph := models.Telegraph{Source: "财联社电报"}
spans := selection.Find("div.telegraph-content-box span")
if spans.Length() == 2 {
telegraph.Time = spans.First().Text()
telegraph.Content = spans.Last().Text()
if spans.Last().HasClass("c-de0422") {
telegraph.IsRed = true
}
}
labels := selection.Find("div a.label-item")
labels.Each(func(i int, selection *goquery.Selection) {
if selection.HasClass("link-label-item") {
telegraph.Url = selection.AttrOr("href", "")
} else {
tag := &models.Tags{
Name: selection.Text(),
Type: "subject",
}
db.Dao.Model(tag).Where("name=? and type=?", selection.Text(), "subject").FirstOrCreate(&tag)
telegraph.SubjectTags = append(telegraph.SubjectTags, selection.Text())
}
})
stocks := selection.Find("div.telegraph-stock-plate-box a")
stocks.Each(func(i int, selection *goquery.Selection) {
telegraph.StocksTags = append(telegraph.StocksTags, selection.Text())
})
//telegraph = append(telegraph, ReplaceSensitiveWords(selection.Text()))
if telegraph.Content != "" {
telegraph.SentimentResult = AnalyzeSentiment(telegraph.Content).Description
cnt := int64(0)
db.Dao.Model(telegraph).Where("time=? and source=?", telegraph.Time, telegraph.Source).Count(&cnt)
if cnt == 0 {
db.Dao.Create(&telegraph)
telegraphs = append(telegraphs, telegraph)
for _, tag := range telegraph.SubjectTags {
tagInfo := &models.Tags{}
db.Dao.Model(models.Tags{}).Where("name=? and type=?", tag, "subject").First(&tagInfo)
if tagInfo.ID > 0 {
db.Dao.Model(models.TelegraphTags{}).Where("telegraph_id=? and tag_id=?", telegraph.ID, tagInfo.ID).FirstOrCreate(&models.TelegraphTags{
TelegraphId: telegraph.ID,
TagId: tagInfo.ID,
})
}
}
}
}
})
return &telegraphs
}
func (m MarketNewsApi) GetNewsList(source string, limit int) *[]*models.Telegraph {
news := &[]*models.Telegraph{}
if source != "" {
db.Dao.Model(news).Preload("TelegraphTags").Where("source=?", source).Order("id desc").Limit(limit).Find(news)
} else {
db.Dao.Model(news).Preload("TelegraphTags").Order("id desc").Limit(limit).Find(news)
}
for _, item := range *news {
tags := &[]models.Tags{}
db.Dao.Model(&models.Tags{}).Where("id in ?", lo.Map(item.TelegraphTags, func(item models.TelegraphTags, index int) uint {
return item.TagId
})).Find(&tags)
tagNames := lo.Map(*tags, func(item models.Tags, index int) string {
return item.Name
})
item.SubjectTags = tagNames
logger.SugaredLogger.Infof("tagNames %v SubjectTags%s", tagNames, item.SubjectTags)
}
return news
}
func (m MarketNewsApi) GetTelegraphList(source string) *[]*models.Telegraph {
news := &[]*models.Telegraph{}
if source != "" {
db.Dao.Model(news).Preload("TelegraphTags").Where("source=?", source).Order("id desc").Limit(20).Find(news)
} else {
db.Dao.Model(news).Preload("TelegraphTags").Order("id desc").Limit(20).Find(news)
}
for _, item := range *news {
tags := &[]models.Tags{}
db.Dao.Model(&models.Tags{}).Where("id in ?", lo.Map(item.TelegraphTags, func(item models.TelegraphTags, index int) uint {
return item.TagId
})).Find(&tags)
tagNames := lo.Map(*tags, func(item models.Tags, index int) string {
return item.Name
})
item.SubjectTags = tagNames
logger.SugaredLogger.Infof("tagNames %v SubjectTags%s", tagNames, item.SubjectTags)
}
return news
}
func (m MarketNewsApi) GetSinaNews(crawlTimeOut uint) *[]models.Telegraph {
news := &[]models.Telegraph{}
response, _ := resty.New().SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
SetHeader("Referer", "https://finance.sina.com.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("https://zhibo.sina.com.cn/api/zhibo/feed?callback=callback&page=1&page_size=20&zhibo_id=152&tag_id=0&dire=f&dpc=1&pagesize=20&id=4161089&type=0&_=" + strconv.FormatInt(time.Now().Unix(), 10))
js := string(response.Body())
js = strutil.ReplaceWithMap(js, map[string]string{
"try{callback(": "var data=",
");}catch(e){};": ";",
})
//logger.SugaredLogger.Info(js)
vm := otto.New()
_, err := vm.Run(js)
if err != nil {
logger.SugaredLogger.Error(err)
}
vm.Run("var result = data.result;")
//vm.Run("var resultStr =JSON.stringify(data);")
vm.Run("var resultData = result.data;")
vm.Run("var feed = resultData.feed;")
vm.Run("var feedStr = JSON.stringify(feed);")
value, _ := vm.Get("feedStr")
//resultStr, _ := vm.Get("resultStr")
//logger.SugaredLogger.Info(resultStr)
feed := make(map[string]any)
err = json.Unmarshal([]byte(value.String()), &feed)
if err != nil {
logger.SugaredLogger.Errorf("json.Unmarshal error:%v", err.Error())
}
var telegraphs []models.Telegraph
if feed["list"] != nil {
for _, item := range feed["list"].([]any) {
telegraph := models.Telegraph{Source: "新浪财经"}
data := item.(map[string]any)
//logger.SugaredLogger.Infof("%s:%s", data["create_time"], data["rich_text"])
telegraph.Content = data["rich_text"].(string)
telegraph.Time = strings.Split(data["create_time"].(string), " ")[1]
tags := data["tag"].([]any)
telegraph.SubjectTags = lo.Map(tags, func(tagItem any, index int) string {
name := tagItem.(map[string]any)["name"].(string)
tag := &models.Tags{
Name: name,
Type: "sina_subject",
}
db.Dao.Model(tag).Where("name=? and type=?", name, "sina_subject").FirstOrCreate(&tag)
return name
})
if _, ok := lo.Find(telegraph.SubjectTags, func(item string) bool { return item == "焦点" }); ok {
telegraph.IsRed = true
}
logger.SugaredLogger.Infof("telegraph.SubjectTags:%v %s", telegraph.SubjectTags, telegraph.Content)
if telegraph.Content != "" {
telegraph.SentimentResult = AnalyzeSentiment(telegraph.Content).Description
cnt := int64(0)
db.Dao.Model(telegraph).Where("time=? and source=?", telegraph.Time, telegraph.Source).Count(&cnt)
if cnt == 0 {
db.Dao.Create(&telegraph)
telegraphs = append(telegraphs, telegraph)
for _, tag := range telegraph.SubjectTags {
tagInfo := &models.Tags{}
db.Dao.Model(models.Tags{}).Where("name=? and type=?", tag, "sina_subject").First(&tagInfo)
if tagInfo.ID > 0 {
db.Dao.Model(models.TelegraphTags{}).Where("telegraph_id=? and tag_id=?", telegraph.ID, tagInfo.ID).FirstOrCreate(&models.TelegraphTags{
TelegraphId: telegraph.ID,
TagId: tagInfo.ID,
})
}
}
}
}
}
return &telegraphs
}
return news
}
func (m MarketNewsApi) GlobalStockIndexes(crawlTimeOut uint) map[string]any {
response, _ := resty.New().SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
SetHeader("Referer", "https://stockapp.finance.qq.com/mstats").
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("https://proxy.finance.qq.com/ifzqgtimg/appstock/app/rank/indexRankDetail2")
js := string(response.Body())
res := make(map[string]any)
json.Unmarshal([]byte(js), &res)
return res["data"].(map[string]any)
}
func (m MarketNewsApi) GetIndustryRank(sort string, cnt int) map[string]any {
url := fmt.Sprintf("https://proxy.finance.qq.com/ifzqgtimg/appstock/app/mktHs/rank?l=%d&p=1&t=01/averatio&ordertype=&o=%s", cnt, sort)
response, _ := resty.New().SetTimeout(time.Duration(5)*time.Second).R().
SetHeader("Referer", "https://stockapp.finance.qq.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get(url)
js := string(response.Body())
res := make(map[string]any)
json.Unmarshal([]byte(js), &res)
return res
}
func (m MarketNewsApi) GetIndustryMoneyRankSina(fenlei, sort string) []map[string]any {
url := fmt.Sprintf("https://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/MoneyFlow.ssl_bkzj_bk?page=1&num=20&sort=%s&asc=0&fenlei=%s", sort, fenlei)
response, _ := resty.New().SetTimeout(time.Duration(5)*time.Second).R().
SetHeader("Host", "vip.stock.finance.sina.com.cn").
SetHeader("Referer", "https://finance.sina.com.cn").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get(url)
js := string(response.Body())
res := &[]map[string]any{}
err := json.Unmarshal([]byte(js), &res)
if err != nil {
logger.SugaredLogger.Error(err)
return *res
}
return *res
}
func (m MarketNewsApi) GetMoneyRankSina(sort string) []map[string]any {
if sort == "" {
sort = "netamount"
}
url := fmt.Sprintf("https://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/MoneyFlow.ssl_bkzj_ssggzj?page=1&num=20&sort=%s&asc=0&bankuai=&shichang=", sort)
response, _ := resty.New().SetTimeout(time.Duration(5)*time.Second).R().
SetHeader("Host", "vip.stock.finance.sina.com.cn").
SetHeader("Referer", "https://finance.sina.com.cn").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get(url)
js := string(response.Body())
res := &[]map[string]any{}
err := json.Unmarshal([]byte(js), &res)
if err != nil {
logger.SugaredLogger.Error(err)
return *res
}
return *res
}
func (m MarketNewsApi) GetStockMoneyTrendByDay(stockCode string, days int) []map[string]any {
url := fmt.Sprintf("http://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/MoneyFlow.ssl_qsfx_zjlrqs?page=1&num=%d&sort=opendate&asc=0&daima=%s", days, stockCode)
response, _ := resty.New().SetTimeout(time.Duration(5)*time.Second).R().
SetHeader("Host", "vip.stock.finance.sina.com.cn").
SetHeader("Referer", "https://finance.sina.com.cn").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").Get(url)
js := string(response.Body())
res := &[]map[string]any{}
err := json.Unmarshal([]byte(js), &res)
if err != nil {
logger.SugaredLogger.Error(err)
return *res
}
return *res
}
func (m MarketNewsApi) TopStocksRankingList(date string) {
url := fmt.Sprintf("http://vip.stock.finance.sina.com.cn/q/go.php/vInvestConsult/kind/lhb/index.phtml?tradedate=%s", date)
response, _ := resty.New().SetTimeout(time.Duration(5)*time.Second).R().
SetHeader("Host", "vip.stock.finance.sina.com.cn").
SetHeader("Referer", "https://finance.sina.com.cn").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").Get(url)
html, _ := convertor.GbkToUtf8(response.Body())
//logger.SugaredLogger.Infof("html:%s", html)
document, err := goquery.NewDocumentFromReader(bytes.NewReader(html))
if err != nil {
return
}
document.Find("table.list_table").Each(func(i int, s *goquery.Selection) {
title := strutil.Trim(s.Find("tr:first-child").First().Text())
logger.SugaredLogger.Infof("title:%s", title)
s.Find("tr:not(:first-child)").Each(func(i int, s *goquery.Selection) {
logger.SugaredLogger.Infof("s:%s", strutil.RemoveNonPrintable(s.Text()))
})
})
}
func (m MarketNewsApi) LongTiger(date string) *[]models.LongTigerRankData {
ranks := &[]models.LongTigerRankData{}
url := "https://datacenter-web.eastmoney.com/api/data/v1/get"
logger.SugaredLogger.Infof("url:%s", url)
params := make(map[string]string)
params["callback"] = "callback"
params["sortColumns"] = "TURNOVERRATE,TRADE_DATE,SECURITY_CODE"
params["sortTypes"] = "-1,-1,1"
params["pageSize"] = "500"
params["pageNumber"] = "1"
params["reportName"] = "RPT_DAILYBILLBOARD_DETAILSNEW"
params["columns"] = "SECURITY_CODE,SECUCODE,SECURITY_NAME_ABBR,TRADE_DATE,EXPLAIN,CLOSE_PRICE,CHANGE_RATE,BILLBOARD_NET_AMT,BILLBOARD_BUY_AMT,BILLBOARD_SELL_AMT,BILLBOARD_DEAL_AMT,ACCUM_AMOUNT,DEAL_NET_RATIO,DEAL_AMOUNT_RATIO,TURNOVERRATE,FREE_MARKET_CAP,EXPLANATION,D1_CLOSE_ADJCHRATE,D2_CLOSE_ADJCHRATE,D5_CLOSE_ADJCHRATE,D10_CLOSE_ADJCHRATE,SECURITY_TYPE_CODE"
params["source"] = "WEB"
params["client"] = "WEB"
params["filter"] = fmt.Sprintf("(TRADE_DATE<='%s')(TRADE_DATE>='%s')", date, date)
resp, err := resty.New().SetTimeout(time.Duration(15)*time.Second).R().
SetHeader("Host", "datacenter-web.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/stock/tradedetail.html").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetQueryParams(params).
Get(url)
if err != nil {
return ranks
}
js := string(resp.Body())
logger.SugaredLogger.Infof("resp:%s", js)
js = strutil.ReplaceWithMap(js, map[string]string{
"callback(": "var data=",
");": ";",
})
//logger.SugaredLogger.Info(js)
vm := otto.New()
_, err = vm.Run(js)
_, err = vm.Run("var data = JSON.stringify(data);")
value, err := vm.Get("data")
logger.SugaredLogger.Infof("resp-json:%s", value.String())
data := gjson.Get(value.String(), "result.data")
logger.SugaredLogger.Infof("resp:%v", data)
err = json.Unmarshal([]byte(data.String()), ranks)
if err != nil {
logger.SugaredLogger.Error(err)
return ranks
}
for _, rankData := range *ranks {
temp := &models.LongTigerRankData{}
db.Dao.Model(temp).Where(&models.LongTigerRankData{
TRADEDATE: rankData.TRADEDATE,
SECUCODE: rankData.SECUCODE,
}).First(temp)
if temp.SECURITYTYPECODE == "" {
db.Dao.Model(temp).Create(&rankData)
}
}
return ranks
}
func (m MarketNewsApi) IndustryResearchReport(industryCode string, days int) []any {
beginDate := time.Now().Add(-time.Duration(days) * 24 * time.Hour).Format("2006-01-02")
endDate := time.Now().Format("2006-01-02")
if strutil.Trim(industryCode) != "" {
beginDate = time.Now().Add(-time.Duration(days) * 365 * time.Hour).Format("2006-01-02")
}
logger.SugaredLogger.Infof("IndustryResearchReport-name:%s", industryCode)
params := map[string]string{
"industry": "*",
"industryCode": industryCode,
"beginTime": beginDate,
"endTime": endDate,
"pageNo": "1",
"pageSize": "50",
"p": "1",
"pageNum": "1",
"pageNumber": "1",
"qType": "1",
}
url := "https://reportapi.eastmoney.com/report/list"
logger.SugaredLogger.Infof("beginDate:%s endDate:%s", beginDate, endDate)
resp, err := resty.New().SetTimeout(time.Duration(15)*time.Second).R().
SetHeader("Host", "reportapi.eastmoney.com").
SetHeader("Origin", "https://data.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/report/stock.jshtml").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetHeader("Content-Type", "application/json").
SetQueryParams(params).Get(url)
respMap := map[string]any{}
if err != nil {
return []any{}
}
json.Unmarshal(resp.Body(), &respMap)
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
return respMap["data"].([]any)
}
func (m MarketNewsApi) StockResearchReport(stockCode string, days int) []any {
beginDate := time.Now().Add(-time.Duration(days) * 24 * time.Hour).Format("2006-01-02")
endDate := time.Now().Format("2006-01-02")
if strutil.ContainsAny(stockCode, []string{"."}) {
stockCode = strings.Split(stockCode, ".")[0]
beginDate = time.Now().Add(-time.Duration(days) * 365 * time.Hour).Format("2006-01-02")
} else {
stockCode = strutil.ReplaceWithMap(stockCode, map[string]string{
"sh": "",
"sz": "",
"gb_": "",
"us": "",
"us_": "",
})
beginDate = time.Now().Add(-time.Duration(days) * 365 * time.Hour).Format("2006-01-02")
}
logger.SugaredLogger.Infof("StockResearchReport-stockCode:%s", stockCode)
type Req struct {
BeginTime string `json:"beginTime"`
EndTime string `json:"endTime"`
IndustryCode string `json:"industryCode"`
RatingChange string `json:"ratingChange"`
Rating string `json:"rating"`
OrgCode interface{} `json:"orgCode"`
Code string `json:"code"`
Rcode string `json:"rcode"`
PageSize int `json:"pageSize"`
PageNo int `json:"pageNo"`
P int `json:"p"`
PageNum int `json:"pageNum"`
PageNumber int `json:"pageNumber"`
}
url := "https://reportapi.eastmoney.com/report/list2"
logger.SugaredLogger.Infof("beginDate:%s endDate:%s", beginDate, endDate)
resp, err := resty.New().SetTimeout(time.Duration(15)*time.Second).R().
SetHeader("Host", "reportapi.eastmoney.com").
SetHeader("Origin", "https://data.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/report/stock.jshtml").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetHeader("Content-Type", "application/json").
SetBody(&Req{
Code: stockCode,
IndustryCode: "*",
BeginTime: beginDate,
EndTime: endDate,
PageNo: 1,
PageSize: 50,
P: 1,
PageNum: 1,
PageNumber: 1,
}).Post(url)
respMap := map[string]any{}
if err != nil {
return []any{}
}
json.Unmarshal(resp.Body(), &respMap)
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
return respMap["data"].([]any)
}
func (m MarketNewsApi) StockNotice(stock_list string) []any {
var stockCodes []string
for _, stockCode := range strings.Split(stock_list, ",") {
if strutil.ContainsAny(stockCode, []string{"."}) {
stockCode = strings.Split(stockCode, ".")[0]
stockCodes = append(stockCodes, stockCode)
} else {
stockCode = strutil.ReplaceWithMap(stockCode, map[string]string{
"sh": "",
"sz": "",
"gb_": "",
"us": "",
"us_": "",
})
stockCodes = append(stockCodes, stockCode)
}
}
url := "https://np-anotice-stock.eastmoney.com/api/security/ann?page_size=50&page_index=1&ann_type=SHA%2CCYB%2CSZA%2CBJA%2CINV&client_source=web&f_node=0&stock_list=" + strings.Join(stockCodes, ",")
resp, err := resty.New().SetTimeout(time.Duration(15)*time.Second).R().
SetHeader("Host", "np-anotice-stock.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/notices/hsa/5.html").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
Get(url)
respMap := map[string]any{}
if err != nil {
return []any{}
}
json.Unmarshal(resp.Body(), &respMap)
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
return (respMap["data"].(map[string]any))["list"].([]any)
}
func (m MarketNewsApi) EMDictCode(code string, cache *freecache.Cache) []any {
respMap := map[string]any{}
d, _ := cache.Get([]byte(code))
if d != nil {
json.Unmarshal(d, &respMap)
return respMap["data"].([]any)
}
url := "https://reportapi.eastmoney.com/report/bk"
params := map[string]string{
"bkCode": code,
}
resp, err := resty.New().SetTimeout(time.Duration(15)*time.Second).R().
SetHeader("Host", "reportapi.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").
SetHeader("Content-Type", "application/json").
SetQueryParams(params).Get(url)
if err != nil {
return []any{}
}
json.Unmarshal(resp.Body(), &respMap)
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
cache.Set([]byte(code), resp.Body(), 60*60*24)
return respMap["data"].([]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 := 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/").
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("TradingViewNews err:%s", err.Error())
return TVNews
}
respMap := map[string]any{}
err = json.Unmarshal(resp.Body(), &respMap)
if err != nil {
return TVNews
}
items, err := json.Marshal(respMap["items"])
if err != nil {
return TVNews
}
json.Unmarshal(items, TVNews)
return TVNews
}
func (m MarketNewsApi) XUEQIUHotStock(size int, marketType string) *[]models.HotItem {
request := resty.New().SetTimeout(time.Duration(30) * time.Second).R()
_, err := request.
SetHeader("Host", "xueqiu.com").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
Get("https://xueqiu.com/hq#hot")
//cookies := resp.Header().Get("Set-Cookie")
//logger.SugaredLogger.Infof("cookies:%s", cookies)
url := fmt.Sprintf("https://stock.xueqiu.com/v5/stock/hot_stock/list.json?page=1&size=%d&_type=%s&type=%s", size, marketType, marketType)
res := &models.XUEQIUHot{}
_, err = request.
SetHeader("Host", "stock.xueqiu.com").
SetHeader("Origin", "https://xueqiu.com").
SetHeader("Referer", "https://xueqiu.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
//SetHeader("Cookie", "cookiesu=871730774144180; device_id=ee75cebba8a35005c9e7baf7b7dead59; s=ch12b12pfi; Hm_lvt_1db88642e346389874251b5a1eded6e3=1746247619; xq_a_token=361dcfccb1d32a1d9b5b65f1a188b9c9ed1e687d; xqat=361dcfccb1d32a1d9b5b65f1a188b9c9ed1e687d; xq_r_token=450d1db0db9659a6af7cc9297bfa4fccf1776fae; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOi0xLCJpc3MiOiJ1YyIsImV4cCI6MTc1MzgzODAwNiwiY3RtIjoxNzUxMjUxMzc2MDY3LCJjaWQiOiJkOWQwbjRBWnVwIn0.TjEtQ5WEN4ajnVjVnY3J-Qq9LjL-F0eat9Cefv_tLJLqsPhzD2y8Lc1CeIu0Ceqhlad7O_yW1tR9nb2dIjDpyOPzWKxvwSOKXLm8XMoz4LMgE2pysBCH4TsetzHsEOhBsY467q-JX3WoFuqo-dqv1FfLSondZCspjEMFdgPFt2V-2iXJY05YUwcBVUvL74mT9ZjNq0KaDeRBJk_il6UR8yibG7RMbe9xWYz5dSO_wJwWuxvnZ8u9EXC2m-TV7-QHVxFHR_5e8Fodrzg0yIcLU4wBTSoIIQDUKqngajX2W-nUAdo6fr78NNDmoswFVH7T7XMuQciMAqj9MpMCVW3Sog; u=871730774144180; ssxmod_itna=iq+h7KAImDORKYQ4Y5G=nxBKDtD7D3qCD0dGMDxeq7tDRDFqApKDHtA68oon7ziBA0+PbZ9xGN4oYxiNDAPq0iDC+Wjxs9Orw5KQb9iqP4MAn0TbNsbtU22eqbCe=S3vTv6xoDHxY=DU1GzeieDx=PD5xDTDWeDGDD3DmnsDi5YD0KDjBYpH+omDYPDEBYDaxDbDimwY4GCrDDCtc5Dw6bmzDDzznL5WWAPzWffZg3YcFgxf8GwD7y3Dla4rMhw23=cz0Efdk0A5hYDXotDvhoY1/H6neEvOt3o=Q0ruT+5RuxoRhDxCmh5tGP32xBD5G0xS2xcb4quDK0Dy2ZmY/DDWM0qmEeSEDeOCIq1fw1misCY=WAzoOtMwDzGdUjpRk5Z0xQBDI2IMw4H7qNiNBLxWiDD; ssxmod_itna2=iq+h7KAImDORKYQ4Y5G=nxBKDtD7D3qCD0dGMDxeq7tDRDFqApKDHtA68oon7ziBA0+PbZYxD3boBmiEPtDFOEPAeFmDDsuGSxf46oGKwGHd8wtUjFe+oV1lxUzutkGly=nCyCjq=UTHxMxFCr1DsFiKPuEpPVO7GrOyk5Aymnc0+11AFND7v16PvwrFQH4I72=3O1OpK7rGw+poWNCxjj=Ka5QDFWAvEzrDFQcIH=GpKpS90FAyIzGcTyck+yhQKaojn96dRqeIh=HkaFrlGnKwzO+a49=F7/c/MejoR3QM20K9IIOymrMN2bsk2TRdKFiaf4O0ut2MauiOER=iQNW2WVgDrkKzD=57r577wEx2hwkqhf8T8BDvkHZRDirC0bNK4O=G3TSkd3wYwq8bst0t9qF/e3M87NYtU2IWYWzqd=BqEfdqGq0R8wxmqLzpeGeuwSTq1OAiB87gDrozjnGkwDKRdrLz8uDjQKVlGhWk8Wd/rXQjx4pG=BNqpW/6TS1wpfxzGf5CrUhtt0j0wC5AUFo2GbX+QXPzD2guxKXrx8lZUQlwWIHyEUz+OLh0eWUkfHfM0YWXlgOejnuUa06rW9y5maDPipGms751hxKcqLq62pQty4iX3QDF6SRQd3tfEBf3CH7r2xe2qq0qdOI5Ge=GezD/Us5Z0xQBwVAZ2N/XvD0HDD").
SetResult(res).
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("XUEQIUHotStock err:%s", err.Error())
return &[]models.HotItem{}
}
//logger.SugaredLogger.Infof("XUEQIUHotStock:%+v", res)
return &res.Data.Items
}
func (m MarketNewsApi) HotEvent(size int) *[]models.HotEvent {
events := &[]models.HotEvent{}
url := fmt.Sprintf("https://xueqiu.com/hot_event/list.json?count=%d", size)
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "xueqiu.com").
SetHeader("Origin", "https://xueqiu.com").
SetHeader("Referer", "https://xueqiu.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetHeader("Cookie", "cookiesu=2617378771242871; s=c2121pp1u71; device_id=237a58584ec58d8e4d4e1040700a644f1; Hm_lvt_1db88642e346389874251b5a1eded6e3=1744100219,1744599115; xq_a_token=b7259d09435458cc3f1a963479abb270a1a016ce; xqat=b7259d09435458cc3f1a963479abb270a1a016ce; xq_r_token=28108bfa1d92ac8a46bbb57722633746218621a3; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOi0xLCJpc3MiOiJ1YyIsImV4cCI6MTc1MjU0MTk4OCwiY3RtIjoxNzUwMjMwNjA2NzI0LCJjaWQiOiJkOWQwbjRBWnVwIn0.kU_fz0luJoE7nr-K4UrNUi5-mAG-vMdXtuC4mUKIppILId4UpF70LB70yunxGiNSw6tPFR3-hyLvztKAHtekCUTm3XjUl5b3tEDP-ZUVqHnWXO_5hoeMI8h-Cfx6ZGlIr5x3icvTPkT0OV5CD5A33-ZDTKhKPf-DhJ_-m7CG5GbX4MseOBeMXuLUQUiYHPKhX1QUc0GTGrCzi8Mki0z49D0LVqCSgbsx3UGfowOOyx85_cXb4OAFvIjwbs2p0o_h-ibIT0ngVkkAyEDetVvlcZ_bkardhseCB7k9BEMgH2z8ihgkVxyy3P0degLmDUruhmqn5uZOCi1pVBDvCv9lBg; u=261737877124287; ssxmod_itna=QuG=D5AKiKDIqCqGKi7G7DgmmPlSDWFqKGHDyx4YK0CDmxjKiddDUQivnb8xpnQcGyGYoYhoqEeDBubrDSxD67DK4GTm+ogiw1o3B=xedQHDgBtN=7/i1K53N+rOjquLMU=kbqYxB3DExGkqj0tPi4DxaPD5xDTDWeDGDD3DnnsDQKDRx0kL0oDIxD1D0bmHUEvh38mDYePLmOmDYPYx94Y8KoDeEgsD7HUl/vIGGEAqjLPFegXLD0HolCqr4DCid1qDm+ECfkjDn9sD0KP8fn+CRoDv=tYr4ibx+o=W+8vstf9mjGe3cXseWdBmoFrmf4DA3bFAxnAxD7vYxADaDoerDGHPoxHF+PKGPtDKmiqQGeB5qbi4eg4KDHKDe3DeG0qeEP9xVUoHDDWMYYM0ICr4FBimBDM7D0x4QOECmhul5QCN/m5/74lGm=7x9Wp7A+i7xQ7wlMD4D; ssxmod_itna2=QuG=D5AKiKDIqCqGKi7G7DgmmPlSDWFqKGHDyx4YK0CDmxjKiddDUQivnb8xpnQcGyGYoYhoqoDirSDhPmGD24GajjDuGE3m7or4DlxOSGewHl6iaus2Q62SRX5CFjCds6ltF9xy6iaUuB262UkhRA8UXST=4/b+y3kGKzlGE8T29FA008ljy9jXXC7f7m7QsK667mlUooWrofk=qGZjxtcUrN1NtuAnne1hj+rQP5UnlFkxf+o7VjmatH7u7bCDlbTt3cz6CH9Fl4vye16W/ellc8I3Q37W7ZwiLGD/zPpZcnd2nsqqo/+zRbKAmz4plzwaDqGUe7f9E+P0IFRKqpRv+buQFHBSpcbwND7Q+9XWmnjI2UwKd98jIS3gPXwxvbx4OuiyH8gZ+OEt7DgE/AY/9W4VxDZrlFWyWnC4y4/I0IpAfaGKpbPmauKbkqawqv93vSf+9HamGe0Dt2PNgT3yiEB4vQP2/DdVpcGBOjFujWoHP32OshLPYI20LRCKddwEGkKqPzPwKPc3X5zuB=w2fUdtwKsAW5kQtsl8clNwjC5uDYrxR0h9xaj0xmD+YuI3GPT7xYTalRImPj2wL2=+91a304xa4bTWtP=dLGARhb/efRi0uktaz8i8C04G0x/ZWUzqRza8GGU=FfRfvb4GZM/q2rVsl0nLvRjGeAKgocLouyXs/uwZu3YxbAx30qCbjG1A533zAxIeIgD=0VAc3ixD").
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("HotEvent err:%s", err.Error())
return events
}
//logger.SugaredLogger.Infof("HotEvent:%s", resp.Body())
respMap := map[string]any{}
err = json.Unmarshal(resp.Body(), &respMap)
items, err := json.Marshal(respMap["list"])
if err != nil {
return events
}
json.Unmarshal(items, events)
return events
}
func (m MarketNewsApi) HotTopic(size int) []any {
url := "https://gubatopic.eastmoney.com/interface/GetData.aspx?path=newtopic/api/Topic/HomePageListRead"
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "gubatopic.eastmoney.com").
SetHeader("Origin", "https://gubatopic.eastmoney.com").
SetHeader("Referer", "https://gubatopic.eastmoney.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetFormData(map[string]string{
"param": fmt.Sprintf("ps=%d&p=1&type=0", size),
"path": "newtopic/api/Topic/HomePageListRead",
"env": "2",
}).
Post(url)
if err != nil {
logger.SugaredLogger.Errorf("HotTopic err:%s", err.Error())
return []any{}
}
//logger.SugaredLogger.Infof("HotTopic:%s", resp.Body())
respMap := map[string]any{}
err = json.Unmarshal(resp.Body(), &respMap)
return respMap["re"].([]any)
}
func (m MarketNewsApi) InvestCalendar(yearMonth string) []any {
if yearMonth == "" {
yearMonth = time.Now().Format("2006-01")
}
url := "https://app.jiuyangongshe.com/jystock-app/api/v1/timeline/list"
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "app.jiuyangongshe.com").
SetHeader("Origin", "https://www.jiuyangongshe.com").
SetHeader("Referer", "https://www.jiuyangongshe.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetHeader("Content-Type", "application/json").
SetHeader("token", "1cc6380a05c652b922b3d85124c85473").
SetHeader("platform", "3").
SetHeader("Cookie", "SESSION=NDZkNDU2ODYtODEwYi00ZGZkLWEyY2ItNjgxYzY4ZWMzZDEy").
SetHeader("timestamp", strconv.FormatInt(time.Now().UnixMilli(), 10)).
SetBody(map[string]string{
"date": yearMonth,
"grade": "0",
}).
Post(url)
if err != nil {
logger.SugaredLogger.Errorf("InvestCalendar err:%s", err.Error())
return []any{}
}
//logger.SugaredLogger.Infof("InvestCalendar:%s", resp.Body())
respMap := map[string]any{}
err = json.Unmarshal(resp.Body(), &respMap)
return respMap["data"].([]any)
}
func (m MarketNewsApi) ClsCalendar() []any {
url := "https://www.cls.cn/api/calendar/web/list?app=CailianpressWeb&flag=0&os=web&sv=8.4.6&type=0&sign=4b839750dc2f6b803d1c8ca00d2b40be"
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "www.cls.cn").
SetHeader("Origin", "https://www.cls.cn").
SetHeader("Referer", "https://www.cls.cn/").
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("ClsCalendar err:%s", err.Error())
return []any{}
}
respMap := map[string]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

@@ -0,0 +1,228 @@
package data
import (
"encoding/json"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/util"
"strings"
"testing"
"github.com/coocood/freecache"
"github.com/tidwall/gjson"
)
// @Author spark
// @Date 2025/4/23 17:58
// @Desc
//-----------------------------------------------------------------------------------
func TestGetSinaNews(t *testing.T) {
db.Init("../../data/stock.db")
NewMarketNewsApi().GetSinaNews(30)
//NewMarketNewsApi().GetNewTelegraph(30)
}
func TestGlobalStockIndexes(t *testing.T) {
resp := NewMarketNewsApi().GlobalStockIndexes(30)
bytes, err := json.Marshal(resp)
if err != nil {
return
}
logger.SugaredLogger.Debugf("resp: %+v", string(bytes))
}
func TestGetIndustryRank(t *testing.T) {
res := NewMarketNewsApi().GetIndustryRank("0", 10)
for s, a := range res["data"].([]any) {
logger.SugaredLogger.Debugf("key: %+v, value: %+v", s, a)
}
}
func TestGetIndustryMoneyRankSina(t *testing.T) {
res := NewMarketNewsApi().GetIndustryMoneyRankSina("0", "netamount")
for i, re := range res {
logger.SugaredLogger.Debugf("key: %+v, value: %+v", i, re)
}
}
func TestGetMoneyRankSina(t *testing.T) {
res := NewMarketNewsApi().GetMoneyRankSina("r3_net")
for i, re := range res {
logger.SugaredLogger.Debugf("key: %+v, value: %+v", i, re)
}
}
func TestGetStockMoneyTrendByDay(t *testing.T) {
res := NewMarketNewsApi().GetStockMoneyTrendByDay("sh600438", 360)
for i, re := range res {
logger.SugaredLogger.Debugf("key: %+v, value: %+v", i, re)
}
}
func TestTopStocksRankingList(t *testing.T) {
NewMarketNewsApi().TopStocksRankingList("2025-05-19")
}
func TestLongTiger(t *testing.T) {
db.Init("../../data/stock.db")
NewMarketNewsApi().LongTiger("2025-06-08")
}
func TestStockResearchReport(t *testing.T) {
db.Init("../../data/stock.db")
resp := NewMarketNewsApi().StockResearchReport("600584.sh", 7)
for _, a := range resp {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestIndustryResearchReport(t *testing.T) {
db.Init("../../data/stock.db")
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) {
db.Init("../../data/stock.db")
resp := NewMarketNewsApi().StockNotice("600584,600900")
for _, a := range resp {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestEMDictCode(t *testing.T) {
db.Init("../../data/stock.db")
resp := NewMarketNewsApi().EMDictCode("016", freecache.NewCache(100))
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))
}
func TestTradingViewNews(t *testing.T) {
db.Init("../../data/stock.db")
resp := NewMarketNewsApi().TradingViewNews()
for _, a := range *resp {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestXUEQIUHotStock(t *testing.T) {
db.Init("../../data/stock.db")
res := NewMarketNewsApi().XUEQIUHotStock(50, "10")
for _, a := range *res {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestHotEvent(t *testing.T) {
db.Init("../../data/stock.db")
res := NewMarketNewsApi().HotEvent(50)
for _, a := range *res {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestHotTopic(t *testing.T) {
db.Init("../../data/stock.db")
res := NewMarketNewsApi().HotTopic(10)
for _, a := range res {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestInvestCalendar(t *testing.T) {
db.Init("../../data/stock.db")
res := NewMarketNewsApi().InvestCalendar("2025-06")
for _, a := range res {
bytes, err := json.Marshal(a)
if err != nil {
continue
}
date := gjson.Get(string(bytes), "date")
list := gjson.Get(string(bytes), "list")
logger.SugaredLogger.Debugf("value: %+v,list: %+v", date.String(), list)
}
}
func TestClsCalendar(t *testing.T) {
db.Init("../../data/stock.db")
res := NewMarketNewsApi().ClsCalendar()
md := strings.Builder{}
for _, a := range res {
bytes, err := json.Marshal(a)
if err != nil {
continue
}
//logger.SugaredLogger.Debugf("value: %+v", string(bytes))
date := gjson.Get(string(bytes), "calendar_day")
md.WriteString("\n### 事件/会议日期:" + date.String())
list := gjson.Get(string(bytes), "items")
//logger.SugaredLogger.Debugf("value: %+v,list: %+v", date.String(), list)
list.ForEach(func(key, value gjson.Result) bool {
logger.SugaredLogger.Debugf("key: %+v,value: %+v", key.String(), gjson.Get(value.String(), "title"))
md.WriteString("\n- " + gjson.Get(value.String(), "title").String())
return true
})
}
logger.SugaredLogger.Debugf("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,24 +8,58 @@ 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
}
}
}
}
}
func TestGetTopNewsList(t *testing.T) {
GetTopNewsList(30)
news := GetTopNewsList(30)
t.Log(news)
}
func TestSearchGuShiTongStockInfo(t *testing.T) {
//db.Init("../../data/stock.db")
db.Init("../../data/stock.db")
SearchGuShiTongStockInfo("hk01810", 60)
SearchGuShiTongStockInfo("sh600745", 60)
SearchGuShiTongStockInfo("gb_goog", 60)
}
func TestGetZSInfo(t *testing.T) {
db.Init("../../data/stock.db")
GetZSInfo("中证银行", "sz399986", 30)
GetZSInfo("上海贝岭", "sh600171", 30)
}

View File

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

View File

@@ -0,0 +1,72 @@
package data
import (
"encoding/json"
"fmt"
"github.com/go-resty/resty/v2"
"go-stock/backend/logger"
"time"
)
// @Author spark
// @Date 2025/6/28 21:02
// @Desc
// -----------------------------------------------------------------------------------
type SearchStockApi struct {
words string
}
func NewSearchStockApi(words string) *SearchStockApi {
return &SearchStockApi{words: words}
}
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").
SetHeader("Origin", "https://xuangu.eastmoney.com").
SetHeader("Referer", "https://xuangu.eastmoney.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetHeader("Content-Type", "application/json").
SetBody(fmt.Sprintf(`{
"keyWord": "%s",
"pageSize": %d,
"pageNo": 1,
"fingerprint": "e38b5faabf9378c8238e57219f0ebc9b",
"gids": [],
"matchWord": "",
"timestamp": "1751113883290349",
"shareToGuba": false,
"requestId": "8xTWgCDAjvQ5lmvz5mDA3Ydk2AE4yoiJ1751113883290",
"needCorrect": true,
"removedConditionIdList": [],
"xcId": "xc0af28549ab330013ed",
"ownSelectAll": false,
"dxInfo": [],
"extraCondition": ""
}`, s.words, pageSize)).Post(url)
if err != nil {
logger.SugaredLogger.Errorf("SearchStock-err:%+v", err)
return map[string]any{}
}
respMap := map[string]any{}
json.Unmarshal(resp.Body(), &respMap)
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
return respMap
}
func (s SearchStockApi) HotStrategy() map[string]any {
url := fmt.Sprintf("https://np-ipick.eastmoney.com/recommend/stock/heat/ranking?count=20&trace=%d&client=web&biz=web_smart_tag", time.Now().Unix())
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "np-ipick.eastmoney.com").
SetHeader("Origin", "https://xuangu.eastmoney.com").
SetHeader("Referer", "https://xuangu.eastmoney.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("HotStrategy-err:%+v", err)
return map[string]any{}
}
respMap := map[string]any{}
json.Unmarshal(resp.Body(), &respMap)
return respMap
}

View File

@@ -0,0 +1,59 @@
package data
import (
"encoding/json"
"github.com/duke-git/lancet/v2/convertor"
"go-stock/backend/db"
"go-stock/backend/logger"
"testing"
)
func TestSearchStock(t *testing.T) {
db.Init("../../data/stock.db")
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("v:%+v", d)
}
}

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,104 +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"`
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,
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,
})
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
@@ -122,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
}

File diff suppressed because one or more lines are too long

View File

@@ -8,7 +8,6 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/PuerkitoBio/goquery"
"github.com/chromedp/chromedp"
@@ -16,26 +15,29 @@ import (
"github.com/duke-git/lancet/v2/slice"
"github.com/duke-git/lancet/v2/strutil"
"github.com/go-resty/resty/v2"
"github.com/robertkrimen/otto"
"github.com/samber/lo"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"golang.org/x/sys/windows/registry"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
"gorm.io/gorm"
"gorm.io/plugin/soft_delete"
"io"
"io/ioutil"
"strings"
"time"
)
const sinaStockUrl = "http://hq.sinajs.cn/rn=%d&list=%s"
const txStockUrl = "http://qt.gtimg.cn/?_=%d&q=%s"
const tushareApiUrl = "http://api.tushare.pro"
type StockDataApi struct {
client *resty.Client
config *Settings
config *SettingConfig
}
type StockInfo struct {
gorm.Model
@@ -151,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 {
@@ -168,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 {
@@ -192,7 +197,7 @@ func (receiver StockBasic) TableName() string {
func NewStockDataApi() *StockDataApi {
return &StockDataApi{
client: resty.New(),
config: GetConfig(),
config: GetSettingConfig(),
}
}
@@ -245,7 +250,7 @@ func (receiver StockDataApi) GetIndexBasic() {
func (receiver StockDataApi) GetStockBaseInfo() {
res := &TushareStockBasicResponse{}
fields := "ts_code,symbol,name,area,industry,cnspell,market,list_date,act_name,act_ent_type,fullname,exchange,list_status,curr_type,enname,delist_date,is_hs"
_, err := receiver.client.R().
resp, err := receiver.client.R().
SetHeader("content-type", "application/json").
SetBody(&TushareRequest{
ApiName: "stock_basic",
@@ -256,8 +261,7 @@ func (receiver StockDataApi) GetStockBaseInfo() {
SetResult(res).
Post(tushareApiUrl)
//logger.SugaredLogger.Infof("GetStockBaseInfo %s", string(resp.Body()))
//resp.Body()写入文件
//ioutil.WriteFile("stock_basic.json", resp.Body(), 0666)
ioutil.WriteFile("stock_basic.json", resp.Body(), 0666)
//logger.SugaredLogger.Infof("GetStockBaseInfo %+v", res)
if err != nil {
logger.SugaredLogger.Error(err.Error())
@@ -290,8 +294,58 @@ func (receiver StockDataApi) GetStockBaseInfo() {
}
func (receiver StockDataApi) GetStockCodeRealTimeData(StockCodes ...string) (*[]StockInfo, error) {
stockInfos := make([]StockInfo, 0)
codes := slice.JoinFunc(StockCodes, ",", func(s string) string {
hkcodes := slice.Filter(StockCodes, func(i int, s string) bool {
return strutil.HasPrefixAny(s, []string{"hk", "HK", "sh", "sz"})
})
if hkcodes != nil && len(hkcodes) > 0 {
hkcodesStr := slice.JoinFunc(hkcodes, ",", func(s string) string {
if strutil.HasPrefixAny(s, []string{"hk", "HK"}) {
return "r_" + strings.ToLower(s)
} else {
return strings.ToLower(s)
}
})
url := fmt.Sprintf(txStockUrl, time.Now().Unix(), hkcodesStr)
resp, err := receiver.client.R().
SetHeader("Host", "qt.gtimg.cn").
SetHeader("Referer", "https://gu.qq.com/").
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(url)
logger.SugaredLogger.Infof("GetStockCodeRealTimeData %s", url)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return &[]StockInfo{}, err
}
str := GB18030ToUTF8(resp.Body())
dataStr := strutil.SplitAndTrim(strings.Trim(str, "\n"), ";")
for _, data := range dataStr {
stockData, err := ParseTxStockData(data)
if err != nil {
logger.SugaredLogger.Error(err.Error())
continue
}
stockInfos = append(stockInfos, *stockData)
go func() {
var count int64
db.Dao.Model(&StockInfo{}).Where("code = ?", stockData.Code).Count(&count)
if count == 0 {
db.Dao.Model(&StockInfo{}).Create(stockData)
} else {
db.Dao.Model(&StockInfo{}).Where("code = ?", stockData.Code).Updates(stockData)
}
}()
}
}
szzsusCodes := slice.Filter(StockCodes, func(i int, s string) bool {
return !strutil.HasPrefixAny(s, []string{"hk", "HK", "sh", "sz"})
})
codes := slice.JoinFunc(szzsusCodes, ",", func(s string) string {
if strings.HasPrefix(s, "us") {
s = strings.Replace(s, "us", "gb_", 1)
}
@@ -313,12 +367,9 @@ func (receiver StockDataApi) GetStockCodeRealTimeData(StockCodes ...string) (*[]
return &[]StockInfo{}, err
}
stockInfos := make([]StockInfo, 0)
str := GB18030ToUTF8(resp.Body())
dataStr := strutil.SplitEx(str, "\n", true)
if len(dataStr) == 0 {
return &[]StockInfo{}, errors.New("获取股票信息失败,请检查股票代码是否正确")
}
for _, data := range dataStr {
//logger.SugaredLogger.Info(data)
stockData, err := ParseFullSingleStockData(data)
@@ -327,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() {
@@ -351,6 +405,28 @@ func (receiver StockDataApi) Follow(stockCode string) string {
logger.SugaredLogger.Error(err)
return "关注失败"
}
if strings.HasPrefix(stockCode, "us") {
stockCode = strings.Replace(stockCode, "us", "gb_", 1)
}
if strings.HasPrefix(stockCode, "US") {
stockCode = strings.Replace(stockCode, "US", "gb_", 1)
}
count := int64(0)
db.Dao.Model(&FollowedStock{}).Where("is_del = ?", 0).Count(&count)
logger.SugaredLogger.Errorf("Follow-count %v", count)
if count >= 63 {
return "最多只能关注63只股票"
}
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)
@@ -414,15 +490,64 @@ func (receiver StockDataApi) SetAlarmChangePercent(val, alarmPrice float64, stoc
return "设置成功"
}
func (receiver StockDataApi) SetStockSort(sort int64, stockCode string) {
if strutil.HasPrefixAny(stockCode, []string{"gb_"}) {
stockCode = strings.ToLower(stockCode)
stockCode = strings.Replace(stockCode, "gb_", "us", 1)
func (receiver StockDataApi) SetStockSort(newSort int64, stockCode string) {
//if strutil.HasPrefixAny(stockCode, []string{"gb_"}) {
// stockCode = strings.ToLower(stockCode)
// stockCode = strings.Replace(stockCode, "gb_", "us", 1)
//}
// 获取当前排序值
var currentStock FollowedStock
if err := db.Dao.Model(&FollowedStock{}).Where("stock_code = ?", strings.ToLower(stockCode)).First(&currentStock).Error; err != nil {
logger.SugaredLogger.Error("找不到当前股票: ", err.Error())
return
}
err := db.Dao.Model(&FollowedStock{}).Where("stock_code = ?", strings.ToLower(stockCode)).Update("sort", sort).Error
if err != nil {
logger.SugaredLogger.Error(err.Error())
oldSort := currentStock.Sort
// 如果排序值没有变化,直接返回
if oldSort == newSort {
return
}
// 检查新排序位置是否被占用
var count int64
if err := db.Dao.Model(&FollowedStock{}).Where("sort = ?", newSort).Count(&count).Error; err != nil {
logger.SugaredLogger.Error("检查新排序位置被占用失败: ", err.Error())
return
}
if count == 0 {
// 新位置未被占用,直接更新当前记录
if err := db.Dao.Model(&FollowedStock{}).
Where("stock_code = ?", strings.ToLower(stockCode)).
Update("sort", newSort).Error; err != nil {
logger.SugaredLogger.Error("更新排序位置失败: ", err.Error())
}
} else {
// 新位置已被占用,需要移动其他记录
if newSort < oldSort {
// 向前移动:将中间记录向后移动
if err := db.Dao.Model(&FollowedStock{}).
Where("sort >= ? AND sort < ?", newSort, oldSort).
Update("sort", gorm.Expr("sort + 1")).Error; err != nil {
logger.SugaredLogger.Error("向前排序更新失败: ", err.Error())
}
} else {
// 向后移动:将中间记录向前移动
if err := db.Dao.Model(&FollowedStock{}).
Where("sort > ? AND sort <= ?", oldSort, newSort).
Update("sort", gorm.Expr("sort - 1")).Error; err != nil {
logger.SugaredLogger.Error("向后排序更新失败: ", err.Error())
}
}
// 更新目标记录的排序
if err := db.Dao.Model(&FollowedStock{}).
Where("stock_code = ?", strings.ToLower(stockCode)).
Update("sort", newSort).Error; err != nil {
logger.SugaredLogger.Error("更新股票排序失败: ", err.Error())
}
}
}
func (receiver StockDataApi) SetStockAICron(cron string, stockCode string) {
if strutil.HasPrefixAny(stockCode, []string{"gb_"}) {
@@ -508,6 +633,146 @@ func GB18030ToUTF8(bs []byte) string {
return string(d)
}
func ParseTxStockData(data string) (*StockInfo, error) {
//v_r_hk09660="100~地平线机器人-W~09660~6.240~5.690~5.800~192659034.0~0~0~6.240~0~0~0~0~0~0~0~0~0~6.240~0~0~0~0~0~0~0~0~0~192659034.0~2025/04/29
//13:41:04~0.550~9.67~6.450~5.710~6.240~192659034.0~1180471843.140~0~32.51~~0~0~13.01~691.1364~823.6983~HORIZONROBOT-W~0.00~10.380~3.320~1.07~-16.03~0~0~0~0~0~32.51~6.40~1.74~600~73.33~17.96~GP~19.70~11.51~-0.95~-18.54~44.44~13200293682.00~11075904412.00~32.51~0.000~6.127~56.39~HKD~1~30";
//v_sz002241="51~歌尔股份~002241~22.26~22.27~0.00~0~0~0~22.26~1004~0.00~0~0.00~0~0.00~0~0.00~0~22.26~1004~0.00~558~0.00~0~0.00~0~0.00~0~~20250509092233~-0.01~-0.04~0.00~0.00~22.26/0/0~0~0~0.00~28.21~~0.00~0.00~0.00~686.46~777.09~2.31~24.50~20.04~0.00~-558~0.00~41.44~29.16~~~1.24~0.0000~0.0000~0~
//~GP-A~-13.75~6.76~1.09~8.18~3.39~30.63~15.70~6.87~17.47~-23.95~3083811231~3490989083~-21.75~12.02~3083811231~~~39.36~-0.04~~CNY~0~~0.00~0";
datas := strutil.SplitAndTrim(data, "=", "\"")
if len(datas) < 2 {
return nil, fmt.Errorf("invalid data format")
}
var result map[string]string
var err error
if strutil.ContainsAny(datas[0], []string{"v_r_hk", "v_hk", "v_sz", "v_sh"}) {
result, err = ParseTxHKStockData(datas)
}
//logger.SugaredLogger.Infof("股票数据解析完成: %v", result)
marshal, err := json.Marshal(result)
if err != nil {
logger.SugaredLogger.Errorf("json.Marshal error:%s", err.Error())
return nil, err
}
//logger.SugaredLogger.Infof("股票数据解析完成marshal: %s", marshal)
stockInfo := &StockInfo{}
err = json.Unmarshal(marshal, &stockInfo)
if err != nil {
logger.SugaredLogger.Errorf("json.Unmarshal error:%s", err.Error())
return nil, err
}
//logger.SugaredLogger.Infof("股票数据解析完成stockInfo: %+v", stockInfo)
return stockInfo, nil
}
func ParseTxHKStockData(datas []string) (map[string]string, error) {
//v_r_hk09660="
//100~ 0
//地平线机器人-W~ 1
//09660~ 2
//6.270~ 3 当前价
//5.690~ 4 昨收价
//5.800~ 5 开盘价
//195083034.0~
//0~
//0~
//6.270~
//0~
//0~
//0~
//0~
//0~
//0~
//0~
//0~
//0~
//6.270~
//0~0~0~0~0~0~0~0~0~
//195083034.0~
//2025/04/29 13:45:41~ 30 当前时间
//0.580~
//10.19~
//6.450~ 最高价
//5.710~ 最低价
//6.270~
//195083034.0~
//1195673623.140~
//0~
//32.66
//~~0~0~13.01~694.4592~827.6584~HORIZONROBOT-W~0.00~10.380~3.320~1.06~-18.71~0~0~0~0~0~32.66~6.43~1.76~600~74.17~18.53~GP~19.70~11.51~-0.48~-18.15~45.14~13200293682.00~11075904412.00~32.66~0.000~6.129~57.14~HKD~1~30";
result := make(map[string]string)
stockCode := strutil.ReplaceWithMap(datas[0], map[string]string{
"v_r_": "",
"v_": "",
})
result["股票代码"] = stockCode
parts := strutil.SplitAndTrim(datas[1], "~")
//logger.SugaredLogger.Infof("股票数据解析完成 len: %v", len(parts))
if len(parts) < 35 {
return nil, fmt.Errorf("invalid data format")
}
result["股票名称"] = parts[1]
result["当前价格"] = parts[3]
result["昨日收盘价"] = parts[4]
result["今日开盘价"] = parts[5]
result["今日最高价"] = parts[33]
result["今日最低价"] = parts[34]
if strutil.HasPrefixAny(stockCode, []string{"sz", "sh"}) {
result["买一报价"] = parts[9]
result["买一申报"] = parts[10]
result["买二报价"] = parts[11]
result["买二申报"] = parts[12]
result["买三报价"] = parts[13]
result["买三申报"] = parts[14]
result["买四报价"] = parts[15]
result["买四申报"] = parts[16]
result["买五报价"] = parts[17]
result["买五申报"] = parts[18]
result["卖一报价"] = parts[19]
result["卖一申报"] = parts[20]
result["卖二报价"] = parts[21]
result["卖二申报"] = parts[22]
result["卖三报价"] = parts[23]
result["卖三申报"] = parts[24]
result["卖四报价"] = parts[25]
result["卖四申报"] = parts[26]
result["卖五报价"] = parts[27]
result["卖五申报"] = parts[28]
}
timestr := ""
if strutil.ContainsAny(parts[30], []string{"/"}) {
timestr = strutil.ReplaceWithMap(parts[30], map[string]string{
"/": "-",
"\n": " ",
})
result["日期"] = strutil.SplitAndTrim(timestr, " ", "")[0]
result["时间"] = strutil.SplitAndTrim(timestr, " ", "")[1]
} else {
result["日期"] = strutil.Trim(parts[29])[0:4] + "-" + strutil.Trim(parts[29])[4:6] + "-" + strutil.Trim(parts[29])[6:8]
result["时间"] = strutil.Trim(parts[29])[8:10] + ":" + strutil.Trim(parts[29])[10:12] + ":" + strutil.Trim(parts[29])[12:14]
result["今日最高价"] = parts[32]
result["今日最低价"] = parts[33]
}
//logger.SugaredLogger.Infof("股票数据解析完成 %s %s 时间: %s,%s", parts[1], parts[3], parts[29], parts[30])
//logger.SugaredLogger.Infof("股票数据解析完成 时间: %v", timestr)
//logger.SugaredLogger.Infof("股票数据解析完成: %v", result)
return result, nil
}
func ParseFullSingleStockData(data string) (*StockInfo, error) {
datas := strutil.SplitAndTrim(data, "=", "\"")
if len(datas) < 2 {
@@ -912,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{
@@ -937,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)
@@ -1047,52 +1316,68 @@ 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()
// 分时数据
func (receiver StockDataApi) GetStockMinutePriceData(stockCode string) (*[]MinuteData, string) {
url := fmt.Sprintf("https://web.ifzq.gtimg.cn/appstock/app/minute/query?code=%s", stockCode)
if strutil.HasPrefixAny(stockCode, []string{"gb_", "GB_"}) {
stockCode = strings.Replace(strings.ToUpper(stockCode), "GB_", "us", 1) + ".OQ"
}
defer key.Close()
path, _, err := key.GetStringValue("Path")
//logger.SugaredLogger.Infof("Chrome安装路径%s", path)
if err != nil {
return "", false
if strutil.HasPrefixAny(stockCode, []string{"us", "US"}) {
url = fmt.Sprintf("https://web.ifzq.gtimg.cn/appstock/app/UsMinute/query?code=%s", stockCode)
}
return path + "\\chrome.exe", true
}
logger.SugaredLogger.Infof("GetStockMinutePriceData url:%s", url)
res := make(map[string]interface{})
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
SetHeader("Host", "web.ifzq.gtimg.cn").
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(url)
// CheckBrowserOnWindows 在 Windows 系统上检查Edge浏览器是否安装并返回安装路径
func CheckBrowserOnWindows() (string, bool) {
if path, ok := checkChromeOnWindows(); ok {
return path, true
}
date := ""
minuteDatas := &[]MinuteData{}
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
logger.SugaredLogger.Errorf("err:%s", err.Error())
return minuteDatas, date
}
//logger.SugaredLogger.Infof("resp:%s", resp.Body())
json.Unmarshal(resp.Body(), &res)
code, _ := convertor.ToInt(res["code"])
if res["data"] != nil && code == 0 {
data := res["data"].(map[string]interface{})
if stockData, ok := data[stockCode]; ok {
m := stockData.(map[string]interface{})
if d, ok := m["data"]; ok {
if m2, ok := d.(map[string]any); ok {
minutePriceData := m2["data"]
datas := minutePriceData.([]any)
for _, item := range datas {
minuteDataSplit := strutil.SplitEx(strutil.ReplaceWithMap(item.(string), map[string]string{
"\r\n": " ",
}), " ", true)
price, _ := convertor.ToFloat(minuteDataSplit[1])
volume, _ := convertor.ToFloat(minuteDataSplit[2])
amount := float64(0)
if len(minuteDataSplit) >= 4 {
amount, _ = convertor.ToFloat(minuteDataSplit[3])
}
minuteData := &MinuteData{
Time: minuteDataSplit[0][0:2] + ":" + minuteDataSplit[0][2:4],
Price: price,
Volume: volume,
Amount: amount,
}
*minuteDatas = append(*minuteDatas, *minuteData)
}
date = m2["date"].(string)
}
}
}
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
return minuteDatas, date
}
func (receiver StockDataApi) GetKLineData(stockCode string, kLineType string, days int64) *[]KLineData {
url := fmt.Sprintf("http://quotes.sina.cn/cn/api/json_v2.php/CN_MarketDataService.getKLineData?symbol=%s&scale=%s&ma=yes&days=%d", stockCode, kLineType, days)
url := fmt.Sprintf("http://quotes.sina.cn/cn/api/json_v2.php/CN_MarketDataService.getKLineData?symbol=%s&scale=%s&ma=yes&datalen=%d", stockCode, kLineType, days)
K := &[]KLineData{}
_, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
SetHeader("Host", "quotes.sina.cn").
@@ -1105,6 +1390,359 @@ func (receiver StockDataApi) GetKLineData(stockCode string, kLineType string, da
}
return K
}
func (receiver StockDataApi) GetHK_KLineData(stockCode string, kLineType string, days int64) *[]KLineData {
logger.SugaredLogger.Infof("GetHK_KLineData stockCode:%s,kLineType:%s,days:%d", stockCode, kLineType, days)
if strutil.HasPrefixAny(stockCode, []string{"gb_", "GB_"}) {
stockCode = strings.Replace(stockCode, "gb_", "us", 1) + ".OQ"
}
url := fmt.Sprintf("https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param=%s,%s,,,%d,qfq", stockCode, kLineType, days)
//logger.SugaredLogger.Infof("url:%s", url)
K := &[]KLineData{}
res := make(map[string]interface{})
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
SetHeader("Host", "web.ifzq.gtimg.cn").
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(url)
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
return K
}
//logger.SugaredLogger.Infof("resp:%s", resp.Body())
json.Unmarshal(resp.Body(), &res)
code, _ := convertor.ToInt(res["code"])
if code != 0 {
return K
}
if res["data"] != nil && code == 0 {
data := res["data"].(map[string]interface{})[stockCode].(map[string]interface{})
if data != nil {
var day []any
if data["qfqday"] != nil {
day = data["qfqday"].([]any)
}
if data["day"] != nil {
day = data["day"].([]any)
}
for _, v := range day {
if v != nil {
vv := v.([]any)
KLine := &KLineData{
Day: convertor.ToString(vv[0]),
Open: convertor.ToString(vv[1]),
Close: convertor.ToString(vv[2]),
High: convertor.ToString(vv[3]),
Low: convertor.ToString(vv[4]),
Volume: convertor.ToString(vv[5]),
}
*K = append(*K, *KLine)
}
}
}
}
return K
}
func (receiver StockDataApi) GetSinaHKStockInfo() {
pageSize := 500
for i := 1; i <= 3060/pageSize; i++ {
infos := getSinaStockInfo(receiver, i, pageSize)
for i, info := range *infos {
logger.SugaredLogger.Infof("infos:%d,%s:%s", i, info.Symbol, info.Name)
}
}
}
func getSinaStockInfo(receiver StockDataApi, page, pageSize int) *[]models.SinaStockInfo {
infos := &[]models.SinaStockInfo{}
url := "https://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/Market_Center.getHKStockData?page=%d&num=%d&sort=symbol&asc=1&node=qbgg_hk&_s_r_a=init"
_, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).SetProxy("http://localhost:10809").R().
SetHeader("Host", "vip.stock.finance.sina.com.cn").
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").
SetResult(infos).
Get(fmt.Sprintf(url, page, pageSize))
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
}
return infos
}
func (receiver StockDataApi) getDCStockInfo(market string, page, pageSize int) {
//m:105,m:106,m:107 //美股
//m:128+t:3,m:128+t:4,m:128+t:1,m:128+t:2 //港股
fs := "m:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23,m:0+t:81+s:2048"
switch market {
case "hk":
fs = "m:128+t:3,m:128+t:4,m:128+t:1,m:128+t:2"
case "us":
fs = "m:105,m:106,m:107"
}
url := "https://push2.eastmoney.com/api/qt/clist/get?np=1&fltt=1&invt=2&cb=data&fs=%s&fields=f12,f13,f14,f1,f2,f4,f3,f152,f5,f6,f7,f15,f18,f16,f17,f10,f8,f9,f23,f100,f265&fid=f3&pn=%d&pz=%d&po=1&dect=1&wbp2u=|0|0|0|web&_=%d"
sprintfUrl := fmt.Sprintf(url, fs, page, pageSize, time.Now().UnixMilli())
logger.SugaredLogger.Infof("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(sprintfUrl)
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
return
}
body := string(resp.Body())
logger.SugaredLogger.Infof("resp:%s", body)
vm := otto.New()
vm.Run("function data(res){return res};")
val, err := vm.Run(body)
if err != nil {
logger.SugaredLogger.Errorf("vm.Run error:%v", err.Error())
}
value, _ := val.Object().Value().Export()
marshal, err := json.Marshal(value)
data := make(map[string]any)
err = json.Unmarshal(marshal, &data)
if err != nil {
logger.SugaredLogger.Errorf("json.Unmarshal error:%v", err.Error())
}
logger.SugaredLogger.Infof("resp:%s", data)
if data["data"] != nil {
datas := data["data"].(map[string]any)
total := datas["total"].(float64)
diff := datas["diff"].([]any)
logger.SugaredLogger.Infof("total:%d", int(total))
for k, item := range diff {
stock := item.(map[string]any)
logger.SugaredLogger.Infof("k:%d,%s:%s:%s %s:%s", k, stock["f14"], stock["f12"], DCToTsCode(stock["f12"].(string)), stock["f100"], stock["f265"])
if market == "" {
stockInfo := &StockBasic{
Symbol: stock["f12"].(string),
TsCode: DCToTsCode(stock["f12"].(string)),
Name: stock["f14"].(string),
BKName: stock["f100"].(string),
BKCode: stock["f265"].(string),
}
db.Dao.Model(&StockBasic{}).Where("symbol = ?", stockInfo.Symbol).First(stockInfo)
logger.SugaredLogger.Infof("stockInfo:%+v", stockInfo)
if stockInfo.ID == 0 {
db.Dao.Model(&StockBasic{}).Create(stockInfo)
} else {
stockInfo = &StockBasic{
Symbol: stock["f12"].(string),
TsCode: DCToTsCode(stock["f12"].(string)),
Name: stock["f14"].(string),
BKName: stock["f100"].(string),
BKCode: stock["f265"].(string),
}
db.Dao.Model(&StockBasic{}).Where("symbol = ?", stockInfo.Symbol).Updates(stockInfo)
}
}
if market == "hk" {
stockInfo := &models.StockInfoHK{
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".HK",
Name: stock["f14"].(string),
BKName: stock["f100"].(string),
BKCode: stock["f265"].(string),
}
db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", stockInfo.Code).First(stockInfo)
logger.SugaredLogger.Infof("stockInfo:%+v", stockInfo)
if stockInfo.ID == 0 {
db.Dao.Model(&models.StockInfoHK{}).Create(stockInfo)
} else {
stockInfo = &models.StockInfoHK{
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".HK",
Name: stock["f14"].(string),
BKName: stock["f100"].(string),
BKCode: stock["f265"].(string),
}
db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", stockInfo.Code).Updates(stockInfo)
}
}
if market == "us" {
stockInfo := &models.StockInfoUS{
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".US",
Name: stock["f14"].(string),
BKName: stock["f100"].(string),
BKCode: stock["f265"].(string),
}
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)
}
}
}
}
}
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().
SetHeader("Host", "stock.gtimg.cn").
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, pageSize))
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
return
}
js := "var " + string(resp.Body())
vm := otto.New()
_, err = vm.Run(js)
_, err = vm.Run("var data = JSON.stringify(list_data);")
if err != nil {
return
}
value, err := vm.Get("data")
data := make(map[string]any)
err = json.Unmarshal([]byte(value.String()), &data)
if err != nil {
logger.SugaredLogger.Errorf("json.Unmarshal error:%v", err.Error())
}
logger.SugaredLogger.Infof("resp:%s", data)
if data["code"] != nil && data["code"].(float64) == 0 {
d := data["data"].(map[string]any)
saveHKStockInfo(d)
page_count := int64(d["page_count"].(float64))
logger.SugaredLogger.Infof("page_count:%d", page_count)
page := int64(1)
for page > page_count {
urlx := fmt.Sprintf("https://stock.gtimg.cn/data/hk_rank.php?board=main_all&metric=price&pageSize=%d&reqPage=%d&order=desc&var_name=list_data", pageSize, page)
logger.SugaredLogger.Infof("url:%s", urlx)
resp, err = receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
SetHeader("Host", "stock.gtimg.cn").
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(urlx)
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
break
}
js = "var " + string(resp.Body())
_, err = vm.Run(js)
_, err = vm.Run("var data = JSON.stringify(list_data);")
if err != nil {
return
}
value, err = vm.Get("data")
data = make(map[string]any)
err = json.Unmarshal([]byte(value.String()), &data)
if err != nil {
logger.SugaredLogger.Errorf("json.Unmarshal error:%v", err.Error())
}
logger.SugaredLogger.Infof("resp:%s", data)
if data != nil && data["code"] != nil && data["code"].(float64) == 0 {
if data["data"] != nil {
d = data["data"].(map[string]any)
saveHKStockInfo(d)
}
}
page++
}
//
}
}
func saveHKStockInfo(d map[string]any) {
for _, v := range d["page_data"].([]any) {
vv := v.(string)
splits := strings.Split(vv, "~")
stock := &models.StockInfoHK{
Code: strutil.PadStart(splits[0], 5, "0") + ".HK",
Name: splits[1],
}
logger.SugaredLogger.Infof("vv:%s", vv)
db.Dao.Model(stock).Where("code = ?", stock.Code).First(stock)
if stock.ID == 0 {
logger.SugaredLogger.Infof("stock:%+v", stock)
db.Dao.Model(&models.StockInfoHK{}).Create(stock)
}
}
}
func (receiver StockDataApi) GetCommonKLineData(stockCode string, kLineType string, days int64) *[]KLineData {
url := fmt.Sprintf("https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param=%s,%s,,,%d,qfq", stockCode, kLineType, days)
logger.SugaredLogger.Infof("url:%s", url)
K := &[]KLineData{}
res := make(map[string]interface{})
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
SetHeader("Host", "web.ifzq.gtimg.cn").
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(url)
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
return K
}
logger.SugaredLogger.Infof("resp:%s", resp.Body())
json.Unmarshal(resp.Body(), &res)
code, _ := convertor.ToInt(res["code"])
if code != 0 {
return K
}
if res["data"] != nil && code == 0 {
data := res["data"].(map[string]interface{})[stockCode].(map[string]interface{})
if data != nil {
var day []any
if data["qfqday"] != nil {
day = data["qfqday"].([]any)
}
if data["day"] != nil {
day = data["day"].([]any)
}
for _, v := range day {
if v != nil {
vv := v.([]any)
KLine := &KLineData{
Day: convertor.ToString(vv[0]),
Open: convertor.ToString(vv[1]),
Close: convertor.ToString(vv[2]),
High: convertor.ToString(vv[3]),
Low: convertor.ToString(vv[4]),
Volume: convertor.ToString(vv[5]),
}
*K = append(*K, *KLine)
}
}
}
}
return K
}
// JSONToMarkdownTable 将JSON数据转换为Markdown表格
func JSONToMarkdownTable(jsonData []byte) (string, error) {
@@ -1160,3 +1798,10 @@ type KLineData struct {
Close string `json:"close"`
Volume string `json:"volume"`
}
type MinuteData struct {
Time string `json:"time"`
Price float64 `json:"price"`
Volume float64 `json:"volume"`
Amount float64 `json:"amount"`
}

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,16 +4,19 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/duke-git/lancet/v2/convertor"
"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
@@ -22,41 +25,70 @@ import (
//-----------------------------------------------------------------------------------
func TestGetTelegraph(t *testing.T) {
GetTelegraphList(30)
db.Init("../../data/stock.db")
//telegraphs := GetTelegraphList(30)
//for _, telegraph := range *telegraphs {
// logger.SugaredLogger.Info(telegraph)
//}
list := NewMarketNewsApi().GetNewTelegraph(30)
for _, telegraph := range *list {
logger.SugaredLogger.Infof("telegraph:%+v", telegraph)
}
}
func TestGetFinancialReports(t *testing.T) {
db.Init("../../data/stock.db")
//GetFinancialReports("sz000802", 30)
//GetFinancialReports("hk00927", 30)
GetFinancialReports("gb_aapl", 30)
//GetFinancialReports("gb_aapl", 30)
GetFinancialReportsByXUEQIU("sz000802", 30)
GetFinancialReportsByXUEQIU("gb_aapl", 30)
GetFinancialReportsByXUEQIU("hk00927", 30)
}
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")
}
func TestSearchStockPriceInfo(t *testing.T) {
db.Init("../../data/stock.db")
//SearchStockPriceInfo("中信证券", "hk06030", 30)
//SearchStockPriceInfo("上海贝岭", "sh600171", 30)
SearchStockPriceInfo("苹果公司", "gb_aapl", 30)
SearchStockPriceInfo("博安生物", "hk06955", 30)
SearchStockPriceInfo("上海贝岭", "sh600171", 30)
//SearchStockPriceInfo("苹果公司", "gb_aapl", 30)
//SearchStockPriceInfo("微创光电", "bj430198", 30)
getZSInfo("创业板指数", "sz399006", 30)
//getZSInfo("创业板指数", "sz399006", 30)
//getZSInfo("上证综合指数", "sh000001", 30)
//getZSInfo("沪深300指数", "sh000300", 30)
}
func TestGetStockMinutePriceData(t *testing.T) {
db.Init("../../data/stock.db")
data, date := NewStockDataApi().GetStockMinutePriceData("usTSLA.OQ")
logger.SugaredLogger.Infof("date:%s", date)
logger.SugaredLogger.Infof("%+#v", *data)
}
func TestGetKLineData(t *testing.T) {
db.Init("../../data/stock.db")
k := NewStockDataApi().GetKLineData("sh600171", "240", 30)
@@ -71,6 +103,39 @@ func TestGetKLineData(t *testing.T) {
logger.SugaredLogger.Infof("markdownTable:\n%s", markdownTable)
}
func TestGetHK_KLineData(t *testing.T) {
db.Init("../../data/stock.db")
k := NewStockDataApi().GetHK_KLineData("hk01810", "day", 1)
jsonData, _ := json.Marshal(*k)
markdownTable, err := JSONToMarkdownTable(jsonData)
if err != nil {
logger.SugaredLogger.Errorf("json.Marshal error:%s", err.Error())
}
logger.SugaredLogger.Infof("markdownTable:\n%s", markdownTable)
}
func TestGetHKStockInfo(t *testing.T) {
db.Init("../../data/stock.db")
//NewStockDataApi().GetHKStockInfo(200)
//NewStockDataApi().GetSinaHKStockInfo()
//m:105,m:106,m:107 //美股
//m:128+t:3,m:128+t:4,m:128+t:1,m:128+t:2 //港股
//287 224 605
for i := 1; i <= 605; i++ {
NewStockDataApi().getDCStockInfo("us", i, 20)
time.Sleep(time.Duration(random.RandInt(1, 3)) * time.Second)
}
}
func TestParseTxStockData(t *testing.T) {
str := "v_r_hk09660=\"100~地平线机器人-W~09660~6.340~5.690~5.800~210980204.0~0~0~6.340~0~0~0~0~0~0~0~0~0~6.340~0~0~0~0~0~0~0~0~0~210980204.0~2025/04/29\n14:14:52~0.650~11.42~6.450~5.710~6.340~210980204.0~1295585259.040~0~33.03~~0~0~13.01~702.2123~836.8986~HORIZONROBOT-W~0.00~10.380~3.320~1.00~-53.74~0~0~0~0~0~33.03~6.50~1.90~600~76.11~19.85~GP~19.70~11.51~0.63~-17.23~46.76~13200293682.00~11075904412.00~33.03~0.000~6.141~58.90~HKD~1~30\";"
//str = "v_sz002241=\"51~歌尔股份~002241~22.26~22.27~0.00~0~0~0~22.26~1004~0.00~0~0.00~0~0.00~0~0.00~0~22.26~1004~0.00~558~0.00~0~0.00~0~0.00~0~~20250509092233~-0.01~-0.04~0.00~0.00~22.26/0/0~0~0~0.00~28.21~~0.00~0.00~0.00~686.46~777.09~2.31~24.50~20.04~0.00~-558~0.00~41.44~29.16~~~1.24~0.0000~0.0000~0~\n~GP-A~-13.75~6.76~1.09~8.18~3.39~30.63~15.70~6.87~17.47~-23.95~3083811231~3490989083~-21.75~12.02~3083811231~~~39.36~-0.04~~CNY~0~~0.00~0\";"
str = "v_sz002241=\"51~歌尔股份~002241~21.92~22.27~22.14~109872~40211~69642~21.91~25~21.90~961~21.89~257~21.88~748~21.87~665~21.92~86~21.93~168~21.94~556~21.95~171~21.96~85~~20250509094209~-0.35~-1.57~22.16~21.84~21.92/109872/241183171~109872~24118~0.36~27.78~~22.16~21.84~1.44~675.97~765.22~2.27~24.50~20.04~2.57~1590~21.95~40.80~28.71~~~1.24~24118.3171~0.0000~0~\n~GP-A~-15.07~5.13~1.11~8.18~3.39~30.63~15.70~5.23~15.67~-25.11~3083811231~3490989083~42.72~10.31~3083811231~~~37.23~0.18~~CNY~0~~21.85~1952\";"
//str = "v_r_hk09660=\"100~地平线机器人-W~09660~6.860~7.000~7.010~21157200.0~0~0~6.860~0~0~0~0~0~0~0~0~0~6.860~0~0~0~0~0~0~0~0~0~21157200.0~2025/05/09\n09:43:13~-0.140~-2.00~7.030~6.730~6.860~21157200.0~144331073.000~0~35.74~~0~0~4.29~759.8070~905.5401~HORIZONROBOT-W~0.00~10.380~3.320~2.93~11.10~0~0~0~0~0~35.74~7.04~0.19~600~90.56~4.73~GP~19.70~11.51~17.26~48.48~13.58~13200293682.00~11075904412.00~35.74~0.000~6.822~71.93~HKD~1~30\";"
info, _ := ParseTxStockData(str)
logger.SugaredLogger.Infof("%+#v", info)
}
func TestGetRealTimeStockPriceInfo(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
@@ -130,7 +195,7 @@ func TestParseFullSingleStockData(t *testing.T) {
func TestNewStockDataApi(t *testing.T) {
db.Init("../../data/stock.db")
stockDataApi := NewStockDataApi()
datas, _ := stockDataApi.GetStockCodeRealTimeData("sh600859", "sh600745", "gb_tsla")
datas, _ := stockDataApi.GetStockCodeRealTimeData("sh600859", "sh600745", "gb_tsla", "hk09660", "hk00700")
for _, data := range *datas {
t.Log(data)
}
@@ -191,7 +256,7 @@ func TestReadFile(t *testing.T) {
func TestFollowedList(t *testing.T) {
db.Init("../../data/stock.db")
stockDataApi := NewStockDataApi()
stockDataApi.GetFollowList()
stockDataApi.GetFollowList(1)
}

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

@@ -0,0 +1,291 @@
package data
import (
"bufio"
"fmt"
"go-stock/backend/logger"
"os"
"strings"
"github.com/go-ego/gse"
)
// 金融情感词典,包含股票市场相关的专业词汇
var (
seg gse.Segmenter
// 正面金融词汇及其权重
positiveFinanceWords = map[string]float64{
"上涨": 2.0, "涨停": 3.0, "牛市": 3.0, "反弹": 2.0, "新高": 2.5,
"利好": 2.5, "增持": 2.0, "买入": 2.0, "推荐": 1.5, "看多": 2.0,
"盈利": 2.0, "增长": 2.0, "超预期": 2.5, "强劲": 1.5, "回升": 1.5,
"复苏": 2.0, "突破": 2.0, "创新高": 3.0, "回暖": 1.5, "上扬": 1.5,
"利好消息": 3.0, "收益增长": 2.5, "利润增长": 2.5, "业绩优异": 2.5,
"潜力股": 2.0, "绩优股": 2.0, "强势": 1.5, "走高": 1.5, "攀升": 1.5,
"大涨": 2.5, "飙升": 3.0, "井喷": 3.0, "爆发": 2.5, "暴涨": 3.0,
}
// 负面金融词汇及其权重
negativeFinanceWords = map[string]float64{
"下跌": 2.0, "跌停": 3.0, "熊市": 3.0, "回调": 1.5, "新低": 2.5,
"利空": 2.5, "减持": 2.0, "卖出": 2.0, "看空": 2.0, "亏损": 2.5,
"下滑": 2.0, "萎缩": 2.0, "不及预期": 2.5, "疲软": 1.5, "恶化": 2.0,
"衰退": 2.0, "跌破": 2.0, "创新低": 3.0, "走弱": 1.5, "下挫": 1.5,
"利空消息": 3.0, "收益下降": 2.5, "利润下滑": 2.5, "业绩不佳": 2.5,
"垃圾股": 2.0, "风险股": 2.0, "弱势": 1.5, "走低": 1.5, "缩量": 2.5,
"大跌": 2.5, "暴跌": 3.0, "崩盘": 3.0, "跳水": 3.0, "重挫": 3.0,
}
// 否定词,用于反转情感极性
negationWords = map[string]struct{}{
"不": {}, "没": {}, "无": {}, "非": {}, "未": {}, "别": {}, "勿": {},
}
// 程度副词,用于调整情感强度
degreeWords = map[string]float64{
"非常": 1.8, "极其": 2.2, "太": 1.8, "很": 1.5,
"比较": 0.8, "稍微": 0.6, "有点": 0.7, "显著": 1.5,
"大幅": 1.8, "急剧": 2.0, "轻微": 0.6, "小幅": 0.7,
}
// 转折词,用于识别情感转折
transitionWords = map[string]struct{}{
"但是": {}, "然而": {}, "不过": {}, "却": {}, "可是": {},
}
)
func init() {
// 加载默认词典
err := seg.LoadDict()
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
}
// SentimentResult 情感分析结果类型
type SentimentResult struct {
Score float64 // 情感得分
Category SentimentType // 情感类别
PositiveCount int // 正面词数量
NegativeCount int // 负面词数量
Description string // 情感描述
}
// SentimentType 情感类型枚举
type SentimentType int
const (
Positive SentimentType = iota
Negative
Neutral
)
// AnalyzeSentiment 判断文本的情感
func AnalyzeSentiment(text string) SentimentResult {
// 初始化得分
score := 0.0
positiveCount := 0
negativeCount := 0
// 分词(简单按单个字符分割)
words := splitWords(text)
// 检查文本是否包含转折词,并分割成两部分
var transitionIndex int
var hasTransition bool
for i, word := range words {
if _, ok := transitionWords[word]; ok {
transitionIndex = i
hasTransition = true
break
}
}
// 处理有转折的文本
if hasTransition {
// 转折前的部分
preTransitionWords := words[:transitionIndex]
preScore, prePos, preNeg := calculateScore(preTransitionWords)
// 转折后的部分,权重加倍
postTransitionWords := words[transitionIndex+1:]
postScore, postPos, postNeg := calculateScore(postTransitionWords)
postScore *= 1.5 // 转折后的情感更重要
score = preScore + postScore
positiveCount = prePos + postPos
negativeCount = preNeg + postNeg
} else {
// 没有转折的文本
score, positiveCount, negativeCount = calculateScore(words)
}
// 确定情感类别
var category SentimentType
switch {
case score > 1.0:
category = Positive
case score < -1.0:
category = Negative
default:
category = Neutral
}
return SentimentResult{
Score: score,
Category: category,
PositiveCount: positiveCount,
NegativeCount: negativeCount,
Description: GetSentimentDescription(category),
}
}
// 计算情感得分
func calculateScore(words []string) (float64, int, int) {
score := 0.0
positiveCount := 0
negativeCount := 0
// 遍历每个词,计算情感得分
for i, word := range words {
// 首先检查是否为程度副词
degree, isDegree := degreeWords[word]
// 检查是否为否定词
_, isNegation := negationWords[word]
// 检查是否为金融正面词
if posScore, isPositive := positiveFinanceWords[word]; isPositive {
// 检查前一个词是否为否定词或程度副词
if i > 0 {
prevWord := words[i-1]
if _, isNeg := negationWords[prevWord]; isNeg {
score -= posScore
negativeCount++
continue
}
if deg, isDeg := degreeWords[prevWord]; isDeg {
score += posScore * deg
positiveCount++
continue
}
}
score += posScore
positiveCount++
continue
}
// 检查是否为金融负面词
if negScore, isNegative := negativeFinanceWords[word]; isNegative {
// 检查前一个词是否为否定词或程度副词
if i > 0 {
prevWord := words[i-1]
if _, isNeg := negationWords[prevWord]; isNeg {
score += negScore
positiveCount++
continue
}
if deg, isDeg := degreeWords[prevWord]; isDeg {
score -= negScore * deg
negativeCount++
continue
}
}
score -= negScore
negativeCount++
continue
}
// 处理程度副词(如果后面跟着情感词)
if isDegree && i+1 < len(words) {
nextWord := words[i+1]
if posScore, isPositive := positiveFinanceWords[nextWord]; isPositive {
score += posScore * degree
positiveCount++
continue
}
if negScore, isNegative := negativeFinanceWords[nextWord]; isNegative {
score -= negScore * degree
negativeCount++
continue
}
}
// 处理否定词(如果后面跟着情感词)
if isNegation && i+1 < len(words) {
nextWord := words[i+1]
if posScore, isPositive := positiveFinanceWords[nextWord]; isPositive {
score -= posScore
negativeCount++
continue
}
if negScore, isNegative := negativeFinanceWords[nextWord]; isNegative {
score += negScore
positiveCount++
continue
}
}
}
return score, positiveCount, negativeCount
}
// 简单的分词函数,考虑了中文和英文
func splitWords(text string) []string {
return seg.Cut(text, true)
}
// GetSentimentDescription 获取情感类别的文本描述
func GetSentimentDescription(category SentimentType) string {
switch category {
case Positive:
return "看涨"
case Negative:
return "看跌"
case Neutral:
return "中性"
default:
return "未知"
}
}
func main() {
// 从命令行读取输入
reader := bufio.NewReader(os.Stdin)
fmt.Println("请输入要分析的股市相关文本输入exit退出")
for {
fmt.Print("> ")
text, err := reader.ReadString('\n')
if err != nil {
fmt.Println("读取输入时出错:", err)
continue
}
// 去除换行符
text = strings.TrimSpace(text)
// 检查是否退出
if text == "exit" {
break
}
// 分析情感
result := AnalyzeSentiment(text)
// 输出结果
fmt.Printf("情感分析结果: %s (得分: %.2f, 正面词:%d, 负面词:%d)\n",
GetSentimentDescription(result.Category),
result.Score,
result.PositiveCount,
result.NegativeCount)
}
}

View File

@@ -0,0 +1,36 @@
package data
import (
"fmt"
"strings"
"testing"
)
// @Author spark
// @Date 2025/6/19 13:05
// @Desc
//-----------------------------------------------------------------------------------
func TestAnalyzeSentiment(t *testing.T) {
// 分析情感
text := " 【调查韩国近两成中小学生过度使用智能手机或互联网】财联社6月19日电韩国女性家族部18日公布的一项年度调查结果显示接受调查的韩国中小学生中共计约17.3%、即超过21万人使用智能手机或互联网的程度达到了“危险水平”这意味着他们因过度依赖智能手机或互联网而需要关注或干预这一比例引人担忧。 (新华社)\n"
text = "消息人士称联合利华Unilever正在为Graze零食品牌寻找买家。\n"
text = "【韩国未来5年将投入51万亿韩元发展文化产业】 据韩联社韩国文化体育观光部文体部今后5年将投入51万亿韩元约合人民币2667亿元预算落实总统李在明在竞选时期提出的“将韩国打造成全球五大文化强国之一”的承诺。\n"
//text = "【油气股持续拉升 国际实业午后涨停】财联社6月19日电油气股午后持续拉升国际实业、宝莫股份午后涨停准油股份、山东墨龙。茂化实华此前涨停通源石油、海默科技、贝肯能源、中曼石油、科力股份等多股涨超5%。\n"
//text = " 【三大指数均跌逾1% 下跌个股近4800只】财联社6月19日电指数持续走弱沪指下挫跌逾1.00%深成指跌1.25%创业板指跌1.39%。核聚变、风电、军工、食品消费等板块指数跌幅居前沪深京三市下跌个股近4800只。\n"
text = "【银行理财首单网下打新落地】财联社6月20日电记者从多渠道获悉光大理财以申报价格17元参与信通电子网下打新并成功入围有效报价成为行业内首家参与网下打新的银行理财公司。光大理财工作人员向证券时报记者表示本次光大理财是以其管理的混合类产品“阳光橙增盈绝对收益策略”参与了此次网下打新该产品为光大理财“固收+”银行理财产品。资料显示信通电子成立于1996年核心产品包括输电线路智能巡检系统、变电站智能辅控系统、移动智能终端及其他产品。根据其招股说明书信通电子2023、2024年营业收入分别较上年增长19.08%和7.97%净利润分别较上年增长5.6%和15.11%。 (证券时报)"
text = " 【以军称拦截数枚伊朗导弹】财联社6月20日电据央视新闻报道以军在贝尔谢巴及周边区域拦截了数枚伊朗导弹但仍有导弹或拦截残骸落地。以色列国防军发文表示搜救队伍正在一处“空中物体落地”的所在区域开展工作公众目前可以离开避难场所。伊朗方面对上述说法暂无回应。"
words := splitWords(text)
fmt.Println(strings.Join(words, " "))
result := AnalyzeSentiment(text)
// 输出结果
fmt.Printf("情感分析结果: %s (得分: %.2f, 正面词:%d, 负面词:%d)\n",
result.Description,
result.Score,
result.PositiveCount,
result.NegativeCount)
}

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

@@ -64,7 +64,13 @@ func GetTableMarkdown(document *goquery.Document, waitVisible string, markdown *
document.Find(waitVisible).First().Find("tr").Each(func(index int, item *goquery.Selection) {
row := ""
item.Find("th, td").Each(func(i int, cell *goquery.Selection) {
text := cell.Text()
text := cell.Children().FilterFunction(func(i int, s *goquery.Selection) bool {
return isVisible(s)
}).Text()
if text == "" {
text = cell.Text()
}
row += "|" + text
})
row += "|"
@@ -85,3 +91,21 @@ func GetTableMarkdown(document *goquery.Document, waitVisible string, markdown *
})
logger.SugaredLogger.Infof("\n%s", markdown.String())
}
// isVisible 函数用于判断元素是否可见
func isVisible(s *goquery.Selection) bool {
// 检查 display 属性
display, _ := s.Attr("style")
if strings.Contains(strings.ToLower(display), "display: none") {
return false
}
// 检查 visibility 属性
if strings.Contains(strings.ToLower(display), "visibility: hidden") {
return false
}
// 检查 opacity 属性
if strings.Contains(strings.ToLower(display), "opacity: 0") {
return false
}
return true
}

View File

@@ -150,13 +150,15 @@ func (receiver AIResponseResult) TableName() string {
type VersionInfo struct {
gorm.Model
Version string `json:"version"`
Content string `json:"content"`
Icon string `json:"icon"`
Alipay string `json:"alipay"`
Wxpay string `json:"wxpay"`
BuildTimeStamp int64 `json:"buildTimeStamp"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
Version string `json:"version"`
Content string `json:"content"`
Icon string `json:"icon"`
Alipay string `json:"alipay"`
Wxpay string `json:"wxpay"`
Wxgzh string `json:"wxgzh"`
BuildTimeStamp int64 `json:"buildTimeStamp"`
OfficialStatement string `json:"officialStatement"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
}
func (receiver VersionInfo) TableName() string {
@@ -170,6 +172,8 @@ type StockInfoHK struct {
FullName string `json:"fullName"`
EName string `json:"eName"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
BKName string `json:"bk_name"`
BKCode string `json:"bk_code"`
}
func (receiver StockInfoHK) TableName() string {
@@ -185,6 +189,8 @@ type StockInfoUS struct {
Exchange string `json:"exchange"`
Type string `json:"type"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
BKName string `json:"bk_name"`
BKCode string `json:"bk_code"`
}
func (receiver StockInfoUS) TableName() string {
@@ -194,6 +200,12 @@ func (receiver StockInfoUS) TableName() string {
type Resp struct {
Code int `json:"code"`
Message string `json:"message"`
Error struct {
Code string `json:"code"`
Message string `json:"message"`
Param string `json:"param"`
Type string `json:"type"`
} `json:"error"`
}
type PromptTemplate struct {
@@ -215,3 +227,463 @@ type Prompt struct {
Content string `json:"content"`
Type string `json:"type"`
}
type Telegraph struct {
gorm.Model
Time string `json:"time"`
Content string `json:"content"`
SubjectTags []string `json:"subjects" gorm:"-:all"`
StocksTags []string `json:"stocks" gorm:"-:all"`
IsRed bool `json:"isRed"`
Url string `json:"url"`
Source string `json:"source"`
TelegraphTags []TelegraphTags `json:"tags" gorm:"-:migration;foreignKey:TelegraphId"`
SentimentResult string `json:"sentimentResult" gorm:"-:all"`
}
type TelegraphTags struct {
gorm.Model
TagId uint `json:"tagId"`
TelegraphId uint `json:"telegraphId"`
}
func (t TelegraphTags) TableName() string {
return "telegraph_tags"
}
type Tags struct {
gorm.Model
Name string `json:"name"`
Type string `json:"type"`
}
func (p Tags) TableName() string {
return "tags"
}
func (p Telegraph) TableName() string {
return "telegraph_list"
}
type SinaStockInfo struct {
Symbol string `json:"symbol"`
Name string `json:"name"`
Engname string `json:"engname"`
Tradetype string `json:"tradetype"`
Lasttrade string `json:"lasttrade"`
Prevclose string `json:"prevclose"`
Open string `json:"open"`
High string `json:"high"`
Low string `json:"low"`
Volume string `json:"volume"`
Currentvolume string `json:"currentvolume"`
Amount string `json:"amount"`
Ticktime string `json:"ticktime"`
Buy string `json:"buy"`
Sell string `json:"sell"`
High52Week string `json:"high_52week"`
Low52Week string `json:"low_52week"`
Eps string `json:"eps"`
Dividend string `json:"dividend"`
StocksSum string `json:"stocks_sum"`
Pricechange string `json:"pricechange"`
Changepercent string `json:"changepercent"`
MarketValue string `json:"market_value"`
PeRatio string `json:"pe_ratio"`
}
type LongTigerRankData struct {
ACCUMAMOUNT float64 `json:"ACCUM_AMOUNT"`
BILLBOARDBUYAMT float64 `json:"BILLBOARD_BUY_AMT"`
BILLBOARDDEALAMT float64 `json:"BILLBOARD_DEAL_AMT"`
BILLBOARDNETAMT float64 `json:"BILLBOARD_NET_AMT"`
BILLBOARDSELLAMT float64 `json:"BILLBOARD_SELL_AMT"`
CHANGERATE float64 `json:"CHANGE_RATE"`
CLOSEPRICE float64 `json:"CLOSE_PRICE"`
DEALAMOUNTRATIO float64 `json:"DEAL_AMOUNT_RATIO"`
DEALNETRATIO float64 `json:"DEAL_NET_RATIO"`
EXPLAIN string `json:"EXPLAIN"`
EXPLANATION string `json:"EXPLANATION"`
FREEMARKETCAP float64 `json:"FREE_MARKET_CAP"`
SECUCODE string `json:"SECUCODE" gorm:"index"`
SECURITYCODE string `json:"SECURITY_CODE"`
SECURITYNAMEABBR string `json:"SECURITY_NAME_ABBR"`
SECURITYTYPECODE string `json:"SECURITY_TYPE_CODE"`
TRADEDATE string `json:"TRADE_DATE" gorm:"index"`
TURNOVERRATE float64 `json:"TURNOVERRATE"`
}
func (l LongTigerRankData) TableName() string {
return "long_tiger_rank"
}
type TVNews struct {
Id string `json:"id"`
Title string `json:"title"`
Published int `json:"published"`
Urgency int `json:"urgency"`
Permission string `json:"permission"`
StoryPath string `json:"storyPath"`
Provider struct {
Id string `json:"id"`
Name string `json:"name"`
LogoId string `json:"logo_id"`
} `json:"provider"`
}
type XUEQIUHot struct {
Data struct {
Items []HotItem `json:"items"`
ItemsSize int `json:"items_size"`
} `json:"data"`
ErrorCode int `json:"error_code"`
ErrorDescription string `json:"error_description"`
}
type HotItem struct {
Type int `json:"type"`
Code string `json:"code"`
Name string `json:"name"`
Value float64 `json:"value"`
Increment int `json:"increment"`
RankChange int `json:"rank_change"`
HasExist interface{} `json:"has_exist"`
Symbol string `json:"symbol"`
Percent float64 `json:"percent"`
Current float64 `json:"current"`
Chg float64 `json:"chg"`
Exchange string `json:"exchange"`
StockType int `json:"stock_type"`
SubType string `json:"sub_type"`
Ad int `json:"ad"`
AdId interface{} `json:"ad_id"`
ContentId interface{} `json:"content_id"`
Page interface{} `json:"page"`
Model interface{} `json:"model"`
Location interface{} `json:"location"`
TradeSession interface{} `json:"trade_session"`
CurrentExt interface{} `json:"current_ext"`
PercentExt interface{} `json:"percent_ext"`
}
type HotEvent struct {
PicSize interface{} `json:"pic_size"`
Tag string `json:"tag"`
Id int `json:"id"`
Pic string `json:"pic"`
Hot int `json:"hot"`
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))
}

BIN
build/screenshot/img13.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

BIN
build/screenshot/img_12.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

BIN
build/screenshot/img_13.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

BIN
build/screenshot/img_14.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

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,24 +9,39 @@
"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",
"@vicons/ionicons5": "^0.13.0",
"date-fns": "^4.1.0",
"echarts": "^5.6.0",
"file-saver": "^2.0.5",
"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",
"@vicons/fluent": "^0.13.0",
"@vicons/ionicons4": "^0.13.0",
"@vicons/ionicons5": "^0.13.0",
"@vicons/material": "^0.13.0",
"@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": "^5.4.12"
"vite": "^6.3.5"
},
"keywords": [
"AI赋能股票分析",

View File

@@ -1 +1 @@
cf858d682535e094e087a036e009d7f8
b0b9f944d9af9c00b6d48234793db58c

View File

@@ -1,32 +1,56 @@
<script setup>
import {
EventsEmit,
EventsOff,
EventsOn,
Quit,
WindowFullscreen, WindowGetPosition,
WindowFullscreen,
WindowHide,
WindowSetPosition,
WindowUnfullscreen
} from '../wailsjs/runtime'
import {h, onBeforeMount, onMounted, ref} from "vue";
import { RouterLink } from 'vue-router'
import {darkTheme, NGradientText, NIcon, NText,} from 'naive-ui'
import {h, onBeforeMount, onBeforeUnmount, onMounted, ref} from "vue";
import {RouterLink, useRouter} from 'vue-router'
import {createDiscreteApi,darkTheme,lightTheme , NIcon, NText,NButton,dateZhCN,zhCN} from 'naive-ui'
import {
SettingsOutline,
AlarmOutline,
AnalyticsOutline,
BarChartSharp, Bonfire, BonfireOutline, EaselSharp,
ExpandOutline, Flag,
Flame, FlameSharp, InformationOutline,
LogoGithub,
NewspaperOutline,
NewspaperSharp, Notifications,
PowerOutline, Pulse,
ReorderTwoOutline,
ExpandOutline,
PowerOutline, LogoGithub, MoveOutline, WalletOutline, StarOutline, AlarmOutline, SparklesOutline,
SettingsOutline, Skull, SkullOutline, SkullSharp,
SparklesOutline,
StarOutline,
Wallet, WarningOutline,
} from '@vicons/ionicons5'
import {GetConfig} from "../wailsjs/go/main/App";
const enableNews= ref(false)
const contentStyle = ref("")
const enableDarkTheme = ref(true)
const content = ref('数据来源于网络,仅供参考;投资有风险,入市需谨慎')
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";
import {FireFilled, FireOutlined, NotificationFilled, StockOutlined} from "@vicons/antd";
const router = useRouter()
const loading = ref(true)
const loadingMsg = ref("加载数据中...")
const enableNews = ref(false)
const contentStyle = ref("")
const enableFund = ref(false)
const enableDarkTheme = ref(null)
const content = ref('未经授权,禁止商业目的!\n\n数据来源于网络,仅供参考;投资有风险,入市需谨慎')
const isFullscreen = ref(false)
const activeKey = ref('')
const containerRef= ref({})
const realtimeProfit= ref(0)
const telegraph= ref([])
const activeKey = ref('stock')
const containerRef = ref({})
const realtimeProfit = ref(0)
const telegraph = ref([])
const groupList = ref([])
const menuOptions = ref([
{
label: () =>
@@ -35,20 +59,337 @@ const menuOptions = ref([
{
to: {
name: 'stock',
params: {
query: {
groupName: '全部',
groupId: 0,
},
}
params: {},
},
onClick: () => {
activeKey.value = 'stock'
},
},
{ default: () => '股票自选',}
{default: () => '股票自选',}
),
key: 'stock',
icon: renderIcon(StarOutline),
children:[
children: [
{
label: ()=> h(NText, {type:realtimeProfit.value>0?'error':'success'},{ default: () => '当日盈亏 '+realtimeProfit.value+"¥"}),
key: 'realtimeProfit',
show: realtimeProfit.value,
icon: renderIcon(WalletOutline),
label: () =>
h(
'a',
{
href: '#',
type: 'info',
onClick: () => {
activeKey.value = 'stock'
//console.log("push",item)
router.push({
name: 'stock',
query: {
groupName: '全部',
groupId: 0,
},
})
EventsEmit("changeTab", {ID: 0, name: '全部'})
},
to: {
name: 'stock',
query: {
groupName: '全部',
groupId: 0,
},
}
},
{default: () => '全部',}
),
key: 0,
}
],
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
params: {}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '市场快讯'})
},
},
{default: () => '市场行情'}
),
key: 'market',
icon: renderIcon(NewspaperOutline),
children: [
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "市场快讯",
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '市场快讯'})
},
},
{default: () => '市场快讯',}
),
key: 'market1',
icon: renderIcon(NewspaperSharp),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "全球股指",
},
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '全球股指'})
},
},
{default: () => '全球股指',}
),
key: 'market2',
icon: renderIcon(BarChartSharp),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "重大指数",
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '重大指数'})
},
},
{default: () => '重大指数',}
),
key: 'market3',
icon: renderIcon(AnalyticsOutline),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "行业排名",
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '行业排名'})
},
},
{default: () => '行业排名',}
),
key: 'market4',
icon: renderIcon(Flag),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "个股资金流向",
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '个股资金流向'})
},
},
{default: () => '个股资金流向',}
),
key: 'market5',
icon: renderIcon(Pulse),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "龙虎榜",
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '龙虎榜'})
},
},
{default: () => '龙虎榜',}
),
key: 'market6',
icon: renderIcon(Dragon),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "个股研报",
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '个股研报'})
},
},
{default: () => '个股研报',}
),
key: 'market7',
icon: renderIcon(StockOutlined),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "公司公告",
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '公司公告'})
},
},
{default: () => '公司公告',}
),
key: 'market8',
icon: renderIcon(NotificationFilled),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "行业研究",
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '行业研究'})
},
},
{default: () => '行业研究',}
),
key: 'market9',
icon: renderIcon(ReportSearch),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "当前热门",
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '当前热门'})
},
},
{default: () => '当前热门',}
),
key: 'market10',
icon: renderIcon(Gripfire),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "指标选股",
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '指标选股'})
},
},
{default: () => '指标选股',}
),
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),
},
]
},
@@ -59,17 +400,22 @@ const menuOptions = ref([
{
to: {
name: 'fund',
params: {
query: {
name: '基金自选',
},
}
},
onClick: () => {
activeKey.value = 'fund'
},
},
{ default: () => '基金自选',}
{default: () => '基金自选',}
),
show: enableFund.value,
key: 'fund',
icon: renderIcon(SparklesOutline),
children:[
children: [
{
label: ()=> h(NText, {type:realtimeProfit.value>0?'error':'success'},{ default: () => '功能完善中!'}),
label: () => h(NText, {type: realtimeProfit.value > 0 ? 'error' : 'success'}, {default: () => '功能完善中!'}),
key: 'realtimeProfit',
show: realtimeProfit.value,
icon: renderIcon(AlarmOutline),
@@ -82,12 +428,36 @@ const menuOptions = ref([
RouterLink,
{
to: {
name: 'settings',
params: {
}
name: 'agent',
query: {
name:"Ai智能体",
},
onClick: () => {
activeKey.value = 'agent'
},
}
},
{ default: () => '设置' }
{default: () => 'Ai智能体'}
),
key: 'agent',
icon: renderIcon(Robot),
},
{
label: () =>
h(
RouterLink,
{
to: {
name: 'settings',
query: {
name:"设置",
},
onClick: () => {
activeKey.value = 'settings'
},
}
},
{default: () => '设置'}
),
key: 'settings',
icon: renderIcon(SettingsOutline),
@@ -99,30 +469,35 @@ const menuOptions = ref([
{
to: {
name: 'about',
params: {
query: {
name:"关于",
}
}
},
onClick: () => {
activeKey.value = 'about'
},
},
{ default: () => '关于' }
{default: () => '关于'}
),
key: 'about',
icon: renderIcon(LogoGithub),
},
{
label: ()=> h("a", {
show:false,
label: () => h("a", {
href: '#',
onClick: toggleFullscreen,
title: '全屏 Ctrl+F 退出全屏 Esc',
}, { default: () => isFullscreen.value?'取消全屏':'全屏' }),
}, {default: () => isFullscreen.value ? '取消全屏' : '全屏'}),
key: 'full',
icon: renderIcon(ExpandOutline),
},
{
label: ()=> h("a", {
label: () => h("a", {
href: '#',
onClick: WindowHide,
title: '隐藏到托盘区 Ctrl+H',
}, { default: () => '隐藏到托盘区' }),
title: '隐藏到托盘区 Ctrl+Z',
}, {default: () => '隐藏到托盘区'}),
key: 'hide',
icon: renderIcon(ReorderTwoOutline),
},
@@ -136,28 +511,32 @@ const menuOptions = ref([
// icon: renderIcon(MoveOutline),
// },
{
label: ()=> h("a", {
label: () => h("a", {
href: '#',
onClick: Quit,
}, { default: () => '退出程序' }),
}, {default: () => '退出程序'}),
key: 'exit',
icon: renderIcon(PowerOutline),
},
])
function renderIcon(icon) {
return () => h(NIcon, null, { default: () => h(icon) })
return () => h(NIcon, null, {default: () => h(icon)})
}
function toggleFullscreen(e) {
activeKey.value = 'full'
//console.log(e)
if (isFullscreen.value) {
WindowUnfullscreen()
//e.target.innerHTML = '全屏'
} else {
WindowFullscreen()
// e.target.innerHTML = '取消全屏'
}
isFullscreen.value=!isFullscreen.value
if (isFullscreen.value) {
WindowUnfullscreen()
//e.target.innerHTML = '全屏'
} else {
WindowFullscreen()
// e.target.innerHTML = '取消全屏'
}
isFullscreen.value = !isFullscreen.value
}
// const drag = ref(false)
// const lastPos= ref({x:0,y:0})
// function toggleStartMoveWindow(e) {
@@ -175,11 +554,29 @@ function toggleFullscreen(e) {
// }
// window.addEventListener('mousemove', dragstart)
EventsOn("realtime_profit",(data)=>{
realtimeProfit.value=data
EventsOn("realtime_profit", (data) => {
realtimeProfit.value = data
})
EventsOn("telegraph",(data)=>{
telegraph.value=data
EventsOn("telegraph", (data) => {
telegraph.value = data
})
EventsOn("loadingMsg", (data) => {
if(data==="done"){
loadingMsg.value = "加载完成..."
EventsEmit("loadingDone", "app")
loading.value = false
}else{
loading.value = true
loadingMsg.value = data
}
})
onBeforeUnmount(() => {
EventsOff("realtime_profit")
EventsOff("loadingMsg")
EventsOff("telegraph")
EventsOff("newsPush")
})
window.onerror = function (msg, source, lineno, colno, error) {
@@ -195,89 +592,173 @@ window.onerror = function (msg, source, lineno, colno, error) {
return true;
};
onBeforeMount(()=>{
// GetConfig().then((res)=>{
// console.log(res)
// if(res.darkTheme){
// enableDarkTheme.value=darkTheme
// }
// if(res.enableNews){
// enableNews.value=true
// }
// })
})
onMounted(()=>{
contentStyle.value="max-height: calc(90vh);overflow: hidden"
GetConfig().then((res)=>{
console.log(res)
if(res.darkTheme){
enableDarkTheme.value=darkTheme
}else{
enableDarkTheme.value=null
onBeforeMount(() => {
GetVersionInfo().then(result => {
if(result.officialStatement){
content.value = result.officialStatement+"\n\n"+content.value
}
if(res.enableNews){
enableNews.value=true
})
GetGroupList().then(result => {
groupList.value = result
menuOptions.value.map((item) => {
//console.log(item)
if (item.key === 'stock') {
item.children.push(...groupList.value.map(item => {
return {
label: () =>
h(
'a',
{
href: '#',
type: 'info',
onClick: () => {
//console.log("push",item)
router.push({
name: 'stock',
query: {
groupName: item.name,
groupId: item.ID,
},
})
setTimeout(() => {
EventsEmit("changeTab", item)
}, 100)
},
to: {
name: 'stock',
query: {
groupName: item.name,
groupId: item.ID,
},
}
},
{default: () => item.name,}
),
key: item.ID,
}
}))
}
})
})
GetConfig().then((res) => {
//console.log(res)
enableFund.value = res.enableFund
menuOptions.value.filter((item) => {
if (item.key === 'fund') {
item.show = res.enableFund
}
})
if (res.darkTheme) {
enableDarkTheme.value = darkTheme
} else {
enableDarkTheme.value = null
}
})
})
onMounted(() => {
contentStyle.value = "max-height: calc(92vh);overflow: hidden"
GetConfig().then((res) => {
if (res.enableNews) {
enableNews.value = true
}
enableFund.value = res.enableFund
const {notification } =createDiscreteApi(["notification"], {
configProviderProps: {
theme: enableDarkTheme.value ? darkTheme : lightTheme ,
max: 3,
},
})
EventsOn("newsPush", (data) => {
//console.log(data)
if(data.isRed){
notification.create({
//type:"error",
// avatar: () => h(NIcon,{component:Notifications,color:"red"}),
title: data.time,
content: () => h('div',{type:"error",style:{
"text-align":"left",
"font-size":"14px",
"color":"#f67979"
}}, { default: () => data.content }),
meta: () => h(NText,{type:"warning"}, { default: () => data.source}),
duration:1000*40,
})
}else{
notification.create({
//type:"info",
//avatar: () => h(NIcon,{component:Notifications}),
title: data.time,
content: () => h('div',{type:"info",style:{
"text-align":"left",
"font-size":"14px",
"color": data.source==="go-stock"?"#F98C24":"#549EC8"
}}, { default: () => data.content }),
meta: () => h(NText,{type:"warning"}, { default: () => data.source}),
duration:1000*30 ,
})
}
})
})
})
</script>
<template>
<n-config-provider ref="containerRef" :theme="enableDarkTheme" >
<n-message-provider >
<n-config-provider ref="containerRef" :theme="enableDarkTheme" :locale="zhCN" :date-locale="dateZhCN">
<n-message-provider>
<n-notification-provider>
<n-modal-provider>
<n-dialog-provider>
<n-watermark
:content="content"
cross
selectable
:font-size="16"
:line-height="16"
:width="500"
:height="400"
:x-offset="50"
:y-offset="150"
:rotate="-15"
>
<n-flex justify="center">
<n-grid x-gap="12" :cols="1">
<!--
<n-gi style="position: relative;top:1px;z-index: 19;width: 100%" v-if="telegraph.length>0">
</n-gi>
-->
<n-gi>
<n-marquee :speed="100" style="position: relative;top:0;z-index: 19;width: 100%" v-if="(telegraph.length>0)&&(enableNews)">
<n-tag type="warning" v-for="item in telegraph" style="margin-right: 10px">
{{item}}
</n-tag>
<!-- <n-text type="warning"> {{telegraph[0]}}</n-text>-->
</n-marquee>
<n-scrollbar :style="contentStyle">
<RouterView />
</n-scrollbar>
</n-gi>
<n-gi style="position: fixed;bottom:0;z-index: 9;width: 100%;">
<n-card size="small" style="--wails-draggable:drag">
<n-menu style="font-size: 18px;"
v-model:value="activeKey"
mode="horizontal"
:options="menuOptions"
responsive
/>
</n-card>
</n-gi>
</n-grid>
</n-flex>
</n-watermark>
</n-dialog-provider>
</n-modal-provider>
<n-modal-provider>
<n-dialog-provider>
<n-watermark
:content="content"
cross
selectable
:font-size="16"
:line-height="16"
:width="500"
:height="400"
:x-offset="50"
:y-offset="150"
:rotate="-15"
>
<n-flex>
<n-grid x-gap="12" :cols="1">
<n-gi>
<n-spin :show="loading">
<template #description>
{{ loadingMsg }}
</template>
<n-marquee :speed="100" style="position: relative;top:0;z-index: 19;width: 100%"
v-if="(telegraph.length>0)&&(enableNews)">
<n-tag type="warning" v-for="item in telegraph" style="margin-right: 10px">
{{ item }}
</n-tag>
</n-marquee>
<n-scrollbar :style="contentStyle">
<n-skeleton v-if="loading" height="calc(100vh)" />
<RouterView/>
</n-scrollbar>
</n-spin>
</n-gi>
<n-gi style="position: fixed;bottom:0;z-index: 9;width: 100%;">
<n-card size="small" style="--wails-draggable:no-drag">
<n-menu style="font-size: 18px;"
v-model:value="activeKey"
mode="horizontal"
:options="menuOptions"
responsive
/>
</n-card>
</n-gi>
</n-grid>
</n-flex>
</n-watermark>
</n-dialog-provider>
</n-modal-provider>
</n-notification-provider>
</n-message-provider>
</n-config-provider>

View File

@@ -0,0 +1,102 @@
<script setup lang="ts">
import {nextTick, onBeforeMount, onMounted, onUnmounted, ref} from 'vue'
import {ClsCalendar} from "../../wailsjs/go/main/App";
import { addMonths, format ,parse} from 'date-fns';
import { zhCN } from 'date-fns/locale';
import {useMessage} from 'naive-ui'
import {Star48Filled} from "@vicons/fluent";
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0'); // 月份从0开始需要+1
const day = String(today.getDate()).padStart(2, '0');
// 常见格式YYYY-MM-DD
const formattedDate = `${year}-${month}-${day}`;
const formattedYM = `${year}-${month}`;
const list = ref([])
const message=useMessage()
function goBackToday() {
setTimeout(() => {
nextTick(
() => {
const elementById = document.getElementById(formattedDate);
if (elementById) {
elementById.scrollIntoView({
behavior: 'auto',
block: 'start'
})
}
}
)
}, 500)
}
onBeforeMount(() => {
ClsCalendar().then(res => {
list.value = res
goBackToday();
})
})
function getweekday(date){
let day=parse(date, 'yyyy-MM-dd', new Date())
return format(day, 'EEEE', {locale: zhCN})
}
</script>
<template>
<!-- <n-timeline size="large" style="text-align: left">-->
<!-- <n-timeline-item v-for="item in list" :key="item.date" :title="item.date" type="info" >-->
<!-- <n-list>-->
<!-- <n-list-item v-for="l in item.list" :key="l.article_id ">-->
<!-- <n-text>{{l.title}}</n-text>-->
<!-- </n-list-item>-->
<!-- </n-list>-->
<!-- </n-timeline-item>-->
<!-- </n-timeline>-->
<n-list bordered style="max-height: calc(100vh - 230px);text-align: left;">
<n-scrollbar style="max-height: calc(100vh - 230px);" >
<n-list-item v-for="(item, index) in list" :id="item.calendar_day" :key="item.calendar_day">
<n-thing :title="item.calendar_day +' '+item.week">
<n-list :bordered="false" hoverable>
<n-list-item v-for="(l,i ) in item.items" :key="l.id ">
<n-flex justify="space-between">
<n-text :type="item.calendar_day===formattedDate?'warning':'info'">{{i+1}}# {{l.title}}
<n-tag v-if="l.event" size="small" round type="success">事件</n-tag>
<n-tag v-if="l.economic" size="small" round type="error">数据</n-tag>
</n-text>
<n-rate v-if="l.event&&(l.event.star>0)" readonly :default-value="l.event.star">
<n-icon :component="Star48Filled"/>
</n-rate>
<n-rate v-if="l.economic&&(l.economic.star>0)" readonly :default-value="l.economic.star" >
<n-icon :component="Star48Filled"/>
</n-rate>
</n-flex>
<n-flex v-if="l.economic">
<n-tag type="warning" :bordered="false" :size="'small'">公布{{l.economic.actual }}</n-tag>
<n-tag type="warning" :bordered="false" :size="'small'">预测{{l.economic.consensus}}</n-tag>
<n-tag type="warning" :bordered="false" :size="'small'">前值{{l.economic.front}}</n-tag>
</n-flex>
</n-list-item>
</n-list>
</n-thing>
</n-list-item>
<n-list-item v-if="list.length==0">
<n-text type="info">没有数据</n-text>
</n-list-item>
<n-list-item v-else style="text-align: center;">
<n-button-group>
<n-button strong secondary type="warning" @click="goBackToday">回到今天</n-button>
</n-button-group>
</n-list-item>
</n-scrollbar>
</n-list>
</template>
<style scoped>
</style>

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

@@ -0,0 +1,37 @@
<script setup lang="ts">
import {onBeforeMount, onUnmounted, ref} from 'vue'
import {HotEvent} from "../../wailsjs/go/main/App";
const list = ref([])
const task =ref()
onBeforeMount(async () => {
list.value = await HotEvent(50)
task.value=setInterval(async ()=>{
list.value = await HotEvent(50)
}, 1000*10)
})
onUnmounted(async ()=>{
clearInterval(task.value)
})
</script>
<template>
<n-list bordered>
<template #header>
雪球热门
</template>
<n-list-item v-for="(item, index) in list" :key="index">
<n-thing :title="item.tag" :description="item.content" >
<template v-if="item.pic" #avatar>
<n-avatar :src="item.pic" :size="60">
</n-avatar>
</template>
</n-thing>
</n-list-item>
</n-list>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,88 @@
<script setup lang="ts">
import {onBeforeMount, onUnmounted, ref} from 'vue'
import {HotStock} from "../../wailsjs/go/main/App";
import KLineChart from "./KLineChart.vue";
import {ArrowBack, ArrowDown, ArrowUp} from "@vicons/ionicons5";
const {marketType}=defineProps(
{
marketType: {
type: String,
default: '10'
}
}
)
const task =ref()
const list = ref([])
onBeforeMount(async () => {
list.value = await HotStock(marketType)
task.value = setInterval(async () => {
list.value = await HotStock(marketType)
}, 5000)
})
onUnmounted(()=>{
clearInterval(task.value)
})
function getMarketCode(item) {
if (item.exchange === 'SZ') {
return item.code.toLowerCase()
}
if (item.exchange === 'SH') {
return item.code.toLowerCase()
}
if (item.exchange === 'HK') {
return (item.exchange + item.code).toLowerCase()
}
return ("gb_"+item.code).toLowerCase()
}
</script>
<template>
<n-table striped size="small">
<n-thead>
<n-tr>
<n-th>股票名称</n-th>
<n-th>涨跌幅</n-th>
<n-th>当前价格</n-th>
<n-th>热度</n-th>
<n-th>热度变化</n-th>
<n-th>排名变化</n-th>
</n-tr>
</n-thead>
<n-tbody>
<n-tr v-for="item in list" :key="item.code">
<n-td><n-text type="info">
<n-popover trigger="hover" placement="right">
<template #trigger>
<n-tag type="info" :bordered="false"> {{item.name}} {{item.code}}</n-tag>
</template>
<k-line-chart style="width: 800px" :code="getMarketCode(item)" :chart-height="500" :name="item.name" :k-days="20" :dark-theme="true"></k-line-chart>
</n-popover>
</n-text></n-td>
<n-td><n-text :type="item.percent>0?'error':'success'">{{item.percent}}%</n-text></n-td>
<n-td><n-text type="info">{{item.current}}</n-text></n-td>
<n-td><n-text type="info">{{item.value}}</n-text></n-td>
<n-td><n-text :type="item.increment>0?'error':'success'">
{{item.increment}}
<n-icon v-if="item.increment>0" :component="ArrowUp"/>
<n-icon v-else :component="ArrowDown"/>
</n-text></n-td>
<n-td>
<n-text :type="item.rank_change>0?'error':'success'">
{{item.rank_change}}
<n-icon v-if="item.rank_change>0" :component="ArrowUp"/>
<n-text v-else-if="item.rank_change==0" ></n-text>
<n-icon v-else :component="ArrowDown"/>
</n-text>
</n-td>
</n-tr>
</n-tbody>
</n-table>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,83 @@
<script setup lang="ts">
import {onBeforeMount, onUnmounted, ref} from 'vue'
import {HotTopic, OpenURL} from "../../wailsjs/go/main/App";
import {Environment} from "../../wailsjs/runtime";
const list = ref([])
const task =ref()
onBeforeMount(async () => {
list.value = await HotTopic(10)
setInterval(async ()=>{
list.value = await HotTopic(10)
}, 1000*10)
})
onUnmounted(()=>{
clearInterval(task.value)
})
function openCenteredWindow(url, width, height) {
const left = (window.screen.width - width) / 2;
const top = (window.screen.height - height) / 2;
Environment().then(env => {
switch (env.platform) {
case 'windows':
window.open(
url,
'centeredWindow',
`width=${width},height=${height},left=${left},top=${top}`
)
break
default:
OpenURL(url)
break
}
})
}
function showPage(htid) {
openCenteredWindow(`https://gubatopic.eastmoney.com/topic_v3.html?htid=${htid}`, 1000, 600)
}
</script>
<template>
<n-list bordered hoverable clickable>
<!-- <template #header>-->
<!-- 股吧热门-->
<!-- </template>-->
<n-list-item v-for="(item, index) in list" :key="index">
<n-thing :title="item.nickname" :description="item.desc" :description-style="'font-size: 14px;'" @click="showPage(item.htid)">
<template v-if="item.squareImg" #avatar>
<n-avatar :src="item.squareImg" :size="60">
</n-avatar>
</template>
<template v-if="item.stock_list" #footer>
<n-flex>
<n-tag type="info" v-for="(v, i) in item.stock_list" :bordered="false" size="small">
{{v.name}}
</n-tag>
</n-flex>
</template>
<template v-if="item.clickNumber" #header-extra>
<n-flex>
<n-button secondary type="warning" size="tiny">讨论数<n-number-animation
show-separator
:from="0"
:to="item.postNumber"
/>
</n-button >
<n-tag :bordered="false" type="warning" size="small">浏览量<n-number-animation
show-separator
:from="0"
:to="item.clickNumber"
/>
</n-tag>
</n-flex>
</template>
</n-thing>
</n-list-item>
</n-list>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,115 @@
<script setup>
import {onBeforeMount, ref} from 'vue'
import {GetStockList, IndustryResearchReport,EMDictCode} from "../../wailsjs/go/main/App";
import {ArrowDownOutline, CaretDown, CaretUp, PulseOutline, Refresh, RefreshCircleSharp,} from "@vicons/ionicons5";
import {useMessage} from "naive-ui";
import {BrowserOpenURL} from "../../wailsjs/runtime";
const message=useMessage()
const list = ref([])
const options = ref([])
function getIndustryResearchReport(value) {
message.loading("正在刷新数据...")
IndustryResearchReport(value).then(result => {
console.log(result)
list.value = result
})
}
onBeforeMount(()=>{
getIndustryResearchReport('');
})
function ratingChangeName(ratingChange){
if(ratingChange===0){
return '调高'
}else if(ratingChange===1){
return '调低'
}else if(ratingChange===2){
return '首次'
}else if(ratingChange===3){
return '维持'
}else if (ratingChange===4){
return '无变化'
}else{
return ''
}
}
function openWin(code) {
BrowserOpenURL("https://pdf.dfcfw.com/pdf/H3_"+code+"_1.pdf?1749744888000.pdf")
}
function EMDictCodeList(keyVal){
if (keyVal){
EMDictCode('016').then(result => {
console.log(result)
options.value=result.filter((value,index,array) => value.bkName.includes(keyVal)||value.firstLetter.includes(keyVal)||value.bkCode.includes(keyVal)).map(item => {
return {
label: item.bkName+" - "+item.bkCode,
value: item.bkCode
}
})
})
}else{
getIndustryResearchReport('')
}
}
function handleSearch(value) {
getIndustryResearchReport(value)
}
</script>
<template>
<n-card>
<n-auto-complete :options="options" placeholder="请输入行业名称关键词搜索" clearable filterable :on-select="handleSearch" :on-update:value="EMDictCodeList" />
</n-card>
<n-table striped size="small">
<n-thead>
<n-tr>
<!-- <n-th>代码</n-th>-->
<!-- <n-th>名称</n-th>-->
<n-th>行业</n-th>
<n-th>标题</n-th>
<n-th>东财评级</n-th>
<n-th>评级变动</n-th>
<n-th>机构评级</n-th>
<n-th>分析师</n-th>
<n-th>机构</n-th>
<n-th> <n-flex justify="space-between">日期<n-icon @click="getIndustryResearchReport" color="#409EFF" :size="20" :component="RefreshCircleSharp"/></n-flex></n-th>
</n-tr>
</n-thead>
<n-tbody>
<n-tr v-for="item in list" :key="item.infoCode">
<!-- <n-td>{{item.stockCode}}</n-td>-->
<!-- <n-td :title="item.stockCode">-->
<!-- <n-popover trigger="hover" placement="right">-->
<!-- <template #trigger>-->
<!-- <n-tag type="info" :bordered="false">{{item.stockName}}</n-tag>-->
<!-- </template>-->
<!-- <k-line-chart style="width: 800px" :code="getmMarketCode(item.market,item.stockCode)" :chart-height="500" :name="item.stockName" :k-days="20" :dark-theme="true"></k-line-chart>-->
<!-- </n-popover>-->
<!-- </n-td>-->
<n-td><n-tag type="info" :bordered="false">{{item.industryName}}</n-tag></n-td>
<n-td>
<n-a type="info" @click="openWin(item.infoCode)"><n-text type="success">{{item.title}}</n-text></n-a>
</n-td>
<n-td><n-text :type="item.emRatingName==='增持'?'error':'info'">
{{item.emRatingName}}
</n-text></n-td>
<n-td><n-text :type="item.ratingChange===0?'error':'info'">{{ratingChangeName(item.ratingChange)}}</n-text></n-td>
<n-td>{{item.sRatingName }}</n-td>
<n-td><n-ellipsis style="max-width: 120px">{{item.researcher}}</n-ellipsis></n-td>
<n-td>{{item.orgSName}}</n-td>
<n-td>{{item.publishDate.substring(0,10)}}</n-td>
</n-tr>
</n-tbody>
</n-table>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,108 @@
<script setup lang="ts">
import {nextTick, onBeforeMount, onMounted, onUnmounted, ref} from 'vue'
import {InvestCalendarTimeLine} from "../../wailsjs/go/main/App";
import { addMonths, format ,parse} from 'date-fns';
import { zhCN } from 'date-fns/locale';
import {useMessage} from 'naive-ui'
import {Star48Filled} from "@vicons/fluent";
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0'); // 月份从0开始需要+1
const day = String(today.getDate()).padStart(2, '0');
// 常见格式YYYY-MM-DD
const formattedDate = `${year}-${month}-${day}`;
const formattedYM = `${year}-${month}`;
const list = ref([])
const message=useMessage()
function goBackToday() {
setTimeout(() => {
nextTick(
() => {
const elementById = document.getElementById(formattedDate);
if (elementById) {
elementById.scrollIntoView({
behavior: 'auto',
block: 'start'
})
}
}
)
}, 500)
}
onBeforeMount(() => {
InvestCalendarTimeLine(formattedYM).then(res => {
list.value = res
goBackToday();
})
})
onMounted(()=>{
})
function loadMore(){
if (list.value.length>0){
let day=parse(list.value[list.value.length-1].date, 'yyyy-MM-dd', new Date())
let nextMonth=addMonths(day,1)
let ym = format(nextMonth, 'yyyy-MM');
console.log(ym)
InvestCalendarTimeLine(ym).then(res => {
if (res.length==0){
message.warning("没有更多数据了")
return
}
list.value.push( ...res)
})
}
}
function getweekday(date){
let day=parse(date, 'yyyy-MM-dd', new Date())
return format(day, 'EEEE', {locale: zhCN})
}
</script>
<template>
<!-- <n-timeline size="large" style="text-align: left">-->
<!-- <n-timeline-item v-for="item in list" :key="item.date" :title="item.date" type="info" >-->
<!-- <n-list>-->
<!-- <n-list-item v-for="l in item.list" :key="l.article_id ">-->
<!-- <n-text>{{l.title}}</n-text>-->
<!-- </n-list-item>-->
<!-- </n-list>-->
<!-- </n-timeline-item>-->
<!-- </n-timeline>-->
<n-list bordered style="max-height: calc(100vh - 230px);text-align: left;">
<n-scrollbar style="max-height: calc(100vh - 230px);" >
<n-list-item v-for="(item, index) in list" :id="item.date" :key="item.date">
<n-thing :title="item.date+' '+getweekday(item.date)">
<n-list :bordered="false" hoverable>
<n-list-item v-for="(l,i ) in item.list" :key="l.article_id ">
<n-flex justify="space-between">
<n-text :type="item.date===formattedDate?'warning':'info'">{{i+1}}# {{l.title}}</n-text>
<n-rate v-if="l.like_count>0" readonly :default-value="l.like_count" :count="l.like_count" >
<n-icon :component="Star48Filled"/>
</n-rate>
</n-flex>
</n-list-item>
</n-list>
</n-thing>
</n-list-item>
<n-list-item v-if="list.length==0">
<n-text type="info">没有数据</n-text>
</n-list-item>
<n-list-item v-else style="text-align: center;">
<n-button-group>
<n-button strong secondary type="info" @click="loadMore">加载更多</n-button>
<n-button strong secondary type="warning" @click="goBackToday">回到今天</n-button>
</n-button-group>
</n-list-item>
</n-scrollbar>
</n-list>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,386 @@
<script setup>
import {GetStockKLine} from "../../wailsjs/go/main/App";
import * as echarts from "echarts";
import {onMounted, ref} from "vue";
import _ from "lodash";
const { code,name,darkTheme,kDays ,chartHeight} = defineProps({
code: {
type: String,
default: ''
},
name: {
type: String,
default: ''
},
kDays: {
type: Number,
default: 14
},
chartHeight: {
type: Number,
default: 500
},
darkTheme: {
type: Boolean,
default: false
}
})
const upColor = '#ec0000';
const upBorderColor = '';
const downColor = '#00da3c';
const downBorderColor = '';
const kLineChartRef = ref(null);
onMounted(() => {
handleKLine(code,name)
})
function handleKLine(code,name){
GetStockKLine(code,name,365).then(result => {
//console.log("GetStockKLine",result)
const chart = echarts.init(kLineChartRef.value);
const categoryData = [];
const values = [];
const volumns=[];
for (let i = 0; i < result.length; i++) {
let resultElement=result[i]
//console.log("resultElement:{}",resultElement)
categoryData.push(resultElement.day)
let flag=resultElement.close>resultElement.open?1:-1
values.push([
resultElement.open,
resultElement.close,
resultElement.low,
resultElement.high
])
volumns.push([i,resultElement.volume/10000,flag])
}
////console.log("categoryData",categoryData)
////console.log("values",values)
let option = {
title: {
text: name+" "+code,
left: '20px',
textStyle: {
color: darkTheme?'#ccc':'#456'
}
},
darkMode: darkTheme,
//backgroundColor: '#1c1c1c',
// color:['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc'],
animation: false,
legend: {
right: 20,
top: 0,
data: ['日K', 'MA5', 'MA10', 'MA20', 'MA30'],
textStyle: {
color: darkTheme?'#ccc':'#456'
},
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
lineStyle: {
color: '#376df4',
width: 2,
opacity: 1
}
},
borderWidth: 2,
borderColor: darkTheme?'#456':'#ccc',
backgroundColor: darkTheme?'#456':'#fff',
padding: 10,
textStyle: {
color: darkTheme?'#ccc':'#456'
},
formatter: function (params) {//修改鼠标划过显示为中文
//console.log("params",params)
let currentItemData = _.filter(params, (param) => param.seriesIndex === 0)[0].data;
let ma5=_.filter(params, (param) => param.seriesIndex === 1)[0].data;//ma5的值
let ma10=_.filter(params, (param) => param.seriesIndex === 2)[0].data;//ma10的值
let ma20=_.filter(params, (param) => param.seriesIndex === 3)[0].data;//ma20的值
let ma30=_.filter(params, (param) => param.seriesIndex === 4)[0].data;//ma30的值
let volum=_.filter(params, (param) => param.seriesIndex === 5)[0].data;
return _.filter(params, (param) => param.seriesIndex === 0)[0].name + '<br>' +
'开盘:' + currentItemData[1] + '<br>' +
'收盘:' + currentItemData[2] + '<br>' +
'最低:' + currentItemData[3] + '<br>' +
'最高:' + currentItemData[4] + '<br>' +
'成交量(万手):' + volum[1] + '<br>' +
'MA5日均线:' + ma5 + '<br>' +
'MA10日均线:' + ma10 + '<br>' +
'MA20日均线:' + ma20 + '<br>' +
'MA30日均线:' + ma30
}
},
axisPointer: {
link: [
{
xAxisIndex: 'all'
}
],
label: {
backgroundColor: '#888'
}
},
visualMap: {
show: false,
seriesIndex: 5,
dimension: 2,
pieces: [
{
value: -1,
color: downColor
},
{
value: 1,
color: upColor
}
]
},
grid: [
{
left: '8%',
right: '8%',
height: '50%',
},
{
left: '8%',
right: '8%',
top: '66%',
height: '15%'
}
],
xAxis: [
{
type: 'category',
data: categoryData,
boundaryGap: false,
axisLine: { onZero: false },
splitLine: { show: false },
min: 'dataMin',
max: 'dataMax',
axisPointer: {
z: 100
}
},
{
type: 'category',
gridIndex: 1,
data: categoryData,
boundaryGap: false,
axisLine: { onZero: false },
axisTick: { show: false },
splitLine: { show: false },
axisLabel: { show: false },
min: 'dataMin',
max: 'dataMax'
}
],
yAxis: [
{
scale: true,
splitArea: {
show: true
}
},
{
scale: true,
gridIndex: 1,
splitNumber: 2,
axisLabel: { show: false },
axisLine: { show: false },
axisTick: { show: false },
splitLine: { show: false }
}
],
dataZoom: [
{
type: 'inside',
xAxisIndex: [0, 1],
start: 100-kDays,
end: 100
},
{
show: true,
xAxisIndex: [0, 1],
type: 'slider',
top: '85%',
start: 100-kDays,
end: 100
}
],
series: [
{
name: '日K',
type: 'candlestick',
data: values,
itemStyle: {
color: upColor,
color0: downColor,
// borderColor: upBorderColor,
// borderColor0: downBorderColor
},
markPoint: {
//symbol: 'none',
label: {
formatter: function (param) {
return param != null ? param.value + '' : '';
}
},
data: [
{
name: '最高',
type: 'max',
valueDim: 'highest'
},
{
name: '最低',
type: 'min',
valueDim: 'lowest'
},
{
name: '平均收盘价',
type: 'average',
valueDim: 'close'
}
],
tooltip: {
formatter: function (param) {
return param.name + '<br>' + (param.data.coord || '');
}
}
},
markLine: {
symbol: ['none', 'none'],
data: [
[
{
name: 'from lowest to highest',
type: 'min',
valueDim: 'lowest',
symbol: 'circle',
symbolSize: 10,
label: {
show: false
},
emphasis: {
label: {
show: false
}
}
},
{
type: 'max',
valueDim: 'highest',
symbol: 'circle',
symbolSize: 10,
label: {
show: false
},
emphasis: {
label: {
show: false
}
}
}
],
{
name: 'min line on close',
type: 'min',
valueDim: 'close'
},
{
name: 'max line on close',
type: 'max',
valueDim: 'close'
}
]
}
},
{
name: 'MA5',
type: 'line',
data: calculateMA(5,values),
smooth: true,
showSymbol: false,
lineStyle: {
opacity: 0.6
}
},
{
name: 'MA10',
type: 'line',
data: calculateMA(10,values),
smooth: true,
showSymbol: false,
lineStyle: {
opacity: 0.6
}
},
{
name: 'MA20',
type: 'line',
data: calculateMA(20,values),
smooth: true,
showSymbol: false,
lineStyle: {
opacity: 0.6
}
},
{
name: 'MA30',
type: 'line',
data: calculateMA(30,values),
smooth: true,
showSymbol: false,
lineStyle: {
opacity: 0.6
}
},
{
name: '成交量(手)',
type: 'bar',
xAxisIndex: 1,
yAxisIndex: 1,
itemStyle: {
color: '#7fbe9e'
},
data: volumns
}
]
};
chart.setOption(option);
chart.on('click',{seriesName:'日K'}, function(params) {
//console.log("click:",params);
});
})
}
function calculateMA(dayCount,values) {
var result = [];
for (var i = 0, len = values.length; i < len; i++) {
if (i < dayCount) {
result.push('-');
continue;
}
var sum = 0;
for (var j = 0; j < dayCount; j++) {
sum += +values[i - j][1];
}
result.push((sum / dayCount).toFixed(2));
}
return result;
}
</script>
<template>
<div ref="kLineChartRef" style="width: 100%;height: auto;--wails-draggable:no-drag" :style="{height:chartHeight+'px'}" ></div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,229 @@
<script setup lang="ts">
import {onBeforeMount, ref} from 'vue'
import {LongTigerRank} from "../../wailsjs/go/main/App";
import {BrowserOpenURL} from "../../wailsjs/runtime";
import {ArrowDownOutline} from "@vicons/ionicons5";
import _ from "lodash";
import KLineChart from "./KLineChart.vue";
import MoneyTrend from "./moneyTrend.vue";
import {NButton, NText, useMessage} from "naive-ui";
const message = useMessage()
const lhbList= ref([])
const EXPLANATIONs = ref([])
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0'); // 月份从0开始需要+1
const day = String(today.getDate()).padStart(2, '0');
// 常见格式YYYY-MM-DD
const formattedDate = `${year}-${month}-${day}`;
const SearchForm= ref({
dateValue: formattedDate,
EXPLANATION:null,
})
onBeforeMount(() => {
longTiger(formattedDate);
})
function longTiger_old(date) {
if(date) {
SearchForm.value.dateValue = date
}
let loading1=message.loading("正在获取龙虎榜数据...",{
duration: 0,
})
LongTigerRank(date).then(res => {
lhbList.value = res
loading1.destroy()
if (res.length === 0) {
message.info("暂无数据,请切换日期")
}
EXPLANATIONs.value=_.uniqBy(_.map(lhbList.value,function (item){
return {
label: item['EXPLANATION'],
value: item['EXPLANATION'],
}
}),'label');
})
}
function longTiger(date) {
if (date) {
SearchForm.value.dateValue = date;
}
let loading1 = message.loading("正在获取龙虎榜数据...", {
duration: 0,
});
const fetchDate = (currentDate, retryCount = 0) => {
if (retryCount > 7) { // 防止无限循环最多尝试7次
lhbList.value = [];
EXPLANATIONs.value = [];
loading1.destroy();
message.info("暂无历史数据");
return;
}
LongTigerRank(currentDate).then(res => {
if (res.length === 0) {
const previousDate = new Date(currentDate);
previousDate.setDate(previousDate.getDate() - 1);
const year = previousDate.getFullYear();
const month = String(previousDate.getMonth() + 1).padStart(2, '0');
const day = String(previousDate.getDate()).padStart(2, '0');
const prevFormattedDate = `${year}-${month}-${day}`;
message.info(`当前日期 ${currentDate} 暂无数据,尝试查询前一日:${prevFormattedDate}`);
SearchForm.value.dateValue = prevFormattedDate;
fetchDate(prevFormattedDate, retryCount + 1); // 递归调用
} else {
lhbList.value = res;
loading1.destroy();
EXPLANATIONs.value = _.uniqBy(_.map(lhbList.value, function (item) {
return {
label: item['EXPLANATION'],
value: item['EXPLANATION'],
};
}), 'label');
}
}).catch(err => {
loading1.destroy();
message.error("获取数据失败,请重试");
console.error(err);
});
};
fetchDate(date || formattedDate);
}
function handleEXPLANATION(value, option){
SearchForm.value.EXPLANATION = value
if(value){
LongTigerRank(SearchForm.value.dateValue).then(res => {
lhbList.value=_.filter(res, function(o) { return o['EXPLANATION']===value; });
if (res.length === 0) {
message.info("暂无数据,请切换日期")
}
})
}else{
longTiger(SearchForm.value.dateValue)
}
}
</script>
<template>
<n-form :model="SearchForm" >
<n-grid :cols="24" :x-gap="24">
<n-form-item-gi :span="4" label="日期" path="dateValue" label-placement="left">
<n-date-picker v-model:formatted-value="SearchForm.dateValue"
value-format="yyyy-MM-dd" type="date" :on-update:value="(v,v2)=>longTiger(v2)"/>
</n-form-item-gi>
<n-form-item-gi :span="8" label="上榜原因" path="EXPLANATION" label-placement="left">
<n-select clearable placeholder="上榜原因过滤" v-model:value="SearchForm.EXPLANATION" :options="EXPLANATIONs" :on-update:value="handleEXPLANATION"/>
</n-form-item-gi>
<n-form-item-gi :span="10" label="" label-placement="left">
<n-text type="error">*当天的龙虎榜数据通常在收盘结束后一小时左右更新</n-text>
</n-form-item-gi>
</n-grid>
</n-form>
<n-table :single-line="false" striped>
<n-thead>
<n-tr>
<n-th>代码</n-th>
<!-- <n-th width="90px">日期</n-th>-->
<n-th width="60px">名称</n-th>
<n-th>收盘价</n-th>
<n-th width="60px">涨跌幅</n-th>
<n-th>龙虎榜净买额()</n-th>
<n-th>龙虎榜买入额()</n-th>
<n-th>龙虎榜卖出额()</n-th>
<n-th>龙虎榜成交额()</n-th>
<!-- <n-th>市场总成交额()</n-th>-->
<!-- <n-th>净买额占总成交比</n-th>-->
<!-- <n-th>成交额占总成交比</n-th>-->
<n-th width="60px" data-field="TURNOVERRATE">换手率<n-icon :component="ArrowDownOutline" /></n-th>
<n-th>流通市值(亿)</n-th>
<n-th>上榜原因</n-th>
<!-- <n-th>解读</n-th>-->
</n-tr>
</n-thead>
<n-tbody>
<n-tr v-for="(item, index) in lhbList" :key="index">
<n-td>
<n-tag :bordered=false type="info">{{ item.SECUCODE.split('.')[1].toLowerCase()+item.SECUCODE.split('.')[0] }}</n-tag>
</n-td>
<!-- <n-td>
{{item.TRADE_DATE.substring(0,10)}}
</n-td>-->
<n-td>
<!-- <n-text :type="item.CHANGE_RATE>0?'error':'success'">{{ item.SECURITY_NAME_ABBR }}</n-text>-->
<n-popover trigger="hover" placement="right">
<template #trigger>
<n-button tag="a" text :type="item.CHANGE_RATE>0?'error':'success'" :bordered=false >{{ item.SECURITY_NAME_ABBR }}</n-button>
</template>
<k-line-chart style="width: 800px" :code="item.SECUCODE.split('.')[1].toLowerCase()+item.SECUCODE.split('.')[0]" :chart-height="500" :name="item.SECURITY_NAME_ABBR" :k-days="20" :dark-theme="true"></k-line-chart>
</n-popover>
</n-td>
<n-td>
<n-text :type="item.CHANGE_RATE>0?'error':'success'">{{ item.CLOSE_PRICE }}</n-text>
</n-td>
<n-td>
<n-text :type="item.CHANGE_RATE>0?'error':'success'">{{ (item.CHANGE_RATE).toFixed(2) }}%</n-text>
</n-td>
<n-td>
<!-- <n-text :type="item.BILLBOARD_NET_AMT>0?'error':'success'">{{ (item.BILLBOARD_NET_AMT/10000).toFixed(2) }}</n-text>-->
<n-popover trigger="hover" placement="right">
<template #trigger>
<n-button tag="a" text :type="item.BILLBOARD_NET_AMT>0?'error':'success'" :bordered=false >{{ (item.BILLBOARD_NET_AMT/10000).toFixed(2) }}</n-button>
</template>
<money-trend :code="item.SECUCODE.split('.')[1].toLowerCase()+item.SECUCODE.split('.')[0]" :name="item.SECURITY_NAME_ABBR" :days="360" :dark-theme="true" :chart-height="500" style="width: 800px"></money-trend>
</n-popover>
</n-td>
<n-td>
<n-text :type="'error'">{{ (item.BILLBOARD_BUY_AMT/10000).toFixed(2) }}</n-text>
</n-td>
<n-td>
<n-text :type="'success'">{{ (item.BILLBOARD_SELL_AMT/10000).toFixed(2) }}</n-text>
</n-td>
<n-td>
<n-text :type="'info'">{{ (item.BILLBOARD_DEAL_AMT/10000).toFixed(2) }}</n-text>
</n-td>
<!-- <n-td>-->
<!-- <n-text :type="'info'">{{ (item.ACCUM_AMOUNT/10000).toFixed(2) }}</n-text>-->
<!-- </n-td>-->
<!-- <n-td>-->
<!-- <n-text :type="item.DEAL_NET_RATIO>0?'error':'success'">{{ (item.DEAL_NET_RATIO).toFixed(2) }}%</n-text>-->
<!-- </n-td>-->
<!-- <n-td>-->
<!-- <n-text :type="'info'">{{ (item.DEAL_AMOUNT_RATIO).toFixed(2) }}%</n-text>-->
<!-- </n-td>-->
<n-td>
<n-text :type="'info'">{{ (item.TURNOVERRATE).toFixed(2) }}%</n-text>
</n-td>
<n-td>
<n-text :type="'info'">{{ (item.FREE_MARKET_CAP/100000000).toFixed(2) }}</n-text>
</n-td>
<n-td>
<n-text :type="'info'">{{ item.EXPLANATION }}</n-text>
</n-td>
<!-- <n-td>
<n-text :type="item.CHANGE_RATE>0?'error':'success'">{{ item.EXPLAIN }}</n-text>
</n-td>-->
</n-tr>
</n-tbody>
</n-table>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,293 @@
<script setup lang="ts">
import {h, onBeforeMount, onMounted, onUnmounted, ref} from 'vue'
import {SearchStock, GetHotStrategy, OpenURL, Follow, GetFollowList} from "../../wailsjs/go/main/App";
import {useMessage, NText, NTag, NButton} from 'naive-ui'
import {Environment} from "../../wailsjs/runtime"
import {RefreshCircleSharp} from "@vicons/ionicons5";
import {EventsEmit} from "../../wailsjs/runtime";
const message = useMessage()
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) {
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,
resizable: true,
minWidth: 200,
ellipsis: {
tooltip: true
},
children: item.children.filter(item => !item.hiddenNeed).map(item => {
return {
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 {
return {
title: item.title + (item.unit ? '[' + item.unit + ']' : ''),
key: item.key,
resizable: true,
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'
}
},
}
}
})
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(() => {
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-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}` })
}
if (isNumeric(value)) {
let type='info';
if (Number(value)<0){
type='success';
}
if(Number(value)>=0&&Number(value)<=5){
type='warning';
}
if (Number(value)>5){
type='error';
}
return h(NText, { type: type }, { default: () => `${value}` })
}else{
if(column.key=='SECURITY_SHORT_NAME'){
return h(NButton, { type: 'info',bordered: false ,size:'small',onClick:()=>{
//https://quote.eastmoney.com/sz300558.html#fullScreenChart
openCenteredWindow(`https://quote.eastmoney.com/${rowData.MARKET_SHORT_NAME}${rowData.SECURITY_CODE}.html#fullScreenChart`,1240,700)
}}, { default: () => `${value}` })
}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>
</style>

View File

@@ -0,0 +1,148 @@
<script setup lang="ts">
import {onBeforeMount, ref} from 'vue'
import {GetStockList, StockNotice} from "../../wailsjs/go/main/App";
import {BrowserOpenURL} from "../../wailsjs/runtime";
import {RefreshCircleSharp} from "@vicons/ionicons5";
import _ from "lodash";
import KLineChart from "./KLineChart.vue";
import MoneyTrend from "./moneyTrend.vue";
import {useMessage} from "naive-ui";
const {stockCode}=defineProps(
{
stockCode: {
type: String,
default: ''
}
}
)
const list = ref([])
const options = ref([])
const message=useMessage()
function getNotice(stockCodes) {
StockNotice(stockCodes).then(result => {
console.log(result)
list.value = result
})
}
onBeforeMount (()=>{
//message.info("正在获取数据"+stockCode)
getNotice(stockCode);
})
function findStockList(query){
if (query){
GetStockList(query).then(result => {
options.value=result.map(item => {
return {
label: item.name+" - "+item.ts_code,
value: item.ts_code
}
})
})
}else{
getNotice("")
}
}
function handleSearch(value) {
getNotice(value)
}
function openWin(code) {
BrowserOpenURL("https://pdf.dfcfw.com/pdf/H2_"+code+"_1.pdf?1750092081000.pdf")
}
function getTypeColor(name){
if(name.includes("质押")||name.includes("冻结")||name.includes("解冻")||name.includes("解押")||name.includes("解禁")){
return "error"
}
if(name.includes("异常")||name.includes("减持")||name.includes("增发")||name.includes("重大")){
return "error"
}
if(name.includes("季度报告")||name.includes("年度报告")||name.includes("澄清公告")||name.includes("风险")){
return "error"
}
if(name.includes("终止")||name.includes("复牌")||name.includes("停牌")||name.includes("退市")){
return "error"
}
if(name.includes("破产")||name.includes("清算")){
return "error"
}
if(name.includes("回购")||name.includes("重组")||name.includes("诉讼")||name.includes("仲裁")||name.includes("转让")||name.includes("收购")){
return "warning"
}
if(name.includes("调研")||name.includes("募集")){
return "warning"
}
return "info"
}
function getmMarketCode(market,code) {
if(market==="0"){
return "sz"+code
}else if(market==="1"){
return "sh"+code
}else if(market==="2"){
return "bj"+code
}else if(market==="3"){
return "hk"+code
}else{
return code
}
}
</script>
<template>
<n-card>
<n-auto-complete :options="options" placeholder="请输入A股名称或者代码" clearable filterable :on-select="handleSearch" :on-update:value="findStockList" />
</n-card>
<n-table striped size="small">
<n-thead>
<n-tr>
<n-th>股票代码</n-th>
<n-th>股票名称</n-th>
<n-th>公告标题</n-th>
<n-th>公告类型</n-th>
<n-th>公告日期</n-th>
<n-th><n-flex>数据更新时间<n-icon @click="getNotice('')" color="#409EFF" :size="20" :component="RefreshCircleSharp"/></n-flex></n-th>
</n-tr>
</n-thead>
<n-tbody>
<n-tr v-for="item in list" :key="item.art_code">
<n-td>
<n-popover trigger="hover" placement="right">
<template #trigger>
<n-tag type="info" :bordered="false">{{item.codes[0].stock_code }}</n-tag>
</template>
<money-trend style="width: 800px" :code="getmMarketCode(item.codes[0].market_code,item.codes[0].stock_code)" :name="item.codes[0].short_name" :days="360" :dark-theme="true" :chart-height="500"></money-trend>
</n-popover>
</n-td>
<n-td>
<n-popover trigger="hover" placement="right">
<template #trigger>
<n-tag type="info" :bordered="false">{{item.codes[0].short_name }}</n-tag>
</template>
<k-line-chart style="width: 800px" :code="getmMarketCode(item.codes[0].market_code,item.codes[0].stock_code)" :chart-height="500" :name="item.codes[0].short_name" :k-days="20" :dark-theme="true"></k-line-chart>
</n-popover>
</n-td>
<n-td>
<n-a type="info" @click="openWin(item.art_code)"><n-text :type="getTypeColor(item.columns[0].column_name)"> {{item.title}}</n-text></n-a>
</n-td>
<n-td>
<n-text :type="getTypeColor(item.columns[0].column_name)">{{item.columns[0].column_name }}</n-text>
</n-td>
<n-td>
<n-tag type="info">{{item.notice_date.substring(0,10) }}</n-tag>
</n-td>
<n-td>
<n-tag type="info">{{item.display_time.substring(0,19)}}</n-tag>
</n-td>
</n-tr>
</n-tbody>
</n-table>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,136 @@
<script setup>
import {onBeforeMount, ref} from 'vue'
import {GetStockList, StockResearchReport} from "../../wailsjs/go/main/App";
import {ArrowDownOutline, CaretDown, CaretUp, PulseOutline, Refresh, RefreshCircleSharp,} from "@vicons/ionicons5";
import KLineChart from "./KLineChart.vue";
import MoneyTrend from "./moneyTrend.vue";
import {useMessage} from "naive-ui";
import {BrowserOpenURL} from "../../wailsjs/runtime";
const {stockCode}=defineProps(
{
stockCode: {
type: String,
default: ''
}
}
)
const message=useMessage()
const list = ref([])
const options = ref([])
function getStockResearchReport(value) {
StockResearchReport(value).then(result => {
//console.log(result)
list.value = result
})
}
onBeforeMount(()=>{
getStockResearchReport(stockCode);
})
function ratingChangeName(ratingChange){
if(ratingChange===0){
return '调高'
}else if(ratingChange===1){
return '调低'
}else if(ratingChange===2){
return '首次'
}else if(ratingChange===3){
return '维持'
}else if (ratingChange===4){
return '无变化'
}else{
return ''
}
}
function getmMarketCode(market,code) {
if(market==="SHENZHEN"){
return "sz"+code
}else if(market==="SHANGHAI"){
return "sh"+code
}else if(market==="BEIJING"){
return "bj"+code
}else if(market==="HONGKONG"){
return "hk"+code
}else{
return code
}
}
function openWin(code) {
BrowserOpenURL("https://pdf.dfcfw.com/pdf/H3_"+code+"_1.pdf?1749744888000.pdf")
}
function findStockList(query){
if (query){
GetStockList(query).then(result => {
options.value=result.map(item => {
return {
label: item.name+" - "+item.ts_code,
value: item.ts_code
}
})
})
}else{
getStockResearchReport('')
}
}
function handleSearch(value) {
getStockResearchReport(value)
}
</script>
<template>
<n-card>
<n-auto-complete :options="options" placeholder="请输入A股名称或者代码" clearable filterable :on-select="handleSearch" :on-update:value="findStockList" />
</n-card>
<n-table striped size="small">
<n-thead>
<n-tr>
<!-- <n-th>代码</n-th>-->
<n-th>名称</n-th>
<n-th>行业</n-th>
<n-th>标题</n-th>
<n-th>东财评级</n-th>
<n-th>评级变动</n-th>
<n-th>机构评级</n-th>
<n-th>分析师</n-th>
<n-th>机构</n-th>
<n-th> <n-flex justify="space-between">日期<n-icon @click="getStockResearchReport" color="#409EFF" :size="20" :component="RefreshCircleSharp"/></n-flex></n-th>
</n-tr>
</n-thead>
<n-tbody>
<n-tr v-for="item in list" :key="item.infoCode">
<!-- <n-td>{{item.stockCode}}</n-td>-->
<n-td :title="item.stockCode">
<n-popover trigger="hover" placement="right">
<template #trigger>
<n-tag type="info" :bordered="false">{{item.stockName}}</n-tag>
</template>
<k-line-chart style="width: 800px" :code="getmMarketCode(item.market,item.stockCode)" :chart-height="500" :name="item.stockName" :k-days="20" :dark-theme="true"></k-line-chart>
</n-popover>
</n-td>
<n-td><n-tag type="info" :bordered="false">{{item.indvInduName}}</n-tag></n-td>
<n-td>
<n-a type="info" @click="openWin(item.infoCode)">{{item.title}}</n-a>
</n-td>
<n-td><n-text :type="item.emRatingName==='增持'?'error':'info'">
{{item.emRatingName}}
</n-text></n-td>
<n-td><n-text :type="item.ratingChange===0?'error':'info'">{{ratingChangeName(item.ratingChange)}}</n-text></n-td>
<n-td>{{item.sRatingName}}</n-td>
<n-td>{{item.researcher}}</n-td>
<n-td>{{item.orgSName}}</n-td>
<n-td>{{item.publishDate.substring(0,10)}}</n-td>
</n-tr>
</n-tbody>
</n-table>
</template>
<style scoped>
</style>

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 {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,10 +25,22 @@ 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()
EventsOff("updateVersion")
})
EventsOn("updateVersion",async (msg) => {
@@ -43,8 +59,8 @@ EventsOn("updateVersion",async (msg) => {
const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
console.log("GitHub UTC 时间:", utcDate);
console.log("转换后的本地时间:", formattedDate);
//console.log("GitHub UTC 时间:", utcDate);
//console.log("转换后的本地时间:", formattedDate);
notify.info({
avatar: () =>
h(NAvatar, {
@@ -69,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: () => '查看' })
}
@@ -79,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>
@@ -112,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" />
@@ -134,6 +185,8 @@ 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 />
<a href="https://github.com/2lovecode" target="_blank">@2lovecode</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'
@@ -47,7 +47,7 @@ onBeforeMount(()=>{
})
GetFollowedFund().then(result => {
followList.value = result
console.log("followList",followList.value)
//console.log("followList",followList.value)
})
})
@@ -60,7 +60,7 @@ onMounted(() => {
//ws.value = new WebSocket('ws://localhost:16688/ws'); // 替换为你的 WebSocket 服务器地址
ws.value.onopen = () => {
console.log('WebSocket 连接已打开');
//console.log('WebSocket 连接已打开');
};
ws.value.onmessage = (event) => {
@@ -74,13 +74,13 @@ onMounted(() => {
};
ws.value.onclose = () => {
console.log('WebSocket 连接已关闭');
//console.log('WebSocket 连接已关闭');
};
ticker.value=setInterval(() => {
GetFollowedFund().then(result => {
followList.value = result
console.log("followList",followList.value)
//console.log("followList",followList.value)
})
}, 1000*60)
@@ -103,7 +103,7 @@ function AddFund(){
message.success("关注成功")
GetFollowedFund().then(result => {
followList.value = result
console.log("followList",followList.value)
//console.log("followList",followList.value)
})
}
})
@@ -114,7 +114,7 @@ function unFollow(code){
message.success("取消关注成功")
GetFollowedFund().then(result => {
followList.value = result
console.log("followList",followList.value)
//console.log("followList",followList.value)
})
}
})
@@ -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

@@ -0,0 +1,94 @@
<script setup>
import {CaretDown, CaretUp, RefreshCircleOutline} from "@vicons/ionicons5";
import {NText,useMessage} from "naive-ui";
import {onBeforeUnmount, onMounted, onUnmounted, ref} from "vue";
import {GetIndustryMoneyRankSina} from "../../wailsjs/go/main/App";
import KLineChart from "./KLineChart.vue";
const props = defineProps({
headerTitle: {
type: String,
default: '行业资金排名(净流入)'
},
fenlei: {
type: String,
default: '0'
},
sort: {
type: String,
default: 'netamount'
},
})
const message = useMessage()
const dataList= ref([])
const sort = ref(props.sort)
const fenlei= ref(props.fenlei)
const interval = ref(null)
onMounted(()=>{
sort.value=props.sort
fenlei.value=props.fenlei
GetRankData()
interval.value=setInterval(()=>{
GetRankData()
},1000*60)
})
onBeforeUnmount(()=>{
clearInterval(interval.value)
})
function GetRankData(){
message.loading("正在刷新数据...")
GetIndustryMoneyRankSina(fenlei.value,sort.value).then(result => {
if(result.length>0){
dataList.value = result
//console.log(result)
}
})
}
</script>
<template>
<n-table striped size="small">
<n-thead>
<n-tr>
<n-th>板块名称</n-th>
<n-th>涨跌幅</n-th>
<n-th>流入资金/</n-th>
<n-th>流出资金/</n-th>
<n-th>净流入/<n-icon v-if="sort==='0'" :component="CaretDown"/><n-icon v-if="sort==='1'" :component="CaretUp"/></n-th>
<n-th>净流入率</n-th>
<n-th>领涨股</n-th>
<n-th>涨跌幅</n-th>
<n-th>最新价</n-th>
<n-th>净流入率</n-th>
</n-tr>
</n-thead>
<n-tbody>
<n-tr v-for="item in dataList" :key="item.category">
<n-td><n-tag :bordered=false type="info">{{item.name}}</n-tag></n-td>
<n-td> <n-text :type="item.avg_changeratio>0?'error':'success'">{{(item.avg_changeratio*100).toFixed(2)}}%</n-text></n-td>
<n-td><n-text type="info">{{(item.inamount/10000).toFixed(2)}}</n-text></n-td>
<n-td><n-text type="info">{{(item.outamount/10000).toFixed(2)}}</n-text></n-td>
<n-td><n-text :type="item.netamount>0?'error':'success'">{{(item.netamount/10000).toFixed(2)}}</n-text></n-td>
<n-td><n-text :type="item.ratioamount>0?'error':'success'">{{(item.ratioamount*100).toFixed(2)}}%</n-text></n-td>
<n-td>
<!-- <n-text type="info">{{item.ts_name}}</n-text>-->
<n-popover trigger="hover" placement="right">
<template #trigger>
<n-button tag="a" text :type="item.ts_changeratio>0?'error':'success'" :bordered=false >{{ item.ts_name }}</n-button>
</template>
<k-line-chart style="width: 800px" :code="item.ts_symbol" :chart-height="500" :name="item.ts_name" :k-days="20" :dark-theme="true"></k-line-chart>
</n-popover>
</n-td>
<n-td><n-text :type="item.ts_changeratio>0?'error':'success'">{{(item.ts_changeratio*100).toFixed(2)}}%</n-text></n-td>
<n-td><n-text type="info">{{item.ts_trade}}</n-text></n-td>
<n-td><n-text :type="item.ts_ratioamount>0?'error':'success'">{{(item.ts_ratioamount*100).toFixed(2)}}%</n-text></n-td>
</n-tr>
</n-tbody>
</n-table>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,707 @@
<script setup>
import {computed, h, onBeforeMount, onBeforeUnmount, onMounted, ref} from 'vue'
import {
GetAIResponseResult,
GetConfig,
GetIndustryRank,
GetPromptTemplates,
GetTelegraphList,
GlobalStockIndexes,
ReFleshTelegraphList,
SaveAIResponseResult,
SaveAsMarkdown,
ShareAnalysis,
SummaryStockNews,
GetAiConfigs
} from "../../wailsjs/go/main/App";
import {EventsOff, EventsOn} from "../../wailsjs/runtime";
import NewsList from "./newsList.vue";
import KLineChart from "./KLineChart.vue";
import { CaretDown, CaretUp, PulseOutline,} from "@vicons/ionicons5";
import {NAvatar, NButton, NFlex, NText, useMessage, useNotification} from "naive-ui";
import {MdPreview} from "md-editor-v3";
import {useRoute} from 'vue-router'
import RankTable from "./rankTable.vue";
import IndustryMoneyRank from "./industryMoneyRank.vue";
import StockResearchReportList from "./StockResearchReportList.vue";
import StockNoticeList from "./StockNoticeList.vue";
import LongTigerRankList from "./LongTigerRankList.vue";
import IndustryResearchReportList from "./IndustryResearchReportList.vue";
import HotStockList from "./HotStockList.vue";
import HotEvents from "./HotEvents.vue";
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');
const message = useMessage()
const notify = useNotification()
const panelHeight = ref(window.innerHeight - 240)
const telegraphList = ref([])
const sinaNewsList = ref([])
const common = ref([])
const america = ref([])
const europe = ref([])
const asia = ref([])
const other = ref([])
const globalStockIndexes = ref(null)
const summaryModal = ref(false)
const summaryBTN = ref(true)
const darkTheme = ref(false)
const theme = computed(() => {
return darkTheme ? 'dark' : 'light'
})
const aiSummary = ref(``)
const aiSummaryTime = ref("")
const modelName = ref("")
const chatId = ref("")
const question = ref(``)
const aiConfigId = ref(null)
const sysPromptId = ref(null)
const loading = ref(true)
const aiConfigs = ref([])
const sysPromptOptions = ref([])
const userPromptOptions = ref([])
const promptTemplates = ref([])
const industryRanks = ref([])
const sort = ref("0")
const nowTab = ref("市场快讯")
const indexInterval = ref(null)
const indexIndustryRank = ref(null)
const stockCode= ref('')
const enableTools= ref(true)
function getIndex() {
GlobalStockIndexes().then((res) => {
globalStockIndexes.value = res
common.value = res["common"]
america.value = res["america"]
europe.value = res["europe"]
asia.value = res["asia"]
other.value = res["other"]
})
}
onBeforeMount(() => {
nowTab.value = route.query.name
stockCode.value = route.query.stockCode
GetConfig().then(result => {
summaryBTN.value = result.openAiEnable
darkTheme.value = result.darkTheme
})
GetPromptTemplates("", "").then(res => {
promptTemplates.value = res
sysPromptOptions.value = promptTemplates.value.filter(item => item.type === '模型系统Prompt')
userPromptOptions.value = promptTemplates.value.filter(item => item.type === '模型用户Prompt')
})
GetAiConfigs().then(res=>{
aiConfigs.value = res
aiConfigId.value = res[0].ID
})
GetTelegraphList("财联社电报").then((res) => {
telegraphList.value = res
})
GetTelegraphList("新浪财经").then((res) => {
sinaNewsList.value = res
})
getIndex();
industryRank();
indexInterval.value = setInterval(() => {
getIndex()
}, 3000)
indexIndustryRank.value = setInterval(() => {
industryRank()
}, 1000 * 10)
})
onBeforeUnmount(() => {
EventsOff("changeMarketTab")
EventsOff("newTelegraph")
EventsOff("newSinaNews")
EventsOff("summaryStockNews")
clearInterval(indexInterval.value)
clearInterval(indexIndustryRank.value)
})
EventsOn("changeMarketTab", async (msg) => {
//message.info(msg.name)
updateTab(msg.name)
})
EventsOn("newTelegraph", (data) => {
if (data!=null) {
for (let i = 0; i < data.length; i++) {
telegraphList.value.pop()
}
telegraphList.value.unshift(...data)
}
})
EventsOn("newSinaNews", (data) => {
if (data!=null) {
for (let i = 0; i < data.length; i++) {
sinaNewsList.value.pop()
}
sinaNewsList.value.unshift(...data)
}
})
//获取页面高度
window.onresize = () => {
panelHeight.value = window.innerHeight - 240
}
function getAreaName(code) {
switch (code) {
case "america":
return "美洲"
case "europe":
return "欧洲"
case "asia":
return "亚洲"
case "common":
return "常用"
case "other":
return "其他"
}
}
function changeIndustryRankSort() {
if (sort.value === "0") {
sort.value = "1"
} else {
sort.value = "0"
}
industryRank()
}
function industryRank() {
GetIndustryRank(sort.value, 150).then(result => {
if (result.length > 0) {
//console.log(result)
industryRanks.value = result
} else {
message.info("暂无数据")
}
})
}
function reAiSummary() {
aiSummary.value = ""
summaryModal.value = true
loading.value = true
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
loading.value = false
const date = new Date(result.CreatedAt);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
aiSummaryTime.value = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
modelName.value = result.modelName
} else {
aiSummaryTime.value = ""
aiSummary.value = ""
modelName.value = ""
//SummaryStockNews(question.value, sysPromptId.value,enableTools.value)
}
})
}
function updateTab(name) {
summaryBTN.value = (name === "市场快讯");
nowTab.value = name
}
EventsOn("summaryStockNews", async (msg) => {
loading.value = false
////console.log(msg)
if (msg === "DONE") {
await SaveAIResponseResult("市场资讯", "市场资讯", aiSummary.value, chatId.value, question.value,aiConfigId.value)
message.info("AI分析完成")
message.destroyAll()
} else {
if (msg.chatId) {
chatId.value = msg.chatId
}
if (msg.question) {
question.value = msg.question
}
if (msg.content) {
aiSummary.value = aiSummary.value + msg.content
}
if (msg.extraContent) {
aiSummary.value = aiSummary.value + msg.extraContent
}
if (msg.model) {
modelName.value = msg.model
}
if (msg.time) {
aiSummaryTime.value = msg.time
}
}
})
async function copyToClipboard() {
try {
await navigator.clipboard.writeText(aiSummary.value);
message.success('分析结果已复制到剪切板');
} catch (err) {
message.error('复制失败: ' + err);
}
}
function saveAsMarkdown() {
SaveAsMarkdown('市场资讯', '市场资讯').then(result => {
message.success(result)
})
}
function share() {
ShareAnalysis('市场资讯', '市场资讯').then(msg => {
//message.info(msg)
notify.info({
avatar: () =>
h(NAvatar, {
size: 'small',
round: false,
src: icon.value
}),
title: '分享到社区',
duration: 1000 * 30,
content: () => {
return h('div', {
style: {
'text-align': 'left',
'font-size': '14px',
}
}, {default: () => msg})
},
})
})
}
function ReFlesh(source) {
//console.log("ReFlesh:", source)
ReFleshTelegraphList(source).then(res => {
if (source === "财联社电报") {
telegraphList.value = res
}
if (source === "新浪财经") {
sinaNewsList.value = res
}
})
}
</script>
<template>
<n-card>
<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>
<news-list :newsList="telegraphList" :header-title="'财联社电报'" @update:message="ReFlesh"></news-list>
</n-gi>
<n-gi>
<news-list :newsList="sinaNewsList" :header-title="'新浪财经'" @update:message="ReFlesh"></news-list>
</n-gi>
</n-grid>
</n-tab-pane>
<n-tab-pane name="全球股指" tab="全球股指">
<n-tabs type="segment" animated>
<n-tab-pane name="全球指数" tab="全球指数">
<n-grid :cols="5" :y-gap="0">
<n-gi v-for="(val, key) in globalStockIndexes" :key="key">
<n-list bordered>
<template #header>
{{ getAreaName(key) }}
</template>
<n-list-item v-for="item in val" :key="item.code">
<n-grid :cols="3" :y-gap="0">
<n-gi>
<n-text :type="item.zdf>0?'error':'success'">
<n-image :src="item.img" width="20"/> &nbsp;{{ item.name }}
</n-text>
</n-gi>
<n-gi>
<n-text :type="item.zdf>0?'error':'success'">{{ item.zxj }}</n-text>&nbsp;
<n-text :type="item.zdf>0?'error':'success'">
<n-number-animation :precision="2" :from="0" :to="item.zdf"/>
%
</n-text>
</n-gi>
<n-gi>
<n-text :type="item.state === 'open' ? 'success' : 'warning'">{{
item.state === 'open' ? '开市' : '休市'
}}
</n-text>
</n-gi>
</n-grid>
</n-list-item>
</n-list>
</n-gi>
</n-grid>
</n-tab-pane>
<n-tab-pane name="上证指数" tab="上证指数">
<k-line-chart code="sh000001" :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="sz399001" :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="sz399006" :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="hkHSI" :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="us.IXIC" :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="us.DJI" :chart-height="panelHeight" name="道琼斯" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="标普500" tab="标普500">
<k-line-chart code="us.INX" :chart-height="panelHeight" name="标普500" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
</n-tabs>
</n-tab-pane>
<n-tab-pane name="重大指数" tab="重大指数">
<n-tabs type="segment" animated>
<n-tab-pane name="恒生科技指数" tab="恒生科技指数">
<k-line-chart code="hkHSTECH" :chart-height="panelHeight" name="恒生科技指数" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="科创50" tab="科创50" >
<k-line-chart code="sh000688" :chart-height="panelHeight" name="科创50" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="科创芯片" tab="科创芯片" >
<k-line-chart code="sh000685" :chart-height="panelHeight" name="科创芯片" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="证券龙头" tab="证券龙头" >
<k-line-chart code="sz399437" :chart-height="panelHeight" name="证券龙头" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="高端装备" tab="高端装备" >
<k-line-chart code="sz399437" :chart-height="panelHeight" name="高端装备" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="中证银行" tab="中证银行">
<k-line-chart code="sz399986" :chart-height="panelHeight" name="中证银行" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="上证医药" tab="上证医药">
<k-line-chart code="sh000037" :chart-height="panelHeight" name="上证医药" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="沪深300" tab="沪深300">
<k-line-chart code="sh000300" :chart-height="panelHeight" name="沪深300" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="上证50" tab="上证50">
<k-line-chart code="sh000016" :chart-height="panelHeight" name="上证50" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="中证A500" tab="中证A500">
<k-line-chart code="sh000510" :chart-height="panelHeight" name="中证A500" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="中证1000" tab="中证1000">
<k-line-chart code="sh000852" :chart-height="panelHeight" name="中证1000" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="中证白酒" tab="中证白酒">
<k-line-chart code="sz399997" :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="usYINN.AM" :chart-height="panelHeight" name="富时中国三倍做多" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="VIX恐慌指数" tab="VIX恐慌指数">
<k-line-chart code="usUVXY.AM" :chart-height="panelHeight" name="VIX恐慌指数" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
</n-tabs>
</n-tab-pane>
<n-tab-pane name="行业排名" tab="行业排名">
<n-tabs type="card" animated>
<n-tab-pane name="行业涨幅排名" tab="行业涨幅排名">
<n-table striped>
<n-thead>
<n-tr>
<n-th>行业名称</n-th>
<n-th @click="changeIndustryRankSort">行业涨幅
<n-icon v-if="sort==='0'" :component="CaretDown"/>
<n-icon v-if="sort==='1'" :component="CaretUp"/>
</n-th>
<n-th>行业5日涨幅</n-th>
<n-th>行业20日涨幅</n-th>
<n-th>领涨股</n-th>
<n-th>涨幅</n-th>
<n-th>最新价</n-th>
</n-tr>
</n-thead>
<n-tbody>
<n-tr v-for="item in industryRanks" :key="item.bd_code">
<n-td>
<n-tag :bordered=false type="info">{{ item.bd_name }}</n-tag>
</n-td>
<n-td>
<n-text :type="item.bd_zdf>0?'error':'success'">{{ item.bd_zdf }}%</n-text>
</n-td>
<n-td>
<n-text :type="item.bd_zdf5>0?'error':'success'">{{ item.bd_zdf5 }}%</n-text>
</n-td>
<n-td>
<n-text :type="item.bd_zdf20>0?'error':'success'">{{ item.bd_zdf20 }}%</n-text>
</n-td>
<n-td>
<n-text :type="item.nzg_zdf>0?'error':'success'"> {{ item.nzg_name }}
<n-text type="info">{{ item.nzg_code }}</n-text>
</n-text>
</n-td>
<n-td>
<n-text :type="item.nzg_zdf>0?'error':'success'"> {{ item.nzg_zdf }}%</n-text>
</n-td>
<n-td>
<n-text :type="item.nzg_zdf>0?'error':'success'">{{ item.nzg_zxj }}</n-text>
</n-td>
</n-tr>
</n-tbody>
</n-table>
<n-table striped>
<n-thead>
<n-tr>
<n-th>行业名称</n-th>
<n-th @click="changeIndustryRankSort">行业涨幅
<n-icon v-if="sort==='0'" :component="CaretDown"/>
<n-icon v-if="sort==='1'" :component="CaretUp"/>
</n-th>
<n-th>行业5日涨幅</n-th>
<n-th>行业20日涨幅</n-th>
<n-th>领涨股</n-th>
<n-th>涨幅</n-th>
<n-th>最新价</n-th>
</n-tr>
</n-thead>
<n-tbody>
<n-tr v-for="item in industryRanks" :key="item.bd_code">
<n-td>
<n-tag :bordered=false type="info">{{ item.bd_name }}</n-tag>
</n-td>
<n-td>
<n-text :type="item.bd_zdf>0?'error':'success'">{{ item.bd_zdf }}%</n-text>
</n-td>
<n-td>
<n-text :type="item.bd_zdf5>0?'error':'success'">{{ item.bd_zdf5 }}%</n-text>
</n-td>
<n-td>
<n-text :type="item.bd_zdf20>0?'error':'success'">{{ item.bd_zdf20 }}%</n-text>
</n-td>
<n-td>
<n-text :type="item.nzg_zdf>0?'error':'success'"> {{ item.nzg_name }}
<n-text type="info">{{ item.nzg_code }}</n-text>
</n-text>
</n-td>
<n-td>
<n-text :type="item.nzg_zdf>0?'error':'success'"> {{ item.nzg_zdf }}%</n-text>
</n-td>
<n-td>
<n-text :type="item.nzg_zdf>0?'error':'success'">{{ item.nzg_zxj }}</n-text>
</n-td>
</n-tr>
</n-tbody>
</n-table>
</n-tab-pane>
<n-tab-pane name="行业资金排名(净流入)" tab="行业资金排名">
<industryMoneyRank :fenlei="'0'" :header-title="'行业资金排名(净流入)'" :sort="'netamount'"/>
</n-tab-pane>
<n-tab-pane name="证监会行业资金排名(净流入)" tab="证监会行业资金排名">
<industryMoneyRank :fenlei="'2'" :header-title="'证监会行业资金排名(净流入)'" :sort="'netamount'"/>
</n-tab-pane>
<n-tab-pane name="概念板块资金排名(净流入)" tab="概念板块资金排名">
<industryMoneyRank :fenlei="'1'" :header-title="'概念板块资金排名(净流入)'" :sort="'netamount'"/>
</n-tab-pane>
</n-tabs>
</n-tab-pane>
<n-tab-pane name="个股资金流向" tab="个股资金流向">
<n-tabs type="card" animated>
<n-tab-pane name="netamount" tab="净流入额排名">
<RankTable :header-title="'净流入额排名'" :sort="'netamount'"/>
</n-tab-pane>
<n-tab-pane name="outamount" tab="流出资金排名">
<RankTable :header-title="'流出资金排名'" :sort="'outamount'"/>
</n-tab-pane>
<n-tab-pane name="ratioamount" tab="净流入率排名">
<RankTable :header-title="'净流入率排名'" :sort="'ratioamount'"/>
</n-tab-pane>
<n-tab-pane name="r0_net" tab="主力净流入额排名">
<RankTable :header-title="'主力净流入额排名'" :sort="'r0_net'"/>
</n-tab-pane>
<n-tab-pane name="r0_out" tab="主力流出排名">
<RankTable :header-title="'主力流出排名'" :sort="'r0_out'"/>
</n-tab-pane>
<n-tab-pane name="r0_ratio" tab="主力净流入率排名">
<RankTable :header-title="'主力净流入率排名'" :sort="'r0_ratio'"/>
</n-tab-pane>
<n-tab-pane name="r3_net" tab="散户净流入额排名">
<RankTable :header-title="'散户净流入额排名'" :sort="'r3_net'"/>
</n-tab-pane>
<n-tab-pane name="r3_out" tab="散户流出排名">
<RankTable :header-title="'散户流出排名'" :sort="'r3_out'"/>
</n-tab-pane>
<n-tab-pane name="r3_ratio" tab="散户净流入率排名">
<RankTable :header-title="'散户净流入率排名'" :sort="'r3_ratio'"/>
</n-tab-pane>
</n-tabs>
</n-tab-pane>
<n-tab-pane name="龙虎榜" tab="龙虎榜">
<LongTigerRankList />
</n-tab-pane>
<n-tab-pane name="个股研报" tab="个股研报">
<StockResearchReportList :stock-code="stockCode"/>
</n-tab-pane>
<n-tab-pane name="公司公告" tab="公司公告 ">
<StockNoticeList :stock-code="stockCode" />
</n-tab-pane>
<n-tab-pane name="行业研究" tab="行业研究 ">
<IndustryResearchReportList/>
</n-tab-pane>
<n-tab-pane name="当前热门" tab="当前热门">
<n-tabs type="card" animated>
<n-tab-pane name="全球" tab="全球">
<HotStockList :market-type="'10'"/>
</n-tab-pane>
<n-tab-pane name="沪深" tab="沪深">
<HotStockList :market-type="'12'"/>
</n-tab-pane>
<n-tab-pane name="港股" tab="港股">
<HotStockList :market-type="'13'"/>
</n-tab-pane>
<n-tab-pane name="美股" tab="美股">
<HotStockList :market-type="'11'"/>
</n-tab-pane>
<n-tab-pane name="热门话题" tab="热门话题">
<n-grid :cols="1" :y-gap="10">
<n-grid-item>
<HotTopics/>
</n-grid-item>
<!-- <n-grid-item>-->
<!-- <HotEvents/>-->
<!-- </n-grid-item>-->
</n-grid>
</n-tab-pane>
<n-tab-pane name="重大事件时间轴" tab="重大事件时间轴">
<InvestCalendarTimeLine />
</n-tab-pane>
<n-tab-pane name="财经日历" tab="财经日历">
<ClsCalendarTimeLine />
</n-tab-pane>
</n-tabs>
</n-tab-pane>
<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;"
:title="'AI市场资讯总结'">
<n-spin size="small" :show="loading">
<MdPreview style="height: 440px;text-align: left" :modelValue="aiSummary" :theme="theme"/>
</n-spin>
<template #footer>
<n-flex justify="space-between" ref="tipsRef">
<n-text type="info" v-if="aiSummaryTime">
<n-tag v-if="modelName" type="warning" round :title="chatId" :bordered="false">{{ modelName }}</n-tag>
{{ aiSummaryTime }}
</n-text>
<n-text type="error">*AI分析结果仅供参考请以实际行情为准投资需谨慎风险自担</n-text>
</n-flex>
</template>
<template #action>
<n-flex justify="left" style="margin-bottom: 10px">
<n-switch v-model:value="enableTools" :round="false">
<template #checked>
启用AI函数工具调用
</template>
<template #unchecked>
不启用AI函数工具调用
</template>
</n-switch>
<n-gradient-text type="error" style="margin-left: 10px">*AI函数工具调用可以增强AI获取数据的能力,但会消耗更多tokens</n-gradient-text>
</n-flex>
<n-flex justify="space-between" style="margin-bottom: 10px">
<n-select style="width: 32%" v-model:value="aiConfigId" label-field="name" value-field="ID"
:options="aiConfigs" placeholder="请选择AI模型服务配置"/>
<n-select style="width: 32%" v-model:value="sysPromptId" label-field="name" value-field="ID"
:options="sysPromptOptions" placeholder="请选择系统提示词"/>
<n-select style="width: 32%" v-model:value="question" label-field="name" value-field="content"
:options="userPromptOptions" placeholder="请选择用户提示词"/>
</n-flex>
<n-flex justify="right">
<n-input v-model:value="question" style="text-align: left" clearable
type="textarea"
:show-count="true"
placeholder="请输入您的问题:例如 总结和分析股票市场新闻中的投资机会"
:autosize="{
minRows: 2,
maxRows: 5
}"
/>
<n-button size="tiny" type="warning" @click="reAiSummary">再次总结</n-button>
<n-button size="tiny" type="success" @click="copyToClipboard">复制到剪切板</n-button>
<n-button size="tiny" type="primary" @click="saveAsMarkdown">保存为Markdown文件</n-button>
<n-button size="tiny" type="error" @click="share">分享到项目社区</n-button>
</n-flex>
</template>
</n-modal>
<div style="position: fixed;bottom: 18px;right:25px;z-index: 10;" v-if="summaryBTN">
<n-input-group>
<n-button type="primary" @click="getAiSummary">
<n-icon :component="PulseOutline"/> &nbsp;AI总结
</n-button>
</n-input-group>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,374 @@
<script setup lang="ts">
import {onMounted, ref} from "vue";
import {GetStockMoneyTrendByDay} from "../../wailsjs/go/main/App";
import * as echarts from "echarts";
const {code, name, darkTheme, days, chartHeight} = defineProps({
code: {
type: String,
default: ''
},
name: {
type: String,
default: ''
},
days: {
type: Number,
default: 14
},
chartHeight: {
type: Number,
default: 500
},
darkTheme: {
type: Boolean,
default: false
}
})
const LineChartRef = ref(null);
onMounted(
() => {
handleLine(code, days)
}
)
const handleLine = (code, days) => {
GetStockMoneyTrendByDay(code, days).then(result => {
//console.log("GetStockMoneyTrendByDay", result)
const chart = echarts.init(LineChartRef.value);
const categoryData = [];
const netamount_values = [];
const r0_net_values = [];
const trades_values = [];
let volume = []
let min = 0
let max = 0
for (let i = 0; i < result.length; i++) {
let resultElement = result[i]
categoryData.push(resultElement.opendate)
let netamount = (resultElement.netamount / 10000).toFixed(2);
netamount_values.push(netamount)
let price = Number(resultElement.trade);
trades_values.push(price)
r0_net_values.push((resultElement.r0_net / 10000).toFixed(2))
if (min === 0 || min > price) {
min = price
}
if (max < price) {
max = price
}
if (i > 0) {
let b = Number(Number(result[i].netamount) + Number(result[i - 1].netamount)) / 10000
volume.push(b.toFixed(2))
} else {
volume.push((Number(result[i].netamount) / 10000).toFixed(2))
}
}
//console.log("volume", volume)
const upColor = '#ec0000';
const downColor = '#00da3c';
let option = {
title: {
text: name,
left: '20px',
textStyle: {
color: darkTheme?'#ccc':'#456'
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
lineStyle: {
color: '#376df4',
width: 1,
opacity: 1
}
},
borderWidth: 2,
borderColor: darkTheme?'#456':'#ccc',
backgroundColor: darkTheme?'#456':'#fff',
padding: 10,
textStyle: {
color: darkTheme?'#ccc':'#456'
},
},
axisPointer: {
link: [
{
xAxisIndex: 'all'
}
],
label: {
backgroundColor: '#888'
}
},
legend: {
show: true,
data: ['当日净流入', '主力当日净流入','累计净流入', '股价'],
selected: {
'当日净流入': true,
'主力当日净流入': true,
'累计净流入': true,
'股价': true,
},
//orient: 'vertical',
textStyle: {
color: darkTheme ? 'rgb(253,252,252)' : '#456'
},
right: 150,
},
dataZoom: [
{
type: 'inside',
xAxisIndex: [0, 1],
start: 86,
end: 100
},
{
show: true,
xAxisIndex: [0, 1],
type: 'slider',
top: '90%',
start: 86,
end: 100
}
],
grid: [
{
left: '8%',
right: '8%',
height: '50%',
},
{
left: '8%',
right: '8%',
top: '74%',
height: '15%'
},
],
xAxis: [
{
type: 'category',
data: categoryData,
axisPointer: {
z: 100
},
boundaryGap: false,
axisLine: { onZero: false },
splitLine: { show: false },
min: 'dataMin',
max: 'dataMax',
},
{
gridIndex: 1,
type: 'category',
data: categoryData,
axisLabel: {
show: false
},
}
],
yAxis: [
{
name: '当日净流入/万',
type: 'value',
axisLine: {
show: true
},
splitLine: {
show: false
},
},
{
name: '股价',
type: 'value',
min: min - 1,
max: max + 1,
minInterval: 0.01,
axisLine: {
show: true
},
splitLine: {
show: false
},
},
{
gridIndex: 1,
name: '累计净流入/万',
type: 'value',
axisLine: {
show: true
},
splitLine: {
show: false
},
},
],
series: [
{
yAxisIndex: 0,
name: '当日净流入',
data: netamount_values,
smooth: false,
showSymbol: false,
lineStyle: {
width: 2
},
markPoint: {
symbol: 'arrow',
symbolRotate: 90,
symbolSize: [10, 20],
symbolOffset: [10, 0],
itemStyle: {
color: '#0d7dfc'
},
label: {
position: 'right',
},
data: [
{type: 'max', name: 'Max'},
{type: 'min', name: 'Min'}
]
},
markLine: {
data: [
{
type: 'average',
name: 'Average',
lineStyle: {
color: '#0077ff',
width: 0.5
},
},
]
},
type: 'line'
},
{
yAxisIndex: 0,
name: '主力当日净流入',
data: r0_net_values,
smooth: false,
showSymbol: false,
lineStyle: {
width: 2
},
// markPoint: {
// symbol: 'arrow',
// symbolRotate: 90,
// symbolSize: [10, 20],
// symbolOffset: [10, 0],
// itemStyle: {
// color: '#0d7dfc'
// },
// label: {
// position: 'right',
// },
// data: [
// {type: 'max', name: 'Max'},
// {type: 'min', name: 'Min'}
// ]
// },
// markLine: {
// data: [
// {
// type: 'average',
// name: 'Average',
// lineStyle: {
// color: '#0077ff',
// width: 0.5
// },
// },
// ]
// },
type: 'bar'
},
{
yAxisIndex: 1,
name: '股价',
type: 'line',
data: trades_values,
smooth: true,
showSymbol: false,
lineStyle: {
width: 3
},
markPoint: {
symbol: 'arrow',
symbolRotate: 90,
symbolSize: [10, 20],
symbolOffset: [10, 0],
itemStyle: {
color: '#f39509'
},
label: {
position: 'right',
},
data: [
{type: 'max', name: 'Max'},
{type: 'min', name: 'Min'}
]
},
markLine: {
data: [
{
type: 'average',
name: 'Average',
lineStyle: {
color: '#f39509',
width: 0.5
},
},
]
},
},
{
type: 'bar',
xAxisIndex: 1,
yAxisIndex: 2,
name: '累计净流入',
data: volume,
smooth: true,
showSymbol: false,
lineStyle: {
width: 2
},
markPoint: {
symbol: 'arrow',
symbolRotate: 90,
symbolSize: [10, 20],
symbolOffset: [10, 0],
// itemStyle: {
// color: '#f39509'
// },
label: {
position: 'right',
},
data: [
{type: 'max', name: 'Max'},
{type: 'min', name: 'Min'}
]
},
},
]
};
chart.setOption(option);
})
}
</script>
<template>
<div ref="LineChartRef" style="width: 100%;height: auto;" :style="{height:chartHeight+'px'}"></div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,58 @@
<script setup>
import {ReFleshTelegraphList} from "../../wailsjs/go/main/App";
import {RefreshCircle, RefreshCircleSharp, RefreshOutline} from "@vicons/ionicons5";
const { headerTitle,newsList } = defineProps({
headerTitle: {
type: String,
default: '市场资讯'
},
newsList: {
type: Array,
default: () => []
},
})
const emits = defineEmits(['update:message'])
const updateMessage = () => {
emits('update:message', headerTitle)
}
</script>
<template>
<n-list bordered>
<template #header>
<n-flex justify="space-between">
<n-tag :bordered="false" size="large" type="success" >{{ headerTitle }}</n-tag>
<n-button :bordered="false" @click="updateMessage"><n-icon color="#409EFF" size="25" :component="RefreshCircleSharp"/></n-button>
</n-flex>
</template>
<n-list-item v-for="item in newsList">
<n-space justify="start">
<n-text justify="start" :bordered="false" :type="item.isRed?'error':'info'">
<n-tag size="small" :type="item.isRed?'error':'warning'" :bordered="false"> {{ item.time }}</n-tag>
{{ item.content }}
</n-text>
</n-space>
<n-space v-if="item.subjects" style="margin-top: 2px">
<n-tag :bordered="false" type="success" size="small" v-for="sub in item.subjects">
{{ sub }}
</n-tag>
<n-space v-if="item.stocks">
<n-tag :bordered="false" type="warning" size="small" v-for="sub in item.stocks">
{{ sub }}
</n-tag>
</n-space>
<n-tag v-if="item.url" :bordered="false" type="warning" size="small">
<a :href="item.url" target="_blank">
<n-text type="warning">查看原文</n-text>
</a>
</n-tag>
</n-space>
</n-list-item>
</n-list>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,101 @@
<script setup>
import {CaretDown, CaretUp, RefreshCircleOutline} from "@vicons/ionicons5";
import {NText,useMessage} from "naive-ui";
import {onBeforeUnmount, onMounted, onUnmounted, ref} from "vue";
import {GetMoneyRankSina} from "../../wailsjs/go/main/App";
import KLineChart from "./KLineChart.vue";
const props = defineProps({
headerTitle: {
type: String,
default: '净流入额排名'
},
sort: {
type: String,
default: 'netamount'
},
})
const message = useMessage()
const dataList= ref([])
const sort = ref(props.sort)
const interval = ref(null)
onMounted(()=>{
sort.value=props.sort
GetMoneyRankSinaData()
interval.value=setInterval(()=>{
GetMoneyRankSinaData()
},1000*60)
})
onBeforeUnmount(()=>{
clearInterval(interval.value)
})
function GetMoneyRankSinaData(){
message.loading("正在刷新数据...")
GetMoneyRankSina(sort.value).then(result => {
if(result.length>0){
dataList.value = result
}
})
}
</script>
<template>
<n-table striped size="small">
<n-thead>
<n-tr>
<n-th>代码</n-th>
<n-th>名称</n-th>
<n-th>最新价</n-th>
<n-th>涨跌幅</n-th>
<n-th>换手率</n-th>
<n-th>成交额/</n-th>
<n-th>流出资金/</n-th>
<n-th>流入资金/</n-th>
<n-th>净流入/</n-th>
<n-th>净流入率</n-th>
<n-th v-if="sort === 'r0_net'||sort==='r0_out'">主力流出/</n-th>
<n-th v-if="sort === 'r0_net'">主力流入/</n-th>
<n-th v-if="sort === 'r0_net'">主力净流入/</n-th>
<n-th >主力净流入率</n-th>
<n-th v-if="sort === 'r3_net'||sort==='r3_out'">散户流出/</n-th>
<n-th v-if="sort === 'r3_net'">散户流入/</n-th>
<n-th v-if="sort === 'r3_net'">散户净流入/</n-th>
<n-th >散户净流入率</n-th>
</n-tr>
</n-thead>
<n-tbody>
<n-tr v-for="item in dataList" :key="item.symbol">
<n-td><n-tag :bordered=false type="info">{{ item.symbol }}</n-tag></n-td>
<n-td>
<n-popover trigger="hover" placement="right">
<template #trigger>
<n-button tag="a" text :type="item.changeratio>0?'error':'success'" :bordered=false >{{ item.name }}</n-button>
</template>
<k-line-chart style="width: 800px" :code="item.symbol" :chart-height="500" :name="item.name" :k-days="20" :dark-theme="true"></k-line-chart>
</n-popover>
</n-td>
<n-td><n-text :type="item.changeratio>0?'error':'success'">{{item.trade}}</n-text></n-td>
<n-td><n-text :type="item.changeratio>0?'error':'success'">{{(item.changeratio*100).toFixed(2)}}%</n-text></n-td>
<n-td><n-text :type="item.turnover>500?'error':'info'">{{(item.turnover/100).toFixed(2)}}%</n-text></n-td>
<n-td><n-text type="info">{{(item.amount/10000).toFixed(2)}}</n-text></n-td>
<n-td><n-text type="info"> {{(item.outamount/10000).toFixed(2)}}</n-text></n-td>
<n-td><n-text type="info"> {{(item.inamount/10000).toFixed(2)}}</n-text></n-td>
<n-td><n-text type="info"> {{(item.netamount/10000).toFixed(2)}}</n-text></n-td>
<n-td><n-text :type="item.ratioamount>0?'error':'success'"> {{(item.ratioamount*100).toFixed(2)}}%</n-text></n-td>
<n-td v-if="sort === 'r0_net'||sort==='r0_out'"><n-text type="success"> {{(item.r0_out/10000).toFixed(2)}}</n-text></n-td>
<n-td v-if="sort === 'r0_net'"><n-text type="error"> {{(item.r0_in/10000).toFixed(2)}}</n-text></n-td>
<n-td v-if="sort === 'r0_net'"><n-text :type="item.r0_net>0?'error':'success'"> {{(item.r0_net/10000).toFixed(2)}}</n-text></n-td>
<n-td ><n-text :type="item.r0_ratio>0?'error':'success'"> {{(item.r0_ratio*100).toFixed(2)}}%</n-text></n-td>
<n-td v-if="sort === 'r3_net'||sort==='r3_out'"><n-text type="success"> {{(item.r3_out/10000).toFixed(2)}}</n-text></n-td>
<n-td v-if="sort === 'r3_net'"><n-text type="error"> {{(item.r3_in/10000).toFixed(2)}}</n-text></n-td>
<n-td v-if="sort === 'r3_net'"><n-text :type="item.r3_net>0?'error':'success'"> {{(item.r3_net/10000).toFixed(2)}}</n-text></n-td>
<n-td ><n-text :type="item.r3_ratio>0?'error':'success'"> {{(item.r3_ratio*100).toFixed(2)}}%</n-text></n-td>
</n-tr>
</n-tbody>
</n-table>
</template>
<style scoped>
</style>

View File

@@ -1,159 +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,
})
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
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';
@@ -162,36 +201,36 @@ 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
formValue.value.enableNews = config.enableNews
formValue.value.darkTheme = config.darkTheme
// formRef.value.resetFields()
formValue.value.enableFund = config.enableFund
formValue.value.enablePushNews = config.enablePushNews
formValue.value.enableOnlyPushRedNews = config.enableOnlyPushRedNews
formValue.value.sponsorCode = config.sponsorCode
formValue.value.httpProxy=config.httpProxy
formValue.value.httpProxyEnabled=config.httpProxyEnabled
};
reader.readAsText(file);
};
@@ -200,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,
@@ -210,217 +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-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="6" label="是否启用钉钉推送:" path="dingPush.enable" >
<n-switch v-model:value="formValue.dingPush.enable" />
</n-form-item-gi>
<n-form-item-gi :span="6" label="是否启用本地推送:" path="localPush.enable" >
<n-switch v-model:value="formValue.localPush.enable" />
</n-form-item-gi>
<n-form-item-gi :span="5" label="弹幕功能:" path="enableDanmu" >
<n-switch v-model:value="formValue.enableDanmu" />
</n-form-item-gi>
<n-form-item-gi :span="5" label="是否显示滚动快讯(重启生效)" path="enableNews" >
<n-switch v-model:value="formValue.enableNews" />
</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>
@@ -428,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,6 +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

@@ -1,18 +1,23 @@
import {createMemoryHistory, createRouter, createWebHashHistory} from 'vue-router'
import {createMemoryHistory, createRouter, createWebHashHistory, createWebHistory} from 'vue-router'
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 marketView from "../components/market.vue";
import agentChat from "../components/agent-chat.vue"
const routes = [
{ path: '/', component: stockView,name: 'stock' },
{ path: '/', component: stockView,name: 'stock'},
{ path: '/fund', component: fundView,name: 'fund' },
{ path: '/settings', component: settingsView,name: 'settings' },
{ path: '/about', component: about,name: 'about' },
{ path: '/about', component: aboutView,name: 'about' },
{ path: '/market', component: marketView,name: 'market' },
{ path: '/agent', component: agentChat,name: 'agent' },
]
const router = createRouter({
//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'
})],
}),
]
})

81
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>;
@@ -11,10 +12,22 @@ export function AddPrompt(arg1:models.Prompt):Promise<string>;
export function AddStockGroup(arg1:number,arg2:string):Promise<string>;
export function CheckUpdate():Promise<void>;
export function AnalyzeSentiment(arg1:string):Promise<data.SentimentResult>;
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>>;
export function DelPrompt(arg1:number):Promise<string>;
export function EMDictCode(arg1:string):Promise<Array<any>>;
export function ExportConfig():Promise<string>;
export function Follow(arg1:string):Promise<string>;
@@ -23,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>;
@@ -33,26 +48,74 @@ 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>>;
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>;
export function GetStockList(arg1:string):Promise<Array<data.StockBasic>>;
export function GetStockMinutePriceLineData(arg1:string,arg2:string):Promise<Record<string, any>>;
export function GetStockMoneyTrendByDay(arg1:string,arg2:number):Promise<Array<Record<string, any>>>;
export function GetTelegraphList(arg1:string):Promise<any>;
export function GetVersionInfo():Promise<models.VersionInfo>;
export function GetfundList(arg1:string):Promise<Array<data.FundBasic>>;
export function GlobalStockIndexes():Promise<Record<string, any>>;
export function Greet(arg1:string):Promise<data.StockInfo>;
export function NewChatStream(arg1:string,arg2:string,arg3:string,arg4:any):Promise<void>;
export function HotEvent(arg1:number):Promise<any>;
export function HotStock(arg1:string):Promise<any>;
export function HotTopic(arg1:number):Promise<Array<any>>;
export function IndustryResearchReport(arg1:string):Promise<Array<any>>;
export function InitializeGroupSort():Promise<boolean>;
export function InvestCalendarTimeLine(arg1:string):Promise<Array<any>>;
export function LongTigerRank(arg1:string):Promise<any>;
export function NewChatStream(arg1:string,arg2:string,arg3:string,arg4:number,arg5:any,arg6:boolean):Promise<void>;
export function 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>;
export function SendDingDingMessageByType(arg1:string,arg2:string,arg3:number):Promise<string>;
@@ -67,8 +130,16 @@ export function SetStockSort(arg1:number,arg2:string):Promise<void>;
export function ShareAnalysis(arg1:string,arg2:string):Promise<string>;
export function StockNotice(arg1:string):Promise<Array<any>>;
export function StockResearchReport(arg1:string):Promise<Array<any>>;
export function SummaryStockNews(arg1:string,arg2:number,arg3:any,arg4:boolean):Promise<void>;
export function 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>;

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

@@ -18,14 +18,38 @@ export function AddStockGroup(arg1, arg2) {
return window['go']['main']['App']['AddStockGroup'](arg1, arg2);
}
export function CheckUpdate() {
return window['go']['main']['App']['CheckUpdate']();
export function AnalyzeSentiment(arg1) {
return window['go']['main']['App']['AnalyzeSentiment'](arg1);
}
export function ChatWithAgent(arg1, arg2, arg3) {
return window['go']['main']['App']['ChatWithAgent'](arg1, arg2, arg3);
}
export function CheckSponsorCode(arg1) {
return window['go']['main']['App']['CheckSponsorCode'](arg1);
}
export function CheckStockBaseInfo(arg1) {
return window['go']['main']['App']['CheckStockBaseInfo'](arg1);
}
export function CheckUpdate(arg1) {
return window['go']['main']['App']['CheckUpdate'](arg1);
}
export function ClsCalendar() {
return window['go']['main']['App']['ClsCalendar']();
}
export function DelPrompt(arg1) {
return window['go']['main']['App']['DelPrompt'](arg1);
}
export function EMDictCode(arg1) {
return window['go']['main']['App']['EMDictCode'](arg1);
}
export function ExportConfig() {
return window['go']['main']['App']['ExportConfig']();
}
@@ -42,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']();
}
@@ -62,14 +90,54 @@ 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);
}
export function GetIndustryRank(arg1, arg2) {
return window['go']['main']['App']['GetIndustryRank'](arg1, arg2);
}
export function GetMoneyRankSina(arg1) {
return window['go']['main']['App']['GetMoneyRankSina'](arg1);
}
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);
}
export function GetStockKLine(arg1, arg2, arg3) {
return window['go']['main']['App']['GetStockKLine'](arg1, arg2, arg3);
}
export function GetStockList(arg1) {
return window['go']['main']['App']['GetStockList'](arg1);
}
export function GetStockMinutePriceLineData(arg1, arg2) {
return window['go']['main']['App']['GetStockMinutePriceLineData'](arg1, arg2);
}
export function GetStockMoneyTrendByDay(arg1, arg2) {
return window['go']['main']['App']['GetStockMoneyTrendByDay'](arg1, arg2);
}
export function GetTelegraphList(arg1) {
return window['go']['main']['App']['GetTelegraphList'](arg1);
}
export function GetVersionInfo() {
return window['go']['main']['App']['GetVersionInfo']();
}
@@ -78,12 +146,56 @@ export function GetfundList(arg1) {
return window['go']['main']['App']['GetfundList'](arg1);
}
export function GlobalStockIndexes() {
return window['go']['main']['App']['GlobalStockIndexes']();
}
export function Greet(arg1) {
return window['go']['main']['App']['Greet'](arg1);
}
export function NewChatStream(arg1, arg2, arg3, arg4) {
return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3, arg4);
export function HotEvent(arg1) {
return window['go']['main']['App']['HotEvent'](arg1);
}
export function HotStock(arg1) {
return window['go']['main']['App']['HotStock'](arg1);
}
export function HotTopic(arg1) {
return window['go']['main']['App']['HotTopic'](arg1);
}
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);
}
export function LongTigerRank(arg1) {
return window['go']['main']['App']['LongTigerRank'](arg1);
}
export function NewChatStream(arg1, arg2, arg3, arg4, arg5, arg6) {
return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3, arg4, arg5, arg6);
}
export function 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);
}
export function RemoveGroup(arg1) {
@@ -94,14 +206,26 @@ 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);
}
export function SendDingDingMessage(arg1, arg2) {
return window['go']['main']['App']['SendDingDingMessage'](arg1, arg2);
}
@@ -130,6 +254,18 @@ export function ShareAnalysis(arg1, arg2) {
return window['go']['main']['App']['ShareAnalysis'](arg1, arg2);
}
export function StockNotice(arg1) {
return window['go']['main']['App']['StockNotice'](arg1);
}
export function StockResearchReport(arg1) {
return window['go']['main']['App']['StockResearchReport'](arg1);
}
export function SummaryStockNews(arg1, arg2, arg3, arg4) {
return window['go']['main']['App']['SummaryStockNews'](arg1, arg2, arg3, arg4);
}
export function UnFollow(arg1) {
return window['go']['main']['App']['UnFollow'](arg1);
}
@@ -141,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);
}

110
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 {
@@ -290,7 +342,27 @@ export namespace data {
export class Settings {
export class SentimentResult {
Score: number;
Category: number;
PositiveCount: number;
NegativeCount: number;
Description: string;
static createFrom(source: any = {}) {
return new SentimentResult(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.Score = source["Score"];
this.Category = source["Category"];
this.PositiveCount = source["PositiveCount"];
this.NegativeCount = source["NegativeCount"];
this.Description = source["Description"];
}
}
export class SettingConfig {
ID: number;
// Go type: time
CreatedAt: any;
@@ -305,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;
@@ -321,9 +387,16 @@ export namespace data {
enableNews: boolean;
darkTheme: boolean;
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 = {}) {
@@ -339,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"];
@@ -355,6 +422,13 @@ export namespace data {
this.enableNews = source["enableNews"];
this.darkTheme = source["darkTheme"];
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 {
@@ -400,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);
@@ -428,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 {
@@ -676,7 +754,9 @@ export namespace models {
icon: string;
alipay: string;
wxpay: string;
wxgzh: string;
buildTimeStamp: number;
officialStatement: string;
IsDel: number;
static createFrom(source: any = {}) {
@@ -694,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"];
}

68
go.mod
View File

@@ -1,25 +1,33 @@
module go-stock
go 1.23
toolchain go1.23.0
go 1.25.0
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.4
github.com/wailsapp/wails/v2 v2.10.1
go.uber.org/zap v1.27.0
golang.org/x/sys v0.30.0
golang.org/x/text v0.22.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
@@ -27,24 +35,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
@@ -54,22 +80,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.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.33.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/net v0.35.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

291
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,16 +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=
@@ -43,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=
@@ -76,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=
@@ -89,66 +202,170 @@ 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=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robertkrimen/otto v0.5.1 h1:avDI4ToRk8k1hppLdYFTuuzND41n37vPGJU7547dGf0=
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/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/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=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
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.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
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=
@@ -159,8 +376,11 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
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.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
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=
@@ -168,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=
@@ -175,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=
@@ -185,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.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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=
@@ -196,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=
@@ -206,22 +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.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
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=
@@ -237,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=

Some files were not shown because too many files have changed in this diff Show More