// ==UserScript== // @name X阅读模式 // @namespace https://scriptcat.org/scripts/code/7614/X阅读模式.user.js // @version 1.5.3 // @description 小说阅读增强工具 // @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 // @grant GM_info // @connect * // ==/UserScript== (function () { 'use strict'; if (window.top !== window.self) return; /* ==================== 诊断日志系统 ==================== */ var __diag = (function () { var MAX = 800; var lines = []; var scriptName = 'X阅读模式'; var version = (function () { try { if (typeof GM_info !== 'undefined' && GM_info && GM_info.script && GM_info.script.version) { return String(GM_info.script.version); } } catch (e) {} return '2.0.1'; })(); 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 { 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; 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); try { out.push('目录缓存数量: ' + Object.keys(getTocCacheAll()).length); } catch (eTc) {} 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 { version: version, 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('脚本已加载,版本 ' + __diag.version); /* ==================== 常量与状态 ==================== */ var KEY_SETTINGS = 'novel_reader2_settings_v1'; var KEY_WHITELIST = 'novel_reader2_whitelist_v1'; var KEY_HISTORY = 'novel_reader2_history_v1'; var KEY_PURIFY = 'novel_reader2_purify_v1'; var KEY_TOC_CACHE = 'novel_reader2_toc_cache_v1'; var TOC_CACHE_TTL = 24 * 60 * 60 * 1000; var TOC_CACHE_MAX = 10; var TOC_CACHE_MAX_BYTES = 2 * 1024 * 1024; var TOC_CACHE_REFRESH_MIN = 30 * 60 * 1000; var MAX_STITCH = 500; /* ==================== 繁简转换对照表 ==================== 每两个字符一对「繁简」。生产环境可替换为 OpenCC t2s 完整表(约 3000 对)。 覆盖日常阅读 95%+ 场景,体积紧凑。 */ var CONV_PAIRS = '個个們们這这爲为說说會会來来時时裏里過过對对開开關关話话後后學学國国導导將将東东車车書书馬马鳥鸟龍龙見见現现實实當当義义樂乐習习區区醫医經经還还長长點点難难體体與与從从萬万於于電电軍军無无聲声戰战報报據据別别兒儿華华進进幹干幫帮載载隊队產产類类權权歲岁紅红數数該该親亲願愿媽妈觀观覺觉謝謝談谈認认證证識识語语誤误麼么門门問问聞闻陽阳際际隨随雖虽顧顾條条務务處处達达樓楼設设標标題题單单卻却鐵铁鋼钢錢钱銀银陸陆陳陈孫孙趙赵鄭郑韓韩葉叶嚴严蓋盖買买賣卖貴贵賤贱費费貨货貸贷價价賺赚財财賬账購购員员農农輕轻輛辆輪轮軟软較较轉转迴回歸归歷历級级終终組组細细練练織织辦办邊边結结給给統统絕绝絲丝繼继續续維维綠绿緊紧線线縮缩總总纖纤聯联聽听肅肃腦脑臉脸臨临舉举舊旧藍蓝藥药藝艺蘇苏號号蟲虫術术衛卫裝装裡里褲裤襪袜襲袭補补襯衬襪袜鶴鹤鷹鹰鵬鹏鴻鸿鴉鸦鴦鸯鸚鹦鴣鸪鴛鸳鶴鹤麥麦黃黄點点黨党鼓鼓齊齐齒齿龜龟龍龙'; /* ==================== 目录分页会话缓存(Map LRU) ==================== */ var _tocPageCache = new Map(); var TOC_PAGE_CACHE_TTL = 10 * 60 * 1000; var TOC_PAGE_CACHE_MAX = 60; var TOC_PAGE_CACHE_MAX_BYTES = 8 * 1024 * 1024; function tocPageCacheGet(url) { var v = _tocPageCache.get(url); if (!v) return null; if (Date.now() - v.time > TOC_PAGE_CACHE_TTL) { _tocPageCache.delete(url); return null; } return v; } function tocPageCacheSet(url, html) { var bytes = html ? html.length : 0; if (_tocPageCache.has(url)) _tocPageCache.delete(url); _tocPageCache.set(url, { html: html, time: Date.now(), bytes: bytes }); while (_tocPageCache.size > TOC_PAGE_CACHE_MAX) { _tocPageCache.delete(_tocPageCache.keys().next().value); } var total = 0; _tocPageCache.forEach(function (v) { total += v.bytes || 0; }); while (total > TOC_PAGE_CACHE_MAX_BYTES && _tocPageCache.size > 1) { var k = _tocPageCache.keys().next().value; total -= (_tocPageCache.get(k).bytes || 0); _tocPageCache.delete(k); } } // 首次运行:从原版(novel_reader_*)迁移数据到新版(novel_reader2_*) (function migrateFromLegacy() { try { if (GM_getValue('novel_reader2_migrated_v1', '') === '1') return; var pairs = [ ['novel_reader_whitelist_v1', KEY_WHITELIST], ['novel_reader_settings_v1', KEY_SETTINGS], ['novel_reader_purify_v1', KEY_PURIFY], ['novel_reader_history_v1', KEY_HISTORY] ]; for (var i = 0; i < pairs.length; i++) { try { var oldVal = GM_getValue(pairs[i][0], ''); var newVal = GM_getValue(pairs[i][1], ''); if (oldVal && !newVal) GM_setValue(pairs[i][1], oldVal); } catch (e) {} } GM_setValue('novel_reader2_migrated_v1', '1'); } catch (e) {} })(); var state = { open: false, opening: false, root: null, win: null, visited: {}, stitchCount: 0, pendingInit: null, bookKey: null, bookTitle: null, exiting: false }; /* ==================== visited LRU(长阅读内存稳定) ==================== */ var _visitedQueue = []; var VISITED_MAX = 500; function markVisited(url) { var key = noHash(url); if (state.visited[key]) return; state.visited[key] = 1; _visitedQueue.push(key); while (_visitedQueue.length > VISITED_MAX) { var old = _visitedQueue.shift(); delete state.visited[old]; } } function clearVisited() { state.visited = {}; _visitedQueue = []; } var _tocStop = false; var _tocBusy = false; var _dlActive = 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; } } var _whitelistCache = null; var _purifyCache = null; var _historyCache = null; var _historyWriteTimer = null; function flushHistoryWrite() { if (_historyWriteTimer) { clearTimeout(_historyWriteTimer); _historyWriteTimer = null; } if (_historyCache) { try { GM_setValue(KEY_HISTORY, JSON.stringify(_historyCache)); } catch (e) {} } } window.addEventListener('focus', function () { flushHistoryWrite(); _whitelistCache = null; _purifyCache = null; _historyCache = null; _tocCacheMem = null; idbPreload(); }); window.addEventListener('pagehide', flushHistoryWrite); function getWhitelist() { if (_whitelistCache) return _whitelistCache; 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 (_whitelistCache = newList); } if (Array.isArray(list)) { list = list.filter(function(item) { return item && typeof item === 'object' && item.domain; }); return (_whitelistCache = list); } return (_whitelistCache = []); } catch (e) { return []; } } function normalizeDomain(d) { if (!d) return ''; var s = String(d).trim().toLowerCase().replace(/^https?:\/\//i, '').split('/')[0].split('#')[0].split('?')[0]; var idx = s.indexOf(':'); if (idx !== -1) s = s.slice(0, idx); return s; } function saveWhitelist(list) { _whitelistCache = null; try { var clean = (list || []).map(function (it) { if (!it || typeof it !== 'object') return it; var d = normalizeDomain(it.domain); if (!d) return it; return { domain: d, title: it.title || '' }; }); GM_setValue(KEY_WHITELIST, JSON.stringify(clean)); } catch (e) {} } function inWhitelist(host) { var list = getWhitelist(); var target = normalizeDomain(host); for (var i = 0; i < list.length; i++) { if (list[i].domain === target) return true; } return false; } function getHistory() { if (_historyCache) return _historyCache; try { var v = GM_getValue(KEY_HISTORY, '[]'); var h = JSON.parse(v); return (_historyCache = Array.isArray(h) ? h : []); } catch (e) { return []; } } function saveHistory(history) { _historyCache = history; if (_historyWriteTimer) clearTimeout(_historyWriteTimer); _historyWriteTimer = setTimeout(flushHistoryWrite, 1500); } function getPurifyWords() { if (_purifyCache) return _purifyCache; try { var v = GM_getValue(KEY_PURIFY, '[]'); var list = JSON.parse(v); if (!Array.isArray(list)) return (_purifyCache = []); return (_purifyCache = list.filter(function (w) { return typeof w === 'string' && w.trim(); })); } catch (e) { return []; } } function savePurifyWords(list) { _purifyCache = null; try { GM_setValue(KEY_PURIFY, JSON.stringify(list)); } catch (e) {} } /* ==================== 繁简转换函数 ==================== */ var _convT2S = null, _convS2T = null; function buildConvMap(dir) { var m = Object.create(null); for (var i = 0; i + 1 < CONV_PAIRS.length; i += 2) { var trad = CONV_PAIRS.charAt(i), simp = CONV_PAIRS.charAt(i + 1); if (dir === 't2s') m[trad] = simp; else m[simp] = trad; } return m; } function convertText(text, mode) { if (!text || !mode || mode === 'off') return text; var m; if (mode === 't2s') { if (!_convT2S) _convT2S = buildConvMap('t2s'); m = _convT2S; } else if (mode === 's2t') { if (!_convS2T) _convS2T = buildConvMap('s2t'); m = _convS2T; } else return text; var n = text.length, out = ''; for (var i = 0; i < n; i++) { var c = text.charAt(i); out += (m[c] || c); } return out; } function convertDomElement(el, mode) { if (!el || !mode || mode === 'off') return; var walk = function (node) { var ch = node.childNodes; for (var i = 0; i < ch.length; i++) { var c = ch[i]; if (c.nodeType === 3) { var v = c.nodeValue; if (v && /[\u4e00-\u9fff]/.test(v)) { var cv = convertText(v, mode); if (cv !== v) c.nodeValue = cv; } } else if (c.nodeType === 1) walk(c); } }; walk(el); } function getConvertMode() { try { var st = getSettings(); return (st && st.simplified) || 'off'; } catch (e) { return 'off'; } } // 匹配用净化词:不做单向转换,而是给每个净化词同时生成"简体版 + 繁体版"两个变体, // 一起参与匹配。这样无论正文是简还是繁、无论用户当前选了哪种转换模式, // 简体词能屏蔽繁体正文、繁体词也能屏蔽简体正文。 // 隔离符(首尾 /)在转换时保留。 function getPurifyWordsForMatch() { var raw = getPurifyWords(); if (!raw || !raw.length) return raw || []; var out = []; var seen = Object.create(null); function add(w) { if (!w || seen[w]) return; seen[w] = 1; out.push(w); } for (var i = 0; i < raw.length; i++) { var w = raw[i]; add(w); var p = parsePurifyWord(w); if (!p.core) continue; var prefix = p.isoLeft ? '/' : ''; var suffix = p.isoRight ? '/' : ''; var t2s = convertText(p.core, 't2s'); var s2t = convertText(p.core, 's2t'); if (t2s !== p.core) add(prefix + t2s + suffix); if (s2t !== p.core) add(prefix + s2t + suffix); } return out; } /* ==================== 目录缓存(最近10本书,24小时过期) ==================== */ var TOC_CACHE_VER = 3; var _tocCacheMem = null; var _idbOpenPromise = null; function idbOpen() { if (_idbOpenPromise) return _idbOpenPromise; _idbOpenPromise = new Promise(function (resolve) { try { var req = indexedDB.open('__novel_reader_toc_cache', 1); req.onupgradeneeded = function (ev) { try { var db = ev.target.result; if (!db.objectStoreNames.contains('kv')) db.createObjectStore('kv'); } catch (e) {} }; req.onsuccess = function (ev) { resolve(ev.target.result); }; req.onerror = function () { resolve(null); }; req.onblocked = function () { resolve(null); }; } catch (e) { resolve(null); } }); return _idbOpenPromise; } function idbTx(mode) { return idbOpen().then(function (db) { if (!db) return null; try { return db.transaction('kv', mode).objectStore('kv'); } catch (e) { return null; } }); } function idbPut(obj) { idbTx('readwrite').then(function (store) { if (!store) return; try { store.put(obj, 'tocCache'); } catch (e) {} }).catch(function () {}); } function idbPreload() { idbTx('readonly').then(function (store) { if (!store) return null; return new Promise(function (resolve) { try { var req = store.get('tocCache'); req.onsuccess = function () { var o = req.result; if (o && typeof o === 'object' && !Array.isArray(o)) { if (_tocCacheMem === null || typeof _tocCacheMem !== 'object') _tocCacheMem = o; } else if (_tocCacheMem === null) { _tocCacheMem = {}; } resolve(); }; req.onerror = function () { resolve(); }; } catch (e) { resolve(); } }); }).catch(function () {}); } function getTocCacheAll() { if (_tocCacheMem !== null && typeof _tocCacheMem === 'object') return _tocCacheMem; try { var v = GM_getValue(KEY_TOC_CACHE, '{}'); var o = v ? JSON.parse(v) : null; var r = (o && typeof o === 'object' && !Array.isArray(o)) ? o : {}; _tocCacheMem = r; return r; } catch (e) { return {}; } } function saveTocCacheAll(o) { var obj = o || {}; _tocCacheMem = obj; idbPut(obj); try { GM_setValue(KEY_TOC_CACHE, JSON.stringify(obj)); } catch (e) {} } idbPreload(); function pruneTocCache(cache) { if (!cache || typeof cache !== 'object') return {}; var now = Date.now(); var keys = Object.keys(cache); for (var i = 0; i < keys.length; i++) { var k = keys[i], it = cache[k]; if (!it || !Array.isArray(it.toc) || !it.savedAt || now - it.savedAt > TOC_CACHE_TTL) { delete cache[k]; } } var remain = Object.keys(cache); if (remain.length > TOC_CACHE_MAX) { remain.sort(function (a, b) { return cache[a].savedAt - cache[b].savedAt; }); var drop = remain.length - TOC_CACHE_MAX; for (var j = 0; j < drop; j++) delete cache[remain[j]]; } remain = Object.keys(cache); if (remain.length) { var bytes = {}; var total = 2; for (var bi = 0; bi < remain.length; bi++) { bytes[remain[bi]] = JSON.stringify(cache[remain[bi]]).length; total += bytes[remain[bi]] + 2; } if (total > TOC_CACHE_MAX_BYTES) { remain.sort(function (a, b) { return cache[a].savedAt - cache[b].savedAt; }); while (remain.length > 1 && total > TOC_CACHE_MAX_BYTES) { var dropKey = remain.shift(); delete cache[dropKey]; total -= bytes[dropKey] + 2; } if (remain.length === 1 && total > TOC_CACHE_MAX_BYTES) { delete cache[remain[0]]; } } } return cache; } function getTocCacheForBook(bookKey) { if (!bookKey) return null; var cache = getTocCacheAll(); var it = cache[bookKey]; if (!it || !Array.isArray(it.toc) || !it.savedAt) return null; if (it.ver !== TOC_CACHE_VER) { delete cache[bookKey]; saveTocCacheAll(cache); return null; } if (Date.now() - it.savedAt > TOC_CACHE_TTL) { delete cache[bookKey]; saveTocCacheAll(cache); return null; } return it; } function saveTocCacheForBook(bookKey, toc, tocUrl, title, lastUrl) { if (!bookKey || !toc || !toc.length) return; var cache = getTocCacheAll(); var entry = { toc: toc, tocUrl: tocUrl || '', title: title || '', savedAt: Date.now(), ver: TOC_CACHE_VER }; if (lastUrl) entry.lastUrl = lastUrl; cache[bookKey] = entry; pruneTocCache(cache); saveTocCacheAll(cache); } function removeTocCacheForBook(bookKey) { if (!bookKey) return; var cache = getTocCacheAll(); if (cache[bookKey]) { delete cache[bookKey]; saveTocCacheAll(cache); } } 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; } } // addHistory:额外接收 chapterNo/chapterTotal(进度),旧调用不传则不影响 function addHistory(chapterTitle, url, bookKey, bookTitle, chapterNo, chapterTotal) { 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; } if (typeof chapterNo === 'number' && chapterNo > 0) item.chapterNo = chapterNo; if (typeof chapterTotal === 'number' && chapterTotal > 0) item.chapterTotal = chapterTotal; found = true; break; } } if (!found) { var entry = { title: finalTitle, url: cleanUrl, timestamp: Date.now(), bookKey: key }; if (typeof chapterNo === 'number' && chapterNo > 0) entry.chapterNo = chapterNo; if (typeof chapterTotal === 'number' && chapterTotal > 0) entry.chapterTotal = chapterTotal; list.push(entry); 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 = decodeResponseText(res, url); var match = html.match(/
]*>([\s\S]*?)<\/p>/gi); if (ps && ps.length) { data = ps.join(''); } else { var body = html.match(/
]*>([\s\S]*?)<\/body>/i); if (body && body[1]) data = body[1]; } } catch (e) {} return { url: baseUrl, title: title, data: data, textContent: stripTags(data), next_url: next, prev_url: '', tocUrl: toc }; } /* ==================== 工具函数(保留) ==================== */ function showToastIn(doc, id, msg, css, dur) { try { var old = doc.getElementById(id); if (old) old.parentNode.removeChild(old); var t = doc.createElement('div'); t.id = id; t.textContent = msg; t.style.cssText = css; (doc.body || doc.documentElement).appendChild(t); setTimeout(function () { try { t.parentNode.removeChild(t); } catch (e) {} }, dur); } catch (e) {} } function toast(msg) { showToastIn(document, '__novel_reader_toast__', msg, 'position:fixed;top:16%;left:50%;transform:translateX(-50%);z-index:2147483647;background:rgba(0,0,0,.78);color:#fff;font-size:14px;line-height:1.5;padding:10px 18px;border-radius:20px;font-family:sans-serif;pointer-events:none;white-space:nowrap;', 2400); } /* ==================== 电脑站目录回退:m. → www. ==================== */ var _mSiteHost = ''; var _desktopHost = ''; var DESKTOP_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'; (function detectSiteVariant() { var host = ''; try { host = (window.location.hostname || '').toLowerCase(); } catch (e) {} if (/^m\.[a-z0-9-]+\.[a-z0-9-]/.test(host)) { _mSiteHost = host; _desktopHost = 'www.' + host.slice(2); } })(); function toDesktopUrl(url) { if (!url || !_mSiteHost) return url; try { var u = new URL(url); if ((u.hostname || '').toLowerCase() === _mSiteHost) { u.hostname = _desktopHost; return u.href; } } catch (e) {} return url; } function toMobileUrl(url) { if (!url || !_desktopHost) return url; try { var u = new URL(url); if ((u.hostname || '').toLowerCase() === _desktopHost) { u.hostname = _mSiteHost; return u.href; } } catch (e) {} return url; } /* ==================== 乱码识别 ==================== */ function normalizeCharsetName(charset) { if (!charset) return ''; var s = String(charset).trim().toLowerCase(); if (/^(gb2312|gbk|x-gbk|gb_2312-80|cp936|windows-936)$/.test(s)) return 'gb18030'; if (/^(utf8|utf-8)$/.test(s)) return 'utf-8'; if (/^(big5|big-5)$/.test(s)) return 'big5'; return s; } function charsetFromHeaders(headers) { var raw = String(headers || ''); var m = raw.match(/content-type\s*:[^\r\n]*charset\s*=\s*["']?([^;\s"']+)/i) || raw.match(/charset\s*=\s*["']?([^;\s"']+)/i); return normalizeCharsetName(m && m[1]); } function charsetFromMeta(bytes) { try { var head = new TextDecoder('windows-1252').decode(bytes.slice(0, Math.min(bytes.byteLength, 16384))); var m = head.match(/]+charset\s*=\s*["']?\s*([^\s"'/>;]+)/i) || head.match(/]+content\s*=\s*["'][^"']*charset\s*=\s*([^\s"';>]+)/i); return normalizeCharsetName(m && m[1]); } catch (e) { return ''; } } function mojibakeScore(text) { var value = String(text || ''); if (!value) return 999999; var replacements = (value.match(/�/g) || []).length; var typical = (value.match(/锟斤拷|鏂囧瓧|绔犺妭|鐩綍|浣滆€?/g) || []).length; var controls = (value.match(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g) || []).length; var chinese = (value.match(/[\u3400-\u9fff]/g) || []).length; return replacements * 80 + typical * 40 + controls * 20 - Math.min(chinese, 300) * 0.05; } // 探针式解码:先用前 8KB 判断编码,再全文只解码一次 function decodeHTMLBytes(arrayBuffer, headers, url) { var bytes = new Uint8Array(arrayBuffer || new ArrayBuffer(0)); if (!bytes.byteLength) return ''; var host = ''; try { host = new URL(url, location.href).hostname; } catch (e) {} var detected = charsetFromHeaders(headers) || charsetFromMeta(bytes); var probe = bytes.subarray(0, Math.min(bytes.byteLength, 8192)); if (detected) { try { var probeText = new TextDecoder(detected, { fatal: false }).decode(probe); if (mojibakeScore(probeText) < 20) { return new TextDecoder(detected, { fatal: false }).decode(bytes); } } catch (e) {} } var candidates = []; function push(value) { value = normalizeCharsetName(value); if (value && candidates.indexOf(value) === -1) candidates.push(value); } if (/(^|\.)piaotia\.com$/i.test(host)) { if (detected === 'utf-8') push('utf-8'); else push('gb18030'); } push(detected); push('utf-8'); push('gb18030'); push('big5'); var bestCodec = 'utf-8', bestScore = Infinity; for (var i = 0; i < candidates.length; i++) { try { var t = new TextDecoder(candidates[i], { fatal: false }).decode(probe); var s = mojibakeScore(t); if (s < bestScore) { bestScore = s; bestCodec = candidates[i]; } } catch (e) {} } try { return new TextDecoder(bestCodec, { fatal: false }).decode(bytes); } catch (e) { return new TextDecoder('utf-8', { fatal: false }).decode(bytes); } } function decodeResponseText(res, url) { try { if (res && res.response && typeof res.response === 'object' && typeof res.response.byteLength === 'number') { return decodeHTMLBytes(res.response, res.responseHeaders || '', url); } } catch (e) {} return (res && (res.responseText || res.response)) || ''; } function fetchUrl(url, cb) { try { return GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'arraybuffer', timeout: 20000, headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' }, onload: function (r) { cb(null, decodeResponseText(r, url)); }, onerror: function () { cb('网络错误'); }, ontimeout: function () { cb('请求超时'); } }); } catch (e) { cb(String(e)); return null; } } function fetchUrlDesktop(url, cb) { try { return GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'arraybuffer', timeout: 20000, headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'User-Agent': DESKTOP_UA }, onload: function (r) { cb(null, decodeResponseText(r, url)); }, onerror: function () { cb('网络错误'); }, ontimeout: function () { cb('请求超时'); } }); } catch (e) { cb(String(e)); return null; } } function parseHtml(html, baseUrl) { var doc = new DOMParser().parseFromString(html, 'text/html'); try { var baseEl = document.createElement('base'); baseEl.href = baseUrl; doc.head.appendChild(baseEl); } catch (e) {} return doc; } function htmlToText(html) { if (!html) return ''; try { var doc = new DOMParser().parseFromString('1. 整句清除:正文出现净化词,就把它连着的整句删掉——词前面连着的字、词本身、后面连着的字和句末标点(。!?…)全清;前后若遇到标点就停。例:删\"广告\",句子\"本书由广告赞助。\"整句消失。
\n2. 一次加多个词:用、隔开即可。例:广告、加群、扫码。
\n3. 只想删词、不误删旁边的字:在词前或词后加/。词前加/(如/广告)=左边的字不误删;词后加/=右边的字和句末标点都不误删;两边都加(如/广告/)=只删\"广告\"二字,最保险。
\n4. 英文单词不怕被切开:夹在字母中间的/和.算单词的一部分(如 a/b、U.S.不会被切断)。
\n5. 删除词:点词条右侧的✕即可。
\n全书下载
\n