用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/QianJinGuo/wiki --skill wechat-mp-interface-restriction-200013命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
基于 SOC 职业分类
正在显示 SKILL.md
| name | wechat-mp-interface-restriction-200013 |
| title | WeChat MP ret=200013 — Interface-Level Restriction Diagnosis & Fix |
| description | Use when WeChat returns ret=200013. Split-test then fix. |
| version | 1.0.0 |
| author | Hermes Agent |
| tags | ["wechat","we-mp-rss",200013,"freq-control","troubleshooting","playwright"] |
Trigger: we-mp-rss collection returns ret=200013 frequencey control on article lists; pipeline stale for days despite valid cookie/session; a parallel agent or past note claims "账号级封禁" (account-level ban).
Same logged-in session, same browser (Chrome CDP on 9222, navigate to mp.weixin.qq.com home first):
# searchbiz = article SEARCH (should be ret:0 if session healthy)
fetch('/cgi-bin/searchbiz?action=search_biz&begin=0&count=5&query=机器&token=<t>&lang=zh_CN&f=json&ajax=1', {credentials:'include'})
# appmsgpublish = article LIST (the gated endpoint)
fetch('/cgi-bin/appmsgpublish?sub=list&sub_action=list_ex&begin=0&count=5&fakeid=<f>&type=101_1&free_publish_type=1&token=<t>&lang=zh_CN&f=json&ajax=1', {credentials:'include'})
| searchbiz | appmsgpublish | Verdict |
|---|---|---|
| ret:0 | ret:200013 | Interface-level tightening — the 2026-07-30 change. Session fine, account fine. |
| ret:200013 | ret:200013 | Account/session-level problem (different track). |
If the account were banned, search would fail too. A probe that only tests the list endpoint (host-direct + proxy + container, all 200013) does NOT prove account ban — it omits the control. Do not accept "账号级封禁" conclusions without the split test.
searchbiz), single-article bodies (/s/xxx), and the MP dashboard keep working; only cross-account article LIST endpoints (appmsgpublish, appmsg) are gated.core/wx/base.py bugs: (a) config user_agent read then overwritten by random.choice(USER_AGENTS) (iPhone UAs → stricter mobile-web controls); (b) fix_header wrote "Refer": url — typo, must be "Referer". Fix both; keep fixed UA from config.
Upstream rachelos/we-mp-rss commit c8ae61e ("1.5.2-Fix") adds core/wx/model/free_publish.py + a _fallback_to_free_publish in web.py probing 4 endpoints in order: free_publish → publish → appmsgpublish → appmsg.
⚠️ PITFALL: upstream c8ae61e ships with GIT CONFLICT MARKERS (<<<<<<< HEAD / ======= / >>>>>>> cadb9035...) inside core/wx/model/web.py — the commit itself is broken and causes SyntaxError on import. Must strip markers (keep BOTH sides: _fallback_to_free_publish AND _set_feed_status) before mounting.
⚠️ PITFALL 2: the 200013 branch never reaches the fallback. The official retry block does if ret==200013: retry; if retries exhausted: break — so free_publish degradation (triggered only in the generic ret != 0 branch) never fires for 200013. Patch: after 3 retries × (60s, 120s, 180s), call self._fallback_to_free_publish(...) and return.
core/wx/model/playwright_mp.py (upstream) launches headless chromium with saved cookies, navigates the MP publish page, and INTERCEPTS the page's own XHR responses (/cgi-bin/) — page-context requests carry full csrf/page state, bypassing the pure-API gate (theory).
To enable:
git checkout upstream/main -- core/wx/model/playwright_mp.py (fetch upstream first: git remote add upstream https://github.com/rachelos/we-mp-rss.git && git fetch upstream main)core/wx/base.py Model(): add elif type in ("playwright","browser"): from core.wx.model.playwright_mp import MpsPlaywrightplaywright_mp.py, web.py, free_publish.py, base.py, config.yaml from host (see Step 4); set GATHER.MODEL=playwrightdocker exec we-mp-rss sh -c "PLAYWRIGHT_DOWNLOAD_HOST=https://npmmirror.com/mirrors/playwright/ /app/env_x86_64/bin/python3 -m playwright install chromium" (image ships the python pkg but NOT browser binaries; ~104 MiB)⚠️ PITFALL 3: upstream playwright_mp.py navigates to a DEAD page. Its MP_PUBLISH_URL constant and the inline published_url both use cgi-bin/appms?t=media/appmsg_list_v2 — that page is 404 since the 2026-07-30 change (probe: HTTP 404, 39-byte body). Patch both to cgi-bin/appmsgpublish?sub=list&sub_action=list_ex&...&type=101_1&free_publish_type=1 (probe: HTTP 200, ~15.7KB).
⚠️ PITFALL 4 (FINAL VERDICT): even with the correct page, Playwright does NOT recover the list. Verified 08-12: chromium launches, cookies load, appmsgpublish page returns HTTP 200 — but the page fires ZERO /cgi-bin/ XHR requests (response-intercept probe shows nothing). The page JS detects the server-side gate and does not render the list at all. Playwright logs 未拦截到 API 响应...未获取到数据, 0 articles. Conclusion: the interface gate is enforced server-side at the page level too — NOT locally fixable; leave collection paused and wait for WeChat to lift it.
In-container sed/file edits are LOST on docker compose up --force-recreate. To persist:
volumes:
- /Users/jinguo/data:/app/data
- /Users/jinguo/projects/we-mp-rss/main.py:/app/main.py
- /Users/jinguo/projects/we-mp-rss/jobs/mps.py:/app/jobs/mps.py
- /Users/jinguo/projects/we-mp-rss/core/wx/base.py:/app/core/wx/base.py
- /Users/jinguo/projects/we-mp-rss/core/wx/model/web.py:/app/core/wx/model/web.py
- /Users/jinguo/projects/we-mp-rss/core/wx/model/free_publish.py:/app/core/wx/model/free_publish.py
- /Users/jinguo/projects/we-mp-rss/core/wx/model/playwright_mp.py:/app/core/wx/model/playwright_mp.py
- /Users/jinguo/projects/we-mp-rss/config.yaml:/app/config.yaml
docker cp we-mp-rss:/app/config.yaml ./config.yaml then edit host copy (enable_job: True, gather.model: playwright).config.yaml enable_job is hardcoded (enable_job: False), NOT ${ENABLE_JOB:-...} — the compose SERVER_ENABLE_JOB env var does nothing for it. Edit the file.enable_job False → start_job() never registers. Startup log must say 已开启定时任务 (not 未开启定时任务). The cascade dispatch job (创建待认领任务...没有在线子节点) is a red herring — it has always dispatched with 0 child nodes; the real collector is the 定时采集 job.While waiting out the gate, collection MUST be truly stopped — every probe/request resets the cooldown window. The authoritative stop is the DB task status, not env vars or cron:
-- inside container: UPDATE message_tasks SET status=0 WHERE id='5da2b787-b861-4782-b8dc-fd9518805827';
-- then docker restart; startup log MUST show "没有任务" (scheduler loads no collection job)
Resume: UPDATE message_tasks SET status=1 ... + restart (startup log shows the job again).
enable_job: False, changing cron, or pausing the discover cron do NOT reliably stop the container-internal "全量公众号定时采集" job.docker logs we-mp-rss --since <restart> | grep -icE 'appmsgpublish|frequencey|Task started' → must be 0.任务进度 21/21 at 00:00/06:00/12:00). Never trust another agent's summary — check the container log's Task started / frequencey counts yourself.wechat-cookie-renew cron (09:00) RUNNING while blocked: it only renews the session (does NOT probe list APIs), so the session stays fresh for when the gate lifts.wechat-monitor alert (366h → grows) is the unblock signal; it stops when DB max(create_time) advances.While the list APIs stay gated, content flow CAN be restored without touching the account: wechat2rss.xlab.app (github.com/ttttmr/Wechat2RSS, 1.5k★) runs its OWN WeChat collection and publishes RSS for 395+ public accounts. Zero dependency on the user's account → zero risk of resetting the cooldown window. This is the concrete "恢复采集" answer (user rejected Sogou 方案A, but this is a different path — third-party RSS mirror, not Sogou scraping).
Coverage check (30 user feeds → 11 matched): 机器之心/量子位/新智元/腾讯技术工程/阿里技术/字节跳动技术团队/PaperWeekly/美团技术团队/阿里云开发者/夕小瑶科技说/小米技术. Full list: curl -sL -x http://127.0.0.1:10808 https://wechat2rss.xlab.app/list/all/ then regex href="(https://wechat2rss\.xlab\.app/feed/[^"]+\.xml)"[^>]*>([^<]{2,30})</a> (note: hrefs are FULL URLs ending .xml — a bare /feed/ regex matches nothing).
Integration into the wiki RSS pipeline:
~/.blogwatcher-cli/blogwatcher-cli.db, blogs table: id,name,url,feed_url,scrape_selector,last_scanned): blogwatcher-cli add <name> <url> --feed-url <feed> dedups by URL domain — all feeds share https://wechat2rss.xlab.app so only the FIRST add succeeds; insert the rest via SQLite with a unique url (use feed.replace('.xml','')) and feed_url set.rss-inbox-recovery.py GOOD_FEEDS (skill script, the main capture path): append the 11 entries with WeChat- prefixed names. Python urllib reads HTTPS_PROXY automatically — no code change for proxy.HTTPS_PROXY=http://127.0.0.1:10808 blogwatcher-cli scan "WeChat-xxx" --unsafe-client.⚠️ blogwatcher SSRF guard blocks loopback proxies: blogwatcher-cli scan without --unsafe-client fails with response filter round trip failed: proxyconnect ... "127.0.0.1" address is loopback. The flag is MANDATORY for these feeds. --unsafe-client is per-invocation (not persisted), so the cron prompt must carry it.
Verified end-to-end: scan → New=20×11 → recovery → 220 files into raw/rss-inbox/ (129 ≥9KB full-text) → wechat-inbox-pipeline (hourly) auto-scores/ingests. Feed freshness: RSS pubDates are current (w2rss claims ~6h avg latency).
When the gate lifts: the mirror can stay as a redundant channel (harmless) or be removed; the native account path resumes via message_tasks SET status=1 + restart.
Ad-hoc verification script (create in temp, run, delete) with AST static assertions + container import probe:
<<<<<<</>>>>>>> markersret==200013 branch contains _fallback_to_free_publish call_fallback_to_free_publish / _set_feed_status / MpsPlaywright / get_Articles methods presentGATHER.MODEL=playwright + config model: playwright presentdocker exec import of MpsPlaywright succeeds AND container file == host file (mount actually live)mp.weixin.qq.com/s/xxx links) keeps the wiki pipeline alive.When an agent cron that should be draining rss-inbox/ shows running in executions.db but nothing progresses for 30+ min:
# 1. No child process = the agent died mid-run (executions.db status stays 'running' forever)
ps aux | grep -E 'cron|<job_id>' | grep -v grep # empty = dead
# 2. Inbox files not being consumed (no recent mtime) = no progress
find raw/rss-inbox -name '*.md' -newermt '5 min ago' | wc -l # 0 = stalled
# 3. Fix: re-fire manually — cronjob run <job_id>
Partial consumption is expected (step0/quick-classify delete skip/reject files with os.remove; many vendor WeChat articles are deterministic skips by design). A zombie round that consumed files WITHOUT ingesting means those files were classified skip/reject — re-running processes the remainder; log.md entry count is the ingest truth, not inbox count.
Do NOT trust another agent's status claims (08-13: an alert email claimed "零采集请求" while the 6-hourly job was still hammering WeChat — verify via container logs Task started/frequencey counts; same for zombie "running" states — verify via ps + file mtimes).
WeChat MP web sessions expire naturally every ~2 days. When the Chrome profile session dies (cookie-renew logs "WeChat 尚未登录!需要扫码一次"; Chrome has only article tabs with no token, or zero mp.weixin.qq.com tabs remain), every API probe returns ret=200003 invalid session — INCLUDING searchbiz. This is a SESSION problem, not a ban signal. Probing "is the 200013 ban lifted?" with a dead session is meaningless: 200003 masks the real state, and both unblock-probe and manual probes silently degrade into session-freshness checks.
Correct sequence: re-scan QR → cookie-renew → THEN probe. A 200013 on a VALID session (token present in Chrome home URL) is the only genuine ban reading. Also watch for restriction escalation: searchbiz flipped from ret:0 (working, 08-13 morning) to ret:200003 (same day) as the account-level restriction widened — a previously-working auxiliary endpoint flipping to invalid-session on a live session means the restriction is spreading, not lifting.
unblock-probe reads token/cookie from the container's driver.token store (wx.lic). Its cookie reconstruction loses page/session context, so appmsgpublish can return ret=200003 invalid session there even when the browser session is healthy (home-URL token present, searchbiz ret:0). A 200003 from the probe therefore does NOT prove "session dead" — it can mean only "the probe's reconstructed cookie context is incomplete." Don't conclude a 200013 lift (or a dead session) from the probe alone.
To get the TRUE ban status, run the split test inside the real Chrome PAGE context (same origin, fetch(...,{credentials:'include'})), not via driver.token requests: navigate a 9222 page to https://mp.weixin.qq.com/ (token is in the home URL), then fetch both endpoints from that page:
searchbiz → ret:0 = session validappmsgpublish → ret:200013 = gate STILL up (NOT unblocked); ret:0 = unblockedVerdict table (searchbiz / appmsgpublish): 0/0 = unblocked; 0/200013 = interface gate still up (keep collection paused); 200003/anything = session genuinely dead → re-scan QR first, then re-run.
Reusable probe: scripts/wechat-unblock-split-test.py (CDP on 9222, prints searchbiz_ret + appmsgpublish_ret + verdict). Verified 08-18: a session the probe saw as 200003 was actually healthy in page context (searchbiz 0), and appmsgpublish returned 200013 freq control = gate STILL up — so a fresh QR/cookie renew does NOT lift an interface-level gate; ~30d tier expected (07-29 + 30d ≈ 08-28).
Implemented as ~/.hermes/scripts/wechat-unblock-probe.py (no_agent cron wechat-unblock-probe 9d9ed43f69b4, daily 09:20 — after cookie-renew at 09:00 so the session is fresh):
UPDATE message_tasks SET status=1 + container restart + hermes cron resume f069a4169bcc (discover) + email via the wechat-monitor SMTP fallback chain.Cookie: header makes requests raise 'latin-1' codec can't encode; parse into a dict and pass via cookies= instead.