wjs-publishing-wechat
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesewjs-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–3 句)
- 抚平特别拗口的句子(保守,能不动就不动)
- 准备配套素材(题图、标题候选、摘要)
不要做的事:
- 不要改变作者的用词偏好
- 不要加 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:
- Correct obvious typos and repeated characters
- Adjust paragraphs (WeChat readers prefer short paragraphs, 1–3 sentences per paragraph)
- Smooth out particularly awkward sentences (conservative, avoid changes if possible)
- 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——欢迎拍砖"这类签名收尾,不构成加后注的理由。
## 后注写完一定要数字数。。超过 1200 就回去再砍一轮。
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)))"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 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.
illustration.pngPrioritize 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 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.
## PostscriptBe sure to count the word count after writing. Use this command: . If it exceeds 1200 words, go back and cut again.
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)))"介绍 skill 的文章:末尾必须附 5 平台安装方法
Articles Introducing Skills: Must Include 5 Platform Installation Methods at the End
触发条件:这篇文章是在介绍 / 推荐 / 解释某个具体的 Claude Code skill(不管是王建硕自己写的,还是别人的)。
前置 — 确认 skill 已发布:
- 王建硕自己的 skill:写完 SKILL.md 后,
wjs-*会自动 rsync + commit + push 到 github.com/jianshuo/claude-skills,无需手动。可用~/.claude/skills-publish-hook.sh确认已上线gh api repos/jianshuo/claude-skills/contents/<skill-name> - 其他人的 skill:写文章前先确认它在公开 git repo 里。没有的话回头让作者先发布,不要让读者装不到
末尾必须附下面这段(直接套用,把 替换成实际 skill 名):
<SKILL_NAME>markdown
undefinedTrigger 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 skills: After writing SKILL.md,
wjs-*will automatically rsync + commit + push to github.com/jianshuo/claude-skills, no manual action needed. Use~/.claude/skills-publish-hook.shto confirm it is onlinegh api repos/jianshuo/claude-skills/contents/<skill-name> - 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 with the actual skill name):
<SKILL_NAME>markdown
undefined安装方法
Installation Methods
不用复制命令。打开你用的 AI agent——Claude Code、Codex、Kimi Code、OpenClaw 都可以,对它说一句:
安装 https://github.com/jianshuo/claude-skills/blob/main/<SKILL_NAME>/SKILL.md
它会自己 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:
Install https://github.com/jianshuo/claude-skills/blob/main/<SKILL_NAME>/SKILL.md
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.mdAfter 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 itWhen 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 or
~/wechat-publish/, configurable by the user)~/code/wechat-publish/
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(不再支持 OpenAI API key fallback)--provider codex - 默认尺寸 (最接近 2.35:1 的 landscape),自动 sips 居中裁到 900×383
1536x1024 - 原图保存为 ,裁剪后是
cover-raw.pngcover.png - 作为
cover-prompt.md(设计哲学),短生成指令作为--instructions——这样 gpt-5.4 能消化长 prompt 后再调 image_generation 工具--prompt - 可调环境变量:(默认
WECHAT_PUBLISH_IMAGE_SIZE)、1536x1024(默认WECHAT_PUBLISH_IMAGE_QUALITY)high
前置依赖:必须装好 :
gpt-image-2-skillbash
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 (ChatGPT Plus 计划即可,不需要 OpenAI 组织验证,gpt-image-2 的中文字渲染明显比 gpt-image-1 准确)
~/.codex/auth.json - 不再支持 直连(
OPENAI_API_KEY仅 Codex provider 支持,且 API 模式会绕过 Codex 的 prompt 优化)--instructions
目标字词的选择:文章标题往往是长短语(如「AI 能力的三个简单层次」),但 prompt 模板对单字 / 两字词更友好。可以建议用户挑核心概念字词:
目标字词用什么?默认是文章标题。建议挑一个核心概念字词(1–4 字),比如「AI 能力的三个简单层次」可以用「三层」或「层次」。
然后生成解释图(无需问用户,自动跑):
bash
~/.claude/skills/wjs-publishing-wechat/scripts/gen-illustration.sh <article-folder>- 读 全文,作为 instructions 传给 gpt-image-2
article.md - 模型理解文章核心结构后,生成扁平卡通解释图
- 不裁剪,模型自选画幅(双行对照通常出 3:2,流程类用横长条,层级深度用竖版)
- 输出 ,直接用作正文配图
illustration.png
重要:解释图必须在 markdown 里被引用—— 里要有 一行,否则 上传的草稿里看不到这张图(虽然图已经传到 CDN)。
article.mdupload-draft.sh默认插入位置:正文最后落点之后(默认无后注;如果有 则放在后注之前;如果有 则放在安装方法之前)——把解释图当作"整件事画起来就是这样"的视觉总结,配一句口语化引导(例如"整件事画出来,大概就是这样:"),不要写"如图所示"这种说明文腔。如果解释图只针对某一节(不是全文摘要),就紧跟在那一节正文之后。
## 后注## 安装方法安全网:如果生成了但illustration.png漏掉了这一行引用,article.md会自动在最合适的位置(有后注则后注前,否则文末)插入引用并改写upload-draft.sh,确保结果幂等。但首选还是在 Step 5 写article.md时就把它放进去。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 from
titleas the target wordsmeta.json - Internally calls , forces the use of
gpt-image-2-skill(no longer supports OpenAI API key fallback)--provider codex - Default size (the landscape size closest to 2.35:1), automatically cropped to 900×383 with sips centered
1536x1024 - The original image is saved as , and the cropped version is
cover-raw.pngcover.png - is used as
cover-prompt.md(design philosophy), and the short generation instruction is used as--instructions— this way, gpt-5.4 can digest the long prompt before calling the image_generation tool--prompt - Adjustable environment variables: (default
WECHAT_PUBLISH_IMAGE_SIZE),1536x1024(defaultWECHAT_PUBLISH_IMAGE_QUALITY)high
Pre-dependencies: Must have installed:
gpt-image-2-skillbash
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 (ChatGPT Plus plan is sufficient, no OpenAI organization verification required, gpt-image-2 renders Chinese characters significantly more accurately than gpt-image-1)
~/.codex/auth.json - No longer supported: Direct connection via (only the Codex provider supports
OPENAI_API_KEY, and the API mode bypasses Codex's prompt optimization)--instructions
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 and passes it as instructions to gpt-image-2
article.md - 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 , directly used as the image in the main body
illustration.png
Important: The explanatory illustration must be referenced in the markdown — there must be a line in , otherwise the draft uploaded by will not show this image (even though the image has been uploaded to the CDN).
article.mdupload-draft.shDefault insertion position: After the conclusion of the main body (no postscript by default; if there is , place it before the postscript; if there is , 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.
## Postscript## Installation MethodsSafety net: Ifis generated but the reference line is missing inillustration.png,article.mdwill automatically insert the reference in the most appropriate position (before the postscript if there is one, otherwise at the end of the article) and rewriteupload-draft.shto ensure idempotent results. But it is preferred to place it when writingarticle.mdin Step 5.article.md
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}my-first-macarticle.html 转换规则:
- 用 或简单的 markdown 解析(不需要复杂样式,公众号编辑器会重新排版)
pandoc - 保留段落分隔()
<p> - 保留加粗()和列表
<strong> - 不要内联 CSS——公众号会清掉
bash
pandoc article.md -f markdown -t html -o article.htmlCreate 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}my-first-macarticle.html conversion rules:
- Use or simple markdown parsing (no complex styles needed, the WeChat editor will reformat it)
pandoc - Keep paragraph breaks ()
<p> - Keep bold () and lists
<strong> - 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
undefinedundefinedStep 6: 发布(用 upload-draft.sh
走 md2wechat 底层)
upload-draft.shStep 6: Publish (Use upload-draft.sh
with md2wechat under the hood)
upload-draft.sh文章包准备好后,跑一行就能把文章作为草稿推到公众号后台:
bash
~/.claude/skills/wjs-publishing-wechat/scripts/upload-draft.sh \
<workspace>/articles/YYYY-MM-DD-{slug}脚本内部做了 4 件事(用 的低层命令,绕过它高层 的 API key 限制):
md2wechatconvert- → 拿到
md2wechat upload_image cover.pngthumb_media_id - 如果 存在但
illustration.png没引用:自动在最合适的位置(有article.md则后注前,否则文末)插入## 后注并改写(幂等安全网)。然后article.md→ 拿到 WeChat CDNmd2wechat upload_image illustration.pngwechat_url - 从 生成
article.md:content.html- 去掉 frontmatter 和正文 H1(避免 md2wechat inspect 的 DUPLICATE_H1 警告)
- 支持的 markdown 块:/
<p>/<h2>/<h3>/<img>/<strong>/<em>/<code>/<ul>/<ol>/<li>(markdown pipe table)<table> - Raw HTML 块透传:以 开头的块(典型用例:
<包一段淡底色 + 灰字的引用 / 注释卡片)会原样输出,不被包成<section style="background:#f7f5f0;…">…</section>。整段必须是一个块——内部不能有空行打断,否则会被切碎。作者自己负责 HTML 合法 + WeChat 编辑器能吃<p> - 段落和图片等不写 inline CSS —— 让微信编辑器的默认 line-height / font-size / color 接管
- 段落之间用 作为间距块(和编辑器里手动按两次回车的源码一致;不能省,否则相邻
<p><br></p>在没有 margin 的情况下会贴在一起;也不能用<p>或空<br><br>,会被编辑器规范化吃掉)<p></p> - 段内多行 → 自动 分行:如果一个 markdown 段落(行之间不留空行)写了多行,转 HTML 时每个
<br>被替换成\n,整段还在一个<br>里。专门给排比 / 并列短句这种「视觉上一行一句、但属于同一段」的写法用——参见 [[wangjianshuo-perspective]] 里的「排比 / 并列短句要分行写,不要分段写」。<p>加粗也可以跨这些行(inline 正则用**...**而不是<br>作内部边界)\n - 结构性样式例外(这些 inline style 必须加,不加就破坏可读性):
- :
<h2>(比正文大两号 + 粗体)font-size:1.4em; font-weight:bold; - :
<h3>(比正文大一号 + 粗体)font-size:1.2em; font-weight:bold; - (即
<strong>):**...**(纯红粗体——作者刻意要的视觉点。仅作用于 markdowncolor:#ff0000;转出来的**bold**;raw HTML 块里手写的带显式 inline style 的<strong>不会被覆盖)<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" / "python" 等),转成
```bash ... ```,多行用<p><code>…</code></p>连接。不做<br>—— WeChat 编辑器对<pre>块不友好,而单行短命令用 inline-styled<pre>视觉更干净<code> - 命令展示的首选写法:在 article.md 里直接用 inline (一对反引号包命令),而不是 fenced ```bash 块。除非真的有多行连续 shell 流程必须用块,否则 inline 比 block 更适合公众号阅读
`...` - 替换成 CDN URL
./illustration.png
- 再从 装出
meta.jsondraft.json - 草稿写入 / 更新:
- 如果 里已经有
publish.json(说明这篇之前发过草稿了),先跑draft_media_id复用同一个 media_id 在 backend 原地更新——WeChat 后台自带版本控制,不会产生新草稿;只有update-draft-via-api.py时间戳被刷新draft_updated_at - 老 media_id 被用户在后台删了 → API 返回 ,脚本自动 fallback 到
errcode=40007建一个新的md2wechat create_draft - 想强制建新的(比如要保留旧版做对照):
export WECHAT_PUBLISH_FORCE_NEW=1 - 这条「优先 update」是 2026-05 加的,之前每次都建新草稿——同一篇反复改会污染草稿箱
- 如果
为什么自己写 update? md2wechat 的 CLI 只暴露了 ,但 WeChat 后台 API 是有 的(md2wechat binary 里都能 grep 到这个 URL,只是没接出来)。所以本 skill 用 30 行 Python 直接调,绕过 md2wechat CLI 这层限制。
create_draftdraft/update- 走的是当前 shell 的 (必要!WeChat IP 白名单认的是 proxy 出口 IP,不是本机直出 IP)
HTTPS_PROXY - 老 自动 fallback;其他错误(45004 description 超长等)直接 bubble up,跟创建路径一致
40007 invalid media_id
前置依赖:
- CLI 已安装并配置好
md2wechat+WECHAT_APPID(WECHAT_SECRET验证)md2wechat config show - 当前公网 IP 已加进公众号后台白名单:mp.weixin.qq.com → 设置与开发 → 基本配置 → IP 白名单。漏掉这一步会返回 ,加白名单几十秒生效
errcode=40164 - 详细命令、provider 选择、品牌档案,参考 skill
/md2wechat
为什么不用 ? 实测发现这条「一键」路径在默认配置下走不通:
md2wechat convert --draft- (默认)需要
--mode api(md2wechat.cn 付费云渲染服务),普通用户没有MD2WECHAT_API_KEY - 不直接出 HTML,而是返回一份 prompt 让外部 AI 渲染,不闭环
--mode ai
所以本 skill 用 + 两条底层命令组合,自己拼 HTML 和 draft JSON。 把这套流程封装成一行。
upload_imagecreate_draftupload-draft.shStep 6.1 — 可选:先 inspect / preview 检查
bash
cd <workspace>/articles/YYYY-MM-DD-{slug}
md2wechat inspect article.md # 检查元数据、字数、发布就绪状态
md2wechat preview article.md # 生成本地 HTML 预览(degraded 模式,能看个大概)发布前如想确认元数据有没有超长、摘要是不是空,跑 。否则直接跳到 6.2。
inspectStep 6.2 — 一行发布
bash
~/.claude/skills/wjs-publishing-wechat/scripts/upload-draft.sh \
/Users/jianshuo/code/wechat-publish/articles/YYYY-MM-DD-{slug}成功后输出 ,并在文章目录里留下 和 两个产物,便于复查或下次直接 重发。
draft media_idcontent.htmldraft.jsonmd2wechat create_draft draft.jsonStep 6.3 — 后台预览发布
upload-draft.shhttps://mp.weixin.qq.com/openxdg-open要禁用 auto-open(比如批量跑多篇时怕开一堆 tab):。
export WECHAT_PUBLISH_NO_OPEN=1注:草稿的精确编辑深链 URL 形如 ,但 是后台数据库内部 ID(不等于 API 返回的 ), 又是 session-bound,所以没法从 API 返回值拼出深链。打开草稿箱让用户选是当前能做的最稳的事。
…appmsg_edit_v2?action=edit&appmsgid=XXX&token=YYY&…appmsgidmedia_idtoken到草稿箱 → 找到刚上传的文章 → 手机预览 → 发布。
如果出错:
- :把当前公网 IP 加进 WeChat MP 后台白名单
errcode=40164 not in whitelist - :
errcode=45004的meta.json为空或太短summary - 封面相关:确认 路径正确、尺寸 ≥ 900×383
cover.png - token / appid:看配置
md2wechat config validate
Optional — 高级排版:如需第一屏判断、CTA、作者名片等模块,在 加 语法(需要 才能渲染)。本 skill 默认不加,保持作者原文清洁。
article.md:::blockMD2WECHAT_API_KEYOnce 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 , bypasses the API key restriction of its high-level command):
md2wechatconvert- → Get
md2wechat upload_image cover.pngthumb_media_id - If exists but is not referenced in
illustration.png: Automatically insertarticle.mdin the most appropriate position (beforeif there is one, otherwise at the end of the article) and rewrite## Postscript(idempotent safety net). Thenarticle.md→ Get WeChat CDNmd2wechat upload_image illustration.pngwechat_url - Generate from
content.html: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>(markdown pipe table)<table> - Raw HTML block passthrough: Blocks starting with (typical use case:
<wrapping a quote/comment card with light background + gray text) will be output as-is, not wrapped in<section style="background:#f7f5f0;…">…</section>. 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<p> - Do not write inline CSS for paragraphs and images — let the WeChat editor's default line-height / font-size / color take over
- Use as spacing between paragraphs (consistent with the source code of manually pressing Enter twice in the editor; cannot be omitted, otherwise adjacent
<p><br></p>will stick together without margin; also cannot use<p>or empty<br><br>, which will be normalized and removed by the editor)<p></p> - Multi-line within a paragraph → automatically line breaks: If a markdown paragraph (no empty lines between lines) has multiple lines, each
<br>is replaced with\nduring HTML conversion, and the entire paragraph remains in one<br>. 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]].<p>bold can also span these lines (inline regex uses**...**instead of<br>as the internal boundary)\n - Structural style exceptions (these inline styles must be added, otherwise readability will be damaged):
- :
<h2>(two sizes larger than the main body + bold)font-size:1.4em; font-weight:bold; - :
<h3>(one size larger than the main body + bold)font-size:1.2em; font-weight:bold; - (i.e.,
<strong>):**...**(pure red bold — a visual point intentionally set by the author. Only applies tocolor:#ff0000;converted from markdown<strong>; manually written**bold**with explicit inline style in raw HTML blocks will not be overwritten)<strong style="..."> - :
<table>border-collapse:collapse; width:100%; - :
<th>/<td>(border:1px solid #d9d9d9; padding:6px 10px;additionally has<th>)background:#f6f6f6 - :
<code>(without this, commands and ordinary text are mixed and indistinguishable)font-family:Menlo,Consolas,monospace; background:#f4f4f4; padding:1px 6px; border-radius:3px; font-size:0.92em;
- 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 (): The script automatically strips the ``` fences and language names ("bash" / "python" etc.), converts to
```bash ... ```, and connects multiple lines with<p><code>…</code></p>. Do not use<br>— the WeChat editor is not friendly to<pre>blocks, and short commands using inline-styled<pre>look cleaner visually<code> - 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 with the CDN URL
./illustration.png
- Assemble from
draft.jsonmeta.json - Draft writing / updating:
- If already exists in
draft_media_id(indicating this article was previously sent as a draft), first runpublish.jsonto 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 theupdate-draft-via-api.pytimestamp will be refresheddraft_updated_at - If the old media_id was deleted by the user in the backend → API returns , the script automatically falls back to
errcode=40007to create a new draftmd2wechat create_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
- If
Why write our own update? The md2wechat CLI only exposes , but the WeChat backend API has (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.
create_draftdraft/update- Uses the of the current shell (necessary! The WeChat IP whitelist recognizes the proxy exit IP, not the local direct IP)
HTTPS_PROXY - Automatically falls back on old ; other errors (e.g., 45004 description too long) are directly bubbled up, consistent with the creation path
40007 invalid media_id
Pre-dependencies:
- CLI is installed and configured with
md2wechat+WECHAT_APPID(verify withWECHAT_SECRET)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 , and adding to the whitelist takes effect in tens of seconds
errcode=40164 - For detailed commands, provider selection, and brand profiles, refer to the skill
/md2wechat
Why not use ? It was found through actual testing that this "one-click" path does not work under default configurations:
md2wechat convert --draft- (default) requires
--mode api(paid cloud rendering service from md2wechat.cn), which ordinary users do not haveMD2WECHAT_API_KEY - does not directly output HTML, but returns a prompt for external AI rendering, which is not closed-loop
--mode ai
So this skill combines the two low-level commands + to assemble HTML and draft JSON by itself. encapsulates this process into one command.
upload_imagecreate_draftupload-draft.shStep 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 . Otherwise, skip directly to 6.2.
inspectStep 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 , and leaves two products and in the article directory for review or re-publishing directly with next time.
draft media_idcontent.htmldraft.jsonmd2wechat create_draft draft.jsonStep 6.3 — Backend Preview and Publication
After succeeds, it will automatically open with the default browser (uses on macOS, 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.
upload-draft.shhttps://mp.weixin.qq.com/openxdg-openTo disable auto-open (e.g., avoid opening multiple tabs when running multiple articles in batches): .
export WECHAT_PUBLISH_NO_OPEN=1Note: The precise edit deep link URL of the draft is like , but is the internal ID of the backend database (not equal to the returned by the API), and 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.
…appmsg_edit_v2?action=edit&appmsgid=XXX&token=YYY&…appmsgidmedia_idtokenGo to the draft box → find the article just uploaded → preview on mobile → publish.
If errors occur:
- : Add the current public IP to the WeChat MP backend whitelist
errcode=40164 not in whitelist - : The
errcode=45004insummaryis empty or too shortmeta.json - Cover-related: Confirm the path is correct and the size is ≥ 900×383
cover.png - 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 syntax in (requires to render). This skill does not add it by default to keep the author's original text clean.
:::blockarticle.mdMD2WECHAT_API_KEYStep 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 发布 + 评论拉取权限。判断方法:调用试一次。返回 48001 → 个人主体(这条 Step 7 整段不可用,跳到 Step 8 走 cookie fallback)。返回 0 → 企业主体,下面流程都可用。mass/preview如果你的账号被 48001 卡死:用 Step 8 - cookie-based 拉留言 替代这一段。
前提:公众号是企业主体认证订阅号或服务号。个人主体认证(黄 V)也不行——必须是公司主体。
为什么单独设计成两个命令、不并进 :
upload-draft.sh- 订阅号每天只有 1 次 API 群发配额,自动触发跑错就废了
- 群发跳过"草稿箱后台预览 → 改个错别字"这道人工把关——保留它
两个命令各自做什么:
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.jsonmsg_data_id - 翻页拉所有留言(一页 50 条,自动翻完)
comment/list - 默认输出 Markdown 到 (含昵称前缀、时间、精选标记、点赞数、公开回复);
comments.md输出原始 API payload 到--jsoncomments.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/*all returncomment/*.errcode=48001Only "enterprise subject authenticated" (authenticated with company business license) service accounts/subscription accounts have API publishing + comment fetching permissions.Judgment method: Try callingonce. 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.mass/previewIf 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- Subscription accounts have only 1 API mass sending quota per day, and an automatic trigger error will waste it
- 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 to send the created draft to your own WeChat
cgi-bin/message/mass/preview - Does not consume the daily mass sending quota, specifically used for "one last look at the actual effect before mass sending"
- Run only after confirming it is OK in WeChat
--send
mass-send.sh <folder> --send- Calls (
cgi-bin/message/mass/sendall) to truly send to all followersfilter.is_to_all=true - After success, automatically calls to enable the comment function for this article (not enabling it will return
cgi-bin/comment/openwhen fetching comments)errcode=88000 - Writes +
msg_idback tomsg_data_id, and the downstreampublish.jsonfinds this article based on thisfetch-comments.sh
fetch-comments.sh <folder> [--md|--json|--both]- Reads from
msg_data_idpublish.json - Fetches all comments by pagination (returns 50 items per page, automatically paginates until complete)
comment/list - By default outputs Markdown to (includes nickname prefix, time, featured mark, like count, public reply);
comments.mdoutputs the original API payload to--jsoncomments.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 返回 JSON。带上后台登录的 cookie + URL 里的 session token 就能跑通。
mp.weixin.qq.com/misc/appmsgcomment?action=...&token=...&begin=...&count=...前提:浏览器登录了 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 . You can make it work by bringing the cookie from the backend login + the session token in the URL.
mp.weixin.qq.com/misc/appmsgcomment?action=...&token=...&begin=...&count=...Precondition: Logged in to mp.weixin.qq.com in the browser. Choose one of the two paths:
路径 A(推荐):复用 gstack 持久浏览器 — fetch-comments-via-gstack.sh
fetch-comments-via-gstack.shPath A (Recommended): Reuse gstack Persistent Browser — fetch-comments-via-gstack.sh
fetch-comments-via-gstack.sh原始 cookie 几小时就过期;但 gstack 维护的 Chromium profile()里的登录态实际能撑 3-14 天(社区经验值,按 7 天规划比较稳)——只要偶尔有访问保持热度。这条路径把所有手工抓包步骤都消掉了。
~/.gstack/chromium-profile/两条死规矩:
- gstack profile 要独占 mp.weixin.qq.com 这个域。不要在系统 Chrome / Safari / 其他浏览器同时登录同一个公众号——并发登录会让 gstack 这边的 session 失效,又得重扫码。
- 失败模式:当 session 真的死了,请求会返回 HTML 登录页(不是 JSON),脚本会报「non-JSON response → cookie expired, re-grab」。这时跑 + 扫码即可。
browse goto https://mp.weixin.qq.com/
bash
undefinedOriginal cookies expire in a few hours; but the login state in the Chromium profile maintained by gstack () 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.
~/.gstack/chromium-profile/Two strict rules:
- 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.
- 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 + scan the code.
browse goto https://mp.weixin.qq.com/
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]
<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]
<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
fetch-comments-by-cookie.shPath B (Fallback): Manually Capture Cookie — fetch-comments-by-cookie.sh
fetch-comments-by-cookie.shgstack 没装、或想一次性快速拉,可以直接走这条:
bash
undefinedIf gstack is not installed, or you want to fetch quickly once, you can directly use this path:
bash
undefined1. 浏览器抓包(一次性,几分钟)
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>
--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}
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>'
<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}
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.
undefinedFile 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:
- (github.com/Wangnov/gpt-image-2-skill)—— gen-cover-ai.sh / gen-illustration.sh 走这里调 gpt-image-2,只走
gpt-image-2-skill(两个脚本已硬编码),需要--provider codex。不支持 OpenAI API key 直连~/.codex/auth.json - skill /
/md2wechatCLI —— upload-draft.sh 用它的md2wechat+upload_image命令(需要create_draft/WECHAT_APPID,且当前 IP 在白名单里)WECHAT_SECRET
注:仓库里仍保留(浏览器 + 剪贴板手动发布流),仅作为 md2wechat 配置未就绪 / 不能加 IP 白名单时的备用方案。本 skill 默认路径不再使用它。publish.sh
Auto-publish: 本 skill 由自动同步到 github.com/jianshuo/claude-skills(每次编辑后自动 commit + push)。~/.claude/skills-publish-hook.sh
~/.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:
- (github.com/Wangnov/gpt-image-2-skill) — gen-cover-ai.sh / gen-illustration.sh call gpt-image-2 through this, only use
gpt-image-2-skill(hard-coded in both scripts), requires--provider codex. Does not support direct connection via OpenAI API key~/.codex/auth.json - skill /
/md2wechatCLI — upload-draft.sh uses itsmd2wechat+upload_imagecommands (requirescreate_draft/WECHAT_APPID, and current IP is in the whitelist)WECHAT_SECRET
Note:(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.publish.sh
Auto-publish: This skill is automatically synchronized to github.com/jianshuo/claude-skills by(automatically commit + push after each edit).~/.claude/skills-publish-hook.sh
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 字 |
| 为演示框架编一段完整例子 | 让 |
| Do Not | Reason |
|---|---|
| Change "I think" to "The author believes" | Changes the author's identity |
| Add "subheadings" to interrupt the text | WeChat 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 end | The author will decide whether to add it |
| Change "today" to a specific date | The author intentionally uses "today" |
| Add examples / citations / data by yourself | This is writing, not completion |
| Output the full text directly without showing the diff | Users cannot see what you changed |
| Article exceeds 1500 words | Most 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 framework | Let |
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)
- 确认工作目录(默认 ,可由用户配置)
~/wechat-publish/ - 接收用户输入(粘贴或文件)
- 写 (用户原始输入)
original.md - 写 (润色版)→ 列 diff 给用户
article.md - 用 AskUserQuestion 问标题候选
- 自动跑 gen-cover-ai.sh 生成题图 + gen-illustration.sh 生成解释图(不问用户)
- 生成 、
article.htmlmeta.json - 输出发布指引
- Confirm the working directory (default , configurable by the user)
~/wechat-publish/ - Receive user input (paste or file)
- Write (user's original input)
original.md - Write (polished version) → List the diff for the user
article.md - Use AskUserQuestion to ask about title candidates
- Automatically run gen-cover-ai.sh to generate cover image + gen-illustration.sh to generate explanatory illustration (no need to ask the user)
- Generate ,
article.htmlmeta.json - Output publication guidelines
Done When
Done When
- 文件夹存在
articles/YYYY-MM-DD-{slug}/ - 包含 article.md、article.html、cover.png、meta.json、original.md
- meta.json 字段齐全
- 用户拿到了发布指引
- 用户没说"再改改"
- folder exists
articles/YYYY-MM-DD-{slug}/ - 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"