-
Notifications
You must be signed in to change notification settings - Fork 436
Expand file tree
/
Copy pathbypasscf.js
More file actions
836 lines (793 loc) · 31.3 KB
/
Copy pathbypasscf.js
File metadata and controls
836 lines (793 loc) · 31.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
import fs from "fs";
import path from "path";
import puppeteer from "puppeteer-extra";
import StealthPlugin from "puppeteer-extra-plugin-stealth";
import dotenv from "dotenv";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
import TelegramBot from "node-telegram-bot-api";
import fetch from "node-fetch";
import { parseStringPromise } from "xml2js";
import { parseRss } from "./src/parse_rss.js";
import { processAndSaveTopicData } from "./src/topic_data.js";
import {
getProxyConfig,
getPuppeteerProxyArgs,
testProxyConnection,
getCurrentIP,
} from "./src/proxy_config.js";
dotenv.config();
// 捕获未处理的异常/Promise拒绝,避免因 Target closed 之类错误导致进程退出
process.on("unhandledRejection", (reason) => {
try {
const msg = (reason && reason.message) ? reason.message : String(reason);
console.warn("[unhandledRejection]", msg);
} catch {
console.warn("[unhandledRejection] (non-string reason)");
}
});
process.on("uncaughtException", (err) => {
try {
const msg = (err && err.message) ? err.message : String(err);
console.warn("[uncaughtException]", msg);
} catch {
console.warn("[uncaughtException] (non-string error)");
}
});
// 截图保存的文件夹
// const screenshotDir = "screenshots";
// if (!fs.existsSync(screenshotDir)) {
// fs.mkdirSync(screenshotDir);
// }
puppeteer.use(StealthPlugin());
// Load the default .env file
if (fs.existsSync(".env.local")) {
console.log("Using .env.local file to supply config environment variables");
const envConfig = dotenv.parse(fs.readFileSync(".env.local"));
for (const k in envConfig) {
process.env[k] = envConfig[k];
}
} else {
console.log(
"Using .env file to supply config environment variables, you can create a .env.local file to overwrite defaults, it doesn't upload to git"
);
}
// 读取以分钟为单位的运行时间限制
const runTimeLimitMinutes = process.env.RUN_TIME_LIMIT_MINUTES || 20;
// 将分钟转换为毫秒
const runTimeLimitMillis = runTimeLimitMinutes * 60 * 1000;
console.log(
`运行时间限制为:${runTimeLimitMinutes} 分钟 (${runTimeLimitMillis} 毫秒)`
);
// 设置一个定时器,在运行时间到达时终止进程
const shutdownTimer = setTimeout(() => {
console.log("时间到,Reached time limit, shutting down the process...");
process.exit(0); // 退出进程
}, runTimeLimitMillis);
const token = process.env.TELEGRAM_BOT_TOKEN;
const chatId = process.env.TELEGRAM_CHAT_ID;
const groupId = process.env.TELEGRAM_GROUP_ID;
const specificUser = process.env.SPECIFIC_USER || "14790897";
const maxConcurrentAccounts = parseInt(process.env.MAX_CONCURRENT_ACCOUNTS) || 3; // 每批最多同时运行的账号数
const usernames = process.env.USERNAMES.split(",");
const passwords = process.env.PASSWORDS ? process.env.PASSWORDS.split(",") : [];
// 读取每个账号对应的Cookie(逗号分隔,与USERNAMES一一对应),有Cookie则跳过表单登录
const cookiesEnv = process.env.COOKIES ? process.env.COOKIES.split(",") : [];
const loginUrl = process.env.WEBSITE || "https://linux.do"; //在GitHub action环境里它不能读取默认环境变量,只能在这里设置默认值
const delayBetweenInstances = 10000;
const totalAccounts = usernames.length; // 总的账号数
const delayBetweenBatches =
runTimeLimitMillis / Math.ceil(totalAccounts / maxConcurrentAccounts);
const isLikeSpecificUser = process.env.LIKE_SPECIFIC_USER === "true"; // 只有明确设置为"true"才开启
const isAutoLike = process.env.AUTO_LIKE !== "false"; // 默认开启,只有明确设置为"false"才关闭
const hideAccountInfo = process.env.HIDE_ACCOUNT_INFO !== "false"; // 默认隐藏账号信息,只有明确设置为"false"才显示
const enableRssFetch = process.env.ENABLE_RSS_FETCH === "true"; // 是否开启抓取RSS,只有明确设置为"true"才开启,默认为false
const enableTopicDataFetch = process.env.ENABLE_TOPIC_DATA_FETCH === "true"; // 是否开启抓取话题数据,只有明确设置为"true"才开启,默认为false
// 账号名脱敏函数,默认仅显示首字母加***
function maskUsername(username) {
if (!hideAccountInfo) return username;
if (!username || username.length === 0) return "***";
return username[0] + "***";
}
console.log(
`RSS抓取功能状态: ${enableRssFetch ? "开启" : "关闭"} (环境变量值: "${process.env.ENABLE_RSS_FETCH || ''}"),勿设置`
);
console.log(
`话题数据抓取功能状态: ${
enableTopicDataFetch ? "开启" : "关闭"
} (环境变量值: "${process.env.ENABLE_TOPIC_DATA_FETCH || ''}"),勿设置`
);
// 代理配置
const proxyConfig = getProxyConfig();
if (proxyConfig) {
console.log(
`代理配置: ${proxyConfig.type}://${proxyConfig.host}:${proxyConfig.port}`
);
// 测试代理连接
console.log("正在测试代理连接...");
const proxyWorking = await testProxyConnection(proxyConfig);
if (proxyWorking) {
console.log("✅ 代理连接测试成功");
} else {
console.log("❌ 代理连接测试失败,将使用直连");
}
} else {
console.log("未配置代理,使用直连");
const currentIP = await getCurrentIP();
if (currentIP) {
console.log(`当前IP地址: ${currentIP}`);
}
}
let bot;
if (token && (chatId || groupId)) {
bot = new TelegramBot(token);
}
// 简单的 Telegram 发送重试
async function tgSendWithRetry(id, message, maxRetries = 3) {
let lastErr;
for (let i = 0; i < maxRetries; i++) {
try {
await bot.sendMessage(id, message);
return true;
} catch (e) {
lastErr = e;
const delay = 1500 * (i + 1);
console.error(
`Telegram send failed (attempt ${i + 1}/${maxRetries}): ${
e && e.message ? e.message : e
}`
);
await new Promise((r) => setTimeout(r, delay));
}
}
throw lastErr;
}
async function sendToTelegram(message) {
if (!bot || !chatId) return;
try {
await tgSendWithRetry(chatId, message, 3);
console.log("Telegram message sent successfully");
} catch (error) {
console.error(
"Error sending Telegram message:",
error && error.code ? error.code : "",
error && error.message
? error.message.slice(0, 100)
: String(error).slice(0, 100)
);
}
}
async function sendToTelegramGroup(message) {
if (!bot || !groupId) {
console.error("sendToTelegramGroup: bot 或 groupId 不存在");
return;
}
// 过滤空内容,避免 Telegram 400 错误
if (!message || !String(message).trim()) {
console.warn("Telegram 群组推送内容为空,跳过发送");
return;
}
// 分割长消息,Telegram单条最大4096字符
const MAX_LEN = 4000;
if (typeof message === "string" && message.length > MAX_LEN) {
let start = 0;
let part = 1;
while (start < message.length) {
const chunk = message.slice(start, start + MAX_LEN);
try {
await tgSendWithRetry(groupId, chunk, 3);
console.log(`Telegram group message part ${part} sent successfully`);
} catch (error) {
console.error(
`Error sending Telegram group message part ${part}:`,
error
);
}
start += MAX_LEN;
part++;
}
} else {
try {
await tgSendWithRetry(groupId, message, 3);
console.log("Telegram group message sent successfully");
} catch (error) {
console.error("Error sending Telegram group message:", error);
}
}
}
//随机等待时间
function delayClick(time) {
return new Promise(function (resolve) {
setTimeout(resolve, time);
});
}
(async () => {
try {
// 有Cookie则跳过密码数量校验
if (
cookiesEnv.filter((c) => c && c.trim()).length === 0 &&
passwords.length !== usernames.length
) {
console.log(
`usernames: ${usernames.length}, passwords: ${passwords.length}`,
);
throw new Error("用户名和密码的数量不匹配!");
}
// 并发启动浏览器实例进行登录
const loginTasks = usernames.map((username, index) => {
const password = passwords[index] || "";
const cookie = cookiesEnv[index] ? cookiesEnv[index].trim() : null;
const delay = (index % maxConcurrentAccounts) * delayBetweenInstances; // 使得每一组内的浏览器可以分开启动
return () => {
// 确保这里返回的是函数,因为settimeout本身是异步的所以必须在外面给他一个promise await才能让它同步的等待这个时间才能执行
// 可以改为 return async () => {
// await new Promise((resolve) => setTimeout(resolve, delay));
// return await launchBrowserForUser(username, password, cookie);
// }; 更好理解
return new Promise((resolve, reject) => {
setTimeout(() => {
launchBrowserForUser(username, password, cookie)
.then(resolve)
.catch(reject);
}, delay);
});
};
});
// 依次执行每个批次的任务
for (let i = 0; i < totalAccounts; i += maxConcurrentAccounts) {
console.log(`当前批次:${i + 1} - ${i + maxConcurrentAccounts}`);
// 执行每批次最多 4 个账号
const batch = loginTasks
.slice(i, i + maxConcurrentAccounts)
.map(async (task) => {
const { browser } = await task(); // 运行任务并获取浏览器实例
return browser;
}); // 等待当前批次的任务完成
const browsers = await Promise.all(batch); // Task里面的任务本身是没有进行await的, 所以会继续执行下面的代码
// 如果还有下一个批次,等待指定的时间,同时,如果总共只有一个账号,也需要继续运行
if (i + maxConcurrentAccounts < totalAccounts || i === 0) {
console.log(`等待 ${delayBetweenBatches / 1000} 秒`);
await new Promise((resolve) =>
setTimeout(resolve, delayBetweenBatches),
);
} else {
console.log("没有下一个批次,即将结束");
}
console.log(
`批次 ${
Math.floor(i / maxConcurrentAccounts) + 1
} 完成,关闭浏览器...,浏览器对象:${browsers}`,
);
// 关闭所有浏览器实例
for (const browser of browsers) {
await browser.close();
}
}
console.log("所有账号登录操作已完成");
// 等待所有登录操作完成
// await Promise.all(loginTasks);
} catch (error) {
// 错误处理逻辑
console.error("发生错误:", error);
if (token && chatId) {
sendToTelegram(`${error.message}`);
}
}
})();
// 将浏览器Cookie字符串(如 "name=value; name2=value2")解析为 puppeteer setCookie 所需的对象数组
function parseCookieString(cookieStr, domain) {
return cookieStr
.split(";")
.map((part) => part.trim())
.filter((part) => part.includes("="))
.map((part) => {
const eqIndex = part.indexOf("=");
const name = part.substring(0, eqIndex).trim();
const value = part.substring(eqIndex + 1).trim();
return { name, value, domain, path: "/" };
});
}
async function launchBrowserForUser(username, password, cookie = null) {
let browser = null; // 在 try 之外声明 browser 变量
try {
console.log("当前用户:", maskUsername(username));
const browserOptions = {
headless: "auto",
args: ["--no-sandbox", "--disable-setuid-sandbox"], // Linux 需要的安全设置
};
// 添加代理配置到浏览器选项
const proxyConfig = getProxyConfig();
if (proxyConfig) {
const proxyArgs = getPuppeteerProxyArgs(proxyConfig);
browserOptions.args.push(...proxyArgs);
console.log(
`为用户 ${maskUsername(username)} 启用代理: ${proxyConfig.type}://${proxyConfig.host}:${proxyConfig.port}`
);
// 如果有用户名密码,puppeteer-real-browser会自动处理
if (proxyConfig.username && proxyConfig.password) {
browserOptions.proxy = {
host: proxyConfig.host,
port: proxyConfig.port,
username: proxyConfig.username,
password: proxyConfig.password,
};
}
}
var { connect } = await import("puppeteer-real-browser");
const { page, browser: newBrowser } = await connect(browserOptions);
browser = newBrowser; // 将 browser 初始化
// 启动截图功能
// takeScreenshots(page);
//登录操作
await navigatePage(loginUrl, page, browser);
await delayClick(8000);
// 设置额外的 headers
await page.setExtraHTTPHeaders({
"accept-language": "en-US,en;q=0.9",
});
// 验证 `navigator.webdriver` 属性是否为 undefined
// const isWebDriverUndefined = await page.evaluate(() => {
// return `${navigator.webdriver}`;
// });
// console.log("navigator.webdriver is :", isWebDriverUndefined); // 输出应为 false
page.on("pageerror", (error) => {
console.error(`Page error: ${error.message}`);
});
page.on("error", async (error) => {
// console.error(`Error: ${error.message}`);
// 检查是否是 localStorage 的访问权限错误
if (
error.message.includes(
"Failed to read the 'localStorage' property from 'Window'"
)
) {
console.log("Trying to refresh the page to resolve the issue...");
await page.reload(); // 刷新页面
// 重新尝试你的操作...
}
});
page.on("console", async (msg) => {
// console.log("PAGE LOG:", msg.text());
// 使用一个标志变量来检测是否已经刷新过页面
if (
!page._isReloaded &&
msg.text().includes("the server responded with a status of 429")
) {
// 设置标志变量为 true,表示即将刷新页面
page._isReloaded = true;
//由于油候脚本它这个时候可能会导航到新的网页,会导致直接执行代码报错,所以使用这个来在每个新网页加载之前来执行
try {
await page.evaluateOnNewDocument(() => {
localStorage.setItem("autoLikeEnabled", "false");
});
} catch (e) {
// Fallback to immediate evaluate when target already navigated/closed
try {
if (!page.isClosed || !page.isClosed()) {
await page.evaluate(() => {
localStorage.setItem("autoLikeEnabled", "false");
});
}
} catch (e2) {
console.warn(
`Skip disabling autoLike due to closed target: ${
(e2 && e2.message) ? e2.message : e2
}`
);
}
}
// 等待一段时间,比如 3 秒
await new Promise((resolve) => setTimeout(resolve, 3000));
console.log("Retrying now...");
// 尝试刷新页面
// await page.reload();
}
});
// 登录操作:优先使用Cookie,否则使用表单登录
if (cookie) {
console.log("检测到Cookie,跳过表单登录,直接设置Cookie");
const domain = new URL(loginUrl).hostname;
const cookieObjects = parseCookieString(cookie, domain);
await page.setCookie(...cookieObjects);
console.log(`已设置 ${cookieObjects.length} 个Cookie,正在刷新页面...`);
await page.reload({ waitUntil: "domcontentloaded" });
await delayClick(2000);
} else {
console.log("登录操作");
await login(page, username, password);
}
// 查找具有类名 "avatar" 的 img 元素验证登录是否成功
// 若存在 span.auth-buttons 则说明处于未登录状态
const avatarImg = await page.$("img.avatar");
const authButtons = await page.$("span.auth-buttons");
if (authButtons) {
console.log("找到 auth-buttons,用户未登录,登录失败");
throw new Error("登录失败:页面显示未登录状态(auth-buttons)");
} else if (avatarImg) {
console.log("找到avatarImg,登录成功");
} else {
console.log("未找到avatarImg,登录失败");
throw new Error("登录失败");
}
//真正执行阅读脚本
let externalScriptPath;
if (isLikeSpecificUser === "true") {
const randomChoice = Math.random() < 0.5; // 生成一个随机数,50% 概率为 true
if (randomChoice) {
externalScriptPath = path.join(
dirname(fileURLToPath(import.meta.url)),
"index_likeUser_activity.js"
);
console.log("使用index_likeUser_activity");
} else {
externalScriptPath = path.join(
dirname(fileURLToPath(import.meta.url)),
"index_likeUser.js"
);
console.log("使用index_likeUser");
}
} else {
externalScriptPath = path.join(
dirname(fileURLToPath(import.meta.url)),
"index.js"
);
}
const externalScript = fs.readFileSync(externalScriptPath, "utf8");
// 在每个新的文档加载时执行外部脚本
await page.evaluateOnNewDocument(
(...args) => {
const [specificUser, scriptToEval, isAutoLike] = args;
localStorage.setItem("read", true);
localStorage.setItem("specificUser", specificUser);
localStorage.setItem("isFirstRun", "false");
localStorage.setItem("autoLikeEnabled", isAutoLike);
console.log("当前点赞用户:", specificUser);
eval(scriptToEval);
},
specificUser,
externalScript,
isAutoLike
); //变量必须从外部显示的传入, 因为在浏览器上下文它是读取不了的
// 添加一个监听器来监听每次页面加载完成的事件
page.on("load", async () => {
// await page.evaluate(externalScript); //因为这个是在页面加载好之后执行的,而脚本是在页面加载好时刻来判断是否要执行,由于已经加载好了,脚本就不会起作用
});
// 如果是Linuxdo,就导航到我的帖子,但我感觉这里写没什么用,因为外部脚本已经定义好了,不对,这里不会点击按钮,所以不会跳转,需要手动跳转
if (loginUrl == "https://linux.do") {
await page.goto("https://linux.do/t/topic/13716/790", {
waitUntil: "domcontentloaded",
timeout: parseInt(process.env.NAV_TIMEOUT_MS || process.env.NAV_TIMEOUT || "120000", 10),
});
} else if (loginUrl == "https://meta.appinn.net") {
await page.goto("https://meta.appinn.net/t/topic/52006", {
waitUntil: "domcontentloaded",
timeout: parseInt(process.env.NAV_TIMEOUT_MS || process.env.NAV_TIMEOUT || "120000", 10),
});
} else {
await page.goto(`${loginUrl}/t/topic/1`, {
waitUntil: "domcontentloaded",
timeout: parseInt(process.env.NAV_TIMEOUT_MS || process.env.NAV_TIMEOUT || "120000", 10),
});
}
// Ensure automation injected after navigation (fallback in case init-script failed)
try {
await page.evaluate(
(specificUser, scriptToEval, isAutoLike) => {
if (!window.__autoInjected) {
localStorage.setItem("read", true);
localStorage.setItem("specificUser", specificUser);
localStorage.setItem("isFirstRun", "false");
localStorage.setItem("autoLikeEnabled", isAutoLike);
try { eval(scriptToEval); } catch (e) { console.error("eval external script failed", e); }
window.__autoInjected = true;
}
},
specificUser,
externalScript,
isAutoLike
);
} catch (e) {
console.warn(`Post-navigation inject failed: ${e && e.message ? e.message : e}`);
}
if (token && chatId) {
sendToTelegram(`${username} 登录成功`);
} // 监听页面跳转到新话题,自动推送RSS example:https://linux.do/t/topic/525305.rss
// 记录已推送过的 topicId,防止重复推送
if (enableRssFetch || enableTopicDataFetch) {
const pushedTopicIds = new Set();
const processedTopicIds = new Set(); // 用于话题数据处理的记录
page.on("framenavigated", async (frame) => {
if (frame.parentFrame() !== null) return;
const url = frame.url();
const match = url.match(/https:\/\/linux\.do\/t\/topic\/(\d+)/);
if (match) {
const topicId = match[1];
// RSS抓取处理
if (enableRssFetch && !pushedTopicIds.has(topicId)) {
pushedTopicIds.add(topicId);
const rssUrl = `https://linux.do/t/topic/${topicId}.rss`;
console.log("检测到话题跳转,抓取RSS:", rssUrl);
try {
// 停顿1.5秒再抓取
await new Promise((r) => setTimeout(r, 1500));
const rssPage = await browser.newPage();
await rssPage.goto(rssUrl, {
waitUntil: "domcontentloaded",
timeout: 20000,
});
// 停顿0.5秒再获取内容,确保页面渲染完成
await new Promise((r) => setTimeout(r, 1000));
const xml = await rssPage.evaluate(() => document.body.innerText);
await rssPage.close();
const parsedData = await parseRss(xml);
sendToTelegramGroup(parsedData);
} catch (e) {
console.error("抓取或发送RSS失败:", e, "可能是非公开话题");
}
}
// 话题数据抓取处理
if (enableTopicDataFetch && !processedTopicIds.has(topicId)) {
processedTopicIds.add(topicId);
console.log("检测到话题跳转,抓取话题数据:", url);
try {
// 停顿1秒再处理话题数据
await new Promise((r) => setTimeout(r, 1000));
await processAndSaveTopicData(page, url);
} catch (e) {
console.error("抓取或保存话题数据失败:", e);
}
}
}
// 停顿0.5秒后允许下次抓取
await new Promise((r) => setTimeout(r, 500));
});
}
return { browser };
} catch (err) {
// throw new Error(err);
console.log("Error in launchBrowserForUser:", err);
if (token && chatId) {
sendToTelegram(`${err.message}`);
}
return { browser }; // 错误时仍然返回 browser
}
}
async function login(page, username, password, retryCount = 3) {
// 使用XPath查询找到包含"登录"或"login"文本的按钮
let loginButtonFound = await page.evaluate(() => {
let loginButton = Array.from(document.querySelectorAll("button")).find(
(button) =>
button.textContent.includes("登录") ||
button.textContent.includes("login")
); // 注意loginButton 变量在外部作用域中是无法被 page.evaluate 内部的代码直接修改的。page.evaluate 的代码是在浏览器环境中执行的,这意味着它们无法直接影响 Node.js 环境中的变量
// 如果没有找到,尝试根据类名查找
if (!loginButton) {
loginButton = document.querySelector(".login-button");
}
if (loginButton) {
loginButton.click();
console.log("Login button clicked.");
return true; // 返回true表示找到了按钮并点击了
} else {
console.log("Login button not found.");
return false; // 返回false表示没有找到按钮
}
});
if (!loginButtonFound) {
if (loginUrl == "https://meta.appinn.net") {
await page.goto("https://meta.appinn.net/t/topic/52006", {
waitUntil: "domcontentloaded",
timeout: parseInt(process.env.NAV_TIMEOUT_MS || process.env.NAV_TIMEOUT || "120000", 10),
});
await page.click(".discourse-reactions-reaction-button");
} else {
await page.goto(`${loginUrl}/t/topic/1`, {
waitUntil: "domcontentloaded",
timeout: parseInt(process.env.NAV_TIMEOUT_MS || process.env.NAV_TIMEOUT || "120000", 10),
});
try {
await page.click(".discourse-reactions-reaction-button");
} catch (error) {
console.log("没有找到点赞按钮,可能是页面没有加载完成或按钮不存在");
}
}
}
// 等待用户名输入框加载
await page.waitForSelector("#login-account-name");
// 模拟人类在找到输入框后的短暂停顿
await delayClick(1000); // 延迟500毫秒
// 清空输入框并输入用户名
await page.click("#login-account-name", { clickCount: 3 });
await page.type("#login-account-name", username, {
delay: 100,
}); // 输入时在每个按键之间添加额外的延迟
await delayClick(1000);
// 等待密码输入框加载
// await page.waitForSelector("#login-account-password");
// 模拟人类在输入用户名后的短暂停顿
// delayClick; // 清空输入框并输入密码
await page.click("#login-account-password", { clickCount: 3 });
await page.type("#login-account-password", password, {
delay: 100,
});
// 模拟人类在输入完成后思考的短暂停顿
await delayClick(1000);
// 假设登录按钮的ID是'login-button',点击登录按钮
await page.waitForSelector("#login-button");
await delayClick(1000); // 模拟在点击登录按钮前的短暂停顿
await page.click("#login-button");
try {
await Promise.all([
page.waitForNavigation({ waitUntil: "domcontentloaded" }), // 等待 页面跳转 DOMContentLoaded 事件
// 去掉上面一行会报错:Error: Execution context was destroyed, most likely because of a navigation. 可能是因为之后没等页面加载完成就执行了脚本
page.click("#login-button", { force: true }), // 点击登录按钮触发跳转
]); //注意如果登录失败,这里会一直等待跳转,导致脚本执行失败 这点四个月之前你就发现了结果今天又遇到(有个用户遇到了https://linux.do/t/topic/169209/82),但是你没有在这个报错你提示我8.5
} catch (error) {
const alertError = await page.$(".alert.alert-error");
if (alertError) {
const alertText = await page.evaluate((el) => el.innerText, alertError); // 使用 evaluate 获取 innerText
if (
alertText.includes("incorrect") ||
alertText.includes("Incorrect ") ||
alertText.includes("不正确")
) {
throw new Error(
`非超时错误,请检查用户名密码是否正确,失败用户 ${maskUsername(username)}, 错误信息:${alertText}`
);
} else {
throw new Error(
`非超时错误,也不是密码错误,可能是IP导致,需使用中国美国香港台湾IP,失败用户 ${maskUsername(username)},错误信息:${alertText}`
);
}
} else {
if (retryCount > 0) {
console.log("Retrying login...");
await page.reload({ waitUntil: "domcontentloaded", timeout: parseInt(process.env.NAV_TIMEOUT_MS || process.env.NAV_TIMEOUT || "120000", 10) });
await delayClick(2000); // 增加重试前的延迟
return await login(page, username, password, retryCount - 1);
} else {
throw new Error(
`Navigation timed out in login.超时了,可能是IP质量问题,失败用户 ${maskUsername(username)},
${error}`
); //{password}
}
}
}
await delayClick(1000);
}
async function navigatePage(url, page, browser) {
try {
page.setDefaultNavigationTimeout(
parseInt(process.env.NAV_TIMEOUT_MS || process.env.NAV_TIMEOUT || "120000", 10)
);
} catch {}
await page.goto(url, { waitUntil: "domcontentloaded" }); //如果使用默认的load,linux下页面会一直加载导致无法继续执行
const startTime = Date.now(); // 记录开始时间
let pageTitle = await page.title(); // 获取当前页面标题
while (pageTitle.includes("Just a moment") || pageTitle.includes("请稍候")) {
console.log("The page is under Cloudflare protection. Waiting...");
await delayClick(2000); // 每次检查间隔2秒
// 重新获取页面标题
pageTitle = await page.title();
// 检查是否超过15秒
if (Date.now() - startTime > 35000) {
console.log("Timeout exceeded, aborting actions.");
sendToTelegram(`超时了,无法通过Cloudflare验证`);
await browser.close();
// todo: 这里其实不能关的m因为我们是在最后统一关的你不能在这里关m如果你在这关,后面就会触发attempted to sue detached frame
return; // 超时则退出函数
}
}
console.log("页面标题:", pageTitle);
}
// 每秒截图功能
async function takeScreenshots(page) {
let screenshotIndex = 0;
setInterval(async () => {
screenshotIndex++;
const screenshotPath = path.join(
screenshotDir,
`screenshot-${screenshotIndex}.png`
);
try {
await page.screenshot({ path: screenshotPath, fullPage: true });
console.log(`Screenshot saved: ${screenshotPath}`);
} catch (error) {
console.error("Error taking screenshot:", error);
}
}, 1000);
// 注册退出时删除文件夹的回调函数
process.on("exit", () => {
try {
fs.rmdirSync(screenshotDir, { recursive: true });
console.log(`Deleted folder: ${screenshotDir}`);
} catch (error) {
console.error(`Error deleting folder ${screenshotDir}:`, error);
}
});
}
import express from "express";
const healthApp = express();
const HEALTH_PORT = process.env.HEALTH_PORT || 7860;
// 健康探针路由
healthApp.get("/health", (req, res) => {
const memoryUsage = process.memoryUsage();
// 将字节转换为MB
const memoryUsageMB = {
rss: `${(memoryUsage.rss / (1024 * 1024)).toFixed(2)} MB`, // 转换为MB并保留两位小数
heapTotal: `${(memoryUsage.heapTotal / (1024 * 1024)).toFixed(2)} MB`,
heapUsed: `${(memoryUsage.heapUsed / (1024 * 1024)).toFixed(2)} MB`,
external: `${(memoryUsage.external / (1024 * 1024)).toFixed(2)} MB`,
arrayBuffers: `${(memoryUsage.arrayBuffers / (1024 * 1024)).toFixed(2)} MB`,
};
const healthData = {
status: "OK",
timestamp: new Date().toISOString(),
memoryUsage: memoryUsageMB,
uptime: process.uptime().toFixed(2), // 保留两位小数
};
res.status(200).json(healthData);
});
healthApp.get("/", (req, res) => {
res.send(`
<html>
<head>
<title>Auto Read</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
color: #333;
margin: 0;
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.container {
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
max-width: 600px;
text-align: center;
}
h1 {
color: #007bff;
}
p {
font-size: 18px;
margin: 15px 0;
}
a {
color: #007bff;
text-decoration: none;
font-weight: bold;
}
a:hover {
text-decoration: underline;
}
footer {
margin-top: 20px;
font-size: 14px;
color: #555;
}
</style>
</head>
<body>
<div class="container">
<h1>Welcome to the Auto Read App</h1>
<p>You can check the server's health at <a href="/health">/health</a>.</p>
<p>GitHub: <a href="https://github.com/14790897/auto-read-liunxdo" target="_blank">https://github.com/14790897/auto-read-liunxdo</a></p>
<footer>© 2024 Auto Read App</footer>
</div>
</body>
</html>
`);
});
healthApp.listen(HEALTH_PORT, () => {
console.log(
`Health check endpoint is running at http://localhost:${HEALTH_PORT}/health`
);
});