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"; dotenv.config(); // 截图保存的文件夹 // 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 specificUser = process.env.SPECIFIC_USER || "14790897"; const maxConcurrentAccounts = 4; // 每批最多同时运行的账号数 const usernames = process.env.USERNAMES.split(","); const passwords = process.env.PASSWORDS.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); let bot; if (token && chatId) { bot = new TelegramBot(token); } function sendToTelegram(message) { if (!bot) return; bot .sendMessage(chatId, message) .then(() => { console.log("Telegram message sent successfully"); }) .catch((error) => { console.error("Error sending Telegram message:", error); }); } //随机等待时间 function delayClick(time) { return new Promise(function (resolve) { setTimeout(resolve, time); }); } (async () => { try { if (usernames.length !== passwords.length) { console.log(usernames.length, usernames, passwords.length, passwords); throw new Error("用户名和密码的数量不匹配!"); } // 并发启动浏览器实例进行登录 const loginTasks = usernames.map((username, index) => { const password = passwords[index]; const delay = (index % maxConcurrentAccounts) * delayBetweenInstances; // 使得每一组内的浏览器可以分开启动 return () => { // 确保这里返回的是函数 return new Promise((resolve, reject) => { setTimeout(() => { launchBrowserForUser(username, password) .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}`); } } })(); async function launchBrowserForUser(username, password) { let browser = null; // 在 try 之外声明 browser 变量 try { console.log("当前用户:", username); const browserOptions = { headless: "auto", args: ["--no-sandbox", "--disable-setuid-sandbox"], // Linux 需要的安全设置 }; // 如果环境变量不是 'dev',则添加代理配置 // if (process.env.ENVIRONMENT !== "dev") { // browserOptions["proxy"] = { // host: "38.154.227.167", // port: "5868", // username: "pqxujuyl", // password: "y1nmb5kjbz9t", // }; // } 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; //由于油候脚本它这个时候可能会导航到新的网页,会导致直接执行代码报错,所以使用这个来在每个新网页加载之前来执行 await page.evaluateOnNewDocument(() => { localStorage.setItem("autoLikeEnabled", "false"); }); // 等待一段时间,比如 3 秒 await new Promise((resolve) => setTimeout(resolve, 3000)); console.log("Retrying now..."); // 尝试刷新页面 // await page.reload(); } }); // //登录操作 console.log("登录操作"); await login(page, username, password); // 查找具有类名 "avatar" 的 img 元素验证登录是否成功 const avatarImg = await page.$("img.avatar"); if (avatarImg) { console.log("找到avatarImg,登录成功"); } else { console.log("未找到avatarImg,登录失败"); throw new Error("登录失败"); } //真正执行阅读脚本 const externalScriptPath = path.join( dirname(fileURLToPath(import.meta.url)), "index_likeUser.js" ); const externalScript = fs.readFileSync(externalScriptPath, "utf8"); // 在每个新的文档加载时执行外部脚本 await page.evaluateOnNewDocument( (...args) => { const [specificUser, scriptToEval] = args; localStorage.setItem("read", true); localStorage.setItem("specificUser", specificUser); localStorage.setItem("isFirstRun", "false"); console.log("当前点赞用户:", specificUser); eval(scriptToEval); }, specificUser, externalScript ); //变量必须从外部显示的传入, 因为在浏览器上下文它是读取不了的 // 添加一个监听器来监听每次页面加载完成的事件 page.on("load", async () => { // await page.evaluate(externalScript); //因为这个是在页面加载好之后执行的,而脚本是在页面加载好时刻来判断是否要执行,由于已经加载好了,脚本就不会起作用 }); // 如果是Linuxdo,就导航到我的帖子,但我感觉这里写没什么用,因为外部脚本已经定义好了,不对,这里不会点击按钮,所以不会跳转,需要手动跳转 if (loginUrl == "https://linux.do") { await page.goto("https://linux.do/t/topic/13716/630", { waitUntil: "domcontentloaded", }); } else if (loginUrl == "https://meta.appinn.net") { await page.goto("https://meta.appinn.net/t/topic/52006", { waitUntil: "domcontentloaded", }); } else { await page.goto(`${loginUrl}/t/topic/1`, { waitUntil: "domcontentloaded", }); } if (token && chatId) { sendToTelegram(`${username} 登录成功`); } 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", }); await page.click(".discourse-reactions-reaction-button"); } else { await page.goto(`${loginUrl}/t/topic/1`, { waitUntil: "domcontentloaded", }); await page.click(".discourse-reactions-reaction-button"); } } // 等待用户名输入框加载 await page.waitForSelector("#login-account-name"); // 模拟人类在找到输入框后的短暂停顿 await delayClick(500); // 延迟500毫秒 // 清空输入框并输入用户名 await page.click("#login-account-name", { clickCount: 3 }); await page.type("#login-account-name", username, { delay: 100, }); // 输入时在每个按键之间添加额外的延迟 // 等待密码输入框加载 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 page.click("#login-button"); await delayClick(500); // 模拟在点击登录按钮前的短暂停顿 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( `非超时错误,请检查用户名密码是否正确,失败用户 ${username}, 错误信息:${alertText}` ); } else { throw new Error( `非超时错误,也不是密码错误,失败用户 ${username},错误信息:${alertText}` ); } } else { if (retryCount > 0) { console.log("Retrying login..."); await delayClick(2000); // 增加重试前的延迟 return await login(page, username, password, retryCount - 1); } else { throw new Error( `Navigation timed out in login.超时了,可能是IP质量问题,失败用户 ${username}, ${error}` ); //{password} } } } await delayClick(1000); } async function navigatePage(url, page, browser) { await page.goto(url, { waitUntil: "domcontentloaded" }); //如果使用默认的load,linux下页面会一直加载导致无法继续执行 const startTime = Date.now(); // 记录开始时间 let pageTitle = await page.title(); // 获取当前页面标题 while (pageTitle.includes("Just a moment")) { 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."); await browser.close(); 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(` Auto Read

Welcome to the Auto Read App

You can check the server's health at /health.

GitHub: https://github.com/14790897/auto-read-liunxdo

`); }); healthApp.listen(HEALTH_PORT, () => { console.log( `Health check endpoint is running at http://localhost:${HEALTH_PORT}/health` ); });