Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Post articles and image-text content to WeChat Official Account via API or Chrome CDP, with markdown-to-WeChat HTML conversion.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/wechat-agent-browser.ts
1import { spawnSync } from 'node:child_process';2import fs from 'node:fs';3import path from 'node:path';4import process from 'node:process';56const WECHAT_URL = 'https://mp.weixin.qq.com/';7const SESSION = 'wechat-post';89function sleep(ms: number): Promise<void> {10return new Promise((resolve) => setTimeout(resolve, ms));11}1213function quoteForLog(arg: string): string {14return /[\s"'\\]/.test(arg) ? JSON.stringify(arg) : arg;15}1617function toSafeJsStringLiteral(value: string): string {18return JSON.stringify(value)19.replace(/\u2028/g, '\\u2028')20.replace(/\u2029/g, '\\u2029');21}2223function runAgentBrowser(args: string[]): {24success: boolean;25output: string;26spawnError?: string;27} {28const result = spawnSync('agent-browser', ['--session', SESSION, ...args], {29encoding: 'utf8',30stdio: ['pipe', 'pipe', 'pipe']31});32const spawnError = result.error?.message?.trim();33const output = result.stdout || result.stderr || '';34return {35success: result.status === 0,36output: output || spawnError || '',37spawnError38};39}4041function ab(args: string[], json = false): string {42const fullArgs = json ? [...args, '--json'] : args;43console.log(`[ab] agent-browser --session ${SESSION} ${fullArgs.map(quoteForLog).join(' ')}`);44const result = runAgentBrowser(fullArgs);45if (result.spawnError) {46throw new Error(`agent-browser failed to start: ${result.spawnError}`);47}48if (!result.success) {49console.error(`[ab] Error: ${result.output.trim()}`);50}51return result.output.trim();52}5354function abRaw(args: string[]): { success: boolean; output: string } {55return runAgentBrowser(args);56}5758interface SnapshotElement {59ref: string;60role: string;61name: string;62}6364function parseSnapshot(output: string): SnapshotElement[] {65const elements: SnapshotElement[] = [];66const refPattern = /\[ref=(@?\w+)\]/g;67const lines = output.split('\n');6869for (const line of lines) {70const match = line.match(/\[ref=([@\w]+)\]/);71if (match) {72const ref = match[1].startsWith('@') ? match[1] : `@${match[1]}`;73const roleMatch = line.match(/^-\s+(\w+)/);74const nameMatch = line.match(/"([^"]+)"/);75elements.push({76ref,77role: roleMatch?.[1] || 'unknown',78name: nameMatch?.[1] || ''79});80}81}82return elements;83}8485function findElementByText(snapshot: string, text: string): string | null {86const lines = snapshot.split('\n');87for (const line of lines) {88if (line.includes(`"${text}"`) || line.includes(text)) {89const match = line.match(/\[ref=([@\w]+)\]/);90if (match) {91return match[1].startsWith('@') ? match[1] : `@${match[1]}`;92}93}94}95return null;96}9798function findElementBySelector(snapshot: string, selector: string): string | null {99return null;100}101102interface WeChatOptions {103title: string;104content: string;105images: string[];106submit?: boolean;107keepOpen?: boolean;108}109110async function postToWeChat(options: WeChatOptions): Promise<void> {111const { title, content, images, submit = false, keepOpen = true } = options;112113if (title.length > 20) throw new Error(`Title too long: ${title.length} chars (max 20)`);114if (content.length > 1000) throw new Error(`Content too long: ${content.length} chars (max 1000)`);115if (images.length === 0) throw new Error('At least one image is required');116117const absoluteImages = images.map(p => path.isAbsolute(p) ? p : path.resolve(process.cwd(), p));118for (const img of absoluteImages) {119if (!fs.existsSync(img)) throw new Error(`Image not found: ${img}`);120}121122console.log('[wechat] Opening WeChat Official Account...');123ab(['open', WECHAT_URL, '--headed']);124await sleep(5000);125126console.log('[wechat] Checking login status...');127let url = ab(['get', 'url']);128console.log(`[wechat] Current URL: ${url}`);129130const waitForLogin = async (timeoutMs = 120_000): Promise<boolean> => {131const start = Date.now();132while (Date.now() - start < timeoutMs) {133url = ab(['get', 'url']);134if (url.includes('/cgi-bin/home')) return true;135console.log('[wechat] Waiting for login...');136await sleep(3000);137}138return false;139};140141if (!url.includes('/cgi-bin/home')) {142console.log('[wechat] Not logged in. Please scan QR code...');143const loggedIn = await waitForLogin();144if (!loggedIn) throw new Error('Login timeout');145}146console.log('[wechat] Logged in.');147await sleep(2000);148149console.log('[wechat] Getting page snapshot...');150let snapshot = ab(['snapshot']);151console.log(snapshot);152153console.log('[wechat] Looking for "图文" menu...');154const tuWenRef = findElementByText(snapshot, '图文');155156if (!tuWenRef) {157console.log('[wechat] Using eval to find and click menu...');158ab(['eval', "document.querySelectorAll('.new-creation__menu .new-creation__menu-item')[2].click()"]);159} else {160console.log(`[wechat] Clicking menu ref: ${tuWenRef}`);161ab(['click', tuWenRef]);162}163164await sleep(4000);165166console.log('[wechat] Checking for new tab...');167const tabsOutput = ab(['tab']);168console.log(`[wechat] Tabs: ${tabsOutput}`);169170const tabLines = tabsOutput.split('\n');171const editorTabLine = tabLines.find(l => l.includes('appmsg') || (!l.includes('cgi-bin/home') && l.includes('mp.weixin.qq.com')));172173if (tabLines.length > 1) {174const tabMatch = tabsOutput.match(/\[(\d+)\].*(?:appmsg|edit)/i);175if (tabMatch) {176console.log(`[wechat] Switching to editor tab ${tabMatch[1]}...`);177ab(['tab', tabMatch[1]]);178} else {179const lastTabMatch = tabsOutput.match(/\[(\d+)\]/g);180if (lastTabMatch && lastTabMatch.length > 1) {181const lastTab = lastTabMatch[lastTabMatch.length - 1].match(/\d+/)?.[0];182if (lastTab) {183console.log(`[wechat] Switching to last tab ${lastTab}...`);184ab(['tab', lastTab]);185}186}187}188}189190await sleep(3000);191192url = ab(['get', 'url']);193console.log(`[wechat] Editor URL: ${url}`);194195console.log('[wechat] Getting editor snapshot...');196snapshot = ab(['snapshot']);197console.log(snapshot.substring(0, 2000));198199console.log('[wechat] Uploading images...');200const fileInputSelector = '.js_upload_btn_container input[type=file]';201const fileInputSelectorJs = toSafeJsStringLiteral(fileInputSelector);202203ab(['eval', `{204const input = document.querySelector(${fileInputSelectorJs});205if (input) input.style.display = 'block';206}`]);207await sleep(500);208209const uploadResult = abRaw(['upload', fileInputSelector, ...absoluteImages]);210console.log(`[wechat] Upload result: ${uploadResult.output}`);211212if (!uploadResult.success) {213console.log('[wechat] Using alternative upload method...');214for (const img of absoluteImages) {215console.log(`[wechat] Uploading: ${img}`);216const imgUrlJs = toSafeJsStringLiteral(`file://${img}`);217const imgFileNameJs = toSafeJsStringLiteral(path.basename(img));218ab(['eval', `219const input = document.querySelector(${fileInputSelectorJs});220if (input) {221const dt = new DataTransfer();222fetch(${imgUrlJs}).then(r => r.blob()).then(b => {223const file = new File([b], ${imgFileNameJs}, { type: 'image/png' });224dt.items.add(file);225input.files = dt.files;226input.dispatchEvent(new Event('change', { bubbles: true }));227});228}229`]);230await sleep(2000);231}232}233234console.log('[wechat] Waiting for uploads to complete...');235await sleep(10000);236237console.log('[wechat] Filling title...');238snapshot = ab(['snapshot', '-i']);239const titleRef = findElementByText(snapshot, 'title') || findElementByText(snapshot, '标题');240241if (titleRef) {242ab(['fill', titleRef, title]);243} else {244const titleJs = toSafeJsStringLiteral(title);245ab(['eval', `const t = document.querySelector('#title'); if(t) { t.value = ${titleJs}; t.dispatchEvent(new Event('input', {bubbles: true})); }`]);246}247await sleep(500);248249console.log('[wechat] Clicking on content editor...');250const editorRef = findElementByText(snapshot, 'js_pmEditorArea') || findElementByText(snapshot, 'textbox');251252if (editorRef) {253ab(['click', editorRef]);254} else {255ab(['eval', "document.querySelector('.js_pmEditorArea')?.click()"]);256}257await sleep(500);258259console.log('[wechat] Typing content...');260const lines = content.split('\n');261for (let i = 0; i < lines.length; i++) {262const line = lines[i];263if (line.length > 0) {264const lineJs = toSafeJsStringLiteral(line);265ab(['eval', `document.execCommand('insertText', false, ${lineJs})`]);266}267if (i < lines.length - 1) {268ab(['press', 'Enter']);269}270await sleep(100);271}272273console.log('[wechat] Content typed.');274await sleep(1000);275276if (submit) {277console.log('[wechat] Saving as draft...');278const submitRef = findElementByText(snapshot, 'js_submit') || findElementByText(snapshot, '保存');279if (submitRef) {280ab(['click', submitRef]);281} else {282ab(['eval', "document.querySelector('#js_submit')?.click()"]);283}284await sleep(3000);285console.log('[wechat] Draft saved!');286} else {287console.log('[wechat] Article composed (preview mode). Add --submit to save as draft.');288}289290if (!keepOpen) {291console.log('[wechat] Closing browser...');292ab(['close']);293} else {294console.log('[wechat] Done. Browser window left open.');295}296}297298function printUsage(): never {299console.log(`Post to WeChat Official Account using agent-browser300301Usage:302npx -y bun wechat-agent-browser.ts [options]303304Options:305--title <text> Article title (max 20 chars, required)306--content <text> Article content (max 1000 chars, required)307--image <path> Add image (can be repeated, 1+ images, required)308--submit Save as draft (default: preview only)309--close Close browser after operation (default: keep open)310--help Show this help311312Examples:313npx -y bun wechat-agent-browser.ts --title "测试" --content "内容" --image ./photo.png314npx -y bun wechat-agent-browser.ts --title "测试" --content "内容" --image a.png --image b.png --submit315`);316process.exit(0);317}318319async function main(): Promise<void> {320const args = process.argv.slice(2);321if (args.includes('--help') || args.includes('-h')) printUsage();322323const images: string[] = [];324let submit = false;325let keepOpen = true;326let title: string | undefined;327let content: string | undefined;328329for (let i = 0; i < args.length; i++) {330const arg = args[i]!;331if (arg === '--image' && args[i + 1]) {332images.push(args[++i]!);333} else if (arg === '--title' && args[i + 1]) {334title = args[++i];335} else if (arg === '--content' && args[i + 1]) {336content = args[++i];337} else if (arg === '--submit') {338submit = true;339} else if (arg === '--close') {340keepOpen = false;341}342}343344if (!title) {345console.error('Error: --title is required');346process.exit(1);347}348if (!content) {349console.error('Error: --content is required');350process.exit(1);351}352if (images.length === 0) {353console.error('Error: At least one --image is required');354process.exit(1);355}356357await postToWeChat({ title, content, images, submit, keepOpen });358}359360await main().catch((err) => {361console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);362process.exit(1);363});364