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 包
cd tools/wechat-publisher
python3 -m venv venv
venv/bin/pip install -e .
2. 创建配置文件
在 ~/.wechat-publisher/.env 中写入:
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 命令找不到,用全路径:
# 或建立软链接
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 已交付正文图片、截图和封面,直接复用,不重新生成或覆盖。仍缺少素材时才保留下列占位标记:
[插图:图片内容描述]
[绘图提示:可复制的英文 prompt,适合 Midjourney / DALL-E / 即梦 等生图工具]
例:
[插图:传统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负向词,进一步压制文字渲染。
密钥只放在运行环境中,不写入仓库或文章文件。
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
脚本会:
- 调用
https://api.siliconflow.cn/v1/images/generations生成图片。 - 立即下载图片到
~/公众号草稿/images/<文章名>/,不要只保存临时 URL。 - 把正文占位符替换成真实 Markdown 图片;没有占位符时,自动在正文段落后插入配图,例如
。 - 生成封面图
images/<文章名>/cover.jpg。
硬性检查: 继续发布前,文章中的所有图片路径都必须存在,所有占位符都已替换,并且有可用封面文件或 cover_media_id。只有实际执行回退脚本时,才要求命令输出至少一个 IMAGE: ... 和一个 COVER: ...。否则停止并报告缺失项。
第四步:对抗式事实审查(强制,不可跳过)
发布到草稿箱之前,必须自动跑一轮对抗式审查:
- 派 2 个独立审查 agent(带 WebSearch/WebFetch),立场设定为「假定作者写错/编造,逐条推翻」:一个专攻数字/榜单/价格/时间,一个专攻引语/人物身份/定性表述
- 审查结论与原始调研冲突时,必须亲自去一手来源(官方原文)定案,不采信任何单一 agent
- 绝对化表述(「没法刷」「史上最X」「全球第一」)要么有一手来源,要么加「有分析说」「据官方说法」限定
- 全部修正后才进入发布步骤,发布后把审查结果汇报给用户(❌抓出的错误 / ⚠️措辞修正 / ✅确认无误清单)
第五步:预检并尝试发布
wechat-publisher 会自动上传正文 Markdown 图片到微信 CDN。封面图用 --cover-file 上传成微信永久素材,再用返回的 media_id 创建草稿。已经是 mmbiz.qpic.cn 的图片不会重复上传。
发布前,CLI 会先执行统一的内容契约:移除开头 YAML frontmatter,只消费 <!-- kakarot:delivery-appendix --> 之前的公开正文,并拒绝正文里的一级标题、未隔离的截图清单/封面方案/备选标题/事实确认项。标题只通过 --title 传入,因此不会在正文中重复。摘要提取、图片上传、外链检查和 HTML 格式化全部使用同一份公开正文。
外链默认逐一做只读可达性检查,404 等失效链接会阻止上传。只有网络环境暂时不可用、且已经用其他方式逐个核验链接时,才能临时传 --skip-link-check;它不是常规发布选项。
# 检查当前公网 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 — 创建草稿
# 从文件
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 — 更新现有草稿
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)
wechat-publisher upload-image photo.jpg
# → SUCCESS: Image uploaded → http://mmbiz.qpic.cn/...
upload-cover — 上传封面图(返回 media_id)
wechat-publisher upload-cover cover.jpg
# → SUCCESS: Cover uploaded (media_id=xxx)
样式机制
formatter.py 自动将 markdown 转为微信内联样式 HTML:
| 元素 | 效果 |
|---|---|
| 正文段落 | 16px, 深灰, 行距 1.85 |
===高亮=== |
蓝色渐变底强调 |
==注意小句== |
仅把字变成主题蓝(无底色、不加粗),标关键句用,代替加粗 |
> 引用 |
💡 蓝色竖线卡片 |
## 二级标题 |
左竖线小标题:蓝色左竖线 + 同色蓝字 + 加粗,字号与正文一致(前置图标,标题没带 emoji 会自动补 🔹) |
### 三级标题 |
同款,竖线略细 |
--- |
· · · 分隔符 |
`代码` |
浅灰底圆角代码 |
代码块 |
深色圆角代码块 |
**加粗** |
700 字重深色 |
*斜体* |
斜体 |
| 表格 | 微信兼容表格样式 |
| 外链 | 正文标注序号,底部生成参考资料 |
内容质检规则
高流量 AI 公众号文章发布前要做四项检查:
- 标题有具体对象、反常识或真实体验,不使用空泛震惊体。
- 前三行必须交代“发生了什么”和“为什么值得读”。
- 只强调真正决定读者理解的关键判断,避免按固定段数机械加粗;跨平台母稿优先使用通用的
**加粗**,不主动加入平台私有语法。 - 配图必须服务理解,且图里不要出现任何文字(生图模型画中文会出错别字)。优先用视觉比喻/概念图传达意思,绘图提示用纯英文手写、有创意,不要把正文照搬进 prompt。
- 一手来源标为 POC、Preview、beta、experimental 或 RC 的能力保留成熟度限定;事实、官方说法和作者推断分层表达。
- 官方仓库与文档的具体链接必须通过预检实际打开;不能只凭记忆猜默认分支是
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 白名单。