feat(baoyu-post-to-wechat): adapt to new WeChat UI and fix digest/cover handling

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.
This commit is contained in:
Jim Liu 宝玉 2026-02-10 15:46:09 -06:00
parent 6cbf0f4e52
commit 569beebdd6
6 changed files with 178 additions and 71 deletions

View File

@ -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: 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 ```bash
/baoyu-post-to-wechat 图 --markdown article.md --images ./photos/ /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 图 --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 图 --title "标题" --content "内容" --image img1.png --submit
``` ```
**Article (文章)** - Full markdown/HTML with rich formatting: **Article (文章)** - Full markdown/HTML with rich formatting:

View File

@ -519,12 +519,12 @@ npx skills add jimliu/baoyu-skills
发布内容到微信公众号,支持两种模式: 发布内容到微信公众号,支持两种模式:
**图模式** - 多图配短标题和正文: **图模式** - 多图配短标题和正文:
```bash ```bash
/baoyu-post-to-wechat 图 --markdown article.md --images ./photos/ /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 图 --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 图 --title "标题" --content "内容" --image img1.png --submit
``` ```
**文章模式** - 完整 markdown/HTML 富文本格式: **文章模式** - 完整 markdown/HTML 富文本格式:

View File

@ -1,6 +1,6 @@
--- ---
name: baoyu-post-to-wechat 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 # Post to WeChat Official Account

View File

@ -1,7 +1,9 @@
# Image-Text Posting (图发表) # Image-Text Posting (图发表, formerly 图文)
Post image-text messages with multiple images to WeChat Official Account. 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 ## Usage
```bash ```bash

View File

@ -517,6 +517,13 @@ async function main(): Promise<void> {
process.exit(1); 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}`); console.error(`[wechat-api] Title: ${title}`);
if (author) console.error(`[wechat-api] Author: ${author}`); if (author) console.error(`[wechat-api] Author: ${author}`);
if (digest) console.error(`[wechat-api] Digest: ${digest.slice(0, 50)}...`); if (digest) console.error(`[wechat-api] Digest: ${digest.slice(0, 50)}...`);
@ -547,11 +554,14 @@ async function main(): Promise<void> {
htmlContent = processedHtml; htmlContent = processedHtml;
let thumbMediaId = ""; let thumbMediaId = "";
const coverPath = args.cover || const rawCoverPath = args.cover ||
frontmatter.featureImage || frontmatter.featureImage ||
frontmatter.coverImage || frontmatter.coverImage ||
frontmatter.cover || frontmatter.cover ||
frontmatter.image; frontmatter.image;
const coverPath = rawCoverPath && !path.isAbsolute(rawCoverPath) && args.cover
? path.resolve(process.cwd(), rawCoverPath)
: rawCoverPath;
if (coverPath) { if (coverPath) {
console.error(`[wechat-api] Uploading cover: ${coverPath}`); console.error(`[wechat-api] Uploading cover: ${coverPath}`);

View File

@ -397,7 +397,7 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise<void>
await sleep(2000); 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', { const menuResult = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', {
expression: ` expression: `
const menuItems = document.querySelectorAll('.new-creation__menu .new-creation__menu-item'); const menuItems = document.querySelectorAll('.new-creation__menu .new-creation__menu-item');
@ -417,7 +417,7 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise<void>
const initialIds = new Set(initialTargets.targetInfos.map(t => t.targetId)); const initialIds = new Set(initialTargets.targetInfos.map(t => t.targetId));
console.log(`[wechat-browser] Initial targets count: ${initialTargets.targetInfos.length}`); 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', { const menuPos = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', {
expression: ` expression: `
(function() { (function() {
@ -427,10 +427,10 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise<void>
const title = item.querySelector('.new-creation__menu-title'); const title = item.querySelector('.new-creation__menu-title');
const text = title?.textContent?.trim() || ''; const text = title?.textContent?.trim() || '';
console.log('Menu item text:', text); console.log('Menu item text:', text);
if (text === '图文') { if (text === '图文' || text === '贴图') {
item.scrollIntoView({ block: 'center' }); item.scrollIntoView({ block: 'center' });
const rect = item.getBoundingClientRect(); 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 }); 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<void>
console.log(`[wechat-browser] Menu position: ${menuPos.result.value}`); console.log(`[wechat-browser] Menu position: ${menuPos.result.value}`);
const pos = menuPos.result.value !== 'null' ? JSON.parse(menuPos.result.value) : null; 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', { await cdp.send('Input.dispatchMouseEvent', {
type: 'mousePressed', type: 'mousePressed',
x: pos.x, x: pos.x,
@ -533,11 +533,22 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise<void>
console.log(`[wechat-browser] Images: ${absolutePaths.join(', ')}`); console.log(`[wechat-browser] Images: ${absolutePaths.join(', ')}`);
const { root } = await cdp.send<{ root: { nodeId: number } }>('DOM.getDocument', {}, { sessionId }); 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, nodeId: root.nodeId,
selector: '.js_upload_btn_container input[type=file]', selector: '.js_upload_btn_container input[type=file]',
}, { sessionId }); }, { 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'); if (!nodeId) throw new Error('File input not found');
await cdp.send('DOM.setFileInputFiles', { await cdp.send('DOM.setFileInputFiles', {
@ -545,7 +556,30 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise<void>
files: absolutePaths, files: absolutePaths,
}, { sessionId }); }, { 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...'); console.log('[wechat-browser] Filling title...');
await cdp.send('Runtime.evaluate', { await cdp.send('Runtime.evaluate', {
@ -561,73 +595,134 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise<void>
}, { sessionId }); }, { sessionId });
await sleep(500); await sleep(500);
console.log('[wechat-browser] Clicking on content editor...'); console.log('[wechat-browser] Filling content...');
const editorPos = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', { // Try ProseMirror editor first (new WeChat UI), then fallback to old editor
const contentResult = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', {
expression: ` expression: `
(function() { (function() {
const editor = document.querySelector('.js_pmEditorArea'); const contentHtml = ${JSON.stringify('<p>' + content.split('\n').filter(l => l.trim()).join('</p><p>') + '</p>')};
if (editor) {
const rect = editor.getBoundingClientRect(); // New UI: ProseMirror contenteditable
return JSON.stringify({ x: rect.x + 50, y: rect.y + 20 }); 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, returnByValue: true,
}, { sessionId }); }, { sessionId });
if (editorPos.result.value === 'null') throw new Error('Content editor not found'); const contentStatus = contentResult.result.value;
const editorClickPos = JSON.parse(editorPos.result.value); console.log(`[wechat-browser] Content result: ${contentStatus}`);
await cdp.send('Input.dispatchMouseEvent', { if (contentStatus === 'editor_not_found') {
type: 'mousePressed', throw new Error('Content editor not found');
x: editorClickPos.x, }
y: editorClickPos.y,
button: 'left', // Fallback: old editor uses keyboard simulation
clickCount: 1, if (contentStatus.startsWith('{')) {
}, { sessionId }); const editorClickPos = JSON.parse(contentStatus);
await sleep(50); if (editorClickPos.type === 'old') {
await cdp.send('Input.dispatchMouseEvent', { console.log('[wechat-browser] Using old editor with keyboard simulation...');
type: 'mouseReleased', await cdp.send('Input.dispatchMouseEvent', {
x: editorClickPos.x, type: 'mousePressed',
y: editorClickPos.y, x: editorClickPos.x,
button: 'left', y: editorClickPos.y,
clickCount: 1, button: 'left',
}, { sessionId }); clickCount: 1,
await sleep(300); }, { sessionId });
await sleep(50);
console.log('[wechat-browser] Typing content with keyboard simulation...'); await cdp.send('Input.dispatchMouseEvent', {
const lines = content.split('\n'); type: 'mouseReleased',
for (let i = 0; i < lines.length; i++) { x: editorClickPos.x,
const line = lines[i]; y: editorClickPos.y,
if (line.length > 0) { button: 'left',
await cdp.send('Input.insertText', { text: line }, { sessionId }); clickCount: 1,
} }, { sessionId });
if (i < lines.length - 1) { await sleep(300);
await cdp.send('Input.dispatchKeyEvent', {
type: 'keyDown', const lines = content.split('\n');
key: 'Enter', for (let i = 0; i < lines.length; i++) {
code: 'Enter', const line = lines[i];
windowsVirtualKeyCode: 13, if (line!.length > 0) {
}, { sessionId }); await cdp.send('Input.insertText', { text: line }, { sessionId });
await cdp.send('Input.dispatchKeyEvent', { }
type: 'keyUp', if (i < lines.length - 1) {
key: 'Enter', await cdp.send('Input.dispatchKeyEvent', {
code: 'Enter', type: 'keyDown',
windowsVirtualKeyCode: 13, key: 'Enter',
}, { sessionId }); code: 'Enter',
} windowsVirtualKeyCode: 13,
await sleep(50); }, { 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); await sleep(500);
if (submit) { if (submit) {
console.log('[wechat-browser] Saving as draft...'); console.log('[wechat-browser] Saving as draft...');
await cdp.send('Runtime.evaluate', { const submitResult = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', {
expression: `document.querySelector('#js_submit')?.click()`, 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 }); }, { sessionId });
console.log(`[wechat-browser] Submit result: ${submitResult.result.value}`);
await sleep(3000); 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!'); console.log('[wechat-browser] Draft saved!');
} else { } else {
console.log('[wechat-browser] Article composed (preview mode). Add --submit to save as draft.'); 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<void>
} }
function printUsage(): never { function printUsage(): never {
console.log(`Post image-text () to WeChat Official Account console.log(`Post image-text (图) to WeChat Official Account
Usage: Usage:
npx -y bun wechat-browser.ts [options] npx -y bun wechat-browser.ts [options]