Spaces:
Build error
Build error
| const fs = require('fs'); | |
| const os = require('os'); | |
| const path = require('path'); | |
| const axios = require('axios'); | |
| const express = require('express'); | |
| const puppeteer = require('puppeteer-extra'); | |
| const StealthPlugin = require('puppeteer-extra-plugin-stealth'); | |
| puppeteer.use(StealthPlugin()); | |
| const app = express(); | |
| app.use(express.json()); | |
| // CORS 支持 - 解决 iframe 嵌入问题 | |
| app.use((req, res, next) => { | |
| res.header('Access-Control-Allow-Origin', '*'); | |
| res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); | |
| res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); | |
| if (req.method === 'OPTIONS') { | |
| return res.sendStatus(200); | |
| } | |
| next(); | |
| }); | |
| // =============================== | |
| // 数据持久化目录 | |
| // =============================== | |
| const dataDir = process.env.DATA_DIR || '/tmp/data'; | |
| const accountsFile = path.join(dataDir, 'accounts.json'); | |
| if (!fs.existsSync(dataDir)) { | |
| fs.mkdirSync(dataDir, { recursive: true }); | |
| } | |
| // =============================== | |
| // 配置 | |
| // =============================== | |
| const config = { | |
| port: parseInt(process.env.PORT) || 7860, | |
| mail: { | |
| // zeabur-mail 临时邮箱服务地址 | |
| tempMailUrl: process.env.TEMP_MAIL_URL || 'https://your-tempmail-service.com' | |
| }, | |
| yesCaptcha: { | |
| apiKey: process.env.YESCAPTCHA_API_KEY || '' | |
| }, | |
| recaptcha: { | |
| websiteKey: process.env.RECAPTCHA_WEBSITE_KEY || 'YOUR_RECAPTCHA_WEBSITE_KEY', | |
| websiteURL: process.env.RECAPTCHA_WEBSITE_URL || 'https://accountverification.business.gemini.google' | |
| }, | |
| browser: { | |
| headless: true, | |
| executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || '/usr/bin/chromium', | |
| timeout: parseInt(process.env.BROWSER_TIMEOUT) || 60000 | |
| }, | |
| polling: { | |
| interval: parseInt(process.env.MAIL_POLL_INTERVAL) || 3000, | |
| maxAttempts: parseInt(process.env.MAIL_POLL_MAX_ATTEMPTS) || 25 | |
| }, | |
| businessGemini: { | |
| url: process.env.BUSINESS_GEMINI_URL || '', | |
| adminPassword: process.env.BUSINESS_GEMINI_PASSWORD || '', | |
| accountId: parseInt(process.env.BUSINESS_GEMINI_ACCOUNT_ID) || 0 | |
| }, | |
| schedule: { | |
| // 定时注册:间隔小时数,0 表示禁用,支持小数(如 0.1 = 6分钟) | |
| registerIntervalHours: parseFloat(process.env.SCHEDULE_REGISTER_HOURS) || 0, | |
| // 每次定时注册的账号数量 | |
| registerCount: parseInt(process.env.SCHEDULE_REGISTER_COUNT) || 1, | |
| // 定时刷新:间隔小时数,0 表示禁用,支持小数 | |
| refreshIntervalHours: parseFloat(process.env.SCHEDULE_REFRESH_HOURS) || 0 | |
| }, | |
| // 账号操作间隔时间(秒) | |
| interval: { | |
| register: parseInt(process.env.REGISTER_INTERVAL_SECONDS) || 60, // 注册间隔,默认60秒 | |
| refresh: parseInt(process.env.REFRESH_INTERVAL_SECONDS) || 30 // 刷新间隔,默认30秒 | |
| } | |
| }; | |
| // =============================== | |
| // 运行时配置(可通过前端修改) | |
| // =============================== | |
| const runtimeConfigFile = path.join(dataDir, 'runtime_config.json'); | |
| let runtimeConfig = { | |
| registerIntervalSeconds: config.interval.register, | |
| refreshIntervalSeconds: config.interval.refresh, | |
| scheduleRegisterHours: config.schedule.registerIntervalHours, | |
| scheduleRegisterCount: config.schedule.registerCount, | |
| scheduleRefreshHours: config.schedule.refreshIntervalHours, | |
| // 服务配置 | |
| tempMailUrl: config.mail.tempMailUrl, | |
| businessGeminiUrl: config.businessGemini.url, | |
| businessGeminiPassword: config.businessGemini.adminPassword, | |
| yesCaptchaApiKey: config.yesCaptcha.apiKey | |
| }; | |
| function loadRuntimeConfig() { | |
| try { | |
| if (fs.existsSync(runtimeConfigFile)) { | |
| const saved = JSON.parse(fs.readFileSync(runtimeConfigFile, 'utf8')); | |
| runtimeConfig = { ...runtimeConfig, ...saved }; | |
| // 同步到config对象 | |
| config.interval.register = runtimeConfig.registerIntervalSeconds; | |
| config.interval.refresh = runtimeConfig.refreshIntervalSeconds; | |
| config.schedule.registerIntervalHours = runtimeConfig.scheduleRegisterHours; | |
| config.schedule.registerCount = runtimeConfig.scheduleRegisterCount; | |
| config.schedule.refreshIntervalHours = runtimeConfig.scheduleRefreshHours; | |
| // 服务配置 | |
| if (runtimeConfig.tempMailUrl) config.mail.tempMailUrl = runtimeConfig.tempMailUrl; | |
| if (runtimeConfig.businessGeminiUrl) config.businessGemini.url = runtimeConfig.businessGeminiUrl; | |
| if (runtimeConfig.businessGeminiPassword) config.businessGemini.adminPassword = runtimeConfig.businessGeminiPassword; | |
| if (runtimeConfig.yesCaptchaApiKey) config.yesCaptcha.apiKey = runtimeConfig.yesCaptchaApiKey; | |
| } | |
| } catch (e) { | |
| console.error('加载运行时配置失败:', e.message); | |
| } | |
| } | |
| function saveRuntimeConfig() { | |
| try { | |
| fs.writeFileSync(runtimeConfigFile, JSON.stringify(runtimeConfig, null, 2)); | |
| } catch (e) { | |
| console.error('保存运行时配置失败:', e.message); | |
| } | |
| } | |
| // =============================== | |
| // 账号存储和日志 | |
| // =============================== | |
| let runtimeAccounts = []; | |
| let logs = []; | |
| const MAX_LOGS = 1000; // 增加最大日志数量,主要依靠时间清理 | |
| const LOG_RETENTION_HOURS = 24; // 日志保留24小时 | |
| let registerStatus = { running: false, total: 0, completed: 0, results: [] }; | |
| let businessGeminiAccounts = []; | |
| let lastSyncTime = null; | |
| let refreshStatus = { running: false, lastResult: null }; | |
| let lastRefreshTime = null; | |
| let lastScheduledRegisterTime = null; | |
| let lastScheduledRefreshTime = null; | |
| let scheduleTimers = { register: null, refresh: null }; | |
| function loadAccounts() { | |
| try { | |
| if (fs.existsSync(accountsFile)) { | |
| runtimeAccounts = JSON.parse(fs.readFileSync(accountsFile, 'utf8')); | |
| console.log(`📂 加载了 ${runtimeAccounts.length} 个账号`); | |
| } | |
| } catch (e) { | |
| console.error('加载账号失败:', e.message); | |
| } | |
| } | |
| function saveAccounts() { | |
| try { | |
| fs.writeFileSync(accountsFile, JSON.stringify(runtimeAccounts, null, 2)); | |
| } catch (e) { | |
| console.error('保存账号失败:', e.message); | |
| } | |
| } | |
| function addLog(level, message, email = null) { | |
| const log = { time: new Date().toISOString(), level, message, email }; | |
| logs.unshift(log); | |
| // 清理超过24小时的日志 | |
| cleanupOldLogs(); | |
| console.log(`[${level}] ${email ? `[${email}] ` : ''}${message}`); | |
| } | |
| // 清理超过保留时间的日志 | |
| function cleanupOldLogs() { | |
| const cutoffTime = new Date(Date.now() - LOG_RETENTION_HOURS * 60 * 60 * 1000); | |
| const originalLength = logs.length; | |
| logs = logs.filter(log => new Date(log.time) > cutoffTime); | |
| // 作为备用,如果日志数量仍然过多,保留最新的 | |
| if (logs.length > MAX_LOGS) { | |
| logs = logs.slice(0, MAX_LOGS); | |
| } | |
| // 如果清理了日志,记录清理信息 | |
| if (originalLength > logs.length) { | |
| console.log(`[SYSTEM] 清理了 ${originalLength - logs.length} 条超过${LOG_RETENTION_HOURS}小时的旧日志,当前保留 ${logs.length} 条`); | |
| } | |
| } | |
| const sleep = (ms) => new Promise(r => setTimeout(r, ms)); | |
| function createTempUserDataDir() { | |
| return fs.mkdtempSync(path.join(os.tmpdir(), 'pptr-profile-')); | |
| } | |
| // =============================== | |
| // zeabur-mail 临时邮箱 API 集成 | |
| // =============================== | |
| async function createTempMailbox() { | |
| if (!config.mail.tempMailUrl) { | |
| throw new Error('未配置临时邮箱服务地址 (TEMP_MAIL_URL)'); | |
| } | |
| try { | |
| addLog('INFO', `创建临时邮箱: ${config.mail.tempMailUrl}`); | |
| const response = await axios.post(`${config.mail.tempMailUrl}/api/mailboxes`, {}, { | |
| headers: { 'Content-Type': 'application/json' }, | |
| timeout: 30000 | |
| }); | |
| const { address, token, url } = response.data; | |
| addLog('SUCCESS', `邮箱创建成功: ${address}`); | |
| return { | |
| email: address, | |
| jwtUrl: url || `${config.mail.tempMailUrl}/?jwt=${token}`, | |
| token | |
| }; | |
| } catch (error) { | |
| addLog('ERROR', `创建邮箱失败: ${error.message}`); | |
| throw error; | |
| } | |
| } | |
| // =============================== | |
| // 邮件获取类 - 适配 zeabur-mail API | |
| // =============================== | |
| class ZeaburMailFetcher { | |
| constructor(email, mailConfig = null) { | |
| this.email = email; | |
| this.jwtUrl = mailConfig?.jwtUrl || ''; | |
| } | |
| async tryFetchOnce() { | |
| try { | |
| if (!this.jwtUrl) { | |
| addLog('WARN', '未配置邮件 JWT URL', this.email); | |
| return null; | |
| } | |
| const urlObj = new URL(this.jwtUrl); | |
| const baseUrl = `${urlObj.protocol}//${urlObj.host}`; | |
| const jwt = urlObj.searchParams.get('jwt'); | |
| if (!jwt) { | |
| addLog('WARN', 'JWT URL 中未找到 jwt 参数', this.email); | |
| return null; | |
| } | |
| const response = await axios.get(`${baseUrl}/api/emails`, { | |
| params: { jwt }, | |
| headers: { 'Content-Type': 'application/json', 'User-Agent': 'Mozilla/5.0' }, | |
| timeout: 10000 | |
| }); | |
| const mails = response.data.emails || []; | |
| addLog('INFO', `获取到 ${mails.length} 封邮件`, this.email); | |
| for (const mail of mails) { | |
| const subject = mail.subject || ''; | |
| addLog('INFO', `检查邮件: subject="${subject.substring(0, 50)}"`, this.email); | |
| // zeabur-mail 使用 text_content 和 html_content 字段 | |
| const contentParts = []; | |
| if (mail.text_content) contentParts.push(mail.text_content); | |
| if (mail.html_content) contentParts.push(mail.html_content); | |
| if (contentParts.length === 0) contentParts.push(JSON.stringify(mail)); | |
| const content = contentParts.join(' '); | |
| // 匹配6位验证码 | |
| const codeMatch = content.match(/\b([A-Z0-9]{6})\b/g); | |
| if (codeMatch) { | |
| addLog('INFO', `找到可能的验证码: ${codeMatch.join(', ')}`, this.email); | |
| const excluded = ['GOOGLE', 'GEMINI', 'VERIFY', 'ACCESS', 'BUSINE', 'SIGNIN']; | |
| // 优先找字母+数字组合 | |
| for (const code of codeMatch) { | |
| const hasLetter = /[A-Z]/.test(code); | |
| const hasNumber = /[0-9]/.test(code); | |
| if (!excluded.includes(code) && hasLetter && hasNumber) { | |
| addLog('SUCCESS', `验证码: ${code}`, this.email); | |
| return code; | |
| } | |
| } | |
| // 其次找任意非排除的6位码 | |
| for (const code of codeMatch) { | |
| if (!excluded.includes(code)) { | |
| addLog('SUCCESS', `验证码: ${code}`, this.email); | |
| return code; | |
| } | |
| } | |
| } | |
| } | |
| addLog('WARN', '未找到验证码', this.email); | |
| return null; | |
| } catch (error) { | |
| addLog('ERROR', `获取邮件失败: ${error.message}`, this.email); | |
| return null; | |
| } | |
| } | |
| } | |
| async function startPollingForCode(email, mailConfig = null) { | |
| const fetcher = new ZeaburMailFetcher(email, mailConfig); | |
| for (let i = 1; i <= config.polling.maxAttempts; i++) { | |
| addLog('INFO', `尝试获取验证码 (${i}/${config.polling.maxAttempts})`, email); | |
| const code = await fetcher.tryFetchOnce(); | |
| if (code) return code; | |
| await sleep(config.polling.interval); | |
| } | |
| return null; | |
| } | |
| // =============================== | |
| // YesCaptcha 验证码处理 | |
| // =============================== | |
| async function getCaptchaToken(apiKey) { | |
| if (!apiKey) return null; | |
| try { | |
| addLog('INFO', '请求 YesCaptcha Token...'); | |
| const createResp = await axios.post('https://api.yescaptcha.com/createTask', { | |
| clientKey: apiKey, | |
| task: { | |
| websiteURL: config.recaptcha.websiteURL, | |
| websiteKey: config.recaptcha.websiteKey, | |
| pageAction: 'verify_oob_code', | |
| type: 'RecaptchaV3TaskProxylessM1' | |
| } | |
| }); | |
| addLog('INFO', `YesCaptcha 响应: ${JSON.stringify(createResp.data)}`); | |
| const taskId = createResp.data.taskId; | |
| if (!taskId) { | |
| addLog('ERROR', `YesCaptcha 未返回 taskId, 错误: ${createResp.data.errorDescription || createResp.data.errorCode || '未知'}`); | |
| return null; | |
| } | |
| for (let i = 0; i < 30; i++) { | |
| await sleep(2000); | |
| const resultResp = await axios.post('https://api.yescaptcha.com/getTaskResult', { | |
| clientKey: apiKey, | |
| taskId | |
| }); | |
| if (resultResp.data.status === 'ready') { | |
| addLog('SUCCESS', 'YesCaptcha Token 获取成功'); | |
| return resultResp.data.solution.gRecaptchaResponse; | |
| } | |
| } | |
| addLog('ERROR', 'YesCaptcha Token 获取超时'); | |
| return null; | |
| } catch (error) { | |
| addLog('ERROR', `YesCaptcha 错误: ${error.message}`); | |
| return null; | |
| } | |
| } | |
| function patchPayload(rawBody, newToken) { | |
| if (!rawBody) return rawBody; | |
| const params = new URLSearchParams(rawBody); | |
| let fReq = params.get('f.req'); | |
| if (!fReq) return rawBody; | |
| const tokenRegex = /0[3c]AFc[a-zA-Z0-9_\-]{50,}/g; | |
| if (tokenRegex.test(fReq)) { | |
| fReq = fReq.replace(tokenRegex, newToken); | |
| params.set('f.req', fReq); | |
| } | |
| return params.toString(); | |
| } | |
| // =============================== | |
| // 推送到 Business Gemini - 适配 cookie-refresher 格式 | |
| // =============================== | |
| async function pushToBusinessGemini(cookieData, geminiConfig = null, email = null, mailConfig = null) { | |
| const targetConfig = geminiConfig || config.businessGemini; | |
| if (!targetConfig.url || !targetConfig.adminPassword) { | |
| addLog('WARN', 'Business Gemini 未配置,跳过推送', email); | |
| return { success: false, error: 'Business Gemini 未配置' }; | |
| } | |
| try { | |
| addLog('INFO', `推送到 Business Gemini: ${targetConfig.url}`, email); | |
| // 1. 登录获取 session | |
| const loginResp = await axios.post(`${targetConfig.url}/api/auth/login`, { | |
| password: targetConfig.adminPassword | |
| }, { | |
| headers: { 'Content-Type': 'application/json' }, | |
| timeout: 30000 | |
| }); | |
| const setCookie = loginResp.headers['set-cookie']; | |
| const sessionCookie = setCookie ? setCookie[0].split(';')[0] : ''; | |
| const accountId = targetConfig.accountId || 0; | |
| // 2. 构建推送数据 - 适配 business-gemini 格式 | |
| // business-gemini 需要: secure_c_ses, host_c_oses, csesidx, team_id | |
| const pushData = { | |
| secure_c_ses: cookieData.secure_c_ses || '', | |
| host_c_oses: cookieData.host_c_oses || '', | |
| csesidx: cookieData.csesidx || '', | |
| team_id: cookieData.team_id || '', | |
| tempmail_name: email || '', | |
| tempmail_url: mailConfig?.jwtUrl || '' // 添加临时邮箱URL | |
| }; | |
| // 添加调试日志 | |
| addLog('INFO', `推送数据: tempmail_name="${pushData.tempmail_name}", tempmail_url="${pushData.tempmail_url}"`, email); | |
| // 3. 尝试更新账号 | |
| try { | |
| await axios.put(`${targetConfig.url}/api/accounts/${accountId}`, pushData, { | |
| headers: { 'Content-Type': 'application/json', 'Cookie': sessionCookie }, | |
| timeout: 30000 | |
| }); | |
| addLog('SUCCESS', `推送成功!账号ID: ${accountId}`, email); | |
| return { success: true, accountId, action: 'update' }; | |
| } catch (e) { | |
| if (e.response && e.response.status === 404) { | |
| // 账号不存在,添加新账号 | |
| addLog('INFO', '账号不存在,添加新账号...', email); | |
| await axios.post(`${targetConfig.url}/api/accounts`, { | |
| ...pushData, | |
| user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' | |
| }, { | |
| headers: { 'Content-Type': 'application/json', 'Cookie': sessionCookie }, | |
| timeout: 30000 | |
| }); | |
| addLog('SUCCESS', `新账号添加成功!`, email); | |
| return { success: true, accountId, action: 'create' }; | |
| } | |
| throw e; | |
| } | |
| } catch (error) { | |
| addLog('ERROR', `推送失败: ${error.message}`, email); | |
| return { success: false, error: error.message }; | |
| } | |
| } | |
| // =============================== | |
| // 从 Business Gemini 同步账号 | |
| // =============================== | |
| async function syncBusinessGeminiAccounts() { | |
| if (!config.businessGemini.url || !config.businessGemini.adminPassword) { | |
| return { success: false, error: 'Business Gemini 未配置' }; | |
| } | |
| try { | |
| const loginResp = await axios.post(`${config.businessGemini.url}/api/auth/login`, { | |
| password: config.businessGemini.adminPassword | |
| }, { headers: { 'Content-Type': 'application/json' }, timeout: 30000 }); | |
| const setCookie = loginResp.headers['set-cookie']; | |
| const sessionCookie = setCookie ? setCookie[0].split(';')[0] : ''; | |
| const accountsResp = await axios.get(`${config.businessGemini.url}/api/accounts`, { | |
| headers: { 'Cookie': sessionCookie }, | |
| timeout: 30000 | |
| }); | |
| const accounts = accountsResp.data.accounts || []; | |
| businessGeminiAccounts = accounts; | |
| lastSyncTime = new Date(); | |
| addLog('INFO', `从 Business Gemini 同步了 ${accounts.length} 个账号`); | |
| return { | |
| success: true, | |
| accounts, | |
| available: accounts.filter(a => a.available !== false).length, | |
| unavailable: accounts.filter(a => a.available === false).length | |
| }; | |
| } catch (error) { | |
| addLog('ERROR', `同步 Business Gemini 账号失败: ${error.message}`); | |
| return { success: false, error: error.message }; | |
| } | |
| } | |
| // =============================== | |
| // 核心登录任务 | |
| // =============================== | |
| async function runLoginTask(email, password, captchaKey, mailConfig = null) { | |
| const userDataDir = createTempUserDataDir(); | |
| const taskId = Date.now().toString(); | |
| addLog('INFO', `开始登录任务`, email); | |
| let captchaNeeded = false; | |
| let lastBatchUrl = null; | |
| let lastBatchBody = null; | |
| let browser = null; | |
| const browserArgs = [ | |
| '--no-sandbox', | |
| '--disable-setuid-sandbox', | |
| '--disable-dev-shm-usage', | |
| '--disable-gpu', | |
| '--no-first-run', | |
| '--no-zygote', | |
| '--single-process', | |
| '--disable-extensions', | |
| '--disable-background-networking', | |
| '--disable-default-apps', | |
| '--disable-sync', | |
| '--disable-translate', | |
| '--hide-scrollbars', | |
| '--metrics-recording-only', | |
| '--mute-audio', | |
| '--js-flags=--max-old-space-size=256', | |
| '--window-size=1280,800' | |
| ]; | |
| try { | |
| browser = await puppeteer.launch({ | |
| headless: 'new', | |
| executablePath: config.browser.executablePath, | |
| userDataDir, | |
| args: browserArgs, | |
| protocolTimeout: 60000, | |
| // 添加内存限制 | |
| ignoreDefaultArgs: ['--disable-extensions'], | |
| defaultViewport: { width: 1280, height: 800 }, | |
| // 限制并发连接 | |
| pipe: true | |
| }); | |
| } catch (launchError) { | |
| addLog('ERROR', `浏览器启动失败: ${launchError.message}`, email); | |
| try { fs.rmSync(userDataDir, { recursive: true, force: true }); } catch (e) {} | |
| return { success: false, error: `浏览器启动失败: ${launchError.message}` }; | |
| } | |
| try { | |
| const page = await browser.newPage(); | |
| await page.setCacheEnabled(false); | |
| await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'); | |
| await page.setViewport({ width: 1280, height: 800 }); | |
| await page.setExtraHTTPHeaders({ 'Accept-Language': 'en-US,en;q=0.9' }); | |
| await page.setRequestInterception(true); | |
| page.on('request', req => { | |
| const url = req.url(); | |
| if (url.includes('batchexecute') && req.method() === 'POST') { | |
| lastBatchUrl = url; | |
| lastBatchBody = req.postData(); | |
| } | |
| const blockedTypes = ['image', 'media', 'font', 'stylesheet']; | |
| if (blockedTypes.includes(req.resourceType())) { | |
| return req.abort().catch(() => {}); | |
| } | |
| if (url.includes('google-analytics') || url.includes('googletagmanager') || url.includes('doubleclick')) { | |
| return req.abort().catch(() => {}); | |
| } | |
| req.continue().catch(() => {}); | |
| }); | |
| page.on('response', async res => { | |
| if (res.url().includes('batchexecute')) { | |
| try { | |
| const text = await res.text(); | |
| if (text.includes('CAPTCHA_CHECK_FAILED')) { | |
| captchaNeeded = true; | |
| } | |
| } catch (e) {} | |
| } | |
| }); | |
| // Step 1: 访问认证首页 | |
| addLog('INFO', 'Step 1: 访问认证首页', email); | |
| await page.goto('https://auth.business.gemini.google/', { waitUntil: 'networkidle2', timeout: 60000 }); | |
| await sleep(2000); | |
| // Step 2: 设置 Cookie | |
| addLog('INFO', 'Step 2: 设置 Cookie', email); | |
| await page.setCookie({ | |
| name: '__Host-AP_SignInXsrf', | |
| value: 'KdLRzKwwBTD5wo8nUollAbY6cW0', | |
| domain: 'auth.business.gemini.google', | |
| path: '/', | |
| secure: true, | |
| httpOnly: true, | |
| sameSite: 'Strict' | |
| }); | |
| // Step 3: 访问登录页面 | |
| const targetUrl = `https://auth.business.gemini.google/login/email?continueUrl=https%3A%2F%2Fbusiness.gemini.google%2F&loginHint=${encodeURIComponent(email)}&xsrfToken=KdLRzKwwBTD5wo8nUollAbY6cW0`; | |
| addLog('INFO', 'Step 3: 访问登录页面', email); | |
| await page.goto(targetUrl, { waitUntil: 'networkidle2', timeout: 60000 }); | |
| await sleep(3000); | |
| // Step 4: 处理 CAPTCHA | |
| if (captchaNeeded) { | |
| addLog('INFO', 'Step 4: 处理 CAPTCHA', email); | |
| const newToken = await getCaptchaToken(captchaKey); | |
| if (newToken && lastBatchBody) { | |
| const newPostData = patchPayload(lastBatchBody, newToken); | |
| await page.evaluate(async (url, body) => { | |
| await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body, credentials: 'include' }); | |
| }, lastBatchUrl, newPostData); | |
| captchaNeeded = false; | |
| await sleep(5000); | |
| } | |
| } | |
| // Step 5: 点击 Resend code | |
| const resendButton = await page.$('button[jsname="WGPTvf"]'); | |
| if (resendButton) { | |
| addLog('INFO', 'Step 5: 点击 Resend code', email); | |
| captchaNeeded = false; | |
| await resendButton.click(); | |
| await sleep(3000); | |
| if (captchaNeeded && lastBatchBody) { | |
| const token = await getCaptchaToken(captchaKey); | |
| if (token) { | |
| await page.evaluate(async (url, body) => { | |
| await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body, credentials: 'include' }); | |
| }, lastBatchUrl, patchPayload(lastBatchBody, token)); | |
| await sleep(5000); | |
| } | |
| } | |
| } | |
| // Step 6: 获取验证码 - 等待 20 秒后开始轮询 | |
| addLog('INFO', 'Step 6: 等待验证码邮件 (20秒)...', email); | |
| await sleep(20000); | |
| const code = await startPollingForCode(email, mailConfig); | |
| if (!code) { | |
| throw new Error("获取验证码超时"); | |
| } | |
| // Step 7: 输入验证码 | |
| addLog('INFO', `Step 7: 输入验证码 ${code}`, email); | |
| const inputSelectors = ['input[jsname="ovqh0b"]', 'input[type="text"]', 'input[autocomplete="one-time-code"]', 'input:not([type="hidden"])']; | |
| let inputElement = null; | |
| for (const sel of inputSelectors) { | |
| inputElement = await page.$(sel); | |
| if (inputElement) break; | |
| } | |
| if (!inputElement) throw new Error('找不到验证码输入框'); | |
| await inputElement.type(code, { delay: 100 }); | |
| // Step 8: 点击 Verify | |
| addLog('INFO', 'Step 8: 点击 Verify', email); | |
| const verifyBtn = await page.$('button[jsname="XooR8e"]') || await page.$('button[type="submit"]'); | |
| if (verifyBtn) await verifyBtn.click(); | |
| // 增加等待时间,让账号完成初始化 | |
| addLog('INFO', '等待账号初始化完成 (5秒)...', email); | |
| await sleep(5000); | |
| // 检查是否需要同意条款 | |
| const currentUrl = page.url(); | |
| addLog('INFO', `验证后当前 URL: ${currentUrl}`, email); | |
| if (currentUrl.includes('/admin/create') || currentUrl.includes('/agree')) { | |
| const agreeBtn = await page.$('button.agree-button'); | |
| if (agreeBtn) { | |
| addLog('INFO', '点击同意条款按钮...', email); | |
| await agreeBtn.click(); | |
| await sleep(5000); | |
| } | |
| } | |
| // Step 9: 提取 Cookie | |
| addLog('INFO', 'Step 9: 提取 Cookie', email); | |
| let hostCoses = '', secureCSes = ''; | |
| for (let i = 0; i < 15; i++) { | |
| const client = await page.target().createCDPSession(); | |
| const { cookies } = await client.send('Network.getAllCookies'); | |
| await client.detach(); | |
| for (const c of cookies) { | |
| if (c.name === '__Host-C_OSES') hostCoses = c.value; | |
| if (c.name === '__Secure-C_SES') secureCSes = c.value; | |
| } | |
| if (hostCoses && secureCSes) break; | |
| await sleep(1000); | |
| if (i === 5) await page.goto('https://business.gemini.google/', { waitUntil: 'networkidle2' }).catch(() => {}); | |
| } | |
| if (!hostCoses || !secureCSes) throw new Error('Cookie 提取失败'); | |
| // 提取 csesidx 和 team_id | |
| let finalUrl = page.url(); | |
| let csesidx = '', team_id = ''; | |
| addLog('INFO', `当前页面 URL: ${finalUrl}`, email); | |
| // 尝试从 URL 提取 | |
| try { | |
| const urlObj = new URL(finalUrl); | |
| csesidx = urlObj.searchParams.get('csesidx') || ''; | |
| // 重要发现:cid 就是 team_id! | |
| // 从 csesidx 参数可以推断出对应的 cid (team_id) | |
| if (csesidx) { | |
| // 先尝试从 URL 路径提取 cid | |
| const cidMatch = finalUrl.match(/\/cid\/([a-f0-9-]+)(?:\?|$)/); | |
| if (cidMatch) { | |
| team_id = cidMatch[1]; | |
| addLog('INFO', `✓ 从URL路径提取到 team_id (cid): ${team_id}`, email); | |
| } | |
| // 如果URL路径没有cid,csesidx本身可能就包含了team_id信息 | |
| // 或者我们需要通过其他方式获取 | |
| if (!team_id) { | |
| // 尝试从 URL 参数提取 | |
| team_id = urlObj.searchParams.get('team_id') || urlObj.searchParams.get('teamId') || urlObj.searchParams.get('cid') || ''; | |
| } | |
| } | |
| } catch (e) { | |
| addLog('WARN', `URL解析失败: ${e.message}`, email); | |
| } | |
| // 如果还没有 team_id,尝试访问主页获取 | |
| if (!team_id) { | |
| addLog('INFO', '尝试从主页获取 team_id...', email); | |
| try { | |
| await page.goto('https://business.gemini.google/', { waitUntil: 'networkidle2', timeout: 30000 }); | |
| await sleep(3000); | |
| finalUrl = page.url(); | |
| addLog('INFO', `主页 URL: ${finalUrl}`, email); | |
| // 从主页 URL 提取 team_id(支持完整的UUID格式) | |
| const cidMatch = finalUrl.match(/\/cid\/([a-f0-9-]+)(?:\?|$)/); | |
| if (cidMatch) { | |
| team_id = cidMatch[1]; | |
| addLog('INFO', `✓ 从主页URL提取到 team_id: ${team_id}`, email); | |
| } | |
| // 如果还没有 team_id,尝试访问主页获取 | |
| if (!team_id) { | |
| addLog('INFO', '尝试从主页获取 team_id...', email); | |
| try { | |
| await page.goto('https://business.gemini.google/', { waitUntil: 'networkidle2', timeout: 30000 }); | |
| // 增加等待时间,让页面完全加载和初始化 | |
| addLog('INFO', '等待主页完全加载 (15秒)...', email); | |
| await sleep(15000); | |
| finalUrl = page.url(); | |
| addLog('INFO', `主页 URL: ${finalUrl}`, email); | |
| // 从主页 URL 提取 team_id | |
| const cidMatch = finalUrl.match(/\/cid\/([a-f0-9-]+)(?:\?|$)/); | |
| if (cidMatch) { | |
| team_id = cidMatch[1]; | |
| addLog('INFO', `✓ 从主页URL提取到 team_id: ${team_id}`, email); | |
| } | |
| // 如果主页没有重定向到聊天界面,尝试多种方式进入聊天 | |
| if (!team_id) { | |
| addLog('INFO', '尝试进入聊天界面获取 team_id...', email); | |
| // 方式1: 尝试直接访问聊天入口 | |
| const chatUrls = [ | |
| 'https://business.gemini.google/chat', | |
| 'https://business.gemini.google/app', | |
| 'https://business.gemini.google/workspace' | |
| ]; | |
| for (const chatUrl of chatUrls) { | |
| try { | |
| addLog('INFO', `尝试访问: ${chatUrl}`, email); | |
| await page.goto(chatUrl, { waitUntil: 'networkidle2', timeout: 15000 }); | |
| await sleep(3000); | |
| const newUrl = page.url(); | |
| addLog('INFO', `访问后 URL: ${newUrl}`, email); | |
| const newCidMatch = newUrl.match(/\/cid\/([a-f0-9-]+)(?:\?|$)/); | |
| if (newCidMatch) { | |
| team_id = newCidMatch[1]; | |
| addLog('INFO', `✓ 从聊天界面提取到 team_id: ${team_id}`, email); | |
| break; | |
| } | |
| } catch (e) { | |
| addLog('WARN', `访问 ${chatUrl} 失败: ${e.message}`, email); | |
| } | |
| } | |
| // 方式2: 如果还没有,回到主页尝试点击按钮 | |
| if (!team_id) { | |
| addLog('INFO', '回到主页尝试点击聊天按钮...', email); | |
| await page.goto('https://business.gemini.google/', { waitUntil: 'networkidle2', timeout: 30000 }); | |
| await sleep(3000); | |
| // 尝试点击各种可能的按钮 | |
| const buttonSelectors = [ | |
| 'button[data-testid="start-chat"]', | |
| 'button:has-text("开始聊天")', | |
| 'button:has-text("Start chat")', | |
| 'button:has-text("Chat")', | |
| 'button:has-text("开始")', | |
| 'a[href*="/cid/"]', | |
| 'a[href*="/chat"]', | |
| '[data-testid="chat-button"]', | |
| '.chat-button', | |
| '[role="button"]:has-text("Chat")', | |
| 'button[aria-label*="chat"]', | |
| 'button[aria-label*="Chat"]' | |
| ]; | |
| for (const selector of buttonSelectors) { | |
| try { | |
| const elements = await page.$$(selector); | |
| if (elements.length > 0) { | |
| addLog('INFO', `找到 ${elements.length} 个匹配元素: ${selector}`, email); | |
| for (let i = 0; i < elements.length; i++) { | |
| try { | |
| const element = elements[i]; | |
| const text = await element.textContent(); | |
| addLog('INFO', `尝试点击元素 ${i + 1}: "${text}"`, email); | |
| await element.click(); | |
| await sleep(5000); | |
| const newUrl = page.url(); | |
| addLog('INFO', `点击后 URL: ${newUrl}`, email); | |
| const newCidMatch = newUrl.match(/\/cid\/([a-f0-9-]+)(?:\?|$)/); | |
| if (newCidMatch) { | |
| team_id = newCidMatch[1]; | |
| addLog('INFO', `✓ 从聊天界面提取到 team_id: ${team_id}`, email); | |
| break; | |
| } | |
| } catch (e) { | |
| addLog('WARN', `点击元素失败: ${e.message}`, email); | |
| } | |
| } | |
| if (team_id) break; | |
| } | |
| } catch (e) { | |
| // 继续尝试下一个选择器 | |
| } | |
| } | |
| } | |
| } | |
| // 如果还没有,尝试从页面内容提取 | |
| if (!team_id) { | |
| const pageContent = await page.content(); | |
| // 尝试多种模式匹配(支持完整UUID格式) | |
| const patterns = [ | |
| /"teamId"\s*:\s*"?([a-f0-9-]+)"?/i, | |
| /"team_id"\s*:\s*"?([a-f0-9-]+)"?/i, | |
| /teamId["\']?\s*[:=]\s*["\']?([a-f0-9-]+)/i, | |
| /team_id["\']?\s*[:=]\s*["\']?([a-f0-9-]+)/i, | |
| /\/cid\/([a-f0-9-]+)/i, | |
| /cid[=:]([a-f0-9-]+)/i, | |
| /"configId"\s*:\s*"?([a-f0-9-]+)"?/i, | |
| /"config_id"\s*:\s*"?([a-f0-9-]+)"?/i, | |
| /data-team-id["\']?\s*[:=]\s*["\']?([a-f0-9-]+)/i, | |
| /teamid["\']?\s*[:=]\s*["\']?([a-f0-9-]+)/i | |
| ]; | |
| for (const pattern of patterns) { | |
| const match = pageContent.match(pattern); | |
| if (match) { | |
| team_id = match[1]; | |
| addLog('INFO', `✓ 从页面内容提取到 team_id: ${team_id} (模式: ${pattern.source})`, email); | |
| break; | |
| } | |
| } | |
| } | |
| } catch (e) { | |
| addLog('WARN', `获取 team_id 失败: ${e.message}`, email); | |
| } | |
| } | |
| // 如果还没有,尝试从页面内容提取 | |
| if (!team_id) { | |
| const pageContent = await page.content(); | |
| // 尝试多种模式匹配 | |
| const patterns = [ | |
| /"teamId"\s*:\s*"?(\d+)"?/, | |
| /"team_id"\s*:\s*"?(\d+)"?/, | |
| /teamId["\']?\s*[:=]\s*["\']?(\d+)/, | |
| /team_id["\']?\s*[:=]\s*["\']?(\d+)/, | |
| /\/cid\/(\d+)/, | |
| /cid[=:](\d+)/, | |
| /"configId"\s*:\s*"?(\d+)"?/, | |
| /"config_id"\s*:\s*"?(\d+)"?/ | |
| ]; | |
| for (const pattern of patterns) { | |
| const match = pageContent.match(pattern); | |
| if (match) { | |
| team_id = match[1]; | |
| addLog('INFO', `✓ 从页面内容提取到 team_id: ${team_id} (模式: ${pattern.source})`, email); | |
| break; | |
| } | |
| } | |
| } | |
| } catch (e) { | |
| addLog('WARN', `获取 team_id 失败: ${e.message}`, email); | |
| } | |
| } | |
| // 如果还是没有,尝试从当前页面内容提取 | |
| if (!team_id) { | |
| try { | |
| addLog('INFO', '尝试从当前页面内容提取 team_id...', email); | |
| const currentPageContent = await page.content(); | |
| const patterns = [ | |
| /"teamId"\s*:\s*"?([a-f0-9-]+)"?/i, | |
| /"team_id"\s*:\s*"?([a-f0-9-]+)"?/i, | |
| /teamId["\']?\s*[:=]\s*["\']?([a-f0-9-]+)/i, | |
| /team_id["\']?\s*[:=]\s*["\']?([a-f0-9-]+)/i, | |
| /team[_-]?id["\']?\s*[:=]\s*["\']?([a-f0-9-]+)/i, | |
| /"configId"\s*:\s*"?([a-f0-9-]+)"?/i, | |
| /"config_id"\s*:\s*"?([a-f0-9-]+)"?/i | |
| ]; | |
| for (const pattern of patterns) { | |
| const match = currentPageContent.match(pattern); | |
| if (match) { | |
| team_id = match[1]; | |
| addLog('INFO', `✓ 从当前页面提取到 team_id: ${team_id} (模式: ${pattern.source})`, email); | |
| break; | |
| } | |
| } | |
| } catch (e) { | |
| addLog('WARN', `从页面内容提取 team_id 失败: ${e.message}`, email); | |
| } | |
| } | |
| addLog('INFO', `提取结果 - csesidx: ${csesidx}, team_id: ${team_id}`, email); | |
| addLog('SUCCESS', '登录成功!', email); | |
| // 返回 business-gemini 格式的 Cookie 数据 | |
| return { | |
| success: true, | |
| email, | |
| password, | |
| // 兼容旧格式 | |
| cookies: `${hostCoses.trim()}::${secureCSes.trim()}`, | |
| // business-gemini 格式 | |
| cookieData: { | |
| secure_c_ses: secureCSes.trim(), | |
| host_c_oses: hostCoses.trim(), | |
| csesidx, | |
| team_id | |
| } | |
| }; | |
| } catch (error) { | |
| addLog('ERROR', `登录失败: ${error.message}`, email); | |
| return { success: false, error: error.message }; | |
| } finally { | |
| if (browser) await browser.close(); | |
| try { fs.rmSync(userDataDir, { recursive: true, force: true }); } catch (e) {} | |
| } | |
| } | |
| // =============================== | |
| // 自动注册单个账号 | |
| // =============================== | |
| async function registerSingleAccount(accountIndex) { | |
| try { | |
| // 1. 创建临时邮箱 | |
| addLog('INFO', `[注册 ${accountIndex + 1}] 创建临时邮箱...`); | |
| const mailbox = await createTempMailbox(); | |
| // 2. 登录获取 Cookie | |
| addLog('INFO', `[注册 ${accountIndex + 1}] 开始登录: ${mailbox.email}`); | |
| const loginResult = await runLoginTask( | |
| mailbox.email, | |
| '', | |
| config.yesCaptcha.apiKey, | |
| { jwtUrl: mailbox.jwtUrl } | |
| ); | |
| if (!loginResult.success || !loginResult.cookieData) { | |
| throw new Error(loginResult.error || '登录失败'); | |
| } | |
| // 3. 推送到 Business Gemini - 获取正确的账号ID | |
| // 先同步现有账号,获取最新的账号列表 | |
| const syncResult = await syncBusinessGeminiAccounts(); | |
| let pushAccountId; | |
| if (syncResult.success && syncResult.accounts) { | |
| // 找到最大的账号ID,然后+1 | |
| const maxId = syncResult.accounts.reduce((max, acc, index) => { | |
| return Math.max(max, index); | |
| }, -1); | |
| pushAccountId = maxId + 1; | |
| } else { | |
| // 如果同步失败,使用本地账号数量作为ID | |
| pushAccountId = runtimeAccounts.length; | |
| } | |
| // ✅ 验证 team_id 是否存在,如果没有则跳过推送 | |
| if (!loginResult.cookieData.team_id || loginResult.cookieData.team_id.trim() === '') { | |
| addLog('WARN', `[注册 ${accountIndex + 1}] team_id 为空,跳过推送到后台`, mailbox.email); | |
| addLog('SUCCESS', `[注册 ${accountIndex + 1}] 完成: ${mailbox.email} (未推送,team_id缺失)`, mailbox.email); | |
| return { success: true, skipped: true, reason: 'team_id_missing' }; | |
| } | |
| addLog('INFO', `[注册 ${accountIndex + 1}] 推送 Cookie 到后台 (ID: ${pushAccountId})`, mailbox.email); | |
| const pushResult = await pushToBusinessGemini(loginResult.cookieData, { | |
| url: config.businessGemini.url, | |
| adminPassword: config.businessGemini.adminPassword, | |
| accountId: pushAccountId | |
| }, mailbox.email, { jwtUrl: mailbox.jwtUrl }); | |
| // 4. 添加到本地账号列表 | |
| const newAccount = { | |
| email: mailbox.email, | |
| mailJwtUrl: mailbox.jwtUrl, | |
| accountId: pushAccountId, | |
| captchaKey: '', | |
| createdAt: new Date().toISOString() | |
| }; | |
| return { success: true, account: newAccount, pushResult }; | |
| } catch (error) { | |
| addLog('ERROR', `[注册 ${accountIndex + 1}] 失败: ${error.message}`); | |
| return { success: false, error: error.message }; | |
| } | |
| } | |
| // =============================== | |
| // 批量自动注册 | |
| // =============================== | |
| async function runAutoRegister(count) { | |
| if (registerStatus.running) { | |
| addLog('WARN', '注册任务正在运行中'); | |
| return; | |
| } | |
| registerStatus = { running: true, total: count, completed: 0, results: [] }; | |
| addLog('INFO', `开始批量注册 ${count} 个账号`); | |
| for (let i = 0; i < count; i++) { | |
| try { | |
| const result = await registerSingleAccount(i); | |
| registerStatus.completed++; | |
| if (result.success) { | |
| if (result.skipped) { | |
| // 跳过的账号(team_id缺失) | |
| registerStatus.results.push({ | |
| index: i + 1, | |
| success: true, | |
| skipped: true, | |
| reason: result.reason, | |
| message: 'team_id缺失,跳过推送' | |
| }); | |
| addLog('SUCCESS', `[注册 ${i + 1}/${count}] 完成: ${result.email || '未知邮箱'} (跳过推送)`); | |
| } else { | |
| // 正常成功的账号 - 不再添加到本地列表,因为已推送到后台 | |
| registerStatus.results.push({ | |
| index: i + 1, | |
| success: true, | |
| skipped: false, | |
| email: result.account.email, | |
| accountId: result.account.accountId, | |
| message: '注册成功' | |
| }); | |
| addLog('SUCCESS', `[注册 ${i + 1}/${count}] 完成: ${result.account.email}`); | |
| } | |
| } else { | |
| registerStatus.results.push({ | |
| index: i + 1, | |
| success: false, | |
| skipped: false, | |
| message: result.error | |
| }); | |
| } | |
| if (i < count - 1) { | |
| addLog('INFO', `等待 ${config.interval.register} 秒后注册下一个账号...`); | |
| await sleep(config.interval.register * 1000); | |
| } | |
| } catch (error) { | |
| registerStatus.completed++; | |
| registerStatus.results.push({ | |
| index: i + 1, | |
| success: false, | |
| message: error.message | |
| }); | |
| } | |
| } | |
| const successCount = registerStatus.results.filter(r => r.success && !r.skipped).length; | |
| const skippedCount = registerStatus.results.filter(r => r.success && r.skipped).length; | |
| const failedCount = registerStatus.results.filter(r => !r.success).length; | |
| let summaryMsg = `批量注册完成: ${successCount}/${count} 成功`; | |
| if (skippedCount > 0) { | |
| summaryMsg += `, ${skippedCount} 跳过(team_id缺失)`; | |
| } | |
| if (failedCount > 0) { | |
| summaryMsg += `, ${failedCount} 失败`; | |
| } | |
| addLog('INFO', summaryMsg); | |
| registerStatus.running = false; | |
| } | |
| // =============================== | |
| // 自动刷新任务 | |
| // =============================== | |
| async function runAutoRefresh() { | |
| if (refreshStatus.running) { | |
| addLog('WARN', '刷新任务正在运行中,跳过'); | |
| return; | |
| } | |
| const accounts = runtimeAccounts; | |
| if (!accounts || accounts.length === 0) { | |
| addLog('WARN', '未配置自动刷新账号'); | |
| return; | |
| } | |
| refreshStatus.running = true; | |
| refreshStatus.lastResult = []; | |
| lastRefreshTime = new Date(); | |
| addLog('INFO', `开始自动刷新 Cookie (${accounts.length} 个账号)`); | |
| for (let i = 0; i < accounts.length; i++) { | |
| const account = accounts[i]; | |
| try { | |
| addLog('INFO', `处理账号 ${i + 1}/${accounts.length}`, account.email); | |
| const result = await runLoginTask( | |
| account.email, | |
| account.password || '', | |
| account.captchaKey || config.yesCaptcha.apiKey, | |
| { jwtUrl: account.mailJwtUrl } | |
| ); | |
| if (result.success && result.cookieData) { | |
| const pushResult = await pushToBusinessGemini(result.cookieData, { | |
| url: config.businessGemini.url, | |
| adminPassword: config.businessGemini.adminPassword, | |
| accountId: account.accountId ?? i | |
| }, account.email); | |
| refreshStatus.lastResult.push({ | |
| email: account.email, | |
| accountId: account.accountId ?? i, | |
| success: pushResult.success, | |
| message: pushResult.success ? '刷新并推送成功' : pushResult.error, | |
| time: new Date().toISOString() | |
| }); | |
| } else { | |
| refreshStatus.lastResult.push({ | |
| email: account.email, | |
| accountId: account.accountId ?? i, | |
| success: false, | |
| message: result.error || '登录失败', | |
| time: new Date().toISOString() | |
| }); | |
| } | |
| } catch (error) { | |
| addLog('ERROR', `处理失败: ${error.message}`, account.email); | |
| refreshStatus.lastResult.push({ | |
| email: account.email, | |
| accountId: account.accountId ?? i, | |
| success: false, | |
| message: error.message, | |
| time: new Date().toISOString() | |
| }); | |
| } | |
| if (i < accounts.length - 1) { | |
| addLog('INFO', `等待 ${config.interval.refresh} 秒后处理下一个账号...`); | |
| await sleep(config.interval.refresh * 1000); | |
| } | |
| } | |
| addLog('INFO', '自动刷新完成'); | |
| refreshStatus.running = false; | |
| } | |
| // 自动刷新后台过期账号 | |
| async function runAutoRefreshExpiredAccounts() { | |
| if (refreshStatus.running) { | |
| return { success: false, message: '刷新任务正在运行中' }; | |
| } | |
| addLog('INFO', '开始检测后台过期账号...'); | |
| const syncResult = await syncBusinessGeminiAccounts(); | |
| if (!syncResult.success || !syncResult.accounts) { | |
| addLog('ERROR', '同步后台账号失败'); | |
| return { success: false, message: '同步后台账号失败' }; | |
| } | |
| const backendAccounts = syncResult.accounts; | |
| const expiredAccounts = backendAccounts.filter(acc => acc.available === false || acc.cookie_expired === true); | |
| if (expiredAccounts.length === 0) { | |
| addLog('INFO', '没有发现过期账号'); | |
| return { success: true, message: '没有过期账号需要刷新', refreshed: 0 }; | |
| } | |
| addLog('INFO', `发现 ${expiredAccounts.length} 个过期账号,开始刷新...`); | |
| refreshStatus.running = true; | |
| refreshStatus.lastResult = []; | |
| lastRefreshTime = new Date(); | |
| let successCount = 0; | |
| let failCount = 0; | |
| for (let i = 0; i < expiredAccounts.length; i++) { | |
| const account = expiredAccounts[i]; | |
| const accountIndex = backendAccounts.indexOf(account); | |
| if (!account.tempmail_name || !account.tempmail_url) { | |
| addLog('WARN', `账号 ${accountIndex} 缺少邮箱信息,跳过`); | |
| failCount++; | |
| continue; | |
| } | |
| try { | |
| addLog('INFO', `[${i + 1}/${expiredAccounts.length}] 刷新账号 ${accountIndex}: ${account.tempmail_name}`); | |
| const result = await runLoginTask(account.tempmail_name, '', config.yesCaptcha.apiKey, { jwtUrl: account.tempmail_url }); | |
| if (result.success && result.cookieData) { | |
| const pushResult = await pushToBusinessGemini(result.cookieData, { | |
| url: config.businessGemini.url, | |
| adminPassword: config.businessGemini.adminPassword, | |
| accountId: accountIndex | |
| }, account.tempmail_name, { jwtUrl: account.tempmail_url }); | |
| if (pushResult.success) { | |
| addLog('SUCCESS', `账号 ${accountIndex} 刷新成功`, account.tempmail_name); | |
| successCount++; | |
| // 从本地管理列表中移除已成功刷新的账号 | |
| const localIndex = runtimeAccounts.findIndex(a => a.email === account.tempmail_name); | |
| if (localIndex !== -1) { | |
| runtimeAccounts.splice(localIndex, 1); | |
| saveAccounts(); | |
| addLog('INFO', `已从本地管理移除: ${account.tempmail_name}`); | |
| } | |
| } else { | |
| addLog('ERROR', `账号 ${accountIndex} 推送失败`, account.tempmail_name); | |
| failCount++; | |
| } | |
| } else { | |
| addLog('ERROR', `账号 ${accountIndex} 登录失败`, account.tempmail_name); | |
| failCount++; | |
| } | |
| } catch (error) { | |
| addLog('ERROR', `账号 ${accountIndex} 处理失败: ${error.message}`, account.tempmail_name); | |
| failCount++; | |
| } | |
| if (i < expiredAccounts.length - 1) { | |
| addLog('INFO', `等待 ${config.interval.refresh} 秒后处理下一个账号...`); | |
| await sleep(config.interval.refresh * 1000); | |
| } | |
| } | |
| refreshStatus.running = false; | |
| const summary = `过期账号刷新完成: ${successCount}/${expiredAccounts.length} 成功, ${failCount} 失败`; | |
| addLog('INFO', summary); | |
| return { success: true, message: summary, total: expiredAccounts.length, refreshed: successCount, failed: failCount }; | |
| } | |
| // =============================== | |
| // API 路由 | |
| // =============================== | |
| // 账号管理 | |
| app.get('/api/accounts', (req, res) => { | |
| res.json({ accounts: runtimeAccounts }); | |
| }); | |
| app.post('/api/accounts', (req, res) => { | |
| const { email, mailJwtUrl, accountId, captchaKey } = req.body; | |
| if (!email) return res.status(400).json({ error: '邮箱不能为空' }); | |
| const exists = runtimeAccounts.find(a => a.email === email); | |
| if (exists) return res.status(400).json({ error: '账号已存在' }); | |
| const newAccount = { | |
| email, | |
| mailJwtUrl: mailJwtUrl || '', | |
| accountId: accountId ?? runtimeAccounts.length, | |
| captchaKey: captchaKey || '', | |
| createdAt: new Date().toISOString() | |
| }; | |
| runtimeAccounts.push(newAccount); | |
| saveAccounts(); | |
| addLog('INFO', `添加账号: ${email}`); | |
| res.json({ success: true, account: newAccount }); | |
| }); | |
| app.put('/api/accounts/:index', (req, res) => { | |
| const index = parseInt(req.params.index); | |
| if (index < 0 || index >= runtimeAccounts.length) { | |
| return res.status(404).json({ error: '账号不存在' }); | |
| } | |
| const { email, mailJwtUrl, accountId, captchaKey } = req.body; | |
| runtimeAccounts[index] = { | |
| ...runtimeAccounts[index], | |
| email: email || runtimeAccounts[index].email, | |
| mailJwtUrl: mailJwtUrl ?? runtimeAccounts[index].mailJwtUrl, | |
| accountId: accountId ?? runtimeAccounts[index].accountId, | |
| captchaKey: captchaKey ?? runtimeAccounts[index].captchaKey | |
| }; | |
| saveAccounts(); | |
| addLog('INFO', `更新账号: ${runtimeAccounts[index].email}`); | |
| res.json({ success: true, account: runtimeAccounts[index] }); | |
| }); | |
| app.delete('/api/accounts/:index', (req, res) => { | |
| const index = parseInt(req.params.index); | |
| if (index < 0 || index >= runtimeAccounts.length) { | |
| return res.status(404).json({ error: '账号不存在' }); | |
| } | |
| const removed = runtimeAccounts.splice(index, 1)[0]; | |
| saveAccounts(); | |
| addLog('INFO', `删除账号: ${removed.email}`); | |
| res.json({ success: true, removed }); | |
| }); | |
| // 注册相关 | |
| app.get('/api/register-status', (req, res) => { | |
| res.json(registerStatus); | |
| }); | |
| app.post('/api/register', async (req, res) => { | |
| const { count } = req.body; | |
| const registerCount = parseInt(count) || 1; | |
| if (registerCount < 1 || registerCount > 10) { | |
| return res.status(400).json({ error: '注册数量必须在 1-10 之间' }); | |
| } | |
| if (registerStatus.running) { | |
| return res.json({ success: false, message: '注册任务正在运行中' }); | |
| } | |
| if (!config.mail.tempMailUrl) { | |
| return res.status(400).json({ error: '未配置临时邮箱服务地址 (TEMP_MAIL_URL)' }); | |
| } | |
| if (!config.yesCaptcha.apiKey) { | |
| return res.status(400).json({ error: '未配置 YesCaptcha API Key' }); | |
| } | |
| res.json({ success: true, message: `开始注册 ${registerCount} 个账号` }); | |
| runAutoRegister(registerCount); | |
| }); | |
| // Business Gemini 同步 | |
| app.get('/api/business-gemini/accounts', async (req, res) => { | |
| const result = await syncBusinessGeminiAccounts(); | |
| res.json(result); | |
| }); | |
| // 从后台同步过期账号到本地(只同步过期的、有邮箱URL的账号) | |
| app.post('/api/sync-expired-accounts', async (req, res) => { | |
| const syncResult = await syncBusinessGeminiAccounts(); | |
| if (!syncResult.success || !syncResult.accounts) { | |
| return res.json({ success: false, error: '同步后台账号失败' }); | |
| } | |
| const backendAccounts = syncResult.accounts; | |
| let addedCount = 0; | |
| let noUrlCount = 0; | |
| let alreadyExistsCount = 0; | |
| // 只同步过期的账号 | |
| for (let i = 0; i < backendAccounts.length; i++) { | |
| const acc = backendAccounts[i]; | |
| // 只处理过期账号 | |
| if (acc.available !== false && acc.cookie_expired !== true) { | |
| continue; | |
| } | |
| // 必须有邮箱信息 | |
| if (!acc.tempmail_name || !acc.tempmail_url) { | |
| noUrlCount++; | |
| continue; | |
| } | |
| // 检查是否已存在 | |
| const exists = runtimeAccounts.find(a => a.email === acc.tempmail_name); | |
| if (exists) { | |
| alreadyExistsCount++; | |
| continue; | |
| } | |
| runtimeAccounts.push({ | |
| email: acc.tempmail_name, | |
| mailJwtUrl: acc.tempmail_url, | |
| accountId: i, | |
| captchaKey: '' | |
| }); | |
| addedCount++; | |
| } | |
| saveAccounts(); | |
| addLog('INFO', `同步过期账号: 新增 ${addedCount} 个, 缺少邮箱URL ${noUrlCount} 个, 已存在 ${alreadyExistsCount} 个`); | |
| res.json({ success: true, added: addedCount, noUrl: noUrlCount, alreadyExists: alreadyExistsCount }); | |
| }); | |
| // 日志 | |
| app.get('/api/logs', (req, res) => { | |
| // 在返回日志前先清理旧日志 | |
| cleanupOldLogs(); | |
| const limit = parseInt(req.query.limit) || 50; | |
| res.json({ | |
| logs: logs.slice(0, limit), | |
| total: logs.length, | |
| retention_hours: LOG_RETENTION_HOURS | |
| }); | |
| }); | |
| app.delete('/api/logs', (req, res) => { | |
| logs = []; | |
| res.json({ success: true }); | |
| }); | |
| // 刷新状态 | |
| app.get('/api/refresh-status', (req, res) => { | |
| res.json({ | |
| running: refreshStatus.running, | |
| lastRefreshTime: lastRefreshTime ? lastRefreshTime.toISOString() : null, | |
| lastResult: refreshStatus.lastResult, | |
| accountCount: runtimeAccounts.length | |
| }); | |
| }); | |
| // 定时任务状态 | |
| app.get('/api/schedule-status', (req, res) => { | |
| res.json({ | |
| register: { | |
| enabled: config.schedule.registerIntervalHours > 0, | |
| intervalHours: config.schedule.registerIntervalHours, | |
| count: config.schedule.registerCount, | |
| lastRun: lastScheduledRegisterTime ? lastScheduledRegisterTime.toISOString() : null | |
| }, | |
| refresh: { | |
| enabled: config.schedule.refreshIntervalHours > 0, | |
| intervalHours: config.schedule.refreshIntervalHours, | |
| lastRun: lastScheduledRefreshTime ? lastScheduledRefreshTime.toISOString() : null | |
| } | |
| }); | |
| }); | |
| app.post('/api/trigger-refresh', async (req, res) => { | |
| if (refreshStatus.running) { | |
| return res.json({ success: false, message: '刷新任务正在运行中' }); | |
| } | |
| res.json({ success: true, message: '刷新任务已触发' }); | |
| runAutoRefresh(); | |
| }); | |
| // 触发刷新后台过期账号 | |
| app.post('/api/trigger-refresh-expired', async (req, res) => { | |
| if (refreshStatus.running) { | |
| return res.json({ success: false, message: '刷新任务正在运行中' }); | |
| } | |
| res.json({ success: true, message: '过期账号刷新任务已触发' }); | |
| runAutoRefreshExpiredAccounts().catch(err => { | |
| addLog('ERROR', `自动刷新过期账号失败: ${err.message}`); | |
| }); | |
| }); | |
| // 单个账号刷新 | |
| app.post('/api/refresh/:index', async (req, res) => { | |
| const index = parseInt(req.params.index); | |
| if (index < 0 || index >= runtimeAccounts.length) { | |
| return res.status(404).json({ error: '账号不存在' }); | |
| } | |
| const account = runtimeAccounts[index]; | |
| addLog('INFO', `手动刷新账号`, account.email); | |
| try { | |
| const result = await runLoginTask( | |
| account.email, | |
| account.password || '', | |
| account.captchaKey || config.yesCaptcha.apiKey, | |
| { jwtUrl: account.mailJwtUrl } | |
| ); | |
| if (result.success && result.cookieData) { | |
| const pushResult = await pushToBusinessGemini(result.cookieData, { | |
| url: config.businessGemini.url, | |
| adminPassword: config.businessGemini.adminPassword, | |
| accountId: account.accountId ?? index | |
| }, account.email); | |
| res.json({ success: true, result, pushResult }); | |
| } else { | |
| res.json({ success: false, error: result.error }); | |
| } | |
| } catch (error) { | |
| res.json({ success: false, error: error.message }); | |
| } | |
| }); | |
| // 手动登录 | |
| app.post('/api/login', async (req, res) => { | |
| const { email, password = '', captchaKey, mailJwtUrl, autoPush, accountId } = req.body; | |
| if (!email) return res.status(400).json({ success: false, error: '请提供邮箱' }); | |
| const finalCaptchaKey = captchaKey || config.yesCaptcha.apiKey; | |
| if (!finalCaptchaKey) return res.status(400).json({ success: false, error: '请提供 YesCaptcha API Key' }); | |
| const mailConfig = mailJwtUrl ? { jwtUrl: mailJwtUrl } : null; | |
| if (!mailConfig) { | |
| return res.status(400).json({ success: false, error: '请提供邮件 JWT URL' }); | |
| } | |
| try { | |
| const result = await runLoginTask(email, password, finalCaptchaKey, mailConfig); | |
| if (result.success && result.cookieData && autoPush !== false) { | |
| result.pushResult = await pushToBusinessGemini(result.cookieData, { | |
| url: config.businessGemini.url, | |
| adminPassword: config.businessGemini.adminPassword, | |
| accountId: accountId ?? config.businessGemini.accountId | |
| }, email); | |
| } | |
| res.json(result); | |
| } catch (err) { | |
| res.status(500).json({ success: false, error: err.message }); | |
| } | |
| }); | |
| app.get('/health', (req, res) => { | |
| res.json({ | |
| status: 'ok', | |
| accounts: runtimeAccounts.length, | |
| config: { | |
| tempMailUrl: !!config.mail.tempMailUrl, | |
| businessGeminiUrl: !!config.businessGemini.url, | |
| yesCaptchaKey: !!config.yesCaptcha.apiKey | |
| } | |
| }); | |
| }); | |
| // =============================== | |
| // 运行时配置 API | |
| // =============================== | |
| app.get('/api/runtime-config', (req, res) => { | |
| res.json({ | |
| success: true, | |
| config: { | |
| registerIntervalSeconds: runtimeConfig.registerIntervalSeconds, | |
| refreshIntervalSeconds: runtimeConfig.refreshIntervalSeconds, | |
| scheduleRegisterHours: runtimeConfig.scheduleRegisterHours, | |
| scheduleRegisterCount: runtimeConfig.scheduleRegisterCount, | |
| scheduleRefreshHours: runtimeConfig.scheduleRefreshHours, | |
| // 服务配置 | |
| tempMailUrl: config.mail.tempMailUrl || '', | |
| businessGeminiUrl: config.businessGemini.url || '', | |
| businessGeminiPassword: config.businessGemini.adminPassword ? '******' : '', | |
| yesCaptchaApiKey: config.yesCaptcha.apiKey ? '******' : '' | |
| } | |
| }); | |
| }); | |
| app.put('/api/runtime-config', (req, res) => { | |
| const { registerIntervalSeconds, refreshIntervalSeconds, scheduleRegisterHours, scheduleRegisterCount, scheduleRefreshHours, tempMailUrl, businessGeminiUrl, businessGeminiPassword, yesCaptchaApiKey } = req.body; | |
| // 更新运行时配置 | |
| if (registerIntervalSeconds !== undefined) { | |
| runtimeConfig.registerIntervalSeconds = Math.max(10, parseInt(registerIntervalSeconds) || 60); | |
| config.interval.register = runtimeConfig.registerIntervalSeconds; | |
| } | |
| if (refreshIntervalSeconds !== undefined) { | |
| runtimeConfig.refreshIntervalSeconds = Math.max(10, parseInt(refreshIntervalSeconds) || 30); | |
| config.interval.refresh = runtimeConfig.refreshIntervalSeconds; | |
| } | |
| if (scheduleRegisterHours !== undefined) { | |
| runtimeConfig.scheduleRegisterHours = Math.max(0, parseFloat(scheduleRegisterHours) || 0); | |
| config.schedule.registerIntervalHours = runtimeConfig.scheduleRegisterHours; | |
| } | |
| if (scheduleRegisterCount !== undefined) { | |
| runtimeConfig.scheduleRegisterCount = Math.max(1, Math.min(10, parseInt(scheduleRegisterCount) || 1)); | |
| config.schedule.registerCount = runtimeConfig.scheduleRegisterCount; | |
| } | |
| if (scheduleRefreshHours !== undefined) { | |
| runtimeConfig.scheduleRefreshHours = Math.max(0, parseFloat(scheduleRefreshHours) || 0); | |
| config.schedule.refreshIntervalHours = runtimeConfig.scheduleRefreshHours; | |
| } | |
| // 服务配置(只有非空且非占位符时才更新) | |
| if (tempMailUrl !== undefined && tempMailUrl !== '') { | |
| runtimeConfig.tempMailUrl = tempMailUrl; | |
| config.mail.tempMailUrl = tempMailUrl; | |
| } | |
| if (businessGeminiUrl !== undefined && businessGeminiUrl !== '') { | |
| runtimeConfig.businessGeminiUrl = businessGeminiUrl; | |
| config.businessGemini.url = businessGeminiUrl; | |
| } | |
| if (businessGeminiPassword !== undefined && businessGeminiPassword !== '' && businessGeminiPassword !== '******') { | |
| runtimeConfig.businessGeminiPassword = businessGeminiPassword; | |
| config.businessGemini.adminPassword = businessGeminiPassword; | |
| } | |
| if (yesCaptchaApiKey !== undefined && yesCaptchaApiKey !== '' && yesCaptchaApiKey !== '******') { | |
| runtimeConfig.yesCaptchaApiKey = yesCaptchaApiKey; | |
| config.yesCaptcha.apiKey = yesCaptchaApiKey; | |
| } | |
| // 保存到文件 | |
| saveRuntimeConfig(); | |
| // 重启定时任务 | |
| restartScheduledTasks(); | |
| addLog('INFO', `运行时配置已更新`); | |
| res.json({ | |
| success: true, | |
| config: runtimeConfig | |
| }); | |
| }); | |
| // 重启定时任务 | |
| function restartScheduledTasks() { | |
| // 清除现有定时器 | |
| if (scheduleTimers.register) { | |
| clearInterval(scheduleTimers.register); | |
| scheduleTimers.register = null; | |
| } | |
| if (scheduleTimers.refresh) { | |
| clearInterval(scheduleTimers.refresh); | |
| scheduleTimers.refresh = null; | |
| } | |
| // 重新启动定时任务 | |
| startScheduledTasks(); | |
| } | |
| // =============================== | |
| // 前端页面 | |
| // =============================== | |
| app.get('/', (req, res) => { | |
| res.send(` | |
| <!DOCTYPE html> | |
| <html lang="zh"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Gemini Auto v6.3</title> | |
| <style> | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; } | |
| .container { max-width: 1200px; margin: 0 auto; } | |
| .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; align-items: start; } | |
| @media (max-width: 900px) { .grid { grid-template-columns: 1fr; } } | |
| .card { background: white; border-radius: 16px; padding: 24px; margin-bottom: 20px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); word-wrap: break-word; overflow-wrap: break-word; } | |
| h1 { color: #333; margin-bottom: 8px; font-size: 24px; } | |
| h2 { color: #555; margin-bottom: 15px; font-size: 18px; } | |
| .subtitle { color: #666; margin-bottom: 20px; font-size: 14px; } | |
| .form-group { margin-bottom: 12px; } | |
| label { display: block; margin-bottom: 4px; font-weight: 600; color: #444; font-size: 13px; } | |
| input, select, textarea { width: 100%; padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 13px; } | |
| input:focus, select:focus, textarea:focus { outline: none; border-color: #667eea; } | |
| button { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; padding: 10px 20px; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; margin-right: 8px; margin-bottom: 8px; } | |
| button:hover { transform: translateY(-1px); } | |
| button:disabled { opacity: 0.6; cursor: not-allowed; transform: none; } | |
| button.danger { background: #dc3545; } | |
| button.success { background: #28a745; } | |
| button.secondary { background: #6c757d; } | |
| .status { padding: 6px 12px; border-radius: 12px; font-size: 11px; font-weight: 600; display: inline-block; margin-right: 6px; margin-bottom: 6px; } | |
| .status-ok { background: #d4edda; color: #155724; } | |
| .status-warn { background: #fff3cd; color: #856404; } | |
| .account-list { max-height: 300px; overflow-y: auto; } | |
| .account-item { display: flex; justify-content: space-between; align-items: center; padding: 12px; border: 1px solid #e0e0e0; border-radius: 8px; margin-bottom: 8px; } | |
| .account-item:hover { background: #f8f9fa; } | |
| .account-info { flex: 1; } | |
| .account-email { font-weight: 600; color: #333; } | |
| .account-meta { font-size: 12px; color: #666; margin-top: 4px; } | |
| .account-actions button { padding: 6px 12px; font-size: 12px; margin: 0 2px; } | |
| .log-container { max-height: 400px; overflow-y: auto; background: #1e1e1e; border-radius: 8px; padding: 12px; font-family: monospace; font-size: 12px; word-break: break-all; overflow-wrap: break-word; } | |
| .log-item { padding: 4px 0; border-bottom: 1px solid #333; word-break: break-word; } | |
| .log-time { color: #888; } | |
| .log-level { padding: 2px 6px; border-radius: 4px; font-size: 10px; margin: 0 6px; } | |
| .log-level.INFO { background: #17a2b8; color: white; } | |
| .log-level.SUCCESS { background: #28a745; color: white; } | |
| .log-level.WARN { background: #ffc107; color: black; } | |
| .log-level.ERROR { background: #dc3545; color: white; } | |
| .log-message { color: #ddd; word-break: break-word; } | |
| .log-email { color: #667eea; word-break: break-word; } | |
| .result { background: #f8f9fa; border-radius: 8px; padding: 12px; margin-top: 12px; font-family: monospace; font-size: 12px; white-space: pre-wrap; word-break: break-all; max-height: 200px; overflow-y: auto; } | |
| .result.success { border-left: 4px solid #28a745; } | |
| .result.error { border-left: 4px solid #dc3545; } | |
| .refresh-result { margin-top: 12px; } | |
| .refresh-item { padding: 8px; border-radius: 6px; margin-bottom: 6px; font-size: 13px; } | |
| .refresh-item.success { background: #d4edda; } | |
| .refresh-item.error { background: #f8d7da; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="card"> | |
| <h1>🤖 Gemini Business 自动化 v6</h1> | |
| <p class="subtitle">整合 zeabur-mail + business-gemini + cookie-refresher</p> | |
| <div> | |
| <span class="status ${config.yesCaptcha.apiKey ? 'status-ok' : 'status-warn'}">YesCaptcha: ${config.yesCaptcha.apiKey ? '✓' : '○'}</span> | |
| <span class="status ${config.businessGemini.url ? 'status-ok' : 'status-warn'}">Business Gemini: ${config.businessGemini.url ? '✓' : '○'}</span> | |
| <span class="status ${config.mail.tempMailUrl ? 'status-ok' : 'status-warn'}">临时邮箱: ${config.mail.tempMailUrl ? '✓' : '○'}</span> | |
| </div> | |
| </div> | |
| <div class="grid"> | |
| <div> | |
| <div class="card"> | |
| <h2>🏢 Business Gemini 后台账号</h2> | |
| <div id="bgAccountList" style="margin-bottom: 12px;">加载中...</div> | |
| <button class="secondary" onclick="syncBGAccounts()">🔄 同步账号</button> | |
| <button class="success" onclick="importFromBackend()" style="margin-left: 8px;">📥 导入到本地</button> | |
| </div> | |
| <div class="card"> | |
| <h2>🔴 过期Cookie管理</h2> | |
| <p style="font-size: 11px; color: #666; margin-bottom: 8px;">自动从后台同步过期账号,刷新成功后自动移除</p> | |
| <div id="accountList" class="account-list">加载中...</div> | |
| <button onclick="syncExpiredAccounts()" class="secondary" style="margin-top: 8px;">🔄 同步过期账号</button> | |
| <hr style="margin: 15px 0; border: none; border-top: 1px solid #eee;"> | |
| <h3 style="font-size: 14px; margin-bottom: 10px;">🤖 自动注册账号</h3> | |
| <div class="form-group"> | |
| <label>注册数量 (1-10)</label> | |
| <input type="number" id="registerCount" value="1" min="1" max="10"> | |
| </div> | |
| <button onclick="startRegister()" id="registerBtn">🚀 开始自动注册</button> | |
| <div id="registerStatus" style="margin-top: 12px;"></div> | |
| </div> | |
| <div class="card"> | |
| <h2>🔄 刷新状态</h2> | |
| <div id="refreshStatus">加载中...</div> | |
| <button onclick="triggerRefresh()" style="margin-top: 12px;">🔄 立即刷新全部过期</button> | |
| </div> | |
| <div class="card"> | |
| <h2>⚙️ 系统设置</h2> | |
| <h3 style="font-size: 14px; margin-bottom: 10px;">🔗 服务配置</h3> | |
| <div class="form-group"> | |
| <label>临时邮箱服务地址</label> | |
| <input type="text" id="cfgTempMailUrl" placeholder="https://zeabur-mail.zeabur.app"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Business Gemini 后台地址</label> | |
| <input type="text" id="cfgBusinessGeminiUrl" placeholder="https://business-gemini.zeabur.app"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Business Gemini 密码</label> | |
| <input type="password" id="cfgBusinessGeminiPassword" placeholder="******"> | |
| </div> | |
| <div class="form-group"> | |
| <label>YesCaptcha API Key</label> | |
| <input type="password" id="cfgYesCaptchaApiKey" placeholder="******"> | |
| </div> | |
| <hr style="margin: 15px 0; border: none; border-top: 1px solid #eee;"> | |
| <h3 style="font-size: 14px; margin-bottom: 10px;">⏱️ 操作间隔</h3> | |
| <p style="font-size: 11px; color: #666; margin-bottom: 10px;">设置各操作之间的间隔时间,避免频繁操作被检测</p> | |
| <div class="form-group"> | |
| <label>注册间隔(秒)</label> | |
| <input type="number" id="cfgRegisterInterval" min="10" placeholder="60"> | |
| </div> | |
| <div class="form-group"> | |
| <label>刷新间隔(秒)</label> | |
| <input type="number" id="cfgRefreshInterval" min="10" placeholder="30"> | |
| </div> | |
| <hr style="margin: 15px 0; border: none; border-top: 1px solid #eee;"> | |
| <h3 style="font-size: 14px; margin-bottom: 10px;">⏰ 定时任务</h3> | |
| <div class="form-group"> | |
| <label>定时注册间隔(小时)</label> | |
| <input type="number" id="cfgScheduleRegisterHours" min="0" step="0.1" placeholder="0"> | |
| <small style="color: #888; font-size: 11px;">0 = 禁用,0.5 = 30分钟</small> | |
| </div> | |
| <div class="form-group"> | |
| <label>每次定时注册数量</label> | |
| <input type="number" id="cfgScheduleRegisterCount" min="1" max="10" placeholder="1"> | |
| </div> | |
| <div class="form-group"> | |
| <label>定时刷新间隔(小时)</label> | |
| <input type="number" id="cfgScheduleRefreshHours" min="0" step="0.1" placeholder="0"> | |
| <small style="color: #888; font-size: 11px;">0 = 禁用,0.5 = 30分钟</small> | |
| </div> | |
| <button onclick="saveSettings()" class="success">💾 保存设置</button> | |
| <button onclick="loadSettings()" class="secondary">🔄 重新加载</button> | |
| <div id="settingsStatus" style="margin-top: 8px; font-size: 12px;"></div> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="card"> | |
| <h2>📧 手动登录</h2> | |
| <div class="form-group"> | |
| <label>选择账号</label> | |
| <select id="loginAccountSelect" onchange="onAccountSelect()"> | |
| <option value="">-- 手动输入 --</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label>邮箱地址</label> | |
| <input type="email" id="loginEmail" placeholder="xxx@example.com"> | |
| </div> | |
| <div class="form-group"> | |
| <label>邮件 JWT URL (zeabur-mail)</label> | |
| <input type="text" id="loginMailJwt" placeholder="https://zeabur-mail.../?jwt=..."> | |
| </div> | |
| <div class="form-group"> | |
| <label>YesCaptcha API Key</label> | |
| <input type="text" id="loginCaptchaKey" value="${config.yesCaptcha.apiKey}"> | |
| </div> | |
| <div class="form-group"> | |
| <label>推送账号ID</label> | |
| <input type="number" id="loginAccountId" value="0"> | |
| </div> | |
| <button onclick="doLogin()" id="loginBtn">🚀 开始登录</button> | |
| <div id="loginResult" class="result" style="display:none;"></div> | |
| </div> | |
| <div class="card"> | |
| <h2>📜 实时日志</h2> | |
| <button class="secondary" onclick="loadLogs()">刷新</button> | |
| <button class="danger" onclick="clearLogs()">清空</button> | |
| <div id="logContainer" class="log-container" style="margin-top: 12px;">加载中...</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let accountsData = []; | |
| // 安全的 fetch 包装,处理可能返回 HTML 的情况 | |
| async function safeFetch(url, options = {}) { | |
| const resp = await fetch(url, options); | |
| const text = await resp.text(); | |
| try { | |
| return JSON.parse(text); | |
| } catch (e) { | |
| console.error('API 返回非 JSON:', text.substring(0, 100)); | |
| throw new Error('服务暂时不可用,请稍后重试'); | |
| } | |
| } | |
| async function loadAccounts() { | |
| try { | |
| const data = await safeFetch('/api/accounts'); | |
| accountsData = data.accounts || []; | |
| const list = document.getElementById('accountList'); | |
| if (data.accounts.length === 0) { | |
| list.innerHTML = '<p style="color:#666;text-align:center;padding:20px;">暂无过期账号</p>'; | |
| } else { | |
| list.innerHTML = data.accounts.map((acc, i) => \` | |
| <div class="account-item"> | |
| <div class="account-info"> | |
| <div class="account-email">\${acc.email}</div> | |
| <div class="account-meta">ID: \${acc.accountId ?? i} | JWT: \${acc.mailJwtUrl ? '✓' : '✗'}</div> | |
| </div> | |
| <div class="account-actions"> | |
| <button class="success" onclick="refreshAccount(\${i})">刷新</button> | |
| <button class="danger" onclick="deleteAccount(\${i})">移除</button> | |
| </div> | |
| </div> | |
| \`).join(''); | |
| } | |
| const select = document.getElementById('loginAccountSelect'); | |
| select.innerHTML = '<option value="">-- 手动输入 --</option>' + | |
| data.accounts.map((acc, i) => \`<option value="\${i}">\${acc.email} (ID: \${acc.accountId ?? i})</option>\`).join(''); | |
| } catch (e) { | |
| document.getElementById('accountList').innerHTML = '<p style="color:red;">加载失败</p>'; | |
| } | |
| } | |
| // 从后台同步过期账号到本地列表 | |
| async function syncExpiredAccounts() { | |
| try { | |
| const data = await safeFetch('/api/sync-expired-accounts', { method: 'POST' }); | |
| if (data.success) { | |
| let msg = \`同步完成: 新增 \${data.added} 个过期账号\`; | |
| if (data.noUrl > 0) { | |
| msg += \`\\n⚠️ \${data.noUrl} 个账号缺少邮箱URL,无法自动刷新\`; | |
| } | |
| if (data.alreadyExists > 0) { | |
| msg += \`\\n已存在 \${data.alreadyExists} 个\`; | |
| } | |
| alert(msg); | |
| loadAccounts(); | |
| loadLogs(); | |
| } else { | |
| alert('同步失败: ' + (data.error || '未知错误')); | |
| } | |
| } catch (e) { | |
| alert('同步失败: ' + e.message); | |
| } | |
| } | |
| function onAccountSelect() { | |
| const select = document.getElementById('loginAccountSelect'); | |
| const index = select.value; | |
| if (index === '' || !accountsData[index]) { | |
| document.getElementById('loginEmail').value = ''; | |
| document.getElementById('loginMailJwt').value = ''; | |
| document.getElementById('loginAccountId').value = '0'; | |
| return; | |
| } | |
| const acc = accountsData[index]; | |
| document.getElementById('loginEmail').value = acc.email || ''; | |
| document.getElementById('loginMailJwt').value = acc.mailJwtUrl || ''; | |
| document.getElementById('loginAccountId').value = acc.accountId ?? index; | |
| } | |
| async function startRegister() { | |
| const count = parseInt(document.getElementById('registerCount').value) || 1; | |
| if (count < 1 || count > 10) return alert('注册数量必须在 1-10 之间'); | |
| if (!confirm(\`确定要自动注册 \${count} 个账号吗?\`)) return; | |
| const btn = document.getElementById('registerBtn'); | |
| btn.disabled = true; | |
| btn.textContent = '⏳ 注册中...'; | |
| try { | |
| const data = await safeFetch('/api/register', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ count }) | |
| }); | |
| if (!data.success) { | |
| alert(data.error || data.message); | |
| btn.disabled = false; | |
| btn.textContent = '🚀 开始自动注册'; | |
| } | |
| } catch (e) { | |
| alert('启动失败: ' + e.message); | |
| btn.disabled = false; | |
| btn.textContent = '🚀 开始自动注册'; | |
| } | |
| } | |
| async function loadRegisterStatus() { | |
| try { | |
| const data = await safeFetch('/api/register-status'); | |
| const container = document.getElementById('registerStatus'); | |
| const btn = document.getElementById('registerBtn'); | |
| if (data.running) { | |
| btn.disabled = true; | |
| btn.textContent = \`⏳ 注册中 (\${data.completed}/\${data.total})\`; | |
| let html = \`<p>进度: \${data.completed}/\${data.total}</p>\`; | |
| if (data.results.length > 0) { | |
| html += '<div class="refresh-result">'; | |
| data.results.forEach(r => { | |
| html += \`<div class="refresh-item \${r.success ? 'success' : 'error'}">\${r.success ? '✅' : '❌'} #\${r.index} \${r.email || ''} - \${r.message}</div>\`; | |
| }); | |
| html += '</div>'; | |
| } | |
| container.innerHTML = html; | |
| } else { | |
| btn.disabled = false; | |
| btn.textContent = '🚀 开始自动注册'; | |
| if (data.results && data.results.length > 0) { | |
| let html = '<div class="refresh-result">'; | |
| data.results.forEach(r => { | |
| html += \`<div class="refresh-item \${r.success ? 'success' : 'error'}">\${r.success ? '✅' : '❌'} #\${r.index} \${r.email || ''} - \${r.message}</div>\`; | |
| }); | |
| html += '</div>'; | |
| container.innerHTML = html; | |
| } else { | |
| container.innerHTML = ''; | |
| } | |
| } | |
| } catch (e) {} | |
| } | |
| async function syncBGAccounts() { | |
| const container = document.getElementById('bgAccountList'); | |
| container.innerHTML = '同步中...'; | |
| try { | |
| const data = await safeFetch('/api/business-gemini/accounts'); | |
| if (data.success) { | |
| let html = \`<p><strong>总账号:</strong> \${data.accounts.length} | <strong>可用:</strong> <span style="color:green">\${data.available}</span> | <strong>不可用:</strong> <span style="color:red">\${data.unavailable}</span></p>\`; | |
| if (data.accounts.length > 0) { | |
| html += '<div style="max-height: 200px; overflow-y: auto; margin-top: 8px;">'; | |
| data.accounts.forEach((acc, i) => { | |
| const status = acc.available !== false ? '✅' : '❌'; | |
| const email = acc.tempmail_name || '-'; | |
| html += \`<div style="padding: 4px 0; border-bottom: 1px solid #eee; font-size: 12px;">\${status} #\${i} \${email}</div>\`; | |
| }); | |
| html += '</div>'; | |
| } | |
| container.innerHTML = html; | |
| } else { | |
| container.innerHTML = \`<p style="color:red;">同步失败: \${data.error}</p>\`; | |
| } | |
| } catch (e) { | |
| container.innerHTML = \`<p style="color:red;">同步失败: \${e.message}</p>\`; | |
| } | |
| } | |
| async function importFromBackend() { | |
| if (!confirm('从后台导入账号到本地?')) return; | |
| try { | |
| const data = await safeFetch('/api/import-from-backend', { method: 'POST' }); | |
| if (data.success) { | |
| alert('导入成功: ' + data.imported + ' 个账号'); | |
| loadAccounts(); | |
| loadLogs(); | |
| } else { | |
| alert('导入失败: ' + data.error); | |
| } | |
| } catch (e) { | |
| alert('导入失败'); | |
| } | |
| } | |
| async function deleteAccount(index) { | |
| if (!confirm('确定删除此账号?')) return; | |
| try { | |
| await fetch('/api/accounts/' + index, { method: 'DELETE' }); | |
| loadAccounts(); | |
| loadLogs(); | |
| } catch (e) { | |
| alert('删除失败'); | |
| } | |
| } | |
| async function refreshAccount(index) { | |
| if (!confirm('确定刷新此账号?')) return; | |
| try { | |
| const data = await safeFetch('/api/refresh/' + index, { method: 'POST' }); | |
| alert(data.success ? '刷新成功!' : '刷新失败: ' + data.error); | |
| loadLogs(); | |
| } catch (e) { | |
| alert('刷新失败: ' + e.message); | |
| } | |
| } | |
| async function loadRefreshStatus() { | |
| try { | |
| const data = await safeFetch('/api/refresh-status'); | |
| let html = '<p><strong>状态:</strong> ' + (data.running ? '🔄 运行中' : '⏸️ 空闲') + '</p>'; | |
| html += '<p><strong>账号数:</strong> ' + data.accountCount + '</p>'; | |
| html += '<p><strong>上次刷新:</strong> ' + (data.lastRefreshTime ? new Date(data.lastRefreshTime).toLocaleString() : '从未') + '</p>'; | |
| if (data.lastResult && data.lastResult.length > 0) { | |
| html += '<div class="refresh-result">'; | |
| data.lastResult.forEach(r => { | |
| html += '<div class="refresh-item ' + (r.success ? 'success' : 'error') + '">' + | |
| (r.success ? '✅' : '❌') + ' ' + r.email + ' - ' + r.message + '</div>'; | |
| }); | |
| html += '</div>'; | |
| } | |
| document.getElementById('refreshStatus').innerHTML = html; | |
| } catch (e) { | |
| document.getElementById('refreshStatus').innerHTML = '<p>加载失败</p>'; | |
| } | |
| } | |
| async function triggerRefresh() { | |
| if (!confirm('确定刷新全部过期账号?')) return; | |
| try { | |
| const data = await safeFetch('/api/trigger-refresh-expired', { method: 'POST' }); | |
| alert(data.message); | |
| loadRefreshStatus(); | |
| loadLogs(); | |
| loadAccounts(); | |
| } catch (e) { | |
| alert('触发失败'); | |
| } | |
| } | |
| async function loadLogs() { | |
| try { | |
| const data = await safeFetch('/api/logs?limit=100'); | |
| const container = document.getElementById('logContainer'); | |
| // 显示日志统计信息 | |
| const statsHtml = \` | |
| <div style="margin-bottom: 12px; padding: 8px; background: #f5f5f5; border-radius: 4px; font-size: 12px; color: #666;"> | |
| 📊 当前日志: \${data.total || data.logs.length} 条 | | |
| ⏰ 保留策略: \${data.retention_hours || 24} 小时 | | |
| 🔄 自动清理: 每小时执行 | |
| </div> | |
| \`; | |
| if (data.logs.length === 0) { | |
| container.innerHTML = statsHtml + '<p style="color:#888;text-align:center;">暂无日志</p>'; | |
| return; | |
| } | |
| const logsHtml = data.logs.map(log => \` | |
| <div class="log-item"> | |
| <span class="log-time">\${new Date(log.time).toLocaleTimeString()}</span> | |
| <span class="log-level \${log.level}">\${log.level}</span> | |
| \${log.email ? '<span class="log-email">[' + log.email + ']</span>' : ''} | |
| <span class="log-message">\${log.message}</span> | |
| </div> | |
| \`).join(''); | |
| container.innerHTML = statsHtml + logsHtml; | |
| } catch (e) { | |
| document.getElementById('logContainer').innerHTML = '<p style="color:red;">加载失败</p>'; | |
| } | |
| } | |
| async function clearLogs() { | |
| if (!confirm('确定清空日志?')) return; | |
| await fetch('/api/logs', { method: 'DELETE' }); | |
| loadLogs(); | |
| } | |
| async function doLogin() { | |
| const btn = document.getElementById('loginBtn'); | |
| const result = document.getElementById('loginResult'); | |
| btn.disabled = true; | |
| btn.textContent = '⏳ 处理中...'; | |
| result.style.display = 'block'; | |
| result.className = 'result'; | |
| result.textContent = '启动中...'; | |
| try { | |
| const data = await safeFetch('/api/login', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| email: document.getElementById('loginEmail').value, | |
| captchaKey: document.getElementById('loginCaptchaKey').value, | |
| mailJwtUrl: document.getElementById('loginMailJwt').value, | |
| accountId: parseInt(document.getElementById('loginAccountId').value) || 0, | |
| autoPush: true | |
| }) | |
| }); | |
| result.className = 'result ' + (data.success ? 'success' : 'error'); | |
| result.textContent = JSON.stringify(data, null, 2); | |
| loadLogs(); | |
| } catch (e) { | |
| result.className = 'result error'; | |
| result.textContent = 'Error: ' + e.message; | |
| } | |
| btn.disabled = false; | |
| btn.textContent = '🚀 开始登录'; | |
| } | |
| // 设置相关函数 | |
| async function loadSettings() { | |
| try { | |
| const data = await safeFetch('/api/runtime-config'); | |
| if (data.success && data.config) { | |
| // 服务配置 | |
| document.getElementById('cfgTempMailUrl').value = data.config.tempMailUrl || ''; | |
| document.getElementById('cfgBusinessGeminiUrl').value = data.config.businessGeminiUrl || ''; | |
| document.getElementById('cfgBusinessGeminiPassword').value = data.config.businessGeminiPassword || ''; | |
| document.getElementById('cfgYesCaptchaApiKey').value = data.config.yesCaptchaApiKey || ''; | |
| // 间隔配置 | |
| document.getElementById('cfgRegisterInterval').value = data.config.registerIntervalSeconds || 60; | |
| document.getElementById('cfgRefreshInterval').value = data.config.refreshIntervalSeconds || 30; | |
| document.getElementById('cfgScheduleRegisterHours').value = data.config.scheduleRegisterHours || 0; | |
| document.getElementById('cfgScheduleRegisterCount').value = data.config.scheduleRegisterCount || 1; | |
| document.getElementById('cfgScheduleRefreshHours').value = data.config.scheduleRefreshHours || 0; | |
| document.getElementById('settingsStatus').innerHTML = '<span style="color: green;">✓ 设置已加载</span>'; | |
| } | |
| } catch (e) { | |
| document.getElementById('settingsStatus').innerHTML = '<span style="color: red;">加载失败: ' + e.message + '</span>'; | |
| } | |
| } | |
| async function saveSettings() { | |
| const statusEl = document.getElementById('settingsStatus'); | |
| statusEl.innerHTML = '<span style="color: #666;">保存中...</span>'; | |
| try { | |
| const data = await safeFetch('/api/runtime-config', { | |
| method: 'PUT', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| // 服务配置 | |
| tempMailUrl: document.getElementById('cfgTempMailUrl').value, | |
| businessGeminiUrl: document.getElementById('cfgBusinessGeminiUrl').value, | |
| businessGeminiPassword: document.getElementById('cfgBusinessGeminiPassword').value, | |
| yesCaptchaApiKey: document.getElementById('cfgYesCaptchaApiKey').value, | |
| // 间隔配置 | |
| registerIntervalSeconds: parseInt(document.getElementById('cfgRegisterInterval').value) || 60, | |
| refreshIntervalSeconds: parseInt(document.getElementById('cfgRefreshInterval').value) || 30, | |
| scheduleRegisterHours: parseFloat(document.getElementById('cfgScheduleRegisterHours').value) || 0, | |
| scheduleRegisterCount: parseInt(document.getElementById('cfgScheduleRegisterCount').value) || 1, | |
| scheduleRefreshHours: parseFloat(document.getElementById('cfgScheduleRefreshHours').value) || 0 | |
| }) | |
| }); | |
| if (data.success) { | |
| statusEl.innerHTML = '<span style="color: green;">✓ 设置已保存</span>'; | |
| loadLogs(); | |
| } else { | |
| statusEl.innerHTML = '<span style="color: red;">保存失败</span>'; | |
| } | |
| } catch (e) { | |
| statusEl.innerHTML = '<span style="color: red;">保存失败: ' + e.message + '</span>'; | |
| } | |
| } | |
| // 初始化 | |
| loadAccounts(); | |
| loadRefreshStatus(); | |
| loadRegisterStatus(); | |
| syncBGAccounts(); | |
| loadLogs(); | |
| loadSettings(); | |
| // 定时刷新 | |
| setInterval(loadRefreshStatus, 5000); | |
| setInterval(loadRegisterStatus, 3000); | |
| setInterval(loadLogs, 10000); | |
| setInterval(loadAccounts, 15000); | |
| setInterval(syncBGAccounts, 60000); | |
| </script> | |
| </body> | |
| </html> | |
| `); | |
| }); | |
| // =============================== | |
| // 定时任务 | |
| // =============================== | |
| function startScheduledTasks() { | |
| // 定时注册 | |
| if (config.schedule.registerIntervalHours > 0) { | |
| const intervalMs = config.schedule.registerIntervalHours * 60 * 60 * 1000; | |
| // 格式化时间显示 | |
| let timeDisplay; | |
| if (config.schedule.registerIntervalHours >= 1) { | |
| timeDisplay = `${config.schedule.registerIntervalHours} 小时`; | |
| } else { | |
| const minutes = Math.round(config.schedule.registerIntervalHours * 60); | |
| timeDisplay = `${minutes} 分钟`; | |
| } | |
| addLog('INFO', `定时注册已启用: 每 ${timeDisplay} 注册 ${config.schedule.registerCount} 个账号`); | |
| scheduleTimers.register = setInterval(async () => { | |
| if (registerStatus.running) { | |
| addLog('WARN', '定时注册: 上一次任务仍在运行,跳过'); | |
| return; | |
| } | |
| addLog('INFO', `定时注册: 开始注册 ${config.schedule.registerCount} 个账号`); | |
| lastScheduledRegisterTime = new Date(); | |
| await runAutoRegister(config.schedule.registerCount); | |
| }, intervalMs); | |
| } | |
| // 定时刷新 - 改为刷新后台过期账号 | |
| if (config.schedule.refreshIntervalHours > 0) { | |
| const intervalMs = config.schedule.refreshIntervalHours * 60 * 60 * 1000; | |
| // 格式化时间显示 | |
| let timeDisplay; | |
| if (config.schedule.refreshIntervalHours >= 1) { | |
| timeDisplay = `${config.schedule.refreshIntervalHours} 小时`; | |
| } else { | |
| const minutes = Math.round(config.schedule.refreshIntervalHours * 60); | |
| timeDisplay = `${minutes} 分钟`; | |
| } | |
| addLog('INFO', `定时刷新已启用: 每 ${timeDisplay} 刷新后台过期账号`); | |
| scheduleTimers.refresh = setInterval(async () => { | |
| if (refreshStatus.running) { | |
| addLog('WARN', '定时刷新: 上一次任务仍在运行,跳过'); | |
| return; | |
| } | |
| addLog('INFO', '定时刷新: 开始刷新后台过期账号'); | |
| lastScheduledRefreshTime = new Date(); | |
| // 改为刷新后台过期账号,而不是本地账号 | |
| await runAutoRefreshExpiredAccounts(); | |
| }, intervalMs); | |
| } | |
| } | |
| // =============================== | |
| // 启动服务 | |
| // =============================== | |
| loadAccounts(); | |
| loadRuntimeConfig(); | |
| app.listen(config.port, '0.0.0.0', () => { | |
| addLog('INFO', '服务启动'); | |
| addLog('INFO', '端口: ' + config.port); | |
| addLog('INFO', 'Business Gemini: ' + (config.businessGemini.url || '未配置')); | |
| addLog('INFO', '临时邮箱服务: ' + (config.mail.tempMailUrl || '未配置')); | |
| addLog('INFO', '已加载账号: ' + runtimeAccounts.length + ' 个'); | |
| // 格式化定时注册时间显示 | |
| let registerTimeDisplay = '禁用'; | |
| if (config.schedule.registerIntervalHours > 0) { | |
| if (config.schedule.registerIntervalHours >= 1) { | |
| registerTimeDisplay = `${config.schedule.registerIntervalHours}小时`; | |
| } else { | |
| const minutes = Math.round(config.schedule.registerIntervalHours * 60); | |
| registerTimeDisplay = `${minutes}分钟`; | |
| } | |
| } | |
| // 格式化定时刷新时间显示 | |
| let refreshTimeDisplay = '禁用'; | |
| if (config.schedule.refreshIntervalHours > 0) { | |
| if (config.schedule.refreshIntervalHours >= 1) { | |
| refreshTimeDisplay = `${config.schedule.refreshIntervalHours}小时`; | |
| } else { | |
| const minutes = Math.round(config.schedule.refreshIntervalHours * 60); | |
| refreshTimeDisplay = `${minutes}分钟`; | |
| } | |
| } | |
| addLog('INFO', `定时注册: ${registerTimeDisplay}`); | |
| addLog('INFO', `定时刷新: ${refreshTimeDisplay}`); | |
| // 启动定时任务 | |
| startScheduledTasks(); | |
| // 启动定期日志清理任务(每小时清理一次) | |
| setInterval(() => { | |
| cleanupOldLogs(); | |
| }, 60 * 60 * 1000); // 每小时执行一次 | |
| addLog('INFO', `日志保留策略: ${LOG_RETENTION_HOURS}小时,每小时自动清理`); | |
| addLog('INFO', `操作间隔: 注册${config.interval.register}秒, 刷新${config.interval.refresh}秒`); | |
| }); | |