Compare commits

...

212 Commits

Author SHA1 Message Date
ArvinLovegood
469c4826a3 feat(stock):添加板块概念数据查询功能
- 扩展SearchStockByIndicators工具描述,增加更多股票筛选示例
- 新增SearchBk函数用于查询板块/概念/指数整体数据
- 实现SearchBk在OpenAI API中的调用逻辑
- 更新股票数据标题为"工具筛选出的相关股票数据"
- 添加SearchBk API接口实现
- 更新测试用例验证板块查询功能
2026-01-27 17:25:18 +08:00
ArvinLovegood
6c31f5aa76 feat(ai):添加AI推荐股票批量创建功能
- 在后端服务中实现BatchCreateAiRecommendStocks方法支持批量插入
- 前端组件中增加bkName字段到搜索关键词匹配
- 更新API文档描述,细化止盈价区间格式说明
- 添加批量创建工具函数定义和参数校验规则
- 实现OpenAI API中的批量调用处理逻辑
- 优化板块名称描述为"板块/概念/行业名称"
- 完善风险提示和备注字段的功能说明
2026-01-27 13:17:26 +08:00
ArvinLovegood
e797b64d41 feat(ai-recommend):优化股票推荐记录功能
- 在后端API中集成股票实时数据获取功能,添加当前价格、涨跌幅等字段
- 更新前端组件显示股票最新价格、涨跌幅度和推荐时价格对比
- 添加板块概念字段展示和买入建议标签显示
- 集成lancet库进行数据转换处理
- 更新OpenAI系统提示词以确保调用股票推荐保存函数
2026-01-26 18:30:09 +08:00
ArvinLovegood
12299e8b47 feat(data):添加AI分析报告和ai推荐股票的历史数据查看功能
- 创建aiRecommendStocksList组件展示AI股票推荐列表
- 创建researchIndex组件作为研究分析主界面包含分析报告和股票推荐
- 创建researchReport组件展示AI分析报告列表和详情预览
2026-01-25 14:35:03 +08:00
ArvinLovegood
7412d56409 feat(data):添加AI分析报告和ai推荐股票的历史数据查看功能
- 创建aiRecommendStocksList组件展示AI股票推荐列表
- 创建researchIndex组件作为研究分析主界面包含分析报告和股票推荐
- 创建researchReport组件展示AI分析报告列表和详情预览
2026-01-25 14:17:57 +08:00
ArvinLovegood
f75b457082 feat(stock):添加AI股票推荐功能和分析报告管理
- 新增AI股票推荐相关数据模型和数据库迁移
- 实现AI分析报告列表查询和删除功能
- 集成股票推荐记录的增删查接口
- 添加研究中心界面和股票推荐记录展示页面
- 更新K线图组件支持股票名称显示和数值计算
- 优化后端日志配置和测试代码调试功能
- 添加VIP等级管理和批量删除响应结果功能
2026-01-25 14:13:53 +08:00
ArvinLovegood
a43095cdd4 fix(pricing): 移除VIP2套餐中的硅基流动AI分析服务描述
- 从前端组件about.vue中移除VIP2套餐的硅基流动AI分析服务描述
- 从README.md中移除VIP2套餐的硅基流动AI分析服务描述
- 统一VIP2套餐功能描述为仅包含市场资讯同步功能
2026-01-23 16:01:31 +08:00
ArvinLovegood
6ca0d0df32 feat(settings):添加模型api服务专属HTTP代理配置功能
- 在AIConfig模型中新增httpProxy和httpProxyEnabled字段
- 更新OpenAi结构体以支持代理配置
- 修改OpenAI API调用逻辑使用实例级别的代理设置
- 在前端设置页面添加代理开关和地址输入框
- 扩展数据库AIConfig表结构以存储代理配置
- 增加代理配置的增删改查功能支持
- 调整默认maxTokens值从1024到4096
2026-01-19 09:55:33 +08:00
ArvinLovegood
fea9b06a27 feat(settings):添加模型api服务专属HTTP代理配置功能
- 在AIConfig模型中新增httpProxy和httpProxyEnabled字段
- 更新OpenAi结构体以支持代理配置
- 修改OpenAI API调用逻辑使用实例级别的代理设置
- 在前端设置页面添加代理开关和地址输入框
- 扩展数据库AIConfig表结构以存储代理配置
- 增加代理配置的增删改查功能支持
- 调整默认maxTokens值从1024到4096
2026-01-19 09:47:42 +08:00
ArvinLovegood
1d1e437b47 feat(stock):添加股票资金流向和概念信息查询功能
- 新增 GetStockMoneyData 工具用于查询今日股票资金流入排名
- 新增 GetStockConceptInfo 工具用于获取股票所属概念详细信息
- 添加 StockMoneyDataResp、StockMoneyData、StockMoneyDataDiff 数据模型
- 添加 StockConceptInfoResp、StockConceptInfoResult、StockConceptInfo 数据模型
- 实现 GetStockMoneyData 方法从东方财富接口获取资金流向数据
- 实现 GetStockConceptInfo 方法通过股票代码查询概念题材信息
- 在 OpenAI API 中集成两个新工具的调用逻辑
- 添加相关单元测试验证功能正常工作
2026-01-16 19:32:04 +08:00
ArvinLovegood
eca2f8adee feat(app):添加新闻推送时间限制和代理配置支持
- 在app.go中添加时间差检查,仅当数据时间与当前时间差小于5分钟时才推送新闻
- 在openai_api.go中添加HTTP代理配置支持,根据设置启用代理连接
- 重构openai_api.go中的请求体构建逻辑,支持动态配置thinking参数
- 更新openai_api_test.go中的测试参数以匹配新功能
- 移除settings_api.go中的默认设置初始化逻辑以优化启动流程
2026-01-15 16:42:12 +08:00
ArvinLovegood
49d2109d60 fix(config):修正K线数据天数配置限制
- 将默认K线天数从120调整为60
- 更新前端表单验证最大值为60
- 修复后端配置默认值为60天
- 添加更详细的选股语言示例说明
2026-01-07 17:51:12 +08:00
ArvinLovegood
f9294fbffd feat(stock):添加热门股票排名功能
- 在选股自然语言描述中增加了近期趋势示例说明
- 新增HotStockTable工具函数用于获取热门股票数据
- 实现HotStockTable工具的调用逻辑和参数处理
- 集成雪球热门股票API接口
- 添加分页大小参数支持,默认为50条记录
- 实现Markdown表格格式的热门股票排名展示
2026-01-06 15:33:35 +08:00
ArvinLovegood
cc12a886c1 feat(news):扩展新闻标签匹配和添加热门股票数据功能
- 在新闻标签匹配中增加"外媒简讯"和"外媒"标签支持
- 过滤掉"rotating_light"和"loudspeaker"标签避免重复处理
- 优化GetSource函数支持多标签匹配判断新闻来源
- 添加THSHotStrategy数据模型定义热门策略数据结构
- 实现StrategySquare接口获取热门选股策略数据
- 在OpenAI接口中增加热门股票排名数据查询功能
- 为HotItem模型添加markdown标签支持表格显示
- 增加测试函数验证热门策略表格功能
2026-01-04 13:31:09 +08:00
ArvinLovegood
34ea989d47 refactor(data):优化数据API和测试用例
- 调整并发请求数量,移除市场指数行情的获取逻辑
- 注释掉多个市场指数行情查询的协程,减少不必要的数据请求
- 添加投资者互动数据和宏观经济数据的获取功能
- 更新24小时新闻列表的格式化方式,包含时间和内容标题
- 移除顶部新闻列表和外媒全球新闻资讯的获取逻辑
- 调整测试用例中的参数值,将历史数据天数从30改为5
- 修复股价数据检查逻辑,移除对空字符串的特殊处理
2026-01-01 22:34:19 +08:00
ArvinLovegood
aadff1c5eb ci(workflow):添加macOS平台的Intel和ARM64架构构建支持
- 添加 go-stock-darwin-intel 构建配置
- 添加 go-stock-darwin-arm64 构建配置
- 扩展 macOS 平台的架构覆盖范围
2026-01-01 16:18:29 +08:00
ArvinLovegood
6b7fed00a3 refactor(openai_api):优化新闻资讯获取逻辑
- 移除 TradingViewNews 和 ReutersNew 相关的 goroutine 调用
- 将 GetNewsList2 替换为 GetNews24HoursList 获取24小时市场资讯
- 添加 ClsCalendar 功能获取近期重大事件/会议信息
- 调整 WaitGroup 的计数以匹配新的并发任务数量
- 修改时间格式化显示为标准格式 "2006-01-02 15:04:05"
2026-01-01 13:58:07 +08:00
ArvinLovegood
5af0e28601 feat(telegraph):电报标签功能和前端显示优化
- 实现电报数据的标签关联功能,支持主题标签存储
- 添加电报列表分页查询接口,支持标签预加载
- 在前端新闻列表组件中添加时间显示功能
- 优化新闻列表按日期分组显示的布局结构
- 添加定时刷新财联社电报、新浪财经和外媒数据功能
- 根据新闻列表长度动态调整网格列数显示
2026-01-01 12:00:32 +08:00
ArvinLovegood
c4af77954e refactor(app):将syncNews函数改为方法并集成情感分析
- 将syncNews函数转换为App结构体的方法
- 在新闻同步逻辑中集成情感分析功能
- 添加新闻推送功能到同步流程中
- 修复VIP用户新闻同步的调用方式
2026-01-01 09:07:25 +08:00
ArvinLovegood
8236adb680 feat(news):添加新闻标签旋转灯标识处理
- 新增 rotating_light 标签检测逻辑
- 根据标签内容动态设置 isRed 属性
- 优化新闻电报数据结构的红色标识字段处理
2026-01-01 08:48:01 +08:00
ArvinLovegood
0adfdab441 feat(app):添加VIP2用户市场资讯自动同步功能
- 在VIP2级别中添加自动同步最近24小时市场资讯功能
- 实现自动同步外媒简讯、财联社电报、新浪财经资讯
- 重构VIP验证逻辑到独立的isVip方法中
- 添加syncNews方法实现资讯获取和存储功能
- 更新前端about组件中VIP2功能描述
- 更新README中VIP2功能说明
- 添加NtfyNews数据模型定义
- 添加golang.org/x/exp依赖包
2025-12-31 18:27:26 +08:00
ArvinLovegood
b1c618a9de feat(app):添加VIP用户市场资讯自动同步功能
- 在VIP2级别中添加自动同步最近24小时市场资讯功能
- 实现自动同步外媒简讯、财联社电报、新浪财经资讯
- 重构VIP验证逻辑到独立的isVip方法中
- 添加syncNews方法实现资讯获取和存储功能
- 更新前端about组件中VIP2功能描述
- 更新README中VIP2功能说明
- 添加NtfyNews数据模型定义
- 添加golang.org/x/exp依赖包
2025-12-31 18:27:08 +08:00
ArvinLovegood
709c372bf3 feat(app):添加VIP用户市场资讯自动同步功能
- 在VIP2级别中添加自动同步最近24小时市场资讯功能
- 实现自动同步外媒简讯、财联社电报、新浪财经资讯
- 重构VIP验证逻辑到独立的isVip方法中
- 添加syncNews方法实现资讯获取和存储功能
- 更新前端about组件中VIP2功能描述
- 更新README中VIP2功能说明
- 添加NtfyNews数据模型定义
- 添加golang.org/x/exp依赖包
2025-12-31 18:06:51 +08:00
ArvinLovegood
beb022c448 docs(readme): 更新重大更新部分
- 添加2025.11.21新增带频率权重的情感分析功能
- 插入新功能截图img_1.png
- 保留原有市场资讯AI分析和总结功能说明
- 维持市场行情模块介绍内容
2025-12-19 17:21:37 +08:00
ArvinLovegood
d1aed3419b docs(readme): 更新日志新增AI思考模式与热门选股策略功能
- 在更新日志中添加2025.12.16新增AI思考模式与热门选股策略功能
- 补充相关功能描述与日期信息
- 维护文档结构与格式一致性
2025-12-19 17:16:30 +08:00
ArvinLovegood
48511c61df docs(readme):更新README文档中的更新日志和功能描述
- 添加2025.11.21新增带频率权重的情感分析功能说明
- 添加2025.10.30 AI智能体功能开关及页面水印移除说明
- 添加2025.09.27机构研究报告AI工具函数说明
- 添加2025.08.09 AI智能体聊天功能说明
- 更新选股自然语言描述,增加技术指标和成交量筛选示例
2025-12-19 17:15:05 +08:00
ArvinLovegood
163e8800f9 docs(readme):更新技术支持联系方式
- 将技术支持微信联系方式改为QQ
- 移除微信二维码图片显示
- 更新表格中的联系方式描述
2025-12-17 22:59:20 +08:00
ArvinLovegood
70e0d19c58 feat(ai):新增AI思考模式与热门选股策略功能
- 在NewChatStream和SummaryStockNews接口中增加think参数以支持AI思考模式
- 更新前端组件market.vue和stock.vue,添加思考模式开关控件
- 升级github.com/cloudwego/eino依赖至v0.7.9版本
- 修改ToolFunction结构体,新增Strict字段并调整Parameters为指针类型
- 实现HotStrategyTable工具函数,用于获取并展示热门选股策略表格
- 优化市场新闻API中的去重逻辑,当标题为空时使用内容进行判重
- 在AI对话流中增加对reasoning_content的支持,提升推理过程可见性
- 完善AskAi和AskAiWithTools方法,支持传递thinkingMode参数控制模型推理行为
- 调整FunctionParameters结构体,新增AdditionalProperties字段以增强灵活性
- 修复测试文件中IndustryResearchReport调用参数及注释问题
2025-12-16 15:49:59 +08:00
ArvinLovegood
4e04bacf22 refactor(data):重构情感分析与词频统计模块
- 将 SentimentResult 和 WordFreqWithWeight 结构体移至 models 包统一管理
- 更新 AnalyzeSentiment 和 AnalyzeSentimentWithFreqWeight 函数返回值类型
- 优化 NewsAnalyze 函数实现新闻内容自动获取与分析
- 新增定时任务定期执行新闻情感分析
- 完善数据库模型迁移,支持词频与情感分析结果存储
- 调整前端接口声明文件,确保类型一致性
- 移除冗余代码注释,提升代码可读性
2025-12-12 15:35:50 +08:00
ArvinLovegood
8c75c8533a feat(news):优化新闻列表展示并提取标题
- 后端从富文本中提取 Telegraph 标题
- 前端使用折叠面板展示带标题的新闻项
- 无标题新闻项保持原有展示方式
- 调整标签和文字的排版与样式
- 支持展开/收起新闻详情内容
- 保留时间标签和高亮显示逻辑
2025-12-11 18:39:58 +08:00
ArvinLovegood
1ad02d4b0c style(newsList):优化新闻列表文本显示样式
- 为新闻标题和内容添加换行样式以防止文本溢出
- 使用条件渲染仅在存在标题时显示标题文本
- 调整新闻内容文本的断行属性以提高可读性
- 移除不再使用的渐变文本组件以简化模板结构
2025-12-11 18:26:11 +08:00
ArvinLovegood
a354ab8925 feat(news):添加新闻标题字段并更新测试代码
- 在 Telegraph 模型中新增 Title 字段以存储新闻标题
- 更新 market_news_api.go 文件以支持标题数据的提取和赋值
2025-12-11 16:47:35 +08:00
ArvinLovegood
6d50be8541 fix(main):添加全局panic捕获
- 增加defer函数捕获程序异常
- 记录panic堆栈信息便于调试
- 注释掉包含构建密钥的日志输出

chore(test): 新增字符串处理工具包引入

- 引入lancet/v2/strutil包用于测试
- 添加调试日志展示字符串截取功能
- 完善市场资讯API测试用例
2025-12-10 17:52:18 +08:00
ArvinLovegood
6303d535bc refactor(app):移除测试代码中的授权令牌并调整更新检查逻辑
移除app_test.go中硬编码的GitHub授权令牌,避免安全风险。在app.go的CheckUpdate方法中,将授权令牌从请求头中移除,改为无认证方式获取版本信息,同时优化VIP用户下载链接逻辑。
2025-12-10 16:26:09 +08:00
ArvinLovegood
b464d8f563 chore(config):更新访问令牌
- 替换 app.go 中的旧访问令牌为新令牌
- 注释掉 app_test.go 中的访问令牌以避免测试时使用
- 确保所有 GitHub API 请求使用最新的认证凭证
2025-12-10 16:24:03 +08:00
ArvinLovegood
eea0856c1c feat(stock):更新股票研究与市场新闻功能
- 修改股票研究工具描述,明确获取分析师研究报告
- 调整 TradingView 新闻接口参数,优化请求过滤条件
- 新增代理测试与通知推送测试方法
- 扩大股票搜索范围,提升数据获取数量
- 更新东方财富接口 User-Agent,增强请求稳定性
- 优化美股与港股数据抓取逻辑及休眠时间
- 增加前端消息墙展示标签页,集成外部链接页面
- 调整数据库操作注释,便于后续调试与维护
2025-12-10 15:55:06 +08:00
ArvinLovegood
1dd77d5c08 feat(stock):增强股票情感分析功能并优化GitHub请求
- 为GitHub API请求添加授权头部信息
- 禁用OpenAI接口中的思考模式
- 重构情感分析初始化逻辑,增加异常恢复机制
- 优化词典加载流程,提升系统稳定性
- 调整词语权重计算方式,提高准确性
- 更新测试用例,增强覆盖场景
- 移除无用的错误包引用,清理依赖项
- 修复URL格式化问题,确保请求正确性
2025-12-02 18:45:15 +08:00
ArvinLovegood
9c1c0382ca chore(deps):更新多个依赖库版本
-更新多个依赖库版本
2025-12-02 16:06:54 +08:00
ArvinLovegood
46065f448b feat(openai):启用模型思考模式并设置工具选择
- 在请求体中添加 thinking 字段以启用模型思考模式
- 设置 tool_choice 为 required 以强制使用工具调用
- 保持现有模型配置和其他参数不变
2025-12-02 14:23:52 +08:00
ArvinLovegood
a7cee69e68 chore(deps):更新模块依赖版本
- 更新 github.com/PuerkitoBio/goquery 从 v1.10.1 到 v1.11.0
- 更新 github.com/chromedp/chromedp 从 v0.14.1 到 v0.14.2
- 更新 github.com/cloudwego/eino 从 v0.4.1 到 v0.7.3
- 更新 github.com/cloudwego/eino-ext/components/model/ark 从 v0.1.19 到 v0.1.51
- 更新 github.com/cloudwego/eino-ext/components/model/deepseek 到最新提交
- 更新 github.com/cloudwego/eino-ext/components/model/openai 从 v0.0.0 到 v0.1.5
- 更新 github.com/duke-git/lancet/v2 从 v2.3.4 到 v2.3.8
- 更新 github.com/go-resty/resty/v2 从 v2.16.2 到 v2.17.0
- 更新 github.com/samber/lo 从 v1.49.1 到 v1.52.0
- 更新 github.com/stretchr/testify 从 v1.10.0 到 v1.11.1
- 更新 github.com/tidwall/gjson 从 v1.14.4 到 v1.18.0
- 更新 github.com/wailsapp/wails/v2 从 v2.10.1 到 v2.11.0
- 更新 go.uber.org/zap 从 v1.27.0 到 v1.27.1
- 更新 gorm.io/gorm 从 v1.25.12 到 v1.31.1
- 更新 gorm.io/plugin/dbresolver 从 v1.5.3 到 v1.6.2
- 移除 github.com/getkin/kin-openapi 间接依赖
- 移除 github.com/meguminnnnnnnnn/go-openai 旧版本,更新为 v0.1.0
- 移除 github.com/openai/openai-go 间接依赖
- 添加 github.com/bahlo/generic-list-go 作为间接依赖
- 添加 github.com/eino-contrib/jsonschema 作为间接依赖
- 添加 github.com/gorilla/websocket 作为间接依赖
- 添加 github.com/wk8/go-ordered-map/v2 作为间接依赖
- 添加 google.golang.org/protobuf 和 gopkg.in/check.v1 作为间接依赖
2025-11-30 19:43:30 +08:00
ArvinLovegood
459441f838 feat(stock):优化股票情绪分析词典加载逻辑
- 引入 errors 包处理词典加载错误
- 使用 github.com/vcaesar/cedar 优化词典存储结构
- 修复重复添加词汇时的错误处理逻辑
- 增强用户词典读取稳定性,避免空行导致崩溃
- 改进词汇频率更新机制,提高分词准确性
2025-11-30 10:56:57 +08:00
ArvinLovegood
b4b3b61e8c feat(db):优化数据库连接配置并调整SQLite缓存大小
- 调整SQLite连接字符串,设置缓存大小为-524288
- 修改数据库最大空闲连接数为4
- 修改数据库最大打开连接数为10
- 重新排列导入包顺序,将标准库包放在第三方库之前
2025-11-27 18:10:27 +08:00
ArvinLovegood
b6a99940ab feat(db):优化数据库连接配置并调整SQLite缓存大小
- 调整SQLite连接字符串,设置缓存大小为-524288
- 修改数据库最大空闲连接数为4
- 修改数据库最大打开连接数为10
- 重新排列导入包顺序,将标准库包放在第三方库之前
2025-11-27 18:06:15 +08:00
ArvinLovegood
5b0f34a3bd chore(deps): 更新 golang.org/x 包依赖版本
- 将 golang.org/x/crypto 从 v0.39.0 升级到 v0.44.0
- 将 golang.org/x/net 从 v0.38.0 升级到 v0.47.0
- 将 golang.org/x/sys 从 v0.36.0 升级到 v0.38.0
- 将 golang.org/x/text 从 v0.26.0 升级到 v0.31.0
- 将 golang.org/x/term 从 v0.32.0 升级到 v0.37.0
2025-11-27 17:55:29 +08:00
ArvinLovegood
e34ebf9895 chore(deps):更新前端依赖包版本
- 升级 @vitejs/plugin-vue 至 6.0.2 版本
- 升级 naive-ui 至 2.43.2 版本
- 升级 vite 至 7.2.4 版本
- 更新相关子依赖及类型定义文件
- 调整部分组件展示结构以支持图片显示
2025-11-26 18:27:35 +08:00
ArvinLovegood
c3521c6d7f feat(stock):新增东财用户标识支持并优化新闻抓取逻辑
- 在设置中增加东财唯一标识(qgqpBId)字段,用于搜索股票接口鉴权
- 优化 Telegraph 列表获取逻辑,使用协程并发执行多个数据源请求
- 调整 TradingView 新闻定时任务间隔时间,从60秒减少到10秒
- 修改新闻列表排序规则,优先按数据时间降序排列
- 更新 TradingView 新闻去重条件,由内容匹配改为时间和标题匹配
- 限制 TradingView 新闻详情处理数量,最多只处理前10条
- 完善错误提示信息显示逻辑,兼容不同字段的消息返回
- 删除冗余的 GetLevel 函数,直接在赋值处判断等级逻辑
- 增加测试初始化情感分析模块调用
- 前端表单同步增加 qgqpBId 字段绑定与持久化配置支持
2025-11-26 14:58:12 +08:00
ArvinLovegood
93b37ca621 feat(market):新增外媒新闻功能并优化展示逻辑
- 在定时任务中增加TradingViewNews新闻抓取
- 增加TradingView新闻详情接口及数据解析逻辑
- 修改Telegraph模型字段索引提升查询性能
- 前端新增"外媒"新闻列表展示模块
- 根据HTTP代理配置动态调整新闻栏布局
- 修复时间转换及去重逻辑提高数据准确性
- 调整新闻列表显示样式增强可读性
2025-11-25 17:44:13 +08:00
ArvinLovegood
7069af869b feat(frontend):优化市场分析组件并增强标签处理逻辑
- 添加主要指数(mainIndex),包括中美日韩等地重要城市指数
- 更新图表展示逻辑,使用mainIndex代替原有的china索引
- 引入数字动画效果,提升用户体验
- 在后端增加标签类型过滤,仅处理type为"subject"的标签
- 调整标签词频,通过ReAddToken方法提高关键词权重
2025-11-25 12:23:54 +08:00
ArvinLovegood
dbb6789c05 feat(frontend):市场快讯里面添加全球股指展示功能并优化情绪分析图表
- 引入 GlobalStockIndexes 接口获取全球主要股指数据
- 添加中国主要股指筛选逻辑(上海、深圳、香港等)
- 优化市场情绪图表数值显示精度
- 调整图表布局结构,增强可视化效果
- 修改市场情绪强弱指标名称提升可读性
- 定时获取最新股指数据(每2秒刷新)
- 调整负面金融词汇权重以提高分析准确性
2025-11-24 18:08:10 +08:00
ArvinLovegood
8aed4d2753 feat(dict):优化金融股票分词字典结构与内容
- 删除重复或冗余的词条,如“人工智能”、“云计算”等在多个分类中重复出现的词汇
- 调整并统一章节编号,确保从一至九的连续性和逻辑性
- 移除不再适用的覆盖场景描述,提升字典的专业性与准确性
- 更新权重说明注释,去除不必要的分数细节,保持清晰易懂
- 重新组织词条顺序,使同类项集中,提高检索效率
- 清理负权重词汇列表中的多余条目,强化过滤机制
- 精简A股龙头公司条目,聚焦更广泛的财务与估值指标词条
- 统一格式排版,增强可读性和维护便利性
2025-11-23 20:48:28 +08:00
ArvinLovegood
6bd1bdae02 feat(dict):优化金融股票分词字典结构与内容
- 删除重复或冗余的词条,如“人工智能”、“云计算”等在多个分类中重复出现的词汇
- 调整并统一章节编号,确保从一至九的连续性和逻辑性
- 移除不再适用的覆盖场景描述,提升字典的专业性与准确性
- 更新权重说明注释,去除不必要的分数细节,保持清晰易懂
- 重新组织词条顺序,使同类项集中,提高检索效率
- 清理负权重词汇列表中的多余条目,强化过滤机制
- 精简A股龙头公司条目,聚焦更广泛的财务与估值指标词条
- 统一格式排版,增强可读性和维护便利性
2025-11-23 20:40:57 +08:00
ArvinLovegood
9a40d343aa feat(dict): 优化金融股票分词字典结构与内容
- 删除重复或冗余的词条,如“人工智能”、“云计算”等在多个分类中重复出现的词汇
- 调整并统一章节编号,确保从一至九的连续性和逻辑性
- 移除不再适用的覆盖场景描述,提升字典的专业性与准确性
- 更新权重说明注释,去除不必要的分数细节,保持清晰易懂
- 重新组织词条顺序,使同类项集中,提高检索效率
- 清理负权重词汇列表中的多余条目,强化过滤机制
- 精简A股龙头公司条目,聚焦更广泛的财务与估值指标词条
- 统一格式排版,增强可读性和维护便利性
2025-11-23 20:39:06 +08:00
ArvinLovegood
e4cdad6ffe feat(data): 更新用户词典,新增热点概念与板块词汇
- 添加负权重词汇以降低无差别匹配干扰
- 新增核心热点概念词汇,权重设为700分
- 扩展重点赛道板块词汇,权重设为500分
- 增加事件驱动型概念词汇,权重设为400分
- 调整部分已有词汇格式,确保兼容性
2025-11-23 20:22:29 +08:00
ArvinLovegood
a0005dab96 feat(data): 更新用户词典文件
- 调整了原有词汇的权重值从0.1为-0.1
- 新增多个金融及行业相关词汇,如基金、保险等
- 增加了热点概念词汇,例如冰雪旅游、新能源汽车等
- 添加了具体公司或产品名称,如摩尔线程及其相关概念
- 保留并确认具身智能一词的权重与分类不变
2025-11-23 20:03:23 +08:00
ArvinLovegood
c945ca9322 feat(data):调整新闻获取逻辑与情感分析词频权重
- 修改市场新闻接口查询逻辑,按创建时间倒序排序
- 增加单次获取新闻数量上限至10000条
- 调整股票名称及板块名称在分词器中的基础频率权重
- 修改标签添加时的基础频率值
- 更新情感分析中词语权重判断条件,使用动态基准频率替代固定值200
2025-11-23 19:12:07 +08:00
ArvinLovegood
7bbc6831f4 feat(frontend):实现词云图按权重和频次切换显示
- 添加工具箱按钮用于切换数据展示方式
- 支持按权重、频次和分数三种方式展示词云图
- 优化图表标题和坐标轴标签颜色适配暗色主题
- 调整树图布局顶部间距以避免遮挡标题
- 隐藏树图面包屑导航提升界面简洁性
2025-11-23 11:20:32 +08:00
ArvinLovegood
ab0ccc4fe0 feat(frontend):调整市场情绪仪表盘配置与数据映射逻辑
- 为仪表盘启用暗色主题支持
- 修改仪表盘数值范围从[0,1]到[-100,100]
- 更新颜色分段以匹配新的评分区间
- 调整轴标签位置并重新定义文案映射规则
- 修正数据传入方式,将原始分数转换为适配图表的比例值
- 优化后端接口调用参数,移除固定来源限制以获取更全面的新闻数据
2025-11-23 08:13:35 +08:00
ArvinLovegood
fa658357c9 feat(sentiment):新增市场情绪分析功能
- 添加 AnalyzeMartket 组件用于展示情绪热力图和仪表盘
- 引入 ECharts 实现数据可视化
- 配置定时任务定期更新图表数据
- 在 backend 中扩展情感词典,新增关键词如“被抓”、“超”等
- 优化 sentiment 分析逻辑,提升准确性
- 移除旧版 treemap 图表实现,统一使用新组件渲染
- 调整 single instance ID 为 go-stock
- 更新 base.txt 词库,增加“亏损”、“加工”等词汇
2025-11-22 23:35:02 +08:00
ArvinLovegood
746589a972 feat(backend):优化词典加载与情感分析逻辑
- 重构词典初始化流程,提升加载效率
- 新增CalcToken调用以确保词典更新生效
- 改进用户词典读取路径并增强格式校验
- 忽略空行及注释行,防止无效词条干扰分析
- 使用ReAddToken方法替换原有AddToken实现热更新
- 调整词频权重过滤阈值,提高情感识别准确度
- 引入Score字段用于treemap可视化展示
- 更新基础词典和用户词典内容,强化专业术语覆盖
- 注释冗余测试文本,避免影响正式环境运行
- 增加日志记录以便追踪词典加载与匹配过程
2025-11-22 20:16:14 +08:00
ArvinLovegood
401dd17fa8 feat(stock):加载用户自定义词典进行情感分析(用户可以自己定义词典调整热词权重)
- 在默认词典加载成功后,增加加载用户自定义词典逻辑
- 判断用户词典文件是否存在,避免加载空文件
- 记录用户词典加载成功或失败的日志信息
- 保持原有默认词典加载流程不变
- 确保词典加载错误不会中断程序运行
- 统一词典加载后的日志输出格式
2025-11-22 17:28:40 +08:00
ArvinLovegood
c365bd9534 feat(frontend): 添加官方声明显示功能
- 在 App.vue 中新增 officialStatement 响应式变量
- 从版本信息接口获取官方声明内容并存储
- 将官方声明动态插入到窗口标题中
- 调整窗口标题显示逻辑以包含官方声明
2025-11-22 17:26:31 +08:00
ArvinLovegood
104ee51e13 feat(frontend): 添加官方声明显示功能
- 在 App.vue 中新增 officialStatement 响应式变量
- 从版本信息接口获取官方声明内容并存储
- 将官方声明动态插入到窗口标题中
- 调整窗口标题显示逻辑以包含官方声明
2025-11-22 17:26:12 +08:00
ArvinLovegood
00f3e5f0e0 feat(data): 添加用户词典文件
- 新增包含13个常用词汇的用户词典
- 词汇涵盖公司、国家、市场等业务相关术语
- 为自然语言处理模块提供基础词汇支持
2025-11-22 17:18:35 +08:00
ArvinLovegood
483ffa2244 feat(data): 优化金融分词词典与情感分析逻辑
- 更新基础词典文件,完善覆盖场景并调整部分词条权重
- 移除重复或冗余的美股及中概股名称条目
- 提升多个关键金融术语如“人工智能”、“半导体”等权重至350
- 新增“冲高”、“打开涨停”等交易行为相关词汇并设定合理权重
- 完善“十五五规划”相关内容条目,并分类整理结构
- 在情感分析模块引入basefreq常量替代硬编码数值
- 调整股票名称和板块名称添加逻辑中的频率值计算方式
- 重构用户自定义词典加载流程,增强兼容性和健壮性
- 支持更灵活的用户词典格式(支持词、频、词性三元素)
- 修改词频结果结构体,新增Score字段用于综合评分
- 优化排序规则,依据频率与权重乘积进行降序排列
- 增加调试日志输出,便于追踪分析过程与结果
- 前端Treemap图表展示逻辑同步更新以适配新的评分标准
2025-11-22 17:17:55 +08:00
ArvinLovegood
63d278b9aa feat(stock):加载用户自定义词典进行情感分析(用户可以自己定义词典调整热词权重)
- 在默认词典加载成功后,增加加载用户自定义词典逻辑
- 判断用户词典文件是否存在,避免加载空文件
- 记录用户词典加载成功或失败的日志信息
- 保持原有默认词典加载流程不变
- 确保词典加载错误不会中断程序运行
- 统一词典加载后的日志输出格式
2025-11-22 13:02:33 +08:00
ArvinLovegood
5621d40c71 feat(market):调整词频树图数据计算方式
- 将词频树图中的值计算方式从 Frequency 改为 Frequency * Weight
- 更新 treemap 数据映射逻辑以反映新的权重计算
- 确保前端展示的数据更加准确地反映词汇的重要性
2025-11-22 12:58:41 +08:00
ArvinLovegood
26e9753b94 feat(data):添加十五五规划重点领域关键词到基础词典
- 新增涵盖七个大类的政策关键词汇
- 设置词汇权重范围为310-350,适配政策资讯分词场景
- 包含核心战略方向、科技创新与数字经济等领域术语
- 添加能源与绿色转型相关高频词汇
- 补充高端制造与新兴产业的专业表达
- 增加乡村振兴与农业现代化关键词
- 纳入对外开放与贸易升级术语
- 更新社会民生与公共服务领域用词
2025-11-22 12:55:11 +08:00
ArvinLovegood
b7f6dbd2da feat(data):更新股票情感分析词典并优化测试代码
- 在 base.txt 中新增多个知名美股和中概股词汇,提升分词准确性
- 调整测试代码逻辑,移除随机新闻获取,使用固定文本进行情感分析验证
- 初始化数据库连接和情感分析模块,确保测试环境正常运行
- 添加详细的市场新闻示例文本,增强测试覆盖度和结果可靠性
2025-11-22 08:03:44 +08:00
ArvinLovegood
18dd01b613 feat(data):热词算法优化(最近24小时资讯+去重)
- 新增 GetNews24HoursList 方法,用于获取最近24小时内的新闻数据
- 支持按来源筛选新闻,如“财联社电报”
- 添加内容去重逻辑,避免重复新闻条目
- 自动加载新闻关联的标签信息,并构建主题标签字段
- 优化查询逻辑,提升数据获取效率与准确性
2025-11-22 07:48:00 +08:00
ArvinLovegood
81bb33a135 feat(sentiment):新增带频率权重的情感分析功能
- 新增 AnalyzeSentimentWithFreqWeight 方法,支持词频统计与权重分析
- 扩展前端组件 market.vue,集成词频热力图展示功能
- 更新后端词典库,新增 base.txt 金融专业词汇字典
- 引入 ECharts 实现词频 TreeMap 可视化展示
- 优化情感分析算法,增加对股票名称及行业标签的识别支持
- 完善词频过滤逻辑,去除标点符号与无效字符干扰
- 增加词典初始化方法 InitAnalyzeSentiment,提升分析准确性
2025-11-21 20:17:57 +08:00
ArvinLovegood
9926b61fac fix(data): 调整新闻等级判断逻辑
- 修改 GetLevel 函数中的比较操作符,确保更准确的等级判定
- 将大于等于 "C" 的条件改为严格大于 "C",以符合新的业务需求
2025-11-21 13:35:28 +08:00
ArvinLovegood
5e975b060c fix(backend):偶尔修复闪退BUG
- 添加空数据检查避免nil指针异常
2025-11-21 09:12:54 +08:00
ArvinLovegood
e8f063fd9b feat(news):添加新闻情绪标签显示功能(情绪标签仅供参考)
- 在新闻列表项中新增情绪标签展示
- 根据情绪结果动态设置标签颜色和文本
- 支持看涨、看跌和其他情绪状态的可视化呈现
2025-11-20 19:25:00 +08:00
ArvinLovegood
8b0b53fae7 refactor(data):重构股票数据获取与新闻电报处理逻辑
- 修改 TelegraphList 方法返回类型为 *[]models.Telegraph
- 更新获取新闻电报的调用方式,替换原有接口方法
- 新增 Telegraph 数据创建及标签关联逻辑
- 调整股票基础信息更新策略,采用批量删除后插入
- 移除旧有的增量更新逻辑,提高数据同步效率
- 增加对 Telegraph 标签(subjects)的解析与存储支持
- 修正模型字段注解,移除无效的 gorm 标签配置
- 添加测试函数用于验证股票基础信息同步功能
2025-11-20 18:55:12 +08:00
ArvinLovegood
b29c380055 feat(backend):更新财联社电报爬取功能
- 实现 TelegraphList 方法用于获取财联社电报数据
- 添加 GetLevel 函数判断电报等级是否为重要
- 修改去重逻辑,使用 content 和 time作为唯一标识- 在模型中增加 DataTime 字段以支持时间查询
- 更新测试文件,新增对 TelegraphList 的测试用例- 调整 GetNewsList2 方法,触发 TelegraphList 爬取任务
2025-11-18 17:46:00 +08:00
ArvinLovegood
cf58a707c7 refactor(stock):重构股票数据接口并新增历史资金流向功能
- 调整 import 包顺序,优化代码结构
- 新增 GetStockHistoryMoneyData 方法用于获取历史资金流向
- 移除无用空行,提升代码可读性- 更新 README 文档中的大模型平台链接信息
- 删除测试文件中冗余的日志调试代码行
2025-11-16 11:17:45 +08:00
ArvinLovegood
1ae1bb0116 feat(vip):移除水印内容显示
- 更新窗口标题以包含授权声明
- 移除水印内容显示为空字符串
2025-10-31 18:37:26 +08:00
ArvinLovegood
d8971935ee feat(settings):添加AI智能体功能开关(建议关闭,使用体验不理想)
- 在设置界面新增AI智能体启用开关
- 支持保存和读取AI智能体启用状态
- 更新配置模型以支持新的启用选项
- 动态控制菜单项显示状态
- 同步前后端配置结构以支持新功能
2025-10-31 17:11:17 +08:00
ArvinLovegood
9c68458b81 feat(stockhotmap):添加财联社行情数据标签页
- 在 stockhotmap 组件中新增财联社-行情数据标签页
- 配置嵌入链接指向 https://www.cls.cn/quotation
- 设置高度为 calc(100vh - 252px) 以适配页面布局
2025-10-31 16:54:05 +08:00
ArvinLovegood
b367d1eb40 feat(stockhotmap):启用百度股市通和摸鱼网页标签页
- 取消注释百度股市通标签页,恢复其功能
- 取消注释摸鱼标签页,恢复其功能
-保持其他标签页配置不变
2025-10-29 16:18:29 +08:00
ArvinLovegood
8fe79adbb1 feat(stock):增加股票搜索结果数量并优化搜索条件
- 将随机返回的股票数量从5-10支增加到5-20支
- 更新测试用例中的搜索关键词,提高筛选条件准确性
- 调整搜索逻辑以适应新的数据范围和质量要求
2025-10-29 16:13:30 +08:00
ArvinLovegood
1d81fdba87 chore(deps):指标选股问题
- 将 User-Agent 中的 Firefox 版本从 140.0 更新为 145.0
2025-10-27 18:16:25 +08:00
ArvinLovegood
6aca0e15cc fix(backend): 指标选股问题
- 修改fingerprint和requestId为新的固定值
- 将timestamp设置为动态生成的时间戳
- 更新xcId为新的标识符
- 在测试中增加返回结果的日志输出
2025-10-27 18:15:02 +08:00
ArvinLovegood
173ce6f243 fix(backend):修复新闻列表排序逻辑
- 将新闻列表按ID降序排列,确保最新新闻优先显示
- 保持is_red字段的降序排序,确保重要新闻优先显示
- 修复了查询条件中缺少的排序字段逗号问题
2025-09-29 18:10:55 +08:00
ArvinLovegood
e7875e73d3 feat(backend):新增并使用GetNewsList2方法以支持标签功能
- 在MarketNewsApi中添加GetNewsList2方法,支持根据source和limit获取新闻列表
- GetNewsList2方法中预加载TelegraphTags并关联标签名称
- 修改openai_api.go中调用GetNewsList的地方为GetNewsList2- 调整获取新闻列表的参数,使用固定source和随机limit值
2025-09-29 17:34:31 +08:00
ArvinLovegood
ca4727db80 fix(app):更新股票研究报告工具描述
- 修改了GetStockResearchReport函数的描述信息
- 简化了描述文本,去除冗余的"机构的"前缀
- 保持了原有功能和参数结构不变
2025-09-27 19:12:51 +08:00
ArvinLovegood
84ffe7c5fd refactor(openai_api):移除行业板块相关工具调用逻辑
- 注释掉QueryBKDictInfo工具的调用实现
- 注释掉GetIndustryResearchReport工具的调用实现
- 移除对freecache包的依赖引用- 保留GetStockResearchReport工具的调用逻辑
- 简化工具调用处理流程
2025-09-27 18:55:28 +08:00
ArvinLovegood
da02d1bd1c feat(stock):更新股票研究报接口参数并完善行业研究描述
- 修改行业研究工具函数描述,增加调用前需查询行业代码的提示
- 更新股票研究报接口测试用例中的股票代码参数值
- 完善行业研究报相关功能的使用说明和参数校验逻辑- 优化研究报数据获取流程,提升接口稳定性与准确性
2025-09-27 16:59:05 +08:00
ArvinLovegood
bae2bf9c5c docs(readme): 更新AI智能选股功能描述
- 在功能说明中添加AI智能体功能的描述
- 保持其他功能状态和备注信息不变
2025-09-27 15:46:33 +08:00
ArvinLovegood
6568b5949a docs(readme): 更新AI智能选股功能状态
- 将AI智能选股功能状态从开发中更新为已完成
- 修改功能描述为"市场行情-》AI总结"
- 调整了功能备注信息的表述方式
2025-09-27 15:44:03 +08:00
ArvinLovegood
c4287f9b78 feat(ai):新增了机构/券商的研究报告AI工具函数
- 添加 QueryBKDictInfo 工具用于获取板块/行业字典信息
- 实现 GetIndustryResearchReport 工具用于获取行业研究报告
- 实现 GetStockResearchReport 工具用于获取股票研究报告
- 在数据库中新增 BKDict 模型并自动迁移
- 更新 MarketNewsApi 测试用例以支持新工具调用
- 在 OpenAI API 中集成新工具的调用逻辑与响应处理
2025-09-27 15:39:37 +08:00
ArvinLovegood
87441d8923 feat(ai):新增了机构/券商的研究报告AI工具函数
- 添加 QueryBKDictInfo 工具用于获取板块/行业字典信息
- 实现 GetIndustryResearchReport 工具用于获取行业研究报告
- 实现 GetStockResearchReport 工具用于获取股票研究报告
- 在数据库中新增 BKDict 模型并自动迁移
- 更新 MarketNewsApi 测试用例以支持新工具调用
- 在 OpenAI API 中集成新工具的调用逻辑与响应处理
2025-09-27 15:39:25 +08:00
ArvinLovegood
ebd166e72b fix(agent):调整代理最大步骤数并修复前端显示逻辑
- 减少代理工具调用的最大步骤数计算方式
- 启用并修复前端聊天组件中的推理内容显示
- 修复删除分组时传递正确参数类型
- 更新依赖项,移除旧版本的 chromedp 和 golang.org/x/sys
2025-09-27 10:59:45 +08:00
ArvinLovegood
494a60debe refactor(backend):注释掉获取股势通资讯的代码
- 在 openai_api.go 中注释掉了获取股势通资讯的相关代码
- 在 openai_api_test.go 中添加了对 SearchGuShiTongStockInfo 函数的测试用例
2025-09-16 15:14:38 +08:00
ArvinLovegood
b3e2565a02 build: 更新多个依赖至最新版本 2025-09-16 14:19:11 +08:00
ArvinLovegood
c0a87d5d2e perf(app):优化股票基础信息更新逻辑
- 移除了定时任务中重复的股票基础信息检查逻辑
- 在获取股票基础信息前增加数据库记录数量检查,避免重复更新
2025-08-26 18:28:10 +08:00
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
110 changed files with 1760454 additions and 2335 deletions

View File

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

View File

@@ -10,8 +10,9 @@
![扫码_搜索联合传播样式-白色版.png](build/screenshot/%E6%89%AB%E7%A0%81_%E6%90%9C%E7%B4%A2%E8%81%94%E5%90%88%E4%BC%A0%E6%92%AD%E6%A0%B7%E5%BC%8F-%E7%99%BD%E8%89%B2%E7%89%88.png)
### 📈 交流群
- QQ交流群2[点击链接加入群聊【go-stock交流群2】892666282](https://qm.qq.com/q/5mYiy6Yxh0)
- QQ交流群[点击链接加入群聊【go-stock交流群】491605333(已满会定期清理,随缘入群)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0YQ8qD3exahsD4YLNhzQTWe5ssstWC89&authKey=usOMMRFtIQDC%2FYcatHYapcxQbJ7PwXPHK9OypTXWzNjAq%2FRVvQu9bj2lRgb%2BSZ3p&noverify=0&group_code=491605333)
[//]: # (- QQ交流群2[点击链接加入群聊【go-stock交流群2】:892666282]&#40;https://qm.qq.com/q/5mYiy6Yxh0&#41;)
- QQ交流群[点击链接加入群聊【go-stock交流群】491605333(定期清理,随缘入群)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0YQ8qD3exahsD4YLNhzQTWe5ssstWC89&authKey=usOMMRFtIQDC%2FYcatHYapcxQbJ7PwXPHK9OypTXWzNjAq%2FRVvQu9bj2lRgb%2BSZ3p&noverify=0&group_code=491605333)
### ✨ 简介
- 本项目基于Wails和NaiveUI开发结合AI大模型构建的股票分析工具。
@@ -21,34 +22,45 @@
- 开发环境主要基于Windows10+,其他平台未测试或功能受限。
### 📦 立即体验
- 安装版:[go-stock-amd64-installer.exe](https://github.com/ArvinLovegood/go-stock/releases)
[//]: # (- 安装版:[go-stock-amd64-installer.exe]&#40;https://github.com/ArvinLovegood/go-stock/releases&#41;)
- 绿色版:[go-stock-windows-amd64.exe](https://github.com/ArvinLovegood/go-stock/releases)
- MACOS绿色版[go-stock-darwin-universal](https://github.com/ArvinLovegood/go-stock/releases)
[//]: # (- MACOS安装版[go-stock-darwin-universal.pkg]&#40;https://github.com/ArvinLovegood/go-stock/releases&#41;)
### 💬 支持大模型/平台
| 模型 | 状态 | 备注 |
| --- | --- |---------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [OpenAI](https://platform.openai.com/) | ✅ | 可接入任何 OpenAI 接口格式模型 |
| [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 |
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型运行平台 |
| [AnythingLLM](https://anythingllm.com/) | ✅ | 本地知识库 |
| [DeepSeek](https://www.deepseek.com/) | ✅ | deepseek-reasoner,deepseek-chat |
| [大模型聚合平台](https://cloud.siliconflow.cn/i/foufCerk) | ✅ | 如:[硅基流动](https://cloud.siliconflow.cn/i/foufCerk)[火山方舟](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ) ,[优云智算](https://www.compshare.cn/image-community?ytag=GPU_YY-gh_gostock) |
| 模型 | 状态 | 备注 |
| --- | --- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [OpenAI](https://platform.openai.com/) | ✅ | 可接入任何 OpenAI 接口格式模型 |
| [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 |
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型运行平台 |
| [AnythingLLM](https://anythingllm.com/) | ✅ | 本地知识库 |
| [DeepSeek](https://www.deepseek.com/) | ✅ | deepseek-reasoner,deepseek-chat |
| [大模型聚合平台](https://cloud.siliconflow.cn/i/foufCerk) | ✅ | 如:[硅基流动](https://cloud.siliconflow.cn/i/foufCerk)[火山方舟](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ) |
### <span style="color: #568DF4;">各位亲爱的朋友们,如果您对这个项目感兴趣,请先给我一个<i style="color: #EA2626;">star</i>吧,谢谢!</span>💕
- 优云智算by UCloud万卡规模4090免费用10小时新人注册另增50万tokens海量热门源项目镜像一键部署[注册链接](https://www.compshare.cn/image-community?ytag=GPU_YY-gh_gostock)
- 经测试目前硅基流动(siliconflow)提供的deepSeek api 服务比较稳定,注册即送2000万Tokens[注册链接](https://cloud.siliconflow.cn/i/foufCerk)
- 火山方舟:每个模型注册即送50万tokens[注册链接](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ)
[//]: # (- 优云智算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全部功能,启动时自动同步最近24小时市场资讯(包括外媒简讯) |
| 每月赞助 X RMB | vipX | 🧩 更多计划视go-stock开源项目发展情况而定...(承接GitHub项目README广告推广💖) |
## 🧩 重大功能开发计划
| 功能说明 | 状态 | 备注 |
|-----------------|----|----------------------------------------------------------------------------------------------------------|
| 股票分析知识库 | 🚧 | 未来计划 |
| Ai智能选股 | 🚧 | Ai智能选股功能开发中(下半年重点开发计划) |
| Ai智能选股 | | Ai智能选股功能(市场行情-》AI总结/AI智能体功能) |
| ETF支持 | 🚧 | ETF数据支持 (目前可以查看净值和估值) |
| 美股支持 | ✅ | 美股数据支持 |
| 港股支持 | ✅ | 港股数据支持 |
@@ -57,6 +69,15 @@
| 不再强制依赖Chrome浏览器 | ✅ | 默认使用edge浏览器抓取新闻资讯 |
## 👀 更新日志
### 2025.12.16 新增AI思考模式与热门选股策略功能
### 2025.11.21 新增带频率权重的情感分析功能
### 2025.10.30 添加AI智能体功能开关(默认关闭,因为使用体验不理想),移除页面水印
### 2025.09.27 添加机构/券商的研究报告AI工具函数
### 2025.08.09 添加AI智能体聊天功能
### 2025.07.08 实现软件自动更新功能
### 2025.07.07 卡片添加迷你分时图
### 2025.07.05 MacOs支持
### 2025.07.01 AI分析集成工具函数AI分析将更加智能
### 2025.06.30 添加指标选股功能
### 2025.06.27 添加财经日历和重大事件时间轴功能
### 2025.06.25 添加热门股票、事件和话题功能
@@ -100,6 +121,8 @@
## 🦄 重大更新
### BIG NEWS !!! 重大更新!!!
- 2025.11.21 新增带频率权重的情感分析功能
![img_1.png](build/screenshot/img15.png)
- 2025.04.25 市场资讯支持AI分析和总结让AI帮你读市场
![img.png](img.png)
- 2025.04.24 新增市场行情模块:即时掌握全球市场行情资讯/动态从此再也不用偷摸去各大财经网站啦。go-stock一键帮你搞定
@@ -146,15 +169,15 @@
## 🐳 关于技术支持申明
- 本软件基于开源技术构建使用Wails、NaiveUI、Vue、AI大模型等开源项目。 技术上如有问题,可以先向对应的开源社区请求帮助。
- 开源不易,本人精力和时间有限,如需一对一技术支持,请先赞助。联系微信(备注 技术支持)ArvinLovegood
- 开源不易,本人精力和时间有限,如需一对一技术支持,请先赞助。联系QQ(备注 技术支持)506808970
<img src="./build/wx.jpg" width="301px" height="402px" alt="ArvinLovegood">
[//]: # (<img src="./build/wx.jpg" width="301px" height="402px" alt="ArvinLovegood">)
| 技术支持方式 | 赞助(元) |
|:--------------------------------|:-----:|
| 加 QQ506808970微信ArvinLovegood | 100/次 |
| 长期技术支持(不限次数,新功能优先体验等) | 5000 |
| 加 QQ506808970 | 100/次 |
| 长期技术支持(不限次数,新功能优先体验等) | 5000 |

1295
app.go

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,11 @@
package main
import (
"go-stock/backend/agent"
"go-stock/backend/data"
"go-stock/backend/models"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// @Author spark
@@ -24,38 +27,86 @@ func (a *App) StockNotice(stockCode string) []any {
func (a *App) IndustryResearchReport(industryCode string) []any {
return data.NewMarketNewsApi().IndustryResearchReport(industryCode, 7)
}
func (a App) EMDictCode(code string) []any {
func (a *App) EMDictCode(code string) []any {
return data.NewMarketNewsApi().EMDictCode(code, a.cache)
}
func (a App) AnalyzeSentiment(text string) data.SentimentResult {
func (a *App) AnalyzeSentiment(text string) models.SentimentResult {
return data.AnalyzeSentiment(text)
}
func (a App) HotStock(marketType string) *[]models.HotItem {
func (a *App) HotStock(marketType string) *[]models.HotItem {
return data.NewMarketNewsApi().XUEQIUHotStock(100, marketType)
}
func (a App) HotEvent(size int) *[]models.HotEvent {
func (a *App) HotEvent(size int) *[]models.HotEvent {
if size <= 0 {
size = 10
}
return data.NewMarketNewsApi().HotEvent(size)
}
func (a App) HotTopic(size int) []any {
func (a *App) HotTopic(size int) []any {
if size <= 0 {
size = 10
}
return data.NewMarketNewsApi().HotTopic(size)
}
func (a App) InvestCalendarTimeLine(yearMonth string) []any {
func (a *App) InvestCalendarTimeLine(yearMonth string) []any {
return data.NewMarketNewsApi().InvestCalendar(yearMonth)
}
func (a App) ClsCalendar() []any {
func (a *App) ClsCalendar() []any {
return data.NewMarketNewsApi().ClsCalendar()
}
func (a App) SearchStock(words string) map[string]any {
func (a *App) SearchStock(words string) map[string]any {
return data.NewSearchStockApi(words).SearchStock(5000)
}
func (a *App) GetHotStrategy() map[string]any {
return data.NewSearchStockApi("").HotStrategy()
}
func (a *App) ChatWithAgent(question string, aiConfigId int, sysPromptId *int) {
ch := agent.NewStockAiAgentApi().Chat(question, aiConfigId, sysPromptId)
for msg := range ch {
runtime.EventsEmit(a.ctx, "agent-message", msg)
}
}
func (a *App) AnalyzeSentimentWithFreqWeight(text string) map[string]any {
result, cleanFrequencies := data.NewsAnalyze(text, false)
return map[string]any{
"result": result,
"frequencies": cleanFrequencies,
}
}
func (a *App) GetAIResponseResultList(query models.AIResponseResultQuery) *models.AIResponseResultPageData {
page, err := data.NewAIResponseResultService().GetAIResponseResultList(query)
if err != nil {
return &models.AIResponseResultPageData{}
}
return page
}
func (a *App) DeleteAIResponseResult(id string) string {
err := data.NewAIResponseResultService().DeleteAIResponseResult(id)
if err != nil {
return "删除失败"
}
return "删除成功"
}
func (a *App) BatchDeleteAIResponseResult(ids []uint) string {
err := data.NewAIResponseResultService().BatchDeleteAIResponseResult(ids)
if err != nil {
return "删除失败"
}
return "删除成功"
}
func (a *App) GetAiRecommendStocksList(query models.AiRecommendStocksQuery) *models.AiRecommendStocksPageData {
page, err := data.NewAiRecommendStocksService().GetAiRecommendStocksList(&query)
if err != nil {
return &models.AiRecommendStocksPageData{}
}
return page
}

View File

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

View File

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

View File

@@ -1,9 +1,15 @@
package main
import (
"context"
"encoding/json"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"testing"
"time"
"github.com/go-resty/resty/v2"
)
// @Author spark
@@ -23,3 +29,46 @@ func TestIsUSTradingTime(t *testing.T) {
t.Log(IsUSTradingTime(time.Now()))
}
func TestCheckStockBaseInfo(t *testing.T) {
db.Init("./data/stock.db")
NewApp().CheckStockBaseInfo(context.Background())
}
func TestJson(t *testing.T) {
db.Init("./data/stock.db")
jsonStr := "{\n\t\t\"id\" : 3334,\n\t\t\"created_at\" : \"2025-02-28 16:49:31.8342514+08:00\",\n\t\t\"updated_at\" : \"2025-02-28 16:49:31.8342514+08:00\",\n\t\t\"deleted_at\" : null,\n\t\t\"code\" : \"PUK.US\",\n\t\t\"name\" : \"英国保诚集团\",\n\t\t\"full_name\" : \"\",\n\t\t\"e_name\" : \"\",\n\t\t\"exchange\" : \"NASDAQ\",\n\t\t\"type\" : \"stock\",\n\t\t\"is_del\" : 0,\n\t\t\"bk_name\" : null,\n\t\t\"bk_code\" : null\n\t}"
v := &models.StockInfoUS{}
json.Unmarshal([]byte(jsonStr), v)
logger.SugaredLogger.Infof("v:%+v", v)
db.Dao.Model(v).Updates(v)
}
func TestUpdateCheck(t *testing.T) {
releaseVersion := &models.GitHubReleaseVersion{}
_, err := resty.New().R().
SetResult(releaseVersion).
SetHeader("Accept", "application/vnd.github+json").
SetHeader("X-GitHub-Api-Version", "2022-11-28").
Get("https://api.github.com/repos/ArvinLovegood/go-stock/releases/latest")
// https://api.github.com/repos/OWNER/REPO/releases/latest
if err != nil {
logger.SugaredLogger.Errorf("get github release version error:%s", err.Error())
return
}
logger.SugaredLogger.Infof("releaseVersion:%+v", releaseVersion)
}
func TestGetScreenResolution(t *testing.T) {
x, y, w, h, err := getScreenResolution()
if err != nil {
logger.SugaredLogger.Errorf("get screen resolution error:%s", err.Error())
return
}
logger.SugaredLogger.Infof("x:%d,y:%d,w:%d,h:%d", x, y, w, h)
}

216
app_windows.go Normal file
View File

@@ -0,0 +1,216 @@
//go:build windows
// +build windows
package main
import (
"context"
"fmt"
"go-stock/backend/data"
"go-stock/backend/db"
"go-stock/backend/logger"
"time"
"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"
)
// 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(width), int(height), 1456, 768, nil
return int(1366), int(768), 1456, 768, nil
}

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

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

View File

@@ -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,88 @@
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"
"github.com/duke-git/lancet/v2/fileutil"
)
// @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")
md := strings.Builder{}
ch := NewStockAiAgentApi().Chat("分析一下立讯精密", 0, nil)
for message := range ch {
logger.SugaredLogger.Infof("res:%s", message.String())
md.WriteString(message.String())
}
logger.SugaredLogger.Info(md.String())
fileutil.WriteStringToFile("../../data/result.md", md.String(), false)
}

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,34 @@
package tools
import (
"context"
"encoding/json"
"go-stock/backend/data"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/schema"
"github.com/coocood/freecache"
)
// @Author spark
// @Date 2025/9/27 14:09
// @Desc
// -----------------------------------------------------------------------------------
type ToolQueryBKDict struct{}
func (t ToolQueryBKDict) Info(ctx context.Context) (*schema.ToolInfo, error) {
return &schema.ToolInfo{
Name: "QueryBKDictInfo",
Desc: "获取所有板块/行业名称或者代码(bkCode,bkName)",
}, nil
}
func (t ToolQueryBKDict) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
resp := data.NewMarketNewsApi().EMDictCode("016", freecache.NewCache(100))
bytes, err := json.Marshal(resp)
return string(bytes), err
}
func GetQueryBKDictTool() tool.InvokableTool {
return &ToolQueryBKDict{}
}

View File

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

View File

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

View File

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

@@ -0,0 +1,138 @@
// Package data ai_recommend_stocks_api.go
package data
import (
"go-stock/backend/db"
"go-stock/backend/models"
"github.com/duke-git/lancet/v2/slice"
"github.com/duke-git/lancet/v2/strutil"
)
type AiRecommendStocksService struct{}
func NewAiRecommendStocksService() *AiRecommendStocksService {
return &AiRecommendStocksService{}
}
// CreateAiRecommendStocks 创建AI推荐股票记录
func (s *AiRecommendStocksService) CreateAiRecommendStocks(recommend *models.AiRecommendStocks) error {
result := db.Dao.Create(recommend)
return result.Error
}
func (s *AiRecommendStocksService) BatchCreateAiRecommendStocks(recommends []*models.AiRecommendStocks) error {
result := db.Dao.Create(recommends)
return result.Error
}
// GetAiRecommendStocksList 分页查询AI推荐股票记录
func (s *AiRecommendStocksService) GetAiRecommendStocksList(query *models.AiRecommendStocksQuery) (*models.AiRecommendStocksPageData, error) {
var list []models.AiRecommendStocks
var total int64
q := db.Dao.Model(&models.AiRecommendStocks{})
// 构建查询条件
if query.StockCode != "" {
q.Or("stock_code LIKE ?", "%"+query.StockCode+"%")
}
if query.StockName != "" {
q.Or("stock_name LIKE ?", "%"+query.StockName+"%")
}
if query.BkCode != "" {
q.Or("bk_code LIKE ?", "%"+query.BkCode+"%")
}
if query.BkName != "" {
q.Or("bk_name LIKE ?", "%"+query.BkName+"%")
}
if query.StartDate != "" && query.EndDate != "" {
query.StartDate = strutil.ReplaceWithMap(query.StartDate, map[string]string{
"T": " ",
"Z": "",
})
query.StartDate = strutil.ReplaceWithMap(query.StartDate, map[string]string{
"T": " ",
"Z": "",
})
q = q.Where("data_time BETWEEN ? AND ?", query.StartDate, query.EndDate)
}
// 计算总数
err := q.Count(&total).Error
if err != nil {
return nil, err
}
// 设置默认分页参数
page := query.Page
pageSize := query.PageSize
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > 100 {
pageSize = 10
}
// 执行分页查询
offset := (page - 1) * pageSize
err = q.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&list).Error
if err != nil {
return nil, err
}
totalPages := int((total + int64(pageSize) - 1) / int64(pageSize))
stockCodes := slice.Map(list, func(index int, item models.AiRecommendStocks) string {
return ConvertTushareCodeToStockCode(item.StockCode)
})
stockData, _ := NewStockDataApi().GetStockCodeRealTimeData(stockCodes...)
for _, info := range *stockData {
for idx, item := range list {
if ConvertTushareCodeToStockCode(item.StockCode) == ConvertTushareCodeToStockCode(info.Code) {
list[idx].StockCurrentPrice = info.Price
list[idx].StockPrePrice = info.PreClose
list[idx].StockCurrentPriceTime = info.Date + " " + info.Time
}
}
}
return &models.AiRecommendStocksPageData{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
}, nil
}
// GetAiRecommendStocksByID 根据ID获取AI推荐股票记录
func (s *AiRecommendStocksService) GetAiRecommendStocksByID(id uint) (*models.AiRecommendStocks, error) {
var recommend models.AiRecommendStocks
err := db.Dao.First(&recommend, id).Error
if err != nil {
return nil, err
}
return &recommend, nil
}
// UpdateAiRecommendStocks 更新AI推荐股票记录
func (s *AiRecommendStocksService) UpdateAiRecommendStocks(id uint, recommend *models.AiRecommendStocks) error {
result := db.Dao.Model(&models.AiRecommendStocks{}).Where("id = ?", id).Updates(recommend)
return result.Error
}
// DeleteAiRecommendStocks 根据ID删除AI推荐股票记录
func (s *AiRecommendStocksService) DeleteAiRecommendStocks(id uint) error {
// 使用软删除
result := db.Dao.Where("id = ?", id).Delete(&models.AiRecommendStocks{})
return result.Error
}
// BatchDeleteAiRecommendStocks 批量删除AI推荐股票记录
func (s *AiRecommendStocksService) BatchDeleteAiRecommendStocks(ids []uint) error {
// 使用软删除
result := db.Dao.Where("id IN ?", ids).Delete(&models.AiRecommendStocks{})
return result.Error
}

View File

@@ -0,0 +1,97 @@
package data
import (
"go-stock/backend/db"
"go-stock/backend/models"
"github.com/duke-git/lancet/v2/strutil"
)
type AIResponseResultService struct{}
func NewAIResponseResultService() *AIResponseResultService {
return &AIResponseResultService{}
}
// GetAIResponseResultList 分页查询AI响应结果
func (s *AIResponseResultService) GetAIResponseResultList(query models.AIResponseResultQuery) (*models.AIResponseResultPageData, error) {
var list []models.AIResponseResult
var total int64
q := db.Dao.Model(&models.AIResponseResult{})
// 构建查询条件
if query.ChatId != "" {
q.Where("chat_id LIKE ?", "%"+query.ChatId+"%")
}
if query.ModelName != "" {
q.Or("model_name LIKE ?", "%"+query.ModelName+"%")
}
if query.StockCode != "" {
q.Or("stock_code LIKE ?", "%"+query.StockCode+"%")
}
if query.Question != "" {
q.Or("question LIKE ?", "%"+query.Question+"%")
}
if query.StartDate != "" && query.EndDate != "" {
query.StartDate = strutil.ReplaceWithMap(query.StartDate, map[string]string{
"T": " ",
"Z": "",
})
query.StartDate = strutil.ReplaceWithMap(query.StartDate, map[string]string{
"T": " ",
"Z": "",
})
q = q.Where("created_at BETWEEN ? AND ?", query.StartDate, query.EndDate)
}
// 计算总数
err := q.Count(&total).Error
if err != nil {
return nil, err
}
// 设置默认分页参数
page := query.Page
pageSize := query.PageSize
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > 100 {
pageSize = 10
}
// 执行分页查询
offset := (page - 1) * pageSize
err = q.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&list).Error
if err != nil {
return nil, err
}
totalPages := int((total + int64(pageSize) - 1) / int64(pageSize))
return &models.AIResponseResultPageData{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
}, nil
}
// DeleteAIResponseResult 根据ID删除AI响应结果
func (s *AIResponseResultService) DeleteAIResponseResult(id string) error {
// 使用软删除
result := db.Dao.Where("id = ?", id).Delete(&models.AIResponseResult{})
return result.Error
}
// BatchDeleteAIResponseResult 批量删除AI响应结果
func (s *AIResponseResultService) BatchDeleteAIResponseResult(ids []uint) error {
// 使用软删除
result := db.Dao.Where("id IN ?", ids).Delete(&models.AIResponseResult{})
return result.Error
}

View File

@@ -0,0 +1,26 @@
package data
import (
"go-stock/backend/db"
"go-stock/backend/models"
"testing"
)
// @Author spark
// @Date 2026/1/23 17:39
// @Desc
//-----------------------------------------------------------------------------------
func TestAIResponseResultService_GetAIResponseResultList(t *testing.T) {
db.Init("../../data/stock.db")
service := NewAIResponseResultService()
list, err := service.GetAIResponseResultList(models.AIResponseResultQuery{
Page: 1,
PageSize: 10,
})
if err != nil {
return
}
t.Log(list)
}

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

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

View File

@@ -0,0 +1,428 @@
# 金融股票全场景分词字典(最终去重优化版)
# 格式:单词 权重 词性 | 权重280-350分核心术语优先匹配无重复词汇
# 一、净买卖与资金流向(核心交易表述)
净卖出 340 v
净买入 340 v
净卖出额 330 n
净买入额 330 n
净卖出量 330 n
净买入量 330 n
资金净流出 340 n
资金净流入 340 n
净额 330 n
买卖净额 330 n
资金净额 330 n
北向资金净买入 330 n
北向资金净卖出 330 n
南向资金净买入 320 n
南向资金净卖出 320 n
主力资金净买入 330 n
主力资金净卖出 330 n
散户资金净买入 320 n
散户资金净卖出 320 n
机构资金净买入 330 n
机构资金净卖出 330 n
游资净买入 320 n
游资净卖出 320 n
大单净买入 320 n
大单净卖出 320 n
中单净买入 320 n
中单净卖出 320 n
小单净买入 320 n
小单净卖出 320 n
净买入占比 320 n
净卖出占比 320 n
净买入率 320 n
净卖出率 320 n
连续净买入 320 v
连续净卖出 320 v
单日净买入 320 n
单日净卖出 320 n
累计净买入 320 n
累计净卖出 320 n
净买入创纪录 310 adj
净卖出创纪录 310 adj
净买入放量 310 v
净卖出放量 310 v
净买入缩量 310 v
净卖出缩量 310 v
净多 310 n
净空 310 n
净多头 310 n
净空头 310 n
净多头头寸 310 n
净空头头寸 310 n
跌超 310 n
跌逾 310 n
# 二、金融资讯与市场分析
金融资讯 350 n
市场快讯 340 n
财经新闻 340 n
政策解读 330 n
市场分析 330 n
行业研报 320 n
宏观经济 330 n
微观层面 310 n
基本面 320 n
技术面 320 n
资金面 320 n
政策面 320 n
市场情绪 320 n
风险偏好 310 n
流动性 320 n
估值修复 310 n
价值投资 310 n
趋势投资 310 n
波段操作 310 n
左侧交易 290 n
右侧交易 290 n
止损止盈 300 n
仓位管理 300 n
资产配置 310 n
分散投资 290 n
集中投资 290 n
风险控制 310 n
系统性风险 300 n
非系统性风险 290 n
黑天鹅事件 310 n
灰犀牛事件 300 n
熔断机制 300 n
市场监管 310 n
信息披露 310 n
内幕交易 300 n
操纵市场 300 n
亏损 100 n
加工 100 n
# 三、全球主要股指(含中英文缩写)
# 中国市场
A股 350 n
港股 350 n
上证指数 350 n
深证成指 350 n
创业板指 340 n
科创板指 330 n
北证50 330 n
沪深300 350 n
沪深300指数 350 n
中证500 340 n
中证500指数 340 n
中证1000 330 n
中证1000指数 330 n
上证50 340 n
上证50指数 340 n
科创50 330 n
科创50指数 330 n
上证综指 350 n
富时中国A50指数 340 n
恒生指数 340 n
恒生科技指数 340 n
恒生国企指数 330 n
H股指数 330 n
# 美洲市场
道琼斯工业平均指数 350 n
标普500指数 350 n
纳斯达克综合指数 340 n
纳斯达克100指数 340 n
罗素2000指数 320 n
标普400中型股指数 310 n
标普600小型股指数 310 n
纽约证交所综合指数 310 n
纳斯达克中国金龙指数 310 n
# 欧洲市场
德国DAX指数 330 n
法国CAC40指数 330 n
富时100指数 330 n
欧元斯托克50指数 320 n
英国富时250指数 310 n
意大利富时MIB指数 310 n
西班牙IBEX 35指数 310 n
# 亚太其他市场
日经225指数 330 n
日经500指数 310 n
韩国综合股价指数 320 n
韩国kospi指数 320 n
KOSPI 310 n
澳洲标普200指数 310 n
印度孟买敏感指数 310 n
Sensex 300 n
印度Nifty 50指数 310 n
# 全球综合指数
MSCI指数 320 n
MSCI全球指数 330 n
MSCI新兴市场指数 330 n
富时罗素全球指数 320 n
摩根大通全球债券指数 310 n
全球股指 300 n
发达市场指数 300 n
新兴市场指数 300 n
金砖国家指数 300 n
G20国家指数 300 n
# 股指衍生工具
指数期货 320 n
股指期货 320 n
富时中国A50指数期货 320 n
沪深300股指期货 320 n
标普500股指期货 320 n
纳斯达克100股指期货 310 n
指数成分股 320 n
指数权重股 320 n
指数涨幅 320 n
指数跌幅 320 n
指数反弹 310 n
指数回调 310 n
指数创新高 310 v
指数创新低 310 v
指数估值 310 n
指数市盈率 310 n
# 四、财务与估值核心指标
市盈率 350 n
PE 350 n
动态市盈率 340 n
静态市盈率 340 n
滚动市盈率 340 n
市净率 350 n
PB 350 n
市销率 330 n
PS 330 n
市现率 320 n
PCF 320 n
净资产收益率 350 n
ROE 350 n
总资产收益率 330 n
ROA 330 n
毛利率 340 n
净利率 340 n
销售净利率 330 n
资产负债率 340 n
营收 340 n
营业收入 340 n
净利润 350 n
归母净利润 340 n
扣非净利润 340 n
EPS 330 n
每股收益 330 n
现金流 340 n
经营活动现金流 330 n
自由现金流 330 n
营收增长率 330 n
净利润增长率 330 n
股息率 320 n
分红率 320 n
换手率 330 n
成交量 340 n
成交额 340 n
量比 320 n
振幅 320 n
# 五、政策与宏观经济
货币政策 330 n
财政政策 330 n
稳健货币政策 320 n
积极财政政策 320 n
宽松政策 320 n
紧缩政策 320 n
利率 330 n
基准利率 320 n
LPR 330 n
贷款市场报价利率 320 n
存款准备金率 320 n
MLF 320 n
中期借贷便利 310 n
逆回购 320 n
正回购 310 n
汇率 330 n
人民币汇率 330 n
美元汇率 320 n
通胀 320 n
CPI 330 n
PPI 330 n
GDP 330 n
国内生产总值 320 n
PMI 330 n
采购经理人指数 320 n
行业政策 320 n
产业政策 320 n
税收政策 310 n
补贴政策 310 n
关税 310 n
贸易政策 310 n
地缘政治 310 n
大宗商品 320 n
原油价格 310 n
黄金价格 310 n
有色金属价格 300 n
# 六、金融产品与机构
股票 320 n
基金 320 n
公募基金 310 n
私募基金 310 n
ETF 320 n
指数基金 310 n
混合型基金 300 n
股票型基金 310 n
债券型基金 300 n
货币基金 290 n
REITs 310 n
可转债 310 n
可交换债 300 n
期货 310 n
股指期货 310 n
国债期货 300 n
商品期货 300 n
期权 300 n
融资融券 310 n
两融余额 300 n
北向资金 320 n
南向资金 310 n
沪股通 310 n
深股通 310 n
陆股通 310 n
证券公司 310 n
券商 320 n
基金公司 300 n
保险公司 300 n
银行 310 n
监管机构 310 n
证监会 320 n
交易所 320 n
上交所 320 n
深交所 320 n
北交所 310 n
港交所 310 n
社保基金 310 n
养老金 300 n
QFII 300 n
RQFII 290 n
北向资金机构 300 n
# 七、热点概念与行业
AI 330 n
人工智能 350 n
算力 330 n
大数据 320 n
云计算 320 n
半导体 350 n
芯片 350 n
集成电路 340 n
新能源 350 n
光伏 340 n
锂电 320 n
储能 340 n
充电桩 310 n
新能源车 320 n
智能汽车 310 n
自动驾驶 330 n
军工 310 n
国防军工 300 n
医药 310 n
创新药 310 n
医疗器械 300 n
CXO 300 n
白酒 310 n
消费 320 n
可选消费 300 n
必选消费 300 n
食品饮料 310 n
家电 300 n
地产 300 n
房地产 300 n
基建 300 n
新基建 310 n
数字经济 350 n
数字货币 310 n
区块链 300 n
元宇宙 300 n
低空经济 340 n
人形机器人 330 n
工业互联网 330 n
物联网 300 n
5G 300 n
6G 340 n
# 八、交易操作与行情
上涨 310 v
下跌 310 v
涨停 310 v
跌停 310 v
反弹 300 v
反转 300 v
回调 300 v
横盘 290 v
震荡 290 v
跳水 300 v
拉升 300 v
砸盘 300 v
护盘 290 v
建仓 300 v
加仓 300 v
减仓 300 v
清仓 300 v
平仓 300 v
抄底 300 v
逃顶 300 v
追涨 290 v
杀跌 290 v
套牢 280 v
解套 280 v
净流入 300 n
净流出 300 n
主力资金 300 n
资金流入 290 v
资金流出 290 v
放量 290 v
缩量 290 v
高换手 290 n
低换手 280 n
高估值 290 n
低估值 290 n
超预期 300 v
不及预期 300 v
符合预期 290 v
利好 310 n
利空 310 n
政策利好 310 n
业绩利好 310 n
风险警示 300 n
涨停板 300 n
跌停板 300 n
一字涨停 290 n
一字跌停 290 n
打开涨停 320 v
打开跌停 320 v
集合竞价 290 n
连续竞价 280 n
开盘价 340 n
收盘价 340 n
最高价 330 n
最低价 330 n
均价 330 n
昨日收盘价 320 n
涨跌额 330 n
涨跌幅 340 n
涨幅 340 n
跌幅 340 n
涨停价 330 n
跌停价 330 n
熔断 330 n
临时停牌 320 n
复牌 320 v
停牌 320 n
量价齐升 320 n
量价背离 320 n
高开 320 n
低开 320 n
平开 320 n
高走 320 v
低走 320 v
震荡上行 320 v
震荡下行 320 v
# 九、委托交易与规则
限价委托 340 n
市价委托 340 n
止损委托 330 n

View File

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,185 @@
# 补充热点概念与板块Jieba/gse兼容格式
# 权重说明核心热点500-700分事件类400分负权重词汇按需求保留
# 一、负权重低优先级词汇(减少无差别匹配干扰)
公司 -0.1 n
国家 -0.1 n
国际 -0.1 n
会议 -0.1 n
市场 -0.1 n
经济 -0.1 n
技术 -0.1 n
记者 -0.1 n
时间 -0.1 n
项目 -0.1 n
问题 -0.1 n
企业 -0.1 n
财联社 -0.1 n
上涨 -0.1 v
下跌 -0.1 v
期货 -0.1 n
跌幅 -0.1 n
跌超 -0.1 adj
股票 -0.1 n
基金 -0.1 n
电讯 -0.1 n
建筑 -0.1 n
平开 -0.1 n
保险 -0.1 n
行业 -0.1 n
其他 -0.1 n
# 二、核心热点概念700分最高优先级
比特币 700 n
摩尔线程 700 n
摩尔线程概念 700 n
AI算力 700 n
生成式AI 700 n
量子计算 700 n
脑机接口 700 n
6G通信 700 n
人形机器人 700 n
固态电池 700 n
ChatGPT概念 700 n
Web3.0 700 n
元宇宙 700 n
数字孪生 700 n
量子通信 700 n
# 三、重点赛道板块500分高优先级
冰雪旅游 500 n
特高压 500 n
跨境电商 500 n
新能源汽车 500 n
机器人 500 n
具身智能 500 n
油气 500 n
商业航天 500 n
光伏储能 500 n
锂电材料 500 n
半导体设备 500 n
集成电路 500 n
创新药 500 n
CXO 500 n
医疗器械 500 n
数字经济 500 n
数字货币 500 n
区块链 500 n
低空经济 500 n
工业互联网 500 n
物联网 500 n
5G应用 500 n
充电桩 500 n
氢能源 500 n
核聚变 500 n
工业母机 500 n
新材料 500 n
生物制造 500 n
智能网联汽车 500 n
乡村振兴 500 n
国企改革 500 n
央企重组 500 n
跨境金融 500 n
自贸港 500 n
一带一路 500 n
绿色低碳 500 n
碳交易 500 n
数据要素 500 n
数字基建 500 n
东数西算 500 n
国产替代 500 n
信创 500 n
网络安全 500 n
算力网络 500 n
边缘计算 500 n
虚拟现实 500 n
增强现实 500 n
智能穿戴 500 n
智能家居 500 n
车联网 500 n
激光雷达 500 n
氮化镓 500 n
碳化硅 500 n
第三代半导体 500 n
EDA工具 500 n
光刻胶 500 n
芯片设计 500 n
封装测试 500 n
储能电池 500 n
钠离子电池 500 n
氢燃料电池 500 n
光伏组件 500 n
风电设备 500 n
特高压设备 500 n
电力物联网 500 n
智能电网 500 n
轨道交通 500 n
航空航天 500 n
海洋工程 500 n
高端装备 500 n
军工电子 500 n
卫星互联网 500 n
北斗导航 500 n
国产大飞机 500 n
生物医药 500 n
基因测序 500 n
疫苗 500 n
医疗美容 500 n
养老产业 500 n
教育信息化 500 n
体育产业 500 n
文化创意 500 n
旅游复苏 500 n
预制菜 500 n
白酒 500 n
食品饮料 500 n
家电下乡 500 n
房地产复苏 500 n
基建投资 500 n
新型城镇化 500 n
冷链物流 500 n
快递物流 500 n
跨境支付 500 n
金融科技 500 n
消费电子 500 n
元宇宙基建 500 n
数字藏品 500 n
NFT 500 n
绿色电力 500 n
节能降碳 500 n
抽水蓄能 500 n
生物质能 500 n
地热能 500 n
潮汐能 500 n
# 四、事件驱动型概念400分中优先级
俄乌冲突 400 n
中东局势 400 n
美联储加息 400 n
降息预期 400 n
贸易摩擦 400 n
供应链重构 400 n
能源危机 400 n
粮食安全 400 n
疫情复苏 400 n
政策利好 400 n
产业扶持 400 n
技术突破 400 n
并购重组 400 n
IPO提速 400 n
解禁潮 400 n
北向资金流入 400 n
南向资金流入 400 n
主力资金异动 400 n
行业景气度 400 n
业绩预增 400 n
商誉减值 400 n
退市风险 400 n
监管新规 400 n
税收优惠 400 n
补贴政策 400 n
基建刺激 400 n
消费刺激 400 n
新能源补贴 400 n
碳达峰政策 400 n
碳中和目标 400 n

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -4,6 +4,15 @@ import (
"bytes"
"encoding/json"
"fmt"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"go-stock/backend/util"
"net/url"
"strconv"
"strings"
"time"
"github.com/PuerkitoBio/goquery"
"github.com/coocood/freecache"
"github.com/duke-git/lancet/v2/convertor"
@@ -12,12 +21,6 @@ import (
"github.com/robertkrimen/otto"
"github.com/samber/lo"
"github.com/tidwall/gjson"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"strconv"
"strings"
"time"
)
// @Author spark
@@ -31,12 +34,80 @@ func NewMarketNewsApi() *MarketNewsApi {
return &MarketNewsApi{}
}
func (m MarketNewsApi) TelegraphList(crawlTimeOut int64) *[]models.Telegraph {
//https://www.cls.cn/nodeapi/telegraphList
url := "https://www.cls.cn/nodeapi/telegraphList"
res := map[string]any{}
_, _ = resty.New().SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
SetHeader("Referer", "https://www.cls.cn/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
SetResult(&res).
Get(url)
var telegraphs []models.Telegraph
if v, _ := convertor.ToInt(res["error"]); v == 0 {
if res["data"] == nil {
return m.GetNewTelegraph(30)
}
data := res["data"].(map[string]any)
rollData := data["roll_data"].([]any)
for _, v := range rollData {
news := v.(map[string]any)
ctime, _ := convertor.ToInt(news["ctime"])
dataTime := time.Unix(ctime, 0).Local()
telegraph := models.Telegraph{
Title: news["title"].(string),
Content: news["content"].(string),
Time: dataTime.Format("15:04:05"),
DataTime: &dataTime,
Url: news["shareurl"].(string),
Source: "财联社电报",
IsRed: (news["level"].(string)) != "C",
SentimentResult: AnalyzeSentiment(news["content"].(string)).Description,
}
cnt := int64(0)
if telegraph.Title == "" {
db.Dao.Model(telegraph).Where("content=?", telegraph.Content).Count(&cnt)
} else {
db.Dao.Model(telegraph).Where("title=?", telegraph.Title).Count(&cnt)
}
if cnt > 0 {
continue
}
telegraphs = append(telegraphs, telegraph)
db.Dao.Model(&models.Telegraph{}).Create(&telegraph)
logger.SugaredLogger.Debugf("telegraph: %+v", &telegraph)
if news["subjects"] == nil {
continue
}
subjects := news["subjects"].([]any)
for _, subject := range subjects {
name := subject.(map[string]any)["subject_name"].(string)
tag := &models.Tags{
Name: name,
Type: "subject",
}
db.Dao.Model(tag).Where("name=? and type=?", name, "subject").FirstOrCreate(&tag)
db.Dao.Model(models.TelegraphTags{}).Where("telegraph_id=? and tag_id=?", telegraph.ID, tag.ID).FirstOrCreate(&models.TelegraphTags{
TelegraphId: telegraph.ID,
TagId: tag.ID,
})
}
}
//db.Dao.Model(&models.Telegraph{}).Create(&telegraphs)
//logger.SugaredLogger.Debugf("telegraphs: %+v", &telegraphs)
}
return &telegraphs
}
func (m MarketNewsApi) GetNewTelegraph(crawlTimeOut int64) *[]models.Telegraph {
url := "https://www.cls.cn/telegraph"
response, _ := resty.New().SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
SetHeader("Referer", "https://www.cls.cn/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get(fmt.Sprintf(url))
Get(url)
var telegraphs []models.Telegraph
//logger.SugaredLogger.Info(string(response.Body()))
document, _ := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
@@ -75,7 +146,7 @@ func (m MarketNewsApi) GetNewTelegraph(crawlTimeOut int64) *[]models.Telegraph {
if telegraph.Content != "" {
telegraph.SentimentResult = AnalyzeSentiment(telegraph.Content).Description
cnt := int64(0)
db.Dao.Model(telegraph).Where("time=? and source=?", telegraph.Time, telegraph.Source).Count(&cnt)
db.Dao.Model(telegraph).Where("time=? and content=?", telegraph.Time, telegraph.Content).Count(&cnt)
if cnt == 0 {
db.Dao.Create(&telegraph)
telegraphs = append(telegraphs, telegraph)
@@ -98,9 +169,9 @@ func (m MarketNewsApi) GetNewTelegraph(crawlTimeOut int64) *[]models.Telegraph {
func (m MarketNewsApi) GetNewsList(source string, limit int) *[]*models.Telegraph {
news := &[]*models.Telegraph{}
if source != "" {
db.Dao.Model(news).Preload("TelegraphTags").Where("source=?", source).Order("id desc").Limit(limit).Find(news)
db.Dao.Model(news).Preload("TelegraphTags").Where("source=?", source).Order("data_time desc,time desc").Limit(limit).Find(news)
} else {
db.Dao.Model(news).Preload("TelegraphTags").Order("id desc").Limit(limit).Find(news)
db.Dao.Model(news).Preload("TelegraphTags").Order("data_time desc,time desc").Limit(limit).Find(news)
}
for _, item := range *news {
tags := &[]models.Tags{}
@@ -115,12 +186,57 @@ func (m MarketNewsApi) GetNewsList(source string, limit int) *[]*models.Telegrap
}
return news
}
func (m MarketNewsApi) GetNewsList2(source string, limit int) *[]*models.Telegraph {
NewMarketNewsApi().TelegraphList(30)
news := &[]*models.Telegraph{}
if source != "" {
db.Dao.Model(news).Preload("TelegraphTags").Where("source=?", source).Order("data_time desc,is_red desc").Limit(limit).Find(news)
} else {
db.Dao.Model(news).Preload("TelegraphTags").Order("data_time desc,is_red desc").Limit(limit).Find(news)
}
for _, item := range *news {
tags := &[]models.Tags{}
db.Dao.Model(&models.Tags{}).Where("id in ?", lo.Map(item.TelegraphTags, func(item models.TelegraphTags, index int) uint {
return item.TagId
})).Find(&tags)
tagNames := lo.Map(*tags, func(item models.Tags, index int) string {
return item.Name
})
item.SubjectTags = tagNames
logger.SugaredLogger.Infof("tagNames %v SubjectTags%s", tagNames, item.SubjectTags)
}
return news
}
func (m MarketNewsApi) GetTelegraphList(source string) *[]*models.Telegraph {
news := &[]*models.Telegraph{}
if source != "" {
db.Dao.Model(news).Preload("TelegraphTags").Where("source=?", source).Order("id desc").Limit(20).Find(news)
db.Dao.Model(news).Preload("TelegraphTags").Where("source=?", source).Order("data_time desc,time desc").Limit(50).Find(news)
} else {
db.Dao.Model(news).Preload("TelegraphTags").Order("id desc").Limit(20).Find(news)
db.Dao.Model(news).Preload("TelegraphTags").Order("data_time desc,time desc").Limit(50).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) GetTelegraphListWithPaging(source string, page, pageSize int) *[]*models.Telegraph {
// 计算偏移量
offset := (page - 1) * pageSize
news := &[]*models.Telegraph{}
if source != "" {
db.Dao.Model(news).Preload("TelegraphTags").Where("source=?", source).Order("data_time desc,time desc").Limit(pageSize).Offset(offset).Find(news)
} else {
db.Dao.Model(news).Preload("TelegraphTags").Order("data_time desc,time desc").Limit(pageSize).Offset(offset).Find(news)
}
for _, item := range *news {
tags := &[]models.Tags{}
@@ -176,7 +292,12 @@ func (m MarketNewsApi) GetSinaNews(crawlTimeOut uint) *[]models.Telegraph {
data := item.(map[string]any)
//logger.SugaredLogger.Infof("%s:%s", data["create_time"], data["rich_text"])
telegraph.Content = data["rich_text"].(string)
telegraph.Title = strutil.SubInBetween(data["rich_text"].(string), "【", "】")
telegraph.Time = strings.Split(data["create_time"].(string), " ")[1]
dataTime, _ := time.ParseInLocation("2006-01-02 15:04:05", data["create_time"].(string), time.Local)
if &dataTime != nil {
telegraph.DataTime = &dataTime
}
tags := data["tag"].([]any)
telegraph.SubjectTags = lo.Map(tags, func(tagItem any, index int) string {
name := tagItem.(map[string]any)["name"].(string)
@@ -195,7 +316,11 @@ func (m MarketNewsApi) GetSinaNews(crawlTimeOut uint) *[]models.Telegraph {
if telegraph.Content != "" {
telegraph.SentimentResult = AnalyzeSentiment(telegraph.Content).Description
cnt := int64(0)
db.Dao.Model(telegraph).Where("time=? and source=?", telegraph.Time, telegraph.Source).Count(&cnt)
if telegraph.Title == "" {
db.Dao.Model(telegraph).Where("content=?", telegraph.Content).Count(&cnt)
} else {
db.Dao.Model(telegraph).Where("title=?", telegraph.Title).Count(&cnt)
}
if cnt == 0 {
db.Dao.Create(&telegraph)
telegraphs = append(telegraphs, telegraph)
@@ -549,10 +674,19 @@ func (m MarketNewsApi) EMDictCode(code string, cache *freecache.Cache) []any {
return respMap["data"].([]any)
}
func (m MarketNewsApi) TradingViewNews() *[]models.TVNews {
func (m MarketNewsApi) TradingViewNews() *[]models.Telegraph {
client := resty.New()
config := GetSettingConfig()
if config.HttpProxyEnabled && config.HttpProxy != "" {
client.SetProxy(config.HttpProxy)
}
TVNews := &[]models.TVNews{}
url := "https://news-mediator.tradingview.com/news-flow/v2/news?filter=lang:zh-Hans&filter=provider:panews,reuters&client=screener&streaming=false"
resp, err := resty.New().SetProxy("http://127.0.0.1:10809").SetTimeout(time.Duration(30)*time.Second).R().
news := &[]models.Telegraph{}
// url := "https://news-mediator.tradingview.com/news-flow/v2/news?filter=lang:zh-Hans&filter=area:WLD&client=screener&streaming=false"
//url := "https://news-mediator.tradingview.com/news-flow/v2/news?filter=area%3AWLD&filter=lang%3Azh-Hans&client=screener&streaming=false"
url := "https://news-mediator.tradingview.com/news-flow/v2/news?filter=lang%3Azh-Hans&client=screener&streaming=false"
resp, err := client.SetTimeout(time.Duration(15)*time.Second).R().
SetHeader("Host", "news-mediator.tradingview.com").
SetHeader("Origin", "https://cn.tradingview.com").
SetHeader("Referer", "https://cn.tradingview.com/").
@@ -560,19 +694,85 @@ func (m MarketNewsApi) TradingViewNews() *[]models.TVNews {
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("TradingViewNews err:%s", err.Error())
return TVNews
return news
}
respMap := map[string]any{}
err = json.Unmarshal(resp.Body(), &respMap)
if err != nil {
return TVNews
return news
}
items, err := json.Marshal(respMap["items"])
if err != nil {
return TVNews
return news
}
json.Unmarshal(items, TVNews)
return TVNews
for i, a := range *TVNews {
if i > 10 {
break
}
detail := NewMarketNewsApi().TradingViewNewsDetail(a.Id)
dataTime := time.Unix(int64(a.Published), 0).Local()
description := ""
sentimentResult := ""
if detail != nil {
description = detail.ShortDescription
sentimentResult = AnalyzeSentiment(description).Description
}
if a.Title == "" {
continue
}
telegraph := &models.Telegraph{
Title: a.Title,
Content: description,
DataTime: &dataTime,
IsRed: false,
Time: dataTime.Format("15:04:05"),
Source: "外媒",
Url: fmt.Sprintf("https://cn.tradingview.com/news/%s", a.Id),
SentimentResult: sentimentResult,
}
cnt := int64(0)
if telegraph.Title == "" {
db.Dao.Model(telegraph).Where("content=?", telegraph.Content).Count(&cnt)
} else {
db.Dao.Model(telegraph).Where("title=?", telegraph.Title).Count(&cnt)
}
if cnt > 0 {
continue
}
db.Dao.Model(&models.Telegraph{}).Where("time=? and title=? and source=?", telegraph.Time, telegraph.Title, "外媒").FirstOrCreate(&telegraph)
*news = append(*news, *telegraph)
}
return news
}
func (m MarketNewsApi) TradingViewNewsDetail(id string) *models.TVNewsDetail {
//https://news-headlines.tradingview.com/v3/story?id=panews%3A9be7cf057e3f9%3A0&lang=zh-Hans
newsDetail := &models.TVNewsDetail{}
newsUrl := fmt.Sprintf("https://news-headlines.tradingview.com/v3/story?id=%s&lang=zh-Hans", url.QueryEscape(id))
client := resty.New()
config := GetSettingConfig()
if config.HttpProxyEnabled && config.HttpProxy != "" {
client.SetProxy(config.HttpProxy)
}
request := client.SetTimeout(time.Duration(3) * time.Second).R()
_, err := request.
SetHeader("Host", "news-headlines.tradingview.com").
SetHeader("Origin", "https://cn.tradingview.com").
SetHeader("Referer", "https://cn.tradingview.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0) Gecko/20100101 Firefox/146.0").
//SetHeader("TE", "trailers").
//SetHeader("Priority", "u=4").
//SetHeader("Connection", "keep-alive").
SetResult(newsDetail).
Get(newsUrl)
if err != nil {
logger.SugaredLogger.Errorf("TradingViewNewsDetail err:%s", err.Error())
return newsDetail
}
logger.SugaredLogger.Infof("resp:%+v", newsDetail)
return newsDetail
}
func (m MarketNewsApi) XUEQIUHotStock(size int, marketType string) *[]models.HotItem {
@@ -599,7 +799,7 @@ func (m MarketNewsApi) XUEQIUHotStock(size int, marketType string) *[]models.Hot
logger.SugaredLogger.Errorf("XUEQIUHotStock err:%s", err.Error())
return &[]models.HotItem{}
}
logger.SugaredLogger.Infof("XUEQIUHotStock:%+v", res)
//logger.SugaredLogger.Infof("XUEQIUHotStock:%+v", res)
return &res.Data.Items
}
@@ -701,3 +901,264 @@ func (m MarketNewsApi) ClsCalendar() []any {
err = json.Unmarshal(resp.Body(), &respMap)
return respMap["data"].([]any)
}
func (m MarketNewsApi) GetGDP() *models.GDPResp {
res := &models.GDPResp{}
url := "https://datacenter-web.eastmoney.com/api/data/v1/get?callback=data&columns=REPORT_DATE%2CTIME%2CDOMESTICL_PRODUCT_BASE%2CFIRST_PRODUCT_BASE%2CSECOND_PRODUCT_BASE%2CTHIRD_PRODUCT_BASE%2CSUM_SAME%2CFIRST_SAME%2CSECOND_SAME%2CTHIRD_SAME&pageNumber=1&pageSize=20&sortColumns=REPORT_DATE&sortTypes=-1&source=WEB&client=WEB&reportName=RPT_ECONOMY_GDP&p=1&pageNo=1&pageNum=1&_=" + strconv.FormatInt(time.Now().Unix(), 10)
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "datacenter-web.eastmoney.com").
SetHeader("Origin", "https://datacenter.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/cjsj/gdp.html").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("GDP err:%s", err.Error())
return res
}
body := resp.Body()
logger.SugaredLogger.Debugf("GDP:%s", body)
vm := otto.New()
vm.Run("function data(res){return res};")
val, err := vm.Run(body)
if err != nil {
logger.SugaredLogger.Errorf("GDP err:%s", err.Error())
return res
}
data, _ := val.Object().Value().Export()
logger.SugaredLogger.Infof("GDP:%v", data)
marshal, err := json.Marshal(data)
if err != nil {
return res
}
json.Unmarshal(marshal, &res)
logger.SugaredLogger.Infof("GDP:%+v", res)
return res
}
func (m MarketNewsApi) GetCPI() *models.CPIResp {
res := &models.CPIResp{}
url := "https://datacenter-web.eastmoney.com/api/data/v1/get?callback=data&columns=REPORT_DATE%2CTIME%2CNATIONAL_SAME%2CNATIONAL_BASE%2CNATIONAL_SEQUENTIAL%2CNATIONAL_ACCUMULATE%2CCITY_SAME%2CCITY_BASE%2CCITY_SEQUENTIAL%2CCITY_ACCUMULATE%2CRURAL_SAME%2CRURAL_BASE%2CRURAL_SEQUENTIAL%2CRURAL_ACCUMULATE&pageNumber=1&pageSize=20&sortColumns=REPORT_DATE&sortTypes=-1&source=WEB&client=WEB&reportName=RPT_ECONOMY_CPI&p=1&pageNo=1&pageNum=1&_=" + strconv.FormatInt(time.Now().Unix(), 10)
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "datacenter-web.eastmoney.com").
SetHeader("Origin", "https://datacenter.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/cjsj/gdp.html").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("GetCPI err:%s", err.Error())
return res
}
body := resp.Body()
logger.SugaredLogger.Debugf("GetCPI:%s", body)
vm := otto.New()
vm.Run("function data(res){return res};")
val, err := vm.Run(body)
if err != nil {
logger.SugaredLogger.Errorf("GetCPI err:%s", err.Error())
return res
}
data, _ := val.Object().Value().Export()
logger.SugaredLogger.Infof("GetCPI:%v", data)
marshal, err := json.Marshal(data)
if err != nil {
return res
}
json.Unmarshal(marshal, &res)
logger.SugaredLogger.Infof("GetCPI:%+v", res)
return res
}
// GetPPI PPI
func (m MarketNewsApi) GetPPI() *models.PPIResp {
res := &models.PPIResp{}
url := "https://datacenter-web.eastmoney.com/api/data/v1/get?callback=data&columns=REPORT_DATE,TIME,BASE,BASE_SAME,BASE_ACCUMULATE&pageNumber=1&pageSize=20&sortColumns=REPORT_DATE&sortTypes=-1&source=WEB&client=WEB&reportName=RPT_ECONOMY_PPI&p=1&pageNo=1&pageNum=1&_=" + strconv.FormatInt(time.Now().Unix(), 10)
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "datacenter-web.eastmoney.com").
SetHeader("Origin", "https://datacenter.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/cjsj/gdp.html").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("GetPPI err:%s", err.Error())
return res
}
body := resp.Body()
vm := otto.New()
vm.Run("function data(res){return res};")
val, err := vm.Run(body)
if err != nil {
return res
}
data, _ := val.Object().Value().Export()
marshal, err := json.Marshal(data)
if err != nil {
return res
}
json.Unmarshal(marshal, &res)
return res
}
func (m MarketNewsApi) GetPMI() *models.PMIResp {
res := &models.PMIResp{}
url := "https://datacenter-web.eastmoney.com/api/data/v1/get?callback=data&columns=REPORT_DATE%2CTIME%2CMAKE_INDEX%2CMAKE_SAME%2CNMAKE_INDEX%2CNMAKE_SAME&pageNumber=1&pageSize=20&sortColumns=REPORT_DATE&sortTypes=-1&source=WEB&client=WEB&reportName=RPT_ECONOMY_PMI&p=1&pageNo=1&pageNum=1&_=" + strconv.FormatInt(time.Now().Unix(), 10)
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "datacenter-web.eastmoney.com").
SetHeader("Origin", "https://datacenter.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/cjsj/gdp.html").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
Get(url)
if err != nil {
return res
}
body := resp.Body()
vm := otto.New()
vm.Run("function data(res){return res};")
val, err := vm.Run(body)
if err != nil {
return res
}
data, _ := val.Object().Value().Export()
marshal, err := json.Marshal(data)
if err != nil {
return res
}
json.Unmarshal(marshal, &res)
return res
}
func (m MarketNewsApi) GetIndustryReportInfo(infoCode string) string {
url := "https://data.eastmoney.com/report/zw_industry.jshtml?infocode=" + infoCode
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "data.eastmoney.com").
SetHeader("Origin", "https://data.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/report/industry.jshtml").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("GetIndustryReportInfo err:%s", err.Error())
return ""
}
body := resp.Body()
//logger.SugaredLogger.Debugf("GetIndustryReportInfo:%s", body)
doc, err := goquery.NewDocumentFromReader(strings.NewReader(string(body)))
title, _ := doc.Find("div.c-title").Html()
content, _ := doc.Find("div.ctx-content").Html()
//logger.SugaredLogger.Infof("GetIndustryReportInfo:\n%s\n%s", title, content)
markdown, err := util.HTMLToMarkdown(title + content)
if err != nil {
return ""
}
logger.SugaredLogger.Infof("GetIndustryReportInfo markdown:\n%s", markdown)
return markdown
}
func (m MarketNewsApi) ReutersNew() *models.ReutersNews {
client := resty.New()
config := GetSettingConfig()
if config.HttpProxyEnabled && config.HttpProxy != "" {
client.SetProxy(config.HttpProxy)
}
news := &models.ReutersNews{}
//url := "https://www.reuters.com/pf/api/v3/content/fetch/articles-by-section-alias-or-id-v1?query={\"arc-site\":\"reuters\",\"fetch_type\":\"collection\",\"offset\":0,\"section_id\":\"/world/\",\"size\":9,\"uri\":\"/world/\",\"website\":\"reuters\"}&d=300&mxId=00000000&_website=reuters"
url := "https://www.reuters.com/pf/api/v3/content/fetch/recent-stories-by-sections-v1?query=%7B%22section_ids%22%3A%22%2Fworld%2F%22%2C%22size%22%3A4%2C%22website%22%3A%22reuters%22%7D&d=334&mxId=00000000&_website=reuters"
_, err := client.SetTimeout(time.Duration(5)*time.Second).R().
SetHeader("Host", "www.reuters.com").
SetHeader("Origin", "https://www.reuters.com").
SetHeader("Referer", "https://www.reuters.com/world/china/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetResult(news).
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("ReutersNew err:%s", err.Error())
return news
}
logger.SugaredLogger.Infof("Articles:%+v", news.Result.Articles)
return news
}
func (m MarketNewsApi) InteractiveAnswer(page int, pageSize int, keyWord string) *models.InteractiveAnswer {
client := resty.New()
config := GetSettingConfig()
if config.HttpProxyEnabled && config.HttpProxy != "" {
client.SetProxy(config.HttpProxy)
}
url := fmt.Sprintf("https://irm.cninfo.com.cn/newircs/index/search?_t=%d", time.Now().Unix())
answers := &models.InteractiveAnswer{}
logger.SugaredLogger.Infof("请求url:%s", url)
resp, err := client.SetTimeout(time.Duration(5)*time.Second).R().
SetHeader("Host", "irm.cninfo.com.cn").
SetHeader("Origin", "https://irm.cninfo.com.cn").
SetHeader("Referer", "https://irm.cninfo.com.cn/views/interactiveAnswer").
SetHeader("handleError", "true").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0").
SetFormData(map[string]string{
"pageNo": convertor.ToString(page),
"pageSize": convertor.ToString(pageSize),
"searchTypes": "11",
"highLight": "true",
"keyWord": keyWord,
}).
SetResult(answers).
Post(url)
if err != nil {
logger.SugaredLogger.Errorf("InteractiveAnswer-err:%+v", err)
}
logger.SugaredLogger.Debugf("InteractiveAnswer-resp:%s", resp.Body())
return answers
}
func (m MarketNewsApi) CailianpressWeb(searchWords string) *models.CailianpressWeb {
res := &models.CailianpressWeb{}
_, err := resty.New().SetTimeout(time.Second*10).R().
SetHeader("Content-Type", "application/json").
SetHeader("Host", "www.cls.cn").
SetHeader("Origin", "https://www.cls.cn").
SetHeader("Referer", "https://www.cls.cn/telegraph").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
SetBody(fmt.Sprintf(`{"app":"CailianpressWeb","os":"web","sv":"8.4.6","category":"","keyword":"%s"}`, searchWords)).
SetResult(res).
Post("https://www.cls.cn/api/csw?app=CailianpressWeb&os=web&sv=8.4.6&sign=9f8797a1f4de66c2370f7a03990d2737")
if err != nil {
return nil
}
logger.SugaredLogger.Debug(res)
return res
}
func (m MarketNewsApi) GetNews24HoursList(source string, limit int) *[]*models.Telegraph {
news := &[]*models.Telegraph{}
if source != "" {
db.Dao.Model(news).Preload("TelegraphTags").Where("source=? and created_at>?", source, time.Now().Add(-24*time.Hour)).Order("data_time desc,is_red desc").Limit(limit).Find(news)
} else {
db.Dao.Model(news).Preload("TelegraphTags").Where("created_at>?", time.Now().Add(-24*time.Hour)).Order("data_time desc,is_red desc").Limit(limit).Find(news)
}
// 内容去重
uniqueNews := make([]*models.Telegraph, 0)
seenContent := make(map[string]bool)
for _, item := range *news {
tags := &[]models.Tags{}
db.Dao.Model(&models.Tags{}).Where("id in ?", lo.Map(item.TelegraphTags, func(item models.TelegraphTags, index int) uint {
return item.TagId
})).Find(&tags)
tagNames := lo.Map(*tags, func(item models.Tags, index int) string {
return item.Name
})
item.SubjectTags = tagNames
//logger.SugaredLogger.Infof("tagNames %v SubjectTags%s", tagNames, item.SubjectTags)
// 使用内容作为去重键值,可以考虑只使用内容的前几个字符或哈希值
contentKey := strings.TrimSpace(item.Content)
if contentKey != "" && !seenContent[contentKey] {
seenContent[contentKey] = true
uniqueNews = append(uniqueNews, item)
}
}
return &uniqueNews
}

View File

@@ -2,10 +2,19 @@ package data
import (
"encoding/json"
"github.com/coocood/freecache"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"go-stock/backend/util"
"path/filepath"
"strings"
"testing"
"github.com/coocood/freecache"
"github.com/duke-git/lancet/v2/random"
"github.com/duke-git/lancet/v2/strutil"
"github.com/go-resty/resty/v2"
"github.com/tidwall/gjson"
)
// @Author spark
@@ -15,7 +24,12 @@ import (
func TestGetSinaNews(t *testing.T) {
db.Init("../../data/stock.db")
NewMarketNewsApi().GetSinaNews(30)
InitAnalyzeSentiment()
news := NewMarketNewsApi().GetSinaNews(30)
for i, telegraph := range *news {
logger.SugaredLogger.Debugf("key: %+v, value: %+v", i, telegraph)
}
//NewMarketNewsApi().GetNewTelegraph(30)
}
@@ -33,7 +47,6 @@ 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) {
@@ -68,9 +81,12 @@ func TestLongTiger(t *testing.T) {
func TestStockResearchReport(t *testing.T) {
db.Init("../../data/stock.db")
resp := NewMarketNewsApi().StockResearchReport("600584.sh", 7)
resp := NewMarketNewsApi().StockResearchReport("688082", 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))
}
}
@@ -79,8 +95,11 @@ func TestIndustryResearchReport(t *testing.T) {
resp := NewMarketNewsApi().IndustryResearchReport("", 7)
for _, a := range resp {
logger.SugaredLogger.Debugf("value: %+v", a)
data := a.(map[string]any)
logger.SugaredLogger.Debugf("value: %s infoCode:%s", data["title"], data["infoCode"])
logger.SugaredLogger.Debugf("url: https://pdf.dfcfw.com/pdf/H3_%s_1.pdf", data["infoCode"])
//NewMarketNewsApi().GetIndustryReportInfo(data["infoCode"].(string))
}
}
func TestStockNotice(t *testing.T) {
@@ -98,15 +117,22 @@ func TestEMDictCode(t *testing.T) {
for _, a := range resp {
logger.SugaredLogger.Debugf("value: %+v", a)
}
bytes, err := json.Marshal(resp)
if err != nil {
return
}
dict := &[]models.BKDict{}
json.Unmarshal(bytes, dict)
logger.SugaredLogger.Debugf("value: %s", string(bytes))
md := util.MarkdownTableWithTitle("行业/板块代码", dict)
logger.SugaredLogger.Debugf(md)
}
func TestTradingViewNews(t *testing.T) {
db.Init("../../data/stock.db")
resp := NewMarketNewsApi().TradingViewNews()
for _, a := range *resp {
logger.SugaredLogger.Debugf("value: %+v", a)
}
InitAnalyzeSentiment()
NewMarketNewsApi().TradingViewNews()
}
func TestXUEQIUHotStock(t *testing.T) {
@@ -116,6 +142,8 @@ func TestXUEQIUHotStock(t *testing.T) {
logger.SugaredLogger.Debugf("value: %+v", a)
}
md := util.MarkdownTableWithTitle("当前热门股票排名", res)
logger.SugaredLogger.Debugf(md)
}
func TestHotEvent(t *testing.T) {
@@ -140,14 +168,129 @@ func TestInvestCalendar(t *testing.T) {
db.Init("../../data/stock.db")
res := NewMarketNewsApi().InvestCalendar("2025-06")
for _, a := range res {
logger.SugaredLogger.Debugf("value: %+v", a)
bytes, err := json.Marshal(a)
if err != nil {
continue
}
date := gjson.Get(string(bytes), "date")
list := gjson.Get(string(bytes), "list")
logger.SugaredLogger.Debugf("value: %+v,list: %+v", date.String(), list)
}
}
func TestClsCalendar(t *testing.T) {
db.Init("../../data/stock.db")
res := NewMarketNewsApi().ClsCalendar()
md := strings.Builder{}
for _, a := range res {
logger.SugaredLogger.Debugf("value: %+v", a)
bytes, err := json.Marshal(a)
if err != nil {
continue
}
//logger.SugaredLogger.Debugf("value: %+v", string(bytes))
date := gjson.Get(string(bytes), "calendar_day")
md.WriteString("\n### 事件/会议日期:" + date.String())
list := gjson.Get(string(bytes), "items")
//logger.SugaredLogger.Debugf("value: %+v,list: %+v", date.String(), list)
list.ForEach(func(key, value gjson.Result) bool {
logger.SugaredLogger.Debugf("key: %+v,value: %+v", key.String(), gjson.Get(value.String(), "title"))
md.WriteString("\n- " + gjson.Get(value.String(), "title").String())
return true
})
}
logger.SugaredLogger.Debugf("md:\n %s", md.String())
}
func TestGetGDP(t *testing.T) {
res := NewMarketNewsApi().GetGDP()
md := util.MarkdownTableWithTitle("国内生产总值(GDP)", res.GDPResult.Data)
logger.SugaredLogger.Debugf(md)
}
func TestGetCPI(t *testing.T) {
res := NewMarketNewsApi().GetCPI()
md := util.MarkdownTableWithTitle("居民消费价格指数(CPI)", res.CPIResult.Data)
logger.SugaredLogger.Debugf(md)
}
// PPI
func TestGetPPI(t *testing.T) {
res := NewMarketNewsApi().GetPPI()
md := util.MarkdownTableWithTitle("工业品出厂价格指数(PPI)", res.PPIResult.Data)
logger.SugaredLogger.Debugf(md)
}
// PMI
func TestGetPMI(t *testing.T) {
res := NewMarketNewsApi().GetPMI()
md := util.MarkdownTableWithTitle("采购经理人指数(PMI)", res.PMIResult.Data)
logger.SugaredLogger.Debugf(md)
}
func TestGetIndustryReportInfo(t *testing.T) {
NewMarketNewsApi().GetIndustryReportInfo("AP202507151709216483")
}
func TestReutersNew(t *testing.T) {
db.Init("../../data/stock.db")
NewMarketNewsApi().ReutersNew()
}
func TestInteractiveAnswer(t *testing.T) {
db.Init("../../data/stock.db")
datas := NewMarketNewsApi().InteractiveAnswer(1, 100, "立讯精密")
logger.SugaredLogger.Debugf("PageSize:%d", datas.PageSize)
md := util.MarkdownTableWithTitle("投资互动", datas.Results)
logger.SugaredLogger.Debugf(md)
}
func TestGetNewsList2(t *testing.T) {
db.Init("../../data/stock.db")
news := NewMarketNewsApi().GetNewsList2("财联社电报", random.RandInt(100, 500))
messageText := strings.Builder{}
for _, telegraph := range *news {
messageText.WriteString("## " + telegraph.Time + ":" + "\n")
messageText.WriteString("### " + telegraph.Content + "\n")
}
logger.SugaredLogger.Debugf("value: %s", messageText.String())
}
func TestTelegraphList(t *testing.T) {
db.Init("../../data/stock.db")
InitAnalyzeSentiment()
NewMarketNewsApi().TelegraphList(30)
}
func TestProxy(t *testing.T) {
response, err := resty.New().
SetProxy("http://go-stock:778d4ff2-73f3-4d56-b3c3-d9a730a06ae3@stock.sparkmemory.top:8888").
R().
SetHeader("Host", "news-mediator.tradingview.com").
SetHeader("Origin", "https://cn.tradingview.com").
SetHeader("Referer", "https://cn.tradingview.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
//Get("https://api.ipify.org")
Get("https://news-mediator.tradingview.com/news-flow/v2/news?filter=lang%3Azh-Hans&client=screener&streaming=false&user_prostatus=non_pro")
if err != nil {
logger.SugaredLogger.Error(err)
return
}
logger.SugaredLogger.Debugf("value: %s", response.String())
}
func TestNtfy(t *testing.T) {
//attach := "http://go-stock.sparkmemory.top/%E5%88%86%E6%9E%90%E6%8A%A5%E5%91%8A/%E8%B5%84%E9%87%91%E6%B5%81%E5%90%91/2025-12/AI%EF%BC%9A%E5%B8%82%E5%9C%BA%E5%88%86%E6%9E%90%E6%8A%A5%E5%91%8A-[2025.12.11_12.02.01].html"
//post, err := resty.New().SetBaseURL("https://go-stock.sparkmemory.top:16667").R().
// SetHeader("Filename", "AI市场分析报告-[2025.12.11_12.02.01].html").
// SetHeader("Icon", "https://go-stock.sparkmemory.top/appicon.png").
// SetHeader("Attach", attach).
// SetBody("AI市场分析报告-[2025.12.11_12.02.01]").Post("/go-stock")
//if err != nil {
// logger.SugaredLogger.Error(err)
// return
//}
//logger.SugaredLogger.Debugf("value: %s", post.String())
logger.SugaredLogger.Debugf("value: %s", filepath.Base("https://go-stock.sparkmemory.top/%E5%88%86%E6%9E%90%E6%8A%A5%E5%91%8A/2025/12/11/%E5%B8%82%E5%9C%BA%E8%B5%84%E8%AE%AF[%E5%B8%82%E5%9C%BA%E8%B5%84%E8%AE%AF]-(2025-12-11)AI%E5%88%86%E6%9E%90%E7%BB%93%E6%9E%9C_20251211131509.html"))
logger.SugaredLogger.Debugf("value: %s", strutil.After("/data/go-stock-site/docs/分析报告/2025/12/09/市场资讯[市场资讯]-(2025-12-09)AI分析结果.md", "/data/go-stock-site/docs/"))
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,24 +3,26 @@ package data
import (
"context"
"go-stock/backend/db"
log "go-stock/backend/logger"
"testing"
)
func TestNewDeepSeekOpenAiConfig(t *testing.T) {
db.Init("../../data/stock.db")
InitAnalyzeSentiment()
var tools []Tool
tools = append(tools, Tool{
Type: "function",
Function: ToolFunction{
Name: "SearchStockByIndicators",
Description: "通过解析自然语言,形成选股指标或策略,返回符合指标或策略的股票列表",
Parameters: FunctionParameters{
Description: "根据自然语言筛选股票,返回自然语言选股条件要求的股票所有相关数据",
Parameters: &FunctionParameters{
Type: "object",
Properties: map[string]any{
"words": map[string]any{
"type": "string",
"description": "选股指标或策略的自然语言",
"description": "选股自然语言,并且条件使用;分隔,或者条件使用,分隔。例如:创新药;PE<30;净利润增长率>50%;",
},
},
Required: []string{"words"},
@@ -28,14 +30,19 @@ func TestNewDeepSeekOpenAiConfig(t *testing.T) {
},
})
ai := NewDeepSeekOpenAi(context.TODO())
ai := NewDeepSeekOpenAi(context.TODO(), 11)
//res := ai.NewChatStream("长电科技", "sh600584", "长电科技分析和总结", nil)
res := ai.NewSummaryStockNewsStreamWithTools("总结市场资讯,发掘潜力标的/行业/板块/概念,控制风险,最后按风险登记生成指标选股策略汇总表,每个策略中的指标分号分隔,写成一行", nil, tools)
res := ai.NewSummaryStockNewsStreamWithTools("总结市场资讯,发掘潜力标的/行业/板块/概念,控制风险。调用工具函数验证", nil, tools, false)
for {
select {
case msg := <-res:
t.Log(msg)
if len(msg) > 0 {
t.Log(msg)
if msg["content"] == "DONE" {
return
}
}
}
}
}
@@ -47,8 +54,17 @@ func TestGetTopNewsList(t *testing.T) {
func TestSearchGuShiTongStockInfo(t *testing.T) {
db.Init("../../data/stock.db")
SearchGuShiTongStockInfo("hk01810", 60)
SearchGuShiTongStockInfo("sh600745", 60)
SearchGuShiTongStockInfo("gb_goog", 60)
//SearchGuShiTongStockInfo("hk01810", 60)
msgs := SearchGuShiTongStockInfo("sh600745", 60)
for _, msg := range *msgs {
log.SugaredLogger.Infof("%s", msg)
}
//SearchGuShiTongStockInfo("gb_goog", 60)
}
func TestGetZSInfo(t *testing.T) {
db.Init("../../data/stock.db")
GetZSInfo("中证银行", "sz399986", 5)
GetZSInfo("上海贝岭", "sh600171", 5)
}

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

@@ -3,9 +3,13 @@ package data
import (
"encoding/json"
"fmt"
"github.com/go-resty/resty/v2"
"go-stock/backend/logger"
"go-stock/backend/models"
"go-stock/backend/util"
"time"
"github.com/duke-git/lancet/v2/mathutil"
"github.com/go-resty/resty/v2"
)
// @Author spark
@@ -20,36 +24,140 @@ func NewSearchStockApi(words string) *SearchStockApi {
return &SearchStockApi{words: words}
}
func (s SearchStockApi) SearchStock(pageSize int) map[string]any {
qgqpBId := NewSettingsApi().Config.QgqpBId
if qgqpBId == "" {
return map[string]any{
"code": -1,
"message": "请先获取东财用户标识qgqp_b_id打开浏览器,访问东财网站按F12打开开发人员工具-》网络面板随便点开一个请求复制请求cookie中qgqp_b_id对应的值。保存到设置中的东财唯一标识输入框",
}
}
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("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:145.0) Gecko/20100101 Firefox/145.0").
SetHeader("Content-Type", "application/json").
SetBody(fmt.Sprintf(`{
"keyWord": "%s",
"pageSize": %d,
"pageNo": 1,
"fingerprint": "e38b5faabf9378c8238e57219f0ebc9b",
"fingerprint": "%s",
"gids": [],
"matchWord": "",
"timestamp": "1751113883290349",
"timestamp": "%d",
"shareToGuba": false,
"requestId": "8xTWgCDAjvQ5lmvz5mDA3Ydk2AE4yoiJ1751113883290",
"requestId": "",
"needCorrect": true,
"removedConditionIdList": [],
"xcId": "xc0af28549ab330013ed",
"xcId": "",
"ownSelectAll": false,
"dxInfo": [],
"extraCondition": ""
}`, s.words, pageSize)).Post(url)
}`, s.words, pageSize, qgqpBId, time.Now().Unix())).Post(url)
if err != nil {
logger.SugaredLogger.Errorf("SearchStock-err:%+v", err)
return map[string]any{}
return map[string]any{
"code": -1,
"message": err.Error(),
}
}
respMap := map[string]any{}
json.Unmarshal(resp.Body(), &respMap)
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
return respMap
}
func (s SearchStockApi) SearchBk(pageSize int) map[string]any {
url := "https://np-tjxg-b.eastmoney.com/api/smart-tag/bkc/v3/pw/search-code"
qgqpBId := NewSettingsApi().Config.QgqpBId
if qgqpBId == "" {
return map[string]any{
"code": -1,
"message": "请先获取东财用户标识qgqp_b_id打开浏览器,访问东财网站按F12打开开发人员工具-》网络面板随便点开一个请求复制请求cookie中qgqp_b_id对应的值。保存到设置中的东财唯一标识输入框",
}
}
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "np-tjxg-g.eastmoney.com").
SetHeader("Origin", "https://xuangu.eastmoney.com").
SetHeader("Referer", "https://xuangu.eastmoney.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:145.0) Gecko/20100101 Firefox/145.0").
SetHeader("Content-Type", "application/json").
SetBody(fmt.Sprintf(`{
"keyWord": "%s",
"pageSize": %d,
"pageNo": 1,
"fingerprint": "%s",
"gids": [],
"matchWord": "",
"timestamp": "%d",
"shareToGuba": false,
"requestId": "",
"needCorrect": true,
"removedConditionIdList": [],
"xcId": "",
"ownSelectAll": false,
"dxInfo": [],
"extraCondition": ""
}`, s.words, pageSize, qgqpBId, time.Now().Unix())).Post(url)
if err != nil {
logger.SugaredLogger.Errorf("SearchStock-err:%+v", err)
return map[string]any{
"code": -1,
"message": err.Error(),
}
}
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
}
func (s SearchStockApi) HotStrategyTable() string {
markdownTable := ""
res := s.HotStrategy()
bytes, _ := json.Marshal(res)
strategy := &models.HotStrategy{}
json.Unmarshal(bytes, strategy)
for _, data := range strategy.Data {
data.Chg = mathutil.RoundToFloat(100*data.Chg, 2)
}
markdownTable = util.MarkdownTableWithTitle("当前热门选股策略", strategy.Data)
return markdownTable
}
func (s SearchStockApi) StrategySquare() map[string]any {
//https://backtest.10jqka.com.cn/strategysquare/list?order=desc&page=1&pageNum=10&sortType=hot&keyword=
url := "https://backtest.10jqka.com.cn/strategysquare/list?order=desc&page=1&pageNum=10&sortType=hot&keyword="
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "backtest.10jqka.com.cn").
SetHeader("Origin", "https://backtest.10jqka.com.cn").
SetHeader("Referer", "https://backtest.10jqka.com.cn/strategysquare/list").
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("StrategySquare-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
}

View File

@@ -1,25 +1,89 @@
package data
import (
"encoding/json"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"go-stock/backend/util"
"math"
"testing"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/mathutil"
"github.com/duke-git/lancet/v2/random"
)
func TestSearchStock(t *testing.T) {
db.Init("../../data/stock.db")
res := NewSearchStockApi("算力股;净利润连续3年增长").SearchStock(10)
e := convertor.ToString(math.Floor(float64(9*random.RandFloat(0, 1, 12) + 1)))
for i := 0; i < 19; i++ {
e += convertor.ToString(math.Floor(float64(9 * random.RandFloat(0, 1, 12))))
}
logger.SugaredLogger.Infof("e:%s", e)
//res := NewSearchStockApi("量比大于2基本面优秀2025年三季报已披露主力连续3日净流入非创业板非科创板非ST").SearchStock(20)
res := NewSearchStockApi("今日涨幅前5的概念板块").SearchBk(50)
logger.SugaredLogger.Infof("res:%+v", res)
data := res["data"].(map[string]any)
result := data["result"].(map[string]any)
dataList := result["dataList"].([]any)
for _, v := range dataList {
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("%s:%s", d["INDUSTRY"], d["SECURITY_SHORT_NAME"])
//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
}
//columns := result["columns"].([]any)
//for _, v := range columns {
// logger.SugaredLogger.Infof("v:%+v", v)
//}
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()
bytes, err := json.Marshal(res)
if err != nil {
return
}
strategy := &models.HotStrategy{}
json.Unmarshal(bytes, strategy)
for _, data := range strategy.Data {
data.Chg = mathutil.RoundToFloat(100*data.Chg, 2)
}
markdownTable := util.MarkdownTable(strategy.Data)
logger.SugaredLogger.Infof("res:%s", markdownTable)
//dataList := res["data"].([]any)
//for _, v := range dataList {
// d := v.(map[string]any)
// logger.SugaredLogger.Infof("v:%+v", d)
//}
}
func TestSearchStockApi_HotStrategyTable(t *testing.T) {
db.Init("../../data/stock.db")
res := NewSearchStockApi("").StrategySquare()
logger.SugaredLogger.Infof("res:%+v", res)
}

View File

@@ -2,8 +2,12 @@ package data
import (
"encoding/json"
"errors"
"go-stock/backend/db"
"go-stock/backend/logger"
"time"
"github.com/samber/lo"
"gorm.io/gorm"
)
@@ -15,128 +19,216 @@ type Settings struct {
DingRobot string `json:"dingRobot"`
UpdateBasicInfoOnStart bool `json:"updateBasicInfoOnStart"`
RefreshInterval int64 `json:"refreshInterval"`
OpenAiEnable bool `json:"openAiEnable"`
OpenAiBaseUrl string `json:"openAiBaseUrl"`
OpenAiApiKey string `json:"openAiApiKey"`
OpenAiModelName string `json:"openAiModelName"`
OpenAiMaxTokens int `json:"openAiMaxTokens"`
OpenAiTemperature float64 `json:"openAiTemperature"`
OpenAiApiTimeOut int `json:"openAiApiTimeOut"`
Prompt string `json:"prompt"`
CheckUpdate bool `json:"checkUpdate"`
QuestionTemplate string `json:"questionTemplate"`
CrawlTimeOut int64 `json:"crawlTimeOut"`
KDays int64 `json:"kDays"`
EnableDanmu bool `json:"enableDanmu"`
BrowserPath string `json:"browserPath"`
EnableNews bool `json:"enableNews"`
DarkTheme bool `json:"darkTheme"`
BrowserPoolSize int `json:"browserPoolSize"`
EnableFund bool `json:"enableFund"`
EnablePushNews bool `json:"enablePushNews"`
OpenAiEnable bool `json:"openAiEnable"`
Prompt string `json:"prompt"`
CheckUpdate bool `json:"checkUpdate"`
QuestionTemplate string `json:"questionTemplate"`
CrawlTimeOut int64 `json:"crawlTimeOut"`
KDays int64 `json:"kDays"`
EnableDanmu bool `json:"enableDanmu"`
BrowserPath string `json:"browserPath"`
EnableNews bool `json:"enableNews"`
DarkTheme bool `json:"darkTheme"`
BrowserPoolSize int `json:"browserPoolSize"`
EnableFund bool `json:"enableFund"`
EnablePushNews bool `json:"enablePushNews"`
EnableOnlyPushRedNews bool `json:"enableOnlyPushRedNews"`
SponsorCode string `json:"sponsorCode"`
HttpProxy string `json:"httpProxy"`
HttpProxyEnabled bool `json:"httpProxyEnabled"`
EnableAgent bool `json:"enableAgent"`
QgqpBId string `json:"qgqpBId" gorm:"column:qgqp_b_id"`
}
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"`
HttpProxy string `json:"httpProxy"`
HttpProxyEnabled bool `json:"httpProxyEnabled"`
}
func NewSettingsApi(settings *Settings) *SettingsApi {
func (AIConfig) TableName() string {
return "ai_config"
}
type SettingConfig struct {
*Settings
AiConfigs []*AIConfig `json:"aiConfigs"`
}
type SettingsApi struct {
Config *SettingConfig
}
func NewSettingsApi() *SettingsApi {
return &SettingsApi{
Config: *settings,
Config: GetSettingConfig(),
}
}
func (s SettingsApi) UpdateConfig() string {
func (s *SettingsApi) Export() string {
d, _ := json.MarshalIndent(s.Config, "", " ")
return string(d)
}
func UpdateConfig(s *SettingConfig) string {
count := int64(0)
db.Dao.Model(s.Config).Count(&count)
db.Dao.Model(&Settings{}).Count(&count)
if count > 0 {
db.Dao.Model(s.Config).Where("id=?", s.Config.ID).Updates(map[string]any{
"local_push_enable": s.Config.LocalPushEnable,
"ding_push_enable": s.Config.DingPushEnable,
"ding_robot": s.Config.DingRobot,
"update_basic_info_on_start": s.Config.UpdateBasicInfoOnStart,
"refresh_interval": s.Config.RefreshInterval,
"open_ai_enable": s.Config.OpenAiEnable,
"open_ai_base_url": s.Config.OpenAiBaseUrl,
"open_ai_api_key": s.Config.OpenAiApiKey,
"open_ai_model_name": s.Config.OpenAiModelName,
"open_ai_max_tokens": s.Config.OpenAiMaxTokens,
"open_ai_temperature": s.Config.OpenAiTemperature,
"tushare_token": s.Config.TushareToken,
"prompt": s.Config.Prompt,
"check_update": s.Config.CheckUpdate,
"open_ai_api_time_out": s.Config.OpenAiApiTimeOut,
"question_template": s.Config.QuestionTemplate,
"crawl_time_out": s.Config.CrawlTimeOut,
"k_days": s.Config.KDays,
"enable_danmu": s.Config.EnableDanmu,
"browser_path": s.Config.BrowserPath,
"enable_news": s.Config.EnableNews,
"dark_theme": s.Config.DarkTheme,
"enable_fund": s.Config.EnableFund,
"enable_push_news": s.Config.EnablePushNews,
db.Dao.Model(&Settings{}).Where("id=?", s.ID).Updates(map[string]any{
"local_push_enable": s.LocalPushEnable,
"ding_push_enable": s.DingPushEnable,
"ding_robot": s.DingRobot,
"update_basic_info_on_start": s.UpdateBasicInfoOnStart,
"refresh_interval": s.RefreshInterval,
"open_ai_enable": s.OpenAiEnable,
"tushare_token": s.TushareToken,
"prompt": s.Prompt,
"check_update": s.CheckUpdate,
"question_template": s.QuestionTemplate,
"crawl_time_out": s.CrawlTimeOut,
"k_days": s.KDays,
"enable_danmu": s.EnableDanmu,
"browser_path": s.BrowserPath,
"enable_news": s.EnableNews,
"dark_theme": s.DarkTheme,
"enable_fund": s.EnableFund,
"enable_push_news": s.EnablePushNews,
"enable_only_push_red_news": s.EnableOnlyPushRedNews,
"sponsor_code": s.SponsorCode,
"http_proxy": s.HttpProxy,
"http_proxy_enabled": s.HttpProxyEnabled,
"enable_agent": s.EnableAgent,
"qgqp_b_id": s.QgqpBId,
})
//更新AiConfig
err := updateAiConfigs(s.AiConfigs)
if err != nil {
logger.SugaredLogger.Errorf("更新AI模型服务配置失败: %v", err)
return "更新AI模型服务配置失败: " + err.Error()
}
} else {
logger.SugaredLogger.Infof("未找到配置,创建默认配置:%+v", s.Config)
db.Dao.Model(s.Config).Create(&Settings{
LocalPushEnable: s.Config.LocalPushEnable,
DingPushEnable: s.Config.DingPushEnable,
DingRobot: s.Config.DingRobot,
UpdateBasicInfoOnStart: s.Config.UpdateBasicInfoOnStart,
RefreshInterval: s.Config.RefreshInterval,
OpenAiEnable: s.Config.OpenAiEnable,
OpenAiBaseUrl: s.Config.OpenAiBaseUrl,
OpenAiApiKey: s.Config.OpenAiApiKey,
OpenAiModelName: s.Config.OpenAiModelName,
OpenAiMaxTokens: s.Config.OpenAiMaxTokens,
OpenAiTemperature: s.Config.OpenAiTemperature,
TushareToken: s.Config.TushareToken,
Prompt: s.Config.Prompt,
CheckUpdate: s.Config.CheckUpdate,
OpenAiApiTimeOut: s.Config.OpenAiApiTimeOut,
QuestionTemplate: s.Config.QuestionTemplate,
CrawlTimeOut: s.Config.CrawlTimeOut,
KDays: s.Config.KDays,
EnableDanmu: s.Config.EnableDanmu,
BrowserPath: s.Config.BrowserPath,
EnableNews: s.Config.EnableNews,
DarkTheme: s.Config.DarkTheme,
EnableFund: s.Config.EnableFund,
EnablePushNews: s.Config.EnablePushNews,
})
logger.SugaredLogger.Infof("未找到配置,创建默认配置")
// 创建主配置
result := db.Dao.Model(&Settings{}).Create(&Settings{})
if result.Error != nil {
logger.SugaredLogger.Error("创建配置失败:", result.Error)
return "创建配置失败: " + result.Error.Error()
}
}
return "保存成功!"
}
func (s SettingsApi) GetConfig() *Settings {
var settings Settings
db.Dao.Model(&Settings{}).First(&settings)
func updateAiConfigs(aiConfigs []*AIConfig) error {
if len(aiConfigs) == 0 {
err := db.Dao.Exec("DELETE FROM ai_config").Error
if err != nil {
return err
}
return db.Dao.Exec("DELETE FROM sqlite_sequence WHERE name='ai_config'").Error
}
var ids []uint
lo.ForEach(aiConfigs, func(item *AIConfig, index int) {
ids = append(ids, item.ID)
})
var existAiConfigs []*AIConfig
err := db.Dao.Model(&AIConfig{}).Select("id").Where("id in (?) ", ids).Find(&existAiConfigs).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
idMap := make(map[uint]bool)
lo.ForEach(existAiConfigs, func(item *AIConfig, index int) {
idMap[item.ID] = true
})
var addAiConfigs []*AIConfig
var notDeleteIds []uint
var e error
lo.ForEach(aiConfigs, func(item *AIConfig, index int) {
if e != nil {
return
}
if !idMap[item.ID] {
addAiConfigs = append(addAiConfigs, item)
} else {
notDeleteIds = append(notDeleteIds, item.ID)
e = db.Dao.Model(&AIConfig{}).Where("id=?", item.ID).Updates(map[string]interface{}{
"name": item.Name,
"base_url": item.BaseUrl,
"api_key": item.ApiKey,
"model_name": item.ModelName,
"max_tokens": item.MaxTokens,
"temperature": item.Temperature,
"time_out": item.TimeOut,
"http_proxy": item.HttpProxy,
"http_proxy_enabled": item.HttpProxyEnabled,
}).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 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
}
if settings.KDays < 30 {
settings.KDays = 120
settings.KDays = 60
}
}
if settings.BrowserPath == "" {
settings.BrowserPath, _ = CheckBrowserOnWindows()
settings.BrowserPath, _ = CheckBrowser()
}
if settings.BrowserPoolSize <= 0 {
settings.BrowserPoolSize = 1
}
return &settings
}
settingConfig.Settings = settings
settingConfig.AiConfigs = aiConfigs
func (s SettingsApi) Export() string {
d, _ := json.MarshalIndent(s.GetConfig(), "", " ")
return string(d)
return settingConfig
}

View File

@@ -9,6 +9,15 @@ import (
"context"
"encoding/json"
"fmt"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"io"
"io/ioutil"
url2 "net/url"
"strings"
"time"
"github.com/PuerkitoBio/goquery"
"github.com/chromedp/chromedp"
"github.com/duke-git/lancet/v2/convertor"
@@ -17,18 +26,10 @@ import (
"github.com/go-resty/resty/v2"
"github.com/robertkrimen/otto"
"github.com/samber/lo"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"golang.org/x/sys/windows/registry"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
"gorm.io/gorm"
"gorm.io/plugin/soft_delete"
"io"
"io/ioutil"
"strings"
"time"
)
const sinaStockUrl = "http://hq.sinajs.cn/rn=%d&list=%s"
@@ -38,7 +39,7 @@ const tushareApiUrl = "http://api.tushare.pro"
type StockDataApi struct {
client *resty.Client
config *Settings
config *SettingConfig
}
type StockInfo struct {
gorm.Model
@@ -154,6 +155,8 @@ type StockBasic struct {
IsHs string `json:"is_hs"`
ActName string `json:"act_name"`
ActEntType string `json:"act_ent_type"`
BKName string `json:"bk_name"`
BKCode string `json:"bk_code"`
}
type FollowedStock struct {
@@ -171,6 +174,7 @@ type FollowedStock struct {
Cron *string
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
Groups []GroupStock `gorm:"foreignKey:StockCode;references:StockCode"`
AiConfigId int
}
func (receiver FollowedStock) TableName() string {
@@ -195,7 +199,7 @@ func (receiver StockBasic) TableName() string {
func NewStockDataApi() *StockDataApi {
return &StockDataApi{
client: resty.New(),
config: GetConfig(),
config: GetSettingConfig(),
}
}
@@ -376,6 +380,9 @@ func (receiver StockDataApi) GetStockCodeRealTimeData(StockCodes ...string) (*[]
logger.SugaredLogger.Error(err.Error())
continue
}
if stockData == nil {
continue
}
stockInfos = append(stockInfos, *stockData)
go func() {
@@ -414,6 +421,15 @@ func (receiver StockDataApi) Follow(stockCode string) string {
}
stockCode = strings.ToLower(stockCode)
// 检查是否已经关注过该股票
var existingStock FollowedStock
result := db.Dao.Model(&FollowedStock{}).Where("stock_code = ? AND is_del = ?", stockCode, 0).First(&existingStock)
if result.Error == nil {
// 股票已经关注过
return "已经关注了"
}
maxSort := int64(0)
db.Dao.Model(&FollowedStock{}).Raw("select max(sort) as sort from followed_stock").Scan(&maxSort)
@@ -1163,7 +1179,7 @@ func getHKStockPriceInfo(stockCode string, crawlTimeOut int64) *[]string {
return &messages
}
func getZSInfo(name, stockCode string, crawlTimeOut int64) string {
func GetZSInfo(name, stockCode string, crawlTimeOut int64) string {
url := "https://finance.sina.com.cn/realstock/company/" + stockCode + "/nc.shtml"
crawlerAPI := CrawlerApi{}
crawlerBaseInfo := CrawlerBaseInfo{
@@ -1188,6 +1204,10 @@ func getZSInfo(name, stockCode string, crawlTimeOut int64) string {
price := strutil.RemoveWhiteSpace(document.Find("div#price").First().Text(), false)
hqTime := strutil.RemoveWhiteSpace(document.Find("div#hqTime").First().Text(), false)
if strutil.ContainsAny(price, []string{"-", "--"}) {
return "暂无数据"
}
var markdown strings.Builder
markdown.WriteString(fmt.Sprintf("### 时间:%s %s%s \n", hqTime, name, price))
GetTableMarkdown(document, "div#hqDetails table", &markdown)
@@ -1298,50 +1318,6 @@ func SearchStockInfoByCode(stock string) *[]string {
return &messages
}
// checkChromeOnWindows 在 Windows 系统上检查谷歌浏览器是否安装
func checkChromeOnWindows() (string, bool) {
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe`, registry.QUERY_VALUE)
if err != nil {
// 尝试在 WOW6432Node 中查找(适用于 64 位系统上的 32 位程序)
key, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe`, registry.QUERY_VALUE)
if err != nil {
return "", false
}
defer key.Close()
}
defer key.Close()
path, _, err := key.GetStringValue("Path")
//logger.SugaredLogger.Infof("Chrome安装路径%s", path)
if err != nil {
return "", false
}
return path + "\\chrome.exe", true
}
// CheckBrowserOnWindows 在 Windows 系统上检查Edge浏览器是否安装并返回安装路径
func CheckBrowserOnWindows() (string, bool) {
if path, ok := checkChromeOnWindows(); ok {
return path, true
}
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe`, registry.QUERY_VALUE)
if err != nil {
// 尝试在 WOW6432Node 中查找(适用于 64 位系统上的 32 位程序)
key, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe`, registry.QUERY_VALUE)
if err != nil {
return "", false
}
defer key.Close()
}
defer key.Close()
path, _, err := key.GetStringValue("Path")
//logger.SugaredLogger.Infof("Edge安装路径%s", path)
if err != nil {
return "", false
}
return path + "\\msedge.exe", true
}
// 分时数据
func (receiver StockDataApi) GetStockMinutePriceData(stockCode string) (*[]MinuteData, string) {
url := fmt.Sprintf("https://web.ifzq.gtimg.cn/appstock/app/minute/query?code=%s", stockCode)
@@ -1499,7 +1475,7 @@ func getSinaStockInfo(receiver StockDataApi, page, pageSize int) *[]models.SinaS
func (receiver StockDataApi) getDCStockInfo(market string, page, pageSize int) {
//m:105,m:106,m:107 //美股
//m:128+t:3,m:128+t:4,m:128+t:1,m:128+t:2 //港股
fs := ""
fs := "m:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23,m:0+t:81+s:2048"
switch market {
case "hk":
fs = "m:128+t:3,m:128+t:4,m:128+t:1,m:128+t:2"
@@ -1507,62 +1483,108 @@ func (receiver StockDataApi) getDCStockInfo(market string, page, pageSize int) {
fs = "m:105,m:106,m:107"
}
url := "https://push2.eastmoney.com/api/qt/clist/get?cb=jQuery371047843066631541353_1745889398012&fs=%s&fields=f12,f13,f14,f19,f1,f2,f4,f3,f152,f17,f18,f15,f16,f5,f6&fid=f3&pn=%d&pz=%d&po=1&dect=1"
url := "https://push2.eastmoney.com/api/qt/clist/get?np=1&fltt=1&invt=2&cb=data&fs=%s&fields=f12,f13,f14,f1,f2,f4,f3,f152,f5,f6,f7,f15,f18,f16,f17,f10,f8,f9,f23,f100,f265&fid=f3&pn=%d&pz=%d&po=1&dect=1&wbp2u=|0|0|0|web&_=%d"
sprintfUrl := fmt.Sprintf(url, fs, page, pageSize, time.Now().UnixMilli())
logger.SugaredLogger.Infof("page:%d url:%s", page, sprintfUrl)
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
SetHeader("Host", "push2.eastmoney.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(fmt.Sprintf(url, fs, page, pageSize))
SetHeader("Referer", "https://quote.eastmoney.com/center/gridlist.html").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0) Gecko/20100101 Firefox/146.0").
Get(sprintfUrl)
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
return
}
body := string(resp.Body())
body = strutil.ReplaceWithMap(body, map[string]string{
"jQuery371047843066631541353_1745889398012(": "",
");": "",
})
js := "var d=" + body
logger.SugaredLogger.Infof("resp:%s", body)
vm := otto.New()
_, err = vm.Run(js)
_, err = vm.Run("var data = JSON.stringify(d);")
value, err := vm.Get("data")
data := make(map[string]any)
err = json.Unmarshal([]byte(value.String()), &data)
vm.Run("function data(res){return res};")
val, err := vm.Run(body)
if err != nil {
logger.SugaredLogger.Errorf("vm.Run error:%v", err.Error())
}
value, _ := val.Object().Value().Export()
marshal, err := json.Marshal(value)
data := make(map[string]any)
err = json.Unmarshal(marshal, &data)
if err != nil {
logger.SugaredLogger.Errorf("json:%s", value.String())
logger.SugaredLogger.Errorf("json.Unmarshal error:%v", err.Error())
}
logger.SugaredLogger.Infof("resp:%s", data)
if data["data"] != nil {
datas := data["data"].(map[string]any)
total := datas["total"].(float64)
diff := datas["diff"].(map[string]any)
diff := datas["diff"].([]any)
logger.SugaredLogger.Infof("total:%d", int(total))
for k, item := range diff {
stock := item.(map[string]any)
logger.SugaredLogger.Infof("k:%s,%s:%s", k, stock["f14"], stock["f12"])
logger.SugaredLogger.Infof("k:%d,%s:%s:%s %s:%s", k, stock["f14"], stock["f12"], DCToTsCode(stock["f12"].(string)), stock["f100"], stock["f265"])
if market == "" {
stockInfo := &StockBasic{
Symbol: stock["f12"].(string),
TsCode: DCToTsCode(stock["f12"].(string)),
Name: stock["f14"].(string),
BKName: stock["f100"].(string),
BKCode: stock["f265"].(string),
}
db.Dao.Model(&StockBasic{}).Where("symbol = ?", stockInfo.Symbol).First(stockInfo)
logger.SugaredLogger.Infof("stockInfo:%+v", stockInfo)
if stockInfo.ID == 0 {
db.Dao.Model(&StockBasic{}).Create(stockInfo)
} else {
stockInfo = &StockBasic{
Symbol: stock["f12"].(string),
TsCode: DCToTsCode(stock["f12"].(string)),
Name: stock["f14"].(string),
BKName: stock["f100"].(string),
BKCode: stock["f265"].(string),
}
db.Dao.Model(&StockBasic{}).Where("symbol = ?", stockInfo.Symbol).Updates(stockInfo)
}
}
if market == "hk" {
stockInfo := &models.StockInfoHK{
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".HK",
Name: stock["f14"].(string),
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".HK",
Name: stock["f14"].(string),
BKName: stock["f100"].(string),
BKCode: stock["f265"].(string),
}
db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", stockInfo.Code).First(stockInfo)
logger.SugaredLogger.Infof("stockInfo:%+v", stockInfo)
if stockInfo.ID == 0 {
db.Dao.Model(&models.StockInfoHK{}).Create(stockInfo)
} else {
stockInfo = &models.StockInfoHK{
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".HK",
Name: stock["f14"].(string),
BKName: stock["f100"].(string),
BKCode: stock["f265"].(string),
}
db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", stockInfo.Code).Updates(stockInfo)
}
}
if market == "us" {
stockInfo := &models.StockInfoUS{
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".US",
Name: stock["f14"].(string),
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".US",
Name: stock["f14"].(string),
BKName: stock["f100"].(string),
BKCode: stock["f265"].(string),
}
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", stockInfo.Code).First(stockInfo)
logger.SugaredLogger.Infof("stockInfo:%+v", stockInfo)
if stockInfo.ID == 0 {
db.Dao.Model(&models.StockInfoUS{}).Create(stockInfo)
} else {
stockInfo = &models.StockInfoUS{
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".US",
Name: stock["f14"].(string),
BKName: stock["f100"].(string),
BKCode: stock["f265"].(string),
}
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", stockInfo.Code).Updates(stockInfo)
}
}
@@ -1571,6 +1593,25 @@ func (receiver StockDataApi) getDCStockInfo(market string, page, pageSize int) {
}
}
func DCToTsCode(dcCode string) string {
//北京证券交易所 883、87、88 等) 创新型中小企业(专精特新为主)
//上海证券交易所 660、688 等) 大盘蓝筹、科创板(高新技术)
//深圳证券交易所 0、3000、002、30 等) 中小盘、创业板(成长型创新企业)
switch dcCode[0:1] {
case "8":
return dcCode + ".BJ"
case "9":
return dcCode + ".BJ"
case "6":
return dcCode + ".SH"
case "0":
return dcCode + ".SZ"
case "3":
return dcCode + ".SZ"
}
return ""
}
func (receiver StockDataApi) GetHKStockInfo(pageSize int) {
url := "https://stock.gtimg.cn/data/hk_rank.php?board=main_all&metric=price&pageSize=%d&reqPage=1&order=desc&var_name=list_data"
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
@@ -1705,6 +1746,70 @@ func (receiver StockDataApi) GetCommonKLineData(stockCode string, kLineType stri
return K
}
// GetStockHistoryMoneyData 获取股票历史资金流向数据
func (receiver StockDataApi) GetStockHistoryMoneyData() {
}
// GetStockMoneyData 获取个股资金流数据
func (receiver StockDataApi) GetStockMoneyData() models.StockMoneyDataResp {
var resData models.StockMoneyDataResp
url := "https://push2.eastmoney.com/api/qt/clist/get?cb=data&fid=f62&po=1&pz=50&pn=1&np=1&fltt=2&invt=2&ut=8dec03ba335b81bf4ebdf7b29ec27d15&fs=m:0+t:6+f:!2,m:0+t:13+f:!2,m:0+t:80+f:!2,m:1+t:2+f:!2,m:1+t:23+f:!2,m:0+t:7+f:!2,m:1+t:3+f:!2&fields=f12,f14,f2,f3,f62,f184,f66,f69,f72,f75,f78,f81,f84,f87,f204,f205,f124,f1,f13,f100,f265"
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
SetHeader("Host", "push2.eastmoney.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)
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
}
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("err:%s", err.Error())
}
value, err := val.Export()
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
}
marshal, err := json.Marshal(value)
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
return models.StockMoneyDataResp{}
}
err = json.Unmarshal(marshal, &resData)
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
return models.StockMoneyDataResp{}
}
return resData
}
// 获取股票概念题材信息
func (receiver StockDataApi) GetStockConceptInfo(stockCode string) models.StockConceptInfoResp {
//601138.SH
url := "https://datacenter.eastmoney.com/securities/api/data/v1/get?reportName=RPT_F10_CORETHEME_BOARDTYPE&columns=SECUCODE%2CSECURITY_CODE%2CSECURITY_NAME_ABBR%2CNEW_BOARD_CODE%2CBOARD_NAME%2CSELECTED_BOARD_REASON%2CIS_PRECISE%2CBOARD_RANK%2CBOARD_YIELD%2CDERIVE_BOARD_CODE&quoteColumns=f3~05~NEW_BOARD_CODE~BOARD_YIELD&filter=(SECUCODE%3D%22" + stockCode + "%22)(IS_PRECISE%3D%221%22)&pageNumber=1&pageSize=&sortTypes=1&sortColumns=BOARD_RANK&source=HSF10&client=PC&v=005634233622011753"
logger.SugaredLogger.Infof("url:%s", url2.QueryEscape(url))
var data models.StockConceptInfoResp
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
SetHeader("Host", "datacenter.eastmoney.com").
SetHeader("Referer", "https://emweb.securities.eastmoney.com/").
SetHeader("Origin", "https://emweb.securities.eastmoney.com").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0").
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
}
err = json.Unmarshal(resp.Body(), &data)
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
return models.StockConceptInfoResp{}
}
return data
}
// JSONToMarkdownTable 将JSON数据转换为Markdown表格
func JSONToMarkdownTable(jsonData []byte) (string, error) {
var data []map[string]interface{}

View File

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

View File

@@ -4,17 +4,19 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/random"
"github.com/duke-git/lancet/v2/strutil"
"github.com/go-resty/resty/v2"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/util"
"io/ioutil"
"regexp"
"strings"
"testing"
"time"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/random"
"github.com/duke-git/lancet/v2/strutil"
"github.com/go-resty/resty/v2"
)
// @Author spark
@@ -48,14 +50,23 @@ func TestGetFinancialReports(t *testing.T) {
func TestGetTelegraphSearch(t *testing.T) {
db.Init("../../data/stock.db")
searchWords := "半导体 新能源汽车 机器人"
//url := "https://www.cls.cn/searchPage?keyword=%E9%97%BB%E6%B3%B0%E7%A7%91%E6%8A%80&type=telegram"
messages := SearchStockInfo("谷歌", "telegram", 30)
messages := SearchStockInfo(searchWords, "telegram", 30)
for _, message := range *messages {
logger.SugaredLogger.Info(message)
}
//https://www.cls.cn/stock?code=sh600745
}
func TestCailianpressWeb(t *testing.T) {
db.Init("../../data/stock.db")
searchWords := "半导体 新能源汽车 机器人"
res := NewMarketNewsApi().CailianpressWeb(searchWords)
md := util.MarkdownTableWithTitle(searchWords+"财联社新闻", res.List)
logger.SugaredLogger.Info(md)
}
func TestSearchStockInfoByCode(t *testing.T) {
db.Init("../../data/stock.db")
SearchStockInfoByCode("sh600745")
@@ -63,7 +74,7 @@ func TestSearchStockInfoByCode(t *testing.T) {
func TestSearchStockPriceInfo(t *testing.T) {
db.Init("../../data/stock.db")
//SearchStockPriceInfo("中信证券", "hk06030", 30)
SearchStockPriceInfo("博安生物", "hk06955", 30)
SearchStockPriceInfo("上海贝岭", "sh600171", 30)
//SearchStockPriceInfo("苹果公司", "gb_aapl", 30)
//SearchStockPriceInfo("微创光电", "bj430198", 30)
@@ -110,9 +121,10 @@ func TestGetHKStockInfo(t *testing.T) {
//NewStockDataApi().GetSinaHKStockInfo()
//m:105,m:106,m:107 //美股
//m:128+t:3,m:128+t:4,m:128+t:1,m:128+t:2 //港股
for i := 1; i <= 592; i++ {
NewStockDataApi().getDCStockInfo("us", i, 20)
time.Sleep(time.Duration(random.RandInt(1, 3)) * time.Second)
//274 224 605
for i := 197; i <= 274; i++ {
NewStockDataApi().getDCStockInfo("", i, 20)
time.Sleep(time.Duration(random.RandInt(2, 5)) * time.Second)
}
}
@@ -183,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", "hk09660", "hk00700")
datas, _ := stockDataApi.GetStockCodeRealTimeData("sz002352", "sh600859", "sh600745", "gb_tsla", "hk09660", "hk00700")
for _, data := range *datas {
t.Log(data)
}
@@ -253,3 +265,35 @@ func TestStockDataApi_GetIndexBasic(t *testing.T) {
stockDataApi := NewStockDataApi()
stockDataApi.GetIndexBasic()
}
func TestName(t *testing.T) {
db.Init("../../data/stock.db")
stockBasics := &[]StockBasic{}
resty.New().SetProxy("").R().
SetHeader("user", "go-stock").
SetResult(stockBasics).
Get("http://8.134.249.145:18080/go-stock/stock_basic.json")
logger.SugaredLogger.Infof("%+v", stockBasics)
//db.Dao.Unscoped().Model(&StockBasic{}).Where("1=1").Delete(&StockBasic{})
//err := db.Dao.CreateInBatches(stockBasics, 400).Error
//if err != nil {
// t.Log(err.Error())
//}
}
func TestGetStockMoneyData(t *testing.T) {
db.Init("../../data/stock.db")
stockDataApi := NewStockDataApi()
res := stockDataApi.GetStockMoneyData()
logger.SugaredLogger.Infof("%s", util.MarkdownTableWithTitle("今日个股资金流向Top50", res.Data.Diff))
}
func TestGetStockConceptInfo(t *testing.T) {
db.Init("../../data/stock.db")
stockDataApi := NewStockDataApi()
res := stockDataApi.GetStockConceptInfo("601138.SH")
logger.SugaredLogger.Infof("%s", util.MarkdownTableWithTitle("601138.SH所属概念/板块信息", res.Result.Data))
}

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

@@ -2,37 +2,50 @@ package data
import (
"bufio"
_ "embed"
"fmt"
"github.com/go-ego/gse"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"os"
"regexp"
"sort"
"strings"
"unicode"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/fileutil"
"github.com/duke-git/lancet/v2/strutil"
"github.com/go-ego/gse"
)
const basefreq float64 = 100
// 金融情感词典,包含股票市场相关的专业词汇
var (
seg gse.Segmenter
// 正面金融词汇及其权重
positiveFinanceWords = map[string]float64{
"上涨": 2.0, "涨停": 3.0, "牛市": 3.0, "反弹": 2.0, "新高": 2.5,
"涨": 1.0, "上涨": 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,
"大涨": 2.5, "飙升": 3.0, "井喷": 3.0, "暴涨": 3.0,
}
// 负面金融词汇及其权重
negativeFinanceWords = map[string]float64{
"下跌": 2.0, "跌停": 3.0, "熊市": 3.0, "回调": 1.5, "新低": 2.5,
"跌": 2.0, "下跌": 2.0, "跌停": 3.0, "熊市": 3.0, "回调": 2.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,
"衰退": 2.0, "跌破": 2.0, "创新低": 3.0, "走弱": 2.5, "下挫": 2.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,
"垃圾股": 2.0, "风险股": 2.0, "弱势": 2.5, "走低": 2.5, "缩量": 2.5,
"大跌": 2.5, "暴跌": 3.0, "崩盘": 3.0, "跳水": 3.0, "重挫": 3.0, "跌超": 2.5, "跌逾": 2.5, "跌近": 3.0,
"被抓": 3.0, "被抓捕": 3.0, "回吐": 3.0, "转跌": 3.0,
}
// 否定词,用于反转情感极性
@@ -44,7 +57,7 @@ var (
degreeWords = map[string]float64{
"非常": 1.8, "极其": 2.2, "太": 1.8, "很": 1.5,
"比较": 0.8, "稍微": 0.6, "有点": 0.7, "显著": 1.5,
"大幅": 1.8, "急剧": 2.0, "轻微": 0.6, "小幅": 0.7,
"大幅": 1.8, "急剧": 2.0, "轻微": 0.6, "小幅": 0.7, "逾": 1.8, "超": 1.8,
}
// 转折词,用于识别情感转折
@@ -53,34 +66,269 @@ var (
}
)
func init() {
// 加载默认词典
err := seg.LoadDict()
//go:embed data/dict/base.txt
var baseDict string
//go:embed data/dict/zh/s_1.txt
var zhDict string
func InitAnalyzeSentiment() {
defer func() {
if r := recover(); r != nil {
logger.SugaredLogger.Error(fmt.Sprintf("panic: %v", r))
}
}()
// 加载简体中文词典
//err := seg.LoadDict("zh_s")
//if err != nil {
// logger.SugaredLogger.Error(err.Error())
//}
err := seg.LoadDictEmbed(baseDict)
if err != nil {
logger.SugaredLogger.Error(err.Error())
} else {
logger.SugaredLogger.Info("加载默认词典成功")
}
seg.CalcToken()
stocks := &[]StockBasic{}
db.Dao.Model(&StockBasic{}).Find(stocks)
for _, stock := range *stocks {
if strutil.Trim(stock.Name) == "" {
continue
}
err := seg.AddToken(stock.Name, basefreq+100, "n")
if strutil.Trim(stock.BKName) != "" {
err = seg.AddToken(stock.BKName, basefreq+100, "n")
}
if err != nil {
logger.SugaredLogger.Errorf("添加%s失败:%s", stock.Name, err.Error())
}
}
logger.SugaredLogger.Info("加载股票名称词典成功")
stockhks := &[]models.StockInfoHK{}
db.Dao.Model(&models.StockInfoHK{}).Find(stockhks)
for _, stock := range *stockhks {
if strutil.Trim(stock.Name) == "" {
continue
}
err := seg.AddToken(stock.Name, basefreq+100, "n")
if strutil.Trim(stock.BKName) != "" {
err = seg.AddToken(stock.BKName, basefreq+100, "n")
}
if err != nil {
logger.SugaredLogger.Errorf("添加%s失败:%s", stock.Name, err.Error())
}
}
logger.SugaredLogger.Info("加载港股名称词典成功")
//stockus := &[]models.StockInfoUS{}
//db.Dao.Model(&models.StockInfoUS{}).Where("trim(name) != ?", "").Find(stockus)
//for _, stock := range *stockus {
// err := seg.AddToken(stock.Name, 500)
// if err != nil {
// logger.SugaredLogger.Errorf("添加%s失败:%s", stock.Name, err.Error())
// }
//}
tags := &[]models.Tags{}
db.Dao.Model(&models.Tags{}).Where("type = ?", "subject").Find(tags)
for _, tag := range *tags {
if tag.Name == "" {
continue
}
err := seg.AddToken(tag.Name, basefreq+100, "n")
if err != nil {
logger.SugaredLogger.Errorf("添加%s失败:%s", tag.Name, err.Error())
} else {
logger.SugaredLogger.Infof("添加tags词典[%s]成功", tag.Name)
}
}
logger.SugaredLogger.Info("加载tags词典成功")
seg.CalcToken()
//加载用户自定义词典 先判断用户词典是否存在
if fileutil.IsExist("data/dict/user.txt") {
lines, err := fileutil.ReadFileByLine("data/dict/user.txt")
if err != nil {
logger.SugaredLogger.Error(err.Error())
return
}
for _, line := range lines {
if len(line) == 0 || line[0] == '#' {
continue
}
k := strutil.SplitAndTrim(line, " ")
if len(k) == 0 {
continue
}
_, _, ok := seg.Find(k[0])
switch len(k) {
case 1:
if ok {
err = seg.ReAddToken(k[0], basefreq)
} else {
err = seg.AddToken(k[0], basefreq)
}
case 2:
freq, _ := convertor.ToFloat(k[1])
if ok {
err = seg.ReAddToken(k[0], freq)
} else {
err = seg.AddToken(k[0], freq)
}
case 3:
freq, _ := convertor.ToFloat(k[1])
if ok {
err = seg.ReAddToken(k[0], freq, k[2])
} else {
err = seg.AddToken(k[0], freq, k[2])
}
default:
logger.SugaredLogger.Errorf("用户词典格式错误:%s", line)
}
logger.SugaredLogger.Infof("添加用户词典[%s]成功", line)
}
if err != nil {
logger.SugaredLogger.Error(err.Error())
} else {
logger.SugaredLogger.Infof("加载用户词典成功")
}
} else {
logger.SugaredLogger.Info("用户词典不存在")
}
seg.CalcToken()
}
// SentimentResult 情感分析结果类型
type SentimentResult struct {
Score float64 // 情感得分
Category SentimentType // 情感类别
PositiveCount int // 正面词数量
NegativeCount int // 负面词数量
Description string // 情感描述
// getWordWeight 获取词汇权重
func getWordWeight(word string) float64 {
// 从分词器获取词汇权重
freq, pos, ok := seg.Dictionary().Find([]byte(word))
if ok {
logger.SugaredLogger.Infof("获取%s的权重:%f,pos:%s,ok:%v", word, freq, pos, ok)
return freq
}
return 0
}
// SentimentType 情感类型枚举
type SentimentType int
// SortByWeightAndFrequency 按权重和频次排序词频结果
func SortByWeightAndFrequency(frequencies map[string]models.WordFreqWithWeight) []models.WordFreqWithWeight {
// 将map转换为slice以便排序
freqSlice := make([]models.WordFreqWithWeight, 0, len(frequencies))
for _, freq := range frequencies {
freqSlice = append(freqSlice, freq)
}
// 按权重*频次降序排列
sort.Slice(freqSlice, func(i, j int) bool {
return freqSlice[i].Weight*float64(freqSlice[i].Frequency) > freqSlice[j].Weight*float64(freqSlice[j].Frequency)
})
logger.SugaredLogger.Infof("排序后的结果:%v", freqSlice)
return freqSlice
}
// FilterAndSortWords 过滤标点符号并按权重频次排序
func FilterAndSortWords(frequencies map[string]models.WordFreqWithWeight) []models.WordFreqWithWeight {
// 先过滤标点符号和分隔符
cleanFrequencies := FilterPunctuationAndSeparators(frequencies)
// 再按权重和频次排序
sortedFrequencies := SortByWeightAndFrequency(cleanFrequencies)
return sortedFrequencies
}
func FilterPunctuationAndSeparators(frequencies map[string]models.WordFreqWithWeight) map[string]models.WordFreqWithWeight {
filteredWords := make(map[string]models.WordFreqWithWeight)
for word, freqInfo := range frequencies {
// 过滤纯标点符号和分隔符
if !isPunctuationOrSeparator(word) {
filteredWords[word] = freqInfo
}
}
return filteredWords
}
// isPunctuationOrSeparator 判断是否为标点符号或分隔符
func isPunctuationOrSeparator(word string) bool {
// 空字符串
if strings.TrimSpace(word) == "" {
return true
}
// 检查是否全部由标点符号组成
for _, r := range word {
if !unicode.IsPunct(r) && !unicode.IsSymbol(r) && !unicode.IsSpace(r) {
return false
}
}
return true
}
// FilterWithRegex 使用正则表达式过滤标点和特殊字符
func FilterWithRegex(frequencies map[string]models.WordFreqWithWeight) map[string]models.WordFreqWithWeight {
filteredWords := make(map[string]models.WordFreqWithWeight)
// 匹配标点符号、特殊字符的正则表达式
punctuationRegex := regexp.MustCompile(`^[[:punct:][:space:]]+$`)
for word, freqInfo := range frequencies {
// 过滤纯标点符号
if !punctuationRegex.MatchString(word) && strings.TrimSpace(word) != "" {
filteredWords[word] = freqInfo
}
}
return filteredWords
}
// countWordFrequencyWithWeight 统计词频并包含权重信息
func countWordFrequencyWithWeight(text string) map[string]models.WordFreqWithWeight {
words := splitWords(text)
freqMap := make(map[string]models.WordFreqWithWeight)
// 统计词频
wordCount := make(map[string]int)
for _, word := range words {
wordCount[word]++
}
// 构建包含权重的结果
for word, frequency := range wordCount {
weight := getWordWeight(word)
if weight >= basefreq {
freqMap[word] = models.WordFreqWithWeight{
Word: word,
Frequency: frequency,
Weight: weight,
Score: float64(frequency) * weight,
}
}
}
return freqMap
}
// AnalyzeSentimentWithFreqWeight 带权重词频统计的情感分析
func AnalyzeSentimentWithFreqWeight(text string) (models.SentimentResult, map[string]models.WordFreqWithWeight) {
// 原有情感分析逻辑
result := AnalyzeSentiment(text)
// 带权重的词频统计
frequencies := countWordFrequencyWithWeight(text)
return result, frequencies
}
const (
Positive SentimentType = iota
Positive models.SentimentType = iota
Negative
Neutral
)
// AnalyzeSentiment 判断文本的情感
func AnalyzeSentiment(text string) SentimentResult {
func AnalyzeSentiment(text string) models.SentimentResult {
// 初始化得分
score := 0.0
positiveCount := 0
@@ -120,7 +368,7 @@ func AnalyzeSentiment(text string) SentimentResult {
}
// 确定情感类别
var category SentimentType
var category models.SentimentType
switch {
case score > 1.0:
category = Positive
@@ -130,7 +378,7 @@ func AnalyzeSentiment(text string) SentimentResult {
category = Neutral
}
return SentimentResult{
return models.SentimentResult{
Score: score,
Category: category,
PositiveCount: positiveCount,
@@ -243,7 +491,7 @@ func splitWords(text string) []string {
}
// GetSentimentDescription 获取情感类别的文本描述
func GetSentimentDescription(category SentimentType) string {
func GetSentimentDescription(category models.SentimentType) string {
switch category {
case Positive:
return "看涨"
@@ -288,3 +536,43 @@ func main() {
result.NegativeCount)
}
}
func SaveAnalyzeSentimentWithFreqWeight(frequencies []models.WordFreqWithWeight) {
sort.Slice(frequencies, func(i, j int) bool {
return frequencies[i].Frequency > frequencies[j].Frequency
})
wordAnalyzes := make([]models.WordAnalyze, 0)
for _, freq := range frequencies[:10] {
wordAnalyze := models.WordAnalyze{
WordFreqWithWeight: freq,
}
wordAnalyzes = append(wordAnalyzes, wordAnalyze)
}
db.Dao.CreateInBatches(wordAnalyzes, 1000)
}
func SaveStockSentimentAnalysis(result models.SentimentResult) {
db.Dao.Create(&models.SentimentResultAnalyze{
SentimentResult: result,
})
}
func NewsAnalyze(text string, save bool) (models.SentimentResult, []models.WordFreqWithWeight) {
if text == "" {
telegraphs := NewMarketNewsApi().GetNews24HoursList("", 1000*10)
messageText := strings.Builder{}
for _, telegraph := range *telegraphs {
messageText.WriteString(telegraph.Content + "\n")
}
text = messageText.String()
}
result, frequencies := AnalyzeSentimentWithFreqWeight(text)
// 过滤标点符号和分隔符
cleanFrequencies := FilterAndSortWords(frequencies)
if save {
go SaveAnalyzeSentimentWithFreqWeight(cleanFrequencies)
go SaveStockSentimentAnalysis(result)
}
return result, cleanFrequencies
}

View File

@@ -2,8 +2,12 @@ package data
import (
"fmt"
"go-stock/backend/db"
"go-stock/backend/logger"
"strings"
"testing"
"github.com/duke-git/lancet/v2/random"
)
// @Author spark
@@ -12,25 +16,34 @@ import (
//-----------------------------------------------------------------------------------
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日电据央视新闻报道以军在贝尔谢巴及周边区域拦截了数枚伊朗导弹但仍有导弹或拦截残骸落地。以色列国防军发文表示搜救队伍正在一处“空中物体落地”的所在区域开展工作公众目前可以离开避难场所。伊朗方面对上述说法暂无回应。"
db.Init("../../data/stock.db")
InitAnalyzeSentiment()
messageText := strings.Builder{}
news := NewMarketNewsApi().GetNewsList2("", random.RandInt(500, 1000))
for _, telegraph := range *news {
messageText.WriteString(telegraph.Content + "\n")
}
text := messageText.String()
//text = " 【周六你需要知道的隔夜全球要闻:美联储鸽声重振 美股走势回稳】 1、纽约联储行长威廉姆斯表示随着劳动力市场走软美联储近期内仍有再次降息的空间。 2、美联储理事斯蒂芬·米兰表示自上次联邦公开市场委员会FOMC会议以来的经济数据应“促使人们偏向鸽派立场”。 3、波士顿联邦储备银行行长柯林斯表示由于通胀可能在一段时间内保持高位维持利率不变“目前合适”。 4、据CME“美联储观察”截至北京时间11月22日6时30分美联储12月降息25个基点的概率为69.4%维持利率不变的概率为30.6%。 5、美国劳工统计局表示11月CPI报告将于12月18日发布同时取消了10月CPI报告发布表示无法追溯采集政府停摆期间未能收集的部分数据。 6、俄罗斯总统普京表示已收到美提出解决俄乌冲突的计划俄罗斯愿意进行和平谈判。美国总统特朗普表示他认为27日是乌克兰接受美国支持的和平计划的最后期限。 7、美联储高官鸽派言论提振市场情绪美股三大指数收盘集体上涨道琼斯指数涨1.08%标普500指数涨0.98%纳斯达克综合指数涨0.88%。甲骨文跌超5%英伟达跌超1%。纳指本周累计跌2.74%标普500指数累跌1.95%道指累跌1.91%。英伟达本周累跌5.9%。 8、热门中概股多数上涨纳斯达克中国金龙指数收涨1.23%。蔚来涨超3%哔哩哔哩、理想汽车涨超2%京东、小鹏汽车涨超1%。 9、国际油价下跌交易员评估乌克兰与俄罗斯可能达成和平协议的前景。WTI 1月期货下跌1.6%结算价报每桶58.06美元为过去五个交易日中第四次下跌。布伦特1月期货下跌1.3%结算价报每桶62.56美元。 10、美联储延长压力测试改进方案征询期为银行反馈提供更多时间。 11、由于美国人对个人财务状况的看法恶化美国消费者信心在11月跌至接近纪录最低水平密歇根大学数据显示11月消费者信心指数降至5110月为53.6。 12、日本央行政策委员会委员Kazuyuki Masu表示日本央行接近作出加息决定。 13、穆迪将意大利信用评级从BAA3上调至BAA2展望稳定。\n"
//text = "财联社电英伟达周五冲高回落股价涨幅收于1%,市场普遍认为其走势疲软"
//text = "【本轮巴以冲突已致加沙地带69733人死亡】财联社11月22日电当地时间22日下午以军对加沙城西部一辆汽车发动空袭已造成5人死亡多人受伤。自2023年10月7日巴以新一轮大规模冲突爆发以来以色列对加沙地带的袭击已造成69733人死亡、170863人受伤。"
//text = "【牛肉加工亏损 美国泰森公司关停缩减相关业务】财联社11月22日电受牛肉加工业务亏损影响当地时间21日美国泰森食品公司发布公告称将关闭位于内布拉斯加州的一家大型牛肉加工厂还计划缩小得克萨斯州一家牛肉加工厂的生产规模。根据泰森食品公司的公告被关闭的这家工厂位于内布拉斯加州列克星敦日均可宰杀并处理大约5000头牛约占全美日均牛肉屠宰数量的4.8%。与此同时公司还计划缩小得克萨斯州一家牛肉加工厂的生产规模这家工厂每天大约可屠宰6000头牛。据悉泰森此次业务调整影响两个工厂大约5000个工作岗位。《华尔街日报》报道称泰森是美国四大肉类加工公司中首家关闭主要牛肉加工厂的公司其最新财报显示2025财年牛肉加工是唯一亏损的业务部门调整后的营业亏损为4.26亿美元。"
// 分析情感
words := splitWords(text)
fmt.Println(strings.Join(words, " "))
result := AnalyzeSentiment(text)
result, frequencies := AnalyzeSentimentWithFreqWeight(text)
// 过滤标点符号和分隔符
cleanFrequencies := FilterPunctuationAndSeparators(frequencies)
// 输出结果
fmt.Printf("情感分析结果: %s (得分: %.2f, 正面词:%d, 负面词:%d)\n",
logger.SugaredLogger.Infof("情感分析结果: %s (得分: %.2f, 正面词:%d, 负面词:%d)\n 词频统计结果: %v",
result.Description,
result.Score,
result.PositiveCount,
result.NegativeCount)
result.NegativeCount,
cleanFrequencies,
)
}

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

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

View File

@@ -1,9 +1,10 @@
package models
import (
"time"
"gorm.io/gorm"
"gorm.io/plugin/soft_delete"
"time"
)
// @Author spark
@@ -148,6 +149,34 @@ func (receiver AIResponseResult) TableName() string {
return "ai_response_result"
}
// AIResponseResultQuery 分页查询参数
type AIResponseResultQuery struct {
Page int `form:"page" json:"page"` // 页码
PageSize int `form:"pageSize" json:"pageSize"` // 每页大小
ChatId string `form:"chatId" json:"chatId"` // 聊天ID筛选
ModelName string `form:"modelName" json:"modelName"` // 模型名称筛选
StockCode string `form:"stockCode" json:"stockCode"` // 股票代码筛选
StockName string `form:"stockName" json:"stockName"` // 股票名称筛选
Question string `form:"question" json:"question"` // 问题内容模糊搜索
StartDate string `form:"startDate" json:"startDate"` // 开始日期
EndDate string `form:"endDate" json:"endDate"` // 结束日期
}
// AIResponseResultPageResp 分页查询响应
type AIResponseResultPageResp struct {
Code int `json:"code"`
Message string `json:"message"`
Data AIResponseResultPageData `json:"data"`
}
type AIResponseResultPageData struct {
List []AIResponseResult `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
TotalPages int `json:"totalPages"`
}
type VersionInfo struct {
gorm.Model
Version string `json:"version"`
@@ -155,6 +184,7 @@ type VersionInfo struct {
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"`
@@ -171,6 +201,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 {
@@ -186,6 +218,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 {
@@ -195,6 +229,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 {
@@ -220,14 +260,16 @@ type Prompt struct {
type Telegraph struct {
gorm.Model
Time string `json:"time"`
Content string `json:"content"`
DataTime *time.Time `json:"dataTime" gorm:"index"`
Title string `json:"title" gorm:"index"`
Content string `json:"content" gorm:"index"`
SubjectTags []string `json:"subjects" gorm:"-:all"`
StocksTags []string `json:"stocks" gorm:"-:all"`
IsRed bool `json:"isRed"`
IsRed bool `json:"isRed" gorm:"index"`
Url string `json:"url"`
Source string `json:"source"`
Source string `json:"source" gorm:"index"`
TelegraphTags []TelegraphTags `json:"tags" gorm:"-:migration;foreignKey:TelegraphId"`
SentimentResult string `json:"sentimentResult" gorm:"-:all"`
SentimentResult string `json:"sentimentResult" gorm:"index"`
}
type TelegraphTags struct {
gorm.Model
@@ -318,6 +360,22 @@ type TVNews struct {
LogoId string `json:"logo_id"`
} `json:"provider"`
}
type TVNewsDetail struct {
ShortDescription string `json:"shortDescription"`
Tags []struct {
Title string `json:"title"`
Args []struct {
Id string `json:"id"`
Value string `json:"value"`
} `json:"args"`
} `json:"tags"`
Copyright string `json:"copyright"`
Id string `json:"id"`
Title string `json:"title"`
Published int `json:"published"`
Urgency int `json:"urgency"`
StoryPath string `json:"storyPath"`
}
type XUEQIUHot struct {
Data struct {
@@ -329,29 +387,29 @@ type XUEQIUHot struct {
}
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 int `json:"type" md:"-"`
Code string `json:"code" md:"股票代码"`
Name string `json:"name" md:"股票名称"`
Value float64 `json:"value" md:"热度"`
Increment int `json:"increment" md:"热度变化"`
RankChange int `json:"rank_change" md:"排名变化"`
HasExist interface{} `json:"has_exist" md:"-"`
Symbol string `json:"symbol" md:"-"`
Percent float64 `json:"percent" md:"涨跌幅(%)"`
Current float64 `json:"current" md:"股价"`
Chg float64 `json:"chg" md:"股价变化"`
Exchange string `json:"exchange" md:"交易所代码"`
StockType int `json:"stock_type" md:"-"`
SubType string `json:"sub_type" md:"-"`
Ad int `json:"ad" md:"-"`
AdId interface{} `json:"ad_id" md:"-"`
ContentId interface{} `json:"content_id" md:"-"`
Page interface{} `json:"page" md:"-"`
Model interface{} `json:"model" md:"-"`
Location interface{} `json:"location" md:"-"`
TradeSession interface{} `json:"trade_session" md:"-"`
CurrentExt interface{} `json:"current_ext" md:"-"`
PercentExt interface{} `json:"percent_ext" md:"-"`
}
type HotEvent struct {
@@ -363,3 +421,534 @@ type HotEvent struct {
StatusCount int `json:"status_count"`
Content string `json:"content"`
}
type GDP struct {
REPORTDATE string `json:"REPORT_DATE" md:"报告时间"`
TIME string `json:"TIME" md:"报告期"`
DOMESTICLPRODUCTBASE float64 `json:"DOMESTICL_PRODUCT_BASE" md:"国内生产总值(亿元)"`
SUMSAME float64 `json:"SUM_SAME" md:"国内生产总值同比增长(%)"`
FIRSTPRODUCTBASE float64 `json:"FIRST_PRODUCT_BASE" md:"第一产业(亿元)"`
FIRSTSAME int `json:"FIRST_SAME" md:"第一产业同比增长(%)"`
SECONDPRODUCTBASE float64 `json:"SECOND_PRODUCT_BASE" md:"第二产业(亿元)"`
SECONDSAME float64 `json:"SECOND_SAME" md:"第二产业同比增长(%)"`
THIRDPRODUCTBASE float64 `json:"THIRD_PRODUCT_BASE" md:"第三产业(亿元)"`
THIRDSAME float64 `json:"THIRD_SAME" md:"第三产业同比增长(%)"`
}
type CPI struct {
REPORTDATE string `json:"REPORT_DATE" md:"报告时间"`
TIME string `json:"TIME" md:"报告期"`
NATIONALBASE float64 `json:"NATIONAL_BASE" md:"全国当月"`
NATIONALSAME float64 `json:"NATIONAL_SAME" md:"全国当月同比增长(%)"`
NATIONALSEQUENTIAL float64 `json:"NATIONAL_SEQUENTIAL" md:"全国当月环比增长(%)"`
NATIONALACCUMULATE float64 `json:"NATIONAL_ACCUMULATE" md:"全国当月累计"`
CITYBASE float64 `json:"CITY_BASE" md:"城市当月"`
CITYSAME float64 `json:"CITY_SAME" md:"城市当月同比增长(%)"`
CITYSEQUENTIAL float64 `json:"CITY_SEQUENTIAL" md:"城市当月环比增长(%)"`
CITYACCUMULATE int `json:"CITY_ACCUMULATE" md:"城市当月累计"`
RURALBASE float64 `json:"RURAL_BASE" md:"农村当月"`
RURALSAME float64 `json:"RURAL_SAME" md:"农村当月同比增长(%)"`
RURALSEQUENTIAL int `json:"RURAL_SEQUENTIAL" md:"农村当月环比增长(%)"`
RURALACCUMULATE float64 `json:"RURAL_ACCUMULATE" md:"农村当月累计"`
}
type PPI struct {
REPORTDATE string `json:"REPORT_DATE" md:"报告时间"`
TIME string `json:"TIME" md:"报告期"`
BASE float64 `json:"BASE" md:"当月"`
BASESAME float64 `json:"BASE_SAME" md:"当月同比增长(%)"`
BASEACCUMULATE float64 `json:"BASE_ACCUMULATE" md:"累计"`
}
type PMI struct {
REPORTDATE string `md:"报告时间" json:"REPORT_DATE"`
TIME string `md:"报告期" json:"TIME"`
MAKEINDEX float64 `md:"制造业指数" json:"MAKE_INDEX"`
MAKESAME float64 `md:"制造业指数同比增长(%)" json:"MAKE_SAME"`
NMAKEINDEX float64 `md:"非制造业" json:"NMAKE_INDEX"`
NMAKESAME float64 `md:"非制造业同比增长(%)" json:"NMAKE_SAME"`
}
type DCResp struct {
Version string `json:"version"`
Success bool `json:"success"`
Message string `json:"message"`
Code int `json:"code"`
}
type GDPResult struct {
Pages int `json:"pages"`
Data []GDP `json:"data"`
Count int `json:"count"`
}
type CPIResult struct {
Pages int `json:"pages"`
Data []CPI `json:"data"`
Count int `json:"count"`
}
type PPIResult struct {
Pages int `json:"pages"`
Data []PPI `json:"data"`
Count int `json:"count"`
}
type PMIResult struct {
Pages int `json:"pages"`
Data []PMI `json:"data"`
Count int `json:"count"`
}
type GDPResp struct {
DCResp
GDPResult GDPResult `json:"result"`
}
type CPIResp struct {
DCResp
CPIResult CPIResult `json:"result"`
}
type PPIResp struct {
DCResp
PPIResult PPIResult `json:"result"`
}
type PMIResp struct {
DCResp
PMIResult PMIResult `json:"result"`
}
type OldSettings struct {
gorm.Model
TushareToken string `json:"tushareToken"`
LocalPushEnable bool `json:"localPushEnable"`
DingPushEnable bool `json:"dingPushEnable"`
DingRobot string `json:"dingRobot"`
UpdateBasicInfoOnStart bool `json:"updateBasicInfoOnStart"`
RefreshInterval int64 `json:"refreshInterval"`
OpenAiEnable bool `json:"openAiEnable"`
OpenAiBaseUrl string `json:"openAiBaseUrl"`
OpenAiApiKey string `json:"openAiApiKey"`
OpenAiModelName string `json:"openAiModelName"`
OpenAiMaxTokens int `json:"openAiMaxTokens"`
OpenAiTemperature float64 `json:"openAiTemperature"`
OpenAiApiTimeOut int `json:"openAiApiTimeOut"`
Prompt string `json:"prompt"`
CheckUpdate bool `json:"checkUpdate"`
QuestionTemplate string `json:"questionTemplate"`
CrawlTimeOut int64 `json:"crawlTimeOut"`
KDays int64 `json:"kDays"`
EnableDanmu bool `json:"enableDanmu"`
BrowserPath string `json:"browserPath"`
EnableNews bool `json:"enableNews"`
DarkTheme bool `json:"darkTheme"`
BrowserPoolSize int `json:"browserPoolSize"`
EnableFund bool `json:"enableFund"`
EnablePushNews bool `json:"enablePushNews"`
SponsorCode string `json:"sponsorCode"`
}
func (receiver OldSettings) TableName() string {
return "settings"
}
type ReutersNews struct {
StatusCode int `json:"statusCode"`
Message string `json:"message"`
Result struct {
ParentSectionName string `json:"parent_section_name"`
Pagination struct {
Size int `json:"size"`
ExpectedSize int `json:"expected_size"`
TotalSize int `json:"total_size"`
Orderby string `json:"orderby"`
} `json:"pagination"`
DateModified time.Time `json:"date_modified"`
FetchType string `json:"fetch_type"`
Articles []struct {
Id string `json:"id"`
CanonicalUrl string `json:"canonical_url"`
Website string `json:"website"`
Web string `json:"web"`
Native string `json:"native"`
UpdatedTime time.Time `json:"updated_time"`
PublishedTime time.Time `json:"published_time"`
ArticleType string `json:"article_type"`
DisplayMyNews bool `json:"display_my_news"`
DisplayNewsletterSignup bool `json:"display_newsletter_signup"`
DisplayNotifications bool `json:"display_notifications"`
DisplayRelatedMedia bool `json:"display_related_media"`
DisplayRelatedOrganizations bool `json:"display_related_organizations"`
ContentCode string `json:"content_code"`
Source struct {
Name string `json:"name"`
OriginalName string `json:"original_name"`
} `json:"source"`
Title string `json:"title"`
BasicHeadline string `json:"basic_headline"`
Distributor string `json:"distributor"`
Description string `json:"description"`
PrimaryMediaType string `json:"primary_media_type,omitempty"`
PrimaryTag struct {
ShortBio string `json:"short_bio"`
Description string `json:"description"`
Slug string `json:"slug"`
Text string `json:"text"`
TopicUrl string `json:"topic_url"`
CanFollow bool `json:"can_follow,omitempty"`
IsTopic bool `json:"is_topic,omitempty"`
} `json:"primary_tag"`
WordCount int `json:"word_count"`
ReadMinutes int `json:"read_minutes"`
Kicker struct {
Path string `json:"path"`
Names []string `json:"names"`
Name string `json:"name,omitempty"`
} `json:"kicker"`
AdTopics []string `json:"ad_topics"`
Thumbnail struct {
Url string `json:"url"`
Caption string `json:"caption,omitempty"`
Type string `json:"type"`
ResizerUrl string `json:"resizer_url"`
Location string `json:"location,omitempty"`
Id string `json:"id"`
Authors string `json:"authors,omitempty"`
AltText string `json:"alt_text"`
Width int `json:"width"`
Height int `json:"height"`
Subtitle string `json:"subtitle"`
Slug string `json:"slug,omitempty"`
UpdatedAt time.Time `json:"updated_at"`
Company string `json:"company,omitempty"`
PurchaseLicensingPath string `json:"purchase_licensing_path,omitempty"`
} `json:"thumbnail"`
Authors []struct {
Id string `json:"id,omitempty"`
Name string `json:"name"`
FirstName string `json:"first_name,omitempty"`
LastName string `json:"last_name,omitempty"`
Company string `json:"company"`
Thumbnail struct {
Url string `json:"url"`
Type string `json:"type"`
ResizerUrl string `json:"resizer_url"`
} `json:"thumbnail"`
SocialLinks []struct {
Site string `json:"site"`
Url string `json:"url"`
} `json:"social_links,omitempty"`
Byline string `json:"byline"`
Description string `json:"description,omitempty"`
TopicUrl string `json:"topic_url,omitempty"`
Role string `json:"role,omitempty"`
} `json:"authors"`
DisplayTime time.Time `json:"display_time"`
ThumbnailDark struct {
Url string `json:"url"`
Type string `json:"type"`
ResizerUrl string `json:"resizer_url"`
Id string `json:"id"`
AltText string `json:"alt_text"`
Width int `json:"width"`
Height int `json:"height"`
Subtitle string `json:"subtitle"`
UpdatedAt time.Time `json:"updated_at"`
} `json:"thumbnail_dark,omitempty"`
} `json:"articles"`
Section struct {
Id string `json:"id"`
AdUnitCode string `json:"ad_unit_code"`
Website string `json:"website"`
Name string `json:"name"`
PageTitle string `json:"page_title"`
CanFollow bool `json:"can_follow"`
Language string `json:"language"`
Type string `json:"type"`
Advertising struct {
Sponsored string `json:"sponsored"`
} `json:"advertising"`
VideoPlaylistId string `json:"video_playlistId"`
MobileAdUnitPath string `json:"mobile_ad_unit_path"`
AdUnitPath string `json:"ad_unit_path"`
CollectionAlias string `json:"collection_alias"`
SectionAbout string `json:"section_about"`
Title string `json:"title"`
Personalization struct {
Id string `json:"id"`
Type string `json:"type"`
ShowTags bool `json:"show_tags"`
CanFollow bool `json:"can_follow"`
} `json:"personalization"`
} `json:"section"`
AdUnitPath string `json:"ad_unit_path"`
ResponseTime int64 `json:"response_time"`
} `json:"result"`
Id string `json:"_id"`
}
type InteractiveAnswer struct {
PageNo int `json:"pageNo"`
PageSize int `json:"pageSize"`
TotalRecord int `json:"totalRecord"`
TotalPage int `json:"totalPage"`
Results []InteractiveAnswerResults `json:"results"`
Count bool `json:"count"`
}
type InteractiveAnswerResults struct {
EsId string `json:"esId" md:"-"`
IndexId string `json:"indexId" md:"-"`
ContentType int `json:"contentType" md:"-"`
Trade []string `json:"trade" md:"行业名称"`
MainContent string `json:"mainContent" md:"投资者提问"`
StockCode string `json:"stockCode" md:"股票代码"`
Secid string `json:"secid" md:"-"`
CompanyShortName string `json:"companyShortName" md:"股票名称"`
CompanyLogo string `json:"companyLogo,omitempty" md:"-"`
BoardType []string `json:"boardType" md:"-"`
PubDate string `json:"pubDate" md:"发布时间"`
UpdateDate string `json:"updateDate" md:"-"`
Author string `json:"author" md:"-"`
AuthorName string `json:"authorName" md:"-"`
PubClient string `json:"pubClient" md:"-"`
AttachedId string `json:"attachedId" md:"-"`
AttachedContent string `json:"attachedContent" md:"上市公司回复"`
AttachedAuthor string `json:"attachedAuthor" md:"-"`
AttachedPubDate string `json:"attachedPubDate" md:"回复时间"`
Score float64 `json:"score" md:"-"`
TopStatus int `json:"topStatus" md:"-"`
PraiseCount int `json:"praiseCount" md:"-"`
PraiseStatus bool `json:"praiseStatus" md:"-"`
FavoriteStatus bool `json:"favoriteStatus" md:"-"`
AttentionCompany bool `json:"attentionCompany" md:"-"`
IsCheck string `json:"isCheck" md:"-"`
QaStatus int `json:"qaStatus" md:"-"`
PackageDate string `json:"packageDate" md:"-"`
RemindStatus bool `json:"remindStatus" md:"-"`
InterviewLive bool `json:"interviewLive" md:"-"`
}
type CailianpressWeb struct {
Total int `json:"total"`
List []struct {
Title string `json:"title" md:"资讯标题"`
Ctime int `json:"ctime" md:"资讯时间"`
Content string `json:"content" md:"资讯内容"`
Author string `json:"author" md:"资讯发布者"`
} `json:"list"`
}
type BKDict struct {
gorm.Model `md:"-"`
BkCode string `json:"bkCode" md:"行业/板块代码"`
BkName string `json:"bkName" md:"行业/板块名称"`
FirstLetter string `json:"firstLetter" md:"first_letter"`
FubkCode string `json:"fubkCode" md:"fubk_code"`
PublishCode string `json:"publishCode" md:"publish_code"`
}
func (b BKDict) TableName() string {
return "bk_dict"
}
type WordAnalyze struct {
gorm.Model
DataTime *time.Time `json:"dataTime" gorm:"index;autoCreateTime"`
WordFreqWithWeight
}
// WordFreqWithWeight 词频统计结果,包含权重信息
type WordFreqWithWeight struct {
Word string
Frequency int
Weight float64
Score float64
}
// SentimentResult 情感分析结果类型
type SentimentResult struct {
Score float64 // 情感得分
Category SentimentType // 情感类别
PositiveCount int // 正面词数量
NegativeCount int // 负面词数量
Description string // 情感描述
}
type SentimentResultAnalyze struct {
gorm.Model
DataTime *time.Time `json:"dataTime" gorm:"index;autoCreateTime"`
SentimentResult
}
// SentimentType 情感类型枚举
type SentimentType int
type HotStrategy struct {
ChgEffect bool `json:"chgEffect"`
Code int `json:"code"`
Data []*HotStrategyData `json:"data"`
Message string `json:"message"`
}
type HotStrategyData struct {
Chg float64 `json:"chg" md:"平均涨幅(%)"`
Code string `json:"code" md:"-"`
HeatValue int `json:"heatValue" md:"热度值"`
Market string `json:"market" md:"-"`
Question string `json:"question" md:"选股策略"`
Rank int `json:"rank" md:"-"`
}
type NtfyNews struct {
Id string `json:"id"`
Time int `json:"time"`
Expires int `json:"expires"`
Event string `json:"event"`
Topic string `json:"topic"`
Title string `json:"title"`
Message string `json:"message"`
Tags []string `json:"tags"`
Icon string `json:"icon"`
}
type THSHotStrategy struct {
Result struct {
Num int `json:"num"`
List []struct {
Author struct {
Avatar string `json:"avatar"`
UserName string `json:"userName"`
UserId int `json:"userId"`
} `json:"author"`
Property struct {
Id int `json:"id"`
Name string `json:"name"`
Query string `json:"query"`
Logic string `json:"logic"`
BuyPosition interface{} `json:"buyPosition"`
Ctime string `json:"ctime"`
Tags []string `json:"tags"`
WinRate string `json:"winRate"`
AnnualYield string `json:"annualYield"`
Type int `json:"type"`
} `json:"property"`
Interaction struct {
CommentNum int `json:"commentNum"`
CollectNum int `json:"collectNum"`
IsCollected bool `json:"isCollected"`
IsSubscribe int `json:"isSubscribe"`
IsPublish int `json:"isPublish"`
Pid int `json:"pid"`
} `json:"interaction"`
} `json:"list"`
} `json:"result"`
}
type StockMoneyDataResp struct {
Rc int `json:"rc"`
Rt int `json:"rt"`
Svr int `json:"svr"`
Lt int `json:"lt"`
Full int `json:"full"`
Dlmkts string `json:"dlmkts"`
Data StockMoneyData `json:"data"`
}
type StockMoneyData struct {
Total int `json:"total"`
Diff []StockMoneyDataDiff `json:"diff"`
}
type StockMoneyDataDiff struct {
F1 int `json:"f1" md:"-"`
F12 string `json:"f12" md:"股票代码"`
F13 int `json:"f13" md:"-"`
F14 string `json:"f14" md:"股票名称"`
F2 float64 `json:"f2" md:"最新价"`
F3 float64 `json:"f3" md:"今日涨跌幅(%)"`
F62 float64 `json:"f62" md:"今日主力净额(元)"`
F184 float64 `json:"f184" md:"今日主力净占比(%)"`
F66 float64 `json:"f66" md:"今日超大单净额(元)"`
F69 float64 `json:"f69" md:"今日超大单净占比(%)"`
F72 float64 `json:"f72" md:"今日大单净额(元)"`
F75 float64 `json:"f75" md:"今日大单净占比(%)"`
F78 float64 `json:"f78" md:"今日中单净额(元)"`
F81 float64 `json:"f81" md:"今日中单净占比(%)"`
F84 float64 `json:"f84" md:"今日小单净额(元)"`
F87 float64 `json:"f87" md:"今日小单净占比(%)"`
F124 int `json:"f124" md:"f124"`
F100 string `json:"f100" md:"所属板块"`
F265 string `json:"f265" md:"板块代码"`
}
type StockConceptInfoResp struct {
Version string `json:"version"`
Result StockConceptInfoResult `json:"result"`
Success bool `json:"success"`
Message string `json:"message"`
Code int `json:"code"`
}
type StockConceptInfoResult struct {
Pages int `json:"pages"`
Data []StockConceptInfo `json:"data"`
Count int `json:"count"`
}
type StockConceptInfo struct {
SECUCODE string `json:"SECUCODE" md:"完整股票代码"`
SECURITYCODE string `json:"SECURITY_CODE" md:"股票代码"`
SECURITYNAMEABBR string `json:"SECURITY_NAME_ABBR" md:"股票名称"`
NEWBOARDCODE string `json:"NEW_BOARD_CODE" md:"板块/概念代码"`
BOARDNAME string `json:"BOARD_NAME" md:"板块/概念名称"`
SELECTEDBOARDREASON string `json:"SELECTED_BOARD_REASON" md:"板块/概念描述"`
ISPRECISE string `json:"IS_PRECISE" md:"-"`
BOARDRANK int `json:"BOARD_RANK" md:"-"`
BOARDYIELD float64 `json:"BOARD_YIELD" md:"板块/概念涨跌幅(%)"`
DERIVEBOARDCODE string `json:"DERIVE_BOARD_CODE" md:"-"`
}
type AiRecommendStocks struct {
gorm.Model
DataTime *time.Time `json:"dataTime" gorm:"index;autoCreateTime"`
ModelName string `json:"modelName" md:"模型名称"`
StockCode string `json:"stockCode" md:"股票代码"`
StockName string `json:"stockName" md:"股票名称"`
BkCode string `json:"bkCode" md:"行业/板块代码"`
BkName string `json:"bkName" md:"行业/板块名称"`
StockPrice string `json:"stockPrice" md:"推荐时股票价格"`
StockCurrentPrice string `json:"stockCurrentPrice" md:"当前价格"`
StockCurrentPriceTime string `json:"stockCurrentPriceTime" md:"当前价格时间"`
StockClosePrice string `json:"stockClosePrice" md:"推荐时股票收盘价格"`
StockPrePrice string `json:"stockPrePrice" md:"前一交易日股票价格"`
RecommendReason string `json:"recommendReason" md:"推荐理由/驱动因素/逻辑"`
RecommendBuyPrice string `json:"recommendBuyPrice" md:"ai建议买入价"`
RecommendStopProfitPrice string `json:"recommendStopProfitPrice" md:"ai建议止盈价"`
RecommendStopLossPrice string `json:"recommendStopLossPrice" md:"ai建议止损价"`
RiskRemarks string `json:"riskRemarks" md:"风险提示"`
Remarks string `json:"remarks" md:"备注"`
}
func (receiver AiRecommendStocks) TableName() string { return "ai_recommend_stocks" }
type AiRecommendStocksQuery struct {
Page int `form:"page" json:"page"` // 页码
PageSize int `form:"pageSize" json:"pageSize"` // 每页大小
StockCode string `form:"stockCode" json:"stockCode"` // 股票代码筛选
StockName string `form:"stockName" json:"stockName"` // 股票名称筛选
BkCode string `form:"bkCode" json:"bkCode"` // 板块代码筛选
BkName string `form:"bkName" json:"bkName"` // 板块名称筛选
StartDate string `form:"startDate" json:"startDate"` // 开始日期
EndDate string `form:"endDate" json:"endDate"` // 结束日期
}
type AiRecommendStocksPageResp struct {
Code int `json:"code"`
Message string `json:"message"`
Data AiRecommendStocksPageData `json:"data"`
}
type AiRecommendStocksPageData struct {
List []AiRecommendStocks `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
TotalPages int `json:"totalPages"`
}

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/img15.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

193
data/dict/user.txt Normal file
View File

@@ -0,0 +1,193 @@
# 补充热点概念与板块Jieba/gse兼容格式
# 权重说明核心热点500-700分事件类400分负权重词汇按需求保留
# 一、负权重低优先级词汇(减少无差别匹配干扰)
公司 -0.1 n
国家 -0.1 n
国际 -0.1 n
会议 -0.1 n
市场 -0.1 n
经济 -0.1 n
技术 -0.1 n
记者 -0.1 n
时间 -0.1 n
项目 -0.1 n
问题 -0.1 n
企业 -0.1 n
财联社 -0.1 n
上涨 -0.1 v
下跌 -0.1 v
期货 -0.1 n
跌幅 -0.1 n
跌超 -0.1 adj
股票 -0.1 n
基金 -0.1 n
电讯 -0.1 n
建筑 -0.1 n
平开 -0.1 n
保险 -0.1 n
行业 -0.1 n
其他 -0.1 n
a股 -0.1 n
港股 -0.1 n
etf -0.1 n
涨幅 -0.1 n
交易所 -0.1 n
证券 -0.1 n
ai -0.1 n
# 二、核心热点概念700分最高优先级
端侧AI 700 n
AI应用 700 n
比特币 700 n
摩尔线程 700 n
摩尔线程概念 700 n
AI算力 700 n
生成式AI 700 n
量子计算 700 n
脑机接口 700 n
6G通信 700 n
人形机器人 700 n
固态电池 700 n
ChatGPT概念 700 n
Web3.0 700 n
元宇宙 700 n
数字孪生 700 n
量子通信 700 n
# 三、重点赛道板块500分高优先级
冰雪旅游 500 n
特高压 500 n
跨境电商 500 n
新能源汽车 500 n
机器人 500 n
具身智能 500 n
油气 500 n
商业航天 500 n
光伏储能 500 n
锂电材料 500 n
半导体设备 500 n
集成电路 500 n
创新药 500 n
CXO 500 n
医疗器械 500 n
数字经济 500 n
数字货币 500 n
区块链 500 n
低空经济 500 n
工业互联网 500 n
物联网 500 n
5G应用 500 n
充电桩 500 n
氢能源 500 n
核聚变 500 n
工业母机 500 n
新材料 500 n
生物制造 500 n
智能网联汽车 500 n
乡村振兴 500 n
国企改革 500 n
央企重组 500 n
跨境金融 500 n
自贸港 500 n
一带一路 500 n
绿色低碳 500 n
碳交易 500 n
数据要素 500 n
数字基建 500 n
东数西算 500 n
国产替代 500 n
信创 500 n
网络安全 500 n
算力网络 500 n
边缘计算 500 n
虚拟现实 500 n
增强现实 500 n
智能穿戴 500 n
智能家居 500 n
车联网 500 n
激光雷达 500 n
氮化镓 500 n
碳化硅 500 n
第三代半导体 500 n
EDA工具 500 n
光刻胶 500 n
芯片设计 500 n
封装测试 500 n
储能电池 500 n
钠离子电池 500 n
氢燃料电池 500 n
光伏组件 500 n
风电设备 500 n
特高压设备 500 n
电力物联网 500 n
智能电网 500 n
轨道交通 500 n
航空航天 500 n
海洋工程 500 n
高端装备 500 n
军工电子 500 n
卫星互联网 500 n
北斗导航 500 n
国产大飞机 500 n
生物医药 500 n
基因测序 500 n
疫苗 500 n
医疗美容 500 n
养老产业 500 n
教育信息化 500 n
体育产业 500 n
文化创意 500 n
旅游复苏 500 n
预制菜 500 n
白酒 500 n
食品饮料 500 n
家电下乡 500 n
房地产复苏 500 n
基建投资 500 n
新型城镇化 500 n
冷链物流 500 n
快递物流 500 n
跨境支付 500 n
金融科技 500 n
消费电子 500 n
元宇宙基建 500 n
数字藏品 500 n
NFT 500 n
绿色电力 500 n
节能降碳 500 n
抽水蓄能 500 n
生物质能 500 n
地热能 500 n
潮汐能 500 n
# 四、事件驱动型概念400分中优先级
俄乌冲突 400 n
中东局势 400 n
美联储加息 400 n
降息预期 400 n
贸易摩擦 400 n
供应链重构 400 n
能源危机 400 n
粮食安全 400 n
疫情复苏 400 n
政策利好 400 n
产业扶持 400 n
技术突破 400 n
并购重组 400 n
IPO提速 400 n
解禁潮 400 n
北向资金流入 400 n
南向资金流入 400 n
主力资金异动 400 n
行业景气度 400 n
业绩预增 400 n
商誉减值 400 n
退市风险 400 n
监管新规 400 n
税收优惠 400 n
补贴政策 400 n
基建刺激 400 n
消费刺激 400 n
新能源补贴 400 n
碳达峰政策 400 n
碳中和目标 400 n

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 {
}

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

@@ -0,0 +1,48 @@
/* 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']
AiRecommendStocksList: typeof import('./src/components/aiRecommendStocksList.vue')['default']
AnalyzeMartket: typeof import('./src/components/AnalyzeMartket.vue')['default']
ClsCalendarTimeLine: typeof import('./src/components/ClsCalendarTimeLine.vue')['default']
EmbeddedUrl: typeof import('./src/components/EmbeddedUrl.vue')['default']
Fund: typeof import('./src/components/fund.vue')['default']
HotEvents: typeof import('./src/components/HotEvents.vue')['default']
HotStockList: typeof import('./src/components/HotStockList.vue')['default']
HotTopics: typeof import('./src/components/HotTopics.vue')['default']
IndustryMoneyRank: typeof import('./src/components/industryMoneyRank.vue')['default']
IndustryResearchReportList: typeof import('./src/components/IndustryResearchReportList.vue')['default']
InvestCalendarTimeLine: typeof import('./src/components/InvestCalendarTimeLine.vue')['default']
KLineChart: typeof import('./src/components/KLineChart.vue')['default']
LongTigerRankList: typeof import('./src/components/LongTigerRankList.vue')['default']
Market: typeof import('./src/components/market.vue')['default']
MoneyTrend: typeof import('./src/components/moneyTrend.vue')['default']
NewsList: typeof import('./src/components/newsList.vue')['default']
RankTable: typeof import('./src/components/rankTable.vue')['default']
ResearchIndex: typeof import('./src/components/researchIndex.vue')['default']
ResearchReport: typeof import('./src/components/researchReport.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SelectStock: typeof import('./src/components/SelectStock.vue')['default']
Settings: typeof import('./src/components/settings.vue')['default']
Stock: typeof import('./src/components/stock.vue')['default']
Stockhotmap: typeof import('./src/components/stockhotmap.vue')['default']
StockNoticeList: typeof import('./src/components/StockNoticeList.vue')['default']
StockResearchReportList: typeof import('./src/components/StockResearchReportList.vue')['default']
StockSparkLine: typeof import('./src/components/stockSparkLine.vue')['default']
TChat: typeof import('@tdesign-vue-next/chat')['Chat']
TChatAction: typeof import('@tdesign-vue-next/chat')['ChatAction']
TChatContent: typeof import('@tdesign-vue-next/chat')['ChatContent']
TChatLoading: typeof import('@tdesign-vue-next/chat')['ChatLoading']
TChatSender: typeof import('@tdesign-vue-next/chat')['ChatSender']
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1 +1 @@
2d63c3a999d797889c01d6c96451b197
f4fb0059ba6044c039be717fcc2e40bc

View File

@@ -6,17 +6,18 @@ import {
Quit,
WindowFullscreen,
WindowHide,
WindowUnfullscreen
WindowUnfullscreen,
WindowSetTitle
} from '../wailsjs/runtime'
import {h, onBeforeMount, onBeforeUnmount, onMounted, ref} from "vue";
import {RouterLink, useRouter} from 'vue-router'
import {createDiscreteApi,darkTheme,lightTheme , NIcon, NText,dateZhCN,zhCN} from 'naive-ui'
import {createDiscreteApi,darkTheme,lightTheme , NIcon, NText,NButton,dateZhCN,zhCN} from 'naive-ui'
import {
AlarmOutline,
AnalyticsOutline,
BarChartSharp, Bonfire, BonfireOutline, EaselSharp,
BarChartSharp, Bonfire, BonfireOutline, DiamondOutline, EaselSharp,
ExpandOutline, Flag,
Flame, FlameSharp, InformationOutline,
Flame, FlameSharp, FlaskOutline, InformationOutline,
LogoGithub,
NewspaperOutline,
NewspaperSharp, Notifications,
@@ -28,8 +29,8 @@ import {
Wallet, WarningOutline,
} from '@vicons/ionicons5'
import {AnalyzeSentiment, GetConfig, GetGroupList,GetVersionInfo} from "../wailsjs/go/main/App";
import {Dragon, Fire, Gripfire} from "@vicons/fa";
import {ReportSearch} from "@vicons/tabler";
import {Dragon, Fire, FirefoxBrowser, Gripfire, Robot} from "@vicons/fa";
import {ReportAnalytics, ReportMoney, ReportSearch} from "@vicons/tabler";
import {LocalFireDepartmentRound} from "@vicons/material";
import {BoxSearch20Regular, CommentNote20Filled} from "@vicons/fluent";
import {FireFilled, FireOutlined, NotificationFilled, StockOutlined} from "@vicons/antd";
@@ -43,14 +44,16 @@ const loadingMsg = ref("加载数据中...")
const enableNews = ref(false)
const contentStyle = ref("")
const enableFund = ref(false)
const enableAgent = ref(false)
const enableDarkTheme = ref(null)
const content = ref('数据来源于网络,仅供参考;投资有风险,入市需谨慎\n\n未经授权,禁止商业目的!')
const content = ref('未经授权,禁止商业目的!\n\n数据来源于网络,仅供参考;投资有风险,入市需谨慎')
const isFullscreen = ref(false)
const activeKey = ref('')
const activeKey = ref('stock')
const containerRef = ref({})
const realtimeProfit = ref(0)
const telegraph = ref([])
const groupList = ref([])
const officialStatement= ref("")
const menuOptions = ref([
{
label: () =>
@@ -64,7 +67,10 @@ const menuOptions = ref([
groupId: 0,
},
params: {},
}
},
onClick: () => {
activeKey.value = 'stock'
},
},
{default: () => '股票自选',}
),
@@ -79,6 +85,7 @@ const menuOptions = ref([
href: '#',
type: 'info',
onClick: () => {
activeKey.value = 'stock'
//console.log("push",item)
router.push({
name: 'stock',
@@ -114,6 +121,7 @@ const menuOptions = ref([
params: {}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '市场快讯'})
},
},
@@ -135,6 +143,7 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '市场快讯'})
},
},
@@ -156,6 +165,7 @@ const menuOptions = ref([
},
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '全球股指'})
},
},
@@ -173,14 +183,15 @@ const menuOptions = ref([
to: {
name: 'market',
query: {
name: "指标行情",
name: "重大指数",
}
},
onClick: () => {
EventsEmit("changeMarketTab", {ID: 0, name: '指标行情'})
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '重大指数'})
},
},
{default: () => '指标行情',}
{default: () => '重大指数',}
),
key: 'market3',
icon: renderIcon(AnalyticsOutline),
@@ -198,6 +209,7 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '行业排名'})
},
},
@@ -219,6 +231,7 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '个股资金流向'})
},
},
@@ -240,6 +253,7 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '龙虎榜'})
},
},
@@ -261,6 +275,7 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '个股研报'})
},
},
@@ -282,6 +297,7 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '公司公告'})
},
},
@@ -303,6 +319,7 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '行业研究'})
},
},
@@ -324,6 +341,7 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '当前热门'})
},
},
@@ -345,6 +363,7 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '指标选股'})
},
},
@@ -353,6 +372,28 @@ const menuOptions = ref([
key: 'market11',
icon: renderIcon(BoxSearch20Regular),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "名站优选",
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '名站优选'})
},
},
{default: () => '名站优选',}
),
key: 'market12',
icon: renderIcon(FirefoxBrowser),
},
]
},
{
@@ -362,8 +403,13 @@ const menuOptions = ref([
{
to: {
name: 'fund',
params: {},
}
query: {
name: '基金自选',
},
},
onClick: () => {
activeKey.value = 'fund'
},
},
{default: () => '基金自选',}
),
@@ -379,6 +425,98 @@ const menuOptions = ref([
},
]
},
{
label: () =>
h(
RouterLink,
{
to: {
name: 'agent',
query: {
name:"Ai智能体",
},
onClick: () => {
activeKey.value = 'agent'
},
}
},
{default: () => 'Ai智能体'}
),
key: 'agent',
show:enableAgent.value,
icon: renderIcon(Robot),
},
{
label: () =>
h(
RouterLink,
{
to: {
name: 'research',
query: {
name:"研究中心",
},
},
onClick: () => {
activeKey.value = 'research'
setTimeout(() => {
EventsEmit("changeResearchTab", {ID: 0, name: 'AI分析报告'})
}, 100)
},
},
{default: () => '研究中心'}
),
key: 'research',
icon: renderIcon(FlaskOutline),
children:[
{
label: () =>
h(
RouterLink,
{
to: {
name: 'research',
query: {
name:"AI分析报告",
},
},
onClick: () => {
activeKey.value = 'research'
setTimeout(() => {
EventsEmit("changeResearchTab", {ID: 0, name: 'AI分析报告'})
}, 100)
},
},
{default: () => 'AI分析报告'}
),
key: 'research1',
icon: renderIcon(ReportAnalytics),
},
{
label: () =>
h(
RouterLink,
{
to: {
name: 'research',
query: {
name:"股票推荐记录",
},
},
onClick: () => {
activeKey.value = 'research'
setTimeout(() => {
EventsEmit("changeResearchTab", {ID: 1, name: '股票推荐记录'})
}, 100)
},
},
{default: () => '股票推荐记录'}
),
key: 'research2',
icon: renderIcon(DiamondOutline),
}
],
},
{
label: () =>
h(
@@ -386,7 +524,12 @@ const menuOptions = ref([
{
to: {
name: 'settings',
params: {}
query: {
name:"设置",
},
onClick: () => {
activeKey.value = 'settings'
},
}
},
{default: () => '设置'}
@@ -401,8 +544,13 @@ const menuOptions = ref([
{
to: {
name: 'about',
params: {}
}
query: {
name:"关于",
}
},
onClick: () => {
activeKey.value = 'about'
},
},
{default: () => '关于'}
),
@@ -410,6 +558,7 @@ const menuOptions = ref([
icon: renderIcon(LogoGithub),
},
{
show:false,
label: () => h("a", {
href: '#',
onClick: toggleFullscreen,
@@ -422,7 +571,7 @@ const menuOptions = ref([
label: () => h("a", {
href: '#',
onClick: WindowHide,
title: '隐藏到托盘区 Ctrl+H',
title: '隐藏到托盘区 Ctrl+Z',
}, {default: () => '隐藏到托盘区'}),
key: 'hide',
icon: renderIcon(ReorderTwoOutline),
@@ -451,6 +600,7 @@ function renderIcon(icon) {
}
function toggleFullscreen(e) {
activeKey.value = 'full'
//console.log(e)
if (isFullscreen.value) {
WindowUnfullscreen()
@@ -521,6 +671,7 @@ onBeforeMount(() => {
GetVersionInfo().then(result => {
if(result.officialStatement){
content.value = result.officialStatement+"\n\n"+content.value
officialStatement.value = result.officialStatement
}
})
@@ -571,11 +722,15 @@ onBeforeMount(() => {
GetConfig().then((res) => {
//console.log(res)
enableFund.value = res.enableFund
enableAgent.value = res.enableAgent
menuOptions.value.filter((item) => {
if (item.key === 'fund') {
item.show = res.enableFund
}
if (item.key === 'agent') {
item.show = res.enableAgent
}
})
if (res.darkTheme) {
@@ -587,12 +742,14 @@ onBeforeMount(() => {
})
onMounted(() => {
WindowSetTitle("go-stockAI赋能股票分析✨ "+officialStatement.value+" 未经授权,禁止商业目的! [数据来源于网络,仅供参考;投资有风险,入市需谨慎]")
contentStyle.value = "max-height: calc(92vh);overflow: hidden"
GetConfig().then((res) => {
if (res.enableNews) {
enableNews.value = true
}
enableFund.value = res.enableFund
enableAgent.value = res.enableAgent
const {notification } =createDiscreteApi(["notification"], {
configProviderProps: {
theme: enableDarkTheme.value ? darkTheme : lightTheme ,
@@ -606,16 +763,24 @@ onMounted(() => {
//type:"error",
// avatar: () => h(NIcon,{component:Notifications,color:"red"}),
title: data.time,
content: () => h(NText,{type:"error"}, { default: () => data.content }),
content: () => h('div',{type:"error",style:{
"text-align":"left",
"font-size":"14px",
"color":"#f67979"
}}, { default: () => data.content }),
meta: () => h(NText,{type:"warning"}, { default: () => data.source}),
duration:1000*40,
})
}else{
notification.create({
notification.create({
//type:"info",
//avatar: () => h(NIcon,{component:Notifications}),
title: data.time,
content: () => h(NText,{type:"info"}, { default: () => data.content }),
content: () => h('div',{type:"info",style:{
"text-align":"left",
"font-size":"14px",
"color": data.source==="go-stock"?"#F98C24":"#549EC8"
}}, { default: () => data.content }),
meta: () => h(NText,{type:"warning"}, { default: () => data.source}),
duration:1000*30 ,
})
@@ -631,7 +796,7 @@ onMounted(() => {
<n-modal-provider>
<n-dialog-provider>
<n-watermark
:content="content"
:content="''"
cross
selectable
:font-size="16"
@@ -662,7 +827,7 @@ onMounted(() => {
</n-spin>
</n-gi>
<n-gi style="position: fixed;bottom:0;z-index: 9;width: 100%;">
<n-card size="small" style="--wails-draggable:drag">
<n-card size="small" style="--wails-draggable:no-drag">
<n-menu style="font-size: 18px;"
v-model:value="activeKey"
mode="horizontal"

View File

@@ -0,0 +1,316 @@
<script setup>
import {AnalyzeSentimentWithFreqWeight,GlobalStockIndexes} from "../../wailsjs/go/main/App";
import * as echarts from "echarts";
import {onMounted,onUnmounted, ref} from "vue";
import _ from "lodash";
const { name,darkTheme,kDays ,chartHeight} = defineProps({
name: {
type: String,
default: ''
},
kDays: {
type: Number,
default: 14
},
chartHeight: {
type: Number,
default: 500
},
darkTheme: {
type: Boolean,
default: false
}
})
const common = ref([])
const america = ref([])
const europe = ref([])
const asia = ref([])
const mainIndex = ref([])
const chinaIndex = ref([])
const other = ref([])
const globalStockIndexes = ref(null)
const chartRef = ref(null);
const gaugeChartRef = ref(null);
const triggerAreas=ref(["main","extra","arrow"])
let handleChartInterval=null
let handleIndexInterval=null
onMounted(() => {
handleChart()
getIndex()
handleChartInterval=setInterval(function () {
handleChart()
}, 1000 * 60)
handleIndexInterval=setInterval(function () {
getIndex()
}, 1000 * 2)
})
onUnmounted(()=>{
clearInterval(handleChartInterval)
clearInterval(handleIndexInterval)
})
function getIndex() {
GlobalStockIndexes().then((res) => {
globalStockIndexes.value = res
common.value = res["common"]
america.value = res["america"]
europe.value = res["europe"]
asia.value = res["asia"]
other.value = res["other"]
mainIndex.value=asia.value.filter(function (item) {
return ['上海',"深圳","香港","台湾","北京","东京","首尔","纽约","纳斯达克"].includes(item.location)
}).concat(america.value.filter(function (item) {
return ['上海',"深圳","香港","台湾","北京","东京","首尔","纽约","纳斯达克"].includes(item.location)
}))
chinaIndex.value=asia.value.filter(function (item) {
return ['上海',"深圳","香港","台湾","北京"].includes(item.location)
})
})
}
function handleChart(){
const formatUtil = echarts.format;
AnalyzeSentimentWithFreqWeight("").then((res) => {
const treemapchart = echarts.init(chartRef.value);
const gaugeChart=echarts.init(gaugeChartRef.value);
let data = res['frequencies'].map(item => ({
name: item.Word,
// value: item.Frequency,
// value: item.Weight,
frequency: item.Frequency,
weight: item.Weight,
value: item.Score,
}));
let data2 = res['frequencies'].map(item => ({
name: item.Word,
value: item.Frequency,
// value: item.Weight,
frequency: item.Frequency,
weight: item.Weight,
//value: item.Score,
}));
let data3 = res['frequencies'].map(item => ({
name: item.Word,
//value: item.Frequency,
value: item.Weight,
frequency: item.Frequency,
weight: item.Weight,
//value: item.Score,
}));
let option = {
darkMode: darkTheme,
title: {
text:name,
left: 'center',
textStyle: {
color: darkTheme?'#ccc':'#456'
}
},
legend: {
show: false
},
toolbox: {
left: '20px',
tooltip:{
textStyle: {
color: darkTheme?'#ccc':'#456'
}
},
feature: {
saveAsImage: {title: '保存图片'},
restore: {
title: '默认',
},
myTool2: {
show: true,
title: '按权重',
icon:"path://M393.8816 148.1216a29.3376 29.3376 0 0 1-15.2576 38.0928c-43.776 17.152-81.92 43.8272-114.2784 76.2368A345.7536 345.7536 0 0 0 159.5392 512 352.8704 352.8704 0 0 0 512 864.4608a351.744 351.744 0 0 0 249.5488-102.912 353.536 353.536 0 0 0 76.2368-114.2784c5.6832-15.2576 22.8352-20.992 38.0928-15.2576 15.2576 5.7344 20.992 22.8864 15.2576 38.0928a421.2224 421.2224 0 0 1-89.6 133.376A412.6208 412.6208 0 0 1 512 921.6c-226.7136 0-409.6-182.8864-409.6-409.6 0-108.544 41.9328-211.456 120.0128-289.5872A421.2224 421.2224 0 0 1 355.84 132.864a29.3376 29.3376 0 0 1 38.0928 15.2576zM512 102.4c226.7136 0 409.6 182.8864 409.6 409.6 0 15.2576-13.312 28.5696-28.5696 28.5696H512A29.2864 29.2864 0 0 1 483.4304 512V130.9696c0-15.2576 13.312-28.5696 28.5696-28.5696z m28.5696 59.0336v321.9968h321.9968a350.976 350.976 0 0 0-321.9968-321.9968z",
onclick: function (){
treemapchart.setOption( {series:{
data: data3
}})
}
},
myTool1: {
show: true,
title: '按频次',
icon:"path://M895.466667 476.8l-87.424-87.424v-123.626667a49.770667 49.770667 0 0 0-49.770667-49.770666h-123.626667L547.2 128.533333a49.792 49.792 0 0 0-70.4 0l-87.424 87.424h-123.626667a49.770667 49.770667 0 0 0-49.770666 49.770667v123.626667L128.533333 476.8a49.792 49.792 0 0 0 0 70.4l87.424 87.424v123.626667a49.770667 49.770667 0 0 0 49.770667 49.770666h123.626667l87.424 87.424a49.792 49.792 0 0 0 70.4 0l87.424-87.424h123.626666a49.770667 49.770667 0 0 0 49.770667-49.770666v-123.626667l87.424-87.424a49.749333 49.749333 0 0 0 0.042667-70.4z m-137.216 137.194667v144.256h-144.256L512 860.266667l-101.994667-101.994667h-144.256v-144.256L163.733333 512l101.994667-101.994667v-144.256h144.256L512 163.733333l101.994667 101.994667h144.256v144.256L860.266667 512l-102.016 101.994667z M414.378667 514.730667l28.672 10.922666c-18.090667 47.445333-38.229333 92.16-60.757334 133.802667l-30.037333-13.653333a1042.133333 1042.133333 0 0 0 62.122667-131.072zM381.952 367.616L355.669333 384c25.258667 26.282667 45.056 50.176 60.074667 72.021333l25.6-17.749333c-13.994667-20.48-33.792-44.032-59.392-70.656zM537.258667 455.338667c-0.682667 43.690667-6.144 79.189333-16.725334 106.837333-14.336 32.768-44.373333 60.416-89.429333 82.944l21.162667 25.941333c52.224-26.624 85.333333-60.074667 99.328-100.693333 1.706667-5.12 3.413333-10.24 4.778666-15.36 21.504 45.738667 52.906667 83.968 93.866667 115.370667l21.504-24.917334c-51.2-34.474667-86.357333-81.237333-105.813333-140.288 1.706667-15.701333 2.730667-32.085333 2.730666-49.834666h-31.402666z M508.586667 434.858667h115.712c-6.826667 25.258667-15.018667 47.786667-24.917334 66.901333l31.744 8.874667a627.008 627.008 0 0 0 27.989334-85.674667v-21.162667H517.12c3.413333-14.336 6.144-29.354667 8.874667-45.738666l-32.426667-5.12c-7.850667 59.392-25.6 105.813333-52.906667 139.264l26.965334 19.114666c16.725333-19.114667 30.378667-44.373333 40.96-76.458666z",
onclick: function (){
treemapchart.setOption( {series:{
data: data2
}})
}
}
}
},
tooltip: {
formatter: function (info) {
var value = info.value.toFixed(2);
var frequency = info.data.frequency;
var weight = info.data.weight;
return [
'<div class="tooltip-title">' + info.name+ '</div>',
'热度: ' + formatUtil.addCommas(value) + '',
'<div class="tooltip-title">频次: ' + formatUtil.addCommas(frequency)+ '</div>',
'<div class="tooltip-title">权重: ' + formatUtil.addCommas(weight)+ '</div>',
].join('');
}
},
series: [
{
type: 'treemap',
breadcrumb:{show: false},
left: '0',
top: '40',
right: '0',
bottom: '0',
tooltip: {
show: true
},
data: data
}
]
};
treemapchart.setOption(option);
let option2 = {
darkMode: darkTheme,
series: [
{
type: 'gauge',
startAngle: 180,
endAngle: 0,
center: ['50%', '75%'],
radius: '90%',
min: -100,
max: 100,
splitNumber: 8,
axisLine: {
lineStyle: {
width: 6,
color: [
// [0.25, '#FF6E76'],
// [0.5, '#FDDD60'],
// [0.75, '#58D9F9'],
// [1, '#7CFFB2'],
[0.25, '#03fb6a'],
[0.5, '#58e1f9'],
[0.75, '#ef5922'],
[1, '#f11d29'],
]
}
},
pointer: {
icon: 'path://M12.8,0.7l12,40.1H0.7L12.8,0.7z',
length: '12%',
width: 20,
offsetCenter: [0, '-60%'],
itemStyle: {
color: 'auto'
}
},
axisTick: {
length: 12,
lineStyle: {
color: 'auto',
width: 2
}
},
splitLine: {
length: 20,
lineStyle: {
color: 'auto',
width: 5
}
},
axisLabel: {
color: darkTheme?'#ccc':'#456',
fontSize: 20,
distance: -45,
rotate: 'tangential',
formatter: function (value) {
if (value ===100) {
return '极热';
} else if (value === 50) {
return '乐观';
} else if (value === 0) {
return '中性';
}else if (value === -50) {
return '谨慎';
} else if (value === -100) {
return '冰点';
}
return '';
}
},
title: {
offsetCenter: [0, '-10%'],
fontSize: 20
},
detail: {
fontSize: 30,
offsetCenter: [0, '-35%'],
valueAnimation: true,
formatter: function (value) {
return value.toFixed(2) + '';
},
color: 'inherit'
},
data: [
{
value: res.result.Score*0.2,
name: '市场情绪强弱'
}
]
}
]
};
gaugeChart.setOption(option2);
})
}
</script>
<template>
<n-collapse :trigger-areas="triggerAreas" :default-expanded-names="['1']" display-directive="show">
<n-collapse-item name="1" >
<template #header>
<n-flex>
<n-tag size="small" :bordered="false" v-for="(item, index) in mainIndex" :type="item.zdf>0?'error':'success'">
<n-flex>
<n-image :width="20" :src="item.img" />
<n-text style="font-size: 14px" :type="item.zdf>0?'error':'success'">{{item.name}}&nbsp;{{item.zxj}}</n-text>
<n-number-animation :precision="2" :from="0" :to="item.zdf" style="font-size: 14px"/>
<n-text style="margin-left: -12px;font-size: 14px" :type="item.zdf>0?'error':'success'">%</n-text>
</n-flex>
</n-tag>
</n-flex>
</template>
<template #header-extra>
主要股指
</template>
<n-grid :cols="24" :y-gap="0">
<n-gi span="6">
<div ref="gaugeChartRef" style="width: 100%;height: auto;--wails-draggable:no-drag" :style="{height:chartHeight+'px'}" ></div>
</n-gi>
<n-gi span="18">
<div ref="chartRef" style="width: 100%;height: auto;--wails-draggable:no-drag" :style="{height:chartHeight+'px'}" ></div>
</n-gi>
</n-grid>
</n-collapse-item>
</n-collapse>
</template>
<style scoped>
</style>

View File

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

@@ -59,7 +59,7 @@ function getMarketCode(item) {
<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>
<k-line-chart style="width: 800px" :code="getMarketCode(item)" :chart-height="500" :stockName="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>

View File

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

View File

@@ -4,12 +4,12 @@ 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({
const { code,stockName,darkTheme,kDays ,chartHeight} = defineProps({
code: {
type: String,
default: ''
},
name: {
stockName: {
type: String,
default: ''
},
@@ -33,13 +33,15 @@ const downBorderColor = '';
const kLineChartRef = ref(null);
onMounted(() => {
handleKLine(code,name)
handleKLine(code,stockName)
})
function handleKLine(code,name){
GetStockKLine(code,name,365).then(result => {
function handleKLine(code,stockName){
console.log("handleKLine",code,stockName)
const chart = echarts.init(kLineChartRef.value);
chart.showLoading()
GetStockKLine(code,stockName,365).then(result => {
//console.log("GetStockKLine",result)
const chart = echarts.init(kLineChartRef.value);
const categoryData = [];
const values = [];
const volumns=[];
@@ -47,24 +49,28 @@ function handleKLine(code,name){
let resultElement=result[i]
//console.log("resultElement:{}",resultElement)
categoryData.push(resultElement.day)
let flag=resultElement.close>resultElement.open?1:-1
let flag=Number(resultElement.close)>Number(resultElement.open)?1:-1
if(i>0){
flag=Number(resultElement.close)>Number(result[i-1].close)?1:-1
}
values.push([
resultElement.open,
resultElement.close,
resultElement.low,
resultElement.high
Number(resultElement.open),
Number(resultElement.close),
Number(resultElement.low),
Number(resultElement.high)
])
volumns.push([i,resultElement.volume/10000,flag])
volumns.push([i,Number(resultElement.volume)/10000,flag])
}
////console.log("categoryData",categoryData)
////console.log("values",values)
let option = {
title: {
text: name+" "+code,
left: '20px',
text: stockName+" "+categoryData[values.length-1]+" "+values[values.length-1][1]+" "+((values[values.length-1][1]-values[values.length-2][1])/values[values.length-2][1]*100).toFixed(2)+"%",
left: '0px',
textStyle: {
color: darkTheme?'#ccc':'#456'
}
color: Number(values[values.length-1][1])>Number(values[values.length-2][1])?'red':'green',
fontSize: 14
},
},
darkMode: darkTheme,
//backgroundColor: '#1c1c1c',
@@ -150,7 +156,7 @@ function handleKLine(code,name){
left: '8%',
right: '8%',
top: '66%',
height: '15%'
height: '18%'
}
],
xAxis: [
@@ -184,7 +190,11 @@ function handleKLine(code,name){
scale: true,
splitArea: {
show: true
}
},
axisLabel: { show: true },
axisLine: { show: true },
axisTick: { show: true },
splitLine: { show: false }
},
{
scale: true,
@@ -354,10 +364,7 @@ function handleKLine(code,name){
]
};
chart.setOption(option);
chart.on('click',{seriesName:'日K'}, function(params) {
//console.log("click:",params);
});
chart.hideLoading()
})
}
function calculateMA(dayCount,values) {
@@ -378,7 +385,7 @@ function calculateMA(dayCount,values) {
</script>
<template>
<div ref="kLineChartRef" style="width: 100%;height: auto;" :style="{height:chartHeight+'px'}"></div>
<div ref="kLineChartRef" style="width: 100%;height: auto;--wails-draggable:no-drag" :style="{height:chartHeight+'px'}" ></div>
</template>
<style scoped>

View File

@@ -168,7 +168,7 @@ function handleEXPLANATION(value, option){
<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>
<k-line-chart style="width: 800px" :code="item.SECUCODE.split('.')[1].toLowerCase()+item.SECUCODE.split('.')[0]" :chart-height="500" :stockName="item.SECURITY_NAME_ABBR" :k-days="20" :dark-theme="true"></k-line-chart>
</n-popover>
</n-td>
<n-td>

View File

@@ -1,14 +1,46 @@
<script setup lang="ts">
import {h, onBeforeMount, onMounted, onUnmounted, ref} from 'vue'
import {SearchStock} from "../../wailsjs/go/main/App";
import {useMessage, NText, NTag} from 'naive-ui'
import {SearchStock, GetHotStrategy, OpenURL, Follow, GetFollowList} from "../../wailsjs/go/main/App";
import {useMessage, NText, NTag, NButton} from 'naive-ui'
import {Environment} from "../../wailsjs/runtime"
import {RefreshCircleSharp} from "@vicons/ionicons5";
import {EventsEmit} from "../../wailsjs/runtime";
const message = useMessage()
const search = ref('')
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){
if (!search.value) {
message.warning('请输入选股指标或者要求')
return
}
@@ -16,82 +48,213 @@ function Search() {
const loading = message.loading("正在获取选股数据...", {duration: 0});
SearchStock(search.value).then(res => {
loading.destroy()
console.log(res)
if(res.code==100){
traceInfo.value=res.data.traceInfo.showText
message.success(res.msg)
columns.value=res.data.result.columns.filter(item=>!item.hiddenNeed&&(item.title!="市场码"&&item.title!="市场简称")).map(item=>{
if(item.children){
// console.log(res)
if (res.code == 100) {
traceInfo.value = res.data.traceInfo.showText
// message.success(res.msg)
columns.value = res.data.result.columns.filter(item => !item.hiddenNeed && (item.title != "市场码" && item.title != "市场简称")).map(item => {
if (item.children) {
return {
title:item.title+(item.unit?'['+item.unit+']':''),
key:item.key,
title: item.title + (item.unit ? '[' + item.unit + ']' : ''),
key: item.key,
resizable: true,
minWidth:200,
minWidth: 200,
ellipsis: {
tooltip: true
},
children:item.children.filter(item=>!item.hiddenNeed).map(item=>{
children: item.children.filter(item => !item.hiddenNeed).map(item => {
return {
title:item.dateMsg,
key:item.key,
minWidth:100,
title: item.dateMsg,
key: item.key,
minWidth: 100,
resizable: true,
ellipsis: {
tooltip: true
}
},
sorter: (row1, row2) => {
if (isNumeric(row1[item.key]) && isNumeric(row2[item.key])) {
return row1[item.key] - row2[item.key];
} else {
return 'default'
}
},
}
})
}
}else{
} else {
return {
title:item.title+(item.unit?'['+item.unit+']':''),
key:item.key,
title: item.title + (item.unit ? '[' + item.unit + ']' : ''),
key: item.key,
resizable: true,
minWidth:100,
minWidth: 120,
ellipsis: {
tooltip: true
}
},
sorter: (row1, row2) => {
if (isNumeric(row1[item.key]) && isNumeric(row2[item.key])) {
return row1[item.key] - row2[item.key];
} else {
return 'default'
}
},
}
}
})
dataList.value=res.data.result.dataList
}else {
message.error(res.msg)
}
columns.value.push({
title: '操作',
key: 'actions',
width: 80,
fixed: 'right', // 固定在右侧
render: (row) => {
return h(
NButton,
{
strong: true,
tertiary: true,
size: 'small',
type: 'warning', // 橙色按钮
style: 'font-size: 14px; padding: 0 10px;', // 稍微大一点的按钮
onClick: () => handleFollow(row)
},
{ default: () => '关注' }
)
}
});
dataList.value = res.data.result.dataList
console.log("sss"+columns.value. length)
// 计算并设置表格宽度
tableScrollX.value = calculateTableWidth(columns.value);
} else {
if(res.msg){
message.error(res.msg)
}
if(res.message){
message.error(res.message)
}
}
}).catch(err => {
message.error(err)
})
}
// 修改handleFollow方法使用stock.vue的AddStock逻辑
function handleFollow(row) {
let code=row.MARKET_SHORT_NAME.toLowerCase()+row.SECURITY_CODE
Follow(code).then(result => {
if (result === "关注成功") {
message.success(result)
} else {
message.error(result)
}
});
}
function isNumeric(value) {
return !isNaN(parseFloat(value)) && isFinite(value);
}
onBeforeMount(() => {
Search()
GetHotStrategy().then(res => {
console.log(res)
if (res.code == 1) {
hotStrategy.value = res.data
search.value = hotStrategy.value[0].question
Search()
}
}).catch(err => {
message.error(err)
})
})
function DoSearch(question) {
search.value = question
Search()
}
function openCenteredWindow(url, width, height) {
const left = (window.screen.width - width) / 2;
const top = (window.screen.height - height) / 2;
Environment().then(env => {
switch (env.platform) {
case 'windows':
window.open(
url,
'centeredWindow',
`width=${width},height=${height},left=${left},top=${top},location=no,menubar=no,toolbar=no,display=standalone`
)
break
default:
OpenURL(url)
}
})
}
</script>
<template>
<n-flex>
<n-input-group>
<n-input 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">
<n-tag type="info" :bordered="false">当前选股条件<n-tag type="warning" :bordered="true">{{traceInfo}}</n-tag></n-tag>
<!-- <n-button type="primary" size="small">保存策略</n-button>-->
</n-flex>
<n-data-table
:max-height="'calc(100vh - 312px)'"
size="small"
:columns="columns"
:data="dataList"
:pagination="false"
:scroll-x="1800"
:render-cell="(value, rowData, column) => {
<n-grid :cols="24" style="max-height: calc(100vh - 165px)">
<n-gi :span="4">
<n-list bordered style="text-align: left;" hoverable clickable>
<n-scrollbar style="max-height: calc(100vh - 170px);">
<n-list-item v-for="item in hotStrategy" :key="item.rank" @click="DoSearch(item.question)">
<n-ellipsis line-clamp="1" :tooltip="true">
<n-tag size="small" :bordered="false" type="info">#{{ item.rank }}</n-tag>
<n-text type="warning">{{ item.question }}</n-text>
<template #tooltip>
<div style="text-align: center;max-width: 180px">
<n-text type="warning">{{ item.question }}</n-text>
</div>
</template>
</n-ellipsis>
</n-list-item>
</n-scrollbar>
</n-list>
<!-- <n-virtual-list :items="hotStrategy" :item-size="hotStrategy.length">-->
<!-- <template #default="{ item, index }">-->
<!-- <n-card :title="''" size="small">-->
<!-- <template #header-extra>-->
<!-- {{item.rank}}-->
<!-- </template>-->
<!-- <n-ellipsis expand-trigger="click" line-clamp="3" :tooltip="false" >-->
<!-- <n-text type="warning">{{item.question }}</n-text>-->
<!-- </n-ellipsis>-->
<!-- </n-card>-->
<!-- </template>-->
<!-- </n-virtual-list>-->
</n-gi>
<n-gi :span="20">
<n-flex style="--wails-draggable:no-drag">
<n-input-group style="text-align: left">
<n-input :rows="1" clearable v-model:value="search" placeholder="请输入选股指标或者要求"/>
<n-button type="primary" @click="Search">搜索A股</n-button>
</n-input-group>
</n-flex>
<n-flex justify="start" v-if="traceInfo" style="margin: 5px 0;--wails-draggable:no-drag">
<n-ellipsis line-clamp="1" :tooltip="true">
<n-text type="info" :bordered="false">选股条件</n-text>
<n-text type="warning" :bordered="true">{{ traceInfo }}</n-text>
<template #tooltip>
<div style="text-align: center;max-width: 580px">
<n-text type="warning">{{ traceInfo }}</n-text>
</div>
</template>
</n-ellipsis>
<!-- <n-button type="primary" size="small">保存策略</n-button>-->
</n-flex>
<n-data-table
:striped="true"
:max-height="'calc(100vh - 150px)'"
size="medium"
:columns="columns"
:data="dataList"
:pagination="{pageSize: 10}"
:scroll-x="tableScrollX"
:render-cell="(value, rowData, column) => {
if(column.key=='SECURITY_CODE'||column.key=='SERIAL'){
return h(NText, { type: 'info',border: false }, { default: () => `${value}` })
@@ -110,13 +273,24 @@ onBeforeMount(() => {
return h(NText, { type: type }, { default: () => `${value}` })
}else{
if(column.key=='SECURITY_SHORT_NAME'){
return h(NTag, { type: 'info',bordered: false }, { default: () => `${value}` })
return h(NButton, { type: 'info',bordered: false ,size:'small',onClick:()=>{
//https://quote.eastmoney.com/sz300558.html#fullScreenChart
openCenteredWindow(`https://quote.eastmoney.com/${rowData.MARKET_SHORT_NAME}${rowData.SECURITY_CODE}.html#fullScreenChart`,1240,700)
}}, { default: () => `${value}` })
}else{
return h(NText, { type: 'info' }, { default: () => `${value}` })
}
}
}"
/>
/>
<div style="margin-top: -25px">共找到
<n-tag type="info" :bordered="false">{{ dataList.length }}</n-tag>
只股
</div>
</n-gi>
</n-grid>
</template>
<style scoped>

View File

@@ -123,7 +123,7 @@ function getmMarketCode(market,code) {
<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>
<k-line-chart style="width: 800px" :code="getmMarketCode(item.codes[0].market_code,item.codes[0].stock_code)" :chart-height="500" :stockName="item.codes[0].short_name" :k-days="20" :dark-theme="true"></k-line-chart>
</n-popover>
</n-td>
<n-td>

View File

@@ -111,7 +111,7 @@ function handleSearch(value) {
<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>
<k-line-chart style="width: 800px" :code="getmMarketCode(item.market,item.stockCode)" :chart-height="500" :stockName="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>

View File

@@ -3,15 +3,22 @@
// preview.css相比style.css少了编辑器那部分样式
import 'md-editor-v3/lib/preview.css';
import {h, onBeforeUnmount, onMounted, ref} from 'vue';
import {CheckUpdate, GetVersionInfo} from "../../wailsjs/go/main/App";
import {EventsOff, EventsOn} from "../../wailsjs/runtime";
import {NAvatar, NButton, useNotification} from "naive-ui";
import {CheckUpdate, GetVersionInfo,GetSponsorInfo,OpenURL} from "../../wailsjs/go/main/App";
import {EventsOff, EventsOn,Environment} from "../../wailsjs/runtime";
import {NAvatar, NButton, useNotification,NText} from "naive-ui";
import { addMonths, format ,parse} from 'date-fns';
import { zhCN } from 'date-fns/locale';
const updateLog = ref('');
const versionInfo = ref('');
const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/master/build/appicon.png');
const alipay =ref('https://github.com/ArvinLovegood/go-stock/raw/master/build/screenshot/alipay.jpg')
const wxpay =ref('https://github.com/ArvinLovegood/go-stock/raw/master/build/screenshot/wxpay.jpg')
const wxgzh =ref('https://github.com/ArvinLovegood/go-stock/raw/dev/build/screenshot/%E6%89%AB%E7%A0%81_%E6%90%9C%E7%B4%A2%E8%81%94%E5%90%88%E4%BC%A0%E6%92%AD%E6%A0%B7%E5%BC%8F-%E7%99%BD%E8%89%B2%E7%89%88.png')
const notify = useNotification()
const vipLevel=ref("");
const vipStartTime=ref("");
const vipEndTime=ref("");
const expired=ref(false)
onMounted(() => {
document.title = '关于软件';
@@ -21,7 +28,25 @@ onMounted(() => {
icon.value = res.icon;
alipay.value=res.alipay;
wxpay.value=res.wxpay;
wxgzh.value=res.wxgzh;
GetSponsorInfo().then((res) => {
vipLevel.value = res.vipLevel;
vipStartTime.value = res.vipStartTime;
vipEndTime.value = res.vipEndTime;
//判断时间是否到期
if (res.vipLevel) {
if (res.vipEndTime < format(new Date(), 'yyyy-MM-dd HH:mm:ss')) {
notify.warning({content: 'VIP已到期'})
expired.value = true;
}
}
})
});
})
onBeforeUnmount(() => {
notify.destroyAll()
@@ -70,7 +95,16 @@ EventsOn("updateVersion",async (msg) => {
type: 'primary',
size: 'small',
onClick: () => {
window.open(msg.html_url)
Environment().then(env => {
switch (env.platform) {
case 'windows':
window.open(msg.html_url)
break
default :
OpenURL(msg.html_url)
break
}
})
}
}, { default: () => '查看' })
}
@@ -80,21 +114,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="[80,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="[70,10]" type="success">
<n-gradient-text :type="expired?'error':'warning'" :size="50" >go-stock</n-gradient-text><n-tag :bordered="false" size="small" type="warning">VIP{{vipLevel}}</n-tag>
</n-badge>
</h1>
<n-button size="tiny" @click="CheckUpdate" type="info" tertiary >检查更新</n-button>
<n-gradient-text :type="expired?'error':'warning'" v-if="vipLevel" >vip到期时间{{vipEndTime}}</n-gradient-text>
<n-button size="tiny" @click="CheckUpdate(1)" type="info" tertiary >检查更新</n-button>
<div style="justify-self: center;text-align: left" >
<p>自选股行情实时监控基于Wails和NaiveUI构建的AI赋能股票分析工具</p>
<p>目前已支持A股港股美股未来计划加入基金ETF等支持</p>
@@ -113,14 +148,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全部功能,启动时自动同步最近24小时市场资讯(包括外媒简讯) 💕</n-td>
</n-tr>
<n-tr>
<n-td>每月赞助 X RMB</n-td><n-td>vipX</n-td><n-td>🧩 更多计划视go-stock开源项目发展情况而定...(承接GitHub项目README广告推广💖)</n-td>
</n-tr>
</n-tbody>
</n-table>
</n-flex>
<n-divider title-placement="center">关于作者</n-divider>
<n-space vertical>
<!-- <h1>关于作者</h1>-->
<n-avatar width="100" src="https://avatars.githubusercontent.com/u/7401917?v=4" />
<h2><a href="https://github.com/ArvinLovegood" target="_blank">@ArvinLovegood</a></h2>
<p>一个热爱编程的小白欢迎关注我的Github</p>
<n-image width="300" src="https://go-stock.sparkmemory.top/assets/%E6%89%AB%E7%A0%81_%E6%90%9C%E7%B4%A2%E8%81%94%E5%90%88%E4%BC%A0%E6%92%AD%E6%A0%B7%E5%BC%8F-%E7%99%BD%E8%89%B2%E7%89%88-DEJtWc_y.png" />
<p>一个热爱编程的小白欢迎关注我的Github/微信公众号</p>
<n-image width="300" :src="wxgzh" />
<p>开源不易如果觉得好用可以请作者喝杯咖啡</p>
<n-flex justify="center">
<n-image width="200" :src="alipay" />
@@ -135,6 +195,7 @@ EventsOn("updateVersion",async (msg) => {
</p>
<p>
感谢以下开发者
<a href="https://github.com/GiCo001" target="_blank">@Gico</a><n-divider vertical />
<a href="https://github.com/CodeNoobLH" target="_blank">浓睡不消残酒</a><n-divider vertical />
<a href="https://github.com/gnim2600" target="_blank">@gnim2600</a><n-divider vertical />
<a href="https://github.com/XXXiaohuayanGGG" target="_blank">@XXXiaohuayanGGG</a><n-divider vertical />

View File

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

View File

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

View File

@@ -0,0 +1,405 @@
<script setup>
import {computed, h, onBeforeMount, onBeforeUnmount, onMounted,onUnmounted, ref,reactive} from 'vue'
import {
GetAiRecommendStocksList,
GetConfig,
GetSponsorInfo,
SaveAsMarkdown,
ShareAnalysis
} from "../../wailsjs/go/main/App";
import {NAvatar, NButton, NEllipsis, NTag, NText, useMessage, useNotification} from "naive-ui";
import KLineChart from "./KLineChart.vue";
import {format} from "date-fns";
const notify = useNotification()
const vipLevel=ref("");
const vipStartTime=ref("");
const vipEndTime=ref("");
const expired=ref(false)
const isValidVip=ref(false) // 是否是会员
onBeforeMount(()=> {
GetConfig().then(result => {
if (result.darkTheme) {
editorDataRef.darkTheme = true
}
})
GetSponsorInfo().then((res) => {
// console.log(res)
vipLevel.value = res.vipLevel;
vipStartTime.value = res.vipStartTime;
vipEndTime.value = res.vipEndTime;
//判断时间是否到期
if (res.vipLevel) {
if (res.vipEndTime < format(new Date(), 'yyyy-MM-dd HH:mm:ss')) {
//notify.warning({content: 'VIP已到期'})
expired.value = true;
}
}else{
//notify.success({content: '未开通VIP'})
}
isValidVip.value = !(vipLevel.value === "" || Number(vipLevel.value) <= 0);
})
})
onMounted(() => {
query({
page: 1,
pageSize: paginationReactive.pageSize,
order: "desc",
keyword: paginationReactive.keyword,
startDate: paginationReactive.range[0],
endDate: paginationReactive.range[1]
}).then((data) => {
console.log( data)
dataRef.value = data.data
paginationReactive.page = 1
paginationReactive.pageCount = data.pageCount
paginationReactive.itemCount = data.total
loadingRef.value = false
})
})
const message = useMessage()
const mdPreviewRef = ref(null)
const mdEditorRef = ref(null)
const editorDataRef = reactive({
show: false,
loading: false,
darkTheme: false,
chatId: "",
modelName: "",
CreatedAt: "",
stockName: "",
stockCode: "",
question: "",
content: "",
})
const dataRef = ref([])
const loadingRef = ref(true)
// StockClosePrice string `json:"StockClosePrice" md:"推荐时股票收盘价格"`
// StockPrePrice string `json:"stockPrePricePrice" md:"前一交易日股票价格"`
// RecommendReason string `json:"recommendReason" md:"推荐理由/驱动因素/逻辑"`
// RecommendBuyPrice string `json:"recommendBuyPrice" md:"ai建议买入价"`
// RecommendStopProfitPrice string `json:"recommendStopProfitPrice" md:"ai建议止盈价"`
// RecommendStopLossPrice string `json:"recommendStopLossPrice" md:"ai建议止损价"`
// RiskRemarks string `json:"riskRemarks" md:"风险提示"`
// Remarks string `json:"remarks" md:"备注"`
const columnsRef = ref([
{
title: '推荐时间',
key: 'dataTime',
render(row, index) {
//2026-01-14T22:13:27.2693252+08:00 格式化为常用时间格式
return row.CreatedAt.substring(0, 19).replace('T', ' ')
}
},
{
title: '板块概念',
key: 'bkName'
},
{
title: '股票名称',
key: 'stockName',
render(row, index) {
return h(NText, { type: "info" }, { default: () => row.stockName })
}
},
{
title: '股票代码',
key: 'stockCode'
},
{
title: '最新',
key: 'stockCurrentPrice',
minWidth: 120,
render(row, index) {
let diff = ((Number(row.stockCurrentPrice) - Number(row.stockPrePrice))/ Number(row.stockPrePrice)*100).toFixed(2)
if(Number(row.stockCurrentPrice)< Number(row.stockPrePrice)) {
return [h(NText, { type: "success", bordered: false }, { default: () => row.stockCurrentPrice+` | ${diff}%` })]
} else {
return [h(NText, { type: "error" , bordered: false}, { default: () => row.stockCurrentPrice+` | ${diff}%` })]
}
}
},
{
title: '推荐时',
key: 'stockPrice',
render(row, index) {
if(vipLevel.value===""|| Number(vipLevel.value) <=0){
return h(NText, { type: "info" }, { default: () => row.stockPrice })
}
let diff = ((Number(row.stockCurrentPrice) - Number(row.stockPrice))/ Number(row.stockPrice)*100).toFixed(2)
let flagStr="暂平"
let flag="info"
if(Number(row.stockCurrentPrice)>Number(row.stockPrice)) {
flagStr="暂赢 "+diff+"%"
flag="error"
}else if(Number(row.stockCurrentPrice)===Number(row.stockPrice)){
flagStr="暂平"
flag="info"
}else{
flagStr="暂亏 "+ diff+"%"
flag="success"
}
return [h(NText, { type: "info" }, { default: () => row.stockPrice }),h(NTag, { type: flag,size: "tiny", bordered: false }, { default: () => flagStr })]
}
},
{
title: '昨收',
key: 'stockPrePrice',
render(row, index) {
return h(NText, { type: "info" }, { default: () => row.stockPrePrice })
}
},
{
title: 'ai建议买入价',
key: 'recommendBuyPrice',
render(row, index) {
if(vipLevel.value===""|| Number(vipLevel.value) <=0){
return h(NText, { type: "info" }, { default: () => row.recommendBuyPrice })
}
if(row.recommendBuyPrice.includes("-")){
let prices= row.recommendBuyPrice.split("-")
if(Number(row.stockCurrentPrice)>=Number(prices[0])&&Number(row.stockCurrentPrice)<=Number(prices[1])){
return [h(NText, { type: "success" }, { default: () => row.recommendBuyPrice }),h(NTag, { type: "error", size: "tiny", bordered: false }, { default: () => "Buy" })]
}
}
return h(NText, { type: "info" }, { default: () => row.recommendBuyPrice })
}
},
{
title: 'ai建议止盈价',
key: 'recommendStopProfitPrice'
},
{
title: 'ai建议止损价',
key: 'recommendStopLossPrice'
},
{
title: '推荐理由',
key: 'recommendReason',
ellipsis: {
tooltip: isValidVip
}
},
{
title: '风险提示',
key: 'riskRemarks',
ellipsis: {
tooltip: isValidVip
}
},
{
title: '备注',
key: 'remarks',
ellipsis: {
tooltip: isValidVip
}
},
{
title: '操作',
render(row, index) {
return h(
NButton,
{
strong: true,
tertiary: true,
size: 'small',
type: 'warning', // 橙色按钮
style: 'font-size: 14px; padding: 0 10px;', // 稍微大一点的按钮
onClick: () => rowProps(row)
},
{ default: () => '查看详细' }
)
}
},
])
const paginationReactive = reactive({
page: 1,
pageCount: 1,
pageSize: 12,
itemCount: 0,
keyword: "",
range: [new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000), new Date(new Date().getTime() + 24 * 60 * 60 * 1000)],
prefix({ itemCount }) {
return `${itemCount} 条记录`
}
})
const modalDataRef = reactive({
visible: false,
title: "",
content: "",
riskRemarks: "",
stockCode: "",
stockName: "",
remarks: "",
})
const theme = computed(() => {
return editorDataRef.darkTheme ? 'dark' : 'light'
})
function query({
page,
pageSize = 10,
order = 'desc',
keyword = "",
startDate = "",
endDate = ""
}) {
return new Promise((resolve) => {
GetAiRecommendStocksList({
"page": page,
"pageSize": pageSize,
"modelName":keyword,
"stockName":keyword,
"stockCode":keyword,
"bkName":keyword,
"startDate": startDate,
"endDate": endDate
}).then((res) => {
const pagedData =res.list
const total = res.total
const pageCount =res.totalPages
resolve({
pageCount,
data: pagedData,
total
})
})
})
}
function handlePageChange(currentPage) {
if (!loadingRef.value) {
loadingRef.value = true
query({
page: currentPage,
pageSize: paginationReactive.pageSize,
order: "desc",
keyword: paginationReactive.keyword,
startDate: formatDate(paginationReactive.range[0]), // Format date to string
endDate: formatDate(paginationReactive.range[1]) // Format date to string
}).then((data) => {
dataRef.value = data.data
paginationReactive.page = currentPage
paginationReactive.pageCount = data.pageCount
paginationReactive.itemCount = data.total
loadingRef.value = false
})
}
}
function handleSearch() {
if (!loadingRef.value) {
loadingRef.value = true
query({
page: 1,
pageSize: paginationReactive.pageSize,
order: "desc",
keyword: paginationReactive.keyword,
startDate: formatDate(paginationReactive.range[0]),
endDate: formatDate(paginationReactive.range[1])
}).then((data) => {
dataRef.value = data.data
paginationReactive.page = 1
paginationReactive.pageCount = data.pageCount
paginationReactive.itemCount = data.total
loadingRef.value = false
})
}
}
function formatDate(dateString) {
const date = new Date(dateString)
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')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
function getStockCode(stockCode) {
if(stockCode.indexOf( ".")>0){
stockCode=stockCode.split(".")[1]+stockCode.split(".")[0]
}
//转化为小写
stockCode=stockCode.toLowerCase()
return stockCode
}
function rowProps(row) {
return {
style: 'cursor: pointer;',
onClick: () => {
if(vipLevel.value===""|| Number(vipLevel.value) <=0){
notify.warning({content: '未开通VIP或者已经过期'})
return
}
//message.info(row.stockName)
modalDataRef.title = row.stockName
modalDataRef.content = row.recommendReason
modalDataRef.riskRemarks = row.riskRemarks
modalDataRef.stockCode = getStockCode(row.stockCode)
modalDataRef.stockName = row.stockName
modalDataRef.visible = true
modalDataRef.remarks = row.remarks
}
}
}
</script>
<template>
<n-input-group>
<n-date-picker v-model:value="paginationReactive.range" type="datetimerange" style="width: 50%"/>
<n-input clearable placeholder="输入关键词搜索" v-model:value="paginationReactive.keyword"/>
<n-button type="primary" ghost @click="handleSearch" @input="handleSearch">
搜索
</n-button>
</n-input-group>
<n-data-table
remote
:row-props="rowProps"
size="small"
:columns="columnsRef"
:data="dataRef"
:loading="loadingRef"
:pagination="paginationReactive"
:row-key="(rowData)=>rowData.ID"
@update:page="handlePageChange"
flex-height
style="height: calc(100vh - 210px);margin-top: 10px"
/>
<n-modal v-model:show="modalDataRef.visible" :title="modalDataRef.title" preset="card" style="width: 850px;">
<n-gradient-text :size="16" type="warning">{{modalDataRef.remarks}}</n-gradient-text>
<n-card size="small">
<KLineChart style="width: 800px" :code="getStockCode(modalDataRef.stockCode)" :chart-height="500" :stock-name="modalDataRef.stockName" :k-days="30" :dark-theme="editorDataRef.darkTheme"></KLineChart>
</n-card>
<n-card size="small">
<n-text type="info">{{modalDataRef.content}}</n-text>
<n-divider><n-gradient-text type="error">风险提示</n-gradient-text></n-divider>
<n-text type="error">{{modalDataRef.riskRemarks}}</n-text>
</n-card>
</n-modal>
</template>
<style scoped>
</style>

View File

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

View File

@@ -1,5 +1,6 @@
<script setup>
import {computed, h, onBeforeMount, onBeforeUnmount, ref} from 'vue'
import * as echarts from "echarts";
import {computed, h, onBeforeMount, onBeforeUnmount, onMounted,onUnmounted, ref} from 'vue'
import {
GetAIResponseResult,
GetConfig,
@@ -11,7 +12,8 @@ import {
SaveAIResponseResult,
SaveAsMarkdown,
ShareAnalysis,
SummaryStockNews
SummaryStockNews,
GetAiConfigs,
} from "../../wailsjs/go/main/App";
import {EventsOff, EventsOn} from "../../wailsjs/runtime";
import NewsList from "./newsList.vue";
@@ -32,6 +34,7 @@ import HotTopics from "./HotTopics.vue";
import InvestCalendarTimeLine from "./InvestCalendarTimeLine.vue";
import ClsCalendarTimeLine from "./ClsCalendarTimeLine.vue";
import SelectStock from "./SelectStock.vue";
import Stockhotmap from "./stockhotmap.vue";
const route = useRoute()
const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/master/build/appicon.png');
@@ -42,6 +45,7 @@ const panelHeight = ref(window.innerHeight - 240)
const telegraphList = ref([])
const sinaNewsList = ref([])
const foreignNewsList = ref([])
const common = ref([])
const america = ref([])
const europe = ref([])
@@ -51,6 +55,7 @@ const globalStockIndexes = ref(null)
const summaryModal = ref(false)
const summaryBTN = ref(true)
const darkTheme = ref(false)
const httpProxyEnabled = ref(false)
const theme = computed(() => {
return darkTheme ? 'dark' : 'light'
})
@@ -59,8 +64,10 @@ const aiSummaryTime = ref("")
const modelName = ref("")
const chatId = ref("")
const question = ref(``)
const sysPromptId = ref(0)
const aiConfigId = ref(null)
const sysPromptId = ref(null)
const loading = ref(true)
const aiConfigs = ref([])
const sysPromptOptions = ref([])
const userPromptOptions = ref([])
const promptTemplates = ref([])
@@ -70,6 +77,10 @@ const nowTab = ref("市场快讯")
const indexInterval = ref(null)
const indexIndustryRank = ref(null)
const stockCode= ref('')
const enableTools= ref(true)
const thinkingMode = ref(true)
const treemapRef = ref(null);
let treemapchart =null;
function getIndex() {
GlobalStockIndexes().then((res) => {
@@ -82,14 +93,13 @@ function getIndex() {
})
}
onBeforeMount(() => {
nowTab.value = route.query.name
stockCode.value = route.query.stockCode
GetConfig().then(result => {
summaryBTN.value = result.openAiEnable
darkTheme.value = result.darkTheme
httpProxyEnabled.value = result.httpProxyEnabled
})
GetPromptTemplates("", "").then(res => {
promptTemplates.value = res
@@ -97,12 +107,19 @@ onBeforeMount(() => {
userPromptOptions.value = promptTemplates.value.filter(item => item.type === '模型用户Prompt')
})
GetAiConfigs().then(res=>{
aiConfigs.value = res
aiConfigId.value = res[0].ID
})
GetTelegraphList("财联社电报").then((res) => {
telegraphList.value = res
})
GetTelegraphList("新浪财经").then((res) => {
sinaNewsList.value = res
})
GetTelegraphList("外媒").then((res) => {
foreignNewsList.value = res
})
getIndex();
industryRank();
indexInterval.value = setInterval(() => {
@@ -111,8 +128,16 @@ onBeforeMount(() => {
indexIndustryRank.value = setInterval(() => {
industryRank()
ReFlesh("财联社电报")
ReFlesh("新浪财经")
ReFlesh("外媒")
}, 1000 * 10)
})
onMounted(() => {
})
onBeforeUnmount(() => {
EventsOff("changeMarketTab")
@@ -123,22 +148,38 @@ onBeforeUnmount(() => {
clearInterval(indexIndustryRank.value)
})
onUnmounted(() => {
});
EventsOn("changeMarketTab", async (msg) => {
//message.info(msg.name)
console.log(msg.name)
updateTab(msg.name)
})
EventsOn("newTelegraph", (data) => {
for (let i = 0; i < data.length; i++) {
telegraphList.value.pop()
if (data!=null) {
for (let i = 0; i < data.length; i++) {
telegraphList.value.pop()
}
telegraphList.value.unshift(...data)
}
telegraphList.value.unshift(...data)
})
EventsOn("newSinaNews", (data) => {
if (data!=null) {
for (let i = 0; i < data.length; i++) {
sinaNewsList.value.pop()
}
sinaNewsList.value.unshift(...data)
}
})
EventsOn("tradingViewNews", (data) => {
if (data!=null) {
for (let i = 0; i < data.length; i++) {
foreignNewsList.value.pop()
}
foreignNewsList.value.unshift(...data)
}
})
//获取页面高度
@@ -186,13 +227,14 @@ function reAiSummary() {
aiSummary.value = ""
summaryModal.value = true
loading.value = true
SummaryStockNews(question.value, sysPromptId.value)
SummaryStockNews(question.value,aiConfigId.value, sysPromptId.value,enableTools.value,thinkingMode.value)
}
function getAiSummary() {
summaryModal.value = true
loading.value = true
GetAIResponseResult("市场资讯").then(result => {
loading.value = false
if (result.content) {
aiSummary.value = result.content
question.value = result.question
@@ -211,7 +253,7 @@ function getAiSummary() {
aiSummaryTime.value = ""
aiSummary.value = ""
modelName.value = ""
SummaryStockNews(question.value, sysPromptId.value)
//SummaryStockNews(question.value, sysPromptId.value,enableTools.value)
}
})
}
@@ -225,7 +267,7 @@ EventsOn("summaryStockNews", async (msg) => {
loading.value = false
////console.log(msg)
if (msg === "DONE") {
SaveAIResponseResult("市场资讯", "市场资讯", aiSummary.value, chatId.value, question.value)
await SaveAIResponseResult("市场资讯", "市场资讯", aiSummary.value, chatId.value, question.value,aiConfigId.value)
message.info("AI分析完成")
message.destroyAll()
@@ -299,22 +341,37 @@ function ReFlesh(source) {
if (source === "新浪财经") {
sinaNewsList.value = res
}
if (source === "外媒") {
foreignNewsList.value = res
}
})
}
</script>
<template>
<n-card>
<n-tabs type="line" animated @update-value="updateTab" :value="nowTab">
<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-grid :cols="1" :y-gap="0">
<n-gi>
<news-list :newsList="telegraphList" :header-title="'财联社电报'" @update:message="ReFlesh"></news-list>
<AnalyzeMartket :dark-theme="darkTheme" :chart-height="300" :kDays="1" :name="'最近24小时热词'" />
</n-gi>
<n-gi>
<news-list :newsList="sinaNewsList" :header-title="'新浪财经'" @update:message="ReFlesh"></news-list>
<n-grid :cols="foreignNewsList.length?3:2" :y-gap="0">
<n-gi>
<news-list :newsList="telegraphList" :header-title="'财联社电报'" @update:message="ReFlesh"></news-list>
</n-gi>
<n-gi>
<news-list :newsList="sinaNewsList" :header-title="'新浪财经'" @update:message="ReFlesh"></news-list>
</n-gi>
<n-gi v-if="foreignNewsList.length>0">
<news-list :newsList="foreignNewsList" :header-title="'外媒'" @update:message="ReFlesh"></news-list>
</n-gi>
</n-grid>
</n-gi>
</n-grid>
</n-tab-pane>
<n-tab-pane name="全球股指" tab="全球股指">
<n-tabs type="segment" animated>
@@ -354,67 +411,91 @@ function ReFlesh(source) {
</n-grid>
</n-tab-pane>
<n-tab-pane name="上证指数" tab="上证指数">
<k-line-chart code="sh000001" :chart-height="panelHeight" name="上证指数" :k-days="20"
<k-line-chart code="sh000001" :chart-height="panelHeight" stockName="上证指数" :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"
<k-line-chart code="sz399001" :chart-height="panelHeight" stockName="深证成指" :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"
<k-line-chart code="sz399006" :chart-height="panelHeight" stockName="创业板指" :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"
<k-line-chart code="hkHSI" :chart-height="panelHeight" stockName="恒生指数" :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"
<k-line-chart code="us.IXIC" :chart-height="panelHeight" stockName="纳斯达克" :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"
<k-line-chart code="us.DJI" :chart-height="panelHeight" stockName="道琼斯" :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"
<k-line-chart code="us.INX" :chart-height="panelHeight" stockName="标普500" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
</n-tabs>
</n-tab-pane>
<n-tab-pane name="指标行情" tab="指标行情">
<n-tab-pane name="重大指数" tab="重大指数">
<n-tabs type="segment" animated>
<n-tab-pane name="科创50" tab="科创50">
<k-line-chart code="sh000688" :chart-height="panelHeight" name="科创50" :k-days="20"
<n-tab-pane name="恒生科技指数" tab="恒生科技指数">
<k-line-chart code="hkHSTECH" :chart-height="panelHeight" stockName="恒生科技指数" :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" stockName="科创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" stockName="科创芯片" :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" stockName="证券龙头" :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" stockName="高端装备" :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" stockName="中证银行" :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" stockName="上证医药" :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"
<k-line-chart code="sh000300" :chart-height="panelHeight" stockName="沪深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"
<k-line-chart code="sh000016" :chart-height="panelHeight" stockName="上证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"
<k-line-chart code="sh000510" :chart-height="panelHeight" stockName="中证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"
<k-line-chart code="sh000852" :chart-height="panelHeight" stockName="中证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"
<k-line-chart code="sz399997" :chart-height="panelHeight" stockName="中证白酒" :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"
<k-line-chart code="usYINN.AM" :chart-height="panelHeight" stockName="富时中国三倍做多" :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"
<k-line-chart code="usUVXY.AM" :chart-height="panelHeight" stockName="VIX恐慌指数" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
</n-tabs>
@@ -598,6 +679,9 @@ function ReFlesh(source) {
<n-tab-pane name="指标选股" tab="指标选股">
<select-stock />
</n-tab-pane>
<n-tab-pane name="名站优选" tab="名站优选">
<Stockhotmap />
</n-tab-pane>
</n-tabs>
</n-card>
<n-modal transform-origin="center" v-model:show="summaryModal" preset="card" style="width: 800px;"
@@ -615,10 +699,33 @@ function ReFlesh(source) {
</n-flex>
</template>
<template #action>
<n-flex justify="left" style="margin-bottom: 10px">
<n-switch v-model:value="enableTools" :round="false">
<template #checked>
启用AI函数工具调用
</template>
<template #unchecked>
不启用AI函数工具调用
</template>
</n-switch>
<n-switch v-model:value="thinkingMode" :round="false">
<template #checked>
启用思考模式
</template>
<template #unchecked>
不启用思考模式
</template>
</n-switch>
<n-gradient-text type="error" style="margin-left: 10px">*AI函数工具调用可以增强AI获取数据的能力,但会消耗更多tokens</n-gradient-text>
</n-flex>
<n-flex justify="space-between" style="margin-bottom: 10px">
<n-select style="width: 49%" v-model:value="sysPromptId" label-field="name" value-field="ID"
<n-select style="width: 32%" v-model:value="aiConfigId" label-field="name" value-field="ID"
:options="aiConfigs" placeholder="请选择AI模型服务配置"/>
<n-select style="width: 32%" v-model:value="sysPromptId" label-field="name" value-field="ID"
:options="sysPromptOptions" placeholder="请选择系统提示词"/>
<n-select style="width: 49%" v-model:value="question" label-field="name" value-field="content"
<n-select style="width: 32%" v-model:value="question" label-field="name" value-field="content"
:options="userPromptOptions" placeholder="请选择用户提示词"/>
</n-flex>
<n-flex justify="right">
@@ -651,5 +758,4 @@ function ReFlesh(source) {
</template>
<style scoped>
</style>

View File

@@ -1,6 +1,7 @@
<script setup>
import {ReFleshTelegraphList} from "../../wailsjs/go/main/App";
import {RefreshCircle, RefreshCircleSharp, RefreshOutline} from "@vicons/ionicons5";
import {computed, h, onBeforeMount, onBeforeUnmount, onMounted,onUnmounted, ref} from 'vue'
const { headerTitle,newsList } = defineProps({
headerTitle: {
@@ -18,6 +19,30 @@ const emits = defineEmits(['update:message'])
const updateMessage = () => {
emits('update:message', headerTitle)
}
// 使用 ref 创建响应式时间数据
const time = ref(new Date())
// 更新时间的函数
const updateTime = () => {
time.value = new Date()
}
let timer = null
// 组件挂载时启动定时器
onMounted(() => {
if (headerTitle === '财联社电报') {
// 每秒更新一次时间
timer = setInterval(updateTime, 1000)
}
})
// 组件卸载时清除定时器
onUnmounted(() => {
if (timer) {
clearInterval(timer)
}
})
</script>
<template>
@@ -25,12 +50,29 @@ const updateMessage = () => {
<template #header>
<n-flex justify="space-between">
<n-tag :bordered="false" size="large" type="success" >{{ headerTitle }}</n-tag>
<n-tag :bordered="false" size="large" type="info" v-if="headerTitle==='财联社电报'"> <n-time :time="time"/></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-list-item v-for="(item,idx) in newsList" :key="item.ID">
<n-space justify="center" v-if="idx!==0 && item.dataTime.substring(0,10) !== newsList[idx-1].dataTime.substring(0,10)">
<n-divider>
{{ item.dataTime.substring(0,10) }}
</n-divider>
</n-space>
<n-space justify="start" >
<n-collapse v-if="item.title" arrow-placement="right" >
<n-collapse-item :name="item.title">
<template #header>
<n-tag size="small" :type="item.isRed?'error':'warning'" :bordered="false"> {{ item.time }}</n-tag>
<n-text size="small" :type="item.isRed?'error':'info'" :bordered="false">{{ item.title }}</n-text>
</template>
<n-text justify="start" :bordered="false" :type="item.isRed?'error':'info'">
{{ item.content }}
</n-text>
</n-collapse-item>
</n-collapse>
<n-text v-if="!item.title" 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>
@@ -49,6 +91,9 @@ const updateMessage = () => {
<n-text type="warning">查看原文</n-text>
</a>
</n-tag>
<n-tag v-if="item.sentimentResult" :bordered="false" :type="item.sentimentResult==='看涨'?'error':item.sentimentResult==='看跌'?'success':'info'" size="small">
{{ item.sentimentResult }}
</n-tag>
</n-space>
</n-list-item>
</n-list>

View File

@@ -72,7 +72,7 @@ function GetMoneyRankSinaData(){
<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>
<k-line-chart style="width: 800px" :code="item.symbol" :chart-height="500" :stockName="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>

View File

@@ -0,0 +1,49 @@
<script setup>
import {computed, h, onBeforeMount, onBeforeUnmount, onMounted,onUnmounted, ref,reactive} from 'vue'
import {GetAIResponseResultList} from "../../wailsjs/go/main/App";
import {NButton, NEllipsis, NText} from "naive-ui";
import ResearchReport from "./researchReport.vue";
import AiRecommendStocksList from "./aiRecommendStocksList.vue";
import {EventsOff, EventsOn} from "../../wailsjs/runtime";
import {useRoute} from 'vue-router'
const nowTab = ref("AI分析报告")
const route = useRoute()
onBeforeMount(() => {
nowTab.value = route.query.name
})
onBeforeUnmount(() => {
EventsOff("changeResearchTab")
})
onUnmounted(() => {
});
EventsOn("changeResearchTab", async (msg) => {
console.log("changeResearchTab", msg)
updateTab(msg.name)
})
function updateTab(name) {
nowTab.value = name
}
</script>
<template>
<n-card>
<n-tabs type="line" animated @update-value="updateTab" :value="nowTab" style="--wails-draggable:no-drag">
<n-tab-pane name="AI分析报告">
<ResearchReport/>
</n-tab-pane>
<n-tab-pane name="股票推荐记录">
<AiRecommendStocksList/>
</n-tab-pane>
</n-tabs>
</n-card>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,292 @@
<script setup>
import {computed, h, onBeforeMount, onBeforeUnmount, onMounted,onUnmounted, ref,reactive} from 'vue'
import {GetAIResponseResultList, GetConfig, SaveAsMarkdown, ShareAnalysis} from "../../wailsjs/go/main/App";
import {NAvatar, NButton, NEllipsis, NText, useMessage} from "naive-ui";
import {MdEditor, MdPreview} from 'md-editor-v3';
onBeforeMount(()=> {
GetConfig().then(result => {
if (result.darkTheme) {
editorDataRef.darkTheme = true
}
})
})
onMounted(() => {
query({
page: 1,
pageSize: paginationReactive.pageSize,
order: "desc",
keyword: paginationReactive.keyword,
startDate: paginationReactive.range[0],
endDate: paginationReactive.range[1]
}).then((data) => {
console.log( data)
dataRef.value = data.data
paginationReactive.page = 1
paginationReactive.pageCount = data.pageCount
paginationReactive.itemCount = data.total
loadingRef.value = false
})
})
const message = useMessage()
const mdPreviewRef = ref(null)
const mdEditorRef = ref(null)
const editorDataRef = reactive({
show: false,
loading: false,
darkTheme: false,
chatId: "",
modelName: "",
CreatedAt: "",
stockName: "",
stockCode: "",
question: "",
content: "",
})
const dataRef = ref([])
const loadingRef = ref(true)
const columnsRef = ref([
{
title: '分析时间',
key: 'CreatedAt',
render(row, index) {
//2026-01-14T22:13:27.2693252+08:00 格式化为常用时间格式
return row.CreatedAt.substring(0, 19).replace('T', ' ')
}
},
{
title: '模型名称',
key: 'modelName'
},
{
title: '分析对象',
key: 'stockName'
},
{
title: '提示词',
key: 'question',
render(row, index) {
return h(NEllipsis, { tooltip: true ,style: "max-width: 240px;"}, {default: () => h(NText,{type: "info"},{default: () => row.question}),})
}
},
{
title: '操作',
render(row, index) {
return h(
NButton,
{
strong: true,
tertiary: true,
size: 'small',
type: 'warning', // 橙色按钮
style: 'font-size: 14px; padding: 0 10px;', // 稍微大一点的按钮
onClick: () => showReport(row)
},
{ default: () => '查看分析报告' }
)
}
},
])
const paginationReactive = reactive({
page: 1,
pageCount: 1,
pageSize: 12,
itemCount: 0,
keyword: "",
range: [new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000), new Date(new Date().getTime() + 24 * 60 * 60 * 1000)],
prefix({ itemCount }) {
return `${itemCount} 条记录`
}
})
const theme = computed(() => {
return editorDataRef.darkTheme ? 'dark' : 'light'
})
function showReport(row) {
editorDataRef.show = true
editorDataRef.chatId = row.chatId
editorDataRef.modelName = row.modelName
editorDataRef.CreatedAt = row.CreatedAt.substring(0, 19).replace('T', ' ')
editorDataRef.stockName = row.stockName
editorDataRef.stockCode = row.stockCode
editorDataRef.question = row.question
editorDataRef.content = row.content
editorDataRef.loading = false
}
function query({
page,
pageSize = 10,
order = 'desc',
keyword = "",
startDate = "",
endDate = ""
}) {
return new Promise((resolve) => {
GetAIResponseResultList({
"page": page,
"pageSize": pageSize,
"modelName":keyword,
"question":keyword,
"stockName":keyword,
"stockCode":keyword,
"startDate":startDate,
"endDate":endDate
}).then((res) => {
const pagedData =res.list
const total = res.total
const pageCount =res.totalPages
resolve({
pageCount,
data: pagedData,
total
})
})
})
}
function handlePageChange(currentPage) {
if (!loadingRef.value) {
loadingRef.value = true
query({
page: currentPage,
pageSize: paginationReactive.pageSize,
order: "desc",
keyword: paginationReactive.keyword,
startDate: formatDate(paginationReactive.range[0]),
endDate: formatDate(paginationReactive.range[1])
}).then((data) => {
dataRef.value = data.data
paginationReactive.page = currentPage
paginationReactive.pageCount = data.pageCount
paginationReactive.itemCount = data.total
loadingRef.value = false
})
}
}
function handleSearch() {
if (!loadingRef.value) {
loadingRef.value = true
query({
page: 1,
pageSize: paginationReactive.pageSize,
order: "desc",
keyword: paginationReactive.keyword,
startDate: formatDate(paginationReactive.range[0]),
endDate: formatDate(paginationReactive.range[1])
}).then((data) => {
dataRef.value = data.data
paginationReactive.page = 1
paginationReactive.pageCount = data.pageCount
paginationReactive.itemCount = data.total
loadingRef.value = false
})
}
}
function share(code, name) {
ShareAnalysis(code, name).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 saveAsMarkdown(code,name) {
SaveAsMarkdown(code, name).then(result => {
if(result !== ""){
message.success(result)
}
})
}
async function copyToClipboard() {
try {
await navigator.clipboard.writeText(editorDataRef.content);
message.success('分析结果已复制到剪切板');
} catch (err) {
message.error('复制失败: ' + err);
}
}
function formatDate(dateString) {
const date = new Date(dateString)
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')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
</script>
<template>
<n-input-group>
<n-date-picker v-model:value="paginationReactive.range" type="datetimerange" style="width: 50%"/>
<n-input clearable placeholder="输入关键词搜索" v-model:value="paginationReactive.keyword"/>
<n-button type="primary" ghost @click="handleSearch" @input="handleSearch">
搜索
</n-button>
</n-input-group>
<n-data-table
remote
size="small"
:columns="columnsRef"
:data="dataRef"
:loading="loadingRef"
:pagination="paginationReactive"
:row-key="(rowData)=>rowData.ID"
@update:page="handlePageChange"
flex-height
style="height: calc(100vh - 210px);margin-top: 10px"
/>
<n-modal transform-origin="center" v-model:show="editorDataRef.show" preset="card" style="width: 800px;"
:title="'['+editorDataRef.stockName+']AI分析'">
<n-spin size="small" :show="editorDataRef.loading">
<MdPreview ref="mdPreviewRef" style="height: 540px;text-align: left"
:modelValue="editorDataRef.content" :theme="theme"/>
</n-spin>
<template #footer>
<n-flex justify="space-between" ref="tipsRef">
<n-text type="info" v-if="editorDataRef.chatId">
<n-tag v-if="editorDataRef.modelName" type="warning" round :title="editorDataRef.chatId" :bordered="false">
{{ editorDataRef.modelName }}
</n-tag>
{{ editorDataRef.CreatedAt }}
</n-text>
<n-text type="error">*AI分析结果仅供参考请以实际行情为准投资需谨慎风险自担</n-text>
</n-flex>
</template>
<template #action>
<n-flex justify="right">
<n-button size="tiny" type="success" @click="copyToClipboard">复制到剪切板</n-button>
<n-button size="tiny" type="primary" @click="saveAsMarkdown(editorDataRef.stockCode,editorDataRef.stockName)">保存为Markdown文件</n-button>
<n-button size="tiny" type="error" @click="share(editorDataRef.stockCode,editorDataRef.stockName)">分享到项目社区</n-button>
</n-flex>
</template>
</n-modal>
</template>
<style scoped>
</style>

View File

@@ -1,165 +1,210 @@
<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,
httpProxy:"",
httpProxyEnabled:false,
},
enableDanmu: false,
browserPath: '',
enableNews: false,
darkTheme: true,
enableFund: false,
enablePushNews: false,
enableOnlyPushRedNews: false,
sponsorCode: "",
httpProxy:"",
httpProxyEnabled:false,
enableAgent: false,
qgqpBId: '',
})
// 添加一个新的AI配置到列表
function addAiConfig() {
formValue.value.openAI.aiConfigs.push(new data.AIConfig({
name: '',
baseUrl: 'https://api.deepseek.com',
apiKey: '',
model: 'deepseek-chat',
modelName: 'deepseek-chat',
temperature: 0.1,
maxTokens: 1024,
prompt:"",
timeout: 5,
questionTemplate: "{{stockName}}分析和总结",
crawlTimeOut:30,
kDays:30,
},
enableDanmu:false,
browserPath: '',
enableNews:false,
darkTheme:true,
enableFund:false,
enablePushNews:false,
})
const promptTemplates=ref([])
onMounted(()=>{
GetConfig().then(res=>{
maxTokens: 4096,
timeOut: 60,
httpProxy:"",
httpProxyEnabled:false,
}));
}
// 从列表中移除一个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,
httpProxy:"",
httpProxyEnabled:false,
}
formValue.value.enableDanmu = res.enableDanmu
formValue.value.browserPath = res.browserPath
formValue.value.enableNews = res.enableNews
formValue.value.darkTheme = res.darkTheme
formValue.value.enableFund = res.enableFund
formValue.value.enablePushNews = res.enablePushNews
formValue.value.enableOnlyPushRedNews = res.enableOnlyPushRedNews
formValue.value.sponsorCode = res.sponsorCode
formValue.value.httpProxy=res.httpProxy;
formValue.value.httpProxyEnabled=res.httpProxyEnabled;
formValue.value.enableAgent = res.enableAgent;
formValue.value.qgqpBId = res.qgqpBId;
//console.log(res)
})
//message.info("加载完成")
GetPromptTemplates("","").then(res=>{
//console.log(res)
promptTemplates.value=res
GetPromptTemplates("", "").then(res => {
promptTemplates.value = res
})
})
onBeforeUnmount(() => {
message.destroyAll()
})
function saveConfig(){
let config= new data.Settings({
ID:formValue.value.ID,
dingPushEnable:formValue.value.dingPush.enable,
dingRobot:formValue.value.dingPush.dingRobot,
localPushEnable:formValue.value.localPush.enable,
updateBasicInfoOnStart:formValue.value.updateBasicInfoOnStart,
refreshInterval:formValue.value.refreshInterval,
openAiEnable:formValue.value.openAI.enable,
openAiBaseUrl:formValue.value.openAI.baseUrl,
openAiApiKey:formValue.value.openAI.apiKey,
openAiModelName:formValue.value.openAI.model,
openAiMaxTokens:formValue.value.openAI.maxTokens,
openAiTemperature:formValue.value.openAI.temperature,
tushareToken:formValue.value.tushareToken,
prompt:formValue.value.openAI.prompt,
openAiApiTimeOut:formValue.value.openAI.timeout,
questionTemplate:formValue.value.openAI.questionTemplate,
crawlTimeOut:formValue.value.openAI.crawlTimeOut,
kDays:formValue.value.openAI.kDays,
enableDanmu:formValue.value.enableDanmu,
browserPath:formValue.value.browserPath,
enableNews:formValue.value.enableNews,
darkTheme:formValue.value.darkTheme,
enableFund:formValue.value.enableFund,
enablePushNews:formValue.value.enablePushNews
function saveConfig() {
console.log('开始保存设置', formValue.value);
// 构建配置时包含aiConfigs列表
let config = new data.SettingConfig({
ID: formValue.value.ID,
dingPushEnable: formValue.value.dingPush.enable,
dingRobot: formValue.value.dingPush.dingRobot,
localPushEnable: formValue.value.localPush.enable,
updateBasicInfoOnStart: formValue.value.updateBasicInfoOnStart,
refreshInterval: formValue.value.refreshInterval,
openAiEnable: formValue.value.openAI.enable,
aiConfigs: formValue.value.openAI.aiConfigs,
// 序列化aiConfigs列表以传递给后端
tushareToken: formValue.value.tushareToken,
prompt: formValue.value.openAI.prompt,
questionTemplate: formValue.value.openAI.questionTemplate,
crawlTimeOut: formValue.value.openAI.crawlTimeOut,
kDays: formValue.value.openAI.kDays,
enableDanmu: formValue.value.enableDanmu,
browserPath: formValue.value.browserPath,
enableNews: formValue.value.enableNews,
darkTheme: formValue.value.darkTheme,
enableFund: formValue.value.enableFund,
enablePushNews: formValue.value.enablePushNews,
enableOnlyPushRedNews: formValue.value.enableOnlyPushRedNews,
sponsorCode: formValue.value.sponsorCode,
httpProxy:formValue.value.httpProxy,
httpProxyEnabled:formValue.value.httpProxyEnabled,
enableAgent: formValue.value.enableAgent,
qgqpBId: formValue.value.qgqpBId
})
//console.log("Settings",config)
UpdateConfig(config).then(res=>{
message.success(res)
EventsEmit("updateSettings", config);
})
if (config.sponsorCode) {
CheckSponsorCode(config.sponsorCode).then(res => {
if (res.code) {
UpdateConfig(config).then(res => {
message.success(res)
EventsEmit("updateSettings", config);
})
} else {
message.error(res.msg)
}
})
} else {
UpdateConfig(config).then(res => {
message.success(res)
EventsEmit("updateSettings", config);
})
}
}
function getHeight() {
return document.documentElement.clientHeight
}
function sendTestNotice(){
let markdown="### go-stock test\n"+new Date()
let msg='{' +
function sendTestNotice() {
let markdown = "### go-stock test\n" + new Date()
let msg = '{' +
' "msgtype": "markdown",' +
' "markdown": {' +
' "title":"go-stock'+new Date()+'",' +
' "text": "'+markdown+'"' +
' "title":"go-stock' + new Date() + '",' +
' "text": "' + markdown + '"' +
' },' +
' "at": {' +
' "isAtAll": true' +
' }' +
' }'
SendDingDingMessageByType(msg, "test-"+new Date().getTime(),1).then(res=>{
SendDingDingMessageByType(msg, "test-" + new Date().getTime(), 1).then(res => {
message.info(res)
})
}
function exportConfig(){
ExportConfig().then(res=>{
function exportConfig() {
ExportConfig().then(res => {
message.info(res)
})
}
function importConfig(){
function importConfig() {
let input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
@@ -168,30 +213,25 @@ function importConfig(){
let reader = new FileReader();
reader.onload = (e) => {
let config = JSON.parse(e.target.result);
//console.log(config)
formValue.value.ID = config.ID
formValue.value.tushareToken = config.tushareToken
formValue.value.dingPush = {
enable:config.dingPushEnable,
dingRobot:config.dingRobot
enable: config.dingPushEnable,
dingRobot: config.dingRobot
}
formValue.value.localPush = {
enable:config.localPushEnable,
enable: config.localPushEnable,
}
formValue.value.updateBasicInfoOnStart = config.updateBasicInfoOnStart
formValue.value.refreshInterval = config.refreshInterval
// 导入AI配置
formValue.value.openAI = {
enable:config.openAiEnable,
baseUrl: config.openAiBaseUrl,
apiKey:config.openAiApiKey,
model:config.openAiModelName,
temperature:config.openAiTemperature,
maxTokens:config.openAiMaxTokens,
prompt:config.prompt,
timeout:config.openAiApiTimeOut,
questionTemplate:config.questionTemplate,
crawlTimeOut:config.crawlTimeOut,
kDays:config.kDays
enable: config.openAiEnable,
aiConfigs: config.aiConfigs || [],
prompt: config.prompt,
questionTemplate: config.questionTemplate,
crawlTimeOut: config.crawlTimeOut,
kDays: config.kDays
}
formValue.value.enableDanmu = config.enableDanmu
formValue.value.browserPath = config.browserPath
@@ -199,7 +239,12 @@ function importConfig(){
formValue.value.darkTheme = config.darkTheme
formValue.value.enableFund = config.enableFund
formValue.value.enablePushNews = config.enablePushNews
// formRef.value.resetFields()
formValue.value.enableOnlyPushRedNews = config.enableOnlyPushRedNews
formValue.value.sponsorCode = config.sponsorCode
formValue.value.httpProxy=config.httpProxy
formValue.value.httpProxyEnabled=config.httpProxyEnabled
formValue.value.enableAgent = config.enableAgent
formValue.value.qgqpBId = config.qgqpBId
};
reader.readAsText(file);
};
@@ -208,8 +253,6 @@ function importConfig(){
window.onerror = function (event, source, lineno, colno, error) {
//console.log(event, source, lineno, colno, error)
// 将错误信息发送给后端
EventsEmit("frontendError", {
page: "settings.vue",
message: event,
@@ -218,223 +261,265 @@ window.onerror = function (event, source, lineno, colno, error) {
colno: colno,
error: error ? error.stack : null
});
//message.error("发生错误:"+event)
return true;
};
const showManagePromptsModal=ref(false)
const promptTypeOptions=[
{label:"模型系统Prompt",value:'模型系统Prompt'},
{label:"模型用户Prompt",value:'模型用户Prompt'},]
const formPromptRef=ref(null)
const formPrompt=ref({
ID:0,
Name:'',
Content:'',
Type:'',
const showManagePromptsModal = ref(false)
const promptTypeOptions = [
{label: "模型系统Prompt", value: '模型系统Prompt'},
{label: "模型用户Prompt", value: '模型用户Prompt'},]
const formPromptRef = ref(null)
const formPrompt = ref({
ID: 0,
Name: '',
Content: '',
Type: '',
})
function managePrompts(){
formPrompt.value.ID=0
showManagePromptsModal.value=true
function managePrompts() {
formPrompt.value.ID = 0
showManagePromptsModal.value = true
}
function savePrompt(){
AddPrompt(formPrompt.value).then(res=>{
function savePrompt() {
AddPrompt(formPrompt.value).then(res => {
message.success(res)
GetPromptTemplates("","").then(res=>{
//console.log(res)
promptTemplates.value=res
GetPromptTemplates("", "").then(res => {
promptTemplates.value = res
})
showManagePromptsModal.value=false
showManagePromptsModal.value = false
})
}
function editPrompt(prompt){
//console.log(prompt)
formPrompt.value.ID=prompt.ID
formPrompt.value.Name=prompt.name
formPrompt.value.Content=prompt.content
formPrompt.value.Type=prompt.type
showManagePromptsModal.value=true
function editPrompt(prompt) {
formPrompt.value.ID = prompt.ID
formPrompt.value.Name = prompt.name
formPrompt.value.Content = prompt.content
formPrompt.value.Type = prompt.type
showManagePromptsModal.value = true
}
function deletePrompt(ID){
DelPrompt(ID).then(res=>{
function deletePrompt(ID) {
DelPrompt(ID).then(res => {
message.success(res)
GetPromptTemplates("","").then(res=>{
//console.log(res)
promptTemplates.value=res
GetPromptTemplates("", "").then(res => {
promptTemplates.value = res
})
})
}
</script>
<template>
<n-flex justify="left" style="margin-top: 12px;padding-left: 12px;">
<n-form ref="formRef" :label-placement="'left'" :label-align="'left'" >
<n-grid :cols="24" :x-gap="24" style="text-align: left" :layout-shift-disabled="true">
<n-gi :span="24">
<n-text type="success" style="font-size: 25px;font-weight: bold">基础设置</n-text>
</n-gi>
<n-form-item-gi :span="10" label="Tushare &nbsp;&nbsp;Token" path="tushareToken" >
<n-input type="text" placeholder="Tushare api token" v-model:value="formValue.tushareToken" clearable />
</n-form-item-gi>
<n-form-item-gi :span="4" label="启动时更新A股/指数信息:" path="updateBasicInfoOnStart" >
<n-switch v-model:value="formValue.updateBasicInfoOnStart" />
</n-form-item-gi>
<n-form-item-gi :span="4" label="数据刷新间隔" path="refreshInterval" >
<n-input-number v-model:value="formValue.refreshInterval" placeholder="请输入数据刷新间隔(秒)">
<template #suffix>
</template>
</n-input-number>
</n-form-item-gi>
<n-form-item-gi :span="6" label="暗黑主题" path="darkTheme" >
<n-switch v-model:value="formValue.darkTheme" />
</n-form-item-gi>
<n-form-item-gi :span="10" label="浏览器安装路径" path="browserPath" >
<n-input type="text" placeholder="浏览器安装路径" v-model:value="formValue.browserPath" clearable />
</n-form-item-gi>
<n-form-item-gi :span="6" label="指数基金" path="enableFund" >
<n-switch v-model:value="formValue.enableFund" />
</n-form-item-gi>
</n-grid>
<n-flex justify="left" style="text-align: left; --wails-draggable:no-drag">
<n-form ref="formRef" :label-placement="'left'" :label-align="'left'">
<n-space vertical size="large">
<n-card :title="() => h(NTag, { type: 'primary', bordered: false }, () => '基础设置')" size="small">
<n-grid :cols="24" :x-gap="24" style="text-align: left">
<n-form-item-gi :span="10" label="Tushare Token" path="tushareToken">
<n-input type="text" placeholder="Tushare api token" v-model:value="formValue.tushareToken" clearable/>
</n-form-item-gi>
<n-form-item-gi :span="4" label="启动时更新基础信息:" path="updateBasicInfoOnStart">
<n-switch v-model:value="formValue.updateBasicInfoOnStart"/>
</n-form-item-gi>
<n-form-item-gi :span="4" label="数据刷新间隔:" path="refreshInterval">
<n-input-number v-model:value="formValue.refreshInterval" placeholder="请输入数据刷新间隔(秒)">
<template #suffix></template>
</n-input-number>
</n-form-item-gi>
<n-form-item-gi :span="6" label="暗黑主题:" path="darkTheme">
<n-switch v-model:value="formValue.darkTheme"/>
</n-form-item-gi>
<n-form-item-gi :span="10" label="浏览器安装路径" path="browserPath">
<n-input type="text" placeholder="浏览器安装路径" v-model:value="formValue.browserPath" clearable/>
</n-form-item-gi>
<n-form-item-gi :span="3" label="指数基金" path="enableFund">
<n-switch v-model:value="formValue.enableFund"/>
</n-form-item-gi>
<n-form-item-gi :span="3" label="AI智能体" path="enableAgent">
<n-switch v-model:value="formValue.enableAgent"/>
</n-form-item-gi>
<n-form-item-gi :span="11" label="东财唯一标识:" path="qgqpBId">
<n-input type="text" placeholder="东财唯一标识" v-model:value="formValue.qgqpBId" clearable/>
</n-form-item-gi>
<n-grid :cols="24" :x-gap="24" style="text-align: left">
<n-gi :span="24">
<n-text type="success" style="font-size: 25px;font-weight: bold">通知设置</n-text>
</n-gi>
<n-form-item-gi :span="4" label="钉钉推送:" path="dingPush.enable" >
<n-switch v-model:value="formValue.dingPush.enable" />
</n-form-item-gi>
<n-form-item-gi :span="4" label="本地推送:" path="localPush.enable" >
<n-switch v-model:value="formValue.localPush.enable" />
</n-form-item-gi>
<n-form-item-gi :span="4" label="弹幕功能:" path="enableDanmu" >
<n-switch v-model:value="formValue.enableDanmu" />
</n-form-item-gi>
<n-form-item-gi :span="4" label="显示滚动快讯:" path="enableNews" >
<n-switch v-model:value="formValue.enableNews" />
</n-form-item-gi>
<n-form-item-gi :span="4" label="市场资讯提醒:" path="enablePushNews" >
<n-switch v-model:value="formValue.enablePushNews" />
</n-form-item-gi>
<n-form-item-gi :span="22" v-if="formValue.dingPush.enable" label="钉钉机器人接口地址:" path="dingPush.dingRobot" >
<n-input placeholder="请输入钉钉机器人接口地址" v-model:value="formValue.dingPush.dingRobot"/>
<n-button type="primary" @click="sendTestNotice">发送测试通知</n-button>
</n-form-item-gi>
</n-grid>
<n-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">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-card :title="() => h(NTag, { type: 'primary', bordered: false }, () => '通知设置')" size="small">
<n-grid :cols="24" :x-gap="24" style="text-align: left">
<n-form-item-gi :span="3" label="钉钉推送:" path="dingPush.enable">
<n-switch v-model:value="formValue.dingPush.enable"/>
</n-form-item-gi>
<n-form-item-gi :span="3" label="本地推送:" path="localPush.enable">
<n-switch v-model:value="formValue.localPush.enable"/>
</n-form-item-gi>
<n-form-item-gi :span="3" label="弹幕功能:" path="enableDanmu">
<n-switch v-model:value="formValue.enableDanmu"/>
</n-form-item-gi>
<n-form-item-gi :span="3" label="显示滚动快讯:" path="enableNews">
<n-switch v-model:value="formValue.enableNews"/>
</n-form-item-gi>
<n-form-item-gi :span="3" label="市场资讯提醒:" path="enablePushNews">
<n-switch v-model:value="formValue.enablePushNews"/>
</n-form-item-gi>
<n-form-item-gi v-if="formValue.enablePushNews" :span="4" label="只提醒红字或关注个股的新闻:" path="enableOnlyPushRedNews">
<n-switch v-model:value="formValue.enableOnlyPushRedNews"/>
</n-form-item-gi>
<n-form-item-gi :span="22" v-if="formValue.dingPush.enable" label="钉钉机器人接口地址:"
path="dingPush.dingRobot">
<n-input placeholder="请输入钉钉机器人接口地址" v-model:value="formValue.dingPush.dingRobot"/>
<n-button type="primary" @click="sendTestNotice">发送测试通知</n-button>
</n-form-item-gi>
</n-grid>
</n-card>
<n-card :title="() => h(NTag, { type: 'primary', bordered: false }, () => 'AI设置')" size="small">
<n-grid :cols="24" :x-gap="24" style="text-align: left;">
<n-form-item-gi :span="24" label="AI诊股" path="openAI.enable">
<n-switch v-model:value="formValue.openAI.enable"/>
</n-form-item-gi>
<n-form-item-gi :span="6" v-if="formValue.openAI.enable" label="Crawler Timeout(秒)"
title="资讯采集超时时间(秒)" path="openAI.crawlTimeOut">
<n-input-number min="30" step="1" v-model:value="formValue.openAI.crawlTimeOut"/>
</n-form-item-gi>
<n-form-item-gi :span="4" v-if="formValue.openAI.enable" title="天数越多消耗tokens越多"
label="日K线数据(天)" path="openAI.kDays">
<n-input-number min="30" step="1" max="60" 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-form-item-gi :span="12" label="http代理" :path="`openAI.aiConfigs[${index}].httpProxyEnabled`">
<n-switch v-model:value="aiConfig.httpProxyEnabled"/>
</n-form-item-gi>
<n-form-item-gi :span="12" v-if="aiConfig.httpProxyEnabled" title="http代理地址" :path="`openAI.aiConfigs[${index}].httpProxy`">
<n-input type="text" placeholder="http代理地址" v-model:value="aiConfig.httpProxy" clearable/>
</n-form-item-gi>
</n-grid>
</n-card>
<n-button type="primary" dashed @click="addAiConfig" style="width: 100%;">+ 添加AI配置</n-button>
</n-space>
</n-gi>
<n-gi :span="24">
<n-divider/>
</n-gi>
<n-gi :span="24">
<n-space vertical>
<n-space justify="center">
<n-button type="warning" @click="managePrompts">管理提示词模板</n-button>
<n-button type="primary" strong @click="saveConfig">保存设置</n-button>
<n-button type="info" @click="exportConfig">导出配置</n-button>
<n-button type="error" @click="importConfig">导入配置</n-button>
</n-space>
<n-flex justify="start" style="margin-top: 10px" v-if="promptTemplates.length > 0">
<n-tag :bordered="false" type="warning">提示词模板:</n-tag>
<n-tag size="medium" secondary v-for="prompt in promptTemplates" closable
@close="deletePrompt(prompt.ID)" @click="editPrompt(prompt)" :title="prompt.content"
:type="prompt.type === '模型系统Prompt' ? 'success' : 'info'" :bordered="false">{{
prompt.name
}}
</n-tag>
</n-flex>
</n-space>
</n-gi>
</n-grid>
</n-card>
</n-space>
</n-form>
</n-flex>
<n-modal v-model:show="showManagePromptsModal" closable :mask-closable="false">
<n-card
style="width: 800px;height: 600px;text-align: left"
:bordered="false"
:title="(formPrompt.ID>0?'修改':'添加')+'提示词'"
size="huge"
role="dialog"
aria-modal="true"
>
<n-form ref="formPromptRef" :label-placement="'left'" :label-align="'left'" >
<n-form-item label="名称">
<n-input v-model:value="formPrompt.Name" placeholder="请输入提示词名称" />
<n-modal v-model:show="showManagePromptsModal" closable :mask-closable="false">
<n-card style="width: 800px; height: 600px; text-align: left" :bordered="false"
:title="(formPrompt.ID > 0 ? '修改' : '添加') + '提示词'" size="huge" role="dialog" aria-modal="true">
<n-form ref="formPromptRef" :label-placement="'left'" :label-align="'left'">
<n-form-item label="名称">
<n-input v-model:value="formPrompt.Name" placeholder="请输入提示词名称"/>
</n-form-item>
<n-form-item label="类型">
<n-select v-model:value="formPrompt.Type" :options="promptTypeOptions" placeholder="请选择提示词类型" />
<n-form-item label="类型">
<n-select v-model:value="formPrompt.Type" :options="promptTypeOptions" placeholder="请选择提示词类型"/>
</n-form-item>
<n-form-item label="内容">
<n-input v-model:value="formPrompt.Content"
type="textarea"
:show-count="true"
placeholder="请输入prompt"
:autosize="{
minRows: 12,
maxRows: 12,
}"
/>
<n-form-item label="内容">
<n-input v-model:value="formPrompt.Content" type="textarea" :show-count="true" placeholder="请输入prompt"
:autosize="{ minRows: 12, maxRows: 12, }"/>
</n-form-item>
</n-form>
<template #footer>
<n-flex justify="end">
<n-button type="primary" @click="savePrompt">
保存
</n-button>
<n-button type="warning" @click="showManagePromptsModal=false">
取消
</n-button>
<n-button type="primary" @click="savePrompt">保存</n-button>
<n-button type="warning" @click="showManagePromptsModal = false">取消</n-button>
</n-flex>
</template>
</n-card>
@@ -442,5 +527,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,43 @@
<script setup lang="ts">
import {h} from 'vue'
import {NTag,NImage} from 'naive-ui'
import EmbeddedUrl from "./EmbeddedUrl.vue";
</script>
<template>
<n-tabs type="line" animated>
<n-tab-pane name="选股通" tab="选股通">
<embedded-url url="https://xuangutong.com.cn" :height="'calc(100vh - 252px)'"/>
</n-tab-pane>
<n-tab-pane name="百度股市通" tab="百度股市通">
<embedded-url url="https://gushitong.baidu.com" :height="'calc(100vh - 252px)'"/>
</n-tab-pane>
<n-tab-pane name="东财大盘星图" tab="东财大盘星图">
<embedded-url url="https://quote.eastmoney.com/stockhotmap/" :height="'calc(100vh - 252px)'"/>
</n-tab-pane>
<n-tab-pane name="TopHub" tab="TopHub(今日热榜)">
<embedded-url url="https://tophub.today/c/finance" :height="'calc(100vh - 252px)'"/>
</n-tab-pane>
<n-tab-pane name="摸鱼" tab="摸鱼">
<embedded-url url="https://996.ninja/" :height="'calc(100vh - 252px)'"/>
</n-tab-pane>
<n-tab-pane name="财联社-行情数据" tab="财联社-行情数据">
<embedded-url url="https://www.cls.cn/quotation" :height="'calc(100vh - 252px)'"/>
</n-tab-pane>
<n-tab-pane name="消息墙" tab="消息墙">
<embedded-url url="https://go-stock.sparkmemory.top:16667/go-stock" :height="'calc(100vh - 252px)'"/>
</n-tab-pane>
<n-tab-pane name="欢迎推荐更多有趣的财经网页" tab="欢迎推荐更多有趣的财经网页">
</n-tab-pane>
<!-- <n-tab-pane name="自在量化" tab="自在量化">-->
<!-- <embedded-url url="https://quant.zizizaizai.com/home"/>-->
<!-- </n-tab-pane>-->
</n-tabs>
</template>
<style scoped>
</style>

View File

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

View File

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

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