// ==UserScript== // @name 花瓣网助手:自动屏蔽付费素材 + 快速下载原图 // @name:en Huaban Helper: Block Paid Assets + Quick Original Downloader // @namespace https://github.com/liuyunixs // @id huaban-helper-combined@workbuddy // @version 2.3.3 // @description 在花瓣网自动隐藏付费/VIP素材,同时支持 Alt+左键点击任意网页图片下载最高清原图(按网页名称命名、文件头识别真实格式)。花瓣网走官方接口取签名原图与发布内容命名。右键为浏览器原生菜单。 // @description:en Block paid/VIP assets on Huaban, plus Alt+left-click any web image to download the original (highest-resolution) file with web-name naming and magic-byte format detection. Huaban uses the official API for signed originals and raw_text naming. Right-click is the native menu. // @author liuyunixs / WorkBuddy // @license MIT // @icon data:image/png;base64,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 // @homepageURL https://github.com/liuyunixs // @match https://huaban.com/* // @match *://*/* // @match file:///* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_xmlhttpRequest // @grant GM_addStyle // @connect huaban.com // @connect huabanimg.com // @connect self // @connect * // @run-at document-idle // ==/UserScript== (function () { 'use strict'; /* ============ 全局配置 ============ */ const CONFIG = { // —— 屏蔽付费素材(仅花瓣网生效)—— showMarker: true, // 被标记/显示时添加「付费」角标 // —— 快速下载原图 —— MAX_NAME_LEN: 80, SRCSET_USE_LARGEST: true, SHOW_TOAST: true, FETCH_TIMEOUT: 9000, AUTO_FIX_NETERR: true, // 自动处理花瓣网「网络错误」弹窗(自动重试 / 关闭) }; // 付费素材「隐藏 / 显示」开关:true = 隐藏;false = 显示(粉色背景标记) // 状态持久化,可通过篡改猴菜单随时切换 const HIDE_PAID_KEY = 'hb_helper_hide_paid'; let hidePaid = GM_getValue(HIDE_PAID_KEY, true); // 自动屏蔽功能「总开关」:true = 开启(扫描并标记/隐藏付费素材);false = 关闭(彻底不处理) // 状态持久化,可通过篡改猴菜单「花瓣网自动屏蔽付费素材:开/关」切换 const BLOCKER_ENABLED_KEY = 'hb_helper_blocker_enabled'; let blockerEnabled = GM_getValue(BLOCKER_ENABLED_KEY, true); /* ============================================================ * 第一部分:花瓣网自动屏蔽付费素材(仅花瓣网生效) * ============================================================ */ let _blockHiddenCount = 0; // 付费/版权素材的角标文本关键词(含 EPS、版权素材等) // 注:PAID_TEXT 用于步骤 1 / 6 的特定 class 元素;更通用的「版权素材 / EPS 类型」漏判 // 由 isPaidCard() 第 8 步的兜底扫描(遍历短文本元素)覆盖。 const PAID_TEXT = /版权素材|版权|付费|VIP|购买|商用|授权|EPS|买断|素材库|会员素材|企业素材|商用授权|可商用/i; function isPaidCard(cardEl) { // 1. 检查内部付费标识 div(文本可能是「ID:12345」,也可能是「版权素材」「EPS」「VIP 素材」等) const paidMarker = cardEl.querySelector('.__2p__B98x, .QzLweiwl'); if (paidMarker) { const text = paidMarker.textContent || ''; if (/ID[::]\s*\d+/i.test(text) || PAID_TEXT.test(text)) return true; } // 2. 检查是否有 data-material-id 属性(官方付费素材容器) if (cardEl.hasAttribute && cardEl.hasAttribute('data-material-id')) { return true; } // 3. 检查卡片内的链接是否包含 buyVip 关键词 const link = cardEl.querySelector('a[href*="buyVip"]'); if (link) return true; // 4. 检查 KKIUywzb 容器是否存在 data-material-type 且不为空(EPS/版权等素材类型都在此) const inner = cardEl.querySelector('.KKIUywzb'); if (inner) { const matType = inner.getAttribute('data-material-type'); if (matType && matType.trim() !== '') return true; } // 5. 任意带 vip 关键字的 class 即判定为付费(不依赖文案) const vipEl = cardEl.querySelector('[class*="vip" i]'); if (vipEl) return true; // 6. 版权 / EPS 角标:badge/tag/mark/label/flag 等小角标元素,文本命中付费关键词(限制长度避免误伤正文) const badges = cardEl.querySelectorAll( '[class*="badge" i], [class*="tag" i], [class*="mark" i], [class*="label" i], [class*="flag" i], [class*="ribbon" i]' ); for (const el of badges) { const t = (el.textContent || '').trim(); if (t.length > 0 && t.length <= 40 && PAID_TEXT.test(t)) return true; } // 7. data 属性显式标记 if (cardEl.getAttribute) { const dp = cardEl.getAttribute('data-paid') || cardEl.getAttribute('data-vip') || cardEl.getAttribute('data-is-paid'); if (dp === 'true' || dp === '1') return true; } // 8. 兜底扫描(不依赖易变混淆 class):遍历卡片内所有短文本元素, // 专门解决「版权素材 / EPS 类型」素材未落入上述特定 class 时的漏判。 // —— HIGH_CONF 用子串匹配最无歧义的关键词(EPS / 版权素材 几乎不会误伤普通文案); // —— 其余关键词仍用边界正则,避免正常描述里出现「授权 / 商用」等词被误判。 const HIGH_CONF = /版权素材|版权图片|EPS|VIP素材|会员素材|企业素材|素材库/i; const STRICT_PAID = /(^|[\s·•\-:|:、])(版权|付费|VIP|VIP素材|购买|买断|会员素材|企业素材|商用授权|可商用素材|版权素材|版权图片)([\s·•\-:|:、]|$)/i; const STRONG_PAID = /^(VIP|VIP素材|付费|版权|版权素材|版权图片|购买|买断|会员素材|企业素材|素材库|EPS)$/i; const allNodes = cardEl.querySelectorAll('*'); for (const el of allNodes) { const t = (el.textContent || '').trim(); if (t.length === 0 || t.length > 40) continue; // 跳过聚合了大段文本的容器,只看「小标签 / 叶子」级别文本,避免误伤长正文 if (el.children.length > 0 && t.length > 20) continue; if (HIGH_CONF.test(t) || STRONG_PAID.test(t) || STRICT_PAID.test(t)) return true; } // 9. 购买 / 会员入口:卡片内存在指向 /vip、/material、buy、pay 的链接, // 或按钮/链接文本命中付费词 → 强判定为付费/版权素材(不依赖混淆 class)。 const buyLink = cardEl.querySelector('a[href*="/vip" i], a[href*="/material" i], a[href*="buy" i], a[href*="pay" i], button'); if (buyLink) { const lt = (buyLink.textContent || '').trim(); if (lt.length > 0 && lt.length <= 40 && /购买|开通会员|立即购买|会员|付费|版权|授权|商用|素材库/i.test(lt)) return true; } return false; } function applyPaidStyle(cardEl) { if (hidePaid) { cardEl.classList.add('hb-paid-hidden'); cardEl.classList.remove('hb-paid-pinkbg'); } else { cardEl.classList.remove('hb-paid-hidden'); cardEl.classList.add('hb-paid-pinkbg'); } if (CONFIG.showMarker) cardEl.classList.add('hb-paid-marker'); else cardEl.classList.remove('hb-paid-marker'); } // 把任意命中的元素归一化为「卡片容器」: // 优先用稳定信号 (1) data-pin-id 属性;(2) 指向 /pins/{id} 的 的卡片 wrapper。 // 【v2.3.0】花瓣网已迁移到 Next.js,旧混淆 class(__7b58EDIh 等)全部失效, // 改用「路由锚点 + data 属性」定位,不再依赖任何易变混淆 class。 function normalizeCard(el) { if (!el || el.nodeType !== 1) return null; // 1. 向上找 data-pin-id(最稳定) let p = el, depth = 0; while (p && p !== document.body && depth < 12) { if (p.hasAttribute && p.hasAttribute('data-pin-id')) return p; p = p.parentElement; depth++; } // 2. 指向 /pins/ 的 :直接以 自身作为卡片根(最安全,避免误选整页容器)。 // 若日后发现 外层才是卡片盒子(隐藏后留空白),可改回向上收敛一层。 if (el.tagName === 'A' && /^\/pins\//.test(el.getAttribute('href') || '')) { return el; } return el; } function processCard(rawEl) { if (!blockerEnabled) return; const cardEl = normalizeCard(rawEl); if (!cardEl || cardEl.dataset.huabanPaidProcessed) return; cardEl.dataset.huabanPaidProcessed = 'true'; if (isPaidCard(cardEl)) { _blockHiddenCount++; applyPaidStyle(cardEl); } } function scanAllCards() { if (!blockerEnabled) return; // 稳定锚点优先;旧混淆 class 仅作兼容保留(已失效,命中概率为 0 但无害)。 const selectors = [ 'a[href^="/pins/"]', '[data-pin-id]', '.__7b58EDIh', '.BR9Lhzqi.__7b58EDIh', ]; const done = new Set(); selectors.forEach((sel) => { document.querySelectorAll(sel).forEach((el) => { if (done.has(el)) return; done.add(el); processCard(el); }); }); if (_blockHiddenCount > 0) { console.log(`[花瓣屏蔽] 已隐藏 ${_blockHiddenCount} 个付费素材`); } } function initBlockerObserver() { const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type !== 'childList') continue; for (const node of mutation.addedNodes) { if (node.nodeType !== 1) continue; if (node.matches && ( node.matches('.__7b58EDIh') || node.matches('[data-pin-id]') || node.matches('.BR9Lhzqi') )) { processCard(node); } const innerCards = node.querySelectorAll && node.querySelectorAll('.__7b58EDIh, [data-pin-id], .BR9Lhzqi'); if (innerCards) { innerCards.forEach(processCard); } } } }); observer.observe(document.body, { childList: true, subtree: true }); } function injectBlockerStyles() { GM_addStyle(` .hb-paid-hidden { display: none !important; } .hb-paid-pinkbg { position: relative !important; outline: 2px solid #ff8fb3 !important; outline-offset: -2px; } .hb-paid-pinkbg::after { content: ""; position: absolute; inset: 0; background: rgba(255, 105, 180, 0.35) !important; pointer-events: none; z-index: 5; } .hb-paid-marker::before { content: "付费"; position: absolute; top: 4px; left: 4px; z-index: 9999; padding: 2px 6px; background: rgba(255, 59, 48, 0.9) !important; color: #fff !important; font-size: 11px; border-radius: 3px; pointer-events: none; font-weight: bold; } .hb-top-ad-hidden { display: none !important; } `); } /* ---------- 顶部活动横幅广告拦截 ---------- */ function isTopAdKeyword(text) { // 【v2.2.6】泛化关键词匹配,覆盖花瓣网常见活动文案变体,避免换季/换活动即漏判。 // 拆分为「高置信子串」和「组合边界」两类,减少误伤正文。 const HIGH = /清爽入夏|宠粉有礼|素材下载低至|领专属价|0\.06元|会员特惠|下载低至|限时福利|立即领取|抢福利|查看更多优惠|新客专享|首充|开通会员|升级会员|清凉一夏|夏日福利|暑期限定|开学季|双十一|618|年终盛典|新春特惠|限时折扣|端午特惠|国庆特惠|中秋福利|元旦有礼|新人专享|新人礼|0元购|立减|满减|大礼包|专属福利|瓜分|抽奖|红包雨|狂欢盛典|会员日|年卡|季卡|包月|包年|续费|领券|优惠券|新用户专享|1元购|限时秒杀|福利社|充值中心|开通VIP|升级VIP/i; if (HIGH.test(text)) return true; // 组合匹配:短文本中出现「会员/福利/优惠/折扣/活动/领取/专享/特惠」+ 数字/价格/按钮信号 if (/(会员|VIP|福利|优惠|折扣|活动|领取|专享|特惠|限时).{0,10}(\d|元|折|券|价|购|送|礼|抢|领|点击|立即)/i.test(text)) return true; if (/(\d|元|折|券|价).{0,10}(会员|VIP|福利|优惠|折扣|领取|专享|特惠|限时)/i.test(text)) return true; // 纯促销动作短语(≤25字) if (text.length <= 25 && /(领|抢|拿|享|购|买|开通|升级).{0,10}(福利|优惠|折扣|特权|会员|VIP|好礼|礼包|红包|券|价)/i.test(text)) return true; return false; } // 判断候选容器是不是真正的站点导航/头部(含搜索框、logo、nav),避免误伤 function isRealSiteHeader(el) { const cls = (el.className || '').toString(); if (/header|navbar|topnav|toolbar|logo|global-nav|main-nav/i.test(cls)) return true; if (el.querySelector && el.querySelector('input[type="search"], input[type="text"], nav, [class*="logo" i], [class*="search" i]')) return true; return false; } // 站点头部 / 导航 / 搜索栏:受最高保护,绝不被隐藏或错误位移。 // 仅检查元素自身特征及其是否直接包含搜索输入,不去向上遍历祖先, // 以免误伤「头部内的促销条」这类本应隐藏的元素。 function isProtectedNav(el) { if (!el || el.nodeType !== 1) return false; const cls = (el.className || '').toString().toLowerCase(); const id = (el.id || '').toString().toLowerCase(); if (el.tagName === 'HEADER' || el.tagName === 'NAV') return true; if (el.getAttribute && el.getAttribute('role') === 'search') return true; if (/(^|[\s-])(header|head|navbar|nav|topnav|topbar|toolbar|logo|global-nav|main-nav|search|searchbar|search-bar|top-bar|app-bar|appbar|menu)([\s-]|$)/i.test(cls + ' ' + id)) return true; if (el.querySelector && el.querySelector('input, form, [role="search"]')) return true; return false; } // 向上遍历祖先:若元素处于站点头部 / 导航 / 搜索栏子树内(搜索下拉、联想面板等 // 通常是 header 的后代,部分 SPA 会 portal 到 body),一律视为受保护区,绝不隐藏。 function isInsideProtectedNav(el) { let cur = el; let depth = 0; while (cur && cur !== document.body && depth < 16) { const cls = (cur.className || '').toString().toLowerCase(); const id = (cur.id || '').toString().toLowerCase(); const tag = cur.tagName; if (tag === 'HEADER' || tag === 'NAV') return true; if (cur.getAttribute && cur.getAttribute('role') === 'search') return true; if (/(header|head|navbar|nav|topnav|topbar|toolbar|logo|global-nav|main-nav|search|searchbar|search-bar|top-bar|app-bar|appbar|menu)/.test(cls + ' ' + id)) return true; if (cur.querySelector && cur.querySelector('input, form, [role="search"]')) return true; cur = cur.parentElement; depth++; } return false; } // 搜索 / 弹层类 UI:role=dialog、aria-modal,或 class/id 含 search/overlay/modal/popup 等。 // 这类元素即便满足「顶部全宽矮条」几何特征,也绝不是广告,必须跳过。 function isSearchOverlay(el) { if (!el || el.nodeType !== 1) return false; const cls = (el.className || '').toString().toLowerCase(); const id = (el.id || '').toString().toLowerCase(); if (el.getAttribute && (el.getAttribute('role') === 'dialog' || el.getAttribute('aria-modal') === 'true')) return true; // 必须用「分隔符/词边界」匹配,否则 huaban 这类普通 class(如 "modalOpenWidth")会被子串 // "modal" 误判成弹层,反而把真正的顶部广告条放过(v2.2.8 修复)。真实弹层类名多为 // modal / ant-modal / modal-content 等形式,均带边界,不受影响。 if (/(^|[\s_-])(search|overlay|modal|popup|dropdown|suggest|panel|tooltip|combobox|listbox)([\s_-]|$)/i.test(cls + ' ' + id)) return true; return false; } // 从命中文案的元素向上,找到最可能「整条广告横幅」的容器: // 优先 class 显式 banner/advert 等;否则用几何特征(紧贴视口顶部 + 全宽 + 矮条 + 块级)兜底。 // 【v2.2.7】放宽:即便广告条位于 header/nav 容器内,只要它自身明显是广告(含 banner 类 / 促销文案 / // 顶部全宽矮条),仍返回并隐藏;仅排除「导航/搜索/logo 自身」,避免误伤站点头部。 function findTopAdContainer(el) { if (!el) return null; let cur = el; let depth = 0; while (cur && cur !== document.body && depth < 16) { const cls = (cur.className || '').toString(); const isBannerCls = /banner|promo|promotion|notice|activity|advert|popup|float|topbar|top-bar|announcement|suspension|notify|activity-bar/i.test(cls); // 1. 显式广告/banner 类(混淆 class 名里偶尔仍会带这些词)—— 即便处于 header 内也返回 if (isBannerCls) { if (!isRealSiteHeader(cur) && !isSearchOverlay(cur)) return cur; } // 2. 几何特征兜底:紧贴视口顶部、全宽、矮、块级 → 典型的顶部横幅(排除导航/搜索/logo 自身) try { const rect = cur.getBoundingClientRect(); const style = window.getComputedStyle(cur); const inTop = rect.top >= -16 && rect.top < 80; const fullW = rect.width >= window.innerWidth * 0.55; const short = rect.height > 0 && rect.height < 180; const blockLike = style.display !== 'inline' && style.display !== 'inline-block' && style.display !== 'none'; if (inTop && fullW && short && blockLike && !isRealSiteHeader(cur) && !isProtectedNav(cur) && !isSearchOverlay(cur)) { return cur; } } catch (e) {} cur = cur.parentElement; depth++; } return null; } // 判断元素是否「明显是广告条」(用于豁免导航保护):含 banner/promo 类,或文本命中促销关键词 function isBannerLikeElement(el) { if (!el) return false; const cls = (el.className || '').toString(); if (/banner|promo|promotion|advert|activity|活动|notice|announcement|suspension|topbar|top-bar|notify|welfare|coupon/i.test(cls)) return true; const t = (el.textContent || '').replace(/\s+/g, ' ').trim(); if (/(会员|福利|优惠|活动|领|券|VIP|专享|特惠|限时|宠粉|清爽|抽奖|红包|0\.06|专属价|下载低至)/i.test(t)) return true; return false; } // 通用隐藏:从命中文案/元素向上找「整条广告横幅」容器并隐藏(幂等,靠 data-huaban-top-ad 去重) // 【v2.2.7】放宽豁免:广告条即便处于 header/nav 容器内,只要自身明显是广告仍隐藏; // 仅当 target 自身是导航/搜索/logo(且非广告条)时才放弃。 function markAd(el, reason) { if (!el) return false; const target = findTopAdContainer(el); if (!target || target.dataset.huabanTopAd === 'true') return false; if (isProtectedNav(target) || isRealSiteHeader(target) || isSearchOverlay(target)) { if (!isBannerLikeElement(target)) return false; } if (isInsideProtectedNav(target) && !isBannerLikeElement(target)) return false; target.dataset.huabanTopAd = 'true'; target.classList.add('hb-top-ad-hidden'); console.log('[花瓣屏蔽] 已拦截顶部广告:', (reason || ''), '→', (target.className || target.tagName)); fixLayoutAfterTopAdHidden(); return true; } // 可疑「活动/会员/优惠券」入口:href 命中促销类关键词(仍受 markAd 的「顶部几何门控」约束,不会误伤页脚链接) const SUS_HREF = /(\/|\.)(activity|promo|promotion|vip|member|coupon|welfare|event|act\b|ad\b|banner|topic|marketing|sales|recharge|pay\/|upgrade)/i; // 可疑广告图片:src / data-src / data-original 文件名含 banner/act/promo 等 const SUS_IMG = /banner|act|promo|topad|\/ad\/|activity|welfare|coupon/i; function blockTopAds() { if (!blockerEnabled) return; let blocked = 0; const checkText = (text, el) => { if (!text || !isTopAdKeyword(text)) return; if (markAd(el, text.trim().slice(0, 50))) blocked++; }; // 1. 文本节点(按钮/文案文字都算) const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false); let node; while ((node = walker.nextNode())) { checkText(node.textContent, node.parentElement); } // 2. 图片 alt / 链接 title / aria-label(广告可能为纯图片形式) document.querySelectorAll('img[alt], a[title], [aria-label]').forEach((el) => { const t = el.getAttribute('alt') || el.getAttribute('title') || el.getAttribute('aria-label') || ''; checkText(t, el); }); // 3. 可疑活动链接:href 命中促销类关键词(如 /activity、/vip、/coupon) document.querySelectorAll('a[href]').forEach((el) => { const h = el.getAttribute('href') || ''; if (SUS_HREF.test(h) && markAd(el, '可疑活动链接:' + h.slice(0, 40))) blocked++; }); // 4. 可疑广告图片:src/data-src/data-original 文件名含 banner/act/promo 等 document.querySelectorAll('img[src], img[data-src], img[data-original]').forEach((el) => { const s = el.getAttribute('src') || el.getAttribute('data-src') || el.getAttribute('data-original') || ''; if (SUS_IMG.test(s) && markAd(el, '可疑广告图片:' + s.slice(0, 40))) blocked++; }); if (blocked > 0) { console.log(`[花瓣屏蔽] 本次拦截 ${blocked} 个顶部广告`); } else { console.log('[花瓣屏蔽] 关键词未命中,启用几何兜底'); } // 几何兜底始终作为补充扫描:兜住「文案在背景图/iconfont 里」或关键词漏判的顶部广告条。 // 内部有去重与导航保护,不会误伤站点头部。即便关键词已命中,也补扫一次以防漏网。 blockTopAdsGeometric(); // 顶部全宽矮条型 iframe 兜底:第三方广告常挂在 iframe 内(跨域读不到内容), // 只要它处于页面最顶、全宽、矮,直接隐藏元素本身。 try { const vw = window.innerWidth; document.querySelectorAll('iframe').forEach((f) => { if (f.dataset.huabanTopAd === 'true') return; const rect = f.getBoundingClientRect(); if (rect.top >= -16 && rect.top < 100 && rect.width >= vw * 0.5 && rect.height > 0 && rect.height < 220) { f.dataset.huabanTopAd = 'true'; f.classList.add('hb-top-ad-hidden'); console.log('[花瓣屏蔽] 隐藏顶部 iframe 广告:', f.src || f.getAttribute('data-src') || '(无src)'); fixLayoutAfterTopAdHidden(); } }); } catch (e) {} } // 几何兜底:仅针对页面最顶部(top<90)的全宽(≥55%视口)矮条(12~180px), // 排除含输入框/nav/logo 以及常驻 UI 文案(发现/关注/首页/搜索…)的容器,隐藏剩下的。 function blockTopAdsGeometric() { if (!blockerEnabled) return; const vw = window.innerWidth; const UI_TEXT = /发现|关注|首页|搜索|你可能感兴趣|登录|注册|消息|通知|我的|创作|客服|下载客户端|分类|热门|推荐|最新|投稿|上传|APP|扫码|客户端/i; // 【v2.2.6】降低几何兜底门槛:不再强制要求 PROMO_SIGNAL 文本命中, // 只要满足「顶部全宽矮条 + 非导航/搜索」且含链接/按钮/图片,即视为候选广告。 // 纯图片广告(无文本关键词)也能被拦截。 const PROMO_SIGNAL = /(banner|promo|advert|活动|福利|优惠|领|券|会员|VIP|专享|特惠|限时|宠粉|清爽|抽奖|红包|notice|announcement|suspension|welfare|coupon)/i; // 【v2.3.1】放宽并「全量」识别:不再只取最顶一条,而是隐藏所有顶部广告状条; // 判定窗口放宽(top<130、宽≥50%、高≤200),覆盖略靠下/非整屏宽的促销条。 const hidden = []; document.querySelectorAll('*').forEach((el) => { if (el.dataset.huabanTopAd === 'true') return; if (isSearchOverlay(el)) return; const rect = el.getBoundingClientRect(); if (rect.top < -16 || rect.top > 130) return; if (rect.width < vw * 0.5) return; if (rect.height < 12 || rect.height > 200) return; if (el.querySelector && el.querySelector('input, nav, [class*="logo" i]')) return; const cls = (el.className || '').toString(); const isBannerCls = /(banner|promo|promotion|advert|activity|活动|notice|announcement|suspension|topbar|top-bar|notify|welfare|coupon)/i.test(cls); const t = (el.textContent || '').replace(/\s+/g, ' ').trim(); const isBannerLike = isBannerCls || PROMO_SIGNAL.test(t); // 广告条即便处于 header 内也允许;仅排除「导航/搜索/logo 自身」(且非广告条) if ((isProtectedNav(el) || isRealSiteHeader(el)) && !isBannerLike) return; if (!isBannerLike && isInsideProtectedNav(el)) return; if (UI_TEXT.test(t)) return; // 是否「明显是广告」:promo class 或促销文案 const hasPromoClass = /(banner|promo|promotion|advert|activity|活动|notice|announcement|suspension|topbar|top-bar|notify|welfare|coupon)/i.test((el.className || '').toString() + ' ' + (el.id || '').toString()); const hasPromoText = PROMO_SIGNAL.test(t); const clearlyAd = hasPromoClass || hasPromoText; // 是否有「可点击/图片/背景图」内容(纯图广告、按钮式促销入口) const hasLinkOrBtn = el.querySelector && (el.querySelector('a, button') || el.tagName === 'A'); const hasImg = el.querySelector && el.querySelector('img'); let hasBgImage = false; try { const bi = window.getComputedStyle(el).backgroundImage; hasBgImage = !!(bi && bi !== 'none' && bi !== ''); } catch (e) {} const hasContent = hasLinkOrBtn || hasImg || hasBgImage; // 仅当「明显是广告」或「有内容的顶部条」才隐藏,避免误伤普通容器 if (!clearlyAd && !hasContent) return; hidden.push({ el, top: rect.top }); }); // 按 top 排序逐个隐藏(idempotent:已标记的会跳过;导航保护在上方已过滤) hidden.sort((a, b) => a.top - b.top); hidden.forEach(({ el }) => { if (el.dataset.huabanTopAd === 'true') return; el.dataset.huabanTopAd = 'true'; el.classList.add('hb-top-ad-hidden'); console.log('[花瓣屏蔽] 几何兜底拦截顶部条:', (el.className || el.tagName), 'top=' + Math.round(el.getBoundingClientRect().top), 'text=', (el.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 40)); fixLayoutAfterTopAdHidden(); }); } // banner 隐藏后,修正下方 fixed/sticky 顶部元素以及 body/html 的 padding-top/margin-top, // 避免出现「广告没了,但导航栏还停在原来被挤下来的位置」的断层空白。 function fixLayoutAfterTopAdHidden() { // 收集当前可见的 fixed/sticky 元素(排除已被隐藏的广告条自身) const fixedEls = []; document.querySelectorAll('*').forEach((el) => { if (el.classList.contains('hb-top-ad-hidden')) return; const style = window.getComputedStyle(el); if ((style.position === 'fixed' || style.position === 'sticky') && el.offsetHeight > 0) { fixedEls.push(el); } }); if (!fixedEls.length) return; // 最小 top 即 banner 把下面元素挤下来的距离 let minTop = Infinity; fixedEls.forEach((el) => { const rect = el.getBoundingClientRect(); if (rect.top >= -2 && rect.top < minTop) minTop = rect.top; }); if (!isFinite(minTop) || minTop <= 2) return; const offset = minTop; // 把所有基于顶部定位的 fixed/sticky 元素整体上移 offset。 // 关键:每个元素「仅首次」保存它真实的原始 top,之后一律基于该原始值计算, // 这样无论 markAd 被调用多少次(多次扫描 / 多个广告),位移都只生效一次, // 不会因为重复叠加把头部 / 搜索栏推到屏幕外。 fixedEls.forEach((el) => { const cs = window.getComputedStyle(el); if (el.dataset.huabanOriginalTop === undefined) { const orig = (cs.top === 'auto' || cs.top === '') ? 'auto' : String(parseFloat(cs.top)); el.dataset.huabanOriginalTop = orig; } const origTop = parseFloat(el.dataset.huabanOriginalTop); if (isNaN(origTop)) return; // top:auto 等不处理 el.style.setProperty('top', (origTop - offset) + 'px', 'important'); el.dataset.huabanLayoutFixed = 'true'; console.log('[花瓣屏蔽] 修正顶部布局,上移:', (el.className || el.tagName), 'by', offset); }); // 同步修正 body / html 的 padding-top / margin-top(同样基于首次保存值,幂等) [document.body, document.documentElement].forEach((el) => { if (!el) return; ['paddingTop', 'marginTop'].forEach((prop) => { const cssProp = prop === 'paddingTop' ? 'padding-top' : 'margin-top'; const val = parseFloat(window.getComputedStyle(el)[prop]) || 0; if (val > offset - 2 && val < offset + 2) { const origKey = prop === 'paddingTop' ? 'huabanOriginalPadding' : 'huabanOriginalMargin'; if (el.dataset[origKey] === undefined) el.dataset[origKey] = window.getComputedStyle(el)[prop] || '0px'; el.style.setProperty(cssProp, '0px', 'important'); el.dataset.huabanLayoutFixed = 'true'; } }); }); } // 关闭总开关时恢复被修正过的 top / padding / margin function restoreTopLayout() { document.querySelectorAll('[data-huaban-layout-fixed]').forEach((el) => { if (el.dataset.huabanOriginalTop !== undefined) { if (el.dataset.huabanOriginalTop) el.style.setProperty('top', el.dataset.huabanOriginalTop, 'important'); else el.style.removeProperty('top'); delete el.dataset.huabanOriginalTop; } if (el === document.body || el === document.documentElement) { if (el.dataset.huabanOriginalPadding !== undefined) { if (el.dataset.huabanOriginalPadding) el.style.setProperty('padding-top', el.dataset.huabanOriginalPadding, 'important'); else el.style.removeProperty('padding-top'); delete el.dataset.huabanOriginalPadding; } if (el.dataset.huabanOriginalMargin !== undefined) { if (el.dataset.huabanOriginalMargin) el.style.setProperty('margin-top', el.dataset.huabanOriginalMargin, 'important'); else el.style.removeProperty('margin-top'); delete el.dataset.huabanOriginalMargin; } } delete el.dataset.huabanLayoutFixed; }); } // 诊断:把页面顶部区域所有「全宽条」候选 + 含广告关键词的元素 + iframe 打印到控制台, // 用于广告没拦住时定位真实 DOM 结构。菜单「🌸 顶部广告诊断」触发。 function diagnoseTopAds() { const vw = window.innerWidth, vh = window.innerHeight; console.log('%c[花瓣屏蔽-诊断] 视口 ' + vw + 'x' + vh + ' scrollY=' + Math.round(window.scrollY), 'color:#0a8'); // 1. iframe 列表(广告可能在 iframe 内,普通扫描扫不到) const iframes = Array.from(document.querySelectorAll('iframe')).map((f) => f.src || f.getAttribute('data-src') || '(无src)'); if (iframes.length) console.log('%c[花瓣屏蔽-诊断] 页面 iframe(' + iframes.length + '):', 'color:#a50', iframes); else console.log('[花瓣屏蔽-诊断] 页面无 iframe'); // 2. 全宽候选(放宽:top∈[-50,vh], width≥35%vw, height∈[8,400])附带背景色(彩色促销条线索) const cands = []; document.querySelectorAll('*').forEach((el) => { const rect = el.getBoundingClientRect(); if (rect.top < -50 || rect.top > vh) return; if (rect.width < vw * 0.35) return; if (rect.height < 8 || rect.height > 400) return; const t = (el.textContent || '').replace(/\s+/g, ' ').trim(); if (t.length < 2) return; let bg = ''; try { bg = window.getComputedStyle(el).backgroundColor || ''; } catch (e) {} cands.push({ tag: el.tagName.toLowerCase(), cls: (el.className || '').toString().slice(0, 70), top: Math.round(rect.top), w: Math.round(rect.width), h: Math.round(rect.height), text: t.slice(0, 60), bg }); }); cands.sort((a, b) => a.top - b.top || b.w - a.w); console.log('%c[花瓣屏蔽-诊断] 全宽候选(前60,含背景色):', 'color:#0a8'); cands.slice(0, 60).forEach((c, i) => console.log(`#${i} <${c.tag}> top=${c.top} w=${c.w} h=${c.h} bg=${c.bg} cls="${c.cls}" text="${c.text}"`)); // 2b. 纯图片 / 彩色横幅候选(不依赖文本,专门兜住「文案在背景图/iconfont 里」的广告): // 放宽:top∈[-50,400]、width≥25%vw、height∈[8,240],有 background-image 或非透明背景色即打印, // 即便文本为空也列出,方便定位无文本广告条的真实 class / 结构。 const visualCands = []; document.querySelectorAll('*').forEach((el) => { const rect = el.getBoundingClientRect(); if (rect.top < -50 || rect.top > 400) return; if (rect.width < vw * 0.25) return; if (rect.height < 8 || rect.height > 240) return; let bg = '', bgImg = ''; try { const cs = window.getComputedStyle(el); bg = cs.backgroundColor || ''; bgImg = cs.backgroundImage || ''; } catch (e) {} const hasBgImg = !!(bgImg && bgImg !== 'none' && bgImg !== ''); const isColored = !!bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent' && bg !== 'rgb(0, 0, 0)' && bg !== ''; if (!hasBgImg && !isColored) return; const t = (el.textContent || '').replace(/\s+/g, ' ').trim(); visualCands.push({ tag: el.tagName.toLowerCase(), cls: (el.className || '').toString().slice(0, 70), top: Math.round(rect.top), w: Math.round(rect.width), h: Math.round(rect.height), bg, bgImg: hasBgImg, text: t.slice(0, 40) }); }); visualCands.sort((a, b) => a.top - b.top || b.w - a.w); console.log('%c[花瓣屏蔽-诊断] 纯图片/彩色横幅候选(前40,含背景图):', 'color:#a50'); visualCands.slice(0, 40).forEach((c, i) => console.log(`#${i} <${c.tag}> top=${c.top} w=${c.w} h=${c.h} bg=${c.bg} bgImg=${c.bgImg} cls="${c.cls}" text="${c.text}"`)); // 4. 可疑活动链接 / 广告图片(促销入口线索,即使无文案也能定位) document.querySelectorAll('a[href]').forEach((el) => { const h = el.getAttribute('href') || ''; if (SUS_HREF.test(h)) { const r = el.getBoundingClientRect(); console.log(` 可疑活动链接 top=${Math.round(r.top)} w=${Math.round(r.width)} href="${h.slice(0, 80)}" cls="${(el.className || '').toString().slice(0, 40)}"`); } }); document.querySelectorAll('img[src], img[data-src], img[data-original]').forEach((el) => { const s = el.getAttribute('src') || el.getAttribute('data-src') || el.getAttribute('data-original') || ''; if (SUS_IMG.test(s)) { const r = el.getBoundingClientRect(); console.log(` 可疑广告图片 top=${Math.round(r.top)} w=${Math.round(r.width)} src="${s.slice(0, 80)}"`); } }); // 3. 关键词强制打印:含广告文案的元素,无论尺寸(命中即广告线索) const KW = /清爽|宠粉|领专属价|专属价|会员|福利|0\.06|VIP|下载低至|开通会员|升级会员|限时|优惠券|首充|新客|活动|领取|抢|领券/i; let kwHit = 0; document.querySelectorAll('*').forEach((el) => { const t = (el.textContent || '').replace(/\s+/g, ' ').trim(); if (t.length < 2 || t.length > 200) return; if (!KW.test(t)) return; const rect = el.getBoundingClientRect(); const chain = []; let p = el, n = 0; while (p && p !== document.body && n < 4) { chain.push((p.tagName || '').toLowerCase() + '.' + (p.className || '').toString().slice(0, 18)); p = p.parentElement; n++; } console.log(`<${el.tagName.toLowerCase()}> top=${Math.round(rect.top)} w=${Math.round(rect.width)} cls="${(el.className || '').toString().slice(0, 50)}" text="${t.slice(0, 50)}" 父链=[${chain.join(' < ')}]`); kwHit++; }); if (!kwHit) console.log('[花瓣屏蔽-诊断] 当前 DOM 未找到任何广告关键词——广告可能:(a)已关闭/未加载;(b)在 iframe 内;(c)用图片/iconfont 无文本。请确认诊断时黄色广告条仍可见,并告诉我当前在哪个页面(首页/发现/详情)。'); return cands; } // 诊断付费素材卡片:按 /pins/ 路由定位所有 pin 卡片,打印真实 class 与文本, // 用于花瓣网改版后校准 isPaidCard 的关键词。菜单「🌸 付费素材诊断」触发。 function diagnosePaidCards() { const cards = Array.from(document.querySelectorAll('a[href^="/pins/"]')); console.log('%c[花瓣屏蔽-付费诊断] 找到 pin 卡片 ' + cards.length + ' 个(按 /pins/ 路由)', 'color:#0a8'); const KW = /版权素材|版权图片|EPS|VIP素材|会员素材|企业素材|素材库|商用授权|可商用|购买|授权|VIP|会员|开通会员|付费/i; let n = 0; cards.slice(0, 120).forEach((a, i) => { const t = (a.textContent || '').replace(/\s+/g, ' ').trim(); const cls = (a.className || '').toString().slice(0, 60); const href = (a.getAttribute('href') || '').slice(0, 40); if (i < 6) console.log(`#${i} (sample) href="${href}" cls="${cls}" text="${t.slice(0, 50)}"`); if (KW.test(t)) { console.log(`#${i} ★付费命中 text="${t.slice(0, 60)}" cls="${cls}" href="${href}"`); n++; } }); console.log(`[花瓣屏蔽-付费诊断] 含付费关键词卡片 ${n} / ${cards.length} 个`); if (n === 0) console.log('[花瓣屏蔽-付费诊断] 0 命中:说明付费卡片文本不在白名单,或标记改成了图标/新文案。请把「任一付费卡片」的 class 与文本(F12 检查元素复制)发我,我来校准。'); return cards.length; } let _topAdScanTimer = null; let _topAdPeriodicId = null; let _topAdPeriodicStart = 0; function scheduleTopAdScan(delayMs) { clearTimeout(_topAdScanTimer); _topAdScanTimer = setTimeout(() => { _topAdScanTimer = null; blockTopAds(); }, delayMs); } // 持久周期扫描:首 60 秒每 2 秒扫一次,之后每 8 秒扫一次, // 覆盖异步/延迟加载的顶部广告(CSS visibility 变化、懒加载、SPA 路由切换后动态渲染)。 function startPeriodicTopAdScan() { stopPeriodicTopAdScan(); _topAdPeriodicStart = Date.now(); _topAdPeriodicId = setInterval(() => { if (!blockerEnabled) return; const elapsed = Date.now() - _topAdPeriodicStart; if (elapsed < 60000) { // 首 60 秒高频 blockTopAds(); } else if (elapsed % 8000 < 2000) { // 之后每 8 秒扫一次(用 interval 对齐,每 2 秒检查一次是否需要扫) blockTopAds(); } }, 2000); } function stopPeriodicTopAdScan() { if (_topAdPeriodicId) { clearInterval(_topAdPeriodicId); _topAdPeriodicId = null; } } function initTopAdObserver() { let timer = null; const observer = new MutationObserver((mutations) => { // 1. childList:新增节点 → 可能包含广告 for (const m of mutations) { if (m.type === 'childList' && m.addedNodes.length > 0) { if (timer) return; timer = setTimeout(() => { timer = null; blockTopAds(); }, 300); return; } } // 2. attributes:已存在元素的 style/class 变化 → 广告从隐藏变为可见 for (const m of mutations) { if (m.type === 'attributes') { const el = m.target; if (!el || el.nodeType !== 1) continue; const attr = m.attributeName; if (attr === 'style' || attr === 'class') { try { const rect = el.getBoundingClientRect(); // 仅关注「页面顶部 + 全宽」的元素属性变化(避免全页扫描) if (rect.top >= -16 && rect.top < 200 && rect.width >= window.innerWidth * 0.4) { scheduleTopAdScan(200); return; } } catch (e) {} } } } }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); } /* ---------- 手动点选屏蔽(兜底,应对混淆 class / 文案取不到 / 动态加载) ---------- */ const MANUAL_AD_SIGS_KEY = 'hb_manual_ad_sigs'; let pickMode = false; function getManualAdSigs() { try { return GM_getValue(MANUAL_AD_SIGS_KEY, []) || []; } catch (e) { return []; } } function saveManualAdSig(sig) { if (!sig) return; const arr = getManualAdSigs(); if (!arr.includes(sig)) { arr.push(sig); GM_setValue(MANUAL_AD_SIGS_KEY, arr); } } function clearManualAdSigs() { try { GM_setValue(MANUAL_AD_SIGS_KEY, []); } catch (e) {} } // 套用已保存的「文本签名」:顶部区域里文本含该签名的元素直接隐藏(应对混淆 class 换名) function applyManualAdSignatures() { if (!blockerEnabled) return; const sigs = getManualAdSigs(); if (!sigs.length) return; const vw = window.innerWidth; document.querySelectorAll('*').forEach((el) => { if (el.dataset.huabanTopAd === 'true') return; if (isRealSiteHeader(el)) return; if (isProtectedNav(el)) return; if (isInsideProtectedNav(el)) return; const rect = el.getBoundingClientRect(); if (rect.top < -16 || rect.top > 160) return; if (rect.width < vw * 0.3) return; const t = (el.textContent || '').replace(/\s+/g, ' ').trim(); for (const sig of sigs) { if (sig && t.indexOf(sig) !== -1) { el.dataset.huabanTopAd = 'true'; el.classList.add('hb-top-ad-hidden'); console.log('[花瓣屏蔽] 手动签名命中并隐藏:', sig, '→', (el.className || el.tagName)); fixLayoutAfterTopAdHidden(); return; } } }); } function exitPickMode() { pickMode = false; if (document.body) document.body.style.cursor = ''; if (window.__hbPickOnClick) { document.removeEventListener('click', window.__hbPickOnClick, true); window.__hbPickOnClick = null; } if (window.__hbPickOnKey) { document.removeEventListener('keydown', window.__hbPickOnKey, true); window.__hbPickOnKey = null; } } // 进入点选模式:用户点击任意元素即隐藏其「够宽的容器」,含稳定文本则记住、下次自动隐藏 function enterPickMode() { if (!blockerEnabled) { toast('请先开启「花瓣网自动屏蔽付费素材」'); return; } if (pickMode) { exitPickMode(); toast('已退出点选模式'); return; } pickMode = true; if (document.body) document.body.style.cursor = 'crosshair'; toast('点选屏蔽:点击要隐藏的横幅/广告(按 Esc 取消)'); const onKey = (e) => { if (e.key === 'Escape') { exitPickMode(); toast('已取消点选'); } }; const onClick = (e) => { e.preventDefault(); e.stopPropagation(); exitPickMode(); let el = e.target; if (!el || !el.getBoundingClientRect) { return; } // 向上收敛到「够宽」的容器,避免只隐藏 tiny 文字节点 while (el && el.parentElement && el.tagName !== 'BODY' && el.getBoundingClientRect().width < window.innerWidth * 0.5) { el = el.parentElement; } if (!el || el === document.body) { toast('取消:所选范围过大'); return; } if (isRealSiteHeader(el)) { toast('这是站点导航,已跳过'); return; } const t = (el.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 60); el.dataset.huabanTopAd = 'true'; el.classList.add('hb-top-ad-hidden'); if (t.length >= 2) saveManualAdSig(t); fixLayoutAfterTopAdHidden(); toast('已隐藏所选元素' + (t ? '(已记住:' + t.slice(0, 20) + ')' : '(纯图片,仅本次会话)')); }; document.addEventListener('click', onClick, true); document.addEventListener('keydown', onKey, true); window.__hbPickOnClick = onClick; window.__hbPickOnKey = onKey; } function toggleHidePaid() { if (!blockerEnabled) { toast('请先开启「花瓣网自动屏蔽付费素材」'); return; } hidePaid = !hidePaid; GM_setValue(HIDE_PAID_KEY, hidePaid); // 重新套用样式到已处理过的卡片 document.querySelectorAll('[data-huaban-paid-processed]').forEach((el) => { if (isPaidCard(el)) applyPaidStyle(el); else el.classList.remove('hb-paid-hidden', 'hb-paid-pinkbg', 'hb-paid-marker'); }); scanAllCards(); toast('付费素材已' + (hidePaid ? '隐藏' : '显示(粉色背景)')); } // 自动屏蔽功能「总开关」:开 = 扫描并标记/隐藏;关 = 彻底清除标记并不再处理 function setBlockerEnabled(on) { blockerEnabled = on; GM_setValue(BLOCKER_ENABLED_KEY, on); if (on) { scanAllCards(); applyManualAdSignatures(); startPeriodicTopAdScan(); } else { stopPeriodicTopAdScan(); // 关闭:移除所有标记与隐藏,重置处理标记以便将来重新扫描 document.querySelectorAll('.hb-paid-hidden, .hb-paid-pinkbg, .hb-paid-marker, .hb-top-ad-hidden').forEach((el) => { el.classList.remove('hb-paid-hidden', 'hb-paid-pinkbg', 'hb-paid-marker', 'hb-top-ad-hidden'); }); document.querySelectorAll('[data-huaban-paid-processed]').forEach((el) => { delete el.dataset.huabanPaidProcessed; }); document.querySelectorAll('[data-huaban-top-ad]').forEach((el) => { delete el.dataset.huabanTopAd; }); _blockHiddenCount = 0; } refreshBlockerMenu(); toast('花瓣网自动屏蔽付费素材已' + (on ? '开启' : '关闭')); } // 动态刷新菜单(标签显示当前状态;支持注销时实时更新,旧引擎降级为仅初始化注册一次) let _cmdSwitch = null, _cmdMode = null, _menuRegistered = false; function refreshBlockerMenu() { const switchLabel = '🌸 花瓣网自动屏蔽付费素材:' + (blockerEnabled ? '已开启 ✓' : '已关闭'); const modeLabel = '🌸 ' + (hidePaid ? '显示付费素材(粉色背景)' : '隐藏付费素材'); if (typeof GM_unregisterMenuCommand === 'function') { if (_cmdSwitch) GM_unregisterMenuCommand(_cmdSwitch); if (_cmdMode) GM_unregisterMenuCommand(_cmdMode); _cmdSwitch = GM_registerMenuCommand(switchLabel, () => setBlockerEnabled(!blockerEnabled)); _cmdMode = GM_registerMenuCommand(modeLabel, toggleHidePaid); } else if (!_menuRegistered) { // 不支持注销:只在初始化注册一次,状态靠 toast 提示 GM_registerMenuCommand(switchLabel, () => setBlockerEnabled(!blockerEnabled)); GM_registerMenuCommand(modeLabel, toggleHidePaid); _menuRegistered = true; } } function initBlocker() { console.log('[花瓣屏蔽] 自动屏蔽付费素材脚本已加载(功能:' + (blockerEnabled ? '开启' : '关闭') + ',付费素材当前:' + (hidePaid ? '隐藏' : '显示(粉色背景)') + ')'); injectBlockerStyles(); refreshBlockerMenu(); if (blockerEnabled) { const runScans = () => { setTimeout(scanAllCards, 500); setTimeout(blockTopAds, 600); applyManualAdSignatures(); startPeriodicTopAdScan(); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', runScans); } else { runScans(); } // SPA 导航检测:花瓣网是 SPA,URL 变化(pushState/replaceState/hashchange/popstate) // 后页面内容会整体替换,需要重新扫描。 let lastUrl = location.href; const onUrlChange = () => { if (location.href === lastUrl) return; lastUrl = location.href; console.log('[花瓣屏蔽] 检测到 SPA 路由变化,重新扫描'); // 路由变化后 DOM 会重新渲染,稍微延迟再扫 [600, 1200, 2000].forEach((d) => setTimeout(() => { if (!blockerEnabled) return; scanAllCards(); blockTopAds(); applyManualAdSignatures(); }, d)); // 重置周期扫描计时器(新页面重新高频扫 60 秒) startPeriodicTopAdScan(); }; // 劫持 history API const origPush = history.pushState; const origReplace = history.replaceState; history.pushState = function () { origPush.apply(this, arguments); onUrlChange(); }; history.replaceState = function () { origReplace.apply(this, arguments); onUrlChange(); }; window.addEventListener('hashchange', onUrlChange); window.addEventListener('popstate', onUrlChange); } initBlockerObserver(); initTopAdObserver(); initNetErrFixer(); if (typeof GM_registerMenuCommand === 'function') { GM_registerMenuCommand('🌸 顶部广告诊断(打印到控制台)', () => { diagnoseTopAds(); toast('已把顶部候选元素打印到控制台(F12 → Console)'); }); GM_registerMenuCommand('🌸 付费素材诊断(打印卡片结构)', () => { diagnosePaidCards(); toast('已把付费卡片真实 class/文本打印到控制台(F12 → Console)'); }); GM_registerMenuCommand('🌸 手动点选屏蔽元素(顶部广告兜底)', () => enterPickMode()); GM_registerMenuCommand('🌸 清除手动屏蔽记忆', () => { clearManualAdSigs(); toast('已清除手动屏蔽记忆(已隐藏元素将在刷新后恢复)'); }); GM_registerMenuCommand('🌸 自动修复网络错误弹窗:' + (CONFIG.AUTO_FIX_NETERR ? '已开启 ✓' : '已关闭'), () => { CONFIG.AUTO_FIX_NETERR = !CONFIG.AUTO_FIX_NETERR; toast('网络错误弹窗自动修复已' + (CONFIG.AUTO_FIX_NETERR ? '开启' : '关闭')); }); } } /* ============================================================ * 第二部分:快速下载原图(Alt + 左键,全站生效) * 引擎:v3.0.5 并发竞速(页面 fetch + GM_xhr 双通道、智能 Referer 防盗链) * ============================================================ */ /* ----------------------- 工具函数 ----------------------- */ function isImageUrl(u) { return /\.(jpe?g|png|gif|webp|bmp|svg|avif|tiff?|ico)(\?|#|$)/i.test(u); } function toAbs(url) { try { return new URL(url, location.href).href; } catch (e) { return url; } } function cleanText(s) { if (!s) return ''; return s.replace(/\s+/g, ' ').replace(/[\r\n\t]+/g, ' ').trim(); } function isJunk(s) { if (!s || s.length > 120) return true; if (/^(点击|查看|click|view|enlarge|放大|加载中|loading)/i.test(s)) return true; if (/^https?:\/\//i.test(s)) return true; return false; } function getExt(url) { try { const u = new URL(url, location.href); const path = decodeURIComponent(u.pathname); const m = path.match(/\.([a-z0-9]{2,5})$/i); if (m) return '.' + m[1].toLowerCase(); const q = u.search.match(/[?&][^=?]*\.([a-z0-9]{2,5})(?:$|&)/i); if (q) return '.' + q[1].toLowerCase(); } catch (e) {} return ''; } function mimeToExt(mime) { const m = { 'image/jpeg': '.jpg', 'image/jpg': '.jpg', 'image/png': '.png', 'image/gif': '.gif', 'image/webp': '.webp', 'image/bmp': '.bmp', 'image/svg+xml': '.svg', 'image/avif': '.avif', 'image/tiff': '.tif', }; return m[(mime || '').toLowerCase().split(';')[0].trim()] || ''; } function sanitize(name) { let s = (name || '').trim().replace(/\s+/g, ' '); s = s.replace(/[\\/:*?"<>|\r\n\t]/g, ''); s = s.replace(/[\u200E\u200F\u202A-\u202E\u2066-\u2069]/g, ''); s = s.replace(/\.{2,}/g, '.'); s = s.replace(/\.[a-z0-9]{2,5}$/i, ''); s = s.slice(0, CONFIG.MAX_NAME_LEN).trim(); s = s.replace(/[. ]+$/g, ''); return s; } function isMeaninglessName(s) { if (!s) return true; if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s)) return true; if (/^[0-9a-f]{16,}$/i.test(s)) return true; if (/^\d{10,}$/.test(s)) return true; if (/^[a-z0-9]{20,}$/i.test(s)) return true; return false; } function dedupe(arr) { return Array.from(new Set(arr)); } /* ----------------------- 元素级原图地址 ----------------------- */ function largestFromSrcset(set) { const parts = set.split(',').map((p) => p.trim()).filter(Boolean); let best = '', bestN = -1; for (const p of parts) { const m = p.match(/^(\S+)\s+(?:(\d+)w|(\d+(?:\.\d+)?)x)$/); if (!m) continue; const u = m[1]; const n = m[2] ? parseInt(m[2], 10) : parseFloat(m[3]) * (window.devicePixelRatio || 1) * 1000; if (n > bestN) { bestN = n; best = u; } } return best; } function getOriginalUrl(el) { let url = ''; const dataKeys = [ 'original', 'src', 'real-src', 'realSrc', 'real', 'large', 'big', 'zoom', 'full', 'hires', 'high', 'hd', 'lazy-src', 'ori', 'origin', 'osrc', 'large-src', 'big-src', 'full-src', 'original-src', 'source-src', 'orjURL', 'zoom-src', 'hd-src', 'true-src', 'bdata-src', ]; for (const k of dataKeys) { const v = el.getAttribute && el.getAttribute('data-' + k); if (v && /^(https?:|\/\/|data:image\/)/.test(v)) { url = v; break; } } if (CONFIG.SRCSET_USE_LARGEST && el.srcset) { const s = largestFromSrcset(el.srcset); if (s) url = s; } if (!url) url = el.currentSrc || el.src || ''; try { const a = el.closest && el.closest('a[href]'); if (a) { const h = a.getAttribute('href'); if (h && isImageUrl(h) && toAbs(h) !== toAbs(url)) url = h; } } catch (e) {} return toAbs(url); } function getBgImage(el) { try { const cs = getComputedStyle(el); const bi = cs.backgroundImage || cs.webkitMaskImage || ''; if (bi && bi !== 'none') { const m = bi.match(/url\(["']?(.*?)["']?\)/); if (m && m[1]) return m[1]; } } catch (e) {} return null; } function getImageFromTarget(target) { if (!target || target.nodeType !== 1) return null; if (target.tagName === 'IMG') return { el: target, bgUrl: null }; const bg = getBgImage(target); if (bg) return { el: target, bgUrl: bg }; const found = findNearestImg(target); if (found) return { el: found, bgUrl: null }; return null; } function findNearestImg(node) { if (!node || !node.querySelector) return null; const child = node.querySelector('img'); if (child) return child; let p = node.parentElement, depth = 0; while (p && depth < 5) { const im = p.querySelector && p.querySelector('img'); if (im) return im; p = p.parentElement; depth++; } return null; } /* ----------------------- 花瓣网:接口取原图 ----------------------- */ function isHuabanPage() { return /(^|\.)huaban\.com$/i.test(location.hostname); } function isHuabanCdn(url) { try { const h = new URL(url).hostname; return /hbimg/i.test(h) && /\.huaban(img)?\.com$/i.test(h); } catch (e) { return false; } } // Referer 智能匹配:避免给跨站图片硬塞第三方 Referer 触发 CDN 反盗链/头异常 function refererFor(url) { try { const h = new URL(url, location.href).hostname; if (/\.huaban(img)?\.com$/i.test(h)) return 'https://huaban.com/'; if (h === location.hostname) return location.href; } catch (e) {} return ''; // 跨站第三方图:不带 Referer } function huabanCdnOriginal(url) { if (!isHuabanCdn(url)) return url; try { const u = new URL(url); u.hostname = u.hostname.replace(/^gd-hbimg-edge\./i, 'gd-hbimg.'); u.pathname = u.pathname .replace(/_(?:sq|fw|sf|sh|fh)\d+.*$/i, '') .replace(/\/format\/.*$/i, ''); u.search = ''; u.hash = ''; return u.href; } catch (e) { return url; } } function huabanPinIdFromContext(target) { let node = target; for (let i = 0; i < 15 && node; i++) { try { const a = node.closest && node.closest('a[href]'); if (a) { const mm = (a.getAttribute('href') || '').match(/\/pins?\/(\d+)/i); if (mm) return mm[1]; } const holder = node.closest && node.closest('[data-pin-id], [data-pinid]'); if (holder) { const did = holder.getAttribute('data-pin-id') || holder.getAttribute('data-pinid'); if (did && /^\d+$/.test(did)) return did; } } catch (e) {} node = node.parentElement; } const img = target && target.tagName === 'IMG' ? target : (target && target.querySelector ? target.querySelector('img') : null); if (img) { const did = img.getAttribute('data-id') || img.getAttribute('data-pin-id'); if (did && /^\d{5,}$/.test(did)) return did; const key = huabanKeyFromUrl(img.currentSrc || img.src || ''); if (key) { const pid = huabanPinIdByImgKey(key); if (pid) return pid; } } const m = location.pathname.match(/\/pins?\/(\d+)/i); if (m) return m[1]; return null; } function huabanKeyFromUrl(u) { try { if (!u || !isHuabanCdn(u)) return ''; const p = new URL(u, location.href).pathname.replace(/^\//, ''); return p.replace(/_(?:sq|fw|sf|sh|fh)\d+.*$/i, '').replace(/\/format\/.*$/i, ''); } catch (e) { return ''; } } function huabanPinIdByImgKey(key) { try { const links = document.querySelectorAll('a[href*="/pins/"]'); for (const a of links) { const img = a.querySelector('img'); const src = img ? (img.currentSrc || img.getAttribute('src') || '') : ''; if (src && src.indexOf(key) !== -1) { const m = (a.getAttribute('href') || '').match(/\/pins?\/(\d+)/i); if (m) return m[1]; } } } catch (e) {} try { const st = window.__INITIAL_STATE__ || window.__APOLLO_STATE__ || window.__PRELOADED_STATE__; if (st) { const pid = findPinIdInState(st, key, 0, { n: 8000 }); if (pid) return pid; } } catch (e) {} try { const scripts = document.querySelectorAll('script:not([src])'); for (const s of scripts) { const t = s.textContent || ''; if (t.length < 20 || t.length > 3000000) continue; const i = t.indexOf(key); if (i === -1) continue; const before = t.slice(Math.max(0, i - 2000), i); const re = /"pin_id"\s*:\s*(\d+)/g; let m, last = null; while ((m = re.exec(before))) last = m[1]; if (last) return last; } } catch (e) {} return null; } function findPinIdInState(obj, key, depth, budget) { if (!obj || typeof obj !== 'object' || depth > 8 || budget.n <= 0) return null; budget.n--; const f = obj.file; if (f && typeof f === 'object' && f.key === key) { const id = obj.pin_id || obj.pinId || obj.id; if (id && /^\d+$/.test(String(id))) return String(id); } for (const k in obj) { const r = findPinIdInState(obj[k], key, depth + 1, budget); if (r) return r; } return null; } function getHuabanPin(pinId, cb) { const apiUrl = 'https://huaban.com/v3/pins/' + pinId; const parse = (json) => { const pin = json && json.pin; if (!pin) throw new Error('no pin'); const f = pin.file || {}; const urls = []; if (f.url) urls.push(f.url); if (f.bucket && f.key) urls.push('https://' + f.bucket + '.huaban.com/' + f.key); return { urls: dedupe(urls), ext: mimeToExt(f.type), width: f.width || 0, height: f.height || 0, rawText: cleanText(pin.raw_text || ''), board: (pin.board && pin.board.title) || '', }; }; if (isHuabanPage()) { fetch(apiUrl, { headers: { 'Accept': 'application/json' } }) .then((r) => r.json()) .then((j) => cb(parse(j))) .catch(() => cb(null)); return; } GM_xmlhttpRequest({ method: 'GET', url: apiUrl, headers: { 'User-Agent': navigator.userAgent, 'Referer': refererFor(apiUrl) || 'https://huaban.com/', 'Accept': 'application/json' }, timeout: CONFIG.FETCH_TIMEOUT, onload: (res) => { try { cb(parse(JSON.parse(res.responseText))); } catch (e) { cb(null); } }, onerror: () => cb(null), ontimeout: () => cb(null), }); } /* ----------------------- 通用站:缩略图规则变换 ----------------------- */ function transformThumbUrl(url) { const out = []; try { const u = new URL(url); const h = u.hostname; const p = u.pathname; if (/-\d{2,4}x\d{2,4}(\.[a-z0-9]{2,5})$/i.test(p)) { out.push(u.origin + p.replace(/-\d{2,4}x\d{2,4}(\.[a-z0-9]{2,5})$/i, '$1')); } if (/(?:_thumb|-thumb|_small|-small|_medium|-medium|_s)(\.[a-z0-9]{2,5})$/i.test(p)) { out.push(u.origin + p.replace(/(?:_thumb|-thumb|_small|-small|_medium|-medium|_s)(\.[a-z0-9]{2,5})$/i, '$1')); } if (/sinaimg\.cn$/i.test(h)) { out.push(url.replace(/\/(orj360|bmiddle|orj1080|thumb150|thumbnail|square|mw690)\//i, '/oslarge/')); } if (/twimg\.com$/i.test(h) && u.search) { const nu = new URL(url); nu.searchParams.set('name', 'orig'); out.push(nu.href); } if (/pinimg\.com$/i.test(h)) { out.push(url.replace(/\/\d{2,4}x\//, '/originals/')); } if (/hdslb\.com$/i.test(h) && /@/.test(p)) { out.push(u.origin + p.replace(/@.*$/, '')); } } catch (e) {} return dedupe(out).filter((x) => x && x !== url); } /* ----------------------- 通用站:进详情页找原图 ----------------------- */ function fetchDetailCandidates(pageUrl, cb) { GM_xmlhttpRequest({ method: 'GET', url: pageUrl, headers: { 'User-Agent': navigator.userAgent, 'Accept': 'text/html,application/xhtml+xml' }, timeout: CONFIG.FETCH_TIMEOUT, onload: (res) => { try { const doc = new DOMParser().parseFromString(res.responseText || '', 'text/html'); const urls = []; const abs = (x) => { try { return new URL(x, pageUrl).href; } catch (e) { return ''; } }; doc.querySelectorAll('meta[property="og:image"], meta[name="og:image"], meta[property="twitter:image"], meta[name="twitter:image"]') .forEach((m) => { const u = abs(m.getAttribute('content') || ''); if (u && isImageUrl(u)) urls.push({ url: u, trust: 'og' }); }); doc.querySelectorAll('a[href]').forEach((a) => { const h = a.getAttribute('href') || ''; if (isImageUrl(h)) { const u = abs(h); if (u) urls.push({ url: u, trust: 'struct' }); } }); const imgs = Array.from(doc.querySelectorAll('img')).map((im) => { const w = parseInt(im.getAttribute('width') || '0', 10); const hh = parseInt(im.getAttribute('height') || '0', 10); let best = ''; const keys = ['original', 'src', 'real-src', 'large', 'big', 'full', 'hires', 'zoom', 'lazy-src']; for (const k of keys) { const v = im.getAttribute('data-' + k); if (v && /^(https?:|\/\/)/.test(v)) { best = v; break; } } if (!best && im.getAttribute('srcset')) best = largestFromSrcset(im.getAttribute('srcset')); if (!best) best = im.getAttribute('src') || ''; return { u: abs(best), area: w * hh, dataDriven: !!im.getAttribute('data-original') || !!im.getAttribute('data-src') }; }).filter((x) => x.u && isImageUrl(x.u)); imgs.sort((a, b) => (b.dataDriven - a.dataDriven) || (b.area - a.area)); imgs.slice(0, 2).forEach((x) => urls.push({ url: x.u, trust: x.dataDriven ? 'struct' : 'og' })); const ogTitle = (doc.querySelector('meta[property="og:title"], meta[name="og:title"]') || {}).content || ''; const title = cleanText(ogTitle || (doc.title || '')); cb({ urls: dedupe(urls.map((x) => x.url)).map((u) => urls.find((x) => x.url === u)), title: title }); } catch (e) { cb({ urls: [], title: '' }); } }, onerror: () => cb({ urls: [], title: '' }), ontimeout: () => cb({ urls: [], title: '' }), }); } /* ----------------------- 取网页显示的名称 ----------------------- */ function collectNameCandidates(el) { const cands = []; const push = (s, score) => { s = cleanText(s); if (s && !isJunk(s)) cands.push({ s, score }); }; push(el.alt, 50); push(el.title, 45); push(el.getAttribute && el.getAttribute('aria-label'), 40); ['name', 'caption', 'title', 'alt', 'desc', 'label', 'figcaption'].forEach((k) => { const v = el.getAttribute && el.getAttribute('data-' + k); if (v) push(v, 42); }); let node = el; while (node && node.parentElement && node.tagName !== 'FIGURE') node = node.parentElement; if (node && node.tagName === 'FIGURE') { const fc = node.querySelector('figcaption'); if (fc) push(fc.textContent, 60); } try { const a = el.closest && el.closest('a[href]'); if (a) { push(a.getAttribute('title'), 72); push(a.getAttribute('aria-label'), 66); const aText = Array.from(a.childNodes) .filter((n) => n.nodeType === 3 && n.textContent.trim()) .map((n) => n.textContent).join(' '); if (aText.length <= 60) push(aText, 35); a.querySelectorAll('.description, .pin-desc, .pin-title, [class*="desc" i], [class*="title" i]') .forEach((c) => push(c.textContent, 58)); } } catch (e) {} const captionSel = 'figcaption, .description, .pin-desc, .pin-title, [class*="caption" i], [class*="title" i], ' + '[class*="desc" i], [class*="name" i], [class*="info" i], [id*="caption" i], ' + '[class*="图注" i], [class*="说明" i], [class*="图名" i]'; const container = el.closest ? el.closest('figure, li, .item, [class*="item" i], [class*="card" i], a[href], div, td') : null; if (container) container.querySelectorAll(captionSel).forEach((c) => push(c.textContent, 55)); if (el.parentElement) { Array.from(el.parentElement.children).forEach((ch) => { if (ch !== el && ch.textContent && ch.textContent.trim().length <= 60) push(ch.textContent, 30); }); const parentText = Array.from(el.parentElement.childNodes) .filter((n) => n.nodeType === 3 && n.textContent.trim()) .map((n) => n.textContent).join(' '); if (parentText.length <= 60) push(parentText, 22); } return cands; } function getImageName(el) { const cands = collectNameCandidates(el); if (cands.length === 0) return ''; const seen = new Set(); const uniq = cands.filter((c) => { const k = c.s.toLowerCase(); if (seen.has(k)) return false; seen.add(k); return true; }); uniq.sort((a, b) => b.score - a.score || a.s.length - b.s.length); return uniq[0].s; } function fallbackName(url) { try { const u = new URL(url, location.href); const seg = u.pathname.split('/').pop(); const m = seg && seg.match(/^[^.?#]+/); if (m && m[0] && m[0] !== '') return m[0].slice(0, CONFIG.MAX_NAME_LEN); } catch (e) {} return 'image_' + Date.now(); } function getMeta(name) { const m = document.querySelector('meta[property="' + name + '"], meta[name="' + name + '"]'); return m ? m.getAttribute('content') : ''; } function resolveName(name, url) { let base = sanitize(name); if (!base || isMeaninglessName(base)) { const pageName = sanitize(getMeta('og:title') || getMeta('twitter:title') || document.title); if (pageName && !isMeaninglessName(pageName)) base = pageName; } if (!base) base = fallbackName(url); return base; } /* ----------------------- 下载(blob + ,文件名 100% 精确)------ */ function sniffExt(buf) { const b = new Uint8Array(buf); if (b.length < 4) return ''; if (b[0] === 0x3C) return 'HTML'; if (b[0] === 0xFF && b[1] === 0xD8) return '.jpg'; if (b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4E && b[3] === 0x47) return '.png'; if (b[0] === 0x47 && b[1] === 0x49 && b[2] === 0x46) return '.gif'; if (b[0] === 0x42 && b[1] === 0x4D) return '.bmp'; if (b.length > 11 && b[0] === 0x52 && b[1] === 0x49 && String.fromCharCode(b[8], b[9], b[10], b[11]) === 'WEBP') return '.webp'; return ''; } // blob URL + 落盘(文件名由脚本 100% 控制) function save(blob, fileName) { const u = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = u; a.download = fileName; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(u), 15000); } const _dlUsed = {}; const STALL_TIMEOUT = 8000; // 传输 stall:收到过数据后 8s 无新字节才判传输中断(真卡死) const OVERALL_TIMEOUT = 15000; // 整体兜底:15s 无任何进展(连接被拦/无响应/无进度事件环境)才判死,慢图靠此撑到 onload const HARD_TIMEOUT = 60000; // GM_xhr 自带硬超时 function fmtSize(n) { return n >= 1048576 ? (n / 1048576).toFixed(1) + 'MB' : Math.round(n / 1024) + 'KB'; } // 校验 + 命名 + 落盘。返回 Promise function finishBlob(blob, url, baseName, extFallback, onOk) { if (!blob || blob.size < 400) return Promise.resolve('响应过小(' + (blob ? blob.size : 0) + 'B)'); return blob.slice(0, 16).arrayBuffer().then((buf) => { const sniffed = sniffExt(buf); if (sniffed === 'HTML') return '返回的是网页(反爬/重定向)'; if (onOk() === false) return null; // 别的候选已完成,静默丢弃 let ext = sniffed || mimeToExt(blob.type) || ''; if (ext && !ext.startsWith('.')) ext = '.' + ext; if (!ext) ext = getExt(url) || ''; if (!ext) ext = extFallback || '.jpg'; const fn = baseName + ext; save(blob, fn); toast('已下载:' + fn + '(' + fmtSize(blob.size) + ')'); return null; }).catch(() => '数据校验异常'); } // GM_xmlhttpRequest 抓数据(arraybuffer,页面侧重组 blob——MV3 下直接返 blob 可能是空壳) // onWin:本候选成功落盘后回调,用于 abort 其它竞速候选;onabort 自清 stall 定时器 function blobViaXhr(url, baseName, extFallback, onOk, onNext, onProgress, onWin) { const startTime = Date.now(); let lastData = startTime; let gotHeaders = false; // 是否已收到响应头(readyState>=2),一定随连接进展触发 let gotProgress = false; // onprogress 是否触发过(部分 MV3 环境不触发) let wd = null, req = null; const stop = () => { if (wd) { clearInterval(wd); wd = null; } }; const fail = (reason) => { stop(); onNext(reason); }; const hdrs = { Accept: '*/*' }; const _rf = refererFor(url); if (_rf) hdrs.Referer = _rf; try { req = GM_xmlhttpRequest({ method: 'GET', url, responseType: 'arraybuffer', headers: hdrs, timeout: HARD_TIMEOUT, onabort: () => { stop(); }, onloadstart: () => { lastData = Date.now(); }, onreadystatechange: (resp) => { if (resp && resp.readyState >= 2) { gotHeaders = true; lastData = Date.now(); } }, onprogress: (e) => { gotProgress = true; lastData = Date.now(); if (onProgress && e && e.loaded) onProgress(e.loaded); }, onload: (res) => { stop(); if (res.status && res.status >= 400) { fail('HTTP ' + res.status); return; } const buf = res.response; if (!buf || !buf.byteLength) { fail('空响应'); return; } const ctM = (res.responseHeaders || '').match(/content-type:\s*([^\r\n;]+)/i); const blob = new Blob([buf], { type: ctM ? ctM[1].trim() : '' }); finishBlob(blob, url, baseName, extFallback, onOk) .then((reason) => { if (reason) fail(reason); else if (onWin) onWin(); }); }, onerror: () => fail('XHR 网络错误/被拦截(检查脚本域名授权)'), ontimeout: () => fail('XHR 硬超时'), }); } catch (e) { fail('请求被拦截(检查脚本域名授权)'); return req; } wd = setInterval(() => { const now = Date.now(); if (gotHeaders && gotProgress && now - lastData > STALL_TIMEOUT) { stop(); try { req && req.abort && req.abort(); } catch (e) {} onNext('传输中断(' + Math.round(STALL_TIMEOUT / 1000) + 's无数据)'); } else if (now - startTime > OVERALL_TIMEOUT) { stop(); try { req && req.abort && req.abort(); } catch (e) {} onNext(gotHeaders ? ('下载超时(' + Math.round(OVERALL_TIMEOUT / 1000) + 's未完成)') : ('连接超时(' + Math.round(OVERALL_TIMEOUT / 1000) + 's无响应/被拦截)')); } }, 1000); return req; } // 页面原生 fetch 通道(直接走浏览器:命中已加载图片的缓存/CDN,不受 GM_xhr 跨域代理挂起影响; // 跨域无 CORS 时快速失败,交由 GM_xhr 兜底)。返回 { abort } function blobViaFetch(url, baseName, extFallback, onOk, onNext, onProgress, onWin) { const ctrl = new AbortController(); const startTime = Date.now(); let lastData = startTime, gotHeaders = false, gotProgress = false; let wd = null; const stop = () => { if (wd) { clearInterval(wd); wd = null; } }; const fail = (reason) => { stop(); onNext(reason); }; wd = setInterval(() => { const now = Date.now(); if (gotHeaders && gotProgress && now - lastData > STALL_TIMEOUT) { try { ctrl.abort(); } catch (e) {} stop(); onNext('传输中断(' + Math.round(STALL_TIMEOUT / 1000) + 's无数据)'); } else if (now - startTime > OVERALL_TIMEOUT) { try { ctrl.abort(); } catch (e) {} stop(); onNext(gotHeaders ? ('下载超时(' + Math.round(OVERALL_TIMEOUT / 1000) + 's未完成)') : 'fetch跨域被拦截(无CORS)'); } }, 1000); fetch(url, { signal: ctrl.signal, mode: 'cors', cache: 'force-cache', credentials: 'omit', referrerPolicy: 'no-referrer' }) .then((res) => { if (!res.ok) { fail('HTTP ' + res.status); return; } gotHeaders = true; lastData = Date.now(); const reader = res.body && res.body.getReader ? res.body.getReader() : null; if (!reader) { return res.arrayBuffer().then((buf) => ({ buf: buf, chunks: null, received: buf ? buf.byteLength : 0 })); } const chunks = []; let received = 0; const pump = () => reader.read().then(({ done, value }) => { if (done) return { buf: null, chunks, received }; gotProgress = true; received += value.byteLength; lastData = Date.now(); if (onProgress) onProgress(received); chunks.push(value); return pump(); }); return pump(); }) .then((data) => { if (!data) return; stop(); let buf = data.buf; if (!buf && data.chunks) { buf = new Uint8Array(data.received); let pos = 0; for (const c of data.chunks) { buf.set(c, pos); pos += c.byteLength; } buf = buf.buffer; } if (!buf || !buf.byteLength) { fail('空响应'); return; } const blob = new Blob([buf], { type: '' }); // 类型由 finishBlob 按魔数嗅探 finishBlob(blob, url, baseName, extFallback, onOk) .then((reason) => { if (reason) fail(reason); else if (onWin) onWin(); }); }) .catch((err) => { if (err && err.name === 'AbortError') return; fail('fetch跨域被拦截(无CORS)'); }); return { abort: () => { try { ctrl.abort(); } catch (e) {} } }; } // 统一入口:并发竞速下载(所有候选同时发起,第一个成功落盘即 abort 其余),典型 <1s function raceDownload(urls, name, extHint) { const queue = dedupe(urls).filter((u) => /^https?:/i.test(u)); if (!queue.length) { toast('未找到可下载的图片地址'); return null; } const ext = getExt(queue[0]) || extHint || '.jpg'; let base = resolveName(name, queue[0]); if (_dlUsed[base + ext]) { let n = 1; while (_dlUsed[base + '_' + n + ext]) n++; base = base + '_' + n; } _dlUsed[base + ext] = true; let done = false; const runners = []; const reasons = []; let lastProg = 0; const onOk = () => { if (done) return false; done = true; return true; }; const progress = (loaded, idx) => { const now = Date.now(); if (now - lastProg < 400) return; lastProg = now; toast('下载中… ' + fmtSize(loaded) + (queue.length > 1 ? '(竞速 ' + queue.length + ' 路)' : '')); }; const checkAllFail = () => { if (done) return; if (runners.length && runners.every((r) => r && r.finished)) { const joined = reasons.slice(0, 2).join(';') || '所有候选地址均不可用'; const authHint = /被拦截|无响应|跨域/.test(joined) ? '(持续出现请检查篡改猴该脚本的域名权限是否已允许 huaban.com 等,或在当前站点限制了跨域下载)' : ''; console.log('[快速下载原图] 下载失败详情:', reasons); toast('下载失败:' + joined + authHint); } }; const onWin = (winnerIdx) => { runners.forEach((r, i) => { if (i === winnerIdx || !r) return; r.finished = true; r.reqs.forEach((x) => { if (x.req && x.req.abort) { try { x.req.abort(); } catch (e) {} } }); }); }; function launch(u, idx) { const r = { finished: false, reqs: [] }; runners[idx] = r; // GM_xhr 主判定:失败才记为该候选失败(并 abort 掉 fetch 路) r.reqs.push({ _isFetch: false, req: blobViaXhr(u, base, extHint, onOk, (reason) => { if (r.finished) return; r.finished = true; reasons.push('地址' + idx + ':' + reason); r.reqs.forEach((x) => { if (x._isFetch && x.req && x.req.abort) { try { x.req.abort(); } catch (e) {} } }); checkAllFail(); }, (loaded) => progress(loaded, idx), () => onWin(idx)), }); // 页面 fetch 加速通道:命中缓存/开 CORS 秒下;失败静默,不计入失败原因 r.reqs.push({ _isFetch: true, req: blobViaFetch(u, base, extHint, onOk, () => {}, (loaded) => progress(loaded, idx), () => onWin(idx)), }); } queue.forEach((u, i) => launch(u, i)); return { // 竞速中途追加候选(如花瓣接口返回的签名原图),并动态优化命名 add(extraUrls, newBase, newExt) { if (done) return; if (newBase) { const nb = resolveName(newBase, queue[0]); if (nb && !isMeaninglessName(nb)) base = nb; } const more = dedupe(extraUrls || []).filter((u) => /^https?:/i.test(u) && queue.indexOf(u) === -1); more.forEach((u) => { queue.push(u); launch(u, queue.length - 1); }); }, }; } /* ----------------------- 主流程 ----------------------- */ function handleHuaban(hit, elUrl) { const cdnOrig = huabanCdnOriginal(elUrl); const domName = getImageName(hit.el); toast('正在下载原图…'); // 0 延迟并发竞速:裸 key CDN 原图 + 页面原始 src 同时开拉 const racer = raceDownload(dedupe([cdnOrig, elUrl]), domName, ''); // 异步取官方接口:签名原图 + raw_text 命名,回来即追加进竞速并优化文件名 const pinId = huabanPinIdFromContext(hit.el); if (pinId && racer) { getHuabanPin(pinId, (pin) => { if (pin && pin.urls.length) { racer.add(pin.urls, pin.rawText || domName || pin.board, pin.ext || ''); } }); } } function handleGeneric(hit, elUrl) { const cands = dedupe([elUrl].concat(transformThumbUrl(elUrl))); const name = getImageName(hit.el); toast('正在下载原图…'); raceDownload(cands, name, ''); const wrapA = hit.el.closest ? hit.el.closest('a[href]') : null; const aHref = wrapA && wrapA.getAttribute('href'); if (aHref && !isImageUrl(aHref)) { fetchDetailCandidates(toAbs(aHref), (dc) => { if (dc && dc.urls.length) { console.log('[快速下载原图] 详情页候选:', dc.urls.map((x) => x.url)); } }); } } /* ----------------------- Alt + 左键监听 ----------------------- */ // Alt + 左键图片 = 下载原图;普通右键 = 浏览器原生菜单 // 捕获阶段 + stopPropagation 拦在站点自己的点击处理之前,避免误跳转 document.addEventListener('click', (e) => { if (!e.altKey) return; const hit = getImageFromTarget(e.target); if (!hit) return; e.preventDefault(); e.stopPropagation(); const elUrl = hit.bgUrl ? toAbs(hit.bgUrl) : getOriginalUrl(hit.el); if (isHuabanPage() || isHuabanCdn(elUrl)) handleHuaban(hit, elUrl); else handleGeneric(hit, elUrl); }, true); /* ----------------------- 轻提示 ----------------------- */ let toastTimer = null; function toast(msg) { if (!CONFIG.SHOW_TOAST) return; let box = document.getElementById('rcid-toast'); if (!box) { box = document.createElement('div'); box.id = 'rcid-toast'; box.style.cssText = 'position:fixed;left:50%;bottom:48px;transform:translateX(-50%);' + 'z-index:2147483647;background:rgba(20,20,22,.92);color:#fff;' + 'padding:10px 16px;border-radius:8px;font-size:14px;line-height:1.4;' + 'max-width:80vw;box-shadow:0 4px 16px rgba(0,0,0,.4);' + 'font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;' + 'pointer-events:none;transition:opacity .3s;'; document.body.appendChild(box); } box.textContent = msg; box.style.opacity = '1'; clearTimeout(toastTimer); toastTimer = setTimeout(() => { if (box) box.style.opacity = '0'; }, 2600); } /* ----------------------- 下载功能:使用说明菜单 ----------------------- */ GM_registerMenuCommand('快速下载原图 - 使用说明', () => { alert( 'Alt + 左键图片 = 下载原图(按网页名称命名)\n' + '普通右键 = 浏览器原生菜单(脚本不拦截)\n\n' + '下载机制:\n' + '· 多个候选原图【并发竞速】,最快者先落盘,典型 <1 秒\n' + '· 每候选走【页面 fetch + GM_xhr 双通道】:fetch 命中浏览器缓存/CDN 最快,GM_xhr 兜底跨域\n\n' + '原图判断逻辑:\n' + '1. 花瓣网:调官方 v3 接口拿签名原图 + 发布内容命名(异步追加,不拖慢开拉)\n' + '2. 缩略图外链详情页:抓 og:image / 主图 data-original / srcset 最大图\n' + '3. 常见缩略图规则变换:WordPress -300x200、微博 orj360→oslarge、twimg name=orig、Pinterest /236x/→/originals/、B站 @后缀\n\n' + '文件名优先级(通用站):\n' + '包裹图片 的 title/aria-label > figcaption > alt/title/data-* > 相邻短文本 > 详情页标题 > URL 文件名\n' + '花瓣网:发布内容(raw_text) > 网页文字 > 画板名 > 花瓣_\n\n' + '失败提示自查:\n' + '· 连接超时/被拦截 → 篡改猴域名授权未放行(设置→域名 允许 huaban.com、hbimg.huaban.com 等)\n' + '· 传输中断 → 网络波动;fetch跨域被拦截 → 当前站点禁止跨域下载(换花瓣站内即可)' ); }); /* ============================================================ * 第三部分:花瓣网「网络错误」弹窗自动修复(仅花瓣网生效) * ------------------------------------------------------------ * 说明:花瓣网前端自己会在接口失败时弹出「网络错误,请重试或检查网络状态」。 * 本脚本并不拦截 / 改写网络请求,无法「造成」该错误;但为提升浏览体验,这里 * 提供轻量自动处理:检测到该弹窗后自动点击「重试」,连续失败则自动关闭, * 避免它挡住页面。可经菜单「🌸 自动修复网络错误弹窗」随时开关。 * ============================================================ */ const NETERR_TEXT = /网络错误|网络异常|加载失败|请求失败|连接失败|服务异常|请重试或检查网络状态|网络开小差|出错了/i; let _neterrScheduled = false; let _neterrLastKey = ''; let _neterrRetries = 0; let _neterrLastAction = 0; // 只在少数「浮层类」容器里找错误文案,避免全文档遍历带来性能开销 function findNetErrPopup() { const sels = [ '[role="dialog"]', '[role="alertdialog"]', '[class*="modal" i]', '[class*="dialog" i]', '[class*="toast" i]', '[class*="popup" i]', '[class*="overlay" i]', '[class*="mask" i]', '[class*="message" i]', '[class*="notice" i]', '[class*="alert" i]', ]; const cands = document.querySelectorAll(sels.join(',')); for (const el of cands) { const t = (el.textContent || '').replace(/\s+/g, ' ').trim(); if (t.length < 4 || t.length > 200) continue; if (NETERR_TEXT.test(t)) return el; } // 兜底:直接在视口内且 position:fixed 的浮层(无稳定 class 时) const fixed = document.querySelectorAll('div, section'); for (const el of fixed) { const cs = window.getComputedStyle(el); if (cs.position !== 'fixed') continue; const t = (el.textContent || '').replace(/\s+/g, ' ').trim(); if (t.length < 4 || t.length > 200) continue; if (NETERR_TEXT.test(t)) return el; } return null; } function closeNetErrPopup(popup) { const btns = popup.querySelectorAll('button, [role="button"], a, [class*="close" i], [class*="cancel" i]'); for (const b of btns) { const bt = (b.textContent || '').trim(); const cls = (b.className || '') + ' ' + (b.getAttribute && b.getAttribute('aria-label') || ''); if (/关闭|取消|知道了|不再提示|×|x/i.test(bt) || /(close|cancel|dismiss)/i.test(cls)) { b.click(); toast('已关闭网络错误提示'); return true; } } // 退路:直接隐藏整个浮层容器 let node = popup; while (node && node !== document.body) { const cs = window.getComputedStyle(node); if (cs.position === 'fixed' || cs.position === 'absolute' || /(modal|dialog|toast|popup|overlay|mask)/i.test((node.className || '') + ' ' + (node.id || ''))) { node.style.setProperty('display', 'none', 'important'); toast('已隐藏网络错误提示'); return true; } node = node.parentElement; } return false; } function autoFixNetErr() { if (!CONFIG.AUTO_FIX_NETERR) return; if (document.hidden) return; const now = Date.now(); if (now - _neterrLastAction < 3000) return; // 全局冷却,避免狂点 const popup = findNetErrPopup(); if (!popup) return; const key = (popup.textContent || '').replace(/\s+/g, '').slice(0, 80); if (key !== _neterrLastKey) { _neterrLastKey = key; _neterrRetries = 0; } // 优先找「重试 / 刷新 / 重新加载」按钮 const allBtns = popup.querySelectorAll('button, [role="button"], a'); let retryBtn = null; for (const b of allBtns) { const bt = (b.textContent || '').trim(); if (/重试|刷新|重新加载|再试一次|重新尝试|retry|reload|try again/i.test(bt)) { retryBtn = b; break; } } if (retryBtn && _neterrRetries < 3) { _neterrRetries++; _neterrLastAction = now; console.log('[花瓣屏蔽] 检测到网络错误弹窗,自动点击「重试」(第 ' + _neterrRetries + ' 次)'); toast('检测到网络错误,已自动重试'); retryBtn.click(); } else { // 重试达上限或无重试按钮:关闭弹窗 _neterrLastAction = now; closeNetErrPopup(popup); } } function initNetErrFixer() { if (!CONFIG.AUTO_FIX_NETERR) return; // 轻量监听:DOM 变化时节流检查(不遍历全文档) const obs = new MutationObserver(() => { if (_neterrScheduled) return; _neterrScheduled = true; setTimeout(() => { _neterrScheduled = false; autoFixNetErr(); }, 600); }); try { obs.observe(document.body, { childList: true, subtree: true }); } catch (e) {} // 初次 + 周期兜底 setTimeout(autoFixNetErr, 1500); setInterval(autoFixNetErr, 4000); } /* ============================================================ * 初始化 * ============================================================ */ // 屏蔽付费素材:仅花瓣网生效 if (isHuabanPage()) { initBlocker(); } })();