| name | wechat-mp-rss-extractor |
| description | 扫描 we-mp-rss 本地服务(localhost:8001)的全部微信订阅号 RSS feed,提取全文文章到 raw/wechat-inbox/。Plan A: Chrome CDP(真实登录浏览器)→ Plan B: API/Playwright fallback → 占位文件。 |
| version | 3.1.1 |
| author | Hermes Agent |
| category | wiki |
| 相关 | ["rss-to-wiki-pipeline","web-content-reviewer","wiki-pipeline","inbox-screener","content-fetching"] |
微信公众号 RSS 提取器 v2(主动补抓全文)
Plan A/Plan B 决策树 → references/chrome-cdp-content-fetch.md(v3.0: Chrome CDP 为主 → API/Playwright 兜底,CDP 内联在 extractor.py 中,非独立脚本)。
每日自动发现管线(2026-07-04):09:00 cookie-renew → 09:10 article-discover(fetch_all_article())→ 09:30 inbox-pipeline(CDP 抓内容)→ 10:00 pipeline-monitor。
容器内部 cron Bug(2026-07-04 修复):main.py 中 cfg.args.job == "True" 应为 str(cfg.args.job) == "True"(布尔值 vs 字符串)。修复通过 volume mount 持久化(/Users/jinguo/projects/we-mp-rss/main.py:/app/main.py)。保持 SERVER_ENABLE_JOB=False 禁用,作为 Plan B 备用。
每日自动发现管线(2026-07-04):
09:00 → wechat-cookie-renew(续期 cookie)
09:10 → wechat-article-discover(外部 cron:docker exec fetch_all_article())
09:30 → wechat-inbox-pipeline(extractor → CDP → 入库)
10:00 → wechat-pipeline-monitor(48h 无新文章告警)
外部 cron 脚本:~/.hermes/scripts/wechat-article-discover.sh — 等待容器就绪 → docker exec fetch_all_article()。
BROWSER_TYPE=chromium(2026-07-04 修复):容器 env 曾设为 BROWSER_TYPE=webkit 但只安装了 chromium 浏览器。这导致 Playwright 启动 webkit 失败 → code_src:None → 二维码截图空白 → CDP 验证也因容器 Playwright 不可用无法提取二维码。修复:compose 中改为 BROWSER_TYPE=chromium。
CDP fetch 已内联(2026-07-04):fetch_article_content_via_cdp() 直接实现在 wechat-mp-rss-extractor.py 中,不再是独立脚本。使用 Target.createTarget(非已废弃的 /json/new)创建标签页,通过 Runtime.evaluate 提取 #js_content。
MaxPage=3(2026-07-04 修复):jobs/mps.py 中 fetch_all_article() 和 do_job() 的 MaxPage=1 改为 MaxPage=3,每轮抓取最多 15 篇/号。通过 volume mount 持久化(/Users/jinguo/projects/we-mp-rss/jobs/mps.py:/app/jobs/mps.py)。
ContentTaskQueue 已禁用(2026-07-04):GATHER.CONTENT=False + GATHER.CONTENT_AUTO_CHECK=False。容器内 Playwright WebKit 未安装且被微信反爬拦截,CDP 已完全替代其功能。
Volume mounts 持久化(2026-07-04):compose 中挂载 main.py 和 mps.py 的修复版本,确保每次重启 container 后 Plan B 修复生效。
历史文章补录(2026-07-04):wechat-backfill-export.py --db-path=/Users/jinguo/data/db.db --start-date=2026-07-02 --end-date=2026-07-04 → 354 篇有全文 → 232 篇已入库跳过 → 109 篇新导出到 wechat-inbox。
订阅添加(通过 __biz):用户提供微信文章 URL → Playwright 提取 window.biz(base64 fakeid)→ 解码 → 生成 MP_WXS_{数字} → INSERT INTO feeds → docker restart we-mp-rss。详见 references/subscription-management-via-biz.md。
中断期间文章未入库检查 → references/database-backfill-workflow.md — 当 Cookie 过期修复后,检查数据库中是否有未走 pipeline 的文章需补录、批量导出到 wechat-inbox 的完整流程。
Pipeline 健康诊断 → references/pipeline-health-diagnostics.md — 回答"pipeline 是否正常收录"的 8 步诊断清单(含"存活但停滞"上游同步停滞检测 + 两层故障诊断)。
DB recovery + subscription management → — 容器重启后订阅列表为空恢复、compose 密码行截断修复、.env + .gitignore 配置、扫码授权添加订阅流程、Wiki 订阅推荐方法论。
→ — 调查 sogou-weixin-mcp-server 补录历史文章的可行性,验证码问题导致自动化失败。
→ — 基于 wiki 覆盖缺口分析推荐新公众号的完整流程。
→ — 42 天中断修复实战:根因分析、Cookie 更新流程、Redis 缓存清理、批量补录操作、Docker 容器更新。
→ — 2026-07-04 验证成功。
→ — 48h 无新文章邮件告警。
→ — YAML 包裹格式要求。
通过 we-mp-rss (localhost:8001) 扫描全部公众号,使用 RSS 获取元信息 + REST API 主动触发补抓 + 拉取全文 的三段式方案,彻底解决 RSS content:encoded 为空的问题。
架构(v3 — Chrome CDP 为主)
we-mp-rss (localhost:8001)
│
├─ GET /rss?limit=30&offset=0 ──► 全部公众号 fakeid + 名称
│
├─ FOR each fakeid:
│ ├─ GET /rss/{fakeid} ──► RSS items(含 id=article_id, guid=微信URL)
│ │
│ ├─ 检查 content:encoded:
│ │ ├─ 已有全文 → html2text → raw/wechat-inbox/
│ │ │
│ └─ 空 content → Plan A: Chrome CDP(已被微信反爬持续封锁 18 天+)[extractor.py]
│ ├─ Target.createTarget → 导航 → 等待 8s → #js_content?.innerText
│ │ └─ 成功(2026-07-04 后罕见)→ 写 inbox ✅
│ │ ⚠️ 2026-07-05~07-22 连续 0%:CDP 返回 <100 chars 骨架 HTML
│ │ → 当前实际内容来源于 ContentTaskQueue 异步补齐,
│ │ RSS content:encoded 在发布后数小时~数天逐步填充
│ │
│ └─ Plan B: API/Playwright fallback(CDP 失败时)
│ │ ├─ POST refresh + poll
│ │ └─ 成功 → html2text → raw/wechat-inbox/
│ │
│ └─ 所有方案都失败 → 占位文件(标题 + 原链接)→ raw/wechat-inbox/
│
└─ raw/wechat-inbox/ ← 等 web-content-reviewer 评分
## cascade 调度系统:容器内部的文章发现主力
we-mp-rss 容器内部运行 APScheduler 级联调度系统,独立于外部 cron 持续发现文章。
### 关键配置(2026-07-06 实测)
| 组件 | 值 | 说明 |
|------|:--:|------|
| 任务调度器 | APScheduler(Python) | 容器启动时 `main.py` 初始化 |
| 轮询间隔 | `cascade.task_poll_interval = 30` 秒 | 每 30s 拉取待处理任务 |
| 文章发现 | `ENABLE_JOB=True` | 开启元信息发现(调微信 `appmsgpublish` API) |
| 内容抓取 | `GATHER.CONTENT=False` + `SERVER_ENABLE_JOB=False` | 禁用(由 CDP 替代) |
| 调度入口 | `cascade_schedule_service.start()` | 容器启动即运行 |
| 同步服务 | `cascade_sync_service.start_periodic_sync()` | 后台线程持续同步 |
| 文章更新 | `UpdateArticle` callback | 发现新文章后写入 `articles` 表 |
### 两条数据通道
容器内部
┌──────────────────────────────┐
│ cascade 调度 (APScheduler) │
│ 每 30s 轮询 │
│ → 调微信 appmsgpublish API │
│ → UpdateArticle callback │
│ → 写 articles 表 │
└──────┬───────────────────────┘
│
├──→ articles 表 (SQLite) ←── cascade 写入通道
│ create_time 可能滞后(不影响 RSS)
│
└──→ RSS 生成 ←── 独立数据源
pubDate 反映真实文章发现时间
extractor 从此通道读取
**核心洞察**:`articles` 表的 `create_time` 停止更新 ≠ pipeline 停摆。RSS 可能仍在正常更新。诊断时永远先查 RSS 端点再查 articles 表。
### 故障排查
```bash
# 快速诊断:RSS 是否健康
curl -s "http://localhost:8001/rss/MP_WXS_3236757533?limit=3" | grep -o '<pubDate>[^<]*</pubDate>'
# 检查 cascade 任务
docker logs we-mp-rss --tail 50 2>&1 | grep -i "cascade\|任务\|job\|成功\|失败"
# 检查 articles 表最新记录
docker exec we-mp-rss sqlite3 /app/data/db.db "SELECT datetime(MAX(create_time),'unixepoch') FROM articles"
外部 article-discover cron 的角色
wechat-article-discover.sh(每天 09:10)是外部担保,不是 RSS 更新源:
- 用 30s/feed 可控超时(
signal.alarm(30) + try/finally 确保 alarm 清除)
⚠️ Pitfall:signal.alarm() 必须用 try/finally 包裹:裸 signal.alarm(N); get_Articles(); signal.alarm(0) 模式下,如果 get_Articles() 在 C 扩展层阻塞,SIGALRM 可能无法被 Python 捕获,默认 SIGALRM 行为杀死整个进程(exit 142 = 128+14)。修复:用 try: get_Articles() finally: signal.alarm(0) 确保 alarm 总是被清除;同时把超时从 20s 放宽到 30s 给慢 feed 余量。
- 每次限 30 feeds(全量覆盖)
- cascade 正常时冗余,cascade 故障时保命
⚠️ 陷阱:不要因为 articles 表 create_time 陈旧就判定 pipeline 停摆。extractor 读取 RSS 端点,不依赖 articles 表。
> ⚠️ **文章发现机制说明(2026-07-06 修正)**:we-mp-rss 容器内部使用 APScheduler 级联调度系统(`ENABLE_JOB=True`,每 30s 轮询)持续发现文章。**RSS 端点 (`/rss/{fakeid}`) 和容器内 `articles` 表是两个独立的数据通道**。`articles.create_time` 停止更新不代表 RSS 停更——RSS 有独立的数据源。
>
> **两种 "Alive but Stale" 场景区分:**
> 1. **Cookie 过期(真·停摆)**:微信 API 返回 `200009: not found`,容器日志报错,所有 feed 0 新文章。→ 需要重新扫码 + 续期 cookie。
> 2. **articles 表滞后(假·停摆)**:`articles.create_time` 停在旧日期,但 RSS 端点仍返回当日文章,extractor 和 pipeline 正常运转。→ 是容器内部 cascade 调度写入 articles 表的通道有问题,但不影响 wiki pipeline。
>
> **快速分辨**:`curl -s "http://localhost:8001/rss/MP_WXS_3236757533?limit=3" | grep -o '<pubDate>[^<]*</pubDate>'` — 如果返回当日日期,则 pipeline 健康。
>
> **Cookie 过期修复**:`python3 ~/.hermes/scripts/wechat-cookie-renew.py` → Chrome DevTools 提取 cookie → 写入 `wx.lic`(需 YAML `token_data:` 包裹) → 重启容器。
> **完整架构文档** → `~/wiki/drafts/wechat-pipeline-architecture.md`(所有组件、cron、脚本、数据流)。
> **深度分析(2026-07-06)** → `~/wiki/drafts/wechat-pipeline-deep-analysis.md`(4 个 cron 的逐项源码审计、已知风险评级、修复建议)。
> **容器内部 cascade 调度系统**(2026-07-06 发现):we-mp-rss 容器使用 APScheduler 级联调度系统,`ENABLE_JOB=True`,每 ~30s 轮询任务(`cascade.task_poll_interval`)。这是文章发现的主力——独立于外部 cron。外部 `wechat-article-discover` 仅为外部担保。详见本章"cascade 调度系统"一节。
> **CDP 批量补录** → [scripts/wechat-backfill-cdp.py](scripts/wechat-backfill-cdp.py) — 从 DB 读取 `has_content=0` 的文章,用 CDP 逐篇补抓正文写入 inbox。2026-07-04 实测:39 篇缺正文文章 → 12 篇恢复 ✅、6 篇已存在、21 篇 URL 失效(旧文章被微信删除)。
> **补录指南** → `~/wiki/drafts/BACKFILL_GUIDE.md`(场景 A: 已有 URL 缺正文 / 场景 B: 完全丢失期)。
> **profile_ext 限制** → 微信历史文章页(profile_ext)需要额外人机验证,无法自动扫描丢失期的文章列表。详见 `devops/we-mp-rss-troubleshooting` skill 的 `references/historical-article-backfill-limitation.md`。
## 核心改进(v2 → v3)
| 问题 | v2 做法 | v3 做法 |
|------|---------|---------|
| RSS content 为空 | POST refresh 等后台 Playwright 异步补抓 | **Plan A: Chrome CDP 实时提取**(~3-5s/篇,零反爬) |
| 补抓成功率 | 容器内 Playwright ≈0%(2026-07 微信反爬升级) | **CDP 真实浏览器 ≈100%**(绕过所有反爬检测) |
| Playwright 兜底 | 容器内 Playwright + anti_crawler_config | **Plan B: API fallback**(旧方案保留,极少触发) |
| CDP 提取(2026-07 起被微信反爬持续封锁) | 永久跳过或占位文件 | **CDP 提取已被微信反爬持续封锁 18 天+**(2026-07-05~07-22 连续多轮实测 0%,全部 <100 chars)。当前内容获取依赖 ContentTaskQueue 异步补齐 RSS content:encoded(数小时~数天后),CDP 实时提取已不可用。 |
| 成功率 | ~32% 有全文(RSS)/ ~0%(容器 Playwright) | **≈85%+**(RSS content:encoded 异步补齐 + ContentTaskQueue),CDP 实时提取已不可用(2026-07-05~07-22 连续 0%)。extractor 现在主要作为"发现+占位符"工具运行,正文在后续 cron 轮次中逐步由 ContentTaskQueue 补全。 |
## 使用的 API
| 端点 | 用途 | 是否需要认证 |
|------|------|:----------:|
| `GET /rss?limit=30` | 获取全部公众号列表 | ❌ |
| `GET /rss/{fakeid}` | 获取单个账号 RSS feed(含 article.id + 微信URL) | ❌ |
| `POST /api/v1/wx/articles/{article_id}/refresh` | 触发单篇文章内容补抓 | ✅ (需要 Token) |
| `GET /api/v1/wx/articles/{article_id}?content=true` | 获取文章详情(含全文) | ❌ (当前代码注释了 auth) |
| `GET /api/v1/task-queue/content/status` | 查看内容补抓队列 | ✅ |
## 认证获取
`POST /api/v1/wx/articles/{article_id}/refresh` 需要登录 Token。
### 获取 JWT Token(API 方式)
`POST /api/v1/wx/auth/token` 是标准的 OAuth2 token endpoint,接收 **form-encoded** 数据(不是 JSON):
```bash
curl -s -X POST "http://localhost:8001/api/v1/wx/auth/token" \
-d "username=admin&password=admin@123"
# 返回 {"access_token":"eyJ...","token_type":"bearer","expires_in":259200}
这个 token 用于其他 API 调用的 Bearer Authorization header:
curl -s "http://localhost:8001/api/v1/wx/auth/qr/code" \
-H "Authorization: Bearer $TOKEN"
⚠️ 注意:/api/login?username=admin&password=...(GET)返回 null 且不设 cookie,不是有效的 API 认证方式。
获取方式:通过浏览器登录 http://localhost:8001/,获取 Cookie 或构造 Authorization header。
环境变量与依赖
认证变量
WERSS_AK + WERSS_SK 定义在 ~/.zshrc 中,但在 cron shell (bash, non-interactive) 中 .zshrc 不会自动加载,且 p10k 插件脚本在 bash 下报错。
正确做法: 在调用脚本前手动 export:
source ~/.zshrc 2>/dev/null
export PATH="$PATH:/Users/jinguo/Library/Python/3.14/bin"
python3 scripts/wechat-mp-rss-extractor.py --latest=5
Python 依赖
feedparser 和 html2text 已通过 pip3 install --break-system-packages --user 安装。
⚠️ Cron 环境坑: cron 的 bash 环境不加载 ~/.zshrc,且 ~/Library/Python/3.14/bin/python3 路径不存在。系统 Python 3(/usr/bin/python3)已自带 feedparser,直接用 /usr/bin/python3 调用脚本即可,无需修改 PATH。
# 正确做法:用系统 python3,手动 export WERSS 变量
source ~/.zshrc 2>/dev/null
export WERSS_AK="WKxxxxx"
export WERSS_SK="SKxxxxx"
cd ~/wiki && /usr/bin/python3 scripts/wechat-mp-rss-extractor.py --latest=5
运行
# 默认扫描全部,每个公众号最近 10 篇,写入 raw/wechat-inbox/
# ⚠️ --latest=10 在 21 个账号下仅需 ~7s(实测 2026-05-17),无需限制篇数
# ⚠️ 必须用 /usr/bin/python3(系统 Python 3.11 已自带 feedparser + html2text)
/usr/bin/python3 ~/wiki/scripts/wechat-mp-rss-extractor.py
# dry-run(不写入文件,不触发补抓)
/usr/bin/python3 ~/wiki/scripts/wechat-mp-rss-extractor.py --dry-run
# 限制篇数(默认即可,--latest=10 全量扫描仅 ~7s)
/usr/bin/python3 ~/wiki/scripts/wechat-mp-rss-extractor.py
# 只扫描部分账号
/usr/bin/python3 ~/wiki/scripts/wechat-mp-rss-extractor.py --fakeids=MP_WXS_3537616032,MP_WXS_3869059014
Cron 调用模板
source ~/.wiki-cron.env
cd ~/wiki && /usr/bin/python3 scripts/wechat-mp-rss-extractor.py --latest=10 --no-refresh
--no-refresh 跳过 API 补抓轮询,避免 extractor 在无正文文章上 hang 住。ContentTaskQueue 在后台异步补抓正文。
⚡ Foreground+timeout 替代方案(2026-07-30 验证)
background+kill 是官方推荐模式,但 foreground+超时兜底也是一个更简单的替代选择。extractor 在 hang 之前会先写完文件,因此 foreground 超时不会丢失数据。
# Step 1: 直接 foreground 调用,设足够大的 timeout
terminal(command="cd ~/wiki && source ~/.zshrc 2>/dev/null; /usr/bin/python3 scripts/wechat-mp-rss-extractor.py --latest=10 --no-refresh",
timeout=120)
# 即使 timeout (exit 124),extractor 可能已经写入了文件
# 2026-07-30 实测:foreground timeout=120,exit 124,已写入 18 个 inbox 文件
# Step 2: 检查 inbox 文件
ls -lt ~/wiki/raw/wechat-inbox/*.md 2>/dev/null | head -5
选择指南:
- 对可用性不敏感的 cron(出错跳过即可)→ foreground+timeout 更简洁,少 3 轮 process() 调用
- 需要精确控制 hang 检测(75s kill 决定)→ background+kill 更可控
- 共享 Hermes session 环境 → foreground(不需要额外 process action 权限)
验证:2026-07-30 pipeline cron:foreground timeout=120,exit 124,18 个 inbox 文件全部写入正常,pipeline 后续阶段正常运行。
⚡ 2026-07-04 补充:就算要用 background+kill,这是完整的三步流程
当 terminal(background=true) + process(action='kill') 是唯一可行模式时,不要假设 kill 后 extractor 什么都没做。2026-07-04 实测:extractor 启动后 78s (timeout) 被 kill,但已向 raw/wechat-inbox/ 写入了 25 篇新文件。
完整三步流程(已验证):
# Step 1: background 启动(不要 notify_on_complete — cron 每 20m 一次会刷屏)
# 用 --no-refresh 避免 content-refresh poll,但仍然可能 hang(feedparser 超时)
terminal(background=true,
command="cd ~/wiki && /usr/bin/python3 scripts/wechat-mp-rss-extractor.py --latest=10 --no-refresh",
timeout=120)
# Step 2: 等 60s 后 poll
process(action="poll", session_id="proc_xxx")
# 如果还活着(uptime_seconds > 75),大概率 hang 了
# Step 3: kill 但别跑——先检查文件
process(action="kill", session_id="proc_xxx")
ls -lt raw/wechat-inbox/*.md | head -5 # 检查新文件
echo "新文件: $(ls raw/wechat-inbox/*.md 2>/dev/null | wc -l)"
即使 --no-refresh 也可能 hang(2026-07-04 新发现): 现有文档假设 hang 的唯一来源是 content refresh poll loop(--no-refresh 可回避)。但 90s timeout 在 --no-refresh 下仍出现。推测另一 hang 源:feedparser.parse() 在某个慢速 RSS endpoint 上超时。这意味着 background+kill 模式是唯一可靠的运行方式,无论是否用 --no-refresh。
核心教训:extractor 可能在写文件之后才 hang——所以 kill 前/后必须检查 inbox 有无新文件。不要把 "extractor 超时" 等同于 "无新文章"。
Phase 1 预检:5s curl 跳过 offline 场景(2026-06-07 验证,3+ 连续 cron 命中)
当 we-mp-rss Docker 容器 down(Errno 61] Connection refused)时,整个 /usr/bin/python3 .../extractor.py --latest=10 仍会运行 ~5-10s 才返回 "Cannot reach we-mp-rss"。优化:先 5s curl 探测,refused 直接跳过。
# Pre-flight check
if ! curl --max-time 5 -s http://localhost:8001/rss?limit=1 >/dev/null 2>&1; then
echo "[$(date '+%H:%M')] wechat-mp-rss: down (refused) — skipping Phase 1" >> ~/wiki/cron-status.log
# Skip Phase 1 entirely, proceed to Phase 2
else
/usr/bin/python3 /Users/jinguo/wiki/scripts/wechat-mp-rss-extractor.py --latest=10
fi
状态判断:
curl exit 0 → we-mp-rss alive, run full extractor
curl exit 7 (Connection refused) → Docker down, skip + log
curl exit 28 (Operation timeout) → we-mp-rss hung at API level, skip + log
- 任何其他 exit → unknown, still run extractor (let it print diagnostic)
节省时间: 5-15s per offline cron × 24 crons/day = 2-6 min/day.
⚠️ Cron 模式下 extractor 可能 hang 75s+(已知稳态)。如果 terminal() 同步调用,cron 会被 180s terminal 超时整体 block。必须用 terminal(background=true, timeout=180) 启动 → process(action='wait'/'poll') 检查 → 75s 不动则 process(action='kill') 强制终止。不要用 bash -c "command & sleep N && kill" 或 GNU timeout(macOS 无、且 Hermes 拒绝 shell-level 背景化)。详见下方"API 调用中的坑"第 N 行新增条目。
we-mp-rss /rss 端点默认只返回前 10 个公众号(limit=10),必须传 ?limit=50&offset=0 才能获取全部当前 30 个订阅。这是通过阅读 API 源代码发现的——前端文档未说明。
认证方式
推荐使用 Access Key + Secret Key(通过环境变量 WERSS_AK + WERSS_SK,已在 ~/.zshrc 中永久设置)。
AK-SK 认证的 Header 格式为:
Authorization: AK-SK WKxxxxx:SKxxxxx
这与标准 JWT Bearer 不同,是 we-mp-rss 自定义的认证方案。
关键发现(踩坑记录):
- 运行时传
WERSS_TOKEN=<JWT> 也能工作,但需要先通过登录获取 JWT
- AK-SK 从 we-mp-rss 的 Access Key 管理界面创建,SK 只在创建时显示一次
- 已在 ~/.zshrc 中永久设置(
WERSS_AK + WERSS_SK),新终端开 zsh 后无需再传——直接用 python3 ~/wiki/scripts/wechat-mp-rss-extractor.py 即可
调试时发现的问题(2026-05-15)
| 现象 | 根因 | 解决方案 |
|---|
--dry-run 打印空(无任何输出) | dry-run 时 process_article 返回 None 但无日志 | 用 --latest=1 实跑检查 |
--latest=5 只显示 4 个 GET | 实际是触发了 API 的文章数,不是总处理数 | 正常行为,RSS 有全文时直接写入不打印 |
| wechat-inbox 188 篇全是旧文章 | 三保险去重机制跳过所有文章(已标记已读) | 正常——所有文章都已在之前扫描中处理过 |
| API GET 返回 404 | article_id 在 DB 中不存在(历史文章被删除) | 跳过,不触发 refresh |
| 脚本输出只有 "Inbox: ..." 和几个 "GET" | 被跳过的重复文章无任何输出,属正常行为 | 用 find raw/wechat-inbox -newer heartbeat/*.last-run -name "*.md" 验证是否有新文件 |
验证新文章的方法
⚠️ 关键诊断陷阱:we-mp-rss 容器的 articles 表和 RSS 生成是两条独立数据路径。articles.create_time 可能停在旧日期,但 RSS 端点的 pubDate 可能仍是今天的。检查 pipeline 是否健康时,永远先查 RSS 端点,再查 articles 表。
# 正确的一步诊断:检查 RSS 端点的文章日期
curl -s "http://localhost:8001/rss/MP_WXS_3236757533?limit=3" | grep -o '<pubDate>[^<]*</pubDate>'
# 如果 RSS 显示今天的日期 → pipeline 健康,articles 表滞后是独立问题
```bash
# 方法1:检查心跳文件时间 vs 最新文件时间
stat -f "%Sm" ~/wiki/heartbeat/wechat-mp-scan.last-run
ls -lt ~/wiki/raw/wechat-inbox/*.md | head -3
# 方法2:统计 inbox 总数(不变说明没有新文章)
ls ~/wiki/raw/wechat-inbox/*.md | wc -l
# 方法3:查看 cron-status.log
tail ~/wiki/cron-status.log
# 方法4(最重要):检查 extractor 输出中的 exists_in_articles 计数
# 这是 extractor 入口级 URL 黑名单拦截数(非 inbox 内部查重)
# 如果存在大量 exists_in_articles + wechat-inbox 文件数不增长 = 正常状态
/usr/bin/python3 ~/wiki/scripts/wechat-mp-rss-extractor.py 2>&1 | grep exists_in_articles
已确认的全文覆盖(2026-05-20 实测)
21 个账号全量扫描(每个账号取最新 10 篇):
- ✅ 账号完全正常:高可用架构、阿里云云原生、花叔、腾讯技术工程、阿里技术、架构师、机器之心、量子位、新智元、AI寒武纪、AGI Hunt 等 — 多篇实测 10-288KB 全文
- ⚠️ 混有少量已删除条目(7 chars 占位符):各账号均有少量文章被发布者删除,非账号级采集失败,账号本身有大量正常全文
- ❌ 非账号问题,是内容问题:7 chars 条目的根因是微信服务器返回"该内容已被发布者删除",wechat-mp-rss 的 Playwright 已正确识别并返回
content: "DELETED",这是微信侧内容不存在,不是提取工具问题
- ⚠️ status=1000(DB 有记录但 content_html 为空):ContentTaskQueue 补抓后仍为空,根因是文章被微信标记为不可抓取,Readability / TinyFish / browser CLI 均无法兜底
wechat-mp-rss 内部已使用 Playwright(重要发现 2026-05-20)
wechat-mp-rss 的内容抓取链路:
/app/driver/wxarticle.py:
WXArticleFetcher.get_article_content()
→ PlaywrightController (browser_type=webkit, mobile_mode=True)
→ anti_crawler_config (UA、viewport、extra_http_headers 反检测)
→ 检测 "该内容已被发布者删除" → 返回 content: "DELETED"
→ 检测 "当前环境异常" → 返回 fetch_error
结论:browser CLI / computer_use 在微信文章场景下不会比 wechat-mp-rss 更好,因为它们能做的事(JS 渲染、移动端 UA、反检测配置)wechat-mp-rss 已经做了。
唯一可能有差异的方向:residential proxy(改变 IP 出口),但:
- 是收费项目($5-20/GB 或 $50-500/月)
- 解决的是 IP 被 block 的场景,对"内容已被发布者删除"无效
- 当前 wechat-mp-rss 跑在本地 Docker,IP 应该无问题
不要 尝试的方案:
- browser CLI → 同一个人 IP + 同等渲染能力,无额外收益
- computer_use → 云端沙盒 IP(通常是数据中心 IP)反而更易被微信 block
- Readability → 是 HTML→Markdown 转换器,输入空 HTML 输出空,无法恢复不存在的内容
we-mp-rss 文章 ID 获取(重要)
RSS 中的 <id> 标签是数据库 article_id(如 3000551159-2653565136_1),用于后续 API 调用(refresh、mark-read)。
但 feedparser 可能错误地将 <guid>(微信 URL)映射到 entry.id,导致 article_id 变成微信 URL。
解决方案:解析原始 XML 中的 <id> 标签,而非依赖 feedparser 的 entry.id 映射。
import xml.etree.ElementTree as ET
root = ET.fromstring(raw_xml)
items = root.findall('.//item')
for item in items:
id_el = item.find("id")
article_id = id_el.text if id_el is not None else ""
标题预过滤(2026-07-04 新增)
在 process_article() 入库前增加一道标题级预过滤,拦截明显低质的文章,减少 inbox-screener 的评分压力。在 url_exists_in_articles() 去重检查之前执行。
scripts/wechat-mp-rss-extractor.py 中的 should_skip_article(title, feed_name) 函数实现。
过滤类别(最终版 2026-07-04)
| 类别 | 拦截词 | 依据 | 已移除的过宽词 |
|---|
| recruitment | 招聘/英雄帖/校招/招人/诚聘/加入我们 | 纯招聘帖,零技术内容 | 岗位("AI对岗位技能的影响"误杀)、年薪 |
| event | 报名/专场/分享会/论文分享/交流会 | 活动报名帖,pipeline 数据确认全部 vxc<49 | 人才交流 |
| digest | 速递/关键词Top/每周关键词/AI速递 | 摘要聚合,vxc 通常 9-20 | — |
| emotional_news | 炸崩/真相来了/名单曝光/惊天 | 情绪新闻,无技术深度 | 蒸发(叶小钗误杀)、离职潮、召回率(ML术语) |
| marketing | 纸质版/劝你别买/打折 | 广告营销 | 培养计划、限时、你值得拥有 |
| education_activity | 产教融合/教育社区 | 教育活动报道 | 产教(单字子串) |
⚠️ 设计原则:宁可漏放,不可误杀
标题预过滤是在 extractor 入口处做的快速判断,没有上下文理解能力。一旦误杀,文章永远进不了 inbox,也就不会被 LLM 评分和入库。
已修复的误杀案例:
| 关键词 | 误杀原因 | 修复 |
|---|
岗位 | "AI 对岗位技能的影响"是技术文章 | 已移除 |
召回率 | ML 标准术语("RAG 召回率优化") | 已移除 |
培养计划 | "腾讯犀牛鸟开源人才培养计划"含技术内容 | 已移除 |
蒸发 | 叶小钗的"3小时蒸发200万"是 AI 项目实战,标题含"蒸发"但内容是技术案例 | 已移除。教训:常见动词在技术文章标题中可能作为场景描述出现,不应拦截 |
过滤词选择原则:
- 只选 95%+ 概率为低质的词(招聘、报名、速递)
- 避免常见通用动词(蒸发、推出、发布、上线)
- 避免 ML/技术标准术语(召回率、模型、训练)
- 高价值作者(如叶小钗)的文章即使标题像新闻也应放行 — 作者风格不等于内容质量
统计输出
extractor 运行后会输出过滤统计到 Summary:
title_filter_skipped (total=12): recruitment=3, event=4, digest=2, emotional_news=3
去重机制(实际有效层 + 失效层)
⚠️ 重要修正(2026-05-20 实测): mark_read API 标记已读后,RSS feed 仍返回该文章,并非有效去重层。真正的去重依赖 url_exists_in_articles() 黑名单检查。
| 层级 | 机制 | 实际效果 |
|---|
| 1 | url_exists_in_articles() 检查 raw/articles/ URL 黑名单 | ✅ 真实去重层 — 所有已入库文章在 extractor 入口即被过滤 |
| 2 | article_exists_in_inbox() 检查同一 cron run 内重复 | ⚠️ 仅防同轮重复,无跨轮效果 |
| 3 | slug 文件名冲突检查 | ⚠️ 标题修改时可能误拦 |
4 | PUT /read?is_read=true | ❌ 失效 — API 返回 200 成功,但 RSS feed 仍返回该 article_id |
实测验证(2026-05-20):
mark_read PUT status: 200, {"code":0,"is_read":true}
标记后 article_id 仍在 RSS feed 中: True
结论:mark_read 调用成功但对 RSS 生成无影响,是假去重保险。实际生产过滤完全依赖 exists_in_articles 黑名单(202/210 篇在入口过滤)。
# 标记已读的 API 调用(成功写入 inbox 后自动执行,但注意:不对 RSS 去重)
PUT /api/v1/wx/articles/{article_id}/read?is_read=true
Authorization: AK-SK xxx:xxx
# ⚠️ 返回 {"code":0,"is_read":true} 但 RSS 仍返回该文章,不可作为去重依据
API 调用中的坑
| 坑 | 说明 | 规避 |
|---|
GET api/v1/wx/articles/{id}?content=true 有认证才返回全文 | 无认证返回 200 但 content: "";有 AK-SK 认证才返回真实 HTML | 必须设置 WERSS_AK + WERSS_SK 环境变量 |
POST refresh 需要 AK-SK 或 Bearer Token | 未认证返回 401 | 设置环境变量 WERSS_AK + WERSS_SK |
| 触发 refresh 后 content 补完可能需要数分钟 | ContentTaskQueue 异步执行,后台用浏览器抓取微信 | 轮询 30s 后标记 pending_refresh,下次 cron 再试 |
AK-SK 认证格式是 AK-SK {ak}:{sk} 而不是 Bearer | 与标准 JWT Bearer 不同 | 环境变量模式下自动切换 |
RSS <id> 标签和 feedparser 的 entry.id 可能不一致 | 部分 feed 的 <id> 是 article_id,部分却是微信 URL | 用原始 XML 解析 <id> 标签,而非依赖 feedparser 的 entry.id 映射 |
mark_read API 对 RSS 去重无效 | PUT 返回 200 success 但 RSS feed 仍返回该 article_id | 不能依赖 mark_read 做去重;raw/articles/ URL 黑名单是唯一真实去重层 |
mark_read 是假保险但必须调用 | 每次成功写入 inbox 后仍应调用 mark_read | 保留此调用——它可能在未来版本的 wechat-mp-rss 中生效,且对其他消费者有意义 |
| raw/articles/ 中的 source_url 独立于 wechat-inbox/ 去重 | inbox 只对自己目录内的文件去重 | 两目录无交叉去重,但通过 source: wechat 标记可区分来源 |
| 微信 HTML 转换质量问题 | html2text 压扁代码块、表格错位、装饰线残留、空图片标记 | convert_html_to_md() 新增多阶段后处理(代码块保护、表格对齐、去装饰、去空图、清理尾空格) |
execute_code 的 read_file + write_file 反复产生双倍行号缓存污染 | 同一 session 内多次调用 read_file 返回缓存行号前缀写入原文 | Skill 脚本的编辑必须用 skill_manage(action='write_file');终端 python3 heredoc 是安全的替代方案 |
| RSS content:encoded 覆盖随时间变化 | 首次扫描 32%,数小时后因后台 ContentTaskQueue 补全升至 78-85% | 无需主动干预,多次 cron 后 RSS 层的 content 覆盖自然提高 |
| GET article detail 返回的 content 字段可能为空 | API 返回 200 但 data.content="",has_content=0 | 先 trigger refresh 再 poll(最多 30s),仍为空则标记 pending_refresh 等下次 cron |
| TinyFish 兜底 |
⚠️ Extractor 标题预过滤规则(2026-07-04 新增)
scripts/wechat-mp-rss-extractor.py 新增 should_skip_article() 函数,在文章写入 inbox 前做标题级预过滤。过滤类别:
| 类别 | 关键词 | 误杀风险 |
|---|
| recruitment | 招聘, 英雄帖, 校招, 招人, 诚聘, 加入我们 | 极低 |
| event | 报名, 专场, 分享会, 论文分享, 交流会 | 极低 |
| digest | 速递, 关键词top, 每周关键词, ai速递 | 极低 |
| emotional_news | 炸崩, 真相来了, 名单曝光, 惊天 | 低 |
| marketing | 纸质版, 劝你别买, 打折 | 低 |
| education_activity | 产教融合, 教育社区 | 低 |
核心原则:宁放勿杀。 2026-07-04 实测 蒸发 误杀了叶小钗的 AI 项目实战文章后已移除该词。每个过滤词必须满足:出现在标题时 >99% 是低质内容。存疑的词不要加,留给 inbox-screener 的 LLM 评分去判断。
| API 404 + DB 有记录但 content_html 为空 | 2026-05-17 09:42 实测:9 篇新文章在 DB 中 has_content=1 但 content_html 为空(仅 7 chars 纯文本),API 返回 404(无论有无认证),ContentTaskQueue refresh 触发后 100s 内仍未补全。根因:文章已被系统标记为「不可抓取」(status=1000),浏览器爬虫无法获取全文。 | 跳过此类文章,不触发 TinyFish 兜底。下次 cron 可能仍无法获取。可通过 docker cp we-mp-rss:/app/data/db.db /tmp/we-mp-rss.db && sqlite3 /tmp/we-mp-rss.db "SELECT id, has_content, length(content), length(content_html), status FROM articles WHERE id='...'" 诊断。 |
| browser 工具无法完成微信扫码登录 | 2026-07-01 实测:mp.weixin.qq.com 使用 iframe 加载扫码登录,browser 工具无法渲染二维码或完成扫码流程。即使用户在其他浏览器完成扫码,browser 会话也是独立的。浏览器工具在微信公众号平台登录场景下不可用 | 使用 Playwright Profile(本地 Chrome)采集,或让用户直接提供已登录后的 URL |
| 完全丢失期处理 | Cookie 过期期间(如 2026-05-10 至 05-19),数据库中完全没有该时段记录(不是有记录无内容,是完全没有)。这与"DB 有记录但未走 pipeline"是两个不同的场景。 | 需手动从公众号历史记录采集:1) 登录 mp.weixin.qq.com → 2) 进入公众号主页查看历史消息 → 3) 复制丢失期文章 URL 列表 → 4) 用 Playwright Profile 或 browser 工具批量抓取。 |
| extractor 文件含双重 frontmatter(2026-07-05 实测) | extractor 写入的 inbox 文件在 process_article() 时生成第一组 frontmatter(含 source: wechat、source_url: 等),同时 RSS content:encoded 的转换正文可能包含自己嵌入的 --- 分隔符和第二个 H1,导致文件含有两套 --- block 和两个 H1。当从 inbox 复制到 raw/articles/ 时,双重 frontmatter 污染 raw 存档。2026-07-05 实测:20KB SkillOpt 文章复制到 raw/articles/ 后需清理第二组 frontmatter。2026-08-02 升级:可能出现三重 frontmatter + 双 H1(extractor frontmatter → RSS 内重复 extractor frontmatter → 嵌入 frontmatter,字段格式还可能不同,如 source_published: 2026年7月27日 18:30 vs 2026-07-27)。split('---') 索引法对三重结构失效(parts[3:] 仍含完整 frontmatter)。可靠做法:找 H1 第二次出现的位置,从那里切片(正文总是从最后一个 H1 开始)。详见 llm-wiki/references/2026-08-02-wechat-triple-frontmatter.md。 | 从 inbox 复制到 raw/articles/ 后必须手动处理:
(1) 删除第二组 --- block:text.replace('\\n---\\nsource: wechat\\nsource_url:', '\\n', 1)
(2) 去重 H1 标题行(两个相同的 # Title 保留一个)
(3) 校验文件开头只有一组 --- 包裹的 YAML frontmatter
也可写一个通用的 clean_raw_article() 函数在入库时自动处理。2026-07-05 实测:修复后文件大小 20KB 保持不变(仅 ~200 字节 frontmatter 重复,不影响内容)。2026-08-02 补充:清理后文件大小大幅缩水是正常现象,不等于内容丢失——sysom 巡检 skill 文章原文件 9460B,清理双重 frontmatter + 双 H1 后 raw 仅 4231B,但实际正文从 3968 chars → 3970 chars(几乎不变)。多出的 ~5200B 全是重复 frontmatter block + 嵌入 URL 长行。验证方法(不要因字节差恐慌):比较「原文件最后一个 # H1 之后的内容」与「清理后 frontmatter 之后的内容」的字符数与关键 probe 字符串(如 memgraph / 19 项巡检 / 12.95 GB 等正文特征词)是否都命中。 |
⚠️ 容器级问题排查指引
we-mp-rss 容器的问题(Cookie 过期、代理配置、Playwright 浏览器、端口映射、双 DB 不同步、cascade 无 worker)请查阅 devops/we-mp-rss-troubleshooting skill,该 skill 覆盖以下故障模式:
| 故障 | 症状 | 修复章节 |
|---|
| Cookie 过期 | RSS 返回旧文章 (5月),cascade timeout | §4, §5, §7 |
| 代理空/指向 127.0.0.1 | sync_time 不更新,ProxyError 日志 | §4 fix 命令 |
端口映射 8001:3000 | curl localhost:8001 返回 Connection reset | §3b |
| BROWSER_TYPE=webkit 导致 Playwright 失败 | 容器 env 中 BROWSER_TYPE=webkit 但只安装了 chromium 浏览器,启动 WebKit 失败 → code_src:None → 二维码截图空 | 改为 BROWSER_TYPE=chromium 后重启容器即可。2026-07-04 实测修复。 |
| 双 DB feeds 不同步 | 新号在列表但 0 文章,cascade 不派发 | §9 |
| Cascade 无 worker | 所有任务 pending,cascade_nodes 为空 | §18 |
| 新号 0 文章(无 cascade 任务) | RSS 空,articles 表无数据 | §19 |
微信文章提取:Plan A → Plan B(v3.0,2026-07-04)
当 extractor 处理 RSS 中无正文的文章时,按以下顺序尝试:
Step 1: RSS content:encoded 已有全文?
├─ 是 → html2text → raw/wechat-inbox/ ✅
│
└─ 否 → Plan A: Chrome CDP(真实登录浏览器)
├─ wechat-cdp-fetch.py → CDP 创建 tab → 导航到文章 URL
│ → 等待 #js_content 渲染 → 提取 innerHTML
│ → html2text → raw/wechat-inbox/
│ → CDP 关闭 tab
│ ⚠️ 2026-07-05~07-22 连续 0%(微信反爬持续封锁 18 天+)
│ 实际成功率约 0%,CDP 实时提取已被微信骨架 HTML 阻塞。
│ 当前内容获取依赖 ContentTaskQueue 异步补齐 RSS content:encoded。
│
└─ Plan A 失败 → Plan B: API/Playwright fallback
├─ POST refresh + poll 3s×2
→ GET /articles/{id}?content=true
→ html2text → raw/wechat-inbox/
⚠️ 成功率 ~0%(容器内 Playwright 被微信反爬)
│
└─ Plan B 也失败 → 占位文件
→ 原文链接 + "未能获取" 标记
关键点
| 事项 | 说明 |
|---|
| cd cdp_ok stats | 提取成功时 STATS["cdp_ok"]++,失败时 cdp_fail++ |
| dry-run | CDP 跳过 dry-run(不占用 Chrome 资源) |
| timeout | CDP 脚本 timeout=25s,超过则 fallback |
| cdp_fail → api | 自动 fallback,日志印 "CDP failed, falling back to API" |
| cdp_content_too_short | CDP 返回 <100 chars(常见 8-77 chars)而非完整正文。比完全失败更微妙——CDP 打开了页面但微信只返回了骨架 HTML。AX tree 可能为空或极简。2026-07-05~07-22 持续 0%:连续多轮实测全部 CDP 尝试 return too short(7月5日 4/4,7月22日 5+/5+),已不是临时波动而是微信反爬的持续封锁。fallback 链自动处理,extractor 写占位文件等待 ContentTaskQueue 异步补齐。 |
| Chrome 焦点抢夺 | Target.createTarget 默认创建前台标签页,Chrome 窗口抢夺焦点打断用户工作。修复:所有 CDP 脚本的 Target.createTarget params 加 "background": True,标签页后台静默创建,不激活窗口。2026-07-07 批量修复 3 个脚本:wechat-cdp-fetch.py、wechat-mp-rss-extractor.py(内联 CDP)、wechat-history-backfill.py(skill 版 + wiki 版各 1 处,共 4 处 patch)。参见 CDP 文档 background 参数。 |
| 无 Chrome | CDP 脚本 exit 1,extractor 直接走 Plan B |
Tier 0: 文章是某第三方项目的再传播?→ 直接去项目源站(2026-06-15 实测验证)
触发条件:
- 标题含第三方产品/项目名(不是原作者作品)
- 截图/封面含项目 logo、URL 水印、或风格名(如 "VOID NODE 92"、"BBS 1992")
- 全文本质是"介绍 + 截图 + 上手指南",而非作者原创分析
行动:
1. 从截图 OCR 或标题提取项目名(例: "奇珍博物馆"、"治AI味")
2. 搜 project.com / 官网域名(优先 .com,常见路径 /docs /zh /mcp /api)
3. 项目源站 > 再传播文章:
- 实时更新(再传播文章可能滞后数月)
- 信息密度更高(无营销/二次解读)
- 提供可操作接口(DESIGN.md 下载、MCP 端点、API key 申请)
2026-06-15 实测:小威Volt《治AI味,奇珍博物馆》→ designbycurio.com/zh/
拿到 DESIGN.md schema + MCP 端点 + 500+ 风格清单,比微信全文信息密度高 5-10x
速度: ~30s(识别项目 + 导航到源站 docs)
Tier 1: SQLite DB 直查
docker cp we-mp-rss:/app/data/db.db /tmp/we-rss-db
sqlite3 ... "SELECT title, content_html FROM articles WHERE url=?"
命中 + has_content=1 → html2text → raw/articles/
速度: ~100ms, 成功率: 100%(已在缓存的)
Tier 2: RSS + API refresh
curl /rss?fakeid=X → POST /refresh → poll 30s → GET /articles/{id}?content=true
速度: 5-30s, 成功率: ≈90%
Tier 3: Playwright Profile(已登录微信的本地 Chrome)
node ~/wiki/scripts/playwright-profile/fetch-with-profile.js "URL" --text
速度: 10-30s, 成功率: 高(需 Profile 已登录微信)
当 Tier 2 触发 refresh 后仍 content 为空时使用
Tier 4: browser 工具(Hermes Browserbase 栈)
browser_navigate(url=URL) → browser_console(expression="document.querySelector('#js_content').innerText")
速度: 5-10s, 成功率: 高(带反检测)
当 Tier 1-3 全部不可用时使用:we-mp-rss 容器 down + curl 直连返回 verify.html
2026-06-13 验证:Claude Code 8 道关卡 CSDN 文章走此 Tier 成功入库
限制: Browserbase 隐式限速(连续调用会触发反检测),单次提取最佳
Tier 5 (最后): 跳过 + 标记
写 log.md 注明"无法获取全文,原因: <X>",等下次 cron 重试
关键:
- 永远不要
curl 直连 mp.weixin.qq.com(302 → verify.html)
- Tier 0 先于其他所有 tier:识别再传播文章 → 直接去源站,能省 80% 时间并拿到更高质量信息
- 优先 Tier 1(DB)——快且无需重新触发爬虫
- Tier 4 是 last-resort 救命稻草——当本地服务全 down 时仍能完成单次入库
- 用户粘贴 URL 快速路径(2026-06-30 修正):当用户直接在聊天中粘贴微信 URL 要求入库时:
Tier 1: SQLite DB 直查(几乎必然 miss,URL 未经过 we-mp-rss 管线)
Tier 2: RSS + API refresh(依赖 we-mp-rss 基础设施,通常 miss)
Tier 3: Playwright Profile ✅ 首选路径
node ~/wiki/scripts/playwright-profile/fetch-with-profile.js "URL" --text
速度: 10-30s, 成功率: 高(需 Profile 已登录微信)
2026-06-30 验证:3/3 篇用户粘贴微信 URL 均成功提取完整正文
Tier 4: browser 工具(Hermes Browserbase 栈,last resort)
browser_navigate → browser_console #js_content innerText
关键修正(2026-06-30):Tier 3(Playwright Profile)不依赖 we-mp-rss 基础设施,本地 Chrome Profile 已登录微信,对用户粘贴的 URL 成功率接近 100%。优先 Tier 3,Tier 4 仅当 Tier 3 失败时使用。2026-06-15 的旧建议(Tier 1 miss → 直接跳 Tier 4)已过时——Tier 3 更快、更可靠、不消耗 Browserbase 配额。
详细见 references/tier0-source-detection.md。
快速路径:直接查 SQLite DB(最新发现)
we-mp-rss 的 DB 已在本地(/app/data/db.db),按 URL 直接查比走 RSS+API 刷新快得多。
import sqlite3, subprocess
# 获取 DB(容器路径 → 本地路径)
subprocess.run(['docker', 'cp', 'we-mp-rss:/app/data/db.db', '/tmp/we-mp-rss.db'], check=True)
conn = sqlite3.connect('/tmp/we-mp-rss.db')
cursor = conn.cursor()
ARTICLE_URL = "https://mp.weixin.qq.com/s/J8IgrW3LgRROLUC8Q5gvZg"
cursor.execute(
"SELECT title, content_html, has_content FROM articles WHERE url = ?",
(ARTICLE_URL,)
)
row = cursor.fetchone()
if row and row[2] == 1: # has_content
title, html_content, _ = row
print(f"Found: {title}, HTML: {len(html_content)} chars")
# html2text 转换后写入 raw articles
else:
print("Not in cache — use RSS+refresh or TinyFish fallback")
DB 关键字段:url、content(纯文本)、content_html(完整 HTML)、has_content(1=有全文)、title、mp_id、publish_time。
性能对比:
| 方式 | 速度 | 成功率 |
|---|
| 直接 SQLite 查询 | ~100ms | 100%(已在缓存的) |
| RSS + API refresh | 5s+ | ~90% |
| TinyFish 兜底 | 慢 | 有失败率 |
适用场景:cron 任务收到微信 URL 后,优先查 DB,命中则直接用本地 content;未命中才走 RSS+refresh 或 TinyFish。
注意:docker cp 会覆盖同名文件,频繁调用时用唯一临时文件名:/tmp/we-mp-rss-{timestamp}.db。
Frontmatter 字段说明
Extractor 写入 raw/wechat-inbox/ 的每个文件包含以下 YAML frontmatter 字段:
| 字段 | 示例 | 说明 |
|---|
source | wechat | 固定值,标识来源为微信 |
source_url | https://mp.weixin.qq.com/s/xxx | 微信文章原文 URL。⚠️ 含 __biz/sn 等 query params,入库前 strip |
ingested | 2026-07-07 | Extractor 写入 inbox 的日期 |
| | |
注意:无 title: 前文字段 | — | 标题不在 YAML 中,在 frontmatter 关闭 --- 后的第一个 # H1 heading 行。下游 prescreen/评分脚本必须从 # 行提取标题,不要在 YAML 中查找 title:。示例:---\nsource: wechat\n...\n---\n\n# 实际文章标题在此。RSS inbox 文件也是同样格式(无 YAML title: 字段)。 |
feed_name | 高可用架构 | 来源公众号名称 |
wechat_mp_fakeid | MP_WXS_3000551159 | 公众号唯一标识 |
source_published | 2026-07-07 | 文章原始发布日期(微信服务器发布日,非 ingest 日期)。这是下游 ingest 脚本和报告查询"原始发布日期"时应读取的字段。不是 publish_date 也不是 date |
sha256 | abc123... | 文件内容的 SHA256 哈希 |
⚠️ 常见陷阱:下游 ingest 脚本 (/tmp/create_entities.py、report scripts) 经常错误地查找 publish_date: 或 date: 字段来报告"原始发布日期"。正确的字段名是 source_published:。2026-07-07 实测:15 篇新入库的 WeChat 文章中 0 篇含有 publish_date 或 date,全部使用 source_published。
快速路径:原生 RSS(AWS 等平台通用)
大型技术博客通常有原生 RSS,优先于任何抓取工具:
# AWS Blog
curl "https://aws.amazon.com/blogs/machine-learning/feed/" -H "User-Agent: Mozilla/5.0"
# 返回 <content:encoded> 44KB+ 完整 HTML,远超 Jina 的 21KB
# 发现方法:URL + /feed/ 或 ?format=rss
通用发现模式:
https://example.com/blog/feed/
https://example.com/rss
https://example.com/blog?format=rss
替代方案:URL 直投入库
对于新关注的公众号或不依赖自动采集的场景,可以直接粘贴微信文章 URL 进行入库,绕过 we-mp-rss 容器。
工作流
| 需求 | 操作 | 路径 |
|---|
| 单篇入库 | 贴 URL → 抓取全文 → raw/articles/ → entity → commit | Tier 3: Playwright Profile(已登录微信的本地 Chrome) |
| 批量加订阅 | 攒一批同一新号的 URL → 一次性处理 | 单篇入库 + 提取 __biz → 注入 feeds 表 |
优势
- 不受容器反爬限制(本地 Chrome 已登录微信,成功率接近 100%)
- 不需要处理 proxy、cascade 调度等容器运维
- 速度 10-30s/篇
限制
- 需要用户主动发 URL(非自动)
- 适合精选入库,不适合全量订阅
推荐分工
- 21 个老号:we-mp-rss 容器自动采集
- 新号:看到好文章贴 URL 即时入库
- 新号全量:攒一批 URL 后一次性批量处理
| 扫描日期 | 公众号数 | 每号篇数 | RSS 有全文 | 覆盖比例 |
|---|
| 2026-05-09 | 20 | 2 | 34/40 | 85% |
| 2026-05-09 (cron) | 20 | 2 | 29/40 | 72% (定时扫描时覆盖率较低) |
| 2026-05-17 | 21 | 10 | 0/210 | 0% (9篇API 404, 1篇slug冲突, 200篇已处理) |
| 2026-05-17 (手动) | 3 | 5 | 1/5 | 1 篇成功 (AGI-之路), 4 篇被删 |
| 2026-05-17 (cron) | 21 | 10 | 0/210 | 0% — 全部 200+ 篇已处理,9 篇 404,1 篇 slug 冲突,2 篇静默跳过(重复标题导致 API 调用被 slug 去重跳过) |
| 2026-05-17 (cron) | 21 | 10 | 0/210 | 0% — 全部 199 篇已处理(exists_in_articles 黑名单过滤),7 篇触发 GET 但被 slug 去重跳过。RSS 持续返回全部条目(mark_read 不影响 RSS,详见去重机制说明) |
| 2026-05-17 (cron) | 21 | 10 | 0/210 | 0% 新篇 — 199 篇已处理,11 篇 RSS 空内容(9 篇新但 API 404 + DB content_html 为空,2 篇 slug 冲突),ContentTaskQueue 未补全 |
| 2026-05-21 (cron) | 21 | 10 | ≈90% | 全量扫描 210 篇:201 篇 exists_in_articles 拦截,1 篇入库(腾讯员工公寓,lifestyle 删除),8 篇 API 404/空。成熟 wiki 正常状态,inbox 保持干净。 |
| 2026-05-22 (cron) | 21 | 10 | ≈90% | 0 新入库——全量扫描 210 篇,202 篇 exists_in_articles 拦截(成熟 wiki 正常状态),8 篇 api_empty。wechat-inbox = 0,candidates.md 全量处理(56 URLs → 5 fetched → 1 ingested)。 |
| 2026-07-01 (补录) | |
成熟 wiki 正常状态(2026-05-20 确认,2026-05-22 验证): 当 wechat-inbox = 0 + RSS GET count 显示大量 exists_in_articles 时,说明 raw/articles/ URL 黑名单过滤在 extractor 入口生效,所有文章都已在之前入库。这是正确的运行状态,不需要任何手动干预。candidates.md 也会在同一次 run 中被完全处理(34 URLs → filtered → 5 ingested → 0 remaining)。
性能: --latest=10 全量扫描 21 个 feed 仅 ~7s(实测 2026-05-17)。
当前状态(2026-05-17 08:19): inbox 199 篇,RSS 持续返回全部条目(mark_read 不影响 RSS,详见去重机制)。
Pipeline 诊断快速参考
分辨真·停摆 vs 假·停滞
当怀疑 pipeline 不更新时:
# Step 1: 查 RSS 端点 — 最快最准
curl -s "http://localhost:8001/rss/MP_WXS_3236757533?limit=3" | grep -o '<pubDate>[^<]*</pubDate>'
# 如果返回当日日期 → pipeline 健康 ✅
# 如果返回旧日期(5天前)→ 真·停摆
# Step 2: RSS 健康但想确认 → 查 extractor 输出
ls -lt ~/wiki/raw/wechat-inbox/*.md 2>/dev/null | head -3
# Step 3: 查容器日志
docker logs we-mp-rss --tail 30 2>&1 | grep -iE "error|token|timeout|200009"
# Step 4: 查 monitor 告警状态
cat ~/.hermes/data/wechat-pipeline-monitor-state.json 2>/dev/null | python3 -m json.tool
| 信号 | 真·停摆(Cookie 过期) | 假·停滞(articles 表滞后) |
|---|
| RSS pubDate | 全部旧日期(5+天前) | 今天日期 ✅ |
| inbox 文件 | 今天无新文件 | 今天有新文件 ✅ |
| 容器日志 | 200009: not found 或 timeout | 无错误,任务正常完成 |
| extractor | rss_empty > 0, 无 cdp_ok | exists_in_articles 大, cdp_ok 正常 |
| 操作 | 重新扫码 + 续期 cookie | 无操作(pipeline 正常) |
常见误解
articles 表 create_time 停止更新 ≠ pipeline 停摆。RSS 端点有独立数据源,extractor 读 RSS 不读 articles 表。
wechat-article-discover 每天一次 ≠ 文章发现每天仅一次。容器内部 cascade 调度(APScheduler,30s 轮询)持续发现文章。
- extractor 超时 ≠ 无新文章。extractor 可能在写文件之后才 hang。kill 前/后必须检查 inbox 文件。
Title Pre-Filter (added 2026-07-04)
A lightweight should_skip_article(title, feed_name) function added to wechat-mp-rss-extractor.py filters out obvious low-quality articles before they enter raw/wechat-inbox/. This saves pipeline LLM scoring resources.
Current filter categories (tightened 2026-07-04)
Design principle: Conservative — better to let one low-quality article through to LLM scoring than block a high-quality one. Removed overly broad keywords after false-positive incidents.
| Category | Keywords | Removed (too broad) |
|---|
| recruitment | 招聘, 英雄帖, 校招, 招人, 诚聘, 加入我们 | 岗位, 年薪 |
| event | 报名, 专场, 分享会, 论文分享, 交流会 | 人才交流 |
| digest | 速递, 关键词top, 每周关键词, ai速递 | — |
| emotional_news | 炸崩, 真相来了, 名单曝光, 惊天 | 蒸发 (叶小钗误杀), 离职潮, 召回率 (ML术语) |
| marketing | 纸质版, 劝你别买, 打折 | 培养计划, 限时, 你值得拥有 |
| education_activity | 产教融合, 教育社区 | 产教 |
Pitfall: 过滤词不能太粗暴 (2026-07-04)
Verified case: "3小时蒸发200万一个AI客服引发的灾难" — title contained 蒸发 (categorized as emotional_news), but article was actually a genuine AI project case study from 叶小钗 (high-value wiki author). The title's emotional framing was clickbait-style, but the content was legitimate engineering practice.
Lesson: Title keyword pre-filters must be conservative. Better to let one low-quality article through to LLM scoring than to block one high-quality article. Single Chinese characters or short substrings that could appear in legitimate technical contexts should NOT be filter keywords.
Removed keywords (after user correction, 2026-07-04 → 2026-07-05):
Removed from batch-ingest domain filter (2026-07-05):
- '突发', '降智', '砍半', '打工版', '复活', '蒸发', '风雨飘摇', '挽救' — user explicitly stated breaking-news and emotional-trigger words should NOT be filtered; many legitimate AI news articles use these headlines
- '震撼', '访谈' — never in active filters but user flagged them as too broad if ever considered
Body-level keyword trap (2026-07-05):
- '来了' — appears in virtually every Chinese tech article body (AI来了, 问题来了, 时代来了). Was in REJECT_BODY_KEYWORDS causing vc=64 (Claude+Qwen MSM) and vc=49 (Om AI) articles to be domain-rejected AFTER LLM scoring. Removed.
- '一键' — appears in "一键三连", "一键部署", "一键生成" etc. Removed.
- Lesson: Body-level negative keywords in Chinese articles are treacherous — single common characters appear everywhere. Prefer title-only checks.
File size threshold: 5KB to 1KB (2026-07-05):
The batch ingest (wechat-batch-ingest.py) auto-deleted any file <5KB before LLM scoring. This killed short-but-dense Chinese AI articles. Threshold lowered to 1KB.
岗位 — too generic ("AI对岗位技能的影响" false positive)
召回率 — ML technical term ("提升RAG系统召回率")
培养计划 — could be legitimate open-source training program
年薪 — could appear in salary analysis
离职潮 — could be legitimate tech industry analysis
限时 — too broad
你值得拥有 — narrow but unnecessary
人才交流 — could be legitimate
Stats output: The extractor now prints title-filter counts per category:
title_filter_skipped (total=12): recruitment=3, event=4, digest=2, emotional_news=3
RSS content 异步补完模型(核心发现)
we-mp-rss 的 RSS content:encoded 不是实时生成的,而是由后台 ContentTaskQueue 异步补齐:
sequenceDiagram
we-mp-rss->>微信: 采集文章(同步)
微信-->>we-mp-rss: 标题+摘要+URL
we-mp-rss->>ContentTaskQueue: 入队任务:抓取全文
Note over ContentTaskQueue: 数分钟~数小时后
ContentTaskQueue->>微信: 登录浏览器抓取全文
微信-->>ContentTaskQueue: HTML content
ContentTaskQueue->>DB: 写入 content:encoded
Note over RSS端点: 此时 GET /rss/{fakeid} 才有全文
对策略的影响(重要):
- RSS content:encoded 覆盖率是随时间增长的,不是固定属性
- 发布后 ~30min: ~32% 有全文
- ~3小时后: ~78%
- ~8小时后: ~85%(趋近终态)
- 已知不可恢复的文章:status=1000 的条目(DB 有记录但 content_html 为空),ContentTaskQueue 补抓后仍为空,属于微信层面的限制,Readability/TinyFish 均无法兜底
脚本不必在首次扫描时就强求全文——错过或空的文章会在下一次 cron 中自然补全。pending_refresh 状态不是失败,是"异步处理中"。