📚书山聚合
书山聚合
书山 (1935)5天前
2026.10.12日更新:
修复关闭书架/进度同步时晋江来源不生效
修复指定番茄小说时个人中心与书架消失
SVG气泡示例
(颜色:$color 段评数量:$displayText)
❤️🔥登录必看❤️🔥
1.输入邮箱密码后必须点✓保存生效,
否则读取不到输入的账号密码
2.跳转卡顿在设置→其他设置中清除WebView缓存
🔔适配兼容
iOS 适配:源阅,栖阅,轻悦时光,用心读书
📖获取书源请访问书山发布页
https://fb.shushan.vip
阅读软件多平台聚合下载站
https://legado.vossc.com
🔥注:阅读请使用最新测试版
1.所有源站每20秒最多12章,把预下载调到0-3之间使用
💯[打赏VIP无请求限制,专属书源]
2.自定义源站示例:番茄小说,七猫 多个源站用英文,号分隔
源站名参考 登录URL //书源配置列表 v参数
3.源站带有❇️图标表示只能指定或@搜索
4.搜索模式:
书名@来源 多个来源用英文,号分隔
示例:
系统@番茄小说,多个源站使用英文,号分隔
书名@类型 返回对应类型所有源站数据,可直接使用@方式搜索或者点击登陆切换模式
示例:
系统@小说 听书 漫画 短剧等
默认书名排序
指定作者优先:
作者名@作者 作者名@作者,源站 {
"bookSourceComment": "2026.10.12日更新:\n修复关闭书架\/进度同步时晋江来源不生效\n修复指定番茄小说时个人中心与书架消失\n\n\n\nSVG气泡示例\n(颜色:$color 段评数量:$displayText)\n\n❤️🔥登录必看❤️🔥\n1.输入邮箱密码后必须点✓保存生效,\n否则读取不到输入的账号密码\n2.跳转卡顿在设置→其他设置中清除WebView缓存\n\n🔔适配兼容\niOS 适配:源阅,栖阅,轻悦时光,用心读书\n📖获取书源请访问书山发布页\n https:\/\/fb.shushan.vip\n\n阅读软件多平台聚合下载站\nhttps:\/\/legado.vossc.com\n\n🔥注:阅读请使用最新测试版\n1.所有源站每20秒最多12章,把预下载调到0-3之间使用\n💯[打赏VIP无请求限制,专属书源]\n2.自定义源站示例:番茄小说,七猫 多个源站用英文,号分隔\n源站名参考 登录URL \/\/书源配置列表 v参数\n3.源站带有❇️图标表示只能指定或@搜索\n\n4.搜索模式:\n书名@来源 多个来源用英文,号分隔\n示例: \n系统@番茄小说,多个源站使用英文,号分隔\n书名@类型 返回对应类型所有源站数据,可直接使用@方式搜索或者点击登陆切换模式\n示例: \n系统@小说 听书 漫画 短剧等\n默认书名排序\n指定作者优先:\n作者名@作者 作者名@作者,源站",
"bookSourceGroup": "聚合书源",
"bookSourceName": "📚书山聚合",
"bookSourceType": 0,
"bookSourceUrl": "书山聚合",
"bookUrlPattern": "https?:\\\/\\\/.*\\\/details.*",
"customButton": true,
"customOrder": 0,
"enabled": true,
"enabledCookieJar": true,
"enabledExplore": true,
"eventListener": true,
"exploreUrl": "<js>\nlet config = getConfig.call(this);\nif (!config.host || !config.gender) {\n config = { gender: \"boy\", host: getServerHost(), tag: \"多元\" };\n setConfigs.call(this, config);\n java.toast(\"配置初始化完成\");\n}\nif (!config.tag) config.tag = \"多元\";\nlet obj = (title, url, type) => ({\n title: title,\n url: url,\n style: { layout_flexGrow: 1, layout_flexBasisPercent: type }\n});\nlet sort = [];\nlet controls = [];\nlet allSourceOptions = [{label:'☁️聚合搜索', value:''}];\nlet sourceNameToType = {};\nlet sourceTypeToNames = {};\ntry {\n let apiResponse = JSON.parse(java.ajax(getServerHost() + \"\/api\/get_config\"));\n if (apiResponse && Array.isArray(apiResponse)) {\n apiResponse.forEach(item => {\n if (item.name && item.key) {\n allSourceOptions.push({\n label: item.name,\n value: item.name\n });\n if (item.type) {\n sourceNameToType[item.name] = item.type;\n if (!sourceTypeToNames[item.type]) {\n sourceTypeToNames[item.type] = [];\n }\n sourceTypeToNames[item.type].push(item.name);\n }\n }\n });\n }\n} catch(e) {\n java.toast(\"获取源站列表失败,请尝试切换服务器后刷新\");\n}\nlet modeOptions = [\"📖小说\", \"🎧听书\", \"🌈漫画\", \"▶️视频\"];\nlet modeDisplayToName = {'📖小说':'小说','🎧听书':'听书','🌈漫画':'漫画','▶️视频':'视频'};\nlet modeNameToDisplay = {'小说':'📖小说','听书':'🎧听书','漫画':'🌈漫画','视频':'▶️视频'};\nlet modeTypeMap = {'小说':'novel','听书':'audio','漫画':'comic','视频':'video'};\nlet defaultMode = \"📖小说\";\nlet currentModeName = \"\";\nif (config.source) {\n if (modeNameToDisplay[config.source]) {\n defaultMode = modeNameToDisplay[config.source];\n currentModeName = config.source;\n } else if (sourceNameToType[config.source]) {\n let type = sourceNameToType[config.source];\n let typeToMode = {'novel':'📖小说','audio':'🎧听书','comic':'🌈漫画','video':'▶️视频'};\n defaultMode = typeToMode[type] || \"📖小说\";\n currentModeName = modeDisplayToName[defaultMode] || \"小说\";\n }\n} else {\n currentModeName = \"小说\";\n}\nfunction getSourceOptionsByMode(modeName) {\n let type = modeTypeMap[modeName] || 'novel';\n let options = [{label:'☁️聚合搜索', value:''}];\n if (sourceTypeToNames[type]) {\n sourceTypeToNames[type].forEach(name => {\n options.push({label: name, value: name});\n });\n }\n return options;\n}\nlet currentSourceOptions = getSourceOptionsByMode(currentModeName);\nlet sourceLabels = currentSourceOptions.map(item => item.label);\nlet currentSource = config.source || '';\nlet defaultSourceLabel = '☁️聚合搜索';\nlet modeNames = ['小说','听书','漫画','视频'];\nif (!modeNames.includes(currentSource)) {\n for (let i = 0; i < currentSourceOptions.length; i++) {\n if (currentSourceOptions[i].value == currentSource) {\n defaultSourceLabel = currentSourceOptions[i].label;\n break;\n }\n }\n}\nlet mapObj = {};\ncurrentSourceOptions.forEach(item => { mapObj[item.label] = item.value; });\nlet mapStr = JSON.stringify(mapObj).replace(\/\\\\\/g,'\\\\\\\\').replace(\/\"\/g,'\\\\\"');\ncontrols.push({\n title: \"☃关键词💭:书名\/作者\",\n type: \"text\",\n style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.6 }\n});\ncontrols.push({\n title: \"🔍搜索\",\n type: \"button\",\n action: \"java.searchBook(infoMap['☃关键词💭:书名\/作者'] || '', source.bookSourceName + '::' + source.getKey())\",\n style: { layout_flexGrow: 1, layout_flexBasisPercent: -1, layout_alignSelf: \"baseline\" }\n});\ncontrols.push({\n title: \"🔄刷新\",\n type: \"button\",\n action: \"java.refreshExplore()\",\n style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.4, layout_alignSelf: \"baseline\" }\n});\ncontrols.push({\n title: \"⚙登录\",\n type: \"button\",\n action: \"java.open('login','http',null)\",\n style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.4, layout_alignSelf: \"baseline\" }\n});\ncontrols.push({\n title: \"性别\",\n type: \"select\",\n chars: [\"♂️男频\", \"♀️女频\"],\n default: config.gender == \"boy\" ? \"♂️男频\" : \"♀️女频\",\n action: \"(function(){ var raw=infoMap['性别']||''; var v=(raw.indexOf('男')>=0)?'boy':'girl'; setConfigs.call(this,{gender:v}); java.refreshExplore(); })()\",\n style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.4 }\n});\nlet modeMapStr = JSON.stringify(modeDisplayToName).replace(\/\\\\\/g,'\\\\\\\\').replace(\/\"\/g,'\\\\\"');\ncontrols.push({\n title: \"模式\",\n type: \"select\",\n chars: modeOptions,\n default: defaultMode,\n action: \"(function(){ var raw=infoMap['模式']||''; var modeMap=JSON.parse('\" + modeMapStr + \"'); var modeName=modeMap[raw]||'小说'; setConfigs.call(this,{source:modeName}); java.refreshExplore(); })()\",\n style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.4 }\n});\nlet currentStyle = config.imgstyle || \"TEXT\";\nlet currentSizeLabel = currentStyle == \"TEXT\" ? \"🔵大号\" : \"🟢小号\";\ncontrols.push({\n title: \"段评图标\",\n type: \"select\",\n chars: [\"🟢小号\", \"🔵大号\"],\n default: currentSizeLabel,\n action: \"(function(){ var raw=infoMap['段评图标']||''; var v='TEXT'; if(raw.indexOf('小号')>=0){ v='text'; } else if(raw.indexOf('大号')>=0){ v='TEXT'; } setConfigs.call(this,{imgstyle:v}); var msg = v=='TEXT'?'大号':'小号'; java.toast('\\\\n🥽段评气泡-'+msg+'样式'); java.refreshExplore(); })()\",\n style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.4 }\n});\nlet sourceOptionsStr = JSON.stringify(currentSourceOptions).replace(\/\\\\\/g,'\\\\\\\\').replace(\/\"\/g,'\\\\\"');\ncontrols.push({\n title: \"源站\",\n type: \"select\",\n chars: sourceLabels,\n default: defaultSourceLabel,\n action: \"(function(){ var raw=infoMap['源站']||''; var options=JSON.parse('\" + sourceOptionsStr + \"'); var map={}; options.forEach(function(item){ map[item.label]=item.value; }); setConfigs.call(this,{source:map[raw]||''}); java.refreshExplore(); })()\",\n style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.4 }\n});\nif (String(config.source || '').indexOf(\"晋江\") >= 0) {\n let tagMap = {\"🏷 言情\":\"言情\",\"🏷 纯爱\":\"纯爱\",\"🏷 百合\":\"百合\",\"🏷 无Cp\":\"无Cp\",\"🏷 多元\":\"多元\"};\n let tagMapStr = JSON.stringify(tagMap).replace(\/\\\\\/g,'\\\\\\\\').replace(\/\"\/g,'\\\\\"');\n controls.push({\n title: \"标签频道\",\n type: \"select\",\n chars: [\"🏷 言情\", \"🏷 纯爱\", \"🏷 百合\", \"🏷 无Cp\", \"🏷 多元\"],\n default: \"🏷 \" + (config.tag || \"多元\"),\n action: \"(function(){ var raw=infoMap['标签频道']||''; var tagMap=JSON.parse('\" + tagMapStr + \"'); var tagName=tagMap[raw]||'多元'; setConfigs.call(this,{tag:tagName}); java.refreshExplore(); })()\",\n style: { layout_flexGrow: 1, layout_flexBasisPercent: 1 }\n });\n}\n\nlet currentType = config.gender || \"\";\nlet currentSource2 = config.source || \"\";\nlet firstSource = String(currentSource2).split(',')[0].trim();\n\nconst getGenderDisplayName = (gender) => {\n if (gender == \"boy\") return \"男频\";\n if (gender == \"girl\") return \"女频\";\n return \"\";\n};\nconst genderDisplay = getGenderDisplayName(currentType);\nconst excludedSources = [\"小说\", \"听书\", \"漫画\", \"视频\", \"短剧\", \"音频\", \"番茄小说\"];\nif (firstSource && !excludedSources.includes(firstSource)) {\n try {\n let jjTypeId = config.type_id || '';\n let apiUrl = `${getServerHost()}\/type_api?source=${firstSource}`;\n if (currentType) {\n apiUrl += `&gender=${currentType}`;\n }\n if (jjTypeId) {\n apiUrl += `&type=${jjTypeId}`;\n }\n if (config.tag) {\n apiUrl += `&tag=${encodeURIComponent(config.tag)}`;\n }\n let response = JSON.parse(java.ajax(apiUrl));\n if (response.data.found) {\n response.data.found.forEach(item => {\n let finalUrl = \"\";\n if (item.url) {\n if (item.url.includes('jj_signin') || item.url.includes('jjbookshelf')) {\n finalUrl = item.url;\n } else {\n finalUrl = `${getServerHost()}\/type_api?source=${firstSource}&page={{page}}&url=${item.url}`;\n if (currentType) {\n finalUrl += `&gender=${currentType}`;\n }\n if (config.tag) {\n finalUrl += `&tag=${encodeURIComponent(config.tag)}`;\n }\n }\n }\n sort.push({\n title: item.title,\n url: finalUrl,\n style: item.style || { layout_flexGrow: 1, layout_flexBasisPercent: 0.29 }\n });\n });\n } else {\n sort.push({\n title: `【${firstSource}】${genderDisplay ? `【${genderDisplay}】` : ''}暂无数据`,\n url: \"\",\n style: { layout_flexGrow: 1, layout_flexBasisPercent: 1 }\n });\n }\n } catch(e) {\n sort.push({\n title: `【${firstSource}】${genderDisplay ? `【${genderDisplay}】` : ''}暂无数据`,\n url: \"\",\n style: { layout_flexGrow: 1, layout_flexBasisPercent: 1 }\n });\n }\n sort = controls.concat(sort);\n JSON.stringify(sort);\n} else {\n let recommendUrl = getServerHost() + \"\/read_recommend?session=\" + getSessionId();\n sort.push({\n title: '个性推荐',\n url: recommendUrl,\n style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.45 }\n });\n sort.push({\n title: '猜你喜欢',\n url: 'https:\/\/fanqienovel.com\/tab\/cai',\n style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.45 }\n });\n sort.push({\n title: '巅峰榜单',\n url: 'https:\/\/fanqienovel.com\/api\/author\/misc\/top_book_list\/v1\/?limit=100&offset={{(page-1)*100}}',\n style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.29 }\n });\n sort.push({\n title: '出版榜单',\n url: 'https:\/\/fanqienovel.com\/api\/node\/publication\/list?page_index={{(page-1)*100}}&page_count=100',\n style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.29 }\n });\n sort.push({\n title: '爆更榜单',\n url: 'https:\/\/api-lf.fanqiesdk.com\/api\/novel\/channel\/homepage\/rank\/rank_list\/v2\/?aid=13&limit=50&offset={{(page-1)*100}}&side_type=15&type=1',\n style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.29 }\n });\n sort.push({\n title: '黑马榜单',\n url: 'https:\/\/api-lf.fanqiesdk.com\/api\/novel\/channel\/homepage\/rank\/rank_list\/v2\/?aid=13&limit=50&offset={{(page-1)*100}}&side_type=13&type=1',\n style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.29 }\n });\n sort.push({\n title: '热搜榜单',\n url: 'https:\/\/api-lf.fanqiesdk.com\/api\/novel\/channel\/homepage\/rank\/rank_list\/v2\/?aid=13&limit=50&offset={{(page-1)*100}}&side_type=12&type=1',\n style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.29 }\n });\n let showCategories = currentType == \"girl\" ? [0] : [1];\n showCategories.forEach(genderCode => {\n let isMale = genderCode == 1;\n let genderLabel = isMale ? \"男频\" : \"女频\";\n let categories = isMale ? [\n {id: 1141, name: \"西方奇幻\"},\n {id: 1140, name: \"东方仙侠\"},\n {id: 8, name: \"科幻末世\"},\n {id: 261, name: \"都市日常\"},\n {id: 124, name: \"都市修真\"},\n {id: 1014, name: \"都市高武\"},\n {id: 273, name: \"历史古代\"},\n {id: 27, name: \"战神赘婿\"},\n {id: 263, name: \"都市种田\"},\n {id: 258, name: \"传统玄幻\"},\n {id: 272, name: \"历史脑洞\"},\n {id: 262, name: \"都市脑洞\"},\n {id: 257, name: \"玄幻脑洞\"},\n {id: 751, name: \"悬疑灵异\"},\n {id: 504, name: \"抗战谍战\"},\n {id: 746, name: \"游戏体育\"},\n {id: 718, name: \"动漫衍生\"},\n {id: 1016, name: \"男频衍生\"}\n ] : [\n {id: 1139, name: \"古风世情\"},\n {id: 8, name: \"科幻末世\"},\n {id: 746, name: \"游戏体育\"},\n {id: 1015, name: \"女频衍生\"},\n {id: 248, name: \"玄幻言情\"},\n {id: 23, name: \"种田\"},\n {id: 79, name: \"年代\"},\n {id: 267, name: \"现言脑洞\"},\n {id: 246, name: \"宫斗宅斗\"},\n {id: 539, name: \"悬疑脑洞\"},\n {id: 253, name: \"古言脑洞\"},\n {id: 24, name: \"快穿\"},\n {id: 749, name: \"青春甜宠\"},\n {id: 745, name: \"星光璀璨\"},\n {id: 747, name: \"女频悬疑\"},\n {id: 750, name: \"职场婚恋\"},\n {id: 748, name: \"豪门总裁\"},\n {id: 1017, name: \"民国言情\"}\n ];\n sort.push({\n title: `❤️${genderLabel}阅读榜❤️`,\n url: \"\",\n style: { layout_flexGrow: 1, layout_flexBasisPercent: 1 }\n });\n categories.forEach(cat => {\n sort.push({\n title: cat.name,\n url: `${getServerHost()}\/style_top?rank_list_type=3&offset={{(page-1)*10}}&limit=10&category_id=${cat.id}&gender=${genderCode}&rankMold=2`,\n style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.29 }\n });\n });\n sort.push({\n title: `❤️${genderLabel}新书榜❤️`,\n url: \"\",\n style: { layout_flexGrow: 1, layout_flexBasisPercent: 1 }\n });\n categories.forEach(cat => {\n sort.push({\n title: cat.name,\n url: `${getServerHost()}\/style_top?rank_list_type=3&offset={{(page-1)*10}}&limit=10&category_id=${cat.id}&gender=${genderCode}&rankMold=1`,\n style: { layout_flexGrow: 1, layout_flexBasisPercent: 0.29 }\n });\n });\n });\n let fqCategoryArr = [];\n showCategories = currentType == \"girl\" ? [0] : [1];\n showCategories.forEach(i => {\n try {\n let response = JSON.parse(java.ajax(`${getServerHost()}\/type_style?new_category_tab=${i}`));\n let categoryGroups = response.data;\n categoryGroups.forEach(group => {\n fqCategoryArr.push(obj(`❤️${group.name}❤️`, null, 1));\n group.data.forEach(category => {\n let gender, genre_type;\n if (i == 0) {\n gender = 0;\n genre_type = 0;\n } else if (i == 1) {\n gender = 1;\n genre_type = 0;\n } else {\n gender = 1;\n genre_type = 0;\n }\n fqCategoryArr.push({\n title: category.name,\n url: `${getServerHost()}\/type_style?category_id=${category.category_id}&genre_type=${genre_type}&gender=${gender}&offset={{(page-1)*100}}&selected_items=`,\n style: {\n layout_flexGrow: 1,\n layout_flexBasisPercent: 0.29\n }\n });\n });\n });\n } catch (e) {\n fqCategoryArr.push(obj(\"分类加载失败,请重试\", null, 1));\n }\n });\n fqCategoryArr.forEach(item => {\n if (item.style && item.style.layout_flexBasisPercent) {\n sort.push({\n title: item.title,\n url: item.url,\n style: item.style\n });\n } else {\n sort.push({\n title: item.title,\n url: item.url,\n style: {\n layout_flexGrow: 1,\n layout_flexBasisPercent: 0.29\n }\n });\n }\n });\n let hasValidSession = false;\n let username = \"\";\n try {\n let uinfo = java.ajax(getServerHost() + \"\/book_user?session=\" + getSessionId());\n uinfo = JSON.parse(uinfo);\n if (uinfo.data.name) {\n username = uinfo.data.name;\n hasValidSession = true;\n }\n } catch (e) {\n }\n if (!hasValidSession) {\n java.toast(\"番茄登录已过期,请重新登录\");\n }\n let gro = [];\n let pushGro = (title, url, type) => gro.push(obj(title, url, type));\n let sArr = [];\n if (hasValidSession) {\n try {\n let book_shelf_info = JSON.parse(java.ajax(getServerHost() + \"\/book_shelf?session=\" + getSessionId()));\n if (book_shelf_info.code == 0) {\n let groups_bookids = { \"未分组\": [] };\n book_shelf_info.data.book_shelf_info.forEach(i => {\n if (!groups_bookids[i.group_name ? i.group_name : \"未分组\"]) \n groups_bookids[i.group_name] = [];\n groups_bookids[i.group_name ? i.group_name : \"未分组\"].push(i.book_id);\n });\n Object.keys(groups_bookids).forEach(k => {\n pushGro(k, \"https:\/\/fanqienovel.com\/fqbookshelf\/groupName\/\" + k, 0.4);\n });\n if (Object.keys(groups_bookids).length % 2 != 0) pushGro(\"占位\", \"\", 0.4);\n sArr.push(obj(username + '的个人中心', '', 1));\n sArr.push(obj(\"我的书架\", \"https:\/\/fanqienovel.com\/fqbookshelf\", 1));\n sArr = sArr.concat(gro);\n sArr.push(obj(\"阅读历史\", getServerHost() + \"\/read_history?session=\" + getSessionId() + \"&page={{page}}\", 1));\n } else {\n java.toast(\"获取书架信息失败,登录可能已过期\");\n }\n } catch (e) {\n java.toast(\"番茄登录过期,已隐藏个人中心\");\n }\n } else {\n java.toast(\"您还未登录番茄账号,无法同步数据\");\n }\n sort = sArr.concat(sort);\n sort = controls.concat(sort);\n JSON.stringify(sort);\n}\n<\/js>",
"header": "@js:\nJSON.stringify({\n\t\"User-Agent\":java.getWebViewUA(),\n\t\"X-Api-Key\": getSecretKey(),\n\t\"X-Novel-Token\": \"SHUSAN_READ_2025\"\n\t})",
"jsLib": "var hosts = [\n \"https:\/\/v1.vossc.com\",\n \"https:\/\/v2.vossc.com\",\n \"https:\/\/v3.vossc.com\",\n \"https:\/\/v4.vossc.com\",\n \"http:\/\/1.94.248.5:7001\",\n \"http:\/\/113.44.163.166:7001\"\n];\n\nfunction getHosts() {\n return hosts;\n}\n\nfunction getServerHost() {\n let { source } = this;\n try {\n const config = JSON.parse(source.getVariable());\n return config?.[0]?.host || getHosts()[0];\n } catch (e) {\n return getHosts()[0];\n }\n}\n\nfunction getParagraphApiUrl() {\n let host = getServerHost.call(this);\n return host + '\/para';\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 checkEnv() {\n let { java, source } = this;\n let isModified = false;\n try {\n new Packages.io.legato.kazusa.utils.TimeoutCancellationException('');\n isModified = true;\n } catch (e) {\n isModified = typeof source.loginUi == 'function' ? false : true;\n }\n\n return isModified;\n}\n\nfunction isQRead() {\n let { java } = this;\n try {\n return java.qread && java.qread() == \"1\";\n } catch (e) {\n return false;\n }\n}\n\nfunction getSecretKey() {\n let { java, source, cookie } = this;\n try {\n var headerValue = source.getLoginHeader();\n if (headerValue) {\n var headerStr = String(headerValue);\n if (headerStr && headerStr.length >= 10) {\n return java.base64Encode(headerStr);\n }\n }\n \n var cookieHeader = cookie.getCookie(getServerHost.call(this)) || '';\n var tokenMatch = cookieHeader.match(\/X-Novel-Token=([^;]+)\/);\n if (tokenMatch) {\n return tokenMatch[1];\n }\n \n java.toast(\"❌ 未登录,请先登录\");\n return \"\";\n } catch (e) {\n java.toast(\"❌ 获取登陆状态失败\");\n return \"\";\n }\n}\n\nfunction JJtoken() {\n let { java, source } = this;\n const config = getConfig.call(this);\n \n let loginInfoKey = '';\n let configKey = '';\n \n try {\n const loginInfo = source.getLoginInfoMap() || {};\n loginInfoKey = String(loginInfo['晋江登录Token'] || '').trim();\n } catch (e) {}\n \n configKey = String(config.jj_token || '').trim();\n \n let key = '';\n if (loginInfoKey.length > 0) {\n key = loginInfoKey;\n } else if (configKey.length > 0) {\n key = configKey;\n }\n \n return java.base64Encode(key);\n}\n\nfunction isBookSyncEnabled() {\n try {\n let config = getConfig.call(this);\n return (config.book_sync || \"off\") == \"on\";\n } catch (e) {\n return false;\n }\n}\n\nfunction getConfig() {\n let { source } = this;\n try {\n let configStr = source.getVariable();\n if (!configStr || configStr.trim() === \"\") {\n return { host: getServerHost.call(this) };\n }\n let config = JSON.parse(configStr);\n if (!config || !Array.isArray(config) || config.length === 0) {\n return { host: getServerHost.call(this) };\n }\n return config[0] || { host: getServerHost.call(this) };\n } catch (e) {\n return { host: getServerHost.call(this) };\n }\n}\n\nfunction setConfig(key, value) {\n let { source } = this;\n try {\n let config = getConfig.call(this);\n config[key] = value;\n source.setVariable(JSON.stringify([config]));\n return true;\n } catch (e) {\n return false;\n }\n}\n\nfunction setConfigs(obj) {\n let { source } = this;\n try {\n let config = getConfig.call(this);\n for (let k in obj) {\n if (obj.hasOwnProperty(k)) {\n config[k] = obj[k];\n }\n }\n source.setVariable(JSON.stringify([config]));\n return true;\n } catch (e) {\n return false;\n }\n}\n\nfunction getSessionId() {\n const { cookie, source } = this;\n try {\n let cookieHeader = String(cookie.getCookie('fanqienovel.com'));\n let sessionId = cookieHeader.match(\/sessionid=([^;]+)\/)?.[1] || null;\n if (sessionId) {\n return sessionId;\n }\n } catch (e) {}\n try {\n let loginInfo = source.getLoginInfoMap() || {};\n return loginInfo['番茄登录Token'] || \"\";\n } catch (e) {\n return \"\";\n }\n}\n\nfunction splitArray(input, size) {\n let output = []\n for (let i = 0; i < input.length; i += size) {\n output.push(input.slice(i, i + size))\n }\n return output\n}\n\nfunction getCommentIconColor() {\n let {\n source\n } = this;\n let color = \"#666666\";\n let loginInfo = source.getLoginInfoMap() || \"\";\n if (loginInfo['段评图标颜色'] && loginInfo['段评图标颜色'].startsWith('#')) {\n color = loginInfo['段评图标颜色'];\n }\n return color;\n}\n\nfunction wrapContentForIOS(content) {\n if (content == null || content == undefined) {\n return '';\n }\n if (typeof content !== 'string') {\n try {\n content = String(content);\n } catch (e) {\n return '';\n }\n }\n const pureContent = content.trim();\n if (!deviceType.call(this)) {\n return pureContent;\n }\n let lines = pureContent.split('\\n').map(line => {\n if (line && line.trim()) {\n const trimLine = line.trim().replace(\/\\s+\/g, ' ');\n return `<div rs-native>${trimLine}<\/div>`;\n }\n return line || '';\n });\n return lines.join('\\n');\n}\n\nfunction createCommentHtmlTag(displayText, url, sourceName) {\n url = url + (url.includes('?') ? '&' : '?') + 'key=' + getSecretKey.call(this);\n const encodedUrl = url.replace(\/&\/g, \"&\").replace(\/'\/g, \"\\\\'\");\n return `<comment count=\"${displayText}\" onPress=\"java.showReadingBrowser('${encodedUrl}','${sourceName}段评')\">`;\n}\n\n\nfunction buildCommentUrl(sourceType, bid, cid, para) {\n let host = getServerHost.call(this);\n\n switch (sourceType) {\n case 'jj':\n return host + '\/jj_comment?bid=' + bid + '&cid=' + cid + '¶=' + para;\n case 'qm':\n return host + '\/qm_comment?item_id=' + cid + '&book_id=' + bid + '¶graph_id=' + para;\n case 'qq':\n return host + '\/qq_comment?bid=' + bid + '&cid=' + cid + '¶=' + para;\n case 'qd':\n return host + '\/qd_comment?bid=' + bid + '&cid=' + cid + '¶=' + para;\n case 'fq':\n default:\n return host + '\/idea_comment?item_id=' + cid + '&book_id=' + bid + '¶=' + para + '&sessionid=' + getSessionId.call(this);\n }\n}\n\nfunction getCommentSourceName(sourceType) {\n switch (sourceType) {\n case 'jj':\n return '晋江';\n case 'qm':\n return '七猫';\n case 'qq':\n return '企鹅';\n case 'qd':\n return '起点';\n case 'fq':\n default:\n return '番茄';\n }\n}\n\nfunction startCommentBrowser(url, title) {\n let { java, source } = this;\n \n let userHeight = source.getLoginInfoMap().get(\"📐段评高度%\");\n let heightPercentage = 0.75; \/\/ 默认\n if (userHeight) {\n let h = parseFloat(userHeight);\n if (!isNaN(h)) {\n if (h > 1) h = h \/ 100;\n heightPercentage = Math.min(1, Math.max(0, h));\n }\n }\n \n let userRadius = source.getLoginInfoMap().get(\"📐段评圆角,填0-50之间\");\n let cornerRadius = 20; \/\/ 默认20\n if (userRadius) {\n let r = parseInt(userRadius);\n if (!isNaN(r)) {\n cornerRadius = Math.min(50, Math.max(0, r)); \/\/ 限制在0-50之间\n }\n }\n \n if (isQRead.call(this)) {\n java.startBrowserDp(url, title);\n } else {\n let isModifiedVersion = checkEnv.call(this);\n if (isModifiedVersion) {\n try {\n let htmlContent = java.ajax(url);\n java.showBrowser(\n url,\n htmlContent,\n `window.java=java;`,\n JSON.stringify({\n \"expandedCornersRadius\": cornerRadius,\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\": heightPercentage,\n \"isHideable\": true\n })\n );\n } catch (e) {\n java.showBrowser(\n url,\n null,\n `window.java=java;`,\n JSON.stringify({\n \"expandedCornersRadius\": cornerRadius,\n \"dismissOnTouchOutside\": true,\n \"hardwareAccelerated\": true,\n \"isNestedScrollingEnabled\": true,\n \"isGestureInsetBottomIgnored\": true,\n \"heightPercentage\": heightPercentage,\n \"isHideable\": true\n })\n );\n }\n } else {\n java.startBrowser(url, title);\n }\n }\n}\n\nfunction createSvg(number, color, bid, cid, para, sourceType = 'fq') {\n let { java, source, cache } = this;\n \n let numVal = Number(number);\n if (isNaN(numVal) || numVal <= 0) {\n return '';\n }\n \n var displayText = numVal > 99 ? \"99+\" : String(numVal);\n var date = String(Date.now()).match(\/(\\d{6}$)\/)[1];\n\n let isIOS = deviceType.call(this);\n if (isIOS) {\n let commentUrl = buildCommentUrl.call(this, sourceType, bid, cid, para);\n let sourceName = getCommentSourceName(sourceType);\n return createCommentHtmlTag.call(this, displayText, commentUrl, sourceName);\n }\n\n let isQingRead = isQRead.call(this);\n let isModifiedVersion = checkEnv.call(this);\n \n let customSvgRaw = source.getLoginInfoMap().get(\"自定义气泡SVG\");\n \n let customSvg = \"\";\n if (customSvgRaw) {\n if (typeof customSvgRaw == 'function') {\n try { customSvg = customSvgRaw(); } catch (e) {}\n } else if (typeof customSvgRaw == 'string') {\n customSvg = customSvgRaw;\n } else {\n customSvg = String(customSvgRaw);\n }\n }\n \n const isValidSvg = customSvg && customSvg.trim().startsWith('<svg') && customSvg.trim().endsWith('<\/svg>');\n \n let imageStyle = getConfig.call(this).imgstyle || \"TEXT\";\n \n var svg;\n if (isQingRead || isModifiedVersion) {\n if (isValidSvg) {\n svg = customSvg.replace(\/\\$color\/g, color).replace(\/\\$displayText\/g, displayText);\n } else {\n svg = `<svg width=\"160\" height=\"120\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\"><path d=\"M 55 10 L 120 10 Q 150 10 150 40 L 150 80 Q 150 110 120 110 L 55 110 Q 25 110 25 80 L 25 75 L 3 60 L 25 45 L 25 40 Q 25 10 55 10 Z\" fill=\"none\" stroke=\"${color}\" stroke-width=\"5\" stroke-linejoin=\"round\"\/><text x=\"87\" y=\"77\" font-family=\"Arial, sans-serif\" text-anchor=\"middle\" dominant-baseline=\"middle\" font-size=\"50\" font-weight=\"bold\" fill=\"${color}\">${displayText}<\/text><\/svg>`;\n }\n } else {\n svg = isValidSvg ? customSvg : `<svg width=\"1000\" height=\"909\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\"><path d=\"M80,80 h840 a60,60 0 0 1 60,60 v580 a60,60 0 0 1 -60,60 h-620 l-140,90 v-90 h-80 a60,60 0 0 1 -60,-60 v-580 a60,60 0 0 1 60,-60 z\" fill=\"none\" stroke=\"${color}\" stroke-width=\"16\" stroke-linejoin=\"round\"\/><text x=\"500\" y=\"450\" font-family=\"Arial, sans-serif\" text-anchor=\"middle\" font-size=\"360\" fill=\"${color}\" dy=\"0.3em\">${displayText}<\/text><\/svg>`;\n }\n\n var encodedSvg = java.base64Encode(svg);\n\n const jsFunction = {\n 'jj': 'showJJCmt',\n 'qm': 'showQmCmt',\n 'qq': 'showQQCmt',\n 'qd': 'showQDCmt',\n 'snm': 'showSnmCmt',\n 'fq': 'showCmt'\n }[sourceType] || 'showCmt';\n\n const baseData = `data:image\/svg+xml;base64,${encodedSvg},{'${isModifiedVersion ? 'click' : 'js'}':'${jsFunction}(\"${bid}\",\"${cid}\",\"${para}\",\"${date}\")','style':'${imageStyle}'}`;\n\n if (sourceType == 'qq' && para == 0) return baseData;\n return deviceType.call(this) ? baseData : `<img src=\"${baseData}\">`;\n}\n\nfunction createGodReviewSvg(reviews, bid, cid, paraId) {\n let { java, source, cache } = this;\n \n if (!reviews || reviews.length == 0) return '';\n \n let review = reviews[0];\n let content = review.content || '';\n \n content = content.replace(\/https?:\\\/\\\/[^\\s]+\/g, \"\")\n .replace(\/qdd\\.gg\\\/[^\\s]+\/g, \"\")\n .replace(\/\\[fn=\\d+\\]\/g, \"\");\n \n let displayText = content.trim();\n let maxChars = 21;\n \n if (displayText.length > maxChars) {\n displayText = displayText.substring(0, maxChars) + '…';\n }\n \n if (!displayText) return '';\n \n let isModifiedVersion = checkEnv.call(this);\n \n let tagColor = \"#FF4444\";\n \n let textColor = \"#FF4444\";\n try {\n let loginInfo = source.getLoginInfoMap() || \"\";\n if (loginInfo['神评文字颜色'] && loginInfo['神评文字颜色'].startsWith('#')) {\n textColor = loginInfo['神评文字颜色'];\n }\n } catch(e) {}\n \n let borderColor = \"#CCCCCC\";\n try {\n let loginInfo = source.getLoginInfoMap() || \"\";\n if (loginInfo['神评边框颜色'] && loginInfo['神评边框颜色'].startsWith('#')) {\n borderColor = loginInfo['神评边框颜色'];\n }\n } catch(e) {}\n \n displayText = displayText.replace(\/&\/g, '&')\n .replace(\/<\/g, '<')\n .replace(\/>\/g, '>')\n .replace(\/\"\/g, '"')\n .replace(\/'\/g, ''');\n\n let svg = `<svg viewBox=\"0 0 600 60\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"display:block;margin-top:2px;width:100%;max-width:600px;height:auto;\">\n <rect x=\"2\" y=\"2\" width=\"596\" height=\"56\" fill=\"rgba(255,255,255,0.10)\" rx=\"16\" stroke=\"${borderColor}\" stroke-width=\"0.8\"\/>\n <rect x=\"16\" y=\"14\" width=\"60\" height=\"32\" rx=\"10\" fill=\"${tagColor}\"\/>\n <text x=\"46\" y=\"36\" font-family=\"system-ui, -apple-system, sans-serif\" font-size=\"18\" text-anchor=\"middle\" font-weight=\"600\" fill=\"#FFFFFF\">神评论<\/text>\n <text x=\"90\" y=\"40\" font-family=\"system-ui, -apple-system, sans-serif\" font-size=\"23\" font-weight=\"500\" fill=\"${textColor}\">${displayText}<\/text>\n <\/svg>`;\n \n let encodedSvg = java.base64Encode(svg);\n \n let jsFunction = 'showQDCmt';\n let date = String(Date.now()).match(\/(\\d{6}$)\/)[1];\n \n const baseData = `data:image\/svg+xml;base64,${encodedSvg},{'${isModifiedVersion ? 'click' : 'js'}':'${jsFunction}(\"${bid}\",\"${cid}\",\"${paraId}\",\"${date}\")','style':'full'}`;\n \n return `<img src=\"${baseData}\">`;\n}\n\nfunction createChapterEndSvg(bid, cid, totalCount, authorName, authorExpectText, commentList) {\n let { java, source, cache } = this;\n \n let color = getCommentIconColor.call(this);\n let isModifiedVersion = checkEnv.call(this);\n \n let countText = totalCount > 999 ? \"999+\" : String(totalCount);\n \n var EMOJI = {\n 1:\"👏\",2:\"🌹\",3:\"🤝\",4:\"😁\",5:\"😄\",6:\"🥺\",7:\"🙂\",8:\"😏\",9:\"😙\",10:\"👆🏻🐽\",\n 11:\"🙄\",12:\"😭\",13:\"😵\",14:\"😥\",15:\"🖕🏻\",16:\"🥵\",17:\"😓\",18:\"🤫\",19:\"😂\",20:\"😢\",\n 21:\"😍\",22:\"🤕🔨\",23:\"😑\",24:\"😫\",25:\"🤗\",26:\"🤪\",27:\"🙏\",28:\"😣\",29:\"💪\",30:\"💀\",\n 31:\"😳\",32:\"😎\",33:\"🤭\",34:\"😄👏\",35:\"👍🏻\",36:\"🤓\",37:\"😡\",38:\"🙁\",39:\"😄❓\",40:\"😞\",\n 41:\"😧\",42:\"💋\",43:\"☺️\",44:\"🤬\",45:\"😴\",46:\"🤠🚬\",47:\"😱\",48:\"🐷\",49:\"😪\",50:\"🤐\",\n 51:\"🥴\",52:\"🌙\",53:\"❤️\",54:\"🔪\",55:\"🎁\",56:\"💔\",57:\"👊🏻\",58:\"😒\",59:\"✌🏻️\",60:\"😮\",\n 61:\"🤨\",62:\"😴\",63:\"👏🏻\",64:\"🐲\",65:\"⭐\",66:\"🌧️\",67:\"🍉\",68:\"🍵\",69:\"🔥\",70:\"💯\"\n };\n \n function removeFnEmoji(s) {\n return s ? s.replace(\/\\[fn=\\d+\\]\/g, '') : '';\n }\n \n function replaceFnEmoji(s) {\n return s ? s.replace(\/\\[fn=(\\d+)\\]\/g, function(match, id) {\n return EMOJI[parseInt(id)] || '';\n }) : '';\n }\n \n let commentListArray = [];\n if (commentList && typeof commentList == 'object' && commentList.length !== undefined) {\n commentListArray = commentList;\n } else if (Array.isArray(commentList)) {\n commentListArray = commentList;\n }\n \n let maxComments = 3;\n let validComments = [];\n for (let i = 0; i < commentListArray.length && validComments.length < maxComments; i++) {\n let c = commentListArray[i] || {};\n let content = c.Content || c.content || '';\n if (content && content.trim()) {\n let userName = c.UserName || c.userName || c.nickname || '匿名';\n let likeCount = c.AgreeAmount || c.agreeAmount || c.likeCount || 0;\n let formattedContent = replaceFnEmoji(content.trim());\n validComments.push({\n userName: userName.replace(\/&\/g, '&').replace(\/<\/g, '<').replace(\/>\/g, '>').replace(\/\"\/g, '"').replace(\/'\/g, '''),\n content: formattedContent.replace(\/&\/g, '&').replace(\/<\/g, '<').replace(\/>\/g, '>').replace(\/\"\/g, '"').replace(\/'\/g, '''),\n likeCount: likeCount\n });\n }\n }\n \n if (validComments.length == 0) {\n var emptyHeight = 150;\n var svg = `<svg viewBox=\"0 0 700 ${emptyHeight}\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"display:block;margin-top:16px;width:100%;max-width:700px;height:auto;\">\n <rect x=\"0\" y=\"0\" width=\"700\" height=\"${emptyHeight}\" rx=\"18\" fill=\"none\" stroke=\"${color}\" stroke-width=\"0.5\" opacity=\"0.6\"\/>\n <text x=\"32\" y=\"44\" font-family=\"system-ui, -apple-system, sans-serif\" font-size=\"24\" font-weight=\"700\" fill=\"${color}\" opacity=\"1\">💬 本章说<\/text>\n <text x=\"668\" y=\"44\" text-anchor=\"end\" font-family=\"system-ui, -apple-system, sans-serif\" font-size=\"18\" font-weight=\"600\" fill=\"${color}\" opacity=\"0.9\">${countText}条评论 ›<\/text>\n <line x1=\"28\" y1=\"58\" x2=\"672\" y2=\"58\" stroke=\"${color}\" stroke-width=\"0.3\" opacity=\"0.35\"\/>\n <text x=\"350\" y=\"110\" text-anchor=\"middle\" font-family=\"system-ui, -apple-system, sans-serif\" font-size=\"16\" fill=\"${color}\" opacity=\"0.55\">还没有评论,快来抢沙发吧~<\/text>\n<\/svg>`;\n \n var encodedSvg = java.base64Encode(svg);\n var jsFunction = 'showQDCmt';\n var date = String(Date.now()).match(\/(\\d{6}$)\/)[1];\n var baseData = `data:image\/svg+xml;base64,${encodedSvg},{'${isModifiedVersion ? 'click' : 'js'}':'${jsFunction}(\"${bid}\",\"${cid}\",\"benzhang\",\"${date}\")','style':'FULL'}`;\n return '\\n' + `<img src=\"${baseData}\">`;\n }\n \n let headerHeight = 100;\n let cardHeight = 130;\n let cardGap = 24;\n let bottomPadding = -20;\n let totalHeight = headerHeight + validComments.length * (cardHeight + cardGap) + bottomPadding;\n \n var svg = `<svg viewBox=\"0 0 700 ${totalHeight}\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"display:block;margin-top:16px;width:100%;max-width:700px;height:auto;\">\n <rect x=\"0\" y=\"0\" width=\"700\" height=\"${totalHeight}\" rx=\"18\" fill=\"none\" stroke=\"${color}\" stroke-width=\"0.5\" opacity=\"0.6\"\/>\n <text x=\"32\" y=\"44\" font-family=\"system-ui, -apple-system, sans-serif\" font-size=\"24\" font-weight=\"700\" fill=\"${color}\" opacity=\"1\">💬 本章说<\/text>\n <text x=\"668\" y=\"44\" text-anchor=\"end\" font-family=\"system-ui, -apple-system, sans-serif\" font-size=\"18\" font-weight=\"600\" fill=\"${color}\" opacity=\"0.9\">${countText}条评论 ›<\/text>\n <line x1=\"28\" y1=\"58\" x2=\"672\" y2=\"58\" stroke=\"${color}\" stroke-width=\"0.3\" opacity=\"0.35\"\/>`;\n \n validComments.forEach(function(c, idx) {\n let yPos = 77 + idx * (cardHeight + cardGap);\n let maxCharsPerLine = 23;\n let contentDisplay = c.content;\n \n if (contentDisplay.length > maxCharsPerLine * 2) {\n contentDisplay = contentDisplay.substring(0, maxCharsPerLine * 2) + '…';\n }\n \n let hasTwoLines = contentDisplay.length > maxCharsPerLine;\n let line1Text = hasTwoLines ? contentDisplay.substring(0, maxCharsPerLine) : contentDisplay;\n let line2Text = hasTwoLines ? contentDisplay.substring(maxCharsPerLine) : '';\n \n let likeText = c.likeCount > 999 ? \"999+\" : String(c.likeCount);\n let likeWidth = likeText.length * 13 + 50;\n let likeX = 668 - likeWidth;\n \n let avatarColors = [\"#e07078\", \"#6090d0\", \"#50b878\", \"#d0a860\", \"#9060c8\"];\n let avatarColor = avatarColors[idx % avatarColors.length];\n let initial = c.userName.charAt(0);\n \n let cardBg = idx % 2 == 0 ? 'rgba(128,128,128,0.1)' : 'rgba(128,128,128,0.05)';\n \n svg += `\n <rect x=\"14\" y=\"${yPos}\" width=\"672\" height=\"${cardHeight}\" rx=\"14\" fill=\"${cardBg}\"\/>\n <circle cx=\"50\" cy=\"${yPos + 34}\" r=\"19\" fill=\"${avatarColor}\" opacity=\"0.95\"\/>\n <text x=\"50\" y=\"${yPos + 40}\" text-anchor=\"middle\" font-family=\"system-ui, -apple-system, sans-serif\" font-size=\"16\" font-weight=\"700\" fill=\"#ffffff\">${initial}<\/text>\n <text x=\"82\" y=\"${yPos + 28}\" font-family=\"system-ui, -apple-system, sans-serif\" font-size=\"21\" font-weight=\"700\" fill=\"${color}\" opacity=\"1\">${c.userName}<\/text>\n <text x=\"82\" y=\"${yPos + 72}\" font-family=\"system-ui, -apple-system, sans-serif\" font-size=\"24\" font-weight=\"400\" fill=\"${color}\" opacity=\"1\"> ${line1Text}<\/text>`;\n if (hasTwoLines) {\n svg += `\n <text x=\"82\" y=\"${yPos + 104}\" font-family=\"system-ui, -apple-system, sans-serif\" font-size=\"24\" font-weight=\"400\" fill=\"${color}\" opacity=\"1\"> ${line2Text}<\/text>`;\n }\n svg += `\n <g transform=\"translate(${likeX}, ${yPos + 6})\">\n <rect x=\"0\" y=\"0\" width=\"${likeWidth}\" height=\"28\" rx=\"14\" fill=\"none\" stroke=\"${color}\" stroke-width=\"0.7\" opacity=\"0.7\"\/>\n <g transform=\"translate(9, 4)\">\n <path d=\"M12 8V4a3 3 0 0 0-3-3L5 10v10h10.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM6 20H3a2 2 0 0 1-2-2v-6a2 2 0 0 1 2-2h3\" fill=\"none\" stroke=\"${color}\" stroke-width=\"1.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" transform=\"scale(0.78)\" opacity=\"0.85\"\/>\n <\/g>\n <text x=\"${likeWidth \/ 2 + 8}\" y=\"19\" text-anchor=\"middle\" font-family=\"system-ui, -apple-system, sans-serif\" font-size=\"14\" font-weight=\"600\" fill=\"${color}\" opacity=\"0.85\">${likeText}<\/text>\n <\/g>`;\n });\n \n svg += `\n<\/svg>`;\n \n var encodedSvg = java.base64Encode(svg);\n var jsFunction = 'showQDCmt';\n var date = String(Date.now()).match(\/(\\d{6}$)\/)[1];\n var baseData = `data:image\/svg+xml;base64,${encodedSvg},{'${isModifiedVersion ? 'click' : 'js'}':'${jsFunction}(\"${bid}\",\"${cid}\",\"benzhang\",\"${date}\")','style':'FULL'}`;\n \n return '\\n' + `<img src=\"${baseData}\">`;\n}\n\nfunction checkClickRateLimit(lastCallKey) {\n let { cache } = this;\n let lastCallTime = cache.getFromMemory(lastCallKey) || 0;\n let currentTime = Date.now();\n if (currentTime - lastCallTime < 1000) {\n return false;\n }\n cache.putMemory(lastCallKey, currentTime);\n return true;\n}\n\nfunction handleCommentClick(sourceType, bid, cid, para, date, cacheKey) {\n let { cache } = this;\n let isModifiedVersion = checkEnv.call(this);\n let isQingRead = isQRead.call(this);\n\n if (!isModifiedVersion && !isQingRead) {\n let mname = cacheKey;\n let load = (cache.getFromMemory(mname) ?? \"-\").split(\"-\");\n if (load[0] != \"1\" || load[1] != date) {\n cache.putMemory(mname, \"1-\" + date);\n return;\n }\n let lastCallKey = `last_call_${sourceType}_${bid}_${cid}_${para}`;\n if (!checkClickRateLimit.call(this, lastCallKey)) {\n return;\n }\n }\n\n let commentUrl = buildCommentUrl.call(this, sourceType, bid, cid, para);\n let title = isModifiedVersion ? getCommentSourceName(sourceType) + \"段评\" : (cache.getFromMemory(cacheKey + \"-text\") ?? getCommentSourceName(sourceType) + \"段评\");\n startCommentBrowser.call(this, commentUrl, title);\n}\n\nfunction getComments(content, bid, cid, sourceType = 'fq', contentMd5 = null, extraParams = {}) {\n let { java, cache, source } = this;\n\n if (content == null || content == undefined) {\n return '';\n }\n if (typeof content !== 'string') {\n try {\n content = String(content);\n } catch (e) {\n return '';\n }\n }\n\n let apiBase = getParagraphApiUrl.call(this);\n\n try {\n let config = getConfig.call(this);\n let dialogue = config.dialogue || '';\n let isDialogueOn = (dialogue === 'true');\n\n let rawLines = content.split(\"\\n\").filter(line => line && line.trim());\n\n let lines = [];\n let contentLineMap = [];\n let textLineIdx = [];\n let lineIndex = 0;\n\n rawLines.forEach(line => {\n let trimmed = line.trim();\n let isImgOnlyLine = \/^<img[^>]*>$\/i.test(trimmed);\n let isCacheImgLine = \/^<img[^>]*src=\"[^\"]*cache\\\/images[^\"]*\"[^>]*>$\/i.test(trimmed);\n\n if (isDialogueOn && isImgOnlyLine && !isCacheImgLine) {\n return;\n }\n\n let idx = lines.length;\n lines.push(line);\n\n if (!isImgOnlyLine) {\n let textOnly = String(line)\n .replace(\/<[^>]*>\/g, '')\n .replace(\/ \/g, ' ')\n .replace(\/\\s+\/g, ' ')\n .trim();\n\n if (textOnly.length > 0) {\n contentLineMap[lineIndex] = idx;\n textLineIdx.push(idx);\n lineIndex++;\n }\n } else if (isCacheImgLine) {\n textLineIdx.push(idx);\n }\n });\n\n if (lines.length == 0) {\n return wrapContentForIOS.call(this, content);\n }\n\n let isModifiedVersion = checkEnv.call(this);\n let isQingRead = isQRead.call(this);\n let isIOS = deviceType.call(this);\n\n let apiUrl = buildParagraphUrl.call(this, apiBase, sourceType, bid, cid, contentMd5, extraParams);\n if (!apiUrl) {\n return wrapContentForIOS.call(this, content);\n }\n\n let response;\n try {\n response = java.ajax(apiUrl);\n } catch (e) {\n return wrapContentForIOS.call(this, content);\n }\n\n if (!response || (typeof response == 'string' && response.trim() == '')) {\n return wrapContentForIOS.call(this, content);\n }\n\n let result;\n try {\n result = JSON.parse(response);\n } catch (e) {\n return wrapContentForIOS.call(this, content);\n }\n\n if (!result || result.code !== 0 || !result.data || !result.data.list) {\n return wrapContentForIOS.call(this, content);\n }\n\n let paragraphList = result.data.list;\n if (!Array.isArray(paragraphList) || paragraphList.length == 0) {\n return wrapContentForIOS.call(this, content);\n }\n\n let reviews = [];\n let chapterEndData = null;\n\n if (sourceType == 'qd') {\n try {\n if (result.data.reviews && Array.isArray(result.data.reviews)) {\n reviews = result.data.reviews;\n }\n } catch (e) {\n reviews = [];\n }\n\n try {\n let host = getServerHost.call(this);\n let authorUrl = host + '\/qd_comment?action=comments&bid=' + bid + '&cid=' + cid + '¶=1&author=1';\n let authorResponse;\n try {\n authorResponse = java.ajax(authorUrl);\n } catch (e) {\n authorResponse = null;\n }\n\n if (authorResponse) {\n let authorData;\n try {\n authorData = JSON.parse(authorResponse);\n } catch (e) {\n authorData = null;\n }\n\n if (authorData) {\n const activity = authorData?.Data?.Getchapteractivity;\n\n if (activity && activity.AuthorName) {\n var authorName = activity.AuthorName || \"\";\n var authorSay = activity.AuthorComments || \"\";\n var authorExpect = activity.AskMonthTicket?.AuthorExpectText || \"\";\n var displayText = authorSay || authorExpect || \"\";\n\n if (authorName && displayText) {\n authorName = authorName.replace(\/&\/g, '&').replace(\/<\/g, '<').replace(\/>\/g, '>');\n displayText = displayText.replace(\/&\/g, '&').replace(\/<\/g, '<').replace(\/>\/g, '>');\n\n var textLines = [];\n var temp = displayText;\n var maxChars = 30;\n while (temp.length > maxChars) {\n textLines.push(temp.substring(0, maxChars));\n temp = temp.substring(maxChars);\n }\n if (temp.length > 0) textLines.push(temp);\n\n var textHeight = textLines.length * 30;\n var totalHeight = 60 + textHeight;\n var avatarChar = authorName.charAt(0);\n var avatarColor = \"#FF6B6B\";\n\n var svg = '<svg viewBox=\"0 0 700 ' + totalHeight + '\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"display:block;margin-bottom:8px;width:100%;max-width:700px;height:auto;\">\\n';\n svg += '<rect x=\"0\" y=\"0\" width=\"700\" height=\"' + totalHeight + '\" fill=\"rgba(255,68,68,0.04)\" rx=\"12\" stroke=\"#FF4444\" stroke-width=\"0.3\"\/>\\n';\n svg += '<circle cx=\"50\" cy=\"38\" r=\"22\" fill=\"' + avatarColor + '\"\/>\\n';\n svg += '<text x=\"50\" y=\"44\" font-size=\"20\" text-anchor=\"middle\" font-weight=\"700\" fill=\"#FFFFFF\">' + avatarChar + '<\/text>\\n';\n svg += '<text x=\"86\" y=\"30\" font-size=\"17\" font-weight=\"700\" fill=\"#333333\">' + authorName + '<\/text>\\n';\n svg += '<text x=\"' + (86 + authorName.length * 17 + 6) + '\" y=\"30\" font-size=\"14\" font-weight=\"500\" fill=\"#FF4444\">作家的话<\/text>\\n';\n\n textLines.forEach(function(line, idx) {\n svg += '<text x=\"86\" y=\"' + (60 + idx * 30) + '\" font-size=\"18\" font-weight=\"400\" fill=\"#666666\">' + line + '<\/text>\\n';\n });\n\n svg += '<\/svg>';\n\n var authorSvgBase64 = java.base64Encode(svg);\n var authorDataUrl = 'data:image\/svg+xml;base64,' + authorSvgBase64 + ',{\"style\":\"FULL\"}';\n lines.push('<img src=\"' + authorDataUrl + '\" \/>');\n }\n }\n }\n }\n } catch (e) {}\n\n try {\n let host = getServerHost.call(this);\n let chapterEndUrl = host + '\/paras\/proxy_paragraph?bid=' + bid + '&cid=' + cid;\n let chapterEndResponse;\n try {\n chapterEndResponse = java.ajax(chapterEndUrl);\n } catch (e) {\n chapterEndResponse = null;\n }\n\n if (chapterEndResponse) {\n let chapterEndResult;\n try {\n chapterEndResult = JSON.parse(chapterEndResponse);\n } catch (e) {\n chapterEndResult = null;\n }\n\n if (chapterEndResult) {\n const endActivity = chapterEndResult?.Data?.Getchapteractivity;\n const endComments = chapterEndResult?.Data?.Getchapterendcomments;\n if (endActivity && endComments) {\n chapterEndData = {\n totalCount: endComments.TotalCount || 0,\n authorName: endActivity.AuthorName || '',\n authorExpectText: endActivity.AskMonthTicket?.AuthorExpectText || '',\n commentList: endComments.DataList || []\n };\n }\n }\n }\n } catch (e) {}\n }\n\n let reviewsByParagraph = {};\n try {\n reviews.forEach(review => {\n let paraId = review.paragraph_id;\n if (!reviewsByParagraph[paraId]) {\n reviewsByParagraph[paraId] = [];\n }\n reviewsByParagraph[paraId].push(review);\n });\n } catch (e) {}\n\n lines = lines.map(line => {\n if (typeof line !== 'string') {\n try {\n return String(line);\n } catch (e) {\n return '';\n }\n }\n return line;\n });\n\n let hasTitleComment = false;\n let titleCommentData = null;\n\n paragraphList.forEach((item, index) => {\n let paraId = item.paragraph_id || item.paragraphId;\n let count = item.comment_count || item.textCount || 0;\n let hot = (item.hot == 1 || item.is_hot_comment == 1 || item.isHotComment == true);\n if (count <= 0) return;\n\n let textPos;\n\n if (sourceType == 'fq') {\n let contentLineIndex = parseInt(paraId);\n if (contentLineIndex < 0 || contentLineIndex >= contentLineMap.length) {\n return;\n }\n textPos = contentLineMap[contentLineIndex];\n } else if (sourceType == 'qq') {\n if (paraId == 0) {\n hasTitleComment = true;\n titleCommentData = item;\n return;\n }\n textPos = parseInt(paraId) - 1;\n } else if (sourceType == 'qd') {\n if (paraId == -1) {\n hasTitleComment = true;\n titleCommentData = item;\n return;\n }\n textPos = parseInt(paraId) - 1;\n } else if (sourceType == 'qm') {\n textPos = index;\n } else if (sourceType == 'snm') {\n textPos = parseInt(paraId) - 1;\n } else if (sourceType == 'jj') {\n textPos = parseInt(paraId) - 1;\n } else {\n textPos = parseInt(paraId);\n }\n\n if (textPos < 0 || textPos >= lines.length) return;\n\n let lineIndex = textPos;\n let lineContent = lines[lineIndex];\n\n if (typeof lineContent !== 'string') return;\n\n let isCacheImgLine = \/<img[^>]*src=\"[^\"]*cache\\\/images[^\"]*\"[^>]*>\/i.test(lineContent);\n\n if (isCacheImgLine && isDialogueOn) {\n let jsFunction = {\n 'jj': 'showJJCmt',\n 'qm': 'showQmCmt',\n 'qq': 'showQQCmt',\n 'qd': 'showQDCmt',\n 'snm': 'showSnmCmt',\n 'fq': 'showCmt'\n }[sourceType] || 'showCmt';\n\n let meta = `{'click':'${jsFunction}(\"${bid}\",\"${cid}\",\"${paraId}\",\"${count}\")','style':'FULL'}`;\n let srcMatch = lineContent.match(\/src=\"([^\"]+cache\\\/images[^\"]+)\"\/i);\n if (srcMatch) {\n lines[lineIndex] = lineContent.replace(\n \/<img[^>]*src=\"[^\"]*cache\\\/images[^\"]*\"[^>]*\\\/?>\/i,\n '<img src=\"' + srcMatch[1] + ',' + meta + '\" \/>'\n );\n }\n return;\n }\n\n let textOnly = lineContent.replace(\/<img[^>]*>\/gi, '').replace(\/ \/g, ' ').trim();\n\n if (!textOnly) return;\n\n if (sourceType == 'fq') {\n if (!isModifiedVersion && !isQingRead) {\n cache.putMemory(`fq-${bid}-${cid}-${paraId}-text`, textOnly);\n }\n } else if (!isModifiedVersion && !isQingRead) {\n cache.putMemory(`${sourceType}-${bid}-${cid}-${paraId}-text`, lineContent);\n }\n\n let color = hot ? \"#f06060\" : getCommentIconColor.call(this);\n let commentTag = createSvg.call(this, count, color, bid, cid, paraId, sourceType);\n\n let reviewTag = '';\n if (sourceType == 'qd' && reviewsByParagraph[paraId]) {\n reviewTag = createGodReviewSvg.call(this, reviewsByParagraph[paraId], bid, cid, paraId);\n }\n\n if (isIOS) {\n lines[lineIndex] += commentTag;\n if (reviewTag) {\n lines[lineIndex] += '<br>' + reviewTag;\n }\n } else {\n if (commentTag.startsWith('data:image')) {\n lines[lineIndex] += `<img src=\"${commentTag}\">`;\n } else {\n lines[lineIndex] += commentTag;\n }\n if (reviewTag) {\n if (reviewTag.startsWith('data:image')) {\n lines[lineIndex] += '\\n' + `<img src=\"${reviewTag}\">`;\n } else {\n lines[lineIndex] += '\\n' + reviewTag;\n }\n }\n }\n });\n\n let resultContent = lines.join(\"\\n\");\n if (sourceType == 'qd' && chapterEndData && chapterEndData.totalCount > 0) {\n let chapterEndSvg = createChapterEndSvg.call(this, bid, cid, chapterEndData.totalCount, chapterEndData.authorName, chapterEndData.authorExpectText, chapterEndData.commentList);\n resultContent += chapterEndSvg;\n }\n\n resultContent = resultContent.replace(\/(<img[^>]*>)<\\\/img>\/gi, '$1');\n resultContent = resultContent.replace(\/<\\\/img>\/gi, '');\n\n return wrapContentForIOS.call(this, resultContent);\n } catch (e) {\n return wrapContentForIOS.call(this, content);\n }\n}\n\nfunction buildParagraphUrl(apiBase, sourceType, bid, cid, contentMd5, extraParams) {\n let secretKey = getSecretKey.call(this);\n switch (sourceType) {\n case 'jj':\n let jjUrl = `${apiBase}?source=jj&book_id=${bid}&chapter_id=${cid}`;\n if (extraParams.bookid && extraParams.chapterid) {\n jjUrl += `&bookid=${extraParams.bookid}&chapterid=${extraParams.chapterid}`;\n }\n let token = JJtoken.call(this);\n if (token) jjUrl += `&token=${token}`;\n return jjUrl;\n case 'qm':\n if (!contentMd5) return null;\n return `${apiBase}?source=qm&book_id=${bid}&chapter_id=${cid}&content_md5=${contentMd5}`;\n case 'qq':\n return `${apiBase}?source=qq&book_id=${bid}&chapter_id=${cid}`;\n case 'qd':\n return `${apiBase}?source=qd&book_id=${bid}&chapter_id=${cid}`;\n case 'fq':\n default:\n return `${apiBase}?source=fq&book_id=${bid}&chapter_id=${cid}`;\n }\n}\n\nfunction showJJCmt(bid, cid, para, date) {\n handleCommentClick.call(this, 'jj', bid, cid, para, date, `jj-${bid}-${cid}-${para}`);\n}\n\nfunction showQDCmt(bid, cid, para, date) {\n handleCommentClick.call(this, 'qd', bid, cid, para, date, `qd-${bid}-${cid}-${para}`);\n}\n\nfunction showCmt(bid, cid, para, date) {\n handleCommentClick.call(this, 'fq', bid, cid, para, date, `fq-${bid}-${cid}-${para}`);\n}\n\nfunction showQmCmt(bid, cid, para, date) {\n handleCommentClick.call(this, 'qm', bid, cid, para, date, `qm-${bid}-${cid}-${para}`);\n}\n\nfunction showQQCmt(bid, cid, para, date) {\n handleCommentClick.call(this, 'qq', bid, cid, para, date, `qq-${bid}-${cid}-${para}`);\n}\n\nfunction getSortKey() {\n\t let { java } = this;\n let config = getConfig.call(this);\n const sortKey = config.sortkey || '';\n return sortKey;\n}\n\nfunction getTtsTone(sourceType) {\n let config = getConfig.call(this);\n if (sourceType == 'fanqie') {\n return config.fanqie_tts_tone || \"1\";\n } else if (sourceType == 'shuqi') {\n return config.shuqi_tts_tone || \"yifei\";\n }\n return \"\";\n}\n\nfunction getBookData(bookId) {\n let { java, source } = this;\n let newUrl = getServerHost.call(this) + '\/detail?book_id=' + bookId;\n let response = java.ajax(newUrl);\n let responseData = JSON.parse(response);\n \n if (Array.isArray(responseData.data)) {\n return responseData.data[0];\n } else if (responseData.data && typeof responseData.data == 'object') {\n return responseData.data;\n } else if (responseData.code == 0 && responseData.data) {\n return responseData.data;\n }\n return null;\n}\n\nfunction syncReadingHistory(bookId) {\n let { java, source } = this;\n if (bookId) {\n var sessionid = getSessionId.call(this);\n if (sessionid && sessionid.trim() !== \"\") {\n var syncUrl = getServerHost.call(this) + \"\/update_history?book_id=\" + bookId + \"&sessionid=\" + sessionid;\n java.ajax(syncUrl);\n }\n }\n}\n\nfunction buildChapterUrl(x, catalog) {\n let { java } = this;\n const isFanqieApp = ['番茄小说', '番茄短剧', '番茄听书', '番茄畅听'].includes(catalog.source);\n\n let chapterParams = {\n cid: x.cid,\n source: catalog.source\n };\n\n if (catalog.source == '书旗听书') {\n const itemIdMatch = x.url?.match(\/[?&]?item_ids=(\\d+)\/);\n if (itemIdMatch) {\n chapterParams.bookid = catalog.bookid;\n chapterParams.item_ids = itemIdMatch[1];\n }\n }\n\n if (catalog.source == '酷我听书') {\n const kwItemIdMatch = x.url?.match(\/[?&]item_id=(\\d+)\/);\n if (kwItemIdMatch) {\n chapterParams.item_id = kwItemIdMatch[1];\n }\n }\n\n if (!isFanqieApp) {\n chapterParams.url = catalog.url;\n } else {\n chapterParams.book_id = catalog.bookid;\n chapterParams.item_id = x.url?.match(\/item_id=(\\d+)\/)?.[1] || null;\n }\n\n if (['企鹅看书', 'QQ阅读', '起点'].includes(catalog.source) && x.url && typeof x.url == 'string') {\n let cMatch = x.url.match(\/chapterid=(\\d+)\/);\n if (cMatch) {\n chapterParams.bookid = catalog.bookid;\n chapterParams.chapterid = cMatch[1];\n }\n }\n\n if (catalog.source == '晋江') {\n let chapterIdMatch = x.url?.match(\/chapterId=(\\d+)\/);\n if (chapterIdMatch) {\n chapterParams.novelId = catalog.bookid;\n chapterParams.chapterId = chapterIdMatch[1];\n }\n }\n\n if (catalog.source == '半夏') {\n let bjBidMatch = x.url?.match(\/bookid=(\\d+)\/);\n let bjChapterIdMatch = x.url?.match(\/chapterid=(\\d+)\/);\n let novelIdMatch = x.url?.match(\/novelId=(\\d+)\/);\n let chapterIdMatch = x.url?.match(\/chapterId=(\\d+)\/);\n if (bjBidMatch && bjChapterIdMatch) {\n chapterParams.bookid = bjBidMatch[1];\n chapterParams.chapterid = bjChapterIdMatch[1];\n }\n if (novelIdMatch && chapterIdMatch) {\n chapterParams.novelId = novelIdMatch[1];\n chapterParams.chapterId = chapterIdMatch[1];\n }\n }\n\n if (catalog.source == '七猫小说') {\n chapterParams.qm_url = x.url;\n }\n\n if (catalog.source == '七猫短剧') {\n chapterParams.qm_id = catalog.bookid;\n }\n\n if (catalog.source == '七猫听书') {\n chapterParams.book_id = catalog.bookid;\n let qmItemIdMatch = x.url?.match(\/item_id=(\\d+)\/);\n if (qmItemIdMatch) {\n chapterParams.item_id = qmItemIdMatch[1];\n }\n }\n\n if (catalog.source == '少年梦') {\n let snmBidMatch = x.url?.match(\/bid=(\\d+)\/);\n let snmCidMatch = x.url?.match(\/cid=(\\d+)\/);\n if (snmBidMatch) chapterParams.bookid = snmBidMatch[1];\n if (snmCidMatch) chapterParams.chapterid = snmCidMatch[1];\n }\n\n let commentUrl = \"\";\n if (catalog.source == '七猫小说') {\n const qmBookId = catalog.bookid;\n const qmChapterId = x.url?.match(\/item_id=(\\d+)\/)?.[1] || null;\n if (qmBookId && qmChapterId) {\n commentUrl = `${getServerHost.call(this)}\/qm_comment?book_id=${qmBookId}&item_id=${qmChapterId}&type=chapter`;\n }\n } else if (catalog.source == '晋江' && chapterParams.novelId && chapterParams.chapterId) {\n commentUrl = `${getServerHost.call(this)}\/jj_comment?bid=${chapterParams.novelId}&cid=${chapterParams.chapterId}`;\n } else if (catalog.source == '半夏' && chapterParams.novelId && chapterParams.chapterId) {\n commentUrl = `${getServerHost.call(this)}\/jj_comment?bid=${chapterParams.novelId}&cid=${chapterParams.chapterId}`;\n if (chapterParams.bookid && chapterParams.chapterid) {\n commentUrl += `&bookid=${chapterParams.bookid}&chapterid=${chapterParams.chapterid}`;\n }\n } else if (isFanqieApp) {\n const bookId = catalog.bookid;\n const itemId = x.url?.match(\/item_id=(\\d+)\/)?.[1] || null;\n if (bookId && itemId) {\n commentUrl = `${getServerHost.call(this)}\/idea_comment?book_id=${bookId}&item_id=${itemId}&sessionid=${getSessionId.call(this)}`;\n }\n } else if (catalog.source == '起点' && chapterParams.bookid && chapterParams.chapterid) {\n commentUrl = `${getServerHost.call(this)}\/qd_comment?bid=${chapterParams.bookid}&cid=${chapterParams.chapterid}¶=benzhang`;\n }\n\n chapterParams.commentUrl = commentUrl;\n\n let encodedParams = java.base64Encode(JSON.stringify(chapterParams));\n\n let commentPart = '{\"type\":\"qingci\"}';\n\n return `data:chapterUrl;base64,${encodedParams},${commentPart}`;\n}\n\nfunction showTabTips(catalog) {\n var tabTips = {\n audio: '听书',\n video: '视频',\n comic: '漫画'\n };\n if (tabTips[catalog.tab]) {\n this.java.toast('当前为' + tabTips[catalog.tab] + '模式');\n }\n}\n\nfunction buildSearchResult(key, sourceParam, host) {\n let { java } = this;\n\n let fanqieMatch = null;\n if (key && typeof key === 'string') {\n fanqieMatch = key.match(\/^(\\d{19})@?(番茄小说|番茄听书|番茄漫画|番茄畅听|番茄短剧)?$\/);\n }\n\n if (fanqieMatch) {\n let bookId = fanqieMatch[1];\n let inlineSource = fanqieMatch[2] || '';\n return handleFanqieSearch.call(this, bookId, host, inlineSource);\n } else if (key && key.length == 19 && !Number.isNaN(parseInt(key))) {\n return handleFanqieSearch.call(this, key, host, '');\n } else if (key && (key.length == 6 || key.length == 7 || key.length == 8) && !Number.isNaN(parseInt(key))) {\n return handleJjwxcSearch.call(this, key, host);\n } else {\n return host + '\/search?login=search&key={{encodeURIComponent(key)}}&page={{page}}' + sourceParam;\n }\n}\n\nfunction handleFanqieSearch(key, host, inlineSource) {\n let { java } = this;\n\n var u = host + `\/detail?book_id=${key}`;\n var kdy = java.base64Encode(u);\n var response = JSON.parse(java.ajax(u));\n var nam = response.data.book_name || (response.data[0] && response.data[0].book_name) || '未知';\n\n var validFanqieSources = ['番茄小说', '番茄听书', '番茄漫画', '番茄畅听', '番茄短剧'];\n\n var sourceName = '';\n\n if (inlineSource && validFanqieSources.indexOf(inlineSource) !== -1) {\n sourceName = inlineSource;\n }\n\n if (!sourceName) {\n try {\n var customSource = (source.getLoginInfoMap())['自定义源站'];\n if (customSource !== undefined && customSource !== null && String(customSource).trim() !== '') {\n var cs = String(customSource).trim();\n if (validFanqieSources.indexOf(cs) !== -1) {\n sourceName = cs;\n }\n }\n } catch (e) {}\n }\n\n if (!sourceName) {\n var config = getConfig.call(this);\n var cfgSource = config && config.source;\n if (cfgSource && validFanqieSources.indexOf(cfgSource) !== -1) {\n sourceName = cfgSource;\n }\n }\n\n if (!sourceName) {\n sourceName = '番茄小说';\n }\n\n return host + `\/details?url=${kdy}&source=${sourceName}&name=${encodeURIComponent(nam)}`;\n}\n\nfunction handleJjwxcSearch(key, host) {\n let { java } = this;\n var jjApiUrl = `http:\/\/app-cdn.jjwxc.net\/androidapi\/novelbasicinfo?novelId=${key}`;\n var jjResponse = java.ajax(jjApiUrl);\n var jjData = JSON.parse(jjResponse);\n var novelName = jjData.novelName || '未知';\n var encodedUrl = java.base64Encode(jjApiUrl);\n return host + `\/details?url=${encodedUrl}&source=晋江&name=${encodeURIComponent(novelName)}`;\n}\n\nfunction syncReadingProgress(chapterParams) {\n const { java } = this;\n try {\n if (!isBookSyncEnabled.call(this)) return;\n\n let _src = chapterParams.source;\n let _bid, _cid, _tk;\n\n if (['番茄小说','番茄听书','番茄畅听'].indexOf(_src) >= 0 && chapterParams.book_id && chapterParams.item_id) {\n _bid = chapterParams.book_id;\n _cid = chapterParams.item_id;\n let _sid = getSessionId.call(this);\n if (_sid && String(_sid).trim() !== '') _tk = _sid;\n } else if (_src == '晋江' && chapterParams.novelId && chapterParams.chapterId) {\n _bid = chapterParams.novelId;\n _cid = chapterParams.chapterId;\n let _ut = JJtoken.call(this);\n if (_ut) _tk = _ut;\n }\n\n if (_tk) {\n java.ajax(getServerHost.call(this) + \"\/progress?source=\" + encodeURIComponent(_src)\n + \"&token=\" + encodeURIComponent(_tk)\n + \"&book_id=\" + encodeURIComponent(_bid)\n + \"&item_id=\" + encodeURIComponent(_cid));\n }\n } catch (e) {\n java.log(\"同步阅读进度失败: \" + e);\n }\n return;\n}\n\nfunction syncFanqieShelf(isAdd) {\n let { java, book, source } = this;\n try {\n if (!isBookSyncEnabled.call(this)) return;\n if (!book || !book.tocUrl) return;\n\n let tocUrl = String(book.tocUrl);\n let base64Str = tocUrl.replace(\/^data:catalogUrl;base64,\/, '');\n let idx = base64Str.indexOf(',');\n if (idx >= 0) base64Str = base64Str.substring(0, idx);\n let catalog = JSON.parse(java.base64Decode(base64Str));\n\n if (!catalog) return;\n\n let fanqieSources = [\"番茄小说\", \"番茄听书\", \"番茄畅听\", \"番茄漫画\", \"番茄短剧\"];\n if (fanqieSources.indexOf(catalog.source) < 0) return;\n\n let bookId = String(catalog.bookid || \"\");\n if (!bookId) return;\n\n let sessionid = getSessionId.call(this);\n if (!sessionid) {\n java.toast(\"未获取到番茄登录状态,请先登录\");\n return;\n }\n let cookie_ = 'sessionid=' + sessionid;\n\n if (isAdd) {\n java.toast(\"正在推送到番茄书架...\");\n let res = java.ajax(\n \"https:\/\/reading.snssdk.com\/reading\/bookapi\/bookshelf\/add\/v?aid=1967,\" +\n JSON.stringify({\n method: \"POST\",\n headers: { \"Cookie\": cookie_ },\n body: JSON.stringify({\n add_book_source: 0,\n identify_data: [{\n asterisked: false,\n book_id: bookId,\n book_type: 0,\n modify_time: 0\n }]\n })\n })\n );\n let data = JSON.parse(res);\n if (data.code == 0) {\n java.toast(\"推送成功\");\n } else {\n java.toast(\"推送失败:\" + (data.message || \"未知错误\"));\n }\n } else {\n java.toast(\"正在从番茄书架移除...\");\n let res = java.ajax(\n \"https:\/\/reading.snssdk.com\/reading\/bookapi\/bookshelf\/delete\/v?aid=1967,\" +\n JSON.stringify({\n method: \"POST\",\n headers: { \"Cookie\": cookie_ },\n body: JSON.stringify({\n identify_data: [{\n book_id: bookId,\n book_type: 0,\n modify_time: Date.now()\n }]\n })\n })\n );\n let data = JSON.parse(res);\n if (data.code == 0) {\n java.toast(\"移除成功\");\n } else {\n java.toast(\"移除失败:\" + (data.message || \"未知错误\"));\n }\n }\n } catch (e) {\n java.log(\"番茄书架同步失败: \" + e);\n java.toast(\"番茄书架同步失败\");\n }\n}\n\nfunction syncJjShelf(isAdd) {\n let { java, book } = this;\n try {\n if (!isBookSyncEnabled.call(this)) return;\n if (!book || !book.tocUrl) return;\n\n let str = String(book.tocUrl);\n let b64 = str.replace(\/^data:catalogUrl;base64,\/, '');\n let idx = b64.indexOf(',');\n if (idx >= 0) b64 = b64.substring(0, idx);\n\n let catalog = JSON.parse(java.base64Decode(b64));\n if (!catalog) return;\n\n if (catalog.source != '晋江') return;\n\n let bookId = String(catalog.bookid || '');\n if (!bookId) {\n let m = String(catalog.url || '').match(\/novelId=(\\d+)\/);\n if (m) bookId = m[1];\n }\n if (!bookId) {\n java.toast(\"无法获取晋江书籍ID\");\n return;\n }\n\n let tokenB64 = JJtoken.call(this);\n if (!tokenB64) {\n java.toast(\"未配置晋江登录Token,无法同步书架\");\n return;\n }\n\n let action = isAdd ? 'add' : 'del';\n let url = getServerHost.call(this) + '\/jj_bookshelf?action=' + action\n + '&book_id=' + bookId\n + '&token=' + encodeURIComponent(tokenB64);\n\n if (isAdd) {\n let config = getConfig.call(this);\n let typeId = config.type_id || \"0\";\n url += '&type=' + typeId;\n }\n\n java.toast(isAdd ? \"正在加入晋江书架...\" : \"正在从晋江书架移除...\");\n\n let data = JSON.parse(java.ajax(url));\n\n if (data.code == \"200\" || data.code == 200) {\n let msg = data?.data?.list?.[0]?.message || data.message || (isAdd ? \"加入成功\" : \"移除成功\");\n java.toast(msg);\n } else {\n java.toast((isAdd ? \"加入失败:\" : \"移除失败:\") + (data.message || \"未知错误\"));\n }\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 = \"G@tY3$jK7#mL2&pW8\";\n var iv = \"C!eH4&sM6@wP9^zX1\";\n var crypto = java.createSymmetricCrypto(\"AES\/CBC\/PKCS5Padding\", key, iv);\n return crypto.decryptStr(encryptedBytes);\n } catch (e) {\n return data;\n }\n}",
"lastUpdateTime": "1791808968283",
"loginUi": "[{\n \"name\": \"邮箱\",\n \"type\": \"text\",\n \"action\": \"\"\n }, {\n \"name\": \"密码\",\n \"type\": \"password\",\n \"action\": \"\"\n }, {\n \"name\": \"⭕账号登录\",\n \"type\": \"button\",\n \"action\": \"login(true)\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.45\n }\n }, {\n \"name\": \"🔑账号注册\",\n \"type\": \"button\",\n \"action\": \"key()\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.45\n }\n }, {\n \t\"name\": \"🔘退出登录\",\n \"type\": \"button\",\n \"action\": \"user_logout()\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.45\n }\n }, {\n \t\"name\": \"🔐修改密码\",\n \"type\": \"button\",\n \"action\": \"password()\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.45\n }\n }, {\n \t\"name\": \"🔮信息查询\",\n \"type\": \"button\",\n \"action\": \"get_user()\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.45\n }\n }, {\n \"name\": \"👤用户后台\",\n \"type\": \"button\",\n \"action\": \"user_center()\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.45\n }\n }, {\n \"name\": \"☕打赏享福利\",\n \"type\": \"button\",\n \"action\": \"km()\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.45\n }\n },\n {\n \"name\": \"📚书山发布页\",\n \"type\": \"button\",\n \"action\": \"fb()\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.45\n }\n }, {\n \"name\": \"🎚服务器切换\",\n \"type\": \"button\",\n \"action\": \"sethost()\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.45\n }\n }, {\n \"name\": \"🔄服务器检测\",\n \"type\": \"button\",\n \"action\": \"checkCurrentServer()\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.45\n }\n }, {\n \"name\": \"❤️段评开关\",\n \"type\": \"button\",\n \"action\": \"toggleParacomment()\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.45\n }\n }, {\n \t\"name\": \"🧭书源更新\",\n \"type\": \"button\",\n \"action\": \"version()\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.45\n }\n }, {\n \"name\": \"📡源站与模式自定义\",\n \"type\": \"button\",\n \"action\": \"SourceSelector()\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 1\n }\n }, {\n \"name\": \"🍅排序筛选\",\n \"type\": \"button\",\n \"action\": \"SortFilter()\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.45\n }\n }, {\n \"name\": \"🎧TTS音色\",\n \"type\": \"button\",\n \"action\": \"TTSSelector()\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.45\n }\n }, {\n \t \"name\": \"📓晋江收藏分类\",\n \"type\": \"button\",\n \"action\": \"getJjShelfClass()\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.45\n }\n }, {\n \"name\": \"📄简化简介\",\n \"type\": \"button\",\n \"action\": \"toggleSimpleIntro()\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.45\n }\n }, {\n \"name\": \"⚡番茄TXT下载\",\n \"type\": \"button\",\n \"action\": \"toggleFqdown('fqdown')\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 1\n }\n }, {\n \"name\": \"显示当前配置信息\",\n \"type\": \"button\",\n \"action\": \"get_cx()\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 1\n }\n }, {\n \"name\": \"🔄番茄\/晋江APP书架同步\",\n \"type\": \"button\",\n \"action\": \"toggleBookSync()\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 1\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 \"name\": \"🍅番茄账号登录\",\n \"type\": \"button\",\n \"action\": \"fq_login()\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.45\n }\n }, {\n \"name\": \"❌退出番茄账号\",\n \"type\": \"button\",\n \"action\": \"logout()\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.45\n }\n }, {\n \"name\": \"段评图标颜色\",\n \"type\": \"text\"\n }, {\n \"name\": \"番茄登录Token\",\n \"type\": \"password\"\n }, {\n \"name\": \"晋江登录Token\",\n \"type\": \"password\"\n }, {\n \"name\": \"🗑️清理缓存\",\n \"type\": \"button\",\n \"action\": \"cleanCache()\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.45\n }\n }, {\n \"name\": \"🔄 强制刷新\",\n \"type\": \"button\",\n \"action\": \"ChapterRefresh()\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.45\n }\n }, {\n \"name\": \"自定义气泡SVG\",\n \"type\": \"text\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 1\n }\n }, {\n \"name\": \"神评文字颜色\",\n \"type\": \"text\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.45\n }\n }, {\n \"name\": \"神评边框颜色\",\n \"type\": \"text\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.45\n }\n }, {\n \"name\": \"下面设置仅在阅读Sigma生效\",\n \"type\": \"button\",\n \"action\": \"\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 1\n }\n }, {\n \"name\": \"📐段评高度%\",\n \"type\": \"text\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 1\n }\n }, {\n \"name\": \"📐段评圆角,填0-50之间\",\n \"type\": \"text\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 1\n }\n }\n]",
"loginUrl": "\/\/请勿修改版本号\nconst localVersion = \"5.65\";\n\nfunction login(flag) {\n const host = getServerHost();\n \n if (flag == undefined) {\n const loginInfo = source.getLoginInfoMap();\n var email = loginInfo['邮箱'];\n var password = loginInfo['密码'];\n } else {\n source.putLoginInfo(JSON.stringify(result));\n var email = result['邮箱'];\n var password = result['密码'];\n }\n \n if (email) email = String(email).replace(\/\\s+\/g, '').trim();\n if (password) password = String(password).replace(\/\\s+\/g, '').trim();\n \n if (!email || !password) {\n java.toast(\"\\n❌ 请先填写账号和密码并点击✓保存后登录\");\n return;\n }\n \n java.toast(\"正在登录...\");\n const url = host + '\/login';\n const body = \"email=\" + encodeURIComponent(email) + \"&password=\" + encodeURIComponent(password);\n \n try {\n const response = java.ajax(url + \",\" + JSON.stringify({\n method: \"POST\",\n body: body,\n timeout: 10000\n }));\n \n const data = JSON.parse(response);\n \n if (data.code == 200) {\n const user = data.data.user;\n const deviceInfo = data.data.device || {};\n \n source.putLoginHeader(user.api_key);\n \n const nickname = user.nickname || \"用户\";\n const maskedEmail = email.includes('@') ? \n email.substring(0, 3) + \"****\" + email.substring(email.indexOf('@')) : \n email;\n const memberStatus = (user.is_member || false) ? \"会员用户\" : \"普通用户\";\n const welcomeMsg = (user.is_member || false) ? \"欢迎回来,尊贵的会员\" : \"欢迎回来\";\n \n const onlineCount = deviceInfo.online_count || 0;\n const deviceLimit = deviceInfo.device_limit || 1;\n const totalCount = deviceInfo.total_count || 0;\n \n var deviceStatusText = \"\";\n if (onlineCount >= deviceLimit && totalCount > 0) {\n deviceStatusText = \"⚠️ 设备已满(\" + onlineCount + \"\/\" + deviceLimit + \")\\n新设备登录将挤掉旧设备\";\n } else if (onlineCount > 0) {\n deviceStatusText = \"📱 在线设备:\" + onlineCount + \"\/\" + deviceLimit;\n } else {\n deviceStatusText = \"📱 当前无其他设备在线\";\n }\n \n function maskDeviceId(deviceId) {\n if (!deviceId || deviceId.length <= 8) return deviceId || '未知';\n return deviceId.substring(0, 4) + \"****\" + deviceId.substring(deviceId.length - 4);\n }\n \n function maskIp(ip) {\n if (!ip) return '';\n if (ip.indexOf('.') !== -1 && ip.indexOf(':') == -1) {\n var parts = ip.split('.');\n if (parts.length == 4) return parts[0] + '.' + parts[1] + '.*.*';\n }\n if (ip.indexOf(':') !== -1) {\n var parts = ip.split(':');\n var validParts = [];\n for (var i = 0; i < parts.length; i++) {\n if (parts[i] !== '') validParts.push(parts[i]);\n }\n if (validParts.length >= 4) return validParts.slice(0, 4).join(':') + ':*:*';\n }\n return ip;\n }\n \n var deviceListText = \"\";\n var deviceList = (deviceInfo && deviceInfo.list) ? deviceInfo.list : [];\n if (deviceList && deviceList.length > 0) {\n deviceListText = \"\\n── 设备列表 ──\";\n for (var i = 0; i < deviceList.length; i++) {\n var dev = deviceList[i];\n var statusIcon = dev.is_online ? '🟢' : '⚫';\n var maskedId = maskDeviceId(dev.device_id);\n var maskedIp = maskIp(dev.ip_address);\n var ipText = maskedIp ? ' | ' + maskedIp : '';\n deviceListText += \"\\n\" + statusIcon + \" \" + maskedId + ipText;\n }\n }\n \n java.toast(\n \"\\n───────────────\\n\" +\n \"🌟 \" + welcomeMsg + \" 🌟\\n\" +\n \"───────────────\\n\" +\n \"昵称:\" + nickname + \"\\n\" +\n \"邮箱:\" + maskedEmail + \"\\n\" +\n \"状态:\" + memberStatus + \"\\n\" +\n \"───────────────\\n\" +\n deviceStatusText +\n deviceListText + \"\\n\" +\n \"───────────────\"\n );\n } else {\n java.toast(\n \"\\n───────────────\\n\" +\n \"❌ 登录失败 ❌\\n\" +\n \"───────────────\\n\" +\n \"错误代码:\" + (data.code || \"未知\") + \"\\n\" +\n \"错误信息:\" + (data.message || \"未知错误\") + \"\\n\" +\n \"───────────────\"\n );\n }\n \n } catch (e) {\n java.toast(\n \"\\n───────────────\\n\" +\n \"❌ 网络错误 ❌\\n\" +\n \"───────────────\\n\" +\n \"连接服务器失败\\n\" +\n \"请检查网络连接\\n\" +\n \"请切换服务器尝试\\n\" +\n \"───────────────\"\n );\n }\n}\n\nfunction user_logout() {\n let hadLogin = false;\n \n if (source.getLoginHeader() && String(source.getLoginHeader()).trim().length > 0) {\n hadLogin = true;\n }\n \n try {\n var apiUrl = getServerHost() + '\/api\/logout';\n var response = java.ajax(apiUrl + \",\" + JSON.stringify({\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application\/json\"\n },\n timeout: 5000\n }));\n } catch (e) {}\n \n try {\n cookie.removeCookie(getServerHost());\n } catch (e) {}\n \n source.putLoginHeader(\"\");\n \n if (!source.getLoginHeader() || String(source.getLoginHeader()).trim().length == 0) {\n java.toast(hadLogin ? \"\\n❗ 已成功退出登录\" : \"\\nℹ️ 当前未登录状态\");\n }\n}\n\nfunction hasValidCustomSource() {\n return false;\n}\n\nfunction updateConfig(key, value, message, gender) {\n if (gender !== undefined) {\n setConfigs.call(this, {[key]: value, gender: gender});\n } else {\n setConfig.call(this, key, value);\n }\n java.toast(message || \"设置已更新\");\n}\n\nfunction get_user() {\n try {\n var apiKey = getSecretKey();\n if (!apiKey) {\n java.toast(\"❌ 未获取到密钥,请先登录\");\n return;\n }\n\n var timestamp = Math.floor(Date.now() \/ 1000);\n var apiUrl = getServerHost() + '\/login';\n \n var postData = {\n key: apiKey,\n timestamp: timestamp\n };\n \n var response = java.ajax(apiUrl + \",\" + JSON.stringify({\n method: \"POST\",\n body: JSON.stringify(postData),\n headers: {\n \"Content-Type\": \"application\/json\"\n },\n timeout: 10000\n }));\n \n var data = JSON.parse(response);\n \n if (data.data.user) {\n var userInfo = data.data.user;\n var deviceInfo = data.data.device || {};\n \n var statusDisplay = \"\";\n switch(userInfo.status) {\n case \"正常\":\n statusDisplay = \"✅ 正常\";\n break;\n case \"会员\":\n statusDisplay = \"👑VIP会员\";\n break;\n case \"封禁\":\n statusDisplay = \"🚫 封禁\";\n break;\n default:\n statusDisplay = userInfo.status || \"正常\";\n }\n \n var email = userInfo.email || \"未绑定\";\n var maskedEmail = \"未绑定\";\n if (email !== \"未绑定\" && email.includes(\"@\")) {\n var parts = email.split(\"@\");\n var username = parts[0];\n var domain = parts[1];\n if (username.length > 2) {\n maskedEmail = username.substring(0, 2) + \"****@\" + domain;\n } else {\n maskedEmail = username + \"****@\" + domain;\n }\n }\n \n var nickname = userInfo.nickname || \"未设置\";\n if (nickname == \"未设置\") {\n nickname = \"👤 \" + nickname;\n } else {\n nickname = \"👤 \" + nickname;\n }\n \n \/\/ 设备信息处理\n var onlineCount = deviceInfo.online_count || 0;\n var deviceLimit = deviceInfo.device_limit || 1;\n var totalCount = deviceInfo.total_count || 0;\n \n var deviceStatusText = \"\";\n if (onlineCount >= deviceLimit && totalCount > 0) {\n deviceStatusText = \"⚠️ 设备已满(\" + onlineCount + \"\/\" + deviceLimit + \")\";\n } else if (onlineCount > 0) {\n deviceStatusText = \"📱 在线设备:\" + onlineCount + \"\/\" + deviceLimit;\n } else {\n deviceStatusText = \"📱 当前无设备在线\";\n }\n \n function maskDeviceId(deviceId) {\n if (!deviceId || deviceId.length <= 8) return deviceId || '未知';\n return deviceId.substring(0, 4) + \"****\" + deviceId.substring(deviceId.length - 4);\n }\n \n function maskIp(ip) {\n if (!ip) return '';\n if (ip.indexOf('.') !== -1 && ip.indexOf(':') == -1) {\n var parts = ip.split('.');\n if (parts.length == 4) return parts[0] + '.' + parts[1] + '.*.*';\n }\n if (ip.indexOf(':') !== -1) {\n var parts = ip.split(':');\n var validParts = [];\n for (var i = 0; i < parts.length; i++) {\n if (parts[i] !== '') validParts.push(parts[i]);\n }\n if (validParts.length >= 4) return validParts.slice(0, 4).join(':') + ':*:*';\n }\n return ip;\n }\n \n function formatTime(timeStr) {\n if (!timeStr || timeStr == \"从未使用\") return \"未知\";\n return timeStr;\n }\n \n var deviceListText = \"\";\n var deviceList = (deviceInfo && deviceInfo.list) ? deviceInfo.list : [];\n if (deviceList && deviceList.length > 0) {\n deviceListText = \"\\n── 设备列表 ──\";\n for (var i = 0; i < deviceList.length; i++) {\n var dev = deviceList[i];\n var statusIcon = dev.is_online ? '🟢' : '⚫';\n var maskedId = maskDeviceId(dev.device_id);\n var maskedIp = maskIp(dev.ip_address);\n var deviceName = dev.device_name || '未知设备';\n var trustedIcon = dev.is_trusted ? ' 🔒' : '';\n var lastSeen = dev.last_seen || '未知';\n \n deviceListText += \"\\n\" + statusIcon + \" \" + maskedId + trustedIcon;\n deviceListText += \"\\n 📱 \" + deviceName + \" | \" + (maskedIp || '未知IP');\n deviceListText += \"\\n ⏰ \" + lastSeen;\n if (i < deviceList.length - 1) {\n deviceListText += \"\\n\";\n }\n }\n } else {\n deviceListText = \"\\n── 设备列表 ──\\n暂无设备记录\";\n }\n \n var displayContent = \n \"\\n────────────\\n\" +\n \"📊 用 户 信 息 📊\\n\" +\n \"────────────\\n\" +\n nickname + \"\\n\" +\n \"────────────\\n\" +\n \"📧 绑定邮箱\\n\" + maskedEmail + \"\\n\" +\n \"────────────\\n\" +\n \"📊 累计阅读\\n\" + (userInfo.total_usage || 0) + \" 次\\n\" +\n \"────────────\\n\" +\n \"📈 今日阅读\\n\" + (userInfo.today_usage || 0) + \" 次\\n\" +\n \"────────────\\n\" +\n \"🔄 账户状态\\n\" + statusDisplay + \"\\n\" +\n \"────────────\\n\" +\n \"📅 注册时间\\n\" + (userInfo.created_at || \"未知\") + \"\\n\" +\n \"────────────\\n\" +\n \"🖥️ 设备管理\\n\" +\n \"总数:\" + totalCount + \" | \" + deviceStatusText + \"\\n\" +\n (deviceInfo.current_device_online ? \"🟢 当前设备在线\\n\" : \"\") +\n (deviceInfo.is_kicked ? \"⚠️ 设备已被踢出:\" + deviceInfo.kicked_reason + \"\\n\" : \"\") +\n deviceListText + \"\\n\";\n \n if (userInfo.is_banned) {\n displayContent += \"────────────\\n\" +\n \"🚫 封禁信息\\n\" + (userInfo.banned_info || \"已封禁\") + \"\\n\";\n }\n \n if (userInfo.last_used && userInfo.last_used !== \"从未使用\") {\n displayContent += \"────────────\\n\" +\n \"⏰ 最后使用\\n\" + userInfo.last_used + \"\\n\";\n }\n \n if (userInfo.is_member) {\n displayContent += \"────────────\\n\" +\n \"💎 会员状态\\n👑 尊贵VIP会员\\n\";\n }\n \n displayContent += \"────────────\\n\" +\n \"✨ 查询完成\";\n \n java.longToast(displayContent);\n \n } else {\n var errorMsg = data.message || \"未知错误\";\n java.longToast(\"❌ 查询失败:\" + errorMsg);\n }\n \n } catch (e) {\n java.longToast(\"❌ 网络连接异常,请稍后重试\");\n }\n}\n\nfunction get_cx() {\n try {\n var currentConfig = getConfig.call(this);\n var currentHost = currentConfig.host || hosts[0];\n var currentSource = currentConfig.source || \"☁️ 聚合搜索 ☁️\";\n\n var currentGender = currentConfig.gender == \"boy\" ? \"男🧸频\" :\n (currentConfig.gender == \"girl\" ? \"女🎀频\" : \"未设置\");\n\n var fanqieTone = currentConfig.fanqie_tts_tone || \"\";\n var shuqiTone = currentConfig.shuqi_tts_tone || \"\";\n var toneParts = [];\n if (fanqieTone) toneParts.push(\"🍅番茄:\" + fanqieTone);\n if (shuqiTone) toneParts.push(\"♟️书旗:\" + shuqiTone);\n var currentTone = toneParts.length > 0 ? toneParts.join(\" \") : \"默认音色\";\n\n var username = \"未登录\";\n try {\n var cookie_ = String(cookie.getKey(\"fanqienovel.com\", \"sessionid\")) ||\n (source.getLoginInfoMap())['番茄登录Token'];\n if (cookie_) {\n var userInfo = JSON.parse(java.ajax(\"https:\/\/fanqienovel.com\/api\/user\/info\/v2,\" + JSON.stringify({\n method: \"GET\",\n headers: { \"Cookie\": \"sessionid=\" + cookie_ },\n timeout: 5000\n })));\n username = userInfo.data?.name || \"已登录(未获取到用户名)\";\n }\n } catch (e) {\n username = \"登录状态检查失败\";\n }\n\n var message =\n \"\\n───────────────\\n\" +\n \"☑️ 配 置 状 态 ☑️\\n\" +\n \"───────────────\\n\" +\n \"🔘 服 务 器 🔘\\n\" + currentHost + \"\\n\" +\n \"───────────────\\n\" +\n \"🌋 书 源 🌋\\n\" + currentSource + \"\\n\" +\n \"───────────────\\n\" +\n \"🏔️ 偏 好 🏔️\\n\" + currentGender + \"\\n\" +\n \"───────────────\\n\" +\n \"🎧 AI 音 色 🎧\\n\" + currentTone + \"\\n\" +\n \"───────────────\\n\" +\n \"🍅 番 茄 账 号 🍅\\n\" + username + \"\\n\" +\n \"───────────────\\n\" +\n \"\\n✨ 提示:点击按钮切换配置\";\n\n java.longToast(message);\n } catch (e) {\n setConfig.call(this, 'host', getHosts()[0]);\n java.toast(\"⚠️ 配置初始化完成,请重试\");\n }\n}\n\nthis.sethost = function() {\n try {\n var currentHost = getConfig.call(this).host || getHosts()[0];\n var currentIndex = getHosts().indexOf(currentHost);\n var newIndex = (currentIndex + 1) % getHosts().length;\n var newHost = getHosts()[newIndex];\n setConfig.call(this, 'host', newHost);\n java.toast(\"\\n切换到:\\n\" + newHost + \"\");\n } catch(e) {\n setConfig.call(this, 'host', getHosts()[0]);\n java.toast(\"\\n已初始化:\" + getHosts()[0]);\n }\n};\n\nfunction Map(e) {\n var infomap = source.getLoginInfoMap();\n const value = infomap[e];\n if (!value) {\n java.longToast(\"请先登录❗\");\n }\n return value;\n}\n\nfunction checkCurrentServer() {\n try {\n var currentHost = getServerHost() +'\/detection';\n java.longToast(`\\n\\n♻️正在检测当前服务器:${currentHost}\\n请稍等~`);\n let date1 = new Date().getTime();\n let html = java.ajax(currentHost);\n let date2 = new Date().getTime();\n let t = date2 - date1;\n let c = String(html).indexOf('书山');\n let time = t \/ 1000 + 's';\n let logTime = '【' + currentHost + '】\\n┋┋\\n' + '解析时间:' + time;\n \n if (c == -1 || t > 5000) {\n java.longToast('\\n💔【访问失败提示】\\n' + '┏┅━┅━┅━┅━┅┅━┅━┅┓\\n┋┋\\n' + logTime + '\\n┋┋\\n♣️当前接口无法访问(可能被墙)♣️\\n┋┋\\n请切换其他接口\/切换网络环境\\n┋┋' + '\\n┗┅━┅━┅━┅━┅┅━┅━┅┛');\n } else if (t < 1000) {\n java.longToast('\\n💖【网络环境优良】\\n' + '┏┅━┅━┅━┅━┅┅━┅━┅┓\\n┋┋\\n' + logTime + '\\n┋┋\\n❤️延迟低,推荐使用此接口❤️\\n┋┋\\n网络环境优良,请继续保持状态\\n┋┋' + '\\n┗┅━┅━┅━┅━┅┅━┅━┅┛');\n } else if (t >= 1000 && t < 2000) {\n java.longToast('\\n💛【网络环境一般】\\n' + '┏┅━┅━┅━┅━┅┅━┅━┅┓\\n┋┋\\n' + logTime + '\\n┋┋\\n♦️延迟一般,勉强可使用♦️\\n┋┋\\n请切换其他接口或切换网络环境\\n┋┋' + '\\n┗┅━┅━┅━┅━┅┅━┅━┅┛');\n } else if (t >= 2000 && t < 5000) {\n java.longToast('\\n💔【网络环境堪忧】\\n' + '┏┅━┅━┅━┅━┅┅━┅━┅┓\\n┋┋\\n' + logTime + '\\n┋┋\\n♠延迟过高,不建议使用♠\\n┋┋\\n请切换其他接口或切换网络环境\\n┋┋' + '\\n┗┅━┅━┅━┅━┅┅━┅━┅┛');\n }\n } catch(e) {\n java.longToast('\\n❌【服务器检测失败】\\n' + '┏┅━┅━┅━┅━┅┅━┅━┅┓\\n┋┋\\n检测过程中发生错误\\n┋┋\\n请检查网络连接或服务器状态\\n┋┋' + '\\n┗┅━┅━┅━┅━┅┅━┅━┅┛');\n }\n}\n\nfunction fq_login() {\n try { \tjava.startBrowserAwait(\"https:\/\/fanqienovel.com\/main\/writer\/login\", \"登录\");\n } catch (e) {\n java.toast(e);\n }\n try {\n cookie.removeCookie(\"snssdk.com\");\n } catch (e) {}\n \n let cookieHeader = String(cookie.getCookie('fanqienovel.com'));\n \n const match = cookieHeader.match(\/sessionid=([^;]+)\/);\n java.toast(match);\n let sessionId = match ? match[1] : null;\n\n let cookieString = sessionId ? \"sessionid=\" + sessionId : source.getLoginInfoMap()['番茄登录Token'] || \"\";\n java.toast(cookieString);\n let user;\n try {\n user = JSON.parse(java.ajax(\"https:\/\/fanqienovel.com\/api\/user\/info\/v2,\" + JSON.stringify({\n method: \"GET\",\n headers: { \"Cookie\": cookieString }\n }))).data.name;\n } catch (e) { \n java.log(e); \n }\n \n if (!cookieString || cookieString == \"sessionid=\" || !user) {\n java.toast(\"未获取到登录凭据,登录失败\");\n return false;\n }\n \n java.toast(\"\\n\\n欢迎 \" + user + \"\\n登录成功!\");\n return true;\n}\n\nfunction logout() {\n cookie.removeCookie(\"fanqienovel.com\");\n cookie.removeCookie(\"snssdk.com\");\n const manualToken = (source.getLoginInfoMap())['番茄登录Token'];\n \n const hasValidToken = manualToken && String(manualToken).trim().length > 0;\n \n if (hasValidToken) {\n java.toast(\"请手动移除填写的Token\");\n } else {\n java.toast(\"退出登录成功\");\n }\n}\n\nfunction toggleParacomment() {\n let config = getConfig.call(this);\n let status = config.yunpara || \"off\";\n if (status == \"on\") {\n setConfigs.call(this, { yunpara: \"off\" });\n java.toast(\"\\n🍅番茄,📓晋江,🐱七猫,🐧QQ阅读,🐧企鹅看书\\n❌段评已关闭\");\n } else {\n setConfigs.call(this, { yunpara: \"on\" });\n java.toast(\"\\n🍅番茄,📓晋江,🐱七猫,🐧QQ阅读,🐧企鹅看书\\n✅段评已开启,需要下载缓存时请关闭\");\n }\n}\n\nfunction toggleImgStyle() {\n let config = getConfig.call(this);\n let current = config.imgstyle || \"TEXT\";\n let next = (current == \"TEXT\") ? \"text\" : \"TEXT\";\n setConfigs.call(this, { imgstyle: next });\n let label = (next == \"TEXT\") ? \"大号\" : \"小号\";\n java.toast(\"\\n🥽段评气泡已切换为:\" + label);\n}\n\nfunction ChapterRefresh() {\n let config = getConfig.call(this);\n let status = config.chapter_refresh_force || \"off\";\n \n if (status == \"on\") {\n setConfigs.call(this, { chapter_refresh_force: \"off\" });\n java.toast(\"\\n☁️ 已切换为正常阅读\");\n } else {\n setConfigs.call(this, { chapter_refresh_force: \"on\" });\n java.toast(\"\\n🔄 绕过缓存强制刷新(建议只在某点和某江缺字时启用)\");\n }\n}\n\nfunction toggleBookSync() {\n let config = getConfig.call(this);\n let status = config.book_sync || \"off\";\n if (status == \"off\") {\n setConfigs.call(this, { book_sync: \"on\" });\n java.toast(\"\\n🍅番茄\/📓晋江\\n✅已开启 - 将自动同步书架与阅读进度\");\n } else {\n setConfigs.call(this, { book_sync: \"off\" });\n java.toast(\"\\n🍅番茄\/📓晋江\\n❌已关闭 - 停止同步书架与阅读进度\");\n }\n}\n\nfunction toggleFqdown(a) {\n let config = getConfig.call(this);\n let status = config[a] || \"off\";\n if (status == \"off\") {\n let obj = {}; obj[a] = \"on\";\n setConfigs.call(this, obj);\n java.toast(\"\\n🍅番茄小说TXT下载功能已开启,在线阅读或换源需要关闭该功能\");\n } else {\n let obj = {}; obj[a] = \"off\";\n setConfigs.call(this, obj);\n java.toast(\"\\n🍅番茄小说TXT全本下载功能已关闭\");\n }\n}\n\nfunction toggleSimpleIntro() {\n let config = getConfig.call(this);\n let status = config.simple_intro || \"off\";\n if (status == \"off\") {\n setConfigs.call(this, { simple_intro: \"on\" });\n java.toast(\"\\n📄 简化简介已开启\\n🍅番茄系列只显示源站和简介\");\n } else {\n setConfigs.call(this, { simple_intro: \"off\" });\n java.toast(\"\\n📄 简化简介已关闭\\n恢复完整简介\");\n }\n}\n\nfunction cleanCache() {\n try {\n var cleanUrl = getServerHost() + '\/cron\/cache';\n \n java.toast(\"正在清理缓存...\");\n \n var response = java.ajax(cleanUrl + \",\" + JSON.stringify({\n method: \"GET\",\n timeout: 5000\n }));\n \n var result = JSON.parse(response);\n \n if (result.isSuccess) {\n java.toast(\"✅ 缓存清理成功\");\n } else {\n java.toast(\"❌ 清理失败:\" + (result.errorMsg || \"未知错误\"));\n }\n \n } catch (e) {\n java.toast(\"❌ 清理失败:\" + e);\n }\n}\n\nfunction SortFilter() {\n try {\n var cfg = getConfig.call(this);\n var savedParams = cfg.sortkey || \"\";\n \n if (savedParams.startsWith('\">')) {\n savedParams = savedParams.substring(2);\n } else if (savedParams.startsWith('>')) {\n savedParams = savedParams.substring(1);\n }\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 {\n --bg: #0a0a0f;\n --text: #e8e8f0;\n --accent: #ff6b35;\n --accent-glow: rgba(255, 107, 53, 0.4);\n --gold: #f0c040;\n}\n\n*{margin:0;padding:0;box-sizing:border-box}\n\nbody{\n font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei',sans-serif;\n background:var(--bg);\n background-image: radial-gradient(ellipse at 30% 20%, #2a1205 0%, #0a0e27 50%, #1a0d1a 100%);\n color:var(--text);\n min-height:100vh;\n padding:20px;\n position:relative;\n overflow-x:hidden;\n}\n\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:0.1;transform:scale(0.8)}50%{opacity:0.9;transform:scale(1.1)}100%{opacity:0.2;transform:scale(0.9)}}\n\n.container{\n width:100%;max-width:640px;margin:0 auto;position:relative;z-index:10;\n background:rgba(15,12,20,0.6);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);\n border-radius:24px;border:1px solid rgba(255,255,255,0.08);\n box-shadow:0 20px 60px rgba(0,0,0,0.6),0 0 60px rgba(255,107,53,0.1);\n overflow:hidden;animation:cardIn 0.6s cubic-bezier(0.22,0.61,0.36,1);\n}\n@keyframes cardIn{from{opacity:0;transform:translateY(20px) scale(0.98)}to{opacity:1;transform:translateY(0) scale(1)}}\n\n.header{padding:28px 24px 20px;text-align:center;border-bottom:1px solid rgba(255,255,255,0.06);background:linear-gradient(180deg,rgba(255,107,53,0.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,0.95);text-shadow:0 0 24px rgba(255,107,53,0.5)}\n.header p{font-size:12px;color:rgba(255,255,255,0.45);margin-top:6px;letter-spacing:2px}\n\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,0.12);border:1px solid rgba(255,107,53,0.3);border-radius:14px;font-size:13px;color:rgba(255,255,255,0.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:0.6;transform:scale(1.2)}}\n.current-badge strong{color:#fff;font-weight:600}\n\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,0.85);margin-bottom:12px;letter-spacing:0.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,0.15);border:1px solid rgba(255,107,53,0.3);color:rgba(255,255,255,0.6);font-weight:400;margin-left:auto}\n\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,0.03);border:1px solid rgba(255,255,255,0.08);cursor:pointer;transition:all 0.3s cubic-bezier(0.22,0.61,0.36,1);text-align:center;overflow:hidden}\n.opt-card:hover{border-color:rgba(255,255,255,0.2);background:rgba(255,255,255,0.06);transform:translateY(-2px);box-shadow:0 8px 24px rgba(0,0,0,0.3)}\n.opt-card.selected{border-color:var(--accent);background:rgba(255,107,53,0.18);box-shadow:0 0 20px rgba(255,107,53,0.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,0.85);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:0 4px}\n.opt-card.selected .opt-name{color:#fff}\n\n.clear-area{padding:20px 24px 0}\n.clear-btn{width:100%;padding:12px;border:1px dashed rgba(255,255,255,0.25);border-radius:14px;background:transparent;font-size:13px;color:rgba(255,255,255,0.5);cursor:pointer;transition:all 0.3s;display:flex;align-items:center;justify-content:center;gap:8px}\n.clear-btn:hover{border-color:rgba(255,107,53,0.6);color:var(--accent);background:rgba(255,107,53,0.06)}\n.clear-btn svg{width:14px;height:14px}\n\n.footer{text-align:center;font-size:11px;color:rgba(255,255,255,0.3);padding:16px 24px 22px;letter-spacing:1px;border-top:1px solid rgba(255,255,255,0.05);margin-top:20px}\n\n@media (max-width:480px){\n .grid{grid-template-columns:repeat(3,1fr);gap:8px}\n .header h1{font-size:20px}\n .section{padding:16px 16px 0}\n .clear-area{padding:16px 16px 0}\n}\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 };\n\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}\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});\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}\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}\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\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 setConfigs.call(this, { sortkey: selectedItems });\n \n if (selectedItems == \"\") {\n java.toast(\"已清除所有筛选条件\");\n } else {\n java.toast(\"设置成功,该设置仅番茄小说发现页生效\");\n }\n \n } catch(e) {\n java.log(e);\n java.toast(\"已取消筛选设置\");\n }\n}\n\nfunction TTSSelector() {\n try {\n var config = getConfig.call(this);\n var currentFanqieTone = config.fanqie_tts_tone || \"\";\n var currentShuqiTone = config.shuqi_tts_tone || \"\";\n \n var fanqieTones = [\n { id: \"80\", name: \"多人对话\/升级\" },\n { id: \"1\", name: \"甜美少女\" },\n { id: \"2\", name: \"清亮青叔\" },\n { id: \"74\", name: \"成熟大叔\/升级\" },\n { id: \"5\", name: \"开朗青年\" },\n { id: \"6\", name: \"温柔淑女\" },\n { id: \"8\", name: \"风雅青叔\" },\n { id: \"12\", name: \"清纯少女\" },\n { id: \"17\", name: \"磁性青叔\" },\n { id: \"29\", name: \"儒雅大叔\" },\n { id: \"30\", name: \"优雅御姐\" },\n { id: \"31\", name: \"斯文青叔\" },\n { id: \"32\", name: \"知性主播\" },\n { id: \"100\", name: \"俏皮御姐\" },\n { id: \"103\", name: \"双音灵动\" }\n ];\n \n var shuqiTones = [\n { id: \"multi_role\", name: \"多角色对话\" },\n { id: \"double_role\", name: \"双角色对话\" },\n { id: \"yifei\", name: \"甜美少女\" },\n { id: \"mingge\", name: \"成熟男人\" },\n { id: \"xiaohou\", name: \"阳光青年\" },\n { id: \"qianxue\", name: \"知性主播\" },\n { id: \"SQ_M03_PB_1218_01\", name: \"沉稳男士\" },\n { id: \"F1029_PB_1128\", name: \"温婉淑女\" },\n { id: \"zh-CN-YunxiNeural\", name: \"讲书博主小帅\" },\n { id: \"zh-CN-XiaochenNeural\", name: \"讲书博主小美\" },\n { id: \"SQ_iVc8TnhZXioTB52v\", name: \"清朗学长\" },\n { id: \"SQ_5Oh4F5zWa8EZbWvr\", name: \"清冷校花\" },\n { id: \"zh_male_quarkttsV1_novel_King196\", name: \"沉稳青叔\" }\n ];\n \n function getToneName(id, list) {\n for (var i = 0; i < list.length; i++) {\n if (list[i].id == id) return list[i].name;\n }\n return id || \"未选择\";\n }\n \n var fanqieName = currentFanqieTone ? getToneName(currentFanqieTone, fanqieTones) : \"未选择\";\n var shuqiName = currentShuqiTone ? getToneName(currentShuqiTone, shuqiTones) : \"未选择\";\n \n function buildTones(list, source, selectedId) {\n var h = '<div class=\"grid\">';\n list.forEach(function(t) {\n var sel = (selectedId == t.id);\n h += '<div class=\"tone-card' + (sel ? ' selected' : '') + '\" data-id=\"' + t.id + '\" data-source=\"' + source + '\" onclick=\"pick(this)\">';\n h += '<div class=\"tone-icon\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z\"\/><path d=\"M19 10v2a7 7 0 0 1-14 0v-2\"\/><line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\"\/><line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\"\/><\/svg><\/div>';\n h += '<div class=\"tone-name\">' + t.name + '<\/div>';\n h += '<\/div>';\n });\n h += '<\/div>';\n return h;\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>TTS音色<\/title>\n<style>\n:root {\n --bg: #0a0a0f;\n --text: #e8e8f0;\n --accent: #ff6b35;\n --accent-glow: rgba(255, 107, 53, 0.4);\n --blue: #4a90e2;\n --blue-glow: rgba(74, 144, 226, 0.4);\n}\n\n*{margin:0;padding:0;box-sizing:border-box}\n\nbody{\n font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei',sans-serif;\n background:var(--bg);\n background-image: radial-gradient(ellipse at 30% 20%, #2a1205 0%, #0a0e27 50%, #1a0d1a 100%);\n color:var(--text);\n min-height:100vh;\n padding:20px;\n position:relative;\n overflow-x:hidden;\n}\n\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:0.1;transform:scale(0.8)}50%{opacity:0.9;transform:scale(1.1)}100%{opacity:0.2;transform:scale(0.9)}}\n\n.container{\n width:100%;max-width:640px;margin:0 auto;position:relative;z-index:10;\n background:rgba(15,12,20,0.6);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);\n border-radius:24px;border:1px solid rgba(255,255,255,0.08);\n box-shadow:0 20px 60px rgba(0,0,0,0.6),0 0 60px rgba(255,107,53,0.1);\n overflow:hidden;animation:cardIn 0.6s cubic-bezier(0.22,0.61,0.36,1);\n}\n@keyframes cardIn{from{opacity:0;transform:translateY(20px) scale(0.98)}to{opacity:1;transform:translateY(0) scale(1)}}\n\n.header{padding:28px 24px 20px;text-align:center;border-bottom:1px solid rgba(255,255,255,0.06);background:linear-gradient(180deg,rgba(255,107,53,0.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,0.95);text-shadow:0 0 24px rgba(255,107,53,0.5)}\n.header p{font-size:12px;color:rgba(255,255,255,0.45);margin-top:6px;letter-spacing:2px}\n\n.current-row{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:20px 24px 0}\n.current-box{padding:12px 16px;background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.08);border-radius:14px}\n.current-box .label{display:flex;align-items:center;gap:6px;font-size:11px;color:rgba(255,255,255,0.45);margin-bottom:4px;letter-spacing:0.5px}\n.current-box .label svg{width:12px;height:12px}\n.current-box .value{font-size:13px;font-weight:600;color:rgba(255,255,255,0.9);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.current-box.fanqie .label svg{color:var(--accent)}\n.current-box.fanqie .value{color:var(--accent)}\n.current-box.shuqi .label svg{color:var(--blue)}\n.current-box.shuqi .value{color:var(--blue)}\n\n.tabs{display:flex;margin:20px 24px 0;padding:4px;background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.08);border-radius:14px;position:relative}\n.tab-btn{flex:1;padding:10px;border:none;background:transparent;font-size:13px;font-weight:600;color:rgba(255,255,255,0.5);cursor:pointer;border-radius:10px;transition:all 0.3s;display:flex;align-items:center;justify-content:center;gap:6px;position:relative;z-index:2}\n.tab-btn svg{width:14px;height:14px}\n.tab-btn.active{color:#fff}\n.tab-indicator{position:absolute;top:4px;left:4px;width:calc(50% - 4px);height:calc(100% - 8px);border-radius:10px;background:linear-gradient(135deg,rgba(255,107,53,0.35),rgba(255,107,53,0.15));border:1px solid rgba(255,107,53,0.4);box-shadow:0 0 16px rgba(255,107,53,0.2);transition:transform 0.3s cubic-bezier(0.22,0.61,0.36,1);z-index:1}\n.tab-indicator.right{transform:translateX(100%)}\n\n.panel{display:none;padding:20px 24px 0}\n.panel.active{display:block}\n\n.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}\n.tone-card{position:relative;padding:14px 6px;border-radius:14px;background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.08);cursor:pointer;transition:all 0.3s cubic-bezier(0.22,0.61,0.36,1);text-align:center;overflow:hidden}\n.tone-card:hover{border-color:rgba(255,255,255,0.2);background:rgba(255,255,255,0.06);transform:translateY(-2px);box-shadow:0 8px 24px rgba(0,0,0,0.3)}\n.tone-card.selected{border-color:var(--accent);background:rgba(255,107,53,0.18);box-shadow:0 0 20px rgba(255,107,53,0.3)}\n.tone-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.tone-icon{display:flex;align-items:center;justify-content:center;margin-bottom:6px}\n.tone-icon svg{width:18px;height:18px;color:var(--accent);filter:drop-shadow(0 0 6px var(--accent-glow));transition:all 0.3s}\n.tone-card.selected .tone-icon svg{color:#fff}\n.tone-name{font-size:12px;font-weight:500;color:rgba(255,255,255,0.85);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:0 4px}\n.tone-card.selected .tone-name{color:#fff}\n\n#panel-shuqi .tone-card.selected{border-color:var(--blue);background:rgba(74,144,226,0.18);box-shadow:0 0 20px rgba(74,144,226,0.3)}\n#panel-shuqi .tone-card.selected::after{background:var(--blue)}\n#panel-shuqi .tone-icon svg{color:var(--blue);filter:drop-shadow(0 0 6px var(--blue-glow))}\n#panel-shuqi .tone-card.selected .tone-icon svg{color:#fff}\n\n.clear-area{padding:20px 24px 0}\n.clear-btn{width:100%;padding:12px;border:1px dashed rgba(255,255,255,0.25);border-radius:14px;background:transparent;font-size:13px;color:rgba(255,255,255,0.5);cursor:pointer;transition:all 0.3s;display:flex;align-items:center;justify-content:center;gap:8px}\n.clear-btn:hover{border-color:rgba(255,107,53,0.6);color:var(--accent);background:rgba(255,107,53,0.06)}\n.clear-btn svg{width:14px;height:14px}\n\n.footer{text-align:center;font-size:11px;color:rgba(255,255,255,0.3);padding:16px 24px 22px;letter-spacing:1px;border-top:1px solid rgba(255,255,255,0.05);margin-top:20px}\n\n@media (max-width:480px){\n .grid{grid-template-columns:repeat(3,1fr);gap:8px}\n .header h1{font-size:20px}\n .panel{padding:16px 16px 0}\n .current-row{margin:16px 16px 0}\n .tabs{margin:16px 16px 0}\n .clear-area{padding:16px 16px 0}\n}\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\"><path d=\"M3 18v-6a9 9 0 0 1 18 0v6\"\/><path d=\"M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z\"\/><\/svg>\n<h1>TTS 音色<\/h1>\n<\/div>\n<p>番茄和书旗独立设置<\/p>\n<\/div>\n<div class=\"current-row\">\n<div class=\"current-box fanqie\">\n<div class=\"label\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"\/><path d=\"M12 6v6l4 2\"\/><\/svg>番茄<\/div>\n<div class=\"value\" id=\"fanqie-value\">` + fanqieName + `<\/div>\n<\/div>\n<div class=\"current-box shuqi\">\n<div class=\"label\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 19.5A2.5 2.5 0 0 1 6.5 17H20\"\/><path d=\"M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z\"\/><\/svg>书旗<\/div>\n<div class=\"value\" id=\"shuqi-value\">` + shuqiName + `<\/div>\n<\/div>\n<\/div>\n<div class=\"tabs\">\n<div class=\"tab-indicator\" id=\"indicator\"><\/div>\n<button class=\"tab-btn active\" data-tab=\"fanqie\" onclick=\"switchTab('fanqie')\">\n<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"\/><path d=\"M12 6v6l4 2\"\/><\/svg>\n番茄\n<\/button>\n<button class=\"tab-btn\" data-tab=\"shuqi\" onclick=\"switchTab('shuqi')\">\n<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 19.5A2.5 2.5 0 0 1 6.5 17H20\"\/><path d=\"M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z\"\/><\/svg>\n书旗\n<\/button>\n<\/div>\n<div class=\"panel active\" id=\"panel-fanqie\">` + buildTones(fanqieTones, \"fanqie\", currentFanqieTone) + `<\/div>\n<div class=\"panel\" id=\"panel-shuqi\">` + buildTones(shuqiTones, \"shuqi\", currentShuqiTone) + `<\/div>\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;\">` + currentFanqieTone + '|' + currentShuqiTone + `<\/div>\n<script>\nvar fanqieTone = '` + currentFanqieTone + `';\nvar shuqiTone = '` + currentShuqiTone + `';\nvar fanqieMap = {};\nvar shuqiMap = {};\nvar fanqieList = ` + JSON.stringify(fanqieTones) + `;\nvar shuqiList = ` + JSON.stringify(shuqiTones) + `;\nfor (var i = 0; i < fanqieList.length; i++) fanqieMap[fanqieList[i].id] = fanqieList[i].name;\nfor (var i = 0; i < shuqiList.length; i++) shuqiMap[shuqiList[i].id] = shuqiList[i].name;\n\nfunction switchTab(tab) {\n document.querySelectorAll('.tab-btn').forEach(function(b) {\n b.classList.toggle('active', b.getAttribute('data-tab') == tab);\n });\n document.querySelectorAll('.panel').forEach(function(p) {\n p.classList.toggle('active', p.id == 'panel-' + tab);\n });\n document.getElementById('indicator').classList.toggle('right', tab == 'shuqi');\n}\n\nfunction pick(el) {\n var id = el.getAttribute('data-id');\n var source = el.getAttribute('data-source');\n if (source == 'fanqie') {\n fanqieTone = (fanqieTone == id) ? '' : id;\n document.querySelectorAll('#panel-fanqie .tone-card').forEach(function(c) {\n c.classList.toggle('selected', c.getAttribute('data-id') == fanqieTone);\n });\n document.getElementById('fanqie-value').textContent = fanqieTone ? (fanqieMap[fanqieTone] || fanqieTone) : '未选择';\n } else {\n shuqiTone = (shuqiTone == id) ? '' : id;\n document.querySelectorAll('#panel-shuqi .tone-card').forEach(function(c) {\n c.classList.toggle('selected', c.getAttribute('data-id') == shuqiTone);\n });\n document.getElementById('shuqi-value').textContent = shuqiTone ? (shuqiMap[shuqiTone] || shuqiTone) : '未选择';\n }\n updateOutput();\n}\n\nfunction clearAll() {\n fanqieTone = '';\n shuqiTone = '';\n document.querySelectorAll('.tone-card').forEach(function(c) { c.classList.remove('selected'); });\n document.getElementById('fanqie-value').textContent = '未选择';\n document.getElementById('shuqi-value').textContent = '未选择';\n updateOutput();\n}\n\nfunction updateOutput() {\n document.getElementById('result-output').textContent = fanqieTone + '|' + shuqiTone;\n}\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, \"TTS音色\", false);\n \n if (!result || !result.body()) {\n java.toast(\"已取消音色设置\");\n return;\n }\n \n var body = result.body();\n var match = body.match(\/<div id=\"result-output\"[^>]*>([^<]*)<\\\/div>\/);\n var newFanqieTone = \"\";\n var newShuqiTone = \"\";\n if (match && match[1]) {\n var parts = match[1].split('|');\n newFanqieTone = parts[0] || \"\";\n newShuqiTone = parts[1] || \"\";\n }\n \n setConfigs.call(this, {\n fanqie_tts_tone: newFanqieTone,\n shuqi_tts_tone: newShuqiTone\n });\n \n function findToneName(list, id) {\n for (var i = 0; i < list.length; i++) {\n if (list[i].id == id) return list[i].name;\n }\n return id;\n }\n \n var msg = '';\n if (newFanqieTone) msg += '番茄: ' + findToneName(fanqieTones, newFanqieTone);\n if (newShuqiTone) msg += (msg ? ' | ' : '') + '书旗: ' + findToneName(shuqiTones, newShuqiTone);\n if (!msg) msg = '已清除音色';\n java.toast(msg);\n \n } catch(e) {\n java.log(e);\n java.toast(\"已取消音色设置\");\n }\n}\n\nfunction AvatarFontSelector() {\n try {\n var config = getConfig.call(this);\n var tx1 = config.tx1 || '';\n var tx2 = config.tx2 || '';\n var ttf = config.ttf || '霞鹜文楷';\n var dialogue = config.dialogue || '';\n var bubble = config.bubble || 'default';\n \n var avatars = [];\n try {\n var avatarApiUrl = getServerHost() + '\/api\/get_config?dialogue=true';\n var avatarResponse = java.ajax(avatarApiUrl);\n var avatarData = JSON.parse(avatarResponse);\n if (Array.isArray(avatarData)) {\n avatars = avatarData;\n }\n } catch(e) {\n java.log(\"获取头像列表失败: \" + e);\n }\n \n var fonts = [];\n try {\n var fontApiUrl = getServerHost() + '\/api\/get_config?ttf=true';\n var fontResponse = java.ajax(fontApiUrl);\n var fontData = JSON.parse(fontResponse);\n if (Array.isArray(fontData)) {\n fonts = fontData;\n }\n } catch(e) {\n java.log(\"获取字体列表失败: \" + e);\n }\n \n var bubbles = [];\n try {\n var bubbleApiUrl = getServerHost() + '\/api\/get_config?bubble=true';\n var bubbleResponse = java.ajax(bubbleApiUrl);\n var bubbleData = JSON.parse(bubbleResponse);\n if (Array.isArray(bubbleData)) {\n bubbles = bubbleData;\n }\n } catch(e) {\n java.log(\"获取气泡样式失败: \" + e);\n }\n \n if (fonts.length == 0) {\n fonts = [\n { id: \"霞鹜文楷\", name: \"霞鹜文楷\" },\n { id: \"仓耳今楷\", name: \"仓耳今楷\" },\n { id: \"MiSans-VF\", name: \"MiSans-VF\" }\n ];\n }\n \n if (bubbles.length == 0) {\n bubbles = [\n { id: \"default\", name: \"默认样式\", desc: \"白色圆角气泡\", preview: { bg_color: '#ffffff', border_color: '#788caa', text_color: '#282828' } },\n { id: \"wechat\", name: \"微信风格\", desc: \"微信绿色气泡\", preview: { bg_color: '#95ec69', border_color: '#95ec69', text_color: '#1e1e1e' } },\n { id: \"qq\", name: \"QQ风格\", desc: \"简洁白色气泡\", preview: { bg_color: '#ffffff', border_color: '#dcdcdc', text_color: '#323232' } },\n { id: \"dark\", name: \"深色风格\", desc: \"适合夜间模式\", preview: { bg_color: '#282832', border_color: '#3c3c46', text_color: '#e6e6f0' } },\n { id: \"pink\", name: \"粉色风格\", desc: \"粉色系气泡\", preview: { bg_color: '#fff0f5', border_color: '#ffb6c1', text_color: '#502832' } },\n { id: \"blue\", name: \"蓝色风格\", desc: \"淡蓝色气泡\", preview: { bg_color: '#f0f8ff', border_color: '#add8e6', text_color: '#1e3246' } },\n { id: \"minimal\", name: \"极简风格\", desc: \"无边框极简\", preview: { bg_color: '#ffffff', border_color: 'transparent', text_color: '#323232' } }\n ];\n }\n \n var fontHtml = '';\n for (var i = 0; i < fonts.length; i++) {\n var font = fonts[i];\n var fontId = font.id || font.name;\n var fontName = font.name || font.id;\n var isSelected = (ttf == fontId);\n fontHtml += '<button onclick=\"selectFont(\\'' + fontId + '\\')\" data-id=\"' + fontId + '\" class=\"font-btn' + (isSelected ? ' selected' : '') + '\">' + fontName + '<\/button>';\n }\n \n var bubbleHtml = '';\n for (var i = 0; i < bubbles.length; i++) {\n var bubbleStyle = bubbles[i];\n var bubbleId = bubbleStyle.id || bubbleStyle.name;\n var bubbleName = bubbleStyle.name || bubbleStyle.id;\n var bubbleDesc = bubbleStyle.desc || '';\n var isSelectedBubble = (bubble == bubbleId);\n var previewBg = bubbleStyle.preview && bubbleStyle.preview.bg_color ? bubbleStyle.preview.bg_color : '#ffffff';\n var previewBorder = bubbleStyle.preview && bubbleStyle.preview.border_color ? bubbleStyle.preview.border_color : '#cccccc';\n var previewText = bubbleStyle.preview && bubbleStyle.preview.text_color ? bubbleStyle.preview.text_color : '#333333';\n \n bubbleHtml += '<div class=\"bubble-slide' + (isSelectedBubble ? ' selected' : '') + '\" onclick=\"selectBubble(\\'' + bubbleId + '\\', this)\" data-id=\"' + bubbleId + '\">';\n bubbleHtml += '<div class=\"bubble-preview\" style=\"background:' + previewBg + ';border-color:' + previewBorder + ';\">';\n bubbleHtml += '<span style=\"color:' + previewText + ';\">你好呀<\/span>';\n bubbleHtml += '<\/div>';\n bubbleHtml += '<span class=\"bubble-name\">' + bubbleName + '<\/span>';\n if (bubbleDesc) {\n bubbleHtml += '<span class=\"bubble-desc\">' + bubbleDesc + '<\/span>';\n }\n bubbleHtml += '<\/div>';\n }\n \n var avatarHtml = '';\n if (avatars.length > 0) {\n avatarHtml += '<div class=\"avatar-selector\" data-side=\"left\">';\n avatarHtml += '<div class=\"avatar-section-title\"><span class=\"title-dot\"><\/span>左侧头像 <span class=\"selection-indicator\" id=\"left-indicator\">' + (tx1 || '未选择') + '<\/span><\/div>';\n avatarHtml += '<div class=\"avatar-grid\">';\n for (var i = 0; i < avatars.length; i++) {\n var avatar = avatars[i];\n var isSelectedLeft = (tx1 == avatar.name);\n avatarHtml += '<div class=\"avatar-item' + (isSelectedLeft ? ' selected' : '') + '\" onclick=\"selectAvatar(\\'left\\', \\'' + avatar.name + '\\', this)\" data-side=\"left\" data-avatar=\"' + avatar.name + '\">';\n avatarHtml += '<div class=\"avatar-img-wrapper\"><img src=\"' + getServerHost() + avatar.url + '\" alt=\"' + avatar.name + '\" loading=\"lazy\" \/><\/div>';\n avatarHtml += '<span>' + avatar.name + '<\/span>';\n avatarHtml += '<\/div>';\n }\n avatarHtml += '<\/div>';\n avatarHtml += '<\/div>';\n \n avatarHtml += '<div class=\"avatar-selector\" data-side=\"right\" style=\"margin-top:24px;\">';\n avatarHtml += '<div class=\"avatar-section-title\"><span class=\"title-dot gold\"><\/span>右侧头像 <span class=\"selection-indicator\" id=\"right-indicator\">' + (tx2 || '未选择') + '<\/span><\/div>';\n avatarHtml += '<div class=\"avatar-grid\">';\n for (var i = 0; i < avatars.length; i++) {\n var avatar = avatars[i];\n var isSelectedRight = (tx2 == avatar.name);\n avatarHtml += '<div class=\"avatar-item' + (isSelectedRight ? ' selected' : '') + '\" onclick=\"selectAvatar(\\'right\\', \\'' + avatar.name + '\\', this)\" data-side=\"right\" data-avatar=\"' + avatar.name + '\">';\n avatarHtml += '<div class=\"avatar-img-wrapper\"><img src=\"' + getServerHost() + avatar.url + '\" alt=\"' + avatar.name + '\" loading=\"lazy\" \/><\/div>';\n avatarHtml += '<span>' + avatar.name + '<\/span>';\n avatarHtml += '<\/div>';\n }\n avatarHtml += '<\/div>';\n avatarHtml += '<\/div>';\n } else {\n avatarHtml = '<div class=\"empty-state\">暂无可用头像<\/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\">\n<title>头像字体设置<\/title>\n<style>\n:root {\n --bg: #0a0a0f;\n --bg-gradient: radial-gradient(ellipse at 30% 20%, #1a1040 0%, #0a0e27 50%, #0d1b3e 100%);\n --text: #e8e8f0;\n --text-secondary: #8888a0;\n --accent: #6c5ce7;\n --accent-glow: rgba(108, 92, 231, 0.4);\n --gold: #f0c040;\n}\n\n*{margin:0;padding:0;box-sizing:border-box}\n\nbody{\n font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Hiragino Sans GB','Microsoft YaHei',sans-serif;\n background:var(--bg);\n background-image:var(--bg-gradient);\n color:var(--text);\n min-height:100vh;\n padding:20px;\n position:relative;\n overflow-x:hidden;\n}\n\n.stars-container{\n position:fixed;\n top:0;\n left:0;\n width:100%;\n height:100%;\n pointer-events:none;\n z-index:0;\n overflow:hidden;\n}\n\n.star{\n position:absolute;\n border-radius:50%;\n background:#fff;\n animation:twinkle var(--duration) ease-in-out infinite alternate;\n animation-delay:var(--delay);\n opacity:0.3;\n}\n\n.star-sm{width:1px;height:1px;--duration:2.5s;opacity:0.3}\n.star-md{width:2px;height:2px;--duration:3.2s;opacity:0.5}\n.star-lg{width:3px;height:3px;--duration:4.5s;opacity:0.7;box-shadow:0 0 4px rgba(255,255,255,0.3)}\n.star-xl{width:4px;height:4px;--duration:5.8s;opacity:0.8;box-shadow:0 0 8px rgba(255,255,255,0.4),0 0 16px rgba(255,255,255,0.1)}\n\n@keyframes twinkle{\n 0%{opacity:0.2;transform:scale(0.8)}\n 50%{opacity:1;transform:scale(1.2)}\n 100%{opacity:0.3;transform:scale(0.9)}\n}\n\n.shooting-star{\n position:absolute;\n width:80px;\n height:1px;\n background:linear-gradient(90deg,rgba(255,255,255,0.8),transparent);\n border-radius:1px;\n animation:shoot var(--shoot-duration) linear infinite;\n animation-delay:var(--shoot-delay);\n opacity:0;\n top:var(--top);\n left:var(--left);\n}\n\n.shooting-star::after{\n content:'';\n position:absolute;\n top:-2px;\n right:0;\n width:4px;\n height:4px;\n border-radius:50%;\n background:#fff;\n box-shadow:0 0 6px rgba(255,255,255,0.6);\n}\n\n@keyframes shoot{\n 0%{transform:translate(0,0) rotate(-35deg);opacity:0}\n 5%{opacity:1}\n 15%{opacity:1}\n 20%{opacity:0;transform:translate(-350px,250px) rotate(-35deg)}\n 100%{opacity:0}\n}\n\n.particles{\n position:fixed;\n top:0;\n left:0;\n width:100%;\n height:100%;\n pointer-events:none;\n z-index:1;\n}\n\n.particle{\n position:absolute;\n border-radius:50%;\n animation:floatUp var(--duration) linear infinite;\n animation-delay:var(--delay);\n opacity:0;\n background:var(--color);\n box-shadow:0 0 12px var(--color);\n}\n\n@keyframes floatUp{\n 0%{opacity:0;transform:translateY(100vh) scale(0.2)}\n 10%{opacity:0.45}\n 90%{opacity:0.45}\n 100%{opacity:0;transform:translateY(-100px) scale(1)}\n}\n\n.container{\n width:100%;\n max-width:680px;\n margin:0 auto;\n position:relative;\n z-index:10;\n background:rgba(10,14,27,0.45);\n backdrop-filter:blur(20px);\n -webkit-backdrop-filter:blur(20px);\n border-radius:28px;\n border:1px solid rgba(255,255,255,0.1);\n box-shadow:0 20px 60px rgba(0,0,0,0.6),0 0 80px rgba(108,92,231,0.15);\n overflow:hidden;\n animation:cardIn 0.8s cubic-bezier(0.22,0.61,0.36,1);\n}\n\n@keyframes cardIn{\n from{opacity:0;transform:translateY(30px) scale(0.98)}\n to{opacity:1;transform:translateY(0) scale(1)}\n}\n\n.header{\n padding:28px 28px 20px;\n border-bottom:1px solid rgba(255,255,255,0.08);\n background:rgba(255,255,255,0.02);\n text-align:center;\n}\n\n.header h1{\n font-size:24px;\n font-weight:700;\n letter-spacing:0.5px;\n color:rgba(255,255,255,0.95);\n text-shadow:0 0 30px rgba(108,92,231,0.5);\n}\n\n.header p{\n font-size:13px;\n color:rgba(255,255,255,0.55);\n margin-top:6px;\n letter-spacing:1px;\n}\n\n.section{\n padding:24px 28px;\n border-bottom:1px solid rgba(255,255,255,0.06);\n}\n\n.section:last-of-type{\n border-bottom:none;\n}\n\n.section-title{\n font-size:14px;\n font-weight:600;\n color:rgba(255,255,255,0.85);\n margin-bottom:16px;\n letter-spacing:0.3px;\n text-transform:uppercase;\n display:flex;\n align-items:center;\n gap:8px;\n}\n\n.title-dot{\n width:8px;\n height:8px;\n border-radius:50%;\n background:var(--accent);\n box-shadow:0 0 8px var(--accent-glow);\n display:inline-block;\n}\n\n.title-dot.gold{\n background:var(--gold);\n box-shadow:0 0 8px rgba(240,192,64,0.4);\n}\n\n.avatar-section-title{\n font-size:13px;\n color:rgba(255,255,255,0.7);\n margin-bottom:12px;\n letter-spacing:0.5px;\n display:flex;\n align-items:center;\n gap:8px;\n flex-wrap:wrap;\n}\n\n.selection-indicator{\n font-size:11px;\n padding:4px 10px;\n border-radius:20px;\n background:rgba(108,92,231,0.2);\n border:1px solid rgba(108,92,231,0.4);\n color:rgba(255,255,255,0.9);\n letter-spacing:0.5px;\n}\n\n.avatar-grid{\n display:grid;\n grid-template-columns:repeat(4,1fr);\n gap:12px;\n}\n\n.avatar-item{\n padding:12px 8px;\n border:2px solid rgba(255,255,255,0.12);\n border-radius:16px;\n background:rgba(255,255,255,0.03);\n cursor:pointer;\n transition:all 0.3s cubic-bezier(0.22,0.61,0.36,1);\n text-align:center;\n position:relative;\n}\n\n.avatar-item:hover{\n border-color:rgba(255,255,255,0.3);\n background:rgba(255,255,255,0.08);\n transform:translateY(-2px);\n box-shadow:0 8px 24px rgba(0,0,0,0.3);\n}\n\n.avatar-item.selected{\n border-color:var(--accent);\n background:rgba(108,92,231,0.15);\n box-shadow:0 0 24px rgba(108,92,231,0.3);\n}\n\n.avatar-item.selected::after{\n content:'✓';\n position:absolute;\n top:6px;\n right:8px;\n font-size:14px;\n color:#fff;\n background:var(--accent);\n border-radius:50%;\n width:20px;\n height:20px;\n display:flex;\n align-items:center;\n justify-content:center;\n box-shadow:0 0 8px rgba(108,92,231,0.6);\n}\n\n.avatar-img-wrapper{\n width:64px;\n height:64px;\n margin:0 auto 8px;\n border-radius:50%;\n overflow:hidden;\n border:2px solid rgba(255,255,255,0.2);\n background:rgba(255,255,255,0.05);\n}\n\n.avatar-item.selected .avatar-img-wrapper{\n border-color:var(--accent);\n box-shadow:0 0 16px rgba(108,92,231,0.4);\n}\n\n.avatar-item img{\n width:100%;\n height:100%;\n object-fit:cover;\n border-radius:50%;\n display:block;\n}\n\n.avatar-item span{\n display:block;\n font-size:11px;\n color:rgba(255,255,255,0.7);\n white-space:nowrap;\n overflow:hidden;\n text-overflow:ellipsis;\n}\n\n.font-grid{\n display:grid;\n grid-template-columns:repeat(3,1fr);\n gap:12px;\n}\n\n.font-btn{\n padding:14px 10px;\n border:1px solid rgba(255,255,255,0.12);\n border-radius:14px;\n background:rgba(255,255,255,0.03);\n cursor:pointer;\n transition:all 0.3s cubic-bezier(0.22,0.61,0.36,1);\n text-align:center;\n font-size:13px;\n font-weight:500;\n color:rgba(255,255,255,0.7);\n}\n\n.font-btn:hover{\n border-color:rgba(255,255,255,0.3);\n background:rgba(255,255,255,0.08);\n transform:translateY(-2px);\n box-shadow:0 8px 24px rgba(0,0,0,0.3);\n}\n\n.font-btn.selected{\n background:rgba(108,92,231,0.2);\n border-color:var(--accent);\n color:#fff;\n box-shadow:0 4px 16px rgba(108,92,231,0.3);\n}\n\n.bubble-slider-wrapper{\n position:relative;\n overflow:hidden;\n padding:4px 0;\n}\n\n.bubble-slider{\n display:flex;\n gap:12px;\n overflow-x:auto;\n scroll-snap-type:x mandatory;\n -webkit-overflow-scrolling:touch;\n scrollbar-width:none;\n -ms-overflow-style:none;\n padding:8px 4px;\n}\n\n.bubble-slider::-webkit-scrollbar{\n display:none;\n}\n\n.bubble-slide{\n flex:0 0 auto;\n scroll-snap-align:center;\n width:110px;\n padding:14px 10px;\n border:2px solid rgba(255,255,255,0.12);\n border-radius:16px;\n background:rgba(255,255,255,0.03);\n cursor:pointer;\n transition:all 0.3s cubic-bezier(0.22,0.61,0.36,1);\n text-align:center;\n position:relative;\n}\n\n.bubble-slide:hover{\n border-color:rgba(255,255,255,0.3);\n background:rgba(255,255,255,0.08);\n transform:translateY(-2px);\n box-shadow:0 8px 24px rgba(0,0,0,0.3);\n}\n\n.bubble-slide.selected{\n border-color:var(--accent);\n background:rgba(108,92,231,0.15);\n box-shadow:0 0 24px rgba(108,92,231,0.3);\n}\n\n.bubble-slide.selected::after{\n content:'✓';\n position:absolute;\n top:6px;\n right:8px;\n font-size:14px;\n color:#fff;\n background:var(--accent);\n border-radius:50%;\n width:20px;\n height:20px;\n display:flex;\n align-items:center;\n justify-content:center;\n box-shadow:0 0 8px rgba(108,92,231,0.6);\n}\n\n.bubble-preview{\n width:100%;\n max-width:70px;\n margin:0 auto 8px;\n padding:8px 12px;\n border-radius:16px;\n border:1px solid #cccccc;\n font-size:12px;\n display:inline-block;\n white-space:nowrap;\n overflow:hidden;\n}\n\n.bubble-name{\n display:block;\n font-size:12px;\n color:rgba(255,255,255,0.8);\n font-weight:500;\n margin-top:4px;\n white-space:nowrap;\n overflow:hidden;\n text-overflow:ellipsis;\n}\n\n.bubble-desc{\n display:block;\n font-size:10px;\n color:rgba(255,255,255,0.4);\n margin-top:2px;\n white-space:nowrap;\n overflow:hidden;\n text-overflow:ellipsis;\n}\n\n.bubble-hint{\n text-align:center;\n font-size:10px;\n color:rgba(255,255,255,0.3);\n margin-top:8px;\n letter-spacing:1px;\n}\n\n.empty-state{\n text-align:center;\n padding:40px 20px;\n color:rgba(255,255,255,0.4);\n font-size:14px;\n letter-spacing:1px;\n}\n\n.switch-section{\n padding:24px 28px;\n border-bottom:1px solid rgba(255,255,255,0.06);\n}\n\n.switch-row{\n display:flex;\n align-items:center;\n justify-content:space-between;\n padding:8px 0;\n}\n\n.switch-label{\n font-size:14px;\n color:rgba(255,255,255,0.85);\n display:flex;\n align-items:center;\n gap:8px;\n}\n\n.switch-desc{\n font-size:12px;\n color:rgba(255,255,255,0.5);\n margin-top:4px;\n}\n\n.switch{\n position:relative;\n width:52px;\n height:28px;\n cursor:pointer;\n}\n\n.switch input{\n opacity:0;\n width:0;\n height:0;\n}\n\n.switch-slider{\n position:absolute;\n top:0;\n left:0;\n right:0;\n bottom:0;\n background:rgba(255,255,255,0.15);\n border-radius:28px;\n transition:all 0.3s cubic-bezier(0.22,0.61,0.36,1);\n border:1px solid rgba(255,255,255,0.2);\n}\n\n.switch-slider::before{\n content:'';\n position:absolute;\n width:22px;\n height:22px;\n left:2px;\n bottom:2px;\n background:#fff;\n border-radius:50%;\n transition:all 0.3s cubic-bezier(0.22,0.61,0.36,1);\n box-shadow:0 2px 8px rgba(0,0,0,0.3);\n}\n\n.switch input:checked + .switch-slider{\n background:var(--accent);\n border-color:var(--accent);\n box-shadow:0 0 16px rgba(108,92,231,0.5);\n}\n\n.switch input:checked + .switch-slider::before{\n transform:translateX(24px);\n}\n\n.clear-area{\n padding:0 28px 28px;\n}\n\n.clear-btn{\n width:100%;\n padding:12px;\n border:1px dashed rgba(255,255,255,0.25);\n border-radius:14px;\n background:transparent;\n font-size:13px;\n color:rgba(255,255,255,0.5);\n cursor:pointer;\n transition:all 0.3s;\n}\n\n.clear-btn:hover{\n border-color:rgba(255,255,255,0.5);\n color:#fff;\n background:rgba(255,255,255,0.05);\n}\n\n.action-hint{\n text-align:center;\n font-size:12px;\n color:rgba(255,255,255,0.35);\n padding:16px 28px 20px;\n border-top:1px solid rgba(255,255,255,0.06);\n letter-spacing:1px;\n}\n\n@media (max-width:480px){\n .avatar-grid{grid-template-columns:repeat(3,1fr)}\n .font-grid{grid-template-columns:repeat(3,1fr)}\n .section{padding:18px 16px}\n .header{padding:20px 16px}\n .avatar-img-wrapper{width:52px;height:52px}\n .bubble-slide{width:100px}\n}\n<\/style>\n<\/head>\n<body>\n<div class=\"stars-container\" id=\"starsContainer\"><\/div>\n<div class=\"particles\" id=\"particles\"><\/div>\n\n<div class=\"container\">\n<div class=\"header\">\n<h1>✦ 头像与字体设置 ✦<\/h1>\n<p>左右头像可以独立选择<\/p>\n<\/div>\n\n<div class=\"switch-section\">\n<div class=\"switch-row\">\n<div>\n<div class=\"switch-label\"><span class=\"title-dot\"><\/span>对话生图功能<\/div>\n<div class=\"switch-desc\">开启后自动将对话内容转换为图片<\/div>\n<\/div>\n<label class=\"switch\">\n<input type=\"checkbox\" id=\"dialogueSwitch\" onchange=\"toggleDialogue(this)\" ` + (dialogue == 'true' ? 'checked' : '') + `>\n<span class=\"switch-slider\"><\/span>\n<\/label>\n<\/div>\n<\/div>\n\n<div class=\"section\">\n<div class=\"section-title\"><span class=\"title-dot\"><\/span>气泡样式 <span style=\"font-size:11px;color:rgba(255,255,255,0.4);text-transform:none;\">← 左右滑动选择 →<\/span><\/div>\n<div class=\"bubble-slider-wrapper\">\n<div class=\"bubble-slider\" id=\"bubbleSlider\">` + bubbleHtml + `<\/div>\n<\/div>\n<div class=\"bubble-hint\">左右滑动查看更多样式<\/div>\n<\/div>\n\n<div class=\"section\">\n<div class=\"section-title\"><span class=\"title-dot\"><\/span>头像选择<\/div>\n` + avatarHtml + `\n<\/div>\n<div class=\"section\">\n<div class=\"section-title\"><span class=\"title-dot\"><\/span>字体选择<\/div>\n<div class=\"font-grid\">` + fontHtml + `<\/div>\n<\/div>\n<div id=\"result-output\" style=\"display:none;\">` + tx1 + '|' + tx2 + '|' + ttf + '|' + dialogue + '|' + bubble + `<\/div>\n<div class=\"clear-area\">\n<button onclick=\"clearAll()\" class=\"clear-btn\">恢复默认设置<\/button>\n<\/div>\n<div class=\"action-hint\">✦ 选择完成后点击右上角确认生效 ✦<\/div>\n<\/div>\n<script>\nvar selectedFont = '` + ttf + `';\nvar selectedTx1 = '` + tx1 + `';\nvar selectedTx2 = '` + tx2 + `';\nvar selectedDialogue = '` + dialogue + `';\nvar selectedBubble = '` + bubble + `';\n\nfunction selectFont(id) {\n selectedFont = id;\n document.querySelectorAll('.font-btn').forEach(function(b) {\n b.classList.remove('selected');\n if (b.getAttribute('data-id') == id) { b.classList.add('selected'); }\n });\n updateResult();\n}\n\nfunction selectAvatar(side, avatarName, element) {\n if (side == 'left') {\n selectedTx1 = avatarName;\n document.getElementById('left-indicator').textContent = avatarName;\n } else {\n selectedTx2 = avatarName;\n document.getElementById('right-indicator').textContent = avatarName;\n }\n \n var selector = element.closest('.avatar-selector');\n selector.querySelectorAll('.avatar-item').forEach(function(item) {\n if (item == element) {\n item.classList.add('selected');\n } else {\n item.classList.remove('selected');\n }\n });\n \n updateResult();\n}\n\nfunction selectBubble(id, element) {\n selectedBubble = id;\n document.querySelectorAll('.bubble-slide').forEach(function(b) {\n b.classList.remove('selected');\n if (b.getAttribute('data-id') == id) { b.classList.add('selected'); }\n });\n updateResult();\n}\n\nfunction toggleDialogue(checkbox) {\n selectedDialogue = checkbox.checked ? 'true' : '';\n updateResult();\n}\n\nfunction updateResult() {\n document.getElementById('result-output').textContent = selectedTx1 + '|' + selectedTx2 + '|' + selectedFont + '|' + selectedDialogue + '|' + selectedBubble;\n}\n\nfunction clearAll() {\n selectedFont = '霞鹜文楷';\n selectedTx1 = '';\n selectedTx2 = '';\n selectedDialogue = '';\n selectedBubble = 'default';\n document.getElementById('left-indicator').textContent = '未选择';\n document.getElementById('right-indicator').textContent = '未选择';\n document.getElementById('dialogueSwitch').checked = false;\n document.querySelectorAll('.font-btn').forEach(function(b) {\n b.classList.remove('selected');\n if (b.getAttribute('data-id') == '霞鹜文楷') { b.classList.add('selected'); }\n });\n document.querySelectorAll('.avatar-item').forEach(function(item) {\n item.classList.remove('selected');\n });\n document.querySelectorAll('.bubble-slide').forEach(function(item) {\n item.classList.remove('selected');\n if (item.getAttribute('data-id') == 'default') { item.classList.add('selected'); }\n });\n updateResult();\n}\n\ndocument.querySelectorAll('.avatar-selector').forEach(function(selector) {\n var side = selector.getAttribute('data-side');\n var selectedName = side == 'left' ? selectedTx1 : selectedTx2;\n if (selectedName) {\n selector.querySelectorAll('.avatar-item').forEach(function(item) {\n if (item.getAttribute('data-avatar') == selectedName) {\n item.classList.add('selected');\n }\n });\n }\n});\n\ndocument.querySelectorAll('.bubble-slide').forEach(function(item) {\n if (item.getAttribute('data-id') == selectedBubble) {\n item.classList.add('selected');\n }\n});\n\nvar bubbleSlider = document.getElementById('bubbleSlider');\nif (bubbleSlider) {\n var selectedBubbleElement = bubbleSlider.querySelector('.bubble-slide.selected');\n if (selectedBubbleElement) {\n selectedBubbleElement.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });\n }\n}\n\n(function() {\n var starsContainer = document.getElementById('starsContainer');\n if (!starsContainer) return;\n \n starsContainer.innerHTML = '';\n \n var starCount = 150;\n var starSizes = ['star-sm', 'star-sm', 'star-sm', 'star-sm', 'star-md', 'star-md', 'star-lg', 'star-xl'];\n \n for (var i = 0; i < starCount; i++) {\n var star = document.createElement('div');\n var sizeClass = starSizes[Math.floor(Math.random() * starSizes.length)];\n star.className = 'star ' + sizeClass;\n \n star.style.left = Math.random() * 100 + '%';\n star.style.top = Math.random() * 100 + '%';\n \n var duration = 1.5 + Math.random() * 4.5;\n star.style.setProperty('--duration', duration + 's');\n star.style.setProperty('--delay', Math.random() * 5 + 's');\n \n star.style.opacity = 0.2 + Math.random() * 0.6;\n \n starsContainer.appendChild(star);\n }\n \n var shootingStars = [\n { top: '5%', left: '80%', delay: 0, duration: 6 },\n { top: '15%', left: '70%', delay: 4, duration: 8 },\n { top: '30%', left: '85%', delay: 9, duration: 7 },\n { top: '45%', left: '75%', delay: 14, duration: 9 },\n { top: '60%', left: '90%', delay: 20, duration: 6.5 },\n { top: '75%', left: '80%', delay: 26, duration: 7.5 }\n ];\n \n shootingStars.forEach(function(s) {\n var meteor = document.createElement('div');\n meteor.className = 'shooting-star';\n meteor.style.setProperty('--top', s.top);\n meteor.style.setProperty('--left', s.left);\n meteor.style.setProperty('--shoot-duration', s.duration + 's');\n meteor.style.setProperty('--shoot-delay', s.delay + 's');\n meteor.style.transform = 'rotate(' + (-25 + Math.random() * 20) + 'deg)';\n starsContainer.appendChild(meteor);\n });\n})();\n\n(function() {\n var particlesContainer = document.getElementById('particles');\n var colors = ['#6c5ce7', '#a78bfa', '#f0c040', '#fbbf24', '#00d2ff', '#7c3aed'];\n for (var j = 0; j < 20; j++) {\n var particle = document.createElement('div');\n particle.className = 'particle';\n particle.style.left = Math.random() * 100 + '%';\n var size = Math.random() * 3 + 2;\n particle.style.width = size + 'px';\n particle.style.height = size + 'px';\n var color = colors[Math.floor(Math.random() * colors.length)];\n particle.style.setProperty('--color', color);\n particle.style.boxShadow = '0 0 ' + (Math.random() * 10 + 6) + 'px ' + color;\n particle.style.setProperty('--duration', Math.random() * 12 + 10 + 's');\n particle.style.setProperty('--delay', Math.random() * 8 + 's');\n particlesContainer.appendChild(particle);\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 \n var newTx1 = '';\n var newTx2 = '';\n var newTtf = '霞鹜文楷';\n var newDialogue = '';\n var newBubble = 'default';\n \n var resultMatch = body.match(\/<div id=\"result-output\"[^>]*>([^<]*)<\\\/div>\/);\n if (resultMatch && resultMatch[1]) {\n var parts = resultMatch[1].split('|');\n newTx1 = parts[0] || '';\n newTx2 = parts[1] || '';\n newTtf = parts[2] || '霞鹜文楷';\n newDialogue = parts[3] || '';\n newBubble = parts[4] || 'default';\n }\n \n setConfigs.call(this, {\n tx1: newTx1,\n tx2: newTx2,\n ttf: newTtf,\n dialogue: newDialogue,\n bubble: newBubble\n });\n \n java.toast(\"头像1: \" + (newTx1 || \"默认\") + \"\\n头像2: \" + (newTx2 || \"默认\") + \"\\n字体: \" + newTtf + \"\\n气泡样式: \" + newBubble + \"\\n对话生图: \" + (newDialogue == 'true' ? \"已开启\" : \"已关闭\"));\n \n } catch(e) {\n java.log(e);\n java.toast(\"已取消设置\");\n }\n}\n\nfunction loginJjScan() {\n try {\n var headers = {\n \"source\": \"android\",\n \"versiontype\":\"reading\",\n \"versioncode\": \"467\",\n \"version-code\": \"467\"\n };\n \n var e = java.ajax(\"https:\/\/android.jjwxc.com\/app.jjwxc\/android\/reading\/Login\/scanCodeLogin?versionCode=467\" + \",\" + JSON.stringify({headers: headers}));\n \/\/java.log(\"scanCodeLogin: \" + e);\n \n var eData = JSON.parse(e).data;\n var jjscanurl = \"https:\/\/www.jjwxc.net?action=scanCode&jjreader_key=\" + eData.jjreader_key + \"&device_key=\" + eData.device_key;\n var enjj = encodeURIComponent(jjscanurl);\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}' +\n 'body{font-family:-apple-system,BlinkMacSystemFont,\"PingFang SC\",\"Microsoft YaHei\",sans-serif;' +\n 'background:#0a0a0f;' +\n '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;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:0.1;transform:scale(0.8)}50%{opacity:0.9;transform:scale(1.1)}100%{opacity:0.2;transform:scale(0.9)}}' +\n '.container{width:100%;max-width:420px;position:relative;z-index:10;' +\n 'background:rgba(15,12,20,0.75);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);' +\n 'border-radius:24px;border:1px solid rgba(255,255,255,0.08);' +\n 'box-shadow:0 20px 60px rgba(0,0,0,0.6),0 0 60px rgba(193,74,99,0.15);' +\n 'overflow:hidden;animation:cardIn 0.6s cubic-bezier(0.22,0.61,0.36,1)}' +\n '@keyframes cardIn{from{opacity:0;transform:translateY(20px) scale(0.98)}to{opacity:1;transform:translateY(0) scale(1)}}' +\n '.header{padding:24px 24px 18px;text-align:center;' +\n 'border-bottom:1px solid rgba(255,255,255,0.06);' +\n 'background:linear-gradient(180deg,rgba(193,74,99,0.15),transparent)}' +\n '.header-title{display:flex;align-items:center;justify-content:center;gap:10px;margin-bottom:6px}' +\n '.header-title svg{width:24px;height:24px;color:#c14a63;filter:drop-shadow(0 0 8px rgba(193,74,99,0.5))}' +\n '.header h1{font-size:20px;font-weight:700;letter-spacing:1px;' +\n '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,0.45);letter-spacing:2px;margin-top:4px}' +\n '.content{padding:24px}' +\n '.qr-wrapper{position:relative;width:240px;height:240px;margin:0 auto 20px;' +\n 'border-radius:20px;background:#fff;padding:12px;' +\n 'box-shadow:0 8px 32px rgba(193,74,99,0.25),0 0 0 1px rgba(255,255,255,0.08);' +\n 'animation:qrGlow 3s ease-in-out infinite}' +\n '@keyframes qrGlow{0%,100%{box-shadow:0 8px 32px rgba(193,74,99,0.25),0 0 0 1px rgba(255,255,255,0.08)}50%{box-shadow:0 8px 40px rgba(193,74,99,0.4),0 0 0 1px rgba(255,255,255,0.15)}}' +\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,0.75);line-height:1.5}' +\n '.step-num{flex-shrink:0;width:22px;height:22px;border-radius:50%;' +\n 'background:rgba(193,74,99,0.2);border:1px solid rgba(193,74,99,0.5);' +\n 'color:#c14a63;font-size:11px;font-weight:700;' +\n 'display:flex;align-items:center;justify-content:center;margin-top:1px}' +\n '.tip{display:flex;align-items:flex-start;gap:8px;padding:12px 14px;' +\n 'background:rgba(240,192,64,0.08);border:1px solid rgba(240,192,64,0.25);' +\n 'border-radius:12px;font-size:12px;color:rgba(240,192,64,0.95);line-height:1.5}' +\n '.tip svg{width:14px;height:14px;flex-shrink:0;margin-top:2px;color:#f0c040}' +\n '.footer{padding:14px 24px 20px;text-align:center;font-size:11px;color:rgba(255,255,255,0.3);' +\n 'border-top:1px solid rgba(255,255,255,0.05);letter-spacing:1px}' +\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\">' +\n '<rect x=\"3\" y=\"3\" width=\"7\" height=\"7\"\/><rect x=\"14\" y=\"3\" width=\"7\" height=\"7\"\/>' +\n '<rect x=\"3\" y=\"14\" width=\"7\" height=\"7\"\/><rect x=\"14\" y=\"14\" width=\"7\" height=\"7\"\/>' +\n '<\/svg>' +\n '<h1>晋江扫码登录<\/h1>' +\n '<\/div>' +\n '<p>SCAN TO LOGIN<\/p>' +\n '<\/div>' +\n '<div class=\"content\">' +\n '<div class=\"qr-wrapper\">' +\n '<img src=\"https:\/\/api.qrtool.cn\/?text=' + enjj + '\">' +\n '<\/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>' +\n '<div class=\"tip\">' +\n '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">' +\n '<circle cx=\"12\" cy=\"12\" r=\"10\"\/><line x1=\"12\" y1=\"16\" x2=\"12\" y2=\"12\"\/><line x1=\"12\" y1=\"8\" x2=\"12.01\" y2=\"8\"\/>' +\n '<\/svg>' +\n '<span>请先扫码授权,再点右上角 ✓。提前点击会导致二维码失效,需要重新扫码<\/span>' +\n '<\/div>' +\n '<\/div>' +\n '<div class=\"footer\">晋江文学城 · 安全登录<\/div>' +\n '<\/div>' +\n '<script>' +\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\";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 u = java.startBrowserAwait(\"data:text\/html;base64,\" + java.base64Encode(html), \"扫码登录\", false).body();\n \n if (u) {\n headers[\"Content-Type\"] = \"application\/octet-stream\";\n var checkLogin = \"https:\/\/android.jjwxc.com\/app.jjwxc\/android\/reading\/Login\/checkJjreaderLogin\";\n var body = encode.call(this, JSON.stringify({\n \"versionCode\": \"467\",\n \"jjreader_key\": eData.jjreader_key,\n \"device_key\": eData.device_key\n }));\n \n var checkLoginpost = {\n \"method\": \"POST\",\n \"body\": body,\n \"headers\": headers\n };\n \n checkLogin = checkLogin + \",\" + JSON.stringify(checkLoginpost);\n var t = java.ajax(checkLogin);\n t = JSON.parse(t).data?.token ?? JSON.parse(t).message;\n \n if (\/\\d+_\/.test(t)) {\n setConfigs.call(this, { jj_token: t });\n java.toast(\"✅ 登录成功\\nToken已保存:\\n\" + t);\n } else {\n java.toast(t);\n }\n }\n } catch (e) {\n java.log(e);\n java.toast(\"扫码登录失败,请重试\");\n }\n}\n\nfunction logoutJj() {\n try {\n setConfigs.call(this, { jj_token: \"\" });\n java.toast(\"✅ 已退出晋江登录\");\n } catch (e) {\n java.toast(\"退出失败:\" + e);\n }\n}\n\nfunction getJjShelfClass() {\n let { java, source } = this;\n try {\n var tokenB64 = JJtoken();\n if (!tokenB64) {\n java.toast(\"请先配置晋江登录Token\");\n return;\n }\n\n var token = java.base64Decode(tokenB64);\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\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 '<i class=\"fas fa-folder\"><\/i>' +\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<link rel=\"stylesheet\" href=\"https:\/\/cdn.bootcdn.net\/ajax\/libs\/font-awesome\/6.5.2\/css\/all.min.css\">\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 i{color:#c14a63}\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-card i{font-size:1.2rem;color:#c14a63;margin-bottom:6px;display:block}\n.class-card.selected i{color:#fff}\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 <i class=\"fas fa-check-circle\"><\/i>\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 SourceSelector() {\n try {\n var config = getConfig.call(this);\n\n var typeToItems = {};\n var typeLabels = { novel: '小说', audio: '听书', comic: '漫画', video: '视频' };\n var typeIcons = {\n novel: 'fa-solid fa-book-open',\n audio: 'fa-solid fa-headphones',\n comic: 'fa-solid fa-images',\n video: 'fa-solid fa-clapperboard'\n };\n var typeKeywords = { novel: '小说', audio: '听书', comic: '漫画', video: '视频' };\n var typeOrder = ['novel', 'audio', 'comic', 'video'];\n\n try {\n var apiUrl = getServerHost() + '\/api\/get_config';\n var resp = JSON.parse(java.ajax(apiUrl));\n if (resp && Array.isArray(resp)) {\n resp.forEach(function (item) {\n if (!item.name || !item.key) return;\n var t = item.type || 'novel';\n if (!typeToItems[t]) typeToItems[t] = [];\n typeToItems[t].push({\n name: item.name,\n key: item.key,\n free: item.free || ''\n });\n });\n }\n } catch (e) {\n java.toast(\"获取源站列表失败,请切换服务器后重试\");\n return;\n }\n\n var currentRaw = config.source || '';\n var currentIsAll = false;\n var currentAllType = '';\n for (var k in typeKeywords) {\n if (typeKeywords[k] == currentRaw) {\n currentIsAll = true;\n currentAllType = k;\n break;\n }\n }\n var selectedNames = [];\n if (currentRaw && !currentIsAll) {\n selectedNames = currentRaw.split(',').map(function (s) { return s.trim(); }).filter(function (s) { return s; });\n }\n var selectedSet = {};\n selectedNames.forEach(function (n) { selectedSet[n] = true; });\n\n var firstType = 'novel';\n if (currentIsAll) {\n firstType = currentAllType;\n } else if (selectedNames.length > 0) {\n for (var t in typeToItems) {\n for (var i = 0; i < typeToItems[t].length; i++) {\n if (typeToItems[t][i].name == selectedNames[0]) { firstType = t; break; }\n }\n if (firstType == t) break;\n }\n }\n\n var currentGender = config.gender || 'boy';\n\n var tabBtnsHtml = '';\n var panelsHtml = '';\n\n typeOrder.forEach(function (t) {\n var items = typeToItems[t] || [];\n var active = (t == firstType) ? ' active' : '';\n var count = items.length;\n\n tabBtnsHtml += '<button class=\"tab-btn' + active + '\" data-tab=\"' + t + '\" onclick=\"switchTab(\\'' + t + '\\')\">'\n + '<i class=\"' + typeIcons[t] + '\"><\/i>'\n + '<span>' + typeLabels[t] + '<\/span>'\n + '<em>' + count + '<\/em>'\n + '<\/button>';\n\n var panel = '<div class=\"panel' + active + '\" id=\"panel-' + t + '\">';\n panel += '<div class=\"grid\">';\n\n var allSel = (currentIsAll && currentAllType == t) ? ' selected' : '';\n panel += '<div class=\"src-card all-card' + allSel + '\"'\n + ' data-type=\"' + t + '\" data-name=\"\" data-all=\"1\"'\n + ' onclick=\"pick(this)\">'\n + '<span class=\"src-name\"><i class=\"fa-solid fa-star card-ico\"><\/i>全部<\/span>'\n + '<\/div>';\n\n items.forEach(function (it) {\n var sel = selectedSet[it.name] ? ' selected' : '';\n var vip = (it.free == 'svip') ? '<span class=\"vip-tag\">VIP<\/span>' : '';\n panel += '<div class=\"src-card' + sel + '\" data-type=\"' + t\n + '\" data-name=\"' + it.name + '\" data-all=\"0\"'\n + ' onclick=\"pick(this)\">'\n + '<span class=\"src-name\">' + it.name + '<\/span>'\n + vip\n + '<\/div>';\n });\n\n panel += '<\/div><\/div>';\n panelsHtml += panel;\n });\n\n var initDisplay = '未选择';\n if (currentIsAll) {\n initDisplay = typeLabels[currentAllType] + ' · 全部';\n } else if (selectedNames.length > 0) {\n initDisplay = selectedNames.join('、');\n }\n var initCount = currentIsAll ? 'ALL' : selectedNames.length;\n\n var boyActive = (currentGender == 'boy') ? ' active' : '';\n var girlActive = (currentGender == 'girl') ? ' active' : '';\n\n var host = getServerHost();\n var html = `<!DOCTYPE html>\n<html lang=\"zh-CN\">\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<link rel=\"stylesheet\" href=\"https:\/\/cdn.bootcdn.net\/ajax\/libs\/font-awesome\/6.5.2\/css\/all.min.css\">\n<style>\n*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}\n\n:root{\n --rose:#e07a8b;\n --rose-dark:#c25a72;\n --rose-soft:#f7a8b8;\n --gold:#c9a55c;\n --gold-dark:#a8802e;\n --ink-1:#4a3a44;\n --ink-2:#6b5765;\n --ink-3:#9b8b98;\n --line:rgba(190,150,170,.2);\n}\n\nhtml,body{height:100%}\n\nbody{\n font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei',sans-serif;\n color:var(--ink-1);\n min-height:100vh;\n padding:18px 14px 32px;\n position:relative;\n overflow-x:hidden;\n background:#fdf6f8;\n}\n\n.bg-layer{\n position:fixed;inset:0;z-index:0;pointer-events:none;\n background:\n radial-gradient(680px 420px at 8% -6%, rgba(255,205,220,.85), transparent 60%),\n radial-gradient(720px 460px at 98% 4%, rgba(255,224,190,.7), transparent 62%),\n radial-gradient(700px 500px at 50% 108%, rgba(214,182,255,.35), transparent 60%),\n linear-gradient(180deg,#fff8fa 0%,#fdf3f6 45%,#f9edf2 100%);\n}\n\n#bubbles{\n position:fixed;inset:0;z-index:1;pointer-events:none;overflow:hidden;\n}\n\n.bubble{\n position:absolute;\n bottom:-120px;\n border-radius:50%;\n background:radial-gradient(circle at 30% 30%, rgba(255,255,255,.95), rgba(255,255,255,.15) 60%, rgba(255,220,230,.35) 100%);\n box-shadow:\n 0 0 12px rgba(255,220,230,.6),\n 0 0 24px rgba(240,180,200,.25),\n inset -3px -5px 8px rgba(240,180,200,.25),\n inset 4px 6px 10px rgba(255,255,255,.9);\n animation:rise linear infinite;\n will-change:transform,opacity;\n}\n\n@keyframes rise{\n 0%{transform:translate3d(0,0,0) scale(.6);opacity:0;}\n 10%{opacity:.9;}\n 35%{transform:translate3d(var(--drift),-38vh,0) scale(1);}\n 65%{transform:translate3d(calc(var(--drift) * -0.7), -68vh, 0) scale(1.05);}\n 90%{opacity:.7;}\n 100%{transform:translate3d(var(--drift),-115vh,0) scale(1.1);opacity:0;}\n}\n\n.wrap{\n position:relative;z-index:2;\n width:100%;max-width:640px;margin:0 auto;\n background:rgba(255,250,252,.72);\n backdrop-filter:blur(28px) saturate(160%);\n -webkit-backdrop-filter:blur(28px) saturate(160%);\n border-radius:28px;\n border:1px solid rgba(255,255,255,.9);\n box-shadow:\n 0 30px 60px -28px rgba(200,120,150,.45),\n 0 12px 30px -18px rgba(180,140,180,.35),\n 0 1px 0 rgba(255,255,255,.9) inset;\n overflow:hidden;\n animation:riseIn .7s cubic-bezier(.22,.61,.36,1);\n}\n@keyframes riseIn{from{opacity:0;transform:translateY(20px) scale(.985)}to{opacity:1;transform:none}}\n\n.head{\n position:relative;\n padding:28px 24px 20px;\n text-align:center;\n background:\n radial-gradient(120% 150% at 50% -30%, rgba(255,214,224,.85), transparent 65%),\n linear-gradient(180deg, rgba(255,255,255,.6), transparent);\n}\n.head::after{\n content:'';\n position:absolute;left:50%;bottom:0;\n width:180px;height:1px;transform:translateX(-50%);\n background:linear-gradient(90deg,transparent,var(--line) 30%,var(--line) 70%,transparent);\n}\n\n.head-orn{\n display:flex;align-items:center;justify-content:center;gap:10px;\n margin-bottom:10px;\n color:var(--rose);\n font-size:11px;\n letter-spacing:6px;\n opacity:.7;\n}\n.head-orn::before,.head-orn::after{\n content:'';\n width:36px;height:1px;\n background:linear-gradient(90deg,transparent,var(--rose-soft));\n}\n.head-orn::after{background:linear-gradient(270deg,transparent,var(--rose-soft))}\n\n.head h1{\n font-size:22px;\n font-weight:700;\n letter-spacing:3px;\n background:linear-gradient(100deg,#c25a72 15%,#e07a8b 45%,#c9a55c 90%);\n -webkit-background-clip:text;\n background-clip:text;\n -webkit-text-fill-color:transparent;\n filter:drop-shadow(0 1px 0 rgba(255,255,255,.9));\n}\n\n.save-tip{\n display:flex;align-items:flex-start;gap:9px;\n margin:14px 4px 0;\n padding:10px 14px;\n border-radius:12px;\n font-size:11.5px;\n line-height:1.55;\n text-align:left;\n color:var(--rose-dark);\n background:linear-gradient(135deg, rgba(255,240,244,.95), rgba(255,222,232,.75));\n border:1px solid rgba(240,180,200,.5);\n box-shadow:0 6px 14px -8px rgba(200,120,150,.5), 0 1px 0 rgba(255,255,255,.9) inset;\n}\n.save-tip i{\n font-size:13px;\n color:var(--rose);\n flex:none;\n margin-top:1px;\n}\n.save-tip span{flex:1}\n\n.gender-wrap{\n margin:16px 22px 0;\n display:flex;gap:8px;\n padding:5px;\n border-radius:16px;\n background:linear-gradient(135deg, rgba(255,255,255,.7), rgba(255,236,240,.45));\n border:1px solid rgba(255,255,255,.9);\n box-shadow:0 6px 18px -14px rgba(200,120,150,.6), 0 1px 0 rgba(255,255,255,.9) inset;\n}\n.gender-btn{\n flex:1;\n display:flex;align-items:center;justify-content:center;gap:7px;\n padding:10px 6px;\n border-radius:12px;\n border:1px solid transparent;\n background:transparent;\n cursor:pointer;\n color:var(--ink-3);\n font-family:inherit;\n font-size:12px;\n font-weight:600;\n letter-spacing:.5px;\n transition:all .28s cubic-bezier(.4,0,.2,1);\n}\n.gender-btn i{font-size:13px;transition:all .28s}\n.gender-btn:hover{color:var(--rose);background:rgba(255,240,244,.65)}\n.gender-btn.active{\n color:#fff;\n background:linear-gradient(145deg,#e07a8b,#c25a72);\n box-shadow:\n 0 10px 22px -10px rgba(194,90,114,.75),\n 0 1px 0 rgba(255,255,255,.4) inset;\n}\n.gender-btn.active i{transform:scale(1.1)}\n.gender-btn.girl.active{\n background:linear-gradient(145deg,#c78fd0,#a26bb0);\n box-shadow:\n 0 10px 22px -10px rgba(162,107,176,.75),\n 0 1px 0 rgba(255,255,255,.4) inset;\n}\n\n.badge-wrap{\n margin:14px 22px 0;\n display:flex;align-items:center;gap:10px;\n padding:11px 15px;\n border-radius:16px;\n background:linear-gradient(135deg, rgba(255,255,255,.75), rgba(255,236,240,.55));\n border:1px solid rgba(255,255,255,.9);\n box-shadow:\n 0 8px 22px -14px rgba(200,120,150,.55),\n 0 1px 0 rgba(255,255,255,.9) inset;\n}\n.badge-dot{\n width:7px;height:7px;flex:none;border-radius:50%;\n background:var(--rose);\n box-shadow:0 0 0 3px rgba(224,122,139,.18), 0 0 10px rgba(224,122,139,.6);\n animation:beat 1.8s ease-in-out infinite;\n}\n@keyframes beat{0%,100%{transform:scale(1);opacity:1}50%{transform:scale(1.3);opacity:.65}}\n\n.badge-lbl{font-size:11px;color:var(--ink-3);letter-spacing:1px;flex:none}\n.badge-val{\n flex:1;text-align:right;\n font-size:12.5px;font-weight:700;\n color:var(--rose-dark);\n white-space:nowrap;overflow:hidden;text-overflow:ellipsis;\n}\n.cnt-pill{\n flex:none;\n font-size:9.5px;font-weight:800;\n padding:2.5px 9px;border-radius:999px;\n color:#fff;\n letter-spacing:.4px;\n background:linear-gradient(135deg,#e07a8b,#c25a72);\n box-shadow:0 3px 10px -4px rgba(194,90,114,.8);\n}\n\n.tabs{\n display:flex;gap:5px;\n margin:12px 22px 0;\n padding:5px;\n border-radius:16px;\n background:linear-gradient(135deg, rgba(255,255,255,.7), rgba(255,236,240,.45));\n border:1px solid rgba(255,255,255,.9);\n box-shadow:0 6px 18px -14px rgba(200,120,150,.6), 0 1px 0 rgba(255,255,255,.9) inset;\n}\n.tab-btn{\n flex:1;\n position:relative;\n display:flex;flex-direction:column;align-items:center;gap:3px;\n padding:9px 4px;\n border-radius:12px;\n border:none;\n background:transparent;\n cursor:pointer;\n color:var(--ink-3);\n font-family:inherit;\n transition:all .28s cubic-bezier(.4,0,.2,1);\n}\n.tab-btn i{font-size:14px;transition:all .28s}\n.tab-btn span{font-size:10.5px;font-weight:600;letter-spacing:.3px}\n.tab-btn em{\n position:absolute;top:3px;right:6px;\n font-style:normal;font-size:9px;font-weight:800;\n padding:1px 5px;border-radius:999px;\n background:rgba(224,122,139,.12);\n color:var(--rose);\n transition:all .25s;\n}\n.tab-btn:hover{color:var(--rose);background:rgba(255,240,244,.65)}\n.tab-btn.active{\n color:#fff;\n background:linear-gradient(145deg,#e07a8b,#c25a72);\n box-shadow:\n 0 10px 22px -10px rgba(194,90,114,.75),\n 0 1px 0 rgba(255,255,255,.4) inset;\n}\n.tab-btn.active i{transform:scale(1.1)}\n.tab-btn.active em{background:rgba(255,255,255,.28);color:#fff}\n\n.panel{display:none;padding:16px 22px 0}\n.panel.active{display:block;animation:softIn .4s cubic-bezier(.22,.61,.36,1)}\n@keyframes softIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}\n\n.grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:9px}\n\n.src-card{\n position:relative;\n padding:15px 8px;\n border-radius:14px;\n background:linear-gradient(160deg, rgba(255,255,255,.85), rgba(255,244,247,.55));\n border:1px solid rgba(255,255,255,.95);\n cursor:pointer;\n text-align:center;\n user-select:none;\n transition:all .25s cubic-bezier(.4,0,.2,1);\n box-shadow:\n 0 6px 14px -12px rgba(180,120,150,.55),\n 0 1px 0 rgba(255,255,255,.9) inset;\n}\n.src-card:hover{\n transform:translateY(-2px);\n background:linear-gradient(160deg,#fff,#fff2f6);\n border-color:rgba(247,168,184,.6);\n box-shadow:\n 0 12px 24px -14px rgba(200,120,150,.55),\n 0 0 0 1px rgba(247,168,184,.25),\n 0 1px 0 rgba(255,255,255,.95) inset;\n}\n.src-card:active{transform:translateY(-1px) scale(.98)}\n\n.src-name{\n position:relative;z-index:1;\n font-size:12.5px;font-weight:600;\n color:var(--ink-2);\n line-height:1.3;\n word-break:break-all;\n transition:color .2s;\n display:inline-flex;align-items:center;gap:4px;\n}\n\n.src-card.selected{\n background:linear-gradient(145deg,#ffe4ea,#f7c8d3);\n border-color:rgba(224,122,139,.9);\n box-shadow:\n 0 14px 26px -14px rgba(194,90,114,.7),\n 0 0 0 1px rgba(224,122,139,.35) inset;\n}\n.src-card.selected .src-name{color:var(--rose-dark);font-weight:700}\n\n.all-card{\n background:linear-gradient(160deg, #fff7ea, #fdeccb);\n border-color:rgba(243,207,139,.65);\n}\n.all-card .card-ico{\n font-size:11px;\n color:var(--gold);\n transition:all .3s;\n}\n.all-card:hover{\n border-color:rgba(201,165,92,.9);\n background:linear-gradient(160deg,#fff9f0,#fbe4b8);\n}\n.all-card.selected{\n background:linear-gradient(145deg,#fde7b8,#f3c97a);\n border-color:rgba(201,165,92,.9);\n box-shadow:\n 0 14px 26px -14px rgba(180,140,70,.7),\n 0 0 0 1px rgba(201,165,92,.35) inset;\n}\n.all-card.selected .src-name{color:#7a5a1c}\n.all-card.selected .card-ico{\n color:#7a5a1c;\n transform:rotate(72deg) scale(1.15);\n}\n\n.vip-tag{\n position:absolute;bottom:5px;right:6px;\n display:inline-flex;align-items:center;justify-content:center;\n font-size:8px;font-weight:800;\n padding:1px 5px;\n border-radius:6px;\n color:#8a6420;\n background:rgba(243,207,139,.55);\n border:1px solid rgba(201,165,92,.55);\n letter-spacing:.3px;\n line-height:1.2;\n}\n\n.foot{padding:20px 22px 24px}\n\n.clear-btn{\n display:flex;align-items:center;justify-content:center;gap:8px;\n width:100%;\n padding:12px;\n border-radius:14px;\n border:1px dashed rgba(224,122,139,.45);\n background:linear-gradient(135deg, rgba(255,255,255,.7), rgba(255,236,240,.4));\n color:var(--rose);\n font-family:inherit;\n font-size:12.5px;\n font-weight:600;\n letter-spacing:1px;\n cursor:pointer;\n transition:all .28s;\n box-shadow:0 1px 0 rgba(255,255,255,.9) inset;\n}\n.clear-btn i{font-size:12px;transition:transform .4s}\n.clear-btn:hover{\n border-color:rgba(224,122,139,.9);\n background:linear-gradient(135deg,#fff,#ffeef2);\n color:var(--rose-dark);\n}\n.clear-btn:hover i{transform:rotate(-180deg)}\n.clear-btn:active{transform:scale(.985)}\n\n@media (max-width:400px){\n .grid{grid-template-columns:repeat(3,minmax(0,1fr));gap:7px}\n .src-card{padding:13px 5px;border-radius:12px}\n .src-name{font-size:11px}\n .head h1{font-size:20px}\n .tab-btn span{font-size:9.5px}\n .tab-btn i{font-size:13px}\n .gender-wrap{margin:14px 16px 0}\n .badge-wrap{margin:12px 16px 0}\n .tabs{margin:10px 16px 0}\n .panel{padding:14px 16px 0}\n .foot{padding:18px 16px 22px}\n}\n<\/style>\n<\/head>\n<body>\n\n<div class=\"bg-layer\"><\/div>\n<div id=\"bubbles\"><\/div>\n\n<div class=\"wrap\">\n\n <div class=\"head\">\n <div class=\"head-orn\">SOURCE<\/div>\n <h1>源站选择<\/h1>\n <div class=\"save-tip\">\n <i class=\"fa-solid fa-circle-info\"><\/i>\n <span>可跨分类多选;选「全部」即该分类全部源站,与具体源站互斥。多选时发现页仅展示第一个源站。选好后点右上角 ✓ 保存。<\/span>\n <\/div>\n <\/div>\n\n <div class=\"gender-wrap\">\n <button class=\"gender-btn boy` + boyActive + `\" data-gender=\"boy\" onclick=\"pickGender('boy')\">\n <i class=\"fa-solid fa-mars\"><\/i>男频\n <\/button>\n <button class=\"gender-btn girl` + girlActive + `\" data-gender=\"girl\" onclick=\"pickGender('girl')\">\n <i class=\"fa-solid fa-venus\"><\/i>女频\n <\/button>\n <\/div>\n\n <div class=\"badge-wrap\">\n <span class=\"badge-dot\"><\/span>\n <span class=\"badge-lbl\">已选<\/span>\n <span id=\"current-value\" class=\"badge-val\">` + initDisplay + `<\/span>\n <span id=\"current-count\" class=\"cnt-pill\">` + initCount + `<\/span>\n <\/div>\n\n <div class=\"tabs\">` + tabBtnsHtml + `<\/div>\n\n <div>` + panelsHtml + `<\/div>\n\n <div class=\"foot\">\n <button class=\"clear-btn\" onclick=\"clearAll()\">\n <i class=\"fa-regular fa-trash-can\"><\/i>\n 清空所有选择\n <\/button>\n <\/div>\n\n<\/div>\n\n<div id=\"result-output\" style=\"display:none;\">` + (currentIsAll ? ('__ALL__' + currentAllType) : selectedNames.join(',')) + `<\/div>\n<div id=\"gender-output\" style=\"display:none;\">` + currentGender + `<\/div>\n\n<script>\nvar TYPE_LABEL = { novel:'小说', audio:'听书', comic:'漫画', video:'视频' };\n\nfunction switchTab(t){\n document.querySelectorAll('.tab-btn').forEach(function(b){\n b.classList.toggle('active', b.getAttribute('data-tab') == t);\n });\n document.querySelectorAll('.panel').forEach(function(p){\n p.classList.toggle('active', p.id == 'panel-' + t);\n });\n}\n\nfunction pickGender(g){\n document.querySelectorAll('.gender-btn').forEach(function(b){\n b.classList.toggle('active', b.getAttribute('data-gender') == g);\n });\n document.getElementById('gender-output').textContent = g;\n}\n\nfunction pick(el){\n var isAll = el.getAttribute('data-all') == '1';\n if (isAll) {\n if (el.classList.contains('selected')) {\n el.classList.remove('selected');\n } else {\n document.querySelectorAll('.src-card').forEach(function(c){ c.classList.remove('selected'); });\n el.classList.add('selected');\n }\n } else {\n document.querySelectorAll('.all-card').forEach(function(c){ c.classList.remove('selected'); });\n el.classList.toggle('selected');\n }\n updateOutput();\n}\n\nfunction updateOutput(){\n var allSel = document.querySelector('.all-card.selected');\n var valEl = document.getElementById('current-value');\n var cntEl = document.getElementById('current-count');\n\n if (allSel) {\n var t = allSel.getAttribute('data-type');\n document.getElementById('result-output').textContent = '__ALL__' + t;\n valEl.textContent = TYPE_LABEL[t] + ' · 全部';\n cntEl.textContent = 'ALL';\n return;\n }\n var names = [];\n document.querySelectorAll('.src-card.selected').forEach(function(c){\n var n = c.getAttribute('data-name');\n if (n) names.push(n);\n });\n document.getElementById('result-output').textContent = names.join(',');\n valEl.textContent = names.length ? names.join('、') : '未选择';\n cntEl.textContent = names.length;\n}\n\nfunction clearAll(){\n document.querySelectorAll('.src-card').forEach(function(c){ c.classList.remove('selected'); });\n updateOutput();\n}\n\n(function spawnBubbles(){\n var box = document.getElementById('bubbles');\n if (!box) return;\n var count = 18;\n for (var i = 0; i < count; i++) {\n var b = document.createElement('span');\n b.className = 'bubble';\n var size = 10 + Math.random() * 26;\n b.style.width = size + 'px';\n b.style.height = size + 'px';\n b.style.left = (Math.random() * 100) + '%';\n\n var drift = (Math.random() * 80 - 40).toFixed(1) + 'px';\n b.style.setProperty('--drift', drift);\n\n var dur = (10 + Math.random() * 12).toFixed(2) + 's';\n b.style.animationDuration = dur;\n b.style.animationDelay = (-Math.random() * dur.replace('s','')) + 's';\n\n var o = (0.35 + Math.random() * 0.4).toFixed(2);\n b.style.setProperty('opacity', o);\n\n box.appendChild(b);\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已取消源站设置');\n return;\n }\n\n var body = result.body();\n var match = body.match(\/<div id=\"result-output\"[^>]*>([^<]*)<\\\/div>\/);\n var raw = (match && match[1]) ? match[1].trim() : '';\n\n var genderMatch = body.match(\/<div id=\"gender-output\"[^>]*>([^<]*)<\\\/div>\/);\n var newGender = (genderMatch && genderMatch[1]) ? genderMatch[1].trim() : currentGender;\n\n var typeKeywordMap = { novel: '小说', audio: '听书', comic: '漫画', video: '视频' };\n var newSource = '';\n var toastMsg = '';\n\n if (raw.indexOf('__ALL__') === 0) {\n var t = raw.substring(7);\n newSource = typeKeywordMap[t] || '小说';\n toastMsg = '\\n已设置为「' + (typeLabels[t] || '') + ' · 全部」';\n } else if (!raw) {\n newSource = '';\n toastMsg = '\\n已清空源站选择(聚合搜索)';\n } else {\n newSource = raw;\n toastMsg = '\\n已保存 ' + raw.split(',').length + ' 个源站';\n }\n\n setConfigs.call(this, { source: newSource, gender: newGender });\n\n var genderText = newGender == 'girl' ? '女频' : '男频';\n java.toast(toastMsg + ' · ' + genderText);\n\n } catch (e) {\n java.log(e);\n java.toast('\\n源站设置失败');\n }\n}\n\nfunction vip() { java.startBrowserAwait(getServerHost() + '\/coffee', \"喝咖啡\"); }\nfunction fb() { java.startBrowserAwait('https:\/\/fb.shushan.vip', \"发布页\"); }\nfunction km() { java.startBrowserAwait('https:\/\/www.kufaka.com\/item\/9mbtvr', \"卡密\"); }\nfunction key() { java.startBrowserAwait(getServerHost() + '\/user_login', \"登陆注册\"); }\nfunction user_center() { java.startBrowserAwait(getServerHost() + '\/user_center', \"用户中心\"); }\nfunction version() { java.startBrowserAwait(getServerHost() + '\/version?id=' + localVersion, \"版本检查\"); }\nfunction help() { java.startBrowserAwait(getServerHost() + '\/help', \"使用帮助\"); }\nfunction password() { java.startBrowserAwait(getServerHost() + '\/forgot_password', \"修改密码\"); }\nfunction boy() { updateConfig(\"gender\", \"boy\", \"\\n已设置为\\n🧸男频🧸\\n刷新发现页生效\"); }\nfunction girl() { updateConfig(\"gender\", \"girl\", \"\\n已设置为\\n🎀女频🎀\\n刷新发现页生效\"); }",
"respondTime": 180000,
"ruleBookInfo": {
"author": "$.author",
"canReName": "1",
"coverUrl": "$.cover",
"downloadUrls": "{{$.book_url##[|]}}\n@js:\nlet url = java.base64Decode(result);\nlet source = \"{{$.source}}\".toLowerCase();\n\nif (source == \"番茄小说\") {\n let book_id = \"{{$.bookid}}\";\n java.toast('\\n❗请等待正在下载变成下载完成后在导入');\n \n if (book_id) {\n getServerHost() + `\/down?key={{getSecretKey()}}&book_id=${book_id}`;\n } else {\n url;\n }\n} else if (source == \"爱去\") {\n let bookIdMatch = url.match(\/txt-(\\d+)\\.htm\/);\n if (bookIdMatch && bookIdMatch[1]) {\n let bookId = bookIdMatch[1];\n let requestUrl = 'https:\/\/www.aiqu666.com\/txt-xx\/softdownfree.asp?softid=' + bookId + '&ckm=mianfei';\n let html = java.ajax(requestUrl);\n let downloadSection = html.match(\/<img[^>]*icon_downloadserver[^>]*>([\\s\\S]*?)<span style=\"FONT-SIZE: 18px; COLOR: #0000ff\">下载帮助:\/);\n \n if (downloadSection) {\n let firstMatch = downloadSection[1].match(\/第一下载地址[^<]*<a[^>]*href=\"([^\"]+\\.(txt|zip|rar|epub))\"\/);\n if (firstMatch && firstMatch[1]) {\n firstMatch[1];\n } else {\n let allLinks = downloadSection[1].match(\/href=\"([^\"]+\\.(txt|zip|rar|epub))\"\/g);\n allLinks && allLinks.length > 1 ? allLinks[1].replace('href=\"', '').replace('\"', '') : '';\n }\n } else {\n '';\n }\n } else {\n url;\n }\n} else {\n url;\n}",
"init": "<js>\nif (String(baseUrl).startsWith(\"data:\")) {\n let res = JSON.parse(java.hexDecodeToString(result));\n \n if (res.source == \"番茄小说\" && (getConfig.call(this).fqdown == \"on\")) {\n book.type = 128;\n java.toast('\\n正在准备TXT,请等待后端缓存完毕后点击阅读导入…');\n }\n\n let request = getServerHost() + `\/details,${JSON.stringify({\n method: \"POST\",\n body: res\n })}`;\n \n result = java.ajax(request);\n let response = JSON.parse(result);\n if (response.data.tab == \"download\") {\n book.type = 128;\n }\n} \n\/\/java.log(result);\nresult;\n<\/js>\n$.data",
"intro": " \n✨ 源站:{{$.source}}{{\"\\n\"+\"\"}}\n{{$.desc}}\n<js>\nconst tomatoSources = [\"番茄小说\", \"番茄听书\", \"番茄畅听\"];\nconst jjwxcSources = [\"晋江\"];\n\nconst sourceName = \"{{$.source}}\";\nconst bookIdFromBackend = \"{{$.bookid}}\";\n\nlet finalResult = result;\n\nif (tomatoSources.includes(sourceName)) {\n try {\n let syncStatus = getConfig.call(this).book_sync || \"off\";\n let fqdownStatus = getConfig.call(this).fqdown || \"off\";\n let simpleIntro = getConfig.call(this).simple_intro || \"off\";\n let bookId = bookIdFromBackend;\n\n if (simpleIntro == \"on\" && bookId) {\n let bookData = getBookData(bookId);\n let abstract = bookData ? (bookData.book_abstract_v2 || bookData.abstract || \"\") : \"\";\n\n let output = ` \n✨ 源站:${sourceName}{{\"\\n\"+\"\"}}\n📜 简介:${abstract}{{\"\\n\"+\"\"}}`;\n\n finalResult = output;\n } else if (bookId) {\n let bookData = getBookData(bookId);\n\n if (bookData) {\n if (syncStatus == \"on\") {\n syncReadingHistory(bookId);\n }\n\n let bookName = bookData.original_book_name || bookData.book_name || \"\";\n let aliasName = bookData.book_flight_alias_name || bookData.sub_title || \"\";\n let createTime = bookData.create_time ? bookData.create_time.split('T')[0] : (bookData.first_online_time ? java.timeFormat(parseInt(Object.values(JSON.parse(bookData.first_online_time))[0])*1000).split(' ')[0] : \"\");\n let updateTime = bookData.last_chapter_update_time ? java.timeFormat(parseInt(bookData.last_chapter_update_time)*1000) : \"\";\n\n let authorName = \"\";\n if (bookData.original_authors) {\n try {\n let authorsParsed = JSON.parse(bookData.original_authors);\n authorName = Array.isArray(authorsParsed) ? authorsParsed.map(a => a.AuthorName || a.author_name || a).join(', ') : bookData.original_authors;\n } catch(e) {\n authorName = bookData.original_authors;\n }\n } else {\n authorName = bookData.author || \"\";\n }\n\n let roleName = \"\";\n if (bookData.roles) {\n try {\n let rolesParsed = JSON.parse(bookData.roles);\n roleName = Array.isArray(rolesParsed) ? rolesParsed.join(', ') : bookData.roles;\n } catch(e) {\n roleName = bookData.roles;\n }\n } else if (bookData.role) {\n roleName = bookData.role;\n }\n\n let bookTagsData = bookData.pure_category_tags || bookData.tags || \"\";\n let readCount = bookData.read_count || \"0\";\n let bookStatus = bookData.book_search_visible == 'false' ? \"下架\" : \"正常\";\n let abstract = bookData.book_abstract_v2 || bookData.abstract || \"\";\n let copyrightInfo = bookData.copyright_info || \"\";\n\n let output = ` \n✨ 源站:${sourceName}{{\"\\n\"+\"\"}}\n📕 源名:${bookName}`;\n\n if (aliasName) output += `\\n📖 别名:${aliasName}`;\n\n if (sourceName == \"番茄小说\" && fqdownStatus == \"on\") {\n output += `\\n⚠️ 当前为TXT下载模式,在线阅读或换源请在登陆关闭后刷新详情页!`;\n }\n\n output += `\\n✏️ 开坑:${createTime} \n🧭 更新:${updateTime}‎\n👤 作者:${authorName}`;\n\n if (roleName) output += `\\n🎭 主角:${roleName}`;\n\n output += `\n🏷️ 标签:${bookTagsData}\n👁️ 在线:${readCount}人在读\n🔗 书籍状态:${bookStatus} `;\n\n output += `\\n🔄 阅读记录:${syncStatus == \"on\" ? \"✅已开启同步\" : \"❌已关闭同步\"}`;\n\n if (sourceName == \"番茄听书\") {\n try {\n let ttsUrl = `https:\/\/reading.snssdk.com\/reading\/bookapi\/audio\/toneinfo\/?book_id=${bookId}&aid=1967`;\n let response = java.ajax(ttsUrl);\n let data = JSON.parse(response);\n let ttsData = data.data;\n\n if (ttsData) {\n let ttsTones = ttsData.tts_tones || [];\n let recommendTone = ttsData.recommend_tone;\n\n if (recommendTone) {\n for (var i = 0; i < ttsTones.length; i++) {\n if (ttsTones[i].id == recommendTone) {\n output += '\\n🎤 推荐音色:' + ttsTones[i].title + ' (音色ID: ' + ttsTones[i].id + ')';\n break;\n }\n }\n }\n\n if (ttsTones.length > 0) {\n var availableTones = '';\n for (var j = 0; j < ttsTones.length; j++) {\n if (j > 0) availableTones += '、';\n availableTones += ttsTones[j].title + '(音色ID: ' + ttsTones[j].id + ')';\n }\n output += '\\n🎧 可用音色:' + availableTones;\n }\n }\n } catch (e) {}\n }\n\n if (sourceName == \"番茄听书\" || sourceName == \"番茄小说\") {\n try {\n let ttsUrl = `https:\/\/reading.snssdk.com\/reading\/bookapi\/audio\/toneinfo\/?book_id=${bookId}&aid=1967`;\n let response = java.ajax(ttsUrl);\n let data = JSON.parse(response);\n let ttsData = data.data;\n\n if (ttsData) {\n let audioTones = ttsData.audio_tones || [];\n\n let rawAbookIds = [];\n try {\n let matches = response.match(\/\"abook_id\"\\s*:\\s*(\\d+)\/g) || [];\n for (var r = 0; r < matches.length; r++) {\n let idMatch = matches[r].match(\/(\\d+)\/);\n if (idMatch) rawAbookIds.push(idMatch[1]);\n }\n } catch (e) {}\n\n if (audioTones.length > 0) {\n output += '\\n🎙️ 真人主播(复制 ID@番茄畅听 搜索):';\n for (var m = 0; m < audioTones.length; m++) {\n var toneTitle = audioTones[m].title || '';\n var abookId = rawAbookIds[m] || String(audioTones[m].abook_id || '');\n output += '\\n ' + toneTitle + '(书籍ID: ' + abookId + '@番茄畅听)';\n }\n } else {\n output += '\\n🎙️ 真人主播:本书不支持真人主播';\n }\n }\n } catch (e) {}\n }\n\n output += `{{\"\\n\"+\"\"}}\n📜 简介:${abstract}{{\"\\n\"+\"\"}}`;\n\n if (copyrightInfo) {\n output += `\\n📍 ${copyrightInfo.split(',')[0]}。`;\n }\n\n finalResult = output;\n }\n }\n } catch (e) {\n }\n} else if (jjwxcSources.some(source => sourceName.includes(source))) {\n try {\n let novelId = bookIdFromBackend;\n\n if (novelId) {\n let currentVariable = book.getVariable(\"custom\") || \"\";\n\n if (!currentVariable.includes(\"开启段评\") && !currentVariable.includes(\"关闭段评\")) {\n try {\n let checkUrl = `https:\/\/android.jjwxc.com\/app.jjwxc\/android\/reading\/authorNovelSetting\/getSetting?versionCode=381&novelid=${novelId}&setting_type=author_paragraph_comment_switch`;\n\n let request = `${checkUrl},${JSON.stringify({\n headers: {\n \"versiontype\": \"reading\",\n \"versionCode\": \"381\"\n }\n })}`;\n\n let ajaxResult = java.ajax(request);\n let checkResponse = JSON.parse(ajaxResult);\n\n let paraEnabled = checkResponse.data?.paragraph_switch == \"1\";\n\n let newVariable = currentVariable + (paraEnabled ? \"开启段评\" : \"关闭段评\");\n book.putVariable(\"custom\", newVariable);\n\n } catch(e) {\n let newVariable = currentVariable + \"关闭段评\";\n book.putVariable(\"custom\", newVariable);\n }\n }\n }\n } catch (e) {\n }\n}\nfinalResult;\n<\/js>",
"kind": "$.tags",
"name": "$.title",
"tocUrl": "<js>\nlet catalog = {\n source: result.source,\n url: result.book_url,\n name: result.title,\n tab: result.tab || \"novel\",\n bookid: result.bookid || \"\"\n};\n\nvar tabTips = {audio: '听书', video: '视频', comic: '漫画'}; \nif (tabTips[catalog.tab]) {\n java.toast('当前为' + tabTips[catalog.tab] + '模式');\n}\n\nyunurl = java.base64Encode(JSON.stringify(catalog));\nresult = `data:catalogUrl;base64,${yunurl},{\"type\":\"susan\"}`;\n<\/js>",
"wordCount": "$.wordCount"
},
"ruleContent": {
"callBackJs": "if (event == \"addBookShelf\") {\n if (isBookSyncEnabled.call(this)) {\n syncFanqieShelf(true);\n syncJjShelf(true);\n }\n}\n\nif (event == \"delBookShelf\") {\n if (isBookSyncEnabled.call(this)) {\n syncFanqieShelf(false);\n syncJjShelf(false);\n }\n}\n\nif (event == \"clickCustomButton\") {\n let targetUrl = \"\";\n\n if (chapter && chapter.url) {\n try {\n let chUrl = String(chapter.url);\n let base64Str = chUrl.replace(\/^data:chapterUrl;base64,\/, '');\n let idx = base64Str.indexOf(',');\n if (idx >= 0) base64Str = base64Str.substring(0, idx);\n let params = JSON.parse(java.base64Decode(base64Str));\n if (params.commentUrl) {\n targetUrl = params.commentUrl;\n }\n } catch (e) {}\n } else if (book && book.tocUrl) {\n try {\n let tocUrl = String(book.tocUrl);\n let base64Str = tocUrl.replace(\/^data:catalogUrl;base64,\/, '');\n let idx = base64Str.indexOf(',');\n if (idx >= 0) base64Str = base64Str.substring(0, idx);\n let catalog = JSON.parse(java.base64Decode(base64Str));\n let source = catalog.source || \"\";\n let bookid = catalog.bookid || \"\";\n\n if (bookid) {\n let urlMap = {\n \"起点\": \"\/qd_role?bookid=\" + bookid,\n \"七猫小说\": \"\/qm_comment?type=book&book_id=\" + bookid,\n \"番茄小说\": \"\/fq_comment?book_id=\" + bookid,\n \"番茄听书\": \"\/fq_comment?book_id=\" + bookid,\n \"番茄畅听\": \"\/fq_comment?book_id=\" + bookid,\n \"番茄漫画\": \"\/fq_comment?book_id=\" + bookid\n };\n\n if (source == \"晋江\") {\n let authorId = \"\";\n try {\n let jjUa = java.getWebViewUA() + '\/JINJIANG-Android\/513';\n let resp = java.ajax('https:\/\/app-cdn.jjwxc.net\/androidapi\/novelbasicinfo?novelId=' + bookid + ',' + JSON.stringify({\n headers: { 'User-Agent': jjUa }\n }));\n let info = JSON.parse(resp);\n authorId = info.authorId || '';\n } catch (e) {\n authorId = '';\n }\n targetUrl = getServerHost() + \"\/jj_author?novelid=\" + bookid + \"&authorid=\" + authorId;\n } else if (urlMap[source]) {\n targetUrl = getServerHost() + urlMap[source];\n }\n }\n } catch (e) {}\n }\n\n if (targetUrl) {\n targetUrl += \"&key=\" + getSecretKey();\n java.showBrowser(\n targetUrl,\n null,\n \"window.java=java;\",\n JSON.stringify({\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.8,\n \"isHideable\": true\n })\n );\n } else {\n java.toast(\"可能是不支持的来源,请等待目录加载完毕\");\n }\n true;\n}",
"content": "<js>eval(function(p,a,c,k,e,r){e=function(c){return(c<62?'':e(parseInt(c\/62)))+((c=c%62)>35?String.fromCharCode(c+29):c.toString(36))};if('0'.replace(0,e)==0){while(c--)r[e(c)]=k[c];k=[function(e){return r[e]||e}];e=function(){return'([3-8bcf-mo-rt-zB-UWYZ]|1\\\\w)'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\\\b'+e(c)+'\\\\b','g'),k[c]);return p}('5 3=o.z(7.hexDecodeToString(h));5 Z=1g()?\\'ios\\':\\'1h\\';5 p=\\'\\';4(Z==\\'1h\\'){q{p=7.1i()}r(e){q{p=7.1j()}r(e2){p=\\'\\'}}}8{q{p=7.1j()}r(e){q{p=7.1i()}r(e2){p=\\'\\'}}}4(!1g()){q{eval(String(f.loginUrl));5 g=1l.H(I);5 J=Date.J();5 1m=2*60*60*1000;4(!g.10||(J-g.10)>1m){setConfig.H(I,\\'10\\',J);5 c=o.z(7.K(y()+\"\/k?vid=sy\"));4(c.k){4(c.1o&&c.1o.toString().toLowerCase()==\"L\"&&c.k>B){7.11(k());7.l(\"请更新书源!下载页面已打开。\")}5 v=c.k>B?\"\\\\n✨发现新版本V\"+c.k+\" (当前V\"+B+\")\"+(c.v?\"\\\\n\"+c.v:\\'\\'):c.k<B?\"\\\\n⚠️版本异常\\\\n本地V\"+B+\" > 云端V\"+c.k+\"\\\\n请检查书源配置\":\\'\\';4(v)7.l(v)}8 4(c.v){7.l(c.v)}}}r(e){}}5 g=1l.H(I);5 C=g.C||\\'\\';5 D=g.D||\\'\\';5 E=g.E||\\'霞鹜文楷\\';5 M=g.M||\\'\\';5 N={\"Content-1p\":\"application\/json\",\"X-Api-Key\":13(),\"X-1q-1p\":Z,\"X-1q-Id\":p};3.k=\"12\";4(g.chapter_refresh_force==\"on\"){3.refresh=\\'L\\'}4(3.f==\\'晋江\\'){3.token=JJtoken()||\"\"}4(3.f==\\'七猫小说\\'&&3.O){5 14=3.O.15(\/m=(\\\\d+)\/);5 16=3.O.15(\/w=(\\\\d+)\/);5 17=3.O.15(\/P=([a-f0-9]+)\/);4(14)3.m=14[1];4(16)3.w=16[1];4(17)3.P=17[1]}5 F=g.F||\\'default\\';4(M==\\'L\\'){3.M=\\'L\\';4(C)3.C=C;4(D)3.D=D;4(E)3.E=E;4(F)3.F=F}syncReadingProgress.H(I,3);4(3.f==\\'番茄听书\\'||3.f==\\'番茄畅听\\'){q{5 Q=1s(\\'fanqie\\');5 R=y()+\\'\/1t?1u=\\'+Q+\\'&18=\\'+3.w;5 S=7.K(R+\",\"+o.19({1a:\"1v\",1b:N}));5 i=o.z(S);4(i.b&&i.b[0]&&i.b[0].T){h=i.b[0].T}8{7.l(\"当前音色不支持该章节,请切换其他音色尝试\");h=\"\"}}r(e){7.l(\"获取音频失败,请切换音色或重试\");h=\"\"}}8 4(3.f==\\'书旗听书\\'){q{5 1c=3.j||\\'\\';5 1d=3.18||\\'\\';5 Q=1s(\\'shuqi\\');4(1c&&1d){5 R=y()+\\'\/1t?j=\\'+1c+\\'&18=\\'+1d+\\'&1u=\\'+Q;5 S=7.K(R+\",\"+o.19({1a:\"1v\",1b:N}));5 i=o.z(S);4(i.b&&i.b[0]&&i.b[0].T){h=i.b[0].T}8{7.l(\"当前音色不支持该章节,请切换其他音色尝试\");h=\"\"}}}r(e){7.l(\"获取音频失败,请切换音色或重试\");h=\"\"}}8 4(3.f==\\'番茄短剧\\'||3.f==\\'七猫短剧\\'){5 G=\\'\\';4(3.f==\\'番茄短剧\\'&&3.m){G=y()+\\'\/1w?m=\\'+3.m+\\'&1x=\\'+13()}8 4(3.f==\\'七猫短剧\\'&&3.1e){G=y()+\\'\/1w?1e=\\'+3.1e+\\'&1x=\\'+13()}4(G){4(1f.1y==U.1z){7.11(G,U.1A||\\'视频播放\\');7.l(\\'视频加载中...\\');h=\"▶️刷新正文进入播放界面\"}8{h=\"▶️点击章节进入播放界面\"}}}8{5 1B=y()+`\/1C,${o.19({1a:\"POST\",1b:N,body:3})}`;5 t=o.z(7.K(1B));5 6=t.b.1C;5 1D=\/^[A-Za-z0-9+\/]*={0,2}$\/.test(6);4(1D){q{6=decode(6)}r(e){6=7.base64Decode(6)}}4(t.b&&t.b.tab==\"video\"){4(1f.1y==U.1z){7.11(6,U.1A||\\'视频播放\\');7.l(\\'视频加载中...\\');6=\"▶️刷新正文进入播放界面\"}8{6=\"▶️点击章节进入播放界面\"}}4((g.yunpara||\"off\")==\"on\"){5 s=3.f;4(s==\\'番茄小说\\'&&3.m&&3.w){6=x(6,3.m,3.w,\\'fq\\')}8 4(s==\\'晋江\\'&&3.W&&3.Y){5 1E=1f.getVariable(\"custom\")||\"\";4(!1E.includes(\"关闭段评\")){6=x(6,3.W,3.Y,\\'jj\\',1G,{})}}8 4(s==\\'半夏\\'&&3.W&&3.Y){6=x(6,3.W,3.Y,\\'jj\\',1G,{j:3.j,u:3.u})}8 4(s==\\'七猫小说\\'&&3.m&&3.w&&3.P){6=x(6,3.m,3.w,\\'qm\\',3.P)}8 4((s==\\'企鹅看书\\'||s==\\'QQ阅读\\')&&3.j&&3.u){6=x(6,3.j,3.u,\\'qq\\')}8 4(s==\\'起点\\'&&3.j&&3.u){6=x(6,3.j,3.u,\\'qd\\')}8 4(s==\\'少年梦\\'&&3.j&&3.u){6=x(6,3.j,3.u,\\'snm\\')}}4(t.b.1H){6+=\"\\\\n\"+t.b.1H}4(t.b.1I){6+=\"\\\\n【📢作者有话说】\\\\n\"+t.b.1I}h=6}',[],107,'|||chapterParams|if|let|finalResult|java|else|||data|versionData|||source|config|result|apiData|bookid|version|toast|book_id||JSON|deviceId|try|catch||response|chapterid|msg|item_id|getComments|getServerHost|parse||localVersion|tx1|tx2|ttf|bubble|videoUrl|call|this|now|ajax|true|dialogue|commonHeaders|qm_url|content_md5|toneId|apiUrl|audioResponse|main_url|chapter||novelId||chapterId|deviceTypeStr|lastUpdateCheck|startBrowser||getSecretKey|bookIdMatch|match|itemIdMatch|contentMd5Match|item_ids|stringify|method|headers|bookId|itemId|qm_id|book|deviceType|android|androidId|deviceID||getConfig|fourHours||force|Type|Device||getTtsTone|audio|tone_id|GET|player|key|durChapterIndex|index|title|chapterUrl|content|isBase64|customVar||null|notice|sayBody'.split('|'),0,{}))<\/js>",
"payAction": "let b64 = String(baseUrl);\nlet prefix = 'data:chapterUrl;base64,';\nif (b64.indexOf(prefix) === 0) {\n b64 = b64.substring(prefix.length);\n}\nlet commaIdx = b64.indexOf(',');\nif (commaIdx >= 0) {\n b64 = b64.substring(0, commaIdx);\n}\n\nlet chapterParams = JSON.parse(java.base64Decode(b64));\n\nnid = String(chapterParams.novelId || '');\ncid = String(chapterParams.chapterId || '');\n\ntoken = java.base64Decode(JJtoken());\nenbody = String(Date.now())+\":\"+token+\":\"+nid;\nsign = encode(enbody);\nbody = \"versionCode=444&novelId=\"+nid+\"&chapters=\"+cid+\"&orderType=15&isautoBuy=0&autosubscribeStatus=0¬UseBackpoint=&sign=\"+sign;\nurl = \"https:\/\/android.jjwxc.net\/androidapi\/buy,\";\npost = {\n \"method\": \"POST\",\n \"body\": String(body)\n};\na = java.ajax(url + JSON.stringify(post));\nif (\/购买成功\/.test(JSON.parse(a).message)) {\n java.longToast(JSON.parse(a).message);\n result = true;\n} else {\n mm = JSON.parse(a).message == \"\" \n ? JSON.parse(a).popupdata.popupmessage + \"\\n❗️进晋江app进行一次验证后❗️才能在阅读app使用购买功能\" \n : JSON.parse(a).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));\nlet source = chapterParams.source;\nlet bookId = chapterParams.book_id || '';\nlet itemId = chapterParams.item_id || '';\n\nconst sourceConfig = {\n '番茄听书': { source: null, params: { item_id: itemId, tone_id: getTtsTone('fanqie'), book_id: bookId } },\n '番茄畅听': { source: null, params: { item_id: itemId, tone_id: getTtsTone('fanqie'), book_id: bookId } },\n '七猫听书': { source: 'qm', params: { book_id: bookId, item_id: itemId }, require: ['bookId', 'itemId'] },\n '酷我听书': { source: 'kw', params: { item_id: itemId }, require: ['itemId'] },\n};\n\nlet cfg = sourceConfig[source];\nlet apiUrl = '';\n\nif (cfg) {\n let ok = !cfg.require || cfg.require.every(k => ({ bookId, itemId })[k]);\n if (ok) {\n let query = Object.entries(cfg.params)\n .filter(([, v]) => v !== '' && v != null)\n .map(([k, v]) => `${k}=${v}`)\n .join('&');\n if (cfg.source) query = `source=${cfg.source}&` + query;\n apiUrl = getServerHost() + '\/audio_lrc?' + query;\n }\n}\n\nif (apiUrl) {\n try {\n result = decode(java.ajax(apiUrl));\n } catch (e) {\n result = '';\n }\n} else {\n result = '';\n}"
},
"ruleExplore": {
"author": "$.author",
"bookList": "<js>\nfunction getBookIdFull(url) {\n const {java} = this;\n let $ = JSON.parse(url).data;\n let arr = [];\n if ($.book_shelf_info && $.book_shelf_info.length > 0) {\n arr = $.book_shelf_info.map(item => item.book_id);\n } else if ($.data_list && $.data_list.length > 0) {\n arr = $.data_list.map(item => item.book_id_str);\n } else {\n java.toast(\"获取 book_id 失败,你可能需要登录!\");\n }\n return arr;\n}\n\nlet session = getSessionId()\n\ngetShelf = () => {\n let book_shelf_info = java.ajax(getServerHost() + \"\/book_shelf?session=\" + session)\n \n bid = getBookIdFull(book_shelf_info)\n \n let id_list = splitArray(bid, 200)\n let urls = []\n id_list.forEach(i => {\n urls.push(getServerHost() +\"\/detail?book_id=\" + i.join(\",\"))\n })\n res = java.ajaxAll(urls)\n\n let resp = {book_info: []}\n res.forEach(r => {\n resp.book_info = resp.book_info.concat(JSON.parse(r.body()).data)\n })\n\n return resp\n}\n\nfunction getByGroupName(name) {\n let book_shelf_info = JSON.parse(java.ajax(getServerHost() +\"\/book_shelf?session=\" + session))\n \n let group_bookids = {\n \"未分组\": []\n }\n book_shelf_info.data.book_shelf_info.forEach(i => {\n if (!group_bookids[i.group_name ? i.group_name : \"未分组\"]) group_bookids[i.group_name] = []\n group_bookids[i.group_name ? i.group_name : \"未分组\"].push(i.book_id)\n })\n \n if (!group_bookids[decodeURIComponent(name)]) return {data: []}\n \n let book_ids = splitArray(group_bookids[decodeURIComponent(name)], 200)\n let urls = []\n\n book_ids.forEach(i => { \turls.push(getServerHost() +\"\/detail?book_id=\" + i.join(\",\"))\n })\n \n res = java.ajaxAll(urls)\n\n let resp = {book_info: []}\n res.forEach(r => {\n resp.book_info = resp.book_info.concat(JSON.parse(r.body()).data)\n })\n\n return resp\n}\n\nfunction getByTabIndex(index) {\n let url = _mlsec.requestHeader(\n \"bookmall\/tab\",\n \"version_name=5.8.9.32\",\n null,\n \"sessionid=\" + session\n )\n let all = JSON.parse(java.ajax(url))\n let tab = all.data.tab_item[0].cell_data[index].cell_data\n if (!tab) tab = []\n let bookList = []\n for (let i of tab) {\n bookList = bookList.concat(i.book_data)\n }\n return { book_info: bookList }\n}\n\nfunction getGuessYouLike() {\n let apiUrl = 'https:\/\/reading.snssdk.com\/reading\/bookapi\/bookmall\/tab\/v\/?version_name=5.8.9.32&device_id=1024&device_type=114514&iid=2048&aid=1967&channel=0&os_version=0&app_name=novelapp&version_code=58932&device_platform=android'\n let response = java.ajax(apiUrl + ', ' + JSON.stringify({\n headers: {\n 'Cookie': \"sessionid=\" + session\n }\n }))\n let responseData = JSON.parse(response)\n let finalResult = {\n code: 0,\n message: 'SUCCESS',\n data: {\n cell_view: {\n book_data: []\n }\n }\n }\n if (responseData.data && responseData.data.tab_item && responseData.data.tab_item.length > 0 && responseData.data.tab_item[0].cell_data && responseData.data.tab_item[0].cell_data.length > 0 && responseData.data.tab_item[0].cell_data[0].cell_data && responseData.data.tab_item[0].cell_data[0].cell_data.length > 0 && responseData.data.tab_item[0].cell_data[0].cell_data[0].book_data) {\n finalResult.data.cell_view.book_data = responseData.data.tab_item[0].cell_data[0].cell_data[0].book_data\n }\n return finalResult\n}\n\nfunction normalizeResponse(data) {\n if (data.data && data.data.book_list && Array.isArray(data.data.book_list)) {\n return data.data.book_list;\n }\n \n if (data.book_info) return data.book_info;\n if (data.data && data.data.book_info) return data.data.book_info;\n if (data.data && Array.isArray(data.data)) return data.data;\n if (data.list) return data.list;\n if (data.book_list) return data.book_list;\n if (data.data && data.data.publication_list) return data.data.publication_list;\n if (data.result) return data.result;\n if (data.data && data.data.result) return data.data.result;\n if (data.data && data.data.cell_view && data.data.cell_view.book_data) {\n return data.data.cell_view.book_data;\n }\n if (data.data && data.data.records) return data.data.records;\n if (data.data && data.data.list) return data.data.list;\n \n return [];\n}\n\nif (baseUrl.includes(\"jj_signin\")) {\n let decodedToken = java.base64Decode(JJtoken());\n let timestamp = Date.now();\n let enbody = timestamp + ':' + decodedToken;\n let encodedEnbody = encode(enbody);\n \n let signUrl = \"https:\/\/android.jjwxc.net\/androidapi\/signin,{\\\"method\\\":\\\"POST\\\",\\\"body\\\":\\\"versionCode=507&sign=\" + encodedEnbody + \"\\\"}\";\n \n let signinResult = java.ajax(signUrl);\n \n let result = JSON.parse(signinResult);\n let message = result.message || \"签到请求完成\";\n \n let responseData = {\n \"isSuccess\": result.code == \"200\",\n \"errorMsg\": result.code == \"200\" ? \"success\" : \"fail\",\n \"data\": [{\n \"title\": message\n }]\n };\n \n JSON.stringify(responseData);\n} else if (baseUrl.includes(\"jjbookshelf\")) {\n let token = JJtoken();\n let config = getConfig.call(this);\n let latestType = config.type_id || '';\n let jjUrl = baseUrl.replace(\/type=[^&]*\/, 'type=' + latestType);\n if (!\/type=\/.test(jjUrl)) {\n jjUrl += (jjUrl.includes('?') ? '&' : '?') + 'type=' + latestType;\n }\n jjUrl = jjUrl + token;\n result = JSON.parse(java.ajax(jjUrl));\n JSON.stringify(result);\n} else if (baseUrl.includes(\"type_style\")) {\n let sortKey = getSortKey() || '';\n let newUrl = baseUrl + sortKey;\n result = JSON.parse(java.ajax(newUrl));\n \n let normalizedData = normalizeResponse(result);\n JSON.stringify({data: normalizedData});\n} else if (baseUrl.endsWith(\"bookshelf\")) {\n result = getShelf(\"bookshelf\/info\")\n} else if(\/read_history\/.test(baseUrl)) {\n let pageMatch = baseUrl.match(\/page=(\\d+)\/);\n let page = pageMatch ? pageMatch[1] : '1';\n \n let history_response = java.ajax(getServerHost() + \"\/read_history?session=\" + session + \"&page=\" + page)\n let history_data = JSON.parse(history_response)\n \n if (history_data.data.data_list) {\n let book_ids = history_data.data.data_list.map(item => item.book_id_str)\n \n let id_list = splitArray(book_ids, 200)\n let urls = []\n id_list.forEach(i => {\n urls.push(getServerHost() +\"\/detail?book_id=\" + i.join(\",\"))\n })\n \n let resp = {book_info: []}\n urls.forEach(url => {\n let detailResponse = java.ajax(url)\n let detailData = JSON.parse(detailResponse)\n if (detailData.data) {\n resp.book_info = resp.book_info.concat(detailData.data)\n }\n })\n \n result = resp\n } else {\n result = {book_info: []}\n }\n} else if (baseUrl.includes(\"style_top\")) {\n result = JSON.parse(result);\n} else if (baseUrl.includes(\"\/tab\/cai\")) {\n result = getGuessYouLike();\n} else {\n let w = baseUrl.split(\"\/\")\n if (baseUrl.includes(\"groupName\")) {\n result = getByGroupName(w[w.length - 1])\n } else if (baseUrl.includes(\"tab\")) {\n result = getByTabIndex(parseInt(w[w.length - 1]))\n } else {\n result = JSON.parse(result)\n if (result.data && result.data.data) {\n result = {book_info: result.data.data}\n }\n }\n}\n\nif (!baseUrl.includes(\"jjbookshelf\") && !baseUrl.includes(\"type_style\") && !baseUrl.includes(\"jj_signin\")) {\n let normalizedData = normalizeResponse(result);\n JSON.stringify({data: normalizedData})\n}\n<\/js>\n$.data[*]",
"bookUrl": "<js>\nlet source = result.source;\nlet book_url = result.book_url;\nlet title = result.title || result.book_name || result.bookName;\nlet book_id = result.book_id || result.series_id || result.bookId;\nlet cover = result.thumb_url || result.thumb_url || result.thumbUri || result.cover;\nlet desc = result.desc || result.abstract || result.book_abstract_v2;\n\nlet detail = {\n source: source,\n url: book_url,\n cover: cover,\n desc: desc,\n name: title\n};\n\nif (book_id && \/^\\d{19}$\/.test(book_id)) {\n detail.cover = cover;\n detail.desc = desc;\n detail.url = java.base64Encode(`https:\/\/api5-normal-sinfonlineb.fqnovel.com\/reading\/bookapi\/multi-detail\/v\/?aid=1967&iid=1&version_code=999&book_id=${book_id}`);\n detail.source = \"番茄小说\";\n} else if (book_url) {\n detail.url = java.base64Encode(book_url);\n}\n\nlet yunurl = java.base64Encode(JSON.stringify(detail));\n`data:detailsUrl;base64,${yunurl},{\"type\":\"susan\"}`;\n<\/js>",
"coverUrl": "$.thumb_url||$.audio_thumb_uri||$.thumbUri||$.cover",
"intro": "$.abstract||$.desc",
"kind": "$.tags",
"lastChapter": "$.lastChapterTitle||$.last_chapter_title||$.latestChapterTitle",
"name": "$.book_name||$.bookName||$.original_book_name||$.title",
"wordCount": "$.word_number||$.WordsCount"
},
"ruleSearch": {
"author": "$.author",
"bookList": "$.data",
"bookUrl": "<js>\nlet source = String(result.source);\nlet book_url = result.book_url;\nlet title = result.title;\nlet cover = result.cover;\nlet desc = result.desc;\n\nlet detail = {\n source: source,\n url: book_url,\n cover: cover,\n desc: desc,\n name: title\n};\n\nyunurl = java.base64Encode(JSON.stringify(detail));\n`data:detailsUrl;base64,${yunurl},{\"type\":\"susan\"}`;\n<\/js>",
"checkKeyWord": "我在精神病院学斩神@番茄小说",
"coverUrl": "$.cover",
"intro": "$.desc",
"kind": "$.tags",
"lastChapter": "{{$.source}}·{{$.latestChapterTitle}}",
"name": "$.title",
"wordCount": "$.wordCount"
},
"ruleToc": {
"chapterList": "<js>\nlet res = JSON.parse(java.hexDecodeToString(result));\nlet catalog = {source: res.source, url: res.url, name: res.name, bookid: res.bookid || \"\", tab: res.tab || \"novel\"};\n\nshowTabTips(catalog);\n\nlet op = {method: \"POST\", body: catalog};\n\nlet catalogUrl = getServerHost() + `\/catalog,${JSON.stringify(op)}`;\nlet response = java.ajax(catalogUrl);\n\/\/java.log(response);\n\nlet device = deviceType() ? 'ios' : 'android';\n\nif (device == 'android') {\n var typeMap = {audio:32, comic:64};\n book.type = catalog.tab in typeMap ? typeMap[catalog.tab] : 8;\n} else {\n var typeMap = {audio:1, comic:2, video:3};\n book.type = catalog.tab in typeMap ? typeMap[catalog.tab] : 0;\n}\n\nJSON.parse(response).data.map((x) => {\n if (x.isVolume == true) {\n x.url = \"\";\n return x;\n }\n \n x.url = buildChapterUrl(x, catalog);\n return x;\n});\n<\/js>",
"chapterName": "title",
"chapterUrl": "url",
"isPay": "isPay",
"isVip": "isVip",
"isVolume": "isVolume",
"updateTime": "tag"
},
"searchUrl": "<js>\nlet config = getConfig.call(this);\nif (!config.host || !config.gender) {\n config = { gender: \"boy\", host: getServerHost(), tag: \"多元\" };\n setConfigs.call(this, config);\n java.toast(\"配置初始化完成\");\n}\n\nvar sourceVal = '';\nvar customSource = '';\n\ntry {\n customSource = (source.getLoginInfoMap())['自定义源站'];\n} catch(e) {\n customSource = '';\n}\n\nif (customSource !== undefined && customSource !== null && String(customSource).trim() !== \"\") {\n sourceVal = String(customSource).trim();\n} else if (config.source) {\n sourceVal = config.source;\n}\n\nvar param = sourceVal ? '&source=' + encodeURIComponent(sourceVal) : '';\n\nresult = buildSearchResult(key, param, getServerHost());\n<\/js>",
"weight": 0
}