├── .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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------