// ==UserScript==
// @name skyeysnow auto reply
// @namespace Violentmonkey Scripts
// @version 1.0.0
// @author Roach Lin / Modified
// @description 天雪论坛水楼自动回复脚本(超级防御型代码,详见说明)。仅供学习与研究,使用者需自行承担账号风险。
// @match https://skyeysnow.com/*
// @match http://skyeysnow.com/*
// @match https://www.skyey2.com/*
// @match http://www.skyey2.com/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @run-at document-idle
// @license MIT
// ==/UserScript==
/*
* 免责声明
* 本脚本用于在该论坛水楼帖自动回复。论坛将此类自动化行为视为违规操作
* 可能导致封号、扣分、封 IP 等后果。请自行评估并承担风险,作者不对使用后果负责。
*/
(function () {
'use strict';
// ================= 存储键 =================
var STORAGE_KEY_URL = 'skyeysnow_water_tid';
var STORAGE_KEY_DATE = 'skyeysnow_water_set_ym';
var STORAGE_KEY_DELAY_MIN = 'skyeysnow_reply_delay_min';
var STORAGE_KEY_DELAY_MAX = 'skyeysnow_reply_delay_max';
var STORAGE_KEY_AUTO_JUMP = 'skyeysnow_auto_jump';
var STORAGE_KEY_POEMS = 'skyeysnow_poems';
var STORAGE_KEY_NIGHT_START = 'skyeysnow_night_start';
var STORAGE_KEY_NIGHT_END = 'skyeysnow_night_end';
var STORAGE_KEY_NIGHT_DELAY_MIN = 'skyeysnow_night_delay_min';
var STORAGE_KEY_NIGHT_DELAY_MAX = 'skyeysnow_night_delay_max';
var STORAGE_KEY_NIGHT_MAX_COUNT = 'skyeysnow_night_max_count';
var STORAGE_KEY_NIGHT_SENT = 'skyeysnow_night_sent_count';
var STORAGE_KEY_NIGHT_DATE = 'skyeysnow_night_date';
var STORAGE_KEY_NIGHT_WARNED = 'skyeysnow_night_warned';
var STORAGE_KEY_PENDING_TARGET = 'skyeysnow_pending_target_time';
var STORAGE_KEY_LOG = 'skyeysnow_operation_log';
var STORAGE_KEY_MAX_LOG = 'skyeysnow_max_log';
var STORAGE_KEY_MY_USERNAME = 'skyeysnow_my_username';
var STORAGE_KEY_POST_GAP = 'skyeysnow_post_gap';
// 主控选举相关键
var STORAGE_KEY_LEADER_ID = 'skyeysnow_leader_id';
var STORAGE_KEY_LEADER_HB = 'skyeysnow_leader_heartbeat';
// 一次性会话标志(sessionStorage:同标签页刷新保留,关闭即失效)
var SESSION_KEY_NEED_REFRESH = 'skyeysnow_need_refresh';
var SESSION_KEY_NO_USERNAME_ALERTED = 'skyeysnow_no_username_alerted';
var SESSION_KEY_SKIP_TID_PROMPT = 'skyeysnow_skip_tid_prompt';
var SESSION_KEY_TAB_ID = 'skyeysnow_tab_id';
// 日志渲染回调(模块内私有,避免污染 window)
var _logDisplayCallback = null;
var _settingsOpen = false;
// 本标签页唯一 ID(sessionStorage:刷新保留、关标签页失效)
// sessionStorage 在隐私模式 / 企业策略下可能抛 SecurityError,
// 若此处不捕获会导致整个脚本静默不执行,故降级为内存级 ID。
var TAB_ID = (function () {
try {
var id = sessionStorage.getItem(SESSION_KEY_TAB_ID);
if (!id) {
id = 'tab_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8);
sessionStorage.setItem(SESSION_KEY_TAB_ID, id);
}
return id;
} catch (e) {
console.warn('[AutoReply] sessionStorage 不可用,TAB_ID 降级为内存级:', e);
return 'tab_volatile_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8);
}
})();
// 主控心跳参数
var LEADER_TIMEOUT_MS = 6000; // 心跳超过此时间未更新,主控视为已失效
var HEARTBEAT_INTERVAL_MS = 2000; // 主控心跳间隔
var _heartbeatTimer = null;
// ================= 工具函数 =================
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
});
}
function getThreadTid() {
var params = new URLSearchParams(window.location.search);
return params.get('tid');
}
function getCurrentYM() {
var d = new Date();
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0');
}
// 日志时间戳(含日期,便于跨天区分)
function getTimestamp() {
var d = new Date();
return d.getFullYear() + '-' +
String(d.getMonth() + 1).padStart(2, '0') + '-' +
String(d.getDate()).padStart(2, '0') + ' ' +
String(d.getHours()).padStart(2, '0') + ':' +
String(d.getMinutes()).padStart(2, '0') + ':' +
String(d.getSeconds()).padStart(2, '0');
}
// 上限非法时的统一回退规则:至少比 min 大 10,且不低于该配置的默认上限。
// 抽取以避免各处回退规则不一致(历史上曾出现 min+10 与固定值 120 两种写法)。
function fallbackMax(min, defMax) {
return Math.max(min + 10, defMax);
}
function getDelayMin() {
var v = Number(GM_getValue(STORAGE_KEY_DELAY_MIN, 25));
return isNaN(v) || v < 1 ? 25 : v;
}
function getDelayMax() {
var v = Number(GM_getValue(STORAGE_KEY_DELAY_MAX, 35));
var min = getDelayMin();
return isNaN(v) || v < min ? fallbackMax(min, 35) : v;
}
function shouldAutoJump() {
var v = GM_getValue(STORAGE_KEY_AUTO_JUMP, true);
return v !== false && v !== 'false';
}
function getPoems() {
var raw = GM_getValue(STORAGE_KEY_POEMS, '');
if (raw && String(raw).trim().length > 0) {
return String(raw).split('\n').filter(function (line) {
return line.trim().length > 0;
});
}
return [
'山有木兮木有枝,心悦君兮君不知。',
'人生若只如初见,何事秋风悲画扇。',
'十年生死两茫茫,不思量,自难忘。',
'曾经沧海难为水,除却巫山不是云。',
'玲珑骰子安红豆,入骨相思知不知。',
'只愿君心似我心,定不负相思意。',
'平生不会相思,才会相思,便害相思。',
'愿得一心人,白头不相离。',
'山无陵,江水为竭。冬雷震震,夏雨雪。天地合,乃敢与君绝。',
'入我相思门,知我相思苦。'
];
}
function isNightTime() {
var start = Number(GM_getValue(STORAGE_KEY_NIGHT_START, 23));
var end = Number(GM_getValue(STORAGE_KEY_NIGHT_END, 8));
start = isNaN(start) ? 23 : start;
end = isNaN(end) ? 8 : end;
var hour = new Date().getHours();
if (start <= end) {
return hour >= start && hour < end;
}
return hour >= start || hour < end;
}
// 当前夜间时段的唯一标识(跨零点时归属到开始那天)
function getNightPeriodKey() {
var start = Number(GM_getValue(STORAGE_KEY_NIGHT_START, 23));
var end = Number(GM_getValue(STORAGE_KEY_NIGHT_END, 8));
start = isNaN(start) ? 23 : start;
end = isNaN(end) ? 8 : end;
var now = new Date();
var hour = now.getHours();
var periodDate = new Date(now);
// 跨零点时段,零点之后仍属于"昨天开始"的那个夜间时段
if (start > end && hour < end) {
periodDate.setDate(periodDate.getDate() - 1);
}
return periodDate.getFullYear() + '-' +
String(periodDate.getMonth() + 1).padStart(2, '0') + '-' +
String(periodDate.getDate()).padStart(2, '0');
}
function getCurrentDelayRange() {
if (isNightTime()) {
var nMin = Number(GM_getValue(STORAGE_KEY_NIGHT_DELAY_MIN, 60));
var nMax = Number(GM_getValue(STORAGE_KEY_NIGHT_DELAY_MAX, 120));
nMin = isNaN(nMin) || nMin < 1 ? 60 : nMin;
nMax = isNaN(nMax) || nMax < nMin ? fallbackMax(nMin, 120) : nMax;
return { min: nMin, max: nMax };
}
return { min: getDelayMin(), max: getDelayMax() };
}
function canSendAtNight() {
if (!isNightTime()) return true;
var maxCount = Number(GM_getValue(STORAGE_KEY_NIGHT_MAX_COUNT, 3));
maxCount = isNaN(maxCount) || maxCount < 1 ? 3 : maxCount;
var periodKey = getNightPeriodKey();
var savedPeriod = GM_getValue(STORAGE_KEY_NIGHT_DATE, '');
var sentCount = Number(GM_getValue(STORAGE_KEY_NIGHT_SENT, 0));
if (savedPeriod !== periodKey) {
GM_setValue(STORAGE_KEY_NIGHT_DATE, periodKey);
GM_setValue(STORAGE_KEY_NIGHT_SENT, 0);
GM_setValue(STORAGE_KEY_NIGHT_WARNED, '');
return true;
}
return sentCount < maxCount;
}
function recordNightSent() {
if (!isNightTime()) return;
GM_setValue(STORAGE_KEY_NIGHT_DATE, getNightPeriodKey());
var sentCount = Number(GM_getValue(STORAGE_KEY_NIGHT_SENT, 0));
GM_setValue(STORAGE_KEY_NIGHT_SENT, isNaN(sentCount) ? 1 : sentCount + 1);
}
function getCurrentOrigin() {
return window.location.origin || 'https://skyeysnow.com';
}
// ================= 主控标签页选举 =================
// 设计:
// - GM 存储中保存 leaderId + 心跳时间戳
// - 每个标签页在 sessionStorage 中有唯一 TAB_ID
// - doReply / 主入口先尝试成为主控;抢不到则静默等待
// - 主控启动 2 秒心跳;超过 LEADER_TIMEOUT_MS 未更新则其他页可抢占
// - 刷新页面 TAB_ID 不变,主控身份自然保留
// - 关闭页面不主动释放(等待心跳超时自动失效),避免刷新瞬间被其他页抢占
function _isLeader() {
return GM_getValue(STORAGE_KEY_LEADER_ID, '') === TAB_ID;
}
function _tryBecomeLeader() {
var leaderId = GM_getValue(STORAGE_KEY_LEADER_ID, '');
var hb = Number(GM_getValue(STORAGE_KEY_LEADER_HB, 0));
var now = Date.now();
// 自己已是主控 → 刷新心跳
if (leaderId === TAB_ID) {
GM_setValue(STORAGE_KEY_LEADER_HB, now);
return true;
}
// 无主控 或 主控心跳超时 → 抢占
if (!leaderId || (now - hb) > LEADER_TIMEOUT_MS) {
GM_setValue(STORAGE_KEY_LEADER_ID, TAB_ID);
GM_setValue(STORAGE_KEY_LEADER_HB, now);
// 写入后回读校验:GM 存储非原子,两个标签页可能同时判定超时后都写入,
// 回读可确保最终只有后写入的 TAB_ID 存活,避免双方都认为抢到主控。
if (GM_getValue(STORAGE_KEY_LEADER_ID, '') !== TAB_ID) {
console.log('[AutoReply][多标签] 抢主控失败,已被其他标签页抢先');
return false;
}
console.log('[AutoReply][多标签] 本页接管主控身份(原主控: ' + (leaderId || '无') + ')');
return true;
}
return false;
}
function _startHeartbeat() {
if (_heartbeatTimer) return;
_heartbeatTimer = setInterval(function () {
if (_isLeader()) {
GM_setValue(STORAGE_KEY_LEADER_HB, Date.now());
}
}, HEARTBEAT_INTERVAL_MS);
}
function _stopHeartbeat() {
if (_heartbeatTimer) {
clearInterval(_heartbeatTimer);
_heartbeatTimer = null;
}
}
// ================= 日志 =================
function addLog(type, message) {
var timestamp = getTimestamp();
var icon = '';
switch (type) {
case 'auto': icon = '[自动发送] '; break;
case 'test': icon = '[测试发送] '; break;
case 'error': icon = '[错误] '; break;
case 'info': icon = '[信息] '; break;
default: icon = '[日志] '; break;
}
var logEntry = { time: timestamp, type: type, message: icon + message };
var logs = [];
try {
logs = JSON.parse(GM_getValue(STORAGE_KEY_LOG, '[]'));
} catch (e) {
logs = [];
}
logs.unshift(logEntry);
var maxLog = Number(GM_getValue(STORAGE_KEY_MAX_LOG, 100));
maxLog = isNaN(maxLog) || maxLog < 10 ? 100 : maxLog;
if (logs.length > maxLog) logs = logs.slice(0, maxLog);
try {
GM_setValue(STORAGE_KEY_LOG, JSON.stringify(logs));
} catch (e) {
console.warn('[AutoReply] 日志持久化失败:', e);
}
if (_logDisplayCallback) {
_logDisplayCallback(logs);
}
}
function getLogs() {
try {
return JSON.parse(GM_getValue(STORAGE_KEY_LOG, '[]'));
} catch (e) {
return [];
}
}
function clearLogs() {
GM_setValue(STORAGE_KEY_LOG, '[]');
if (_logDisplayCallback) _logDisplayCallback([]);
}
// ================= 倒计时 =================
function getRemainingTimeString() {
var targetTime = Number(GM_getValue(STORAGE_KEY_PENDING_TARGET, 0));
if (!Number.isFinite(targetTime) || targetTime <= 0) return '未启动';
if (targetTime <= Date.now()) return '即将触发...';
var remaining = targetTime - Date.now();
var m = Math.floor(remaining / 60000);
var s = Math.floor((remaining % 60000) / 1000);
return m + '分' + s.toString().padStart(2, '0') + '秒';
}
// ================= 设置面板:日志区域 =================
function injectSettingsExtras(settingsBox) {
var logSection = document.createElement('div');
logSection.style.cssText = 'margin-top:12px;border-top:1px solid #eee;padding-top:10px;';
logSection.innerHTML =
'
' +
'操作日志' +
'' +
'
' +
'';
settingsBox.appendChild(logSection);
var clearBtn = settingsBox.querySelector('#clear-log-btn');
if (clearBtn) {
clearBtn.onclick = function () {
if (confirm('确定要清空所有操作日志吗?')) {
clearLogs();
addLog('info', '日志已清空');
}
};
}
updateLogDisplay();
}
function updateLogDisplay() {
var logContainer = document.getElementById('auto-reply-log');
if (!logContainer) return;
var logs = getLogs();
if (logs.length === 0) {
logContainer.innerHTML = '
暂无日志
';
return;
}
var html = '';
for (var i = 0; i < logs.length; i++) {
var log = logs[i];
var color = '#555';
if (log.type === 'auto') color = '#28a745';
else if (log.type === 'test') color = '#e88e00';
else if (log.type === 'error') color = '#dc3545';
else if (log.type === 'info') color = '#007bff';
html += '