| const http = require('http'); |
| const { exec } = require('child_process'); |
|
|
| |
| let publicIP = 'unknown'; |
|
|
| |
| let lastReportTime = null; |
| let lastReportSuccess = false; |
|
|
| |
| const TARGET_IMAGE = process.env.TARGET_IMAGE || ''; |
| const DOCKER_ACCOUNTS = process.env.DOCKER_ACCOUNTS || ''; |
| const REPORT_URL = process.env.REPORT_URL || ''; |
| const INTERVAL_SECONDS = parseInt(process.env.INTERVAL_SECONDS) || 60; |
| const MARK_DURATION_SECONDS = parseInt(process.env.MARK_DURATION_SECONDS) || 3600; |
|
|
| |
| const accounts = DOCKER_ACCOUNTS.split(',') |
| .map(acc => { |
| const [user, pass] = acc.trim().split(':'); |
| return user && pass ? { user, pass } : null; |
| }) |
| .filter(acc => acc); |
|
|
| |
| const accountMarkedTime = new Map(); |
| let currentAccountIndex = 0; |
| let loggedInAccount = null; |
| let anonymousMarkedTime = null; |
|
|
| |
| function getAvailableAccount() { |
| if (accounts.length === 0) return null; |
|
|
| const now = Date.now(); |
| const markDuration = MARK_DURATION_SECONDS * 1000; |
|
|
| |
| for (const [user, markedTime] of accountMarkedTime) { |
| if (now - markedTime >= markDuration) { |
| accountMarkedTime.delete(user); |
| console.log(`账号 ${user} 标记已移除`); |
| } |
| } |
|
|
| |
| for (let i = 0; i < accounts.length; i++) { |
| const idx = (currentAccountIndex + i) % accounts.length; |
| const acc = accounts[idx]; |
| if (!accountMarkedTime.has(acc.user)) { |
| currentAccountIndex = idx; |
| return acc; |
| } |
| } |
|
|
| |
| return null; |
| } |
|
|
| |
| function getTimeUntilNextUnmark() { |
| if (accountMarkedTime.size === 0) return 0; |
|
|
| const now = Date.now(); |
| const markDuration = MARK_DURATION_SECONDS * 1000; |
| let minWait = Infinity; |
|
|
| for (const markedTime of accountMarkedTime.values()) { |
| const wait = markDuration - (now - markedTime); |
| if (wait < minWait && wait > 0) minWait = wait; |
| } |
|
|
| return minWait === Infinity ? 0 : minWait; |
| } |
|
|
| |
| async function fetchPublicIP() { |
| return new Promise((resolve) => { |
| exec('curl -s --connect-timeout 5 https://api.ipify.org', (error, stdout, stderr) => { |
| if (error || !stdout.trim()) { |
| console.error('获取公网 IP 失败:', error?.message || 'empty response'); |
| resolve('unknown'); |
| } else { |
| resolve(stdout.trim()); |
| } |
| }); |
| }); |
| } |
|
|
| |
| async function startIPUpdate() { |
| publicIP = await fetchPublicIP(); |
| console.log(`公网 IP: ${publicIP}`); |
|
|
| |
| setInterval(async () => { |
| publicIP = await fetchPublicIP(); |
| }, 5 * 60 * 1000); |
| } |
|
|
| |
| async function reportToURL() { |
| if (!REPORT_URL) return; |
|
|
| return new Promise((resolve) => { |
| exec(`curl -s --connect-timeout 10 "${REPORT_URL}"`, (error, stdout, stderr) => { |
| lastReportTime = new Date().toISOString(); |
| if (error) { |
| lastReportSuccess = false; |
| console.error(`[${lastReportTime}] 上报失败:`, error.message); |
| } else { |
| lastReportSuccess = true; |
| console.log(`[${lastReportTime}] 上报成功`); |
| } |
| resolve(); |
| }); |
| }); |
| } |
|
|
| |
| async function startReportTask() { |
| if (!REPORT_URL) { |
| console.log('REPORT_URL 未配置,跳过上报'); |
| return; |
| } |
|
|
| |
| await reportToURL(); |
|
|
| |
| setInterval(async () => { |
| await reportToURL(); |
| }, 6 * 60 * 60 * 1000); |
| } |
|
|
| |
| const images = TARGET_IMAGE.split(',').map(img => img.trim()).filter(img => img); |
|
|
| |
| let currentImageIndex = 0; |
|
|
| console.log(`配置: ${images.length} 个镜像, ${accounts.length} 个账号, 间隔 ${INTERVAL_SECONDS} 秒`); |
|
|
| |
| async function dockerLogin(account) { |
| return new Promise((resolve) => { |
| exec(`echo "${account.pass}" | docker login -u "${account.user}" --password-stdin`, (error, stdout, stderr) => { |
| if (error) { |
| console.error(`账号 ${account.user} 登录失败:`, stderr.trim()); |
| resolve(false); |
| } else { |
| console.log(`账号 ${account.user} 登录成功`); |
| resolve(true); |
| } |
| }); |
| }); |
| } |
|
|
| |
| async function runManifestInspect() { |
| if (images.length === 0) { |
| console.log('没有配置 TARGET_IMAGE'); |
| return; |
| } |
|
|
| |
| const image = images[currentImageIndex]; |
|
|
| |
| const account = getAvailableAccount(); |
|
|
| if (accounts.length > 0 && !account) { |
| |
| const waitTime = getTimeUntilNextUnmark(); |
| console.log(`所有账号都被标记,等待 ${Math.round(waitTime / 1000)} 秒后重试`); |
| setTimeout(() => runManifestInspect(), waitTime); |
| return; |
| } |
|
|
| |
| if (accounts.length === 0 && anonymousMarkedTime) { |
| const now = Date.now(); |
| const markDuration = MARK_DURATION_SECONDS * 1000; |
| if (now - anonymousMarkedTime < markDuration) { |
| |
| const waitTime = markDuration - (now - anonymousMarkedTime); |
| console.log(`匿名用户已被标记,等待 ${Math.round(waitTime / 1000)} 秒后重试`); |
| setTimeout(() => runManifestInspect(), waitTime); |
| return; |
| } else { |
| |
| anonymousMarkedTime = null; |
| console.log('匿名用户标记已移除'); |
| } |
| } |
|
|
| |
| if (account) { |
| |
| if (loggedInAccount?.user !== account.user) { |
| const loginSuccess = await dockerLogin(account); |
| if (!loginSuccess) { |
| |
| accountMarkedTime.set(account.user, Date.now()); |
| console.log(`账号 ${account.user} 登录失败,已标记,${MARK_DURATION_SECONDS} 秒后移除标记`); |
| loggedInAccount = null; |
| currentAccountIndex = (currentAccountIndex + 1) % accounts.length; |
| return; |
| } |
| loggedInAccount = account; |
| } |
| } |
|
|
| |
| await new Promise((resolve) => { |
| exec(`docker manifest inspect ${image}`, (error, stdout, stderr) => { |
| const timestamp = new Date().toISOString(); |
| if (error) { |
| const errorMsg = stderr.trim(); |
| console.error(`[${timestamp}] 检查镜像 ${image} 失败:`, errorMsg); |
|
|
| |
| if (errorMsg.includes('toomanyrequests') || errorMsg.includes('rate limit')) { |
| if (account) { |
| accountMarkedTime.set(account.user, Date.now()); |
| console.log(`账号 ${account.user} 触发 rate limit,已标记,${MARK_DURATION_SECONDS} 秒后移除标记`); |
| currentAccountIndex = (currentAccountIndex + 1) % accounts.length; |
| loggedInAccount = null; |
| } else { |
| |
| anonymousMarkedTime = Date.now(); |
| console.log(`匿名用户触发 rate limit,已标记,${MARK_DURATION_SECONDS} 秒后移除标记`); |
| } |
| } |
| } else { |
| console.log(`[${timestamp}] 检查镜像 ${image} 成功${account ? ` (账号: ${account.user})` : ' (匿名)'}`); |
| } |
| resolve(); |
| }); |
| }); |
|
|
| |
| currentImageIndex = (currentImageIndex + 1) % images.length; |
| } |
|
|
| |
| async function startBackgroundTask() { |
| |
| await runManifestInspect(); |
|
|
| |
| function scheduleNext() { |
| setTimeout(async () => { |
| await runManifestInspect(); |
| scheduleNext(); |
| }, INTERVAL_SECONDS * 1000); |
| } |
| scheduleNext(); |
| } |
|
|
| |
| const server = http.createServer((req, res) => { |
| |
| const hiddenImages = images.map(img => { |
| if (img.length <= 2) return img; |
| return img[0] + '...' + img[img.length - 1]; |
| }); |
|
|
| |
| const accountStatus = accounts.map(acc => ({ |
| user: acc.user, |
| marked: accountMarkedTime.has(acc.user) |
| })); |
|
|
| |
| let anonymousMarked = false; |
| let anonymousMarkedRemaining = 0; |
| if (anonymousMarkedTime) { |
| const now = Date.now(); |
| const elapsed = now - anonymousMarkedTime; |
| const markDuration = MARK_DURATION_SECONDS * 1000; |
| if (elapsed >= markDuration) { |
| anonymousMarkedTime = null; |
| } else { |
| anonymousMarked = true; |
| anonymousMarkedRemaining = Math.round((markDuration - elapsed) / 1000); |
| } |
| } |
|
|
| res.writeHead(200, { 'Content-Type': 'application/json' }); |
| res.end(JSON.stringify({ |
| status: 'running', |
| publicIP: publicIP, |
| imageCount: images.length, |
| images: hiddenImages, |
| intervalSeconds: INTERVAL_SECONDS, |
| accounts: accountStatus, |
| currentAccount: accounts[currentAccountIndex]?.user || null, |
| anonymousMarked: anonymousMarked, |
| anonymousMarkedRemaining: anonymousMarkedRemaining, |
| lastReportTime: lastReportTime, |
| lastReportSuccess: lastReportSuccess |
| })); |
| }); |
|
|
| |
| const PORT = 7860; |
| server.listen(PORT, () => { |
| console.log(`HTTP 服务器运行在端口 ${PORT}`); |
| startIPUpdate(); |
| startReportTask(); |
| startBackgroundTask(); |
| }); |
|
|