得奇小说网

https://www.deqixs.co

ethereal-essence (13554) 16小时前 下载:309

小说 小说 免费 连载
得奇小说网,免费在线阅读小说
二维码导入(APP尚未完成该功能)
// @uuid        019e120f-2f99-79cb-b0b2-cfe87b90adf0
// @name        得奇小说网
// @version     1.1.0
// @author      Ethereal
// @url         https://www.deqixs.co
// @logo        https://www.deqixs.co/favicon.ico
// @type        novel
// @enabled     true
// @tags        小说,免费,连载
// @description 得奇小说网,免费在线阅读小说

var BASE = 'https://www.deqixs.co';

var UA_CHROME = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';

var BOOK_PAGE_HEADERS = {
    'User-Agent': UA_CHROME,
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': 'zh-CN,zh;q=0.9',
    'Accept-Encoding': 'gzip, deflate',
    'Referer': BASE + '/',
};

var LONG_GET_SECS = 90;
var LONG_POST_SECS = 90;

var BOOK_META_MEM_MS = 300000;
var bookMetaMem = {};
var BOOK_META_DISK_SCOPE = 'deqixs.co_meta';
var BOOK_META_DISK_MS = 86400000;

function cloneBookMeta(m) {
    if (!m) return null;
    return {
        name: m.name,
        author: m.author,
        coverUrl: m.coverUrl,
        intro: m.intro,
        kind: m.kind,
        lastChapter: m.lastChapter,
        tocUrl: m.tocUrl
    };
}

function bookMetaMemGet(aid) {
    if (!aid) return null;
    var key = 'k' + aid;
    var row = bookMetaMem[key];
    if (!row || !row.meta) return null;
    if (Date.now() - row.t > BOOK_META_MEM_MS) {
        delete bookMetaMem[key];
        return null;
    }
    legado.log('bookInfo meta RAM hit aid=' + aid);
    return cloneBookMeta(row.meta);
}

function bookMetaMemPut(aid, meta) {
    if (!aid || !meta || !meta.name) return;
    bookMetaMem['k' + aid] = { t: Date.now(), meta: cloneBookMeta(meta) };
}

function bookMetaDiskGet(aid) {
    if (!aid || typeof legado.config.read !== 'function') return null;
    var raw = legado.config.read(BOOK_META_DISK_SCOPE, 'm' + aid);
    if (!raw) return null;
    try {
        var row = JSON.parse(raw);
        if (!row || !row.name || typeof row.t !== 'number') return null;
        if (Date.now() - row.t > BOOK_META_DISK_MS) return null;
        legado.log('bookInfo meta disk hit aid=' + aid);
        return {
            name: row.name,
            author: row.author || '',
            coverUrl: row.coverUrl || '',
            intro: row.intro || '',
            kind: row.kind || '',
            lastChapter: row.lastChapter || ''
        };
    } catch (e) {
        return null;
    }
}

function bookMetaDiskPut(aid, meta) {
    if (!aid || !meta || !meta.name || typeof legado.config.write !== 'function') return;
    var row = {
        t: Date.now(),
        name: meta.name,
        author: meta.author || '',
        coverUrl: meta.coverUrl || '',
        intro: meta.intro || '',
        kind: meta.kind || '',
        lastChapter: meta.lastChapter || ''
    };
    try {
        legado.config.write(BOOK_META_DISK_SCOPE, 'm' + aid, JSON.stringify(row));
    } catch (e) {}
}

function sleepMs(ms) {
    if (typeof setTimeout !== 'function') {
        return Promise.resolve();
    }
    return new Promise(function(resolve) {
        setTimeout(resolve, ms);
    });
}

function isBookTocIndexUrl(url) {
    var p = String(url).replace(/^https?:\/\/[^/?#]+/i, '').split(/[?#]/)[0];
    return /\/books\/\d+\/$/.test(p);
}

async function httpGetBookPage(url, timeoutSecs, optHeaders) {
    var secs = timeoutSecs || LONG_GET_SECS;
    var hdr = optHeaders && typeof optHeaders === 'object' ? optHeaders : BOOK_PAGE_HEADERS;
    var lastErr = null;
    function noteErr(tag, e) {
        lastErr = e;
        legado.log(tag + ' ' + e);
    }
    // Harmony HttpClient often hard-caps ~10s; timeoutSecs may be ignored. Repeated legado.http.get
    // frequently succeeds on a later try (~8s) while the first hits the wall.
    var nativeGetAttempts = isBookTocIndexUrl(url) ? 5 : 3;
    var skipRequestFirst = isBookTocIndexUrl(url);
    if (!skipRequestFirst && typeof legado.http.request === 'function') {
        try {
            var r1 = await legado.http.request(url, {
                method: 'GET',
                headers: hdr,
                timeoutSecs: secs,
            });
            if (typeof r1 === 'string' && r1.length) {
                return r1;
            }
        } catch (e) {
            noteErr('http.request GET', e);
        }
    }
    var gi;
    for (gi = 0; gi < nativeGetAttempts; gi++) {
        try {
            var g1 = await legado.http.get(url, hdr);
            if (typeof g1 === 'string' && g1.length) {
                return g1;
            }
        } catch (eg) {
            noteErr('http.get try=' + (gi + 1), eg);
        }
        if (gi < nativeGetAttempts - 1) {
            await sleepMs(450);
        }
    }
    if (typeof fetch === 'function') {
        try {
            var resp = await fetch(url, {
                method: 'GET',
                headers: hdr,
                timeoutSecs: secs,
            });
            if (resp && typeof resp.text === 'function') {
                var ft = await resp.text();
                if (typeof ft === 'string' && ft.length) {
                    return ft;
                }
            }
        } catch (ef) {
            noteErr('fetch GET', ef);
        }
    }
    if (skipRequestFirst && typeof legado.http.request === 'function') {
        try {
            var r2 = await legado.http.request(url, {
                method: 'GET',
                headers: hdr,
                timeoutSecs: secs,
            });
            if (typeof r2 === 'string' && r2.length) {
                return r2;
            }
        } catch (e2) {
            noteErr('http.request GET(after toc retries)', e2);
        }
    }
    try {
        return await legado.http.get(url, hdr);
    } catch (e3) {
        noteErr('http.get final', e3);
        if (lastErr) {
            throw lastErr;
        }
        throw e3;
    }
}

async function httpPostBookPage(url, body, headers, timeoutSecs) {
    var secs = timeoutSecs || LONG_POST_SECS;
    if (typeof legado.http.request === 'function') {
        try {
            var p1 = await legado.http.request(url, {
                method: 'POST',
                body: body,
                headers: headers,
                timeoutSecs: secs,
            });
            if (typeof p1 === 'string' && p1.length) {
                return p1;
            }
        } catch (e) {
            legado.log('http.request POST ' + e);
        }
    }
    try {
        var po = await legado.http.post(url, body, headers);
        if (typeof po === 'string' && po.length) {
            return po;
        }
    } catch (ep) {
        legado.log('http.post ' + ep);
    }
    if (typeof fetch === 'function') {
        try {
            var hdr = {};
            if (headers && typeof headers === 'object') {
                for (var k in headers) {
                    if (Object.prototype.hasOwnProperty.call(headers, k) && k.toLowerCase() !== 'content-length') {
                        hdr[k] = headers[k];
                    }
                }
            }
            var resp = await fetch(url, {
                method: 'POST',
                headers: hdr,
                body: body,
                timeoutSecs: secs,
            });
            if (resp && typeof resp.text === 'function') {
                var t = await resp.text();
                if (typeof t === 'string') {
                    return t;
                }
            }
        } catch (ef) {
            legado.log('fetch POST ' + ef);
        }
    }
    return await legado.http.post(url, body, headers);
}

var AUTHOR_PREFIX = '\u4f5c\u8005\uff1a';
var SKIP_START_READ = '\u5f00\u59cb\u9605\u8bfb';
var SKIP_ADD_SHELF = '\u52a0\u5165\u4e66\u67b6';
var SKIP_RECOMMEND = '\u63a8\u8350\u672c\u4e66';
var SKIP_TXT_DL = 'TXT\u4e0b\u8f7d';

function _trim(s) {
    if (!s) return '';
    return s.replace(/^[\s\u3000\u00A0]+|[\s\u3000\u00A0]+$/g, '');
}

function coverUrlFromArticleId(articleId) {
    var n = parseInt(articleId, 10);
    if (!n || n !== n) return '';
    var seg = String(Math.floor(n / 1000));
    return BASE + '/files/article/image/' + seg + '/' + articleId + '/' + articleId + 's.jpg';
}

function articleIdFromBookUrl(url) {
    var m = url.match(/\/books\/(\d+)/);
    return m ? m[1] : '';
}

function canonicalTocUrl(bookUrl) {
    var aid = articleIdFromBookUrl(bookUrl);
    return aid ? (BASE + '/books/' + aid + '/') : bookUrl;
}

function isArticleinfoErrorPage(html) {
    return html && html.indexOf('\u51fa\u73b0\u9519\u8bef') >= 0;
}

function extractChapterLinksRegex(html, aid) {
    var aidNum = String(parseInt(aid, 10));
    if (!aidNum || aidNum === 'NaN') return [];
    var re = new RegExp('<a\\s+href="([^"]*\\/books\\/' + aidNum + '\\/(\\d+)\\.html)"[^>]*>([^<]*)<\\/a>', 'gi');
    var chapters = [];
    var seen = {};
    var m;
    while ((m = re.exec(html)) !== null) {
        var href = m[1];
        var url = href.indexOf('http') === 0 ? href : (BASE + href);
        var plain = url.split('?')[0].split('#')[0];
        if (!/\/books\/\d+\/\d+\.html$/i.test(plain)) {
            continue;
        }
        if (seen[url]) {
            continue;
        }
        var nameTrim = _trim(m[3]);
        if (!nameTrim) {
            continue;
        }
        if (nameTrim === SKIP_START_READ || nameTrim === SKIP_ADD_SHELF || nameTrim === SKIP_RECOMMEND || nameTrim === SKIP_TXT_DL) {
            continue;
        }
        seen[url] = true;
        chapters.push({ name: nameTrim, url: url });
    }
    chapters.sort(function(a, b) {
        var idA = parseInt(a.url.replace(/.*\/(\d+)\.html.*/, '$1'), 10);
        var idB = parseInt(b.url.replace(/.*\/(\d+)\.html.*/, '$1'), 10);
        return idA - idB;
    });
    return chapters;
}

function chapterListFromDom(html) {
    var doc = legado.dom.parse(html);
    var chapters = [];
    var seen = {};
    var links = legado.dom.selectAll(doc, 'dl.book.chapterlist a');
    for (var i = 0; i < links.length; i++) {
        var link = links[i];
        var url = legado.dom.attr(link, 'href');
        var name = legado.dom.text(link);
        if (!url || url.indexOf('javascript') === 0) {
            continue;
        }
        if (url.indexOf('http') !== 0) {
            url = BASE + url;
        }
        if (!/\/books\/\d+\/\d+\.html$/i.test(url.split('?')[0].split('#')[0])) {
            continue;
        }
        if (seen[url]) {
            continue;
        }
        var nameTrim = _trim(name);
        if (nameTrim === SKIP_START_READ || nameTrim === SKIP_ADD_SHELF || nameTrim === SKIP_RECOMMEND || nameTrim === SKIP_TXT_DL) {
            continue;
        }
        seen[url] = true;
        chapters.push({ name: nameTrim, url: url });
    }
    chapters.sort(function(a, b) {
        var idA = parseInt(a.url.replace(/.*\/(\d+)\.html.*/, '$1'), 10);
        var idB = parseInt(b.url.replace(/.*\/(\d+)\.html.*/, '$1'), 10);
        return idA - idB;
    });
    return chapters;
}

function parseBookInfoFromDoc(doc) {
    var title = legado.dom.selectAttr(doc, 'meta[property="og:title"]', 'content');
    if (!_trim(title)) {
        title = legado.dom.selectText(doc, 'h1.booktitle');
    }

    var author = legado.dom.selectAttr(doc, 'meta[property="og:novel:author"]', 'content');
    if (!_trim(author)) {
        var authorEl = legado.dom.select(doc, 'p.booktag a.red');
        var authorTitle = legado.dom.attr(authorEl, 'title');
        if (authorTitle && authorTitle.indexOf(AUTHOR_PREFIX) === 0) {
            author = authorTitle.substring(AUTHOR_PREFIX.length);
        } else {
            author = legado.dom.text(authorEl);
        }
    }

    var coverUrl = legado.dom.selectAttr(doc, 'meta[property="og:image"]', 'content');
    if (!_trim(coverUrl)) {
        coverUrl = legado.dom.attr(legado.dom.select(doc, 'img.thumbnail'), 'src');
    }

    var kind = legado.dom.selectAttr(doc, 'meta[property="og:novel:category"]', 'content');
    if (!_trim(kind)) {
        kind = legado.dom.selectText(doc, 'ol.breadcrumb li:nth-child(2) a');
    }

    var lastChapter = legado.dom.selectAttr(doc, 'meta[property="og:novel:latest_chapter_name"]', 'content');
    if (!_trim(lastChapter)) {
        lastChapter = legado.dom.text(legado.dom.select(doc, 'a.bookchapter'));
    }

    var introDoc = legado.dom.select(doc, 'p.bookintro');
    legado.dom.remove(introDoc, 'img');
    var intro = legado.dom.text(introDoc);

    return {
        name: _trim(title),
        author: _trim(author),
        coverUrl: _trim(coverUrl),
        intro: _trim(intro),
        kind: _trim(kind),
        lastChapter: _trim(lastChapter)
    };
}

function escapeRegExpMeta(str) {
    return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function metaPropertyContent(html, prop) {
    if (!html || !prop) return '';
    var esc = escapeRegExpMeta(prop);
    var re1 = new RegExp('<meta[^>]*property="' + esc + '"[^>]*content="([^"]*)"', 'i');
    var m = html.match(re1);
    if (m) return _trim(m[1]);
    var re2 = new RegExp('<meta[^>]*content="([^"]*)"[^>]*property="' + esc + '"', 'i');
    m = html.match(re2);
    return m ? _trim(m[1]) : '';
}

function decodeBasicEntities(s) {
    if (!s) return '';
    return s
        .replace(/&nbsp;/gi, ' ')
        .replace(/&emsp;/gi, '    ')
        .replace(/&lt;/g, '<')
        .replace(/&gt;/g, '>')
        .replace(/&amp;/g, '&')
        .replace(/&quot;/g, '"')
        .replace(/&#39;/g, "'");
}

function parseBookInfoFast(html) {
    if (!html) return null;
    var title = metaPropertyContent(html, 'og:title');
    if (!_trim(title)) {
        var h1m = html.match(/<h1[^>]*class="[^"]*booktitle[^"]*"[^>]*>([^<]*)</i);
        title = h1m ? h1m[1] : '';
    }
    var author = metaPropertyContent(html, 'og:novel:author');
    if (!_trim(author)) {
        var atm = html.match(/<a[^>]*class="[^"]*red[^"]*"[^>]*title="\u4f5c\u8005\uff1a([^"]*)"[^>]*>/i);
        author = atm ? atm[1] : '';
    }
    var coverUrl = metaPropertyContent(html, 'og:image');
    if (!_trim(coverUrl)) {
        var imgm = html.match(/<img[^>]*class="[^"]*thumbnail[^"]*"[^>]*src="([^"]+)"/i);
        if (!imgm) {
            imgm = html.match(/<img[^>]*src="([^"]+)"[^>]*class="[^"]*thumbnail[^"]*"/i);
        }
        if (imgm) {
            coverUrl = imgm[1];
        }
    }
    var kind = metaPropertyContent(html, 'og:novel:category');
    if (!_trim(kind)) {
        var bcm = html.match(/<ol[^>]*class="[^"]*breadcrumb[^"]*"[^>]*>([\s\S]*?)<\/ol>/i);
        if (bcm) {
            var inner = bcm[1];
            var la = [];
            var reA = /<a[^>]*>([^<]+)<\/a>/gi;
            var mm;
            while ((mm = reA.exec(inner)) !== null) {
                la.push(_trim(mm[1]));
            }
            if (la.length >= 2) {
                kind = la[1];
            }
        }
    }
    var lastChapter = metaPropertyContent(html, 'og:novel:latest_chapter_name');
    if (!_trim(lastChapter)) {
        var bcm2 = html.match(/<a[^>]*class="[^"]*bookchapter[^"]*"[^>]*title="([^"]*)"/i);
        if (bcm2) {
            lastChapter = bcm2[1];
        } else {
            var bcm3 = html.match(/<a[^>]*class="[^"]*bookchapter[^"]*"[^>]*>([^<]+)<\/a>/i);
            if (bcm3) {
                lastChapter = bcm3[1];
            }
        }
    }
    var intro = '';
    var introMatch = html.match(/<p[^>]*class="[^"]*bookintro[^"]*"[^>]*>([\s\S]*?)<\/p>/i);
    if (introMatch) {
        intro = introMatch[1].replace(/<img\b[^>]*>/gi, '').replace(/<[^>]+>/g, '');
        intro = decodeBasicEntities(intro);
    }
    return {
        name: _trim(title),
        author: _trim(author),
        coverUrl: _trim(coverUrl),
        intro: _trim(intro),
        kind: _trim(kind),
        lastChapter: _trim(lastChapter)
    };
}

async function search(keyword, page) {
    legado.log('searching: ' + keyword);
    var p = parseInt(page, 10);
    if (!p || p < 1) {
        p = 1;
    }
    var q =
        'searchkey=' +
        encodeURIComponent(keyword) +
        '&action=search&searchtype=articlename&page=' +
        encodeURIComponent(String(p));
    var searchUrl = BASE + '/modules/article/search.php?' + q;
    legado.log('search GET: ' + searchUrl);
    var html = '';
    try {
        html = await httpGetBookPage(searchUrl, LONG_GET_SECS);
    } catch (e) {
        legado.log('search GET err: ' + e);
        html = '';
    }
    if (!html || html.length < 80 || (html.indexOf('bookbox') < 0 && html.indexOf('og:novel:book_name') < 0)) {
        legado.log('search POST fallback');
        var body = 'searchkey=' + encodeURIComponent(keyword) + '&action=search&searchtype=articlename';
        if (p > 1) {
            body += '&page=' + encodeURIComponent(String(p));
        }
        var headers = {
            'User-Agent': UA_CHROME,
            'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
            'Content-Length': String(body.length),
            'Referer': BASE + '/',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Encoding': 'gzip, deflate',
        };
        try {
            html = await httpPostBookPage(BASE + '/modules/article/search.php', body, headers, LONG_POST_SECS);
        } catch (e2) {
            legado.log('search POST err: ' + e2);
            html = '';
        }
    }
    if (!html) {
        legado.log('search returned empty response');
        return [];
    }

    legado.log('search response length: ' + html.length);

    var doc = legado.dom.parse(html);

    var ogTitle = legado.dom.selectAttr(doc, 'meta[property="og:novel:book_name"]', 'content');
    var ogUrl = legado.dom.selectAttr(doc, 'meta[property="og:novel:read_url"]', 'content');
    var ogAuthor = legado.dom.selectAttr(doc, 'meta[property="og:novel:author"]', 'content');
    var ogCover = legado.dom.selectAttr(doc, 'meta[property="og:image"]', 'content');
    var ogLatest = legado.dom.selectAttr(doc, 'meta[property="og:novel:latest_chapter_name"]', 'content');
    var ogKind = legado.dom.selectAttr(doc, 'meta[property="og:novel:category"]', 'content');

    if (ogTitle && ogUrl) {
        legado.log('single result: ' + ogTitle);
        return [{
            name: _trim(ogTitle),
            author: _trim(ogAuthor),
            bookUrl: ogUrl,
            coverUrl: _trim(ogCover),
            kind: _trim(ogKind),
            lastChapter: _trim(ogLatest)
        }];
    }

    var items = legado.dom.selectAll(doc, 'div.bookbox');
    if (!items || items.length === 0) {
        legado.log('no results found');
        return [];
    }

    legado.log('found ' + items.length + ' results');
    var results = [];
    for (var i = 0; i < items.length; i++) {
        var item = items[i];
        var titleEl = legado.dom.select(item, 'h4.bookname a');
        var title = legado.dom.text(titleEl);
        var url = legado.dom.attr(titleEl, 'href');
        if (!title || !url) continue;
        if (url.indexOf('http') !== 0) {
            url = BASE + url;
        }

        var authorText = legado.dom.text(legado.dom.select(item, 'div.author'));
        var author = authorText.indexOf(AUTHOR_PREFIX) === 0 ? authorText.substring(AUTHOR_PREFIX.length) : authorText;

        var lastChapterEl = legado.dom.select(item, 'div.cat a');
        var lastChapter = legado.dom.text(lastChapterEl);

        var articleIdMatch = url.match(/\/books\/(\d+)\//);
        var articleId = articleIdMatch ? articleIdMatch[1] : '';
        var coverUrl = coverUrlFromArticleId(articleId);

        results.push({
            name: _trim(title),
            author: _trim(author),
            bookUrl: url,
            coverUrl: coverUrl,
            kind: '',
            lastChapter: _trim(lastChapter)
        });
    }
    return results;
}

async function bookInfo(bookUrl) {
    legado.log('bookInfo: ' + bookUrl);
    var aid = articleIdFromBookUrl(bookUrl);
    var tocUrlCanonical = canonicalTocUrl(bookUrl);
    var html = '';
    var meta = null;

    if (aid) {
        var cached = bookMetaMemGet(aid);
        if (cached) {
            cached.tocUrl = tocUrlCanonical;
            return cached;
        }
        var diskMeta = bookMetaDiskGet(aid);
        if (diskMeta) {
            diskMeta.tocUrl = tocUrlCanonical;
            bookMetaMemPut(aid, diskMeta);
            return diskMeta;
        }
        var liteUrl = BASE + '/modules/article/articleinfo.php?id=' + encodeURIComponent(aid);
        legado.log('bookInfo lite: ' + liteUrl);
        try {
            html = await httpGetBookPage(liteUrl, 45);
        } catch (e) {
            legado.log('bookInfo lite err: ' + e);
            html = '';
        }
        if (html && isArticleinfoErrorPage(html)) {
            html = '';
        }
        if (html && html.length > 200) {
            meta = parseBookInfoFast(html);
            if (!meta || !meta.name) {
                legado.log('bookInfo lite dom fallback');
                try {
                    var docLite = legado.dom.parse(html);
                    meta = parseBookInfoFromDoc(docLite);
                } catch (e2) {
                    legado.log('bookInfo lite dom err: ' + e2);
                }
            }
            if (meta && meta.name) {
                meta.tocUrl = tocUrlCanonical;
                bookMetaMemPut(aid, meta);
                bookMetaDiskPut(aid, meta);
                legado.log('bookInfo from lite ok');
                return meta;
            }
        }
    }

    legado.log('bookInfo full toc: ' + tocUrlCanonical);
    try {
        html = await httpGetBookPage(tocUrlCanonical, LONG_GET_SECS);
    } catch (e3) {
        legado.log('bookInfo full toc err: ' + e3);
        html = '';
    }
    if (!aid) {
        try {
            html = await httpGetBookPage(bookUrl, LONG_GET_SECS);
        } catch (e4) {
            legado.log('bookInfo bookUrl err: ' + e4);
            html = '';
        }
    }
    if (!html) {
        legado.log('bookInfo returned empty');
        return null;
    }

    meta = parseBookInfoFast(html);
    if (!meta || !meta.name) {
        legado.log('bookInfo full regex fallback dom');
        var doc = legado.dom.parse(html);
        meta = parseBookInfoFromDoc(doc);
    }

    if (!meta || !meta.name) {
        legado.log('bookInfo missing title');
        return null;
    }

    meta.tocUrl = tocUrlCanonical;
    if (aid) {
        bookMetaMemPut(aid, meta);
        bookMetaDiskPut(aid, meta);
    }
    return meta;
}

async function chapterList(tocUrl) {
    legado.log('chapterList: ' + tocUrl);
    var aid = articleIdFromBookUrl(tocUrl);
    var html = '';
    var chapters = [];
    var maxAttempts = 3;
    for (var attempt = 1; attempt <= maxAttempts; attempt++) {
        try {
            html = await httpGetBookPage(tocUrl, LONG_GET_SECS);
        } catch (e) {
            legado.log('chapterList GET err try=' + attempt + ' ' + e);
            html = '';
        }
        if (html && html.length > 800) {
            chapters = aid ? extractChapterLinksRegex(html, aid) : [];
            if (!chapters.length) {
                legado.log('chapterList regex empty, dom fallback');
                chapters = chapterListFromDom(html);
            }
            if (chapters.length) {
                legado.log('chapterList: ' + chapters.length + ' chapters (try ' + attempt + ')');
                return chapters;
            }
        }
        if (attempt < maxAttempts) {
            legado.log('chapterList retry after empty/slow toc (try ' + attempt + '/' + maxAttempts + ')');
            await sleepMs(400);
        }
    }
    legado.log('chapterList returned empty after ' + maxAttempts + ' tries');
    return [];
}

function extractChapterHeadingsFromPage(pageHtml) {
    var list = [];
    if (!pageHtml) return list;
    var m = pageHtml.match(/<li[^>]*class="[^"]*active[^"]*"[^>]*>([^<]+)<\/li>/i);
    if (m && m[1]) {
        list.push(_trim(m[1]));
    }
    m = pageHtml.match(/<h1[^>]*class="[^"]*pt10[^"]*"[^>]*>\s*([^<]+)/i);
    if (m && m[1]) {
        var h1t = _trim(m[1]);
        h1t = h1t.replace(/\s*\(\u7b2c\/\u9875\)\s*$/, '');
        list.push(h1t);
    }
    m = pageHtml.match(/<title>([^<]+)<\/title>/i);
    if (m && m[1]) {
        var parts = m[1].split('_');
        if (parts.length >= 3) {
            list.push(_trim(parts[parts.length - 2]));
        }
    }
    var seen = {};
    var out = [];
    for (var i = 0; i < list.length; i++) {
        var s = list[i];
        if (!s || s.length < 2 || seen[s]) {
            continue;
        }
        seen[s] = true;
        out.push(s);
    }
    return out;
}

function stripDuplicateChapterHeadings(plain, headings) {
    if (!plain || !headings || !headings.length) {
        return plain;
    }
    var c = plain;
    var guard = 0;
    while (guard < 12) {
        guard++;
        var t = _trim(c);
        if (!t) {
            break;
        }
        var changed = false;
        for (var h = 0; h < headings.length; h++) {
            var ph = headings[h];
            if (!ph) {
                continue;
            }
            if (t.indexOf(ph) === 0) {
                c = c.substring(c.indexOf(ph) + ph.length);
                c = c.replace(/^[\s\u3000\u00A0\n\r]+/, '');
                changed = true;
                break;
            }
            var nl = t.indexOf('\n');
            var line0 = nl < 0 ? t : t.substring(0, nl);
            line0 = _trim(line0);
            if (line0 === ph) {
                c = nl < 0 ? '' : t.substring(nl + 1);
                changed = true;
                break;
            }
        }
        if (!changed) {
            break;
        }
    }
    return c;
}

async function chapterContent(chapterUrl) {
    legado.log('chapterContent: ' + chapterUrl);

    var articleId = '';
    var chapterId = '';
    var parts = chapterUrl.split('/');
    for (var i = 0; i < parts.length; i++) {
        if (parts[i] === 'books' && i + 1 < parts.length) {
            articleId = parts[i + 1];
        }
    }
    var cidMatch = chapterUrl.match(/\/(\d+)\.html/);
    if (cidMatch) {
        chapterId = cidMatch[1];
    }

    if (!articleId || !chapterId) {
        legado.log('Cannot extract IDs from URL: ' + chapterUrl);
        return '';
    }

    legado.log('articleId=' + articleId + ' chapterId=' + chapterId);

    var headers = {
        'User-Agent': UA_CHROME,
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        'Referer': BASE + '/',
    };

    var pageHtml = await httpGetBookPage(chapterUrl, LONG_GET_SECS, headers);
    if (!pageHtml) {
        legado.log('chapter page returned empty');
        return '';
    }
    legado.log('chapter page loaded, length: ' + pageHtml.length);

    var tokenUrl = BASE + '/scripts/chapter.js.php?aid=' + articleId + '&cid=' + chapterId + '&referrer=' + encodeURIComponent(chapterUrl);
    var tokenHeaders = {
        'User-Agent': UA_CHROME,
        'Referer': chapterUrl,
        'Accept': '*/*',
    };
    var tokenHtml = await httpGetBookPage(tokenUrl, 45, tokenHeaders);

    if (!tokenHtml) {
        legado.log('Token request returned empty');
        return '';
    }

    legado.log('token response: ' + tokenHtml.substring(0, 150));

    var tokenMatch = tokenHtml.match(/var chapterToken = '([^']+)'/);
    var tsMatch = tokenHtml.match(/var timestamp = (\d+)/);
    var nonceMatch = tokenHtml.match(/var nonce = '([^']+)'/);

    if (!tokenMatch || !tsMatch || !nonceMatch) {
        legado.log('Token parse failed');
        return '';
    }

    var token = tokenMatch[1];
    var timestamp = tsMatch[1];
    var nonce = nonceMatch[1];

    legado.log('token=' + token + ' ts=' + timestamp + ' nonce=' + nonce);

    var apiUrl = BASE + '/modules/article/ajax2.php?aid=' + articleId + '&cid=' + chapterId + '&token=' + token + '&timestamp=' + timestamp + '&nonce=' + nonce;
    var apiHeaders = {
        'User-Agent': UA_CHROME,
        'Referer': chapterUrl,
        'X-Requested-With': 'XMLHttpRequest',
        'Accept': 'application/json, text/javascript, */*; q=0.01',
    };
    var apiResponse = await httpGetBookPage(apiUrl, 45, apiHeaders);

    legado.log('API response: ' + apiResponse.substring(0, 200));

    var data;
    try {
        data = JSON.parse(apiResponse);
    } catch (e) {
        legado.log('JSON parse error: ' + e);
        return '';
    }

    if (!data || data.status !== 1 || !data.data || !data.data.content) {
        legado.log('chapter API status not success');
        return '';
    }

    var content = data.data.content;
    content = content.replace(/^\s*<h[1-4][^>]*>[^<]*<\/h[1-4]>\s*(<br\s*\/?>\s*)*/i, '');
    content = content.replace(/^\s*<p[^>]*>\s*<strong>[^<]{1,200}<\/strong>\s*<\/p>\s*(<br\s*\/?>\s*)*/i, '');
    content = content.replace(/<br\s*\/?>/gi, '\n');
    content = content.replace(/<[^>]+>/g, '');
    content = content.replace(/&emsp;/g, '    ');
    content = content.replace(/&nbsp;/g, ' ');
    content = content.replace(/&amp;/g, '&');
    content = content.replace(/&lt;/g, '<');
    content = content.replace(/&gt;/g, '>');
    content = content.replace(/&quot;/g, '"');
    content = content.replace(/&#39;/g, "'");
    content = content.replace(/\n{3,}/g, '\n\n');
    content = content.replace(/[\u200B\u200C\u200D\uFEFF]/g, '');
    content = _trim(content);
    var heads = extractChapterHeadingsFromPage(pageHtml);
    content = stripDuplicateChapterHeadings(content, heads);
    content = _trim(content);
    legado.log('content length: ' + content.length);
    return content;
}

async function explore(page, category) {
    legado.log('explore: page=' + page + ' category=' + category);

    if (category === 'GETALL' || !category) {
        return [
            '\u5168\u90e8',
            '\u7384\u5e7b',
            '\u90fd\u5e02',
            '\u4ed9\u4fa0',
            '\u5386\u53f2',
            '\u79d1\u5e7b',
            '\u8bf8\u5929',
            '\u60ac\u7591',
            '\u4f53\u80b2',
            '\u6e38\u620f',
            '\u7efc\u5408'
        ];
    }

    var categoryMap = {
        '\u5168\u90e8': '0',
        '\u7384\u5e7b': '1',
        '\u90fd\u5e02': '2',
        '\u4ed9\u4fa0': '3',
        '\u5386\u53f2': '4',
        '\u79d1\u5e7b': '5',
        '\u8bf8\u5929': '6',
        '\u60ac\u7591': '7',
        '\u4f53\u80b2': '8',
        '\u6e38\u620f': '9',
        '\u7efc\u5408': '10'
    };

    var sortId = categoryMap[category];
    if (!sortId) {
        legado.log('unknown category: ' + category);
        return [];
    }

    var url = BASE + '/sort/' + sortId + '/' + page + '.html';
    legado.log('explore url: ' + url);

    var html = await httpGetBookPage(url, LONG_GET_SECS);
    if (!html) {
        legado.log('explore returned empty');
        return [];
    }

    var doc = legado.dom.parse(html);
    var items = legado.dom.selectAll(doc, 'div.bookbox');
    if (!items || items.length === 0) {
        legado.log('no books found');
        return [];
    }

    var results = [];
    for (var i = 0; i < items.length; i++) {
        var item = items[i];
        var titleEl = legado.dom.select(item, 'h4.bookname a');
        var title = legado.dom.text(titleEl);
        var bUrl = legado.dom.attr(titleEl, 'href');
        if (!title || !bUrl) continue;
        if (bUrl.indexOf('http') !== 0) {
            bUrl = BASE + bUrl;
        }

        var author = '';
        var authorEls = legado.dom.selectAll(item, 'div.author');
        for (var j = 0; j < authorEls.length; j++) {
            var at = legado.dom.text(authorEls[j]);
            if (at.indexOf(AUTHOR_PREFIX) === 0) {
                author = at.substring(AUTHOR_PREFIX.length);
                break;
            }
        }

        var lastChapterEl = legado.dom.select(item, 'div.cat a');
        var lastChapter = legado.dom.text(lastChapterEl);

        var articleIdMatch = bUrl.match(/\/books\/(\d+)\//);
        var articleId = articleIdMatch ? articleIdMatch[1] : '';
        var coverUrl = coverUrlFromArticleId(articleId);

        results.push({
            name: _trim(title),
            author: _trim(author),
            bookUrl: bUrl,
            coverUrl: coverUrl,
            kind: category,
            lastChapter: _trim(lastChapter)
        });
    }

    legado.log('explore results: ' + results.length);
    return results;
}

async function TEST(type) {
    if (type === '__list__') return ['search', 'explore', 'bookInfo', 'chapterList', 'chapterContent'];
    if (type === 'search') {
        var r = await search('\u6597\u7834', 1);
        if (!r || r.length < 1) return { passed: false, message: 'search empty' };
        return { passed: true, message: 'search count=' + r.length };
    }
    if (type === 'explore') {
        var b = await explore(1, '\u7384\u5e7b');
        if (!b || b.length < 1) return { passed: false, message: 'explore empty' };
        return { passed: true, message: 'explore count=' + b.length };
    }
    if (type === 'bookInfo') {
        var r = await bookInfo('https://www.deqixs.co/books/3305/');
        return { passed: !!(r && r.name), message: 'bookInfo name=' + (r ? r.name : '') };
    }
    if (type === 'chapterList') {
        var r = await chapterList('https://www.deqixs.co/books/3305/');
        return { passed: r.length > 0, message: 'chapterList cnt=' + r.length + ' first=' + (r[0] ? r[0].name : '') };
    }
    if (type === 'chapterContent') {
        var r = await chapterContent('https://www.deqixs.co/books/3305/2905167.html');
        return { passed: r.length > 100, message: 'chapterContent len=' + r.length };
    }
    return { passed: false, message: 'unknown type ' + type };
}
广告