This duck helps you take an articlealready writtengraphic, through a series ofdeterministic mechanical steps, and finally save it into your own Official Account'sdraft box——
Only saves a draft, never mass-sends. After saving you get mediaId, then you go to the Official Account backend to confirm with your own eyes and manually mass-send.
📍 Which baton it takes over: when the user says "help me write a WeChat Official Account article", the body text is first written by you (the agent) (topic selection, writing, headline creation, cover routines, and compliance checks are now all handled by
doubaoyavia intent routing). After the body text is finalized, look at the end state the user wants: if only a finished draft is needed, it stops there; if they wantformatted WeChat Official Account HTML or wantthe article in their own draft box, only then come back here. When the user has not expressed the latter intent, ask first — this step writes into their own Official Account backend.
⚠️ Write capability: it will write to your own Official Account backend. So it only does the "save draft" step; the mass-send hand must be your own. Go through doubaoya.com as one line, authentication uses your own key
DOUBAOYA_API_KEY(in the formdyh_…)。
Division of labor: the body text is written byyou (the agent)according to requirements; this pipelinedoes not write the body text for you, it only automates the subsequent deterministic operations (verify account, render, upload images, save draft).
Single source of truth:pipeline.json
The 10-step SOP and all hard rules are declared in pipeline.json(steps[] + hardRules[]).
This SKILL.md and the orchestration script scripts/pipeline.mjs both take it as authoritative— to change the process, first change pipeline.json, do not hard-code it in various places.
Among themstep 6 "guided design" is executed by the agent(choose style / generate cover / generate supporting images / layout confirmation, see belowguided design), it feeds the output (
--coverlocal cover + local<img>in the Markdown) into the subsequent mechanical steps;pipeline.mjsitself is still a deterministic executor for render → upload images → save draft.
10-step SOP
- Identify task type — confirm it is "push an already-written article into the WeChat Official Account draft box".
- Read identity context — load andecho the IP/identity profile (name / alias /
isNotdisambiguation / tone). - whoami verify account —
GET /api/agent/whoami, resolve the local key into the entry for the target account (the key only stays in memory). - Draft pre-check —
GET /api/skills(assertwechat-draft-publishexists) +GET /api/wechat/status(confirm the Official Account, resolve appid/nickname). - md→HTML —
--mdrender into WeChat Official Account inline-style HTML (preserve as-is<img src>);--htmluse directly. - Guided design — Choose a style → AI-generated cover (
--cover-guard, 1536x1024) → Generate illustrations (1024x1024, dropped into the Markdown source then back to step 5 for re-rendering) → Layout confirmation. Guided by default; "you have full authority" is the escape hatch. See belowGuided design。 - Image preprocessing — Scan
<img>,Local images pre-uploaded client-sideto the image host (compress first if >1MB) and rewrite the HTML; external links are kept as-is. - Cover — Local cover pre-uploaded as thumb; if none, fall back to the Doubao Duck default cover.
- Save draft —
POST /api/wechat/publish(draft/add)。 - Verification report — Title / Official Account / Number of body images uploaded / Cover / Style used / mediaId / Mass send: No。
Hard rules (hardRules, enforced in code)
- Only save drafts, never mass send — There is no mass-send path at all; the pipelinerejectsany
--mass-send/--broadcast/mass-send parameter. - Must verify the target account via whoami before publishing — If step 3 fails, step 8 does not run.
- Load identity context before making content judgments。
- Discovery goes through
/api/skills, execution goes through/api/wechat/status+/api/wechat/publish, not through/invoke。 - Local images must be pre-uploaded client-side(the server cannot read files on your local machine).
All calls go to Doubao Duck (protocol, copied from doubaoya-gateway)
Capabilities used by this Skill(only capability and detail endpoints are named; input parametersare not written here, fetched fresh before each call):
| operationKey | Detail endpoint | Used in which step |
|---|---|---|
skill.wechat.render ⚠️Dedicated |
GET /api/skills/wechat-render |
Step 5 md→HTML (the server-side layout path) |
skill.ai.imageGen |
GET /api/skills/gpt-image-gen |
Step 6 generate cover / illustrations (scripts/gen-image.mjs is just a thin wrapper around it) |
skill.wechat.draftPublish ⚠️Dedicated |
GET /api/skills/wechat-draft-publish |
Step 9 save draft |
- Authentication: Allcallsrequire endpoints to have
Authorization: Bearer $DOUBAOYA_API_KEY. Prefer reading from the environment variableDOUBAOYA_API_KEY; if it's not in the environment,ask the user once, and never ask again afterward. 🔴 Not a single character may be echoed, printed, or written to logs—the prefix is also part of the key. To report status, only say "set / not set"; do not print any truncated form (${KEY:0:6}this way of writing is printing the key). Base URLhttps://doubaoya.com。 - Fetch the spec first, then assemble parameters:
GET <详情端点>(no auth, free). FollowinputContract→inputUiSchema'sfields→requestSchema(example value, not the spec), take them in order,stop as soon as you find one nearby。 🔴 Never assemble input parameters from memory or from the field names in this document—field names are never written here precisely so that you have nothing to copy. - Follow
execution.targetto call it; don't assemble the address yourself: The same detail response containsexecution.target.methodandexecution.target.path; prefixing the base URL gives the address to call.execution.modeisdedicated, the method is not necessarilyPOST(there isPUT); when it isunavailable, there is notarget, don't call it, truthfully tell the user that this capability is temporarily unavailable. ⚠️ Two of the three above arededicated routes—their call addresses have nothing to do with the detail endpoint,cannot be inferred, you can only readtarget(this is also the origin of this Skill's hard rule "do not go through/invoke"). - The two routes do not fall back to each other:
/api/skills/<slug>/invokeand/api/apis/<platform>/<slug>/callaretwo disjoint setsof their respective entry points; using a slug from the wrong set to hit the other one always returns 404,retrying in different ways is useless. So item 3 is not a suggestion: the address can only come from the detail response. - Read the envelope: success and failure are both at the same level
{ success, requestId, data, error }。 first look atsuccess——trueto getdata,falsereaderror.code/error.message. A success envelope may also have three optional fields (absence is normal, not an anomaly):noResult: the query is valid, there is just no data,not billed. Do not treat it as a failure and retry; truthfully tell the user there are no results and suggest changing the conditions.notice: this Skill has a newer notice,relay it verbatim, it does not affect this result.detailUrl: this result's detail page on doubaoya.com, which can be given to the user to click.
- What to do about errors(
HTTP/error.code): 401MISSING_API_KEY/UNAUTHORIZED→ have the user go to the key center to generate or rebuild it, and update the environment variable; 400VALIDATION_ERROR→ followmessageto change the input parameters; before changing,re-fetch the spec once; 400DEDICATED_ROUTE→ you went wrong to the generic proxy,messagesays which one to hit, resend according toexecution.target; 402INSUFFICIENT_CREDITS→ prompt the user to top up; 404SKILL_NOT_FOUND/ENDPOINT_NOT_FOUND→ see item 4,go to the other set's discovery interface to find it, do not guess the slug; when the discovery interface also does not have this capability,it is most likely that the local skill has expired(the capability it named has long been delisted): tell the user "your local duck skill may have expired", have them run/dby-update(or say "update doubaoya"), thenretry only this once. 🔴 If the retry is still 404, truthfully inform them that the capability has been delisted,no more updates allowed, no loops allowed。 503CAPABILITY_UNAVAILABLE→ do not retry, switch capabilities or truthfully inform them; 502PROVIDER_FAILED→ temporary upstream failure,the quota has been automatically refunded, you can retry directly. 🔴 Only the 404 above follows "update first, then retry";no other error may trigger an update— 401 is a key problem, 400 is an input parameter problem, 402 is a balance problem; updating the skill cannot cure any of them.
This document never writes pricing numbers. will silently reprice; copying it in means quoting the wrong price to the user; each capability's current price is in its detail response, so if you need to tell the user, fetch it fresh and say it fresh. Remember only two things that do not change with price:saving drafts and generating images both cost money, server-side layout rendering does not cost money, and for the two steps that cost money, ask the user before acting.
When to go read doubaoya-gateway
The protocol above isComplete, and with it you can fire off this Skill's three calls; in the normal flow you don't need to read anything else. Only in these two situations should you read that gateway Skill:
| Situation | Which one to read |
|---|---|
| What the user wants goes beyond this pipeline's three calls (switching to another capability), and you need to first know which one exists | doubaoya-gateway of references/capability-index.md(is for routing only, and does not include input parameters) |
| After choosing a capability and before actually firing the request, you want to confirm whether this one has any known pitfalls | doubaoya-gateway of references/routing-pitfalls.md |
🔴 Don't copy the index table back into this file: the capability catalog changes once a week, and the day you copy it in it starts to rot.
Before writing the body
The body is written by **you (the agent)**; the two points below determine what this draft looks like on a real device—go through them before you start writing.
🔴 Do not write a title in the body
WeChat Official Accountalwaysuses the draft's title field to render the article page's main title. If the body also contains the same title, on a real device it willdisplay twice。
- Writing Markdown: the bodystarts directly from the first paragraph, and the highest level used is only
##. The title only goes through the--titleparameter; don't write it into the body. - Writing HTML and sending directly(
--html): don't put<h1>at the beginning of the HTML (or<h2>/ bold large text used as a main title)— this path sends the fileas-is, and nothing will deduplicate it for you. - Running the renderer separately:
render-wechat-html.mjs --title "标题"will, at the top of the body,insert a<h1>, which is for local preview of the whole article's effect;don't take this output and publish it directly; for publishing usepipeline.mjs。
pipeline.mjs --mdthis path has already stripped the frontmatter at the start of the source file and the single#title (normalizeDraftMarkdown), and does not inject--titleinto the body. But that is afallback, not permission: a second title in the middle of the body, or rewriting the title with##, it cannot handle.
First pull a writing specification:GET /api/wechat/writing-spec
✅ The interface is already live, so you can pull it normally. Getting 401 means
DOUBAOYA_API_KEYis missing or incorrect—prompt the user to check the key configuration, don't skip it. Only when encounteringa network error or a real 404 should you degrade:skip this stepand write as usual (the above item + body component syntax is already enough); don't retry in an infinite loop, and don't report it to the user as a failure.
Pull it once before writing the body, and organize the structure according to it before you start writing. It turns "what content should be written as what markdown structure" and "the hard constraints under which the platform will reject the whole article / silently drop content" into a passage of text you can directly follow.
curl -sS https://doubaoya.com/api/wechat/writing-spec \
-H "Authorization: Bearer $DOUBAOYA_API_KEY"
Read-only, free, no credits deducted(this path doesn't enter billing at all), and it doesn't change any of the user's configurations. Authentication is the same as other interfaces
(Bearer key, or web login state); unauthenticated 401.
In the success envelope's data there isa passage of markdown writing specification(This is what you should follow when writing), the meta-information of this layout,
and where to go to customize the layout.For the specific fields, read them from this actual response, don't read from memory or this document—
this document deliberately does not list the field table; for the reason, see item 2 of the protocol above.
Users who have never set up a layout still get a usable spec(default theme + only the "structural suggestions" part is output), it does not return empty and does not error; the response will tell you whether this spec uses the default layout, and if so, just relay the customization entry point to the user, don't treat it as an error.
The spec body is divided into two parts,with different conditions for holding true:
- Structural suggestions + platform hard constraints — unrelated to layout,always hold true, follow them.
- Presentation of this theme — only holds true when this pieceactually uses the server-side layout rendering. When this pipelinedoes not explicitly pin a local theme
(without
--theme,config.jsonand without writingmdThemeas a path), it will use the sameDOUBAOYA_API_KEYautomatically fetchthe compiled version of the server-side default layout(GET /api/wechat/theme?format=compiled) to render— at this timethe second part applies, follow it. When explicitly--theme/config.mdThemepins a local theme, or the server-side theme is not fetched and it falls back to the local theme, the second partdoes not apply, only follow the first part (the pipeline log will print which theme was actually used this time).
Theme source and priority
Which theme is used for rendering is taken as the first one that holds true in the following order (see themes/README.md):
--theme <path|neutral>(including--designconverted theme) — explicitly specified, always wins;config.jsonexplicitly written as a path inmdTheme— pins the local theme, sends no request;- server-side compiled theme
GET /api/wechat/theme?format=compiled— the default layout you set in the doubaoya.com layout studio (engine-2 theme server-side has been compiled into the all-literal form recognized by the local renderer). Fetching has a 5s timeout, if it cannot be fetched, gracefully fall back: 401 warns to check the key (does not interrupt), 404/network errors only print a single message, no retry; - project default theme
themes/benya-clean.json。
one exception: the item in the spec saying "the
:::component syntax and> [!NOTE]prompt block renderer do not parse, don't write them" refers to theserver-side renderer. This pipeline'srender-wechat-html.mjsparsesthese two syntaxes (see the next section), and when going through this pipeline, use them as usual. After the layout is changedfetch again— don't treat this passage as long-term factual cache.
Two rendering paths, the criterion is "whether there is anything local to handle"
Step 5 md→HTML has two paths,not chosen arbitrarily:
| When to take it | Which path to take |
|---|---|
The body containslocal images, or uses the above ::: component syntax, or the userhas no keyand just wants to see the layout first |
local renderer scripts/render-wechat-html.mjs(that is, pipeline.mjs the one taken internally) |
| It just turns a piece of clean markdown into WeChat official account HTML, with no local images and no component syntax used | Platform rendering capability skill.wechat.render(detail endpoint GET /api/skills/wechat-render), call it according to clauses 2 and 3 of the protocol above |
The platform rendering onecosts no money, and it uses exactly the layout set by the user in the doubaoya.com typesetting studio,eliminating the whole class of problems caused by
theme dual-source mismatch; the local renderer is irreplaceable only where the server side cannot reach (local files, component syntax, no key).
⚠️ It is adedicated route: the call address has nothing to do with the detail endpoint, and it can only read execution of target。
Body component syntax (component layer)
The following set can be used directly in body markdowncomponent syntax, and when rendering it automatically applies the color scheme (primary color / heading color / body color) of the current typesetting theme, with no need to hand-write HTML:
:::关注卡…:::(alias:::follow) — a follow-guide card, with customizable text inside the card (leave blank to use the default "Tap the card above to follow us").> [!NOTE] 标题、> [!TIP]、> [!WARN]— three-level tip box (info / tip / warning); the title is optional, with body text below. NOTE uses the theme primary color, TIP green, WARN orange.:::金句…:::(alias:::quote-card) — a centered large-text quote card, heading color + light theme background.:::标题 小节标题(alias:::title) — a decorative small heading with a badge + bottom fade line.:::分割(alias:::divider) — a decorative divider with a centered small icon + fade lines on both sides.
Example:
:::关注卡
点击上方名片,关注我们
:::
> [!TIP] 小贴士
> 先定选题,再动笔。
:::金句
真正的高手,都在偷偷做时间的朋友
:::
:::标题 一、为什么
:::分割
Unknown component names (such as :::xxx) will not error — they are output as-is with a hint. The HTML produced by components is purely inline-styled, with no class / id, meeting the WeChat official account red lines. Theme JSON may optionally add a components section to override the built-in templates (advanced).
Composition structure (don't reinvent the wheel)
scripts/pipeline.mjs is the orchestrator; it composes three zero-dependency modules:
| Stage | Module | Description |
|---|---|---|
| Account resolution | scripts/account-verify.mjs |
resolveAccountKey({account, baseUrl}): candidates from multiple sources (env / ~/.doubaoya / Keychain) → whoami one by one → pick the right key by target account, with the key only in memory. When multiple keys point to different accounts and --account is not specified, report the account corresponding to each key and stop. |
| md→WeChat official account HTML | scripts/render-wechat-html.mjs |
renderWechatHtml(md,{title}): zero-dependency inline-style rendering,preserving image src as-is。 |
| Cover/accompanying image generation | scripts/gen-image.mjs |
generateImage({prompt,size,out,styleId,coverGuard,referenceImage}): zero-dependency, a thin shell over capability skill.ai.imageGen(detail endpoint GET /api/skills/gpt-image-gen), synchronous return, billed. When referenceImageis passed (local path/URL/data:/raw base64, CLI --reference-image), it goes through operation:"edit" conditionalization,preserving the IP character in the reference image; if not passed, it is text-to-image. Also exports resolveReferenceImage(ref)(a small utility for local image → data: URL). Style library assets/styles/index.json, using env DOUBAOYA_API_KEY(no additional key needed). Produces a local jpeg → feed it to --cover or place it into the body with <img src> ,Does not touch the publishing contract. Called by the agent in guided design (not mechanically triggered by pipeline.mjs). |
| Automatic image layout | scripts/plan-figures.mjs |
planFigures(markdown,{maxFigures,minChars}) → {figures[],meta}:Deterministic rules(no LLM) decide at which h2 subsection ends to place images + visual suggestions. Selected by subsection effective word count exceeding a threshold (default 160), with the number of images tiered by total word count (<1800→3, 1800–3000→4, >3000→5) capped. CLI node plan-figures.mjs --md <文章> [--max-figures N] [--min-chars N] [--json]. The workbench "Auto Image" calls it, and the output is directly filled into design-config.images[](afterHeading anchors), consumed by the existing pipeline injection logic,without changing the publishing chain。 |
| Upload images + save draft | scripts/preprocess-and-publish.mjs |
vendored from wechat-draft-publish skill (the two must stay in sync). Local image pre-upload + >1MB compression + save draft (draft/add, no mass send). |
The orchestrator strings these three steps together, and adds identity context loading, pre-checks, hard gates, and structured reporting.
Guided design (cover / images / layout)
Step 6 — complete visual design before and after rendering.Guidance is the default: stop and ask the user at the 4 points below;escape hatch: if the user says
"You have full authority over the cover and images / I'm in a hurry", skip all stops and use config.defaultStyleId to automatically produce a version.
Image generation goes through capability skill.ai.imageGen(details endpoint GET /api/skills/gpt-image-gen),no extra key needed
(using the DOUBAOYA_API_KEYalready used for publishing). To generate one by one in the conversation, use the zero-dependency thin shell scripts/gen-image.mjs,
when the key is missing it reports a clear error and does not crash.This step costs money, ask the user before starting(pull current prices live, this article does not write numbers).
- Choose style — list the
assets/styles/index.json's 6 styles (name+id) and their respective sample imagesassets/styles/<id>.jpgfor the user to choose (or the user says "you decide"). 6 starter styles:杂志编辑风(magazine-editorial)、极简大字(minimal-bigtype)、真实摄影感(photo-real)、扁平插画(flat-illustration)、国潮中式(guochao-chinese)、商务信息图(biz-infographic)。 - Cover — AI reads the article and distills a cover concept (subject + atmosphere), generates 1
1536x1024in the chosen style, shows it to the user → choose / regenerate / upload your own / use fallback. Once decided, set it into--cover <本地jpeg>。The cover must add--cover-guard(press the subject into the horizontal middle band, leave atmospheric background above and below, to prevent WeChat's 2.35:1 center crop from cutting off key content):node scripts/gen-image.mjs --prompt "<封面概念>" --style <风格id> --cover-guard \ --size 1536x1024 --out <暂存目录>/cover.jpg - Images — scan the article structure (generally 1
##under each subheading), propose the number of images and their respective visuals, generate them one by one1024x1024and place them into<img src=本地路径>with Markdown source(not the rendered HTML — only by putting them in the source will the theme apply captions/rounded corners/spacing).
After images are placed into Markdownreturn to step 5 to re-render. These local images will be uploaded by the existingnode scripts/gen-image.mjs --prompt "<该段画面>" --style <风格id> \ --size 1024x1024 --out <暂存目录>/fig1.jpgpreprocess-and-publish.mjsviaimage, no need to change any publishing chain。 - Layout — confirm which theme set to use (by default automatically taken according totheme source and priority: when a key is present, first pull
the compiled version of the server-side default layout, or use
--theme/config.mdThemeto pin a local theme; for writing themes see below "Replicate reference layout style"), andconfirm the renderer is actually called。
gen-image.mjsGenerated local jpeg path, cover fed intopipeline.mjs --cover, and illustrations are placed into the body via<img src>— neither touches the WeChat-side publishing contract. The upstream image generation key is only on the doubaoya server side, and the skill side only uses the key.
Use the design workbench (visual alternative)
If you don't want to select styles / generate images step by step in the command line, you can launch a local web workbench and finish it all in one go, producing a design-config.json, then hand it to
pipeline.mjs --design to consume. The workbench has zero dependencies (Node built-in http + global fetch), binds only to 127.0.0.1, writes only local artifacts, does not publish, and does not commit.
export DOUBAOYA_API_KEY="dyh_你的密钥"
node scripts/design-studio.mjs --md <文章.md> --title "<标题>" \
[--out <默认同目录 文章.design.json>] [--port 4599]
Register a cartoon IP (optional, keeps the image consistent throughout the article): put your cartoon IP character image into assets/ip/(or "Upload IP" at the top of the page),
and in config.json point ipImage to it. After registration, the cover and illustrations default toreference-image-conditioned generation
(operation:"edit" + referenceImage),preserving the same characterto keep the visual consistent throughout the article; if not registered, it falls back to text-to-image.
See assets/ip/README.md。
Page three sections:①Layout = real-time theme card skin preview (375px mobile WeChat official account frame on the left);②Cover = choose image generation style →
generate candidates (by default applies the current IP reference image, can regenerate / upload your own) → pick one;③Illustrations (auto layout) = click "Auto Illustrate" →
backend plan-figures.mjs(deterministic rules, no LLM) automatically picks positions (at the end of information-rich h2 subsections, count tiered by word count) →
generate each one using the IP reference image andautomatically place them, the user only does "replace one / delete / regenerate all",does not manually pick anchors. Top "Save Config"
writes out design-config.json(including ip and the auto-filled images[], passing schemas/design-config.schema.json validation).
The generated cover/illustration jpeg is saved in the same directory as design-config In the same directory, .design/assets/。
After getting design-config.json , enter the pipeline (apply theme + set cover + inject illustrations by h2 anchor):
node scripts/pipeline.mjs --md <文章.md> --title "<标题>" --design <文章.design.json> --dry-run
--designtheme / cover is the default value; when an explicit--theme/--coverconflicts with itthe command line takes priority and warns. Illustrations are inserted byafterHeadinganchor at the end of the corresponding h2 subsection; if the anchor is not found, append to the end of the article and warn. The workbench +--designis equivalent to the command line above guide, choose one of the two, and neither touches the WeChat-side publishing contract.
Getting started: config + identity profile
# 1. 复制配置模板,填你自己的值(见 config.example.README.md 逐字段说明)
cp config.example.json config.json
# 2. 复制身份 profile 模板,改成你自己账号的身份卡
cp profiles/example-ip.json profiles/my-ip.json
# 再在 config.json 里把 ipProfile 指向 profiles/my-ip.json
config.json Key fields:targetAccount(pick account when there are multiple keys),appid / publicAccountName(select/validate official account),
ipProfile(identity card path),coverFallback(fallback cover flag).null = auto-detect.config.json belongs to you personally, don't commit it to a public repository.
Identity context takes priority (general rule, not someone's story)
An account name / IP name is very likely to be the same as acommon noun or product category. If the identity context is not loaded first, the agent may take this
proper noun and misread it as a literal common noun, causing the topic selection, illustrations, and cover to all go off track. The isNot in the profile is exactly thisdisambiguation rule externalized as data: step 2 of the pipeline reads it and echoes it back, making clear "this is an account name, not that generic noun".
The example profile (profiles/example-ip.json, fictional 示例·日常号) demonstrates the schema—follow it to writeyour ownaccount's identity card.
See profiles/README.md。
CLI usage
export DOUBAOYA_API_KEY="dyh_你的密钥" # 或放 ~/.doubaoya/key、Keychain(account-verify 会找)
# A. 从 Markdown 开始(渲染 → 传图 → 存草稿)
node scripts/pipeline.mjs --md article.md --title "标题" --config ./config.json
# B. 已有排好版的 HTML,直接发
node scripts/pipeline.mjs --html article.html --title "标题"
# C. 指定账号 + 公众号 + 本地封面 + 摘要
node scripts/pipeline.mjs --md a.md --title "标题" \
--account you@example.com --appid wx0123... --cover cover.png --digest "本期摘要"
# D. 干跑:只渲染+校验+扫描本地图,什么都不发
node scripts/pipeline.mjs --md a.md --title "标题" --dry-run
# E. 起可视化设计工作台选主题/封面/配图 → 产出 design-config.json(见「用设计工作台」)
node scripts/design-studio.mjs --md a.md --title "标题" # 网页里点完「保存配置」
# F. 用设计工作台产出的 design-config 跑流水线(套主题 + 设封面 + 按 h2 锚点注入配图)
node scripts/pipeline.mjs --md a.md --title "标题" --design a.design.json --dry-run
Parameters:--md | --html(choose one of two),--title(required),--account、--appid、--cover、--digest、
--config、--profile、--theme、--design、--output-processed-html、--base-url、--dry-run、--help。
only saves drafts: this pipelinehas nomass-send parameter whatsoever. Passing
--mass-send/--broadcast/any flag containing "mass send" will bedirectly rejected。
Replicate a reference layout style → reusable theme
Want your layout to look like a WeChat official account you admire, or some style you can describe? Extract it onceinto a theme.json,
thenreuse it forever(each render only needs --theme my-theme.json, see the CLI below). The theme contract'sauthorityis
themes/THEME-SCHEMA.md(top-level only has meta/palette/page/elements/decorations).
The validator is scripts/validate-theme.mjs, the applier is scripts/render-wechat-html.mjs --theme(or pipeline.mjs --theme)。
Writing a theme is a one-time job; the resulting
theme.jsonis used from then on. The default theme isthemes/benya-clean.json(this duck's premium "refreshing knowledge" style,recommended). Don't want to write from scratch? Start from the built-in themesthemes/benya-clean.json(default/recommended) /themes/magazine.json/themes/minimal.json/themes/knowledge.jsonpick the closest onecopy and modify。
Path A: Replicate the layout of a WeChat official account article (given a URL)
Flow = fetch → (zero-token heuristic) extract draft → LLM polish → validate → render. Among these, "extract draft" is afast zero-token first pass(using heuristics to pull out colors/layout), what really gets it to "polished" isyour (the LLM's) refine of the draft—this is exactly our advantage over purely heuristic tools.
The heuristic extraction algorithm comes from oaker-io/wewrite(MIT © 2026 OpenClaw) 's
analyze_styles(), ported toscripts/extract-theme.mjsas a zero-dependency Node rewrite (attribution in the file header +meta.notes)。
-
Fetch the reference body text(one-time style learning; it fetches apublicarticle, no login, no bulk):
node scripts/fetch-article.mjs --url "https://mp.weixin.qq.com/s/..." --out ref.htmlIt extracts the body text
#js_content,keeps all inlinestyle="…"(these inline styles are the data we want to analyze), removes<script>/<style>/注释, and prints astyle fingerprint: tag counts, most frequentcolors, usedfont sizes。If the link can't be fetched due to anti-scraping/expiry, the script will clearly tell you: open the article in a browser, view the source, and paste the body HTML into a local file for analysis (the authorization steps apply to any WeChat official account body HTML, not just this fetcher).
-
Extract candidate theme draft(
extract-theme.mjs,zero-token fast first pass):node scripts/extract-theme.mjs --html ref.html --name "参考风格" --out my-theme.json # 或一步到位(内部复用 fetch-article 抓正文): node scripts/extract-theme.mjs --url "https://mp.weixin.qq.com/s/..." --name "参考风格" --out my-theme.jsonIt groups inline styles by tag and pulls out the
text/text_light/ primary accent color(non-gray weighted count of strong/section/h1-3/span,font-size≥20pxweight ×5) / background / typography (font size, line height, letter spacing) / quote border and background / code color / corner radius, lay it onto a neutral base template(using{{token}}to tint), producing apassingvalidate-theme.mjsoftheme.jsondraft.When the signal is weak (e.g., 135/Xiumi exports write colors on
spanrather thanp), it willfall back to neutral defaults and warn "low confidence"—normal, leave it to the next refinement step. -
You (the LLM) refine the draft against the reference(our core value—the things heuristics cannot see, you fill in): Check each item against the CHECKLIST below
my-theme.json,fix the primary color, clean up dirty values (2em→ specific line height, move color from span totext, etc.), add decorative divider lines / title treatments:- Headings h1–h3: color bar / background block / centered or not / font size / font weight / text color (→
elements.h1..h3.style, decorative bar useswrapBefore)。 - Body
p:font-size/line-height/color/letter-spacing/ paragraph spacingmargin(→elements.p.styleandpage)。 - Quote
blockquote: left border / background / text color (→elements.blockquote.style)。 - List marker: bullet style (→
elements.li.marker+ul/ol/li.style)。 - Image: corner radius / shadow / centered / caption (→
elements.img.style+figureStyle/captionStyle)。 - Emphasis / link color:
strong/em/ahandling and primary color (→elements.strong/em/a+palette.accent/link)。 - Palette: check the extracted 3–5 colorsfor reasonableness (
text/heading/accent/accent2/muted/bgSoft/border/link); Heuristics often mistake a high-frequency decorative color for the primary color—check against the scraper fingerprint's "most frequent color" and change it back to the true primary color. - Divider decorations: fancy divider lines in the text →
elements.hr.html; whole-article card/border background →decorations.articleWrap; named divider fragments →decorations.sectionDivider(heuristics cannot extract these, so you fill them in).
- Headings h1–h3: color bar / background block / centered or not / font size / font weight / text color (→
-
Validate → Render:
node scripts/validate-theme.mjs my-theme.json # 有硬错误就按提示改 node scripts/render-wechat-html.mjs --md a.md --title "标题" --theme my-theme.json # 或直接进流水线: node scripts/pipeline.mjs --md a.md --title "标题" --theme my-theme.json
Honest expectations: HTML exported by official-account editors (Xiumi / 135, etc.) is very noisy—full of one-off inline styles.
extract-theme.mjsis aquick first pass, only guaranteeing that the rough color scheme skeleton is extracted; tuning it to "look like" the original depends on yourrefinementin step 3. Keep onlythe set of patterns that appear repeatedly, and don't treat every one-off style as part of the theme.
Path B: Write the theme directly from a text style description
No reference article needed:You (the agent) fill in theme.jsondirectly according to the schema based on the described tone, then validate and render.
Example: "cold minimalist magazine style" → low saturation palette, thin border/hairline hr, ample whitespace (large margin/line-height),
restrained near-small-caps headings (large letter spacing, non-high-saturation colors). Likewise, first validate-theme.mjs then render --theme.
To start, it's also recommendedCopy themes/benya-clean.json(default/recommended) / magazine.json(magazine style) / minimal.json(minimal) / knowledge.json(knowledge card), then revise again.
Everything is subject to themes/THEME-SCHEMA.md ; for the theme index see themes/README.md。
Prerequisites (layered: not every step requires binding a WeChat Official Account)
Common prerequisites:Node ≥ 18(built-in fetch), zero external dependencies. Beyond that,it is divided into three layers according to what you want to do——
Users who just want to see the layout effect, write/switch themes, or plan image placement,can get the job done without a key and without binding a WeChat Official Account:
| What you want to do | What else is needed besides Node | How to run it |
|---|---|---|
| md → WeChat Official Account inline-style HTML (local output / view layout effect) | None | node scripts/render-wechat-html.mjs --md a.md --theme themes/benya-clean.json --out a.html |
| Check themes / write themes / import external theme formats | None | scripts/validate-theme.mjs、scripts/import-theme.mjs、scripts/extract-theme.mjs --html ref.html |
| Replicate the layout of a certainpublicarticle | Public internet (no key needed) | scripts/fetch-article.mjs --url …、scripts/extract-theme.mjs --url … |
| Automatic image layout planning (deterministic rules, no LLM) | None | node scripts/plan-figures.mjs --md a.md |
Start the local design workbench: real-time preview, skin switching, automatic image placement, save design-config |
None (a key is only needed when you click "Generate" in the page) | node scripts/design-studio.mjs --md a.md --title "标题" |
| AI-generated cover / AI-generated images | One DOUBAOYA_API_KEY(costs money, current price fetched live) |
scripts/gen-image.mjs, or click Generate in the workbench |
| Use thedefault layoutyou set on doubaoya.com to render | One DOUBAOYA_API_KEY(if it cannot be fetched, it silently falls back to the local theme without interruption) |
The pipeline automatically fetches GET /api/wechat/theme?format=compiled |
Run pipeline.mjs(including --dry-run) |
key + WeChat Official Account already bound on doubaoya.com | node scripts/pipeline.mjs --md a.md --title "标题" --dry-run |
| Local image pre-upload / save draft | Same as above (saving a draft costs money, automatically falls back on failure) | pipeline.mjs、wechat-draft-publish |
⚠️
--dry-runis not a key-free preview. Although it sends nothing, the whoami account verification and draft prerequisite check (GET /api/wechat/status) both come before itahead of it: without a key it stops at "no available localDOUBAOYA_API_KEY", and with a key but no bound account it stops at "the target account has no bound WeChat Official Account". If you have not bound an account yet and just want to see how this article will look: userender-wechat-html.mjsor the design workbench (both are purely local). Note that when running the renderer alone,--titlewill insert a<h1>at the top of the body (for local preview); do not use that output for publishing—seedo not write a title in the body。
After binding the account and configuring the key, run --dry-runonce before publishing to confirm that the identity context, target account, WeChat Official Account, and local image scan are all correct, then formally save the draft.
Next step (after the draft is saved)
Once the draft is in the box, the user's end state of "wanting a publishable WeChat Official Account article" has already been achieved—this is usually the endpoint。 The mass-send hand is always in the user's own hands: This skill has no mass-send path whatsoever; ask them to go to the official account backend and confirm the draft with their own eyes (formatting, cover, images all correct) before manually mass-sending.
After publishing, if the user still wants to go further, options include:
| What the user wants next | Next step |
|---|---|
| After a few days of data, look at this account's posting performance / run a health check | wechat-account-analyzer |
| Track your own or competitors' posting cadence | doubaoya(hit the official account posting list endpoint) |
| Pull the body text of already-published articles and archive them | doubaoya |
| Use retrospective signals to mine the next round of topics | doubaoya(mine topics / chase hot topics, and also pull samples from here to start writing) |
| Can't say clearly which step to reach | dby(hop-by-hop navigation of the official account flywheel) |
Update this skill
npx skills update wechat-article-pipeline # 全局安装的加 -g
Recent changes:
- Invocation knowledge changed to a gateway delegation form: The three capabilities used by this Skill now name the operationKey together with the detail endpoint, and the invocation protocol is inlined verbatim (seeinvocations all blow up), input specs are always pulled fresh from the detail endpoint before invocation——the returned-field tables and pricing numbers that were previously baked into the body have been deleted entirely (a contract baked into the distributed artifact is bound to drift, and prices will be adjusted silently).The ten-step SOP and terminal-state judgment have not moved a single step. At the same time, step 5 is clarified into two paths: clean markdown goes through the free platform rendering capability; local images / component syntax / no key only then goes through the local renderer.
- When no local theme is explicitly pinned, the pipeline now willprefer to pull the server-compiled theme(the default formatting you set on doubaoya.com,
GET /api/wechat/theme?format=compiled; if it can't be pulled, gracefully fall back to local). For those who want to pin down the local theme,--themeorconfig.mdThemejust write it as a path. At the same time,validate-theme.mjsfor engine-2 themes (meta.engine:2/tokens/ tokens with a dot) upgraded from a warning to ahard error——such themes can only use the server-compiled version.- The default Markdown formatting theme has been switched to
benya-clean(this duck · knowledge fresh). To keep using the old versionmagazine(magazine style), inconfig.jsonpointmdThemeback tothemes/magazine.json, or add--theme themes/magazine.json。