
个人主页:
wengqidaifeng
✨ 永远在路上,永远向前走
个人专栏:
数据结构
C语言
嵌入式小白启动!
重要OJ算法题详解
蓝桥杯备战
C++从菜鸟到强手
python启航
AI大模型Agent:拥抱未来,赋能自己
文章目录
- [拆解 tmtpost-news-daily:一个 OpenClaw 每日早报 Skill 如何落地](#拆解 tmtpost-news-daily:一个 OpenClaw 每日早报 Skill 如何落地)
-
- 一、这个项目到底要解决什么问题
- 二、示例项目里,需求是怎么被定义的
-
- [1. 数据抓取](#1. 数据抓取)
- [2. 文档生成](#2. 文档生成)
- [3. 飞书通知](#3. 飞书通知)
- [4. 定时调度](#4. 定时调度)
- 三、项目结构有什么特点
- 四、主流程是怎么串起来的
- [五、为什么爬虫层要同时准备 Playwright 和 requests 两套方案](#五、为什么爬虫层要同时准备 Playwright 和 requests 两套方案)
-
- [1. Playwright 是主方案](#1. Playwright 是主方案)
- [2. requests 是降级方案](#2. requests 是降级方案)
- [3. 双路径设计体现了什么](#3. 双路径设计体现了什么)
- 六、数据模型为什么要单独抽出来
-
- [1. `Article` 统一了上下游协议](#1.
Article统一了上下游协议) - [2. `BaseCrawler` 统一了抓取接口](#2.
BaseCrawler统一了抓取接口)
- [1. `Article` 统一了上下游协议](#1.
- [七、MarkdownBuilder 为什么值得单独表扬](#七、MarkdownBuilder 为什么值得单独表扬)
- 八、飞书通知模块体现了什么产品意识
- 九、配置文件设计有哪些亮点与风险
- [十、Cron 与 `scripts/run.py` 解决了什么问题](#十、Cron 与
scripts/run.py解决了什么问题) - [十一、测试说明了这不是一次性 Demo](#十一、测试说明了这不是一次性 Demo)
- 十二、我们实际阅读工程时观察到的两个真实问题
-
- [1. 依赖缺失问题](#1. 依赖缺失问题)
- [2. 路径与导入稳定性问题](#2. 路径与导入稳定性问题)
- 十三、这个项目最值得初学者学的,不是爬虫技巧,而是工程组织方式
- 十四、如果继续演进,这个项目还能怎么升级
-
- [1. 凭据治理](#1. 凭据治理)
- [2. 抓取策略增强](#2. 抓取策略增强)
- [3. 内容质量优化](#3. 内容质量优化)
- [4. 调度与运行可观测性](#4. 调度与运行可观测性)
- [5. 测试完善](#5. 测试完善)
- 十五、从数据流角度重画这个项目
- 十六、爬虫稳定性:为什么真实世界比样例页面复杂
- 十七、反爬与伦理边界:新闻抓取项目应该补上的工程意识
-
- [1. 控制频率](#1. 控制频率)
- [2. 尊重站点规则](#2. 尊重站点规则)
- [3. 减少无意义请求](#3. 减少无意义请求)
- [4. 不绕过高强度访问控制](#4. 不绕过高强度访问控制)
- [十八、Markdown 输出为什么适合作为自动化中间产物](#十八、Markdown 输出为什么适合作为自动化中间产物)
-
- [1. 人类可读](#1. 人类可读)
- [2. 机器可处理](#2. 机器可处理)
- [3. 适合版本管理](#3. 适合版本管理)
- [4. 便于二次分发](#4. 便于二次分发)
- 十九、飞书卡片设计:把"完整信息"变成"可消费信息"
- [二十、Cron 运行时:最容易被忽略的不是代码,而是环境](#二十、Cron 运行时:最容易被忽略的不是代码,而是环境)
- 二十一、配置设计的下一步:从单配置文件到配置分层
- 二十二、测试策略:当前测试很好,但还可以更贴近真实风险
-
- [1. Markdown 快照测试](#1. Markdown 快照测试)
- [2. 飞书 payload 结构测试](#2. 飞书 payload 结构测试)
- [3. 爬虫解析 fixture 测试](#3. 爬虫解析 fixture 测试)
- [4. 配置缺失测试](#4. 配置缺失测试)
- [5. 集成冒烟测试](#5. 集成冒烟测试)
- [二十三、从早报 Skill 推广到其他业务场景](#二十三、从早报 Skill 推广到其他业务场景)
- 二十四、生产化演进路线:不要一次性重写
-
- 第一步:清理敏感配置
- 第二步:补日志目录
- [第三步:增加 dry-run](#第三步:增加 dry-run)
- [第四步:增加 fixture 测试](#第四步:增加 fixture 测试)
- 第五步:增加运行摘要
- 第六步:增加失败告警
- 二十五、参考资料
- 二十六、结语
拆解 tmtpost-news-daily:一个 OpenClaw 每日早报 Skill 如何落地
面向已经理解 Skill 基础、想看完整工程样板的读者:这篇文章从需求、代码、配置、调度、测试和生产化演进几个角度,拆开一个真实早报 Skill 的落地过程。
如果说 weather-bit 和 qrcode-gen 主要用来建立 Skill 的基本感觉,那么配套示例里的 tmtpost-news-daily 才真正展示了一个"有业务目标、有工程结构、有调度流程、有测试意识"的 OpenClaw Skill 应该长什么样。它不是简单的脚本拼接,而是一次比较完整的 Agent 能力落地演示:从需求定义,到技术方案,到代码实现,再到定时触发、消息推送和基础测试,全链路都具备了。
这篇文章就以仓库里的真实工程为主线,拆解一个每日早报 Skill 是怎么从想法变成可运行项目的。
#mermaid-svg-Y6uBaQjmyvhC5Ozq{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Y6uBaQjmyvhC5Ozq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .error-icon{fill:#552222;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .marker.cross{stroke:#333333;}#mermaid-svg-Y6uBaQjmyvhC5Ozq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Y6uBaQjmyvhC5Ozq p{margin:0;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .cluster-label text{fill:#333;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .cluster-label span{color:#333;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .cluster-label span p{background-color:transparent;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .label text,#mermaid-svg-Y6uBaQjmyvhC5Ozq span{fill:#333;color:#333;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .node rect,#mermaid-svg-Y6uBaQjmyvhC5Ozq .node circle,#mermaid-svg-Y6uBaQjmyvhC5Ozq .node ellipse,#mermaid-svg-Y6uBaQjmyvhC5Ozq .node polygon,#mermaid-svg-Y6uBaQjmyvhC5Ozq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .rough-node .label text,#mermaid-svg-Y6uBaQjmyvhC5Ozq .node .label text,#mermaid-svg-Y6uBaQjmyvhC5Ozq .image-shape .label,#mermaid-svg-Y6uBaQjmyvhC5Ozq .icon-shape .label{text-anchor:middle;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .rough-node .label,#mermaid-svg-Y6uBaQjmyvhC5Ozq .node .label,#mermaid-svg-Y6uBaQjmyvhC5Ozq .image-shape .label,#mermaid-svg-Y6uBaQjmyvhC5Ozq .icon-shape .label{text-align:center;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .node.clickable{cursor:pointer;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .arrowheadPath{fill:#333333;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Y6uBaQjmyvhC5Ozq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Y6uBaQjmyvhC5Ozq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Y6uBaQjmyvhC5Ozq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .cluster text{fill:#333;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .cluster span{color:#333;}#mermaid-svg-Y6uBaQjmyvhC5Ozq div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Y6uBaQjmyvhC5Ozq rect.text{fill:none;stroke-width:0;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .icon-shape,#mermaid-svg-Y6uBaQjmyvhC5Ozq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .icon-shape p,#mermaid-svg-Y6uBaQjmyvhC5Ozq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .icon-shape .label rect,#mermaid-svg-Y6uBaQjmyvhC5Ozq .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Y6uBaQjmyvhC5Ozq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Y6uBaQjmyvhC5Ozq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Y6uBaQjmyvhC5Ozq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} config.yaml
分类、数量、输出、Webhook
main.py
流程编排
PlaywrightCrawler
动态页面抓取
RequestsCrawler
降级抓取
Article\[\]
title / url / intro / category
MarkdownBuilder
生成早报文档
FeishuCardNotifier
构建卡片摘要
output/tmtpost-daily-日期.md
飞书群通知
OpenClaw Cron
每天 08:00
注意:本文资料地址:
OpenClaw Agent 与 Skill 开发公开资料集
一、这个项目到底要解决什么问题
项目目标非常清晰:每天早上 8 点,自动抓取钛媒体中"金融"和"AGI"两个分类的热门文章,整理为 Markdown 早报文档,并通过飞书机器人把重点内容推送到群里。
这个目标看起来不复杂,但其实已经覆盖了一个自动化 Agent 项目的典型闭环:
- 定时触发
- 外部数据采集
- 结构化提取
- 内容组织
- 文档生成
- 即时通知
这也是它特别适合教学的原因。因为它既足够真实,又不会复杂到让初学者完全失去抓手。
二、示例项目里,需求是怎么被定义的
配套仓库中同时提供了与该项目强相关的文档:
代码/tmtpost-news-daily/docs/tmtpost-news-daily-requirements.md代码/tmtpost-news-daily/docs/tmtpost-news-daily-technical-design.md
这说明这个示例不是"先写代码再补文档",而是展示了一个更标准的流程:需求先行,方案随后,代码落地,最后再进入测试和运行。
从需求层看,项目被拆成四块:
1. 数据抓取
目标站点是钛媒体,聚焦两个分类:
- 金融
- AGI/人工智能
单条文章至少提取:
- 标题
- URL
- 简介
每个分类默认最多抓 20 篇,可配置。
2. 文档生成
输出为 Markdown,文件命名规则清楚,内容结构清楚,并且对空分类、空简介都定义了明确处理规则。
3. 飞书通知
不是把整份文档照搬过去,而是只推送前 3 篇重点内容,同时附上完整文档路径。这种设计很合理,因为 IM 适合摘要式触达,不适合承载超长正文。
4. 定时调度
通过 OpenClaw Cron 在 Asia/Shanghai 时区下每天 08:00 执行,形成稳定自动化流程。
这套需求定义非常值得学习,因为它把"用户价值"和"技术可实施性"平衡得很好。
三、项目结构有什么特点
阅读仓库结构后会发现,这个项目已经具备一个小型 Python 自动化工程的标准雏形:
text
tmtpost-news-daily/
├── SKILL.md
├── requirements.txt
├── config.yaml
├── main.py
├── docs/
├── scripts/
├── src/
└── tests/
这里最值得讲的是分层意识。
SKILL.md
用于对外描述这个 Skill 是什么、怎么装、怎么跑、如何注册 Cron。它对应的是 Agent 和使用者首先看到的入口层说明。
config.yaml
用于承载运行时可变参数,比如分类 URL、文章上限、输出目录、飞书 Webhook、重试次数、定时配置等。这意味着业务变化不必改代码。
main.py
是流程编排入口,不承担全部细节,而是把各模块按顺序串起来。
src/
把业务核心拆成配置、爬虫、生成器、通知器四个部分,利于维护、复用与测试。
scripts/
负责与 OpenClaw Cron 的运行入口衔接,解决工作目录、路径和跨平台执行问题。
tests/
提供对关键模块的基础保障,说明这个示例并没有把 Skill 仅仅当作"跑一下就行"的脚本。
四、主流程是怎么串起来的
从 main.py 可以看到,这个项目的执行顺序非常清晰:
- 加载配置
- 抓取所有分类文章
- 统计总数
- 生成 Markdown 早报
- 组装文章列表
- 推送飞书卡片
这种写法有几个好处。
第一,主程序非常像一份流程图,阅读成本低。
第二,模块间职责边界清楚,后续想替换某个实现时成本较低。
第三,日志埋点自然,便于排查是配置、抓取、生成还是通知环节出了问题。
一个自动化项目做到这一步,已经比很多"全堆在一个脚本里"的实现高出一个层次。
五、为什么爬虫层要同时准备 Playwright 和 requests 两套方案
这是整个项目里最有工程味的一部分。
1. Playwright 是主方案
src/crawler/playwright_crawler.py 负责动态页面抓取。它做了几件典型事情:
- 启动 Chromium
- 设置伪装参数和 User-Agent
- 等待页面加载
- 滚动页面触发懒加载
- 解析文章容器
- 提取标题、链接和简介
钛媒体这类站点存在动态渲染、懒加载和复杂 DOM 结构,仅靠静态请求未必稳定,因此 Playwright 作为主路径是合理的。
2. requests 是降级方案
src/crawler/requests_crawler.py 在 Playwright 不可用时接管任务。它通过:
requests.Session- 浏览器请求头伪装
BeautifulSoupHTML 解析
来完成相对保守的抓取逻辑。
3. 双路径设计体现了什么
体现的是"自动化系统要考虑失败后的继续运行"。真实环境下,Playwright 可能因为以下原因失败:
- 没装浏览器内核
- 依赖缺失
- 环境不支持
- 页面访问超时
- 目标站点策略变化
如果没有降级方案,整个任务就会直接断掉。有了 fallback,哪怕结果质量略降,也能提高整体可用性。
这正是工程系统和演示脚本的区别。
六、数据模型为什么要单独抽出来
src/crawler/base.py 里定义了 Article 数据类和 BaseCrawler 抽象基类。看上去只是"写得更规范",其实意义很大。
1. Article 统一了上下游协议
一旦文章数据被统一表示为:
titleurlintrocategory
那么无论数据来自 Playwright 还是 requests,后续的 MarkdownBuilder 和 FeishuCardNotifier 都不用关心来源差异。
2. BaseCrawler 统一了抓取接口
上层只需要知道 fetch(category, url, limit) 会返回文章列表,而不用管内部怎么实现。这种抽象让替换实现变得非常容易。
以后如果要新增:
- API 爬虫
- RSS 爬虫
- 第三方聚合源
也可以沿用同样接口扩展。
七、MarkdownBuilder 为什么值得单独表扬
src/generator/markdown_builder.py 的设计相当克制,但很扎实。它不是"顺手写个文件",而是把生成职责完整模块化了。
它处理了几个实际问题:
- 标题按年月日渲染
- 多分类顺序输出
- 简介截断到 100 字
- 分类为空时给出空状态
- 输出目录不存在时自动创建
- 文件名按日期模板生成
这说明作者已经开始按"产品输出"而不是"脚本副产物"的方式思考文档生成。很多自动化项目最后难用,就是因为输出格式混乱,不利于阅读和归档;而 Markdown 早报恰好兼顾了:
- 人类可读性
- 文件可保存性
- 后续二次处理便利性
八、飞书通知模块体现了什么产品意识
src/notifier/feishu_card.py 是整个项目里最接近"面向用户体验"的模块。它做的不只是发消息,而是考虑了以下细节:
- 标题格式统一
- 副标题展示总篇数
- 卡片中只取前 N 篇文章
- 每条展示标题链接和简介
- 无内容时有空状态文案
- 底部附完整早报路径
- 推送失败自动重试
这几个点看似普通,但恰好体现出一个自动化项目从"能跑"到"能用"的关键跨越。因为真正的通知系统不是"有结果就全扔过去",而是要适配用户的阅读习惯和沟通场景。
飞书卡片里只展示前 3 篇,就是典型的场景适配:群里要的是快速感知,而完整文档才负责沉淀。
九、配置文件设计有哪些亮点与风险
config.yaml 中配置了:
- 分类与 URL
- 抓取数量和超时
- 输出目录和文件模板
- 飞书 Webhook
- 重试参数
- 定时表达式和时区
亮点
亮点在于"可变因素尽量从代码中拿出来"。这意味着:
- 改抓取栏目不用改源码
- 改通知参数不用改源码
- 改输出命名不用改源码
- 改调度策略时至少有统一配置入口
风险
但这里也有一个真实项目里必须注意的问题:仓库中的 feishu.webhook_url 当前是明文写入的。教学示例可以理解,但在企业或公开协作环境中,Webhook、API Key、Token 最好都不要直接提交到仓库。
更推荐的方式是:
- 环境变量注入
- 本地私有覆盖配置
- 配置模板 +
.gitignore实际凭据文件
十、Cron 与 scripts/run.py 解决了什么问题
示例中的 SKILL.md 给出了 OpenClaw Cron 的注册方式,而项目中的 scripts/run.py 进一步承担了真正的运行桥接工作。
它做的事情包括:
- 计算 Skill 根目录
- 切换工作目录
- 确保 Python 路径可导入
src - 通过子进程调用
main.py
这个脚本看上去简单,却非常实用。因为定时任务的运行环境经常和手工终端不一致,路径、工作目录、模块导入都容易出问题。专门加一个运行脚本,能显著降低"手工跑得通,Cron 跑不通"的概率。
十一、测试说明了这不是一次性 Demo
仓库中提供了多组测试:
test_config.pytest_markdown_builder.pytest_feishu_card.pytest_base_crawler.py
从内容上看,它们主要覆盖了:
- 配置校验
- 文档生成格式
- 飞书卡片结构与重试逻辑
- 数据模型和抽象基类
这类测试虽然还没有完全覆盖爬虫真实抓取链路,但已经足够说明一个重要事实:这个项目在有意识地向"工程化 Skill"靠拢,而不是停留在演示脚本。
十二、我们实际阅读工程时观察到的两个真实问题
为了更贴近真实落地,我在当前环境里还顺手跑了该项目测试,结果暴露出两个很典型的现实问题。
1. 依赖缺失问题
当前环境没有安装 playwright,因此测试收集阶段就会因为导入 playwright.sync_api 失败而报错。这提醒我们:Skill 项目文档里虽然写了依赖,但真实部署时必须把环境初始化步骤执行到位。
2. 路径与导入稳定性问题
当前仓库位于带中文目录的路径下,测试日志中出现了异常路径编码表现,且 pytest 过程中存在 src 导入不稳定现象。这类问题在 Windows 本地、中文用户名或非 UTF-8 处理链路中并不罕见。
这不是说项目设计有根本性错误,而是说明从"示例项目"走向"长期运行工程"时,需要额外关注:
- 依赖安装一致性
- 路径编码兼容性
- 工作目录稳定性
- CI 或统一运行环境
十三、这个项目最值得初学者学的,不是爬虫技巧,而是工程组织方式
很多人看到 tmtpost-news-daily 会先关注:
- 怎么抓取页面
- 正则怎么写
- 飞书卡片 JSON 怎么拼
这些当然重要,但最值得学习的其实是它背后的组织方式:
- 先有需求文档
- 再有技术方案
- 再拆模块
- 再写配置
- 再补脚本
- 再做测试
- 最后接入定时调度
这套流程是可以迁移到任何其他 Skill 项目的。今天你抓新闻,明天也可以换成:
- 抓竞品动态
- 拉取行业报告
- 监控接口状态
- 生成日报周报
- 汇总知识库变更
Skill 的外壳会变,但这套工程路径不会变。
十四、如果继续演进,这个项目还能怎么升级
如果把它作为生产候选项目继续往前推,我会优先考虑下面几个方向:
1. 凭据治理
把 Webhook 从仓库配置中拿掉,改成环境变量或本地私有配置。
2. 抓取策略增强
增加更健壮的页面选择器、文章去重、失败告警和站点结构变化检测。
3. 内容质量优化
引入摘要清洗、去噪、关键词提炼,甚至接入模型做二次结构化整理。
4. 调度与运行可观测性
补充更完整的运行日志、失败告警和历史输出索引。
5. 测试完善
为爬虫增加可控 fixture、样例 HTML 测试和更完整的集成测试。
十五、从数据流角度重画这个项目
前面是按模块拆解项目,如果换成数据流视角,tmtpost-news-daily 的结构会更清楚。
它的数据流可以写成:
text
config.yaml
-> categories
-> crawler.fetch()
-> Article[]
-> MarkdownBuilder.build()
-> output/tmtpost-daily-YYYY-MM-DD.md
-> FeishuCardNotifier.notify()
-> 飞书群卡片
这个流向说明了三个关键点。
第一,配置是起点。分类名称、URL、文章数量、输出目录、飞书参数都来自配置文件。配置一旦不稳定,后面模块都会受影响。
第二,Article 是核心中间格式。只要爬虫输出稳定的 Article,后面的 Markdown 和飞书通知就不需要关心页面结构。
第三,最终产物有两个:一个是完整 Markdown 文件,一个是飞书卡片摘要。前者负责归档,后者负责触达。
这种"中间模型 + 双输出"的设计非常值得学习。很多自动化项目一开始只关注发送通知,结果没有沉淀文件;也有项目只生成文件,结果没人看到。这里把两者都做了,实用性明显更高。
十六、爬虫稳定性:为什么真实世界比样例页面复杂
tmtpost-news-daily 用 Playwright 作为主路径,是一个非常现实的选择。现代网站经常存在:
- JavaScript 动态渲染
- 懒加载
- 无限滚动
- 图片或卡片异步加载
- 登录态差异
- 反爬策略
- DOM 结构变化
如果只用 requests 抓初始 HTML,很可能拿不到完整列表。Playwright 控制真实浏览器,能等待页面渲染、执行脚本、滚动页面,因此更接近用户实际看到的页面。
但 Playwright 也不是万能的。它的问题在于:
- 环境依赖更重
- 需要安装浏览器内核
- 在服务器上可能遇到沙箱、字体、图形环境问题
- 执行速度比 requests 慢
- 资源占用更高
这就是为什么项目准备了 requests 降级方案。降级不是为了完美替代,而是为了在主路径失败时保留最基本可用性。
如果进一步增强爬虫层,可以考虑:
- 对页面结构变化做日志记录
- 把选择器提取到配置
- 增加页面快照保存
- 对重复 URL 去重
- 对空结果发出告警
- 为每个分类单独捕获异常
- 将抓取时间、状态码、文章数写入运行摘要
一个生产级早报系统,不一定保证每天抓到同样多文章,但应该保证"抓不到时有人知道为什么"。
十七、反爬与伦理边界:新闻抓取项目应该补上的工程意识
新闻抓取类 Skill 很容易被做成"能抓就行",但真实落地时还需要考虑反爬与合规边界。这个项目抓取的是公开网页,但仍然应该遵守基本原则。
1. 控制频率
每日早报任务一天跑一次,天然频率较低,这是合理的。不要把它改成高频轮询,尤其不要短时间反复访问同一分类页。
2. 尊重站点规则
如果目标网站提供 RSS、开放 API 或明确的数据接口,优先使用更稳定、更合规的接口。页面抓取适合没有接口或学习演示场景,但不一定是长期最佳方案。
3. 减少无意义请求
当前项目只抓分类页和有限文章简介,这是克制的。如果未来要补正文摘要,应设置上限、缓存和去重,不要每次全量抓取。
4. 不绕过高强度访问控制
如果页面出现验证码、登录限制或明确拒绝自动化访问,不应该通过激进手段绕过。对企业项目来说,稳定性和合规性比多抓几篇文章更重要。
这些内容不影响本地学习,但如果把项目带到真实环境,就应该写进技术方案或运行说明。
十八、Markdown 输出为什么适合作为自动化中间产物
MarkdownBuilder 的设计很朴素,但选择 Markdown 作为早报格式非常聪明。原因有几个。
1. 人类可读
Markdown 不需要专门软件,直接打开就能看。标题、分类、链接和简介结构都清楚。
2. 机器可处理
Markdown 是纯文本,后续可以继续被 Agent 读取、摘要、转换为 HTML、导出 PDF 或同步到知识库。
3. 适合版本管理
如果每天生成一份早报,Markdown 很适合纳入归档目录,甚至可以用 Git 跟踪变化。
4. 便于二次分发
同一份 Markdown 可以复制到飞书文档、企业微信、邮件、博客系统或内部知识库。
因此,Markdown 不只是输出格式,而是自动化链路里的中间资产。飞书卡片负责即时提醒,Markdown 负责长期沉淀。这种双层输出模式很适合企业内部日报、周报和巡检报告。
如果继续优化 Markdown 生成,可以考虑:
- 增加生成时间
- 增加总文章数
- 增加每个分类文章数
- 增加来源说明
- 增加任务运行状态
- 增加"抓取失败分类"提示
这样早报不仅是内容文件,也能成为运行记录。
十九、飞书卡片设计:把"完整信息"变成"可消费信息"
飞书卡片模块有一个很好的取舍:只展示前 N 篇文章,而不是把所有文章都塞进群消息。
在协作工具里,信息太多等于没有信息。群通知真正需要的是:
- 一眼知道今天有没有内容
- 一眼看到最重要的几条
- 能点击标题进入原文
- 能找到完整早报路径
这正是当前卡片结构做的事。
如果继续产品化,可以给卡片增加一些轻量增强:
- 按分类展示 top 文章
- 在副标题里显示分类数量
- 在底部增加生成时间
- 抓取失败时显示警告
- 没有 Webhook 时只生成文档不推送
- 推送失败时保留本地失败记录
不过要注意,卡片不是越复杂越好。飞书卡片适合摘要,不适合长文。完整内容应该回到 Markdown 文档。
二十、Cron 运行时:最容易被忽略的不是代码,而是环境
通过 OpenClaw Cron 注册每日 08:00 任务,这一步把 Skill 从"手动工具"升级成"自动服务"。但定时任务最容易出问题的地方,不是主逻辑,而是运行环境。
常见问题包括:
- 工作目录不对
- Python 解释器不对
- 依赖环境不对
- 环境变量缺失
- 相对路径失效
- 日志看不到
- Windows 编码异常
- Playwright 浏览器内核没安装
scripts/run.py 的价值正在这里。它先定位 Skill 根目录,再切换工作目录,然后用当前 Python 调 main.py。这能减少不少"手动跑可以,定时跑不行"的问题。
如果继续增强 Cron 运行,可以考虑:
- 在启动时打印 Python 路径
- 打印当前工作目录
- 打印配置文件路径
- 对关键依赖做预检查
- 把每次运行日志写入
logs/ - 对失败任务生成错误摘要
自动化任务一旦定时运行,就会逐渐变成"没人盯着也要可靠"。这时,运行环境可观测性就非常重要。
二十一、配置设计的下一步:从单配置文件到配置分层
当前项目的 config.yaml 对教学非常友好,所有配置集中在一个文件里。但真实项目可能需要配置分层。
常见分层方式是:
config.example.yaml:提交到仓库,作为模板config.local.yaml:本地私有配置,不提交- 环境变量:放 Webhook、Token、API Key
- 命令行参数:临时覆盖日期、输出目录、日志级别
这样做可以解决两个问题。
第一,避免敏感信息入仓库。Webhook URL、API Key、Token 都不应该出现在公开代码中。
第二,支持不同环境。开发环境、测试环境、生产环境可能使用不同 URL、不同输出目录、不同 Webhook。
对 tmtpost-news-daily 来说,可以设计成:
text
config.yaml # 默认非敏感配置
config.local.yaml # 本机覆盖
FEISHU_WEBHOOK_URL # 敏感 Webhook
LOG_LEVEL # 日志级别
这样既保留示例项目的简单性,也更接近真实工程。
二十二、测试策略:当前测试很好,但还可以更贴近真实风险
现有测试覆盖了配置、Markdown 生成、飞书卡片和基础数据模型。这些都是低成本、高收益的测试。下一步可以按风险继续补。
1. Markdown 快照测试
给定固定 Article 列表,生成 Markdown 后和预期文本对比。这样一旦格式变化,测试能立刻发现。
2. 飞书 payload 结构测试
当前已经测了基本结构,可以进一步测试:
- 超长标题
- 空简介
- 超过 top N 的文章
- 空文章列表
- 路径为空
3. 爬虫解析 fixture 测试
不要每次测试都访问真实钛媒体页面。可以保存一份样例 HTML,测试解析器能否提取标题和 URL。这类测试稳定、快速,也不依赖网络。
4. 配置缺失测试
测试缺少 categories、缺少 output.directory、max_articles_per_category 非法等情况。当前项目已经覆盖了一部分,这是好方向。
5. 集成冒烟测试
提供一个不真实推送、不真实访问网络的 dry-run 模式,验证主流程能从配置走到文档生成。这样可以在 CI 或本地快速确认项目没有坏。
测试的目标不是追求覆盖率数字,而是覆盖最容易导致早报断掉的地方。
二十三、从早报 Skill 推广到其他业务场景
tmtpost-news-daily 的模式可以迁移到很多任务。它的通用模板是:
text
定时触发 -> 获取数据 -> 结构化整理 -> 生成文档 -> 推送摘要
这个模板可以扩展成:
- 竞品动态日报
- 招聘市场周报
- GitHub 项目更新
- 接口健康巡检
- 舆情监控摘要
- 销售线索日报
- 客服问题分类报告
- 内部知识库新增内容摘要
每个新场景只需要替换三个部分:
- 数据源
- 结构化模型
- 输出模板
比如把钛媒体换成 GitHub,就可以抓取 release、issue、star 增长和 trending 项目;把飞书早报换成接口巡检,就可以把 Article 换成 ServiceStatus;把 Markdown 模板换成表格,就能生成巡检报告。
这就是这个示例项目真正的价值:它不是一个新闻脚本,而是一个自动化 Skill 模板。
二十四、生产化演进路线:不要一次性重写
如果要把 tmtpost-news-daily 继续做成长期运行的内部工具,不建议一开始大改。更稳的路线是逐步演进。
第一步:清理敏感配置
把 Webhook 移到环境变量或私有配置。
第二步:补日志目录
每次运行生成一份日志,至少记录分类、文章数、输出路径和推送结果。
第三步:增加 dry-run
允许只生成 Markdown,不推送飞书,方便调试。
第四步:增加 fixture 测试
固定样例 HTML,保障解析逻辑。
第五步:增加运行摘要
在 Markdown 顶部或日志里写入运行状态。
第六步:增加失败告警
当连续多天抓取为空或推送失败时,给维护者发告警。
这条路线的好处是每一步都小,但每一步都增加真实可靠性。
二十五、参考资料
- Playwright 官方文档:理解浏览器自动化、页面等待、选择器和无头运行
- Beautiful Soup 文档:理解 requests 降级模式下的 HTML 解析
- 飞书开放平台机器人文档:了解自定义机器人和 Webhook 推送
- OpenClaw Capabilities 文档:理解 Skill、工具、插件和自动化任务之间的关系
二十六、结语
tmtpost-news-daily 的真正价值,不只是它能每天抓几篇新闻,而是它把一个 OpenClaw Skill 如何从需求变成工程,做了一次完整示范。它告诉我们,Skill 开发不是"写个 SKILL.md 就结束",也不是"脚本能跑就算成功",而是要把:
- 能力定义
- 运行配置
- 执行逻辑
- 结果生成
- 外部推送
- 定时调度
- 基础测试
全部串起来,才算真正落地。
对于想从 OpenClaw 入门走向实战的人来说,这个项目就是一条非常好的桥梁。它不只是一个演示脚本,更是一份很有参考价值的 Agent 自动化小型工程模板。