个人订阅号无法通过api直接发布,所以订阅号想做自动化发帖就只能借助自动化脚本来实现。
大概的逻辑就是通过api将文章内容添加到草稿箱,然后脚本进入草稿箱进行发帖。
目前脚本是内置cookie 方式,后续改成扫码登录方式,方便小白使用。
/**
* WeChat Official Account Automation Bot (JavaScript Version)
* 微信公众号自动化机器人
*
* Features:
* - Cookie-based login
* - Navigate to draft box
* - Create and manage drafts (NO auto-publish for compliance)
*/
const puppeteer = require('puppeteer');
/**
* WeChat Cookie Structure
* @typedef {Object} WeChatCookie
* @property {string} name - Cookie name
* @property {string} value - Cookie value
* @property {string} domain - Cookie domain
* @property {string} path - Cookie path
*/
/**
* WeChat MP Bot Class
*/
class WeChatMPBot {
constructor() {
/** @type {puppeteer.Browser | null} */
this.browser = null;
/** @type {puppeteer.Page | null} */
this.page = null;
/** @type {WeChatCookie[]} */
this.cookies = [
{ name: 'uuid', value: 'xxxxxxx', domain: '.qq.com', path: '/' },
{ name: 'rand_info', value: 'xxx', domain: '.qq.com', path: '/' },
{ name: 'slave_bizuin', value: '3936817789', domain: '.qq.com', path: '/' },
{ name: 'data_bizuin', value: '3936817789', domain: '.qq.com', path: '/' },
{ name: 'bizuin', value: '3936817789', domain: '.qq.com', path: '/' },
{ name: 'data_ticket', value: 'xxxxx', domain: '.qq.com', path: '/' },
{ name: 'slave_sid', value: 'xxxxxx', domain: '.qq.com', path: '/' },
{ name: 'slave_user', value: 'xxxx', domain: '.qq.com', path: '/' },
{ name: 'xid', value: 'xxxx', domain: '.qq.com', path: '/' },
{ name: 'mm_lang', value: 'zh_CN', domain: '.qq.com', path: '/' },
{ name: 'noticeLoginFlag', value: '1', domain: '.qq.com', path: '/' },
{ name: 'pac_uid', value: 'xx', domain: '.qq.com', path: '/' },
{ name: 'iip', value: '0', domain: '.qq.com', path: '/' },
{ name: 'pgv_pvid', value: '8438042815', domain: '.qq.com', path: '/' },
{ name: 'rewardsn', value: '', domain: '.qq.com', path: '/' },
{ name: 'wxtokenkey', value: '777', domain: '.qq.com', path: '/' },
{ name: 'ua_id', value: 'xxx-xxx=', domain: '.qq.com', path: '/' },
{ name: 'wxuin', value: 'xxx', domain: '.qq.com', path: '/' },
{ name: 'ts_uid', value: 'xxx', domain: '.qq.com', path: '/' },
{ name: 'luin', value: 'xxx', domain: '.qq.com', path: '/' },
{ name: 'lskey', value: 'xxx', domain: '.qq.com', path: '/' },
{ name: 'ts_refer', value: 'mp.weixin.qq.com/cgi-bin/appmsg', domain: '.qq.com', path: '/' }
];
}
/**
* Initialize browser and page
*/
async init() {
console.log('🚀 正在启动浏览器...');
this.browser = await puppeteer.launch({
headless: false,
defaultViewport: null,
args: [
'--start-maximized',
'--disable-blink-features=AutomationControlled',
'--no-sandbox',
'--disable-setuid-sandbox'
]
});
const pages = await this.browser.pages();
this.page = pages[0];
await this.page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
console.log('✅ 浏览器启动成功');
}
/**
* Login with cookies
* @returns {Promise<boolean>}
*/
async loginWithCookies() {
if (!this.page) {
throw new Error('页面未初始化');
}
try {
console.log('🔐 开始使用Cookie登录...');
await this.page.goto('https://mp.weixin.qq.com/', {
waitUntil: 'networkidle2',
timeout: 30000
});
console.log('📋 正在设置Cookie...');
for (const cookie of this.cookies) {
await this.page.setCookie(cookie);
}
console.log('Cookie设置完成,重新加载页面...');
await this.page.reload({ waitUntil: 'networkidle2' });
console.log('⏱️ 等待登录状态确认...');
try {
await this.page.waitForSelector('.main_bd, .weui-desktop-menu, .menu_item, .weui-desktop-account', {
timeout: 10000
});
console.log('✅ 登录相关元素已加载');
} catch (error) {
console.log('⚠️ 等待登录元素超时,继续检查登录状态...');
}
const isLoggedIn = await this.checkLoginStatus();
if (isLoggedIn) {
console.log('✅ 登录成功!');
await this.startCookieKeepAlive();
return true;
} else {
console.log('❌ 登录失败或Cookie已过期');
return false;
}
} catch (error) {
console.error('登录过程中出现错误:', error);
return false;
}
}
/**
* Cookie keep-alive mechanism
* @private
*/
async startCookieKeepAlive() {
if (!this.page) return;
setInterval(async () => {
try {
const isLoggedIn = await this.checkLoginStatus();
if (!isLoggedIn) {
console.log('🔄 检测到登录状态异常,尝试刷新Cookie...');
for (const cookie of this.cookies) {
await this.page.setCookie(cookie);
}
await this.page.reload({ waitUntil: 'networkidle2' });
await this.page.waitForTimeout(3000);
const refreshResult = await this.checkLoginStatus();
if (refreshResult) {
console.log('✅ Cookie刷新成功');
} else {
console.log('❌ Cookie刷新失败,可能需要手动登录');
}
}
} catch (error) {
console.log('Cookie保活检查出错:', error);
}
}, 30000);
}
/**
* Check login status
* @private
* @returns {Promise<boolean>}
*/
async checkLoginStatus() {
if (!this.page) return false;
return await this.page.evaluate(() => {
return !!(
document.querySelector('.main_bd') ||
document.querySelector('.weui-desktop-menu') ||
document.querySelector('.menu_item') ||
document.querySelector('.weui-desktop-account') ||
document.querySelector('#menuBar')
);
});
}
/**
* Wait for specified seconds
* @param {number} seconds
*/
async wait(seconds) {
await this.page?.waitForTimeout(seconds * 1000);
}
/**
* Get current page information
* @returns {Promise<{title: string, url: string}>}
*/
async getCurrentPageInfo() {
if (!this.page) {
return { title: '', url: '' };
}
const title = await this.page.title();
const url = this.page.url();
return { title, url };
}
/**
* Close browser
*/
async close() {
if (this.browser) {
await this.browser.close();
this.browser = null;
this.page = null;
}
}
/**
* Navigate to draft box
* @returns {Promise<boolean>}
*/
async navigateToDraftBox() {
if (!this.page) {
throw new Error('页面未初始化');
}
try {
console.log('🗂️ 正在直接导航到草稿箱...');
for (let attempt = 1; attempt <= 3; attempt++) {
console.log(`📝 尝试第 ${attempt} 次导航到草稿箱...`);
const token = await this.extractToken();
if (!token) {
console.log('❌ 无法获取token,可能需要重新登录');
await this.page.reload({ waitUntil: 'networkidle2' });
await this.page.waitForTimeout(3000);
continue;
}
const draftUrl = `https://mp.weixin.qq.com/cgi-bin/appmsg?begin=0&count=10&type=77&action=list_card&token=${token}&lang=zh_CN`;
console.log('📁 正在进入草稿箱页面...');
console.log('🔗 草稿箱URL:', draftUrl);
await this.page.goto(draftUrl, {
waitUntil: 'networkidle2',
timeout: 30000
});
console.log('⏱️ 等待草稿箱页面加载...');
try {
await this.page.waitForSelector('body, .main_bd, .weui-desktop-main', {
timeout: 8000
});
console.log('✅ 草稿箱页面已加载');
} catch (error) {
console.log('⚠️ 等待草稿箱页面超时,继续检查URL...');
}
const currentUrl = this.page.url();
console.log('📄 当前URL:', currentUrl);
if (currentUrl.includes('type=77')) {
console.log('✅ 成功进入草稿箱页面');
const loginStatus = await this.checkLoginStatus();
if (!loginStatus) {
console.log('⚠️ 登录状态异常,尝试重新设置Cookie...');
for (const cookie of this.cookies) {
await this.page.setCookie(cookie);
}
await this.page.reload({ waitUntil: 'networkidle2' });
try {
await this.page.waitForSelector('.main_bd, .weui-desktop-menu, .menu_item', {
timeout: 5000
});
} catch (error) {
console.log('⚠️ 等待登录状态恢复超时');
}
continue;
}
console.log('⏱️ 等待草稿箱内容加载...');
try {
await this.page.waitForSelector('.publish_enable_button, .appmsg_card, .media_appmsg_item', {
timeout: 10000
});
console.log('✅ 已成功进入草稿箱区域');
} catch (error) {
console.log('⚠️ 未检测到草稿内容,可能草稿箱为空');
}
return true;
} else if (currentUrl.includes('login') || currentUrl.includes('redirect')) {
console.log('⚠️ 检测到登录页面,Cookie可能已失效');
console.log('💡 请更新Cookie或手动登录');
return false;
} else {
console.log(`⚠️ 第 ${attempt} 次尝试未正确进入草稿箱,当前URL:`, currentUrl);
if (attempt < 3) {
console.log('🔄 等待3秒后重试...');
await this.page.waitForTimeout(3000);
}
}
}
console.log('❌ 多次尝试后仍无法进入草稿箱');
return false;
} catch (error) {
console.error('导航到草稿箱时出错:', error);
return false;
}
}
/**
* Select latest draft (REMOVED AUTO-PUBLISH FOR COMPLIANCE)
* This method only navigates to the draft, does NOT auto-publish
* @returns {Promise<boolean>}
*/
async selectLatestDraft() {
if (!this.page) {
throw new Error('页面未初始化');
}
try {
console.log('🔍 正在查找最新的草稿文章...');
console.log('⚠️ 合规提示:此工具仅打开草稿,不会自动发布');
console.log('⏱️ 等待文章列表加载...');
try {
await this.page.waitForSelector('.publish_enable_button, .appmsg_card, .media_appmsg_item, .card_appmsg_item', {
timeout: 15000
});
console.log('✅ 文章列表已加载');
} catch (error) {
console.log('⚠️ 等待文章列表超时,尝试继续操作...');
}
console.log('🔍 查找草稿文章...');
const publishButtons = await this.page.$$('.publish_enable_button span');
if (publishButtons.length > 0) {
console.log(`📝 找到 ${publishButtons.length} 个草稿`);
const firstPublishButton = publishButtons[0];
const articleInfo = await this.page.evaluate((button) => {
let container = button.parentElement;
while (container && !container.className.includes('publish_card')) {
container = container.parentElement;
}
if (container) {
const titleEl = container.querySelector('.appmsg_title, .title, h3');
return {
title: titleEl ? titleEl.textContent?.trim().substring(0, 50) : '未知文章',
found: true
};
}
return { title: '未知文章', found: false };
}, firstPublishButton);
console.log(`📝 找到草稿: ${articleInfo.title}`);
console.log('✅ 草稿定位成功');
console.log('');
console.log('🎯 下一步操作建议:');
console.log('1. 使用 Draft API (/cgi-bin/draft/update) 插入广告内容');
console.log('2. 用户在微信后台手动预览和发布');
console.log('3. 不要使用自动发布功能(合规要求)');
return true;
} else {
console.log('❌ 未找到草稿文章');
console.log('💡 提示:请确认草稿箱中有文章');
return false;
}
} catch (error) {
console.error('选择最新草稿时出错:', error);
return false;
}
}
/**
* Check and handle cookie expiration
* @returns {Promise<boolean>}
*/
async handleCookieExpiration() {
if (!this.page) return false;
try {
console.log('🔍 检查Cookie状态...');
const currentUrl = this.page.url();
console.log('📄 当前页面:', currentUrl);
if (currentUrl.includes('login') || currentUrl.includes('redirect') || currentUrl.includes('passport')) {
console.log('❌ Cookie已失效,需要重新登录');
console.log('💡 请获取新的Cookie并更新代码中的Cookie值');
console.log('🔧 或者手动登录后继续操作');
return false;
}
const needsLogin = await this.page.evaluate(() => {
const bodyText = document.body.textContent || '';
return bodyText.includes('登录') ||
bodyText.includes('扫码') ||
bodyText.includes('请先登录') ||
document.querySelector('.login_container') !== null;
});
if (needsLogin) {
console.log('❌ 页面显示需要登录,Cookie可能已失效');
return false;
}
console.log('✅ Cookie状态正常');
return true;
} catch (error) {
console.error('检查Cookie状态时出错:', error);
return false;
}
}
/**
* Keep browser open for manual operations
*/
keepBrowserOpen() {
console.log('🔄 浏览器将保持打开状态,您可以继续手动操作');
console.log('💡 如需关闭,请手动关闭浏览器窗口或调用 close() 方法');
console.log('');
console.log('🚨 如果出现Cookie失效的情况:');
console.log('1. 手动在浏览器中登录微信公众平台');
console.log('2. 获取新的Cookie字符串');
console.log('3. 更新代码中的Cookie值');
console.log('4. 重新运行程序');
}
/**
* Extract token from page
* @private
* @returns {Promise<string | null>}
*/
async extractToken() {
if (!this.page) return null;
try {
const token = await this.page.evaluate(() => {
const urlMatch = window.location.href.match(/token=([^&]+)/);
if (urlMatch) return urlMatch[1];
if (window.wx && window.wx.data && window.wx.data.token) {
return window.wx.data.token;
}
const scripts = document.getElementsByTagName('script');
for (let i = 0; i < scripts.length; i++) {
const script = scripts[i];
const content = script.innerHTML;
const tokenMatch = content.match(/token['"\s]*:['"\s]*['"]([^'"]+)['"]/);
if (tokenMatch) return tokenMatch[1];
}
return null;
});
return token;
} catch (error) {
console.log('无法提取token:', error);
return null;
}
}
}
/**
* Main function
*/
async function main() {
const bot = new WeChatMPBot();
try {
await bot.init();
console.log('🚀 浏览器已启动');
const loginSuccess = await bot.loginWithCookies();
if (loginSuccess) {
console.log('🚀 开始执行草稿箱操作...');
} else {
console.log('⚠️ 登录检测未完全成功,但继续尝试执行任务...');
}
console.log('🚀 开始执行草稿箱操作...');
const draftBoxOpened = await bot.navigateToDraftBox();
if (draftBoxOpened) {
console.log('✅ 成功进入草稿箱');
const draftSelected = await bot.selectLatestDraft();
if (draftSelected) {
console.log('');
console.log('✅ 草稿操作完成');
console.log('');
console.log('📋 合规提示:');
console.log('- 此工具已移除自动发布功能');
console.log('- 请使用 Draft API 插入广告内容');
console.log('- 用户需要在微信后台手动审核和发布');
}
} else {
console.log('❌ 无法进入草稿箱,尝试手动操作');
}
const pageInfo = await bot.getCurrentPageInfo();
console.log(`📄 当前页面: ${pageInfo.title}`);
console.log(`🔗 URL: ${pageInfo.url}`);
bot.keepBrowserOpen();
} catch (error) {
console.error('❌ 执行过程中出现错误:', error);
await bot.close();
}
}
// Export class and main function
module.exports = {
WeChatMPBot,
main
};
// Run main function if executed directly
if (require.main === module) {
main().catch(console.error);
}
cookie 也可以独立一个采集页面,支持外网访问,这样即使你不在服务器旁边,发现cookie掉了,也可以远程扫码采集。