Refactor and translate comments and docstrings in utility modules to English for better clarity and maintainability. Update Gunicorn and application startup messages for consistency in language. Enhance documentation with English translations for better accessibility.

This commit is contained in:
dienakdz
2026-04-06 16:47:36 +07:00
parent 3ca291a346
commit 11e2e5aaa6
64 changed files with 2323 additions and 2336 deletions
+106 -106
View File
@@ -1,13 +1,13 @@
"""
Search service v2.0 - 增强版搜索服务
整合多个搜索引擎,支持 API Key 轮换和故障转移
Search service v2.0 - Enhanced version of search service
Integrate multiple search engines and support API Key rotation and failover
支持的搜索引擎(按优先级):
1. Tavily - 专为AI设计,免费1000次/月
2. SerpAPI - Google/Bing 结果抓取
3. Google CSE - 自定义搜索引擎
Supported search engines (in order of priority):
1. Tavily - specially designed for AI, free 1000 times/month
2. SerpAPI - Google/Bing result crawling
3. Google CSE Custom Search Engine
4. Bing Search API
5. DuckDuckGo - 免费兜底
5. DuckDuckGo Get the scoop for free
参考:daily_stock_analysis-main/src/search_service.py
"""
@@ -34,21 +34,21 @@ _google_quota_reset_time = 0
@dataclass
class SearchResult:
"""搜索结果数据类"""
"""Search result data class"""
title: str
snippet: str # 摘要
snippet: str # summary
url: str
source: str # 来源网站
source: str # Source website
published_date: Optional[str] = None
sentiment: str = 'neutral' # 情绪标签
sentiment: str = 'neutral' # Emotion tags
def to_text(self) -> str:
"""转换为文本格式"""
"""Convert to text format"""
date_str = f" ({self.published_date})" if self.published_date else ""
return f"{self.source}{self.title}{date_str}\n{self.snippet}"
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
"""Convert to dictionary"""
return {
'title': self.title,
'link': self.url,
@@ -61,16 +61,16 @@ class SearchResult:
@dataclass
class SearchResponse:
"""搜索响应"""
"""search response"""
query: str
results: List[SearchResult]
provider: str # 使用的搜索引擎
provider: str # search engine used
success: bool = True
error_message: Optional[str] = None
search_time: float = 0.0 # 搜索耗时(秒)
search_time: float = 0.0 # Search time (seconds)
def to_context(self, max_results: int = 5) -> str:
"""将搜索结果转换为可用于 AI 分析的上下文"""
"""Transform search results into context that can be used for AI analysis"""
if not self.success or not self.results:
return f"搜索 '{self.query}' 未找到相关结果。"
@@ -81,20 +81,20 @@ class SearchResponse:
return "\n".join(lines)
def to_list(self) -> List[Dict[str, Any]]:
"""转换为列表格式(兼容旧接口)"""
"""Convert to list format (compatible with old interface)"""
return [r.to_dict() for r in self.results]
class BaseSearchProvider(ABC):
"""搜索引擎基类"""
"""Search engine base class"""
def __init__(self, api_keys: List[str], name: str):
"""
初始化搜索引擎
Initialize search engine
Args:
api_keys: API Key 列表(支持多个 key 负载均衡)
name: 搜索引擎名称
api_keys: API Key list (supports multiple key load balancing)
name: search engine name
"""
self._api_keys = api_keys
self._name = name
@@ -108,58 +108,58 @@ class BaseSearchProvider(ABC):
@property
def is_available(self) -> bool:
"""检查是否有可用的 API Key"""
"""Check if there is an available API Key"""
return bool(self._api_keys)
def _get_next_key(self) -> Optional[str]:
"""
获取下一个可用的 API Key(负载均衡)
Get the next available API Key (load balancing)
策略:轮询 + 跳过错误过多的 key
Strategy: polling + skip keys with too many errors
"""
if not self._key_cycle:
return None
# 最多尝试所有 key
# Try at most all keys
for _ in range(len(self._api_keys)):
key = next(self._key_cycle)
# 跳过错误次数过多的 key(超过 3 次)
# Skip keys with too many errors (more than 3 times)
if self._key_errors.get(key, 0) < 3:
return key
# 所有 key 都有问题,重置错误计数并返回第一个
# There is a problem with all keys, reset the error count and return the first one
logger.warning(f"[{self._name}] 所有 API Key 都有错误记录,重置错误计数")
self._key_errors = {key: 0 for key in self._api_keys}
return self._api_keys[0] if self._api_keys else None
def _record_success(self, key: str) -> None:
"""记录成功使用"""
"""Record successful use"""
self._key_usage[key] = self._key_usage.get(key, 0) + 1
# 成功后减少错误计数
# Decrement error count on success
if key in self._key_errors and self._key_errors[key] > 0:
self._key_errors[key] -= 1
def _record_error(self, key: str) -> None:
"""记录错误"""
"""Log errors"""
self._key_errors[key] = self._key_errors.get(key, 0) + 1
logger.warning(f"[{self._name}] API Key {key[:8]}... 错误计数: {self._key_errors[key]}")
@abstractmethod
def _do_search(self, query: str, api_key: str, max_results: int, days: int = 7) -> SearchResponse:
"""执行搜索(子类实现)"""
"""Perform a search (subclass implementation)"""
pass
def search(self, query: str, max_results: int = 5, days: int = 7) -> SearchResponse:
"""
执行搜索
Perform a search
Args:
query: 搜索关键词
max_results: 最大返回结果数
days: 搜索最近几天的时间范围(默认7天)
query: search keyword
max_results: Maximum number of results returned
days: Search the time range of the last few days (default 7 days)
Returns:
SearchResponse 对象
SearchResponse object
"""
api_key = self._get_next_key()
if not api_key:
@@ -199,7 +199,7 @@ class BaseSearchProvider(ABC):
@staticmethod
def _extract_domain(url: str) -> str:
"""从 URL 提取域名作为来源"""
"""Extract domain name from URL as source"""
try:
parsed = urlparse(url)
domain = parsed.netloc.replace('www.', '')
@@ -210,31 +210,31 @@ class BaseSearchProvider(ABC):
class TavilySearchProvider(BaseSearchProvider):
"""
Tavily 搜索引擎
Tavily search engine
特点:
- 专为 AI/LLM 优化的搜索 API
- 免费版每月 1000 次请求
- 返回结构化的搜索结果
Features:
- Search API optimized for AI/LLM
- Free version 1000 requests per month
- Return structured search results
文档:https://docs.tavily.com/
Documentation: https://docs.tavily.com/
"""
def __init__(self, api_keys: List[str]):
super().__init__(api_keys, "Tavily")
def _do_search(self, query: str, api_key: str, max_results: int, days: int = 7) -> SearchResponse:
"""执行 Tavily 搜索"""
"""Perform a Tavily search"""
try:
from tavily import TavilyClient
except ImportError:
# 如果未安装 tavily-python,使用 REST API
# If tavily-python is not installed, use the REST API
return self._do_search_rest(query, api_key, max_results, days)
try:
client = TavilyClient(api_key=api_key)
# 执行搜索
# Perform a search
response = client.search(
query=query,
search_depth="advanced",
@@ -244,7 +244,7 @@ class TavilySearchProvider(BaseSearchProvider):
days=days,
)
# 解析结果
# Parse results
results = []
for item in response.get('results', []):
results.append(SearchResult(
@@ -276,7 +276,7 @@ class TavilySearchProvider(BaseSearchProvider):
)
def _do_search_rest(self, query: str, api_key: str, max_results: int, days: int = 7) -> SearchResponse:
"""使用 REST API 执行 Tavily 搜索(备选方案)"""
"""Performing Tavily searches using the REST API (alternative)"""
try:
url = "https://api.tavily.com/search"
headers = {
@@ -332,20 +332,20 @@ class TavilySearchProvider(BaseSearchProvider):
class SerpAPISearchProvider(BaseSearchProvider):
"""
SerpAPI 搜索引擎
SerpAPI search engine
特点:
- 支持 GoogleBing、百度等多种搜索引擎
- 免费版每月 100 次请求
Features:
-Support multiple search engines such as Google, Bing, Baidu, etc.
- Free version 100 requests per month
文档:https://serpapi.com/
Documentation: https://serpapi.com/
"""
def __init__(self, api_keys: List[str]):
super().__init__(api_keys, "SerpAPI")
def _do_search(self, query: str, api_key: str, max_results: int, days: int = 7) -> SearchResponse:
"""执行 SerpAPI 搜索"""
"""Perform a SerpAPI search"""
try:
from serpapi import GoogleSearch
except ImportError:
@@ -405,7 +405,7 @@ class SerpAPISearchProvider(BaseSearchProvider):
)
def _do_search_rest(self, query: str, api_key: str, max_results: int, days: int = 7) -> SearchResponse:
"""使用 REST API 执行 SerpAPI 搜索"""
"""Performing SerpAPI searches using the REST API"""
try:
tbs = "qdr:w"
if days <= 1:
@@ -467,14 +467,14 @@ class SerpAPISearchProvider(BaseSearchProvider):
class GoogleSearchProvider(BaseSearchProvider):
"""Google Custom Search (CSE) 搜索引擎"""
"""Google Custom Search (CSE) search engine"""
def __init__(self, api_key: str, cx: str):
super().__init__([api_key] if api_key else [], "Google")
self._cx = cx
def _do_search(self, query: str, api_key: str, max_results: int, days: int = 7) -> SearchResponse:
"""执行 Google 搜索"""
"""Perform a Google search"""
global _google_quota_exhausted, _google_quota_reset_time
if not self._cx:
@@ -495,7 +495,7 @@ class GoogleSearchProvider(BaseSearchProvider):
'num': min(max_results, 10),
}
# 添加时间限制
# Add time limit
if days <= 1:
params['dateRestrict'] = 'd1'
elif days <= 7:
@@ -550,13 +550,13 @@ class GoogleSearchProvider(BaseSearchProvider):
class BingSearchProvider(BaseSearchProvider):
"""Bing Search API 搜索引擎"""
"""Bing Search API search engine"""
def __init__(self, api_key: str):
super().__init__([api_key] if api_key else [], "Bing")
def _do_search(self, query: str, api_key: str, max_results: int, days: int = 7) -> SearchResponse:
"""执行 Bing 搜索"""
"""Perform a Bing search"""
try:
url = "https://api.bing.microsoft.com/v7.0/search"
headers = {"Ocp-Apim-Subscription-Key": api_key}
@@ -600,15 +600,15 @@ class BingSearchProvider(BaseSearchProvider):
class DuckDuckGoSearchProvider(BaseSearchProvider):
"""DuckDuckGo 搜索引擎(免费,无需 API Key"""
"""DuckDuckGo search engine (free, no API Key required)"""
def __init__(self):
super().__init__(['free'], "DuckDuckGo")
def _do_search(self, query: str, api_key: str, max_results: int, days: int = 7) -> SearchResponse:
"""执行 DuckDuckGo 搜索"""
"""Perform a DuckDuckGo search"""
try:
# 使用 DuckDuckGo Instant Answer API
# Using DuckDuckGo Instant Answer API
url = "https://api.duckduckgo.com/"
params = {
'q': query,
@@ -623,7 +623,7 @@ class DuckDuckGoSearchProvider(BaseSearchProvider):
results = []
# 获取 RelatedTopics
# Get RelatedTopics
related_topics = data.get('RelatedTopics', [])
for topic in related_topics[:max_results]:
if isinstance(topic, dict):
@@ -646,7 +646,7 @@ class DuckDuckGoSearchProvider(BaseSearchProvider):
source='DuckDuckGo',
))
# 检查 AbstractURL
# Check AbstractURL
if data.get('AbstractURL') and len(results) < max_results:
results.insert(0, SearchResult(
title=data.get('Heading', query),
@@ -655,7 +655,7 @@ class DuckDuckGoSearchProvider(BaseSearchProvider):
source='DuckDuckGo',
))
# 如果没有结果,尝试 HTML 版本
# If no results, try the HTML version
if not results:
results = self._search_html(query, max_results)
@@ -676,7 +676,7 @@ class DuckDuckGoSearchProvider(BaseSearchProvider):
)
def _search_html(self, query: str, max_results: int) -> List[SearchResult]:
"""DuckDuckGo HTML 搜索备选"""
"""DuckDuckGo HTML search alternatives"""
try:
url = "https://lite.duckduckgo.com/lite/"
headers = {
@@ -715,12 +715,12 @@ class DuckDuckGoSearchProvider(BaseSearchProvider):
class SearchService:
"""
搜索服务
Search service
功能:
1. 管理多个搜索引擎
2. 自动故障转移
3. 结果聚合和格式化
Function:
1. Manage multiple search engines
2. Automatic failover
3. Result aggregation and formatting
"""
def __init__(self):
@@ -730,17 +730,17 @@ class SearchService:
self._init_providers()
def _load_config(self):
"""加载配置"""
"""Load configuration"""
config = load_addon_config()
self._config = config.get('search', {})
self.provider = self._config.get('provider', 'google')
self.max_results = int(self._config.get('max_results', 10))
def _init_providers(self):
"""初始化搜索引擎(按优先级排序)"""
"""Initialize search engines (sorted by priority)"""
from app.config import APIKeys
# 1. TavilyAI优化搜索)
# 1. Tavily (AI optimized search)
tavily_keys = APIKeys.TAVILY_API_KEYS
if tavily_keys:
self._providers.append(TavilySearchProvider(tavily_keys))
@@ -765,7 +765,7 @@ class SearchService:
self._providers.append(BingSearchProvider(bing_api_key))
logger.info("已配置 Bing 搜索")
# 5. DuckDuckGo(免费兜底)
# 5. DuckDuckGo (Free Tips)
self._providers.append(DuckDuckGoSearchProvider())
logger.info("已配置 DuckDuckGo 搜索(免费兜底)")
@@ -774,25 +774,25 @@ class SearchService:
@property
def is_available(self) -> bool:
"""检查是否有可用的搜索引擎"""
"""Check if a search engine is available"""
return any(p.is_available for p in self._providers)
def search(self, query: str, num_results: int = None, date_restrict: str = None, days: int = 7) -> List[Dict[str, Any]]:
"""
执行搜索(兼容旧接口)
Perform a search (compatible with old interface)
Args:
query: 搜索关键词
num_results: 最大返回结果数
date_restrict: 时间限制(Google 格式,如 'd7'
days: 搜索最近几天(优先级高于 date_restrict
query: search keyword
num_results: Maximum number of results returned
date_restrict: time restriction (Google format, such as 'd7')
days: Search the last few days (priority higher than date_restrict)
Returns:
搜索结果列表
Search result list
"""
limit = num_results if num_results else self.max_results
# 解析 date_restrict days
# Parse date_restrict to days
if date_restrict and not days:
if date_restrict.startswith('d'):
days = int(date_restrict[1:])
@@ -806,17 +806,17 @@ class SearchService:
def search_with_fallback(self, query: str, max_results: int = 5, days: int = 7) -> SearchResponse:
"""
执行搜索(带自动故障转移)
Perform a search (with automatic failover)
Args:
query: 搜索关键词
max_results: 最大返回结果数
days: 搜索最近几天
query: search keyword
max_results: Maximum number of results returned
days: Search the last few days
Returns:
SearchResponse 对象
SearchResponse object
"""
# 依次尝试各个搜索引擎
# Try each search engine in sequence
for provider in self._providers:
if not provider.is_available:
continue
@@ -828,7 +828,7 @@ class SearchService:
else:
logger.warning(f"{provider.name} 搜索失败: {response.error_message},尝试下一个引擎")
# 所有引擎都失败
# all engines fail
return SearchResponse(
query=query,
results=[],
@@ -845,27 +845,27 @@ class SearchService:
max_results: int = 5
) -> SearchResponse:
"""
搜索股票相关新闻
Search stock related news
Args:
stock_code: 股票代码
stock_name: 股票名称
market: 市场类型
max_results: 最大返回结果数
stock_code: stock code
stock_name: stock name
market: market type
max_results: Maximum number of results returned
Returns:
SearchResponse 对象
SearchResponse object
"""
# 智能确定搜索时间范围
# Intelligently determine search time range
today_weekday = datetime.now().weekday()
if today_weekday == 0: # 周一
if today_weekday == 0: # on Monday
search_days = 3
elif today_weekday >= 5: # 周末
elif today_weekday >= 5: # weekend
search_days = 2
else:
search_days = 1
# 根据市场类型构建搜索查询
# Build search queries based on market type
if market == "USStock":
query = f"{stock_name} {stock_code} stock news latest"
elif market == "Crypto":
@@ -886,7 +886,7 @@ class SearchService:
event_types: Optional[List[str]] = None
) -> SearchResponse:
"""
搜索股票特定事件(年报预告、减持等)
Search for specific stock events (annual report preview, shareholding reduction, etc.)
"""
if event_types is None:
event_types = ["年报预告", "减持公告", "业绩快报"]
@@ -897,12 +897,12 @@ class SearchService:
return self.search_with_fallback(query, max_results=5, days=30)
# 单例实例
# Singleton instance
_search_service: Optional[SearchService] = None
def get_search_service() -> SearchService:
"""获取搜索服务单例"""
"""Get the search service singleton"""
global _search_service
if _search_service is None:
_search_service = SearchService()
@@ -910,6 +910,6 @@ def get_search_service() -> SearchService:
def reset_search_service() -> None:
"""重置搜索服务(用于测试或配置更新后)"""
"""Reset the search service (for testing or after configuration updates)"""
global _search_service
_search_service = None