您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
✨ 终极场景化控制!动态流智能展开,图书/影视/同城页直接跳转,"提醒"中的链接纯净跳转。
// ==UserScript== // @name 豆瓣全文无跳转展开 // @namespace https://github.com/yourname/douban-fulltext // @version 3.8.0 // @description ✨ 终极场景化控制!动态流智能展开,图书/影视/同城页直接跳转,"提醒"中的链接纯净跳转。 // @author MA GUANG + Qwen3-Max + Claude Sonnet 4 // @license MIT // @match *://www.douban.com/ // @match *://www.douban.com/?* // @match *://www.douban.com/people/* // @match *://www.douban.com/people/*?* // @match *://www.douban.com/group/* // @match *://www.douban.com/topic/* // @match *://www.douban.com/note/* // @match *://book.douban.com/annotation/* // @match *://book.douban.com/review/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @connect www.douban.com // @connect book.douban.com // @run-at document-idle // ==/UserScript== (function() { 'use strict'; // 自定义配置区域 const DEFAULT_CONFIG = { charLimit: 1500, // 字数阈值,小于该数字后自动展开,超过该数字后跳转到新页面,可自行修改 imageLimit: 4, // 图片数量阈值,小于该数字后自动展开,超过该数字后跳转到新页面,可自行修改 }; // 获取配置 function getConfig(key) { return GM_getValue(key, DEFAULT_CONFIG[key]); } // 样式注入 GM_addStyle(` .douban-fulltext-loading { display: inline-block; padding: 2px 6px; color: #666; font-size: 12px; background: #f5f5f5; border-radius: 12px; margin-left: 5px; vertical-align: middle; animation: loading 1.4s infinite linear; } @keyframes loading { 0% { opacity: 0.4; } 50% { opacity: 1; } 100% { opacity: 0.4; } } .douban-fulltext-threshold-info { display: inline-block; padding: 4px 8px; color: #e74c3c; font-size: 11px; background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 12px; margin-left: 5px; vertical-align: middle; font-weight: bold; white-space: nowrap; max-width: 200px; overflow: hidden; text-overflow: ellipsis; } .douban-fulltext-error { display: inline-block; color: #e51c23; font-size: 12px; margin-left: 5px; cursor: pointer; text-decoration: underline; vertical-align: middle; } .douban-fulltext-success { color: #4CAF50; font-size: 12px; margin-left: 5px; vertical-align: middle; } `); /** * 精准统计正文内容字数 */ function countDoubanBodyTextChars(contentElement) { if (!contentElement) return 0; const clonedElement = contentElement.cloneNode(true); // 移除无关元素 const elementsToRemove = [ '.ft', '.note-ft', '.desc', '.info', '.meta', '.actions', '.footer', '.copyright', '.author', '.date', '.source', '.tags', '.category', '.aside', '.sidebar', '.related', '.recommend', '.comments', '.comment', '.reply' ]; elementsToRemove.forEach(selector => { clonedElement.querySelectorAll(selector).forEach(el => { el.remove(); }); }); // 获取并清理文本 let textContent = clonedElement.textContent || ''; // 移除特定干扰文本 textContent = textContent.replace(/\d+人阅读/g, ''); textContent = textContent.replace(/表示其中内容是对原文的摘抄/g, ''); textContent = textContent.replace(/说明 · · · · · ·/g, ''); textContent = textContent.replace(/引自.*$/gm, ''); textContent = textContent.replace(/来自 豆瓣App/g, ''); const cleanText = textContent .replace(/\s+/g, ' ') .replace(/[\u200B-\u200D\uFEFF]/g, '') .trim(); return cleanText.length; } /** * 安全替换内容 */ function safeReplaceContent(originalContainer, newContent, loadingElement, fullTextLink) { try { if (!newContent || !newContent.textContent || newContent.textContent.trim().length < 10) { throw new Error('新内容无效或过短'); } const wrapper = document.createElement('div'); wrapper.className = 'douban-fulltext-container'; const clonedContent = newContent.cloneNode(true); // 修复图片 clonedContent.querySelectorAll('img[data-origin]').forEach(img => { if (!img.src || img.src.includes('placeholder')) { img.src = img.dataset.origin; } }); originalContainer.innerHTML = ''; originalContainer.appendChild(wrapper); wrapper.appendChild(clonedContent); const success = document.createElement('span'); success.className = 'douban-fulltext-success'; success.textContent = '✓ 已展开'; if (loadingElement && loadingElement.parentNode) { loadingElement.parentNode.replaceChild(success, loadingElement); } if (fullTextLink && fullTextLink.parentNode) { fullTextLink.parentNode.removeChild(fullTextLink); } return true; } catch (error) { console.error('安全替换失败:', error); return false; } } /** * 检查内容是否符合阈值条件 */ function checkContentThresholds(contentElement) { const charLimit = getConfig('charLimit'); const imageLimit = getConfig('imageLimit'); const charCount = countDoubanBodyTextChars(contentElement); const images = contentElement.querySelectorAll('img:not([src*="icon"]):not([src*="avatar"]):not([src*="placeholder"])'); const imageCount = images.length; const exceedsCharLimit = charCount > charLimit; const exceedsImageLimit = imageCount > imageLimit; return { charCount, imageCount, exceedsCharLimit, exceedsImageLimit, shouldOpenInNewPage: exceedsCharLimit || exceedsImageLimit }; } /** * 🚀 核心函数:判断链接是否应直接跳转 * 在动态流中,以下链接应直接跳转,不做任何分析: * 1. 图书主页面 (book.douban.com/subject/) * 2. 影视主页面 (movie.douban.com/subject/) * 3. 同城活动页面 (www.douban.com/location/) */ function shouldDirectJump(fullUrl) { return ( fullUrl.includes('book.douban.com/subject/') || fullUrl.includes('movie.douban.com/subject/') || fullUrl.includes('www.douban.com/location/') ); } /** * 🚫 终极修复:“提醒”弹窗链接纯净跳转 * 1. 为链接添加特殊标记 * 2. 克隆新元素以清除所有原有事件 */ function handleReminderLinks() { // 选择提醒弹窗中的所有链接 const reminderLinks = document.querySelectorAll('#top-nav-notimenu a[href]'); reminderLinks.forEach(link => { // 跳过已处理的链接 if (link.dataset.reminderProcessed) return; link.dataset.reminderProcessed = 'true'; // 🚩 标记这是“提醒”中的链接 link.dataset.reminderLink = 'true'; // 🚫 核心修复:克隆新元素,彻底清除原有事件监听器 const newLink = link.cloneNode(true); newLink.dataset.reminderLink = 'true'; // 新元素也要标记 // 替换旧链接 link.parentNode.replaceChild(newLink, link); // 为新链接添加纯净的直接跳转事件 newLink.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); console.log('✅ 在“提醒”中检测到链接,纯净直接跳转:', this.href); window.open(this.href, '_blank'); }); }); } /** * 处理动态流中的"全文"链接 */ function processFeedFullTextLink(link) { // 🚫 核心修复:如果是“提醒”中的链接,直接跳过,不处理 if (link.dataset.reminderLink === 'true') { console.log('🚫 跳过“提醒”中的链接:', link.href); return; } if (link.dataset.fulltextProcessed) return; link.dataset.fulltextProcessed = 'true'; const isFullTextLink = link.textContent.includes('全文') || link.textContent.includes('展开') || link.textContent.includes('...') || link.href.includes('_dtcc=1'); if (!isFullTextLink) return; const fullUrl = link.href; // 🚀 场景1:如果是图书/影视/同城页面,直接跳转 if (shouldDirectJump(fullUrl)) { link.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); console.log('🚀 在动态流中检测到特殊页面,直接跳转:', fullUrl); window.open(fullUrl, '_blank'); }); return; } // 🚀 场景2:其他链接,进行字数统计和阈值判断 let contentContainer = null; if (window.location.hostname === 'www.douban.com' && window.location.pathname.includes('/note/')) { contentContainer = link.closest('.note-content, .content, .obligate, #link-report, .article-content'); } else { contentContainer = link.closest('.content, .brief, .status-content, .topic-content, .obligate, .note-content, .status-text, .topic-reply-content, .article-content'); } if (!contentContainer) { console.warn('无法找到合适的内容容器'); return; } const originalContent = contentContainer.innerHTML; link.addEventListener('click', async function(e) { e.preventDefault(); e.stopPropagation(); const loading = document.createElement('span'); loading.className = 'douban-fulltext-loading'; loading.textContent = '分析中...'; this.parentNode.insertBefore(loading, this); try { const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: fullUrl, headers: { 'Referer': window.location.href, 'Accept': 'text/html,application/xhtml+xml' }, timeout: 8000, onload: resolve, onerror: reject, ontimeout: () => reject(new Error('请求超时')) }); }); if (response.status !== 200) { throw new Error(`HTTP ${response.status}`); } const parser = new DOMParser(); const doc = parser.parseFromString(response.responseText, 'text/html'); let fullContent = null; if (fullUrl.includes('/annotation/') || fullUrl.includes('/review/')) { fullContent = doc.querySelector('.note > .content') || doc.querySelector('.note-content') || doc.querySelector('.content'); } else if (fullUrl.includes('/note/')) { fullContent = doc.querySelector('#link-report') || doc.querySelector('.note-content') || doc.querySelector('.content'); } else if (fullUrl.includes('/topic/')) { fullContent = doc.querySelector('.topic-content .content') || doc.querySelector('.topic-content') || doc.querySelector('.content'); } else { fullContent = doc.querySelector('.content, .brief, .status-content, .topic-content, .obligate'); } if (!fullContent) { throw new Error('无法提取有效内容'); } // 检查阈值 const thresholdInfo = checkContentThresholds(fullContent); if (thresholdInfo.shouldOpenInNewPage) { loading.textContent = ''; // 单行提示 const thresholdInfoSpan = document.createElement('span'); thresholdInfoSpan.className = 'douban-fulltext-threshold-info'; thresholdInfoSpan.textContent = `内容过多(${thresholdInfo.charCount}字/${thresholdInfo.imageCount}图),新页打开`; loading.parentNode.replaceChild(thresholdInfoSpan, loading); setTimeout(() => { window.open(fullUrl, '_blank'); }, 2000); return; } // 在当前页面展开 loading.textContent = '展开中...'; const success = safeReplaceContent(contentContainer, fullContent, loading, this); if (!success) { throw new Error('内容替换失败'); } } catch (error) { console.error('展开全文失败:', error.message); // 恢复原始内容 contentContainer.innerHTML = originalContent; if (loading.parentNode) { loading.textContent = ''; const errorDiv = document.createElement('span'); errorDiv.className = 'douban-fulltext-error'; errorDiv.textContent = '加载失败,点击查看原文'; errorDiv.onclick = () => { window.open(fullUrl, '_blank'); }; loading.parentNode.replaceChild(errorDiv, loading); } } }); } /** * 扫描并处理页面中的所有"全文"链接 */ function handleFullTextLinks() { // 处理“提醒”弹窗中的链接 handleReminderLinks(); // 处理动态流中的"全文"链接 document.querySelectorAll('a[href*="_dtcc=1"], a[href*="/topic/"], a[href*="/note/"], a[href*="/annotation/"], a[href*="/review/"]').forEach(link => { processFeedFullTextLink(link); }); } // 初始化 setTimeout(handleFullTextLinks, 1500); const observer = new MutationObserver(function(mutations) { clearTimeout(observer.debounceTimer); observer.debounceTimer = setTimeout(handleFullTextLinks, 300); }); observer.observe(document.body, { childList: true, subtree: true }); console.log('豆瓣全文无跳转展开脚本已启动'); })();