--- name: blogwatcher-daily description: RSS 订阅监控 + 文章管理 + 导出。使用 RSSHub + feedparser 抓取订阅,自动去重入库 SQLite;支持按单个订阅更新/查看/标记已读/删除文章,可将文章按 txt/markdown/html 导出。附「自然语言操作指南」,AI 智能体可直接把用户口语映射到 CLI 命令。 version: 1.5 category: custom tags: [rss, blog, monitoring, automation, export] metadata: author: Hermes Agent last_updated: 2026-08-30 platform: macos, ubuntu custom_skill_path: /Users/weishen/.hermes/skills/custom/ installation_note: "blogwatcher-daily 脚本在 Mac mini 本地运行(依赖 feedparser)。需要 RSSHub 服务(http://192.168.3.45:1200)访问 YouTube/Bilibili 等被墙源。" --- # Blogwatcher Daily RSS 订阅监控自动化,抓取后去重入库,可按 txt / markdown / html 三种格式导出。 ## 依赖 ```bash 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 数据库(自动创建) ``` ## 使用方法 ### 扫描订阅(默认) ```bash python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py ``` - 扫描所有订阅,新增文章存入数据库 - 控制台打印每个订阅新增文章数量及总计 ### 添加订阅 ```bash python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py \ --add "频道名" "URL" ``` **YouTube 频道自动转换**:直接贴 YouTube 频道 URL 或 feed URL, 脚本自动识别并转为 RSSHub 格式,无需手动拼接。 ```bash # 以下三种方式效果相同: --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" ``` ### 列出订阅 ```bash python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py --list ``` ### 强制回扫(`--all`) ```bash # 强制抓取每频道10篇(忽略已读状态) python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py --all ``` - 每频道最多10篇 - 用途:历史内容回扫、测试订阅状态 ### 更新单个订阅(`--update SUB`) 只抓某一个订阅,不动其它源。SUB 可传订阅名或 `--list` 中的序号(详见下方「订阅标识(SUB)」)。 ```bash # 按名字更新 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`) 从数据库读文章(不联网),可选只看未读: ```bash # 该订阅全部文章(按 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 → 只标记指定订阅。 ```bash # 全库标记为已读 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` 显示顺序一致。 ```bash # 删除某订阅下所有文章 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 参数按以下顺序匹配: 1. 数字 → 视为 `--list` 中的 1-based 序号 2. 精确名称(区分大小写) 3. 忽略大小写精确匹配 4. 忽略大小写子串匹配 匹配到多条时脚本报错并列出候选:AI 智能体应向用户回问「你指的是 X 还是 Y?」或改用序号消歧。 ### 导出文章(`--export`) 从 SQLite 里读取文章并输出到 stdout,格式通过 `--export {txt,markdown,html}` 选择: ```bash # 今天所有文章 → 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`:完整 `` 单文件,内嵌简洁 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 参数按下列顺序匹配: 1. **数字** → 视为 `--list` 中的 1-based 序号 2. **精确名称**(区分大小写) 3. **忽略大小写精确** 4. **忽略大小写子串** 匹配到多条时脚本会打印候选并退出 1。此时 AI 智能体应: - 向用户回问「你指的是 X 还是 Y?」 - 或改用 `--list` 里的序号做无歧义定位 匹配不到时也退出 1。 ### AI 智能体决策要点 1. **看/查询类**(无副作用):`--list`、`--articles`。可直接执行。 2. **抓取类**(可能改 DB):`--update`、无参扫描、`--all`。安全,直接跑。 3. **写入类**(会改 DB):`--mark-read`、`--add`。可直接执行,但结束后简报结果。 4. **删除类**(不可逆):`--delete`。**先展示要删的内容再执行**,除非用户在同一轮明确肯定。 5. **导出类**(只读):`--export`。stdout 输出,AI 应帮用户决定重定向到哪个文件。 6. **SUB 消歧**:能用序号就优先用序号(`--list` 是廉价的),避免多义。 ## 添加订阅示例 ### YouTube 频道 ```bash 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 ```bash python3 ~/.hermes/skills/custom/blogwatcher-daily/scripts/blogwatcher-daily.py \ --add "B站频道" "http://192.168.3.45:1200/bilibili/user/{uid}" ``` ### 普通 RSS ```bash 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 自动扫描: ```bash 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 串起来: ```cron 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 ``` ## 数据流程 ### 扫描入库(默认命令) 1. **加载订阅**:从 `subscriptions.txt` 读取所有 name|URL 对 2. **URL 路由**:`build_fetch_url()` 自动判断: - YouTube → 转为 `http://192.168.3.45:1200/youtube/channel/{id}` - RSSHub URL → 直接使用 - 其他 → 直接访问原始 URL(绕过不稳定的 `/rss/` 路由) 3. **抓取**:curl 获取 XML,SSL 跳过验证 4. **解析**:feedparser 提取 title、link、description、pub_date 5. **去重**:按 link 去重,已存在则跳过 6. **存储**:新文章存入 SQLite 7. **输出**:控制台打印每个订阅新增数量与总计(不生成文件) ### 导出(`--export`) 1. **查询**:`fetch_articles()` 按 `date(fetched_at, 'localtime')` 过滤 + 可选 `feed_url` 按订阅过滤(`--sub`)+ 可选 `LIMIT N` 2. **分组**:`_group_by_channel()` 按 `channel_title` 分桶 3. **格式化**:`FORMATTERS[fmt]` 分发到 `format_txt` / `format_markdown` / `format_html` 4. **输出**:正文写 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 内嵌,单文件独立可用