Files
atlas/blogwatcher-daily/SKILL.md
2026-08-30 07:42:25 +08:00

453 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: blogwatcher-daily
description: RSS 订阅监控 + 文章管理 + 导出。使用 RSSHub + feedparser 抓取订阅,自动去重入库 SQLite;支持按单个订阅更新/查看/标记已读/删除文章,可将文章按 txt/markdown/html 导出。附「自然语言操作指南」,AI 智能体可直接把用户口语映射到 CLI 命令。
version: 1.4
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
```
**过滤规则**(`--date` 与 `--limit` 的组合语义)
| 参数组合 | 行为 |
|---------|------|
| `--export FMT`(默认) | 今天(本地时区)的所有文章 |
| `--export FMT --date X` | 指定日期 X 的所有文章 |
| `--export FMT --limit N` | 忽略日期,取**全库最新 N 篇** |
| `--export FMT --date X --limit N` | 日期 X 的最新 N 篇 |
**参数速查**
| 参数 | 说明 |
|------|------|
| `--export {txt,markdown,html}` | 触发导出模式并指定格式 |
| `--date YYYY-MM-DD` | 目标日期(本地时区,默认今天) |
| `--limit N` | 文章数上限;单独使用时忽略日期 |
**输出去向**
- 正文 → **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` |
| "今天的文章导 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')` 过滤 + 可选 `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 内嵌,单文件独立可用