clip family'sOfficial Account batch member: unlike soia-pkm-clip-wechat-article(single-article, paste-URL archiving), this skill targets the batch scenario of "pulling my own Official Account's historical articles into the vault all at once". It is only for archivingthat you manage yourselfOfficial Accounts—it is not a general-purpose Official Account crawler.
Customer-readable description
What this skill can do
Batch-archive articles already published on a WeChat Official Account managed by the user into an Obsidian vault. Supports three routes: the official API, the WeChat Official Account backend interface, and logged-in-session Cookie, and deduplicates by URL
| What the customer wants | What the skill does | What the customer can see |
|---|---|---|
| Complete the work covered by this skill | Read the user request, necessary context, and this skill's body workflow, and execute the minimal reliable steps | The customer will see Obsidian/vault file changes, terminal logs, generated artifact paths, and a final receipt. |
| Missing dependencies, permissions, configuration, or key | Stop actions that require external state and clearly state what is missing | Installation commands, application addresses, configuration paths, or questions requiring customer confirmation |
| Execution completed | Summarize successes, skips, failures, file changes, and verification results | A completion receipt that can be copied into a ticket/log |
How the customer uses it
- State the goal in natural language and provide the necessary inputs: files, URLs, repos, workspaces, proposals, vaults, or platform account status.
- The Agent first determines whether this skill is triggered, then checks dependencies, configuration, permissions, and risky actions.
- For actions that can be dry-run or previewed, provide a preview first; when deletion, overwriting, sending, publishing, or writing remote state is involved, seek customer confirmation first.
- After execution, verify the real output; do not substitute "looks successful" for evidence.
- The final reply must give the customer a visible summary: what was done, a log summary, file changes, issues, and next steps.
Dependencies and installation
Install this skill (a single skill):
npx skills add soia-team/soia-open-skills -g -a '*' -s soia-pkm-clip-wechat-account -y
Configuration conventions:
~/.config/soia-skills/soia-open-skills/soia-pkm/soia-pkm-clip-wechat-account/config.yml
SOIA_PKM_CLIP_WECHAT_ACCOUNT_CONFIG_FILE=<custom-config-path>
# 兼容别名:$SOIA_PKM_CLIP_GZH_CONFIG_FILE(以及原有 $SOIA_PKM_CLIP_GZH_ENV_FILE)
- If this skill does not require private configuration, you may skip creating
config.yml。 - If an API key, cookie, session, provider home, or local path is required, it may only be placed in a private
config.yml, the process environment, or the provider's own login state; it must not be written into the repository, vault body, or logs. - Strong dependencies, optional dependencies, and third-party skill relationships must be governed by this
SKILL.mdsubsequent "dependencies / prerequisites / resources / boundaries" description; if it is not clearly written, first supplement the description or ask the customer—do not guess. - A third-party skill may only declare dependencies and installation methods; it must not directly modify third-party skill files.
Logs and completion receipt
Every execution must let the customer see the process and results. Minimum receipt format:
完成:<一句话说明本次完成了什么>。
日志摘要:
- started: <检查到的输入/配置/依赖,不打印秘密值>
- processed: <数量或范围>
- created/updated: <数量或路径>
- skipped/failed: <数量和原因>
文件变化:
- <绝对路径或“未改动文件”>
验证:
- <运行过的检查、命令或人工核对点>
问题与下一步:
- <缺 key / 缺依赖 / 需要客户确认 / 建议下一条命令;没有则写“无”>
Delivery order: batch archiving must first write all articles to disk, then uniformly output the above receipt; it must not be reversed;author rely on --account-name fallback,content_complete: false and other uncertain fields must be explicitly marked as "unverified" in the receipt; do not fabricate.
How to choose among the three routes
| Route A · Official API | Route C · Official Account backend interface | Route B · Logged-in Cookie | |
|---|---|---|---|
| Coverage | Only articles sent via the **Drafts "Publish"** | All history, including old articles sent manually via mass send | All history, including old articles sent manually via mass send |
| Account requirements | Requires a verified service account/subscription account | Any Official Account, as long as you can log in to the backend | Any Official Account, as long as you can log in to the backend |
| Credentials | AppID/AppSecret, valid long-term + IP whitelist | token/Cookie, expires in a few hours | key/pass_ticket/appmsg_token, expires in a few hours to a few days |
| Difficulty of obtaining credentials | One-time configuration (generated in the backend) | Capture 1 pair of values (token+Cookie) | Capture 3 tickets + Cookie; easy to miss one or mismatch the pair |
| Degree of officialness | Documented at developers.weixin.qq.com | Unofficial, community reverse-engineered (source-level verification) | Unofficial, community reverse-engineered, not found in official documentation |
| Script | scripts/fetch_api.py |
scripts/fetch_mp.py |
scripts/fetch_cookie.py |
Recommendation: first run Route A to scout (the one-time cost of setting up credentials is low, and they can be reused long-term for incremental sync; most personal unverified accounts will hit the permission threshold directly, see below). To read all history (including old articles sent manually via mass send),prefer Route C—the credentials only require capturing one pair of token/Cookie, which is less error-prone than Route B requiring three tickets to be paired, and is the recommended route in this skill for reading historical articles of your own account. Route B is kept as a fallback when Route C fails (for example, if both interfaces are revised someday). All three routes only run on accounts you can log in to/manage yourself; when accounts that have been run are landed, they will be automatically deduplicated by url (skipping articles already archived by each other).
Route C is further divided into two sub-paths internally; the script automatically selects based on whether you pass --name/--fakeid :If not passed (default), use appmsgpublish to read the account you are currently logged in to; if passed, --name/--fakeid only then use searchbiz+appmsg to read a specified/someone else's account—see the Route C subsection below for the reason.
Pre-execution confirmation and authorization
Confirmation strategy
Before formally scraping the body/writing files, first use a small sample such as --dry-run --limit 3 to scout, and report to the client the Official Accounts, order of magnitude of article count, and time range expected to be processed this time; after the client confirms the scope is correct, then open up full scraping. Explicitly invoking this skill and passing parameters such as --name/--limit are only recommended inputs and do not constitute a reason to skip this confirmation—the only skip condition is that the client's current sentence explicitly says "scrape directly/no need to confirm"; after skipping, the receipt must state the scope assumptions used this time.
Pre-use confirmation (consent)
Routes B / C go through the Official Account backendunofficial interfaces, which carries ToS risk. On first use of this skill, or when the version declared in this section changes, the following statement must first be shown and the client's explicit consent obtained before continuing:
Some routes of this skill (Route B / Route C) use unofficial interfaces of the WeChat Official Account backend, which are not documented in the official WeChat documentation and carry ToS risk; they are limited to archiving accounts you manage yourself, and must not be used to scrape other people's Official Accounts. (Disclaimer version v1)
- After the customer agrees, write
{accepted: true, acceptedAt: <ISO 时间戳>, disclaimerVersion: "v1"}intoconsent.jsonunder the private configuration directory (at the same level asconfig.yml:~/.config/soia-skills/soia-open-skills/soia-pkm/soia-pkm-clip-wechat-account/consent.json)。 - already exists and
disclaimerVersionmatches the current version (v1) → skip the display, continue directly, and merely mention in the log "Terms of Use confirmed (<acceptedAt>)". - Customer refuses → stop, do not perform any scraping action.
- Route A (official API) uses interfaces documented in the official documentation and is not subject to this consent constraint.
Route A · Official API
Interface basis (verified 2026-07 on developers.weixin.qq.com + WeChat Open Community, field names have been written into the script comments)
| Interface | Method | Request | Key response fields | Basis |
|---|---|---|---|---|
| Get access_token | GET | /cgi-bin/token?grant_type=client_credential&appid=&secret= |
access_token, expires_in |
Official documentation(complete request/response examples obtained) |
| Get successful publish list | POST | /cgi-bin/freepublish/batchget?access_token=,body {offset,count,no_content} |
total_count,item_count,item[].article_id,item[].update_time,item[].content.news_item[].{title,author,digest,content,content_source_url,thumb_media_id,show_cover_pic,need_open_comment,only_fans_can_comment,url,is_deleted} |
Official documentation(complete example body could not be obtained during scraping; field names have been cross-checked and found consistent with multiple independent posts on the WeChat Open Community —suggest first using --dry-run --limit 3 to calibrate with a real test once) |
| Get permanent material list | POST | /cgi-bin/material/batchget_material?access_token=,body {type:"news",offset,count}(count officially specified as 1-20) |
total_count,item_count,item[].media_id,item[].update_time,item[].content.news_item[].{title,thumb_media_id,show_cover_pic,author,digest,content,url,content_source_url} |
Official documentation(complete request/response examples obtained) |
⚠️ Coverage limitation (be sure to read first)
freepublish/batchgetOnly returns articles sent via the draft box's "publish" action. Content that only went through "mass send" but was never clicked to "publish", as well as old-version graphic messages from before the draft box feature went live, currently officiallyhave no API at all that can retrieve them — this is not an implementation flaw of this skill, but a platform limitation cross-confirmed by multiple posts on the WeChat Open Community.material/batchget_material(type=news) returns permanent graphic materials; according to community feedback, once an article is officially "published" via the draft box, it may disappear from this material list. The two interfaces are in acomplementary relationship, and even taking the union does not equal "the entire published history".- The "Publishing Capabilities" related interface documentation states:As of 2025-07, the calling permissions for these interfaces will be revoked for personal entity accounts, uncertified enterprise entity accounts, and accounts that do not support certification. Most personal subscription accounts will most likely fail the entire route, and a certified service account/subscription account is required (the same account threshold as
soia-pkm-publish-wechat-draft'sdraft/add). - Conclusion: Route A is better suited for "spot-checking/verifying/incrementally syncing already-published content"; do not expect it to be a "full backup". To read the entire history (including old articles manually mass-sent), prefer Route C, with Route B as a fallback.
Credentials and restrictions
WECHAT_APP_ID/WECHAT_APP_SECRET: The acquisition path and IP whitelist configuration are the same assoia-pkm-publish-wechat-draftSKILL.md's section "How to obtain and configure the WeChat Official Account AppID / AppSecret", and are not repeated here — the two skills use the same set of Official Account developer credentials.- Private configuration auto-detection:
$SOIA_PKM_CLIP_WECHAT_ACCOUNT_CONFIG_FILE(compatible alias$SOIA_PKM_CLIP_GZH_CONFIG_FILE, as well as the original$SOIA_PKM_CLIP_GZH_ENV_FILE)>~/.config/soia-skills/soia-open-skills/soia-pkm/soia-pkm-clip-wechat-account/config.yml. The configuration file uses YAMLenv:mapping, see examples inconfig.example.yml; keys do not go into the vault, nor into this open-source skill repository.
Usage
python3 scripts/fetch_api.py [--out <vault内相对目录>] [--limit N] [--dry-run] \
[--vault <path>] [--account-name <公众号显示名>] [--force] [--page-size 20]
--dry-run: only fetch the list and print it, without writing files —for the first run, a dry-run is strongly recommended, to verify thattotal_count/item_countand the number of articles retrieved match expectations (especially since thefreepublish/batchgetfield has not been 100% verified against the official example body).--account-name: neither interface returns the official account nickname; use it for file names/source information; if not provided, "公众号" is used as a placeholder in the file name.--force: articles already archived underurlare skipped by default; only with this added will they be overwritten and rewritten.
Path C · Official account backend interface (recommended: read all history of your own account)
Reuses the mp.weixin.qq.com backend(author logs into the backend, not the reader-side profile_ext) interface; like Path B, it can read all history (including old articles sent manually via mass messaging), but the credentials only require capturing a pair of token/Cookie, which is less error-prone than Path B requiring a matching set of three tickets (key/pass_ticket/appmsg_token).
Path C internally has two sub-paths,scripts/fetch_mp.py automatically switches based on whether you pass --name/--fakeid :
Default (do not pass --name/--fakeid) |
pass --name or --fakeid |
|
|---|---|---|
| Interface | appmsgpublish(author-side "Publish Records") |
searchbiz(name → fakeid) + appmsg?action=list_ex(fakeid → list) |
| Whose account is read | The account you are currently logged into, no need to specify name/fakeid | The account corresponding to the specified name/fakeid (can be another account you also manage) |
| Applicable scenarios | Most common scenario: you just want to back up your own account | You are logged into the backend with one account, but want to read another account you also manage and whose name/fakeid you know |
| Verification status | Tested: ran successfully on a real account on 2026-07-09,ret=0,total_count=152 |
source-level verification, no official documentation found, marked "pending user testing calibration" |
Why the default path does not use searchbiz:searchbiz is a fuzzy search of the official account list by name, designed for "finding someone else's account"; searching with your own official account's name often fails to find yourself (returns an empty list), so it is not suitable as the default path for "I am already logged in, I am this account".appmsgpublish directly reads the publish records of the account corresponding to the current login state, so this problem does not exist.
Interface basis (source-level verification + appmsgpublish already tested)
| Interface | Method | Request | Key response fields | Basis |
|---|---|---|---|---|
| appmsgpublish (default, reads your own account) | GET | /cgi-bin/appmsgpublish,params sub=list&begin=<0,20,40,...>&count=20&type=101_1&free_publish_type=1&sub_action=list_ex&token=&lang=zh_CN&f=json&ajax=1 |
base_resp.ret;publish_page(JSON string,json.loads followed by {total_count, publish_list[]}); each publish_list[].publish_info(is another layer of JSON string),json.loads followed by {appmsgex[]}, each appmsgex[] contains title/link/update_time/digest |
cv-cat/WechatOAApis — utils/wx_utils.py source-level verification,and verified by the user on a real account on 2026-07-09 |
| searchbiz (name → fakeid) | GET | /cgi-bin/searchbiz,params action=search_biz&begin=0&count=5&query=<公众号名>&token=&lang=zh_CN&f=json&ajax=1 |
base_resp.ret,list[].{alias,fakeid,nickname,round_head_img,service_type} |
wnma3mz/wechat_articles_spider — ArticlesUrls.py::official_info() and cv-cat/WechatOAApis — utils/wx_utils.py::get_fakeid_params() two independent repositories with completely identical parameters |
| appmsg (fakeid → article list) | GET | /cgi-bin/appmsg,params action=list_ex&begin=<0,5,10,...>&count=5&fakeid=<fakeid>&type=9&query=&token=&lang=zh_CN&f=json&ajax=1 |
base_resp.ret,app_msg_cnt,app_msg_list[].{aid,appmsgid,cover,digest,itemidx,link,title,update_time}(does not include the author field) |
wnma3mz/wechat_articles_spider — ArticlesUrls.py::__get_articles_data() |
appmsgpublish and appmsg?action=list_ex are not the same interface; both the response structure and the pagination step differ—they should be interfaces called by the old and new versions of the mp backend frontend respectively, and the script has two independent implementations (list_own_account() vs fetch_article_list()), which cannot be mixed.
⚠️ Limitations and risks (read first)
-
Unofficial interface: like route B, it has no official documentation backing it and may be adjusted at any time.
appmsgpublishAlthough it has been tested and passed, it is likewise not an interface included in the official documentation. -
Only for archiving official accounts that you manage yourself:
--nameAfter a hit, it will print nickname/fakeid and prompt for confirmation; if it is not your own account, press Ctrl+C to interrupt; the default path reads the currently logged-in account itself, so there is no risk of mistaking the account. -
token/Cookie will expire: community reports usually expire within a few hours, there is no refresh mechanism, and after expiration
base_resp.retwill return non-0 (the script gives targeted prompts according to the table below; the two subpaths share the same set of prompts, cross-checked by community reverse engineering, no official documentation found, and still subject to "pending user calibration by actual testing"):retMeaning Handling 200003invalid session token/cookie has expired; log in to the backend again, capture packets, and replace 200013freq control rate limiting triggered; community reports usually require waiting a long time, do not rerun immediately 200040invalid csrf token token and Cookie are suspected to have been captured from different login sessions -
Neither subpath list interface returns the author field: when landing,
authoruses--account-name/WECHAT_ACCOUNT_NAMEas a fallback, which is not guaranteed to equal the article's real byline author; for the exact author, manually check the original article page. -
Rate limiting risk: by default the script sets the request interval per page/per article to
--sleep 3seconds; community reports say that even with throttling of 5 seconds/page, scraping nearly a thousand articles may still triggerfreq control; for batch scraping a full year of history, it is recommended to use--limitto run in batches.
Credential acquisition
Log in to mp.weixin.qq.com backend, open any article edit page or article list page, press F12 to open the browser developer tools "Network" panel, and from theaddress bar URL copy the token parameter, and from the request headers copy the complete Cookie string. Fill it in according to config.example.yml into the private config.yml of env.WECHAT_MP_TOKEN / env.WECHAT_MP_COOKIE。
⚠️ Note token you need to take the one in the address bar URL (the numeric string),notthe ticket whose request parameter name in the "Network" panel is appmsg_token —the two are different tickets, andappmsgpublish recognizes the address bar token。
Usage
# 默认:不传 --name/--fakeid,读你自己当前登录的账号(appmsgpublish,推荐)
python3 scripts/fetch_mp.py [--out <目录>] [--limit N] [--dry-run] \
[--vault <path>] [--account-name <显示名>] [--force] [--sleep 3] [--page-size 20]
# 传 --name 或 --fakeid:读指定/别人的号(searchbiz + appmsg?action=list_ex)
python3 scripts/fetch_mp.py --name <公众号名> [--out <目录>] [--limit N] [--dry-run] \
[--vault <path>] [--account-name <显示名>] [--force] [--sleep 3] [--page-size 5]
- Do not pass
--name/--fakeid(default, recommended): reads your own currently logged-in account, usingappmsgpublish,--page-sizedefault 20. - Pass
--nameand--fakeid(choose one of the two): usessearchbiz+appmsg?action=list_ex,--page-sizedefault 5;--namewill first runsearchbizsearch and take the first matching result; whenfakeidis known, directly passing--fakeidto skip the search step is more reliable. --dry-run: only paginate to pull the list and print title+link,does not fetch the body and does not write files——Faster than Route B's dry-run (Route B's dry-run still fetches all article bodies). For the first run, it's recommended to do a dry-run first to verify whether the article count and titles match expectations.--page-sizeWhen not passed, the default value is automatically taken according to the subpath used (20 for your own account / 5 for a specified account). Both are stable values that have been verified/community-tested; do not increase them.- After each list item gets
link, the script directly GETs the article's public page and parses#js_content(the same article body fetching approach as Route B /soia-pkm-clip-wechat-articlesingle-article archiving; no additional cookie is needed).
Route B · Logged-in Cookie
API basis (unofficial, community reverse engineering, cross-checked)
GET https://mp.weixin.qq.com/mp/profile_ext
?action=getmsg&__biz=<biz>&f=json&offset=<offset>&count=10&is_ok=1
&scene=124&uin=<uin,通常固定777>&key=<key>&pass_ticket=<pass_ticket>
&wxtoken=&appmsg_token=<appmsg_token>&x5=0
Header: Cookie: <登录态 Cookie>
响应:{"ret":0,"errmsg":"ok","msg_count":N,"can_msg_continue":0|1,
"general_msg_list":"<JSON 字符串,需二次 json.loads>","next_offset":N,...}
general_msg_list 解析后:
{"list":[{"comm_msg_info":{"datetime":<unix ts>},
"app_msg_ext_info":{"title","author","content_url","is_multi",
"multi_app_msg_item_list":[同字段的合集子项]}}]}
Basis sources: cross-checked reverse-engineering records of the same API from multiple independent technical blogs (Cnblogs/CSDN/Zhihu, 2019-2024). The field names are consistent with each other, butnone are official documentation, and developers.weixin.qq.com does not include this API. The script has been implemented accordingly and this has been written into the code comments, but it is still marked as "pending user real-world calibration"—on the first run, be sure to --dry-run --limit 3。
Note: the original requirement given to the user included a reference implementation link (
zjp1997720/wechat-article-search); in actual testing, that project actually calls Sogou WeChat Search (weixin.sogou.com), and does not use the technical route of fetchingprofile_extwith cookies. It does not match the goal of "reading the full history," so Route B did not copy it and instead switched to the aboveprofile_extsolution that has been independently reproduced by multiple parties.
⚠️ Limitations and risks (be sure to read first)
- Unofficial API: There is no official documentation backing it, and WeChat may adjust the implementation at any time and make it stop working; if problems occur, there is no official support channel.
- Only for archiving official accounts that you manage yourself: Multiple reference materials suggest that this kind of reverse-engineered call "should be used only for learning and research purposes." Scraping the historical articles of someone else's official account may violate WeChat's Terms of Service. This script is designed as self-service backup for your own account; do not use it to scrape other people's accounts.
- Tickets expire:
key/pass_ticket/appmsg_tokenis a session ticket. Community reports range from a few hours to a few days; there is no refresh mechanism. After it expires, if an error occurs, you must log in again, capture packets, and replace it. - Rate-limiting risk: By default, the script waits
--sleep 1.5seconds between requests; do not set it too fast. For batch scraping a full year of history, it is recommended to run it in batches (with--limit), and do not pull hundreds of articles at once.
Credential acquisition
Log in to mp.weixin.qq.com backend, open any historical article of that official account (or profile_ext?action=home), find same-domain requests in the browser developer tools "Network" panel, copy __biz/key/pass_ticket/appmsg_tokenfrom the request URL, and copy the complete Cookie string from the request headers. Fill it into the private config.example.yml according to config.yml。
Usage
python3 scripts/fetch_cookie.py --biz <__biz> [--out <目录>] [--limit N] [--dry-run] \
[--vault <path>] [--account-name <显示名>] [--force] [--sleep 1.5]
- The list API returns 10 items per page (the community-tested upper limit). The script automatically pages by
can_msg_continue/next_offset. - After each list item gets
content_url, the script directly GETs the article's public page and parses#js_content(the same article body fetching approach assoia-pkm-clip-wechat-articlesingle-article archiving; no additional cookie is needed). --dry-runwill still page through the entire list, fetch all article bodies, and then print (without writing files). When the volume is large, it takes a long time—if you want to quickly verify the list itself, first look at the per-page output printed to stderr拿到 N 条,累计 M。
Implementation specification (shared by all three routes)
- Transit area, not the endpoint: what lands in the vaultInbox layer, not the final location after
soia-pkm-organize-article-mocis put in place—organizestockpile it here before going live, then relocate it to the 40/50 zone together with the "works library refactor". - Default output directory:
Inbox/gzh-articles/<年>/(a relative path inside the vault, the generic default in the code). For this vault, it is recommended to set it to your own staging area, e.g.<vault-inbox-dir>/gzh-articles/—override it with--out <vault-relative-dir>or theenv.OBSIDIAN_GZH_OUTin your private config (the default deliberately does not hardcode a specific Chinese path, following this repo's SKILL_SPEC.md "no hardcoded personal paths" rule). - Filename:
<出版日期>-公众号-<账号显示名>-<标题>.md, and on name conflicts a short suffix is added based on url characteristics. - frontmatter:
tags:[公众号原创, 待归位]、source: 公众号自有、url、title、author、published_at、captured_at、route: api|cookie|mp-backend、content_complete、topics: [](topicsis an extra item this skill adds relative to the user's original field list, to make it convenient for subsequentorganizeto directly fill in the category; leave it as an empty array when there is no content, and it does not affect other fields). - Body: HTML → Markdown goes through stdlib
html.parser(zero third-party dependencies, consistent with the other scripts in this repo); 100% layout fidelity is not guaranteed, and complex inline styles/WeChat-exclusive components will be flattened into plain text + image links.content_complete: falseindicates that body extraction failed or is empty, and the original link needs to be manually checked. - Idempotent: all three scripts recursively search under
urlinout_dirby--forceoverwrites); running the three paths alternately will not land the same article twice.
After archiving
Following the clip family convention, AI can subsequently:
- Add a "## Summary" (30-80 characters) to each article.
- Determine/backfill frontmatter
topics。 - After the works library refactor is settled, hand it to
organizeto batch-relocate fromInbox/gzh-articles.
Closed-loop position
★clip-gzh(批量收) → organize(归位/分类) → distill(收藏→观点) → compose(观点→文) → publish(发)
and soia-pkm-clip-wechat-article(single-article archiving) are complementary: clip-wechat handles "see a forwarded link, save an article on the fly"; clip-gzh handles "batch-migrating the historical backlog of my own official account". The two are not fully consistent in their landing conventions (clip-gzh has an extra route/content_complete field), so they are not merged for now; whether to converge them into one set will be evaluated together during the works library refactor.