diff --git a/services/heartbeat.js b/services/heartbeat.js index c1d7dd9..3171370 100644 --- a/services/heartbeat.js +++ b/services/heartbeat.js @@ -4,7 +4,10 @@ const { readToken, loadProxies, headers } = require("../utils/file"); const { logger } = require("../utils/logger"); const { withTokenRefresh } = require("../utils/token"); const fs = require('fs').promises; -const { DATA_PATHS, HEARTBEAT_INTERVAL, NODE_TEST_INTERVAL } = require('../config'); +const { DATA_PATHS, TIME_INTERVALS } = require('../config'); + +const HEARTBEAT_INTERVAL = TIME_INTERVALS.HEARTBEAT_INTERVAL; +const NODE_TEST_INTERVAL = TIME_INTERVALS.NODE_TEST_INTERVAL; // 新增:保存points到本地文件 async function savePoints(username, points, timestamp = Date.now()) { @@ -74,25 +77,33 @@ async function fetchPoints(token, username, agent, API_BASE) { // 保存运行时间记录 async function saveRunTimes(username, type, timestamp = Date.now()) { try { + logger(`开始保存运行时间 - ${username} - ${type}`, 'info'); + + // 读取现有数据 let timeData = []; try { const fileData = await fs.readFile(DATA_PATHS.RUNTIME_FILE, 'utf8'); timeData = JSON.parse(fileData); } catch (error) { + logger(`读取运行时间文件失败: ${error.message}, 将创建新文件`, 'warn'); timeData = []; } - const existingIndex = timeData.findIndex(p => p.username === username); + // 计算时间 const nextRun = type === 'heartbeat' ? timestamp + HEARTBEAT_INTERVAL : timestamp + NODE_TEST_INTERVAL; - const timeRecord = existingIndex >= 0 ? timeData[existingIndex] : { + // 更新记录 + const existingIndex = timeData.findIndex(p => p.username === username); + const timeRecord = { username, - lastHeartbeat: null, - nextHeartbeat: null, - lastNodeTest: null, - nextNodeTest: null + ...(existingIndex >= 0 ? timeData[existingIndex] : { + lastHeartbeat: null, + nextHeartbeat: null, + lastNodeTest: null, + nextNodeTest: null + }) }; if (type === 'heartbeat') { @@ -103,40 +114,75 @@ async function saveRunTimes(username, type, timestamp = Date.now()) { timeRecord.nextNodeTest = nextRun; } + // 更新数组 if (existingIndex >= 0) { timeData[existingIndex] = timeRecord; } else { timeData.push(timeRecord); } - await fs.writeFile(DATA_PATHS.RUNTIME_FILE, JSON.stringify(timeData, null, 2)); - logger(`Runtime saved for ${username} - ${type}`, 'info'); + // 保存数据 + const recordToSave = JSON.stringify(timeData, null, 2); + await fs.writeFile(DATA_PATHS.RUNTIME_FILE, recordToSave, 'utf8'); + + logger(`成功保存运行时间 - ${username}: + - 类型: ${type} + - 当前时间戳: ${timestamp} + - 下次运行时间戳: ${nextRun}`, 'success'); + } catch (error) { - logger(`Error saving runtime for ${username}: ${error.message}`, 'error'); + logger(`保存运行时间失败 - ${username}: ${error.message}`, 'error'); + logger(`错误堆栈: ${error.stack}`, 'debug'); + throw error; } } // 检查是否需要运行 async function shouldRun(username, type) { try { + logger(`检查${username}是否需要运行${type}`, 'info'); + const fileData = await fs.readFile(DATA_PATHS.RUNTIME_FILE, 'utf8'); const timeData = JSON.parse(fileData); + logger(`成功读取运行时间记录,共${timeData.length}条记录`, 'info'); const record = timeData.find(p => p.username === username); - if (!record) return true; + if (!record) { + logger(`未找到${username}的运行记录,允许运行`, 'info'); + return true; + } const now = Date.now(); if (type === 'heartbeat') { - return !record.nextHeartbeat || now >= record.nextHeartbeat; + const shouldExecute = !record.nextHeartbeat || now >= record.nextHeartbeat; + const timeLeft = record.nextHeartbeat ? Math.max(0, record.nextHeartbeat - now) : 0; + + logger(`${username}的心跳检查: + - 当前时间戳: ${now} + - 下次运行时间戳: ${record.nextHeartbeat || 'null'} + - 剩余时间: ${Math.floor(timeLeft / 1000)}秒 + - 是否运行: ${shouldExecute ? '是' : '否'}`, 'info'); + + return shouldExecute; } else { - return !record.nextNodeTest || now >= record.nextNodeTest; + const shouldExecute = !record.nextNodeTest || now >= record.nextNodeTest; + const timeLeft = record.nextNodeTest ? Math.max(0, record.nextNodeTest - now) : 0; + + logger(`${username}的节点测试检查: + - 当前时间戳: ${now} + - 下次运行时间戳: ${record.nextNodeTest || 'null'} + - 剩余时间: ${Math.floor(timeLeft / 1000)}秒 + - 是否运行: ${shouldExecute ? '是' : '否'}`, 'info'); + + return shouldExecute; } } catch (error) { - return true; // 如果文件不存在或出错,默认允许运行 + logger(`检查${username}运行时间时出错: ${error.message},默认允许运行`, 'warn'); + return true; } } -// Function to send heartbeat +// 修改 sendHeartbeat 函数,在成功时保存运行时间 async function sendHeartbeat(API_BASE) { const proxies = await loadProxies(); if (proxies.length === 0) { diff --git a/services/login.js b/services/login.js index 1983067..98a4b8a 100644 --- a/services/login.js +++ b/services/login.js @@ -26,6 +26,29 @@ async function readUsersFromFile() { } } +// 添加简单的文件锁机制 +let isWriting = false; +const writeQueue = []; + +async function writeWithLock(operation) { + if (isWriting) { + // 如果正在写入,将操作加入队列 + await new Promise(resolve => writeQueue.push(resolve)); + } + + isWriting = true; + try { + await operation(); + } finally { + isWriting = false; + if (writeQueue.length > 0) { + // 处理队列中的下一个写入操作 + const next = writeQueue.shift(); + next(); + } + } +} + // 修改:登录函数,增加详细日志 async function login(email, password, API_BASE, proxy) { try { @@ -72,7 +95,11 @@ async function login(email, password, API_BASE, proxy) { const data = await response.json(); if (data.token) { logger(`New token received for ${email}`, 'info'); - await saveToken({ token: data.token, username: email }); + // 使用写入锁保存token + await writeWithLock(async () => { + logger(`Saving token for ${email}...`, 'info'); + await saveToken({ token: data.token, username: email }); + }); logger(`New login successful for ${email}!`, 'success'); return { success: true, @@ -129,20 +156,22 @@ async function loginWithAllAccounts(API_BASE) { failedAccounts: [] }; + logger(`开始处理账号,共${accounts.length}个`, 'info'); + for (let i = 0; i < accounts.length; i++) { const account = accounts[i]; const proxy = proxies[i % proxies.length]; - logger(`Processing account ${i + 1}/${accounts.length}: ${account.email}`, 'info'); + logger(`处理第${i + 1}个账号: ${account.email}`, 'info'); const loginResult = await login(account.email, account.password, API_BASE, proxy); if (loginResult.success) { if (loginResult.isNewLogin) { results.newLogins++; - logger(`New login successful for ${account.email}`, 'success'); + logger(`${account.email} 新登录成功,当前新登录数: ${results.newLogins}`, 'success'); } else { results.validTokens++; - logger(`Using existing valid token for ${account.email}`, 'success'); + logger(`${account.email} 使用现有token,当前有效token数: ${results.validTokens}`, 'success'); } } else { results.failed++; @@ -150,8 +179,15 @@ async function loginWithAllAccounts(API_BASE) { email: account.email, error: loginResult.error }); - logger(`Failed to process ${account.email}: ${loginResult.error}`, 'error'); + logger(`${account.email} 处理失败,当前失败数: ${results.failed}`, 'error'); } + + // 每次循环都输出当前统计 + logger(`当前统计: + - 总账号数: ${results.total} + - 有效token数: ${results.validTokens} + - 新登录数: ${results.newLogins} + - 失败数: ${results.failed}`, 'info'); } logger(`Login process completed: diff --git a/utils/logger.js b/utils/logger.js index a75b1f6..6396c61 100644 --- a/utils/logger.js +++ b/utils/logger.js @@ -1,4 +1,19 @@ const chalk = require('chalk'); +const fs = require('fs'); +const path = require('path'); +const { DATA_PATHS } = require('../config'); + +// 确保日志目录存在 +const logDir = path.join(process.cwd(), 'logs'); +if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }); +} + +// 创建当前日期的日志文件 +const getLogFile = () => { + const date = new Date().toISOString().split('T')[0]; + return path.join(logDir, `${date}.log`); +}; function logger(message, level = 'info', value = "") { const now = new Date().toISOString(); @@ -10,7 +25,13 @@ function logger(message, level = 'info', value = "") { debug: chalk.magenta, }; const color = colors[level] || chalk.white; + + // 控制台输出(带颜色) console.log(color(`[${now}] [${level.toUpperCase()}]: ${message}`), chalk.yellow(value)); + + // 写入文件(不带颜色) + const logMessage = `[${now}] [${level.toUpperCase()}]: ${message} ${value}\n`; + fs.appendFileSync(getLogFile(), logMessage); } module.exports = { logger }; diff --git a/utils/token.js b/utils/token.js index 4ae3964..25c644d 100644 --- a/utils/token.js +++ b/utils/token.js @@ -6,7 +6,7 @@ const { headers } = require('./file'); const { DATA_PATHS, CONFIG_PATHS } = require('../config'); const TOKEN_FILE = DATA_PATHS.TOKENS_FILE; -const ACCOUNT_FILE = CONFIG_PATHS.ACCOUNTS_FILE; +const ACCOUNT_FILE = CONFIG_PATHS.ACCOUNT_FILE; // Helper function to add delay @@ -14,56 +14,51 @@ const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); // Function to save the token with retry mechanism async function saveToken(data, retries = 3, backoff = 1000) { + logger(`开始保存token - 用户: ${data.username}`, 'info'); + for (let i = 0; i < retries; i++) { try { - // Create a temporary file for atomic write - const tempFile = `${DATA_PATHS.TOKENS_FILE}.tmp`; - + const tempFile = `${TOKEN_FILE}.tmp`; + let tokens = []; try { - const fileData = await fs.readFile(DATA_PATHS.TOKENS_FILE, 'utf8'); + logger(`读取现有tokens文件: ${TOKEN_FILE}`, 'info'); + const fileData = await fs.readFile(TOKEN_FILE, 'utf8'); tokens = JSON.parse(fileData); + logger(`成功读取${tokens.length}个现有token`, 'info'); } catch (error) { - logger("No previous tokens found, creating new file.", "info"); + logger(`读取tokens文件失败: ${error.message}`, 'warn'); } const tokenIndex = tokens.findIndex(token => token.username === data.username); - + if (tokenIndex !== -1) { tokens[tokenIndex] = data; - logger(`Token for ${data.username} updated.`); + logger(`更新用户${data.username}的token`, 'info'); } else { tokens.push(data); - logger(`Token for ${data.username} added.`); + logger(`添加用户${data.username}的新token`, 'info'); } - // Write to temporary file first + logger(`写入临时文件: ${tempFile}`, 'info'); await fs.writeFile(tempFile, JSON.stringify(tokens, null, 2)); - // Atomically rename temp file to actual file try { - await fs.rename(tempFile, DATA_PATHS.TOKENS_FILE); - logger('Token saved successfully!', "success"); - return; // Success - exit the retry loop + logger(`重命名临时文件到: ${TOKEN_FILE}`, 'info'); + await fs.rename(tempFile, TOKEN_FILE); + logger(`Token保存成功! 当前共有${tokens.length}个token`, 'success'); + return; } catch (renameError) { - // If rename fails, try to clean up temp file - try { - await fs.unlink(tempFile); - } catch (unlinkError) { - // Ignore unlink errors - } + logger(`重命名失败: ${renameError.message}`, 'error'); throw renameError; } - } catch (error) { if (i === retries - 1) { - // Last retry failed - logger('Error saving token:', "error", error); + logger(`Token保存最终失败: ${error.message}`, 'error'); throw error; } - // Wait before retrying with exponential backoff + logger(`重试保存token (${i + 2}/${retries})`, 'warn'); await delay(backoff * Math.pow(2, i)); - logger(`Retrying token save for ${data.username} (attempt ${i + 2}/${retries})`, "info"); } } } @@ -71,17 +66,30 @@ async function saveToken(data, retries = 3, backoff = 1000) { // 读取账号信息 async function readAccountCredentials() { try { + if (!ACCOUNT_FILE) { + throw new Error('Account file path is not defined'); + } + + logger(`Reading accounts from: ${ACCOUNT_FILE}`, 'info'); const fileData = await fs.readFile(ACCOUNT_FILE, 'utf8'); - return fileData + + if (!fileData) { + throw new Error('Account file is empty'); + } + + const accounts = fileData .split('\n') .filter(line => line.trim() !== '') .map(line => { const [email, password] = line.split(':').map(s => s.trim()); return { email, password }; }); + + logger(`Successfully read ${accounts.length} accounts`, 'info'); + return accounts; } catch (error) { - logger('Error reading account credentials:', 'error', error); - return []; + logger(`Error reading account credentials: ${error.message}`, 'error'); + throw new Error(`Failed to read account credentials: ${error.message}`); } } @@ -104,11 +112,19 @@ async function verifyToken(token, API_BASE) { // 获取已存token async function getExistingToken(username) { try { + logger(`尝试获取${username}的token`, 'info'); const fileData = await fs.readFile(TOKEN_FILE, 'utf8'); const tokens = JSON.parse(fileData); + logger(`当前共有${tokens.length}个token记录`, 'info'); const tokenData = tokens.find(t => t.username === username); + if(tokenData) { + logger(`找到${username}的token`, 'info'); + } else { + logger(`未找到${username}的token`, 'info'); + } return tokenData?.token; } catch (error) { + logger(`读取token失败: ${error.message}`, 'warn'); return null; } } @@ -175,5 +191,6 @@ module.exports = { verifyToken, getExistingToken, refreshToken, - withTokenRefresh -}; \ No newline at end of file + withTokenRefresh, + saveToken +};