18 KiB
name, description, version, category, tags, metadata
| name | description | version | category | tags | metadata | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| blogwatcher-daily | RSS 订阅监控 + 文章管理 + 导出。使用 RSSHub + feedparser 抓取订阅,自动去重入库 SQLite;支持按单个订阅更新/查看/标记已读/删除文章,可将文章按 txt/markdown/html 导出。附「自然语言操作指南」,AI 智能体可直接把用户口语映射到 CLI 命令。 | 1.5 | custom |
|
|
Blogwatcher Daily
RSS 订阅监控自动化,抓取后去重入库,可按 txt / markdown / html 三种格式导出。
依赖
pip3 install feedparser
feedparser 是 Python 最成熟的 RSS 解析库,支持 RSS 1.0/2.0/Atom、任意编码、畸形 XML。HTML 导出使用 Python 标准库
html.escape,无第三方依赖。
实际抓取架构(重要)
YouTube 频道 URL
↓ 脚本自动识别并转为 RSSHub 格式
http://192.168.3.45:1200/youtube/channel/{id}
↓
curl → RSSHub → YouTube
普通 RSS(Engadget, Slashdot 等)
↓ 脚本直接访问(绕过 RSSHub /rss/ 路由)
原始 RSS URL
↓
curl → 目标网站
关键发现:RSSHub 的 /rss/{url} 路由不稳定(返回 RSSHub 欢迎页),
因此普通 RSS 源直接访问,不走 RSSHub 代理。
脚本内部 build_fetch_url() 根据 URL 类型自动选择路由:
- YouTube → RSSHub
- 已有的 RSSHub URL → 直接使用
- 其他 → 直接访问原始 URL
目录结构
~/.hermes/skills/custom/blogwatcher-daily/
├── SKILL.md # 本文件
├── scripts/
│ └── blogwatcher-daily.py # 扫描入库 + 订阅管理 + 文章导出(单文件)
├── subscriptions.txt # 订阅列表(name|URL)
└── blogwatcher.db # SQLite 数据库(自动创建)
使用方法
扫描订阅(默认)
python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py
- 扫描所有订阅,新增文章存入数据库
- 控制台打印每个订阅新增文章数量及总计
添加订阅
python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py \
--add "频道名" "URL"
YouTube 频道自动转换:直接贴 YouTube 频道 URL 或 feed URL, 脚本自动识别并转为 RSSHub 格式,无需手动拼接。
# 以下三种方式效果相同:
--add "Tech With Tim" "https://www.youtube.com/channel/UC4JX40jDee_tINbkjycV4Sg"
--add "Tech With Tim" "https://www.youtube.com/feeds/videos.xml?channel_id=UC4JX40jDee_tINbkjycV4Sg"
--add "Tech With Tim" "http://192.168.3.45:1200/youtube/channel/UC4JX40jDee_tINbkjycV4Sg"
列出订阅
python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py --list
强制回扫(--all)
# 强制抓取每频道10篇(忽略已读状态)
python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py --all
- 每频道最多10篇
- 用途:历史内容回扫、测试订阅状态
更新单个订阅(--update SUB)
只抓某一个订阅,不动其它源。SUB 可传订阅名或 --list 中的序号(详见下方「订阅标识(SUB)」)。
# 按名字更新
python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py \
--update "Tech With Tim"
# 按 --list 里的序号更新(例如第 3 个订阅)
python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py --update 3
# 强制回扫单个订阅最新 10 篇(叠加 --all)
python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py \
--update 3 --all
- 抓取失败 → 打印错误、退出 0(不打断脚本管道)
- 订阅未找到 → 退出 1
查看某订阅的文章(--articles SUB)
从数据库读文章(不联网),可选只看未读:
# 该订阅全部文章(按 fetched_at 降序)
python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py \
--articles "Engadget"
# 只看未读
python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py \
--articles "Engadget" --unread
# 也可用序号
python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py \
--articles 22 --unread
输出示例:
📄 [Engadget] 未读文章 (244 篇):
[1] 🆕 How long is a new Fire TV Stick actually supposed to last?
🔗 https://www.engadget.com/...
📅 Sat, 30 Aug 2026 12:00:00 GMT
...
- 已读用
📖,未读用🆕 - 每篇左侧的
[N]是订阅内 1-based 序号,可直接用于下方--delete --article-id N
标记已读(--mark-read [SUB])
不带参数 → 全库标记;带 SUB → 只标记指定订阅。
# 全库标记为已读
python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py --mark-read
# 只标记某个订阅
python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py \
--mark-read "Tech With Tim"
python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py \
--mark-read 3
删除文章(--delete SUB [--article-id N])
删除该订阅下所有文章或单篇。序号 N 与 --articles 显示顺序一致。
# 删除某订阅下所有文章
python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py \
--delete "Jon Law"
# 删除某订阅下第 2 篇(先用 --articles 查序号)
python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py \
--delete "Jon Law" --article-id 2
# 序号也支持
python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py \
--delete 2 --article-id 1
- 空库删除是幂等 no-op,退出 0
- 序号越界 → 退出 1,不改动 DB
- 删除不可撤销,请配合
--articles SUB先看清再删
订阅标识(SUB)
--update / --articles / --mark-read / --delete 的 SUB 参数按以下顺序匹配:
- 数字 → 视为
--list中的 1-based 序号 - 精确名称(区分大小写)
- 忽略大小写精确匹配
- 忽略大小写子串匹配
匹配到多条时脚本报错并列出候选:AI 智能体应向用户回问「你指的是 X 还是 Y?」或改用序号消歧。
导出文章(--export)
从 SQLite 里读取文章并输出到 stdout,格式通过 --export {txt,markdown,html} 选择:
# 今天所有文章 → Markdown
python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py --export markdown
# 指定日期 → HTML
python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py \
--export html --date 2026-08-28 > digest.html
# 全库最新 20 篇 → 纯文本
python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py \
--export txt --limit 20
# 单订阅最新 20 篇 → Markdown(按订阅名或 --list 序号)
python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py \
--export markdown --sub "异次元软件世界" --limit 20
过滤规则(--date / --limit / --sub 的组合语义)
| 参数组合 | 行为 |
|---|---|
--export FMT(默认) |
今天(本地时区)的所有文章 |
--export FMT --date X |
指定日期 X 的所有文章 |
--export FMT --limit N |
忽略日期,取全库最新 N 篇 |
--export FMT --date X --limit N |
日期 X 的最新 N 篇 |
--export FMT --sub SUB |
该订阅全部文章(--sub 也会让默认日期失效) |
--export FMT --sub SUB --limit N |
该订阅最新 N 篇 |
--export FMT --sub SUB --date X |
该订阅在日期 X 的文章 |
--export FMT --sub SUB --date X --limit N |
该订阅在日期 X 的最新 N 篇 |
--sub的 SUB 参数解析规则与--articles / --update / --mark-read / --delete完全一致(序号 → 精确 → 忽略大小写精确 → 忽略大小写子串),详见「订阅标识(SUB)」。
参数速查
| 参数 | 说明 |
|---|---|
--export {txt,markdown,html} |
触发导出模式并指定格式 |
--date YYYY-MM-DD |
目标日期(本地时区,默认今天) |
--limit N |
文章数上限;单独使用时忽略日期 |
--sub SUB |
只导出该订阅的文章(SUB=--list 序号或名称) |
输出去向
- 正文 → stdout(可直接
> file.md重定向) - 进度 → stderr(
📊 N 篇文章 → FMT)
格式细节
txt:分频道分组,纯文本无标记,适合终端查看或管道markdown:## 【频道】+- [title](link)+ 描述引用块html:完整<!DOCTYPE html>单文件,内嵌简洁 CSS,标题/URL/描述全部经html.escape转义(防 XSS)
自然语言操作指南(AI 智能体路由表)
用户通常不会记 CLI 参数。下面是常见口语意图 → 应执行命令的映射,AI 智能体应据此选择命令。
命令前缀统一是:
python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py
下表省略前缀,只列参数。
1. 抓取 / 更新
| 用户可能说 | 命令 | 备注 |
|---|---|---|
| "刷新所有博客"、"扫一下今天有什么新的"、"跑一下每日抓取"、"更新一下" | (无参数) | 全量扫描所有订阅 |
| "更新 Tech With Tim"、"抓一下 Engadget 最新的"、"看看第 3 个订阅有没有更新" | --update "SUB" |
SUB = 名字或序号 |
| "强制回扫 X 最新 10 篇"、"忽略已读重新抓 X"、"重新拉 X 全部" | --update "SUB" --all |
忽略去重、强抓 10 篇 |
| "强制回扫所有订阅"、"忽略已读全部重抓" | --all |
全量强抓 |
2. 查看订阅
| 用户可能说 | 命令 |
|---|---|
| "列出所有订阅"、"我订了哪些博客"、"有哪些频道"、"看订阅列表" | --list |
3. 查看文章
| 用户可能说 | 命令 |
|---|---|
| "看看 Engadget 里都有什么文章"、"X 的所有文章"、"列出 X 的文章" | --articles "SUB" |
| "X 还没看的"、"X 有哪些未读"、"X 的未读列表"、"哪些没读过" | --articles "SUB" --unread |
| "第 3 个订阅有什么文章" | --articles 3 |
AI 智能体判断"未读"意图的关键词:未读 / 没看 / 还没读 / 新的 / unread / 待看。
4. 标记已读
| 用户可能说 | 命令 |
|---|---|
| "全部标为已读"、"清一下未读"、"我都看过了"、"全部当作看过" | --mark-read |
| "把 X 都当作看过了"、"X 全部标为已读"、"这个频道我不感兴趣了先标已读" | --mark-read "SUB" |
5. 删除文章
| 用户可能说 | 命令 | 危险度 |
|---|---|---|
| "删掉 X 的所有文章"、"清空 X 的历史"、"重置 X" | --delete "SUB" |
高 — 建议先 --articles SUB 确认 |
| "删除 X 里第 3 篇"、"把 X 的第 2 条去掉"、"X 的 [5] 号删了" | --delete "SUB" --article-id N |
中 — 序号来自 --articles 输出 |
重要:删除不可逆。AI 智能体收到"删除 / 清空 / 移除"类意图时,除非用户明确肯定,应先执行
--articles SUB展示待删内容再回问一次。
6. 添加订阅
| 用户可能说 | 命令 |
|---|---|
| "订阅 X"、"添加频道 X"、"加个 RSS"、"帮我关注 X"(附 URL) | --add "NAME" "URL" |
- YouTube URL、RSSHub URL、原生 RSS URL 都直接传,脚本内部自动路由。
7. 导出
| 用户可能说 | 命令 |
|---|---|
| "把今天的文章导出成 markdown"、"生成今日摘要 md" | --export markdown |
| "导出 2026-08-28 的 HTML"、"我要那天的网页版" | --export html --date 2026-08-28 |
| "最新 20 篇导出为 txt"、"给我全库最新 20 篇" | --export txt --limit 20 |
| "把异次元软件世界的文章导成 markdown"、"只导 Engadget 的" | --export markdown --sub "SUB" |
| "导出异次元最新 20 篇 md"、"给我 Slashdot 最近 20 条 html" | --export markdown --sub "SUB" --limit 20 |
| "把 X 昨天的文章导出成 html" | --export html --sub "SUB" --date YYYY-MM-DD |
| "今天的文章导 html 到桌面" | --export html > ~/Desktop/$(date +%Y-%m-%d).html |
订阅标识(SUB)如何解析
--update / --articles / --mark-read / --delete 的 SUB 参数按下列顺序匹配:
- 数字 → 视为
--list中的 1-based 序号 - 精确名称(区分大小写)
- 忽略大小写精确
- 忽略大小写子串
匹配到多条时脚本会打印候选并退出 1。此时 AI 智能体应:
- 向用户回问「你指的是 X 还是 Y?」
- 或改用
--list里的序号做无歧义定位
匹配不到时也退出 1。
AI 智能体决策要点
- 看/查询类(无副作用):
--list、--articles。可直接执行。 - 抓取类(可能改 DB):
--update、无参扫描、--all。安全,直接跑。 - 写入类(会改 DB):
--mark-read、--add。可直接执行,但结束后简报结果。 - 删除类(不可逆):
--delete。先展示要删的内容再执行,除非用户在同一轮明确肯定。 - 导出类(只读):
--export。stdout 输出,AI 应帮用户决定重定向到哪个文件。 - SUB 消歧:能用序号就优先用序号(
--list是廉价的),避免多义。
添加订阅示例
YouTube 频道
python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py \
--add "Tech With Tim" "http://192.168.3.45:1200/youtube/channel/UC4JX40jDee_tINbkjycV4Sg"
Bilibili
python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py \
--add "B站频道" "http://192.168.3.45:1200/bilibili/user/{uid}"
普通 RSS
python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py \
--add "博客名" "http://192.168.3.45:1200/rss/https://example.com/feed.xml"
RSSHub vs 直接 RSS
脚本自动判断路由,不需要手动选择:
| 源类型 | 路由方式 | 示例 |
|---|---|---|
| YouTube 频道/用户/feed | RSSHub | 自动识别并代理 |
| RSSHub URL(已存储) | 直接使用 | http://192.168.3.45:1200/youtube/channel/... |
| 普通 RSS(Engadget, Slashdot 等) | 直接访问 | https://www.engadget.com/rss.xml |
⚠️ RSSHub 的
/rss/{url}代理路由不稳定(实测试返回欢迎页),普通 RSS 不要走 RSSHub。
配置
| 配置项 | 默认值 |
|---|---|
| RSSHub 地址 | http://192.168.3.45:1200(可设置 RSSHUB_URL 环境变量覆盖) |
| 数据库 | ~/.hermes/skills/custom/blogwatcher-daily/blogwatcher.db |
| 订阅列表 | ~/.hermes/skills/custom/blogwatcher-daily/subscriptions.txt |
Cron Job 设置
推荐每天早上 6:00 自动扫描:
cronjob --create \
--name "Blogwatcher Daily" \
--schedule "0 6 * * *" \
--repeat 999 \
--deliver "telegram:5038825565" \
--prompt "使用 blogwatcher-daily 技能执行每日 RSS 扫描。
执行步骤:
1. 加载 blogwatcher-daily 技能
2. 运行扫描:python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py
3. 汇报本次新增文章数量"
如需扫描 + 落盘 HTML 摘要(例如放进 Obsidian vault 或自建 dashboard),可用原生 crontab 串起来:
0 6 * * * python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py && \
python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py \
--export html > ~/blogwatcher/$(date +\%Y-\%m-\%d).html
数据流程
扫描入库(默认命令)
- 加载订阅:从
subscriptions.txt读取所有 name|URL 对 - URL 路由:
build_fetch_url()自动判断:- YouTube → 转为
http://192.168.3.45:1200/youtube/channel/{id} - RSSHub URL → 直接使用
- 其他 → 直接访问原始 URL(绕过不稳定的
/rss/路由)
- YouTube → 转为
- 抓取:curl 获取 XML,SSL 跳过验证
- 解析:feedparser 提取 title、link、description、pub_date
- 去重:按 link 去重,已存在则跳过
- 存储:新文章存入 SQLite
- 输出:控制台打印每个订阅新增数量与总计(不生成文件)
导出(--export)
- 查询:
fetch_articles()按date(fetched_at, 'localtime')过滤 + 可选feed_url按订阅过滤(--sub)+ 可选LIMIT N - 分组:
_group_by_channel()按channel_title分桶 - 格式化:
FORMATTERS[fmt]分发到format_txt/format_markdown/format_html - 输出:正文写 stdout,
📊 N 篇文章 → FMT进度写 stderr
已知问题
| 问题 | 说明 | 状态 |
|---|---|---|
| How to of the Day | wikiHow 完全封了爬虫 | ❌ 不可用 |
| How-To Geek | 远程服务器关闭连接 | ❌ 不可用 |
feedparser 已修复:電腦玩物 ✅、阿榮福利味 ✅、异次元软件世界 ✅、Slashdot ✅
注意事项
- 首次使用需
pip3 install feedparser - YouTube 路由需要 RSSHub 容器内配置
HTTP_PROXY/HTTPS_PROXY环境变量 - 每次最多取每源 10 篇(TED Talks 等除外,取 20 篇)
- 数据库自动创建,无需手动初始化
- OPML 文件可用 Python 解析后批量导入(参考 /wiki-ingest 流程中的 OPML 解析代码)
- 导出的"今天"按本地时区(SQLite
date(fetched_at, 'localtime')),不是 UTC - HTML 导出使用
html.escape转义标题/链接/描述,可安全嵌入网页;CSS 内嵌,单文件独立可用