wechat-editor: the agent owns content, the server owns WeChat format
local/wechat-editor
WeChat Official Accounts strip style blocks, class attributes and external resource references, so syncing a paper deep-read or blog post there is not copy and paste but a format rewrite plus a resource migration. The split: the agent fetches the article, writes the title (max 64 characters) and digest (max 120) and picks a cover; the server converts to inline HTML, uploads images to WeChat's own image service and rewrites URLs, creates a draft via draft/add and submits for publishing when asked.
Our takeFormulas and diagrams survive because they are pre-rendered server-side: headless Chromium via Playwright plus MathJax tex-svg turns inline and display math into inline SVG, Mermaid goes through the same path, and WeChat supports SVG natively so nothing blurs on zoom. The other rule to remember is that publishing is asynchronous — a 200 only means WeChat accepted the submission, and real status has to be polled with freepublish/get.
Why this needs its own line
WeChat Official Accounts do not accept ordinary web HTML. Its article editor strips <style> blocks, class attributes and external resource references almost entirely, so styling has to be inline on every tag and images have to be hosted on WeChat's own image service. Syncing one of this site's paper deep-reads or blog posts to the account is therefore not copy and paste; it is a format rewrite plus a resource migration. This skill splits the job in two: the agent owns content (fetch the article, write the title and digest, pick a cover), the production server owns format (convert to WeChat inline HTML, upload images, call the draft and publish APIs).
In the digital twin the worker is wechat-editor, task key wechat:<log_id-or-slug>, reported as a start/finish pair.
Who owns what
| Side | Does | Why there |
|---|---|---|
| Agent | Fetches the URL content, writes the title (64 chars max), digest (120 chars max), cover image URL and Markdown body | Editorial judgement: it has to read the source and restructure it for a phone screen |
| Server | Validates credentials, converts to WeChat inline HTML, uploads the cover (auto RGBA to RGB JPG), uploads body images to the WeChat image host and rewrites the URLs, calls draft/add, submits for publishing when asked, returns {log_id, media_id, status} | AppSecret and access_token can only live on the server, and the conversion needs a headless browser |
Calls prefer SSH to localhost:8000 on the server over the public domain: production sits behind a CDN, and hitting a write endpoint through the domain can land on a cached response and behave strangely. Automation uses X-API-Key over the public domain, admin sessions use a cookie. WeChat's API is never called locally; everything goes through /api/wechat/* or /api/admin/wechat/*.
How formulas and diagrams survive
This is the hardest stretch of the chain. WeChat articles do not run KaTeX or MathJax at render time and do not execute JavaScript, so formulas have to be converted to vector graphics on the server beforehand. The converter runs Playwright headless Chromium with MathJax tex-svg.js: inline and display math become inline <svg> elements, which WeChat supports natively and which stay sharp at any screen scale. Mermaid diagrams take the same path, loading mermaid.min.js to SVG and screenshotting where needed. After rendering, a post-processing pass removes MathJax helper elements (aria labels, MathML output) and adds inline style to every <svg>, otherwise WeChat's sanitiser loses them. When a headless browser is unavailable there is a _render_fallback() path that at least keeps text and code blocks readable.
Two traps in the WeChat API
The Python side uses wechatpy 1.8.x, which wraps material and media uploads, but the draft box and free-publish APIs are newer and the library does not wrap them, so the endpoints get called directly: draft/add to create the draft, freepublish/submit to publish, freepublish/get to poll status. Covers go to material/add_material?type=image while body images go to media/uploadimg; the two endpoints have different quotas and purposes and mixing them up fails. Covers also get an automatic fix: WeChat rejects RGBA PNG, so the server runs _ensure_rgb() to produce an RGB JPG, which means handing over a paper's fig1.png is enough.
How the body is written
- Never paste the full text: distil it into something suited to the account, with short paragraphs for phone screens
- Formulas in dollar delimiters, which the server turns into images; code blocks in triple backticks, which the server highlights
- Images referenced in Markdown; local
static/img/...paths are uploaded to the WeChat image host and the URLs rewritten - No mixed Chinese and English typesetting inside a Chinese article
- A paper that already has
detail_htmlcan go straight through the admin draft endpoint with justthumb_path,digestandauthor; the server handles the conversion
Publishing is asynchronous
With action=draft (the default) the article sits in the account's draft box for a human to edit or release from the WeChat platform. With action=publish, WeChat reviews the submission; it does not go live instantly, and status has to be polled with freepublish/get. So "it published" is a claim to make carefully: a 200 response only means WeChat accepted the submission. History lives under admin, WeChat configuration, publish records, or at GET /api/wechat/logs?limit=20. AppID, AppSecret and the API key are sensitive: they are configured only in the admin panel and never written into code, scripts or logs.
Honest limits
This line depends on Playwright, the MathJax and Mermaid static assets, configured account credentials and the /api/wechat/* endpoints on this site's production server. It cannot be installed as an open-source package and has no one-line install command, and WeChat's sanitisation rules and quotas change whenever the platform decides they should. What transfers is the method: separate content authorship from platform format adaptation completely, pre-render formulas and diagrams to vectors on the server instead of hoping the client runs scripts, move images onto the target platform's own host, and treat asynchronous review status as a first-class thing to poll rather than assuming submission equals live. Those four hold for any destination with aggressive rich-text sanitising: email, Zhihu, Juejin, Notion.