Skip to content

next 工程 启动定时任务 #31

@leno23

Description

@leno23

D:\project\chatape_web-knowledge\package.json

{
  "dependencies": {
    "puppeteer": "^23.0.0"
  }
}

D:\project\chatape_web-knowledge\vercel.json

{
  "crons": [
    {
      "path": "/api/faucet/schedule",
      "schedule": "0 23 * * *"
    }
  ]
}

D:\project\chatape_web-knowledge.env.faucet.example

# Faucet API Configuration

# Your application URL (required for scheduled tasks)
NEXT_PUBLIC_APP_URL=https://your-domain.com

# Cron secret for protecting the schedule endpoint (optional but recommended)
# Generate a random string for this value
CRON_SECRET=your-random-secret-key-here

# Example:
# NEXT_PUBLIC_APP_URL=https://chatape.vercel.app
# CRON_SECRET=abc123xyz789secretkey

D:\project\chatape_web-knowledge.github\workflows\faucet-cron.yml

name: Daily Faucet Task

on:
  schedule:
    # Runs at 7:00 AM Beijing time (23:00 UTC previous day)
    - cron: '0 23 * * *'
  workflow_dispatch: # Allows manual trigger

jobs:
  run-faucet:
    runs-on: ubuntu-latest
    steps:
      - name: Trigger Faucet Task
        run: |
          curl -X GET "${{ secrets.APP_URL }}/api/faucet/schedule" \
            -H "Authorization: Bearer ${{ secrets.CRON_SECRET }}"

D:\project\chatape_web-knowledge\app\api\faucet\README.md

Faucet API - 自动领取 Sepolia ETH

这是一个自动化任务,每天早上7点访问 Google Cloud Faucet 页面并自动领取 Sepolia ETH。

功能特点

  • ✅ 每天早上7点自动执行
  • ✅ 自动填写钱包地址:0xf0aC9747345c23B6ba451d9103F8C2785800998D
  • ✅ 自动点击领取按钮
  • ✅ 等待交易完成确认
  • ✅ 失败自动重试(最多50次,每次间隔30秒)

安装依赖

npm install puppeteer
#
yarn add puppeteer
#
pnpm add puppeteer

环境变量配置

.env.local 文件中添加以下配置:

# 应用URL(用于定时任务回调)
NEXT_PUBLIC_APP_URL=https://your-domain.com

# Cron任务密钥(可选,用于保护定时任务端点)
CRON_SECRET=your-secret-key-here

API 端点

1. POST /api/faucet

手动触发领取任务

请求示例:

curl -X POST http://localhost:3000/api/faucet

响应示例:

{
  "success": true,
  "message": "Faucet task completed successfully",
  "attempts": 1,
  "timestamp": "2024-01-01T07:00:00.000Z"
}

2. GET /api/faucet

查看API状态

请求示例:

curl http://localhost:3000/api/faucet

3. GET /api/faucet/schedule

定时任务端点(由 Cron 服务调用)

请求示例:

curl -X GET http://localhost:3000/api/faucet/schedule \
  -H "Authorization: Bearer your-secret-key"

部署方式

方式一:Vercel Cron(推荐)

  1. 部署到 Vercel
  2. vercel.json 已配置好定时任务(每天23:00 UTC = 北京时间早上7:00)
  3. 在 Vercel 项目设置中添加环境变量:
    • NEXT_PUBLIC_APP_URL
    • CRON_SECRET(可选)

方式二:GitHub Actions

  1. 在 GitHub 仓库的 Settings > Secrets 中添加:
    • APP_URL: 你的应用URL
    • CRON_SECRET: 定时任务密钥
  2. GitHub Actions 会每天自动触发(.github/workflows/faucet-cron.yml

方式三:外部 Cron 服务

使用 cron-job.org、EasyCron 等服务,配置每天早上7点调用:

GET https://your-domain.com/api/faucet/schedule
Header: Authorization: Bearer your-secret-key

本地测试

# 启动开发服务器
npm run dev

# 手动触发任务
curl -X POST http://localhost:3000/api/faucet

注意事项

  1. Puppeteer 依赖:确保安装了 puppeteer 及其依赖
  2. 执行时间:任务可能需要几分钟完成,请确保服务器超时设置足够长
  3. 重试机制:如果失败会自动重试50次,每次间隔30秒
  4. 日志监控:建议监控日志以确保任务正常执行

故障排查

Puppeteer 安装问题

如果在服务器上运行遇到问题,可能需要安装额外的依赖:

Ubuntu/Debian:

apt-get install -y \
  chromium-browser \
  fonts-liberation \
  libasound2 \
  libatk-bridge2.0-0 \
  libatk1.0-0 \
  libcups2 \
  libdbus-1-3 \
  libdrm2 \
  libgbm1 \
  libgtk-3-0 \
  libnspr4 \
  libnss3 \
  libxcomposite1 \
  libxdamage1 \
  libxrandr2 \
  xdg-utils

Vercel 部署注意

Vercel 的 Serverless Functions 有执行时间限制:

  • Hobby 计划:10秒
  • Pro 计划:60秒
  • Enterprise 计划:900秒

建议使用 Pro 或 Enterprise 计划,或考虑使用其他支持长时间运行的平台。

许可证

MIT

D:\project\chatape_web-knowledge\app\api\faucet\faucet-task.ts

import puppeteer, { Browser, Page } from 'puppeteer';

const TARGET_URL = 'https://cloud.google.com/application/web3/faucet/ethereum/sepolia';
const WALLET_ADDRESS = '0xf0aC9747345c23B6ba451d9103F8C2785800998D';
const MAX_RETRIES = 50;
const RETRY_DELAY = 30000; // 30 seconds

interface TaskResult {
  success: boolean;
  attempts: number;
  error?: string;
}

export async function runFaucetTask(): Promise<TaskResult> {
  let browser: Browser | null = null;
  let attempts = 0;

  try {
    console.log('Starting faucet task...');
    
    browser = await puppeteer.launch({
      headless: true,
      args: [
        '--no-sandbox',
        '--disable-setuid-sandbox',
        '--disable-dev-shm-usage',
        '--disable-accelerated-2d-canvas',
        '--disable-gpu'
      ]
    });

    const page = await browser.newPage();
    await page.setViewport({ width: 1920, height: 1080 });

    // Navigate to the faucet page
    console.log('Navigating to faucet page...');
    await page.goto(TARGET_URL, { 
      waitUntil: 'networkidle2',
      timeout: 60000 
    });

    // Wait for page to load
    await page.waitForTimeout(3000);

    for (attempts = 1; attempts <= MAX_RETRIES; attempts++) {
      try {
        console.log(`Attempt ${attempts}/${MAX_RETRIES}`);

        // Find and fill the input field
        console.log('Looking for input field...');
        const inputSelector = 'input[type="text"], input[placeholder*="address"], input[name*="address"]';
        await page.waitForSelector(inputSelector, { timeout: 10000 });
        
        await page.evaluate((selector: string, address: string) => {
          const input = document.querySelector(selector) as HTMLInputElement;
          if (input) {
            input.value = address;
            input.dispatchEvent(new Event('input', { bubbles: true }));
            input.dispatchEvent(new Event('change', { bubbles: true }));
          }
        }, inputSelector, WALLET_ADDRESS);

        console.log('Wallet address filled');
        await page.waitForTimeout(1000);

        // Find and click the "Get 0.05 Sepolia ETH" button
        console.log('Looking for submit button...');
        const buttonSelectors = [
          'button:contains("Get 0.05 Sepolia ETH")',
          'button[type="submit"]',
          'button.submit',
          'button'
        ];

        let buttonClicked = false;
        for (const selector of buttonSelectors) {
          try {
            const buttons = await page.$$('button');
            for (const button of buttons) {
              const text = await page.evaluate((el: Element) => el.textContent, button);
              if (text && text.includes('Get 0.05 Sepolia ETH')) {
                await button.click();
                buttonClicked = true;
                console.log('Submit button clicked');
                break;
              }
            }
            if (buttonClicked) break;
          } catch (e) {
            continue;
          }
        }

        if (!buttonClicked) {
          throw new Error('Could not find submit button');
        }

        // Wait for the success message
        console.log('Waiting for transaction completion...');
        const maxWaitTime = 120000; // 2 minutes
        const startTime = Date.now();

        while (Date.now() - startTime < maxWaitTime) {
          try {
            const successElement = await page.$('.drip-status-card-banner-body');
            
            if (successElement) {
              const text = await page.evaluate((el: Element) => el.textContent, successElement);
              
              if (text && text.includes('Transaction complete! Check your wallet address')) {
                console.log('✅ Transaction completed successfully!');
                return {
                  success: true,
                  attempts
                };
              }
            }
          } catch (e) {
            // Element not found yet, continue waiting
          }

          await page.waitForTimeout(2000); // Check every 2 seconds
        }

        // If we reach here, the transaction didn't complete in time
        console.log(`Attempt ${attempts} timed out, retrying...`);
        
        if (attempts < MAX_RETRIES) {
          console.log(`Waiting ${RETRY_DELAY / 1000} seconds before retry...`);
          await page.waitForTimeout(RETRY_DELAY);
          
          // Reload the page for next attempt
          await page.reload({ waitUntil: 'networkidle2' });
          await page.waitForTimeout(3000);
        }

      } catch (error: any) {
        console.error(`Error in attempt ${attempts}:`, error.message);
        
        if (attempts < MAX_RETRIES) {
          console.log(`Waiting ${RETRY_DELAY / 1000} seconds before retry...`);
          await page.waitForTimeout(RETRY_DELAY);
          
          // Reload the page for next attempt
          try {
            await page.reload({ waitUntil: 'networkidle2', timeout: 30000 });
            await page.waitForTimeout(3000);
          } catch (reloadError) {
            console.error('Error reloading page:', reloadError);
            // Try to navigate again
            await page.goto(TARGET_URL, { waitUntil: 'networkidle2', timeout: 60000 });
            await page.waitForTimeout(3000);
          }
        }
      }
    }

    return {
      success: false,
      attempts,
      error: 'Maximum retries reached without success'
    };

  } catch (error: any) {
    console.error('Fatal error in faucet task:', error);
    return {
      success: false,
      attempts,
      error: error.message
    };
  } finally {
    if (browser) {
      await browser.close();
      console.log('Browser closed');
    }
  }
}

D:\project\chatape_web-knowledge\app\api\faucet\schedule\route.ts

import { NextRequest, NextResponse } from 'next/server';

export const dynamic = 'force-dynamic';

// This endpoint can be called by a cron job service (like Vercel Cron, GitHub Actions, or external cron services)
export async function GET(request: NextRequest) {
  try {
    // Verify the request is from an authorized source (optional but recommended)
    const authHeader = request.headers.get('authorization');
    const cronSecret = process.env.CRON_SECRET;
    
    if (cronSecret && authHeader !== `Bearer ${cronSecret}`) {
      return NextResponse.json({
        success: false,
        message: 'Unauthorized'
      }, { status: 401 });
    }

    // Check if it's the right time (7:00 AM in your timezone)
    const now = new Date();
    const hour = now.getHours();
    
    console.log(`Cron job triggered at ${now.toISOString()}`);

    // Trigger the faucet task
    const response = await fetch(`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/faucet`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      }
    });

    const result = await response.json();

    return NextResponse.json({
      success: true,
      message: 'Scheduled task executed',
      timestamp: now.toISOString(),
      taskResult: result
    });

  } catch (error: any) {
    console.error('Schedule error:', error);
    return NextResponse.json({
      success: false,
      message: 'Schedule execution failed',
      error: error.message
    }, { status: 500 });
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions