// ==UserScript==
// @name linuxdo保活
// @namespace http://tampermonkey.net/
// @version 0.2.5.2
// @description linuxdo自动浏览帖子,自动点赞
// @author oxzk
// @match https://linux.do/*
// @grant GM_setValue
// @grant GM_getValue
// @license MIT
// @homepageURL https://greasyfork.org/zh-CN/scripts/560774-linuxdo%E4%BF%9D%E6%B4%BB
// @downloadURL none
// ==/UserScript==
;(function () {
'use strict'
// ==================== 配置 ====================
const CONFIG = {
scroll: {
interval: 1200, // 滚动间隔(毫秒)
step: 600, // 每次滚动像素
duration: 30, // 滚动持续时间(秒)
},
limits: {
viewThreshold: 1, // 浏览量阈值,超过才点赞
maxTopics: 200, // 最大浏览帖子数
maxLikesPerPost: 3, // 每帖最大点赞数
likeInterval: 3000, // 点赞间隔(毫秒)
maxRunTime: 360, // 最大运行时间(分钟)
browseTimeout: 60000, // 单帖浏览超时(毫秒)
},
urls: {
base: 'https://linux.do/new',
},
iframe: {
width: '50%',
height: '100%',
top: '0px',
left: '0px',
position: 'fixed',
zIndex: '9999',
},
logging: {
enabled: true,
levels: { error: true, warn: true, info: true, debug: false },
},
storage: {
stats: 'linuxdoStats',
enabled: 'linuxdoHelperEnabled',
},
}
// ==================== 工具函数 ====================
const utils = {
// 延时函数
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
// 随机延时
randomSleep: (maxMs) => utils.sleep(Math.floor(Math.random() * maxMs) + 1000),
// Promise 超时包装
withTimeout: (promise, ms) =>
Promise.race([promise, new Promise((_, reject) => setTimeout(() => reject(new Error('操作超时')), ms))]),
// 格式化时间
formatDuration: (seconds) => {
const h = Math.floor(seconds / 3600)
const m = Math.floor((seconds % 3600) / 60)
const s = seconds % 60
return `${h}时${m}分${s}秒`
},
// 解析浏览量
parseViewCount: (text) => {
const match = text?.match(/此话题已被浏览\s*([\d,]+)\s*次/)
return match ? parseInt(match[1].replace(/,/g, '')) : 0
},
// 数组随机打乱
shuffle: (arr) => [...arr].sort(() => Math.random() - 0.5),
}
// ==================== 日志模块 ====================
const logger = {
_log: (level, ...args) => {
if (CONFIG.logging.enabled && CONFIG.logging.levels[level]) {
const method =
level === 'error' ? 'error' : level === 'warn' ? 'warn' : level === 'debug' ? 'debug' : 'log'
console[method](`[LinuxDo助手]`, ...args)
}
},
error: (...args) => logger._log('error', '❌', ...args),
warn: (...args) => logger._log('warn', '⚠️', ...args),
info: (...args) => logger._log('info', ...args),
debug: (...args) => logger._log('debug', '🔍', ...args),
}
// ==================== 统计模块 ====================
const stats = {
totalViews: 0,
totalLikes: 0,
sessionViews: 0,
sessionLikes: 0,
startTime: Date.now(),
load() {
const saved = GM_getValue(CONFIG.storage.stats, {})
this.totalViews = saved.totalViews || 0
this.totalLikes = saved.totalLikes || 0
logger.info('📊 历史统计 - 浏览:', this.totalViews, '点赞:', this.totalLikes)
},
save() {
GM_setValue(CONFIG.storage.stats, {
totalViews: this.totalViews,
totalLikes: this.totalLikes,
})
},
addView() {
this.sessionViews++
this.totalViews++
this.save()
},
addLike() {
this.sessionLikes++
this.totalLikes++
this.save()
},
getRunTime() {
return Math.floor((Date.now() - this.startTime) / 1000)
},
print() {
logger.info('\n📊 统计信息')
logger.info('-------------------')
logger.info(`🕒 运行时间:${utils.formatDuration(this.getRunTime())}`)
logger.info(`👀 本次浏览:${this.sessionViews}帖`)
logger.info(`❤️ 本次点赞:${this.sessionLikes}次`)
logger.info(`📈 总浏览数:${this.totalViews}帖`)
logger.info(`💖 总点赞数:${this.totalLikes}次`)
logger.info('-------------------\n')
},
}
// ==================== 开关控制 ====================
const switchControl = {
get enabled() {
return GM_getValue(CONFIG.storage.enabled, false)
},
set enabled(value) {
GM_setValue(CONFIG.storage.enabled, value)
},
toggle() {
// 清理本次会话数据
stats.sessionViews = 0
stats.sessionLikes = 0
stats.startTime = Date.now()
core.likeLimited = false
const newState = !this.enabled
this.enabled = newState
logger.info(`助手已${newState ? '启用' : '禁用'}`)
if (newState) {
window.location.href = CONFIG.urls.base
}
return newState
},
}
// ==================== UI 模块 ====================
const ui = {
link: null,
use: null,
statsPanel: null,
statsTimer: null,
updateIcon(enabled) {
if (this.use) {
this.use.setAttribute('href', enabled ? '#pause' : '#play')
}
if (this.link) {
this.link.title = enabled ? '停止助手' : '启动助手'
this.link.classList.toggle('active', enabled)
}
// 显示/隐藏统计面板
if (enabled) {
this.showStatsPanel()
} else {
this.hideStatsPanel()
}
},
createStatsPanel() {
if (this.statsPanel) return
this.statsPanel = document.createElement('div')
this.statsPanel.id = 'linuxdo-stats-panel'
this.statsPanel.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
padding: 12px 16px;
border-radius: 8px;
font-size: 13px;
line-height: 1.6;
z-index: 10000;
min-width: 160px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
display: none;
`
document.body.appendChild(this.statsPanel)
},
updateStatsPanel() {
if (!this.statsPanel) return
// 检查 429 限制是否已过期,自动恢复
if (core.likeLimited && core.likeLimitedUntil && Date.now() >= core.likeLimitedUntil) {
core.likeLimited = false
core.likeLimitedWaitTime = ''
core.likeLimitedUntil = 0
logger.info('✅ 点赞限制已解除,恢复点赞')
}
// 构建 429 限制提示(带倒计时)
let limitInfo = ''
if (core.likeLimited) {
let countdown = ''
if (core.likeLimitedUntil) {
const remaining = Math.max(0, Math.ceil((core.likeLimitedUntil - Date.now()) / 1000))
countdown = ` (${utils.formatDuration(remaining)})`
}
limitInfo = `
🚫 点赞受限${countdown}
`
}
this.statsPanel.innerHTML = `
📊 LinuxDo助手
🕒 运行: ${utils.formatDuration(stats.getRunTime())}
👀 浏览: ${stats.sessionViews}/${CONFIG.limits.maxTopics}
❤️ 点赞: ${stats.sessionLikes}
📈 总浏览: ${stats.totalViews}
💖 总点赞: ${stats.totalLikes}
${limitInfo}
`
},
showStatsPanel() {
this.createStatsPanel()
this.statsPanel.style.display = 'block'
this.updateStatsPanel()
// 每秒更新
if (!this.statsTimer) {
this.statsTimer = setInterval(() => this.updateStatsPanel(), 1000)
}
},
hideStatsPanel() {
if (this.statsPanel) {
this.statsPanel.style.display = 'none'
}
if (this.statsTimer) {
clearInterval(this.statsTimer)
this.statsTimer = null
}
},
createSwitchIcon() {
const container = document.getElementById('toggle-current-user')?.parentElement
if (!container) {
logger.error('未找到导航栏容器')
return
}
const li = document.createElement('li')
li.className = 'header-dropdown-toggle linux-do-tools'
this.link = document.createElement('a')
this.link.href = 'javascript:void(0)'
this.link.className = 'btn no-text icon btn-flat'
this.link.tabIndex = 0
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
svg.setAttribute('class', 'fa d-icon svg-icon prefix-icon svg-string')
this.use = document.createElementNS('http://www.w3.org/2000/svg', 'use')
this.updateIcon(switchControl.enabled)
svg.appendChild(this.use)
this.link.appendChild(svg)
li.appendChild(this.link)
this.link.addEventListener('click', () => {
this.updateIcon(switchControl.toggle())
})
container.parentNode.insertBefore(li, container.nextSibling)
},
}
// ==================== 核心功能 ====================
const core = {
currentIframe: null,
popstateHandler: null,
likeLimited: false, // 429 限制标志
likeLimitedWaitTime: '', // 429 等待时间(显示用)
likeLimitedUntil: 0, // 429 限制解除时间戳
// 检查是否应停止
shouldStop() {
if (stats.sessionViews >= CONFIG.limits.maxTopics) {
logger.info(`🛑 已达最大浏览数 ${CONFIG.limits.maxTopics}`)
return true
}
if (stats.getRunTime() >= CONFIG.limits.maxRunTime * 60) {
logger.info(`🛑 已达最大运行时间 ${CONFIG.limits.maxRunTime}分钟`)
return true
}
return false
},
// 停止脚本
stop() {
switchControl.enabled = false
stats.print()
logger.info('✨ 脚本已停止')
},
// 获取帖子列表
getTopics() {
const topics = []
const elements = document.querySelectorAll('#list-area .title')
elements.forEach((el) => {
const row = el.closest('tr')
if (!row || row.querySelector('.topic-statuses .pinned')) return
const viewsEl = row.querySelector('.num.views .number')
const viewsTitle = viewsEl?.getAttribute('title') || ''
topics.push({
title: el.textContent.trim(),
url: el.href,
views: utils.parseViewCount(viewsTitle),
})
})
logger.info(`📋 找到 ${topics.length} 个帖子`)
return topics
},
// 点赞操作
async likePost(targetWindow) {
if (this.likeLimited) {
logger.warn('点赞已被限制(429),跳过')
return
}
const doc = targetWindow.document
const csrfToken = doc.querySelector('meta[name="csrf-token"]')?.content
if (!csrfToken) {
logger.warn('未找到 CSRF token,跳过点赞')
return
}
// 筛选可点赞的帖子
const likeable = [...doc.querySelectorAll('button.btn-toggle-reaction-like')]
.filter((btn) => btn.title.includes('点赞此帖子'))
.map((btn) => btn.closest('article[data-post-id]')?.dataset?.postId)
.filter(Boolean)
.slice(0, CONFIG.limits.maxLikesPerPost)
if (!likeable.length) {
logger.debug('无可点赞帖子')
return
}
for (const postId of likeable) {
if (this.likeLimited) break
const ok = await this.sendLike(postId, csrfToken, targetWindow)
if (ok) {
stats.addLike()
logger.info('👍 点赞成功')
}
await utils.randomSleep(CONFIG.limits.likeInterval)
}
},
// 发送点赞请求(使用 iframe 的 fetch 绕过5秒盾)
async sendLike(postId, csrfToken, targetWindow) {
try {
// 使用 iframe 的 fetch,复用已通过 CF 验证的会话
const fetchFn = targetWindow?.fetch || fetch
const response = await fetchFn.call(
targetWindow || window,
`https://linux.do/discourse-reactions/posts/${postId}/custom-reactions/heart/toggle.json`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-Token': csrfToken,
},
credentials: 'include',
}
)
if (response.status === 429) {
let waitTime = ''
let waitSeconds = 0
try {
const text = await response.text()
const match = text.match(/(\d+)\s*(分钟|小时|秒)后/)
if (match) {
const num = parseInt(match[1])
const unit = match[2]
waitTime = `${num} ${unit}后`
// 转换为秒
waitSeconds = unit === '小时' ? num * 3600 : unit === '分钟' ? num * 60 : num
}
} catch {}
this.likeLimited = true
this.likeLimitedWaitTime = waitTime || '未知'
this.likeLimitedUntil = waitSeconds ? Date.now() + waitSeconds * 1000 : 0
logger.warn(`🚫 点赞频率限制(429),${waitTime ? waitTime + '可恢复' : '后续不再点赞'}`)
return false
}
if (!response.ok) {
logger.warn(`点赞失败: ${response.status}`)
return false
}
return true
} catch (e) {
logger.error('点赞请求失败:', e.message)
return false
}
},
// 浏览单个帖子
async browseTopic(topic) {
logger.info(`📖 浏览: ${topic.title}`)
stats.addView()
const iframe = document.createElement('iframe')
Object.assign(iframe.style, CONFIG.iframe)
iframe.src = `${topic.url}?_t=${Date.now()}`
// 清理旧的 iframe
if (this.currentIframe) {
this.currentIframe.remove()
}
this.currentIframe = iframe
// 防止 history 污染(只添加一次)
if (!this.popstateHandler) {
this.popstateHandler = (e) => e.stopPropagation()
window.addEventListener('popstate', this.popstateHandler, true)
}
document.body.appendChild(iframe)
// 等待加载
await new Promise((resolve) => {
iframe.onload = resolve
})
// 点赞
await this.likePost(iframe.contentWindow)
// 滚动浏览
await this.scrollIframe(iframe)
// 清理
iframe.remove()
this.currentIframe = null
stats.print()
},
// iframe 滚动
async scrollIframe(iframe) {
return new Promise((resolve) => {
const startTime = Date.now()
const { interval, step, duration } = CONFIG.scroll
const timer = setInterval(() => {
try {
const win = iframe.contentWindow
const doc = win.document.documentElement
const atBottom = win.scrollY + win.innerHeight + 1 >= doc.scrollHeight
const timeout = Date.now() - startTime >= duration * 1000
if (atBottom || timeout) {
clearInterval(timer)
resolve()
return
}
win.scrollBy(0, step)
} catch (e) {
clearInterval(timer)
resolve()
}
}, interval)
})
},
// 主浏览循环
async browseLoop() {
try {
const topics = utils.shuffle(this.getTopics())
for (const topic of topics) {
if (this.shouldStop()) {
this.stop()
return
}
if (!switchControl.enabled) {
logger.info('⏹️ 用户停止')
return
}
try {
await utils.withTimeout(this.browseTopic(topic), CONFIG.limits.browseTimeout)
} catch (e) {
logger.warn(`帖子浏览超时,跳过: ${topic.title}`)
}
await utils.randomSleep(3000)
}
// 继续下一轮
if (!this.shouldStop() && switchControl.enabled) {
ui.updateIcon(switchControl.toggle())
this.stop()
logger.info('📄 当前页面完成,停止10秒后重新启动...')
await utils.sleep(10000)
logger.info('🔄 已清理历史数据,重新开始浏览...')
ui.updateIcon(switchControl.toggle()) // 重新启用,跳转到 base URL
}
} catch (e) {
logger.error('浏览出错:', e.message)
}
},
}
// ==================== 入口 ====================
async function main() {
ui.createSwitchIcon()
if (!switchControl.enabled) return
stats.load()
if (window.location.href.includes(CONFIG.urls.base)) {
if (core.shouldStop()) {
core.stop()
return
}
await core.browseLoop()
}
}
// 启动(检查是否在 iframe 中,避免重复执行)
if (window.self !== window.top) {
logger.debug('当前在 iframe 中,跳过主逻辑')
} else {
const tryStart = () => {
if (document.body && document.getElementById('toggle-current-user')?.parentElement) {
main()
return true
}
return false
}
if (document.readyState === 'complete') {
tryStart() || setTimeout(main, 1000)
} else {
let started = false
const startOnce = () => {
if (started) return
started = tryStart()
}
// 多重保障:DOMContentLoaded、load、超时
document.addEventListener('DOMContentLoaded', startOnce)
window.addEventListener('load', startOnce)
// 超时兜底,防止页面长时间未加载完
setTimeout(() => {
if (!started) {
started = true
logger.warn('页面加载超时,强制启动')
main()
}
}, 50000)
}
}
})()