Skip to content

Instantly share code, notes, and snippets.

@sorrycc
Created January 13, 2026 03:21
Show Gist options
  • Select an option

  • Save sorrycc/f680de25d3fec8851c4e12c24a09981e to your computer and use it in GitHub Desktop.

Select an option

Save sorrycc/f680de25d3fec8851c4e12c24a09981e to your computer and use it in GitHub Desktop.
Linux.do 自动浏览 + 点赞 + 实时统计面板
// ==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