Compare commits

...

16 Commits

Author SHA1 Message Date
ArvinLovegood
5e7f34652a feat(frontend):增加AI函数工具调用开关
- 在市场和股票组件中添加启用/禁用 AI 函数工具调用的开关
- 修改相关函数以支持 enableTools 参数,控制是否启用工具调用
- 优化 AI 总结新闻和聊天流函数,根据 enableTools 决定是否使用工具
2025-07-03 14:25:30 +08:00
ArvinLovegood
5b9a81d770 refactor(market-news):优化市场新闻API日志输出
- 注释掉 XUEQIUHotStock 的日志输出,减少不必要的日志信息
- 调整前端股票组件中的关注和 AI 分析逻辑
- 优化 AI 分析相关的用户交互和数据处理
- 美化模态框标题和按钮文案
2025-07-03 12:42:30 +08:00
ArvinLovegood
7021a59ee6 refactor(data):使用随机数替代固定参数以提高数据获取效率
- 在获取财经新闻列表时,使用随机数替代固定的数量参数
- 在搜索股票时,使用随机数替代固定的搜索数量
- 更新README
2025-07-03 10:22:22 +08:00
ArvinLovegood
433dea0772 feat(app):更新SearchStockByIndicators函数描述
- 扩展了函数描述,说明可以同时查询多个股票名称
- 调整了示例,使用多个股票名称进行查询
2025-07-02 18:57:16 +08:00
ArvinLovegood
378a5c47ba fix(backend):处理不支持函数调用的模型
- 当收到 "Function call is not supported for this model." 错误消息时
- 移除所有 "tool" 类型的消息和包含 "tool_calls" 的消息
- 使用剩余的消息重新调用 AskAi函数
2025-07-02 18:46:38 +08:00
ArvinLovegood
9a60736739 fix(backend):优化AI工具调用逻辑
- 当模型不支持函数调用时,重新使用 AI 模型询问
- 添加函数调用相关的消息结构
- 优化错误处理逻辑
2025-07-02 18:41:47 +08:00
ArvinLovegood
efe6365ea5 feat(frontend):增加股票名称点击事件打开行情页面
- 在 SelectStock 组件中添加了 openCenteredWindow 函数,用于打开居中窗口
- 点击股票名称时,会打开东方财富网的股票行情页面
- 优化了表格列的排序功能,支持数值类型的列进行排序- 调整了表格列的最小宽度,提高可读性
2025-07-02 17:40:15 +08:00
ArvinLovegood
062df80712 feat(frontend):添加热门策略功能并优化选股组件
- 在 App.d.ts 和 App.js 中添加 GetHotStrategy 函数
- 在 app_common.go 中实现 GetHotStrategy 方法
- 在 search_stock_api.go 中添加 HotStrategy 方法获取热门策略数据
- 更新 SelectStock.vue 组件,集成热门策略功能并优化界面布局
2025-07-02 16:10:02 +08:00
ArvinLovegood
528482db48 refactor(data):调整财联社电报新闻获取数量
- 将获取新闻列表的数量从 500 条调整为 100 条
- 这一改动可以减少接口请求的数据量,提高响应速度
2025-07-02 14:06:29 +08:00
ArvinLovegood
746e5ec98a feat(app):集成AI工具并优化股票数据获取
- 在 App 结构中添加 AiTools 字段,用于存储 AI 工具配置
- 新增 AddTools 函数,定义了两个 AI 工具:SearchStockByIndicators 和 GetStockKLine- 修改 NewApp 函数,初始化时加载 AI 工具配置- 更新相关函数,支持使用 AI 工具进行股票数据查询- 优化股票 K 线数据获取逻辑,增加对不同市场股票代码的支持
2025-07-02 12:29:57 +08:00
ArvinLovegood
6d345ae91d feat(app):集成AI工具并优化股票数据获取
- 在 App 结构中添加 AiTools 字段,用于存储 AI 工具配置
- 新增 AddTools 函数,定义了两个 AI 工具:SearchStockByIndicators 和 GetStockKLine- 修改 NewApp 函数,初始化时加载 AI 工具配置- 更新相关函数,支持使用 AI 工具进行股票数据查询- 优化股票 K 线数据获取逻辑,增加对不同市场股票代码的支持
2025-07-02 12:13:52 +08:00
ArvinLovegood
888a97e4d3 feat(app):更新SearchStockByIndicators工具函数描述并优化错误处理
- 更新 SearchStockByIndicators 函数描述,使其更准确地反映功能
- 在 Resp 结构中添加 Error 字段,用于处理错误信息
- 修改 openai_api.go 和 openai_api_test.go 中的错误处理逻辑
- 优化消息发送格式,提高错误信息的可读性
2025-07-02 10:25:03 +08:00
ArvinLovegood
ebeaf104bb feat(data):新增SummaryStockNewsStreamWithTools功能
- 在 OpenAi 结构中添加了新的方法 NewSummaryStockNewsStreamWithTools,支持使用工具进行股票分析
- 在 app.go 中调用了新方法,集成了股票搜索工具- 修改了 SearchStockApi 的 SearchStock 方法,增加了 pageSize 参数
- 更新了相关测试文件以适应新的功能
2025-07-01 19:27:59 +08:00
ArvinLovegood
b945a0e0e1 feat(frontend):在获取版本信息时,将官方声明添加到内容开头
- 修改了 App.vue 文件中的 onBeforeMount 钩子
- 在获取到官方声明后,将其添加到现有内容的开头
- 通过换行符分隔官方声明和原有内容
2025-07-01 12:43:03 +08:00
ArvinLovegood
111252f8bd feat(frontend):优化选股组件功能和界面
- 添加输入校验,提醒用户输入选股指标或要求
- 增加选股条件展示区域
- 优化按钮样式和布局
- 调整表格高度以适应新内容
2025-07-01 11:52:42 +08:00
ArvinLovegood
2e5ec6ace8 feat:添加官方声明内容
- 在 VersionInfo 结构中增加 OfficialStatement 字段
- 在前端 App.vue 中添加官方声明内容的获取和显示
- 在 main.go 中定义 OFFICIAL_STATEMENT 变量
- 更新 GitHub Actions 构建配置,添加 OFFICIAL_STATEMENT环境变量
2025-07-01 09:47:46 +08:00
18 changed files with 805 additions and 116 deletions

View File

@ -9,6 +9,7 @@ on:
env: env:
# Necessary for most environments as build failure can occur due to OOM issues # Necessary for most environments as build failure can occur due to OOM issues
NODE_OPTIONS: "--max-old-space-size=4096" NODE_OPTIONS: "--max-old-space-size=4096"
OFFICIAL_STATEMENT: ${{ vars.OFFICIAL_STATEMENT }}
jobs: jobs:
build: build:
@ -38,7 +39,7 @@ jobs:
echo "::set-output name=commit_message::$commit_message" echo "::set-output name=commit_message::$commit_message"
- name: Build wails x go-stock - name: Build wails x go-stock
uses: ArvinLovegood/wails-build-action@v3.4 uses: ArvinLovegood/wails-build-action@v3.5
id: build id: build
with: with:
build-name: ${{ matrix.build.name }} build-name: ${{ matrix.build.name }}
@ -47,4 +48,5 @@ jobs:
go-version: '1.24' go-version: '1.24'
build-tags: ${{ github.ref_name }} build-tags: ${{ github.ref_name }}
build-commit-message: ${{ steps.get_commit_message.outputs.commit_message }} build-commit-message: ${{ steps.get_commit_message.outputs.commit_message }}
build-statement: ${{ env.OFFICIAL_STATEMENT }}
node-version: '20.x' node-version: '20.x'

View File

@ -37,8 +37,8 @@
### <span style="color: #568DF4;">各位亲爱的朋友们,如果您对这个项目感兴趣,请先给我一个<i style="color: #EA2626;">star</i>吧,谢谢!</span>💕 ### <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) - 优云智算by UCloud万卡规模4090免费用10小时新人注册另增50万tokens海量热门源项目镜像一键部署[注册链接](https://www.compshare.cn/image-community?ytag=GPU_YY-gh_gostock)
- 经测试目前硅基流动(siliconflow)提供的deepSeek api 服务比较稳定注册即送2000万Tokens[注册链接](https://cloud.siliconflow.cn/i/foufCerk) - 火山方舟新用户每个模型注册即送50万tokens[注册链接](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ)
- 火山方舟每个模型注册即送50万tokens[注册链接](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ) - 硅基流动(siliconflow)注册即送2000万Tokens[注册链接](https://cloud.siliconflow.cn/i/foufCerk)
- Tushare大数据开放社区,免费提供各类金融数据,助力行业和量化研究(注意Tushare只需要120积分即可注册完成个人资料补充即可得120积分)[注册链接](https://tushare.pro/register?reg=701944) - Tushare大数据开放社区,免费提供各类金融数据,助力行业和量化研究(注意Tushare只需要120积分即可注册完成个人资料补充即可得120积分)[注册链接](https://tushare.pro/register?reg=701944)
- 软件快速迭代开发中,请大家优先测试和使用最新发布的版本。 - 软件快速迭代开发中,请大家优先测试和使用最新发布的版本。
- 欢迎大家提出宝贵的建议欢迎提issue,PR。当然更欢迎[赞助我](#都划到这了如果我的项目对您有帮助请赞助我吧)。💕 - 欢迎大家提出宝贵的建议欢迎提issue,PR。当然更欢迎[赞助我](#都划到这了如果我的项目对您有帮助请赞助我吧)。💕
@ -57,6 +57,7 @@
| 不再强制依赖Chrome浏览器 | ✅ | 默认使用edge浏览器抓取新闻资讯 | | 不再强制依赖Chrome浏览器 | ✅ | 默认使用edge浏览器抓取新闻资讯 |
## 👀 更新日志 ## 👀 更新日志
### 2025.07.01 AI分析集成工具函数AI分析将更加智能
### 2025.06.30 添加指标选股功能 ### 2025.06.30 添加指标选股功能
### 2025.06.27 添加财经日历和重大事件时间轴功能 ### 2025.06.27 添加财经日历和重大事件时间轴功能
### 2025.06.25 添加热门股票、事件和话题功能 ### 2025.06.25 添加热门股票、事件和话题功能

78
app.go
View File

@ -36,6 +36,7 @@ type App struct {
cache *freecache.Cache cache *freecache.Cache
cron *cron.Cron cron *cron.Cron
cronEntrys map[string]cron.EntryID cronEntrys map[string]cron.EntryID
AiTools []data.Tool
} }
// NewApp creates a new App application struct // NewApp creates a new App application struct
@ -44,13 +45,68 @@ func NewApp() *App {
cache := freecache.NewCache(cacheSize) cache := freecache.NewCache(cacheSize)
c := cron.New(cron.WithSeconds()) c := cron.New(cron.WithSeconds())
c.Start() c.Start()
var tools []data.Tool
tools = AddTools(tools)
return &App{ return &App{
cache: cache, cache: cache,
cron: c, cron: c,
cronEntrys: make(map[string]cron.EntryID), cronEntrys: make(map[string]cron.EntryID),
AiTools: tools,
} }
} }
func AddTools(tools []data.Tool) []data.Tool {
tools = append(tools, data.Tool{
Type: "function",
Function: data.ToolFunction{
Name: "SearchStockByIndicators",
Description: "根据自然语言筛选股票,返回自然语言选股条件要求的股票所有相关数据。输入股票名称可以获取当前股票最新的股价交易数据和基础财务指标信息,多个股票名称使用,分隔。工具限制:不允许并行调用",
Parameters: data.FunctionParameters{
Type: "object",
Properties: map[string]any{
"words": map[string]any{
"type": "string",
"description": "选股自然语言。" +
"例1创新药,半导体;PE<30;净利润增长率>50%。 " +
"例2上证指数,科创50。 " +
"例3长电科技,上海贝岭。" +
"例4长电科技,上海贝岭;KDJ,MACD,RSI,BOLL,主力净流入/流出" +
"例5换手率大于3%小于25%.量比1以上. 10日内有过涨停.股价处于峰值的二分之一以下.流通股本<100亿.当日和连续四日净流入;股价在20日均线以上.分时图股价在均线之上.热门板块下涨幅领先的A股. 当日量能20000手以上.沪深个股.近一年市盈率波动小于150%.MACD金叉;不要ST股及不要退市股非北交所每股收益>0。" +
"例6沪深主板.流通市值小于100亿.市值大于10亿.60分钟dif大于dea.60分钟skdj指标k值大于d值.skdj指标k值小于90.换手率大于3%.成交额大于1亿元.量比大于2.涨幅大于2%小于7%.股价大于5小于50.创业板.10日均线大于20日均线;不要ST股及不要退市股;不要北交所;不要科创板;不要创业板。" +
"例7股价在20日线上一月之内涨停次数>=1量比大于1换手率大于3%,流通市值大于 50亿小于200亿。" +
"例8基本条件前期有爆量回调到 10 日线,当日是缩量阴线,均线趋势向上。;优选条件:一月之内涨停次数>=1",
},
},
Required: []string{"words"},
},
},
})
tools = append(tools, data.Tool{
Type: "function",
Function: data.ToolFunction{
Name: "GetStockKLine",
Description: "获取股票日K线数据。工具限制不允许并行调用",
Parameters: data.FunctionParameters{
Type: "object",
Properties: map[string]any{
"days": map[string]any{
"type": "string",
"description": "日K数据条数",
},
"stockCode": map[string]any{
"type": "string",
"description": "股票代码A股sh,sz开头;港股hk开头,美股us开头",
},
},
Required: []string{"days", "stockCode"},
},
},
})
return tools
}
// startup is called at application startup // startup is called at application startup
func (a *App) startup(ctx context.Context) { func (a *App) startup(ctx context.Context) {
defer PanicHandler() defer PanicHandler()
@ -311,7 +367,7 @@ func (a *App) AddCronTask(follow data.FollowedStock) func() {
return func() { return func() {
go runtime.EventsEmit(a.ctx, "warnMsg", "开始自动分析"+follow.Name+"_"+follow.StockCode) go runtime.EventsEmit(a.ctx, "warnMsg", "开始自动分析"+follow.Name+"_"+follow.StockCode)
ai := data.NewDeepSeekOpenAi(a.ctx) ai := data.NewDeepSeekOpenAi(a.ctx)
msgs := ai.NewChatStream(follow.Name, follow.StockCode, "", nil) msgs := ai.NewChatStream(follow.Name, follow.StockCode, "", nil, a.AiTools)
var res strings.Builder var res strings.Builder
chatId := "" chatId := ""
@ -747,8 +803,13 @@ func (a *App) SendDingDingMessageByType(message string, stockCode string, msgTyp
return data.NewDingDingAPI().SendDingDingMessage(message) return data.NewDingDingAPI().SendDingDingMessage(message)
} }
func (a *App) NewChatStream(stock, stockCode, question string, sysPromptId *int) { func (a *App) NewChatStream(stock, stockCode, question string, sysPromptId *int, enableTools bool) {
msgs := data.NewDeepSeekOpenAi(a.ctx).NewChatStream(stock, stockCode, question, sysPromptId) var msgs <-chan map[string]any
if enableTools {
msgs = data.NewDeepSeekOpenAi(a.ctx).NewChatStream(stock, stockCode, question, sysPromptId, a.AiTools)
} else {
msgs = data.NewDeepSeekOpenAi(a.ctx).NewChatStream(stock, stockCode, question, sysPromptId, []data.Tool{})
}
for msg := range msgs { for msg := range msgs {
runtime.EventsEmit(a.ctx, "newChatStream", msg) runtime.EventsEmit(a.ctx, "newChatStream", msg)
} }
@ -769,6 +830,7 @@ func (a *App) GetVersionInfo() *models.VersionInfo {
Alipay: GetImageBase(alipay), Alipay: GetImageBase(alipay),
Wxpay: GetImageBase(wxpay), Wxpay: GetImageBase(wxpay),
Content: VersionCommit, Content: VersionCommit,
OfficialStatement: OFFICIAL_STATEMENT,
} }
} }
@ -1127,8 +1189,14 @@ func (a *App) GlobalStockIndexes() map[string]any {
return data.NewMarketNewsApi().GlobalStockIndexes(30) return data.NewMarketNewsApi().GlobalStockIndexes(30)
} }
func (a *App) SummaryStockNews(question string, sysPromptId *int) { func (a *App) SummaryStockNews(question string, sysPromptId *int, enableTools bool) {
msgs := data.NewDeepSeekOpenAi(a.ctx).NewSummaryStockNewsStream(question, sysPromptId) var msgs <-chan map[string]any
if enableTools {
msgs = data.NewDeepSeekOpenAi(a.ctx).NewSummaryStockNewsStreamWithTools(question, sysPromptId, a.AiTools)
} else {
msgs = data.NewDeepSeekOpenAi(a.ctx).NewSummaryStockNewsStream(question, sysPromptId)
}
for msg := range msgs { for msg := range msgs {
runtime.EventsEmit(a.ctx, "summaryStockNews", msg) runtime.EventsEmit(a.ctx, "summaryStockNews", msg)
} }

View File

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

View File

@ -599,7 +599,7 @@ func (m MarketNewsApi) XUEQIUHotStock(size int, marketType string) *[]models.Hot
logger.SugaredLogger.Errorf("XUEQIUHotStock err:%s", err.Error()) logger.SugaredLogger.Errorf("XUEQIUHotStock err:%s", err.Error())
return &[]models.HotItem{} return &[]models.HotItem{}
} }
logger.SugaredLogger.Infof("XUEQIUHotStock:%+v", res) //logger.SugaredLogger.Infof("XUEQIUHotStock:%+v", res)
return &res.Data.Items return &res.Data.Items
} }

View File

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

View File

@ -8,14 +8,38 @@ import (
func TestNewDeepSeekOpenAiConfig(t *testing.T) { func TestNewDeepSeekOpenAiConfig(t *testing.T) {
db.Init("../../data/stock.db") db.Init("../../data/stock.db")
var tools []Tool
tools = append(tools, Tool{
Type: "function",
Function: ToolFunction{
Name: "SearchStockByIndicators",
Description: "根据自然语言筛选股票,返回自然语言选股条件要求的股票所有相关数据",
Parameters: FunctionParameters{
Type: "object",
Properties: map[string]any{
"words": map[string]any{
"type": "string",
"description": "选股自然语言,并且条件使用;分隔,或者条件使用,分隔。例如:创新药;PE<30;净利润增长率>50%;",
},
},
Required: []string{"words"},
},
},
})
ai := NewDeepSeekOpenAi(context.TODO()) ai := NewDeepSeekOpenAi(context.TODO())
res := ai.NewChatStream("长电科技", "sh600584", "长电科技分析和总结", nil) //res := ai.NewChatStream("长电科技", "sh600584", "长电科技分析和总结", nil)
res := ai.NewSummaryStockNewsStreamWithTools("总结市场资讯,发掘潜力标的/行业/板块/概念,控制风险,最后按风险登记生成指标选股策略汇总表,每个策略中的指标分号分隔,写成一行", nil, tools)
for { for {
select { select {
case msg := <-res: case msg := <-res:
if len(msg) > 0 {
t.Log(msg) t.Log(msg)
} }
} }
}
} }
func TestGetTopNewsList(t *testing.T) { func TestGetTopNewsList(t *testing.T) {

View File

@ -19,7 +19,7 @@ type SearchStockApi struct {
func NewSearchStockApi(words string) *SearchStockApi { func NewSearchStockApi(words string) *SearchStockApi {
return &SearchStockApi{words: words} return &SearchStockApi{words: words}
} }
func (s SearchStockApi) SearchStock() map[string]any { func (s SearchStockApi) SearchStock(pageSize int) map[string]any {
url := "https://np-tjxg-g.eastmoney.com/api/smart-tag/stock/v3/pw/search-code" 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(). resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "np-tjxg-g.eastmoney.com"). SetHeader("Host", "np-tjxg-g.eastmoney.com").
@ -29,7 +29,7 @@ func (s SearchStockApi) SearchStock() map[string]any {
SetHeader("Content-Type", "application/json"). SetHeader("Content-Type", "application/json").
SetBody(fmt.Sprintf(`{ SetBody(fmt.Sprintf(`{
"keyWord": "%s", "keyWord": "%s",
"pageSize": 50000, "pageSize": %d,
"pageNo": 1, "pageNo": 1,
"fingerprint": "e38b5faabf9378c8238e57219f0ebc9b", "fingerprint": "e38b5faabf9378c8238e57219f0ebc9b",
"gids": [], "gids": [],
@ -43,7 +43,7 @@ func (s SearchStockApi) SearchStock() map[string]any {
"ownSelectAll": false, "ownSelectAll": false,
"dxInfo": [], "dxInfo": [],
"extraCondition": "" "extraCondition": ""
}`, s.words)).Post(url) }`, s.words, pageSize)).Post(url)
if err != nil { if err != nil {
logger.SugaredLogger.Errorf("SearchStock-err:%+v", err) logger.SugaredLogger.Errorf("SearchStock-err:%+v", err)
return map[string]any{} return map[string]any{}
@ -53,3 +53,20 @@ func (s SearchStockApi) SearchStock() map[string]any {
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"]) //logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
return respMap return respMap
} }
func (s SearchStockApi) HotStrategy() map[string]any {
url := fmt.Sprintf("https://np-ipick.eastmoney.com/recommend/stock/heat/ranking?count=20&trace=%d&client=web&biz=web_smart_tag", time.Now().Unix())
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "np-ipick.eastmoney.com").
SetHeader("Origin", "https://xuangu.eastmoney.com").
SetHeader("Referer", "https://xuangu.eastmoney.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("HotStrategy-err:%+v", err)
return map[string]any{}
}
respMap := map[string]any{}
json.Unmarshal(resp.Body(), &respMap)
return respMap
}

View File

@ -9,7 +9,7 @@ import (
func TestSearchStock(t *testing.T) { func TestSearchStock(t *testing.T) {
db.Init("../../data/stock.db") db.Init("../../data/stock.db")
res := NewSearchStockApi("算力股;净利润连续3年增长").SearchStock() res := NewSearchStockApi("算力股;净利润连续3年增长").SearchStock(10)
data := res["data"].(map[string]any) data := res["data"].(map[string]any)
result := data["result"].(map[string]any) result := data["result"].(map[string]any)
dataList := result["dataList"].([]any) dataList := result["dataList"].([]any)
@ -23,3 +23,14 @@ func TestSearchStock(t *testing.T) {
//} //}
} }
func TestSearchStockApi_HotStrategy(t *testing.T) {
db.Init("../../data/stock.db")
res := NewSearchStockApi("").HotStrategy()
logger.SugaredLogger.Infof("res:%+v", res)
dataList := res["data"].([]any)
for _, v := range dataList {
d := v.(map[string]any)
logger.SugaredLogger.Infof("v:%+v", d)
}
}

View File

@ -156,6 +156,7 @@ type VersionInfo struct {
Alipay string `json:"alipay"` Alipay string `json:"alipay"`
Wxpay string `json:"wxpay"` Wxpay string `json:"wxpay"`
BuildTimeStamp int64 `json:"buildTimeStamp"` BuildTimeStamp int64 `json:"buildTimeStamp"`
OfficialStatement string `json:"officialStatement"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"` IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
} }
@ -194,6 +195,12 @@ func (receiver StockInfoUS) TableName() string {
type Resp struct { type Resp struct {
Code int `json:"code"` Code int `json:"code"`
Message string `json:"message"` 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 { type PromptTemplate struct {

View File

@ -27,7 +27,7 @@ import {
StarOutline, StarOutline,
Wallet, WarningOutline, Wallet, WarningOutline,
} from '@vicons/ionicons5' } from '@vicons/ionicons5'
import {AnalyzeSentiment, GetConfig, GetGroupList} from "../wailsjs/go/main/App"; import {AnalyzeSentiment, GetConfig, GetGroupList,GetVersionInfo} from "../wailsjs/go/main/App";
import {Dragon, Fire, Gripfire} from "@vicons/fa"; import {Dragon, Fire, Gripfire} from "@vicons/fa";
import {ReportSearch} from "@vicons/tabler"; import {ReportSearch} from "@vicons/tabler";
import {LocalFireDepartmentRound} from "@vicons/material"; import {LocalFireDepartmentRound} from "@vicons/material";
@ -518,6 +518,12 @@ window.onerror = function (msg, source, lineno, colno, error) {
}; };
onBeforeMount(() => { onBeforeMount(() => {
GetVersionInfo().then(result => {
if(result.officialStatement){
content.value = result.officialStatement+"\n\n"+content.value
}
})
GetGroupList().then(result => { GetGroupList().then(result => {
groupList.value = result groupList.value = result
menuOptions.value.map((item) => { menuOptions.value.map((item) => {

View File

@ -1,21 +1,28 @@
<script setup lang="ts"> <script setup lang="ts">
import {h, onBeforeMount, onMounted, onUnmounted, ref} from 'vue' import {h, onBeforeMount, onMounted, onUnmounted, ref} from 'vue'
import {SearchStock} from "../../wailsjs/go/main/App"; import {SearchStock,GetHotStrategy} from "../../wailsjs/go/main/App";
import {useMessage, NText, NTag} from 'naive-ui' import {useMessage, NText, NTag,NButton} from 'naive-ui'
import {RefreshCircleSharp} from "@vicons/ionicons5";
const message = useMessage() const message = useMessage()
const search = ref('科技股;换手率连续3日大于2') const search = ref('')
const columns = ref([]) const columns = ref([])
const dataList = ref([]) const dataList = ref([])
const hotStrategy = ref([])
const traceInfo = ref('')
function Search() { function Search() {
if(!search.value){
message.warning('请输入选股指标或者要求')
return
}
const loading = message.loading("正在获取选股数据...", {duration: 0}); const loading = message.loading("正在获取选股数据...", {duration: 0});
SearchStock(search.value).then(res => { SearchStock(search.value).then(res => {
loading.destroy() loading.destroy()
//console.log(res) // console.log(res)
if(res.code==100){ if(res.code==100){
message.success(res.msg) 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=>{ columns.value=res.data.result.columns.filter(item=>!item.hiddenNeed&&(item.title!="市场码"&&item.title!="市场简称")).map(item=>{
if(item.children){ if(item.children){
return { return {
title:item.title+(item.unit?'['+item.unit+']':''), title:item.title+(item.unit?'['+item.unit+']':''),
@ -33,7 +40,14 @@ function Search() {
resizable: true, resizable: true,
ellipsis: { ellipsis: {
tooltip: true tooltip: true
},
sorter: (row1, row2) => {
if(isNumeric(row1[item.key])&&isNumeric(row2[item.key])){
return row1[item.key] - row2[item.key];
}else{
return 'default'
} }
},
} }
}) })
} }
@ -42,11 +56,21 @@ function Search() {
title:item.title+(item.unit?'['+item.unit+']':''), title:item.title+(item.unit?'['+item.unit+']':''),
key:item.key, key:item.key,
resizable: true, resizable: true,
minWidth:100, minWidth:120,
ellipsis: { ellipsis: {
tooltip: true 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 dataList.value=res.data.result.dataList
@ -62,36 +86,94 @@ function isNumeric(value) {
} }
onBeforeMount(() => { onBeforeMount(() => {
GetHotStrategy().then(res => {
console.log(res)
if(res.code==1){
hotStrategy.value=res.data
search.value=hotStrategy.value[0].question
Search() 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;
return window.open(
url,
'centeredWindow',
`width=${width},height=${height},left=${left},top=${top},location=no,menubar=no,toolbar=no,display=standalone`
);
}
</script> </script>
<template> <template>
<n-grid :cols="24" style="max-height: calc(100vh - 170px)">
<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> <n-flex>
<n-input-group> <n-input-group style="text-align: left">
<n-input v-model:value="search" placeholder="请输入选股指标或者要求" /> <n-input :rows="1" clearable v-model:value="search" placeholder="请输入选股指标或者要求" />
<n-button type="success" @click="Search">搜索A股</n-button> <n-button type="primary" @click="Search">搜索A股</n-button>
</n-input-group> </n-input-group>
</n-flex> </n-flex>
<!-- <n-table striped size="small">--> <n-flex justify="start" v-if="traceInfo" style="margin: 5px 0">
<!-- <n-thead>-->
<!-- <n-tr>--> <n-ellipsis line-clamp="1" :tooltip="true" >
<!-- <n-th v-for="item in columns">{{item.title}}</n-th>--> <n-text type="info" :bordered="false">选股条件</n-text><n-text type="warning" :bordered="true">{{traceInfo}}</n-text>
<!-- </n-tr>--> <template #tooltip>
<!-- </n-thead>--> <div style="text-align: center;max-width: 580px">
<!-- <n-tbody>--> <n-text type="warning">{{traceInfo}}</n-text>
<!-- <n-tr v-for="(item,index) in dataList">--> </div>
<!-- <n-td v-for="d in columns">{{item[d.key]}}</n-td>--> </template>
<!-- </n-tr>--> </n-ellipsis>
<!-- </n-tbody>-->
<!-- </n-table>--> <!-- <n-button type="primary" size="small">保存策略</n-button>-->
</n-flex>
<n-data-table <n-data-table
:max-height="'calc(100vh - 285px)'" :striped="true"
size="small" :max-height="'calc(100vh - 250px)'"
size="medium"
:columns="columns" :columns="columns"
:data="dataList" :data="dataList"
:pagination="false" :pagination="{pageSize: 9}"
:scroll-x="1800" :scroll-x="1800"
:render-cell="(value, rowData, column) => { :render-cell="(value, rowData, column) => {
@ -112,13 +194,20 @@ onBeforeMount(() => {
return h(NText, { type: type }, { default: () => `${value}` }) return h(NText, { type: type }, { default: () => `${value}` })
}else{ }else{
if(column.key=='SECURITY_SHORT_NAME'){ 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:()=>{
openCenteredWindow(`https://quote.eastmoney.com/concept/${rowData.MARKET_SHORT_NAME}${rowData.SECURITY_CODE}.html`,1240,700)
}}, { default: () => `${value}` })
}else{ }else{
return h(NText, { type: 'info' }, { default: () => `${value}` }) return h(NText, { type: 'info' }, { default: () => `${value}` })
} }
} }
}" }"
/> />
<n-text>共找到<n-tag type="info" :bordered="false">{{dataList.length}}</n-tag>只股</n-text>
</n-gi>
</n-grid>
</template> </template>
<style scoped> <style scoped>

View File

@ -70,6 +70,7 @@ const nowTab = ref("市场快讯")
const indexInterval = ref(null) const indexInterval = ref(null)
const indexIndustryRank = ref(null) const indexIndustryRank = ref(null)
const stockCode= ref('') const stockCode= ref('')
const enableTools= ref(true)
function getIndex() { function getIndex() {
GlobalStockIndexes().then((res) => { GlobalStockIndexes().then((res) => {
@ -186,7 +187,7 @@ function reAiSummary() {
aiSummary.value = "" aiSummary.value = ""
summaryModal.value = true summaryModal.value = true
loading.value = true loading.value = true
SummaryStockNews(question.value, sysPromptId.value) SummaryStockNews(question.value, sysPromptId.value,enableTools.value)
} }
function getAiSummary() { function getAiSummary() {
@ -211,7 +212,7 @@ function getAiSummary() {
aiSummaryTime.value = "" aiSummaryTime.value = ""
aiSummary.value = "" aiSummary.value = ""
modelName.value = "" modelName.value = ""
SummaryStockNews(question.value, sysPromptId.value) //SummaryStockNews(question.value, sysPromptId.value,enableTools.value)
} }
}) })
} }
@ -615,6 +616,17 @@ function ReFlesh(source) {
</n-flex> </n-flex>
</template> </template>
<template #action> <template #action>
<n-flex justify="left" style="margin-bottom: 10px">
<n-switch v-model:value="enableTools" :round="false">
<template #checked>
启用AI函数工具调用
</template>
<template #unchecked>
不启用AI函数工具调用
</template>
</n-switch>
<n-gradient-text type="error" style="margin-left: 10px">*AI函数工具调用可以增强AI获取数据的能力,但会消耗更多tokens</n-gradient-text>
</n-flex>
<n-flex justify="space-between" style="margin-bottom: 10px"> <n-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: 49%" v-model:value="sysPromptId" label-field="name" value-field="ID"
:options="sysPromptOptions" placeholder="请选择系统提示词"/> :options="sysPromptOptions" placeholder="请选择系统提示词"/>

View File

@ -63,6 +63,7 @@ import vueDanmaku from 'vue3-danmaku'
import {keys, padStart} from "lodash"; import {keys, padStart} from "lodash";
import {useRoute, useRouter} from 'vue-router' import {useRoute, useRouter} from 'vue-router'
import MoneyTrend from "./moneyTrend.vue"; import MoneyTrend from "./moneyTrend.vue";
import {TaskTools} from "@vicons/carbon";
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@ -101,6 +102,7 @@ const modalShow3 = ref(false)
const modalShow4 = ref(false) const modalShow4 = ref(false)
const modalShow5 = ref(false) const modalShow5 = ref(false)
const addBTN = ref(true) const addBTN = ref(true)
const enableTools= ref(false)
const formModel = ref({ const formModel = ref({
name: "", name: "",
code: "", code: "",
@ -223,8 +225,11 @@ onMounted(() => {
if (!stocks.value.includes(followedStock.StockCode)) { if (!stocks.value.includes(followedStock.StockCode)) {
stocks.value.push(followedStock.StockCode) stocks.value.push(followedStock.StockCode)
} }
Greet(followedStock.StockCode).then(result => {
updateData(result)
})
} }
monitor() //monitor()
message.destroyAll() message.destroyAll()
}) })
@ -464,6 +469,7 @@ function removeMonitor(code, name, key) {
UnFollow(code).then(result => { UnFollow(code).then(result => {
message.success(result) message.success(result)
monitor()
}) })
} }
@ -581,7 +587,6 @@ async function monitor() {
showPopover.value = true showPopover.value = true
} }
for (let code of stocks.value) { for (let code of stocks.value) {
Greet(code).then(result => { Greet(code).then(result => {
updateData(result) updateData(result)
}) })
@ -590,8 +595,7 @@ async function monitor() {
function GetSortKey(sort, code) { function GetSortKey(sort, code) {
let sortKey = padStart(sort, 8, '0') + "_" + code return padStart(sort, 8, '0') + "_" + code
return sortKey
} }
function onSelect(item) { function onSelect(item) {
@ -1354,7 +1358,7 @@ function aiReCheckStock(stock, stockCode) {
// //
//message.info("sysPromptId:"+data.sysPromptId) //message.info("sysPromptId:"+data.sysPromptId)
NewChatStream(stock, stockCode, data.question, data.sysPromptId) NewChatStream(stock, stockCode, data.question, data.sysPromptId,enableTools.value)
} }
function aiCheckStock(stock, stockCode) { function aiCheckStock(stock, stockCode) {
@ -1383,12 +1387,12 @@ function aiCheckStock(stock, stockCode) {
data.time = "" data.time = ""
data.name = stock data.name = stock
data.code = stockCode data.code = stockCode
data.loading = true data.loading = false
modalShow4.value = true modalShow4.value = true
message.loading("ai检测中...", { // message.loading("ai...", {
duration: 0, // duration: 0,
}) // })
NewChatStream(stock, stockCode, "", data.sysPromptId) // NewChatStream(stock, stockCode, "", data.sysPromptId)
} }
}) })
} }
@ -1573,18 +1577,20 @@ function AddStockGroupInfo(groupId, code, name) {
} }
function updateTab(name) { function updateTab(name) {
stocks.value = []
currentGroupId.value = Number(name) currentGroupId.value = Number(name)
GetFollowList(currentGroupId.value).then(result => { GetFollowList(currentGroupId.value).then(result => {
stocks.value = []
followList.value = result followList.value = result
for (const followedStock of result) { for (const followedStock of result) {
if (followedStock.StockCode.startsWith("us")) { if (followedStock.StockCode.startsWith("us")) {
followedStock.StockCode = "gb_" + followedStock.StockCode.replace("us", "").toLowerCase() followedStock.StockCode = "gb_" + followedStock.StockCode.replace("us", "").toLowerCase()
} }
////console.log("followList",followedStock.StockCode)
stocks.value.push(followedStock.StockCode) stocks.value.push(followedStock.StockCode)
Greet(followedStock.StockCode).then(result => {
updateData(result)
})
} }
monitor() //monitor()
message.destroyAll() message.destroyAll()
}) })
} }
@ -1739,11 +1745,10 @@ function searchStockReport(stockCode) {
@click="removeMonitor(result['股票代码'],result['股票名称'],result.key)"> @click="removeMonitor(result['股票代码'],result['股票名称'],result.key)">
取消关注 取消关注
</n-button>&nbsp; </n-button>&nbsp;
<n-button size="tiny" v-if="data.openAiEnable" secondary type="warning"
@click="aiCheckStock(result['股票名称'],result['股票代码'])"> <n-button size="tiny" v-if="data.openAiEnable" secondary type="warning" @click="aiCheckStock(result['股票名称'],result['股票代码'])">
AI分析 AI分析
</n-button> </n-button>
</template> </template>
<template #footer> <template #footer>
<n-flex justify="center"> <n-flex justify="center">
@ -1876,10 +1881,10 @@ function searchStockReport(stockCode) {
@click="removeMonitor(result['股票代码'],result['股票名称'],result.key)"> @click="removeMonitor(result['股票代码'],result['股票名称'],result.key)">
取消关注 取消关注
</n-button>&nbsp; </n-button>&nbsp;
<n-button size="tiny" v-if="data.openAiEnable" secondary type="warning"
@click="aiCheckStock(result['股票名称'],result['股票代码'])"> <n-button size="tiny" v-if="data.openAiEnable" secondary type="warning" @click="aiCheckStock(result['股票名称'],result['股票代码'])">
AI分析 AI分析
</n-button>&nbsp; </n-button>
<n-button secondary type="error" size="tiny" <n-button secondary type="error" size="tiny"
@click="delStockGroup(result['股票代码'],result['股票名称'],group.ID)">移出分组 @click="delStockGroup(result['股票代码'],result['股票名称'],group.ID)">移出分组
</n-button> </n-button>
@ -2045,7 +2050,7 @@ function searchStockReport(stockCode) {
</n-modal> </n-modal>
<n-modal transform-origin="center" v-model:show="modalShow4" preset="card" style="width: 800px;" <n-modal transform-origin="center" v-model:show="modalShow4" preset="card" style="width: 800px;"
:title="'['+data.name+']AI分析结果'"> :title="'['+data.name+']AI分析'">
<n-spin size="small" :show="data.loading"> <n-spin size="small" :show="data.loading">
<MdEditor v-if="enableEditor" :toolbars="toolbars" ref="mdEditorRef" style="height: 440px;text-align: left" <MdEditor v-if="enableEditor" :toolbars="toolbars" ref="mdEditorRef" style="height: 440px;text-align: left"
:modelValue="data.airesult" :theme="theme"> :modelValue="data.airesult" :theme="theme">
@ -2069,7 +2074,17 @@ function searchStockReport(stockCode) {
</n-flex> </n-flex>
</template> </template>
<template #action> <template #action>
<n-flex justify="left" style="margin-bottom: 10px">
<n-switch v-model:value="enableTools" :round="false">
<template #checked>
启用AI函数工具调用
</template>
<template #unchecked>
不启用AI函数工具调用
</template>
</n-switch>
<n-gradient-text type="error" style="margin-left: 10px">*AI函数工具调用可以增强AI获取数据的能力,但会消耗更多tokens</n-gradient-text>
</n-flex>
<n-flex justify="space-between" style="margin-bottom: 10px"> <n-flex justify="space-between" style="margin-bottom: 10px">
<n-select style="width: 49%" v-model:value="data.sysPromptId" label-field="name" value-field="ID" <n-select style="width: 49%" v-model:value="data.sysPromptId" label-field="name" value-field="ID"
:options="sysPromptOptions" placeholder="请选择系统提示词"/> :options="sysPromptOptions" placeholder="请选择系统提示词"/>
@ -2087,7 +2102,7 @@ function searchStockReport(stockCode) {
}" }"
/> />
<!-- <n-button size="tiny" type="error" @click="enableEditor=!enableEditor">编辑/预览</n-button>--> <!-- <n-button size="tiny" type="error" @click="enableEditor=!enableEditor">编辑/预览</n-button>-->
<n-button size="tiny" type="warning" @click="aiReCheckStock(data.name,data.code)">再次分析</n-button> <n-button size="tiny" type="warning" @click="aiReCheckStock(data.name,data.code)">开始AI分析</n-button>
<n-button size="tiny" type="info" @click="saveAsImage(data.name,data.code)">保存为图片</n-button> <n-button size="tiny" type="info" @click="saveAsImage(data.name,data.code)">保存为图片</n-button>
<n-button size="tiny" type="success" @click="copyToClipboard">复制到剪切板</n-button> <n-button size="tiny" type="success" @click="copyToClipboard">复制到剪切板</n-button>
<n-button size="tiny" type="primary" @click="saveAsMarkdown">保存为Markdown文件</n-button> <n-button size="tiny" type="primary" @click="saveAsMarkdown">保存为Markdown文件</n-button>

View File

@ -39,6 +39,8 @@ export function GetGroupList():Promise<Array<data.Group>>;
export function GetGroupStockList(arg1:number):Promise<Array<data.GroupStock>>; export function GetGroupStockList(arg1:number):Promise<Array<data.GroupStock>>;
export function GetHotStrategy():Promise<Record<string, any>>;
export function GetIndustryMoneyRankSina(arg1:string,arg2:string):Promise<Array<Record<string, any>>>; export function GetIndustryMoneyRankSina(arg1:string,arg2:string):Promise<Array<Record<string, any>>>;
export function GetIndustryRank(arg1:string,arg2:number):Promise<Array<any>>; export function GetIndustryRank(arg1:string,arg2:number):Promise<Array<any>>;
@ -79,7 +81,7 @@ export function InvestCalendarTimeLine(arg1:string):Promise<Array<any>>;
export function LongTigerRank(arg1:string):Promise<any>; export function LongTigerRank(arg1:string):Promise<any>;
export function NewChatStream(arg1:string,arg2:string,arg3:string,arg4:any):Promise<void>; export function NewChatStream(arg1:string,arg2:string,arg3:string,arg4:any,arg5:boolean):Promise<void>;
export function NewsPush(arg1:any):Promise<void>; export function NewsPush(arg1:any):Promise<void>;
@ -113,7 +115,7 @@ export function StockNotice(arg1:string):Promise<Array<any>>;
export function StockResearchReport(arg1:string):Promise<Array<any>>; export function StockResearchReport(arg1:string):Promise<Array<any>>;
export function SummaryStockNews(arg1:string,arg2:any):Promise<void>; export function SummaryStockNews(arg1:string,arg2:any,arg3:boolean):Promise<void>;
export function UnFollow(arg1:string):Promise<string>; export function UnFollow(arg1:string):Promise<string>;

View File

@ -74,6 +74,10 @@ export function GetGroupStockList(arg1) {
return window['go']['main']['App']['GetGroupStockList'](arg1); return window['go']['main']['App']['GetGroupStockList'](arg1);
} }
export function GetHotStrategy() {
return window['go']['main']['App']['GetHotStrategy']();
}
export function GetIndustryMoneyRankSina(arg1, arg2) { export function GetIndustryMoneyRankSina(arg1, arg2) {
return window['go']['main']['App']['GetIndustryMoneyRankSina'](arg1, arg2); return window['go']['main']['App']['GetIndustryMoneyRankSina'](arg1, arg2);
} }
@ -154,8 +158,8 @@ export function LongTigerRank(arg1) {
return window['go']['main']['App']['LongTigerRank'](arg1); return window['go']['main']['App']['LongTigerRank'](arg1);
} }
export function NewChatStream(arg1, arg2, arg3, arg4) { export function NewChatStream(arg1, arg2, arg3, arg4, arg5) {
return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3, arg4); return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3, arg4, arg5);
} }
export function NewsPush(arg1) { export function NewsPush(arg1) {
@ -222,8 +226,8 @@ export function StockResearchReport(arg1) {
return window['go']['main']['App']['StockResearchReport'](arg1); return window['go']['main']['App']['StockResearchReport'](arg1);
} }
export function SummaryStockNews(arg1, arg2) { export function SummaryStockNews(arg1, arg2, arg3) {
return window['go']['main']['App']['SummaryStockNews'](arg1, arg2); return window['go']['main']['App']['SummaryStockNews'](arg1, arg2, arg3);
} }
export function UnFollow(arg1) { export function UnFollow(arg1) {

View File

@ -701,6 +701,7 @@ export namespace models {
alipay: string; alipay: string;
wxpay: string; wxpay: string;
buildTimeStamp: number; buildTimeStamp: number;
officialStatement: string;
IsDel: number; IsDel: number;
static createFrom(source: any = {}) { static createFrom(source: any = {}) {
@ -719,6 +720,7 @@ export namespace models {
this.alipay = source["alipay"]; this.alipay = source["alipay"];
this.wxpay = source["wxpay"]; this.wxpay = source["wxpay"];
this.buildTimeStamp = source["buildTimeStamp"]; this.buildTimeStamp = source["buildTimeStamp"];
this.officialStatement = source["officialStatement"];
this.IsDel = source["IsDel"]; this.IsDel = source["IsDel"];
} }

View File

@ -53,6 +53,7 @@ var stocksBinUS []byte
var Version string var Version string
var VersionCommit string var VersionCommit string
var OFFICIAL_STATEMENT string
func main() { func main() {
checkDir("data") checkDir("data")