feat: add OpenAI-compatible API support

This commit is contained in:
zhulinsen
2026-01-11 15:46:23 +08:00
parent 3ba7342c3e
commit 5d5c095be5
7 changed files with 234 additions and 41 deletions

View File

@@ -12,15 +12,38 @@ STOCK_LIST=600519,300750,002594
# Tushare Pro Token可选从 https://tushare.pro 获取)
TUSHARE_TOKEN=your_tushare_token_here
# AI 分析配置
# Gemini API Key从 Google AI Studio 获取
GEMINI_API_KEY=your_gemini_api_key_here
# Gemini 模型名称
# ===================================
# AI 模型配置(二选一,至少配置一个
# ===================================
#
# 【方案一】使用 Gemini推荐有免费额度
# 从 https://aistudio.google.com/ 获取免费 API Key
# 留空或删除此行则跳过 Gemini直接使用方案二
#
GEMINI_API_KEY=
GEMINI_MODEL=gemini-3-flash-preview
GEMINI_MODEL_FALLBACK=gemini-2.5-flash
# Gemini API 请求间隔(秒,防止限流)
GEMINI_REQUEST_DELAY=2.0
# 【方案二】使用 OpenAI 兼容 API支持多种国产模型
# 如果不想用 Gemini可以只配置下面三项去掉注释
# 支持OpenAI、DeepSeek、通义千问、Moonshot、智谱GLM 等
#
# ┌──────────────────────────────────────────────────────────────┐
# │ 配置规则: │
# │ OPENAI_API_KEY = 必填,你的 API Key │
# │ OPENAI_BASE_URL = 可选,第三方 API 地址OpenAI官方可不填
# │ OPENAI_MODEL = 可选,模型名称(默认 gpt-4o-mini
# └──────────────────────────────────────────────────────────────┘
#
# --- OpenAI 官方(只需填 Key---
# OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxx
#
# --- DeepSeek国产便宜好获取---
# OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxx
# OPENAI_BASE_URL=https://api.deepseek.com/v1
# OPENAI_MODEL=deepseek-chat
# 搜索引擎配置(用于获取股票新闻)
# Tavily API Keys支持多个逗号分隔
TAVILY_API_KEYS=your_tavily_key_here

View File

@@ -12,6 +12,14 @@
- 飞书机器人支持
- Web 管理界面
## [1.1.0] - 2026-01-11
### 新增
- 🤖 OpenAI 兼容 API 支持
- 支持 DeepSeek、通义千问、Moonshot、智谱 GLM 等
- Gemini 和 OpenAI 格式二选一
- 自动降级重试机制
## [1.0.0] - 2026-01-10
### 新增

View File

@@ -18,11 +18,14 @@
- **定时推送** - 支持企业微信机器人自动推送
- **零成本部署** - GitHub Actions 免费运行,无需服务器
- **💰 白嫖 Gemini API** - Google AI Studio 提供免费额度,个人使用完全够用
- **🔄 多模型支持** - 支持 OpenAI 兼容 APIDeepSeek、通义千问等作为备选
### 📊 数据来源
- **行情数据**: AkShare免费、Tushare、Baostock、YFinance
- **新闻搜索**: Tavily、SerpAPI
- **AI 分析**: Google Geminigemini-3-flash-preview—— [免费获取 API Key](https://aistudio.google.com/)
- **AI 分析**:
- 主力Google Geminigemini-3-flash-preview—— [免费获取](https://aistudio.google.com/)
- 备选应大家要求也支持了OpenAI 兼容 APIDeepSeek、通义千问、Moonshot 等)
### 🛡️ 交易理念内置
-**严禁追高** - 乖离率 > 5% 自动标记「危险」
@@ -46,12 +49,17 @@
| Secret 名称 | 说明 | 必填 |
|------------|------|:----:|
| `GEMINI_API_KEY` | [Google AI Studio](https://aistudio.google.com/) 获取 | ✅ |
| `GEMINI_API_KEY` | [Google AI Studio](https://aistudio.google.com/) 获取 | ✅* |
| `WECHAT_WEBHOOK_URL` | 企业微信机器人 Webhook | ✅ |
| `STOCK_LIST` | 自选股代码,如 `600519,300750,002594` | ✅ |
| `TAVILY_API_KEYS` | [Tavily](https://tavily.com/) 搜索 API新闻搜索 | 推荐 |
| `SERPAPI_API_KEYS` | [SerpAPI](https://serpapi.com/) Key | 可选 |
| `TUSHARE_TOKEN` | [Tushare Pro](https://tushare.pro/) Token | 可选 |
| `OPENAI_API_KEY` | OpenAI 兼容 API Key备选 | 可选 |
| `OPENAI_BASE_URL` | OpenAI 兼容 API 地址 | 可选 |
| `OPENAI_MODEL` | OpenAI 兼容模型名称 | 可选 |
> *注:`GEMINI_API_KEY` 和 `OPENAI_API_KEY` 至少配置一个,系统优先使用 Gemini
#### 3. 启用 Actions

View File

@@ -384,20 +384,79 @@ class GeminiAnalyzer:
def __init__(self, api_key: Optional[str] = None):
"""
初始化 Gemini 分析器
初始化 AI 分析器
优先级Gemini > OpenAI 兼容 API
Args:
api_key: Gemini API Key可选默认从配置读取
"""
self._api_key = api_key or get_config().gemini_api_key
config = get_config()
self._api_key = api_key or config.gemini_api_key
self._model = None
self._current_model_name = None # 当前使用的模型名称
self._using_fallback = False # 是否正在使用备选模型
self._use_openai = False # 是否使用 OpenAI 兼容 API
self._openai_client = None # OpenAI 客户端
if self._api_key:
self._init_model()
# 检查 Gemini API Key 是否有效(过滤占位符)
gemini_key_valid = self._api_key and not self._api_key.startswith('your_') and len(self._api_key) > 10
# 优先尝试初始化 Gemini
if gemini_key_valid:
try:
self._init_model()
except Exception as e:
logger.warning(f"Gemini 初始化失败: {e},尝试 OpenAI 兼容 API")
self._init_openai_fallback()
else:
logger.warning("Gemini API Key 未配置,AI 分析功能将不可用")
# Gemini Key 未配置,尝试 OpenAI
logger.info("Gemini API Key 未配置,尝试使用 OpenAI 兼容 API")
self._init_openai_fallback()
# 两者都未配置
if not self._model and not self._openai_client:
logger.warning("未配置任何 AI API KeyAI 分析功能将不可用")
def _init_openai_fallback(self) -> None:
"""
初始化 OpenAI 兼容 API 作为备选
支持所有 OpenAI 格式的 API包括
- OpenAI 官方
- DeepSeek
- 通义千问
- Moonshot 等
"""
config = get_config()
# 检查 OpenAI API Key 是否有效(过滤占位符)
openai_key_valid = (
config.openai_api_key and
not config.openai_api_key.startswith('your_') and
len(config.openai_api_key) > 10
)
if not openai_key_valid:
logger.debug("OpenAI 兼容 API 未配置或配置无效")
return
try:
from openai import OpenAI
# base_url 可选,不填则使用 OpenAI 官方默认地址
client_kwargs = {"api_key": config.openai_api_key}
if config.openai_base_url and config.openai_base_url.startswith('http'):
client_kwargs["base_url"] = config.openai_base_url
self._openai_client = OpenAI(**client_kwargs)
self._current_model_name = config.openai_model
self._use_openai = True
logger.info(f"OpenAI 兼容 API 初始化成功 (base_url: {config.openai_base_url}, model: {config.openai_model})")
except ImportError:
logger.error("未安装 openai 库,请运行: pip install openai")
except Exception as e:
logger.error(f"OpenAI 兼容 API 初始化失败: {e}")
def _init_model(self) -> None:
"""
@@ -472,16 +531,11 @@ class GeminiAnalyzer:
def is_available(self) -> bool:
"""检查分析器是否可用"""
return self._model is not None
return self._model is not None or self._openai_client is not None
def _call_api_with_retry(self, prompt: str, generation_config: dict) -> str:
def _call_openai_api(self, prompt: str, generation_config: dict) -> str:
"""
调用 Gemini API带有重试和模型切换机制
处理 429 限流错误:
1. 先指数退避重试
2. 多次失败后切换到备选模型
3. 备选模型也失败则抛出异常
调用 OpenAI 兼容 API
Args:
prompt: 提示词
@@ -494,6 +548,69 @@ class GeminiAnalyzer:
max_retries = config.gemini_max_retries
base_delay = config.gemini_retry_delay
for attempt in range(max_retries):
try:
if attempt > 0:
delay = base_delay * (2 ** (attempt - 1))
delay = min(delay, 60)
logger.info(f"[OpenAI] 第 {attempt + 1} 次重试,等待 {delay:.1f} 秒...")
time.sleep(delay)
response = self._openai_client.chat.completions.create(
model=self._current_model_name,
messages=[
{"role": "system", "content": self.SYSTEM_PROMPT},
{"role": "user", "content": prompt}
],
temperature=generation_config.get('temperature', 0.7),
max_tokens=generation_config.get('max_output_tokens', 8192),
)
if response and response.choices and response.choices[0].message.content:
return response.choices[0].message.content
else:
raise ValueError("OpenAI API 返回空响应")
except Exception as e:
error_str = str(e)
is_rate_limit = '429' in error_str or 'rate' in error_str.lower() or 'quota' in error_str.lower()
if is_rate_limit:
logger.warning(f"[OpenAI] API 限流,第 {attempt + 1}/{max_retries} 次尝试: {error_str[:100]}")
else:
logger.warning(f"[OpenAI] API 调用失败,第 {attempt + 1}/{max_retries} 次尝试: {error_str[:100]}")
if attempt == max_retries - 1:
raise
raise Exception("OpenAI API 调用失败,已达最大重试次数")
def _call_api_with_retry(self, prompt: str, generation_config: dict) -> str:
"""
调用 AI API带有重试和模型切换机制
优先级Gemini > Gemini 备选模型 > OpenAI 兼容 API
处理 429 限流错误:
1. 先指数退避重试
2. 多次失败后切换到备选模型
3. Gemini 完全失败后尝试 OpenAI
Args:
prompt: 提示词
generation_config: 生成配置
Returns:
响应文本
"""
# 如果已经在使用 OpenAI 模式,直接调用 OpenAI
if self._use_openai:
return self._call_openai_api(prompt, generation_config)
config = get_config()
max_retries = config.gemini_max_retries
base_delay = config.gemini_retry_delay
last_error = None
tried_fallback = getattr(self, '_using_fallback', False)
@@ -503,7 +620,7 @@ class GeminiAnalyzer:
if attempt > 0:
delay = base_delay * (2 ** (attempt - 1)) # 指数退避: 5, 10, 20, 40...
delay = min(delay, 60) # 最大60秒
logger.info(f"[LLM] 第 {attempt + 1} 次重试,等待 {delay:.1f} 秒...")
logger.info(f"[Gemini] 第 {attempt + 1} 次重试,等待 {delay:.1f} 秒...")
time.sleep(delay)
response = self._model.generate_content(
@@ -525,21 +642,40 @@ class GeminiAnalyzer:
is_rate_limit = '429' in error_str or 'quota' in error_str.lower() or 'rate' in error_str.lower()
if is_rate_limit:
logger.warning(f"[LLM] API 限流 (429),第 {attempt + 1}/{max_retries} 次尝试: {error_str[:100]}")
logger.warning(f"[Gemini] API 限流 (429),第 {attempt + 1}/{max_retries} 次尝试: {error_str[:100]}")
# 如果已经重试了一半次数且还没切换过备选模型,尝试切换
if attempt >= max_retries // 2 and not tried_fallback:
if self._switch_to_fallback_model():
tried_fallback = True
logger.info("[LLM] 已切换到备选模型,继续重试")
logger.info("[Gemini] 已切换到备选模型,继续重试")
else:
logger.warning("[LLM] 切换备选模型失败,继续使用当前模型重试")
logger.warning("[Gemini] 切换备选模型失败,继续使用当前模型重试")
else:
# 非限流错误,记录并继续重试
logger.warning(f"[LLM] API 调用失败,第 {attempt + 1}/{max_retries} 次尝试: {error_str[:100]}")
logger.warning(f"[Gemini] API 调用失败,第 {attempt + 1}/{max_retries} 次尝试: {error_str[:100]}")
# 所有重试都失败
raise last_error or Exception("Gemini API 调用失败,已达最大重试次数")
# Gemini 所有重试都失败,尝试 OpenAI 兼容 API
if self._openai_client:
logger.warning("[Gemini] 所有重试失败,切换到 OpenAI 兼容 API")
try:
return self._call_openai_api(prompt, generation_config)
except Exception as openai_error:
logger.error(f"[OpenAI] 备选 API 也失败: {openai_error}")
raise last_error or openai_error
elif config.openai_api_key and config.openai_base_url:
# 尝试懒加载初始化 OpenAI
logger.warning("[Gemini] 所有重试失败,尝试初始化 OpenAI 兼容 API")
self._init_openai_fallback()
if self._openai_client:
try:
return self._call_openai_api(prompt, generation_config)
except Exception as openai_error:
logger.error(f"[OpenAI] 备选 API 也失败: {openai_error}")
raise last_error or openai_error
# 所有方式都失败
raise last_error or Exception("所有 AI API 调用失败,已达最大重试次数")
def analyze(
self,

View File

@@ -44,6 +44,11 @@ class Config:
gemini_max_retries: int = 5 # 最大重试次数
gemini_retry_delay: float = 5.0 # 重试基础延时(秒)
# OpenAI 兼容 API备选当 Gemini 不可用时使用)
openai_api_key: Optional[str] = None
openai_base_url: Optional[str] = None # 如: https://api.openai.com/v1
openai_model: str = "gpt-4o-mini" # OpenAI 兼容模型名称
# === 搜索引擎配置(支持多 Key 负载均衡)===
tavily_api_keys: List[str] = field(default_factory=list) # Tavily API Keys
serpapi_keys: List[str] = field(default_factory=list) # SerpAPI Keys
@@ -139,6 +144,9 @@ class Config:
gemini_request_delay=float(os.getenv('GEMINI_REQUEST_DELAY', '2.0')),
gemini_max_retries=int(os.getenv('GEMINI_MAX_RETRIES', '5')),
gemini_retry_delay=float(os.getenv('GEMINI_RETRY_DELAY', '5.0')),
openai_api_key=os.getenv('OPENAI_API_KEY'),
openai_base_url=os.getenv('OPENAI_BASE_URL'),
openai_model=os.getenv('OPENAI_MODEL', 'gpt-4o-mini'),
tavily_api_keys=tavily_api_keys,
serpapi_keys=serpapi_keys,
wechat_webhook_url=os.getenv('WECHAT_WEBHOOK_URL'),
@@ -172,8 +180,10 @@ class Config:
if not self.tushare_token:
warnings.append("提示:未配置 Tushare Token将使用其他数据源")
if not self.gemini_api_key:
warnings.append("警告:未配置 Gemini API KeyAI 分析功能将不可用")
if not self.gemini_api_key and not self.openai_api_key:
warnings.append("警告:未配置 Gemini 或 OpenAI API KeyAI 分析功能将不可用")
elif not self.gemini_api_key:
warnings.append("提示:未配置 Gemini API Key将使用 OpenAI 兼容 API")
if not self.tavily_api_keys and not self.serpapi_keys:
warnings.append("提示:未配置搜索引擎 API Key (Tavily/SerpAPI),新闻搜索功能将不可用")

View File

@@ -321,8 +321,8 @@ class MarketAnalyzer:
Returns:
大盘复盘报告文本
"""
if not self.analyzer:
logger.warning("[大盘] AI分析器未配置使用模板生成报告")
if not self.analyzer or not self.analyzer.is_available():
logger.warning("[大盘] AI分析器未配置或不可用,使用模板生成报告")
return self._generate_template_review(overview, news)
# 构建 Prompt
@@ -331,17 +331,24 @@ class MarketAnalyzer:
try:
logger.info("[大盘] 调用大模型生成复盘报告...")
# 使用 analyzer 的内部模型直接调用
response = self.analyzer._model.generate_content(
prompt,
generation_config={
'temperature': 0.7,
'max_output_tokens': 2048,
}
)
generation_config = {
'temperature': 0.7,
'max_output_tokens': 2048,
}
if response and response.text:
review = response.text.strip()
# 根据 analyzer 使用的 API 类型调用
if self.analyzer._use_openai:
# 使用 OpenAI 兼容 API
review = self.analyzer._call_openai_api(prompt, generation_config)
else:
# 使用 Gemini API
response = self.analyzer._model.generate_content(
prompt,
generation_config=generation_config,
)
review = response.text.strip() if response and response.text else None
if review:
logger.info(f"[大盘] 复盘报告生成成功,长度: {len(review)} 字符")
return review
else:

View File

@@ -20,6 +20,7 @@ numpy>=1.24.0 # 数值计算
# AI 分析
google-generativeai>=0.8.0 # Gemini API
openai>=1.0.0 # OpenAI 兼容 API可选支持 DeepSeek/通义千问等)
# 搜索引擎(用于获取股票新闻)
tavily-python>=0.3.0 # Tavily 搜索 API每月 1000 次免费)