📚书山聚合

书山聚合

书山 (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, \"&amp;\").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=' + para;\n        case 'qm':\n            return host + '\/qm_comment?item_id=' + cid + '&book_id=' + bid + '&paragraph_id=' + para;\n        case 'qq':\n            return host + '\/qq_comment?bid=' + bid + '&cid=' + cid + '&para=' + para;\n        case 'qd':\n            return host + '\/qd_comment?bid=' + bid + '&cid=' + cid + '&para=' + para;\n        case 'fq':\n        default:\n            return host + '\/idea_comment?item_id=' + cid + '&book_id=' + bid + '&para=' + 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, '&amp;')\n                             .replace(\/<\/g, '&lt;')\n                             .replace(\/>\/g, '&gt;')\n                             .replace(\/\"\/g, '&quot;')\n                             .replace(\/'\/g, '&apos;');\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, '&amp;').replace(\/<\/g, '&lt;').replace(\/>\/g, '&gt;').replace(\/\"\/g, '&quot;').replace(\/'\/g, '&apos;'),\n                content: formattedContent.replace(\/&\/g, '&amp;').replace(\/<\/g, '&lt;').replace(\/>\/g, '&gt;').replace(\/\"\/g, '&quot;').replace(\/'\/g, '&apos;'),\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(\/&nbsp;\/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 + '&para=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, '&amp;').replace(\/<\/g, '&lt;').replace(\/>\/g, '&gt;');\n                                displayText = displayText.replace(\/&\/g, '&amp;').replace(\/<\/g, '&lt;').replace(\/>\/g, '&gt;');\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(\/&nbsp;\/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}&para=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": "&nbsp;&nbsp;\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 = `&nbsp;&nbsp;\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 = `&nbsp;&nbsp;\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}&lrm;\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&notUseBackpoint=&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
}
广告