wjs-publishing-wechat

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

wjs-publishing-wechat

wjs-publishing-wechat

帮助用户写微信公众号文章。轻润色,不重写。 自动生成题图和解释图,输出可直接粘贴到公众号后台的内容包。
Help users write WeChat Official Account articles. Light polishing, no rewriting. Automatically generate cover images and explanatory illustrations, and output content packages that can be directly pasted into the WeChat Official Account backend.

Core Principle

Core Principle

保留作者的语气和节奏。 用户的思路和表达方式是文章的灵魂。你只做四件事:
  1. 修明显错字和重复字
  2. 调整段落(微信读者习惯短段落,每段 1–3 句)
  3. 抚平特别拗口的句子(保守,能不动就不动)
  4. 准备配套素材(题图、标题候选、摘要)
不要做的事:
  • 不要改变作者的用词偏好
  • 不要加 AI 味儿的连接词("首先"、"其次"、"综上所述"、"总而言之"、"值得注意的是")
  • 不要把口语改成书面语
  • 不要加 emoji(除非原文有)
  • 不要重新组织段落顺序
  • 不要"提升"作者的表达——他写他的,你只是清洁工
Preserve the author's tone and rhythm. The user's ideas and expression style are the soul of the article. You only need to do four things:
  1. Correct obvious typos and repeated characters
  2. Adjust paragraphs (WeChat readers prefer short paragraphs, 1–3 sentences per paragraph)
  3. Smooth out particularly awkward sentences (conservative, avoid changes if possible)
  4. Prepare supporting materials (cover image, title candidates, abstract)
Things NOT to do:
  • Do not change the author's word preference
  • Do not add AI-style connecting words ("First", "Second", "In summary", "All in all", "It is worth noting that")
  • Do not convert spoken language to written language
  • Do not add emojis (unless included in the original text)
  • Do not reorganize paragraph order
  • Do not "improve" the author's expression — let them write as they wish, you are just a cleaner

长度与例子(硬约束)

Length and Examples (Hard Constraints)

默认 800–1000 字。 2000 字算超长,必须有特别理由(系列长文、技术深度文)才能放过。第一稿就按这个预算写,不要写完再砍——按预算写出来的文章节奏紧,砍出来的文章会残留拼接感。
写到例子段时,过一遍这把尺
这个例子是真的具体(真事 / 真人 / 真数字 / 真细节),还是为了演示框架编出来的?
后者直接删,让
illustration.png
自己承担"演示结构"的功能——结构图已经把整套流程画出来了,正文再用文字把同一套结构展开一遍,是双倍的、空的内容。
优先保留:开头反差 / 钩子 + 核心框架 + 1 句点睛 + 软着陆结尾。
优先砍掉:演示性例子、重复阐释、"怎么用 / 入口在哪"这类 instructional 段落(让被介绍的工具 / skill 的 README 自己说)。
默认不要写
## 后注
。文章在正文最后落点收束即可。只有当真的有必要的致谢、来源标注、对前文的重要补充(不是凑数)时才加——比如转述别人的框架必须致谢原作者。"correct me if I am wrong——欢迎拍砖"这类签名收尾,不构成加后注的理由。
写完一定要数字数。
python3 -c "import re; t=open('article.md').read(); t=re.sub(r'\!\[.*?\]\(.*?\)','',t); print(len(re.findall(r'[一-鿿]',t)) + len(re.findall(r'[A-Za-z]+',t)))"
。超过 1200 就回去再砍一轮。
Default length: 800–1000 words. 2000 words is considered overly long, and only allowed for special reasons (series articles, in-depth technical articles). Write according to this word count from the first draft, do not cut down after writing — articles written to the word count have tight rhythm, while cut-down articles will have a spliced feel.
When writing example paragraphs, check against this rule:
Is this example truly specific (real event / real person / real numbers / real details), or made up just to demonstrate a framework?
Delete the latter and let
illustration.png
take on the function of "demonstrating structure" — the structure diagram already shows the entire process, and expanding the same structure in text in the main body is redundant and empty content.
Prioritize retaining: Opening contrast/hook + core framework + 1 key sentence + soft landing conclusion.
Prioritize cutting: Demonstrative examples, repeated explanations, instructional paragraphs like "how to use / where to find the entrance" (let the README of the introduced tool/skill explain this).
Do not write
## Postscript
by default
. The article should conclude at the end of the main body. Add it only when truly necessary for acknowledgments, source citations, or important supplements to the previous content (not for padding) — such as acknowledging the original author when paraphrasing others' frameworks. Sign-offs like "correct me if I am wrong — welcome feedback" do not justify adding a postscript.
Be sure to count the word count after writing. Use this command:
python3 -c "import re; t=open('article.md').read(); t=re.sub(r'\!\[.*?\]\(.*?\)','',t); print(len(re.findall(r'[一-鿿]',t)) + len(re.findall(r'[A-Za-z]+',t)))"
. If it exceeds 1200 words, go back and cut again.

介绍 skill 的文章:末尾必须附 5 平台安装方法

Articles Introducing Skills: Must Include 5 Platform Installation Methods at the End

触发条件:这篇文章是在介绍 / 推荐 / 解释某个具体的 Claude Code skill(不管是王建硕自己写的,还是别人的)。
前置 — 确认 skill 已发布
  • 王建硕自己的
    wjs-*
    skill:写完 SKILL.md 后,
    ~/.claude/skills-publish-hook.sh
    会自动 rsync + commit + push 到 github.com/jianshuo/claude-skills,无需手动。可用
    gh api repos/jianshuo/claude-skills/contents/<skill-name>
    确认已上线
  • 其他人的 skill:写文章前先确认它在公开 git repo 里。没有的话回头让作者先发布,不要让读者装不到
末尾必须附下面这段(直接套用,把
<SKILL_NAME>
替换成实际 skill 名):
markdown
undefined
Trigger condition: This article introduces/recommends/explains a specific Claude Code skill (whether written by Wang Jianshuo or others).
Precondition — Confirm the skill is published:
  • Wang Jianshuo's
    wjs-*
    skills: After writing SKILL.md,
    ~/.claude/skills-publish-hook.sh
    will automatically rsync + commit + push to github.com/jianshuo/claude-skills, no manual action needed. Use
    gh api repos/jianshuo/claude-skills/contents/<skill-name>
    to confirm it is online
  • Others' skills: Confirm it is in a public git repo before writing the article. If not, ask the author to publish it first, so readers can install it
Must include the following section at the end (directly replace
<SKILL_NAME>
with the actual skill name):
markdown
undefined

安装方法

Installation Methods

不用复制命令。打开你用的 AI agent——Claude Code、Codex、Kimi Code、OpenClaw 都可以,对它说一句:
它会自己 fetch、放到自己平台的 skill 目录里、提示你重启对话。
用 Hermes 的话直接命令行:
```bash hermes skills install https://github.com/jianshuo/claude-skills/blob/main/<SKILL_NAME>/SKILL.md ```
装完之后,对 agent 说一句「<一句最自然的触发语,紧扣这个 skill 的入口>」,就能用。

**几条规则**:
1. 这段**不计入** 800–1000 字预算(是附录性工具信息,不是正文)
2. **为什么是"对 agent 说一句话"而不是 `cp -r` 命令**:在 agent 时代,安装 = 让 agent fetch URL 并写到自己平台的 skill 目录。任何能上网的 agent 都能搞定,不需要用户记每个平台的目录路径。`cp -r` 命令对公众号读者过于技术,已经是上一时代的安装方式
3. URL 用 `github.com/<owner>/<repo>/blob/main/<path>` 形式:在浏览器能直接看到内容(读者可以先点开看再决定装不装),LLM agent 也能自动从 blob URL 抽出 markdown 原文(不需要手动转 raw.githubusercontent.com)
4. Hermes 单独列**命令行**形式:因为 Hermes 是 skill registry CLI 而非 chat agent,没有"对它说一句话"的入口,但它的 `hermes skills install <URL>` 接受同一个 URL,是最干净的等价物
5. 最后那句「装完之后,对 agent 说一句『……』」要根据当前 skill 的实际触发语写。例如「我想吃一堑长一智,最近这件事——」/「帮我准备一篇公众号」。**不要漏这一句**——读者装完不知道怎么开始用,整个安装段就白费
6. 通常放在文章最后(默认无 `## 后注`);如果这篇例外性地有 `## 后注`,安装方法放在后注之前
7. 如果将来出了新的支持 SKILL.md 的 agent 平台,**在第一段平台列表加一个名字即可**("Claude Code、Codex、Kimi Code、OpenClaw、新平台 都可以"),不需要为它写新一行命令
No need to copy commands. Open your AI agent — Claude Code, Codex, Kimi Code, OpenClaw all work — and say to it:
It will automatically fetch it, place it in the skill directory of its platform, and prompt you to restart the conversation.
For Hermes, use the command line directly:
bash
hermes skills install https://github.com/jianshuo/claude-skills/blob/main/<SKILL_NAME>/SKILL.md
After installation, say to the agent "<the most natural trigger phrase closely related to this skill's entry>" to use it.

**Rules**:
1. This section **does not count** towards the 800–1000 word budget (it is appendix tool information, not main body content)
2. **Why "tell the agent a sentence" instead of `cp -r` command**: In the agent era, installation = letting the agent fetch the URL and write it to its platform's skill directory. Any agent with internet access can handle this, no need for users to remember the directory path of each platform. The `cp -r` command is too technical for WeChat Official Account readers and belongs to the previous era of installation methods
3. Use the `github.com/<owner>/<repo>/blob/main/<path>` format for URLs: It can be directly viewed in the browser (readers can check it before deciding to install), and LLM agents can automatically extract the markdown content from the blob URL (no need to manually convert to raw.githubusercontent.com)
4. List the **command line** form separately for Hermes: Because Hermes is a skill registry CLI rather than a chat agent, it has no "tell it a sentence" entry, but its `hermes skills install <URL>` accepts the same URL and is the cleanest equivalent
5. The final sentence "After installation, say to the agent '...'" should be written according to the actual trigger phrase of the current skill. For example, "I want to learn from my mistakes, about this recent incident——" / "Help me prepare an official account article". **Do not omit this sentence** — if readers don't know how to start using it after installation, the entire installation section is useless
6. Usually placed at the end of the article (no `## Postscript` by default); if the article exceptionally has `## Postscript`, place the installation method before the postscript
7. If new agent platforms supporting SKILL.md emerge in the future, **just add the name to the first paragraph's platform list** ("Claude Code, Codex, Kimi Code, OpenClaw, New Platform all work"), no need to write a new command line for it

When This Skill Fires

When This Skill Fires

  • 用户提供一段思路、草稿、或语音转写文字
  • 用户说"帮我写一篇公众号"、"润色一下"、"准备发布"
  • 用户在公众号写作工作目录下工作(默认
    ~/wechat-publish/
    ~/code/wechat-publish/
    ,可由用户配置)
  • The user provides a set of ideas, a draft, or speech-to-text text
  • The user says "Help me write an official account article", "Polish this", "Prepare for publication"
  • The user works in the WeChat publication working directory (default
    ~/wechat-publish/
    or
    ~/code/wechat-publish/
    , configurable by the user)

Workflow

Workflow

Step 0: 接收输入

Step 0: Receive Input

用户会以以下形式给你内容:
  • 完整草稿(最常见)
  • 几段散乱的思路 / bullet points
  • 一段长文字,没有分段
  • 语音转写(可能有错字、重复)
如果输入太散,问一个问题:"这是想写一篇文章,还是几个独立想法?" —— 但只问这一次。
Users will provide content in the following forms:
  • Complete draft (most common)
  • Several scattered ideas / bullet points
  • A long text without paragraph breaks
  • Speech-to-text (may have typos, repetitions)
If the input is too scattered, ask one question: "Is this intended to be an article, or several independent ideas?" — but only ask once.

Step 1: 轻润色

Step 1: Light Polishing

打开一个 markdown 文件,把用户的内容粘进去。然后做下面这些:
  • 修错字("的得地"乱用、同音字错字、重复字"我我")
  • 段落切分:每 1–3 句一段。微信里长段落很难读
  • 拗口的地方做最小改动。如果改动后语气变了,宁可不改
  • 标点统一:中文用全角逗号句号,英文/数字之间空格
  • 保留原本的开头和结尾——这是作者的标志性特征
改动的尺度参考: 如果你改的字数超过原文的 5%,你改太多了。退回去。
Open a markdown file, paste the user's content into it. Then only do the following:
  • Correct typos (misuse of "的/得/地", homophone errors, repeated characters like "我我")
  • Split paragraphs: 1–3 sentences per paragraph. Long paragraphs are hard to read in WeChat
  • Make minimal changes to awkward parts. If the tone changes after modification, prefer not to change
  • Unify punctuation: Use full-width commas and periods for Chinese, add spaces between English/numbers
  • Keep the original opening and conclusion — these are the author's signature features
Modification scale reference: If the number of words you modify exceeds 5% of the original text, you have modified too much. Go back and adjust.

Step 2: 标题候选

Step 2: Title Candidates

给用户 3 个标题候选
  • A) 直白型:直接说文章讲什么
  • B) 故事型:从一个场景或冲突切入
  • C) 用户原文里的一句话:从草稿里摘最有味道的一句
不要做:标题党、夸张、"震惊"、"必看"。
Provide the user with 3 title candidates:
  • A) Straightforward type: Directly state what the article is about
  • B) Story type: Start with a scenario or conflict
  • C) A sentence from the user's original text: Extract the most vivid sentence from the draft
Do not: Use clickbait, exaggeration, "Shocking", "Must-read".

Step 3: 摘要 (50–80 字)

Step 3: Abstract (50–80 words)

公众号摘要是发到朋友圈/对话框时的预览。要点:
  • 不是文章第一段的复制
  • 一句话说清楚读者会获得什么
  • 用作者的语气,不是营销腔
The WeChat Official Account abstract is the preview when shared to Moments/dialog boxes. Key points:
  • Not a copy of the first paragraph of the article
  • Clearly state what readers will gain in one sentence
  • Use the author's tone, not marketing language

Step 4: 配图(每篇两张)

Step 4: Images (Two per Article)

每篇文章配 两张图:
  • 题图 cover.png — 进入文章前的封面,严格 2.35:1(900×383, 即 900÷383=2.349),进 WeChat 编辑器封面字段。强字体、强构图、文字主导
  • 解释图 illustration.png — 正文里的配图,比例由内容决定(模型自选),帮读者一眼看懂文章核心结构。扁平卡通,有标签和流程
题图固定走 AI 生成(不问用户,每张约 $0.05–0.20):
bash
~/.claude/skills/wjs-publishing-wechat/scripts/gen-cover-ai.sh <article-folder> ["目标字词"]
  • 不传第二个参数时,从
    meta.json
    title
    当目标字词
  • 内部调用
    gpt-image-2-skill
    强制走
    --provider codex
    (不再支持 OpenAI API key fallback)
  • 默认尺寸
    1536x1024
    (最接近 2.35:1 的 landscape),自动 sips 居中裁到 900×383
  • 原图保存为
    cover-raw.png
    ,裁剪后是
    cover.png
  • cover-prompt.md
    作为
    --instructions
    (设计哲学),短生成指令作为
    --prompt
    ——这样 gpt-5.4 能消化长 prompt 后再调 image_generation 工具
  • 可调环境变量:
    WECHAT_PUBLISH_IMAGE_SIZE
    (默认
    1536x1024
    )、
    WECHAT_PUBLISH_IMAGE_QUALITY
    (默认
    high
前置依赖:必须装好
gpt-image-2-skill
bash
git clone https://github.com/Wangnov/gpt-image-2-skill /tmp/g
cp -r /tmp/g/skills/gpt-image-2-skill ~/.claude/skills/
并且必须有 Codex 鉴权:
  • 唯一支持:Codex
    ~/.codex/auth.json
    (ChatGPT Plus 计划即可,不需要 OpenAI 组织验证,gpt-image-2 的中文字渲染明显比 gpt-image-1 准确)
  • 不再支持
    OPENAI_API_KEY
    直连(
    --instructions
    仅 Codex provider 支持,且 API 模式会绕过 Codex 的 prompt 优化)
目标字词的选择:文章标题往往是长短语(如「AI 能力的三个简单层次」),但 prompt 模板对单字 / 两字词更友好。可以建议用户挑核心概念字词:
目标字词用什么?默认是文章标题。建议挑一个核心概念字词(1–4 字),比如「AI 能力的三个简单层次」可以用「三层」或「层次」。
然后生成解释图(无需问用户,自动跑):
bash
~/.claude/skills/wjs-publishing-wechat/scripts/gen-illustration.sh <article-folder>
  • article.md
    全文,作为 instructions 传给 gpt-image-2
  • 模型理解文章核心结构后,生成扁平卡通解释图
  • 不裁剪,模型自选画幅(双行对照通常出 3:2,流程类用横长条,层级深度用竖版)
  • 输出
    illustration.png
    ,直接用作正文配图
重要:解释图必须在 markdown 里被引用——
article.md
里要有
![](./illustration.png)
一行,否则
upload-draft.sh
上传的草稿里看不到这张图(虽然图已经传到 CDN)。
默认插入位置:正文最后落点之后(默认无后注;如果有
## 后注
则放在后注之前;如果有
## 安装方法
则放在安装方法之前)——把解释图当作"整件事画起来就是这样"的视觉总结,配一句口语化引导(例如"整件事画出来,大概就是这样:"),不要写"如图所示"这种说明文腔。如果解释图只针对某一节(不是全文摘要),就紧跟在那一节正文之后。
安全网:如果生成了
illustration.png
article.md
漏掉了这一行引用,
upload-draft.sh
会自动在最合适的位置(有后注则后注前,否则文末)插入引用并改写
article.md
,确保结果幂等。但首选还是在 Step 5 写
article.md
时就把它放进去。
如果用户对某张图不满意,直接重跑对应脚本——每次结果不同。
Each article is paired with two images:
  • Cover image cover.png — The cover before entering the article, strictly 2.35:1 (900×383, i.e., 900÷383=2.349), for the cover field in the WeChat editor. Strong fonts, strong composition, text-driven
  • Explanatory illustration illustration.png — The image in the main body, ratio determined by content (model-selected), helps readers understand the core structure of the article at a glance. Flat cartoon style, with labels and processes
Cover images are generated by AI by default (no need to ask the user, each costs about $0.05–0.20):
bash
~/.claude/skills/wjs-publishing-wechat/scripts/gen-cover-ai.sh <article-folder> ["target words"]
  • If the second parameter is not passed, take
    title
    from
    meta.json
    as the target words
  • Internally calls
    gpt-image-2-skill
    , forces the use of
    --provider codex
    (no longer supports OpenAI API key fallback)
  • Default size
    1536x1024
    (the landscape size closest to 2.35:1), automatically cropped to 900×383 with sips centered
  • The original image is saved as
    cover-raw.png
    , and the cropped version is
    cover.png
  • cover-prompt.md
    is used as
    --instructions
    (design philosophy), and the short generation instruction is used as
    --prompt
    — this way, gpt-5.4 can digest the long prompt before calling the image_generation tool
  • Adjustable environment variables:
    WECHAT_PUBLISH_IMAGE_SIZE
    (default
    1536x1024
    ),
    WECHAT_PUBLISH_IMAGE_QUALITY
    (default
    high
    )
Pre-dependencies: Must have
gpt-image-2-skill
installed:
bash
git clone https://github.com/Wangnov/gpt-image-2-skill /tmp/g
cp -r /tmp/g/skills/gpt-image-2-skill ~/.claude/skills/
And must have Codex authentication:
  • Only supported: Codex
    ~/.codex/auth.json
    (ChatGPT Plus plan is sufficient, no OpenAI organization verification required, gpt-image-2 renders Chinese characters significantly more accurately than gpt-image-1)
  • No longer supported: Direct connection via
    OPENAI_API_KEY
    (only the Codex provider supports
    --instructions
    , and the API mode bypasses Codex's prompt optimization)
Selection of target words: Article titles are often long phrases (e.g., "Three Simple Levels of AI Capabilities"), but the prompt template is more friendly to single characters/2-character words. You can suggest the user select core concept words:
What target words to use? The default is the article title. It is recommended to select a core concept word (1–4 characters), for example, "Three Simple Levels of AI Capabilities" can use "Three Levels" or "Levels".
Then generate the explanatory illustration(no need to ask the user, run automatically):
bash
~/.claude/skills/wjs-publishing-wechat/scripts/gen-illustration.sh <article-folder>
  • Reads the full text of
    article.md
    and passes it as instructions to gpt-image-2
  • After the model understands the core structure of the article, it generates a flat cartoon explanatory illustration
  • No cropping, the model selects the frame size (two-line comparison usually uses 3:2, process types use horizontal long strips, hierarchical depth uses vertical version)
  • Outputs
    illustration.png
    , directly used as the image in the main body
Important: The explanatory illustration must be referenced in the markdown — there must be a line
![](./illustration.png)
in
article.md
, otherwise the draft uploaded by
upload-draft.sh
will not show this image (even though the image has been uploaded to the CDN).
Default insertion position: After the conclusion of the main body (no postscript by default; if there is
## Postscript
, place it before the postscript; if there is
## Installation Methods
, place it before the installation methods) — treat the explanatory illustration as a visual summary of "this is what the whole thing looks like", with a colloquial guide (e.g., "When you draw the whole thing, it looks roughly like this:"), do not write explanatory language like "As shown in the figure". If the explanatory illustration only targets a certain section (not a full-text summary), place it immediately after the main body of that section.
Safety net: If
illustration.png
is generated but the reference line is missing in
article.md
,
upload-draft.sh
will automatically insert the reference in the most appropriate position (before the postscript if there is one, otherwise at the end of the article) and rewrite
article.md
to ensure idempotent results. But it is preferred to place it when writing
article.md
in Step 5.
If the user is not satisfied with a certain image, directly re-run the corresponding script — the result will be different each time.

Step 5: 输出文件包

Step 5: Output File Package

在用户的工作目录下(默认
~/wechat-publish/articles/
)创建文件夹:
articles/2026-05-09-{slug}/
├── article.md           # 润色后的 markdown 源文件
├── article.html         # 转成 HTML,直接粘贴用
├── cover.png            # 题图 900×383 (2.35:1 严格)
├── illustration.png     # 解释图(任意比例,模型自选)
├── meta.json            # { title, summary, author, date, slug }
└── original.md          # 用户原始输入,备份
{slug}
从标题生成:拼音首字母 + 关键词,限制 30 字符以内。例如"我的第一台 Mac" →
my-first-mac
article.html 转换规则:
  • pandoc
    或简单的 markdown 解析(不需要复杂样式,公众号编辑器会重新排版)
  • 保留段落分隔(
    <p>
  • 保留加粗(
    <strong>
    )和列表
  • 不要内联 CSS——公众号会清掉
bash
pandoc article.md -f markdown -t html -o article.html
Create a folder in the user's working directory (default
~/wechat-publish/articles/
):
articles/2026-05-09-{slug}/
├── article.md           # Polished markdown source file
├── article.html         # Converted to HTML, ready for direct pasting
├── cover.png            # Cover image 900×383 (strict 2.35:1)
├── illustration.png     # Explanatory illustration (any ratio, model-selected)
├── meta.json            # { title, summary, author, date, slug }
└── original.md          # Backup of user's original input
{slug}
is generated from the title: Pinyin initials + keywords, limited to 30 characters or less. For example, "My First Mac" →
my-first-mac
.
article.html conversion rules:
  • Use
    pandoc
    or simple markdown parsing (no complex styles needed, the WeChat editor will reformat it)
  • Keep paragraph breaks (
    <p>
    )
  • Keep bold (
    <strong>
    ) and lists
  • Do not use inline CSS — the WeChat editor will clear it
bash
pandoc article.md -f markdown -t html -o article.html

如果没有 pandoc:

If pandoc is not available:

用 Python 的 markdown 包 / Node 的 marked / 或手写最简实现

Use Python's markdown package / Node's marked / or write a minimal implementation

undefined
undefined

Step 6: 发布(用
upload-draft.sh
走 md2wechat 底层)

Step 6: Publish (Use
upload-draft.sh
with md2wechat under the hood)

文章包准备好后,跑一行就能把文章作为草稿推到公众号后台:
bash
~/.claude/skills/wjs-publishing-wechat/scripts/upload-draft.sh \
  <workspace>/articles/YYYY-MM-DD-{slug}
脚本内部做了 4 件事(用
md2wechat
的低层命令,绕过它高层
convert
的 API key 限制):
  1. md2wechat upload_image cover.png
    → 拿到
    thumb_media_id
  2. 如果
    illustration.png
    存在但
    article.md
    没引用
    :自动在最合适的位置(有
    ## 后注
    则后注前,否则文末)插入
    ![整件事画起来,是这样的](./illustration.png)
    并改写
    article.md
    (幂等安全网)。然后
    md2wechat upload_image illustration.png
    → 拿到 WeChat CDN
    wechat_url
  3. article.md
    生成
    content.html
    • 去掉 frontmatter 和正文 H1(避免 md2wechat inspect 的 DUPLICATE_H1 警告)
    • 支持的 markdown 块:
      <p>
      /
      <h2>
      /
      <h3>
      /
      <img>
      /
      <strong>
      /
      <em>
      /
      <code>
      /
      <ul>
      /
      <ol>
      /
      <li>
      /
      <table>
      (markdown pipe table)
    • Raw HTML 块透传:以
      <
      开头的块(典型用例:
      <section style="background:#f7f5f0;…">…</section>
      包一段淡底色 + 灰字的引用 / 注释卡片)会原样输出,不被包成
      <p>
      。整段必须是一个块——内部不能有空行打断,否则会被切碎。作者自己负责 HTML 合法 + WeChat 编辑器能吃
    • 段落和图片等不写 inline CSS —— 让微信编辑器的默认 line-height / font-size / color 接管
    • 段落之间用
      <p><br></p>
      作为间距块(和编辑器里手动按两次回车的源码一致;不能省,否则相邻
      <p>
      在没有 margin 的情况下会贴在一起;也不能用
      <br><br>
      或空
      <p></p>
      ,会被编辑器规范化吃掉)
    • 段内多行 → 自动
      <br>
      分行
      :如果一个 markdown 段落(行之间不留空行)写了多行,转 HTML 时每个
      \n
      被替换成
      <br>
      ,整段还在一个
      <p>
      里。专门给排比 / 并列短句这种「视觉上一行一句、但属于同一段」的写法用——参见 [[wangjianshuo-perspective]] 里的「排比 / 并列短句要分行写,不要分段写」。
      **...**
      加粗也可以跨这些行(inline 正则用
      <br>
      而不是
      \n
      作内部边界)
    • 结构性样式例外(这些 inline style 必须加,不加就破坏可读性):
      • <h2>
        font-size:1.4em; font-weight:bold;
        (比正文大两号 + 粗体)
      • <h3>
        font-size:1.2em; font-weight:bold;
        (比正文大一号 + 粗体)
      • <strong>
        (即
        **...**
        ):
        color:#ff0000;
        (纯红粗体——作者刻意要的视觉点。仅作用于 markdown
        **bold**
        转出来的
        <strong>
        ;raw HTML 块里手写的带显式 inline style 的
        <strong style="...">
        不会被覆盖)
      • <table>
        border-collapse:collapse; width:100%;
      • <th>/<td>
        border:1px solid #d9d9d9; padding:6px 10px;
        <th>
        另加
        background:#f6f6f6
      • <code>
        font-family:Menlo,Consolas,monospace; background:#f4f4f4; padding:1px 6px; border-radius:3px; font-size:0.92em;
        (不加,命令和普通文字混一起看不出是命令)
    • 判定原则:装饰性样式(行高、颜色、字体)让微信编辑器接管;结构性样式(标题层级、表格边框、代码视觉块)必须 inline——不加就退化成正文 / 几行裸文字,块的意义丢失
    • Fenced code block (
      ```bash ... ```
      )
      :脚本自动剥掉 ``` 围栏 语言名("bash" / "python" 等),转成
      <p><code>…</code></p>
      ,多行用
      <br>
      连接。不做
      <pre>
      —— WeChat 编辑器对
      <pre>
      块不友好,而单行短命令用 inline-styled
      <code>
      视觉更干净
    • 命令展示的首选写法:在 article.md 里直接用 inline
      `...`
      (一对反引号包命令),而不是 fenced ```bash 块。除非真的有多行连续 shell 流程必须用块,否则 inline 比 block 更适合公众号阅读
    • ./illustration.png
      替换成 CDN URL
  4. 再从
    meta.json
    装出
    draft.json
  5. 草稿写入 / 更新
    • 如果
      publish.json
      里已经有
      draft_media_id
      (说明这篇之前发过草稿了),先跑
      update-draft-via-api.py
      复用同一个 media_id 在 backend 原地更新——WeChat 后台自带版本控制,不会产生新草稿;只有
      draft_updated_at
      时间戳被刷新
    • 老 media_id 被用户在后台删了 → API 返回
      errcode=40007
      ,脚本自动 fallback 到
      md2wechat create_draft
      建一个新的
    • 想强制建新的(比如要保留旧版做对照):
      export WECHAT_PUBLISH_FORCE_NEW=1
    • 这条「优先 update」是 2026-05 加的,之前每次都建新草稿——同一篇反复改会污染草稿箱
为什么自己写 update? md2wechat 的 CLI 只暴露了
create_draft
,但 WeChat 后台 API 是有
draft/update
的(md2wechat binary 里都能 grep 到这个 URL,只是没接出来)。所以本 skill 用 30 行 Python 直接调,绕过 md2wechat CLI 这层限制。
  • 走的是当前 shell 的
    HTTPS_PROXY
    (必要!WeChat IP 白名单认的是 proxy 出口 IP,不是本机直出 IP)
  • 40007 invalid media_id
    自动 fallback;其他错误(45004 description 超长等)直接 bubble up,跟创建路径一致
前置依赖
  • md2wechat
    CLI 已安装并配置好
    WECHAT_APPID
    +
    WECHAT_SECRET
    md2wechat config show
    验证)
  • 当前公网 IP 已加进公众号后台白名单:mp.weixin.qq.com → 设置与开发 → 基本配置 → IP 白名单。漏掉这一步会返回
    errcode=40164
    ,加白名单几十秒生效
  • 详细命令、provider 选择、品牌档案,参考
    /md2wechat
    skill
为什么不用
md2wechat convert --draft
实测发现这条「一键」路径在默认配置下走不通:
  • --mode api
    (默认)需要
    MD2WECHAT_API_KEY
    (md2wechat.cn 付费云渲染服务),普通用户没有
  • --mode ai
    不直接出 HTML,而是返回一份 prompt 让外部 AI 渲染,不闭环
所以本 skill 用
upload_image
+
create_draft
两条底层命令组合,自己拼 HTML 和 draft JSON。
upload-draft.sh
把这套流程封装成一行。
Step 6.1 — 可选:先 inspect / preview 检查
bash
cd <workspace>/articles/YYYY-MM-DD-{slug}
md2wechat inspect article.md      # 检查元数据、字数、发布就绪状态
md2wechat preview article.md      # 生成本地 HTML 预览(degraded 模式,能看个大概)
发布前如想确认元数据有没有超长、摘要是不是空,跑
inspect
。否则直接跳到 6.2。
Step 6.2 — 一行发布
bash
~/.claude/skills/wjs-publishing-wechat/scripts/upload-draft.sh \
  /Users/jianshuo/code/wechat-publish/articles/YYYY-MM-DD-{slug}
成功后输出
draft media_id
,并在文章目录里留下
content.html
draft.json
两个产物,便于复查或下次直接
md2wechat create_draft draft.json
重发。
Step 6.3 — 后台预览发布
upload-draft.sh
成功后会自动用默认浏览器打开
https://mp.weixin.qq.com/
(macOS 用
open
,Linux 用
xdg-open
)。如果浏览器已登录,会直接进 home,点一下「草稿箱」就能看到刚才那条。
要禁用 auto-open(比如批量跑多篇时怕开一堆 tab):
export WECHAT_PUBLISH_NO_OPEN=1
注:草稿的精确编辑深链 URL 形如
…appmsg_edit_v2?action=edit&appmsgid=XXX&token=YYY&…
,但
appmsgid
是后台数据库内部 ID(不等于 API 返回的
media_id
),
token
又是 session-bound,所以没法从 API 返回值拼出深链。打开草稿箱让用户选是当前能做的最稳的事。
到草稿箱 → 找到刚上传的文章 → 手机预览 → 发布。
如果出错
  • errcode=40164 not in whitelist
    :把当前公网 IP 加进 WeChat MP 后台白名单
  • errcode=45004
    meta.json
    summary
    为空或太短
  • 封面相关:确认
    cover.png
    路径正确、尺寸 ≥ 900×383
  • token / appid:
    md2wechat config validate
    看配置
Optional — 高级排版:如需第一屏判断、CTA、作者名片等模块,在
article.md
:::block
语法(需要
MD2WECHAT_API_KEY
才能渲染)。本 skill 默认不加,保持作者原文清洁。
Once the article package is ready, run one command to push the article as a draft to the WeChat Official Account backend:
bash
~/.claude/skills/wjs-publishing-wechat/scripts/upload-draft.sh \
  <workspace>/articles/YYYY-MM-DD-{slug}
The script does 4 things internally (uses low-level commands of
md2wechat
, bypasses the API key restriction of its high-level
convert
command):
  1. md2wechat upload_image cover.png
    → Get
    thumb_media_id
  2. If
    illustration.png
    exists but is not referenced in
    article.md
    : Automatically insert
    ![The whole thing looks like this when drawn](./illustration.png)
    in the most appropriate position (before
    ## Postscript
    if there is one, otherwise at the end of the article) and rewrite
    article.md
    (idempotent safety net). Then
    md2wechat upload_image illustration.png
    → Get WeChat CDN
    wechat_url
  3. Generate
    content.html
    from
    article.md
    :
    • Remove frontmatter and H1 in the main body (avoid DUPLICATE_H1 warning from md2wechat inspect)
    • Supported markdown blocks:
      <p>
      /
      <h2>
      /
      <h3>
      /
      <img>
      /
      <strong>
      /
      <em>
      /
      <code>
      /
      <ul>
      /
      <ol>
      /
      <li>
      /
      <table>
      (markdown pipe table)
    • Raw HTML block passthrough: Blocks starting with
      <
      (typical use case:
      <section style="background:#f7f5f0;…">…</section>
      wrapping a quote/comment card with light background + gray text) will be output as-is, not wrapped in
      <p>
      . The entire block must be one block — no empty lines inside to break it, otherwise it will be split. The author is responsible for ensuring the HTML is valid and compatible with the WeChat editor
    • Do not write inline CSS for paragraphs and images — let the WeChat editor's default line-height / font-size / color take over
    • Use
      <p><br></p>
      as spacing between paragraphs (consistent with the source code of manually pressing Enter twice in the editor; cannot be omitted, otherwise adjacent
      <p>
      will stick together without margin; also cannot use
      <br><br>
      or empty
      <p></p>
      , which will be normalized and removed by the editor)
    • Multi-line within a paragraph → automatically
      <br>
      line breaks
      : If a markdown paragraph (no empty lines between lines) has multiple lines, each
      \n
      is replaced with
      <br>
      during HTML conversion, and the entire paragraph remains in one
      <p>
      . Specifically designed for parallel/coordinate short phrases written as "one sentence per line visually, but belonging to the same paragraph" — refer to "Parallel/coordinate short phrases should be written in separate lines, not separate paragraphs" in [[wangjianshuo-perspective]].
      **...**
      bold can also span these lines (inline regex uses
      <br>
      instead of
      \n
      as the internal boundary)
    • Structural style exceptions (these inline styles must be added, otherwise readability will be damaged):
      • <h2>
        :
        font-size:1.4em; font-weight:bold;
        (two sizes larger than the main body + bold)
      • <h3>
        :
        font-size:1.2em; font-weight:bold;
        (one size larger than the main body + bold)
      • <strong>
        (i.e.,
        **...**
        ):
        color:#ff0000;
        (pure red bold — a visual point intentionally set by the author. Only applies to
        <strong>
        converted from markdown
        **bold**
        ; manually written
        <strong style="...">
        with explicit inline style in raw HTML blocks will not be overwritten)
      • <table>
        :
        border-collapse:collapse; width:100%;
      • <th>/<td>
        :
        border:1px solid #d9d9d9; padding:6px 10px;
        (
        <th>
        additionally has
        background:#f6f6f6
        )
      • <code>
        :
        font-family:Menlo,Consolas,monospace; background:#f4f4f4; padding:1px 6px; border-radius:3px; font-size:0.92em;
        (without this, commands and ordinary text are mixed and indistinguishable)
    • Judgment principle: Decorative styles (line height, color, font) are handled by the WeChat editor; structural styles (title hierarchy, table borders, code visual blocks) must be inline — without them, they will degenerate into main body text / several lines of bare text, losing the meaning of the block
    • Fenced code block (
      ```bash ... ```
      )
      : The script automatically strips the ``` fences and language names ("bash" / "python" etc.), converts to
      <p><code>…</code></p>
      , and connects multiple lines with
      <br>
      . Do not use
      <pre>
      — the WeChat editor is not friendly to
      <pre>
      blocks, and short commands using inline-styled
      <code>
      look cleaner visually
    • Preferred way to display commands: Use inline
      `...`
      (a pair of backticks wrapping the command) directly in article.md, instead of fenced ```bash blocks. Unless there is really a multi-line continuous shell process that must use blocks, inline is more suitable for WeChat Official Account reading
    • Replace
      ./illustration.png
      with the CDN URL
  4. Assemble
    draft.json
    from
    meta.json
  5. Draft writing / updating:
    • If
      draft_media_id
      already exists in
      publish.json
      (indicating this article was previously sent as a draft), first run
      update-draft-via-api.py
      to reuse the same media_id for in-place update in the backend — the WeChat backend has built-in version control, no new draft will be generated; only the
      draft_updated_at
      timestamp will be refreshed
    • If the old media_id was deleted by the user in the backend → API returns
      errcode=40007
      , the script automatically falls back to
      md2wechat create_draft
      to create a new draft
    • To force creation of a new draft (e.g., to keep the old version for comparison):
      export WECHAT_PUBLISH_FORCE_NEW=1
    • This "priority update" was added in May 2026; previously, a new draft was created every time — repeated revisions of the same article would pollute the draft box
Why write our own update? The md2wechat CLI only exposes
create_draft
, but the WeChat backend API has
draft/update
(this URL can be grep'ed in the md2wechat binary, but it is not exposed). So this skill uses 30 lines of Python to call it directly, bypassing the md2wechat CLI layer.
  • Uses the
    HTTPS_PROXY
    of the current shell (necessary! The WeChat IP whitelist recognizes the proxy exit IP, not the local direct IP)
  • Automatically falls back on old
    40007 invalid media_id
    ; other errors (e.g., 45004 description too long) are directly bubbled up, consistent with the creation path
Pre-dependencies:
  • md2wechat
    CLI is installed and configured with
    WECHAT_APPID
    +
    WECHAT_SECRET
    (verify with
    md2wechat config show
    )
  • Current public IP has been added to the WeChat Official Account backend whitelist: mp.weixin.qq.com → Settings and Development → Basic Configuration → IP Whitelist. Omitting this step will return
    errcode=40164
    , and adding to the whitelist takes effect in tens of seconds
  • For detailed commands, provider selection, and brand profiles, refer to the
    /md2wechat
    skill
Why not use
md2wechat convert --draft
?
It was found through actual testing that this "one-click" path does not work under default configurations:
  • --mode api
    (default) requires
    MD2WECHAT_API_KEY
    (paid cloud rendering service from md2wechat.cn), which ordinary users do not have
  • --mode ai
    does not directly output HTML, but returns a prompt for external AI rendering, which is not closed-loop
So this skill combines the two low-level commands
upload_image
+
create_draft
to assemble HTML and draft JSON by itself.
upload-draft.sh
encapsulates this process into one command.
Step 6.1 — Optional: Inspect / Preview First
bash
cd <workspace>/articles/YYYY-MM-DD-{slug}
md2wechat inspect article.md      # Check metadata, word count, publication readiness status
md2wechat preview article.md      # Generate local HTML preview (degraded mode, can get a rough idea)
If you want to confirm whether metadata is too long or the abstract is empty before publishing, run
inspect
. Otherwise, skip directly to 6.2.
Step 6.2 — One-Command Publication
bash
~/.claude/skills/wjs-publishing-wechat/scripts/upload-draft.sh \
  /Users/jianshuo/code/wechat-publish/articles/YYYY-MM-DD-{slug}
Upon success, it outputs the
draft media_id
, and leaves two products
content.html
and
draft.json
in the article directory for review or re-publishing directly with
md2wechat create_draft draft.json
next time.
Step 6.3 — Backend Preview and Publication
After
upload-draft.sh
succeeds, it will automatically open
https://mp.weixin.qq.com/
with the default browser
(uses
open
on macOS,
xdg-open
on Linux). If the browser is already logged in, it will directly enter the homepage, and you can click "Draft Box" to see the draft just uploaded.
To disable auto-open (e.g., avoid opening multiple tabs when running multiple articles in batches):
export WECHAT_PUBLISH_NO_OPEN=1
.
Note: The precise edit deep link URL of the draft is like
…appmsg_edit_v2?action=edit&appmsgid=XXX&token=YYY&…
, but
appmsgid
is the internal ID of the backend database (not equal to the
media_id
returned by the API), and
token
is session-bound, so it is impossible to assemble the deep link from the API return value. Opening the draft box for the user to select is the most stable thing that can be done currently.
Go to the draft box → find the article just uploaded → preview on mobile → publish.
If errors occur:
  • errcode=40164 not in whitelist
    : Add the current public IP to the WeChat MP backend whitelist
  • errcode=45004
    : The
    summary
    in
    meta.json
    is empty or too short
  • Cover-related: Confirm the
    cover.png
    path is correct and the size is ≥ 900×383
  • Token / appid: Check the configuration with
    md2wechat config validate
Optional — Advanced Typesetting: If modules like first-screen judgment, CTA, author business card are needed, add
:::block
syntax in
article.md
(requires
MD2WECHAT_API_KEY
to render). This skill does not add it by default to keep the author's original text clean.

Step 7(可选)—— API 群发 + 拉留言

Step 7 (Optional) — API Mass Sending + Fetch Comments

⚠️ 2025-07 政策变化 — 个人主体账号 API 发布权限被回收
自 2025-07 起,微信回收了个人主体认证账号(即使有黄色 V)的「发布能力 API」调用权限。
mass/preview
/
mass/sendall
/
freepublish/*
/
comment/*
全部返回
errcode=48001
只有「企业主体认证」(公司营业执照认证)的服务号 / 订阅号才有 API 发布 + 评论拉取权限。
判断方法:调用
mass/preview
试一次。返回 48001 → 个人主体(这条 Step 7 整段不可用,跳到 Step 8 走 cookie fallback)。返回 0 → 企业主体,下面流程都可用。
如果你的账号被 48001 卡死:用 Step 8 - cookie-based 拉留言 替代这一段。
前提:公众号是企业主体认证订阅号或服务号。个人主体认证(黄 V)也不行——必须是公司主体。
为什么单独设计成两个命令、不并进
upload-draft.sh
  1. 订阅号每天只有 1 次 API 群发配额,自动触发跑错就废了
  2. 群发跳过"草稿箱后台预览 → 改个错别字"这道人工把关——保留它
两个命令各自做什么
mass-send.sh <folder> --preview <my-openid>
  • cgi-bin/message/mass/preview
    ,把已创建的草稿发给你自己的微信
  • 不消耗当日群发配额,专用于"群发前最后看一眼实际效果"
  • 微信里看 OK 了再走
    --send
mass-send.sh <folder> --send
  • cgi-bin/message/mass/sendall
    (
    filter.is_to_all=true
    ),真群发给所有粉丝
  • 成功后自动
    cgi-bin/comment/open
    打开这篇的评论功能(不打开拉评论会返回
    errcode=88000
  • msg_id
    +
    msg_data_id
    写回
    publish.json
    ,下游
    fetch-comments.sh
    凭这个找到这篇
fetch-comments.sh <folder> [--md|--json|--both]
  • publish.json
    msg_data_id
  • 翻页拉所有留言(
    comment/list
    一页 50 条,自动翻完)
  • 默认输出 Markdown 到
    comments.md
    (含昵称前缀、时间、精选标记、点赞数、公开回复);
    --json
    输出原始 API payload 到
    comments.json
典型流程(已认证账号):
upload-draft.sh <folder>                        # 创建草稿 + 写 publish.json
mass-send.sh <folder> --preview <my-openid>     # 自己手机里看一眼
mass-send.sh <folder> --send                    # 真群发 + 打开评论
⚠️ Policy Change in July 2025 — API Publishing Rights for Personal Subject Accounts Revoked
Since July 2025, WeChat has revoked the API call permissions for "publishing capabilities" of personal subject authenticated accounts (even with yellow V).
mass/preview
/
mass/sendall
/
freepublish/*
/
comment/*
all return
errcode=48001
.
Only "enterprise subject authenticated" (authenticated with company business license) service accounts/subscription accounts have API publishing + comment fetching permissions.
Judgment method: Try calling
mass/preview
once. Returns 48001 → personal subject (this entire Step 7 is unavailable, skip to Step 8 for cookie fallback). Returns 0 → enterprise subject, all processes below are available.
If your account is blocked by 48001: Use Step 8 - Cookie-based Comment Fetching to replace this section.
Precondition: The WeChat Official Account is an enterprise subject authenticated subscription account or service account. Personal subject authentication (yellow V) is not acceptable — must be a company subject.
Why design it as two separate commands instead of integrating into
upload-draft.sh
:
  1. Subscription accounts have only 1 API mass sending quota per day, and an automatic trigger error will waste it
  2. Mass sending skips the manual check step of "preview in draft box backend → correct typos" — retain this step
What the two commands do respectively:
mass-send.sh <folder> --preview <my-openid>
  • Calls
    cgi-bin/message/mass/preview
    to send the created draft to your own WeChat
  • Does not consume the daily mass sending quota, specifically used for "one last look at the actual effect before mass sending"
  • Run
    --send
    only after confirming it is OK in WeChat
mass-send.sh <folder> --send
  • Calls
    cgi-bin/message/mass/sendall
    (
    filter.is_to_all=true
    ) to truly send to all followers
  • After success, automatically calls
    cgi-bin/comment/open
    to enable the comment function for this article (not enabling it will return
    errcode=88000
    when fetching comments)
  • Writes
    msg_id
    +
    msg_data_id
    back to
    publish.json
    , and the downstream
    fetch-comments.sh
    finds this article based on this
fetch-comments.sh <folder> [--md|--json|--both]
  • Reads
    msg_data_id
    from
    publish.json
  • Fetches all comments by pagination (
    comment/list
    returns 50 items per page, automatically paginates until complete)
  • By default outputs Markdown to
    comments.md
    (includes nickname prefix, time, featured mark, like count, public reply);
    --json
    outputs the original API payload to
    comments.json
Typical process (authenticated accounts):
upload-draft.sh <folder>                        # Create draft + write publish.json
mass-send.sh <folder> --preview <my-openid>     # Check it on your mobile phone
mass-send.sh <folder> --send                    # Truly mass send + enable comments

等几分钟到几小时让粉丝看到 + 留言

Wait a few minutes to hours for followers to see it + leave comments

fetch-comments.sh <folder> # comments.md 出炉

**publish.json 字段**(增量写,永不丢之前的字段):
- `draft_media_id` / `draft_created_at` — `upload-draft.sh` 写
- `preview_msg_id` / `preview_sent_at` / `preview_to` — `mass-send.sh --preview` 写
- `msg_id` / `msg_data_id` / `mass_sent_at` / `comments_open` — `mass-send.sh --send` 写

**何时不要走这条路**:
- 未认证账号(48001):去做个人认证再来
- 已经用后台手动群发了这一篇:`msg_data_id` 拿不到了,只能 mp.weixin.qq.com 后台留言管理人肉看 / 导出
- 用 `freepublish/submit` 永久链接发的:不算"群发"、不出现在历史消息、`msg_data_id` 也拿不到——同样只能后台看

**常见 errcode**:
- `48001` — api unauthorized:**最常见原因不是未认证,是 2025-07 政策把个人主体的 API 发布权限回收了**(见本节顶部说明)。换企业主体或走 Step 8 cookie fallback
- `45028` — 当日群发配额已用完
- `88000` — 评论未开启(`mass-send.sh --send` 已经会自动 `comment/open`;如果跳过了 --send 直接拉,会撞这个)
fetch-comments.sh <folder> # comments.md is generated

**publish.json fields** (incremental writing, never lose previous fields):
- `draft_media_id` / `draft_created_at` — written by `upload-draft.sh`
- `preview_msg_id` / `preview_sent_at` / `preview_to` — written by `mass-send.sh --preview`
- `msg_id` / `msg_data_id` / `mass_sent_at` / `comments_open` — written by `mass-send.sh --send`

**When not to use this path**:
- Unauthenticated accounts (48001): Get personal authentication first
- Already manually mass sent this article via the backend: `msg_data_id` cannot be obtained, only manually check/export comments via the mp.weixin.qq.com backend comment management
- Sent via `freepublish/submit` permanent link: Not considered "mass sending", does not appear in historical messages, `msg_data_id` cannot be obtained — also can only check via the backend

**Common errcodes**:
- `48001` — api unauthorized: **The most common reason is not lack of authentication, but WeChat revoked API publishing rights for personal subjects in July 2025** (see the note at the top of this section). Switch to an enterprise subject or use Step 8 cookie fallback
- `45028` — Daily mass sending quota exhausted
- `88000` — Comments not enabled (`mass-send.sh --send` will automatically `comment/open`; if you skip --send and directly fetch comments, you will hit this)

Step 8(可选)—— Cookie fallback:拉留言(个人主体唯一可用路径)

Step 8 (Optional) — Cookie Fallback: Fetch Comments (Only Available Path for Personal Subjects)

适用场景:公众号是个人主体认证(Step 7 的官方 API 路径被 48001 卡死),但仍想 programmatic 拉留言。
原理:mp.weixin.qq.com 后台是个 SPA,所有留言数据通过内部 endpoint
mp.weixin.qq.com/misc/appmsgcomment?action=...&token=...&begin=...&count=...
返回 JSON。带上后台登录的 cookie + URL 里的 session token 就能跑通。
前提:浏览器登录了 mp.weixin.qq.com。两条路径,选一条:
Applicable scenario: The WeChat Official Account is personal subject authenticated (the official API path in Step 7 is blocked by 48001), but you still want to programmatically fetch comments.
Principle: The mp.weixin.qq.com backend is an SPA, and all comment data is returned as JSON via the internal endpoint
mp.weixin.qq.com/misc/appmsgcomment?action=...&token=...&begin=...&count=...
. You can make it work by bringing the cookie from the backend login + the session token in the URL.
Precondition: Logged in to mp.weixin.qq.com in the browser. Choose one of the two paths:

路径 A(推荐):复用 gstack 持久浏览器 —
fetch-comments-via-gstack.sh

Path A (Recommended): Reuse gstack Persistent Browser —
fetch-comments-via-gstack.sh

原始 cookie 几小时就过期;但 gstack 维护的 Chromium profile(
~/.gstack/chromium-profile/
)里的登录态实际能撑 3-14 天(社区经验值,按 7 天规划比较稳)——只要偶尔有访问保持热度。这条路径把所有手工抓包步骤都消掉了。
两条死规矩
  1. gstack profile 要独占 mp.weixin.qq.com 这个域。不要在系统 Chrome / Safari / 其他浏览器同时登录同一个公众号——并发登录会让 gstack 这边的 session 失效,又得重扫码。
  2. 失败模式:当 session 真的死了,请求会返回 HTML 登录页(不是 JSON),脚本会报「non-JSON response → cookie expired, re-grab」。这时跑
    browse goto https://mp.weixin.qq.com/
    + 扫码即可。
bash
undefined
Original cookies expire in a few hours; but the login state in the Chromium profile maintained by gstack (
~/.gstack/chromium-profile/
) can actually last 3-14 days (community experience, planning for 7 days is more stable) — as long as you visit occasionally to keep it active. This path eliminates all manual packet capture steps.
Two strict rules:
  1. The gstack profile must exclusively use the mp.weixin.qq.com domain. Do not log in to the same WeChat Official Account in system Chrome / Safari / other browsers at the same time — concurrent login will invalidate the session on the gstack side, and you will have to scan the code again.
  2. Failure mode: When the session actually dies, the request will return an HTML login page (not JSON), and the script will report "non-JSON response → cookie expired, re-grab". At this point, run
    browse goto https://mp.weixin.qq.com/
    + scan the code.
bash
undefined

一次性 setup(per machine):扫码登录 mp.weixin.qq.com 到 gstack profile

One-time setup (per machine): Scan code to log in to mp.weixin.qq.com in the gstack profile

~/.claude/skills/gstack/browse/dist/browse goto https://mp.weixin.qq.com/
~/.claude/skills/gstack/browse/dist/browse goto https://mp.weixin.qq.com/

用手机微信扫码

Scan the code with your mobile WeChat

一次性 setup(per article):保存 appmsgcomment URL 模板

One-time setup (per article): Save the appmsgcomment URL template

(URL 是 per-article 稳定的;token 部分脚本会每次从浏览器读最新值替换掉)

(The URL is stable per article; the script will read the latest token from the browser and replace it each time)

echo '<完整 appmsgcomment URL>' > <article-folder>/comment-url.txt
echo '<full appmsgcomment URL>' > <article-folder>/comment-url.txt

之后每次拉留言(零手工步骤):

Fetch comments every time afterwards (zero manual steps):

~/.claude/skills/wjs-publishing-wechat/scripts/fetch-comments-via-gstack.sh
<article-folder> [--md|--json|--both]

脚本流程:
1. `browse goto mp.weixin.qq.com/cgi-bin/home` — 刷新会话 + 校验登录
2. `browse url` — 抓当前 `token=`
3. `browse cookies` — 拿全套 cookie,过滤 weixin.qq.com 域,转成 Cookie header
4. 把 `comment-url.txt` 里的 token 替换成最新的
5. 调 `fetch-comments-by-cookie.sh`(下面路径 B 的脚本)跑完拉取

何时会失败:浏览器 profile 被微信踢出登录(异地登录 / 长期不用)。这时脚本会清楚告诉你重跑 `browse goto https://mp.weixin.qq.com/` + 扫码。
~/.claude/skills/wjs-publishing-wechat/scripts/fetch-comments-via-gstack.sh
<article-folder> [--md|--json|--both]

Script process:
1. `browse goto mp.weixin.qq.com/cgi-bin/home` — Refresh session + verify login
2. `browse url` — Capture the current `token=`
3. `browse cookies` — Get the full set of cookies, filter for the weixin.qq.com domain, convert to Cookie header
4. Replace the token in `comment-url.txt` with the latest one
5. Call `fetch-comments-by-cookie.sh` (the script in Path B below) to complete the fetching

When it fails: The browser profile is logged out by WeChat (异地登录 / long-term non-use). At this point, the script will clearly tell you to re-run `browse goto https://mp.weixin.qq.com/` + scan the code.

路径 B(fallback):手抓 cookie —
fetch-comments-by-cookie.sh

Path B (Fallback): Manually Capture Cookie —
fetch-comments-by-cookie.sh

gstack 没装、或想一次性快速拉,可以直接走这条:
bash
undefined
If gstack is not installed, or you want to fetch quickly once, you can directly use this path:
bash
undefined

1. 浏览器抓包(一次性,几分钟)

1. Browser packet capture (one-time, takes a few minutes)

a. 登录 mp.weixin.qq.com → 留言管理 → 找到目标文章

a. Log in to mp.weixin.qq.com → Comment Management → Find the target article

b. 打开 DevTools (Cmd+Opt+I) → Network 标签 → 筛选 Fetch/XHR

b. Open DevTools (Cmd+Opt+I) → Network tab → Filter Fetch/XHR

c. 在页面上翻一页评论,或点"加载更多"

c. Turn a page of comments on the page, or click "Load More"

d. 找请求 URL 含 'appmsgcomment' 的那条,右键 → Copy → "Copy as cURL (bash)"

d. Find the request URL containing 'appmsgcomment', right-click → Copy → "Copy as cURL (bash)"

e. 从 curl 命令里抠出 -H 'Cookie: ...' 那段(整段 cookie 字符串)和 URL

e. Extract the -H 'Cookie: ...' section (the entire cookie string) and the URL from the curl command

2. 跑脚本

2. Run the script

~/.claude/skills/wjs-publishing-wechat/scripts/fetch-comments-by-cookie.sh
<article-folder>
--url '<完整 URL,含 begin=0 那一段>'
--cookie '<整段 cookie 字符串>'

输出:`<article-folder>/comments.md`(同 fetch-comments.sh 格式)。

**caveats**:
- 路径 B 的 cookie 几小时就要重抓——这正是路径 A 存在的理由
- 内部 API 的字段名 / endpoint 可能随后台版本变;脚本用 heuristics 找 `comment_list` / `comments` / `data.list` 等常见字段。如果版本变了,让 Claude 现场 patch 几行 JSON path
- 不要把抓到的 cookie 发到 git 或 chat 公开渠道——它等于你的登录态

输出给用户的最后一段话,固定格式:
准备好了。文章在 articles/YYYY-MM-DD-{slug}/
发布(一行): ~/.claude/skills/wjs-publishing-wechat/scripts/upload-draft.sh
articles/YYYY-MM-DD-{slug}
成功后到 mp.weixin.qq.com 草稿箱预览 / 发布。
article.md 是源文件,下次改用这个。
undefined
~/.claude/skills/wjs-publishing-wechat/scripts/fetch-comments-by-cookie.sh
<article-folder>
--url '<full URL, including the section with begin=0>'
--cookie '<entire cookie string>'

Output: `<article-folder>/comments.md` (same format as fetch-comments.sh).

**Caveats**:
- The cookie in Path B needs to be re-captured every few hours — this is why Path A exists
- The field names / endpoints of the internal API may change with backend versions; the script uses heuristics to find common fields like `comment_list` / `comments` / `data.list`. If the version changes, let Claude patch a few lines of JSON path on the spot
- Do not send the captured cookie to public channels like git or chat — it is equivalent to your login state

The final paragraph output to the user must be in this fixed format:
Ready. The article is in articles/YYYY-MM-DD-{slug}/
Publish (one command): ~/.claude/skills/wjs-publishing-wechat/scripts/upload-draft.sh
articles/YYYY-MM-DD-{slug}
After success, go to mp.weixin.qq.com draft box to preview / publish.
article.md is the source file, use this for future revisions.
undefined

File Layout (skill 自身)

File Layout (Skill Itself)

~/.claude/skills/wjs-publishing-wechat/
├── SKILL.md                       # 本文件
├── README.md                      # 公开版 readme(GitHub 上展示用)
├── prompts/
│   ├── cover-prompt.md            # AI 题图 prompt 模板([目标字词] 占位符)
│   └── illustration-prompt.md     # AI 解释图 prompt 模板([文章内容] 占位符)
└── scripts/
    ├── gen-cover-ai.sh            # 题图: 2.35:1 强约束, 自动裁到 900×383
    ├── gen-illustration.sh        # 解释图: 比例自适应, 不裁剪
    ├── upload-draft.sh            # Step 6 主路径:upload_image × 2 + create_draft + 写 publish.json + 打开浏览器
    ├── mass-send.sh               # Step 7(可选, 仅企业主体):mass/preview 或 mass/sendall + 自动 comment/open
    ├── fetch-comments.sh          # Step 7(可选, 仅企业主体):拉 msg_data_id 对应的所有留言 → comments.md
    ├── fetch-comments-by-cookie.sh # Step 8(可选, 个人主体唯一可用):cookie fallback,浏览器抓包后拉留言
    └── publish.sh                 # legacy 备用:浏览器 + 剪贴板手动流(md2wechat 不可用时的兜底)
依赖的外部 skill:
  • gpt-image-2-skill
    (github.com/Wangnov/gpt-image-2-skill)—— gen-cover-ai.sh / gen-illustration.sh 走这里调 gpt-image-2,只走
    --provider codex
    (两个脚本已硬编码),需要
    ~/.codex/auth.json
    。不支持 OpenAI API key 直连
  • /md2wechat
    skill /
    md2wechat
    CLI —— upload-draft.sh 用它的
    upload_image
    +
    create_draft
    命令(需要
    WECHAT_APPID
    /
    WECHAT_SECRET
    ,且当前 IP 在白名单里)
注:仓库里仍保留
publish.sh
(浏览器 + 剪贴板手动发布流),仅作为 md2wechat 配置未就绪 / 不能加 IP 白名单时的备用方案。本 skill 默认路径不再使用它。
Auto-publish: 本 skill 由
~/.claude/skills-publish-hook.sh
自动同步到 github.com/jianshuo/claude-skills(每次编辑后自动 commit + push)。
~/.claude/skills/wjs-publishing-wechat/
├── SKILL.md                       # This file
├── README.md                      # Public readme (displayed on GitHub)
├── prompts/
│   ├── cover-prompt.md            # AI cover image prompt template ([target words] placeholder)
│   └── illustration-prompt.md     # AI explanatory illustration prompt template ([article content] placeholder)
└── scripts/
    ├── gen-cover-ai.sh            # Cover image: 2.35:1 strict constraint, automatically cropped to 900×383
    ├── gen-illustration.sh        # Explanatory illustration: Adaptive ratio, no cropping
    ├── upload-draft.sh            # Step 6 main path: upload_image × 2 + create_draft + write publish.json + open browser
    ├── mass-send.sh               # Step 7 (optional, enterprise subject only): mass/preview or mass/sendall + automatic comment/open
    ├── fetch-comments.sh          # Step 7 (optional, enterprise subject only): Fetch all comments corresponding to msg_data_id → comments.md
    ├── fetch-comments-by-cookie.sh # Step 8 (optional, only available for personal subjects): Cookie fallback, fetch comments after browser packet capture
    └── publish.sh                 # Legacy backup: Browser + clipboard manual flow (fallback when md2wechat is not configured)
Dependent external skills:
  • gpt-image-2-skill
    (github.com/Wangnov/gpt-image-2-skill) — gen-cover-ai.sh / gen-illustration.sh call gpt-image-2 through this, only use
    --provider codex
    (hard-coded in both scripts), requires
    ~/.codex/auth.json
    . Does not support direct connection via OpenAI API key
  • /md2wechat
    skill /
    md2wechat
    CLI — upload-draft.sh uses its
    upload_image
    +
    create_draft
    commands (requires
    WECHAT_APPID
    /
    WECHAT_SECRET
    , and current IP is in the whitelist)
Note:
publish.sh
(browser + clipboard manual publication flow) is still retained in the repository, only as a backup when md2wechat is not configured / cannot add IP whitelist. This skill no longer uses it in the default path.
Auto-publish: This skill is automatically synchronized to github.com/jianshuo/claude-skills by
~/.claude/skills-publish-hook.sh
(automatically commit + push after each edit).

Polish Heuristics (具体到字)

Polish Heuristics (Down to the Character)

错字模式 → 改:
  • "的得地" 误用:根据语法判断
  • 重复字:"我我"、"是是"、"了了" → 删一个
  • 同音字:考虑上下文("在"vs"再","做"vs"作")
段落切分时机:
  • 一句话讲完一个意思,下一句换主语 → 分段
  • 出现"但是"、"不过"、"所以"、"后来"在句首 → 考虑分段
  • 一段超过 80 字 → 找最近的句号分
不要分段:
  • 排比句、列举(保持节奏)
  • 对话(按对话格式)
Typo patterns → Correct:
  • Misuse of "的/得/地": Judge according to grammar
  • Repeated characters: "我我", "是是", "了了" → Delete one
  • Homophones: Consider context ("在"vs"再", "做"vs"作")
Paragraph splitting时机:
  • After finishing one idea, the next sentence changes subject → Split paragraph
  • When "But", "However", "So", "Later" appear at the beginning of a sentence → Consider splitting paragraph
  • A paragraph exceeds 80 words → Split at the nearest period
Do not split paragraphs:
  • Parallel sentences, enumerations (maintain rhythm)
  • Dialogue (follow dialogue format)

Anti-Patterns (绝对不做)

Anti-Patterns (Absolutely Not to Do)

不要原因
把"我觉得"改成"笔者认为"改变了作者身份
加"小标题"打断行文微信读者不需要导航
把口语句尾"吧/呢/啊"删掉删掉就不是这个人写的了
在结尾加"欢迎关注"、"点赞在看"作者会自己决定要不要
把"今天"改成具体日期作者用"今天"是有意为之
自己加举例 / 引用 / 数据这是写作,不是补全
改动后没给 diff,直接全文输出用户看不见你改了什么
文章超过 1500 字多半是某段空例子在撑场。砍掉那段,看会不会自然回到 1000 字
为演示框架编一段完整例子
illustration.png
承担演示。正文只留点睛,不复述结构图
Do NotReason
Change "I think" to "The author believes"Changes the author's identity
Add "subheadings" to interrupt the textWeChat readers do not need navigation
Remove spoken sentence endings like "吧/呢/啊"Removing them makes it no longer sound like the author
Add "Welcome to follow", "Like and share" at the endThe author will decide whether to add it
Change "today" to a specific dateThe author intentionally uses "today"
Add examples / citations / data by yourselfThis is writing, not completion
Output the full text directly without showing the diffUsers cannot see what you changed
Article exceeds 1500 wordsMost likely an empty example is padding. Cut that paragraph and see if it naturally returns to 1000 words
Write a complete example to demonstrate a frameworkLet
illustration.png
take on the demonstration. Only keep the key points in the main body, do not repeat the structure diagram

Showing the Diff

Showing the Diff

每次润色完,先告诉用户你改了什么,再问要不要继续:
我改了 7 处:
1. L3: "我我觉得" → "我觉得"(重复字)
2. L8: 长段落 (120字) 拆成两段
3. L15: "通过…的方式" → "用…"(口语化保留)
...

要看完整结果吗?
如果改动 ≤ 3 处,可以直接给完整结果,不用列 diff。
After each polishing, first tell the user what you changed, then ask if they want to continue:
I made 7 changes:
1. L3: "我我觉得" → "我觉得" (repeated character)
2. L8: Split long paragraph (120 words) into two paragraphs
3. L15: "通过…的方式" → "用…" (retain spoken style)
...

Do you want to see the complete result?
If there are ≤ 3 changes, you can directly provide the complete result without listing the diff.

Running the Skill (实操步骤)

Running the Skill (Practical Steps)

  1. 确认工作目录(默认
    ~/wechat-publish/
    ,可由用户配置)
  2. 接收用户输入(粘贴或文件)
  3. original.md
    (用户原始输入)
  4. article.md
    (润色版)→ 列 diff 给用户
  5. 用 AskUserQuestion 问标题候选
  6. 自动跑 gen-cover-ai.sh 生成题图 + gen-illustration.sh 生成解释图(不问用户)
  7. 生成
    article.html
    meta.json
  8. 输出发布指引
  1. Confirm the working directory (default
    ~/wechat-publish/
    , configurable by the user)
  2. Receive user input (paste or file)
  3. Write
    original.md
    (user's original input)
  4. Write
    article.md
    (polished version) → List the diff for the user
  5. Use AskUserQuestion to ask about title candidates
  6. Automatically run gen-cover-ai.sh to generate cover image + gen-illustration.sh to generate explanatory illustration (no need to ask the user)
  7. Generate
    article.html
    ,
    meta.json
  8. Output publication guidelines

Done When

Done When

  • articles/YYYY-MM-DD-{slug}/
    文件夹存在
  • 包含 article.md、article.html、cover.png、meta.json、original.md
  • meta.json 字段齐全
  • 用户拿到了发布指引
  • 用户没说"再改改"
  • articles/YYYY-MM-DD-{slug}/
    folder exists
  • Contains article.md, article.html, cover.png, meta.json, original.md
  • meta.json has complete fields
  • User has received the publication guidelines
  • User did not say "Revise it again"