// ==UserScript== // @name 番茄小说阅读助手-关关同学修复版 // @namespace http://tampermonkey.net/ // @version 3.3 // @description 修复书籍ID检测,正文直替+全文下载,基于oiapi接口 // @author Modified // @license MIT License // @match https://fanqienovel.com/* // @require https://cdn.jsdelivr.net/npm/file-saver@2.0.5/dist/FileSaver.min.js // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @connect fanqienovel.com // @connect oiapi.net // @run-at document-end // ==/UserScript== (function() { 'use strict'; // ========== 接口配置 ========== const API_URL = 'https://oiapi.net/api/FqRead'; const API_KEY = ''; const Utils = { delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }, // 修复:新增reader路径匹配,支持从阅读页URL直接提取书籍ID extractBookId(input) { if (!input) return null; const patterns = [ /book_id=(\d+)/, /\/page\/(\d+)/, /\/reader\/(\d+)/, /^(\d+)$/ ]; for (const pattern of patterns) { const match = input.match(pattern); if (match) return match[1]; } return null; }, safeJSONParse(text, defaultValue = null) { try { return JSON.parse(text); } catch (e) { return defaultValue; } } }; // ========== 正文自动替换模块 ========== class ContentReplacer { constructor() { this.currentURL = window.location.href; this.chapterCache = null; this.cacheBookId = null; this.isReplacing = false; this.init(); } init() { const pageType = this.detectPageType(); if (pageType === 'reader') { this.setupReaderPage(); } } detectPageType() { const url = window.location.href; if (url.includes('/reader/')) return 'reader'; if (url.includes('/page/')) return 'page'; return null; } setupReaderPage() { setTimeout(() => this.replaceReaderContent(), 1500); // 监听章节切换 setInterval(() => { if (window.location.href !== this.currentURL) { this.currentURL = window.location.href; setTimeout(() => this.replaceReaderContent(), 1000); } }, 1000); } // 获取章节列表(带缓存) fetchChapterList(bookId) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: `${API_URL}?key=${API_KEY}&method=chapters&id=${bookId}`, timeout: 30000, onload: (res) => { try { const data = JSON.parse(res.responseText); if (data && data.data && Array.isArray(data.data)) { resolve(data.data); } else { reject(new Error('章节列表格式异常')); } } catch (e) { reject(e); } }, onerror: reject }); }); } // 增强:多层级提取书籍ID getCurrentBookId() { // 1. 优先从URL直接提取 const urlId = Utils.extractBookId(window.location.href); if (urlId) return urlId; // 2. 从页面返回链接提取 const bookLink = document.querySelector('a[href*="/page/"]'); if (bookLink) { const linkId = Utils.extractBookId(bookLink.getAttribute('href')); if (linkId) return linkId; } // 3. 兜底:从页面全局数据提取 if (window.__INITIAL_STATE__) { try { const state = window.__INITIAL_STATE__; const stateId = state?.reader?.bookId || state?.page?.bookId || state?.book?.id; if (stateId) return String(stateId); } catch(e) {} } return null; } async replaceReaderContent() { if (this.isReplacing) return; const contentDiv = document.querySelector('.muye-reader-content.noselect, .muye-reader-content'); if (!contentDiv) return; const bookId = this.getCurrentBookId(); if (!bookId) return; // 获取当前章节标题 const titleEl = document.querySelector('.muye-reader-title'); if (!titleEl) return; const currentTitle = titleEl.innerText.trim().replace(/\s+/g, ''); this.isReplacing = true; try { // 缓存命中判断 if (!this.chapterCache || this.cacheBookId !== bookId) { this.chapterCache = await this.fetchChapterList(bookId); this.cacheBookId = bookId; } // 匹配当前章节序号 const currentChapter = this.chapterCache.find(ch => { const chTitle = ch.chapter_title.replace(/\s+/g, ''); return chTitle === currentTitle || currentTitle.includes(chTitle) || chTitle.includes(currentTitle); }); if (!currentChapter) { this.isReplacing = false; return; } // 请求解析正文 GM_xmlhttpRequest({ method: 'GET', url: `${API_URL}?key=${API_KEY}&method=chapter&id=${bookId}&chapter=${currentChapter.chapter}`, timeout: 30000, onload: (response) => { try { const data = Utils.safeJSONParse(response.responseText); if (data?.data && Array.isArray(data.data) && data.data[0]?.content) { let content = data.data[0].content; // 按换行拆分为段落 const paragraphs = content.split(/\n+/) .map(line => line.trim() ? `

${line.trim()}

` : '') .join('\n'); // 保留容器本身,只替换内部内容 if (contentDiv.classList.length > 1) { contentDiv.classList = contentDiv.classList[0]; } contentDiv.innerHTML = paragraphs; contentDiv.classList.remove('noselect'); // ========== 移除所有遮挡 ========== const vipBanner = document.querySelector('.muye-to-vip'); if (vipBanner) vipBanner.remove(); const toFanqie = document.querySelector('.muye-to-fanqie'); if (toFanqie) toFanqie.remove(); // 解除模糊/灰度滤镜 const readerBox = document.querySelector('.muye-reader-box'); if (readerBox) { readerBox.style.filter = ''; readerBox.style.backdropFilter = ''; readerBox.classList.remove('serial-filter-gray'); } contentDiv.style.filter = ''; contentDiv.style.userSelect = 'text'; // 清除遮罩类元素 const masks = contentDiv.querySelectorAll('[class*="mask"], [class*="blur"], [class*="lock"]'); masks.forEach(el => el.remove()); document.title = document.title.replace(/在线免费阅读_番茄小说官网$/, ''); console.log('[FanqieEnhancer] 正文替换成功'); } } catch (e) { console.error('[FanqieEnhancer] 正文替换失败:', e); } finally { this.isReplacing = false; } }, onerror: () => { this.isReplacing = false; console.error('[FanqieEnhancer] 正文请求失败'); } }); } catch (err) { this.isReplacing = false; console.error('[FanqieEnhancer] 章节匹配失败:', err); } } } // ========== 全文下载模块 ========== class FanqieDownloader { constructor() { this.config = { maxLogEntries: 100, version: '3.3' }; this.state = { currentBookId: null, currentBookName: null, isDownloading: false, abortController: null }; this.cache = { elements: {} }; this.init(); } init() { this.addStyles(); this.createPanel(); this.cacheElements(); this.bindEvents(); this.loadSettings(); setTimeout(() => this.detectCurrentPageBook(), 1500); window.fanqieDownloader = this; console.log('[FanqieEnhancer] 初始化完成 v' + this.config.version); } cacheElements() { const ids = [ 'fanqieBody', 'fanqieToggle', 'fanqieBookId', 'fanqieStart', 'fanqieEnd', 'fanqieConcurrency', 'fanqieProgress', 'fanqieProgressFill', 'fanqieProgressPercent', 'fanqieProgressStatus', 'fanqieSuccessCount', 'fanqieFailCount', 'fanqieSpeed', 'fanqieLog', 'fanqieCurrentBook', 'fanqieCurrentBookInfo', 'fanqieDownloadCurrent', 'fanqieEncoding' ]; ids.forEach(id => { this.cache.elements[id] = document.getElementById(id); }); } addStyles() { const styles = ` .muye-reader-content.noselect::after, .muye-reader-content.noselect::before { display: none !important; } .muye-to-vip, .muye-to-vip-mask, .muye-to-fanqie, .reader-unlock, .reader-lock-overlay, [class*="vip"], [class*="lock"], [class*="mask"], [class*="blur"] { display: none !important; } .fanqie-downloader-panel { position: fixed; top: 160px; right: 20px; width: 300px; background: white; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.15); z-index: 9999; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; overflow: hidden; font-size: 12px; transition: all 0.3s ease; } .fanqie-downloader-panel.fanqie-collapsed { width: auto; min-width: 0; right: 0; border-radius: 8px 0 0 8px; } .fanqie-downloader-panel.fanqie-collapsed .fanqie-downloader-header { padding: 6px 10px; font-size: 11px; border-radius: 8px 0 0 8px; } .fanqie-downloader-panel.fanqie-collapsed .fanqie-downloader-header span { display: none; } .fanqie-downloader-header { background: linear-gradient(135deg, #2196F3, #42A5F5); color: white; padding: 10px 12px; font-size: 13px; font-weight: 600; display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none; } .fanqie-downloader-header button { background: rgba(255,255,255,0.2); border: none; color: white; width: 22px; height: 22px; border-radius: 50%; cursor: pointer; font-size: 12px; transition: all 0.3s; } .fanqie-downloader-body { padding: 10px 12px; max-height: 400px; overflow-y: auto; } .fanqie-downloader-body.hidden { display: none; } .fanqie-input-group { margin-bottom: 8px; } .fanqie-input-group label { display: block; font-size: 12px; color: #666; margin-bottom: 3px; } .fanqie-input-group input, .fanqie-input-group select { width: 100%; padding: 6px 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 12px; transition: border-color 0.3s; box-sizing: border-box; } .fanqie-input-row { display: flex; gap: 8px; } .fanqie-input-row .fanqie-input-group { flex: 1; } .fanqie-btn { width: 100%; padding: 8px; border: none; border-radius: 4px; font-size: 12px; cursor: pointer; margin-bottom: 6px; transition: all 0.3s; } .fanqie-btn-primary { background: #2196F3; color: white; } .fanqie-btn-primary:hover:not(:disabled) { background: #1976D2; } .fanqie-btn-primary:disabled { background: #ccc; cursor: not-allowed; } .fanqie-progress { margin-top: 10px; padding: 8px; background: #f8f8f8; border-radius: 4px; } .fanqie-progress-bar { height: 4px; background: #e0e0e0; border-radius: 2px; overflow: hidden; margin-bottom: 6px; } .fanqie-progress-fill { height: 100%; background: linear-gradient(90deg, #2196F3, #42A5F5); transition: width 0.3s; width: 0%; } .fanqie-progress-text { font-size: 11px; color: #666; display: flex; justify-content: space-between; } .fanqie-log { margin-top: 8px; max-height: 150px; overflow-y: auto; background: #1e1e1e; color: #0f0; padding: 10px; border-radius: 6px; font-size: 12px; font-family: monospace; } .fanqie-log-entry { margin-bottom: 3px; animation: fadeIn 0.3s ease; } @keyframes fadeIn { from { opacity: 0; transform: translateY(-5px); } to { opacity: 1; transform: translateY(0); } } .fanqie-log-error { color: #ff6b6b; } .fanqie-log-success { color: #69f0ae; } .fanqie-log-info { color: #64b5f6; } .fanqie-log-warning { color: #ffd93d; } .fanqie-current-book { background: #e3f2fd; border: 1px solid #2196F3; border-radius: 6px; padding: 10px; margin-bottom: 12px; } .fanqie-current-book-title { font-weight: 600; color: #1976D2; margin-bottom: 5px; } .fanqie-current-book-id { font-size: 12px; color: #666; } .fanqie-stats { display: flex; justify-content: space-between; font-size: 12px; color: #666; margin-top: 5px; } .fanqie-section-title { font-size: 13px; font-weight: 600; color: #333; margin: 15px 0 10px 0; padding-bottom: 5px; border-bottom: 1px solid #eee; cursor: pointer; user-select: none; } `; const styleEl = document.createElement('style'); styleEl.textContent = styles; document.head.appendChild(styleEl); } createPanel() { const panel = document.createElement('div'); panel.className = 'fanqie-downloader-panel'; panel.innerHTML = `
📚 番茄关关改良 v${this.config.version}
⚙️ 下载设置
`; document.body.appendChild(panel); } bindEvents() { this.cache.elements.fanqieToggle?.addEventListener('click', () => this.togglePanel()); this.cache.elements.fanqieDownloadCurrent?.addEventListener('click', () => this.downloadCurrentBook()); this.initDraggable(); // 配置折叠 const configToggle = document.getElementById('fanqieConfigToggle'); const configContent = document.getElementById('fanqieConfigContent'); const toggleIcon = document.getElementById('fanqieConfigToggleIcon'); if (configToggle && configContent) { configToggle.addEventListener('click', () => { const isHidden = configContent.style.display === 'none'; configContent.style.display = isHidden ? 'block' : 'none'; toggleIcon.textContent = isHidden ? '▼' : '▲'; }); } // 并发数限制 if (this.cache.elements.fanqieConcurrency) { this.cache.elements.fanqieConcurrency.addEventListener('input', () => { let val = parseInt(this.cache.elements.fanqieConcurrency.value); if (val > 30) this.cache.elements.fanqieConcurrency.value = 30; if (val < 1) this.cache.elements.fanqieConcurrency.value = 1; }); } // 保存设置 ['fanqieConcurrency', 'fanqieEncoding'].forEach(id => { this.cache.elements[id]?.addEventListener('change', () => this.saveSettings()); }); } initDraggable() { const panel = document.querySelector('.fanqie-downloader-panel'); const header = document.querySelector('.fanqie-downloader-header'); if (!panel || !header) return; let isDragging = false; let startX, startY, startLeft, startTop; header.addEventListener('mousedown', (e) => { if (e.target.tagName === 'BUTTON') return; isDragging = true; startX = e.clientX; startY = e.clientY; const rect = panel.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; panel.style.transition = 'none'; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; panel.style.left = startLeft + (e.clientX - startX) + 'px'; panel.style.top = startTop + (e.clientY - startY) + 'px'; panel.style.right = 'auto'; }); document.addEventListener('mouseup', () => { isDragging = false; panel.style.transition = ''; }); } saveSettings() { const settings = { concurrency: this.cache.elements.fanqieConcurrency?.value || '5', encoding: this.cache.elements.fanqieEncoding?.value || 'utf-8' }; GM_setValue('fanqieSettings', JSON.stringify(settings)); } loadSettings() { try { const saved = GM_getValue('fanqieSettings', '{}'); const settings = JSON.parse(saved); if (settings.concurrency) this.cache.elements.fanqieConcurrency.value = settings.concurrency; if (settings.encoding) this.cache.elements.fanqieEncoding.value = settings.encoding; } catch (e) { console.log('[FanqieEnhancer] 加载设置失败:', e); } } togglePanel() { const body = this.cache.elements.fanqieBody; const panel = document.querySelector('.fanqie-downloader-panel'); const btn = this.cache.elements.fanqieToggle; if (body.classList.contains('hidden')) { body.classList.remove('hidden'); panel.classList.remove('fanqie-collapsed'); btn.textContent = '−'; } else { body.classList.add('hidden'); panel.classList.add('fanqie-collapsed'); btn.textContent = '+'; } } log(message, type = 'info') { const logDiv = this.cache.elements.fanqieLog; if (!logDiv) return; const entry = document.createElement('div'); entry.className = `fanqie-log-entry fanqie-log-${type}`; entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`; logDiv.appendChild(entry); while (logDiv.children.length > this.config.maxLogEntries) { logDiv.removeChild(logDiv.firstChild); } logDiv.scrollTop = logDiv.scrollHeight; } updateProgress(percent, status, successCount, failCount, speed) { const els = this.cache.elements; if (els.fanqieProgressFill) els.fanqieProgressFill.style.width = percent + '%'; if (els.fanqieProgressPercent) els.fanqieProgressPercent.textContent = percent + '%'; if (status && els.fanqieProgressStatus) els.fanqieProgressStatus.textContent = status; if (successCount !== undefined && els.fanqieSuccessCount) els.fanqieSuccessCount.textContent = `成功: ${successCount}`; if (failCount !== undefined && els.fanqieFailCount) els.fanqieFailCount.textContent = `失败: ${failCount}`; if (speed !== undefined && els.fanqieSpeed) els.fanqieSpeed.textContent = `速度: ${speed.toFixed(1)}章/秒`; } showProgress() { if (this.cache.elements.fanqieProgress) { this.cache.elements.fanqieProgress.style.display = 'block'; } } // 修复:增强书籍ID检测逻辑,三层兜底 detectCurrentPageBook() { try { const url = window.location.href; let bookId = null; let bookName = null; // 1. 优先从URL直接提取(阅读页/主页通用) bookId = Utils.extractBookId(url); // 2. 从DOM元素提取 if (!bookId) { const bookLink = document.querySelector('a[href*="/page/"]'); if (bookLink) { bookId = Utils.extractBookId(bookLink.getAttribute('href')); } } // 3. 兜底从页面全局数据提取 if (!bookId && window.__INITIAL_STATE__) { try { const state = window.__INITIAL_STATE__; bookId = state?.reader?.bookId || state?.page?.bookId || state?.book?.id; if (bookId) bookId = String(bookId); } catch(e) {} } // 提取书名 const titleEl = document.querySelector('.muye-reader-title, h1, .book-title'); if (titleEl) bookName = titleEl.textContent.trim(); if (bookId) { this.state.currentBookId = bookId; this.state.currentBookName = bookName; if (this.cache.elements.fanqieCurrentBook) { this.cache.elements.fanqieCurrentBook.style.display = 'block'; } if (this.cache.elements.fanqieCurrentBookInfo) { this.cache.elements.fanqieCurrentBookInfo.textContent = `${bookName || '未知书名'} (ID: ${bookId})`; } if (this.cache.elements.fanqieBookId) { this.cache.elements.fanqieBookId.value = bookId; } this.log(`✅ 检测到小说: ${bookName || '未知书名'} (${bookId})`, 'success'); } else { this.log('⚠️ 未能检测到书籍ID,请手动输入', 'warning'); this.log('💡 手动方法:打开小说主页,复制地址栏/page/后面的数字', 'info'); } } catch (error) { this.log(`检测页面小说失败: ${error.message}`, 'error'); } } downloadCurrentBook() { if (this.state.currentBookId) { this.cache.elements.fanqieBookId.value = this.state.currentBookId; this.fetchChapterList(); } } setDownloadingState(isDownloading) { this.state.isDownloading = isDownloading; const downloadBtn = this.cache.elements.fanqieDownloadCurrent; if (downloadBtn) { downloadBtn.disabled = isDownloading; downloadBtn.textContent = isDownloading ? '⏳ 下载中...' : '📥 下载全本TXT'; } } async fetchChapterList() { if (this.state.isDownloading) { this.log('⚠️ 下载正在进行中', 'warning'); return; } const input = this.cache.elements.fanqieBookId?.value.trim(); const bookId = Utils.extractBookId(input); if (!bookId) { alert('请输入有效的小说ID或 /page/ 链接'); return; } this.state.abortController = new AbortController(); this.setDownloadingState(true); this.showProgress(); this.log(`开始获取小说 ${bookId} 的章节列表...`); GM_xmlhttpRequest({ method: 'GET', url: `${API_URL}?key=${API_KEY}&method=chapters&id=${bookId}`, timeout: 30000, onload: (response) => { try { const data = JSON.parse(response.responseText); this.processChapterData(data, bookId); } catch (e) { this.log('❌ 解析响应失败: ' + e.message, 'error'); this.setDownloadingState(false); } }, onerror: () => { this.log('❌ 章节列表请求失败', 'error'); this.setDownloadingState(false); } }); } async processChapterData(data, bookId) { let chapterListWithVolume = null; let bookInfo = null; // 适配oiapi返回格式 if (data && data.data && Array.isArray(data.data) && data.data.length > 0) { chapterListWithVolume = [data.data]; // 单卷结构 bookInfo = { bookName: data.data[0]?.title || '未知书名' }; this.log(`成功获取章节列表,共 ${data.data.length} 章`, 'info'); } if (chapterListWithVolume && chapterListWithVolume.length > 0) { const volumes = []; let globalChapterIndex = 0; for (let vIndex = 0; vIndex < chapterListWithVolume.length; vIndex++) { const volume = chapterListWithVolume[vIndex]; const volumeChapters = []; if (!Array.isArray(volume)) continue; for (const ch of volume) { const chapterNum = ch.chapter || globalChapterIndex + 1; const title = ch.chapter_title || `第${chapterNum}章`; volumeChapters.push({ chapter: chapterNum, title: title, realChapterOrder: chapterNum, globalIndex: globalChapterIndex++ }); } const volumeTitle = volume[0]?.volume_name || volume[0]?.volumeTitle || `第${vIndex+1}卷`; if (volumeChapters.length > 0) { volumes.push({ volumeIndex: vIndex + 1, volumeTitle: volumeTitle, chapters: volumeChapters }); } } const totalChapters = volumes.reduce((sum, v) => sum + v.chapters.length, 0); let bookName = bookInfo?.bookName || '未知书名'; if (this.state.currentBookId === bookId && this.state.currentBookName) { bookName = this.state.currentBookName; } this.log(`✅ 《${bookName}》共 ${volumes.length} 卷 ${totalChapters} 章`, 'success'); this.cache.elements.fanqieEnd.value = totalChapters; await this.startDownload(bookId, bookName, volumes, totalChapters); } else { this.log('❌ 获取章节列表失败:接口返回数据为空', 'error'); this.setDownloadingState(false); } } async startDownload(bookId, bookName, volumes, totalChapters) { const startIndex = parseInt(this.cache.elements.fanqieStart?.value) - 1 || 0; let endIndex = parseInt(this.cache.elements.fanqieEnd?.value) - 1; if (isNaN(endIndex) || endIndex < 0) endIndex = totalChapters - 1; const concurrency = Math.min(parseInt(this.cache.elements.fanqieConcurrency?.value) || 5, 30); const delayTime = 200; const chaptersToDownload = []; for (const volume of volumes) { for (const ch of volume.chapters) { if (ch.globalIndex >= startIndex && ch.globalIndex <= endIndex) { chaptersToDownload.push({ ...ch, volumeIndex: volume.volumeIndex, volumeTitle: volume.volumeTitle }); } } } const total = chaptersToDownload.length; const results = []; let successCount = 0; let failCount = 0; const maxFail = 10; let consecutiveFails = 0; const downloadStartTime = Date.now(); this.log(`📥 开始下载《${bookName}》: 共${total}章, 并发数${concurrency}`); for (let i = 0; i < chaptersToDownload.length; i += concurrency) { if (this.state.abortController?.signal.aborted) { this.log('⏹️ 下载已取消', 'warning'); break; } const batch = chaptersToDownload.slice(i, i + concurrency); const batchPromises = batch.map(ch => this.downloadChapter(bookId, ch).then(result => ({ ...result, ch })) ); const batchResults = await Promise.all(batchPromises); for (const { success, content, ch } of batchResults) { if (success) { results.push({ volumeIndex: ch.volumeIndex, volumeTitle: ch.volumeTitle, chapterOrder: ch.realChapterOrder, title: ch.title, content: content, globalIndex: ch.globalIndex }); successCount++; consecutiveFails = 0; } else { failCount++; consecutiveFails++; this.log(`❌ [卷${ch.volumeIndex}] ${ch.title} 获取失败`, 'error'); } } const progress = Math.round(((i + batch.length) / total) * 100); const elapsed = (Date.now() - downloadStartTime) / 1000; const speed = elapsed > 0 ? (successCount + failCount) / elapsed : 0; this.updateProgress(progress, `下载中... (${Math.min(i + batch.length, total)}/${total})`, successCount, failCount, speed); if (consecutiveFails >= maxFail) { this.log(`⚠️ 连续失败${maxFail}次,停止下载`, 'error'); break; } if (delayTime > 0 && i + concurrency < chaptersToDownload.length) { await Utils.delay(delayTime); } } results.sort((a, b) => { if (a.volumeIndex !== b.volumeIndex) return a.volumeIndex - b.volumeIndex; return a.chapterOrder - b.chapterOrder; }); this.downloadTxt(results, bookName, bookId); this.setDownloadingState(false); } async downloadChapter(bookId, chapter) { return new Promise((resolve) => { const url = `${API_URL}?key=${API_KEY}&method=chapter&id=${bookId}&chapter=${chapter.chapter}`; GM_xmlhttpRequest({ method: 'GET', url: url, timeout: 30000, onload: (response) => { try { const data = Utils.safeJSONParse(response.responseText); const content = this.parseChapterContent(data); resolve({ success: !!content, content: content }); } catch (e) { resolve({ success: false, content: null }); } }, onerror: () => resolve({ success: false, content: null }), ontimeout: () => resolve({ success: false, content: null }) }); }); } parseChapterContent(data) { let content = null; // 适配oiapi返回格式:data.data[0].content if (data && data.data && Array.isArray(data.data) && data.data[0]?.content) { content = data.data[0].content; } if (!content) return null; // 移除正文开头重复的章节标题 content = content.replace(/^第[0-9零一二三四五六七八九十百千]+章.*?\n/, ''); return content.trim(); } downloadTxt(results, bookName, bookId) { const lines = []; const encoding = this.cache.elements.fanqieEncoding?.value || 'utf-8'; lines.push(`《${bookName}》`); lines.push(`下载时间: ${new Date().toLocaleString()}`); lines.push(''); let currentVolume = 0; for (const item of results) { if (item.volumeTitle && item.volumeIndex !== currentVolume) { currentVolume = item.volumeIndex; lines.push(''); lines.push(`=== ${item.volumeTitle} ===`); lines.push(''); } lines.push(''); lines.push(item.title); lines.push(''); lines.push(item.content); lines.push(''); } const content = lines.join('\n'); let blob; if (encoding === 'gbk') { const gbkEncoder = new TextEncoder('gbk'); blob = new Blob([gbkEncoder.encode(content)], { type: 'text/plain;charset=gbk' }); } else { blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); } const safeBookName = bookName.replace(/[\\/:*?"<>|]/g, '_'); saveAs(blob, `${safeBookName}.txt`); this.log('💾 文件已下载: ' + `${safeBookName}.txt`, 'success'); } } // 启动模块 new ContentReplacer(); new FanqieDownloader(); })();