// ==UserScript==
// @name linux.do 小助手
// @description 自动浏览、点赞、只看楼主、楼层号、保存帖子到本地、清爽模式。
// @namespace Violentmonkey Scripts
// @match https://linux.do/*
// @grant none
// @version 1.0.1
// @author quantumcat & nulluser
// @license MIT
// @icon https://www.google.com/s2/favicons?domain=linux.do
// ==/UserScript==
// 配置项
const CONFIG = {
scroll: {
minSpeed: 10,
maxSpeed: 15,
minDistance: 2,
maxDistance: 4,
checkInterval: 500,
fastScrollChance: 0.08,
fastScrollMin: 80,
fastScrollMax: 200
},
time: {
browseTime: 3600000,
restTime: 600000,
minPause: 300,
maxPause: 500,
loadWait: 1500,
},
article: {
commentLimit: 1000,
topicListLimit: 100,
retryLimit: 3
},
mustRead: {
posts: [
{
id: '1051',
url: 'https://linux.do/t/topic/1051/'
},
{
id: '5973',
url: 'https://linux.do/t/topic/5973'
},
{
id: '102770',
url: 'https://linux.do/t/topic/102770'
},
{
id: '154010',
url: 'https://linux.do/t/topic/154010'
},
{
id: '149576',
url: 'https://linux.do/t/topic/149576'
},
{
id: '22118',
url: 'https://linux.do/t/topic/22118'
},
],
likesNeeded: 5 // 需要点赞的数量
}
};
// 工具函数
const Utils = {
random: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min,
sleep: (ms) => new Promise(resolve => setTimeout(resolve, ms)),
isPageLoaded: () => {
const loadingElements = document.querySelectorAll('.loading, .infinite-scroll');
return loadingElements.length === 0;
},
isNearBottom: () => {
const {scrollHeight, clientHeight, scrollTop} = document.documentElement;
return (scrollTop + clientHeight) >= (scrollHeight - 200);
},
debounce: (func, wait) => {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
};
// 存储管理
const Storage = {
get: (key, defaultValue = null) => {
try {
const value = localStorage.getItem(key);
return value ? JSON.parse(value) : defaultValue;
} catch {
return defaultValue;
}
},
set: (key, value) => {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (error) {
console.error('Storage error:', error);
return false;
}
}
};
class BrowseController {
constructor() {
this.isScrolling = false;
this.scrollInterval = null;
this.pauseTimeout = null;
this.accumulatedTime = Storage.get('accumulatedTime', 0);
this.lastActionTime = Date.now();
this.isTopicPage = window.location.href.includes("/t/topic/");
this.autoRunning = Storage.get('autoRunning', false);
this.topicList = Storage.get('topicList', []);
this.firstUseChecked = Storage.get('firstUseChecked', false);
this.likesCount = Storage.get('likesCount', 0);
this.selectedPost = Storage.get('selectedPost', null);
this.autoLikeEnabled = Storage.get('autoLikeEnabled', false);
this.cleanModeEnabled = Storage.get('cleanModeEnabled', false);
this.likedTopics = Storage.get('likedTopics', []); // 新增:记录已点赞的主题ID
this.setupButton();
this.initFloorNumberDisplay();
this.applyCleanModeStyles();
this.initOnlyOwnerView();
if (!this.firstUseChecked) {
this.handleFirstUse();
} else if (this.autoRunning) {
if (this.isTopicPage) {
this.startScrolling();
if (this.autoLikeEnabled) {
this.autoLikeTopic();
}
} else {
this.getLatestTopics().then(() => this.navigateNextTopic());
}
}
if (this.autoLikeEnabled && this.isTopicPage) {
this.autoLikeTopic();
}
}
setupButton() {
this.container = document.createElement("div");
Object.assign(this.container.style, {
position: "fixed",
right: "20px",
bottom: "30%",
display: "flex",
flexDirection: "column",
gap: "10px",
zIndex: "9999"
});
this.button = document.createElement("button");
Object.assign(this.button.style, {
padding: "12px 24px",
fontSize: "16px",
backgroundColor: this.autoRunning ? "#ff6b6b" : "#4caf50",
border: "none",
borderRadius: "6px",
color: "white",
cursor: "pointer",
boxShadow: "0 4px 8px rgba(0,0,0,0.2)",
transition: "background-color 0.3s, transform 0.2s"
});
this.button.textContent = this.autoRunning ? "停止阅读" : "开始阅读";
this.button.addEventListener("click", () => this.handleButtonClick());
this.button.addEventListener("mouseover", () => {
this.button.style.transform = "scale(1.05)";
});
this.button.addEventListener("mouseout", () => {
this.button.style.transform = "scale(1)";
});
this.toggleContainer = document.createElement("div");
Object.assign(this.toggleContainer.style, {
display: "flex",
alignItems: "center",
gap: "8px",
backgroundColor: "white",
padding: "8px 12px",
borderRadius: "6px",
boxShadow: "0 2px 5px rgba(0,0,0,0.1)"
});
this.toggleLabel = document.createElement("label");
this.toggleLabel.textContent = "自动点赞主题";
Object.assign(this.toggleLabel.style, {
fontSize: "14px",
color: "#333",
cursor: "pointer"
});
this.toggleSwitch = document.createElement("input");
this.toggleSwitch.type = "checkbox";
this.toggleSwitch.checked = this.autoLikeEnabled;
Object.assign(this.toggleSwitch.style, {
width: "40px",
height: "20px",
cursor: "pointer"
});
this.toggleSwitch.addEventListener("change", () => {
this.autoLikeEnabled = this.toggleSwitch.checked;
Storage.set('autoLikeEnabled', this.autoLikeEnabled);
console.log(`自动点赞主题: ${this.autoLikeEnabled ? '开启' : '关闭'}`);
if (this.autoLikeEnabled && this.isTopicPage) {
this.autoLikeTopic();
}
});
this.cleanModeContainer = document.createElement("div");
Object.assign(this.cleanModeContainer.style, {
display: "flex",
alignItems: "center",
gap: "8px",
backgroundColor: "white",
padding: "8px 12px",
borderRadius: "6px",
boxShadow: "0 2px 5px rgba(0,0,0,0.1)"
});
this.cleanModeLabel = document.createElement("label");
this.cleanModeLabel.textContent = "清爽模式";
Object.assign(this.cleanModeLabel.style, {
fontSize: "14px",
color: "#333",
cursor: "pointer"
});
this.cleanModeSwitch = document.createElement("input");
this.cleanModeSwitch.type = "checkbox";
this.cleanModeSwitch.checked = this.cleanModeEnabled;
Object.assign(this.cleanModeSwitch.style, {
width: "40px",
height: "20px",
cursor: "pointer"
});
this.cleanModeSwitch.addEventListener("change", () => {
this.cleanModeEnabled = this.cleanModeSwitch.checked;
Storage.set('cleanModeEnabled', this.cleanModeEnabled);
console.log(`清爽模式: ${this.cleanModeEnabled ? '开启' : '关闭'}`);
this.toggleCleanMode();
});
this.toggleContainer.appendChild(this.toggleSwitch);
this.toggleContainer.appendChild(this.toggleLabel);
this.cleanModeContainer.appendChild(this.cleanModeSwitch);
this.cleanModeContainer.appendChild(this.cleanModeLabel);
this.container.appendChild(this.button);
this.container.appendChild(this.toggleContainer);
this.container.appendChild(this.cleanModeContainer);
document.body.appendChild(this.container);
}
toggleCleanMode() {
const sidebarToggle = document.querySelector('button.btn-sidebar-toggle');
if (sidebarToggle && this.cleanModeEnabled) {
if (sidebarToggle.getAttribute('aria-expanded') === 'true') {
console.log('清爽模式启用,收起边栏');
sidebarToggle.click();
}
}
this.applyCleanModeStyles();
}
applyCleanModeStyles() {
let styleElement = document.getElementById('clean-mode-styles');
if (styleElement) {
styleElement.remove();
}
if (this.cleanModeEnabled) {
styleElement = document.createElement('style');
styleElement.id = 'clean-mode-styles';
styleElement.textContent = `
p:contains("希望你喜欢这里。有问题,请提问,或搜索现有帖子。") {
display: none !important;
}
div#global-notice-alert-global-notice.alert.alert-info.alert-global-notice {
display: none !important;
}
a[href="https://linux.do/t/topic/482293"] {
display: none !important;
}
div.link-bottom-line a.badge-category__wrapper {
display: none !important;
}
td.posters.topic-list-data {
display: none !important;
}
a.discourse-tag.box[href^="/tag/"] {
display: none !important;
}
`;
document.head.appendChild(styleElement);
}
}
initOnlyOwnerView() {
this.createToggleButton();
this.observePageChanges();
this.toggleVisibility();
}
toggleVisibility() {
const displayMode = localStorage.getItem("on_off") || "当前查看全部";
const userId = document.getElementById("post_1")?.getAttribute('data-user-id');
if (userId) {
document.querySelectorAll('article').forEach(article => {
article.style.display = (displayMode === "当前只看楼主" && article.dataset.userId !== userId) ? 'none' : '';
});
}
}
createToggleButton() {
if (document.getElementById("toggleVisibilityBtn")) {
return;
}
const btn = document.createElement("button");
btn.id = "toggleVisibilityBtn";
btn.textContent = localStorage.getItem("on_off") || "当前查看全部";
btn.onclick = () => {
const newText = btn.textContent === '当前查看全部' ? '当前只看楼主' : '当前查看全部';
document.getElementsByClassName("start-date")[0]?.click();
btn.textContent = newText;
localStorage.setItem("on_off", newText);
this.toggleVisibility();
};
btn.style.backgroundColor = "#333";
btn.style.color = "#FFF";
btn.style.border = "none";
btn.style.padding = "8px 16px";
btn.style.marginLeft = "10px";
btn.style.borderRadius = "5px";
btn.style.cursor = "pointer";
const saveButton = document.querySelector('.save-to-local-btn');
if (saveButton) {
saveButton.parentElement.appendChild(btn);
} else {
const firstPostContent = document.querySelector('.boxed.onscreen-post[data-post-id] .cooked');
if (firstPostContent) {
firstPostContent.appendChild(btn);
}
}
}
observePageChanges() {
const observer = new MutationObserver(() => {
if (document.querySelector(".timeline-footer-controls") && !document.getElementById("toggleVisibilityBtn")) {
this.createToggleButton();
}
this.toggleVisibility();
});
observer.observe(document.body, { childList: true, subtree: true });
}
initFloorNumberDisplay() {
this.addFloorNumbers();
this.initMutationObserver();
this.setupRandomJumpButton();
this.monitorURLChangeAndUpdateButton();
}
addFloorNumbers() {
document.querySelectorAll('.boxed.onscreen-post').forEach((post) => {
if (!post.querySelector('.floor-number')) {
const floorNumber = document.createElement('div');
floorNumber.className = 'floor-number';
floorNumber.textContent = '楼层: ' + post.id.split("_")[1];
floorNumber.style.cssText = 'color: grey; margin-left: 10px;';
post.querySelector('.topic-meta-data').appendChild(floorNumber);
}
});
this.setupSaveButton();
}
initMutationObserver() {
const observer = new MutationObserver(() => {
this.addFloorNumbers();
this.setupSaveButton();
this.toggleCleanMode();
});
observer.observe(document.body, { childList: true, subtree: true });
}
randomJump() {
fetch(window.location.href + '.json')
.then(response => response.json())
.then(data => {
if (data && data.posts_count) {
const postId = 1 + Math.floor(Math.random() * data.posts_count);
const currentUrl = new URL(window.location.href);
const list1 = currentUrl.pathname.split("/");
if (list1[list1.length - 2] === "topic") {
list1.push(postId);
} else if (list1[list1.length - 3] === "topic") {
list1[list1.length - 1] = postId;
}
const newUrl = list1.join("/");
window.location.href = newUrl;
alert('恭喜楼层【' + postId + '】的用户被抽中!');
}
})
.catch(error => console.error('Error:', error));
}
setupRandomJumpButton() {
const randomButton = document.createElement('button');
randomButton.id = "randomButton1";
randomButton.textContent = '随机楼层';
Object.assign(randomButton.style, {
position: "fixed",
bottom: "25%",
right: "20px",
width: "80px",
height: "30px",
backgroundColor: "#007bff",
color: "white",
border: "none",
borderRadius: "5px",
fontSize: "15px",
cursor: "pointer",
zIndex: "9999",
display: this.isTopicPage ? 'block' : 'none'
});
randomButton.onclick = () => this.randomJump();
this.container.appendChild(randomButton);
}
setupSaveButton() {
const firstPost = document.querySelector('.boxed.onscreen-post[data-post-id]');
if (firstPost && firstPost.id.includes('post_1')) {
if (!firstPost.querySelector('.save-to-local-btn')) {
const saveButton = document.createElement('button');
saveButton.className = 'save-to-local-btn';
saveButton.textContent = '保存到本地';
Object.assign(saveButton.style, {
padding: '8px 16px',
fontSize: '16px',
backgroundColor: '#ff9800',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
marginTop: '10px',
boxShadow: '0 2px 5px rgba(0,0,0,0.2)',
transition: 'background-color 0.3s, transform 0.2s'
});
saveButton.addEventListener('mouseover', () => {
saveButton.style.transform = 'scale(1.05)';
});
saveButton.addEventListener('mouseout', () => {
saveButton.style.transform = 'scale(1)';
});
saveButton.addEventListener('click', () => this.savePostToLocal(firstPost));
const postContent = firstPost.querySelector('.cooked');
if (postContent) {
postContent.appendChild(saveButton);
}
}
}
}
async savePostToLocal(postElement) {
try {
const topicTitle = document.querySelector('.fancy-title')?.textContent.trim() || 'Untitled_Topic';
const postContent = postElement.querySelector('.cooked');
if (!postContent) {
alert('无法获取帖子内容!');
return;
}
const contentClone = postContent.cloneNode(true);
contentClone.querySelector('.save-to-local-btn')?.remove();
const images = contentClone.querySelectorAll('img');
for (const img of images) {
try {
const response = await fetch(img.src);
const blob = await response.blob();
const reader = new FileReader();
await new Promise((resolve) => {
reader.onload = resolve;
reader.readAsDataURL(blob);
});
img.src = reader.result;
} catch (error) {
console.error('图片加载失败:', img.src, error);
img.alt = '[图片加载失败]';
}
}
const htmlContent = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${topicTitle}</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.post-content { max-width: 800px; margin: 0 auto; }
img { max-width: 100%; height: auto; }
</style>
</head>
<body>
<div class="post-content">
<h1>${topicTitle}</h1>
${contentClone.innerHTML}
</div>
</body>
</html>
`;
const blob = new Blob([htmlContent], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
const fileName = topicTitle
.replace(/[\\/:*?"<>|]/g, '_')
.replace(/\s+/g, '_')
+ '.html';
link.download = fileName;
link.click();
URL.revokeObjectURL(url);
alert('帖子内容已保存到本地!');
} catch (error) {
console.error('保存帖子失败:', error);
alert('保存失败,请查看控制台错误信息。');
}
}
monitorURLChangeAndUpdateButton() {
let lastURL = location.href;
setInterval(() => {
const currentURL = location.href;
if (currentURL !== lastURL) {
lastURL = currentURL;
this.updateButtonVisibility();
this.toggleCleanMode();
if (this.autoLikeEnabled && /^https:\/\/linux\.do\/t\/topic\//.test(currentURL)) {
this.autoLikeTopic();
}
}
}, 1000);
}
updateButtonVisibility() {
const isTopicPage = /^https:\/\/linux\.do\/t\/topic\//.test(location.href);
const randomButton = document.getElementById('randomButton1');
if (randomButton) {
randomButton.style.display = isTopicPage ? 'block' : 'none';
}
}
handleButtonClick() {
if (this.isScrolling || this.autoRunning) {
this.stopScrolling();
this.autoRunning = false;
Storage.set('autoRunning', false);
this.button.textContent = "开始阅读";
this.button.style.backgroundColor = "#4caf50";
} else {
this.autoRunning = true;
Storage.set('autoRunning', true);
this.button.textContent = "停止阅读";
this.button.style.backgroundColor = "#ff6b6b";
if (!this.firstUseChecked) {
this.handleFirstUse();
} else if (this.isTopicPage) {
this.startScrolling();
if (this.autoLikeEnabled) {
this.autoLikeTopic();
}
} else {
this.getLatestTopics().then(() => this.navigateNextTopic());
}
}
}
async autoLikeTopic() {
if (!this.autoLikeEnabled) return;
// 获取当前主题ID
const match = window.location.pathname.match(/\/t\/topic\/(\d+)/);
if (!match) {
console.log("无法获取当前主题ID");
return;
}
const topicId = match[1];
// 检查是否已经点赞过此主题
if (this.likedTopics.includes(topicId)) {
console.log(`主题 ${topicId} 已经点赞过,跳过点赞操作`);
return;
}
console.log("正在检查是否需要自动点赞主题...");
await Utils.sleep(2000);
const likeButton = document.querySelector('div.discourse-reactions-reaction-button button.btn-toggle-reaction-like');
if (likeButton && !likeButton.classList.contains('has-like') && !likeButton.classList.contains('liked')) {
likeButton.scrollIntoView({ behavior: 'smooth', block: 'center' });
await Utils.sleep(1000);
console.log("找到主题点赞按钮,执行点击操作");
likeButton.click();
// 记录已点赞的主题ID
this.likedTopics.push(topicId);
Storage.set('likedTopics', this.likedTopics);
console.log(`已记录点赞主题 ${topicId}`);
} else {
console.log("未找到可点赞的主题按钮或已点赞");
// 如果页面显示已点赞,也记录到列表中,防止重复操作
if (likeButton && (likeButton.classList.contains('has-like') || likeButton.classList.contains('liked'))) {
if (!this.likedTopics.includes(topicId)) {
this.likedTopics.push(topicId);
Storage.set('likedTopics', this.likedTopics);
console.log(`主题 ${topicId} 已点赞,记录到列表`);
}
}
}
}
async handleFirstUse() {
if (!this.autoRunning) return;
if (!this.selectedPost) {
const randomIndex = Math.floor(Math.random() * CONFIG.mustRead.posts.length);
this.selectedPost = CONFIG.mustRead.posts[randomIndex];
Storage.set('selectedPost', this.selectedPost);
console.log(`随机选择文章: ${this.selectedPost.url}`);
window.location.href = this.selectedPost.url;
return;
}
const currentUrl = window.location.href;
if (currentUrl.includes(this.selectedPost.url)) {
console.log(`当前在选中的文章页面,已点赞数: ${this.likesCount}`);
while (this.likesCount < CONFIG.mustRead.likesNeeded && this.autoRunning) {
await this.likeRandomComment();
if (this.likesCount >= CONFIG.mustRead.likesNeeded) {
console.log('完成所需点赞数量,开始正常浏览');
Storage.set('firstUseChecked', true);
this.firstUseChecked = true;
await this.getLatestTopics();
await this.navigateNextTopic();
break;
}
await Utils.sleep(1000);
}
} else {
window.location.href = this.selectedPost.url;
}
}
async likeRandomComment() {
if (!this.autoRunning) return false;
const likeButtons = Array.from(document.querySelectorAll('.like-button, .like-count, [data-like-button], .discourse-reactions-reaction-button'))
.filter(button =>
button &&
button.offsetParent !== null &&
!button.classList.contains('has-like') &&
!button.classList.contains('liked')
);
if (likeButtons.length > 0) {
const randomButton = likeButtons[Math.floor(Math.random() * likeButtons.length)];
randomButton.scrollIntoView({ behavior: 'smooth', block: 'center' });
await Utils.sleep(1000);
if (!this.autoRunning) return false;
console.log('找到可点赞的评论,准备点赞');
randomButton.click();
this.likesCount++;
Storage.set('likesCount', this.likesCount);
await Utils.sleep(1000);
return true;
}
window.scrollBy({
top: 500,
behavior: 'smooth'
});
await Utils.sleep(1000);
console.log('当前位置没有找到可点赞的评论,继续往下找');
return false;
}
async getLatestTopics() {
let page = 1;
let topicList = [];
let retryCount = 0;
while (topicList.length < CONFIG.article.topicListLimit && retryCount < CONFIG.article.retryLimit) {
try {
const response = await fetch(`https://linux.do/latest.json?no_definitions=true&page=${page}`);
const data = await response.json();
if (data?.topic_list?.topics) {
const filteredTopics = data.topic_list.topics.filter(topic =>
topic.posts_count < CONFIG.article.commentLimit
);
topicList.push(...filteredTopics);
page++;
} else {
break;
}
} catch (error) {
console.error('获取文章列表失败:', error);
retryCount++;
await Utils.sleep(1000);
}
}
if (topicList.length > CONFIG.article.topicListLimit) {
topicList = topicList.slice(0, CONFIG.article.topicListLimit);
}
this.topicList = topicList;
Storage.set('topicList', topicList);
console.log(`已获取 ${topicList.length} 篇文章`);
}
async getNextTopic() {
if (this.topicList.length === 0) {
await this.getLatestTopics();
}
if (this.topicList.length > 0) {
const topic = this.topicList.shift();
Storage.set('topicList', this.topicList);
return topic;
}
return null;
}
async startScrolling() {
if (this.isScrolling) return;
this.isScrolling = true;
this.button.textContent = "停止阅读";
this.button.style.backgroundColor = "#ff6b6b";
this.lastActionTime = Date.now();
while (this.isScrolling) {
const speed = Utils.random(CONFIG.scroll.minSpeed, CONFIG.scroll.maxSpeed);
const distance = Utils.random(CONFIG.scroll.minDistance, CONFIG.scroll.maxDistance);
const scrollStep = distance * 2.5;
window.scrollBy({
top: scrollStep,
behavior: 'smooth'
});
if (Utils.isNearBottom()) {
await Utils.sleep(800);
if (Utils.isNearBottom() && Utils.isPageLoaded()) {
console.log("已到达页面底部,准备导航到下一篇文章...");
await Utils.sleep(1000);
await this.navigateNextTopic();
break;
}
}
await Utils.sleep(speed);
this.accumulateTime();
if (Math.random() < CONFIG.scroll.fastScrollChance) {
const fastScroll = Utils.random(CONFIG.scroll.fastScrollMin, CONFIG.scroll.fastScrollMax);
window.scrollBy({
top: fastScroll,
behavior: 'smooth'
});
await Utils.sleep(200);
}
}
}
async waitForPageLoad() {
let attempts = 0;
const maxAttempts = 5;
while (attempts < maxAttempts) {
if (Utils.isPageLoaded()) {
return true;
}
await Utils.sleep(300);
attempts++;
}
return false;
}
stopScrolling() {
this.isScrolling = false;
clearInterval(this.scrollInterval);
clearTimeout(this.pauseTimeout);
this.button.textContent = "开始阅读";
this.button.style.backgroundColor = "#4caf50";
}
accumulateTime() {
const now = Date.now();
this.accumulatedTime += now - this.lastActionTime;
Storage.set('accumulatedTime', this.accumulatedTime);
this.lastActionTime = now;
if (this.accumulatedTime >= CONFIG.time.browseTime) {
this.accumulatedTime = 0;
Storage.set('accumulatedTime', 0);
this.pauseForRest();
}
}
async pauseForRest() {
this.stopScrolling();
console.log("休息10分钟...");
await Utils.sleep(CONFIG.time.restTime);
console.log("休息结束,继续浏览...");
this.startScrolling();
}
async navigateNextTopic() {
const nextTopic = await this.getNextTopic();
if (nextTopic) {
console.log("导航到新文章:", nextTopic.title);
const url = nextTopic.last_read_post_number
? `https://linux.do/t/topic/${nextTopic.id}/${nextTopic.last_read_post_number}`
: `https://linux.do/t/topic/${nextTopic.id}`;
window.location.href = url;
} else {
console.log("没有更多文章,返回首页");
window.location.href = "https://linux.do/latest";
}
}
resetFirstUse() {
Storage.set('firstUseChecked', false);
Storage.set('likesCount', 0);
Storage.set('selectedPost', null);
this.firstUseChecked = false;
this.likesCount = 0;
this.selectedPost = null;
console.log('已重置首次使用状态');
}
}
// 初始化
(function() {
new BrowseController();
})();