mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
feat: add OpenAI-compatible API support
This commit is contained in:
33
.env.example
33
.env.example
@@ -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
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
- 飞书机器人支持
|
||||
- Web 管理界面
|
||||
|
||||
## [1.1.0] - 2026-01-11
|
||||
|
||||
### 新增
|
||||
- 🤖 OpenAI 兼容 API 支持
|
||||
- 支持 DeepSeek、通义千问、Moonshot、智谱 GLM 等
|
||||
- Gemini 和 OpenAI 格式二选一
|
||||
- 自动降级重试机制
|
||||
|
||||
## [1.0.0] - 2026-01-10
|
||||
|
||||
### 新增
|
||||
|
||||
12
README.md
12
README.md
@@ -18,11 +18,14 @@
|
||||
- **定时推送** - 支持企业微信机器人自动推送
|
||||
- **零成本部署** - GitHub Actions 免费运行,无需服务器
|
||||
- **💰 白嫖 Gemini API** - Google AI Studio 提供免费额度,个人使用完全够用
|
||||
- **🔄 多模型支持** - 支持 OpenAI 兼容 API(DeepSeek、通义千问等)作为备选
|
||||
|
||||
### 📊 数据来源
|
||||
- **行情数据**: AkShare(免费)、Tushare、Baostock、YFinance
|
||||
- **新闻搜索**: Tavily、SerpAPI
|
||||
- **AI 分析**: Google Gemini(gemini-3-flash-preview)—— [免费获取 API Key](https://aistudio.google.com/)
|
||||
- **AI 分析**:
|
||||
- 主力:Google Gemini(gemini-3-flash-preview)—— [免费获取](https://aistudio.google.com/)
|
||||
- 备选:应大家要求,也支持了OpenAI 兼容 API(DeepSeek、通义千问、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
|
||||
|
||||
|
||||
176
analyzer.py
176
analyzer.py
@@ -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 Key,AI 分析功能将不可用")
|
||||
|
||||
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,
|
||||
|
||||
14
config.py
14
config.py
@@ -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 Key,AI 分析功能将不可用")
|
||||
if not self.gemini_api_key and not self.openai_api_key:
|
||||
warnings.append("警告:未配置 Gemini 或 OpenAI API Key,AI 分析功能将不可用")
|
||||
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),新闻搜索功能将不可用")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 次免费)
|
||||
|
||||
Reference in New Issue
Block a user