From af4fc083fe8b3fb0738bdfe298d1ecd71db9e10b Mon Sep 17 00:00:00 2001 From: leether Date: Fri, 17 Jul 2026 21:22:56 +0800 Subject: [PATCH] Fix draft/update 47001: shape articles as single object The update branch reused the draft/add payload (articles as array); draft/update requires a single article object and rejects the array with errcode 47001 (data format error). Root cause triple-verified: code read, official doc (articles: object), and a live A/B probe on XINZHE (array -> 47001, object -> ok). Add applyDraftUpdateShape and call it on a shallow copy at the update branch so downstream evidence bookkeeping keeps the array shape. Regression test pins both the object shape and the copy-at-call-site contract. Live-verified end-to-end on the relay with a probe draft (draft/update ok, completion_status: verified), probe cleaned up. --- CHANGELOG.md | 1 + harness/test-draft-update-shape.mjs | 35 +++++++++++++++++++++++++++++ package.json | 2 +- scripts/create_wechat_draft.mjs | 20 ++++++++++++++--- 4 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 harness/test-draft-update-shape.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 43689f4..db42b77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ ### Fixed +- **draft/update 47001**:`create_wechat_draft --update` 复用了 draft/add 的 `articles` 数组形状,而 draft/update 要求单篇文章对象,微信返回 47001 data format error(relay 现场 A/B 复现:数组 → 47001,对象 → ok);新增 `applyDraftUpdateShape` 统一转换,harness 测试钉住形状 - **Pipeline 自洽修补**:显式串联 render/preflight/orchestrator 的封面图和 `--skip-image-check` 参数,避免 `pre_image_missing` 隐式破坏默认入口 - **AutoHeal 阻断逻辑**:preflight JSON 改为完整解析,存在不可自动修复的 L1/Agent 失败时不再继续进入 bundle - **Bundle/relay 契约**:封面图纳入 bundle,relay 目录日期由本地生成,远程推送命令正确传递标题、作者、封面和 `--crop-235-1` diff --git a/harness/test-draft-update-shape.mjs b/harness/test-draft-update-shape.mjs new file mode 100644 index 0000000..d71cd98 --- /dev/null +++ b/harness/test-draft-update-shape.mjs @@ -0,0 +1,35 @@ +import assert from "node:assert/strict"; +import { applyDraftUpdateShape } from "../scripts/create_wechat_draft.mjs"; + +// draft/update requires `articles` as a single article object, while +// draft/add uses an array. Reusing the array shape yields WeChat errcode +// 47001 (data format error) -- verified live on 2026-07-17 against the +// XINZHE account (array -> 47001, object -> ok). + +const payload = { articles: [{ title: "T", author: "A", content: "

x

" }] }; +const shaped = applyDraftUpdateShape(payload, "MID123", "2"); +assert.equal(shaped, payload, "returns the same payload for convenience"); +assert.equal(payload.media_id, "MID123"); +assert.equal(payload.index, 2); +assert.equal(Array.isArray(payload.articles), false, "articles must become a single object"); +assert.equal(payload.articles.title, "T"); +assert.equal(payload.articles.author, "A"); + +// Already-object payloads pass through untouched. +const already = { articles: { title: "T2" } }; +applyDraftUpdateShape(already, "MID9", "0"); +assert.equal(already.articles.title, "T2"); +assert.equal(already.index, 0); + +// Missing or invalid index falls back to 0. +const fallback = { articles: [{ title: "T3" }] }; +applyDraftUpdateShape(fallback, "MID5", "not-a-number"); +assert.equal(fallback.index, 0); + +// Call-site contract: callers pass a shallow copy so downstream bookkeeping +// (push-result, audit) keeps reading plan.payload.articles[0] as an array. +const src = { articles: [{ title: "T4" }] }; +applyDraftUpdateShape({ ...src }, "MID7", "0"); +assert.equal(Array.isArray(src.articles), true, "original payload keeps array shape"); + +console.log("test-draft-update-shape: ok"); diff --git a/package.json b/package.json index 5f9877f..0c6afce 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "render": "node scripts/render_wechat_editorial.mjs", "publish": "node scripts/create_wechat_draft.mjs", "relay:check": "node scripts/sync_relay_scripts.mjs --check --json", - "check": "find scripts harness -name '*.mjs' -print | sort | xargs -n1 node --check && node harness/test-code-generator-contract.mjs && node harness/test-preflight-fixtures.mjs && node harness/test-orchestrator-command-contract.mjs && node harness/test-relay-script-sync-contract.mjs && node harness/test-publish-evidence-contract.mjs && node harness/test-reconcile-wechat-drafts.mjs && node harness/test-self-report-no-write.mjs && node harness/run-generated-check-tests.mjs" + "check": "find scripts harness -name '*.mjs' -print | sort | xargs -n1 node --check && node harness/test-code-generator-contract.mjs && node harness/test-preflight-fixtures.mjs && node harness/test-orchestrator-command-contract.mjs && node harness/test-relay-script-sync-contract.mjs && node harness/test-publish-evidence-contract.mjs && node harness/test-draft-update-shape.mjs && node harness/test-reconcile-wechat-drafts.mjs && node harness/test-self-report-no-write.mjs && node harness/run-generated-check-tests.mjs" }, "engines": { "node": ">=18.0.0" diff --git a/scripts/create_wechat_draft.mjs b/scripts/create_wechat_draft.mjs index cddbd0d..a9c9e0c 100755 --- a/scripts/create_wechat_draft.mjs +++ b/scripts/create_wechat_draft.mjs @@ -9,6 +9,20 @@ import { parseArgs, printHelp, requireArg } from "./lib/memory-lib.mjs"; const stableTokenEndpoint = "https://api.weixin.qq.com/cgi-bin/stable_token"; const draftAddEndpoint = "https://api.weixin.qq.com/cgi-bin/draft/add"; const draftUpdateEndpoint = "https://api.weixin.qq.com/cgi-bin/draft/update"; + +export function applyDraftUpdateShape(payload, updateMediaId, updateIndex) { + // draft/add expects `articles` as an array, but draft/update requires a + // single article object. Reusing the add shape for update fails with + // WeChat errcode 47001 (data format error). Mutates the given payload; + // callers should pass a shallow copy so downstream bookkeeping keeps the + // original array shape. Returns the payload for convenience. + payload.media_id = String(updateMediaId); + payload.index = parseInt(updateIndex, 10) || 0; + if (Array.isArray(payload.articles)) { + payload.articles = payload.articles[0]; + } + return payload; +} const imageAddEndpoint = "https://api.weixin.qq.com/cgi-bin/material/add_material"; const defaultEnvPath = path.resolve(process.cwd(), ".env"); const targetCoverAspectRatio = 2.35; @@ -1531,14 +1545,14 @@ export async function createWechatDraft(argv = process.argv.slice(2)) { let endpoint = draftAddEndpoint; const isUpdate = Boolean(plan.updateMediaId); + let requestPayload = plan.payload; if (isUpdate) { endpoint = draftUpdateEndpoint; - plan.payload.media_id = plan.updateMediaId; - plan.payload.index = parseInt(plan.updateIndex, 10) || 0; + requestPayload = applyDraftUpdateShape({ ...plan.payload }, plan.updateMediaId, plan.updateIndex); } const url = `${endpoint}?access_token=${encodeURIComponent(accessToken)}`; - const result = postJson(url, plan.payload); + const result = postJson(url, requestPayload); if (result.errcode && result.errcode !== 0) { const action = isUpdate ? "draft/update" : "draft/add";