threeperson
发布于 2025-10-17 / 3 阅读
0
0

订阅号自动发帖脚本

个人订阅号无法通过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掉了,也可以远程扫码采集。


评论