// ==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();
}
})();