SuperAPI AI Directory

wechat-draft-publish

General & other

View repository

Updated 10-16 08:02

About this skill

Official Account draft publishing (Doubaoya)

This duck helps you put aalready-written image-and-text articleinto your own Official Account'sdraft box—only saves a draft,never mass-sends/pushes. After saving it gives you a mediaId, then you go to the Official Account backend to confirm with your own eyes and manually mass-send.

⚠️ This is a "write" capability: unlike this duck's read-only topic-selection / search skills, this skill willwrite to your own Official Account backend. So it is more cautious than read-type skills—it only does the "save draft" step, and the final mass-send is always in your own hands.

Goes through doubaoya.com one line, authentication uses your own key (environment variable DOUBAOYA_API_KEY, in the form dyh_…)。


What it does / doesn't do

  • ✅ Saves {标题 + 公众号风格 HTML 正文} into the Official Accountdraft box。
  • ✅ External-link images in the body (<img src="http…">) will beautomatically transferredto Official Account image hosting addresses (mmbiz); individual images that fail to transfer will beskipped, without affecting the whole article.
  • ✅ When the cover (thumb) is not specified, it willautomatically fall back toone.
  • ✅ Each success deducts 1 point—this 1 point is deducted only after the draft is saved successfully; if publishing fails (502 WECHAT_PUBLISH_FAILED / WECHAT_COVER_FAILED) the server willautomatically refund this 1 point, and parameters blocked up front by 400 VALIDATION_ERROR are not deducted at all.
  • ❌ Never mass-sends / does not push / does not schedule—only saves a draft. The mass-send step is always completed manually by you in the Official Account backend.
  • ❌ Cannot bind an Official Account for you—you must first authorize at doubaoya.com.

A few constraints to honestly tell the user

  • 🔴 Do not write the title again in the body. Official Accountalwaysuses the draft's title field to render the article page's main title,contentHtml and if at the start of <h1>you put another of the same name <h2> (orit will display twiceon real devices. Bodystarts directly from the first paragraph, and the highest level used in the body is only <h2>. On this path nothing deduplicates for you.
  • contentHtml isOfficial Account-style HTML body (not markdown). If the user provides markdown, first convert it into Official Account HTML before sending.
  • In the body,external-linkimages (http(s):// / mmbiz) will be automatically transferred by the server into Official Account image hosting addresses; individual transfer failures will be skipped and will not affect the whole article.
  • Local images(<img src="/Users/.../x.png">、./a.jpg、file://…) andLocal coverThe server cannot read it, and sending it directly will cause it to be silently discarded—you must first, on the client side,pre-uploadand then send (see “Local images must be pre-uploaded on the client side” below; when there are local images/cover, use scripts/preprocess-and-publish.mjs)。
  • Save only as a draft, never send as a mass message.
  • Need tofirst bind the official account authorization on the doubaoya.com → Official Account page, otherwise there is no publishable official account (this step is an OAuth authorization, which this skill cannot do).

Haven’t written the body yet? First pull a writing guideline

✅ The API is already live, just fetch normally. If you get 401 it means DOUBAOYA_API_KEY is missing or incorrect—prompt the user to check the key configuration; do not skip it. Only when encounteringnetwork errors or a real 404 should you degrade:skipand write as usual; do not retry in an infinite loop, and do not report it to the user as a failure.

If the body is something you (the agent) are writing now, fetch it once before starting to write, and organize the structure according to it:

curl -sS https://doubaoya.com/api/wechat/writing-spec \
  -H "Authorization: Bearer $DOUBAOYA_API_KEY"

read-only, free, no credits deducted, authentication is the same as above (Bearer key or login state).data.spec is a markdown passage that clearly states “what content should use what structure” + “platform hard constraints”; just write according to it;data.isDefault:true means the user has not yet set formatting—what is still provided is a usable guideline, not an error; simply relay data.customizeUrl to them so they can customize it.


Workflow (3 steps)

1. First see which official accounts are already bound

GET https://doubaoya.com/api/wechat/status
Authorization: Bearer $DOUBAOYA_API_KEY

Success envelope:

{
  "success": true,
  "requestId": "req_...",
  "data": {
    "accounts": [
      {
        "authorizerAppid": "wx1234567890abcdef",
        "nickname": "本鸭运营笔记",
        "headImgUrl": "https://...",
        "status": "authorized",
        "principalName": "某某科技有限公司",
        "createdAt": "2026-06-01T..."
      }
    ]
  },
  "error": null
}

Reliable fields are based on nickname / authorizerAppid .

Decide the next step based on the number of data.accounts :

  • Exactly 1 → use its authorizerAppid。
  • Multiple → list {nickname, authorizerAppid} to the user,ask which one they want to send to, and do not guess for them.
  • 0 → tell the user:First go to doubaoya.com → Official Account page to bind the official account authorization, this skill cannot bind it for them.

2. Save draft

POST https://doubaoya.com/api/wechat/publish
Authorization: Bearer $DOUBAOYA_API_KEY
Content-Type: application/json

{
  "authorizerAppid": "wx1234567890abcdef",   // 必填,来自第 1 步
  "title": "标题",                            // 必填
  "contentHtml": "<p>公众号风格 HTML 正文</p>", // 必填,不是 markdown
  "digest": "一句话摘要"                       // 可选
}

Optional fields also include thumbMediaId / author / sourceUrl(all may be omitted).

Success envelope (note that the result key is mediaId, camelCase, not media_id):

{
  "success": true,
  "requestId": "req_...",
  "data": { "mediaId": "xxxxxxxxxxxxxxxx" },
  "error": null
}

3. Report the result

After getting data.mediaId , clearly tell the user:

“Saved to the official account draft box; after confirming in the official account backend,send manually as a mass message”, and report mediaId to them.

Do not take it upon yourself to say “published / pushed”—it is only a draft.


⚠️ Local images must first be “pre-uploaded on the client side” before sending

The server-side POST /api/wechat/publish will automatically move theexternal image links(http(s):// in the body and mmbiz image hosting) to the official account image hosting—but it runs on the server,and cannot read files on your local machine. So if the body HTML containslocal images(for example <img src="/Users/.../x.png">、./imgs/a.jpg、file://...), or you have alocal cover image, publishing directly will cause these images to besilently discarded。

Correct approach: byA client that can read local filesFirst upload local images to the official account image hosting, rewrite the HTML, then publish.

Determine which are "local images"

Scan contentHtml for all <img ... src="X">, for eachunique src:

  • Local (requires pre-upload): absolute path /Users/..., relative path ./a.jpg / ../a.jpg, bare relative path,file://..., Windows drive letter path.
  • External links (kept as-is):http:// / https:// starts with,data: starts with, or is already official account image hosting mmbiz.qpic.cn / mmbiz.qlogo.cn. These are handled by the server,do not touch。

Upload interface POST /api/wechat/media/upload

POST {baseUrl}/api/wechat/media/upload
Authorization: Bearer $DOUBAOYA_API_KEY
Content-Type: application/json

{
  "authorizerAppid": "wx1234567890abcdef",  // 必填
  "dataBase64": "<图片字节的 base64>",        // 必填
  "filename": "x.jpg",                       // 可选
  "purpose": "image"                          // "image"(正文图)| "thumb"(封面)
}
  • purpose: "image" → returns { "url": "https://mmbiz.qpic.cn/..." }, replace this url with the local src in the bodyalloccurrences of.
  • purpose: "thumb" → returns { "mediaId": "...", "url": "..." }, use mediaId as the publishing thumbMediaId。

WeChat restriction: body images ≤ 1MB. Local images exceeding 1MB must becompressed/scaled firstthen uploaded, otherwise the interface will reject (envelope error.message will be a 1MB-related Chinese message). Compression can use sharp(if installed), or macOS built-in sips:

sips -Z 1600 --setProperty formatOptions 70 in.png --out out.jpg

Complete process

  1. Scan contentHtml, pick out local image src.
  2. One by one: read file bytes → base64 → POST /api/wechat/media/upload(purpose:"image") → use returned url to replace all occurrences of that src.
  3. Has local cover:POST .../media/upload(purpose:"thumb") → get mediaId as thumbMediaId。
  4. POST /api/wechat/publish, body usesafter rewritingHTML (at this point all images are mmbiz external links, the server transfer logic passes them through as-is).

One-click script scripts/preprocess-and-publish.mjs(Node, zero dependencies)

Replaces publish_draft.py forbody contains local images / has local coverscenarios. Uses only Node built-in modules + global fetch(requires Node ≥ 18),sharp is an optional dependency (falls back to sipswhen missing). It completes "parse official account → pre-upload local images → rewrite HTML → publish draft" in one command.

# 正文含本地图片
node "$SKILL_PATH/scripts/preprocess-and-publish.mjs" \
  --html article.html --title "标题"

# 带本地封面
node "$SKILL_PATH/scripts/preprocess-and-publish.mjs" \
  --html article.html --title "标题" --cover cover.png --digest "一句话摘要"

# 绑定了多个公众号时指定 appid
node "$SKILL_PATH/scripts/preprocess-and-publish.mjs" \
  --html a.html --title "标题" --appid wx1234567890abcdef

# 只扫描本地图、不上传不发布(自检用,不需要密钥)
node "$SKILL_PATH/scripts/preprocess-and-publish.mjs" --html a.html --dry-run
  • Images with relative paths are resolved relative tothe directory containing the body HTML file.
  • Read DOUBAOYA_API_KEY and DOUBAOYA_BASE_URL(default https://doubaoya.com) from environment variables.
  • On error, print the Chinese reason per envelope error.code / error.message , exit code non-zero.

Bodydoes not havelocal images, and also no local cover, use the lighter publish_draft.py (see above); either can be chosen.


curl quick reference

# 1. 看已绑定公众号
curl -sS https://doubaoya.com/api/wechat/status \
  -H "Authorization: Bearer $DOUBAOYA_API_KEY"

# 2. 存草稿
curl -sS https://doubaoya.com/api/wechat/publish \
  -H "Authorization: Bearer $DOUBAOYA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "authorizerAppid": "wx1234567890abcdef",
        "title": "本鸭的第一篇草稿",
        "contentHtml": "<p>正文 HTML,不是 markdown。</p>",
        "digest": "一句话摘要"
      }'

Run script scripts/publish_draft.py

Zero dependencies (Python 3 standard library urllib), completes the whole "check binding → save draft" process in one command.

# 正文直接传(公众号风格 HTML)
python3 "$SKILL_PATH/scripts/publish_draft.py" \
  --title "本鸭的第一篇草稿" \
  --content "<p>正文 HTML,不是 markdown。</p>"

# 正文从文件读(推荐,长文更稳)
python3 "$SKILL_PATH/scripts/publish_draft.py" \
  --title "本鸭的第一篇草稿" \
  --content-file article.html \
  --digest "一句话摘要"

# 绑定了多个公众号时,指定用哪个
python3 "$SKILL_PATH/scripts/publish_draft.py" \
  --title "标题" --content-file article.html --appid wx1234567890abcdef

Script behavior:

  • First GET /api/wechat/status: if exactly 1 binding → auto-select (will indicate on stderr which one was chosen); if multiple and --appid not provided → list them so you can rerun with a specified one; if 0 → prompt to bind first.
  • Then POST /api/wechat/publish, after success print mediaId and the reminder "Saved to the official account draft box, go to the official account backend to confirm and then manually mass-send".
  • On error, print according to envelope error.code / error.message , exit code nonzero.
  • Parameters:--title(required),--content or --content-file(one of the two is required),--appid(optional),--digest(optional).

Get the key (secret key)

  1. Open doubaoya.com → Log in
  2. Go to Key Center → Generate key(in the form dyh_…)

Configure it in the environment variable (the script only recognizes this one):

export DOUBAOYA_API_KEY="dyh_你的密钥"

Iron rule: never print the key, never write it to a file, never echo it back to the user. The script itself also never outputs the key. All requests are sent only to doubaoya.com, do not take the key to any other domain.


Error handling

First look at the envelope success: if it is true , only then read data; otherwise read error.code / error.message. Common error codes:

HTTP error.code Meaning What you should do
401 UNAUTHORIZED Invalid key / session expired Check DOUBAOYA_API_KEY, go to Key Center to revoke and regenerate
403 FORBIDDEN This official accountdoes not belong tothe user behind the current key This means you are using an official account bound by someone else, or the key and the bound account do not match—switch to a key for the account bound by the user themselves
400 VALIDATION_ERROR Missing authorizerAppid / title / contentHtml Follow message to fill in the required fields
402 INSUFFICIENT_CREDITS / NO_CREDIT_ACCOUNT Insufficient points (this capability deducts 1 point per success) Have the user top up at doubaoya.com and try again
502 WECHAT_PUBLISH_FAILED / WECHAT_COVER_FAILED Rejected by WeChat (the 1 deducted point is automatically refunded) Do not blindly retry—first read error.message, see the next section "WeChat-side error codes"
503 WECHAT_NOT_CONFIGURED Platform-side official account capabilitynot configured The user cannot resolve this on their own; indicate that this is a platform configuration issue, try again later / contact doubaoya.com
504 WECHAT_PUBLISH_TIMEOUT Timeout returned, butthe server is still finishing publishing this article in the background Do not start another article and republish. After waiting 30 seconds, usethe exact same title and bodyto retry—the server deduplicates by content and will return the result of the same article to you without creating a second draft; you can also have the user directly check the drafts box in the official account backend. If the backend ultimately succeeds, that 1 point is retained; if it ultimately fails, it is automatically refunded

If GET /api/wechat/status 's accounts is empty → this is not an error code issue, it is thatno official account has been bound yet: have the user first go to doubaoya.com → Official Account page to bind one; this skill cannot bind it for them.

WeChat-side error codes (WECHAT_PUBLISH_FAILED the real cause of

⚠️ 502 does not mean "just retry later." The vast majority of WECHAT_PUBLISH_FAILED arethe body/parameters themselves being non-compliant, WeChat will reject it for the same reason every time—retrying a hundred times will not succeed and will only waste the user's time.Only rate limiting (45009) is truly the category that should be retried.

The server has already translated these codes into Chinese and put them into error.message(with errcode=… errmsg=… the original text, for troubleshooting). Please directly relay error.message to the user, do not rewrite it yourself as "upstream failure, please retry".

errcode Meaning Will retrying help? What to do
45166 In the body there areinvalid WeChat Official Account article links(mp.weixin.qq.com/s/… , and the articles they point to have been deleted or never existed). As long as there is even one, the entire draft cannot be created ❌ will never succeed Open each WeChat Official Account article link in the body one by one, and delete it or replace it with a valid one.error.message will list the suspicious links. Off-site links do not trigger this error (they are only stripped to plain text by WeChat)
45002 The body exceeds the draft size limit.Measuredthe true boundary is 695,000 ✅ / 700,000 ❌ bytes (UTF-8) — the official documentation says both "no more than 2kb" and "less than 1M" on this point, and neither is correct. The server blocks it before deducting credits based on 680,000 bytesas a preemptive interception (leaving margin for image URL rewriting at publish time), so you usually get VALIDATION_ERROR instead of 45002 ❌ Trim the body or split it into multiple articles
45003 Title too long: limit 64 Chinese characters(128 bytes, ASCII counts as 1, Chinese characters count as 2) ❌ Shorten the title
45004 Digest digest too long: limit 120 Chinese characters(240 bytes) ❌ Shorten the digest, or simply omit it and let WeChat truncate automatically
45110 Author author too long: the limit is only 8 Chinese characters(16 ASCII characters) ❌ Shorten the byline
40007 Cover media_id invalid: does not belong to this account / has been deleted / is not permanent material ❌ Remove thumbMediaId to use the default cover, or re-upload the cover to get a new id
45028 The WeChat Official Account draft box is full ❌ Have the user clean up drafts in the WeChat Official Account backend
48001 This WeChat Official Accountdoes not have permission for the draft box API. The draft box requires an account that has passedWeChat verification, and unverified subscription accounts/service accounts cannot call it successfully ❌ First complete WeChat verification in the WeChat Official Account backend
61023 Authorization has expired (refresh_token expired or was unbound) ❌ Go to doubaoya.com → WeChat Official Account page and scan the QR code again to authorize
40164 The server outbound IP is not in the WeChat whitelist ❌ the user cannot change it Platform-side configuration issue; contact doubaoya.com and include the requestId
45009 Calling the WeChat API too frequently and being rate-limited ✅ Only this category should be retried Wait a few minutes and retry

RetrySafety(will not create duplicate drafts) and retryingusefulnessare two different things: a retry with the same title and same body will be deduplicated, so retrying will not mess up the draft box; but for the codes marked ❌ in the table above, retrying just gets the same error again.


Directory structure

wechat-draft-publish/
├── SKILL.md                      # 本文件
└── scripts/
    ├── publish_draft.py          # 零依赖脚本(urllib):查绑定 → 存草稿(无本地图片时用)
    └── preprocess-and-publish.mjs # Node 脚本:预上传本地正文图/封面 → 改写 HTML → 存草稿

About the notice field in the response

After calling the doubaoya API corresponding to this skill, if the top level of the returned JSON contains the notice field, please relay this sentence to the user verbatim—this is a notice that this skill has an update, it does not affect the result of this call, and no retry or change in behavior is needed.