wechat-feeds
Build and maintain a local library of WeChat Official Account (微信公众号) articles. The bundled tool (tool/) logs into the 公众号 admin platform in your own Chrome, then pulls each subscribed account's article list and full text into data/<account>/<date>-<title>.md, with a SQLite index (feeds.db) for querying.
Two things this skill does:
- Set up / maintain the library — install, log in, subscribe to accounts.
- Incrementally update — pull new articles up to now (the
update-feedsworkflow below), including automatic session re-login handling.
How it works (architecture)
CLI (bun, tool/src) ──HTTP──▶ cdp-proxy.mjs ──WebSocket(CDP)──▶ your Chrome (logged into mp.weixin.qq.com)
- The scraper never talks to WeChat directly. It asks the CDP proxy to run
fetch()inside a logged-in mp.weixin.qq.com tab, so every request is same-origin and carries the real login cookie — no separate auth, no easy anti-crawl trip. - Login state is a
token(insession.json) plus the browser's cookie. The token expires often; the cookie lasts a few days.refreshre-derives the token from a still-valid cookie; when the cookie itself dies you re-login in the browser once.
Prerequisites
- Bun (runs the CLI; uses
bun:sqlite) — https://bun.sh - Node.js 22+ or Bun (runs the CDP proxy; needs a global
WebSocket) - Google Chrome you can start with remote debugging
- A WeChat Official Account you can log into
mp.weixin.qq.comwith (any account works — the 图文素材 search reads other public accounts' published articles)
One-time setup
All commands run from the tool/ directory.
cd tool
bun install
# 1. Start Chrome with remote debugging (keep it running).
# macOS:
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222
# Linux: google-chrome --remote-debugging-port=9222
# Windows: chrome.exe --remote-debugging-port=9222
# 2. Start the CDP proxy (separate terminal; keep it running).
node cdp-proxy.mjs # or: bun cdp-proxy.mjs
# 3. Log in — opens the 公众号 login page; scan with WeChat.
bun run src/cli.ts login
# 4. Subscribe to accounts (by name or by a share link to one of their articles).
bun run src/cli.ts add 公众号名称
bun run src/cli.ts add https://mp.weixin.qq.com/s/xxxxx
bun run src/cli.ts list
Optional — wire login-expiry alerts to a channel of your choice (Feishu / Slack / email / desktop). When unset, the alert just prints to the console:
# macOS desktop notification example:
export WECHAT_FEEDS_NOTIFY_CMD='osascript -e "display notification \"$WECHAT_FEEDS_MSG\""'
The update-feeds workflow
Incrementally update all subscribed accounts to now. Run the steps in order; never do anything destructive (don't clear cookies, don't log out).
1. Current time & pre-update baseline
TZ=Asia/Shanghai date '+%Y-%m-%d %H:%M'— note current time.- Latest article date already in the DB (the "before" baseline):
cd tool && sqlite3 -readonly feeds.db "SELECT date(MAX(publish_time),'unixepoch','+8 hours') FROM articles WHERE fetched_at IS NOT NULL;"
2. Check the runtime
- CDP proxy healthy:
curl -s http://localhost:3456/health→ expect"connected":true. If no response, the proxy isn't running — start it (node cdp-proxy.mjs) and make sure Chrome is up with remote debugging. - An mp tab exists:
curl -s http://localhost:3456/targets | grep -o 'mp.weixin.qq.com' | head -1. If empty, open one and wait:curl -s "http://localhost:3456/new?url=https://mp.weixin.qq.com/" ; sleep 6
3. Run the incremental pull (background)
Pulls take a few minutes — run in the background from tool/:
- If the user passed args, pass them straight through:
bun run src/cli.ts pull <args>(e.g.--since=2026-05-01). - Otherwise default to the last ~two weeks (efficient; paging stops automatically once it reaches older articles):
(GNUSINCE=$(TZ=Asia/Shanghai date -v-14d '+%Y-%m-%d'); bun run src/cli.ts pull --since=$SINCEdate:SINCE=$(date -d '14 days ago' '+%Y-%m-%d').)
4. Monitor output; handle re-login (the key step)
Track the background command's output:
- If you see
🔗 登录链接已通知: the browser login has fully expired (the cookie died — happens roughly every few days; normal, not an error). This pull round skips every account. Re-login and re-run, in this order:- Tell the user: open
https://mp.weixin.qq.com/in the CDP-controlled Chrome (the one started with--remote-debugging-port) and log in (the browser usually remembers the account — often no QR scan needed). - After they confirm login, run
bun run src/cli.ts refreshfirst to sync the token (required — pulling with the old token gives200040 invalid csrf token). Wait for✅ 刷新成功. - Then re-run step 3's pull with the same
--sincewindow. This time the session is valid and it pulls in full.
- Tell the user: open
- If no re-login notice appears, the session is valid — just wait for the pull to finish (exit 0). Don't re-trigger pull while it's running.
5. Report
After the pull completes:
- Latest date & total now:
sqlite3 -readonly feeds.db "SELECT date(MAX(publish_time),'unixepoch','+8 hours'), COUNT(*) FROM articles WHERE fetched_at IS NOT NULL;" - Summarize new-article counts per account from the output.
- Report to the user: updated from [before date] to [latest date], N new articles, and list the accounts with the most additions.
Reading the library
- Markdown body:
data/<account nickname>/<YYYY-MM-DD>-<title>.md— YAML frontmatter (title/author/date/url/digest) + Markdown text. - SQLite index (
feeds.db): query what exists, who posted, when, and where the file is.
accounts(fakeid PK, nickname, alias, added_at)
articles(id PK, fakeid FK, title, url, author,
publish_time, -- Unix SECONDS → datetime(publish_time,'unixepoch','+8 hours')
fetched_at, -- Unix MILLIS → datetime(fetched_at/1000,'unixepoch','+8 hours'); NULL = metadata only, body not fetched
file_path)
Use fetched_at IS NOT NULL to mean "body actually fetched" (more reliable than file_path).
CLI reference
bun run src/cli.ts login log into the 公众号 platform (QR)
bun run src/cli.ts add <name|url> subscribe to an account
bun run src/cli.ts list list subscriptions + article counts
bun run src/cli.ts pull pull new articles (default max 100/account)
bun run src/cli.ts pull --today only today's
bun run src/cli.ts pull --date=MM-DD a specific date
bun run src/cli.ts pull --since=YYYY-MM-DD from a date to now
bun run src/cli.ts pull --max=50 cap per account
bun run src/cli.ts pull --concurrency=6 accounts pulled in parallel (default 4; higher = faster but more rate-limit risk)
bun run src/cli.ts refresh re-derive token from a live cookie
bun run src/cli.ts relogin send a re-login alert (cookie fully dead)
bun run src/cli.ts keepalive for a scheduler (cron/launchd): refresh, alert if dead
bun run src/cli.ts status show login status
Notes & red flags
- Rate limits: WeChat's
appmsgpublishendpoint is frequency-controlled. The tool paces itself (per-article + per-page sleeps) and defaults to concurrency 4. Pushing concurrency high risks anti-crawl. If accounts start failing, lower it and re-run. - Accounts >14 days behind may be missed by the default window — pass a wider
--since=YYYY-MM-DD. - Keep the Chrome mp tab open (can be minimized) — every API call runs inside it.
- Nothing secret is committed:
feeds.db,session.json,config.json, anddata/are gitignored. Your subscription list lives infeeds.db, not in source. - Use responsibly: this drives your own logged-in session against WeChat's platform. Respect WeChat's Terms of Service, article copyright, and reasonable request rates. For personal reading/archival of accounts you follow.