在huggingface上保活serv00及CT8(通过CF worker反代telegram同步发送消息)

前在github上部署的定时脚本建议撤掉,有封号风险。
在佬友 @Coker 的提点下,在huggingface成功加入telegram通知账号登录状态
废话不多说,先上效果图
hugging的logs:

访问huggingface网页:

telegram中:

开始前准备:

必须

1.huggingface账号

2.serv00账号
https://www.serv00.com

非必须

这三个是推送登录的消息所必需的,没有也能用脚本
1.cloudflare账号

2.绑定在cloudflare的域名
3.telegram账号

huggingface脚本

创建空间时,名字建议取怪一点。

Dockerfile文件

FROM node:20-slim

WORKDIR /app

RUN apt-get update && apt-get install -y \
    wget \
    gnupg \
    ca-certificates \
    libgconf-2-4 \
    libxss1 \
    libappindicator1 \
    libasound2 \
    libatk1.0-0 \
    libc6 \
    libcairo2 \
    libcups2 \
    libdbus-1-3 \
    libexpat1 \
    libfontconfig1 \
    libgcc1 \
    libgdk-pixbuf2.0-0 \
    libglib2.0-0 \
    libgtk-3-0 \
    libnspr4 \
    libpango-1.0-0 \
    libpangocairo-1.0-0 \
    libstdc++6 \
    libx11-6 \
    libx11-xcb1 \
    libxcb1 \
    libxcomposite1 \
    libxcursor1 \
    libxdamage1 \
    libxext6 \
    libxfixes3 \
    libxi6 \
    libxrandr2 \
    libxrender1 \
    libxss1 \
    libxtst6 \
    lsb-release \
    xdg-utils \
    && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/google-chrome.gpg \
    && echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome.gpg] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list \
    && apt-get update \
    && apt-get install -y google-chrome-stable \
    && rm -rf /var/lib/apt/lists/*

# 创建 logs 目录并更改其权限
RUN mkdir -p /app/logs && chown -R node:node /app

# 切换到非 root 用户
USER node

COPY --chown=node:node package*.json ./

RUN npm install

COPY --chown=node:node . .

EXPOSE 8080

CMD ["node", "server.js"]

login.js文件

const puppeteer = require('puppeteer');
const axios = require('axios');

// Telegram 配置
let telegramConfig;
try {
  telegramConfig = JSON.parse(process.env.TELEGRAM_JSON || '{}');
} catch (error) {
  console.error('Error parsing TELEGRAM_JSON:', error);
}

function formatToISO(date) {
  return date.toISOString().replace('T', ' ').replace('Z', '').replace(/\.\d{3}Z/, '');
}

async function delayTime(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function sendTelegramMessage(message) {
  if (!telegramConfig || !telegramConfig.cloudflareWorkerUrl || !telegramConfig.telegramBotToken || !telegramConfig.telegramBotUserId || !telegramConfig.customAuthKey) {
    console.log('Telegram configuration not set or incomplete, skipping notification');
    return;
  }

  console.log('Attempting to send Telegram message...');
  const url = `${telegramConfig.cloudflareWorkerUrl}/${telegramConfig.telegramBotToken}/sendMessage`;
  try {
    const response = await axios.post(url, {
      chat_id: telegramConfig.telegramBotUserId,
      text: message
    }, {
      headers: {
        'X-Custom-Auth': telegramConfig.customAuthKey
      }
    });
    console.log('Telegram notification sent successfully');
  } catch (error) {
    console.error('Error sending Telegram notification:', error.response ? error.response.data : error.message);
  }
}

async function loginAccount(account, browser) {
  const { username, password, panelnum, type } = account;
  const page = await browser.newPage();

  let url = type === 'ct8' 
    ? 'https://panel.ct8.pl/login/?next=/' 
    : `https://panel${panelnum}.serv00.com/login/?next=/`;

  try {
    // 设置自定义 headers 来绕过 Cloudflare
    await page.setExtraHTTPHeaders({
      'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
      'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
      'Accept-Language': 'en-US,en;q=0.5',
      'Accept-Encoding': 'gzip, deflate, br',
      'Connection': 'keep-alive',
      'Upgrade-Insecure-Requests': '1',
      'Sec-Fetch-Dest': 'document',
      'Sec-Fetch-Mode': 'navigate',
      'Sec-Fetch-Site': 'none',
      'Sec-Fetch-User': '?1',
      'Cache-Control': 'max-age=0',
    });

    await page.goto(url, { waitUntil: 'networkidle0' });

    const usernameInput = await page.$('#id_username');
    if (usernameInput) {
      await usernameInput.click({ clickCount: 3 });
      await usernameInput.press('Backspace');
    }

    await page.type('#id_username', username);
    await page.type('#id_password', password);

    const loginButton = await page.$('#submit');
    if (loginButton) {
      await loginButton.click();
    } else {
      throw new Error('无法找到登录按钮');
    }

    await page.waitForNavigation({ timeout: 30000 });

    const isLoggedIn = await page.evaluate(() => {
      const logoutButton = document.querySelector('a[href="/logout/"]');
      return logoutButton !== null;
    });

    const nowUtc = formatToISO(new Date());
    const nowBeijing = formatToISO(new Date(new Date().getTime() + 8 * 60 * 60 * 1000));
    
    if (isLoggedIn) {
      const message = `账号 ${username} (${type}) 于北京时间 ${nowBeijing}(UTC时间 ${nowUtc})登录成功!`;
      console.log(message);
      await sendTelegramMessage(`${type}, [${nowBeijing.split(' ')[1].split('.')[0]}]\n${message}`);
      return { success: true, message };
    } else {
      const message = `账号 ${username} (${type}) 登录失败,请检查账号和密码是否正确。`;
      console.error(message);
      await sendTelegramMessage(`${type}, [${nowBeijing.split(' ')[1].split('.')[0]}]\n${message}`);
      return { success: false, message };
    }
  } catch (error) {
    const message = `账号 ${username} (${type}) 登录时出现错误: ${error}`;
    console.error(message);
    await sendTelegramMessage(`${type}, [${formatToISO(new Date()).split(' ')[1].split('.')[0]}]\n${message}`);
    return { success: false, message };
  } finally {
    await page.close();
  }
}

(async () => {
  const accountsJson = process.env.ACCOUNTS_JSON;
  const accounts = JSON.parse(accountsJson);

  const browser = await puppeteer.launch({
    headless: true,
    args: ['--no-sandbox', '--disable-setuid-sandbox'],
    executablePath: '/usr/bin/google-chrome-stable'
  });

  const results = [];
  for (const account of accounts) {
    const result = await loginAccount(account, browser);
    results.push({ ...account, ...result });

    const delay = Math.floor(Math.random() * 8000) + 1000;
    await delayTime(delay);
  }

  await browser.close();

  const successfulLogins = results.filter(r => r.success);
  const failedLogins = results.filter(r => !r.success);

  // 生成表格形式的输出
  console.log('\n登录结果汇总:');
  console.log('| 账号 | 类型 | 状态 | 消息 |');
  console.log('|------|------|------|------|');
  results.forEach(({ username, type, success, message }) => {
    console.log(`| ${username} | ${type} | ${success ? '成功' : '失败'} | ${message} |`);
  });

  let summaryMessage = '\n登录结果统计:\n';
  summaryMessage += `成功登录的账号:${successfulLogins.length}\n`;
  summaryMessage += `登录失败的账号:${failedLogins.length}\n`;

  if (failedLogins.length > 0) {
    summaryMessage += '\n登录失败的账号列表:\n';
    failedLogins.forEach(({ username, type }) => {
      summaryMessage += `- ${username} (${type})\n`;
    });
  }

  console.log(summaryMessage);
  await sendTelegramMessage(`${accounts[0].type}, [${formatToISO(new Date()).split(' ')[1].split('.')[0]}]\n${summaryMessage}`);
})();

package.json文件

{
  "name": "account-keep-alive",
  "version": "1.0.0",
  "description": "Auto-login account keep-alive script",
  "main": "server.js",
  "dependencies": {
    "puppeteer": "^10.0.0",
    "express": "^4.17.1",
    "axios": "^0.21.1"
  },
  "scripts": {
    "start": "node server.js"
  }
}

server.js文件

给出两个版本,第一个版本访问huggingface网页不会显示你的账号登录信息,适用于不想别人知道serv00/ct8账号名的佬友,第二种适用于想通过访问huggingface网站就能直接获取账号登录信息的佬友

无网页账号信息:

const express = require('express');
const { exec } = require('child_process');
const fs = require('fs');
const path = require('path');
const app = express();
const port = process.env.PORT || 8080;

// 使用 HOME 环境变量来创建日志目录
const logDir = path.join(process.env.HOME, 'logs');
if (!fs.existsSync(logDir)) {
    fs.mkdirSync(logDir, { recursive: true });
}

function log(message) {
    const timestamp = new Date().toISOString();
    const logMessage = `${timestamp}: ${message}\n`;
    console.log(logMessage);
    fs.appendFileSync(path.join(logDir, 'app.log'), logMessage);
}

app.get('/', (req, res) => {
    res.send(`
        <h1>登录脚本状态</h1>
        <p>上次执行时间:${lastRunTime || '尚未执行'}</p>
        <p>下次执行时间:${nextRunTime || '未设置'}</p>
        <a href="/run">立即执行脚本</a>
        <br><br>
        <a href="/logs">查看日志</a>
    `);
});

app.get('/logs', (req, res) => {
    const logPath = path.join(logDir, 'app.log');
    fs.readFile(logPath, 'utf8', (err, data) => {
        if (err) {
            res.status(500).send('无法读取日志文件');
            return;
        }
        res.send(`<pre>${data}</pre>`);
    });
});

let lastRunTime = null;
let nextRunTime = null;

function runLoginScript() {
    log('开始执行登录脚本');
    exec('node login.js', (error, stdout, stderr) => {
        if (error) {
            log(`执行错误: ${error.message}`);
            return;
        }
        if (stderr) {
            log(`脚本错误输出: ${stderr}`);
            return;
        }
        log(`脚本输出:\n${stdout}`);
    });
    lastRunTime = new Date().toISOString();
    nextRunTime = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString();
}

app.get('/run', (req, res) => {
    runLoginScript();
    res.send('脚本执行已启动,请查看日志获取详细信息。');
});

// 每7天自动运行一次脚本
setInterval(runLoginScript, 7 * 24 * 60 * 60 * 1000);

app.listen(port, () => {
    log(`服务器运行在 http://localhost:${port}`);
    runLoginScript(); // 启动时立即运行一次
});

有网页账号信息

const express = require('express');
const { exec } = require('child_process');
const fs = require('fs');
const path = require('path');
const app = express();
const port = process.env.PORT || 8080;

// 使用 HOME 环境变量来创建日志目录
const logDir = path.join(process.env.HOME, 'logs');
if (!fs.existsSync(logDir)) {
    fs.mkdirSync(logDir, { recursive: true });
}

let lastRunTime = null;
let nextRunTime = null;
let lastRunResults = null;

function log(message) {
    const timestamp = new Date().toISOString();
    const logMessage = `${timestamp}: ${message}\n`;
    console.log(logMessage);
    fs.appendFileSync(path.join(logDir, 'app.log'), logMessage);
}

app.get('/', (req, res) => {
    let tableHtml = '';
    let summaryHtml = '';
    
    if (lastRunResults) {
        tableHtml = '<h2>上次登录结果:</h2>';
        tableHtml += '<table border="1"><tr><th>账号</th><th>类型</th><th>状态</th><th>消息</th></tr>';
        lastRunResults.forEach(result => {
            tableHtml += `<tr><td>${result.username}</td><td>${result.type}</td><td>${result.success ? '成功' : '失败'}</td><td>${result.message}</td></tr>`;
        });
        tableHtml += '</table>';

        const successfulLogins = lastRunResults.filter(r => r.success);
        const failedLogins = lastRunResults.filter(r => !r.success);

        summaryHtml = '<h2>登录结果统计:</h2>';
        summaryHtml += `<p>成功登录的账号:${successfulLogins.length}</p>`;
        summaryHtml += `<p>登录失败的账号:${failedLogins.length}</p>`;

        if (failedLogins.length > 0) {
            summaryHtml += '<h3>登录失败的账号列表:</h3><ul>';
            failedLogins.forEach(({ username, type }) => {
                summaryHtml += `<li>${username} (${type})</li>`;
            });
            summaryHtml += '</ul>';
        }
    }

    res.send(`
        <h1>登录脚本状态</h1>
        <p>上次执行时间:${lastRunTime || '尚未执行'}</p>
        <p>下次执行时间:${nextRunTime || '未设置'}</p>
        <a href="/run">立即执行脚本</a>
        <br><br>
        <a href="/logs">查看日志</a>
        ${tableHtml}
        ${summaryHtml}
    `);
});

app.get('/logs', (req, res) => {
    const logPath = path.join(logDir, 'app.log');
    fs.readFile(logPath, 'utf8', (err, data) => {
        if (err) {
            res.status(500).send('无法读取日志文件');
            return;
        }
        res.send(`<pre>${data}</pre>`);
    });
});

function runLoginScript() {
    log('开始执行登录脚本');
    exec('node login.js', (error, stdout, stderr) => {
        if (error) {
            log(`执行错误: ${error.message}`);
            return;
        }
        if (stderr) {
            log(`脚本错误输出: ${stderr}`);
            return;
        }
        log(`脚本输出:\n${stdout}`);
        
        // 解析输出以获取结果
        const lines = stdout.split('\n');
        const resultStartIndex = lines.findIndex(line => line.includes('| 账号 | 类型 | 状态 | 消息 |'));
        if (resultStartIndex !== -1) {
            lastRunResults = lines.slice(resultStartIndex + 2)
                .filter(line => line.trim().startsWith('|'))
                .map(line => {
                    const [, username, type, status, message] = line.split('|').map(item => item.trim());
                    return { username, type, success: status === '成功', message };
                });
        }
    });
    lastRunTime = new Date().toISOString();
    nextRunTime = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString();
}

app.get('/run', (req, res) => {
    runLoginScript();
    res.send('脚本执行已启动,请稍后刷新页面查看结果。');
});

// 每7天自动运行一次脚本
setInterval(runLoginScript, 7 * 24 * 60 * 60 * 1000);

app.listen(port, () => {
    log(`服务器运行在 http://localhost:${port}`);
    runLoginScript(); // 启动时立即运行一次
});

README.md文件末尾---前加入app_port: 8080

cloudflare的worker脚本

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
  // 确保请求方法是 POST
  if (request.method !== 'POST') {
    return new Response('Only POST requests are allowed', { status: 405 })
  }

  // 从请求 URL 中提取 bot token 和方法
  const url = new URL(request.url)
  const pathParts = url.pathname.split('/')
  const botToken = pathParts[1]
  const method = pathParts[2]

  // 验证自定义认证头
  const authHeader = request.headers.get('X-Custom-Auth')
  if (authHeader !== 'your-secret-key') {
    return new Response('Unauthorized', { status: 401 })
  }

  // 构建 Telegram API URL
  const telegramUrl = `https://api.telegram.org/bot${botToken}/${method}`

  // 转发请求到 Telegram API
  const telegramResponse = await fetch(telegramUrl, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: request.body
  })

  // 返回 Telegram 的响应
  return new Response(telegramResponse.body, {
    status: telegramResponse.status,
    headers: telegramResponse.headers
  })
}

your-secret-key头部改为你自己想设置的,避免别人滥用你的反代

worker需要使用自定义域名,只有这样huggingface才能请求

huggingface后台变量:

ACCOUNTS_JSON

[  
  { "username": "serv00user1", "password": "serv00password1", "panelnum": "0", "type": "serv00" },
  { "username": "serv00user2", "password": "serv00password2", "panelnum": "4", "type": "serv00" },
  { "username": "serv00user3", "password": "serv00password3", "panelnum": "7", "type": "serv00" },
  { "username": "ct8user1", "password": "ct8password1", "type": "ct8" },
  { "username": "ct8user2", "password": "ct8password2", "type": "ct8" }
]

TELEGRAM_JSON

{
  "cloudflareWorkerUrl": "https://你CF worker域名",
  "telegramBotToken": "YOUR_BOT_TOKEN",
  "telegramBotUserId": "YOUR_USER_ID",
  "customAuthKey": "你设置的头部"
}

变量部分不太懂的参考这里:

脚本每七天自动登录一次

最后,由于huggingface不定期访问会休眠,所以需要加个定时任务,下面的随便选一个进行保活即可。

https://console.rapjob.com/

11 个赞

前者能c吗,能的话我就去c

2 个赞

都是免费的啊

总结

此文本将被隐藏

太强了吧,不需要tg消息是不是不用动

那就不用cloudflare那部分
访问huggingface网页也能看到登录信息

太好了,支持支持!!

1 个赞

感谢分享 之前用的论坛佬 vercel Serv00 Vercel 自动保活

1 个赞

那我再去改改~~

1 个赞

定时任务地址填写什么?

这个废弃了,换一个