用GitHub Actions将RSS自动化同步到Notion的完整方案
先交代一个场景你有没有经历过这样的循环——浏览器收藏夹里存了几百个“以后再看”的链接真正想看时却找不到手机上装了四五个 RSS 阅读器换来换去没有一个是真正顺手的甚至因为某个服务关停精心维护的订阅源一夜归零。我自己就因为这个折腾了很久最后把目光落在 Notion 上理由很简单它本质是一张不受约束的数据库表RSS 抓下来的内容可以像积木一样随意重组。但 Notion 缺一个能力——定时去抓取外部内容。于是就有了这套方案用 GitHub Actions 作为定时触发器用 Python 脚本做 RSS 解析和 Notion API 写入把“订阅—抓取—归档—通知”整条链路自动化。整个过程完全不需要自己买服务器仓库的免费额度足够个人使用而且所有配置对新手也比较友好。这篇文章适合谁适合那些不想被推荐算法驯化、希望自己掌握信息源的人也适合想搞明白 GitHub Actions 到底怎么落地的人。你可以没有服务器可以没有域名甚至不太会写代码只要能看懂 YAML 长什么样跟着操作基本能跑通。核心内容是方案选型、数据库设计、Workflow 写法、以及我踩过的几个坑最后会专门聊一下“24 小时过期刷新”这类问题的处理思路。1. 需求分析与方案选型为什么是这三样东西的组合1.1 传统 RSS 阅读器和 Notion 的本质差别RSS 阅读器已经存在二十年了市面上从 Google Reader 时代到现在各种客户端数量并不少。但我个人的感受是阅读器解决了“聚合阅读”的问题却没有解决“沉淀和整理”的问题。你在一款阅读器里看到的每一篇文章本质上都困在它的“容器”里——想批量打标签想按自己的字段做筛选想和其他笔记、任务、资料库建立关联基本做不到。大多数阅读器的数据模型是封闭的你能做的只有“标星”“归档”“全部已读”仅此而已。Notion 则相反它的核心是数据库。你可以把一条 RSS 文章抽象成一行记录字段自己定义视图自己搭——想看未读列表就做看板视图想按主题复盘就做表格视图想跟踪阅读进度就做状态字段。这种自由度是传统阅读器给不了的。所以方案选型的第一个关键决定是不把 Notion 当阅读器而是把它当数据仓库。抓取和阅读分离抓取用自动化完成阅读和整理在 Notion 里自由发挥。1.2 为什么选择 GitHub Actions 而非云服务器这里其实有过几个备选方案自己租一台轻量服务器跑 cron、用群晖的定时任务、或者用一些在线定时服务。最终选 GitHub Actions完全是基于个人使用场景的考量。首先是成本。个人 RSS 抓取任务的运行时间通常在几秒到几分钟之间GitHub Actions 对私有仓库提供每月 2000 分钟的免费额度对公开仓库完全免费。这个量级对于“每隔几小时跑一次脚本”来说简直绰绰有余一年下来不会产生一分钱费用。租服务器虽然也不贵但你需要维护环境、处理安全更新、关注宕机这些运维成本对于一个以“信息管理”为核心的项目来说太沉重了。其次是触发方式的灵活性。GitHub Actions 支持 cron 定时触发、支持手动触发workflow_dispatch、支持 Webhook 触发、也支持仓库变更触发。配合 concurrency 控制你几乎可以搭建出任意复杂度的自动化流程。而且调度本身就由 GitHub 托管不需要你操心服务器时区、重启后服务是否自动拉起等问题。第三是收敛配置和代码。你把 workflow YAML 文件、Python 脚本、依赖清单、密钥配置全部放进同一个仓库后整个项目就是“一处配置全局复现”。以后换电脑、迁移、给别人参考都特别方便因为所有状态都记录在 Git 历史里。1.3 为什么 RSS 源管理值得被自动化RSS 源的一个特点就是“不稳定”。有些站点今天还正常输出明天就改了路径有些站点的 feed 里文章格式发生了变化导致解析失败还有更新频率不一的站点——有的每天发布有的一周才更新一次。如果全部靠手动刷新阅读器一天点几十次刷新按钮还是很烦的。在这种场景下自动化的价值不只是“省时间”更是“不遗漏”。设置一个合理的抓取频率比如每两小时让任务自己检查更新就能做到“打开 Notion 即最新”。同时RSS 源也是稀缺资源。网络上很多站点不提供官方 feed需要通过 RSSHub 等工具生成这些生成类的源往往有缓存时间或 token 有效期手动维护容易漏掉“悄悄失效”的情况。自动化部署后可以在脚本里增强失败检测——比如连续抓取失败时推送通知提醒你去检查源是否依然可用。这一点是手工刷新完全做不到的。2. 方案整体设计数据流、数据库结构和触发链路2.1 数据流梳理从源站到 Notion 要经过哪几步在写任何代码之前先把一条信息从源头走到 Notion 数据库的完整路径画出来直接在脑子里画就行GitHub Actions 按计划触发 workflow。workflow 拉取仓库代码设置 Python 环境安装依赖。运行抓取脚本逐个请求订阅列表里的 RSS 地址。解析 feed提取标题、链接、发布时间、摘要、作者等信息。与 Notion 数据库已有的记录做比对识别新文章。对新文章调用 Notion API在数据库中添加记录。记录本次运行的状态可选通知渠道推送“新增 X 篇文章”的摘要。这个流程里有几个关键决策点抓取频率、去重策略、失败处理。频率设置我建议抓平衡——太频繁容易被对方站点封太少会错过实时更新的内容。我个人常用的频率是每 2 小时一次如果有特殊时效性需求可以把那条源单独放到一个高频 workflow 里。去重策略是整个流程中最容易踩坑的地方。常见做法是以文章的链接 URL 做唯一标识因为链接不会变即使标题或摘要被修改链接仍然稳定。另一个备选是使用 feed 里自带的 GUID 字段但这个字段在不同站点实现差异较大有的站点不提供有的站点会随内容更新而变化可靠性不如链接。2.2 Notion 数据库字段设计在 Notion 里新建一个数据库之前建议先想清楚你到底需要哪些字段。太少了后期想筛选没维度太多了录入时看着累。我整理的这套字段方案是经过几个版本迭代后沉淀下来的字段名称类型说明标题标题类型文章的标题作为记录的主标识链接URL 类型原文链接用于去重和点击阅读摘要富文本类型feed 中的简介或截取的前几段发布时间日期类型文章的原始发布时间用于排序抓取时间日期类型记录被脚本写入的时间用于观察延迟来源富文本类型站点名称或 feed 标识标签多选类型可以按主题归类也可以留给后续手动整理状态单选类型未读 / 已读 / 稍后读 / 已归档作者富文本类型feed 中有则填没有可留空站点图标URL 类型用于在看板视图中展示来源站点的 favicon为什么强调“提前想好”因为 Notion API 在创建记录时如果传入的字段在数据库里不存在会被忽略或报错。后期再改字段类型历史数据也不会自动迁移。所以建议启动前先花十分钟设计字段真的不要着急写代码。关于“站点图标”这个字段是我后加的但也值得讲一下。纯文本的数据库看起来略显干瘪尤其在做看板视图时如果每条记录前面有 favicon视觉上会清楚很多。favicon 可以从前端自动获取也可以用一个现成的第三方图标服务直接在 URL 里拼站点域名。更优雅的方案是在脚本里解析每个 RSS 源中image标签或icon标签把站点图标保存为图片 URL 写入字段。这样即使以后换了 Notion 的展示方式图标仍然在数据里。2.3 触发链路设计Workflow 的三种运行的场景GitHub Actions 的触发方式我实际用到的有三类。第一类是定时触发这是主链路YAML 里写好 cron 表达式比如0 */2 * * *表示每两小时整点运行。第二类是手动触发在 GitHub 仓库的 Actions 页面可以点“Run workflow”这在我临时想立即抓一次时特别有用比如刚添加了一个订阅源不想等下一个定时周期。第三类是仓库更新触发也就是当你 push 了新的订阅源列表或修改脚本后自动重新运行一次验证配置是否正确并能及时把新源的内容抓回来。把这三类触发方式组合起来用基本上就把“日常维护”覆盖全了。你不需要额外搭建任何调度服务一个 workflow 文件就能承载这套逻辑。2.4 选择 Python 技术栈的理由脚本语言本身我选了 Python。原因很朴素RSS 解析库feedparser几乎是标准答案它对各种畸形 feed 的容错能力远好于自己写正则或使用 Node.js 的类似库。而且 Notion API 的请求逻辑也不复杂用requests就够不需要引入重型框架。环境方面GitHub Actions 官方维护的actions/setup-python提供了一个非常稳定的 Python 运行环境依赖安装也很快。依赖版本方面我的建议是不要把版本锁死除非你遇到了具体的兼容性问题。让 pip 根据 requirements 里的大版本约束自行选择比一手动更新小版本要省心。不过如果你使用的第三方库发布了不兼容的升级版本锁版本反而是更稳妥的做法——这个取舍需要自行判断常用feedparser6.0,7.0这种区间写法作为折中。3. 核心实现Workflow 写法与 Notion API 对接3.1 GitHub Secrets密钥管理的正确姿势在写任何代码之前先把密钥准备到位。这里涉及一个安全问题Notion API Key也叫 Internal Integration Token和数据库 ID 这些信息绝不能硬编码在代码里也不能直接写进仓库。只要被推到 GitHub 上即使用完立刻删除 history也可能被爬虫抓到。正确的做法是使用仓库的 Secrets 功能。进入仓库的 Settings - Secrets and variables - Actions然后添加以下变量NOTION_TOKEN在 Notion 的 Integrations 页面创建的 Internal Integration Secret形如secret_xxxxx。NOTION_DATABASE_IDNotion 数据库页面的 URL 中提取的 32 位字符串格式是https://www.notion.so/username/[database_id]?v...。DINGTALK_WEBHOOK可选钉钉机器人的 Webhook 地址用于推送新增通知。DINGTALK_SECRET可选钉钉机器人的加签密钥。使用加签方式在 Webhook 地址中需要拼接timestampxxxsignxxx。WEWE_REFRESH_TOKEN可选如果你使用 wewe-rss 这类工具作为订阅源可能需要刷新凭证。Secrets 在 workflow 中的使用方式是通过${{ secrets.XXX }}语法然后把它写入环境变量再在 Python 脚本中用os.environ读取。这正是为了避免把敏感信息暴露到日志中。这里有个容易犯的错误在 workflow 里直接用echo打印 secrets 内容这会被记录在日志里虽然 GitHub 会自动脱敏但保险起见还是别这么干。3.2 Workflow YAML 的完整结构与逐行解读下面是一个我实测过的精简版 workflow放在仓库的.github/workflows/rss-notion.yml位置name: RSS to Notion on: schedule: - cron: 0 */2 * * * workflow_dispatch: push: branches: - main paths: - config.yaml - src/** concurrency: group: rss-notion cancel-in-progress: false jobs: fetch: runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Checkout code uses: actions/checkoutv4 - name: Set up Python uses: actions/setup-pythonv5 with: python-version: 3.12 cache: pip - name: Install dependencies run: pip install -r requirements.txt - name: Run RSS fetcher env: NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} NOTION_DATABASE_ID: ${{ secrets.NOTION_DATABASE_ID }} DINGTALK_WEBHOOK: ${{ secrets.DINGTALK_WEBHOOK }} DINGTALK_SECRET: ${{ secrets.DINGTALK_SECRET }} run: python src/fetch_rss.py这段配置里有几个值得注意的设计点。第一是concurrency配置。这个非常关键如果定时触发和手动触发时间重叠可能出现两个 workflow 并发运行一起去查数据库、一起写入相同的数据导致重复记录。设置concurrency后后触发的那个会等到先触发的跑完才开始或者直接排队。这里用cancel-in-progress: false是因为我不想取消正在进行的抓取而是希望新请求等待。第二是timeout-minutes: 10。RSS 抓取偶尔会遇到某个源响应很慢的情况如果源站迟迟不返回整个 workflow 会卡住直到 GitHub 超时。设置团队级别的超时配合 Python 脚本里 requests 的 timeout 参数双保险。第三是actions/setup-python的cache: pip选项。这个会缓存 pip 依赖第二次运行时就无需重新下载所有包速度提升明显。第一次运行还是需要完整安装之后就会快很多。第四是push的paths过滤器。配置仓库中config.yaml或src/目录有任何变更时才触发 workflow。这个设计避免了我每次修改 README 或无关文件时都白白跑一次抓取。3.3 Python 抓取脚本解析 RSS 与写 Notion先看依赖文件requirements.txtfeedparser6.0,7.0 requests2.31,3.0 PyYAML6.0,7.0 python-dateutil2.8,3.0核心抓取逻辑放在src/fetch_rss.py中我拆几个关键片段来说。首先读取订阅源列表。我用一个config.yaml来管理 RSS 源不用在代码里修改sources: - name: 少数派 url: https://sspai.com/feed - name: 阮一峰的网络日志 url: https://www.ruanyifeng.com/blog/atom.xml - name: Hacker News Front Page url: https://hnrss.org/frontpage读取时用 PyYAML 加载然后循环处理import yaml, os, requests, feedparser, time from datetime import datetime, timezone def load_sources(pathconfig.yaml): with open(path, r, encodingutf-8) as f: data yaml.safe_load(f) return data[sources]然后逐条获取 feed。这里有第一个容易踩坑的点很多站点的 RSS 并没有返回标准Content-Type或者对默认的 User-Agent 做了拦截。所以我在请求头里伪装成常见的浏览器 UA并且明确设置请求超时时间HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36 } def fetch_feed(url, retries2): for attempt in range(retries): try: resp requests.get(url, headersHEADERS, timeout15) resp.raise_for_status() return feedparser.parse(resp.content) except Exception as e: print(fFetch failed for {url}, attempt {attempt1}: {e}) time.sleep(3) return None抓取完 feed 后接下来要做的是“识别出哪些是新文章”。办法是调用 Notion API 查询数据库里已经存在的链接把已有链接放入一个集合中然后遍历 feed 条目时判断其链接是否已经存在于集合里。这里要特别说明查询效率问题。如果数据库已经积累了几千条记录一次性拉取全部记录的开销会越来越大。我一开始这样做跑到后面每次查询都要好几秒。后来改成只查询最近 30 天内的记录按“发布时间”过滤对 RSS 场景足够了因为正常情况下 30 天前的文章如果当时没抓到现在也不需要再补了。这个优化帮我在数据量变大后仍然保持快速运行。调用 Notion API 的过滤查询示例def fetch_existing_links(database_id, headers, days30): from datetime import timedelta start datetime.now(timezone.utc) - timedelta(daysdays) filter_payload { filter: { property: 发布时间, date: {on_or_after: start.isoformat()} }, page_size: 100 } links set() url fhttps://api.notion.com/v1/databases/{database_id}/query while True: resp requests.post(url, headersheaders, jsonfilter_payload) resp.raise_for_status() data resp.json() for result in data[results]: prop result[properties] if 链接 in prop and prop[链接].get(url): links.add(prop[链接][url].strip()) if data.get(has_more): filter_payload[start_cursor] data[next_cursor] else: break return links写入新记录同样不是单纯 POST 一下就行。Notion API 要求你按数据库的字段类型构造 payload格式稍微繁琐但结构是固定的。下面这段代码展示了怎么把一条解析后的 feed 条目写入 Notion 记录def create_page(database_id, headers, item): payload { parent: {database_id: database_id}, properties: { 标题: {title: [{text: {content: item[title]}}]}, 链接: {url: item[link]}, 摘要: {rich_text: [{text: {content: truncate(item.get(summary, ), 300)}}]}, 发布时间: {date: {start: item.get(published, )}}, 抓取时间: {date: {start: datetime.now(timezone.utc).isoformat()}}, 来源: {rich_text: [{text: {content: item.get(source, )}}]}, 作者: {rich_text: [{text: {content: item.get(author, )}}]}, 状态: {select: {name: 未读}}, 站点图标: {url: item.get(icon, )}, }, } resp requests.post( fhttps://api.notion.com/v1/pages, headersheaders, jsonpayload, ) resp.raise_for_status()写入时需要注意Notion API 有速率限制在工作区级别大约是每秒 3 个请求。如果一次跑步要写几十条记录不加控制就可能被 429。稳妥的做法是在每写一两条之间加一个time.sleep(0.5)简单有效。这是我在跑到上百条记录时才遇到的问题前期数据量比较小通常不会触发限制。3.4 新增文章的识别与去重逻辑细节去重是整个流程中最关键也最容易出 bug 的部分。如果处理不好跑几次就会发现数据库里塞满重复记录。除了上文提到的用“近 30 天已有链接集合”外还要注意链接格式的规范化。例如有的源站会为同一篇文章输出两个不同的链接https://example.com/post/123和https://example.com/post/123?utm_sourcerss。如果不做处理就会被认为是两条记录。我的做法是统一去掉?之后的跟踪参数只保留 URL 主体from urllib.parse import urlparse, urlunparse def normalize_url(url): parsed urlparse(url) return urlunparse((parsed.scheme, parsed.netloc, parsed.path, , , ))另外feedparser 对某些站点返回的时间格式可能解析失败。此时item.get(published)为空字符串Notion API 写入日期字段时会报格式错误。所以我在解析时间时加了一层兜底def parse_time(entry): if hasattr(entry, published_parsed) and entry.published_parsed: return datetime(*entry.published_parsed[:6], tzinfotimezone.utc).isoformat() if hasattr(entry, updated_parsed) and entry.updated_parsed: return datetime(*entry.updated_parsed[:6], tzinfotimezone.utc).isoformat() return None如果解析结果为空宁可把发布时间的字段留空也不要传非法值导致整条记录写入失败。3.5 钉钉 Webhook 推送把通知接到聊天窗口热词里有人提到“订阅 rss 推送到钉钉 webhook”这个功能可以在同一个 workflow 中实现。在抓取完成后如果本次新增文章数大于 0就向钉钉机器人发送一条 Markdown 消息标题类似“RSS 更新新增 5 篇文章”正文列出文章标题和链接。钉钉机器人的加签方式稍微有点绕但逻辑其实很简单。你需要把当前时间戳和密钥拼成字符串做 HMAC-SHA256 签名再进行 Base64 编码最终拼到 Webhook URL 的 query 参数上。具体代码import time, hmac, hashlib, base64, urllib.parse def dingtalk_notify(webhook, secret, title, entries): timestamp str(round(time.time() * 1000)) string_to_sign f{timestamp}\n{secret} hmac_code hmac.new(secret.encode(utf-8), string_to_sign.encode(utf-8), digestmodhashlib.sha256).digest() sign urllib.parse.quote_plus(base64.b64encode(hmac_code)) url f{webhook}timestamp{timestamp}sign{sign} content f### {title}\n for e in entries[:10]: content f- [{e[title]}]({e[link]})\n payload {msgtype: markdown, markdown: {title: title, text: content}} requests.post(url, jsonpayload)注意钉钉机器人对消息内容有长度限制一长串列表点太多会被截断或拒绝。我只取前 10 条放进通知其余的留到 Notion 中查看。这样做既不会漏掉重要信息也避免了被接口限制。4. 定时更新、Token 过期与自愈策略4.1 cron 调度时间与仓库时区的坑GitHub Actions 的 cron 使用 UTC 时间这一点不熟悉的人很容易踩坑。比如你写0 8 * * *实际对应的北京时间是下午 4 点不是早上 8 点。要换算成北京时间标准做法是0 0 * * *对应北京时间早上 8 点或者用30 22 * * *对应北京时间早上 6 点 30 分。也可以直接使用TZAsia/Shanghai环境变量配合 cron 工具来规避换算问题但在 GitHub Actions 里更推荐直接算好 UTC 偏移自己心里有数。另一个更隐蔽的问题是GitHub Actions 的 cron 并不保证精确到秒级甚至在负载高峰期会延迟几分钟甚至更久。这不是故障而是平台调度策略。所以不要把需要精确到分钟级别的任务寄托在 Actions 上如果真有这个需求应该考虑独立定时服务。4.2 wewe-rss 这类工具的 24 小时过期问题怎么处理热词里多次出现“wewe-rss 24小时后过期怎么延长”这个我专门多说几句。wewe-rss 是第一方微信公众号转 RSS 的工具它为了维持登录状态使用了一种会过期的 token 机制。很多人在配置完成后第二天发现抓取失败看日志发现认证过期确实很烦。解决思路有几个层次。第一个层次是“延长 Token 的有效期”。如果你能控制部署 wewe-rss 的服务端可以检查它的配置项看看AUTH_CODE或类似参数的过期设置有的版本支持修改有效期但有些版本写死了 24 小时。第二个层次是“自动刷新”。如果服务端提供了 refresh token 接口可以在配置文件中预留一个刷新逻辑每次 GitHub Actions 跑之前先用 refresh token 获取新的访问 token再发起请求。但这样做的前提是服务端支持不支持的话技术上也强求不了。第三个层次是最简单可行的在 GitHub Actions 中设置一个专门用于检查“源可用性”的任务如果发现 wewe-rss 的某个源返回认证失败就自动发送通知钉钉、Telegram 等提醒你去手动刷新。这虽然不能完全自动化但至少让你不会在三天后才发现源坏了。这里还有个操作技巧如果你经常遇到 wewe-rss 的 token 需要刷新可以考虑把“刷新动作”作为 workflow 的一个独立 job 单独管理手动触发时只跑这个 job而不是重跑整个抓取流程可以节省很多时间。4.3 失败重试与健康检查自动化的敌人是“悄然失败”。如果 workflow 报错了但没人知道那这套系统就形同虚设。所以我在 workflow 中加了一个失败通知步骤- name: Notify failure if: failure() uses: actions/github-scriptv7 with: script: | const runUrl https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}; const text RSS 抓取任务失败请查看${runUrl}; await fetch(process.env.DINGTALK_WEBHOOK, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ msgtype: text, text: { content: text } }), });使用if: failure()可以让这个步骤只在前面任务失败时执行。如果你配置了健康检查也可以在这里加一个请求给 uptime 监控比如 UptimeRobot 或自建的 Uptime Kuma。这样 workflow 一旦失败你手机会立刻收到通知不用打开 GitHub 才发现红灯。4.4 通过 Notion 数据库反向跟踪运行状态我习惯在 Notion 里专门建一个“运行日志”数据库每次 workflow 运行结束时脚本把运行时间、新增文章数、源失败列表写入一条记录。这样即使没有主动推送也可以随时在 Notion 中查看这套自动化方案的健康程度。这个设计的好处是你可以把“运行日志”数据库和主数据库放在同一个页面用 Notion 的关系属性关联甚至可以做一个小 dashboard统计每日新增文章数、各来源占比等。虽然是锦上添花但有了这些数据排查问题时效率会高很多。5. 常见问题与排查技巧实录5.1 Notion API 返回 401 或 404这两个报错可以说是最常见的。401 表示认证失败通常原因是 Token 错误或者 Token 对应的 Integration 没有被允许访问你的数据库。404 则表示找不到数据库或页面但要注意如果 Token 权限不足Notion 也有可能会返回 404 而不是 403这是它的一种安全机制防止未授权用户探测资源是否存在。排查步骤很简单先去 Notion 的 Integrations 页面确认 Token 有效然后进入数据库页面点击右上角的三个点菜单找到 Connections 选项把对应的 Integration 添加进去。这个步骤遗漏是大部分 404 的根源——创建 Integration 之后还需要手动建立关联很多人会卡在这一步。5.2 数据库字段写入失败如果日志显示创建 page 时返回 400最常见的错误是字段类型不匹配。比如你给 URL 类型的字段传了字符串或给日期类型传了非 ISO 格式的值。排查方式是把 API 返回的错误信息完整打印出来通常它会精确指出是哪个属性出了问题。Notion API 的报错信息比很多平台要友好不要忽略它。5.3 图标不显示或 favicon 服务超时站点图标字段一直不显示原因通常有两个。一是图标 URL 本身不可访问某些站点禁止外部引用二是 Notion 的图片代理会缓存一部分图标新域名可能需要一些时间才能生效。处理方式是在脚本中加入一个简单的 URL 可访问性检查如果图标地址返回非 200 状态码就回退到默认图标。这对老站点特别有用。5.4 GitHub Actions 总是报 “Process completed with exit code 1”这个问题过于笼统但九成是 Python 脚本里的异常没被捕获。建议在脚本入口统一包一层 try/except打印完整堆栈。另外为了方便排查应该把 Python 的 print 输出中文并在 workflow 中加一步actions/upload-artifact把抓取结果以 JSON 文件形式上传这样即使运行失败也能看到抓到了什么数据。- name: Upload debug artifacts if: always() uses: actions/upload-artifactv4 with: name: debug-output path: | output/*.json if-no-files-found: ignore5.5 重复记录的终极解决方案如果你的数据库里已经积累了重复记录最快的清理方式是写一个一次性脚本读取所有记录按链接分组保留最早创建的那条删除其余。删除时调用 Notion API 的 DELETE 方法对 page 执行 archive 操作。这类脚本不建议在 workflow 中频繁跑手动执行一次即可。为了防止后续再产生重复可以在写入前不仅查询近 30 天链接还加一个内存中的待写入链接集合避免同一个 feed 里出现两条相同链接。这种问题在某些站点输出不规范时可能出现——明明这次任务里只读了一遍但 feed 里同时包含一个链接的多个变体规范化处理后它们恰好相同。5.6 运行时长膨胀几百条记录后明显变慢数据量变大后查询已有链接和逐条写入都会变慢。除了前文说的“只查 30 天”优化还可以引入批量写入思路。Notion API 不支持真正的批量创建但可以配合asyncio在速率限制内并发写入提升吞吐。不过考虑到个人用量绝大多数情况下没必要做这层优化先跑起来等真遇到瓶颈再说。6. 实操经验总结与小技巧补充这套方案已经稳定运行了比较长的时间从最初的纯手动刷新到现在全自动归档体验提升非常明显。有几个小经验在文档和教程里很少会提到这里一并分享。第一个是“把配置和代码分离”的思路。不要把 RSS 源列表硬编码在 Python 脚本里放到 YAML 配置文件中这样以后添加源只需要改配置、提交推送workflow 会在 push 时自动触发一次不需要手动运行任何东西。这个体验非常顺滑像在维护一个可以自我更新的清单。第二个是“优先级”的设计。不是所有源都要同样的更新频率。对于更新频繁的科技新闻类源我设置了一个单独的 workflow 每 30 分钟跑一次对于个人博客这类更新慢的源每 6 小时跑一次就足够了。通过拆分 workflow可以避免一把抓导致的高频源延迟。第三个是“善用 Notion 的重复模板”。Notion 数据库支持为每条记录设置模板模板里可以预置默认标签、默认状态甚至默认关联的阅读笔记页。当自动化写入新记录时这些默认值会自动生效省去手动设置的麻烦。还有一个小建议加上“阅读笔记”这个关联功能。在 Notion 中创建另一个数据库“阅读笔记”RSS 数据库里的每条记录可以关联一篇笔记页。这样当你在 RSS 数据库里看到感兴趣的文章点开它可以随手记录想法。整个信息处理链路从“订阅”到“阅读”到“输出”就完整了。这个项目后续还可以往几个方向扩展把 Notion 数据库作为一个中间层接一个 Telegram Bot 用来推送到手机上阅读或者结合 Notion 的评论功能做标记再或者把全文抓取下来存入 Notion 页面实现彻底的离线阅读。但这些都是“有了基础之后再考虑”的事当前最重要的是先把自动化的闭环跑起来。