星愿瓶(同人本地源)
http://114.67.83.0:54726
ah-fa-fa (14292)08/08 12:43
新增了书源支持刷3次广告领取阅读权限的按钮,在登录页点击单次刷广告或刷全部广告的按钮,完成状态也可以点按钮查询,此功能风险自行判断。以前叫胡桃爱读书,现在叫星愿瓶app的本地版本书源,账号需要自行前往星愿瓶app注册,以及获取到阅读权限,才能阅读
{
"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(\/ \/g, ' ').replace(\/&\/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
}