// ==UserScript== // @name 知乎 · 盐选与软广屏蔽 // @namespace https://local/userscript/zhihu-hide-yanxuan // @version 3.2.0 // @description 屏蔽知乎推荐流里的盐选内容与软广。网络层直接过滤接口 JSON(不渲染不闪烁),页面层兜底;官方广告直接隐藏,疑似软广按多信号打分折叠,可逐条复核、一键信任作者。另在屏幕右缘提供圆形按钮,一键触发知乎文章底部的「收起」。 // @author WorkBuddy // @match *://www.zhihu.com/* // @match *://m.zhihu.com/* // @run-at document-start // @grant none // @noframes // @license Apache-2.0 // ==/UserScript== (function () { 'use strict'; /* ============================================================ * 0. 常量与状态 * ============================================================ */ const NS = 'zhyx'; const ID_PILL = NS + '-pill'; const ID_PANEL = NS + '-panel'; const ID_CBTN = NS + '-cbtn'; // 屏幕右缘的圆形按钮:一键触发知乎的「收起」 const CLS_HIDE = NS + '-hidden'; // 盐选 / 官方广告:直接消失 const CLS_FOLD = NS + '-folded'; // 疑似软广:折叠(可复核后展开) const KEY_ENABLED = NS + ':enabled'; const KEY_STATS = NS + ':stats'; const KEY_TRUST = NS + ':trust'; // 已信任的作者 url_token const KEY_ALLOW = NS + ':allow'; // 手动放行的内容指纹 const KEY_SENS = NS + ':sens'; const LOG = '[净化]'; /* ---- 0.1 安全存储(隐私模式 / file:// 下 localStorage 会抛异常) ---- */ const store = (function () { const probe = NS + ':probe'; let ok = true; try { localStorage.setItem(probe, '1'); localStorage.removeItem(probe); } catch (e) { ok = false; } const mem = {}; return { get: function (k) { try { return ok ? localStorage.getItem(k) : (k in mem ? mem[k] : null); } catch (e) { return k in mem ? mem[k] : null; } }, set: function (k, v) { try { if (ok) { localStorage.setItem(k, v); return; } } catch (e) { /* 降级到内存 */ } mem[k] = v; } }; })(); function readJSON(key, fallback) { try { const v = JSON.parse(store.get(key) || 'null'); return v === null || v === undefined ? fallback : v; } catch (e) { return fallback; } } /* ---- 0.2 状态 ---- */ let enabled = store.get(KEY_ENABLED) !== '0'; let sessionNet = 0; let sessionDom = 0; const stats = (function () { const s = readJSON(KEY_STATS, {}); return { yxTotal: s.yxTotal || 0, yxNet: s.yxNet || 0, yxDom: s.yxDom || 0, adTotal: s.adTotal || 0, adHide: s.adHide || 0, adFold: s.adFold || 0, recent: Array.isArray(s.recent) ? s.recent : [] }; })(); const trust = readJSON(KEY_TRUST, []); // 作者 token 白名单 const allow = readJSON(KEY_ALLOW, []); // 内容指纹白名单 const SENS = { loose: { fold: 70, hide: 120, label: '宽松' }, normal: { fold: 55, hide: 100, label: '标准' }, strict: { fold: 40, hide: 70, label: '严格' } }; let sens = SENS[store.get(KEY_SENS)] ? store.get(KEY_SENS) : 'normal'; let saveTimer = null; function saveStats() { if (saveTimer) return; saveTimer = setTimeout(function () { saveTimer = null; store.set(KEY_STATS, JSON.stringify({ yxTotal: stats.yxTotal, yxNet: stats.yxNet, yxDom: stats.yxDom, adTotal: stats.adTotal, adHide: stats.adHide, adFold: stats.adFold, recent: stats.recent.slice(0, 16) })); store.set(KEY_TRUST, JSON.stringify(trust.slice(-200))); store.set(KEY_ALLOW, JSON.stringify(allow.slice(-200))); }, 400); } let foldSeq = 0; let renderTimer = null; function scheduleRender() { if (renderTimer) return; renderTimer = setTimeout(function () { renderTimer = null; renderUI(); }, 120); } function bump(channel, kind, title, reasons) { if (kind === 'yx') { stats.yxTotal++; if (channel === 'net') { stats.yxNet++; sessionNet++; } else { stats.yxDom++; sessionDom++; } } else { stats.adTotal++; if (channel === 'net') stats.adHide++; else if (reasons && reasons.hide) stats.adHide++; else stats.adFold++; } const entry = { t: String(title || '').slice(0, 60), c: channel, k: kind }; if (reasons) { entry.r = (reasons.text || []).join(' · ').slice(0, 90); if (reasons.id) entry.id = reasons.id; if (reasons.key) entry.key = reasons.key; if (reasons.author) entry.a = reasons.author; // 作者 token,供"信任该作者"用 } stats.recent.unshift(entry); stats.recent = stats.recent.slice(0, 16); saveStats(); scheduleRender(); } function hash(s) { let h = 5381; for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) | 0; return (h >>> 0).toString(36); } /* ============================================================ * 1. 识别规则 * ============================================================ */ /* ---- 1.1 盐选 ---- */ const YX_URL_RES = [ /\/xen\/market\//i, /\/market\/paid_column\//i, /\/paid_column\//i, /\/remix\/(paid_column|albums)\//i, /\boia\.zhihu\.com/i ]; const YX_TAG_TEXTS = ['盐选专栏', '盐言故事', '盐选故事', '盐选会员', '盐选']; const YX_TEXT_RE = /盐选|盐言故事/; const YX_TYPE_RE = /paid_column|paid_content|yanxuan|盐选/i; const KEY_LABEL_RE = /^(badge|badge_text|label|label_text|tag|tags|type|card_type|content_type|kind|category|action_text|subtitle|sub_title|source_name|filter_type)$/i; /* ---- 1.2 官方广告(有硬标识) ---- */ // 卡片角标里出现这些词 = 官方广告,直接定案(不再看其他信号) const AD_HARD_TAG_TEXTS = ['广告', '商业推广', '含商业推广', '赞助内容', '知+']; // 接口里判定官方广告:这些字段一旦命中就是板上钉钉 const AD_BOOL_KEYS = ['is_ad', 'is_promotion', 'is_commercial', 'is_advertisement']; const AD_TYPE_VALUES = ['ad', 'advertisement', 'promotion', 'commercial']; /* ---- 1.3 带货链接 ---- */ const COMMERCE_URL_RES = [ /link\.zhihu\.com/i, // 知乎站外跳转中转域名,带货/广告专用 /click\.zhihu\.com/i, /item\.jd\.com/i, /item\.taobao\.com/i, /detail\.tmall\.com/i, /youzan\.com/i, /union-click/i ]; /* ---- 1.4 引流话术 ---- */ const CTA_PATTERNS = [ /私信我|私我|私聊我|加我微信|加微信|加V信|加\s?V\b|微信号/, /关注公众号|公众号[::]|搜索公众号|同名公众号/, /评论区?扣\s?[11一]|扣1|回复\d+领取/, /免费领取|领取资料|领取教程|资料包|送你一份|免单/, /点击下方|下方链接|下方卡片|戳链接|链接在(评论|主页)/, /限时优惠|粉丝专属|内部价|专属优惠|优惠码|折扣码|报我名字/, /扫码|长按识别|扫一扫|二维码/, /一对一(咨询|指导)|免费咨询|免费试听|预约体验|加群/, /橱窗|小黄车|同款链接|某宝|某东/ ]; /* ---- 1.5 作者名 / 卡片 --- */ const AUTHOR_NAME_RES = /(测评|评测|精选|甄选|课堂|教育|培训|咨询|留学|考研|考证|健身|护肤|数码|房产|保险|理财|财税|搬家|装修|口腔|整形)$/; const ITEM_SELECTORS = [ '[data-za-detail-view-path-module="FeedItem"]', '.TopstoryItem', '.List-item', 'div[role="listitem"]', '.Card' ]; /* ============================================================ * 2. 通用判定工具 * ============================================================ */ // 卡片内是否存在"文本恰好等于某标签"的短叶子节点 function shortTagHit(card, texts, maxLen) { const limit = maxLen || 6; const leaves = card.querySelectorAll('span, div, a, em, i, b'); for (let i = 0; i < leaves.length; i++) { const node = leaves[i]; if (node.children.length) continue; const t = (node.textContent || '').trim(); if (!t || t.length > limit) continue; if (texts.indexOf(t) !== -1) return t; } return ''; } function hitByUrl(card, res) { const links = card.querySelectorAll('a[href]'); for (let i = 0; i < links.length; i++) { const href = links[i].getAttribute('href') || ''; if (!href) continue; for (let j = 0; j < res.length; j++) if (res[j].test(href)) return true; } return false; } function getAuthor(card) { const a = card.querySelector('a[href*="/people/"]'); if (!a) return { token: '', name: '' }; const m = (a.getAttribute('href') || '').match(/\/people\/([^/?#]+)/); return { token: m ? m[1] : '', name: (a.textContent || '').trim().slice(0, 30) }; } function isOwnUI(el) { return !!(el && el.closest && el.closest('#' + ID_PANEL + ', #' + ID_PILL + ', #' + ID_CBTN)); } function looksLikeArticleBody(el) { if (el.querySelector('h1')) return true; if (el.matches && el.matches('.QuestionHeader, .Post-Header')) return true; return false; } /* ============================================================ * 3. 网络层 * ============================================================ */ const API_RES = [ /\/api\/v3\/feed\//i, /\/api\/v4\/feed\//i, /\/api\/v3\/moments/i, /\/api\/v4\/moments/i, /topstory\/recommend/i ]; function isApiWatched(url) { if (!url || typeof url !== 'string') return false; return API_RES.some(function (re) { return re.test(url); }); } function looksLikeFeedItem(node) { if (!node || typeof node !== 'object' || Array.isArray(node)) return false; if ('target' in node || 'card_type' in node) return true; if (typeof node.type === 'string' && (node.id || node.data)) return true; return false; } /** * 盐选判定:只看 URL 值 + 标签类/类型类字段的值, * 绝不扫标题正文 —— 否则标题里带"盐选"的正常内容会被误删。 */ function isYanxuanItem(item) { let hit = false; const walk = function (v, key, depth) { if (hit || depth > 6 || v === null || v === undefined) return; if (typeof v === 'string') { if (/^https?:\/\//i.test(v)) { if (YX_URL_RES.some(function (re) { return re.test(v); })) hit = true; return; } if (key && KEY_LABEL_RE.test(key)) { if (YX_TEXT_RE.test(v) || YX_TYPE_RE.test(v)) hit = true; } return; } if (Array.isArray(v)) { for (let i = 0; i < v.length; i++) walk(v[i], key, depth + 1); return; } if (typeof v === 'object') { for (const k in v) if (Object.prototype.hasOwnProperty.call(v, k)) walk(v[k], k, depth + 1); } }; try { walk(item, '', 0); } catch (e) { return false; } return hit; } /** 官方广告判定:布尔位 / 类型位命中即可,不猜。 */ function isOfficialAd(item) { try { for (let i = 0; i < AD_BOOL_KEYS.length; i++) { if (item[AD_BOOL_KEYS[i]] === true) return true; const t = item.target; if (t && t[AD_BOOL_KEYS[i]] === true) return true; } const typeKeys = ['type', 'card_type', 'feed_card_type', 'content_type']; for (let i = 0; i < typeKeys.length; i++) { const a = item[typeKeys[i]]; const b = item.target && item.target[typeKeys[i]]; if (typeof a === 'string' && AD_TYPE_VALUES.indexOf(a.toLowerCase()) !== -1) return true; if (typeof b === 'string' && AD_TYPE_VALUES.indexOf(b.toLowerCase()) !== -1) return true; } return false; } catch (e) { return false; } } function pickTitle(item) { try { const c = [ item.title, item.target && item.target.title, item.object && item.object.title, item.target && item.target.question && item.target.question.title, item.feed_title ]; for (let i = 0; i < c.length; i++) { if (typeof c[i] === 'string' && c[i].trim()) return c[i].trim(); } } catch (e) { /* ignore */ } return ''; } /** 过滤响应体;返回 null 表示没动过。任何异常都放行原数据。 */ function filterPayload(text) { if (!text || typeof text !== 'string') return null; if (text.indexOf('盐选') === -1 && text.indexOf('盐言故事') === -1 && text.indexOf('/xen/market/') === -1 && text.indexOf('paid_column') === -1 && text.indexOf('"is_ad"') === -1 && text.indexOf('"ad_info"') === -1) { return null; } let json; try { json = JSON.parse(text); } catch (e) { return null; } let removed = 0; const walk = function (node, depth) { if (!node || typeof node !== 'object' || depth > 8) return; if (Array.isArray(node)) { for (let i = node.length - 1; i >= 0; i--) { const item = node[i]; let kind = null; if (looksLikeFeedItem(item)) { if (isYanxuanItem(item)) kind = 'yx'; else if (isOfficialAd(item)) kind = 'ad'; } if (kind) { bump('net', kind, pickTitle(item)); node.splice(i, 1); removed++; } else { walk(item, depth + 1); } } return; } for (const k in node) { if (Object.prototype.hasOwnProperty.call(node, k)) walk(node[k], depth + 1); } }; try { walk(json, 0); } catch (e) { return null; } return removed > 0 ? { data: json, removed: removed } : null; } function installFetchHook() { if (typeof window.fetch !== 'function') return; const native = window.fetch; window.fetch = function (input, init) { const url = (typeof input === 'string') ? input : (input && input.url) || ''; const p = native.apply(this, arguments); if (!enabled || !isApiWatched(url)) return p; return p.then(function (res) { try { if (!res || !res.ok) return res; return res.clone().text().then(function (text) { const out = filterPayload(text); if (!out) return res; const headers = new Headers(res.headers); headers.delete('content-length'); console.log(LOG, '网络层拦截', out.removed, '条'); return new Response(JSON.stringify(out.data), { status: res.status, statusText: res.statusText, headers: headers }); }).catch(function () { return res; }); } catch (e) { return res; } }); }; } function installXHRHook() { const proto = XMLHttpRequest.prototype; const rawOpen = proto.open; const rawSend = proto.send; if (typeof rawOpen !== 'function' || typeof rawSend !== 'function') return; proto.open = function () { this[NS + '_url'] = arguments[1]; this[NS + '_done'] = false; return rawOpen.apply(this, arguments); }; proto.send = function () { const xhr = this; // 钩子必须在 open 阶段挂上,才能比业务代码的 onload 更早拿到响应 if (enabled && isApiWatched(xhr[NS + '_url'] || '')) { xhr.addEventListener('readystatechange', function () { if (xhr.readyState !== 4 || xhr[NS + '_done']) return; xhr[NS + '_done'] = true; try { const type = xhr.responseType; if (type && type !== 'text' && type !== 'json') return; const raw = (type === 'json') ? JSON.stringify(xhr.response) : xhr.responseText; if (!raw) return; const out = filterPayload(raw); if (!out) return; const text = JSON.stringify(out.data); Object.defineProperty(xhr, 'responseText', { configurable: true, get: function () { return text; } }); Object.defineProperty(xhr, 'response', { configurable: true, get: function () { return type === 'json' ? out.data : text; } }); console.log(LOG, '网络层拦截(XHR)', out.removed, '条'); } catch (e) { /* 放行 */ } }); } return rawSend.apply(this, arguments); }; } /* ============================================================ * 4. 软广打分 * ============================================================ */ /** * 多信号累加打分。没有单一规则能定义"软广", * 所以每条规则只贡献一点权重,总分过阈值才动作。 */ function scoreAd(card, text) { let score = 0; const why = []; // 1) 官方广告标识 —— 这一条就足以定案 const hardTag = shortTagHit(card, AD_HARD_TAG_TEXTS, 6); if (hardTag) { score += 100; why.push('带「' + hardTag + '」标识'); } // 2) 带货 / 站外跳转链接 if (hitByUrl(card, COMMERCE_URL_RES)) { score += 45; why.push('含站外带货链接'); } // 3) 引流话术:命中越多分越高 const ctaHits = []; for (let i = 0; i < CTA_PATTERNS.length; i++) { const m = text.match(CTA_PATTERNS[i]); if (m) ctaHits.push(m[0]); } if (ctaHits.length) { score += ctaHits.length * 14; why.push('引流话术「' + ctaHits.slice(0, 2).join('、') + '」'); } // 4) 机构号角标 if (shortTagHit(card, ['机构号'], 5)) { score += 18; why.push('机构号'); } // 5) 作者名像营销号 const author = getAuthor(card); if (author.name && AUTHOR_NAME_RES.test(author.name)) { score += 10; why.push('作者名像营销号'); } // 6) 正文反复出现价格 const prices = text.match(/[¥¥]\s?\d+/g); if (prices && prices.length >= 2) { score += 12; why.push('多次出现价格'); } return { score: score, why: why, author: author }; } /* ============================================================ * 5. 页面层执行 * ============================================================ */ function cardKey(card) { const a = getAuthor(card); const text = (card.textContent || '').replace(/\s+/g, ' ').trim(); return hash(a.token + '|' + text.slice(0, 40)); } function hideYanxuan(card) { if (card.offsetHeight > window.innerHeight * 1.2) return false; if (looksLikeArticleBody(card)) return false; if (card.classList.contains(CLS_HIDE)) return false; card.classList.add(CLS_HIDE); const h = card.querySelector('h1, h2, h3'); bump('dom', 'yx', h ? h.textContent.trim() : ''); return true; } function foldAd(card, res, hide) { if (card.offsetHeight > window.innerHeight * 1.2) return false; if (looksLikeArticleBody(card)) return false; if (card.classList.contains(CLS_FOLD) || card.classList.contains(CLS_HIDE)) return false; const key = cardKey(card); if (allow.indexOf(key) !== -1) return false; const id = 'f' + (++foldSeq); card.dataset[NS + 'Id'] = id; card.dataset[NS + 'Key'] = key; card.classList.add(hide ? CLS_HIDE : CLS_FOLD); const h = card.querySelector('h1, h2, h3'); const au = getAuthor(card); bump('dom', 'ad', h ? h.textContent.trim() : '', { text: res.why, hide: hide, id: id, key: key, author: au.token }); return true; } function scan() { if (!enabled) return; const cand = new Set(); // 5.1 从盐选链接反查卡片 const anchors = document.querySelectorAll( 'a[href*="xen/market"], a[href*="paid_column"], a[href*="remix"], a[href*="oia.zhihu.com"]' ); for (let i = 0; i < anchors.length; i++) { const c = findCard(anchors[i]); if (c) cand.add(c); } // 5.2 所有信息流卡片 const cards = document.querySelectorAll(ITEM_SELECTORS.join(',')); for (let i = 0; i < cards.length; i++) cand.add(cards[i]); // 5.3 只保留"最内层"候选,避免把整个信息流容器当成一张卡片 const list = []; cand.forEach(function (c) { if (!isOwnUI(c)) list.push(c); }); const inner = list.filter(function (c) { for (let i = 0; i < list.length; i++) { if (list[i] !== c && c.contains(list[i])) return false; } return true; }); const th = SENS[sens]; for (let i = 0; i < inner.length; i++) { const card = inner[i]; // 盐选 / 官方广告角标 → 直接隐藏 if (!card.classList.contains(CLS_HIDE) && (hitByUrl(card, YX_URL_RES) || shortTagHit(card, YX_TAG_TEXTS, 6))) { hideYanxuan(card); continue; } if (!card.classList.contains(CLS_HIDE) && shortTagHit(card, AD_HARD_TAG_TEXTS, 6)) { foldAd(card, { score: 100, why: ['带官方广告标识'] }, true); continue; } if (card.classList.contains(CLS_HIDE) || card.classList.contains(CLS_FOLD)) continue; // 软广打分 const author = getAuthor(card); if (author.token && trust.indexOf(author.token) !== -1) continue; const text = (card.textContent || '').replace(/\s+/g, ' '); if (text.length < 12) continue; const res = scoreAd(card, text); if (res.score >= th.hide) foldAd(card, res, true); else if (res.score >= th.fold) foldAd(card, res, false); } } function findCard(node) { if (isOwnUI(node)) return null; let el = node; while (el && el !== document.body && el.parentElement) { for (let i = 0; i < ITEM_SELECTORS.length; i++) { if (el.matches && el.matches(ITEM_SELECTORS[i])) return el; } el = el.parentElement; } el = node; while (el && el !== document.body && el.parentElement) { const parent = el.parentElement; let sameTag = 0; for (let i = 0; i < parent.children.length; i++) { if (parent.children[i].tagName === el.tagName) sameTag++; } const h = el.offsetHeight; if (sameTag >= 2 && h > 60 && h < window.innerHeight * 1.2) return el; el = parent; } return null; } function releaseAll() { const els = document.querySelectorAll('.' + CLS_HIDE + ', .' + CLS_FOLD); for (let i = 0; i < els.length; i++) els[i].classList.remove(CLS_HIDE, CLS_FOLD); } /* ============================================================ * 5.5 一键收起 * * 知乎文章 / 回答的「收起」按钮长在正文末尾,长文要滚很久才够得着。 * 这里不去改知乎的 DOM,只是替你去点它 —— 找到按钮、派发一次真实点击。 * ============================================================ */ const COLLAPSE_TEXTS = ['收起', '收起全文']; const CLICKABLE_SEL = 'button, [role="button"], .Button, .Button--plain'; const CLICK_SEQ = ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']; function isVisible(el) { try { const r = el.getBoundingClientRect(); if (r.width < 4 || r.height < 4) return false; let n = el, d = 0; while (n && n.nodeType === 1 && d++ < 12) { const cs = window.getComputedStyle ? window.getComputedStyle(n) : null; if (cs) { const op = cs.opacity; if (cs.display === 'none' || cs.visibility === 'hidden' || (op !== '' && op !== undefined && Number(op) === 0)) { return false; } } n = n.parentElement; } return true; } catch (e) { return false; } } /** * 找知乎的「收起」按钮。 * 取离视口中心最近的那个 —— 多回答页面上,你正看着哪个,收起的就是哪个。 */ function findCollapseTarget() { let all; try { all = document.querySelectorAll(CLICKABLE_SEL); } catch (e) { return null; } const vc = (window.innerHeight || 800) / 2; let best = null, bestD = Infinity; for (let i = 0; i < all.length; i++) { const el = all[i]; if (isOwnUI(el)) continue; const t = (el.textContent || '').trim(); if (COLLAPSE_TEXTS.indexOf(t) === -1) continue; if (!isVisible(el)) continue; let d; try { const r = el.getBoundingClientRect(); d = Math.abs(r.top + r.height / 2 - vc); } catch (e) { d = 1e9; } if (d < bestD) { bestD = d; best = el; } } return best; } /** * 派发完整的指针 / 鼠标事件序列。 * 知乎用 React 合成事件,只调 el.click() 有时不生效, * 所以按 pointerdown → mousedown → pointerup → mouseup → click 走一遍。 */ function simulateClick(el) { let r = { left: 0, top: 0, width: 0, height: 0 }; try { r = el.getBoundingClientRect(); } catch (e) { /* 用默认值 */ } const base = { bubbles: true, cancelable: true, view: window, clientX: Math.round(r.left + r.width / 2), clientY: Math.round(r.top + r.height / 2) }; for (let i = 0; i < CLICK_SEQ.length; i++) { try { el.dispatchEvent(new MouseEvent(CLICK_SEQ[i], base)); } catch (e) { /* 单个事件失败不影响后续 */ } } } let cbtnShown = false; /** 页面上有「收起」才显示圆按钮;面板打开时让位给面板。 */ function updateCollapseButton() { const btn = document.getElementById(ID_CBTN); if (!btn) return; const panel = document.getElementById(ID_PANEL); const hit = !(panel && panel.classList.contains('zhyx-open')) && !!findCollapseTarget(); if (hit === cbtnShown) return; cbtnShown = hit; btn.classList.toggle('zhyx-show', hit); } /* ============================================================ * 6. 界面 * ============================================================ */ const CSS = ` .${CLS_HIDE} { display: none !important; } .${CLS_FOLD} { display: none !important; } #${ID_PILL} { /* bottom 从 22px 上调到 108px,避开知乎页面底部自带的「收起」按钮 */ position: fixed; right: 18px; bottom: 108px; z-index: 2147483647; display: flex; align-items: center; gap: 7px; padding: 8px 14px; font: 500 12px/1 -apple-system, "Microsoft YaHei", "PingFang SC", sans-serif; color: #0C447C; background: rgba(255,255,255,.62); -webkit-backdrop-filter: blur(14px) saturate(180%); backdrop-filter: blur(14px) saturate(180%); border: 1px solid rgba(255,255,255,.75); border-radius: 999px; box-shadow: 0 2px 12px rgba(0,0,0,.10); cursor: pointer; user-select: none; transition: transform .15s ease, opacity .2s ease; } #${ID_PILL}:hover { transform: translateY(-2px); } #${ID_PILL} .zhyx-dot { width: 6px; height: 6px; border-radius: 50%; background: #1D9E75; flex: 0 0 auto; } #${ID_PILL}.zhyx-off .zhyx-dot { background: #B4B2A9; } #${ID_PILL}.zhyx-off { color: #5F5E5A; } #${ID_PILL}.zhyx-hidden-pill { opacity: 0; pointer-events: none; } /* ---- 屏幕右缘圆形按钮:一键触发知乎文章底部的「收起」 ---- */ #${ID_CBTN} { position: fixed; right: 16px; top: 50%; width: 44px; height: 44px; margin-top: -22px; z-index: 2147483645; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border-radius: 50%; color: #0C447C; background: rgba(255,255,255,.62); -webkit-backdrop-filter: blur(14px) saturate(180%); backdrop-filter: blur(14px) saturate(180%); border: 1px solid rgba(255,255,255,.75); box-shadow: 0 2px 12px rgba(0,0,0,.10); cursor: pointer; user-select: none; opacity: 0; pointer-events: none; transform: translateX(10px); transition: opacity .2s ease, transform .2s ease, background .15s ease; } #${ID_CBTN}.zhyx-show { opacity: 1; pointer-events: auto; transform: translateX(0); } #${ID_CBTN}.zhyx-show:hover { transform: translateX(-3px); background: rgba(255,255,255,.88); } #${ID_CBTN}.zhyx-tap { transform: translateX(-3px) scale(.88); } #${ID_CBTN} svg { width: 20px; height: 20px; display: block; } #${ID_PANEL} { position: fixed; top: 0; right: 0; bottom: 0; width: 336px; z-index: 2147483646; box-sizing: border-box; padding: 22px 20px 28px; overflow-y: auto; font: 400 13px/1.6 -apple-system, "Microsoft YaHei", "PingFang SC", sans-serif; color: #1F2937; background: rgba(255,255,255,.58); -webkit-backdrop-filter: blur(22px) saturate(180%); backdrop-filter: blur(22px) saturate(180%); border-left: 1px solid rgba(255,255,255,.7); box-shadow: -10px 0 40px rgba(0,0,0,.13); transform: translateX(102%); transition: transform .3s cubic-bezier(.22,.61,.36,1); } #${ID_PANEL}.zhyx-open { transform: translateX(0); } #${ID_PANEL} .zhyx-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; } #${ID_PANEL} .zhyx-title { font-size: 14px; font-weight: 500; color: #0C447C; } #${ID_PANEL} .zhyx-close { width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 15px; color: #5F5E5A; cursor: pointer; background: rgba(255,255,255,.55); border: 1px solid rgba(0,0,0,.06); } #${ID_PANEL} .zhyx-close:hover { background: #FFFFFF; } #${ID_PANEL} .zhyx-sec { font-size: 11px; color: #5F5E5A; letter-spacing: .06em; margin: 18px 0 8px 2px; } #${ID_PANEL} .zhyx-sec:first-of-type { margin-top: 0; } #${ID_PANEL} .zhyx-hero { padding: 16px; border-radius: 14px; margin-bottom: 10px; background: rgba(255,255,255,.55); border: 1px solid rgba(255,255,255,.8); } #${ID_PANEL} .zhyx-num { font-size: 32px; font-weight: 500; line-height: 1.1; color: #185FA5; font-variant-numeric: tabular-nums; } #${ID_PANEL} .zhyx-hero-ad .zhyx-num { color: #993C1D; } #${ID_PANEL} .zhyx-unit { font-size: 12px; color: #5F5E5A; margin-top: 3px; } #${ID_PANEL} .zhyx-split { display: flex; gap: 10px; margin-bottom: 4px; } #${ID_PANEL} .zhyx-cell { flex: 1; padding: 10px 12px; border-radius: 11px; background: rgba(255,255,255,.5); border: 1px solid rgba(255,255,255,.75); } #${ID_PANEL} .zhyx-cell b { display: block; font-size: 16px; font-weight: 500; color: #1F2937; font-variant-numeric: tabular-nums; } #${ID_PANEL} .zhyx-cell span { font-size: 11px; color: #5F5E5A; } #${ID_PANEL} .zhyx-row { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border-radius: 11px; margin-top: 10px; background: rgba(255,255,255,.5); border: 1px solid rgba(255,255,255,.75); } #${ID_PANEL} .zhyx-seg { display: flex; gap: 2px; padding: 2px; border-radius: 8px; background: rgba(0,0,0,.05); } #${ID_PANEL} .zhyx-seg i { font-style: normal; font-size: 11px; padding: 3px 9px; border-radius: 6px; cursor: pointer; color: #5F5E5A; } #${ID_PANEL} .zhyx-seg i.zhyx-sel { background: #FFFFFF; color: #0C447C; font-weight: 500; } #${ID_PANEL} .zhyx-switch { width: 40px; height: 22px; border-radius: 999px; background: #B4B2A9; position: relative; cursor: pointer; transition: background .2s ease; flex: 0 0 auto; } #${ID_PANEL} .zhyx-switch.zhyx-on { background: #1D9E75; } #${ID_PANEL} .zhyx-switch i { position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: #FFFFFF; transition: transform .2s ease; } #${ID_PANEL} .zhyx-switch.zhyx-on i { transform: translateX(18px); } #${ID_PANEL} .zhyx-recent { border-radius: 11px; overflow: hidden; min-height: 56px; background: rgba(255,255,255,.45); border: 1px solid rgba(255,255,255,.7); } #${ID_PANEL} .zhyx-item { display: flex; align-items: flex-start; gap: 7px; padding: 9px 12px; font-size: 12px; line-height: 1.5; border-bottom: 1px solid rgba(0,0,0,.05); } #${ID_PANEL} .zhyx-item:last-child { border-bottom: none; } #${ID_PANEL} .zhyx-item em { font-style: normal; flex: 0 0 auto; padding: 1px 5px; border-radius: 4px; font-size: 10px; background: #E6F1FB; color: #185FA5; } #${ID_PANEL} .zhyx-item em.zhyx-k-ad { background: #FAECE7; color: #993C1D; } #${ID_PANEL} .zhyx-item p { margin: 0; flex: 1; min-width: 0; color: #374151; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } #${ID_PANEL} .zhyx-why { display: block; color: #9CA3AF; font-size: 11px; margin-top: 2px; } #${ID_PANEL} .zhyx-acts { display: flex; gap: 6px; flex: 0 0 auto; } #${ID_PANEL} .zhyx-act { font-size: 10px; padding: 2px 6px; border-radius: 5px; cursor: pointer; color: #5F5E5A; background: rgba(255,255,255,.7); border: 1px solid rgba(0,0,0,.07); } #${ID_PANEL} .zhyx-act:hover { color: #0C447C; border-color: #B5D4F4; } #${ID_PANEL} .zhyx-empty { padding: 18px 12px; text-align: center; font-size: 12px; color: #9CA3AF; } #${ID_PANEL} .zhyx-foot { display: flex; align-items: center; justify-content: space-between; margin-top: 12px; font-size: 11px; color: #5F5E5A; } #${ID_PANEL} .zhyx-reset { cursor: pointer; padding: 5px 10px; border-radius: 7px; border: 1px solid rgba(0,0,0,.07); background: rgba(255,255,255,.5); } #${ID_PANEL} .zhyx-reset:hover { color: #A32D2D; } @media (prefers-color-scheme: dark) { #${ID_PANEL} { color: #E8E8E6; background: rgba(28,28,30,.55); border-left-color: rgba(255,255,255,.12); } #${ID_PANEL} .zhyx-title { color: #85B7EB; } #${ID_PANEL} .zhyx-hero, #${ID_PANEL} .zhyx-cell, #${ID_PANEL} .zhyx-row, #${ID_PANEL} .zhyx-recent { background: rgba(255,255,255,.07); border-color: rgba(255,255,255,.1); } #${ID_PANEL} .zhyx-num { color: #85B7EB; } #${ID_PANEL} .zhyx-hero-ad .zhyx-num { color: #F0997B; } #${ID_PANEL} .zhyx-cell b { color: #E8E8E6; } #${ID_PANEL} .zhyx-unit, #${ID_PANEL} .zhyx-cell span, #${ID_PANEL} .zhyx-sec { color: #A0A0A0; } #${ID_PANEL} .zhyx-item p { color: #D0D0CE; } #${ID_PANEL} .zhyx-item { border-bottom-color: rgba(255,255,255,.07); } #${ID_PANEL} .zhyx-close, #${ID_PANEL} .zhyx-act, #${ID_PANEL} .zhyx-reset { background: rgba(255,255,255,.1); color: #C0C0C0; border-color: rgba(255,255,255,.12); } #${ID_PANEL} .zhyx-seg { background: rgba(255,255,255,.08); } #${ID_PANEL} .zhyx-seg i.zhyx-sel { background: rgba(255,255,255,.16); color: #B5D4F4; } #${ID_PILL} { color: #B5D4F4; background: rgba(28,28,30,.6); border-color: rgba(255,255,255,.14); } #${ID_CBTN} { color: #B5D4F4; background: rgba(28,28,30,.6); border-color: rgba(255,255,255,.14); } #${ID_CBTN}.zhyx-show:hover { background: rgba(50,50,54,.85); } } `; function injectCSS() { const head = document.head || document.documentElement; if (!head || document.getElementById(NS + '-style')) return; const s = document.createElement('style'); s.id = NS + '-style'; s.textContent = CSS; head.appendChild(s); } function escapeHtml(s) { return String(s).replace(/[&<>"']/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; }); } let lastSignature = ''; function renderUI() { const pill = document.getElementById(ID_PILL); const panel = document.getElementById(ID_PANEL); if (!pill || !panel) return; const sig = [ enabled, sens, stats.yxTotal, stats.yxNet, stats.yxDom, stats.adTotal, stats.adHide, stats.adFold, stats.recent.length, trust.length ].join('|'); if (sig === lastSignature) return; lastSignature = sig; pill.classList.toggle('zhyx-off', !enabled); pill.querySelector('.zhyx-label').textContent = '已屏蔽 ' + (stats.yxTotal + stats.adTotal) + (enabled ? '' : ' · 暂停'); panel.querySelector('#zhyx-num').textContent = String(stats.yxTotal); panel.querySelector('#zhyx-net').textContent = String(stats.yxNet); panel.querySelector('#zhyx-dom').textContent = String(stats.yxDom); panel.querySelector('#zhyx-adnum').textContent = String(stats.adTotal); panel.querySelector('#zhyx-adhide').textContent = String(stats.adHide); panel.querySelector('#zhyx-adfold').textContent = String(stats.adFold); panel.querySelector('.zhyx-switch').classList.toggle('zhyx-on', enabled); panel.querySelector('#zhyx-trustcount').textContent = '已信任 ' + trust.length + ' 位作者'; const segs = panel.querySelectorAll('#zhyx-sens i'); for (let i = 0; i < segs.length; i++) { segs[i].classList.toggle('zhyx-sel', segs[i].getAttribute('data-v') === sens); } const box = panel.querySelector('#zhyx-recent'); if (!stats.recent.length) { box.innerHTML = '
' + escapeHtml(r.t || '(无标题)') + (r.r ? '' + escapeHtml(r.r) + '' : '') + '
' + acts + '