# Wechat Publisher

> 公众号文章发布工具。当用户说"存到公众号"、"发布到公众号"、"发到草稿箱"、"推到公众号"、"写到公众号"时触发。配合 kakarot-writer skill 使用：先用 kakarot-writer 风格生成 markdown 文章 → 用 wechat-publisher CLI 格式化并存入微信草稿箱。也支持纯 markdown 文件直接发布。 触发词：存到公众号、发布到公众号、发到草稿箱、推到公众号、写到公众号、发公众号、wechat publish 不触发情况：纯写作不涉及发布、不需要存草稿箱的情况。

- Skill: `zhangs-11/wechat-publisher` (Agent Skill, multi-file: 28 files)
- Install (CLI): `npx skillmds@latest add zhangs-11/wechat-publisher`
- Raw SKILL.md: https://api.skillmd.com/api/skills/zhangs-11/wechat-publisher/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: Zhangs-11 (https://skillmd.com/u/zhangs-11)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/zhangs-11/wechat-publisher

---


# wechat-publisher 公众号发布工具

作者在「卡卡罗特学AI」公众号写文章，写完后需要一键存到微信草稿箱，不用手动复制粘贴。

## 架构概览

```
kakarot-writer skill 生成 markdown 文章
       │
       ▼
复用 writer 已交付的正文图 + 21:9 封面
       │（素材缺失时才补图）
       ▼
guizang-social-card-skill 或 scripts/generate_wechat_images.py
       │
       ▼
wechat-publisher create --title "xxx" --content-file xxx.md --cover-file images/<文章名>/cover.jpg
       │
       ├─ formatter.py  →  Markdown → 微信兼容 HTML
       ├─ token.py      →  access_token 缓存管理
       ├─ client.py     →  微信 API HTTP 调用
       └─ 微信服务器     →  草稿箱 +1
```

## 安装

仅首次安装时执行。如果用户已经装过，跳过这一步。

### 1. 安装 Python 包

```bash
cd tools/wechat-publisher
python3 -m venv venv
venv/bin/pip install -e .
```

### 2. 创建配置文件

在 `~/.wechat-publisher/.env` 中写入：

```bash
WECHAT_APP_ID=wx你的AppID
WECHAT_APP_SECRET=你的AppSecret
WECHAT_AUTHOR=卡卡罗特学AI
WECHAT_DEFAULT_COVER_MEDIA_ID=你的封面media_id
```

**注意：** 密钥在用户本地 `~/.wechat-publisher/.env`，不在仓库里。`WECHAT_DEFAULT_COVER_MEDIA_ID` 先用 `wechat-publisher upload-cover` 上传一张封面图获取 media_id 再填入。

### 3. 添加 PATH

如果 `wechat-publisher` 命令找不到，用全路径：

```bash
# 或建立软链接
ln -sf $(pwd)/tools/wechat-publisher/venv/bin/wechat-publisher ~/.local/bin/wechat-publisher
```

### 4. IP 白名单

微信 API 要求调用方的公网 IP 在公众号后台白名单中。如果遇到 `40164` 错误，告诉用户当前 IP 并让用户去 mp.weixin.qq.com → 开发 → 基本配置 → IP 白名单 添加。

## 工作流

当用户说"写一篇 XX 文章存到公众号"时，必须按以下步骤执行：

### 第一步：写作
调用 kakarot-writer skill 生成 markdown 文章。

标题、事实边界、文章主线和口语风格以 kakarot-writer 的完整规则为准。发布 skill 不再重复发明另一套写作规则，避免两个 skill 互相冲突。

### 发布前编辑检查

进入配图前，确认文章已经完成 kakarot-writer 的自检，尤其检查：

- 标题承诺能被正文证据兑现，没有暗示不存在的实测或独家经历。
- 没有把推测写成亲历；数据、发布日期、榜单和引用没有未说明的事实缺口。
- 前三段已经建立事件、阅读理由或作者关系，全文围绕同一条主线推进。
- 口语来自真实语境，没有为了模仿作者批量堆叠口癖和夸张标点。

如果存在事实缺口，先向用户确认或收窄断言，不要带着问题进入发布流程。

如果 `kakarot-writer` 已交付正文图片、截图和封面，直接复用，不重新生成或覆盖。仍缺少素材时才保留下列占位标记：

```markdown
[插图：图片内容描述]
[绘图提示：可复制的英文 prompt，适合 Midjourney / DALL-E / 即梦 等生图工具]
```

例：
```markdown
[插图：传统RAG工作流程图]
[绘图提示：A clean technical diagram showing the classic RAG pipeline, flat design with blue and white color scheme, modern minimalist style.]
```

这告诉用户两个信息：**哪里需要配图**、**用什么 prompt 生成图片**。

**绘图提示的写法（重要）：**
- **有创意，用视觉比喻概括语义**，不要把正文文字照搬进去。prompt 描述的是「一个画面/场景」，不是「这段话写了啥」。
- **纯英文**。中文一旦进 prompt，生图模型 Z-Image-Turbo 会把中文直接画到图上，且常带错别字。
- **画面里不要出现任何文字**（prompt 末尾脚本会自动追加强力的「无文字」约束 + negative_prompt，你自己写的时候也别要求图上有字）。
- 例（解释「海量上下文被收束成清晰的推理」）：`A vast turbulent cloud of glowing particles funneling through a sleek device into one calm focused beam of blue light, flat editorial illustration, blue palette, no text`

### 第二步：保存
将文章保存到 `~/公众号草稿/` 目录。

### 第三步：检查并补齐图片

先检查文章中引用的本地图片是否存在，并确认已有 `21:9` 主封面。只要 `kakarot-writer` 已完成真实截图、正文图和封面，就跳过生图脚本，避免把已经确认的素材替换成概念图。

素材不完整时，优先回到 `kakarot-writer` 的交付清单补齐；封面优先调用 `guizang-social-card-skill` 的真实素材 B 方案。只有专用 Skill 不可用、且文章仍缺少必要图片时，才使用本 Skill 自带的 SiliconFlow 回退脚本，默认模型是 `Tongyi-MAI/Z-Image-Turbo`。

如果文章包含 `[插图：...]` / `[绘图提示：...]`，脚本会按这些 prompt 生成对应正文图（**优先走这条**，因为 prompt 是你手写的、有创意）。如果文章没有占位符，脚本会按正文段落自动插入 3 张配图，并生成封面图——auto 兜底通道会先用对话模型（`deepseek-ai/DeepSeek-V3`）把中文段落转成英文视觉概念再生图，**绝不把中文塞进画面**，从根上避免图上出现原文和错别字。封面也走概念化（不再把标题原文塞进 prompt，并去掉「杂志封面」这类诱导加标题字的措辞，缩写如 AI/GPT 也会被剔除）。封面对文字最敏感，**要最稳就手动传 `--cover-prompt "英文创意概念"`**。

**图片目录**：每篇文章的图存在以文件名命名的独立子目录 `images/<文章名>/` 下（封面 `cover.jpg`、正文 `01-image.jpg`…），多篇之间不会再互相覆盖。

> 所有生图 prompt 末尾都会自动叠加统一创作方向（视觉比喻、蓝色调、画面零文字）+ `negative_prompt` 负向词，进一步压制文字渲染。

**密钥只放在运行环境中，不写入仓库或文章文件。**

```bash
export SILICONFLOW_API_KEY="用户提供的 SiliconFlow API Key"
export WECHAT_PUBLISHER_SKILL_DIR="${WECHAT_PUBLISHER_SKILL_DIR:-$HOME/.codex/skills/wechat-publisher}"
export WECHAT_PUBLISHER_PYTHON="$WECHAT_PUBLISHER_SKILL_DIR/tools/wechat-publisher/venv/bin/python"
export WECHAT_PUBLISHER_BIN="$WECHAT_PUBLISHER_SKILL_DIR/tools/wechat-publisher/venv/bin/wechat-publisher"

"$WECHAT_PUBLISHER_PYTHON" "$WECHAT_PUBLISHER_SKILL_DIR/scripts/generate_wechat_images.py" \
  --article ~/公众号草稿/文件名.md \
  --title "文章标题" \
  --auto-insert 3
```

脚本会：

1. 调用 `https://api.siliconflow.cn/v1/images/generations` 生成图片。
2. 立即下载图片到 `~/公众号草稿/images/<文章名>/`，不要只保存临时 URL。
3. 把正文占位符替换成真实 Markdown 图片；没有占位符时，自动在正文段落后插入配图，例如 `![配图1](images/<文章名>/01-image.jpg)`。
4. 生成封面图 `images/<文章名>/cover.jpg`。

**硬性检查：** 继续发布前，文章中的所有图片路径都必须存在，所有占位符都已替换，并且有可用封面文件或 `cover_media_id`。只有实际执行回退脚本时，才要求命令输出至少一个 `IMAGE: ...` 和一个 `COVER: ...`。否则停止并报告缺失项。

### 第四步：对抗式事实审查（强制，不可跳过）

发布到草稿箱之前，必须自动跑一轮对抗式审查：

1. 派 2 个独立审查 agent（带 WebSearch/WebFetch），立场设定为「假定作者写错/编造，逐条推翻」：一个专攻**数字/榜单/价格/时间**，一个专攻**引语/人物身份/定性表述**
2. 审查结论与原始调研冲突时，必须亲自去一手来源（官方原文）定案，不采信任何单一 agent
3. 绝对化表述（「没法刷」「史上最X」「全球第一」）要么有一手来源，要么加「有分析说」「据官方说法」限定
4. 全部修正后才进入发布步骤，发布后把审查结果汇报给用户（❌抓出的错误 / ⚠️措辞修正 / ✅确认无误清单）

### 第五步：预检并尝试发布

`wechat-publisher` 会自动上传正文 Markdown 图片到微信 CDN。封面图用 `--cover-file` 上传成微信永久素材，再用返回的 `media_id` 创建草稿。已经是 `mmbiz.qpic.cn` 的图片不会重复上传。

发布前，CLI 会先执行统一的内容契约：移除开头 YAML frontmatter，只消费 `<!-- kakarot:delivery-appendix -->` 之前的公开正文，并拒绝正文里的一级标题、未隔离的截图清单/封面方案/备选标题/事实确认项。标题只通过 `--title` 传入，因此不会在正文中重复。摘要提取、图片上传、外链检查和 HTML 格式化全部使用同一份公开正文。

外链默认逐一做只读可达性检查，`404` 等失效链接会阻止上传。只有网络环境暂时不可用、且已经用其他方式逐个核验链接时，才能临时传 `--skip-link-check`；它不是常规发布选项。

```bash
# 检查当前公网 IP
curl -s ip.sb

# 只读预检，不上传图片、不创建草稿
"$WECHAT_PUBLISHER_BIN" preflight \
  --title "文章标题" \
  --content-file ~/公众号草稿/文件名.md \
  --cover-file ~/公众号草稿/images/<文章名>/cover.jpg

# 尝试发布
"$WECHAT_PUBLISHER_BIN" create \
  --title "文章标题" \
  --content-file ~/公众号草稿/文件名.md \
  --cover-file ~/公众号草稿/images/<文章名>/cover.jpg \
  --digest "120字以内摘要"
```

### 第六步：结果处理

**成功** → 告知用户 `SUCCESS: Draft created (media_id=xxx)`

**失败（40164 IP白名单）** → 告诉用户当前IP，让用户去微信后台添加白名单，然后重新运行发布命令

**失败（其他错误）** → 根据错误信息处理

**读取超时** → 创建或更新结果未知。先检查公众号草稿箱，不要立刻重复执行，以免生成重复草稿。

## 命令参考

### create — 创建草稿

```bash
# 从文件
wechat-publisher create --title "标题" --content-file article.md

# 自动上传封面文件
wechat-publisher create --title "标题" --content-file article.md --cover-file images/<文章名>/cover.jpg

# 从管道
cat article.md | wechat-publisher create --title "标题"
```

### update — 更新现有草稿

```bash
wechat-publisher update --media-id "xxx" --title "新标题" --content-file article.md --cover-file images/<文章名>/cover.jpg
```

**⚠️ 必须每次都带 `--cover-file` 或 `--cover-media-id`**，否则封面会被重置为默认封面（WECHAT_DEFAULT_COVER_MEDIA_ID）。只改正文/标题时也不能省。

### upload-image — 上传正文图片（返回 CDN URL）

```bash
wechat-publisher upload-image photo.jpg
# → SUCCESS: Image uploaded → http://mmbiz.qpic.cn/...
```

### upload-cover — 上传封面图（返回 media_id）

```bash
wechat-publisher upload-cover cover.jpg
# → SUCCESS: Cover uploaded (media_id=xxx)
```

## 样式机制

`formatter.py` 自动将 markdown 转为微信内联样式 HTML：

| 元素 | 效果 |
|------|------|
| 正文段落 | 16px, 深灰, 行距 1.85 |
| `===高亮===` | 蓝色渐变底强调 |
| `==注意小句==` | 仅把字变成主题蓝（无底色、不加粗），标关键句用，代替加粗 |
| `> 引用` | 💡 蓝色竖线卡片 |
| `## 二级标题` | 左竖线小标题：蓝色左竖线 + 同色蓝字 + 加粗，字号与正文一致（前置图标，标题没带 emoji 会自动补 🔹） |
| `### 三级标题` | 同款，竖线略细 |
| `---` | `· · ·` 分隔符 |
| `` `代码` `` | 浅灰底圆角代码 |
| ```代码块``` | 深色圆角代码块 |
| `**加粗**` | 700 字重深色 |
| `*斜体*` | 斜体 |
| 表格 | 微信兼容表格样式 |
| 外链 | 正文标注序号，底部生成参考资料 |

## 内容质检规则

高流量 AI 公众号文章发布前要做四项检查：

1. 标题有具体对象、反常识或真实体验，不使用空泛震惊体。
2. 前三行必须交代“发生了什么”和“为什么值得读”。
3. 只强调真正决定读者理解的关键判断，避免按固定段数机械加粗；跨平台母稿优先使用通用的 `**加粗**`，不主动加入平台私有语法。
4. 配图必须服务理解，且**图里不要出现任何文字**（生图模型画中文会出错别字）。优先用视觉比喻/概念图传达意思，绘图提示用纯英文手写、有创意，不要把正文照搬进 prompt。
5. 一手来源标为 POC、Preview、beta、experimental 或 RC 的能力保留成熟度限定；事实、官方说法和作者推断分层表达。
6. 官方仓库与文档的具体链接必须通过预检实际打开；不能只凭记忆猜默认分支是 `main` 或 `master`。

## 常见错误

| 错误 | 原因 | 处理 |
|------|------|------|
| 40164 | IP 不在白名单 | 获取当前 IP，让用户添加白名单 |
| 40007 invalid media_id | 封面 media_id 无效或为空 | 上传封面图获取正确的 media_id |
| 40001 | token 过期或无效 | 会自动刷新，持久失败检查 appsecret 是否正确 |
| 45009 | 接口频率超限 | 会自动重试 |

## 发布前失败保护

- 如果缺少封面，命令会失败并提示配置 `WECHAT_DEFAULT_COVER_MEDIA_ID`、传 `--cover-media-id`，或传 `--cover-file`。
- 如果正文仍包含 `[插图：...]` / `[绘图提示：...]`，命令会失败，防止半成品进入草稿箱。
- 如果遇到 40164，命令会提示去微信后台添加当前公网 IP 白名单。

