├── package.json ├── LICENSE ├── .gitignore ├── README.md └── automation.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wechat-mp-automation", 3 | "version": "1.3.2", 4 | "description": "An automated tool for posting pages on WeChatMediaPlatform(https://mp.weixin.qq.com) using puppeteer.", 5 | "main": "automation.js", 6 | "scripts": { 7 | "debug": "env SHOW_BROWSER=s node automation.js", 8 | "start": "node automation.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/LinusLing/WeChatMediaPlatformAutomation.git" 14 | }, 15 | "keywords": [ 16 | "Automation", 17 | "wechat", 18 | "weixin", 19 | "MediaPlatform" 20 | ], 21 | "author": "Linus Ling", 22 | "license": "MIT", 23 | "dependencies": { 24 | "clipboardy": "^3.0.0", 25 | "commander": "^2.20.0", 26 | "js-localdate-plus": "^1.0.1", 27 | "open": "^6.4.0", 28 | "puppeteer": "^4.0.0" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/LinusLing/WeChatMediaPlatformAutomation/issues" 32 | }, 33 | "bin": { 34 | "wechat-mp-automation": "./automation.js" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 小铁匠Linus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ErrorResult.png 2 | confirmSend.png 3 | config.json 4 | 5 | # OS X 6 | .DS_Store 7 | 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # Optional npm cache directory 50 | .npm 51 | 52 | # Optional eslint cache 53 | .eslintcache 54 | 55 | # Optional REPL history 56 | .node_repl_history 57 | 58 | # Output of 'npm pack' 59 | *.tgz 60 | 61 | # Yarn Integrity file 62 | .yarn-integrity 63 | 64 | # dotenv environment variables file 65 | .env 66 | 67 | # next.js build output 68 | .next 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WeChatMediaPlatformAutomation 2 | 3 | 一款在微信公众号( https://mp.weixin.qq.com )自动预览/发布文章的命令行工具。 4 | 5 | ## 如何使用 6 | 7 | 1. 安装:`npm install wechat-mp-automation -g ` 8 | 9 | 2. 打开命令行执行: 10 | 1. 配置文件方式: 11 | 1. `wechat-mp-automation -C YOUR_CONFIG_JSON_FILE_PATH` 12 | 2. 非配置文件方式: 13 | 1. 非原创:`wechat-mp-automation -t [标题] -a [作者] -u [账号] -p [密码]` 14 | 2. 声明原创:`wechat-mp-automation -t [标题] -a [作者] -u [账号] -p [密码] -o` 15 | 3. 其余参数,参看如下帮助文档👇 16 | 17 | 3. 过程中的扫码: 18 | 19 | 1. 一次扫码,验证身份后登录 20 | 2. 若设置了只预览不发布(1.2.0 起支持 `--preview`),无需扫码即可预览文章 21 | 3. 1.2.0 前版本或未设置预览的情况,还需一次扫码,确认群发(如群发前,未异常报错的话) 22 | 23 | > 本工具不以任何形式保存账号和密码!!! 24 | 25 | > puppeteer 安装失败可以参考[这里](https://github.com/cnpm/cnpmjs.org/issues/1246#issuecomment-454268958) 26 | 27 | ## 帮助文档 28 | 29 | ```git 30 | $ wechat-mp-automation -h 31 | Usage: wechat-mp-automation [options] 32 | 33 | Options: 34 | -V, --version output the version number 35 | -C, --configPath [xxx] 配置文件的本地路径(支持所有自定义参数) 36 | -t, --title [xxx] 文章标题 37 | -a, --author [xxx] 文章作者 38 | -c, --content [xxx] 文章内容[可选],默认从粘贴板复制 39 | -u, --username [xxx] 公众号账号 40 | -p, --password [xxx] 公众号密码 41 | -o, --original 声明原创[可选] 42 | --preview 预览而不发布[可选] 43 | --preview_username [xxx~yyy] 预览名单[可选],以~间隔多个微信号(自行保证微信号已关注公众号) 44 | --skip_typing 跳过文章标题、作者、文章的填写和封面图片选择(声明原创除外)[可选] 45 | --last_edit 选中最近编辑的文章[可选],请自行确保当前有“最近编辑”的文章 46 | -h, --help output usage information 47 | ``` 48 | 49 | config.json demo: 50 | ```json 51 | { 52 | "title": "test", 53 | "author": "小铁匠Linus", 54 | "username": "YOUR_USERNAME", 55 | "password": "YOUR_PASSWORD", 56 | "original": "true" 57 | } 58 | ``` 59 | 60 | ## CHANGELOG 61 | 62 |
63 | 1.3.2 64 |
65 |

1. 适配新版本的群发界面

66 |

2. 优化二维码的截取展示

67 |

3. 优化参数读取

68 |
69 | 70 |
71 | 1.3.1 72 |
73 |

1. 修复点击封面图片选择失效的问题

74 |
75 | 76 |
77 | 1.3.0 78 |
79 |

1. 支持新版本的公众号后台

80 |
81 | 82 |
83 | 1.2.0 84 |
85 |

1. 支持预览文章,而不发布

86 |

2. 选择预览时,支持指定预览的微信号名单(自行保证微信号已关注公众号)

87 |

3. 支持跳过填写内容,建议用于二次预览或发布的情况

88 |

4. 支持选择最近编辑的文章功能,避免每次都新建群发

89 |

5. 未指定文章内容时,采用剪贴板粘贴的方式填入内容,替换原模拟键盘输入的方式

90 |
91 | 92 |
93 | 1.1.1 94 |
95 |

1. 登录默认选择账号密码登录

96 |

2. 官网页面元素的更正,恢复群发流程

97 |
98 | 99 |
100 | 1.1.0 101 |
102 |

1. 支持使用 JSON 格式的本地配置文件作为参数,避免命令行泄漏关键信息

103 |

2. 支持在发布过程中展示文章内容

104 |
105 | 106 | ## Demo 107 | 108 | 1. 利用**文章内容默认从粘贴板复制**的特性,配合一行命令生成公众号内容的工具 [wechat-format-cli](https://github.com/LinusLing/wechat-format-cli) 使用更香 109 | 110 | ![cli.png](https://i.loli.net/2020/06/19/GDEwdxrHnTVRyZe.png) 111 | 112 | 2. 预览最近编辑的文章(用于上一次异常报错或想查看最近一次编辑的文章) 113 | 114 | ![1.2.0.png](https://i.loli.net/2020/06/19/FzryZdN5VsXoplw.png) 115 | 116 | 2. 自动发布成功的流程示例 117 | 118 | ![CorrectResult.png](https://i.loli.net/2019/07/23/5d371a7398b4141770.png) 119 | 120 | 2. 发布失败流程及失败原因 121 | 122 | ![error_progress.png](https://i.loli.net/2019/07/23/5d371a73c0f5f58172.png) 123 | 124 | ![ErrorResult.png](https://i.loli.net/2019/07/23/5d37086e81ff423521.png) 125 | 126 | ## TODO 127 | 128 | 1. 通过指定特定文件来上传文章内容 129 | 2. 文章发布前的设置可进行自定义(比如~~预览~~、图片选择等) 130 | 3. 支持更多种类的创作(~~图文消息~~、文字消息、视频消息、音频消息、图片消息、转载等) 131 | 132 | ## Issues 133 | 134 | [意见与建议](https://github.com/LinusLing/WeChatMediaPlatformAutomation/issues/new) 135 | 136 | ## 赞赏 137 | 138 |
139 |
140 | -------------------------------------------------------------------------------- /automation.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const puppeteer = require('puppeteer'); 4 | const open = require('open') 5 | const clipboardy = require('clipboardy'); 6 | const LocalDate = require('js-localdate-plus'); 7 | var fs = require('fs'); 8 | const program = require('commander'); 9 | 10 | program 11 | .version('1.3.2') 12 | .usage(' [options]') 13 | .option('-C, --configPath [xxx]', '配置文件的本地路径(支持所有自定义参数)') 14 | .option('-t, --title [xxx]', '文章标题') 15 | .option('-a, --author [xxx]', '文章作者') 16 | .option('-c, --content [xxx]', '文章内容[可选],默认从粘贴板复制') 17 | .option('-u, --username [xxx]', '公众号账号') 18 | .option('-p, --password [xxx]', '公众号密码') 19 | .option('-o, --original', '声明原创[可选]') 20 | .option('--preview', '预览而不发布[可选]') 21 | .option('--preview_username [xxx~yyy]', '预览名单[可选],以~间隔多个微信号(自行保证微信号已关注公众号)') 22 | .option('--skip_typing', '跳过文章标题、作者、文章的填写和封面图片选择(声明原创除外)[可选]') 23 | .option('--last_edit', '选中最近编辑的文章[可选],请自行确保当前有“最近编辑”的文章') 24 | .parse(process.argv); 25 | 26 | let title; 27 | let author; 28 | let content; 29 | let username; 30 | let password; 31 | let original; 32 | let preview; 33 | let preview_username; 34 | let skip_typing; 35 | let last_edit; 36 | 37 | if (program.configPath !== undefined) { 38 | try { 39 | const contents = fs.readFileSync(program.configPath); 40 | const jsonContent = JSON.parse(contents); 41 | title = jsonContent.title || undefined; 42 | author = jsonContent.author || undefined; 43 | content = jsonContent.content || undefined; 44 | username = jsonContent.username || undefined; 45 | password = jsonContent.password || undefined; 46 | original = jsonContent.original || undefined; 47 | preview = jsonContent.preview || undefined; 48 | preview_username = program.preview_username && program.preview_username.split("~") || undefined; 49 | skip_typing = jsonContent.skip_typing || undefined; 50 | last_edit = jsonContent.last_edit || undefined; 51 | console.log('读取配置文件成功'); 52 | } catch (error) { 53 | console.log('读取配置文件失败'); 54 | console.log(error); 55 | } 56 | } 57 | 58 | console.log("----------配置内容 begin----------"); 59 | 60 | if (title === undefined) { 61 | if (program.title === undefined) { 62 | console.log('缺少文章标题, -h 了解如何使用'); 63 | return; 64 | } else { 65 | title = String(program.title); 66 | console.log('文章标题:' + title); 67 | } 68 | } 69 | 70 | if (author === undefined) { 71 | if (program.author === undefined) { 72 | console.log('缺少文章作者, -h 了解如何使用'); 73 | return; 74 | } else { 75 | author = String(program.author); 76 | console.log('文章作者:' + author); 77 | } 78 | } 79 | if (content === undefined) { 80 | if (program.content === undefined) {} else { 81 | content = String(program.content); 82 | } 83 | } 84 | if (username === undefined) { 85 | if (program.username === undefined) { 86 | console.log('缺少公众号账号, -h 了解如何使用'); 87 | return; 88 | } else { 89 | username = String(program.username); 90 | } 91 | } 92 | if (password === undefined) { 93 | if (program.password === undefined) { 94 | console.log('缺少公众号密码, -h 了解如何使用'); 95 | return; 96 | } else { 97 | password = String(program.password); 98 | } 99 | } 100 | if (original === undefined) { 101 | if (program.original === undefined) {} else { 102 | original = program.original; 103 | } 104 | } 105 | console.log((!original ? "文章不" : "文章将") + "声明原创"); 106 | 107 | if (preview === undefined) { 108 | if (program.preview === undefined) {} else { 109 | preview = program.preview; 110 | console.log("文章不会发布,只预览"); 111 | } 112 | } 113 | 114 | if (preview_username === undefined) { 115 | if (program.preview_username === undefined) {} else { 116 | preview_username = program.preview_username.split("~"); 117 | console.log("可预览本文章的微信号:" + preview_username + "(关注公众号后,才能接收图文消息预览)"); 118 | } 119 | } 120 | 121 | if (skip_typing === undefined) { 122 | if (program.skip_typing === undefined) {} else { 123 | skip_typing = program.skip_typing; 124 | console.log("将跳过文章标题、作者、文章的填写和封面图片选择(声明原创除外)"); 125 | } 126 | } 127 | 128 | if (last_edit === undefined) { 129 | if (program.last_edit === undefined) {} else { 130 | last_edit = program.last_edit; 131 | } 132 | } 133 | console.log(last_edit ? "将选中最近编辑的文章" : "将新建群发的文章"); 134 | 135 | console.log("----------配置内容 end----------"); 136 | 137 | const url = "https://mp.weixin.qq.com/" 138 | 139 | function autoLogin() { 140 | return new Promise(async (resolve, reject) => { 141 | const browserConfig = process.env.SHOW_BROWSER ? { 142 | headless: false, 143 | slowMo: 100 144 | } : {} 145 | browserConfig['defaultViewport'] = null; // 页面最大化 146 | const browser = await puppeteer.launch(browserConfig); 147 | let page = await browser.newPage(); 148 | await page.setViewport({ 149 | width: 1200, 150 | height: 890, 151 | }); 152 | 153 | try { 154 | let clickAndWaitForTarget = async (clickSelector, page, browser) => { 155 | const pageTarget = page.target(); //save this to know that this was the opener 156 | await page.click(clickSelector); //click on a link 157 | const newTarget = await browser.waitForTarget(target => target.opener() === pageTarget); //check that you opened this page, rather than just checking the url 158 | const newPage = await newTarget.page(); //get the page object 159 | // await newPage.once("load",()=>{}); //this doesn't work; wait till page is loaded 160 | await newPage.waitForSelector("body"); //wait for page to be loaded 161 | 162 | return newPage; 163 | }; 164 | 165 | // 打开首页 166 | console.log("正在打开登录首页..."); 167 | await page.goto(url); 168 | 169 | // 登录 170 | console.log("正在登录..."); 171 | const element = await page.$('[class="login__type__container login__type__container__scan"]'); 172 | if (element) { 173 | await page.click('#header > div.banner > div > div > div.login__type__container.login__type__container__scan > a') 174 | } 175 | 176 | await page.waitForSelector('#header > div.banner > div > div > div.login__type__container.login__type__container__account > form > div.login_btn_panel > a'); 177 | //type the name 178 | await page.focus('#header > div.banner > div > div > div.login__type__container.login__type__container__account > form > div.login_input_panel > div:nth-child(1) > div > span > input') 179 | await page.keyboard.type(username); 180 | //type the pwd 181 | await page.focus('#header > div.banner > div > div > div.login__type__container.login__type__container__account > form > div.login_input_panel > div:nth-child(2) > div > span > input') 182 | await page.keyboard.type(password); 183 | await page.waitFor(50); 184 | //Click on the submit button 185 | await page.click('#header > div.banner > div > div > div.login__type__container.login__type__container__account > form > div.login_btn_panel > a') 186 | 187 | // 扫码登录 188 | console.log("扫码登录中..."); 189 | const IMAGE_SELECTOR = '#app > div.weui-desktop-layout__main__bd > div > div.js_scan.weui-desktop-qrcheck > div.weui-desktop-qrcheck__qrcode-area > div > img' 190 | await page.waitForSelector(IMAGE_SELECTOR); 191 | await page.waitFor(500); 192 | await page.screenshot({ 193 | path: 'screenshot.png', 194 | clip: { 195 | x: 390, 196 | y: 270, 197 | width: 420, 198 | height: 350 199 | } 200 | }); 201 | open('screenshot.png'); 202 | 203 | if (last_edit) { 204 | // 最近编辑 205 | console.log("打开最近编辑的文章中..."); 206 | const LAST_EDIT_BUTTON_SELECTOR = '#app > div.main_bd > div:nth-child(5) > div.weui-desktop-panel__bd > div > div > div:nth-child(1) > span:nth-child(1) > div > div > div.weui-desktop-card__action > div > div.weui-desktop-tooltip__wrp.weui-desktop-link'; 207 | await Promise.race([ 208 | page.waitForSelector(LAST_EDIT_BUTTON_SELECTOR) 209 | ]); 210 | 211 | await page.hover(LAST_EDIT_BUTTON_SELECTOR); 212 | await page.waitFor(500); 213 | page = await clickAndWaitForTarget(LAST_EDIT_BUTTON_SELECTOR, page, browser); 214 | 215 | // 删除扫码登录截图 216 | fs.unlinkSync('screenshot.png'); 217 | 218 | await page.waitFor(5000); 219 | } else { 220 | // 新建群发(图文消息 div:nth-child(1)) 221 | console.log("新建群发文章中..."); 222 | const NEW_POST = '#app > div.main_bd > div:nth-child(3) > div.weui-desktop-panel__bd > div > div:nth-child(1)' 223 | const element2 = await Promise.race([ 224 | page.waitForSelector(NEW_POST) 225 | ]); 226 | 227 | await page.waitFor(500); 228 | page = await clickAndWaitForTarget(NEW_POST, page, browser); 229 | 230 | // 删除扫码登录截图 231 | fs.unlinkSync('screenshot.png'); 232 | 233 | await page.waitFor(5000); 234 | } 235 | 236 | if (!skip_typing) { 237 | // 文章标题 238 | console.log("正在填写文章标题..."); 239 | await page.click('#title'); 240 | await page.waitFor(100); 241 | await page.keyboard.type(String(title)); 242 | await page.waitFor(100); 243 | 244 | // 文章作者 245 | console.log("正在填写文章作者..."); 246 | await page.keyboard.press('Tab', { 247 | delay: 100 248 | }); 249 | await page.keyboard.type(String(author)); 250 | await page.waitFor(100); 251 | 252 | // 文章内容 253 | console.log("正在填写文章内容..."); 254 | await page.keyboard.press('Tab', { 255 | delay: 100 256 | }); 257 | 258 | var pasted_content; 259 | if (content) { 260 | // 指定文章内容时,模拟键盘输入内容 261 | pasted_content = content; 262 | await page.keyboard.type(String(pasted_content)); 263 | } else { 264 | // 未指定文章内容时,采用剪贴板粘贴的方式填入内容 265 | pasted_content = await clipboardy.read(); 266 | // https://stackoverflow.com/questions/11750447/performing-a-copy-and-paste-with-selenium-2#answer-41046276 267 | // https://github.com/puppeteer/puppeteer/blob/56742ebe8cbb353d7739faee358f60832ef113e5/src/USKeyboardLayout.ts 268 | await page.keyboard.down('ShiftLeft') 269 | await page.keyboard.press('Insert') 270 | await page.keyboard.up('ShiftLeft') 271 | } 272 | await page.waitFor(100); 273 | 274 | console.log("----------文章内容 begin----------"); 275 | console.log(pasted_content) 276 | console.log("----------文章内容 end----------"); 277 | 278 | // 封面图片选择 279 | console.log("正在自动选择封面图片..."); 280 | await page.hover('#js_cover_area > div.select-cover__btn.js_cover_btn_area'); 281 | await page.waitFor(500); 282 | await page.click('#js_imagedialog'); 283 | await page.waitFor(500); 284 | await page.click('#js_imagedialog'); 285 | await page.waitForSelector('div > div.weui-desktop-media-list-wrp.weui-desktop-img-picker__list__wrp.js_img-picker_wrapper > ul > li:nth-child(1)'); 286 | 287 | let day = (new LocalDate()).getDay(); 288 | const len = await page.$$eval('.weui-desktop-img-picker__list > li.weui-desktop-img-picker__item > i', links => { 289 | return links.length 290 | }); 291 | let offset = day % len + 1; 292 | const left = 'div > div.weui-desktop-media-list-wrp.weui-desktop-img-picker__list__wrp.js_img-picker_wrapper > ul > li:nth-child('; 293 | const right = ')'; 294 | await page.click(left + String(offset) + right); 295 | 296 | await page.click('div.weui-desktop-dialog__wrp.weui-desktop-dialog_img-picker.weui-desktop-dialog_img-picker-with-crop > div > div.weui-desktop-dialog__ft > button'); 297 | await page.waitFor(1200); 298 | 299 | // 选择图片完成 300 | const IMG_DONE = "div.weui-desktop-dialog__wrp.weui-desktop-dialog_img-picker.weui-desktop-dialog_img-picker-with-crop > div > div.weui-desktop-dialog__ft > button:nth-child(3)"; 301 | await page.waitForSelector(IMG_DONE); 302 | await page.waitFor(200); 303 | await page.click(IMG_DONE); 304 | await page.waitFor(2000); 305 | } 306 | 307 | if (original) { 308 | // 声明原创 309 | console.log("正在声明原创..."); 310 | 311 | await page.evaluate(() => { 312 | document.querySelector('#js_original > div.unorigin.js_original_type > div.setting-group__content > a').click(); 313 | }); 314 | await page.waitForSelector("label[for='js_copyright_agree'"); 315 | await page.click('body > div.dialog_wrp.simple.align_edge.original_dialog.ui-draggable > div > div.dialog_bd > div.step_panel.step_agreement.js_step_panel > div > div > div > div.tool_area.new-tool_area > label > i'); 316 | await page.click('body > div.dialog_wrp.simple.align_edge.original_dialog.ui-draggable > div > div.dialog_ft > span:nth-child(1) > button'); 317 | await page.waitFor(50); 318 | await page.click('#js_original_article_type > div > a'); 319 | await page.waitFor(50); 320 | await page.click('#js_original_article_type > div > div > div > div.weui-desktop-dropdown__list__cascade__container.js_scroll_area.js_data > dl > dd > dl:nth-child(2) > dt'); 321 | await page.waitFor(50); 322 | await page.click('body > div.dialog_wrp.simple.align_edge.original_dialog.ui-draggable > div > div.dialog_ft > span:nth-child(3) > button'); 323 | await page.waitFor(500); 324 | } else { 325 | await page.waitFor(500); 326 | } 327 | 328 | if (preview) { 329 | // 预览 330 | console.log("正在预览文章..."); 331 | const PREVIEW_BTN = "#js_preview > button"; 332 | await page.waitForSelector(PREVIEW_BTN); 333 | await page.click(PREVIEW_BTN); 334 | await page.waitFor(500); 335 | 336 | const element = await page.$('[class="weui-desktop-form-tag__name"]'); 337 | if (!element) { 338 | console.log("正在填写预览名单..."); 339 | // 没有默认预览的名单,则添加 preview_username 中的名单 340 | await page.waitForSelector('#js_preview_wxname'); 341 | await page.focus('#js_preview_wxname'); 342 | for (const key in preview_username) { 343 | const username = preview_username[key]; 344 | await page.keyboard.type(username); 345 | await page.keyboard.press('Enter', { 346 | delay: 100 347 | }); 348 | await page.waitFor(1500); 349 | } 350 | } 351 | 352 | // 预览确认 353 | console.log("预览确认中..."); 354 | const PREVIEW_CONFIRM_BTN = "body > div.dialog_wrp.label_block.wechat_send_dialog.ui-draggable > div > div.dialog_ft > span.btn.btn_primary.btn_input.js_btn_p > button" 355 | await page.waitForSelector(PREVIEW_CONFIRM_BTN); 356 | await page.click(PREVIEW_CONFIRM_BTN); 357 | 358 | await page.waitFor(500); 359 | console.log("预览发布成功。"); 360 | } else { 361 | // 保存并转发 362 | console.log("正在保存文章并转发..."); 363 | const SEND_BTN = "#js_send > button"; 364 | await page.waitForSelector(SEND_BTN); 365 | await page.click(SEND_BTN); 366 | await page.waitFor(500); 367 | 368 | // 群发+确认群发 369 | console.log("扫码确认群发中..."); 370 | const SCAN_SEND_BTN = "div.weui-desktop-dialog__wrp > div > div.weui-desktop-dialog__ft > div > div.weui-desktop-popover__wrp > button"; 371 | await page.waitForSelector(SCAN_SEND_BTN); 372 | await page.click(SCAN_SEND_BTN); 373 | const CONFIRM_SEND_BTN = "div.weui-desktop-dialog__wrp > div > div.weui-desktop-dialog__ft > div > button.weui-desktop-btn.weui-desktop-btn_primary"; 374 | await page.waitForSelector(CONFIRM_SEND_BTN); 375 | await page.waitFor(2000); 376 | await page.click(CONFIRM_SEND_BTN); 377 | 378 | // 等待确认二维码 379 | await page.waitForSelector('body > div.dialog_wrp.ui-draggable > div > div.dialog_bd > div > div > div.qrcode_wrp > img') 380 | await page.waitFor(500); 381 | await page.screenshot({ 382 | path: 'confirmSend.png', 383 | clip: { 384 | x: 260, 385 | y: 640, 386 | width: 280, 387 | height: 330 388 | } 389 | }); 390 | open('confirmSend.png'); 391 | 392 | // 等待发布成功页面展示 393 | const MAIN_BD = "#app > div.main_bd"; 394 | await page.waitForSelector(MAIN_BD); 395 | await page.waitFor(500); 396 | console.log("群发发布成功。"); 397 | 398 | // 删除扫码确认发布截图 399 | fs.unlinkSync('confirmSend.png'); 400 | } 401 | 402 | // 结束 403 | browser.close(); 404 | return resolve(); 405 | } catch (e) { 406 | // 异常时截图保存 407 | await page.screenshot({ 408 | path: 'ErrorResult.png', 409 | }); 410 | console.log("发生异常,详情请见 ErrorResult.png"); 411 | // 结束 412 | browser.close(); 413 | return reject(e); 414 | } 415 | }) 416 | } 417 | autoLogin().catch(console.error); --------------------------------------------------------------------------------