[{"bookSourceComment":"增加了一天3次刷广告领取阅读权限的按钮，风险问题自行判断\n1045801523群号\n纯免费，盗版源\ntg：https:\/\/t.me\/+SCsvr5O7YIc3NmIx","bookSourceGroup":"星愿瓶,本地源,刷广告","bookSourceName":"星愿瓶（同人本地源）","bookSourceType":0,"bookSourceUrl":"http:\/\/114.67.83.0:54726","bookUrlPattern":"^https?:\/\/114\\.67\\.(?:83\\.0|110\\.225):\\d+\/__legado_payload__\\?xy=","customButton":false,"customOrder":1,"enabled":true,"enabledCookieJar":false,"enabledExplore":true,"eventListener":false,"exploreUrl":"@js:\nXY.setEnv(java,source);\nvar out=[];\nvar ranks=[['总榜','all'],['日榜','day'],['小时榜','hour'],['周榜','week'],['月榜','month'],['反榜','trash']];\nfunction head(t){out.push({title:t,url:null,style:{layout_flexGrow:1,layout_flexBasisPercent:1}})}\nfunction add(t,c,r){out.push({title:t,url:\"@js:XY.setEnv(java,source);XY.dataUrl({kind:'rank',category:'\"+c+\"',rank_type:'\"+r+\"',page:Number(page||1)})\",style:{layout_flexGrow:0,layout_flexBasisPercent:0.29}})}\nhead('短篇排行榜');for(var i=0;i<ranks.length;i++)add(ranks[i][0],'short',ranks[i][1]);\nhead('长篇排行榜');for(var j=0;j<ranks.length;j++)add(ranks[j][0],'long',ranks[j][1]);\nJSON.stringify(out);","header":"{\"Content-Type\": \"text\/plain\", \"Accept\": \"application\/json, text\/plain, *\/*\", \"User-Agent\": \"okhttp\/4.12.0\"}","jsLib":"\nvar XY = (function () {\n  var ENV_JAVA = null;\n  var ENV_SOURCE = null;\n  function setEnv(j, s) {\n    ENV_JAVA = j || null;\n    ENV_SOURCE = s || null;\n    if (!ENV_SOURCE && ENV_JAVA) {\n      try { ENV_SOURCE = ENV_JAVA.getSource(); } catch (e) {}\n    }\n    return true;\n  }\n  function jx() {\n    if (!ENV_JAVA) throw new Error('阅读 Java 扩展未绑定');\n    return ENV_JAVA;\n  }\n  function srcObj() {\n    if (!ENV_SOURCE) throw new Error('阅读书源对象未绑定');\n    return ENV_SOURCE;\n  }\n  var CFG = {\n    appVersion: '3.1.5',\n    merchantId: 0,\n    siteId: 1,\n    bases: [\n      'http:\/\/114.67.83.0:54726\/',\n      'http:\/\/114.67.83.0:4\/',\n      'http:\/\/114.67.83.0:44335\/',\n      'http:\/\/114.67.83.0:5\/',\n      'http:\/\/114.67.83.0:55576\/',\n      'http:\/\/114.67.110.225:56593\/',\n      'http:\/\/114.67.110.225:5\/',\n      'http:\/\/114.67.110.225:62543\/',\n      'http:\/\/114.67.110.225:6\/'\n    ],\n    handshake: 'api\/app\/an\/a\/api\/get_notice.php',\n    publicKey: 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzWodhjsv7g9Wos0blunNH5LquiDPAP6ANbb\/2PwMF7lI1jK3D5CYriLZJcl5XU+6EFUazOAYMS+gOaI9EprjrYZW+7j0IoiB7E5tl47ij6N2PZT3bJeiha29Vv1DzzgW4hKtN\/i+3CY6H+ne1FlVNTuO2viLqiX3JnG8kSCVq1jXGcFU4c\/fTbE4HrjzSPWFjQCewDwCrlchBJtLgW1XWrRdrlHzgnxYRKgkGgz8\/dqGI2Xy5n5c1UCgDdtkwjiMbxIWohgD9TjYtf16oooRFlbiNJbFccSZdIoeAfG7kj4I3mBIdcG8wpyVfGGwFveP87B7pwlJAXyPl41LDalNLQIDAQAB'\n  };\n  var BACKUP_URL = 'http:\/\/xypapp.3ss.top\/by\/by.php';\n  var BACKUP_SECRET = 'xingyuanping_backup_api_fixed_secret_2026';\n\n  var SEARCH = {\n    '超级搜索': { key: 'local', path: 'api\/app\/an\/a\/api\/local_search.php' },\n    '纸糊': { key: 'paper', path: 'api\/app\/an\/a\/api\/parse.php' },\n    '绿江': { key: 'jj', path: 'api\/app\/an\/a\/api\/ksearch\/jj.php' },\n    '潇湘\/红袖': { key: 'qidian', path: 'api\/app\/an\/a\/api\/ksearch\/qidian.php' },\n    '番茄\/蛋花\/头条': { key: 'fanqie', path: 'api\/app\/an\/a\/api\/ksearch\/fanqie.php' },\n    '七猫\/熊猫脑洞': { key: 'qimao', path: 'api\/app\/an\/a\/api\/ksearch\/qimao.php' },\n    '书旗-短篇': { key: 'shuqi_short', path: 'api\/app\/an\/a\/api\/ksearch\/shuqi_short.php' },\n    '书旗-长篇': { key: 'shuqi_long', path: 'api\/app\/an\/a\/api\/ksearch\/shuqi_long.php' }\n  };\n\n  function text(v) { return v == null ? '' : String(v); }\n  function nowSec() { return Math.floor(Date.now() \/ 1000); }\n  function utf8(v) { return new Packages.java.lang.String(text(v)).getBytes('UTF-8'); }\n  function fromUtf8(v) { return String(new Packages.java.lang.String(v, 'UTF-8')); }\n  function randomBytes(n) {\n    return new Packages.java.security.SecureRandom().generateSeed(Number(n));\n  }\n  function concat(a, b) {\n    var out = new Packages.java.io.ByteArrayOutputStream();\n    out.write(a, 0, a.length);\n    out.write(b, 0, b.length);\n    return out.toByteArray();\n  }\n  function concatAll(arr) {\n    var out = new Packages.java.io.ByteArrayOutputStream();\n    for (var i = 0; i < arr.length; i++) out.write(arr[i], 0, arr[i].length);\n    return out.toByteArray();\n  }\n  function slice(a, start, end) {\n    return Packages.java.util.Arrays.copyOfRange(a, start, end);\n  }\n  function b64e(bytes) {\n    try { return String(Packages.java.util.Base64.getEncoder().encodeToString(bytes)); }\n    catch (e) { return String(Packages.android.util.Base64.encodeToString(bytes, Packages.android.util.Base64.NO_WRAP)); }\n  }\n  function b64d(v) {\n    var s = text(v).replace(\/-\/g, '+').replace(\/_\/g, '\/');\n    while (s.length % 4) s += '=';\n    try { return Packages.java.util.Base64.getDecoder().decode(s); }\n    catch (e) { return Packages.android.util.Base64.decode(s, Packages.android.util.Base64.DEFAULT); }\n  }\n  function b64url(v) { return b64e(utf8(v)).replace(\/\\+\/g, '-').replace(\/\\\/\/g, '_').replace(\/=+$\/g, ''); }\n  function hex(bytes) {\n    var out = '';\n    for (var i = 0; i < bytes.length; i++) {\n      var h = (bytes[i] & 255).toString(16);\n      out += h.length < 2 ? '0' + h : h;\n    }\n    return out;\n  }\n  function digest(alg, v) {\n    return hex(Packages.java.security.MessageDigest.getInstance(alg).digest(utf8(v)));\n  }\n  function digestBytes(alg, v) {\n    return Packages.java.security.MessageDigest.getInstance(alg).digest(utf8(v));\n  }\n  function randomHex(n) { return hex(randomBytes(Math.ceil(n \/ 2))).slice(0, n); }\n  function rsa(bytes) {\n    var spec = new Packages.java.security.spec.X509EncodedKeySpec(b64d(CFG.publicKey));\n    var key = Packages.java.security.KeyFactory.getInstance('RSA').generatePublic(spec);\n    var c = Packages.javax.crypto.Cipher.getInstance('RSA\/ECB\/OAEPWithSHA-1AndMGF1Padding');\n    try {\n      var oaep = new Packages.javax.crypto.spec.OAEPParameterSpec(\n        'SHA-1', 'MGF1', Packages.java.security.spec.MGF1ParameterSpec.SHA1,\n        Packages.javax.crypto.spec.PSource.PSpecified.DEFAULT\n      );\n      c.init(Packages.javax.crypto.Cipher.ENCRYPT_MODE, key, oaep);\n    } catch (e) { c.init(Packages.javax.crypto.Cipher.ENCRYPT_MODE, key); }\n    return b64e(c.doFinal(bytes));\n  }\n  function aesEncrypt(plain, key, aad) {\n    var iv = randomBytes(12);\n    var c = Packages.javax.crypto.Cipher.getInstance('AES\/GCM\/NoPadding');\n    var ks = new Packages.javax.crypto.spec.SecretKeySpec(key, 'AES');\n    var gs = new Packages.javax.crypto.spec.GCMParameterSpec(128, iv);\n    c.init(Packages.javax.crypto.Cipher.ENCRYPT_MODE, ks, gs);\n    if (aad) c.updateAAD(utf8(aad));\n    var all = c.doFinal(utf8(plain));\n    return { iv: iv, cipher: slice(all, 0, all.length - 16), tag: slice(all, all.length - 16, all.length) };\n  }\n  function aesDecrypt(env, key) {\n    var iv = b64d(env.iv), body = concat(b64d(env.c), b64d(env.tag));\n    var c = Packages.javax.crypto.Cipher.getInstance('AES\/GCM\/NoPadding');\n    c.init(Packages.javax.crypto.Cipher.DECRYPT_MODE,\n      new Packages.javax.crypto.spec.SecretKeySpec(key, 'AES'),\n      new Packages.javax.crypto.spec.GCMParameterSpec(128, iv));\n    return fromUtf8(c.doFinal(body));\n  }\n  function encodeBody(plain) {\n    var payload = b64url(plain);\n    return randomHex(4) + payload + randomHex(4);\n  }\n  function decodeBody(raw) {\n    var s = text(raw).trim();\n    if (s.length < 9) throw new Error('服务器响应过短');\n    return fromUtf8(b64d(s.slice(4, -4)));\n  }\n  function cget(k) {\n    try { return text(srcObj().get(text(k))); } catch (e) { return ''; }\n  }\n  function cput(k, v) {\n    srcObj().put(text(k), text(v));\n  }\n  function deviceId() {\n    var d = cget('xy_device_id');\n    if (!\/^[0-9a-f]{64}$\/.test(d)) {\n      var fp = 'shiba|google|shiba|shiba|Google|Pixel 8|shiba';\n      try {\n        var aid = text(jx().androidId());\n        if (aid) fp = aid + '|android|xingyuan|legado';\n      } catch (e) {}\n      d = digest('SHA-256', fp);\n      cput('xy_device_id', d);\n    }\n    return d;\n  }\n  function join(base, path) { return text(base).replace(\/\\\/+$\/, '') + '\/' + text(path).replace(\/^\\\/+\/, ''); }\n  function ajaxPost(url, body) {\n    var opt = {\n      method: 'POST', body: text(body),\n      headers: {\n        'Content-Type': 'text\/plain', 'Accept': 'application\/json, text\/plain, *\/*',\n        'Cache-Control': 'no-cache', 'Connection': 'close',\n        'User-Agent': 'okhttp\/4.12.0'\n      },\n      connectTimeout: 60000, readTimeout: 60000\n    };\n    return text(jx().ajax(url + ',' + JSON.stringify(opt))).trim();\n  }\n  function backupKey() {\n    return digestBytes('SHA-256', BACKUP_SECRET);\n  }\n  function backupEncrypt(plain) {\n    var bk = backupKey();\n    var iv = randomBytes(16);\n    var c = Packages.javax.crypto.Cipher.getInstance('AES\/CBC\/PKCS5Padding');\n    c.init(Packages.javax.crypto.Cipher.ENCRYPT_MODE,\n      new Packages.javax.crypto.spec.SecretKeySpec(bk, 'AES'),\n      new Packages.javax.crypto.spec.IvParameterSpec(iv));\n    var ct = c.doFinal(utf8(plain));\n    var mac = Packages.javax.crypto.Mac.getInstance('HmacSHA256');\n    mac.init(new Packages.javax.crypto.spec.SecretKeySpec(bk, 'HmacSHA256'));\n    mac.update(concat(iv, ct));\n    return b64e(concatAll([iv, ct, mac.doFinal()]));\n  }\n  function backupDecrypt(v) {\n    var bk = backupKey();\n    var all = b64d(v);\n    if (all.length < 48) throw new Error('备份响应过短');\n    var iv = slice(all, 0, 16);\n    var sig = slice(all, all.length - 32, all.length);\n    var ct = slice(all, 16, all.length - 32);\n    var mac = Packages.javax.crypto.Mac.getInstance('HmacSHA256');\n    mac.init(new Packages.javax.crypto.spec.SecretKeySpec(bk, 'HmacSHA256'));\n    mac.update(concat(iv, ct));\n    if (!Packages.java.util.Arrays.equals(mac.doFinal(), sig)) throw new Error('备份响应 HMAC 校验失败');\n    var c = Packages.javax.crypto.Cipher.getInstance('AES\/CBC\/PKCS5Padding');\n    c.init(Packages.javax.crypto.Cipher.DECRYPT_MODE,\n      new Packages.javax.crypto.spec.SecretKeySpec(bk, 'AES'),\n      new Packages.javax.crypto.spec.IvParameterSpec(iv));\n    return fromUtf8(c.doFinal(ct));\n  }\n  function backupPost(url, jsonBody) {\n    var opt = {\n      method: 'POST', body: text(jsonBody),\n      headers: {\n        'Content-Type': 'application\/json', 'Accept': 'application\/json, text\/plain, *\/*',\n        'User-Agent': 'okhttp\/4.12.0'\n      },\n      connectTimeout: 15000, readTimeout: 15000\n    };\n    return text(jx().ajax(url + ',' + JSON.stringify(opt))).trim();\n  }\n  function getBackupDomains(did) {\n    var p = {\n      action: 'domains', timestamp: nowSec(), platform: 'android',\n      app_version: CFG.appVersion, device_id: did,\n      client_info: { network: 'wifi', system_version: 'Android 11' }\n    };\n    var resp = backupPost(BACKUP_URL, JSON.stringify({ data: backupEncrypt(JSON.stringify(p)) }));\n    var r = JSON.parse(resp);\n    var d = JSON.parse(backupDecrypt(r.data));\n    var domains = (d.data && d.data.backup_domains) || [];\n    var out = [];\n    for (var i = 0; i < domains.length; i++) {\n      if (domains[i].enabled && domains[i].base_url) {\n        var burl = text(domains[i].base_url);\n        while (burl.charAt(burl.length - 1) === '\/') burl = burl.slice(0, -1);\n        out.push(burl + '\/');\n      }\n    }\n    return out;\n  }\n  function decryptResponse(raw, key) {\n    var s = text(raw).trim();\n    if (s.charAt(0) === '{') return JSON.parse(s);\n    return JSON.parse(aesDecrypt(JSON.parse(decodeBody(s)), key));\n  }\n  function handshake(force) {\n    var saved = cget('xy_aes_key');\n    if (!force && saved) return b64d(saved);\n    var preferred = cget('xy_base_url'), bases = CFG.bases.slice(0);\n    if (preferred) { bases = bases.filter(function (x) { return x !== preferred; }); bases.unshift(preferred); }\n    try {\n      var bd = getBackupDomains(deviceId());\n      for (var bi = bd.length - 1; bi >= 0; bi--) { if (bases.indexOf(bd[bi]) < 0) bases.unshift(bd[bi]); }\n    } catch (e) {}\n    var errors = [];\n    for (var i = 0; i < bases.length; i++) {\n      try {\n        var key = randomBytes(32), ts = nowSec(), d = deviceId(), ek = rsa(key);\n        var sigHash = digest('SHA-256', d + '|' + ts + '|' + ek);\n        var req = {\n          d: d, k: ek, t: ts, s: rsa(utf8(sigHash)), merchant_id: CFG.merchantId,\n          site_id: CFG.siteId, platform: 'android', device_name: 'Legado', app_version: CFG.appVersion, is_rooted: false\n        };\n        var result = decryptResponse(ajaxPost(join(bases[i], CFG.handshake), encodeBody(JSON.stringify(req))), key);\n        if (Number(result.code) !== 0) throw new Error(result.message || ('code=' + result.code));\n        cput('xy_base_url', bases[i]); cput('xy_aes_key', b64e(key));\n        return key;\n      } catch (e) { errors.push(bases[i] + ' ' + text(e.message || e)); }\n    }\n    throw new Error('星愿瓶所有线路握手失败：' + errors.join('；'));\n  }\n  function requestBody(data, key) {\n    var ts = nowSec(), nonce = randomHex(16), d = deviceId();\n    var enc = aesEncrypt(JSON.stringify(data), key, ts + '|' + nonce + '|' + d);\n    return encodeBody(JSON.stringify({ d: d, t: ts, n: nonce, iv: b64e(enc.iv), c: b64e(enc.cipher), tag: b64e(enc.tag) }));\n  }\n  function securePost(path, data, retried) {\n    try {\n      var key = handshake(false), base = cget('xy_base_url') || CFG.bases[0];\n      return decryptResponse(ajaxPost(join(base, path), requestBody(data, key)), key);\n    } catch (e) {\n      if (retried) throw e;\n      cput('xy_aes_key', '');\n      handshake(true);\n      return securePost(path, data, true);\n    }\n  }\n  function common(extra) {\n    var out = { token: cget('xy_token'), merchant_id: CFG.merchantId, site_id: CFG.siteId };\n    extra = extra || {};\n    for (var k in extra) out[k] = extra[k];\n    return out;\n  }\n  function ok(r, label) {\n    if (!r || Number(r.code) !== 0) throw new Error(label + '：' + text(r && r.message || '接口返回失败'));\n    return r.data || {};\n  }\n  function token() { return cget('xy_token'); }\n  function login(user, pass) {\n    if (!text(user).trim() || !text(pass)) throw new Error('请输入账号和密码');\n    var r = securePost('api\/app\/an\/a\/api\/login.php', {\n      username: text(user).trim(), password: text(pass), device_name: 'Legado', app_version: CFG.appVersion\n    });\n    var d = ok(r, '登录失败');\n    if (!d.token) throw new Error('登录成功但未返回 Token');\n    cput('xy_token', d.token); cput('xy_username', text(user).trim());\n    return d;\n  }\n  function searchSource() {\n    var s = cget('xy_search_source');\n    if (SEARCH[s]) return s;\n    try {\n      var m = source.getLoginInfoMap() || {};\n      s = text(m['搜索来源']);\n    } catch (e) {}\n    return SEARCH[s] ? s : '超级搜索';\n  }\n  function saveSearchSource(v) {\n    var s = text(v);\n    if (!SEARCH[s]) s = '超级搜索';\n    cput('xy_search_source', s);\n    return s;\n  }\n  function dataUrl(obj) {\n    var base = cget('xy_base_url') || CFG.bases[0];\n    return join(base, '__legado_payload__') + '?xy=' + b64url(JSON.stringify(obj));\n  }\n  function payload(raw) {\n    var s = text(raw).trim();\n    var match = s.match(\/[?&]xy=([^&#]+)\/);\n    if (match && match[1]) s = fromUtf8(b64d(decodeURIComponent(match[1])));\n    else if (\/^data:text\\\/plain;base64,\/i.test(s)) s = fromUtf8(b64d(s.replace(\/^data:text\\\/plain;base64,\/i, '').split(\/[#,]\/)[0]));\n    return JSON.parse(s || '{}');\n  }\n  function stripHtml(v) { return text(v).replace(\/<[^>]+>\/g, '').replace(\/&nbsp;\/g, ' ').replace(\/&amp;\/g, '&').trim(); }\n  function pick(o, names, def) {\n    o = o || {};\n    for (var i = 0; i < names.length; i++) if (o[names[i]] != null && text(o[names[i]]) !== '') return o[names[i]];\n    return def == null ? '' : def;\n  }\n  function resultList(d) {\n    if (Array.isArray(d)) return d;\n    var names = ['list', 'results', 'books', 'items'];\n    for (var i = 0; i < names.length; i++) if (Array.isArray(d && d[names[i]])) return d[names[i]];\n    return [];\n  }\n  function normalizeSearch(item, sourceName) {\n    var src = SEARCH[sourceName] || SEARCH['超级搜索'];\n    var title = stripHtml(pick(item, ['display_title', 'title', 'book_name', 'book_title', 'article_title', 'name']));\n    var author = stripHtml(pick(item, ['author', 'author_name', 'display_info', 'cp_name']));\n    var intro = stripHtml(pick(item, ['intro', 'abstract', 'book_intro', 'article_intro', 'content', 'sub_title']));\n    var cover = pick(item, ['image_link', 'img_url', 'cover_url', 'cover', 'book_cover', 'article_cover', 'audio_thumb_uri']);\n    var kind = stripHtml(pick(item, ['category', 'ptags', 'top_category_name', 'first_category_name', 'book_type_name', 'labels']));\n    var p;\n    if (src.key === 'paper') p = { kind: 'paper', mark: pick(item, ['mark']), title: title };\n    else if (src.key === 'local') p = { kind: 'direct', book_id: pick(item, ['book_id', 'bookId']), article_id: pick(item, ['article_id', 'articleId']) };\n    else p = { kind: 'external', source: src.key, book_id: pick(item, ['book_id', 'bookId', 'id']), book_type: pick(item, ['book_type'], 0) };\n    return {\n      name: title || ('星愿瓶书籍 ' + text(p.book_id || p.mark)), author: author,\n      intro: intro, coverUrl: text(cover), kind: kind || sourceName,\n      lastChapter: stripHtml(pick(item, ['last_chapter_title', 'state', 'sub_title', 'likeText'])),\n      wordCount: text(pick(item, ['word_count', 'word_number', 'score', 'rating_score'])),\n      bookUrl: dataUrl(p)\n    };\n  }\n  function search(p) {\n    var sourceName = SEARCH[p.source] ? p.source : searchSource(), src = SEARCH[sourceName];\n    var page = Math.max(1, Number(p.page || 1)), keyword = text(p.key).trim();\n    if (!keyword) return [];\n    var d;\n    if (src.key === 'paper') {\n      d = ok(securePost(src.path, common({ channel_id: 9, content: keyword })), '纸糊搜索失败');\n      var paper = resultList(d), start = (page - 1) * 20;\n      paper = paper.slice(start, start + 20);\n      return paper.map(function (x) { return normalizeSearch(x, sourceName); });\n    }\n    d = ok(securePost(src.path, common({ keyword: keyword, page: page, page_size: 20 })), sourceName + '搜索失败');\n    return resultList(d).map(function (x) { return normalizeSearch(x, sourceName); });\n  }\n  function rank(p) {\n    var cat = p.category === 'long' ? 'long' : 'short';\n    var path = cat === 'long' ? 'api\/app\/an\/a\/api\/get_book_rank_list.php' : 'api\/app\/an\/a\/api\/get_rank_list.php';\n    var d = ok(securePost(path, common({ rank_type: text(p.rank_type || 'all'), page: Math.max(1, Number(p.page || 1)), page_size: 20 })), '排行榜加载失败');\n    return resultList(d).map(function (x) {\n      var isLong = cat === 'long';\n      return {\n        name: stripHtml(pick(x, isLong ? ['book_title', 'title'] : ['article_title', 'title'])),\n        author: stripHtml(pick(x, ['author_name', 'author'])),\n        intro: stripHtml(pick(x, isLong ? ['book_intro', 'intro'] : ['article_intro', 'intro'])),\n        coverUrl: text(pick(x, isLong ? ['book_cover', 'cover'] : ['article_cover', 'cover'])),\n        kind: (isLong ? '长篇' : '短篇') + ' · 第' + text(pick(x, ['rank_position'], '?')) + '名',\n        lastChapter: '热度 ' + text(pick(x, ['rank_score', 'period_votes'], 0)),\n        wordCount: text(pick(x, ['rating_score'], '')),\n        bookUrl: dataUrl({ kind: 'direct', book_id: pick(x, ['book_id']), article_id: pick(x, ['article_id']) })\n      };\n    });\n  }\n  function listFromPayload(raw) {\n    var p = payload(raw);\n    if (p.kind === 'search') return search(p);\n    if (p.kind === 'rank') return rank(p);\n    return [];\n  }\n  function externalUrl(p) {\n    var id = encodeURIComponent(text(p.book_id));\n    if (p.source === 'jj') return 'https:\/\/www.jjwxc.net\/onebook.php?novelid=' + id;\n    if (p.source === 'qidian') return 'https:\/\/www.qidian.com\/book\/' + id + '\/';\n    if (p.source === 'fanqie') return 'https:\/\/m.toutiao.com\/novel\/book\/page\/v1\/share\/page\/?book_id=' + id;\n    if (p.source === 'qimao') return Number(p.book_type) === 4\n      ? 'https:\/\/app-share.wtzw.com\/app-h5\/freebook\/short-story-detail\/' + id\n      : 'https:\/\/app-share.wtzw.com\/app-h5\/freebook\/article-detail\/' + id;\n    if (p.source === 'shuqi_short') return 'https:\/\/t.shuqi.com\/shortNovel\/reader\/' + id;\n    if (p.source === 'shuqi_long') return 'http:\/\/c10.shuqireader.com\/sapi\/book\/sharecover\/?bookId=' + id + '&anyUpTime=1670376345&topClass=502&platform=ios';\n    throw new Error('不支持的外部搜索来源：' + p.source);\n  }\n  function resolveBookId(p) {\n    if (p.kind === 'direct' && p.book_id) return text(p.book_id);\n    var d;\n    if (p.kind === 'paper') d = ok(securePost('api\/app\/an\/a\/api\/parse_article.php', common({ mark: text(p.mark) })), '纸糊结果解析失败');\n    else if (p.kind === 'external') d = ok(securePost('api\/app\/an\/a\/api\/parse.php', common({ channel_id: 9, content: externalUrl(p) })), '外部书籍解析失败');\n    else throw new Error('书籍链接参数无效');\n    var id = pick(d, ['book_id', 'bookId']);\n    if (!id) throw new Error('解析成功但未返回星愿瓶书籍 ID');\n    return text(id);\n  }\n  function getBook(id) { return ok(securePost('api\/app\/an\/a\/api\/get_book.php', common({ book_id: Number(id) })), '详情加载失败'); }\n  function bookInfo(raw) {\n    if (!token()) throw new Error('请先在书源登录页登录星愿瓶');\n    var p = payload(raw), id = resolveBookId(p), d = getBook(id);\n    var b = d.bookinfo || d.book_info || d.book || d;\n    var chapters = resultList({ list: d.chapters || d.chapter_list || [] });\n    var last = chapters.length ? chapters[chapters.length - 1] : {};\n    var categories = pick(b, ['categories', 'category'], []);\n    if (Array.isArray(categories)) categories = categories.map(function (x) { return typeof x === 'object' ? pick(x, ['name', 'category_name', 'title']) : text(x); }).join(', ');\n    return {\n      name: stripHtml(pick(b, ['book_title', 'book_name', 'title'])),\n      author: stripHtml(pick(b, ['author_name', 'author'])),\n      coverUrl: text(pick(b, ['book_cover', 'cover', 'cover_url'])),\n      intro: stripHtml(pick(b, ['book_abstract', 'abstract', 'intro'])),\n      kind: stripHtml(categories),\n      lastChapter: stripHtml(pick(last, ['article_title', 'chapter_title', 'title'])),\n      wordCount: text(pick(b, ['article_num', 'chapter_count'], chapters.length)) + '章',\n      tocUrl: dataUrl({ kind: 'toc', book_id: id })\n    };\n  }\n  function chapters(raw) {\n    var p = payload(raw), d = getBook(p.book_id), list = d.chapters || d.chapter_list || [];\n    return list.map(function (x, i) {\n      var aid = pick(x, ['article_id', 'chapter_id', 'id']);\n      return {\n        chapterName: stripHtml(pick(x, ['article_title', 'chapter_title', 'title'], '第' + (i + 1) + '章')),\n        chapterUrl: dataUrl({ kind: 'chapter', book_id: text(p.book_id), article_id: text(aid) }),\n        isVip: false, isPay: false,\n        updateTime: text(pick(x, ['update_time', 'updated_at'])),\n        wordCount: text(pick(x, ['word_count', 'article_word_count']))\n      };\n    });\n  }\n  function content(raw) {\n    var p = payload(raw);\n    var d = ok(securePost('api\/app\/an\/a\/api\/get_article.php', common({ book_id: Number(p.book_id), article_id: Number(p.article_id) })), '正文加载失败');\n    var body = pick(d, ['article_content', 'content', 'chapter_content']);\n    if (!text(body).trim()) {\n      var note = pick(d, ['message', 'article_abstract', 'abstract'], '正文为空或账号暂无访问权限');\n      throw new Error(text(note));\n    }\n    return text(body).replace(\/\\r\\n\/g, '\\n').replace(\/\\r\/g, '\\n');\n  }\n  function getVideoRewardRules() {\n    return securePost('api\/app\/an\/a\/api\/get_video_reward_rules.php', common({}));\n  }\n  function createVideoAd(syspt) {\n    return securePost('api\/app\/an\/a\/api\/get_video_ad.php', common({ syspt: Number(syspt || 1) }));\n  }\n  function reportTransId(guanggaoid, transId) {\n    var gid = text(guanggaoid).trim(), tid = text(transId).trim();\n    if (!gid) throw new Error('广告任务标识不能为空');\n    if (!tid) throw new Error('广告 SDK 交易标识不能为空');\n    return securePost('api\/app\/an\/a\/api\/ad_report_transid.php', common({ guanggaoid: gid, transId: tid }));\n  }\n  function getVideoAdStatus(guanggaoid) {\n    var gid = text(guanggaoid).trim();\n    if (!gid) throw new Error('广告任务标识不能为空');\n    return securePost('api\/app\/an\/a\/api\/get_video_ad_status.php', common({ guanggaoid: gid }));\n  }\n  function simulateTransId() {\n    return 'sdk_trans_' + randomHex(24);\n  }\n  function extractAdId(r) {\n    if (!r || Number(r.code) !== 0) return '';\n    var d = r.data;\n    if (typeof d === 'string' || typeof d === 'number') return String(d);\n    if (d && typeof d === 'object') return String(d.guanggaoid || d.id || '');\n    return '';\n  }\n  function todayStr() {\n    var sdf = new Packages.java.text.SimpleDateFormat('yyyyMMdd');\n    return String(sdf.format(new Packages.java.util.Date()));\n  }\n  function getTodayAdCount() {\n    var key = 'xy_ad_count_' + todayStr();\n    var localCount = Number(cget(key) || 0);\n    var serverCount = 0;\n    try {\n      var prog = getVideoRewardRules();\n      if (prog && Number(prog.code) === 0) serverCount = Number((prog.data || {}).today_watch_count || 0);\n    } catch (e) {}\n    var count = Math.max(localCount, serverCount);\n    if (serverCount > localCount) cput(key, String(serverCount));\n    return count;\n  }\n  function incTodayAdCount() {\n    var key = 'xy_ad_count_' + todayStr();\n    cput(key, String(Number(cget(key) || 0) + 1));\n  }\n  function watchAdAuto() {\n    var count = getTodayAdCount();\n    if (count >= 3) throw new Error('今日已成功刷3次，无法继续');\n    var ad = createVideoAd(1);\n    if (!ad || Number(ad.code) !== 0) throw new Error('创建广告任务失败：' + text(ad && ad.message || '未知错误'));\n    var gid = extractAdId(ad);\n    if (!gid) throw new Error('未获取到广告标识');\n    var tid = simulateTransId();\n    var report = reportTransId(gid, tid);\n    if (!report || Number(report.code) !== 0) throw new Error('上报transId失败：' + text(report && report.message || ''));\n    var st, sd, lastMsg = '';\n    for (var i = 0; i < 15; i++) {\n      if (i > 0) Packages.java.lang.Thread.sleep(2000);\n      st = getVideoAdStatus(gid);\n      sd = (st && st.data) || {};\n      lastMsg = text(st && st.message || '');\n      if (Number(st && st.code) === 0 && Number(sd.status) >= 1) {\n        incTodayAdCount();\n        return { success: true, gid: gid, award: text(sd.award_name || sd.award_code || sd.reward_name || '已到账') };\n      }\n    }\n    return { success: false, gid: gid, message: lastMsg || '轮询超时' };\n  }\n  function watchAdAll(onProgress) {\n    var target = 3, done = getTodayAdCount();\n    var remaining = Math.max(0, target - done);\n    if (remaining === 0) return { total: 0, success: 0, results: [], alreadyDone: done, message: '今日已成功刷3次，无需再刷' };\n    var results = [];\n    var success = 0;\n    for (var i = 0; i < remaining; i++) {\n      if (onProgress) { try { onProgress(i + 1, remaining, success, done); } catch (e) {} }\n      try {\n        var r = watchAdAuto();\n        results.push(r);\n        if (r.success) {\n          success++;\n          if (done + success >= 3) break;\n        }\n        else { break; }\n      } catch (e) {\n        results.push({ success: false, message: text(e.message || e) });\n        break;\n      }\n      if (i < remaining - 1) Packages.java.lang.Thread.sleep(2000);\n    }\n    return { total: remaining, success: success, results: results, alreadyDone: done };\n  }\n  return {\n    setEnv: setEnv, config: CFG, searchNames: Object.keys(SEARCH), token: token, login: login,\n    searchSource: searchSource, saveSearchSource: saveSearchSource,\n    dataUrl: dataUrl, listFromPayload: listFromPayload, bookInfo: bookInfo,\n    chapters: chapters, content: content, handshake: handshake, securePost: securePost,\n    getVideoRewardRules: getVideoRewardRules, createVideoAd: createVideoAd,\n    reportTransId: reportTransId, getVideoAdStatus: getVideoAdStatus,\n    simulateTransId: simulateTransId, watchAdAuto: watchAdAuto, watchAdAll: watchAdAll,\n    getTodayAdCount: getTodayAdCount\n  };\n})();\n","lastUpdateTime":"1786164208174","loginUi":"@js:\nXY.setEnv(java,source);\nvar FULL={layout_flexGrow:1,layout_flexBasisPercent:1};\nvar HALF={layout_flexGrow:1,layout_flexBasisPercent:0.45};\nvar current=XY.searchSource();\nresult=JSON.stringify([\n  {name:'账号',type:'text',style:FULL},\n  {name:'密码',type:'password',style:FULL},\n  {name:'搜索来源',type:'select',chars:XY.searchNames,default:current,action:'xySaveSource()',style:FULL},\n  {name:'登录 \/ 重新登录',type:'button',action:'xyLogin()',style:HALF},\n  {name:'检测会话',type:'button',action:'xyCheck()',style:HALF},\n  {name:'查询奖励进度',type:'button',action:'xyAdRules()',style:HALF},\n  {name:'自动刷广告(单次)',type:'button',action:'xyAutoWatchAd()',style:HALF},\n  {name:'自动刷全部广告',type:'button',action:'xyAutoWatchAll()',style:HALF}\n]);","loginUrl":"function xyLoginMap(){\n  try{if(typeof result==='object'&&result)return result}catch(e){}\n  try{return source.getLoginInfoMap()||{}}catch(e){return {}}\n}\nfunction xySay(v,longMode){try{if(longMode&&java.longToast)java.longToast(String(v));else java.toast(String(v))}catch(e){}}\nfunction xyRequireAdLogin(){if(!XY.token())throw new Error('请先登录星愿瓶账号')}\nfunction xyPendingAd(){try{return String(source.get('xy_ad_guanggaoid')||'').trim()}catch(e){return ''}}\nfunction xyClearAd(){source.put('xy_ad_guanggaoid','');source.put('xy_ad_trans_id','')}\nfunction xySaveSource(){XY.setEnv(java,source);var m=xyLoginMap();var s=XY.saveSearchSource(m['搜索来源']);xySay('搜索来源已切换为：'+s,false)}\nfunction xyLogin(){\n  XY.setEnv(java,source);var m=xyLoginMap();XY.saveSearchSource(m['搜索来源']);\n  try{XY.login(m['账号'],m['密码']);xySay('星愿瓶登录成功\\n搜索来源：'+XY.searchSource(),true)}\n  catch(e){xySay('登录失败：'+String(e.message||e),true);throw e}\n}\nfunction xyAdRules(){\n  XY.setEnv(java,source);\n  try{\n    xyRequireAdLogin();var r=XY.getVideoRewardRules();\n    if(!r||Number(r.code)!==0)throw new Error(String(r&&r.message||'奖励规则获取失败'));\n    var d=r.data||{},rule=d.reward_rule||{},detail=rule.rule_details||{};\n    var watched=Number(d.today_watch_count||0),total=Number(detail.total_required_count||d.total_required_count||0);\n    var msg='今日广告进度：'+watched+(total?'\/'+total:' 次');\n    var ruleName=d.rule_name||rule.rule_name||'';if(ruleName)msg+='\\n规则：'+String(ruleName);\n    var pending=xyPendingAd();if(pending)msg+='\\n有待确认任务：'+pending;\n    xySay(msg,true);\n  }catch(e){xySay('奖励进度查询失败：'+String(e.message||e),true)}\n}\nfunction xyCheck(){XY.setEnv(java,source);try{XY.handshake(false);xySay(XY.token()?'会话有效，当前已登录':'协议连接正常，但尚未登录',true)}catch(e){xySay('连接检测失败：'+String(e.message||e),true)}}\nfunction xyAutoWatchAd(){\n  XY.setEnv(java,source);\n  try{\n    xyRequireAdLogin();\n    var pending=xyPendingAd();\n    if(pending){xyClearAd();xySay('检测到上次残留任务，已清理，请重新点击',true);return}\n    xySay('正在自动刷广告(单次)...',false);\n    var r=XY.watchAdAuto();\n    if(r.success)xySay('✅ 刷广告成功\\n奖励：'+r.award,true);\n    else xySay('⚠️ 刷广告未成功\\n'+String(r.message||'未知'),true);\n  }catch(e){xySay('❌ 刷广告失败\\n'+String(e.message||e),true)}\n}\nfunction xyAutoWatchAll(){\n  XY.setEnv(java,source);\n  try{\n    xyRequireAdLogin();\n    var pending=xyPendingAd();\n    if(pending){xyClearAd();xySay('检测到上次残留任务，已清理，请重新点击',true);return}\n    var done=XY.getTodayAdCount();\n    var remaining=Math.max(0,3-done);\n    if(remaining===0){xySay('✅ 今日已成功刷3次\\n无需再刷',true);return}\n    xySay('开始自动刷广告\\n今日已完成'+done+'次，还需刷'+remaining+'次',false);\n    var r=XY.watchAdAll(function(cur,total,success,prevDone){\n      xySay('正在刷第'+cur+'\/'+total+'次\\n✅ 已成功'+success+'次',false);\n    });\n    var msg;\n    if(r.total===0){\n      msg='✅ 今日已成功刷3次\\n无需再刷';\n    }else{\n      var totalDone=done+r.success;\n      msg='✅ 刷广告完成\\n今日共成功'+totalDone+'\/3次';\n      if(r.success>0)msg+='\\n本次新刷成功'+r.success+'次';\n      if(r.success<r.total){\n        var failed=r.results.filter(function(x){return !x.success});\n        if(failed.length)msg+='\\n⚠️ 失败原因：'+String(failed[0].message||'');\n      }\n      if(totalDone>=3)msg+='\\n今日额度已用完';\n    }\n    xySay(msg,true);\n  }catch(e){xySay('❌ 自动刷广告失败\\n'+String(e.message||e),true)}\n}\nfunction login(){xyLogin()}","respondTime":180000,"ruleBookInfo":{"author":"$.author","coverUrl":"$.coverUrl","init":"<js>XY.setEnv(java,source);JSON.stringify(XY.bookInfo(baseUrl))<\/js>","intro":"$.intro","kind":"$.kind","lastChapter":"$.lastChapter","name":"$.name","tocUrl":"$.tocUrl","wordCount":"$.wordCount"},"ruleContent":{"content":"@js:XY.setEnv(java,source);(function(){var last=null;for(var i=0;i<3;i++){try{return XY.content(baseUrl)}catch(e){last=e;var m=String(e&&e.message||e);if(m.indexOf('请求频繁')<0)throw e;Packages.java.lang.Thread.sleep(1100)}}throw last})()"},"ruleExplore":{"author":"author","bookList":"<js>XY.setEnv(java,source);XY.listFromPayload(baseUrl)<\/js>","bookUrl":"bookUrl","coverUrl":"coverUrl","intro":"intro","kind":"kind","lastChapter":"lastChapter","name":"name","wordCount":"wordCount"},"ruleSearch":{"author":"author","bookList":"<js>XY.setEnv(java,source);XY.listFromPayload(baseUrl)<\/js>","bookUrl":"bookUrl","coverUrl":"coverUrl","intro":"intro","kind":"kind","lastChapter":"lastChapter","name":"name","wordCount":"wordCount"},"ruleToc":{"chapterList":"<js>XY.setEnv(java,source);XY.chapters(baseUrl)<\/js>","chapterName":"chapterName","chapterUrl":"chapterUrl","isPay":"isPay","isVip":"isVip","updateTime":"updateTime"},"searchUrl":"@js:XY.setEnv(java,source);XY.dataUrl({kind:'search',source:XY.searchSource(),key:String(key||'').trim(),page:Number(page||1)})","weight":0}]