feat: add MiniMax Coding Plan web search provider (#493)

* feat: add MiniMax Coding Plan web search provider

- Add MiniMaxSearchProvider class with POST API to api.minimaxi.com
- Priority: Bocha > MiniMax > Tavily > Brave > SerpAPI
- Dual time-filtering: query augmentation + client-side date filtering
- Circuit breaker: 3 consecutive failures -> 300s cooldown
- New MINIMAX_API_KEY env var (supports multiple keys, comma-separated)
- Update config.py, search_service.py, .env.example

* docs: add MiniMax search provider to all documentation

- .env.example: add MINIMAX_API_KEY and BOCHA_API_KEYS with priority note
- README.md: update tech stack table and secrets table
- docs/full-guide.md: add MiniMax to config tables and search service section
- docs/full-guide_EN.md: add MiniMax to config tables and search service section
- docs/README_EN.md: update tech stack and secrets tables
- docs/README_CHT.md: update tech stack and secrets tables
- docs/DEPLOY.md: add MiniMax to env var tables
- docs/DEPLOY_EN.md: add MiniMax to env var tables
- docs/docker/zeabur-deployment.md: add MiniMax to analysis config table
- docs/CHANGELOG.md: add MiniMax provider to Unreleased section

* update doc

* refactor(search_service): 调整搜索提供者初始化顺序及参数位置

将MiniMax搜索提供者的参数位置和初始化顺序调整到最后,保持代码逻辑清晰

* 添加其他多处minimax_api_keys支持并调整参数顺序

* 增加 Bocha 和 Minimax 的前端配置

* fix: 将MINIMAX_API_KEYS改为单数形式

* docs(CHANGELOG): 调整MiniMax搜索提供商的优先级顺序

更新CHANGELOG.md中MiniMax搜索提供商的优先级描述,从Bocha > MiniMax > Tavily > Brave > SerpAPI调整为Bocha > Tavily > Brave > SerpAPI > MiniMax

* fix: 统一 MINIMAX_API_KEY 为 MINIMAX_API_KEYS 以保持命名一致性

---------

Co-authored-by: mumu <42829555+ZhuLinsen@users.noreply.github.com>
This commit is contained in:
Haode Du
2026-03-09 21:29:56 +08:00
committed by GitHub
parent 5c6cfe8238
commit 049e7e8f59
18 changed files with 308 additions and 6 deletions

View File

@@ -102,7 +102,15 @@ GEMINI_TEMPERATURE=0.7
# 高级YAML 配置(可选,标准 LiteLLM 格式,参考 litellm_config.example.yaml
# LITELLM_CONFIG=./litellm_config.yaml
# ===================================
# 搜索引擎配置(用于获取股票新闻)
# ===================================
# Bocha API Keys中文搜索优化支持AI摘要支持多个逗号分隔
# 获取: https://open.bocha.cn/
# BOCHA_API_KEYS=your_bocha_key_here
# MiniMax API KeyCoding Plan Web Search支持多个逗号分隔
# 获取: https://platform.minimaxi.com/
# MINIMAX_API_KEYS=your_minimax_key_here
# Tavily API Keys支持多个逗号分隔
TAVILY_API_KEYS=
# SerpAPI Keys支持多个逗号分隔

View File

@@ -47,7 +47,7 @@
|------|------|
| AI 模型 | [AIHubMix](https://aihubmix.com/?aff=CfMq)、Gemini、OpenAI 兼容、DeepSeek、通义千问、Claude 等(统一通过 [LiteLLM](https://github.com/BerriAI/litellm) 调用,支持多 Key 负载均衡)|
| 行情数据 | AkShare、Tushare、Pytdx、Baostock、YFinance |
| 新闻搜索 | Tavily、SerpAPI、Bocha、Brave |
| 新闻搜索 | Tavily、SerpAPI、Bocha、Brave、MiniMax |
> 注:美股历史数据与实时行情统一使用 YFinance确保复权一致性
@@ -136,6 +136,7 @@
|------------|------|:----:|
| `STOCK_LIST` | 自选股代码,如 `600519,hk00700,AAPL,TSLA` | ✅ |
| `TAVILY_API_KEYS` | [Tavily](https://tavily.com/) 搜索 API新闻搜索 | 推荐 |
| `MINIMAX_API_KEYS` | [MiniMax](https://platform.minimaxi.com/) Coding Plan Web Search结构化搜索结果 | 可选 |
| `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用逗号分隔 | 可选 |

View File

@@ -25,9 +25,11 @@ const categoryDescriptionMap: Partial<Record<SystemConfigCategory, string>> = {
const fieldTitleMap: Record<string, string> = {
STOCK_LIST: '自选股列表',
TUSHARE_TOKEN: 'Tushare Token',
BOCHA_API_KEYS: 'Bocha API Keys',
TAVILY_API_KEYS: 'Tavily API Keys',
SERPAPI_API_KEYS: 'SerpAPI API Keys',
BRAVE_API_KEYS: 'Brave API Keys',
MINIMAX_API_KEYS: 'MiniMax API Keys',
REALTIME_SOURCE_PRIORITY: '实时数据源优先级',
ENABLE_REALTIME_TECHNICAL_INDICATORS: '盘中实时技术面',
LITELLM_MODEL: '主模型',
@@ -65,9 +67,11 @@ const fieldTitleMap: Record<string, string> = {
const fieldDescriptionMap: Record<string, string> = {
STOCK_LIST: '使用逗号分隔股票代码例如600519,300750。',
TUSHARE_TOKEN: '用于接入 Tushare Pro 数据服务的凭据。',
BOCHA_API_KEYS: '用于新闻检索的 Bocha 密钥,支持逗号分隔多个(最高优先级)。',
TAVILY_API_KEYS: '用于新闻检索的 Tavily 密钥,支持逗号分隔多个。',
SERPAPI_API_KEYS: '用于新闻检索的 SerpAPI 密钥,支持逗号分隔多个。',
BRAVE_API_KEYS: '用于新闻检索的 Brave Search 密钥,支持逗号分隔多个。',
MINIMAX_API_KEYS: '用于新闻检索的 MiniMax 密钥,支持逗号分隔多个(最低优先级)。',
REALTIME_SOURCE_PRIORITY: '按逗号分隔填写数据源调用优先级。',
ENABLE_REALTIME_TECHNICAL_INDICATORS: '盘中分析时用实时价计算 MA5/MA10/MA20 与多头排列Issue #234关闭则用昨日收盘。',
LITELLM_MODEL: '主模型,格式 provider/model如 gemini/gemini-2.5-flash。配置渠道后自动推断。',

View File

@@ -83,12 +83,13 @@ class MarketCommand(BotCommand):
# 初始化搜索服务
search_service = None
if config.bocha_api_keys or config.tavily_api_keys or config.brave_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 or config.minimax_api_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,
minimax_keys=config.minimax_api_keys,
news_max_age_days=config.news_max_age_days,
)

View File

@@ -9,10 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
## [Unreleased]
### Changed
- 🔎 **Fetcher failure observability** — historical data logs now record fetcher start/success/failure with elapsed time, explicit failover transitions, and clearer final outcomes; Efinance/Eastmoney failures now include upstream endpoint and normalized categories such as `remote_disconnect` and `timeout`; Akshare 新浪/腾讯实时行情日志 now also include upstream endpoint and classified failures for HTTP status, disconnects, and malformed payloads
### Added
- **智能导入 (P1)** — 支持图片、CSV/Excel、剪贴板多源导入Vision LLM 同时提取代码+名称+置信度;名称→代码解析引擎(本地映射+拼音+AkShare fallback置信度分层确认高自动勾选、中/低需人工确认);统一预览与合并流程
- **EXTRACT_PROMPT 文档** — `docs/image-extract-prompt.md` 记录完整 prompt 便于 PR 审查PR 模板新增 EXTRACT_PROMPT 变更展示区块
- 📖 **LLM 配置指南** — 新增 [docs/LLM_CONFIG_GUIDE.md](LLM_CONFIG_GUIDE.md)系统讲解三层配置、快速上手、Vision/Agent/Web UI/校验排错;同步更新 README、full-guide、.env.example、FAQ、英文版指南
- 🔍 **MiniMax Coding Plan web search provider** — new `MiniMaxSearchProvider` with circuit breaker (3 failures → 300s cooldown), dual time-filtering (query augmentation + client-side date filtering), priority: Bocha > Tavily > Brave > SerpAPI > **MiniMax**; configured via `MINIMAX_API_KEYS` env var
### Changed
- **API 响应扩展(兼容提醒)** — `POST /api/v1/stocks/extract-from-image` 响应新增 `items` 字段(`code/name/confidence` 明细);保留 `codes` 以兼容旧客户端。对严格 JSON Schema 且不接受未知字段的客户端,需评估并适配该字段变更。

View File

@@ -196,6 +196,7 @@ journalctl -u stock-analyzer -f
| `SCHEDULE_TIME` | `18:00` | 每日执行时间 |
| `MARKET_REVIEW_ENABLED` | `true` | 是否启用大盘复盘 |
| `TAVILY_API_KEYS` | - | 新闻搜索(可选) |
| `MINIMAX_API_KEYS` | - | MiniMax 搜索(可选) |
---
@@ -360,6 +361,7 @@ git push -u origin main
| `CUSTOM_WEBHOOK_URLS` | 自定义 Webhook多个逗号分隔 | 可选* |
| `STOCK_LIST` | 自选股列表,如 `600519,300750` | ✅ |
| `TAVILY_API_KEYS` | Tavily 搜索 API Key | 推荐 |
| `MINIMAX_API_KEYS` | MiniMax Coding Plan Web Search | 可选 |
| `SERPAPI_API_KEYS` | SerpAPI Key | 可选 |
| `TUSHARE_TOKEN` | Tushare Token | 可选 |
| `GEMINI_MODEL` | 模型名称(默认 gemini-2.0-flash | 可选 |

View File

@@ -196,6 +196,7 @@ journalctl -u stock-analyzer -f
| `SCHEDULE_TIME` | `18:00` | Daily execution time |
| `MARKET_REVIEW_ENABLED` | `true` | Enable market review |
| `TAVILY_API_KEYS` | - | News search (optional) |
| `MINIMAX_API_KEYS` | - | MiniMax search (optional) |
---
@@ -360,6 +361,7 @@ Add these Secrets:
| `CUSTOM_WEBHOOK_URLS` | Custom Webhook (comma-separated for multiple) | Optional* |
| `STOCK_LIST` | Watchlist, e.g., `600519,300750` | ✅ |
| `TAVILY_API_KEYS` | Tavily Search API Key | Recommended |
| `MINIMAX_API_KEYS` | MiniMax Coding Plan Web Search | Optional |
| `SERPAPI_API_KEYS` | SerpAPI Key | Optional |
| `TUSHARE_TOKEN` | Tushare Token | Optional |
| `GEMINI_MODEL` | Model name (default gemini-2.0-flash) | Optional |

View File

@@ -49,7 +49,7 @@
|------|------|
| AI 模型 | Gemini免費、OpenAI 兼容、DeepSeek、通義千問、Claude、Ollama |
| 行情數據 | AkShare、Tushare、Pytdx、Baostock、YFinance |
| 新聞搜索 | Tavily、SerpAPI、Bocha、Brave |
| 新聞搜索 | Tavily、SerpAPI、Bocha、Brave、MiniMax |
### 內建交易紀律
@@ -119,6 +119,7 @@
|------------|------|:----:|
| `STOCK_LIST` | 自選股代碼,如 `600519,hk00700,AAPL,TSLA` | ✅ |
| `TAVILY_API_KEYS` | [Tavily](https://tavily.com/) 搜索 API新聞搜索 | 推薦 |
| `MINIMAX_API_KEYS` | [MiniMax](https://platform.minimaxi.com/) Coding Plan Web Search結構化搜索結果 | 可選 |
| `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) 備用搜索 | 可選 |

View File

@@ -49,7 +49,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, Brave |
| News Search | Tavily, SerpAPI, Bocha, Brave, MiniMax |
### Built-in Trading Rules
@@ -121,6 +121,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 |
| `MINIMAX_API_KEYS` | [MiniMax](https://platform.minimaxi.com/) Coding Plan Web Search (structured search results) | Optional |
| `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 |

View File

@@ -146,6 +146,7 @@ Dockerfile 已采用多阶段构建,前端会在镜像构建时自动打包。
|--------|------|
| `GEMINI_API_KEY` | Gemini API 密钥 |
| `BOCHA_API_KEYS` | Bocha API 密钥(用逗号分隔) |
| `MINIMAX_API_KEYS` | MiniMax API 密钥(用逗号分隔) |
| `TAVILY_API_KEYS` | Tavily API 密钥(用逗号分隔) |
| `SERPAPI_KEYS` | SerpAPI 密钥(用逗号分隔) |

View File

@@ -108,6 +108,7 @@ daily_stock_analysis/
|------------|------|:----:|
| `STOCK_LIST` | 自选股代码,如 `600519,300750,002594` | ✅ |
| `TAVILY_API_KEYS` | [Tavily](https://tavily.com/) 搜索 API新闻搜索 | 推荐 |
| `MINIMAX_API_KEYS` | [MiniMax](https://platform.minimaxi.com/) Coding Plan Web Search结构化搜索结果 | 可选 |
| `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) 备用搜索 | 可选 |
@@ -215,6 +216,7 @@ daily_stock_analysis/
| 变量名 | 说明 | 必填 |
|--------|------|:----:|
| `TAVILY_API_KEYS` | Tavily 搜索 API Key推荐 | 推荐 |
| `MINIMAX_API_KEYS` | MiniMax Coding Plan Web Search结构化搜索结果 | 可选 |
| `BOCHA_API_KEYS` | 博查搜索 API Key中文优化 | 可选 |
| `BRAVE_API_KEYS` | Brave Search API Key美股优化 | 可选 |
| `SERPAPI_API_KEYS` | SerpAPI 备用搜索 | 可选 |

View File

@@ -101,6 +101,7 @@ Go to your forked repo → `Settings` → `Secrets and variables` → `Actions`
|------------|------|:----:|
| `STOCK_LIST` | Watchlist codes, e.g., `600519,300750,002594` | ✅ |
| `TAVILY_API_KEYS` | [Tavily](https://tavily.com/) Search API (for news search) | Recommended |
| `MINIMAX_API_KEYS` | [MiniMax](https://platform.minimaxi.com/) Coding Plan Web Search (structured search results) | Optional |
| `BOCHA_API_KEYS` | [Bocha Search](https://open.bocha.cn/) Web Search API (Chinese search optimized, supports AI summaries, multiple keys comma-separated) | Optional |
| `SERPAPI_API_KEYS` | [SerpAPI](https://serpapi.com/baidu-search-api?utm_source=github_daily_stock_analysis) Backup search | Optional |
| `TUSHARE_TOKEN` | [Tushare Pro](https://tushare.pro/weborder/#/login?reg=834638) Token | Optional |
@@ -200,7 +201,9 @@ Default schedule: Every weekday at **18:00 (Beijing Time)** automatic execution.
| Variable | Description | Required |
|--------|------|:----:|
| `TAVILY_API_KEYS` | Tavily Search API Key (recommended) | Recommended |
| `MINIMAX_API_KEYS` | MiniMax Coding Plan Web Search (structured results) | Optional |
| `BOCHA_API_KEYS` | Bocha Search API Key (Chinese optimized) | Optional |
| `BRAVE_API_KEYS` | Brave Search API Key (US stocks optimized) | Optional |
| `SERPAPI_API_KEYS` | SerpAPI Backup search | Optional |
### Data Source Configuration

View File

@@ -627,12 +627,13 @@ def main() -> int:
search_service = None
analyzer = None
if config.bocha_api_keys or config.tavily_api_keys or config.brave_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 or config.minimax_api_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,
minimax_keys=config.minimax_api_keys,
news_max_age_days=config.news_max_age_days,
)

View File

@@ -25,6 +25,7 @@ def _get_search_service():
tavily_keys=config.tavily_api_keys,
brave_keys=config.brave_api_keys,
serpapi_keys=config.serpapi_keys,
minimax_keys=config.minimax_api_keys,
news_max_age_days=config.news_max_age_days,
)

View File

@@ -131,6 +131,7 @@ class Config:
# === 搜索引擎配置(支持多 Key 负载均衡)===
bocha_api_keys: List[str] = field(default_factory=list) # Bocha API Keys
minimax_api_keys: List[str] = field(default_factory=list) # MiniMax 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
@@ -526,6 +527,9 @@ class Config:
# 解析搜索引擎 API Keys支持多个 key逗号分隔
bocha_keys_str = os.getenv('BOCHA_API_KEYS', '')
bocha_api_keys = [k.strip() for k in bocha_keys_str.split(',') if k.strip()]
minimax_keys_str = os.getenv('MINIMAX_API_KEYS', '')
minimax_api_keys = [k.strip() for k in minimax_keys_str.split(',') if k.strip()]
tavily_keys_str = os.getenv('TAVILY_API_KEYS', '')
tavily_api_keys = [k.strip() for k in tavily_keys_str.split(',') if k.strip()]
@@ -593,6 +597,7 @@ class Config:
),
vision_provider_priority=os.getenv('VISION_PROVIDER_PRIORITY', 'gemini,anthropic,openai'),
bocha_api_keys=bocha_api_keys,
minimax_api_keys=minimax_api_keys,
tavily_api_keys=tavily_api_keys,
brave_api_keys=brave_api_keys,
serpapi_keys=serpapi_keys,
@@ -1085,13 +1090,14 @@ class Config:
# --- Search engine (informational only) ---
if not (
self.bocha_api_keys
or self.minimax_api_keys
or self.tavily_api_keys
or self.brave_api_keys
or self.serpapi_keys
):
issues.append(ConfigIssue(
severity="info",
message="未配置搜索引擎 API Key (Bocha/Tavily/Brave/SerpAPI),新闻搜索功能将不可用",
message="未配置搜索引擎 API Key (Bocha/MiniMax/Tavily/Brave/SerpAPI),新闻搜索功能将不可用",
field="BOCHA_API_KEY",
))
@@ -1108,6 +1114,7 @@ class Config:
or (self.discord_bot_token and self.discord_main_channel_id)
or self.discord_webhook_url
)
if not has_notification:
issues.append(ConfigIssue(
severity="warning",

View File

@@ -227,6 +227,20 @@ _FIELD_DEFINITIONS: Dict[str, Dict[str, Any]] = {
"validation": {},
"display_order": 21,
},
"BOCHA_API_KEYS": {
"title": "Bocha API Keys",
"description": "Comma-separated Bocha API keys (Chinese search optimization).",
"category": "data_source",
"data_type": "string",
"ui_control": "password",
"is_sensitive": True,
"is_required": False,
"is_editable": True,
"default_value": None,
"options": [],
"validation": {"multi_value": True, "delimiter": ","},
"display_order": 22,
},
"TAVILY_API_KEYS": {
"title": "Tavily API Keys",
"description": "Comma-separated Tavily API keys.",
@@ -283,6 +297,20 @@ _FIELD_DEFINITIONS: Dict[str, Dict[str, Any]] = {
"validation": {"multi_value": True, "delimiter": ","},
"display_order": 51,
},
"MINIMAX_API_KEYS": {
"title": "MiniMax API Key",
"description": "MiniMax API key (search priority: Bocha > MiniMax > Tavily > Brave > SerpAPI).",
"category": "data_source",
"data_type": "string",
"ui_control": "password",
"is_sensitive": True,
"is_required": False,
"is_editable": True,
"default_value": None,
"options": [],
"validation": {"multi_value": True, "delimiter": ","},
"display_order": 52,
},
"ENABLE_REALTIME_QUOTE": {
"title": "Enable Realtime Quote",
"description": "Enable realtime market quotes. Disable to only use historical close prices.",

View File

@@ -87,6 +87,7 @@ class StockAnalysisPipeline:
tavily_keys=self.config.tavily_api_keys,
brave_keys=self.config.brave_api_keys,
serpapi_keys=self.config.serpapi_keys,
minimax_keys=self.config.minimax_api_keys,
news_max_age_days=self.config.news_max_age_days,
)

View File

@@ -723,6 +723,232 @@ class BochaSearchProvider(BaseSearchProvider):
return '未知来源'
class MiniMaxSearchProvider(BaseSearchProvider):
"""
MiniMax Web Search (Coding Plan API)
Features:
- Backed by MiniMax Coding Plan subscription
- Returns structured organic results with title/link/snippet/date
- No native time-range parameter; time filtering is done via query
augmentation and client-side date filtering
- Circuit-breaker protection: 3 consecutive failures -> 300s cooldown
API endpoint: POST https://api.minimaxi.com/v1/coding_plan/search
"""
API_ENDPOINT = "https://api.minimaxi.com/v1/coding_plan/search"
# Circuit-breaker settings
_CB_FAILURE_THRESHOLD = 3
_CB_COOLDOWN_SECONDS = 300 # 5 minutes
def __init__(self, api_keys: List[str]):
super().__init__(api_keys, "MiniMax")
# Circuit breaker state
self._consecutive_failures = 0
self._circuit_open_until: float = 0.0
@property
def is_available(self) -> bool:
"""Check availability considering circuit breaker state."""
if not super().is_available:
return False
if self._consecutive_failures >= self._CB_FAILURE_THRESHOLD:
if time.time() < self._circuit_open_until:
return False
# Cooldown expired -> half-open, allow one probe
return True
def _record_success(self, key: str) -> None:
super()._record_success(key)
# Reset circuit breaker on success
self._consecutive_failures = 0
self._circuit_open_until = 0.0
def _record_error(self, key: str) -> None:
super()._record_error(key)
self._consecutive_failures += 1
if self._consecutive_failures >= self._CB_FAILURE_THRESHOLD:
self._circuit_open_until = time.time() + self._CB_COOLDOWN_SECONDS
logger.warning(
f"[MiniMax] Circuit breaker OPEN "
f"{self._consecutive_failures} consecutive failures, "
f"cooldown {self._CB_COOLDOWN_SECONDS}s"
)
# ------------------------------------------------------------------
# Time-range helpers
# ------------------------------------------------------------------
@staticmethod
def _time_hint(days: int, is_chinese: bool = True) -> str:
"""Build a time-hint string to append to the search query."""
if is_chinese:
if days <= 1:
return "今天"
elif days <= 3:
return "最近三天"
elif days <= 7:
return "最近一周"
else:
return "最近一个月"
else:
if days <= 1:
return "today"
elif days <= 3:
return "past 3 days"
elif days <= 7:
return "past week"
else:
return "past month"
@staticmethod
def _is_within_days(date_str: Optional[str], days: int) -> bool:
"""Check whether *date_str* falls within the last *days* days.
Accepts common formats: ``2025-06-01``, ``2025/06/01``,
``Jun 1, 2025``, ISO-8601 with timezone, etc.
Returns True when date_str is None or unparseable (keep the result).
"""
if not date_str:
return True
try:
from dateutil import parser as dateutil_parser
dt = dateutil_parser.parse(date_str, fuzzy=True)
from datetime import timedelta, timezone
now = datetime.now(timezone.utc) if dt.tzinfo else datetime.now()
return (now - dt) <= timedelta(days=days + 1) # +1 buffer
except Exception:
return True # Keep result when date is unparseable
# ------------------------------------------------------------------
def _do_search(self, query: str, api_key: str, max_results: int, days: int = 7) -> SearchResponse:
"""Execute MiniMax web search."""
try:
# Detect language hint from query (simple heuristic)
has_cjk = any('\u4e00' <= ch <= '\u9fff' for ch in query)
time_hint = self._time_hint(days, is_chinese=has_cjk)
augmented_query = f"{query} {time_hint}"
headers = {
'Authorization': f'Bearer {api_key}',
'Content-Type': 'application/json',
'MM-API-Source': 'Minimax-MCP',
}
payload = {"q": augmented_query}
response = _post_with_retry(
self.API_ENDPOINT, headers=headers, json=payload, timeout=15
)
# HTTP error handling
if response.status_code != 200:
error_msg = self._parse_http_error(response)
logger.warning(f"[MiniMax] Search failed: {error_msg}")
return SearchResponse(
query=query,
results=[],
provider=self.name,
success=False,
error_message=error_msg,
)
data = response.json()
# Check base_resp status
base_resp = data.get('base_resp', {})
if base_resp.get('status_code', 0) != 0:
error_msg = base_resp.get('status_msg', 'Unknown API error')
return SearchResponse(
query=query,
results=[],
provider=self.name,
success=False,
error_message=error_msg,
)
logger.info(f"[MiniMax] Search done, query='{query}'")
logger.debug(f"[MiniMax] Raw response keys: {list(data.keys())}")
# Parse organic results
results: List[SearchResult] = []
for item in data.get('organic', []):
date_val = item.get('date')
# Client-side time filtering
if not self._is_within_days(date_val, days):
continue
results.append(SearchResult(
title=item.get('title', ''),
snippet=(item.get('snippet', '') or '')[:500],
url=item.get('link', ''),
source=self._extract_domain(item.get('link', '')),
published_date=date_val,
))
if len(results) >= max_results:
break
logger.info(f"[MiniMax] Parsed {len(results)} results (after time filter)")
return SearchResponse(
query=query,
results=results,
provider=self.name,
success=True,
)
except requests.exceptions.Timeout:
error_msg = "Request timeout"
logger.error(f"[MiniMax] {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"Network error: {e}"
logger.error(f"[MiniMax] {error_msg}")
return SearchResponse(
query=query, results=[], provider=self.name,
success=False, error_message=error_msg,
)
except Exception as e:
error_msg = f"Unexpected error: {e}"
logger.error(f"[MiniMax] {error_msg}")
return SearchResponse(
query=query, results=[], provider=self.name,
success=False, error_message=error_msg,
)
@staticmethod
def _parse_http_error(response) -> str:
"""Parse HTTP error response from MiniMax API."""
try:
ct = response.headers.get('content-type', '')
if 'json' in ct:
err = response.json()
base_resp = err.get('base_resp', {})
msg = base_resp.get('status_msg') or err.get('message') or str(err)
return msg
return response.text[:200]
except Exception:
return f"HTTP {response.status_code}: {response.text[:200]}"
@staticmethod
def _extract_domain(url: str) -> str:
"""Extract domain from URL as source label."""
try:
from urllib.parse import urlparse
parsed = urlparse(url)
domain = parsed.netloc.replace('www.', '')
return domain or '未知来源'
except Exception:
return '未知来源'
class BraveSearchProvider(BaseSearchProvider):
"""
Brave Search 搜索引擎
@@ -935,6 +1161,7 @@ class SearchService:
tavily_keys: Optional[List[str]] = None,
brave_keys: Optional[List[str]] = None,
serpapi_keys: Optional[List[str]] = None,
minimax_keys: Optional[List[str]] = None,
news_max_age_days: int = 3,
):
"""
@@ -945,6 +1172,7 @@ class SearchService:
tavily_keys: Tavily API Key 列表
brave_keys: Brave Search API Key 列表
serpapi_keys: SerpAPI Key 列表
minimax_keys: MiniMax API Key 列表
news_max_age_days: 新闻最大时效(天)
"""
self._providers: List[BaseSearchProvider] = []
@@ -970,6 +1198,11 @@ class SearchService:
if serpapi_keys:
self._providers.append(SerpAPISearchProvider(serpapi_keys))
logger.info(f"已配置 SerpAPI 搜索,共 {len(serpapi_keys)} 个 API Key")
# 5. MiniMaxCoding Plan Web Search结构化结果
if minimax_keys:
self._providers.append(MiniMaxSearchProvider(minimax_keys))
logger.info(f"已配置 MiniMax 搜索,共 {len(minimax_keys)} 个 API Key")
if not self._providers:
logger.warning("未配置任何搜索引擎 API Key新闻搜索功能将不可用")
@@ -1551,6 +1784,7 @@ def get_search_service() -> SearchService:
tavily_keys=config.tavily_api_keys,
brave_keys=config.brave_api_keys,
serpapi_keys=config.serpapi_keys,
minimax_keys=config.minimax_api_keys,
news_max_age_days=config.news_max_age_days,
)