✿次元姬(知秋)✿
https://cyj.vv9v.cn
zhiqiu123789 (15290)10/26 08:52
反馈群:766236637\ntg群:https://t.me/+dcNnFFZv5KtmYjIx 具体有问题请加反馈,别的请看源注释
{
"bookSourceComment": "版本: 20261026\n作者: 知秋\n反馈群:766236637\ntg群:https:\/\/t.me\/+dcNnFFZv5KtmYjIx\n支持全站小说,以及全站漫画\n速率限制为1分钟80章\n注意:请牢记你的密码,用户名为设备码\n\n登录:阅读内「注册\/登录同步」登录成功后从 Cookie CYJapiToken 写入源变量。\nJWT 过期时请求会提示并自动打开登录页,关闭后重新同步。\n账号信息 \/ 上游绑定 \/ 改密请在 Web 控制台完成。\n\n搜索前缀:\n- 无前缀:小说\n- c: \/ c::漫画\n- g: \/ g::共享搜索(share=true);漫画共享用 c:g:关键词\n- 纯数字:书号 bid(当前类型)\n- a:\/v:\/p:其它类型(暂未开放)\n\n发现类型请用发现页自定义按钮切换;登录页可切男频\/女频与 API 地址。\n",
"bookSourceGroup": "cyj,api",
"bookSourceName": "✿次元姬(知秋)✿",
"bookSourceType": 0,
"bookSourceUrl": "https:\/\/cyj.vv9v.cn",
"concurrentRate": "1000",
"customButton": true,
"customOrder": 0,
"enabled": true,
"enabledCookieJar": true,
"enabledExplore": true,
"eventListener": true,
"exploreUrl": "@js:\nvar full = { layout_flexGrow: 1, layout_flexBasisPercent: 1 }\nvar half = { layout_flexGrow: 1, layout_flexBasisPercent: 0.45 }\nsyncExplorePrefsFromMap.call(this)\nfunction exploreTypeLabel() {\n var t = ''\n try { t = String(source.get('exploreType') || source.get('type') || 'all') } catch (e) { }\n if (t === 'novel') return '小说'\n if (t === 'comic') return '漫画'\n return '全部'\n}\nfunction genderLabel() {\n try {\n return String(source.get('gender') || '1') === '0' ? '女频' : '男频'\n } catch (e) {\n return '男频'\n }\n}\nfunction styleLabel() {\n try {\n return String(source.get('exploreStyle') || 'default') === 'compact' ? '紧凑' : '默认'\n } catch (e) {\n return '默认'\n }\n}\nvar compact = false\ntry { compact = String(source.get('exploreStyle') || '') === 'compact' } catch (eC) { }\nvar out = []\nout.push({\n title: '✿ 发现设置 ✿',\n type: 'button',\n action: \"java.toast('切换下拉后会自动刷新分类')\",\n style: full,\n})\nout.push({\n title: '发现类型',\n type: 'select',\n chars: ['全部', '小说', '漫画'],\n default: exploreTypeLabel(),\n action: \"applyExploreTypeFromMap(String(infoMap.get().get('发现类型')||''))\",\n style: half,\n})\nout.push({\n title: '小说频道',\n type: 'select',\n chars: ['男频', '女频'],\n default: genderLabel(),\n action: \"applyExploreGenderFromMap(String(infoMap.get().get('小说频道')||''))\",\n style: half,\n})\nout.push({\n title: '发现样式',\n type: 'select',\n chars: ['默认', '紧凑'],\n default: styleLabel(),\n action: \"applyExploreStyleFromMap(String(infoMap.get().get('发现样式')||''))\",\n style: half,\n})\nout.push({\n title: '立即刷新发现',\n type: 'button',\n action: 'refreshExploreNow()',\n style: half,\n})\nvar exploreType = String(source.get('exploreType') || source.get('type') || 'all')\nif (exploreType !== 'all' && exploreType !== 'novel' && exploreType !== 'comic') {\n exploreType = 'all'\n}\ntry { source.put('type', exploreType) } catch (e) { }\nvar gender = String(source.get('gender') || '1')\nvar data = requestApi.call(this, '\/explore\/init', {\n type: exploreType,\n gender: gender,\n}) || []\nvar dense = { layout_flexGrow: 1, layout_flexBasisPercent: 0.2 }\nvar normalPct = 0.25\nfor (var i = 0; i < data.length; i++) {\n var item = data[i]\n if (!item || item.title == null) continue\n var hasUrl = !!(item.url && String(item.url).trim())\n if (hasUrl) {\n var st = item.style || {}\n var pct = normalPct\n try {\n if (st.layout_flexBasisPercent != null) pct = Number(st.layout_flexBasisPercent)\n } catch (eP) { }\n if (compact) {\n out.push({\n title: item.title,\n url: item.url,\n style: dense,\n })\n } else {\n out.push({\n title: item.title,\n url: item.url,\n style: {\n layout_flexGrow: 1,\n layout_flexBasisPercent: pct > 0 && pct < 1 ? pct : normalPct,\n },\n })\n }\n } else {\n out.push(item)\n }\n}\nJSON.stringify(out)\n",
"jsLib": "var DEFAULT_API = 'https:\/\/cyj.vv9v.cn'\nvar COOKIE_TOKEN_KEY = 'CYJapiToken'\nfunction bookSourceOf(self) {\n return (self && self.source) || (typeof source !== 'undefined' ? source : null)\n}\nfunction javaOf(self) {\n return (self && self.java) || (typeof java !== 'undefined' ? java : null)\n}\nfunction getApiBase() {\n var bs = bookSourceOf(this)\n try {\n var h = String((bs && bs.get('apiHost')) || '').trim()\n if (h) return h.replace(\/\\\/$\/, '')\n } catch (e) { }\n try {\n var info = JSON.parse(String((bs && bs.getLoginInfo()) || '{}'))\n var fromLogin = String(info['API地址'] || info['apiHost'] || '').trim()\n if (fromLogin) return fromLogin.replace(\/\\\/$\/, '')\n } catch (e2) { }\n return DEFAULT_API\n}\nvar api = DEFAULT_API\nfunction refreshApi() {\n api = getApiBase.call(this)\n return api\n}\nfunction getJwt() {\n try {\n var bs = bookSourceOf(this)\n return String((bs && bs.getVariable()) || '').trim()\n } catch (e) {\n return ''\n }\n}\nfunction asBook(r) {\n if (r == null) return {}\n if (typeof r === 'string') {\n try { r = JSON.parse(r) } catch (e) { return {} }\n }\n try {\n if (r && r.data != null && typeof r.data === 'object' && !Array.isArray(r.data)\n && (r.code != null || r.msg != null)\n && r.bid == null && r.type == null && r.name == null) {\n r = r.data\n }\n } catch (e2) { }\n return r || {}\n}\nfunction loginMap(k) {\n var bs = bookSourceOf(this)\n if (!this._mapCache) {\n try { this._mapCache = bs ? bs.getLoginInfoMap() : null } catch (e) { this._mapCache = null }\n }\n var m = this._mapCache\n if (k == null) return m\n if (!m) {\n try {\n var info = JSON.parse(String((bs && bs.getLoginInfo()) || '{}'))\n var v = info[k]\n return v == null ? '' : String(v)\n } catch (e2) { return '' }\n }\n var v2 = m.get ? m.get(k) : m[k]\n return v2 == null ? '' : String(v2)\n}\nfunction headerGet(headers, name) {\n if (!headers) return ''\n var want = String(name || '').toLowerCase()\n try {\n if (typeof headers.get === 'function') {\n var a = headers.get(name)\n if (a) return String(a)\n var b = headers.get(want)\n if (b) return String(b)\n }\n } catch (e0) { }\n try {\n if (typeof headers.get === 'function') {\n var names = headers.names ? headers.names() : null\n if (names) {\n var it = names.iterator ? names.iterator() : null\n if (it) {\n while (it.hasNext()) {\n var n = String(it.next())\n if (n.toLowerCase() === want) {\n var hv = headers.get(n)\n if (hv) return String(hv)\n }\n }\n }\n }\n }\n } catch (e1) { }\n try {\n var keys = Object.keys(headers)\n for (var i = 0; i < keys.length; i++) {\n if (String(keys[i]).toLowerCase() === want) return String(headers[keys[i]] || '')\n }\n } catch (e2) { }\n return ''\n}\nfunction getApiUrl(path, data) {\n refreshApi.call(this)\n var token = getJwt.call(this)\n data = data || {}\n var qs = Object.entries(data)\n .filter(function (kv) { return kv[1] !== undefined && kv[1] !== null && kv[1] !== '' })\n .map(function (v) { return encodeURIComponent(v[0]) + '=' + encodeURIComponent(String(v[1])) })\n .join('&')\n var option = {\n method: 'GET',\n webView: false,\n headers: {\n Authorization: 'Bearer ' + token,\n },\n }\n return api + path + (qs ? ('?' + qs) : '') + ',' + JSON.stringify(option)\n}\nfunction requestApi(path, data) {\n var url = getApiUrl.call(this, path, data || {})\n var javaObj = javaOf(this)\n if (!javaObj) return null\n var requestId = '(none)'\n try {\n var resp = javaObj.connect(url)\n try {\n var hid = headerGet(resp.headers(), 'X-Request-Id')\n if (hid) requestId = hid\n } catch (eH) { }\n var httpCode = 0\n try { httpCode = Number(resp.code()) } catch (eC) { }\n var raw = String(resp.body() || '')\n if (httpCode === 401 || httpCode === 403) {\n javaObj.log('请求 ' + path + ' HTTP ' + httpCode + ' requestId=' + requestId)\n javaObj.longToast('\\n未登录或无权限 (HTTP ' + httpCode + ')\\n请先在登录页「登录同步」\\nrequestId: ' + requestId)\n return null\n }\n var res = {}\n try { res = JSON.parse(raw) } catch (eP) {\n javaObj.log('请求 ' + path + ' 非 JSON:' + raw.slice(0, 200))\n javaObj.longToast('\\n请求失败\\nrequestId: ' + requestId + '\\n响应不是 JSON')\n return null\n }\n if (res.requestId) requestId = String(res.requestId)\n var ok = (res.msg == 'success' || res.msg === 'success') && (res.code == null || Number(res.code) >= 0)\n if (!ok) {\n var msg = String(res.msg || 'error')\n javaObj.log('请求失败 ' + path + ' code=' + res.code + ' msg=' + msg + ' requestId=' + requestId)\n javaObj.longToast('\\n操作失败 code=' + res.code + '\\n' + msg + '\\nrequestId: ' + requestId)\n return null\n }\n if (res.data !== undefined) return res.data\n return res\n } catch (e) {\n try {\n javaObj.log('请求 ' + path + ' 异常:' + (e.message || e) + ' requestId=' + requestId)\n javaObj.longToast('\\n请求异常\\n' + (e.message || e) + '\\nrequestId: ' + requestId)\n } catch (e2) { }\n return null\n }\n}\nfunction deviceId() {\n var javaObj = javaOf(this)\n if (!javaObj) return ''\n try {\n return String(javaObj.androidId() || '').trim()\n } catch (e1) {\n return ''\n }\n}\nfunction isAuthFailResponse(resp) {\n if (!resp) return false\n var code = 0\n try { code = Number(resp.code()) } catch (e) { }\n if (code === 401 || code === 403) return true\n var body = ''\n try { body = String(resp.body() || '') } catch (e2) { }\n if (!body) return false\n if (\/无效token|token\\s*过期|未登录|unauthorized\/i.test(body)) return true\n try {\n var j = JSON.parse(body)\n if (!j) return false\n var msg = String(j.msg || '')\n var c = Number(j.code)\n if ((c < 0 || c === 401 || c === 403) && \/token|登录|鉴权|授权|未登录\/i.test(msg)) return true\n } catch (e3) { }\n return false\n}\nfunction handleAuthFailInLoginCheck(resp) {\n var javaObj = javaOf(this)\n var bs = bookSourceOf(this)\n if (!javaObj || !bs) return resp\n if (String(bs.get('_authChecking') || '') === '1') return resp\n var now = Date.now()\n var last = Number(bs.get('_authReloginAt') || 0)\n if (last && now - last < 45000) {\n try { javaObj.toast('登录已过期,请重新登录同步') } catch (eT) { }\n return resp\n }\n try { bs.put('_authReloginAt', String(now)) } catch (eP) { }\n try { bs.setVariable('') } catch (e0) { }\n try {\n javaObj.longToast('\\n登录已过期或无效\\n即将打开登录页,完成后请关闭页面以同步 JWT')\n } catch (e1) { }\n try { bs.put('_authChecking', '1') } catch (eC) { }\n try {\n var id = deviceId.call(this)\n var q = id ? ('?id=' + encodeURIComponent(id)) : ''\n openConsole.call(this, '\/login' + q, true)\n var token = getJwt.call(this)\n if (!token) return resp\n try {\n javaObj.getHeaderMap().put('Authorization', 'Bearer ' + token)\n } catch (eH) { }\n try {\n if (javaObj.getStrResponse) return javaObj.getStrResponse()\n } catch (eR) {\n try { javaObj.log('auth retry: ' + eR) } catch (eL) { }\n }\n return resp\n } finally {\n try { bs.put('_authChecking', '0') } catch (eF) { }\n }\n}\nfunction openConsole(path, awaitCookie) {\n refreshApi.call(this)\n var javaObj = javaOf(this)\n var bs = bookSourceOf(this)\n if (!javaObj || !bs) return\n var base = api + '\/console'\n var p = String(path || '\/')\n if (p.charAt(0) !== '\/') p = '\/' + p\n var url = base + p\n if (awaitCookie) {\n javaObj.startBrowserAwait(url, '次元姬登录')\n var token = ''\n try {\n token = String(cookie.getKey(api, COOKIE_TOKEN_KEY) || '').trim()\n } catch (e0) {\n try { token = String(javaObj.getCookie(api, COOKIE_TOKEN_KEY) || '').trim() } catch (e1) { }\n }\n if (!token) {\n javaObj.longToast('\\n未检测到登录 Cookie(' + COOKIE_TOKEN_KEY + ')\\n请确认在阅读内置浏览器完成登录后关闭页面')\n return\n }\n bs.setVariable(token)\n javaObj.longToast('\\n✅登录同步成功,JWT 已写入源变量')\n } else {\n javaObj.startBrowser(url, '次元姬控制台')\n }\n}\nfunction parseSearchKey(key) {\n var keyword = String(key || '').trim()\n var type = 'novel'\n var mode = 'novel'\n var share = false\n var unavailable = false\n function peel(re, t, m) {\n if (re.test(keyword)) {\n keyword = keyword.replace(re, '').trim()\n if (t) type = t\n if (m) mode = m\n return true\n }\n return false\n }\n peel(\/^c(:|:)\/i, 'comic', 'comic')\n || peel(\/^a(:|:)\/i, 'audio', 'audio')\n || peel(\/^v(:|:)\/i, 'video', 'video')\n || peel(\/^p(:|:)\/i, 'publication', 'publication')\n if (\/^g(:|:)\/i.test(keyword)) {\n keyword = keyword.replace(\/^g(:|:)\/i, '').trim()\n share = true\n }\n if (type !== 'novel' && type !== 'comic') {\n unavailable = true\n }\n return { keyword: keyword, type: type, mode: mode, share: share, unavailable: unavailable }\n}\nfunction resolveBookId(bId) {\n if (bId && String(bId).match(\/^\\d+$\/)) return String(bId)\n try {\n if (typeof book !== 'undefined' && book && book.bookUrl) {\n var m = String(book.bookUrl).match(\/[?&]bid=(\\d+)\/)\n if (m) return m[1]\n }\n } catch (e0) { }\n try {\n var bs = bookSourceOf(this)\n var t = bs && bs.get('tempBookId')\n if (t) return String(t)\n } catch (e1) { }\n return String(bId || '')\n}\nfunction refreshExploreNow() {\n var javaObj = javaOf(this)\n var bs = bookSourceOf(this)\n try {\n if (javaObj && javaObj.refreshExplore) javaObj.refreshExplore()\n } catch (e1) {\n try { javaObj && javaObj.log('java.refreshExplore: ' + e1) } catch (eL) { }\n }\n try {\n if (bs && bs.refreshExplore) bs.refreshExplore()\n } catch (e2) {\n try { javaObj && javaObj.log('source.refreshExplore: ' + e2) } catch (eL2) { }\n }\n}\nfunction mapGet(key) {\n var k = String(key || '')\n function ok(v) {\n if (v == null) return false\n if (typeof v === 'object') return false\n var s = String(v)\n return s !== '' && s.indexOf('{') !== 0 && s !== '[object Object]'\n }\n try {\n if (typeof infoMap !== 'undefined' && infoMap != null) {\n var v = null\n try {\n var whole = infoMap.get()\n if (whole != null) {\n if (typeof whole.get === 'function') v = whole.get(k)\n else v = whole[k]\n }\n } catch (e0) { v = null }\n if (ok(v)) return String(v)\n }\n } catch (e) { }\n try {\n if (typeof result !== 'undefined' && result != null) {\n var rv = null\n try {\n if (result.get && typeof result.get === 'function') rv = result.get(k)\n } catch (eG) { rv = null }\n if (ok(rv)) return String(rv)\n try {\n rv = result[k]\n if (ok(rv)) return String(rv)\n } catch (eB) { }\n }\n } catch (e2) { }\n return ''\n}\nfunction pickLabel(arg, key, allowed) {\n var v = (arg != null) ? String(arg) : ''\n if (v && allowed.indexOf(v) >= 0) return v\n v = mapGet(key)\n if (v && allowed.indexOf(v) >= 0) return v\n return ''\n}\nfunction setGender(g) {\n var bs = bookSourceOf(this)\n if (bs) bs.put('gender', String(g))\n refreshExploreNow.call(this)\n var javaObj = javaOf(this)\n if (javaObj) javaObj.longToast('\\n✅发现页频道:' + (String(g) === '0' ? '女频' : '男频'))\n}\nfunction setOrder(order, desc) {\n var bs = bookSourceOf(this)\n if (bs) bs.put('order', String(order))\n var javaObj = javaOf(this)\n if (javaObj) javaObj.longToast('\\n✅分类排序:' + desc + '\\n打开分类书单时生效')\n}\nfunction setSearchRank(rank, desc) {\n var bs = bookSourceOf(this)\n if (bs) bs.put('searchRank', String(rank))\n var javaObj = javaOf(this)\n if (javaObj) javaObj.longToast('\\n✅搜索排序:' + desc)\n}\nfunction setExploreType(t) {\n var bs = bookSourceOf(this)\n if (bs) {\n bs.put('exploreType', t)\n bs.put('type', t)\n }\n var label = t\n if (t == 'all') label = '全部'\n else if (t == 'novel') label = '小说'\n else if (t == 'comic') label = '漫画'\n refreshExploreNow.call(this)\n var javaObj = javaOf(this)\n if (javaObj) javaObj.longToast('\\n✅发现类型:' + label)\n}\nfunction setExploreStyle(style) {\n var bs = bookSourceOf(this)\n if (bs) bs.put('exploreStyle', String(style || 'default'))\n refreshExploreNow.call(this)\n var javaObj = javaOf(this)\n if (javaObj) javaObj.longToast('\\n✅发现样式:' + (style === 'compact' ? '紧凑(分类更密)' : '默认'))\n}\nfunction applyExploreTypeFromMap(arg) {\n var v = pickLabel(arg, '发现类型', ['全部', '小说', '漫画'])\n var t = 'all'\n if (v === '小说') t = 'novel'\n else if (v === '漫画') t = 'comic'\n setExploreType.call(this, t)\n}\nfunction applyExploreGenderFromMap(arg) {\n var v = pickLabel(arg, '小说频道', ['男频', '女频'])\n setGender.call(this, v === '女频' ? '0' : '1')\n}\nfunction applyExploreStyleFromMap(arg) {\n var v = pickLabel(arg, '发现样式', ['默认', '紧凑'])\n setExploreStyle.call(this, v === '紧凑' ? 'compact' : 'default')\n}\nfunction applyOrderFromMap(arg) {\n var allowed = ['人气最高', '订阅最多', '最近更新', '最近上架', '最近新书']\n var v = pickLabel(arg, '分类排序', allowed)\n var map = {\n '人气最高': '1',\n '订阅最多': '2',\n '最近更新': '3',\n '最近上架': '4',\n '最近新书': '6',\n }\n setOrder.call(this, map[v] || '1', v || '人气最高')\n}\nfunction applySearchRankFromMap(arg) {\n var v = pickLabel(arg, '搜索排序', ['按推荐', '按人气', '按销量', '按更新'])\n var map = {\n '按推荐': '0',\n '按人气': '1',\n '按销量': '2',\n '按更新': '3',\n }\n setSearchRank.call(this, map[v] || '0', v || '按推荐')\n}\nfunction applyForceRefreshFromMap(arg) {\n var v = pickLabel(arg, '强制刷新章节', ['开', '关'])\n var on = (v === '开')\n var bs = bookSourceOf(this)\n if (bs) bs.put('强制刷新章节', on ? '1' : '0')\n var javaObj = javaOf(this)\n if (javaObj) {\n javaObj.longToast(on\n ? '\\n✅章节强制刷新已开启\\n无权限时服务端会忽略'\n : '\\n✅章节强制刷新已关闭')\n }\n}\nfunction syncExplorePrefsFromMap() {\n var bs = bookSourceOf(this)\n if (!bs) return\n try {\n if (typeof infoMap === 'undefined' || infoMap == null) return\n var tl = mapGet('发现类型')\n if (tl === '小说') { bs.put('exploreType', 'novel'); bs.put('type', 'novel') }\n else if (tl === '漫画') { bs.put('exploreType', 'comic'); bs.put('type', 'comic') }\n else if (tl === '全部') { bs.put('exploreType', 'all'); bs.put('type', 'all') }\n var gl = mapGet('小说频道')\n if (gl === '女频') bs.put('gender', '0')\n else if (gl === '男频') bs.put('gender', '1')\n var sl = mapGet('发现样式')\n if (sl === '紧凑') bs.put('exploreStyle', 'compact')\n else if (sl === '默认') bs.put('exploreStyle', 'default')\n } catch (e) { }\n}\n",
"lastUpdateTime": "1792975970938",
"loginCheckJs": "; (function (a) {\n try {\n if (typeof isAuthFailResponse === 'function' && isAuthFailResponse(a)) {\n return handleAuthFailInLoginCheck(a)\n }\n } catch (e) {\n try { java.log('loginCheckJs: ' + e) } catch (e2) { }\n }\n return a\n})(result)\n",
"loginUi": "@js:\nvar fullStyle = { layout_flexGrow: 1, layout_flexBasisPercent: 1 }\nvar halfStyle = { layout_flexGrow: 1, layout_flexBasisPercent: 0.45 }\nvar thirdStyle = { layout_flexGrow: 1, layout_flexBasisPercent: 0.28 }\nfunction btn(name, action, style) {\n return {\n name: name,\n type: 'button',\n action: action || '',\n style: style || halfStyle,\n }\n}\nfunction hdr(title) {\n return btn('✿ ' + title + ' ✿', \"java.toast('\" + title + \"')\", fullStyle)\n}\nfunction exploreTypeLabel() {\n var t = ''\n try { t = String(source.get('exploreType') || source.get('type') || 'all') } catch (e) { }\n if (t === 'novel') return '小说'\n if (t === 'comic') return '漫画'\n return '全部'\n}\nfunction genderLabel() {\n try {\n return String(source.get('gender') || '1') === '0' ? '女频' : '男频'\n } catch (e) {\n return '男频'\n }\n}\nfunction orderLabel() {\n var o = ''\n try { o = String(source.get('order') || '1') } catch (e) { }\n var map = {\n '1': '人气最高',\n '2': '订阅最多',\n '3': '最近更新',\n '4': '最近上架',\n '6': '最近新书',\n }\n return map[o] || '人气最高'\n}\nfunction searchRankLabel() {\n var o = ''\n try { o = String(source.get('searchRank') || '0') } catch (e) { }\n var map = {\n '0': '按推荐',\n '1': '按人气',\n '2': '按销量',\n '3': '按更新',\n }\n return map[o] || '按推荐'\n}\nfunction styleLabel() {\n try {\n return String(source.get('exploreStyle') || 'default') === 'compact' ? '紧凑' : '默认'\n } catch (e) {\n return '默认'\n }\n}\nfunction forceRefreshDefault() {\n try {\n return String(source.get('强制刷新章节') || '0') === '1' ? '开' : '关'\n } catch (e) {\n return '关'\n }\n}\nfunction jwtEmoji() {\n try {\n return String(source.getVariable() || '').trim() ? '✅' : '❌'\n } catch (e) {\n return '❌'\n }\n}\nvar apiDefault = ''\ntry {\n apiDefault = String(source.get('apiHost') || '')\n} catch (e0) { }\nif (!apiDefault) {\n try {\n var info = JSON.parse(String(source.getLoginInfo() || '{}'))\n apiDefault = String(info['API地址'] || '')\n } catch (e1) { }\n}\nvar main = [\n hdr('账号'),\n btn('注册同步', 'syncRegister()', halfStyle),\n btn('登录同步', 'syncLogin()', halfStyle),\n btn('账号信息', 'openAccount()', thirdStyle),\n btn('上游绑定', 'openUpstream()', thirdStyle),\n btn('修改密码', 'openPassword()', thirdStyle),\n btn('查询本地 JWT', 'queryJwt()', fullStyle),\n hdr('发现设置'),\n {\n name: '发现类型',\n type: 'select',\n chars: ['全部', '小说', '漫画'],\n default: exploreTypeLabel(),\n action: \"applyExploreTypeFromMap(String((result.get&&result.get('发现类型'))||''))\",\n style: halfStyle,\n },\n {\n name: '小说频道',\n type: 'select',\n chars: ['男频', '女频'],\n default: genderLabel(),\n action: \"applyExploreGenderFromMap(String((result.get&&result.get('小说频道'))||''))\",\n style: halfStyle,\n },\n {\n name: '发现样式',\n type: 'select',\n chars: ['默认', '紧凑'],\n default: styleLabel(),\n action: \"applyExploreStyleFromMap(String((result.get&&result.get('发现样式'))||''))\",\n style: halfStyle,\n },\n {\n name: '分类排序',\n type: 'select',\n chars: ['人气最高', '订阅最多', '最近更新', '最近上架', '最近新书'],\n default: orderLabel(),\n action: \"applyOrderFromMap(String((result.get&&result.get('分类排序'))||''))\",\n style: halfStyle,\n },\n {\n name: '搜索排序',\n type: 'select',\n chars: ['按推荐', '按人气', '按销量', '按更新'],\n default: searchRankLabel(),\n action: \"applySearchRankFromMap(String((result.get&&result.get('搜索排序'))||''))\",\n style: halfStyle,\n },\n hdr('调试'),\n { name: 'API地址', type: 'text', default: apiDefault, style: fullStyle },\n btn('保存 API 地址', 'saveApiHost()', fullStyle),\n { name: '手动JWT', type: 'text', default: '', style: fullStyle },\n btn('保存手动 JWT', 'saveManualJwt()', fullStyle),\n hdr('下载 \/ 购买'),\n {\n name: '强制刷新章节',\n type: 'toggle',\n chars: ['关', '开'],\n default: forceRefreshDefault(),\n action: \"applyForceRefreshFromMap(String((result.get&&result.get('强制刷新章节'))||''))\",\n style: fullStyle,\n },\n { name: '书籍ID', type: 'text', style: fullStyle },\n btn('全本后台下载', 'downloadNovel()', fullStyle),\n { name: '购买数量', type: 'text', default: '5', style: fullStyle },\n btn('购买全本', 'buy(true)', halfStyle),\n btn('购买指定数量', 'buy(false)', halfStyle),\n]\nresult = JSON.stringify(main)\n",
"loginUrl": "function login() { }\nfunction syncLogin() {\n refreshApi.call(this)\n var id = deviceId.call(this)\n var q = id ? ('?id=' + encodeURIComponent(id)) : ''\n openConsole.call(this, '\/login' + q, true)\n}\nfunction syncRegister() {\n refreshApi.call(this)\n var id = deviceId.call(this)\n if (!id) {\n java.longToast('\\n无法获取设备 id(oaid\/androidId),仍打开注册页')\n }\n var q = id ? ('?id=' + encodeURIComponent(id)) : ''\n openConsole.call(this, '\/register' + q, true)\n}\nfunction openAccount() {\n openConsole.call(this, '\/', false)\n}\nfunction openUpstream() {\n openConsole.call(this, '\/upstream', false)\n}\nfunction openPassword() {\n openConsole.call(this, '\/password', false)\n}\nfunction saveApiHost() {\n var host = String((result && (result['API地址'] || result.get && result.get('API地址'))) || '').trim().replace(\/\\\/$\/, '')\n if (host && !\/^https?:\\\/\\\/\/i.test(host)) {\n return java.longToast('\\n❌API地址须以 http:\/\/ 或 https:\/\/ 开头')\n }\n source.put('apiHost', host)\n try {\n var info = JSON.parse(String(source.getLoginInfo() || '{}'))\n info['API地址'] = host\n source.putLoginInfo(JSON.stringify(info))\n } catch (e) { }\n try {\n var map = new Packages.java.util.HashMap()\n map.put('API地址', host)\n java.upLoginData(map)\n } catch (e2) { }\n java.longToast(host ? ('\\n✅API地址已保存:' + host) : '\\n✅已恢复默认 API 地址')\n}\nfunction saveManualJwt() {\n var token = String((result && (result['手动JWT'] || result.get && result.get('手动JWT'))) || '').trim()\n if (!token) {\n return java.longToast('\\n❌请粘贴 JWT')\n }\n source.setVariable(token)\n java.longToast('\\n✅JWT 已写入源变量')\n}\nfunction switchForceChapRefresh() {\n var old = source.get('强制刷新章节') || '0'\n var on = old != '1'\n source.put('强制刷新章节', on ? '1' : '0')\n java.longToast(on\n ? '\\n✅章节强制刷新已开启\\n无权限时服务端会忽略'\n : '\\n✅章节强制刷新已关闭')\n}\nfunction downloadNovel() {\n var bid = String((result && (result['书籍ID'] || result.get && result.get('书籍ID'))) || '').trim()\n if (!\/^\\d+$\/.test(bid)) {\n return java.toast('请输入有效书籍ID')\n }\n var data = requestApi.call(this, '\/novel\/download', { bid: bid })\n if (!data) return\n java.longToast('\\n✅全本下载已提交')\n}\nfunction buy(all) {\n var bid = String((result && (result['书籍ID'] || result.get && result.get('书籍ID'))) || '').trim()\n if (!\/^\\d+$\/.test(bid)) {\n return java.longToast('\\n❌请输入有效书籍ID')\n }\n var data\n if (all) {\n data = requestApi.call(this, '\/novel\/buy_novel', { bid: bid })\n } else {\n var num = Number(String((result && (result['购买数量'] || result.get && result.get('购买数量'))) || '5'))\n if (!num || num <= 0) num = 5\n data = requestApi.call(this, '\/novel\/buy_chapter', { bid: bid, num: num })\n }\n if (!data) return\n java.longToast('\\n✅购买完成')\n}\nfunction queryJwt() {\n var token = getJwt.call(this)\n if (!token) {\n return java.longToast('\\n❌尚未登录,请先「注册同步」或「登录同步」')\n }\n java.longToast('\\n✅源变量已有 JWT(长度 ' + token.length + ')\\n账号\/上游请到 Web 控制台查看')\n}\n",
"respondTime": 180000,
"ruleBookInfo": {
"author": "@js:\nvar r=asBook(result); r.author || ''",
"coverUrl": "@js:\nvar r=asBook(result); r.cover || ''",
"init": "@js:\nvar d = result\ntry { if (typeof d === 'string') d = JSON.parse(d) } catch (e) { d = {} }\ntry {\n if (d && d.data != null && typeof d.data === 'object' && !Array.isArray(d.data)\n && (d.code != null || d.msg != null) && d.bid == null && d.type == null) {\n d = d.data\n }\n} catch (eWrap) { }\nvar bid = ''\ntry { bid = String((d && d.bid) || (d && d.id) || '') } catch (e0) { }\nif (!bid) {\n try {\n var m = String(baseUrl || '').match(\/[?&]bid=(\\d+)\/)\n if (m) bid = m[1]\n } catch (e1) { }\n}\nvar t = ''\ntry { t = String((d && d.type) || '') } catch (eType) { t = '' }\nif (!t) {\n try {\n var mBs = String(baseUrl || '').match(\/[?&]_bsType=([^&,]+)\/)\n if (mBs && mBs[1]) t = decodeURIComponent(mBs[1])\n } catch (eBs) { }\n}\nif (!t) {\n try { t = String(source.get('searchMode') || 'novel') } catch (eSm) { t = 'novel' }\n}\nif (!t) t = 'novel'\nif (d && typeof d === 'object') {\n if (!d.bid && bid) d.bid = bid\n if (!d.id && bid) d.id = bid\n d.type = t === 'tts' ? 'novel' : t\n d.mode = t\n}\ntry { if (bid) source.put('tempBookId', String(bid)) } catch (e3) { }\nJSON.stringify(d || {})\n",
"intro": "@js:\nvar rData = {}\ntry {\n rData = (typeof asBook === 'function') ? asBook(result) : (\n (typeof result === 'string') ? JSON.parse(result) : (result || {})\n )\n} catch (e) { rData = {} }\nfunction esc(s) {\n s = (s == null) ? '' : String(s)\n return s.replace(\/&\/g, '&').replace(\/<\/g, '<').replace(\/>\/g, '>').replace(\/\"\/g, '"')\n}\nfunction firstAuthorChar(name) {\n var s = String(name || '')\n var stripped = s.replace(\/^[^\\u4e00-\\u9fffA-Za-z]+\/, '')\n if (!stripped) stripped = s.trim()\n return stripped ? stripped.charAt(0) : '作'\n}\nfunction pickAccent() {\n var hues = [12, 28, 152, 168, 188, 205, 222, 248, 332]\n var h = hues[Math.floor(Math.random() * hues.length)]\n var s = 44 + Math.floor(Math.random() * 14)\n var l = 34 + Math.floor(Math.random() * 8)\n var c1 = 'hsl(' + h + ' ' + s + '% ' + (l - 7) + '%)'\n var c2 = 'hsl(' + h + ' ' + (s + 6) + '% ' + l + '%)'\n var c3 = 'hsl(' + h + ' ' + s + '% ' + (l - 4) + '%)'\n return {\n heroBg: 'linear-gradient(145deg, ' + c1 + ' 0%, ' + c2 + ' 45%, ' + c3 + ' 100%)',\n accent: 'hsl(' + h + ' ' + (s + 4) + '% ' + l + '%)',\n accentSoft: 'hsla(' + h + ' ' + (s + 4) + '% ' + l + '% \/ 0.16)',\n shadow: 'hsla(' + h + ' ' + s + '% ' + l + '% \/ 0.3)',\n }\n}\nvar authorName = esc(rData.author || '')\nvar authorInitial = esc(firstAuthorChar(rData.author || ''))\nvar bookName = esc(rData.name || '本书')\nvar introDesc = String(rData.desc || '').trim()\nvar introDescHtml = esc(introDesc)\nvar accent = pickAccent()\nvar status = esc(rData.status || '')\nvar category = [rData.category, rData.subCategory].filter(Boolean).map(esc).join(' · ')\nvar html = `\n<useweb>\n<style>\n:root {\n --ink: #1c1917;\n --mute: #78716c;\n --line: #e7e5e4;\n --paper: #fafaf9;\n --panel: #ffffff;\n --accent: ${accent.accent};\n --accent-soft: ${accent.accentSoft};\n --radius: 14px;\n --font: \"Segoe UI\", \"PingFang SC\", \"Noto Sans SC\", system-ui, sans-serif;\n}\n* { box-sizing: border-box; }\nbody { margin: 0; font-family: var(--font); color: var(--ink); background: transparent; }\n.wrap {\n width: min(520px, 94vw);\n margin: 10px auto 18px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n.hero {\n background: ${accent.heroBg};\n color: #f8fafc;\n border-radius: var(--radius);\n padding: 16px;\n display: flex;\n gap: 12px;\n align-items: center;\n box-shadow: 0 10px 28px ${accent.shadow};\n}\n.av.ph {\n width: 52px; height: 52px; border-radius: 50%;\n flex-shrink: 0;\n border: 2px solid rgba(255,255,255,.35);\n background: rgba(255,255,255,.15);\n display: flex; align-items: center; justify-content: center;\n font-size: 1.2rem; font-weight: 750; line-height: 1;\n color: #f8fafc;\n}\n.h-main { min-width: 0; flex: 1; }\n.h-name { font-size: 1.05rem; font-weight: 750; }\n.h-book { margin-top: 6px; font-size: 0.72rem; opacity: .7; }\n.h-meta { margin-top: 4px; font-size: 0.72rem; opacity: .75; }\n.panel {\n background: var(--panel);\n border: 1px solid var(--line);\n border-radius: var(--radius);\n padding: 14px 14px 12px;\n}\n.panel-h { margin-bottom: 10px; }\n.panel-h h2 { margin: 0; font-size: 0.95rem; font-weight: 750; }\n.dossier {\n margin: 0;\n white-space: pre-wrap;\n word-break: break-word;\n font-size: 0.82rem;\n line-height: 1.65;\n color: #292524;\n background: var(--paper);\n border-radius: 10px;\n padding: 12px;\n border-left: 3px solid var(--accent);\n}\n<\/style>\n<div class=\"wrap\">\n <section class=\"hero\">\n <span class=\"av ph\">${authorInitial}<\/span>\n <div class=\"h-main\">\n <div class=\"h-name\"><span>${authorName || '作者'}<\/span><\/div>\n <div class=\"h-book\">${bookName}<\/div>\n ${status || category ? `<div class=\"h-meta\">${[status, category].filter(Boolean).join(' · ')}<\/div>` : ''}\n <\/div>\n <\/section>\n <section class=\"panel\">\n <div class=\"panel-h\"><h2>本书档案<\/h2><\/div>\n <pre class=\"dossier\">${introDescHtml || '暂无简介'}<\/pre>\n <\/section>\n<\/div>\n<\/useweb>\n`\nhtml\n",
"kind": "@js:\nvar r=asBook(result);\n[r.status, r.category, r.subCategory, r.tag].filter(Boolean).join(',')\n",
"lastChapter": "@js:\nvar r=asBook(result); r.lastChapter || ''",
"name": "@js:\nvar r=asBook(result); r.name || ''",
"tocUrl": "@js:\nfunction pickStr() {\n for (var i = 0; i < arguments.length; i++) {\n var v = arguments[i]\n if (v == null) continue\n try { v = String(v).trim() } catch (e) { continue }\n if (v && v !== 'null' && v !== 'undefined') return v\n }\n return ''\n}\nvar bid = pickStr(\n java.getString('bid'),\n java.getString('id'),\n (typeof result === 'object' && result && result.bid),\n (typeof result === 'object' && result && result.id),\n source.get('tempBookId')\n)\nif (!bid) {\n try {\n var mBid = String(baseUrl || '').match(\/[?&]bid=(\\d+)\/)\n if (mBid) bid = mBid[1]\n } catch (e0) { }\n}\nvar t = pickStr(\n java.getString('mode'),\n java.getString('type'),\n (typeof result === 'object' && result && result.mode),\n (typeof result === 'object' && result && result.type),\n 'novel'\n)\nif (!bid) {\n try { java.toast('无法解析书籍ID,无法加载目录') } catch (e3) { }\n}\ntry { source.put('tempBookId', String(bid)) } catch (e4) { }\n'data:;base64,' + java.base64Encode(String(bid)) + ',{\"type\":\"' + (t || 'novel') + '\",\"bid\":\"' + bid + '\"}'\n",
"wordCount": "@js:\nvar r=asBook(result); r.charCount || ''"
},
"ruleContent": {
"callBackJs": "if (event == 'clickCustomButton' || event == 'longClickCustomButton') {\n try { source.put('menuTag', 'main') } catch (e) { }\n java.open('login')\n true\n} else {\n false\n}\n",
"content": "@js:\nvar cid = java.hexDecodeToString(result)\nvar meta = JSON.parse(baseUrl.slice(baseUrl.indexOf('{')))\nvar type = meta.type || 'novel'\nvar bid = meta.bid\nif (type == 'novel' || type == 'tts') {\n var chapParams = { bid: bid, cid: cid }\n try {\n if (String(source.get('强制刷新章节') || '') == '1') chapParams.forceRefresh = '1'\n } catch (eFr) { }\n var data = requestApi.call(this, '\/novel\/chap', chapParams)\n if (!data || data.content == null) {\n ''\n } else {\n String(data.content)\n }\n} else if (type == 'comic') {\n var comicParams = { bid: bid, cid: cid }\n try {\n if (String(source.get('强制刷新章节') || '') == '1') comicParams.forceRefresh = '1'\n } catch (eFr2) { }\n var comicData = requestApi.call(this, '\/comic\/chap', comicParams)\n var pics = comicData && comicData.pics\n if (!pics || !pics.length) {\n ''\n } else {\n var html = ''\n for (var i = 0; i < pics.length; i++) {\n var pic = pics[i]\n var imgSrc = ''\n if (typeof pic === 'string') {\n imgSrc = pic\n } else if (pic) {\n imgSrc = String(pic.imgUrl || pic.url || '')\n }\n if (imgSrc) html += '<img src=\"' + imgSrc + '\">'\n }\n html\n }\n} else {\n java.longToast('\\n暂不支持正文类型:' + type)\n ''\n}\n",
"imageStyle": "FULL",
"payAction": ";(function () {\n try {\n var type = ''\n var mType = String(chapter.url).match(\/\"type\"\\s*:\\s*\"([^\"]+)\"\/)\n if (mType) type = mType[1]\n if (type != 'novel' && type != 'comic') {\n java.toast('\\n当前类型不支持章节购买:' + (type || 'unknown'))\n return false\n }\n var bid = ''\n var mBid = String(chapter.url).match(\/\"bid\"\\s*:\\s*\"(\\d+)\"\/)\n if (mBid) bid = mBid[1]\n if (!bid) {\n var mBook = String(book.bookUrl || '').match(\/bid=(\\d+)\/)\n if (mBook) bid = mBook[1]\n }\n var chapId = ''\n var mB64 = String(chapter.url).match(\/base64,([^,]+)\/)\n if (mB64) chapId = java.base64Decode(mB64[1])\n if (!bid || !chapId) {\n java.toast('\\n无法解析书籍\/章节 ID')\n return false\n }\n var path = type === 'comic' ? '\/comic\/buy_chapter' : '\/novel\/buy_chapter'\n var data = requestApi.call(this, path, {\n bid: bid,\n cid: chapId,\n })\n if (data) {\n java.longToast('\\n✅购买成功,请刷新章节')\n return true\n }\n return false\n } catch (e) {\n java.log('payAction: ' + e)\n return false\n }\n}).call(this)\n"
},
"ruleExplore": {
"author": "author",
"bookList": "$.data.books[*]",
"bookUrl": "@js:\nvar t = java.getString('type') || 'novel'\nvar bid = java.getString('bid') || java.getString('id')\nvar searchMode = ''\ntry { searchMode = String(source.get('searchMode') || '').trim() } catch (e) { }\nif (!t || t === 'null') t = 'novel'\nvar infoType = t\nvar params = { bid: bid }\nif (searchMode && searchMode !== 'novel') params._bsType = searchMode\nelse if (t && t !== 'novel') params._bsType = t\ngetApiUrl('\/' + infoType + '\/info', params)\n",
"coverUrl": "cover",
"intro": "desc",
"kind": "{{$.status}}\n{{$.category}}\n{{$.subCategory}}\n",
"lastChapter": "lastChapter",
"name": "name",
"wordCount": "charCount"
},
"ruleSearch": {
"author": "author",
"bookList": "$.data[*]",
"bookUrl": "@js:\nvar t = java.getString('type') || 'novel'\nvar bid = java.getString('bid') || java.getString('id')\nvar searchMode = ''\ntry { searchMode = String(source.get('searchMode') || '').trim() } catch (e) { }\nif (!t || t === 'null') t = 'novel'\nvar infoType = t\nvar params = { bid: bid }\nif (searchMode && searchMode !== 'novel') params._bsType = searchMode\nelse if (t && t !== 'novel') params._bsType = t\ngetApiUrl('\/' + infoType + '\/info', params)\n",
"checkKeyWord": "诡秘之主",
"coverUrl": "cover",
"intro": "desc",
"kind": "{{$.status}}\n{{$.category}}\n{{$.subCategory}}\n",
"lastChapter": "lastChapter",
"name": "name",
"wordCount": "charCount"
},
"ruleToc": {
"chapterList": "@js:\nvar bid = ''\ntry { bid = String(java.hexDecodeToString(result) || '').trim() } catch (e0) { bid = '' }\nif (!\/^\\d+$\/.test(bid)) {\n try {\n var raw = String(result == null ? '' : result).trim()\n if (\/^\\d+$\/.test(raw)) bid = raw\n } catch (e1) { }\n}\nvar meta = {}\ntry {\n meta = JSON.parse(baseUrl.slice(baseUrl.indexOf('{')))\n} catch (e2) { meta = {} }\nvar type = String(meta.type || '').trim()\nif (!type || type === 'null' || type === 'undefined') type = 'novel'\ntry {\n var metaBid = String(meta.bid || '').trim()\n if (\/^\\d+$\/.test(metaBid)) bid = metaBid\n} catch (e3) { }\nif (!bid) {\n try { bid = String(source.get('tempBookId') || '').trim() } catch (e4) { }\n}\nvar apiType = (type == 'tts') ? 'novel' : type\nif (!apiType) apiType = 'novel'\nvar chaps = []\nif (!bid) {\n java.toast('无法解析书籍ID,获取目录失败')\n} else if (apiType !== 'novel' && apiType !== 'comic') {\n java.longToast('\\n暂未开放目录类型:' + apiType)\n} else {\n book.type = apiType === 'comic' ? 64 : 8\n try {\n var loginInfo = JSON.parse(source.getLoginInfo() || '{}')\n loginInfo.书籍ID = bid\n source.putLoginInfo(JSON.stringify(loginInfo))\n } catch (eLi) { }\n var catalog = requestApi.call(this, '\/' + apiType + '\/catalog', { bid: bid }) || []\n var sharedRows = requestApi.call(this, '\/' + apiType + '\/cache', { bid: bid }) || []\n var shareMap = {}\n for (var ci = 0; ci < sharedRows.length; ci++) {\n var d = sharedRows[ci]\n var id = d.cid != null ? d.cid : d.chapId\n if (id != null) shareMap[String(id)] = d\n }\n function fmtTime(timestamp) {\n var date = new Date(timestamp)\n var year = date.getFullYear()\n var month = String(date.getMonth() + 1).padStart(2, '0')\n var day = String(date.getDate()).padStart(2, '0')\n var hours = String(date.getHours()).padStart(2, '0')\n var minutes = String(date.getMinutes()).padStart(2, '0')\n return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes\n }\n for (var vi = 0; vi < catalog.length; vi++) {\n var volume = catalog[vi]\n chaps.push({\n name: volume.volumeName,\n voltype: true,\n })\n var chapters = volume.chapters || []\n for (var chi = 0; chi < chapters.length; chi++) {\n var chapRow = chapters[chi]\n var chapId = chapRow.cid\n var chapName = chapRow.chapName\n var count = apiType === 'comic' ? (chapRow.picCount || 0) : (chapRow.charCount || 0)\n var unit = apiType === 'comic' ? '页' : '字'\n var isVip = chapRow.isVip\n var isBuy = chapRow.isBuy\n var displayTime = chapRow.displayTime\n var isShare = !!shareMap[String(chapId)]\n var url = 'data:;base64,' + java.base64Encode(String(chapId)) + ',{\"type\":\"' + type + '\",\"bid\":\"' + bid + '\"}'\n chaps.push({\n id: chapId,\n name: (isVip ? (isBuy ? ' 🔑 ' : isShare ? ' 🍎 ' : '') : '') + chapName,\n url: url,\n info: fmtTime(displayTime) + ' | ' + (count + unit) + (isShare ? ' 🍎 ' : isBuy ? ' 🔑 ' : ''),\n isVip: !isShare && isVip,\n isBuy: isBuy,\n })\n }\n }\n}\nchaps\n",
"chapterName": "name",
"chapterUrl": "url",
"isPay": "isBuy",
"isVip": "isVip",
"isVolume": "voltype",
"updateTime": "info"
},
"searchUrl": "@js:\nvar parsed = parseSearchKey.call(this, key)\ntry { source.put('searchMode', parsed.mode) } catch (e) { }\nif (parsed.unavailable) {\n java.longToast('\\n暂未开放:' + parsed.type + '\\n当前支持小说 \/ 漫画(c:)(可用 g: 共享)')\n getApiUrl('\/novel\/search', { keyword: '0', page: 1, share: true })\n} else {\n var apiType = parsed.mode === 'comic' ? 'comic' : 'novel'\n var params = {\n keyword: parsed.keyword || key,\n page: page,\n }\n if (parsed.share) params.share = true\n try {\n var rank = String(source.get('searchRank') || '').trim()\n if (rank === '0' || rank === '1' || rank === '2' || rank === '3') {\n params.rankType = rank\n }\n } catch (e2) { }\n getApiUrl('\/' + apiType + '\/search', params)\n}\n",
"variableComment": "JWT(由登录同步自动写入;也可手动粘贴)",
"weight": 0
}