mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
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:
@@ -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 Key(Coding Plan Web Search,支持多个,逗号分隔)
|
||||
# 获取: https://platform.minimaxi.com/
|
||||
# MINIMAX_API_KEYS=your_minimax_key_here
|
||||
# Tavily API Keys(支持多个,逗号分隔)
|
||||
TAVILY_API_KEYS=
|
||||
# SerpAPI Keys(支持多个,逗号分隔)
|
||||
|
||||
@@ -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用逗号分隔) | 可选 |
|
||||
|
||||
@@ -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)。配置渠道后自动推断。',
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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 且不接受未知字段的客户端,需评估并适配该字段变更。
|
||||
|
||||
@@ -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) | 可选 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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) 備用搜索 | 可選 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 密钥(用逗号分隔) |
|
||||
|
||||
|
||||
@@ -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 备用搜索 | 可选 |
|
||||
|
||||
@@ -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
|
||||
|
||||
3
main.py
3
main.py
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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. MiniMax(Coding 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,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user