亲爱的朋友,在使用本脚本前,请知悉:大多数论坛将自动化发帖视为违规行为。使用本脚本可能导致账号被封禁、扣除积分等后果。本脚本仅供个人学习与技术研究使用,一切操作风险需由您自行承担。请理性使用,遵守论坛规则。
脚本装好后,我们需要告诉它一些基本信息。
访问天雪论坛(skyeysnow.com 或 skyey2.com)的任意页面,点击浏览器右上角的 脚本猫 图标,在弹出的面板里找到"当前页面脚本"中的 天雪论坛自动回复刷分用户脚本,点它下面的 "打开设置"。
脚本猫的面板只会列出当前页面能匹配到的脚本,所以要先把天雪论坛的页面打开再点图标;"打开设置"就排在脚本名下方。面板里没看到的话,点一下脚本名展开即可。
在弹出的面板中,你需要关注以下几个核心设置:
为了模拟真人,脚本分两种工作节奏:
填好后,点击面板底部的"保存"按钮即可。
设置完成后,你只需要保持浏览器打开着天雪论坛的页面,脚本就会在后台默默帮你回复了。为了确保长期挂机成功,请务必在浏览器中禁用浏览器节能模式,并关闭“自动将标签页置于睡眠状态”,并将www.skyey2.com和skyeysnow.com列入“使这些站点保持活动状态”的白名单(这些设置通常位于浏览器设置中的 系统和性能/性能 中)
如果你想确认它是否正常工作,可以:
不用担心脚本会疯狂刷屏,它内置了多重保护:
Q1:我开了电脑但脚本没回复,怎么回事?
A:脚本需要网页保持打开状态。如果你关闭了天雪论坛的网页,或者电脑进入了休眠/睡眠状态,或者在脚本猫里没有启用这个脚本,它就会停止工作。
Q2:我想换一批回复的诗句怎么办?
A:点击脚本猫图标 → 在本脚本下面点 "打开设置",在"回复诗句"框里删掉旧的,换上新的,记得一行一句,然后保存。全部删空的话,脚本会退回使用内置的 10 句古诗。
Q3:白天和夜间的间隔时间可以改吗?
A:完全可以!在设置面板里,你可以自由调整白天和夜间的"延迟下限"和"延迟上限"(单位是分钟)。
Q4:我想让脚本先停一会怎么办?
A:把设置面板里的"水楼 tid"清空后保存,脚本就不会再自动发帖了;想恢复时把 tid 填回去再保存即可。更干脆的做法是:直接关掉水楼帖的标签页,或者在脚本猫弹窗里把这个脚本的开关关掉。
Q5:日志在哪里看、怎么清空?
A:都在"打开设置"的面板里。面板底部是操作日志(最新的在最上面),右上角有一个"清空日志"按钮,点它会先让你确认一次。
| 遇到的问题 | 尝试的解决方法 |
|---|---|
| 点击图标没反应 | 刷新当前网页,或检查脚本猫是否被禁用 |
| 装好了却完全没反应 | Chrome / Edge 新版要在扩展详情里打开「允许用户脚本」,然后刷新页面 |
| 提示找不到 tid | 检查设置里的 tid 是否填写正确,且帖子确实存在 |
| 日志显示"跳过" | 这是正常的防刷机制,说明还没到发帖时间或间隔不够 |
| 设置了夜间模式但白天也发 | 检查电脑的系统时间是否准确,以及夜间开始/结束时间是否设置正确 |
| 页面偶尔自己整页刷新 | 这是正常行为:发帖前脚本要刷新一次拿最新楼层 |
| 日志里出现"未检测到用户名" | 在设置面板里手动填写"我的用户名",保存后刷新页面 |
最后的小建议:建议把夜间最大发送数保持在 5 条以内,白天间隔设置在 25 分钟以上,这样最接近真人的发帖习惯,也更安全哦!
以下为专业内容:
这一节不聊代码,只讲你打开水楼帖之后,在浏览器里实际能看到什么。
打开页面后的自动动作:你只要打开天雪论坛的页面,脚本就已经开始工作了。如果还没配置过 tid,它会弹一个输入框让你填;如果配置好了却不在水楼帖页面上,而你又开着"自动跳转到水楼帖",浏览器会自己跳到水楼帖去(你会看到地址栏的 tid 变了)。
等待期间:进入水楼帖后,脚本开始"等号"。页面上看不出什么动静,你可以点开设置面板,顶部那一行会实时显示"距离下次发帖:X分XX秒"。
到点前的一步:整页刷新:真正动手之前,脚本会先把页面整页刷新一次——你会看到浏览器重新加载这个帖子。这一步是为了拿到最新的楼层列表,让防刷判断基于最新数据。
到点那一刻:
发完之后:脚本往操作日志里写一条记录(面板里能看到),然后过了 3 秒重新开始下一轮等待,按你设定的白天/夜间延迟挑下一个时间点。如果点提交时出了错,它同样会记一条错误日志,并且 3 秒后把下一轮接上,不会就此躺平。
这一轮的完整顺序可以记成一句话:
等待 → 必要时先自动整页刷新拿到最新楼层 → 从诗句列表随机挑一句 → 写进页面底部的快捷回复框 → 处理"跳转到最后一页"控件 → 点击提交按钮 → 发帖成功后写日志 → 3 秒后重新开始下一轮等待。
需要提醒的是:脚本不会替你打开浏览器、也不会替你把网页开着。它只在你已经打开天雪论坛页面时工作——页面关掉、电脑睡眠,它也就停了。
本脚本是一个 Discuz! 论坛水楼自动回复工具,核心行为链为:
读取配置 → 计算延迟 → 等待 → 自动发帖 → 记录日志 → 重启定时器
| 特性 | 说明 |
|---|---|
| 白天/夜间双模式 | 不同时段使用不同的延迟范围和发送配额 |
| 夜间限额 | 每夜最多发送 N 条,跨零点自动重置计数 |
| 防连续刷楼 | 检测自己发帖后需间隔若干条其他用户帖子才发 |
| 多标签互斥(主控选举) | 多标签页同时挂机时,仅一个标签页负责发帖,其余静默 |
| 心跳保活 | 主控每 2 秒写入心跳时间戳,6 秒未更新则其他页可抢占 |
| 设置面板 | 菜单命令 GM_registerMenuCommand 呼出 HTML 弹窗(脚本猫里显示在脚本名下方) |
| 操作日志 | GM 存储持久化,支持清空、颜色区分类型 |
| XSS 防护 | 所有用户输入经过 escapeHtml 转义 |
| 存储异常自愈 | 读写统一收口,读取失败退化为默认值,写入失败只告警 |
| 定时触发异常自愈 | 定时触发异常时复位状态并自动重新排程,调度器不会静默停摆 |
@run-at document-idle → 页面 DOM 解析完成后执行 IIFE
主入口逻辑(文件末尾):
所有 GM 存储键在脚本顶部集中声明,共 21 个 GM 键 + 4 个 sessionStorage 键。所有读写都经 §3.1 的安全存储层,脚本正文不直接出现 GM_getValue / GM_setValue / sessionStorage。
| 键名 | 用途 | 默认值 | 数据类型 |
|---|---|---|---|
| skyeysnow_water_tid | 水楼帖子 ID | '' | 字符串(纯数字) |
| skyeysnow_water_set_ym | 月份检测标记 | '' | 字符串(YYYY-MM) |
| skyeysnow_reply_delay_min | 白天延迟下限(分钟) | 25 | 数字 |
| skyeysnow_reply_delay_max | 白天延迟上限(分钟) | 35 | 数字 |
| skyeysnow_auto_jump | 自动跳转开关 | true | 布尔 |
| skyeysnow_poems | 回复诗句(换行分隔) | 内置 10 句古诗 | 字符串 |
| skyeysnow_night_start | 夜间开始小时 | 23 | 数字(0-23) |
| skyeysnow_night_end | 夜间结束小时 | 8 | 数字(0-23) |
| skyeysnow_night_delay_min | 夜间延迟下限(分钟) | 60 | 数字 |
| skyeysnow_night_delay_max | 夜间延迟上限(分钟) | 120 | 数字 |
| skyeysnow_night_max_count | 夜间最多发送条数 | 3 | 数字 |
| skyeysnow_night_sent_count | 夜间已发送计数 | 0 | 数字 |
| skyeysnow_night_date | 夜间时段日期标识 | '' | 字符串(YYYY-MM-DD) |
| skyeysnow_night_warned | 夜间限额警告标记 | '' | 字符串 |
| skyeysnow_pending_target_time | 下次发帖目标时间戳 | 0 | 数字(timestamp) |
| skyeysnow_operation_log | 操作日志(JSON 数组) | [] | 字符串(JSON) |
| skyeysnow_max_log | 最大日志条数 | 100 | 数字 |
| skyeysnow_my_username | 用户自定义用户名 | '' | 字符串 |
| skyeysnow_post_gap | 防刷间隔所需其他帖子数 | 1 | 数字 |
| skyeysnow_leader_id | 主控标签页 ID | '' | 字符串(TAB_ID) |
| skyeysnow_leader_heartbeat | 主控心跳时间戳 | 0 | 数字(timestamp) |
日志上限另有下限保护:skyeysnow_max_log 小于 10 时回退为默认上限 100(MIN_MAX_LOG = 10,DEFAULT_MAX_LOG = 100)。
| 键名 | 用途 | 生命周期 |
|---|---|---|
| skyeysnow_need_refresh | 标记需刷新页面以获取最新 DOM | 页面刷新即清除 |
| skyeysnow_no_username_alerted | 标记已弹出过「未检测到用户名」警告 | 页面刷新即清除 |
| skyeysnow_skip_tid_prompt | 标记跳过 tid 提示 | 页面刷新即清除 |
| skyeysnow_tab_id | 本标签页唯一标识 | 关闭标签页即清除 |
设计意图:sessionStorage 用于「一次性」标志,避免跨标签页污染,同时保证刷新页面后状态可恢复。
所有 GM 与 sessionStorage 的读写统一收口在这一层,共有 5 个函数:
| 函数 | 行为 |
|---|---|
| gmGet(key, defValue) | 读 GM 存储;抛异常时 console.warn 并返回 defValue |
| gmSet(key, value) | 写 GM 存储;成功返回 true,抛异常时告警并返回 false |
| sessionGet(key) | 读 sessionStorage;异常返回 null |
| sessionSet(key, value) | 写 sessionStorage;异常静默降级为空操作 |
| sessionRemove(key) | 删 sessionStorage;异常静默降级为空操作 |
设计意图:存储不可用(隐私模式、企业策略)时,读取退化为默认值、写入只告警,任何存储异常都不能中断发帖与调度主流程。GM_getValue(key, defValue) 与 GM_setValue(key, value) 在脚本里各只有一处调用,都在本节;其余全部代码只认 gmGet / gmSet。
sessionStorage 则除本节三个函数外,只有 TAB_ID 初始化会直接读它(原因见下),其余全部走 sessionGet / sessionSet / sessionRemove。
唯一的例外是 TAB_ID 初始化:它在安全层之前就要用 sessionStorage 取本标签页 ID,因此自带一行 try/catch,失败时降级为内存级 ID。
XSS 防护核心函数。对 & < > " ' 五个字符做 HTML 实体编码。
调用点:
从 window.location.search 中解析 tid 参数。
返回 YYYY-MM 格式字符串,用于月份检测。
返回 YYYY-MM-DD HH:MM:SS 格式的日志时间戳(含日期,便于跨天区分)。
统一的上限非法回退规则:Math.max(min + 10, defMax)。避免各处回退逻辑不一致。
返回 window.location.origin,兜底 'https://skyeysnow.com'。
「Number(存储值) + 合法性校验 + 回退」的唯一入口,签名如下:
function readNumber(key, defValue, valid, makeFallback)
所有数值型配置(白天/夜间延迟上下限、夜间最多条数、防刷间隔、日志上限)都经此函数读取,默认值与校验规则各只写一份。
读取白天延迟配置。下限要求 >= 1;上限要求 >= 下限,否则回退为 fallbackMax(下限, 35),即不低于「下限 + 10」且不低于默认上限 35。
读取夜间开始/结束小时,非法时回退为默认的 23 / 8。
读取夜间延迟配置,规则与白天一致:下限 >= 1,上限 >= 下限,否则回退 fallbackMax(下限, 120)。
读取夜间最多发送条数,要求 >= 1,否则回退为 3。
读取防刷间隔所需其他帖子数,要求 >= 0,否则回退为 1。
读取自动跳转开关,统一经 gmGet 读取,并兼容不同脚本管理器返回布尔值 / 字符串的差异:
var v = gmGet(STORAGE_KEY_AUTO_JUMP, true);
return v !== false && v !== 'false';
读取诗句配置,按换行分割过滤空行。为空时返回内置 10 句古诗。
夜间时段的唯一计算入口,一次返回:
{ start, end, hour, isNight, periodDate }
判断规则:
start <= end 普通区间:hour >= start && hour < end
start > end 跨零点区间:hour >= start || hour < end
跨零点且 hour < end 时,periodDate 减 1 天。
直接返回 getNightWindow().isNight。
取 getNightWindow().periodDate 格式化为 YYYY-MM-DD,作为当前夜间时段的唯一标识。
根据 isNightTime() 返回当前时段的延迟范围对象 {min, max}。
夜间发送权限检查:
夜间发送计数 +1,同时更新 NIGHT_DATE 为当前 periodKey。
多个标签页同时运行脚本时,仅一个标签页负责发帖,避免重复发帖和 GM 存储竞争。
| 变量/常量 | 说明 |
|---|---|
| TAB_ID | 本标签页唯一 ID(sessionStorage 持久化,降级为内存级) |
| LEADER_TIMEOUT_MS | 心跳超时阈值 = 6000ms |
| HEARTBEAT_INTERVAL_MS | 心跳间隔 = 2000ms |
| STORAGE_KEY_LEADER_ID | GM 存储中的主控 ID |
| STORAGE_KEY_LEADER_HB | GM 存储中的心跳时间戳 |
| _heartbeatTimer | 心跳 interval 定时器 ID |
读取 GM 中的主控 ID,判断是否等于本页 TAB_ID。
读取 GM 中的 leaderId 和 heartbeat(heartbeat 非数字时按 0 处理——否则「是否已超时」的判断恒为否,本页永远不会接管主控)
若 leaderId === TAB_ID → 刷新心跳,返回 true(自己已是主控)
若 leaderId 为空 或 (now - hb > LEADER_TIMEOUT_MS) → 抢占:
a. 写入 leaderId = TAB_ID
b. 写入 heartbeat = now
c. 回读 leaderId 校验(防止双写竞态)
d. 回读不一致 → 返回 false(被别人抢先了)
e. 回读一致 → 返回 true(抢占成功)
竞态分析:两个标签页同时检测到主控超时后同时写入,后写入的会覆盖先写入的。回读校验确保只有最后写入的那个通过,另一个被降级。极端情况下两个都通过的概率极低(纳秒级窗口),最坏后果仅为多发一条帖子。
| 事件 | 主控行为 | 非主控行为 |
|---|---|---|
| 页面加载 | 尝试抢占主控 → 成功则启动心跳 | 抢占失败 → 静默等待 |
| 定时器触发 | 执行发帖逻辑 | 检查主控身份 → 非主控则跳过 |
| 页面卸载 | 不清理主控身份(等心跳超时自动失效) | 清理定时器 |
| 主控标签页关闭 | 心跳 6 秒后超时 | 检测到超时 → 抢占主控 |
设计取舍:不主动释放主控身份是为了避免刷新页面瞬间被其他页抢占导致状态不一致。代价是关闭标签页后有 6 秒的「僵尸主控」窗口。
{
"time": "2026-10-10 22:30:45",
"type": "auto|test|error|info",
"message": "[自动发送] 自动回复已触发(诗句:xxx)"
}
日志渲染回调的调用包在 try/catch 中:渲染失败只告警,不能反向打断发帖 / 调度主流程。
JSON.parse 失败返回空数组;解析结果不是数组(存储被改写)也返回空数组,否则 addLog 的 unshift 会抛错并顺着调用链中断本次发帖。
渲染日志到设置面板的 #auto-reply-log 区域,按类型着色:
渲染时跳过损坏条目(非对象),避免渲染抛错反向打断调用方。
_logDisplayCallback 是模块内私有函数指针,在脚本初始化时指向 updateLogDisplay。addLog 写入存储后自动调用回调刷新 UI,无需轮询。
getRemainingTimeString() 计算 PENDING_TARGET 与当前时间的差值,格式化为 X分Y秒 或 即将触发… 或 未启动。设置面板中每秒更新显示。
这是整个脚本的核心控制流函数,所有分支路径如下:
doReply(immediate)
│
├─ [非immediate] 主控检查
│ ├─ 抢占主控成功 → 启动心跳
│ └─ 抢占失败 → 静默跳过,重启定时器,_autoReplyPending = false,return
│
├─ [非immediate] 夜间限额检查
│ ├─ 已达上限 → 记录日志(每个夜间时段只记一次),重启定时器,_autoReplyPending = false,return
│ └─ 未达上限 → 继续
│
├─ [非immediate][gap>0] 用户名检查
│ ├─ 用户名缺失 → 暂停自动回复,弹窗警告(一次性),_autoReplyPending = false,return
│ └─ 有用户名 → 继续
│
├─ [非immediate][gap>0] 防刷前强制刷新
│ ├─ 有 SESSION_KEY_NEED_REFRESH 标记 → 清除标记,继续(说明刚刷新完)
│ └─ 无标记 → 写入 SESSION_KEY_NEED_REFRESH,location.reload(),return(页面刷新后从else分支进入)
│
├─ [非immediate] 防刷检查 canPostNow()
│ ├─ 不通过 → 记录日志,重启定时器,_autoReplyPending = false,return
│ └─ 通过 → 继续
│
├─ 内容准备
│ ├─ 诗句为空 → 记录错误,重启定时器,_autoReplyPending = false,return
│ ├─ DOM 元素缺失 → 记录错误,重启定时器,_autoReplyPending = false,return
│ └─ 正常 → 继续
│
├─ 执行发帖
│ ├─ 随机选一句诗 → 写入 #fastpostmessage
│ ├─ 调用 ensureJumpToLastPage()
│ ├─ submitEl.click() 触发提交
│ ├─ [非immediate] 注册 8 秒兜底刷新定时器(_postReloadTimeout)
│ ├─ 记录日志(auto/test)
│ ├─ [非immediate] recordNightSent()
│ ├─ [非immediate] 3 秒后重启定时器
│ ├─ _autoReplyPending = false
│ ├─ 清除 PENDING_TARGET
│ └─ 移除 visibilitychange 监听
│
└─ catch (e)
├─ 记录错误日志
├─ [非immediate] 3 秒后重启定时器
├─ _autoReplyPending = false
└─ 清除 PENDING_TARGET
提交后注册的那个 8 秒定时器(_postReloadTimeout)在到达 8000ms 之前一定会被清掉:
_postReloadTimeout = setTimeout(..., 8000));_scheduleRestart('post', ...),3 秒后执行 startAutoReply();startAutoReply() 第一步是 stopAutoReply() → cleanupTimers(true),而 cleanupTimers() 明确包含 clearTimeout(_postReloadTimeout)。所以这个 8 秒扫描既不会因为 readyState 判断而失效,也根本轮不到触发——它不存在生效路径。提交后页面的刷新始终由 Discuz 论坛自身的跳转完成,脚本不提供手动兜底刷新。
该标志防止同一时刻多个发帖请求并发执行。
doReply 内部,所有退出路径(包括异常路径)都会把它复位为 false;doReply 之外的定时触发路径同样需要异常边界:若定时回调里抛出未预料的异常,_autoReplyPending 会永久卡在 true,调度器从此彻底静默。_onTimerTick()(定时触发的统一入口,见 §3.8)与 _triggerReplyOnStartup()(启动期立即触发的统一入口,见 §3.8)共同提供:异常时记录「定时触发异常」/「启动时触发回复异常」日志、复位 pending、并在 3 秒后自动重新排程。楼层号 parseInt 失败(NaN)的容器会被过滤掉,否则会污染后续楼层排序。
防刷检查依赖 DOM 数据,而 DOM 可能因其他标签页发帖而过期。因此:
调用 cleanupTimers(true) — 保留 PENDING_TARGET(不清零),因为:
统一清理 4 个定时器 + 1 个事件监听 + 心跳:
| 清理对象 | 类型 |
|---|---|
| _autoReplyCheckInterval | setInterval(每秒轮询) |
| _postReloadTimeout | setTimeout(提交后 8 秒兜底) |
| _restartTimers.post | setTimeout(提交成功后 3 秒重启) |
| _restartTimers.exception | setTimeout(异常后 3 秒重启) |
| _autoReplyVisibilityHandler | visibilitychange 监听 |
| _heartbeatTimer | 主控心跳 interval |
「延迟重启」的统一实现,slotKey 取 'post' 或 'exception',分别对应 _restartTimers 的两个槽位:
提交成功路径用 'post' 槽位、日志为「自动回复定时器已重新启动」;提交异常与定时触发异常用 'exception' 槽位、日志为「异常后自动回复定时器已重新启动」。
「本轮跳过,把调度器交还下一轮」的统一实现,供主控抢占失败、夜间限额、防刷不通过、诗句为空、DOM 元素缺失等跳过分支共用:
定时到点的统一入口,1 秒轮询与 visibilitychange 恢复都经此进入:
启动期立即触发一轮回复的统一入口(「检测到需要刷新」与「持久化定时器已过期」两条路径共用),与 _onTimerTick() 的异常兜底同构:
不兜住的话,异常会冲出脚本顶层,其后的「脚本初始化完成」日志与页面卸载清理都不会执行。
beforeunload 事件调用 cleanupTimers(true) — 清理定时器但保留 PENDING_TARGET,保证刷新后能恢复倒计时,同时复位 _settingsOpen。
+--------------------------------------------------+
| 天雪水楼脚本设置 |
| 距离下次发帖:XX分XX秒 [倒计时] |
| ------------------------------------------------ |
| 水楼 tid |
| [________] |
| [白天延迟下限] [白天延迟上限] |
| [x] 自动跳转到水楼帖 |
| ------------------------------------------------ |
| 夜间模式 |
| [开始小时] [结束小时] |
| [夜间延迟下限] [夜间延迟上限] |
| 夜间最多发送条数 |
| [____] |
| ------------------------------------------------ |
| 防连续刷楼 |
| 我的用户名 |
| [________] |
| 间隔所需其他帖子数 |
| [____] |
| [提示文本] |
| 回复诗句(每行一句) |
| [________] (textarea) |
| ------------------------------------------------ |
| [保存] [立即发一条(测试)] [重置月份] [关闭] |
| ------------------------------------------------ |
| 操作日志 [清空日志] |
| [日志内容区域] |
+--------------------------------------------------+
关键性质:写入配置与「是否跳转」的决定彼此独立——不管用户点确定还是取消,配置都已经保存下来了,取消只会让本次不跳转。「立即跳转」的询问在写入之前发出,两者没有先后依赖关系。
清空 STORAGE_KEY_DATE(月份检测标记)、停止定时器、清除 SESSION_KEY_SKIP_TID_PROMPT 标记 → 刷新页面后会重新提示输入水楼 tid。
位于设置面板的日志区域,点击后先 confirm,确认则清空日志并写入一条「日志已清空」。
使用 for 循环(非递归)最多提示 10 次:
SESSION_KEY_SKIP_TID_PROMPT 标记,return/^\d+$/)→ 弹窗警告,继续循环SESSION_KEY_SKIP_TID_PROMPT → 跳转至该帖10 次仍未输入合法值 → 写入 SESSION_KEY_SKIP_TID_PROMPT,并弹出与脚本内一致的提示:多次输入无效,已跳过本次 tid 设置。可稍后从脚本猫弹窗里点「打开设置」重新配置。
页面加载
│
├─ 未配置 tid / 月份变更 → promptForTid() → 用户输入 → 跳转至水楼帖
│
├─ 需自动跳转 → location.href 跳转
│
└─ 已在水楼帖
│
├─ 尝试成为主控 → 成功:启动心跳
│
├─ 刚完成防刷前强制刷新 → 直接触发本轮
│ 否则 → 恢复持久化定时器 / 启动新定时器
│
├─ 写入「脚本初始化完成,自动回复已启动」日志
│
└─ 等待定时器触发 → doReply() → 发帖 → 3秒后重启
标签页A(主控) 标签页B(备胎)
│ │
每2秒写心跳 每25~35分钟检查
│ │
│ 检测到主控超时?
│ │
├─ 执行发帖 ──────────► 是:抢占主控
│ │
│ ├─ 抢占成功 → 启动心跳
│ └─ 抢占失败 → 静默等待
│
关闭标签页
│
心跳停止
│
6秒后超时
│
───────────────────────────► 标签页B检测到超时 → 抢占主控
每日 23:00 进入夜间模式
│
├─ 夜间延迟范围生效(60~120分钟)
├─ 每夜限额计数开始(默认3条)
├─ 每发一条 → 计数+1
└─ 达到上限 → 跳过发送 + 每个夜间时段记录一次日志
│
▼
次日 08:00 进入白天模式
│
├─ 限额计数重置
├─ 白天延迟范围生效(25~35分钟)
└─ 恢复正常发送
时段归属:跨零点的夜间窗口(如 23:00~08:00)统一由 getNightWindow().periodDate 归属到「开始那天」,因此次日 0~8 点发的帖仍计入前一晚的配额。
不同脚本管理器对 GM 存储返回布尔值的处理不一致(有的返回 boolean,有的返回字符串 "true"/"false")。脚本统一使用以下模式(经 gmGet 读取):
function shouldAutoJump() {
var v = gmGet(STORAGE_KEY_AUTO_JUMP, true);
return v !== false && v !== 'false';
}
try { logs = JSON.parse(gmGet(STORAGE_KEY_LOG, '[]')); }
catch (e) { logs = []; }
解析失败或结果不是数组时按空日志处理,防止 GM 存储损坏导致脚本静默失败。
脚本猫对 GM 存储的实现与另一类旧式脚本管理器有两处不同(来源:脚本猫官方 API 文档 docs.scriptcat.org/docs/dev/api/),本脚本都已核对且不受影响:
GM_setValue(key, undefined) 在脚本猫里会删除该键,另一类管理器则会把它原样存成 undefined。本脚本所有写入都是数字、字符串或 JSON.stringify() 的结果,没有一处传 undefined。window.close() 可能丢掉这次写入,官方建议 await GM.setValue。本脚本从不主动关闭标签页,也没有依赖"写完立刻关"的时序。| 注入点 | 防护方式 |
|---|---|
| 日志渲染 | escapeHtml() 转义时间戳和消息 |
| 设置面板 value 注入 | escapeHtml() 转义 tid/用户名/诗句 |
| textarea 内容注入 | escapeHtml() 转义诗句 |
设置面板保存时对所有用户输入做类型和范围校验:
任一校验不通过即 alert 并终止本次保存,不写入任何配置。
| 编号 | 问题 | 影响 | 建议方案 |
|---|---|---|---|
| 1 | 非主控页定时器持续运行 | 轻微资源浪费 | 非主控页仅在主控超时后启动定时器 |
| 2 | _tryBecomeLeader 理论竞态窗口 | 极低概率双主控 | 可接受,论坛挂机场景不影响稳定性 |
| 3 | ensureJumpToLastPage 依赖 #fastpostrefresh | 某些主题找不到控件 | 降级为静默跳过(已有) |
| 4 | 主控关闭后 6 秒接管延迟 | 短时间内重复发帖风险 | 可接受 |
| 5 | 用户名自动读取可能暴露身份 | 用户隐私 | 可改为默认空值 + 提示手动填写 |
| 6 | 作者解析失败(匿名/已注销楼层 author 为 null)被计为「别人的帖子」 | 极端情况下放宽防刷判定 | 可对 author 为 null 的楼层单独计数 |
| 7 | canPostNow() 只统计当前页 DOM 的楼层 | 跨页数据不参与判定 | 依赖发帖前的强制刷新缓解 |
搜索 [AutoReply] 可过滤所有脚本日志。多标签相关日志含 [多标签] 子标签。
临时将夜间开始时间设为当前小时,触发夜间逻辑验证,调试完毕后再改回。
打开 2 个标签页同时挂机,观察控制台:
在控制台里,脚本内部的所有读写都走 gmGet / gmSet 这一层(读取失败退化为默认值),对应到脚本猫的 GM 存储就是下面这些键:
skyeysnow_leader_id // 当前主控 ID
skyeysnow_leader_heartbeat // 主控心跳时间戳
skyeysnow_pending_target_time // 下次发帖时间戳
skyeysnow_night_sent_count // 夜间已发送计数
skyeysnow_operation_log // 操作日志(JSON 字符串)
如果长时间没有任何日志,先看控制台是否有 [AutoReply] 定时触发异常。正常情况下异常后 3 秒会自动重新排程,并写入一条「异常后自动回复定时器已重新启动」。
基本逻辑40行,防御性编程1000行