| name | wechat-official-account |
| description | 微信公众号运营管理 — 素材管理、文章导出、草稿创建、发布尝试、token维护。覆盖开放平台API的素材操作与发布链路。 |
| author | wangchen |
| version | 1.1.0 |
| credentials | [{"name":"WECHAT_APPID","description":"微信公众号appid(填入你的)"},{"name":"WECHAT_APPSECRET","description":"微信公众号appsecret(填入你的)"}] |
微信公众号运营管理
通过微信开放平台官方API操作自己的公众号。适用于文章导出、素材管理、数据分析等场景。
⚠️ 仅限操作自己的公众号 — API绑定了公众号的appid/appsecret,无法抓取别人的号。
API基础
获取access_token
⚠️ 优先使用 stable_token 接口(POST JSON),旧接口偶发 40001 无效凭证错误。
TOKEN=$(curl -s -X POST "https://api.weixin.qq.com/cgi-bin/stable_token" \
-H "Content-Type: application/json" \
-d "{\"grant_type\":\"client_credential\",\"appid\":\"$APPID\",\"secret\":\"$APPSECRET\"}" \
| python3 -c "import json,sys;print(json.load(sys.stdin).get('access_token',''))")
- 有效期 7200秒(2小时)
- 过期需重新获取,没有refresh_token机制
- 每天调用有限额,普通号2000次/天
- 在 execute_code 中每次调用必须重新获取 token,跨进程不共享,之前获取的 token 在新进程中无效
获取素材列表
curl -s -X POST "https://api.weixin.qq.com/cgi-bin/material/batchget_material?access_token=$TOKEN" \
-H "Content-Type: application/json" \
-d '{"type":"news","offset":0,"count":20}'
回包结构:
{
"total_count": 21,
"item_count": 20,
"item": [
{
"media_id": "xxx",
"update_time": 1629034241, <-- ⚠️ 这是正确的发布时间
"content": {
"news_item": [
{
"title": "北漂回忆2",
"update_time": 0, <-- ⚠️ 这里是0!不要用这个!
"create_time": 0, <-- ⚠️ 这里也是0!
"url": "https://..."
}
]
}
}
]
}
获取单篇素材内容
curl -s -X POST "https://api.weixin.qq.com/cgi-bin/material/get_material?access_token=$TOKEN" \
-H "Content-Type: application/json" \
-d '{"media_id":"xxx"}'
返回 news_item[0].content 字段包含文章全文HTML。
文章发布API
微信开放平台提供草稿箱API用于创建和发布文章。
上传图片
curl -s -F "media=@cover.png" \
"https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=$TOKEN&type=image"
curl -s -F "media=@chart.png" \
"https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token=$TOKEN"
创建草稿
curl -s -X POST "https://api.weixin.qq.com/cgi-bin/draft/add?access_token=$TOKEN" \
-H "Content-Type: application/json; charset=utf-8" \
-d '{
"articles": [{
"title": "标题",
"thumb_media_id": "封面media_id",
"author": "作者",
"digest": "摘要",
"show_cover_pic": 1,
"content": "<section>HTML内容</section>",
"content_source_url": "",
"need_open_comment": 1,
"only_fans_can_comment": 0
}]
}'
⚠️ 45003 / 45004 长度限制陷阱:
title 和 digest 限制按 UTF-8 字节 计算,不是字符数
- 一个中文字 = 3 字节,标题上限约 20 个中文字(64 字节),摘要上限约 40 个中文字(120 字节)
- 报
45003 title size out of limit → 标题字节超了,缩减即可
- 报
45004 description size out of limit → 摘要字节超了
提交发布(⚠️ 需权限)
curl -s -X POST "https://api.weixin.qq.com/cgi-bin/freepublish/submit?access_token=$TOKEN" \
-H "Content-Type: application/json" \
-d '{"media_id":"draft_xxx"}'
48001 权限错误:表示公众号未获得发布API权限。需要:
- 完成微信认证(加V的认证服务号)
- 设置IP白名单(开发→基本配置→IP白名单)
- 若未认证,草稿创建成功后通知用户手动群发
封面图生成
无设计工具时可用 headless Chrome 截图 HTML 生成封面:
google-chrome --headless --disable-gpu --no-sandbox \
--screenshot=cover.png --window-size=900,383 cover.html
curl -F "media=@cover.png" \
"https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=$TOKEN&type=image"
封面素材建议:900×383(2.35:1),深蓝/暗色系保持与排版风格一致。
直接调用 scripts/cover_generate.py 一键生成:
python3 scripts/cover_generate.py "主标题" "副标题" [可选小标] [/tmp/cover.png]
模板文件:templates/cover.html(HTML/CSS 模板) + templates/draft_payload.json(完整 draft/add payload 示例)。
HTML body 提取
生成的文章 HTML 通常是完整文档(含 <html><body>...</body></html>),创建草稿时需要只取 body 内容:
import re
m = re.search(r'<body>(.*?)</body>', html, re.DOTALL)
body = m.group(1).strip()
文章内容HTML注意事项
微信图文编辑器只支持有限的HTML标签:
- 用
<section> 包裹整体结构
- 标题用
<h2>/<h3> + 样式(微信不支持h4)
- 表格用
<table> + 内联样式
- 图片用
<img> + <section style="text-align:center"> 居中
- 提示框用
background + border-left 模拟
- 颜色用十六进制内联样式
不要用的标签: <div> 在微信中可能渲染异常,统一用 <section>。
⚠️ API 推送 vs 手动粘贴的重大差异:
| 问题 | 手动粘贴到编辑器 | API 创建草稿 |
|---|
| 深色主题(#1a1a2e底+浅灰字) | 正常显示 | 变成乱码/看不清 |
<tr style="background:..."> 表格表头底色 | 正常 | 被剥离,显示为白底 |
<section> 嵌套背景色 | 正常 | 可能被重置 |
对策——API 推送文章必须用浅色/白底配色方案:
- 主背景:白色
#fff,不设外层深色底
- 表头背景必须写在
<th style="background-color:#1a1a1a;"> 上,不能写在 <tr> 上
- 正文颜色用
#333/#555/#34495e 等深色,确保白底上可读
- 深色块仅用于结尾引用框等局部区域(
<blockquote> + 白字),不影响全局可读性
linear-gradient 等复杂 CSS 在 API 推送时不可靠,用纯色
完整端到端流程(推一篇公众号文章的最小代码)
import os, re, json, subprocess, requests
APPID = "your_appid_here"
APPSECRET = "your_appsecret_here"
AUTHOR = "王晨"
r = requests.post(
"https://api.weixin.qq.com/cgi-bin/stable_token",
json={"grant_type": "client_credential", "appid": APPID, "secret": APPSECRET},
timeout=15,
)
token = r.json()["access_token"]
with open("/tmp/article.html", "r", encoding="utf-8") as f:
html = f.read()
m = re.search(r"<body[^>]*>(.*?)</body>", html, re.DOTALL)
body = m.group(1).strip() if m else html
title = "标题"
digest = "摘要"
print(len(title.encode("utf-8")), "/ 64 ", len(digest.encode("utf-8")), "/ 120")
r = requests.post(
f"https://api.weixin.qq.com/cgi-bin/material/add_material?access_token={token}&type=image",
files={"media": ("cover.png", open("/tmp/cover.png", "rb"), "image/png")},
timeout=30,
)
thumb_media_id = r.json()[]
article = {: [{
: title,
: thumb_media_id,
: AUTHOR,
: digest,
: ,
: body,
: ,
: ,
: ,
}]}
r = requests.post(
,
data=json.dumps(article, ensure_ascii=).encode(),
headers={: },
timeout=,
)
(r.json())
标题/摘要字节限制的实测值(2026-06-06 推送"尾盘买卖逻辑与技巧"实测通过):
| 字段 | 上限 | 实测推送 | 备注 |
|---|
| title | 64 字节 | 63 字节(21 汉字) | 45003 超限 |
| digest | 120 字节 | 120 字节(40 汉字) | 45004 超限 |
| author | 20 字节 | "王晨"(6 字节) | — |
⚠️ 早期记忆曾记错 digest 为 36 字节(实际是 120)。如果你看到 36 一定是错记,以本 skill 为准。
⚠️ 推封面时的命名边界
绝不要在封面上擅自加系列名/期数,比如"交易笔记 · 第 12 期"——这不是用户告诉你的具体系列。封面顶部小标只放用户明确指定的字样,没有就只留主副标题。
<div class="tag">交易笔记 · 第 12 期</div>
<div class="title">尾盘买卖逻辑与技巧</div>
<div class="sub">从理论到实战的系统整理</div>
如果用户给的是多期文章系列(用户自己说过"这是第 X 期"),才写小标;否则一律不写。
草稿创建编码要求
必须显式指定 UTF-8 编码和 Content-Type charset:
payload = json.dumps(article, ensure_ascii=False).encode("utf-8")
r = requests.post(url, data=payload,
headers={"Content-Type": "application/json; charset=utf-8"})
r = requests.post(url, json=article)
原因: 使用 requests.post(json=...) 时 Content-Type 可能不带 charset=utf-8,导致中文在微信端解析为乱码。
execute_code 中文引号陷阱
在 execute_code 的 Python 字符串中使用中文引号 "" 会导致 SyntaxError,因为 Python 解析器把 " 当成字符串边界:
title = "别再9:30才盯盘了!真正的高手,早在9:15就已看穿一切"
title = '别再9:30才盯盘了!真正的高手,早在9:15就已看穿一切'
title = '\u201c过年加油站\u201d:情绪周期五阶段'
规则:execute_code 中的字符串如果包含中文引号 "",要么用单引号包裹,要么用 \u201c / \u201d 转义。
中文引号 SyntaxError 陷阱
当标题含中文弯引号 "" 时,在 Python 字符串中会触发 SyntaxError——因为 " 看起来像普通双引号 ",导致 Python 误解析字符串边界:
title = "一年45倍!Serenity的"瓶颈理论",A股怎么用?"
title = '一年45倍!Serenity的\u201c瓶颈理论\u201d,A股怎么用?'
title = '一年45倍!Serenity的"瓶颈理论",A股怎么用?'
这个坑也适用于 digest、author 等任何含中文标点的字符串参数。在 execute_code 沙箱中写 JSON payload 时尤其要留意,因为沙箱会将代码作为普通 Python 解析。
草稿文章更新 vs 替换策略
微信 API 没有"更新草稿"的接口。需要替换一篇文章时只能:
- 用
draft/delete 删除旧草稿
- 重新
draft/add 创建新草稿
requests.post(f"https://api.weixin.qq.com/cgi-bin/draft/delete?access_token={token}",
json={"media_id": "draft_xxxxx"})
requests.post(f"https://api.weixin.qq.com/cgi-bin/draft/add?access_token={token}", ...)
文中插图完整流程
封面图和文中插图走不同的 API:
| 用途 | API | 返回 | 用途 |
|---|
| 封面 | material/add_material?type=image | media_id(用于 thumb_media_id) | 文章封面图 |
| 正文插图 | media/uploadimg | url(mmbiz.qpic.cn CDN 链接) | 嵌入 HTML 的 <img> |
完整流程:
with open("image.png", "rb") as f:
r = requests.post(
f"https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token={token}",
files={"media": ("image.png", f, "image/png")})
img_url = r.json()["url"]
article_html = f'''
<section style="text-align:center;margin:20px 0;">
<img src="{img_url}" style="max-width:100%;height:auto;display:block;margin:0 auto;">
</section>
'''
payload = json.dumps({"articles": [{
"title": title, "content": body
}]}, ensure_ascii=False).encode("utf-8")
⚠️ 图文草稿不显示图的排查流程(实战经验 2026-06-06)
症状: API推送草稿后,文章里 <img> 标签在 source 里都有,URL访问也是200,但草稿箱预览看不到图。
根因(按发生概率从高到低):
| # | 原因 | 验证方法 | 修复 |
|---|
| 1 | 草稿箱页面缓存 — 微信草稿箱编辑页有强缓存 | F5/Ctrl+Shift+R 强制刷新草稿箱页面 | 刷新即可 |
| 2 | 图片CDN URL已过期 — media/uploadimg 返回的URL生命周期较短 | curl测试URL状态码,200说明有效 | 重新 media/uploadimg 上传,用新URL重新 draft/add |
| 3 | 图被推到 sz_mmbiz_png/ 子目录(不一定有,但有时URL会被改写) | 在草稿source里检查实际URL | 重新上传并使用新URL |
| 4 | 图片太大未自动展开 — 高图(如>2000px的K线图)默认折叠 | 在草稿编辑器里点开"显示更多图片" | 不需要修复,群发后会自动展开 |
| 5 | 图片URL被微信重写 — 偶尔URL会被改成 __biz 参数形式 | 在草稿编辑器里右键看图实际URL | 重新上传并推送 |
通用排错流程:
import re
draft = get_draft(media_id)
imgs = re.findall(r'<img[^>]+src="([^"]+)"', draft["content"])
print(f"找到 {len(imgs)} 张图")
for i, src in enumerate(imgs, 1):
r = requests.get(src)
print(f"图{i}: {r.status_code} | {len(r.content)} bytes")
最稳的做法:重传+重建
requests.post(f"https://api.weixin.qq.com/cgi-bin/draft/delete?access_token={token}",
json={"media_id": old_media_id})
for img in images:
r = requests.post(
f"https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token={token}",
files={"media": img})
img_urls.append(r.json()["url"])
html = rebuild_html(content, img_urls)
requests.post(f"https://api.weixin.qq.com/cgi-bin/draft/add?access_token={token}",
data=json.dumps({"articles":[{"content": html, ...}]}, ensure_ascii=False).encode("utf-8"),
headers={"Content-Type": "application/json; charset=utf-8"})
关键教训:
- 图文草稿显示问题,重传+重建 是最快的解决方式(< 10秒)
- 草稿source里有图 ≠ 草稿预览显示图 — 两件事要分开验证
- 群发后图片正常显示的概率 > 99%,草稿预览是"参考性"的
### ⚠️ 列表项之间不能有换行/空白(2026-06-06 实测)
**症状:** 推送的 ul/ol 块在草稿箱显示为"每个 li 后面跟一个空 li"。实测:22 个列表块,原始 74 个 li 在草稿里变成 172 个,其中 97 个是空的。
**根因:** 微信 API 存储时会把 `</li>` 和 `<li>` 之间的换行/空白字符(`\n`、空格、制表符)**自动转成空 `<li>`**。
**验证(实测通过):**
- 原始 HTML(li 间有 `\n`)→ 推送后 172 li,97 空
- 修复 HTML(li 间无空白,全部 `</li><li>` 紧挨着)→ 推送后 74 li,0 空
**修复方案:**
```python
import re
new_body = re.sub(
r'<(ul|ol)([^>]*?)>(.*?)</\1>',
lambda m: f'<{m.group(1)}{m.group(2)}>' + re.sub(r'</li>\s+<li', '</li><li', m.group(3)) + f'</{m.group(1)}>',
body,
flags=re.DOTALL
)
规则:以后写带 ul/ol 的文章 HTML 时,li 之间绝对不能留任何空白/换行。
⚠️ 补充:li 内部也不能有纯空白内容(2026-06-06 第二波修复)
症状补充: 即使 li 之间没有空白,有些 <li> 自身内部只含 \n(如 <li>\n</li>),会被微信 API 解析为可见的空列表项。
修复: 推草稿前统一清洗:
new_body = re.sub(r'<li[^>]*>\s*</li>', '', body)
完整清洗流程(推荐):
body = re.sub(r'</li>\\s+<li', '</li><li', body)
body = re.sub(r'<li[^>]*>\\s*</li>', '', body)
草稿乱码修复
从 draft/batchget 拉取的内容如果编码异常(中文字符显示为 æä¹æ 等乱码),是因为内容被以 latin-1 编码存储但实际是 UTF-8 字节。修复方法:
with open("draft_content.html", "r", encoding="utf-8") as f:
raw = f.read()
fixed = raw.encode('latin-1').decode('utf-8')
常见原因: 通过 ima 知识库导出或 AI 助手直接粘贴的含中文 HTML 内容,在通过微信 API 存储时发生了编码 mis-match。修复后重新 draft/add 即可。
⚠️ 从文档生成文章的核心规则
来源文档→公众号文章的成文规则(2026-06-06 用户明确)
-
文章内容中不要出现文章标题。 标题只设置在微信草稿的 title 字段中,HTML 内容 body 里不写任何 <h1> 或等效的标题文本。正文直接从一个引导块或引言开始。
-
尽量不删减原文内容。 从腾讯文档、知识库或任何来源转换文章时,保持原文的完整结构、段落、数据表格和结论。可以调整排版格式(段落划分、标题层级、表格美化),但不能删除、合并、改写核心内容点。
-
清理AI对话前缀。 腾讯文档的内容通常以 用户:...ima:... 开头,创建文章前要去掉这些对话头尾和免责声明,只保留纯内容部分。
-
标题/摘要字节校验。 推送前用 len(title.encode("utf-8")) 和 len(digest.encode("utf-8")) 检查字节数。超限时主动缩短,不要依赖API静默截断。
示例:首板战法文章的生成
来源文档: 和ima的对话 → 首板战法内容
处理步骤:
1. 读文档原始内容
2. 去掉"用户:...ima:..."的对话前缀
3. 去掉结尾"(内容由AI生成,仅供参考)"的免责
4. 分成核心逻辑、四大战法、操作细节等章节
5. 套白底HTML模板排版
6. 生成封面(交易系统·标题)
7. 推送草稿(title字段写标题,body不含标题)
文章导出(Python脚本)
详见 scripts/wechat_export.py。
用法
python3 wechat_export.py list
python3 wechat_export.py save
python3 wechat_export.py get <media_id>
关键陷阱:时间戳来源
batchget_material 接口返回的 news_item[].update_time 永远是0。正确发布时间在 item.update_time(素材级别):
item.get("update_time")
n.get("update_time")
而 get_material(获取单篇)接口的 news_item[].create_time/update_time 有正常值,与batchget不同。
References