同一份结论,发到企业微信、Telegram 和邮件里,长相必须不同。企业微信对字节数敏感,Telegram 会被切成多条,邮件需要完整的 HTML,Discord 对频率限制很严格。通知层的价值,就是把这些「渠道脾气」吸收掉,让上层只管产出内容。
类比:同一份通知,三种送达方式
物业要通知全体业主停水。贴在电梯里要短(企业微信),发短信要分段(Telegram),寄信要正式(邮件)。内容核心一样,但排版、长度、称呼都不同。DSA 的通知服务就是那个「物业文案组」:拿到结论后,按渠道重新排版、分片、降噪,再逐户投递。
14 类渠道:能力不同,策略不同
| 渠道 | 接入方式 | 格式 / 限制 | 适合 |
|---|---|---|---|
| 企业微信 | 群机器人 Webhook | Markdown 约 4000 字节、文本约 2000;图片 ≤ 2MB | 国内团队、日常简报 |
| 飞书 | Webhook / App Bot | 长消息按字节分片(默认 20000);支持富文本与云文档 | 正式报告、归档 |
| 钉钉 | 群机器人 / Stream | Markdown;支持 Stream 长连接 | 国内企业 IM |
| Telegram | Bot Token + Chat ID | 4096 字符分片;Markdown/HTML | 个人、海外 |
| Discord | Webhook | 约 2000 字符;429 按 retry_after 退避 | 社区、小团队 |
| Slack | Bot Token | Mrkdwn 方言,表格需转换 | 海外团队 |
| 邮件 | SMTP | HTML 正文,适合长文 | 归档、正式记录 |
| Pushover | API Token | 移动推送,长度有限 | 个人即时提醒 |
| ntfy | HTTP Topic | 纯文本/简单 Markdown | 自建、轻量 |
| Gotify | 自建服务 | 纯文本 | 私有部署 |
| PushPlus | Token | 微信公众号推送 | 国内个人 |
| ServerChan3 | SendKey | 微信服务号 | 国内个人 |
| 自定义 Webhook | 任意 HTTP 端点 | 由你定义的 JSON | 接入自有系统 |
| AstrBot | 适配器 | 取决于适配器 | 机器人生态集成 |
项目里常说「14 类渠道」,指的就是这些发送器。你不需要全配,配一个能稳定收到消息的就够了。配得越多,失败面越大,噪音也越大。
最少可用配置
企业微信:WECHAT_WEBHOOK_URL;飞书:FEISHU_WEBHOOK_URL;Telegram:TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID;Discord:DISCORD_WEBHOOK_URL;邮件:EMAIL_SENDER + EMAIL_PASSWORD + 收件人。至少配一个,否则分析能跑完但不会主动找你。
路由:什么消息,发给谁
通知不是「一股脑全发」。系统按消息类型路由:
| 消息类型 | 配置变量 | 典型内容 |
|---|---|---|
| 报告 | NOTIFICATION_REPORT_CHANNELS | 每日仪表盘、个股报告、大盘复盘 |
| 告警 | NOTIFICATION_ALERT_CHANNELS | 价格触发、指标触发、决策信号触发 |
| 系统错误 | NOTIFICATION_SYSTEM_ERROR_CHANNELS | 任务失败、配置错误、渠道异常 |
变量为空表示「全量渠道」,未知渠道名也按全量处理,避免因为拼写错误而静默丢消息。这种 fail-open 的选择有代价:可能多发,但比「以为配好了其实没发」好。
降噪:别让系统变成骚扰
通知层有一道「降噪门」,在真正发送前检查:
- 严重级:info < warning < error < critical,低于
NOTIFICATION_MIN_SEVERITY的直接丢弃。 - 静默时段:
HH:MM-HH:MM内不发(时区由NOTIFICATION_TIMEZONE指定)。 - 去重 TTL:相同内容在窗口内只发一次。
- 冷却:同一规则触发后,短时间内不重复提醒。
这些状态保存在进程内存里,并且是 fail-open 的:降噪逻辑本身出错时,倾向于放行而不是拦截。代价是多实例部署时状态不共享——每个实例都可能各发一次。要严格去重,就只保留一个发送入口。
格式适配:一份内容,多种排版
报告可以在两个层面渲染:默认在通知层用字符串拼装;开启 REPORT_RENDERER_ENABLED 后,使用 Jinja2 模板按平台渲染。常见输出形态有:
完整仪表盘
摘要、情报、结论、数据透视、作战计划、历史对比。适合飞书、邮件、Web。
微信精简版
更短的字段截断、表格转键值行,避免在手机窄屏上错位。
一句话简报
一只股票一行,适合自选股很多的批量概览。
分享海报
1080×720 的图片,适合群聊与朋友圈,信息密度低于文本。
格式化层还处理一些很琐碎但很致命的问题:Markdown 表格在 Telegram 不生效、飞书有字节上限、企业微信的 Markdown 语法子集有限、Slack 的表格语法不同。这些不是「文案问题」,而是渠道协议差异。
共分析 3 只股票 · 🟢买入 0 · 🟡观望 2 · 🔴卖出 1
⚪ 中钨高新 (000657)
观望 · 评分 65 · 看多
🟡 新莱应材 (300260)
卖出 · 评分 35 · 看空
完整报告请查看 Web 工作台
中钨高新 000657
观望|评分 65|看多
新莱应材 300260
卖出|评分 35|看空
────────
2026-02-08 18:00
今日共分析 3 只自选股,2 只观望、1 只建议卖出。
| 股票 | 建议 | 评分 |
|---|---|---|
| 中钨高新 | 观望 | 65 |
| 新莱应材 | 卖出 | 35 |
本邮件由 DSA 自动发送 · 不构成投资建议
排版示意,非真实推送内容
Markdown 转图片:当文字排版不可靠时
有些渠道对 Markdown 支持很差,或者长表格在手机上根本看不清。这时可以把报告渲染成图片:
- 由
MARKDOWN_TO_IMAGE_CHANNELS指定哪些渠道需要转图;只转一次,再分发给多个渠道。 - 引擎可选
wkhtmltoimage(默认)、Markdown 文件渲染或 Playwright;缺依赖时自动回退文本。 - 超过
MARKDOWN_TO_IMAGE_MAX_CHARS(默认 15000 字符)会跳过转图,避免生成一张看不清的超长图。 - 分享海报走另一条路径:1080×720 的固定版式,历史报告可单独生成 PNG。
图片是「保底的可读性」,不是「更高级的格式」。它不可搜索、不可复制、对屏幕阅读器不友好,所以只在必要时使用。
Bot:把分析装进 IM
Bot 不是独立进程,而是跟着 --serve* 启动的后台能力。当前实际接入以Stream 长连接为主:飞书用 lark-oapi WebSocket,钉钉用 Stream 模式,自动重连、线程池处理。好处是不需要公网 IP,家里 NAS 也能用。
不要把「Webhook 入口」当成主路径
仓库里仍保留一些历史 Webhook 处理函数,但 FastAPI 并没有挂载统一的 Webhook 路由。想真正收到消息,请启用 FEISHU_STREAM_ENABLED 或 DINGTALK_STREAM_ENABLED,并让进程处于 --serve / --serve-only 模式。
10 个命令
| 命令 | 别名 | 作用 |
|---|---|---|
/help | h、帮助、? | 命令帮助 |
/status | s、状态、info | 系统状态 |
/analyze | a、分析、查 | 单股/指数分析 |
/market | m、大盘、复盘 | 大盘复盘 |
/batch | b、批量、全部 | 批量自选股(仅管理员) |
/ask | 问股 | Agent 策略分析 |
/chat | c、问 | Agent 自由对话 |
/research | 深研、deepsearch | 深度研究 |
/strategies | skills、策略 | 列出策略 |
/history | 历史、会话 | Agent 会话历史 |
默认命令前缀是 /,中文别名可以不带前缀。命令分发器有滑动窗口限流(默认 10 次/60 秒)与管理员校验;/ask、/chat、/research 在 AGENT_MODE=false 时会明确拒绝。非命令消息可以走 LLM 意图路由,但只在私聊/@、且开启 AGENT_NL_ROUTING 与 Agent 模式时生效。
配置清单
# 至少一个通知渠道
WECHAT_WEBHOOK_URL=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...
# FEISHU_WEBHOOK_URL=https://open.feishu.cn/open-apis/bot/v2/hook/...
# TELEGRAM_BOT_TOKEN=...
# TELEGRAM_CHAT_ID=...
# 路由(可选)
NOTIFICATION_REPORT_CHANNELS=wechat,feishu
NOTIFICATION_MIN_SEVERITY=warning
NOTIFICATION_TIMEZONE=Asia/Shanghai
# 图片(可选)
MARKDOWN_TO_IMAGE_CHANNELS=wechat
MD2IMG_ENGINE=wkhtmltoimage
# Bot(可选)
BOT_ENABLED=true
FEISHU_STREAM_ENABLED=true
DINGTALK_STREAM_ENABLED=false
配好后,用 Web 设置页的「测试渠道」或 python main.py --check-notify 验证,不要等第二天 18:00 才发现 Webhook 少了一个字符。
五个常见坑
| 现象 | 原因 | 处理 |
|---|---|---|
| 报告很长但被截断 | 渠道字节/字符上限 | 开启分片或改用图片;检查 FEISHU_MAX_BYTES |
| 表格变乱码/丢失 | 渠道不支持 Markdown 表格 | 用微信/Telegram 专用格式化,或转图片 |
| 同一条消息发了两遍 | 多实例各自触发,降噪状态不共享 | 只保留一个调度入口与发送进程 |
| Bot 没反应 | 没开 Stream、进程不在 serve 模式 | 开 FEISHU_STREAM_ENABLED,用 --serve-only 常驻 |
| 图片发送失败 | 缺 wkhtmltopdf/字体,或超过 2MB | 装依赖或回退文本;减少图片尺寸 |
你只配置了 NOTIFICATION_REPORT_CHANNELS=feishu,没有配置告警和系统错误的路由。一条「任务失败」消息会发到哪里?
NOTIFICATION_ALERT_CHANNELS 与 NOTIFICATION_SYSTEM_ERROR_CHANNELS。这是「宁可多发,不可漏发」的取舍。