From 569beebdd687038102135e89315d0cb23127c4e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jim=20Liu=20=E5=AE=9D=E7=8E=89?= Date: Tue, 10 Feb 2026 15:46:09 -0600 Subject: [PATCH] feat(baoyu-post-to-wechat): adapt to new WeChat UI and fix digest/cover handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename 图文 to 贴图 throughout; add ProseMirror editor support with old editor fallback; add fallback file input selector; add upload progress monitoring; improve save button detection with toast verification; truncate digest > 120 chars at punctuation boundary; fix cover image relative path resolution. --- README.md | 8 +- README.zh.md | 8 +- skills/baoyu-post-to-wechat/SKILL.md | 2 +- .../references/image-text-posting.md | 4 +- .../scripts/wechat-api.ts | 12 +- .../scripts/wechat-browser.ts | 215 +++++++++++++----- 6 files changed, 178 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 5cc1334..763db5a 100644 --- a/README.md +++ b/README.md @@ -519,12 +519,12 @@ Post content and articles to X (Twitter). Supports regular posts with images and Post content to WeChat Official Account (微信公众号). Two modes available: -**Image-Text (图文)** - Multiple images with short title/content: +**Image-Text (贴图)** - Multiple images with short title/content: ```bash -/baoyu-post-to-wechat 图文 --markdown article.md --images ./photos/ -/baoyu-post-to-wechat 图文 --markdown article.md --image img1.png --image img2.png --image img3.png -/baoyu-post-to-wechat 图文 --title "标题" --content "内容" --image img1.png --submit +/baoyu-post-to-wechat 贴图 --markdown article.md --images ./photos/ +/baoyu-post-to-wechat 贴图 --markdown article.md --image img1.png --image img2.png --image img3.png +/baoyu-post-to-wechat 贴图 --title "标题" --content "内容" --image img1.png --submit ``` **Article (文章)** - Full markdown/HTML with rich formatting: diff --git a/README.zh.md b/README.zh.md index c86eebc..7a55f02 100644 --- a/README.zh.md +++ b/README.zh.md @@ -519,12 +519,12 @@ npx skills add jimliu/baoyu-skills 发布内容到微信公众号,支持两种模式: -**图文模式** - 多图配短标题和正文: +**贴图模式** - 多图配短标题和正文: ```bash -/baoyu-post-to-wechat 图文 --markdown article.md --images ./photos/ -/baoyu-post-to-wechat 图文 --markdown article.md --image img1.png --image img2.png --image img3.png -/baoyu-post-to-wechat 图文 --title "标题" --content "内容" --image img1.png --submit +/baoyu-post-to-wechat 贴图 --markdown article.md --images ./photos/ +/baoyu-post-to-wechat 贴图 --markdown article.md --image img1.png --image img2.png --image img3.png +/baoyu-post-to-wechat 贴图 --title "标题" --content "内容" --image img1.png --submit ``` **文章模式** - 完整 markdown/HTML 富文本格式: diff --git a/skills/baoyu-post-to-wechat/SKILL.md b/skills/baoyu-post-to-wechat/SKILL.md index 4a293eb..90a625f 100644 --- a/skills/baoyu-post-to-wechat/SKILL.md +++ b/skills/baoyu-post-to-wechat/SKILL.md @@ -1,6 +1,6 @@ --- name: baoyu-post-to-wechat -description: Posts content to WeChat Official Account (微信公众号) via API or Chrome CDP. Supports article posting (文章) with HTML, markdown, or plain text input, and image-text posting (图文) with multiple images. Use when user mentions "发布公众号", "post to wechat", "微信公众号", or "图文/文章". +description: Posts content to WeChat Official Account (微信公众号) via API or Chrome CDP. Supports article posting (文章) with HTML, markdown, or plain text input, and image-text posting (贴图, formerly 图文) with multiple images. Use when user mentions "发布公众号", "post to wechat", "微信公众号", or "贴图/图文/文章". --- # Post to WeChat Official Account diff --git a/skills/baoyu-post-to-wechat/references/image-text-posting.md b/skills/baoyu-post-to-wechat/references/image-text-posting.md index 6ee3a94..f04e8c1 100644 --- a/skills/baoyu-post-to-wechat/references/image-text-posting.md +++ b/skills/baoyu-post-to-wechat/references/image-text-posting.md @@ -1,7 +1,9 @@ -# Image-Text Posting (图文发表) +# Image-Text Posting (贴图发表, formerly 图文) Post image-text messages with multiple images to WeChat Official Account. +> **Note**: WeChat has renamed "图文" to "贴图" in the Official Account menu (as of 2026). + ## Usage ```bash diff --git a/skills/baoyu-post-to-wechat/scripts/wechat-api.ts b/skills/baoyu-post-to-wechat/scripts/wechat-api.ts index 9c18436..2d093d2 100644 --- a/skills/baoyu-post-to-wechat/scripts/wechat-api.ts +++ b/skills/baoyu-post-to-wechat/scripts/wechat-api.ts @@ -517,6 +517,13 @@ async function main(): Promise { process.exit(1); } + if (digest && digest.length > 120) { + const truncated = digest.slice(0, 117); + const lastPunct = Math.max(truncated.lastIndexOf("。"), truncated.lastIndexOf(","), truncated.lastIndexOf(";"), truncated.lastIndexOf("、")); + digest = lastPunct > 80 ? truncated.slice(0, lastPunct + 1) : truncated + "..."; + console.error(`[wechat-api] Digest truncated to ${digest.length} chars`); + } + console.error(`[wechat-api] Title: ${title}`); if (author) console.error(`[wechat-api] Author: ${author}`); if (digest) console.error(`[wechat-api] Digest: ${digest.slice(0, 50)}...`); @@ -547,11 +554,14 @@ async function main(): Promise { htmlContent = processedHtml; let thumbMediaId = ""; - const coverPath = args.cover || + const rawCoverPath = args.cover || frontmatter.featureImage || frontmatter.coverImage || frontmatter.cover || frontmatter.image; + const coverPath = rawCoverPath && !path.isAbsolute(rawCoverPath) && args.cover + ? path.resolve(process.cwd(), rawCoverPath) + : rawCoverPath; if (coverPath) { console.error(`[wechat-api] Uploading cover: ${coverPath}`); diff --git a/skills/baoyu-post-to-wechat/scripts/wechat-browser.ts b/skills/baoyu-post-to-wechat/scripts/wechat-browser.ts index 19de57d..d7f0670 100644 --- a/skills/baoyu-post-to-wechat/scripts/wechat-browser.ts +++ b/skills/baoyu-post-to-wechat/scripts/wechat-browser.ts @@ -397,7 +397,7 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise await sleep(2000); - console.log('[wechat-browser] Looking for "图文" menu...'); + console.log('[wechat-browser] Looking for "贴图" menu...'); const menuResult = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', { expression: ` const menuItems = document.querySelectorAll('.new-creation__menu .new-creation__menu-item'); @@ -417,7 +417,7 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise const initialIds = new Set(initialTargets.targetInfos.map(t => t.targetId)); console.log(`[wechat-browser] Initial targets count: ${initialTargets.targetInfos.length}`); - console.log('[wechat-browser] Finding "图文" menu position...'); + console.log('[wechat-browser] Finding "贴图" menu position...'); const menuPos = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', { expression: ` (function() { @@ -427,10 +427,10 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise const title = item.querySelector('.new-creation__menu-title'); const text = title?.textContent?.trim() || ''; console.log('Menu item text:', text); - if (text === '图文') { + if (text === '图文' || text === '贴图') { item.scrollIntoView({ block: 'center' }); const rect = item.getBoundingClientRect(); - console.log('Found 图文,rect:', JSON.stringify(rect)); + console.log('Found 贴图,rect:', JSON.stringify(rect)); return JSON.stringify({ x: rect.x + rect.width / 2, y: rect.y + rect.height / 2, width: rect.width, height: rect.height }); } } @@ -442,9 +442,9 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise console.log(`[wechat-browser] Menu position: ${menuPos.result.value}`); const pos = menuPos.result.value !== 'null' ? JSON.parse(menuPos.result.value) : null; - if (!pos) throw new Error('图文 menu not found or not visible'); + if (!pos) throw new Error('贴图 menu not found or not visible'); - console.log('[wechat-browser] Clicking "图文" menu with mouse events...'); + console.log('[wechat-browser] Clicking "贴图" menu with mouse events...'); await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: pos.x, @@ -533,11 +533,22 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise console.log(`[wechat-browser] Images: ${absolutePaths.join(', ')}`); const { root } = await cdp.send<{ root: { nodeId: number } }>('DOM.getDocument', {}, { sessionId }); - const { nodeId } = await cdp.send<{ nodeId: number }>('DOM.querySelector', { + + // Try primary selector, then fallback to any multi-file image input + let { nodeId } = await cdp.send<{ nodeId: number }>('DOM.querySelector', { nodeId: root.nodeId, selector: '.js_upload_btn_container input[type=file]', }, { sessionId }); + if (!nodeId) { + console.log('[wechat-browser] Primary file input not found, trying fallback selector...'); + const fallback = await cdp.send<{ nodeId: number }>('DOM.querySelector', { + nodeId: root.nodeId, + selector: 'input[type=file][multiple][accept*="image"]', + }, { sessionId }); + nodeId = fallback.nodeId; + } + if (!nodeId) throw new Error('File input not found'); await cdp.send('DOM.setFileInputFiles', { @@ -545,7 +556,30 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise files: absolutePaths, }, { sessionId }); - await sleep(1000); + // Dispatch change event to trigger the upload + await cdp.send('Runtime.evaluate', { + expression: ` + const fileInput = document.querySelector('.js_upload_btn_container input[type=file]') || document.querySelector('input[type=file][multiple][accept*="image"]'); + if (fileInput) fileInput.dispatchEvent(new Event('change', { bubbles: true })); + `, + }, { sessionId }); + + // Wait for images to upload + console.log('[wechat-browser] Waiting for images to upload...'); + const targetCount = absolutePaths.length; + for (let i = 0; i < 30; i++) { + await sleep(2000); + const uploadCheck = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', { + expression: ` + const thumbs = document.querySelectorAll('.weui-desktop-upload__thumb, .pic_item, [class*=upload_thumb]'); + JSON.stringify({ uploaded: thumbs.length }); + `, + returnByValue: true, + }, { sessionId }); + const status = JSON.parse(uploadCheck.result.value); + console.log(`[wechat-browser] Upload progress: ${status.uploaded}/${targetCount}`); + if (status.uploaded >= targetCount) break; + } console.log('[wechat-browser] Filling title...'); await cdp.send('Runtime.evaluate', { @@ -561,73 +595,134 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise }, { sessionId }); await sleep(500); - console.log('[wechat-browser] Clicking on content editor...'); - const editorPos = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', { + console.log('[wechat-browser] Filling content...'); + // Try ProseMirror editor first (new WeChat UI), then fallback to old editor + const contentResult = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', { expression: ` (function() { - const editor = document.querySelector('.js_pmEditorArea'); - if (editor) { - const rect = editor.getBoundingClientRect(); - return JSON.stringify({ x: rect.x + 50, y: rect.y + 20 }); + const contentHtml = ${JSON.stringify('

' + content.split('\n').filter(l => l.trim()).join('

') + '

')}; + + // New UI: ProseMirror contenteditable + const pm = document.querySelector('.ProseMirror[contenteditable=true]'); + if (pm) { + pm.innerHTML = contentHtml; + pm.dispatchEvent(new Event('input', { bubbles: true })); + return 'ProseMirror: content set, length=' + pm.textContent.length; } - return 'null'; + + // Old UI: .js_pmEditorArea + const oldEditor = document.querySelector('.js_pmEditorArea'); + if (oldEditor) { + return JSON.stringify({ type: 'old', x: oldEditor.getBoundingClientRect().x + 50, y: oldEditor.getBoundingClientRect().y + 20 }); + } + + return 'editor_not_found'; })() `, returnByValue: true, }, { sessionId }); - if (editorPos.result.value === 'null') throw new Error('Content editor not found'); - const editorClickPos = JSON.parse(editorPos.result.value); + const contentStatus = contentResult.result.value; + console.log(`[wechat-browser] Content result: ${contentStatus}`); - await cdp.send('Input.dispatchMouseEvent', { - type: 'mousePressed', - x: editorClickPos.x, - y: editorClickPos.y, - button: 'left', - clickCount: 1, - }, { sessionId }); - await sleep(50); - await cdp.send('Input.dispatchMouseEvent', { - type: 'mouseReleased', - x: editorClickPos.x, - y: editorClickPos.y, - button: 'left', - clickCount: 1, - }, { sessionId }); - await sleep(300); - - console.log('[wechat-browser] Typing content with keyboard simulation...'); - const lines = content.split('\n'); - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (line.length > 0) { - await cdp.send('Input.insertText', { text: line }, { sessionId }); - } - if (i < lines.length - 1) { - await cdp.send('Input.dispatchKeyEvent', { - type: 'keyDown', - key: 'Enter', - code: 'Enter', - windowsVirtualKeyCode: 13, - }, { sessionId }); - await cdp.send('Input.dispatchKeyEvent', { - type: 'keyUp', - key: 'Enter', - code: 'Enter', - windowsVirtualKeyCode: 13, - }, { sessionId }); - } - await sleep(50); + if (contentStatus === 'editor_not_found') { + throw new Error('Content editor not found'); + } + + // Fallback: old editor uses keyboard simulation + if (contentStatus.startsWith('{')) { + const editorClickPos = JSON.parse(contentStatus); + if (editorClickPos.type === 'old') { + console.log('[wechat-browser] Using old editor with keyboard simulation...'); + await cdp.send('Input.dispatchMouseEvent', { + type: 'mousePressed', + x: editorClickPos.x, + y: editorClickPos.y, + button: 'left', + clickCount: 1, + }, { sessionId }); + await sleep(50); + await cdp.send('Input.dispatchMouseEvent', { + type: 'mouseReleased', + x: editorClickPos.x, + y: editorClickPos.y, + button: 'left', + clickCount: 1, + }, { sessionId }); + await sleep(300); + + const lines = content.split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line!.length > 0) { + await cdp.send('Input.insertText', { text: line }, { sessionId }); + } + if (i < lines.length - 1) { + await cdp.send('Input.dispatchKeyEvent', { + type: 'keyDown', + key: 'Enter', + code: 'Enter', + windowsVirtualKeyCode: 13, + }, { sessionId }); + await cdp.send('Input.dispatchKeyEvent', { + type: 'keyUp', + key: 'Enter', + code: 'Enter', + windowsVirtualKeyCode: 13, + }, { sessionId }); + } + await sleep(50); + } + console.log('[wechat-browser] Content typed via keyboard.'); + } } - console.log('[wechat-browser] Content typed.'); await sleep(500); if (submit) { console.log('[wechat-browser] Saving as draft...'); - await cdp.send('Runtime.evaluate', { - expression: `document.querySelector('#js_submit')?.click()`, + const submitResult = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', { + expression: ` + (function() { + // Try new UI: find button by text + const allBtns = document.querySelectorAll('button'); + for (const btn of allBtns) { + const text = btn.textContent?.trim(); + if (text === '保存为草稿') { + btn.click(); + return 'clicked:保存为草稿'; + } + } + // Fallback: old UI selector + const oldBtn = document.querySelector('#js_submit'); + if (oldBtn) { + oldBtn.click(); + return 'clicked:#js_submit'; + } + // List available buttons for debugging + const btnTexts = []; + allBtns.forEach(b => { + const t = b.textContent?.trim(); + if (t && t.length < 20) btnTexts.push(t); + }); + return 'not_found:' + btnTexts.join(','); + })() + `, + returnByValue: true, }, { sessionId }); + console.log(`[wechat-browser] Submit result: ${submitResult.result.value}`); await sleep(3000); + + // Verify save success by checking for toast + const toastCheck = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', { + expression: ` + const toasts = document.querySelectorAll('.weui-desktop-toast, [class*=toast]'); + const msgs = []; + toasts.forEach(t => { const text = t.textContent?.trim(); if (text) msgs.push(text); }); + JSON.stringify(msgs); + `, + returnByValue: true, + }, { sessionId }); + console.log(`[wechat-browser] Toast messages: ${toastCheck.result.value}`); console.log('[wechat-browser] Draft saved!'); } else { console.log('[wechat-browser] Article composed (preview mode). Add --submit to save as draft.'); @@ -641,7 +736,7 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise } function printUsage(): never { - console.log(`Post image-text (图文) to WeChat Official Account + console.log(`Post image-text (贴图) to WeChat Official Account Usage: npx -y bun wechat-browser.ts [options]