关于 wechat-official-account
使用 `curl + jq` 调用微信公众号 API 的 Shell 技能,面向管理公众号内容和账号操作的 Agent。它支持文章草稿与发布、关注者管理、客服消息和数据统计,并说明账号权限、配额及发布前确认要求。
主要功能
- 01创建、列出和更新文章草稿
- 02发布草稿并查询状态
- 03管理菜单和关注者记录
- 04发送客服消息
- 05获取关注增长和阅读统计
- 06处理令牌流程和 API 错误
适用场景
- 01准备文章草稿供审核
- 02发布已确认的草稿并获取链接
- 03查看关注者资料和增长情况
- 04向符合条件的关注者发送消息
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ acedatacloud/ skills --skill wechat-official-account安装后在 Claude Code 中输入/wechat-official-account确认已加载。
详细介绍
我们使用 curl + jq 调用 WeChat MP 服务端 API。与使用 OAuth bearer 令牌的连接器不同,WeChat MP 采用两步流程:
- 用
AppID + AppSecret换取access_token(有效期 7200s,每个应用每天的全局调用上限约为 2000 次)。 - 在每次其他调用中,将该
access_token作为查询字符串参数传入。
用户的凭据位于 $WECHAT_APP_ID 和 $WECHAT_APP_SECRET 中。切勿记录或回显 $WECHAT_APP_SECRET——请像对待密码一样保护它。
WeChat MP API 返回标准 JSON。即使出错,HTTP 状态码也会是 200;响应内容类似 {"errcode": 40013, "errmsg": "invalid appid"}。errcode == 0 表示成功——任何非零代码都应将原始 errmsg 原样展示给用户。
重要限制——请提前告知用户,以免造成意外
- IP 白名单:每次 API 调用的源 IP 都必须位于此应用的 IP 白名单中(公众平台 → 设置与开发 → 基本配置 → IP 白名单)。如果看到
errcode 40164(“invalid ip”),说明工作进程的出口 IP 不在白名单中;请告知用户将errmsg中显示的 IP 加入白名单后重试。 - 发布需要已认证账号:截至 2025-07,只有已认证的企业主体账号才能调用
freepublish/*和mass/*。个人主体账号和未认证的企业账号会收到权限错误。草稿(draft/*)和客服消息(message/custom/*)通常无需认证即可使用。 - 群发额度严格:服务号每月可发送 4 次,订阅号每天可发送 1 次。请将
freepublish/submit和mass/sendall视为破坏性操作——调用前务必先征得用户确认,即使用户指示“发布它”也不例外。默认创建草稿并粘贴草稿链接。 - 客服消息窗口为 48 小时:
message/custom/send仅适用于最近 48 小时内与账号互动过的关注者的openid。超过该时限会收到errcode 45015。
操作步骤
步骤 0 — 获取 access_token(先执行此步骤,并缓存结果)
# Cache to /tmp so subsequent calls in the same session reuse it.
TOKEN_CACHE="/tmp/wx-mp-token-${WECHAT_APP_ID}.json"
# Reuse cached token if it's still valid (we conservatively refresh
# 5 minutes early to avoid edge-of-window failures).
NOW=$(date +%s)
if [ -f "$TOKEN_CACHE" ] && [ "$(jq -r '.exp_at // 0' "$TOKEN_CACHE")" -gt "$((NOW + 300))" ]; then
WECHAT_ACCESS_TOKEN=$(jq -r '.access_token' "$TOKEN_CACHE")
else
RESP=$(curl -sS "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${WECHAT_APP_ID}&secret=${WECHAT_APP_SECRET}")
WECHAT_ACCESS_TOKEN=$(echo "$RESP" | jq -r '.access_token // empty')
if [ -z "$WECHAT_ACCESS_TOKEN" ]; then
echo "Failed to fetch access_token: $RESP" >&2
exit 1
fi
EXPIRES=$(echo "$RESP" | jq -r '.expires_in // 7200')
jq -nc --arg t "$WECHAT_ACCESS_TOKEN" --argjson e "$((NOW + EXPIRES))" \
'{access_token:$t, exp_at:$e}' > "$TOKEN_CACHE"
fi
echo "OK token=${WECHAT_ACCESS_TOKEN:0:8}…"
如果响应为 {"errcode": 40164, ...}(IP 无效)或 {"errcode": 40013, ...}(appid 无效),请将该错误告知用户;这表示连接器配置本身有误,而非请求有误。
验证连接(复杂操作前务必先执行一次)
# Pull basic public-account self-info; cheapest call that proves the token works.
curl -sS "https://api.weixin.qq.com/cgi-bin/account/getaccountbasicinfo?access_token=${WECHAT_ACCESS_TOKEN}" | jq
上传用于文章正文中的图片(返回公开 URL)
此操作用于文章 HTML 中的 <img src="..."> 标签。返回的 url 由腾讯托管,可用于已发布的文章。
curl -sS -X POST \
"https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token=${WECHAT_ACCESS_TOKEN}" \
-F "media=@/path/to/your-image.jpg"
# → {"url": "http://mmbiz.qpic.cn/..."}
限制:≤ 1 MB,仅支持 JPG/PNG,此接口不限制上传数量。
上传缩略图(用作文章封面)
curl -sS -X POST \
"https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=${WECHAT_ACCESS_TOKEN}&type=thumb" \
-F "media=@/path/to/cover.jpg" | jq '{media_id, url}'
# → {"media_id": "MEDIA_ID", "url": "http://mmbiz.qpic.cn/..."}
返回的 media_id 将在创建草稿时作为 thumb_media_id 传入。
推荐封面尺寸:900×500 px,JPG,最好小于 64 KB。
创建草稿(安全默认选项——不要直接发布)
TITLE="Q1 product update"
AUTHOR="Acme Inc."
THUMB_MEDIA_ID="MEDIA_ID_FROM_PREVIOUS_STEP"
CONTENT_HTML='<p>欢迎关注我们的最新动态。</p><p><img src="http://mmbiz.qpic.cn/..."></p><p>更多内容请见底部「阅读原文」。</p>'
DIGEST="Q1 has been a wild ride — here's what shipped."
SOURCE_URL="https://example.com/q1-recap" # optional; populates 「阅读原文」, leave empty to omit
PAYLOAD=$(jq -nc \
--arg title "$TITLE" \
--arg author "$AUTHOR" \
--arg thumb "$THUMB_MEDIA_ID" \
--arg content "$CONTENT_HTML" \
--arg digest "$DIGEST" \
--arg source_url "$SOURCE_URL" \
'{articles: [{
article_type: "news",
title: $title,
author: $author,
thumb_media_id: $thumb,
content: $content,
digest: $digest,
content_source_url: $source_url,
need_open_comment: 0,
only_fans_can_comment: 0
}]}')
curl -sS -X POST \
"https://api.weixin.qq.com/cgi-bin/draft/add?access_token=${WECHAT_ACCESS_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
--data-raw "$PAYLOAD" | jq
# → {"media_id": "DRAFT_MEDIA_ID"}
告知用户:"草稿已创建。打开 https://mp.weixin.qq.com → 草稿箱进行审核,然后点击「发表」。"——这是最安全的方式,因为由用户在微信自己的界面中控制发布操作。
列出草稿(按最新到最旧排序)
curl -sS -X POST \
"https://api.weixin.qq.com/cgi-bin/draft/batchget?access_token=${WECHAT_ACCESS_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
--data-raw '{"offset": 0, "count": 20, "no_content": 1}' \
| jq '.item[] | {media_id, update_time, title: .content.news_item[0].title}'
获取一篇草稿的完整内容
DRAFT_MEDIA_ID="..."
curl -sS -X POST \
"https://api.weixin.qq.com/cgi-bin/draft/get?access_token=${WECHAT_ACCESS_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
--data-raw "$(jq -nc --arg m "$DRAFT_MEDIA_ID" '{media_id: $m}')" | jq
更新现有草稿
curl -sS -X POST \
"https://api.weixin.qq.com/cgi-bin/draft/update?access_token=${WECHAT_ACCESS_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
--data-raw "$(jq -nc --arg m "$DRAFT_MEDIA_ID" --arg t "Updated title" --arg c "<p>new body</p>" --arg th "$THUMB_MEDIA_ID" '
{media_id: $m, index: 0, articles: {
title: $t, content: $c, thumb_media_id: $th
}}')" | jq
将草稿发布到「发表记录」——破坏性操作,调用前须确认
# Eats one of the monthly publish slots. Always echo back to the user
# what's about to go live and require an explicit "yes" confirmation
# in conversation BEFORE invoking this.
curl -sS -X POST \
"https://api.weixin.qq.com/cgi-bin/freepublish/submit?access_token=${WECHAT_ACCESS_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
--data-raw "$(jq -nc --arg m "$DRAFT_MEDIA_ID" '{media_id: $m}')" | jq
# → {"errcode": 0, "msg_data_id": ..., "publish_id": "..."}
随后轮询发布状态(发布为异步操作,约需 5-30 秒):
PUBLISH_ID="..."
curl -sS -X POST \
"https://api.weixin.qq.com/cgi-bin/freepublish/get?access_token=${WECHAT_ACCESS_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
--data-raw "$(jq -nc --arg p "$PUBLISH_ID" '{publish_id: $p}')" | jq '{publish_status, fail_idx, article_id, article_url: .article_detail.item[0].article_url}'
# publish_status: 0 = success, 1 = publishing, 2 = original-check-failed,
# 3 = failed, 4 = published-but-removed, 5 = unverified-removed
当 publish_status == 0 时,将 article_detail.item[0].article_url 返回给用户——这是规范的 https://mp.weixin.qq.com/s/... URL。
列出已发布文章
curl -sS -X POST \
"https://api.weixin.qq.com/cgi-bin/freepublish/batchget?access_token=${WECHAT_ACCESS_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
--data-raw '{"offset": 0, "count": 20, "no_content": 1}' \
| jq '.item[] | {article_id, update_time, title: .content.news_item[0].title, url: .content.news_item[0].url}'
发送客服消息(发送给单个关注者,须在 48 小时窗口内)
OPENID="oXXXXXXXXXXXXXXXXX" # the recipient follower's openid
TEXT="Hi! Your subscription has been renewed. Thanks for sticking with us."
curl -sS -X POST \
"https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${WECHAT_ACCESS_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
--data-raw "$(jq -nc --arg u "$OPENID" --arg t "$TEXT" '
{touser: $u, msgtype: "text", text: {content: $t}}')" | jq
errcode 45015 表示接收者最近 48 小时内没有给该账号发送消息——请向用户说明这一点;API 无法解决此问题。
如需通过客服消息发送文章卡片,请将 msgtype 改为 "mpnews",并设置 mpnews: {media_id: "..."}(先使用 material/add_news 创建永久图文素材 media_id)。
拉取粉丝增长和阅读统计
BEGIN="2026-04-25"
END="2026-05-01" # max 7-day window for getusersummary
# New / unsubscribed / cumulative followers per day
curl -sS -X POST \
"https://api.weixin.qq.com/datacube/getusersummary?access_token=${WECHAT_ACCESS_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
--data-raw "$(jq -nc --arg b "$BEGIN" --arg e "$END" '{begin_date: $b, end_date: $e}')" \
| jq '.list[] | {date: .ref_date, new: .new_user, lost: .cancel_user, source: .user_source}'
# Article reads per day (max 3-day window for getuserread)
curl -sS -X POST \
"https://api.weixin.qq.com/datacube/getuserread?access_token=${WECHAT_ACCESS_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
--data-raw "$(jq -nc --arg b "$BEGIN" --arg e "$BEGIN" '{begin_date: $b, end_date: $e}')" | jq
datacube/* 需要已认证账号,并且该账号拥有数据立方权限。
管理底部菜单
# Read current menu
curl -sS "https://api.weixin.qq.com/cgi-bin/menu/get?access_token=${WECHAT_ACCESS_TOKEN}" | jq
# Replace the menu (max 3 top-level buttons; each top-level button can have ≤ 5 sub-buttons)
MENU=$(jq -nc '{
button: [
{type: "click", name: "今日推荐", key: "DAILY_REC"},
{name: "更多",
sub_button: [
{type: "view", name: "官网", url: "https://example.com"},
{type: "view", name: "联系我们", url: "https://example.com/contact"}
]}
]
}')
curl -sS -X POST \
"https://api.weixin.qq.com/cgi-bin/menu/create?access_token=${WECHAT_ACCESS_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
--data-raw "$MENU" | jq
# → {"errcode": 0, "errmsg": "ok"}
获取粉丝列表(通过 next_openid 分页)
NEXT=""
curl -sS "https://api.weixin.qq.com/cgi-bin/user/get?access_token=${WECHAT_ACCESS_TOKEN}&next_openid=${NEXT}" \
| jq '{total, count, openids: .data.openid, next: .next_openid}'
# Loop: pass the returned `next_openid` as NEXT until count < 10000.
对于每个 openid,批量获取个人资料(每次最多 100 个):
curl -sS -X POST \
"https://api.weixin.qq.com/cgi-bin/user/info/batchget?access_token=${WECHAT_ACCESS_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
--data-raw '{"user_list": [{"openid": "OPENID1", "lang": "zh_CN"}]}' \
| jq '.user_info_list[] | {openid, nickname, subscribe_time, tagid_list}'
文章 content 的 HTML 规则
draft/add 中的 content 字段只接受 部分 HTML,而不是完整的网页 HTML:
- 允许使用:
<p>、<span>、<strong>、<em>、<a href>、<img src>、<br>、<h1>–<h3>、<ul>/<ol>/<li>、<blockquote>、<section>。 - 仅限内联样式(
<p style="...">)——不允许使用<style>块,也不能通过<link>引用外部 CSS。 - 所有
<img>的srcURL 必须是之前上传的mmbiz.qpic.cnURL(来自media/uploadimg)或已发布的mmbizURL。微信会移除托管在第三方域名上的图片。 - 内容总长度限制:约 20 K 个字符(包括 HTML)。
常见 errcode 速查表
| errcode | 含义 | 告诉用户的内容 |
|---|---|---|
| 0 | 成功 | — |
| 40001 | access_token 无效 | 调用过程中 Token 已过期;清空 $TOKEN_CACHE 后重试 |
| 40013 | appid 无效 | 连接器中的 AppID 有误——请重新添加连接 |
| 40164 | 源 IP 不在白名单中 | 将 errmsg 中显示的 IP 添加到公众平台 → 设置与开发 → 基本配置 → IP白名单 |
| 41001 | 缺少 access_token | Bug——忘记传入 ?access_token=... |
| 45009 | API 每日配额已超限 | 请明天再试;已达到该应用的每日配额 |
| 45015 | 回复消息超出 48 小时 | 此 openid 的客服窗口已关闭;无法通过 API 恢复 |
| 48001 | API 未获授权 | 账号没有此端点的权限(例如,未认证就发布);请参阅 errmsg 中的文档 URL |
| 61450 | 系统错误 | 腾讯端暂时不稳定;等待 1 秒后重试一次 |
为什么使用裸 curl + jq(而不是 SDK)
技能正文必须是自包含的 shell。沙箱不允许调用 pip install wechatpy,而且此处的 API 数量较少(约 15 个端点),自 2018 年以来一直保持稳定。Python SDK(wechatpy、werobot)可用作参数格式的参考,但上面的每个示例都是直接调用文档中说明的端点——SDK 只是对相同 JSON 的轻量封装。