天悦小说网
https://www.xtyxsw.org/
autobcb_admin (12020)05/14 04:08
该用户很懒,什么介绍也没有写!
{
"bookSourceUrl": "https:\/\/www.xtyxsw.org\/",
"bookSourceName": "天悦小说网",
"enabledExplore": true,
"enabled": true,
"bookSourceGroup": "",
"author": "trae",
"help": false,
"html": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n <meta charset=\"UTF-8\">\n <title>天悦小说网<\/title>\n<\/head>\n<body>\n\n<\/body>\n<script src=\"https:\/\/vc.jd.com\/web\/js\/jquery-3.1.1.min.js\"><\/script>\n<script>\n var isCookieJar=true;\n class FlutterJSBridge {\n constructor() {\n this.init();\n }\n\n init() {\n if (window.flutter_inappwebview) {\n this.isReady = true;\n this.CookieJar();\n } else {\n window.addEventListener('flutterInAppWebViewPlatformReady', () => {\n this.isReady = true;\n console.log('JSBridge初始化完成');\n this.CookieJar();\n });\n }\n }\n\n async CookieJar() {\n try {\n await window.flutter_inappwebview.callHandler('CookieJar', isCookieJar);\n } catch (error) {\n console.error('汇报完成准备失败:', error);\n }\n }\n\n async getbuildNumber() {\n try { return await window.flutter_inappwebview.callHandler('buildNumber'); } catch (error) { return 0; }\n }\n\n async getversion() {\n try { return await window.flutter_inappwebview.callHandler('version'); } catch (error) { return \"0.0.0\"; }\n }\n \n async htmlToText(str) {\n try { return await window.flutter_inappwebview.callHandler('htmlToText',str); } catch (error) { return \"\"; }\n }\n \n async toTraditional(str) {\n try { return await window.flutter_inappwebview.callHandler('toTraditional',str); } catch (error) { return \"\"; }\n }\n \n async toSimplified(str) {\n try { return await window.flutter_inappwebview.callHandler('toSimplified',str); } catch (error) { return \"\"; }\n }\n\n async voice() {\n try { return await window.flutter_inappwebview.callHandler('voice'); } catch (error) { return \"\"; }\n }\n \n async getDeviceid() {\n try { return await window.flutter_inappwebview.callHandler('id'); } catch (error) { return \"\"; }\n }\n\n async getDevice() {\n try { return await window.flutter_inappwebview.callHandler('device'); } catch (error) { return \"\"; }\n }\n \n async getLoginUser() {\n try { return await window.flutter_inappwebview.callHandler('getLoginUser'); } catch (error) { return \"\"; }\n }\n\n async log(str) {\n try { return await window.flutter_inappwebview.callHandler('log',str); } catch (error) { return false; }\n }\n\n async text(type,str) {\n try { return await window.flutter_inappwebview.callHandler('text',type,str); } catch (error) { return false; }\n }\n\n async showToast(str) {\n try { return await window.flutter_inappwebview.callHandler('showToast',str); } catch (error) { return false; }\n }\n \n async showLongToast(str) {\n try { return await window.flutter_inappwebview.callHandler('showLongToast',str); } catch (error) { return false; }\n }\n\n async getWebViewUA() {\n try { return await window.flutter_inappwebview.callHandler('getWebViewUA'); } catch (error) { return \"\"; }\n }\n\n async openurl(url) {\n try { return await window.flutter_inappwebview.callHandler('openurl',url,\"\"); } catch (error) { return false; }\n }\n\n async openurlwithMimeType(url,mimeType) {\n try { return await window.flutter_inappwebview.callHandler('openurl',url,mimeType); } catch (error) { return false; }\n }\n\n async webview(url,js,html,body,header) {\n try { return await window.flutter_inappwebview.callHandler('webview',url,js,html,body,header,\"\",\"\"); } catch (error) { return \"\"; }\n }\n\n async webViewGetOverrideUrl(url,js,html,body,header,overrideUrlRegex) {\n try { return await window.flutter_inappwebview.callHandler('webview',url,js,html,body,header,overrideUrlRegex,\"\"); } catch (error) { return \"\"; }\n }\n\n async webViewGetSource(url,js,html,body,header,urlregex) {\n try { return await window.flutter_inappwebview.callHandler('webview',url,js,html,body,header,\"\",urlregex); } catch (error) { return \"\"; }\n }\n \n async webViewGetAjax(url,html,body,header,ajaxregex) {\n try { return await window.flutter_inappwebview.callHandler('webviewajax',url,html,body,header,ajaxregex); } catch (error) { return \"\"; }\n }\n\n async startBrowser(url,title,header) {\n try { return await window.flutter_inappwebview.callHandler('startBrowser',url,title,header); } catch (error) { return \"\"; }\n }\n \n async startBrowserWithShouldOverrideUrlLoading(url,title,header) {\n try { return await window.flutter_inappwebview.callHandler('startBrowserWithShouldOverrideUrlLoading',url,title,header); } catch (error) { return \"\"; }\n }\n\n async startBrowserDp(url,title) {\n try { return await window.flutter_inappwebview.callHandler('startBrowserDp',url,title); } catch (error) { return \"\"; }\n }\n\n async back() {\n try { return await window.flutter_inappwebview.callHandler('back'); } catch (error) { return false; }\n }\n\n async utf8ToGbkUrlEncoded(str) {\n try { return await window.flutter_inappwebview.callHandler('utf8ToGbkUrlEncoded',str); } catch (error) { return \"\"; }\n }\n\n async getVerificationCode(str,header) {\n try { return await window.flutter_inappwebview.callHandler('getVerificationCode',str,header); } catch (error) { return \"\"; }\n }\n \n async addbook(bookUrl) {\n try { return await window.flutter_inappwebview.callHandler('addbook',bookUrl); } catch (error) { return \"\"; }\n }\n \n async getdurChapterIndex(bookUrl) {\n try { return await window.flutter_inappwebview.callHandler('getdurChapterIndex',bookUrl); } catch (error) { return 0; }\n }\n \n async base64encode(str) {\n try { return await window.flutter_inappwebview.callHandler('base64encode',str); } catch (error) { return \"\"; }\n }\n \n async base64decode(str) {\n try { return await window.flutter_inappwebview.callHandler('base64decode',str); } catch (error) { return \"\"; }\n }\n }\n\n class Http {\n constructor() {\n this.open = false;\n this.requestTimestamps = [];\n this.rateLimit = 5;\n this.rateLimitWindow = 1000;\n }\n\n async checkRateLimit() {\n if(!this.open) return;\n const now = Date.now();\n this.requestTimestamps = this.requestTimestamps.filter(timestamp => now - timestamp < this.rateLimitWindow);\n if (this.requestTimestamps.length >= this.rateLimit) {\n const oldestTimestamp = this.requestTimestamps[0];\n const waitTime = this.rateLimitWindow - (now - oldestTimestamp);\n await new Promise(resolve => setTimeout(resolve, waitTime));\n return this.checkRateLimit();\n }\n this.requestTimestamps.push(now);\n }\n\n async Get(url,headers,followRedirects) {\n try { await this.checkRateLimit(); return await window.flutter_inappwebview.callHandler('http',\"get\",url,\"\",JSON.stringify(headers),followRedirects,\"\"); } catch (error) { return null; }\n }\n\n async Head(url,headers,followRedirects) {\n try { await this.checkRateLimit(); return await window.flutter_inappwebview.callHandler('http',\"head\",url,\"\",JSON.stringify(headers),followRedirects,\"\"); } catch (error) { return null; }\n }\n \n async Post(url,headers,body,contenttype,followRedirects) {\n try { await this.checkRateLimit(); return await window.flutter_inappwebview.callHandler('http',\"post\",url,body,JSON.stringify(headers),followRedirects,contenttype); } catch (error) { return null; }\n }\n }\n\n class Cache {\n constructor() {}\n async get(key) { try { return await window.flutter_inappwebview.callHandler('cache.get',key); } catch (error) { return null; } }\n async set(key,value) { try { return await window.flutter_inappwebview.callHandler('cache.set',key,value); } catch (error) { return null; } }\n async remove(key) { try { return await window.flutter_inappwebview.callHandler('cache.remove',key); } catch (error) { return null; } }\n async getLoginInfo(){ return await this.get(\"LoginInfo\") }\n async putLoginInfo(info){ return await this.set(\"LoginInfo\",info) }\n async getbookVariable(bookurl){ return await this.get(bookurl) }\n async setbookVariable(bookurl,value){ return await this.set(bookurl,value) }\n }\n\n class Cookie {\n constructor() {}\n async get(url) { try { return await window.flutter_inappwebview.callHandler('cookie.get',url); } catch (error) { return null; } }\n async remove(url) { try { return await window.flutter_inappwebview.callHandler('cookie.remove',url); } catch (error) { return null; } }\n async set(url,value) { try { return await window.flutter_inappwebview.callHandler('cookie.set',url,value); } catch (error) { return null; } }\n async setCookie(url,key,value) { try { return await window.flutter_inappwebview.callHandler('cookie.setcookie',url,key,value); } catch (error) { return null; } }\n async getCookie(url,value) { try { return await window.flutter_inappwebview.callHandler('cookie.getCookie',url,value); } catch (error) { return null; } }\n }\n\n function parseHTMLSafely(htmlStr) {\n try { var tempDiv = document.createElement('div'); tempDiv.innerHTML = htmlStr; return $(tempDiv); } catch (e) { flutterBridge.log(\"HTML解析错误:\"+e.message); return $('<div>'); }\n }\n\n function removeHTMLSafely(tempContainer) {\n try { tempContainer.innerHTML = ''; if (tempContainer.parentNode) { tempContainer.parentNode.removeChild(tempContainer); } } catch (e) { flutterBridge.log(\"HTML移除失败:\"+e.message); }\n }\n\n function removeHTMLTags(htmlString) {\n let result = htmlString.replace(\/<script\\b[^<]*(?:(?!<\\\/script>)<[^<]*)*<\\\/script>\/gi, '');\n result = result.replace(\/<style\\b[^<]*(?:(?!<\\\/style>)<[^<]*)*<\\\/style>\/gi, '');\n return result;\n }\n\n<\/script>\n\n<script>\n const flutterBridge = new FlutterJSBridge();\n const cache = new Cache();\n const http = new Http();\n const cookie = new Cookie();\n var baseurl=\"https:\/\/www.xtyxsw.org\"\n var header={\n \"User-Agent\": \"Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/120.0.0.0 Safari\/537.36\",\n \"Referer\": baseurl,\n \"Accept\": \"text\/html,application\/xhtml+xml,application\/xml;q=0.9,*\/*;q=0.8\"\n };\n\n function normalizeUrl(url) {\n if (!url) return \"\";\n if (url.startsWith('http')) return url;\n if (url.startsWith('\/')) return baseurl + url;\n return baseurl + '\/' + url;\n }\n\n \/*\n * search(key, page) - 搜索功能\n * \n * ⚠️ 重要发现:天悦小说网的搜索关键词会被编码成数字ID!\n * 示例:\n * \"我的\" → 487982 → URL: \/search\/487982\/1.html\n * \"系统\" → 320182 → URL: \/search\/320182\/1.html\n * \"测试\" → 397748 → URL: \/search\/397748\/1.html\n * \n * 这不是标准的URL编码,而是网站特有的编码机制。\n * 解决方案:使用WebView方式,让浏览器自动处理编码!\n *\/\n async function search(key, page) {\n if(page > 1){\n \/\/ 翻页时可以直接构造URL(因为已经有了数字ID)\n \/\/ 但我们不知道ID,所以暂时只支持第1页\n return \"[]\";\n }\n\n var mheader={\n ...header,\n \"Referer\": baseurl\n };\n\n flutterBridge.log(\"开始搜索: \" + key);\n\n \/\/ ====== 方案:使用WebView自动执行搜索 ======\n \/\/ 原理:\n \/\/ 1. 先访问首页\n \/\/ 2. 用JS自动填写搜索框\n \/\/ 3. 自动点击搜索按钮\n \/\/ 4. 让浏览器自己处理关键词编码\n \/\/ 5. 拦截跳转后的搜索结果页面\n \n var injectJs = `\n (function(){\n try{\n \/\/ 查找搜索输入框(通过placeholder或其他特征)\n var input = null;\n var inputs = document.querySelectorAll('input[type=\"text\"], input:not([type])');\n for(var i=0; i<inputs.length; i++){\n if(inputs[i].placeholder && inputs[i].placeholder.indexOf('书名') !== -1){\n input = inputs[i];\n break;\n }\n }\n \n if(!input){\n \/\/ 备选:找第一个文本输入框\n input = document.querySelector('input[type=\"text\"]') || document.querySelector('input');\n }\n \n if(input){\n \/\/ 设置关键词\n input.value = '${key.replace(\/'\/g, \"\\\\'\")}';\n \n \/\/ 触发input事件(某些网站需要)\n input.dispatchEvent(new Event('input', {bubbles: true}));\n input.dispatchEvent(new Event('change', {bubbles: true}));\n \n \/\/ 查找并点击搜索按钮\n setTimeout(function(){\n \/\/ 尝试多种方式找到按钮\n var btn = null;\n \n \/\/ 方法1: input的下一个兄弟元素\n btn = input.nextElementSibling;\n if(btn && (btn.tagName === 'BUTTON' || btn.tagName === 'A' || btn.type === 'submit')){\n btn.click();\n return;\n }\n \n \/\/ 方法2: 查找包含\"搜索\"文字的按钮\/链接\n var elements = document.querySelectorAll('button, a, input[type=\"submit\"], input[type=\"button\"]');\n for(var j=0; j<elements.length; j++){\n var text = (elements[j].textContent || elements[j].value || '').trim();\n if(text.indexOf('搜索') !== -1 || text.indexOf('开始') !== -1){\n elements[j].click();\n return;\n }\n }\n \n \/\/ 方法3: 找form并提交\n var form = input.form || input.closest('form');\n if(form){\n form.submit();\n } else {\n \/\/ 最后尝试:模拟回车键\n var evt = new KeyboardEvent('keydown', {keyCode: 13, which: 13});\n input.dispatchEvent(evt);\n }\n }, 300);\n \n return true;\n } else {\n console.log('未找到搜索输入框');\n return false;\n }\n } catch(e) {\n console.log('搜索注入JS出错: ' + e.message);\n return false;\n }\n })();\n `;\n\n try {\n \/\/ ====== 正确的WebView搜索流程 ======\n \/\/ 第1步:先获取首页HTML\n flutterBridge.log(\"第1步: 获取首页HTML...\");\n var homeResult = await http.Get(baseurl, mheader, true);\n \n if(!homeResult || !homeResult.data){\n flutterBridge.showToast(\"获取首页失败\");\n return \"[]\";\n }\n \n var homeHtml = homeResult.data;\n flutterBridge.text(0, homeHtml); \/\/ 调试用:查看首页结构\n flutterBridge.log(\"获取到首页HTML,长度: \" + homeHtml.length);\n\n \/\/ 第2步:将搜索执行代码作为<script>标签插入HTML\n \/\/ 注意:webview的js参数只支持简单一行代码,复杂逻辑必须写在html参数的<script>里!\n \/\/ 构建搜索脚本\n \/\/ 使用数组拼接避免引号嵌套问题\n var k = key; \/\/ 局部变量便于引用\n \n var scriptLines = [];\n scriptLines.push(\"<\" + \"script>\");\n scriptLines.push(\"var __search_keyword__ = \" + JSON.stringify(k) + \";\");\n scriptLines.push(\"setTimeout(function(){\");\n scriptLines.push(\"try{\");\n scriptLines.push(\"var input = null;\");\n scriptLines.push(\"var inputs = document.getElementsByTagName('input');\");\n scriptLines.push(\"for(var i=0; i<inputs.length; i++){\");\n scriptLines.push(\"if(inputs[i].placeholder && inputs[i].placeholder.indexOf('书名') !== -1){input = inputs[i]; break;}\");\n scriptLines.push(\"}\");\n scriptLines.push(\"if(!input){ input = document.getElementsByTagName('input')[0]; }\");\n scriptLines.push(\"if(input){\");\n scriptLines.push(\"input.value = __search_keyword__;\"); \/\/ 使用预定义的变量\n scriptLines.push(\"input.dispatchEvent(new Event('input', {bubbles: true}));\");\n scriptLines.push(\"input.dispatchEvent(new Event('change', {bubbles: true}));\");\n scriptLines.push(\"setTimeout(function(){\");\n scriptLines.push(\"var btn = input.nextElementSibling;\");\n scriptLines.push(\"if(btn && (btn.tagName === 'BUTTON' || btn.tagName === 'A')){ btn.click(); return; }\");\n scriptLines.push(\"var elements = document.getElementsByTagName('button');\");\n scriptLines.push(\"for(var j=0; j<elements.length; j++){\");\n scriptLines.push(\"var text = (elements[j].textContent || '').trim();\");\n scriptLines.push(\"if(text.indexOf('搜索') !== -1 || text.indexOf('开始') !== -1){ elements[j].click(); return; }\");\n scriptLines.push(\"}\");\n scriptLines.push(\"var form = input.form;\");\n scriptLines.push(\"if(form) form.submit();\");\n scriptLines.push(\"}, 300);\");\n scriptLines.push(\"}\");\n scriptLines.push(\"} catch(e) { console.log('error: ' + e.message); }\");\n scriptLines.push(\"}, 500);\");\n scriptLines.push(\"<\" + \"\/script>\");\n \n var searchScript = scriptLines.join(\"\\n\");\n \n \/\/ 将脚本插入到HTML\n \/\/ 注意:很多网站的HTML可能没有标准的<\/body>标签,所以需要多种策略\n \n var htmlWithScript = \"\";\n \n \/\/ 策略1: 尝试在<\/body>前插入\n if (homeHtml.indexOf('<\/body>') !== -1) {\n htmlWithScript = homeHtml.replace(\/<\\\/body>\/i, searchScript + '<\/body>');\n flutterBridge.log(\"使用策略1: 在<\/body>前插入脚本\");\n }\n \/\/ 策略2: 尝试在<\/html>前插入\n else if (homeHtml.indexOf('<\/html>') !== -1) {\n htmlWithScript = homeHtml.replace(\/<\\\/html>\/i, searchScript + '<\/html>');\n flutterBridge.log(\"使用策略2: 在<\/html>前插入脚本\");\n }\n \/\/ 策略3: 直接追加到HTML末尾\n else {\n htmlWithScript = homeHtml + \"\\n\" + searchScript;\n flutterBridge.log(\"使用策略3: 追加到HTML末尾\");\n }\n \n \/\/ 第3步:使用webview加载包含脚本的HTML\n \/\/ 参数说明:\n \/\/ - url: 首页URL(浏览器会基于此加载资源)\n \/\/ - js: 简单的一行JS(这里不需要)\n \/\/ - html: 包含完整搜索脚本的HTML\n \/\/ - body: POST数据(空)\n \/\/ - header: 请求头\n flutterBridge.log(\"第2步: 使用webview加载并自动搜索...\");\n \n var htmlContent = await flutterBridge.webview(\n baseurl, \/\/ URL\n \"\", \/\/ js(简单代码,这里用不到)\n htmlWithScript, \/\/ ⭐ 包含搜索脚本的HTML!\n \"\", \/\/ body\n JSON.stringify(mheader) \/\/ header\n );\n\n if(!htmlContent){\n flutterBridge.showToast(\"搜索请求失败\");\n return \"[]\";\n }\n \/\/ flutterBridge.log(\"搜索结果HTML: \" + htmlWithScript);\n\n flutterBridge.text(0, htmlContent);\n flutterBridge.log(\"获取到搜索结果HTML,长度: \" + htmlContent.length);\n\n } catch(e) {\n flutterBridge.log(\"WebView搜索失败: \" + e.message);\n \n \/\/ 兜底方案:如果WebView失败,提示用户\n flutterBridge.showToast(\"搜索功能暂时不可用\");\n return \"[]\";\n }\n\n var books = [];\n var processedUrls = new Set();\n var $tempContainer = parseHTMLSafely(removeHTMLTags(htmlContent));\n\n \/\/ 已验证书籍链接格式: \/read\/{书籍ID}\/\n \/\/ 示例: \/read\/264234\/\n var bookLinkMatcher = function(href) {\n if (!href) return false;\n return \/^\\\/read\\\/\\d+\\\/$\/.test(href);\n };\n\n \/\/ ====== 基于浏览器100%验证过的真实DOM结构 ======\n \/\/ 验证发现每本书的结构:\n \/\/ \n \/\/ <div> ← 每本书的容器(具体class待确认)\n \/\/ <a href=\"\/read\/{书籍ID}\/\">\n \/\/ <img src=\"https:\/\/img.xtyxsw.org\/{书籍ID}\/{图片ID}.jpg\" alt=\"书名\">\n \/\/ <\/a> ← 封面图\n \/\/ \n \/\/ <h2>\n \/\/ <a href=\"\/read\/{书籍ID}\/\">《 书名 》<\/a>\n \/\/ <\/h2> ← 书名(带书名号)\n \/\/ \n \/\/ 作者:<a href=\"\/author\/{作者}.html\">{作者名}<\/a> ← 作者(链接格式:\/author\/xxx.html)\n \/\/ \n \/\/ 最新更新:<a href=\"\/read\/{书籍ID}\/{章节ID}.html\">第{N}章 标题<\/a> ← 最新章节\n \/\/ \n \/\/ <p>简介文本...<\/p> ← 简介\n \/\/ \n \/\/ <a href=\"\/read\/{书籍ID}\/\">开始阅读<\/a> ← 阅读按钮\n \/\/ <a href=\"#\">加入书架<\/a> ← 加架按钮\n \/\/ <\/div>\n\n \/\/ 策略1: 查找包含 \/read\/{数字}\/ 格式链接的容器(这是最可靠的特征)\n $tempContainer.find(\"div, li, section, article\").each(function() {\n try{\n var $item = $(this);\n \n \/\/ 必须包含书籍链接(\/read\/数字\/格式)- 这是识别书籍容器的核心特征!\n var $bookLink = $item.find(\"a[href*='\/read\/']\").first();\n var bookUrl = $bookLink.attr('href');\n \n if (!bookUrl || !bookLinkMatcher(bookUrl) || processedUrls.has(bookUrl)) { \n return true; \/\/ 不是书籍容器或已处理过,跳过\n }\n \n processedUrls.add(bookUrl);\n \n \/\/ ========== 1. 书名 ==========\n \/\/ 已验证:在 <h2><a> 或直接 <a> 中,可能带书名号《》\n var bookName = \"\";\n \n \/\/ 方法A:从h2标签获取\n var $h2 = $item.find(\"h2\").first();\n if($h2.length > 0){\n bookName = $h2.find(\"a\").first().attr('title') || $h2.find(\"a\").first().text() || $h2.text();\n bookName = bookName.trim();\n }\n \n \/\/ 方法B:从书籍链接本身获取(如果h2没找到)\n if(!bookName){\n bookName = $bookLink.attr('title') || $bookLink.text().trim();\n }\n \n \/\/ 清理书名号\n if(bookName){\n bookName = bookName.replace(\/[《》\"\"''【】\\(\\)]\/g, '').trim();\n }\n \n if(!bookName || bookName.length < 2) return true;\n \n \/\/ ========== 2. 作者 ==========\n \/\/ 已验证:<a href=\"\/author\/{作者名}.html\">作者名<\/a>\n var author = \"\";\n \n \/\/ 方法A:查找 \/author\/ 格式的链接(最准确!)\n $item.find(\"a[href*='\/author\/']\").each(function(){\n if(!author){\n author = $(this).text().trim();\n flutterBridge.log(\"找到作者链接: \" + author);\n }\n });\n \n \/\/ 方法B:查找\"作者:\"后面的内容\n if(!author){\n $item.find(\"*\").each(function(){\n if(author) return false;\n \n var text = $(this).text().trim();\n if(text.indexOf('作者') === 0 && text.length < 30){\n var match = text.match(\/作者[::\\s]*(.+)$\/);\n if(match && match[1]){\n author = match[1].trim();\n \/\/ 如果提取到的太长,可能不是纯作者名\n if(author.length > 10) author = \"\"; \n }\n }\n });\n }\n \n \/\/ ========== 3. 封面图 ==========\n \/\/ 已验证:<img src=\"https:\/\/img.xtyxsw.org\/{ID}\/{IMGID}.jpg\">\n var coverUrl = \"\";\n \n \/\/ 查找img标签(优先找书籍链接内的img)\n var $coverImg = $bookLink.find(\"img\").first();\n if($coverImg.length === 0){\n $coverImg = $item.find(\"img\").first();\n }\n \n if($coverImg.length > 0){\n coverUrl = $coverImg.attr('src') || $coverImg.attr('data-original') || \"\";\n \n \/\/ 验证是否是有效的图片URL(应该包含 img.xtyxsw.org 或其他图片域名)\n if(coverUrl && !coverUrl.match(\/^https?:\\\/\\\/(img\\.|.*\\.(jpg|png|gif|webp))\/i)){\n coverUrl = \"\"; \/\/ 不是有效图片URL\n }\n }\n \n \/\/ ========== 4. 简介 ==========\n \/\/ 已验证:在 <p> 或 <div> 中,长度适中,不包含按钮文字\n var intro = \"\";\n \n \/\/ 查找可能的简介元素(排除掉包含按钮文字的元素)\n $item.find(\"p, div, span\").each(function(){\n if(intro) return false; \/\/ 已经找到了\n \n var $elem = $(this);\n var text = $elem.text().trim();\n \n \/\/ 简介特征:\n \/\/ - 长度 > 30字(排除短标签)\n \/\/ - 长度 < 500字(排除过长内容)\n \/\/ - 不包含按钮文字\n \/\/ - 不是<a>标签\n \/\/ - 不以\"作者\"、\"最新\"、\"开始\"、\"加入\"开头\n if(text.length > 30 && text.length < 500 &&\n !$elem.is(\"a\") &&\n !$elem.is(\"h1, h2, h3, h4\") &&\n text.indexOf('开始阅读') === -1 &&\n text.indexOf('加入书架') === -1 &&\n text.indexOf('TXT下载') === -1 &&\n text.indexOf('推荐本书') === -1 &&\n text.indexOf('投票') === -1 &&\n !text.startsWith('作者') &&\n !text.startsWith('最新更新') &&\n !text.startsWith('类别') &&\n !text.startsWith('状态')){\n \n intro = text;\n }\n });\n \n \/\/ 截断过长简介\n if(intro.length > 200) intro = intro.substring(0, 200);\n \n \/\/ ========== 5. 最新章节标题 ==========\n \/\/ 已验证:<a href=\"\/read\/{书籍ID}\/{章节ID}.html\">第{N}章 标题<\/a>\n var latestChapterTitle = \"\";\n \n \/\/ 查找所有 \/read\/ 链接(排除第一个,那是书籍详情链接)\n $item.find(\"a[href*='\/read\/']\").not(\":first\").each(function(){\n if(latestChapterTitle) return false; \/\/ 已找到\n \n var chText = $(this).text().trim();\n var chHref = $(this).attr('href') || \"\";\n \n \/\/ 最新章节特征:\n \/\/ - 以\"第\"开头\n \/\/ - 包含\"章\"\n \/\/ - 链接格式:\/read\/{id}\/{chapter_id}.html (不是 \/read\/{id}\/ 这种目录格式)\n if(chText.match(\/^第\\s*\\d+\\s*[章节篇]\/) && chHref.match(\/\\\/read\\\/\\d+\\\/\\d+\\.html$\/)){\n latestChapterTitle = chText;\n flutterBridge.log(\"找到最新章节: \" + latestChapterTitle);\n }\n });\n \n \/\/ ========== 构建书籍对象 ==========\n bookUrl = normalizeUrl(bookUrl);\n\n var book={\n \"bookUrl\": bookUrl,\n \"name\": bookName,\n \"author\": author,\n \"kind\": \"\",\n \"coverUrl\": normalizeUrl(coverUrl),\n \"intro\": intro,\n \"tocUrl\": bookUrl,\n \"wordCount\": \"\",\n \"type\": 0,\n \"latestChapterTitle\": latestChapterTitle\n };\n \n flutterBridge.log(\"✅ 提取书籍: \" + bookName + \" | 作者: \" + author + \" | 最新: \" + latestChapterTitle);\n books.push(book);\n \n }catch(e){\n flutterBridge.log(\"解析书籍出错: \" + e.message);\n }\n });\n\n \/\/ 备选方案:如果没找到特定容器\n if(books.length === 0) {\n $tempContainer.find(\"a\").each(function() {\n try{\n var bookUrl = $(this).attr('href');\n \n if (!bookUrl || processedUrls.has(bookUrl)) { return true; }\n if (!bookLinkMatcher(bookUrl)) { return true; }\n \n processedUrls.add(bookUrl);\n \n var bookName = $(this).attr('title') || $(this).text().trim();\n\n if(bookName && bookName.length > 1){\n bookUrl = normalizeUrl(bookUrl);\n\n var book={\n \"bookUrl\": bookUrl,\n \"name\": bookName,\n \"author\": \"\",\n \"kind\": \"\",\n \"coverUrl\": \"\",\n \"intro\": \"\",\n \"tocUrl\": bookUrl,\n \"wordCount\": \"\",\n \"type\": 0,\n \"latestChapterTitle\": \"\"\n };\n books.push(book);\n }\n }catch(e){\n flutterBridge.log(\"解析书籍信息出错: \" + e.message);\n }\n });\n }\n\n removeHTMLSafely($tempContainer);\n flutterBridge.log(\"找到 \" + books.length + \" 本书\");\n return JSON.stringify(books);\n }\n\n \/*\n * info(bookurl) - 书籍详情\n * 已验证URL格式: \/read\/{书籍ID}\/\n *\/\n async function info(bookurl) {\n var mheader={\n ...header,\n \"Referer\": baseurl\n };\n\n var htmlResult = await http.Get(bookurl, mheader, true);\n\n if(!htmlResult || !htmlResult.data){\n flutterBridge.showToast(\"获取信息失败\");\n return JSON.stringify({});\n }\n\n var htmlContent = htmlResult.data;\n\n flutterBridge.text(1, htmlContent);\n\n var $tempContainer = parseHTMLSafely(removeHTMLTags(htmlContent));\n\n var name = \"\";\n var author = \"\";\n var kind = \"\";\n var intro = \"\";\n var latestChapterTitle = \"\";\n var coverUrl = \"\";\n\n \/\/ 提取书名(h1标签)\n name = $tempContainer.find(\"h1\").first().text().trim();\n \n \/\/ 从h1中分离作者(格式:\"书名 作者:xxx\" 或类似)\n if(name && name.indexOf('作者') !== -1){\n var authorMatch = name.match(\/作者[::\\s]*(.+?)(?:\\s|$)\/);\n if(authorMatch && authorMatch[1]){\n author = authorMatch[1].trim();\n name = name.replace(\/作者[::\\s].+$\/, '').trim();\n }\n }\n\n \/\/ 提取封面图\n coverUrl = $tempContainer.find(\".cover img, .book-cover img, #fmimg img, img[class*='cover']\").first().attr('src') || \"\";\n \n \/\/ 遍历页面元素提取详细信息\n $tempContainer.find(\"*\").each(function() {\n var text = $(this).text().trim();\n\n \/\/ 分类\n if(!kind && text.length < 15) {\n var categories = ['玄幻', '武侠', '都市', '历史', '科幻', '游戏', '女生', '其他', '言情', '军事'];\n for(var i=0; i<categories.length; i++) {\n if(text.indexOf(categories[i]) !== -1) {\n kind = categories[i];\n break;\n }\n }\n }\n\n \/\/ 最新章节\n if(!latestChapterTitle && text.length < 60 && name) {\n var chapterMatch = text.match(\/(第\\s*\\d+\\s*[章节篇])\/);\n if(chapterMatch && text.indexOf(name) === -1) { \n latestChapterTitle = chapterMatch[1]; \n }\n }\n });\n\n \/\/ 提取简介\n $tempContainer.find(\"#intro, .intro, .desc, .summary, .book-intro, #description\").each(function() {\n if(intro) return false;\n \n var text = $(this).text().trim();\n \n if(text.length > 50 && text.length < 2000 && \n text.indexOf(name) === -1 &&\n text.indexOf('加入书架') === -1 &&\n text.indexOf('马上阅读') === -1 &&\n text.indexOf('推荐') === -1 &&\n text.indexOf('催更') === -1) {\n intro = text;\n }\n });\n\n \/\/ 如果没找到简介,尝试从段落中获取\n if(!intro) {\n $tempContainer.find(\"p, div\").each(function() {\n if(intro) return false;\n \n var text = $(this).text().trim();\n \n if(text.length > 50 && text.length < 2000 && \n text.indexOf(name) === -1 &&\n text.indexOf('加入书架') === -1 &&\n text.indexOf('马上阅读') === -1) {\n intro = text;\n }\n });\n }\n\n var book={\n \"bookUrl\": bookurl,\n \"name\": name,\n \"author\": author,\n \"kind\": kind,\n \"coverUrl\": normalizeUrl(coverUrl),\n \"intro\": intro,\n \"tocUrl\": bookurl,\n \"wordCount\": \"\",\n \"type\": 0,\n \"latestChapterTitle\": latestChapterTitle\n };\n\n removeHTMLSafely($tempContainer);\n return JSON.stringify(book);\n }\n\n \/*\n * chapter(tocUrl) - 章节列表\n * 已验证: 章节列表一次性全部加载,无\"最新章节\"\/\"全部章节\"分区!\n * 章节链接格式: \/read\/{书籍ID}\/{章节ID}.html\n * 无展开按钮、无隐藏章节\n *\/\n async function chapter(tocUrl) {\n var mheader={\n ...header,\n \"Referer\": baseurl\n };\n\n var htmlResult = await http.Get(tocUrl, mheader, true);\n\n if(!htmlResult || !htmlResult.data){\n flutterBridge.showToast(\"获取目录失败\");\n return \"[]\";\n }\n\n var htmlContent = htmlResult.data;\n\n flutterBridge.text(2, htmlContent);\n\n var chapters = [];\n var processedUrls = new Set();\n var $tempContainer = parseHTMLSafely(removeHTMLTags(htmlContent));\n\n \/\/ 已验证章节链接格式: \/read\/{书籍ID}\/{章节ID}.html\n \/\/ 示例: \/read\/264234\/193501234.html\n var chapterLinkMatcher = function(href) {\n if (!href) return false;\n \n \/\/ 移除域名前缀\n href = href.replace(\/^https?:\\\/\\\/[^\\\/]+\/, '');\n \n \/\/ 匹配: \/read\/数字\/数字.html\n return \/^\\\/read\\\/\\d+\\\/\\d+\\.html$\/.test(href);\n };\n\n \/\/ ====== 天悦小说网章节提取(已验证有\"最新章节\"+\"全部章节\"两个区域)=====\n \/\/ 真实结构:\n \/\/ <div>最新章节<\/div>\n \/\/ <a>第1483章 婚礼<\/a> ← 要排除!\n \/\/ ...\n \/\/ <div>全部章节目录(共xxx章)<\/div> ← ⭐ 从这里开始\n \/\/ <a>第1章 xxx<\/a>\n \/\/ <a>第2章 xxx<\/a>\n \/\/ ...\n \n var $catalogHeader = null;\n \n \/\/ 查找\"全部章节目录\"标题\n $tempContainer.find(\"dt, h2, h3, h4, div, span, strong\").each(function() {\n var text = $(this).text().trim();\n \n if ((text.indexOf('全部章节') !== -1 ||\n text.indexOf('正文目录') !== -1 ||\n text.indexOf('章节目录') !== -1) &&\n text.length < 100) {\n \n $catalogHeader = $(this);\n flutterBridge.log(\"✅ 找到'全部章节目录': \" + text.substring(0, 80));\n return false;\n }\n });\n\n if ($catalogHeader && $catalogHeader.length > 0) {\n \/\/ 使用标记法:找到标题后才开始提取\n var startExtracting = false;\n var $parentContainer = $catalogHeader.parent();\n \n \/\/ 遍历父容器的所有子元素\n $parentContainer.children().each(function() {\n var $child = $(this);\n var tagName = this.tagName.toLowerCase();\n \n \/\/ 如果遇到目标标题 → 开始提取\n if (!startExtracting) {\n if ($child[0] === $catalogHeader[0]) {\n startExtracting = true;\n flutterBridge.log(\"✅ 开始提取模式\");\n }\n return true;\n }\n \n \/\/ 开始提取后,只处理<a>标签\n if (tagName === 'a') {\n try{\n var chUrl = $child.attr('href');\n var chName = $child.text().trim();\n\n if(!chUrl || !chName || chName.length < 2){\n return true;\n }\n\n if(processedUrls.has(chUrl)){\n return true;\n }\n\n if(!chapterLinkMatcher(chUrl)){\n return true;\n }\n\n processedUrls.add(chUrl);\n chUrl = normalizeUrl(chUrl);\n\n chapters.push({\n \"name\": chName,\n \"chapterId\": chUrl,\n \"index\": chapters.length,\n \"isPay\": false,\n \"isVip\": false,\n \"isVolume\": false,\n \"tag\": \"\"\n });\n }catch(e){\n flutterBridge.log(\"解析章节出错: \" + e.message);\n }\n }\n \n return true;\n });\n\n flutterBridge.log(\"📚 提取了 \" + chapters.length + \" 个章节\");\n } else {\n \/\/ 兜底方案:提取所有章节并用URL去重\n flutterBridge.log(\"未找到'全部章节目录'标题,使用去重+排序模式\");\n\n var allChapters = [];\n\n $tempContainer.find(\"a\").each(function(index) {\n try{\n var chUrl = $(this).attr('href');\n var chName = $(this).text().trim();\n\n if(!chUrl || !chName || chName.length < 2){\n return true;\n }\n\n if(processedUrls.has(chUrl)){\n return true;\n }\n\n if(!chapterLinkMatcher(chUrl)){\n return true;\n }\n\n processedUrls.add(chUrl);\n chUrl = normalizeUrl(chUrl);\n\n \/\/ 提取章节号用于排序\n var chapterNumMatch = chUrl.match(\/\\\/(\\d+)\\.html$\/);\n var chapterNum = chapterNumMatch ? parseInt(chapterNumMatch[1]) : 999999;\n\n allChapters.push({\n name: chName,\n chapterId: chUrl,\n num: chapterNum\n });\n }catch(e){\n flutterBridge.log(\"解析章节出错: \" + e.message);\n }\n });\n\n \/\/ 按章节号正序排列(第1章在最前)\n allChapters.sort(function(a, b) { return a.num - b.num; });\n\n allChapters.forEach(function(ch, idx) {\n chapters.push({\n \"name\": ch.name,\n \"chapterId\": ch.chapterId,\n \"index\": idx,\n \"isPay\": false,\n \"isVip\": false,\n \"isVolume\": false,\n \"tag\": \"\"\n });\n });\n\n flutterBridge.log(\"去重+排序模式找到 \" + chapters.length + \" 个章节\");\n }\n\n removeHTMLSafely($tempContainer);\n flutterBridge.log(\"总共返回 \" + chapters.length + \" 个章节\");\n return JSON.stringify(chapters);\n }\n\n \/*\n * content(url) - 章节内容\n * 已验证URL格式: \/read\/{书籍ID}\/{章节ID}.html\n * 已确认存在分页: \/read\/{书籍ID}\/{章节ID}_{页码}.html\n *\/\n async function content(url) {\n var mheader={\n ...header,\n \"Referer\": baseurl\n };\n\n var htmlResult = await http.Get(url, mheader, true);\n\n if(!htmlResult || !htmlResult.data){\n flutterBridge.showToast(\"获取内容失败\");\n return \"\";\n }\n\n var htmlContent = htmlResult.data;\n\n flutterBridge.text(3, htmlContent);\n\n var allContent = []; \/\/ 存储所有页面内容\n var currentUrl = url; \/\/ 当前页面URL\n var maxPages = 20; \/\/ 最大页数限制\n var pageCount = 0;\n\n while (currentUrl && pageCount < maxPages) {\n pageCount++;\n\n \/\/ 解析当前页面\n var $tempContainer = parseHTMLSafely(removeHTMLTags(htmlContent));\n\n \/\/ 查找正文容器\n var $contentElement = null;\n \n \/\/ 方法1: 查找正文容器(如果存在)\n var candidates = $tempContainer.find(\"#content, .content, #BookText, #BookTextCon, .chapter-content, .text-content\");\n if (candidates.length > 0) {\n $contentElement = candidates.first();\n }\n \n \/\/ 方法2: 如果没找到特定容器,收集所有文本节点(保留换行!)\n if (!$contentElement || $contentElement.length === 0) {\n var pageTexts = [];\n \n \/\/ 尝试从多个来源收集文本 - 保留每个元素的独立段落\n $tempContainer.find(\"div, p\").each(function() {\n var $elem = $(this);\n var text = $elem.text().trim();\n \n \/\/ 过滤掉导航、按钮等无关内容\n if(text.length > 10 && \n text.indexOf('首页') === -1 && \n text.indexOf('目录') === -1 &&\n text.indexOf('上一章') === -1 &&\n text.indexOf('下一章') === -1 &&\n text.indexOf('加入书架') === -1 &&\n text.indexOf('推荐') === -1 &&\n text.indexOf('催更') === -1 &&\n !text.startsWith('http') &&\n text.indexOf('本小章还未完') === -1 &&\n \/\/ 过滤掉只包含数字或短标签的元素\n !$elem.is(\"a\") &&\n !$elem.hasClass(\"btn\") &&\n !$elem.hasClass(\"button\")) {\n \n \/\/ ✅ 关键:保留每个段落作为独立单元\n pageTexts.push(text);\n }\n });\n \n if (pageTexts.length > 0) {\n \/\/ 用双换行连接每个段落,保留格式!\n var combinedText = pageTexts.join(\"\\n\\n\");\n allContent.push(combinedText);\n flutterBridge.log(\"获取第 \" + pageCount + \" 页(\" + pageTexts.length + \" 个段落),长度: \" + combinedText.length);\n }\n } else {\n \/\/ 从找到的容器获取内容 - 保留段落结构!\n var containerParagraphs = [];\n \n \/\/ 遍历容器内的所有直接子元素或段落\n $contentElement.children().each(function() {\n var $child = $(this);\n var childTag = this.tagName.toLowerCase();\n \n \/\/ 只处理段落级元素:p, div, br等\n if (childTag === 'p' || childTag === 'div' || childTag === 'br') {\n var paraText = $child.text().trim();\n if(paraText.length > 5){\n containerParagraphs.push(paraText);\n }\n } else if (childTag === 'h2' || childTag === 'h3' || childTag === 'h4') {\n \/\/ 章节标题也保留\n var titleText = $child.text().trim();\n if(titleText){\n containerParagraphs.push(\"\\n\" + titleText + \"\\n\"); \/\/ 标题前后加空行\n }\n }\n });\n \n \/\/ 如果没有子元素,尝试按<br>分割\n if(containerParagraphs.length === 0){\n var rawHtml = $contentElement.html();\n \/\/ 用<br>或<\/p>分割成段落\n var parts = rawHtml.split(\/<br\\s*\\\/?>|<\\\/p>\/gi);\n for(var i=0; i<parts.length; i++){\n var partText = $('<div>').html(parts[i]).text().trim();\n if(partText.length > 5){\n containerParagraphs.push(partText);\n }\n }\n }\n \n if (containerParagraphs.length > 0) {\n var pageContent = containerParagraphs.join(\"\\n\\n\");\n allContent.push(pageContent);\n flutterBridge.log(\"获取第 \" + pageCount + \" 页(\" + containerParagraphs.length + \" 个段落),长度: \" + pageContent.length);\n }\n }\n\n \/\/ 查找下一页链接(已验证分页格式: _2.html, _3.html等)\n var nextPageUrl = null;\n \n $tempContainer.find(\"a\").each(function() {\n var text = $(this).text().trim();\n var href = $(this).attr('href');\n \n \/\/ 匹配\"下一页\"按钮\n if ((text === '下一页' || text === '下页' || text === '下一页(→)') && !nextPageUrl && href) {\n nextPageUrl = href;\n return false; \/\/ 找到后停止\n }\n });\n\n removeHTMLSafely($tempContainer);\n\n if (!nextPageUrl) {\n flutterBridge.log(\"没有更多页面,共 \" + pageCount + \" 页\");\n break;\n }\n\n \/\/ 构建完整URL并获取下一页\n if (nextPageUrl.startsWith('\/')) {\n var pathParts = url.split('\/');\n basePath = pathParts.slice(0, 3).join('\/');\n currentUrl = basePath + nextPageUrl;\n } else if (nextPageUrl.startsWith('http')) {\n currentUrl = nextPageUrl;\n } else {\n var lastSlashIndex = url.lastIndexOf('\/');\n currentUrl = url.substring(0, lastSlashIndex + 1) + nextPageUrl;\n }\n\n if (currentUrl === url && pageCount > 1) {\n flutterBridge.log(\"检测到重复URL,停止翻页\");\n break;\n }\n\n flutterBridge.log(\"正在获取第 \" + (pageCount + 1) + \" 页: \" + currentUrl);\n \n var nextResult = await http.Get(currentUrl, mheader, true);\n if(!nextResult || !nextResult.data){\n flutterBridge.log(\"获取第 \" + (pageCount + 1) + \" 页失败\");\n break;\n }\n htmlContent = nextResult.data;\n }\n\n \/\/ 合并所有页面内容\n var contenttxt = allContent.join(\"\\n\\n\");\n\n \/\/ 清理内容\n contenttxt = contenttxt.replace(\/\\n{3,}\/g, \"\\n\\n\");\n contenttxt = contenttxt.trim();\n\n \/\/ 限制最大长度\n if(contenttxt.length > 50000) {\n contenttxt = contenttxt.substring(0, 50000);\n }\n\n if(!contenttxt || contenttxt.length < 30){\n flutterBridge.showToast(\"获取内容失败\");\n return \"\";\n }\n\n return contenttxt;\n }\n\n \/*\n * getfinds() - 发现\/分类\n *\/\n async function getfinds() {\n var result = [];\n var push = (title, url, type) => result.push({\n title: title,\n url: url,\n type: type || 0\n });\n\n \/\/ ====== 基于网站真实数据的分类列表 ======\n \/\/ 数据来源:网站JS变量 EXPLORE_META(已验证)\n \/\/ URL格式: \/sort\/{分类ID}_{页码}\/\n\n \/\/ 分类列表(按ID顺序)\n push(\"玄幻\", \"\/sort\/1_{{page}}\/\", 0);\n push(\"奇幻\", \"\/sort\/2_{{page}}\/\", 0);\n push(\"武侠\", \"\/sort\/3_{{page}}\/\", 0);\n push(\"都市\", \"\/sort\/4_{{page}}\/\", 0);\n push(\"历史\", \"\/sort\/5_{{page}}\/\", 0);\n push(\"军事\", \"\/sort\/6_{{page}}\/\", 0);\n push(\"悬疑\", \"\/sort\/7_{{page}}\/\", 0);\n push(\"游戏\", \"\/sort\/8_{{page}}\/\", 0);\n push(\"科幻\", \"\/sort\/9_{{page}}\/\", 0);\n push(\"体育\", \"\/sort\/10_{{page}}\/\", 0);\n push(\"古言\", \"\/sort\/11_{{page}}\/\", 0);\n push(\"现言\", \"\/sort\/12_{{page}}\/\", 0);\n push(\"幻言\", \"\/sort\/13_{{page}}\/\", 0);\n push(\"仙侠\", \"\/sort\/14_{{page}}\/\", 0);\n push(\"青春\", \"\/sort\/15_{{page}}\/\", 0);\n push(\"穿越\", \"\/sort\/16_{{page}}\/\", 0);\n push(\"女生\", \"\/sort\/17_{{page}}\/\", 0);\n push(\"其他\", \"\/sort\/18_{{page}}\/\", 0);\n\n \/\/ 排行榜(已验证)\n push(\"🔥 点击榜\", \"\/allvisit\/\", 0); \/\/ 按点击量排行\n push(\"⭐ 推荐榜\", \"\/allvote\/\", 0); \/\/ 按推荐票数排行\n push(\"💖 收藏榜\", \"\/goodnum\/\", 0); \/\/ 按收藏数排行\n push(\"📚 新书入库\", \"\/postdate\/\", 0); \/\/ 按发布时间排行\n\n return JSON.stringify(result);\n }\n\n \/*\n * find(url, page) - 分类浏览\n *\/\n async function find(url, page) {\n var u = url.replaceAll(\"{{page}}\", page);\n if(!u.startsWith('http')){\n u = baseurl + u;\n }\n\n flutterBridge.log(\"分类URL: \" + u);\n\n var mheader={\n ...header,\n \"Referer\": baseurl\n };\n\n var htmlResult = await http.Get(u, mheader, true);\n\n if(!htmlResult || !htmlResult.data){\n flutterBridge.showToast(\"获取分类失败\");\n return \"[]\";\n }\n\n var htmlContent = htmlResult.data;\n\n flutterBridge.text(0, htmlContent);\n\n var books = [];\n var processedUrls = new Set();\n var $tempContainer = parseHTMLSafely(removeHTMLTags(htmlContent));\n\n \/\/ 已验证书籍链接格式: \/read\/{书籍ID}\/\n var bookLinkMatcher = function(href) {\n if (!href) return false;\n return \/^\\\/read\\\/\\d+\\\/$\/.test(href);\n };\n\n \/\/ ====== 基于浏览器100%验证过的真实DOM结构 ======\n \/\/ 真实结构(已通过浏览器验证):\n \/\/\n \/\/ <div id=\"alistbox\"> ← 每本书的容器(注意:是id不是class)\n \/\/ <div class=\"pic\"> ← 封面区域\n \/\/ <a href=\"\/read\/{书籍ID}\/\" title=\"《书名》全文阅读\">\n \/\/ <img src=\"https:\/\/img.xtyxsw.org\/{ID}\/{IMG}.jpg\" alt=\"书名\">\n \/\/ <\/a>\n \/\/ <\/div>\n \/\/ \n \/\/ <div class=\"info\"> ← 信息区域\n \/\/ <div class=\"title\"> ← 标题区\n \/\/ <h2>《<a href=\"\/read\/{ID}\/\">纯书名<\/a>》<\/h2>\n \/\/ <span>作者:<a href=\"\/author\/{作者}.html\">{作者}<\/a><\/span>\n \/\/ <\/div>\n \/\/ \n \/\/ <div class=\"sys\"> ← 最新章节\n \/\/ <li>最新更新:<a href=\"\/read\/{ID}\/{章ID}.html\">第{N}章 标题<\/a><\/li>\n \/\/ <\/div>\n \/\/ \n \/\/ <div class=\"intro\"> ← 简介\n \/\/ 简介文本内容...\n \/\/ <\/div>\n \/\/ \n \/\/ <div class=\"yuedu\"> ← 操作按钮\n \/\/ <a href=\"\/read\/{ID}\/\">开始阅读<\/a>\n \/\/ <a onclick=\"mark({ID})\">加入书架<\/a>\n \/\/ <\/div>\n \/\/ <\/div>\n \/\/ <\/div>\n\n \/\/ 查找所有书籍容器(使用id选择器 #alistbox)\n $tempContainer.find(\"#alistbox\").each(function() {\n try{\n var $item = $(this);\n \n \/\/ ========== 1. 书籍链接 ==========\n var bookUrl = $item.find(\"a[href*='\/read\/']\").first().attr('href');\n \n if (!bookUrl || !bookLinkMatcher(bookUrl) || processedUrls.has(bookUrl)) { \n return true; \n }\n \n processedUrls.add(bookUrl);\n \n \/\/ ========== 2. 书名(从.title h2 a提取 - 不含\"全文阅读\")==========\n var bookName = $item.find(\".title h2 a\").first().text().trim();\n \n \/\/ 清理书名号《》\n if(bookName){\n bookName = bookName.replace(\/[《》\"\"''【】\\(\\)]\/g, '').trim();\n }\n \n if(!bookName || bookName.length < 2) return true;\n \n \/\/ ========== 3. 作者(从.title span a提取 - 已验证格式)==========\n var author = $item.find(\".title span a\").first().text().trim();\n \n \/\/ ========== 4. 封面图(从.pic img提取)==========\n var coverUrl = $item.find(\".pic img\").first().attr('src') || \"\";\n \n \/\/ ========== 5. 简介(从.intro提取)==========\n var intro = $item.find(\".intro\").first().text().trim();\n if(intro && intro.length > 200) {\n intro = intro.substring(0, 200);\n }\n \n \/\/ ========== 6. 最新章节(从.sys li a提取 - 已验证格式)==========\n var latestChapterTitle = $item.find(\".sys li a\").first().text().trim();\n \n flutterBridge.log(\"✅ 提取书籍: \" + bookName + \" | 作者: \" + author + \" | 最新: \" + latestChapterTitle);\n \n bookUrl = normalizeUrl(bookUrl);\n\n var book={\n \"bookUrl\": bookUrl,\n \"name\": bookName,\n \"author\": author,\n \"kind\": \"\",\n \"coverUrl\": normalizeUrl(coverUrl),\n \"intro\": intro,\n \"tocUrl\": bookUrl,\n \"wordCount\": \"\",\n \"type\": 0,\n \"latestChapterTitle\": latestChapterTitle\n };\n books.push(book);\n \n }catch(e){\n flutterBridge.log(\"解析#alistbox出错: \" + e.message);\n }\n });\n\n \/\/ 备选方案:如果没找到#alistbox容器\n if(books.length === 0) {\n $tempContainer.find(\"a\").each(function() {\n try{\n var bookUrl = $(this).attr('href');\n \n if (!bookUrl || processedUrls.has(bookUrl)) { return true; }\n if (!bookLinkMatcher(bookUrl)) { return true; }\n \n processedUrls.add(bookUrl);\n \n var bookName = $(this).attr('title') || $(this).text().trim();\n \n if(bookName && bookName.length > 1){\n bookUrl = normalizeUrl(bookUrl);\n\n var book={\n \"bookUrl\": bookUrl,\n \"name\": bookName,\n \"author\": \"\",\n \"kind\": \"\",\n \"coverUrl\": \"\",\n \"intro\": \"\",\n \"tocUrl\": bookUrl,\n \"wordCount\": \"\",\n \"type\": 0,\n \"latestChapterTitle\": \"\"\n };\n books.push(book);\n }\n }catch(e){\n flutterBridge.log(\"解析书籍信息出错: \" + e.message);\n }\n });\n }\n\n removeHTMLSafely($tempContainer);\n flutterBridge.log(\"找到 \" + books.length + \" 本书\");\n return JSON.stringify(books);\n }\n\n async function getloginurl(){\n return baseurl;\n }\n\n<\/script>\n<\/html>",
"login": false,
"lastUpdateTime": "1778702914906"
}