Skip to content
Back to skills

Wjs Publishing Wechat

ASecurity

Use when the user wants to write or publish a 微信公众号 (WeChat Official Account) article — they share rough thoughts, a draft, or notes and ask for help polishing, generating a cover image (题图) and explanation illustration (解释图), or preparing the article for upload to mp.weixin.qq.com. Triggers include "写一篇微信文章", "公众号", "润色", "题图", "发公众号", "/wjs-publishing-wechat".

  • 128 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added May 26, 2026
content-marketingpythongoshellbashnodegitapibackend

Works with

  • claude code
  • cli
  • api

Security analysis

A92/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 17 files and shows the line behind each finding

Scanned May 27, 2026

npx -y skills add jianshuo/claude-skills --skill wjs-publishing-wechat --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Wjs Publishing Wechat?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Wjs Publishing Wechat
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/jianshuo-wjs-publishing-wechat/badge)](https://www.skillsdirectory.com/skills/jianshuo-wjs-publishing-wechat)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: wjs-publishing-wechat
description: Use when the user wants to write or publish a 微信公众号 (WeChat Official Account) article — they share rough thoughts, a draft, or notes and ask for help polishing, generating a cover image (题图) and explanation illustration (解释图), or preparing the article for upload to mp.weixin.qq.com. Triggers include "写一篇微信文章", "公众号", "润色", "题图", "发公众号", "/wjs-publishing-wechat".
---

# wjs-publishing-wechat

帮助用户写微信公众号文章。**轻润色,不重写。** 自动生成题图和解释图,输出可直接粘贴到公众号后台的内容包。

## Core Principle

**保留作者的语气和节奏。** 用户的思路和表达方式是文章的灵魂。你只做四件事:

1. 修明显错字和重复字
2. 调整段落(微信读者习惯短段落,每段 1–3 句)
3. 抚平特别拗口的句子(保守,能不动就不动)
4. 准备配套素材(题图、标题候选、摘要)

**不要做的事:**
- 不要改变作者的用词偏好
- 不要加 AI 味儿的连接词("首先"、"其次"、"综上所述"、"总而言之"、"值得注意的是")
- 不要把口语改成书面语
- 不要加 emoji(除非原文有)
- 不要重新组织段落顺序
- 不要"提升"作者的表达——他写他的,你只是清洁工

## 长度与例子(硬约束)

**默认 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 就回去再砍一轮。

## 加粗加红(每篇必须有,不是可选)

`upload-draft.sh` 会把 markdown 的 `**...**` 渲染成**红色粗体**(`<strong style="color:#ff0000">`)——这是作者刻意要的视觉重点。**每篇文章正文都必须有合理的加粗,一处都没有 = 没写完。**

规则:
- **2–4 处**,打在点睛句、关键结论、核心概念词上。不通篇加,也绝不能零处
- 优先加在:每节的「一句话结论」、全文情绪落点、读者最该记住的那句
- 不要加在:整段、过渡句、罗列项。命令 / 代码用 `` `code` `` 样式或独立代码块,不用加粗
- 标题(H2/H3)已有字号字重,不再 `**` 包

这条**不属于**「不要提升作者表达」的禁区——红色加粗是作者既定风格的一部分,等同于错字和分段,必须补齐。

写完检查:通篇 `**` 成对数在 2–4 之间,且每一处都值得变红。

## 盘古之白(中英之间留空格,每篇必须)

作者要求**中文里所有英文单词/词组前后都留一个空格**——「用 AI 写 skill」「Claude Code 是车」「发过去 13 万字」。纯数字与中文之间不强制(「13万字」可不加),要留白的是**英文**。

机械活,**别靠手敲、靠脚本**(幂等,自动跳过代码块 / 链接 / URL):

```bash
python3 ~/.claude/skills/wjs-publishing-wechat/scripts/pangu.py <folder>/article.md
```

`upload-draft.sh` 在建草稿时会先对 `article.md` 跑一遍 `pangu.py`,所以走完整发布流程的文章自动带盘古之白;但如果只是手写片段 / 单独生成 tweet,记得自己跑一下。

## 命令 / 代码:独立成段,用代码样式

正文里出现安装 / 运行命令时,**默认拉出来单独成段**,不要混在叙述句里写成 inline `` `npm install` ``(除非只是顺带提一句命令名)。两种写法:

- **首选**——和作者既有风格一致的淡底色代码块(raw HTML 块,整段一行,不能有内部空行):
  ```
  <section style="background:#f6f8fa;border-radius:6px;padding:14px 16px;overflow-x:auto;font-family:Menlo,Consolas,monospace;font-size:14px;line-height:1.8;color:#24292e;">npm install -g xxx<br>xxx run</section>
  ```
- 或 fenced ```` ```bash ```` 块,`upload-draft.sh` 会转成独立的 `<p><code>…</code></p>`。

判断:单独展示一行 / 几行命令 → 用上面的块;只在句中提一下命令名 → inline `` `code` `` 即可。

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

**触发条件**:这篇文章是在介绍 / 推荐 / 解释某个具体的 Claude Code skill(不管是王建硕自己写的,还是别人的)。

**前置 — 确认 skill 已发布**:
- 王建硕自己的 `wjs-*` skill:写完 SKILL.md 后,`~/.claude/skills-publish-hook.sh` 会自动 rsync + commit + push 到 [github.com/jianshuo/claude-skills](https://github.com/jianshuo/claude-skills),无需手动。可用 `gh api repos/jianshuo/claude-skills/contents/<skill-name>` 确认已上线
- 其他人的 skill:写文章前先确认它在公开 git repo 里。没有的话回头让作者先发布,不要让读者装不到

**末尾必须附下面这段**(直接套用,把 `<SKILL_NAME>` 替换成实际 skill 名):

```markdown
## 安装方法

不用复制命令。打开你用的 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、新平台 都可以"),不需要为它写新一行命令

## When This Skill Fires

- 用户提供一段思路、草稿、或语音转写文字
- 用户说"帮我写一篇公众号"、"润色一下"、"准备发布"
- 用户在公众号写作工作目录下工作(默认 `~/Library/Mobile Documents/com~apple~CloudDocs/my/我的项目/我的创作/wechat-publish/`,可由用户配置)

## Workflow

### Step 0: 接收输入

用户会以以下形式给你内容:
- 完整草稿(最常见)
- 几段散乱的思路 / bullet points
- 一段长文字,没有分段
- 语音转写(可能有错字、重复)

如果输入太散,**问一个问题**:"这是想写一篇文章,还是几个独立想法?" —— 但只问这一次。

### Step 1: 轻润色

打开一个 markdown 文件,把用户的内容粘进去。然后**只**做下面这些:

- 修错字("的得地"乱用、同音字错字、重复字"我我")
- 段落切分:每 1–3 句一段。微信里长段落很难读
- 拗口的地方做最小改动。如果改动后语气变了,宁可不改
- 标点统一:中文用全角逗号句号,英文/数字之间空格
- 保留原本的开头和结尾——这是作者的标志性特征

**改动的尺度参考:** 如果你改的字数超过原文的 5%,你改太多了。退回去。

### Step 2: 标题候选

给用户 **3 个标题候选**:

- A) 直白型:直接说文章讲什么
- B) 故事型:从一个场景或冲突切入
- C) 用户原文里的一句话:从草稿里摘最有味道的一句

不要做:标题党、夸张、"震惊"、"必看"。

### Step 3: 摘要 (50–80 字)

公众号摘要是发到朋友圈/对话框时的预览。要点:

- 不是文章第一段的复制
- 一句话说清楚读者会获得什么
- 用作者的语气,不是营销腔

### Step 4: 配图(每篇两张)

每篇文章配 **两张图**:

- **题图 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)。

**⚠️ 正文里除 `cover.png` / `illustration.png` 之外的图不会自动上 CDN。** `upload-draft.sh` 只对这两个固定文件名做 `upload_image` + 替换;如果文章正文引用了别的本地图(如用户给的 `img-xxx.png` 截图),`content.html` 里会留下 `./img-xxx.png` 本地路径 → 草稿里是裂图。处理:每张先 `md2wechat upload_image img-xxx.png --json` 拿到 `data.wechat_url`,再把 `article.md` 里的 `./img-xxx.png` 替换成那个 `http://mmbiz.qpic.cn/...` URL,然后正常 `upload-draft.sh`(带 https 的 img src 会原样保留)。验证:`grep -c mmbiz content.html` 应等于正文图片数,`grep -c 'img-' content.html` 应为 0。

默认插入位置:**正文最后落点之后**(默认无后注;如果有 `## 后注` 则放在后注之前;如果有 `## 安装方法` 则放在安装方法之前)。如果解释图只针对某一节(不是全文摘要),就紧跟在那一节正文之后。

**绝不要给解释图写引导语 / 说明文字。** 直接放 `![](./illustration.png)`,前面不写任何「整件事画出来,大概就是这样:」「如图所示」「整件事画起来是这样」之类的话——用户明确反复要求过,图自己说话,正文在文字落点收束即可。详见 [[no-illustration-caption]]。

> 安全网:如果生成了 `illustration.png` 但 `article.md` 漏掉了这一行引用,`upload-draft.sh` 会自动在最合适的位置(有后注则后注前,否则文末)插入引用并改写 `article.md`,确保结果幂等。但首选还是在 Step 5 写 `article.md` 时就把它放进去。

如果用户对某张图不满意,直接重跑对应脚本——每次结果不同。

### Step 5: 输出文件包

在用户的工作目录下(默认 `~/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
# 如果没有 pandoc:
# 用 Python 的 markdown 包 / Node 的 marked / 或手写最简实现
```

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

文章包准备好后,跑一行就能把文章作为草稿推到公众号后台:

```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` 作内部边界)
     - **硬规则:行尾绝不能是逗号「,」。** 段内分行的边界只能落在句末标点(。?!)之后;逗号说明这句话没说完,逗号后面的内容必须接着写在同一行,绝不换行。写完 `article.md` 后过一遍:任何一行以「,」结尾,就把下一行接上去(合成一行),直到句号。这是为了避免「…巨大的公司,↵ 才有了…」这种把一句话硬拆成两行、渲染成两个 `<br>` 的错误。
   - **结构性样式例外**(这些 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/Library/Mobile Documents/com~apple~CloudDocs/my/我的项目/我的创作/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 默认不加,保持作者原文清洁。

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

**⚠️ 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 拉留言](#step-8可选--cookie-fallback拉留言) 替代这一段。

**前提**:公众号是**企业主体认证**订阅号或服务号。个人主体认证(黄 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                    # 真群发 + 打开评论
# 等几分钟到几小时让粉丝看到 + 留言
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 直接拉,会撞这个)

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

**适用场景**:公众号是**个人主体认证**(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。两条路径,选一条:

#### 路径 A(推荐):复用 gstack 持久浏览器 — `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
# 一次性 setup(per machine):扫码登录 mp.weixin.qq.com 到 gstack profile
~/.claude/skills/gstack/browse/dist/browse goto https://mp.weixin.qq.com/
# 用手机微信扫码

# 一次性 setup(per article):保存 appmsgcomment URL 模板
# (URL 是 per-article 稳定的;token 部分脚本会每次从浏览器读最新值替换掉)
echo '<完整 appmsgcomment URL>' > <article-folder>/comment-url.txt

# 之后每次拉留言(零手工步骤):
~/.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/` + 扫码。

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

gstack 没装、或想一次性快速拉,可以直接走这条:

```bash
# 1. 浏览器抓包(一次性,几分钟)
#    a. 登录 mp.weixin.qq.com → 留言管理 → 找到目标文章
#    b. 打开 DevTools (Cmd+Opt+I) → Network 标签 → 筛选 Fetch/XHR
#    c. 在页面上翻一页评论,或点"加载更多"
#    d. 找请求 URL 含 'appmsgcomment' 的那条,右键 → Copy → "Copy as cURL (bash)"
#    e. 从 curl 命令里抠出 -H 'Cookie: ...' 那段(整段 cookie 字符串)和 URL

# 2. 跑脚本
~/.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 是源文件,下次改用这个。
```

## File Layout (skill 自身)

```
~/.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](https://github.com/jianshuo/claude-skills)(每次编辑后自动 commit + push)。

## Polish Heuristics (具体到字)

错字模式 → 改:
- "的得地" 误用:根据语法判断
- 重复字:"我我"、"是是"、"了了" → 删一个
- 同音字:考虑上下文("在"vs"再","做"vs"作")

段落切分时机:
- 一句话讲完一个意思,下一句换主语 → 分段
- 出现"但是"、"不过"、"所以"、"后来"在句首 → 考虑分段
- 一段超过 80 字 → 找最近的句号分

不要分段:
- 排比句、列举(保持节奏)
- 对话(按对话格式)

## Anti-Patterns (绝对不做)

| 不要 | 原因 |
|------|------|
| 把"我觉得"改成"笔者认为" | 改变了作者身份 |
| 加"小标题"打断行文 | 微信读者不需要导航 |
| 把口语句尾"吧/呢/啊"删掉 | 删掉就不是这个人写的了 |
| 在结尾加"欢迎关注"、"点赞在看" | 作者会自己决定要不要 |
| 把"今天"改成具体日期 | 作者用"今天"是有意为之 |
| 自己加举例 / 引用 / 数据 | 这是写作,不是补全 |
| 改动后没给 diff,直接全文输出 | 用户看不见你改了什么 |
| 文章超过 1500 字 | 多半是某段空例子在撑场。砍掉那段,看会不会自然回到 1000 字 |
| 为演示框架编一段完整例子 | 让 `illustration.png` 承担演示。正文只留点睛,不复述结构图 |

## Showing the Diff

每次润色完,**先告诉用户你改了什么**,再问要不要继续:

```
我改了 7 处:
1. L3: "我我觉得" → "我觉得"(重复字)
2. L8: 长段落 (120字) 拆成两段
3. L15: "通过…的方式" → "用…"(口语化保留)
...

要看完整结果吗?
```

如果改动 ≤ 3 处,可以直接给完整结果,不用列 diff。

## Running the Skill (实操步骤)

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. 输出发布指引

## Done When

- [ ] `articles/YYYY-MM-DD-{slug}/` 文件夹存在
- [ ] 包含 article.md、article.html、cover.png、meta.json、original.md
- [ ] meta.json 字段齐全
- [ ] 用户拿到了发布指引
- [ ] 用户没说"再改改"

Files in this skill

  • .gitignore65 B
  • .publish-manifest.json13.2 KB
  • LICENSE1 KB
  • README.md5.3 KB
  • SKILL.md36.3 KB
  • prompts/cover-prompt.md20.6 KB
  • prompts/illustration-prompt.md2.8 KB
  • scripts/fetch-comments-by-cookie.sh12.2 KB
  • scripts/fetch-comments-via-gstack.sh10.8 KB
  • scripts/fetch-comments.sh5.4 KB
  • scripts/gen-cover-ai.sh3.8 KB
  • scripts/gen-illustration.sh2.8 KB
  • scripts/mass-send.sh7.2 KB
  • scripts/pangu.py2.9 KB
  • scripts/publish.sh4.2 KB
  • scripts/update-draft-via-api.py3.9 KB
  • scripts/upload-draft.sh14.4 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.