Compare commits

...

2 Commits

Author SHA1 Message Date
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
10 changed files with 168 additions and 16 deletions

4
app.go
View File

@@ -272,7 +272,7 @@ func (a *App) CheckUpdate(flag int) {
}
body := resp.Body()
if len(body) < 1024 {
if len(body) < 1024*500 {
go runtime.EventsEmit(a.ctx, "newsPush", map[string]any{
"time": "新版本:" + releaseVersion.TagName,
"isRed": true,
@@ -438,7 +438,7 @@ func (a *App) domReady(ctx context.Context) {
//检查新版本
go func() {
a.CheckUpdate(0)
//a.CheckStockBaseInfo(a.ctx)
a.CheckStockBaseInfo(a.ctx)
a.cron.AddFunc("30 05 8,12,20 * * *", func() {
logger.SugaredLogger.Errorf("Checking for updates...")
a.CheckUpdate(0)

View File

@@ -551,9 +551,14 @@ func (m MarketNewsApi) EMDictCode(code string, cache *freecache.Cache) []any {
}
func (m MarketNewsApi) TradingViewNews() *[]models.TVNews {
client := resty.New()
config := GetSettingConfig()
if config.HttpProxyEnabled && config.HttpProxy != "" {
client.SetProxy(config.HttpProxy)
}
TVNews := &[]models.TVNews{}
url := "https://news-mediator.tradingview.com/news-flow/v2/news?filter=lang:zh-Hans&filter=provider:panews,reuters&client=screener&streaming=false"
resp, err := resty.New().SetProxy("http://127.0.0.1:10809").SetTimeout(time.Duration(30)*time.Second).R().
resp, err := client.SetTimeout(time.Duration(5)*time.Second).R().
SetHeader("Host", "news-mediator.tradingview.com").
SetHeader("Origin", "https://cn.tradingview.com").
SetHeader("Referer", "https://cn.tradingview.com/").
@@ -833,7 +838,7 @@ func (m MarketNewsApi) GetPMI() *models.PMIResp {
return res
}
func (m MarketNewsApi) GetIndustryReportInfo(infoCode string) {
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").
@@ -843,7 +848,7 @@ func (m MarketNewsApi) GetIndustryReportInfo(infoCode string) {
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("GetIndustryReportInfo err:%s", err.Error())
return
return ""
}
body := resp.Body()
//logger.SugaredLogger.Debugf("GetIndustryReportInfo:%s", body)
@@ -853,7 +858,25 @@ func (m MarketNewsApi) GetIndustryReportInfo(infoCode string) {
//logger.SugaredLogger.Infof("GetIndustryReportInfo:\n%s\n%s", title, content)
markdown, err := util.HTMLToMarkdown(title + content)
if err != nil {
return
return ""
}
logger.SugaredLogger.Infof("GetIndustryReportInfo markdown:\n%s", markdown)
return markdown
}
func (m MarketNewsApi) ReutersNew() {
url := "https://www.reuters.com/pf/api/v3/content/fetch/articles-by-section-alias-or-id-v1?query=%7B%22arc-site%22%3A%22reuters%22%2C%22fetch_type%22%3A%22collection%22%2C%22offset%22%3A20%2C%22section_id%22%3A%22%2Fworld%2Fchina%2F%22%2C%22size%22%3A9%2C%22uri%22%3A%22%2Fworld%2Fchina%2F%22%2C%22website%22%3A%22reuters%22%7D&d=300&mxId=00000000&_website=reuters"
resp, err := resty.New().SetProxy("http://127.0.0.1:10809").SetTimeout(time.Duration(30)*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").
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("ReutersNew err:%s", err.Error())
return
}
body := resp.Body()
logger.SugaredLogger.Debugf("ReutersNew:%s", body)
}

View File

@@ -79,11 +79,13 @@ func TestStockResearchReport(t *testing.T) {
func TestIndustryResearchReport(t *testing.T) {
db.Init("../../data/stock.db")
resp := NewMarketNewsApi().IndustryResearchReport("", 7)
resp := NewMarketNewsApi().IndustryResearchReport("735", 7)
for _, a := range resp {
logger.SugaredLogger.Debugf("value: %+v", a)
data := a.(map[string]any)
logger.SugaredLogger.Debugf("value: %s infoCode:%s", data["title"], data["infoCode"])
NewMarketNewsApi().GetIndustryReportInfo(data["infoCode"].(string))
}
}
func TestStockNotice(t *testing.T) {
@@ -101,6 +103,11 @@ func TestEMDictCode(t *testing.T) {
for _, a := range resp {
logger.SugaredLogger.Debugf("value: %+v", a)
}
bytes, err := json.Marshal(resp)
if err != nil {
return
}
logger.SugaredLogger.Debugf("value: %s", string(bytes))
}
@@ -108,7 +115,7 @@ func TestTradingViewNews(t *testing.T) {
db.Init("../../data/stock.db")
resp := NewMarketNewsApi().TradingViewNews()
for _, a := range *resp {
logger.SugaredLogger.Debugf("value: %+v", a)
logger.SugaredLogger.Debugf("value: %s", a.Title)
}
}
@@ -204,3 +211,7 @@ func TestGetPMI(t *testing.T) {
func TestGetIndustryReportInfo(t *testing.T) {
NewMarketNewsApi().GetIndustryReportInfo("AP202507151709216483")
}
func TestReutersNew(t *testing.T) {
NewMarketNewsApi().ReutersNew()
}

View File

@@ -338,7 +338,7 @@ func (o *OpenAi) NewSummaryStockNewsStream(userQuestion string, sysPromptId *int
"content": "当前本地时间是:" + time.Now().Format("2006-01-02 15:04:05"),
})
wg := &sync.WaitGroup{}
wg.Add(1)
wg.Add(2)
go func() {
defer wg.Done()
var market strings.Builder
@@ -355,6 +355,25 @@ func (o *OpenAi) NewSummaryStockNewsStream(userQuestion string, sysPromptId *int
"content": "当前市场指数行情情况如下:\n" + market.String(),
})
}()
go func() {
defer wg.Done()
resp := NewMarketNewsApi().TradingViewNews()
var newsText strings.Builder
for _, a := range *resp {
logger.SugaredLogger.Debugf("value: %s", a.Title)
newsText.WriteString(a.Title + "\n")
}
msg = append(msg, map[string]interface{}{
"role": "user",
"content": "外媒全球新闻资讯",
})
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": newsText.String(),
})
}()
wg.Wait()
news := NewMarketNewsApi().GetNewsList("", 100)
@@ -468,9 +487,8 @@ func (o *OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptI
logger.SugaredLogger.Infof("NewChatStream stock:%s stockCode:%s", stock, stockCode)
logger.SugaredLogger.Infof("Prompt%s", sysPrompt)
logger.SugaredLogger.Infof("final question:%s", question)
wg := &sync.WaitGroup{}
wg.Add(7)
wg.Add(8)
go func() {
defer wg.Done()
@@ -714,6 +732,25 @@ func (o *OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptI
})
}()
go func() {
defer wg.Done()
resp := NewMarketNewsApi().TradingViewNews()
var newsText strings.Builder
for _, a := range *resp {
logger.SugaredLogger.Debugf("value: %s", a.Title)
newsText.WriteString(a.Title + "\n")
}
msg = append(msg, map[string]interface{}{
"role": "user",
"content": "外媒全球新闻资讯",
})
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": newsText.String(),
})
}()
wg.Wait()
msg = append(msg, map[string]interface{}{
"role": "user",

View File

@@ -32,6 +32,8 @@ type Settings struct {
EnableFund bool `json:"enableFund"`
EnablePushNews bool `json:"enablePushNews"`
SponsorCode string `json:"sponsorCode"`
HttpProxy string `json:"httpProxy"`
HttpProxyEnabled bool `json:"httpProxyEnabled"`
}
func (receiver Settings) TableName() string {
@@ -99,6 +101,8 @@ func UpdateConfig(s *SettingConfig) string {
"enable_fund": s.EnableFund,
"enable_push_news": s.EnablePushNews,
"sponsor_code": s.SponsorCode,
"http_proxy": s.HttpProxy,
"http_proxy_enabled": s.HttpProxyEnabled,
})
//更新AiConfig

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

@@ -465,3 +465,38 @@ 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"
}

View File

@@ -42,6 +42,8 @@ const formValue = ref({
enableFund: false,
enablePushNews: false,
sponsorCode: "",
httpProxy:"",
httpProxyEnabled:false,
})
// 添加一个新的AI配置到列表
@@ -97,6 +99,8 @@ onMounted(() => {
formValue.value.enableFund = res.enableFund
formValue.value.enablePushNews = res.enablePushNews
formValue.value.sponsorCode = res.sponsorCode
formValue.value.httpProxy=res.httpProxy;
formValue.value.httpProxyEnabled=res.httpProxyEnabled;
})
GetPromptTemplates("", "").then(res => {
@@ -131,7 +135,9 @@ function saveConfig() {
darkTheme: formValue.value.darkTheme,
enableFund: formValue.value.enableFund,
enablePushNews: formValue.value.enablePushNews,
sponsorCode: formValue.value.sponsorCode
sponsorCode: formValue.value.sponsorCode,
httpProxy:formValue.value.httpProxy,
httpProxyEnabled:formValue.value.httpProxyEnabled,
})
if (config.sponsorCode) {
@@ -218,6 +224,8 @@ function importConfig() {
formValue.value.enableFund = config.enableFund
formValue.value.enablePushNews = config.enablePushNews
formValue.value.sponsorCode = config.sponsorCode
formValue.value.httpProxy=config.httpProxy
formValue.value.httpProxyEnabled=config.httpProxyEnabled
};
reader.readAsText(file);
};
@@ -354,10 +362,18 @@ function deletePrompt(ID) {
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="6" v-if="formValue.openAI.enable" title="天数越多消耗tokens越多"
<n-form-item-gi :span="4" v-if="formValue.openAI.enable" title="天数越多消耗tokens越多"
label="日K线数据(天)" path="openAI.kDays">
<n-input-number min="30" step="1" max="365" v-model:value="formValue.openAI.kDays"/>
</n-form-item-gi>
<n-form-item-gi :span="2" label="http代理" path="httpProxyEnabled">
<n-switch v-model:value="formValue.httpProxyEnabled"/>
</n-form-item-gi>
<n-form-item-gi :span="10" v-if="formValue.httpProxyEnabled" title="http代理地址"
label="http代理地址" path="httpProxy">
<n-input type="text" placeholder="http代理地址" v-model:value="formValue.httpProxy" clearable/>
</n-form-item-gi>
<n-gi :span="24" v-if="formValue.openAI.enable">
<n-divider title-placement="left">Prompt 内容设置</n-divider>

View File

@@ -390,6 +390,8 @@ export namespace data {
enableFund: boolean;
enablePushNews: boolean;
sponsorCode: string;
httpProxy: string;
httpProxyEnabled: boolean;
aiConfigs: AIConfig[];
static createFrom(source: any = {}) {
@@ -422,6 +424,8 @@ export namespace data {
this.enableFund = source["enableFund"];
this.enablePushNews = source["enablePushNews"];
this.sponsorCode = source["sponsorCode"];
this.httpProxy = source["httpProxy"];
this.httpProxyEnabled = source["httpProxyEnabled"];
this.aiConfigs = this.convertValues(source["aiConfigs"], AIConfig);
}

22
main.go
View File

@@ -188,6 +188,26 @@ func main() {
}
func updateMultipleModel() {
oldSettings := &models.OldSettings{}
db.Dao.Model(oldSettings).First(oldSettings)
aiConfig := &data.AIConfig{}
db.Dao.Model(aiConfig).First(aiConfig)
if oldSettings.OpenAiEnable && oldSettings.OpenAiApiKey != "" && aiConfig.ID == 0 {
aiConfig.Name = oldSettings.OpenAiModelName
aiConfig.ApiKey = oldSettings.OpenAiApiKey
aiConfig.BaseUrl = oldSettings.OpenAiBaseUrl
aiConfig.ModelName = oldSettings.OpenAiModelName
aiConfig.Temperature = oldSettings.OpenAiTemperature
aiConfig.MaxTokens = oldSettings.OpenAiMaxTokens
aiConfig.TimeOut = oldSettings.OpenAiApiTimeOut
err := db.Dao.Model(aiConfig).Create(aiConfig).Error
if err != nil {
log.SugaredLogger.Error(err.Error())
}
}
}
func AutoMigrate() {
db.Dao.AutoMigrate(&data.StockInfo{})
db.Dao.AutoMigrate(&data.StockBasic{})
@@ -207,6 +227,8 @@ func AutoMigrate() {
db.Dao.AutoMigrate(&models.TelegraphTags{})
db.Dao.AutoMigrate(&models.LongTigerRankData{})
db.Dao.AutoMigrate(&data.AIConfig{})
updateMultipleModel()
}
func initStockDataUS(ctx context.Context) {