mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
feat(search): add Brave Search API support (#255)
* feat(search): add Brave Search API support - Add BraveSearchProvider class with multi-key load balancing - Support time-based filtering (freshness: pd/pw/pm/py) - Priority order: Bocha > Tavily > Brave > SerpAPI - Configure via BRAVE_API_KEYS environment variable - US stock optimized (English language, US region) * docs: add Brave Search API to documentation - Update README.md (CN) - Update docs/full-guide.md - Update docs/README_EN.md - Update docs/README_CHT.md
This commit is contained in:
@@ -58,6 +58,9 @@ GEMINI_REQUEST_DELAY=30
|
||||
TAVILY_API_KEYS=your_tavily_key_here
|
||||
# SerpAPI Keys(支持多个,逗号分隔)
|
||||
SERPAPI_API_KEYS=your_serpapi_key_here
|
||||
# Brave Search API Keys(支持多个,逗号分隔)
|
||||
# 获取: https://brave.com/search/api/
|
||||
BRAVE_API_KEYS=your_brave_key_here
|
||||
|
||||
# ===================================
|
||||
# 通知渠道配置(可同时配置多个,全部推送)
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
|------|------|
|
||||
| AI 模型 | Gemini(免费)、OpenAI 兼容、DeepSeek、通义千问、Claude、Ollama |
|
||||
| 行情数据 | AkShare、Tushare、Pytdx、Baostock、YFinance |
|
||||
| 新闻搜索 | Tavily、SerpAPI、Bocha |
|
||||
| 新闻搜索 | Tavily、SerpAPI、Bocha、Brave |
|
||||
|
||||
### 内置交易纪律
|
||||
|
||||
@@ -114,6 +114,7 @@
|
||||
| `TAVILY_API_KEYS` | [Tavily](https://tavily.com/) 搜索 API(新闻搜索) | 推荐 |
|
||||
| `SERPAPI_API_KEYS` | [SerpAPI](https://serpapi.com/baidu-search-api?utm_source=github_daily_stock_analysis) 全渠道搜索 | 可选 |
|
||||
| `BOCHA_API_KEYS` | [博查搜索](https://open.bocha.cn/) Web Search API(中文搜索优化,支持AI摘要,多个key用逗号分隔) | 可选 |
|
||||
| `BRAVE_API_KEYS` | [Brave Search](https://brave.com/search/api/) API(隐私优先,美股优化,多个key用逗号分隔) | 可选 |
|
||||
| `TUSHARE_TOKEN` | [Tushare Pro](https://tushare.pro/weborder/#/login?reg=834638 ) Token | 可选 |
|
||||
| `WECHAT_MSG_TYPE` | 企微消息类型,默认 markdown,支持配置 text 类型,发送纯 markdown 文本 | 可选 |
|
||||
|
||||
|
||||
@@ -83,10 +83,11 @@ class MarketCommand(BotCommand):
|
||||
|
||||
# 初始化搜索服务
|
||||
search_service = None
|
||||
if config.bocha_api_keys or config.tavily_api_keys or config.serpapi_keys:
|
||||
if config.bocha_api_keys or config.tavily_api_keys or config.brave_api_keys or config.serpapi_keys:
|
||||
search_service = SearchService(
|
||||
bocha_keys=config.bocha_api_keys,
|
||||
tavily_keys=config.tavily_api_keys,
|
||||
brave_keys=config.brave_api_keys,
|
||||
serpapi_keys=config.serpapi_keys
|
||||
)
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
|------|------|
|
||||
| AI 模型 | Gemini(免費)、OpenAI 兼容、DeepSeek、通義千問、Claude、Ollama |
|
||||
| 行情數據 | AkShare、Tushare、Pytdx、Baostock、YFinance |
|
||||
| 新聞搜索 | Tavily、SerpAPI、Bocha |
|
||||
| 新聞搜索 | Tavily、SerpAPI、Bocha、Brave |
|
||||
|
||||
### 內建交易紀律
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
| `STOCK_LIST` | 自選股代碼,如 `600519,hk00700,AAPL,TSLA` | ✅ |
|
||||
| `TAVILY_API_KEYS` | [Tavily](https://tavily.com/) 搜索 API(新聞搜索) | 推薦 |
|
||||
| `BOCHA_API_KEYS` | [博查搜索](https://open.bocha.cn/) Web Search API(中文搜索優化,支持AI摘要,多個key用逗號分隔) | 可選 |
|
||||
| `BRAVE_API_KEYS` | [Brave Search](https://brave.com/search/api/) API(隱私優先,美股優化,多個key用逗號分隔) | 可選 |
|
||||
| `SERPAPI_API_KEYS` | [SerpAPI](https://serpapi.com/baidu-search-api?utm_source=github_daily_stock_analysis) 備用搜索 | 可選 |
|
||||
| `TUSHARE_TOKEN` | [Tushare Pro](https://tushare.pro/weborder/#/login?reg=834638 ) Token | 可選 |
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ English | [简体中文](../README.md) | [繁體中文](README_CHT.md)
|
||||
|------|----------|
|
||||
| LLMs | Gemini (free), OpenAI-compatible, DeepSeek, Qwen, Claude, Ollama |
|
||||
| Market Data | AkShare, Tushare, Pytdx, Baostock, YFinance |
|
||||
| News Search | Tavily, SerpAPI, Bocha |
|
||||
| News Search | Tavily, SerpAPI, Bocha, Brave |
|
||||
|
||||
### Built-in Trading Rules
|
||||
|
||||
@@ -117,6 +117,7 @@ Go to your forked repo → `Settings` → `Secrets and variables` → `Actions`
|
||||
|------------|------|:----:|
|
||||
| `STOCK_LIST` | Watchlist codes, e.g., `600519,AAPL,hk00700` | ✅ |
|
||||
| `TAVILY_API_KEYS` | [Tavily](https://tavily.com/) Search API (for news) | Recommended |
|
||||
| `BRAVE_API_KEYS` | [Brave Search](https://brave.com/search/api/) API (privacy-focused, US stocks optimized) | Optional |
|
||||
| `SERPAPI_API_KEYS` | [SerpAPI](https://serpapi.com/baidu-search-api?utm_source=github_daily_stock_analysis) Backup search | Optional |
|
||||
| `BOCHA_API_KEYS` | [Bocha Search](https://open.bocha.cn/) Web Search API (Chinese search optimized, supports AI summaries, multiple keys comma-separated) | Optional |
|
||||
| `TUSHARE_TOKEN` | [Tushare Pro](https://tushare.pro/weborder/#/login?reg=834638 ) Token | Optional |
|
||||
|
||||
@@ -99,6 +99,7 @@ daily_stock_analysis/
|
||||
| `STOCK_LIST` | 自选股代码,如 `600519,300750,002594` | ✅ |
|
||||
| `TAVILY_API_KEYS` | [Tavily](https://tavily.com/) 搜索 API(新闻搜索) | 推荐 |
|
||||
| `BOCHA_API_KEYS` | [博查搜索](https://open.bocha.cn/) Web Search API(中文搜索优化,支持AI摘要,多个key用逗号分隔) | 可选 |
|
||||
| `BRAVE_API_KEYS` | [Brave Search](https://brave.com/search/api/) API(隐私优先,美股优化,多个key用逗号分隔) | 可选 |
|
||||
| `SERPAPI_API_KEYS` | [SerpAPI](https://serpapi.com/baidu-search-api?utm_source=github_daily_stock_analysis) 备用搜索 | 可选 |
|
||||
| `TUSHARE_TOKEN` | [Tushare Pro](https://tushare.pro/weborder/#/login?reg=834638 ) Token | 可选 |
|
||||
|
||||
@@ -190,6 +191,7 @@ daily_stock_analysis/
|
||||
|--------|------|:----:|
|
||||
| `TAVILY_API_KEYS` | Tavily 搜索 API Key(推荐) | 推荐 |
|
||||
| `BOCHA_API_KEYS` | 博查搜索 API Key(中文优化) | 可选 |
|
||||
| `BRAVE_API_KEYS` | Brave Search API Key(美股优化) | 可选 |
|
||||
| `SERPAPI_API_KEYS` | SerpAPI 备用搜索 | 可选 |
|
||||
|
||||
### 数据源配置
|
||||
|
||||
3
main.py
3
main.py
@@ -422,10 +422,11 @@ def main() -> int:
|
||||
search_service = None
|
||||
analyzer = None
|
||||
|
||||
if config.bocha_api_keys or config.tavily_api_keys or config.serpapi_keys:
|
||||
if config.bocha_api_keys or config.tavily_api_keys or config.brave_api_keys or config.serpapi_keys:
|
||||
search_service = SearchService(
|
||||
bocha_keys=config.bocha_api_keys,
|
||||
tavily_keys=config.tavily_api_keys,
|
||||
brave_keys=config.brave_api_keys,
|
||||
serpapi_keys=config.serpapi_keys
|
||||
)
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ class Config:
|
||||
# === 搜索引擎配置(支持多 Key 负载均衡)===
|
||||
bocha_api_keys: List[str] = field(default_factory=list) # Bocha API Keys
|
||||
tavily_api_keys: List[str] = field(default_factory=list) # Tavily API Keys
|
||||
brave_api_keys: List[str] = field(default_factory=list) # Brave Search API Keys
|
||||
serpapi_keys: List[str] = field(default_factory=list) # SerpAPI Keys
|
||||
|
||||
# === 通知配置(可同时配置多个,全部推送)===
|
||||
@@ -306,6 +307,9 @@ class Config:
|
||||
serpapi_keys_str = os.getenv('SERPAPI_API_KEYS', '')
|
||||
serpapi_keys = [k.strip() for k in serpapi_keys_str.split(',') if k.strip()]
|
||||
|
||||
brave_keys_str = os.getenv('BRAVE_API_KEYS', '')
|
||||
brave_api_keys = [k.strip() for k in brave_keys_str.split(',') if k.strip()]
|
||||
|
||||
# 企微消息类型与最大字节数逻辑
|
||||
wechat_msg_type = os.getenv('WECHAT_MSG_TYPE', 'markdown')
|
||||
wechat_msg_type_lower = wechat_msg_type.lower()
|
||||
@@ -335,6 +339,7 @@ class Config:
|
||||
openai_temperature=float(os.getenv('OPENAI_TEMPERATURE', '0.7')),
|
||||
bocha_api_keys=bocha_api_keys,
|
||||
tavily_api_keys=tavily_api_keys,
|
||||
brave_api_keys=brave_api_keys,
|
||||
serpapi_keys=serpapi_keys,
|
||||
wechat_webhook_url=os.getenv('WECHAT_WEBHOOK_URL'),
|
||||
feishu_webhook_url=os.getenv('FEISHU_WEBHOOK_URL'),
|
||||
@@ -468,8 +473,8 @@ class Config:
|
||||
elif not self.gemini_api_key:
|
||||
warnings.append("提示:未配置 Gemini API Key,将使用 OpenAI 兼容 API")
|
||||
|
||||
if not self.bocha_api_keys and not self.tavily_api_keys and not self.serpapi_keys:
|
||||
warnings.append("提示:未配置搜索引擎 API Key (Bocha/Tavily/SerpAPI),新闻搜索功能将不可用")
|
||||
if not self.bocha_api_keys and not self.tavily_api_keys and not self.brave_api_keys and not self.serpapi_keys:
|
||||
warnings.append("提示:未配置搜索引擎 API Key (Bocha/Tavily/Brave/SerpAPI),新闻搜索功能将不可用")
|
||||
|
||||
# 检查通知配置
|
||||
has_notification = (
|
||||
|
||||
@@ -79,6 +79,7 @@ class StockAnalysisPipeline:
|
||||
self.search_service = SearchService(
|
||||
bocha_keys=self.config.bocha_api_keys,
|
||||
tavily_keys=self.config.tavily_api_keys,
|
||||
brave_keys=self.config.brave_api_keys,
|
||||
serpapi_keys=self.config.serpapi_keys,
|
||||
)
|
||||
|
||||
|
||||
@@ -695,6 +695,182 @@ class BochaSearchProvider(BaseSearchProvider):
|
||||
return '未知来源'
|
||||
|
||||
|
||||
class BraveSearchProvider(BaseSearchProvider):
|
||||
"""
|
||||
Brave Search 搜索引擎
|
||||
|
||||
特点:
|
||||
- 隐私优先的独立搜索引擎
|
||||
- 索引超过300亿页面
|
||||
- 免费层可用
|
||||
- 支持时间范围过滤
|
||||
|
||||
文档:https://brave.com/search/api/
|
||||
"""
|
||||
|
||||
API_ENDPOINT = "https://api.search.brave.com/res/v1/web/search"
|
||||
|
||||
def __init__(self, api_keys: List[str]):
|
||||
super().__init__(api_keys, "Brave")
|
||||
|
||||
def _do_search(self, query: str, api_key: str, max_results: int, days: int = 7) -> SearchResponse:
|
||||
"""执行 Brave 搜索"""
|
||||
try:
|
||||
# 请求头
|
||||
headers = {
|
||||
'X-Subscription-Token': api_key,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
|
||||
# 确定时间范围(freshness 参数)
|
||||
if days <= 1:
|
||||
freshness = "pd" # Past day (24小时)
|
||||
elif days <= 7:
|
||||
freshness = "pw" # Past week
|
||||
elif days <= 30:
|
||||
freshness = "pm" # Past month
|
||||
else:
|
||||
freshness = "py" # Past year
|
||||
|
||||
# 请求参数
|
||||
params = {
|
||||
"q": query,
|
||||
"count": min(max_results, 20), # Brave 最大支持20条
|
||||
"freshness": freshness,
|
||||
"search_lang": "en", # 英文内容(US股票优先)
|
||||
"country": "US", # 美国区域偏好
|
||||
"safesearch": "moderate"
|
||||
}
|
||||
|
||||
# 执行搜索(GET 请求)
|
||||
response = requests.get(
|
||||
self.API_ENDPOINT,
|
||||
headers=headers,
|
||||
params=params,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
# 检查HTTP状态码
|
||||
if response.status_code != 200:
|
||||
error_msg = self._parse_error(response)
|
||||
logger.warning(f"[Brave] 搜索失败: {error_msg}")
|
||||
return SearchResponse(
|
||||
query=query,
|
||||
results=[],
|
||||
provider=self.name,
|
||||
success=False,
|
||||
error_message=error_msg
|
||||
)
|
||||
|
||||
# 解析响应
|
||||
try:
|
||||
data = response.json()
|
||||
except ValueError as e:
|
||||
error_msg = f"响应JSON解析失败: {str(e)}"
|
||||
logger.error(f"[Brave] {error_msg}")
|
||||
return SearchResponse(
|
||||
query=query,
|
||||
results=[],
|
||||
provider=self.name,
|
||||
success=False,
|
||||
error_message=error_msg
|
||||
)
|
||||
|
||||
logger.info(f"[Brave] 搜索完成,query='{query}'")
|
||||
logger.debug(f"[Brave] 原始响应: {data}")
|
||||
|
||||
# 解析搜索结果
|
||||
results = []
|
||||
web_data = data.get('web', {})
|
||||
web_results = web_data.get('results', [])
|
||||
|
||||
for item in web_results[:max_results]:
|
||||
# 解析发布日期(ISO 8601 格式)
|
||||
published_date = None
|
||||
age = item.get('age') or item.get('page_age')
|
||||
if age:
|
||||
try:
|
||||
# 转换 ISO 格式为简单日期字符串
|
||||
dt = datetime.fromisoformat(age.replace('Z', '+00:00'))
|
||||
published_date = dt.strftime('%Y-%m-%d')
|
||||
except (ValueError, AttributeError):
|
||||
published_date = age # 解析失败时使用原始值
|
||||
|
||||
results.append(SearchResult(
|
||||
title=item.get('title', ''),
|
||||
snippet=item.get('description', '')[:500], # 截取到500字符
|
||||
url=item.get('url', ''),
|
||||
source=self._extract_domain(item.get('url', '')),
|
||||
published_date=published_date
|
||||
))
|
||||
|
||||
logger.info(f"[Brave] 成功解析 {len(results)} 条结果")
|
||||
|
||||
return SearchResponse(
|
||||
query=query,
|
||||
results=results,
|
||||
provider=self.name,
|
||||
success=True
|
||||
)
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
error_msg = "请求超时"
|
||||
logger.error(f"[Brave] {error_msg}")
|
||||
return SearchResponse(
|
||||
query=query,
|
||||
results=[],
|
||||
provider=self.name,
|
||||
success=False,
|
||||
error_message=error_msg
|
||||
)
|
||||
except requests.exceptions.RequestException as e:
|
||||
error_msg = f"网络请求失败: {str(e)}"
|
||||
logger.error(f"[Brave] {error_msg}")
|
||||
return SearchResponse(
|
||||
query=query,
|
||||
results=[],
|
||||
provider=self.name,
|
||||
success=False,
|
||||
error_message=error_msg
|
||||
)
|
||||
except Exception as e:
|
||||
error_msg = f"未知错误: {str(e)}"
|
||||
logger.error(f"[Brave] {error_msg}")
|
||||
return SearchResponse(
|
||||
query=query,
|
||||
results=[],
|
||||
provider=self.name,
|
||||
success=False,
|
||||
error_message=error_msg
|
||||
)
|
||||
|
||||
def _parse_error(self, response) -> str:
|
||||
"""解析错误响应"""
|
||||
try:
|
||||
if response.headers.get('content-type', '').startswith('application/json'):
|
||||
error_data = response.json()
|
||||
# Brave API 返回的错误格式
|
||||
if 'message' in error_data:
|
||||
return error_data['message']
|
||||
if 'error' in error_data:
|
||||
return error_data['error']
|
||||
return str(error_data)
|
||||
return response.text[:200]
|
||||
except:
|
||||
return f"HTTP {response.status_code}: {response.text[:200]}"
|
||||
|
||||
@staticmethod
|
||||
def _extract_domain(url: str) -> str:
|
||||
"""从 URL 提取域名作为来源"""
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
parsed = urlparse(url)
|
||||
domain = parsed.netloc.replace('www.', '')
|
||||
return domain or '未知来源'
|
||||
except:
|
||||
return '未知来源'
|
||||
|
||||
|
||||
class SearchService:
|
||||
"""
|
||||
搜索服务
|
||||
@@ -719,6 +895,7 @@ class SearchService:
|
||||
self,
|
||||
bocha_keys: Optional[List[str]] = None,
|
||||
tavily_keys: Optional[List[str]] = None,
|
||||
brave_keys: Optional[List[str]] = None,
|
||||
serpapi_keys: Optional[List[str]] = None,
|
||||
):
|
||||
"""
|
||||
@@ -727,6 +904,7 @@ class SearchService:
|
||||
Args:
|
||||
bocha_keys: 博查搜索 API Key 列表
|
||||
tavily_keys: Tavily API Key 列表
|
||||
brave_keys: Brave Search API Key 列表
|
||||
serpapi_keys: SerpAPI Key 列表
|
||||
"""
|
||||
self._providers: List[BaseSearchProvider] = []
|
||||
@@ -742,7 +920,12 @@ class SearchService:
|
||||
self._providers.append(TavilySearchProvider(tavily_keys))
|
||||
logger.info(f"已配置 Tavily 搜索,共 {len(tavily_keys)} 个 API Key")
|
||||
|
||||
# 3. SerpAPI 作为备选(每月 100 次)
|
||||
# 3. Brave Search(隐私优先,全球覆盖)
|
||||
if brave_keys:
|
||||
self._providers.append(BraveSearchProvider(brave_keys))
|
||||
logger.info(f"已配置 Brave 搜索,共 {len(brave_keys)} 个 API Key")
|
||||
|
||||
# 4. SerpAPI 作为备选(每月 100 次)
|
||||
if serpapi_keys:
|
||||
self._providers.append(SerpAPISearchProvider(serpapi_keys))
|
||||
logger.info(f"已配置 SerpAPI 搜索,共 {len(serpapi_keys)} 个 API Key")
|
||||
@@ -1217,6 +1400,7 @@ def get_search_service() -> SearchService:
|
||||
_search_service = SearchService(
|
||||
bocha_keys=config.bocha_api_keys,
|
||||
tavily_keys=config.tavily_api_keys,
|
||||
brave_keys=config.brave_api_keys,
|
||||
serpapi_keys=config.serpapi_keys,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user