feat: 自定义 Webhook 支持 (钉钉/Discord/Slack/Bark 等)

This commit is contained in:
zhulinsen
2026-01-12 14:19:23 +08:00
parent 5b80815d66
commit 704ff9a7c2
7 changed files with 157 additions and 6 deletions

View File

@@ -79,6 +79,12 @@ SERPAPI_API_KEYS=your_serpapi_key_here
# EMAIL_SENDER=your_email@qq.com
# EMAIL_PASSWORD=your_email_auth_code
# EMAIL_RECEIVERS=receiver@example.com # 可选,留空则发给自己
#
# 【方式五】自定义 Webhook支持多个逗号分隔
# 适用于钉钉、Discord、Slack、Bark、自建服务等任意支持 POST JSON 的 Webhook
# 系统会自动识别常见服务并使用对应格式
#
# CUSTOM_WEBHOOK_URLS=https://oapi.dingtalk.com/robot/send?access_token=xxx,https://hooks.slack.com/services/xxx
# 数据库路径
DATABASE_PATH=./data/stock_analysis.db

View File

@@ -73,6 +73,8 @@ jobs:
EMAIL_SENDER: ${{ secrets.EMAIL_SENDER }}
EMAIL_PASSWORD: ${{ secrets.EMAIL_PASSWORD }}
EMAIL_RECEIVERS: ${{ secrets.EMAIL_RECEIVERS }}
# 方式五:自定义 Webhook支持钉钉、Discord、Slack、Bark等多个用逗号分隔
CUSTOM_WEBHOOK_URLS: ${{ secrets.CUSTOM_WEBHOOK_URLS }}
# 自选股列表 (从 secrets 或使用默认值)
STOCK_LIST: ${{ secrets.STOCK_LIST || '600519' }}

View File

@@ -8,9 +8,17 @@
## [Unreleased]
### 计划中
- 钉钉机器人支持
- Web 管理界面
## [1.3.0] - 2026-01-12
### 新增
- 🔗 自定义 Webhook 支持
- 支持任意 POST JSON 的 Webhook 端点
- 自动识别钉钉、Discord、Slack、Bark 等常见服务格式
- 支持配置多个 Webhook逗号分隔
- 通过 `CUSTOM_WEBHOOK_URLS` 环境变量配置
## [1.2.0] - 2026-01-11
### 新增

View File

@@ -349,13 +349,21 @@ git push -u origin main
| Secret 名称 | 说明 | 必填 |
|------------|------|------|
| `GEMINI_API_KEY` | Gemini AI API Key | ✅ |
| `WECHAT_WEBHOOK_URL` | 企业微信机器人 Webhook | |
| `WECHAT_WEBHOOK_URL` | 企业微信机器人 Webhook | 可选* |
| `FEISHU_WEBHOOK_URL` | 飞书机器人 Webhook | 可选* |
| `TELEGRAM_BOT_TOKEN` | Telegram Bot Token | 可选* |
| `TELEGRAM_CHAT_ID` | Telegram Chat ID | 可选* |
| `EMAIL_SENDER` | 发件人邮箱 | 可选* |
| `EMAIL_PASSWORD` | 邮箱授权码 | 可选* |
| `CUSTOM_WEBHOOK_URLS` | 自定义 Webhook多个逗号分隔 | 可选* |
| `STOCK_LIST` | 自选股列表,如 `600519,300750` | ✅ |
| `TAVILY_API_KEYS` | Tavily 搜索 API Key | 推荐 |
| `SERPAPI_API_KEYS` | SerpAPI Key | 可选 |
| `TUSHARE_TOKEN` | Tushare Token | 可选 |
| `GEMINI_MODEL` | 模型名称(默认 gemini-2.0-flash | 可选 |
> *注:通知渠道至少配置一个,支持多渠道同时推送
#### 3. 验证 Workflow 文件
确保 `.github/workflows/daily_analysis.yml` 文件存在且已提交:

View File

@@ -69,8 +69,11 @@
| `EMAIL_SENDER` | 发件人邮箱(如 `xxx@qq.com` | 可选 |
| `EMAIL_PASSWORD` | 邮箱授权码(非登录密码) | 可选 |
| `EMAIL_RECEIVERS` | 收件人邮箱(留空则发给自己) | 可选 |
| `CUSTOM_WEBHOOK_URLS` | 自定义 Webhook多个用逗号分隔 | 可选 |
> *注:至少配置一个渠道,配置多个则同时推送到所有渠道
>
> 自定义 Webhook 支持钉钉、Discord、Slack、Bark、自建服务等任意支持 POST JSON 的 Webhook
**其他配置**
@@ -231,10 +234,8 @@ daily_stock_analysis/
- [x] 飞书机器人
- [x] Telegram Bot
- [x] 邮件通知SMTP
- [ ] 钉钉机器人
- [ ] Discord Webhook
- [ ] Slack Webhook
- [ ] iOS/Android 推送Bark/Pushover
- [x] 自定义 Webhook支持钉钉、Discord、Slack、Bark 等)
- [ ] iOS/Android 推送Pushover
### 🤖 AI 模型支持
- [x] Google Gemini主力免费额度

View File

@@ -70,6 +70,10 @@ class Config:
email_password: Optional[str] = None # 邮箱密码/授权码
email_receivers: List[str] = field(default_factory=list) # 收件人列表(留空则发给自己)
# 自定义 Webhook支持多个逗号分隔
# 适用于钉钉、Discord、Slack、自建服务等任意支持 POST JSON 的 Webhook
custom_webhook_urls: List[str] = field(default_factory=list)
# === 数据库配置 ===
database_path: str = "./data/stock_analysis.db"
@@ -170,6 +174,7 @@ class Config:
email_sender=os.getenv('EMAIL_SENDER'),
email_password=os.getenv('EMAIL_PASSWORD'),
email_receivers=[r.strip() for r in os.getenv('EMAIL_RECEIVERS', '').split(',') if r.strip()],
custom_webhook_urls=[u.strip() for u in os.getenv('CUSTOM_WEBHOOK_URLS', '').split(',') if u.strip()],
database_path=os.getenv('DATABASE_PATH', './data/stock_analysis.db'),
log_dir=os.getenv('LOG_DIR', './logs'),
log_level=os.getenv('LOG_LEVEL', 'INFO'),

View File

@@ -38,6 +38,7 @@ class NotificationChannel(Enum):
FEISHU = "feishu" # 飞书
TELEGRAM = "telegram" # Telegram
EMAIL = "email" # 邮件
CUSTOM = "custom" # 自定义 Webhook
UNKNOWN = "unknown" # 未知
@@ -80,6 +81,7 @@ class ChannelDetector:
NotificationChannel.FEISHU: "飞书",
NotificationChannel.TELEGRAM: "Telegram",
NotificationChannel.EMAIL: "邮件",
NotificationChannel.CUSTOM: "自定义Webhook",
NotificationChannel.UNKNOWN: "未知渠道",
}
return names.get(channel, "未知渠道")
@@ -128,6 +130,9 @@ class NotificationService:
'receivers': config.email_receivers or ([config.email_sender] if config.email_sender else []),
}
# 自定义 Webhook 配置
self._custom_webhook_urls = getattr(config, 'custom_webhook_urls', []) or []
# 检测所有已配置的渠道
self._available_channels = self._detect_all_channels()
@@ -162,6 +167,10 @@ class NotificationService:
if self._is_email_configured():
channels.append(NotificationChannel.EMAIL)
# 自定义 Webhook
if self._custom_webhook_urls:
channels.append(NotificationChannel.CUSTOM)
return channels
def _is_telegram_configured(self) -> bool:
@@ -1302,6 +1311,116 @@ class NotificationService:
return result
def send_to_custom(self, content: str) -> bool:
"""
推送消息到自定义 Webhook
支持任意接受 POST JSON 的 Webhook 端点
默认发送格式:{"text": "消息内容", "content": "消息内容"}
适用于:
- 钉钉机器人
- Discord Webhook
- Slack Incoming Webhook
- 自建通知服务
- 其他支持 POST JSON 的服务
Args:
content: 消息内容Markdown 格式)
Returns:
是否至少有一个 Webhook 发送成功
"""
if not self._custom_webhook_urls:
logger.warning("未配置自定义 Webhook跳过推送")
return False
success_count = 0
for i, url in enumerate(self._custom_webhook_urls):
try:
# 通用 JSON 格式,兼容大多数 Webhook
# 钉钉格式: {"msgtype": "text", "text": {"content": "xxx"}}
# Slack 格式: {"text": "xxx"}
# Discord 格式: {"content": "xxx"}
# 检测 URL 类型并构造对应格式
payload = self._build_custom_webhook_payload(url, content)
headers = {
'Content-Type': 'application/json',
'User-Agent': 'StockAnalysis/1.0'
}
response = requests.post(
url,
json=payload,
headers=headers,
timeout=30
)
if response.status_code == 200:
logger.info(f"自定义 Webhook {i+1} 推送成功")
success_count += 1
else:
logger.error(f"自定义 Webhook {i+1} 推送失败: HTTP {response.status_code}")
logger.debug(f"响应内容: {response.text[:200]}")
except Exception as e:
logger.error(f"自定义 Webhook {i+1} 推送异常: {e}")
logger.info(f"自定义 Webhook 推送完成:成功 {success_count}/{len(self._custom_webhook_urls)}")
return success_count > 0
def _build_custom_webhook_payload(self, url: str, content: str) -> dict:
"""
根据 URL 构建对应的 Webhook payload
自动识别常见服务并使用对应格式
"""
url_lower = url.lower()
# 钉钉机器人
if 'dingtalk' in url_lower or 'oapi.dingtalk.com' in url_lower:
return {
"msgtype": "markdown",
"markdown": {
"title": "股票分析报告",
"text": content
}
}
# Discord Webhook
if 'discord.com/api/webhooks' in url_lower or 'discordapp.com/api/webhooks' in url_lower:
# Discord 限制 2000 字符
truncated = content[:1900] + "..." if len(content) > 1900 else content
return {
"content": truncated
}
# Slack Incoming Webhook
if 'hooks.slack.com' in url_lower:
return {
"text": content,
"mrkdwn": True
}
# Bark (iOS 推送)
if 'api.day.app' in url_lower:
return {
"title": "股票分析报告",
"body": content[:4000], # Bark 限制
"group": "stock"
}
# 通用格式(兼容大多数服务)
return {
"text": content,
"content": content,
"message": content,
"body": content
}
def send(self, content: str) -> bool:
"""
统一发送接口 - 向所有已配置的渠道发送
@@ -1335,6 +1454,8 @@ class NotificationService:
result = self.send_to_telegram(content)
elif channel == NotificationChannel.EMAIL:
result = self.send_to_email(content)
elif channel == NotificationChannel.CUSTOM:
result = self.send_to_custom(content)
else:
logger.warning(f"不支持的通知渠道: {channel}")
result = False