feat(frontend):优化AI聊天功能并添加新功能

- 新增用户自定义问题输入功能
- 优化 AI回答的展示逻辑
- 添加错误处理和提示
- 更新后端接口以支持新功能
This commit is contained in:
spark 2025-02-16 21:56:07 +08:00
parent dab51f7a70
commit 5ee1ae4a32
9 changed files with 161 additions and 155 deletions

8
app.go
View File

@ -437,16 +437,16 @@ func (a *App) SendDingDingMessageByType(message string, stockCode string, msgTyp
return data.NewDingDingAPI().SendDingDingMessage(message)
}
func (a *App) NewChatStream(stock, stockCode string) {
msgs := data.NewDeepSeekOpenAi(a.ctx).NewChatStream(stock, stockCode)
func (a *App) NewChatStream(stock, stockCode, question string) {
msgs := data.NewDeepSeekOpenAi(a.ctx).NewChatStream(stock, stockCode, question)
for msg := range msgs {
runtime.EventsEmit(a.ctx, "newChatStream", msg)
}
runtime.EventsEmit(a.ctx, "newChatStream", "DONE")
}
func (a *App) SaveAIResponseResult(stockCode, stockName, result string) {
data.NewDeepSeekOpenAi(a.ctx).SaveAIResponseResult(stockCode, stockName, result)
func (a *App) SaveAIResponseResult(stockCode, stockName, result, chatId, question string) {
data.NewDeepSeekOpenAi(a.ctx).SaveAIResponseResult(stockCode, stockName, result, chatId, question)
}
func (a *App) GetAIResponseResult(stock string) *models.AIResponseResult {
return data.NewDeepSeekOpenAi(a.ctx).GetAIResponseResult(stock)

View File

@ -91,20 +91,23 @@ func TestGetHtmlWithActions(t *testing.T) {
actions := []chromedp.Action{
chromedp.Navigate("https://gushitong.baidu.com/stock/ab-600745"),
chromedp.WaitVisible("div.cos-tab"),
chromedp.Click("div.cos-tab:nth-child(5)", chromedp.ByQuery),
chromedp.ScrollIntoView("div.body-box"),
chromedp.WaitVisible("div.body-col"),
chromedp.Click(".header div.cos-tab:nth-child(6)", chromedp.ByQuery),
chromedp.ScrollIntoView("div.finance-container >div.row:nth-child(3)"),
chromedp.WaitVisible("div.cos-tabs-header-container"),
chromedp.Click(".page-content .cos-tabs-header-container .cos-tabs-header .cos-tab:nth-child(1)", chromedp.ByQuery),
chromedp.WaitVisible(".page-content .finance-container .report-col-content", chromedp.ByQuery),
chromedp.Click(".page-content .cos-tabs-header-container .cos-tabs-header .cos-tab:nth-child(4)", chromedp.ByQuery),
chromedp.Evaluate(`window.scrollTo(0, document.body.scrollHeight);`, nil),
chromedp.Sleep(1 * time.Second),
}
htmlContent, success := crawlerAPI.GetHtmlWithActions(&actions, true)
htmlContent, success := crawlerAPI.GetHtmlWithActions(&actions, false)
if success {
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
var messages []string
document.Find("div.finance-hover,div.list-date").Each(func(i int, selection *goquery.Selection) {
document.Find("div.report-table-list-container,div.report-row").Each(func(i int, selection *goquery.Selection) {
text := strutil.RemoveWhiteSpace(selection.Text(), false)
messages = append(messages, text)
logger.SugaredLogger.Infof("搜索到消息-%s: %s", "", text)

View File

@ -89,8 +89,8 @@ type AiResponse struct {
SystemFingerprint string `json:"system_fingerprint"`
}
func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
ch := make(chan string, 512)
func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string) <-chan map[string]any {
ch := make(chan map[string]any, 512)
defer func() {
if err := recover(); err != nil {
@ -115,24 +115,30 @@ func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
},
}
replaceTemplates := map[string]string{
"{{stockName}}": RemoveAllBlankChar(stock),
"{{stockCode}}": RemoveAllBlankChar(stockCode),
question := ""
if userQuestion == "" {
replaceTemplates := map[string]string{
"{{stockName}}": RemoveAllBlankChar(stock),
"{{stockCode}}": RemoveAllBlankChar(stockCode),
}
followedStock := &FollowedStock{
StockCode: stockCode,
}
db.Dao.Model(&followedStock).Where("stock_code = ?", stockCode).First(followedStock)
if followedStock.CostPrice > 0 {
replaceTemplates["{{costPrice}}"] = fmt.Sprintf("%.2f", followedStock.CostPrice)
}
question = strutil.ReplaceWithMap(o.QuestionTemplate, replaceTemplates)
} else {
question = userQuestion
}
followedStock := &FollowedStock{
StockCode: stockCode,
}
db.Dao.Model(&followedStock).Where("stock_code = ?", stockCode).First(followedStock)
if followedStock.CostPrice > 0 {
replaceTemplates["{{costPrice}}"] = fmt.Sprintf("%.2f", followedStock.CostPrice)
}
question := strutil.ReplaceWithMap(o.QuestionTemplate, replaceTemplates)
logger.SugaredLogger.Infof("NewChatStream stock:%s stockCode:%s", stock, stockCode)
logger.SugaredLogger.Infof("Prompt%s", o.Prompt)
logger.SugaredLogger.Infof("User Prompt config:%v", o.QuestionTemplate)
logger.SugaredLogger.Infof("User question:%s", question)
logger.SugaredLogger.Infof("User question:%s", userQuestion)
logger.SugaredLogger.Infof("final question:%s", question)
wg := &sync.WaitGroup{}
wg.Add(5)
@ -141,7 +147,12 @@ func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
messages := SearchStockPriceInfo(stockCode, o.CrawlTimeOut)
if messages == nil || len(*messages) == 0 {
logger.SugaredLogger.Error("获取股票价格失败")
ch <- "***❗获取股票价格失败,分析结果可能不准确***<hr>"
//ch <- "***❗获取股票价格失败,分析结果可能不准确***<hr>"
ch <- map[string]any{
"code": 1,
"question": question,
"extraContent": "***❗获取股票价格失败,分析结果可能不准确***<hr>",
}
go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取股票价格失败,分析结果可能不准确")
return
}
@ -157,10 +168,20 @@ func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
go func() {
defer wg.Done()
if checkIsIndexBasic(stock) {
return
}
messages := GetFinancialReports(stockCode, o.CrawlTimeOut)
if messages == nil || len(*messages) == 0 {
logger.SugaredLogger.Error("获取股票财报失败")
ch <- "***❗获取股票财报失败,分析结果可能不准确***<hr>"
// "***❗获取股票财报失败,分析结果可能不准确***<hr>"
ch <- map[string]any{
"code": 1,
"question": question,
"extraContent": "***❗获取股票财报失败,分析结果可能不准确***<hr>",
}
go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取股票财报失败,分析结果可能不准确")
return
}
@ -237,6 +258,11 @@ func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
go func() {
defer wg.Done()
if checkIsIndexBasic(stock) {
return
}
messages := SearchGuShiTongStockInfo(stockCode, o.CrawlTimeOut)
if messages == nil || len(*messages) == 0 {
logger.SugaredLogger.Error("获取股势通资讯失败")
@ -281,7 +307,12 @@ func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
defer body.Close()
if err != nil {
logger.SugaredLogger.Infof("Stream error : %s", err.Error())
ch <- err.Error()
//ch <- err.Error()
ch <- map[string]any{
"code": 0,
"question": question,
"content": err.Error(),
}
return
}
@ -296,6 +327,8 @@ func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
}
var streamResponse struct {
Id string `json:"id"`
Model string `json:"model"`
Choices []struct {
Delta struct {
Content string `json:"content"`
@ -308,11 +341,27 @@ func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
if err := json.Unmarshal([]byte(data), &streamResponse); err == nil {
for _, choice := range streamResponse.Choices {
if content := choice.Delta.Content; content != "" {
ch <- content
//ch <- content
ch <- map[string]any{
"code": 1,
"question": question,
"chatId": streamResponse.Id,
"model": streamResponse.Model,
"content": content,
}
logger.SugaredLogger.Infof("Content data: %s", content)
}
if reasoningContent := choice.Delta.ReasoningContent; reasoningContent != "" {
ch <- reasoningContent
//ch <- reasoningContent
ch <- map[string]any{
"code": 1,
"question": question,
"chatId": streamResponse.Id,
"model": streamResponse.Model,
"content": reasoningContent,
}
logger.SugaredLogger.Infof("ReasoningContent data: %s", reasoningContent)
}
if choice.FinishReason == "stop" {
@ -322,14 +371,33 @@ func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
} else {
if err != nil {
logger.SugaredLogger.Infof("Stream data error : %s", err.Error())
ch <- 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 <- data
ch <- map[string]any{
"code": 0,
"question": question,
"content": data,
}
}
}
} else {
ch <- line
if strutil.RemoveNonPrintable(line) != "" {
//ch <- line
ch <- map[string]any{
"code": 0,
"question": question,
"content": line,
}
logger.SugaredLogger.Infof("Stream data error : %s", line)
}
}
}
@ -337,6 +405,12 @@ func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
return ch
}
func checkIsIndexBasic(stock string) bool {
count := int64(0)
db.Dao.Model(&IndexBasic{}).Where("name = ?", stock).Count(&count)
return count > 0
}
func SearchGuShiTongStockInfo(stock string, crawlTimeOut int64) *[]string {
crawlerAPI := CrawlerApi{}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(crawlTimeOut)*time.Second)
@ -463,116 +537,6 @@ func GetFinancialReports(stockCode string, crawlTimeOut int64) *[]string {
return &messages
}
func (o OpenAi) NewCommonChatStream(stock, stockCode, apiURL, apiKey, Model string) <-chan string {
ch := make(chan string)
go func() {
defer close(ch)
client := resty.New()
client.SetHeader("Authorization", "Bearer "+apiKey)
client.SetHeader("Content-Type", "application/json")
client.SetRetryCount(3)
msg := []map[string]interface{}{
{
"role": "system",
"content": o.Prompt,
},
}
wg := &sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
messages := SearchStockPriceInfo(stockCode, o.CrawlTimeOut)
price := ""
for _, message := range *messages {
price += message + ";"
}
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": stock + "当前价格:" + price,
})
}()
//go func() {
// defer wg.Done()
// messages := SearchStockInfo(stock, "depth")
// for _, message := range *messages {
// msg = append(msg, map[string]interface{}{
// "role": "assistant",
// "content": message,
// })
// }
//}()
//go func() {
// defer wg.Done()
// messages := SearchStockInfo(stock, "telegram")
// for _, message := range *messages {
// msg = append(msg, map[string]interface{}{
// "role": "assistant",
// "content": message,
// })
// }
//}()
wg.Wait()
msg = append(msg, map[string]interface{}{
"role": "user",
"content": stock + "分析和总结",
})
resp, err := client.R().
SetDoNotParseResponse(true).
SetBody(map[string]interface{}{
"model": Model,
"max_tokens": o.MaxTokens,
"temperature": o.Temperature,
"stream": true,
"messages": msg,
}).
Post(apiURL)
if err != nil {
ch <- err.Error()
return
}
defer resp.RawBody().Close()
scanner := bufio.NewScanner(resp.RawBody())
for scanner.Scan() {
line := scanner.Text()
logger.SugaredLogger.Infof("Received data: %s", line)
if strings.HasPrefix(line, "data:") {
data := strings.TrimPrefix(line, "data:")
if data == "[DONE]" {
return
}
var streamResponse struct {
Choices []struct {
Delta struct {
Content string `json:"content"`
} `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
}
if choice.FinishReason == "stop" {
return
}
}
}
}
}
}()
return ch
}
func GetTelegraphList(crawlTimeOut int64) *[]string {
url := "https://www.cls.cn/telegraph"
response, err := resty.New().SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
@ -617,12 +581,14 @@ func GetTopNewsList(crawlTimeOut int64) *[]string {
return &telegraph
}
func (o OpenAi) SaveAIResponseResult(stockCode, stockName, result string) {
func (o OpenAi) SaveAIResponseResult(stockCode, stockName, result, chatId, question string) {
db.Dao.Create(&models.AIResponseResult{
StockCode: stockCode,
StockName: stockName,
ModelName: o.Model,
Content: result,
ChatId: chatId,
Question: question,
})
}

View File

@ -135,9 +135,11 @@ type Commit struct {
type AIResponseResult struct {
gorm.Model
ChatId string `json:"chatId"`
ModelName string `json:"modelName"`
StockCode string `json:"stockCode"`
StockName string `json:"stockName"`
Question string `json:"question"`
Content string `json:"content"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
}

View File

@ -179,7 +179,9 @@ window.onerror = function (msg, source, lineno, colno, error) {
<n-message-provider >
<n-notification-provider>
<n-modal-provider>
<n-watermark
<n-dialog-provider>
<n-watermark
:content="content"
cross
selectable
@ -218,6 +220,7 @@ window.onerror = function (msg, source, lineno, colno, error) {
</n-flex>
</n-watermark>
</n-dialog-provider>
</n-modal-provider>
</n-notification-provider>
</n-message-provider>

View File

@ -19,6 +19,7 @@ import {
NInputNumber,
NText,
useMessage,
useDialog,
useModal,
useNotification
} from 'naive-ui'
@ -32,7 +33,7 @@ const mdPreviewRef = ref(null)
const message = useMessage()
const modal = useModal()
const notify = useNotification()
const dialog = useDialog()
const stocks=ref([])
const results=ref({})
const ticker=ref({})
@ -56,6 +57,8 @@ const formModel = ref({
const data = reactive({
modelName:"",
chatId: "",
question:"",
name: "",
code: "",
fenshiURL:"",
@ -160,11 +163,19 @@ EventsOn("newChatStream",async (msg) => {
//console.log("newChatStream:->",data.airesult)
data.loading = false
if (msg === "DONE") {
SaveAIResponseResult(data.code, data.name, data.airesult)
SaveAIResponseResult(data.code, data.name, data.airesult, data.chatId,data.question)
message.info("AI分析完成")
message.destroyAll()
} else {
data.airesult = data.airesult + msg
if(msg.code===1){
if(msg.chatId){
data.chatId = msg.chatId
}
if(msg.question){
data.question = msg.question
}
data.airesult = data.airesult + (msg.content||msg.extraContent)
}
}
})
@ -496,13 +507,17 @@ function aiReCheckStock(stock,stockCode) {
message.loading("ai检测中...",{
duration: 0,
})
NewChatStream(stock,stockCode)
//
NewChatStream(stock,stockCode,data.question)
}
function aiCheckStock(stock,stockCode){
GetAIResponseResult(stockCode).then(result => {
if(result.content){
data.modelName=result.modelName
data.chatId=result.chatId
data.question=result.question
data.name=stock
data.code=stockCode
data.loading=false
@ -747,12 +762,25 @@ function saveAsMarkdown() {
</template>
<template #footer>
<n-flex justify="space-between">
<n-text type="info" v-if="data.time" ><n-tag v-if="data.modelName" type="warning" round :bordered="false">{{data.modelName}}</n-tag>{{data.time}}</n-text>
<n-text type="info" v-if="data.time" >
<n-tag v-if="data.modelName" type="warning" round :title="data.chatId" :bordered="false">{{data.modelName}}</n-tag>
{{data.time}}
</n-text>
<n-text type="error" >*AI分析结果仅供参考请以实际行情为准投资需谨慎风险自担</n-text>
</n-flex>
</template>
<template #action>
<n-flex justify="right">
<n-input v-model:value="data.question"
type="textarea"
:show-count="true"
placeholder="请输入您的问题:例如{{stockName}}[{{stockCode}}]分析和总结"
:autosize="{
minRows: 2,
maxRows: 5
}"
/>
<n-button size="tiny" type="warning" @click="aiReCheckStock(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>

View File

@ -19,9 +19,9 @@ export function GetVersionInfo():Promise<models.VersionInfo>;
export function Greet(arg1:string):Promise<data.StockInfo>;
export function NewChatStream(arg1:string,arg2:string):Promise<void>;
export function NewChatStream(arg1:string,arg2:string,arg3:string):Promise<void>;
export function SaveAIResponseResult(arg1:string,arg2:string,arg3:string):Promise<void>;
export function SaveAIResponseResult(arg1:string,arg2:string,arg3:string,arg4:string,arg5:string):Promise<void>;
export function SendDingDingMessage(arg1:string,arg2:string):Promise<string>;

View File

@ -34,12 +34,12 @@ export function Greet(arg1) {
return window['go']['main']['App']['Greet'](arg1);
}
export function NewChatStream(arg1, arg2) {
return window['go']['main']['App']['NewChatStream'](arg1, arg2);
export function NewChatStream(arg1, arg2, arg3) {
return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3);
}
export function SaveAIResponseResult(arg1, arg2, arg3) {
return window['go']['main']['App']['SaveAIResponseResult'](arg1, arg2, arg3);
export function SaveAIResponseResult(arg1, arg2, arg3, arg4, arg5) {
return window['go']['main']['App']['SaveAIResponseResult'](arg1, arg2, arg3, arg4, arg5);
}
export function SendDingDingMessage(arg1, arg2) {

View File

@ -343,9 +343,11 @@ export namespace models {
UpdatedAt: any;
// Go type: gorm
DeletedAt: any;
chatId: string;
modelName: string;
stockCode: string;
stockName: string;
question: string;
content: string;
IsDel: number;
@ -359,9 +361,11 @@ export namespace models {
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
this.DeletedAt = this.convertValues(source["DeletedAt"], null);
this.chatId = source["chatId"];
this.modelName = source["modelName"];
this.stockCode = source["stockCode"];
this.stockName = source["stockName"];
this.question = source["question"];
this.content = source["content"];
this.IsDel = source["IsDel"];
}