// ==UserScript==
// @name X阅读模式
// @namespace https://scriptcat.org/scripts/code/7614/X阅读模式.user.js
// @version 1.3.5
// @description 小说漫画阅读增强工具:自动进入纯净阅读模式,滚动加载下一章,支持目录选章、字号/行距/主题调节、点击翻页。支持批量下载章节为TXT,分页失败自动重试并备注,停止后已下载章节完整保留。当前网站自动加入白名单并拦截无关跳转,自动保存阅读进度,支持净化词过滤正文,油猴菜单可一键导出诊断日志。
// @author 失辛向南
// @license MIT
// @match *://*/*
// @run-at document-end
// @noframes
// @require https://cdnjs.cloudflare.com/ajax/libs/readability/0.4.1/Readability.min.js
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_xmlhttpRequest
// @connect *
// ==/UserScript==
(function () {
'use strict';
if (window.top !== window.self) return;
/* ==================== 诊断日志系统 ==================== */
var __diag = (function () {
var MAX = 800;
var lines = [];
var scriptName = 'X阅读模式';
var version = '1.3.4';
function nowStr() {
try { return new Date().toLocaleString('zh-CN', { hour12: false }); } catch (e) { return new Date().toString(); }
}
function safeStr(o) {
try {
if (o instanceof Error) return o.name + ': ' + o.message + (o.stack ? '\n' + o.stack.split('\n').slice(0, 6).join('\n') : '');
if (typeof o === 'string') return o;
return JSON.stringify(o);
} catch (e) { return String(o); }
}
function push(level, msg) {
try {
var s = safeStr(msg);
if (s.length > 600) s = s.slice(0, 600) + '…';
lines.push('[' + nowStr() + '][' + level + '] ' + s);
if (lines.length > MAX) lines.shift();
} catch (e) {}
}
function wrapConsole() {
['log', 'info', 'warn', 'error', 'debug'].forEach(function (m) {
var orig = console[m];
if (typeof orig !== 'function') return;
try {
console[m] = function () {
try {
var args = Array.prototype.slice.call(arguments);
var text = args.map(function (a) { return typeof a === 'string' ? a : safeStr(a); }).join(' ');
if (m === 'error' || m === 'warn') push(m.toUpperCase(), text);
else if (text.charAt(0) === '[') push(m.toUpperCase(), text);
} catch (e) {}
return orig.apply(console, arguments);
};
} catch (e) {}
});
}
function hookErrors() {
window.addEventListener('error', function (ev) {
try {
// 资源加载失败(img/script/link 等):单独记录具体资源 URL
var t = ev.target;
if (t && t.tagName && !(ev.message)) {
var src = t.src || t.currentSrc || t.href || '';
push('ERROR', '资源加载失败: <' + String(t.tagName).toLowerCase() + '> ' + (src || '(无URL)'));
return;
}
var msg = ev.message || '';
var loc = ((ev.filename || '') + ':' + (ev.lineno || '') + ':' + (ev.colno || '')).replace(/^::/, '');
if (ev.error && ev.error.message) msg = ev.error.message;
// 过滤无 message、无位置、无 error 对象的空错误事件(多为扩展/无关资源注入,无排障价值)
if (!msg && !loc && !ev.error) return;
var extra = '';
if (ev.error && ev.error.stack) {
extra = '\n' + ev.error.stack.split('\n').slice(0, 5).join('\n');
}
push('ERROR', 'window.onerror: ' + msg + ' @ ' + loc + extra);
} catch (e2) {}
}, true);
window.addEventListener('unhandledrejection', function (ev) {
var r = ev.reason;
push('ERROR', 'unhandledrejection: ' + (r && r.message ? r.message : safeStr(r)));
});
}
function buildReport() {
var out = [];
out.push('========== ' + scriptName + ' 诊断日志 ==========');
out.push('版本: ' + version);
out.push('生成时间: ' + nowStr());
out.push('页面: ' + location.href);
out.push('UA: ' + navigator.userAgent);
out.push('视口: ' + (window.innerWidth || '') + 'x' + (window.innerHeight || ''));
try {
out.push('白名单数量: ' + getWhitelist().length);
out.push('历史记录数量: ' + getHistory().length);
var st = getSettings();
out.push('设置: ' + (st ? safeStr(st) : '(默认)'));
} catch (e) { out.push('存储状态读取失败: ' + safeStr(e)); }
out.push('-------- 日志记录 (' + lines.length + ' 条) --------');
for (var i = 0; i < lines.length; i++) out.push(lines[i]);
return out.join('\n');
}
function download() {
try {
var text = buildReport();
var blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
var d = new Date(), pad = function (n) { return n < 10 ? '0' + n : '' + n; };
a.download = scriptName + '-诊断日志-' + d.getFullYear() + pad(d.getMonth() + 1) + pad(d.getDate()) + '-' + pad(d.getHours()) + pad(d.getMinutes()) + pad(d.getSeconds()) + '.txt';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(function () { try { URL.revokeObjectURL(url); } catch (e2) {} }, 5000);
return true;
} catch (e) {
try { prompt('无法自动下载日志,请复制以下内容:', buildReport()); } catch (e2) {}
return false;
}
}
wrapConsole();
hookErrors();
return { push: push, download: download, buildReport: buildReport, info: function (m) { push('INFO', m); }, warn: function (m) { push('WARN', m); }, error: function (m) { push('ERROR', m); } };
})();
__diag.info('脚本已加载,版本 ' + '1.3.1');
/* ==================== 常量与状态 ==================== */
var KEY_SETTINGS = 'novel_reader_settings_v1';
var KEY_WHITELIST = 'novel_reader_whitelist_v1';
var KEY_HISTORY = 'novel_reader_history_v1';
var KEY_PURIFY = 'novel_reader_purify_v1';
var MAX_STITCH = 500;
var state = {
open: false,
opening: false,
root: null,
win: null,
visited: {},
stitchCount: 0,
pendingInit: null,
bookKey: null,
bookTitle: null,
exiting: false // 新增:退出标志,用于 beforeunload 判断
};
var _tocStop = false;
/* ==================== 存储 ==================== */
function getSettings() {
try { var v = GM_getValue(KEY_SETTINGS, ''); var o = v ? JSON.parse(v) : null; return (o && typeof o === 'object') ? o : null; } catch (e) { return null; }
}
function getWhitelist() {
try {
var raw = GM_getValue(KEY_WHITELIST, '[]');
var list = JSON.parse(raw);
if (Array.isArray(list) && list.length > 0 && typeof list[0] === 'string') {
var newList = list.map(function(d) { return { domain: d, title: '' }; });
saveWhitelist(newList);
return newList;
}
if (Array.isArray(list)) {
list = list.filter(function(item) { return item && typeof item === 'object' && item.domain; });
return list;
}
return [];
} catch (e) {
return [];
}
}
function saveWhitelist(list) {
try {
GM_setValue(KEY_WHITELIST, JSON.stringify(list));
} catch (e) {}
}
function inWhitelist(host) {
var list = getWhitelist();
for (var i = 0; i < list.length; i++) {
if (list[i].domain === host) return true;
}
return false;
}
function getHistory() {
try { var v = GM_getValue(KEY_HISTORY, '[]'); var h = JSON.parse(v); return Array.isArray(h) ? h : []; } catch (e) { return []; }
}
function saveHistory(history) { try { GM_setValue(KEY_HISTORY, JSON.stringify(history)); } catch (e) {} }
function getPurifyWords() {
try {
var v = GM_getValue(KEY_PURIFY, '[]');
var list = JSON.parse(v);
if (!Array.isArray(list)) return [];
return list.filter(function (w) { return typeof w === 'string' && w.trim(); });
} catch (e) { return []; }
}
function savePurifyWords(list) {
try { GM_setValue(KEY_PURIFY, JSON.stringify(list)); } catch (e) {}
}
function extractNovelTitle(title) {
if (!title) return '';
var t = title.replace(/[((][^))]*[))]/g, '').trim();
var match = t.match(/^(.*?)\s*第[0-9零一二三四五六七八九十百千万亿两]+[章节回卷话篇集]/);
if (match) {
return match[1].trim() || t;
}
return t || '未命名';
}
function getBookRootKey(url) {
try {
var u = new URL(url);
var path = u.pathname;
if (path.endsWith('/')) return u.origin + path;
var parts = path.split('/').filter(function(p) { return p; });
if (parts.length === 0) return u.origin + '/';
var last = parts[parts.length - 1];
if (/^\d+$/.test(last) || /^\d+\.html?$/i.test(last) || (/^[a-z0-9_-]+\.html?$/i.test(last) && /\d/.test(last))) {
parts.pop();
}
var newPath = '/' + parts.join('/');
if (!newPath.endsWith('/')) newPath += '/';
return u.origin + newPath;
} catch(e) {
return url;
}
}
function addHistory(chapterTitle, url, bookKey, bookTitle) {
if (!chapterTitle || !url) return;
var cleanUrl = url.split('#')[0];
var key = bookKey || getBookRootKey(cleanUrl);
var list = getHistory();
var found = false;
var finalTitle = bookTitle || extractNovelTitle(chapterTitle) || '未命名';
for (var i = 0; i < list.length; i++) {
var item = list[i];
var existingKey = item.bookKey || getBookRootKey(item.url);
if (existingKey === key) {
item.timestamp = Date.now();
item.url = cleanUrl;
if (!item.bookKey) item.bookKey = key;
if (!item.title || item.title === '未命名') {
item.title = finalTitle;
}
found = true;
break;
}
}
if (!found) {
list.push({ title: finalTitle, url: cleanUrl, timestamp: Date.now(), bookKey: key });
if (list.length > 100) {
list.sort(function(a,b) { return a.timestamp - b.timestamp; });
list.shift();
}
}
saveHistory(list);
}
function fetchSiteTitle(domain, callback) {
if (!domain) { callback(''); return; }
var url = 'https://' + domain;
GM_xmlhttpRequest({
method: 'GET',
url: url,
timeout: 5000,
onload: function(res) {
try {
var html = res.responseText;
var match = html.match(/
]*>([^<]*)<\/title>/i);
if (match && match[1]) {
callback(match[1].trim());
} else {
callback('');
}
} catch(e) { callback(''); }
},
onerror: function() { callback(''); },
ontimeout: function() { callback(''); }
});
}
/* ==================== 工具函数 ==================== */
var CHAP_RE = /(第\s*[0-9零一二三四五六七八九十百千两〇○]+\s*[章节節回卷話篇集]|chapter\s*\d+|序章|楔子|尾声|后记|番外)/i;
var NAV_WORDS = /^(首页|书架|排行|分类|搜索|登录|注册|完本|最新|书库|书单|我的|消息|反馈|下载|客户端|繁體|繁体|english|返回顶部|上一页|下一页|上一章|下一章|刷新|加入书签|投推荐票|章节错误|字号|夜间模式|手机阅读|电脑版)$/i;
var TOC_TEXT_RE = /^(目录|目錄|章节目录|返回目录|查看目录|返回书页|书页|目次|章节列表|列表|目录页)$/;
function resolveUrl(href, base) { try { return new URL(href, base).href; } catch (e) { return ''; } }
function isBadHref(h2) { return !h2 || h2.indexOf('javascript:') === 0 || h2.charAt(0) === '#' || h2.indexOf('about:') === 0 || h2.indexOf('mailto:') === 0; }
function noHash(u) { try { return u.split('#')[0]; } catch (e) { return u; } }
function cleanText(t) { return (t || '').replace(/\s+/g, '').trim(); }
// 判断 URL 是否为“章节正文页”:末段形如 123.html / 123_1.html / abc123.html。
// 目录页通常以 / 结尾或无 .html 后缀,不会命中。用于宽松目录匹配的护栏。
function looksLikeChapterUrl(u) {
try {
var segs = (new URL(u).pathname || '').split('/').filter(function (s) { return s; });
var last = segs[segs.length - 1] || '';
return /^\d+(?:_\d+)?\.html?$/i.test(last) ||
(/^[a-z0-9_-]+\.html?$/i.test(last) && /\d/.test(last));
} catch (e) { return false; }
}
function chineseToNumber(ch) {
if (!ch) return 0;
var map = { '零':0,'〇':0,'○':0,'一':1,'二':2,'两':2,'三':3,'四':4,'五':5,'六':6,'七':7,'八':8,'九':9,'十':10,'百':100,'千':1000,'万':10000,'亿':100000000 };
// 无位权数字的“逐位序列”写法:一一零=110、一零二=102(部分站点这样写章号)
var hasWei = false;
for (var w = 0; w < ch.length; w++) {
if (map[ch[w]] >= 10) { hasWei = true; break; }
}
if (!hasWei) {
var n = 0;
for (var d = 0; d < ch.length; d++) {
var dv = map[ch[d]];
if (dv === undefined || dv >= 10) continue;
n = n * 10 + dv;
}
return n;
}
var num = 0, temp = 0;
for (var i=0; i= 10) {
if (temp === 0) temp = 1;
num += temp * val;
temp = 0;
} else {
temp = val;
}
}
return num + temp;
}
function romanToNumber(roman) {
var map = { 'I':1,'V':5,'X':10,'L':50,'C':100,'D':500,'M':1000 };
var total = 0, prev = 0;
for (var i=roman.length-1; i>=0; i--) {
var cur = map[roman[i]];
if (!cur) continue;
if (cur < prev) total -= cur;
else total += cur;
prev = cur;
}
return total;
}
function parseChapterNumber(title) {
if (!title) return null;
var original = String(title).trim();
// 预处理①:去掉所有空格(含全角空格),保证空格不影响章号识别
var clean = original.replace(/[\s\u3000]+/g, '');
var n = pickChapterNumber(clean);
if (n !== null) return n;
// 预处理②:去掉括号及其内容(()、()、【】、〔〕、「」、『』),避免"(第一卷终)"等干扰排序
n = pickChapterNumber(clean.replace(/[((【\[〔〈「『].*?[))】\]〕〉」』]/g, ''));
if (n !== null) return n;
// 兜底:空格分隔的"12 正文"形式在去空格后无法识别,用原标题再试一次
return pickChapterNumber(original);
}
function pickChapterNumber(clean) {
var match;
// 阿拉伯数字章号优先("第405章(第三卷终)"应取章号405,不被括号里的卷号干扰)
match = clean.match(/第\s*(\d+)\s*[章节回话篇集]/i);
if (match) return parseInt(match[1], 10);
// 中文章号(不含"卷":卷是分卷编号,不是章节号,避免"第025章(第一卷终)"取成卷号1)
match = clean.match(/第\s*([〇○零一二三四五六七八九十百千万亿两](?:\s*[〇○零一二三四五六七八九十百千万亿两])*)\s*[章节回话篇集]/i);
if (match) return chineseToNumber(match[1]);
match = clean.match(/chapter\s*(\d+)/i);
if (match) return parseInt(match[1], 10);
match = clean.match(/chapter\s*([IVXLCDM]+)/i);
if (match) return romanToNumber(match[1]);
match = clean.match(/^(\d+)[、..\s]/);
if (match) return parseInt(match[1], 10);
match = clean.match(/^([零一二三四五六七八九十百千万亿两]+)[、..\s]/);
if (match) return chineseToNumber(match[1]);
match = clean.match(/^([IVXLCDM]+)[、..\s]/);
if (match) return romanToNumber(match[1]);
return null;
}
function scoreNext(t) {
// 支持 下一章 及变体:下—章、下-章、下–章
if (/^(下一[章節节回卷篇]|下一章节|next\s*chapter|nextchapter|下[—\-–]?[章節节回卷篇])$/i.test(t)) return 5;
if (/(下一[章節节回卷篇]|下[—\-–]?[章節节回卷篇])/.test(t)) return 4;
// 支持 下一页 及变体:下—页、下-页、下–页
if (/^(下一[页頁張张]|下[—\-–]?[页頁張张]|下页|下節|next\s*page|nextpage)$/i.test(t)) return 2;
if (/^(next|›|»|→|>>?)$/i.test(t)) return 1;
return 0;
}
function scorePrev(t) {
if (/^(上一[章節节回卷篇]|上一章节|prev\s*chapter|prevchapter)$/i.test(t)) return 5;
if (/(上一[章節节回卷篇])/.test(t)) return 4;
if (/^(上一[页頁張张]|上页|prev\s*page|prevpage|previous)$/i.test(t)) return 2;
if (/^(prev|‹|«|←|<)$/i.test(t)) return 1;
return 0;
}
/* ==================== 增强模块:规则库 ==================== */
var SITE_RULES = [
{ name: 'T1', prev: '', next: '', chapter: '.pagee > .main > a[href]', must: ['.pagee > .main > a[href]', '.novelinfvie'] },
{ name: 'T2', prev: '.read_bg .read_nav a#prev_url[href]', next: '.read_bg .read_nav a#next_url[href]', chapter: '.read_bg .read_nav a#info_url[href]' },
{ name: 'T3', prev: '#pagewrap .nav2 .prev a', next: '#pagewrap .nav2 .next a', chapter: '#pagewrap .bcrumb a[rel="category tag"]' },
{ name: 'T4', prev: 'div.container > section.RBGsectionTwo > ul > li.RBGsectionTwo-left > a', next: 'div.container > section.RBGsectionTwo > ul > li.RBGsectionTwo-right > a', chapter: 'div.container > section.RBGsectionTwo > ul > li:nth-child(2) > a' },
{ name: 'T5', prev: '.article > .next_pre > p:nth-child(1) > a', next: '.article > .next_pre > p:nth-child(2) > a', chapter: '' },
{ name: 'T6', prev: '.main > .entry-text > table:nth-child(1) > tbody > tr > td:nth-child(1) > a', next: '.main > .entry-text > table:nth-child(1) > tbody > tr > td:nth-child(2) > a', chapter: '#bcrumb a[rel~="category"]' },
{ name: 'T7', prev: '.container > .post > .page > ul > li:nth-child(1) a', next: '.container > .post > .page > ul > li:nth-child(3) a', chapter: '.container > .breadcrumb > ul > li:nth-child(2) > a' },
{ name: 'T8', prev: '.chapter-page-btn #pb_prev', next: '.chapter-page-btn #pb_next', chapter: '' },
{ name: 'T9', prev: '', next: '', chapter: '#novelbody > .page_chapter > ul > li > a.p3' },
{ name: 'T10', prev: '', next: '', chapter: 'table.title > tbody > tr > td:nth-child(1) > a', must: ['.read_content', '.chapter_content'] },
{ name: 'T11', prev: '#prev_url', next: '#next_url', chapter: '#info_url', must: ['#ReadSet', '.jumbotron', '#chaptercontent', '.container', '.readpage'] },
{ name: 'T12', prev: '#area_newsDet .prevNextBox .pagePrev a', next: '#area_newsDet .prevNextBox .pageNext a', chapter: '#area_newsDet .area_subDetTitle .center a' },
];
/* ==================== 漫画检测与提取 ==================== */
function isComicPage(doc) {
// 已按要求移除漫画网页适配:任何页面都不再判定为漫画,自动加载与内容提取全部走正文(文本)路径。
// 漫画选择器/图片检测全部停用,避免误伤正常小说站点。
return false;
}
function extractComicImages(doc, baseUrl) {
var imgs = [];
var seen = {};
var containers = doc.querySelectorAll(
'.chapter-content, .comic-content, #images, .chapter-images, .comic-pages, ' +
'#manga-container, .comic-viewer, .manga-pages, .comic-panel, .img-content, .page-img, .comic-list, ' +
'.comic-main, .reader-main, .comic-reader'
);
var targetNodes = containers.length ? containers : [doc.body];
targetNodes.forEach(function (container) {
if (!container) return;
var allImgs = container.querySelectorAll('img');
allImgs.forEach(function (img) {
var src = img.getAttribute('src') ||
img.getAttribute('data-src') ||
img.getAttribute('data-original') ||
img.getAttribute('data-lazy-src') ||
img.getAttribute('data-url');
if (!src) return;
var lowerSrc = src.toLowerCase();
if (/logo|avatar|icon|button|header|footer|bg|ad|banner|loading|thumb/i.test(lowerSrc)) return;
var w = img.width || img.naturalWidth || 0;
var h = img.height || img.naturalHeight || 0;
if (w > 0 && h > 0 && (w < 30 || h < 30)) return;
var absUrl = resolveUrl(src, baseUrl);
if (!absUrl) return;
if (seen[absUrl]) return;
seen[absUrl] = true;
imgs.push({ src: absUrl, alt: img.getAttribute('alt') || '' });
});
});
if (imgs.length === 0) {
var allImgs = doc.querySelectorAll('img');
allImgs.forEach(function (img) {
var src = img.getAttribute('src') || img.getAttribute('data-src') || img.getAttribute('data-original');
if (!src) return;
var lowerSrc = src.toLowerCase();
if (/logo|avatar|icon|button|header|footer|bg|ad|banner|loading|thumb/i.test(lowerSrc)) return;
var absUrl = resolveUrl(src, baseUrl);
if (absUrl && !seen[absUrl]) {
seen[absUrl] = true;
imgs.push({ src: absUrl, alt: img.getAttribute('alt') || '' });
}
});
}
return imgs.slice(0, 50);
}
/* ==================== 核心提取函数 ==================== */
function stripTags(html) { return (html || '').replace(/<[^>]*>/g, ''); }
function readabilityParse(doc, opts) {
try {
var article = new Readability(doc.cloneNode(true), opts || {}).parse();
return article || null;
} catch (e) { return null; }
}
function collectRuleContent(doc) {
var contentEls = doc.querySelectorAll('p, div.content, div.chapter-content, #chaptercontent, .novel-content');
var texts = [];
for (var j = 0; j < contentEls.length; j++) {
var text = contentEls[j].innerHTML;
if (text && text.length > 100) texts.push(text);
}
return texts.join(' ');
}
function ruleHref(doc, baseUrl, sel) {
if (!sel) return '';
var a = doc.querySelector(sel);
return a ? resolveUrl(a.getAttribute('href'), baseUrl) : '';
}
function extractFromDoc(doc, baseUrl) {
try {
return extractFromDocInner(doc, baseUrl);
} catch (e) {
// 真实浏览器下个别 DOM 操作可能抛 Illegal invocation 等异常:绝不因此中断,返回空结构让调用方走字符串兜底
try { console.warn('[阅读模式] 内容提取异常:', baseUrl, e && e.message); } catch (e2) {}
return { url: baseUrl, title: (doc && doc.title) || '', data: '', textContent: '', next_url: '', prev_url: '', tocUrl: '', toc: [], isComic: false, images: [] };
}
}
function extractFromDocInner(doc, baseUrl) {
var matchedRule = null;
for (var i = 0; i < SITE_RULES.length; i++) {
var rule = SITE_RULES[i];
if (rule.must && rule.must.every(function (sel) { return doc.querySelector(sel); })) {
matchedRule = rule;
break;
}
}
var content = '';
var title = doc.title || '';
var nextUrl = '', prevUrl = '', tocUrl = '';
if (matchedRule) {
var titleEl = doc.querySelector('title');
if (titleEl) title = titleEl.textContent.trim();
content = collectRuleContent(doc);
if (!content || stripTags(content).trim().length < 150) {
matchedRule = null;
content = '';
}
}
/* 内容提取:命中规则用规则选择器,否则回退 Readability / 段落抓取 */
if (!matchedRule) {
var article = readabilityParse(doc, { charThreshold: 100 });
if (article) {
content = article.content || '';
title = article.title || title;
}
if (!content || content.replace(/\s/g, '').length < 100) {
var paragraphs = doc.querySelectorAll('p');
var pTexts = [];
for (var k = 0; k < paragraphs.length; k++) {
if (paragraphs[k].innerHTML.length > 50) pTexts.push(paragraphs[k].innerHTML);
}
if (pTexts.length > 1) content = pTexts.join(' ');
else {
var bodyText = doc.body.innerHTML;
if (bodyText && bodyText.length > 200) content = bodyText;
}
}
}
/* ---- 下一章 / 目录链接识别:直接照搬 1234.js 逻辑 ----
· rel=next 链接(a[rel=next] 或 link[rel=next])作为 3 分基线
· 全量扫描链接文本,按 scoreNext 取最高分(兼容“下—章/下—页”破折号写法)
· 上一章自动翻页已去掉:不再识别 prev,prev_url 恒为空
· 目录链接:先 TOC_TEXT_RE 精确词表匹配,未命中再走“包含匹配+护栏”宽松兜底 */
function relLink(rel) {
try {
var el = doc.querySelector('a[rel="' + rel + '"], link[rel="' + rel + '"]');
if (el) {
var href = el.getAttribute('href');
if (!isBadHref(href)) {
var u = resolveUrl(href, baseUrl);
if (u && noHash(u) !== noHash(baseUrl)) return u;
}
}
} catch (e) {}
return null;
}
var anchors = [];
try { anchors = Array.prototype.slice.call(doc.querySelectorAll('a[href]')); } catch (e) {}
var selfUrl = noHash(baseUrl);
var rn = relLink('next');
var nextBest = rn ? { url: rn, score: 3 } : null;
anchors.forEach(function (a) {
var href = a.getAttribute('href');
if (isBadHref(href)) return;
var u = resolveUrl(href, baseUrl);
if (!u || noHash(u) === selfUrl) return;
var t = cleanText(a.textContent);
if (!t) return;
var ns = scoreNext(t);
if (ns > 0 && (!nextBest || ns > nextBest.score)) nextBest = { url: u, score: ns };
});
nextUrl = nextBest ? nextBest.url : '';
prevUrl = '';
// 兜底:常规扫描未命中时,读取站点脚本里的 nextpage 变量(如 var nextpage = "/book/25365/11.html")
if (!nextUrl) {
try {
var scrs = doc.querySelectorAll('script');
for (var si = 0; si < scrs.length; si++) {
var st = scrs[si].textContent || '';
var nm = st.match(/var\s+nextpage\s*=\s*["']([^"']+)["']/);
if (nm) {
var nu2 = resolveUrl(nm[1], baseUrl);
if (nu2 && noHash(nu2) !== selfUrl) { nextUrl = nu2; break; }
}
}
} catch (e) {}
}
// 第一轮:精确词表匹配(目录/章节目录/书页…),整串等于词表词,最可靠,优先使用。
// 与第二轮共用护栏:链接目标不得是章节正文页(目录链接指向目录页,不可能指向 章节数字.html)。
for (var j = 0; j < anchors.length; j++) {
var href2 = anchors[j].getAttribute('href');
if (isBadHref(href2)) continue;
if (TOC_TEXT_RE.test(cleanText(anchors[j].textContent))) {
var tu = resolveUrl(href2, baseUrl);
if (tu && noHash(tu) !== selfUrl && !looksLikeChapterUrl(tu)) { tocUrl = tu; break; }
}
}
// 第二轮:宽松“包含匹配”兜底(处理“书页/目录”这类组合文字)。
// 裸包含会误伤:章节标题可能含“目录”(如《第一百二十四章 目录之争》),
// 导航文字也可能带“目录”。因此同时满足三重护栏:
// ① 文字短(目录标签一般 ≤12 字);② 不是章节标题(不匹配 CHAP_RE);
// ③ href 不是正文页(末段不是章节数字.html 文件)。
if (!tocUrl) {
for (var j2 = 0; j2 < anchors.length; j2++) {
var a2 = anchors[j2];
var href3 = a2.getAttribute('href');
if (isBadHref(href3)) continue;
var t3 = cleanText(a2.textContent);
if (!t3 || t3.length > 12) continue;
if (!/(目录|目錄|书页|書頁|章节目录|目次)/.test(t3)) continue;
if (CHAP_RE.test(t3)) continue;
var u3 = resolveUrl(href3, baseUrl);
if (!u3 || noHash(u3) === selfUrl) continue;
if (looksLikeChapterUrl(u3)) continue;
tocUrl = u3;
break;
}
}
// 第三层备用兜底:正文页显式/宽松目录链接都没找到时,用「删掉章节号」推导书根目录 URL 作为目录页。
// 注意:推导出的页可能只是“最近更新/部分章节”页,真正的完整目录由 fetchToc
// 抓取时的“查看完整目录”跟随逻辑(handleFetchToc 内 fullTocUrl 检测)再跳一层补齐。
if (!tocUrl) {
try {
var derivedToc = getBookRootKey(baseUrl);
var derivedPath = '';
try { derivedPath = new URL(derivedToc).pathname; } catch (e) {}
// 只在推导确实“删掉了章节文件段”时启用:排除站点根目录(/)与自身地址
if (derivedToc && derivedPath && derivedPath !== '/' && noHash(derivedToc) !== selfUrl) {
tocUrl = derivedToc;
try { __diag.info('未找到显式目录链接,使用URL推导兜底: ' + tocUrl); } catch (e) {}
}
} catch (e) {}
}
if (!content || content.replace(/\s/g, '').length < 50) {
var art = readabilityParse(doc);
if (art) {
content = art.content || '';
title = art.title || title;
}
}
// 漫画适配已移除:isComic 恒 false,images 恒空数组,统一走正文文本路径
var isComic = false;
var images = [];
return {
url: baseUrl,
title: title,
data: content,
textContent: content ? stripTags(content) : '',
next_url: nextUrl,
prev_url: prevUrl,
tocUrl: tocUrl,
toc: [],
isComic: isComic,
images: images
};
}
// 纯字符串正文/next 兜底:完全不依赖 DOM(避开真实浏览器里 Illegal invocation 类解析异常),
// 用正则抓
段落、var nextpage / rel=next / “下一章”锚点、目录锚点
function fallbackExtractHtml(html, baseUrl) {
var title = '', data = '', next = '', toc = '';
try {
var t = html.match(/