├── .env.example ├── .github └── workflows │ ├── docker-image-dev.yml │ ├── docker-image.yml │ └── snake.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── app.js ├── assets └── banner.jpg ├── dist ├── github-contribution-grid-snake-dark.svg ├── github-contribution-grid-snake.gif └── github-contribution-grid-snake.svg ├── entrypoint.sh ├── package-lock.json ├── package.json ├── start.js └── test.js /.env.example: -------------------------------------------------------------------------------- 1 | APP_USER=gradient@bot.com 2 | APP_PASS=abcedfgh 3 | PROXY_HTTP_PORT=2000 4 | PROXY_SOCKS_PORT=2000 5 | -------------------------------------------------------------------------------- /.github/workflows/docker-image-dev.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: [ "dev" ] 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Log in to Docker Hub 12 | uses: docker/login-action@v3.3.0 13 | with: 14 | username: ${{ secrets.DOCKER_USERNAME }} 15 | password: ${{ secrets.DOCKER_PASSWORD }} 16 | - name: Set up QEMU 17 | uses: docker/setup-qemu-action@v3 18 | - name: Set up Docker Buildx 19 | uses: docker/setup-buildx-action@v2 20 | - name: Build and push Docker image 21 | id: push 22 | uses: docker/build-push-action@v6.7.0 23 | with: 24 | platforms: linux/amd64,linux/arm64 25 | context: . 26 | file: ./Dockerfile 27 | push: true 28 | tags: overtrue/gradient-bot:dev 29 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Log in to Docker Hub 18 | uses: docker/login-action@v3.3.0 19 | with: 20 | username: ${{ secrets.DOCKER_USERNAME }} 21 | password: ${{ secrets.DOCKER_PASSWORD }} 22 | - name: Set up QEMU 23 | uses: docker/setup-qemu-action@v3 24 | - name: Set up Docker Buildx 25 | uses: docker/setup-buildx-action@v2 26 | - name: Build and push Docker image 27 | id: push 28 | uses: docker/build-push-action@v6.7.0 29 | with: 30 | platforms: linux/amd64,linux/arm64 31 | context: . 32 | file: ./Dockerfile 33 | push: true 34 | tags: overtrue/gradient-bot 35 | -------------------------------------------------------------------------------- /.github/workflows/snake.yml: -------------------------------------------------------------------------------- 1 | name: Generate Snake 2 | 3 | on: 4 | schedule: 5 | - cron: "0 */12 * * *" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - uses: Platane/snk@v3 18 | id: snake-gif 19 | with: 20 | github_user_name: ${{ github.repository_owner }} 21 | outputs: | 22 | dist/github-contribution-grid-snake.svg 23 | dist/github-contribution-grid-snake-dark.svg?palette=github-dark 24 | dist/github-contribution-grid-snake.gif?color_snake=#fb4934&color_dots=#fbf1c7,#b8bb26,#fabd2f,#fe8019,#fb4934 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | 28 | - name: Push to GitHub 29 | uses: EndBug/add-and-commit@v9 30 | with: 31 | message: 'Generate Contribution Snake' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Environment variables 8 | .env 9 | .env.local 10 | .env.*.local 11 | 12 | # Runtime data 13 | *.log 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Debug files 19 | error.log 20 | error.html 21 | error.png 22 | *.debug 23 | 24 | # Chrome extension 25 | *.crx 26 | 27 | # IDE 28 | .idea/ 29 | .vscode/ 30 | *.swp 31 | *.swo 32 | 33 | # OS 34 | .DS_Store 35 | Thumbs.db 36 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-slim 2 | 3 | # 安装 Chrome 依赖 4 | RUN apt-get update && apt-get install -y \ 5 | wget \ 6 | gnupg \ 7 | ca-certificates \ 8 | procps \ 9 | fonts-liberation \ 10 | libasound2 \ 11 | libatk-bridge2.0-0 \ 12 | libatk1.0-0 \ 13 | libatspi2.0-0 \ 14 | libcups2 \ 15 | libdbus-1-3 \ 16 | libdrm2 \ 17 | libgbm1 \ 18 | libgtk-3-0 \ 19 | libnspr4 \ 20 | libnss3 \ 21 | libxcomposite1 \ 22 | libxdamage1 \ 23 | libxfixes3 \ 24 | libxkbcommon0 \ 25 | libxrandr2 \ 26 | xdg-utils \ 27 | libu2f-udev \ 28 | libvulkan1 \ 29 | && rm -rf /var/lib/apt/lists/* 30 | 31 | # 安装 Chrome 32 | RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ 33 | && echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list \ 34 | && apt-get update \ 35 | && apt-get install -y google-chrome-stable \ 36 | && rm -rf /var/lib/apt/lists/* 37 | 38 | # 验证 Chrome 安装 39 | RUN google-chrome --version 40 | 41 | # 安装 PM2 42 | RUN npm install pm2 -g 43 | 44 | WORKDIR /app 45 | 46 | # 复制项目文件 47 | COPY package*.json ./ 48 | RUN npm install 49 | 50 | COPY . . 51 | 52 | # 设置环境变量 53 | ENV CHROME_PATH=/usr/bin/google-chrome 54 | ENV CHROME_BIN=/usr/bin/google-chrome 55 | ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true 56 | ENV DISPLAY=:99 57 | 58 | # 创建扩展目录 59 | RUN mkdir -p /root/.config/google-chrome/Default/Extensions 60 | 61 | # 启动脚本 62 | COPY entrypoint.sh /entrypoint.sh 63 | RUN chmod +x /entrypoint.sh 64 | 65 | ENTRYPOINT ["/entrypoint.sh"] 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 overtrue 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gradient Network 挂机脚本保姆级教程 2 | 3 | > 👨‍💻 开发者:小林 (@yoyomyoyoa) 4 | 5 | ## 🌟 这是什么? 6 | 7 | 这是一个帮助你自动挂机赚取 Gradient Network 积分的工具。它可以: 8 | - 自动登录账号 9 | - 保持在线状态 10 | - 24小时挂机运行 11 | - 支持代理IP 12 | 13 | ## 🎯 准备工作 14 | 15 | ### 1. 注册 Gradient Network 账号 16 | - 点击这里注册:[Gradient Network 注册](https://app.gradient.network/signup?code=VV3TZE) 17 | - 记住你的邮箱和密码,后面需要用到 18 | 19 | ### 2. 购买代理(强烈推荐) 20 | 1. 访问 [Proxy-Cheap](https://app.proxy-cheap.com/r/puD3oz) 21 | 2. 注册并登录 22 | 3. 选择 Static Residential 类型的代理 23 | 4. 购买后,你会得到类似这样的代理地址: 24 | ``` 25 | socks5://用户名:密码@代理地址:端口 26 | ``` 27 | 28 | ### 3. 准备服务器 29 | - 推荐使用 Ubuntu 系统的 VPS 30 | - 内存:1GB 及以上 31 | - 建议使用 [Vultr](https://www.vultr.com/) 或 [DigitalOcean](https://www.digitalocean.com/) 32 | 33 | ## 📝 安装步骤 34 | 35 | ### 第一步:连接到服务器 36 | 37 | #### Windows 用户: 38 | 1. 下载并安装 [PuTTY](https://www.putty.org/) 39 | 2. 打开 PuTTY 40 | 3. 输入你的服务器 IP 41 | 4. 点击 "Open" 42 | 5. 输入用户名(通常是 root)和密码 43 | 44 | #### Mac/Linux 用户: 45 | 1. 打开终端 46 | 2. 输入:`ssh root@你的服务器IP` 47 | 3. 输入密码 48 | 49 | ### 第二步:安装必要软件 50 | 51 | 复制以下命令,在服务器终端中运行: 52 | ```bash 53 | # 更新系统 54 | sudo apt update && sudo apt upgrade -y 55 | 56 | # 安装必要工具 57 | sudo apt install -y curl wget git screen 58 | 59 | # 安装 Chrome 依赖 60 | sudo apt install -y fonts-liberation libasound2 libatk-bridge2.0-0 libatk1.0-0 libatspi2.0-0 libcairo2 libcups2 libdbus-1-3 libdrm2 libexpat1 libgbm1 libglib2.0-0 libnspr4 libnss3 libpango-1.0-0 libx11-6 libxcb1 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxkbcommon0 libxrandr2 xdg-utils 61 | 62 | # 下载并安装 Chrome 63 | wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb 64 | sudo apt install -y ./google-chrome-stable_current_amd64.deb 65 | 66 | # 安装 Docker 67 | curl -fsSL https://get.docker.com -o get-docker.sh 68 | sh get-docker.sh 69 | 70 | # 验证安装 71 | google-chrome --version 72 | docker --version 73 | ``` 74 | 75 | ### 第三步:下载并运行程序 76 | 77 | 1. 下载程序: 78 | ```bash 79 | # 克隆代码 80 | git clone https://github.com/mumumusf/gradient-network-bot.git 81 | cd gradient-network-bot 82 | ``` 83 | 84 | 2. 创建 screen 会话(保证程序不会因为断开 SSH 而停止): 85 | ```bash 86 | screen -S gradient-bot 87 | ``` 88 | 89 | 3. 构建并运行 Docker 容器(替换下面的信息为你自己的): 90 | ```bash 91 | # 构建 Docker 镜像 92 | sudo docker build . -t gradient-bot . 93 | 94 | # 运行容器 95 | sudo docker run -d --name gradient-bot -e APP_USER=你的Gradient邮箱 -e APP_PASS=你的Gradient密码 -e PROXY=socks5://代理用户名:代理密码@代理地址:端口 -e DEBUG=true --restart always gradient-bot 96 | ``` 97 | 98 | 4. 查看运行日志: 99 | ```bash 100 | sudo docker logs -f gradient-bot 101 | ``` 102 | 103 | 5. 按 `Ctrl + A` 然后按 `D` 来保持程序在后台运行 104 | 105 | ## 🔍 如何检查程序是否正常运行? 106 | 107 | 1. 重新连接到程序界面: 108 | ```bash 109 | screen -r gradient-bot 110 | ``` 111 | 112 | 2. 检查运行状态: 113 | ```bash 114 | docker ps 115 | ``` 116 | 如果看到 `gradient-bot` 状态是 `Up`,说明程序正在运行 117 | 118 | 3. 查看最新日志: 119 | ```bash 120 | sudo docker logs -f gradient-bot 121 | ``` 122 | 123 | ## ❓ 常见问题解答 124 | 125 | ### 1. 如何判断是否正常运行? 126 | - 运行 `docker ps` 能看到容器在线 127 | - 日志中没有红色报错信息 128 | - 登录网站后积分有增长 129 | 130 | ### 2. 代理在哪里买? 131 | 推荐使用 [Proxy-Cheap](https://app.proxy-cheap.com/r/ksvW8Z): 132 | - 选择 Static Residential 类型 133 | - 稳定性好,价格实惠 134 | - 支持多种支付方式 135 | 136 | ### 3. 遇到问题怎么办? 137 | - 检查网络是否正常 138 | - 确认账号密码是否正确 139 | - 查看运行日志寻找错误信息 140 | - 加入我们的交流群寻求帮助 141 | 142 | ## 📱 联系方式 143 | 144 | - 开发者:小林 145 | - Twitter:[@yoyomyoyoa](https://twitter.com/yoyomyoyoa) 146 | 147 | ## ⚠️ 注意事项 148 | 149 | 1. 请使用可靠的代理服务 150 | 2. 定期检查程序运行状态 151 | 3. 保持服务器稳定在线 152 | 4. 本项目仅供学习使用 153 | 5. 停止并删除旧容器 154 | docker stop gradient-bot1 155 | docker rm gradient-bot1 156 | 157 | 158 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | const path = require("path") 3 | const crypto = require("crypto") 4 | const request = require("request") 5 | const { Builder, By, Key, until } = require("selenium-webdriver") 6 | const chrome = require("selenium-webdriver/chrome") 7 | const proxy = require("selenium-webdriver/proxy") 8 | const proxyChain = require("proxy-chain") 9 | require('console-stamp')(console, { 10 | format: ':date(yyyy/mm/dd HH:MM:ss.l)' 11 | }) 12 | 13 | // 加载环境变量 14 | require("dotenv").config() 15 | 16 | // 设置 ChromeDriver 17 | require("chromedriver") 18 | 19 | const CRX_URL = "https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&prodversion=112&x=id%3D${extensionId}%26installsource%3Dondemand%26uc" 20 | const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36" 21 | 22 | const extensionId = "caacbgbklghmpodbdafajbgdnegacfmo" 23 | const USER = process.env.APP_USER || "" 24 | const PASSWORD = process.env.APP_PASS || "" 25 | const ALLOW_DEBUG = !!process.env.DEBUG?.length || false 26 | const EXTENSION_FILENAME = "app.crx" 27 | const PROXY = process.env.PROXY || undefined 28 | 29 | // 添加启动横幅 30 | console.log("\n") 31 | console.log("========================================================") 32 | console.log(" Gradient Network Bot") 33 | console.log("--------------------------------------------------------") 34 | console.log(" 开发者: 小林 (@yoyomyoyoa)") 35 | console.log("--------------------------------------------------------") 36 | console.log(" 免责声明:") 37 | console.log(" 1. 本项目仅供学习交流使用") 38 | console.log(" 2. 使用本项目产生的任何后果由使用者自行承担") 39 | console.log(" 3. 请遵守相关法律法规,不要滥用") 40 | console.log("========================================================") 41 | console.log("\n") 42 | 43 | console.log("-> 程序启动中...") 44 | console.log("-> 账号:", USER) 45 | console.log("-> 密码:", PASSWORD) 46 | console.log("-> 代理:", PROXY) 47 | console.log("-> 调试模式:", ALLOW_DEBUG ? "开启" : "关闭") 48 | 49 | if (!USER || !PASSWORD) { 50 | console.error("请设置环境变量 APP_USER 和 APP_PASS") 51 | process.exit(1) 52 | } 53 | 54 | if (ALLOW_DEBUG) { 55 | console.log( 56 | "-> 调试模式已开启!将在出错时生成截图和控制台日志!" 57 | ) 58 | } 59 | 60 | async function downloadExtension(extensionId) { 61 | const url = CRX_URL.replace("${extensionId}", extensionId) 62 | const headers = { 63 | "User-Agent": USER_AGENT, 64 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 65 | "Accept-Language": "en-US,en;q=0.5", 66 | "Accept-Encoding": "gzip, deflate, br", 67 | "Connection": "keep-alive", 68 | "Upgrade-Insecure-Requests": "1" 69 | } 70 | 71 | console.log("-> 正在下载扩展,地址:", url) 72 | 73 | // 强制重新下载扩展 74 | if (fs.existsSync(EXTENSION_FILENAME)) { 75 | console.log("-> 删除旧的扩展文件...") 76 | fs.unlinkSync(EXTENSION_FILENAME) 77 | } 78 | 79 | return new Promise((resolve, reject) => { 80 | request({ url, headers, encoding: null }, (error, response, body) => { 81 | if (error) { 82 | console.error("-> 下载扩展时出错:", error) 83 | return reject(error) 84 | } 85 | if (response.statusCode !== 200) { 86 | console.error("-> 下载扩展失败!状态码:", response.statusCode) 87 | return reject(new Error(`下载扩展失败!状态码: ${response.statusCode}`)) 88 | } 89 | fs.writeFileSync(EXTENSION_FILENAME, body) 90 | if (ALLOW_DEBUG) { 91 | const md5 = crypto.createHash("md5").update(body).digest("hex") 92 | console.log("-> 扩展 MD5:", md5) 93 | } 94 | console.log("-> 扩展下载成功!") 95 | resolve() 96 | }) 97 | }) 98 | } 99 | 100 | async function takeScreenshot(driver, filename) { 101 | // if ALLOW_DEBUG is set, taking screenshot 102 | if (!ALLOW_DEBUG) { 103 | return 104 | } 105 | 106 | const data = await driver.takeScreenshot() 107 | fs.writeFileSync(filename, Buffer.from(data, "base64")) 108 | } 109 | 110 | async function generateErrorReport(driver) { 111 | //write dom 112 | const dom = await driver.findElement(By.css("html")).getAttribute("outerHTML") 113 | fs.writeFileSync("error.html", dom) 114 | 115 | await takeScreenshot(driver, "error.png") 116 | 117 | const logs = await driver.manage().logs().get("browser") 118 | fs.writeFileSync( 119 | "error.log", 120 | logs.map((log) => `${log.level.name}: ${log.message}`).join("\n") 121 | ) 122 | } 123 | 124 | // 检查 Chrome 路径 125 | const CHROME_PATHS = [ 126 | "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", 127 | "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", 128 | process.env.CHROME_PATH 129 | ].filter(Boolean) 130 | 131 | async function findChromePath() { 132 | for (const path of CHROME_PATHS) { 133 | if (fs.existsSync(path)) { 134 | console.log("-> Chrome 路径:", path) 135 | return path 136 | } 137 | } 138 | throw new Error("未找到 Chrome 浏览器!请安装 Chrome 浏览器或设置 CHROME_PATH 环境变量。") 139 | } 140 | 141 | async function testProxy(proxyUrl) { 142 | console.log("-> 正在测试代理连接...", proxyUrl) 143 | 144 | return new Promise((resolve, reject) => { 145 | const options = { 146 | url: 'https://api.ipify.org?format=json', 147 | proxy: proxyUrl, 148 | timeout: 10000, 149 | headers: { 150 | 'User-Agent': USER_AGENT 151 | } 152 | } 153 | 154 | request(options, (error, response, body) => { 155 | if (error) { 156 | console.error("-> 代理测试失败:", error.message) 157 | return reject(error) 158 | } 159 | 160 | if (response.statusCode !== 200) { 161 | console.error("-> 代理测试失败! 状态码:", response.statusCode) 162 | return reject(new Error(`代理测试失败! 状态码: ${response.statusCode}`)) 163 | } 164 | 165 | try { 166 | const data = JSON.parse(body) 167 | console.log("-> 代理测试成功! IP:", data.ip) 168 | resolve(data.ip) 169 | } catch (e) { 170 | console.error("-> 代理测试失败! 无法解析响应:", body) 171 | reject(e) 172 | } 173 | }) 174 | }) 175 | } 176 | 177 | async function convertProxyUrl(proxyUrl) { 178 | console.log("-> 正在转换代理地址...") 179 | try { 180 | const url = new URL(proxyUrl) 181 | const protocol = url.protocol.replace(':', '') 182 | const username = url.username 183 | const password = url.password 184 | const host = url.hostname 185 | const port = url.port 186 | 187 | // 如果是 socks5 代理,尝试转换为 http 188 | if (protocol === 'socks5') { 189 | console.log("-> 检测到 SOCKS5 代理,尝试转换为 HTTP...") 190 | const httpProxy = `http://${username}:${password}@${host}:${port}` 191 | console.log("-> 转换后的代理地址:", httpProxy) 192 | return httpProxy 193 | } 194 | 195 | return proxyUrl 196 | } catch (error) { 197 | console.error("-> 代理地址转换失败:", error.message) 198 | return proxyUrl 199 | } 200 | } 201 | 202 | async function startBrowser() { 203 | console.log("-> 正在启动浏览器...") 204 | console.log("-> 浏览器启动配置:") 205 | console.log(" - 无头模式: 开启") 206 | console.log(" - 窗口大小: 1920x1080") 207 | console.log(" - 禁用GPU: 是") 208 | console.log(" - 禁用扩展: 否") 209 | 210 | const chromePath = await findChromePath() 211 | 212 | const options = new chrome.Options() 213 | .setChromeBinaryPath(chromePath) 214 | .addArguments('--headless=new') 215 | .addArguments('--no-sandbox') 216 | .addArguments('--disable-dev-shm-usage') 217 | .addArguments('--disable-gpu') 218 | .addArguments('--window-size=1920,1080') 219 | .addArguments(`--user-agent=${USER_AGENT}`) 220 | .addArguments('--disable-web-security') 221 | .addArguments('--ignore-certificate-errors') 222 | .addArguments('--dns-prefetch-disable') 223 | .addArguments('--disable-features=IsolateOrigins,site-per-process') 224 | 225 | if (PROXY) { 226 | console.log("-> 正在设置代理...", PROXY) 227 | 228 | // 转换代理地址 229 | const convertedProxy = await convertProxyUrl(PROXY) 230 | 231 | // 先测试代理 232 | try { 233 | await testProxy(convertedProxy) 234 | } catch (error) { 235 | console.error("-> 代理不可用,程序退出") 236 | throw new Error("代理连接测试失败,请检查代理配置或更换代理") 237 | } 238 | 239 | const newProxyUrl = await proxyChain.anonymizeProxy(convertedProxy) 240 | console.log("-> 新代理地址:", newProxyUrl) 241 | const proxyUrl = new URL(newProxyUrl) 242 | console.log("-> 代理主机:", proxyUrl.hostname) 243 | console.log("-> 代理端口:", proxyUrl.port) 244 | 245 | // 设置 Chrome 代理 246 | options.addArguments(`--proxy-server=${proxyUrl.protocol}//${proxyUrl.hostname}:${proxyUrl.port}`) 247 | 248 | // 添加代理认证信息 249 | if (proxyUrl.username && proxyUrl.password) { 250 | options.addArguments(`--proxy-auth=${proxyUrl.username}:${proxyUrl.password}`) 251 | } 252 | 253 | // 添加代理相关参数 254 | options.addArguments('--proxy-bypass-list=<-loopback>') 255 | options.addArguments('--host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE localhost"') 256 | 257 | console.log("-> 代理设置完成!") 258 | } else { 259 | console.log("-> 未设置代理!") 260 | } 261 | 262 | // 添加扩展 263 | options.addExtensions(path.resolve(__dirname, EXTENSION_FILENAME)) 264 | console.log("-> 扩展已添加!", EXTENSION_FILENAME) 265 | 266 | try { 267 | const driver = await new Builder() 268 | .forBrowser('chrome') 269 | .setChromeOptions(options) 270 | .build() 271 | 272 | console.log("-> 浏览器启动成功!") 273 | return driver 274 | } catch (error) { 275 | console.error("-> 浏览器启动失败!错误信息:", error.message) 276 | throw error 277 | } 278 | } 279 | 280 | async function getProxyIpInfo(driver, proxyUrl) { 281 | // const url = "https://httpbin.org/ip" 282 | const url = "https://myip.ipip.net" 283 | 284 | console.log("-> Getting proxy IP info:", proxyUrl) 285 | 286 | try { 287 | await driver.get(url) 288 | await driver.wait(until.elementLocated(By.css("body")), 30000) 289 | const pageText = await driver.findElement(By.css("body")).getText() 290 | console.log("-> Proxy IP info:", pageText) 291 | } catch (error) { 292 | console.error("-> Failed to get proxy IP info:", error) 293 | throw new Error("Failed to get proxy IP info!") 294 | } 295 | } 296 | 297 | async function waitForSiteAvailable(driver, maxRetries = 3) { 298 | console.log("-> 检查网站可访问性...") 299 | 300 | for (let i = 0; i < maxRetries; i++) { 301 | try { 302 | console.log(`-> 尝试访问网站 (第 ${i + 1} 次)...`) 303 | await driver.get("https://app.gradient.network/") 304 | 305 | // 等待页面加载 306 | await driver.wait(async () => { 307 | const readyState = await driver.executeScript('return document.readyState') 308 | console.log("-> 页面加载状态:", readyState) 309 | return readyState === 'complete' 310 | }, 30000) 311 | 312 | // 检查是否有错误页面 313 | const title = await driver.getTitle() 314 | console.log("-> 页面标题:", title) 315 | 316 | if (title.includes("can't be reached") || title.includes("ERR_")) { 317 | throw new Error("网站无法访问") 318 | } 319 | 320 | // 等待一下让 JavaScript 完全执行 321 | await driver.sleep(5000) 322 | 323 | // 验证页面内容 324 | const bodyText = await driver.findElement(By.css('body')).getText() 325 | if (bodyText.includes("This site can't be reached") || 326 | bodyText.includes("ERR_") || 327 | bodyText.includes("took too long to respond")) { 328 | throw new Error("页面加载错误") 329 | } 330 | 331 | console.log("-> 网站可以访问!") 332 | return true 333 | 334 | } catch (error) { 335 | console.error(`-> 第 ${i + 1} 次尝试失败:`, error.message) 336 | 337 | if (i < maxRetries - 1) { 338 | console.log("-> 等待 10 秒后重试...") 339 | await driver.sleep(10000) 340 | } else { 341 | throw new Error("网站无法访问,请检查网络连接和代理设置") 342 | } 343 | } 344 | } 345 | } 346 | 347 | async function findElementInShadowRoots(driver, selector) { 348 | console.log("-> 在 Shadow DOM 中查找元素:", selector) 349 | const shadowHosts = await driver.findElements(By.css('*')) 350 | for (const host of shadowHosts) { 351 | try { 352 | const shadowRoot = await driver.executeScript('return arguments[0].shadowRoot', host) 353 | if (shadowRoot) { 354 | console.log("-> 找到 Shadow Root") 355 | const element = await driver.executeScript(` 356 | return arguments[0].shadowRoot.querySelector('${selector}') 357 | `, host) 358 | if (element) { 359 | console.log("-> 在 Shadow DOM 中找到元素") 360 | return element 361 | } 362 | } 363 | } catch (e) { 364 | // 忽略错误,继续查找 365 | } 366 | } 367 | return null 368 | } 369 | 370 | async function findElementInFrames(driver, selector) { 371 | console.log("-> 在 iframe 中查找元素:", selector) 372 | const frames = await driver.findElements(By.css('iframe')) 373 | for (const frame of frames) { 374 | try { 375 | console.log("-> 切换到 iframe") 376 | await driver.switchTo().frame(frame) 377 | const element = await driver.findElement(By.css(selector)) 378 | if (element) { 379 | console.log("-> 在 iframe 中找到元素") 380 | return element 381 | } 382 | } catch (e) { 383 | // 忽略错误,继续查找 384 | } finally { 385 | await driver.switchTo().defaultContent() 386 | } 387 | } 388 | return null 389 | } 390 | 391 | async function findElement(driver, selector) { 392 | console.log("-> 开始查找元素:", selector) 393 | 394 | // 1. 先在主文档中查找 395 | try { 396 | const element = await driver.findElement(By.css(selector)) 397 | if (element) { 398 | console.log("-> 在主文档中找到元素") 399 | return element 400 | } 401 | } catch (e) { 402 | console.log("-> 主文档中未找到元素") 403 | } 404 | 405 | // 2. 在 Shadow DOM 中查找 406 | const shadowElement = await findElementInShadowRoots(driver, selector) 407 | if (shadowElement) { 408 | return shadowElement 409 | } 410 | 411 | // 3. 在 iframe 中查找 412 | const frameElement = await findElementInFrames(driver, selector) 413 | if (frameElement) { 414 | return frameElement 415 | } 416 | 417 | throw new Error(`无法找到元素: ${selector}`) 418 | } 419 | 420 | // 添加状态检查函数 421 | async function checkSupportStatus(driver) { 422 | try { 423 | // 等待 Status 元素出现 424 | await driver.wait( 425 | until.elementLocated(By.xpath('//div[contains(text(), "Status")]')), 426 | 30000 427 | ); 428 | 429 | // 检查连接状态 430 | const supportStatus = await driver 431 | .findElement(By.css(".absolute.mt-3.right-0.z-10")) 432 | .getText(); 433 | 434 | console.log("-> 状态:", supportStatus); 435 | return supportStatus; 436 | } catch (error) { 437 | console.error('-> 状态检查出错:', error.message); 438 | return null; 439 | } 440 | } 441 | 442 | async function reloadExtension(driver) { 443 | try { 444 | // 重新加载扩展 445 | await driver.get(`chrome-extension://${extensionId}/popup.html`); 446 | await driver.navigate().refresh(); 447 | 448 | // 等待页面加载完成 449 | await driver.wait(async () => { 450 | const readyState = await driver.executeScript('return document.readyState'); 451 | return readyState === 'complete'; 452 | }, 10000); 453 | 454 | // 等待扩展初始化 455 | await driver.sleep(5000); 456 | 457 | return true; 458 | } catch (error) { 459 | console.error('-> 扩展重载失败:', error.message); 460 | return false; 461 | } 462 | } 463 | 464 | async function startStatusCheck(driver) { 465 | console.log('-> 开始定时状态检查 (每5分钟)...'); 466 | 467 | setInterval(async () => { 468 | try { 469 | let retryCount = 0; 470 | let status = null; 471 | 472 | while (retryCount < 10) { 473 | // 重新加载扩展 474 | console.log(`-> 第 ${retryCount + 1} 次尝试加载扩展...`); 475 | const reloadSuccess = await reloadExtension(driver); 476 | 477 | if (!reloadSuccess) { 478 | console.log('-> 扩展加载失败,重试...'); 479 | retryCount++; 480 | continue; 481 | } 482 | 483 | // 检查状态 484 | status = await checkSupportStatus(driver); 485 | 486 | if (status === null) { 487 | console.log('-> 状态检查失败,重试...'); 488 | retryCount++; 489 | continue; 490 | } 491 | 492 | if (status.includes('Good')) { 493 | console.log('-> 状态正常 (Good)'); 494 | break; 495 | } else if (status.includes('Disconnected')) { 496 | console.log('-> 状态异常 (Disconnected),重试...'); 497 | retryCount++; 498 | await driver.sleep(5000); // 等待5秒后重试 499 | continue; 500 | } 501 | } 502 | 503 | // 如果10次重试后仍未成功,重启程序 504 | if (retryCount >= 10) { 505 | console.log('-> 达到最大重试次数,准备重启程序...'); 506 | await driver.quit(); 507 | process.exit(1); // 退出程序,PM2 会自动重启 508 | } 509 | 510 | } catch (error) { 511 | console.error('-> 状态检查出错:', error.message); 512 | } 513 | }, 300000); // 每5分钟运行一次 514 | } 515 | 516 | // 主程序 517 | (async function main() { 518 | let driver; 519 | try { 520 | // 先下载扩展 521 | await downloadExtension(extensionId) 522 | 523 | // 启动浏览器 524 | driver = await startBrowser() 525 | 526 | console.log("-> 正在访问 Gradient Network...") 527 | await waitForSiteAvailable(driver) 528 | console.log("-> 页面加载成功!") 529 | 530 | // 等待页面完全加载 531 | await driver.wait(async () => { 532 | const readyState = await driver.executeScript('return document.readyState') 533 | console.log("-> 页面加载状态:", readyState) 534 | return readyState === 'complete' 535 | }, 30000) 536 | 537 | // 等待一下让 JavaScript 完全执行 538 | await driver.sleep(5000) 539 | console.log("-> 等待 JavaScript 执行完成") 540 | 541 | // 保存页面源码和结构信息 542 | if (ALLOW_DEBUG) { 543 | console.log("-> 正在分析页面结构...") 544 | const pageSource = await driver.getPageSource() 545 | fs.writeFileSync('page_source.html', pageSource) 546 | console.log("-> 已保存页面源码") 547 | 548 | const title = await driver.getTitle() 549 | console.log("-> 页面标题:", title) 550 | 551 | const url = await driver.getCurrentUrl() 552 | console.log("-> 当前URL:", url) 553 | 554 | // 获取所有 h1 标签的文本 555 | const h1Elements = await driver.findElements(By.css('h1')) 556 | console.log("-> H1标签数量:", h1Elements.length) 557 | for (const h1 of h1Elements) { 558 | const text = await h1.getText() 559 | console.log("-> H1文本:", text) 560 | } 561 | 562 | // 获取所有输入框 563 | const inputs = await driver.findElements(By.css('input')) 564 | console.log("-> 输入框数量:", inputs.length) 565 | for (const input of inputs) { 566 | const type = await input.getAttribute('type') 567 | const placeholder = await input.getAttribute('placeholder') 568 | console.log("-> 输入框:", { type, placeholder }) 569 | } 570 | } 571 | 572 | console.log("-> 等待登录表单加载...") 573 | 574 | // 尝试多个可能的选择器 575 | const emailSelectors = [ 576 | 'input[placeholder="Enter Email"]', 577 | 'input[type="email"]', 578 | 'input[type="text"]' 579 | ] 580 | 581 | console.log("-> 尝试查找邮箱输入框...") 582 | let emailInput = null 583 | for (const selector of emailSelectors) { 584 | try { 585 | console.log("-> 尝试选择器:", selector) 586 | emailInput = await findElement(driver, selector) 587 | if (emailInput) { 588 | const isDisplayed = await driver.executeScript('return arguments[0].offsetParent !== null', emailInput) 589 | const isEnabled = await emailInput.isEnabled() 590 | console.log("-> 元素状态:", { isDisplayed, isEnabled }) 591 | 592 | if (isDisplayed && isEnabled) { 593 | console.log("-> 找到可用的邮箱输入框:", selector) 594 | break 595 | } 596 | } 597 | } catch (e) { 598 | console.log("-> 未找到或无法使用选择器:", selector, e.message) 599 | } 600 | } 601 | 602 | if (!emailInput) { 603 | console.error("-> 无法找到可用的邮箱输入框!") 604 | if (ALLOW_DEBUG) { 605 | // 尝试获取页面结构 606 | const bodyText = await driver.findElement(By.css('body')).getText() 607 | console.log("-> 页面文本内容:", bodyText) 608 | await generateErrorReport(driver) 609 | } 610 | throw new Error("无法找到可用的登录表单元素") 611 | } 612 | 613 | console.log("-> 等待密码输入框...") 614 | let passwordInput = null 615 | try { 616 | console.log("-> 尝试查找密码输入框...") 617 | passwordInput = await findElement(driver, 'input[placeholder="Enter Password"]') 618 | if (passwordInput) { 619 | const isDisplayed = await driver.executeScript('return arguments[0].offsetParent !== null', passwordInput) 620 | const isEnabled = await passwordInput.isEnabled() 621 | console.log("-> 密码输入框状态:", { isDisplayed, isEnabled }) 622 | 623 | if (!isDisplayed || !isEnabled) { 624 | console.log("-> 等待密码输入框变为可用...") 625 | await driver.sleep(5000) 626 | // 再次检查状态 627 | const isDisplayedAfterWait = await driver.executeScript('return arguments[0].offsetParent !== null', passwordInput) 628 | const isEnabledAfterWait = await passwordInput.isEnabled() 629 | console.log("-> 等待后的密码输入框状态:", { isDisplayedAfterWait, isEnabledAfterWait }) 630 | 631 | if (!isDisplayedAfterWait || !isEnabledAfterWait) { 632 | throw new Error("密码输入框不可用") 633 | } 634 | } 635 | } else { 636 | throw new Error("未找到密码输入框") 637 | } 638 | } catch (error) { 639 | console.error("-> 无法找到或使用密码输入框:", error.message) 640 | if (ALLOW_DEBUG) { 641 | await generateErrorReport(driver) 642 | } 643 | throw error 644 | } 645 | 646 | console.log("-> 等待登录按钮...") 647 | let loginButton = null 648 | try { 649 | console.log("-> 尝试查找登录按钮...") 650 | const buttons = await driver.findElements(By.css('button')) 651 | console.log("-> 找到按钮数量:", buttons.length) 652 | 653 | for (const button of buttons) { 654 | // 获取按钮的完整 HTML 655 | const buttonHtml = await button.getAttribute('outerHTML') 656 | console.log("-> 按钮 HTML:", buttonHtml) 657 | 658 | // 获取按钮内的所有文本,包括子元素 659 | const buttonText = await driver.executeScript(` 660 | return arguments[0].textContent || arguments[0].innerText || ''; 661 | `, button) 662 | 663 | // 检查按钮的类名和其他属性 664 | const className = await button.getAttribute('class') 665 | const type = await button.getAttribute('type') 666 | const value = await button.getAttribute('value') 667 | 668 | const isDisplayed = await driver.executeScript('return arguments[0].offsetParent !== null', button) 669 | const isEnabled = await button.isEnabled() 670 | console.log("-> 按钮信息:", { 671 | text: buttonText, 672 | className, 673 | type, 674 | value, 675 | isDisplayed, 676 | isEnabled 677 | }) 678 | 679 | // 检查按钮文本、类名或其他属性来识别登录按钮 680 | if ((buttonText.toLowerCase().includes('log in') || 681 | buttonText.toLowerCase().includes('login') || 682 | className?.toLowerCase().includes('login') || 683 | type === 'submit') && 684 | isDisplayed && 685 | isEnabled) { 686 | loginButton = button 687 | console.log("-> 找到可用的登录按钮") 688 | break 689 | } 690 | } 691 | 692 | // 如果没有找到登录按钮,尝试使用 XPath 693 | if (!loginButton) { 694 | console.log("-> 尝试使用 XPath 查找登录按钮...") 695 | const xpaths = [ 696 | "//button[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'log in')]", 697 | "//button[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'login')]", 698 | "//button[@type='submit']", 699 | "//button[contains(@class, 'login')]" 700 | ] 701 | 702 | for (const xpath of xpaths) { 703 | try { 704 | const button = await driver.findElement(By.xpath(xpath)) 705 | const isDisplayed = await driver.executeScript('return arguments[0].offsetParent !== null', button) 706 | const isEnabled = await button.isEnabled() 707 | 708 | if (isDisplayed && isEnabled) { 709 | loginButton = button 710 | console.log("-> 使用 XPath 找到可用的登录按钮:", xpath) 711 | break 712 | } 713 | } catch (e) { 714 | console.log("-> XPath 未找到按钮:", xpath) 715 | } 716 | } 717 | } 718 | 719 | if (!loginButton) { 720 | throw new Error("未找到可用的登录按钮") 721 | } 722 | } catch (error) { 723 | console.error("-> 无法找到或使用登录按钮:", error.message) 724 | if (ALLOW_DEBUG) { 725 | await generateErrorReport(driver) 726 | } 727 | throw error 728 | } 729 | 730 | console.log("-> 输入邮箱...") 731 | await emailInput.clear() 732 | await emailInput.sendKeys(USER) 733 | console.log("-> 输入密码...") 734 | await passwordInput.clear() 735 | await passwordInput.sendKeys(PASSWORD) 736 | console.log("-> 点击登录按钮...") 737 | await loginButton.click() 738 | 739 | console.log("-> 等待登录完成...") 740 | 741 | // 保存页面源码以供调试 742 | if (ALLOW_DEBUG) { 743 | const pageSource = await driver.getPageSource() 744 | fs.writeFileSync('login_page.html', pageSource) 745 | console.log("-> 已保存登录页面源码") 746 | } 747 | 748 | // 等待登录成功 749 | try { 750 | await driver.wait(until.elementLocated(By.css('a[href="/dashboard/setting"]')), 30000) 751 | console.log("-> 登录成功!") 752 | } catch (error) { 753 | console.error("-> 登录失败!可能的原因:") 754 | console.error(" 1. 账号或密码错误") 755 | console.error(" 2. 网络连接问题") 756 | console.error(" 3. 页面加载超时") 757 | 758 | // 获取可能的错误信息 759 | try { 760 | const errorMessages = await driver.findElements(By.css('.text-red-500, .text-danger, .error-message')) 761 | for (const element of errorMessages) { 762 | const text = await element.getText() 763 | console.error("-> 错误信息:", text) 764 | } 765 | } catch (e) { 766 | console.error("-> 无法获取错误信息") 767 | } 768 | 769 | throw new Error("登录失败,请检查账号密码或网络连接") 770 | } 771 | 772 | console.log("-> 正在打开扩展...") 773 | 774 | // 截图登录状态 775 | await takeScreenshot(driver, "logined.png") 776 | 777 | await driver.get(`chrome-extension://${extensionId}/popup.html`) 778 | 779 | console.log("-> 扩展已打开!") 780 | 781 | // 等待 Status 元素出现 782 | await driver.wait( 783 | until.elementLocated(By.xpath('//div[contains(text(), "Status")]')), 784 | 30000 785 | ) 786 | 787 | console.log("-> 扩展加载完成!") 788 | 789 | // 处理 "I got it" 按钮 790 | try { 791 | const gotItButton = await driver.findElement( 792 | By.xpath('//button[contains(text(), "I got it")]') 793 | ) 794 | await gotItButton.click() 795 | console.log('-> "I got it" 按钮已点击!') 796 | } catch (error) { 797 | console.log('-> 未找到 "I got it" 按钮(跳过)') 798 | } 799 | 800 | // 检查区域可用性 801 | try { 802 | await driver.findElement( 803 | By.xpath('//*[contains(text(), "Sorry, Gradient is not yet available in your region.")]') 804 | ) 805 | console.log("-> 错误:Gradient 在当前区域不可用") 806 | await driver.quit() 807 | process.exit(1) 808 | } catch (error) { 809 | console.log("-> Gradient 在当前区域可用") 810 | } 811 | 812 | // 检查连接状态 813 | const supportStatus = await driver 814 | .findElement(By.css(".absolute.mt-3.right-0.z-10")) 815 | .getText() 816 | 817 | console.log("-> 状态:", supportStatus) 818 | 819 | if (supportStatus.includes("Disconnected")) { 820 | console.log("-> 连接失败!请检查以下内容:") 821 | console.log(` 822 | - 确保代理正常工作:curl -vv -x ${PROXY} https://myip.ipip.net 823 | - 确保 Docker 镜像是最新版本 824 | - 官方服务可能不稳定,这是正常现象,程序会自动重启 825 | - 如果使用免费代理,可能被官方服务封禁,请尝试其他静态住宅代理 826 | `) 827 | await generateErrorReport(driver) 828 | await driver.quit() 829 | process.exit(1) 830 | } 831 | 832 | console.log("-> 连接成功!开始运行...") 833 | await takeScreenshot(driver, "connected.png") 834 | 835 | console.log({ 836 | support_status: supportStatus, 837 | }) 838 | 839 | console.log("-> 程序已启动!") 840 | 841 | // 启动状态检查 842 | await startStatusCheck(driver); 843 | 844 | // 定期检查状态 845 | setInterval(async () => { 846 | try { 847 | const title = await driver.getTitle(); 848 | console.log(`-> [${USER}] 正在运行...`, title); 849 | if (PROXY) { 850 | console.log(`-> [${USER}] 使用代理 ${PROXY} 运行中...`); 851 | } 852 | } catch (error) { 853 | console.error("-> 状态检查遇到错误:", error.message); 854 | console.log("-> 将在下次检查时重试..."); 855 | } 856 | }, 60000); // 1分钟检查一次 857 | 858 | } catch (error) { 859 | console.error("-> 发生错误:", error.message); 860 | if (ALLOW_DEBUG) { 861 | console.log("-> 正在保存错误截图..."); 862 | try { 863 | if (driver) { 864 | const screenshot = await driver.takeScreenshot(); 865 | const filename = `error_${Date.now()}.png`; 866 | fs.writeFileSync(filename, screenshot, 'base64'); 867 | console.log(`-> 错误截图已保存: ${filename}`); 868 | } 869 | } catch (screenshotError) { 870 | console.error("-> 无法保存错误截图:", screenshotError.message); 871 | } 872 | } 873 | // 不要立即退出,而是重启浏览器 874 | console.log("-> 尝试重启浏览器..."); 875 | try { 876 | if (driver) { 877 | await driver.quit(); 878 | } 879 | // 递归调用main函数重新开始 880 | return main(); 881 | } catch (restartError) { 882 | console.error("-> 重启失败:", restartError.message); 883 | process.exit(1); 884 | } 885 | } 886 | })(); 887 | -------------------------------------------------------------------------------- /assets/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mumumusf/gradient-network-bot/fe471af133f5b48f3227663f43257e3b193017f2/assets/banner.jpg -------------------------------------------------------------------------------- /dist/github-contribution-grid-snake-dark.svg: -------------------------------------------------------------------------------- 1 | Generated with https://github.com/Platane/snk -------------------------------------------------------------------------------- /dist/github-contribution-grid-snake.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mumumusf/gradient-network-bot/fe471af133f5b48f3227663f43257e3b193017f2/dist/github-contribution-grid-snake.gif -------------------------------------------------------------------------------- /dist/github-contribution-grid-snake.svg: -------------------------------------------------------------------------------- 1 | Generated with https://github.com/Platane/snk -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 验证 Chrome 是否存在 4 | if [ ! -f "/usr/bin/google-chrome" ]; then 5 | echo "Chrome not found!" 6 | exit 1 7 | fi 8 | 9 | # 显示 Chrome 版本 10 | google-chrome --version 11 | 12 | # 启动应用 13 | pm2 start app.js --name gradient-bot-no-proxy 14 | pm2 logs 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "chromedriver": "^131.0.4", 4 | "console-stamp": "^3.1.2", 5 | "crypto": "^1.0.1", 6 | "dotenv": "^16.4.5", 7 | "form-data": "^4.0.0", 8 | "proxy-chain": "^2.5.3", 9 | "request": "^2.88.2", 10 | "selenium-webdriver": "^4.24.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /start.js: -------------------------------------------------------------------------------- 1 | // 1. read proxies from file 2 | const fs = require('fs') 3 | const path = require('path') 4 | require('console-stamp')(console, { 5 | format: ':date(yyyy/mm/dd HH:MM:ss.l)' 6 | }) 7 | 8 | let proxies = [] 9 | 10 | try { 11 | proxies = fs.readFileSync(path.resolve(__dirname, 'proxies.txt'), 'utf-8').split('\n').filter(Boolean) 12 | } catch (error) { 13 | console.log('-> No proxies.txt found, or error reading file, will start app without proxy...') 14 | } 15 | 16 | // 2. start pm2 with PROXY env 17 | const { execSync } = require('child_process') 18 | const USER = process.env.APP_USER || '' 19 | const PASSWORD = process.env.APP_PASS || '' 20 | 21 | if (!USER || !PASSWORD) { 22 | console.error("Please set APP_USER and APP_PASS env variables") 23 | process.exit() 24 | } 25 | 26 | if (proxies.length === 0) { 27 | console.error("No proxies found in proxies.txt, will start app without proxy...") 28 | execSync(`APP_USER='${USER}' APP_PASS='${PASSWORD}' pm2 start app.js --name gradient-bot-no-proxy -l gradient-bot-no-proxy.log`) 29 | console.log('-> √ Started gradient-bot-no-proxy') 30 | } else { 31 | console.log(`-> Found ${proxies.length} proxies in proxies.txt`) 32 | let index = 0 33 | for (const proxy of proxies) { 34 | const name = `gradient-${index++}` 35 | execSync(`PROXY=${proxy} APP_USER='${USER}' APP_PASS='${PASSWORD}' pm2 start app.js --name ${name} -l ${name}.log`) 36 | console.log(`-> Started ${name} with proxy ${proxy}`) 37 | } 38 | 39 | // 3. save proxies to file 40 | console.log('-> √ All proxies started!') 41 | } 42 | 43 | // 4. pm2 status 44 | execSync('pm2 status') 45 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const { Builder, By, until, Capabilities } = require("selenium-webdriver") 2 | const chrome = require("selenium-webdriver/chrome") 3 | const url = require("url") 4 | const fs = require("fs") 5 | const crypto = require("crypto") 6 | const request = require("request") 7 | const path = require("path") 8 | const FormData = require("form-data") 9 | const proxy = require("selenium-webdriver/proxy") 10 | require("dotenv").config() 11 | const proxyChain = require('proxy-chain') 12 | 13 | const USER_AGENT = 14 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36" 15 | 16 | const USER = process.env.APP_USER || "" 17 | const PASSWORD = process.env.APP_PASS || "" 18 | const PROXY_HTTP_PORT = process.env.PROXY_HTTP_PORT || 2000 19 | const PROXY_SOCKS_PORT = process.env.PROXY_SOCKS_PORT || 2000 20 | const ALLOW_DEBUG = process.env.ALLOW_DEBUG === "True" 21 | const EXTENSION_FILENAME = "app.crx" 22 | const PROXY = process.env.PROXY || undefined 23 | 24 | console.log("-> Starting...") 25 | console.log("-> User:", USER) 26 | console.log("-> Pass:", PASSWORD) 27 | console.log("-> Proxy:", PROXY) 28 | console.log("-> Proxy HTTP Port:", PROXY_HTTP_PORT) 29 | console.log("-> Proxy SOCKS Port:", PROXY_SOCKS_PORT) 30 | console.log("-> Debug:", ALLOW_DEBUG) 31 | 32 | async function takeScreenshot(driver, filename) { 33 | const data = await driver.takeScreenshot() 34 | fs.writeFileSync(filename, Buffer.from(data, "base64")) 35 | } 36 | 37 | // proxyUrl: http://username:password@host:port 38 | // proxyUrl: socks5://username:password@host:port 39 | function parseProxyUrl(proxyUrl) { 40 | try { 41 | // if without scheme, add http:// 42 | if (!/^https?:\/\//.test(proxyUrl)) { 43 | proxyUrl = `http://${proxyUrl}` 44 | } 45 | 46 | const parsedUrl = url.parse(proxyUrl) 47 | 48 | return { 49 | server: { 50 | http: `http://${parsedUrl.hostname}:${PROXY_HTTP_PORT}`, 51 | https: `http://${parsedUrl.hostname}:${PROXY_HTTP_PORT}`, 52 | socks: `socks5://${parsedUrl.hostname}:${PROXY_SOCKS_PORT}`, 53 | }, 54 | host: parsedUrl.hostname, 55 | port: parsedUrl.port, 56 | auth: parsedUrl.auth, 57 | } 58 | } catch (error) { 59 | console.error(`-> Error proxy URL (${proxyUrl}):`, error) 60 | return proxyUrl 61 | } 62 | } 63 | 64 | async function getProxyIpInfo(proxyUrl) { 65 | const url = "https://httpbin.org/ip" 66 | 67 | console.log("-> Getting proxy IP info:", proxyUrl) 68 | 69 | const options = new chrome.Options() 70 | 71 | const newProxyUrl = await proxyChain.anonymizeProxy('http://storm-overtrue2_ip-217.180.20.38:1234578@eu.stormip.cn:2000') 72 | 73 | options.addArguments(`user-agent=${USER_AGENT}`) 74 | options.addArguments("--headless") 75 | options.addArguments("--disable-dev-shm-usage") 76 | options.addArguments("--disable-gpu") 77 | options.addArguments("--no-sandbox") 78 | // options.addArguments(`--proxy-server=${newProxyUrl}`) 79 | // options.addArguments('--proxy-auth=storm-overtrue2_ip-217.180.20.38:123457') 80 | 81 | options.setProxy( 82 | proxy.manual({ 83 | http: newProxyUrl, 84 | https: newProxyUrl, 85 | }) 86 | ) 87 | 88 | const driver = await new Builder() 89 | .forBrowser("chrome") 90 | .setChromeOptions(options) 91 | .build() 92 | 93 | try { 94 | console.log("-> Proxy IP info started!") 95 | 96 | await driver.get(url) 97 | await driver.wait(until.elementLocated(By.css("body")), 10000) 98 | const pageText = await driver.findElement(By.css("body")).getText() 99 | console.log("-> Proxy IP info:", pageText) 100 | } catch (error) { 101 | console.error("-> Error getting proxy IP info:", error) 102 | } finally { 103 | await driver.quit() 104 | console.log("-> Proxy IP info done!") 105 | } 106 | } 107 | 108 | const parsed = parseProxyUrl('http://storm-overtrue2_ip-217.180.20.38:1234578@eu.stormip.cn:2000') 109 | getProxyIpInfo(parsed.server.http) 110 | --------------------------------------------------------------------------------