我给飞书群养了一只会播报新闻的猫

0 阅读9分钟

每天早上 9 点,打开飞书,迎接我的不是工作消息——而是一只叫"弟弟"的猫,叼着全球科技热点向我问好。

起因:打工人的一点小私心

我养猫。我也上班。

每天早上坐在工位上,打开飞书,扑面而来的全是待办、会议、审批。我就想:能不能让打开飞书这件事,变得稍微没那么痛苦一点?

于是一个念头冒出来——如果家里的猫能给我发消息呢?

不是那种冷冰冰的"今日热点推送",而是一只有脾气、有性格的猫,每天早上用它自己的方式跟我说:"嘿,今天的世界是这样的。"

这就是「弟弟早报」的由来。

先看成品:弟弟长什么样

弟弟每天早上 9 点准时出现在飞书群,发一张精心排版的消息卡片:

  • 猫猫问候:根据星期几切换不同的"弟弟语录",周一打气、周五摸鱼、周日允许你丧
  • 当地天气:温度、天气状况、降雨概率,出门前扫一眼就够
  • 科技热点:从 Hacker News、TechCrunch、VentureBeat 等 6 个源叼回 3 条精选新闻
  • 一个按钮:想看更多?点一下直达 Hacker News

整个卡片紧凑、好看,在手机端和桌面端都有不错的阅读体验。

feishu-card.jpeg

技术栈一览

在动手之前,先交个底——这个项目用到的东西非常少:

组件选型说明
语言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 打开群设置

进入你想推送消息的飞书群,点击右上角 ···设置

feishu-step1.jpeg

1.2 找到「群机器人」

在设置面板中,找到并点击 群机器人

feishu-step2.png

1.3 添加机器人

点击 添加机器人,选择 自定义机器人

feishu-step3.png

feishu-step4.png

1.4 配置机器人信息

填写机器人名称(比如"弟弟")和描述,可以上传一个猫猫头像。点击 添加

feishu-step5.png

1.5 复制 Webhook 地址

添加完成后会显示 Webhook URL。复制并妥善保存这个地址,后面要用。

feishu-step6.png

注意:Webhook URL 相当于这个机器人的"钥匙",不要公开到 GitHub、博客等任何公开场所。我们后面会用 GitHub Secrets 来安全存储它。

更多关于飞书自定义机器人的说明,可以参考飞书官方文档

步骤 2:核心代码——不到 250 行的全部

整个项目只有一个 Python 文件 daily_briefing.py,不到 250 行,做了四件事:

  1. 给猫猫写"台词"
  2. 查天气
  3. 抓新闻
  4. 拼卡片发飞书

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_LATCHENGDU_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]

这里有两个设计选择值得说一下:

  1. 单个源失败不会导致整个脚本崩溃try/except 包住每个源的请求,坏掉一个就跳过,其他照常。对于一个每天跑一次的轻量脚本来说,这种"够用就好"的容错策略很合适。
  2. 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 里:

  1. 进入仓库 → Settings → 左侧栏 Secrets and variablesActions
  2. 点击 New repository secret
  3. Name 填 FEISHU_WEBHOOK,Value 粘贴之前复制的 Webhook 地址
  4. 保存

github-secrets.jpeg

详细步骤可参考 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_NAMESMAINLINE_POOLASIDES 里的文案,换成你家猫/狗/仓鼠的风格
  • 接入 LLM:用 AI 动态生成每日问候语,替代静态模板
  • 多群推送:复制一份 Webhook,循环发送即可

写在最后

这个项目从想法到上线,代码量不到 250 行,依赖 2 个,花费 0 元。

它不复杂,甚至有点"土"——没有 LLM、没有向量数据库、没有微服务架构。但它每天早上准时出现在我的飞书群里,用一只猫的口吻告诉我今天的世界发生了什么。

有时候技术的意义,就是把一个让你会心一笑的小想法,变成每天真实运转的东西。

GitHub 仓库github.com/liyun95/dai…

欢迎 Fork、Star,也欢迎把弟弟改造成你自己的宠物播报员。


如果这篇文章对你有帮助,欢迎点赞收藏。有问题可以在评论区找我,虽然我可能回得比弟弟慢。