漫区卡片式预览封面,一键进入漫画阅读器,支持自动生成系列目录,智能匹配章节标题。
// ==UserScript== // @name Yamibo 漫画阅读器 // @namespace https://bbs.yamibo.com/ // @version 3.5.6 // @author hitori酱 // @description 漫区卡片式预览封面,一键进入漫画阅读器,支持自动生成系列目录,智能匹配章节标题。 // @match https://bbs.yamibo.com/thread-* // @match https://bbs.yamibo.com/forum.php?mod=viewthread&* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_listValues // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/cn2t.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/t2cn.js // @run-at document-end // @noframes // @license MIT License // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAB2AAAAdgB+lymcgAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAr3SURBVHic7Zt7cNTVFcc/v32H7Ca72c2LhEcS5JFIgUB5JSioQCvKWOVRqSMjxAeoVaYKdapFaccq0OfUOlrKjLUUR1HGUstDMSIJRAyEAAnQvNk8NgnJPvLYTbK7v/6xyYbFZMluNgaK35nfzO7v3HvOud/fueeee3+70D9UwO8AEyDeoJcJ+C2g7G+Qgh8CtgM/Cw+TExsdToXRglSQMDpS56fL9QVTa4vY3tUpANuAjX218UdATZhKFl+R84QQawhHMW4bMWEaqjf8akicHQqYWm0k/+kV7M6uWiChrzYSP/1jDboRQqwhfGi8+xYQp47AMEINENtfG38E3BT4joDhdmC48R0Bw+3AcOM7AobbgeHGTU+ALFSKutwudpw6zq6z+Zy/bEIqSEiNjuPhKTNZPWUmUsHDtUt0c6SylANl5/m69hIV5ibMjnYANAol4/UxPHjrdB5Nn+uj3yW6eafwBH8vPEFxowmX6GaSIY6fTJ5BVvoc5BJpUH6HhIC6Vhv37n6Lk3VGj1KpR+2RqlKOVJWysyCP3Q+s5r1zp/h9XjZ1rTaf/nKZHIlEQmtbCzUtVkrarNw2dxa3dEhxuVw029u5//0dHDNW+OjPNZaTayznbwXH2ffg48SrIwL2fdAEdLld3sEbIvWkJqehVUcCYLaZOVdeRK6xnLF/fBm3KCIIAjG6GGKjYoiLjic+Jg6pzPP03KKb1rZWVEoVr+cXoteqOfT5J5ytrERERKfRcWtKGjqNZ0NmabVSVF7EyTojS997m2NrNgQcCYPOATsL8jhZZ0QfqWf25NnewQPoInRERUZ1D04kITqB+em3M/vWWSQlJDEytnfwABJBQoQ6AoVcAUCTpRWUGkREr76ewQNo1ZHMmTwbfaSe/NpL7CzIC9j/QROw62w+AGnJqUgEX3UF/z1NWXUZcpmc70+awfSJ6WhGaABQKBRIpNd+WtPT0lmUsRCVUkV5TTkFFwt8ByBISEtO9fElEAyagPOXTcikMrRqre/9ygsY642oFCpumzqPeEO8j1yp6veM4htIiE3gnvlLUI9QY2yo5nzlBR+5Vq1FJpVR1FgXsP+DJkAplSGKbm+YAjSYGygxlqCQK5gzeQ7hYb5bakEQkMvlAdnRhGtYnLkIlVJFibGEBnODVyYiIooiKllgOiEEBEyKjsPldtNsNQPgcrkoLDkDwJRx30Pj2Y9TWFLIsbPHAZDKpAhC71nMl19/SfZX2X3qP3z8MLmncgGIUEeQOT3To6/0DC6XC4BmqxmX20VqdFzA/g+agDVTZwNQVF6Ey+2iylSFvcPOyOiR3rDvcnZxqd5Im70NANlVT8poqqa2oe/wNdvMlFSV0tnVCcCouESSEpOwO+xUmqpwuV0UlRcBkDVtTsD+D5qA5anTWDD2FiytFnJO51BaUwbAhFHjvW2arE2IoohBawBAJhu42ThDHKIoYrps8t5LT52GIAiU1ZSRczoHS6uFO5PGsyx1asD+D5oAmUTCRyuyuCt5AtY2G44OB9HaaDThGm+bZptnehgi9R6jA8j+PYg1eMK6oal3zkeoIxgZE4+jw4G1zcbC5InsWb7WW20GgpDsBbSqMA49tJ6Vaekep6NifOTtDk/oq7vzgUQycLNajaeuaGlr8bk/ZuQYAFampXPwoXVoVWFB+R6yzZCAgLXDDoC++0n3oK271g9XeVaDQAjQqD2RdDUBMXoPyRaHHcHv4bZ/hGwzBHCuO5H1ZPseOJ1OAA7nfw7gswIA3gS3a98/+9XdbDX3KT/bUBu8w4SQgPauTmpsVgRBQB1+VZEjqPz2De//xY1fudPmpLbFSmtnB2rFwAurKxEyAsrNTYiIhCkVNOb7PilppCFUZnygHnsHbe0OKixNTI4ZGZSOkOWAcvNlAOyOTiy2Nl+h2x0qM16YLS20tTu6bTcFrSd0BFh6nTDWNvrIPj1ygi3bd+Lo6AxYb0dnF1u27+Q/n/nmleq63mWxrJv8YBDyCAD47NhpH9k/PtjP5q07WLT8GQqLSr336xubudxs8X5vMlsxNfQSeaa4lIXLfsrmrTvY/dEhH52Hsk94P1cMIgJCmgN68K/PvmLDmvu833+z8REKz1dwNK+QqQseJiE+GplUSlW1ibQJSZw7uguAjCWPc7H0EmMS43C6XNTUeSJpSto4tr38lI+9jw982Wvbch1FQLwmgtyTxZy7WOWVxeo05H3yFtteforUCUnUNzZTVW0iZWwCjz3cS9STax5gXFIiVdUm6hubSZ2QxNbNT5J3YAdxMb21xdnzZeSeOOM9AitrDp6AkESAiEilpZnoEWp+nrGQZw58yAvb32HfX3/pbaHAxXPrV/Hc+lU4nS5cbjdKhe+m6Oms5TydtZyOzi6kEgkyWd8l88ZX3sDtFnkhcxG/PnqQSkszblFEIgReEIUkAmpbbNidXSTp9DyaPpcx2ij2f5HPRwePeduITgeiy1MQyWTSbwz+SigV8n4Hv2dfNgc+z2NMZBRZ6XNI1unpcDmpabH02f5aCAkBPeGfojMQJpOz895VCILAmo1/4OyFSk8jEdztNhDF/hVdA8UXK1j77KsIgsDb9/yYMJmc5O4dZrCJMEQEeIwnaT3z9I6k8bw4bzGt7Q7ufXQLZy54jrNxu3G3twCBk1BYVMoPVm7A1tLGS/MWsyhlIgDJOo/NYJfCkEZAjzMAW+bfzePTM6g2Xea2lZu800F0duJuawkoEvbsyyZjyWMYaxt4Ynomr8y/2yvrsRlsMRTSCEjR+Za8by5ZwWt3LqXd3sGKp15j8eqXOF1cjujsxNVqQXR1+dVbfLGCFVkvsnztL2i3d7Ap4y7euHu5T5vkbpsVluAICMkq0LMOJ+l8t8ECApsy7mJaXCJP7v+Aw8cKmfmjDcxJn8TSO2exMHMqiaNGYoiLRZDIaDJbqa5t5NMjJ/h4/1GOfX0Gt1tkXFQ0f/7hMhanTPqG7cFOgZAQUGFuQiGVkhih7VO+KGUi59a9wJv5ObyZn0NufjG5+cVset0jVyk9L0KuLpXH62NYNyOTdTMyUUr7djVBo0UplQWdBENCQOboFEbI5X6PpJRSGc/Oms8zs24n51I52ZUl5Fwqp9pmpsnuOTBJ0ugYFakjY1QyC8beQubo5GsedkgEgVWTZ9Bkb/Pbrj+EhID3lz0y4LYCAvNGpzBvdEooTAOwc+mqoPve9L8PuOkJGPAUWLJgHNFNQ3OyM5wYMAF7/3I/vBvcsdP1jJt+CgyYgNUb/83T+/cMpS/DggETsHtfMXsvFA6lL8OC/+spUNNipbG9VQT6rZP9lVlOlVImnTROj7Wl44b8x0iNzUqH5xBmO/B8oP2dDP9/fgZ7uYGt+Fnt/EaAUiGXnjnyLuNTRl+LrOsOo6beR3VtgxPw+7sZvzkgJjrqhhx8IBhwIbT22Vf5IvfUUPoSUlz5gsUfBkxAfWMz5VWDexV9PcJvDhiVECu9VLD3GwJV4u1EaSTU7p3ncz98UTZqdQT1xZ+E2M3AEYocUH+5yUJdffDv3W4E+JsCu+yOjudTZi4T42P0PpHS2eUEFNdUvn7jdg5mB/773VCgOwdIgR1AVjA6lHj+clpDH2tsvF4plr2XId4z1yAumx8jGj+cJ45QSkVBEBpEy1GdaDmqUyrln/bV91u+9gcz+GvBoZRLxPAwqbvHUKRa5pZKBBGoHwqD1xscgChAF7AZeJ3eyvGmIOAL4AQw5Yp7s4BC4OPhcChY/A/fkn/A85DrtAAAAABJRU5ErkJggg== // ==/UserScript== (function() { 'use strict'; let isReading = false; let images = []; let bgIsBlack = true; let zoomLevel = 35; // 默认缩放比例35% let autoReaderEnabled = false; // 全局阅读器开关 // 目录识别相关变量 let seriesDirectory = []; let currentSeriesKey = ''; let isDirectoryMode = false; let savedThreadTitle = ''; let originalSeriesTitle = ''; let searchCache = new Map(); let directoryMemoryCache = new Map(); // 保存原始页面HTML let originalPageHTML = ''; let originalPageTitle = ''; // 事件处理与状态 let readerEventHandlers = {}; let readerEventsBound = false; let readerToolsTimer = null; let unbindReaderEvents = null; // 缓存前缀与过期时长 const CACHE_PREFIX = 'yamibo-directory-cache-'; const CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 持久化缓存索引键(用于 GM_* 环境) const GM_INDEX_KEY = CACHE_PREFIX + 'gm-index'; // 通用存储封装:优先使用 GM_getValue/GM_setValue/GM_deleteValue(若环境支持),否则降级到 sessionStorage function storageGet(key, defaultVal = null) { try { if (typeof GM_getValue === 'function') { let val; try { val = GM_getValue(key, undefined); } catch (e) { val = undefined; } if (val === undefined) { } else if (val === null) { return defaultVal; } else if (typeof val === 'string') { try { return JSON.parse(val); } catch (e) { return val; } } else { return val; } } } catch (e) { } try { const raw = sessionStorage.getItem(key); return raw ? JSON.parse(raw) : defaultVal; } catch (e) { return defaultVal; } } function storageSet(key, value) { try { if (typeof GM_setValue === 'function') { try { GM_setValue(key, value); } catch (e) { try { GM_setValue(key, JSON.stringify(value)); } catch (ee) { throw ee; } } try { let idx = GM_getValue(GM_INDEX_KEY, undefined); if (idx === undefined || idx === null) idx = []; if (typeof idx === 'string') { try { idx = JSON.parse(idx); } catch (e) { idx = []; } } if (!Array.isArray(idx)) idx = []; if (!idx.includes(key)) { idx.push(key); GM_setValue(GM_INDEX_KEY, idx); } } catch (e) { } return true; } } catch (e) { } try { sessionStorage.setItem(key, JSON.stringify(value)); return true; } catch (e) { console.warn('⚠️ storageSet 失败:', e); return false; } } function storageRemove(key) { try { if (typeof GM_deleteValue === 'function') { try { GM_deleteValue(key); } catch (e) { try { GM_setValue(key, null); } catch (ee) {} } try { let idx = GM_getValue(GM_INDEX_KEY, undefined); if (typeof idx === 'string') { try { idx = JSON.parse(idx); } catch (e) { idx = []; } } if (!Array.isArray(idx)) idx = []; const newIdx = idx.filter(k => k !== key); GM_setValue(GM_INDEX_KEY, newIdx); } catch (e) { } return true; } } catch (e) { } try { sessionStorage.removeItem(key); return true; } catch (e) { return false; } } // 清理过期缓存:同时支持 sessionStorage 前缀与 GM 索引 function cleanExpiredCache() { let cleanedCount = 0; // 1) 清理 sessionStorage 前缀缓存 try { const sKeys = Object.keys(sessionStorage).filter(k => k.startsWith(CACHE_PREFIX)); sKeys.forEach(key => { try { const raw = sessionStorage.getItem(key); if (!raw) return; const obj = JSON.parse(raw); const ts = obj.ts || obj.timestamp || 0; if (Date.now() - ts > CACHE_EXPIRY) { sessionStorage.removeItem(key); cleanedCount++; } } catch (e) { try { sessionStorage.removeItem(key); cleanedCount++; } catch (ee) {} } }); } catch (e) { } // 2) 清理 GM_* 存储(使用索引枚举) try { if (typeof GM_getValue === 'function' && typeof GM_setValue === 'function') { let idx = GM_getValue(GM_INDEX_KEY, undefined); if (typeof idx === 'string') { try { idx = JSON.parse(idx); } catch (e) { idx = []; } } if (!Array.isArray(idx)) idx = []; const remaining = []; for (const key of idx) { try { const stored = storageGet(key, null); if (!stored) continue; const ts = stored.ts || stored.timestamp || 0; if (Date.now() - ts > CACHE_EXPIRY) { storageRemove(key); cleanedCount++; } else { remaining.push(key); } } catch (e) { try { storageRemove(key); cleanedCount++; } catch (ee) {} } } // 更新索引为剩余项 try { GM_setValue(GM_INDEX_KEY, remaining); } catch (e) {} } } catch (e) { } if (cleanedCount > 0) console.log('🧹 清理了', cleanedCount, '个过期缓存 (session+GM)'); return cleanedCount; } // 在配额不足或需要收缩时清理最旧的缓存(同时支持 sessionStorage 与 GM) function cleanOldestCache() { try { const entries = []; // 收集 sessionStorage 条目 try { Object.keys(sessionStorage).forEach(key => { if (!key.startsWith(CACHE_PREFIX)) return; try { const obj = JSON.parse(sessionStorage.getItem(key)); const ts = obj.ts || obj.timestamp || 0; entries.push({ key, ts, source: 'session' }); } catch (e) { entries.push({ key, ts: 0, source: 'session' }); } }); } catch (e) {} // 收集 GM_* 条目 via index try { if (typeof GM_getValue === 'function') { let idx = GM_getValue(GM_INDEX_KEY, undefined); if (typeof idx === 'string') { try { idx = JSON.parse(idx); } catch (e) { idx = []; } } if (!Array.isArray(idx)) idx = []; for (const key of idx) { try { const stored = storageGet(key, null); const ts = stored ? (stored.ts || stored.timestamp || 0) : 0; entries.push({ key, ts, source: 'gm' }); } catch (e) { entries.push({ key, ts: 0, source: 'gm' }); } } } } catch (e) {} if (entries.length === 0) return 0; // 按时间戳排序,最旧的在前 entries.sort((a, b) => (a.ts || 0) - (b.ts || 0)); const toDelete = Math.ceil(entries.length / 2); for (let i = 0; i < toDelete; i++) { const e = entries[i]; try { if (e.source === 'session') sessionStorage.removeItem(e.key); else storageRemove(e.key); } catch (err) {} } return toDelete; } catch (e) { console.warn('cleanOldestCache failed', e); return 0; } } // 检查是否启用了全局阅读器模式 function checkAutoReaderStatus() { const stored = localStorage.getItem('yamibo-auto-reader'); return stored === 'true'; } // 设置全局阅读器模式 function setAutoReaderStatus(enabled) { autoReaderEnabled = enabled; localStorage.setItem('yamibo-auto-reader', enabled.toString()); console.log('🔧 全局阅读器模式:', enabled ? '开启' : '关闭'); } /*** 标题标准化函数 ***/ function normalizeSeriesTitle(rawTitle) { if (!rawTitle) return ''; // 首先清理页面title的后缀 let title = rawTitle; if (title.includes(' - ')) { title = title.split(' - ')[0].trim(); } // 移除页数标注 title = title.replace(/[((]\s*\d+\s*p\s*[))]\s*$/i, ''); // 移除章节标识 - 参考脚本的模式 title = title.replace(/第[\d一二三四五六七八九十百千万〇零兩两1234567890\.]+[话話章节節回卷篇]/gi, ' '); title = title.replace(/\d+(?:\.\d+)?\s*[话話章节節回卷篇]/gi, ' '); title = title.replace(/\d+(?:\.\d+)?\s*[上下前后前後左右中篇部卷期全完]+(?:\s*[++&和及與并並,,/]\s*[上下前后前後左右中篇部卷期全完]+)+/gi, ' '); title = title.replace(/[((][上下前后前後中全完]+(?:\s*[,,++&和及與并並/]\s*[上下前后前後中全完]+)*[))]/g, ' '); title = title.replace(/\d+(?:\.\d+)?\s*[上下前后前後左右中篇部卷期全完]+/gi, ' '); title = title.replace(/(?:\s+|[-‐‑‒–—―-~~·•_、::])?\d+(?:\.\d+)*(?:\s*[上下前后前後左右中篇部卷期話话节節全完])?\s*$/g, ' '); title = title.replace(/[--—–~~\u2013\u2014\s]+$/g, ' '); title = title.replace(/[\[\]【】()()]/g, ' '); title = title.replace(/\s+/g, ' ').trim(); if (!title) { return rawTitle.trim(); } return title; } function buildSeriesKey(title) { const normalized = normalizeSeriesTitle(title || ''); const base = normalized || (title || '').trim(); return base.toLowerCase(); } function generateSeriesKey(title) { try { const k = buildSeriesKey(title || ''); // 限制长度并移除特殊字符,避免存储键过长或包含非法字符 return k.replace(/[\s\/:\\#\?&=\+%\*\|<>"'`]/g, '-').substring(0, 120); } catch (e) { return String(title || '').toLowerCase().substring(0, 120); } } // 统一的排除词汇列表,避免重复定义 const EXCLUDE_WORDS = [ '个人汉化', '汉化组', '汉化', '个人翻译', '個人翻譯', '个汉', '翻译', '生肉', '未翻译', '填坑组', '粮食组', '保护协会','創作百合', '创作百合', '熟肉', '字幕', '工作室', '提灯喵', '社团', '官方中字', '出版', '汉化工房', '猫岛汉化组', '和菓子漫画屋', '大友同好會', '透明声彩汉化组', '翻译组', '汉化委员会', '渣翻渣嵌', '合作汉化', '完结','连载','短篇', '合集','同人','raw','韩漫','杂志','英译','单行本','插画','特典','番外','外传','彩页','合订本' ].map(s => s.toLowerCase()); // 统一的文本过滤函数:检查是否为噪音词汇 function isNoiseText(text) { if (!text || typeof text !== 'string') return true; const t = text.toLowerCase(); // 检查是否包含排除词汇 for (const word of EXCLUDE_WORDS) { if (t.includes(word)) return true; } // 常见格式判断:纯数字视为噪音 if (/^\d+$/.test(t)) return true; // 智能长度检查:CJK字符2个即有效,其他字符3个以上有效 const hasCJK = /[\u4e00-\u9fa5\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF]/.test(text); const minLength = hasCJK ? 2 : 3; if (t.length < minLength) return true; return false; } // 保持向后兼容的 isGenericTag 函数 function isGenericTag(text) { return isNoiseText(text); } function getCachedDirectory(seriesKey) { // 🚫 临时禁用缓存读取,强制测试双语解析逻辑 console.log('🔄 缓存读取已禁用,将执行新的双语解析逻辑'); return null; if (!seriesKey) return null; // 1) 先检查内存缓存 try { if (directoryMemoryCache && directoryMemoryCache.has(seriesKey)) { const cached = directoryMemoryCache.get(seriesKey); if (cached && cached.ts && (Date.now() - cached.ts) < CACHE_EXPIRY) { return cached.data || cached.d || null; } else { directoryMemoryCache.delete(seriesKey); } } } catch (e) { } const key = CACHE_PREFIX + seriesKey; // 2) 尝试 GM_getValue try { if (typeof GM_getValue === 'function') { let stored = GM_getValue(key, undefined); if (typeof stored === 'string') { try { stored = JSON.parse(stored); } catch (e) { /* leave as-is */ } } if (stored && (stored.d || stored.data)) { const ts = stored.ts || stored.timestamp || 0; if (Date.now() - ts < CACHE_EXPIRY) { try { directoryMemoryCache.set(seriesKey, { d: stored.d || stored.data, data: stored.d || stored.data, ts: ts || Date.now() }); } catch (e) {} return stored.d || stored.data; } else { try { storageRemove(key); } catch (e) {} return null; } } } } catch (e) { } // 3) 尝试 sessionStorage try { const raw = sessionStorage.getItem(key); if (raw) { let obj = raw; try { obj = JSON.parse(raw); } catch (e) { obj = raw; } if (obj && (obj.d || obj.data)) { const ts = obj.ts || obj.timestamp || 0; if (Date.now() - ts < CACHE_EXPIRY) { try { directoryMemoryCache.set(seriesKey, { d: obj.d || obj.data, data: obj.d || obj.data, ts: ts || Date.now() }); } catch (e) {} return obj.d || obj.data; } else { try { sessionStorage.removeItem(key); } catch (e) {} return null; } } } } catch (e) { } return null; } function setCachedDirectory(seriesKey, directory) { if (!seriesKey || !directory) return false; const key = CACHE_PREFIX + seriesKey; const payload = { ts: Date.now(), data: directory, d: directory }; try { directoryMemoryCache.set(seriesKey, { d: directory, data: directory, ts: payload.ts }); } catch (e) {} try { if (typeof GM_setValue === 'function') { try { GM_setValue(key, payload); } catch (e) { try { GM_setValue(key, JSON.stringify(payload)); } catch (ee) { /* ignore */ } } return true; } } catch (e) { } try { sessionStorage.setItem(key, JSON.stringify(payload)); return true; } catch (e) { console.warn('⚠️ setCachedDirectory 写入失败:', e); return false; } } /*** 相似度计算辅助函数 ***/ // 简繁体标准化函数 - 使用 OpenCC 库 let converter = null; function normalizeChineseVariants(text) { try { // 初始化转换器(繁体转简体) if (!converter && typeof OpenCC !== 'undefined') { converter = OpenCC.Converter({ from: 'tw', to: 'cn' }); } if (converter) { return converter(text); } return text; } catch (error) { console.warn('简繁转换失败,使用原文本:', error); return text; } } // 提取有意义的关键词(排除常见无关词汇) function extractMeaningfulKeywords(title) { const keywords = []; // 1. 检查明显的中外对照分隔符 const dualMatch = title.match(/^(.+?)[\s/||/()()]+(.+?)$/); if (dualMatch) { const part1 = dualMatch[1].trim(); const part2 = dualMatch[2].trim(); if (part1 && part2 && part1 !== part2) { keywords.push(part1, part2); return [...new Set(keywords)]; } } // 2. 没有分隔符的中外混合,允许拆分 const enZhMix = title.match(/([A-Za-z][A-Za-z0-9\.\*\s]*[A-Za-z])([\u4e00-\u9fa5]{2,})/); if (enZhMix) { const en = enZhMix[1].trim(); const zh = enZhMix[2].trim(); if (!isNoiseText(en)) keywords.push(en); if (!isNoiseText(zh)) keywords.push(zh); } // 3. 继续原有的中文、日文、韩文、英文多词短语等提取 const chineseMatches = title.match(/[\u4e00-\u9fa5]{2,8}/g); if (chineseMatches) { chineseMatches.forEach(match => { if (!isNoiseText(match)) keywords.push(match); }); } const japaneseMatches = title.match(/[\u3040-\u309F\u30A0-\u30FF]{2,}/g); if (japaneseMatches) { japaneseMatches.forEach(match => { if (!isNoiseText(match)) keywords.push(match); }); } const koreanMatches = title.match(/[\uAC00-\uD7AF]{2,}/g); if (koreanMatches) { koreanMatches.forEach(match => { if (!isNoiseText(match)) keywords.push(match); }); } const englishMatches = title.match(/[A-Za-z]{3,}(?:\s+[A-Za-z]{2,})+/g); if (englishMatches) { englishMatches.forEach(match => { const cleanMatch = match.trim(); if (cleanMatch.length >= 5 && !isNoiseText(cleanMatch)) { keywords.push(cleanMatch); } }); } const singleEnglishMatches = title.match(/\b[A-Za-z]{5,}\b/g); if (singleEnglishMatches) { singleEnglishMatches.forEach(match => { if (keywords.some(k => k !== match && k.includes(match))) return; if (!isNoiseText(match)) { keywords.push(match); } }); } // 去重,且如果有整体词则不保留其子词 const finalKeywords = []; for (const kw of keywords) { if (finalKeywords.some(k => k.length > kw.length && k.includes(kw))) continue; finalKeywords.push(kw); } return [...new Set(finalKeywords)]; } // 计算核心作品名相似度 function calculateCoreWorkSimilarity(core1, core2) { // 🔧 特殊处理:对于英文为主的标题,使用更严格的匹配 const isEnglishDominant1 = /[A-Za-z]/.test(core1) && !/[\u4e00-\u9fa5\u3040-\u309F\u30A0-\u30FF]/.test(core1); const isEnglishDominant2 = /[A-Za-z]/.test(core2) && !/[\u4e00-\u9fa5\u3040-\u309F\u30A0-\u30FF]/.test(core2); if (isEnglishDominant1 && isEnglishDominant2) { // 对于英文为主的标题,进行词级别的严格匹配 const words1 = core1.toLowerCase().replace(/[^\w\s]/g, ' ').split(/\s+/).filter(w => w.length > 0 && /[a-z]/.test(w)); const words2 = core2.toLowerCase().replace(/[^\w\s]/g, ' ').split(/\s+/).filter(w => w.length > 0 && /[a-z]/.test(w)); console.log(` 🔤 英文词汇对比: [${words1.join(', ')}] vs [${words2.join(', ')}]`); // 计算词汇重叠度 - 更严格的匹配逻辑 const uniqueWords1 = [...new Set(words1)]; const uniqueWords2 = [...new Set(words2)]; const commonUniqueWords = uniqueWords1.filter(w => uniqueWords2.includes(w)); if (commonUniqueWords.length === 0) { console.log(` ❌ 英文标题无共同词汇,相似度为0`); return 0; } // 🎯 严格匹配:要求绝大部分唯一词汇都匹配 const overlapRatio1 = commonUniqueWords.length / uniqueWords1.length; const overlapRatio2 = commonUniqueWords.length / uniqueWords2.length; const minOverlapRatio = Math.min(overlapRatio1, overlapRatio2); console.log(` 📊 唯一词汇: [${uniqueWords1.join(', ')}] vs [${uniqueWords2.join(', ')}]`); console.log(` 📊 共同词汇: [${commonUniqueWords.join(', ')}]`); console.log(` 📊 重叠率: ${(overlapRatio1 * 100).toFixed(1)}% & ${(overlapRatio2 * 100).toFixed(1)}% (最小: ${(minOverlapRatio * 100).toFixed(1)}%)`); // 🎯 更严格的匹配标准 if (minOverlapRatio >= 0.8) { return 0.95; // 高匹配度:两边80%以上词汇重叠 } else if (minOverlapRatio >= 0.6) { return 0.7; // 中等匹配度:两边60%以上词汇重叠 } else { console.log(` ❌ 英文词汇重叠度不足 (${(minOverlapRatio * 100).toFixed(1)}% < 60%),认为不相似`); return 0; // 重叠度不足 } } // 标准化处理(包括简繁体转换) const clean1 = normalizeChineseVariants(core1.replace(/[^\u4e00-\u9fa5\u3040-\u309F\u30A0-\u30FFa-zA-Z0-9]/g, '')).toLowerCase(); const clean2 = normalizeChineseVariants(core2.replace(/[^\u4e00-\u9fa5\u3040-\u309F\u30A0-\u30FFa-zA-Z0-9]/g, '')).toLowerCase(); if (!clean1 || !clean2) return 0; console.log(` 🔤 简繁体标准化: "${clean1}" vs "${clean2}"`); // 1. 完全匹配 if (clean1 === clean2) return 1.0; // 2. 包含关系 if (clean1.includes(clean2) || clean2.includes(clean1)) { const longer = clean1.length > clean2.length ? clean1 : clean2; const shorter = clean1.length <= clean2.length ? clean1 : clean2; return shorter.length / longer.length * 0.95; // 包含关系给0.95分 } // 3. 计算最长公共子串 let maxCommonLength = 0; for (let i = 0; i < clean1.length; i++) { for (let j = 0; j < clean2.length; j++) { let commonLength = 0; while (i + commonLength < clean1.length && j + commonLength < clean2.length && clean1[i + commonLength] === clean2[j + commonLength]) { commonLength++; } maxCommonLength = Math.max(maxCommonLength, commonLength); } } // 4. 基于最长公共子串计算相似度 const minLength = Math.min(clean1.length, clean2.length); if (maxCommonLength >= 4) { // 至少4个字符相同才认为有相似性 const similarity = (maxCommonLength / minLength) * 0.9; // 最高0.9分 return similarity; } return 0; } // 检查关键词匹配(排除翻译组等无关信息) function checkKeywordMatch(core1, core2) { // 提取有意义的关键词片段(4字符以上) const keywords1 = extractMeaningfulKeywords(core1); const keywords2 = extractMeaningfulKeywords(core2); console.log(` 🔍 关键词提取: "${keywords1.join('", "')}" vs "${keywords2.join('", "')}"`); let bestMatch = 0; // 检查直接关键词匹配 for (const kw1 of keywords1) { for (const kw2 of keywords2) { if (kw1.length >= 2 && kw2.length >= 2) { if (kw1 === kw2) { console.log(` 🎯 完全匹配: "${kw1}"`); return 0.95; // 完全匹配给高分 } else if (kw1.includes(kw2) || kw2.includes(kw1)) { const longer = kw1.length > kw2.length ? kw1 : kw2; const shorter = kw1.length <= kw2.length ? kw1 : kw2; const match = shorter.length / longer.length; bestMatch = Math.max(bestMatch, match * 0.9); console.log(` 🎯 包含匹配: "${kw1}" ⟷ "${kw2}" (${(match * 90).toFixed(1)}%)`); } } } } // 如果核心1和核心2都是字符串对象且包含双语信息,进行交叉匹配 if (typeof core1 === 'object' && core1._dualLanguage && typeof core2 === 'object' && core2._dualLanguage) { // 双语对双语:交叉匹配所有组合 const pairs = [ [core1._dualLanguage.chinese, core2._dualLanguage.chinese], [core1._dualLanguage.foreign, core2._dualLanguage.foreign], [core1._dualLanguage.chinese, core2._dualLanguage.foreign], [core1._dualLanguage.foreign, core2._dualLanguage.chinese] ]; for (const [term1, term2] of pairs) { if (term1 && term2) { const kw1Set = extractMeaningfulKeywords(term1); const kw2Set = extractMeaningfulKeywords(term2); for (const kw1 of kw1Set) { for (const kw2 of kw2Set) { if (kw1.length >= 2 && kw2.length >= 2) { if (kw1 === kw2 || kw1.includes(kw2) || kw2.includes(kw1)) { console.log(` 🌐 双语交叉匹配: "${kw1}" ⟷ "${kw2}"`); return 0.92; // 双语匹配给高分 } } } } } } } else if (typeof core1 === 'object' && core1._dualLanguage) { // 双语对单语:用双语的两个部分分别匹配单语 const singleTerms = extractMeaningfulKeywords(core2.toString()); const dualTerms = [ ...extractMeaningfulKeywords(core1._dualLanguage.chinese || ''), ...extractMeaningfulKeywords(core1._dualLanguage.foreign || '') ]; for (const dTerm of dualTerms) { for (const sTerm of singleTerms) { if (dTerm.length >= 2 && sTerm.length >= 2) { if (dTerm === sTerm) { console.log(` 🎯 双语单语完全匹配: "${dTerm}"`); return 0.95; } else if (dTerm.includes(sTerm) || sTerm.includes(dTerm)) { const longer = dTerm.length > sTerm.length ? dTerm : sTerm; const shorter = dTerm.length <= sTerm.length ? dTerm : sTerm; const match = shorter.length / longer.length; bestMatch = Math.max(bestMatch, match * 0.9); console.log(` 🎯 双语单语包含匹配: "${dTerm}" ⟷ "${sTerm}" (${(match * 90).toFixed(1)}%)`); } } } } } else if (typeof core2 === 'object' && core2._dualLanguage) { // 单语对双语:用单语匹配双语的两个部分 const singleTerms = extractMeaningfulKeywords(core1.toString()); const dualTerms = [ ...extractMeaningfulKeywords(core2._dualLanguage.chinese || ''), ...extractMeaningfulKeywords(core2._dualLanguage.foreign || '') ]; for (const sTerm of singleTerms) { for (const dTerm of dualTerms) { if (sTerm.length >= 2 && dTerm.length >= 2) { if (sTerm === dTerm) { console.log(` 🎯 单语双语完全匹配: "${sTerm}"`); return 0.95; } else if (sTerm.includes(dTerm) || dTerm.includes(sTerm)) { const longer = sTerm.length > dTerm.length ? sTerm : dTerm; const shorter = sTerm.length <= dTerm.length ? sTerm : dTerm; const match = shorter.length / longer.length; bestMatch = Math.max(bestMatch, match * 0.9); console.log(` 🎯 单语双语包含匹配: "${sTerm}" ⟷ "${dTerm}" (${(match * 90).toFixed(1)}%)`); } } } } } return bestMatch; } // 提取核心作品名 function extractCoreWorkName(title) { console.log(` 🔍 优化版标题处理: "${title}"`); if (!title || typeof title !== 'string') return ''; // 1. 移除开头的【】[]标签和紧跟的()() let step1 = title .replace(/^(\s*【[^】]*】\s*)+/g, '') // 移除开头【】 .replace(/^(\s*\[[^\]]*\]\s*)+/g, '') // 移除开头[] .replace(/^(\s*[\[【((][^\]】))]*[\]】))]\s*)+/g, '') // 移除开头的所有括号 .replace(/^[\]】))]\s*/, '') // 清理可能残留的右括号 .trim(); // 处理《》书名号 if (step1.includes('《') && step1.includes('》')) { const bookMatch = step1.match(/《([^》]+)》/); if (bookMatch) { const bookTitle = bookMatch[1].trim(); step1 = step1.replace(/《[^》]+》/, bookTitle); // 替换《》为内容 console.log(` 📚 提取书名号内容: "${bookTitle}"`); } } console.log(` 🧹 移除开头标签和括号: "${step1}"`); // 2. 处理|分隔的双语结构 if (step1.includes('|')) { const parts = step1.split('|').map(p => p.trim()); console.log(` 🌐 检测到|分隔双语: [${parts.map(p => `"${p}"`).join(', ')}]`); const validParts = parts.filter(p => p.length > 0); if (validParts.length > 0) { // 返回双语对象结构 const result = new String(validParts[0]); if (validParts.length > 1) { result._dualLanguage = { chinese: validParts.find(p => /[\u4e00-\u9fa5]/.test(p)) || validParts[0], foreign: validParts.find(p => !/[\u4e00-\u9fa5]/.test(p)) || validParts[1] }; } return result; } } // 3. 特殊处理:提取末尾的日文标题 const endJapaneseMatch = step1.match(/【[^】]*】([ひらがなカタカナ\u3040-\u309F\u30A0-\u30FF\u4e00-\u9fa5ー~〜]+)$/); if (endJapaneseMatch) { const japaneseTitle = endJapaneseMatch[1].trim(); console.log(` 🎌 提取末尾日文标题: "${japaneseTitle}"`); return japaneseTitle; } // 4. 清理尾部的汉化组信息 step1 = step1.replace(/\s*(kakukuroi|汉化组|个人汉化|翻译组|汉化|翻译).*$/i, '').trim(); // 5. 处理/分隔的多个关键词 if (step1.includes('/') && !step1.includes('【') && !step1.includes('】')) { const slashMatch = step1.match(/^([^\/【】]+)\s*\/\s*([^\/【】]+?)(?:\s+\d+)?$/); if (slashMatch) { const part1 = slashMatch[1].trim(); const part2 = slashMatch[2].trim(); if (!part1.includes('同人志') && !part2.includes('【')) { console.log(` 🔀 检测到/分隔关键词: "${part1}" / "${part2}"`); const result = new String(part1); result._dualLanguage = { chinese: part1, foreign: part2 }; return result; } } } // 6. 检测章节分隔,确定主体书名范围 let mainContent = step1; // 策略A: 匹配明确的章节标记 const explicitChapterMatch = step1.match(/^(.+?)\s*(?:第?\d+(?:\.\d+)?\s*[话話章节節回卷篇期]|番外|外传|omake|extra|短篇|\d+\s*「)/i); if (explicitChapterMatch) { mainContent = explicitChapterMatch[1].trim(); console.log(` 📖 检测到明确章节标记,主体书名: "${mainContent}"`); } else { // 策略B: 检测各种隐式章节模式 let chapterDetected = false; // B1: "作品名+数字+空格+小标题" const spaceChapterMatch = step1.match(/^([\u4e00-\u9fa5\u3040-\u309F\u30A0-\u30FFA-Za-z\s]+?)(\d{1,3})\s+(.+)$/); if (spaceChapterMatch && !chapterDetected) { const baseName = spaceChapterMatch[1].trim(); const chapterNum = spaceChapterMatch[2]; const subtitle = spaceChapterMatch[3].trim(); // 验证这确实是章节模式:基础名应该是合理的作品名(至少3字符) if (baseName.length >= 3 && chapterNum.length <= 3) { mainContent = baseName; chapterDetected = true; console.log(` 📖 检测到空格分隔章节: 作品名"${baseName}" + 章节"${chapterNum}" + 小标题"${subtitle}"`); } } // B2: "作品名+数字+其他内容" const noSpaceChapterMatch = step1.match(/^([\u4e00-\u9fa5\u3040-\u309F\u30A0-\u30FFA-Za-z]+?)(\d{1,3})([\u4e00-\u9fa5\u3040-\u309F\u30A0-\u30FFA-Za-z].+)$/); if (noSpaceChapterMatch && !chapterDetected) { const baseName = noSpaceChapterMatch[1].trim(); const chapterNum = noSpaceChapterMatch[2]; const subtitle = noSpaceChapterMatch[3].trim(); // 更严格的验证:确保基础名至少4字符,且副标题不是纯数字 if (baseName.length >= 4 && chapterNum.length <= 3 && !/^\d+$/.test(subtitle)) { mainContent = baseName; chapterDetected = true; console.log(` 📖 检测到紧贴章节: 作品名"${baseName}" + 章节"${chapterNum}" + 小标题"${subtitle}"`); } } // B3: "作品名 + 空格 + 数字 + 小标题" const spaceBeforeChapterMatch = step1.match(/^([\u4e00-\u9fa5\u3040-\u309F\u30A0-\u30FFA-Za-z]+)\s+(\d{1,3})\s*(.*)$/); if (spaceBeforeChapterMatch && !chapterDetected) { const baseName = spaceBeforeChapterMatch[1].trim(); const chapterNum = spaceBeforeChapterMatch[2]; const subtitle = spaceBeforeChapterMatch[3].trim(); // 验证:基础名合理,章节号不超过3位 if (baseName.length >= 3 && chapterNum.length <= 3) { mainContent = baseName; chapterDetected = true; console.log(` 📖 检测到前置空格章节: 作品名"${baseName}" + 章节"${chapterNum}" + 小标题"${subtitle}"`); } } } // 7. 移除版本号 mainContent = mainContent.replace(/(\d+(?:\.\d+)+)(?=\s|$|[\((【])/g, '').trim(); // 8. 检查中间的括号 const middleBrackets = mainContent.match(/[\[【((]([^\]】))]+)[\]】))]/g); let meaningfulBrackets = []; if (middleBrackets) { console.log(` 🔍 发现括号: ${middleBrackets.join(', ')}`); middleBrackets.forEach(bracket => { const content = bracket.slice(1, -1).trim(); // 移除括号内的版本号和感叹号 const cleanContent = content.replace(/\d+(?:\.\d+)+/g, '').replace(/[!!]$/, '').trim(); const hasJapanese = /[\u3040-\u309F\u30A0-\u30FF]/.test(cleanContent); const hasKorean = /[\uAC00-\uD7AF]/.test(cleanContent); const hasChinese = /[\u4e00-\u9fa5]/.test(cleanContent); const hasLongEnglish = /[A-Za-z]/.test(cleanContent) && cleanContent.length > 4; // 检查原始内容中是否包含有效的中文(去除版本号后) const originalChinese = /[\u4e00-\u9fa5]/.test(content); const chineseAfterClean = content.replace(/\d+(?:\.\d+)+/g, '').replace(/[!!]$/, '').trim(); const hasValidChinese = originalChinese && /[\u4e00-\u9fa5]/.test(chineseAfterClean) && chineseAfterClean.length >= 2; // 优化:更宽松的判断条件 const isValidContent = hasJapanese || hasKorean || hasLongEnglish || hasValidChinese; // 排除明显的作者标识和展会标签(但不排除有版本号的中文词汇) const isAuthorOrTag = /^[A-Za-z\s]+\d+$/.test(content) || // Key Island13 /[\u4e00-\u9fa5\u3040-\u309F\u30A0-\u30FF]+\([A-Za-z0-9]+\)/.test(content) || // 壁発狂(kao) (/^[\u4e00-\u9fa5A-Za-z0-9]{1,2}$/.test(cleanContent) && !hasValidChinese) || // 短标识符如"苍莓"等,但保留带版本号的中文 /汉化|翻译|合同志|短篇|版本/i.test(cleanContent) || cleanContent === ''; if (isValidContent && !isAuthorOrTag) { // 如果有有效中文,使用清理后的版本 const finalContent = hasValidChinese ? chineseAfterClean : cleanContent; meaningfulBrackets.push(finalContent); console.log(` 💎 提取对照内容: "${finalContent}"`); } else { console.log(` 🚫 忽略: "${content}" (${isAuthorOrTag ? '作者/标签' : '短标识符'})`); } }); } // 9. 生成最终结果 let body = mainContent.replace(/[\[【((][^\]】))]*[\]】))]/g, '').trim(); // 处理各种语言混合情况 // 韩文+中文混合 const koreanChineseMatch = body.match(/^([\uAC00-\uD7AF]+)([\u4e00-\u9fa5]+)$/); if (koreanChineseMatch) { const koreanPart = koreanChineseMatch[1].trim(); const chinesePart = koreanChineseMatch[2].trim(); console.log(` 🇰🇷 检测到韩中混合: 韩文"${koreanPart}" + 中文"${chinesePart}"`); const result = new String(koreanPart); result._dualLanguage = { chinese: chinesePart, foreign: koreanPart }; return result; } // 中文+韩文混合 const chineseKoreanMatch = body.match(/^([\u4e00-\u9fa5]+)\s*([\uAC00-\uD7AF\s]+)$/); if (chineseKoreanMatch) { const chinesePart = chineseKoreanMatch[1].trim(); const koreanPart = chineseKoreanMatch[2].trim(); console.log(` 🇰🇷 检测到中韩混合: 中文"${chinesePart}" + 韩文"${koreanPart}"`); const result = new String(chinesePart); result._dualLanguage = { chinese: chinesePart, foreign: koreanPart }; return result; } // 日文+中文混合 const japaneseChineseMatch = body.match(/^([\u3040-\u309F\u30A0-\u30FF~〜~ー]+)\s+([\u4e00-\u9fa5~〜~]+)$/); if (japaneseChineseMatch) { const japanesePart = japaneseChineseMatch[1].trim(); const chinesePart = japaneseChineseMatch[2].trim(); console.log(` 🇯🇵 检测到日中混合: 日文"${japanesePart}" + 中文"${chinesePart}"`); const result = new String(japanesePart); result._dualLanguage = { chinese: chinesePart, foreign: japanesePart }; return result; } // 处理中文+英文混合 const chineseEnglishMatch = body.match(/^([\u4e00-\u9fa5!!]+)\s+([A-Za-z][A-Za-z\s!]*[A-Za-z!])$/); if (chineseEnglishMatch) { const chinesePart = chineseEnglishMatch[1].replace(/[!!]$/, '').trim(); const englishPart = chineseEnglishMatch[2].replace(/[!!]$/, '').trim(); console.log(` 🔀 检测到中英混合: 中文"${chinesePart}" + 英文"${englishPart}"`); const result = new String(chinesePart); result._dualLanguage = { chinese: chinesePart, foreign: englishPart }; return result; } // 处理英文+中文混合的情况 const mixedMatch = body.match(/^([A-Za-z][A-Za-z0-9\.\*\s]*[A-Za-z])([\u4e00-\u9fa5].*)?$/); if (mixedMatch && mixedMatch[2]) { // 英文部分和中文部分分开 const englishPart = mixedMatch[1].trim(); const chinesePart = mixedMatch[2].trim(); console.log(` 🔀 检测到英中混合: 英文"${englishPart}" + 中文"${chinesePart}"`); // 判断中文部分是否是章节标题 if (/能和你在一起就好|总有一天|第\d+话/.test(chinesePart)) { console.log(` � 中文部分是章节标题,只保留英文`); return englishPart; } else { console.log(` 📖 中文部分是作品名,创建双语结构`); const result = new String(englishPart); result._dualLanguage = { chinese: chinesePart, foreign: englishPart }; return result; } } // 只保留核心标题(移除数字、「」等) const cleanBody = body .replace(/\s*\d+\s*「[^」]*」.*$/g, '') .replace(/\s*\d+\s*$/, '') .trim(); if (cleanBody !== body) { console.log(` ✂️ 清理章节信息: "${cleanBody}"`); } // 如果有有意义的括号内容,创建双语结构 if (meaningfulBrackets.length > 0 && cleanBody) { const result = new String(cleanBody); const chineseBracket = meaningfulBrackets.find(b => /[\u4e00-\u9fa5]/.test(b)); const foreignBracket = meaningfulBrackets.find(b => !/[\u4e00-\u9fa5]/.test(b)); console.log(` 🔍 括号分析: 中文=${chineseBracket} 外文=${foreignBracket} 主体=${cleanBody}`); if (chineseBracket || foreignBracket) { result._dualLanguage = { chinese: chineseBracket || cleanBody, foreign: foreignBracket || cleanBody }; console.log(` 🌐 创建双语结构: 中文="${result._dualLanguage.chinese}" 外文="${result._dualLanguage.foreign}"`); } return result; } console.log(` 🎯 最终结果: "${cleanBody || step1 || title}"`); return cleanBody || step1 || title || ''; const meaningfulBracketMatch = workName.match(/^([^((]+)[((]([^))]*[\u4e00-\u9fa5\u3040-\u309F\u30A0-\u30FF]+[^))]*)[))](.*)$/); if (meaningfulBracketMatch) { const beforeBracket = meaningfulBracketMatch[1].trim(); const insideBracket = meaningfulBracketMatch[2].trim(); const afterBracket = meaningfulBracketMatch[3].trim(); // 检查括号内是否包含无关描述词 const isDescriptive = /短篇|合同志|合集|特典|番外|彩页|后记|前记|完结|连载|单行本|杂志|raw|生肉|汉化|翻译/.test(insideBracket); if (!isDescriptive && insideBracket.length >= 2) { // 保留有意义的括号对照 workName = beforeBracket + '(' + insideBracket + ')'; console.log(` 🌐 保留有意义的括号对照: "${workName}"`); } else { // 移除描述性括号内容 workName = beforeBracket + (afterBracket ? ' ' + afterBracket : ''); console.log(` 🗑️ 移除描述性括号内容: "${insideBracket}"`); } } else { // 移除其他尾部括号内容 workName = workName.replace(/\s*[((][^))]*[))].*$/, ''); } // 2) 移除「」『』等引号中的副标题 workName = workName.replace(/\s*[「『〖〈《"'][^」』〗〉》"']*[」』〗〉》"'].*$/, ''); // 3) 移除章节相关信息(包含 "第X部/话/卷" 等模式) workName = workName.replace(/\s+(第\d+[部卷话篇章节期回集]|第[一二三四五六七八九十]+[部卷话篇章节期回集]).*$/, ''); workName = workName.replace(/\s+\d+(?:\.\d+)?\s*(话|篇|期|回|集|章|节|卷|部|上|下|前|后|中|完|全|特|番外|彩页|特典|后记|前记|完结).*$/, ''); } // 智能相似度匹配函数 - 专注核心作品名匹配 function calculateSimilarity(searchTitle, resultTitle) { console.log(`🔄 计算相似度: "${searchTitle}" vs "${resultTitle}"`); // 提取两个标题的核心作品名(去除翻译组、章节号等干扰信息) const searchCore = extractCoreWorkName(searchTitle); const resultCore = extractCoreWorkName(resultTitle); console.log(` 📝 搜索核心: "${searchCore}"`); console.log(` 📝 结果核心: "${resultCore}"`); // 调试:检查是否有双语信息 if (typeof searchCore === 'object' && searchCore._dualLanguage) { console.log(` 🌐 搜索核心双语信息: 中文="${searchCore._dualLanguage.chinese}" 外文="${searchCore._dualLanguage.foreign}"`); } if (typeof resultCore === 'object' && resultCore._dualLanguage) { console.log(` 🌐 结果核心双语信息: 中文="${resultCore._dualLanguage.chinese}" 外文="${resultCore._dualLanguage.foreign}"`); } // 智能长度检查:CJK字符2个即有效,英文需要3个 function isValidCore(core) { if (!core) return false; const coreStr = core.toString(); const hasCJK = /[\u4e00-\u9fa5\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF]/.test(coreStr); return hasCJK ? coreStr.length >= 2 : coreStr.length >= 3; } if (!isValidCore(searchCore) || !isValidCore(resultCore)) { console.log(` ❌ 核心标题无效,跳过`); return 0; } // 检查是否有关键词直接匹配(排除翻译组等无关词汇) const keywordMatch = checkKeywordMatch(searchCore, resultCore); if (keywordMatch > 0) { console.log(` ✅ 关键词匹配度: ${(keywordMatch * 100).toFixed(1)}%`); return keywordMatch; } // 计算核心作品名的相似度 const similarity = calculateCoreWorkSimilarity(searchCore, resultCore); console.log(` ✅ 核心作品相似度: ${(similarity * 100).toFixed(1)}%`); return similarity; } /*** 目录识别功能 ***/ async function searchSeriesDirectory(seriesTitle) { if (!seriesTitle) { console.log('❌ 搜索失败: 标题为空'); return []; } console.log('🔍 开始搜索系列目录:', seriesTitle); // 动态获取当前帖子所属版块fid(30/37等) const currentForumId = detectForumId(); const forumName = getForumName(currentForumId); console.log(`🎯 搜索限定在: ${forumName} (fid=${currentForumId})`); // 第一步:智能提取搜索关键词 function generateSmartSearchTerms(title) { console.log('🎯 智能分析标题:', title); const searchTerms = new Set(); // 策略1: 提取【】中的完整作品名(最高优先级) const titleBrackets = title.match(/【([^】]*[^汉化组个人翻译Sub字幕工作室提灯喵社团][^】]*)】/g); if (titleBrackets) { titleBrackets.forEach(bracket => { const content = bracket.slice(1, -1).trim(); if (content.length >= 6 && content.length <= 25) { // 完整作品名 searchTerms.add(content); console.log(`✅ 完整作品名: "${content}"`); // 如果作品名超过12字,也添加前8字的版本 if (content.length > 12) { const shortVersion = content.substring(0, 8); searchTerms.add(shortVersion); console.log(`✅ 短版本作品名: "${shortVersion}"`); } } }); } // 策略2: 提取最长连续中文片段 const chineseSegments = title.match(/[\u4e00-\u9fa5]{4,}/g); if (chineseSegments) { // 按长度排序,取最长的2个 const sortedSegments = chineseSegments .filter(seg => seg.length >= 4 && seg.length <= 15) .filter(seg => !isNoiseText(seg)) .sort((a, b) => b.length - a.length) .slice(0, 2); sortedSegments.forEach(seg => { searchTerms.add(seg); console.log(`✅ 中文片段: "${seg}"`); }); } // 策略3: 如果前面没找到好的关键词,尝试日文片段 if (searchTerms.size === 0) { const japaneseSegments = title.match(/[\u3040-\u309F\u30A0-\u30FF]{4,}/g); if (japaneseSegments) { const longestJapanese = japaneseSegments .sort((a, b) => b.length - a.length)[0]; if (longestJapanese && longestJapanese.length >= 4) { searchTerms.add(longestJapanese.substring(0, 8)); console.log(`✅ 日文片段: "${longestJapanese.substring(0, 8)}"`); } } } // 转换为数组并限制数量 const result = Array.from(searchTerms).slice(0, 3); console.log('🎯 最终搜索关键词:', result); return result; } // 优化关键词提取:智能提取作品名 function extractWorkName(title) { // 先移除未翻译标签 let cleanTitle = title.replace(/^\[未翻译\]\s*|\[生肉\]\s*|\[RAW\]\s*/i, ''); // 智能提取括号中的标题信息 const bracketContents = []; // 提取【】中的内容,区分汉化组和标题 const doubleBrackets = cleanTitle.match(/【([^】]+)】/g); if (doubleBrackets) { doubleBrackets.forEach(match => { const content = match.slice(1, -1); // 如果包含"汉化"、"组"、"个人"等关键词,认为是汉化组信息 if (!/汉化|组|个人|翻译|Sub|字幕|工作室/i.test(content)) { bracketContents.push(content); } }); } // 提取[]中的标题内容(排除明显的作者名) const squareBrackets = cleanTitle.match(/\[([^\]]+)\]/g); if (squareBrackets) { squareBrackets.forEach(match => { const content = match.slice(1, -1); // 排除明显的作者名(通常较短且不含中文标点) if (content.length > 3 && !/^[A-Za-z0-9\s]+$/.test(content)) { bracketContents.push(content); } }); } // 去除所有括号和汉化组信息后的主体内容 let mainTitle = cleanTitle .replace(/【[^】]*汉化[^】]*】/gi, '') // 移除汉化组信息 .replace(/【[^】]*组[^】]*】/gi, '') // 秮除包含"组"的信息 .replace(/【[^】]*个人[^】]*】/gi, '') // 移除个人翻译信息 .replace(/\[[^\]]*\]/g, '') // 秘除所有方括号内容 .replace(/【[^】]*】/g, '') // 秮除剩余的双括号内容 .trim(); // 去除章节信息 mainTitle = mainTitle.replace(/\d{1,3}(?:\.\d+)?\s*[话話章节節回卷篇部].*$/i, ''); mainTitle = mainTitle.replace(/第\d+[话話章节節回卷篇部].*$/i, ''); // 清理标点和空格 mainTitle = mainTitle.replace(/[、,,。!?!?;;::\s]+$/, '').trim(); // 生成候选标题列表 const candidates = []; // 1. 主标题 if (mainTitle) { candidates.push(mainTitle); } // 2. 括号中的标题内容 bracketContents.forEach(content => { if (content.length > 3) { candidates.push(content); } }); // 3. 如果主标题包含日文,尝试提取中文部分 if (mainTitle && /[\u3040-\u309F\u30A0-\u30FF]/.test(mainTitle)) { const chinesePart = mainTitle.replace(/[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]*[\u3040-\u309F\u30A0-\u30FF]+[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]*/g, '').trim(); if (chinesePart && chinesePart.length > 2) { candidates.push(chinesePart); } } // 返回最长的候选标题 const bestCandidate = candidates.reduce((longest, current) => current.length > longest.length ? current : longest, ''); return bestCandidate || mainTitle || cleanTitle; } // 生成搜索关键词 - 智能分词策略,优先中文,限制长度,最多2个搜索词 function generateSearchTerms(title) { console.log('🎯 分析标题:', title); // ✨ 优先使用 extractCoreWorkName 提取核心作品名 const coreWorkName = extractCoreWorkName(title); if (coreWorkName && coreWorkName.length >= 2) { console.log('✅ 使用核心作品名作为搜索词:', coreWorkName); const searchTerms = []; // 🔍 检查是否从 extractCoreWorkName 返回了双语信息 const coreWorkNameStr = coreWorkName.toString ? coreWorkName.toString() : String(coreWorkName); let mainTerm = coreWorkNameStr.replace(/[!!??]+$/, '').trim(); let alternateTerm = null; if (coreWorkName._dualLanguage) { console.log('🌐 检测到核心提取的双语信息:', coreWorkName._dualLanguage); const chineseTerm = coreWorkName._dualLanguage.chinese.replace(/[!!??]+$/, '').trim(); const foreignTerm = coreWorkName._dualLanguage.foreign; // 🎯 优先原文,再中文 mainTerm = foreignTerm; // 外文作主词 (优先搜索) alternateTerm = chineseTerm; // 中文作备选 (次优搜索) // 设置双语搜索标记 searchTerms._dualLanguageSearch = { chinese: chineseTerm, foreign: foreignTerm, priority: 'foreign' }; console.log(`🌐 使用双语搜索 (优先外文): 外文"${foreignTerm}" / 中文"${chineseTerm}"`); } // 🔧 清理主词的标点符号 mainTerm = mainTerm.replace(/[!!??]+$/, '').trim(); // 🔧 特殊处理:英文开头+中文章节标题的情况 const englishPrefixMatch = coreWorkNameStr.match(/^([A-Za-z*]+)(.*)$/); if (englishPrefixMatch && englishPrefixMatch[2]) { const englishPart = englishPrefixMatch[1]; const chinesePart = englishPrefixMatch[2]; // 检查中文部分是否包含章节描述词 if (/^[\u4e00-\u9fa5]/.test(chinesePart) && /[能和你在一起就好|希望|梦想|故事|物语|生活|日常|短篇|合同志]/.test(chinesePart)) { console.log(`🎯 检测到英文+中文章节标题模式,优先英文: "${englishPart}"`); mainTerm = englishPart; // 不设置 alternateTerm,只使用英文部分 } } // 如果已经有双语信息,跳过其他处理 if (!coreWorkName._dualLanguage) { const parenMatch = coreWorkNameStr.match(/^([^((]+)[((]([^))]+)[))]/); if (parenMatch) { const part1 = parenMatch[1].trim(); const part2 = parenMatch[2].trim(); const hasChinese1 = /[\u4e00-\u9fa5]/.test(part1); const hasChinese2 = /[\u4e00-\u9fa5]/.test(part2); if (!hasChinese1 && hasChinese2) { // 英文(中文) - 主词用英文,备选用中文 mainTerm = part1.replace(/[\d\.]+$/, '').trim(); alternateTerm = part2.replace(/[\d\.]+$/, '').trim(); console.log(`🔄 英文主词+中文备选: "${mainTerm}" / "${alternateTerm}"`); // 设置双语搜索标记 searchTerms._dualLanguageSearch = { chinese: alternateTerm, foreign: mainTerm }; } else if (hasChinese1 && !hasChinese2) { // 中文(英文) - 主词用中文,备选用英文 mainTerm = part1; alternateTerm = part2; console.log(`🔄 中文主词+英文备选: "${mainTerm}" / "${alternateTerm}"`); // 设置双语搜索标记 searchTerms._dualLanguageSearch = { chinese: mainTerm, foreign: alternateTerm }; } else if (hasChinese1 && hasChinese2) { // 日文(中文) - 优先中文 mainTerm = part2; alternateTerm = part1; console.log(`🔄 中文主词+日文备选: "${mainTerm}" / "${alternateTerm}"`); // 设置双语搜索标记 searchTerms._dualLanguageSearch = { chinese: mainTerm, foreign: alternateTerm }; } } // 处理各种分隔符的对照翻译:/ | () () else { let parts = null; let separator = ''; // 检测分隔符并拆分 if (coreWorkNameStr.includes('/')) { parts = coreWorkNameStr.split('/').map(p => p.trim()); separator = '/'; } else if (coreWorkNameStr.includes('|')) { parts = coreWorkNameStr.split('|').map(p => p.trim()); separator = '|'; } if (parts && parts.length === 2) { const [part1, part2] = parts; const hasChinese1 = /[\u4e00-\u9fa5]/.test(part1); const hasChinese2 = /[\u4e00-\u9fa5]/.test(part2); const hasJapanese1 = /[\u3040-\u309F\u30A0-\u30FF]/.test(part1); const hasJapanese2 = /[\u3040-\u309F\u30A0-\u30FF]/.test(part2); const hasEnglish1 = /[A-Za-z]/.test(part1); const hasEnglish2 = /[A-Za-z]/.test(part2); // 判断语言类型 - 修复语言检测逻辑 // 主要看字符比例,而不是严格排他性 const chineseRatio1 = (part1.match(/[\u4e00-\u9fa5]/g) || []).length / part1.length; const chineseRatio2 = (part2.match(/[\u4e00-\u9fa5]/g) || []).length / part2.length; const japaneseRatio1 = (part1.match(/[\u3040-\u309F\u30A0-\u30FF]/g) || []).length / part1.length; const japaneseRatio2 = (part2.match(/[\u3040-\u309F\u30A0-\u30FF]/g) || []).length / part2.length; const isChinese1 = chineseRatio1 > 0.6 && japaneseRatio1 < 0.3; const isChinese2 = chineseRatio2 > 0.6 && japaneseRatio2 < 0.3; const isJapanese1 = japaneseRatio1 > 0.3 || (hasJapanese1 && chineseRatio1 < 0.6); const isJapanese2 = japaneseRatio2 > 0.3 || (hasJapanese2 && chineseRatio2 < 0.6); const isEnglish1 = hasEnglish1 && !hasChinese1 && !hasJapanese1; const isEnglish2 = hasEnglish2 && !hasChinese2 && !hasJapanese2; const isForeign1 = isJapanese1 || isEnglish1; const isForeign2 = isJapanese2 || isEnglish2; console.log(`🔍 语言检测结果: part1="${part1}" (中文:${chineseRatio1.toFixed(2)} 日文:${japaneseRatio1.toFixed(2)}) part2="${part2}" (中文:${chineseRatio2.toFixed(2)} 日文:${japaneseRatio2.toFixed(2)})`); console.log(`🔍 判定结果: part1=${isChinese1?'中文':isForeign1?'外文':'混合'} part2=${isChinese2?'中文':isForeign2?'外文':'混合'}`); // 策略1:中外文对照 → 分别提取作为双语搜索 (优先原文) if ((isChinese1 && isForeign2) || (isForeign1 && isChinese2)) { const chineseTerm = isChinese1 ? part1 : part2; const foreignTerm = isForeign1 ? part1 : part2; console.log(`🌐 ${separator}分隔-中外对照: 外文"${foreignTerm}" / 中文"${chineseTerm}"`); // 🎯 优先原文,再中文 - 按用户要求调整搜索顺序 mainTerm = foreignTerm; // 外文作主词 (优先搜索) alternateTerm = chineseTerm; // 中文作备选 (次优搜索) // 标记为需要双语搜索策略 searchTerms._dualLanguageSearch = { chinese: chineseTerm, foreign: foreignTerm, priority: 'foreign' // 标记优先搜索外文 }; } // 策略2:日中对照 → 优先日文原文 else if ((isJapanese1 && isChinese2) || (isChinese1 && isJapanese2)) { const chineseTerm = isChinese1 ? part1 : part2; const japaneseTerm = isJapanese1 ? part1 : part2; console.log(`🌐 ${separator}分隔-日中对照: 日文"${japaneseTerm}" / 中文"${chineseTerm}"`); // 🎯 优先日文原文,再中文 mainTerm = japaneseTerm; // 日文作主词 (优先搜索) alternateTerm = chineseTerm; // 中文作备选 (次优搜索) // 标记为需要双语搜索策略 searchTerms._dualLanguageSearch = { chinese: chineseTerm, foreign: japaneseTerm, priority: 'foreign' // 标记优先搜索外文 }; } // 策略3:两组都是中文 → 优先取前面的词 else if (isChinese1 && isChinese2) { mainTerm = part1; alternateTerm = part2; console.log(`🀄 ${separator}分隔-双中文,优先前词: "${mainTerm}" / "${alternateTerm}"`); } // 策略4:其他情况 → 选择较长的 else { mainTerm = part1.length >= part2.length ? part1 : part2; alternateTerm = part1.length >= part2.length ? part2 : part1; console.log(`🔧 ${separator}分隔-选长词: "${mainTerm}" / "${alternateTerm}"`); } } } } // 关闭 if (!coreWorkName._dualLanguage) // 🎯 添加主搜索词 - 支持2字中文 if (mainTerm && (mainTerm.length >= 2)) { // 对中文降低长度要求到2字,其他语言保持3字符 const hasChinese = /[\u4e00-\u9fa5]/.test(mainTerm); if (hasChinese && mainTerm.length >= 2) { searchTerms.push(mainTerm); } else if (!hasChinese && mainTerm.length >= 3) { searchTerms.push(mainTerm); } } // 🎯 添加备选搜索词 - 支持2字中文 if (alternateTerm && (alternateTerm.length >= 2)) { // 对中文降低长度要求到2字,其他语言保持3字符 const hasChinese = /[\u4e00-\u9fa5]/.test(alternateTerm); if (hasChinese && alternateTerm.length >= 2) { searchTerms.push(alternateTerm); } else if (!hasChinese && alternateTerm.length >= 3) { searchTerms.push(alternateTerm); } } // 检查是否为纯英文作品名 const isPureEnglish = /^[A-Za-z\s]+$/.test(coreWorkNameStr); if (isPureEnglish) { // 英文作品名:检查是否包含多个单词 const words = coreWorkNameStr.trim().split(/\s+/); if (words.length > 1) { // ✨ 策略改变:直接使用带空格的原始版本搜索 // 论坛搜索会把空格当作"AND"条件,能找到包含所有词的结果 console.log(`✅ 英文多词(保持空格): "${coreWorkNameStr}"`); // ✨ 移除备选首词逻辑,避免过度分割 // 保持完整的英文标题,不添加单词片段 console.log(`🚫 跳过首词分割,保持完整标题`); } else { // 单个英文单词 console.log(`✅ 单个英文词: "${coreWorkNameStr}"`); } // 纯英文标题不启用双语搜索 searchTerms._pureEnglish = true; } else { // 中文/日文/混合作品名 if (!coreWorkName._dualLanguage) { // 只有在没有双语信息时才添加原始作品名 searchTerms.push(coreWorkNameStr); } // ✨ 如果原标题包含括号中的英文,也添加为双语搜索 const bracketEnglish = title.match(/[((]([A-Za-z\s!?]+)[))]/); if (bracketEnglish) { const englishName = bracketEnglish[1].trim().replace(/[!?]+$/, ''); if (englishName.length >= 3) { // 🎯 优先英文,再中文 searchTerms.length = 0; // 清空原有的 searchTerms.push(englishName); // 英文优先 searchTerms.push(coreWorkNameStr); // 中文其次 // 设置双语搜索标记 searchTerms._dualLanguageSearch = { chinese: coreWorkNameStr, foreign: englishName, priority: 'foreign' }; console.log(`🌐 括号中英对照 (优先英文): 英文"${englishName}" / 中文"${coreWorkNameStr}"`); } } // 优化长度控制:主词过长(>12字符)自动生成6字符短版本 if (coreWorkNameStr.length > 12 && searchTerms.length === 1) { const shortVersion = coreWorkNameStr.substring(0, 6); // 确保短版本不会太短造成搜索泛化(至少4个有效字符) if (shortVersion.length >= 4 && shortVersion.replace(/\s+/g, '').length >= 3) { searchTerms.push(shortVersion); console.log('✅ 主词过长,添加6字符短版本作为备选:', shortVersion); } } } // 去重处理并保留元信息 const uniqueTerms = Array.from(new Set(searchTerms.filter(Boolean))); if (searchTerms._dualLanguageSearch) uniqueTerms._dualLanguageSearch = searchTerms._dualLanguageSearch; if (searchTerms._pureEnglish) uniqueTerms._pureEnglish = searchTerms._pureEnglish; console.log('🎯 最终搜索关键词:', uniqueTerms); return uniqueTerms.slice(0, 2); } // 如果核心提取失败,使用原有的备用逻辑 console.log('⚠️ 核心作品名提取失败,使用备用逻辑'); // 移除常见的噪音标签 let cleanTitle = title .replace(/^\[未翻译\]\s*|\[生肉\]\s*|\[RAW\]\s*/i, '') .replace(/【[^】]*】/g, (match) => { const content = match.slice(1, -1); return isNoiseText(content) ? '' : match; }); const candidates = { chinese: [], // 中文标题候选 japanese: [], // 日文标题候选 mixed: [] // 混合标题候选 }; // 策略1: 提取【】中的核心作品名 const doubleBrackets = cleanTitle.match(/【([^】]+)】/g); if (doubleBrackets) { doubleBrackets.forEach(match => { const content = match.slice(1, -1).trim(); // 统一使用 isNoiseText 过滤 if (content.length >= 3 && content.length <= 20 && !isNoiseText(content)) { // 判断内容类型 const hasChinese = /[\u4e00-\u9fa5]/.test(content); const hasJapanese = /[\u3040-\u309F\u30A0-\u30FF]/.test(content); if (hasChinese && hasJapanese) { candidates.mixed.push(content); } else if (hasChinese) { candidates.chinese.push(content); } else if (hasJapanese) { candidates.japanese.push(content); } console.log(`✅ 找到【】中的标题: "${content}" (类型: ${hasChinese ? '中' : ''}${hasJapanese ? '日' : ''})`); } }); } // 策略2: 提取最长的纯中文片段(统一使用 isNoiseText 过滤) const chineseMatches = cleanTitle.match(/[\u4e00-\u9fa5]{3,}/g); if (chineseMatches) { chineseMatches .filter(match => match.length >= 3) .filter(match => !isNoiseText(match)) .forEach(match => { candidates.chinese.push(match); }); } // 策略3: 提取最长的日文片段 const japaneseMatches = cleanTitle.match(/[\u3040-\u309F\u30A0-\u30FF]{3,}/g); if (japaneseMatches) { japaneseMatches .filter(match => match.length >= 3) .forEach(match => { candidates.japanese.push(match); }); } // 智能选择搜索词 const finalTerms = []; // 优先级1: 中文标题 if (candidates.chinese.length > 0) { const bestChinese = candidates.chinese .sort((a, b) => b.length - a.length)[0]; // 如果中文标题超过12字,截取前6字 let searchTerm = bestChinese; if (searchTerm.length > 12) { searchTerm = searchTerm.substring(0, 6); console.log(`🔤 中文标题过长,截取前6字: "${bestChinese}" -> "${searchTerm}"`); } finalTerms.push(searchTerm); console.log('✅ 优先使用中文标题:', searchTerm); } // 优先级2: 如果没有中文标题,使用日文标题 if (finalTerms.length === 0 && candidates.japanese.length > 0) { const bestJapanese = candidates.japanese .sort((a, b) => b.length - a.length)[0]; // 如果日文标题超过12字,截取前6字 let searchTerm = bestJapanese; if (searchTerm.length > 12) { searchTerm = searchTerm.substring(0, 6); console.log(`🔤 日文标题过长,截取前6字: "${bestJapanese}" -> "${searchTerm}"`); } finalTerms.push(searchTerm); console.log('✅ 使用日文标题:', searchTerm); } // 优先级3: 混合标 if (finalTerms.length === 0 && candidates.mixed.length > 0) { const bestMixed = candidates.mixed .sort((a, b) => b.length - a.length)[0]; // 尝试提取中文部分 const chinesePart = bestMixed.match(/[\u4e00-\u9fa5]+/g); if (chinesePart && chinesePart.length > 0) { let searchTerm = chinesePart.join(''); if (searchTerm.length > 12) { searchTerm = searchTerm.substring(0, 6); } finalTerms.push(searchTerm); console.log('✅ 从混合标题提取中文部分:', searchTerm); } else { let searchTerm = bestMixed; if (searchTerm.length > 12) { searchTerm = searchTerm.substring(0, 6); } finalTerms.push(searchTerm); console.log('✅ 使用混合标题:', searchTerm); } } // 兜底策略:如果都没找到,使用清理后的标题 if (finalTerms.length === 0) { let baseTitle = cleanTitle .replace(/第?\d+(?:\.\d+)?\s*[话話章节節回卷篇期].*$/i, '') .replace(/\d+(?:\.\d+)?\s*$/, '') .replace(/[【】\[\]()()]/g, ' ') .replace(/[、,,。!?!?;;::\s]+/g, ' ') .trim(); if (baseTitle && baseTitle.length >= 3) { // 截取处理 if (baseTitle.length > 10) { baseTitle = baseTitle.substring(0, 6); console.log(`🔤 兜底标题过长,截取前6字: "${baseTitle}"`); } finalTerms.push(baseTitle); console.log('✅ 使用清理后的标题:', baseTitle); } } // 可选的第二个搜索词:如果第一个词是从长标题截取的,可以尝试更短的核心词 if (finalTerms.length === 1 && finalTerms[0].length >= 5) { const firstTerm = finalTerms[0]; // 尝试提取更短的核心词(3-4字) if (firstTerm.length > 4) { const shorterTerm = firstTerm.substring(0, 4); // 避免重复添加 if (shorterTerm !== firstTerm) { finalTerms.push(shorterTerm); console.log('✅ 添加更短的核心词作为备选:', shorterTerm); } } } // 去重处理 const uniqueTerms = Array.from(new Set(finalTerms.filter(Boolean))); console.log('🎯 最终搜索关键词:', uniqueTerms); return uniqueTerms.slice(0, 2); // 最多返回2个搜索词 } // 🌐 双语搜索辅助函数:执行单次搜索并返回结果数量 async function performSingleSearch(keyword, forumId) { if (!keyword) return []; try { console.log(`🔎 单次搜索测试: "${keyword}"`); // 获取搜索页面和FormHash const searchPageResponse = await fetch('https://bbs.yamibo.com/search.php', { method: 'GET', credentials: 'include', headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3', 'Referer': window.location.href, 'User-Agent': navigator.userAgent } }); let formHash = ''; if (searchPageResponse.ok) { const searchPageHtml = await searchPageResponse.text(); const formHashMatch = searchPageHtml.match(/name="formhash"\s+value="([^"]+)"/); if (formHashMatch) formHash = formHashMatch[1]; } await new Promise(resolve => setTimeout(resolve, 1500)); // 防止过快请求 // 执行搜索 const formData = new FormData(); formData.append('formhash', formHash); formData.append('srchtxt', keyword); if (forumId) formData.append('srchfid[]', forumId); formData.append('orderby', 'dateline'); formData.append('ascdesc', 'desc'); formData.append('searchsubmit', 'yes'); const response = await fetch('https://bbs.yamibo.com/search.php?mod=forum', { method: 'POST', credentials: 'include', headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Origin': 'https://bbs.yamibo.com', 'Referer': 'https://bbs.yamibo.com/search.php', 'User-Agent': navigator.userAgent }, body: formData }); if (response.ok) { const html = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); // 提取搜索结果 const results = []; const threadElements = doc.querySelectorAll('#threadlisttableid tbody[id^="normalthread_"]'); threadElements.forEach(thread => { const titleElement = thread.querySelector('th a[href*="thread-"]'); if (titleElement) { results.push({ title: titleElement.textContent.trim(), url: titleElement.href }); } }); console.log(`📊 "${keyword}" 搜索到 ${results.length} 个结果`); return results; } return []; } catch (error) { console.warn(`⚠️ 单次搜索"${keyword}"失败:`, error); return []; } } // 生成搜索关键词 let searchTerms = generateSearchTerms(seriesTitle); // 检查是否有双语搜索标记(仅在真正的双语对照时启用) if (searchTerms && searchTerms._dualLanguageSearch && !searchTerms._pureEnglish) { const { chinese, foreign } = searchTerms._dualLanguageSearch; console.log(`🌐 检测到中外文对照,启用双语搜索: 中文"${chinese}" vs 外文"${foreign}"`); try { // 分别搜索中文和外文,比较结果数量 const chineseResults = await performSingleSearch(chinese, currentForumId); const foreignResults = await performSingleSearch(foreign, currentForumId); console.log(`📊 搜索结果对比: 中文"${chinese}"=${chineseResults.length}个 / 外文"${foreign}"=${foreignResults.length}个`); // 根据搜索结果优化搜索词顺序 if (foreignResults.length > chineseResults.length) { console.log(`✅ 外文搜索结果更多,优先使用外文关键词: "${foreign}"`); searchTerms = [foreign, chinese]; } else { console.log(`✅ 中文搜索结果更优,优先使用中文关键词: "${chinese}"`); searchTerms = [chinese, foreign]; } delete searchTerms._dualLanguageSearch; console.log('🎯 双语搜索优化后关键词:', searchTerms); } catch (error) { console.warn('⚠️ 双语搜索比较失败,使用默认策略:', error); delete searchTerms._dualLanguageSearch; } } if (!searchTerms || searchTerms.length === 0) { searchTerms = generateSmartSearchTerms(seriesTitle); console.log('📝 使用传统搜索逻辑生成关键词:', searchTerms); } console.log('📝 使用搜索方式获取完整系列目录...'); const searchResults = new Map(); try { const cacheKey = seriesTitle; if (searchCache.has(cacheKey)) { console.log('📦 使用内存缓存结果:', searchCache.get(cacheKey).length, '个章节'); return searchCache.get(cacheKey); } // 检查持久化缓存 const seriesKey = generateSeriesKey(seriesTitle); const cachedDirectory = getCachedDirectory(seriesKey); if (cachedDirectory && cachedDirectory.length > 0) { console.log('📦 使用持久化缓存结果:', cachedDirectory.length, '个章节'); searchCache.set(cacheKey, cachedDirectory); return cachedDirectory; } // 检测503错误频率,如果连续失败则停止搜索 console.log('⚠️ 注意:论坛有搜索频率限制,将串行搜索以避免503错误'); let consecutiveFailures = 0; const maxFailures = 1; // 减少到1次失败后就切换策略 let searchBlocked = false; for (let i = 0; i < Math.min(searchTerms.length, 2); i++) { // 限制最多2次搜索 const searchTerm = searchTerms[i]; // 如果连续失败次数过多,停止搜索 if (consecutiveFailures >= maxFailures) { console.log(`❌ 连续失败 ${maxFailures} 次,停止搜索以避免被论坛限制`); break; } // ✅ 如果已经找到足够的结果,跳过后续搜索 if (searchResults.size > 0) { console.log(`✅ 已找到 ${searchResults.size} 个结果,跳过后续搜索`); break; } // 如果不是第一个搜索,等待12秒避免频率限制 if (i > 0) { console.log(`⏱️ 等待12秒后进行下一个搜索 (${i+1}/${Math.min(searchTerms.length, 2)})...`); await new Promise(resolve => setTimeout(resolve, 12000)); } try { console.log('🔎 当前搜索关键词:', searchTerm); console.log('📝 获取搜索页面...'); const searchPageResponse = await fetch('https://bbs.yamibo.com/search.php', { method: 'GET', credentials: 'include', headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3', 'Cache-Control': 'no-cache', 'Pragma': 'no-cache', 'Referer': window.location.href, 'User-Agent': navigator.userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' } }); let formHash = ''; if (searchPageResponse.ok) { const searchPageHtml = await searchPageResponse.text(); const formHashMatch = searchPageHtml.match(/name="formhash"\s+value="([^"]+)"/); if (formHashMatch) { formHash = formHashMatch[1]; console.log('✅ 获取到新的FormHash:', formHash); } } // 等待一小段时间模拟人工操作 await new Promise(resolve => setTimeout(resolve, Math.random() * 2000 + 1000)); // 使用动态获取的fid const formData = new FormData(); formData.append('formhash', formHash); formData.append('srchtxt', searchTerm); formData.append('srchfid[]', currentForumId); // 动态版块ID formData.append('orderby', 'dateline'); formData.append('ascdesc', 'desc'); formData.append('searchsubmit', 'yes'); console.log('🔍 发送搜索请求...'); console.log('📝 搜索参数详情:', { formhash: formHash, srchtxt: searchTerm, srchfid: currentForumId, orderby: 'dateline', ascdesc: 'desc' }); // 先尝试GET搜索(模拟手动搜索) const encodedSearchTerm = encodeURIComponent(searchTerm); const getUrl = `https://bbs.yamibo.com/search.php?mod=forum&srchtxt=${encodedSearchTerm}&formhash=${formHash}&srchfid=${currentForumId}&orderby=dateline&ascdesc=desc&searchsubmit=yes`; console.log('🌐 尝试GET搜索URL:', getUrl); let response = await fetch(getUrl, { method: 'GET', credentials: 'include', headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3', 'Cache-Control': 'no-cache', 'Referer': 'https://bbs.yamibo.com/search.php', 'User-Agent': navigator.userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' } }); // 如果GET失败,回退到POST if (!response.ok) { console.log('❌ GET搜索失败,尝试POST搜索...'); response = await fetch('https://bbs.yamibo.com/search.php?mod=forum', { method: 'POST', credentials: 'include', headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3', 'Origin': 'https://bbs.yamibo.com', 'Referer': 'https://bbs.yamibo.com/search.php', 'Sec-Fetch-Dest': 'document', 'Sec-Fetch-Mode': 'navigate', 'Sec-Fetch-Site': 'same-origin', 'Sec-Fetch-User': '?1', 'Upgrade-Insecure-Requests': '1', 'User-Agent': navigator.userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' }, body: formData }); } else { console.log('✅ GET搜索请求成功'); } if (!response.ok) { console.log(`❌ 搜索请求失败: ${response.status} - ${response.statusText}`); consecutiveFailures++; if (response.status === 503) { console.log('⚠️ 搜索服务暂时不可用,可能是论坛维护中'); } continue; } const html = await response.text(); if (html.includes('需要登录')) { console.log('❌ 需要登录'); continue; } const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); // 调试:检查页面标题和主要内容 const pageTitle = doc.querySelector('title')?.textContent || '无标题'; console.log('📄 搜索结果页面标题:', pageTitle); // 检查是否有搜索结果统计信息 const resultStats = doc.querySelector('.pg .xi2, .ptm .xi2, .pages .xi2'); if (resultStats) { console.log('📊 搜索统计信息:', resultStats.textContent.trim()); } // 改进的搜索结果提取 - 支持多页和多种选择器 const pageSearchResults = []; // 尝试多种可能的选择器(累积式) const selectors = [ '#threadlist li.pbw h3.xs3 a', // 原有主选择器 '#threadlist ul li.pbw h3.xs3 a', // 百合会搜索结果的实际结构! '#threadlist .slst ul li.pbw h3 a', // 搜索结果页面的完整路径 '#threadlist li h3.xs3 a', // 简化版本的搜索结果 '#threadlist .xst', // 标准搜索结果 'a.xst', // 通用搜索结果链接 '.xst', // 搜索结果标题类 'a[href*="thread-"]', // 包含thread的链接 'a[href*="tid="]', // 包含tid的链接 '.s.xst', // 另一种搜索结果格式 '#threadlist tbody .subject a', // 表格形式的搜索结果 '.threadlist .title a', // 标题链接 'h3 a[href*="thread"]', // h3中的帖子链接 'tbody[id^="normalthread"] .xst', // 论坛标准列表格式 'tbody[id^="normalthread"] a[href*="thread"]', // 标准帖子列表中的链接 '.ptm .xst', // 搜索结果页面格式 '#threadlisttableid .xst', // 帖子列表表格中的标题 '#threadlisttableid a[href*="thread"]', // 帖子列表表格中的链接 '.bl .xst', // 搜索结果中的标题 'table .subject a', // 表格中的主题链接 '.ptm a[href*="thread"]' // 搜索结果中的所有帖子链接 ]; let foundResults = false; selectors.forEach(selector => { const elements = doc.querySelectorAll(selector); if (elements.length > 0) { console.log(`✅ 选择器 "${selector}" 找到 ${elements.length} 个结果`); pageSearchResults.push(...Array.from(elements)); foundResults = true; } }); if (!foundResults) { console.log('❌ 所有选择器都未找到结果,打印页面结构用于调试...'); // 调试信息:检查页面是否包含搜索词 const bodyText = doc.body?.textContent || ''; const searchTermLower = searchTerm.toLowerCase(); const containsSearchTerm = bodyText.toLowerCase().includes(searchTermLower); console.log(`🔍 页面是否包含搜索词"${searchTerm}": ${containsSearchTerm}`); // 检查页面是否有错误信息 const errorMsg = doc.querySelector('.error, .alert, .notice'); if (errorMsg) { console.log('⚠️ 页面错误信息:', errorMsg.textContent.trim()); } // 打印页面的主要结构 const allLinks = doc.querySelectorAll('a[href*="thread"], a[href*="tid="]'); console.log(`📋 页面中所有thread链接数量: ${allLinks.length}`); if (allLinks.length > 0) { console.log('📋 前5个thread链接:'); Array.from(allLinks).slice(0, 5).forEach((link, idx) => { console.log(` ${idx + 1}. "${link.textContent.trim()}" (${link.href})`); }); } // 检查是否有特殊的搜索结果结构 const searchResultContainer = doc.querySelector('#threadlist, #threadlisttableid, .searchresult, .ptm'); if (searchResultContainer) { console.log('📋 找到搜索结果容器:', searchResultContainer.tagName + (searchResultContainer.id ? '#' + searchResultContainer.id : '') + (searchResultContainer.className ? '.' + searchResultContainer.className.replace(/\s+/g, '.') : '')); // 检查容器内的链接 const containerLinks = searchResultContainer.querySelectorAll('a'); console.log(`📋 容器内链接数量: ${containerLinks.length}`); } } // 去重处理 const uniqueResults = []; const seenUrls = new Set(); pageSearchResults.forEach(link => { const href = link.href; if (href && !seenUrls.has(href)) { seenUrls.add(href); uniqueResults.push(link); } }); console.log(`📊 搜索结果汇总: 原始 ${pageSearchResults.length} 个,去重后 ${uniqueResults.length} 个`); // 检查是否有下一页,并获取前2页的结果 const hasNextPage = doc.querySelector('.nxt, .next, a[href*="page=2"]'); if (hasNextPage && uniqueResults.length > 0) { console.log('📄 检测到搜索结果有多页,尝试获取第2页...'); try { await new Promise(resolve => setTimeout(resolve, 2000)); const currentUrl = new URL(response.url); currentUrl.searchParams.set('page', '2'); const page2Response = await fetch(currentUrl.toString(), { method: 'GET', credentials: 'include', headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3', 'Referer': response.url, 'User-Agent': navigator.userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' } }); if (page2Response.ok) { const page2Html = await page2Response.text(); const page2Doc = new DOMParser().parseFromString(page2Html, 'text/html'); // 提取第2页结果 const page2Results = []; selectors.forEach(selector => { const elements = page2Doc.querySelectorAll(selector); if (elements.length > 0) { page2Results.push(...Array.from(elements)); } }); // 去重并合并第2页结果 page2Results.forEach(link => { const href = link.href; if (href && !seenUrls.has(href)) { seenUrls.add(href); uniqueResults.push(link); } }); console.log(`✅ 第2页找到额外 ${page2Results.length} 个结果,总计 ${uniqueResults.length} 个`); } else { console.log('❌ 获取第2页失败:', page2Response.status); } } catch (error) { console.warn('⚠️ 获取第2页时出错:', error.message); } } // 使用去重后的结果 const finalResults = uniqueResults; const currentResults = finalResults.map(link => { const originalTitle = link.textContent.trim(); let url = link.href; if (!originalTitle || originalTitle === '快速') return null; if (url.startsWith('/')) { url = `https://bbs.yamibo.com${url}`; } else if (url.startsWith('forum.php')) { url = `https://bbs.yamibo.com/${url}`; } const threadId = url.match(/tid=(\d+)/)?.[1] || url.match(/thread-(\d+)-/)?.[1]; if (!threadId) return null; // ✨ 智能匹配过滤(针对多单词英文搜索优化) const hasEnglish = /[A-Za-z]/.test(searchTerm); if (hasEnglish && searchTerm.length >= 4) { const titleLower = originalTitle.toLowerCase(); const searchLower = searchTerm.toLowerCase(); // 对于多单词英文搜索,检查是否包含所有关键单词 if (searchTerm.includes(' ')) { const searchWords = searchTerm.toLowerCase().split(/\s+/).filter(word => word.length > 2); const allWordsFound = searchWords.every(word => titleLower.includes(word)); if (!allWordsFound) { console.log(` ❌ 多词过滤: "${originalTitle}" 不包含所有关键词 [${searchWords.join(', ')}]`); return null; } else { console.log(` ✅ 多词匹配: "${originalTitle}" 包含所有关键词 [${searchWords.join(', ')}]`); } } else { // 单词搜索,直接检查包含关系 if (!titleLower.includes(searchLower)) { console.log(` ❌ 单词过滤: "${originalTitle}" 不包含 "${searchTerm}"`); return null; } else { console.log(` ✅ 单词匹配: "${originalTitle}" 包含 "${searchTerm}"`); } } } // 直接使用原始标题进行相似度计算,不要预先清理 const similarity = calculateSimilarity(seriesTitle, originalTitle); return { threadId, title: originalTitle, originalTitle: originalTitle, url, normalizedTitle: normalizeSeriesTitle(originalTitle), similarity, searchTerm }; }).filter(Boolean); // 合并当前搜索结果 currentResults.forEach(item => { const existing = searchResults.get(item.threadId); if (!existing || item.similarity > existing.similarity) { searchResults.set(item.threadId, item); } }); console.log(`✅ 搜索词 "${searchTerm}" 完成,找到 ${currentResults.length} 个结果`); // 详细显示搜索结果 if (currentResults.length > 0) { console.log(` 搜索结果详情:`); currentResults.slice(0, 5).forEach((result, idx) => { console.log(` ${idx + 1}. "${result.title}" (相似度: ${(result.similarity * 100).toFixed(1)}%)`); }); } else { console.log(` ⚠️ 搜索词 "${searchTerm}" 未找到任何结果`); // 🔄 回退策略:对于多单词英文搜索无结果,立即尝试首个单词搜索 if (/^[A-Za-z\s]+$/.test(searchTerm) && searchTerm.includes(' ')) { const firstWord = searchTerm.split(/\s+/)[0]; if (firstWord.length >= 4 && !searchTerms.includes(firstWord)) { console.log(`🔄 多词英文搜索无结果,立即尝试首词: "${firstWord}"`); try { // 等待短暂间隔后搜索首词 await new Promise(resolve => setTimeout(resolve, 3000)); // 递归调用相同的搜索逻辑(复用现有的表单构建和请求逻辑) const backupFormData = new FormData(); backupFormData.append('formhash', formHash); backupFormData.append('srchtxt', firstWord); backupFormData.append('srchfid[]', currentForumId); backupFormData.append('orderby', 'dateline'); backupFormData.append('ascdesc', 'desc'); backupFormData.append('searchsubmit', 'yes'); const backupResponse = await fetch('https://bbs.yamibo.com/search.php?mod=forum', { method: 'POST', credentials: 'include', headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3', 'Origin': 'https://bbs.yamibo.com', 'Referer': 'https://bbs.yamibo.com/search.php', 'User-Agent': navigator.userAgent }, body: backupFormData }); if (backupResponse.ok) { const backupHtml = await backupResponse.text(); const backupDoc = new DOMParser().parseFromString(backupHtml, 'text/html'); const backupResults = []; const selectors = [ '#threadlist li.pbw h3.xs3 a', '#threadlist .xst', 'a.xst', 'a[href*="thread-"]' ]; selectors.forEach(selector => { const elements = backupDoc.querySelectorAll(selector); if (elements.length > 0) { backupResults.push(...Array.from(elements)); } }); const processedBackupResults = backupResults.map(link => { const originalTitle = link.textContent.trim(); let url = link.href; if (!originalTitle || originalTitle === '快速') return null; if (url.startsWith('/')) url = `https://bbs.yamibo.com${url}`; const threadId = url.match(/tid=(\d+)/)?.[1] || url.match(/thread-(\d+)-/)?.[1]; if (!threadId) return null; // 对回退结果进行更严格的过滤:必须包含原搜索词的多个关键词 const titleLower = originalTitle.toLowerCase(); const originalWords = searchTerm.toLowerCase().split(/\s+/).filter(w => w.length > 2); const matchCount = originalWords.filter(word => titleLower.includes(word)).length; // 只保留包含原搜索词至少一半关键词的结果 if (matchCount < Math.ceil(originalWords.length / 2)) { return null; } const similarity = calculateSimilarity(seriesTitle, originalTitle); return { threadId, title: originalTitle, originalTitle: originalTitle, url, normalizedTitle: normalizeSeriesTitle(originalTitle), similarity, searchTerm: `${firstWord}(回退)` }; }).filter(Boolean); if (processedBackupResults.length > 0) { console.log(`🎯 首词回退搜索找到 ${processedBackupResults.length} 个相关结果`); processedBackupResults.forEach(item => { const existing = searchResults.get(item.threadId); if (!existing || item.similarity > existing.similarity) { searchResults.set(item.threadId, item); } }); } } } catch (backupError) { console.warn('⚠️ 首词回退搜索失败:', backupError.message); } } } } consecutiveFailures = 0; } catch (error) { console.warn(`❌ 搜索词 "${searchTerm}" 失败:`, error.message); consecutiveFailures++; // 继续下一个搜索词 } } // 智能筛选和排序 let directory = Array.from(searchResults.values()) .filter(item => item.similarity > 0.7); // 简单按发帖时间排序(threadId小的是老帖子,排在前面) directory.sort((a, b) => { const threadIdA = parseInt(a.threadId); const threadIdB = parseInt(b.threadId); console.log(`🔄 排序比较: "${a.title}" (ID:${threadIdA}) vs "${b.title}" (ID:${threadIdB})`); console.log(` 按threadId排序: ${threadIdA} - ${threadIdB} = ${threadIdA - threadIdB}`); return threadIdA - threadIdB; }); console.log(`📊 智能筛选后: 找到 ${directory.length} 个相关章节`); directory.forEach(item => { const chapterNum = extractChapterNumber(item.title); console.log(` "${item.title}" (章节号: ${chapterNum}, 相似度: ${(item.similarity * 100).toFixed(1)}%)`); }); if (directory.length > 0) { // 缓存成功的搜索结果 searchCache.set(cacheKey, directory); // 同时保存到持久化缓存 const seriesKey = generateSeriesKey(seriesTitle); setCachedDirectory(seriesKey, directory); return directory; } console.log('❌ 所有搜索词都未找到相关章节'); // 只有在第一轮搜索完全失败时才尝试备用策略 if (directory.length === 0 && consecutiveFailures < maxFailures) { console.log('🔄 第一轮搜索无结果,尝试简单备用搜索...'); // 生成一个简单的备用搜索词 let backupTerm = ''; // 优先提取最长的中文片段(过滤噪音词) const chineseMatches = seriesTitle.match(/[\u4e00-\u9fa5]{4,}/g); if (chineseMatches && chineseMatches.length > 0) { // 找到第一个不是噪音的中文片段 const validChinese = chineseMatches .sort((a, b) => b.length - a.length) .find(match => !isNoiseText(match)); if (validChinese) { backupTerm = validChinese.substring(0, 5); console.log('🔤 备用搜索使用中文片段:', backupTerm); } else { console.log('❌ 所有中文片段都被判定为噪音,跳过备用搜索'); } } else { // 如果没有中文,提取日文片段 const japaneseMatches = seriesTitle.match(/[\u3040-\u309F\u30A0-\u30FF]{3,}/g); if (japaneseMatches && japaneseMatches.length > 0) { const validJapanese = japaneseMatches.find(match => !isNoiseText(match)); if (validJapanese) { backupTerm = validJapanese.substring(0, 5); console.log('🔤 备用搜索使用日文片段:', backupTerm); } else { console.log('❌ 所有日文片段都被判定为噪音,跳过备用搜索'); } } } if (backupTerm) { try { console.log('🔍 执行备用搜索:', backupTerm); // 等待10秒 await new Promise(resolve => setTimeout(resolve, 10000)); const formData = new FormData(); formData.append('formhash', getFormHash() || ''); formData.append('srchtxt', backupTerm); formData.append('srchfid[]', '30'); formData.append('orderby', 'dateline'); formData.append('ascdesc', 'desc'); formData.append('searchsubmit', 'yes'); const response = await fetch('https://bbs.yamibo.com/search.php?mod=forum', { method: 'POST', credentials: 'include', headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3', 'Referer': 'https://bbs.yamibo.com/search.php', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' }, body: formData }); if (response.ok) { const html = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const backupPageResults = []; const threadList = doc.querySelectorAll('#threadlist li.pbw h3.xs3 a'); if (threadList.length > 0) { backupPageResults.push(...Array.from(threadList)); } else { backupPageResults.push(...Array.from(doc.querySelectorAll('a.xst, a[href*="thread-"]'))); } const backupResults = backupPageResults.map(link => { const title = link.textContent.trim(); let url = link.href; if (!title || title === '快速') return null; if (url.startsWith('/')) { url = `https://bbs.yamibo.com${url}`; } else if (url.startsWith('forum.php')) { url = `https://bbs.yamibo.com/${url}`; } const threadId = url.match(/tid=(\d+)/)?.[1] || url.match(/thread-(\d+)-/)?.[1]; if (!threadId) return null; const similarity = calculateSimilarity(seriesTitle, title); return { threadId, title, originalTitle: title, url, normalizedTitle: normalizeSeriesTitle(title), similarity, searchTerm: `backup:${backupTerm}` }; }).filter(Boolean); backupResults.forEach(item => { const existing = searchResults.get(item.threadId); if (!existing || item.similarity > existing.similarity) { searchResults.set(item.threadId, item); } }); console.log(`✅ 备用搜索找到 ${backupResults.length} 个结果`); directory = Array.from(searchResults.values()) .filter(item => item.similarity > 0.05); } else { console.log(`❌ 备用搜索失败: ${response.status}`); } } catch (error) { console.warn('❌ 备用搜索失败:', error.message); } } } // 如果搜索完全失败 if (directory.length === 0) { console.log('❌ 所有搜索词都未找到相关章节'); } // 最后兜底:提供当前章节作为基础目录 const currentThreadId = getCurrentThreadId(); if (directory.length === 0 && currentThreadId) { console.log('🔧 提供当前章节作为基础目录'); return [{ threadId: currentThreadId, title: seriesTitle, originalTitle: seriesTitle, url: window.location.href, normalizedTitle: normalizeSeriesTitle(seriesTitle), similarity: 1.0, searchTerm: 'current' }]; } return directory; } catch (error) { console.error('❌ 搜索目录失败:', error); console.error('错误堆栈:', error.stack); return []; } } // 提取章节号(支持小数格式和复杂排序) function extractChapterNumber(title) { if (!title || typeof title !== 'string') return 0; // 特殊章节(番外/彩页等)放到最后 const specialPatterns = [ /番外|外传|特别篇|SP|特典|彩页|后记|前记|预告|PV|CM/i, /extra|special|omake|bonus/i ]; for (const p of specialPatterns) { if (p.test(title)) return 99999; } const patterns = [ { regex: /(?:第\s*)?(\d+\.\d+)\s*[话話章节節回卷篇期]?/i, priority: 1, type: 'decimal' }, { regex: /第\s*(\d+)\s*[话話章节節回卷篇]/i, priority: 2, type: 'standard' }, { regex: /(\d+)\s*[话話章节節]/i, priority: 3, type: 'numbered' }, { regex: /[\[\((](\d+(?:\.\d+)?)[\]\))]/, priority: 4, type: 'bracketed' }, { regex: /(?:第\s*)?(\d+)\s*卷/i, priority: 5, type: 'volume' }, { regex: /\s(\d+(?:\.\d+)?)$/, priority: 6, type: 'trailing' }, { regex: /^(\d+(?:\.\d+)?)\s/, priority: 7, type: 'leading' }, { regex: /(\d+(?:\.\d+)?)/, priority: 8, type: 'any' } ]; let bestMatch = null; let bestPriority = Infinity; for (const p of patterns) { const m = title.match(p.regex); if (m && p.priority < bestPriority) { bestMatch = m[1]; bestPriority = p.priority; } } if (bestMatch != null) { if (bestMatch.includes('.')) { // 小数章如 9.5 -> 9500(保持整数比较) const v = parseFloat(bestMatch); if (!isNaN(v)) return Math.round(v * 1000); } else { const v = parseInt(bestMatch, 10); if (!isNaN(v)) return v; } } // 简单中文数字映射(可扩展) const chineseNumbers = { '零': 0, '一': 1, '二': 2, '三': 3, '四': 4, '五': 5, '六': 6, '七': 7, '八': 8, '九': 9, '十': 10, '十一': 11, '十二': 12, '十三': 13, '十四': 14, '十五': 15, '十六': 16, '十七': 17, '十八': 18, '十九': 19, '二十': 20 }; for (const [ch, num] of Object.entries(chineseNumbers)) { if (title.includes(`第${ch}`) || title.includes(`${ch}话`) || title.includes(`${ch}章`)) { return num; } } return 0; } function getFormHash() { if (isReading) { console.log('🔑 FormHash: 阅读模式下跳过FormHash'); return ''; } const formHashInput = document.querySelector('input[name="formhash"]'); return formHashInput ? formHashInput.value : ''; } // 从URL识别帖子所属版块 function detectForumId(url = window.location.href) { // 方式1: 从URL参数中提取 fid const fidMatch = url.match(/[?&]fid=(\d+)/); if (fidMatch) { const fid = fidMatch[1]; console.log(`📍 从URL参数识别版块: fid=${fid}`); return fid; } // 方式2: 从forum.php路径中提取 const forumMatch = url.match(/forum-(\d+)-/); if (forumMatch) { const fid = forumMatch[1]; console.log(`📍 从forum路径识别版块: fid=${fid}`); return fid; } // 方式3: 从thread URL中推断(访问页面时检查页面元素) try { const breadcrumbs = document.querySelectorAll('.z a[href*="forum"]'); const forumIds = []; for (const breadcrumb of breadcrumbs) { // 尝试从 forum-数字- 格式提取 const breadcrumbMatch1 = breadcrumb.href.match(/forum-(\d+)-/); if (breadcrumbMatch1) { const fid = breadcrumbMatch1[1]; const name = breadcrumb.textContent.trim(); forumIds.push({ fid, name, element: breadcrumb }); continue; } // 尝试从 fid= 参数提取 const breadcrumbMatch2 = breadcrumb.href.match(/fid=(\d+)/); if (breadcrumbMatch2) { const fid = breadcrumbMatch2[1]; const name = breadcrumb.textContent.trim(); forumIds.push({ fid, name, element: breadcrumb }); } } // ✨ 选择最后一个版块(最精确的子版块) if (forumIds.length > 0) { const lastForum = forumIds[forumIds.length - 1]; console.log(`📍 从面包屑导航识别版块: fid=${lastForum.fid} (${lastForum.name})`); if (forumIds.length > 1) { console.log(` 💡 跳过了 ${forumIds.length - 1} 个父级版块,选择最精确的子版块`); } return lastForum.fid; } } catch (e) { console.log('⚠️ 无法从面包屑导航识别版块:', e.message); } // 方式4: 从当前页面的返回链接中提取 try { const backLinks = document.querySelectorAll('a[href*="forum"]'); for (const backLink of backLinks) { const linkMatch = backLink.href.match(/fid=(\d+)/); if (linkMatch) { const fid = linkMatch[1]; console.log(`📍 从返回链接识别版块: fid=${fid}`); return fid; } } } catch (e) { // ignore } // 默认返回中文百合漫画区 (fid=30) console.log('📍 无法识别版块,默认使用中文百合漫画区 (fid=30)'); return '30'; } // 获取版块名称(用于日志显示) function getForumName(fid) { const forumNames = { '30': '中文百合漫画区', '37': '百合漫画图源区' }; return forumNames[fid] || `版块${fid}`; } // 检查两个标题是否包含相似词汇 function containsSimilarWords(title1, title2) { const words1 = title1.toLowerCase().split(/[\s\-_]+/).filter(w => w.length > 1); const words2 = title2.toLowerCase().split(/[\s\-_]+/).filter(w => w.length > 1); console.log(`🔍 词汇比较: "${title1}" vs "${title2}"`); console.log(` 词汇1: [${words1.join(', ')}]`); console.log(` 词汇2: [${words2.join(', ')}]`); let commonWords = 0; const matchedPairs = []; for (const word1 of words1) { for (const word2 of words2) { if (word1.includes(word2) || word2.includes(word1)) { commonWords++; matchedPairs.push(`"${word1}" ⟷ "${word2}"`); break; } } } const threshold = Math.min(2, Math.min(words1.length, words2.length)); const isMatch = commonWords >= threshold; console.log(` 匹配词汇: ${matchedPairs.join(', ')}`); console.log(` 匹配数量: ${commonWords}/${threshold} (需要: ${threshold})`); console.log(` 结果: ${isMatch ? '✅ 匹配' : '❌ 不匹配'}`); return isMatch; } /*** 悬浮按钮 ***/ const ioniconsModule = document.createElement('script'); ioniconsModule.type = 'module'; ioniconsModule.src = 'https://unpkg.com/[email protected]/dist/ionicons/ionicons.esm.js'; document.head.appendChild(ioniconsModule); const ioniconsNomodule = document.createElement('script'); ioniconsNomodule.noModule = true; ioniconsNomodule.src = 'https://unpkg.com/[email protected]/dist/ionicons/ionicons.js'; document.head.appendChild(ioniconsNomodule); const fontLink = document.createElement('link'); fontLink.href = 'https://fonts.googleapis.com/css2?family=Poppins&display=swap'; fontLink.rel = 'stylesheet'; document.head.appendChild(fontLink); const button = document.createElement('button'); button.id = 'reader-toggle'; button.innerHTML = ` <span class="icon"><ion-icon name="book-outline"></ion-icon></span> <span class="title">进入阅读模式</span> `; document.body.appendChild(button); GM_addStyle(` #reader-toggle { position: fixed; top: 20%; right: 0px; transform: translateY(-50%); z-index: 99999; list-style: none; width: 60px; height: 60px; background: white; border-radius: 60px; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); cursor: pointer; display: flex; justify-content: center; align-items: center; transition: 0.5s; border: none; font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", "Source Han Sans SC", sans-serif; } #reader-toggle::before { content: ""; position: absolute; inset: 0; border-radius: 40px; background: linear-gradient(45deg, #56CCF2, #2F80ED); opacity: 0; transition: 0.5s; } #reader-toggle::after { content: ""; position: absolute; top: 10px; width: 100%; height: 100%; border-radius: 40px; background: linear-gradient(45deg, #56CCF2, #2F80ED); filter: blur(15px); z-index: -1; opacity: 0; transition: 0.5s; } #reader-toggle:hover { width: 180px; box-shadow: 0 10px 25px rgba(0, 0, 0, 0); } #reader-toggle:hover::before { opacity: 1; } #reader-toggle:hover::after { opacity: 0.5; } #reader-toggle .icon { position: absolute; left: 0; top: 0; width: 60px; height: 60px; color: #777; font-size: 2em; transition: 0.5s; transition-delay: 0.25s; display: flex; align-items: center; justify-content: center; } #reader-toggle .icon ion-icon { width: 24px; height: 24px; } #reader-toggle:hover .icon { transform: scale(0); color: #fff; transition-delay: 0s; } #reader-toggle .title { position: absolute; color: #fff; font-size: 1.4em; text-transform: uppercase; letter-spacing: 0.1em; transform: scale(0); transition: 0.5s; transition-delay: 0s; } #reader-toggle:hover .title { transform: scale(1); transition-delay: 0.25s; } #directory-sidebar { position: fixed; top: 0; right: -400px; width: 400px; height: 100%; background: linear-gradient(145deg, #1e2837 0%, #2a3441 25%, #1f2937 50%, #374151 75%, #1e293b 100%); z-index: 2147483647; transition: right 0.3s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: -15px 0 40px rgba(0, 0, 0, 0.4); display: flex; flex-direction: column; color: white; backdrop-filter: blur(20px); border: none; } #directory-sidebar.open { right: 0; } /* 遮罩层 */ #directory-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.6); z-index: 2147483646; opacity: 0; visibility: hidden; transition: opacity 0.3s ease, visibility 0.3s ease; backdrop-filter: blur(2px); } #directory-overlay.show { opacity: 1; visibility: visible; } #directory-header { display: flex; justify-content: space-between; align-items: center; padding: 10px 20px; background: linear-gradient(135deg, rgba(59, 130, 246, 0.15) 0%, rgba(99, 102, 241, 0.1) 100%); backdrop-filter: blur(15px); border: none; position: relative; } #directory-header::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 1px; background: linear-gradient(90deg, transparent 0%, rgba(59, 130, 246, 0.4) 50%, transparent 100%); } #directory-title { font-size: 16px; font-weight: 600; color: white; letter-spacing: 0.5px; } #directory-close { background: rgba(255, 255, 255, 0.1); border: none; font-size: 18px; cursor: pointer; color: white; padding: 8px; border-radius: 8px; transition: all 0.2s ease; width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(10px); } #directory-close:hover { background: rgba(255, 255, 255, 0.2); transform: scale(1.05); } #directory-close:active { transform: scale(0.95); } #directory-content { flex: 1; overflow-y: auto; padding: 16px 0; scrollbar-width: thin; scrollbar-color: rgba(255, 255, 255, 0.3) transparent; } #directory-content::-webkit-scrollbar { width: 6px; } #directory-content::-webkit-scrollbar-track { background: transparent; } #directory-content::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.3); border-radius: 3px; } #directory-content::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.5); } #directory-list { list-style: none; padding: 0 16px; margin: 0; } .directory-item { display: block; padding: 14px 16px; margin: 0 0 8px 0; background: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.04) 100%); border: none; border-radius: 12px; text-decoration: none !important; color: rgba(255, 255, 255, 0.9); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); cursor: pointer; font-size: 14px; font-weight: 500; backdrop-filter: blur(10px); position: relative; overflow: hidden; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .directory-item::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.1), transparent); transform: translateX(-100%); transition: transform 0.6s; } .directory-item:hover::before { transform: translateX(100%); } .directory-item:hover { background: rgba(255, 255, 255, 0.15); transform: translateX(6px); color: white; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); } .directory-item.current { background: linear-gradient(135deg, #FF6B6B, #FF8E53); color: white; box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3); font-weight: 600; } .directory-item.current:hover { transform: translateX(6px) scale(1.02); box-shadow: 0 6px 20px rgba(255, 107, 107, 0.4); } .directory-item.mainpost { background: linear-gradient(135deg, #4CAF50, #45a049); color: white; } .directory-item.mainpost:hover { background: linear-gradient(135deg, #45a049, #3d8b40); } #directory-loading { text-align: center; padding: 60px 20px; color: rgba(255, 255, 255, 0.7); font-size: 16px; font-weight: 500; } #directory-loading::before { content: '📚'; display: block; font-size: 32px; margin-bottom: 16px; animation: pulse 2s infinite; } @keyframes pulse { 0%, 100% { opacity: 0.7; } 50% { opacity: 1; } } #directory-empty { text-align: center; padding: 60px 20px; color: rgba(255, 255, 255, 0.6); font-size: 16px; } #directory-empty::before { content: '📖'; display: block; font-size: 32px; margin-bottom: 16px; opacity: 0.6; } /* 目录侧边栏的tooltip样式 */ #directory-sidebar [data-tooltip]:hover::after { content: attr(data-tooltip); position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); background: rgba(0, 0, 0, 0.6); color: white; padding: 8px 12px; border-radius: 6px; font-size: 12px; white-space: nowrap; z-index: 2147483650; pointer-events: none; margin-bottom: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); opacity: 0; animation: tooltipFadeIn 0.2s ease forwards; } #directory-sidebar [data-tooltip]:hover::before { content: ''; position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); border: 6px solid transparent; border-top-color: rgba(0, 0, 0, 0.6); z-index: 2147483650; pointer-events: none; margin-bottom: 2px; opacity: 0; animation: tooltipFadeIn 0.2s ease forwards; } `); // 添加快捷键:Ctrl+Shift+R 切换阅读模式 document.addEventListener('keydown', e => { if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'r') { e.preventDefault(); if (!isReading) enterReader(); else exitReader(); } }); button.addEventListener('click', () => { if (!isReading) { enterReader(); setAutoReaderStatus(true); // 开启全局阅读器模式 } else { exitReader(); } }); /*** 目录功能 ***/ async function showDirectoryModal() { // 创建遮罩层 const overlay = document.createElement('div'); overlay.id = 'directory-overlay'; // 创建侧边栏 const sidebar = document.createElement('div'); sidebar.id = 'directory-sidebar'; sidebar.innerHTML = ` <div id="directory-header"> <div id="directory-title">目录</div> <button id="directory-close">×</button> </div> <div id="directory-content"> <div id="directory-loading">正在搜索相关章节...</div> <ul id="directory-list" style="display: none;"></ul> <div id="directory-empty" style="display: none;">未找到相关章节</div> </div> `; document.body.appendChild(overlay); document.body.appendChild(sidebar); // 显示遮罩和侧边栏 setTimeout(() => { overlay.classList.add('show'); sidebar.classList.add('open'); }, 10); // 关闭按钮事件 const closeBtn = sidebar.querySelector('#directory-close'); closeBtn.addEventListener('click', closeSidebar); // 点击遮罩关闭 overlay.addEventListener('click', closeSidebar); function closeSidebar() { overlay.classList.remove('show'); sidebar.classList.remove('open'); setTimeout(() => { overlay.remove(); sidebar.remove(); }, 300); } // 开始搜索目录 loadSeriesDirectory(sidebar); } async function loadSeriesDirectory(modal) { console.log('📖 开始加载系列目录...'); // 如果已有预加载的目录,直接使用 if (seriesDirectory.length > 0) { console.log('✅ 使用预加载的目录:', seriesDirectory.length, '个章节'); displayDirectory(modal, seriesDirectory); return; } // 如果预加载目录为空,先检查持久化缓存 if (originalSeriesTitle || savedThreadTitle) { const titleToCheck = originalSeriesTitle || savedThreadTitle; const seriesKey = generateSeriesKey(titleToCheck); const cachedDirectory = getCachedDirectory(seriesKey); if (cachedDirectory && cachedDirectory.length > 0) { console.log('✅ 从持久化缓存加载目录:', cachedDirectory.length, '个章节'); seriesDirectory = cachedDirectory; displayDirectory(modal, seriesDirectory); return; } } // 尝试多种方式获取页面标题 let titleElement = null; let threadTitle = ''; console.log('🔍 尝试查找页面标题...'); // 简化标题提取逻辑 const h1Element = document.querySelector('h1.ts'); if (h1Element) { const subjectSpan = h1Element.querySelector('#thread_subject, span[id^="thread_subject"]'); if (subjectSpan) { threadTitle = subjectSpan.textContent.trim(); console.log(`✅ 找到标题: "${threadTitle}"`); } else { threadTitle = h1Element.textContent.trim(); console.log(`✅ 找到h1文本: "${threadTitle}"`); } } else { // 兜底:使用原有逻辑 const titleSelectors = [ '#thread_subject', 'span[id^="thread_subject"]', '.ts a', 'h1.ts', '.ntn a', '.ntn', '.bm .mbm h1', '.ts' ]; for (const selector of titleSelectors) { titleElement = document.querySelector(selector); if (titleElement) { threadTitle = titleElement.textContent.trim(); console.log(`✅ 找到标题元素: "${selector}" -> "${threadTitle}"`); break; } else { console.log(`❌ 未找到: "${selector}"`); } } } // 在阅读模式下,优先使用原始系列标题进行目录搜索(避免使用错误的标题) if (isReading) { if (originalSeriesTitle) { threadTitle = originalSeriesTitle; console.log('🎯 阅读模式下使用原始系列标题:', threadTitle); } else if (savedThreadTitle) { threadTitle = savedThreadTitle; console.log('📦 阅读模式下使用预存标题:', threadTitle); } } else if (!threadTitle && originalSeriesTitle) { threadTitle = originalSeriesTitle; console.log('🎯 使用原始系列标题:', threadTitle); } else if (!threadTitle && savedThreadTitle) { threadTitle = savedThreadTitle; console.log('📦 使用预存标题:', threadTitle); } // 如果还是没找到,尝试从页面title获取 if (!threadTitle) { const pageTitle = document.title; console.log('🔍 尝试从页面title获取:', pageTitle); // 移除论坛名称等后缀,只保留漫画标题部分 if (pageTitle.includes(' - ')) { threadTitle = pageTitle.split(' - ')[0].trim(); console.log('📝 从页面title提取:', threadTitle); } else { threadTitle = pageTitle.replace(/\s*-\s*百合会.*$/, '').trim(); console.log('📝 清理后的标题:', threadTitle); } } const currentThreadId = getCurrentThreadId(); console.log('📋 当前页面信息:'); console.log(' 标题元素:', titleElement ? '✅ 找到' : '❌ 未找到'); console.log(' 页面标题:', threadTitle); console.log(' 线程ID:', currentThreadId); // 调试:打印页面中所有可能的标题元素 if (!titleElement) { console.log('🔍 调试:查找所有可能的标题元素...'); const allH1 = document.querySelectorAll('h1'); const allSpans = document.querySelectorAll('span[id*="thread"], span[id*="subject"]'); const allTs = document.querySelectorAll('.ts, .ntn'); console.log(' - h1元素数量:', allH1.length); allH1.forEach((h1, index) => { if (index < 5) { console.log(` h1[${index}]: "${h1.textContent.trim().substring(0, 50)}" (class: ${h1.className})`); } }); console.log(' - 包含thread/subject的span数量:', allSpans.length); allSpans.forEach((span, index) => { if (index < 5) { console.log(` span[${index}]: id="${span.id}", text="${span.textContent.trim().substring(0, 50)}"`); } }); console.log(' - .ts/.ntn元素数量:', allTs.length); allTs.forEach((ts, index) => { if (index < 5) { console.log(` ts[${index}]: class="${ts.className}", text="${ts.textContent.trim().substring(0, 50)}"`); } }); } if (!threadTitle) { console.log('❌ 页面标题为空,显示空状态'); showDirectoryEmpty(modal); return; } try { console.log('🔍 开始搜索目录...'); const directory = await searchSeriesDirectory(threadTitle); console.log('📊 搜索结果:', directory.length, '个章节'); if (directory.length === 0) { console.log('❌ 未找到章节,显示空状态'); showDirectoryEmpty(modal); return; } console.log('✅ 找到章节,开始显示目录界面'); displayDirectory(modal, directory, threadTitle); } catch (error) { console.error('❌ 加载目录失败:', error); console.error('错误详情:', error.message); console.error('错误堆栈:', error.stack); showDirectoryEmpty(modal, '加载失败,请稍后重试'); } } function displayDirectory(modal, directory, threadTitle = '') { // 显示目录 const loading = modal.querySelector('#directory-loading'); const list = modal.querySelector('#directory-list'); const title = modal.querySelector('#directory-title'); loading.style.display = 'none'; list.style.display = 'block'; // 获取页面标题(如果没有提供) if (!threadTitle) { if (savedThreadTitle) { threadTitle = savedThreadTitle; } else { threadTitle = document.title.split(' - ')[0] || '漫画目录'; } } // 简化标题,只显示"目录" title.textContent = '目录'; console.log('📝 目录标题: 目录'); list.innerHTML = ''; const currentThreadId = getCurrentThreadId(); directory.forEach((item, index) => { const li = document.createElement('li'); const link = document.createElement('a'); link.className = 'directory-item'; if (item.source === 'mainpost') { link.classList.add('mainpost'); } link.href = item.url; // 为主楼提取的目录优化显示格式 if (item.source === 'mainpost') { let displayText = item.title; // 识别不同的章节格式并优化显示 if (item.title.match(/^\d{1,2}$/)) { displayText = `第${String(item.title).padStart(2, '0')}话`; } else if (item.title.match(/^\d+\.\d+$/)) { displayText = `第${item.title}话`; } else if (item.title.match(/^\d+[话話章节節回卷篇]/i)) { displayText = item.title; } else if (item.title.match(/卷|番外|彩页|特典/i)) { displayText = item.title; } else if (item.title.match(/^0\d+$/)) { displayText = `第${item.title}话`; } link.textContent = displayText; } else { link.textContent = item.title; } // 添加点击事件处理,无缝加载新章节内容 link.addEventListener('click', async (e) => { e.preventDefault(); console.log('🔄 正在加载章节:', item.title); // 关闭侧边栏 const overlay = document.getElementById('directory-overlay'); const sidebar = document.getElementById('directory-sidebar'); if (overlay && sidebar) { overlay.classList.remove('show'); sidebar.classList.remove('open'); setTimeout(() => { overlay.remove(); sidebar.remove(); }, 300); } // 无缝加载新章节内容 await loadNewChapter(item.url, item.title); }); // 高亮当前章节 if (item.threadId === currentThreadId) { link.classList.add('current'); console.log(`⭐ 当前章节: ${item.title}`); } li.appendChild(link); list.appendChild(li); console.log(`📄 章节 ${index + 1}: ${item.title}`); }); console.log('✅ 目录界面显示完成'); } function showDirectoryEmpty(modal, message = '未找到相关章节') { const loading = modal.querySelector('#directory-loading'); const empty = modal.querySelector('#directory-empty'); loading.style.display = 'none'; empty.style.display = 'block'; empty.textContent = message; } function getCurrentThreadId() { const href = window.location.href; let match = href.match(/thread-(\d+)-/); if (match) { return match[1]; } try { const url = new URL(href); const tidParam = url.searchParams.get('tid'); if (tidParam) { return tidParam; } } catch (e) { // ignore } match = href.match(/[?&]tid=(\d+)/); return match ? match[1] : null; } // 清理所有缓存,并清空内存缓存 function clearAllCache() { console.log('🧹 清理所有目录缓存 (session + GM) ...'); let cleanedCount = 0; // 1) 清理 sessionStorage 前缀条目 try { const sKeys = Object.keys(sessionStorage).filter(k => k.startsWith(CACHE_PREFIX)); sKeys.forEach(k => { try { sessionStorage.removeItem(k); cleanedCount++; } catch (e) {} }); } catch (e) { } // 2) 清理 GM_* 条目(通过索引枚举) try { if (typeof GM_getValue === 'function' && typeof GM_setValue === 'function') { let idx = GM_getValue(GM_INDEX_KEY, undefined); if (typeof idx === 'string') { try { idx = JSON.parse(idx); } catch (e) { idx = []; } } if (!Array.isArray(idx)) idx = []; for (const key of idx) { try { storageRemove(key); cleanedCount++; } catch (e) {} } try { GM_setValue(GM_INDEX_KEY, []); } catch (e) {} } } catch (e) { } // 3) 清理内存缓存 try { if (searchCache && searchCache.clear) searchCache.clear(); } catch (e) {} try { if (directoryMemoryCache && directoryMemoryCache.clear) directoryMemoryCache.clear(); } catch (e) {} console.log(`✅ 已清理 ${cleanedCount} 个持久化缓存条目,且已清空内存缓存`); return cleanedCount; } // 查看缓存状态(session + GM 索引 + 内存统计) function viewCacheStatus() { console.log('📊 缓存状态统计:'); // sessionStorage 条目 let sessionList = []; try { sessionList = Object.keys(sessionStorage).filter(k => k.startsWith(CACHE_PREFIX)); } catch (e) { sessionList = []; } // GM 索引条目 let gmList = []; try { if (typeof GM_getValue === 'function') { let idx = GM_getValue(GM_INDEX_KEY, undefined); if (typeof idx === 'string') { try { idx = JSON.parse(idx); } catch (e) { idx = []; } } if (Array.isArray(idx)) gmList = idx.slice(); } } catch (e) { gmList = []; } console.log(`📦 sessionStorage 缓存数量: ${sessionList.length}`); console.log(`📦 GM_* 缓存数量 (index): ${gmList.length}`); console.log(`🧠 内存缓存 (searchCache): ${typeof searchCache !== 'undefined' && searchCache.size !== undefined ? searchCache.size : '未知'}`); console.log(`🧠 内存目录缓存 (directoryMemoryCache): ${typeof directoryMemoryCache !== 'undefined' && directoryMemoryCache.size !== undefined ? directoryMemoryCache.size : '未知'}`); if (sessionList.length > 0) { console.log('\n📋 sessionStorage 详情:'); sessionList.forEach((k, i) => { try { const obj = JSON.parse(sessionStorage.getItem(k)); const age = Math.round((Date.now() - (obj.ts || obj.timestamp || 0)) / (1000*60*60)); const count = (obj.d || obj.data) ? (obj.d || obj.data).length : '未知'; console.log(` ${i+1}. ${k.replace(CACHE_PREFIX+'session-','')}: ${count}章, ${age}小时前`); } catch (e) { console.log(` ${i+1}. ${k} (无法解析)`); } }); } if (gmList.length > 0) { console.log('\n📋 GM_* 详情 (由索引列出):'); gmList.forEach((k, i) => { try { const obj = storageGet(k, null); const age = obj ? Math.round((Date.now() - (obj.ts || obj.timestamp || 0)) / (1000*60*60)) : '未知'; const count = obj ? ((obj.d || obj.data) ? (obj.d || obj.data).length : '未知') : '未知'; console.log(` ${i+1}. ${k.replace(CACHE_PREFIX,'')}: ${count}章, ${age}小时前`); } catch (e) { console.log(` ${i+1}. ${k} (无法读取)`); } }); } return { sessionCount: sessionList.length, gmCount: gmList.length, memorySearchCount: typeof searchCache !== 'undefined' && searchCache.size !== undefined ? searchCache.size : null, memoryDirCount: typeof directoryMemoryCache !== 'undefined' && directoryMemoryCache.size !== undefined ? directoryMemoryCache.size : null }; } // 必须识别作者 UID,按楼层顺序收集该作者的图片;识别失败直接返回空数组,避免全页搜索 function collectImagesFromDocument(doc = document) { try { const normalizeUrl = u => { if (!u) return null; u = String(u).trim(); if (!u) return null; if (u.startsWith('//')) u = (location.protocol || 'https:') + u; if (!/^https?:\/\//i.test(u)) u = 'https://bbs.yamibo.com/' + u.replace(/^\/+/, ''); u = u.replace(/^https?:\/\/https?:\/\//, 'https://'); return u; }; const isPlaceholder = (img, src) => { if (!src) return true; if (src.includes('static/image/common/none.gif')) { if (img.getAttribute('file') || img.getAttribute('zoomfile') || img.getAttribute('aid')) return false; return true; } return false; }; // 识别并忽略头像/评分用户小图等噪音图片 const isIgnoredImage = (img, src) => { try { if (!src && !img) return true; const s = String(src || '').toLowerCase(); // 明确的头像路径(示例:/uc_server/data/avatar/...) if (/\/uc_server\/data\/avatar\//i.test(s)) return true; // 常见 avatar 文件名 / avatar_small if (/avatar(?:_small|_middle)?\.(jpg|jpeg|png|gif|webp)$/i.test(s)) return true; if (s.includes('/avatar/') || s.includes('/avatars/')) return true; // class 名称包含 avatar 或 user_avatar 等 const cls = (img && img.className) ? String(img.className).toLowerCase() : ''; if (cls.includes('user_avatar') || cls.includes('avatar') || cls.includes('userpic') || cls.includes('authoravatar')) return true; // 如果图片位于明显的评分/头像容器,忽略 if (img && (img.closest('.postrate') || img.closest('.post-ratings') || img.closest('.postratedby') || img.closest('.poster') || img.closest('.author') || img.closest('.avatar') || img.closest('.user')) ) { return true; } // 若图片尺寸非常小(通常为头像),忽略阈值可调整 const wAttr = img && (img.getAttribute('width') || img.width); const hAttr = img && (img.getAttribute('height') || img.height); const w = wAttr ? parseInt(wAttr, 10) : 0; const h = hAttr ? parseInt(hAttr, 10) : 0; if ((w > 0 && w < 80) || (h > 0 && h < 80)) return true; // 若 src 含有明显的评分/头像关键词 if (s.includes('rating') || s.includes('score') || s.includes('grade') || s.includes('usericon')) return true; return false; } catch (e) { return false; } }; const preferredSelectors = [ 'ignore_js_op img', '.savephotop img', 'div.t_f img', 'td.t_f img', '.pcb img', 'img[zoomfile]', 'img[file]', 'img[aid]', 'img.zoom', 'img' ]; // 定位主楼容器,尝试从中识别作者 UID let mainPost = null; try { const posts = doc.querySelectorAll('[id^="post_"], .post'); if (posts && posts.length) { for (const p of posts) { if (p.id && /^post_\d+/.test(p.id)) { mainPost = p; break; } } if (!mainPost) mainPost = posts[0]; } } catch (e) { mainPost = null; } // 必须识别作者 UID,否则直接返回空数组(避免全页抓取) let authorUid = null; if (mainPost) { try { const a = mainPost.querySelector('a[href*="space-uid-"], a[href*="uid="], a[href*="space.php?uid="]'); if (a && a.getAttribute('href')) { const href = a.getAttribute('href'); const m1 = href.match(/space-uid-(\d+)\.html/); const m2 = href.match(/uid=(\d+)/); if (m1) authorUid = m1[1]; else if (m2) authorUid = m2[1]; } } catch (e) { authorUid = null; } } if (!authorUid) { console.log('⚠️ collectImagesFromDocument: 无法识别作者 UID,跳过图片收集'); return []; } const seen = new Set(); const results = []; // 遍历所有 post_* 楼层,按文档顺序只收集该作者的图片 const postsAll = Array.from(doc.querySelectorAll('[id^="post_"]')); for (const p of postsAll) { try { const authorLink = p.querySelector(`a[href*="space-uid-${authorUid}"], a[href*="uid=${authorUid}"], a[href*="space.php?uid=${authorUid}"]`); if (!authorLink) continue; // 非楼主发言,跳过 for (const sel of preferredSelectors) { const imgs = p.querySelectorAll(sel); for (const img of imgs) { const u = normalizeUrl( img.getAttribute('zoomfile') || img.getAttribute('file') || img.getAttribute('src') || img.getAttribute('data-src') ); if (!u) continue; if (isIgnoredImage(img, u)) continue; if (!seen.has(u)) { seen.add(u); results.push(u); } } } } catch (e) { continue; } } return results; } catch (e) { console.warn('collectImagesFromDocument error', e); return []; } } // 初始化时检查全局阅读器状态 function initAutoReader() { autoReaderEnabled = checkAutoReaderStatus(); console.log('🚀 初始化全局阅读器状态:', autoReaderEnabled); // 清理过期缓存 cleanExpiredCache(); // 检查是否有漫画内容,优先检测主楼 const comicImages = collectImagesFromDocument(document); if (comicImages && comicImages.length > 0) { console.log('🎯 检测到漫画内容,立即开始预取目录...'); // 立即开始预取目录信息,不管是否进入阅读模式 preloadDirectoryInfo(); // 如果全局阅读器开启,自动进入阅读模式 if (autoReaderEnabled && !isReading) { console.log('🎯 自动进入阅读模式'); setTimeout(() => { if (button && button.parentNode) { button.remove(); } enterReader(); }, 1000); } } else { console.log('🔍 尝试预取目录信息...'); preloadDirectoryInfo(); } } // 预加载目录信息 async function preloadDirectoryInfo() { if (seriesDirectory.length > 0) { console.log('� 目录已预加载,跳过重复加载'); return; } try { console.log('�📚 开始预加载目录信息...'); // 获取当前页面标题 let threadTitle = ''; const h1Element = document.querySelector('h1.ts'); if (h1Element) { const subjectSpan = h1Element.querySelector('#thread_subject, span[id^="thread_subject"]'); if (subjectSpan) { threadTitle = subjectSpan.textContent.trim(); } else { threadTitle = h1Element.textContent.trim(); } } else { const titleElement = document.querySelector('#thread_subject, span[id^="thread_subject"]'); if (titleElement) { threadTitle = titleElement.textContent.trim(); } else { const pageTitle = document.title; threadTitle = pageTitle.includes(' - ') ? pageTitle.split(' - ')[0].trim() : pageTitle.replace(/\s*-\s*百合会.*$/, '').trim(); } } if (threadTitle) { // 保存标题信息 savedThreadTitle = threadTitle; originalSeriesTitle = threadTitle; console.log('📖 开始预取目录,使用标题:', threadTitle); // 先检查持久化缓存 const seriesKey = generateSeriesKey(threadTitle); const cachedDirectory = getCachedDirectory(seriesKey); if (cachedDirectory && cachedDirectory.length > 0) { console.log('✅ 从持久化缓存预加载完成:', cachedDirectory.length, '个章节'); seriesDirectory = cachedDirectory; return; } // 如果没有缓存,立即开始异步搜索目录 searchSeriesDirectory(threadTitle).then(directory => { if (directory && directory.length > 0) { seriesDirectory = directory; console.log('✅ 预加载完成,获得', directory.length, '个章节'); } else { console.log('⚠️ 预加载未找到目录'); } }).catch(error => { console.warn('⚠️ 预加载目录失败:', error.message); }); console.log('🚀 预加载任务已启动,后台进行中...'); } else { console.log('⚠️ 无法获取页面标题,跳过预加载'); } } catch (error) { console.warn('⚠️ 预加载目录失败:', error.message); } } // 页面加载完成后初始化 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { setTimeout(initAutoReader, 1000); // 确保页面元素完全加载 }); } else { setTimeout(initAutoReader, 1000); // 给页面充足时间渲染 } /*** 进入阅读模式 ***/ function enterReader() { isReading = true; // 保存原始页面HTML和标题 originalPageHTML = document.body.innerHTML; originalPageTitle = document.title; console.log('💾 已保存原始页面内容'); button.remove(); // 在替换页面内容之前,先提取并保存标题 console.log('🔍 进入阅读模式,保存页面标题...'); // 简化标题提取逻辑 const h1Element = document.querySelector('h1.ts'); if (h1Element) { const subjectSpan = h1Element.querySelector('#thread_subject, span[id^="thread_subject"]'); if (subjectSpan) { savedThreadTitle = subjectSpan.textContent.trim(); console.log('💾 保存标题:', savedThreadTitle); } else { savedThreadTitle = h1Element.textContent.trim(); console.log('💾 保存h1文本:', savedThreadTitle); } } else { // 兜底:尝试其他方式 const threadTitleElement = document.querySelector('#thread_subject, span[id^="thread_subject"]'); if (threadTitleElement) { savedThreadTitle = threadTitleElement.textContent.trim(); console.log('💾 保存标题元素:', savedThreadTitle); } else { // 最后兜底用<title> const pageTitle = document.title; savedThreadTitle = pageTitle.includes(' - ') ? pageTitle.split(' - ')[0].trim() : pageTitle.replace(/\s*-\s*百合会.*$/, '').trim(); console.log('💾 从页面title保存:', savedThreadTitle); } } // 保存原始系列标题,用于后续目录搜索 originalSeriesTitle = savedThreadTitle; console.log('🎯 保存原始系列标题:', originalSeriesTitle); // 检查是否已有预加载的目录数据 if (seriesDirectory.length > 0) { console.log('✅ 使用预加载的目录数据:', seriesDirectory.length, '个章节'); } else { // 检查持久化缓存 const seriesKey = generateSeriesKey(savedThreadTitle); const cachedDirectory = getCachedDirectory(seriesKey); if (cachedDirectory && cachedDirectory.length > 0) { console.log('📦 从持久化缓存加载目录:', cachedDirectory.length, '个章节'); seriesDirectory = cachedDirectory; } else { // 缓存也没有,异步搜索 console.log('🔍 开始异步搜索目录...'); searchSeriesDirectory(savedThreadTitle).then(directory => { seriesDirectory = directory; console.log('📋 异步获取到目录:', seriesDirectory.length, '个章节'); }).catch(error => { console.error('❌ 异步获取目录失败:', error); }); } } // 提取漫画图片 images = collectImagesFromDocument(document) .map(u => { if (!u) return null; let url = String(u); if (url.startsWith('//')) url = 'https:' + url; if (!/^https?:\/\//i.test(url)) url = 'https://bbs.yamibo.com/' + url.replace(/^\/+/, ''); if (url.startsWith('http://')) url = url.replace(/^http:\/\//, 'https://'); url = url.replace(/^https?:\/\/https?:\/\//, 'https://'); return url; }) .filter(Boolean); console.log('📸 提取到', images.length, '张图片,URL已处理为HTTPS'); document.body.innerHTML = ` <button id="cw-exit" data-tooltip="退出阅读模式">⏻</button> <div id="cw-toolbar"> <button id="cw-directory" data-tooltip="查看目录 (M)">☰</button> <button id="cw-full" data-tooltip="全屏模式 (F)">⛶</button> <button id="cw-bg" data-tooltip="切换背景色 (B)">◐</button> <div id="cw-zoom"> <button id="cw-zoom-in" data-tooltip="放大图片 (+)">﹢</button> <button id="cw-zoom-out" data-tooltip="缩小图片 (-)">﹣</button> </div> </div> <div id="cw-container"></div> <div id="cw-bottom-bar"> <span id="cw-page-info">1/${images.length}</span> <button id="cw-to-top" data-tooltip="返回顶部 (T)">⬆</button> </div> `; GM_addStyle(` html, body { margin: 0; padding: 0; background: #616161; overflow-y: auto; color: #fff; font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", "Source Han Sans SC", sans-serif !important; } /* 容器样式:负责整体布局 */ #cw-container { display: flex !important; flex-direction: column !important; align-items: center !important; justify-content: flex-start !important; padding: 70px 0 60px !important; gap: 20px !important; margin: 0 auto !important; } /* 图片基础样式 */ #cw-container img { display: block !important; width: var(--img-width, 35vw) !important; /* 使用CSS变量控制宽度 */ height: auto !important; border-radius: 0 !important; user-select: none !important; transition: all 0.3s ease !important; margin: 0 auto !important; } /* 小图保持原始大小 */ #cw-container img[data-original-size="small"] { width: auto !important; max-width: var(--img-width, 35vw) !important; } #cw-exit { position: fixed; top: 15px; left: 20px; z-index: 2147483647; background: rgba(0,0,0,0.6); color: #fff; border: none; padding: 6px 10px; border-radius: 5px; cursor: pointer; font-size: 14px; } #cw-toolbar { position: fixed; top: 15px; right: 20px; display: flex; flex-direction: column; gap: 10px; z-index: 2147483647; pointer-events: auto; } #cw-toolbar button { background: rgba(0,0,0,0.6); color: #fff; border: none; padding: 6px; width: 32px; height: 32px; border-radius: 5px; cursor: pointer; font-size: 14px; transition: background-color 0.2s; display: flex; align-items: center; justify-content: center; } #cw-toolbar button:hover { background: rgba(0,0,0,0.8); transform: scale(1.05); } #cw-toolbar button:active { transform: scale(0.95); } /* 放大缩小按钮的特殊样式 */ #cw-zoom { display: flex; flex-direction: column; gap: 5px; } #cw-zoom-in, #cw-zoom-out { color: white !important; font-size: 20px !important; font-weight: bold !important; } /* ESC按钮的特殊样式 */ #cw-full[data-fullscreen="true"] { font-size: 24px !important; line-height: 16px !important; padding: 6px 10px !important; } /* 底部工具栏 */ #cw-bottom-bar { position: fixed; bottom: 15px; right: 20px; display: flex; align-items: center; gap: 15px; z-index: 2147483647; pointer-events: auto; background: rgba(0,0,0,0.6); padding: 8px 12px; border-radius: 5px; font-size: 14px; } #cw-bottom-bar button { background: rgba(255,255,255,0.2); color: #fff; border: none; padding: 4px 8px; border-radius: 3px; cursor: pointer; font-size: 12px; } #cw-bottom-bar button:hover { background: rgba(255,255,255,0.3); } #cw-page-info { color: #fff; font-weight: bold; } /* 全屏状态下的基础样式 */ :fullscreen #cw-toolbar, :-webkit-full-screen #cw-toolbar, :fullscreen #cw-bottom-bar, :-webkit-full-screen #cw-bottom-bar, :fullscreen #cw-exit, :-webkit-full-screen #cw-exit { display: flex !important; position: fixed !important; z-index: 2147483647 !important; transition: all 0.3s ease !important; opacity: 0 !important; pointer-events: none !important; } /* 鼠标移动时显示工具栏 */ :fullscreen.tools-visible #cw-toolbar, :-webkit-full-screen.tools-visible #cw-toolbar, :fullscreen.tools-visible #cw-bottom-bar, :-webkit-full-screen.tools-visible #cw-bottom-bar, :fullscreen.tools-visible #cw-exit, :-webkit-full-screen.tools-visible #cw-exit { opacity: 1 !important; pointer-events: auto !important; } :fullscreen #cw-toolbar, :-webkit-full-screen #cw-toolbar { top: 15px !important; right: 20px !important; } :fullscreen #cw-bottom-bar, :-webkit-full-screen #cw-bottom-bar { bottom: 15px !important; right: 20px !important; } /* 确保全屏状态下按钮样式保持一致 */ :fullscreen #cw-toolbar button:not(#cw-zoom-in):not(#cw-zoom-out), :-webkit-full-screen #cw-toolbar button:not(#cw-zoom-in):not(#cw-zoom-out) { padding: 6px !important; width: 32px !important; height: 32px !important; font-size: 14px !important; line-height: 16px !important; display: flex !important; align-items: center !important; justify-content: center !important; } /* 全屏状态下的缩放按钮样式 */ :fullscreen #cw-zoom-in, :fullscreen #cw-zoom-out, :-webkit-full-screen #cw-zoom-in, :-webkit-full-screen #cw-zoom-out { padding: 6px !important; width: 32px !important; height: 32px !important; font-size: 18px !important; line-height: 16px !important; color: #ffffff !important; } /* ESC 符号单独设置大小 */ :fullscreen #cw-full[data-fullscreen="true"], :-webkit-full-screen #cw-full[data-fullscreen="true"] { font-size: 24px !important; } /* 自定义tooltip样式,禁用原生tooltip */ [data-tooltip] { position: relative; } [data-tooltip]:hover::after { content: attr(data-tooltip); position: absolute; left: 50%; transform: translateX(-50%); background: rgba(0, 0, 0, 0.9); color: white; padding: 8px 12px; border-radius: 6px; font-size: 12px; white-space: nowrap; z-index: 2147483648; pointer-events: none; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); opacity: 0; animation: tooltipFadeIn 0.2s ease forwards; } [data-tooltip]:hover::before { content: ''; position: absolute; left: 50%; transform: translateX(-50%); border: 6px solid transparent; border-top-color: rgba(0, 0, 0, 0.9); z-index: 2147483648; pointer-events: none; opacity: 0; animation: tooltipFadeIn 0.2s ease forwards; } /* 顶部工具栏按钮 - tooltip显示在下方 */ #cw-toolbar [data-tooltip]:hover::after, #cw-exit[data-tooltip]:hover::after { top: 100%; margin-top: 8px; } #cw-toolbar [data-tooltip]:hover::before, #cw-exit[data-tooltip]:hover::before { top: 100%; border-bottom-color: rgba(0, 0, 0, 0.9); margin-top: 2px; } /* 底部按钮 - tooltip显示在上方 */ #cw-bottom-bar [data-tooltip]:hover::after { bottom: 100%; margin-bottom: 8px; } #cw-bottom-bar [data-tooltip]:hover::before { bottom: 100%; border-top-color: rgba(0, 0, 0, 0.9); margin-bottom: 2px; } /* 右侧按钮tooltip防止超出屏幕 */ #cw-toolbar [data-tooltip]:hover::after { right: 0; left: auto; transform: translateX(-20px); } #cw-toolbar [data-tooltip]:hover::before { right: 20px; left: auto; transform: none; } /* 左侧退出按钮tooltip */ #cw-exit[data-tooltip]:hover::after { left: 0; transform: translateX(20px); } #cw-exit[data-tooltip]:hover::before { left: 20px; transform: none; } /* 确保tooltip在全屏状态下也能正常显示 */ :fullscreen [data-tooltip]:hover::after, :-webkit-full-screen [data-tooltip]:hover::after, :fullscreen [data-tooltip]:hover::before, :-webkit-full-screen [data-tooltip]:hover::before { z-index: 2147483649 !important; } `); // ====== 初始化阅读器UI(渲染图片并绑定按钮) ====== function initReaderUI() { const container = document.getElementById('cw-container'); if (!container) { console.warn('⚠️ cw-container 未找到,无法渲染图片'); return; } // 设置初始缩放(使用 CSS 变量控制宽度) const applyZoom = () => { const vw = Math.max(10, Math.min(90, zoomLevel)); document.documentElement.style.setProperty('--img-width', `${vw}vw`); }; applyZoom(); // 渲染图片列表 container.innerHTML = ''; images.forEach((url, idx) => { const img = document.createElement('img'); img.src = url; img.loading = 'lazy'; img.alt = `page-${idx + 1}`; img.dataset.index = String(idx + 1); container.appendChild(img); }); // 更新页码显示(靠近视口中心的图片) const pageInfo = document.getElementById('cw-page-info'); function updateCurrentPageInfo() { const imgs = container.querySelectorAll('img'); if (!imgs.length) return; const viewportMid = window.scrollY + window.innerHeight / 2; let current = 1; let minDist = Infinity; imgs.forEach((img, i) => { const rect = img.getBoundingClientRect(); const imgMid = window.scrollY + rect.top + rect.height / 2; const dist = Math.abs(imgMid - viewportMid); if (dist < minDist) { minDist = dist; current = i + 1; } }); if (pageInfo) pageInfo.textContent = `${current}/${imgs.length}`; } // 退出、目录、全屏、背景、缩放、回顶部 readerEventHandlers.onExitClick = () => exitReader(); readerEventHandlers.onDirectoryClick = () => showDirectoryModal(); readerEventHandlers.onFullClick = async () => { try { if (!document.fullscreenElement) await document.documentElement.requestFullscreen(); else await document.exitFullscreen(); } catch (e) { console.warn('全屏切换失败:', e); } }; readerEventHandlers.onBgClick = () => { bgIsBlack = !bgIsBlack; document.body.style.background = bgIsBlack ? '#616161' : '#f5f5f5'; document.body.style.color = bgIsBlack ? '#fff' : '#000'; }; readerEventHandlers.onZoomIn = () => { zoomLevel = Math.min(90, zoomLevel + 5); applyZoom(); }; readerEventHandlers.onZoomOut = () => { zoomLevel = Math.max(10, zoomLevel - 5); applyZoom(); }; readerEventHandlers.onToTop = () => window.scrollTo({ top: 0, behavior: 'smooth' }); const throttle = (fn, wait = 100) => { let t = null; return (...args) => { if (t) return; t = setTimeout(() => { fn(...args); t = null; }, wait); }; }; readerEventHandlers.updateCurrentPageInfo = throttle(updateCurrentPageInfo, 120); // 工具栏显示控制 readerEventHandlers.showTools = function() { try { document.documentElement.classList.add('tools-visible'); if (readerToolsTimer) clearTimeout(readerToolsTimer); readerToolsTimer = setTimeout(() => { document.documentElement.classList.remove('tools-visible'); readerToolsTimer = null; }, 3000); } catch (e) { // ignore } }; readerEventHandlers.onMousemove = readerEventHandlers.showTools; readerEventHandlers.onKeydown = readerEventHandlers.showTools; readerEventHandlers.onFullscreen = function() { if (document.fullscreenElement) { readerEventHandlers.showTools(); } else { document.documentElement.classList.remove('tools-visible'); if (readerToolsTimer) { clearTimeout(readerToolsTimer); readerToolsTimer = null; } } }; // 绑定 DOM 事件 const exitBtn = document.getElementById('cw-exit'); if (exitBtn) exitBtn.addEventListener('click', readerEventHandlers.onExitClick); const dirBtn = document.getElementById('cw-directory'); if (dirBtn) dirBtn.addEventListener('click', readerEventHandlers.onDirectoryClick); const fullBtn = document.getElementById('cw-full'); if (fullBtn) fullBtn.addEventListener('click', readerEventHandlers.onFullClick); const bgBtn = document.getElementById('cw-bg'); if (bgBtn) bgBtn.addEventListener('click', readerEventHandlers.onBgClick); const zoomIn = document.getElementById('cw-zoom-in'); const zoomOut = document.getElementById('cw-zoom-out'); if (zoomIn) zoomIn.addEventListener('click', readerEventHandlers.onZoomIn); if (zoomOut) zoomOut.addEventListener('click', readerEventHandlers.onZoomOut); const toTop = document.getElementById('cw-to-top'); if (toTop) toTop.addEventListener('click', readerEventHandlers.onToTop); // 全局事件绑定 if (!readerEventsBound) { window.addEventListener('scroll', readerEventHandlers.updateCurrentPageInfo, { passive: true }); window.addEventListener('mousemove', readerEventHandlers.onMousemove, { passive: true }); window.addEventListener('keydown', readerEventHandlers.onKeydown, { passive: true }); document.addEventListener('fullscreenchange', readerEventHandlers.onFullscreen); readerEventsBound = true; } // 立即更新一次页码并短暂显示工具栏 setTimeout(updateCurrentPageInfo, 200); readerEventHandlers.showTools(); // 提供解绑函数,供 exitReader 调用 unbindReaderEvents = function() { try { if (!readerEventsBound) return; // 移除 DOM 绑定 try { if (exitBtn) exitBtn.removeEventListener('click', readerEventHandlers.onExitClick); } catch (e) {} try { if (dirBtn) dirBtn.removeEventListener('click', readerEventHandlers.onDirectoryClick); } catch (e) {} try { if (fullBtn) fullBtn.removeEventListener('click', readerEventHandlers.onFullClick); } catch (e) {} try { if (bgBtn) bgBtn.removeEventListener('click', readerEventHandlers.onBgClick); } catch (e) {} try { if (zoomIn) zoomIn.removeEventListener('click', readerEventHandlers.onZoomIn); } catch (e) {} try { if (zoomOut) zoomOut.removeEventListener('click', readerEventHandlers.onZoomOut); } catch (e) {} try { if (toTop) toTop.removeEventListener('click', readerEventHandlers.onToTop); } catch (e) {} // 移除全局事件 try { window.removeEventListener('scroll', readerEventHandlers.updateCurrentPageInfo, { passive: true }); } catch (e) {} try { window.removeEventListener('mousemove', readerEventHandlers.onMousemove, { passive: true }); } catch (e) {} try { window.removeEventListener('keydown', readerEventHandlers.onKeydown, { passive: true }); } catch (e) {} try { document.removeEventListener('fullscreenchange', readerEventHandlers.onFullscreen); } catch (e) {} } catch (e) { // ignore } // 清理定时器与状态 if (readerToolsTimer) { clearTimeout(readerToolsTimer); readerToolsTimer = null; } readerEventHandlers = {}; readerEventsBound = false; unbindReaderEvents = null; }; } // 立即初始化阅读器 UI initReaderUI(); } /*** 退出阅读模式 ***/ function exitReader() { isReading = false; setAutoReaderStatus(false); // 关闭全局阅读器模式 // 解绑阅读器事件,避免残留监听 try { if (typeof unbindReaderEvents === 'function') unbindReaderEvents(); } catch (e) { // ignore } // 先退出全屏 if (document.fullscreenElement) { document.exitFullscreen().catch(err => { console.warn('退出全屏失败:', err); }); } // 如果有保存的原始页面内容,直接恢复 if (originalPageHTML) { console.log('🔄 恢复原始页面内容...'); // 移除阅读模式添加的样式 const readerStyles = document.querySelectorAll('style'); readerStyles.forEach(style => { // 移除包含阅读模式特定选择器的样式 if (style.textContent.includes('#cw-container') || style.textContent.includes('#cw-toolbar') || style.textContent.includes('#chapter-loader')) { style.remove(); console.log('🧹 移除阅读模式样式'); } }); document.body.innerHTML = originalPageHTML; document.title = originalPageTitle; // 重新绑定按钮事件 const restoredButton = document.getElementById('reader-toggle'); if (restoredButton) { restoredButton.addEventListener('click', () => { if (!isReading) { enterReader(); setAutoReaderStatus(true); } else { exitReader(); } }); } console.log('✅ 页面内容已恢复,目录缓存保留'); } else { // 如果没有保存的内容,刷新页面 console.log('⚠️ 没有保存的页面内容,刷新页面'); location.reload(); } } // 无缝章节加载:加载提示与错误显示 function showLoadingIndicator(message = '正在加载章节...') { try { // 移除已存在的加载提示 const existing = document.getElementById('chapter-loader'); if (existing) existing.remove(); const loader = document.createElement('div'); loader.id = 'chapter-loader'; loader.innerHTML = ` <div class="loader-content"> <div class="loader-spinner"></div> <div class="loader-text">${message}</div> </div> `; const style = document.createElement('style'); style.id = 'chapter-loader-style'; style.textContent = ` #chapter-loader { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.6); display:flex; align-items:center; justify-content:center; z-index:2147483648; } #chapter-loader .loader-content { color:#fff; padding:18px 24px; border-radius:10px; background: rgba(0,0,0,0.45); text-align:center; } #chapter-loader .loader-spinner { width:30px; height:30px; border:3px solid rgba(255,255,255,0.25); border-top:3px solid #fff; border-radius:50%; animation: yamibo-spin 1s linear infinite; margin:0 auto 8px } @keyframes yamibo-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } #chapter-loader .loader-text { font-size:14px; } `; document.head.appendChild(style); document.body.appendChild(loader); } catch (e) { console.warn('showLoadingIndicator error', e); } } function hideLoadingIndicator() { try { const loader = document.getElementById('chapter-loader'); if (loader) { loader.remove(); } const style = document.getElementById('chapter-loader-style'); if (style) style.remove(); } catch (e) { // ignore } } function showErrorMessage(message) { try { const id = 'chapter-error-msg'; const existing = document.getElementById(id); if (existing) existing.remove(); const el = document.createElement('div'); el.id = id; el.style.cssText = 'position:fixed;left:50%;top:40%;transform:translate(-50%,-50%);background:#ff6b6b;color:#fff;padding:12px 18px;border-radius:8px;z-index:2147483649;font-size:14px;box-shadow:0 8px 30px rgba(0,0,0,0.4)'; el.textContent = message; document.body.appendChild(el); setTimeout(() => { try { el.style.opacity = '0'; setTimeout(() => el.remove(), 250); } catch (e) {} }, 2500); } catch (e) { console.warn('showErrorMessage error', e); } } // 无缝章节切换 async function loadNewChapter(chapterUrl, chapterTitle) { try { console.log('📖 开始无缝加载章节:', chapterTitle); showLoadingIndicator('正在加载章节...'); const response = await fetch(chapterUrl, { method: 'GET', credentials: 'include', headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3', 'Referer': window.location.href, 'User-Agent': navigator.userAgent } }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const html = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); // 提取图片 URL const newImages = collectImagesFromDocument(doc) .map(u => { if (!u) return null; let url = String(u); if (url.startsWith('//')) url = 'https:' + url; if (!/^https?:\/\//i.test(url)) url = 'https://bbs.yamibo.com/' + url.replace(/^\/+/, ''); if (url.startsWith('http://')) url = url.replace(/^http:\/\//, 'https://'); url = url.replace(/^https?:\/\/https?:\/\//, 'https://'); return url; }) .filter(Boolean); if (!newImages || newImages.length === 0) { hideLoadingIndicator(); showErrorMessage('未找到漫画图片,已跳转到原帖'); location.href = chapterUrl; return; } // 更新全局图片数组 images = newImages; // 尝试提取并保存新章节标题 const newH1 = doc.querySelector('h1.ts'); if (newH1) { const subjectSpan = newH1.querySelector('#thread_subject, span[id^="thread_subject"]'); savedThreadTitle = subjectSpan ? subjectSpan.textContent.trim() : newH1.textContent.trim(); } else { const newTitleEl = doc.querySelector('#thread_subject, span[id^="thread_subject"]'); if (newTitleEl) savedThreadTitle = newTitleEl.textContent.trim(); } // 更新浏览器地址栏(不刷新页面) try { window.history.pushState({}, '', chapterUrl); } catch (e) { /* ignore */ } // 只更新图片容器与页码 const container = document.getElementById('cw-container'); if (container) { container.innerHTML = ''; images.forEach((url, idx) => { const img = document.createElement('img'); img.src = url; img.loading = 'lazy'; img.alt = `page-${idx + 1}`; img.dataset.index = String(idx + 1); img.onload = function() { try { const naturalWidth = this.naturalWidth || 0; const viewportWidth = window.innerWidth || 1024; const oneThirdViewport = viewportWidth / 3; if (naturalWidth > 0 && naturalWidth < oneThirdViewport) this.setAttribute('data-original-size', 'small'); else this.removeAttribute('data-original-size'); } catch (e) { // ignore } }; container.appendChild(img); }); const pageInfo = document.getElementById('cw-page-info'); if (pageInfo) pageInfo.textContent = `1/${images.length}`; } // 滚动到顶部 window.scrollTo({ top: 0, behavior: 'smooth' }); hideLoadingIndicator(); console.log('🎉 章节加载完成:', chapterTitle); } catch (error) { console.error('❌ 加载章节失败:', error); hideLoadingIndicator(); showErrorMessage('加载章节失败: ' + (error && error.message ? error.message : String(error))); // 若失败,1s 后回退到原帖(避免卡死在阅读器) setTimeout(() => { try { location.href = chapterUrl; } catch (e) {} }, 1000); } } })();