[油猴脚本]通过关键词匹配,通知你所关心的L站话题

幻雪 2026-08-04 20:41 1

该脚本全程vibe,通过关键词,标签,筛选你关心的话题,命中后浏览器直接弹通知

弹出通知后点击可以跳转到话题




// ==UserScript==
// @name LINUX DO 话题订阅推送
// @namespace https://linux.do/
// @version 1.0.0
// @description 按关键词 / 分类 / 标签订阅 LINUX DO 话题,命中后浏览器通知,点击直达话题。
// @match https://linux.do/*
// @run-at document-idle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @grant GM_notification
// @grant GM_openInTab
// @noframes
// ==/UserScript==

(() => {
"use strict";

/* ---------------- 存储键与默认值 ---------------- */

const K = Object.freeze({
enabled: "ld-push-enabled",
subscribeAll: "ld-push-subscribe-all",
onlyNew: "ld-push-only-new",
matchMode: "ld-push-match-mode",
keywords: "ld-push-keywords",
exclude: "ld-push-exclude",
categories: "ld-push-categories",
tags: "ld-push-tags",
interval: "ld-push-interval",
catchup: "ld-push-catchup",
seen: "ld-push-seen-ids",
lastCheck: "ld-push-last-check",
total: "ld-push-total",
catCache: "ld-push-cat-cache",
});

const DEFAULTS = Object.freeze({
enabled: true,
subscribeAll: false,
onlyNew: true,
matchMode: "any",
interval: 90, // 秒
catchup: 30, // 分钟(离线后最多补推多久内的话题)
seenCap: 1000,
burstCap: 5, // 单次检查最多逐条推送数,超出合并为一条摘要
});

const TOPIC_URL = (topic) =>
`https://linux.do/t/${
topic.slug && /^[\w-]+$/.test(topic.slug) ? topic.slug : "topic"
}/${topic.id}`;

/* ---------------- 样式 ---------------- */

GM_addStyle(`
#ld-push-root {
--ldp-accent: #3b7ddd;
--ldp-bg: #fbfcfd;
--ldp-panel: #f0f3f5;
--ldp-text: #26343b;
--ldp-muted: #66757d;
--ldp-border: rgb(42 59 64 / 14%);
position: fixed; z-index: 1300;
right: max(20px, env(safe-area-inset-right));
bottom: max(84px, calc(env(safe-area-inset-bottom) + 64px));
font-family: "Source Han Sans SC","Noto Sans CJK SC","Microsoft YaHei UI",sans-serif;
color: var(--ldp-text); font-size: 13px;
}
@media (prefers-color-scheme: dark) {
#ld-push-root {
--ldp-bg: #182229; --ldp-panel: #223038;
--ldp-text: #eef3f5; --ldp-muted: #a7b6bd;
--ldp-border: rgb(220 233 237 / 16%);
}
}
#ld-push-root .ldp__trigger {
width: 50px; height: 50px; border-radius: 15px; cursor: pointer;
border: 1px solid var(--ldp-border); background: var(--ldp-bg);
color: var(--ldp-accent); font-size: 21px;
display: grid; place-items: center; position: relative;
box-shadow: 0 14px 38px rgb(15 25 30 / 16%);
transition: transform .15s ease, box-shadow .15s ease;
}
#ld-push-root .ldp__trigger:hover { transform: translateY(-2px); }
#ld-push-root .ldp__dot {
position: absolute; top: 7px; right: 7px; width: 9px; height: 9px;
border-radius: 50%; background: #36b37e;
box-shadow: 0 0 0 2px var(--ldp-bg);
}
#ld-push-root .ldp__panel {
position: absolute; right: 0; bottom: 60px;
width: min(360px, calc(100vw - 24px));
max-height: min(640px, calc(100vh - 40px));
overflow-y: auto; overscroll-behavior: contain;
border: 1px solid var(--ldp-border); border-radius: 20px;
background: var(--ldp-bg); padding: 12px;
box-shadow: 0 24px 64px rgb(15 25 30 / 20%);
display: grid; gap: 10px;
}
#ld-push-root .ldp__panel[hidden] { display: none; }
#ld-push-root header {
display: flex; justify-content: space-between; align-items: center;
padding-bottom: 8px; border-bottom: 1px solid var(--ldp-border);
}
#ld-push-root header strong { font-size: 15px; display: block; }
#ld-push-root header span { color: var(--ldp-muted); font-size: 11px; }
#ld-push-root .ldp__close {
width: 30px; height: 30px; border: 0; border-radius: 10px; cursor: pointer;
background: transparent; color: var(--ldp-muted); font-size: 20px;
}
#ld-push-root .ldp__close:hover { background: var(--ldp-panel); color: var(--ldp-text); }
#ld-push-root .ldp__section {
display: grid; gap: 8px; padding: 10px;
border: 1px solid var(--ldp-border); border-radius: 14px;
background: color-mix(in srgb, var(--ldp-panel) 55%, transparent);
}
#ld-push-root .ldp__title { font-weight: 600; font-size: 12px; color: var(--ldp-muted); }
#ld-push-root .ldp__row {
display: flex; justify-content: space-between; align-items: center; gap: 12px;
}
#ld-push-root .ldp__row small { display: block; color: var(--ldp-muted); font-size: 11px; margin-top: 2px; }
#ld-push-root input[type="checkbox"] { width: 17px; height: 17px; accent-color: var(--ldp-accent); flex: 0 0 auto; }
#ld-push-root .ldp__field { display: grid; gap: 5px; }
#ld-push-root .ldp__field > span { font-size: 12px; color: var(--ldp-muted); display: flex; justify-content: space-between; align-items: center; }
#ld-push-root textarea, #ld-push-root input[type="text"],
#ld-push-root input[type="number"], #ld-push-root select {
width: 100%; box-sizing: border-box; padding: 7px 9px;
border: 1px solid var(--ldp-border); border-radius: 10px;
background: var(--ldp-bg); color: var(--ldp-text); font: inherit; resize: vertical;
}
#ld-push-root input[type="range"] { width: 100%; accent-color: var(--ldp-accent); }
#ld-push-root .ldp__cats {
display: grid; grid-template-columns: 1fr 1fr; gap: 4px 8px;
max-height: 150px; overflow-y: auto; padding: 6px;
border: 1px solid var(--ldp-border); border-radius: 10px; background: var(--ldp-bg);
}
#ld-push-root .ldp__cats label { display: flex; gap: 6px; align-items: center; font-size: 12px; cursor: pointer; }
#ld-push-root .ldp__cats input { width: 14px; height: 14px; }
#ld-push-root .ldp__mini {
border: 1px solid var(--ldp-border); background: var(--ldp-panel); color: var(--ldp-text);
border-radius: 8px; padding: 2px 8px; font-size: 11px; cursor: pointer;
}
#ld-push-root .ldp__actions { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 7px; }
#ld-push-root .ldp__actions button {
min-height: 34px; border: 1px solid var(--ldp-border); border-radius: 11px;
background: var(--ldp-panel); color: var(--ldp-text); cursor: pointer; font: inherit;
}
#ld-push-root .ldp__actions button:first-child { background: var(--ldp-accent); border-color: transparent; color: #fff; }
#ld-push-root .ldp__actions button:hover { filter: brightness(1.06); }
#ld-push-root .ldp__status {
display: block; min-height: 16px; font-size: 11px; line-height: 1.5; color: var(--ldp-muted);
}
#ld-push-root .ldp__status[data-kind="error"] { color: #c0392b; }
#ld-push-root .ldp__status[data-kind="ok"] { color: #2e8b57; }
#ld-push-root .ldp__hint { margin: 0; font-size: 11px; color: var(--ldp-muted); }
`);

/* ---------------- 工具函数 ---------------- */

const bool = (v, d) => (typeof v === "boolean" ? v : d);
const clamp = (v, a, b) => Math.min(b, Math.max(a, v));
const parseList = (v) =>
String(v ?? "")
.split(/[\n,,、;;]+/)
.map((s) => s.trim().toLowerCase())
.filter(Boolean);
const idArray = (v) =>
(Array.isArray(v) ? v : [])
.map((n) => Number(n))
.filter((n) => Number.isInteger(n) && n > 0);
const escapeHtml = (s) =>
String(s).replace(/[&<>"']/g, (c) => ({
"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;",
}[c]));
const timeStr = (ts) => new Date(ts).toLocaleTimeString("zh-CN", { hour12: false });

function loadSettings() {
return {
enabled: bool(GM_getValue(K.enabled), DEFAULTS.enabled),
subscribeAll: bool(GM_getValue(K.subscribeAll), DEFAULTS.subscribeAll),
onlyNew: bool(GM_getValue(K.onlyNew), DEFAULTS.onlyNew),
matchMode: GM_getValue(K.matchMode) === "all" ? "all" : "any",
keywordsRaw: String(GM_getValue(K.keywords, "")),
keywords: parseList(GM_getValue(K.keywords, "")),
excludeRaw: String(GM_getValue(K.exclude, "")),
exclude: parseList(GM_getValue(K.exclude, "")),
categoryIds: idArray(GM_getValue(K.categories, [])),
tagsRaw: String(GM_getValue(K.tags, "")),
tags: parseList(GM_getValue(K.tags, "")),
intervalSec: clamp(Number(GM_getValue(K.interval, DEFAULTS.interval)) || DEFAULTS.interval, 30, 300),
catchupMin: clamp(Number(GM_getValue(K.catchup, DEFAULTS.catchup)) || DEFAULTS.catchup, 1, 240),
};
}

function getCatCache() {
const c = GM_getValue(K.catCache, null);
return c && Array.isArray(c.list) ? c : { ts: 0, list: [] };
}

async function fetchCategories() {
const res = await fetch("/categories.json?include_subcategories=true", {
credentials: "same-origin",
headers: { accept: "application/json" },
});
if (!res.ok) throw new Error(`分类获取失败:HTTP ${res.status}`);
const data = await res.json();
const list = [];
for (const c of data?.category_list?.categories ?? []) {
list.push({ id: c.id, name: String(c.name ?? c.id), parent: null });
for (const s of c.subcategory_list ?? []) {
list.push({ id: s.id, name: `${c.name} / ${s.name}`, parent: c.id });
}
}
const cache = { ts: Date.now(), list };
GM_setValue(K.catCache, cache);
return cache;
}

function expandCategoryIds(selected, cache) {
const set = new Set(selected);
for (const c of cache.list) {
if (c.parent && set.has(c.parent)) set.add(c.id); // 选中父分类 ⇒ 含子分类
}
return set;
}

function topicMatches(topic, s, catIdSet) {
const title = String(topic.title ?? "").toLowerCase();
if (s.exclude.some((k) => title.includes(k))) return false;
if (s.subscribeAll) return true;
const checks = [];
if (s.keywords.length) checks.push(s.keywords.some((k) => title.includes(k)));
if (s.categoryIds.length) checks.push(catIdSet.has(topic.category_id));
if (s.tags.length) {
const tags = Array.isArray(topic.tags)
? topic.tags.map((t) => String(t).toLowerCase())
: [];
checks.push(s.tags.some((t) => tags.includes(t)));
}
if (!checks.length) return false; // 未配置任何筛选 ⇒ 不推送,防安装即轰炸
return s.matchMode === "all" ? checks.every(Boolean) : checks.some(Boolean);
}

/* ---------------- 通知 ---------------- */

const hasGM = typeof GM_notification === "function";

function openTopic(url) {
try {
if (typeof GM_openInTab === "function") {
GM_openInTab(url, { active: true, insert: true });
return;
}
} catch { /* fall through */ }
window.open(url, "_blank", "noopener");
}

function ensureWebPermission() {
if (hasGM || !("Notification" in window)) return;
if (Notification.permission === "default") {
Notification.requestPermission().catch(() => {});
}
}

function pushNotify({ title, body, icon, url, tag }) {
if (hasGM) {
try {
GM_notification({
title, text: body, image: icon, tag, timeout: 10000,
onclick: () => openTopic(url),
});
return;
} catch { /* fall through to web notification */ }
}
if (!("Notification" in window) || Notification.permission !== "granted") return;
const n = new Notification(title, { body, icon, tag });
n.onclick = () => { window.focus(); openTopic(url); n.close(); };
}

function absolutize(path) {
if (!path) return null;
if (path.startsWith("http")) return path;
if (path.startsWith("//")) return "https:" + path;
return "https://linux.do" + path;
}

/* ---------------- 状态显示 ---------------- */

let statusEl = null;
function setStatus(msg, kind = "info") {
if (!statusEl) return;
statusEl.textContent = msg;
statusEl.dataset.kind = kind;
}

/* ---------------- 核心检查逻辑 ---------------- */

async function checkNow(manual = false) {
const s = loadSettings();
const startedAt = Date.now();

const res = await fetch("/latest.json", {
credentials: "same-origin",
headers: { accept: "application/json" },
});
if (res.status === 429 || res.status === 403) {
throw new Error(`被限流(HTTP ${res.status}),已自动降速`);
}
if (!res.ok) throw new Error(`请求失败:HTTP ${res.status}`);
const data = await res.json();

// 多标签页护栏:另一个页面刚检查过则跳过
const lastCheckBefore = Number(GM_getValue(K.lastCheck, 0)) || 0;
if (!manual && lastCheckBefore >= startedAt - 500) {
return { skipped: true };
}

const topics = data?.topic_list?.topics ?? [];
const users = new Map((data?.users ?? []).map((u) => [u.id, u]));

// 首次运行:静默播种,避免存量话题轰炸
if (!lastCheckBefore) {
const seenSeed = topics.map((t) => t.id).slice(-DEFAULTS.seenCap);
GM_setValue(K.seen, seenSeed);
GM_setValue(K.lastCheck, Date.now());
return { seeded: true, count: topics.length };
}

const cache = getCatCache();
const catIdSet = expandCategoryIds(s.categoryIds, cache);
const catName = new Map(cache.list.map((c) => [c.id, c.name]));

// 读取最新 seen(多标签页合并)后再判定
const seen = new Set(idArray(GM_getValue(K.seen, [])));
const cutoff = Math.max(lastCheckBefore, startedAt - s.catchupMin * 60000);
const hits = [];

for (const t of topics) {
if (seen.has(t.id)) continue;
seen.add(t.id);
const created = Date.parse(t.created_at ?? 0) || 0;
const bumped = Date.parse(t.bumped_at ?? t.created_at ?? 0) || 0;
const inWindow = s.onlyNew ? created > cutoff : bumped > cutoff;
if (!inWindow) continue;
if (!topicMatches(t, s, catIdSet)) continue;
hits.push(t);
}

// 先写存储(占坑),再发通知,避免并发重复
GM_setValue(K.seen, [...seen].slice(-DEFAULTS.seenCap));
GM_setValue(K.lastCheck, Date.now());

for (const t of hits.slice(0, DEFAULTS.burstCap)) {
const posterId = t.posters?.[0]?.user_id;
const avatar = absolutize(
users.get(posterId)?.avatar_template?.replace("{size}", "96"),
);
const cat = catName.get(t.category_id);
pushNotify({
title: t.title || `话题 #${t.id}`,
body: `${cat ? `【${cat}】` : ""}${t.posts_count != null ? `${t.posts_count} 回复` : ""} · 点击打开话题`,
icon: t.image_url || avatar || undefined,
url: TOPIC_URL(t),
tag: `ld-topic-${t.id}`,
});
}
if (hits.length > DEFAULTS.burstCap) {
const extra = hits.slice(DEFAULTS.burstCap);
pushNotify({
title: `LINUX DO:另有 ${extra.length} 条命中话题`,
body: extra.map((t) => t.title).filter(Boolean).slice(0, 3).join(" / "),
url: TOPIC_URL(extra[0]),
tag: "ld-topic-burst",
});
}

if (hits.length) {
GM_setValue(K.total, (Number(GM_getValue(K.total, 0)) || 0) + hits.length);
}
return { hits: hits.length, scanned: topics.length };
}

/* ---------------- 调度 ---------------- */

let timer = null;
let running = false;
let backoff = 1;

function schedule(ms) {
clearTimeout(timer);
timer = setTimeout(tick, ms);
}

async function tick() {
const s = loadSettings();
if (!s.enabled) { schedule(20000); return; } // 停用时低频空转等重新启用
if (running) { schedule(5000); return; }
running = true;
try {
const r = await checkNow(false);
backoff = 1;
if (r?.seeded) {
setStatus(`已初始化(跳过存量 ${r.count} 条),开始监听`, "ok");
} else if (!r?.skipped) {
const total = Number(GM_getValue(K.total, 0)) || 0;
setStatus(
`${timeStr(Date.now())} 已检查 ${r.scanned} 条 · 本次命中 ${r.hits} · 累计推送 ${total} · 每 ${s.intervalSec}s`,
r.hits ? "ok" : "info",
);
}
} catch (e) {
backoff = Math.min(backoff * 2, 8);
setStatus(`${timeStr(Date.now())} ${e?.message ?? "检查失败"}(${backoff} 倍降速)`, "error");
} finally {
running = false;
schedule(s.intervalSec * 1000 * backoff + Math.random() * 2000); // 抖动防多标签撞车
}
}

/* ---------------- 面板 UI ---------------- */

function renderCats(host) {
const box = host.querySelector("[data-p-cats]");
if (!box) return;
const cache = getCatCache();
const selected = new Set(idArray(GM_getValue(K.categories, [])));
if (!cache.list.length) {
box.innerHTML = `<span style="color:var(--ldp-muted);font-size:12px">尚未加载,点「刷新分类」拉取</span>`;
return;
}
box.innerHTML = cache.list.map((c) => `
<label title="${escapeHtml(c.name)}">
<input type="checkbox" data-p-cat="${c.id}" ${selected.has(c.id) ? "checked" : ""}>
<span>${escapeHtml(c.name)}</span>
</label>`).join("");
}

function mountUI() {
if (document.getElementById("ld-push-root") || !document.body) return;
const s = loadSettings();

const host = document.createElement("div");
host.id = "ld-push-root";
host.innerHTML = `
<button class="ldp__trigger" type="button" aria-expanded="false" title="话题订阅推送">
🔔<span class="ldp__dot" data-p-dot ${s.enabled ? "" : "hidden"}></span>
</button>
<section class="ldp__panel" hidden role="dialog" aria-label="话题订阅推送设置">
<header>
<div><strong>话题订阅推送</strong><span>命中关键词 / 分类 / 标签时通知</span></div>
<button class="ldp__close" type="button" data-p-close aria-label="关闭">×</button>
</header>

<div class="ldp__section">
<label class="ldp__row"><span><b>启用推送</b><small>保持任一 LINUX DO 页面打开即可</small></span>
<input type="checkbox" data-p-enabled ${s.enabled ? "checked" : ""}></label>
<label class="ldp__row"><span><b>订阅全部新话题</b><small>开启后忽略下方筛选条件</small></span>
<input type="checkbox" data-p-all ${s.subscribeAll ? "checked" : ""}></label>
<label class="ldp__row"><span><b>仅推送新发布</b><small>回帖顶起的老话题不推送</small></span>
<input type="checkbox" data-p-onlynew ${s.onlyNew ? "checked" : ""}></label>
</div>

<div class="ldp__section">
<div class="ldp__title">筛选条件(话题类型)</div>
<label class="ldp__field"><span>命中方式</span>
<select data-p-mode>
<option value="any" ${s.matchMode === "any" ? "selected" : ""}>任一条件命中即推送</option>
<option value="all" ${s.matchMode === "all" ? "selected" : ""}>已填条件需全部命中</option>
</select></label>
<label class="ldp__field"><span>关键词(每行一个,匹配标题,不区分大小写)</span>
<textarea data-p-keywords rows="3" placeholder="抽奖&#10;优惠码&#10;GPT">${escapeHtml(s.keywordsRaw)}</textarea></label>
<label class="ldp__field"><span>排除词(逗号分隔,命中则不推)</span>
<input type="text" data-p-exclude value="${escapeHtml(s.excludeRaw)}" placeholder="已截止, 抽奖专用"></label>
<div class="ldp__field"><span>分类(不勾选 = 不按分类筛选)
<button class="ldp__mini" type="button" data-p-refresh-cats>刷新分类</button></span>
<div class="ldp__cats" data-p-cats></div></div>
<label class="ldp__field"><span>标签(逗号分隔)</span>
<input type="text" data-p-tags value="${escapeHtml(s.tagsRaw)}" placeholder="ai, 羊毛, 资讯"></label>
</div>

<div class="ldp__section">
<div class="ldp__title">运行</div>
<label class="ldp__field"><span>轮询间隔 <b data-p-interval-label>${s.intervalSec}s</b></span>
<input type="range" min="30" max="300" step="15" value="${s.intervalSec}" data-p-interval></label>
<label class="ldp__field"><span>补推窗口(分钟):离线重开最多补推多久内的话题</span>
<input type="number" min="1" max="240" value="${s.catchupMin}" data-p-catchup></label>
<div class="ldp__actions">
<button type="button" data-p-check>立即检查</button>
<button type="button" data-p-test>测试通知</button>
<button type="button" data-p-reset>清空记录</button>
</div>
</div>

<output class="ldp__status" data-p-status>等待首次检查…</output>
<p class="ldp__hint">设置仅保存在本浏览器;点击通知直达话题。</p>
</section>`;
document.body.append(host);

const $q = (sel) => host.querySelector(sel);
const panel = $q(".ldp__panel");
const trigger = $q(".ldp__trigger");
statusEl = $q("[data-p-status]");

const setOpen = (open) => {
panel.hidden = !open;
trigger.setAttribute("aria-expanded", String(open));
};
trigger.addEventListener("click", () => setOpen(panel.hidden));
$q("[data-p-close]").addEventListener("click", () => setOpen(false));
document.addEventListener("pointerdown", (e) => {
if (!panel.hidden && !host.contains(e.target)) setOpen(false);
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && !panel.hidden) setOpen(false);
});

$q("[data-p-enabled]").addEventListener("change", (e) => {
GM_setValue(K.enabled, e.target.checked);
$q("[data-p-dot]").hidden = !e.target.checked;
if (e.target.checked) { ensureWebPermission(); schedule(300); }
setStatus(e.target.checked ? "已启用,开始监听" : "已停用", "info");
});
$q("[data-p-all]").addEventListener("change", (e) =>
GM_setValue(K.subscribeAll, e.target.checked));
$q("[data-p-onlynew]").addEventListener("change", (e) =>
GM_setValue(K.onlyNew, e.target.checked));
$q("[data-p-mode]").addEventListener("change", (e) =>
GM_setValue(K.matchMode, e.target.value === "all" ? "all" : "any"));
$q("[data-p-keywords]").addEventListener("change", (e) =>
GM_setValue(K.keywords, e.target.value));
$q("[data-p-exclude]").addEventListener("change", (e) =>
GM_setValue(K.exclude, e.target.value));
$q("[data-p-tags]").addEventListener("change", (e) =>
GM_setValue(K.tags, e.target.value));
$q("[data-p-interval]").addEventListener("input", (e) => {
const v = clamp(Number(e.target.value) || 90, 30, 300);
$q("[data-p-interval-label]").textContent = `${v}s`;
GM_setValue(K.interval, v);
});
$q("[data-p-catchup]").addEventListener("change", (e) => {
const v = clamp(Number(e.target.value) || 30, 1, 240);
e.target.value = String(v);
GM_setValue(K.catchup, v);
});

$q("[data-p-cats]").addEventListener("change", () => {
const ids = [...host.querySelectorAll("[data-p-cat]:checked")]
.map((i) => Number(i.dataset.pCat));
GM_setValue(K.categories, ids);
});
$q("[data-p-refresh-cats]").addEventListener("click", async (e) => {
e.target.disabled = true;
setStatus("正在拉取分类列表…");
try {
const cache = await fetchCategories();
renderCats(host);
setStatus(`分类已更新(${cache.list.length} 个)`, "ok");
} catch (err) {
setStatus(err.message, "error");
} finally {
e.target.disabled = false;
}
});

$q("[data-p-check]").addEventListener("click", async () => {
setStatus("正在检查…");
try {
const r = await checkNow(true);
setStatus(
r.seeded
? `已初始化(跳过存量 ${r.count} 条)`
: `检查完成:命中 ${r.hits} 条`,
r.hits ? "ok" : "info",
);
} catch (err) {
setStatus(err.message, "error");
}
});
$q("[data-p-test]").addEventListener("click", () => {
ensureWebPermission();
pushNotify({
title: "LINUX DO 订阅推送测试",
body: "看到这条通知说明推送正常,点击可打开最新页",
url: "https://linux.do/latest",
tag: "ld-push-test",
});
setStatus("测试通知已发送(无反应请检查通知权限)", "info");
});
$q("[data-p-reset]").addEventListener("click", () => {
GM_setValue(K.seen, []);
GM_setValue(K.total, 0);
GM_setValue(K.lastCheck, 0);
setStatus("已清空:下次检查将重新播种(不补推存量)", "ok");
});

renderCats(host);
if (!getCatCache().list.length) {
fetchCategories().then(() => renderCats(host)).catch(() => {});
}
}

/* ---------------- 启动 ---------------- */

function boot() {
mountUI();
ensureWebPermission();
schedule(2000 + Math.random() * 3000);

document.addEventListener("visibilitychange", () => {
if (document.hidden) return;
const s = loadSettings();
const last = Number(GM_getValue(K.lastCheck, 0)) || 0;
if (s.enabled && Date.now() - last >= s.intervalSec * 1000) schedule(500);
});

new MutationObserver(() => {
if (!document.getElementById("ld-push-root")) {
statusEl = null;
mountUI();
}
}).observe(document.body, { childList: true });
}

if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", boot, { once: true });
} else {
boot();
}
})();
最新回复 (2)
  • yourui 08-04 20:46
    1

    感谢佬友分享,我拿走试试了,应该非常实用

  • salsal101 08-18 00:42
    2

    不吃429警告吗 多标签90s一次

* 帖子来源Linux.do
返回