发一个boost 的小玩具

2026-04-13 12:581阅读0评论SEO基础
  • 内容介绍
  • 文章标签
  • 相关推荐
问题描述:

回复话题帖子的时候自动判断是否发送boost

PixPin_2026-04-08_12-50-45746×222 16.5 KB

油猴脚本 linux.do 自动切换 boost/回复方式

油猴脚本

// ==UserScript== // @name linux.do 自动切换 boost/回复方式 // @namespace https://linux.do/ // @version 1.3 // @description ≤16字自动走 Boost;17~19字保留普通回复并提示;20字及以上正常回复。支持同帖单用户仅1次 Boost、每帖 50 Boost 上限检测,并在不可 Boost 时自动切换普通回复提示。 // @match https://linux.do/t/* // @run-at document-end // @grant none // ==/UserScript== (function () { "use strict"; const CONFIG = { BOOST_MAX_LEN: 16, REPLY_MIN_LEN: 20, MAX_BOOSTS_PER_TARGET: 50, AUTO_CLOSE_COMPOSER_AFTER_SUCCESS: true, BOOST_TOPIC_REPLY_TO_FIRST_POST: true, TOAST_MS: 3200, DEBUG: false, MAX_TOPIC_CACHE: 8, CURRENT_USER_RETRY_MS: 10 * 60 * 1000, }; const COLORS = { BOOST: "#e67e22", ERROR: "#c0392b", PENDING: "#7f8c8d", UNCERTAIN: "#2d7ff9", WARNING: "#f39c12", SUCCESS: "#27ae60", }; const state = { sending: false, rafId: 0, moTimer: 0, domVersion: 0, composerSyncTimers: [], currentTopicId: null, lastClickedTarget: null, composerTarget: null, currentUser: null, currentUserPromise: null, // 避免反复请求 session/current.json 导致 429。 currentUserFetchTried: false, currentUserRetryAt: 0, topicMetaData: new Map(), topicMetaPromises: new Map(), topicBoostScanCache: { key: "", data: null, }, // topicId => { boostedTargets: Set<postId>, fullTargets: Set<postId> } localBoostGuards: new Map(), }; const qs = (sel, root = document) => root.querySelector(sel); const qsa = (sel, root = document) => Array.from(root.querySelectorAll(sel)); const log = (...args) => CONFIG.DEBUG && console.log("[ld-boost]", ...args); function asElement(node) { return node instanceof Element ? node : null; } function stopEvent(e) { e?.preventDefault?.(); e?.stopPropagation?.(); e?.stopImmediatePropagation?.(); } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function countChars(text) { return [...String(text || "").trim()].length; } function normalizeUsername(value) { return String(value || "").trim().replace(/^@/, "").toLowerCase(); } function normalizeErrorText(value) { return String(value || "").trim().toLowerCase(); } function pickDefined(...values) { for (const v of values) { if (v !== undefined && v !== null && v !== "") return v; } return null; } function pruneMapCache(map, max = CONFIG.MAX_TOPIC_CACHE) { while (map.size > max) { const oldestKey = map.keys().next().value; map.delete(oldestKey); } } function getTopicId() { return ( Number( qs('section#topic[data-topic-id]')?.dataset.topicId || qs('#topic-title h1[data-topic-id]')?.dataset.topicId || location.pathname.match(/\/t\/[^/]+\/(\d+)/)?.[1] || 0 ) || null ); } function resetBoostScanCache() { state.topicBoostScanCache.key = ""; state.topicBoostScanCache.data = null; } function syncTopicChange() { const topicId = getTopicId(); if (state.currentTopicId && topicId && state.currentTopicId !== topicId) { state.lastClickedTarget = null; state.composerTarget = null; resetBoostScanCache(); state.domVersion++; } state.currentTopicId = topicId || state.currentTopicId; } function getCanonicalTopicPath() { const href = qs('#topic-title a.fancy-title[href*="/t/"]')?.getAttribute("href") || qs('#reply-control .reply-details .topic-link[href*="/t/"]')?.getAttribute("href") || location.pathname; const clean = String(href || "").split(/[?#]/)[0]; const match = clean.match(/\/t\/[^/]+\/\d+/); return match ? match[0] : null; } function getCsrfToken() { return qs('meta[name="csrf-token"]')?.content || ""; } function getComposer() { return qs("#reply-control .reply-area"); } function getComposerTextarea() { return qs("#reply-control textarea.d-editor-input"); } function getComposerSubmitBtn() { return qs("#reply-control .submit-panel button.create"); } function getComposerDiscardBtn() { return qs("#reply-control .discard-button"); } function getFirstPostArticle() { return ( qs('section#topic article#post_1[data-post-id]') || qs('section#topic .topic-post[data-post-number="1"] article[data-post-id]') ); } function getArticleByPostNumber(postNumber) { if (!postNumber) return null; return ( qs(`section#topic article#post_${postNumber}[data-post-id]`) || qs(`section#topic .topic-post[data-post-number="${postNumber}"] article[data-post-id]`) ); } function getArticleByPostId(postId) { if (!postId) return null; return qs(`section#topic article[data-post-id="${postId}"]`); } function getArticleForTarget(target) { if (!target) return null; if (target.mode === "topic") return getFirstPostArticle(); return ( (target.postNumber ? getArticleByPostNumber(target.postNumber) : null) || (target.postId ? getArticleByPostId(target.postId) : null) || null ); } function getPostNumberFromTopicHref(href) { try { const url = new URL(href, location.origin); const parts = url.pathname.split("/").filter(Boolean); if (parts[0] !== "t") return null; return Number(parts[3] || 0) || null; } catch { return null; } } function extractUsernameFromArticle(article) { return normalizeUsername( qs('.topic-meta-data .names a[href^="/u/"]', article)?.getAttribute("href")?.split("/u/")[1]?.split(/[/?#]/)[0] || "" ); } function getBoostButtonInArticle(article) { if (!article) return null; const menuArea = qs(".post__menu-area", article); if (!menuArea) return null; const candidates = qsa( [ "button.post-action-menu__boost", "button.discourse-boosts__add-btn", "button.discourse-boosts-trigger", ].join(","), menuArea ); return ( candidates.find((el) => { if (!(el instanceof HTMLElement)) return false; if (el.disabled) return false; if (el.getAttribute("aria-disabled") === "true") return false; return true; }) || null ); } function detectBoostSupportFromArticle(article) { if (!article) return "unknown"; if (getBoostButtonInArticle(article)) return "yes"; // 已渲染出 boosts 区域但没有可点击按钮时,视为当前目标不可 Boost。 const hasBoostHints = !!qs( ".discourse-boosts__post-menu, .discourse-boosts, [class*='discourse-boosts']", article ); return hasBoostHints ? "no" : "unknown"; } function getBoostBubbles(article) { if (!article) return []; return qsa(".discourse-boosts__bubble", article); } function extractUsernameFromBoostBubble(bubble) { if (!bubble) return ""; const fromCard = normalizeUsername( qs("a[data-user-card]", bubble)?.getAttribute("data-user-card") || "" ); if (fromCard) return fromCard; const fromHref = normalizeUsername( qs('a[href^="/u/"]', bubble)?.getAttribute("href")?.split("/u/")[1]?.split(/[/?#]/)[0] || "" ); return fromHref; } function scanTopicBoostsSync() { syncTopicChange(); const topicId = getTopicId(); const cacheKey = `${topicId || 0}:${state.domVersion}`; if (state.topicBoostScanCache.key === cacheKey && state.topicBoostScanCache.data) { return state.topicBoostScanCache.data; } const byPostId = new Map(); const allUsernames = new Set(); qsa("section#topic article[data-post-id]").forEach((article) => { const postId = Number(article.dataset.postId || 0) || null; if (!postId) return; const bubbles = getBoostBubbles(article); const users = new Set(); for (const bubble of bubbles) { const username = extractUsernameFromBoostBubble(bubble); if (username) { users.add(username); allUsernames.add(username); } } byPostId.set(postId, { count: bubbles.length, users, }); }); const data = { byPostId, allUsernames }; state.topicBoostScanCache.key = cacheKey; state.topicBoostScanCache.data = data; return data; } function ensureLocalBoostGuard(topicId = getTopicId()) { if (!topicId) return null; const key = String(topicId); let guard = state.localBoostGuards.get(key); if (!guard) { guard = { boostedTargets: new Set(), fullTargets: new Set(), }; state.localBoostGuards.set(key, guard); pruneMapCache(state.localBoostGuards); } return guard; } function getLocalBoostGuard(topicId = getTopicId()) { if (!topicId) return null; return state.localBoostGuards.get(String(topicId)) || null; } function markLocalBoostedTarget(topicId = getTopicId(), postId) { const guard = ensureLocalBoostGuard(topicId); if (guard && postId) { guard.boostedTargets.add(Number(postId)); } } function markLocalTargetFull(topicId = getTopicId(), postId) { const guard = ensureLocalBoostGuard(topicId); if (guard && postId) { guard.fullTargets.add(Number(postId)); } } function getTargetBoostSnapshot(target, scan) { const article = getArticleForTarget(target); const hasBoostButton = !!getBoostButtonInArticle(article); if (target.postId && scan?.byPostId?.has(target.postId)) { const data = scan.byPostId.get(target.postId); return { hasBoostButton, targetBoostCount: data.count, targetUsers: data.users }; } if (!article) { return { hasBoostButton, targetBoostCount: null, targetUsers: null }; } // 直接基于当前帖子 DOM 统计,避免局部刷新时拿到旧状态。 const targetUsers = new Set(); const bubbles = getBoostBubbles(article); bubbles.forEach((bubble) => { const username = extractUsernameFromBoostBubble(bubble); if (username) targetUsers.add(username); }); return { hasBoostButton, targetBoostCount: bubbles.length, targetUsers }; } function isTargetBoostLimitReached(snapshot, target, localGuard) { // 页面上仍有可点击的 Boost 按钮时,优先认为目标未满。 if (snapshot.hasBoostButton) return false; if (localGuard?.fullTargets?.has?.(Number(target.postId))) return true; return Number.isFinite(snapshot.targetBoostCount) && snapshot.targetBoostCount >= CONFIG.MAX_BOOSTS_PER_TARGET; } function getBoostConstraintStateSync(target) { if (!target) { return { targetBoostCount: null, targetBoostLimitReached: false, myBoostInTarget: false }; } const me = state.currentUser || readCurrentUserFromMeta() || readCurrentUserFromGlobals(); const myUsername = normalizeUsername(me?.username); const scan = scanTopicBoostsSync(); const localGuard = getLocalBoostGuard(target.topicId || getTopicId()); const snapshot = getTargetBoostSnapshot(target, scan); const myBoostInTarget = !!(myUsername && snapshot.targetUsers instanceof Set && snapshot.targetUsers.has(myUsername)); const hasLocalBoostInTarget = !!localGuard?.boostedTargets?.has?.(Number(target.postId)); return { targetBoostCount: snapshot.targetBoostCount, targetBoostLimitReached: isTargetBoostLimitReached(snapshot, target, localGuard), myBoostInTarget: myBoostInTarget || hasLocalBoostInTarget, }; } function makeNormalReplyFallback(code, text, title) { return { blocked: true, code, text, title, fallbackToNormalReply: true, normalReplyText: `普通回复(需≥${CONFIG.REPLY_MIN_LEN}字)`, normalReplyTitle: `${title};已自动切换为普通回复,普通回复通常至少需要 ${CONFIG.REPLY_MIN_LEN} 字`, }; } function getBoostBlockReasonSync(target) { if (!target?.postId) { return { blocked: true, code: "no-target", text: "Boost 目标识别中", title: "正在识别目标帖子", fallbackToNormalReply: false, }; } if (isOwnTargetSync(target)) { return makeNormalReplyFallback( "self", "自己的帖子/话题不可 Boost", "这是你自己的帖子或话题,不支持 Boost" ); } const constraints = getBoostConstraintStateSync(target); if (constraints.myBoostInTarget) { return makeNormalReplyFallback( "duplicate-post", "你已在本帖 Boost 过", "同一用户在同一帖子中只能发送 1 个 Boost" ); } if (constraints.targetBoostLimitReached) { return makeNormalReplyFallback( "limit", `本帖 Boost 已满 ${CONFIG.MAX_BOOSTS_PER_TARGET} 个`, `当前帖子已达到 ${CONFIG.MAX_BOOSTS_PER_TARGET} 个 Boost 上限` ); } if (target.boostSupport === "no") { return makeNormalReplyFallback( "unsupported", "此帖不可 Boost", "当前目标帖子不支持 Boost" ); } return { blocked: false, code: "ok", constraints, fallbackToNormalReply: false, }; } function articleToTarget(article, mode = "post", source = "") { if (!article) return null; const postId = Number(article.dataset.postId || 0) || null; if (!postId) return null; const postNumber = Number( article.id?.match(/^post_(\d+)$/)?.[1] || article.closest(".topic-post,[data-post-number]")?.dataset.postNumber || 0 ) || null; const ownerUserId = Number(article.dataset.userId || 0) || null; const ownerUsername = extractUsernameFromArticle(article) || null; return { topicId: getTopicId(), mode, postId, postNumber, ownerUserId, ownerUsername, boostSupport: detectBoostSupportFromArticle(article), source, }; } function mergeTopicMeta(oldMeta = {}, newMeta = {}) { return { topicId: pickDefined(newMeta.topicId, oldMeta.topicId), firstPostId: pickDefined(newMeta.firstPostId, oldMeta.firstPostId), firstPostOwnerId: pickDefined(newMeta.firstPostOwnerId, oldMeta.firstPostOwnerId), firstPostOwnerUsername: pickDefined(newMeta.firstPostOwnerUsername, oldMeta.firstPostOwnerUsername), firstPostBoostSupport: newMeta.firstPostBoostSupport && newMeta.firstPostBoostSupport !== "unknown" ? newMeta.firstPostBoostSupport : oldMeta.firstPostBoostSupport || "unknown", }; } function setTopicMeta(topicId, meta) { if (!topicId || !meta) return null; const key = String(topicId); const oldMeta = state.topicMetaData.get(key) || {}; const merged = mergeTopicMeta(oldMeta, meta); state.topicMetaData.delete(key); state.topicMetaData.set(key, merged); pruneMapCache(state.topicMetaData); return merged; } function getTopicMetaSync(topicId = getTopicId()) { if (!topicId) return null; const domArticle = getFirstPostArticle(); if (domArticle) { setTopicMeta(topicId, { topicId, firstPostId: Number(domArticle.dataset.postId || 0) || null, firstPostOwnerId: Number(domArticle.dataset.userId || 0) || null, firstPostOwnerUsername: extractUsernameFromArticle(domArticle) || null, firstPostBoostSupport: detectBoostSupportFromArticle(domArticle), }); } return state.topicMetaData.get(String(topicId)) || null; } async function ensureTopicMeta(topicId = getTopicId()) { if (!topicId) return null; const key = String(topicId); const syncMeta = getTopicMetaSync(topicId); if (syncMeta?.firstPostId && (syncMeta?.firstPostOwnerId || syncMeta?.firstPostOwnerUsername)) { return syncMeta; } if (state.topicMetaPromises.has(key)) { return state.topicMetaPromises.get(key); } const promise = (async () => { try { const canonicalPath = getCanonicalTopicPath() || `/t/topic/${topicId}`; const res = await fetch(`${canonicalPath}.json`, { credentials: "same-origin", }); if (!res.ok) { throw new Error(`获取 topic json 失败:HTTP ${res.status}`); } const data = await res.json(); const firstPost = data?.post_stream?.posts?.find((p) => Number(p.post_number) === 1) || data?.post_stream?.posts?.[0] || null; const domFirstPost = getFirstPostArticle(); const fetchedMeta = { topicId, firstPostId: Number(firstPost?.id || data?.post_stream?.stream?.[0] || 0) || null, firstPostOwnerId: Number(firstPost?.user_id || data?.details?.created_by?.id || 0) || null, firstPostOwnerUsername: normalizeUsername(firstPost?.username || data?.details?.created_by?.username || "") || null, firstPostBoostSupport: domFirstPost ? detectBoostSupportFromArticle(domFirstPost) : "unknown", }; return setTopicMeta(topicId, fetchedMeta); } finally { state.topicMetaPromises.delete(key); } })(); state.topicMetaPromises.set(key, promise); return promise; } function targetFromTopicMeta(meta, source = "") { if (!meta) return null; return { topicId: meta.topicId || getTopicId(), mode: "topic", postId: meta.firstPostId || null, postNumber: 1, ownerUserId: meta.firstPostOwnerId || null, ownerUsername: meta.firstPostOwnerUsername || null, boostSupport: meta.firstPostBoostSupport || "unknown", source, }; } function mergeTarget(base = {}, overlay = {}) { return { topicId: pickDefined(overlay.topicId, base.topicId), mode: pickDefined(overlay.mode, base.mode), postId: pickDefined(overlay.postId, base.postId), postNumber: pickDefined(overlay.postNumber, base.postNumber), ownerUserId: pickDefined(overlay.ownerUserId, base.ownerUserId), ownerUsername: pickDefined(overlay.ownerUsername, base.ownerUsername), boostSupport: overlay.boostSupport && overlay.boostSupport !== "unknown" ? overlay.boostSupport : base.boostSupport || "unknown", source: pickDefined(overlay.source, base.source), }; } function getTopicReplyTargetSync() { if (!CONFIG.BOOST_TOPIC_REPLY_TO_FIRST_POST) return null; const domArticle = getFirstPostArticle(); if (domArticle) { return articleToTarget(domArticle, "topic", "topic-first-post-dom"); } const meta = getTopicMetaSync(getTopicId()); if (meta) { return targetFromTopicMeta(meta, "topic-meta-sync"); } return { topicId: getTopicId(), mode: "topic", postId: null, postNumber: 1, ownerUserId: null, ownerUsername: null, boostSupport: "unknown", source: "topic-pending", }; } function refreshTargetSync(target) { if (!target) return null; if (target.mode === "topic") { const domArticle = getFirstPostArticle(); if (domArticle) { return mergeTarget(target, articleToTarget(domArticle, "topic", "topic-refresh-dom")); } const meta = getTopicMetaSync(target.topicId || getTopicId()); if (meta) { return mergeTarget(target, targetFromTopicMeta(meta, "topic-refresh-meta")); } return target; } let article = null; if (target.postNumber) { article = getArticleByPostNumber(target.postNumber); } if (!article && target.postId) { article = getArticleByPostId(target.postId); } if (article) { return mergeTarget(target, articleToTarget(article, "post", "post-refresh-dom")); } return target; } function packUser(raw) { if (!raw) return null; const id = Number(raw.id || raw.user_id || raw.userId || 0) || null; const username = normalizeUsername(raw.username || raw.user || raw.login || ""); if (!id && !username) return null; return { id, username }; } function readCurrentUserFromMeta() { const username = normalizeUsername( qs('meta[name="current-user"]')?.content || qs('meta[name="discourse-current-username"]')?.content || "" ); const id = Number( qs('meta[name="current-user-id"]')?.content || qs('meta[name="discourse-current-user-id"]')?.content || 0 ) || null; return id || username ? { id, username } : null; } function readCurrentUserFromGlobals() { try { if (window.currentUser) { const u = packUser(window.currentUser); if (u) return u; } } catch (err) { log("readCurrentUserFromGlobals currentUser 失败", err); } try { if (window.Discourse?.currentUser) { const u = packUser(window.Discourse.currentUser); if (u) return u; } } catch (err) { log("readCurrentUserFromGlobals Discourse.currentUser 失败", err); } try { const discourseUser = window.require?.("discourse/lib/user")?.default?.current?.(); if (discourseUser) { const u = packUser(discourseUser); if (u) return u; } } catch (err) { log("readCurrentUserFromGlobals discourse/lib/user 失败", err); } return null; } async function ensureCurrentUser() { if (state.currentUser) return state.currentUser; if (state.currentUserPromise) return state.currentUserPromise; state.currentUserPromise = (async () => { let user = readCurrentUserFromMeta() || readCurrentUserFromGlobals(); if (!user) { const now = Date.now(); const canRetry = !state.currentUserFetchTried || now >= state.currentUserRetryAt; // 仅在首次或退避期结束后才请求,避免频繁打到 session/current.json。 if (canRetry) { state.currentUserFetchTried = true; state.currentUserRetryAt = now + CONFIG.CURRENT_USER_RETRY_MS; try { const res = await fetch("/session/current.json", { credentials: "same-origin", }); if (res.ok) { const data = await res.json(); user = packUser(data?.current_user || data?.user || data); } } catch (err) { log("读取当前用户失败", err); } } } state.currentUser = user || null; state.currentUserPromise = null; scheduleUIUpdate(); return state.currentUser; })(); return state.currentUserPromise; } function isOwnTargetSync(target) { const me = state.currentUser || readCurrentUserFromMeta() || readCurrentUserFromGlobals(); if (!me || !target) return false; if (me.id && target.ownerUserId) { return Number(me.id) === Number(target.ownerUserId); } if (me.username && target.ownerUsername) { return normalizeUsername(me.username) === normalizeUsername(target.ownerUsername); } return false; } function resolveTargetFromReplyLauncher(btn) { if (!btn) return null; if ( btn.matches(".post-action-menu__reply.reply.create") || btn.closest(".post-action-menu__reply.reply.create") ) { const article = btn.closest("article[data-post-id]") || btn.closest(".topic-post")?.querySelector("article[data-post-id]"); return articleToTarget(article, "post", "clicked-post-reply"); } if ( btn.matches(".reply-to-post, #topic-footer-buttons .topic-footer-button.create") || btn.closest(".reply-to-post, #topic-footer-buttons .topic-footer-button.create") ) { return getTopicReplyTargetSync(); } return null; } function parseComposerTarget() { const composer = getComposer(); if (!composer) { state.composerTarget = null; return null; } const userLink = qs('.reply-details .action-title .user-link[href*="/t/"]', composer); if (userLink) { const postNumber = getPostNumberFromTopicHref(userLink.getAttribute("href") || ""); if (postNumber) { const article = getArticleByPostNumber(postNumber); if (article) { const target = articleToTarget(article, "post", "composer-user-link"); state.composerTarget = target; return target; } if ( state.lastClickedTarget?.mode === "post" && Number(state.lastClickedTarget.postNumber) === Number(postNumber) ) { const target = refreshTargetSync(state.lastClickedTarget); state.composerTarget = target; return target; } } } const topicLink = qs(".reply-details .action-title .topic-link", composer); if (topicLink) { const target = getTopicReplyTargetSync(); state.composerTarget = target; return target; } if (state.composerTarget) { state.composerTarget = refreshTargetSync(state.composerTarget); return state.composerTarget; } return null; } function getEffectiveTargetSync() { syncTopicChange(); const composerTarget = parseComposerTarget(); if (composerTarget) return refreshTargetSync(composerTarget); if (state.lastClickedTarget) { return refreshTargetSync(state.lastClickedTarget); } return null; } async function getEffectiveTargetForSubmit() { let target = getEffectiveTargetSync(); if (target?.mode === "topic" && (!target.postId || (!target.ownerUserId && !target.ownerUsername))) { await ensureTopicMeta(target.topicId || getTopicId()); target = getEffectiveTargetSync(); } if (!target?.postId) { for (const ms of [60, 150, 300, 600, 1200]) { await sleep(ms); target = getEffectiveTargetSync(); if (target?.mode === "topic" && (!target.postId || (!target.ownerUserId && !target.ownerUsername))) { try { await ensureTopicMeta(target.topicId || getTopicId()); } catch (err) { log("重试补全 topic meta 失败", err); } target = getEffectiveTargetSync(); } if (target?.postId) return target; } } return target; } function showToast(message, type = "info") { let container = qs("#ld-boost-toast"); if (!container) { container = document.createElement("div"); container.id = "ld-boost-toast"; container.style.cssText = [ "position:fixed", "top:20px", "right:20px", "z-index:999999", "padding:10px 14px", "border-radius:10px", "font-size:14px", "color:#fff", "box-shadow:0 8px 24px rgba(0,0,0,.18)", "opacity:0", "transform:translateY(-8px)", "transition:all .2s ease", "pointer-events:none", "max-width:520px", "word-break:break-word", ].join(";"); document.body.appendChild(container); } const bg = type === "success" ? COLORS.SUCCESS : type === "error" ? COLORS.ERROR : COLORS.UNCERTAIN; container.style.background = bg; container.textContent = message; container.style.opacity = "1"; container.style.transform = "translateY(0)"; clearTimeout(container.__ldTimer); container.__ldTimer = setTimeout(() => { container.style.opacity = "0"; container.style.transform = "translateY(-8px)"; }, CONFIG.TOAST_MS); } function scheduleUIUpdate() { cancelAnimationFrame(state.rafId); state.rafId = requestAnimationFrame(updateComposerUI); } function clearComposerSyncTimers() { state.composerSyncTimers.forEach(clearTimeout); state.composerSyncTimers = []; } function scheduleComposerSync() { clearComposerSyncTimers(); [0, 60, 150, 300, 600, 1200].forEach((ms) => { const timer = setTimeout(() => { parseComposerTarget(); scheduleUIUpdate(); }, ms); state.composerSyncTimers.push(timer); }); } function isInternalUIButtonLabel(text) { const t = String(text || "").trim(); return ( t === "Boost 回复" || t === "Boost 目标识别中" || t === "自己的帖子/话题不可 Boost" || t === "此帖不可 Boost" || t === "你已在本帖 Boost 过" || t === "Boost 发送中..." || t.startsWith("普通回复(") || t.startsWith("本帖 Boost 已满 ") ); } function resetSubmitButton(btn) { const label = qs(".d-button-label", btn); if (label) { const currentText = (label.textContent || "").trim(); if (!label.dataset.origLabel) { label.dataset.origLabel = currentText || "回复"; } else if (currentText && !isInternalUIButtonLabel(currentText)) { label.dataset.origLabel = currentText; } label.textContent = label.dataset.origLabel || "回复"; } btn.style.background = ""; btn.style.borderColor = ""; btn.dataset.useBoost = "0"; btn.dataset.boostBlocked = "0"; btn.title = "或按 Ctrl Enter"; } function getNormalReplyHintTitle(block) { return ( block?.normalReplyTitle || `${block?.title || "当前条件无法使用 Boost"};已自动切换为普通回复,普通回复通常至少需要 ${CONFIG.REPLY_MIN_LEN} 字` ); } function updateComposerUI() { if (state.sending) return; syncTopicChange(); const textarea = getComposerTextarea(); const primaryBtn = getComposerSubmitBtn(); if (!textarea || !primaryBtn) return; resetSubmitButton(primaryBtn); const raw = String(textarea.value || "").trim(); const len = countChars(raw); if (!raw) return; const target = getEffectiveTargetSync(); const label = qs(".d-button-label", primaryBtn); if (len > CONFIG.BOOST_MAX_LEN && len < CONFIG.REPLY_MIN_LEN) { if (label) { label.textContent = `普通回复(${len}字,需≥${CONFIG.REPLY_MIN_LEN}字)`; } primaryBtn.style.background = COLORS.WARNING; primaryBtn.style.borderColor = COLORS.WARNING; primaryBtn.title = `已超过 Boost 上限 ${CONFIG.BOOST_MAX_LEN} 字;普通回复通常至少需要 ${CONFIG.REPLY_MIN_LEN} 字`; return; } if (len >= CONFIG.REPLY_MIN_LEN) { primaryBtn.title = "或按 Ctrl Enter"; return; } const block = getBoostBlockReasonSync(target); if (block.blocked) { if (block.fallbackToNormalReply) { if (label) label.textContent = block.normalReplyText || `普通回复(需≥${CONFIG.REPLY_MIN_LEN}字)`; primaryBtn.style.background = COLORS.WARNING; primaryBtn.style.borderColor = COLORS.WARNING; primaryBtn.title = getNormalReplyHintTitle(block); return; } if (label) label.textContent = block.text || "Boost 目标识别中"; primaryBtn.style.background = block.code === "no-target" ? COLORS.PENDING : COLORS.ERROR; primaryBtn.style.borderColor = block.code === "no-target" ? COLORS.PENDING : COLORS.ERROR; primaryBtn.dataset.boostBlocked = "1"; primaryBtn.title = block.title || "当前无法使用 Boost"; return; } const stats = block.constraints || {}; const countDesc = Number.isFinite(stats.targetBoostCount) ? `,当前 ${stats.targetBoostCount}/${CONFIG.MAX_BOOSTS_PER_TARGET}` : ""; if (label) label.textContent = "Boost 回复"; primaryBtn.style.background = target.boostSupport === "unknown" ? COLORS.UNCERTAIN : COLORS.BOOST; primaryBtn.style.borderColor = target.boostSupport === "unknown" ? COLORS.UNCERTAIN : COLORS.BOOST; primaryBtn.dataset.useBoost = "1"; primaryBtn.title = target.boostSupport === "unknown" ? `当前 ${len}/${CONFIG.BOOST_MAX_LEN} 字,目标 #${target.postNumber || "?"},支持状态未确认,将尝试 Boost${countDesc}` : `当前 ${len}/${CONFIG.BOOST_MAX_LEN} 字,将走 Boost(目标 #${target.postNumber || "?"})${countDesc}`; } function tryParseJSON(text) { try { return text ? JSON.parse(text) : null; } catch { return null; } } function extractErrorMessage(payload, fallbackText = "") { if (!payload) return String(fallbackText || "").trim(); if (typeof payload === "string") return payload.trim(); if (Array.isArray(payload)) return payload.map((x) => String(x)).join(";"); if (Array.isArray(payload.errors) && payload.errors.length) { return payload.errors.map((x) => String(x)).join(";"); } if (typeof payload.error === "string" && payload.error) return payload.error.trim(); if (typeof payload.message === "string" && payload.message) return payload.message.trim(); if (typeof payload.detail === "string" && payload.detail) return payload.detail.trim(); return String(fallbackText || "").trim(); } function classifyBoostError(message) { const s = normalizeErrorText(message); if (!s) return "other"; if ( /already|duplicate|same user|one boost|only one|只能.*一次|只能.*1.*次|已经.*boost|已.*boost|重复/.test(s) ) { return "duplicate"; } if ( /limit|max|maximum|full|too many|reached|over|上限|已满|最多|50/.test(s) ) { return "limit"; } return "other"; } async function postBoost(raw, target) { const csrf = getCsrfToken(); if (!csrf) throw new Error("未找到 csrf-token"); const res = await fetch(`/discourse-boosts/posts/${target.postId}/boosts`, { method: "POST", credentials: "same-origin", headers: { "content-type": "application/x-www-form-urlencoded; charset=UTF-8", "x-csrf-token": csrf, "x-requested-with": "XMLHttpRequest", "discourse-logged-in": "true", "discourse-present": "true", }, body: new URLSearchParams({ raw }).toString(), }); const text = await res.text(); const payload = tryParseJSON(text); if (!res.ok) { const message = extractErrorMessage(payload, text || `HTTP ${res.status}`); const err = new Error(message || `HTTP ${res.status}`); err.code = classifyBoostError(message); err.httpStatus = res.status; err.payload = payload; throw err; } return payload || text || { ok: true }; } function closeComposer() { const discard = getComposerDiscardBtn(); if (discard) { discard.click(); return true; } const closeBtn = qs("#reply-control .toggle-save-and-close") || qs("#reply-control .toggle-minimize"); if (closeBtn) { closeBtn.click(); return true; } return false; } function shouldInterceptSubmitAsBoostSync() { const textarea = getComposerTextarea(); const len = countChars(textarea?.value || ""); if (!len || len > CONFIG.BOOST_MAX_LEN) { return false; } const target = getEffectiveTargetSync(); const block = getBoostBlockReasonSync(target); if (block.blocked && block.fallbackToNormalReply) { return false; } return true; } async function sendBoostFromComposer(e, { force = false } = {}) { if (state.sending) { stopEvent(e); return false; } const textarea = getComposerTextarea(); const primaryBtn = getComposerSubmitBtn(); if (!textarea || !primaryBtn) return true; const raw = String(textarea.value || "").trim(); const len = countChars(raw); if (!raw) { stopEvent(e); showToast("内容不能为空", "error"); return false; } if (!force && len > CONFIG.BOOST_MAX_LEN) { return true; } if (len > CONFIG.BOOST_MAX_LEN) { stopEvent(e); showToast(`Boost 最多支持 ${CONFIG.BOOST_MAX_LEN} 个字符,当前 ${len} 个`, "error"); return false; } stopEvent(e); let target = await getEffectiveTargetForSubmit(); if (target?.mode === "topic" && (!target.postId || (!target.ownerUserId && !target.ownerUsername))) { try { const meta = await ensureTopicMeta(target.topicId || getTopicId()); target = mergeTarget(target, targetFromTopicMeta(meta, "submit-topic-meta")); } catch (err) { log("补全 topic meta 失败", err); } } target = refreshTargetSync(target); if (!target?.postId) { showToast("未找到目标 post_id,无法使用 Boost", "error"); scheduleUIUpdate(); return false; } await ensureCurrentUser(); const block = getBoostBlockReasonSync(target); if (block.blocked) { if (block.fallbackToNormalReply) { scheduleUIUpdate(); return true; } showToast(block.title || block.text || "当前无法使用 Boost", "error"); scheduleUIUpdate(); return false; } const label = qs(".d-button-label", primaryBtn); const oldLabel = label?.dataset.origLabel || label?.textContent || "回复"; state.sending = true; primaryBtn.disabled = true; if (label) label.textContent = "Boost 发送中..."; try { await postBoost(raw, target); markLocalBoostedTarget(target.topicId || getTopicId(), target.postId); const constraints = getBoostConstraintStateSync(target); if ( Number.isFinite(constraints.targetBoostCount) && constraints.targetBoostCount + 1 >= CONFIG.MAX_BOOSTS_PER_TARGET ) { markLocalTargetFull(target.topicId || getTopicId(), target.postId); } textarea.value = ""; textarea.dispatchEvent(new Event("input", { bubbles: true })); showToast( `已通过 Boost 发送到 #${target.postNumber}(post_id: ${target.postId})`, "success" ); state.lastClickedTarget = null; state.composerTarget = null; if (CONFIG.AUTO_CLOSE_COMPOSER_AFTER_SUCCESS) { await sleep(120); closeComposer(); } return false; } catch (err) { console.error(err); if (err?.code === "duplicate") { markLocalBoostedTarget(target.topicId || getTopicId(), target.postId); scheduleUIUpdate(); return false; } if (err?.code === "limit") { markLocalTargetFull(target.topicId || getTopicId(), target.postId); scheduleUIUpdate(); return false; } showToast(`Boost 发送失败:${err.message}`, "error"); return false; } finally { state.sending = false; primaryBtn.disabled = false; if (label) label.textContent = oldLabel; scheduleUIUpdate(); } } function primeReplyContext(target) { if (!target) return; if (target.mode === "topic") { void ensureTopicMeta(target.topicId || getTopicId()) .then(() => { if (state.lastClickedTarget?.mode === "topic") { state.lastClickedTarget = refreshTargetSync(state.lastClickedTarget); } scheduleUIUpdate(); }) .catch((err) => { log("primeReplyContext ensureTopicMeta 失败", err); }); } void ensureCurrentUser().then(() => scheduleUIUpdate()); } document.addEventListener( "click", (e) => { const el = asElement(e.target); if (!el) return; const submitBtn = el.closest("#reply-control .submit-panel button.create"); if (submitBtn) { if (shouldInterceptSubmitAsBoostSync()) { void sendBoostFromComposer(e, { force: false }); } return; } const replyLauncher = el.closest( [ ".post-action-menu__reply.reply.create", ".timeline-footer-controls .reply-to-post", "#topic-footer-buttons .topic-footer-button.create" ].join(",") ); if (replyLauncher && !replyLauncher.closest("#reply-control")) { const target = resolveTargetFromReplyLauncher(replyLauncher); if (target) { state.lastClickedTarget = target; state.composerTarget = target; primeReplyContext(target); } scheduleComposerSync(); } }, true ); document.addEventListener( "keydown", (e) => { const el = asElement(e.target); if (!el) return; const inComposer = el.closest("#reply-control textarea.d-editor-input"); if (!inComposer) return; if ((e.ctrlKey || e.metaKey) && e.key === "Enter") { if (shouldInterceptSubmitAsBoostSync()) { void sendBoostFromComposer(e, { force: false }); } } }, true ); document.addEventListener( "input", (e) => { const el = asElement(e.target); if (el?.matches?.("#reply-control textarea.d-editor-input")) { scheduleUIUpdate(); } }, true ); document.addEventListener( "focusin", (e) => { const el = asElement(e.target); if (el?.matches?.("#reply-control textarea.d-editor-input")) { scheduleUIUpdate(); } }, true ); new MutationObserver(() => { clearTimeout(state.moTimer); state.moTimer = setTimeout(() => { syncTopicChange(); state.domVersion++; resetBoostScanCache(); if (getComposer()) { parseComposerTarget(); const target = getEffectiveTargetSync(); if (target?.mode === "topic") { primeReplyContext(target); } } else { state.composerTarget = null; } scheduleUIUpdate(); }, 80); }).observe(document.body, { childList: true, subtree: true, }); window.__ldBoostShortReply = { config: CONFIG, state, getEffectiveTargetSync, ensureTopicMeta, ensureCurrentUser, parseComposerTarget, refreshTargetSync, getBoostConstraintStateSync, getBoostBlockReasonSync, scanTopicBoostsSync, shouldInterceptSubmitAsBoostSync, }; void ensureCurrentUser(); scheduleUIUpdate(); })();

网友解答:
--【壹】--:

是一个话题的所有帖子 还是一个帖子单独的限制啊


--【贰】--:

是的 就是问佬的脚本有没有考虑这个问题特殊处理来着


--【叁】--:

每一条回复 我给你找找始皇发那个 等下哈

image910×272 35.1 KB


--【肆】--:

有点意思,佬可以上传一个到 greasyfork 或 GitHub 地址吗,方便更新


--【伍】--:

image300×168 52.7 KB


--【陆】--:

是的
PixPin_2026-04-08_15-59-09513×273 21.2 KB


--【柒】--:

不是字符,是一个主题只允许50个boost回应,50个人的小火箭


--【捌】--:

如果判定不够字数,回复会自动发boost吗??


--【玖】--:

我还不知道这回事
等我去改一改 看一下情况


--【拾】--:

我看了下 本身 超过50个 就没有小火箭的按钮了 所以就会是普通回复的样子


--【拾壹】--:
greasyfork.org

linux.do 自动切换 boost/回复方式

≤16字自动走 Boost;17~19字保留普通回复并提示;20字及以上正常回复。支持同帖单用户仅1次 Boost、每帖 50 Boost 上限检测,并在不可 Boost 时自动切换普通回复提示。


--【拾贰】--:

佬请问下,bosst限制50个,超过50个时候这个脚本回复的boost会怎么处理


--【拾叁】--:

大召唤术,召唤哈雷表情包,佬友们快来助我


--【拾肆】--:

好东西,mm再也不担心boost吞我评论了


--【拾伍】--:


来了来了


--【拾陆】--:

既有存在50个的话 应该是不能再发送了的(不会显示小火箭)
所以如果直接打在接口上的话 那肯定是报错的