// ==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('豆瓣全文无跳转展开脚本已启动');
})();