用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/huang1125677925/mybook --skill wechat-official-account命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
基于 SOC 职业分类
正在显示 SKILL.md
| 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,无法抓取别人的号。
⚠️ 优先使用 stable_token 接口(POST JSON),旧接口偶发 40001 无效凭证错误。
# 推荐:stable_token(POST + JSON body)
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',''))")
# 备用:旧 token 接口(偶发40001)
# curl -s "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=$APPID&secret=$APPSECRET"
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用于创建和发布文章。
# 封面图(永久素材,返回media_id用于thumb_media_id)
curl -s -F "media=@cover.png" \
"https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=$TOKEN&type=image"
# → {"media_id":"xxx", "url":"http://..."}
# 文中插图(返回url嵌入HTML)
curl -s -F "media=@chart.png" \
"https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token=$TOKEN"
# → {"url":"http://mmbiz.qpic.cn/..."}
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
}]
}'
# → {"media_id":"draft_xxx", "item":[{"index":0, "ad_count":1}]}
⚠️ 45003 / 45004 长度限制陷阱:
title 和 digest 限制按 UTF-8 字节 计算,不是字符数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"}'
# 成功: {"errcode":0, "publish_id":"xxx"}
# 失败: {"errcode":48001, "errmsg":"api unauthorized"}
48001 权限错误:表示公众号未获得发布API权限。需要:
无设计工具时可用 headless Chrome 截图 HTML 生成封面:
# 1. 写一个 styled HTML(深蓝渐变背景 + 标题文字)
# 2. 截图
google-chrome --headless --disable-gpu --no-sandbox \
--screenshot=cover.png --window-size=900,383 cover.html
# 3. 上传
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 通常是完整文档(含 <html><body>...</body></html>),创建草稿时需要只取 body 内容:
import re
m = re.search(r'<body>(.*?)</body>', html, re.DOTALL)
body = m.group(1).strip() # 去掉外层标签,保留所有 section/p/table
微信图文编辑器只支持有限的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 = "王晨"
# Step 1: 拿 token(必须 stable_token,旧接口偶发40001)
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"]
# Step 2: 抽 body(如果是完整 HTML 文档)
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
# Step 3: 标题/摘要字节检查(**必须** 在调用前 print 出来确认)
title = "标题"
digest = "摘要"
print(len(title.encode("utf-8")), "/ 64 ", len(digest.encode("utf-8")), "/ 120")
# 标题 64 字节 ≈ 21 汉字;摘要 120 字节 ≈ 40 汉字
# Step 4: 上传封面(永久素材)
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 期"——这不是用户告诉你的具体系列。封面顶部小标只放用户明确指定的字样,没有就只留主副标题。
<!-- ❌ 错误:擅自加 "交易笔记 · 第 12 期" 这种猜测 -->
<div class="tag">交易笔记 · 第 12 期</div>
<!-- ✅ 安全:只放主副标题 -->
<div class="title">尾盘买卖逻辑与技巧</div>
<div class="sub">从理论到实战的系统整理</div>
如果用户给的是多期文章系列(用户自己说过"这是第 X 期"),才写小标;否则一律不写。
必须显式指定 UTF-8 编码和 Content-Type charset:
# ✅ 正确:显式 encode + 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"})
# ❌ 错误:用 requests 的 json= 参数(charset 可能不完整)
r = requests.post(url, json=article)
原因: 使用 requests.post(json=...) 时 Content-Type 可能不带 charset=utf-8,导致中文在微信端解析为乱码。
在 execute_code 的 Python 字符串中使用中文引号 "" 会导致 SyntaxError,因为 Python 解析器把 " 当成字符串边界:
# ❌ SyntaxError
title = "别再9:30才盯盘了!真正的高手,早在9:15就已看穿一切"
# ^--- Python sees this as end-of-string
# ✅ 用 unicode 转义
title = '别再9:30才盯盘了!真正的高手,早在9:15就已看穿一切'
# 或
title = '\u201c过年加油站\u201d:情绪周期五阶段'
规则:execute_code 中的字符串如果包含中文引号 "",要么用单引号包裹,要么用 \u201c / \u201d 转义。
当标题含中文弯引号 "" 时,在 Python 字符串中会触发 SyntaxError——因为 " 看起来像普通双引号 ",导致 Python 误解析字符串边界:
# ❌ 错误:中文弯引号 "瓶颈理论" 中的 " 被 Python 当成字符串结束符
title = "一年45倍!Serenity的"瓶颈理论",A股怎么用?"
# SyntaxError: invalid syntax
# ✅ 方案1:用 Unicode 转义 \u201c \u201d
title = '一年45倍!Serenity的\u201c瓶颈理论\u201d,A股怎么用?'
# ✅ 方案2:用单引号包裹字符串(如果内部不含单引号)
title = '一年45倍!Serenity的"瓶颈理论",A股怎么用?'
这个坑也适用于
digest、author等任何含中文标点的字符串参数。在execute_code沙箱中写 JSON payload 时尤其要留意,因为沙箱会将代码作为普通 Python 解析。
微信 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> |
完整流程:
# 1. 上传图片 → 获得 CDN URL
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"] # "http://mmbiz.qpic.cn/..."
# 2. 将 URL 嵌入文章 HTML
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>
'''
# 3. 创建草稿(含图的 body)
payload = json.dumps({"articles": [{
"title": title, "content": body # body 中已含 img_url
}]}, ensure_ascii=False).encode("utf-8")
症状: 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 | 重新上传并推送 |
通用排错流程:
# 1. 验证草稿source里是否有 <img> 标签
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):
# 2. 验证每张图URL是否可访问
r = requests.get(src)
print(f"图{i}: {r.status_code} | {len(r.content)} bytes")
# 200 = OK;403/404 = URL失效
最稳的做法:重传+重建
# 删除旧草稿
requests.post(f"https://api.weixin.qq.com/cgi-bin/draft/delete?access_token={token}",
json={"media_id": old_media_id})
# 重新上传所有图片(每次都拿新URL)
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"])
# 用新URL重建HTML并推送
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"})
关键教训:
### ⚠️ 列表项之间不能有换行/空白(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)
完整清洗流程(推荐):
# Step 1: 去掉 li 间空白(防止被解析成新的空 li)
body = re.sub(r'</li>\\s+<li', '</li><li', body)
# Step 2: 去掉只有空白的 li(防止显式空项)
body = re.sub(r'<li[^>]*>\\s*</li>', '', body)
从 draft/batchget 拉取的内容如果编码异常(中文字符显示为 æä¹æ 等乱码),是因为内容被以 latin-1 编码存储但实际是 UTF-8 字节。修复方法:
# 从 batchget 拿到原始内容后
with open("draft_content.html", "r", encoding="utf-8") as f:
raw = f.read()
# 用 re-encode 修复
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不含标题)
详见 scripts/wechat_export.py。
# 列出所有文章
python3 wechat_export.py list
# 下载所有文章为Markdown(文件名 YYYYMMDD_标题.md)
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") # Unix时间戳
# ❌ 错误
n.get("update_time") # 永远是0!
而 get_material(获取单篇)接口的 news_item[].create_time/update_time 有正常值,与batchget不同。
references/api-notes.md — 接口字段说明