技能跑起来了,结果需要推送到你手上:早上 6 点整,日报不用你打开任何网页,已经躺在飞书群里------标题、要点、链接,一条 text 消息。网页要你打开,推送才送到手上。
本文做两件事:给日报推送做一次选型,把凭据安全这条底线讲清楚。前几篇日报都结束在 report.html,一个网页。网页的宿命是你得记得打开它;推送的宿命是它自己跑到你面前。
先把这轮要加的东西放进全景图:

collect → curate → render → report.html,是系列第 3 篇《第一个真实任务》就立起来的骨架。这轮新加有:digest.md 和 items.json 分叉出一条 push.py → 飞书群。看着轻巧,选型、凭据、安全全在这里。
版本口径照旧:截至 2026-08-28,本机实测 Hermes v0.19.0 + deepseek-v4-flash。写稿时我核实过,上游已迭代到 v0.20 线(v0.20.0 代号 Herald,2026-08-03 发布)。本篇要讲的推送与凭据机制------webhook、gateway、hermes send------不是某个版本临时加的功能,是 Hermes 从设计上就有的骨架,不悬在版本上。
两条路:飞书 webhook 还是 Hermes gateway
推送渠道,从零搭到「能用」,差别大得离谱。差距不在代码量,在你要不要替对方平台「收消息」。
我把选型压成一句话:只发不收,一条 webhook URL 就够;要收要发、要多渠道,才轮到 gateway。

| 维度 | 轻量路线:飞书 webhook | 完整路线:Hermes gateway |
|---|---|---|
| 凭据 | 一条 webhook URL | bot token / 账号(.env + config.yaml) |
| 常驻进程 | 不需要 | 需要(gateway 常驻) |
| 收消息 | 只发不收 | 可收可发,双向对话 |
| 适用 | 团队群日报 | 个人全渠道助手 |
轻量路线的主角是飞书自定义机器人:在群里加一个「自定义机器人」,飞书给你一条 webhook 地址。往这条 URL POST 一段 JSON,消息就出现在群里。一条 URL,不需要常驻进程,不需要账号体系。
完整路线的主角是 Hermes gateway:一个常驻的消息网关进程,把 Telegram、Discord、WhatsApp、微信这些平台和 Hermes 本体接起来。你要给每个平台准备 bot token 或账号,配进 Hermes 的配置。
两条路我先各写各的------第 3 节走轻量路,第 4 节拆完整路。选型的判断标准,我留到第 8 节统一给。
轻量路线:push.py,一个纯标准库脚本
先写代码。日报推送器 push.py,放在 daily-report/ 里,只依赖标准库。它干三件事:读 digest.md 的前 500 字符当正文、读 items.json 的前几条标题当要点、把这两样拼成飞书 text 消息的 payload,POST 到 webhook。
三个函数就是全部。读数据:
python
def load_digest() -> str:
"""读 digest.md,截前 500 字符作推送正文。"""
try:
with open("digest.md", encoding="utf-8") as f:
text = f.read().strip()
except FileNotFoundError:
return "(本期未生成汇总,digest.md 不存在)"
return text[:500]
def load_headlines(head: int) -> list:
"""从 items.json 取前 head 条标题,供推送消息使用。"""
try:
with open("items.json", encoding="utf-8") as f:
data = json.load(f)
except FileNotFoundError:
return []
return [it["title"] for it in data.get("items", [])[:head]]
拼 payload、发请求:
python
def build_payload(head: int) -> dict:
"""构造飞书 text 消息 payload。"""
lines = ["【各家 AI 动态日报】", ""]
lines.append(load_digest())
titles = load_headlines(head)
if titles:
lines.append("")
lines.append("今日要点:")
lines.extend(f"{i}. {t}" for i, t in enumerate(titles, 1))
return {
"msg_type": "text",
"content": {"text": "\n".join(lines)},
}
def send(payload: dict, webhook: str) -> str:
"""POST 到飞书 webhook,返回响应体。"""
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
webhook, data=data,
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=15) as resp:
return resp.read().decode("utf-8")
payload 的格式就一句话:这是飞书自定义机器人的标准文本消息体------msg_type 是 text,content.text 是拼好的一整段正文。我在飞书开放平台文档里核过,格式对得上(写作日核实)。
webhook URL 从哪来?不写死、不打印,从环境变量读:
python
ENV_KEY = "FEISHU_WEBHOOK"
MAX_ITEMS = 8 # 推送消息里最多带几条标题,控制长度
这延续了系列第 2 篇《安装部署实战》立下的 key 管理原则------DeepSeek 的 key 走 key_env: DS-KEY 从环境变量读,不写进 config.yaml。凭据进环境变量,不进代码。
真正发消息前,脚本先检查有没有凭据。没凭据就拒绝发送,这就是凭据守卫:
python
webhook = os.environ.get(ENV_KEY, "")
if not webhook:
print(f"错误:未设置环境变量 {ENV_KEY},无法真实推送。", file=sys.stderr)
print("先 `export FEISHU_WEBHOOK=https://open.feishu.cn/open-apis/bot/v2/hook/<你的key>`,或先跑 --dry-run 预览。", file=sys.stderr)
return 1
完整路线:Hermes gateway,让 agent 住进聊天软件
轻量路只解决「推出去」。你要在群里跟 agent 对话------问它「今天有什么重点」、让它把某条展开------就得走 gateway。
Hermes gateway 是一个常驻消息网关进程。gateway --help 第一行就说清它是干什么的:
text
Manage the messaging gateway (Telegram, Discord, WhatsApp, Weixin, and more)
支持的平台远不止这几个。官方文档列了 20 多个:Telegram、Discord、Slack、WhatsApp、Signal、Matrix、钉钉、飞书、企业微信、微信、QQ 机器人......(写作日 WebSearch 核实)。命令面:
text
run/start/stop/restart/status/install/uninstall/list/setup/enroll
本机没跑。gateway status 逐字:
text
✗ Gateway is not running
To start:
hermes gateway run # Run in foreground
hermes gateway install # Install as Windows Scheduled Task (auto-start on login)
gateway list 逐字:✗ default (current) --- not running
常驻的代价,在这两句里就看出来了------要么前台跑着别关,要么装成开机自启的服务。gateway 是「常驻」这条路线的核心词。要接 Telegram/Discord/WhatsApp/微信,你得先有 bot token 或账号:Telegram 找 @BotFather 建 bot 拿 token,Discord 去开发者后台建应用,WhatsApp 走官方云 API 或桥接。这些我本机一个都没配,标「待核实」。
到这儿,你多半已经默认:推送 = 要常驻一个机器人。我第 5 节再拆一层------不一定。
中间件:hermes send,不常驻的推送通道
gateway 把消息「收」进来,也负责把消息「发」出去。但如果你只需要「发」,有一个更薄的入口:hermes send。它的帮助文件把定位写得很清楚:
text
Pipe text from any shell script to any messaging platform Hermes is already configured for. Reuses the gateway's platform credentials (~/.hermes/.env + ~/.hermes/config.yaml) --- no LLM, no agent loop, no running gateway required for bot-token platforms like Telegram/Discord/Slack/Signal.
翻译成大白话:把任意 shell 脚本的输出,管到任何 Hermes 已配置好的消息平台。复用 gateway 配好的凭据;没有 LLM、没有 agent loop;对 Telegram/Discord/Slack/Signal 这类 bot-token 平台,不需要常驻 gateway。
参数就几个:-t 指定平台或频道、-f 从文件读正文、-s 加一行标题、-q 安静模式。典型用法就是把日报文本喂给它:
text
cat digest.md | hermes send --to telegram
一个细节先记住:帮助里的 ~/.hermes 是 Unix 路径。Windows 上 Hermes home 是 %LOCALAPPDATA%\hermes,系列第 4 篇《记忆系统拆解》讲过,凭据就放在那里的 .env 和 config.yaml。
bot-token 平台,hermes send 直连对方平台的 REST 接口,不需要 gateway 常驻。gateway 常驻的价值在「收」------守着群聊等消息进来;只在「发」的场景,脚本和定时任务配好凭据后,直接 hermes send 就推走了。这就是后面 H7 调度要用的推送通道。
凭据安全:webhook URL 和 bot token,都不进代码
这一节最容易被跳过,但它是底线。我直接给判断:凡是能替你把消息发出去的字符串------webhook URL、bot token------都按密码对待。
三个「不」:
- 不进代码------不写死在 push.py 里;
- 不进提交------.env 进 .gitignore;
- 不进文章------本篇不会出现任何真实凭据。
凭据从哪来、进到哪去,画成一张流向图:

流向是单向的:凭据只存在于「运行环境」,进程从环境变量或本地配置文件读它,用完即弃,不打印、不落日志。push.py 靠 os.environ.get(ENV_KEY, "") 读,没读到就拒绝发送------上一节那个守卫的完整报错,逐字是:
text
错误:未设置环境变量 FEISHU_WEBHOOK,无法真实推送。
先 `export FEISHU_WEBHOOK=https://open.feishu.cn/open-apis/bot/v2/hook/<你的key>`,或先跑 --dry-run 预览。
你看,没凭据,脚本根本不给你发。这是「凭据不进代码」最直接的好处:脚本本身是安全的,泄不泄密取决于运行环境,不取决于代码仓库。
再交代一个诚实的细节:飞书官方建议给自定义机器人开启签名校验------加一个 secret,发消息时按 HMAC-SHA256 生成 sign、带 timestamp(写作日核实)。我演示用的是最朴素的裸 webhook,团队群内部够用;生产环境建议按官方文档把签名补上。这层实现我没做,算半个「待核实」。
载体项目落地:推送链路成型
镜头拉回载体项目。整条链路现在是:collect.py 抓源 → curate.py 调 Hermes 汇总 → render.py 出网页,末尾多一档 push.py 推送。就是引言那张全景图。
真跑一遍 dry-run,验证推送内容长什么样------PYTHONIOENCODING=utf-8 python push.py --dry-run --head 3,--head 3 指标题只带前 3 条。输出逐字取自本机:
text
=== DRY RUN:将推送给飞书的消息 payload(不发) ===
【各家 AI 动态日报】
今日无新动态:24 条与昨日已报道重复,已剔除。日报沿用昨日内容。
今日要点:
1. Supporting Thailand's next generation of AI startups
2. Better answers, broader thinking: What students gain from ChatGPT and critical-thinking training
3. Expanding OpenAI's presence in Brazil
payload 共 251 字符
这条输出里藏着两个信号。一个是 digest------「今日无新动态:24 条与昨日已报道重复」,这是系列第 4 篇《记忆系统拆解》那个跨天去重 count=0 守卫的产物:昨天报过的今天重复,curate.py 不白调 Hermes,沿用昨日内容。另一个是标题------三条全是 OpenAI,items.json 按抓取顺序排,OpenAI 的 RSS 在前。推送和网页看到的是同一份数据,只是换了送达方式。
真实推送我没发:缺 FEISHU_WEBHOOK 凭据,标「待核实」。这正是脚本设计的本意------凭据不进代码,脚本没有凭据就发不出去,只能 dry-run 预览。Telegram 同理,缺 bot token,标「待核实」。
什么时候用 webhook,什么时候上 gateway
结论先放:团队群日报,webhook 够用;个人全渠道助手,才轮到 gateway。差别不在「渠道数量」这一个维度上。
我压成三个判断标准:推送频率、渠道数量、要不要收消息。
- 推送频率------一天一次,webhook 一条 URL 就够;每分钟都要发,才需要正经管道。
- 渠道数量------只推一个群,webhook;要同时到 Telegram、Discord、微信,gateway 或 hermes send 一次配好。
- 要不要收消息------这条最锋利。日报只发不收,webhook 和 hermes send 都够;你要在群里跟 agent 对话、让它回你「今天有什么重点」,那就必须 gateway------双向通信需要一个常驻的接收端。
反过来读更扎心:很多人以为「推送 = 要个常驻机器人」,其实只发不收的话,一条 URL 就够了。gateway 的价值在「收」,不在「发」。
结论:推送把日报送到手上,还差「到点自动跑」
回到开头那个画面。日报现在能推了------push.py dry-run 跑通,凭据配好就能发。但还差最后一块拼图:6 点整那一下,谁来触发?
现在全靠人肉。每天 6 点手动跑一遍 push.py,那不叫自动化,叫「每天记得按一下」。这也正好接上系列第 6 篇《技能系统实战》的缺口------技能会跑,但没到点自动跑。
下一篇揭秘:让日报每天 6:00 无人值守地跑完 collect → curate → render → push 整条链。cron 就是那个「到点自动跑」的答案。关注别走丢,下一篇拆调度。