每天早上 9 点,打开飞书,迎接我的不是工作消息——而是一只叫"弟弟"的猫,叼着全球科技热点向我问好。
起因:打工人的一点小私心
我养猫。我也上班。
每天早上坐在工位上,打开飞书,扑面而来的全是待办、会议、审批。我就想:能不能让打开飞书这件事,变得稍微没那么痛苦一点?
于是一个念头冒出来——如果家里的猫能给我发消息呢?
不是那种冷冰冰的"今日热点推送",而是一只有脾气、有性格的猫,每天早上用它自己的方式跟我说:"嘿,今天的世界是这样的。"
这就是「弟弟早报」的由来。
先看成品:弟弟长什么样
弟弟每天早上 9 点准时出现在飞书群,发一张精心排版的消息卡片:
- 猫猫问候:根据星期几切换不同的"弟弟语录",周一打气、周五摸鱼、周日允许你丧
- 当地天气:温度、天气状况、降雨概率,出门前扫一眼就够
- 科技热点:从 Hacker News、TechCrunch、VentureBeat 等 6 个源叼回 3 条精选新闻
- 一个按钮:想看更多?点一下直达 Hacker News
整个卡片紧凑、好看,在手机端和桌面端都有不错的阅读体验。
技术栈一览
在动手之前,先交个底——这个项目用到的东西非常少:
| 组件 | 选型 | 说明 |
|---|---|---|
| 语言 | Python 3.11 | 脚本足矣,无需框架 |
| 新闻源 | RSS (feedparser) | 6 个科技/AI 源,免费、无需 API Key |
| 天气 | Open-Meteo API | 免费、无需注册、无限调用 |
| 推送 | 飞书 Webhook | 群机器人,5 分钟配好 |
| 自动化 | GitHub Actions | 免费 CI/CD,定时触发 |
| 依赖 | requests + feedparser | 就两个包,没了 |
总成本:0 元。 没有服务器、没有数据库、没有付费 API。
步骤 1:在飞书群里创建 Webhook 机器人
这是整个项目的起点。我们需要在飞书群里添加一个「自定义机器人」,拿到一个 Webhook URL——后续所有消息都通过这个 URL 推送进群。
1.1 打开群设置
进入你想推送消息的飞书群,点击右上角 ··· → 设置。
1.2 找到「群机器人」
在设置面板中,找到并点击 群机器人。
1.3 添加机器人
点击 添加机器人,选择 自定义机器人。
1.4 配置机器人信息
填写机器人名称(比如"弟弟")和描述,可以上传一个猫猫头像。点击 添加。
1.5 复制 Webhook 地址
添加完成后会显示 Webhook URL。复制并妥善保存这个地址,后面要用。
注意:Webhook URL 相当于这个机器人的"钥匙",不要公开到 GitHub、博客等任何公开场所。我们后面会用 GitHub Secrets 来安全存储它。
更多关于飞书自定义机器人的说明,可以参考飞书官方文档。
步骤 2:核心代码——不到 250 行的全部
整个项目只有一个 Python 文件 daily_briefing.py,不到 250 行,做了四件事:
- 给猫猫写"台词"
- 查天气
- 抓新闻
- 拼卡片发飞书
2.1 猫猫人设:每天换一种心情
弟弟不是一个机械的播报员。它有自己的"星期观":
DAY_NAMES = {
0: "忙 Day", # 周一
1: "去死 Day", # 周二
2: "未死 Day", # 周三
3: "受死 Day", # 周四
4: "福来 Day", # 周五
5: "洒脱 Day", # 周六
6: "丧 Day", # 周日
}
每天还有对应的"弟弟语录",随机抽取:
MAINLINE_POOL = {
0: ["忙 Day:你们上班,我负责可爱和播报。",
"忙 Day:先上班,再摸猫(我)。"],
4: ["福来 Day:周末的味道我都闻到了。",
"福来 Day:今天适合偷偷开心一下。"],
# ...每天两条,随机播放
}
再加上一些随机"碎碎念",比如:
ASIDES = [
"(新闻是叼来的,但猫粮是要你们挣的。)",
"(摸猫能提升生产力,真的。)",
"(我刚刚伸了个懒腰:今日状态满分。)",
"(你们认真工作,我认真可爱。)",
]
这些文案不是 AI 生成的,是手写的模板 + random.choice() 随机组合。朴素,但有效。每天打开都有点小惊喜。
2.2 天气:Open-Meteo,免费够用
天气接口选了 Open-Meteo——免费、无需注册、无需 API Key,对个人实验来说非常友好。
CHENGDU_LAT = 30.5728
CHENGDU_LON = 104.0668
def fetch_weather_chengdu():
url = "https://api.open-meteo.com/v1/forecast"
params = {
"latitude": CHENGDU_LAT,
"longitude": CHENGDU_LON,
"daily": "weathercode,temperature_2m_max,temperature_2m_min,"
"precipitation_probability_max",
"timezone": "Asia/Shanghai",
"forecast_days": 1,
}
r = requests.get(url, params=params, timeout=20)
r.raise_for_status()
data = r.json()["daily"]
# 用映射表把 weathercode 翻译成中文,然后返回天气描述、最低温、最高温、降雨概率
返回的 weathercode 是 WMO 标准编码,代码里做了一个映射表把它翻译成中文("晴"、"阵雨"、"雷暴"等)。
想换城市? 只需要改
CHENGDU_LAT和CHENGDU_LON两个常量就行。去 Open-Meteo 官网搜你的城市即可获取坐标。
2.3 新闻:RSS 一把梭
不用任何付费新闻 API,直接用 RSS 订阅源。项目内置了 6 个科技/AI 方向的源:
TRENDING_FEEDS = [
("🔥 HN", "https://hnrss.org/frontpage"),
("💻 TechCrunch", "https://techcrunch.com/feed/"),
("🤖 VentureBeat","https://venturebeat.com/category/ai/feed/"),
("⚡ Ars Technica","https://feeds.arstechnica.com/arstechnica/technology-lab"),
("📱 The Verge", "https://www.theverge.com/rss/index.xml"),
("🔬 MIT Tech", "https://www.technologyreview.com/feed/"),
]
抓取逻辑:遍历每个源 → 取第一条有效文章 → 过滤掉招聘帖等噪音 → 去重 → 最多保留 3 条。
def fetch_international_trending(limit=3):
items = []
seen_title = set()
for section, url in TRENDING_FEEDS:
if len(items) >= limit:
break
try:
resp = requests.get(url, headers=headers, timeout=20)
d = feedparser.parse(resp.content)
# 过滤、去重、取第一条
...
except Exception:
continue # 某个源挂了?跳过,不影响其他
return items[:limit]
这里有两个设计选择值得说一下:
- 单个源失败不会导致整个脚本崩溃。
try/except包住每个源的请求,坏掉一个就跳过,其他照常。对于一个每天跑一次的轻量脚本来说,这种"够用就好"的容错策略很合适。 - RSS 抓取跑在 GitHub Actions 的海外服务器上,所以不用担心国内网络访问这些源的问题——脚本执行和推送到飞书之间的链路是通的。
2.4 飞书卡片:交互式卡片
飞书支持一种叫 交互式卡片 的富文本消息格式,比纯文本好看很多。
核心就是拼一个 JSON 结构:
def build_card(dt, weather_tuple, trend_items):
elements = [
# 猫猫问候
{"tag": "div", "text": {"tag": "lark_md", "content": opening}},
{"tag": "hr"},
# 天气
{"tag": "div", "text": {"tag": "lark_md", "content": weather_line}},
{"tag": "hr"},
# 新闻列表
...
# 按钮
{"tag": "action", "actions": [{
"tag": "button",
"text": {"tag": "plain_text", "content": "更多趋势(HN)"},
"url": "https://news.ycombinator.com/",
}]},
# 底部来源说明
{"tag": "note", "elements": [...]},
]
return {
"msg_type": "interactive",
"card": {
"config": {"wide_screen_mode": True}, # 宽屏模式,桌面端更好看
"elements": elements,
},
}
最后通过一个 POST 请求发送到 Webhook:
def send_to_feishu(payload):
r = requests.post(FEISHU_WEBHOOK, json=payload, timeout=20)
r.raise_for_status()
resp = r.json()
if resp.get("code") != 0:
raise RuntimeError(resp) # 飞书返回 HTTP 200 但业务层报错时,也要抛异常
注意这里的双重检查:raise_for_status() 捕获 HTTP 层错误,而 resp.get("code") != 0 捕获飞书业务层错误(比如卡片格式不对)。飞书 Webhook 即使在业务出错时也会返回 HTTP 200,所以第二层检查不能少。
2.5 本地调试:DRY_RUN 模式
写代码的时候不想每次都往飞书群里发消息?设置环境变量 DRY_RUN=1,脚本会把卡片 JSON 打印到终端,而不是发送:
DRY_RUN=1 python daily_briefing.py
调试卡片样式和文案时非常方便。确认没问题了,再走正式推送流程。
步骤 3:用 GitHub Actions 实现自动化
代码写完了,怎么让它每天早上 9 点自动跑?答案是 GitHub Actions——免费、不用自己的服务器。
3.1 Workflow 配置
在仓库里创建 .github/workflows/daily-briefing.yml:
name: Daily Briefing to Feishu
on:
schedule:
- cron: "*/10 * * * *" # 每 10 分钟检查一次
workflow_dispatch: {} # 支持手动触发
jobs:
run:
runs-on: ubuntu-latest
env:
TZ: Asia/Shanghai
FEISHU_WEBHOOK: ${{ secrets.FEISHU_WEBHOOK }}
为什么是每 10 分钟而不是精确的 cron 时间? 因为 GitHub Actions 的 cron 调度不保证精确触发,可能延迟几分钟到几十分钟。所以我采用了一个"高频轮询 + 时间窗口守卫"的策略。
3.2 时间窗口守卫
Workflow 每 10 分钟被触发一次,但只有在上海时间 09:00–09:30 之间才真正执行:
- name: Compute run window
id: timeguard
run: |
run_hour=$(date +%H)
run_min=$(date +%-M)
force_send=false
in_window=false
# 手动触发时跳过时间窗口
if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then
force_send=true
fi
if [ "$run_hour" = "09" ] && [ "$run_min" -le 30 ]; then
in_window=true
fi
echo "force_send=${force_send}" >> "$GITHUB_OUTPUT"
echo "in_window=${in_window}" >> "$GITHUB_OUTPUT"
手动触发(workflow_dispatch)时会设置 force_send=true,跳过时间窗口限制——这在调试时非常方便,不用干等到早上 9 点。
3.3 每日去重缓存
即使在时间窗口内被触发了多次(比如 09:00、09:10、09:20 各触发一次),也只发送一次。靠 GitHub Actions 的 cache 机制实现:
- name: Restore daily send cache
uses: actions/cache@v4
with:
path: .run-cache
key: daily-briefing-${{ env.RUN_DATE }} # 按日期缓存
第一次发送成功后写入缓存标记,后续触发发现缓存已存在就跳过。一天一条,绝不刷屏。
3.4 配置 GitHub Secrets
最后一步,把飞书的 Webhook URL 存到 GitHub 仓库的 Secrets 里:
- 进入仓库 → Settings → 左侧栏 Secrets and variables → Actions
- 点击 New repository secret
- Name 填
FEISHU_WEBHOOK,Value 粘贴之前复制的 Webhook 地址 - 保存
详细步骤可参考 GitHub 官方文档:Using secrets in GitHub Actions。
项目结构
最终的项目结构非常精简:
daily-briefing/
├── .github/workflows/
│ └── daily-briefing.yml # GitHub Actions 定时任务
├── daily_briefing.py # 核心脚本,不到 250 行
├── requirements.txt # 依赖:requests, feedparser
└── README.md
一个文件,两个依赖,零成本部署。
一些可以继续折腾的方向
这个项目刻意保持了简单,但如果你想在此基础上继续折腾:
- 换城市:改
CHENGDU_LAT/CHENGDU_LON坐标即可 - 换新闻源:
TRENDING_FEEDS列表随便加减,任何 RSS 源都行 - 换人设:改
DAY_NAMES、MAINLINE_POOL、ASIDES里的文案,换成你家猫/狗/仓鼠的风格 - 接入 LLM:用 AI 动态生成每日问候语,替代静态模板
- 多群推送:复制一份 Webhook,循环发送即可
写在最后
这个项目从想法到上线,代码量不到 250 行,依赖 2 个,花费 0 元。
它不复杂,甚至有点"土"——没有 LLM、没有向量数据库、没有微服务架构。但它每天早上准时出现在我的飞书群里,用一只猫的口吻告诉我今天的世界发生了什么。
有时候技术的意义,就是把一个让你会心一笑的小想法,变成每天真实运转的东西。
GitHub 仓库:github.com/liyun95/dai…
欢迎 Fork、Star,也欢迎把弟弟改造成你自己的宠物播报员。
如果这篇文章对你有帮助,欢迎点赞收藏。有问题可以在评论区找我,虽然我可能回得比弟弟慢。