企业微信群日报自动化

公司要求每天在腾讯共享文档上写日报,还要求必须在企业微信群里再发一次,这种搬砖行为总是导致我回到公司打开休眠的电脑后,忘记重新登录企业微信而漏接消息,于是萌生了自动化转发的念头。

思路

用puppeteer进行模拟操作,用request通知机器人发送消息,用node-schedule定时运行node脚本。

难点

  1. 腾讯文档内容区域是canvas,无法抓取内容,但可以通过键盘将焦点移动到目标表格上,然后通过id获得输入栏的内容(这个是DOM元素),这样就可以取得目标表格的值了。
  2. 用QQ帐号和密码虽然可以全自动登录,可是当频繁登录时一旦出现安全异常提示,就会一直卡在登录界面,就算扫码也无法进入,后来改为微信扫码登录解决,微信扫码可以频繁登录不报错。
  3. 为了不频繁登录,需要把流程分为两部分,第一部分是微信扫码登录进入腾讯文档(只需初次执行脚本时扫一次即可),第二部分则是定时循环执行:获取日报内容。注意期间不能关闭虚拟浏览器,否则又要重新扫码……当然也可以多建一个机器人把登录二维码截屏发到另一个群里,每次用最新的码登录。
  4. CentOS里运行puppeteer还需要安装很多其他软件,这个需要自行google,而且虚拟浏览器里还会由于缺少中文字体而无法正常显示,想省事就找台Win服务器去跑吧……

核心代码

登录腾讯文档

async function autoReport() {
    const browser = await puppeteer.launch({ headless: !debug; });
    const page = await browser.newPage();
    await page.setViewport({ width: 1200, height: 800 });

    await console.log('- 打开文档链接');
    await page.goto(docURL);

    await console.log('- 点击立即登录');
    await page.click('#blankpage-button-pc');
    await page.waitFor(delay);
    
    await console.log('- 点击登录');
    const wechatButton = await page.$('#wechat-tabs-title');
    await wechatButton.click();
    await page.waitFor(delay);

    await page.screenshot({
        path: './assets/wechat.png',
        fullPage: true
    });
    await exec('start "C:\\Program Files\\Honeyview\\Honeyview.exe" "assets\\wechat.png"');
    await prompt("- 等待微信扫码,回车继续...");

    await page.waitForSelector('#padeditor');
    // await page.waitForNavigation();
    // await page.waitForNavigation({ waitUntil: "networkidle2" });
    await console.log('- 成功进入表格');
    
		await console.log('- 计划任务待命...');
		// 周一到周五每天晚上20:00执行
		await schedule.scheduleJob('0 0 20 * * 1-5', () => {
				takeAction(page);
		}
};

获取表格内容

async function takeAction(page) {
    await console.log(`\n----- ${getDate()}, 星期${day} -----`);

    await console.log('- 切换表');
    await page.mouse.move(weekTab[weekId], 780);
    await page.mouse.down();
    await page.mouse.up();
    await page.waitFor(delay);

    await console.log('- 全选表');
    await page.mouse.move(20, 140);
    await page.mouse.down();
    await page.mouse.up();
    await page.waitFor(delay);

    // 表格焦点下移次数
    for (let i = 0; i < floor; i++) {
        await console.log('- 键盘:下');
        await page.keyboard.press('ArrowDown');
        await page.waitFor(200);
    }

    // 表格焦点右移次数(因为有人名栏,所以要多移动一次)
    for (let i = 0; i < day + 1; i++) {
        await console.log('- 键盘:右');
        await page.keyboard.press('ArrowRight');
        await page.waitFor(200);
    }

    const element = await page.$("#alloy-simple-text-editor");
    let text = await page.evaluate(element => element.textContent, element);

    if (text.indexOf('未完成') != -1 || text === '') {
        await console.log('当前位置未发现日报!');
    } else {
        let tasks = await text.split(sp);
            tasks = await tasks.filter(n => {
                return n;
            });

            tasks = await tasks.map((val, idx) => {
                if (val[0] === ' ') {
                    return val.replace(' ', '');
                } else {
                    return val;
                }
            });

        await console.log(tasks);
        await page.waitFor(delay*10);
				await sendArticle({
						title: `${name}, ${getDate()}`,
						desc : tasks.join('\n'),
						link : docURL,
						image: cover,
						robot: robotURL
				});
    }
}

机器人发图文消息

function sendArticle(arg) {
    var robot = arg.robot || '';
    var data = {
        "msgtype": "news",
        "news": {
            "articles": [{
                "title": arg.title || '标题',
                "description": arg.desc || '描述',
                "url": arg.link || '链接',
                "picurl": arg.image || '图片'
            }]
        }
    };

    var options = {
        uri: robot,
        method: 'POST',
        headers: {
            "content-type": "application/json"
        },
        json: data
    };

    request(options, function (error, response, body) {
        if (!error && response.statusCode == 200) {
            // console.log(response);
        }
    });
}