├── 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 | 
111 |
112 | 2. 预览最近编辑的文章(用于上一次异常报错或想查看最近一次编辑的文章)
113 |
114 | 
115 |
116 | 2. 自动发布成功的流程示例
117 |
118 | 
119 |
120 | 2. 发布失败流程及失败原因
121 |
122 | 
123 |
124 | 
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);
--------------------------------------------------------------------------------