Created
January 13, 2026 03:21
-
-
Save sorrycc/f680de25d3fec8851c4e12c24a09981e to your computer and use it in GitHub Desktop.
Linux.do 自动浏览 + 点赞 + 实时统计面板
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name linuxdo保活优化版(高性能版) | |
| // @namespace http://tampermonkey.net/ | |
| // @version 0.6.0 | |
| // @description Linux.do 自动浏览 + 点赞 + 实时统计面板 + 面板控制启动/停止/暂停(性能优化版) | |
| // @author levi & ChatGPT | |
| // @match https://linux.do/* | |
| // @grant GM_setValue | |
| // @grant GM_getValue | |
| // @license MIT | |
| // @icon https://linux.do/uploads/default/original/3X/9/d/9dd49731091ce8656e94433a26a3ef36062b3994.png | |
| // @noframes | |
| // ==/UserScript== | |
| (() => { | |
| 'use strict'; | |
| const cfgDefaults = { | |
| scrollInterval: 1200, | |
| scrollDuration: 30, | |
| waitMin: 2000, | |
| waitMax: 5000, | |
| maxRunMins: 30 | |
| }; | |
| /** ========== 配置 ========== **/ | |
| const cfg = { | |
| scrollInterval: 1200, | |
| scrollStep: 800, | |
| viewThreshold: 500, | |
| scrollDuration: 30, | |
| maxTopics: 100, | |
| maxRunMins: 30, | |
| iframeStyle: { | |
| width: '320px', height: '480px', position: 'fixed', top: '70px', left: '8px', | |
| zIndex: 9999, border: '1px solid #ccc', borderRadius: '8px', boxShadow: '0 0 8px rgba(0,0,0,0.2)' | |
| }, | |
| log: { enabled: true, info: true, error: true } | |
| }; | |
| /** ========== 工具 ========== **/ | |
| const log = (t, ...a) => cfg.log.enabled && console[t](...a); | |
| const wait = ms => new Promise(r => setTimeout(r, ms)); | |
| const randomWait = () => wait(Math.random() * (cfg.waitMax - cfg.waitMin) + cfg.waitMin); | |
| const shuffle = arr => arr.map(v => [Math.random(), v]).sort((a, b) => a[0] - b[0]).map(v => v[1]); | |
| /** ========== 状态 ========== **/ | |
| let isPaused = false; | |
| let isRunning = false; | |
| let panelInterval = null; | |
| const stats = GM_getValue('linuxdoStats', { totalViews: 0, totalLikes: 0 }); | |
| const session = { start: Date.now(), views: 0, likes: 0 }; | |
| const getEnabled = () => GM_getValue('linuxdoEnabled', false); | |
| const setEnabled = v => GM_setValue('linuxdoEnabled', v); | |
| const getMinimized = () => GM_getValue('linuxdoMinimized', false); | |
| const setMinimized = v => GM_setValue('linuxdoMinimized', v); | |
| const getConfig = (key, def) => GM_getValue('linuxdoCfg_' + key, def); | |
| const setConfig = (key, v) => GM_setValue('linuxdoCfg_' + key, v); | |
| cfg.scrollInterval = getConfig('scrollInterval', cfgDefaults.scrollInterval); | |
| cfg.scrollDuration = getConfig('scrollDuration', cfgDefaults.scrollDuration); | |
| cfg.waitMin = getConfig('waitMin', cfgDefaults.waitMin); | |
| cfg.waitMax = getConfig('waitMax', cfgDefaults.waitMax); | |
| cfg.maxRunMins = getConfig('maxRunMins', cfgDefaults.maxRunMins); | |
| /** ========== UI 面板 ========== **/ | |
| function initPanel() { | |
| if (document.getElementById('ld-panel')) return; | |
| const html = ` | |
| <div class="ld-header" style="cursor:move;background:#2b2b2b;color:#fff;padding:6px 10px;border-radius:8px 8px 0 0;font-size:13px;"> | |
| 🧩 Linuxdo 助手 <span id="ld-min" style="float:right;cursor:pointer;">—</span> | |
| </div> | |
| <div id="ld-body" style="background:#fff;color:#333;padding:8px;font-size:13px;"> | |
| <div>🕒 时间:<span id="ld-time">0:00</span></div> | |
| <div>👀 浏览:<span id="ld-views">0</span></div> | |
| <div>💖 点赞:<span id="ld-likes">0</span></div> | |
| <div>⚙️ 状态:<span id="ld-state" style="color:red;">停止</span></div> | |
| <hr style="margin:6px 0;border:none;border-top:1px solid #ddd;"> | |
| <div style="margin-bottom:4px;">滚动间隔(ms):<input id="ld-scrollInterval" type="number" style="width:70px;" value="${cfg.scrollInterval}"></div> | |
| <div style="margin-bottom:4px;">停留时间(s):<input id="ld-scrollDuration" type="number" style="width:70px;" value="${cfg.scrollDuration}"></div> | |
| <div style="margin-bottom:4px;">等待最小(ms):<input id="ld-waitMin" type="number" style="width:70px;" value="${cfg.waitMin}"></div> | |
| <div style="margin-bottom:4px;">等待最大(ms):<input id="ld-waitMax" type="number" style="width:70px;" value="${cfg.waitMax}"></div> | |
| <div style="margin-bottom:4px;">运行时长(min):<input id="ld-maxRunMins" type="number" style="width:70px;" value="${cfg.maxRunMins}"></div> | |
| <button id="ld-reset" style="margin-top:6px;width:100%;padding:4px;border:none;border-radius:4px;background:#6c757d;color:#fff;">🔄 重置默认</button> | |
| <button id="ld-start" style="margin-top:4px;width:100%;padding:4px;border:none;border-radius:4px;background:#28a745;color:#fff;">▶️ 开始</button> | |
| <button id="ld-pause" style="margin-top:4px;width:100%;padding:4px;border:none;border-radius:4px;background:#007bff;color:#fff;">⏸ 暂停</button> | |
| </div>`; | |
| const panel = Object.assign(document.createElement('div'), { | |
| id: 'ld-panel', | |
| style: `position:fixed;right:20px;bottom:20px;width:180px; | |
| border:1px solid #888;border-radius:8px; | |
| box-shadow:0 0 6px rgba(0,0,0,0.2);font-family:sans-serif;z-index:99999;` | |
| }); | |
| panel.innerHTML = html; | |
| document.body.appendChild(panel); | |
| const body = panel.querySelector('#ld-body'); | |
| if (getMinimized()) body.style.display = 'none'; | |
| const els = { | |
| t: panel.querySelector('#ld-time'), | |
| v: panel.querySelector('#ld-views'), | |
| l: panel.querySelector('#ld-likes'), | |
| s: panel.querySelector('#ld-state'), | |
| start: panel.querySelector('#ld-start'), | |
| pause: panel.querySelector('#ld-pause'), | |
| reset: panel.querySelector('#ld-reset'), | |
| scrollInterval: panel.querySelector('#ld-scrollInterval'), | |
| scrollDuration: panel.querySelector('#ld-scrollDuration'), | |
| waitMin: panel.querySelector('#ld-waitMin'), | |
| waitMax: panel.querySelector('#ld-waitMax'), | |
| maxRunMins: panel.querySelector('#ld-maxRunMins') | |
| }; | |
| const bindConfigInput = (el, key) => { | |
| el.onchange = () => { | |
| const val = parseInt(el.value, 10); | |
| if (!isNaN(val) && val > 0) { | |
| cfg[key] = val; | |
| setConfig(key, val); | |
| } | |
| }; | |
| }; | |
| bindConfigInput(els.scrollInterval, 'scrollInterval'); | |
| bindConfigInput(els.scrollDuration, 'scrollDuration'); | |
| bindConfigInput(els.waitMin, 'waitMin'); | |
| bindConfigInput(els.waitMax, 'waitMax'); | |
| bindConfigInput(els.maxRunMins, 'maxRunMins'); | |
| els.reset.onclick = () => { | |
| Object.keys(cfgDefaults).forEach(key => { | |
| cfg[key] = cfgDefaults[key]; | |
| setConfig(key, cfgDefaults[key]); | |
| if (els[key]) els[key].value = cfgDefaults[key]; | |
| }); | |
| }; | |
| // 拖动逻辑 | |
| const header = panel.querySelector('.ld-header'); | |
| let dx, dy, dragging = false; | |
| header.onmousedown = e => { | |
| dragging = true; | |
| dx = e.clientX - panel.offsetLeft; | |
| dy = e.clientY - panel.offsetTop; | |
| document.onmousemove = ev => { | |
| if (!dragging) return; | |
| Object.assign(panel.style, { | |
| left: ev.clientX - dx + 'px', | |
| top: ev.clientY - dy + 'px', | |
| right: 'auto', | |
| bottom: 'auto' | |
| }); | |
| }; | |
| document.onmouseup = () => (dragging = false, document.onmousemove = null); | |
| }; | |
| // 最小化 | |
| panel.querySelector('#ld-min').onclick = () => { | |
| const isHidden = body.style.display === 'none'; | |
| const prevHeight = panel.offsetHeight; | |
| body.style.display = isHidden ? 'block' : 'none'; | |
| setMinimized(!isHidden); | |
| const newHeight = panel.offsetHeight; | |
| const rect = panel.getBoundingClientRect(); | |
| if (isHidden) { | |
| if (rect.bottom > window.innerHeight) { | |
| panel.style.top = Math.max(0, window.innerHeight - newHeight) + 'px'; | |
| panel.style.bottom = 'auto'; | |
| } | |
| } else { | |
| const currentTop = panel.offsetTop; | |
| panel.style.top = (currentTop + prevHeight - newHeight) + 'px'; | |
| panel.style.bottom = 'auto'; | |
| } | |
| }; | |
| // 暂停/恢复 | |
| els.pause.onclick = () => { | |
| if (!getEnabled()) return; | |
| isPaused = !isPaused; | |
| els.pause.textContent = isPaused ? '▶️ 恢复' : '⏸ 暂停'; | |
| els.pause.style.background = isPaused ? '#28a745' : '#007bff'; | |
| log('info', `助手已${isPaused ? '暂停' : '恢复'}`); | |
| }; | |
| // 开始/停止 | |
| els.start.onclick = async () => { | |
| const running = getEnabled(); | |
| setEnabled(!running); | |
| if (running) { | |
| els.start.textContent = '▶️ 开始'; | |
| els.start.style.background = '#28a745'; | |
| log('info', '助手已停止'); | |
| } else { | |
| els.start.textContent = '🛑 停止'; | |
| els.start.style.background = '#dc3545'; | |
| isPaused = false; | |
| els.pause.textContent = '⏸ 暂停'; | |
| els.pause.style.background = '#007bff'; | |
| session.start = Date.now(); | |
| log('info', '助手已启动'); | |
| runMain(); | |
| } | |
| }; | |
| // 状态更新(仅更新变化字段) | |
| if (panelInterval) clearInterval(panelInterval); | |
| let last = {}; | |
| panelInterval = setInterval(() => { | |
| const mins = Math.floor((Date.now() - session.start) / 60000); | |
| const secs = Math.floor((Date.now() - session.start) / 1000) % 60; | |
| const st = getEnabled() ? (isPaused ? '暂停中' : '运行中') : '停止'; | |
| const clr = getEnabled() ? (isPaused ? 'orange' : 'green') : 'red'; | |
| const cur = { t: `${mins}:${secs.toString().padStart(2, '0')}`, v: session.views, l: session.likes, s: st }; | |
| if (cur.t !== last.t) els.t.textContent = cur.t; | |
| if (cur.v !== last.v) els.v.textContent = cur.v; | |
| if (cur.l !== last.l) els.l.textContent = cur.l; | |
| if (cur.s !== last.s) { els.s.textContent = cur.s; els.s.style.color = clr; } | |
| last = cur; | |
| }, 1000); | |
| } | |
| /** ========== 功能 ========== **/ | |
| const saveStats = () => GM_setValue('linuxdoStats', stats); | |
| async function likeIfNeeded(win, views) { | |
| if (views < cfg.viewThreshold) return; | |
| try { | |
| if (!win || !win.document) return; | |
| const btn = win.document.querySelector('button.btn-toggle-reaction-like'); | |
| if (btn && !btn.title.includes('删除此 heart 回应')) { | |
| btn.click(); | |
| session.likes++; | |
| stats.totalLikes++; | |
| saveStats(); | |
| } | |
| } catch (e) { log('error', '点赞失败', e); } | |
| } | |
| async function browseTopic(topic) { | |
| while (isPaused) await wait(1000); | |
| const iframe = Object.assign(document.createElement('iframe'), { | |
| src: `${topic.url}?_=${Date.now()}`, style: Object.entries(cfg.iframeStyle).map(([k, v]) => `${k}:${v}`).join(';') | |
| }); | |
| document.body.appendChild(iframe); | |
| let loaded = false; | |
| await Promise.race([ | |
| new Promise(r => (iframe.onload = () => { loaded = true; r(); })), | |
| wait(8000) | |
| ]); | |
| if (!loaded) { | |
| log('error', 'iframe 加载超时', topic.url); | |
| iframe.remove(); | |
| return; | |
| } | |
| session.views++; stats.totalViews++; saveStats(); | |
| try { | |
| await likeIfNeeded(iframe.contentWindow, topic.views); | |
| const end = Date.now() + cfg.scrollDuration * 1000; | |
| while (Date.now() < end && getEnabled()) { | |
| if (isPaused) await wait(1000); | |
| try { | |
| iframe.contentWindow.scrollBy(0, cfg.scrollStep); | |
| } catch (e) { | |
| log('error', '滚动失败', e); | |
| break; | |
| } | |
| await wait(cfg.scrollInterval); | |
| } | |
| } catch (e) { | |
| log('error', '浏览帖子失败', e); | |
| } | |
| iframe.remove(); | |
| await randomWait(); | |
| } | |
| async function getTopics() { | |
| return [...document.querySelectorAll('#list-area .title')] | |
| .filter(el => !el.closest('tr')?.querySelector('.pinned')) | |
| .map(el => { | |
| const viewsStr = el.closest('tr')?.querySelector('.num.views .number')?.getAttribute('title') || '0'; | |
| const views = parseInt(viewsStr.replace(/\D/g, ''), 10) || 0; | |
| return { | |
| title: el.textContent.trim(), | |
| url: el.href, | |
| views | |
| }; | |
| }); | |
| } | |
| const shouldStop = () => { | |
| if (!getEnabled()) return true; | |
| if (session.views >= cfg.maxTopics) return true; | |
| return (Date.now() - session.start) / 60000 >= cfg.maxRunMins; | |
| }; | |
| /** ========== 主循环 ========== **/ | |
| async function runMain() { | |
| if (isRunning) return; | |
| isRunning = true; | |
| try { | |
| const topics = shuffle(await getTopics()); | |
| for (const t of topics) { | |
| if (shouldStop()) break; | |
| await browseTopic(t); | |
| } | |
| } catch (e) { | |
| log('error', '主循环异常', e); | |
| } finally { | |
| isRunning = false; | |
| setEnabled(false); | |
| log('info', '助手运行结束'); | |
| } | |
| } | |
| /** ========== 启动 ========== **/ | |
| (document.readyState === 'complete' | |
| ? initPanel() | |
| : window.addEventListener('load', initPanel)); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment