[{"bookSourceName":"❄️听雪聚合","bookSourceType":0,"bookSourceUrl":"听雪聚合","customButton":true,"customOrder":0,"enabled":true,"enabledCookieJar":false,"enabledExplore":true,"eventListener":true,"exploreUrl":"@js:\nvar conf = ensureConfig.call(this);\nvar currentSource = conf.source || '';\nvar currentGender = conf.gender || 'boy';\n\nvar srcList = getSourceList.call(this);\nvar srcMeta = {};\nfor (var i = 0; i < srcList.length; i++) {\n    srcMeta[srcList[i].name] = srcList[i];\n}\n\nvar currentMode = conf.mode || 'novel';\nvar firstSource = String(currentSource || '').split(',')[0].trim();\nif (firstSource && srcMeta[firstSource] && srcMeta[firstSource].tab) {\n    currentMode = srcMeta[firstSource].tab;\n}\n\nvar MODE_LABELS = {\n    novel: '📖小说',\n    audio: '🎧听书',\n    comic: '🌈漫画',\n    video: '▶️短剧',\n    download: '⬇️下载'\n};\nvar LABEL_TO_MODE = {\n    '📖小说': 'novel',\n    '🎧听书': 'audio',\n    '🌈漫画': 'comic',\n    '▶️短剧': 'video',\n    '⬇️下载': 'download'\n};\n\nvar srcOptions = [{ label: '☁️聚合搜索', value: '' }];\nfor (var i = 0; i < srcList.length; i++) {\n    var it = srcList[i];\n    if (it.tab && it.tab != currentMode) continue;\n    srcOptions.push({ label: it.name, value: it.name });\n}\n\nvar defaultLabel = '☁️聚合搜索';\nfor (var k = 0; k < srcOptions.length; k++) {\n    if (srcOptions[k].value == firstSource) { defaultLabel = srcOptions[k].label; break; }\n}\nvar srcMapStr = JSON.stringify(srcOptions).replace(\/\\\\\/g,'\\\\\\\\').replace(\/\"\/g,'\\\\\"');\n\nvar firstModeSource = '';\nfor (var i = 0; i < srcList.length; i++) {\n    if (srcList[i].tab == currentMode && srcList[i].name) { firstModeSource = srcList[i].name; break; }\n}\nvar useSource = firstSource || firstModeSource;\n\nvar controls = [];\ncontrols.push({ title: '☃关键词💭：书名\/作者', type: 'text', style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.6 } });\ncontrols.push({ title: '🔍搜索', type: 'button', action: \"java.searchBook(infoMap['☃关键词💭：书名\/作者'] || '')\", style: { layout_flexGrow: 1, layout_flexBasisPercent: -1, layout_alignSelf: 'baseline' } });\ncontrols.push({ title: '🔄刷新', type: 'button', action: 'java.refreshExplore()', style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.4, layout_alignSelf: 'baseline' } });\ncontrols.push({ title: '⚙登录', type: 'button', action: \"java.open('login','http',null)\", style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.4, layout_alignSelf: 'baseline' } });\n\ncontrols.push({\n    title: '性别',\n    type: 'select',\n    chars: ['♂️男频', '♀️女频'],\n    default: currentGender == 'girl' ? '♀️女频' : '♂️男频',\n    action: \"(function(){var raw=infoMap['性别']||'';var v=(raw.indexOf('女')>=0)?'girl':'boy';setConfig.call(this,{gender:v});java.refreshExplore();})()\",\n    style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.4 }\n});\n\ncontrols.push({\n    title: '模式',\n    type: 'select',\n    chars: ['📖小说', '🎧听书', '🌈漫画', '▶️短剧', '⬇️下载'],\n    default: MODE_LABELS[currentMode] || '📖小说',\n    action: \"(function(){var raw=infoMap['模式']||'';var map=JSON.parse('\" + JSON.stringify(LABEL_TO_MODE).replace(\/\\\\\/g,'\\\\\\\\').replace(\/\"\/g,'\\\\\"') + \"');var v=map[raw]||'novel';setConfig.call(this,{mode:v,source:''});java.refreshExplore();})()\",\n    style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.4 }\n});\n\ncontrols.push({\n    title: '源站',\n    type: 'select',\n    chars: srcOptions.map(function(x){return x.label;}),\n    default: defaultLabel,\n    action: \"(function(){var raw=infoMap['源站']||'';var opts=JSON.parse('\" + srcMapStr + \"');var map={};opts.forEach(function(i){map[i.label]=i.value;});setConfig.call(this,{source:map[raw]||''});java.refreshExplore();})()\",\n    style: { layout_flexGrow: 1, layout_flexBasisPercent: 1 }\n});\n\nvar currentJjTab = getConfig.call(this).jj_tab || '多元';\nif (String(useSource).indexOf('晋江') >= 0) {\n    var jjTabs = [\"🏷 言情\", \"🏷 纯爱\", \"🏷 百合\", \"🏷 无Cp\", \"🏷 多元\"];\n    var jjTagMap = {\"🏷 言情\":\"言情\",\"🏷 纯爱\":\"纯爱\",\"🏷 百合\":\"百合\",\"🏷 无Cp\":\"无Cp\",\"🏷 多元\":\"多元\"};\n    var jjTagMapStr = JSON.stringify(jjTagMap).replace(\/\\\\\/g,'\\\\\\\\').replace(\/\"\/g,'\\\\\"');\n\n    controls.push({\n        title: '频道',\n        type: 'select',\n        chars: jjTabs,\n        default: \"🏷 \" + currentJjTab,\n        action: \"(function(){var raw=infoMap['频道']||'';var tagMap=JSON.parse('\" + jjTagMapStr + \"');var tagName=tagMap[raw]||'多元';setConfig.call(this,{jj_tab:tagName});java.refreshExplore();})()\",\n        style: { layout_flexGrow: 1, layout_flexBasisPercent: 1 }\n    });\n}\n\nvar sort = controls.slice();\n\nvar shelfEntries = [];\nvar shelfFirstSource = String(firstSource || '').trim();\nvar showFanqieShelf = shelfFirstSource == '' || shelfFirstSource.indexOf('番茄') >= 0;\nvar showJjShelf = shelfFirstSource.indexOf('晋江') >= 0;\n\nfunction shelfUrl(sourceName, loginValue, groupValue) {\n    return getHost.call(this) + '\/api\/bookshelf,' + JSON.stringify({\n        method: 'POST',\n        body: JSON.stringify({\n            source: sourceName,\n            logins: String(loginValue || ''),\n            group: String(groupValue || '')\n        }),\n        headers: buildHeaders.call(this)\n    });\n}\n\nfunction makeShelfBookList(sourceName, loginValue) {\n    try {\n        var shelfRes = postJson.call(this, '\/api\/bookshelf', {\n            source: sourceName,\n            logins: String(loginValue || ''),\n            group: ''\n        });\n        if (shelfRes && shelfRes.code == 0 && Array.isArray(shelfRes.data)) {\n            return shelfRes.data;\n        }\n    } catch (e) {}\n    return [];\n}\n\nfunction shelfGroups(books) {\n    var seen = {};\n    var groups = [];\n    for (var i = 0; i < books.length; i++) {\n        var gid = String((books[i] && books[i].group_id) || '').trim();\n        var gname = String((books[i] && books[i].group_name) || '').trim();\n        if (!gid || !gname || seen[gid]) continue;\n        seen[gid] = true;\n        groups.push({ id: gid, name: gname });\n    }\n    return groups;\n}\n\nfunction pushShelfDivider() {\n    shelfEntries.push({\n        title: '────────',\n        url: '',\n        style: { layout_flexGrow: 1, layout_flexBasisPercent: 1 }\n    });\n}\n\nvar fqSession = getSessionId.call(this) || getSessionIdFromCookie.call(this);\nif (showFanqieShelf && fqSession) {\n    var fqBooks = makeShelfBookList.call(this, '番茄', fqSession);\n    var fqGroups = shelfGroups(fqBooks);\n    var fqName = '';\n    try {\n        var userRaw = java.ajax('https:\/\/fanqienovel.com\/api\/user\/info\/v2,' + JSON.stringify({\n            method: 'GET',\n            headers: { Cookie: 'sessionid=' + fqSession }\n        }));\n        var userObj = JSON.parse(userRaw);\n        fqName = String(userObj && userObj.data && userObj.data.name || '');\n    } catch (e) {}\n\n    shelfEntries.push({\n        title: (fqName ? fqName + ' 的个人中心' : '我的番茄书架'),\n        url: '',\n        style: { layout_flexGrow: 1, layout_flexBasisPercent: 1 }\n    });\n    shelfEntries.push({\n        title: '我的书架（全部）',\n        url: shelfUrl.call(this, '番茄', fqSession, ''),\n        style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.5 }\n    });\n    for (var fg = 0; fg < fqGroups.length; fg++) {\n        shelfEntries.push({\n            title: '📁 ' + fqGroups[fg].name,\n            url: shelfUrl.call(this, '番茄', fqSession, fqGroups[fg].id),\n            style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.4 }\n        });\n    }\n\n    pushShelfDivider();\n}\n\nvar jjLogin = getJjToken.call(this);\nif (showJjShelf && jjLogin) {\n    var jjBooks = makeShelfBookList.call(this, '晋江', jjLogin);\n    var jjGroups = shelfGroups(jjBooks);\n\n    shelfEntries.push({\n        title: '📁 个人书架',\n        url: '',\n        style: { layout_flexGrow: 1, layout_flexBasisPercent: 1 }\n    });\n    shelfEntries.push({\n        title: '📓 全部收藏',\n        url: shelfUrl.call(this, '晋江', jjLogin, ''),\n        style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.45 }\n    });\n\n    var jjUncategorized = null;\n    var jjOtherGroups = [];\n    for (var jg = 0; jg < jjGroups.length; jg++) {\n        if (jjGroups[jg].name == '尚未分类') {\n            jjUncategorized = jjGroups[jg];\n        } else {\n            jjOtherGroups.push(jjGroups[jg]);\n        }\n    }\n\n    if (jjUncategorized) {\n        shelfEntries.push({\n            title: '📁 ' + jjUncategorized.name,\n            url: shelfUrl.call(this, '晋江', jjLogin, jjUncategorized.id),\n            style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.29 }\n        });\n    }\n\n    for (var jg2 = 0; jg2 < jjOtherGroups.length; jg2++) {\n        shelfEntries.push({\n            title: '📁 ' + jjOtherGroups[jg2].name,\n            url: shelfUrl.call(this, '晋江', jjLogin, jjOtherGroups[jg2].id),\n            style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.29 }\n        });\n    }\n\n    pushShelfDivider();\n}\n\nif (shelfEntries.length) {\n    sort = sort.concat(shelfEntries);\n}\n\nvar useGender = (currentMode == 'audio') ? 'boy' : currentGender;\nvar fqFilter = conf.fq_filter || '';\n\ntry {\n    var d = postJson.call(this, '\/api\/discover', {\n        source: useSource,\n        gender: useGender,\n        tab: currentJjTab,\n        selected_items: fqFilter\n    });\n    if (d && d.code != 0) {\n        java.toast(d.message);\n    }\n    var data = (d && Array.isArray(d.data)) ? d.data : [];\n    if (data.length > 0) {\n        data.forEach(function (t) {\n            if (!t) return;\n            var st = (t.style && typeof t.style === 'object' && !Array.isArray(t.style)) ? t.style : { layout_flexGrow: 1, layout_flexBasisPercent: 0.3 };\n            var link = t.sing\n                ? (getHost.call(this) + '\/api\/discover\/books,' + JSON.stringify({\n                    method: 'POST',\n                    body: JSON.stringify({\n                        sing: t.sing,\n                        gender: useGender,\n                        source: useSource,\n                        tab: currentJjTab || '',\n                        page: '{{page}}',\n                        selected_items: fqFilter\n                    })\n                }))\n                : '';\n            sort.push({ title: String(t.title || ''), url: link, style: st });\n        });\n    } else {\n        sort.push({ title: '【' + (useSource || '聚合') + '】暂无发现页数据', url: '', style: { layout_flexGrow: 1, layout_flexBasisPercent: 1 } });\n    }\n} catch (e) {\n    sort.push({ title: '发现页加载失败，请检查服务器\/登录', url: '', style: { layout_flexGrow: 1, layout_flexBasisPercent: 1 } });\n}\nJSON.stringify(sort);","header":"@js:\nJSON.stringify(buildHeaders.call(this));","jsLib":"let localVersion = '1.20261110.1';  \/\/本地版本号\n\nfunction getHosts() {\n    return [\n    'https:\/\/tingx.abrdns.com',\n    'http:\/\/154.12.85.126:5000'\n    ];\n}\n\nfunction getStvHosts() {\n    return [\n        'https:\/\/sangtacviet.com',\n        'https:\/\/sangtacviet.app',\n        'https:\/\/sangtacviet.vip',\n        'https:\/\/sangtacviet.pro',\n        'https:\/\/sangtacviet.xyz',\n        'http:\/\/14.225.254.182',\n        'http:\/\/103.82.20.93'\n    ];\n}\n\nfunction getStvHost() {\n    let c = getConfig.call(this);\n    let saved = String(c.stv_host || '').trim();\n    if (saved) return saved.replace(\/\\\/+$\/, '');\n    return 'https:\/\/sangtacviet.com';\n}\n\nfunction setStvHost(host) {\n    let list = getStvHosts();\n    let target = String(host || '').replace(\/\\\/+$\/, '');\n    if (list.indexOf(target) == -1) {\n        return { ok: false, message: '未配置的STV线路：' + host };\n    }\n    setConfig.call(this, { stv_host: target });\n    return { ok: true, host: target };\n}\n\nfunction stvNextHost() {\n    let list = getStvHosts();\n    let cur = getStvHost.call(this);\n    let idx = list.indexOf(cur);\n    let next = list[(idx + 1) % list.length];\n    setConfig.call(this, { stv_host: next });\n    return next;\n}\n\nfunction stvCurrentHostLabel() {\n    let cur = getStvHost.call(this);\n    let list = getStvHosts();\n    let idx = list.indexOf(cur);\n    return (idx >= 0 ? (idx + 1) + '\/' + list.length + ' · ' : '') + cur;\n}\n\nfunction getHost() {\n    let { source } = this;\n    let c = getConfig.call(this);\n    if (c.host) return String(c.host).replace(\/\\\/+$\/, '');\n    let base = '';\n    try { base = \"https:\/\/tingx.abrdns.com\"; } catch (e) {}\n    if (!base && typeof baseUrl !== 'undefined' && baseUrl) base = String(baseUrl);\n    if (!base) base = getHosts()[0];\n    return base.replace(\/\\\/+$\/, '');\n}\n\nfunction getConfig() {\n    let { source } = this;\n    try {\n        let configStr = source.getVariable();\n        if (!configStr || configStr.trim() == \"\") return {};\n        let config = JSON.parse(configStr);\n        if (Array.isArray(config)) return config[0] || {};\n        if (config && typeof config == 'object') return config;\n        return {};\n    } catch (e) { return {}; }\n}\n\nfunction setConfig(obj) {\n    let { source } = this;\n    try {\n        let config = getConfig.call(this);\n        for (let k in obj) { if (obj.hasOwnProperty(k)) config[k] = obj[k]; }\n        source.setVariable(JSON.stringify([config]));\n        return true;\n    } catch (e) { return false; }\n}\n\nfunction ensureConfig() {\n    let c = getConfig.call(this);\n    let changed = false;\n    if (!c.gender) { c.gender = 'boy'; changed = true; }\n    if (c.source == undefined) { c.source = ''; changed = true; }\n    if (!c.sources) { c.sources = { novel: [], audio: [], comic: [], video: [], download: [] }; changed = true; }\n    if (changed) setConfig.call(this, c);\n    return c;\n}\n\nfunction deviceType() {\n    let { java } = this;\n    try {\n        var id = java.deviceID();\n        if (id && id !== '') {\n            return true;\n        }\n    } catch (e) {}\n    \n    try {\n        var id = java.androidId();\n        if (id && id !== '') {\n            return false;\n        }\n    } catch (e) {}\n    \n    return false;\n}\n\nfunction getDeviceId() {\n    let { java } = this;\n    let dev = '';\n    try { let id1 = java.deviceID(); if (id1 && String(id1) !== '') dev = String(id1); } catch (e) {}\n    if (!dev) { try { let id2 = java.androidId(); if (id2 && String(id2) !== '') dev = String(id2); } catch (e) {} }\n    return dev;\n}\n\nfunction checkMsg(res) {\n    let { java } = this;\n    try {\n        let obj = JSON.parse(res);\n        if (obj.code !== 0) {\n            java.toast(obj.message);\n        }\n    } catch (e) {}\n    return res;\n}\n\nfunction getToken() {\n    let { source } = this;\n    let b64 = '';\n    try { b64 = String(source.getLoginHeader() || '').trim(); } catch (e) {}\n    return { b64: b64 };\n}\n\nfunction buildHeaders() {\n    let { java } = this;\n    let t = getToken.call(this);\n    let dev = getDeviceId.call(this);\n    let ts = String(Math.floor(Date.now() \/ 1000));\n\n    let plain = '';\n    if (t.b64) {\n        try { plain = java.base64Decode(t.b64); } catch (e) { plain = ''; }\n    }\n\n    return {\n        'User-Agent': java.getWebViewUA(),\n        'x-novel-token': t.b64,\n        'x-novel-deviceid': dev,\n        'x-novel-timestamap': ts,\n        'x-novel-public': java.md5Encode(plain + dev + ts).toLowerCase(),\n        'x-novel-version': '10'\n    };\n}\n\nfunction postJson(path, obj) {\n    let { java } = this;\n    let headers = buildHeaders.call(this);\n    headers['Content-Type'] = 'application\/json';\n    let opt = { method: 'POST', body: JSON.stringify(obj || {}), headers: headers };\n    let res = java.ajax(getHost.call(this) + path + ',' + JSON.stringify(opt));\n    res = decryptIfEncrypted.call(this, res);\n    try { return JSON.parse(res); } catch (e) { return { code: -1, message: '解析失败', raw: res }; }\n}\n\nfunction getSourceList() {\n    let list = [];\n    try {\n        let arr = postJson.call(this, '\/api\/source', {});\n        if (Array.isArray(arr)) {\n            arr.forEach(function (it) { if (it && it.enabled && it.name) list.push(it); });\n            list.sort(function (a, b) { return (a.sort || 0) - (b.sort || 0); });\n        }\n    } catch (e) {}\n    return list;\n}\n\nfunction getCmtBrowserOptions() {\n    return {\n        expandedCornersRadius: 20,\n        dismissOnTouchOutside: true,\n        isDraggable: true,\n        shouldDimBackground: true,\n        backgroundDimAmount: 0.5,\n        hardwareAccelerated: true,\n        isNestedScrollingEnabled: true,\n        isGestureInsetBottomIgnored: true,\n        setFitToContents: false,\n        heightPercentage: 0.75,\n        isHideable: true\n    };\n}\n\nfunction showCmt(bid, cid, para, date, sourceName) {\n    let { java } = this;\n    let url = getHost.call(this) + '\/comment'\n            + '?tab=paras&book_id=' + bid\n            + '&item_id=' + cid\n            + '&para=' + para\n            + '&source=' + (sourceName || '');\n    try {\n        java.showBrowser(url, null, 'window.java=java;', JSON.stringify(getCmtBrowserOptions.call(this)));\n    } catch (e) {\n        try { java.startBrowser(url, '段评'); } catch (e2) {\n            java.toast('打开段评失败：' + e2);\n        }\n    }\n}\n\nfunction getSessionId() {\n    let config = getConfig.call(this);\n    return String(config.fanqie_sessionid || '');\n}\n\nfunction getSessionIdFromCookie() {\n    const { cookie } = this;\n    try {\n        let cookieHeader = String(cookie.getCookie('fanqienovel.com'));\n        let sessionId = cookieHeader.match(\/sessionid=([^;]+)\/)?.[1] || null;\n        if (sessionId) return sessionId;\n    } catch (e) {}\n    return \"\";\n}\n\nfunction getJjToken() {\n    let { source } = this;\n    let config = getConfig.call(this);\n\n    let loginInfoKey = '';\n    try {\n        let info = source.getLoginInfoMap() || {};\n        loginInfoKey = String(info.get('晋江登录Token') || '').trim();\n    } catch (e) {}\n\n    if (loginInfoKey) return loginInfoKey;\n\n    return String(config.jj_token || '').trim();\n}\n\nfunction buildJjHeaders() {\n    let { java } = this;\n    return {\n        \"source\": \"android\",\n        \"versiontype\": \"reading\",\n        \"versioncode\": \"467\",\n        \"version-code\": \"467\",\n        \"User-Agent\": java.getWebViewUA()\n    };\n}\n\nfunction jjEncode(data) {\n    let { java } = this;\n    return java.createSymmetricCrypto(\"DES\/CBC\/PKCS5Padding\", \"KW8Dvm2N\", \"1ae2c94b\").encryptBase64(data);\n}\n\nfunction isShelfSyncEnabled() {\n    try {\n        return String(getConfig.call(this).shelf_sync || 'off') == 'on';\n    } catch (e) {\n        return false;\n    }\n}\n\nfunction syncShelf(isAdd) {\n    let { java, book } = this;\n    try {\n        if (!book || !book.tocUrl) return;\n        let raw = String(book.tocUrl || '').replace(\/\\\\\/g, '').replace(\/^data:catalogUrl;base64,\/, '');\n        let idx = raw.indexOf(',');\n        if (idx >= 0) raw = raw.substring(0, idx);\n        raw = raw.replace(\/[^A-Za-z0-9+\/=]\/g, '');\n        let catalog = JSON.parse(String(java.base64Decode(raw) || '').replace(\/\\\\\/g, ''));\n        let sourceName = String(catalog.source || '');\n        let bookId = String(catalog.book_id || catalog.bookid || '');\n        if (!bookId) return;\n\n        let apiSource = '';\n        let logins = '';\n        if (sourceName.indexOf('番茄') >= 0) {\n            apiSource = '番茄';\n            logins = getSessionId.call(this) || getSessionIdFromCookie.call(this);\n        } else if (sourceName.indexOf('晋江') >= 0) {\n            apiSource = '晋江';\n            logins = getJjToken.call(this);\n        }\n        if (!apiSource || !logins) {\n            java.toast('书架同步：请先登录' + apiSource);\n            return;\n        }\n\n        let res = postJson.call(this, '\/api\/bookshelf\/sync', {\n            source: apiSource,\n            logins: String(logins),\n            book_id: bookId,\n            action: isAdd ? 'add' : 'del',\n            group: ''\n        });\n        if (res && res.code == 0) {\n            java.toast((isAdd ? '已同步到' : '已从') + apiSource + (isAdd ? '书架' : '书架移除'));\n        } else {\n            java.toast((res && res.message) || '书架同步失败');\n        }\n    } catch (e) {\n        java.log('syncShelf error: ' + e);\n        java.toast('书架同步失败');\n    }\n}\n\nfunction syncProgress(chapterParams) {\n    let { java } = this;\n    try {\n        let conf = getConfig.call(this);\n        if ((conf.progress_sync || 'off') != 'on') return;\n\n        let sourceName = String(chapterParams.source || '');\n        let logins = '';\n        if (\/^晋江\/.test(sourceName)) {\n            logins = String(conf.jj_token || '');\n        } else if (\/^番茄\/.test(sourceName)) {\n            logins = String(conf.fanqie_sessionid || '');\n        }\n        if (!logins) return;\n\n        let headers = buildHeaders.call(this);\n        headers['Content-Type'] = 'application\/json';\n\n        let body = JSON.stringify({\n            source: sourceName,\n            book_id: String(chapterParams.book_id || ''),\n            item_id: String(chapterParams.item_id || ''),\n            logins: logins\n        });\n\n        let opt = { method: 'POST', body: body, headers: headers };\n        let res = java.ajax(getHost.call(this) + '\/api\/progress,' + JSON.stringify(opt));\n        res = decryptIfEncrypted.call(this, res);\n        let obj = JSON.parse(res);\n        if (obj.code !== 0) java.log('进度同步失败：' + (obj.message || '未知错误'));\n    } catch (e) {\n        java.log('进度同步异常：' + e);\n    }\n}\n\nfunction decryptIfEncrypted(res) {\n    let raw = String(res == undefined || res == null ? '' : res).trim();\n    if (!raw) return res;\n\n    if (raw.indexOf('\"aes-256-gcm\"') >= 0 && raw.charAt(0) == '{') {\n        try {\n            let obj = JSON.parse(raw);\n            if (obj.alg == 'aes-256-gcm' && obj.data && obj.iv && obj.tag) {\n                return aesGcmDecrypt.call(this, raw);\n            }\n            if (obj.data && typeof obj.data == 'object' && obj.data.alg == 'aes-256-gcm') {\n                let dec = aesGcmDecrypt.call(this, obj.data);\n                try { obj.data = JSON.parse(dec); }\n                catch (e) { obj.data = dec; }\n                return JSON.stringify(obj);\n            }\n        } catch (e) {}\n    }\n    return res;\n}\n\nfunction aesGcmDecrypt(payload) {\n    let { java } = this;\n    let obj = null;\n    try {\n        obj = (typeof payload == 'string') ? JSON.parse(payload) : payload;\n        if (!obj || obj.alg !== 'aes-256-gcm' || !obj.data || !obj.iv || !obj.tag) return payload;\n    } catch (e) { return payload; }\n\n    try {\n        let C = Packages.javax.crypto.Cipher;\n        let K = Packages.javax.crypto.spec.SecretKeySpec;\n        let G = Packages.javax.crypto.spec.GCMParameterSpec;\n        let S = Packages.java.lang.String;\n        let A = Packages.java.util.Arrays;\n        let Sys = Packages.java.lang.System;\n\n        let keyBytes = new S(\"Lk4#Fj9&Gh1*Ds6^Qw3!Er8@Ty5%Ui0o\").getBytes(\"UTF-8\");\n        let ivBytes = java.base64DecodeToByteArray(obj.iv);\n        let tagBytes = java.base64DecodeToByteArray(obj.tag);\n        let dataBytes = java.base64DecodeToByteArray(obj.data);\n\n        let combined = A.copyOf(dataBytes, dataBytes.length + tagBytes.length);\n        Sys.arraycopy(tagBytes, 0, combined, dataBytes.length, tagBytes.length);\n\n        let cipher = C.getInstance(\"AES\/GCM\/NoPadding\");\n        cipher.init(C.DECRYPT_MODE, new K(keyBytes, \"AES\"), new G(128, ivBytes));\n        return new S(cipher.doFinal(combined), \"UTF-8\").toString();\n    } catch (e) {}\n\n    try {\n        function b64dec(s) {\n            let T = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+\/';\n            let out = [], v = 0, bits = 0;\n            s = String(s).replace(\/[^A-Za-z0-9+\/]\/g, '');\n            for (let i = 0; i < s.length; i++) {\n                v = (v << 6) | T.indexOf(s.charAt(i));\n                bits += 6;\n                if (bits >= 8) { bits -= 8; out.push((v >> bits) & 0xFF); }\n            }\n            return out;\n        }\n        function utf8dec(plain) {\n            let s = '', buf = [];\n            for (let i = 0; i < plain.length; i++) {\n                let c = plain[i];\n                if (c < 0x80) buf.push(c);\n                else if (c < 0xE0) buf.push(((c & 0x1F) << 6) | (plain[++i] & 0x3F));\n                else if (c < 0xF0) buf.push(((c & 0x0F) << 12) | ((plain[++i] & 0x3F) << 6) | (plain[++i] & 0x3F));\n                else {\n                    let cp = ((c & 0x07) << 18) | ((plain[++i] & 0x3F) << 12) | ((plain[++i] & 0x3F) << 6) | (plain[++i] & 0x3F);\n                    cp -= 0x10000;\n                    buf.push(0xD800 + (cp >> 10), 0xDC00 + (cp & 0x3FF));\n                }\n                if (buf.length > 4096) { s += String.fromCharCode.apply(null, buf); buf = []; }\n            }\n            s += String.fromCharCode.apply(null, buf);\n            return s;\n        }\n\n        let SBOX = [\n            0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,\n            0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,\n            0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,\n            0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,\n            0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,\n            0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,\n            0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,\n            0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,\n            0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,\n            0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,\n            0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,\n            0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,\n            0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,\n            0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,\n            0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,\n            0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16\n        ];\n        function xtime(a) { return ((a << 1) ^ (a & 0x80 ? 0x1b : 0)) & 0xFF; }\n        function gmul(a, b) {\n            let p = 0;\n            for (let i = 0; i < 8; i++) {\n                if (b & 1) p ^= a;\n                let hi = a & 0x80;\n                a = (a << 1) & 0xFF;\n                if (hi) a ^= 0x1b;\n                b >>= 1;\n            }\n            return p;\n        }\n        function expandKey(key) {\n            let Nk = 8, w = [], rcon = 1;\n            for (let i = 0; i < Nk; i++) w.push([key[4*i], key[4*i+1], key[4*i+2], key[4*i+3]]);\n            for (let i = Nk; i < 60; i++) {\n                let t = w[i-1].slice();\n                if (i % Nk == 0) {\n                    t = [t[1], t[2], t[3], t[0]].map(x => SBOX[x]);\n                    t[0] ^= rcon;\n                    rcon = xtime(rcon);\n                } else if (i % Nk == 4) {\n                    t = t.map(x => SBOX[x]);\n                }\n                w.push([w[i-Nk][0]^t[0], w[i-Nk][1]^t[1], w[i-Nk][2]^t[2], w[i-Nk][3]^t[3]]);\n            }\n            let rk = [];\n            for (let i = 0; i < w.length; i++) rk.push(w[i][0], w[i][1], w[i][2], w[i][3]);\n            return rk;\n        }\n        function encryptBlock(rk, input) {\n            let st = input.slice(0, 16);\n            for (let i = 0; i < 16; i++) st[i] ^= rk[i];\n            for (let round = 1; round <= 14; round++) {\n                for (let i = 0; i < 16; i++) st[i] = SBOX[st[i]];\n                let t = st.slice();\n                for (let r = 1; r < 4; r++)\n                    for (let c = 0; c < 4; c++)\n                        st[r + 4*c] = t[r + 4*((c + r) % 4)];\n                if (round !== 14) {\n                    for (let c = 0; c < 4; c++) {\n                        let i = 4*c;\n                        let a0 = st[i], a1 = st[i+1], a2 = st[i+2], a3 = st[i+3];\n                        st[i]   = gmul(a0,2)^gmul(a1,3)^a2^a3;\n                        st[i+1] = a0^gmul(a1,2)^gmul(a2,3)^a3;\n                        st[i+2] = a0^a1^gmul(a2,2)^gmul(a3,3);\n                        st[i+3] = gmul(a0,3)^a1^a2^gmul(a3,2);\n                    }\n                }\n                for (let i = 0; i < 16; i++) st[i] ^= rk[round*16 + i];\n            }\n            return st;\n        }\n        function gfMul(x, y) {\n            let z = new Array(16).fill(0);\n            let v = y.slice();\n            for (let i = 0; i < 128; i++) {\n                if ((x[i >> 3] >> (7 - (i & 7))) & 1) {\n                    for (let j = 0; j < 16; j++) z[j] ^= v[j];\n                }\n                let lsb = v[15] & 1;\n                for (let j = 15; j > 0; j--) v[j] = ((v[j] >> 1) | ((v[j-1] & 1) << 7)) & 0xFF;\n                v[0] >>= 1;\n                if (lsb) v[0] ^= 0xE1;\n            }\n            return z;\n        }\n        function writeBitsBE(arr, offset, bits) {\n            for (let i = 0; i < 8; i++) {\n                arr[offset + 7 - i] = bits % 256;\n                bits = Math.floor(bits \/ 256);\n            }\n        }\n        function ghash(H, data, aadBits, ctBits) {\n            let y = new Array(16).fill(0);\n            for (let i = 0; i < data.length; i += 16) {\n                let block = new Array(16).fill(0);\n                for (let j = 0; j < 16 && i + j < data.length; j++) block[j] = data[i+j];\n                for (let j = 0; j < 16; j++) y[j] ^= block[j];\n                y = gfMul(y, H);\n            }\n            let lenBlock = new Array(16).fill(0);\n            writeBitsBE(lenBlock, 0, aadBits);\n            writeBitsBE(lenBlock, 8, ctBits);\n            for (let j = 0; j < 16; j++) y[j] ^= lenBlock[j];\n            y = gfMul(y, H);\n            return y;\n        }\n        function inc32(block) {\n            for (let i = 15; i >= 12; i--) {\n                block[i] = (block[i] + 1) & 0xFF;\n                if (block[i] !== 0) break;\n            }\n        }\n        function gcmDecrypt(key, iv, ct, tag) {\n            let rk = expandKey(key);\n            let H = encryptBlock(rk, new Array(16).fill(0));\n            let J0;\n            if (iv.length == 12) J0 = iv.concat([0, 0, 0, 1]);\n            else J0 = ghash(H, iv, 0, iv.length * 8);\n            let S = ghash(H, ct, 0, ct.length * 8);\n            let ej0 = encryptBlock(rk, J0);\n            let calcTag = new Array(16);\n            for (let i = 0; i < 16; i++) calcTag[i] = S[i] ^ ej0[i];\n            for (let i = 0; i < 16; i++) {\n                if (calcTag[i] !== tag[i]) return null;\n            }\n            let out = [];\n            let ctr = J0.slice();\n            for (let i = 0; i < ct.length; i += 16) {\n                inc32(ctr);\n                let ks = encryptBlock(rk, ctr);\n                for (let j = 0; j < 16 && i + j < ct.length; j++) {\n                    out.push(ct[i+j] ^ ks[j]);\n                }\n            }\n            return out;\n        }\n\n        let key = [];\n        let keyStr = \"Lk4#Fj9&Gh1*Ds6^Qw3!Er8@Ty5%Ui0o\";\n        for (let i = 0; i < keyStr.length; i++) key.push(keyStr.charCodeAt(i) & 0xFF);\n\n        let iv = b64dec(obj.iv);\n        let tag = b64dec(obj.tag);\n        let ct = b64dec(obj.data);\n\n        let plain = gcmDecrypt(key, iv, ct, tag);\n        if (!plain) return payload;\n        return utf8dec(plain);\n    } catch (e) {\n        return payload;\n    }\n}\n\nfunction getJjShelfClass() {\n    let { java } = this;\n    try {\n        var token = getJjToken.call(this);\n        if (!token) {\n            java.toast(\"请先配置晋江登录Token\");\n            return;\n        }\n\n        var url = \"https:\/\/app.jjwxc.org\/app.jjwxc\/android\/reading\/FavoriteClass\/favoriteClass\";\n        var body = encode.call(this, JSON.stringify({\n            \"versionCode\": \"444\",\n            \"token\": token\n        }));\n\n        var headers = {\n            \"versioncode\": \"444\",\n            \"versiontype\": \"reading\",\n            \"User-Agent\": java.getWebViewUA()\n        };\n\n        var res = java.post(url, body, headers).body();\n        var data = JSON.parse(res);\n\n        if (!data || !data.data || !data.data.length) {\n            java.log(\"获取分类失败: \" + res);\n            java.toast(\"获取分类失败，请检查登录状态\");\n            return;\n        }\n\n        var excludeIds = { \"-1\": true, \"-2\": true, \"-3\": true };\n        var classList = [];\n        for (var i = 0; i < data.data.length; i++) {\n            var item = data.data[i];\n            var cid = String(item.classid);\n            if (excludeIds[cid]) continue;\n            classList.push({\n                classId: cid,\n                className: String(item.classname)\n            });\n        }\n\n        if (classList.length == 0) {\n            java.toast(\"暂无可用分类\");\n            return;\n        }\n\n        var config = getConfig.call(this);\n        var savedId = String(config.type_id || \"\");\n\n        var cardsHtml = \"\";\n        for (var j = 0; j < classList.length; j++) {\n            var c = classList[j];\n            var selected = (String(c.classId) == savedId) ? \" selected\" : \"\";\n            cardsHtml +=\n                '<div class=\"class-card' + selected + '\" data-id=\"' + c.classId + '\" onclick=\"pick(this)\">' +\n                    '<div class=\"class-name\">' + c.className + '<\/div>' +\n                    '<div class=\"class-id\">ID: ' + c.classId + '<\/div>' +\n                '<\/div>';\n        }\n\n        var html = `<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no\">\n<title>晋江收藏分类<\/title>\n<style>\n*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}\nbody{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei',sans-serif;background:#0a0a0f;background-image:radial-gradient(ellipse at 30% 20%, #2a1020 0%, #0a0e27 50%, #1a0d1a 100%);color:#e8e8f0;min-height:100vh;padding:20px}\n.header{text-align:center;padding:10px 0 18px}\n.header h1{font-size:1.3rem;font-weight:800;background:linear-gradient(135deg,#e8e8f0 40%,#c14a63);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}\n.header p{font-size:.72rem;color:#8888a0;margin-top:4px}\n.current-bar{display:flex;align-items:center;gap:8px;padding:10px 14px;margin-bottom:16px;background:rgba(193,74,99,.1);border:1px solid rgba(193,74,99,.3);border-radius:10px;font-size:.74rem;color:rgba(255,255,255,.85)}\n.current-bar strong{color:#fff}\n.grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}\n.class-card{position:relative;padding:16px 10px;border-radius:14px;background:rgba(255,255,255,.04);border:2px solid rgba(255,255,255,.08);cursor:pointer;transition:all .25s;text-align:center}\n.class-card:hover{border-color:rgba(255,255,255,.2);background:rgba(255,255,255,.08)}\n.class-card.selected{border-color:#c14a63;background:rgba(193,74,99,.18);box-shadow:0 0 20px rgba(193,74,99,.3)}\n.class-card.selected::after{content:'✓';position:absolute;top:6px;right:8px;width:18px;height:18px;border-radius:50%;background:#c14a63;color:#fff;font-size:11px;display:flex;align-items:center;justify-content:center}\n.class-name{font-size:.8rem;font-weight:600;color:rgba(255,255,255,.9);word-break:break-all}\n.class-id{font-size:.62rem;color:rgba(255,255,255,.4);margin-top:4px}\n.tip{text-align:center;font-size:.68rem;color:rgba(255,255,255,.35);margin-top:20px}\n<\/style>\n<\/head>\n<body>\n<div class=\"header\">\n    <h1>晋江收藏分类<\/h1>\n    <p>选择后点击右上角 ✓ 保存<\/p>\n<\/div>\n<div class=\"current-bar\">\n    <span>当前已选择分类ID：<strong id=\"currentId\">` + (savedId || \"-\") + `<\/strong><\/span>\n<\/div>\n<div class=\"grid\" id=\"grid\">` + cardsHtml + `<\/div>\n<div class=\"tip\">新分类请先在晋江 App 内创建<\/div>\n<div id=\"result-output\" style=\"display:none;\">` + savedId + `<\/div>\n<script>\nfunction pick(el){\n    document.querySelectorAll('.class-card').forEach(function(c){c.classList.remove('selected');});\n    el.classList.add('selected');\n    document.getElementById('currentId').textContent = el.getAttribute('data-id');\n    document.getElementById('result-output').textContent = el.getAttribute('data-id');\n}\n<\/script>\n<\/body>\n<\/html>`;\n\n        var base64 = \"data:text\/html;base64,\" + java.base64Encode(html);\n        var result = java.startBrowserAwait(base64, \"晋江收藏分类\", false);\n\n        if (!result || !result.body()) {\n            java.toast(\"已取消分类设置\");\n            return;\n        }\n\n        var bodyHtml = result.body();\n        var match = bodyHtml.match(\/<div id=\"result-output\"[^>]*>([^<]*)<\\\/div>\/);\n        var newId = \"\";\n        if (match && match[1]) {\n            newId = match[1].trim();\n        }\n\n        if (!newId) {\n            java.toast(\"未选择分类\");\n            return;\n        }\n\n        setConfig.call(this, { type_id: String(newId) });\n        java.toast(\"\\n已保存分类ID：\" + newId);\n\n    } catch (e) {\n        java.log(\"获取书架分类失败: \" + e);\n        java.toast(\"\\n获取书架分类失败\");\n    }\n}\n\nfunction selectSource() {\n    let { java } = this;\n    var base = getHost.call(this);\n    var list = [];\n    try {\n        var opt = { method: 'POST', body: '{}' };\n        var res = java.ajax(base + '\/api\/source,' + JSON.stringify(opt));\n        var arr = JSON.parse(res);\n        if (Array.isArray(arr)) {\n            for (var i = 0; i < arr.length; i++) {\n                var it = arr[i];\n                if (it && it.enabled && it.name) list.push(it);\n            }\n            list.sort(function (a, b) { return (a.sort || 0) - (b.sort || 0); });\n        }\n    } catch (e) {\n        java.toast('源站列表请求失败：' + e);\n        return;\n    }\n    if (!list.length) { java.toast('源站列表为空'); return; }\n\n    var conf = getConfig.call(this);\n    var TABS = ['novel', 'audio', 'comic', 'video', 'download'];\n    var TAB_LABEL = { novel: '小说', audio: '听书', comic: '漫画', video: '短剧', download: '下载' };\n    var TAB_ICON = { novel: 'fa-book-open', audio: 'fa-headphones', comic: 'fa-images', video: 'fa-film', download: 'fa-download' };\n\n    var sourcesCfg = conf.sources || {};\n    TABS.forEach(function (t) { if (!Array.isArray(sourcesCfg[t])) sourcesCfg[t] = []; });\n\n    var allCfg = Array.isArray(conf.allTabs) ? conf.allTabs.slice() : [];\n    TABS.forEach(function (t) {\n        if (sourcesCfg[t].length > 0 && allCfg.indexOf(t) !== -1) {\n            allCfg = allCfg.filter(function (x) { return x !== t; });\n        }\n    });\n\n    var tabMap = {};\n    for (var i = 0; i < list.length; i++) {\n        var item = list[i];\n        var tab = item.tab || 'novel';\n        if (TABS.indexOf(tab) == -1) continue;\n        if (!tabMap[tab]) tabMap[tab] = [];\n        tabMap[tab].push(item);\n    }\n\n    var tabsData = [];\n    for (var t = 0; t < TABS.length; t++) {\n        var tabKey = TABS[t];\n        var items = tabMap[tabKey] || [];\n        var selNames = sourcesCfg[tabKey] || [];\n        var isAll = allCfg.indexOf(tabKey) !== -1;\n        var cards = [];\n        cards.push({ name: '', label: '全部源站', all: true, sel: isAll, lv: '' });\n        for (var j = 0; j < items.length; j++) {\n            var it = items[j];\n            var lvNum = (it.group && it.group.level) ? Number(it.group.level) : Number(it.min_user_group || 1);\n            cards.push({\n                name: it.name,\n                label: it.name,\n                all: false,\n                sel: selNames.indexOf(it.name) !== -1,\n                lv: lvNum > 1 ? ('LV' + lvNum) : ''\n            });\n        }\n        tabsData.push({ tab: tabKey, label: TAB_LABEL[tabKey], icon: TAB_ICON[tabKey], cards: cards });\n    }\n\n    function jsonForHtml(v) {\n        return JSON.stringify(v).replace(\/<\/g, '\\\\u003c').replace(\/>\/g, '\\\\u003e').replace(\/&\/g, '\\\\u0026');\n    }\n    var dataJson = jsonForHtml(tabsData);\n\n    var html = '<!DOCTYPE html><html lang=\"zh-CN\" data-theme=\"glass\" data-mode=\"light\"><head>' +\n        '<meta charset=\"UTF-8\">' +\n        '<meta name=\"viewport\" content=\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover\">' +\n        '<meta name=\"theme-color\" content=\"#E8EEF6\">' +\n        '<title>源站选择<\/title>' +\n        '<link rel=\"stylesheet\" href=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/font-awesome\/6.4.0\/css\/all.min.css\">' +\n        '<style>' +\n        ':root{' +\n        '--bg-rgb:232,238,246;--surface-rgb:255,255,255;--text-rgb:31,38,51;--muted-rgb:108,120,140;' +\n        '--line-rgb:150,165,190;--primary-rgb:91,124,250;' +\n        '--bg:#e8eef6;--surface:#ffffff;--surface-soft:#f6f9ff;--text:#1f2633;--muted:#6c788c;' +\n        '--primary:#5b7cfa;--primary-dark:#4363d8;--primary-ink:#f7f9ff;' +\n        '--border:rgba(var(--line-rgb),.18);--primary-soft:rgba(var(--primary-rgb),.08);' +\n        '--text-soft:rgba(var(--text-rgb),.78);--border-soft:rgba(var(--line-rgb),.16);' +\n        '--card-bg:rgba(var(--surface-rgb),.78);' +\n        '--radius-lg:20px;--radius:14px;--radius-sm:10px;' +\n        '--shadow:0 18px 40px -26px rgba(31,45,80,.35),0 2px 8px rgba(31,45,80,.05);' +\n        '--shadow-card:var(--shadow);' +\n        '--font-cn:\"PingFang SC\",\"Hiragino Sans GB\",\"Microsoft YaHei\",system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif;' +\n        '}' +\n        '*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}' +\n        'html{min-height:100%}' +\n        'body{margin:0;min-height:100vh;color:var(--text);font-family:var(--font-cn);font-size:15px;line-height:1.6;' +\n        'background:var(--bg);position:relative;overflow-x:hidden;' +\n        '-webkit-font-smoothing:antialiased;' +\n        'padding:calc(14px + env(safe-area-inset-top)) 14px calc(22px + env(safe-area-inset-bottom))}' +\n        'body::before{content:\"\";position:fixed;inset:0;z-index:0;pointer-events:none;' +\n        'background:radial-gradient(900px 520px at 12% -8%,rgba(var(--primary-rgb),.16),transparent 68%),' +\n        'radial-gradient(760px 480px at 94% 4%,rgba(120,220,230,.18),transparent 72%),' +\n        'linear-gradient(180deg,var(--bg),var(--bg))}' +\n        '.wrap{max-width:720px;margin:0 auto;position:relative;z-index:1}' +\n        '.glass{background:linear-gradient(155deg,rgba(var(--surface-rgb),.86),rgba(var(--surface-rgb),.58));' +\n        'border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-card);' +\n        'backdrop-filter:blur(22px) saturate(1.2);-webkit-backdrop-filter:blur(22px) saturate(1.2);' +\n        'animation:slideIn .5s cubic-bezier(.22,.61,.36,1)}' +\n        '@keyframes slideIn{from{opacity:0;transform:translateY(18px) scale(.99)}to{opacity:1;transform:none}}' +\n        '.hero{display:flex;align-items:center;gap:14px;padding:17px 18px;margin-bottom:12px}' +\n        '.hero-icon{flex:0 0 auto;width:46px;height:46px;border-radius:14px;display:flex;align-items:center;justify-content:center;font-size:20px;' +\n        'background:linear-gradient(135deg,var(--primary),var(--primary-dark));color:#fff;' +\n        'box-shadow:0 12px 24px -12px rgba(var(--primary-rgb),.85)}' +\n        '.hero h1{margin:0;font-size:19px;line-height:1.25;letter-spacing:.2px}' +\n        '.hero p{margin:5px 0 0;font-size:12px;line-height:1.55;color:var(--muted)}' +\n        '.tabs{display:flex;gap:6px;overflow-x:auto;padding:6px;margin-bottom:12px;scrollbar-width:none;-ms-overflow-style:none}' +\n        '.tabs::-webkit-scrollbar{display:none}' +\n        '.tab{flex:0 0 auto;display:inline-flex;align-items:center;gap:7px;border:1px solid transparent;background:transparent;color:var(--muted);' +\n        'padding:9px 12px;border-radius:12px;font:inherit;font-size:13px;font-weight:600;cursor:pointer;transition:.18s}' +\n        '.tab:hover{background:rgba(var(--surface-rgb),.5);color:var(--text)}' +\n        '.tab.on{background:var(--primary-soft);color:var(--primary);border-color:rgba(var(--primary-rgb),.28)}' +\n        '.tab i{font-size:13px}.tab-count{font-size:10px;line-height:1.6;padding:0 6px;border-radius:999px;background:rgba(var(--line-rgb),.14);opacity:.85}' +\n        '.panel{display:none}.panel.on{display:block;animation:fade .18s ease}' +\n        '@keyframes fade{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}' +\n        '.all-card{width:100%;display:flex;align-items:center;gap:10px;padding:14px 16px;margin-bottom:10px;border-radius:16px;' +\n        'border:1px solid var(--border);background:linear-gradient(155deg,rgba(var(--surface-rgb),.9),rgba(var(--surface-rgb),.6));' +\n        'box-shadow:0 12px 30px -24px rgba(31,45,80,.55);color:var(--text);font:inherit;text-align:left;cursor:pointer;transition:.18s}' +\n        '.all-card .ico{font-size:16px;color:var(--primary)}.all-card .t{flex:1;font-size:14px;font-weight:600}' +\n        '.all-card.sel{border-color:rgba(var(--primary-rgb),.5);background:linear-gradient(155deg,rgba(var(--primary-rgb),.16),rgba(var(--primary-rgb),.05));box-shadow:0 0 0 3px var(--primary-soft)}' +\n        '.check{flex:0 0 auto;width:22px;height:22px;border-radius:50%;border:1px solid var(--border);background:rgba(var(--surface-rgb),.6);' +\n        'display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:800;color:transparent;transition:.18s}' +\n        '.all-card.sel .check,.src-card.sel .check{background:var(--primary);border-color:var(--primary);color:#fff}' +\n        '.grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:9px}' +\n        '@media(max-width:520px){.grid{grid-template-columns:repeat(2,minmax(0,1fr))}}' +\n        '.src-card{position:relative;display:flex;align-items:center;justify-content:center;min-height:78px;padding:12px 10px 14px;' +\n        'border-radius:16px;border:1px solid var(--border);background:linear-gradient(155deg,rgba(var(--surface-rgb),.9),rgba(var(--surface-rgb),.6));' +\n        'box-shadow:0 12px 28px -24px rgba(31,45,80,.5);color:var(--text);font:inherit;font-size:12.5px;line-height:1.4;' +\n        'word-break:break-all;cursor:pointer;transition:.18s}' +\n        '.src-card:active{transform:scale(.97)}' +\n        '.src-card.sel{border-color:rgba(var(--primary-rgb),.52);background:linear-gradient(155deg,rgba(var(--primary-rgb),.16),rgba(var(--primary-rgb),.05));box-shadow:0 0 0 3px var(--primary-soft)}' +\n        '.src-card .name{max-width:100%;text-align:center}' +\n        '.src-card .lv{position:absolute;right:8px;bottom:7px;font-size:9px;font-weight:700;color:#b98a3a;background:rgba(185,138,58,.13);border:1px solid rgba(185,138,58,.32);padding:1px 5px;border-radius:6px}' +\n        '.src-card .check{position:absolute;top:7px;right:7px;width:18px;height:18px;font-size:10px}' +\n        '.empty{text-align:center;color:var(--muted);padding:38px 12px;font-size:13px}' +\n        '.tips{display:flex;align-items:center;gap:9px;margin-top:14px;padding:12px 16px;border-radius:16px;color:var(--muted);font-size:12px;line-height:1.55}' +\n        '.tips .dot{flex:0 0 auto;width:7px;height:7px;border-radius:50%;background:var(--primary);box-shadow:0 0 0 4px var(--primary-soft)}' +\n        '.tips b{color:var(--text)}' +\n        '<\/style><\/head><body><div class=\"wrap\">' +\n        '<header class=\"hero glass\"><div class=\"hero-icon\"><i class=\"fa-solid fa-satellite-dish\"><\/i><\/div><div><h1>源站选择<\/h1><p>按分类多选，点选需要的站点；「全部源站」与具体源站互斥<\/p><\/div><\/header>' +\n        '<div class=\"tabs glass\" id=\"tabs\" role=\"tablist\"><\/div>' +\n        '<main id=\"panels\"><\/main>' +\n        '<footer class=\"tips glass\"><span class=\"dot\"><\/span><span id=\"sel-tip\">已选 <b id=\"sel-count\">0<\/b> 个源站<\/span><\/footer>' +\n        '<\/div><div id=\"result-output\" style=\"display:none;\"><\/div>' +\n        '<script>' +\n        'var DATA=' + dataJson + ';' +\n        'var curTab=DATA.length?DATA[0].tab:\"novel\";' +\n        'var tabsEl=document.getElementById(\"tabs\"),panelsEl=document.getElementById(\"panels\"),outEl=document.getElementById(\"result-output\");' +\n        'function esc(v){return String(v==undefined?\"\":v).replace(\/&\/g,\"&amp;\").replace(\/<\/g,\"&lt;\").replace(\/>\/g,\"&gt;\").replace(\/\"\/g,\"&quot;\").replace(\/\\'\/g,\"&#39;\");}' +\n        'function renderTabs(){var h=\"\";for(var i=0;i<DATA.length;i++){var g=DATA[i];var n=g.cards.length>1?g.cards.length-1:0;' +\n        'h+=\"<button type=\\\\\"button\\\\\" class=\\\\\"tab\"+(g.tab==curTab?\" on\":\"\")+\"\\\\\" role=\\\\\"tab\\\\\" data-tab=\\\\\"\"+esc(g.tab)+\"\\\\\"><i class=\\\\\"fa-solid \"+g.icon+\"\\\\\"><\/i>\"+esc(g.label)+\"<span class=\\\\\"tab-count\\\\\">\"+n+\"<\/span><\/button>\";}tabsEl.innerHTML=h;}' +\n        'function renderPanels(){var h=\"\";for(var i=0;i<DATA.length;i++){var g=DATA[i];h+=\"<section class=\\\\\"panel\"+(g.tab==curTab?\" on\":\"\")+\"\\\\\" data-panel=\\\\\"\"+esc(g.tab)+\"\\\\\">\";' +\n        'var all=g.cards[0];h+=\"<button type=\\\\\"button\\\\\" class=\\\\\"all-card\"+(all.sel?\" sel\":\"\")+\"\\\\\" data-all=\\\\\"1\\\\\" data-name=\\\\\"\\\\\"><i class=\\\\\"fa-solid fa-globe ico\\\\\"><\/i><span class=\\\\\"t\\\\\">全部源站<\/span><span class=\\\\\"check\\\\\"><i class=\\\\\"fa-solid fa-check\\\\\"><\/i><\/span><\/button>\";' +\n        'var others=g.cards.slice(1);if(!others.length){h+=\"<div class=\\\\\"empty\\\\\">该分类暂无源站<\/div>\";}else{h+=\"<div class=\\\\\"grid\\\\\">\";' +\n        'for(var j=0;j<others.length;j++){var c=others[j];h+=\"<button type=\\\\\"button\\\\\" class=\\\\\"src-card\"+(c.sel?\" sel\":\"\")+\"\\\\\" data-name=\\\\\"\"+esc(c.name)+\"\\\\\"><span class=\\\\\"name\\\\\">\"+esc(c.label)+\"<\/span>\"+(c.lv?\"<span class=\\\\\"lv\\\\\">\"+esc(c.lv)+\"<\/span>\":\"\")+\"<span class=\\\\\"check\\\\\"><i class=\\\\\"fa-solid fa-check\\\\\"><\/i><\/span><\/button>\";}' +\n        'h+=\"<\/div>\";}h+=\"<\/section>\";}panelsEl.innerHTML=h;}' +\n        'function renderAll(){renderTabs();renderPanels();}' +\n        'function setTab(tab){curTab=tab;renderAll();}' +\n        'function getGroup(tab){for(var i=0;i<DATA.length;i++){if(DATA[i].tab==tab){return DATA[i];}}return null;}' +\n        'function setSelected(btn,on){if(on){btn.classList.add(\"sel\");}else{btn.classList.remove(\"sel\");}}' +\n        'function syncOut(){var out={novel:[],audio:[],comic:[],video:[],download:[]};var allSel=[];' +\n        'var nodes=panelsEl.querySelectorAll(\".src-card.sel\");for(var i=0;i<nodes.length;i++){var p=nodes[i].parentNode;while(p&&!p.getAttribute(\"data-panel\")){p=p.parentNode;}var t=p?p.getAttribute(\"data-panel\"):\"\";var n=nodes[i].getAttribute(\"data-name\");if(t&&out[t]&&n&&out[t].indexOf(n)<0){out[t].push(n);}}' +\n        'var allNodes=panelsEl.querySelectorAll(\".all-card.sel\");for(var k=0;k<allNodes.length;k++){var p2=allNodes[k].parentNode;while(p2&&!p2.getAttribute(\"data-panel\")){p2=p2.parentNode;}if(p2){allSel.push(p2.getAttribute(\"data-panel\"));}}' +\n        'outEl.textContent=JSON.stringify(allSel)+\"|||\"+JSON.stringify(out);' +\n        'var cnt=0;for(var key in out){if(out.hasOwnProperty(key)){cnt+=out[key].length;}}' +\n        'var el=document.getElementById(\"sel-count\");if(el){el.textContent=String(cnt);}' +\n        'var tip=document.getElementById(\"sel-tip\");if(tip){tip.innerHTML=\"已选 <b id=\\\\\"sel-count\\\\\">\"+cnt+\"<\/b> 个源站\"+(allSel.length?\" · 全部源站分类：\"+allSel.join(\"、\"):\"\");}}' +\n        'function toggleAll(btn){var panel=btn.parentNode;var tab=panel.getAttribute(\"data-panel\");var g=getGroup(tab);if(!g){return;}' +\n        'var on=!g.cards[0].sel;g.cards[0].sel=on;for(var j=1;j<g.cards.length;j++){g.cards[j].sel=false;}' +\n        'var allBtn=panel.querySelector(\".all-card\");var cards=panel.querySelectorAll(\".src-card\");' +\n        'setSelected(allBtn,on);for(var c=0;c<cards.length;c++){setSelected(cards[c],false);}syncOut();}' +\n        'function toggleOne(btn){var panel=btn.parentNode.parentNode;var tab=panel.getAttribute(\"data-panel\");var g=getGroup(tab);if(!g){return;}' +\n        'var name=btn.getAttribute(\"data-name\");g.cards[0].sel=false;' +\n        'var allBtn=panel.querySelector(\".all-card\");if(allBtn){setSelected(allBtn,false);}' +\n        'var on=false;for(var j=1;j<g.cards.length;j++){if(g.cards[j].name==name){g.cards[j].sel=!g.cards[j].sel;on=g.cards[j].sel;break;}}' +\n        'setSelected(btn,on);syncOut();}' +\n        'tabsEl.addEventListener(\"click\",function(e){var el=e.target;while(el&&el!==tabsEl&&!el.getAttribute(\"data-tab\")){el=el.parentNode;}if(el&&el!==tabsEl){setTab(el.getAttribute(\"data-tab\"));}});' +\n        'panelsEl.addEventListener(\"click\",function(e){var el=e.target;while(el&&el!==panelsEl&&!el.classList.contains(\"all-card\")&&!el.classList.contains(\"src-card\")){el=el.parentNode;}if(!el||el==panelsEl){return;}if(el.classList.contains(\"all-card\")){toggleAll(el);}else if(el.classList.contains(\"src-card\")){toggleOne(el);}});' +\n        'renderAll();syncOut();' +\n        '<\/script><\/body><\/html>';\n\n    var dataUrl = 'data:text\/html;base64,' + java.base64Encode(html);\n    var choice = java.startBrowserAwait(dataUrl, '源站选择', false);\n    if (!choice || !choice.body()) { java.toast('已取消源站设置'); return; }\n\n    var body = choice.body();\n    var m = body.match(\/<div id=\"result-output\"[^>]*>([^<]*)<\\\/div>\/);\n    var rawOut = (m && m[1]) ? m[1].trim() : '';\n    rawOut = rawOut.replace(\/&lt;\/g, '<').replace(\/&gt;\/g, '>').replace(\/&amp;\/g, '&').replace(\/&quot;\/g, '\"').replace(\/&#39;\/g, \"'\");\n\n    var newSources = { novel: [], audio: [], comic: [], video: [], download: [] };\n    var newAllTabs = [];\n    try {\n        var parts = rawOut.split('|||');\n        var allArr = JSON.parse(parts[0] || '[]');\n        var srcObj = JSON.parse(parts[1] || '{}');\n        if (Array.isArray(allArr)) newAllTabs = allArr;\n        if (srcObj && typeof srcObj == 'object') {\n            TABS.forEach(function (t) { if (Array.isArray(srcObj[t])) newSources[t] = srcObj[t]; });\n        }\n    } catch (e) {}\n\n    TABS.forEach(function (t) {\n        if (newSources[t].length > 0) {\n            newAllTabs = newAllTabs.filter(function (x) { return x !== t; });\n        }\n    });\n\n    var merged = [];\n    TABS.forEach(function (t) {\n        newSources[t].forEach(function (n) { if (merged.indexOf(n) == -1) merged.push(n); });\n    });\n\n    setConfig.call(this, { sources: newSources, allTabs: newAllTabs, source: merged.join(',') });\n    var cnt = merged.length;\n    var allCnt = newAllTabs.length;\n    java.toast(cnt > 0 ? ('源站已保存（共 ' + cnt + ' 个）') : (allCnt > 0 ? '已保存（全部源站）' : '已保存（未选择具体源站）'));\n}\n\nfunction parseVer(v) {\n    return String(v || '').split('.').map(function (x) { return parseInt(x, 10) || 0; });\n}\n\nfunction cmpVer(a, b) {\n    let pa = parseVer(a), pb = parseVer(b);\n    let len = Math.max(pa.length, pb.length);\n    for (let i = 0; i < len; i++) {\n        let x = pa[i] || 0, y = pb[i] || 0;\n        if (x > y) return 1;\n        if (x < y) return -1;\n    }\n    return 0;\n}\n\nfunction shouldBlockByVersion(serverVerObj) {\n    if (!serverVerObj || !serverVerObj.version) return false;\n    let sv = String(serverVerObj.version);\n    let lv = String(localVersion || '');\n    if (cmpVer(sv, lv) <= 0) return false;\n\n    let isForce = (serverVerObj.force == true || String(serverVerObj.force) == 'true');\n    let tipMsg = serverVerObj.msg || '';   \/\/ ← 用 version.msg\n\n    if (isForce) {\n        try { java.toast('\\n服务端要求强制更新 ' + sv + (tipMsg ? '\\n' + tipMsg : '')); } catch (e) {}\n        try { checkVersion.call(this); } catch (e) {}\n    } else {\n        try { java.toast('有新版本 ' + sv + (tipMsg ? '\\n' + tipMsg : '') + '\\n可在登录页「🔄更新检测」查看'); } catch (e) {}\n    }\n\n    return false;\n}\n\nfunction checkVersion() {\n    let { java } = this;\n    var host = getHost.call(this);\n    var versionApi = host + '\/api\/version?aid=sy';\n    var siteApi = host + '\/api\/site\/public';\n\n    function esc(v) {\n        return String(v == undefined ? '' : v)\n            .replace(\/&\/g, '&amp;').replace(\/<\/g, '&lt;').replace(\/>\/g, '&gt;')\n            .replace(\/\"\/g, '&quot;').replace(\/'\/g, '&#39;');\n    }\n\n    var siteName = '书源更新';\n    try {\n        var siteRes = java.ajax(siteApi);\n        var siteObj = JSON.parse(siteRes);\n        if (siteObj && siteObj.data && siteObj.data.site_name) {\n            siteName = String(siteObj.data.site_name);\n        }\n    } catch (e) {}\n\n    var res;\n    try { res = java.ajax(versionApi); }\n    catch (e) { java.toast('版本检查失败：网络异常'); return; }\n    var obj;\n    try { obj = JSON.parse(res); }\n    catch (e) { java.toast('版本检查失败：返回解析异常'); return; }\n    if (!obj || obj.code !== 0 || !obj.data || !obj.data.version) {\n        java.toast((obj && obj.message) || '版本检查失败'); return;\n    }\n\n    var serverVer = String(obj.data.version);\n    var myLocalVer = String(localVersion || '');\n    var cmp = cmpVer(serverVer, myLocalVer);   \/\/ 复用 jsLib 的 cmpVer\n\n    if (cmp > 0) {\n        var msg = String(obj.data.msg || '有新版本啦');\n        var force = String(obj.data.force || 'false') == 'true';\n        var downloadUrl = String(obj.data.download_url || '');\n        var updateLog = obj.data.update_log || {};\n\n        var dates = Object.keys(updateLog).filter(function (d) {\n            return Array.isArray(updateLog[d]) && updateLog[d].length;\n        }).sort(function (a, b) { return b.localeCompare(a); });\n\n        var latestDate = dates[0] || '';\n        var latestItems = latestDate ? updateLog[latestDate] : [];\n        var historyDates = dates.slice(1);\n\n        var latestLogHtml = '';\n        latestItems.forEach(function (t) {\n            latestLogHtml += '<div class=\"update-text\">' + esc(t) + '<\/div>';\n        });\n\n        var historyHtml = '';\n        historyDates.forEach(function (d) {\n            historyHtml += '<div class=\"history-item\"><div class=\"history-date\"><i class=\"fas fa-calendar-day\"><\/i><span>' + esc(d) + '<\/span><\/div>';\n            updateLog[d].forEach(function (t) {\n                historyHtml += '<div class=\"history-text\">' + esc(t) + '<\/div>';\n            });\n            historyHtml += '<\/div>';\n        });\n        var historyCountText = historyDates.length ? '(' + historyDates.length + '条)' : '';\n\n        var bodyHtml = '<!DOCTYPE html><html lang=\"zh-CN\" data-theme=\"glass\" data-mode=\"light\"><head>' +\n            '<meta charset=\"UTF-8\">' +\n            '<meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0,viewport-fit=cover\">' +\n            '<title>' + esc(siteName) + '<\/title>' +\n            '<link rel=\"stylesheet\" href=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/font-awesome\/6.4.0\/css\/all.min.css\">' +\n            '<style>' +\n            ':root{' +\n            '--bg-rgb:232,238,246;--surface-rgb:255,255,255;--text-rgb:31,38,51;--muted-rgb:108,120,140;' +\n            '--line-rgb:150,165,190;--primary-rgb:91,124,250;' +\n            '--bg:#e8eef6;--surface:#ffffff;--surface-soft:#f6f9ff;--text:#1f2633;--muted:#6c788c;' +\n            '--primary:#5b7cfa;--primary-dark:#4363d8;--primary-ink:#f7f9ff;' +\n            '--border:rgba(var(--line-rgb),.18);--primary-soft:rgba(var(--primary-rgb),.08);' +\n            '--text-soft:rgba(var(--text-rgb),.78);--border-soft:rgba(var(--line-rgb),.16);' +\n            '--card-bg:rgba(var(--surface-rgb),.78);' +\n            '--radius-lg:20px;--radius:14px;--radius-sm:10px;' +\n            '--shadow:0 18px 40px -26px rgba(31,45,80,.35),0 2px 8px rgba(31,45,80,.05);' +\n            '--shadow-card:var(--shadow);' +\n            '--font-cn:\"PingFang SC\",\"Hiragino Sans GB\",\"Microsoft YaHei\",system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif;' +\n            '--font-mono:\"SFMono-Regular\",Consolas,\"Liberation Mono\",Menlo,\"Courier New\",monospace;' +\n            '}' +\n            '*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}' +\n            'html{min-height:100%}' +\n            'body{margin:0;min-height:100vh;color:var(--text);font-family:var(--font-cn);font-size:15px;line-height:1.6;' +\n            'background:var(--bg);overflow-x:hidden;position:relative;' +\n            '-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;' +\n            'display:flex;align-items:center;justify-content:center;padding:calc(18px + env(safe-area-inset-top)) 14px calc(22px + env(safe-area-inset-bottom))}' +\n            '.waves{position:fixed;inset:0;z-index:0;pointer-events:none;overflow:hidden;background:linear-gradient(180deg,#eef3fb 0%,#e3ecf8 50%,#dbe6f5 100%)}' +\n            '.wave{position:absolute;left:-50%;width:200%;height:200%;border-radius:45%;opacity:.55;' +\n            'background:radial-gradient(circle at 50% 50%,rgba(91,124,250,.28),rgba(91,124,250,0) 65%);' +\n            'animation:waveMove 18s linear infinite}' +\n            '.wave.w1{top:-120%;animation-duration:20s}' +\n            '.wave.w2{top:-110%;animation-duration:26s;animation-direction:reverse;opacity:.4;' +\n            'background:radial-gradient(circle at 50% 50%,rgba(120,170,255,.24),rgba(120,170,255,0) 65%)}' +\n            '.wave.w3{top:-100%;animation-duration:32s;opacity:.3;' +\n            'background:radial-gradient(circle at 50% 50%,rgba(160,200,255,.22),rgba(160,200,255,0) 65%)}' +\n            '@keyframes waveMove{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}' +\n            'body::after{content:\"\";position:fixed;inset:0;z-index:0;pointer-events:none;' +\n            'background:radial-gradient(1200px 600px at 15% 0%,rgba(var(--bg-rgb),.1),rgba(var(--bg-rgb),0) 70%),' +\n            'linear-gradient(180deg,rgba(var(--bg-rgb),.06) 0%,rgba(var(--bg-rgb),.14) 55%,rgba(var(--bg-rgb),.22) 100%)}' +\n            'h1,h2,h3,p{margin:0}' +\n            '.wrap{width:100%;max-width:440px;position:relative;z-index:1}' +\n            '.card{box-sizing:border-box;padding:22px;border:1px solid var(--border);border-radius:var(--radius-lg);' +\n            'background:var(--card-bg);box-shadow:var(--shadow-card);' +\n            'backdrop-filter:blur(12px) saturate(1.04);-webkit-backdrop-filter:blur(12px) saturate(1.04);' +\n            'animation:slideIn .6s cubic-bezier(.22,.61,.36,1)}' +\n            '@keyframes slideIn{from{opacity:0;transform:translateY(24px) scale(.98)}to{opacity:1;transform:none}}' +\n            '.head{text-align:center;margin-bottom:18px}' +\n            '.head h1{font-size:22px;letter-spacing:.01em;margin-bottom:6px}' +\n            '.head p{font-size:13.5px;color:var(--muted)}' +\n            '.eyebrow{display:inline-block;color:var(--primary);font-size:11.5px;font-weight:700;letter-spacing:.22em;text-transform:uppercase;margin-bottom:6px}' +\n            '.versions{display:flex;align-items:stretch;gap:10px;margin:16px 0}' +\n            '.v-card{flex:1;min-width:0;padding:14px 12px;border:1px solid var(--border);border-radius:var(--radius);' +\n            'background:rgba(var(--surface-rgb),.6);box-shadow:var(--shadow-card);text-align:center;position:relative;' +\n            'backdrop-filter:blur(10px) saturate(1.05);-webkit-backdrop-filter:blur(10px) saturate(1.05)}' +\n            '.v-card .k{font-size:11.5px;color:var(--muted);display:flex;align-items:center;justify-content:center;gap:5px}' +\n            '.v-card .n{font-size:15px;font-weight:700;margin-top:4px;word-break:break-all;font-family:var(--font-mono)}' +\n            '.v-card .d{font-size:11.5px;color:var(--muted);margin-top:4px}' +\n            '.v-card.latest{background:linear-gradient(140deg,rgba(var(--primary-rgb),.16),rgba(var(--primary-rgb),.04));' +\n            'border-color:rgba(var(--primary-rgb),.34);box-shadow:0 0 26px -10px rgba(var(--primary-rgb),.5),var(--shadow-card);' +\n            'animation:breathe 3s ease-in-out infinite}' +\n            '@keyframes breathe{0%,100%{transform:scale(1)}50%{transform:scale(1.02)}}' +\n            '.v-card.latest .k,.v-card.latest .n{color:var(--primary-dark)}' +\n            '.vs{flex:none;width:34px;height:34px;border-radius:50%;align-self:center;display:flex;align-items:center;justify-content:center;' +\n            'font-size:13px;color:var(--primary);background:rgba(var(--surface-rgb),.85);border:1px solid var(--border);' +\n            'box-shadow:0 6px 16px -10px rgba(var(--primary-rgb),.5);' +\n            'backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);' +\n            'animation:pulseIndicator 1.6s ease-in-out infinite}' +\n            '@keyframes pulseIndicator{0%,100%{transform:scale(1)}50%{transform:scale(1.1)}}' +\n            '.section{margin-top:14px;border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;' +\n            'background:rgba(var(--surface-rgb),.55);box-shadow:var(--shadow-card);' +\n            'backdrop-filter:blur(10px) saturate(1.04);-webkit-backdrop-filter:blur(10px) saturate(1.04);' +\n            'animation:slideIn .5s ease .25s backwards}' +\n            '.section-head{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;' +\n            'border-bottom:1px solid var(--border);background:rgba(var(--surface-rgb),.5)}' +\n            '.section-head h2{font-size:15px;font-weight:600;display:flex;align-items:center;gap:7px}' +\n            '.section-head h2 i{color:var(--primary)}' +\n            '.tag{padding:3px 9px;border-radius:999px;background:var(--primary-soft);color:var(--primary);font-size:11.5px;font-weight:600}' +\n            '.section-body{padding:14px}' +\n            '.log-date{display:flex;align-items:center;gap:6px;color:var(--primary);font-size:12.5px;font-weight:700;' +\n            'padding-bottom:7px;border-bottom:1px dashed var(--border-soft);margin-bottom:7px}' +\n            '.update-text{position:relative;padding-left:15px;margin:6px 0;line-height:1.55;font-size:13.5px;color:var(--text-soft);white-space:pre-wrap;word-break:break-word}' +\n            '.update-text::before{content:\"•\";position:absolute;left:0;color:var(--primary);font-weight:700;font-size:1.15rem;line-height:1}' +\n            '.history-head{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;' +\n            'border-bottom:1px solid var(--border);background:rgba(var(--surface-rgb),.5);cursor:pointer;user-select:none;transition:background .2s}' +\n            '.history-head:hover{background:rgba(var(--surface-rgb),.7)}' +\n            '.history-head h2{font-size:15px;font-weight:600;display:flex;align-items:center;gap:7px}' +\n            '.history-head h2 i{color:var(--primary)}' +\n            '.toggle{background:none;border:none;color:var(--muted);cursor:pointer;font-weight:500;display:flex;align-items:center;gap:4px;font-size:12.5px;transition:color .2s}' +\n            '.toggle:hover{color:var(--primary)}' +\n            '.history-body{padding:0 14px;max-height:0;overflow:hidden;transition:max-height .4s ease,padding .4s ease}' +\n            '.history-body.open{max-height:60vh;overflow-y:auto;padding:14px;scrollbar-width:thin}' +\n            '.history-body.open::-webkit-scrollbar{width:6px}' +\n            '.history-body.open::-webkit-scrollbar-thumb{background:rgba(var(--line-rgb),.35);border-radius:4px}' +\n            '.history-item{margin-bottom:14px;padding-bottom:14px;border-bottom:1px dashed var(--border-soft)}' +\n            '.history-item:last-child{border-bottom:none;margin-bottom:0;padding-bottom:0}' +\n            '.history-date{font-weight:600;margin-bottom:7px;display:flex;align-items:center;gap:4px;font-size:12.5px;color:var(--primary);' +\n            'background:var(--primary-soft);padding:5px 9px;border-radius:6px}' +\n            '.history-text{position:relative;padding-left:15px;margin:6px 0;line-height:1.5;font-size:13px;color:var(--text-soft);white-space:pre-wrap;word-break:break-word}' +\n            '.history-text::before{content:\"•\";position:absolute;left:0;color:var(--primary);font-weight:700;font-size:1.1rem;line-height:1}' +\n            '.btn{display:flex;align-items:center;justify-content:center;gap:8px;box-sizing:border-box;width:100%;padding:13px 18px;' +\n            'margin-top:14px;border:1px solid transparent;border-radius:var(--radius-sm);font:inherit;font-size:14.5px;font-weight:600;' +\n            'text-decoration:none;cursor:pointer;transition:background .16s ease,box-shadow .16s ease,transform .12s ease;' +\n            'color:var(--primary-ink);background:linear-gradient(135deg,var(--primary) 0%,var(--primary) 60%,var(--primary-dark) 100%);' +\n            'box-shadow:0 10px 22px -14px rgba(var(--primary-rgb),.9);position:relative;overflow:hidden}' +\n            '.btn::after{content:\"\";position:absolute;top:-50%;left:-50%;width:200%;height:200%;' +\n            'background:linear-gradient(90deg,transparent,rgba(255,255,255,.35),transparent);transform:rotate(30deg);' +\n            'animation:btnShine 3s ease-in-out infinite}' +\n            '@keyframes btnShine{0%,60%{transform:translateX(-120%) rotate(30deg)}80%,100%{transform:translateX(120%) rotate(30deg)}}' +\n            '.btn:hover{background:linear-gradient(135deg,var(--primary) 0%,var(--primary-dark) 60%,var(--primary-dark) 100%);transform:translateY(-1px)}' +\n            '.btn:active{transform:translateY(1px) scale(.98)}' +\n            '.btn i,.btn span{position:relative;z-index:1}' +\n            '.foot{margin-top:14px;text-align:center;color:var(--muted);font-size:12px;line-height:1.6}' +\n            '@media(max-width:400px){.versions{flex-direction:column}.vs{transform:rotate(90deg)}.card{padding:18px 16px}}' +\n            '<\/style><\/head><body>' +\n            '<div class=\"waves\"><div class=\"wave w1\"><\/div><div class=\"wave w2\"><\/div><div class=\"wave w3\"><\/div><\/div>' +\n            '<div class=\"wrap\"><section class=\"card\">' +\n            '<div class=\"head\">' +\n            '<span class=\"eyebrow\">Version Update<\/span>' +\n            '<h1>' + esc(siteName) + '<\/h1>' +\n            '<p>' + esc(msg) + '<\/p>' +\n            '<\/div>' +\n            '<div class=\"versions\">' +\n            '<div class=\"v-card\"><div class=\"k\"><i class=\"fas fa-cube\"><\/i> 当前版本<\/div><div class=\"n\">v' + esc(myLocalVer) + '<\/div><div class=\"d\">您的当前版本<\/div><\/div>' +\n            '<div class=\"vs\"><i class=\"fas fa-arrow-right\"><\/i><\/div>' +\n            '<div class=\"v-card latest\"><div class=\"k\"><i class=\"fas fa-star\"><\/i> 最新版本<\/div><div class=\"n\">v' + esc(serverVer) + '<\/div><div class=\"d\">可用最新版本<\/div><\/div>' +\n            '<\/div>' +\n            (latestLogHtml ? '<div class=\"section\"><div class=\"section-head\"><h2><i class=\"fas fa-bolt\"><\/i> 最新更新<\/h2><span class=\"tag\">最新发布<\/span><\/div><div class=\"section-body\"><div class=\"log-date\"><i class=\"fas fa-calendar-alt\"><\/i>' + esc(latestDate) + '<\/div>' + latestLogHtml + '<\/div><\/div>' : '') +\n            (downloadUrl ? '<a href=\"' + esc(downloadUrl) + '\" class=\"btn\"><i class=\"fas fa-rocket\"><\/i><span>立即更新<\/span><\/a>' : '') +\n            (historyHtml ? '<div class=\"section\" style=\"padding:0\"><div class=\"history-head\" onclick=\"toggleLogs()\"><h2><i class=\"fas fa-history\"><\/i> 历史更新 <span>' + historyCountText + '<\/span><\/h2><button class=\"toggle\"><span id=\"toggleText\">展开历史<\/span><i class=\"fas fa-chevron-down\" id=\"toggleIcon\"><\/i><\/button><\/div><div class=\"history-body\" id=\"logList\">' + historyHtml + '<\/div><\/div>' : '') +\n            '<p class=\"foot\">' + (force ? '本次为强制更新，请更新后再使用书源' : '更新后返回书源即可生效') + '<\/p>' +\n            '<\/section><\/div>' +\n            '<script>' +\n            'let logsCollapsed=true;' +\n            'function toggleLogs(){logsCollapsed=!logsCollapsed;var l=document.getElementById(\"logList\");var t=document.getElementById(\"toggleText\");var i=document.getElementById(\"toggleIcon\");' +\n            'if(logsCollapsed){l.classList.remove(\"open\");t.textContent=\"展开历史\";i.className=\"fas fa-chevron-down\";}else{l.classList.add(\"open\");t.textContent=\"收起历史\";i.className=\"fas fa-chevron-up\";}}' +\n            '<\/script><\/body><\/html>';\n\n        if (downloadUrl) {\n            try {\n                java.startBrowserAwait('data:text\/html;base64,' + java.base64Encode(bodyHtml), '书源更新', false);\n            } catch (e) {\n                java.toast('打开更新页失败：' + e);\n            }\n        } else {\n            java.longToast('发现新版本：' + serverVer + '\\n' + msg);\n        }\n        return;\n    }\n\n    if (cmp < 0) {\n        java.longToast('非法版本\\n本地版本：' + myLocalVer + '\\n服务器版本：' + serverVer + '\\n本地版本高于服务器版本，请检查书源来源是否被修改。');\n        return;\n    }\n    java.toast('已是最新版本：' + myLocalVer);\n}\n\nfunction paraSettings() {\n    let { java } = this;\n    let conf = getConfig.call(this);\n    let curReview = conf.para_review || 'off';\n    let curStyle = conf.para_style || 'TEXT';\n    let curSy = conf.para_sy || 'off';\n    let curProgress = conf.progress_sync || 'off';\n    let curShelf = conf.shelf_sync || 'off';\n    let curStv = conf.stv_content || 'off';\n\n    var html = '<!DOCTYPE html><html lang=\"zh-CN\"><head>' +\n        '<meta charset=\"UTF-8\">' +\n        '<meta name=\"viewport\" content=\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no\">' +\n        '<title>书源设置<\/title>' +\n        '<link rel=\"stylesheet\" href=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/font-awesome\/6.4.0\/css\/all.min.css\">' +\n        '<style>' +\n        ':root{' +\n        '--bg-rgb:232,238,246;--surface-rgb:255,255,255;--text-rgb:31,38,51;--muted-rgb:108,120,140;' +\n        '--line-rgb:150,165,190;--primary-rgb:91,124,250;' +\n        '--bg:#e8eef6;--surface:#ffffff;--text:#1f2633;--muted:#6c788c;' +\n        '--primary:#5b7cfa;--primary-dark:#4363d8;' +\n        '--border:rgba(var(--line-rgb),.18);--primary-soft:rgba(var(--primary-rgb),.08);' +\n        '--radius-lg:20px;--radius:14px;' +\n        '--shadow:0 18px 40px -26px rgba(31,45,80,.35),0 2px 8px rgba(31,45,80,.05);' +\n        '--font-cn:\"PingFang SC\",\"Hiragino Sans GB\",\"Microsoft YaHei\",system-ui,-apple-system,sans-serif;' +\n        '}' +\n        '*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}' +\n        'html,body{min-height:100%}' +\n        'body{margin:0;min-height:100vh;color:var(--text);font-family:var(--font-cn);font-size:15px;line-height:1.6;' +\n        'background:var(--bg);padding:calc(14px + env(safe-area-inset-top)) 14px calc(22px + env(safe-area-inset-bottom));position:relative;overflow-x:hidden}' +\n        'body::before{content:\"\";position:fixed;inset:0;z-index:0;pointer-events:none;' +\n        'background:radial-gradient(900px 520px at 12% -8%,rgba(var(--primary-rgb),.16),transparent 68%),' +\n        'radial-gradient(760px 480px at 94% 4%,rgba(120,220,230,.18),transparent 72%)}' +\n        '.wrap{max-width:720px;margin:0 auto;position:relative;z-index:1}' +\n        '.glass{background:linear-gradient(155deg,rgba(var(--surface-rgb),.86),rgba(var(--surface-rgb),.58));' +\n        'border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow);' +\n        'backdrop-filter:blur(22px) saturate(1.2);-webkit-backdrop-filter:blur(22px) saturate(1.2)}' +\n        '.hero{display:flex;align-items:center;gap:14px;padding:17px 18px;margin-bottom:12px}' +\n        '.hero-icon{flex:0 0 auto;width:46px;height:46px;border-radius:14px;display:flex;align-items:center;justify-content:center;font-size:20px;' +\n        'background:linear-gradient(135deg,var(--primary),var(--primary-dark));color:#fff}' +\n        '.hero h1{margin:0;font-size:19px;line-height:1.25}' +\n        '.hero p{margin:5px 0 0;font-size:12px;line-height:1.55;color:var(--muted)}' +\n        '.card{padding:16px 18px;margin-bottom:12px}' +\n        '.group-title{font-size:12px;font-weight:700;color:var(--muted);letter-spacing:1px;padding:4px 2px 8px}' +\n        '.row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 0}' +\n        '.row + .row{border-top:1px solid var(--border)}' +\n        '.row .label{font-size:14px;font-weight:600}' +\n        '.row .desc{font-size:11.5px;color:var(--muted);margin-top:2px;line-height:1.5}' +\n        '.switch{position:relative;width:52px;height:28px;flex:none;cursor:pointer}' +\n        '.switch input{opacity:0;width:0;height:0}' +\n        '.switch-slider{position:absolute;inset:0;background:rgba(var(--line-rgb),.35);border-radius:28px;transition:.3s;border:1px solid var(--border)}' +\n        '.switch-slider::before{content:\"\";position:absolute;width:22px;height:22px;left:2px;bottom:2px;background:#fff;border-radius:50%;transition:.3s;box-shadow:0 2px 8px rgba(0,0,0,.3)}' +\n        '.switch input:checked + .switch-slider{background:var(--primary);border-color:var(--primary)}' +\n        '.switch input:checked + .switch-slider::before{transform:translateX(24px)}' +\n        '.seg{display:flex;gap:6px;padding:5px;border-radius:14px;background:rgba(var(--surface-rgb),.6);border:1px solid var(--border);flex:none}' +\n        '.seg button{border:none;background:transparent;font:inherit;font-size:13px;font-weight:600;padding:8px 16px;border-radius:10px;cursor:pointer;color:var(--muted);transition:.18s}' +\n        '.seg button.on{background:linear-gradient(135deg,var(--primary),var(--primary-dark));color:#fff;box-shadow:0 6px 16px -10px rgba(var(--primary-rgb),.9)}' +\n        '.tip{padding:12px 16px;border-radius:14px;font-size:12px;line-height:1.6;color:var(--muted)}' +\n        '.tip b{color:var(--text)}' +\n        '<\/style><\/head><body><div class=\"wrap\">' +\n        '<header class=\"hero glass\"><div class=\"hero-icon\"><i class=\"fa-solid fa-sliders\"><\/i><\/div><div><h1>书源设置<\/h1><p>段评、进度同步、书架同步、STV补全<\/p><\/div><\/header>' +\n\n        \/\/ 段评\n        '<section class=\"card glass\">' +\n        '<div class=\"group-title\">段评<\/div>' +\n        '<div class=\"row\"><div><div class=\"label\">段评开关<\/div><div class=\"desc\">仅番茄小说、QQ阅读、晋江、七猫、少年梦丶书旗支持<\/div><\/div>' +\n        '<label class=\"switch\"><input type=\"checkbox\" id=\"reviewSwitch\" ' + (curReview == 'on' ? 'checked' : '') + '><span class=\"switch-slider\"><\/span><\/label><\/div>' +\n        '<div class=\"row\"><div><div class=\"label\">段评图标大小<\/div><div class=\"desc\">TEXT 大号 \/ text 小号<\/div><\/div>' +\n        '<div class=\"seg\" id=\"styleSeg\">' +\n        '<button type=\"button\" data-val=\"TEXT\" class=\"' + (curStyle == 'TEXT' ? 'on' : '') + '\">TEXT（大）<\/button>' +\n        '<button type=\"button\" data-val=\"text\" class=\"' + (curStyle == 'text' ? 'on' : '') + '\">text（小）<\/button>' +\n        '<\/div><\/div>' +\n        '<div class=\"row\"><div><div class=\"label\">源阅适配<\/div><div class=\"desc\">IOS源阅适配，其他IOS应用段评异常也可尝试开启<\/div><\/div>' +\n        '<label class=\"switch\"><input type=\"checkbox\" id=\"sySwitch\" ' + (curSy == 'on' ? 'checked' : '') + '><span class=\"switch-slider\"><\/span><\/label><\/div>' +\n        '<\/section>' +\n\n        \/\/ 同步\n        '<section class=\"card glass\">' +\n        '<div class=\"group-title\">同步<\/div>' +\n        '<div class=\"row\"><div><div class=\"label\">进度同步<\/div><div class=\"desc\">翻章时上报晋江 \/ 番茄系列阅读进度<\/div><\/div>' +\n        '<label class=\"switch\"><input type=\"checkbox\" id=\"progressSwitch\" ' + (curProgress == 'on' ? 'checked' : '') + '><span class=\"switch-slider\"><\/span><\/label><\/div>' +\n        '<div class=\"row\"><div><div class=\"label\">书架同步<\/div><div class=\"desc\">加入\/移除书架时同步到晋江 \/ 番茄APP<\/div><\/div>' +\n        '<label class=\"switch\"><input type=\"checkbox\" id=\"shelfSwitch\" ' + (curShelf == 'on' ? 'checked' : '') + '><span class=\"switch-slider\"><\/span><\/label><\/div>' +\n        '<\/section>' +\n\n        \/\/ STV\n        '<section class=\"card glass\">' +\n        '<div class=\"group-title\">STV<\/div>' +\n        '<div class=\"row\"><div><div class=\"label\">STV正文补全<\/div><div class=\"desc\">晋江章节在聚合接口无内容时，改用STV接口取正文（需自备账号并登录STV）<\/div><\/div>' +\n        '<label class=\"switch\"><input type=\"checkbox\" id=\"stvSwitch\" ' + (curStv == 'on' ? 'checked' : '') + '><span class=\"switch-slider\"><\/span><\/label><\/div>' +\n        '<\/section>' +\n\n        '<div class=\"tip glass\">当前：段评 <b id=\"curReview\">' + (curReview == 'on' ? '开启' : '关闭') + '<\/b> · 图标 <b id=\"curStyle\">' + curStyle + '<\/b> · 源阅适配 <b id=\"curSy\">' + (curSy == 'on' ? '开启' : '关闭') + '<\/b>' +\n        ' · 进度同步 <b id=\"curProgress\">' + (curProgress == 'on' ? '开启' : '关闭') + '<\/b> · 书架同步 <b id=\"curShelf\">' + (curShelf == 'on' ? '开启' : '关闭') + '<\/b>' +\n        ' · STV正文 <b id=\"curStv\">' + (curStv == 'on' ? '开启' : '关闭') + '<\/b><\/div>' +\n        '<\/div>' +\n        '<div id=\"result-output\" style=\"display:none;\">' + curReview + '|' + curStyle + '|' + curSy + '|' + curProgress + '|' + curShelf + '|' + curStv + '<\/div>' +\n        '<script>' +\n        'var out=document.getElementById(\"result-output\");' +\n        'function sync(){var r=document.getElementById(\"reviewSwitch\").checked?\"on\":\"off\";' +\n        'var s=document.querySelector(\"#styleSeg button.on\").getAttribute(\"data-val\");' +\n        'var y=document.getElementById(\"sySwitch\").checked?\"on\":\"off\";' +\n        'var p=document.getElementById(\"progressSwitch\").checked?\"on\":\"off\";' +\n        'var b=document.getElementById(\"shelfSwitch\").checked?\"on\":\"off\";' +\n        'var t=document.getElementById(\"stvSwitch\").checked?\"on\":\"off\";' +\n        'out.textContent=r+\"|\"+s+\"|\"+y+\"|\"+p+\"|\"+b+\"|\"+t;' +\n        'document.getElementById(\"curReview\").textContent=r==\"on\"?\"开启\":\"关闭\";' +\n        'document.getElementById(\"curStyle\").textContent=s;' +\n        'document.getElementById(\"curSy\").textContent=y==\"on\"?\"开启\":\"关闭\";' +\n        'document.getElementById(\"curProgress\").textContent=p==\"on\"?\"开启\":\"关闭\";' +\n        'document.getElementById(\"curShelf\").textContent=b==\"on\"?\"开启\":\"关闭\";' +\n        'document.getElementById(\"curStv\").textContent=t==\"on\"?\"开启\":\"关闭\";}' +\n        '[\"reviewSwitch\",\"sySwitch\",\"progressSwitch\",\"shelfSwitch\",\"stvSwitch\"].forEach(function(id){document.getElementById(id).addEventListener(\"change\",sync);});' +\n        'document.querySelectorAll(\"#styleSeg button\").forEach(function(btn){btn.addEventListener(\"click\",function(){' +\n        'document.querySelectorAll(\"#styleSeg button\").forEach(function(x){x.classList.remove(\"on\");});' +\n        'btn.classList.add(\"on\");sync();});});' +\n        'sync();' +\n        '<\/script><\/body><\/html>';\n\n    var choice = java.startBrowserAwait('data:text\/html;base64,' + java.base64Encode(html), '书源设置', false);\n    if (!choice || !choice.body()) { java.toast('已取消书源设置'); return; }\n\n    var body = choice.body();\n    var m = body.match(\/<div id=\"result-output\"[^>]*>([^<]*)<\\\/div>\/);\n    var raw = (m && m[1]) ? m[1].trim() : '';\n    var parts = raw.split('|');\n    var review = parts[0] == 'on' ? 'on' : 'off';\n    var style = parts[1] == 'text' ? 'text' : 'TEXT';\n    var sy = parts[2] == 'on' ? 'on' : 'off';\n    var progress = parts[3] == 'on' ? 'on' : 'off';\n    var shelf = parts[4] == 'on' ? 'on' : 'off';\n    var stv = parts[5] == 'on' ? 'on' : 'off';\n\n    setConfig.call(this, {\n        para_review: review,\n        para_style: style,\n        para_sy: sy,\n        progress_sync: progress,\n        shelf_sync: shelf,\n        stv_content: stv\n    });\n    java.toast('已保存：段评' + (review == 'on' ? '开启' : '关闭') +\n        ' · 进度同步' + (progress == 'on' ? '开启' : '关闭') +\n        ' · 书架同步' + (shelf == 'on' ? '开启' : '关闭') +\n        ' · STV正文' + (stv == 'on' ? '开启' : '关闭'));\n}\n\nfunction FqFilter() {\n    let { java, source } = this;\n    try {\n        var cfg = getConfig.call(this);\n        var savedParams = cfg.fq_filter || \"\";\n\n        var config = {\n            lengths: [\"10万\",\"30万\",\"50万\",\"30万以上\",\"50万以上\",\"100万以上\",\"200万以上\",\"300万以上\",\"500万以上\"],\n            statuses: [\"完结\",\"连载\",\"半年完结\",\"3天更新\",\"7天更新\",\"1月更新\"],\n            sorts: [\"热门\",\"新书\",\"评分\",\"字数\"]\n        };\n\n        function buildGroup(title, icon, items, category) {\n            var h = '<div class=\"section\">';\n            h += '<div class=\"section-title\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">' + icon + '<\/svg><span>' + title + '<\/span><span class=\"tag\">单选<\/span><\/div>';\n            h += '<div class=\"grid\">';\n            items.forEach(function(name) {\n                h += '<div class=\"opt-card\" data-cat=\"' + category + '\" data-val=\"' + name + '\" onclick=\"pick(this)\"><div class=\"opt-name\">' + name + '<\/div><\/div>';\n            });\n            h += '<\/div><\/div>';\n            return h;\n        }\n\n        var icons = {\n            length: '<path d=\"M3 6h18M3 12h18M3 18h12\"\/>',\n            status: '<polyline points=\"22 12 18 12 15 21 9 3 6 12 2 12\"\/>',\n            sort: '<path d=\"M3 6h13M3 12h9M3 18h5\"\/><polyline points=\"17 10 21 14 17 18\"\/>'\n        };\n\n        var html = `<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\n<title>番茄筛选<\/title>\n<style>\n:root{--bg:#0a0a0f;--text:#e8e8f0;--accent:#ff6b35;--accent-glow:rgba(255,107,53,.4);--gold:#f0c040}\n*{margin:0;padding:0;box-sizing:border-box}\nbody{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei',sans-serif;background:var(--bg);background-image:radial-gradient(ellipse at 30% 20%,#2a1205 0%,#0a0e27 50%,#1a0d1a 100%);color:var(--text);min-height:100vh;padding:20px;position:relative;overflow-x:hidden}\n.stars{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0}\n.star{position:absolute;border-radius:50%;background:#fff;animation:twinkle var(--duration) ease-in-out infinite alternate;animation-delay:var(--delay)}\n@keyframes twinkle{0%{opacity:.1;transform:scale(.8)}50%{opacity:.9;transform:scale(1.1)}100%{opacity:.2;transform:scale(.9)}}\n.container{width:100%;max-width:640px;margin:0 auto;position:relative;z-index:10;background:rgba(15,12,20,.6);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border-radius:24px;border:1px solid rgba(255,255,255,.08);box-shadow:0 20px 60px rgba(0,0,0,.6),0 0 60px rgba(255,107,53,.1);overflow:hidden;animation:cardIn .6s cubic-bezier(.22,.61,.36,1)}\n@keyframes cardIn{from{opacity:0;transform:translateY(20px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}\n.header{padding:28px 24px 20px;text-align:center;border-bottom:1px solid rgba(255,255,255,.06);background:linear-gradient(180deg,rgba(255,107,53,.12),transparent)}\n.header-title{display:flex;align-items:center;justify-content:center;gap:10px}\n.header-title svg{width:22px;height:22px;color:var(--accent);filter:drop-shadow(0 0 8px var(--accent-glow))}\n.header h1{font-size:22px;font-weight:700;letter-spacing:1px;color:rgba(255,255,255,.95);text-shadow:0 0 24px rgba(255,107,53,.5)}\n.header p{font-size:12px;color:rgba(255,255,255,.45);margin-top:6px;letter-spacing:2px}\n.current-badge{display:flex;align-items:center;justify-content:center;gap:8px;margin:20px 24px 0;padding:12px 20px;background:rgba(255,107,53,.12);border:1px solid rgba(255,107,53,.3);border-radius:14px;font-size:13px;color:rgba(255,255,255,.7)}\n.current-badge .dot{width:8px;height:8px;border-radius:50%;background:var(--accent);box-shadow:0 0 10px var(--accent-glow);animation:pulse 2s ease-in-out infinite}\n@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.6;transform:scale(1.2)}}\n.current-badge strong{color:#fff;font-weight:600}\n.section{padding:20px 24px 0}\n.section:last-of-type{padding-bottom:0}\n.section-title{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600;color:rgba(255,255,255,.85);margin-bottom:12px;letter-spacing:.5px}\n.section-title svg{width:16px;height:16px;color:var(--accent);filter:drop-shadow(0 0 6px var(--accent-glow))}\n.tag{font-size:10px;padding:2px 8px;border-radius:10px;background:rgba(255,107,53,.15);border:1px solid rgba(255,107,53,.3);color:rgba(255,255,255,.6);font-weight:400;margin-left:auto}\n.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}\n.opt-card{position:relative;padding:12px 6px;border-radius:14px;background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.08);cursor:pointer;transition:all .3s cubic-bezier(.22,.61,.36,1);text-align:center;overflow:hidden}\n.opt-card:hover{border-color:rgba(255,255,255,.2);background:rgba(255,255,255,.06);transform:translateY(-2px);box-shadow:0 8px 24px rgba(0,0,0,.3)}\n.opt-card.selected{border-color:var(--accent);background:rgba(255,107,53,.18);box-shadow:0 0 20px rgba(255,107,53,.3)}\n.opt-card.selected::after{content:'✓';position:absolute;top:5px;right:6px;width:16px;height:16px;border-radius:50%;background:var(--accent);color:#fff;font-size:10px;display:flex;align-items:center;justify-content:center;box-shadow:0 0 8px var(--accent-glow)}\n.opt-name{font-size:12px;font-weight:500;color:rgba(255,255,255,.85);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:0 4px}\n.opt-card.selected .opt-name{color:#fff}\n.clear-area{padding:20px 24px 0}\n.clear-btn{width:100%;padding:12px;border:1px dashed rgba(255,255,255,.25);border-radius:14px;background:transparent;font-size:13px;color:rgba(255,255,255,.5);cursor:pointer;transition:all .3s;display:flex;align-items:center;justify-content:center;gap:8px}\n.clear-btn:hover{border-color:rgba(255,107,53,.6);color:var(--accent);background:rgba(255,107,53,.06)}\n.clear-btn svg{width:14px;height:14px}\n.footer{text-align:center;font-size:11px;color:rgba(255,255,255,.3);padding:16px 24px 22px;letter-spacing:1px;border-top:1px solid rgba(255,255,255,.05);margin-top:20px}\n@media (max-width:480px){.grid{grid-template-columns:repeat(3,1fr);gap:8px}.header h1{font-size:20px}.section{padding:16px 16px 0}.clear-area{padding:16px 16px 0}}\n<\/style>\n<\/head>\n<body>\n<div class=\"stars\" id=\"stars\"><\/div>\n<div class=\"container\">\n<div class=\"header\">\n<div class=\"header-title\">\n<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3\"\/><\/svg>\n<h1>番茄筛选<\/h1>\n<\/div>\n<p>选择后点击右上角 ✓ ，并刷新发现页生效<\/p>\n<\/div>\n<div class=\"current-badge\">\n<span class=\"dot\"><\/span>\n当前：<strong id=\"current-value\">` + (savedParams || \"未选择\") + `<\/strong>\n<\/div>\n` + buildGroup(\"字数参数\", icons.length, config.lengths, \"length\") +\n     buildGroup(\"状态参数\", icons.status, config.statuses, \"status\") +\n     buildGroup(\"排序参数\", icons.sort, config.sorts, \"sort\") + `\n<div class=\"clear-area\">\n<button onclick=\"clearAll()\" class=\"clear-btn\">\n<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"3 6 5 6 21 6\"\/><path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\"\/><\/svg>\n清除所有选择\n<\/button>\n<\/div>\n<div class=\"footer\">仅番茄发现页生效<\/div>\n<\/div>\n<div id=\"result-output\" style=\"display:none;\">` + savedParams + `<\/div>\n<script>\nvar config = ` + JSON.stringify(config) + `;\nvar selection = { length: null, status: null, sort: null };\nvar initParams = document.getElementById('result-output').textContent.trim();\nif (initParams && initParams !== \"未选择\" && initParams !== \"undefined\") {\n    initParams.split(\", \").forEach(function(item) {\n        if (config.lengths.indexOf(item) >= 0) selection.length = item;\n        else if (config.statuses.indexOf(item) >= 0) selection.status = item;\n        else if (config.sorts.indexOf(item) >= 0) selection.sort = item;\n    });\n}\ndocument.querySelectorAll('.opt-card').forEach(function(el) {\n    var cat = el.getAttribute('data-cat');\n    var val = el.getAttribute('data-val');\n    if (selection[cat] == val) el.classList.add('selected');\n});\nfunction pick(el) {\n    var cat = el.getAttribute('data-cat');\n    var val = el.getAttribute('data-val');\n    if (selection[cat] == val) {\n        selection[cat] = null;\n        el.classList.remove('selected');\n    } else {\n        selection[cat] = val;\n        document.querySelectorAll('.opt-card[data-cat=\"' + cat + '\"]').forEach(function(c) {\n            c.classList.remove('selected');\n        });\n        el.classList.add('selected');\n    }\n    updateOutput();\n}\nfunction updateOutput() {\n    var all = [];\n    for (var k in selection) { if (selection[k]) all.push(selection[k]); }\n    var text = all.length > 0 ? all.join(\", \") : \"未选择\";\n    document.getElementById('current-value').textContent = text;\n    document.getElementById('result-output').textContent = all.length > 0 ? all.join(\", \") : \"\";\n}\nfunction clearAll() {\n    selection = { length: null, status: null, sort: null };\n    document.querySelectorAll('.opt-card').forEach(function(el) { el.classList.remove('selected'); });\n    updateOutput();\n}\n(function() {\n    var c = document.getElementById('stars');\n    for (var i = 0; i < 60; i++) {\n        var s = document.createElement('div');\n        s.className = 'star';\n        var size = Math.random() < 0.7 ? 1 : (Math.random() < 0.8 ? 2 : 3);\n        s.style.width = size + 'px';\n        s.style.height = size + 'px';\n        s.style.left = Math.random() * 100 + '%';\n        s.style.top = Math.random() * 100 + '%';\n        s.style.setProperty('--duration', (1.5 + Math.random() * 3) + 's');\n        s.style.setProperty('--delay', (Math.random() * 4) + 's');\n        s.style.opacity = 0.2 + Math.random() * 0.6;\n        if (size == 3) s.style.boxShadow = '0 0 6px rgba(255,255,255,0.5)';\n        c.appendChild(s);\n    }\n})();\n<\/script>\n<\/body>\n<\/html>`;\n\n        var base64 = \"data:text\/html;base64,\" + java.base64Encode(html);\n        var result = java.startBrowserAwait(base64, \"番茄筛选\", false);\n\n        if (!result || !result.body()) {\n            java.toast(\"已取消筛选设置\");\n            return;\n        }\n\n        var body = result.body();\n        var resultDivStart = body.indexOf('<div id=\"result-output\"');\n        var selectedItems = \"\";\n\n        if (resultDivStart > -1) {\n            var contentStart = body.indexOf('>', resultDivStart);\n            if (contentStart > -1) {\n                contentStart += 1;\n                var contentEnd = body.indexOf('<\/div>', contentStart);\n                if (contentEnd > contentStart) {\n                    selectedItems = body.substring(contentStart, contentEnd).trim();\n                    if (selectedItems == \"未选择\") {\n                        selectedItems = \"\";\n                    } else {\n                        if (selectedItems.startsWith('\">')) selectedItems = selectedItems.substring(2);\n                        else if (selectedItems.startsWith('>')) selectedItems = selectedItems.substring(1);\n                    }\n                }\n            }\n        }\n\n        setConfig.call(this, { fq_filter: selectedItems });\n\n    if (selectedItems == \"\") {\n        java.toast(\"已清除所有筛选条件\");\n    } else {\n        java.toast(\"设置成功，仅番茄发现页生效\\n当前：\" + selectedItems);\n    }\n        } catch (e) {\n            java.log(e);\n            java.toast(\"已取消筛选设置\");\n    }\n}\n\nfunction encode(data) {\n    let { java } = this;\n    return java.createSymmetricCrypto(\"DES\/CBC\/PKCS5Padding\", String(\"KW8Dvm2N\"), String(\"1ae2c94b\")).encryptBase64(data);\n}\n\nfunction decode(data) {\n    let { java } = this;\n    try {\n        var encryptedBytes = java.base64DecodeToByteArray(data);\n        var key = \"F#kL9$mQpR2&sVzX\";\n        var iv = \"7HnT3!cW8@xM0yBv\";\n        var crypto = java.createSymmetricCrypto(\"AES\/CBC\/PKCS5Padding\", key, iv);\n        return crypto.decryptStr(encryptedBytes);\n    } catch (e) {\n        return data;\n    }\n}\n\nfunction getStvCookie() {\n    let c = getConfig.call(this);\n    return String(c.stv_cookie || '').trim();\n}","lastUpdateTime":"1794356831195","loginUi":"[\n  {\n    \"name\": \"邮箱\",\n    \"type\": \"text\"\n  },\n  {\n    \"name\": \"密码\",\n    \"type\": \"password\"\n  },\n  {\n    \"name\": \"⭕账号登录\",\n    \"type\": \"button\",\n    \"action\": \"login(true)\",\n    \"style\": {\n      \"layout_flexGrow\": 1,\n      \"layout_flexBasisPercent\": 0.45\n    }\n  },\n  {\n    \"name\": \"🔑账号注册\",\n    \"type\": \"button\",\n    \"action\": \"register()\",\n    \"style\": {\n      \"layout_flexGrow\": 1,\n      \"layout_flexBasisPercent\": 0.45\n    }\n  },\n  {\n    \"name\": \"❌退出登录\",\n    \"type\": \"button\",\n    \"action\": \"logout()\",\n    \"style\": {\n      \"layout_flexGrow\": 1,\n      \"layout_flexBasisPercent\": 0.45\n    }\n  },\n  {\n    \"name\": \"🔅用户中心\",\n    \"type\": \"button\",\n    \"action\": \"user()\",\n    \"style\": {\n      \"layout_flexGrow\": 1,\n      \"layout_flexBasisPercent\": 0.45\n    }\n  },\n  {\n    \"name\": \"📡源站选择\",\n    \"type\": \"button\",\n    \"action\": \"selectSource()\",\n    \"style\": {\n      \"layout_flexGrow\": 1,\n      \"layout_flexBasisPercent\": 1\n    }\n  },\n  {\n    \"name\": \"🎚服务器切换\",\n    \"type\": \"button\",\n    \"action\": \"switchServer()\",\n    \"style\": {\n      \"layout_flexGrow\": 1,\n      \"layout_flexBasisPercent\": 0.45\n    }\n  },\n  {\n    \"name\": \"🔄服务器检测\",\n    \"type\": \"button\",\n    \"action\": \"checkServer()\",\n    \"style\": {\n      \"layout_flexGrow\": 1,\n      \"layout_flexBasisPercent\": 0.45\n    }\n  },\n  {\n    \"name\": \"🎁书源设置\",\n    \"type\": \"button\",\n    \"action\": \"paraSettings()\",\n    \"style\": {\n      \"layout_flexGrow\": 1,\n      \"layout_flexBasisPercent\": 0.45\n    }\n  },\n  {\n    \"name\": \"🌟升级用户组\",\n    \"type\": \"button\",\n    \"action\": \"level()\",\n    \"style\": {\n      \"layout_flexGrow\": 1,\n      \"layout_flexBasisPercent\": 0.45\n    }\n  },\n  {\n    \"name\": \"🫧我的气泡\",\n    \"type\": \"button\",\n    \"action\": \"icons()\",\n    \"style\": {\n      \"layout_flexGrow\": 1,\n      \"layout_flexBasisPercent\": 0.45\n    }\n  },\n  {\n    \"name\": \"🔄更新检测\",\n    \"type\": \"button\",\n    \"action\": \"checkVersion()\",\n    \"style\": {\n      \"layout_flexGrow\": 1,\n      \"layout_flexBasisPercent\": 0.45\n    }\n  },\n  {\n    \"name\": \"📓晋江收藏分类\",\n    \"type\": \"button\",\n    \"action\": \"getJjShelfClass()\",\n    \"style\": {\n        \"layout_flexGrow\": 1,\n        \"layout_flexBasisPercent\": 1\n    }\n  },\n  {\n    \"name\": \"🍅番茄筛选\",\n    \"type\": \"button\",\n    \"action\": \"FqFilter()\",\n    \"style\": { \n    \t\"layout_flexGrow\": 1,\n    \t \"layout_flexBasisPercent\": 0.45 \n    \t }\n  },\n  {\n    \"name\": \"🔄强制刷新\",\n    \"type\": \"button\",\n    \"action\": \"toggleRefresh()\",\n    \"style\": {\n      \"layout_flexGrow\": 1,\n      \"layout_flexBasisPercent\": 0.45\n    }\n},\n  {\n    \"name\": \"🍅番茄登录\",\n    \"type\": \"button\",\n    \"action\": \"fq_login()\",\n    \"style\": {\n      \"layout_flexGrow\": 1,\n      \"layout_flexBasisPercent\": 0.45\n    }\n  },\n  {\n    \"name\": \"❌退出番茄\",\n    \"type\": \"button\",\n    \"action\": \"fq_logout()\",\n    \"style\": {\n      \"layout_flexGrow\": 1,\n      \"layout_flexBasisPercent\": 0.45\n    }\n  },\n  {\n    \"name\": \"晋江登录Token\",\n    \"type\": \"password\"\n  },\n  {\n    \"name\": \"📱晋江扫码登录\",\n    \"type\": \"button\",\n    \"action\": \"loginJjScan()\",\n    \"style\": {\n      \"layout_flexGrow\": 1,\n      \"layout_flexBasisPercent\": 0.45\n    }\n},\n{\n    \"name\": \"❌退出晋江\",\n    \"type\": \"button\",\n    \"action\": \"logoutJj()\",\n    \"style\": {\n      \"layout_flexGrow\": 1,\n      \"layout_flexBasisPercent\": 0.45\n    }\n},\n{\n    \"name\": \"STV账号\",\n    \"type\": \"text\"\n  },\n  {\n    \"name\": \"STV密码\",\n    \"type\": \"password\"\n  },\n  {\n    \"name\": \"🔐STV登录\",\n    \"type\": \"button\",\n    \"action\": \"stvLogin()\",\n    \"style\": { \"layout_flexGrow\": 1, \"layout_flexBasisPercent\": 0.45 }\n},\n{\n    \"name\": \"❌退出STV\",\n    \"type\": \"button\",\n    \"action\": \"stvLogout()\",\n    \"style\": { \"layout_flexGrow\": 1, \"layout_flexBasisPercent\": 0.45 }\n},\n  {\n    \"name\": \"🔀STV线路切换\",\n    \"type\": \"button\",\n    \"action\": \"stvSwitchHost()\",\n    \"style\": { \"layout_flexGrow\": 1, \"layout_flexBasisPercent\": 0.45 }\n},\n{\n    \"name\": \"📡当前STV线路\",\n    \"type\": \"button\",\n    \"action\": \"stvShowHost()\",\n    \"style\": { \"layout_flexGrow\": 1, \"layout_flexBasisPercent\": 0.45 }\n}\n]","loginUrl":"function login(flag) {\n    let email = '', pwd = '';\n    try {\n        if (flag == true) {\n            email = result.get('邮箱') || '';\n            pwd = result.get('密码') || '';\n        } else {\n            let info = source.getLoginInfoMap() || {};\n            email = info.get('邮箱') || '';\n            pwd = info.get('密码') || '';\n        }\n    } catch (e) {}\n\n    email = String(email || '').trim();\n    pwd = String(pwd || '').trim();\n    if (!email || !pwd) return java.toast('请先填写邮箱和密码');\n\n    java.toast('正在登录…');\n    try {\n        let opt = { method: 'POST', body: JSON.stringify({ email: email, password: pwd }) };\n        let res = java.ajax(getHost.call(this) + '\/api\/login,' + JSON.stringify(opt));\n        let obj = null;\n        try { obj = JSON.parse(res); } catch (e) { obj = null; }\n\n        if (!obj || obj.code !== 0 || !obj.data || !obj.data.token) {\n            let msg = (obj && obj.message) || '未知错误';\n            return java.toast('\\n❌ 登录失败：' + msg);\n        }\n\n        let d = obj.data;\n        let u = d.user || {};\n        let g = d.group || {};\n        let m = d.membership || {};\n        let benefits = d.benefits || m.benefits || g.benefits || '';\n\n        source.putLoginHeader(d.token);\n\n        let name = u.nickname || u.email || email;\n        let level = g.level || m.level || 1;\n        let groupName = g.name || m.group_name || '普通用户';\n        let isMember = !!(u.is_member || m.is_member);\n        let expires = m.expires_at || d.member_expires_at || '';\n        let masked = d.api_token_masked || '';\n        let rateWin = g.rate_limit_window || '';\n        let rateMax = g.rate_limit_max || '';\n        let devLimit = g.device_limit || '';\n        let resetLimit = g.token_reset_limit || '';\n        let resetDays = g.token_reset_window_days || '';\n\n        let lines = [];\n        lines.push('✅ 登录成功');\n        lines.push('');\n        lines.push('👤 用户：' + name);\n        if (u.email) lines.push('📧 邮箱：' + u.email);\n        lines.push('🏅 等级：Lv' + level + ' · ' + groupName + (isMember ? '（会员）' : '（普通）'));\n        if (expires) lines.push('⏰ 会员到期：' + expires);\n        if (masked) lines.push('🔑 Token：' + masked);\n        if (u.last_login_at) lines.push('🕒 上次登录：' + u.last_login_at);\n        if (rateWin && rateMax) lines.push('🚦 限流：' + rateWin + '秒内最多 ' + rateMax + ' 次');\n        if (devLimit) lines.push('📱 设备上限：' + devLimit + ' 台');\n        if (resetLimit && resetDays) lines.push('🔄 Token重置：每 ' + resetDays + ' 天 ' + resetLimit + ' 次');\n        if (benefits) {\n            lines.push('');\n            lines.push('💡 ' + benefits);\n        }\n\n        java.longToast('\\n' + lines.join('\\n'));\n    } catch (e) {\n        java.toast('\\n❌ 登录失败：' + e);\n    }\n}\n\nfunction logout() {\n    try { source.removeLoginHeader(); } catch (e) {}\n    try { source.removeLoginInfo(); } catch (e) {}\n    java.toast('已退出登录');\n}\n\nfunction fq_login() {\n    try {\n        java.startBrowserAwait(\"https:\/\/fanqienovel.com\/main\/writer\/login\", \"登录\");\n    } catch (e) {\n        java.toast(e);\n        return false;\n    }\n\n    try {\n        cookie.removeCookie(\"snssdk.com\");\n    } catch (e) {}\n\n    let sessionId = getSessionIdFromCookie.call(this);\n    if (!sessionId) sessionId = getSessionId.call(this);\n\n    if (!sessionId || String(sessionId).trim() == \"\") {\n        java.toast(\"未获取到登录凭据，登录失败\");\n        return false;\n    }\n\n    let user = \"\";\n    try {\n        let resp = java.ajax(\"https:\/\/fanqienovel.com\/api\/user\/info\/v2,\" + JSON.stringify({\n            method: \"GET\",\n            headers: { \"Cookie\": \"sessionid=\" + sessionId }\n        }));\n        user = JSON.parse(resp)?.data?.name || \"\";\n    } catch (e) {\n        java.log(e);\n    }\n\n    if (!user) {\n        java.toast(\"登录凭据无效或已过期，登录失败\");\n        return false;\n    }\n\n    setConfig.call(this, { fanqie_sessionid: sessionId });\n\n    java.toast(\"\\n\\n欢迎 \" + user + \"\\n登录成功！\");\n    return true;\n}\n\nfunction fq_logout() {\n    try { cookie.removeCookie(\"fanqienovel.com\"); } catch (e) {}\n    try { cookie.removeCookie(\"snssdk.com\"); } catch (e) {}\n    setConfig.call(this, { fanqie_sessionid: \"\" });\n    java.toast('已退出番茄登录');\n}\n\nfunction loginJjScan() {\n    let { java } = this;\n    try {\n        let headers = buildJjHeaders.call(this);\n        let e = java.ajax(\"https:\/\/android.jjwxc.com\/app.jjwxc\/android\/reading\/Login\/scanCodeLogin?versionCode=467,\" + JSON.stringify({ headers: headers }));\n        let eData = JSON.parse(e).data;\n        let jjscanurl = \"https:\/\/www.jjwxc.net?action=scanCode&jjreader_key=\" + eData.jjreader_key + \"&device_key=\" + eData.device_key;\n        let enjj = encodeURIComponent(jjscanurl);\n\n        let html = '<!DOCTYPE html><html><head><meta charset=\"UTF-8\">' +\n            '<meta name=\"viewport\" content=\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no\">' +\n            '<title>晋江扫码登录<\/title><style>' +\n            '*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}' +\n            'body{font-family:-apple-system,BlinkMacSystemFont,\"PingFang SC\",\"Microsoft YaHei\",sans-serif;' +\n            'background:#0a0a0f;background-image:radial-gradient(ellipse at 30% 20%,#2a1020 0%,#0a0e27 50%,#1a0d1a 100%);' +\n            'color:#e8e8f0;min-height:100vh;padding:20px;display:flex;align-items:center;justify-content:center}' +\n            '.container{width:100%;max-width:420px;background:rgba(15,12,20,.75);backdrop-filter:blur(20px);' +\n            'border-radius:24px;border:1px solid rgba(255,255,255,.08);box-shadow:0 20px 60px rgba(0,0,0,.6);overflow:hidden}' +\n            '.header{padding:24px 24px 18px;text-align:center;border-bottom:1px solid rgba(255,255,255,.06);' +\n            'background:linear-gradient(180deg,rgba(193,74,99,.15),transparent)}' +\n            '.header h1{font-size:20px;font-weight:700;letter-spacing:1px;background:linear-gradient(135deg,#e8e8f0 40%,#c14a63);' +\n            '-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}' +\n            '.header p{font-size:12px;color:rgba(255,255,255,.45);letter-spacing:2px;margin-top:4px}' +\n            '.content{padding:24px}.qr-wrapper{width:240px;height:240px;margin:0 auto 20px;border-radius:20px;' +\n            'background:#fff;padding:12px;box-shadow:0 8px 32px rgba(193,74,99,.25)}' +\n            '.qr-wrapper img{width:100%;height:100%;display:block;border-radius:12px}' +\n            '.steps{display:flex;flex-direction:column;gap:12px;margin-bottom:16px}' +\n            '.step{display:flex;align-items:flex-start;gap:10px;font-size:13px;color:rgba(255,255,255,.75);line-height:1.5}' +\n            '.step-num{flex-shrink:0;width:22px;height:22px;border-radius:50%;background:rgba(193,74,99,.2);' +\n            'border:1px solid rgba(193,74,99,.5);color:#c14a63;font-size:11px;font-weight:700;' +\n            'display:flex;align-items:center;justify-content:center;margin-top:1px}' +\n            '.tip{padding:12px 14px;background:rgba(240,192,64,.08);border:1px solid rgba(240,192,64,.25);' +\n            'border-radius:12px;font-size:12px;color:rgba(240,192,64,.95);line-height:1.5}' +\n            '.footer{padding:14px 24px 20px;text-align:center;font-size:11px;color:rgba(255,255,255,.3);' +\n            'border-top:1px solid rgba(255,255,255,.05);letter-spacing:1px}' +\n            '<\/style><\/head><body><div class=\"container\">' +\n            '<div class=\"header\"><h1>晋江扫码登录<\/h1><p>SCAN TO LOGIN<\/p><\/div>' +\n            '<div class=\"content\"><div class=\"qr-wrapper\"><img src=\"https:\/\/api.qrtool.cn\/?text=' + enjj + '\"><\/div>' +\n            '<div class=\"steps\">' +\n            '<div class=\"step\"><span class=\"step-num\">1<\/span><span>打开已登录晋江的手机 App<\/span><\/div>' +\n            '<div class=\"step\"><span class=\"step-num\">2<\/span><span>进入「我的」，点击右上角「扫码」图标<\/span><\/div>' +\n            '<div class=\"step\"><span class=\"step-num\">3<\/span><span>扫描上方二维码并确认授权<\/span><\/div>' +\n            '<div class=\"step\"><span class=\"step-num\">4<\/span><span>授权后，点击页面右上角 ✓ 完成登录<\/span><\/div>' +\n            '<\/div><div class=\"tip\">请先扫码授权，再点右上角 ✓。提前点击会导致二维码失效，需要重新扫码<\/div>' +\n            '<\/div><div class=\"footer\">晋江文学城 · 安全登录<\/div><\/div><\/body><\/html>';\n\n        let u = java.startBrowserAwait(\"data:text\/html;base64,\" + java.base64Encode(html), \"扫码登录\", false).body();\n\n        if (u) {\n            headers[\"Content-Type\"] = \"application\/octet-stream\";\n            let checkLogin = \"https:\/\/android.jjwxc.com\/app.jjwxc\/android\/reading\/Login\/checkJjreaderLogin\";\n            let body = jjEncode.call(this, JSON.stringify({\n                \"versionCode\": \"467\",\n                \"jjreader_key\": eData.jjreader_key,\n                \"device_key\": eData.device_key\n            }));\n            let checkLoginpost = {\n                \"method\": \"POST\",\n                \"body\": body,\n                \"headers\": headers\n            };\n            checkLogin = checkLogin + \",\" + JSON.stringify(checkLoginpost);\n            let t = java.ajax(checkLogin);\n            t = JSON.parse(t).data?.token ?? JSON.parse(t).message;\n\n            if (\/\\d+_\/.test(t)) {\n                setConfig.call(this, { jj_token: t });\n                java.toast(\"✅ 晋江登录成功\\nToken 已保存\");\n            } else {\n                java.toast(t);\n            }\n        }\n    } catch (e) {\n        java.log(e);\n        java.toast(\"扫码登录失败，请重试\");\n    }\n}\n\nfunction logoutJj() {\n    setConfig.call(this, { jj_token: \"\" });\n    java.toast(\"已退出晋江登录\");\n}\n\nfunction switchServer() {\n    let list = getHosts();\n    if (!list.length) { java.toast('未配置服务器'); return; }\n    let cur = getHost.call(this);\n    let idx = list.indexOf(cur);\n    let next = list[(idx + 1) % list.length];\n    setConfig.call(this, { host: next });\n    java.toast('已切换服务器：\\n' + next);\n}\n\nfunction checkServer() {\n    let cur = getHost.call(this);\n    java.toast('正在检测：\\n' + cur);\n    let t1 = Date.now();\n    try {\n        let opt = { headers: buildHeaders.call(this) };\n        let res = java.ajax(cur + '\/api\/site\/public,' + JSON.stringify(opt));\n        let ms = Date.now() - t1;\n        let obj = null;\n        try { obj = JSON.parse(res); } catch (e) { obj = null; }\n        let siteName = (obj.data && obj.data.site_name) ? String(obj.data.site_name) : '未知';\n        let ok = !!(obj.code == 0 && siteName.indexOf('听雪') >= 0);\n        let elapsed = (obj.meta && obj.meta.elapsedMs != null) ? obj.meta.elapsedMs : ms;\n        let level = elapsed < 800 ? '💖 优秀' : elapsed < 2000 ? '💛 一般' : '💔 较慢';\n        java.longToast(\n            '\\n服务器：' + cur + '\\n' +\n            '站点：' + siteName + (ok ? ' ✅' : ' ❌') + '\\n' +\n            '延迟：' + elapsed + 'ms（' + level + '）'\n        );\n    } catch (e) {\n        java.longToast('\\n服务器：' + cur + '\\n❌ 连接失败\\n' + e);\n    }\n}\n\nfunction toggleRefresh() {\n    let conf = getConfig.call(this);\n    let on = (conf.chapter_refresh_force || 'off') == 'on';\n    setConfig.call(this, { chapter_refresh_force: on ? 'off' : 'on' });\n    java.toast(on ? '\\n✔️已关闭强制刷新' : '\\n❗已开启强制刷新（绕过缓存）');\n}\n\nfunction showConfig() {\n    let c = getConfig.call(this);\n    let s = c.sources || {};\n    let lines = [];\n    ['novel','audio','comic','video','download'].forEach(function(t){\n        let arr = s[t] || [];\n        let label = {novel:'小说',audio:'听书',comic:'漫画',video:'短剧',download:'下载'}[t];\n        lines.push(label + '：' + (arr.length ? arr.join('、') : '全部'));\n    });\n    let msg = '当前配置：\\n' +\n        '服务器：' + getHost.call(this) + '\\n' +\n        lines.join('\\n') + '\\n' +\n        '性别：' + (c.gender == 'girl' ? '女频' : '男频') + '\\n' +\n        '账号：' + (getToken.call(this).b64 ? '已登录' : '未登录') + '\\n' +\n        '番茄：' + (getSessionId.call(this) ? '已登录' : '未登录');\n    java.longToast(msg);\n}\n\nfunction stvLogin() {\n    let { java, source } = this;\n    let info = source.getLoginInfoMap() || {};\n    let username = String(info.get('STV账号') || '').trim();\n    let psw = String(info.get('STV密码') || '').trim();\n    if (!username || !psw) return java.toast('请填写STV账号和密码');\n\n    let host = getStvHost.call(this);\n    java.toast('正在登录STV…');\n    try {\n        let body = 'username=' + encodeURIComponent(username) + '&psw=' + encodeURIComponent(psw);\n        let res = java.post(host + '\/index.php?ajax=login', body, {\n            'x-requested-with': 'com.sangtacviet.mobilereader',\n            'User-Agent': java.getWebViewUA()\n        });\n        let respBody = String(res.body() || '');\n        if (respBody.trim() != 'success') {\n            return java.toast('STV登录失败：' + respBody);\n        }\n        let sets = [];\n        try { sets = res.headers('set-cookie').toArray(); } catch (e) {}\n        let cookieParts = [];\n        for (let i = 0; i < sets.length; i++) {\n            let kv = String(sets[i]).split(';')[0].trim();\n            if (kv) cookieParts.push(kv);\n        }\n        let cookieStr = cookieParts.join('; ');\n        if (!cookieStr) return java.toast('STV登录成功但未取到cookie');\n        setConfig.call(this, { stv_cookie: cookieStr });\n        java.toast('✅ STV登录成功\\n线路：' + host + '\\nCookie：' + cookieStr);\n    } catch (e) {\n        java.toast('STV登录异常：' + e);\n    }\n}\n\nfunction stvLogout() {\n    setConfig.call(this, { stv_cookie: '' });\n    java.toast('已退出STV');\n}\n\nfunction stvSwitchHost() {\n    let { java } = this;\n    let next = stvNextHost.call(this);\n    java.toast('已切换STV线路：\\n' + next);\n}\n\nfunction stvShowHost() {\n    let { java } = this;\n    java.longToast('当前STV线路：\\n' + stvCurrentHostLabel.call(this));\n}\n\nfunction register() { java.startBrowserAwait(getHost.call(this) + '\/spa\/#\/register', \"注册\"); }\nfunction user() { java.startBrowserAwait(getHost.call(this) + '\/spa\/#\/member', \"用户中心\"); }\nfunction icons() { java.startBrowserAwait(getHost.call(this) + '\/spa\/#\/member\/icons', \"段评气泡\"); }\nfunction level() { java.startBrowserAwait('https:\/\/wzyp.cn\/shop\/FXB8NXCR', \"升级\"); }","respondTime":180000,"ruleBookInfo":{"author":"$.author","coverUrl":"$.cover","init":"<js>\nif (String(baseUrl).startsWith(\"data:\")) {\n    let res = JSON.parse(java.hexDecodeToString(result));\n    let request = getHost.call(this) + `\/api\/detail,${JSON.stringify({\n        method: \"POST\",\n        body: JSON.stringify(res),\n        headers: buildHeaders.call(this)\n    })}`;\n    result = java.ajax(request);\n}\nresult = decryptIfEncrypted.call(this, result);\nresult = checkMsg.call(this, result);\nresult;\n<\/js>\n$.data","intro":"&nbsp;&nbsp;\n✨ 源站：{{$.source}}{{\"\\n\"+\"​\"}}\n{{$.desc}}","kind":"$.tags","lastChapter":"{{$.source}} · {{$.latest_chapter_title}}","name":"$.title","tocUrl":"<js>\nlet catalog = {\n    source: result.source,\n    sing: result.sing,\n    book_id: result.book_id,\n    title: result.title,\n    tab: result.tab\n};\n\nlet yunurl = java.base64Encode(JSON.stringify(catalog));\nresult = `data:catalogUrl;base64,${yunurl},{\"type\":\"qingci\"}`;\n<\/js>","wordCount":"$.word_count"},"ruleContent":{"callBackJs":"let handled = false;\n\nif (event == 'addBookShelf' || event == 'delBookShelf') {\n    if (isShelfSyncEnabled.call(this)) {\n        syncShelf.call(this, event == 'addBookShelf');\n    }\n    handled = true;\n}\n\nif (event == 'clickCustomButton') {\n    let targetUrl = '';\n\n    \/\/ 正文页：章评\n    if (chapter && chapter.url) {\n        try {\n            let chUrl = String(chapter.url);\n            let b64 = chUrl.replace(\/^data:contentUrl;base64,\/, '');\n            let idx = b64.indexOf(',');\n            if (idx >= 0) b64 = b64.substring(0, idx);\n            let params = JSON.parse(java.base64Decode(b64));\n            let bid = params.book_id || '';\n            let cid = params.item_id || '';\n            let src = params.source || '';\n            if (bid && cid) {\n                targetUrl = getHost.call(this) + '\/comment?tab=chapter&book_id=' + bid\n                          + '&item_id=' + cid\n                          + '&source=' + src;\n            }\n        } catch (e) {}\n    }\n\n    \/\/ 详情页：书评\n    if (!targetUrl && book && book.tocUrl) {\n        try {\n            let tocUrl = String(book.tocUrl);\n            let b64 = tocUrl.replace(\/^data:catalogUrl;base64,\/, '');\n            let idx = b64.indexOf(',');\n            if (idx >= 0) b64 = b64.substring(0, idx);\n            let catalog = JSON.parse(java.base64Decode(b64));\n            let bid = catalog.book_id || catalog.bookid || '';\n            let src = catalog.source || '';\n            if (bid) {\n                targetUrl = getHost.call(this) + '\/comment?tab=book&book_id=' + bid\n                          + '&source=' + src;\n            }\n        } catch (e) {}\n    }\n\n    if (targetUrl) {\n        java.showBrowser(\n            targetUrl,\n            null,\n            'window.java=java;',\n            JSON.stringify(getCmtBrowserOptions.call(this))\n        );\n    } else {\n        java.toast('请等待章节或详情加载完毕');\n    }\n    handled = true;\n}\n\nhandled;","content":"@js:\nfunction cleanText(t) {\n    if (t == null) return '';\n    return String(t)\n        .replace(\/\\r\\n\/g, '\\n')\n        .replace(\/\\r\/g, '\\n')\n        .replace(\/[ \\t]+\\n\/g, '\\n')\n        .replace(\/\\n{2,}\/g, '\\n')\n        .replace(\/^\\n+|\\n+$\/g, '')\n        .trim();\n}\n\nlet chapterParams = JSON.parse(java.hexDecodeToString(result));\nchapterParams.token = getJjToken();\nchapterParams.version = localVersion;\nlet conf = getConfig.call(this);\nlet sourceName = chapterParams.source || '';\n\nif (conf.chapter_refresh_force == 'on') {\n    chapterParams.force_refresh = 'on';\n}\n\nif (conf.stv_content == 'on') {\n    chapterParams.stv_content = 'on';\n}\n\nlet toneVar = String(book.getVariable('custom') || '');\nif (!toneVar) {\n    if (sourceName == '番茄听书') toneVar = '1';\n    else if (sourceName == '书旗听书') toneVar = 'yifei';\n}\nchapterParams.tone_id = toneVar;\nsyncProgress.call(this, chapterParams);\n\nlet paraOn = (conf.para_review || 'off') == 'on';\nlet paraStyle = conf.para_style || 'TEXT';\nlet syOn = (conf.para_sy || 'off') == 'on';\n\nif (syOn) chapterParams.devicetype = \"ios\";\nelse chapterParams.devicetype = \"android\";\n\nlet paraSources = ['番茄小说', 'QQ阅读', '晋江','半夏', '七猫', '少年梦', '书旗'];\nif (paraOn && paraSources.indexOf(sourceName) != -1) {\n    chapterParams.review = 'on';\n    chapterParams.style = paraStyle;\n}\n\nif (sourceName == '书旗听书' || sourceName == '番茄听书') {\n    let obj = postJson.call(this, '\/api\/audio', chapterParams);\n    result = cleanText((obj.data || {}).main_url || '');\n} else {\n    let obj;\n    try {\n        obj = postJson.call(this, '\/api\/content', chapterParams);\n    } catch (e) {\n        obj = { code: -1, message: '解析失败', raw: String(e) };\n    }\n\n    if (shouldBlockByVersion.call(this, obj.data && obj.data.version)) {\n        result = '';\n    } else {\n        let hasContent = !!(obj.data && obj.data.content != null && String(obj.data.content).trim() != '');\n        let content = hasContent ? obj.data.content : (obj.message || '');\n        let tab = (obj.data && obj.data.tab) || chapterParams.tab || '';\n        let isVideoLink = (tab == 'video' && content && \/^https?:\\\/\\\/\/i.test(String(content)));\n\n        let rawText = hasContent ? String(content) : '';\n        let cnCount = (rawText.match(\/[\\u4e00-\\u9fa5]\/g) || []).length;\n        let valid = hasContent && cnCount >= 20;\n\n        let stvOn = (conf.stv_content || 'off') == 'on';\n        let stvSource = (sourceName == '晋江') ? 'jjwxc' : ((sourceName == '起点') ? 'qidian' : '');\n        let needStv = !isVideoLink && !valid && stvOn && stvSource;\n\n        if (needStv) {\n            let urlIndex = chapterParams.item_id;\n            let bookid = chapterParams.book_id;\n            let h = stvSource;\n            let bhost = getStvHost.call(this);\n            let loginCookie = getStvCookie.call(this);\n\n            if (!loginCookie) {\n                result = '【STV正文获取失败】未登录STV（缺少cookie）';\n            } else {\n                try {\n                    let grantUrl = bhost + '\/io\/grantcontext\/context?hostid=' + h + '&bookid=' + bookid;\n                    let grantHeaders = {\n                        'cookie': loginCookie,\n                        'x-requested-with': 'com.sangtacviet.mobilereader'\n                    };\n                    let grantRes = java.get(grantUrl, grantHeaders);\n                    let scriptCode = String(grantRes.body());\n\n                    let sessionCookie = '';\n                    try {\n                        let sets = grantRes.headers('set-cookie').toArray();\n                        let parts = [];\n                        for (let si = 0; si < sets.length; si++) {\n                            let kv = String(sets[si]).split(';')[0].trim();\n                            if (kv) parts.push(kv);\n                        }\n                        sessionCookie = parts.join('; ');\n                    } catch (e) {}\n\n                    var __chapterKey = '';\n                    var window = {\n                        document: { hidden: false },\n                        Capacitor: '0',\n                        app: {\n                            net: java,\n                            reader: {\n                                __defineGetter__: (k, func) => { __chapterKey = func(); }\n                            }\n                        }\n                    };\n                    var btoa = (s) => String(yuedu.base64Encode(s));\n                    var Capacitor = {};\n                    var key;\n                    var yuedu = java;\n                    var Pkgs = Packages;\n                    Packages = undefined;\n                    java = undefined;\n                    eval('(()=>{var _0xp = globalThis.String.fromCharCode;String.fromCharCode = function() {var args = Array.prototype.slice.call(arguments);return _0xp.apply(null, args).replace(\/const \/g, \"let \");};' + scriptCode + ';return key;})()');\n                    java = yuedu;\n                    Packages = Pkgs;\n                    KEY = __chapterKey;\n\n                    if (!KEY) {\n                        result = '【STV正文获取失败】未取到章节KEY';\n                    } else {\n                        let allCookies = [loginCookie, sessionCookie].filter(Boolean).join('; ');\n                        let stv_cookie = 'transmode=chinese; ' + allCookies;\n                        let SIGN_SUFFIX = '&sajax=readchapter&erogh982^%*%^*';\n                        let SIGN_KEY = KEY + SIGN_SUFFIX;\n                        let sign = java.md5Encode('bookid=' + bookid + '&c=' + urlIndex + '&h=' + h + '&key=' + SIGN_KEY);\n                        let chapterUrl = bhost + '\/index.php?sajax=readchapter&h=' + h + '&bookid=' + bookid + '&c=' + urlIndex + '&key=' + KEY;\n\n                        let chapterOpt = {\n                            method: 'GET',\n                            headers: {\n                                'cookie': stv_cookie,\n                                'X-STV-Sign': sign,\n                                'x-requested-with': 'com.sangtacviet.mobilereader',\n                                'x-stv-transport': 'app',\n                                'Referer': bhost + '\/',\n                                'Origin': bhost\n                            }\n                        };\n                        let stvRaw = '';\n                        let retry = 0;\n                        while (retry < 3) {\n                            stvRaw = java.ajax(chapterUrl + ',' + JSON.stringify(chapterOpt)) + '';\n                            if (stvRaw.indexOf('error code: 1015') == -1) break;\n                            retry++;\n                            if (retry < 3) {\n                                try { java.lang.Thread.sleep(1000); } catch (e) {}\n                            }\n                        }\n                        if (stvRaw.indexOf('error code: 1015') != -1) {\n                            result = '【STV正文获取失败】STV接口返回 error code: 1015（重试3次仍失败）';\n                        } else {\n                        try {\n                            let jsonObj = JSON.parse(stvRaw);\n                            if (jsonObj.data) {\n                                let text = String(jsonObj.data);\n\n                                text = text.replace(\/<br\\s*\\\/?>\/gi, '\\n');\n                                text = text.replace(\/<\\\/p>\/gi, '\\n');\n                                text = text.replace(\/<[^>]+>\/g, '');\n\n                                text = text.replace(\/^\\uFEFF\/, '');\n                                text = text.replace(\/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F]\/g, '');\n                                text = text.replace(\/[\\u200B-\\u200D\\uFEFF]\/g, '');\n\n                                text = text.replace(\/&[^;]*?\\([A-Za-z]{4,}\\)[^;]*?;\/g, '');\n                                text = text.replace(\/<[^>]*?\\([A-Za-z]{4,}\\)[^>]*?>\/g, '');\n                                text = text.replace(\/\\([A-Za-z]{4,}\\)\/g, '');\n\n                                text = text.replace(\/<b><font color=red>[\\s\\S]*?<\\\/b>\/gi, '');\n                                text = text.replace(\/<!--[\\s\\S]*?-->\/g, '');\n\n                                let lines = text.split('\\n');\n                                let kept = [];\n                                for (let li = 0; li < lines.length; li++) {\n                                    let line = lines[li];\n                                    if (\/^Người\/.test(line)) continue;\n                                    if (\/@Bạn\/.test(line)) continue;\n                                    if (\/Đọc tại\/.test(line)) continue;\n                                    if (\/Tải.*Scan\/.test(line)) continue;\n                                    if (\/[（(][^）)]*飞卢小说[^）)]*[）)]\/.test(line)) continue;\n                                    if (\/看无下划线版小说请下载飞卢小说\/.test(line)) continue;\n                                    kept.push(line);\n                                }\n                                text = kept.join('\\n');\n\n                                text = text.replace(\/\\n{2,}\/g, '\\n').trim();\n\n                                if (text) {\n                                    if (h == 'jjwxc') {\n                                        chapterParams.content = text;\n                                        chapterParams.stv_content = stvOn ? 'on' : 'off';\n                                        let obj2 = null;\n                                        try {\n                                            obj2 = postJson.call(this, '\/api\/content', chapterParams);\n                                        } catch (e) {}\n                                        if (paraOn && obj2 && obj2.data && obj2.data.content != null && String(obj2.data.content).trim() != '') {\n                                            result = cleanText(obj2.data.content);\n                                        } else {\n                                            result = cleanText(text);\n                                        }\n                                    } else {\n                                        result = cleanText(text);\n                                    }\n                                } else {\n                                    result = '【STV正文为空】';\n                                }\n                            } else {\n                                let errMsg = jsonObj.err || jsonObj.error || '';\n                                let cnMsg = '';\n                                if (\/truyện không tồn tại\/i.test(errMsg)) cnMsg = '该书在STV系统中不存在';\n                                else if (\/Khởi động lại\/i.test(errMsg)) cnMsg = 'STV要求重启应用（请清除cookie后重新登录）';\n                                else if (\/bị khóa\/i.test(errMsg)) cnMsg = 'STV账号已被锁定';\n                                else if (\/hết hạn\/i.test(errMsg)) cnMsg = 'STV登录已过期，请重新登录';\n                                else if (\/JJMoney trong tài khoản không đủ\/i.test(errMsg)) cnMsg = 'STV账号JJMoney余额不足，请去STV网页端充值\/兑换后再试';\n                                else if (\/lỗi\/i.test(errMsg)) cnMsg = 'STV服务端错误';\n                                else cnMsg = errMsg || '未知错误';\n                                result = '【STV正文获取失败】' + cnMsg;\n                            }\n                        } catch (e) {\n                            result = '【STV正文解析失败】' + e + ' raw=' + String(stvRaw).substring(0, 200);\n                        }\n                        }\n                    }\n                } catch (e) {\n                    java = yuedu;\n                    Packages = Pkgs;\n                    result = '【STV请求异常】' + e;\n                }\n            }\n        } else if (isVideoLink) {\n            if (book.durChapterIndex == chapter.index) {\n                try {\n                    java.startBrowser(String(content), chapter.title);\n                    java.toast('正在加载视频\\n视频加载较慢，请耐心等待');\n                } catch (e) {}\n            }\n            result = '【刷新正文】播放\\n直链:\\n' + cleanText(content);\n        } else {\n            result = cleanText(content);\n        }\n    }\n}","imageStyle":"FULL","payAction":"let raw = String(baseUrl);\nlet b64 = raw.replace(\/^data:contentUrl;base64,\/, '');\nlet idx = b64.indexOf(',');\nif (idx >= 0) b64 = b64.substring(0, idx);\nlet chapterParams = JSON.parse(java.base64Decode(b64));\n\nlet bid = String(chapterParams.book_id || '');\nlet cid = String(chapterParams.item_id || '');\n\nlet token = getJjToken.call(this);\nlet enbody = String(Date.now()) + \":\" + token + \":\" + bid;\nlet sign = encode.call(this, enbody);\nlet body = \"versionCode=444&novelId=\" + bid + \"&chapters=\" + cid + \"&orderType=15&isautoBuy=0&autosubscribeStatus=0&notUseBackpoint=&sign=\" + sign;\nlet url = \"https:\/\/android.jjwxc.net\/androidapi\/buy,\";\nlet post = { \"method\": \"POST\", \"body\": String(body) };\nlet a = java.ajax(url + JSON.stringify(post));\nlet res = JSON.parse(a);\nif (\/购买成功\/.test(res.message)) {\n    java.longToast(res.message);\n    result = true;\n} else {\n    let mm = res.message == \"\"\n        ? (res.popupdata && res.popupdata.popupmessage ? res.popupdata.popupmessage : \"购买失败\") + \"\\n❗️进晋江app进行一次验证后❗️才能在阅读app使用购买功能\"\n        : res.message;\n    java.longToast(mm);\n}","replaceRegex":"##\\(本章完\\)|\\(\\)|\\(第\\d+\/\\d+页\\)|(?:.*(?:蟹蟹|扔了|感谢|投出|灌溉营养液|送的|谢谢).*?(?:手榴|月票|霸王票|小天使|火箭炮|深水鱼雷|浅水炸弹|地雷|营养液)([xX]\\d+)*.*|.*?瓶[；~]|\".*?\"(?:营养液|手榴弹|月票|霸王票|小天使|火箭炮|深水鱼雷|浅水炸弹|地雷)x\\d+|读者.*?(月石|营养液).*|地雷感谢：[\\s\\S]+)|<\\?xml.*?XHTML.*","subContent":"@js:\nlet chapterParams = JSON.parse(java.hexDecodeToString(result));\n\nvar sourceName = chapterParams.source || '';\n\nlet toneVar = String(book.getVariable('custom') || '');\nif (!toneVar) {\n    if (sourceName == '番茄听书') toneVar = '1';\n    else if (sourceName == '书旗听书') toneVar = 'yifei';\n}\nchapterParams.tone_id = toneVar;\n\nlet allowSources = ['番茄听书', '番茄畅听', '七猫听书', '酷我听书', '书旗听书', '喜马拉雅', '懒人听书'];\nif (allowSources.indexOf(sourceName) == -1) {\n    result = '';\n} else {\n    let headers = buildHeaders.call(this);\n    headers['Content-Type'] = 'application\/json';\n    let op = { method: \"POST\", body: JSON.stringify(chapterParams), headers: headers };\n    let url = getHost.call(this) + `\/api\/lrc,${JSON.stringify(op)}`;\n    let res = java.ajax(url);\n    res = decryptIfEncrypted.call(this, res);\n    let obj = JSON.parse(res);\n    result = (obj.data && obj.data.lrc) ? obj.data.lrc : '';\n}"},"ruleExplore":{"author":"$.author","bookList":"<js>\nlet res = decryptIfEncrypted.call(this, result);\nres = checkMsg.call(this, res);\nresult = res;\n<\/js>\n$.data","bookUrl":"<js>\nlet detail = {\n    source: result.source,\n    sing: result.sing,\n    book_id: result.book_id\n};\nlet yunurl = java.base64Encode(JSON.stringify(detail));\nresult = `data:detailsUrl;base64,${yunurl},{\"type\":\"qingci\"}`;\n<\/js>","coverUrl":"$.cover","intro":"$.desc","kind":"$.tags","lastChapter":"$.latest_chapter_title","name":"$.title","wordCount":"$.word_count"},"ruleSearch":{"author":"$.author","bookList":"<js>\nlet res = decryptIfEncrypted.call(this, result);\nres = checkMsg.call(this, res);\nresult = res;\n<\/js>\n$.data.list","bookUrl":"<js>\nlet detail = {\n    source: result.source,\n    sing: result.sing,\n    book_id: result.book_id\n};\nlet yunurl = java.base64Encode(JSON.stringify(detail));\nresult = `data:detailsUrl;base64,${yunurl},{\"type\":\"qingci\"}`;\n<\/js>","checkKeyWord":"十日终焉","coverUrl":"$.cover","intro":"$.desc","kind":"$.tags","lastChapter":"{{$.source}} · {{$.latest_chapter_title}}","name":"$.title","wordCount":"$.word_count"},"ruleToc":{"chapterList":"<js>\nvar res = JSON.parse(java.hexDecodeToString(result));\nlet device = deviceType() ? 'ios' : 'android';\nlet typeMap;\n\nif (device == 'android') {\n    typeMap = { audio: 32, comic: 64 };\n    book.type = res.tab in typeMap ? typeMap[res.tab] : 8;\n} else {\n    typeMap = { audio: 1, comic: 2, video: 3 };\n    book.type = res.tab in typeMap ? typeMap[res.tab] : 0;\n}\nvar headers = buildHeaders.call(this);\nheaders['Content-Type'] = 'application\/json';\nvar op = { method: \"POST\", body: JSON.stringify(res), headers: headers };\nvar request = getHost.call(this) + '\/api\/catalog,' + JSON.stringify(op);\nresult = java.ajax(request);\nresult = decryptIfEncrypted.call(this, result);\nresult = checkMsg.call(this, result);\nresult;\n<\/js>\n$.data","chapterName":"$.title","chapterUrl":"<js>\nif (result.is_volume == true) {\n    result = \"\";\n} else {\n    let content = {\n    \t   sing: result.sing,\n        book_id: result.book_id || \"\",\n        item_id: result.item_id || \"\",\n        content_md5: result.content_md5 || \"\",\n        chapter_index: result.chapter_index,\n        source: result.source\n    };\n    let yunurl = java.base64Encode(JSON.stringify(content));\n    result = `data:contentUrl;base64,${yunurl},{\"type\":\"qingci\"}`;\n}\n<\/js>","isPay":"$.is_pay","isVip":"$.is_vip","isVolume":"$.is_volume","updateTime":"$.tag"},"searchUrl":"<js>\nvar conf = ensureConfig.call(this);\nvar sourceVal = conf.source || '';\nvar param = sourceVal ? '&source=' + sourceVal.split(',').map(function(s){ return s.trim(); }).join(',') : '';\n\nvar mode = conf.mode || 'novel';\nvar modeMap = {\n    '📖小说': 'novel',\n    '🎧听书': 'audio',\n    '🌈漫画': 'comic',\n    '▶️短剧': 'video',\n    '⬇️下载': 'download'\n};\nif (modeMap[mode]) mode = modeMap[mode];\n\nvar sources = conf.sources || {};\nvar allTabs = Array.isArray(conf.allTabs) ? conf.allTabs : [];\nvar tabs = ['novel', 'audio', 'comic', 'video', 'download'];\n\nvar curList = sources[mode] || [];\nif (curList.length == 0 && allTabs.indexOf(mode) == -1) {\n    for (var i = 0; i < tabs.length; i++) {\n        var t = tabs[i];\n        if ((sources[t] || []).length > 0 || allTabs.indexOf(t) != -1) {\n            mode = t;\n            break;\n        }\n    }\n}\n\nresult = getHost.call(this) + '\/api\/search?keyword={{key}}&page={{page}}&tab=' + mode + param;\n<\/js>","weight":0}]