Auto check-in (silent API) + Human-like post scrolling + Smart auto replying (custom phrase database and daily limits) + Read-only idle mode + Glassmorphism floating panel
// ==UserScript==
// @name MaidUpgrade Pro
// @name:zh-CN 女仆论坛全自动升级助手 (极速拟真安全版)
// @namespace http://tampermonkey.net/
// @version 7.2
// @description Auto check-in (silent API) + Human-like post scrolling + Smart auto replying (custom phrase database and daily limits) + Read-only idle mode + Glassmorphism floating panel
// @description:zh-CN 自动签到(静默 API) + 拟真看帖滚动 + 智能自动水贴(自定义词库与日上限) + 纯阅读挂机模式 + 玻璃拟态控制台
// @author mochu
// @match *://bbs.bt.sb
// @match *://bbs.bt.sb/*
// @match https://bbs.bt.sb
// @match https://bbs.bt.sb/*
// @icon https://bbs.bt.sb/uploads/icon/icon-99224e973faa6f7d.png
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @run-at document-end
// @license MIT
// ==/UserScript==
(function () {
'use strict';
console.error('🚀 [MaidUpgrade Pro] v7.2 核心驱动加载中...');
// ═══════════════════════════════════════════
// 默 认 回 复 语 词 库 (v7.2 精选拟真语料库)
// ═══════════════════════════════════════════
const DEFAULT_PHRASES = [
'路过顶贴,支持一下!感谢大佬分享。',
'非常实用的技术分享,先收藏了!',
'原来是这样,今天又学到了新姿势。',
'祝论坛越办越好,打卡盖楼围观。',
'前排留名,感谢楼主提供这么好的资源!',
'日常签到水一把,低调路过。',
'这个思路太赞了,对我有很大启发!',
'战略性留名,感谢楼主无私分享,先赞后看!',
'满满的干货啊!正愁找不到这个,及时雨!',
'楼主出品,必属精品,果断收藏投币走一波。',
'感谢大佬深夜放福利,码字不易,必须顶起来!',
'每一个默默分享的大佬,都值得一记大大的赞!',
'找了很久的资源,终于在这里找到了,感谢楼主。',
'楼主好人一生平安,纯支持,顶上去让更多人看到!',
'细节拉满!这个解决方案比我之前见过的都优雅。',
'刚好卡在某个技术点上,看完大佬的分析瞬间豁然开朗!',
'思路清奇,角度刁钻,收藏起来留着慢慢消化。',
'理论与实操兼备,这才是高质量技术帖该有的样子。',
'细节决定成败,大佬对细节的把控简直无敌,受教了!',
'打卡留名,日常前排围观大佬操作。',
'今天的活跃度就靠这篇好帖了,顶起盖楼!',
'潜水多年,被这篇神帖硬生生炸出来了,必须支持!',
'日常签到,顺便给楼主的优秀分享递茶。',
'前排小板凳已搬好,坐等楼主后续更新!',
'纯路过,单看这排版就知道是精品,顶一下。',
'顺着大佬的思路理了一遍,确实避开了好多坑,感谢!',
'马住马住,回头实践一下!',
'业界良心,必须顶上去。',
'学到了学到了,涨姿势!',
'瑞思拜!大佬太强了。',
'已收藏,感谢无私分享!',
'留名备用,迟早用得上。'
];
// ═══════════════════════════════════════════
// 单 一 树 状 存 储 管 理 器 (仿 NodeSeek Pro)
// ═══════════════════════════════════════════
const DEFAULT_SETTINGS = {
version: '7.2',
cfg_auto_checkin: true,
cfg_auto_read: true,
cfg_auto_reply: false,
cfg_max_replies: 5,
cfg_read_min: 15,
cfg_read_max: 30,
cfg_phrases: DEFAULT_PHRASES.join('\n'),
is_running: false,
stat_date: '',
stat_replies: 0,
stat_reads: 0,
replied_list: [],
read_list: [],
panel_collapsed: false,
panel_position: null
};
let settingsCache = null;
const getStore = () => {
if (settingsCache) return settingsCache;
let saved = null;
try {
saved = GM_getValue('nv_helper_settings', null);
} catch (e) {
console.error('[MaidUpgrade Pro] GM_getValue 发生异常:', e);
}
if (!saved) {
// 升级迁移机制:如果检测到 旧的分散式存储,进行零损平滑迁移
saved = {};
const migrateKey = (oldKey, newKey, def) => {
try {
const val = GM_getValue(oldKey, undefined);
saved[newKey] = val === undefined ? def : val;
} catch {
saved[newKey] = def;
}
};
migrateKey('cfg_auto_checkin', 'cfg_auto_checkin', true);
migrateKey('cfg_auto_read', 'cfg_auto_read', true);
migrateKey('cfg_auto_reply', 'cfg_auto_reply', false);
migrateKey('cfg_max_replies', 'cfg_max_replies', 5);
migrateKey('cfg_read_min', 'cfg_read_min', 15);
migrateKey('cfg_read_max', 'cfg_read_max', 30);
migrateKey('cfg_phrases', 'cfg_phrases', DEFAULT_PHRASES.join('\n'));
migrateKey('is_running', 'is_running', false);
migrateKey('stat_date', 'stat_date', '');
migrateKey('stat_replies', 'stat_replies', 0);
migrateKey('stat_reads', 'stat_reads', 0);
migrateKey('panel_collapsed', 'panel_collapsed', false);
try {
saved.replied_list = JSON.parse(GM_getValue('replied_list', '[]'));
} catch (e) {
saved.replied_list = [];
}
try {
saved.read_list = JSON.parse(GM_getValue('read_list', '[]'));
} catch (e) {
saved.read_list = [];
}
try {
saved.panel_position = JSON.parse(GM_getValue('panel_position', 'null'));
} catch (e) {
saved.panel_position = null;
}
}
// 填充所有未定义字段为默认值
for (const key in DEFAULT_SETTINGS) {
if (saved[key] === undefined) {
saved[key] = DEFAULT_SETTINGS[key];
}
}
settingsCache = saved;
try {
GM_setValue('nv_helper_settings', settingsCache);
} catch (e) {
console.error('[MaidUpgrade Pro] GM_setValue 保存异常:', e);
}
return settingsCache;
};
const getCfg = (key, defVal) => {
const store = getStore();
return store[key] === undefined ? defVal : store[key];
};
const setCfg = (key, val) => {
const store = getStore();
store[key] = val;
try {
GM_setValue('nv_helper_settings', store);
} catch (e) {
console.error('[MaidUpgrade Pro] GM_setValue 保存异常:', e);
}
};
// 获取今日的 Key(用于统计和签到限制)
function getTodayKey() {
const d = new Date();
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
// ═══════════════════════════════════════════
// 全 局 样 式 注 入 (精致毛玻璃 UI)
// ═══════════════════════════════════════════
const STYLE = `
:root {
--nv-bg: rgba(20, 20, 28, 0.85);
--nv-border: rgba(255, 255, 255, 0.08);
--nv-primary: #8B5CF6;
--nv-primary-hover: #7C3AED;
--nv-success: #10B981;
--nv-info: #3B82F6;
--nv-warn: #F59E0B;
--nv-text: #F3F4F6;
--nv-text-sec: #9CA3AF;
}
.nv-panel {
position: fixed;
bottom: 20px;
right: 20px;
width: 320px;
background: var(--nv-bg);
backdrop-filter: blur(16px) saturate(180%);
-webkit-backdrop-filter: blur(16px) saturate(180%);
border: 1px solid var(--nv-border);
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
color: var(--nv-text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
z-index: 999999;
overflow: hidden;
transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
user-select: none;
}
.nv-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: rgba(255, 255, 255, 0.03);
border-bottom: 1px solid var(--nv-border);
cursor: move;
}
.nv-panel-title {
font-size: 13px;
font-weight: 700;
background: linear-gradient(135deg, #A78BFA, #60A5FA);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
display: flex;
align-items: center;
gap: 6px;
}
.nv-btn-icon {
background: none;
border: none;
color: var(--nv-text-sec);
cursor: pointer;
font-size: 14px;
padding: 4px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.nv-btn-icon:hover {
background: rgba(255, 255, 255, 0.08);
color: var(--nv-text);
}
.nv-panel-body {
padding: 14px 16px;
display: flex;
flex-direction: column;
gap: 12px;
max-height: 480px;
overflow-y: auto;
}
.nv-panel-body::-webkit-scrollbar {
width: 4px;
}
.nv-panel-body::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
.nv-section {
display: flex;
flex-direction: column;
gap: 8px;
background: rgba(255, 255, 255, 0.02);
padding: 10px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.02);
}
.nv-status-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
}
.nv-badge {
padding: 2px 8px;
border-radius: 20px;
font-size: 10px;
font-weight: 600;
}
.nv-badge-success { background: rgba(16, 185, 129, 0.15); color: #34D399; border: 1px solid rgba(16, 185, 129, 0.2); }
.nv-badge-info { background: rgba(59, 130, 246, 0.15); color: #60A5FA; border: 1px solid rgba(59, 130, 246, 0.2); }
.nv-badge-warn { background: rgba(245, 158, 11, 0.15); color: #FBBF24; border: 1px solid rgba(245, 158, 11, 0.2); }
.nv-badge-wait { background: rgba(156, 163, 175, 0.15); color: #D1D5DB; border: 1px solid rgba(156, 163, 175, 0.2); }
.nv-toggle-row {
display: flex;
align-items: center;
font-size: 12px;
}
.nv-toggle-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
width: 100%;
}
.nv-toggle-label input[type="checkbox"] {
accent-color: var(--nv-primary);
width: 14px;
height: 14px;
cursor: pointer;
}
.nv-param-row {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 11px;
color: var(--nv-text-sec);
}
.nv-param-row label {
display: flex;
justify-content: space-between;
}
.nv-param-row label span {
color: var(--nv-text);
font-weight: 600;
}
.nv-input-num {
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--nv-border);
border-radius: 6px;
color: var(--nv-text);
padding: 2px 6px;
font-size: 11px;
outline: none;
text-align: center;
transition: border-color 0.2s;
}
.nv-input-num:focus {
border-color: var(--nv-primary);
}
.nv-textarea {
width: 100%;
background: rgba(0, 0, 0, 0.2);
border: 1px solid var(--nv-border);
border-radius: 8px;
color: var(--nv-text);
padding: 6px;
font-size: 11px;
resize: vertical;
outline: none;
box-sizing: border-box;
}
.nv-textarea:focus {
border-color: var(--nv-primary);
}
.nv-logs {
height: 80px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--nv-border);
border-radius: 8px;
padding: 6px;
font-family: monospace;
font-size: 10px;
overflow-y: auto;
white-space: pre-wrap;
color: #A7F3D0;
display: flex;
flex-direction: column;
gap: 2px;
}
.nv-actions {
display: flex;
gap: 10px;
}
.nv-btn {
flex: 1;
padding: 8px 12px;
border: none;
border-radius: 10px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.nv-btn-primary {
background: linear-gradient(135deg, var(--nv-primary), #6366F1);
color: white;
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.2);
}
.nv-btn-primary:hover {
opacity: 0.95;
transform: translateY(-1px);
}
.nv-btn-primary:active {
transform: translateY(0);
}
.nv-btn-secondary {
background: rgba(255, 255, 255, 0.08);
color: var(--nv-text);
border: 1px solid var(--nv-border);
}
.nv-btn-secondary:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.12);
}
.nv-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
box-shadow: none !important;
}
/* 迷你悬浮标样式 */
.nv-panel-collapsed {
position: fixed;
bottom: 20px;
right: 20px;
width: 48px;
height: 48px;
background: var(--nv-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--nv-border);
border-radius: 50%;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 999999;
font-size: 20px;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.nv-panel-collapsed:hover {
transform: scale(1.1) rotate(15deg);
background: rgba(139, 92, 246, 0.2);
border-color: var(--nv-primary);
}
`;
// ═══════════════════════════════════════════
// 工 具 函 数
// ═══════════════════════════════════════════
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
const getCurrentUrl = () => window.location.href;
// 从 URL 深度提取 Rhex 唯一的 Post ID (CUID)
function extractPostId(url) {
if (!url) return null;
// Regex extracts any string starting with 'cmp' followed by exactly 22 alphanumeric characters (25 chars in total)
const match = url.match(/(cmp[a-z0-9]{22})/i);
return match ? match[1].toLowerCase() : null;
}
// Toast 浮窗提示
function showToast(msg, color = '#10B981') {
const el = document.createElement('div');
Object.assign(el.style, {
position: 'fixed', top: '20px', left: '50%', transform: 'translateX(-50%) translateY(-20px)',
zIndex: '9999999', padding: '12px 24px', borderRadius: '12px',
background: color, color: '#fff', fontSize: '13px', fontWeight: '600',
boxShadow: '0 10px 25px rgba(0,0,0,0.2)', opacity: '0',
transition: 'all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)', pointerEvents: 'none'
});
el.textContent = `⭐ ${msg}`;
document.body.appendChild(el);
requestAnimationFrame(() => {
el.style.opacity = '1';
el.style.transform = 'translateX(-50%) translateY(0)';
});
setTimeout(() => {
el.style.opacity = '0';
el.style.transform = 'translateX(-50%) translateY(-20px)';
setTimeout(() => el.remove(), 400);
}, 3000);
}
// 突破 Rhex 论坛 React 19 状态锁
function setReactInputValue(el, value) {
const valueSetter = Object.getOwnPropertyDescriptor(el, 'value')?.set;
const prototype = Object.getPrototypeOf(el);
const prototypeValueSetter = Object.getOwnPropertyDescriptor(prototype, 'value')?.set;
if (prototypeValueSetter && valueSetter !== prototypeValueSetter) {
prototypeValueSetter.call(el, value);
} else if (valueSetter) {
valueSetter.call(el, value);
} else {
el.value = value;
}
el.dispatchEvent(new Event('input', { bubbles: true }));
}
// ═══════════════════════════════════════════
// 控 制 台 类 与 UI 实 现
// ═══════════════════════════════════════════
class UpgradeHelper {
constructor() {
this.today = getTodayKey();
this.initStorage();
this.initDOM();
this.bindEvents();
this.updateUIStatus();
this.startDailyCheckIn();
// 如果挂机状态是开启的,加载后自动开始运作
if (this.isRunning) {
this.addLog('🔄 检测到挂机正在进行中,3秒后自动恢复执行...');
setTimeout(() => this.runRoutine(), 3000);
} else {
this.addLog('💤 挂机已暂停。点击“开始挂机”按钮启动。');
}
}
// 初始化本地运行数据
initStorage() {
// 从统一的 JSON 存储中读取开关状态
this.cfgAutoCheckin = getCfg('cfg_auto_checkin', true);
this.cfgAutoRead = getCfg('cfg_auto_read', true);
this.cfgAutoReply = getCfg('cfg_auto_reply', false);
this.isRunning = getCfg('is_running', false);
// 参数值
this.maxReplies = getCfg('cfg_max_replies', 5);
this.readMin = getCfg('cfg_read_min', 15);
this.readMax = getCfg('cfg_read_max', 30);
this.phrases = getCfg('cfg_phrases', DEFAULT_PHRASES.join('\n'));
// 每日计数器重置
const savedDate = getCfg('stat_date', '');
if (savedDate !== this.today) {
setCfg('stat_date', this.today);
setCfg('stat_replies', 0);
setCfg('stat_reads', 0);
this.todayReplies = 0;
this.todayReads = 0;
} else {
this.todayReplies = getCfg('stat_replies', 0);
this.todayReads = getCfg('stat_reads', 0);
}
// 黑名单/已读列表
this.repliedList = getCfg('replied_list', []);
this.readList = getCfg('read_list', []);
}
initDOM() {
// 注入 CSS 样式
const styleEl = document.createElement('style');
styleEl.innerHTML = STYLE;
document.head.appendChild(styleEl);
// 创建控制面板
this.panel = document.createElement('div');
this.panel.className = 'nv-panel';
this.panel.id = 'nv-panel-root';
this.panel.innerHTML = `
<div id="nv-panel-header" class="nv-panel-header">
<span class="nv-panel-title">⭐ MaidUpgrade Pro v7.2</span>
<button id="nv-minimize-btn" class="nv-btn-icon" title="折叠">➖</button>
</div>
<div class="nv-panel-body">
<!-- 状态展示 -->
<div class="nv-section">
<div class="nv-status-row">
<span>每日签到:</span>
<span id="nv-stat-checkin" class="nv-badge nv-badge-wait">检测中...</span>
</div>
<div class="nv-status-row">
<span>今日水贴:</span>
<span id="nv-stat-replies" class="nv-badge nv-badge-info">0 / 0</span>
</div>
<div class="nv-status-row">
<span>今日浏览:</span>
<span id="nv-stat-reads" class="nv-badge nv-badge-info">0 篇</span>
</div>
<div class="nv-status-row">
<span>助手状态:</span>
<span id="nv-stat-running" class="nv-badge nv-badge-wait">未启动</span>
</div>
</div>
<!-- 开关控制 -->
<div class="nv-section">
<div class="nv-toggle-row">
<label class="nv-toggle-label">
<input type="checkbox" id="chk-checkin" ${this.cfgAutoCheckin ? 'checked' : ''}> 自动静默签到
</label>
</div>
<div class="nv-toggle-row">
<label class="nv-toggle-label">
<input type="checkbox" id="chk-read" ${this.cfgAutoRead ? 'checked' : ''}> 自动挂机浏览
</label>
</div>
<div class="nv-toggle-row">
<label class="nv-toggle-label">
<input type="checkbox" id="chk-reply" ${this.cfgAutoReply ? 'checked' : ''}> 自动水贴升级
</label>
</div>
</div>
<!-- 高级参数 -->
<div class="nv-section">
<div class="nv-param-row">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="color: var(--nv-text); font-weight: 600;">每日水贴上限:</span>
<div style="display: flex; gap: 4px; align-items: center;">
<input type="number" id="num-max-replies" class="nv-input-num" style="width: 70px;" min="1" max="500" value="${this.maxReplies}">
<span style="font-size: 11px;">次</span>
</div>
</div>
</div>
<div class="nv-param-row">
<label>看贴等待时间 (秒):</label>
<div style="display: flex; gap: 8px; align-items: center;">
<input type="number" id="num-min" class="nv-input-num" style="width: 60px;" value="${this.readMin}">
<span>至</span>
<input type="number" id="num-max" class="nv-input-num" style="width: 60px;" value="${this.readMax}">
<span>秒随机</span>
</div>
</div>
</div>
<!-- 自定义词库 -->
<div class="nv-section">
<label style="font-size: 11px; margin-bottom: 2px; display: block; color: var(--nv-text-sec);">自定义回复词库 (一行一条,自动去重):</label>
<textarea id="txt-phrases" class="nv-textarea" rows="3">${this.phrases}</textarea>
</div>
<!-- 动态日志 -->
<div class="nv-section" style="padding-bottom: 0;">
<div id="nv-logger-box" class="nv-logs"></div>
</div>
<!-- 控制按钮 -->
<div class="nv-actions">
<button id="btn-nv-start" class="nv-btn nv-btn-primary" style="flex: 2;">🟢 开始挂机</button>
<button id="btn-nv-pause" class="nv-btn nv-btn-secondary" disabled>🟡 暂停</button>
<button id="btn-nv-reset" class="nv-btn nv-btn-secondary" title="重置今日计数与已水贴黑名单" style="flex: 1; padding: 8px 4px;">🧹 重置</button>
</div>
</div>
`;
document.body.appendChild(this.panel);
// 创建折叠后的迷你悬浮标
this.collapsedIcon = document.createElement('div');
this.collapsedIcon.className = 'nv-panel-collapsed';
this.collapsedIcon.innerHTML = '⭐';
this.collapsedIcon.style.display = 'none';
document.body.appendChild(this.collapsedIcon);
// 读取折叠状态
this.isCollapsed = getCfg('panel_collapsed', false);
if (this.isCollapsed) {
this.panel.style.display = 'none';
this.collapsedIcon.style.display = 'flex';
}
// 读取悬浮窗坐标并恢复位置
const panelPos = getCfg('panel_position', null);
if (panelPos) {
this.panel.style.bottom = 'auto';
this.panel.style.right = 'auto';
this.panel.style.left = panelPos.left + 'px';
this.panel.style.top = panelPos.top + 'px';
}
}
bindEvents() {
// 折叠与展开事件
const minimizeBtn = this.panel.querySelector('#nv-minimize-btn');
minimizeBtn.addEventListener('click', () => {
this.panel.style.display = 'none';
this.collapsedIcon.style.display = 'flex';
setCfg('panel_collapsed', true);
});
this.collapsedIcon.addEventListener('click', () => {
this.panel.style.display = 'block';
this.collapsedIcon.style.display = 'none';
setCfg('panel_collapsed', false);
});
// 绑定表单交互,实时保存配置
const chkCheckin = this.panel.querySelector('#chk-checkin');
chkCheckin.addEventListener('change', (e) => {
this.cfgAutoCheckin = e.target.checked;
setCfg('cfg_auto_checkin', this.cfgAutoCheckin);
this.addLog(`⚙️ 自动签到开关已${this.cfgAutoCheckin ? '【开启】' : '【关闭】'}`);
this.startDailyCheckIn();
});
const chkRead = this.panel.querySelector('#chk-read');
chkRead.addEventListener('change', (e) => {
this.cfgAutoRead = e.target.checked;
setCfg('cfg_auto_read', this.cfgAutoRead);
this.addLog(`⚙️ 挂机浏览开关已${this.cfgAutoRead ? '【开启】' : '【关闭】'}`);
});
const chkReply = this.panel.querySelector('#chk-reply');
chkReply.addEventListener('change', (e) => {
this.cfgAutoReply = e.target.checked;
setCfg('cfg_auto_reply', this.cfgAutoReply);
this.addLog(`⚙️ 自动水贴升级开关已${this.cfgAutoReply ? '【开启】' : '【关闭】'}`);
if (this.cfgAutoReply) {
showToast('自动水贴已启用,请注意安全!', '#F59E0B');
}
});
// 直接填写数字上限绑定
const numMaxReplies = this.panel.querySelector('#num-max-replies');
numMaxReplies.addEventListener('change', (e) => {
let val = parseInt(e.target.value) || 5;
if (val < 1) val = 1;
this.maxReplies = val;
numMaxReplies.value = val;
setCfg('cfg_max_replies', this.maxReplies);
this.updateUIStatus();
this.addLog(`⚙️ 每日水贴上限已更新为: ${this.maxReplies} 次`);
});
numMaxReplies.addEventListener('input', (e) => {
let val = parseInt(e.target.value);
if (!isNaN(val) && val >= 1) {
this.maxReplies = val;
setCfg('cfg_max_replies', this.maxReplies);
this.updateUIStatus();
}
});
const numMin = this.panel.querySelector('#num-min');
const numMax = this.panel.querySelector('#num-max');
const saveTimeConfig = () => {
let min = parseInt(numMin.value) || 5;
let max = parseInt(numMax.value) || 10;
if (min > max) { min = max; numMin.value = min; }
this.readMin = min;
this.readMax = max;
setCfg('cfg_read_min', min);
setCfg('cfg_read_max', max);
this.addLog(`⚙️ 阅读等待时间配置已更新: ${min} 至 ${max} 秒`);
};
numMin.addEventListener('change', saveTimeConfig);
numMax.addEventListener('change', saveTimeConfig);
// 词库配置全自纠错与去重过滤器
const txtPhrases = this.panel.querySelector('#txt-phrases');
txtPhrases.addEventListener('change', (e) => {
const list = e.target.value.split('\n')
.map(p => p.trim())
.filter(p => p.length > 0);
const uniqueList = [...new Set(list)];
this.phrases = uniqueList.join('\n');
txtPhrases.value = this.phrases;
setCfg('cfg_phrases', this.phrases);
this.addLog(`⚙️ 自定义回复词库已更新,已过滤空行并自动去重 (共 ${uniqueList.length} 条)`);
});
// 拖拽面板功能
const header = this.panel.querySelector('#nv-panel-header');
let isDragging = false;
let startX, startY;
header.addEventListener('mousedown', (e) => {
if (e.target.tagName === 'BUTTON') return;
isDragging = true;
startX = e.clientX - this.panel.offsetLeft;
startY = e.clientY - this.panel.offsetTop;
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
let left = e.clientX - startX;
let top = e.clientY - startY;
// 边界限制
left = Math.max(0, Math.min(window.innerWidth - this.panel.offsetWidth, left));
top = Math.max(0, Math.min(window.innerHeight - this.panel.offsetHeight, top));
this.panel.style.bottom = 'auto';
this.panel.style.right = 'auto';
this.panel.style.left = left + 'px';
this.panel.style.top = top + 'px';
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
// 保存悬浮面板坐标
setCfg('panel_position', {
left: this.panel.offsetLeft,
top: this.panel.offsetTop
});
}
});
// 开始、暂停与重置按钮
const btnStart = this.panel.querySelector('#btn-nv-start');
const btnPause = this.panel.querySelector('#btn-nv-pause');
const btnReset = this.panel.querySelector('#btn-nv-reset');
btnStart.addEventListener('click', () => {
this.isRunning = true;
setCfg('is_running', true);
btnStart.disabled = true;
btnPause.disabled = false;
this.addLog('🟢 挂机升级助手启动中...');
showToast('升级助手已启动!', '#10B981');
this.updateUIStatus();
this.runRoutine();
});
btnPause.addEventListener('click', () => {
this.isRunning = false;
setCfg('is_running', false);
btnStart.disabled = false;
btnPause.disabled = true;
this.addLog('🟡 挂机升级助手暂停,可随时继续!');
showToast('已暂停挂机!', '#F59E0B');
this.updateUIStatus();
});
btnReset.addEventListener('click', () => {
if (confirm('🧹 确认清空今日统计数据以及“已水贴”黑名单缓存吗?\n(重置后可对同一个帖子重新挂机测试)')) {
this.todayReplies = 0;
this.todayReads = 0;
this.repliedList = [];
this.readList = [];
setCfg('stat_replies', 0);
setCfg('stat_reads', 0);
setCfg('replied_list', []);
setCfg('read_list', []);
this.updateUIStatus();
this.addLog('🧹 本地计数与已回复黑名单缓存已全部清空重置!');
showToast('重置成功!', '#3B82F6');
}
});
if (this.isRunning) {
btnStart.disabled = true;
btnPause.disabled = false;
}
}
// 添加日志到控制台
addLog(msg) {
const loggerBox = this.panel.querySelector('#nv-logger-box');
if (loggerBox) {
const time = new Date().toLocaleTimeString();
const logItem = document.createElement('div');
logItem.textContent = `[${time}] ${msg}`;
loggerBox.appendChild(logItem);
// 限制最多 50 行,防止DOM过多卡顿
while (loggerBox.children.length > 50) {
loggerBox.children[0].remove();
}
loggerBox.scrollTop = loggerBox.scrollHeight;
}
console.log(`[MaidUpgrade Pro] ${msg}`);
}
// 更新状态数据面板显示
updateUIStatus() {
const badgeCheckin = this.panel.querySelector('#nv-stat-checkin');
const badgeReplies = this.panel.querySelector('#nv-stat-replies');
const badgeReads = this.panel.querySelector('#nv-stat-reads');
const badgeRunning = this.panel.querySelector('#nv-stat-running');
// 签到状态
const isTodayChecked = localStorage.getItem(`nv_checkin_${this.today}`);
if (isTodayChecked) {
badgeCheckin.className = 'nv-badge nv-badge-success';
badgeCheckin.textContent = '已签到';
} else {
badgeCheckin.className = 'nv-badge nv-badge-warn';
badgeCheckin.textContent = '未签到';
}
// 水贴进度
badgeReplies.textContent = `${this.todayReplies} / ${this.maxReplies}`;
if (this.todayReplies >= this.maxReplies) {
badgeReplies.className = 'nv-badge nv-badge-success';
} else {
badgeReplies.className = 'nv-badge nv-badge-info';
}
// 浏览计数
badgeReads.textContent = `${this.todayReads} 篇`;
// 运行状态
if (this.isRunning) {
badgeRunning.className = 'nv-badge nv-badge-success';
badgeRunning.textContent = '正在运行';
} else {
badgeRunning.className = 'nv-badge nv-badge-wait';
badgeRunning.textContent = '已暂停';
}
}
// 每日签到(静默 API 版)
async startDailyCheckIn() {
if (!this.cfgAutoCheckin) return;
const checkinKey = `nv_checkin_${this.today}`;
if (localStorage.getItem(checkinKey)) {
this.updateUIStatus();
return;
}
// 检查登录状态
if (!this.checkLoginStatus()) {
this.addLog('⚠️ 未检测到登录状态,放弃签到API调用');
return;
}
this.addLog('📡 正在静默向 API 发送每日签到请求...');
try {
const resp = await fetch('https://bbs.bt.sb/api/check-in', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include', // 携带 Cookie 凭证
body: JSON.stringify({ action: 'check-in' })
});
if (resp.ok) {
const data = await resp.json().catch(() => ({}));
const payload = data?.data || data;
localStorage.setItem(checkinKey, '1');
this.updateUIStatus();
if (payload.alreadyCheckedIn) {
this.addLog('📌 今日已经签过到啦!');
showToast('今日已签到!', '#3B82F6');
} else {
const reward = payload.points || payload.reward || '';
const streak = payload.currentStreak ? `(连续${payload.currentStreak}天)` : '';
this.addLog(`✅ 签到成功!额外奖励 +${reward} 积分 ${streak}`);
showToast(`签到成功!+${reward} 积分`, '#10B981');
}
} else if (resp.status === 401 || resp.status === 403) {
this.addLog('⚠️ 签到失败:无访问权限,请检查是否处于登录状态。');
} else {
this.addLog(`⚠️ 签到异常:HTTP ${resp.status}`);
}
} catch (err) {
this.addLog(`❌ 签到请求失败:${err.message}`);
}
}
// 检测用户登录状态
checkLoginStatus() {
const loginBtn = document.querySelector('a[href="/login"]');
if (loginBtn && loginBtn.offsetParent !== null) {
return false; // 有登录按钮且可见,代表未登录
}
const avatar = document.querySelector('[class*="avatar"], [class*="Avatar"], img[alt*="avatar"]');
if (avatar) return true;
return !loginBtn;
}
// ═══════════════════════════════════════════
// 核 心 控制 流 (Routine)
// ═══════════════════════════════════════════
async runRoutine() {
if (!this.isRunning) return;
// 首先执行签到
await this.startDailyCheckIn();
// 如果挂机浏览没有开启,我们什么也不做
if (!this.cfgAutoRead) {
this.addLog('💤 自动挂机浏览未开启,无任务。');
return;
}
// 判断当前所在页面
if (getCurrentUrl().includes('/posts/')) {
// 在帖子详情页,执行看帖/回复逻辑
await this.handlePostPage();
} else {
// 在列表页/首页,寻找符合条件的新帖并跳转
await this.handleListPage();
}
}
// 列表页处理逻辑:寻找可以浏览的新贴(CUID 深度去重新机制)
async handleListPage() {
this.addLog('🔍 正在扫描可用帖子链接...');
await delay(1500);
// 1. 获取 DOM 中所有包含帖子详情的 a 标签链接
let rawLinks = Array.from(document.querySelectorAll('a'))
.map(a => a.href)
.filter(href => href && href.includes('/posts/') && !href.endsWith('/posts/'));
// 2. 按 CUID 对候选帖子进行完全去重与筛选,排除已回复和已读的帖子
let seenPids = new Set();
let candidates = [];
for (let url of rawLinks) {
const pid = extractPostId(url);
if (!pid) continue;
// 如果当前循环已遇到该 CUID,或者该 CUID 已在本地回复/阅读黑名单中,彻底排除
if (seenPids.has(pid)) continue;
if (this.repliedList.includes(pid)) continue;
if (this.readList.includes(pid)) continue;
seenPids.add(pid);
candidates.push({ pid, url });
}
// 3. 兜底策略:如果全部链接都处理过了,但在列表页里还有没回复过的帖子,可以选择只读过但没回复的帖子
if (candidates.length === 0) {
seenPids.clear();
for (let url of rawLinks) {
const pid = extractPostId(url);
if (!pid) continue;
if (seenPids.has(pid)) continue;
if (this.repliedList.includes(pid)) continue;
seenPids.add(pid);
candidates.push({ pid, url });
}
}
if (candidates.length > 0) {
// 🎲 真正的无偏物理随机数摇号!
let selected = candidates[Math.floor(Math.random() * candidates.length)];
let targetPost = selected.url;
this.addLog(`🎲 随机锁定未处理帖子 (共 ${candidates.length} 个候选),2秒后跳转...`);
await delay(2000);
window.location.href = targetPost;
// SPA 挂靠:延时触发强制刷新,以重置油猴运行环境
setTimeout(() => {
if (window.location.href !== getCurrentUrl()) window.location.reload();
}, 500);
} else {
this.addLog('⚠️ 当前页面所有的帖子你都已经回复过了!10秒后尝试刷新页面寻找新帖子...');
await delay(10000);
if (this.isRunning) {
window.location.reload();
}
}
}
// 帖子详情页处理逻辑:看帖模拟和回复
async handlePostPage() {
this.addLog('📖 已进入帖子,开始模拟人类阅读行为...');
await delay(1500);
// 模拟人类平滑向下滚动阅读
const scrollHeight = document.body.scrollHeight;
this.addLog('📜 平滑滚动至帖子中部...');
window.scrollTo({ top: scrollHeight * 0.4, behavior: 'smooth' });
await delay(2000);
this.addLog('📜 平滑滚动至评论区...');
window.scrollTo({ top: scrollHeight * 0.85, behavior: 'smooth' });
await delay(3000);
// 随机等待时间计算
const waitTime = Math.floor(Math.random() * (this.readMax - this.readMin + 1)) + this.readMin;
// 获取当前帖子唯一的 Post CUID
const currentPostId = extractPostId(getCurrentUrl());
// 是否需要自动水贴回复?
const canWater = this.cfgAutoReply &&
(this.todayReplies < this.maxReplies) &&
currentPostId &&
!this.repliedList.includes(currentPostId);
if (canWater) {
this.addLog(`💬 计划在本帖子下水贴。随机阅读倒计时: ${waitTime} 秒后自动输入回复...`);
await this.countdown(waitTime);
if (!this.isRunning) return;
// 执行回复
const replySuccess = await this.executeReply();
if (replySuccess) {
this.todayReplies++;
setCfg('stat_replies', this.todayReplies);
if (currentPostId) this.repliedList.push(currentPostId);
if (this.repliedList.length > 500) this.repliedList.shift();
setCfg('replied_list', this.repliedList);
this.updateUIStatus();
this.addLog(`✅ 水贴任务完成!今日水贴已达: ${this.todayReplies} / ${this.maxReplies}`);
}
} else {
// 纯挂机浏览模式
this.addLog(`👀 纯阅读挂机中。随机阅读倒计时: ${waitTime} 秒...`);
await this.countdown(waitTime);
if (!this.isRunning) return;
this.todayReads++;
setCfg('stat_reads', this.todayReads);
if (currentPostId) this.readList.push(currentPostId);
if (this.readList.length > 500) this.readList.shift();
setCfg('read_list', this.readList);
this.updateUIStatus();
this.addLog(`✅ 成功阅读此帖,今日已刷 ${this.todayReads} 篇。`);
}
// 结束,返回首页继续下一个
this.addLog('🚀 任务结束,3秒后返回主页寻找下一个目标帖子...');
await delay(3000);
if (this.isRunning) {
window.location.href = 'https://bbs.bt.sb/';
setTimeout(() => {
window.location.reload();
}, 500);
}
}
// 倒计时动画提示
async countdown(seconds) {
const badgeRunning = this.panel.querySelector('#nv-stat-running');
for (let i = seconds; i > 0; i--) {
if (!this.isRunning) return;
badgeRunning.className = 'nv-badge nv-badge-warn';
badgeRunning.textContent = `倒计时 ${i}s`;
await delay(1000);
}
if (this.isRunning) {
badgeRunning.className = 'nv-badge nv-badge-success';
badgeRunning.textContent = '正在运行';
}
}
// 执行发帖/回复动作 (100% 稳定的 API 静默评论机制)
async executeReply() {
this.addLog('✍️ 正在筹备回复任务...');
// 从 URL 提取 postId (Rhex 论坛帖子 25位纯净 CUID)
let postId = extractPostId(window.location.href);
// 获取回复的随机文本
const phraseList = this.phrases.split('\n').map(p => p.trim()).filter(p => p.length > 0);
const finalPhrases = phraseList.length > 0 ? phraseList : DEFAULT_PHRASES;
const randomText = finalPhrases[Math.floor(Math.random() * finalPhrases.length)];
// 【第一选择:API 静默提交】直接秒发,100% 成功!
if (postId && postId.startsWith('cmp')) {
this.addLog(`📡 锁定 Post CUID: ${postId},正在通过后台 API 发送评论回复...`);
try {
const resp = await fetch('https://bbs.bt.sb/api/comments/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include', // 携带当前已登录的 Cookie 凭证
body: JSON.stringify({
postId: postId,
content: randomText,
useAnonymousIdentity: false, // 设为 false 以便将经验/积分加给您的账号
commentView: 'tree'
})
});
if (resp.ok) {
const data = await resp.json().catch(() => ({}));
this.addLog(`✅ API 静默回复成功!内容: "${randomText}"`);
showToast(`静默回复成功!`, '#10B981');
return true;
} else {
this.addLog(`⚠️ API 回复返回异常 (HTTP ${resp.status}),准备滑入 DOM 模拟兜底方案...`);
}
} catch (err) {
this.addLog(`⚠️ API 提交网络异常: ${err.message},准备滑入 DOM 模拟兜底...`);
}
} else {
this.addLog('⚠️ 未能在 URL 中识别到有效的 Post CUID,正在滑入 DOM 模拟兜底方案...');
}
// 【第二选择:DOM 模拟兜底】
let textarea = document.querySelector('.ProseMirror') ||
document.querySelector('[contenteditable="true"]') ||
Array.from(document.querySelectorAll('textarea')).find(el => el.offsetWidth > 0 || el.offsetHeight > 0);
if (!textarea) {
this.addLog('📜 正在向下滑动以加载评论区...');
window.scrollTo(0, document.body.scrollHeight);
await delay(1500);
textarea = document.querySelector('.ProseMirror') ||
document.querySelector('[contenteditable="true"]') ||
Array.from(document.querySelectorAll('textarea')).find(el => el.offsetWidth > 0 || el.offsetHeight > 0);
}
if (!textarea) {
this.addLog('❌ 无法定位到评论输入框,挂机跳过该贴。');
return false;
}
// 锁定按钮
let container = textarea.closest('form') ||
textarea.closest('div[class*="editor"]') ||
textarea.closest('div[class*="reply"]') ||
textarea.closest('div[class*="comment"]') ||
textarea.parentElement?.parentElement?.parentElement ||
document.body;
this.addLog('✍️ 正在局部定位对应的提交按钮...');
let submitBtn = container.querySelector('button[type="submit"]') ||
Array.from(container.querySelectorAll('button')).find(el => {
const txt = el.textContent.trim();
return txt === '回复' || txt === '发送' || txt.includes('发表') || txt.includes('提交');
});
if (!submitBtn) {
submitBtn = document.querySelector('button[type="submit"]') ||
Array.from(document.querySelectorAll('button')).find(el => {
const txt = el.textContent.trim();
return txt === '回复' || txt === '发送' || txt.includes('发表回复') || txt.includes('提交');
});
}
if (textarea && submitBtn) {
let focusTarget = textarea.querySelector('p') || textarea;
focusTarget.focus();
await delay(300);
try {
if (textarea.tagName === 'TEXTAREA') {
textarea.select();
} else {
const range = document.createRange();
range.selectNodeContents(textarea);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
} catch (e) {
this.addLog('⚠️ 模拟文本选择时遇到轻微异常,继续输入...');
}
await delay(200);
if (textarea.tagName === 'TEXTAREA') {
setReactInputValue(textarea, randomText);
} else {
try {
const beforeInputEvent = new InputEvent('beforeinput', {
inputType: 'insertText',
data: randomText,
bubbles: true,
cancelable: true
});
focusTarget.dispatchEvent(beforeInputEvent);
} catch (e) {
console.warn('beforeinput 尝试失败:', e);
}
await delay(100);
if (!textarea.textContent.includes(randomText)) {
try {
document.execCommand('insertText', false, randomText);
} catch (err) {
console.warn('execCommand 尝试失败:', err);
}
await delay(100);
}
if (!textarea.textContent.includes(randomText)) {
try {
const dataTransfer = new DataTransfer();
dataTransfer.setData('text/plain', randomText);
const pasteEvent = new ClipboardEvent('paste', {
clipboardData: dataTransfer,
bubbles: true,
cancelable: true
});
focusTarget.dispatchEvent(pasteEvent);
} catch (err) {
console.warn('Paste 事件模拟失败:', err);
}
}
textarea.dispatchEvent(new Event('input', { bubbles: true }));
textarea.dispatchEvent(new Event('change', { bubbles: true }));
}
this.addLog(`📝 输入拟真词汇: "${randomText}"`);
await delay(2000);
if (!this.isRunning) return false;
if (submitBtn.disabled) {
this.addLog('⚠️ 发现发送按钮处于锁定状态,尝试激活...');
submitBtn.disabled = false;
}
this.addLog('📡 正在提交回复...');
submitBtn.focus();
submitBtn.click();
submitBtn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
await delay(4000);
return true;
} else {
this.addLog('❌ 定位回复框或提交按钮失败,可能该贴被锁定、已被禁言或未登录。');
return false;
}
}
}
// ═══════════════════════════════════════════
// 单 页 路 由 (SPA) 无 感 双 向 监 听
// ═══════════════════════════════════════════
let lastUrl = window.location.href;
setInterval(() => {
if (window.location.href !== lastUrl) {
const oldUrl = lastUrl;
lastUrl = window.location.href;
const oldIsPost = oldUrl.includes('/posts/');
const newIsPost = lastUrl.includes('/posts/');
if (oldIsPost !== newIsPost) {
console.error('🔄 [MaidUpgrade Pro] 检测到 Next.js SPA 路由切换,物理刷新重置油猴环境...');
window.location.reload();
}
}
}, 500);
// ═══════════════════════════════════════════
// 启 动 入 口
// ═══════════════════════════════════════════
function startPlugin() {
if (window.nvUpgradeHelper) return;
window.nvUpgradeHelper = new UpgradeHelper();
}
if (document.readyState === 'complete' || document.readyState === 'interactive') {
startPlugin();
} else {
window.addEventListener('DOMContentLoaded', startPlugin);
window.addEventListener('load', startPlugin);
}
})();